pyoco 0.5.0__tar.gz → 0.5.1__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.
Files changed (48) hide show
  1. {pyoco-0.5.0 → pyoco-0.5.1}/PKG-INFO +4 -2
  2. {pyoco-0.5.0 → pyoco-0.5.1}/README.md +3 -1
  3. {pyoco-0.5.0 → pyoco-0.5.1}/pyproject.toml +1 -1
  4. {pyoco-0.5.0 → pyoco-0.5.1}/src/pyoco/cli/main.py +51 -5
  5. {pyoco-0.5.0 → pyoco-0.5.1}/src/pyoco/discovery/loader.py +5 -1
  6. pyoco-0.5.1/src/pyoco/discovery/plugins.py +148 -0
  7. {pyoco-0.5.0 → pyoco-0.5.1}/src/pyoco.egg-info/PKG-INFO +4 -2
  8. pyoco-0.5.0/src/pyoco/discovery/plugins.py +0 -92
  9. {pyoco-0.5.0 → pyoco-0.5.1}/setup.cfg +0 -0
  10. {pyoco-0.5.0 → pyoco-0.5.1}/src/pyoco/__init__.py +0 -0
  11. {pyoco-0.5.0 → pyoco-0.5.1}/src/pyoco/cli/entry.py +0 -0
  12. {pyoco-0.5.0 → pyoco-0.5.1}/src/pyoco/client.py +0 -0
  13. {pyoco-0.5.0 → pyoco-0.5.1}/src/pyoco/core/base_task.py +0 -0
  14. {pyoco-0.5.0 → pyoco-0.5.1}/src/pyoco/core/context.py +0 -0
  15. {pyoco-0.5.0 → pyoco-0.5.1}/src/pyoco/core/engine.py +0 -0
  16. {pyoco-0.5.0 → pyoco-0.5.1}/src/pyoco/core/exceptions.py +0 -0
  17. {pyoco-0.5.0 → pyoco-0.5.1}/src/pyoco/core/models.py +0 -0
  18. {pyoco-0.5.0 → pyoco-0.5.1}/src/pyoco/dsl/__init__.py +0 -0
  19. {pyoco-0.5.0 → pyoco-0.5.1}/src/pyoco/dsl/expressions.py +0 -0
  20. {pyoco-0.5.0 → pyoco-0.5.1}/src/pyoco/dsl/nodes.py +0 -0
  21. {pyoco-0.5.0 → pyoco-0.5.1}/src/pyoco/dsl/syntax.py +0 -0
  22. {pyoco-0.5.0 → pyoco-0.5.1}/src/pyoco/dsl/validator.py +0 -0
  23. {pyoco-0.5.0 → pyoco-0.5.1}/src/pyoco/schemas/config.py +0 -0
  24. {pyoco-0.5.0 → pyoco-0.5.1}/src/pyoco/server/__init__.py +0 -0
  25. {pyoco-0.5.0 → pyoco-0.5.1}/src/pyoco/server/api.py +0 -0
  26. {pyoco-0.5.0 → pyoco-0.5.1}/src/pyoco/server/metrics.py +0 -0
  27. {pyoco-0.5.0 → pyoco-0.5.1}/src/pyoco/server/models.py +0 -0
  28. {pyoco-0.5.0 → pyoco-0.5.1}/src/pyoco/server/store.py +0 -0
  29. {pyoco-0.5.0 → pyoco-0.5.1}/src/pyoco/server/webhook.py +0 -0
  30. {pyoco-0.5.0 → pyoco-0.5.1}/src/pyoco/socketless_reset.py +0 -0
  31. {pyoco-0.5.0 → pyoco-0.5.1}/src/pyoco/trace/backend.py +0 -0
  32. {pyoco-0.5.0 → pyoco-0.5.1}/src/pyoco/trace/console.py +0 -0
  33. {pyoco-0.5.0 → pyoco-0.5.1}/src/pyoco/worker/__init__.py +0 -0
  34. {pyoco-0.5.0 → pyoco-0.5.1}/src/pyoco/worker/client.py +0 -0
  35. {pyoco-0.5.0 → pyoco-0.5.1}/src/pyoco/worker/runner.py +0 -0
  36. {pyoco-0.5.0 → pyoco-0.5.1}/src/pyoco.egg-info/SOURCES.txt +0 -0
  37. {pyoco-0.5.0 → pyoco-0.5.1}/src/pyoco.egg-info/dependency_links.txt +0 -0
  38. {pyoco-0.5.0 → pyoco-0.5.1}/src/pyoco.egg-info/requires.txt +0 -0
  39. {pyoco-0.5.0 → pyoco-0.5.1}/src/pyoco.egg-info/top_level.txt +0 -0
  40. {pyoco-0.5.0 → pyoco-0.5.1}/tests/test_cancellation.py +0 -0
  41. {pyoco-0.5.0 → pyoco-0.5.1}/tests/test_cli_cancellation.py +0 -0
  42. {pyoco-0.5.0 → pyoco-0.5.1}/tests/test_dsl.py +0 -0
  43. {pyoco-0.5.0 → pyoco-0.5.1}/tests/test_e2e_socketless.py +0 -0
  44. {pyoco-0.5.0 → pyoco-0.5.1}/tests/test_engine.py +0 -0
  45. {pyoco-0.5.0 → pyoco-0.5.1}/tests/test_engine_state.py +0 -0
  46. {pyoco-0.5.0 → pyoco-0.5.1}/tests/test_integration_v030.py +0 -0
  47. {pyoco-0.5.0 → pyoco-0.5.1}/tests/test_socketless_basic.py +0 -0
  48. {pyoco-0.5.0 → pyoco-0.5.1}/tests/test_state_models.py +0 -0
