pyoco 0.5.0__py3-none-any.whl → 0.5.1__py3-none-any.whl
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.
- pyoco/cli/main.py +51 -5
- pyoco/discovery/loader.py +5 -1
- pyoco/discovery/plugins.py +64 -8
- {pyoco-0.5.0.dist-info → pyoco-0.5.1.dist-info}/METADATA +4 -2
- {pyoco-0.5.0.dist-info → pyoco-0.5.1.dist-info}/RECORD +7 -7
- {pyoco-0.5.0.dist-info → pyoco-0.5.1.dist-info}/WHEEL +0 -0
- {pyoco-0.5.0.dist-info → pyoco-0.5.1.dist-info}/top_level.txt +0 -0
pyoco/cli/main.py
CHANGED
|
@@ -4,13 +4,13 @@ import sys
|
|
|
4
4
|
import os
|
|
5
5
|
import signal
|
|
6
6
|
import time
|
|
7
|
+
from types import SimpleNamespace
|
|
7
8
|
from ..schemas.config import PyocoConfig
|
|
8
9
|
from ..discovery.loader import TaskLoader
|
|
9
10
|
from ..core.models import Flow
|
|
10
11
|
from ..core.engine import Engine
|
|
11
12
|
from ..trace.console import ConsoleTraceBackend
|
|
12
13
|
from ..client import Client
|
|
13
|
-
from ..discovery.plugins import list_available_plugins
|
|
14
14
|
|
|
15
15
|
def main():
|
|
16
16
|
parser = argparse.ArgumentParser(description="Pyoco Workflow Engine")
|
|
@@ -88,6 +88,8 @@ def main():
|
|
|
88
88
|
plugins_sub = plugins_parser.add_subparsers(dest="plugins_command")
|
|
89
89
|
plugins_list = plugins_sub.add_parser("list", help="List discovered plug-ins")
|
|
90
90
|
plugins_list.add_argument("--json", action="store_true", help="Output JSON payload")
|
|
91
|
+
plugins_lint = plugins_sub.add_parser("lint", help="Validate plug-ins for upcoming requirements")
|
|
92
|
+
plugins_lint.add_argument("--json", action="store_true", help="Output JSON payload")
|
|
91
93
|
|
|
92
94
|
args = parser.parse_args()
|
|
93
95
|
|
|
@@ -120,18 +122,52 @@ def main():
|
|
|
120
122
|
return
|
|
121
123
|
|
|
122
124
|
if args.command == "plugins":
|
|
123
|
-
|
|
125
|
+
reports = _collect_plugin_reports()
|
|
124
126
|
if args.plugins_command == "list":
|
|
125
127
|
if getattr(args, "json", False):
|
|
126
|
-
print(json.dumps(
|
|
128
|
+
print(json.dumps(reports, indent=2))
|
|
127
129
|
else:
|
|
128
|
-
if not
|
|
130
|
+
if not reports:
|
|
129
131
|
print("No plug-ins registered under group 'pyoco.tasks'.")
|
|
130
132
|
else:
|
|
131
133
|
print("Discovered plug-ins:")
|
|
132
|
-
for info in
|
|
134
|
+
for info in reports:
|
|
133
135
|
mod = info.get("module") or info.get("value")
|
|
134
136
|
print(f" - {info.get('name')} ({mod})")
|
|
137
|
+
if info.get("error"):
|
|
138
|
+
print(f" ⚠️ error: {info['error']}")
|
|
139
|
+
continue
|
|
140
|
+
for task in info.get("tasks", []):
|
|
141
|
+
warn_msg = "; ".join(task.get("warnings", [])) or "ok"
|
|
142
|
+
print(f" • {task['name']} [{task['origin']}] ({warn_msg})")
|
|
143
|
+
for warn in info.get("warnings", []):
|
|
144
|
+
print(f" ⚠️ {warn}")
|
|
145
|
+
elif args.plugins_command == "lint":
|
|
146
|
+
issues = []
|
|
147
|
+
for info in reports:
|
|
148
|
+
prefix = info["name"]
|
|
149
|
+
if info.get("error"):
|
|
150
|
+
issues.append(f"{prefix}: {info['error']}")
|
|
151
|
+
continue
|
|
152
|
+
for warn in info.get("warnings", []):
|
|
153
|
+
issues.append(f"{prefix}: {warn}")
|
|
154
|
+
for task in info.get("tasks", []):
|
|
155
|
+
for warn in task.get("warnings", []):
|
|
156
|
+
issues.append(f"{prefix}.{task['name']}: {warn}")
|
|
157
|
+
payload = {"issues": issues, "reports": reports}
|
|
158
|
+
if getattr(args, "json", False):
|
|
159
|
+
print(json.dumps(payload, indent=2))
|
|
160
|
+
else:
|
|
161
|
+
if not issues:
|
|
162
|
+
print("✅ All plug-ins look good.")
|
|
163
|
+
else:
|
|
164
|
+
print("⚠️ Plug-in issues found:")
|
|
165
|
+
for issue in issues:
|
|
166
|
+
print(f" - {issue}")
|
|
167
|
+
if issues:
|
|
168
|
+
sys.exit(1)
|
|
169
|
+
else:
|
|
170
|
+
plugins_parser.print_help()
|
|
135
171
|
return
|
|
136
172
|
|
|
137
173
|
if args.command == "server":
|
|
@@ -368,6 +404,16 @@ def main():
|
|
|
368
404
|
sys.exit(2 if args.dry_run else 1)
|
|
369
405
|
return
|
|
370
406
|
|
|
407
|
+
def _collect_plugin_reports():
|
|
408
|
+
dummy = SimpleNamespace(
|
|
409
|
+
tasks={},
|
|
410
|
+
discovery=SimpleNamespace(entry_points=[], packages=[], glob_modules=[]),
|
|
411
|
+
)
|
|
412
|
+
loader = TaskLoader(dummy)
|
|
413
|
+
loader.load()
|
|
414
|
+
return loader.plugin_reports
|
|
415
|
+
|
|
416
|
+
|
|
371
417
|
def _stream_logs(client, args):
|
|
372
418
|
seen_seq = -1
|
|
373
419
|
follow = args.follow
|
pyoco/discovery/loader.py
CHANGED
|
@@ -109,6 +109,7 @@ class TaskLoader:
|
|
|
109
109
|
"value": ep.value,
|
|
110
110
|
"module": getattr(ep, "module", ""),
|
|
111
111
|
"tasks": [],
|
|
112
|
+
"warnings": [],
|
|
112
113
|
}
|
|
113
114
|
registry = PluginRegistry(self, ep.name)
|
|
114
115
|
try:
|
|
@@ -116,7 +117,10 @@ class TaskLoader:
|
|
|
116
117
|
if not callable(hook):
|
|
117
118
|
raise TypeError("Entry point must be callable")
|
|
118
119
|
hook(registry)
|
|
119
|
-
info["tasks"] = list(registry.
|
|
120
|
+
info["tasks"] = list(registry.records)
|
|
121
|
+
info["warnings"] = list(registry.warnings)
|
|
122
|
+
if not registry.records:
|
|
123
|
+
info["warnings"].append("no tasks registered")
|
|
120
124
|
except Exception as exc:
|
|
121
125
|
info["error"] = str(exc)
|
|
122
126
|
if self.strict:
|
pyoco/discovery/plugins.py
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from importlib import metadata as importlib_metadata
|
|
4
|
-
from typing import Any, Callable, Dict, List, Optional
|
|
4
|
+
from typing import Any, Callable, Dict, List, Optional, Type
|
|
5
5
|
|
|
6
6
|
from ..core.models import Task
|
|
7
7
|
from ..dsl.syntax import TaskWrapper
|
|
8
8
|
|
|
9
9
|
|
|
10
|
+
class CallablePluginTask(Task):
|
|
11
|
+
"""Lightweight subclass so callable registrations still appear as Task-derived."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, func: Callable, name: str):
|
|
14
|
+
super().__init__(func=func, name=name)
|
|
15
|
+
|
|
16
|
+
|
|
10
17
|
def iter_entry_points(group: str = "pyoco.tasks"):
|
|
11
18
|
eps = importlib_metadata.entry_points()
|
|
12
19
|
if hasattr(eps, "select"):
|
|
@@ -32,6 +39,8 @@ class PluginRegistry:
|
|
|
32
39
|
self.loader = loader
|
|
33
40
|
self.provider_name = provider_name
|
|
34
41
|
self.registered_names: List[str] = []
|
|
42
|
+
self.records: List[Dict[str, Any]] = []
|
|
43
|
+
self.warnings: List[str] = []
|
|
35
44
|
|
|
36
45
|
def task(
|
|
37
46
|
self,
|
|
@@ -70,23 +79,70 @@ class PluginRegistry:
|
|
|
70
79
|
outputs: Optional[List[str]] = None,
|
|
71
80
|
) -> Task:
|
|
72
81
|
task_name = name or getattr(func, "__name__", f"{self.provider_name}_task")
|
|
73
|
-
task =
|
|
82
|
+
task = CallablePluginTask(func=func, name=task_name)
|
|
74
83
|
if inputs:
|
|
75
84
|
task.inputs.update(inputs)
|
|
76
85
|
if outputs:
|
|
77
86
|
task.outputs.extend(outputs)
|
|
78
|
-
self.
|
|
79
|
-
|
|
87
|
+
self._finalize_task(task, origin="callable")
|
|
88
|
+
return task
|
|
89
|
+
|
|
90
|
+
def task_class(
|
|
91
|
+
self,
|
|
92
|
+
task_cls: Type[Task],
|
|
93
|
+
*args: Any,
|
|
94
|
+
name: Optional[str] = None,
|
|
95
|
+
**kwargs: Any,
|
|
96
|
+
) -> Task:
|
|
97
|
+
if not issubclass(task_cls, Task):
|
|
98
|
+
raise TypeError(f"{task_cls} is not a Task subclass")
|
|
99
|
+
task = task_cls(*args, **kwargs)
|
|
100
|
+
if name:
|
|
101
|
+
task.name = name
|
|
102
|
+
self._finalize_task(task, origin="task_class")
|
|
80
103
|
return task
|
|
81
104
|
|
|
82
105
|
def add(self, obj: Any, *, name: Optional[str] = None) -> None:
|
|
83
106
|
if isinstance(obj, TaskWrapper):
|
|
84
|
-
|
|
85
|
-
|
|
107
|
+
task = obj.task
|
|
108
|
+
if name:
|
|
109
|
+
task.name = name
|
|
110
|
+
self._finalize_task(task, origin="wrapper")
|
|
86
111
|
elif isinstance(obj, Task):
|
|
87
|
-
|
|
88
|
-
|
|
112
|
+
if name:
|
|
113
|
+
obj.name = name
|
|
114
|
+
origin = "task_class" if obj.__class__ is not Task else "task"
|
|
115
|
+
self._finalize_task(obj, origin=origin)
|
|
89
116
|
elif callable(obj):
|
|
90
117
|
self.register_callable(obj, name=name)
|
|
91
118
|
else:
|
|
92
119
|
raise TypeError(f"Unsupported task object: {obj!r}")
|
|
120
|
+
|
|
121
|
+
def _finalize_task(self, task: Task, origin: str) -> None:
|
|
122
|
+
warnings = self._validate_task(task, origin)
|
|
123
|
+
self.loader._register_task(task.name, task)
|
|
124
|
+
self.registered_names.append(task.name)
|
|
125
|
+
self.records.append(
|
|
126
|
+
{
|
|
127
|
+
"name": task.name,
|
|
128
|
+
"origin": origin,
|
|
129
|
+
"class": task.__class__.__name__,
|
|
130
|
+
"warnings": warnings,
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
for msg in warnings:
|
|
134
|
+
self.warnings.append(f"{task.name}: {msg}")
|
|
135
|
+
|
|
136
|
+
def _validate_task(self, task: Task, origin: str) -> List[str]:
|
|
137
|
+
warnings: List[str] = []
|
|
138
|
+
if not getattr(task, "name", None):
|
|
139
|
+
generated = f"{self.provider_name}_{len(self.registered_names) + 1}"
|
|
140
|
+
task.name = generated
|
|
141
|
+
warnings.append(f"name missing; auto-assigned '{generated}'")
|
|
142
|
+
if not callable(getattr(task, "func", None)):
|
|
143
|
+
warnings.append("task.func is not callable")
|
|
144
|
+
if origin == "callable":
|
|
145
|
+
warnings.append("registered via callable; prefer Task subclass for extensibility")
|
|
146
|
+
if task.__class__ is Task and origin not in ("callable", "wrapper"):
|
|
147
|
+
warnings.append("plain Task instance detected; subclass Task for metadata support")
|
|
148
|
+
return warnings
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyoco
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.1
|
|
4
4
|
Summary: A workflow engine with sugar syntax
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -143,7 +143,9 @@ pyoco run --non-cute ...
|
|
|
143
143
|
|
|
144
144
|
## 🧩 Plug-ins
|
|
145
145
|
|
|
146
|
-
Need to share domain-specific tasks? Publish an entry point under `pyoco.tasks` and pyoco will auto-load it. See [docs/plugins.md](docs/plugins.md) for
|
|
146
|
+
Need to share domain-specific tasks? Publish an entry point under `pyoco.tasks` and pyoco will auto-load it. In v0.5.1 we recommend **Task subclasses first** (callables still work with warnings). See [docs/plugins.md](docs/plugins.md) for examples, quickstart, and `pyoco plugins list` / `pyoco plugins lint`.
|
|
147
|
+
|
|
148
|
+
**Big data note:** pass handles, not copies. For large tensors/images, stash paths or handles in `ctx.artifacts`/`ctx.scratch` and let downstream tasks materialize only when needed. For lazy pipelines (e.g., DataPipe), log the pipeline when you actually iterate (typically the training task) instead of materializing upstream.
|
|
147
149
|
|
|
148
150
|
## 📚 Documentation
|
|
149
151
|
|
|
@@ -2,14 +2,14 @@ pyoco/__init__.py,sha256=E2pgDGvGRSVon7dSqIM4UD55LgVpf4jiZZA-70kOcuw,409
|
|
|
2
2
|
pyoco/client.py,sha256=Y95NmMsOKTJ9AZJEg_OzHamC_w32YWmSVS653mpqHVQ,3141
|
|
3
3
|
pyoco/socketless_reset.py,sha256=KsAF4I23_Kbhy9fIWFARzV5QaIOQqbl0U0yPb8a34sM,129
|
|
4
4
|
pyoco/cli/entry.py,sha256=zPIG0Gx-cFO8Cf1Z3wD3Ifz_2sHaryHZ6mCRri2WEqE,93
|
|
5
|
-
pyoco/cli/main.py,sha256=
|
|
5
|
+
pyoco/cli/main.py,sha256=LbhgTgRw9Tr_04hiYLqLP64jdnE1RA8B9Rasetgc_MM,18557
|
|
6
6
|
pyoco/core/base_task.py,sha256=z7hOFntAPv4yCADapS-fhtLe5eWqaO8k3T1r05YEEUE,2106
|
|
7
7
|
pyoco/core/context.py,sha256=TeCUriOmg7qZB3nMRu8HPdPshMW6pMVx48xZLY6a-A4,6524
|
|
8
8
|
pyoco/core/engine.py,sha256=iX2Id8ryFt-xeZgraqnF3uqkI6ubiZt5NBNYWX6Qv1s,24166
|
|
9
9
|
pyoco/core/exceptions.py,sha256=G82KY8PCnAhp3IDDIG8--Uh3EfVa192zei3l6ihfShI,565
|
|
10
10
|
pyoco/core/models.py,sha256=8faYURF43-7IebqzTIorHxpCeC4TZfoXWjGyPNaWhyI,10501
|
|
11
|
-
pyoco/discovery/loader.py,sha256=
|
|
12
|
-
pyoco/discovery/plugins.py,sha256=
|
|
11
|
+
pyoco/discovery/loader.py,sha256=L9Wb2i-d1Hv3EiTFUvuR2mrv7Fc9vt5Bv9ZRuRqAzSg,6132
|
|
12
|
+
pyoco/discovery/plugins.py,sha256=r1KY-OwWXSSe6arVOdfK72pGaI3tpumucg9cXEXA-Z0,4873
|
|
13
13
|
pyoco/dsl/__init__.py,sha256=xWdb60pSRL8lNFk4GHF3EJ4hon0uiWqpv264g6-4gdg,45
|
|
14
14
|
pyoco/dsl/expressions.py,sha256=BtEIxPSf3BU-wPNEicIqX_TVZ4fAnlWGrzrrfc6pU1g,4875
|
|
15
15
|
pyoco/dsl/nodes.py,sha256=qDiIEsAJHnD8dpuOd-Rpy6OORCW6KDW_BdYiA2BKu18,1041
|
|
@@ -27,7 +27,7 @@ pyoco/trace/console.py,sha256=I-BcF405OGLWoacJWeke8vTT9M5JxSBpJL-NazVyxb4,1742
|
|
|
27
27
|
pyoco/worker/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
28
28
|
pyoco/worker/client.py,sha256=862KccXRtfG7zd9ZSLqrpVSV6ev8zeuEHHdtAfLghiM,1557
|
|
29
29
|
pyoco/worker/runner.py,sha256=hyKn5NbuIuF-109CnQbYc8laKbWmwe9ChaLrNUtsVIg,6367
|
|
30
|
-
pyoco-0.5.
|
|
31
|
-
pyoco-0.5.
|
|
32
|
-
pyoco-0.5.
|
|
33
|
-
pyoco-0.5.
|
|
30
|
+
pyoco-0.5.1.dist-info/METADATA,sha256=JLUsGfujXl71AvCSuKDc52v2FjSxlWcIocGyCCzHnrU,5642
|
|
31
|
+
pyoco-0.5.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
32
|
+
pyoco-0.5.1.dist-info/top_level.txt,sha256=2JRVocfaWRbX1VJ3zq1c5wQaOK6fMARS6ptVFWyvRF4,6
|
|
33
|
+
pyoco-0.5.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|