wfc-client 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- wfc_client-0.1.0/LICENSE +21 -0
- wfc_client-0.1.0/PKG-INFO +64 -0
- wfc_client-0.1.0/README.md +39 -0
- wfc_client-0.1.0/pyproject.toml +33 -0
- wfc_client-0.1.0/wfc_client/__init__.py +30 -0
- wfc_client-0.1.0/wfc_client/context.py +137 -0
- wfc_client-0.1.0/wfc_client/decorator.py +35 -0
- wfc_client-0.1.0/wfc_client/errors.py +87 -0
- wfc_client-0.1.0/wfc_client/main.py +46 -0
wfc_client-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ddpoe
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: wfc-client
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pure-stdlib Tier-1 sugar for writing Workflow Canvas (wfc) methods
|
|
5
|
+
Home-page: https://github.com/ddpoe/workflow-canvas
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: workflow,pipeline,methods,reproducibility,provenance
|
|
8
|
+
Author: Dante
|
|
9
|
+
Author-email: dantepoe@noboundssolutions.com
|
|
10
|
+
Requires-Python: >=3.8
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Project-URL: Repository, https://github.com/ddpoe/workflow-canvas
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# wfc-client
|
|
26
|
+
|
|
27
|
+
Pure-stdlib Tier-1 sugar for writing [Workflow Canvas](https://github.com/ddpoe/workflow-canvas) (`wfc`) methods.
|
|
28
|
+
|
|
29
|
+
`wfc-client` is the *opt-in ergonomic* way to write a wfc method. The
|
|
30
|
+
canonical interface is the Tier-2 env-var + file contract — `wfc-client`
|
|
31
|
+
is a thin, zero-dependency wrapper over it. It never installs `wfc`,
|
|
32
|
+
pandas, or any third-party package into your environment, and it never
|
|
33
|
+
copies, reads, or serializes your data bytes: it is a metadata recorder.
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
import wfc_client as wfc
|
|
37
|
+
|
|
38
|
+
@wfc.method
|
|
39
|
+
def qc(ctx):
|
|
40
|
+
clean_path = ctx.workdir / "clean.csv"
|
|
41
|
+
# ... write your file with whatever library you like ...
|
|
42
|
+
ctx.save_artifact("clean", clean_path)
|
|
43
|
+
ctx.log_metric("kept_rows", 100)
|
|
44
|
+
|
|
45
|
+
if __name__ == "__main__":
|
|
46
|
+
wfc.run()
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## The `ctx` surface
|
|
50
|
+
|
|
51
|
+
| Member | Purpose |
|
|
52
|
+
|---|---|
|
|
53
|
+
| `ctx.input(slot)` | Resolved input paths (`list[Path]`) for an input slot. |
|
|
54
|
+
| `ctx.params` | Parsed params dict. |
|
|
55
|
+
| `ctx.workdir` | Scratch dir at `WFC_RUN_DIR/_workdir/` (auto-created). |
|
|
56
|
+
| `ctx.run_dir` | `WFC_RUN_DIR` (advanced). |
|
|
57
|
+
| `ctx.save_artifact(name, path)` | Record that `path` is the declared output `name`. Path only. |
|
|
58
|
+
| `ctx.log_metric(name, value)` | Record a scalar metric. |
|
|
59
|
+
|
|
60
|
+
At `wfc.run()` exit, `wfc-client` writes one `_wfc_results.json` manifest
|
|
61
|
+
of `{outputs, metrics}` with run-dir-relative paths. The host reads it.
|
|
62
|
+
|
|
63
|
+
Exactly one `@wfc.method` per method module is required.
|
|
64
|
+
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# wfc-client
|
|
2
|
+
|
|
3
|
+
Pure-stdlib Tier-1 sugar for writing [Workflow Canvas](https://github.com/ddpoe/workflow-canvas) (`wfc`) methods.
|
|
4
|
+
|
|
5
|
+
`wfc-client` is the *opt-in ergonomic* way to write a wfc method. The
|
|
6
|
+
canonical interface is the Tier-2 env-var + file contract — `wfc-client`
|
|
7
|
+
is a thin, zero-dependency wrapper over it. It never installs `wfc`,
|
|
8
|
+
pandas, or any third-party package into your environment, and it never
|
|
9
|
+
copies, reads, or serializes your data bytes: it is a metadata recorder.
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
import wfc_client as wfc
|
|
13
|
+
|
|
14
|
+
@wfc.method
|
|
15
|
+
def qc(ctx):
|
|
16
|
+
clean_path = ctx.workdir / "clean.csv"
|
|
17
|
+
# ... write your file with whatever library you like ...
|
|
18
|
+
ctx.save_artifact("clean", clean_path)
|
|
19
|
+
ctx.log_metric("kept_rows", 100)
|
|
20
|
+
|
|
21
|
+
if __name__ == "__main__":
|
|
22
|
+
wfc.run()
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## The `ctx` surface
|
|
26
|
+
|
|
27
|
+
| Member | Purpose |
|
|
28
|
+
|---|---|
|
|
29
|
+
| `ctx.input(slot)` | Resolved input paths (`list[Path]`) for an input slot. |
|
|
30
|
+
| `ctx.params` | Parsed params dict. |
|
|
31
|
+
| `ctx.workdir` | Scratch dir at `WFC_RUN_DIR/_workdir/` (auto-created). |
|
|
32
|
+
| `ctx.run_dir` | `WFC_RUN_DIR` (advanced). |
|
|
33
|
+
| `ctx.save_artifact(name, path)` | Record that `path` is the declared output `name`. Path only. |
|
|
34
|
+
| `ctx.log_metric(name, value)` | Record a scalar metric. |
|
|
35
|
+
|
|
36
|
+
At `wfc.run()` exit, `wfc-client` writes one `_wfc_results.json` manifest
|
|
37
|
+
of `{outputs, metrics}` with run-dir-relative paths. The host reads it.
|
|
38
|
+
|
|
39
|
+
Exactly one `@wfc.method` per method module is required.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "wfc-client"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Pure-stdlib Tier-1 sugar for writing Workflow Canvas (wfc) methods"
|
|
5
|
+
authors = ["Dante <dantepoe@noboundssolutions.com>"]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
homepage = "https://github.com/ddpoe/workflow-canvas"
|
|
9
|
+
repository = "https://github.com/ddpoe/workflow-canvas"
|
|
10
|
+
keywords = ["workflow", "pipeline", "methods", "reproducibility", "provenance"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 3 - Alpha",
|
|
13
|
+
"Intended Audience :: Science/Research",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.8",
|
|
18
|
+
"Programming Language :: Python :: 3.9",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
]
|
|
24
|
+
packages = [{ include = "wfc_client" }]
|
|
25
|
+
|
|
26
|
+
# Zero third-party dependencies by design (ADR-020): wfc-client is a
|
|
27
|
+
# metadata recorder that imports only the Python standard library.
|
|
28
|
+
[tool.poetry.dependencies]
|
|
29
|
+
python = ">=3.8"
|
|
30
|
+
|
|
31
|
+
[build-system]
|
|
32
|
+
requires = ["poetry-core"]
|
|
33
|
+
build-backend = "poetry.core.masonry.api"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""wfc-client — pure-stdlib Tier-1 sugar for writing wfc methods.
|
|
2
|
+
|
|
3
|
+
Usage::
|
|
4
|
+
|
|
5
|
+
import wfc_client as wfc
|
|
6
|
+
|
|
7
|
+
@wfc.method
|
|
8
|
+
def qc(ctx):
|
|
9
|
+
clean_path = ctx.workdir / "clean.csv"
|
|
10
|
+
... # write the file
|
|
11
|
+
ctx.save_artifact("clean", clean_path)
|
|
12
|
+
ctx.log_metric("kept_rows", 100)
|
|
13
|
+
|
|
14
|
+
if __name__ == "__main__":
|
|
15
|
+
wfc.run()
|
|
16
|
+
|
|
17
|
+
This package is a strict subset focused on the canonical Tier-2 env-var +
|
|
18
|
+
file contract (ADR-020). It has zero third-party dependencies and never
|
|
19
|
+
imports the full ``wfc`` package, pandas, or sqlmodel. It is a metadata
|
|
20
|
+
recorder: it never copies, reads, or serializes your data bytes.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from .context import RunContext
|
|
26
|
+
from .decorator import method
|
|
27
|
+
from .errors import ContractViolation
|
|
28
|
+
from .main import run
|
|
29
|
+
|
|
30
|
+
__all__ = ["method", "run", "RunContext", "ContractViolation"]
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""RunContext — the ``ctx`` object handed to ``@wfc.method`` functions.
|
|
2
|
+
|
|
3
|
+
This is the Tier-1 sugar over the canonical Tier-2 env-var + file contract
|
|
4
|
+
(ADR-020). It is a *metadata recorder*: it never copies, moves, reads, or
|
|
5
|
+
serializes the user's data bytes. ``save_artifact(name, path)`` records a
|
|
6
|
+
path; ``log_metric(name, value)`` records a scalar; at exit ``_finalize()``
|
|
7
|
+
writes a single ``_wfc_results.json`` manifest the host reads.
|
|
8
|
+
|
|
9
|
+
Pure stdlib: only ``json``, ``os``, ``pathlib``. No wfc / pandas /
|
|
10
|
+
sqlmodel imports, and no ``method.yaml`` read.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
# Filename of the single results channel (outputs + metrics) the host reads.
|
|
20
|
+
RESULTS_FILENAME = "_wfc_results.json"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class RunContext:
|
|
24
|
+
"""Runtime context for a wfc-managed method script.
|
|
25
|
+
|
|
26
|
+
Reads the canonical ``WFC_*`` environment variables the host sets
|
|
27
|
+
before launching the user process, and records declared outputs and
|
|
28
|
+
metrics for the host to archive after the process exits.
|
|
29
|
+
|
|
30
|
+
Attributes:
|
|
31
|
+
run_dir: ``WFC_RUN_DIR`` — the directory the host can read after
|
|
32
|
+
the container exits. All declared outputs must resolve inside
|
|
33
|
+
this directory.
|
|
34
|
+
workdir: A scratch directory at ``WFC_RUN_DIR/_workdir/``, created
|
|
35
|
+
on access. The host deletes it after archiving.
|
|
36
|
+
params: Parsed ``WFC_PARAMS`` dict.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self):
|
|
40
|
+
run_dir_env = os.environ.get("WFC_RUN_DIR")
|
|
41
|
+
if not run_dir_env:
|
|
42
|
+
raise RuntimeError(
|
|
43
|
+
"WFC_RUN_DIR is not set. wfc-client methods must be launched "
|
|
44
|
+
"by `wfc run-step`, which sets WFC_RUN_DIR / WFC_INPUT_PATHS / "
|
|
45
|
+
"WFC_PARAMS before running your script."
|
|
46
|
+
)
|
|
47
|
+
self.run_dir = Path(run_dir_env).resolve()
|
|
48
|
+
self.params = json.loads(os.environ.get("WFC_PARAMS", "{}"))
|
|
49
|
+
|
|
50
|
+
self._input_paths = json.loads(os.environ.get("WFC_INPUT_PATHS", "{}"))
|
|
51
|
+
self._outputs: "dict[str, str]" = {}
|
|
52
|
+
self._metrics: "dict[str, object]" = {}
|
|
53
|
+
self._workdir: "Path | None" = None
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def workdir(self) -> Path:
|
|
57
|
+
"""Scratch directory at ``WFC_RUN_DIR/_workdir/`` (created on access).
|
|
58
|
+
|
|
59
|
+
Located inside ``WFC_RUN_DIR`` so files written here automatically
|
|
60
|
+
satisfy ``save_artifact``'s path-inside-run_dir constraint and are
|
|
61
|
+
reachable by the host after the container exits.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
The path to the scratch directory.
|
|
65
|
+
"""
|
|
66
|
+
if self._workdir is None:
|
|
67
|
+
wd = self.run_dir / "_workdir"
|
|
68
|
+
wd.mkdir(parents=True, exist_ok=True)
|
|
69
|
+
self._workdir = wd
|
|
70
|
+
return self._workdir
|
|
71
|
+
|
|
72
|
+
def input(self, slot_name: str) -> "list[Path]":
|
|
73
|
+
"""Return resolved input paths for an input slot.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
slot_name: The input slot name as declared in ``method.yaml``.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
A list of resolved :class:`~pathlib.Path` objects from
|
|
80
|
+
``WFC_INPUT_PATHS`` for that slot, or an empty list if the
|
|
81
|
+
slot has no inputs.
|
|
82
|
+
"""
|
|
83
|
+
paths = self._input_paths.get(slot_name, [])
|
|
84
|
+
return [Path(p) for p in paths]
|
|
85
|
+
|
|
86
|
+
def save_artifact(self, name: str, source_path) -> None:
|
|
87
|
+
"""Record that the file at ``source_path`` is the declared output ``name``.
|
|
88
|
+
|
|
89
|
+
Does **not** copy, move, read, or serialize the file. The only
|
|
90
|
+
guard is that ``source_path`` must resolve to a path inside
|
|
91
|
+
``WFC_RUN_DIR`` (the bind-mounted directory the host can read
|
|
92
|
+
after the container exits). Output *type/extension* correctness is
|
|
93
|
+
validated host-side after the run, not here.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
name: The declared output name (from ``method.yaml``).
|
|
97
|
+
source_path: Path to the already-written output file. Must
|
|
98
|
+
resolve inside ``WFC_RUN_DIR``; use ``ctx.workdir`` or
|
|
99
|
+
``ctx.run_dir / 'name.ext'``.
|
|
100
|
+
|
|
101
|
+
Raises:
|
|
102
|
+
ValueError: If ``source_path`` resolves outside ``WFC_RUN_DIR``.
|
|
103
|
+
"""
|
|
104
|
+
resolved = Path(source_path).resolve()
|
|
105
|
+
try:
|
|
106
|
+
rel = resolved.relative_to(self.run_dir)
|
|
107
|
+
except ValueError:
|
|
108
|
+
raise ValueError(
|
|
109
|
+
f"save_artifact source must be inside WFC_RUN_DIR (got {source_path}). "
|
|
110
|
+
f"Use ctx.workdir or write to ctx.run_dir / 'name.ext'."
|
|
111
|
+
)
|
|
112
|
+
self._outputs[name] = rel.as_posix()
|
|
113
|
+
|
|
114
|
+
def log_metric(self, name: str, value) -> None:
|
|
115
|
+
"""Record a scalar metric.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
name: Metric name.
|
|
119
|
+
value: Scalar value (number, string, bool).
|
|
120
|
+
"""
|
|
121
|
+
self._metrics[name] = value
|
|
122
|
+
|
|
123
|
+
def _finalize(self) -> Path:
|
|
124
|
+
"""Write the ``_wfc_results.json`` manifest to ``WFC_RUN_DIR``.
|
|
125
|
+
|
|
126
|
+
The manifest is the single results channel for both declared
|
|
127
|
+
outputs and metrics. Output paths are relative to ``WFC_RUN_DIR``
|
|
128
|
+
so the host can join them against its own run-dir without any
|
|
129
|
+
container-vs-host path translation.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
The path to the written manifest.
|
|
133
|
+
"""
|
|
134
|
+
manifest = {"outputs": self._outputs, "metrics": self._metrics}
|
|
135
|
+
manifest_path = self.run_dir / RESULTS_FILENAME
|
|
136
|
+
manifest_path.write_text(json.dumps(manifest, default=str))
|
|
137
|
+
return manifest_path
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""The ``@wfc.method`` marker decorator.
|
|
2
|
+
|
|
3
|
+
Pure stdlib; no wfc / pandas imports.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Callable
|
|
9
|
+
|
|
10
|
+
# Module-level registry of @method-decorated functions for this method module.
|
|
11
|
+
# ``run()`` resolves exactly one entry; zero or more than one is an error.
|
|
12
|
+
_registry: "list[Callable]" = []
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def method(func: Callable) -> Callable:
|
|
16
|
+
"""Mark a function as the wfc method entry point.
|
|
17
|
+
|
|
18
|
+
The decorated function takes a single ``ctx`` argument (a
|
|
19
|
+
:class:`wfc_client.context.RunContext`). It produces outputs by
|
|
20
|
+
calling ``ctx.save_artifact(name, path)`` and metrics via
|
|
21
|
+
``ctx.log_metric(name, value)``. Its return value is ignored — there
|
|
22
|
+
is no return-value parsing.
|
|
23
|
+
|
|
24
|
+
Decorating is a no-op at import time beyond registration; dispatch
|
|
25
|
+
happens when :func:`wfc_client.main.run` is called.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
func: The method function to decorate. Should accept ``(ctx)``.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
The original function, unchanged, with ``_wfc_method = True`` set.
|
|
32
|
+
"""
|
|
33
|
+
func._wfc_method = True # type: ignore[attr-defined]
|
|
34
|
+
_registry.append(func)
|
|
35
|
+
return func
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Exceptions for wfc-client.
|
|
2
|
+
|
|
3
|
+
Pure stdlib; no wfc / pandas / sqlmodel imports.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ContractViolation(RuntimeError):
|
|
10
|
+
"""Raised when a method's outputs/metrics don't satisfy its contracts.
|
|
11
|
+
|
|
12
|
+
Provides an actionable error message showing what was missing,
|
|
13
|
+
what was produced, and where contracts are defined. The message
|
|
14
|
+
shape is preserved from the original in-tree ``wfc.method`` decorator
|
|
15
|
+
so error-message assertions remain stable across the extraction.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
method: str,
|
|
21
|
+
module: "str | None" = None,
|
|
22
|
+
missing_outputs: "list[str] | None" = None,
|
|
23
|
+
missing_metrics: "list[str] | None" = None,
|
|
24
|
+
available_outputs: "list[str] | None" = None,
|
|
25
|
+
available_metrics: "list[str] | None" = None,
|
|
26
|
+
extra_outputs: "list[str] | None" = None,
|
|
27
|
+
# ADR-005: content-level column validation fields
|
|
28
|
+
missing_columns: "list[str] | None" = None,
|
|
29
|
+
available_columns: "list[str] | None" = None,
|
|
30
|
+
slot_name: str = "",
|
|
31
|
+
):
|
|
32
|
+
self.method = method
|
|
33
|
+
self.module = module
|
|
34
|
+
self.missing_outputs = missing_outputs or []
|
|
35
|
+
self.missing_metrics = missing_metrics or []
|
|
36
|
+
self.available_outputs = available_outputs or []
|
|
37
|
+
self.available_metrics = available_metrics or []
|
|
38
|
+
self.extra_outputs = extra_outputs or []
|
|
39
|
+
self.missing_columns = missing_columns or []
|
|
40
|
+
self.available_columns = available_columns or []
|
|
41
|
+
self.slot_name = slot_name
|
|
42
|
+
|
|
43
|
+
lines = [f"ContractViolation: Method '{method}'"]
|
|
44
|
+
if module:
|
|
45
|
+
lines[0] += f" (module '{module}')"
|
|
46
|
+
|
|
47
|
+
if self.missing_columns:
|
|
48
|
+
lines.append("")
|
|
49
|
+
slot_label = f" (slot '{slot_name}')" if slot_name else ""
|
|
50
|
+
lines.append(f" Missing required input columns{slot_label}:")
|
|
51
|
+
for name in self.missing_columns:
|
|
52
|
+
lines.append(f" - {name}")
|
|
53
|
+
if self.available_columns:
|
|
54
|
+
lines.append(f" Available columns: {self.available_columns}")
|
|
55
|
+
|
|
56
|
+
if self.missing_outputs:
|
|
57
|
+
lines.append("")
|
|
58
|
+
lines.append(" Missing required outputs:")
|
|
59
|
+
for name in self.missing_outputs:
|
|
60
|
+
lines.append(f" - {name}")
|
|
61
|
+
|
|
62
|
+
if self.missing_metrics:
|
|
63
|
+
lines.append("")
|
|
64
|
+
lines.append(" Missing required metrics:")
|
|
65
|
+
for name in self.missing_metrics:
|
|
66
|
+
lines.append(f" - {name}")
|
|
67
|
+
|
|
68
|
+
if self.extra_outputs:
|
|
69
|
+
lines.append("")
|
|
70
|
+
lines.append(" Undeclared outputs returned (not in contract):")
|
|
71
|
+
for name in self.extra_outputs:
|
|
72
|
+
lines.append(f" - {name}")
|
|
73
|
+
|
|
74
|
+
if self.available_outputs or self.available_metrics:
|
|
75
|
+
lines.append("")
|
|
76
|
+
lines.append(" You returned:")
|
|
77
|
+
if self.available_outputs:
|
|
78
|
+
lines.append(f" outputs: {self.available_outputs}")
|
|
79
|
+
if self.available_metrics:
|
|
80
|
+
lines.append(f" metrics: {self.available_metrics}")
|
|
81
|
+
|
|
82
|
+
lines.append("")
|
|
83
|
+
lines.append(" Module contracts are defined by register_module(contracts=[...]).")
|
|
84
|
+
lines.append(" Declare each output with ctx.save_artifact(name, path) in your @wfc.method function.")
|
|
85
|
+
lines.append(" Log scalars with ctx.log_metric(name, value).")
|
|
86
|
+
|
|
87
|
+
super().__init__("\n".join(lines))
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""The ``wfc.run()`` entrypoint.
|
|
2
|
+
|
|
3
|
+
Resolves exactly one ``@wfc.method``-decorated function from the module
|
|
4
|
+
registry, builds a :class:`RunContext`, calls the function with ``ctx``,
|
|
5
|
+
and finalizes (writes the ``_wfc_results.json`` manifest). There is no
|
|
6
|
+
return-value parsing — the function's return value is ignored.
|
|
7
|
+
|
|
8
|
+
Pure stdlib.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from .context import RunContext
|
|
14
|
+
from .decorator import _registry
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def run() -> None:
|
|
18
|
+
"""Run the single ``@wfc.method``-decorated function in this module.
|
|
19
|
+
|
|
20
|
+
Resolves exactly one decorated function. Builds a ``RunContext`` from
|
|
21
|
+
the ``WFC_*`` env vars, calls ``func(ctx)``, then writes the results
|
|
22
|
+
manifest. The function's return value is ignored; all outputs flow
|
|
23
|
+
through ``ctx.save_artifact`` and all metrics through ``ctx.log_metric``.
|
|
24
|
+
|
|
25
|
+
Raises:
|
|
26
|
+
RuntimeError: If zero or more than one ``@wfc.method`` function is
|
|
27
|
+
registered in this module.
|
|
28
|
+
"""
|
|
29
|
+
if len(_registry) == 0:
|
|
30
|
+
raise RuntimeError(
|
|
31
|
+
"wfc.run() found no @wfc.method function. Decorate exactly one "
|
|
32
|
+
"function with @wfc.method before the "
|
|
33
|
+
"if __name__ == '__main__': block."
|
|
34
|
+
)
|
|
35
|
+
if len(_registry) > 1:
|
|
36
|
+
names = ", ".join(getattr(f, "__name__", repr(f)) for f in _registry)
|
|
37
|
+
raise RuntimeError(
|
|
38
|
+
f"wfc.run() found {len(_registry)} @wfc.method functions ({names}); "
|
|
39
|
+
f"exactly one is required per method module. Keep one entry point "
|
|
40
|
+
f"and move helpers into undecorated functions."
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
func = _registry[0]
|
|
44
|
+
ctx = RunContext()
|
|
45
|
+
func(ctx)
|
|
46
|
+
ctx._finalize()
|