diff --git a/backend/fluksio/__init__.py b/backend/fluksio/__init__.py index 2709d71..388539f 100644 --- a/backend/fluksio/__init__.py +++ b/backend/fluksio/__init__.py @@ -4,7 +4,8 @@ Two audiences, one import name. In a research repository, ``from fluksio import Port, node, Flow`` is the authoring API: it says which of your existing functions are nodes and which nodes make up a flow, and ``fluksio sync`` uploads what it finds. Those names come from :mod:`fluksio.sdk`, which imports -nothing but the standard library. +nothing but the standard library. ``FLOWS`` and ``build`` come with them, so a +test can check the declarations without an engine. Inside a node, ``import fluksio`` is not this package at all: the worker installs a reporter of its own under that name before any node code runs, so @@ -24,7 +25,7 @@ from importlib.metadata import PackageNotFoundError from importlib.metadata import version as _version from typing import Any -from fluksio.sdk import Flow, Port, node, use +from fluksio.sdk import FLOWS, Flow, Port, build, node, use try: __version__ = _version("fluksio") @@ -32,8 +33,10 @@ except PackageNotFoundError: # pragma: no cover - a checkout that was never ins __version__ = "0.0.0+unknown" __all__ = [ + "FLOWS", "Flow", "Port", + "build", "emit", "load_artifact", "node", diff --git a/backend/tests/sdk/test_build.py b/backend/tests/sdk/test_build.py index a800217..2f68b3d 100644 --- a/backend/tests/sdk/test_build.py +++ b/backend/tests/sdk/test_build.py @@ -364,3 +364,15 @@ def test_a_node_in_a_script_run_directly_is_refused_at_sync_not_at_import(): flow.document() with pytest.raises(SyncError, match="run directly"): flow.shims() + + +def test_the_registry_and_the_builder_come_with_the_authoring_import(): + """A test of the declarations needs no second import path.""" + import fluksio + import fluksio.sdk + + flow = a_flow() + + assert fluksio.FLOWS is fluksio.sdk.FLOWS + assert fluksio.FLOWS["train"] is flow + assert fluksio.build(flow) == fluksio.sdk.build(flow) diff --git a/docs/getting-started/data-science.md b/docs/getting-started/data-science.md index 11a02eb..0086c3c 100644 --- a/docs/getting-started/data-science.md +++ b/docs/getting-started/data-science.md @@ -415,6 +415,10 @@ finetune = Flow( different message, keyword arguments override settings, and `id=` tells two uses of one function apart. `train` above is unaffected by any of it. +A test can check the declarations without an engine. Importing the module +registers its flows in `fluksio.FLOWS`, and `fluksio.build(flow)` returns the +document `sync` would upload, refusing what the engine would refuse. + ## Sync it ```sh diff --git a/worker/fluksio_worker/worker_main.py b/worker/fluksio_worker/worker_main.py index e999588..3cccbcc 100644 --- a/worker/fluksio_worker/worker_main.py +++ b/worker/fluksio_worker/worker_main.py @@ -101,6 +101,9 @@ class _Reporter(ModuleType): #: The same name the SDK exports at top level, so a node written against #: one surface finds it on the other. Set by ``_install_reporter``. logger: logging.Logger + #: The registry `fluksio sync` reads, empty here: nothing registers into + #: it inside a node. Set by ``_install_reporter``. + FLOWS: dict[str, Any] def emit(self, **ports: Any) -> None: """Publish on this node's output ports without returning yet. @@ -174,6 +177,10 @@ class _Reporter(ModuleType): """A flow declaration, already read by `fluksio sync`.""" return _Declared() + def build(self, *args: Any, **kwargs: Any) -> Any: + """A flow document, already built by `fluksio sync`.""" + return {} + class _LogHandler(logging.Handler): """Writes to whatever ``sys.stderr`` is at the time of the record. @@ -409,6 +416,7 @@ def _install_reporter() -> None: # its own logging installs a root handler on the same stream. logger.propagate = False module.logger = logger + module.FLOWS = {} sys.modules["fluksio"] = module