Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
6e1eb3b
Rename the package to intellistream-datahub-sdk and add publishing me…
JosteinGj Aug 13, 2026
fe97afd
Add a NOTICE file naming the copyright holder
JosteinGj Aug 13, 2026
c5e9e79
Add a release CI workflow
JosteinGj Aug 13, 2026
8dac742
Correct the edge docs the api has outgrown
JosteinGj Aug 13, 2026
b8f48cd
Check all features in CI
JosteinGj Aug 13, 2026
4546d6c
Declare the MSRV, and give the bindings crate its own metadata
JosteinGj Aug 14, 2026
7ce9a76
Describe the platforms we actually ship, in the classifiers
JosteinGj Aug 14, 2026
59cd462
Pick registry tags by what peers use and what is actually browsed
JosteinGj Aug 14, 2026
2cc1b56
Drop time-series for industrial, and name manufacturing as an audience
JosteinGj Aug 14, 2026
e8b687d
Name the other sectors the platform serves
JosteinGj Aug 14, 2026
f9d12f0
Drop the sector audience claims
JosteinGj Aug 14, 2026
31f5441
Split the workflow into ci and release, per convention
JosteinGj Aug 14, 2026
26fe586
Rehearse the release pipeline on PRs, and skip publishing outside thi…
JosteinGj Aug 14, 2026
2d0c355
Record that the stranding check races the graph projection
JosteinGj Aug 14, 2026
cc3ea28
Build wheels with a Python the ARM runners actually have
JosteinGj Aug 14, 2026
d0999b6
Let every wheel target report, instead of cancelling on the first fai…
JosteinGj Aug 14, 2026
e9c5024
Stop wrapping the compiler in sccache
JosteinGj Aug 14, 2026
589a1a1
Cache the Rust build directory between wheel runs
JosteinGj Aug 14, 2026
f06ae54
Build the aarch64 wheel on manylinux_2_28
JosteinGj Aug 14, 2026
be0ff09
Use rustls instead of native-tls, and drop the OpenSSL dependency
JosteinGj Aug 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 152 additions & 0 deletions .github/workflows/build-wheels.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
name: Build wheels

on:
workflow_call:

permissions:
contents: read

jobs:
linux:
runs-on: ${{ matrix.platform.runner }}
strategy:
fail-fast: false
matrix:
platform:
- { 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
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
target: ${{ matrix.platform.target }}
args: --release --out dist
manylinux: ${{ matrix.platform.manylinux }}
- uses: actions/upload-artifact@v6
with:
name: wheels-linux-${{ matrix.platform.target }}
path: datahub_python_bindings/dist

musllinux:
runs-on: ${{ matrix.platform.runner }}
strategy:
fail-fast: false
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.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
target: ${{ matrix.platform.target }}
args: --release --out dist
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:
fail-fast: false
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.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
target: ${{ matrix.platform.target }}
args: --release --out dist
- uses: actions/upload-artifact@v6
with:
name: wheels-windows-${{ matrix.platform.target }}
path: datahub_python_bindings/dist

macos:
runs-on: ${{ matrix.platform.runner }}
strategy:
fail-fast: false
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.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
target: ${{ matrix.platform.target }}
args: --release --out dist
- 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.13"
- 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)"
44 changes: 44 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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
91 changes: 91 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
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

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 ref=${GITHUB_REF_NAME:-}"
if [ "$crate" != "$binding" ] || [ "$crate" != "$py" ]; then
echo "::error::manifest versions disagree: $crate / $binding / $py"
exit 1
fi
# 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:
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]
if: startsWith(github.ref, 'refs/tags/') && github.repository_owner == 'IntelliStream-DataHub'
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]
if: startsWith(github.ref, 'refs/tags/') && github.repository_owner == 'IntelliStream-DataHub'
environment: release
steps:
- uses: actions/checkout@v6
- run: cargo publish
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 6 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. 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`)
Expand Down Expand Up @@ -153,9 +156,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 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

Expand Down
21 changes: 17 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
[package]
name = "dataplatform-rust-sdk"
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"
readme = "README.md"
keywords = ["datahub", "sdk", "client", "industrial", "iot"]
categories = ["api-bindings", "database", "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]
Expand All @@ -13,15 +26,15 @@ 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"
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"
Expand Down
5 changes: 5 additions & 0 deletions NOTICE
Original file line number Diff line number Diff line change
@@ -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.
Loading