@@ -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
 
@@ -131,7 +131,9 @@ pyoco run --non-cute ...
131
131
 
132
132
  ## 🧩 Plug-ins
133
133
 
134
- 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.
134
+ 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`.
135
+
136
+ **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.
135
137
 
136
138
  ## 📚 Documentation
137
139
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pyoco"
3
- version = "0.5.0"
3
+ version = "0.5.1"
4
4
  description = "A workflow engine with sugar syntax"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -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
@@ -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:
@@ -0,0 +1,148 @@
1
+ from __future__ import annotations
2
+
3
+ from importlib import metadata as importlib_metadata
4
+ from typing import Any, Callable, Dict, List, Optional, Type
5
+
6
+ from ..core.models import Task
7
+ from ..dsl.syntax import TaskWrapper
8
+
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
+
17
+ def iter_entry_points(group: str = "pyoco.tasks"):
18
+ eps = importlib_metadata.entry_points()
19
+ if hasattr(eps, "select"):
20
+ return list(eps.select(group=group))
21
+ return list(eps.get(group, []))
22
+
23
+
24
+ def list_available_plugins() -> List[Dict[str, Any]]:
25
+ plugins = []
26
+ for ep in iter_entry_points():
27
+ plugins.append(
28
+ {
29
+ "name": ep.name,
30
+ "module": getattr(ep, "module", ""),
31
+ "value": ep.value,
32
+ }
33
+ )
34
+ return plugins
35
+
36
+
37
+ class PluginRegistry:
38
+ def __init__(self, loader: Any, provider_name: str) -> None:
39
+ self.loader = loader
40
+ self.provider_name = provider_name
41
+ self.registered_names: List[str] = []
42
+ self.records: List[Dict[str, Any]] = []
43
+ self.warnings: List[str] = []
44
+
45
+ def task(
46
+ self,
47
+ func: Optional[Callable] = None,
48
+ *,
49
+ name: Optional[str] = None,
50
+ inputs: Optional[Dict[str, Any]] = None,
51
+ outputs: Optional[List[str]] = None,
52
+ ):
53
+ if func is not None:
54
+ self.register_callable(
55
+ func,
56
+ name=name,
57
+ inputs=inputs or {},
58
+ outputs=outputs or [],
59
+ )
60
+ return func
61
+
62
+ def decorator(inner: Callable):
63
+ self.register_callable(
64
+ inner,
65
+ name=name,
66
+ inputs=inputs or {},
67
+ outputs=outputs or [],
68
+ )
69
+ return inner
70
+
71
+ return decorator
72
+
73
+ def register_callable(
74
+ self,
75
+ func: Callable,
76
+ *,
77
+ name: Optional[str] = None,
78
+ inputs: Optional[Dict[str, Any]] = None,
79
+ outputs: Optional[List[str]] = None,
80
+ ) -> Task:
81
+ task_name = name or getattr(func, "__name__", f"{self.provider_name}_task")
82
+ task = CallablePluginTask(func=func, name=task_name)
83
+ if inputs:
84
+ task.inputs.update(inputs)
85
+ if outputs:
86
+ task.outputs.extend(outputs)
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")
103
+ return task
104
+
105
+ def add(self, obj: Any, *, name: Optional[str] = None) -> None:
106
+ if isinstance(obj, TaskWrapper):
107
+ task = obj.task
108
+ if name:
109
+ task.name = name
110
+ self._finalize_task(task, origin="wrapper")
111
+ elif isinstance(obj, Task):
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)
116
+ elif callable(obj):
117
+ self.register_callable(obj, name=name)
118
+ else:
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
 
@@ -1,92 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from importlib import metadata as importlib_metadata
4
- from typing import Any, Callable, Dict, List, Optional
5
-
6
- from ..core.models import Task
7
- from ..dsl.syntax import TaskWrapper
8
-
9
-
10
- def iter_entry_points(group: str = "pyoco.tasks"):
11
- eps = importlib_metadata.entry_points()
12
- if hasattr(eps, "select"):
13
- return list(eps.select(group=group))
14
- return list(eps.get(group, []))
15
-
16
-
17
- def list_available_plugins() -> List[Dict[str, Any]]:
18
- plugins = []
19
- for ep in iter_entry_points():
20
- plugins.append(
21
- {
22
- "name": ep.name,
23
- "module": getattr(ep, "module", ""),
24
- "value": ep.value,
25
- }
26
- )
27
- return plugins
28
-
29
-
30
- class PluginRegistry:
31
- def __init__(self, loader: Any, provider_name: str) -> None:
32
- self.loader = loader
33
- self.provider_name = provider_name
34
- self.registered_names: List[str] = []
35
-
36
- def task(
37
- self,
38
- func: Optional[Callable] = None,
39
- *,
40
- name: Optional[str] = None,
41
- inputs: Optional[Dict[str, Any]] = None,
42
- outputs: Optional[List[str]] = None,
43
- ):
44
- if func is not None:
45
- self.register_callable(
46
- func,
47
- name=name,
48
- inputs=inputs or {},
49
- outputs=outputs or [],
50
- )
51
- return func
52
-
53
- def decorator(inner: Callable):
54
- self.register_callable(
55
- inner,
56
- name=name,
57
- inputs=inputs or {},
58
- outputs=outputs or [],
59
- )
60
- return inner
61
-
62
- return decorator
63
-
64
- def register_callable(
65
- self,
66
- func: Callable,
67
- *,
68
- name: Optional[str] = None,
69
- inputs: Optional[Dict[str, Any]] = None,
70
- outputs: Optional[List[str]] = None,
71
- ) -> Task:
72
- task_name = name or getattr(func, "__name__", f"{self.provider_name}_task")
73
- task = Task(func=func, name=task_name)
74
- if inputs:
75
- task.inputs.update(inputs)
76
- if outputs:
77
- task.outputs.extend(outputs)
78
- self.loader._register_task(task_name, task)
79
- self.registered_names.append(task_name)
80
- return task
81
-
82
- def add(self, obj: Any, *, name: Optional[str] = None) -> None:
83
- 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)
86
- elif isinstance(obj, Task):
87
- self.loader._register_task(name or obj.name, obj)
88
- self.registered_names.append(name or obj.name)
89
- elif callable(obj):
90
- self.register_callable(obj, name=name)
91
- else:
92
- raise TypeError(f"Unsupported task object: {obj!r}")
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes