hexaqueue-cli 0.1.0__tar.gz → 0.2.0__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.
- {hexaqueue_cli-0.1.0 → hexaqueue_cli-0.2.0}/PKG-INFO +6 -5
- {hexaqueue_cli-0.1.0 → hexaqueue_cli-0.2.0}/pyproject.toml +9 -5
- {hexaqueue_cli-0.1.0 → hexaqueue_cli-0.2.0}/pyproject.toml.orig +7 -5
- hexaqueue_cli-0.2.0/src/hexaqueue_cli/commands/__init__.py +13 -0
- hexaqueue_cli-0.2.0/src/hexaqueue_cli/commands/workflow.py +355 -0
- {hexaqueue_cli-0.1.0 → hexaqueue_cli-0.2.0}/src/hexaqueue_cli/main.py +2 -0
- hexaqueue_cli-0.1.0/src/hexaqueue_cli/commands/__init__.py +0 -7
- {hexaqueue_cli-0.1.0 → hexaqueue_cli-0.2.0}/README.md +0 -0
- {hexaqueue_cli-0.1.0 → hexaqueue_cli-0.2.0}/src/hexaqueue_cli/__init__.py +0 -0
- {hexaqueue_cli-0.1.0 → hexaqueue_cli-0.2.0}/src/hexaqueue_cli/adapters/__init__.py +0 -0
- {hexaqueue_cli-0.1.0 → hexaqueue_cli-0.2.0}/src/hexaqueue_cli/adapters/local.py +0 -0
- {hexaqueue_cli-0.1.0 → hexaqueue_cli-0.2.0}/src/hexaqueue_cli/commands/run.py +0 -0
- {hexaqueue_cli-0.1.0 → hexaqueue_cli-0.2.0}/src/hexaqueue_cli/domain/__init__.py +0 -0
- {hexaqueue_cli-0.1.0 → hexaqueue_cli-0.2.0}/src/hexaqueue_cli/domain/parser.py +0 -0
- {hexaqueue_cli-0.1.0 → hexaqueue_cli-0.2.0}/src/hexaqueue_cli/domain/session.py +0 -0
- {hexaqueue_cli-0.1.0 → hexaqueue_cli-0.2.0}/src/hexaqueue_cli/infra/__init__.py +0 -0
- {hexaqueue_cli-0.1.0 → hexaqueue_cli-0.2.0}/src/hexaqueue_cli/infra/bootstrap.py +0 -0
- {hexaqueue_cli-0.1.0 → hexaqueue_cli-0.2.0}/src/hexaqueue_cli/ports/__init__.py +0 -0
- {hexaqueue_cli-0.1.0 → hexaqueue_cli-0.2.0}/src/hexaqueue_cli/ports/client.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hexaqueue-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Hexaqueue CLI - Modern developer and operator terminal interface
|
|
5
5
|
Author: Richard West
|
|
6
6
|
Author-email: Richard West <dopplereffect.us@gmail.com>
|
|
@@ -8,10 +8,11 @@ License-Expression: Apache-2.0
|
|
|
8
8
|
Requires-Dist: hexaqueue-core
|
|
9
9
|
Requires-Dist: hexaqueue-server
|
|
10
10
|
Requires-Dist: hexaqueue-worker
|
|
11
|
-
Requires-Dist:
|
|
12
|
-
Requires-Dist: hexastack-
|
|
13
|
-
Requires-Dist: hexastack-
|
|
14
|
-
Requires-Dist: hexastack-
|
|
11
|
+
Requires-Dist: hexaqueue-workflow
|
|
12
|
+
Requires-Dist: hexastack-cli>=0.5.0
|
|
13
|
+
Requires-Dist: hexastack-core>=0.5.0
|
|
14
|
+
Requires-Dist: hexastack-cqrs>=0.5.0
|
|
15
|
+
Requires-Dist: hexastack-grpc>=0.5.0
|
|
15
16
|
Requires-Dist: pyyaml>=6.0.2
|
|
16
17
|
Requires-Dist: rich>=13.8.0
|
|
17
18
|
Requires-Dist: typer>=0.15.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "hexaqueue-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0"
|
|
4
4
|
description = "Hexaqueue CLI - Modern developer and operator terminal interface"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -9,10 +9,11 @@ dependencies = [
|
|
|
9
9
|
"hexaqueue-core",
|
|
10
10
|
"hexaqueue-server",
|
|
11
11
|
"hexaqueue-worker",
|
|
12
|
-
"
|
|
13
|
-
"hexastack-
|
|
14
|
-
"hexastack-
|
|
15
|
-
"hexastack-
|
|
12
|
+
"hexaqueue-workflow",
|
|
13
|
+
"hexastack-cli>=0.5.0",
|
|
14
|
+
"hexastack-core>=0.5.0",
|
|
15
|
+
"hexastack-cqrs>=0.5.0",
|
|
16
|
+
"hexastack-grpc>=0.5.0",
|
|
16
17
|
"pyyaml>=6.0.2",
|
|
17
18
|
"rich>=13.8.0",
|
|
18
19
|
"typer>=0.15.0",
|
|
@@ -37,3 +38,6 @@ workspace = true
|
|
|
37
38
|
|
|
38
39
|
[tool.uv.sources.hexaqueue-worker]
|
|
39
40
|
workspace = true
|
|
41
|
+
|
|
42
|
+
[tool.uv.sources.hexaqueue-workflow]
|
|
43
|
+
workspace = true
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "hexaqueue-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0"
|
|
4
4
|
description = "Hexaqueue CLI - Modern developer and operator terminal interface"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -12,10 +12,11 @@ dependencies = [
|
|
|
12
12
|
"hexaqueue-core",
|
|
13
13
|
"hexaqueue-server",
|
|
14
14
|
"hexaqueue-worker",
|
|
15
|
-
"
|
|
16
|
-
"hexastack-
|
|
17
|
-
"hexastack-
|
|
18
|
-
"hexastack-
|
|
15
|
+
"hexaqueue-workflow",
|
|
16
|
+
"hexastack-cli>=0.5.0",
|
|
17
|
+
"hexastack-core>=0.5.0",
|
|
18
|
+
"hexastack-cqrs>=0.5.0",
|
|
19
|
+
"hexastack-grpc>=0.5.0",
|
|
19
20
|
"pyyaml>=6.0.2",
|
|
20
21
|
"rich>=13.8.0",
|
|
21
22
|
"typer>=0.15.0",
|
|
@@ -32,3 +33,4 @@ build-backend = "uv_build"
|
|
|
32
33
|
hexaqueue-core = { workspace = true }
|
|
33
34
|
hexaqueue-server = { workspace = true }
|
|
34
35
|
hexaqueue-worker = { workspace = true }
|
|
36
|
+
hexaqueue-workflow = { workspace = true }
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Commands package for hexaqueue-cli.
|
|
2
|
+
|
|
3
|
+
Notes/Architectural Intent:
|
|
4
|
+
Exposes Typer subcommand modules for batch run pipelines and distributed workflow execution.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from hexaqueue_cli.commands.run import app as run_app
|
|
8
|
+
from hexaqueue_cli.commands.workflow import app as workflow_app
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"run_app",
|
|
12
|
+
"workflow_app",
|
|
13
|
+
]
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
"""CLI commands for Hexaflow distributed workflow management in Hexaqueue.
|
|
2
|
+
|
|
3
|
+
Notes/Architectural Intent:
|
|
4
|
+
Provides subcommands for submitting workflow DAG definitions to the cluster,
|
|
5
|
+
inspecting step checkpoints and staged artifact URIs, resuming suspended workflows,
|
|
6
|
+
and aborting active workflows with reverse compensation unwinding.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import importlib.util
|
|
11
|
+
import json
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import typer
|
|
16
|
+
from hexaflow.adapters.storage.sqlite import SqliteStateStore
|
|
17
|
+
from hexaflow.domain.models import WorkflowDefinition
|
|
18
|
+
from hexaflow.domain.state import StepStatus, WorkflowStatus
|
|
19
|
+
from hexaflow.dsl.builder import Workflow
|
|
20
|
+
from rich.console import Console
|
|
21
|
+
from rich.panel import Panel
|
|
22
|
+
from rich.table import Table
|
|
23
|
+
|
|
24
|
+
from hexaqueue_workflow.adapters.engines.distributed import HexaqueueDistributedEngine
|
|
25
|
+
from hexaqueue_workflow.domain.models import ArtifactReference
|
|
26
|
+
|
|
27
|
+
app = typer.Typer(
|
|
28
|
+
name="workflow",
|
|
29
|
+
help="Hexaflow distributed workflow orchestration and cluster scheduling commands.",
|
|
30
|
+
no_args_is_help=True,
|
|
31
|
+
)
|
|
32
|
+
console = Console()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def load_workflow_from_target(target: str) -> WorkflowDefinition:
|
|
36
|
+
"""Dynamically load a Workflow or WorkflowDefinition from a file target.
|
|
37
|
+
|
|
38
|
+
Supports 'path/to/file.py:workflow_name' or 'path/to/file.py' looking for
|
|
39
|
+
the 'workflow' variable or an instance of Workflow/WorkflowDefinition.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
target: String path or module identifier (e.g. 'flows/pipeline.py:main_flow').
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Compiled WorkflowDefinition.
|
|
46
|
+
|
|
47
|
+
Raises:
|
|
48
|
+
typer.BadParameter: If file or workflow variable cannot be loaded.
|
|
49
|
+
|
|
50
|
+
Notes/Architectural Intent:
|
|
51
|
+
Enables seamless loading of user-defined DAG workflows without requiring
|
|
52
|
+
prior compilation or separate registration steps.
|
|
53
|
+
"""
|
|
54
|
+
if ":" in target:
|
|
55
|
+
file_part, attr_name = target.split(":", 1)
|
|
56
|
+
else:
|
|
57
|
+
file_part, attr_name = target, None
|
|
58
|
+
|
|
59
|
+
file_path = Path(file_part).resolve()
|
|
60
|
+
if not file_path.exists():
|
|
61
|
+
msg = f"Workflow file '{file_path}' does not exist"
|
|
62
|
+
raise typer.BadParameter(msg)
|
|
63
|
+
|
|
64
|
+
spec = importlib.util.spec_from_file_location("dynamic_workflow_module", file_path)
|
|
65
|
+
if not spec or not spec.loader:
|
|
66
|
+
msg = f"Cannot load Python module from '{file_path}'"
|
|
67
|
+
raise typer.BadParameter(msg)
|
|
68
|
+
|
|
69
|
+
module = importlib.util.module_from_spec(spec)
|
|
70
|
+
spec.loader.exec_module(module)
|
|
71
|
+
|
|
72
|
+
wf_candidate: Any = None
|
|
73
|
+
if attr_name:
|
|
74
|
+
if not hasattr(module, attr_name):
|
|
75
|
+
msg = f"Module '{file_path}' does not define attribute '{attr_name}'"
|
|
76
|
+
raise typer.BadParameter(msg)
|
|
77
|
+
wf_candidate = getattr(module, attr_name)
|
|
78
|
+
else:
|
|
79
|
+
if hasattr(module, "workflow"):
|
|
80
|
+
wf_candidate = module.workflow
|
|
81
|
+
else:
|
|
82
|
+
for item in vars(module).values():
|
|
83
|
+
if isinstance(item, (Workflow, WorkflowDefinition)):
|
|
84
|
+
wf_candidate = item
|
|
85
|
+
break
|
|
86
|
+
|
|
87
|
+
if wf_candidate is None:
|
|
88
|
+
msg = f"No Workflow or WorkflowDefinition instance found in '{file_path}'"
|
|
89
|
+
raise typer.BadParameter(msg)
|
|
90
|
+
|
|
91
|
+
if isinstance(wf_candidate, Workflow):
|
|
92
|
+
return wf_candidate.compile()
|
|
93
|
+
if isinstance(wf_candidate, WorkflowDefinition):
|
|
94
|
+
return wf_candidate
|
|
95
|
+
|
|
96
|
+
msg = f"Target object '{wf_candidate}' is not a Workflow or WorkflowDefinition"
|
|
97
|
+
raise typer.BadParameter(msg)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def parse_inputs_arg(inputs: str | None) -> dict[str, Any]:
|
|
101
|
+
"""Parse JSON string or key=val pairs into a dictionary.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
inputs: Raw CLI inputs argument string.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Dictionary of input arguments.
|
|
108
|
+
|
|
109
|
+
Raises:
|
|
110
|
+
typer.BadParameter: If JSON decoding fails.
|
|
111
|
+
"""
|
|
112
|
+
if not inputs:
|
|
113
|
+
return {}
|
|
114
|
+
try:
|
|
115
|
+
parsed = json.loads(inputs)
|
|
116
|
+
if isinstance(parsed, dict):
|
|
117
|
+
return parsed
|
|
118
|
+
msg = "Input JSON must be an object/dict"
|
|
119
|
+
raise ValueError(msg)
|
|
120
|
+
except Exception as e:
|
|
121
|
+
msg = f"Invalid JSON inputs string: {e}"
|
|
122
|
+
raise typer.BadParameter(msg) from e
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@app.command("submit")
|
|
126
|
+
def submit_cmd(
|
|
127
|
+
target: str = typer.Argument(
|
|
128
|
+
...,
|
|
129
|
+
help="Path to workflow file, e.g. 'pipeline.py' or 'pipeline.py:my_flow'",
|
|
130
|
+
),
|
|
131
|
+
inputs: str | None = typer.Option(
|
|
132
|
+
None, "--inputs", "-i", help="JSON dictionary of initial inputs"
|
|
133
|
+
),
|
|
134
|
+
db_path: str = typer.Option(
|
|
135
|
+
".hexaflow/state.db", "--db", help="Path to SQLite state database"
|
|
136
|
+
),
|
|
137
|
+
watch: bool = typer.Option(
|
|
138
|
+
False, "--watch", "-w", help="Watch workflow execution until completion"
|
|
139
|
+
),
|
|
140
|
+
) -> None:
|
|
141
|
+
"""Submit a workflow definition for distributed cluster execution.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
target: Target file path or specifier.
|
|
145
|
+
inputs: Optional JSON inputs string.
|
|
146
|
+
db_path: SQLite state store database path.
|
|
147
|
+
watch: Whether to block and watch until terminal completion.
|
|
148
|
+
"""
|
|
149
|
+
workflow = load_workflow_from_target(target)
|
|
150
|
+
initial_inputs = parse_inputs_arg(inputs)
|
|
151
|
+
|
|
152
|
+
store = SqliteStateStore(db_path=db_path)
|
|
153
|
+
engine = HexaqueueDistributedEngine(state_store=store)
|
|
154
|
+
|
|
155
|
+
console.print(
|
|
156
|
+
f"[bold green]Submitting workflow:[/] [cyan]{workflow.name}[/] "
|
|
157
|
+
f"({len(workflow.stages)} stages, {sum(len(s.steps) for s in workflow.stages)} steps)"
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
async def _execute() -> None:
|
|
161
|
+
state = await engine.run_async(workflow, initial_inputs=initial_inputs)
|
|
162
|
+
_render_workflow_state(state, store)
|
|
163
|
+
|
|
164
|
+
asyncio.run(_execute())
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@app.command("status")
|
|
168
|
+
def status_cmd(
|
|
169
|
+
run_id: str = typer.Argument(
|
|
170
|
+
..., help="Workflow execution run identifier to inspect"
|
|
171
|
+
),
|
|
172
|
+
db_path: str = typer.Option(
|
|
173
|
+
".hexaflow/state.db", "--db", help="Path to SQLite state database"
|
|
174
|
+
),
|
|
175
|
+
) -> None:
|
|
176
|
+
"""Inspect status, checkpoints, and staged artifacts for a workflow run.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
run_id: Run identifier to query.
|
|
180
|
+
db_path: SQLite state store database path.
|
|
181
|
+
"""
|
|
182
|
+
store = SqliteStateStore(db_path=db_path)
|
|
183
|
+
state = store.get_run(run_id)
|
|
184
|
+
|
|
185
|
+
if not state:
|
|
186
|
+
console.print(
|
|
187
|
+
f"[bold red]Error:[/] Workflow run '{run_id}' not found in database '{db_path}'."
|
|
188
|
+
)
|
|
189
|
+
raise typer.Exit(code=1)
|
|
190
|
+
|
|
191
|
+
_render_workflow_state(state, store)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@app.command("resume")
|
|
195
|
+
def resume_cmd(
|
|
196
|
+
run_id: str = typer.Argument(..., help="Suspended workflow run identifier"),
|
|
197
|
+
target: str = typer.Argument(
|
|
198
|
+
...,
|
|
199
|
+
help="Path to workflow file, e.g. 'pipeline.py' or 'pipeline.py:my_flow'",
|
|
200
|
+
),
|
|
201
|
+
inputs: str | None = typer.Option(
|
|
202
|
+
None, "--inputs", "-i", help="Patch inputs for resuming step frontier"
|
|
203
|
+
),
|
|
204
|
+
skip_steps: list[str] = typer.Option(
|
|
205
|
+
None, "--skip", help="Step names to explicitly skip"
|
|
206
|
+
),
|
|
207
|
+
db_path: str = typer.Option(
|
|
208
|
+
".hexaflow/state.db", "--db", help="Path to SQLite state database"
|
|
209
|
+
),
|
|
210
|
+
) -> None:
|
|
211
|
+
"""Resume execution of a suspended workflow run from its latest checkpoints.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
run_id: Suspended run identifier.
|
|
215
|
+
target: Target workflow file specifier.
|
|
216
|
+
inputs: Optional patch inputs.
|
|
217
|
+
skip_steps: Optional step names to skip.
|
|
218
|
+
db_path: SQLite state database path.
|
|
219
|
+
"""
|
|
220
|
+
workflow = load_workflow_from_target(target)
|
|
221
|
+
patch_inputs = parse_inputs_arg(inputs)
|
|
222
|
+
|
|
223
|
+
store = SqliteStateStore(db_path=db_path)
|
|
224
|
+
engine = HexaqueueDistributedEngine(state_store=store)
|
|
225
|
+
|
|
226
|
+
console.print(f"[bold yellow]Resuming workflow run:[/] [cyan]{run_id}[/]")
|
|
227
|
+
|
|
228
|
+
async def _resume() -> None:
|
|
229
|
+
state = await engine.resume_async(
|
|
230
|
+
run_id=run_id,
|
|
231
|
+
workflow=workflow,
|
|
232
|
+
patch_inputs=patch_inputs,
|
|
233
|
+
skip_steps=set(skip_steps or ()),
|
|
234
|
+
)
|
|
235
|
+
_render_workflow_state(state, store)
|
|
236
|
+
|
|
237
|
+
asyncio.run(_resume())
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@app.command("abort")
|
|
241
|
+
def abort_cmd(
|
|
242
|
+
run_id: str = typer.Argument(..., help="Workflow run identifier to abort"),
|
|
243
|
+
target: str = typer.Argument(..., help="Path to workflow file"),
|
|
244
|
+
db_path: str = typer.Option(
|
|
245
|
+
".hexaflow/state.db", "--db", help="Path to SQLite state database"
|
|
246
|
+
),
|
|
247
|
+
) -> None:
|
|
248
|
+
"""Abort an active or suspended workflow, unwinding step compensations in reverse order.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
run_id: Run identifier to abort.
|
|
252
|
+
target: Workflow file specifier containing compensations.
|
|
253
|
+
db_path: SQLite state database path.
|
|
254
|
+
"""
|
|
255
|
+
workflow = load_workflow_from_target(target)
|
|
256
|
+
store = SqliteStateStore(db_path=db_path)
|
|
257
|
+
engine = HexaqueueDistributedEngine(state_store=store)
|
|
258
|
+
|
|
259
|
+
console.print(f"[bold red]Aborting workflow run:[/] [cyan]{run_id}[/]")
|
|
260
|
+
|
|
261
|
+
async def _abort() -> None:
|
|
262
|
+
state = await engine.abort_async(run_id=run_id, workflow=workflow)
|
|
263
|
+
_render_workflow_state(state, store)
|
|
264
|
+
|
|
265
|
+
asyncio.run(_abort())
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _render_workflow_state(state: Any, store: SqliteStateStore) -> None:
|
|
269
|
+
"""Render Rich status summary panel and step checkpoint table."""
|
|
270
|
+
status_color = {
|
|
271
|
+
WorkflowStatus.COMPLETED: "bold green",
|
|
272
|
+
WorkflowStatus.RUNNING: "bold cyan",
|
|
273
|
+
WorkflowStatus.SUSPENDED: "bold yellow",
|
|
274
|
+
WorkflowStatus.CANCELLED: "bold magenta",
|
|
275
|
+
WorkflowStatus.PENDING: "bold white",
|
|
276
|
+
}.get(state.status, "white")
|
|
277
|
+
|
|
278
|
+
header_table = Table.grid(padding=(0, 2))
|
|
279
|
+
header_table.add_column(style="bold")
|
|
280
|
+
header_table.add_column()
|
|
281
|
+
header_table.add_row("Run ID:", state.run_id)
|
|
282
|
+
header_table.add_row("Workflow:", state.workflow_name)
|
|
283
|
+
header_table.add_row("Status:", f"[{status_color}]{state.status.value}[/]")
|
|
284
|
+
if state.current_stage:
|
|
285
|
+
header_table.add_row("Current Stage:", state.current_stage)
|
|
286
|
+
if state.error_summary:
|
|
287
|
+
header_table.add_row("Error:", f"[bold red]{state.error_summary}[/]")
|
|
288
|
+
header_table.add_row(
|
|
289
|
+
"Started At:", state.started_at.strftime("%Y-%m-%d %H:%M:%S UTC")
|
|
290
|
+
)
|
|
291
|
+
if state.finished_at:
|
|
292
|
+
header_table.add_row(
|
|
293
|
+
"Finished At:", state.finished_at.strftime("%Y-%m-%d %H:%M:%S UTC")
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
console.print(
|
|
297
|
+
Panel(
|
|
298
|
+
header_table,
|
|
299
|
+
title=f"Workflow Run: {state.workflow_name}",
|
|
300
|
+
expand=False,
|
|
301
|
+
)
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
checkpoints = store.get_checkpoints(state.run_id)
|
|
305
|
+
if not checkpoints:
|
|
306
|
+
console.print("[dim]No step checkpoints recorded yet.[/]")
|
|
307
|
+
return
|
|
308
|
+
|
|
309
|
+
table = Table(title="Step Checkpoints & Staged Artifacts", expand=True)
|
|
310
|
+
table.add_column("Stage", style="cyan")
|
|
311
|
+
table.add_column("Step", style="bold")
|
|
312
|
+
table.add_column("Status", justify="center")
|
|
313
|
+
table.add_column("Attempt", justify="right")
|
|
314
|
+
table.add_column("Duration (s)", justify="right")
|
|
315
|
+
table.add_column("Artifact / Output", style="dim")
|
|
316
|
+
|
|
317
|
+
for chk in checkpoints:
|
|
318
|
+
chk_color = {
|
|
319
|
+
StepStatus.COMPLETED: "[green]COMPLETED[/]",
|
|
320
|
+
StepStatus.RUNNING: "[cyan]RUNNING[/]",
|
|
321
|
+
StepStatus.FAILED: "[red]FAILED[/]",
|
|
322
|
+
StepStatus.SKIPPED: "[dim]SKIPPED[/]",
|
|
323
|
+
StepStatus.PENDING: "[white]PENDING[/]",
|
|
324
|
+
}.get(chk.status, str(chk.status))
|
|
325
|
+
|
|
326
|
+
artifact_info = "-"
|
|
327
|
+
if ArtifactReference.is_artifact_envelope(chk.output_payload):
|
|
328
|
+
uri = chk.output_payload.get("storage_uri", "")
|
|
329
|
+
size = chk.output_payload.get("size_bytes", 0)
|
|
330
|
+
artifact_info = f"[cyan]staged:[/] {uri} ({size}B)"
|
|
331
|
+
elif chk.output_payload is not None:
|
|
332
|
+
raw_str = str(chk.output_payload)
|
|
333
|
+
artifact_info = (raw_str[:30] + "...") if len(raw_str) > 30 else raw_str
|
|
334
|
+
|
|
335
|
+
table.add_row(
|
|
336
|
+
chk.stage_name,
|
|
337
|
+
chk.step_name,
|
|
338
|
+
chk_color,
|
|
339
|
+
str(chk.attempt_number),
|
|
340
|
+
f"{chk.duration_seconds:.3f}",
|
|
341
|
+
artifact_info,
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
console.print(table)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
__all__ = [
|
|
348
|
+
"abort_cmd",
|
|
349
|
+
"app",
|
|
350
|
+
"load_workflow_from_target",
|
|
351
|
+
"parse_inputs_arg",
|
|
352
|
+
"resume_cmd",
|
|
353
|
+
"status_cmd",
|
|
354
|
+
"submit_cmd",
|
|
355
|
+
]
|
|
@@ -12,6 +12,7 @@ from rich.table import Table
|
|
|
12
12
|
|
|
13
13
|
from hexaqueue_cli.adapters.local import LocalClientAdapter
|
|
14
14
|
from hexaqueue_cli.commands.run import app as run_app
|
|
15
|
+
from hexaqueue_cli.commands.workflow import app as workflow_app
|
|
15
16
|
|
|
16
17
|
app = typer.Typer(
|
|
17
18
|
name="hq",
|
|
@@ -19,6 +20,7 @@ app = typer.Typer(
|
|
|
19
20
|
no_args_is_help=True,
|
|
20
21
|
)
|
|
21
22
|
app.add_typer(run_app, name="run")
|
|
23
|
+
app.add_typer(workflow_app, name="workflow")
|
|
22
24
|
|
|
23
25
|
console = Console()
|
|
24
26
|
|
|
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
|