Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

---

Expand Down
2 changes: 1 addition & 1 deletion fluid_build/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)",
)
Expand Down
11 changes: 7 additions & 4 deletions fluid_build/cli/odps.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 2 additions & 1 deletion fluid_build/providers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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).
)


Expand Down
28 changes: 13 additions & 15 deletions fluid_build/providers/odps/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
14 changes: 8 additions & 6 deletions fluid_build/providers/odps_standard/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
95 changes: 95 additions & 0 deletions tests/providers/test_odps_not_a_provider.py
Original file line number Diff line number Diff line change
@@ -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
Loading