From 2287b857bd29fe3907f7d9e87a900cb341c1a544 Mon Sep 17 00:00:00 2001 From: jiangxt2 Date: Tue, 6 Oct 2026 13:57:44 +0800 Subject: [PATCH 1/4] feat(data): stabilize source configuration and dataset references Signed-off-by: jiangxt2 --- ci/test-suites.json | 31 ++++ docs/STABILITY.md | 14 +- docs/architecture/version-policy.md | 6 +- docs/reference/support-matrix.md | 1 + pyproject.toml | 8 + src/tributo/data/contracts/storage.py | 6 +- src/tributo/data/refs.py | 12 +- src/tributo/data/source_config.py | 32 ++-- tests/data/test_source_contract_compat.py | 57 +++++++ tests/fixtures/data-source-contract-v1.json | 152 +++++++++++++++++++ tests/integration/test_dowhy_scipy_compat.py | 88 +++++++++++ tests/test_stability_inventory.py | 13 +- uv.lock | 115 +++++++++++--- 13 files changed, 486 insertions(+), 49 deletions(-) create mode 100644 tests/data/test_source_contract_compat.py create mode 100644 tests/fixtures/data-source-contract-v1.json create mode 100644 tests/integration/test_dowhy_scipy_compat.py diff --git a/ci/test-suites.json b/ci/test-suites.json index ac804e3..5741208 100644 --- a/ci/test-suites.json +++ b/ci/test-suites.json @@ -171,6 +171,37 @@ "log_contract": "pytest terminal output; skips and missing JUnit evidence fail the suite", "rationale": "Dedicated bounded shard for integration-named modules that exercise local contracts without external infrastructure." }, + { + "id": "causal-dependency-contract", + "owner": "training", + "domain": "dependencies", + "tier": "ci_fast", + "workflow": "matrix", + "owns_tests": true, + "entrypoint": ["python", "-m", "pytest"], + "args": [ + "tests/integration/test_dowhy_scipy_compat.py", + "-o", + "addopts=", + "-q", + "--tb=short" + ], + "extras": ["dev", "causal"], + "test_paths": ["tests/integration/test_dowhy_scipy_compat.py"], + "trigger_domains": [], + "trigger_paths": [ + "pyproject.toml", + "uv.lock", + "tests/integration/test_dowhy_scipy_compat.py" + ], + "requires": [], + "budget_seconds": 180, + "ci_allowed": true, + "forbid_skips": true, + "markers": ["ci_safe"], + "log_contract": "pytest output and JUnit evidence with no dependency skips", + "rationale": "Bounded real DoWhy estimation, refutation, and GCM calls validate the locked SciPy closure without services." + }, { "id": "runtime-image", "owner": "core", diff --git a/docs/STABILITY.md b/docs/STABILITY.md index 5da0dfe..3a121d4 100644 --- a/docs/STABILITY.md +++ b/docs/STABILITY.md @@ -112,7 +112,8 @@ from the legacy setup-only propagation rule. | Module | Level | Notes | |--------|-------|-------| -| `tributo.data.source_config` — `SourceConfig` | `beta` | Strict in-memory contract; JSON is the built-in persisted format | +| `tributo.data.source_config` — canonical models and projection helpers | `stable` | Stable fields, defaults, JSON shapes, and projection semantics; `BuiltinSourceConfig`, `SourceConfig`, and `CanonicalSourceInput` use the same stable members | +| `tributo.data.source_config` — legacy conversion and `RawSourceConfig` | `beta` | Compatibility input conversion and unknown-source passthrough | | `tributo.data.provider` — `DataSourceProvider` | `beta` | Logical normalization/planning contract; `normalize()` and `plan()` drive canonical ingestion, while `open()`/`DatasetHandle` remain an independent Provider SPI | | `tributo.data.transform_ir` | `alpha` | Versioned engine-neutral ETL contract | | `tributo.data.transform_compiler` | `developer` | Internal Ray/Daft expression translation | @@ -121,7 +122,7 @@ from the legacy setup-only propagation rule. | `tributo.data.handle_adapters` | `alpha` | Explicit native-handle conversions with conversion evidence; never a routing fallback | | `tributo.data.contracts.handles` | `alpha` | Typed Ray and Daft handle contracts shared by ingestion and writing | | `tributo.data.contracts.modes` | `beta` | Canonical shared `WriteMode` contract; `tributo.data.base` remains a narrow re-export | -| `tributo.data.contracts.storage` | `beta` | Canonical shared `S3Config` contract; credentials are hidden from repr and identity material | +| `tributo.data.contracts.storage` | `stable` | Canonical shared `S3Config` contract; credentials are hidden from repr and identity material | | `tributo.data.writing` | `alpha` | Unified bounded-write Gateway package | | `tributo.data.writing.capabilities` | `alpha` | Native writer capability declarations | | `tributo.data.writing.contracts` | `alpha` | Credential-safe write requests, descriptors, receipts, and errors | @@ -133,7 +134,14 @@ from the legacy setup-only propagation rule. | `tributo.data.graph` | `beta` | Graph data abstraction (GNN; @PublicAPI says beta) | | `tributo.data.base` | `legacy` | Narrow compatibility re-export for `S3Config` and `WriteMode`; no reader, writer, registry, or Connector class | | `tributo.data.provider_registry` | `beta` | Data source provider registry | -| `tributo.data.refs` | `beta` | Data reference value objects | +| `tributo.data.refs` | `stable` | DatasetRef fields, canonical JSON digest, version-1 source identity, and Arrow schema fingerprint | + +Canonical configuration stability does not imply that a Provider, Gateway, +Binding, database, or engine combination is stable or supported. Provider +options retain their provider-owned meaning. Credentials belong to trusted +configuration only: repr hides designated credential fields, while model dumps +and validation details must be sanitized before public logging. DatasetRef and +source identity reject credential material rather than persist it. ### Distributed Lance vector indexing (tributo.vector_index.*) diff --git a/docs/architecture/version-policy.md b/docs/architecture/version-policy.md index d346be9..c7ba6b4 100644 --- a/docs/architecture/version-policy.md +++ b/docs/architecture/version-policy.md @@ -36,8 +36,10 @@ Every public symbol falls into one of these tiers. The canonical inventory is - `alpha` → `beta`: The API has been used in at least one real workload and the interface is unlikely to change significantly. -- `beta` → `stable`: The API has been stable for ≥ 2 minor versions with no - reported interface issues. +- `beta` → `stable`: The public contract, supported scope, and limitations are + documented; compatibility and relevant functional validation pass; no known + unresolved interface issues affect that scope. There is no minimum release + count or waiting period. - `stable` → `deprecated`: A replacement exists, the deprecation window has started, and the migration guide is published. diff --git a/docs/reference/support-matrix.md b/docs/reference/support-matrix.md index de95f67..c27aeba 100644 --- a/docs/reference/support-matrix.md +++ b/docs/reference/support-matrix.md @@ -42,6 +42,7 @@ endpoint. | Capability | Status | Boundary | | --- | --- | --- | +| Canonical source configuration and DatasetRef identity | Stable | Existing JSON shapes, fields, defaults, projection, and identity version 1; Provider/Gateway/Binding execution and provider-private options keep their own contracts | | Local/S3 Parquet and CSV reads | Verified | Native Ray Data or Daft handle through one Gateway | | Local/S3 Iceberg reads | Verified | Built-in bindings use PyIceberg `>=0.11.1,<0.12.0` with `PyArrowFileIO`; Ray may push `row_filter` into the scan, Daft applies it as a lazy residual filter, and empty-table schema is preserved from Iceberg metadata; broader Catalog/delete-file matrix remains gated | | Local/S3 Lance reads | Verified | Native Ray Data or Daft table reader; numeric versions and tags are supported, Daft also supports as-of, and Iceberg snapshot references fail closed | diff --git a/pyproject.toml b/pyproject.toml index cf63d5b..3e0c918 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -333,6 +333,8 @@ disallow_untyped_defs = false [dependency-groups] dev = [ + # Select the compatible official wheel without exceeding DoWhy 0.14 constraints. + "scipy==1.15.3; sys_platform == 'darwin' and platform_machine == 'arm64' and python_version == '3.12'", "pytest-html>=4.2.0", "grpcio-tools>=1.60.0", "boto3>=1.42.91", @@ -348,3 +350,9 @@ dev = [ "ml-dtypes>=0.5.0", "skl2onnx>=1.17.0", ] + +[tool.uv.sources] +# The macOS 14 arm64 wheel fails PROPACK loading on recent macOS. +# Keep the same SciPy release and the upstream DoWhy requirements. +# https://github.com/scipy/scipy/issues/25635 +scipy = { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl", marker = "sys_platform == 'darwin' and platform_machine == 'arm64' and python_version == '3.12'" } diff --git a/src/tributo/data/contracts/storage.py b/src/tributo/data/contracts/storage.py index 44cacd3..52c85f6 100644 --- a/src/tributo/data/contracts/storage.py +++ b/src/tributo/data/contracts/storage.py @@ -12,7 +12,7 @@ __all__ = ["S3Config"] -@PublicAPI(stability="beta") +@PublicAPI(stability="stable") class S3Config(StrictConfigModel): """S3 connection configuration, shared across data modules. @@ -21,6 +21,10 @@ class S3Config(StrictConfigModel): ``AWS_ACCESS_KEY_ID`` → access_key_id, ``AWS_SECRET_ACCESS_KEY`` → secret_access_key, ``AWS_REGION`` → region. + + The model stores supplied values; the consuming Binding resolves fallbacks. + Credentials are excluded from repr, but model dumps and validation details + are trusted configuration data and must not be exposed as public logs. """ access_key_id: Optional[str] = Field( diff --git a/src/tributo/data/refs.py b/src/tributo/data/refs.py index 39a9407..3e2e9be 100644 --- a/src/tributo/data/refs.py +++ b/src/tributo/data/refs.py @@ -151,7 +151,7 @@ def _canonical_json(value: Any) -> str: ) -@PublicAPI(stability="beta") +@PublicAPI(stability="stable") def digest(value: Any) -> str: """SHA-256 of the canonical JSON representation of *value*. @@ -162,7 +162,7 @@ def digest(value: Any) -> str: return hashlib.sha256(_canonical_json(value).encode("utf-8")).hexdigest() -@PublicAPI(stability="beta") +@PublicAPI(stability="stable") def compute_ref_id( *, provider_id: str, @@ -314,13 +314,15 @@ def _uri_has_credentials(uri: str) -> bool: return False -@PublicAPI(stability="beta") +@PublicAPI(stability="stable") def schema_fingerprint(schema: "pa.Schema") -> str: """SHA-256 of the canonical Arrow schema (field structure + metadata). Used to detect schema drift between runs without embedding the full schema in a manifest. Field order matters (reordering columns changes - the fingerprint); metadata is normalized by key order. + the fingerprint); schema-level metadata is normalized by key order. + Field metadata is not included. Type identity uses Arrow's type strings; + changing Arrow's type representation can therefore change the fingerprint. """ import pyarrow as pa @@ -347,7 +349,7 @@ def _bytes_to_str(value: bytes | str) -> str: return value.decode("utf-8") if isinstance(value, bytes) else str(value) -@PublicAPI(stability="beta") +@PublicAPI(stability="stable") @dataclass(frozen=True) class DatasetRef: """Credential-free record of what data was used. diff --git a/src/tributo/data/source_config.py b/src/tributo/data/source_config.py index e206f97..9f79b83 100644 --- a/src/tributo/data/source_config.py +++ b/src/tributo/data/source_config.py @@ -2,6 +2,13 @@ Replaces the hardcoded ``if data_type == "s3": ... elif data_type == "csv": ...`` dispatch in ``training/data_loader.py`` and ``inference/pipeline.py``. + +Canonical models, projection helpers, and the ``BuiltinSourceConfig``, +``SourceConfig``, and ``CanonicalSourceInput`` aliases are stable contracts. +Legacy normalizers and ``RawSourceConfig`` remain beta. Configuration accepts +trusted runtime input; raw dumps and Pydantic validation details are not safe +public logging payloads. Provider execution and private options keep their own +stability and capability boundaries. """ from __future__ import annotations @@ -34,7 +41,7 @@ _SqlDialect = Literal["clickhouse", "doris", "postgresql", "mysql"] -@PublicAPI(stability="beta") +@PublicAPI(stability="stable") class RayReadTaskOptions(StrictConfigModel): """Validated Ray task options supported by the Doris read Binding. @@ -63,7 +70,7 @@ def _validate_finite_num_cpus( return value -@PublicAPI(stability="beta") +@PublicAPI(stability="stable") class ParquetSourceConfig(StrictConfigModel): """Parquet source (local filesystem or S3). @@ -80,7 +87,7 @@ class ParquetSourceConfig(StrictConfigModel): s3: S3Config | None = Field(default=None, repr=False) -@PublicAPI(stability="beta") +@PublicAPI(stability="stable") class CsvSourceConfig(StrictConfigModel): """CSV source (local filesystem or S3). @@ -97,7 +104,7 @@ class CsvSourceConfig(StrictConfigModel): columns: list[str] | None = None -@PublicAPI(stability="beta") +@PublicAPI(stability="stable") class SqlSourceConfig(StrictConfigModel): """Unified SQL data source for ClickHouse, Doris, PostgreSQL, and MySQL. @@ -191,7 +198,7 @@ def _validate_protocol(self) -> "SqlSourceConfig": # --------------------------------------------------------------------------- -@PublicAPI(stability="beta") +@PublicAPI(stability="stable") class SqlPartitioning(BaseModel): """Performance hint for SQL result partitioning. @@ -214,7 +221,7 @@ def _validate_mode(self) -> "SqlPartitioning": return self -@PublicAPI(stability="beta") +@PublicAPI(stability="stable") class IcebergSourceConfig(StrictConfigModel): """Iceberg table source. @@ -253,7 +260,7 @@ class IcebergSourceConfig(StrictConfigModel): SourceConfig = BuiltinSourceConfig -@PublicAPI(stability="beta") +@PublicAPI(stability="stable") class ProviderSourceConfig(StrictConfigModel): """Target ``provider/uri`` canonical source shape. @@ -264,9 +271,10 @@ class ProviderSourceConfig(StrictConfigModel): uri: Canonical URI of the bounded data source (``s3://``, local path, or a dialect-specific connection reference). options: Provider-validated options (format options, table - references, SQL query digest, etc.). May carry credentials — - redaction guarantees they never reach ``repr``, logs, errors, - ``DatasetRef`` or benchmark output. + references, SQL query digest, etc.). May carry runtime credentials + and are excluded from ``repr``. Persisted source identities are + credential-free; ordinary model dumps and validation details + must remain inside the trusted configuration boundary. """ provider: str = Field(min_length=1) @@ -289,7 +297,7 @@ def _provider_projection_option(source: ProviderSourceConfig) -> str | None: return resolve_provider(source).projection_option_name -@PublicAPI(stability="beta") +@PublicAPI(stability="stable") def source_projection(source: CanonicalSourceInput) -> list[str] | None: """Return the provider-native projection configured on ``source``. @@ -314,7 +322,7 @@ def source_projection(source: CanonicalSourceInput) -> list[str] | None: return list(value) if value else None -@PublicAPI(stability="beta") +@PublicAPI(stability="stable") def apply_source_projection( source: CanonicalSourceInput, columns: list[str] ) -> CanonicalSourceInput: diff --git a/tests/data/test_source_contract_compat.py b/tests/data/test_source_contract_compat.py new file mode 100644 index 0000000..3752238 --- /dev/null +++ b/tests/data/test_source_contract_compat.py @@ -0,0 +1,57 @@ +"""Persisted v1 source shapes and identities survive API stabilization.""" + +from __future__ import annotations + +import json +from dataclasses import asdict +from pathlib import Path +from typing import Any + +import pyarrow as pa +import pytest +from pydantic import TypeAdapter + +from tributo.data.base import S3Config as ReexportedS3Config +from tributo.data.contracts.storage import S3Config +from tributo.data.refs import DatasetRef, compute_ref_id, schema_fingerprint +from tributo.data.source_config import CanonicalSourceInput + +_FIXTURE = json.loads( + ( + Path(__file__).resolve().parents[1] + / "fixtures" + / "data-source-contract-v1.json" + ).read_text(encoding="utf-8") +) + + +@pytest.mark.parametrize("record", _FIXTURE["sources"]) +def test_persisted_canonical_source_shape(record: dict[str, Any]) -> None: + adapter: TypeAdapter[CanonicalSourceInput] = TypeAdapter(CanonicalSourceInput) + model = adapter.validate_json(json.dumps(record["input"])) + assert model.model_dump(mode="json") == record["expected"] + reloaded = adapter.validate_json(model.model_dump_json()) + assert reloaded.model_dump(mode="json") == record["expected"] + + +def test_persisted_reference_identity_v1() -> None: + identity = _FIXTURE["identity"] + schema = pa.schema( + [pa.field("x", pa.float64()), pa.field("y", pa.int64(), nullable=False)], + metadata={"version": "1", "owner": "tributo"}, + ) + assert compute_ref_id(**identity) == _FIXTURE["ref_id"] + assert schema_fingerprint(schema) == _FIXTURE["schema_fingerprint"] + serialized = { + "ref_id": _FIXTURE["ref_id"], + "provider_id": identity["provider_id"], + "uri": identity["canonical_uri"], + "schema_fingerprint": _FIXTURE["schema_fingerprint"], + "row_count": 3, + "provenance": "snapshot-7", + } + assert asdict(DatasetRef(**json.loads(json.dumps(serialized)))) == serialized + + +def test_storage_config_compatibility_reexport() -> None: + assert ReexportedS3Config is S3Config diff --git a/tests/fixtures/data-source-contract-v1.json b/tests/fixtures/data-source-contract-v1.json new file mode 100644 index 0000000..71c4fd8 --- /dev/null +++ b/tests/fixtures/data-source-contract-v1.json @@ -0,0 +1,152 @@ +{ + "contract_version": 1, + "sources": [ + { + "input": { + "type": "parquet", + "path": "s3://warehouse/train.parquet", + "columns": [ + "x", + "y" + ], + "s3": { + "region": "us-east-1" + } + }, + "expected": { + "type": "parquet", + "path": "s3://warehouse/train.parquet", + "columns": [ + "x", + "y" + ], + "s3": { + "access_key_id": null, + "secret_access_key": null, + "endpoint": null, + "region": "us-east-1" + } + } + }, + { + "input": { + "type": "csv", + "path": "/data/train.csv" + }, + "expected": { + "type": "csv", + "path": "/data/train.csv", + "s3": null, + "columns": null + } + }, + { + "input": { + "type": "sql", + "dialect": "doris", + "database": "analytics", + "table": "training", + "partitioning": { + "mode": "parallel", + "column": "id", + "num_partitions": 4 + }, + "ray_remote_args": { + "num_cpus": 1, + "scheduling_strategy": "SPREAD", + "max_retries": 2 + } + }, + "expected": { + "type": "sql", + "dialect": "doris", + "host": null, + "port": null, + "http_port": null, + "flight_port": null, + "database": "analytics", + "database_schema": null, + "user": null, + "password": null, + "sql": "", + "table": "training", + "protocol": "mysql", + "tablet_size": null, + "on_query_plan_error": null, + "ray_remote_args": { + "num_cpus": 1, + "scheduling_strategy": "SPREAD", + "max_retries": 2 + }, + "params": null, + "columns": null, + "partitioning": { + "mode": "parallel", + "column": "id", + "num_partitions": 4, + "bound_strategy": "min-max" + } + } + }, + { + "input": { + "type": "iceberg", + "catalog": "warehouse", + "table": "analytics.training", + "snapshot_id": 7, + "selected_fields": [ + "x", + "y" + ] + }, + "expected": { + "type": "iceberg", + "catalog": "warehouse", + "table": "analytics.training", + "catalog_properties": {}, + "s3": null, + "snapshot_id": 7, + "row_filter": null, + "selected_fields": [ + "x", + "y" + ] + } + }, + { + "input": { + "provider": "tributo.parquet", + "uri": "s3://warehouse/train.parquet", + "options": { + "columns": [ + "x", + "y" + ] + } + }, + "expected": { + "provider": "tributo.parquet", + "uri": "s3://warehouse/train.parquet", + "options": { + "columns": [ + "x", + "y" + ] + } + } + } + ], + "identity": { + "provider_id": "tributo.parquet", + "canonical_uri": "s3://warehouse/train.parquet", + "result_affecting_options": { + "columns": [ + "x", + "y" + ] + }, + "version": 1 + }, + "ref_id": "6d1b479b80c79eb1027fb2da97a35d6e34d78de347810ac70e3a674e9586ca14", + "schema_fingerprint": "cf41c4b6f371f9f1783e2a3ad74a78bfa94f5aeb0e4274e215414ee2f2d14f75" +} diff --git a/tests/integration/test_dowhy_scipy_compat.py b/tests/integration/test_dowhy_scipy_compat.py new file mode 100644 index 0000000..1b0c3d8 --- /dev/null +++ b/tests/integration/test_dowhy_scipy_compat.py @@ -0,0 +1,88 @@ +"""Real, bounded DoWhy calls for the locked SciPy dependency closure. + +Optional DoWhy imports remain inside tests so dev-only collection does not +require the causal extra. The manifest owns execution with that extra. +""" + +from __future__ import annotations + +import numpy as np +import pandas as pd +import pytest + +# DoWhy 0.14 contains invalid backslash escapes in Python source docstrings. +# Scope this compilation warning filter to these upstream dependency checks. +pytestmark = pytest.mark.filterwarnings("ignore:invalid escape sequence:SyntaxWarning") + + +def test_linear_estimation_and_placebo_refutation() -> None: + from dowhy import CausalModel + + rng = np.random.default_rng(7) + confounder = rng.normal(size=400) + treatment = rng.binomial(1, 0.5, size=400) + frame = pd.DataFrame( + { + "confounder": confounder, + "treatment": treatment, + "outcome": 3.0 * treatment + 2.0 * confounder, + } + ) + model = CausalModel( + data=frame, + treatment="treatment", + outcome="outcome", + common_causes=["confounder"], + ) + estimand = model.identify_effect(proceed_when_unidentifiable=True) + estimate = model.estimate_effect(estimand, method_name="backdoor.linear_regression") + refutation = model.refute_estimate( + estimand, + estimate, + method_name="placebo_treatment_refuter", + random_seed=7, + num_simulations=10, + n_jobs=1, + ) + + assert float(estimate.value) == pytest.approx(3.0, abs=1e-8) + assert np.isfinite(float(refutation.new_effect)) + assert abs(float(refutation.new_effect)) < 0.5 + + +def test_gcm_fit_attribution_and_counterfactual( + monkeypatch: pytest.MonkeyPatch, +) -> None: + import networkx as nx + from dowhy import gcm + from dowhy.gcm.shapley import ShapleyConfig + + monkeypatch.setattr(gcm.config, "default_n_jobs", 1) + monkeypatch.setattr(gcm.config, "show_progress_bars", False) + rng = np.random.default_rng(11) + cause = rng.normal(size=120) + frame = pd.DataFrame({"cause": cause, "effect": 2.0 * cause}) + model = gcm.InvertibleStructuralCausalModel(nx.DiGraph([("cause", "effect")])) + model.set_causal_mechanism("cause", gcm.EmpiricalDistribution()) + model.set_causal_mechanism( + "effect", gcm.AdditiveNoiseModel(gcm.ml.create_linear_regressor()) + ) + gcm.fit(model, frame) + anomaly = pd.DataFrame({"cause": [6.0], "effect": [12.0]}) + attributions = gcm.attribute_anomalies( + model, + "effect", + anomaly, + num_distribution_samples=100, + shapley_config=ShapleyConfig(num_permutations=4, n_jobs=1), + ) + counterfactual = gcm.counterfactual_samples( + model, + {"cause": lambda values: np.zeros_like(values)}, + observed_data=anomaly, + ) + + assert set(attributions) == {"cause", "effect"} + assert all(np.asarray(values).shape == (1,) for values in attributions.values()) + assert all(np.isfinite(values).all() for values in attributions.values()) + np.testing.assert_allclose(counterfactual["effect"], [0.0], atol=1e-8) diff --git a/tests/test_stability_inventory.py b/tests/test_stability_inventory.py index 16850e3..eab9613 100644 --- a/tests/test_stability_inventory.py +++ b/tests/test_stability_inventory.py @@ -83,14 +83,14 @@ # Data — beta "tributo.data.source_config": "beta", "tributo.data.provider_registry": "beta", - "tributo.data.refs": "beta", + "tributo.data.refs": "stable", # Data — alpha candidate ingestion contract "tributo.data.scan_plan": "developer", "tributo.data.ingestion": "alpha", "tributo.data.handle_adapters": "alpha", "tributo.data.contracts.handles": "alpha", "tributo.data.contracts.modes": "beta", - "tributo.data.contracts.storage": "beta", + "tributo.data.contracts.storage": "stable", "tributo.data.transform_ir": "alpha", # Data — beta "tributo.data.provider": "beta", @@ -209,6 +209,15 @@ #: list the exceptions here. Keys are ``module.symbol``, values are the #: symbol-specific stability. _SYMBOL_OVERRIDES: dict[str, str] = { + "tributo.data.source_config.RayReadTaskOptions": "stable", + "tributo.data.source_config.ParquetSourceConfig": "stable", + "tributo.data.source_config.CsvSourceConfig": "stable", + "tributo.data.source_config.SqlSourceConfig": "stable", + "tributo.data.source_config.SqlPartitioning": "stable", + "tributo.data.source_config.IcebergSourceConfig": "stable", + "tributo.data.source_config.ProviderSourceConfig": "stable", + "tributo.data.source_config.source_projection": "stable", + "tributo.data.source_config.apply_source_projection": "stable", "tributo.plugin.validate_distributed_algorithm_descriptor": "alpha", "tributo.config.AlgorithmExecutionConfig": "alpha", "tributo.config.AlgorithmInputConfig": "alpha", diff --git a/uv.lock b/uv.lock index 745cfdd..fc7ed4d 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 5 requires-python = ">=3.12, <3.14" resolution-markers = [ "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform == 'win32'", @@ -317,7 +317,8 @@ dependencies = [ { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.13' and platform_machine != 'x86_64') or (python_full_version < '3.13' and sys_platform != 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin')" }, { name = "numpy", version = "2.5.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.13' and platform_machine != 'x86_64') or (python_full_version >= '3.13' and sys_platform != 'darwin')" }, { name = "scikit-learn" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.13' and platform_machine == 'x86_64') or (python_full_version < '3.13' and sys_platform != 'darwin')" }, + { name = "scipy", version = "1.15.3", source = { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl" }, marker = "python_full_version < '3.13' and platform_machine != 'x86_64' and sys_platform == 'darwin'" }, { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9a/37/e34b70e97b83393d98bb53cdfcf0213af29ac5932d6752bbf2abbda5b998/bayesian-optimization-1.4.3.tar.gz", hash = "sha256:f9a448e1b52d961301cbc953ce4199709f6c26b1c88994c9b4dadb7752a64550", size = 27803, upload-time = "2023-04-25T21:58:35.41Z" } @@ -382,7 +383,8 @@ dependencies = [ { name = "numpy", version = "2.5.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.13' and platform_machine != 'x86_64') or (python_full_version >= '3.13' and sys_platform != 'darwin')" }, { name = "pandas" }, { name = "plotly" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.13' and platform_machine == 'x86_64') or (python_full_version < '3.13' and sys_platform != 'darwin')" }, + { name = "scipy", version = "1.15.3", source = { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl" }, marker = "python_full_version < '3.13' and platform_machine != 'x86_64' and sys_platform == 'darwin'" }, { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, { name = "six" }, ] @@ -413,7 +415,8 @@ dependencies = [ { name = "pandas" }, { name = "pydot" }, { name = "scikit-learn" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.13' and platform_machine == 'x86_64') or (python_full_version < '3.13' and sys_platform != 'darwin')" }, + { name = "scipy", version = "1.15.3", source = { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl" }, marker = "python_full_version < '3.13' and platform_machine != 'x86_64' and sys_platform == 'darwin'" }, { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, { name = "statsmodels" }, { name = "tqdm" }, @@ -539,7 +542,8 @@ dependencies = [ { name = "cffi" }, { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.13' and platform_machine != 'x86_64') or (python_full_version < '3.13' and sys_platform != 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin')" }, { name = "numpy", version = "2.5.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.13' and platform_machine != 'x86_64') or (python_full_version >= '3.13' and sys_platform != 'darwin')" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.13' and platform_machine == 'x86_64') or (python_full_version < '3.13' and sys_platform != 'darwin')" }, + { name = "scipy", version = "1.15.3", source = { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl" }, marker = "python_full_version < '3.13' and platform_machine != 'x86_64' and sys_platform == 'darwin'" }, { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/81/e2/47f692161779dbd98876015de934943effb667a014e6f79a6d746b3e4c2a/clarabel-0.11.1.tar.gz", hash = "sha256:e7c41c47f0e59aeab99aefff9e58af4a8753ee5269bbeecbd5526fc6f41b9598", size = 253949, upload-time = "2025-06-11T16:49:05.864Z" } @@ -796,7 +800,8 @@ dependencies = [ { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.13' and platform_machine != 'x86_64') or (python_full_version < '3.13' and sys_platform != 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin')" }, { name = "numpy", version = "2.5.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.13' and platform_machine != 'x86_64') or (python_full_version >= '3.13' and sys_platform != 'darwin')" }, { name = "osqp" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.13' and platform_machine == 'x86_64') or (python_full_version < '3.13' and sys_platform != 'darwin')" }, + { name = "scipy", version = "1.15.3", source = { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl" }, marker = "python_full_version < '3.13' and platform_machine != 'x86_64' and sys_platform == 'darwin'" }, { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, { name = "scs" }, ] @@ -984,7 +989,8 @@ dependencies = [ { name = "numpy", version = "2.5.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.13' and platform_machine != 'x86_64') or (python_full_version >= '3.13' and sys_platform != 'darwin')" }, { name = "pandas" }, { name = "scikit-learn" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.13' and platform_machine == 'x86_64') or (python_full_version < '3.13' and sys_platform != 'darwin')" }, + { name = "scipy", version = "1.15.3", source = { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl" }, marker = "python_full_version < '3.13' and platform_machine != 'x86_64' and sys_platform == 'darwin'" }, { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, { name = "statsmodels" }, { name = "sympy" }, @@ -1018,7 +1024,8 @@ dependencies = [ { name = "packaging" }, { name = "pandas" }, { name = "scikit-learn" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.13' and platform_machine == 'x86_64') or (python_full_version < '3.13' and sys_platform != 'darwin')" }, + { name = "scipy", version = "1.15.3", source = { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl" }, marker = "python_full_version < '3.13' and platform_machine != 'x86_64' and sys_platform == 'darwin'" }, { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, { name = "shap" }, { name = "sparse" }, @@ -1852,7 +1859,8 @@ dependencies = [ { name = "narwhals" }, { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.13' and platform_machine != 'x86_64') or (python_full_version < '3.13' and sys_platform != 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin')" }, { name = "numpy", version = "2.5.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.13' and platform_machine != 'x86_64') or (python_full_version >= '3.13' and sys_platform != 'darwin')" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.13' and platform_machine == 'x86_64') or (python_full_version < '3.13' and sys_platform != 'darwin')" }, + { name = "scipy", version = "1.15.3", source = { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl" }, marker = "python_full_version < '3.13' and platform_machine != 'x86_64' and sys_platform == 'darwin'" }, { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/63/8e/4db5e29290d7e619c307fdb8dab0a0514090af2ce3ec483050e024ec6126/lightgbm-4.7.0.tar.gz", hash = "sha256:f8e20f682c9aabd000bcf4a7ed8aa6f473c1adfecccae34ec24e823d156f4af0", size = 1792896, upload-time = "2026-07-18T21:00:56.139Z" } @@ -2107,7 +2115,8 @@ dependencies = [ { name = "pandas" }, { name = "pyarrow" }, { name = "scikit-learn" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.13' and platform_machine == 'x86_64') or (python_full_version < '3.13' and sys_platform != 'darwin')" }, + { name = "scipy", version = "1.15.3", source = { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl" }, marker = "python_full_version < '3.13' and platform_machine != 'x86_64' and sys_platform == 'darwin'" }, { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, { name = "sqlalchemy" }, { name = "waitress", marker = "sys_platform == 'win32'" }, @@ -2197,7 +2206,8 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.13' and platform_machine != 'x86_64') or (python_full_version < '3.13' and sys_platform != 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin')" }, { name = "numpy", version = "2.5.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.13' and platform_machine != 'x86_64') or (python_full_version >= '3.13' and sys_platform != 'darwin')" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.13' and platform_machine == 'x86_64') or (python_full_version < '3.13' and sys_platform != 'darwin')" }, + { name = "scipy", version = "1.15.3", source = { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl" }, marker = "python_full_version < '3.13' and platform_machine != 'x86_64' and sys_platform == 'darwin'" }, { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/92/e7/def9d879d424bc934e9e43b3455e28cd2ac76ace81cdc94be803ea78d29f/momentchi2-0.1.8.tar.gz", hash = "sha256:95d4becfd181d447c0281292c5827d74a09b04c38bd312da48a8a08932e8de1f", size = 11748, upload-time = "2021-08-27T18:56:23.571Z" } @@ -3053,7 +3063,8 @@ dependencies = [ { name = "joblib" }, { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.13' and platform_machine != 'x86_64') or (python_full_version < '3.13' and sys_platform != 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin')" }, { name = "numpy", version = "2.5.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.13' and platform_machine != 'x86_64') or (python_full_version >= '3.13' and sys_platform != 'darwin')" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.13' and platform_machine == 'x86_64') or (python_full_version < '3.13' and sys_platform != 'darwin')" }, + { name = "scipy", version = "1.15.3", source = { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl" }, marker = "python_full_version < '3.13' and platform_machine != 'x86_64' and sys_platform == 'darwin'" }, { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, { name = "setuptools" }, ] @@ -4212,7 +4223,8 @@ dependencies = [ { name = "joblib" }, { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.13' and platform_machine != 'x86_64') or (python_full_version < '3.13' and sys_platform != 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin')" }, { name = "numpy", version = "2.5.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.13' and platform_machine != 'x86_64') or (python_full_version >= '3.13' and sys_platform != 'darwin')" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.13' and platform_machine == 'x86_64') or (python_full_version < '3.13' and sys_platform != 'darwin')" }, + { name = "scipy", version = "1.15.3", source = { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl" }, marker = "python_full_version < '3.13' and platform_machine != 'x86_64' and sys_platform == 'darwin'" }, { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, { name = "threadpoolctl" }, ] @@ -4242,7 +4254,6 @@ resolution-markers = [ "python_full_version < '3.13' and sys_platform == 'win32'", "python_full_version < '3.13' and sys_platform == 'emscripten'", "python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin'", - "python_full_version < '3.13' and platform_machine != 'x86_64' and sys_platform == 'darwin'", "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ @@ -4251,8 +4262,6 @@ dependencies = [ sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/37/4b/683aa044c4162e10ed7a7ea30527f2cbd92e6999c10a8ed8edb253836e9c/scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019", size = 38766735, upload-time = "2025-05-08T16:06:06.471Z" }, - { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6", size = 30173284, upload-time = "2025-05-08T16:06:11.686Z" }, - { url = "https://files.pythonhosted.org/packages/07/9c/0ddb0d0abdabe0d181c1793db51f02cd59e4901da6f9f7848e1f96759f0d/scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477", size = 22446958, upload-time = "2025-05-08T16:06:15.97Z" }, { url = "https://files.pythonhosted.org/packages/af/43/0bce905a965f36c58ff80d8bea33f1f9351b05fad4beaad4eae34699b7a1/scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c", size = 25242454, upload-time = "2025-05-08T16:06:20.394Z" }, { url = "https://files.pythonhosted.org/packages/56/30/a6f08f84ee5b7b28b4c597aca4cbe545535c39fe911845a96414700b64ba/scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45", size = 35210199, upload-time = "2025-05-08T16:06:26.159Z" }, { url = "https://files.pythonhosted.org/packages/0b/1f/03f52c282437a168ee2c7c14a1a0d0781a9a4a8962d84ac05c06b4c5b555/scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49", size = 37309455, upload-time = "2025-05-08T16:06:32.778Z" }, @@ -4260,8 +4269,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2e/2e/025e39e339f5090df1ff266d021892694dbb7e63568edcfe43f892fa381d/scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539", size = 39710549, upload-time = "2025-05-08T16:06:45.729Z" }, { url = "https://files.pythonhosted.org/packages/e6/eb/3bf6ea8ab7f1503dca3a10df2e4b9c3f6b3316df07f6c0ded94b281c7101/scipy-1.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed", size = 40966184, upload-time = "2025-05-08T16:06:52.623Z" }, { url = "https://files.pythonhosted.org/packages/73/18/ec27848c9baae6e0d6573eda6e01a602e5649ee72c27c3a8aad673ebecfd/scipy-1.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759", size = 38728256, upload-time = "2025-05-08T16:06:58.696Z" }, - { url = "https://files.pythonhosted.org/packages/74/cd/1aef2184948728b4b6e21267d53b3339762c285a46a274ebb7863c9e4742/scipy-1.15.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62", size = 30109540, upload-time = "2025-05-08T16:07:04.209Z" }, - { url = "https://files.pythonhosted.org/packages/5b/d8/59e452c0a255ec352bd0a833537a3bc1bfb679944c4938ab375b0a6b3a3e/scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb", size = 22383115, upload-time = "2025-05-08T16:07:08.998Z" }, { url = "https://files.pythonhosted.org/packages/08/f5/456f56bbbfccf696263b47095291040655e3cbaf05d063bdc7c7517f32ac/scipy-1.15.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730", size = 25163884, upload-time = "2025-05-08T16:07:14.091Z" }, { url = "https://files.pythonhosted.org/packages/a2/66/a9618b6a435a0f0c0b8a6d0a2efb32d4ec5a85f023c2b79d39512040355b/scipy-1.15.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825", size = 35174018, upload-time = "2025-05-08T16:07:19.427Z" }, { url = "https://files.pythonhosted.org/packages/b5/09/c5b6734a50ad4882432b6bb7c02baf757f5b2f256041da5df242e2d7e6b6/scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7", size = 37269716, upload-time = "2025-05-08T16:07:25.712Z" }, @@ -4269,8 +4276,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fe/54/4379be86dd74b6ad81551689107360d9a3e18f24d20767a2d5b9253a3f0a/scipy-1.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126", size = 39670869, upload-time = "2025-05-08T16:07:38.002Z" }, { url = "https://files.pythonhosted.org/packages/87/2e/892ad2862ba54f084ffe8cc4a22667eaf9c2bcec6d2bff1d15713c6c0703/scipy-1.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163", size = 40988851, upload-time = "2025-05-08T16:08:33.671Z" }, { url = "https://files.pythonhosted.org/packages/1b/e9/7a879c137f7e55b30d75d90ce3eb468197646bc7b443ac036ae3fe109055/scipy-1.15.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8", size = 38863011, upload-time = "2025-05-08T16:07:44.039Z" }, - { url = "https://files.pythonhosted.org/packages/51/d1/226a806bbd69f62ce5ef5f3ffadc35286e9fbc802f606a07eb83bf2359de/scipy-1.15.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5", size = 30266407, upload-time = "2025-05-08T16:07:49.891Z" }, - { url = "https://files.pythonhosted.org/packages/e5/9b/f32d1d6093ab9eeabbd839b0f7619c62e46cc4b7b6dbf05b6e615bbd4400/scipy-1.15.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e", size = 22540030, upload-time = "2025-05-08T16:07:54.121Z" }, { url = "https://files.pythonhosted.org/packages/e7/29/c278f699b095c1a884f29fda126340fcc201461ee8bfea5c8bdb1c7c958b/scipy-1.15.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb", size = 25218709, upload-time = "2025-05-08T16:07:58.506Z" }, { url = "https://files.pythonhosted.org/packages/24/18/9e5374b617aba742a990581373cd6b68a2945d65cc588482749ef2e64467/scipy-1.15.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723", size = 34809045, upload-time = "2025-05-08T16:08:03.929Z" }, { url = "https://files.pythonhosted.org/packages/e1/fe/9c4361e7ba2927074360856db6135ef4904d505e9b3afbbcb073c4008328/scipy-1.15.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb", size = 36703062, upload-time = "2025-05-08T16:08:09.558Z" }, @@ -4279,6 +4284,62 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/06/0a5e5349474e1cbc5757975b21bd4fad0e72ebf138c5592f191646154e06/scipy-1.15.3-cp313-cp313t-win_amd64.whl", hash = "sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca", size = 40308097, upload-time = "2025-05-08T16:08:27.627Z" }, ] +[[package]] +name = "scipy" +version = "1.15.3" +source = { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl" } +resolution-markers = [ + "python_full_version < '3.13' and platform_machine != 'x86_64' and sys_platform == 'darwin'", +] +dependencies = [ + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" } }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6" }, +] + +[package.metadata] +requires-dist = [ + { name = "array-api-strict", marker = "extra == 'test'", specifier = ">=2.0,<2.1.1" }, + { name = "asv", marker = "extra == 'test'" }, + { name = "cython", marker = "extra == 'test'" }, + { name = "cython-lint", marker = "extra == 'dev'", specifier = ">=0.12.2" }, + { name = "doit", marker = "extra == 'dev'", specifier = ">=0.36.0" }, + { name = "gmpy2", marker = "extra == 'test'" }, + { name = "hypothesis", marker = "extra == 'test'", specifier = ">=6.30" }, + { name = "intersphinx-registry", marker = "extra == 'doc'" }, + { name = "jupyterlite-pyodide-kernel", marker = "extra == 'doc'" }, + { name = "jupyterlite-sphinx", marker = "extra == 'doc'", specifier = ">=0.19.1" }, + { name = "jupytext", marker = "extra == 'doc'" }, + { name = "matplotlib", marker = "extra == 'doc'", specifier = ">=3.5" }, + { name = "meson", marker = "extra == 'test'" }, + { name = "mpmath", marker = "extra == 'test'" }, + { name = "mypy", marker = "extra == 'dev'", specifier = "==1.10.0" }, + { name = "myst-nb", marker = "extra == 'doc'" }, + { name = "ninja", marker = "sys_platform != 'emscripten' and extra == 'test'" }, + { name = "numpy", specifier = ">=1.23.5,<2.5" }, + { name = "numpydoc", marker = "extra == 'doc'" }, + { name = "pooch", marker = "extra == 'doc'" }, + { name = "pooch", marker = "extra == 'test'" }, + { name = "pycodestyle", marker = "extra == 'dev'" }, + { name = "pydata-sphinx-theme", marker = "extra == 'doc'", specifier = ">=0.15.2" }, + { name = "pydevtool", marker = "extra == 'dev'" }, + { name = "pytest", marker = "extra == 'test'" }, + { name = "pytest-cov", marker = "extra == 'test'" }, + { name = "pytest-timeout", marker = "extra == 'test'" }, + { name = "pytest-xdist", marker = "extra == 'test'" }, + { name = "rich-click", marker = "extra == 'dev'" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.0.292" }, + { name = "scikit-umfpack", marker = "extra == 'test'" }, + { name = "sphinx", marker = "extra == 'doc'", specifier = ">=5.0.0,<8.0.0" }, + { name = "sphinx-copybutton", marker = "extra == 'doc'" }, + { name = "sphinx-design", marker = "extra == 'doc'", specifier = ">=0.4.0" }, + { name = "threadpoolctl", marker = "extra == 'test'" }, + { name = "types-psutil", marker = "extra == 'dev'" }, + { name = "typing-extensions", marker = "extra == 'dev'" }, +] +provides-extras = ["test", "doc", "dev"] + [[package]] name = "scipy" version = "1.17.1" @@ -4336,7 +4397,8 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.13' and platform_machine != 'x86_64') or (python_full_version < '3.13' and sys_platform != 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin')" }, { name = "numpy", version = "2.5.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.13' and platform_machine != 'x86_64') or (python_full_version >= '3.13' and sys_platform != 'darwin')" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.13' and platform_machine == 'x86_64') or (python_full_version < '3.13' and sys_platform != 'darwin')" }, + { name = "scipy", version = "1.15.3", source = { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl" }, marker = "python_full_version < '3.13' and platform_machine != 'x86_64' and sys_platform == 'darwin'" }, { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9e/59/5cb7f9612a5a3ff6efd4ab2d899902a536cc5974a7edb589084c5577291c/scs-3.2.11.tar.gz", hash = "sha256:2a5455cf2093d07f84f2f848c199faed52e79cdb3a11fe250b5622b6bbac4913", size = 1691825, upload-time = "2026-01-09T17:53:54.074Z" } @@ -4377,7 +4439,8 @@ dependencies = [ { name = "packaging" }, { name = "pandas" }, { name = "scikit-learn" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.13' and platform_machine == 'x86_64') or (python_full_version < '3.13' and sys_platform != 'darwin')" }, + { name = "scipy", version = "1.15.3", source = { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl" }, marker = "python_full_version < '3.13' and platform_machine != 'x86_64' and sys_platform == 'darwin'" }, { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, { name = "slicer" }, { name = "tqdm" }, @@ -4529,7 +4592,8 @@ dependencies = [ { name = "packaging" }, { name = "pandas" }, { name = "patsy" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.13' and platform_machine == 'x86_64') or (python_full_version < '3.13' and sys_platform != 'darwin')" }, + { name = "scipy", version = "1.15.3", source = { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl" }, marker = "python_full_version < '3.13' and platform_machine != 'x86_64' and sys_platform == 'darwin'" }, { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0d/81/e8d74b34f85285f7335d30c5e3c2d7c0346997af9f3debf9a0a9a63de184/statsmodels-0.14.6.tar.gz", hash = "sha256:4d17873d3e607d398b85126cd4ed7aad89e4e9d89fc744cdab1af3189a996c2a", size = 20689085, upload-time = "2025-12-05T23:08:39.522Z" } @@ -4948,6 +5012,7 @@ dev = [ { name = "pytest-metadata" }, { name = "pytest-timeout" }, { name = "redis" }, + { name = "scipy", version = "1.15.3", source = { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl" }, marker = "python_full_version < '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin'" }, { name = "skl2onnx" }, ] @@ -5051,6 +5116,7 @@ dev = [ { name = "pytest-metadata", specifier = ">=3.1.1" }, { name = "pytest-timeout", specifier = ">=2.4.0" }, { name = "redis", specifier = ">=8.0.0" }, + { name = "scipy", marker = "python_full_version == '3.12.*' and platform_machine == 'arm64' and sys_platform == 'darwin'", url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl" }, { name = "skl2onnx", specifier = ">=1.17.0" }, ] @@ -5359,7 +5425,8 @@ dependencies = [ { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.13' and platform_machine != 'x86_64') or (python_full_version < '3.13' and sys_platform != 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin')" }, { name = "numpy", version = "2.5.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.13' and platform_machine != 'x86_64') or (python_full_version >= '3.13' and sys_platform != 'darwin')" }, { name = "nvidia-nccl-cu12", marker = "sys_platform == 'linux'" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.13' and platform_machine == 'x86_64') or (python_full_version < '3.13' and sys_platform != 'darwin')" }, + { name = "scipy", version = "1.15.3", source = { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl" }, marker = "python_full_version < '3.13' and platform_machine != 'x86_64' and sys_platform == 'darwin'" }, { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fd/41/846d4de2b8fc694073fd3ac5052caf68caa1ea11cb7fa32d7ad9c049b232/xgboost-3.3.0.tar.gz", hash = "sha256:58bcb8a4cace648cdab7b94fa4f16d2c9ff26d90dd4d26907168106fa06d8746", size = 1224702, upload-time = "2026-06-17T21:26:50.846Z" } From 3adc3f494401065fab074e1a55d87f5ed9be15cc Mon Sep 17 00:00:00 2001 From: jiangxt2 Date: Tue, 6 Oct 2026 14:12:09 +0800 Subject: [PATCH 2/4] fix(ci): select causal dependency regression suite Signed-off-by: jiangxt2 --- ci/test-suites.json | 18 ++++++++++++------ tests/test_ci_test_plan.py | 24 ++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/ci/test-suites.json b/ci/test-suites.json index 5741208..67293c8 100644 --- a/ci/test-suites.json +++ b/ci/test-suites.json @@ -6,6 +6,16 @@ "nightly_total_seconds": 1800 }, "path_rules": [ + { + "name": "dependency-closure", + "patterns": ["pyproject.toml", "uv.lock"], + "domains": ["code", "docs", "storage", "dependencies"] + }, + { + "name": "causal-dependency-tests", + "patterns": ["tests/integration/test_dowhy_scipy_compat.py"], + "domains": ["code", "storage", "dependencies"] + }, { "name": "ci-policy", "patterns": [ @@ -188,12 +198,8 @@ ], "extras": ["dev", "causal"], "test_paths": ["tests/integration/test_dowhy_scipy_compat.py"], - "trigger_domains": [], - "trigger_paths": [ - "pyproject.toml", - "uv.lock", - "tests/integration/test_dowhy_scipy_compat.py" - ], + "trigger_domains": ["dependencies"], + "trigger_paths": [], "requires": [], "budget_seconds": 180, "ci_allowed": true, diff --git a/tests/test_ci_test_plan.py b/tests/test_ci_test_plan.py index a121b0e..5567403 100644 --- a/tests/test_ci_test_plan.py +++ b/tests/test_ci_test_plan.py @@ -121,6 +121,30 @@ def test_public_api_generator_change_selects_documentation_gate( ) +@pytest.mark.parametrize( + "path", + ( + "pyproject.toml", + "uv.lock", + "tests/integration/test_dowhy_scipy_compat.py", + ), +) +def test_dependency_change_selects_real_causal_ci_suite( + manifest: ci_test_plan.Manifest, path: str +) -> None: + plan = ci_test_plan.build_plan( + manifest, event="pull_request", mode="pr", changed_paths=[path] + ) + + assert "causal-dependency-contract" in plan["selected"]["ci_fast"] + assert {"suite": "causal-dependency-contract"} in plan["matrix"]["include"] + suite = manifest.suite("causal-dependency-contract") + assert suite.extras == ("dev", "causal") + assert suite.forbid_skips + assert plan["run_unit"] is True + assert plan["run_docs"] is (path != "tests/integration/test_dowhy_scipy_compat.py") + + def test_storage_change_selects_bounded_ci_and_reports_external_validation( manifest: ci_test_plan.Manifest, ) -> None: From f247f924d87d76128c28d102f0491be724418066 Mon Sep 17 00:00:00 2001 From: jiangxt2 Date: Tue, 6 Oct 2026 13:59:15 +0800 Subject: [PATCH 3/4] feat(exporting): stabilize bundle storage contracts Signed-off-by: jiangxt2 --- docs/STABILITY.md | 37 ++++++-- docs/reference/api/model-lifecycle.md | 20 +++++ docs/reference/support-matrix.md | 5 +- src/tributo/_common/storage_profiles.py | 15 ++-- src/tributo/exceptions.py | 4 +- src/tributo/exporting/bundle_reader.py | 11 ++- src/tributo/exporting/manifest.py | 14 +-- src/tributo/exporting/models.py | 40 +++++---- src/tributo/exporting/publisher.py | 14 ++- src/tributo/exporting/repository.py | 12 +-- tests/test_stability_inventory.py | 32 +++++-- .../exporters/test_storage_contract_scope.py | 88 +++++++++++++++++++ 12 files changed, 236 insertions(+), 56 deletions(-) create mode 100644 tests/training/exporters/test_storage_contract_scope.py diff --git a/docs/STABILITY.md b/docs/STABILITY.md index 3a121d4..00b19ff 100644 --- a/docs/STABILITY.md +++ b/docs/STABILITY.md @@ -157,13 +157,15 @@ source identity reject credential material rather than persist it. | Module | Level | Notes | |--------|-------|-------| | `tributo.exporting.service` — `BundleExportService` | `beta` | Primary export orchestration | -| `tributo.exporting.models` — all public models | `beta` | Configuration, artifact, canonical Bundle reference, and exact publication result models | +| `tributo.exporting.models` — core storage values | `stable` | AliasConfig, artifact/producer/reference/validation values, execution/failure results, BundleResult, BundleRef, ResolvedArtifact, and PublishedBundle | +| `tributo.exporting.models` — export configuration, Hook and plugin values | `beta` | Exporter orchestration and Hook payloads retain independent contracts | | `tributo.exporting.protocols` — all protocols | `beta` | Exporter/Validator/SourceProvider contracts | -| `tributo.exporting.manifest` — `ExportManifest` | `beta` | Bundle manifest (schema v1) | -| `tributo.exporting.bundle_reader` — `BundleReader` | `beta` | Repository-routed Bundle consumption with exact manifest-byte and artifact verification | +| `tributo.exporting.manifest` — schema-v1 models and registry | `stable` | Source info, typed signatures, execution records, ExportManifest, and schema reader registration | +| `tributo.exporting.manifest` — `ExportManifestV2` and `compute_bundle_digest` | `beta` | Explainability extension and compatibility digest helper | +| `tributo.exporting.bundle_reader` — `BundleReader` | `stable` | Repository-routed Bundle consumption with exact manifest-byte and artifact verification | | `tributo.exporting.planner` | `beta` | Export plan builder | | `tributo.exporting.executor` | `beta` | Export executor | -| `tributo.exporting.publisher` | `beta` | Bundle publisher | +| `tributo.exporting.publisher` | `stable` | Local/file/S3 core publication; the optional Explainability parameter remains Alpha | | `tributo.exporting.validators` | `beta` | Artifact validator runner | | `tributo.exporting.registries` | `beta` | Exporter/validator registries | | `tributo.exporting.options` | `beta` | Compatibility re-exports; schemas are owned by integration exporters | @@ -173,10 +175,33 @@ source identity reject credential material rather than persist it. | `tributo.exporting.hooks` | `beta` | Adapter and committed-artifact access contracts | | `tributo.exporting.dispatch` | `beta` | Inline Hook dispatch policy | | `tributo.exporting.capabilities` | `beta` | Exporter/Flavor-derived capability declarations | -| `tributo.exporting.repository` | `beta` | Bundle repository and alias store ports | +| `tributo.exporting.repository` | `stable` | Bundle repository/alias ports and their value objects; internal routing remains DeveloperAPI | | `tributo.exporting.runtime` | `beta` | Bundle model runtime and Flavor protocol | | `tributo.exporting.conftest` | `beta` | Public plugin conformance test kit | +The Stable storage model allowlist is `AliasConfig`, `ArtifactFile`, +`ProducerInfo`, `ArtifactRef`, `LogicalArtifact`, `ResolvedArtifact`, +`FailureInfo`, `NodeResult`, `ExportExecutionResult`, `BundleResult`, `BundleRef`, +`PublishedBundle`, and `ValidationResult`. The Stable manifest allowlist is +`ManifestSourceInfo`, `SignatureField`, `ManifestSignature`, +`ManifestExecutionNode`, `ManifestExecution`, `ExportManifest`, and +`ManifestSchemaRegistry`. `BundleCommitBusyError` and `AliasConflict` are Stable +storage errors. The high-level `export`, `ExportSpec`, and `load_bundle` +compatibility facade remain Beta. + +The Stable promise covers the core storage operation and fields. Passing Alpha +`ExplainabilityConfig` opts into its independent contract, and +`ExportManifestV2` remains Beta. `BundleResult.hook_receipts` keeps the Beta +`HookReceipt` payload; Hook delivery and orchestration do not become Stable. +Developer router/assembler injection remains an advanced extension with its +existing constructor parameters. No signature or extension payload is removed. + +`ResolvedArtifact` paths are valid inside their materialization context. +`PublishedBundle.local_dir_ephemeral` distinguishes transient S3 staging from +persistent local publication. Ephemeral paths belong to the staging owner; +BundleExportService guarantees their callback window. Consumers persist +`BundleRef` or `BundleResult.canonical_uri` instead of transient local paths. + ### Explainability (tributo.explainability.*) | Module | Level | Notes | @@ -288,7 +313,7 @@ source identity reject credential material rather than persist it. | Module | Level | Notes | |--------|-------|-------| -| `tributo._common.storage_profiles` | `beta` | Storage profile resolution | +| `tributo._common.storage_profiles` | `stable` | S3 profile values, named/default resolution, and credential-safe diagnostics | | `tributo._common.dependencies` | `beta` | Unified dependency probing layer | | `tributo._common` | `developer` | Internal shared package | | All other `tributo._common.*` | `developer` | Internal shared utilities | diff --git a/docs/reference/api/model-lifecycle.md b/docs/reference/api/model-lifecycle.md index 8cade0d..748a523 100644 --- a/docs/reference/api/model-lifecycle.md +++ b/docs/reference/api/model-lifecycle.md @@ -233,6 +233,14 @@ documentation for every public stability tier. :no-members: ``` +```{autoclass} tributo.exporting.models.PublishedBundle +:no-members: +``` + +```{autoclass} tributo.exporting.models.ResolvedArtifact +:no-members: +``` + ```{autoclass} tributo.exporting.models.SupportRequest :no-members: ``` @@ -331,6 +339,10 @@ documentation for every public stability tier. ## `tributo.exporting.repository` +```{autoclass} tributo.exporting.repository.AliasUpdate +:no-members: +``` + ```{autoclass} tributo.exporting.repository.BundleAliasStore :no-members: ``` @@ -343,6 +355,14 @@ documentation for every public stability tier. :no-members: ``` +```{autoclass} tributo.exporting.repository.RepositoryCommit +:no-members: +``` + +```{autoclass} tributo.exporting.repository.StagedBundle +:no-members: +``` + ## `tributo.exporting.runtime` diff --git a/docs/reference/support-matrix.md b/docs/reference/support-matrix.md index c27aeba..ffc671c 100644 --- a/docs/reference/support-matrix.md +++ b/docs/reference/support-matrix.md @@ -103,8 +103,9 @@ matrix are generated from the same Registry projection. | Capability | Status | Boundary | | --- | --- | --- | -| Local and `file://` bundle publication | Beta | Manifest and digest validation | -| S3 bundle publication | Beta | Manifest-last and alias compare-and-set | +| Local and `file://` bundle publication | Stable | Core Publisher/repository contracts, immutable identity, manifest and digest validation; Explainability and Hook extensions retain their own levels | +| S3 bundle publication | Stable | Core Publisher/repository contracts, manifest-last, leases, idempotency, and alias compare-and-set | +| Bundle manifest/artifact reading and integrity | Stable | BundleReader, schema-v1 fields, exact committed bytes, digest/identity checks, bounded materialization, and context lifetime; extension payload semantics are independent | | HDFS bundle publication | Not implemented | Storage backend extension | | Ray Data batch inference | Beta | Actor-based model reuse | | Batch output to local/S3 Parquet | Implemented | Database sinks are separate | diff --git a/src/tributo/_common/storage_profiles.py b/src/tributo/_common/storage_profiles.py index da109da..b5062e0 100644 --- a/src/tributo/_common/storage_profiles.py +++ b/src/tributo/_common/storage_profiles.py @@ -15,10 +15,15 @@ from tributo.util.annotations import PublicAPI -@PublicAPI(stability="beta") +@PublicAPI(stability="stable") @dataclass(frozen=True) class StorageProfile: - """Resolved S3-compatible storage parameters.""" + """Resolved S3-compatible storage parameters. + + Credential fields are trusted runtime data and excluded from repr. + Profile resolution and the existing boto3 keyword projection are stable; + no credentials are persisted into bundle manifests by this value object. + """ endpoint: str | None = field(default=None, repr=False) region: str | None = None @@ -54,7 +59,7 @@ def to_boto3_kwargs(self) -> dict[str, Any]: return kwargs -@PublicAPI(stability="beta") +@PublicAPI(stability="stable") class StorageProfileResolver: """Resolves a profile *name* to connection parameters. @@ -64,8 +69,8 @@ class StorageProfileResolver: 2. ``boto3`` standard credential chain (env vars, ``~/.aws/credentials``, instance metadata). - Plugins may register additional resolvers via entry-point - ``tributo.storage_resolvers``. + Consumer constructors may receive a custom resolver. The built-in resolver + does not discover an additional resolver entry-point group. """ def __init__(self) -> None: diff --git a/src/tributo/exceptions.py b/src/tributo/exceptions.py index 69ceeed..738dd22 100644 --- a/src/tributo/exceptions.py +++ b/src/tributo/exceptions.py @@ -151,7 +151,7 @@ def __init__(self, message: str, execution_result: Any = None) -> None: self.execution_result = execution_result -@PublicAPI(stability="beta") +@PublicAPI(stability="stable") class BundleCommitBusyError(TributoError): """A bundle commit is temporarily blocked by another active writer. @@ -161,7 +161,7 @@ class BundleCommitBusyError(TributoError): retryable = True -@PublicAPI(stability="beta") +@PublicAPI(stability="stable") class AliasConflict(TributoError): """Alias CAS update failed — concurrent modification detected.""" diff --git a/src/tributo/exporting/bundle_reader.py b/src/tributo/exporting/bundle_reader.py index b94265b..6240d37 100644 --- a/src/tributo/exporting/bundle_reader.py +++ b/src/tributo/exporting/bundle_reader.py @@ -21,9 +21,16 @@ from tributo.util.annotations import PublicAPI -@PublicAPI(stability="beta") +@PublicAPI(stability="stable") class BundleReader: - """Read and verify bundles through storage-independent repository ports.""" + """Read and verify bundles through storage-independent repository ports. + + Stable consumption covers local/file/S3 locations, exact manifest bytes, + immutable references, resource limits, and verified artifact contexts. + Manifest v2 remains readable, but its explainability payload keeps its + existing experimental contract. Custom schema readers and developer router + injection do not turn their extensions into stable first-party capabilities. + """ def __init__( self, diff --git a/src/tributo/exporting/manifest.py b/src/tributo/exporting/manifest.py index b829d55..21213c4 100644 --- a/src/tributo/exporting/manifest.py +++ b/src/tributo/exporting/manifest.py @@ -38,7 +38,7 @@ # ── Manifest models ──────────────────────────────────────────────────────────── -@PublicAPI(stability="beta") +@PublicAPI(stability="stable") class ManifestSourceInfo(BaseModel): """Stable source identification — no temp paths, credentials, or samples.""" @@ -52,7 +52,7 @@ class ManifestSourceInfo(BaseModel): task_type: str | None = None -@PublicAPI(stability="beta") +@PublicAPI(stability="stable") class SignatureField(BaseModel): """A single input/output field with its data type and shape. @@ -87,7 +87,7 @@ def _check_shape(cls, v: tuple[Any, ...]) -> tuple[Any, ...]: return v -@PublicAPI(stability="beta") +@PublicAPI(stability="stable") class ManifestSignature(BaseModel): """Input / output signature recorded in the manifest. @@ -176,7 +176,7 @@ def _check_dynamic_axes_consistency( ) -@PublicAPI(stability="beta") +@PublicAPI(stability="stable") class ManifestExecutionNode(BaseModel): """A single DAG node as recorded in the manifest. @@ -196,7 +196,7 @@ class ManifestExecutionNode(BaseModel): duration_ms: int = 0 -@PublicAPI(stability="beta") +@PublicAPI(stability="stable") class ManifestExecution(BaseModel): """Execution summary recorded in the manifest.""" @@ -206,7 +206,7 @@ class ManifestExecution(BaseModel): nodes: tuple[ManifestExecutionNode, ...] = () -@PublicAPI(stability="beta") +@PublicAPI(stability="stable") class ExportManifest(BaseModel): """Canonical manifest for a published model bundle (schema v1). @@ -291,7 +291,7 @@ def _rebuild_manifest_v2() -> None: # ── Schema registry ──────────────────────────────────────────────────────────── -@PublicAPI(stability="beta") +@PublicAPI(stability="stable") class ManifestSchemaRegistry: """Versioned manifest reader registry. diff --git a/src/tributo/exporting/models.py b/src/tributo/exporting/models.py index f6fe094..65b0f59 100644 --- a/src/tributo/exporting/models.py +++ b/src/tributo/exporting/models.py @@ -25,7 +25,7 @@ ) from tributo.exporting.formats import validate_format_id -from tributo.util.annotations import DeveloperAPI, PublicAPI +from tributo.util.annotations import PublicAPI if TYPE_CHECKING: from tributo.exporting.manifest import ManifestSignature @@ -110,7 +110,7 @@ class HookReceipt(BaseModel): external_references: dict[str, str] = Field(default_factory=dict) -@PublicAPI(stability="beta") +@PublicAPI(stability="stable") class AliasConfig(BaseModel): """Optional stable alias for a published bundle.""" @@ -475,7 +475,7 @@ def to_manifest_signatures( # ── Artifact models ────────────────────────────────────────────────────────── -@PublicAPI(stability="beta") +@PublicAPI(stability="stable") class ArtifactFile(BaseModel): """A single file within a logical artifact.""" @@ -511,7 +511,7 @@ def _check_path(cls, v: str) -> str: return _validate_posix_relative(v, "relative_path") -@PublicAPI(stability="beta") +@PublicAPI(stability="stable") class ProducerInfo(BaseModel): """Exporter identity and version information.""" @@ -525,7 +525,7 @@ class ProducerInfo(BaseModel): effective_options_digest: str = Field(default="", min_length=0) -@PublicAPI(stability="beta") +@PublicAPI(stability="stable") class ArtifactRef(BaseModel): """A reference to a published artifact by node_id and tree digest.""" @@ -581,7 +581,7 @@ def _check_entrypoint(self) -> ArtifactDraft: return self -@PublicAPI(stability="beta") +@PublicAPI(stability="stable") class LogicalArtifact(BaseModel): """A verified, hash-materialized artifact ready for publishing.""" @@ -638,7 +638,7 @@ def compute_tree_digest(cls, files: tuple[ArtifactFile, ...]) -> str: return hashlib.sha256(payload).hexdigest() -@DeveloperAPI +@PublicAPI(stability="stable") class ResolvedArtifact: """In-process view of an artifact with a local root directory. @@ -661,7 +661,7 @@ def path_for(self, relative_path: str) -> Path: # ── Execution results ──────────────────────────────────────────────────────── -@PublicAPI(stability="beta") +@PublicAPI(stability="stable") class FailureInfo(BaseModel): """Structured, sanitised failure detail — no tracebacks or credentials.""" @@ -675,7 +675,7 @@ class FailureInfo(BaseModel): retryable: bool = False -@PublicAPI(stability="beta") +@PublicAPI(stability="stable") class NodeResult(BaseModel): """Result of a single DAG node execution.""" @@ -694,7 +694,7 @@ class NodeResult(BaseModel): duration_ms: int = Field(default=0, ge=0) -@PublicAPI(stability="beta") +@PublicAPI(stability="stable") class ExportExecutionResult(BaseModel): """Complete result of an export DAG execution (before publishing).""" @@ -716,9 +716,14 @@ def succeeded_artifacts(self) -> dict[str, ArtifactRef]: } -@PublicAPI(stability="beta") +@PublicAPI(stability="stable") class BundleResult(BaseModel): - """Immutable result returned to the caller after a successful publish.""" + """Immutable result returned to the caller after a successful publish. + + Core storage identity, artifact, role, and alias fields are stable. + ``hook_receipts`` retains the beta HookReceipt payload contract; storing a + receipt does not promise a hook's external delivery behavior. + """ model_config = ConfigDict(frozen=True, extra="forbid") @@ -740,7 +745,7 @@ class BundleResult(BaseModel): hook_receipts: tuple[HookReceipt, ...] = () -@PublicAPI(stability="beta") +@PublicAPI(stability="stable") class BundleRef(BaseModel): """Immutable reference to a committed bundle. @@ -756,11 +761,14 @@ class BundleRef(BaseModel): manifest_sha256: str -@DeveloperAPI +@PublicAPI(stability="stable") class PublishedBundle: """Transient handle returned within BundleExportService context. - ``local_bundle_dir`` is only valid during the callback window. + ``local_dir_ephemeral`` distinguishes caller-owned staging (S3) from a + persistent local publication directory. Ephemeral files are valid only while + the staging owner keeps them alive; BundleExportService provides that window + to its callback. Persist BundleRef or the result's canonical URI instead. """ def __init__( @@ -779,7 +787,7 @@ def __init__( # ── Validation models ──────────────────────────────────────────────────────── -@PublicAPI(stability="beta") +@PublicAPI(stability="stable") class ValidationResult(BaseModel): """Result of a single validator run.""" diff --git a/src/tributo/exporting/publisher.py b/src/tributo/exporting/publisher.py index 81b741c..7979eef 100644 --- a/src/tributo/exporting/publisher.py +++ b/src/tributo/exporting/publisher.py @@ -30,13 +30,19 @@ logger = logging.getLogger(__name__) -@PublicAPI(stability="beta") +@PublicAPI(stability="stable") class Publisher: """Compatibility facade over assembly and repository ports. Storage-specific atomicity, idempotency, and alias compare-and-set behavior belongs to repository adapters. This facade keeps the existing public method stable while composing those domain contracts. + + Stable publication covers local/file/S3 immutable bundles and alias results. + The ``explainability`` argument retains its alpha contract and produces the + beta Manifest v2 extension. Hook delivery and exporter orchestration are + separate contracts. Advanced assembler/router injection remains a developer + extension; existing constructor arguments are retained. """ def __init__( @@ -47,9 +53,9 @@ def __init__( assembler: BundleAssembler | None = None, repository_router: BundleRepositoryRouter | None = None, ) -> None: - # Compatibility-window argument. Manifest parsing moved to - # BundleReader; keep accepting the old Publisher constructor surface - # until the documented minor-version window closes. + # Compatibility argument. Manifest parsing moved to BundleReader. + # Keep this constructor surface in the current major version; + # removal must follow the Stable API policy and migration guide. del manifest_registry self._assembler = assembler or BundleAssembler() self._repository_router = repository_router or build_default_repository_router( diff --git a/src/tributo/exporting/repository.py b/src/tributo/exporting/repository.py index efd4103..e4ed8f2 100644 --- a/src/tributo/exporting/repository.py +++ b/src/tributo/exporting/repository.py @@ -19,7 +19,7 @@ from tributo.util.annotations import DeveloperAPI, PublicAPI -@PublicAPI(stability="beta") +@PublicAPI(stability="stable") @dataclass(frozen=True) class ReaderResourceLimits: """Resource limits enforced before repository materialization.""" @@ -30,7 +30,7 @@ class ReaderResourceLimits: max_total_bytes: int = 50 * 1024 * 1024 * 1024 -@DeveloperAPI +@PublicAPI(stability="stable") @dataclass(frozen=True) class StagedBundle: """Storage-neutral immutable bundle assembled before commit.""" @@ -43,7 +43,7 @@ class StagedBundle: staging_root: Path -@DeveloperAPI +@PublicAPI(stability="stable") @dataclass(frozen=True) class RepositoryCommit: """Repository result after an immutable bundle commit.""" @@ -55,7 +55,7 @@ class RepositoryCommit: local_dir_ephemeral: bool -@DeveloperAPI +@PublicAPI(stability="stable") @dataclass(frozen=True) class AliasUpdate: """Storage-level alias update result.""" @@ -66,7 +66,7 @@ class AliasUpdate: @runtime_checkable -@PublicAPI(stability="beta") +@PublicAPI(stability="stable") class BundleRepository(Protocol): """Commit, read, and materialize immutable bundles for URI schemes. @@ -118,7 +118,7 @@ def materialize_artifact( @runtime_checkable -@PublicAPI(stability="beta") +@PublicAPI(stability="stable") class BundleAliasStore(Protocol): """Resolve and compare-and-set aliases in a storage namespace.""" diff --git a/tests/test_stability_inventory.py b/tests/test_stability_inventory.py index eab9613..8e96d8f 100644 --- a/tests/test_stability_inventory.py +++ b/tests/test_stability_inventory.py @@ -114,10 +114,10 @@ "tributo.exporting.models": "beta", "tributo.exporting.protocols": "beta", "tributo.exporting.manifest": "beta", - "tributo.exporting.bundle_reader": "beta", + "tributo.exporting.bundle_reader": "stable", "tributo.exporting.planner": "beta", "tributo.exporting.executor": "beta", - "tributo.exporting.publisher": "beta", + "tributo.exporting.publisher": "stable", "tributo.exporting.validators": "beta", "tributo.exporting.registries": "beta", "tributo.exporting.options": "beta", @@ -127,7 +127,7 @@ "tributo.exporting.capabilities": "beta", "tributo.exporting.events": "beta", "tributo.exporting.dispatch": "beta", - "tributo.exporting.repository": "beta", + "tributo.exporting.repository": "stable", "tributo.exporting.runtime": "beta", "tributo.exporting.conftest": "beta", # Explainability — alpha @@ -200,7 +200,7 @@ "tributo.plugin": "beta", # Common — developer "tributo._common": "developer", - "tributo._common.storage_profiles": "beta", + "tributo._common.storage_profiles": "stable", "tributo._common.dependencies": "beta", } @@ -218,6 +218,26 @@ "tributo.data.source_config.ProviderSourceConfig": "stable", "tributo.data.source_config.source_projection": "stable", "tributo.data.source_config.apply_source_projection": "stable", + "tributo.exporting.models.AliasConfig": "stable", + "tributo.exporting.models.ArtifactFile": "stable", + "tributo.exporting.models.ProducerInfo": "stable", + "tributo.exporting.models.ArtifactRef": "stable", + "tributo.exporting.models.LogicalArtifact": "stable", + "tributo.exporting.models.ResolvedArtifact": "stable", + "tributo.exporting.models.FailureInfo": "stable", + "tributo.exporting.models.NodeResult": "stable", + "tributo.exporting.models.ExportExecutionResult": "stable", + "tributo.exporting.models.BundleResult": "stable", + "tributo.exporting.models.BundleRef": "stable", + "tributo.exporting.models.PublishedBundle": "stable", + "tributo.exporting.models.ValidationResult": "stable", + "tributo.exporting.manifest.ManifestSourceInfo": "stable", + "tributo.exporting.manifest.SignatureField": "stable", + "tributo.exporting.manifest.ManifestSignature": "stable", + "tributo.exporting.manifest.ManifestExecutionNode": "stable", + "tributo.exporting.manifest.ManifestExecution": "stable", + "tributo.exporting.manifest.ExportManifest": "stable", + "tributo.exporting.manifest.ManifestSchemaRegistry": "stable", "tributo.plugin.validate_distributed_algorithm_descriptor": "alpha", "tributo.config.AlgorithmExecutionConfig": "alpha", "tributo.config.AlgorithmInputConfig": "alpha", @@ -225,8 +245,8 @@ "tributo.config.LocalRayRuntimeConfig": "alpha", "tributo.exceptions.ResultMaterializationError": "alpha", "tributo.exceptions.BundleExportError": "beta", - "tributo.exceptions.BundleCommitBusyError": "beta", - "tributo.exceptions.AliasConflict": "beta", + "tributo.exceptions.BundleCommitBusyError": "stable", + "tributo.exceptions.AliasConflict": "stable", "tributo.exceptions.UnsupportedArtifactFormat": "beta", "tributo.exceptions.PostPublishCallbackError": "beta", "tributo.exceptions.PluginLoadIssue": "beta", diff --git a/tests/training/exporters/test_storage_contract_scope.py b/tests/training/exporters/test_storage_contract_scope.py new file mode 100644 index 0000000..93a07bb --- /dev/null +++ b/tests/training/exporters/test_storage_contract_scope.py @@ -0,0 +1,88 @@ +"""Stable storage metadata and explicit experimental extension boundaries.""" + +from __future__ import annotations + +import json + +import pytest + +from tributo._common.storage_profiles import StorageProfileResolver +from tributo.exceptions import ( + AliasConflict, + BundleCommitBusyError, + JobConfigurationError, +) +from tributo.explainability.contracts import ( + ExplainabilityConfig, + ExplainabilityDescriptor, +) +from tributo.exporting import export, load_bundle +from tributo.exporting.bundle_reader import BundleReader +from tributo.exporting.manifest import ExportManifest, ExportManifestV2 +from tributo.exporting.models import ( + BundleRef, + BundleResult, + HookReceipt, + PublishedBundle, +) +from tributo.exporting.publisher import Publisher +from tributo.exporting.repository import BundleAliasStore, BundleRepository +from tributo.util.annotations import get_stability + + +def test_core_storage_metadata_does_not_promote_extensions() -> None: + for obj in ( + BundleReader, + Publisher, + BundleRepository, + BundleAliasStore, + BundleRef, + BundleResult, + PublishedBundle, + ExportManifest, + BundleCommitBusyError, + AliasConflict, + ): + assert get_stability(obj) == "stable" + for beta_obj in (ExportManifestV2, HookReceipt, export, load_bundle): + assert get_stability(beta_obj) == "beta" + for alpha_obj in (ExplainabilityConfig, ExplainabilityDescriptor): + assert get_stability(alpha_obj) == "alpha" + + +def test_named_profile_preserves_credential_safety( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setenv( + "TRIBUTO_STORAGE_PROFILE_CONTRACT", + json.dumps( + { + "endpoint": "http://minio.example:9000", + "region": "us-east-1", + "path_style": True, + "access_key_id": "fixture-key", + "secret_access_key": "fixture-secret", + } + ), + ) + profile = StorageProfileResolver().resolve("contract") + assert profile.endpoint == "http://minio.example:9000" + assert profile.path_style + assert profile.access_key_id == "fixture-key" + assert profile.secret_access_key == "fixture-secret" + assert "fixture-key" not in repr(profile) + assert "fixture-secret" not in repr(profile) + assert "minio.example" not in repr(profile) + + +def test_invalid_profile_diagnostic_does_not_echo_input( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setenv( + "TRIBUTO_STORAGE_PROFILE_CONTRACT", + json.dumps({"unknown": "fixture-secret"}), + ) + with pytest.raises(JobConfigurationError) as error: + StorageProfileResolver().resolve("contract") + assert "invalid storage profile fields" in str(error.value) + assert "fixture-secret" not in str(error.value) From 5cc8fe07566458747447df2884d957dd8c265e6e Mon Sep 17 00:00:00 2001 From: jiangxt2 Date: Tue, 6 Oct 2026 14:00:59 +0800 Subject: [PATCH 4/4] feat(exporting): stabilize ONNX bundle runtime Signed-off-by: jiangxt2 --- docs/STABILITY.md | 27 ++- docs/reference/support-matrix.md | 1 + src/tributo/exceptions.py | 2 +- src/tributo/exporting/capabilities.py | 6 +- src/tributo/exporting/models.py | 2 +- src/tributo/exporting/registries.py | 2 +- src/tributo/exporting/runtime.py | 28 ++- .../integrations/flavors/onnx_runtime.py | 2 +- tests/fixtures/bundle-runtime-classifier.json | 11 + tests/serving/test_bundle_runtime.py | 222 +++++++++++++----- tests/test_stability_inventory.py | 10 +- 11 files changed, 221 insertions(+), 92 deletions(-) create mode 100644 tests/fixtures/bundle-runtime-classifier.json diff --git a/docs/STABILITY.md b/docs/STABILITY.md index 00b19ff..44ad7a7 100644 --- a/docs/STABILITY.md +++ b/docs/STABILITY.md @@ -158,7 +158,8 @@ source identity reject credential material rather than persist it. |--------|-------|-------| | `tributo.exporting.service` — `BundleExportService` | `beta` | Primary export orchestration | | `tributo.exporting.models` — core storage values | `stable` | AliasConfig, artifact/producer/reference/validation values, execution/failure results, BundleResult, BundleRef, ResolvedArtifact, and PublishedBundle | -| `tributo.exporting.models` — export configuration, Hook and plugin values | `beta` | Exporter orchestration and Hook payloads retain independent contracts | +| `tributo.exporting.models` — `PluginLoadDiagnostic` | `stable` | Flavor/plugin discovery diagnostic value fields | +| `tributo.exporting.models` — export configuration and Hook values | `beta` | Exporter orchestration and Hook payloads retain independent contracts | | `tributo.exporting.protocols` — all protocols | `beta` | Exporter/Validator/SourceProvider contracts | | `tributo.exporting.manifest` — schema-v1 models and registry | `stable` | Source info, typed signatures, execution records, ExportManifest, and schema reader registration | | `tributo.exporting.manifest` — `ExportManifestV2` and `compute_bundle_digest` | `beta` | Explainability extension and compatibility digest helper | @@ -167,16 +168,17 @@ source identity reject credential material rather than persist it. | `tributo.exporting.executor` | `beta` | Export executor | | `tributo.exporting.publisher` | `stable` | Local/file/S3 core publication; the optional Explainability parameter remains Alpha | | `tributo.exporting.validators` | `beta` | Artifact validator runner | -| `tributo.exporting.registries` | `beta` | Exporter/validator registries | +| `tributo.exporting.registries` — `FlavorRegistry` | `stable` | Flavor registration, lookup, and diagnostics | +| `tributo.exporting.registries` — exporter/source/validator/factory registries | `beta` | Exporter and model-factory orchestration | | `tributo.exporting.options` | `beta` | Compatibility re-exports; schemas are owned by integration exporters | | `tributo.exporting.records` | `beta` | Export record types; `PublicationAttempt` is read-only legacy compatibility and receives no new writes | | `tributo.exporting.gc` | `beta` | Bundle GC | | `tributo.exporting.events` | `beta` | Immutable publication event contract | | `tributo.exporting.hooks` | `beta` | Adapter and committed-artifact access contracts | | `tributo.exporting.dispatch` | `beta` | Inline Hook dispatch policy | -| `tributo.exporting.capabilities` | `beta` | Exporter/Flavor-derived capability declarations | +| `tributo.exporting.capabilities` | `stable` | Capability value structure, discovery projection, and lookup; plugin declarations do not prove execution support | | `tributo.exporting.repository` | `stable` | Bundle repository/alias ports and their value objects; internal routing remains DeveloperAPI | -| `tributo.exporting.runtime` | `beta` | Bundle model runtime and Flavor protocol | +| `tributo.exporting.runtime` | `stable` | Bundle model protocols, loader/runtime, and support entries; first-party executable guarantee is scoped to onnx-runtime-v1 | | `tributo.exporting.conftest` | `beta` | Public plugin conformance test kit | The Stable storage model allowlist is `AliasConfig`, `ArtifactFile`, @@ -202,6 +204,21 @@ persistent local publication. Ephemeral paths belong to the staging owner; BundleExportService guarantees their callback window. Consumers persist `BundleRef` or `BundleResult.canonical_uri` instead of transient local paths. +The Stable Runtime scope includes `BundleReaderLike`, `BundleModel`, +`BundleModelFlavor`, `FlavorSupportEntry`, `BundleModelLoader`, +`BundleModelRuntime`, and `ONNXRuntimeFlavor`. `FlavorRegistry`, +`ArtifactCapability`, `CapabilityRegistry`, `get_default_capability_registry`, +`PluginLoadDiagnostic`, and `UnsupportedArtifactFormat` complete its public +routing and diagnostic contracts. Exporter/validator/factory registries, +optional dependency internals, other flavors, Ray Data batch orchestration, +and HTTP/gRPC/SSE transports keep their existing levels. + +Runtime `close()` releases reader resources exactly once; it does not unload +an in-memory ONNX session. ONNX prediction remains valid after close. Loading +and signature-validation failures close the artifact context before propagating. +The loaded Runtime keeps the validated manifest bytes and role-bound artifact; +custom Flavor execution follows its own declared contract. + ### Explainability (tributo.explainability.*) | Module | Level | Notes | @@ -231,7 +248,7 @@ BundleExportService guarantees their callback window. Consumers persist | `tributo.integrations.exporters.*` | `beta` | Built-in exporter implementations | | `tributo.integrations.exporters.x_learner` | `alpha` | Fixed X-Learner model and causal-report exporter adapters | | `tributo.integrations.flavors` | `beta` | Built-in runtime flavor package | -| `tributo.integrations.flavors.onnx_runtime` | `beta` | ONNX Runtime flavor implementation | +| `tributo.integrations.flavors.onnx_runtime` | `stable` | onnx-runtime-v1 named-tensor loading, prediction, and reader-resource lifetime | | `tributo.integrations.validators` | `beta` | Built-in validator package | | `tributo.integrations.validators.*` | `beta` | Built-in validator implementations | | `tributo.integrations.sources` | `beta` | Built-in source provider package | diff --git a/docs/reference/support-matrix.md b/docs/reference/support-matrix.md index ffc671c..47c2383 100644 --- a/docs/reference/support-matrix.md +++ b/docs/reference/support-matrix.md @@ -107,6 +107,7 @@ matrix are generated from the same Registry projection. | S3 bundle publication | Stable | Core Publisher/repository contracts, manifest-last, leases, idempotency, and alias compare-and-set | | Bundle manifest/artifact reading and integrity | Stable | BundleReader, schema-v1 fields, exact committed bytes, digest/identity checks, bounded materialization, and context lifetime; extension payload semantics are independent | | HDFS bundle publication | Not implemented | Storage backend extension | +| ONNX Bundle model loading and prediction | Stable | BundleModelLoader/Runtime and onnx-runtime-v1 only: named tensors, typed signatures, integrity/security gates, and reader-resource lifetime; transport and orchestration levels are separate | | Ray Data batch inference | Beta | Actor-based model reuse | | Batch output to local/S3 Parquet | Implemented | Database sinks are separate | | Batch explainability | Alpha | Optional SHAP adapter over a declared Bundle role; batch-only Ray ingestion and bounded Parquet results | diff --git a/src/tributo/exceptions.py b/src/tributo/exceptions.py index 738dd22..1295aca 100644 --- a/src/tributo/exceptions.py +++ b/src/tributo/exceptions.py @@ -166,7 +166,7 @@ class AliasConflict(TributoError): """Alias CAS update failed — concurrent modification detected.""" -@PublicAPI(stability="beta") +@PublicAPI(stability="stable") class UnsupportedArtifactFormat(TributoError): """Consumer does not support this artifact format or flavor. diff --git a/src/tributo/exporting/capabilities.py b/src/tributo/exporting/capabilities.py index 8fbc3c2..f6f203f 100644 --- a/src/tributo/exporting/capabilities.py +++ b/src/tributo/exporting/capabilities.py @@ -10,7 +10,7 @@ from tributo.util.annotations import PublicAPI -@PublicAPI(stability="beta") +@PublicAPI(stability="stable") @dataclass(frozen=True) class ArtifactCapability: """Capabilities for one immutable artifact flavor. @@ -36,7 +36,7 @@ class ArtifactCapability: conditional_operations: tuple[str, ...] = () -@PublicAPI(stability="beta") +@PublicAPI(stability="stable") class CapabilityRegistry: """Immutable registry derived from exporter and flavor descriptors.""" @@ -254,7 +254,7 @@ def _build_default_capability_registry() -> CapabilityRegistry: _DEFAULT_CAPABILITY_REGISTRY: CapabilityRegistry | None = None -@PublicAPI(stability="beta") +@PublicAPI(stability="stable") def get_default_capability_registry() -> CapabilityRegistry: """Return the lazily composed first-party capability registry. diff --git a/src/tributo/exporting/models.py b/src/tributo/exporting/models.py index 65b0f59..004e69f 100644 --- a/src/tributo/exporting/models.py +++ b/src/tributo/exporting/models.py @@ -919,7 +919,7 @@ class ExportSource(BaseModel): # ── Plugin diagnostics ─────────────────────────────────────────────────────── -@PublicAPI(stability="beta") +@PublicAPI(stability="stable") class PluginLoadDiagnostic(BaseModel): """Non-fatal plugin loading issue.""" diff --git a/src/tributo/exporting/registries.py b/src/tributo/exporting/registries.py index 5ea8163..e1d5355 100644 --- a/src/tributo/exporting/registries.py +++ b/src/tributo/exporting/registries.py @@ -323,7 +323,7 @@ def record_diagnostic(self, diagnostic: PluginLoadDiagnostic) -> None: # ═══════════════════════════════════════════════════════════════════════════════ -@PublicAPI(stability="beta") +@PublicAPI(stability="stable") class FlavorRegistry: """Registry of ``ModelFlavor`` classes keyed by ``flavor_id``. diff --git a/src/tributo/exporting/runtime.py b/src/tributo/exporting/runtime.py index 0667507..4572135 100644 --- a/src/tributo/exporting/runtime.py +++ b/src/tributo/exporting/runtime.py @@ -27,7 +27,7 @@ import hashlib import logging -from contextlib import ExitStack +from contextlib import AbstractContextManager, ExitStack from dataclasses import dataclass from typing import Any, ClassVar, Literal, Protocol, runtime_checkable @@ -75,7 +75,7 @@ @runtime_checkable -@PublicAPI(stability="beta") +@PublicAPI(stability="stable") class BundleReaderLike(Protocol): """Structural reader contract — satisfied by ``BundleReader``. @@ -100,17 +100,18 @@ def open_artifact( storage_profile: str | None = None, manifest: ExportManifest | None = None, manifest_bytes: bytes | None = None, - ) -> Any: ... + ) -> AbstractContextManager[ResolvedArtifact]: ... @runtime_checkable -@PublicAPI(stability="beta") +@PublicAPI(stability="stable") class BundleModel(Protocol): """A loaded, in-memory model ready for prediction. - After loading, the model must not depend on the bundle's temporary - files — the runtime closes the artifact context as soon as loading - completes, so ``predict`` must work purely in memory. + The runtime retains its artifact context until explicit ``close``; it + does not close it as soon as loading completes. In-memory models, including + the Stable ONNX flavor, must remain usable after those reader resources + close. Other flavors keep their declared loading and execution contracts. """ @property @@ -150,7 +151,7 @@ def predict(self, inputs: dict[str, np.ndarray]) -> dict[str, np.ndarray]: @runtime_checkable -@PublicAPI(stability="beta") +@PublicAPI(stability="stable") class BundleModelFlavor(Protocol): """Loads a ``BundleModel`` from a verified bundle artifact. @@ -200,7 +201,7 @@ def load( # ── Serveable flavor support matrix ─────────────────────────────────────────── -@PublicAPI(stability="beta") +@PublicAPI(stability="stable") @dataclass(frozen=True) class FlavorSupportEntry: """One row of the model-artifact capability support matrix. @@ -324,10 +325,15 @@ class FlavorSupportEntry: # ── Loader ──────────────────────────────────────────────────────────────────── -@PublicAPI(stability="beta") +@PublicAPI(stability="stable") class BundleModelLoader: """Opens a published bundle as a serveable model runtime. + The Stable first-party execution scope is ``onnx-runtime-v1`` with named + tensors, integrity/security gates, typed signatures, and reader lifetime. + Registry routing and rejection contracts are stable; registering a plugin + does not promote that plugin's execution or model semantics. + Args: bundle_reader: Reader for manifest + artifacts; defaults to a fresh ``BundleReader``. @@ -519,7 +525,7 @@ def open( # ── Runtime ─────────────────────────────────────────────────────────────────── -@PublicAPI(stability="beta") +@PublicAPI(stability="stable") class BundleModelRuntime: """A loaded model plus the reader resources it was loaded from. diff --git a/src/tributo/integrations/flavors/onnx_runtime.py b/src/tributo/integrations/flavors/onnx_runtime.py index b8bcc58..a13f388 100644 --- a/src/tributo/integrations/flavors/onnx_runtime.py +++ b/src/tributo/integrations/flavors/onnx_runtime.py @@ -27,7 +27,7 @@ __all__ = ["ONNXRuntimeFlavor"] -@PublicAPI(stability="beta") +@PublicAPI(stability="stable") class ONNXRuntimeFlavor: """Loads ``onnx-runtime-v1`` artifacts into an ONNX Runtime session.""" diff --git a/tests/fixtures/bundle-runtime-classifier.json b/tests/fixtures/bundle-runtime-classifier.json new file mode 100644 index 0000000..88c9c61 --- /dev/null +++ b/tests/fixtures/bundle-runtime-classifier.json @@ -0,0 +1,11 @@ +{ + "format": "onnx", + "ir_version": 10, + "opset": 18, + "operators": [ + "Softmax", + "ArgMax" + ], + "sha256": "f8d52e8e1589c80771fd4876e501dd7db8cb628fa9821c92fe3d2e02c76fa22e", + "model_base64": "CAoSDXRyaWJ1dG8tdGVzdHM68gEKMgoLZmxvYXRfaW5wdXQSDXByb2JhYmlsaXRpZXMiB1NvZnRtYXgqCwoEYXhpcxgBoAECCjwKDXByb2JhYmlsaXRpZXMSBWxhYmVsIgZBcmdNYXgqCwoEYXhpcxgBoAECKg8KCGtlZXBkaW1zGACgAQISGnRyaWJ1dG8tcnVudGltZS1jbGFzc2lmaWVyWiIKC2Zsb2F0X2lucHV0EhMKEQgBEg0KBxIFYmF0Y2gKAggCYhgKBWxhYmVsEg8KDQgHEgkKBxIFYmF0Y2hiJAoNcHJvYmFiaWxpdGllcxITChEIARINCgcSBWJhdGNoCgIIAkIECgAQEg==" +} diff --git a/tests/serving/test_bundle_runtime.py b/tests/serving/test_bundle_runtime.py index 121cee2..3f08a27 100644 --- a/tests/serving/test_bundle_runtime.py +++ b/tests/serving/test_bundle_runtime.py @@ -9,8 +9,11 @@ from __future__ import annotations +import base64 import hashlib import json +from collections.abc import Iterator +from contextlib import contextmanager from pathlib import Path from typing import Any from unittest.mock import patch @@ -31,7 +34,18 @@ UnsupportedArtifactFormat, ) from tributo.exporting.bundle_reader import BundleReader -from tributo.exporting.registries import FlavorRegistry +from tributo.exporting.capabilities import ( + ArtifactCapability, + CapabilityRegistry, + get_default_capability_registry, +) +from tributo.exporting.manifest import ExportManifest +from tributo.exporting.models import BundleRef, PluginLoadDiagnostic, ResolvedArtifact +from tributo.exporting.registries import ( + ExportRegistry, + FlavorRegistry, + ModelFactoryRegistry, +) from tributo.exporting.runtime import ( DEFAULT_ROLE, FLAVOR_SUPPORT_MATRIX, @@ -39,9 +53,29 @@ SECURITY_MODE_SAFE, SERVEABLE_FLAVOR_MATRIX, BundleModel, + BundleModelFlavor, BundleModelLoader, + BundleModelRuntime, + BundleReaderLike, + FlavorSupportEntry, +) +from tributo.integrations.flavors.onnx_runtime import ONNXRuntimeFlavor +from tributo.util.annotations import get_stability + +_ONNX_CLASSIFIER_FIXTURE = ( + Path(__file__).resolve().parents[1] / "fixtures" / "bundle-runtime-classifier.json" ) + +def _write_onnx_classifier_fixture(tmp_path: Path) -> str: + descriptor = json.loads(_ONNX_CLASSIFIER_FIXTURE.read_text(encoding="utf-8")) + payload = base64.b64decode(descriptor["model_base64"], validate=True) + assert hashlib.sha256(payload).hexdigest() == descriptor["sha256"] + path = tmp_path / "runtime-classifier.onnx" + path.write_bytes(payload) + return str(path) + + # ── Fakes ────────────────────────────────────────────────────────────────────── @@ -52,7 +86,7 @@ class _EchoModel: input_names = ("float_input",) output_names = ("label", "probabilities") - input_dtypes = ("float32",) + input_dtypes: tuple[str, ...] = ("float32",) output_dtypes = ("int64", "float32") input_shapes: tuple[tuple[int | None, ...], ...] = ((None, 2),) output_shapes: tuple[tuple[int | None, ...], ...] = ((None,), (None, 2)) @@ -138,7 +172,9 @@ def load( assert architecture_id == "test-dnn-v1" factory_cls = build_factory_registry().get(architecture_id) - return factory_cls().build({}) + model = factory_cls().build({}) + assert isinstance(model, BundleModel) + return model def _loader(flavor: type[Any] = _EchoFlavor) -> BundleModelLoader: @@ -175,6 +211,7 @@ def test_matrix_loader_paths_resolve(self) -> None: assert SERVEABLE_FLAVOR_MATRIX, "matrix must not be empty" for entry in SERVEABLE_FLAVOR_MATRIX: + assert entry.loader is not None module_name, _, attr = entry.loader.partition(":") module = importlib.import_module(module_name) cls = getattr(module, attr) @@ -390,7 +427,7 @@ def test_output_dtype_is_validated_after_predict(self, tmp_path: Path) -> None: bundle = build_test_bundle(tmp_path) class _WrongDtype(_EchoModel): - def predict(self, inputs): + def predict(self, inputs: dict[str, np.ndarray]) -> dict[str, np.ndarray]: outputs = super().predict(inputs) outputs["probabilities"] = outputs["probabilities"].astype(np.float64) return outputs @@ -404,7 +441,7 @@ def test_output_rank_is_validated_after_predict(self, tmp_path: Path) -> None: bundle = build_test_bundle(tmp_path) class _WrongRank(_EchoModel): - def predict(self, inputs): + def predict(self, inputs: dict[str, np.ndarray]) -> dict[str, np.ndarray]: outputs = super().predict(inputs) outputs["probabilities"] = outputs["probabilities"].reshape(-1) return outputs @@ -420,7 +457,7 @@ def test_output_fixed_dimension_is_validated_after_predict( bundle = build_test_bundle(tmp_path) class _WrongShape(_EchoModel): - def predict(self, inputs): + def predict(self, inputs: dict[str, np.ndarray]) -> dict[str, np.ndarray]: rows = inputs["float_input"].shape[0] return { "label": np.zeros(rows, dtype=np.int64), @@ -436,7 +473,7 @@ def test_output_row_count_must_match_input_batch(self, tmp_path: Path) -> None: bundle = build_test_bundle(tmp_path) class _WrongRows(_EchoModel): - def predict(self, inputs): + def predict(self, inputs: dict[str, np.ndarray]) -> dict[str, np.ndarray]: rows = inputs["float_input"].shape[0] return { "label": np.zeros(max(0, rows - 1), dtype=np.int64), @@ -533,7 +570,9 @@ def test_matrix_rejects_invalid_supported_formats( class _InvalidFormatsFlavor(_EchoFlavor): pass - _InvalidFormatsFlavor.supported_formats = supported_formats + monkeypatch.setattr( + _InvalidFormatsFlavor, "supported_formats", supported_formats + ) entry = runtime_module.FlavorSupportEntry( flavor_id=_InvalidFormatsFlavor.flavor_id, artifact_role="model", @@ -675,6 +714,40 @@ def load( # ── Runtime lifecycle ────────────────────────────────────────────────────────── +class _RecordingReader(BundleReader): + """Observe actual materialization contexts without replacing artifacts.""" + + def __init__(self, cache_dir: Path | None = None) -> None: + super().__init__(cache_dir=cache_dir) + self.exits: list[bool] = [] + + @contextmanager + def open_artifact( + self, + manifest_or_bundle_uri: BundleRef | str, + *, + role: str | None = None, + artifact_name: str | None = None, + storage_profile: str | None = None, + manifest: ExportManifest | None = None, + manifest_bytes: bytes | None = None, + ) -> Iterator[ResolvedArtifact]: + with super().open_artifact( + manifest_or_bundle_uri, + role=role, + artifact_name=artifact_name, + storage_profile=storage_profile, + manifest=manifest, + manifest_bytes=manifest_bytes, + ) as artifact: + index = len(self.exits) + self.exits.append(False) + try: + yield artifact + finally: + self.exits[index] = True + + class TestRuntimeLifecycle: """Runtime 持有 reader context;close 幂等;异常关闭不泄漏。""" @@ -709,43 +782,6 @@ def load( ) -> BundleModel: raise RuntimeError("boom") - class _RecordingReader: - def __init__(self) -> None: - self.exits: list[bool] = [] - - def read_manifest_with_bytes( - self, manifest_or_bundle_uri: str, *, storage_profile: str | None = None - ) -> tuple[Any, bytes]: - import json - - from tributo.exporting.manifest import _read_manifest_v1 - - manifest_bytes = (tmp_path / "bundle" / "manifest.json").read_bytes() - raw = json.loads(manifest_bytes) - return _read_manifest_v1(raw, manifest_bytes), manifest_bytes - - def open_artifact( - self, - manifest_or_bundle_uri: str, - *, - role: str | None = None, - artifact_name: str | None = None, - storage_profile: str | None = None, - manifest: Any = None, - manifest_bytes: bytes | None = None, - ) -> Any: - from contextlib import contextmanager - - @contextmanager - def _cm() -> Any: - self.exits.append(False) - try: - yield None - finally: - self.exits[-1] = True - - return _cm() - reader = _RecordingReader() registry = FlavorRegistry() registry.register(_ExplodingFlavor) @@ -774,28 +810,63 @@ class TestOnnxRuntimeEndToEnd: """真实 ONNX 模型经 BundleModelLoader 加载并推理。""" def test_load_and_predict_real_onnx(self, tmp_path: Path) -> None: - import pytest as _pytest - - onnx_path = None - try: - from tests.serving.bundle_fixtures import make_dummy_onnx - - onnx_path = make_dummy_onnx(tmp_path) - except Exception as exc: - _pytest.skip(f"skl2onnx or sklearn not installed: {exc}") - - bundle = build_test_bundle(tmp_path, onnx_path=onnx_path) - loader = BundleModelLoader() # 默认 registry(含内置 onnx-runtime-v1) + bundle = build_test_bundle( + tmp_path, onnx_path=_write_onnx_classifier_fixture(tmp_path) + ) + loader = BundleModelLoader() + inputs = { + "float_input": np.array( + [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]], dtype=np.float32 + ) + } runtime = loader.open(str(bundle), role="inference") try: - result = runtime.predict( - {"float_input": np.array([[0.5, 0.5]], dtype=np.float32)} + result = runtime.predict(inputs) + np.testing.assert_array_equal(result["label"], [0, 0, 1]) + np.testing.assert_allclose( + result["probabilities"], + np.array( + [[0.5, 0.5], [0.7310586, 0.2689414], [0.2689414, 0.7310586]], + dtype=np.float32, + ), + rtol=1e-6, + atol=1e-6, + ) + assert result["label"].dtype == np.int64 + assert result["probabilities"].dtype == np.float32 + runtime.close() + runtime.close() + assert runtime.closed + repeated = runtime.predict(inputs) + np.testing.assert_array_equal(repeated["label"], result["label"]) + np.testing.assert_allclose( + repeated["probabilities"], result["probabilities"] ) - assert "label" in result - assert "probabilities" in result finally: runtime.close() + @pytest.mark.parametrize("failure_kind", ("load", "signature")) + def test_real_onnx_failure_closes_artifact_context( + self, tmp_path: Path, failure_kind: str + ) -> None: + reader = _RecordingReader(cache_dir=tmp_path / "cache") + expected_error: type[ModelLoadError] | type[ModelSchemaMismatchError] + if failure_kind == "load": + bundle = build_test_bundle(tmp_path, model_bytes=b"invalid-onnx-model") + expected_error = ModelLoadError + expected_message = "Failed to load ONNX" + else: + bundle = build_test_bundle( + tmp_path, + onnx_path=_write_onnx_classifier_fixture(tmp_path), + input_field_name="unexpected_input", + ) + expected_error = ModelSchemaMismatchError + expected_message = "input" + with pytest.raises(expected_error, match=expected_message): + BundleModelLoader(bundle_reader=reader).open(str(bundle), role="inference") + assert reader.exits == [True] + def test_default_loader_requires_typed_signature(self, tmp_path: Path) -> None: """默认 loader(真实 ONNXRuntimeFlavor)拒绝空签名 bundle。""" bundle = build_test_bundle(tmp_path, with_signature=False) @@ -818,10 +889,9 @@ def test_file_uri_scheme_accepted(self, tmp_path: Path) -> None: def test_file_uri_real_onnx_predict(self, tmp_path: Path) -> None: """file:// URI + 真实 ONNX 模型端到端推理。""" - from tests.serving.bundle_fixtures import make_dummy_onnx - - onnx_path = make_dummy_onnx(tmp_path) - bundle = build_test_bundle(tmp_path, onnx_path=onnx_path) + bundle = build_test_bundle( + tmp_path, onnx_path=_write_onnx_classifier_fixture(tmp_path) + ) loader = BundleModelLoader() with loader.open("file://" + str(bundle), role="inference") as runtime: result = runtime.predict( @@ -829,3 +899,25 @@ def test_file_uri_real_onnx_predict(self, tmp_path: Path) -> None: ) assert result["label"].shape == (1,) assert result["probabilities"].shape == (1, 2) + + +def test_runtime_public_contract_stability_scope() -> None: + + for obj in ( + BundleReaderLike, + BundleModel, + BundleModelFlavor, + FlavorSupportEntry, + BundleModelLoader, + BundleModelRuntime, + ONNXRuntimeFlavor, + FlavorRegistry, + ArtifactCapability, + CapabilityRegistry, + get_default_capability_registry, + PluginLoadDiagnostic, + UnsupportedArtifactFormat, + ): + assert get_stability(obj) == "stable" + for beta_obj in (ExportRegistry, ModelFactoryRegistry, DependencySpec): + assert get_stability(beta_obj) == "beta" diff --git a/tests/test_stability_inventory.py b/tests/test_stability_inventory.py index 8e96d8f..ababf43 100644 --- a/tests/test_stability_inventory.py +++ b/tests/test_stability_inventory.py @@ -124,11 +124,11 @@ "tributo.exporting.records": "beta", "tributo.exporting.gc": "beta", "tributo.exporting.hooks": "beta", - "tributo.exporting.capabilities": "beta", + "tributo.exporting.capabilities": "stable", "tributo.exporting.events": "beta", "tributo.exporting.dispatch": "beta", "tributo.exporting.repository": "stable", - "tributo.exporting.runtime": "beta", + "tributo.exporting.runtime": "stable", "tributo.exporting.conftest": "beta", # Explainability — alpha "tributo.explainability.conformance": "alpha", @@ -146,7 +146,7 @@ "tributo.integrations.algorithm_inputs.ingestion": "alpha", "tributo.integrations.exporters": "beta", "tributo.integrations.flavors": "beta", - "tributo.integrations.flavors.onnx_runtime": "beta", + "tributo.integrations.flavors.onnx_runtime": "stable", "tributo.integrations.model_importers.artifact": "alpha", "tributo.integrations.model_importers.mlflow": "alpha", "tributo.integrations.model_importers.registry": "alpha", @@ -218,6 +218,8 @@ "tributo.data.source_config.ProviderSourceConfig": "stable", "tributo.data.source_config.source_projection": "stable", "tributo.data.source_config.apply_source_projection": "stable", + "tributo.exporting.registries.FlavorRegistry": "stable", + "tributo.exporting.models.PluginLoadDiagnostic": "stable", "tributo.exporting.models.AliasConfig": "stable", "tributo.exporting.models.ArtifactFile": "stable", "tributo.exporting.models.ProducerInfo": "stable", @@ -247,7 +249,7 @@ "tributo.exceptions.BundleExportError": "beta", "tributo.exceptions.BundleCommitBusyError": "stable", "tributo.exceptions.AliasConflict": "stable", - "tributo.exceptions.UnsupportedArtifactFormat": "beta", + "tributo.exceptions.UnsupportedArtifactFormat": "stable", "tributo.exceptions.PostPublishCallbackError": "beta", "tributo.exceptions.PluginLoadIssue": "beta", "tributo.exceptions.ResourceBudgetExceededError": "beta",