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",