Orcca 0.1.0__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.
- orcca/__init__.py +7 -0
- orcca/cli.py +617 -0
- orcca/click_cli.py +154 -0
- orcca/dagger.py +492 -0
- orcca/executor.py +1432 -0
- orcca/io_utils.py +153 -0
- orcca/launchers.py +39 -0
- orcca/resolver.py +597 -0
- orcca/resources/__init__.py +1 -0
- orcca/resources/rules/business-rules.yaml +187 -0
- orcca/resources/schema/business-rules-linkml.yaml +127 -0
- orcca/resources/schema/flow-linkml.yaml +620 -0
- orcca/runtime_launchers.py +430 -0
- orcca/validator.py +1234 -0
- orcca/workflow.py +518 -0
- orcca/workflow_runner.py +590 -0
- orcca-0.1.0.dist-info/METADATA +269 -0
- orcca-0.1.0.dist-info/RECORD +21 -0
- orcca-0.1.0.dist-info/WHEEL +5 -0
- orcca-0.1.0.dist-info/entry_points.txt +2 -0
- orcca-0.1.0.dist-info/top_level.txt +1 -0
orcca/__init__.py
ADDED
orcca/cli.py
ADDED
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import importlib.util
|
|
5
|
+
import inspect
|
|
6
|
+
import json
|
|
7
|
+
import shutil
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from orcca.dagger import _DagBuildError
|
|
13
|
+
from orcca.executor import _ExecutionError
|
|
14
|
+
from orcca.resolver import _ManifestResolutionError
|
|
15
|
+
from orcca.runtime_launchers import _register_custom_launcher
|
|
16
|
+
from orcca.validator import (
|
|
17
|
+
_ManifestValidationError,
|
|
18
|
+
_format_validation_report,
|
|
19
|
+
)
|
|
20
|
+
from orcca.workflow import Workflow
|
|
21
|
+
from orcca.workflow_runner import _WorkflowRunError, _run_workflow
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CliBootstrapError(Exception):
|
|
25
|
+
"""Raised when CLI bootstrap steps fail."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _find_project_launchers_dir(start_dir: Path) -> Path | None:
|
|
29
|
+
cur = start_dir.resolve()
|
|
30
|
+
for candidate_root in [cur, *cur.parents]:
|
|
31
|
+
launchers_dir = candidate_root / "launchers"
|
|
32
|
+
if launchers_dir.exists() and launchers_dir.is_dir():
|
|
33
|
+
return launchers_dir
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _load_module_from_path(py_file: Path, module_name: str) -> Any:
|
|
38
|
+
spec = importlib.util.spec_from_file_location(module_name, py_file)
|
|
39
|
+
if spec is None or spec.loader is None:
|
|
40
|
+
raise CliBootstrapError(f"Unable to load launcher module from {py_file}")
|
|
41
|
+
module = importlib.util.module_from_spec(spec)
|
|
42
|
+
try:
|
|
43
|
+
spec.loader.exec_module(module)
|
|
44
|
+
except Exception as exc: # noqa: BLE001
|
|
45
|
+
raise CliBootstrapError(f"Failed to execute launcher module: {py_file}: {exc}") from exc
|
|
46
|
+
return module
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _autoload_launcher_modules_from_dir(launchers_dir: Path) -> None:
|
|
50
|
+
launcher_files = sorted(
|
|
51
|
+
p for p in launchers_dir.glob("*.py")
|
|
52
|
+
if p.name != "register.py" and not p.name.startswith("_")
|
|
53
|
+
)
|
|
54
|
+
for launcher_file in launcher_files:
|
|
55
|
+
launcher_id = launcher_file.stem
|
|
56
|
+
module_name = f"orcca_project_launcher_{abs(hash(str(launcher_file.resolve())))}"
|
|
57
|
+
module = _load_module_from_path(launcher_file, module_name)
|
|
58
|
+
candidate = getattr(module, launcher_id, None)
|
|
59
|
+
if candidate is None or not inspect.isfunction(candidate):
|
|
60
|
+
raise CliBootstrapError(
|
|
61
|
+
f"Launcher file '{launcher_file.name}' must define a function named '{launcher_id}'"
|
|
62
|
+
)
|
|
63
|
+
_register_custom_launcher(launcher_id, candidate)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _autoload_project_launchers(args: argparse.Namespace) -> None:
|
|
67
|
+
start_dir: Path
|
|
68
|
+
project_root = getattr(args, "project_root", None)
|
|
69
|
+
workspace_root = getattr(args, "workspace_root", None)
|
|
70
|
+
manifest_path = getattr(args, "manifest", None)
|
|
71
|
+
if isinstance(project_root, str) and project_root:
|
|
72
|
+
start_dir = Path(project_root).resolve()
|
|
73
|
+
elif isinstance(workspace_root, str) and workspace_root:
|
|
74
|
+
start_dir = Path(workspace_root).resolve()
|
|
75
|
+
elif isinstance(manifest_path, str) and manifest_path:
|
|
76
|
+
start_dir = Path(manifest_path).resolve().parent
|
|
77
|
+
else:
|
|
78
|
+
start_dir = Path.cwd()
|
|
79
|
+
|
|
80
|
+
launchers_dir = _find_project_launchers_dir(start_dir)
|
|
81
|
+
if launchers_dir is None:
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
register_file = launchers_dir / "register.py"
|
|
85
|
+
if register_file.exists() and register_file.is_file():
|
|
86
|
+
module_name = f"orcca_project_launchers_{abs(hash(str(register_file.resolve())))}"
|
|
87
|
+
_load_module_from_path(register_file, module_name)
|
|
88
|
+
|
|
89
|
+
_autoload_launcher_modules_from_dir(launchers_dir)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _log(message: str) -> None:
|
|
93
|
+
print(f"[orcca] {message}")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def validate(
|
|
97
|
+
manifest: str | Path,
|
|
98
|
+
report_path: str | Path = ".orcca/validation/manifest_validation_report.html",
|
|
99
|
+
) -> dict[str, Any]:
|
|
100
|
+
"""Resolve, validate, and write a manifest validation report.
|
|
101
|
+
|
|
102
|
+
This is the Python counterpart to `orcca validate`.
|
|
103
|
+
|
|
104
|
+
Parameters
|
|
105
|
+
----------
|
|
106
|
+
manifest : str or pathlib.Path
|
|
107
|
+
Path to the root workflow manifest YAML file.
|
|
108
|
+
report_path : str or pathlib.Path, default=".orcca/validation/manifest_validation_report.html"
|
|
109
|
+
Destination for the HTML validation report.
|
|
110
|
+
Returns
|
|
111
|
+
-------
|
|
112
|
+
dict[str, Any]
|
|
113
|
+
Payload containing the resolved validation result and report path.
|
|
114
|
+
"""
|
|
115
|
+
workflow = Workflow(manifest)
|
|
116
|
+
return workflow.validate(report_path=report_path)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def resolve(
|
|
120
|
+
manifest: str | Path,
|
|
121
|
+
output_path: str | Path = ".orcca/resolved_manifest.yaml",
|
|
122
|
+
) -> dict[str, Any]:
|
|
123
|
+
"""Resolve a manifest and write the resolved YAML to disk.
|
|
124
|
+
|
|
125
|
+
This is the Python counterpart to `orcca resolve`.
|
|
126
|
+
|
|
127
|
+
Parameters
|
|
128
|
+
----------
|
|
129
|
+
manifest : str or pathlib.Path
|
|
130
|
+
Path to the root workflow manifest YAML file.
|
|
131
|
+
output_path : str or pathlib.Path, default=".orcca/resolved_manifest.yaml"
|
|
132
|
+
Destination for the resolved manifest YAML file.
|
|
133
|
+
Returns
|
|
134
|
+
-------
|
|
135
|
+
dict[str, Any]
|
|
136
|
+
Payload containing the resolved manifest path and resolved manifest.
|
|
137
|
+
"""
|
|
138
|
+
workflow = Workflow(manifest)
|
|
139
|
+
return workflow.write_resolved_manifest(output_path=output_path)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def plan(
|
|
143
|
+
manifest: str | Path,
|
|
144
|
+
) -> dict[str, Any]:
|
|
145
|
+
"""Resolve, validate, and compute the workflow plan.
|
|
146
|
+
|
|
147
|
+
This is the Python counterpart to `orcca plan`.
|
|
148
|
+
|
|
149
|
+
Parameters
|
|
150
|
+
----------
|
|
151
|
+
manifest : str or pathlib.Path
|
|
152
|
+
Path to the root workflow manifest YAML file.
|
|
153
|
+
Returns
|
|
154
|
+
-------
|
|
155
|
+
dict[str, Any]
|
|
156
|
+
Planning payload, including validity and dependency information.
|
|
157
|
+
"""
|
|
158
|
+
workflow = Workflow(manifest)
|
|
159
|
+
return workflow.plan()
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def run(
|
|
163
|
+
manifest: str | Path,
|
|
164
|
+
project_root: str | Path = ".",
|
|
165
|
+
run_dir: str | Path | None = None,
|
|
166
|
+
workers: int | None = None,
|
|
167
|
+
task_ids: list[str] | None = None,
|
|
168
|
+
artifact_ids: list[str] | None = None,
|
|
169
|
+
run_validations: bool = False,
|
|
170
|
+
validation_ids: list[str] | None = None,
|
|
171
|
+
no_execute: bool = False,
|
|
172
|
+
no_env_cache: bool = False,
|
|
173
|
+
refresh_env: bool = False,
|
|
174
|
+
) -> Any:
|
|
175
|
+
"""Run the full workflow pipeline.
|
|
176
|
+
|
|
177
|
+
This is the Python counterpart to `orcca run`.
|
|
178
|
+
"""
|
|
179
|
+
workflow = Workflow(manifest, workspace_root=project_root, runs_root=Path(project_root) / ".orcca" / "runs")
|
|
180
|
+
if validation_ids and not run_validations:
|
|
181
|
+
raise _WorkflowRunError("--validation-id requires --run-validations")
|
|
182
|
+
return workflow.run(
|
|
183
|
+
run_dir=run_dir,
|
|
184
|
+
execute=not no_execute,
|
|
185
|
+
workers=workers,
|
|
186
|
+
task_ids=task_ids,
|
|
187
|
+
artifact_ids=artifact_ids,
|
|
188
|
+
run_validations=run_validations,
|
|
189
|
+
validation_ids=validation_ids,
|
|
190
|
+
use_env_cache=not no_env_cache,
|
|
191
|
+
refresh_env=refresh_env,
|
|
192
|
+
progress=_log,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def dag(
|
|
197
|
+
manifest: str | Path,
|
|
198
|
+
project_root: str | Path = ".",
|
|
199
|
+
output_path: str | Path = ".orcca/graphs/artifact-dag.html",
|
|
200
|
+
artifact_id: str | None = None,
|
|
201
|
+
) -> dict[str, Any]:
|
|
202
|
+
"""Export an artifact dependency graph as an HTML file.
|
|
203
|
+
|
|
204
|
+
This is the Python counterpart to `orcca dag`.
|
|
205
|
+
|
|
206
|
+
Parameters
|
|
207
|
+
----------
|
|
208
|
+
manifest
|
|
209
|
+
Path to the root workflow manifest YAML file.
|
|
210
|
+
project_root
|
|
211
|
+
Project workspace root used to resolve the manifest.
|
|
212
|
+
output_path
|
|
213
|
+
Destination HTML file for the graph export.
|
|
214
|
+
artifact_id
|
|
215
|
+
Optional generated artifact id to focus on.
|
|
216
|
+
|
|
217
|
+
Returns
|
|
218
|
+
-------
|
|
219
|
+
dict[str, Any]
|
|
220
|
+
HTML path plus graph summary metadata.
|
|
221
|
+
"""
|
|
222
|
+
workflow = Workflow(manifest, workspace_root=project_root, runs_root=Path(project_root) / ".orcca" / "runs")
|
|
223
|
+
return workflow.export_artifact_dag(output_path=output_path, artifact_id=artifact_id)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def env_cache(
|
|
227
|
+
action: str,
|
|
228
|
+
prune_all: bool = False,
|
|
229
|
+
output_format: str = "pretty",
|
|
230
|
+
) -> dict[str, Any]:
|
|
231
|
+
"""List or prune the package-manager environment cache.
|
|
232
|
+
|
|
233
|
+
This is the Python counterpart to `orcca env-cache`.
|
|
234
|
+
"""
|
|
235
|
+
root = _env_cache_root()
|
|
236
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
237
|
+
if action == "list":
|
|
238
|
+
entries: list[dict[str, Any]] = []
|
|
239
|
+
for d in sorted(p for p in root.iterdir() if p.is_dir()):
|
|
240
|
+
meta_path = d / "env_meta.json"
|
|
241
|
+
meta: dict[str, Any] = {}
|
|
242
|
+
if meta_path.exists():
|
|
243
|
+
try:
|
|
244
|
+
meta = json.loads(meta_path.read_text(encoding="utf-8"))
|
|
245
|
+
except Exception:
|
|
246
|
+
meta = {"error": "invalid_meta_json"}
|
|
247
|
+
entries.append({"path": str(d), "meta": meta})
|
|
248
|
+
return {"cache_root": str(root), "entries": entries}
|
|
249
|
+
|
|
250
|
+
if action == "prune":
|
|
251
|
+
removed = 0
|
|
252
|
+
for d in sorted(p for p in root.iterdir() if p.is_dir()):
|
|
253
|
+
if prune_all or not (d / "env_meta.json").exists():
|
|
254
|
+
shutil.rmtree(d, ignore_errors=True)
|
|
255
|
+
removed += 1
|
|
256
|
+
return {"cache_root": str(root), "removed": removed}
|
|
257
|
+
|
|
258
|
+
raise ValueError(f"unsupported env-cache action '{action}'")
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def init(
|
|
262
|
+
path: str | Path = ".",
|
|
263
|
+
) -> dict[str, Any]:
|
|
264
|
+
"""Scaffold a language-agnostic starter project.
|
|
265
|
+
|
|
266
|
+
This is the Python counterpart to `orcca init`.
|
|
267
|
+
"""
|
|
268
|
+
project_dir = Path(path).resolve()
|
|
269
|
+
project_dir.mkdir(parents=True, exist_ok=True)
|
|
270
|
+
(project_dir / "launchers").mkdir(parents=True, exist_ok=True)
|
|
271
|
+
(project_dir / "workflows").mkdir(parents=True, exist_ok=True)
|
|
272
|
+
(project_dir / "manifests").mkdir(parents=True, exist_ok=True)
|
|
273
|
+
|
|
274
|
+
(project_dir / "launchers" / "default.py").write_text(
|
|
275
|
+
"def default(*, task, runtime, root):\n"
|
|
276
|
+
" executable = runtime.get('executable', 'python')\n"
|
|
277
|
+
" options = runtime.get('options', [])\n"
|
|
278
|
+
" script = task['script']\n"
|
|
279
|
+
" return [executable, *options, script]\n",
|
|
280
|
+
encoding="utf-8",
|
|
281
|
+
)
|
|
282
|
+
(project_dir / "workflows" / "main.py").write_text(
|
|
283
|
+
"from pathlib import Path\nPath('output.txt').write_text('ok\\n', encoding='utf-8')\n",
|
|
284
|
+
encoding="utf-8",
|
|
285
|
+
)
|
|
286
|
+
(project_dir / "requirements.txt").write_text("pandas==2.3.2\n", encoding="utf-8")
|
|
287
|
+
manifest = (
|
|
288
|
+
"id: starter\nname: Starter Workflow\nstudy:\n id: ST-001\n"
|
|
289
|
+
"artifacts:\n - id: out\n name: Out\n type: report\n origin: generated\n path: output.txt\n"
|
|
290
|
+
"runtimes:\n - id: runtime\n programming_language: python\n version: '3.11'\n launcher: default\n"
|
|
291
|
+
" executable: python\n environment:\n type: package_manager\n manager: pip\n lockfile: requirements.txt\n"
|
|
292
|
+
"tasks:\n - id: t1\n runtimeId: runtime\n script: workflows/main.py\n outputs:\n artifactIds: [out]\n"
|
|
293
|
+
"execution:\n mode: serial\n"
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
(project_dir / "manifests" / "workflow.yaml").write_text(manifest, encoding="utf-8")
|
|
297
|
+
return {"initialized": str(project_dir)}
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def cmd_validate(args: argparse.Namespace) -> int:
|
|
301
|
+
"""Resolve a manifest, validate it, and write a report.
|
|
302
|
+
|
|
303
|
+
This command checks the manifest structure and workflow rules without
|
|
304
|
+
running any workflow tasks. It is the CLI entry point for preflight
|
|
305
|
+
validation and is useful in both local development and CI pipelines.
|
|
306
|
+
|
|
307
|
+
Parameters
|
|
308
|
+
----------
|
|
309
|
+
args : argparse.Namespace
|
|
310
|
+
Parsed CLI arguments for the `validate` command.
|
|
311
|
+
|
|
312
|
+
Returns
|
|
313
|
+
-------
|
|
314
|
+
int
|
|
315
|
+
Exit code (`0` when valid, `1` when invalid).
|
|
316
|
+
"""
|
|
317
|
+
result = Workflow(args.manifest).validate(report_path=args.report_path)
|
|
318
|
+
|
|
319
|
+
print(f"validation_report_path: {result['validation_report_path']}")
|
|
320
|
+
print(f"valid: {result['valid']}")
|
|
321
|
+
return 0 if result["valid"] else 1
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def cmd_resolve(args: argparse.Namespace) -> int:
|
|
325
|
+
"""Resolve a manifest and write the expanded YAML to disk.
|
|
326
|
+
|
|
327
|
+
The resolved manifest includes the final values after includes and config
|
|
328
|
+
files have been applied. Use this when you want to inspect or archive the
|
|
329
|
+
exact manifest that downstream tools will consume.
|
|
330
|
+
"""
|
|
331
|
+
result = resolve(args.manifest, output_path=args.output_path)
|
|
332
|
+
print(f"resolved_manifest_path: {result['resolved_manifest_path']}")
|
|
333
|
+
return 0
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def cmd_plan(args: argparse.Namespace) -> int:
|
|
337
|
+
"""Resolve a manifest, validate it, and print planning metadata.
|
|
338
|
+
|
|
339
|
+
The plan output is intended for inspection rather than execution. It helps
|
|
340
|
+
you understand task ordering, generated-artifact dependencies, and whether
|
|
341
|
+
the manifest is valid before any runtime work begins.
|
|
342
|
+
|
|
343
|
+
Examples
|
|
344
|
+
--------
|
|
345
|
+
```{python}
|
|
346
|
+
from orcca.cli import cmd_plan, cmd_run, cmd_env_cache, cmd_init, cmd_inspect_run, build_parser, main
|
|
347
|
+
isinstance(cmd_plan, object)
|
|
348
|
+
```
|
|
349
|
+
True
|
|
350
|
+
"""
|
|
351
|
+
result = Workflow(args.manifest).plan()
|
|
352
|
+
|
|
353
|
+
if not result["valid"]:
|
|
354
|
+
validation = result["validation"]
|
|
355
|
+
print(_format_validation_report(validation))
|
|
356
|
+
return 1
|
|
357
|
+
|
|
358
|
+
print(result)
|
|
359
|
+
|
|
360
|
+
return 0
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def cmd_run(args: argparse.Namespace) -> int:
|
|
364
|
+
"""Run the workflow from manifest resolution through task execution.
|
|
365
|
+
|
|
366
|
+
The run command performs the complete workflow lifecycle: it resolves the
|
|
367
|
+
manifest, validates it, prepares runtimes, executes selected tasks, and
|
|
368
|
+
writes an HTML run report into the run directory.
|
|
369
|
+
|
|
370
|
+
Examples
|
|
371
|
+
--------
|
|
372
|
+
```{python}
|
|
373
|
+
from orcca.cli import cmd_plan, cmd_run, cmd_env_cache, cmd_init, cmd_inspect_run, build_parser, main
|
|
374
|
+
isinstance(cmd_run, object)
|
|
375
|
+
```
|
|
376
|
+
True
|
|
377
|
+
"""
|
|
378
|
+
_log("Starting workflow run")
|
|
379
|
+
result = Workflow(args.manifest, workspace_root=args.project_root, runs_root=Path(args.project_root) / ".orcca" / "runs").run(
|
|
380
|
+
run_dir=args.run_dir,
|
|
381
|
+
execute=not args.no_execute,
|
|
382
|
+
workers=args.workers,
|
|
383
|
+
task_ids=args.task_ids,
|
|
384
|
+
artifact_ids=args.artifact_ids,
|
|
385
|
+
run_validations=args.run_validations,
|
|
386
|
+
validation_ids=args.validation_ids,
|
|
387
|
+
use_env_cache=not args.no_env_cache,
|
|
388
|
+
refresh_env=args.refresh_env,
|
|
389
|
+
progress=_log,
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
status = "SUCCESS" if result.succeeded else "FAILED"
|
|
393
|
+
print(f"Run status: {status}")
|
|
394
|
+
print(f"Run ID: {result.run_id}")
|
|
395
|
+
print(f"Run directory: {result.run_dir}")
|
|
396
|
+
print(f"Run report: {result.run_report_path}")
|
|
397
|
+
if args.run_validations:
|
|
398
|
+
if result.validation_execution is None:
|
|
399
|
+
print("Validation task run: no validation tasks executed")
|
|
400
|
+
else:
|
|
401
|
+
print(f"Validation task run: {'SUCCESS' if result.validation_execution.succeeded else 'FAILED'}")
|
|
402
|
+
|
|
403
|
+
return 0 if result.succeeded else 1
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def cmd_dag(args: argparse.Namespace) -> int:
|
|
407
|
+
"""Export the workflow artifact dependency graph as HTML.
|
|
408
|
+
|
|
409
|
+
Use this command to visualize how generated artifacts depend on one
|
|
410
|
+
another. You can export the full graph or focus on the upstream lineage of
|
|
411
|
+
a single artifact, which is especially helpful when debugging complex
|
|
412
|
+
workflows.
|
|
413
|
+
|
|
414
|
+
Examples
|
|
415
|
+
--------
|
|
416
|
+
```{python}
|
|
417
|
+
from orcca.cli import cmd_dag, build_parser
|
|
418
|
+
isinstance(cmd_dag, object)
|
|
419
|
+
```
|
|
420
|
+
True
|
|
421
|
+
"""
|
|
422
|
+
result = dag(
|
|
423
|
+
args.manifest,
|
|
424
|
+
project_root=args.project_root,
|
|
425
|
+
output_path=args.output_path,
|
|
426
|
+
artifact_id=args.artifact_id,
|
|
427
|
+
)
|
|
428
|
+
print(f"html_path: {result['html_path']}")
|
|
429
|
+
print(f"artifact_id: {result['artifact_id']}")
|
|
430
|
+
print(f"node_count: {result['node_count']}")
|
|
431
|
+
print(f"edge_count: {result['edge_count']}")
|
|
432
|
+
return 0
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def _env_cache_root() -> Path:
|
|
436
|
+
return (Path(".") / ".orcca" / "cache" / "environments").resolve()
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def cmd_env_cache(args: argparse.Namespace) -> int:
|
|
440
|
+
"""Inspect or prune the cached package-manager environments.
|
|
441
|
+
|
|
442
|
+
This command is useful when you want to see which runtime environments are
|
|
443
|
+
already prepared, or when you need to clean out stale environments before
|
|
444
|
+
a rebuild or CI run.
|
|
445
|
+
|
|
446
|
+
Examples
|
|
447
|
+
--------
|
|
448
|
+
```{python}
|
|
449
|
+
from orcca.cli import cmd_plan, cmd_run, cmd_env_cache, cmd_init, cmd_inspect_run, build_parser, main
|
|
450
|
+
isinstance(cmd_env_cache, object)
|
|
451
|
+
```
|
|
452
|
+
True
|
|
453
|
+
"""
|
|
454
|
+
try:
|
|
455
|
+
result = env_cache(args.action, prune_all=args.all)
|
|
456
|
+
except ValueError as exc:
|
|
457
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
458
|
+
return 2
|
|
459
|
+
print(result)
|
|
460
|
+
return 0
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def cmd_init(args: argparse.Namespace) -> int:
|
|
464
|
+
"""Create a starter project template.
|
|
465
|
+
|
|
466
|
+
This command sets up the minimum project structure needed to get started
|
|
467
|
+
with Orcca, including a manifest, workflow directory, launcher file, and
|
|
468
|
+
default runtime wiring.
|
|
469
|
+
|
|
470
|
+
Examples
|
|
471
|
+
--------
|
|
472
|
+
```{python}
|
|
473
|
+
from orcca.cli import cmd_plan, cmd_run, cmd_env_cache, cmd_init, cmd_inspect_run, build_parser, main
|
|
474
|
+
isinstance(cmd_init, object)
|
|
475
|
+
```
|
|
476
|
+
True
|
|
477
|
+
"""
|
|
478
|
+
result = init(path=args.path)
|
|
479
|
+
print(result)
|
|
480
|
+
return 0
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
484
|
+
"""Build the Orcca top-level argument parser and subcommands.
|
|
485
|
+
|
|
486
|
+
Examples
|
|
487
|
+
--------
|
|
488
|
+
```{python}
|
|
489
|
+
from orcca.cli import cmd_plan, cmd_run, cmd_env_cache, cmd_init, cmd_inspect_run, build_parser, main
|
|
490
|
+
parser = build_parser()
|
|
491
|
+
parser.prog is not None
|
|
492
|
+
```
|
|
493
|
+
True
|
|
494
|
+
"""
|
|
495
|
+
parser = argparse.ArgumentParser(
|
|
496
|
+
description="Orcca CLI for validating, planning, and running workflow manifests.",
|
|
497
|
+
epilog="Example: orcca run manifests/workflow.yaml --project-root . --run-dir .orcca/runs/demo",
|
|
498
|
+
)
|
|
499
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
500
|
+
|
|
501
|
+
resolve_parser = subparsers.add_parser("resolve", help="Resolve a manifest and write the resolved YAML")
|
|
502
|
+
resolve_parser.add_argument("manifest", help="Path to root workflow manifest YAML")
|
|
503
|
+
resolve_parser.add_argument(
|
|
504
|
+
"--output-path",
|
|
505
|
+
default=".orcca/resolved_manifest.yaml",
|
|
506
|
+
help="YAML output path for the resolved manifest",
|
|
507
|
+
)
|
|
508
|
+
resolve_parser.set_defaults(func=cmd_resolve)
|
|
509
|
+
|
|
510
|
+
validate_parser = subparsers.add_parser("validate", help="Resolve and validate a workflow manifest")
|
|
511
|
+
validate_parser.add_argument("manifest", help="Path to root workflow manifest YAML")
|
|
512
|
+
validate_parser.add_argument(
|
|
513
|
+
"--report-path",
|
|
514
|
+
default=".orcca/validation/manifest_validation_report.html",
|
|
515
|
+
help="Path to write the manifest validation report HTML",
|
|
516
|
+
)
|
|
517
|
+
validate_parser.set_defaults(func=cmd_validate)
|
|
518
|
+
|
|
519
|
+
plan_parser = subparsers.add_parser("plan", help="Resolve, validate, and print execution planning details")
|
|
520
|
+
plan_parser.add_argument("manifest", help="Path to root workflow manifest YAML")
|
|
521
|
+
plan_parser.set_defaults(func=cmd_plan)
|
|
522
|
+
|
|
523
|
+
run_parser = subparsers.add_parser("run", help="Run end-to-end workflow pipeline")
|
|
524
|
+
run_parser.add_argument("manifest", help="Path to root workflow manifest YAML")
|
|
525
|
+
run_parser.add_argument(
|
|
526
|
+
"--project-root",
|
|
527
|
+
default=".",
|
|
528
|
+
help="Project root used for runtime execution and run artifacts (stored under .orcca/runs)",
|
|
529
|
+
)
|
|
530
|
+
run_parser.add_argument(
|
|
531
|
+
"--run-dir",
|
|
532
|
+
default=None,
|
|
533
|
+
help="Explicit run directory to write artifacts into instead of creating a timestamped subdirectory",
|
|
534
|
+
)
|
|
535
|
+
run_parser.add_argument("--workers", type=int, default=None, help="Override the number of concurrent workers")
|
|
536
|
+
run_parser.add_argument("--task", dest="task_ids", action="append", default=[], help="Target task id to execute; repeat to select multiple tasks")
|
|
537
|
+
run_parser.add_argument("--artifact", dest="artifact_ids", action="append", default=[], help="Target artifact id to generate; repeat to select multiple artifacts")
|
|
538
|
+
run_parser.add_argument(
|
|
539
|
+
"--run-validations",
|
|
540
|
+
action="store_true",
|
|
541
|
+
help="Execute validation tasks declared in validations[].validationTaskId",
|
|
542
|
+
)
|
|
543
|
+
run_parser.add_argument(
|
|
544
|
+
"--validation-id",
|
|
545
|
+
dest="validation_ids",
|
|
546
|
+
action="append",
|
|
547
|
+
default=[],
|
|
548
|
+
help="Validation id to execute (can be repeated). Requires --run-validations.",
|
|
549
|
+
)
|
|
550
|
+
run_parser.add_argument("--no-execute", action="store_true", help="Resolve and validate only; skip task execution")
|
|
551
|
+
run_parser.add_argument("--no-env-cache", action="store_true", help="Disable environment cache reuse for package-manager runtimes")
|
|
552
|
+
run_parser.add_argument("--refresh-env", action="store_true", help="Force rebuild or restore of package-manager environments")
|
|
553
|
+
run_parser.set_defaults(func=cmd_run)
|
|
554
|
+
|
|
555
|
+
dag_parser = subparsers.add_parser("dag", help="Export the artifact dependency graph as HTML")
|
|
556
|
+
dag_parser.add_argument("manifest", help="Path to root workflow manifest YAML")
|
|
557
|
+
dag_parser.add_argument(
|
|
558
|
+
"--project-root",
|
|
559
|
+
default=".",
|
|
560
|
+
help="Project root used to resolve the manifest and default graph output location",
|
|
561
|
+
)
|
|
562
|
+
dag_parser.add_argument(
|
|
563
|
+
"--artifact-id",
|
|
564
|
+
default=None,
|
|
565
|
+
help="Optional generated artifact id to export a focused upstream dependency graph",
|
|
566
|
+
)
|
|
567
|
+
dag_parser.add_argument(
|
|
568
|
+
"--output-path",
|
|
569
|
+
default=".orcca/graphs/artifact-dag.html",
|
|
570
|
+
help="HTML output path for the graph export",
|
|
571
|
+
)
|
|
572
|
+
dag_parser.set_defaults(func=cmd_dag)
|
|
573
|
+
|
|
574
|
+
env_cache_parser = subparsers.add_parser("env-cache", help="List or prune package-manager environment cache")
|
|
575
|
+
env_cache_parser.add_argument("action", choices=["list", "prune"], help="Cache action to perform")
|
|
576
|
+
env_cache_parser.add_argument("--all", action="store_true", help="When pruning, remove every cached environment")
|
|
577
|
+
env_cache_parser.set_defaults(func=cmd_env_cache)
|
|
578
|
+
|
|
579
|
+
init_parser = subparsers.add_parser("init", help="Scaffold a starter project")
|
|
580
|
+
init_parser.add_argument("path", nargs="?", default=".", help="Target project directory")
|
|
581
|
+
init_parser.set_defaults(func=cmd_init)
|
|
582
|
+
|
|
583
|
+
return parser
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def main(argv: list[str] | None = None) -> int:
|
|
587
|
+
"""CLI entrypoint for the `orcca` command.
|
|
588
|
+
|
|
589
|
+
Examples
|
|
590
|
+
--------
|
|
591
|
+
```{python}
|
|
592
|
+
from orcca.cli import cmd_plan, cmd_run, cmd_env_cache, cmd_init, cmd_inspect_run, build_parser, main
|
|
593
|
+
# Run help:
|
|
594
|
+
# main(["--help"])
|
|
595
|
+
isinstance(main, object)
|
|
596
|
+
```
|
|
597
|
+
True
|
|
598
|
+
"""
|
|
599
|
+
parser = build_parser()
|
|
600
|
+
effective_argv = argv if argv is not None else sys.argv[1:]
|
|
601
|
+
if not effective_argv:
|
|
602
|
+
parser.print_help()
|
|
603
|
+
return 0
|
|
604
|
+
args = parser.parse_args(effective_argv)
|
|
605
|
+
|
|
606
|
+
try:
|
|
607
|
+
_autoload_project_launchers(args)
|
|
608
|
+
return int(args.func(args))
|
|
609
|
+
except (CliBootstrapError, _ManifestResolutionError, _ManifestValidationError, _WorkflowRunError, _ExecutionError, _DagBuildError) as exc:
|
|
610
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
611
|
+
return 2
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
if __name__ == "__main__":
|
|
615
|
+
raise SystemExit(main())
|
|
616
|
+
|
|
617
|
+
|