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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ The DataHub REST API this SDK targets is a separate Spring Boot project; the HTT

This crate is a thin async HTTP SDK around a DataHub-style REST API. Entry point is `create_api_service()` in `src/lib.rs`, which returns an `Arc<ApiService>` built with `Arc::new_cyclic` so each subservice holds a `Weak<ApiService>` back-reference. Subservices are fields on `ApiService`:

- `time_series` (`src/timeseries/`) — `TimeSeries` + datapoint ingestion/retrieval
- `time_series` (`src/timeseries/`) — `TimeSeries` + datapoint ingestion/retrieval. Neither `TimeSeries` nor `TimeSeriesUpdate` has **`securityCategories`**: it was stored, writable and returned, but nothing ever read it — no part in access control (dataset grants are Keycloak organization groups), no query filtering on it, and the backend silently dropped any id that did not already exist, so the field never round-tripped. It has been removed server-side along with its join table, and the api reads request bodies strictly, so sending it is now a 400. Files keep their own `securityCategories` (`INode` in `src/generic.rs`) — separate entity, separate question. `ListFieldU64` went with it: it was the only field of that type, so the Python wrapper class is gone too (`ListFieldStr` and `ListFieldIdCollection` remain).
- `units` (`src/unit/`)
- `events` (`src/events/`) — event CRUD, filter/search, plus the vocabulary endpoints (`list_types`/`search_types` and the same pair for sub-types, statuses and sources, over `EventDimension`). Those answer "what values does this tenant actually use" for the four categorical fields and back filter dropdowns; they read small server-side dimension tables rather than scanning events, so they are cheap but *eventually consistent* with the events. Note the route asymmetry the SDK hides: `/events/list/{plural}` but `/events/search/{singular}`. `EventUpdate` has **no `event_time`**: an event's time is immutable after creation — the events table is partitioned by it, so ClickHouse refuses the mutation outright, and the api used to validate the field, echo the new value back with a 200 and then fail to apply it. It has been dropped from the update form, so sending it is now a 400. Record a corrected time as a new event.
- `resources` (`src/resources/`) — hierarchical asset-like entities; relationship edges live in `src/relations/` (`EdgeProxy`, `RelForm`, `RelatedNode`)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -299,13 +299,6 @@ class FieldGeoJson:

# An update is either a replace (`set`) or a delta (`add`/`remove`), never both. The two
# constructors make the illegal mix unrepresentable; there is no bare initializer.
class ListFieldU64:
@classmethod
def set(cls, values: list[int]) -> ListFieldU64: ...
@classmethod
def delta(cls, add: list[int] | None = None, remove: list[int] | None = None) -> ListFieldU64: ...


