diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d5c256b..ebcc5a9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- **ODPS is no longer presented as a cloud provider.** ODPS (Open Data Product + Standard — Bitol / LF-ODPI) is a data-product *spec / export format*, not an + infrastructure provider like `aws`/`gcp`/`snowflake`/`local` (its + `OdpsProvider.apply()` was a no-op). It has been removed from the + `fluid_build.providers` entry-point group and the provider registry, so it no + longer appears in `fluid providers`, `fluid plugins`, or as a `--provider` + choice. The ODPS export is unchanged and fully supported via + `fluid odps export â€Ļ` (and `fluid export-opds`, `fluid generate standard + --format odps`). **Breaking (CLI surface):** `--provider odps` / `--provider + opds` are no longer accepted — use `fluid odps export` instead. + ## [0.8.11] - 2026-06-16 ### Added diff --git a/README.md b/README.md index 88769c7e..ce2143d4 100644 --- a/README.md +++ b/README.md @@ -472,7 +472,7 @@ Providers are the bridge between your declarative contract and your target execu | đŸŒŠī¸ **aws** | Amazon Web Services | S3, Glue, Athena, Redshift, MWAA, IAM. | | â„ī¸ **snowflake** | Snowflake | Databases, schemas, streams, tasks, RBAC, sharing. | -> Export-only providers for open data standards: **odps**, **odcs**, **datamesh-manager**. +> Open-data-standard **export formats** (not cloud providers): **ODPS** (Open Data Product Standard — Bitol / LF-ODPI) via `fluid odps`, plus **odcs** and **datamesh-manager** integrations. --- diff --git a/fluid_build/cli/__init__.py b/fluid_build/cli/__init__.py index 5d931656..f0f4cadd 100644 --- a/fluid_build/cli/__init__.py +++ b/fluid_build/cli/__init__.py @@ -145,7 +145,7 @@ def build_parser() -> argparse.ArgumentParser: ) p.add_argument( "--provider", - choices=["local", "gcp", "snowflake", "odps", "opds", "aws", "azure"], + choices=["local", "gcp", "snowflake", "aws", "azure"], default=os.getenv("FLUID_PROVIDER"), help="Infrastructure provider (env: FLUID_PROVIDER)", ) diff --git a/fluid_build/cli/odps.py b/fluid_build/cli/odps.py index f5e5ff72..f82a6515 100644 --- a/fluid_build/cli/odps.py +++ b/fluid_build/cli/odps.py @@ -208,13 +208,16 @@ def _export_odps_v4_1( args: argparse.Namespace, contract: Dict[str, Any], logger: logging.Logger ) -> int: """ODPS v4.1 (LF/ODPI) export path (single JSON document).""" - from fluid_build.cli.bootstrap import build_provider + # Direct class import — ODPS is a spec-EXPORT format, not a registry/cloud + # provider, so we no longer resolve it via build_provider()/the PROVIDERS + # registry (OdpsProvider was de-registered; see providers/odps/__init__.py). + from fluid_build.providers.odps.odps import OdpsProvider try: - provider = build_provider("odps", None, None, logger) + provider = OdpsProvider() except Exception as e: - logger.error("provider_build_failed", extra={"error": str(e)}) - console_error(f"Error building ODPS v4.1 provider: {e}") + logger.error("provider_build_failed", extra={"error": type(e).__name__}) + console_error(f"Error building ODPS v4.1 exporter: {type(e).__name__}") return 1 provider.opds_version = "4.1" diff --git a/fluid_build/providers/__init__.py b/fluid_build/providers/__init__.py index f5ce24dc..28729bb2 100644 --- a/fluid_build/providers/__init__.py +++ b/fluid_build/providers/__init__.py @@ -399,7 +399,8 @@ def clear_providers() -> None: "fluid_build.providers.gcp", "fluid_build.providers.aws", "fluid_build.providers.snowflake", - "fluid_build.providers.odps", + # odps is a spec-export format, not a cloud provider — intentionally excluded + # from provider preload/discovery (see providers/odps/__init__.py). ) diff --git a/fluid_build/providers/odps/__init__.py b/fluid_build/providers/odps/__init__.py index cedd7186..631621b0 100644 --- a/fluid_build/providers/odps/__init__.py +++ b/fluid_build/providers/odps/__init__.py @@ -12,18 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. -from fluid_build.providers import register_provider - -from .odps import ( - OdpsProvider, # class OdpsProvider(BaseProvider) with name="odps"; implements render() -) - -# Canonical name (matches the pyproject.toml entry-point + OdpsProvider.name). -register_provider("odps", OdpsProvider) -# Back-compat alias so pre-2026-05 invocations of ``--provider opds`` (and -# anyone who registered against the letter-swap key) keep resolving to the -# same class. The CLI argparse ``choices=[..., "odps", "opds", ...]`` accepts -# both spellings; the registry needs to honor the letter-swap to match. -register_provider("opds", OdpsProvider, override=False) - -__all__ = ["OdpsProvider"] +# ODPS (Open Data Product Standard, Bitol / LF-ODPI) is a data-product SPEC / +# serialization format — NOT a cloud/infrastructure provider like aws/gcp/ +# snowflake/local. ``OdpsProvider`` exports a contract to the ODPS spec via +# ``render()``; its ``apply()`` is a no-op. It is therefore intentionally NOT +# registered in the provider registry and NOT advertised as a +# ``fluid_build.providers`` entry-point, so it never appears in ``fluid +# providers`` / ``fluid plugins`` / ``--provider`` as a deployment target. +# +# The class stays importable for the spec-export commands, which construct it +# DIRECTLY: from fluid_build.providers.odps.odps import OdpsProvider +# (see cli/odps.py, cli/export_odps.py, cli/generate_standard.py). This package +# __init__ deliberately exposes no BaseProvider subclass, so the provider +# auto-discovery scan cannot re-register it. diff --git a/fluid_build/providers/odps_standard/__init__.py b/fluid_build/providers/odps_standard/__init__.py index 20539d0c..3131c7c6 100644 --- a/fluid_build/providers/odps_standard/__init__.py +++ b/fluid_build/providers/odps_standard/__init__.py @@ -14,14 +14,16 @@ Specification: https://github.com/bitol-io/open-data-product-standard """ -from fluid_build.providers import register_provider - from .odps import OdpsStandardProvider from .provider import BitolOdpsProvider -# Register under both names — the canonical name is ``odps_bitol`` per the -# Phase 5 disambiguation plan; the ``odps-standard`` legacy name still works. -register_provider("odps_bitol", BitolOdpsProvider) -register_provider("odps-standard", OdpsStandardProvider) +# Bitol/ODPS-standard is a data-product SPEC / export format, not a cloud +# provider — so these exporters are intentionally NOT registered in the +# provider registry and not advertised as `fluid_build.providers` entry-points +# (they never appear in `fluid providers` / `fluid plugins` / `--provider`). +# The classes stay importable for the spec-export commands, which construct +# them DIRECTLY: from fluid_build.providers.odps_standard import ( +# BitolOdpsProvider, OdpsStandardProvider) +# (see cli/odps.py, cli/odps_standard.py, cli/generate_standard.py). __all__ = ["BitolOdpsProvider", "OdpsStandardProvider"] diff --git a/pyproject.toml b/pyproject.toml index 7dfda22e..464a52bf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -360,7 +360,9 @@ local = "fluid_build.providers.local:LocalProvider" aws = "fluid_build.providers.aws:AwsProvider" gcp = "fluid_build.providers.gcp:GcpProvider" snowflake = "fluid_build.providers.snowflake:SnowflakeProvider" -odps = "fluid_build.providers.odps:OdpsProvider" +# NB: odps is intentionally NOT a provider entry-point. ODPS is the Open Data +# Product Standard (a spec / export format), not a cloud provider; its exporter +# is constructed directly by `fluid odps` (see providers/odps/__init__.py). # Metadata source adapters for ``fluid forge data-model from-source`` and the # ``forge_from_source`` MCP tool (issue #247) are a discovered plugin group: diff --git a/tests/providers/test_odps_not_a_provider.py b/tests/providers/test_odps_not_a_provider.py new file mode 100644 index 00000000..2d333d28 --- /dev/null +++ b/tests/providers/test_odps_not_a_provider.py @@ -0,0 +1,95 @@ +# Copyright 2024-2026 Agentics Transformation Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""ODPS is the Open Data Product STANDARD (a spec/export format), NOT a cloud +provider. It must never appear in the provider registry, `fluid providers`, +`fluid plugins`, or the `--provider` choices — while the OdpsProvider exporter +stays importable for `fluid odps`. +""" + +from __future__ import annotations + +from fluid_build import providers as P + +# Every ODPS spec-exporter registry name that must NOT appear as a provider. +_ODPS_EXPORTER_NAMES = ("odps", "opds", "odps_bitol", "odps-standard") + + +def test_no_odps_spec_exporter_in_provider_registry(): + # ODPS spec-exporters (odps/opds + the Bitol odps_bitol/odps-standard + # siblings) are constructed by direct import, never via the registry. + P.discover_providers(force=True) + names = set(P.list_providers()) + for n in _ODPS_EXPORTER_NAMES: + assert n not in names, f"{n!r} must not be a registry provider; got {sorted(names)}" + + +def test_odps_not_a_provider_entry_point(): + # The entry-point reader (what `fluid plugins` uses) must not list odps. + from fluid_build.plugin_manager import installed_plugins + + provider_names = {e["name"] for e in installed_plugins("provider").get("provider", [])} + assert ( + "odps" not in provider_names + ), f"odps must not be a provider entry-point; got {sorted(provider_names)}" + assert "opds" not in provider_names + + +def test_odps_package_init_exposes_no_baseprovider_subclass(): + # With no BaseProvider subclass in the package namespace, the auto-register + # discovery scan can never re-register odps. + import inspect + + import fluid_build.providers.odps as odps_pkg + from fluid_build.providers.base import BaseProvider + + subclasses = [ + obj + for _, obj in inspect.getmembers(odps_pkg, inspect.isclass) + if issubclass(obj, BaseProvider) and obj is not BaseProvider + ] + assert subclasses == [], f"odps/__init__ must expose no BaseProvider subclass; got {subclasses}" + + +def test_odps_exporters_still_importable_directly(): + # The spec-export commands construct these directly — de-registration must + # not break those paths. + from fluid_build.providers.odps.odps import OdpsProvider + from fluid_build.providers.odps_standard import BitolOdpsProvider, OdpsStandardProvider + + for cls in (OdpsProvider, BitolOdpsProvider, OdpsStandardProvider): + assert hasattr(cls(), "render") + + +def test_provider_choices_drop_odps_opds(): + from fluid_build.cli import build_parser + + parser = build_parser() + # Find the --provider argument's choices anywhere in the tree. + found_choices = None + + def _walk(p): + nonlocal found_choices + for action in p._actions: + if "--provider" in getattr(action, "option_strings", []) and action.choices: + found_choices = set(action.choices) + subs = [a for a in p._actions if hasattr(a, "choices") and isinstance(a.choices, dict)] + for s in subs: + for sp in (s.choices or {}).values(): + _walk(sp) + + _walk(parser) + if found_choices is not None: + assert "odps" not in found_choices + assert "opds" not in found_choices