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 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
- infos = list_available_plugins()
125
+ reports = _collect_plugin_reports()
124
126
  if args.plugins_command == "list":
125
127
  if getattr(args, "json", False):
126
- print(json.dumps(infos, indent=2))
128
+ print(json.dumps(reports, indent=2))
127
129
  else:
128
- if not infos:
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 infos:
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.registered_names)
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:
@@ -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 = Task(func=func, name=task_name)
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.loader._register_task(task_name, task)
79
- self.registered_names.append(task_name)
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
- self.loader._register_task(name or obj.task.name, obj.task)
85
- self.registered_names.append(name or obj.task.name)
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
- self.loader._register_task(name or obj.name, obj)
88
- self.registered_names.append(name or obj.name)
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.0
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 the `PluginRegistry` decorator, example `pyproject.toml`, and `pyoco plugins list` CLI helper.
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=W3U-T4SliJHy4wZ70QoN2c9Mep--XxlEa8YkHe9DLuU,16515
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=vC729i1bR358u6YwiVX2uonZ80WxjFGFqJRlhX89Sf0,5942
12
- pyoco/discovery/plugins.py,sha256=pNMWxS03jWPuUV2tApGch2VL40EyKLOOeYT-OPBBBRQ,2806
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.0.dist-info/METADATA,sha256=SEow4x2y_O6SqeVuEU-_7dT12F-XA24sYVD_hMV6TQM,5251
31
- pyoco-0.5.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
32
- pyoco-0.5.0.dist-info/top_level.txt,sha256=2JRVocfaWRbX1VJ3zq1c5wQaOK6fMARS6ptVFWyvRF4,6
33
- pyoco-0.5.0.dist-info/RECORD,,
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