Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 0 additions & 60 deletions python_tests/test_timeseries_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,63 +132,3 @@ async def test_insert_datapoints_missing_timeseries_returns_not_found(async_clie
), f"`missing` should name the absent series: {error.problem}"



"""
TODO! determine what is invalid input and codify in tests
Bellow are draft tests for invalid input

@pytest.mark.asyncio
@pytest.mark.parametrize("metadata", [{"vec": [0,1,2]},{"value_params": {"nested": {}}},{"nonstringable": print}])
async def test_reject_invalid_timeseries_metadata(async_client,metadata):
with pytest.raises(ValueError):
test_insert_ts = intellistream_datahub_sdk.TimeSeries(
name="valid name",
metadata=metadata,
)
await async_client.timeseries.delete([test_insert_ts])

@pytest.mark.asyncio
@pytest.mark.parametrize("name", ["valid name"])
async def test_reject_invalid_timeseries_name(async_client,name):
with pytest.raises(ValueError):
test_insert_ts = intellistream_datahub_sdk.TimeSeries(
name=name,
)
await async_client.timeseries.delete([test_insert_ts])

@pytest.mark.asyncio
@pytest.mark.parametrize("external_id", [1,12,"a","ab"])
async def test_reject_invalid_timeseries_metadata(async_client,external_id):
with pytest.raises(ValueError):
test_insert_ts = intellistream_datahub_sdk.TimeSeries(
name="valid name",
external_id=external_id,
)
await async_client.timeseries.delete([test_insert_ts])


@pytest.mark.asyncio
@pytest.mark.parametrize("value_type", ["",None,"big_int","strings","hex"])
async def test_reject_invalid_timeseries_metadata(async_client,value_type):
with pytest.raises(ValueError):
test_insert_ts = intellistream_datahub_sdk.TimeSeries(
name="valid name",
value_type=value_type,
)
await async_client.timeseries.delete([test_insert_ts])


@pytest.mark.asyncio
@pytest.mark.parametrize("units", ["",None,0],marks=pytest.mark.xfail(reason="TBD what are invalid units"))
@pytest.mark.parametrize("unit_external_id", ["",None,0],marks=pytest.mark.xfail(reason="TBD what are invalid units"))
async def test_reject_invalid_timeseries_unit(async_client,units,unit_external_id):
with pytest.raises(ValueError):
test_insert_ts = intellistream_datahub_sdk.TimeSeries(
name="valid name",
units=units,
unit_external_id=unit_external_id,
)
await async_client.timeseries.delete([test_insert_ts])


"""
130 changes: 130 additions & 0 deletions python_tests/test_timeseries_rejections.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
"""What ``timeseries.create`` refuses, and where.

Wrongly *typed* input never leaves Python: the binding declares ``str`` fields, so ``unit=0`` is a
``TypeError`` from the constructor, and ``value_type`` is checked against the SDK's catalogue there
too. Everything else is the api's call, answered with a 400 ``constraint-violation`` whose
``fields`` name each rejected property.

Two things the api accepts that a caller might expect it to refuse, and which are therefore not
pinned here: a ``unit_external_id`` that names no catalogue entry, and one that disagrees with
``unit`` (``unit="Celsius"`` on ``pressure_bar``). REST does not look the id up at all.
"""
import pytest

from intellistream_datahub_sdk import DataHubException, TimeSeries

from fixtures import async_client, make_ts, sync_client, unique_id # noqa: F401 (fixtures)

ABSENT = object()


def valid(**overrides):
kwargs = {
"external_id": unique_id("reject_ts"),
"name": "rejection probe",
"value_type": "float",
"unit": "a.u",
}
kwargs.update(overrides)
return {k: v for k, v in kwargs.items() if v is not ABSENT}


def rejected_fields(sync_client, ts):
"""Create ``ts`` expecting a 400, and return ``{field: code}`` from the problem."""
try:
with pytest.raises(DataHubException) as excinfo:
sync_client.timeseries.create([ts])
finally:
# Only reached with something to delete if the api regressed and accepted it.
sync_client.timeseries.delete([ts.external_id])
error = excinfo.value
assert error.status_code == 400, error.message
assert error.problem_slug == "constraint-violation", error.message
return {f["field"]: f["code"] for f in error.problem["fields"]}


# --------------------------------------------------------------------------- #
# refused by the constructor
# --------------------------------------------------------------------------- #

@pytest.mark.parametrize("overrides", [
{"unit": 0},
{"unit_external_id": 0},
{"external_id": 12},
{"name": 5},
{"value_type": None},
{"metadata": {"count": 1}},
{"metadata": {"vec": [0, 1, 2]}},
], ids=repr)
def test_a_wrongly_typed_field_is_a_type_error(overrides):
with pytest.raises(TypeError):
TimeSeries(**valid(**overrides))


@pytest.mark.parametrize("value_type", ["", "big_int", "strings", "hex"])
def test_an_unknown_value_type_is_a_value_error(value_type):
with pytest.raises(ValueError, match="ValueType"):
TimeSeries(**valid(value_type=value_type))


# --------------------------------------------------------------------------- #
# refused by the api
# --------------------------------------------------------------------------- #

