From 6e1eb3b1db47b1b5445a4ca44d00481da044ec52 Mon Sep 17 00:00:00 2001 From: jgjesdal Date: Thu, 13 Aug 2026 11:12:27 +0200 Subject: [PATCH 01/20] Rename the package to intellistream-datahub-sdk and add publishing metadata MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Python package was called `datahub-sdk`, which is already taken on PyPI by an unrelated project ("Macaca DataHub Client"). The bare `datahub` name there is also a placeholder for `acryl-datahub`, LinkedIn's metadata platform, so anything in that neighbourhood invites confusion with a much better-known project. Qualifying with the organisation settles both, and the Rust crate takes the same name so the two ecosystems agree on what this library is called. The import names follow: `intellistream_datahub_sdk` in both languages. Consumers who find that long can alias it — `use intellistream_datahub_sdk as dh`, or a rename in Cargo.toml that applies crate-wide. The README shows both. The compiled extension module is now `intellistream_datahub_sdk._core` rather than repeating the package name. That is maturin's usual mixed-layout convention, and here it is also forced: `#[pymodule] fn` expands to a Rust module of the same name, so a package-named one shadows the `intellistream_datahub_sdk` crate the bindings import. Neither manifest carried what its registry requires to publish. Cargo.toml gains description, license, repository, readme, keywords and categories, and the PyPI side gains a license, project URLs, an author and a README to serve as its landing page. `exclude` keeps the bindings and `resources/` out of the published crate. The latter matters: it holds a ~93 MB test fixture that put the package at 46.8 MB, well over the crates.io 10 MiB limit. Excluding it brings the package to 197 KiB. Verified: cargo build, cargo test --no-run, a wheel and an sdist built, and all 509 Python tests collecting against the compiled module. --- .gitignore | 2 +- AGENTS.md | 4 +- Cargo.toml | 16 +- README.md | 32 ++- datahub_python_bindings/Cargo.toml | 2 +- datahub_python_bindings/LICENSE | 201 ++++++++++++++++++ datahub_python_bindings/README.md | 67 ++++++ datahub_python_bindings/pyproject.toml | 21 +- .../python/datahub_sdk/__init__.py | 2 - .../intellistream_datahub_sdk/__init__.py | 2 + .../__init__.pyi | 4 +- .../py.typed | 0 .../src/datasets/async_service.rs | 8 +- datahub_python_bindings/src/datasets/mod.rs | 20 +- .../src/datasets/sync_service.rs | 8 +- .../src/events/async_service.rs | 8 +- datahub_python_bindings/src/events/general.rs | 4 +- datahub_python_bindings/src/events/mod.rs | 24 +-- .../src/events/sync_service.rs | 10 +- .../src/files/async_service.rs | 10 +- datahub_python_bindings/src/files/mod.rs | 14 +- .../src/files/sync_service.rs | 10 +- .../src/functions/async_service.rs | 8 +- datahub_python_bindings/src/functions/mod.rs | 12 +- .../src/functions/sync_service.rs | 8 +- .../src/labels/async_service.rs | 8 +- datahub_python_bindings/src/labels/mod.rs | 10 +- .../src/labels/sync_service.rs | 8 +- datahub_python_bindings/src/lib.rs | 50 ++--- .../src/relations/async_service.rs | 8 +- datahub_python_bindings/src/relations/mod.rs | 20 +- .../src/relations/sync_service.rs | 8 +- .../src/resources/async_service.rs | 12 +- datahub_python_bindings/src/resources/mod.rs | 26 +-- .../src/resources/sync_service.rs | 14 +- .../src/subscriptions/async_service.rs | 8 +- .../src/subscriptions/general.rs | 4 +- .../src/subscriptions/listener.rs | 6 +- .../src/subscriptions/mod.rs | 24 +-- .../src/subscriptions/sync_service.rs | 8 +- .../src/timeseries/async_service.rs | 6 +- .../src/timeseries/construction.rs | 8 +- .../src/timeseries/datapoints.rs | 12 +- datahub_python_bindings/src/timeseries/mod.rs | 18 +- .../src/timeseries/sync_service.rs | 6 +- .../src/units/async_service.rs | 6 +- datahub_python_bindings/src/units/mod.rs | 4 +- .../src/units/sync_service.rs | 6 +- python_tests/README.md | 10 +- python_tests/conftest.py | 8 +- python_tests/filter_fixtures.py | 28 +-- python_tests/fixtures.py | 18 +- python_tests/test_auth_constructor.py | 6 +- python_tests/test_buffering.py | 16 +- python_tests/test_datasets.py | 46 ++-- python_tests/test_edges.py | 34 +-- python_tests/test_entity_fields.py | 4 +- python_tests/test_event_vocabulary.py | 20 +- python_tests/test_events.py | 104 ++++----- python_tests/test_files.py | 22 +- python_tests/test_filter_datasets.py | 22 +- python_tests/test_filter_events.py | 86 ++++---- python_tests/test_filter_paging.py | 36 ++-- python_tests/test_filter_resources.py | 6 +- python_tests/test_filter_search_bodies.py | 70 +++--- python_tests/test_filter_sorting.py | 32 +-- python_tests/test_filter_timeseries.py | 40 ++-- python_tests/test_functions.py | 4 +- python_tests/test_labels.py | 2 +- python_tests/test_multi_tenant_acl.py | 28 +-- python_tests/test_navigation.py | 30 +-- python_tests/test_resource_label_updates.py | 2 +- python_tests/test_resources.py | 6 +- python_tests/test_resources_async.py | 6 +- python_tests/test_subscriptions.py | 38 ++-- python_tests/test_timeseries_async.py | 36 ++-- python_tests/test_timeseries_crud.py | 104 ++++----- python_tests/test_timeseries_datapoints.py | 26 +-- python_tests/test_timeseries_search.py | 8 +- python_tests/test_timeseries_sync.py | 50 ++--- python_tests/test_timezones.py | 40 ++-- python_tests/test_units.py | 2 +- python_tests/test_update_datasets.py | 98 ++++----- python_tests/test_update_events.py | 138 ++++++------ python_tests/test_update_resources.py | 98 ++++----- run_python_tests.sh | 4 +- src/blocking.rs | 2 +- src/datasets/mod.rs | 4 +- src/events/mod.rs | 4 +- src/filters.rs | 2 +- src/lib.rs | 4 +- src/multi_tenant_integration.rs | 2 +- 92 files changed, 1218 insertions(+), 905 deletions(-) create mode 100644 datahub_python_bindings/LICENSE create mode 100644 datahub_python_bindings/README.md delete mode 100644 datahub_python_bindings/python/datahub_sdk/__init__.py create mode 100644 datahub_python_bindings/python/intellistream_datahub_sdk/__init__.py rename datahub_python_bindings/python/{datahub_sdk => intellistream_datahub_sdk}/__init__.pyi (99%) rename datahub_python_bindings/python/{datahub_sdk => intellistream_datahub_sdk}/py.typed (100%) diff --git a/.gitignore b/.gitignore index a87da95..63c78bc 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ Cargo.lock .env CLAUDE.local.md /datahub_python_bindings/target -/datahub_python_bindings/python/datahub_sdk/datahub_sdk.abi3.so +/datahub_python_bindings/python/intellistream_datahub_sdk/_core.abi3.so *.py[cod] # Python virtualenv and caches diff --git a/AGENTS.md b/AGENTS.md index a01bfad..b21901b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -153,9 +153,9 @@ All four searches declare a `filter` of their own entity's type (`SearchAndFilte ## Python bindings (`datahub_python_bindings/`) -A PyO3 crate (built with maturin) that wraps this SDK as the Python package `datahub-sdk` (import name `datahub_sdk`). Binding modules in `datahub_python_bindings/src/` mirror the Rust subservices; the pure-Python side lives in `datahub_python_bindings/python/datahub_sdk`. The platform's `datahub-ml` worker consumes this package, so binding-visible API changes ripple there. +A PyO3 crate (built with maturin) that wraps this SDK as the Python package `intellistream-datahub-sdk` (import name `intellistream_datahub_sdk`). Binding modules in `datahub_python_bindings/src/` mirror the Rust subservices; the pure-Python side lives in `datahub_python_bindings/python/intellistream_datahub_sdk`. The platform's `datahub-ml` worker consumes this package, so binding-visible API changes ripple there. -The Python test suite in `python_tests/` imports the **compiled** `datahub_sdk` module, not the Rust sources — a stale `.so` silently masks source changes. Always run it through `./run_python_tests.sh`, which rebuilds via `maturin develop` first. Extra args are forwarded to pytest (`./run_python_tests.sh -k timeseries`); `--release`, `--no-build`, and `--no-deps` are consumed by the script itself. +The Python test suite in `python_tests/` imports the **compiled** `intellistream_datahub_sdk` module, not the Rust sources — a stale `.so` silently masks source changes. Always run it through `./run_python_tests.sh`, which rebuilds via `maturin develop` first. Extra args are forwarded to pytest (`./run_python_tests.sh -k timeseries`); `--release`, `--no-build`, and `--no-deps` are consumed by the script itself. ## Conventions diff --git a/Cargo.toml b/Cargo.toml index 5b97dbf..2f7aaff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,22 @@ [package] -name = "dataplatform-rust-sdk" +name = "intellistream-datahub-sdk" version = "0.1.0" edition = "2021" +description = "Async Rust SDK for the IntelliStream DataHub REST API: time series and datapoints, events, resources and relationship edges, files, datasets, labels and subscriptions." +license = "Apache-2.0" +repository = "https://github.com/IntelliStream-DataHub/dataplatform-rust-sdk" +readme = "README.md" +keywords = ["datahub", "sdk", "timeseries", "iot", "api-client"] +categories = ["api-bindings", "asynchronous"] +exclude = [ + "/datahub_python_bindings", + "/python_tests", + "/run_python_tests.sh", + "/resources", +] [features] -# Synchronous client (dataplatform_rust_sdk::blocking), same split as reqwest::blocking. +# Synchronous client (intellistream_datahub_sdk::blocking), same split as reqwest::blocking. blocking = [] [dependencies] diff --git a/README.md b/README.md index 718fb6d..99f7d9a 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,34 @@ Services are fields on the `ApiService` returned by `create_api_service()`: `time_series` (including datapoint ingestion), `units`, `events`, `resources`, `datasets`, `files`, `subscriptions` (including WebSocket listening), and `functions`. +## Installation + +```bash +cargo add intellistream-datahub-sdk +``` + +The crate name is a mouthful at every call site, so it is worth renaming in `Cargo.toml` — the +alias then applies across the whole crate, with no `use` line in any module: + +```toml +[dependencies] +dh = { package = "intellistream-datahub-sdk", version = "0.5" } +``` + +```rust +let api = dh::create_api_service(); +``` + +Per-file aliasing works too, if you prefer to keep the real name in the manifest: + +```rust +use intellistream_datahub_sdk as dh; +``` + ## Quick start ```rust -use dataplatform_rust_sdk::create_api_service; +use intellistream_datahub_sdk::create_api_service; #[tokio::main] async fn main() { @@ -30,7 +54,7 @@ on a runtime owned by the client, so behavior is identical. Do not use it from i async context; use the async `ApiService` there. ```rust -use dataplatform_rust_sdk::blocking; +use intellistream_datahub_sdk::blocking; let api = blocking::create_api_service(); let series = api.time_series.search_by_name("engine").unwrap(); @@ -95,8 +119,8 @@ on read. ## Python bindings -`datahub_python_bindings/` wraps this SDK as the Python package `datahub-sdk` (import name -`datahub_sdk`) using PyO3 and maturin. The Python test suite in `python_tests/` runs against +`datahub_python_bindings/` wraps this SDK as the Python package `intellistream-datahub-sdk` (import name +`intellistream_datahub_sdk`) using PyO3 and maturin. The Python test suite in `python_tests/` runs against the **compiled** module — always use the wrapper script, which rebuilds the bindings first: ```bash diff --git a/datahub_python_bindings/Cargo.toml b/datahub_python_bindings/Cargo.toml index 7ce59b5..78d83c4 100644 --- a/datahub_python_bindings/Cargo.toml +++ b/datahub_python_bindings/Cargo.toml @@ -9,7 +9,7 @@ crate-type = ["cdylib"] [dependencies] pyo3 = { version = "0.29", features = ["extension-module", "chrono","chrono-tz","uuid","experimental-inspect", "abi3-py39","multiple-pymethods"] } -dataplatform-rust-sdk = { path = ".." } +intellistream-datahub-sdk = { path = ".." } tokio = { version = "1", features = ["full"] } chrono = { version = "0.4", features = ["serde"] } serde = { version = "1", features = ["derive"] } diff --git a/datahub_python_bindings/LICENSE b/datahub_python_bindings/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/datahub_python_bindings/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/datahub_python_bindings/README.md b/datahub_python_bindings/README.md new file mode 100644 index 0000000..72bb887 --- /dev/null +++ b/datahub_python_bindings/README.md @@ -0,0 +1,67 @@ +# intellistream-datahub-sdk + +Python bindings for the IntelliStream DataHub SDK — a client for the DataHub REST API, implemented +in Rust and exposed to Python with [PyO3](https://pyo3.rs). + +```bash +pip install intellistream-datahub-sdk +``` + +```python +from datetime import datetime, timezone + +import intellistream_datahub_sdk as dh + +client = dh.DataHubClient.from_envfile(".env") + +# Create a time series, then ingest a datapoint into it. +client.timeseries.create([dh.TimeSeries(external_id="sensor_1", name="Sensor 1")]) +client.timeseries.insert_from_lists( + timestamps=[datetime.now(timezone.utc)], + values=[21.5], + ts="sensor_1", +) + +# Find the alarms. +alarms = client.events.filter( + dh.EventFilter(basic_filter=dh.BasicEventFilter(type="ALARM"), limit=100) +) +``` + +Both a synchronous and an asynchronous client are available — `DataHubClient` and +`AsyncDataHubClient`. The async one exposes the same services with awaitable methods. + +Anywhere an entity is identified you may pass its external id, its numeric id, an `IdCollection`, +or the entity itself. + +## What it covers + +| Service | Highlights | +|---|---| +| `timeseries` | time-series CRUD, datapoint ingestion and retrieval | +| `events` | event CRUD, filter/search, and the type/sub-type/status/source vocabularies | +| `resources` | hierarchical assets, plus relationship `edges` between them | +| `files` | upload, download, directory listing, move/rename, trash and restore | +| `datasets` | dataset CRUD and search | +| `labels` | label CRUD | +| `subscriptions` | subscription CRUD and WebSocket listening | +| `functions` | function registration and lookup | +| `units` | unit lookup | + +## Configuration + +The client reads its configuration from a `.env` file or the environment: + +- `BASE_URL` — the backend root, e.g. `https://datahub.example.com` +- either `TOKEN` — a bearer token used as-is +- or `CLIENT_ID`, `CLIENT_SECRET` and `TOKEN_URI` — an OAuth2 client-credentials exchange + +## Type hints + +The package ships `py.typed` and a full `.pyi` stub, so mypy and IDE completion work out of the box. + +## License + +Apache License 2.0 — see [LICENSE](LICENSE). + +Source: diff --git a/datahub_python_bindings/pyproject.toml b/datahub_python_bindings/pyproject.toml index 01fb7ce..5386cb5 100644 --- a/datahub_python_bindings/pyproject.toml +++ b/datahub_python_bindings/pyproject.toml @@ -4,19 +4,28 @@ build-backend = "maturin" [tool.maturin] bindings = "pyo3" -# This tells maturin that the python package is in the 'python' folder python-source = "python" -# The name of the compiled rust module (must match lib.name in Cargo.toml) -module-name = "datahub_sdk" +module-name = "intellistream_datahub_sdk._core" +exclude = ["**/__pycache__/**"] [project] -name = "datahub-sdk" +name = "intellistream-datahub-sdk" version = "0.1.0" -description = "Python bindings for the DataHub Rust SDK" +description = "Python bindings for the IntelliStream DataHub SDK: time series and datapoints, events, resources and relationship edges, files, datasets, labels and subscriptions." +readme = "README.md" +authors = [{ name = "IntelliStream AS", email = "support@intellistream.ai" }] +license = "Apache-2.0" requires-python = ">=3.9" classifiers = [ "Programming Language :: Python :: 3", "Programming Language :: Rust", "Operating System :: OS Independent", -] \ No newline at end of file + "Intended Audience :: Developers", + "Topic :: Database", + "Typing :: Typed", +] + +[project.urls] +Repository = "https://github.com/IntelliStream-DataHub/dataplatform-rust-sdk" +Issues = "https://github.com/IntelliStream-DataHub/dataplatform-rust-sdk/issues" diff --git a/datahub_python_bindings/python/datahub_sdk/__init__.py b/datahub_python_bindings/python/datahub_sdk/__init__.py deleted file mode 100644 index 78df933..0000000 --- a/datahub_python_bindings/python/datahub_sdk/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ - -from .datahub_sdk import * \ No newline at end of file diff --git a/datahub_python_bindings/python/intellistream_datahub_sdk/__init__.py b/datahub_python_bindings/python/intellistream_datahub_sdk/__init__.py new file mode 100644 index 0000000..93e3284 --- /dev/null +++ b/datahub_python_bindings/python/intellistream_datahub_sdk/__init__.py @@ -0,0 +1,2 @@ + +from ._core import * diff --git a/datahub_python_bindings/python/datahub_sdk/__init__.pyi b/datahub_python_bindings/python/intellistream_datahub_sdk/__init__.pyi similarity index 99% rename from datahub_python_bindings/python/datahub_sdk/__init__.pyi rename to datahub_python_bindings/python/intellistream_datahub_sdk/__init__.pyi index e98e0cb..25d1ba4 100644 --- a/datahub_python_bindings/python/datahub_sdk/__init__.pyi +++ b/datahub_python_bindings/python/intellistream_datahub_sdk/__init__.pyi @@ -1,8 +1,8 @@ -"""Type stubs for the datahub_sdk pyo3 extension module. +"""Type stubs for the intellistream_datahub_sdk pyo3 extension module. The runtime is a single flat module: every class is exported at the top level. This stub matches that structure; do not introduce submodules unless the Rust -registration in src/lib.rs::datahub_sdk() also adds them. +registration in src/lib.rs::intellistream_datahub_sdk() also adds them. """ from __future__ import annotations diff --git a/datahub_python_bindings/python/datahub_sdk/py.typed b/datahub_python_bindings/python/intellistream_datahub_sdk/py.typed similarity index 100% rename from datahub_python_bindings/python/datahub_sdk/py.typed rename to datahub_python_bindings/python/intellistream_datahub_sdk/py.typed diff --git a/datahub_python_bindings/src/datasets/async_service.rs b/datahub_python_bindings/src/datasets/async_service.rs index bd4b903..5dd753c 100644 --- a/datahub_python_bindings/src/datasets/async_service.rs +++ b/datahub_python_bindings/src/datasets/async_service.rs @@ -3,14 +3,14 @@ use crate::datasets::{ }; use crate::resources::PyResource; use crate::{DatahubIdentity, Identifiable, PyIdCollection, PySearchAndFilterForm}; -use dataplatform_rust_sdk::ApiService; -use dataplatform_rust_sdk::datasets::{Dataset, DatasetUpdate}; -use dataplatform_rust_sdk::generic::{DataWrapper, IdAndExtId}; +use intellistream_datahub_sdk::ApiService; +use intellistream_datahub_sdk::datasets::{Dataset, DatasetUpdate}; +use intellistream_datahub_sdk::generic::{DataWrapper, IdAndExtId}; use pyo3::{Bound, PyAny, PyResult, Python, pyclass, pymethods}; use pyo3_async_runtimes::tokio::future_into_py; use std::sync::Arc; -#[pyclass(module = "datahub_sdk", name = "DatasetsServiceAsync")] +#[pyclass(module = "intellistream_datahub_sdk", name = "DatasetsServiceAsync")] pub struct PyDatasetsServiceAsync { pub api_service: Arc, } diff --git a/datahub_python_bindings/src/datasets/mod.rs b/datahub_python_bindings/src/datasets/mod.rs index 7d2a0bf..0538cfb 100644 --- a/datahub_python_bindings/src/datasets/mod.rs +++ b/datahub_python_bindings/src/datasets/mod.rs @@ -6,14 +6,14 @@ use crate::PyIdCollection; use crate::events::{PyEvent, PyTimeFilter}; use crate::resources::PyResourceNetwork; use crate::{PyFieldBool, PyFieldStr, PyListFieldStr, PyMapField}; -use dataplatform_rust_sdk::filters::{BasicEventFilter, EventFilter}; -use dataplatform_rust_sdk::datahub::to_snake_lower_cased_allow_start_with_digits; -use dataplatform_rust_sdk::datasets::{ +use intellistream_datahub_sdk::filters::{BasicEventFilter, EventFilter}; +use intellistream_datahub_sdk::datahub::to_snake_lower_cased_allow_start_with_digits; +use intellistream_datahub_sdk::datasets::{ BasicDatasetFilter, Dataset, DatasetFilter, DatasetSearch, DatasetUpdate, DatasetUpdateFields, }; -use dataplatform_rust_sdk::generic::IdAndExtId; -use dataplatform_rust_sdk::resources::RelatedResourcesForm; -use dataplatform_rust_sdk::ApiService; +use intellistream_datahub_sdk::generic::IdAndExtId; +use intellistream_datahub_sdk::resources::RelatedResourcesForm; +use intellistream_datahub_sdk::ApiService; use pyo3::prelude::*; use pyo3::{Bound, PyResult, pyclass, pymethods}; use pyo3_async_runtimes::tokio::future_into_py; @@ -21,7 +21,7 @@ use std::collections::HashMap; use std::sync::Arc; use uuid::Uuid; -#[pyclass(module = "datahub_sdk", name = "Dataset", from_py_object)] +#[pyclass(module = "intellistream_datahub_sdk", name = "Dataset", from_py_object)] #[derive(Clone)] pub struct PyDataset { pub inner: Dataset, @@ -362,7 +362,7 @@ impl PyDataset { /// /// An **empty** list or dict is also no restriction rather than "match nothing" — the backend /// reads a list it was handed with nothing in it as "I had no ids to filter on". -#[pyclass(module = "datahub_sdk", name = "BasicDatasetFilter", from_py_object)] +#[pyclass(module = "intellistream_datahub_sdk", name = "BasicDatasetFilter", from_py_object)] #[derive(Clone)] pub struct PyBasicDatasetFilter { pub inner: BasicDatasetFilter, @@ -452,7 +452,7 @@ impl PyBasicDatasetFilter { /// `limit` defaults to the server's 100 and may not exceed 10000 — above that the request is /// rejected. There is no paging, so a filter broad enough to exceed the cap is truncated; /// narrow it rather than trying to page. -#[pyclass(module = "datahub_sdk", name = "DatasetFilter", from_py_object)] +#[pyclass(module = "intellistream_datahub_sdk", name = "DatasetFilter", from_py_object)] #[derive(Clone)] pub struct PyDatasetFilter { pub inner: DatasetFilter, @@ -512,7 +512,7 @@ impl PyDatasetFilter { /// /// There is deliberately no `policies` or `connected_data_sets` here: the update endpoint does not /// accept them, whatever a `Dataset` can carry on create. -#[pyclass(module = "datahub_sdk", name = "DatasetUpdate", from_py_object)] +#[pyclass(module = "intellistream_datahub_sdk", name = "DatasetUpdate", from_py_object)] #[derive(Clone)] pub struct PyDatasetUpdate { pub inner: DatasetUpdate, diff --git a/datahub_python_bindings/src/datasets/sync_service.rs b/datahub_python_bindings/src/datasets/sync_service.rs index 49b9d91..28c7bc9 100644 --- a/datahub_python_bindings/src/datasets/sync_service.rs +++ b/datahub_python_bindings/src/datasets/sync_service.rs @@ -3,13 +3,13 @@ use crate::datasets::{ }; use crate::resources::PyResource; use crate::{PyIdCollection, PySearchAndFilterForm}; -use dataplatform_rust_sdk::datasets::{Dataset, DatasetUpdate}; -use dataplatform_rust_sdk::generic::IdAndExtId; -use dataplatform_rust_sdk::{ApiService, Resource}; +use intellistream_datahub_sdk::datasets::{Dataset, DatasetUpdate}; +use intellistream_datahub_sdk::generic::IdAndExtId; +use intellistream_datahub_sdk::{ApiService, Resource}; use pyo3::{Bound, PyAny, PyResult, Python, pyclass, pymethods}; use std::sync::Arc; -#[pyclass(module = "datahub_sdk", name = "DatasetsServiceSync")] +#[pyclass(module = "intellistream_datahub_sdk", name = "DatasetsServiceSync")] pub struct PyDatasetsServiceSync { pub api_service: Arc, pub runtime: Arc, diff --git a/datahub_python_bindings/src/events/async_service.rs b/datahub_python_bindings/src/events/async_service.rs index 21f906c..0cc8bfb 100644 --- a/datahub_python_bindings/src/events/async_service.rs +++ b/datahub_python_bindings/src/events/async_service.rs @@ -4,9 +4,9 @@ use crate::events::{ use crate::timeseries::async_service::PyTimeSeriesServiceAsync; use crate::timeseries::{PyTimeSeries, PyTimeSeriesUpdate}; use crate::{PyIdCollection, PySearchAndFilterForm}; -use dataplatform_rust_sdk::events::{EventDimension, EventIdCollection, EventUpdate}; -use dataplatform_rust_sdk::generic::DataWrapper; -use dataplatform_rust_sdk::{ +use intellistream_datahub_sdk::events::{EventDimension, EventIdCollection, EventUpdate}; +use intellistream_datahub_sdk::generic::DataWrapper; +use intellistream_datahub_sdk::{ ApiService, Event, TimeSeries, TimeSeriesUpdate, TimeSeriesUpdateCollection, }; use pyo3::{Bound, PyAny, PyResult, Python, pyclass, pymethods}; @@ -14,7 +14,7 @@ use pyo3_async_runtimes::tokio::future_into_py; use std::sync::Arc; use uuid::Uuid; -#[pyclass(module = "datahub_sdk", name = "EventsServiceAsync")] +#[pyclass(module = "intellistream_datahub_sdk", name = "EventsServiceAsync")] pub struct PyEventsServiceAsync { pub api_service: Arc, } diff --git a/datahub_python_bindings/src/events/general.rs b/datahub_python_bindings/src/events/general.rs index 4d4f739..1b13c0c 100644 --- a/datahub_python_bindings/src/events/general.rs +++ b/datahub_python_bindings/src/events/general.rs @@ -3,7 +3,7 @@ use crate::PyIdCollection; use crate::datetime::py_datetime_to_utc; use crate::resources::PyResource; use chrono::{DateTime, Utc}; -use dataplatform_rust_sdk::generic::IdAndExtId; +use intellistream_datahub_sdk::generic::IdAndExtId; use pyo3::prelude::*; use pyo3_async_runtimes::tokio::future_into_py; use std::collections::HashMap; @@ -36,7 +36,7 @@ impl PyEvent { related_resources: Option>, source: Option, ) -> PyResult { - let mut ev = dataplatform_rust_sdk::Event::new( + let mut ev = intellistream_datahub_sdk::Event::new( external_id, r#type, py_datetime_to_utc(&event_time)?, diff --git a/datahub_python_bindings/src/events/mod.rs b/datahub_python_bindings/src/events/mod.rs index 5855c10..e1bc847 100644 --- a/datahub_python_bindings/src/events/mod.rs +++ b/datahub_python_bindings/src/events/mod.rs @@ -6,12 +6,12 @@ use crate::timeseries::datapoints::{ }; use crate::timeseries::{PyDeleteFilter, PyTimeSeries, PyTimeSeriesUpdate}; use crate::{PyFieldStr, PyFieldU64, PyListFieldIdCollection, PyMapField}; -use dataplatform_rust_sdk::filters::{BasicEventFilter, DataSort, EventFilter, TimeFilter}; -use dataplatform_rust_sdk::events::{ +use intellistream_datahub_sdk::filters::{BasicEventFilter, DataSort, EventFilter, TimeFilter}; +use intellistream_datahub_sdk::events::{ EventDimension, EventIdCollection, EventSearch, EventUpdate, EventUpdateFields, }; -use dataplatform_rust_sdk::generic::IdAndExtId; -use dataplatform_rust_sdk::{ApiService, Event, TimeSeries}; +use intellistream_datahub_sdk::generic::IdAndExtId; +use intellistream_datahub_sdk::{ApiService, Event, TimeSeries}; use std::sync::Arc; use pyo3::exceptions::PyValueError; use pyo3::prelude::*; @@ -23,7 +23,7 @@ pub mod async_service; pub mod general; pub mod sync_service; -#[pyclass(module = "datahub_sdk", name = "Event", from_py_object)] +#[pyclass(module = "intellistream_datahub_sdk", name = "Event", from_py_object)] #[derive(Clone)] pub struct PyEvent { pub inner: Event, @@ -65,7 +65,7 @@ impl PyEvent { } } -#[pyclass(module = "datahub_sdk", name = "EventFilter", from_py_object)] +#[pyclass(module = "intellistream_datahub_sdk", name = "EventFilter", from_py_object)] #[derive(Clone)] pub struct PyEventFilter { pub inner: EventFilter, @@ -185,7 +185,7 @@ impl PyEventFilter { } #[pyclass( - module = "datahub_sdk", + module = "intellistream_datahub_sdk", name = "BasicEventFilter", from_py_object )] @@ -270,7 +270,7 @@ impl PyBasicEventFilter { } } } -#[pyclass(module = "datahub_sdk", name = "TimeFilter", from_py_object)] +#[pyclass(module = "intellistream_datahub_sdk", name = "TimeFilter", from_py_object)] #[derive(Clone)] pub struct PyTimeFilter { inner: TimeFilter, @@ -328,7 +328,7 @@ impl PyTimeFilter { /// carries the `id` (UUID) and/or the `external_id`. Construct with either or both: /// `EventIdCollection(id=my_uuid)` or `EventIdCollection(external_id="...")`. #[pyclass( - module = "datahub_sdk", + module = "intellistream_datahub_sdk", name = "EventIdCollection", from_py_object )] @@ -405,7 +405,7 @@ impl From for EventIdCollection { /// `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`. -#[pyclass(module = "datahub_sdk", name = "EventUpdate")] +#[pyclass(module = "intellistream_datahub_sdk", name = "EventUpdate")] #[derive(Clone)] pub struct PyEventUpdate { pub inner: EventUpdate, @@ -481,7 +481,7 @@ impl PyEventUpdate { /// Request body for `events.search`. `query` is the free-text phrase matched against event /// descriptions; `filter` optionally narrows the candidate set (same fields as `BasicEventFilter`), /// and `limit` caps the result (server max 1000). -#[pyclass(module = "datahub_sdk", name = "EventSearch")] +#[pyclass(module = "intellistream_datahub_sdk", name = "EventSearch")] #[derive(Clone)] pub struct PyEventSearch { pub inner: EventSearch, @@ -518,7 +518,7 @@ impl PyEventSearch { /// Served from dimension tables the write path maintains, not from a scan of the events — cheap /// enough for a typeahead, but *eventually consistent*: a new value appears once the write path /// records it, and a value no event carries any more lingers until the server's reconcile. -#[pyclass(module = "datahub_sdk", name = "EventDimension", eq, eq_int, frozen, hash)] +#[pyclass(module = "intellistream_datahub_sdk", name = "EventDimension", eq, eq_int, frozen, hash)] #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] pub enum PyEventDimension { TYPE, diff --git a/datahub_python_bindings/src/events/sync_service.rs b/datahub_python_bindings/src/events/sync_service.rs index 62ae572..22e76bc 100644 --- a/datahub_python_bindings/src/events/sync_service.rs +++ b/datahub_python_bindings/src/events/sync_service.rs @@ -2,10 +2,10 @@ use crate::events::{ EventIdentifyable, PyEvent, PyEventDimension, PyEventFilter, PyEventSearch, PyEventUpdate, }; use crate::{PyIdCollection, PySearchAndFilterForm}; -use dataplatform_rust_sdk::events::{EventDimension, EventIdCollection, EventUpdate}; -use dataplatform_rust_sdk::filters::EventFilter; -use dataplatform_rust_sdk::generic::DataWrapper; -use dataplatform_rust_sdk::{ +use intellistream_datahub_sdk::events::{EventDimension, EventIdCollection, EventUpdate}; +use intellistream_datahub_sdk::filters::EventFilter; +use intellistream_datahub_sdk::generic::DataWrapper; +use intellistream_datahub_sdk::{ ApiService, Event, TimeSeries, TimeSeriesUpdate, TimeSeriesUpdateCollection, }; use pyo3::{Bound, PyAny, PyResult, Python, pyclass, pymethods}; @@ -14,7 +14,7 @@ use std::sync::Arc; use tokio::runtime; use uuid::Uuid; -#[pyclass(module = "datahub_sdk", name = "EventsServiceSync")] +#[pyclass(module = "intellistream_datahub_sdk", name = "EventsServiceSync")] pub struct PyEventsServiceSync { pub api_service: Arc, pub runtime: Arc, diff --git a/datahub_python_bindings/src/files/async_service.rs b/datahub_python_bindings/src/files/async_service.rs index 9cc2a38..f5f2de3 100644 --- a/datahub_python_bindings/src/files/async_service.rs +++ b/datahub_python_bindings/src/files/async_service.rs @@ -1,12 +1,12 @@ use crate::files::{PyFileDownload, PyFileIdentifiable, PyFileUpdate, PyFileUpload, PyINode}; -use dataplatform_rust_sdk::files::FileUpdate; -use dataplatform_rust_sdk::generic::{DataWrapper, IdAndExtId}; -use dataplatform_rust_sdk::{ApiService, FileUpload}; +use intellistream_datahub_sdk::files::FileUpdate; +use intellistream_datahub_sdk::generic::{DataWrapper, IdAndExtId}; +use intellistream_datahub_sdk::{ApiService, FileUpload}; use pyo3::{Bound, PyAny, PyResult, Python, pyclass, pymethods}; use pyo3_async_runtimes::tokio::future_into_py; use std::sync::Arc; -#[pyclass(module = "datahub_sdk", name = "FilesServiceAsync")] +#[pyclass(module = "intellistream_datahub_sdk", name = "FilesServiceAsync")] pub struct PyFilesServiceAsync { pub api_service: Arc, } @@ -14,7 +14,7 @@ pub struct PyFilesServiceAsync { /// Wrap every returned node so it keeps a handle on the client it came from (enabling /// `related_resource_nodes` navigation). fn to_py_inodes( - result: &DataWrapper, + result: &DataWrapper, service: &Arc, ) -> Vec { result diff --git a/datahub_python_bindings/src/files/mod.rs b/datahub_python_bindings/src/files/mod.rs index 7346fa6..3e8f16b 100644 --- a/datahub_python_bindings/src/files/mod.rs +++ b/datahub_python_bindings/src/files/mod.rs @@ -4,9 +4,9 @@ pub mod sync_service; use crate::datetime::opt_py_datetime_to_utc; use crate::resources::PyResource; use chrono::{DateTime, Utc}; -use dataplatform_rust_sdk::files::{FileDownload, FileUpdate}; -use dataplatform_rust_sdk::generic::{INode, IdAndExtId}; -use dataplatform_rust_sdk::{ApiService, FileUpload}; +use intellistream_datahub_sdk::files::{FileDownload, FileUpdate}; +use intellistream_datahub_sdk::generic::{INode, IdAndExtId}; +use intellistream_datahub_sdk::{ApiService, FileUpload}; use pyo3::prelude::*; use pyo3::types::{PyBytes, PyType}; use pyo3_async_runtimes::tokio::future_into_py; @@ -14,7 +14,7 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::sync::Arc; -#[pyclass(module = "datahub_sdk", name = "INode", from_py_object)] +#[pyclass(module = "intellistream_datahub_sdk", name = "INode", from_py_object)] #[derive(Clone)] pub struct PyINode { inner: INode, @@ -262,7 +262,7 @@ impl PyINode { }) } } -#[pyclass(module = "datahub_sdk", name = "FileUpload", from_py_object)] +#[pyclass(module = "intellistream_datahub_sdk", name = "FileUpload", from_py_object)] #[derive(Clone, Debug)] pub struct PyFileUpload { pub inner: FileUpload, @@ -430,7 +430,7 @@ impl From for IdAndExtId { /// A partial update for one file or folder. Identify the node with `id` or `external_id`; every /// other argument is optional and only sent when given, so an omitted field is left unchanged. -#[pyclass(module = "datahub_sdk", name = "FileUpdate", from_py_object)] +#[pyclass(module = "intellistream_datahub_sdk", name = "FileUpdate", from_py_object)] #[derive(Clone, Debug)] pub struct PyFileUpdate { pub inner: FileUpdate, @@ -513,7 +513,7 @@ impl PyFileUpdate { } /// The result of a download: the file's bytes plus what the server said they are. -#[pyclass(module = "datahub_sdk", name = "FileDownload")] +#[pyclass(module = "intellistream_datahub_sdk", name = "FileDownload")] #[derive(Clone, Debug)] pub struct PyFileDownload { pub inner: FileDownload, diff --git a/datahub_python_bindings/src/files/sync_service.rs b/datahub_python_bindings/src/files/sync_service.rs index efd86d2..d4bdaf3 100644 --- a/datahub_python_bindings/src/files/sync_service.rs +++ b/datahub_python_bindings/src/files/sync_service.rs @@ -1,12 +1,12 @@ use crate::files::{PyFileDownload, PyFileIdentifiable, PyFileUpdate, PyFileUpload, PyINode}; -use dataplatform_rust_sdk::files::FileUpdate; -use dataplatform_rust_sdk::generic::{DataWrapper, IdAndExtId}; -use dataplatform_rust_sdk::{ApiService, FileUpload}; +use intellistream_datahub_sdk::files::FileUpdate; +use intellistream_datahub_sdk::generic::{DataWrapper, IdAndExtId}; +use intellistream_datahub_sdk::{ApiService, FileUpload}; use pyo3::{PyResult, Python, pyclass, pymethods}; use std::sync::Arc; -#[pyclass(module = "datahub_sdk", name = "FilesServiceSync")] +#[pyclass(module = "intellistream_datahub_sdk", name = "FilesServiceSync")] pub struct PyFilesServiceSync { pub api_service: Arc, pub runtime: Arc, @@ -15,7 +15,7 @@ pub struct PyFilesServiceSync { /// Wrap every returned node so it keeps a handle on the client it came from (enabling /// `related_resource_nodes` navigation). fn to_py_inodes( - result: &DataWrapper, + result: &DataWrapper, service: &Arc, ) -> Vec { result diff --git a/datahub_python_bindings/src/functions/async_service.rs b/datahub_python_bindings/src/functions/async_service.rs index 34275f0..4f3d7da 100644 --- a/datahub_python_bindings/src/functions/async_service.rs +++ b/datahub_python_bindings/src/functions/async_service.rs @@ -1,12 +1,12 @@ use crate::functions::{FunctionIdentifyable, PyFunction}; -use dataplatform_rust_sdk::ApiService; -use dataplatform_rust_sdk::functions::Function; -use dataplatform_rust_sdk::generic::IdAndExtId; +use intellistream_datahub_sdk::ApiService; +use intellistream_datahub_sdk::functions::Function; +use intellistream_datahub_sdk::generic::IdAndExtId; use pyo3::prelude::*; use pyo3_async_runtimes::tokio::future_into_py; use std::sync::Arc; -#[pyclass(module = "datahub_sdk", name = "FunctionsServiceAsync")] +#[pyclass(module = "intellistream_datahub_sdk", name = "FunctionsServiceAsync")] pub struct PyFunctionsServiceAsync { pub api_service: Arc, } diff --git a/datahub_python_bindings/src/functions/mod.rs b/datahub_python_bindings/src/functions/mod.rs index 7d37c5e..6f47bbd 100644 --- a/datahub_python_bindings/src/functions/mod.rs +++ b/datahub_python_bindings/src/functions/mod.rs @@ -2,12 +2,12 @@ use crate::PyIdCollection; use crate::events::PyEvent; use crate::relations::PyRelatedNode; use crate::resources::PyResourceNetwork; -use dataplatform_rust_sdk::filters::{BasicEventFilter, EventFilter}; +use intellistream_datahub_sdk::filters::{BasicEventFilter, EventFilter}; use chrono::{DateTime, Utc}; -use dataplatform_rust_sdk::functions::Function; -use dataplatform_rust_sdk::generic::IdAndExtId; -use dataplatform_rust_sdk::resources::RelatedResourcesForm; -use dataplatform_rust_sdk::ApiService; +use intellistream_datahub_sdk::functions::Function; +use intellistream_datahub_sdk::generic::IdAndExtId; +use intellistream_datahub_sdk::resources::RelatedResourcesForm; +use intellistream_datahub_sdk::ApiService; use pyo3::prelude::*; use pyo3::types::{PyBool, PyDict, PyFloat, PyInt, PyList, PyString}; use pyo3::{Bound, PyResult, pyclass, pymethods}; @@ -18,7 +18,7 @@ use std::sync::Arc; pub mod async_service; pub mod sync_service; -#[pyclass(module = "datahub_sdk", name = "Function")] +#[pyclass(module = "intellistream_datahub_sdk", name = "Function")] #[derive(Clone)] pub struct PyFunction { pub inner: Function, diff --git a/datahub_python_bindings/src/functions/sync_service.rs b/datahub_python_bindings/src/functions/sync_service.rs index 3921a53..50fb184 100644 --- a/datahub_python_bindings/src/functions/sync_service.rs +++ b/datahub_python_bindings/src/functions/sync_service.rs @@ -1,11 +1,11 @@ use crate::functions::{FunctionIdentifyable, PyFunction}; -use dataplatform_rust_sdk::ApiService; -use dataplatform_rust_sdk::functions::Function; -use dataplatform_rust_sdk::generic::IdAndExtId; +use intellistream_datahub_sdk::ApiService; +use intellistream_datahub_sdk::functions::Function; +use intellistream_datahub_sdk::generic::IdAndExtId; use pyo3::prelude::*; use std::sync::Arc; -#[pyclass(module = "datahub_sdk", name = "FunctionsServiceSync")] +#[pyclass(module = "intellistream_datahub_sdk", name = "FunctionsServiceSync")] pub struct PyFunctionsServiceSync { pub api_service: Arc, pub runtime: Arc, diff --git a/datahub_python_bindings/src/labels/async_service.rs b/datahub_python_bindings/src/labels/async_service.rs index 4cbb76d..3234e03 100644 --- a/datahub_python_bindings/src/labels/async_service.rs +++ b/datahub_python_bindings/src/labels/async_service.rs @@ -1,12 +1,12 @@ use crate::labels::{require_named, LabelIdentifiable, PyLabel}; -use dataplatform_rust_sdk::generic::IdAndExtId; -use dataplatform_rust_sdk::labels::Label; -use dataplatform_rust_sdk::ApiService; +use intellistream_datahub_sdk::generic::IdAndExtId; +use intellistream_datahub_sdk::labels::Label; +use intellistream_datahub_sdk::ApiService; use pyo3::{Bound, PyAny, PyResult, Python, pyclass, pymethods}; use pyo3_async_runtimes::tokio::future_into_py; use std::sync::Arc; -#[pyclass(module = "datahub_sdk", name = "LabelsServiceAsync")] +#[pyclass(module = "intellistream_datahub_sdk", name = "LabelsServiceAsync")] pub struct PyLabelsServiceAsync { pub api_service: Arc, } diff --git a/datahub_python_bindings/src/labels/mod.rs b/datahub_python_bindings/src/labels/mod.rs index 18f1c5e..1436477 100644 --- a/datahub_python_bindings/src/labels/mod.rs +++ b/datahub_python_bindings/src/labels/mod.rs @@ -1,15 +1,15 @@ pub(crate) mod async_service; pub(crate) mod sync_service; -use dataplatform_rust_sdk::generic::IdAndExtId; -use dataplatform_rust_sdk::labels::Label; +use intellistream_datahub_sdk::generic::IdAndExtId; +use intellistream_datahub_sdk::labels::Label; use pyo3::{pyclass, pymethods, FromPyObject, PyResult}; use pyo3::exceptions::PyValueError; /// A DataHub label (the CRUD entity behind `client.labels`). `name` is the identifier callers /// set; `id`/`color` are usually assigned by the server. Also the shape returned inside a /// `ResourceNetwork` from `resources.fetch_related` (where `color`/`i18n_code` are `None`). -#[pyclass(module = "datahub_sdk", name = "Label", from_py_object)] +#[pyclass(module = "intellistream_datahub_sdk", name = "Label", from_py_object)] #[derive(Clone)] pub struct PyLabel { pub inner: Label, @@ -28,8 +28,8 @@ impl From for Label { /// The graph-DTO label from a traversal carries only id/name/description; widen it to the unified /// `Label` so `ResourceNetwork.labels` and `client.labels` return one and the same Python class. -impl From for PyLabel { - fn from(l: dataplatform_rust_sdk::resources::Label) -> Self { +impl From for PyLabel { + fn from(l: intellistream_datahub_sdk::resources::Label) -> Self { PyLabel { inner: Label { id: l.id, diff --git a/datahub_python_bindings/src/labels/sync_service.rs b/datahub_python_bindings/src/labels/sync_service.rs index d4eabe1..9e08329 100644 --- a/datahub_python_bindings/src/labels/sync_service.rs +++ b/datahub_python_bindings/src/labels/sync_service.rs @@ -1,11 +1,11 @@ use crate::labels::{require_named, LabelIdentifiable, PyLabel}; -use dataplatform_rust_sdk::generic::IdAndExtId; -use dataplatform_rust_sdk::labels::Label; -use dataplatform_rust_sdk::ApiService; +use intellistream_datahub_sdk::generic::IdAndExtId; +use intellistream_datahub_sdk::labels::Label; +use intellistream_datahub_sdk::ApiService; use pyo3::{PyResult, Python, pyclass, pymethods}; use std::sync::Arc; -#[pyclass(module = "datahub_sdk", name = "LabelsServiceSync")] +#[pyclass(module = "intellistream_datahub_sdk", name = "LabelsServiceSync")] pub struct PyLabelsServiceSync { pub api_service: Arc, pub runtime: Arc, diff --git a/datahub_python_bindings/src/lib.rs b/datahub_python_bindings/src/lib.rs index 8b3aa2b..cffb59d 100644 --- a/datahub_python_bindings/src/lib.rs +++ b/datahub_python_bindings/src/lib.rs @@ -37,13 +37,13 @@ use crate::functions::async_service::PyFunctionsServiceAsync; use crate::functions::sync_service::PyFunctionsServiceSync; use crate::units::async_service::PyUnitServiceAsync; use crate::units::sync_service::PyUnitServiceSync; -use dataplatform_rust_sdk::ApiService; -use dataplatform_rust_sdk::datahub::DataHubConfig; -use dataplatform_rust_sdk::fields::{Field, ListField, MapField}; -use dataplatform_rust_sdk::generic::*; -use dataplatform_rust_sdk::http::ResponseError; -use dataplatform_rust_sdk::filters::NodeFilter; -use dataplatform_rust_sdk::{TimeSeriesFilter, TimeSeriesFilterForm}; +use intellistream_datahub_sdk::ApiService; +use intellistream_datahub_sdk::datahub::DataHubConfig; +use intellistream_datahub_sdk::fields::{Field, ListField, MapField}; +use intellistream_datahub_sdk::generic::*; +use intellistream_datahub_sdk::http::ResponseError; +use intellistream_datahub_sdk::filters::NodeFilter; +use intellistream_datahub_sdk::{TimeSeriesFilter, TimeSeriesFilterForm}; use pyo3::create_exception; use pyo3::exceptions::{PyException, PyValueError}; use pyo3::prelude::*; @@ -55,7 +55,7 @@ use std::sync::OnceLock; use units::*; create_exception!( - datahub_sdk, + intellistream_datahub_sdk, DataHubException, PyException, "Error returned by the DataHub API. Carries the HTTP `status_code` and the raw response `message`." @@ -186,7 +186,7 @@ fn build_buffered_config( config } -#[pyclass(module = "datahub_sdk", name = "DataHubClient")] +#[pyclass(module = "intellistream_datahub_sdk", name = "DataHubClient")] pub struct PySyncClient { inner: Arc, runtime: Arc, @@ -357,7 +357,7 @@ impl PySyncClient { } -#[pyclass(module = "datahub_sdk", name = "AsyncDataHubClient")] +#[pyclass(module = "intellistream_datahub_sdk", name = "AsyncDataHubClient")] struct PyAsyncClient { inner: Arc, } @@ -514,7 +514,7 @@ impl PyAsyncClient { } } -#[pyclass(module = "datahub_sdk", name = "IdCollection")] +#[pyclass(module = "intellistream_datahub_sdk", name = "IdCollection")] #[derive(Clone)] pub(crate) struct PyIdCollection { inner: IdAndExtId, @@ -579,7 +579,7 @@ impl PyIdCollection { /// /// It is not a `list` subclass, so `isinstance(page, list)` is `False`; use `page.items` for a real /// list when something demands one. -#[pyclass(module = "datahub_sdk", name = "Page", sequence)] +#[pyclass(module = "intellistream_datahub_sdk", name = "Page", sequence)] pub struct PyPage { items: Py, next_cursor: Option, @@ -662,8 +662,8 @@ pub(crate) fn build_page_request( sort_by: Option, sort_order: Option, cursor: Option, -) -> dataplatform_rust_sdk::filters::PageRequest { - use dataplatform_rust_sdk::filters::{DataSort, PageRequest}; +) -> intellistream_datahub_sdk::filters::PageRequest { + use intellistream_datahub_sdk::filters::{DataSort, PageRequest}; let sort = sort_by.map(|property| DataSort { property: property.into(), order: sort_order, @@ -728,7 +728,7 @@ pub(crate) fn opt_data_set_refs(value: Option>) -> Option); impl From> for PyListFieldU64 { @@ -950,7 +950,7 @@ impl PyListFieldU64 { Self(ListField::delta(add, remove)) } } -#[pyclass(module = "datahub_sdk", name = "ListFieldStr")] +#[pyclass(module = "intellistream_datahub_sdk", name = "ListFieldStr")] #[derive(Clone, Debug)] pub struct PyListFieldStr(ListField); impl From> for PyListFieldStr { @@ -980,7 +980,7 @@ impl PyListFieldStr { /// The related-resource list of an `EventUpdate`. Entries are `IdCollection`s, so a resource can /// be named by id, external_id, or both; `remove` matches on whichever side is given. -#[pyclass(module = "datahub_sdk", name = "ListFieldIdCollection")] +#[pyclass(module = "intellistream_datahub_sdk", name = "ListFieldIdCollection")] #[derive(Clone, Debug)] pub struct PyListFieldIdCollection(ListField); impl From> for PyListFieldIdCollection { @@ -1017,7 +1017,7 @@ impl PyListFieldIdCollection { } } -#[pyclass(module = "datahub_sdk", name = "MapField")] +#[pyclass(module = "intellistream_datahub_sdk", name = "MapField")] #[derive(Clone, Debug)] pub struct PyMapField(pub MapField); @@ -1049,7 +1049,7 @@ impl PyMapField { Self(MapField::delta(add, remove)) } } -#[pyclass(module = "datahub_sdk", name = "FieldStr")] +#[pyclass(module = "intellistream_datahub_sdk", name = "FieldStr")] #[derive(Clone, Debug)] pub struct PyFieldStr(Field); @@ -1081,7 +1081,7 @@ impl PyFieldStr { } } -#[pyclass(module = "datahub_sdk", name = "FieldU64")] +#[pyclass(module = "intellistream_datahub_sdk", name = "FieldU64")] #[derive(Clone, Debug)] pub struct PyFieldU64(Field); @@ -1113,7 +1113,7 @@ impl PyFieldU64 { } } -#[pyclass(module = "datahub_sdk", name = "FieldBool", from_py_object)] +#[pyclass(module = "intellistream_datahub_sdk", name = "FieldBool", from_py_object)] #[derive(Clone, Debug)] pub struct PyFieldBool(Field); @@ -1149,7 +1149,7 @@ impl PyFieldBool { /// a GeoJSON geometry `dict` (`{"type": "Point", "coordinates": [10.75, 59.91]}`) instead of a /// string. It is its own class for the same reason `FieldU64` is: the wrappers are per-payload /// type. -#[pyclass(module = "datahub_sdk", name = "FieldGeoJson", from_py_object)] +#[pyclass(module = "intellistream_datahub_sdk", name = "FieldGeoJson", from_py_object)] #[derive(Clone, Debug)] pub struct PyFieldGeoJson(Field); @@ -1195,7 +1195,7 @@ impl PyFieldGeoJson { // --- Resources --- #[pymodule] -fn datahub_sdk(m: &Bound<'_, PyModule>) -> PyResult<()> { +fn _core(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add("DataHubException", m.py().get_type::())?; m.add_class::()?; m.add_class::()?; diff --git a/datahub_python_bindings/src/relations/async_service.rs b/datahub_python_bindings/src/relations/async_service.rs index d7ced14..1220e30 100644 --- a/datahub_python_bindings/src/relations/async_service.rs +++ b/datahub_python_bindings/src/relations/async_service.rs @@ -1,14 +1,14 @@ use crate::relations::{ EdgeIdentifiable, PyEdgeProxy, PyGraphResult, PyRelForm, PyRelTypeForm, PyRelationshipType, }; -use dataplatform_rust_sdk::generic::IdAndExtId; -use dataplatform_rust_sdk::relations::{RelForm, RelTypeForm}; -use dataplatform_rust_sdk::ApiService; +use intellistream_datahub_sdk::generic::IdAndExtId; +use intellistream_datahub_sdk::relations::{RelForm, RelTypeForm}; +use intellistream_datahub_sdk::ApiService; use pyo3::{pyclass, pymethods, Bound, PyAny, PyResult, Python}; use pyo3_async_runtimes::tokio::future_into_py; use std::sync::Arc; -#[pyclass(module = "datahub_sdk", name = "EdgesServiceAsync")] +#[pyclass(module = "intellistream_datahub_sdk", name = "EdgesServiceAsync")] pub struct PyEdgesServiceAsync { pub api_service: Arc, } diff --git a/datahub_python_bindings/src/relations/mod.rs b/datahub_python_bindings/src/relations/mod.rs index 0de37c6..d6c3a40 100644 --- a/datahub_python_bindings/src/relations/mod.rs +++ b/datahub_python_bindings/src/relations/mod.rs @@ -2,12 +2,12 @@ pub(crate) mod async_service; pub(crate) mod sync_service; use crate::resources::PyResource; -use dataplatform_rust_sdk::graph_data_wrapper::GraphDataWrapper; -use dataplatform_rust_sdk::generic::IdAndExtId; -use dataplatform_rust_sdk::relations::{ +use intellistream_datahub_sdk::graph_data_wrapper::GraphDataWrapper; +use intellistream_datahub_sdk::generic::IdAndExtId; +use intellistream_datahub_sdk::relations::{ EdgeProxy, RelForm, RelTypeForm, RelatedNode, RelationDirection, RelationshipType, }; -use dataplatform_rust_sdk::{ApiService, Resource}; +use intellistream_datahub_sdk::{ApiService, Resource}; use pyo3::prelude::*; use pyo3::{Bound, PyResult, pyclass, pymethods}; use std::collections::HashMap; @@ -17,7 +17,7 @@ use std::sync::Arc; /// for tests and round-trip serde — in normal use these are returned by the server /// inside a `GraphResult`. The Python attribute is `relationship_type` even though /// the wire field is `"type"`. -#[pyclass(module = "datahub_sdk", name = "EdgeProxy")] +#[pyclass(module = "intellistream_datahub_sdk", name = "EdgeProxy")] #[derive(Clone)] pub struct PyEdgeProxy { pub inner: EdgeProxy, @@ -109,7 +109,7 @@ impl PyEdgeProxy { /// one is connected to, with `relationship_type` and (on read) `direction`/`edge_id`. /// Constructible from Python for the create path — pass `id` **or** `external_id` plus a /// `relationship_type`; `direction` and `edge_id` are server-assigned and read-only. -#[pyclass(module = "datahub_sdk", name = "RelatedNode")] +#[pyclass(module = "intellistream_datahub_sdk", name = "RelatedNode")] #[derive(Clone)] pub struct PyRelatedNode { pub inner: RelatedNode, @@ -209,7 +209,7 @@ impl PyRelatedNode { /// Request-side edge form mirroring server-side `RelForm`. Pair this with a list /// of `Resource` and pass both to `ResourcesService.create()`. `relationship_type` /// is keyword-required; the server snake-uppercases it. -#[pyclass(module = "datahub_sdk", name = "RelForm")] +#[pyclass(module = "intellistream_datahub_sdk", name = "RelForm")] #[derive(Clone)] pub struct PyRelForm { pub inner: RelForm, @@ -350,7 +350,7 @@ impl PyRelForm { /// Python view of `GraphDataWrapper`: the nodes and relations returned /// from a graph operation. `.nodes` is `list[Resource]`, `.relations` is `list[EdgeProxy]`. -#[pyclass(module = "datahub_sdk", name = "GraphResult")] +#[pyclass(module = "intellistream_datahub_sdk", name = "GraphResult")] #[derive(Clone)] pub struct PyGraphResult { pub nodes: Vec, @@ -392,7 +392,7 @@ impl PyGraphResult { /// /// Types are normally created on demand the first time a name is used; this is what one looks /// like once stored. -#[pyclass(module = "datahub_sdk", name = "RelationshipType")] +#[pyclass(module = "intellistream_datahub_sdk", name = "RelationshipType")] #[derive(Clone)] pub struct PyRelationshipType { pub inner: RelationshipType, @@ -435,7 +435,7 @@ impl PyRelationshipType { /// /// The server uppercase-snake-cases `name`, so `"Flows To"` and `"flows_to"` both land on /// `FLOWS_TO`. A name that normalises to nothing (only symbols) is rejected with status 400. -#[pyclass(module = "datahub_sdk", name = "RelTypeForm", from_py_object)] +#[pyclass(module = "intellistream_datahub_sdk", name = "RelTypeForm", from_py_object)] #[derive(Clone)] pub struct PyRelTypeForm { pub inner: RelTypeForm, diff --git a/datahub_python_bindings/src/relations/sync_service.rs b/datahub_python_bindings/src/relations/sync_service.rs index 7ba2348..6d59731 100644 --- a/datahub_python_bindings/src/relations/sync_service.rs +++ b/datahub_python_bindings/src/relations/sync_service.rs @@ -1,13 +1,13 @@ use crate::relations::{ EdgeIdentifiable, PyEdgeProxy, PyGraphResult, PyRelForm, PyRelTypeForm, PyRelationshipType, }; -use dataplatform_rust_sdk::generic::IdAndExtId; -use dataplatform_rust_sdk::relations::{RelForm, RelTypeForm}; -use dataplatform_rust_sdk::ApiService; +use intellistream_datahub_sdk::generic::IdAndExtId; +use intellistream_datahub_sdk::relations::{RelForm, RelTypeForm}; +use intellistream_datahub_sdk::ApiService; use pyo3::{pyclass, pymethods, PyResult, Python}; use std::sync::Arc; -#[pyclass(module = "datahub_sdk", name = "EdgesServiceSync")] +#[pyclass(module = "intellistream_datahub_sdk", name = "EdgesServiceSync")] pub struct PyEdgesServiceSync { pub api_service: Arc, pub runtime: Arc, diff --git a/datahub_python_bindings/src/resources/async_service.rs b/datahub_python_bindings/src/resources/async_service.rs index 3093fb7..3b1d9e7 100644 --- a/datahub_python_bindings/src/resources/async_service.rs +++ b/datahub_python_bindings/src/resources/async_service.rs @@ -1,17 +1,17 @@ use crate::relations::{PyGraphResult, PyRelForm}; use crate::resources::{PyResourceFilter, PyResourceNetwork, PyResourceUpdate, ResourceIdentifiable}; -use dataplatform_rust_sdk::resources::ResourceUpdate; +use intellistream_datahub_sdk::resources::ResourceUpdate; use crate::{DataSetRef, PyResource, PySearchAndFilterForm, StringOrList}; -use dataplatform_rust_sdk::generic::IdAndExtId; -use dataplatform_rust_sdk::relations::RelForm; -use dataplatform_rust_sdk::resources::RelatedResourcesForm; -use dataplatform_rust_sdk::{ApiService, Resource}; +use intellistream_datahub_sdk::generic::IdAndExtId; +use intellistream_datahub_sdk::relations::RelForm; +use intellistream_datahub_sdk::resources::RelatedResourcesForm; +use intellistream_datahub_sdk::{ApiService, Resource}; use pyo3::{Bound, PyAny, PyResult, Python, pyclass, pymethods}; use pyo3_async_runtimes::tokio::future_into_py; use std::collections::HashMap; use std::sync::Arc; -#[pyclass(module = "datahub_sdk", name = "ResourcesServiceAsync")] +#[pyclass(module = "intellistream_datahub_sdk", name = "ResourcesServiceAsync")] pub struct PyResourcesServiceAsync { pub api_service: Arc, } diff --git a/datahub_python_bindings/src/resources/mod.rs b/datahub_python_bindings/src/resources/mod.rs index c65366d..3dbfc26 100644 --- a/datahub_python_bindings/src/resources/mod.rs +++ b/datahub_python_bindings/src/resources/mod.rs @@ -1,12 +1,12 @@ use crate::relations::{PyEdgeProxy, PyRelatedNode}; use chrono::{DateTime, Utc}; -use dataplatform_rust_sdk::datahub::to_snake_lower_cased_allow_start_with_digits; -use dataplatform_rust_sdk::generic::IdAndExtId; +use intellistream_datahub_sdk::datahub::to_snake_lower_cased_allow_start_with_digits; +use intellistream_datahub_sdk::generic::IdAndExtId; use crate::events::PyEvent; -use dataplatform_rust_sdk::filters::{BasicEventFilter, EventFilter}; -use dataplatform_rust_sdk::relations::RelatedNode; -use dataplatform_rust_sdk::resources::RelatedResourcesForm; -use dataplatform_rust_sdk::{ApiService, Resource}; +use intellistream_datahub_sdk::filters::{BasicEventFilter, EventFilter}; +use intellistream_datahub_sdk::relations::RelatedNode; +use intellistream_datahub_sdk::resources::RelatedResourcesForm; +use intellistream_datahub_sdk::{ApiService, Resource}; use geojson::Geometry; use pyo3::exceptions::PyValueError; use pyo3::types::PyAny; @@ -26,13 +26,13 @@ pub mod async_service; pub mod sync_service; use crate::{PyFieldGeoJson, PyFieldStr, PyFieldU64, PyListFieldStr, PyMapField}; -use dataplatform_rust_sdk::resources::{ResourceUpdate, ResourceUpdateFields}; +use intellistream_datahub_sdk::resources::{ResourceUpdate, ResourceUpdateFields}; /// One resource's update for `resources.update`. Target the resource by a `Resource`, its numeric /// id, or its external id; every field is optional and uses the same wrappers as elsewhere /// (`FieldStr` for scalars, `ListFieldStr` for labels, `MapField` for metadata). Mirrors /// `TimeSeriesUpdate`. -#[pyclass(module = "datahub_sdk", name = "ResourceUpdate")] +#[pyclass(module = "intellistream_datahub_sdk", name = "ResourceUpdate")] #[derive(Clone)] pub struct PyResourceUpdate { pub inner: ResourceUpdate, @@ -132,7 +132,7 @@ impl From for IdAndExtId { } } -#[pyclass(module = "datahub_sdk", name = "Resource", from_py_object)] +#[pyclass(module = "intellistream_datahub_sdk", name = "Resource", from_py_object)] #[derive(Clone)] pub struct PyResource { pub inner: Resource, @@ -366,7 +366,7 @@ use crate::labels::PyLabel; /// Result of a graph traversal (`resources.fetch_related(...)`): the connected /// sub-graph of `nodes`, the `edges` between them, and their `labels`. -#[pyclass(module = "datahub_sdk", name = "ResourceNetwork")] +#[pyclass(module = "intellistream_datahub_sdk", name = "ResourceNetwork")] #[derive(Clone)] pub struct PyResourceNetwork { pub nodes: Vec, @@ -378,7 +378,7 @@ impl PyResourceNetwork { /// Build the Python view of a graph traversal, stamping `client` onto every node so /// callers can chain navigation off the returned resources. pub fn from_network( - network: dataplatform_rust_sdk::resources::ResourceNetwork, + network: intellistream_datahub_sdk::resources::ResourceNetwork, client: Arc, ) -> Self { Self { @@ -535,10 +535,10 @@ impl PyResource { /// must **all** be present. A `None` `metadata` value matches the key alone. `data_set_id` takes /// numeric ids, external ids or `IdCollection`s and expands down the dataset hierarchy; `None` /// (no restriction) differs from `[]` (narrow to no datasets, matching nothing). -#[pyclass(module = "datahub_sdk", name = "ResourceFilter", from_py_object)] +#[pyclass(module = "intellistream_datahub_sdk", name = "ResourceFilter", from_py_object)] #[derive(Clone)] pub struct PyResourceFilter { - pub inner: dataplatform_rust_sdk::resources::ResourceFilter, + pub inner: intellistream_datahub_sdk::resources::ResourceFilter, } #[pymethods] diff --git a/datahub_python_bindings/src/resources/sync_service.rs b/datahub_python_bindings/src/resources/sync_service.rs index 50d879e..04c70f4 100644 --- a/datahub_python_bindings/src/resources/sync_service.rs +++ b/datahub_python_bindings/src/resources/sync_service.rs @@ -1,21 +1,21 @@ use crate::relations::{PyGraphResult, PyRelForm}; use crate::resources::{PyResourceFilter, ResourceIdentifiable}; use crate::resources::{PyResource, PyResourceNetwork, PyResourceUpdate}; -use dataplatform_rust_sdk::resources::ResourceUpdate; +use intellistream_datahub_sdk::resources::ResourceUpdate; use crate::resources::async_service::PyResourcesServiceAsync; use crate::{DataSetRef, PySearchAndFilterForm, StringOrList, opt_data_set_refs, opt_patterns}; -use dataplatform_rust_sdk::filters::NodeFilter; -use dataplatform_rust_sdk::generic::IdAndExtId; -use dataplatform_rust_sdk::relations::RelForm; -use dataplatform_rust_sdk::resources::{ +use intellistream_datahub_sdk::filters::NodeFilter; +use intellistream_datahub_sdk::generic::IdAndExtId; +use intellistream_datahub_sdk::relations::RelForm; +use intellistream_datahub_sdk::resources::{ FetchNearestResourcesForm, RelatedResourcesForm, ResourceFilter, ResourceRetreiver, }; -use dataplatform_rust_sdk::{ApiService, Resource}; +use intellistream_datahub_sdk::{ApiService, Resource}; use pyo3::{PyResult, Python, pyclass, pymethods}; use std::collections::HashMap; use std::sync::Arc; -#[pyclass(module = "datahub_sdk", name = "ResourcesServiceSync")] +#[pyclass(module = "intellistream_datahub_sdk", name = "ResourcesServiceSync")] pub struct PyResourcesServiceSync { pub api_service: Arc, pub runtime: Arc, diff --git a/datahub_python_bindings/src/subscriptions/async_service.rs b/datahub_python_bindings/src/subscriptions/async_service.rs index f17bcca..f9815d3 100644 --- a/datahub_python_bindings/src/subscriptions/async_service.rs +++ b/datahub_python_bindings/src/subscriptions/async_service.rs @@ -4,15 +4,15 @@ use crate::subscriptions::{ PyDataSort, PySubscription, PySubscriptionRetriever, SubscriptionIdentifyable, SubscriptionTimeseriesId, }; -use dataplatform_rust_sdk::ApiService; -use dataplatform_rust_sdk::generic::IdAndExtId; -use dataplatform_rust_sdk::subscriptions::Subscription; +use intellistream_datahub_sdk::ApiService; +use intellistream_datahub_sdk::generic::IdAndExtId; +use intellistream_datahub_sdk::subscriptions::Subscription; use pyo3::exceptions::PyException; use pyo3::prelude::*; use pyo3_async_runtimes::tokio::future_into_py; use std::sync::Arc; -#[pyclass(module = "datahub_sdk", name = "SubscriptionsServiceAsync")] +#[pyclass(module = "intellistream_datahub_sdk", name = "SubscriptionsServiceAsync")] pub struct PySubscriptionsServiceAsync { pub api_service: Arc, } diff --git a/datahub_python_bindings/src/subscriptions/general.rs b/datahub_python_bindings/src/subscriptions/general.rs index 8e16160..43eff88 100644 --- a/datahub_python_bindings/src/subscriptions/general.rs +++ b/datahub_python_bindings/src/subscriptions/general.rs @@ -1,8 +1,8 @@ use crate::PyIdCollection; use crate::subscriptions::{PySubscription, SubscriptionTimeseriesId}; use chrono::{DateTime, Utc}; -use dataplatform_rust_sdk::generic::IdAndExtId; -use dataplatform_rust_sdk::subscriptions::Subscription; +use intellistream_datahub_sdk::generic::IdAndExtId; +use intellistream_datahub_sdk::subscriptions::Subscription; use pyo3::pymethods; #[pymethods] diff --git a/datahub_python_bindings/src/subscriptions/listener.rs b/datahub_python_bindings/src/subscriptions/listener.rs index 3a59d34..3a51e6d 100644 --- a/datahub_python_bindings/src/subscriptions/listener.rs +++ b/datahub_python_bindings/src/subscriptions/listener.rs @@ -1,5 +1,5 @@ use crate::subscriptions::PySubscriptionMessage; -use dataplatform_rust_sdk::subscriptions::SubscriptionListener; +use intellistream_datahub_sdk::subscriptions::SubscriptionListener; use pyo3::exceptions::{PyException, PyStopAsyncIteration, PyStopIteration}; use pyo3::prelude::*; use pyo3_async_runtimes::tokio::future_into_py; @@ -11,7 +11,7 @@ type SharedListener = Arc>>; /// Synchronous Python wrapper around the Rust `SubscriptionListener`. Iterating drives the /// underlying WebSocket: `for msg in listener:` blocks until the next message or returns when /// the connection closes cleanly. -#[pyclass(module = "datahub_sdk", name = "SubscriptionListener")] +#[pyclass(module = "intellistream_datahub_sdk", name = "SubscriptionListener")] pub struct PySubscriptionListener { pub(crate) listener: SharedListener, pub(crate) runtime: Arc, @@ -175,7 +175,7 @@ impl PySubscriptionListener { } /// Asynchronous Python wrapper. Use `async for msg in listener:` on the asyncio side. -#[pyclass(module = "datahub_sdk", name = "SubscriptionListenerAsync")] +#[pyclass(module = "intellistream_datahub_sdk", name = "SubscriptionListenerAsync")] pub struct PySubscriptionListenerAsync { pub(crate) listener: SharedListener, } diff --git a/datahub_python_bindings/src/subscriptions/mod.rs b/datahub_python_bindings/src/subscriptions/mod.rs index cc93bf6..8fb49d9 100644 --- a/datahub_python_bindings/src/subscriptions/mod.rs +++ b/datahub_python_bindings/src/subscriptions/mod.rs @@ -1,7 +1,7 @@ use crate::PyIdCollection; use crate::timeseries::PyTimeSeries; -use dataplatform_rust_sdk::generic::IdAndExtId; -use dataplatform_rust_sdk::subscriptions::{ +use intellistream_datahub_sdk::generic::IdAndExtId; +use intellistream_datahub_sdk::subscriptions::{ DataCollectionString, DataSort, DataWrapperMessage, EventAction, EventObject, Subscription, SubscriptionFilter, SubscriptionMessage, SubscriptionRetriever, WsDatapoint, }; @@ -16,7 +16,7 @@ pub mod general; pub mod listener; pub mod sync_service; -#[pyclass(module = "datahub_sdk", name = "Subscription")] +#[pyclass(module = "intellistream_datahub_sdk", name = "Subscription")] #[derive(Clone)] pub struct PySubscription { pub inner: Subscription, @@ -33,7 +33,7 @@ impl From for Subscription { } } -#[pyclass(module = "datahub_sdk", name = "SubscriptionFilter")] +#[pyclass(module = "intellistream_datahub_sdk", name = "SubscriptionFilter")] #[derive(Clone, Default)] pub struct PySubscriptionFilter { pub inner: SubscriptionFilter, @@ -76,7 +76,7 @@ impl PySubscriptionFilter { } } -#[pyclass(module = "datahub_sdk", name = "DataSort")] +#[pyclass(module = "intellistream_datahub_sdk", name = "DataSort")] #[derive(Clone, Default)] pub struct PyDataSort { pub inner: DataSort, @@ -125,7 +125,7 @@ impl PyDataSort { } } -#[pyclass(module = "datahub_sdk", name = "SubscriptionRetriever")] +#[pyclass(module = "intellistream_datahub_sdk", name = "SubscriptionRetriever")] #[derive(Clone)] pub struct PySubscriptionRetriever { pub inner: SubscriptionRetriever, @@ -240,7 +240,7 @@ impl From for IdAndExtId { // -- WebSocket message wrappers ------------------------------------------------------------ -#[pyclass(module = "datahub_sdk", name = "EventAction")] +#[pyclass(module = "intellistream_datahub_sdk", name = "EventAction")] #[derive(Debug, Clone, Copy, PartialEq, Eq, EnumString, Display)] #[strum(serialize_all = "UPPERCASE")] pub enum PyEventAction { @@ -275,7 +275,7 @@ impl PyEventAction { } } -#[pyclass(module = "datahub_sdk", name = "EventObject")] +#[pyclass(module = "intellistream_datahub_sdk", name = "EventObject")] #[derive(Debug, Clone, Copy, PartialEq, Eq, EnumString, Display)] #[strum(serialize_all = "UPPERCASE")] pub enum PyEventObject { @@ -322,7 +322,7 @@ impl PyEventObject { } } -#[pyclass(module = "datahub_sdk", name = "WsDatapoint")] +#[pyclass(module = "intellistream_datahub_sdk", name = "WsDatapoint")] #[derive(Clone)] pub struct PyWsDatapoint { pub inner: WsDatapoint, @@ -356,7 +356,7 @@ impl PyWsDatapoint { } } -#[pyclass(module = "datahub_sdk", name = "DataCollectionString")] +#[pyclass(module = "intellistream_datahub_sdk", name = "DataCollectionString")] #[derive(Clone)] pub struct PyDataCollectionString { pub inner: DataCollectionString, @@ -401,7 +401,7 @@ impl PyDataCollectionString { } } -#[pyclass(module = "datahub_sdk", name = "DataWrapperMessage")] +#[pyclass(module = "intellistream_datahub_sdk", name = "DataWrapperMessage")] #[derive(Clone)] pub struct PyDataWrapperMessage { pub inner: DataWrapperMessage, @@ -438,7 +438,7 @@ impl PyDataWrapperMessage { } } -#[pyclass(module = "datahub_sdk", name = "SubscriptionMessage")] +#[pyclass(module = "intellistream_datahub_sdk", name = "SubscriptionMessage")] #[derive(Clone)] pub struct PySubscriptionMessage { pub inner: SubscriptionMessage, diff --git a/datahub_python_bindings/src/subscriptions/sync_service.rs b/datahub_python_bindings/src/subscriptions/sync_service.rs index 5b3660f..e696177 100644 --- a/datahub_python_bindings/src/subscriptions/sync_service.rs +++ b/datahub_python_bindings/src/subscriptions/sync_service.rs @@ -3,16 +3,16 @@ use crate::subscriptions::{ PyDataSort, PySubscription, PySubscriptionRetriever, SubscriptionIdentifyable, SubscriptionTimeseriesId, }; -use dataplatform_rust_sdk::ApiService; -use dataplatform_rust_sdk::generic::IdAndExtId; -use dataplatform_rust_sdk::subscriptions::{ +use intellistream_datahub_sdk::ApiService; +use intellistream_datahub_sdk::generic::IdAndExtId; +use intellistream_datahub_sdk::subscriptions::{ Subscription, SubscriptionFilter, SubscriptionRetriever, }; use pyo3::exceptions::{PyException, PyValueError}; use pyo3::prelude::*; use std::sync::Arc; -#[pyclass(module = "datahub_sdk", name = "SubscriptionsServiceSync")] +#[pyclass(module = "intellistream_datahub_sdk", name = "SubscriptionsServiceSync")] pub struct PySubscriptionsServiceSync { pub api_service: Arc, pub runtime: Arc, diff --git a/datahub_python_bindings/src/timeseries/async_service.rs b/datahub_python_bindings/src/timeseries/async_service.rs index 2401e76..786462c 100644 --- a/datahub_python_bindings/src/timeseries/async_service.rs +++ b/datahub_python_bindings/src/timeseries/async_service.rs @@ -9,16 +9,16 @@ use crate::{ PyTimeSeriesFilterForm, }; use crate::datetime::py_datetime_to_utc; -use dataplatform_rust_sdk::generic::{ +use intellistream_datahub_sdk::generic::{ DataWrapper, DatapointString, DatapointsCollection, DeleteFilter, IdAndExtId, RetrieveFilter, }; -use dataplatform_rust_sdk::{ApiService, TimeSeries, TimeSeriesUpdate, TimeSeriesUpdateCollection}; +use intellistream_datahub_sdk::{ApiService, TimeSeries, TimeSeriesUpdate, TimeSeriesUpdateCollection}; use pyo3::prelude::*; use pyo3::{Bound, PyAny, PyResult, Python, pyclass, pymethods}; use pyo3_async_runtimes::tokio::future_into_py; use std::sync::Arc; -#[pyclass(module = "datahub_sdk", name = "TimeSeriesServiceAsync")] +#[pyclass(module = "intellistream_datahub_sdk", name = "TimeSeriesServiceAsync")] pub struct PyTimeSeriesServiceAsync { pub api_service: Arc, } diff --git a/datahub_python_bindings/src/timeseries/construction.rs b/datahub_python_bindings/src/timeseries/construction.rs index 34e814b..c7cc269 100644 --- a/datahub_python_bindings/src/timeseries/construction.rs +++ b/datahub_python_bindings/src/timeseries/construction.rs @@ -2,10 +2,10 @@ use crate::relations::PyRelatedNode; use crate::timeseries::datapoints::{PyDatapointString, PyDatapointsCollectionString}; use crate::timeseries::{PyTimeSeries, ValueType}; use crate::{DatahubIdentity, Identifiable}; -use dataplatform_rust_sdk::TimeSeries; -use dataplatform_rust_sdk::datahub::to_snake_lower_cased_allow_start_with_digits; -use dataplatform_rust_sdk::generic::{DatapointString, DatapointsCollection}; -use dataplatform_rust_sdk::relations::RelatedNode; +use intellistream_datahub_sdk::TimeSeries; +use intellistream_datahub_sdk::datahub::to_snake_lower_cased_allow_start_with_digits; +use intellistream_datahub_sdk::generic::{DatapointString, DatapointsCollection}; +use intellistream_datahub_sdk::relations::RelatedNode; use pyo3::exceptions::PyValueError; use pyo3::prelude::*; use std::collections::HashMap; diff --git a/datahub_python_bindings/src/timeseries/datapoints.rs b/datahub_python_bindings/src/timeseries/datapoints.rs index 23eb189..1b942cb 100644 --- a/datahub_python_bindings/src/timeseries/datapoints.rs +++ b/datahub_python_bindings/src/timeseries/datapoints.rs @@ -1,14 +1,14 @@ use crate::datetime::{opt_py_datetime_to_utc, py_datetime_to_utc}; use crate::timeseries::PyTimeseriesIdentifiable; use chrono::{DateTime, Utc}; -use dataplatform_rust_sdk::generic::{ +use intellistream_datahub_sdk::generic::{ Datapoint, DatapointString, DatapointsCollection, IdAndExtId, Identifiable, RetrieveFilter, }; use pyo3::prelude::*; use pyo3::types::{PyDict, PyType}; use pyo3::{Bound, Py, Python, pyclass, pymethods}; -#[pyclass(module = "datahub_sdk", name = "DatapointsCollectionString")] +#[pyclass(module = "intellistream_datahub_sdk", name = "DatapointsCollectionString")] #[derive(Clone, Debug)] pub struct PyDatapointsCollectionString { pub inner: DatapointsCollection, @@ -24,7 +24,7 @@ impl From for DatapointsCollection, @@ -84,7 +84,7 @@ impl PyDatapointsCollectionDatapoints { } } -#[pyclass(module = "datahub_sdk", name = "DatapointString")] +#[pyclass(module = "intellistream_datahub_sdk", name = "DatapointString")] #[derive(Clone)] pub struct PyDatapointString { pub inner: DatapointString, @@ -141,7 +141,7 @@ impl PyDatapointString { } } -#[pyclass(module = "datahub_sdk", name = "RetrieveFilter")] +#[pyclass(module = "intellistream_datahub_sdk", name = "RetrieveFilter")] #[derive(Clone)] pub struct PyRetrieveFilter { inner: RetrieveFilter, @@ -219,7 +219,7 @@ impl PyRetrieveFilter { self.inner.cursor.as_ref() } } -#[pyclass(module = "datahub_sdk", name = "Datapoint")] +#[pyclass(module = "intellistream_datahub_sdk", name = "Datapoint")] #[derive(Clone)] pub struct PyDatapoint { inner: Datapoint, diff --git a/datahub_python_bindings/src/timeseries/mod.rs b/datahub_python_bindings/src/timeseries/mod.rs index b997324..9e40284 100644 --- a/datahub_python_bindings/src/timeseries/mod.rs +++ b/datahub_python_bindings/src/timeseries/mod.rs @@ -11,16 +11,16 @@ use crate::{ PyMapField, }; use chrono::{DateTime, Utc}; -use dataplatform_rust_sdk::fields::{Field, ListField, MapField}; -use dataplatform_rust_sdk::generic::{ +use intellistream_datahub_sdk::fields::{Field, ListField, MapField}; +use intellistream_datahub_sdk::generic::{ Datapoint, DatapointString, DatapointsCollection, DeleteFilter, IdAndExtId, RetrieveFilter, }; use crate::events::PyEvent; use crate::resources::{PyResource, PyResourceNetwork}; -use dataplatform_rust_sdk::filters::{BasicEventFilter, EventFilter}; -use dataplatform_rust_sdk::resources::RelatedResourcesForm; -use dataplatform_rust_sdk::{ApiService, TimeSeries, TimeSeriesUpdate, TimeSeriesUpdateFields}; +use intellistream_datahub_sdk::filters::{BasicEventFilter, EventFilter}; +use intellistream_datahub_sdk::resources::RelatedResourcesForm; +use intellistream_datahub_sdk::{ApiService, TimeSeries, TimeSeriesUpdate, TimeSeriesUpdateFields}; use pyo3::exceptions::PyValueError; use pyo3::prelude::*; use pyo3::pyclass; @@ -84,7 +84,7 @@ pub mod sync_service; /// the connected node (a Timeseries, Dataset, Asset or Policy) /// /// -#[pyclass(module = "datahub_sdk", name = "TimeSeries")] +#[pyclass(module = "intellistream_datahub_sdk", name = "TimeSeries")] #[derive(Clone)] pub struct PyTimeSeries { pub inner: TimeSeries, @@ -227,7 +227,7 @@ impl From for IdAndExtId { /// ---------- /// ts: Timeseries #[pyclass( - module = "datahub_sdk", + module = "intellistream_datahub_sdk", name = "TimeSeriesUpdate", from_py_object )] @@ -345,7 +345,7 @@ impl PyTimeSeriesUpdate { /// ---------- /// /// -#[pyclass(module = "datahub_sdk", name = "DeleteFilter")] +#[pyclass(module = "intellistream_datahub_sdk", name = "DeleteFilter")] #[derive(Clone, Debug)] pub struct PyDeleteFilter { inner: DeleteFilter, @@ -404,7 +404,7 @@ impl PyDeleteFilter { /// ("decimal" is accepted as an alias for Float and normalised to "float") /// /// from pyhton these can be passed directly as case-insensitive literal strings -#[pyclass(module = "datahub_sdk", skip_from_py_object)] +#[pyclass(module = "intellistream_datahub_sdk", skip_from_py_object)] #[derive(Debug, Clone, Copy, PartialEq, Eq, EnumString, Display)] #[strum(serialize_all = "camelCase")] // Ensures internal string representation is lowercase pub enum ValueType { diff --git a/datahub_python_bindings/src/timeseries/sync_service.rs b/datahub_python_bindings/src/timeseries/sync_service.rs index 298fcee..bd51364 100644 --- a/datahub_python_bindings/src/timeseries/sync_service.rs +++ b/datahub_python_bindings/src/timeseries/sync_service.rs @@ -5,12 +5,12 @@ use crate::timeseries::datapoints::{ }; use crate::{DatahubIdentity, Identifiable}; use crate::{PyIdCollection, PyRetrieveFilter, PySearchAndFilterForm, PyTimeSeriesFilterForm}; -use dataplatform_rust_sdk::generic::{DataWrapper, IdAndExtId}; -use dataplatform_rust_sdk::{ApiService, TimeSeriesUpdateCollection}; +use intellistream_datahub_sdk::generic::{DataWrapper, IdAndExtId}; +use intellistream_datahub_sdk::{ApiService, TimeSeriesUpdateCollection}; use pyo3_async_runtimes::tokio::future_into_py; use std::sync::Arc; -#[pyclass(module = "datahub_sdk", name = "TimeSeriesServiceSync")] +#[pyclass(module = "intellistream_datahub_sdk", name = "TimeSeriesServiceSync")] pub struct PyTimeSeriesServiceSync { pub api_service: Arc, pub runtime: Arc, diff --git a/datahub_python_bindings/src/units/async_service.rs b/datahub_python_bindings/src/units/async_service.rs index a35e0e3..b7760f7 100644 --- a/datahub_python_bindings/src/units/async_service.rs +++ b/datahub_python_bindings/src/units/async_service.rs @@ -1,12 +1,12 @@ use crate::PyIdCollection; use crate::units::PyUnit; -use dataplatform_rust_sdk::ApiService; -use dataplatform_rust_sdk::generic::{DataWrapper, IdAndExtId}; +use intellistream_datahub_sdk::ApiService; +use intellistream_datahub_sdk::generic::{DataWrapper, IdAndExtId}; use pyo3::{Bound, PyAny, PyResult, Python, pyclass, pymethods}; use pyo3_async_runtimes::tokio::future_into_py; use std::sync::Arc; -#[pyclass(module = "datahub_sdk", name = "UnitServiceAsync")] +#[pyclass(module = "intellistream_datahub_sdk", name = "UnitServiceAsync")] pub(crate) struct PyUnitServiceAsync { pub(crate) api_service: Arc, } diff --git a/datahub_python_bindings/src/units/mod.rs b/datahub_python_bindings/src/units/mod.rs index 8245cbe..d3e69cb 100644 --- a/datahub_python_bindings/src/units/mod.rs +++ b/datahub_python_bindings/src/units/mod.rs @@ -1,4 +1,4 @@ -use dataplatform_rust_sdk::{TimeSeries, Unit}; +use intellistream_datahub_sdk::{TimeSeries, Unit}; use pyo3::{pyclass, pymethods}; use std::collections::HashMap; @@ -33,7 +33,7 @@ pub mod sync_service; /// source_reference: /// url to the source of the unit /// -#[pyclass(module = "datahub_sdk", name = "Unit")] +#[pyclass(module = "intellistream_datahub_sdk", name = "Unit")] #[derive(Clone)] pub struct PyUnit { pub inner: Unit, diff --git a/datahub_python_bindings/src/units/sync_service.rs b/datahub_python_bindings/src/units/sync_service.rs index aa9e892..a2ba210 100644 --- a/datahub_python_bindings/src/units/sync_service.rs +++ b/datahub_python_bindings/src/units/sync_service.rs @@ -1,12 +1,12 @@ use crate::PyIdCollection; use crate::units::PyUnit; -use dataplatform_rust_sdk::ApiService; -use dataplatform_rust_sdk::generic::{DataWrapper, IdAndExtId}; +use intellistream_datahub_sdk::ApiService; +use intellistream_datahub_sdk::generic::{DataWrapper, IdAndExtId}; use pyo3::prelude::*; use std::sync::Arc; use tokio::runtime::Runtime; -#[pyclass(module = "datahub_sdk", name = "UnitServiceSync")] +#[pyclass(module = "intellistream_datahub_sdk", name = "UnitServiceSync")] pub(crate) struct PyUnitServiceSync { pub(crate) api_service: Arc, pub(crate) runtime: Arc, diff --git a/python_tests/README.md b/python_tests/README.md index 01d7b0a..25f2ca7 100644 --- a/python_tests/README.md +++ b/python_tests/README.md @@ -1,6 +1,6 @@ # Running the Python tests -The tests in this directory exercise the **`datahub_sdk`** Python module, which is the +The tests in this directory exercise the **`intellistream_datahub_sdk`** Python module, which is the PyO3 binding layer compiled from the `datahub_python_bindings/` crate. Because the module is native (Rust compiled to a `.so`), you must **build and install it into your Python environment with `maturin` before the tests can import it** — there is no pure-Python @@ -79,9 +79,9 @@ That file pins: --- -## 3. Build and install the `datahub_sdk` bindings +## 3. Build and install the `intellistream_datahub_sdk` bindings -`maturin develop` compiles the Rust crate and installs the resulting `datahub_sdk` module +`maturin develop` compiles the Rust crate and installs the resulting `intellistream_datahub_sdk` module directly into the active venv's `site-packages`, so it becomes importable from anywhere. ```bash @@ -97,7 +97,7 @@ the parent SDK — the tests import the **compiled** module, not the source. Verify the install: ```bash -python3 -c "import datahub_sdk; print('ok')" +python3 -c "import intellistream_datahub_sdk; print('ok')" ``` --- @@ -167,7 +167,7 @@ python -m pytest python_tests ## Troubleshooting -- **`ModuleNotFoundError: No module named 'datahub_sdk'`** — the venv isn't active, or you +- **`ModuleNotFoundError: No module named 'intellistream_datahub_sdk'`** — the venv isn't active, or you haven't run `maturin develop` since creating it. Activate the venv and rebuild. - **`ModuleNotFoundError: No module named 'fixtures'` / `'python_tests'`** — you're not running from the repo root, or you used bare `pytest` instead of `python -m pytest`. Run diff --git a/python_tests/conftest.py b/python_tests/conftest.py index 9a1a5a5..35b80c4 100644 --- a/python_tests/conftest.py +++ b/python_tests/conftest.py @@ -19,7 +19,7 @@ so orphaned instances of those are not reclaimable this way — prefer fixed, self-healing external ids for them (a delete-before-create in the test/factory). """ -import datahub_sdk +import intellistream_datahub_sdk import pytest from fixtures import ENV_FILE, TEST_PREFIX, _safe_delete_each @@ -43,8 +43,8 @@ def _sweep(client) -> None: # Events — no list endpoint, but the filter API accepts an external-id prefix. try: - filt = datahub_sdk.EventFilter( - basic_filter=datahub_sdk.BasicEventFilter(external_id=f"{TEST_PREFIX}*") + filt = intellistream_datahub_sdk.EventFilter( + basic_filter=intellistream_datahub_sdk.BasicEventFilter(external_id=f"{TEST_PREFIX}*") ) _safe_delete_each(client.events.delete, _matching_prefix(client.events.filter(filt))) except Exception: @@ -71,7 +71,7 @@ def _prefix_sweep(): happened to build. If no backend is configured (e.g. only the offline buffering tests run), there is nothing to sweep.""" try: - client = datahub_sdk.DataHubClient.from_envfile(ENV_FILE) + client = intellistream_datahub_sdk.DataHubClient.from_envfile(ENV_FILE) except Exception: yield return diff --git a/python_tests/filter_fixtures.py b/python_tests/filter_fixtures.py index 1e385d2..5562060 100644 --- a/python_tests/filter_fixtures.py +++ b/python_tests/filter_fixtures.py @@ -37,7 +37,7 @@ import pandas as pd import pytest -import datahub_sdk +import intellistream_datahub_sdk from fixtures import TEST_PREFIX from polling import poll_until @@ -78,17 +78,17 @@ def datasets(sync_client, prefix, token): _delete_quietly(sync_client.datasets.delete, [child_ext, parent_ext]) parent = sync_client.datasets.create([ - datahub_sdk.Dataset(external_id=parent_ext, name=f"Filter Parent {token}", + intellistream_datahub_sdk.Dataset(external_id=parent_ext, name=f"Filter Parent {token}", metadata={"tier": "gold", f"dsonly_{token}": "yes"}) ])[0] child = sync_client.datasets.create([ - datahub_sdk.Dataset(external_id=child_ext, name=f"Filter Child {token}", + intellistream_datahub_sdk.Dataset(external_id=child_ext, name=f"Filter Child {token}", metadata={"tier": "silver"}) ])[0] # Stored from = parent, to = child. Reversing it silently produces no hierarchy at all: the # closure query walks rel_start -> rel_end and simply finds nothing to descend into. sync_client.edges.create([ - datahub_sdk.RelForm(relationship_type="BELONGS_TO", + intellistream_datahub_sdk.RelForm(relationship_type="BELONGS_TO", from_external_id=parent.external_id, to_external_id=child.external_id) ]) @@ -128,7 +128,7 @@ def timeseries_corpus(sync_client, datasets, prefix, token): } _delete_quietly(sync_client.timeseries.delete, [s["external_id"] for s in specs.values()]) created = sync_client.timeseries.create( - [datahub_sdk.TimeSeries(**spec) for spec in specs.values()] + [intellistream_datahub_sdk.TimeSeries(**spec) for spec in specs.values()] ) by_external_id = {ts.external_id: ts for ts in created} corpus = {key: by_external_id[spec["external_id"]] for key, spec in specs.items()} @@ -159,7 +159,7 @@ def resource_corpus(sync_client, datasets, prefix, token): ), } _delete_quietly(sync_client.resources.delete, [s["external_id"] for s in specs.values()]) - sync_client.resources.create([datahub_sdk.Resource(**spec) for spec in specs.values()]) + sync_client.resources.create([intellistream_datahub_sdk.Resource(**spec) for spec in specs.values()]) # Read them back so the fixture hands out server-assigned ids. stored = { r.external_id: r @@ -196,12 +196,12 @@ def event_corpus(sync_client, datasets, prefix, token): ), } externals = [s["external_id"] for s in specs.values()] - sync_client.events.create([datahub_sdk.Event(**spec) for spec in specs.values()]) + sync_client.events.create([intellistream_datahub_sdk.Event(**spec) for spec in specs.values()]) # Poll rather than sleep: the projection lag is usually milliseconds and occasionally seconds. def visible(): - return sync_client.events.filter(datahub_sdk.EventFilter( - datahub_sdk.BasicEventFilter(external_id=f"{prefix}*"), limit=50)) + return sync_client.events.filter(intellistream_datahub_sdk.EventFilter( + intellistream_datahub_sdk.BasicEventFilter(external_id=f"{prefix}*"), limit=50)) found = poll_until(visible, lambda events: len(events) >= len(specs)) assert len(found) >= len(specs), ( @@ -246,7 +246,7 @@ def sortable_timeseries(sync_client, datasets, prefix, token): } _delete_quietly(sync_client.timeseries.delete, [s["external_id"] for s in specs.values()]) for index in creation_order: - sync_client.timeseries.create([datahub_sdk.TimeSeries( + sync_client.timeseries.create([intellistream_datahub_sdk.TimeSeries( external_id=specs[index]["external_id"], name=specs[index]["name"], unit="bar", value_type="float", data_set_id=child.id)]) time.sleep(0.02) @@ -273,7 +273,7 @@ def null_source_resources(sync_client, datasets, prefix, token): externals = [s["external_id"] for s in specs.values()] _delete_quietly(sync_client.resources.delete, externals) sync_client.resources.create([ - datahub_sdk.Resource(labels=["ASSET"], is_root=True, **spec) for spec in specs.values() + intellistream_datahub_sdk.Resource(labels=["ASSET"], is_root=True, **spec) for spec in specs.values() ]) yield specs @@ -306,7 +306,7 @@ def sortable_events(sync_client, datasets, prefix, token): spec["type"] = f"{spec['type_letter']}_type_{token}" sync_client.events.create([ - datahub_sdk.Event( + intellistream_datahub_sdk.Event( external_id=spec["external_id"], type=spec["type"], sub_type="electrical", status=spec["status"], source=f"src_{token}", data_set_id=child.id, event_time=base + pd.Timedelta(minutes=spec["offset"]), @@ -315,8 +315,8 @@ def sortable_events(sync_client, datasets, prefix, token): ]) def visible(): - return sync_client.events.filter(datahub_sdk.EventFilter( - datahub_sdk.BasicEventFilter(external_id=f"{prefix}_sort_ev_*"), limit=50)) + return sync_client.events.filter(intellistream_datahub_sdk.EventFilter( + intellistream_datahub_sdk.BasicEventFilter(external_id=f"{prefix}_sort_ev_*"), limit=50)) found = poll_until(visible, lambda events: len(events) >= len(specs)) assert len(found) >= len(specs), "the sortable event corpus never became visible" diff --git a/python_tests/fixtures.py b/python_tests/fixtures.py index 35e40f3..ddcf72c 100644 --- a/python_tests/fixtures.py +++ b/python_tests/fixtures.py @@ -10,7 +10,7 @@ import uuid from time import sleep -import datahub_sdk +import intellistream_datahub_sdk import numpy as np import pandas as pd import pytest @@ -41,12 +41,12 @@ def _safe_delete_each(delete_fn, entities) -> None: @pytest.fixture(scope="module") def async_client(): - return datahub_sdk.AsyncDataHubClient.from_envfile(ENV_FILE) + return intellistream_datahub_sdk.AsyncDataHubClient.from_envfile(ENV_FILE) @pytest.fixture(scope="module") def sync_client(): - return datahub_sdk.DataHubClient.from_envfile(ENV_FILE) + return intellistream_datahub_sdk.DataHubClient.from_envfile(ENV_FILE) # --------------------------------------------------------------------------- # @@ -65,7 +65,7 @@ def _make(**kwargs): kwargs.setdefault("external_id", unique_id("ts")) kwargs.setdefault("value_type", "float") kwargs.setdefault("unit", "a.u") - ts = datahub_sdk.TimeSeries(**kwargs) + ts = intellistream_datahub_sdk.TimeSeries(**kwargs) # ensure a clean slate in case a previous failed run leaked the ext id sync_client.timeseries.delete([ts]) result = sync_client.timeseries.create([ts])[0] @@ -84,7 +84,7 @@ def make_dataset(sync_client): def _make(**kwargs): kwargs.setdefault("external_id", unique_id("dataset")) - ds = datahub_sdk.Dataset(**kwargs) + ds = intellistream_datahub_sdk.Dataset(**kwargs) sync_client.datasets.delete([ds]) result = sync_client.datasets.create([ds])[0] created.append(result) @@ -102,7 +102,7 @@ def make_function(sync_client): def _make(**kwargs): kwargs.setdefault("external_id", unique_id("fn")) - fn = datahub_sdk.Function(**kwargs) + fn = intellistream_datahub_sdk.Function(**kwargs) sync_client.functions.delete([fn.external_id]) result = sync_client.functions.create([fn])[0] created.append(result.external_id) @@ -117,7 +117,7 @@ def _make(**kwargs): def make_events(sync_client): """Factory that creates a batch of events and deletes them at teardown. - Pass a list of ``datahub_sdk.Event`` objects.""" + Pass a list of ``intellistream_datahub_sdk.Event`` objects.""" created = [] def _make(events): @@ -162,7 +162,7 @@ def make_subscription(sync_client): def _make(**kwargs): kwargs.setdefault("external_id", unique_id("sub")) - sub = datahub_sdk.Subscription(**kwargs) + sub = intellistream_datahub_sdk.Subscription(**kwargs) result = sync_client.subscriptions.create([sub])[0] created.append(result.external_id) return result @@ -178,7 +178,7 @@ def _make(**kwargs): @pytest.fixture(scope="module") def ts_float(sync_client): - ts = datahub_sdk.TimeSeries( + ts = intellistream_datahub_sdk.TimeSeries( external_id=unique_id("float"), name="test_float", value_type="float", diff --git a/python_tests/test_auth_constructor.py b/python_tests/test_auth_constructor.py index b3e1476..1070e82 100644 --- a/python_tests/test_auth_constructor.py +++ b/python_tests/test_auth_constructor.py @@ -17,7 +17,7 @@ """ import os -import datahub_sdk +import intellistream_datahub_sdk import pytest from fixtures import ENV_FILE @@ -87,14 +87,14 @@ def _federated_kwargs(env): def _assert_lists_units_sync(base_url, kwargs): - client = datahub_sdk.DataHubClient(base_url, **kwargs) + client = intellistream_datahub_sdk.DataHubClient(base_url, **kwargs) units = client.units.list() assert isinstance(units, list) assert len(units) > 0 async def _assert_lists_units_async(base_url, kwargs): - client = datahub_sdk.AsyncDataHubClient(base_url, **kwargs) + client = intellistream_datahub_sdk.AsyncDataHubClient(base_url, **kwargs) units = await client.units.list() assert isinstance(units, list) assert len(units) > 0 diff --git a/python_tests/test_buffering.py b/python_tests/test_buffering.py index afa46eb..6d11e7c 100644 --- a/python_tests/test_buffering.py +++ b/python_tests/test_buffering.py @@ -13,7 +13,7 @@ from pathlib import Path from uuid import UUID -import datahub_sdk +import intellistream_datahub_sdk import pytest from fixtures import unique_id @@ -21,9 +21,9 @@ ENV_FILE = os.path.join(os.path.dirname(__file__), "..", ".env") -def _unreachable_buffered_client(buffer_dir: Path) -> "datahub_sdk.DataHubClient": +def _unreachable_buffered_client(buffer_dir: Path) -> "intellistream_datahub_sdk.DataHubClient": """A client whose base URL refuses connections, with durable buffering pointed at buffer_dir.""" - return datahub_sdk.DataHubClient( + return intellistream_datahub_sdk.DataHubClient( base_url="http://127.0.0.1:9", token="dummy-token", enable_buffering=True, @@ -37,7 +37,7 @@ def test_event_buffers_to_disk_with_uuid_v7(tmp_path): # Server unreachable -> create() buffers instead of raising, and confirms no items. result = client.events.create( - [datahub_sdk.Event(type="test", external_id=unique_id("buffer_event"), event_time=datetime.now(timezone.utc))] + [intellistream_datahub_sdk.Event(type="test", external_id=unique_id("buffer_event"), event_time=datetime.now(timezone.utc))] ) assert result == [] @@ -60,9 +60,9 @@ def test_event_without_event_time_is_rejected(): # unrepresentable, so this fails at construction rather than at send. Rust callers get a compile # error; Python is the only surface where this needs a test. with pytest.raises(TypeError): - datahub_sdk.Event(type="test", external_id="py_no_time_event") + intellistream_datahub_sdk.Event(type="test", external_id="py_no_time_event") - ev = datahub_sdk.Event(type="test", external_id="py_no_time_event", event_time=datetime.now(timezone.utc) + ev = intellistream_datahub_sdk.Event(type="test", external_id="py_no_time_event", event_time=datetime.now(timezone.utc) ) with pytest.raises(TypeError): ev.event_time = None @@ -85,14 +85,14 @@ def test_datapoints_buffer_to_disk(tmp_path): reason="needs a live backend (.env with BASE_URL + auth)", ) def test_live_event_gets_uuid_v7(): - client = datahub_sdk.DataHubClient.from_envfile(ENV_FILE) + client = intellistream_datahub_sdk.DataHubClient.from_envfile(ENV_FILE) # A fresh external id per run (TEST_PREFIX-carrying) so re-runs never pile up # copies under one id, and the conftest janitor's event sweep can reclaim it # if teardown is skipped. ext_id = unique_id("event") try: created = client.events.create( - [datahub_sdk.Event(type="test", external_id=ext_id, event_time=datetime.now(timezone.utc))] + [intellistream_datahub_sdk.Event(type="test", external_id=ext_id, event_time=datetime.now(timezone.utc))] ) assert len(created) == 1 assert isinstance(created[0].id, UUID) # binding returns a real uuid.UUID diff --git a/python_tests/test_datasets.py b/python_tests/test_datasets.py index e085000..f13ee9f 100644 --- a/python_tests/test_datasets.py +++ b/python_tests/test_datasets.py @@ -5,7 +5,7 @@ """ import uuid -import datahub_sdk +import intellistream_datahub_sdk import pytest from fixtures import async_client, make_dataset, sync_client, unique_id @@ -14,13 +14,13 @@ def test_create_by_ids_delete_roundtrip(sync_client): ext_a = unique_id("dataset_a") ext_b = unique_id("dataset_b") - ds_a = datahub_sdk.Dataset( + ds_a = intellistream_datahub_sdk.Dataset( external_id=ext_a, name=ext_a, description="dataset a", metadata={"env": "test"}, ) - ds_b = datahub_sdk.Dataset(external_id=ext_b, name=ext_b) + ds_b = intellistream_datahub_sdk.Dataset(external_id=ext_b, name=ext_b) try: created = sync_client.datasets.create([ds_a, ds_b]) @@ -51,7 +51,7 @@ def test_create_by_ids_delete_roundtrip(sync_client): def test_create_preserves_metadata_and_description(sync_client): ext = unique_id("dataset_meta") - ds = datahub_sdk.Dataset( + ds = intellistream_datahub_sdk.Dataset( external_id=ext, name=ext, description="with metadata", @@ -100,7 +100,7 @@ async def test_async_list_honours_limit(async_client, make_dataset): capped = await async_client.datasets.list(1) assert len(capped) == 1 - with pytest.raises(datahub_sdk.DataHubException): + with pytest.raises(intellistream_datahub_sdk.DataHubException): await async_client.datasets.list(10_001) @@ -121,8 +121,8 @@ def test_sync_list_and_filter(sync_client, make_dataset): assert any(d.external_id == ext_id for d in sync_client.datasets.list()) narrowed = sync_client.datasets.filter( - datahub_sdk.DatasetFilter( - datahub_sdk.BasicDatasetFilter(external_id=[ext_id]) + intellistream_datahub_sdk.DatasetFilter( + intellistream_datahub_sdk.BasicDatasetFilter(external_id=[ext_id]) ) ) assert [d.external_id for d in narrowed] == [ext_id] @@ -130,8 +130,8 @@ def test_sync_list_and_filter(sync_client, make_dataset): # An unmatchable criterion is an empty result, not an unfiltered one. assert ( sync_client.datasets.filter( - datahub_sdk.DatasetFilter( - datahub_sdk.BasicDatasetFilter(external_id=["ds_does_not_exist_xyz"]) + intellistream_datahub_sdk.DatasetFilter( + intellistream_datahub_sdk.BasicDatasetFilter(external_id=["ds_does_not_exist_xyz"]) ) ) == [] @@ -140,7 +140,7 @@ def test_sync_list_and_filter(sync_client, make_dataset): # An argument-free filter places no restriction, same as list(). assert any( d.external_id == ext_id - for d in sync_client.datasets.filter(datahub_sdk.DatasetFilter()) + for d in sync_client.datasets.filter(intellistream_datahub_sdk.DatasetFilter()) ) @@ -149,15 +149,15 @@ def test_sync_filter_by_metadata_and_prefix(sync_client, make_dataset): make_dataset(external_id=ext_id, name=ext_id, metadata={"owner": ext_id}) by_metadata = sync_client.datasets.filter( - datahub_sdk.DatasetFilter( - datahub_sdk.BasicDatasetFilter(metadata={"owner": ext_id}) + intellistream_datahub_sdk.DatasetFilter( + intellistream_datahub_sdk.BasicDatasetFilter(metadata={"owner": ext_id}) ) ) assert [d.external_id for d in by_metadata] == [ext_id] by_prefix = sync_client.datasets.filter( - datahub_sdk.DatasetFilter( - datahub_sdk.BasicDatasetFilter(external_id=f"{ext_id}*") + intellistream_datahub_sdk.DatasetFilter( + intellistream_datahub_sdk.BasicDatasetFilter(external_id=f"{ext_id}*") ) ) assert [d.external_id for d in by_prefix] == [ext_id] @@ -178,12 +178,12 @@ def test_sync_search(sync_client, make_dataset): assert any(d.external_id == ext_id for d in hits) # A query under the server's 3-character minimum is rejected, not matched loosely. - with pytest.raises(datahub_sdk.DataHubException): + with pytest.raises(intellistream_datahub_sdk.DataHubException): sync_client.datasets.search("ab") # An underscore is outside the allowed charset — this is what stops an # external id from being usable as a query. - with pytest.raises(datahub_sdk.DataHubException): + with pytest.raises(intellistream_datahub_sdk.DataHubException): sync_client.datasets.search(f"sdk_{token}") @@ -205,10 +205,10 @@ def test_sync_update(sync_client, make_dataset): updated = sync_client.datasets.update( [ - datahub_sdk.DatasetUpdate( + intellistream_datahub_sdk.DatasetUpdate( ext_id, - description=datahub_sdk.FieldStr("after"), - metadata=datahub_sdk.MapField.delta(add={"owner": "sdk_tests"}), + description=intellistream_datahub_sdk.FieldStr("after"), + metadata=intellistream_datahub_sdk.MapField.delta(add={"owner": "sdk_tests"}), ) ] ) @@ -236,8 +236,8 @@ async def test_async_filter_search_update_policies(async_client, make_dataset): ) narrowed = await async_client.datasets.filter( - datahub_sdk.DatasetFilter( - datahub_sdk.BasicDatasetFilter(external_id=[ext_id]) + intellistream_datahub_sdk.DatasetFilter( + intellistream_datahub_sdk.BasicDatasetFilter(external_id=[ext_id]) ) ) assert [d.external_id for d in narrowed] == [ext_id] @@ -247,8 +247,8 @@ async def test_async_filter_search_update_policies(async_client, make_dataset): updated = await async_client.datasets.update( [ - datahub_sdk.DatasetUpdate( - ext_id, description=datahub_sdk.FieldStr("after") + intellistream_datahub_sdk.DatasetUpdate( + ext_id, description=intellistream_datahub_sdk.FieldStr("after") ) ] ) diff --git a/python_tests/test_edges.py b/python_tests/test_edges.py index 2e4c77c..90e9e74 100644 --- a/python_tests/test_edges.py +++ b/python_tests/test_edges.py @@ -4,7 +4,7 @@ effect of `resources.create(nodes, relations)`; this service is for linking resources that already exist, and for reading, deleting or cataloguing relationships on their own. """ -import datahub_sdk +import intellistream_datahub_sdk import pytest from fixtures import async_client, sync_client, unique_id @@ -42,24 +42,24 @@ def test_get_by_ids_and_delete(sync_client): # `labels` may not be null on create. `is_root` anchors the component to the graph # root: without one the projection picks an anchor itself, non-deterministically, so # the stranding check below would name a different victim between runs. - datahub_sdk.Resource( + intellistream_datahub_sdk.Resource( external_id=a, name="python edge node a", labels=["ASSET"], is_root=True ), - datahub_sdk.Resource( + intellistream_datahub_sdk.Resource( external_id=b, name="python edge node b", labels=["ASSET"] ), - datahub_sdk.Resource( + intellistream_datahub_sdk.Resource( external_id=c, name="python edge node c", labels=["ASSET"] ), ], [ - datahub_sdk.RelForm( + intellistream_datahub_sdk.RelForm( relationship_type="SDK_TEST_LINK", from_external_id=a, to_external_id=b ), - datahub_sdk.RelForm( + intellistream_datahub_sdk.RelForm( relationship_type="SDK_TEST_LINK", from_external_id=b, to_external_id=c ), - datahub_sdk.RelForm( + intellistream_datahub_sdk.RelForm( relationship_type="SDK_TEST_LINK", from_external_id=a, to_external_id=c ), ], @@ -111,7 +111,7 @@ def edge_between(frm, to): "the deleted link is gone", lambda n: all(e.id != a_to_c for e in n.edges), ) - with pytest.raises(datahub_sdk.DataHubException) as excinfo: + with pytest.raises(intellistream_datahub_sdk.DataHubException) as excinfo: sync_client.edges.delete([b_to_c]) message = str(excinfo.value) assert "disconnect" in message, message @@ -145,8 +145,8 @@ def test_create_type_normalises_the_name(sync_client): # There is no delete-type endpoint, so this name is seeded once and reused across runs. name = "Sdk Py Test Rel Type" try: - sync_client.edges.create_types([datahub_sdk.RelTypeForm(name, description="from pytest")]) - except datahub_sdk.DataHubException: + sync_client.edges.create_types([intellistream_datahub_sdk.RelTypeForm(name, description="from pytest")]) + except intellistream_datahub_sdk.DataHubException: # Already seeded by an earlier run. Once the server returns 409 for a duplicate this is # the expected path; today it answers 200 with nothing. Either way the type must exist. pass @@ -157,30 +157,30 @@ def test_create_type_normalises_the_name(sync_client): def test_unusable_type_name_is_rejected(sync_client): - with pytest.raises(datahub_sdk.DataHubException): - sync_client.edges.create_types([datahub_sdk.RelTypeForm("!!!")]) + with pytest.raises(intellistream_datahub_sdk.DataHubException): + sync_client.edges.create_types([intellistream_datahub_sdk.RelTypeForm("!!!")]) def test_create_between_existing_resources(sync_client): a, b = unique_id("edge_link_a"), unique_id("edge_link_b") sync_client.resources.create( [ - datahub_sdk.Resource( + intellistream_datahub_sdk.Resource( external_id=a, name="python edge link a", labels=["ASSET"], is_root=True ), - datahub_sdk.Resource( + intellistream_datahub_sdk.Resource( external_id=b, name="python edge link b", labels=["ASSET"] ), ], [], ) - form = datahub_sdk.RelForm( + form = intellistream_datahub_sdk.RelForm( relationship_type="SDK_TEST_LINK", from_external_id=a, to_external_id=b ) try: try: created = sync_client.edges.create([form]) - except datahub_sdk.DataHubException as exc: + except intellistream_datahub_sdk.DataHubException as exc: # POST /edges/create is newer than some backends; skip rather than fail there. if "405" in str(exc): pytest.skip("this backend has no POST /edges/create") @@ -191,7 +191,7 @@ def test_create_between_existing_resources(sync_client): assert edge_id is not None # (start, end, type) is unique, so the same link again conflicts. - with pytest.raises(datahub_sdk.DataHubException): + with pytest.raises(intellistream_datahub_sdk.DataHubException): sync_client.edges.create([form]) # No separate edge delete here: this link is b's only route to the graph root, so removing diff --git a/python_tests/test_entity_fields.py b/python_tests/test_entity_fields.py index a03a07e..01b3850 100644 --- a/python_tests/test_entity_fields.py +++ b/python_tests/test_entity_fields.py @@ -1,4 +1,4 @@ -"""Pure-unit tests for the entity classes exposed by the `datahub_sdk` bindings. +"""Pure-unit tests for the entity classes exposed by the `intellistream_datahub_sdk` bindings. Unlike the rest of the suite, these tests do **not** touch a live backend or use the client fixtures. They only exercise the Python surface of the value objects: @@ -16,7 +16,7 @@ """ import datetime -import datahub_sdk as dh +import intellistream_datahub_sdk as dh import pytest diff --git a/python_tests/test_event_vocabulary.py b/python_tests/test_event_vocabulary.py index 4f7be68..fa84091 100644 --- a/python_tests/test_event_vocabulary.py +++ b/python_tests/test_event_vocabulary.py @@ -4,17 +4,17 @@ tenant actually use?" for the four categorical event fields, so they back filter dropdowns and autocompletes. They return plain strings, not events. """ -import datahub_sdk +import intellistream_datahub_sdk import pytest from fixtures import async_client, sync_client DIMENSIONS = [ - datahub_sdk.EventDimension.TYPE, - datahub_sdk.EventDimension.SUB_TYPE, - datahub_sdk.EventDimension.STATUS, - datahub_sdk.EventDimension.SOURCE, + intellistream_datahub_sdk.EventDimension.TYPE, + intellistream_datahub_sdk.EventDimension.SUB_TYPE, + intellistream_datahub_sdk.EventDimension.STATUS, + intellistream_datahub_sdk.EventDimension.SOURCE, ] @@ -33,16 +33,16 @@ def test_list_dimensions_are_distinct(sync_client): def test_named_helpers_match_the_generic_form(sync_client): assert sync_client.events.list_types() == sync_client.events.list_dimension( - datahub_sdk.EventDimension.TYPE + intellistream_datahub_sdk.EventDimension.TYPE ) assert sync_client.events.list_sub_types() == sync_client.events.list_dimension( - datahub_sdk.EventDimension.SUB_TYPE + intellistream_datahub_sdk.EventDimension.SUB_TYPE ) assert sync_client.events.list_statuses() == sync_client.events.list_dimension( - datahub_sdk.EventDimension.STATUS + intellistream_datahub_sdk.EventDimension.STATUS ) assert sync_client.events.list_sources() == sync_client.events.list_dimension( - datahub_sdk.EventDimension.SOURCE + intellistream_datahub_sdk.EventDimension.SOURCE ) @@ -106,7 +106,7 @@ async def test_async_vocabulary(async_client): assert isinstance(types, list) assert types == await async_client.events.list_dimension( - datahub_sdk.EventDimension.TYPE + intellistream_datahub_sdk.EventDimension.TYPE ) if types: diff --git a/python_tests/test_events.py b/python_tests/test_events.py index f0707a9..5dd176c 100644 --- a/python_tests/test_events.py +++ b/python_tests/test_events.py @@ -2,7 +2,7 @@ from datetime import datetime, timedelta from time import sleep -import datahub_sdk +import intellistream_datahub_sdk import pandas as pd import pytest from pytest_asyncio import fixture @@ -23,8 +23,8 @@ def event_dataset(sync_client): # sweep can reclaim these + all derived events if a run is killed before # teardown. The events below inherit this prefix through their external ids. dataset_name = unique_id("event_dataset") - sync_client.datasets.delete([datahub_sdk.Dataset(external_id=dataset_name)]) - event_dataset = sync_client.datasets.create([datahub_sdk.Dataset(external_id=dataset_name)])[0] + sync_client.datasets.delete([intellistream_datahub_sdk.Dataset(external_id=dataset_name)]) + event_dataset = sync_client.datasets.create([intellistream_datahub_sdk.Dataset(external_id=dataset_name)])[0] yield event_dataset sync_client.datasets.delete([event_dataset]) @pytest.fixture(scope="module") @@ -41,7 +41,7 @@ def test_events(sync_client,event_dataset): sub_type = f"{event_dataset.external_id}_test_event_{i}_sub_type" related_resources = [] source = f"{event_dataset.external_id}_test_event_{i} source" - events.append(datahub_sdk.Event(external_id=external_id, + events.append(intellistream_datahub_sdk.Event(external_id=external_id, metadata=metadata, description=description, type=type, @@ -71,7 +71,7 @@ def test_events_func_scope(sync_client,event_dataset): data_set_id = event_dataset_id related_resources = [] source = f"{event_dataset.external_id}_func_scope_test_event_{i} source" - events.append(datahub_sdk.Event(external_id=external_id, + events.append(intellistream_datahub_sdk.Event(external_id=external_id, metadata=metadata, description=description, type=type, @@ -129,8 +129,8 @@ def test_filter_by_external_id_prefix(sync_client, test_events,event_dataset): target_string = f"{event_dataset.external_id}_test_event_5" # `external_id_prefix` is gone; a trailing `*` asks the same question in the field that # also takes exact ids. - basic_filter = datahub_sdk.BasicEventFilter(external_id=f"{target_string}*") - filt = datahub_sdk.EventFilter(basic_filter=basic_filter) + basic_filter = intellistream_datahub_sdk.BasicEventFilter(external_id=f"{target_string}*") + filt = intellistream_datahub_sdk.EventFilter(basic_filter=basic_filter) results = poll_until(lambda: sync_client.events.filter(filt), lambda r: len(r) >= 1) assert len(results) >= 1 @@ -142,10 +142,10 @@ def test_filter_by_type(sync_client, test_events): # by external_id — the /events/filter endpoint can echo the same row twice # under indexing lag, so an exact ``== 1`` count is race-prone. target = test_events[10] - basic_filter = datahub_sdk.BasicEventFilter( + basic_filter = intellistream_datahub_sdk.BasicEventFilter( type=[target.type], ) - filt = datahub_sdk.EventFilter(basic_filter=basic_filter) + filt = intellistream_datahub_sdk.EventFilter(basic_filter=basic_filter) results = poll_until( lambda: sync_client.events.filter(filt), @@ -155,10 +155,10 @@ def test_filter_by_type(sync_client, test_events): assert all(e.type == target.type for e in results) def test_filter_by_sub_type(sync_client, test_events): target = test_events[99] - basic_filter = datahub_sdk.BasicEventFilter( + basic_filter = intellistream_datahub_sdk.BasicEventFilter( sub_type=[target.sub_type], ) - filt = datahub_sdk.EventFilter(basic_filter=basic_filter) + filt = intellistream_datahub_sdk.EventFilter(basic_filter=basic_filter) results = poll_until( lambda: sync_client.events.filter(filt), @@ -168,20 +168,20 @@ def test_filter_by_sub_type(sync_client, test_events): assert all(e.sub_type == target.sub_type for e in results) @pytest.mark.parametrize("time_filter,expected_idx", [ - (datahub_sdk.TimeFilter( + (intellistream_datahub_sdk.TimeFilter( start=pd.Timestamp("2025-01-02", tz="UTC"), end=pd.Timestamp("2025-01-04", tz="UTC")), slice(1,3)), - (datahub_sdk.TimeFilter( + (intellistream_datahub_sdk.TimeFilter( start=pd.Timestamp("2025-01-03", tz="UTC")), slice(3, None)), - (datahub_sdk.TimeFilter( + (intellistream_datahub_sdk.TimeFilter( end=pd.Timestamp("2025-01-03", tz="UTC")), slice(None,3)), ]) def test_filter_by_event_time_range(sync_client, test_events,time_filter,expected_idx): # Events are 1 day apart. Filter for the first 3 days. - basic_filter = datahub_sdk.BasicEventFilter(event_time=time_filter) - filt = datahub_sdk.EventFilter(basic_filter=basic_filter) + basic_filter = intellistream_datahub_sdk.BasicEventFilter(event_time=time_filter) + filt = intellistream_datahub_sdk.EventFilter(basic_filter=basic_filter) # The time filter isn't dataset-scoped, so we only assert the window returns something # (poll past ingestion lag) rather than pinning exact membership. @@ -194,8 +194,8 @@ def test_filter_by_metadata(sync_client, test_events,target_idx): target = test_events[target_idx] target_metadata = target.metadata - basic_filter = datahub_sdk.BasicEventFilter(metadata=target_metadata) - filt = datahub_sdk.EventFilter(basic_filter=basic_filter) + basic_filter = intellistream_datahub_sdk.BasicEventFilter(metadata=target_metadata) + filt = intellistream_datahub_sdk.EventFilter(basic_filter=basic_filter) results = poll_until( lambda: sync_client.events.filter(filt), @@ -215,10 +215,10 @@ def test_filter_by_source(sync_client, test_events): # searchable description column, and the field was accepted and ignored. Use /events/search # for free text. target = test_events[7] - basic_filter = datahub_sdk.BasicEventFilter( + basic_filter = intellistream_datahub_sdk.BasicEventFilter( source=[target.source], ) - filt = datahub_sdk.EventFilter(basic_filter=basic_filter) + filt = intellistream_datahub_sdk.EventFilter(basic_filter=basic_filter) results = poll_until( lambda: sync_client.events.filter(filt), @@ -228,9 +228,9 @@ def test_filter_by_source(sync_client, test_events): assert all(e.source == target.source for e in results) def test_filter_with_limit(sync_client, test_events,event_dataset): - basic_filter = datahub_sdk.BasicEventFilter(external_id=f"{event_dataset.external_id}*") + basic_filter = intellistream_datahub_sdk.BasicEventFilter(external_id=f"{event_dataset.external_id}*") # Using the EventFilter limit field - filt = datahub_sdk.EventFilter(basic_filter=basic_filter, limit=5) + filt = intellistream_datahub_sdk.EventFilter(basic_filter=basic_filter, limit=5) results = poll_until(lambda: sync_client.events.filter(filt), lambda r: len(r) == 5) assert len(results) == 5 @@ -240,8 +240,8 @@ def test_filter_by_data_set_ids(sync_client, test_events, event_dataset): # dataSetIds must go over the wire as the backend's List ([{"id": ...}]); a flat id # array is rejected with HTTP 400. All fixture events live in event_dataset. target = test_events[0] - basic_filter = datahub_sdk.BasicEventFilter(data_set_id=[event_dataset.id]) - filt = datahub_sdk.EventFilter(basic_filter=basic_filter) + basic_filter = intellistream_datahub_sdk.BasicEventFilter(data_set_id=[event_dataset.id]) + filt = intellistream_datahub_sdk.EventFilter(basic_filter=basic_filter) results = poll_until( lambda: sync_client.events.filter(filt), @@ -258,26 +258,26 @@ def test_filter_by_related_resources(sync_client, event_dataset): # the external id. res_ext = unique_id("evfilt_res") sync_client.resources.delete([res_ext]) - sync_client.resources.create([datahub_sdk.Resource( + sync_client.resources.create([intellistream_datahub_sdk.Resource( external_id=res_ext, name="ev filter res", is_root=True, labels=["ASSET"])]) res = next(r for r in sync_client.resources.by_ids([res_ext]) if r.external_id == res_ext) ev_ext = unique_id("evfilt_ev") # Attach by external id only; the backend resolves the numeric id and returns both sides. - sync_client.events.create([datahub_sdk.Event(type="test", external_id=ev_ext, event_time=pd.Timestamp.now(tz="UTC"), + sync_client.events.create([intellistream_datahub_sdk.Event(type="test", external_id=ev_ext, event_time=pd.Timestamp.now(tz="UTC"), data_set_id=event_dataset.id, - related_resources=[datahub_sdk.IdCollection(external_id=res_ext)])]) + related_resources=[intellistream_datahub_sdk.IdCollection(external_id=res_ext)])]) try: - by_id = datahub_sdk.EventFilter( - basic_filter=datahub_sdk.BasicEventFilter( - related_resources=[datahub_sdk.IdCollection(id=res.id)])) + by_id = intellistream_datahub_sdk.EventFilter( + basic_filter=intellistream_datahub_sdk.BasicEventFilter( + related_resources=[intellistream_datahub_sdk.IdCollection(id=res.id)])) r1 = poll_until(lambda: sync_client.events.filter(by_id), lambda r: ev_ext in {e.external_id for e in r}) assert ev_ext in {e.external_id for e in r1}, "filter by related resource id did not find the event" - by_ext = datahub_sdk.EventFilter( - basic_filter=datahub_sdk.BasicEventFilter( - related_resources=[datahub_sdk.IdCollection(external_id=res_ext)])) + by_ext = intellistream_datahub_sdk.EventFilter( + basic_filter=intellistream_datahub_sdk.BasicEventFilter( + related_resources=[intellistream_datahub_sdk.IdCollection(external_id=res_ext)])) r2 = poll_until(lambda: sync_client.events.filter(by_ext), lambda r: ev_ext in {e.external_id for e in r}) assert ev_ext in {e.external_id for e in r2}, "filter by related resource external id did not find the event" @@ -297,17 +297,17 @@ def test_filter_by_created_time(sync_client, test_events, event_dataset): now = pd.Timestamp.now(tz="UTC") day = pd.Timedelta(days=1) - after = datahub_sdk.EventFilter(basic_filter=datahub_sdk.BasicEventFilter( + after = intellistream_datahub_sdk.EventFilter(basic_filter=intellistream_datahub_sdk.BasicEventFilter( external_id=f"{event_dataset.external_id}*", - created_time=datahub_sdk.TimeFilter(start=now - day))) + created_time=intellistream_datahub_sdk.TimeFilter(start=now - day))) r = poll_until(lambda: sync_client.events.filter(after), lambda r: target.external_id in {e.external_id for e in r}) assert target.external_id in {e.external_id for e in r}, "created_time (after) filter did not find the event" # Now that we know it's propagated, the complementary window must exclude it. - before = datahub_sdk.EventFilter(basic_filter=datahub_sdk.BasicEventFilter( + before = intellistream_datahub_sdk.EventFilter(basic_filter=intellistream_datahub_sdk.BasicEventFilter( external_id=f"{event_dataset.external_id}*", - created_time=datahub_sdk.TimeFilter(end=now - day))) + created_time=intellistream_datahub_sdk.TimeFilter(end=now - day))) r_before = sync_client.events.filter(before) assert target.external_id not in {e.external_id for e in r_before}, ( "created_time (before yesterday) must not return a just-created event" @@ -320,16 +320,16 @@ def test_filter_by_last_updated_time(sync_client, test_events, event_dataset): now = pd.Timestamp.now(tz="UTC") day = pd.Timedelta(days=1) - after = datahub_sdk.EventFilter(basic_filter=datahub_sdk.BasicEventFilter( + after = intellistream_datahub_sdk.EventFilter(basic_filter=intellistream_datahub_sdk.BasicEventFilter( external_id=f"{event_dataset.external_id}*", - last_updated_time=datahub_sdk.TimeFilter(start=now - day))) + last_updated_time=intellistream_datahub_sdk.TimeFilter(start=now - day))) r = poll_until(lambda: sync_client.events.filter(after), lambda r: target.external_id in {e.external_id for e in r}) assert target.external_id in {e.external_id for e in r}, "last_updated_time (after) filter did not find the event" - before = datahub_sdk.EventFilter(basic_filter=datahub_sdk.BasicEventFilter( + before = intellistream_datahub_sdk.EventFilter(basic_filter=intellistream_datahub_sdk.BasicEventFilter( external_id=f"{event_dataset.external_id}*", - last_updated_time=datahub_sdk.TimeFilter(end=now - day))) + last_updated_time=intellistream_datahub_sdk.TimeFilter(end=now - day))) r_before = sync_client.events.filter(before) assert target.external_id not in {e.external_id for e in r_before}, ( "last_updated_time (before yesterday) must not return a just-created event" @@ -345,7 +345,7 @@ def test_filter_by_last_updated_time(sync_client, test_events, event_dataset): @pytest.fixture(scope="function") def single_event(sync_client, event_dataset): external_id = f"{event_dataset.external_id}_uuid_event_{uuid.uuid4().hex}" - ev = datahub_sdk.Event(type="test", external_id=external_id, + ev = intellistream_datahub_sdk.Event(type="test", external_id=external_id, data_set_id=event_dataset.id, event_time=pd.Timestamp("2025-01-01", tz="UTC"), ) @@ -362,7 +362,7 @@ def test_created_event_has_uuid_v7_id(single_event): def test_by_ids_with_uuid_collection(sync_client, single_event): - selector = datahub_sdk.EventIdCollection(id=single_event.id) + selector = intellistream_datahub_sdk.EventIdCollection(id=single_event.id) fetched = poll_until(lambda: sync_client.events.by_ids([selector]), lambda r: len(r) == 1, timeout=10) assert len(fetched) == 1 assert fetched[0].id == single_event.id @@ -379,7 +379,7 @@ def test_by_ids_with_bare_uuid(sync_client, single_event): def test_delete_by_uuid(sync_client, single_event): # Confirm the event is queryable (read-after-write), then delete it by its UUID. poll_until(lambda: sync_client.events.by_ids([single_event.id]), lambda r: len(r) == 1, timeout=10) - sync_client.events.delete([datahub_sdk.EventIdCollection(id=single_event.id)]) + sync_client.events.delete([intellistream_datahub_sdk.EventIdCollection(id=single_event.id)]) remaining = poll_until(lambda: sync_client.events.by_ids([single_event.id]), lambda r: r == [], timeout=10) assert remaining == [] @@ -392,7 +392,7 @@ def test_delete_by_uuid(sync_client, single_event): def test_event_id_collection_requires_an_identifier(): # Constructing with neither an id nor an external_id is a usage error. with pytest.raises(ValueError): - datahub_sdk.EventIdCollection() + intellistream_datahub_sdk.EventIdCollection() # --------------------------------------------------------------------------- @@ -422,7 +422,7 @@ def dimension_events(sync_client, event_dataset): events = [] base = pd.Timestamp("2024-06-01", tz="UTC") for i in range(3): - events.append(datahub_sdk.Event(external_id=f"{event_dataset.external_id}_dim_event_{token}_{i}", + events.append(intellistream_datahub_sdk.Event(external_id=f"{event_dataset.external_id}_dim_event_{token}_{i}", description=f"dimension event {token} number {i}", type=dim["type"], sub_type=dim["sub_type"], @@ -464,10 +464,10 @@ def test_get_missing_returns_none(sync_client): def test_update_event(sync_client, single_event): # Make sure the event is queryable before updating it. poll_until(lambda: sync_client.events.get(single_event.id), lambda e: e is not None) - update = datahub_sdk.EventUpdate( + update = intellistream_datahub_sdk.EventUpdate( single_event, - status=datahub_sdk.FieldStr(value="acknowledged"), - description=datahub_sdk.FieldStr(value="updated by test"), + status=intellistream_datahub_sdk.FieldStr(value="acknowledged"), + description=intellistream_datahub_sdk.FieldStr(value="updated by test"), ) def _updated(result): @@ -487,7 +487,7 @@ def _updated(result): def test_search_by_description(sync_client, dimension_events): token = dimension_events["token"] results = poll_until( - lambda: sync_client.events.search(datahub_sdk.EventSearch(query=token)), + lambda: sync_client.events.search(intellistream_datahub_sdk.EventSearch(query=token)), lambda r: len(r) >= 1, **POLL_SEARCH, ) @@ -498,8 +498,8 @@ def test_search_by_description(sync_client, dimension_events): def test_search_with_filter_and_limit(sync_client, dimension_events): # The free-text search can be narrowed with a BasicEventFilter and capped. token = dimension_events["token"] - basic_filter = datahub_sdk.BasicEventFilter(type=[dimension_events["type"]]) - search = datahub_sdk.EventSearch(query=token, filter=basic_filter, limit=2) + basic_filter = intellistream_datahub_sdk.BasicEventFilter(type=[dimension_events["type"]]) + search = intellistream_datahub_sdk.EventSearch(query=token, filter=basic_filter, limit=2) results = poll_until(lambda: sync_client.events.search(search), lambda r: len(r) >= 1, **POLL_SEARCH) assert len(results) >= 1 # not a vacuous pass on an empty result assert len(results) <= 2 diff --git a/python_tests/test_files.py b/python_tests/test_files.py index a754008..05efc3a 100644 --- a/python_tests/test_files.py +++ b/python_tests/test_files.py @@ -7,7 +7,7 @@ """ import os -import datahub_sdk +import intellistream_datahub_sdk import pytest from fixtures import async_client, make_dataset, make_resource, sync_client, unique_id @@ -31,7 +31,7 @@ def test_upload_and_list(sync_client): except Exception: pass - upload = datahub_sdk.FileUpload( + upload = intellistream_datahub_sdk.FileUpload( path=_IMAGE_PATH, destination_path="/images/", external_id=ext_id, @@ -89,7 +89,7 @@ def test_get_search_update_download_trash_restore(sync_client, tmp_path): with open(_IMAGE_PATH, "rb") as handle: source_bytes = handle.read() - upload = datahub_sdk.FileUpload( + upload = intellistream_datahub_sdk.FileUpload( path=_IMAGE_PATH, destination_path="/pylifecycle/", external_id=ext_id, @@ -122,7 +122,7 @@ def test_get_search_update_download_trash_restore(sync_client, tmp_path): assert destination.read_bytes() == source_bytes updated = sync_client.files.update( - datahub_sdk.FileUpdate( + intellistream_datahub_sdk.FileUpdate( external_id=ext_id, name="renamed.jpg", path="/pylifecycle/moved", @@ -156,7 +156,7 @@ def test_get_search_update_download_trash_restore(sync_client, tmp_path): def test_file_update_requires_a_selector(): with pytest.raises(ValueError): - datahub_sdk.FileUpdate(name="renamed.jpg") + intellistream_datahub_sdk.FileUpdate(name="renamed.jpg") # --------------------------------------------------------------------------- # @@ -179,7 +179,7 @@ def uploaded_file(sync_client): except Exception: pass - upload = datahub_sdk.FileUpload( + upload = intellistream_datahub_sdk.FileUpload( path=_IMAGE_PATH, destination_path="/pyupdatefields/", external_id=ext_id, @@ -198,7 +198,7 @@ def uploaded_file(sync_client): def test_update_source_and_metadata(sync_client, uploaded_file): - updated = sync_client.files.update(datahub_sdk.FileUpdate( + updated = sync_client.files.update(intellistream_datahub_sdk.FileUpdate( external_id=uploaded_file.external_id, source="updated_source", metadata={"b": "2"}, @@ -212,7 +212,7 @@ def test_update_source_and_metadata(sync_client, uploaded_file): def test_update_data_set_id(sync_client, uploaded_file, make_dataset): dataset = make_dataset(name=unique_id("file_upd_ds")) - updated = sync_client.files.update(datahub_sdk.FileUpdate( + updated = sync_client.files.update(intellistream_datahub_sdk.FileUpdate( external_id=uploaded_file.external_id, data_set_id=dataset.id ))[0] @@ -222,12 +222,12 @@ def test_update_data_set_id(sync_client, uploaded_file, make_dataset): def test_update_related_resources(sync_client, uploaded_file, make_resource): ext = unique_id("file_upd_res") created = make_resource([ - datahub_sdk.Resource(external_id=ext, name="File update probe", + intellistream_datahub_sdk.Resource(external_id=ext, name="File update probe", is_root=True, labels=["ASSET"]) ]) resource_id = created.nodes[0].id - updated = sync_client.files.update(datahub_sdk.FileUpdate( + updated = sync_client.files.update(intellistream_datahub_sdk.FileUpdate( external_id=uploaded_file.external_id, related_resources=[resource_id] ))[0] @@ -236,7 +236,7 @@ def test_update_related_resources(sync_client, uploaded_file, make_resource): def test_update_leaves_omitted_fields_unchanged(sync_client, uploaded_file): """There is no clear-it form; an omitted field is the "leave it alone" instruction.""" - updated = sync_client.files.update(datahub_sdk.FileUpdate( + updated = sync_client.files.update(intellistream_datahub_sdk.FileUpdate( external_id=uploaded_file.external_id, description="only the description moves" ))[0] diff --git a/python_tests/test_filter_datasets.py b/python_tests/test_filter_datasets.py index 5227e1e..079b61f 100644 --- a/python_tests/test_filter_datasets.py +++ b/python_tests/test_filter_datasets.py @@ -21,8 +21,8 @@ """ import pytest -import datahub_sdk -from datahub_sdk import DataHubException +import intellistream_datahub_sdk +from intellistream_datahub_sdk import DataHubException from fixtures import async_client, sync_client, unique_id # noqa: F401 (fixtures) from filter_fixtures import datasets, prefix, token # noqa: F401 (fixtures) @@ -37,7 +37,7 @@ def flt(sync_client, prefix): """Filter within this run's corpus unless the test overrides ``external_id``.""" def _filter(limit=None, **criteria): criteria.setdefault("external_id", f"{prefix}*") - form = datahub_sdk.DatasetFilter(datahub_sdk.BasicDatasetFilter(**criteria), limit=limit) + form = intellistream_datahub_sdk.DatasetFilter(intellistream_datahub_sdk.BasicDatasetFilter(**criteria), limit=limit) return externals(sync_client.datasets.filter(form)) return _filter @@ -150,7 +150,7 @@ def test_the_retired_flags_are_not_accepted(sync_client): """ for retired in ["write_protected", "deactivated"]: with pytest.raises(TypeError): - datahub_sdk.BasicDatasetFilter(**{retired: False}) + intellistream_datahub_sdk.BasicDatasetFilter(**{retired: False}) # --------------------------------------------------------------------------- # @@ -160,7 +160,7 @@ def test_the_retired_flags_are_not_accepted(sync_client): def test_an_argument_free_filter_returns_everything(sync_client, datasets, both): """The same thing ``/datasets/list`` does — the server implements ``list`` by calling the filter handler with an empty filter.""" - from_filter = externals(sync_client.datasets.filter(datahub_sdk.DatasetFilter())) + from_filter = externals(sync_client.datasets.filter(intellistream_datahub_sdk.DatasetFilter())) assert from_filter >= both assert externals(sync_client.datasets.list(limit=10_000)) >= both @@ -188,8 +188,8 @@ def test_an_unmatchable_criterion_gives_an_empty_result_not_an_unfiltered_one(fl # --------------------------------------------------------------------------- # def test_limit_caps_the_page(sync_client, datasets, prefix): - form = datahub_sdk.DatasetFilter( - datahub_sdk.BasicDatasetFilter(external_id=f"{prefix}*"), limit=1) + form = intellistream_datahub_sdk.DatasetFilter( + intellistream_datahub_sdk.BasicDatasetFilter(external_id=f"{prefix}*"), limit=1) assert len(sync_client.datasets.filter(form)) == 1 @@ -198,8 +198,8 @@ def test_limit_zero_falls_back_to_the_default(flt, both): def test_a_limit_above_the_ceiling_is_refused(sync_client, prefix): - form = datahub_sdk.DatasetFilter( - datahub_sdk.BasicDatasetFilter(external_id=f"{prefix}*"), limit=10_001) + form = intellistream_datahub_sdk.DatasetFilter( + intellistream_datahub_sdk.BasicDatasetFilter(external_id=f"{prefix}*"), limit=10_001) with pytest.raises(DataHubException) as excinfo: sync_client.datasets.filter(form) assert excinfo.value.status_code == 400 @@ -212,11 +212,11 @@ def test_the_retired_criteria_are_not_accepted(sync_client): for retired, value in [("external_id_prefix", "sap_"), ("ids", [1]), ("external_ids", ["sap_*"]), ("names", ["SAP*"]), ("sources", ["sap"])]: with pytest.raises(TypeError): - datahub_sdk.BasicDatasetFilter(**{retired: value}) + intellistream_datahub_sdk.BasicDatasetFilter(**{retired: value}) @pytest.mark.asyncio async def test_async_filter_matches_the_sync_one(async_client, sync_client, datasets, prefix): - form = datahub_sdk.DatasetFilter(datahub_sdk.BasicDatasetFilter(external_id=f"{prefix}*")) + form = intellistream_datahub_sdk.DatasetFilter(intellistream_datahub_sdk.BasicDatasetFilter(external_id=f"{prefix}*")) assert externals(await async_client.datasets.filter(form)) == externals( sync_client.datasets.filter(form)) diff --git a/python_tests/test_filter_events.py b/python_tests/test_filter_events.py index 9009ed4..b0903bd 100644 --- a/python_tests/test_filter_events.py +++ b/python_tests/test_filter_events.py @@ -28,8 +28,8 @@ import pandas as pd import pytest -import datahub_sdk -from datahub_sdk import DataHubException +import intellistream_datahub_sdk +from intellistream_datahub_sdk import DataHubException from fixtures import async_client, sync_client, unique_id # noqa: F401 (fixtures) from filter_fixtures import datasets, event_corpus, prefix, token # noqa: F401 (fixtures) @@ -50,8 +50,8 @@ def flt(sync_client, prefix): """ def _filter(limit=None, expect_rows=True, **criteria): criteria.setdefault("external_id", f"{prefix}*") - request = datahub_sdk.EventFilter( - datahub_sdk.BasicEventFilter(**criteria), limit=limit or 100) + request = intellistream_datahub_sdk.EventFilter( + intellistream_datahub_sdk.BasicEventFilter(**criteria), limit=limit or 100) def fetch(): return externals(sync_client.events.filter(request)) @@ -211,7 +211,7 @@ def test_data_set_scope_by_id_and_by_external_id(flt, event_corpus, datasets, al _parent, child = datasets assert flt(data_set_id=[child.id]) == {alarm} assert flt(data_set_id=[child.external_id]) == {alarm} - assert flt(data_set_id=[datahub_sdk.IdCollection(id=child.id)]) == {alarm} + assert flt(data_set_id=[intellistream_datahub_sdk.IdCollection(id=child.id)]) == {alarm} def test_a_parent_data_set_stands_in_for_its_children(flt, event_corpus, datasets, alarm, warning): @@ -226,8 +226,8 @@ def test_empty_and_absent_data_set_scopes_are_opposites(sync_client, event_corpu """``None`` is "no data set restriction", ``[]`` is "narrow to no data sets". Opposite answers, so the SDK has to keep them apart all the way onto the wire.""" def run(data_set_ids): - return externals(sync_client.events.filter(datahub_sdk.EventFilter( - datahub_sdk.BasicEventFilter(external_id=f"{prefix}*", data_set_id=data_set_ids)))) + return externals(sync_client.events.filter(intellistream_datahub_sdk.EventFilter( + intellistream_datahub_sdk.BasicEventFilter(external_id=f"{prefix}*", data_set_id=data_set_ids)))) assert run([]) == set() assert poll_until(lambda: run(None), bool, timeout=10.0) == both @@ -262,39 +262,39 @@ def test_related_resources_must_all_be_attached(sync_client, datasets, token): except Exception: pass created = sync_client.resources.create([ - datahub_sdk.Resource(external_id=res_a, name=f"RR A {token}", labels=["ASSET"], + intellistream_datahub_sdk.Resource(external_id=res_a, name=f"RR A {token}", labels=["ASSET"], is_root=True, data_set_id=child.id), - datahub_sdk.Resource(external_id=res_b, name=f"RR B {token}", labels=["ASSET"], + intellistream_datahub_sdk.Resource(external_id=res_b, name=f"RR B {token}", labels=["ASSET"], is_root=True, data_set_id=child.id), ]) resource_id = {r.external_id: r.id for r in created.nodes} now = pd.Timestamp.now(tz="UTC") sync_client.events.create([ - datahub_sdk.Event(external_id=ev_both, type=f"rr_{token}", event_time=now, + intellistream_datahub_sdk.Event(external_id=ev_both, type=f"rr_{token}", event_time=now, data_set_id=child.id, - related_resources=[datahub_sdk.IdCollection(external_id=res_a), - datahub_sdk.IdCollection(external_id=res_b)]), - datahub_sdk.Event(external_id=ev_one, type=f"rr_{token}", event_time=now, + related_resources=[intellistream_datahub_sdk.IdCollection(external_id=res_a), + intellistream_datahub_sdk.IdCollection(external_id=res_b)]), + intellistream_datahub_sdk.Event(external_id=ev_one, type=f"rr_{token}", event_time=now, data_set_id=child.id, - related_resources=[datahub_sdk.IdCollection(external_id=res_a)]), + related_resources=[intellistream_datahub_sdk.IdCollection(external_id=res_a)]), ]) try: def run(related): - return externals(sync_client.events.filter(datahub_sdk.EventFilter( - datahub_sdk.BasicEventFilter(external_id=f"{own_prefix}_ev_*", + return externals(sync_client.events.filter(intellistream_datahub_sdk.EventFilter( + intellistream_datahub_sdk.BasicEventFilter(external_id=f"{own_prefix}_ev_*", related_resources=related)))) - by_a = [datahub_sdk.IdCollection(external_id=res_a)] + by_a = [intellistream_datahub_sdk.IdCollection(external_id=res_a)] assert poll_until(lambda: run(by_a), lambda r: len(r) >= 2, timeout=15.0) == {ev_both, ev_one} # Both required: only the event carrying both qualifies. - by_both = [datahub_sdk.IdCollection(external_id=res_a), - datahub_sdk.IdCollection(external_id=res_b)] + by_both = [intellistream_datahub_sdk.IdCollection(external_id=res_a), + intellistream_datahub_sdk.IdCollection(external_id=res_b)] assert run(by_both) == {ev_both} # A numeric id names the same resource as its external id. - assert run([datahub_sdk.IdCollection(id=resource_id[res_b])]) == {ev_both} + assert run([intellistream_datahub_sdk.IdCollection(id=resource_id[res_b])]) == {ev_both} finally: try: sync_client.events.delete([ev_both, ev_one]) @@ -320,17 +320,17 @@ def test_event_time_window_bounds_the_result(flt, event_corpus, alarm, warning): meaning wearing the name of the first. """ now = pd.Timestamp.now(tz="UTC") - assert flt(event_time=datahub_sdk.TimeFilter(start=now - pd.Timedelta(hours=1))) == {warning} - assert flt(event_time=datahub_sdk.TimeFilter(end=now - pd.Timedelta(hours=1))) == {alarm} - assert flt(event_time=datahub_sdk.TimeFilter( + assert flt(event_time=intellistream_datahub_sdk.TimeFilter(start=now - pd.Timedelta(hours=1))) == {warning} + assert flt(event_time=intellistream_datahub_sdk.TimeFilter(end=now - pd.Timedelta(hours=1))) == {alarm} + assert flt(event_time=intellistream_datahub_sdk.TimeFilter( start=now - pd.Timedelta(days=3), end=now + pd.Timedelta(hours=1))) == {alarm, warning} def test_created_time_is_ingest_time_not_event_time(flt, event_corpus, both): """Both events were ingested just now, however far apart their event times are.""" now = pd.Timestamp.now(tz="UTC") - assert flt(created_time=datahub_sdk.TimeFilter(start=now - pd.Timedelta(minutes=10))) == both - assert flt(created_time=datahub_sdk.TimeFilter(end=now - pd.Timedelta(days=1)), + assert flt(created_time=intellistream_datahub_sdk.TimeFilter(start=now - pd.Timedelta(minutes=10))) == both + assert flt(created_time=intellistream_datahub_sdk.TimeFilter(end=now - pd.Timedelta(days=1)), expect_rows=False) == set() @@ -342,7 +342,7 @@ def test_an_absent_filter_places_no_restriction(sync_client, event_corpus, both) """An argument-free filter returns the tenant's events, not none of them.""" everything = poll_until( lambda: externals(sync_client.events.filter( - datahub_sdk.EventFilter(limit=1000))), + intellistream_datahub_sdk.EventFilter(limit=1000))), lambda found: found >= both, timeout=15.0, ) @@ -352,8 +352,8 @@ def test_an_absent_filter_places_no_restriction(sync_client, event_corpus, both) @pytest.mark.parametrize("empty", [[], ["", " "]]) def test_empty_and_blank_lists_place_no_restriction(sync_client, event_corpus, prefix, both, empty): scoped = poll_until( - lambda: externals(sync_client.events.filter(datahub_sdk.EventFilter( - datahub_sdk.BasicEventFilter(external_id=f"{prefix}*", type=empty, + lambda: externals(sync_client.events.filter(intellistream_datahub_sdk.EventFilter( + intellistream_datahub_sdk.BasicEventFilter(external_id=f"{prefix}*", type=empty, sub_type=empty, status=empty, source=empty)))), bool, timeout=10.0, @@ -372,10 +372,10 @@ def test_garbled_criteria_match_nothing_without_erroring(flt, event_corpus): def test_a_bare_string_means_a_one_element_list(sync_client, event_corpus, prefix, alarm, token): - scalar = sync_client.events.filter(datahub_sdk.EventFilter( - datahub_sdk.BasicEventFilter(external_id=f"{prefix}*", type=f"alarm_{token}"))) - listed = sync_client.events.filter(datahub_sdk.EventFilter( - datahub_sdk.BasicEventFilter(external_id=[f"{prefix}*"], type=[f"alarm_{token}"]))) + scalar = sync_client.events.filter(intellistream_datahub_sdk.EventFilter( + intellistream_datahub_sdk.BasicEventFilter(external_id=f"{prefix}*", type=f"alarm_{token}"))) + listed = sync_client.events.filter(intellistream_datahub_sdk.EventFilter( + intellistream_datahub_sdk.BasicEventFilter(external_id=[f"{prefix}*"], type=[f"alarm_{token}"]))) assert externals(scalar) == externals(listed) == {alarm} @@ -385,8 +385,8 @@ def test_a_bare_string_means_a_one_element_list(sync_client, event_corpus, prefi def test_limit_caps_the_page(sync_client, event_corpus, prefix): capped = poll_until( - lambda: sync_client.events.filter(datahub_sdk.EventFilter( - datahub_sdk.BasicEventFilter(external_id=f"{prefix}*"), limit=1)), + lambda: sync_client.events.filter(intellistream_datahub_sdk.EventFilter( + intellistream_datahub_sdk.BasicEventFilter(external_id=f"{prefix}*"), limit=1)), bool, timeout=10.0, ) @@ -395,8 +395,8 @@ def test_limit_caps_the_page(sync_client, event_corpus, prefix): def test_a_limit_above_the_ceiling_is_refused(sync_client, prefix): with pytest.raises(DataHubException) as excinfo: - sync_client.events.filter(datahub_sdk.EventFilter( - datahub_sdk.BasicEventFilter(external_id=f"{prefix}*"), limit=10_001)) + sync_client.events.filter(intellistream_datahub_sdk.EventFilter( + intellistream_datahub_sdk.BasicEventFilter(external_id=f"{prefix}*"), limit=10_001)) assert excinfo.value.status_code == 400 @@ -404,8 +404,8 @@ def _ordered_page(sync_client, prefix, both, **sort): return [ event.external_id for event in poll_until( - lambda: sync_client.events.filter(datahub_sdk.EventFilter( - datahub_sdk.BasicEventFilter(external_id=f"{prefix}*"), limit=100, **sort)), + lambda: sync_client.events.filter(intellistream_datahub_sdk.EventFilter( + intellistream_datahub_sdk.BasicEventFilter(external_id=f"{prefix}*"), limit=100, **sort)), lambda found: externals(found) >= both, timeout=10.0, ) @@ -453,14 +453,14 @@ def test_the_default_order_is_event_time_then_id_ascending(sync_client, datasets externals_by_offset = {i: f"{own_prefix}_{i}" for i in range(6)} for offset in order_of_creation: - sync_client.events.create([datahub_sdk.Event( + sync_client.events.create([intellistream_datahub_sdk.Event( external_id=externals_by_offset[offset], type=f"ord_{token}", event_time=base + pd.Timedelta(minutes=offset), data_set_id=child.id)]) try: expected = [externals_by_offset[i] for i in range(6)] page = poll_until( - lambda: sync_client.events.filter(datahub_sdk.EventFilter( - datahub_sdk.BasicEventFilter(external_id=f"{own_prefix}*"), limit=100)), + lambda: sync_client.events.filter(intellistream_datahub_sdk.EventFilter( + intellistream_datahub_sdk.BasicEventFilter(external_id=f"{own_prefix}*"), limit=100)), lambda found: externals(found) == set(expected), timeout=20.0, ) @@ -482,11 +482,11 @@ def test_the_retired_criteria_are_not_accepted(sync_client): ("external_ids", "p*"), ("types", ["alarm"]), ("sub_types", ["x"]), ("statuses", ["OPEN"]), ("sources", ["sap"]), ("data_set_ids", [1])]: with pytest.raises(TypeError): - datahub_sdk.BasicEventFilter(**{retired: value}) + intellistream_datahub_sdk.BasicEventFilter(**{retired: value}) @pytest.mark.asyncio async def test_async_filter_matches_the_sync_one(async_client, sync_client, event_corpus, prefix): - request = datahub_sdk.EventFilter(datahub_sdk.BasicEventFilter(external_id=f"{prefix}*")) + request = intellistream_datahub_sdk.EventFilter(intellistream_datahub_sdk.BasicEventFilter(external_id=f"{prefix}*")) assert externals(await async_client.events.filter(request)) == externals( sync_client.events.filter(request)) diff --git a/python_tests/test_filter_paging.py b/python_tests/test_filter_paging.py index 5f1f413..c48e503 100644 --- a/python_tests/test_filter_paging.py +++ b/python_tests/test_filter_paging.py @@ -27,8 +27,8 @@ import pytest -import datahub_sdk -from datahub_sdk import DataHubException +import intellistream_datahub_sdk +from intellistream_datahub_sdk import DataHubException from fixtures import async_client, sync_client, unique_id # noqa: F401 (fixtures) from filter_fixtures import ( # noqa: F401 (fixtures) @@ -61,7 +61,7 @@ def ts_page(sync_client, prefix): """One page of the sortable timeseries corpus.""" def _page(**paging): return sync_client.timeseries.filter( - datahub_sdk.TimeSeriesFilterForm(external_id=f"{prefix}_sort_ts_*", **paging)) + intellistream_datahub_sdk.TimeSeriesFilterForm(external_id=f"{prefix}_sort_ts_*", **paging)) return _page @@ -109,7 +109,7 @@ def test_a_page_behaves_like_a_list(ts_page, by_index): def test_an_empty_page_is_falsey(sync_client, prefix): page = sync_client.timeseries.filter( - datahub_sdk.TimeSeriesFilterForm(external_id="no_such_external_id_at_all")) + intellistream_datahub_sdk.TimeSeriesFilterForm(external_id="no_such_external_id_at_all")) assert len(page) == 0 assert bool(page) is False assert page == [] @@ -165,7 +165,7 @@ def test_paging_the_default_order(ts_page, sync_client, prefix): """No explicit sort, so the walk runs in the default newest-created-first order.""" rows, _requests = walk(ts_page, limit=2) expected = ids_of(sync_client.timeseries.filter( - datahub_sdk.TimeSeriesFilterForm(external_id=f"{prefix}_sort_ts_*", limit=100))) + intellistream_datahub_sdk.TimeSeriesFilterForm(external_id=f"{prefix}_sort_ts_*", limit=100))) assert rows == expected @@ -180,7 +180,7 @@ def test_paging_across_a_run_of_tied_values(ts_page, sync_client, prefix): assert len(rows) == 6 assert len(set(rows)) == 6, f"a tied boundary repeated or dropped rows: {rows}" - unpaged = ids_of(sync_client.timeseries.filter(datahub_sdk.TimeSeriesFilterForm( + unpaged = ids_of(sync_client.timeseries.filter(intellistream_datahub_sdk.TimeSeriesFilterForm( external_id=f"{prefix}_sort_ts_*", sort_by="dataSetId", sort_order="asc", limit=100))) assert rows == unpaged @@ -235,12 +235,12 @@ def test_every_filter_endpoint_rejects_an_unreadable_cursor(sync_client, prefix, """ calls = { "timeseries": lambda: sync_client.timeseries.filter( - datahub_sdk.TimeSeriesFilterForm(limit=2, cursor="not-a-cursor")), + intellistream_datahub_sdk.TimeSeriesFilterForm(limit=2, cursor="not-a-cursor")), "resources": lambda: sync_client.resources.filter(limit=2, cursor="not-a-cursor"), "datasets": lambda: sync_client.datasets.filter( - datahub_sdk.DatasetFilter(limit=2, cursor="not-a-cursor")), + intellistream_datahub_sdk.DatasetFilter(limit=2, cursor="not-a-cursor")), "events": lambda: sync_client.events.filter( - datahub_sdk.EventFilter(limit=2, cursor="not-a-cursor")), + intellistream_datahub_sdk.EventFilter(limit=2, cursor="not-a-cursor")), } for endpoint, call in calls.items(): with pytest.raises(DataHubException) as excinfo: @@ -286,7 +286,7 @@ def test_a_malformed_boundary_is_a_400_and_not_a_500(sync_client, prefix, sortab for property_name in ["id", "dataSetId", "createdTime", "lastUpdatedTime"]: cursor = forge_cursor(property_name, "asc", "5", "not-a-number") with pytest.raises(DataHubException) as excinfo: - sync_client.timeseries.filter(datahub_sdk.TimeSeriesFilterForm( + sync_client.timeseries.filter(intellistream_datahub_sdk.TimeSeriesFilterForm( external_id=f"{prefix}_sort_ts_*", limit=2, sort_by=property_name, cursor=cursor)) assert excinfo.value.status_code == 400, \ f"{property_name}: {excinfo.value.status_code} {excinfo.value.message[:100]}" @@ -302,7 +302,7 @@ def test_an_injection_payload_in_the_cursor_boundary_is_data(sync_client, prefix distinguishes data from syntax; "it returned no rows" would not. """ def page_after(boundary): - return ids_of(sync_client.timeseries.filter(datahub_sdk.TimeSeriesFilterForm( + return ids_of(sync_client.timeseries.filter(intellistream_datahub_sdk.TimeSeriesFilterForm( external_id=f"{prefix}_sort_ts_*", limit=10, sort_by="name", sort_order="asc", cursor=forge_cursor("name", "asc", "1", boundary)))) @@ -311,7 +311,7 @@ def page_after(boundary): assert page_after(payload) == ordinary, f"payload={payload!r}" # And the table is still there afterwards. - assert ids_of(sync_client.timeseries.filter(datahub_sdk.TimeSeriesFilterForm( + assert ids_of(sync_client.timeseries.filter(intellistream_datahub_sdk.TimeSeriesFilterForm( external_id=f"{prefix}_sort_ts_*", sort_by="name", sort_order="asc"))) == by_index @@ -370,8 +370,8 @@ def test_datasets_page(sync_client, datasets, prefix): parent, child = datasets def page(cursor=None): - return sync_client.datasets.filter(datahub_sdk.DatasetFilter( - datahub_sdk.BasicDatasetFilter(external_id=f"{prefix}_ds_*"), + return sync_client.datasets.filter(intellistream_datahub_sdk.DatasetFilter( + intellistream_datahub_sdk.BasicDatasetFilter(external_id=f"{prefix}_ds_*"), limit=1, sort_by="name", sort_order="asc", cursor=cursor)) rows, requests = walk(lambda cursor=None, **_: page(cursor)) @@ -398,8 +398,8 @@ def page(cursor=None): @pytest.fixture def ev_page(sync_client, prefix, sortable_events): def _page(**paging): - request = datahub_sdk.EventFilter( - datahub_sdk.BasicEventFilter(external_id=f"{prefix}_sort_ev_*"), **paging) + request = intellistream_datahub_sdk.EventFilter( + intellistream_datahub_sdk.BasicEventFilter(external_id=f"{prefix}_sort_ev_*"), **paging) return sync_client.events.filter(request) return _page @@ -480,12 +480,12 @@ def test_an_exhausted_event_walk_omits_the_cursor_rather_than_nulling_it(ev_page @pytest.mark.asyncio async def test_async_paging_matches_the_sync_client(async_client, sync_client, prefix, sortable_timeseries, by_index): - first = await async_client.timeseries.filter(datahub_sdk.TimeSeriesFilterForm( + first = await async_client.timeseries.filter(intellistream_datahub_sdk.TimeSeriesFilterForm( external_id=f"{prefix}_sort_ts_*", limit=2, sort_by="name", sort_order="asc")) assert ids_of(first) == by_index[:2] assert first.next_cursor is not None - second = await async_client.timeseries.filter(datahub_sdk.TimeSeriesFilterForm( + second = await async_client.timeseries.filter(intellistream_datahub_sdk.TimeSeriesFilterForm( external_id=f"{prefix}_sort_ts_*", limit=2, sort_by="name", sort_order="asc", cursor=first.next_cursor)) assert ids_of(second) == by_index[2:4] diff --git a/python_tests/test_filter_resources.py b/python_tests/test_filter_resources.py index 8aad67b..9bf1f6c 100644 --- a/python_tests/test_filter_resources.py +++ b/python_tests/test_filter_resources.py @@ -20,8 +20,8 @@ """ import pytest -import datahub_sdk -from datahub_sdk import DataHubException +import intellistream_datahub_sdk +from intellistream_datahub_sdk import DataHubException from fixtures import async_client, sync_client, unique_id # noqa: F401 (fixtures) from filter_fixtures import ( # noqa: F401 (fixtures) @@ -156,7 +156,7 @@ def test_data_set_scope_accepts_ids_external_ids_and_collections(flt, resource_c assert flt(data_set_id=[child.id]) == both assert flt(data_set_id=[child.external_id]) == both, \ "an external id is a valid data set reference here now; it used to be ids only" - assert flt(data_set_id=[datahub_sdk.IdCollection(external_id=child.external_id)]) == both + assert flt(data_set_id=[intellistream_datahub_sdk.IdCollection(external_id=child.external_id)]) == both def test_a_parent_data_set_stands_in_for_its_children(flt, resource_corpus, datasets): diff --git a/python_tests/test_filter_search_bodies.py b/python_tests/test_filter_search_bodies.py index 7e81d2a..1cd838a 100644 --- a/python_tests/test_filter_search_bodies.py +++ b/python_tests/test_filter_search_bodies.py @@ -20,7 +20,7 @@ """ import pytest -import datahub_sdk +import intellistream_datahub_sdk from fixtures import async_client, sync_client, unique_id # noqa: F401 (fixtures) from filter_fixtures import ( # noqa: F401 (fixtures) @@ -46,7 +46,7 @@ def test_timeseries_search_finds_the_corpus_before_any_filtering(sync_client, ti """The baseline every test below narrows from. If this is empty the search index has not caught up and the narrowing assertions would pass for the wrong reason.""" hits = poll_until( - lambda: sync_client.timeseries.search(datahub_sdk.SearchAndFilterForm(query=f"Pump Alpha {token}")), + lambda: sync_client.timeseries.search(intellistream_datahub_sdk.SearchAndFilterForm(query=f"Pump Alpha {token}")), bool, ) assert timeseries_corpus["pump_1"].external_id in externals(hits) @@ -57,7 +57,7 @@ def test_timeseries_search_filter_narrows_on_an_inherited_node_field(sync_client hand-rolled post-filter did not implement — it was accepted and dropped.""" query = f"Pump {token}" unfiltered = poll_until( - lambda: sync_client.timeseries.search(datahub_sdk.SearchAndFilterForm(query=query)), + lambda: sync_client.timeseries.search(intellistream_datahub_sdk.SearchAndFilterForm(query=query)), lambda hits: len(externals(hits)) >= 2, ) assert externals(unfiltered) >= { @@ -65,8 +65,8 @@ def test_timeseries_search_filter_narrows_on_an_inherited_node_field(sync_client } narrowed = sync_client.timeseries.search( - datahub_sdk.SearchAndFilterForm(query=query), - filter=datahub_sdk.TimeSeriesFilterForm(name=[f"Pump Alpha {token}"]), + intellistream_datahub_sdk.SearchAndFilterForm(query=query), + filter=intellistream_datahub_sdk.TimeSeriesFilterForm(name=[f"Pump Alpha {token}"]), ) assert externals(narrowed) == {timeseries_corpus["pump_1"].external_id} @@ -74,14 +74,14 @@ def test_timeseries_search_filter_narrows_on_an_inherited_node_field(sync_client def test_timeseries_search_filter_narrows_on_a_timeseries_only_field(sync_client, timeseries_corpus, token): query = f"Pump {token}" by_unit = sync_client.timeseries.search( - datahub_sdk.SearchAndFilterForm(query=query), - filter=datahub_sdk.TimeSeriesFilterForm(unit=["celsius"]), + intellistream_datahub_sdk.SearchAndFilterForm(query=query), + filter=intellistream_datahub_sdk.TimeSeriesFilterForm(unit=["celsius"]), ) assert externals(by_unit) == {timeseries_corpus["pump_x1"].external_id} by_value_type = sync_client.timeseries.search( - datahub_sdk.SearchAndFilterForm(query=query), - filter=datahub_sdk.TimeSeriesFilterForm(value_type=["TEXT"]), + intellistream_datahub_sdk.SearchAndFilterForm(query=query), + filter=intellistream_datahub_sdk.TimeSeriesFilterForm(value_type=["TEXT"]), ) assert externals(by_value_type) == set(), "both Pump series are FLOAT" @@ -89,14 +89,14 @@ def test_timeseries_search_filter_narrows_on_a_timeseries_only_field(sync_client def test_timeseries_search_filter_narrows_by_metadata_and_labels(sync_client, timeseries_corpus, token): query = f"Pump {token}" by_metadata = sync_client.timeseries.search( - datahub_sdk.SearchAndFilterForm(query=query), - filter=datahub_sdk.TimeSeriesFilterForm(metadata={f"tsk_{token}": "beta"}), + intellistream_datahub_sdk.SearchAndFilterForm(query=query), + filter=intellistream_datahub_sdk.TimeSeriesFilterForm(metadata={f"tsk_{token}": "beta"}), ) assert externals(by_metadata) == {timeseries_corpus["pump_x1"].external_id} by_label = sync_client.timeseries.search( - datahub_sdk.SearchAndFilterForm(query=query), - filter=datahub_sdk.TimeSeriesFilterForm(labels=["NO_SUCH_LABEL_XYZ"]), + intellistream_datahub_sdk.SearchAndFilterForm(query=query), + filter=intellistream_datahub_sdk.TimeSeriesFilterForm(labels=["NO_SUCH_LABEL_XYZ"]), ) assert externals(by_label) == set() @@ -111,15 +111,15 @@ def test_timeseries_search_filter_narrows_by_data_set(sync_client, timeseries_co query = f"{token}" in_child = sync_client.timeseries.search( - datahub_sdk.SearchAndFilterForm(query=query), - filter=datahub_sdk.TimeSeriesFilterForm(data_set_id=[child.id]), + intellistream_datahub_sdk.SearchAndFilterForm(query=query), + filter=intellistream_datahub_sdk.TimeSeriesFilterForm(data_set_id=[child.id]), ) assert timeseries_corpus["valve"].external_id not in externals(in_child) # Naming the parent covers the child, so the whole corpus is back in scope. under_parent = sync_client.timeseries.search( - datahub_sdk.SearchAndFilterForm(query=query), - filter=datahub_sdk.TimeSeriesFilterForm(data_set_id=[parent.id]), + intellistream_datahub_sdk.SearchAndFilterForm(query=query), + filter=intellistream_datahub_sdk.TimeSeriesFilterForm(data_set_id=[parent.id]), ) assert externals(under_parent) >= { timeseries_corpus["pump_1"].external_id, timeseries_corpus["valve"].external_id @@ -130,7 +130,7 @@ def test_timeseries_search_filter_is_optional(sync_client, timeseries_corpus, to """Omitting it must place no restriction — the search has to keep working for callers who never pass one.""" with_none = sync_client.timeseries.search( - datahub_sdk.SearchAndFilterForm(query=f"Pump Alpha {token}"), filter=None) + intellistream_datahub_sdk.SearchAndFilterForm(query=f"Pump Alpha {token}"), filter=None) assert timeseries_corpus["pump_1"].external_id in externals(with_none) @@ -141,12 +141,12 @@ def test_timeseries_search_ranking_survives_the_filter(sync_client, timeseries_c """ query = f"{token}" unfiltered = [ts.external_id for ts in poll_until( - lambda: sync_client.timeseries.search(datahub_sdk.SearchAndFilterForm(query=query)), + lambda: sync_client.timeseries.search(intellistream_datahub_sdk.SearchAndFilterForm(query=query)), lambda hits: len(hits) >= 2, )] filtered = [ts.external_id for ts in sync_client.timeseries.search( - datahub_sdk.SearchAndFilterForm(query=query), - filter=datahub_sdk.TimeSeriesFilterForm(value_type=["FLOAT"]), + intellistream_datahub_sdk.SearchAndFilterForm(query=query), + filter=intellistream_datahub_sdk.TimeSeriesFilterForm(value_type=["FLOAT"]), )] kept = [external_id for external_id in unfiltered if external_id in set(filtered)] @@ -158,8 +158,8 @@ async def test_timeseries_search_filter_works_on_the_async_client( async_client, sync_client, timeseries_corpus, token ): narrowed = await async_client.timeseries.search( - datahub_sdk.SearchAndFilterForm(query=f"Pump {token}"), - filter=datahub_sdk.TimeSeriesFilterForm(name=[f"Pump Alpha {token}"]), + intellistream_datahub_sdk.SearchAndFilterForm(query=f"Pump {token}"), + filter=intellistream_datahub_sdk.TimeSeriesFilterForm(name=[f"Pump Alpha {token}"]), ) assert externals(narrowed) == {timeseries_corpus["pump_1"].external_id} @@ -177,14 +177,14 @@ async def test_timeseries_search_filter_works_on_the_async_client( def test_resource_search_honours_its_filter(sync_client, resource_corpus, token): query = f"Node {token}" unfiltered = poll_until( - lambda: sync_client.resources.search(datahub_sdk.SearchAndFilterForm(query=query)), + lambda: sync_client.resources.search(intellistream_datahub_sdk.SearchAndFilterForm(query=query)), lambda hits: len(externals(hits)) >= 2, ) assert externals(unfiltered) >= {r.external_id for r in resource_corpus.values()} narrowed = sync_client.resources.search( - datahub_sdk.SearchAndFilterForm(query=query), - filter=datahub_sdk.ResourceFilter(labels=["FLT_BETA"]), + intellistream_datahub_sdk.SearchAndFilterForm(query=query), + filter=intellistream_datahub_sdk.ResourceFilter(labels=["FLT_BETA"]), ) assert externals(narrowed) == {resource_corpus["root"].external_id} @@ -204,7 +204,7 @@ def test_dataset_search_honours_its_filter(sync_client, datasets, token): assert externals(unfiltered) >= {parent.external_id, child.external_id} narrowed = sync_client.datasets.search( - query, filter=datahub_sdk.BasicDatasetFilter(metadata={"tier": "gold"})) + query, filter=intellistream_datahub_sdk.BasicDatasetFilter(metadata={"tier": "gold"})) assert externals(narrowed) == {parent.external_id} @@ -216,11 +216,11 @@ def test_dataset_search_honours_its_filter(sync_client, datasets, token): def test_event_search_honours_its_filter(sync_client, event_corpus, prefix, token): query = f"alarm {token}" unfiltered = poll_until( - lambda: sync_client.events.search(datahub_sdk.EventSearch(query)), bool) + lambda: sync_client.events.search(intellistream_datahub_sdk.EventSearch(query)), bool) assert externals(unfiltered), "the event search index never returned the corpus" - narrowed = sync_client.events.search(datahub_sdk.EventSearch( - query, filter=datahub_sdk.BasicEventFilter(status=["CLOSED"]))) + narrowed = sync_client.events.search(intellistream_datahub_sdk.EventSearch( + query, filter=intellistream_datahub_sdk.BasicEventFilter(status=["CLOSED"]))) assert externals(narrowed) == {f"{prefix}_ev_alarmX1"} @@ -232,12 +232,12 @@ def test_the_ignored_filters_are_at_least_accepted(sync_client, resource_corpus, """ parent, _child = datasets assert sync_client.resources.search( - datahub_sdk.SearchAndFilterForm(query=f"Node {token}"), - filter=datahub_sdk.ResourceFilter(labels=["FLT_BETA"]), + intellistream_datahub_sdk.SearchAndFilterForm(query=f"Node {token}"), + filter=intellistream_datahub_sdk.ResourceFilter(labels=["FLT_BETA"]), ) is not None assert sync_client.datasets.search( - f"Filter {token}", filter=datahub_sdk.BasicDatasetFilter(metadata={"tier": "gold"}) + f"Filter {token}", filter=intellistream_datahub_sdk.BasicDatasetFilter(metadata={"tier": "gold"}) ) is not None - assert sync_client.events.search(datahub_sdk.EventSearch( - f"alarm {token}", filter=datahub_sdk.BasicEventFilter(status=["CLOSED"])) + assert sync_client.events.search(intellistream_datahub_sdk.EventSearch( + f"alarm {token}", filter=intellistream_datahub_sdk.BasicEventFilter(status=["CLOSED"])) ) is not None diff --git a/python_tests/test_filter_sorting.py b/python_tests/test_filter_sorting.py index e79edd6..96bdc01 100644 --- a/python_tests/test_filter_sorting.py +++ b/python_tests/test_filter_sorting.py @@ -23,7 +23,7 @@ """ import pytest -import datahub_sdk +import intellistream_datahub_sdk from fixtures import async_client, sync_client, unique_id # noqa: F401 (fixtures) from filter_fixtures import ( # noqa: F401 (fixtures) @@ -46,7 +46,7 @@ def ts_sorted(sync_client, prefix): """The sortable timeseries corpus, in the order the server returns it.""" def _sorted(**paging): return ids_of(sync_client.timeseries.filter( - datahub_sdk.TimeSeriesFilterForm(external_id=f"{prefix}_sort_ts_*", **paging))) + intellistream_datahub_sdk.TimeSeriesFilterForm(external_id=f"{prefix}_sort_ts_*", **paging))) return _sorted @@ -76,21 +76,21 @@ def test_sorting_by_id(ts_sorted, by_index, sync_client, prefix): """``id`` is both a sortable property and the implicit tie-breaker. Ids are assigned in creation order, which is deliberately not index order — so this is a third distinct sequence.""" rows = sync_client.timeseries.filter( - datahub_sdk.TimeSeriesFilterForm(external_id=f"{prefix}_sort_ts_*", sort_by="id", + intellistream_datahub_sdk.TimeSeriesFilterForm(external_id=f"{prefix}_sort_ts_*", sort_by="id", sort_order="asc")) numeric = [ts.id for ts in rows] assert numeric == sorted(numeric) assert set(ids_of(rows)) == set(by_index) descending = sync_client.timeseries.filter( - datahub_sdk.TimeSeriesFilterForm(external_id=f"{prefix}_sort_ts_*", sort_by="id", + intellistream_datahub_sdk.TimeSeriesFilterForm(external_id=f"{prefix}_sort_ts_*", sort_by="id", sort_order="desc")) assert [ts.id for ts in descending] == sorted(numeric, reverse=True) def test_sorting_by_created_time_is_creation_order(ts_sorted, sync_client, prefix): rows = sync_client.timeseries.filter( - datahub_sdk.TimeSeriesFilterForm(external_id=f"{prefix}_sort_ts_*", + intellistream_datahub_sdk.TimeSeriesFilterForm(external_id=f"{prefix}_sort_ts_*", sort_by="createdTime", sort_order="asc")) stamps = [ts.created_time for ts in rows] assert stamps == sorted(stamps), f"not ascending by createdTime: {stamps}" @@ -100,9 +100,9 @@ def test_sorting_by_created_time_is_creation_order(ts_sorted, sync_client, prefi def test_the_default_order_is_newest_created_first(ts_sorted, sync_client, prefix): """What the node filters returned before they could be sorted, kept as the default.""" unsorted = sync_client.timeseries.filter( - datahub_sdk.TimeSeriesFilterForm(external_id=f"{prefix}_sort_ts_*")) + intellistream_datahub_sdk.TimeSeriesFilterForm(external_id=f"{prefix}_sort_ts_*")) explicit = sync_client.timeseries.filter( - datahub_sdk.TimeSeriesFilterForm(external_id=f"{prefix}_sort_ts_*", + intellistream_datahub_sdk.TimeSeriesFilterForm(external_id=f"{prefix}_sort_ts_*", sort_by="createdTime", sort_order="desc")) assert ids_of(unsorted) == ids_of(explicit) @@ -159,14 +159,14 @@ def test_ties_are_broken_by_id(sync_client, prefix, sortable_timeseries): Sorted by ``dataSetId``, which is identical for all six. """ def page(): - return ids_of(sync_client.timeseries.filter(datahub_sdk.TimeSeriesFilterForm( + return ids_of(sync_client.timeseries.filter(intellistream_datahub_sdk.TimeSeriesFilterForm( external_id=f"{prefix}_sort_ts_*", sort_by="dataSetId", sort_order="asc"))) first, second = page(), page() assert first == second, "a tied sort must still be deterministic" by_id = {ts.external_id: ts.id for ts in sync_client.timeseries.filter( - datahub_sdk.TimeSeriesFilterForm(external_id=f"{prefix}_sort_ts_*"))} + intellistream_datahub_sdk.TimeSeriesFilterForm(external_id=f"{prefix}_sort_ts_*"))} assert [by_id[external_id] for external_id in first] == sorted(by_id.values()), \ "ties should fall back to id ascending" @@ -174,10 +174,10 @@ def page(): def test_the_tie_breaker_follows_the_sort_direction(sync_client, prefix, sortable_timeseries): """Descending means descending all the way down, or the two halves of the order disagree and a keyset boundary lands in the wrong place.""" - descending = ids_of(sync_client.timeseries.filter(datahub_sdk.TimeSeriesFilterForm( + descending = ids_of(sync_client.timeseries.filter(intellistream_datahub_sdk.TimeSeriesFilterForm( external_id=f"{prefix}_sort_ts_*", sort_by="dataSetId", sort_order="desc"))) by_id = {ts.external_id: ts.id for ts in sync_client.timeseries.filter( - datahub_sdk.TimeSeriesFilterForm(external_id=f"{prefix}_sort_ts_*"))} + intellistream_datahub_sdk.TimeSeriesFilterForm(external_id=f"{prefix}_sort_ts_*"))} assert [by_id[external_id] for external_id in descending] == sorted( by_id.values(), reverse=True) @@ -218,8 +218,8 @@ def test_datasets_sort_by_name(sync_client, datasets, prefix, token): parent, child = datasets def page(order): - return ids_of(sync_client.datasets.filter(datahub_sdk.DatasetFilter( - datahub_sdk.BasicDatasetFilter(external_id=f"{prefix}_ds_*"), + return ids_of(sync_client.datasets.filter(intellistream_datahub_sdk.DatasetFilter( + intellistream_datahub_sdk.BasicDatasetFilter(external_id=f"{prefix}_ds_*"), sort_by="name", sort_order=order))) # "Filter Child" sorts before "Filter Parent". @@ -245,8 +245,8 @@ def page(order): @pytest.fixture def ev_sorted(sync_client, prefix, sortable_events): def _sorted(**paging): - request = datahub_sdk.EventFilter( - datahub_sdk.BasicEventFilter(external_id=f"{prefix}_sort_ev_*"), limit=50, **paging) + request = intellistream_datahub_sdk.EventFilter( + intellistream_datahub_sdk.BasicEventFilter(external_id=f"{prefix}_sort_ev_*"), limit=50, **paging) return poll_until( lambda: ids_of(sync_client.events.filter(request)), lambda found: len(found) >= 4, @@ -316,6 +316,6 @@ def test_events_sort_by_a_nullable_property(ev_sorted, sortable_events): @pytest.mark.asyncio async def test_async_sorting_matches_the_sync_client(async_client, sync_client, prefix, sortable_timeseries, by_index): - from_async = await async_client.timeseries.filter(datahub_sdk.TimeSeriesFilterForm( + from_async = await async_client.timeseries.filter(intellistream_datahub_sdk.TimeSeriesFilterForm( external_id=f"{prefix}_sort_ts_*", sort_by="name", sort_order="asc")) assert ids_of(from_async) == by_index diff --git a/python_tests/test_filter_timeseries.py b/python_tests/test_filter_timeseries.py index eb35e3a..710a02b 100644 --- a/python_tests/test_filter_timeseries.py +++ b/python_tests/test_filter_timeseries.py @@ -14,8 +14,8 @@ """ import pytest -import datahub_sdk -from datahub_sdk import DataHubException +import intellistream_datahub_sdk +from intellistream_datahub_sdk import DataHubException from fixtures import async_client, sync_client, unique_id # noqa: F401 (fixtures) from filter_fixtures import ( # noqa: F401 (fixtures) @@ -47,7 +47,7 @@ def flt(sync_client, prefix): """ def _filter(**criteria): criteria.setdefault("external_id", f"{prefix}*") - return externals(sync_client.timeseries.filter(datahub_sdk.TimeSeriesFilterForm(**criteria))) + return externals(sync_client.timeseries.filter(intellistream_datahub_sdk.TimeSeriesFilterForm(**criteria))) return _filter @@ -132,9 +132,9 @@ def test_a_bare_string_means_a_one_element_list(sync_client, timeseries_corpus, making the single form a `TypeError` would tax the common case to serve the rare one. """ scalar = sync_client.timeseries.filter( - datahub_sdk.TimeSeriesFilterForm(external_id=f"{prefix}*", name=f"Pump Alpha {token}")) + intellistream_datahub_sdk.TimeSeriesFilterForm(external_id=f"{prefix}*", name=f"Pump Alpha {token}")) listed = sync_client.timeseries.filter( - datahub_sdk.TimeSeriesFilterForm(external_id=[f"{prefix}*"], name=[f"Pump Alpha {token}"])) + intellistream_datahub_sdk.TimeSeriesFilterForm(external_id=[f"{prefix}*"], name=[f"Pump Alpha {token}"])) assert externals(scalar) == externals(listed) == {timeseries_corpus["pump_1"].external_id} @@ -244,14 +244,14 @@ def test_an_unknown_value_type_matches_nothing(flt, timeseries_corpus): def test_an_absent_filter_places_no_restriction(sync_client, timeseries_corpus): """An argument-free form must return the tenant's timeseries, not none of them.""" - everything = sync_client.timeseries.filter(datahub_sdk.TimeSeriesFilterForm()) + everything = sync_client.timeseries.filter(intellistream_datahub_sdk.TimeSeriesFilterForm()) assert externals(everything) >= {ts.external_id for ts in timeseries_corpus.values()} def test_none_valued_criteria_are_omitted_entirely(sync_client, timeseries_corpus, prefix): """Passing ``None`` is the same as not passing the argument — it must not reach the wire as a ``null`` the server then reads as a restriction.""" - assert externals(sync_client.timeseries.filter(datahub_sdk.TimeSeriesFilterForm( + assert externals(sync_client.timeseries.filter(intellistream_datahub_sdk.TimeSeriesFilterForm( external_id=f"{prefix}*", name=None, unit=None, value_type=None, metadata=None, id=None, labels=None, source=None, data_set_id=None, ))) == {ts.external_id for ts in timeseries_corpus.values()} @@ -265,7 +265,7 @@ def test_an_empty_or_blank_list_places_no_restriction(sync_client, timeseries_co ``data_set_id`` is the documented exception and is covered separately. """ - scoped = externals(sync_client.timeseries.filter(datahub_sdk.TimeSeriesFilterForm( + scoped = externals(sync_client.timeseries.filter(intellistream_datahub_sdk.TimeSeriesFilterForm( external_id=f"{prefix}*", name=empty, unit=empty, labels=empty, value_type=empty))) assert scoped == {ts.external_id for ts in timeseries_corpus.values()} @@ -293,7 +293,7 @@ def test_data_set_scope_by_id_and_by_external_id(flt, timeseries_corpus, dataset in_child = {timeseries_corpus["pump_1"].external_id, timeseries_corpus["pump_x1"].external_id} assert flt(data_set_id=[child.id]) == in_child assert flt(data_set_id=[child.external_id]) == in_child - assert flt(data_set_id=[datahub_sdk.IdCollection(id=child.id)]) == in_child + assert flt(data_set_id=[intellistream_datahub_sdk.IdCollection(id=child.id)]) == in_child def test_a_parent_data_set_stands_in_for_its_children(flt, timeseries_corpus, datasets): @@ -309,7 +309,7 @@ def test_an_explicit_empty_data_set_scope_matches_nothing(sync_client, timeserie dropping the predicate instead would widen the query to everything the caller can read — the opposite of what they asked for. """ - assert externals(sync_client.timeseries.filter(datahub_sdk.TimeSeriesFilterForm( + assert externals(sync_client.timeseries.filter(intellistream_datahub_sdk.TimeSeriesFilterForm( external_id=f"{prefix}*", data_set_id=[]))) == set() @@ -362,12 +362,12 @@ def test_created_time_window_bounds_the_result(flt, timeseries_corpus): everything = {ts.external_id for ts in timeseries_corpus.values()} window = pd.Timedelta(minutes=10) - assert flt(created_time=datahub_sdk.TimeFilter(start=created - window)) == everything - assert flt(created_time=datahub_sdk.TimeFilter(end=created + window)) == everything - assert flt(created_time=datahub_sdk.TimeFilter( + assert flt(created_time=intellistream_datahub_sdk.TimeFilter(start=created - window)) == everything + assert flt(created_time=intellistream_datahub_sdk.TimeFilter(end=created + window)) == everything + assert flt(created_time=intellistream_datahub_sdk.TimeFilter( start=created - window, end=created + window)) == everything # A window that closes before the corpus existed excludes all of it. - assert flt(created_time=datahub_sdk.TimeFilter(end=created - window)) == set() + assert flt(created_time=intellistream_datahub_sdk.TimeFilter(end=created - window)) == set() # --------------------------------------------------------------------------- # @@ -384,14 +384,14 @@ def test_separate_criteria_and_together(flt, timeseries_corpus, token): def test_limit_caps_the_page(sync_client, timeseries_corpus, prefix): capped = sync_client.timeseries.filter( - datahub_sdk.TimeSeriesFilterForm(external_id=f"{prefix}*", limit=2)) + intellistream_datahub_sdk.TimeSeriesFilterForm(external_id=f"{prefix}*", limit=2)) assert len(capped) == 2 def test_limit_zero_falls_back_to_the_default(sync_client, timeseries_corpus, prefix): """SQL reads ``LIMIT 0`` as "return nothing", which is indistinguishable from "nothing matched" — so the server treats a non-positive limit as unset instead.""" - assert externals(sync_client.timeseries.filter(datahub_sdk.TimeSeriesFilterForm( + assert externals(sync_client.timeseries.filter(intellistream_datahub_sdk.TimeSeriesFilterForm( external_id=f"{prefix}*", limit=0))) == {ts.external_id for ts in timeseries_corpus.values()} @@ -399,7 +399,7 @@ def test_a_limit_above_the_ceiling_is_refused(sync_client, prefix): """10000 is the cap, and exceeding it is a 400 rather than a silently clamped page.""" with pytest.raises(DataHubException) as excinfo: sync_client.timeseries.filter( - datahub_sdk.TimeSeriesFilterForm(external_id=f"{prefix}*", limit=10_001)) + intellistream_datahub_sdk.TimeSeriesFilterForm(external_id=f"{prefix}*", limit=10_001)) assert excinfo.value.status_code == 400 @@ -410,13 +410,13 @@ def test_a_negative_limit_is_rejected_client_side(sync_client, prefix): """ with pytest.raises(OverflowError): sync_client.timeseries.filter( - datahub_sdk.TimeSeriesFilterForm(external_id=f"{prefix}*", limit=-5)) + intellistream_datahub_sdk.TimeSeriesFilterForm(external_id=f"{prefix}*", limit=-5)) @pytest.mark.asyncio async def test_async_filter_matches_the_sync_one(async_client, sync_client, timeseries_corpus, prefix): from_async = await async_client.timeseries.filter( - datahub_sdk.TimeSeriesFilterForm(external_id=f"{prefix}*", unit=["bar"])) + intellistream_datahub_sdk.TimeSeriesFilterForm(external_id=f"{prefix}*", unit=["bar"])) from_sync = sync_client.timeseries.filter( - datahub_sdk.TimeSeriesFilterForm(external_id=f"{prefix}*", unit=["bar"])) + intellistream_datahub_sdk.TimeSeriesFilterForm(external_id=f"{prefix}*", unit=["bar"])) assert externals(from_async) == externals(from_sync) diff --git a/python_tests/test_functions.py b/python_tests/test_functions.py index 871208d..d5c630c 100644 --- a/python_tests/test_functions.py +++ b/python_tests/test_functions.py @@ -3,7 +3,7 @@ Mirrors `src/functions/test.rs`. Round-trips a function through the live API. Skipped if the backend is unreachable (the fixture takes care of that). """ -import datahub_sdk +import intellistream_datahub_sdk import pytest from fixtures import sync_client, unique_id @@ -11,7 +11,7 @@ def test_create_list_by_external_id_delete(sync_client): ext_id = unique_id("fn") - fn = datahub_sdk.Function( + fn = intellistream_datahub_sdk.Function( external_id=ext_id, name="Function SDK roundtrip", ) diff --git a/python_tests/test_labels.py b/python_tests/test_labels.py index 22a90ec..f3d78ce 100644 --- a/python_tests/test_labels.py +++ b/python_tests/test_labels.py @@ -7,7 +7,7 @@ import time import pytest -from datahub_sdk import DataHubException, Label, Resource +from intellistream_datahub_sdk import DataHubException, Label, Resource from fixtures import async_client, make_resource, sync_client, unique_id diff --git a/python_tests/test_multi_tenant_acl.py b/python_tests/test_multi_tenant_acl.py index d90aee6..1285dbd 100644 --- a/python_tests/test_multi_tenant_acl.py +++ b/python_tests/test_multi_tenant_acl.py @@ -69,7 +69,7 @@ import os import uuid -import datahub_sdk +import intellistream_datahub_sdk import pytest from fixtures import ENV_FILE, unique_id @@ -88,7 +88,7 @@ def search_marker(): def make_resource(external_id, name=None, data_set_id=None, description=None): """A resource the backend will accept. ``labels`` is @NotNull server-side, so it has to be set even though the Python constructor defaults it to None.""" - return datahub_sdk.Resource( + return intellistream_datahub_sdk.Resource( name=name or external_id, external_id=external_id, data_set_id=data_set_id, @@ -136,7 +136,7 @@ def client_for(env, prefix, scope=SCOPE_ALL_ORGS): client_secret = env.get(f"{prefix}_CLIENT_SECRET") if not (client_id and client_secret): pytest.skip(f"{prefix}_CLIENT_ID / _SECRET not configured") - return datahub_sdk.DataHubClient( + return intellistream_datahub_sdk.DataHubClient( env["BASE_URL"], token_url=env["TOKEN_URI"], client_id=client_id, @@ -155,7 +155,7 @@ def require_healthy_realm(env): control = client_for(env, "MT_ORG_A") try: control.units.list() - except datahub_sdk.DataHubException as e: + except intellistream_datahub_sdk.DataHubException as e: pytest.skip( f"the control principal MT_ORG_A is itself refused with HTTP {e.status_code}, " "so a 401 would prove nothing — fix the realm first " @@ -177,7 +177,7 @@ def test_multi_org_principal_with_wildcard_scope_is_rejected(env): require_healthy_realm(env) multi = client_for(env, "MT_MULTI") - with pytest.raises(datahub_sdk.DataHubException) as excinfo: + with pytest.raises(intellistream_datahub_sdk.DataHubException) as excinfo: multi.units.list() assert excinfo.value.status_code == 401 @@ -191,7 +191,7 @@ def test_principal_without_an_organization_selector_is_rejected(env): require_healthy_realm(env) multi = client_for(env, "MT_MULTI", scope=None) - with pytest.raises(datahub_sdk.DataHubException) as excinfo: + with pytest.raises(intellistream_datahub_sdk.DataHubException) as excinfo: multi.units.list() assert excinfo.value.status_code == 401 @@ -233,7 +233,7 @@ def _visible(client, external_id): """ try: return bool(client.resources.by_ids([external_id])) - except datahub_sdk.DataHubException as e: + except intellistream_datahub_sdk.DataHubException as e: if e.status_code == 404: return False raise @@ -258,7 +258,7 @@ def test_same_external_id_in_two_orgs_are_independent(env): for client in (org_a, org_b): try: client.resources.delete([external_id]) - except datahub_sdk.DataHubException: + except intellistream_datahub_sdk.DataHubException: pass @@ -275,7 +275,7 @@ def acl_dataset_id(env): admin = client_for(env, "MT_ORG_A") try: found = admin.datasets.by_ids([external_id]) - except datahub_sdk.DataHubException as e: + except intellistream_datahub_sdk.DataHubException as e: if e.status_code == 401: # The seeding principal's own token is refused, so there is nothing to test here. # test_single_org_principal_with_wildcard_scope_succeeds is the one that fails loudly. @@ -297,7 +297,7 @@ def test_read_only_grant_reads_but_cannot_write(env, acl_dataset_id): assert _visible(reader, seeded), "a read grant should see the seeded resource" denied = unique_id("mt_acl_denied") - with pytest.raises(datahub_sdk.DataHubException) as excinfo: + with pytest.raises(intellistream_datahub_sdk.DataHubException) as excinfo: reader.resources.create([make_resource(denied, data_set_id=acl_dataset_id)]) assert excinfo.value.status_code == 403 # Unlike a 401, a 403 does carry a body — RFC 9457 problem+json naming the @@ -326,7 +326,7 @@ def test_write_only_grant_writes_but_cannot_read(env, acl_dataset_id): written = unique_id("mt_acl_write") created = writer.resources.create([make_resource(written, data_set_id=acl_dataset_id)]) try: - with pytest.raises(datahub_sdk.DataHubException) as excinfo: + with pytest.raises(intellistream_datahub_sdk.DataHubException) as excinfo: created.nodes[0].neighbors() assert excinfo.value.status_code == 403 assert "read" in excinfo.value.message @@ -340,8 +340,8 @@ def test_dataset_management_requires_a_blanket_write_grant(env): writer = client_for(env, "MT_WRITEONLY") external_id = unique_id("mt_acl_ds") - with pytest.raises(datahub_sdk.DataHubException) as excinfo: - writer.datasets.create([datahub_sdk.Dataset(external_id=external_id, name=external_id)]) + with pytest.raises(intellistream_datahub_sdk.DataHubException) as excinfo: + writer.datasets.create([intellistream_datahub_sdk.Dataset(external_id=external_id, name=external_id)]) assert excinfo.value.status_code == 403 assert "manage" in excinfo.value.message or "all-datasets" in excinfo.value.message @@ -359,7 +359,7 @@ def test_search_omits_denied_rows_rather_than_raising(env, acl_dataset_id): marker = search_marker() admin.resources.create([make_resource(seeded, name=marker, data_set_id=acl_dataset_id)]) try: - form = datahub_sdk.SearchAndFilterForm(query=marker) + form = intellistream_datahub_sdk.SearchAndFilterForm(query=marker) # Control first, so an empty result below means 'narrowed', not 'never created'. assert any(r.external_id == seeded for r in admin.resources.search(form)) assert not any(r.external_id == seeded for r in outsider.resources.search(form)) diff --git a/python_tests/test_navigation.py b/python_tests/test_navigation.py index 2753761..3e634e4 100644 --- a/python_tests/test_navigation.py +++ b/python_tests/test_navigation.py @@ -15,9 +15,9 @@ import asyncio import datetime as dt -import datahub_sdk +import intellistream_datahub_sdk import pytest -from datahub_sdk import ( +from intellistream_datahub_sdk import ( DataHubException, Event, IdCollection, @@ -55,11 +55,11 @@ def test_local_objects_have_no_client(): event_time=dt.datetime.now(dt.timezone.utc), ).related_resource_nodes() with pytest.raises(RuntimeError): - datahub_sdk.Dataset(external_id=unique_id("ds")).neighbors() + intellistream_datahub_sdk.Dataset(external_id=unique_id("ds")).neighbors() with pytest.raises(RuntimeError): - datahub_sdk.Function(external_id=unique_id("fn")).neighbors() + intellistream_datahub_sdk.Function(external_id=unique_id("fn")).neighbors() with pytest.raises(RuntimeError): - datahub_sdk.INode( + intellistream_datahub_sdk.INode( name="local node", external_id=unique_id("nd"), path="/x", size=0, related_resources=[1, 2], ).related_resource_nodes() @@ -69,22 +69,22 @@ def test_local_objects_have_no_client(): with pytest.raises(RuntimeError): TimeSeries(external_id=unique_id("ts")).related_events() with pytest.raises(RuntimeError): - datahub_sdk.Dataset(external_id=unique_id("ds")).related_events() + intellistream_datahub_sdk.Dataset(external_id=unique_id("ds")).related_events() with pytest.raises(RuntimeError): - datahub_sdk.Function(external_id=unique_id("fn")).related_events() + intellistream_datahub_sdk.Function(external_id=unique_id("fn")).related_events() def test_navigation_methods_exist_on_classes(): # Guards against a future merge silently dropping the navigation methods (as the # relations refactor did) — the compiled module must expose them. - assert hasattr(datahub_sdk.Resource, "neighbors") - assert hasattr(datahub_sdk.TimeSeries, "neighbors") - assert hasattr(datahub_sdk.Dataset, "neighbors") - assert hasattr(datahub_sdk.Function, "neighbors") - assert hasattr(datahub_sdk.Event, "related_resource_nodes") - assert hasattr(datahub_sdk.INode, "related_resource_nodes") - for cls in (datahub_sdk.Resource, datahub_sdk.TimeSeries, datahub_sdk.Dataset, - datahub_sdk.Function): + assert hasattr(intellistream_datahub_sdk.Resource, "neighbors") + assert hasattr(intellistream_datahub_sdk.TimeSeries, "neighbors") + assert hasattr(intellistream_datahub_sdk.Dataset, "neighbors") + assert hasattr(intellistream_datahub_sdk.Function, "neighbors") + assert hasattr(intellistream_datahub_sdk.Event, "related_resource_nodes") + assert hasattr(intellistream_datahub_sdk.INode, "related_resource_nodes") + for cls in (intellistream_datahub_sdk.Resource, intellistream_datahub_sdk.TimeSeries, intellistream_datahub_sdk.Dataset, + intellistream_datahub_sdk.Function): assert hasattr(cls, "neighbors_async") assert hasattr(cls, "related_events") assert hasattr(cls, "related_events_async") diff --git a/python_tests/test_resource_label_updates.py b/python_tests/test_resource_label_updates.py index 837ef3c..27337b1 100644 --- a/python_tests/test_resource_label_updates.py +++ b/python_tests/test_resource_label_updates.py @@ -12,7 +12,7 @@ import time import pytest -from datahub_sdk import ListFieldStr, ListFieldU64, MapField, Resource, ResourceUpdate +from intellistream_datahub_sdk import ListFieldStr, ListFieldU64, MapField, Resource, ResourceUpdate from fixtures import async_client, make_resource, sync_client, unique_id diff --git a/python_tests/test_resources.py b/python_tests/test_resources.py index bec553f..7e91d35 100644 --- a/python_tests/test_resources.py +++ b/python_tests/test_resources.py @@ -6,9 +6,9 @@ """ import time -import datahub_sdk +import intellistream_datahub_sdk import pytest -from datahub_sdk import DataHubException, EdgeProxy, GraphResult, RelForm, Resource +from intellistream_datahub_sdk import DataHubException, EdgeProxy, GraphResult, RelForm, Resource from fixtures import sync_client, unique_id from polling import poll_until @@ -112,7 +112,7 @@ def test_search_resources(sync_client): try: time.sleep(SEARCH_INDEX_DELAY) - form = datahub_sdk.SearchAndFilterForm(query=name, limit=5) + form = intellistream_datahub_sdk.SearchAndFilterForm(query=name, limit=5) results = sync_client.resources.search(form) assert isinstance(results, list) assert len(results) <= 5 diff --git a/python_tests/test_resources_async.py b/python_tests/test_resources_async.py index b6ce4d3..8cc103f 100644 --- a/python_tests/test_resources_async.py +++ b/python_tests/test_resources_async.py @@ -6,9 +6,9 @@ """ import asyncio -import datahub_sdk +import intellistream_datahub_sdk import pytest -from datahub_sdk import DataHubException, GraphResult, RelForm, Resource +from intellistream_datahub_sdk import DataHubException, GraphResult, RelForm, Resource from fixtures import async_client, unique_id @@ -105,7 +105,7 @@ async def test_search_resources(async_client): try: await asyncio.sleep(SEARCH_INDEX_DELAY) - form = datahub_sdk.SearchAndFilterForm(query=name, limit=5) + form = intellistream_datahub_sdk.SearchAndFilterForm(query=name, limit=5) results = await async_client.resources.search(form) assert isinstance(results, list) assert len(results) <= 5 diff --git a/python_tests/test_subscriptions.py b/python_tests/test_subscriptions.py index 9950311..69eaeaf 100644 --- a/python_tests/test_subscriptions.py +++ b/python_tests/test_subscriptions.py @@ -8,7 +8,7 @@ import time from datetime import datetime, timezone -import datahub_sdk +import intellistream_datahub_sdk import pandas as pd import pytest @@ -28,7 +28,7 @@ def test_create_list_delete(sync_client, subscription_timeseries): ts_a_ext, ts_b_ext = subscription_timeseries sub_ext = unique_id("sub") - sub = datahub_sdk.Subscription( + sub = intellistream_datahub_sdk.Subscription( external_id=sub_ext, name=f"Sub Test {sub_ext}", timeseries=[ts_a_ext, ts_b_ext], @@ -52,8 +52,8 @@ def test_create_list_delete(sync_client, subscription_timeseries): assert any(s.external_id == sub_ext for s in filtered) # Same call via an explicit retriever. - retriever = datahub_sdk.SubscriptionRetriever( - filter=datahub_sdk.SubscriptionFilter(timeseries=[ts_a_ext]), + retriever = intellistream_datahub_sdk.SubscriptionRetriever( + filter=intellistream_datahub_sdk.SubscriptionFilter(timeseries=[ts_a_ext]), limit=100, ) filtered_via_retriever = sync_client.subscriptions.list(retriever) @@ -77,7 +77,7 @@ def test_create_over_missing_timeseries_raises(sync_client): that refusal to the caller as an exception — the same exception surface a dataset-ACL 403 uses. (A genuine 403 needs a token without dataset read access, which this single-token harness can't mint; the create-side refusal path is exercised here.)""" - sub = datahub_sdk.Subscription( + sub = intellistream_datahub_sdk.Subscription( external_id=unique_id("sub_missing_ts"), name="Sub Over Missing TS", timeseries=[unique_id("nonexistent_ts")], @@ -87,7 +87,7 @@ def test_create_over_missing_timeseries_raises(sync_client): def test_list_rejects_retriever_and_kwargs_together(sync_client): - retriever = datahub_sdk.SubscriptionRetriever() + retriever = intellistream_datahub_sdk.SubscriptionRetriever() with pytest.raises(ValueError): sync_client.subscriptions.list(retriever, limit=10) @@ -103,11 +103,11 @@ def test_ws_datapoint_as_float(): via a deserialized SubscriptionMessage we can build by sending a real datapoint over the listen stream — covered by the listen test below. Here we only sanity-check that the types are exposed.""" - assert hasattr(datahub_sdk, "WsDatapoint") - assert hasattr(datahub_sdk, "SubscriptionMessage") - assert hasattr(datahub_sdk, "SubscriptionListener") - assert hasattr(datahub_sdk, "EventAction") - assert hasattr(datahub_sdk, "EventObject") + assert hasattr(intellistream_datahub_sdk, "WsDatapoint") + assert hasattr(intellistream_datahub_sdk, "SubscriptionMessage") + assert hasattr(intellistream_datahub_sdk, "SubscriptionListener") + assert hasattr(intellistream_datahub_sdk, "EventAction") + assert hasattr(intellistream_datahub_sdk, "EventObject") # --- Listen end-to-end ----------------------------------------------------------------- @@ -122,7 +122,7 @@ def test_listen_end_to_end(sync_client): ts_ext = unique_id("listen_ts") sub_ext = unique_id("listen") - ts = datahub_sdk.TimeSeries( + ts = intellistream_datahub_sdk.TimeSeries( external_id=ts_ext, name="Sub Listen TS", value_type="float", @@ -131,7 +131,7 @@ def test_listen_end_to_end(sync_client): ) sync_client.timeseries.create([ts]) - sub = datahub_sdk.Subscription( + sub = intellistream_datahub_sdk.Subscription( external_id=sub_ext, name=f"Sub Listen {sub_ext}", timeseries=[ts_ext], @@ -187,14 +187,14 @@ def test_listen_context_manager_closes_cleanly(sync_client): ts_ext = unique_id("ctx_ts") sub_ext = unique_id("ctx") - ts = datahub_sdk.TimeSeries( + ts = intellistream_datahub_sdk.TimeSeries( external_id=ts_ext, name="Sub Ctx TS", value_type="float", unit="a.u", ) sync_client.timeseries.create([ts]) - sub = datahub_sdk.Subscription( + sub = intellistream_datahub_sdk.Subscription( external_id=sub_ext, name=f"Sub Ctx {sub_ext}", timeseries=[ts_ext], @@ -226,11 +226,11 @@ def test_listen_fans_out_all_bound_timeseries(sync_client): sub_ext = f"sub_fan_{suffix}" ts_objs = [ - datahub_sdk.TimeSeries(external_id=ext, name=f"Fan TS {i}", value_type="float", unit="a.u") + intellistream_datahub_sdk.TimeSeries(external_id=ext, name=f"Fan TS {i}", value_type="float", unit="a.u") for i, ext in enumerate(ts_exts) ] sync_client.timeseries.create(ts_objs) - sub = datahub_sdk.Subscription( + sub = intellistream_datahub_sdk.Subscription( external_id=sub_ext, name=f"Fan Sub {suffix}", timeseries=ts_exts ) sync_client.subscriptions.create([sub]) @@ -307,11 +307,11 @@ def test_listen_partial_refusal_keeps_valid_subscription(sync_client): sub_ext = unique_id("sub_mix") bogus_ext = unique_id("sub_mix_missing") - ts = datahub_sdk.TimeSeries( + ts = intellistream_datahub_sdk.TimeSeries( external_id=ts_ext, name="Sub Mix TS", value_type="float", unit="a.u" ) sync_client.timeseries.create([ts]) - sub = datahub_sdk.Subscription( + sub = intellistream_datahub_sdk.Subscription( external_id=sub_ext, name=f"Sub Mix {sub_ext}", timeseries=[ts_ext] ) sync_client.subscriptions.create([sub]) diff --git a/python_tests/test_timeseries_async.py b/python_tests/test_timeseries_async.py index 72e1ced..2393e0b 100644 --- a/python_tests/test_timeseries_async.py +++ b/python_tests/test_timeseries_async.py @@ -2,7 +2,7 @@ import numpy as np import pytest -import datahub_sdk +import intellistream_datahub_sdk import pandas as pd from python_tests.fixtures import * @@ -21,7 +21,7 @@ def test_async_client(async_client): ] ) async def test_retrieve_datapoints(async_client,start,end,inserted_data,ts_float,test_data): - datapoints_filter = datahub_sdk.RetrieveFilter(start=start,end=end,ts= ts_float) + datapoints_filter = intellistream_datahub_sdk.RetrieveFilter(start=start,end=end,ts= ts_float) datapoints = await async_client.timeseries.retrieve_datapoints(datapoints_filter) datapoints = datapoints[0].as_dict() s = pd.Series(datapoints["values"], index=datapoints["timestamps"]) @@ -45,12 +45,12 @@ async def test_retrieve_datapoints(async_client,start,end,inserted_data,ts_float async def test_delete_datapoints(async_client,fresh_inserted_data,ts_float,start,end,test_data): #start = pd.Timestamp('2023-04-01', tz='UTC') #send = pd.Timestamp('2023-04-03', tz='UTC') - delete_target = datahub_sdk.DeleteFilter(ts=ts_float, inclusive_begin=start,exclusive_end=end) + delete_target = intellistream_datahub_sdk.DeleteFilter(ts=ts_float, inclusive_begin=start,exclusive_end=end) await async_client.timeseries.delete_datapoints([delete_target]) #await asyncio.sleep(20) """ # commented out because it takes a while to delete datapoints making test flaky fix could be to create special test query with final - datapoints_filter = datahub_sdk.RetrieveFilter(start=pd.Timestamp("2023-01-01",tz='UTC'), + datapoints_filter = intellistream_datahub_sdk.RetrieveFilter(start=pd.Timestamp("2023-01-01",tz='UTC'), end=pd.Timestamp("2023-05-01",tz='UTC'), ts= ts_float) datapoints = await async_client.timeseries.retrieve_datapoints(datapoints_filter) @@ -83,21 +83,21 @@ async def test_retrieve_latest_datapoint(async_client,inserted_data,test_data,ts ] ) async def test_insert(async_client,timestamps,values,value_type): - test_insert_ts = datahub_sdk.TimeSeries(name="test insert",value_type=value_type,unit="a.u") + test_insert_ts = intellistream_datahub_sdk.TimeSeries(name="test insert",value_type=value_type,unit="a.u") await async_client.timeseries.delete([test_insert_ts]) created = (await async_client.timeseries.create([test_insert_ts]))[0] try: if value_type == "bigint": - data = [datahub_sdk.DatapointString.from_int(ind,val) for ind,val in zip(timestamps,values)] + data = [intellistream_datahub_sdk.DatapointString.from_int(ind,val) for ind,val in zip(timestamps,values)] elif value_type == "float": - data = [datahub_sdk.DatapointString.from_float(ind,val) for ind,val in zip(timestamps,values)] + data = [intellistream_datahub_sdk.DatapointString.from_float(ind,val) for ind,val in zip(timestamps,values)] - vals=datahub_sdk.DatapointsCollectionString(datapoints=data,ts=test_insert_ts) + vals=intellistream_datahub_sdk.DatapointsCollectionString(datapoints=data,ts=test_insert_ts) inserted_datapoints = await async_client.timeseries.insert_datapoints(input=[vals]) # A successful insert is acknowledged with 204 No Content, so the body carries no items. assert inserted_datapoints == [] - retrieved_datapoints = await async_client.timeseries.retrieve_datapoints(datahub_sdk.RetrieveFilter( + retrieved_datapoints = await async_client.timeseries.retrieve_datapoints(intellistream_datahub_sdk.RetrieveFilter( start=pd.Timestamp("2019-01-01",tz="UTC"), end=pd.Timestamp("2025-01-01",tz="UTC"), ts=test_insert_ts)) @@ -112,14 +112,14 @@ async def test_insert(async_client,timestamps,values,value_type): @pytest.mark.asyncio async def test_insert_datapoints_missing_timeseries_returns_not_found(async_client): - nonexistent = datahub_sdk.TimeSeries( + nonexistent = intellistream_datahub_sdk.TimeSeries( external_id="nonexistent_ts_for_404_test", value_type="float", unit="a.u", ) - dp = datahub_sdk.DatapointString.from_float(pd.Timestamp("2025-01-01", tz="UTC"), 42.0) - vals = datahub_sdk.DatapointsCollectionString(datapoints=[dp], ts=nonexistent) - with pytest.raises(datahub_sdk.DataHubException) as exc_info: + dp = intellistream_datahub_sdk.DatapointString.from_float(pd.Timestamp("2025-01-01", tz="UTC"), 42.0) + vals = intellistream_datahub_sdk.DatapointsCollectionString(datapoints=[dp], ts=nonexistent) + with pytest.raises(intellistream_datahub_sdk.DataHubException) as exc_info: await async_client.timeseries.insert_datapoints(input=[vals]) assert exc_info.value.status_code == 404 assert "Could not find following timeseries" in exc_info.value.message @@ -134,7 +134,7 @@ async def test_insert_datapoints_missing_timeseries_returns_not_found(async_clie @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 = datahub_sdk.TimeSeries( + test_insert_ts = intellistream_datahub_sdk.TimeSeries( name="valid name", metadata=metadata, ) @@ -144,7 +144,7 @@ async def test_reject_invalid_timeseries_metadata(async_client,metadata): @pytest.mark.parametrize("name", ["valid name"]) async def test_reject_invalid_timeseries_name(async_client,name): with pytest.raises(ValueError): - test_insert_ts = datahub_sdk.TimeSeries( + test_insert_ts = intellistream_datahub_sdk.TimeSeries( name=name, ) await async_client.timeseries.delete([test_insert_ts]) @@ -153,7 +153,7 @@ async def test_reject_invalid_timeseries_name(async_client,name): @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 = datahub_sdk.TimeSeries( + test_insert_ts = intellistream_datahub_sdk.TimeSeries( name="valid name", external_id=external_id, ) @@ -164,7 +164,7 @@ async def test_reject_invalid_timeseries_metadata(async_client,external_id): @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 = datahub_sdk.TimeSeries( + test_insert_ts = intellistream_datahub_sdk.TimeSeries( name="valid name", value_type=value_type, ) @@ -176,7 +176,7 @@ async def test_reject_invalid_timeseries_metadata(async_client,value_type): @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 = datahub_sdk.TimeSeries( + test_insert_ts = intellistream_datahub_sdk.TimeSeries( name="valid name", units=units, unit_external_id=unit_external_id, diff --git a/python_tests/test_timeseries_crud.py b/python_tests/test_timeseries_crud.py index 269c9b1..a8aaaa9 100644 --- a/python_tests/test_timeseries_crud.py +++ b/python_tests/test_timeseries_crud.py @@ -17,7 +17,7 @@ import pytest -import datahub_sdk +import intellistream_datahub_sdk from python_tests.fixtures import * # noqa: F401,F403 (sync_client fixture) @@ -62,7 +62,7 @@ def test_create_delete_roundtrip(sync_client, kwargs): # unit is required by the backend (timeseries.unit.not.blank); default it # unless a case overrides it. kwargs = {"unit": "a.u", **kwargs} - ts = datahub_sdk.TimeSeries(external_id=ext_id, **kwargs) + ts = intellistream_datahub_sdk.TimeSeries(external_id=ext_id, **kwargs) sync_client.timeseries.delete([ts]) try: @@ -90,7 +90,7 @@ def test_create_delete_roundtrip(sync_client, kwargs): def test_create_batch_multiple(sync_client): ext_ids = [_uid("batch") for _ in range(3)] series = [ - datahub_sdk.TimeSeries(external_id=e, value_type="float", unit="a.u") + intellistream_datahub_sdk.TimeSeries(external_id=e, value_type="float", unit="a.u") for e in ext_ids ] sync_client.timeseries.delete(series) @@ -107,7 +107,7 @@ def test_create_batch_multiple(sync_client): def test_create_value_type_decimal_alias_normalises_to_float(sync_client, alias): """"decimal" (any case) is a legacy alias that normalises to canonical "float".""" ext_id = _uid("alias") - ts = datahub_sdk.TimeSeries(external_id=ext_id, value_type=alias, unit="a.u") + ts = intellistream_datahub_sdk.TimeSeries(external_id=ext_id, value_type=alias, unit="a.u") sync_client.timeseries.delete([ts]) try: created = sync_client.timeseries.create([ts])[0] @@ -118,7 +118,7 @@ def test_create_value_type_decimal_alias_normalises_to_float(sync_client, alias) def test_delete_by_external_id_string(sync_client): ext_id = _uid("delstr") - ts = datahub_sdk.TimeSeries(external_id=ext_id, value_type="float", unit="a.u") + ts = intellistream_datahub_sdk.TimeSeries(external_id=ext_id, value_type="float", unit="a.u") sync_client.timeseries.delete([ts]) sync_client.timeseries.create([ts]) try: @@ -159,8 +159,8 @@ def test_update_scalar_str_set_value(sync_client, make_ts, field, new_value, att source="original_source", ) - update = datahub_sdk.TimeSeriesUpdate( - ts, **{field: datahub_sdk.FieldStr(value=new_value)} + update = intellistream_datahub_sdk.TimeSeriesUpdate( + ts, **{field: intellistream_datahub_sdk.FieldStr(value=new_value)} ) updated = sync_client.timeseries.update([update])[0] assert getattr(updated, attr) == new_value @@ -173,8 +173,8 @@ def test_update_change_external_id(sync_client, make_ts): ts = make_ts(name="rename target") new_ext = _uid("renamed") - update = datahub_sdk.TimeSeriesUpdate( - ts, external_id=datahub_sdk.FieldStr(value=new_ext) + update = intellistream_datahub_sdk.TimeSeriesUpdate( + ts, external_id=intellistream_datahub_sdk.FieldStr(value=new_ext) ) updated = sync_client.timeseries.update([update])[0] assert updated.external_id == new_ext @@ -207,8 +207,8 @@ def test_update_scalar_str_set_null(sync_client, make_ts, field, attr): source="please_clear_me", ) - update = datahub_sdk.TimeSeriesUpdate( - ts, **{field: datahub_sdk.FieldStr(set_null=True)} + update = intellistream_datahub_sdk.TimeSeriesUpdate( + ts, **{field: intellistream_datahub_sdk.FieldStr(set_null=True)} ) updated = sync_client.timeseries.update([update])[0] assert getattr(updated, attr) is None @@ -219,13 +219,13 @@ def test_update_set_value_then_set_null(sync_client, make_ts): """A field can be set and then cleared across two updates.""" ts = make_ts(description="first") - set_update = datahub_sdk.TimeSeriesUpdate( - ts, description=datahub_sdk.FieldStr(value="second") + set_update = intellistream_datahub_sdk.TimeSeriesUpdate( + ts, description=intellistream_datahub_sdk.FieldStr(value="second") ) assert sync_client.timeseries.update([set_update])[0].description == "second" - null_update = datahub_sdk.TimeSeriesUpdate( - ts, description=datahub_sdk.FieldStr(set_null=True) + null_update = intellistream_datahub_sdk.TimeSeriesUpdate( + ts, description=intellistream_datahub_sdk.FieldStr(set_null=True) ) assert sync_client.timeseries.update([null_update])[0].description is None @@ -243,7 +243,7 @@ def test_update_set_value_then_set_null(sync_client, make_ts): def test_update_name_set_null(sync_client, make_ts): ts = make_ts(name="Clear my name") - update = datahub_sdk.TimeSeriesUpdate(ts, name=datahub_sdk.FieldStr(set_null=True)) + update = intellistream_datahub_sdk.TimeSeriesUpdate(ts, name=intellistream_datahub_sdk.FieldStr(set_null=True)) assert not sync_client.timeseries.update([update])[0].name @@ -251,7 +251,7 @@ def test_update_name_set_null(sync_client, make_ts): def test_update_external_id_set_null(sync_client, make_ts): ts = make_ts() - update = datahub_sdk.TimeSeriesUpdate(ts, external_id=datahub_sdk.FieldStr(set_null=True)) + update = intellistream_datahub_sdk.TimeSeriesUpdate(ts, external_id=intellistream_datahub_sdk.FieldStr(set_null=True)) assert not sync_client.timeseries.update([update])[0].external_id @@ -262,8 +262,8 @@ def test_update_external_id_set_null(sync_client, make_ts): def test_update_metadata_add_merges_and_overwrites(sync_client, make_ts): ts = make_ts(metadata={"keep": "1", "overwrite": "old"}) - update = datahub_sdk.TimeSeriesUpdate( - ts, metadata=datahub_sdk.MapField.delta(add={"overwrite": "new", "added": "2"}) + update = intellistream_datahub_sdk.TimeSeriesUpdate( + ts, metadata=intellistream_datahub_sdk.MapField.delta(add={"overwrite": "new", "added": "2"}) ) md = sync_client.timeseries.update([update])[0].metadata or {} @@ -275,8 +275,8 @@ def test_update_metadata_add_merges_and_overwrites(sync_client, make_ts): def test_update_metadata_set_replaces_whole_map(sync_client, make_ts): ts = make_ts(metadata={"a": "1", "b": "2", "c": "3"}) - update = datahub_sdk.TimeSeriesUpdate( - ts, metadata=datahub_sdk.MapField.set({"only": "9"}) + update = intellistream_datahub_sdk.TimeSeriesUpdate( + ts, metadata=intellistream_datahub_sdk.MapField.set({"only": "9"}) ) md = sync_client.timeseries.update([update])[0].metadata or {} @@ -287,8 +287,8 @@ def test_update_metadata_set_replaces_whole_map(sync_client, make_ts): def test_update_metadata_remove_keys(sync_client, make_ts): ts = make_ts(metadata={"a": "1", "b": "2", "c": "3"}) - update = datahub_sdk.TimeSeriesUpdate( - ts, metadata=datahub_sdk.MapField.delta(remove=["b", "c"]) + update = intellistream_datahub_sdk.TimeSeriesUpdate( + ts, metadata=intellistream_datahub_sdk.MapField.delta(remove=["b", "c"]) ) md = sync_client.timeseries.update([update])[0].metadata or {} @@ -300,7 +300,7 @@ def test_update_metadata_cleared_by_an_empty_set(sync_client, make_ts): """``MapField`` has no ``setNull``; an empty ``set`` is how a map is emptied.""" ts = make_ts(metadata={"a": "1", "b": "2"}) - update = datahub_sdk.TimeSeriesUpdate(ts, metadata=datahub_sdk.MapField.set({})) + update = intellistream_datahub_sdk.TimeSeriesUpdate(ts, metadata=intellistream_datahub_sdk.MapField.set({})) assert not (sync_client.timeseries.update([update])[0].metadata or {}) @@ -326,8 +326,8 @@ def test_update_metadata_cleared_by_an_empty_set(sync_client, make_ts): def test_update_security_categories_set(sync_client, make_ts): ts = make_ts(security_categories=[1, 2]) - update = datahub_sdk.TimeSeriesUpdate( - ts, security_categories=datahub_sdk.ListFieldU64.set([3, 4]) + 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] @@ -337,8 +337,8 @@ def test_update_security_categories_set(sync_client, make_ts): def test_update_security_categories_add(sync_client, make_ts): ts = make_ts(security_categories=[1, 2]) - update = datahub_sdk.TimeSeriesUpdate( - ts, security_categories=datahub_sdk.ListFieldU64.delta(add=[3]) + 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} @@ -348,8 +348,8 @@ def test_update_security_categories_add(sync_client, make_ts): def test_update_security_categories_remove(sync_client, make_ts): ts = make_ts(security_categories=[1, 2, 3]) - update = datahub_sdk.TimeSeriesUpdate( - ts, security_categories=datahub_sdk.ListFieldU64.delta(remove=[2]) + 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 []) @@ -365,8 +365,8 @@ def test_update_security_categories_cleared_by_an_empty_set(sync_client, make_ts """ ts = make_ts(security_categories=[1, 2]) - update = datahub_sdk.TimeSeriesUpdate( - ts, security_categories=datahub_sdk.ListFieldU64.set([]) + update = intellistream_datahub_sdk.TimeSeriesUpdate( + ts, security_categories=intellistream_datahub_sdk.ListFieldU64.set([]) ) assert not (sync_client.timeseries.update([update])[0].security_categories or []) @@ -376,20 +376,20 @@ def test_update_security_categories_cleared_by_an_empty_set(sync_client, make_ts # --------------------------------------------------------------------------- # def test_update_data_set_id_set_and_null(sync_client, make_ts): - ds = datahub_sdk.Dataset(external_id=_uid("ds"), name="update target dataset") + ds = intellistream_datahub_sdk.Dataset(external_id=_uid("ds"), name="update target dataset") sync_client.datasets.delete([ds]) created_ds = sync_client.datasets.create([ds])[0] try: ts = make_ts() - set_update = datahub_sdk.TimeSeriesUpdate( - ts, data_set_id=datahub_sdk.FieldU64(value=created_ds.id) + set_update = intellistream_datahub_sdk.TimeSeriesUpdate( + ts, data_set_id=intellistream_datahub_sdk.FieldU64(value=created_ds.id) ) updated = sync_client.timeseries.update([set_update])[0] assert updated.data_set_id == created_ds.id - null_update = datahub_sdk.TimeSeriesUpdate( - ts, data_set_id=datahub_sdk.FieldU64(set_null=True) + null_update = intellistream_datahub_sdk.TimeSeriesUpdate( + ts, data_set_id=intellistream_datahub_sdk.FieldU64(set_null=True) ) cleared = sync_client.timeseries.update([null_update])[0] assert cleared.data_set_id is None @@ -411,7 +411,7 @@ def test_update_cannot_change_value_type(make_ts): ts = make_ts(value_type="text") with pytest.raises(TypeError): - datahub_sdk.TimeSeriesUpdate(ts, value_type="bigint") + intellistream_datahub_sdk.TimeSeriesUpdate(ts, value_type="bigint") # --------------------------------------------------------------------------- # @@ -420,22 +420,22 @@ def test_update_cannot_change_value_type(make_ts): def test_update_target_by_created_object(sync_client, make_ts): ts = make_ts(name="orig") - update = datahub_sdk.TimeSeriesUpdate(ts, name=datahub_sdk.FieldStr(value="by object")) + update = intellistream_datahub_sdk.TimeSeriesUpdate(ts, name=intellistream_datahub_sdk.FieldStr(value="by object")) assert sync_client.timeseries.update([update])[0].name == "by object" def test_update_target_by_external_id_string(sync_client, make_ts): ts = make_ts(name="orig") - update = datahub_sdk.TimeSeriesUpdate( - ts.external_id, name=datahub_sdk.FieldStr(value="by ext-id string") + update = intellistream_datahub_sdk.TimeSeriesUpdate( + ts.external_id, name=intellistream_datahub_sdk.FieldStr(value="by ext-id string") ) assert sync_client.timeseries.update([update])[0].name == "by ext-id string" def test_update_target_by_numeric_id(sync_client, make_ts): ts = make_ts(name="orig") - update = datahub_sdk.TimeSeriesUpdate( - ts.id, name=datahub_sdk.FieldStr(value="by numeric id") + update = intellistream_datahub_sdk.TimeSeriesUpdate( + ts.id, name=intellistream_datahub_sdk.FieldStr(value="by numeric id") ) assert sync_client.timeseries.update([update])[0].name == "by numeric id" @@ -447,12 +447,12 @@ def test_update_target_by_numeric_id(sync_client, make_ts): def test_update_multiple_fields_in_one_call(sync_client, make_ts): ts = make_ts(name="orig", description="orig desc", metadata={"k": "v"}) - update = datahub_sdk.TimeSeriesUpdate( + update = intellistream_datahub_sdk.TimeSeriesUpdate( ts, - name=datahub_sdk.FieldStr(value="multi name"), - description=datahub_sdk.FieldStr(value="multi desc"), - unit=datahub_sdk.FieldStr(value="multi unit"), - metadata=datahub_sdk.MapField.delta(add={"k2": "v2"}), + name=intellistream_datahub_sdk.FieldStr(value="multi name"), + description=intellistream_datahub_sdk.FieldStr(value="multi desc"), + unit=intellistream_datahub_sdk.FieldStr(value="multi unit"), + metadata=intellistream_datahub_sdk.MapField.delta(add={"k2": "v2"}), ) updated = sync_client.timeseries.update([update])[0] @@ -468,8 +468,8 @@ def test_update_batch_distinct_series(sync_client, make_ts): ts2 = make_ts(name="batch two") updates = [ - datahub_sdk.TimeSeriesUpdate(ts1, name=datahub_sdk.FieldStr(value="batch one updated")), - datahub_sdk.TimeSeriesUpdate(ts2, name=datahub_sdk.FieldStr(value="batch two updated")), + intellistream_datahub_sdk.TimeSeriesUpdate(ts1, name=intellistream_datahub_sdk.FieldStr(value="batch one updated")), + intellistream_datahub_sdk.TimeSeriesUpdate(ts2, name=intellistream_datahub_sdk.FieldStr(value="batch two updated")), ] updated = sync_client.timeseries.update(updates) by_ext = {u.external_id: u.name for u in updated} @@ -481,7 +481,7 @@ def test_update_batch_distinct_series(sync_client, make_ts): def test_update_noop_preserves_existing_fields(sync_client, make_ts): ts = make_ts(name="keep me", description="keep this too", metadata={"a": "1"}) - update = datahub_sdk.TimeSeriesUpdate(ts) # no field wrappers supplied + update = intellistream_datahub_sdk.TimeSeriesUpdate(ts) # no field wrappers supplied updated = sync_client.timeseries.update([update])[0] assert updated.name == "keep me" @@ -497,4 +497,4 @@ def test_update_without_identifier_rejected(): # neither id nor external_id is rejected client-side, so an identifier-less # update can never be constructed (let alone reach the backend). with pytest.raises(Exception): - datahub_sdk.IdCollection() + intellistream_datahub_sdk.IdCollection() diff --git a/python_tests/test_timeseries_datapoints.py b/python_tests/test_timeseries_datapoints.py index ad08f58..4473d31 100644 --- a/python_tests/test_timeseries_datapoints.py +++ b/python_tests/test_timeseries_datapoints.py @@ -20,7 +20,7 @@ import pandas as pd import pytest -import datahub_sdk +import intellistream_datahub_sdk from python_tests.fixtures import * # noqa: F401,F403 @@ -29,7 +29,7 @@ # --------------------------------------------------------------------------- # def test_datapoints_aggregates_daily(sync_client, inserted_data, ts_float): - rf = datahub_sdk.RetrieveFilter( + rf = intellistream_datahub_sdk.RetrieveFilter( ts=ts_float, start=pd.Timestamp("2023-01-01", tz="UTC"), end=pd.Timestamp("2023-04-30", tz="UTC"), @@ -53,7 +53,7 @@ def test_datapoints_limit_and_cursor_exposed(sync_client, inserted_data, ts_floa # traversal is covered by ``test_datapoints_heavy``). At this scale we verify # that ``limit`` caps the result and that a complete small page exposes no # further cursor. - rf = datahub_sdk.RetrieveFilter( + rf = intellistream_datahub_sdk.RetrieveFilter( ts=ts_float, start=pd.Timestamp("2023-01-01", tz="UTC"), end=pd.Timestamp("2023-05-01", tz="UTC"), @@ -63,7 +63,7 @@ def test_datapoints_limit_and_cursor_exposed(sync_client, inserted_data, ts_floa assert len(page.get_datapoints()) <= 10 # Retrieving the whole series fits in one page -> no continuation cursor. - rf = datahub_sdk.RetrieveFilter( + rf = intellistream_datahub_sdk.RetrieveFilter( ts=ts_float, start=pd.Timestamp("2023-01-01", tz="UTC"), end=pd.Timestamp("2023-05-01", tz="UTC"), @@ -109,7 +109,7 @@ def test_datapoints_heavy(sync_client): # Clean slate in case a previous run leaked the series. sync_client.timeseries.delete([ext_id]) - ts = datahub_sdk.TimeSeries( + ts = intellistream_datahub_sdk.TimeSeries( external_id=ext_id, name="Py SDK Heavy Datapoints", description="Generated by the python sdk heavy datapoints test.", @@ -132,7 +132,7 @@ def test_datapoints_heavy(sync_client): # --- raw retrieval (100k limit), values bounded to [160, 200] --------- start = pd.Timestamp("2025-01-01", tz="UTC") - rf = datahub_sdk.RetrieveFilter(ts=created, start=start, limit=100000) + rf = intellistream_datahub_sdk.RetrieveFilter(ts=created, start=start, limit=100000) dps = sync_client.timeseries.retrieve_datapoints(rf)[0].get_datapoints() assert len(dps) == 100000 for dp in dps: @@ -140,12 +140,12 @@ def test_datapoints_heavy(sync_client): assert 160.0 <= dp.value <= 200.0 # limit=200 returns exactly 200 - rf = datahub_sdk.RetrieveFilter(ts=created, start=start, limit=200) + rf = intellistream_datahub_sdk.RetrieveFilter(ts=created, start=start, limit=200) dps = sync_client.timeseries.retrieve_datapoints(rf)[0].get_datapoints() assert len(dps) == 200 # one-hour window at full second resolution -> 3600 points - rf = datahub_sdk.RetrieveFilter( + rf = intellistream_datahub_sdk.RetrieveFilter( ts=created, start=pd.Timestamp("2025-01-01T06:00:00", tz="UTC"), end=pd.Timestamp("2025-01-01T07:00:00", tz="UTC"), @@ -158,7 +158,7 @@ def test_datapoints_heavy(sync_client): sleep(90) # --- daily aggregates ------------------------------------------------- - rf = datahub_sdk.RetrieveFilter( + rf = intellistream_datahub_sdk.RetrieveFilter( ts=created, start=pd.Timestamp("2025-01-01", tz="UTC"), end=pd.Timestamp("2025-03-01", tz="UTC"), @@ -183,13 +183,13 @@ def test_datapoints_heavy(sync_client): # --- deletion -------------------------------------------------------- delete_after = pd.Timestamp("2025-02-05", tz="UTC") - delete_filter = datahub_sdk.DeleteFilter( + delete_filter = intellistream_datahub_sdk.DeleteFilter( ts=created, inclusive_begin=delete_after ) sync_client.timeseries.delete_datapoints([delete_filter]) sleep(90) - rf = datahub_sdk.RetrieveFilter( + rf = intellistream_datahub_sdk.RetrieveFilter( ts=created, start=pd.Timestamp("2025-01-01T06:00:00", tz="UTC"), limit=5000, @@ -204,14 +204,14 @@ def _validate_raw_datapoints_with_cursor(sync_client, ts): start = pd.Timestamp("2025-01-01", tz="UTC") end = pd.Timestamp("2025-03-01", tz="UTC") # limit=0 -> backend default max page (100k) - rf = datahub_sdk.RetrieveFilter(ts=ts, start=start, end=end, limit=0) + rf = intellistream_datahub_sdk.RetrieveFilter(ts=ts, start=start, end=end, limit=0) page = sync_client.timeseries.retrieve_datapoints(rf)[0] cursor = page.next_cursor assert cursor assert len(page.get_datapoints()) == 100000 while cursor: - rf = datahub_sdk.RetrieveFilter( + rf = intellistream_datahub_sdk.RetrieveFilter( ts=ts, start=start, end=end, limit=0, cursor=cursor ) page = sync_client.timeseries.retrieve_datapoints(rf)[0] diff --git a/python_tests/test_timeseries_search.py b/python_tests/test_timeseries_search.py index 7d23cee..681b290 100644 --- a/python_tests/test_timeseries_search.py +++ b/python_tests/test_timeseries_search.py @@ -8,7 +8,7 @@ import pytest -import datahub_sdk +import intellistream_datahub_sdk from python_tests.fixtures import * # noqa: F401,F403 (sync_client fixture) @@ -31,11 +31,11 @@ def test_search_finds_created_series(sync_client, make_ts, field): time.sleep(SEARCH_INDEX_DELAY) if field == "name": - form = datahub_sdk.SearchAndFilterForm(name=unique_name) + form = intellistream_datahub_sdk.SearchAndFilterForm(name=unique_name) elif field == "query": - form = datahub_sdk.SearchAndFilterForm(query=unique_name) + form = intellistream_datahub_sdk.SearchAndFilterForm(query=unique_name) else: - form = datahub_sdk.SearchAndFilterForm(description=unique_description) + form = intellistream_datahub_sdk.SearchAndFilterForm(description=unique_description) results = sync_client.timeseries.search(form) assert isinstance(results, list) diff --git a/python_tests/test_timeseries_sync.py b/python_tests/test_timeseries_sync.py index 3d89f05..548ad17 100644 --- a/python_tests/test_timeseries_sync.py +++ b/python_tests/test_timeseries_sync.py @@ -3,7 +3,7 @@ import numpy as np import pytest -import datahub_sdk +import intellistream_datahub_sdk import pandas as pd import polars as pl from python_tests.fixtures import * @@ -50,7 +50,7 @@ def test_list_negative_limit_rejected(sync_client): ] ) def test_retrieve_datapoints(sync_client,start,end,inserted_data,ts_float,test_data): - datapoints_filter = datahub_sdk.RetrieveFilter(start=start,end=end,ts= ts_float) + datapoints_filter = intellistream_datahub_sdk.RetrieveFilter(start=start,end=end,ts= ts_float) datapoints = sync_client.timeseries.retrieve_datapoints(datapoints_filter) datapoints = datapoints[0].as_dict() s = pd.Series(datapoints["values"], index=datapoints["timestamps"]) @@ -61,7 +61,7 @@ def test_retrieve_datapoints(sync_client,start,end,inserted_data,ts_float,test_d assert np.allclose(s,test_data[start:end]) def test_create_timeseries_invalid_value_type(sync_client): with pytest.raises(ValueError): - invalid = datahub_sdk.TimeSeries(name="test insert",value_type="invalid_string",unit="a.u") + invalid = intellistream_datahub_sdk.TimeSeries(name="test insert",value_type="invalid_string",unit="a.u") sync_client.timeseries.create([invalid]) @pytest.mark.parametrize( @@ -78,12 +78,12 @@ def test_create_timeseries_invalid_value_type(sync_client): def test_delete_datapoints(sync_client,fresh_inserted_data,ts_float,start,end,test_data): #start = pd.Timestamp('2023-04-01', tz='UTC') #send = pd.Timestamp('2023-04-03', tz='UTC') - delete_target = datahub_sdk.DeleteFilter(ts=ts_float, inclusive_begin=start,exclusive_end=end) + delete_target = intellistream_datahub_sdk.DeleteFilter(ts=ts_float, inclusive_begin=start,exclusive_end=end) sync_client.timeseries.delete_datapoints([delete_target]) # asyncio.sleep(20) """ # commented out because it takes a while to delete datapoints making test flaky fix could be to create special test query with final - datapoints_filter = datahub_sdk.RetrieveFilter(start=pd.Timestamp("2023-01-01",tz='UTC'), + datapoints_filter = intellistream_datahub_sdk.RetrieveFilter(start=pd.Timestamp("2023-01-01",tz='UTC'), end=pd.Timestamp("2023-05-01",tz='UTC'), ts= ts_float) datapoints = sync_client.timeseries.retrieve_datapoints(datapoints_filter) @@ -118,29 +118,29 @@ def test_insert(sync_client,make_ts,timestamps,values,value_type): created = make_ts(name="test insert", value_type=value_type) if value_type == "bigint": - data = [datahub_sdk.DatapointString.from_int(ind,val) for ind,val in zip(timestamps,values)] + data = [intellistream_datahub_sdk.DatapointString.from_int(ind,val) for ind,val in zip(timestamps,values)] elif value_type == "float": - data = [datahub_sdk.DatapointString.from_float(ind,val) for ind,val in zip(timestamps,values)] + data = [intellistream_datahub_sdk.DatapointString.from_float(ind,val) for ind,val in zip(timestamps,values)] - vals=datahub_sdk.DatapointsCollectionString(datapoints=data,ts=created) + vals=intellistream_datahub_sdk.DatapointsCollectionString(datapoints=data,ts=created) inserted_datapoints = sync_client.timeseries.insert_datapoints(input=[vals]) # A successful insert is acknowledged with 204 No Content, so the body carries no items. assert inserted_datapoints == [] - retrieved_datapoints = sync_client.timeseries.retrieve_datapoints(datahub_sdk.RetrieveFilter( + retrieved_datapoints = sync_client.timeseries.retrieve_datapoints(intellistream_datahub_sdk.RetrieveFilter( start=pd.Timestamp("2019-01-01",tz="UTC"), end=pd.Timestamp("2025-01-01",tz="UTC"), ts=created)) assert retrieved_datapoints def test_insert_datapoints_missing_timeseries_returns_not_found(sync_client): - nonexistent = datahub_sdk.TimeSeries( + nonexistent = intellistream_datahub_sdk.TimeSeries( external_id="nonexistent_ts_for_404_test", value_type="float", unit="a.u", ) - dp = datahub_sdk.DatapointString.from_float(pd.Timestamp("2025-01-01", tz="UTC"), 42.0) - vals = datahub_sdk.DatapointsCollectionString(datapoints=[dp], ts=nonexistent) - with pytest.raises(datahub_sdk.DataHubException) as exc_info: + dp = intellistream_datahub_sdk.DatapointString.from_float(pd.Timestamp("2025-01-01", tz="UTC"), 42.0) + vals = intellistream_datahub_sdk.DatapointsCollectionString(datapoints=[dp], ts=nonexistent) + with pytest.raises(intellistream_datahub_sdk.DataHubException) as exc_info: sync_client.timeseries.insert_datapoints(input=[vals]) assert exc_info.value.status_code == 404 assert "Could not find following timeseries" in exc_info.value.message @@ -148,7 +148,7 @@ def test_insert_datapoints_missing_timeseries_returns_not_found(sync_client): def test_invalid_retrieve_latest_datapoint(sync_client): with pytest.raises(IndexError): - nonexistant_ts = datahub_sdk.TimeSeries(external_id="nonexistent_ts",value_type="bigint",unit="a.u") + 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] """ @@ -158,7 +158,7 @@ def test_invalid_retrieve_latest_datapoint(sync_client): @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 = datahub_sdk.TimeSeries( + test_insert_ts = intellistream_datahub_sdk.TimeSeries( name="valid name", metadata=metadata, ) @@ -167,7 +167,7 @@ def test_reject_invalid_timeseries_metadata(sync_client,metadata): @pytest.mark.parametrize("name", ["valid name"]) def test_reject_invalid_timeseries_name(sync_client,name): with pytest.raises(ValueError): - test_insert_ts = datahub_sdk.TimeSeries( + test_insert_ts = intellistream_datahub_sdk.TimeSeries( name=name, ) sync_client.timeseries.delete([test_insert_ts]) @@ -175,7 +175,7 @@ def test_reject_invalid_timeseries_name(sync_client,name): @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 = datahub_sdk.TimeSeries( + test_insert_ts = intellistream_datahub_sdk.TimeSeries( name="valid name", external_id=external_id, ) @@ -185,7 +185,7 @@ def test_reject_invalid_timeseries_metadata(sync_client,external_id): @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 = datahub_sdk.TimeSeries( + test_insert_ts = intellistream_datahub_sdk.TimeSeries( name="valid name", value_type=value_type, ) @@ -196,7 +196,7 @@ def test_reject_invalid_timeseries_metadata(sync_client,value_type): @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 = datahub_sdk.TimeSeries( + test_insert_ts = intellistream_datahub_sdk.TimeSeries( name="valid name", units=units, unit_external_id=unit_external_id, @@ -214,13 +214,13 @@ def test_timeseries_update_with_fields(sync_client, make_ts): # 2. Prepare Update Fields using Field structs # Note: TimeSeriesUpdate.__init__ expects these types for specific fields - new_name = datahub_sdk.FieldStr(value="Updated Name") - new_unit = datahub_sdk.FieldStr(value="Updated Unit") - new_metadata = datahub_sdk.MapField.delta(add={"status": "updated", "version": "2"}) + new_name = intellistream_datahub_sdk.FieldStr(value="Updated Name") + new_unit = intellistream_datahub_sdk.FieldStr(value="Updated Unit") + new_metadata = intellistream_datahub_sdk.MapField.delta(add={"status": "updated", "version": "2"}) # 3. Create the Update object # The first argument 'ts' is the Identifyable (the created_ts itself) - ts_update = datahub_sdk.TimeSeriesUpdate( + ts_update = intellistream_datahub_sdk.TimeSeriesUpdate( created_ts, name=new_name, unit=new_unit, @@ -243,9 +243,9 @@ def test_timeseries_update_set_null(sync_client, make_ts): created_ts = make_ts(name="Null Test", description="I should be deleted", value_type="text") # Use FieldStr with set_null=True to clear the description - null_description = datahub_sdk.FieldStr(set_null=True) + null_description = intellistream_datahub_sdk.FieldStr(set_null=True) - ts_update = datahub_sdk.TimeSeriesUpdate( + ts_update = intellistream_datahub_sdk.TimeSeriesUpdate( created_ts, description=null_description ) diff --git a/python_tests/test_timezones.py b/python_tests/test_timezones.py index 9764f82..ace3fad 100644 --- a/python_tests/test_timezones.py +++ b/python_tests/test_timezones.py @@ -16,7 +16,7 @@ from time import sleep from zoneinfo import ZoneInfo -import datahub_sdk +import intellistream_datahub_sdk import numpy as np import pandas as pd import pytest @@ -52,24 +52,24 @@ @pytest.mark.parametrize("dt", EQUIVALENT_AWARE) def test_event_constructor_accepts_any_timezone(dt): - ev = datahub_sdk.Event(type="test", external_id="tz_evt", event_time=dt) + ev = intellistream_datahub_sdk.Event(type="test", external_id="tz_evt", event_time=dt) assert ev.event_time == UTC_NOON @pytest.mark.parametrize("dt", EQUIVALENT_AWARE) def test_event_time_setter_accepts_any_timezone(dt): - ev = datahub_sdk.Event(type="test", external_id="tz_evt", event_time=UTC_NOON) + ev = intellistream_datahub_sdk.Event(type="test", external_id="tz_evt", event_time=UTC_NOON) ev.event_time = dt assert ev.event_time == UTC_NOON def test_event_rejects_naive_datetime(): with pytest.raises(TypeError): - datahub_sdk.Event(type="test", external_id="tz_evt", event_time=datetime(2025, 1, 1, 12, 0)) + intellistream_datahub_sdk.Event(type="test", external_id="tz_evt", event_time=datetime(2025, 1, 1, 12, 0)) def test_event_setter_rejects_naive_datetime(): - ev = datahub_sdk.Event(type="test", external_id="tz_evt", event_time=UTC_NOON) + ev = intellistream_datahub_sdk.Event(type="test", external_id="tz_evt", event_time=UTC_NOON) with pytest.raises(TypeError): ev.event_time = datetime(2025, 1, 1, 12, 0) @@ -77,32 +77,32 @@ def test_event_setter_rejects_naive_datetime(): @pytest.mark.parametrize("dt", EQUIVALENT_AWARE) def test_time_filter_accepts_any_timezone(dt): # A single bound (start only) -> TimeFilter::After; must not raise on any tz. - datahub_sdk.TimeFilter(start=dt) + intellistream_datahub_sdk.TimeFilter(start=dt) def test_time_filter_mixed_timezones_compare_correctly(): # start (Oslo 13:00 = 12:00Z) is before end (New York 08:00 = 13:00Z): valid range. start = datetime(2025, 1, 1, 13, 0, tzinfo=ZoneInfo("Europe/Oslo")) end = datetime(2025, 1, 1, 8, 0, tzinfo=ZoneInfo("America/New_York")) - datahub_sdk.TimeFilter(start=start, end=end) + intellistream_datahub_sdk.TimeFilter(start=start, end=end) @pytest.mark.parametrize("dt", EQUIVALENT_AWARE) def test_datapoint_string_accepts_any_timezone(dt): # DatapointString stores the timestamp as an epoch-millis string. expected_ms = str(int(UTC_NOON.timestamp() * 1000)) - dp = datahub_sdk.DatapointString(dt, "1.0") + dp = intellistream_datahub_sdk.DatapointString(dt, "1.0") assert dp.timestamp == expected_ms def test_datapoint_string_rejects_naive_datetime(): with pytest.raises(TypeError): - datahub_sdk.DatapointString(datetime(2025, 1, 1, 12, 0), "1.0") + intellistream_datahub_sdk.DatapointString(datetime(2025, 1, 1, 12, 0), "1.0") @pytest.mark.parametrize("dt", EQUIVALENT_AWARE) def test_retrieve_filter_accepts_any_timezone(dt): - rf = datahub_sdk.RetrieveFilter("some_external_id", start=dt, end=dt) + rf = intellistream_datahub_sdk.RetrieveFilter("some_external_id", start=dt, end=dt) assert rf.start == UTC_NOON assert rf.end == UTC_NOON @@ -113,26 +113,26 @@ def test_retrieve_filter_accepts_any_timezone(dt): def test_naive_pandas_timestamp_is_rejected(): # pandas Timestamp subclasses datetime, so a naive one hits the same naive guard. with pytest.raises(TypeError): - datahub_sdk.Event(type="test", external_id="tz_evt", event_time=pd.Timestamp("2025-01-01 12:00")) + intellistream_datahub_sdk.Event(type="test", external_id="tz_evt", event_time=pd.Timestamp("2025-01-01 12:00")) def test_pandas_nat_is_rejected(): with pytest.raises(TypeError): - datahub_sdk.Event(type="test", external_id="tz_evt", event_time=pd.NaT) + intellistream_datahub_sdk.Event(type="test", external_id="tz_evt", event_time=pd.NaT) def test_numpy_datetime64_is_rejected_with_helpful_error(): # numpy datetime64 is not a datetime and has no timezone; it must be rejected with a # message that points at the fix, not an opaque AttributeError. with pytest.raises(TypeError) as exc: - datahub_sdk.Event(type="test", external_id="tz_evt", event_time=np.datetime64("2025-01-01T12:00")) + intellistream_datahub_sdk.Event(type="test", external_id="tz_evt", event_time=np.datetime64("2025-01-01T12:00")) assert "datetime64" in str(exc.value) or "pd.Timestamp" in str(exc.value) def test_numpy_datetime64_converted_via_pandas_is_accepted(): # The documented workaround round-trips to the right instant. dt = pd.Timestamp(np.datetime64("2025-01-01T12:00")).tz_localize("UTC") - ev = datahub_sdk.Event(type="test", external_id="tz_evt", event_time=dt) + ev = intellistream_datahub_sdk.Event(type="test", external_id="tz_evt", event_time=dt) assert ev.event_time == UTC_NOON @@ -146,8 +146,8 @@ def test_dst_named_zone_resolves_offset_by_date(): # tzinfo (the pre-fix datapoints path) can't handle a ZoneInfo at all. winter = datetime(2025, 1, 15, 12, 0, tzinfo=OSLO) # +01:00 -> 11:00Z summer = datetime(2025, 7, 15, 12, 0, tzinfo=OSLO) # +02:00 -> 10:00Z - assert datahub_sdk.Event(type="test", external_id="w", event_time=winter).event_time == datetime(2025, 1, 15, 11, 0, tzinfo=UTC) - assert datahub_sdk.Event(type="test", external_id="s", event_time=summer).event_time == datetime(2025, 7, 15, 10, 0, tzinfo=UTC) + assert intellistream_datahub_sdk.Event(type="test", external_id="w", event_time=winter).event_time == datetime(2025, 1, 15, 11, 0, tzinfo=UTC) + assert intellistream_datahub_sdk.Event(type="test", external_id="s", event_time=summer).event_time == datetime(2025, 7, 15, 10, 0, tzinfo=UTC) # --------------------------------------------------------------------------- # @@ -163,7 +163,7 @@ def _retrieve_with_retry(sync_client, ts, start, end, attempts=15, delay=1.0): """Poll retrieve_datapoints until points appear (ClickHouse ingest lag).""" for _ in range(attempts): dps = sync_client.timeseries.retrieve_datapoints( - datahub_sdk.RetrieveFilter(ts=ts, start=start, end=end, limit=1000) + intellistream_datahub_sdk.RetrieveFilter(ts=ts, start=start, end=end, limit=1000) )[0].get_datapoints() if dps: return dps @@ -218,7 +218,7 @@ def test_event_non_utc_offset_survives_roundtrip(sync_client, make_dataset, make event_time = datetime(2025, 6, 1, 18, 0, tzinfo=OSLO) # summer = +02:00 -> 16:00Z expected_utc = datetime(2025, 6, 1, 16, 0, tzinfo=UTC) - ev = datahub_sdk.Event(type="test", external_id=unique_id("tz_evt"), + ev = intellistream_datahub_sdk.Event(type="test", external_id=unique_id("tz_evt"), event_time=event_time, data_set_id=ds.id, ) @@ -248,14 +248,14 @@ def test_delete_datapoints_non_utc_boundary(sync_client, make_ts): assert len(inserted) == 2, "both points should exist before the delete" sync_client.timeseries.delete_datapoints( - [datahub_sdk.DeleteFilter(ts=ts, inclusive_begin=boundary)] + [intellistream_datahub_sdk.DeleteFilter(ts=ts, inclusive_begin=boundary)] ) sleep(90) # ClickHouse delete latency remaining = { dp.timestamp for dp in sync_client.timeseries.retrieve_datapoints( - datahub_sdk.RetrieveFilter( + intellistream_datahub_sdk.RetrieveFilter( ts=ts, start=datetime(2025, 6, 1, tzinfo=UTC), end=datetime(2025, 6, 2, tzinfo=UTC), diff --git a/python_tests/test_units.py b/python_tests/test_units.py index 903755b..008cc18 100644 --- a/python_tests/test_units.py +++ b/python_tests/test_units.py @@ -3,7 +3,7 @@ Units are reference data — there's no create endpoint, only read paths. Exercises every endpoint on `UnitServiceSync`: list, by_ids, by_external_ids. """ -import datahub_sdk as dh +import intellistream_datahub_sdk as dh import pytest from fixtures import sync_client diff --git a/python_tests/test_update_datasets.py b/python_tests/test_update_datasets.py index 9ead167..8cb6336 100644 --- a/python_tests/test_update_datasets.py +++ b/python_tests/test_update_datasets.py @@ -16,7 +16,7 @@ """ import pytest -import datahub_sdk +import intellistream_datahub_sdk from fixtures import make_dataset, sync_client, unique_id from polling import poll_until @@ -63,8 +63,8 @@ def _make(**kwargs): def test_scalar_set_value(sync_client, new_dataset, field, value, attr): dataset = new_dataset(description="original description") - updated = _apply(sync_client, datahub_sdk.DatasetUpdate( - dataset.external_id, **{field: datahub_sdk.FieldStr(value=value)} + updated = _apply(sync_client, intellistream_datahub_sdk.DatasetUpdate( + dataset.external_id, **{field: intellistream_datahub_sdk.FieldStr(value=value)} )) assert getattr(updated, attr) == value @@ -73,13 +73,13 @@ def test_external_id_set_value(sync_client, new_dataset): dataset = new_dataset() new_ext = unique_id("ds_renamed") - _apply(sync_client, datahub_sdk.DatasetUpdate( - dataset.external_id, external_id=datahub_sdk.FieldStr(value=new_ext) + _apply(sync_client, intellistream_datahub_sdk.DatasetUpdate( + dataset.external_id, external_id=intellistream_datahub_sdk.FieldStr(value=new_ext) )) found = poll_until( - lambda: sync_client.datasets.filter(datahub_sdk.DatasetFilter( - datahub_sdk.BasicDatasetFilter(external_id=[new_ext]) + lambda: sync_client.datasets.filter(intellistream_datahub_sdk.DatasetFilter( + intellistream_datahub_sdk.BasicDatasetFilter(external_id=[new_ext]) )), bool, ) @@ -91,18 +91,18 @@ def test_duplicate_external_id_is_rejected(sync_client, new_dataset): taken = new_dataset() other = new_dataset() - with pytest.raises(datahub_sdk.DataHubException): - sync_client.datasets.update([datahub_sdk.DatasetUpdate( - other.external_id, external_id=datahub_sdk.FieldStr(value=taken.external_id) + with pytest.raises(intellistream_datahub_sdk.DataHubException): + sync_client.datasets.update([intellistream_datahub_sdk.DatasetUpdate( + other.external_id, external_id=intellistream_datahub_sdk.FieldStr(value=taken.external_id) )]) def test_name_below_the_minimum_length_is_rejected(sync_client, new_dataset): dataset = new_dataset() - with pytest.raises(datahub_sdk.DataHubException): - sync_client.datasets.update([datahub_sdk.DatasetUpdate( - dataset.external_id, name=datahub_sdk.FieldStr(value="ab") + with pytest.raises(intellistream_datahub_sdk.DataHubException): + sync_client.datasets.update([intellistream_datahub_sdk.DatasetUpdate( + dataset.external_id, name=intellistream_datahub_sdk.FieldStr(value="ab") )]) @@ -114,8 +114,8 @@ def test_description_set_null(sync_client, new_dataset): """The only field whose ``setNull`` the dataset service forwards to the resource layer.""" dataset = new_dataset(description="please clear me") - updated = _apply(sync_client, datahub_sdk.DatasetUpdate( - dataset.external_id, description=datahub_sdk.FieldStr(set_null=True) + updated = _apply(sync_client, intellistream_datahub_sdk.DatasetUpdate( + dataset.external_id, description=intellistream_datahub_sdk.FieldStr(set_null=True) )) assert updated.description is None @@ -124,8 +124,8 @@ def test_description_set_null(sync_client, new_dataset): def test_name_set_null(sync_client, new_dataset): dataset = new_dataset(name="Clear my name") - updated = _apply(sync_client, datahub_sdk.DatasetUpdate( - dataset.external_id, name=datahub_sdk.FieldStr(set_null=True) + updated = _apply(sync_client, intellistream_datahub_sdk.DatasetUpdate( + dataset.external_id, name=intellistream_datahub_sdk.FieldStr(set_null=True) )) assert not updated.name @@ -134,8 +134,8 @@ def test_name_set_null(sync_client, new_dataset): def test_external_id_set_null(sync_client, new_dataset): dataset = new_dataset() - updated = _apply(sync_client, datahub_sdk.DatasetUpdate( - dataset.external_id, external_id=datahub_sdk.FieldStr(set_null=True) + updated = _apply(sync_client, intellistream_datahub_sdk.DatasetUpdate( + dataset.external_id, external_id=intellistream_datahub_sdk.FieldStr(set_null=True) )) assert not updated.external_id @@ -147,8 +147,8 @@ def test_external_id_set_null(sync_client, new_dataset): def test_metadata_set_replaces_whole_map(sync_client, new_dataset): dataset = new_dataset(metadata={"a": "1", "b": "2"}) - updated = _apply(sync_client, datahub_sdk.DatasetUpdate( - dataset.external_id, metadata=datahub_sdk.MapField.set({"only": "9"}) + updated = _apply(sync_client, intellistream_datahub_sdk.DatasetUpdate( + dataset.external_id, metadata=intellistream_datahub_sdk.MapField.set({"only": "9"}) )) md = updated.metadata or {} assert md.get("only") == "9" @@ -158,9 +158,9 @@ def test_metadata_set_replaces_whole_map(sync_client, new_dataset): def test_metadata_add_merges_and_overwrites(sync_client, new_dataset): dataset = new_dataset(metadata={"keep": "1", "overwrite": "old"}) - updated = _apply(sync_client, datahub_sdk.DatasetUpdate( + updated = _apply(sync_client, intellistream_datahub_sdk.DatasetUpdate( dataset.external_id, - metadata=datahub_sdk.MapField.delta(add={"overwrite": "new", "added": "2"}), + metadata=intellistream_datahub_sdk.MapField.delta(add={"overwrite": "new", "added": "2"}), )) md = updated.metadata or {} assert md.get("keep") == "1" @@ -171,8 +171,8 @@ def test_metadata_add_merges_and_overwrites(sync_client, new_dataset): def test_metadata_remove_keys(sync_client, new_dataset): dataset = new_dataset(metadata={"a": "1", "b": "2", "c": "3"}) - updated = _apply(sync_client, datahub_sdk.DatasetUpdate( - dataset.external_id, metadata=datahub_sdk.MapField.delta(remove=["b", "c"]) + updated = _apply(sync_client, intellistream_datahub_sdk.DatasetUpdate( + dataset.external_id, metadata=intellistream_datahub_sdk.MapField.delta(remove=["b", "c"]) )) md = updated.metadata or {} assert md.get("a") == "1" @@ -183,8 +183,8 @@ def test_metadata_cleared_by_an_empty_set(sync_client, new_dataset): """``MapField`` has no ``setNull``; an empty ``set`` is how a map is emptied.""" dataset = new_dataset(metadata={"a": "1", "b": "2"}) - updated = _apply(sync_client, datahub_sdk.DatasetUpdate( - dataset.external_id, metadata=datahub_sdk.MapField.set({}) + updated = _apply(sync_client, intellistream_datahub_sdk.DatasetUpdate( + dataset.external_id, metadata=intellistream_datahub_sdk.MapField.set({}) )) assert not (updated.metadata or {}) @@ -196,8 +196,8 @@ def test_metadata_cleared_by_an_empty_set(sync_client, new_dataset): def test_labels_add(sync_client, new_dataset): dataset = new_dataset() - _apply(sync_client, datahub_sdk.DatasetUpdate( - dataset.external_id, labels=datahub_sdk.ListFieldStr.delta(add=["CURATED"]) + _apply(sync_client, intellistream_datahub_sdk.DatasetUpdate( + dataset.external_id, labels=intellistream_datahub_sdk.ListFieldStr.delta(add=["CURATED"]) )) assert _labels_of(sync_client, dataset.external_id) == ["CURATED", "DATASET"] @@ -205,11 +205,11 @@ def test_labels_add(sync_client, new_dataset): def test_labels_remove(sync_client, new_dataset): dataset = new_dataset() - _apply(sync_client, datahub_sdk.DatasetUpdate( - dataset.external_id, labels=datahub_sdk.ListFieldStr.delta(add=["CURATED", "DRAFT"]) + _apply(sync_client, intellistream_datahub_sdk.DatasetUpdate( + dataset.external_id, labels=intellistream_datahub_sdk.ListFieldStr.delta(add=["CURATED", "DRAFT"]) )) - _apply(sync_client, datahub_sdk.DatasetUpdate( - dataset.external_id, labels=datahub_sdk.ListFieldStr.delta(remove=["DRAFT"]) + _apply(sync_client, intellistream_datahub_sdk.DatasetUpdate( + dataset.external_id, labels=intellistream_datahub_sdk.ListFieldStr.delta(remove=["DRAFT"]) )) assert _labels_of(sync_client, dataset.external_id) == ["CURATED", "DATASET"] @@ -217,11 +217,11 @@ def test_labels_remove(sync_client, new_dataset): def test_labels_set_replaces_and_keeps_the_type_label(sync_client, new_dataset): dataset = new_dataset() - _apply(sync_client, datahub_sdk.DatasetUpdate( - dataset.external_id, labels=datahub_sdk.ListFieldStr.delta(add=["OLD"]) + _apply(sync_client, intellistream_datahub_sdk.DatasetUpdate( + dataset.external_id, labels=intellistream_datahub_sdk.ListFieldStr.delta(add=["OLD"]) )) - _apply(sync_client, datahub_sdk.DatasetUpdate( - dataset.external_id, labels=datahub_sdk.ListFieldStr.set(["FRESH"]) + _apply(sync_client, intellistream_datahub_sdk.DatasetUpdate( + dataset.external_id, labels=intellistream_datahub_sdk.ListFieldStr.set(["FRESH"]) )) assert _labels_of(sync_client, dataset.external_id) == ["DATASET", "FRESH"] @@ -229,8 +229,8 @@ def test_labels_set_replaces_and_keeps_the_type_label(sync_client, new_dataset): def test_type_label_cannot_be_removed(sync_client, new_dataset): dataset = new_dataset() - _apply(sync_client, datahub_sdk.DatasetUpdate( - dataset.external_id, labels=datahub_sdk.ListFieldStr.delta(remove=["DATASET"]) + _apply(sync_client, intellistream_datahub_sdk.DatasetUpdate( + dataset.external_id, labels=intellistream_datahub_sdk.ListFieldStr.delta(remove=["DATASET"]) )) assert "DATASET" in _labels_of(sync_client, dataset.external_id) @@ -242,11 +242,11 @@ def test_type_label_cannot_be_removed(sync_client, new_dataset): def test_multiple_fields_in_one_update(sync_client, new_dataset): dataset = new_dataset(description="orig", metadata={"k": "v"}) - updated = _apply(sync_client, datahub_sdk.DatasetUpdate( + updated = _apply(sync_client, intellistream_datahub_sdk.DatasetUpdate( dataset.external_id, - name=datahub_sdk.FieldStr(value="Multi name"), - description=datahub_sdk.FieldStr(value="multi desc"), - metadata=datahub_sdk.MapField.delta(add={"k2": "v2"}), + name=intellistream_datahub_sdk.FieldStr(value="Multi name"), + description=intellistream_datahub_sdk.FieldStr(value="multi desc"), + metadata=intellistream_datahub_sdk.MapField.delta(add={"k2": "v2"}), )) assert updated.name == "Multi name" @@ -258,13 +258,13 @@ def test_multiple_fields_in_one_update(sync_client, new_dataset): def test_update_persists_beyond_the_echo(sync_client, new_dataset): dataset = new_dataset(description="before") - _apply(sync_client, datahub_sdk.DatasetUpdate( - dataset.external_id, description=datahub_sdk.FieldStr(value="after") + _apply(sync_client, intellistream_datahub_sdk.DatasetUpdate( + dataset.external_id, description=intellistream_datahub_sdk.FieldStr(value="after") )) stored = poll_until( - lambda: sync_client.datasets.filter(datahub_sdk.DatasetFilter( - datahub_sdk.BasicDatasetFilter(external_id=[dataset.external_id]) + lambda: sync_client.datasets.filter(intellistream_datahub_sdk.DatasetFilter( + intellistream_datahub_sdk.BasicDatasetFilter(external_id=[dataset.external_id]) )), lambda found: any(d.description == "after" for d in found), ) @@ -274,7 +274,7 @@ def test_update_persists_beyond_the_echo(sync_client, new_dataset): def test_update_targeting_by_numeric_id(sync_client, new_dataset): dataset = new_dataset() - updated = _apply(sync_client, datahub_sdk.DatasetUpdate( - dataset.id, description=datahub_sdk.FieldStr(value="by numeric id") + updated = _apply(sync_client, intellistream_datahub_sdk.DatasetUpdate( + dataset.id, description=intellistream_datahub_sdk.FieldStr(value="by numeric id") )) assert updated.description == "by numeric id" diff --git a/python_tests/test_update_events.py b/python_tests/test_update_events.py index beff7fe..a06ea98 100644 --- a/python_tests/test_update_events.py +++ b/python_tests/test_update_events.py @@ -17,7 +17,7 @@ import pytest -import datahub_sdk +import intellistream_datahub_sdk from fixtures import make_dataset, make_resource, sync_client, unique_id from polling import poll_until @@ -55,9 +55,9 @@ def _make(**kwargs): kwargs.setdefault("external_id", unique_id("evt_upd")) kwargs.setdefault("type", "sdk_update_probe") kwargs.setdefault("event_time", datetime.now(timezone.utc)) - event = sync_client.events.create([datahub_sdk.Event(**kwargs)])[0] + event = sync_client.events.create([intellistream_datahub_sdk.Event(**kwargs)])[0] created_ids.append(event.id) - _apply(sync_client, datahub_sdk.EventUpdate(event)) + _apply(sync_client, intellistream_datahub_sdk.EventUpdate(event)) return event yield _make @@ -91,8 +91,8 @@ def test_scalar_set_value(sync_client, new_event, field, value, attr): source="original_source", ) - updated = _apply(sync_client, datahub_sdk.EventUpdate( - event, **{field: datahub_sdk.FieldStr(value=value)} + updated = _apply(sync_client, intellistream_datahub_sdk.EventUpdate( + event, **{field: intellistream_datahub_sdk.FieldStr(value=value)} )) assert getattr(updated, attr) == value @@ -118,8 +118,8 @@ def test_scalar_set_null(sync_client, new_event, field, attr): source="please_clear_me", ) - updated = _apply(sync_client, datahub_sdk.EventUpdate( - event, **{field: datahub_sdk.FieldStr(set_null=True)} + updated = _apply(sync_client, intellistream_datahub_sdk.EventUpdate( + event, **{field: intellistream_datahub_sdk.FieldStr(set_null=True)} )) assert getattr(updated, attr) is None @@ -133,9 +133,9 @@ def test_type_set_null(sync_client, new_event): """ event = new_event(type="sdk_clearable_type") - with pytest.raises(datahub_sdk.DataHubException): - _apply(sync_client, datahub_sdk.EventUpdate( - event, type=datahub_sdk.FieldStr(set_null=True) + with pytest.raises(intellistream_datahub_sdk.DataHubException): + _apply(sync_client, intellistream_datahub_sdk.EventUpdate( + event, type=intellistream_datahub_sdk.FieldStr(set_null=True) )) @@ -147,8 +147,8 @@ def test_external_id_set_value(sync_client, new_event): event = new_event() new_ext = unique_id("evt_renamed") - updated = _apply(sync_client, datahub_sdk.EventUpdate( - event, external_id=datahub_sdk.FieldStr(value=new_ext) + updated = _apply(sync_client, intellistream_datahub_sdk.EventUpdate( + event, external_id=intellistream_datahub_sdk.FieldStr(value=new_ext) )) assert updated.external_id == new_ext @@ -163,8 +163,8 @@ def test_external_id_set_value(sync_client, new_event): def test_external_id_set_null(sync_client, new_event): event = new_event() - updated = _apply(sync_client, datahub_sdk.EventUpdate( - event, external_id=datahub_sdk.FieldStr(set_null=True) + updated = _apply(sync_client, intellistream_datahub_sdk.EventUpdate( + event, external_id=intellistream_datahub_sdk.FieldStr(set_null=True) )) assert updated.external_id is None @@ -178,8 +178,8 @@ def test_event_time_set_value(sync_client, new_event): event = new_event(event_time=datetime(2025, 1, 1, tzinfo=timezone.utc)) when = datetime(2026, 3, 4, 5, 6, 7, tzinfo=timezone.utc) - updated = _apply(sync_client, datahub_sdk.EventUpdate( - event, event_time=datahub_sdk.FieldStr(value=when.isoformat()) + updated = _apply(sync_client, intellistream_datahub_sdk.EventUpdate( + event, event_time=intellistream_datahub_sdk.FieldStr(value=when.isoformat()) )) assert updated.event_time.replace(microsecond=0) == when @@ -187,9 +187,9 @@ def test_event_time_set_value(sync_client, new_event): def test_event_time_unparseable_is_rejected(sync_client, new_event): event = new_event() - with pytest.raises(datahub_sdk.DataHubException): - sync_client.events.update([datahub_sdk.EventUpdate( - event, event_time=datahub_sdk.FieldStr(value="not a timestamp") + with pytest.raises(intellistream_datahub_sdk.DataHubException): + sync_client.events.update([intellistream_datahub_sdk.EventUpdate( + event, event_time=intellistream_datahub_sdk.FieldStr(value="not a timestamp") )]) @@ -198,8 +198,8 @@ def test_event_time_set_null(sync_client, new_event): when = datetime(2025, 1, 1, tzinfo=timezone.utc) event = new_event(event_time=when) - updated = _apply(sync_client, datahub_sdk.EventUpdate( - event, event_time=datahub_sdk.FieldStr(set_null=True) + updated = _apply(sync_client, intellistream_datahub_sdk.EventUpdate( + event, event_time=intellistream_datahub_sdk.FieldStr(set_null=True) )) assert updated.event_time != when @@ -212,8 +212,8 @@ def test_data_set_id_set_value(sync_client, new_event, make_dataset): dataset = make_dataset(name=unique_id("evt_upd_ds")) event = new_event() - updated = _apply(sync_client, datahub_sdk.EventUpdate( - event, data_set_id=datahub_sdk.FieldU64(value=dataset.id) + updated = _apply(sync_client, intellistream_datahub_sdk.EventUpdate( + event, data_set_id=intellistream_datahub_sdk.FieldU64(value=dataset.id) )) assert updated.data_set_id == dataset.id @@ -223,8 +223,8 @@ def test_data_set_id_set_null(sync_client, new_event, make_dataset): dataset = make_dataset(name=unique_id("evt_upd_ds_null")) event = new_event(data_set_id=dataset.id) - updated = _apply(sync_client, datahub_sdk.EventUpdate( - event, data_set_id=datahub_sdk.FieldU64(set_null=True) + updated = _apply(sync_client, intellistream_datahub_sdk.EventUpdate( + event, data_set_id=intellistream_datahub_sdk.FieldU64(set_null=True) )) assert updated.data_set_id is None @@ -238,9 +238,9 @@ def test_data_set_id_unknown_is_rejected(sync_client, new_event): """The resource update rejects this with 400; the event update accepts it.""" event = new_event() - with pytest.raises(datahub_sdk.DataHubException): - sync_client.events.update([datahub_sdk.EventUpdate( - event, data_set_id=datahub_sdk.FieldU64(value=2**62) + with pytest.raises(intellistream_datahub_sdk.DataHubException): + sync_client.events.update([intellistream_datahub_sdk.EventUpdate( + event, data_set_id=intellistream_datahub_sdk.FieldU64(value=2**62) )]) @@ -251,8 +251,8 @@ def test_data_set_id_unknown_is_rejected(sync_client, new_event): def test_metadata_set_replaces_whole_map(sync_client, new_event): event = new_event(metadata={"a": "1", "b": "2"}) - updated = _apply(sync_client, datahub_sdk.EventUpdate( - event, metadata=datahub_sdk.MapField.set({"only": "9"}) + updated = _apply(sync_client, intellistream_datahub_sdk.EventUpdate( + event, metadata=intellistream_datahub_sdk.MapField.set({"only": "9"}) )) md = updated.metadata or {} assert md.get("only") == "9" @@ -262,8 +262,8 @@ def test_metadata_set_replaces_whole_map(sync_client, new_event): def test_metadata_add_merges_and_overwrites(sync_client, new_event): event = new_event(metadata={"keep": "1", "overwrite": "old"}) - updated = _apply(sync_client, datahub_sdk.EventUpdate( - event, metadata=datahub_sdk.MapField.delta(add={"overwrite": "new", "added": "2"}) + updated = _apply(sync_client, intellistream_datahub_sdk.EventUpdate( + event, metadata=intellistream_datahub_sdk.MapField.delta(add={"overwrite": "new", "added": "2"}) )) md = updated.metadata or {} assert md.get("keep") == "1" @@ -274,8 +274,8 @@ def test_metadata_add_merges_and_overwrites(sync_client, new_event): def test_metadata_remove_keys(sync_client, new_event): event = new_event(metadata={"a": "1", "b": "2", "c": "3"}) - updated = _apply(sync_client, datahub_sdk.EventUpdate( - event, metadata=datahub_sdk.MapField.delta(remove=["b", "c"]) + updated = _apply(sync_client, intellistream_datahub_sdk.EventUpdate( + event, metadata=intellistream_datahub_sdk.MapField.delta(remove=["b", "c"]) )) md = updated.metadata or {} assert md.get("a") == "1" @@ -286,8 +286,8 @@ def test_metadata_cleared_by_an_empty_set(sync_client, new_event): """``MapField`` has no ``setNull``; an empty ``set`` is how a map is emptied.""" event = new_event(metadata={"a": "1", "b": "2"}) - updated = _apply(sync_client, datahub_sdk.EventUpdate( - event, metadata=datahub_sdk.MapField.set({}) + updated = _apply(sync_client, intellistream_datahub_sdk.EventUpdate( + event, metadata=intellistream_datahub_sdk.MapField.set({}) )) assert not (updated.metadata or {}) @@ -300,9 +300,9 @@ def test_metadata_cleared_by_an_empty_set(sync_client, new_event): def two_resources(sync_client, make_resource): ext_a, ext_b = unique_id("evt_rel_a"), unique_id("evt_rel_b") make_resource([ - datahub_sdk.Resource(external_id=ext_a, name="Event update probe A", + intellistream_datahub_sdk.Resource(external_id=ext_a, name="Event update probe A", is_root=True, labels=["ASSET"]), - datahub_sdk.Resource(external_id=ext_b, name="Event update probe B", + intellistream_datahub_sdk.Resource(external_id=ext_b, name="Event update probe B", is_root=True, labels=["ASSET"]), ]) return ext_a, ext_b @@ -314,12 +314,12 @@ def _related_ext_ids(event): def test_related_resources_set_replaces_list(sync_client, new_event, two_resources): ext_a, ext_b = two_resources - event = new_event(related_resources=[datahub_sdk.IdCollection(external_id=ext_a)]) + event = new_event(related_resources=[intellistream_datahub_sdk.IdCollection(external_id=ext_a)]) - updated = _apply(sync_client, datahub_sdk.EventUpdate( + updated = _apply(sync_client, intellistream_datahub_sdk.EventUpdate( event, - related_resources=datahub_sdk.ListFieldIdCollection.set( - [datahub_sdk.IdCollection(external_id=ext_b)] + related_resources=intellistream_datahub_sdk.ListFieldIdCollection.set( + [intellistream_datahub_sdk.IdCollection(external_id=ext_b)] ), )) assert _related_ext_ids(updated) == [ext_b] @@ -327,12 +327,12 @@ def test_related_resources_set_replaces_list(sync_client, new_event, two_resourc def test_related_resources_add_keeps_existing(sync_client, new_event, two_resources): ext_a, ext_b = two_resources - event = new_event(related_resources=[datahub_sdk.IdCollection(external_id=ext_a)]) + event = new_event(related_resources=[intellistream_datahub_sdk.IdCollection(external_id=ext_a)]) - updated = _apply(sync_client, datahub_sdk.EventUpdate( + updated = _apply(sync_client, intellistream_datahub_sdk.EventUpdate( event, - related_resources=datahub_sdk.ListFieldIdCollection.delta( - add=[datahub_sdk.IdCollection(external_id=ext_b)] + related_resources=intellistream_datahub_sdk.ListFieldIdCollection.delta( + add=[intellistream_datahub_sdk.IdCollection(external_id=ext_b)] ), )) assert _related_ext_ids(updated) == sorted([ext_a, ext_b]) @@ -341,14 +341,14 @@ def test_related_resources_add_keeps_existing(sync_client, new_event, two_resour def test_related_resources_remove(sync_client, new_event, two_resources): ext_a, ext_b = two_resources event = new_event(related_resources=[ - datahub_sdk.IdCollection(external_id=ext_a), - datahub_sdk.IdCollection(external_id=ext_b), + intellistream_datahub_sdk.IdCollection(external_id=ext_a), + intellistream_datahub_sdk.IdCollection(external_id=ext_b), ]) - updated = _apply(sync_client, datahub_sdk.EventUpdate( + updated = _apply(sync_client, intellistream_datahub_sdk.EventUpdate( event, - related_resources=datahub_sdk.ListFieldIdCollection.delta( - remove=[datahub_sdk.IdCollection(external_id=ext_a)] + related_resources=intellistream_datahub_sdk.ListFieldIdCollection.delta( + remove=[intellistream_datahub_sdk.IdCollection(external_id=ext_a)] ), )) assert _related_ext_ids(updated) == [ext_b] @@ -357,10 +357,10 @@ def test_related_resources_remove(sync_client, new_event, two_resources): def test_related_resources_cleared_by_an_empty_set(sync_client, new_event, two_resources): """The list has no ``setNull`` either; an empty ``set`` detaches every resource.""" ext_a, _ = two_resources - event = new_event(related_resources=[datahub_sdk.IdCollection(external_id=ext_a)]) + event = new_event(related_resources=[intellistream_datahub_sdk.IdCollection(external_id=ext_a)]) - updated = _apply(sync_client, datahub_sdk.EventUpdate( - event, related_resources=datahub_sdk.ListFieldIdCollection.set([]) + updated = _apply(sync_client, intellistream_datahub_sdk.EventUpdate( + event, related_resources=intellistream_datahub_sdk.ListFieldIdCollection.set([]) )) assert updated.related_resources == [] @@ -368,7 +368,7 @@ def test_related_resources_cleared_by_an_empty_set(sync_client, new_event, two_r def test_related_resource_without_an_identifier_is_rejected(sync_client, new_event): """An entry naming neither an id nor an external id cannot be built at all.""" with pytest.raises(Exception): - datahub_sdk.IdCollection() + intellistream_datahub_sdk.IdCollection() # --------------------------------------------------------------------------- # @@ -378,12 +378,12 @@ def test_related_resource_without_an_identifier_is_rejected(sync_client, new_eve def test_multiple_fields_in_one_update(sync_client, new_event): event = new_event(description="orig", status="new", metadata={"k": "v"}) - updated = _apply(sync_client, datahub_sdk.EventUpdate( + updated = _apply(sync_client, intellistream_datahub_sdk.EventUpdate( event, - description=datahub_sdk.FieldStr(value="multi desc"), - status=datahub_sdk.FieldStr(value="resolved"), - source=datahub_sdk.FieldStr(value="multi_source"), - metadata=datahub_sdk.MapField.delta(add={"k2": "v2"}), + description=intellistream_datahub_sdk.FieldStr(value="multi desc"), + status=intellistream_datahub_sdk.FieldStr(value="resolved"), + source=intellistream_datahub_sdk.FieldStr(value="multi_source"), + metadata=intellistream_datahub_sdk.MapField.delta(add={"k2": "v2"}), )) assert updated.description == "multi desc" @@ -396,8 +396,8 @@ def test_multiple_fields_in_one_update(sync_client, new_event): def test_update_persists_beyond_the_echo(sync_client, new_event): event = new_event(description="before") - _apply(sync_client, datahub_sdk.EventUpdate( - event, description=datahub_sdk.FieldStr(value="after") + _apply(sync_client, intellistream_datahub_sdk.EventUpdate( + event, description=intellistream_datahub_sdk.FieldStr(value="after") )) stored = poll_until( @@ -410,7 +410,7 @@ def test_update_persists_beyond_the_echo(sync_client, new_event): def test_noop_update_preserves_existing_fields(sync_client, new_event): event = new_event(description="keep me", status="new", metadata={"a": "1"}) - updated = _apply(sync_client, datahub_sdk.EventUpdate(event)) + updated = _apply(sync_client, intellistream_datahub_sdk.EventUpdate(event)) assert updated.description == "keep me" assert updated.status == "new" @@ -420,18 +420,18 @@ def test_noop_update_preserves_existing_fields(sync_client, new_event): def test_update_targeting_by_uuid_and_by_external_id(sync_client, new_event): event = new_event(description="orig") - by_uuid = _apply(sync_client, datahub_sdk.EventUpdate( - event.id, description=datahub_sdk.FieldStr(value="by uuid") + by_uuid = _apply(sync_client, intellistream_datahub_sdk.EventUpdate( + event.id, description=intellistream_datahub_sdk.FieldStr(value="by uuid") )) assert by_uuid.description == "by uuid" - by_ext = _apply(sync_client, datahub_sdk.EventUpdate( - event.external_id, description=datahub_sdk.FieldStr(value="by external id") + by_ext = _apply(sync_client, intellistream_datahub_sdk.EventUpdate( + event.external_id, description=intellistream_datahub_sdk.FieldStr(value="by external id") )) assert by_ext.description == "by external id" def test_update_of_an_unknown_event_changes_nothing(sync_client): - assert sync_client.events.update([datahub_sdk.EventUpdate( - uuid.uuid4(), description=datahub_sdk.FieldStr(value="nobody") + assert sync_client.events.update([intellistream_datahub_sdk.EventUpdate( + uuid.uuid4(), description=intellistream_datahub_sdk.FieldStr(value="nobody") )]) == [] diff --git a/python_tests/test_update_resources.py b/python_tests/test_update_resources.py index 3bd9322..3c41b24 100644 --- a/python_tests/test_update_resources.py +++ b/python_tests/test_update_resources.py @@ -10,7 +10,7 @@ """ import pytest -import datahub_sdk +import intellistream_datahub_sdk from fixtures import make_dataset, make_resource, sync_client, unique_id from polling import poll_until @@ -46,7 +46,7 @@ def _make(**kwargs): kwargs.setdefault("name", "SDK update probe") kwargs.setdefault("is_root", True) kwargs.setdefault("labels", ["ASSET"]) - make_resource([datahub_sdk.Resource(**kwargs)]) + make_resource([intellistream_datahub_sdk.Resource(**kwargs)]) return kwargs["external_id"] return _make @@ -67,8 +67,8 @@ def _make(**kwargs): def test_scalar_set_value(sync_client, new_resource, field, value, attr): ext = new_resource(description="original description", source="original_source") - updated = _apply(sync_client, datahub_sdk.ResourceUpdate( - ext, **{field: datahub_sdk.FieldStr(value=value)} + updated = _apply(sync_client, intellistream_datahub_sdk.ResourceUpdate( + ext, **{field: intellistream_datahub_sdk.FieldStr(value=value)} )) assert getattr(updated, attr) == value @@ -77,8 +77,8 @@ def test_external_id_set_value(sync_client, new_resource): ext = new_resource() new_ext = unique_id("res_renamed") - updated = _apply(sync_client, datahub_sdk.ResourceUpdate( - ext, external_id=datahub_sdk.FieldStr(value=new_ext) + updated = _apply(sync_client, intellistream_datahub_sdk.ResourceUpdate( + ext, external_id=intellistream_datahub_sdk.FieldStr(value=new_ext) )) assert updated.external_id == new_ext @@ -94,9 +94,9 @@ def test_name_below_the_minimum_length_is_rejected(sync_client, new_resource): """Names are validated on update, not just on create: 3–512 characters.""" ext = new_resource() - with pytest.raises(datahub_sdk.DataHubException): - sync_client.resources.update([datahub_sdk.ResourceUpdate( - ext, name=datahub_sdk.FieldStr(value="ab") + with pytest.raises(intellistream_datahub_sdk.DataHubException): + sync_client.resources.update([intellistream_datahub_sdk.ResourceUpdate( + ext, name=intellistream_datahub_sdk.FieldStr(value="ab") )]) @@ -114,8 +114,8 @@ def test_name_below_the_minimum_length_is_rejected(sync_client, new_resource): def test_scalar_set_null(sync_client, new_resource, field, attr): ext = new_resource(description="please clear me", source="please_clear_me") - updated = _apply(sync_client, datahub_sdk.ResourceUpdate( - ext, **{field: datahub_sdk.FieldStr(set_null=True)} + updated = _apply(sync_client, intellistream_datahub_sdk.ResourceUpdate( + ext, **{field: intellistream_datahub_sdk.FieldStr(set_null=True)} )) assert getattr(updated, attr) is None @@ -124,8 +124,8 @@ def test_scalar_set_null(sync_client, new_resource, field, attr): def test_name_set_null(sync_client, new_resource): ext = new_resource(name="Clear my name") - updated = _apply(sync_client, datahub_sdk.ResourceUpdate( - ext, name=datahub_sdk.FieldStr(set_null=True) + updated = _apply(sync_client, intellistream_datahub_sdk.ResourceUpdate( + ext, name=intellistream_datahub_sdk.FieldStr(set_null=True) )) assert not updated.name @@ -134,8 +134,8 @@ def test_name_set_null(sync_client, new_resource): def test_external_id_set_null(sync_client, new_resource): ext = new_resource() - updated = _apply(sync_client, datahub_sdk.ResourceUpdate( - ext, external_id=datahub_sdk.FieldStr(set_null=True) + updated = _apply(sync_client, intellistream_datahub_sdk.ResourceUpdate( + ext, external_id=intellistream_datahub_sdk.FieldStr(set_null=True) )) assert not updated.external_id @@ -148,13 +148,13 @@ def test_data_set_id_set_and_null(sync_client, new_resource, make_dataset): dataset = make_dataset(name=unique_id("res_upd_ds")) ext = new_resource() - updated = _apply(sync_client, datahub_sdk.ResourceUpdate( - ext, data_set_id=datahub_sdk.FieldU64(value=dataset.id) + updated = _apply(sync_client, intellistream_datahub_sdk.ResourceUpdate( + ext, data_set_id=intellistream_datahub_sdk.FieldU64(value=dataset.id) )) assert updated.data_set_id == dataset.id - cleared = _apply(sync_client, datahub_sdk.ResourceUpdate( - ext, data_set_id=datahub_sdk.FieldU64(set_null=True) + cleared = _apply(sync_client, intellistream_datahub_sdk.ResourceUpdate( + ext, data_set_id=intellistream_datahub_sdk.FieldU64(set_null=True) )) assert cleared.data_set_id is None @@ -162,9 +162,9 @@ def test_data_set_id_set_and_null(sync_client, new_resource, make_dataset): def test_data_set_id_unknown_is_rejected(sync_client, new_resource): ext = new_resource() - with pytest.raises(datahub_sdk.DataHubException): - sync_client.resources.update([datahub_sdk.ResourceUpdate( - ext, data_set_id=datahub_sdk.FieldU64(value=2**62) + with pytest.raises(intellistream_datahub_sdk.DataHubException): + sync_client.resources.update([intellistream_datahub_sdk.ResourceUpdate( + ext, data_set_id=intellistream_datahub_sdk.FieldU64(value=2**62) )]) @@ -175,8 +175,8 @@ def test_data_set_id_unknown_is_rejected(sync_client, new_resource): def test_metadata_set_replaces_whole_map(sync_client, new_resource): ext = new_resource(metadata={"a": "1", "b": "2"}) - updated = _apply(sync_client, datahub_sdk.ResourceUpdate( - ext, metadata=datahub_sdk.MapField.set({"only": "9"}) + updated = _apply(sync_client, intellistream_datahub_sdk.ResourceUpdate( + ext, metadata=intellistream_datahub_sdk.MapField.set({"only": "9"}) )) md = updated.metadata or {} assert md.get("only") == "9" @@ -186,8 +186,8 @@ def test_metadata_set_replaces_whole_map(sync_client, new_resource): def test_metadata_add_merges_and_overwrites(sync_client, new_resource): ext = new_resource(metadata={"keep": "1", "overwrite": "old"}) - updated = _apply(sync_client, datahub_sdk.ResourceUpdate( - ext, metadata=datahub_sdk.MapField.delta(add={"overwrite": "new", "added": "2"}) + updated = _apply(sync_client, intellistream_datahub_sdk.ResourceUpdate( + ext, metadata=intellistream_datahub_sdk.MapField.delta(add={"overwrite": "new", "added": "2"}) )) md = updated.metadata or {} assert md.get("keep") == "1" @@ -198,8 +198,8 @@ def test_metadata_add_merges_and_overwrites(sync_client, new_resource): def test_metadata_remove_keys(sync_client, new_resource): ext = new_resource(metadata={"a": "1", "b": "2", "c": "3"}) - updated = _apply(sync_client, datahub_sdk.ResourceUpdate( - ext, metadata=datahub_sdk.MapField.delta(remove=["b", "c"]) + updated = _apply(sync_client, intellistream_datahub_sdk.ResourceUpdate( + ext, metadata=intellistream_datahub_sdk.MapField.delta(remove=["b", "c"]) )) md = updated.metadata or {} assert md.get("a") == "1" @@ -210,8 +210,8 @@ def test_metadata_cleared_by_an_empty_set(sync_client, new_resource): """``MapField`` has no ``setNull``; an empty ``set`` is how a map is emptied.""" ext = new_resource(metadata={"a": "1", "b": "2"}) - updated = _apply(sync_client, datahub_sdk.ResourceUpdate( - ext, metadata=datahub_sdk.MapField.set({}) + updated = _apply(sync_client, intellistream_datahub_sdk.ResourceUpdate( + ext, metadata=intellistream_datahub_sdk.MapField.set({}) )) assert not (updated.metadata or {}) @@ -227,8 +227,8 @@ def test_metadata_cleared_by_an_empty_set(sync_client, new_resource): def test_geolocation_set_value(sync_client, new_resource): ext = new_resource(geolocation=_POINT) - updated = _apply(sync_client, datahub_sdk.ResourceUpdate( - ext, geolocation=datahub_sdk.FieldGeoJson(value=_MOVED) + updated = _apply(sync_client, intellistream_datahub_sdk.ResourceUpdate( + ext, geolocation=intellistream_datahub_sdk.FieldGeoJson(value=_MOVED) )) assert updated.geolocation["type"] == "Point" @@ -239,8 +239,8 @@ def test_geolocation_set_value(sync_client, new_resource): def test_geolocation_set_null(sync_client, new_resource): ext = new_resource(geolocation=_POINT) - updated = _apply(sync_client, datahub_sdk.ResourceUpdate( - ext, geolocation=datahub_sdk.FieldGeoJson(set_null=True) + updated = _apply(sync_client, intellistream_datahub_sdk.ResourceUpdate( + ext, geolocation=intellistream_datahub_sdk.FieldGeoJson(set_null=True) )) assert updated.geolocation is None @@ -248,8 +248,8 @@ def test_geolocation_set_null(sync_client, new_resource): def test_geolocation_can_be_added_to_a_resource_that_had_none(sync_client, new_resource): ext = new_resource() - updated = _apply(sync_client, datahub_sdk.ResourceUpdate( - ext, geolocation=datahub_sdk.FieldGeoJson(value=_POINT) + updated = _apply(sync_client, intellistream_datahub_sdk.ResourceUpdate( + ext, geolocation=intellistream_datahub_sdk.FieldGeoJson(value=_POINT) )) assert updated.geolocation["coordinates"] == [10.75, 59.91] @@ -261,8 +261,8 @@ def test_geolocation_accepts_any_geojson_geometry(sync_client, new_resource): "coordinates": [[[10.0, 59.0], [11.0, 59.0], [11.0, 60.0], [10.0, 59.0]]], } - updated = _apply(sync_client, datahub_sdk.ResourceUpdate( - ext, geolocation=datahub_sdk.FieldGeoJson(value=polygon) + updated = _apply(sync_client, intellistream_datahub_sdk.ResourceUpdate( + ext, geolocation=intellistream_datahub_sdk.FieldGeoJson(value=polygon) )) assert updated.geolocation["type"] == "Polygon" @@ -270,7 +270,7 @@ def test_geolocation_accepts_any_geojson_geometry(sync_client, new_resource): def test_geolocation_rejects_non_geojson(): """The dict is parsed into a geometry client-side, so a bad one fails before any request.""" with pytest.raises(ValueError): - datahub_sdk.FieldGeoJson(value={"lat": 59.91, "lon": 10.75}) + intellistream_datahub_sdk.FieldGeoJson(value={"lat": 59.91, "lon": 10.75}) # --------------------------------------------------------------------------- # @@ -280,12 +280,12 @@ def test_geolocation_rejects_non_geojson(): def test_multiple_fields_in_one_update(sync_client, new_resource): ext = new_resource(description="orig", metadata={"k": "v"}) - updated = _apply(sync_client, datahub_sdk.ResourceUpdate( + updated = _apply(sync_client, intellistream_datahub_sdk.ResourceUpdate( ext, - name=datahub_sdk.FieldStr(value="Multi name"), - description=datahub_sdk.FieldStr(value="multi desc"), - source=datahub_sdk.FieldStr(value="multi_source"), - metadata=datahub_sdk.MapField.delta(add={"k2": "v2"}), + name=intellistream_datahub_sdk.FieldStr(value="Multi name"), + description=intellistream_datahub_sdk.FieldStr(value="multi desc"), + source=intellistream_datahub_sdk.FieldStr(value="multi_source"), + metadata=intellistream_datahub_sdk.MapField.delta(add={"k2": "v2"}), )) assert updated.name == "Multi name" @@ -298,8 +298,8 @@ def test_multiple_fields_in_one_update(sync_client, new_resource): def test_update_persists_beyond_the_echo(sync_client, new_resource): ext = new_resource(description="before") - _apply(sync_client, datahub_sdk.ResourceUpdate( - ext, description=datahub_sdk.FieldStr(value="after") + _apply(sync_client, intellistream_datahub_sdk.ResourceUpdate( + ext, description=intellistream_datahub_sdk.FieldStr(value="after") )) stored = poll_until( @@ -312,7 +312,7 @@ def test_update_persists_beyond_the_echo(sync_client, new_resource): def test_noop_update_preserves_existing_fields(sync_client, new_resource): ext = new_resource(description="keep this too", metadata={"a": "1"}) - updated = _apply(sync_client, datahub_sdk.ResourceUpdate(ext)) + updated = _apply(sync_client, intellistream_datahub_sdk.ResourceUpdate(ext)) assert updated.name == "SDK update probe" assert updated.description == "keep this too" @@ -323,7 +323,7 @@ def test_update_targeting_by_numeric_id(sync_client, new_resource): ext = new_resource() node_id = poll_until(lambda: sync_client.resources.by_ids([ext]), bool)[0].id - updated = _apply(sync_client, datahub_sdk.ResourceUpdate( - node_id, description=datahub_sdk.FieldStr(value="by numeric id") + updated = _apply(sync_client, intellistream_datahub_sdk.ResourceUpdate( + node_id, description=intellistream_datahub_sdk.FieldStr(value="by numeric id") )) assert updated.description == "by numeric id" diff --git a/run_python_tests.sh b/run_python_tests.sh index fadb341..3ffff75 100755 --- a/run_python_tests.sh +++ b/run_python_tests.sh @@ -2,7 +2,7 @@ # # Setup-and-run wrapper for the Python (PyO3) test suite. # -# The tests import the *compiled* `datahub_sdk` module, not the Rust sources, so a +# The tests import the *compiled* `intellistream_datahub_sdk` module, not the Rust sources, so a # stale `.so` silently masquerades as a source bug (e.g. ids deserialized as the old # numeric type after the wire format changed to strings). This script always rebuilds # the bindings before running, so the tests reflect the current `src/` and @@ -84,7 +84,7 @@ if [[ $DO_BUILD -eq 1 ]]; then command -v maturin >/dev/null 2>&1 || die "maturin not found; drop --no-deps or 'pip install maturin'" build_flags=() [[ $RELEASE -eq 1 ]] && build_flags+=(--release) - log "Building datahub_sdk bindings with 'maturin develop ${build_flags[*]}'" + log "Building intellistream_datahub_sdk bindings with 'maturin develop ${build_flags[*]}'" ( cd "$BINDINGS_DIR" && maturin develop "${build_flags[@]}" ) else warn "Skipping 'maturin develop' (--no-build): tests run against the existing .so, which may be stale" diff --git a/src/blocking.rs b/src/blocking.rs index b9dfd49..4fadd6d 100644 --- a/src/blocking.rs +++ b/src/blocking.rs @@ -7,7 +7,7 @@ //! there is exactly one implementation of each call. //! //! ```no_run -//! use dataplatform_rust_sdk::blocking; +//! use intellistream_datahub_sdk::blocking; //! //! let api = blocking::create_api_service(); //! let series = api.time_series.search_by_name("engine").unwrap(); diff --git a/src/datasets/mod.rs b/src/datasets/mod.rs index eb8b7eb..0ffb75b 100644 --- a/src/datasets/mod.rs +++ b/src/datasets/mod.rs @@ -272,8 +272,8 @@ impl Dataset { /// and untouched by the server. /// /// ```no_run -/// # use dataplatform_rust_sdk::datasets::DatasetUpdate; -/// # use dataplatform_rust_sdk::fields::Field; +/// # use intellistream_datahub_sdk::datasets::DatasetUpdate; +/// # use intellistream_datahub_sdk::fields::Field; /// let update = DatasetUpdate::by_external_id("sap_work_orders") /// .description(Field::value("SAP work orders — live sync")); /// ``` diff --git a/src/events/mod.rs b/src/events/mod.rs index 5ec1c1e..4301a5c 100644 --- a/src/events/mod.rs +++ b/src/events/mod.rs @@ -728,8 +728,8 @@ impl Default for EventSearch { /// collection fields. For example: /// /// ``` -/// use dataplatform_rust_sdk::events::EventUpdate; -/// use dataplatform_rust_sdk::fields::{Field, MapField}; +/// use intellistream_datahub_sdk::events::EventUpdate; +/// use intellistream_datahub_sdk::fields::{Field, MapField}; /// /// EventUpdate::by_external_id("alarm_x") /// .status(Field::value("acknowledged")) diff --git a/src/filters.rs b/src/filters.rs index 534fa07..ba1f8ae 100644 --- a/src/filters.rs +++ b/src/filters.rs @@ -520,7 +520,7 @@ pub struct RelatedResourceFilter { /// /// ```rust /// use serde_json; -/// use dataplatform_rust_sdk::{filters::Filter}; +/// use intellistream_datahub_sdk::{filters::Filter}; /// use serde_json::json; /// let filter = Filter::And(vec![ /// Filter::Equals { diff --git a/src/lib.rs b/src/lib.rs index 1805030..13ee85e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -85,8 +85,8 @@ pub struct ApiService { /// /// ```no_run /// fn main() { -/// dataplatform_rust_sdk::block_on(async { -/// let api = dataplatform_rust_sdk::create_api_service(); +/// intellistream_datahub_sdk::block_on(async { +/// let api = intellistream_datahub_sdk::create_api_service(); /// // .await SDK calls here /// }); /// } diff --git a/src/multi_tenant_integration.rs b/src/multi_tenant_integration.rs index cf2cf3f..5433ba7 100644 --- a/src/multi_tenant_integration.rs +++ b/src/multi_tenant_integration.rs @@ -50,7 +50,7 @@ //! client per tenant: //! //! ```no_run -//! # use dataplatform_rust_sdk::{datahub::DataHubConfig, ApiService}; +//! # use intellistream_datahub_sdk::{datahub::DataHubConfig, ApiService}; //! let mut config = DataHubConfig::from_env().unwrap(); //! config.set_scope("organization:acme"); // or SCOPE=organization:acme in .env //! let acme = ApiService::new(config.clone()); From fe97afd157921a1210f201a1649747a506048708 Mon Sep 17 00:00:00 2001 From: jgjesdal Date: Thu, 13 Aug 2026 11:13:15 +0200 Subject: [PATCH 02/20] Add a NOTICE file naming the copyright holder Apache-2.0 puts the copyright notice in NOTICE, not in LICENSE: the `Copyright [yyyy] [name of copyright owner]` line in the latter sits inside the "APPENDIX: How to apply the Apache License to your work", which is boilerplate to attach to your own source files. Filling it in would leave the repo with a non-verbatim copy of the licence text for licence scanners to puzzle over. Section 4(d) obliges anyone redistributing the work to carry the NOTICE along, so this is also the part that actually travels. maturin picks it up next to LICENSE without being asked, and both now ship in the wheel and sdist under `dist-info/licenses/`; cargo includes the root copy in the .crate. --- NOTICE | 5 +++++ datahub_python_bindings/NOTICE | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 NOTICE create mode 100644 datahub_python_bindings/NOTICE diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..cdc99fa --- /dev/null +++ b/NOTICE @@ -0,0 +1,5 @@ +IntelliStream DataHub SDK +Copyright 2026 IntelliStream AS + +Licensed under the Apache License, Version 2.0. +See the LICENSE file for the full license text. diff --git a/datahub_python_bindings/NOTICE b/datahub_python_bindings/NOTICE new file mode 100644 index 0000000..cdc99fa --- /dev/null +++ b/datahub_python_bindings/NOTICE @@ -0,0 +1,5 @@ +IntelliStream DataHub SDK +Copyright 2026 IntelliStream AS + +Licensed under the Apache License, Version 2.0. +See the LICENSE file for the full license text. From c5e9e79a2b9a8943135af1777df454a6622c77c0 Mon Sep 17 00:00:00 2001 From: jgjesdal Date: Thu, 13 Aug 2026 11:13:15 +0200 Subject: [PATCH 03/20] Add a release CI workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Builds wheels across linux/musllinux/windows/macos plus an sdist, and publishes to PyPI and crates.io on a version tag. Adapted from `maturin generate-ci` rather than used as generated, in two respects: every maturin step needs `working-directory: datahub_python_bindings`, since running at the repo root would point maturin at the Rust crate's Cargo.toml instead of the bindings; and the free-threaded build steps are dropped, because an abi3-py39 build already yields one wheel covering 3.9+. The guard job is the reason a stray tag cannot publish anything. It checks the three manifest versions agree with each other, and on a tag that the tag matches them, so tagging v0.5.0 while the manifests still say 0.1.0 fails before a single wheel is built. Every other job needs it. Verified against the tree: an unrelated tag, a malformed one and a version mismatch are each rejected. Publishing goes through the `release` environment, so it can be held for manual approval in repo settings, and uses PyPI trusted publishing so no token is stored. crates.io still needs CARGO_REGISTRY_TOKEN — it has no OIDC equivalent. A smoke-test job installs the built linux wheel and imports it, so a wheel that builds but cannot load never reaches the registry. --- .github/workflows/CI.yml | 222 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 .github/workflows/CI.yml diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml new file mode 100644 index 0000000..23cc6cc --- /dev/null +++ b/.github/workflows/CI.yml @@ -0,0 +1,222 @@ +name: CI + +on: + push: + branches: [main] + tags: ['v*'] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +env: + BINDINGS_DIR: datahub_python_bindings + +jobs: + guard: + name: Version guard + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Check manifests agree, and match the tag when there is one + run: | + set -euo pipefail + crate=$(grep -m1 '^version' Cargo.toml | cut -d'"' -f2) + binding=$(grep -m1 '^version' "$BINDINGS_DIR/Cargo.toml" | cut -d'"' -f2) + py=$(grep -m1 '^version' "$BINDINGS_DIR/pyproject.toml" | cut -d'"' -f2) + echo "crate=$crate binding=$binding pyproject=$py" + + if [ "$crate" != "$binding" ] || [ "$crate" != "$py" ]; then + echo "::error::manifest versions disagree: $crate / $binding / $py" + exit 1 + fi + + if [ "${GITHUB_REF_TYPE:-}" = "tag" ]; then + tag="$GITHUB_REF_NAME" + # Final releases only: a prerelease cannot satisfy all three manifests at once, + # since Cargo wants semver (0.5.0-rc.1) and PyPI wants PEP 440 (0.5.0rc1). + if ! printf '%s' "$tag" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$'; then + echo "::error::'$tag' is not a release tag (expected vX.Y.Z)" + exit 1 + fi + if [ "${tag#v}" != "$crate" ]; then + echo "::error::tag $tag does not match manifest version $crate" + exit 1 + fi + fi + + crate: + name: Crate package check + runs-on: ubuntu-latest + needs: guard + steps: + - uses: actions/checkout@v6 + - run: cargo publish --dry-run + + linux: + runs-on: ${{ matrix.platform.runner }} + needs: guard + strategy: + matrix: + platform: + - { runner: ubuntu-22.04, target: x86_64 } + - { runner: ubuntu-22.04, target: x86 } + - { runner: ubuntu-22.04, target: aarch64 } + - { runner: ubuntu-22.04, target: armv7 } + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + with: + python-version: "3.9" + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + working-directory: ${{ env.BINDINGS_DIR }} + target: ${{ matrix.platform.target }} + args: --release --out dist + sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} + manylinux: auto + - uses: actions/upload-artifact@v6 + with: + name: wheels-linux-${{ matrix.platform.target }} + path: ${{ env.BINDINGS_DIR }}/dist + + musllinux: + runs-on: ${{ matrix.platform.runner }} + needs: guard + strategy: + matrix: + platform: + - { runner: ubuntu-22.04, target: x86_64 } + - { runner: ubuntu-22.04, target: aarch64 } + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + with: + python-version: "3.9" + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + working-directory: ${{ env.BINDINGS_DIR }} + target: ${{ matrix.platform.target }} + args: --release --out dist + sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} + manylinux: musllinux_1_2 + - uses: actions/upload-artifact@v6 + with: + name: wheels-musllinux-${{ matrix.platform.target }} + path: ${{ env.BINDINGS_DIR }}/dist + + windows: + runs-on: ${{ matrix.platform.runner }} + needs: guard + strategy: + matrix: + platform: + - { runner: windows-latest, target: x64 } + - { runner: windows-11-arm, target: aarch64 } + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + with: + python-version: "3.9" + architecture: ${{ matrix.platform.target }} + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + working-directory: ${{ env.BINDINGS_DIR }} + target: ${{ matrix.platform.target }} + args: --release --out dist + sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} + - uses: actions/upload-artifact@v6 + with: + name: wheels-windows-${{ matrix.platform.target }} + path: ${{ env.BINDINGS_DIR }}/dist + + macos: + runs-on: ${{ matrix.platform.runner }} + needs: guard + strategy: + matrix: + platform: + - { runner: macos-15-intel, target: x86_64 } + - { runner: macos-latest, target: aarch64 } + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + with: + python-version: "3.9" + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + working-directory: ${{ env.BINDINGS_DIR }} + target: ${{ matrix.platform.target }} + args: --release --out dist + sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} + - uses: actions/upload-artifact@v6 + with: + name: wheels-macos-${{ matrix.platform.target }} + path: ${{ env.BINDINGS_DIR }}/dist + + sdist: + runs-on: ubuntu-latest + needs: guard + steps: + - uses: actions/checkout@v6 + - name: Build sdist + uses: PyO3/maturin-action@v1 + with: + working-directory: ${{ env.BINDINGS_DIR }} + command: sdist + args: --out dist + - uses: actions/upload-artifact@v6 + with: + name: wheels-sdist + path: ${{ env.BINDINGS_DIR }}/dist + + smoke-test: + name: Install and import the wheel + runs-on: ubuntu-latest + needs: linux + steps: + - uses: actions/setup-python@v6 + with: + python-version: "3.9" + - uses: actions/download-artifact@v7 + with: + name: wheels-linux-x86_64 + path: dist + - run: | + pip install dist/*.whl + python -c "import intellistream_datahub_sdk as dh; print(dh.DataHubClient)" + + release-pypi: + name: Publish to PyPI + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') + needs: [crate, linux, musllinux, windows, macos, sdist, smoke-test] + environment: release + permissions: + id-token: write + attestations: write + steps: + - uses: actions/download-artifact@v7 + - name: Generate artifact attestation + uses: actions/attest@v4 + with: + subject-path: 'wheels-*/*' + - uses: astral-sh/setup-uv@v7 + - run: uv publish --trusted-publishing always 'wheels-*/*' + + release-crates: + name: Publish to crates.io + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') + needs: [crate, linux, musllinux, windows, macos, sdist, smoke-test] + environment: release + steps: + - uses: actions/checkout@v6 + - run: cargo publish + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} From 8dac74294e5174756225473099de548a374ee9d0 Mon Sep 17 00:00:00 2001 From: jgjesdal Date: Thu, 13 Aug 2026 12:20:59 +0200 Subject: [PATCH 04/20] Correct the edge docs the api has outgrown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AGENTS.md still said `get` and `by_ids` both answer an unknown id with 200-and-nothing. Api #275 changed that: `get` 404s, batch lookups deliberately did not. The service doc comments were updated when #77 landed; this brings the reference doc in line. `delete` refusing to strand a resource was undocumented in either place. It answers 400 naming the resource in `fields` as `strandedResource` — not the bare 500 an earlier note described, since the server grew a proper problem body in the meantime. Recorded at the call site with the shape a caller will see. No test change here: #80 already covers the rule, and better than a version of this commit written before it landed did — a rooted triangle exercises the stranding rule rather than sidestepping it. --- AGENTS.md | 7 +++++-- src/relations/service.rs | 14 ++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index b21901b..fd73dc8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -43,7 +43,10 @@ This crate is a thin async HTTP SDK around a DataHub-style REST API. Entry point - `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}`. - `resources` (`src/resources/`) — hierarchical asset-like entities; relationship edges live in `src/relations/` (`EdgeProxy`, `RelForm`, `RelatedNode`) -- `edges` (`src/relations/service.rs`) — the `/edges` endpoints: `get`/`by_ids`/`create`/`delete` plus the relationship-type catalogue (`types`/`create_types`). Edges normally come into being through `resources.create(nodes, relations)`; this service is for linking resources that already exist and for reading or deleting an edge on its own. Three server behaviours contradict its OpenAPI and are documented at each call site: `get` and `by_ids` answer an unknown id with 200-and-nothing rather than the documented 404, and `create_types` fails silently on a duplicate name — the unique-hash collision surfaces at commit, after the handler returned, so the caller gets a 200 with an empty *body*, and in a batch the valid new types are rolled back with it. `test_duplicate_relationship_type_conflicts` encodes the intended 409 and is red until the server-side fix lands. +- `edges` (`src/relations/service.rs`) — the `/edges` endpoints: `get`/`by_ids`/`create`/`delete` plus the relationship-type catalogue (`types`/`create_types`). Edges normally come into being through `resources.create(nodes, relations)`; this service is for linking resources that already exist and for reading or deleting an edge on its own. `get` answers an unknown id with 404 and a `problem+json` body; `by_ids`, like every batch lookup, answers 200 with the found subset and silently omits what is missing. (`get` used to be 200-and-nothing despite documenting a 404 — api #275 made single-resource by-id GETs consistently 404 and deliberately left batch lookups alone.) Two further behaviours are worth knowing, and are documented at each call site: + + - `create_types` fails silently on a duplicate name — the unique-hash collision surfaces at commit, after the handler returned, so the caller gets a 200 with an empty *body*, and in a batch the valid new types are rolled back with it. This one does contradict the OpenAPI: `test_duplicate_relationship_type_conflicts` encodes the intended 409 and is red until the server-side fix lands. + - `delete` will not remove an edge that is an endpoint's only route to the graph root: `ResourceService.delete` refuses rather than orphan the node, answering **400** with the stranded resource named in `fields` (`{"type": "strandedResource", "externalId": …}`) and a message saying to include it in the deletion or keep a connecting path. Practically, an edge is separately deletable only when both endpoints stay reachable without it; otherwise it goes away with the resources. - `datasets` (`src/datasets/`) - `files` (`src/files/`) — raw-`PUT` upload via `execute_file_upload_request` (content is the body, metadata rides in `X-Datahub-*` headers), plus directory listing, get/search, `FileUpdate` (rename/move/re-dataset), trash + restore, delete, and download (`download` in memory, `download_to_path` streamed) - `subscriptions` (`src/subscriptions/`) — subscription CRUD, plus `listen.rs`: WebSocket listening against the api's subscription-listen endpoint (`tokio-tungstenite`) @@ -153,7 +156,7 @@ All four searches declare a `filter` of their own entity's type (`SearchAndFilte ## Python bindings (`datahub_python_bindings/`) -A PyO3 crate (built with maturin) that wraps this SDK as the Python package `intellistream-datahub-sdk` (import name `intellistream_datahub_sdk`). Binding modules in `datahub_python_bindings/src/` mirror the Rust subservices; the pure-Python side lives in `datahub_python_bindings/python/intellistream_datahub_sdk`. The platform's `datahub-ml` worker consumes this package, so binding-visible API changes ripple there. +A PyO3 crate (built with maturin) that wraps this SDK as the Python package `intellistream-datahub-sdk` (import name `intellistream_datahub_sdk`). Binding modules in `datahub_python_bindings/src/` mirror the Rust subservices; the pure-Python side lives in `datahub_python_bindings/python/intellistream_datahub_sdk`. The Python test suite in `python_tests/` imports the **compiled** `intellistream_datahub_sdk` module, not the Rust sources — a stale `.so` silently masks source changes. Always run it through `./run_python_tests.sh`, which rebuilds via `maturin develop` first. Extra args are forwarded to pytest (`./run_python_tests.sh -k timeseries`); `--release`, `--no-build`, and `--no-deps` are consumed by the script itself. diff --git a/src/relations/service.rs b/src/relations/service.rs index 22b3a01..fab118f 100644 --- a/src/relations/service.rs +++ b/src/relations/service.rs @@ -87,6 +87,20 @@ impl EdgesService { /// /// Deletes the link only; the resources at each end are untouched. Idempotent: unknown ids are /// silently skipped, so this cannot be used to detect whether an edge existed. + /// + /// It will not, however, delete an edge that is an endpoint's only route to the graph root: + /// `ResourceService.delete` refuses rather than orphan the node, and the call comes back + /// **400** naming the resource that would be stranded: + /// + /// ```json + /// {"error":{"code":400, + /// "fields":[{"externalId":"node_b","type":"strandedResource"}], + /// "message":"Deleting this selection would disconnect resource(s) [node_b] from + /// the graph root. Include them in the deletion or keep a connecting path."}} + /// ``` + /// + /// So an edge is separately deletable only when both endpoints stay reachable without it — + /// otherwise it goes away with the resources it connects. pub async fn delete(&self, json: &I) -> Result, ResponseError> where for<'a> &'a I: Into>, From b8f48cd57614186203500ed8c965b06e01a3fd02 Mon Sep 17 00:00:00 2001 From: jgjesdal Date: Thu, 13 Aug 2026 13:46:50 +0200 Subject: [PATCH 05/20] Check all features in CI `cargo publish` verifies default features only, so a feature-gated module can break without failing a release. `blocking` did exactly that between #88 and #89: it named TimeSeriesFilter and ResourceFilter without importing them, and nothing caught it because plain `cargo build` and `cargo test` never look at that module. 0.5.0 would have shipped a crate advertising a feature that does not build. #89/#90 have since repaired the imports, so this adds only the check that would have caught it: the crate job now builds and test-compiles with --all-features before the dry run. --- .github/workflows/CI.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 23cc6cc..7c135ee 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -52,6 +52,10 @@ jobs: needs: guard steps: - uses: actions/checkout@v6 + # `cargo publish` verifies default features only, so a feature-gated module can break + # without failing the release. `blocking` did exactly that. + - run: cargo build --all-features + - run: cargo test --no-run --all-features - run: cargo publish --dry-run linux: From 4546d6ca0c3d6e62ff766e15b6704ed788fbd769 Mon Sep 17 00:00:00 2001 From: jgjesdal Date: Fri, 14 Aug 2026 08:33:09 +0200 Subject: [PATCH 06/20] Declare the MSRV, and give the bindings crate its own metadata MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit rust-version is 1.85 on both crates. That is the highest rust-version declared by any dependency resolved for the host target — getrandom, jobserver and uuid — and it is also what the bindings crate's 2024 edition requires anyway, so the two agree. (A tree-wide scan reports 1.87, but that comes from wasip2/wasip3/wit-bindgen, which are not in the native graph.) Derived from declared dependency metadata rather than bisected with a 1.85 toolchain, so it is a floor: cargo will now refuse an older compiler with a clear message instead of failing somewhere in a dependency. The bindings crate had no description, license or repository, which cargo warned about on every build of it. It also carries publish = false now: it is built by maturin for PyPI and has a path dependency on the root crate, so it could never be published to crates.io and should not be attempted by accident. --- Cargo.toml | 1 + datahub_python_bindings/Cargo.toml | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 2f7aaff..8823206 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ name = "intellistream-datahub-sdk" version = "0.1.0" edition = "2021" +rust-version = "1.85" description = "Async Rust SDK for the IntelliStream DataHub REST API: time series and datapoints, events, resources and relationship edges, files, datasets, labels and subscriptions." license = "Apache-2.0" repository = "https://github.com/IntelliStream-DataHub/dataplatform-rust-sdk" diff --git a/datahub_python_bindings/Cargo.toml b/datahub_python_bindings/Cargo.toml index 78d83c4..bfff66f 100644 --- a/datahub_python_bindings/Cargo.toml +++ b/datahub_python_bindings/Cargo.toml @@ -2,6 +2,11 @@ name = "datahub_python_bindings" version = "0.1.0" edition = "2024" +rust-version = "1.85" +description = "PyO3 bindings for the IntelliStream DataHub SDK, published to PyPI as intellistream-datahub-sdk." +license = "Apache-2.0" +repository = "https://github.com/IntelliStream-DataHub/dataplatform-rust-sdk" +publish = false [lib] name = "datahub_python_sdk" From 7ce9a76a735a2b5a66c940f407d05a9ed1e3a0d2 Mon Sep 17 00:00:00 2001 From: jgjesdal Date: Fri, 14 Aug 2026 08:38:13 +0200 Subject: [PATCH 07/20] Describe the platforms we actually ship, in the classifiers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "Operating System :: OS Independent" means pure Python. This package ships compiled extension modules, so it was a false claim: name Linux, macOS and Windows instead, matching what the CI matrix builds (manylinux, musllinux, win x64/aarch64, macOS x86_64/aarch64). Adds a development status, the Python versions the abi3-py39 wheel covers, and CPython as the implementation — abi3 is CPython's stable ABI, so PyPy resolves to the sdist and builds from source. Classifiers are descriptive only. What decides whether an install succeeds is the wheel platform tag and requires-python, neither of which this touches. --- datahub_python_bindings/pyproject.toml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/datahub_python_bindings/pyproject.toml b/datahub_python_bindings/pyproject.toml index 5386cb5..db7d8e8 100644 --- a/datahub_python_bindings/pyproject.toml +++ b/datahub_python_bindings/pyproject.toml @@ -18,10 +18,20 @@ authors = [{ name = "IntelliStream AS", email = "support@intellistream.ai" }] license = "Apache-2.0" requires-python = ">=3.9" classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Operating System :: POSIX :: Linux", + "Operating System :: MacOS :: MacOS X", + "Operating System :: Microsoft :: Windows", "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Rust", - "Operating System :: OS Independent", - "Intended Audience :: Developers", "Topic :: Database", "Typing :: Typed", ] From 59cd462f7a86e8382f74d68c23f670f13d0b3643 Mon Sep 17 00:00:00 2001 From: jgjesdal Date: Fri, 14 Aug 2026 08:44:12 +0200 Subject: [PATCH 08/20] Pick registry tags by what peers use and what is actually browsed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There is no "SDK" or "Dataplatform" topic on PyPI — the Topic:: tree is a closed vocabulary. `Software Development :: Libraries` is how an SDK is expressed there, and a data platform is `Database` plus `Scientific/Engineering :: Information Analysis`. That is the set influxdb-client and snowflake-connector-python converged on; cognite-sdk, the closest peer by API shape, publishes no classifiers at all. Also broadens the audience beyond Developers, following databricks-sdk and snowflake. On crates.io the keywords are chosen by browse traffic: `dataplatform` has 0 crates and `datahub` 1, so the brand words carry no discovery on their own. `client` (1800 crates) replaces `api-client` (190), and `time-series` (117) is the more used spelling than `timeseries` (85). `datahub` stays for people looking for this exact thing. Adds the `database` category — Database interfaces — which is what elasticsearch, the nearest equivalent client crate, sits under alongside API bindings. --- Cargo.toml | 4 ++-- datahub_python_bindings/pyproject.toml | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8823206..01bacf8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,8 +7,8 @@ description = "Async Rust SDK for the IntelliStream DataHub REST API: time serie license = "Apache-2.0" repository = "https://github.com/IntelliStream-DataHub/dataplatform-rust-sdk" readme = "README.md" -keywords = ["datahub", "sdk", "timeseries", "iot", "api-client"] -categories = ["api-bindings", "asynchronous"] +keywords = ["datahub", "sdk", "client", "time-series", "iot"] +categories = ["api-bindings", "database", "asynchronous"] exclude = [ "/datahub_python_bindings", "/python_tests", diff --git a/datahub_python_bindings/pyproject.toml b/datahub_python_bindings/pyproject.toml index db7d8e8..aae3602 100644 --- a/datahub_python_bindings/pyproject.toml +++ b/datahub_python_bindings/pyproject.toml @@ -20,6 +20,8 @@ requires-python = ">=3.9" classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", + "Intended Audience :: Information Technology", + "Intended Audience :: Science/Research", "Operating System :: POSIX :: Linux", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", @@ -33,6 +35,9 @@ classifiers = [ "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Rust", "Topic :: Database", + "Topic :: Scientific/Engineering :: Information Analysis", + "Topic :: Software Development :: Libraries", + "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed", ] From 2cc1b56c32b6a25bdb92a4e8c2a6a9670c38733c Mon Sep 17 00:00:00 2001 From: jgjesdal Date: Fri, 14 Aug 2026 08:50:19 +0200 Subject: [PATCH 09/20] Drop time-series for industrial, and name manufacturing as an audience MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a platform client — timeseries are one entity type among events, resources, edges, datasets, files, labels and subscriptions — so leading with time-series described a narrower library than this is. PyPI has no Industrial audience; Manufacturing is the industrial one in that vocabulary, alongside the other sector entries. --- Cargo.toml | 2 +- datahub_python_bindings/pyproject.toml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 01bacf8..cee723a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ description = "Async Rust SDK for the IntelliStream DataHub REST API: time serie license = "Apache-2.0" repository = "https://github.com/IntelliStream-DataHub/dataplatform-rust-sdk" readme = "README.md" -keywords = ["datahub", "sdk", "client", "time-series", "iot"] +keywords = ["datahub", "sdk", "client", "industrial", "iot"] categories = ["api-bindings", "database", "asynchronous"] exclude = [ "/datahub_python_bindings", diff --git a/datahub_python_bindings/pyproject.toml b/datahub_python_bindings/pyproject.toml index aae3602..4ee4ecd 100644 --- a/datahub_python_bindings/pyproject.toml +++ b/datahub_python_bindings/pyproject.toml @@ -21,6 +21,7 @@ classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "Intended Audience :: Information Technology", + "Intended Audience :: Manufacturing", "Intended Audience :: Science/Research", "Operating System :: POSIX :: Linux", "Operating System :: MacOS :: MacOS X", From e8b687d55a3697b470cdb048872f39bedbc7b50d Mon Sep 17 00:00:00 2001 From: jgjesdal Date: Fri, 14 Aug 2026 09:01:44 +0200 Subject: [PATCH 10/20] Name the other sectors the platform serves MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Finance and telco alongside manufacturing. PyPI does not cap classifier count — these are filter facets, and the platform is genuinely used in all three. --- datahub_python_bindings/pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/datahub_python_bindings/pyproject.toml b/datahub_python_bindings/pyproject.toml index 4ee4ecd..b587b6f 100644 --- a/datahub_python_bindings/pyproject.toml +++ b/datahub_python_bindings/pyproject.toml @@ -20,9 +20,11 @@ requires-python = ">=3.9" classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", + "Intended Audience :: Financial and Insurance Industry", "Intended Audience :: Information Technology", "Intended Audience :: Manufacturing", "Intended Audience :: Science/Research", + "Intended Audience :: Telecommunications Industry", "Operating System :: POSIX :: Linux", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", From f9d12f069205cce8af7d564fe9a71bd4a7747b63 Mon Sep 17 00:00:00 2001 From: jgjesdal Date: Fri, 14 Aug 2026 09:41:47 +0200 Subject: [PATCH 11/20] Drop the sector audience claims MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Manufacturing, finance and telco were claims about markets, not about what the library is, and no comparable SDK makes them: cognite-sdk and alpaca-py publish no classifiers at all, boto3 lists only Developers. The sector audiences on PyPI are used by libraries unambiguously of a domain — python-snap7 for PLCs, pysnmp and scapy for network gear, QuantLib for quant finance — not by platform clients. They can go back in a patch release once there are users to point at; both registries show the newest version's metadata, so nothing is lost by waiting. --- datahub_python_bindings/pyproject.toml | 3 --- 1 file changed, 3 deletions(-) diff --git a/datahub_python_bindings/pyproject.toml b/datahub_python_bindings/pyproject.toml index b587b6f..aae3602 100644 --- a/datahub_python_bindings/pyproject.toml +++ b/datahub_python_bindings/pyproject.toml @@ -20,11 +20,8 @@ requires-python = ">=3.9" classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", - "Intended Audience :: Financial and Insurance Industry", "Intended Audience :: Information Technology", - "Intended Audience :: Manufacturing", "Intended Audience :: Science/Research", - "Intended Audience :: Telecommunications Industry", "Operating System :: POSIX :: Linux", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", From 31f5441a874fe8e9ab6c08b8259cfa3e41ef4cfa Mon Sep 17 00:00:00 2001 From: jgjesdal Date: Fri, 14 Aug 2026 10:28:44 +0200 Subject: [PATCH 12/20] Split the workflow into ci and release, per convention MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One file doing PR checks and publishing is not what comparable projects do. PyO3 and maturin both pair ci/test with release.yml; ruff splits further into publish-pypi and publish-crates; cryptography into ci, wheel-builder and pypi-publish. ci.yml and release.yml are the names that show up almost everywhere. The wheel matrix lives in build-wheels.yml, called by both through workflow_call. That is not a third workflow — it has no trigger of its own — it exists so the wheels CI validates are built by the same definition that release ships. Two copies of a four-platform matrix would drift, and the failure mode is silent: you validate one thing and publish another. release.yml keeps the tag checks; ci.yml only asserts the three manifests agree, since there is no tag to compare against on a branch or PR. --- .github/workflows/CI.yml | 226 ----------------------------- .github/workflows/build-wheels.yml | 135 +++++++++++++++++ .github/workflows/ci.yml | 44 ++++++ .github/workflows/release.yml | 77 ++++++++++ 4 files changed, 256 insertions(+), 226 deletions(-) delete mode 100644 .github/workflows/CI.yml create mode 100644 .github/workflows/build-wheels.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml deleted file mode 100644 index 7c135ee..0000000 --- a/.github/workflows/CI.yml +++ /dev/null @@ -1,226 +0,0 @@ -name: CI - -on: - push: - branches: [main] - tags: ['v*'] - pull_request: - workflow_dispatch: - -permissions: - contents: read - -env: - BINDINGS_DIR: datahub_python_bindings - -jobs: - guard: - name: Version guard - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - name: Check manifests agree, and match the tag when there is one - run: | - set -euo pipefail - crate=$(grep -m1 '^version' Cargo.toml | cut -d'"' -f2) - binding=$(grep -m1 '^version' "$BINDINGS_DIR/Cargo.toml" | cut -d'"' -f2) - py=$(grep -m1 '^version' "$BINDINGS_DIR/pyproject.toml" | cut -d'"' -f2) - echo "crate=$crate binding=$binding pyproject=$py" - - if [ "$crate" != "$binding" ] || [ "$crate" != "$py" ]; then - echo "::error::manifest versions disagree: $crate / $binding / $py" - exit 1 - fi - - if [ "${GITHUB_REF_TYPE:-}" = "tag" ]; then - tag="$GITHUB_REF_NAME" - # Final releases only: a prerelease cannot satisfy all three manifests at once, - # since Cargo wants semver (0.5.0-rc.1) and PyPI wants PEP 440 (0.5.0rc1). - if ! printf '%s' "$tag" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$'; then - echo "::error::'$tag' is not a release tag (expected vX.Y.Z)" - exit 1 - fi - if [ "${tag#v}" != "$crate" ]; then - echo "::error::tag $tag does not match manifest version $crate" - exit 1 - fi - fi - - crate: - name: Crate package check - runs-on: ubuntu-latest - needs: guard - steps: - - uses: actions/checkout@v6 - # `cargo publish` verifies default features only, so a feature-gated module can break - # without failing the release. `blocking` did exactly that. - - run: cargo build --all-features - - run: cargo test --no-run --all-features - - run: cargo publish --dry-run - - linux: - runs-on: ${{ matrix.platform.runner }} - needs: guard - strategy: - matrix: - platform: - - { runner: ubuntu-22.04, target: x86_64 } - - { runner: ubuntu-22.04, target: x86 } - - { runner: ubuntu-22.04, target: aarch64 } - - { runner: ubuntu-22.04, target: armv7 } - steps: - - uses: actions/checkout@v6 - - uses: actions/setup-python@v6 - with: - python-version: "3.9" - - name: Build wheels - uses: PyO3/maturin-action@v1 - with: - working-directory: ${{ env.BINDINGS_DIR }} - target: ${{ matrix.platform.target }} - args: --release --out dist - sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} - manylinux: auto - - uses: actions/upload-artifact@v6 - with: - name: wheels-linux-${{ matrix.platform.target }} - path: ${{ env.BINDINGS_DIR }}/dist - - musllinux: - runs-on: ${{ matrix.platform.runner }} - needs: guard - strategy: - matrix: - platform: - - { runner: ubuntu-22.04, target: x86_64 } - - { runner: ubuntu-22.04, target: aarch64 } - steps: - - uses: actions/checkout@v6 - - uses: actions/setup-python@v6 - with: - python-version: "3.9" - - name: Build wheels - uses: PyO3/maturin-action@v1 - with: - working-directory: ${{ env.BINDINGS_DIR }} - target: ${{ matrix.platform.target }} - args: --release --out dist - sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} - manylinux: musllinux_1_2 - - uses: actions/upload-artifact@v6 - with: - name: wheels-musllinux-${{ matrix.platform.target }} - path: ${{ env.BINDINGS_DIR }}/dist - - windows: - runs-on: ${{ matrix.platform.runner }} - needs: guard - strategy: - matrix: - platform: - - { runner: windows-latest, target: x64 } - - { runner: windows-11-arm, target: aarch64 } - steps: - - uses: actions/checkout@v6 - - uses: actions/setup-python@v6 - with: - python-version: "3.9" - architecture: ${{ matrix.platform.target }} - - name: Build wheels - uses: PyO3/maturin-action@v1 - with: - working-directory: ${{ env.BINDINGS_DIR }} - target: ${{ matrix.platform.target }} - args: --release --out dist - sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} - - uses: actions/upload-artifact@v6 - with: - name: wheels-windows-${{ matrix.platform.target }} - path: ${{ env.BINDINGS_DIR }}/dist - - macos: - runs-on: ${{ matrix.platform.runner }} - needs: guard - strategy: - matrix: - platform: - - { runner: macos-15-intel, target: x86_64 } - - { runner: macos-latest, target: aarch64 } - steps: - - uses: actions/checkout@v6 - - uses: actions/setup-python@v6 - with: - python-version: "3.9" - - name: Build wheels - uses: PyO3/maturin-action@v1 - with: - working-directory: ${{ env.BINDINGS_DIR }} - target: ${{ matrix.platform.target }} - args: --release --out dist - sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} - - uses: actions/upload-artifact@v6 - with: - name: wheels-macos-${{ matrix.platform.target }} - path: ${{ env.BINDINGS_DIR }}/dist - - sdist: - runs-on: ubuntu-latest - needs: guard - steps: - - uses: actions/checkout@v6 - - name: Build sdist - uses: PyO3/maturin-action@v1 - with: - working-directory: ${{ env.BINDINGS_DIR }} - command: sdist - args: --out dist - - uses: actions/upload-artifact@v6 - with: - name: wheels-sdist - path: ${{ env.BINDINGS_DIR }}/dist - - smoke-test: - name: Install and import the wheel - runs-on: ubuntu-latest - needs: linux - steps: - - uses: actions/setup-python@v6 - with: - python-version: "3.9" - - uses: actions/download-artifact@v7 - with: - name: wheels-linux-x86_64 - path: dist - - run: | - pip install dist/*.whl - python -c "import intellistream_datahub_sdk as dh; print(dh.DataHubClient)" - - release-pypi: - name: Publish to PyPI - runs-on: ubuntu-latest - if: startsWith(github.ref, 'refs/tags/') - needs: [crate, linux, musllinux, windows, macos, sdist, smoke-test] - environment: release - permissions: - id-token: write - attestations: write - steps: - - uses: actions/download-artifact@v7 - - name: Generate artifact attestation - uses: actions/attest@v4 - with: - subject-path: 'wheels-*/*' - - uses: astral-sh/setup-uv@v7 - - run: uv publish --trusted-publishing always 'wheels-*/*' - - release-crates: - name: Publish to crates.io - runs-on: ubuntu-latest - if: startsWith(github.ref, 'refs/tags/') - needs: [crate, linux, musllinux, windows, macos, sdist, smoke-test] - environment: release - steps: - - uses: actions/checkout@v6 - - run: cargo publish - env: - CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} diff --git a/.github/workflows/build-wheels.yml b/.github/workflows/build-wheels.yml new file mode 100644 index 0000000..8f3812c --- /dev/null +++ b/.github/workflows/build-wheels.yml @@ -0,0 +1,135 @@ +name: Build wheels + +on: + workflow_call: + +permissions: + contents: read + +jobs: + linux: + runs-on: ${{ matrix.platform.runner }} + strategy: + matrix: + platform: + - { runner: ubuntu-22.04, target: x86_64 } + - { runner: ubuntu-22.04, target: x86 } + - { runner: ubuntu-22.04, target: aarch64 } + - { runner: ubuntu-22.04, target: armv7 } + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + with: + python-version: "3.9" + - uses: PyO3/maturin-action@v1 + with: + working-directory: datahub_python_bindings + target: ${{ matrix.platform.target }} + args: --release --out dist + sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} + manylinux: auto + - uses: actions/upload-artifact@v6 + with: + name: wheels-linux-${{ matrix.platform.target }} + path: datahub_python_bindings/dist + + musllinux: + runs-on: ${{ matrix.platform.runner }} + strategy: + matrix: + platform: + - { runner: ubuntu-22.04, target: x86_64 } + - { runner: ubuntu-22.04, target: aarch64 } + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + with: + python-version: "3.9" + - uses: PyO3/maturin-action@v1 + with: + working-directory: datahub_python_bindings + target: ${{ matrix.platform.target }} + args: --release --out dist + sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} + manylinux: musllinux_1_2 + - uses: actions/upload-artifact@v6 + with: + name: wheels-musllinux-${{ matrix.platform.target }} + path: datahub_python_bindings/dist + + windows: + runs-on: ${{ matrix.platform.runner }} + strategy: + matrix: + platform: + - { runner: windows-latest, target: x64 } + - { runner: windows-11-arm, target: aarch64 } + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + with: + python-version: "3.9" + architecture: ${{ matrix.platform.target }} + - uses: PyO3/maturin-action@v1 + with: + working-directory: datahub_python_bindings + target: ${{ matrix.platform.target }} + args: --release --out dist + sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} + - uses: actions/upload-artifact@v6 + with: + name: wheels-windows-${{ matrix.platform.target }} + path: datahub_python_bindings/dist + + macos: + runs-on: ${{ matrix.platform.runner }} + strategy: + matrix: + platform: + - { runner: macos-15-intel, target: x86_64 } + - { runner: macos-latest, target: aarch64 } + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + with: + python-version: "3.9" + - uses: PyO3/maturin-action@v1 + with: + working-directory: datahub_python_bindings + target: ${{ matrix.platform.target }} + args: --release --out dist + sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} + - uses: actions/upload-artifact@v6 + with: + name: wheels-macos-${{ matrix.platform.target }} + path: datahub_python_bindings/dist + + sdist: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: PyO3/maturin-action@v1 + with: + working-directory: datahub_python_bindings + command: sdist + args: --out dist + - uses: actions/upload-artifact@v6 + with: + name: wheels-sdist + path: datahub_python_bindings/dist + + smoke-test: + name: Install and import the wheel + runs-on: ubuntu-latest + needs: linux + steps: + - uses: actions/setup-python@v6 + with: + python-version: "3.9" + - uses: actions/download-artifact@v7 + with: + name: wheels-linux-x86_64 + path: dist + - run: | + pip install dist/*.whl + python -c "import intellistream_datahub_sdk as dh; print(dh.DataHubClient)" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..05d3a5e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,44 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + versions: + name: Manifest versions agree + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - run: | + set -euo pipefail + crate=$(grep -m1 '^version' Cargo.toml | cut -d'"' -f2) + binding=$(grep -m1 '^version' datahub_python_bindings/Cargo.toml | cut -d'"' -f2) + py=$(grep -m1 '^version' datahub_python_bindings/pyproject.toml | cut -d'"' -f2) + echo "crate=$crate binding=$binding pyproject=$py" + if [ "$crate" != "$binding" ] || [ "$crate" != "$py" ]; then + echo "::error::manifest versions disagree: $crate / $binding / $py" + exit 1 + fi + + crate: + name: Crate builds, tests and packages + runs-on: ubuntu-latest + needs: versions + steps: + - uses: actions/checkout@v6 + # cargo publish verifies default features only, so a feature-gated module can break + # without failing a release. `blocking` did exactly that. + - run: cargo build --all-features + - run: cargo test --no-run --all-features + - run: cargo publish --dry-run + + wheels: + name: Wheels + needs: versions + uses: ./.github/workflows/build-wheels.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..b1c8236 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,77 @@ +name: Release + +on: + push: + tags: ['v*'] + +permissions: + contents: read + +jobs: + versions: + name: Tag matches the manifests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - run: | + set -euo pipefail + crate=$(grep -m1 '^version' Cargo.toml | cut -d'"' -f2) + binding=$(grep -m1 '^version' datahub_python_bindings/Cargo.toml | cut -d'"' -f2) + py=$(grep -m1 '^version' datahub_python_bindings/pyproject.toml | cut -d'"' -f2) + echo "crate=$crate binding=$binding pyproject=$py tag=$GITHUB_REF_NAME" + if [ "$crate" != "$binding" ] || [ "$crate" != "$py" ]; then + echo "::error::manifest versions disagree: $crate / $binding / $py" + exit 1 + fi + # Final releases only: a prerelease cannot satisfy all three manifests at once, + # since Cargo wants semver (0.5.0-rc.1) and PyPI wants PEP 440 (0.5.0rc1). + if ! printf '%s' "$GITHUB_REF_NAME" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$'; then + echo "::error::'$GITHUB_REF_NAME' is not a release tag (expected vX.Y.Z)" + exit 1 + fi + if [ "${GITHUB_REF_NAME#v}" != "$crate" ]; then + echo "::error::tag $GITHUB_REF_NAME does not match manifest version $crate" + exit 1 + fi + + crate: + name: Crate builds, tests and packages + runs-on: ubuntu-latest + needs: versions + steps: + - uses: actions/checkout@v6 + - run: cargo build --all-features + - run: cargo test --no-run --all-features + - run: cargo publish --dry-run + + wheels: + name: Wheels + needs: versions + uses: ./.github/workflows/build-wheels.yml + + pypi: + name: Publish to PyPI + runs-on: ubuntu-latest + needs: [crate, wheels] + environment: release + permissions: + id-token: write + attestations: write + steps: + - uses: actions/download-artifact@v7 + - uses: actions/attest@v4 + with: + subject-path: 'wheels-*/*' + - uses: astral-sh/setup-uv@v7 + - run: uv publish --trusted-publishing always 'wheels-*/*' + + crates-io: + name: Publish to crates.io + runs-on: ubuntu-latest + needs: [crate, wheels] + environment: release + steps: + - uses: actions/checkout@v6 + - run: cargo publish + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} From 26fe586868beecb2fd12c5bf969147218e02fda9 Mon Sep 17 00:00:00 2001 From: jgjesdal Date: Fri, 14 Aug 2026 11:04:07 +0200 Subject: [PATCH 13/20] Rehearse the release pipeline on PRs, and skip publishing outside this repo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit release.yml only fired on a tag, so the one time it ever ran was the moment it mattered, doing something that cannot be undone: a half-completed release leaves a version on one registry and not the other, and neither lets you reuse it. None of this has executed on a runner yet — the runner labels, the attestation step and the workflow_call are all still theory. It now also runs on a PR that touches a version or the release machinery. github.ref is refs/pull/N/merge there, never refs/tags/, so the publish jobs skip while everything before them runs for real. The tag checks in the guard are conditional on GITHUB_REF_TYPE for the same reason — a rehearsal has no tag to compare against. Pattern taken from maturin, which rehearses on changes to pyproject.toml, Cargo.toml and the workflow. The repository_owner check is defence in depth rather than the real control: a fork cannot publish here anyway, since it gets none of our secrets and PyPI's trusted publishing pins the owner and repo in the OIDC claim. What it does cover is a fork inside this org, which can see org-level secrets, and it turns a confusing auth failure in someone's fork into a job that simply says skipped. --- .github/workflows/release.yml | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b1c8236..c885e2b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,6 +3,15 @@ name: Release on: push: tags: ['v*'] + # Rehearse the whole pipeline when the version or the release machinery changes, so it is + # not first exercised during an actual release. The publish jobs skip unless this is a tag. + pull_request: + paths: + - Cargo.toml + - datahub_python_bindings/Cargo.toml + - datahub_python_bindings/pyproject.toml + - .github/workflows/release.yml + - .github/workflows/build-wheels.yml permissions: contents: read @@ -18,20 +27,23 @@ jobs: crate=$(grep -m1 '^version' Cargo.toml | cut -d'"' -f2) binding=$(grep -m1 '^version' datahub_python_bindings/Cargo.toml | cut -d'"' -f2) py=$(grep -m1 '^version' datahub_python_bindings/pyproject.toml | cut -d'"' -f2) - echo "crate=$crate binding=$binding pyproject=$py tag=$GITHUB_REF_NAME" + echo "crate=$crate binding=$binding pyproject=$py ref=${GITHUB_REF_NAME:-}" if [ "$crate" != "$binding" ] || [ "$crate" != "$py" ]; then echo "::error::manifest versions disagree: $crate / $binding / $py" exit 1 fi - # Final releases only: a prerelease cannot satisfy all three manifests at once, - # since Cargo wants semver (0.5.0-rc.1) and PyPI wants PEP 440 (0.5.0rc1). - if ! printf '%s' "$GITHUB_REF_NAME" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$'; then - echo "::error::'$GITHUB_REF_NAME' is not a release tag (expected vX.Y.Z)" - exit 1 - fi - if [ "${GITHUB_REF_NAME#v}" != "$crate" ]; then - echo "::error::tag $GITHUB_REF_NAME does not match manifest version $crate" - exit 1 + # Only a tag push carries a version to check against; a rehearsal has none. + if [ "${GITHUB_REF_TYPE:-}" = "tag" ]; then + # Final releases only: a prerelease cannot satisfy all three manifests at once, + # since Cargo wants semver (0.5.0-rc.1) and PyPI wants PEP 440 (0.5.0rc1). + if ! printf '%s' "$GITHUB_REF_NAME" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$'; then + echo "::error::'$GITHUB_REF_NAME' is not a release tag (expected vX.Y.Z)" + exit 1 + fi + if [ "${GITHUB_REF_NAME#v}" != "$crate" ]; then + echo "::error::tag $GITHUB_REF_NAME does not match manifest version $crate" + exit 1 + fi fi crate: @@ -53,6 +65,7 @@ jobs: name: Publish to PyPI runs-on: ubuntu-latest needs: [crate, wheels] + if: startsWith(github.ref, 'refs/tags/') && github.repository_owner == 'IntelliStream-DataHub' environment: release permissions: id-token: write @@ -69,6 +82,7 @@ jobs: name: Publish to crates.io runs-on: ubuntu-latest needs: [crate, wheels] + if: startsWith(github.ref, 'refs/tags/') && github.repository_owner == 'IntelliStream-DataHub' environment: release steps: - uses: actions/checkout@v6 From 2d0c35512f58ff355032dc53767329e32fb817fd Mon Sep 17 00:00:00 2001 From: jgjesdal Date: Fri, 14 Aug 2026 12:49:03 +0200 Subject: [PATCH 14/20] Record that the stranding check races the graph projection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #80 measured it and wrote it into a test helper's doc comment, where a caller will never look: the refusal reads the graph projection, which lags the write, so deleting too soon after creating an edge is wrongly *allowed* — 6 times out of 6 immediately after create, refused 6 out of 6 half a second later. That inverts the rule documented alongside it, so it belongs at the call site and in the reference doc rather than only next to the fixture that works around it. Acting early does not raise; it strands the node. --- AGENTS.md | 2 +- src/relations/service.rs | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index fd73dc8..06021d5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -46,7 +46,7 @@ This crate is a thin async HTTP SDK around a DataHub-style REST API. Entry point - `edges` (`src/relations/service.rs`) — the `/edges` endpoints: `get`/`by_ids`/`create`/`delete` plus the relationship-type catalogue (`types`/`create_types`). Edges normally come into being through `resources.create(nodes, relations)`; this service is for linking resources that already exist and for reading or deleting an edge on its own. `get` answers an unknown id with 404 and a `problem+json` body; `by_ids`, like every batch lookup, answers 200 with the found subset and silently omits what is missing. (`get` used to be 200-and-nothing despite documenting a 404 — api #275 made single-resource by-id GETs consistently 404 and deliberately left batch lookups alone.) Two further behaviours are worth knowing, and are documented at each call site: - `create_types` fails silently on a duplicate name — the unique-hash collision surfaces at commit, after the handler returned, so the caller gets a 200 with an empty *body*, and in a batch the valid new types are rolled back with it. This one does contradict the OpenAPI: `test_duplicate_relationship_type_conflicts` encodes the intended 409 and is red until the server-side fix lands. - - `delete` will not remove an edge that is an endpoint's only route to the graph root: `ResourceService.delete` refuses rather than orphan the node, answering **400** with the stranded resource named in `fields` (`{"type": "strandedResource", "externalId": …}`) and a message saying to include it in the deletion or keep a connecting path. Practically, an edge is separately deletable only when both endpoints stay reachable without it; otherwise it goes away with the resources. + - `delete` will not remove an edge that is an endpoint's only route to the graph root: `ResourceService.delete` refuses rather than orphan the node, answering **400** with the stranded resource named in `fields` (`{"type": "strandedResource", "externalId": …}`) and a message saying to include it in the deletion or keep a connecting path. Practically, an edge is separately deletable only when both endpoints stay reachable without it; otherwise it goes away with the resources. The check reads the graph projection, which lags the write, so deleting too soon after creating the edge gets the *wrong answer* rather than an error — the refusal does not fire and the node is stranded. `relations::tests` measured 6/6 wrongly allowed immediately after create, 6/6 refused 500ms later; its `await_graph` helper is what the live tests wait on. - `datasets` (`src/datasets/`) - `files` (`src/files/`) — raw-`PUT` upload via `execute_file_upload_request` (content is the body, metadata rides in `X-Datahub-*` headers), plus directory listing, get/search, `FileUpdate` (rename/move/re-dataset), trash + restore, delete, and download (`download` in memory, `download_to_path` streamed) - `subscriptions` (`src/subscriptions/`) — subscription CRUD, plus `listen.rs`: WebSocket listening against the api's subscription-listen endpoint (`tokio-tungstenite`) diff --git a/src/relations/service.rs b/src/relations/service.rs index fab118f..7182cc1 100644 --- a/src/relations/service.rs +++ b/src/relations/service.rs @@ -101,6 +101,12 @@ impl EdgesService { /// /// So an edge is separately deletable only when both endpoints stay reachable without it — /// otherwise it goes away with the resources it connects. + /// + /// The check reads the graph projection, which lags the write, and acting too early gets the + /// *wrong answer* rather than an error: the projection cannot yet see what the edge was + /// holding up, so the delete succeeds and strands the node. Measured in `relations::tests`, + /// deleting immediately after creating the edge was wrongly allowed 6 times out of 6, while + /// the same delete 500ms later was refused 6 out of 6. pub async fn delete(&self, json: &I) -> Result, ResponseError> where for<'a> &'a I: Into>, From cc3ea28ac690c3ff69eebd29fb69ca053239a5d0 Mon Sep 17 00:00:00 2001 From: jgjesdal Date: Fri, 14 Aug 2026 13:17:23 +0200 Subject: [PATCH 15/20] Build wheels with a Python the ARM runners actually have MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit setup-python takes x86/x64/arm64, never aarch64, so passing the maturin target through as the architecture failed on windows-11-arm before it got as far as looking for an interpreter. The runner's native architecture is already right, so the input goes away. 3.9 would have failed there regardless: CPython ships no Windows ARM64 build before 3.11. The build interpreter does not reach the artifact — abi3-py39 fixes the tag at cp39 whatever builds it, as the local 3.14 build producing cp39-abi3 shows — so all jobs now use 3.13, which exists everywhere in the matrix. --- .github/workflows/build-wheels.yml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-wheels.yml b/.github/workflows/build-wheels.yml index 8f3812c..f86d4a5 100644 --- a/.github/workflows/build-wheels.yml +++ b/.github/workflows/build-wheels.yml @@ -20,7 +20,7 @@ jobs: - uses: actions/checkout@v6 - uses: actions/setup-python@v6 with: - python-version: "3.9" + python-version: "3.13" - uses: PyO3/maturin-action@v1 with: working-directory: datahub_python_bindings @@ -44,7 +44,7 @@ jobs: - uses: actions/checkout@v6 - uses: actions/setup-python@v6 with: - python-version: "3.9" + python-version: "3.13" - uses: PyO3/maturin-action@v1 with: working-directory: datahub_python_bindings @@ -68,8 +68,7 @@ jobs: - uses: actions/checkout@v6 - uses: actions/setup-python@v6 with: - python-version: "3.9" - architecture: ${{ matrix.platform.target }} + python-version: "3.13" - uses: PyO3/maturin-action@v1 with: working-directory: datahub_python_bindings @@ -92,7 +91,7 @@ jobs: - uses: actions/checkout@v6 - uses: actions/setup-python@v6 with: - python-version: "3.9" + python-version: "3.13" - uses: PyO3/maturin-action@v1 with: working-directory: datahub_python_bindings @@ -125,7 +124,7 @@ jobs: steps: - uses: actions/setup-python@v6 with: - python-version: "3.9" + python-version: "3.13" - uses: actions/download-artifact@v7 with: name: wheels-linux-x86_64 From d0999b6acc36e99cd42db16e9bf9ee96ffcaf62b Mon Sep 17 00:00:00 2001 From: jgjesdal Date: Fri, 14 Aug 2026 13:39:44 +0200 Subject: [PATCH 16/20] Let every wheel target report, instead of cancelling on the first failure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GitHub defaults matrices to fail-fast, so one failing target cancels its siblings mid-run. The result reads as though most of the matrix is broken when a single leg failed and the rest never finished — which is the opposite of what a wheel matrix is for. Each run should say which targets work and which do not. --- .github/workflows/build-wheels.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/build-wheels.yml b/.github/workflows/build-wheels.yml index f86d4a5..65bebcb 100644 --- a/.github/workflows/build-wheels.yml +++ b/.github/workflows/build-wheels.yml @@ -10,6 +10,7 @@ jobs: linux: runs-on: ${{ matrix.platform.runner }} strategy: + fail-fast: false matrix: platform: - { runner: ubuntu-22.04, target: x86_64 } @@ -36,6 +37,7 @@ jobs: musllinux: runs-on: ${{ matrix.platform.runner }} strategy: + fail-fast: false matrix: platform: - { runner: ubuntu-22.04, target: x86_64 } @@ -60,6 +62,7 @@ jobs: windows: runs-on: ${{ matrix.platform.runner }} strategy: + fail-fast: false matrix: platform: - { runner: windows-latest, target: x64 } @@ -83,6 +86,7 @@ jobs: macos: runs-on: ${{ matrix.platform.runner }} strategy: + fail-fast: false matrix: platform: - { runner: macos-15-intel, target: x86_64 } From e9c5024044bd59fb86197c3b6150259a1fcbc3a0 Mon Sep 17 00:00:00 2001 From: jgjesdal Date: Fri, 14 Aug 2026 13:40:31 +0200 Subject: [PATCH 17/20] Stop wrapping the compiler in sccache MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ring compiles hand-written .S files for its ARM primitives, and sccache mishandles assembly: the architecture predefines the cross gcc would set never reach the file, so ring's asm_base.h aborts with "ARM assembler must define __ARM_ARCH". It only shows on the cross-compiled ARM targets, which is why the native ones looked fine. ring is not optional here — it is rustls's default crypto provider — so the cache goes instead. It was saving compile time on a build that has to be correct first. --- .github/workflows/build-wheels.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/build-wheels.yml b/.github/workflows/build-wheels.yml index 65bebcb..bd0429f 100644 --- a/.github/workflows/build-wheels.yml +++ b/.github/workflows/build-wheels.yml @@ -27,7 +27,6 @@ jobs: working-directory: datahub_python_bindings target: ${{ matrix.platform.target }} args: --release --out dist - sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} manylinux: auto - uses: actions/upload-artifact@v6 with: @@ -52,7 +51,6 @@ jobs: working-directory: datahub_python_bindings target: ${{ matrix.platform.target }} args: --release --out dist - sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} manylinux: musllinux_1_2 - uses: actions/upload-artifact@v6 with: @@ -77,7 +75,6 @@ jobs: working-directory: datahub_python_bindings target: ${{ matrix.platform.target }} args: --release --out dist - sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} - uses: actions/upload-artifact@v6 with: name: wheels-windows-${{ matrix.platform.target }} @@ -101,7 +98,6 @@ jobs: working-directory: datahub_python_bindings target: ${{ matrix.platform.target }} args: --release --out dist - sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} - uses: actions/upload-artifact@v6 with: name: wheels-macos-${{ matrix.platform.target }} From 589a1a12b4201978f2f5e4045d46257b58cf38ff Mon Sep 17 00:00:00 2001 From: jgjesdal Date: Fri, 14 Aug 2026 13:50:40 +0200 Subject: [PATCH 18/20] Cache the Rust build directory between wheel runs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every leg recompiles the full tree from scratch, which dominates the run. rust-cache caches directories rather than wrapping the compiler, so it does not reintroduce the assembly problem that made sccache unusable here. Keyed per target: cross-compiled artifacts live under target/ and must not be restored into a run building a different one. Expect partial benefit on the Linux legs. maturin runs those inside a container and only bind-mounts the workspace, so target/ persists to the host and is cached, while the container's own cargo registry is not — the downloads repeat, the compiles do not. The native Windows and macOS legs cache both. --- .github/workflows/build-wheels.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/build-wheels.yml b/.github/workflows/build-wheels.yml index bd0429f..af466dd 100644 --- a/.github/workflows/build-wheels.yml +++ b/.github/workflows/build-wheels.yml @@ -22,6 +22,10 @@ jobs: - uses: actions/setup-python@v6 with: python-version: "3.13" + - uses: Swatinem/rust-cache@v2 + with: + workspaces: datahub_python_bindings + key: ${{ matrix.platform.target }} - uses: PyO3/maturin-action@v1 with: working-directory: datahub_python_bindings @@ -46,6 +50,10 @@ jobs: - uses: actions/setup-python@v6 with: python-version: "3.13" + - uses: Swatinem/rust-cache@v2 + with: + workspaces: datahub_python_bindings + key: ${{ matrix.platform.target }} - uses: PyO3/maturin-action@v1 with: working-directory: datahub_python_bindings @@ -70,6 +78,10 @@ jobs: - uses: actions/setup-python@v6 with: python-version: "3.13" + - uses: Swatinem/rust-cache@v2 + with: + workspaces: datahub_python_bindings + key: ${{ matrix.platform.target }} - uses: PyO3/maturin-action@v1 with: working-directory: datahub_python_bindings @@ -93,6 +105,10 @@ jobs: - uses: actions/setup-python@v6 with: python-version: "3.13" + - uses: Swatinem/rust-cache@v2 + with: + workspaces: datahub_python_bindings + key: ${{ matrix.platform.target }} - uses: PyO3/maturin-action@v1 with: working-directory: datahub_python_bindings From f06ae54ea401f191050ec81cd451075d5f828812 Mon Sep 17 00:00:00 2001 From: jgjesdal Date: Fri, 14 Aug 2026 14:22:31 +0200 Subject: [PATCH 19/20] Build the aarch64 wheel on manylinux_2_28 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit manylinux2014 is CentOS 7, whose GCC 4.8 does not define __ARM_ARCH when preprocessing .S files, and ring's asm_base.h refuses to assemble without it. Only aarch64 is affected; the other Linux targets keep manylinux2014 and its lower glibc floor. Not sccache, as the previous commit assumed — the same failure occurs with the compiler invoked directly. Removing it was harmless since rust-cache covers caching better, but it was not the cause. Costs aarch64 users glibc 2.17-2.27: RHEL 7 and Ubuntu 16.04 era, both long EOL. glibc 2.28 covers RHEL 8, Debian 10 and Ubuntu 18.10 onwards. --- .github/workflows/build-wheels.yml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-wheels.yml b/.github/workflows/build-wheels.yml index af466dd..9e0d90b 100644 --- a/.github/workflows/build-wheels.yml +++ b/.github/workflows/build-wheels.yml @@ -13,10 +13,12 @@ jobs: fail-fast: false matrix: platform: - - { runner: ubuntu-22.04, target: x86_64 } - - { runner: ubuntu-22.04, target: x86 } - - { runner: ubuntu-22.04, target: aarch64 } - - { runner: ubuntu-22.04, target: armv7 } + - { runner: ubuntu-22.04, target: x86_64, manylinux: auto } + - { runner: ubuntu-22.04, target: x86, manylinux: auto } + # manylinux2014 is CentOS 7 with GCC 4.8, which does not define __ARM_ARCH when + # preprocessing .S files; ring's asm_base.h refuses to assemble without it. + - { runner: ubuntu-22.04, target: aarch64, manylinux: "2_28" } + - { runner: ubuntu-22.04, target: armv7, manylinux: auto } steps: - uses: actions/checkout@v6 - uses: actions/setup-python@v6 @@ -31,7 +33,7 @@ jobs: working-directory: datahub_python_bindings target: ${{ matrix.platform.target }} args: --release --out dist - manylinux: auto + manylinux: ${{ matrix.platform.manylinux }} - uses: actions/upload-artifact@v6 with: name: wheels-linux-${{ matrix.platform.target }} From be0ff09b808b7df79e2377d485713d7989855cac Mon Sep 17 00:00:00 2001 From: jgjesdal Date: Fri, 14 Aug 2026 13:29:46 +0200 Subject: [PATCH 20/20] Use rustls instead of native-tls, and drop the OpenSSL dependency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit openssl-sys was reaching us three ways — reqwest, oauth2 and tokio-tungstenite all defaulted to native-tls — and it is what breaks the aarch64-musl cross build: openssl-sys wants a native OpenSSL for the target and maturin's container has none. rustls was already in the tree. oauth2 5.x defaults to rustls-tls, so both stacks were being compiled and only one was doing any work. This mostly removes the redundant one: reqwest gets default-features = false plus rustls-tls-native-roots (and charset, http2, system-proxy back, which default-tls took with it), and tokio-tungstenite swaps native-tls for rustls-tls-native-roots. native-roots rather than webpki-roots deliberately: it keeps reading the OS trust store, so an internal CA or a TLS-inspecting proxy still works. Bundling Mozilla's root set would reject exactly the corporate setups this SDK is deployed into. Two consequences beyond the build fix. The wheel no longer vendors libcrypto/libssl, so nothing of ours can collide with the OpenSSL another native extension has already loaded. And with no OpenSSL to vendor, auditwheel's glibc floor drops from manylinux_2_38 to manylinux_2_34, widening the range of distributions that can install it. Verified: builds with --all-features, tests compile, the offline unit tests pass, real HTTPS handshakes succeed against public hosts, and the wheel builds with an empty .libs/. --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index cee723a..cfcfb64 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,7 @@ tokio = { version = "1", features = ["full"] } serde = { version = "1", features = ["derive"] } serde_json = "1" oauth2 = { version = "5.0.0", features = ["reqwest-blocking"] } -reqwest = { version = "0.12", features = ["blocking", "json", "multipart", "stream"] } +reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "multipart", "stream", "rustls-tls-native-roots", "charset", "http2", "system-proxy"] } chrono = { version ="0.4", features = ["serde"] } rand = "0.9" futures = "0.3" @@ -34,7 +34,7 @@ dotenv = "0.15" uuid = { version = "1.16", features = ["v4", "v7", "serde"] } regex = "1.11.1" tokio-util = "0.7" -tokio-tungstenite = { version = "0.24", features = ["native-tls"] } +tokio-tungstenite = { version = "0.24", features = ["rustls-tls-native-roots"] } infer = { version = "0.19", default-features = false, features = ["std"] } thiserror = "2.0.17" zstd = "0.13"