class ListFieldStr:
@classmethod
def set(cls, values: list[str]) -> ListFieldStr: ...
Expand Down Expand Up @@ -344,7 +337,6 @@ class TimeSeries:
unit_external_id: str | None = None,
description: str | None = None,
metadata: dict[str, str] | None = None,
security_categories: list[int] | None = None,
data_set_id: int | None = None,
id: int | None = None,
related_resources: list[RelatedNode] | None = None,
Expand Down Expand Up @@ -385,10 +377,6 @@ class TimeSeries:
@metadata.setter
def metadata(self, value: dict[str, str] | None) -> None: ...
@property
def security_categories(self) -> list[int] | None: ...
@security_categories.setter
def security_categories(self, value: list[int] | None) -> None: ...
@property
def data_set_id(self) -> int | None: ...
@data_set_id.setter
def data_set_id(self, value: int | None) -> None: ...
Expand Down Expand Up @@ -452,7 +440,6 @@ class TimeSeriesUpdate:
unit: FieldStr | None = None,
description: FieldStr | None = None,
unit_external_id: FieldStr | None = None,
security_categories: ListFieldU64 | None = None,
data_set_id: FieldU64 | None = None,
source: FieldStr | None = None,
) -> None: ...
Expand All @@ -473,8 +460,6 @@ class TimeSeriesUpdate:
@property
def unit_external_id(self) -> FieldStr: ...
@property
def security_categories(self) -> ListFieldU64: ...
@property
def data_set_id(self) -> FieldU64: ...
@property
def source(self) -> FieldStr: ...
Expand Down
4 changes: 2 additions & 2 deletions datahub_python_bindings/src/events/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -353,8 +353,8 @@ impl From<EventIdentifyable> for EventIdCollection {

/// One event's update for `events.update`. Target the event by an `Event`, its UUID `id`, or its
/// `external_id`; every field is optional and uses the same wrappers as the other services
/// (`FieldStr`/`FieldU64` for scalars, `ListFieldU64`/`ListFieldStr` for the related-resource
/// lists, `MapField` for metadata). Mirrors `ResourceUpdate`.
/// (`FieldStr`/`FieldU64` for scalars, `ListFieldIdCollection` for the related-resource list,
/// `MapField` for metadata). Mirrors `ResourceUpdate`.
#[pyclass(module = "intellistream_datahub_sdk", name = "EventUpdate")]
#[derive(Clone)]
pub struct PyEventUpdate {
Expand Down
28 changes: 0 additions & 28 deletions datahub_python_bindings/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -954,33 +954,6 @@ impl DatahubIdentity for Identifiable {
}
}

#[pyclass(module = "intellistream_datahub_sdk", name = "ListFieldU64")]
#[derive(Clone, Debug)]
pub struct PyListFieldU64(ListField<u64>);
impl From<ListField<u64>> for PyListFieldU64 {
fn from(ts: ListField<u64>) -> Self {
Self(ts)
}
}
impl From<PyListFieldU64> for ListField<u64> {
fn from(ts: PyListFieldU64) -> Self {
ts.0
}
}
#[pymethods]
impl PyListFieldU64 {
/// Replace the whole list.
#[classmethod]
pub fn set(_cls: Py<PyType>, values: Vec<u64>) -> Self {
Self(ListField::set(values))
}
/// Add and/or remove entries, keeping the rest. Pass `add`, `remove`, or both.
#[classmethod]
#[pyo3(signature=(add=None, remove=None))]
pub fn delta(_cls: Py<PyType>, add: Option<Vec<u64>>, remove: Option<Vec<u64>>) -> Self {
Self(ListField::delta(add, remove))
}
}
#[pyclass(module = "intellistream_datahub_sdk", name = "ListFieldStr")]
#[derive(Clone, Debug)]
pub struct PyListFieldStr(ListField<String>);
Expand Down Expand Up @@ -1241,7 +1214,6 @@ fn _core(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<PyLabelsServiceSync>()?;
m.add_class::<PyLabelsServiceAsync>()?;
m.add_class::<PyFieldU64>()?;
m.add_class::<PyListFieldU64>()?;
m.add_class::<PyFieldStr>()?;
m.add_class::<PyListFieldStr>()?;
m.add_class::<PyListFieldIdCollection>()?;
Expand Down
3 changes: 0 additions & 3 deletions datahub_python_bindings/src/timeseries/construction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ impl PyTimeSeries {
description = None,
unit = None,
unit_external_id = None,
security_categories = None,
data_set_id = None,
related_resources = None,
source = None
Expand All @@ -49,7 +48,6 @@ impl PyTimeSeries {
description: Option<String>,
unit: Option<String>,
unit_external_id: Option<String>,
security_categories: Option<Vec<u64>>,
data_set_id: Option<u64>,
related_resources: Option<Vec<PyRelatedNode>>,
source: Option<String>,
Expand All @@ -75,7 +73,6 @@ impl PyTimeSeries {
unit,
description,
unit_external_id,
security_categories,
data_set_id,
value_type: value_type.to_string(),
source,
Expand Down
8 changes: 0 additions & 8 deletions datahub_python_bindings/src/timeseries/general.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,6 @@ impl PyTimeSeries {
self.inner.unit_external_id = value;
}
#[getter]
pub fn security_categories(&self) -> Option<&Vec<u64>> {
self.inner.security_categories.as_ref()
}
#[setter]
pub fn set_security_categories(&mut self, value: Option<Vec<u64>>) {
self.inner.security_categories = value;
}
#[getter]
pub fn data_set_id(&self) -> Option<u64> {
self.inner.data_set_id
}
Expand Down
12 changes: 1 addition & 11 deletions datahub_python_bindings/src/timeseries/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crate::timeseries::datapoints::{
};
use crate::timeseries::sync_service::PyTimeSeriesServiceSync;
use crate::{
DatahubIdentity, Identifiable, PyFieldStr, PyFieldU64, PyIdCollection, PyListFieldU64,
DatahubIdentity, Identifiable, PyFieldStr, PyFieldU64, PyIdCollection,
PyMapField,
};
use chrono::{DateTime, Utc};
Expand Down Expand Up @@ -69,9 +69,6 @@ pub mod sync_service;
/// External id for the units of the timeseries this is used to connecto to datahub units system.
/// The units system will allow you to convert between units and easily convert between units-systems for unified storage.
///
/// security_categories: list[int], default None
/// Currenty not supported used.
///
/// data_set_id: int
/// the id of the datasets this timeseries belongs to
/// related_resources: list[RelatedNode]
Expand Down Expand Up @@ -256,7 +253,6 @@ impl PyTimeSeriesUpdate {
unit=None,
description=None,
unit_external_id=None,
security_categories=None,
data_set_id=None,
source=None,
))]
Expand All @@ -269,7 +265,6 @@ impl PyTimeSeriesUpdate {
unit: Option<PyFieldStr>,
description: Option<PyFieldStr>,
unit_external_id: Option<PyFieldStr>,
security_categories: Option<PyListFieldU64>,
data_set_id: Option<PyFieldU64>,
source: Option<PyFieldStr>,
) -> PyResult<Self> {
Expand All @@ -281,7 +276,6 @@ impl PyTimeSeriesUpdate {
unit: unit.map(|s| s.0).unwrap_or_default(),
description: description.map(|s| s.0).unwrap_or_default(),
unit_external_id: unit_external_id.map(|s| s.0).unwrap_or_default(),
security_categories: security_categories.map(|s| s.0).unwrap_or_default(),
data_set_id: data_set_id.map(|s| s.0).unwrap_or_default(),
source: source.map(|s| s.0).unwrap_or_default(),
};
Expand Down Expand Up @@ -326,10 +320,6 @@ impl PyTimeSeriesUpdate {
self.inner.update.unit_external_id.clone().into()
}
#[getter]
fn security_categories(&self) -> PyListFieldU64 {
self.inner.update.security_categories.clone().into()
}
#[getter]
fn data_set_id(&self) -> PyFieldU64 {
self.inner.update.data_set_id.clone().into()
}
Expand Down
7 changes: 0 additions & 7 deletions python_tests/test_entity_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ def test_minimal_constructor_and_defaults(self):
assert ts.unit_external_id is None
assert ts.description is None
assert ts.metadata is None
assert ts.security_categories is None
assert ts.data_set_id is None
assert ts.source is None

Expand All @@ -54,7 +53,6 @@ def test_full_constructor_round_trips_through_getters(self):
unit_external_id="u-ext",
description="a description",
metadata={"k": "v"},
security_categories=[1, 2],
data_set_id=99,
source="sap_pi",
)
Expand All @@ -65,7 +63,6 @@ def test_full_constructor_round_trips_through_getters(self):
assert ts.unit_external_id == "u-ext"
assert ts.description == "a description"
assert ts.metadata == {"k": "v"}
assert ts.security_categories == [1, 2]
assert ts.data_set_id == 99
assert ts.source == "sap_pi"

Expand Down Expand Up @@ -96,7 +93,6 @@ def test_setters_update_every_field(self):
ts.unit_external_id = "u-ext"
ts.description = "a description"
ts.metadata = {"k": "v"}
ts.security_categories = [1, 2, 3]
ts.data_set_id = 42
ts.source = "sap_pi"

Expand All @@ -106,7 +102,6 @@ def test_setters_update_every_field(self):
assert ts.unit_external_id == "u-ext"
assert ts.description == "a description"
assert ts.metadata == {"k": "v"}
assert ts.security_categories == [1, 2, 3]
assert ts.data_set_id == 42
assert ts.source == "sap_pi"

Expand All @@ -126,12 +121,10 @@ def test_optional_setters_accept_none(self):
ts.data_set_id = None
ts.description = None
ts.unit_external_id = None
ts.security_categories = None
ts.source = None
assert ts.unit is None
assert ts.metadata is None
assert ts.data_set_id is None
assert ts.security_categories is None
assert ts.source is None

def test_invalid_value_type_raises(self):
Expand Down
4 changes: 2 additions & 2 deletions python_tests/test_resource_label_updates.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import time

import pytest
from intellistream_datahub_sdk import ListFieldStr, ListFieldU64, MapField, Resource, ResourceUpdate
from intellistream_datahub_sdk import ListFieldIdCollection, ListFieldStr, MapField, Resource, ResourceUpdate

from fixtures import async_client, make_resource, sync_client, unique_id

Expand Down Expand Up @@ -47,7 +47,7 @@ def test_resource_update_requires_a_target():
def test_field_update_is_set_or_delta_only():
# `set` and `delta` are the only constructors; there is no bare initializer, so a replace can
# never be built carrying an add/remove delta — the illegal mix is simply not expressible.
for wrapper in (ListFieldStr, ListFieldU64, MapField):
for wrapper in (ListFieldStr, ListFieldIdCollection, MapField):
with pytest.raises(TypeError):
wrapper() # no __init__

Expand Down
68 changes: 0 additions & 68 deletions python_tests/test_timeseries_crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

* FieldStr / FieldU64 scalar fields -> set-value and set_null
* MapField (metadata) -> add (merge), set (replace), remove (by key)
* ListFieldU64 (security_categories) -> add, set (replace), remove (by value)
* value_type re-typing
* targeting an update by created-object / external-id string / numeric id
* multi-field updates, batch updates, and no-op updates
Expand Down Expand Up @@ -304,73 +303,6 @@ def test_update_metadata_cleared_by_an_empty_set(sync_client, make_ts):
assert not (sync_client.timeseries.update([update])[0].metadata or {})


# --------------------------------------------------------------------------- #
# UPDATE — ListFieldU64 (security_categories): add / set / remove
#
# These exercise the three ListFieldU64 serialisation paths (set/add/remove).
# They are xfail because the backend silently drops arbitrary security-category
# ids — verified by creating a series with security_categories=[1, 2] and getting
# back securityCategories=[]. Real categories aren't creatable through this SDK,
# so persistence can't be asserted; strict=False surfaces an xpass if the backend
# starts honouring them.
# --------------------------------------------------------------------------- #

_SEC_CAT_XFAIL = pytest.mark.xfail(
reason="backend does not persist arbitrary security-category ids "
"(needs pre-existing categories not creatable via this SDK)",
strict=False,
)


@_SEC_CAT_XFAIL
def test_update_security_categories_set(sync_client, make_ts):
ts = make_ts(security_categories=[1, 2])

update = intellistream_datahub_sdk.TimeSeriesUpdate(
ts, security_categories=intellistream_datahub_sdk.ListFieldU64.set([3, 4])
)
updated = sync_client.timeseries.update([update])[0]
assert sorted(updated.security_categories or []) == [3, 4]


@_SEC_CAT_XFAIL
def test_update_security_categories_add(sync_client, make_ts):
ts = make_ts(security_categories=[1, 2])

update = intellistream_datahub_sdk.TimeSeriesUpdate(
ts, security_categories=intellistream_datahub_sdk.ListFieldU64.delta(add=[3])
)
updated = sync_client.timeseries.update([update])[0]
assert set(updated.security_categories or []) >= {1, 2, 3}


@_SEC_CAT_XFAIL
def test_update_security_categories_remove(sync_client, make_ts):
ts = make_ts(security_categories=[1, 2, 3])

update = intellistream_datahub_sdk.TimeSeriesUpdate(
ts, security_categories=intellistream_datahub_sdk.ListFieldU64.delta(remove=[2])
)
updated = sync_client.timeseries.update([update])[0]
cats = set(updated.security_categories or [])
assert 2 not in cats
assert {1, 3} <= cats


def test_update_security_categories_cleared_by_an_empty_set(sync_client, make_ts):
"""``ListFieldU64`` has no ``setNull`` either; an empty ``set`` empties the list.

Not xfail: the backend drops arbitrary category ids on the way in, so the list is already
empty — clearing it is the one security-category assertion that holds either way.
"""
ts = make_ts(security_categories=[1, 2])

update = intellistream_datahub_sdk.TimeSeriesUpdate(
ts, security_categories=intellistream_datahub_sdk.ListFieldU64.set([])
)
assert not (sync_client.timeseries.update([update])[0].security_categories or [])


# --------------------------------------------------------------------------- #
# UPDATE — data_set_id (FieldU64): set value and clear
# --------------------------------------------------------------------------- #
Expand Down
Loading