@pytest.mark.parametrize("unit", [ABSENT, None, "", " "], ids=["absent", "None", "empty", "blank"])
def test_unit_is_required(sync_client, unit):
fields = rejected_fields(sync_client, TimeSeries(**valid(unit=unit)))
assert fields == {"unit": "timeseries.unit.not.blank"}


def test_a_unit_external_id_does_not_stand_in_for_unit(sync_client):
"""Unlike the MCP ``timeseries_create`` tool, REST does not fill ``unit`` in from the catalogue."""
fields = rejected_fields(
sync_client, TimeSeries(**valid(unit=ABSENT, unit_external_id="pressure_bar"))
)
assert fields == {"unit": "timeseries.unit.not.blank"}


def test_unit_is_at_most_64_characters(sync_client, make_ts):
assert make_ts(**valid(unit="x" * 64)).unit == "x" * 64

fields = rejected_fields(sync_client, TimeSeries(**valid(unit="x" * 65)))
assert set(fields) == {"unit"}


@pytest.mark.parametrize("unit_external_id", ["", "ab"])
def test_unit_external_id_is_at_least_3_characters(sync_client, unit_external_id):
fields = rejected_fields(
sync_client, TimeSeries(**valid(unit_external_id=unit_external_id))
)
assert set(fields) == {"unitExternalId"}


@pytest.mark.parametrize("name", ["", "ab", "x" * 513], ids=["empty", "2 chars", "513 chars"])
def test_name_is_3_to_512_characters(sync_client, name):
fields = rejected_fields(sync_client, TimeSeries(**valid(name=name)))
assert set(fields) == {"name"}


def test_external_id_is_at_least_3_characters(sync_client):
fields = rejected_fields(sync_client, TimeSeries(**valid(external_id="ab")))
assert set(fields) == {"externalId"}


def test_every_rejected_field_is_named_at_once(sync_client):
fields = rejected_fields(
sync_client, TimeSeries(**valid(name="", unit="", unit_external_id="ab"))
)
assert {"name", "unit", "unitExternalId"} <= set(fields)


@pytest.mark.asyncio
async def test_the_async_client_surfaces_the_same_rejection(async_client, sync_client):
ts = TimeSeries(**valid(unit=""))
try:
with pytest.raises(DataHubException) as excinfo:
await async_client.timeseries.create([ts])
finally:
sync_client.timeseries.delete([ts.external_id])
assert excinfo.value.status_code == 400
assert excinfo.value.problem_slug == "constraint-violation"
56 changes: 0 additions & 56 deletions python_tests/test_timeseries_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,62 +158,6 @@ def test_invalid_retrieve_latest_datapoint(sync_client):
nonexistant_ts = intellistream_datahub_sdk.TimeSeries(external_id="nonexistent_ts",value_type="bigint",unit="a.u")
sync_client.timeseries.retrieve_latest_datapoints(input=[nonexistant_ts])[0]

"""
TODO! determine what is invalid input and codify in tests
Bellow are draft tests for invalid input

@pytest.mark.parametrize("metadata", [{"vec": [0,1,2]},{"value_params": {"nested": {}}},{"nonstringable": print}])
def test_reject_invalid_timeseries_metadata(sync_client,metadata):
with pytest.raises(ValueError):
test_insert_ts = intellistream_datahub_sdk.TimeSeries(
name="valid name",
metadata=metadata,
)
sync_client.timeseries.delete([test_insert_ts])

@pytest.mark.parametrize("name", ["valid name"])
def test_reject_invalid_timeseries_name(sync_client,name):
with pytest.raises(ValueError):
test_insert_ts = intellistream_datahub_sdk.TimeSeries(
name=name,
)
sync_client.timeseries.delete([test_insert_ts])

@pytest.mark.parametrize("external_id", [1,12,"a","ab"])
def test_reject_invalid_timeseries_metadata(sync_client,external_id):
with pytest.raises(ValueError):
test_insert_ts = intellistream_datahub_sdk.TimeSeries(
name="valid name",
external_id=external_id,
)
sync_client.timeseries.delete([test_insert_ts])


@pytest.mark.parametrize("value_type", ["",None,"big_int","strings","hex"])
def test_reject_invalid_timeseries_metadata(sync_client,value_type):
with pytest.raises(ValueError):
test_insert_ts = intellistream_datahub_sdk.TimeSeries(
name="valid name",
value_type=value_type,
)
sync_client.timeseries.delete([test_insert_ts])


@pytest.mark.parametrize("units", ["",None,0],marks=pytest.mark.xfail(reason="TBD what are invalid units"))
@pytest.mark.parametrize("unit_external_id", ["",None,0],marks=pytest.mark.xfail(reason="TBD what are invalid units"))
def test_reject_invalid_timeseries_unit(sync_client,units,unit_external_id):
with pytest.raises(ValueError):
test_insert_ts = intellistream_datahub_sdk.TimeSeries(
name="valid name",
units=units,
unit_external_id=unit_external_id,
)
sync_client.timeseries.delete([test_insert_ts])


"""



def test_timeseries_update_with_fields(sync_client, make_ts):
# 1. Create a new unique TS for updating (make_ts deletes it at teardown)
Expand Down
Loading