yaml-workflow 0.1.2__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.
- yaml_workflow/__init__.py +5 -0
- yaml_workflow/__main__.py +4 -0
- yaml_workflow/cli.py +590 -0
- yaml_workflow/engine.py +559 -0
- yaml_workflow/examples/advanced_hello_world.yaml +184 -0
- yaml_workflow/examples/hello_world.yaml +39 -0
- yaml_workflow/examples/test_resume.yaml +54 -0
- yaml_workflow/exceptions.py +189 -0
- yaml_workflow/state.py +10 -0
- yaml_workflow/tasks/__init__.py +132 -0
- yaml_workflow/tasks/base.py +101 -0
- yaml_workflow/tasks/basic_tasks.py +86 -0
- yaml_workflow/tasks/batch_processor.py +428 -0
- yaml_workflow/tasks/file_tasks.py +556 -0
- yaml_workflow/tasks/file_utils.py +60 -0
- yaml_workflow/tasks/python_tasks.py +132 -0
- yaml_workflow/tasks/shell_tasks.py +144 -0
- yaml_workflow/tasks/template_tasks.py +53 -0
- yaml_workflow/workspace.py +358 -0
- yaml_workflow-0.1.2.dist-info/METADATA +125 -0
- yaml_workflow-0.1.2.dist-info/RECORD +24 -0
- yaml_workflow-0.1.2.dist-info/WHEEL +4 -0
- yaml_workflow-0.1.2.dist-info/entry_points.txt +2 -0
- yaml_workflow-0.1.2.dist-info/licenses/LICENSE +21 -0
yaml_workflow/cli.py
ADDED
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command-line interface for the workflow engine.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import importlib.resources
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import shutil
|
|
10
|
+
import sys
|
|
11
|
+
from datetime import datetime, timedelta
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Dict, List, Optional, Tuple
|
|
14
|
+
|
|
15
|
+
import yaml
|
|
16
|
+
|
|
17
|
+
from . import __version__ # Import version
|
|
18
|
+
from .engine import WorkflowEngine
|
|
19
|
+
from .exceptions import WorkflowError
|
|
20
|
+
from .workspace import get_workspace_info
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class WorkflowArgumentParser(argparse.ArgumentParser):
|
|
24
|
+
"""Custom argument parser that handles workflow parameters."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, *args, **kwargs):
|
|
27
|
+
super().__init__(*args, **kwargs)
|
|
28
|
+
self.workflow_params = []
|
|
29
|
+
|
|
30
|
+
def error(self, message):
|
|
31
|
+
"""Custom error handling for workflow parameters."""
|
|
32
|
+
if "unrecognized arguments" in message:
|
|
33
|
+
# Check if the unrecognized argument is a parameter
|
|
34
|
+
args = message.split(": ")[-1].split()
|
|
35
|
+
for arg in args:
|
|
36
|
+
# Skip standard flags like --version, --help
|
|
37
|
+
if arg in ["--version", "--help"]:
|
|
38
|
+
super().error(message)
|
|
39
|
+
return
|
|
40
|
+
if "=" in arg:
|
|
41
|
+
self.workflow_params.append(arg)
|
|
42
|
+
else:
|
|
43
|
+
# If it's not a parameter, raise an error
|
|
44
|
+
print(
|
|
45
|
+
f"Invalid parameter format: {arg}\nParameters must be in the format: name=value",
|
|
46
|
+
file=sys.stderr,
|
|
47
|
+
)
|
|
48
|
+
sys.exit(1)
|
|
49
|
+
else:
|
|
50
|
+
super().error(message)
|
|
51
|
+
|
|
52
|
+
def parse_args(self, args=None, namespace=None):
|
|
53
|
+
"""Parse arguments and collect workflow parameters."""
|
|
54
|
+
self.workflow_params = []
|
|
55
|
+
args = super().parse_args(args, namespace)
|
|
56
|
+
if hasattr(args, "params"):
|
|
57
|
+
args.params.extend(self.workflow_params)
|
|
58
|
+
return args
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def parse_params(args_list: List[str]) -> Dict[str, str]:
|
|
62
|
+
"""Parse command line parameters."""
|
|
63
|
+
result = {}
|
|
64
|
+
for arg in args_list:
|
|
65
|
+
try:
|
|
66
|
+
name, value = arg.split("=", 1)
|
|
67
|
+
# Remove leading '--' if present
|
|
68
|
+
name = name.lstrip("-")
|
|
69
|
+
result[name.strip()] = value.strip()
|
|
70
|
+
except ValueError:
|
|
71
|
+
raise ValueError(
|
|
72
|
+
f"Invalid parameter format: {arg}\nParameters must be in the format: name=value"
|
|
73
|
+
)
|
|
74
|
+
return result
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def run_workflow(args):
|
|
78
|
+
"""Run a workflow."""
|
|
79
|
+
try:
|
|
80
|
+
try:
|
|
81
|
+
param_dict = parse_params(args.params)
|
|
82
|
+
except ValueError as e:
|
|
83
|
+
print(str(e), file=sys.stderr)
|
|
84
|
+
sys.exit(1)
|
|
85
|
+
|
|
86
|
+
# If resuming, check the existing workspace first
|
|
87
|
+
if args.resume and args.workspace:
|
|
88
|
+
workspace_path = Path(args.workspace)
|
|
89
|
+
if workspace_path.exists():
|
|
90
|
+
metadata_path = workspace_path / ".workflow_metadata.json"
|
|
91
|
+
if metadata_path.exists():
|
|
92
|
+
try:
|
|
93
|
+
with open(metadata_path) as f:
|
|
94
|
+
metadata = json.load(f)
|
|
95
|
+
except json.JSONDecodeError as e:
|
|
96
|
+
raise ValueError(
|
|
97
|
+
f"Cannot resume: Invalid metadata file format - {str(e)}"
|
|
98
|
+
)
|
|
99
|
+
except Exception as e:
|
|
100
|
+
raise ValueError(
|
|
101
|
+
f"Cannot resume: Failed to read metadata file - {str(e)}"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if metadata.get("execution_state", {}).get("status") == "failed":
|
|
105
|
+
failed_step = metadata["execution_state"].get("failed_step")
|
|
106
|
+
if failed_step:
|
|
107
|
+
print(
|
|
108
|
+
f"Found failed workflow state, resuming from step: {failed_step['step_name']}"
|
|
109
|
+
)
|
|
110
|
+
else:
|
|
111
|
+
raise ValueError("No failed step found to resume from.")
|
|
112
|
+
else:
|
|
113
|
+
raise ValueError(
|
|
114
|
+
"Cannot resume: workflow is not in failed state"
|
|
115
|
+
)
|
|
116
|
+
else:
|
|
117
|
+
raise ValueError("Cannot resume: No workflow metadata found")
|
|
118
|
+
else:
|
|
119
|
+
raise ValueError("Cannot resume: Workspace directory not found")
|
|
120
|
+
|
|
121
|
+
# Create workflow engine
|
|
122
|
+
engine = WorkflowEngine(
|
|
123
|
+
workflow=args.workflow, workspace=args.workspace, base_dir=args.base_dir
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Parse skip steps
|
|
127
|
+
skip_step_list = []
|
|
128
|
+
if args.skip_steps:
|
|
129
|
+
skip_step_list = [step.strip() for step in args.skip_steps.split(",")]
|
|
130
|
+
print(f"Skipping steps: {', '.join(skip_step_list)}")
|
|
131
|
+
|
|
132
|
+
# Handle start-from and resume logic
|
|
133
|
+
start_from_step = None
|
|
134
|
+
resume_from = None
|
|
135
|
+
|
|
136
|
+
# Check start-from first (takes precedence)
|
|
137
|
+
if args.start_from:
|
|
138
|
+
start_from_step = args.start_from
|
|
139
|
+
print(f"Starting workflow from step: {start_from_step}")
|
|
140
|
+
# Check resume flag - only if workflow is in failed state
|
|
141
|
+
elif args.resume:
|
|
142
|
+
state = engine.state
|
|
143
|
+
if state.metadata["execution_state"]["status"] == "failed":
|
|
144
|
+
failed_step = state.metadata["execution_state"]["failed_step"]
|
|
145
|
+
if failed_step:
|
|
146
|
+
resume_from = failed_step["step_name"]
|
|
147
|
+
print(f"Resuming workflow from failed step: {resume_from}")
|
|
148
|
+
else:
|
|
149
|
+
raise ValueError("No failed step found to resume from.")
|
|
150
|
+
else:
|
|
151
|
+
raise ValueError("Cannot resume: workflow is not in failed state")
|
|
152
|
+
|
|
153
|
+
# Run workflow with appropriate parameters
|
|
154
|
+
results = engine.run(
|
|
155
|
+
param_dict,
|
|
156
|
+
resume_from=resume_from,
|
|
157
|
+
start_from=start_from_step,
|
|
158
|
+
skip_steps=skip_step_list,
|
|
159
|
+
flow=args.flow,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Print completion status
|
|
163
|
+
print("\n=== Workflow Status ===")
|
|
164
|
+
if resume_from:
|
|
165
|
+
print(f"✓ Workflow resumed from '{resume_from}' and completed successfully")
|
|
166
|
+
elif start_from_step:
|
|
167
|
+
print(
|
|
168
|
+
f"✓ Workflow started from '{start_from_step}' and completed successfully"
|
|
169
|
+
)
|
|
170
|
+
else:
|
|
171
|
+
print("✓ Workflow completed successfully")
|
|
172
|
+
|
|
173
|
+
if skip_step_list:
|
|
174
|
+
print(f"• Skipped steps: {', '.join(skip_step_list)}")
|
|
175
|
+
if args.flow:
|
|
176
|
+
print(f"• Flow executed: {args.flow}")
|
|
177
|
+
|
|
178
|
+
# Print step outputs in a clean format
|
|
179
|
+
if results.get("outputs"):
|
|
180
|
+
print("\n=== Step Outputs ===")
|
|
181
|
+
for step_name, output in results["outputs"].items():
|
|
182
|
+
# Skip empty outputs or None values
|
|
183
|
+
if output is None or (isinstance(output, str) and not output.strip()):
|
|
184
|
+
continue
|
|
185
|
+
print(f"\n• {step_name}:")
|
|
186
|
+
if isinstance(output, (dict, list)):
|
|
187
|
+
formatted_output = json.dumps(output, indent=2)
|
|
188
|
+
print(" " + formatted_output.replace("\n", "\n "))
|
|
189
|
+
else:
|
|
190
|
+
print(" " + str(output).replace("\n", "\n "))
|
|
191
|
+
|
|
192
|
+
print("\n=== Workspace Info ===")
|
|
193
|
+
print(f"• Location: {engine.workspace}")
|
|
194
|
+
# Get run number from the workspace metadata
|
|
195
|
+
run_number = engine.state.metadata.get("run_number", "unknown")
|
|
196
|
+
print(f"• Run number: {run_number}")
|
|
197
|
+
|
|
198
|
+
except WorkflowError as e:
|
|
199
|
+
print(f"Workflow error: {str(e)}", file=sys.stderr)
|
|
200
|
+
sys.exit(1)
|
|
201
|
+
except Exception as e:
|
|
202
|
+
print(f"Error: {str(e)}", file=sys.stderr)
|
|
203
|
+
sys.exit(1)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def list_workflows(args):
|
|
207
|
+
"""List available workflows."""
|
|
208
|
+
workflow_dir = Path(args.base_dir)
|
|
209
|
+
if not workflow_dir.exists():
|
|
210
|
+
print(f"Directory not found: {workflow_dir}", file=sys.stderr)
|
|
211
|
+
sys.exit(1)
|
|
212
|
+
|
|
213
|
+
print("\nAvailable workflows:")
|
|
214
|
+
# Recursively find all .yaml files
|
|
215
|
+
found = False
|
|
216
|
+
for workflow in sorted(workflow_dir.rglob("*.yaml")):
|
|
217
|
+
try:
|
|
218
|
+
# Try to load the file to verify it's a valid workflow
|
|
219
|
+
with open(workflow) as f:
|
|
220
|
+
content = yaml.safe_load(f)
|
|
221
|
+
|
|
222
|
+
# Handle both top-level workflow and direct steps format
|
|
223
|
+
if isinstance(content, dict):
|
|
224
|
+
if "workflow" in content:
|
|
225
|
+
content = content["workflow"]
|
|
226
|
+
|
|
227
|
+
# Check if it's a valid workflow file
|
|
228
|
+
if "steps" in content:
|
|
229
|
+
name = content.get("usage", {}).get("name") or workflow.stem
|
|
230
|
+
desc = content.get("usage", {}).get(
|
|
231
|
+
"description", "No description available"
|
|
232
|
+
)
|
|
233
|
+
print(f"\n- {workflow.relative_to(workflow_dir)}")
|
|
234
|
+
print(f" Name: {name}")
|
|
235
|
+
print(f" Description: {desc}")
|
|
236
|
+
found = True
|
|
237
|
+
|
|
238
|
+
except Exception:
|
|
239
|
+
# Skip files that can't be parsed as YAML
|
|
240
|
+
continue
|
|
241
|
+
|
|
242
|
+
if not found:
|
|
243
|
+
print(
|
|
244
|
+
"No workflow files found. Workflows should be YAML files containing 'steps' section."
|
|
245
|
+
)
|
|
246
|
+
print(
|
|
247
|
+
f"\nMake sure you have workflow YAML files in the '{workflow_dir}' directory."
|
|
248
|
+
)
|
|
249
|
+
print("You can specify a different directory with --base-dir option.")
|
|
250
|
+
print()
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def validate_workflow(args):
|
|
254
|
+
"""Validate a workflow file."""
|
|
255
|
+
try:
|
|
256
|
+
# Just try to create the engine, which will validate the workflow
|
|
257
|
+
WorkflowEngine(args.workflow)
|
|
258
|
+
print("Workflow validation successful")
|
|
259
|
+
except Exception as e:
|
|
260
|
+
print(f"Validation failed: {e}", file=sys.stderr)
|
|
261
|
+
sys.exit(1)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def list_workspaces(args):
|
|
265
|
+
"""List workflow run directories."""
|
|
266
|
+
base_dir_path = Path(args.base_dir)
|
|
267
|
+
if not base_dir_path.exists():
|
|
268
|
+
print(f"Base directory not found: {base_dir_path}", file=sys.stderr)
|
|
269
|
+
sys.exit(1)
|
|
270
|
+
|
|
271
|
+
# Get all run directories
|
|
272
|
+
runs = []
|
|
273
|
+
pattern = f"*_run_*" if not args.workflow else f"{args.workflow}_run_*"
|
|
274
|
+
|
|
275
|
+
for run_dir in base_dir_path.glob(pattern):
|
|
276
|
+
if run_dir.is_dir():
|
|
277
|
+
try:
|
|
278
|
+
info = get_workspace_info(run_dir)
|
|
279
|
+
runs.append(
|
|
280
|
+
{
|
|
281
|
+
"name": run_dir.name,
|
|
282
|
+
"created": datetime.fromisoformat(info["created_at"]),
|
|
283
|
+
"size": info["size"],
|
|
284
|
+
"files": info["files"],
|
|
285
|
+
}
|
|
286
|
+
)
|
|
287
|
+
except Exception as e:
|
|
288
|
+
print(f"Warning: Could not get info for {run_dir}: {e}")
|
|
289
|
+
|
|
290
|
+
# Sort by creation time
|
|
291
|
+
runs.sort(key=lambda x: x["created"], reverse=True)
|
|
292
|
+
|
|
293
|
+
if not runs:
|
|
294
|
+
print("No workflow runs found.")
|
|
295
|
+
return
|
|
296
|
+
|
|
297
|
+
print("\nWorkflow runs:")
|
|
298
|
+
for run in runs:
|
|
299
|
+
size_mb = run["size"] / (1024 * 1024)
|
|
300
|
+
age = datetime.now() - run["created"]
|
|
301
|
+
print(f"- {run['name']}")
|
|
302
|
+
print(f" Created: {run['created'].isoformat()} ({age.days} days ago)")
|
|
303
|
+
print(f" Size: {size_mb:.1f} MB")
|
|
304
|
+
print(f" Files: {run['files']}")
|
|
305
|
+
print()
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def clean_workspaces(args):
|
|
309
|
+
"""Clean up old workflow runs."""
|
|
310
|
+
base_dir_path = Path(args.base_dir)
|
|
311
|
+
if not base_dir_path.exists():
|
|
312
|
+
print(f"Base directory not found: {base_dir_path}", file=sys.stderr)
|
|
313
|
+
sys.exit(1)
|
|
314
|
+
|
|
315
|
+
cutoff = datetime.now() - timedelta(days=args.older_than)
|
|
316
|
+
pattern = f"*_run_*" if not args.workflow else f"{args.workflow}_run_*"
|
|
317
|
+
|
|
318
|
+
to_delete = []
|
|
319
|
+
for run_dir in base_dir_path.glob(pattern):
|
|
320
|
+
if run_dir.is_dir():
|
|
321
|
+
try:
|
|
322
|
+
info = get_workspace_info(run_dir)
|
|
323
|
+
created = datetime.fromisoformat(info["created_at"])
|
|
324
|
+
if created < cutoff:
|
|
325
|
+
to_delete.append((run_dir, info))
|
|
326
|
+
except Exception as e:
|
|
327
|
+
print(f"Warning: Could not process {run_dir}: {e}")
|
|
328
|
+
|
|
329
|
+
if not to_delete:
|
|
330
|
+
print("No old workflow runs to clean up.")
|
|
331
|
+
return
|
|
332
|
+
|
|
333
|
+
print("\nWorkflow runs to remove:")
|
|
334
|
+
total_size = 0
|
|
335
|
+
for run_dir, info in to_delete:
|
|
336
|
+
size_mb = info["size"] / (1024 * 1024)
|
|
337
|
+
total_size += info["size"]
|
|
338
|
+
age = datetime.now() - datetime.fromisoformat(info["created_at"])
|
|
339
|
+
print(f"- {run_dir.name}")
|
|
340
|
+
print(f" Age: {age.days} days")
|
|
341
|
+
print(f" Size: {size_mb:.1f} MB")
|
|
342
|
+
|
|
343
|
+
total_size_mb = total_size / (1024 * 1024)
|
|
344
|
+
print(f"\nTotal space to be freed: {total_size_mb:.1f} MB")
|
|
345
|
+
|
|
346
|
+
if not args.dry_run:
|
|
347
|
+
for run_dir, _ in to_delete:
|
|
348
|
+
try:
|
|
349
|
+
shutil.rmtree(run_dir)
|
|
350
|
+
print(f"Removed: {run_dir}")
|
|
351
|
+
except Exception as e:
|
|
352
|
+
print(f"Error removing {run_dir}: {e}")
|
|
353
|
+
else:
|
|
354
|
+
print("\nDry run - no files were deleted")
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def remove_workspaces(args):
|
|
358
|
+
"""Remove specific workflow runs."""
|
|
359
|
+
base_dir_path = Path(args.base_dir)
|
|
360
|
+
if not base_dir_path.exists():
|
|
361
|
+
print(f"Base directory not found: {base_dir_path}", file=sys.stderr)
|
|
362
|
+
sys.exit(1)
|
|
363
|
+
|
|
364
|
+
to_remove = []
|
|
365
|
+
for run_name in args.runs:
|
|
366
|
+
run_dir = base_dir_path / run_name
|
|
367
|
+
if not run_dir.exists():
|
|
368
|
+
print(f"Warning: Run directory not found: {run_dir}")
|
|
369
|
+
continue
|
|
370
|
+
if not run_dir.is_dir():
|
|
371
|
+
print(f"Warning: Not a directory: {run_dir}")
|
|
372
|
+
continue
|
|
373
|
+
to_remove.append(run_dir)
|
|
374
|
+
|
|
375
|
+
if not to_remove:
|
|
376
|
+
print("No valid run directories to remove.")
|
|
377
|
+
return
|
|
378
|
+
|
|
379
|
+
print("\nWorkflow runs to remove:")
|
|
380
|
+
total_size = 0
|
|
381
|
+
for run_dir in to_remove:
|
|
382
|
+
try:
|
|
383
|
+
info = get_workspace_info(run_dir)
|
|
384
|
+
size_mb = info["size"] / (1024 * 1024)
|
|
385
|
+
total_size += info["size"]
|
|
386
|
+
print(f"- {run_dir.name}")
|
|
387
|
+
print(f" Size: {size_mb:.1f} MB")
|
|
388
|
+
print(f" Files: {info['files']}")
|
|
389
|
+
except Exception as e:
|
|
390
|
+
print(f"Warning: Could not get info for {run_dir}: {e}")
|
|
391
|
+
|
|
392
|
+
total_size_mb = total_size / (1024 * 1024)
|
|
393
|
+
print(f"\nTotal space to be freed: {total_size_mb:.1f} MB")
|
|
394
|
+
|
|
395
|
+
if not args.force:
|
|
396
|
+
response = input("\nAre you sure you want to remove these runs? [y/N] ")
|
|
397
|
+
if response.lower() != "y":
|
|
398
|
+
print("Operation cancelled.")
|
|
399
|
+
return
|
|
400
|
+
|
|
401
|
+
for run_dir in to_remove:
|
|
402
|
+
try:
|
|
403
|
+
shutil.rmtree(run_dir)
|
|
404
|
+
print(f"Removed: {run_dir}")
|
|
405
|
+
except Exception as e:
|
|
406
|
+
print(f"Error removing {run_dir}: {e}")
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def init_project(args):
|
|
410
|
+
"""Initialize a new project with example workflows."""
|
|
411
|
+
try:
|
|
412
|
+
# Create target directory if it doesn't exist
|
|
413
|
+
target_dir = Path(args.dir)
|
|
414
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
415
|
+
|
|
416
|
+
# Get examples directory from package
|
|
417
|
+
examples_dir = Path(__file__).parent / "examples"
|
|
418
|
+
|
|
419
|
+
if args.example:
|
|
420
|
+
# Copy specific example
|
|
421
|
+
example_file = examples_dir / f"{args.example}.yaml"
|
|
422
|
+
if not example_file.exists():
|
|
423
|
+
print(f"Example '{args.example}' not found", file=sys.stderr)
|
|
424
|
+
sys.exit(1)
|
|
425
|
+
shutil.copy2(example_file, target_dir)
|
|
426
|
+
print(f"Initialized project with example: {args.example}")
|
|
427
|
+
else:
|
|
428
|
+
# Copy all examples
|
|
429
|
+
for example in examples_dir.glob("*.yaml"):
|
|
430
|
+
shutil.copy2(example, target_dir)
|
|
431
|
+
print(f"Initialized project with examples in: {target_dir}")
|
|
432
|
+
|
|
433
|
+
except Exception as e:
|
|
434
|
+
print(f"Error initializing project: {e}", file=sys.stderr)
|
|
435
|
+
sys.exit(1)
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def main():
|
|
439
|
+
"""Main entry point for the CLI."""
|
|
440
|
+
parser = WorkflowArgumentParser(description="YAML Workflow Engine CLI")
|
|
441
|
+
parser.formatter_class = argparse.RawDescriptionHelpFormatter
|
|
442
|
+
parser.description = f"""YAML Workflow Engine CLI v{__version__}
|
|
443
|
+
|
|
444
|
+
Commands:
|
|
445
|
+
run Run a workflow
|
|
446
|
+
list List available workflows
|
|
447
|
+
validate Validate a workflow file
|
|
448
|
+
workspace Workspace management commands
|
|
449
|
+
init Initialize a new project with example workflows
|
|
450
|
+
"""
|
|
451
|
+
parser.add_argument(
|
|
452
|
+
"--version",
|
|
453
|
+
action="version",
|
|
454
|
+
version=f"%(prog)s {__version__}",
|
|
455
|
+
help="Show program version and exit",
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
subparsers = parser.add_subparsers(dest="command", help="Commands")
|
|
459
|
+
|
|
460
|
+
# Run command
|
|
461
|
+
run_parser = subparsers.add_parser("run", help="Run a workflow", add_help=True)
|
|
462
|
+
run_parser.add_argument("workflow", help="Path to workflow file")
|
|
463
|
+
run_parser.add_argument("--workspace", help="Custom workspace directory")
|
|
464
|
+
run_parser.add_argument(
|
|
465
|
+
"--base-dir", default="runs", help="Base directory for workflow runs"
|
|
466
|
+
)
|
|
467
|
+
run_parser.add_argument(
|
|
468
|
+
"--resume", action="store_true", help="Resume workflow from last failed step"
|
|
469
|
+
)
|
|
470
|
+
run_parser.add_argument(
|
|
471
|
+
"--start-from", help="Start workflow execution from specified step"
|
|
472
|
+
)
|
|
473
|
+
run_parser.add_argument(
|
|
474
|
+
"--skip-steps", help="Comma-separated list of steps to skip during execution"
|
|
475
|
+
)
|
|
476
|
+
run_parser.add_argument(
|
|
477
|
+
"--flow",
|
|
478
|
+
help="Name of the flow to execute (default: use flow specified in workflow file)",
|
|
479
|
+
)
|
|
480
|
+
run_parser.add_argument(
|
|
481
|
+
"params", nargs="*", help="Parameters in the format name=value or --name=value"
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
# List command
|
|
485
|
+
list_parser = subparsers.add_parser("list", help="List available workflows")
|
|
486
|
+
list_parser.add_argument(
|
|
487
|
+
"--base-dir", default="workflows", help="Base directory containing workflows"
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
# Validate command
|
|
491
|
+
validate_parser = subparsers.add_parser("validate", help="Validate a workflow file")
|
|
492
|
+
validate_parser.add_argument("workflow", help="Path to workflow file")
|
|
493
|
+
|
|
494
|
+
# Workspace commands
|
|
495
|
+
workspace_parser = subparsers.add_parser(
|
|
496
|
+
"workspace", help="Workspace management commands"
|
|
497
|
+
)
|
|
498
|
+
workspace_subparsers = workspace_parser.add_subparsers(
|
|
499
|
+
dest="workspace_command", help="Workspace commands"
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
# Workspace list command
|
|
503
|
+
workspace_list_parser = workspace_subparsers.add_parser(
|
|
504
|
+
"list", help="List workflow run directories"
|
|
505
|
+
)
|
|
506
|
+
workspace_list_parser.add_argument(
|
|
507
|
+
"--base-dir", "-b", default="runs", help="Base directory for workflow runs"
|
|
508
|
+
)
|
|
509
|
+
workspace_list_parser.add_argument(
|
|
510
|
+
"--workflow", "-w", help="Filter by workflow name"
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
# Workspace clean command
|
|
514
|
+
workspace_clean_parser = workspace_subparsers.add_parser(
|
|
515
|
+
"clean", help="Clean up old workflow runs"
|
|
516
|
+
)
|
|
517
|
+
workspace_clean_parser.add_argument(
|
|
518
|
+
"--base-dir", "-b", default="runs", help="Base directory for workflow runs"
|
|
519
|
+
)
|
|
520
|
+
workspace_clean_parser.add_argument(
|
|
521
|
+
"--older-than", "-o", type=int, default=30, help="Remove runs older than N days"
|
|
522
|
+
)
|
|
523
|
+
workspace_clean_parser.add_argument(
|
|
524
|
+
"--workflow", "-w", help="Clean only runs of this workflow"
|
|
525
|
+
)
|
|
526
|
+
workspace_clean_parser.add_argument(
|
|
527
|
+
"--dry-run",
|
|
528
|
+
"-n",
|
|
529
|
+
action="store_true",
|
|
530
|
+
help="Show what would be deleted without actually deleting",
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
# Workspace remove command
|
|
534
|
+
workspace_remove_parser = workspace_subparsers.add_parser(
|
|
535
|
+
"remove", help="Remove specific workflow runs"
|
|
536
|
+
)
|
|
537
|
+
workspace_remove_parser.add_argument(
|
|
538
|
+
"runs", nargs="+", help="Names of runs to remove"
|
|
539
|
+
)
|
|
540
|
+
workspace_remove_parser.add_argument(
|
|
541
|
+
"--base-dir", "-b", default="runs", help="Base directory for workflow runs"
|
|
542
|
+
)
|
|
543
|
+
workspace_remove_parser.add_argument(
|
|
544
|
+
"--force", "-f", action="store_true", help="Don't ask for confirmation"
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
# Init command
|
|
548
|
+
init_parser = subparsers.add_parser(
|
|
549
|
+
"init", help="Initialize a new project with example workflows"
|
|
550
|
+
)
|
|
551
|
+
init_parser.add_argument(
|
|
552
|
+
"--dir", default="workflows", help="Directory to create workflows in"
|
|
553
|
+
)
|
|
554
|
+
init_parser.add_argument("--example", help="Specific example workflow to copy")
|
|
555
|
+
|
|
556
|
+
args = parser.parse_args()
|
|
557
|
+
|
|
558
|
+
if not args.command:
|
|
559
|
+
parser.print_help()
|
|
560
|
+
sys.exit(1)
|
|
561
|
+
|
|
562
|
+
try:
|
|
563
|
+
if args.command == "run":
|
|
564
|
+
run_workflow(args)
|
|
565
|
+
elif args.command == "list":
|
|
566
|
+
list_workflows(args)
|
|
567
|
+
elif args.command == "validate":
|
|
568
|
+
validate_workflow(args)
|
|
569
|
+
elif args.command == "workspace":
|
|
570
|
+
if args.workspace_command == "list":
|
|
571
|
+
list_workspaces(args)
|
|
572
|
+
elif args.workspace_command == "clean":
|
|
573
|
+
clean_workspaces(args)
|
|
574
|
+
elif args.workspace_command == "remove":
|
|
575
|
+
remove_workspaces(args)
|
|
576
|
+
else:
|
|
577
|
+
workspace_parser.print_help()
|
|
578
|
+
sys.exit(1)
|
|
579
|
+
elif args.command == "init":
|
|
580
|
+
init_project(args)
|
|
581
|
+
except KeyboardInterrupt:
|
|
582
|
+
print("\nOperation cancelled by user", file=sys.stderr)
|
|
583
|
+
sys.exit(1)
|
|
584
|
+
except Exception as e:
|
|
585
|
+
print(f"Error: {str(e)}", file=sys.stderr)
|
|
586
|
+
sys.exit(1)
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
if __name__ == "__main__":
|
|
590
|
+
main()
|