hexaqueue-cli 0.2.0__tar.gz → 0.3.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.2.0 → hexaqueue_cli-0.3.0}/PKG-INFO +5 -5
- {hexaqueue_cli-0.2.0 → hexaqueue_cli-0.3.0}/pyproject.toml +5 -5
- {hexaqueue_cli-0.2.0 → hexaqueue_cli-0.3.0}/pyproject.toml.orig +5 -5
- {hexaqueue_cli-0.2.0 → hexaqueue_cli-0.3.0}/src/hexaqueue_cli/adapters/__init__.py +2 -2
- hexaqueue_cli-0.3.0/src/hexaqueue_cli/adapters/presenter.py +371 -0
- {hexaqueue_cli-0.2.0 → hexaqueue_cli-0.3.0}/src/hexaqueue_cli/commands/run.py +22 -23
- {hexaqueue_cli-0.2.0 → hexaqueue_cli-0.3.0}/src/hexaqueue_cli/commands/workflow.py +33 -90
- {hexaqueue_cli-0.2.0 → hexaqueue_cli-0.3.0}/src/hexaqueue_cli/domain/__init__.py +2 -0
- hexaqueue_cli-0.3.0/src/hexaqueue_cli/domain/options.py +25 -0
- hexaqueue_cli-0.3.0/src/hexaqueue_cli/infra/__init__.py +8 -0
- hexaqueue_cli-0.3.0/src/hexaqueue_cli/infra/options.py +73 -0
- {hexaqueue_cli-0.2.0 → hexaqueue_cli-0.3.0}/src/hexaqueue_cli/main.py +7 -40
- hexaqueue_cli-0.2.0/src/hexaqueue_cli/infra/__init__.py +0 -7
- {hexaqueue_cli-0.2.0 → hexaqueue_cli-0.3.0}/README.md +0 -0
- {hexaqueue_cli-0.2.0 → hexaqueue_cli-0.3.0}/src/hexaqueue_cli/__init__.py +0 -0
- {hexaqueue_cli-0.2.0 → hexaqueue_cli-0.3.0}/src/hexaqueue_cli/adapters/local.py +0 -0
- {hexaqueue_cli-0.2.0 → hexaqueue_cli-0.3.0}/src/hexaqueue_cli/commands/__init__.py +0 -0
- {hexaqueue_cli-0.2.0 → hexaqueue_cli-0.3.0}/src/hexaqueue_cli/domain/parser.py +0 -0
- {hexaqueue_cli-0.2.0 → hexaqueue_cli-0.3.0}/src/hexaqueue_cli/domain/session.py +0 -0
- {hexaqueue_cli-0.2.0 → hexaqueue_cli-0.3.0}/src/hexaqueue_cli/infra/bootstrap.py +0 -0
- {hexaqueue_cli-0.2.0 → hexaqueue_cli-0.3.0}/src/hexaqueue_cli/ports/__init__.py +0 -0
- {hexaqueue_cli-0.2.0 → hexaqueue_cli-0.3.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.3.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>
|
|
@@ -9,10 +9,10 @@ Requires-Dist: hexaqueue-core
|
|
|
9
9
|
Requires-Dist: hexaqueue-server
|
|
10
10
|
Requires-Dist: hexaqueue-worker
|
|
11
11
|
Requires-Dist: hexaqueue-workflow
|
|
12
|
-
Requires-Dist: hexastack-cli>=0.
|
|
13
|
-
Requires-Dist: hexastack-core>=0.
|
|
14
|
-
Requires-Dist: hexastack-cqrs>=0.
|
|
15
|
-
Requires-Dist: hexastack-grpc>=0.
|
|
12
|
+
Requires-Dist: hexastack-cli>=0.6.0
|
|
13
|
+
Requires-Dist: hexastack-core>=0.6.0
|
|
14
|
+
Requires-Dist: hexastack-cqrs>=0.6.0
|
|
15
|
+
Requires-Dist: hexastack-grpc>=0.6.0
|
|
16
16
|
Requires-Dist: pyyaml>=6.0.2
|
|
17
17
|
Requires-Dist: rich>=13.8.0
|
|
18
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.3.0"
|
|
4
4
|
description = "Hexaqueue CLI - Modern developer and operator terminal interface"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -10,10 +10,10 @@ dependencies = [
|
|
|
10
10
|
"hexaqueue-server",
|
|
11
11
|
"hexaqueue-worker",
|
|
12
12
|
"hexaqueue-workflow",
|
|
13
|
-
"hexastack-cli>=0.
|
|
14
|
-
"hexastack-core>=0.
|
|
15
|
-
"hexastack-cqrs>=0.
|
|
16
|
-
"hexastack-grpc>=0.
|
|
13
|
+
"hexastack-cli>=0.6.0",
|
|
14
|
+
"hexastack-core>=0.6.0",
|
|
15
|
+
"hexastack-cqrs>=0.6.0",
|
|
16
|
+
"hexastack-grpc>=0.6.0",
|
|
17
17
|
"pyyaml>=6.0.2",
|
|
18
18
|
"rich>=13.8.0",
|
|
19
19
|
"typer>=0.15.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "hexaqueue-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
4
4
|
description = "Hexaqueue CLI - Modern developer and operator terminal interface"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -13,10 +13,10 @@ dependencies = [
|
|
|
13
13
|
"hexaqueue-server",
|
|
14
14
|
"hexaqueue-worker",
|
|
15
15
|
"hexaqueue-workflow",
|
|
16
|
-
"hexastack-cli>=0.
|
|
17
|
-
"hexastack-core>=0.
|
|
18
|
-
"hexastack-cqrs>=0.
|
|
19
|
-
"hexastack-grpc>=0.
|
|
16
|
+
"hexastack-cli>=0.6.0",
|
|
17
|
+
"hexastack-core>=0.6.0",
|
|
18
|
+
"hexastack-cqrs>=0.6.0",
|
|
19
|
+
"hexastack-grpc>=0.6.0",
|
|
20
20
|
"pyyaml>=6.0.2",
|
|
21
21
|
"rich>=13.8.0",
|
|
22
22
|
"typer>=0.15.0",
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
"""Terminal and pipeline output presenters for Hexaqueue CLI commands.
|
|
2
|
+
|
|
3
|
+
Notes/Architectural Intent:
|
|
4
|
+
Provides formatted output rendering across multiple formats (table, json, markdown, plain, rich).
|
|
5
|
+
Ensures seamless piping into downstream Unix utilities when non-interactive, while
|
|
6
|
+
delivering rich visual dashboards for human terminal operators.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from pydantic import BaseModel
|
|
17
|
+
from rich.console import Console
|
|
18
|
+
from rich.panel import Panel
|
|
19
|
+
from rich.table import Table
|
|
20
|
+
|
|
21
|
+
from hexaqueue_cli.domain.options import OutputFormat
|
|
22
|
+
from hexaqueue_core.domain.job import JobSpec
|
|
23
|
+
from hexaqueue_server.domain.models import RunStatusReport
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"CliPresenter",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class CliPresenter:
|
|
31
|
+
"""Presenter formatting CLI models and reports across supported output formats.
|
|
32
|
+
|
|
33
|
+
Notes/Architectural Intent:
|
|
34
|
+
Implements rendering for table, json, markdown, plain, and rich.
|
|
35
|
+
Allows commands to dispatch results cleanly without mixing presentation logic
|
|
36
|
+
with orchestration.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
console: Console | None = None,
|
|
42
|
+
stderr_console: Console | None = None,
|
|
43
|
+
) -> None:
|
|
44
|
+
"""Initialize CliPresenter with optional consoles.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
console: Optional Rich console for stdout.
|
|
48
|
+
stderr_console: Optional Rich console for stderr.
|
|
49
|
+
"""
|
|
50
|
+
no_color = bool(os.environ.get("NO_COLOR"))
|
|
51
|
+
self._console = console or Console(no_color=no_color, highlight=not no_color)
|
|
52
|
+
self._stderr = stderr_console or Console(
|
|
53
|
+
stderr=True, no_color=no_color, highlight=not no_color
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def render_run_status(
|
|
57
|
+
self,
|
|
58
|
+
report: RunStatusReport,
|
|
59
|
+
format_type: str | OutputFormat = OutputFormat.TABLE,
|
|
60
|
+
) -> None:
|
|
61
|
+
"""Render a pipeline run status report in the chosen format.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
report: RunStatusReport domain model.
|
|
65
|
+
format_type: Chosen output format string or OutputFormat enum.
|
|
66
|
+
"""
|
|
67
|
+
fmt = (
|
|
68
|
+
format_type.value
|
|
69
|
+
if isinstance(format_type, OutputFormat)
|
|
70
|
+
else str(format_type).lower().strip()
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
data = {
|
|
74
|
+
"run_id": report.run_id,
|
|
75
|
+
"state": (
|
|
76
|
+
report.state.value
|
|
77
|
+
if hasattr(report.state, "value")
|
|
78
|
+
else str(report.state)
|
|
79
|
+
),
|
|
80
|
+
"outcome": (
|
|
81
|
+
report.outcome.value
|
|
82
|
+
if hasattr(report.outcome, "value")
|
|
83
|
+
else (str(report.outcome) if report.outcome else None)
|
|
84
|
+
),
|
|
85
|
+
"total_jobs": report.total_jobs,
|
|
86
|
+
"completed_jobs": report.completed_jobs,
|
|
87
|
+
"failed_jobs": report.failed_jobs,
|
|
88
|
+
"pending_jobs": report.pending_jobs,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if fmt == OutputFormat.JSON.value:
|
|
92
|
+
sys.stdout.write(json.dumps(data, indent=2) + "\n")
|
|
93
|
+
sys.stdout.flush()
|
|
94
|
+
elif fmt == OutputFormat.MARKDOWN.value:
|
|
95
|
+
lines = [
|
|
96
|
+
f"### Run Status: {report.run_id}",
|
|
97
|
+
"",
|
|
98
|
+
"| Field | Value |",
|
|
99
|
+
"|---|---|",
|
|
100
|
+
f"| Run ID | `{report.run_id}` |",
|
|
101
|
+
f"| State | **{data['state']}** |",
|
|
102
|
+
f"| Outcome | {data['outcome'] or '-'} |",
|
|
103
|
+
f"| Total Jobs | {report.total_jobs} |",
|
|
104
|
+
f"| Completed | {report.completed_jobs} |",
|
|
105
|
+
f"| Failed | {report.failed_jobs} |",
|
|
106
|
+
f"| Pending | {report.pending_jobs} |",
|
|
107
|
+
]
|
|
108
|
+
sys.stdout.write("\n".join(lines) + "\n")
|
|
109
|
+
sys.stdout.flush()
|
|
110
|
+
elif fmt == OutputFormat.PLAIN.value:
|
|
111
|
+
for k, v in data.items():
|
|
112
|
+
sys.stdout.write(f"{k}\t{v}\n")
|
|
113
|
+
sys.stdout.flush()
|
|
114
|
+
else:
|
|
115
|
+
# table or rich
|
|
116
|
+
table = Table(title=f"Run Status: {report.run_id}")
|
|
117
|
+
table.add_column("Run ID", style="cyan")
|
|
118
|
+
table.add_column("State", style="magenta")
|
|
119
|
+
table.add_column(
|
|
120
|
+
"Outcome",
|
|
121
|
+
style="green"
|
|
122
|
+
if str(report.outcome) in ("COMPLETED", "SUCCEEDED")
|
|
123
|
+
else "yellow",
|
|
124
|
+
)
|
|
125
|
+
table.add_column("Total", justify="center")
|
|
126
|
+
table.add_column("Completed", justify="center", style="green")
|
|
127
|
+
table.add_column("Failed", justify="center", style="red")
|
|
128
|
+
table.add_column("Pending", justify="center", style="yellow")
|
|
129
|
+
table.add_row(
|
|
130
|
+
report.run_id,
|
|
131
|
+
data["state"],
|
|
132
|
+
str(report.outcome) if report.outcome else "-",
|
|
133
|
+
str(report.total_jobs),
|
|
134
|
+
str(report.completed_jobs),
|
|
135
|
+
str(report.failed_jobs),
|
|
136
|
+
str(report.pending_jobs),
|
|
137
|
+
)
|
|
138
|
+
self._console.print(table)
|
|
139
|
+
|
|
140
|
+
def render_job(
|
|
141
|
+
self,
|
|
142
|
+
job: JobSpec,
|
|
143
|
+
format_type: str | OutputFormat = OutputFormat.TABLE,
|
|
144
|
+
) -> None:
|
|
145
|
+
"""Render a single JobSpec in the chosen format.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
job: JobSpec domain model.
|
|
149
|
+
format_type: Chosen output format string or OutputFormat enum.
|
|
150
|
+
"""
|
|
151
|
+
fmt = (
|
|
152
|
+
format_type.value
|
|
153
|
+
if isinstance(format_type, OutputFormat)
|
|
154
|
+
else str(format_type).lower().strip()
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
state_val = job.state.value if hasattr(job.state, "value") else str(job.state)
|
|
158
|
+
outcome_val = (
|
|
159
|
+
job.outcome.value
|
|
160
|
+
if hasattr(job.outcome, "value")
|
|
161
|
+
else (str(job.outcome) if job.outcome else None)
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
data = {
|
|
165
|
+
"id": job.id,
|
|
166
|
+
"run_id": job.run_id,
|
|
167
|
+
"name": job.name,
|
|
168
|
+
"state": state_val,
|
|
169
|
+
"outcome": outcome_val,
|
|
170
|
+
"command": job.command,
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if fmt == OutputFormat.JSON.value:
|
|
174
|
+
sys.stdout.write(json.dumps(data, indent=2) + "\n")
|
|
175
|
+
sys.stdout.flush()
|
|
176
|
+
elif fmt == OutputFormat.MARKDOWN.value:
|
|
177
|
+
lines = [
|
|
178
|
+
f"### Job Status: {job.id}",
|
|
179
|
+
"",
|
|
180
|
+
"| Field | Value |",
|
|
181
|
+
"|---|---|",
|
|
182
|
+
f"| Job ID | `{job.id}` |",
|
|
183
|
+
f"| Run ID | `{job.run_id}` |",
|
|
184
|
+
f"| Name | {job.name} |",
|
|
185
|
+
f"| State | **{state_val}** |",
|
|
186
|
+
f"| Outcome | {outcome_val or '-'} |",
|
|
187
|
+
f"| Command | `{job.command}` |",
|
|
188
|
+
]
|
|
189
|
+
sys.stdout.write("\n".join(lines) + "\n")
|
|
190
|
+
sys.stdout.flush()
|
|
191
|
+
elif fmt == OutputFormat.PLAIN.value:
|
|
192
|
+
for k, v in data.items():
|
|
193
|
+
sys.stdout.write(f"{k}\t{v}\n")
|
|
194
|
+
sys.stdout.flush()
|
|
195
|
+
else:
|
|
196
|
+
table = Table(title=f"Job Status: {job.id}")
|
|
197
|
+
table.add_column("Job ID", style="cyan")
|
|
198
|
+
table.add_column("Run ID", style="blue")
|
|
199
|
+
table.add_column("Name")
|
|
200
|
+
table.add_column("State", style="magenta")
|
|
201
|
+
table.add_column("Outcome")
|
|
202
|
+
table.add_row(
|
|
203
|
+
job.id,
|
|
204
|
+
job.run_id,
|
|
205
|
+
job.name,
|
|
206
|
+
state_val,
|
|
207
|
+
str(job.outcome) if job.outcome else "-",
|
|
208
|
+
)
|
|
209
|
+
self._console.print(table)
|
|
210
|
+
|
|
211
|
+
def _serialize_checkpoints(self, checkpoints: list[Any]) -> list[dict[str, Any]]:
|
|
212
|
+
"""Extract serialized dictionary representations of step checkpoints."""
|
|
213
|
+
chk_list: list[dict[str, Any]] = []
|
|
214
|
+
for chk in checkpoints:
|
|
215
|
+
chk_status = (
|
|
216
|
+
chk.status.value if hasattr(chk.status, "value") else str(chk.status)
|
|
217
|
+
)
|
|
218
|
+
payload = (
|
|
219
|
+
chk.output_payload
|
|
220
|
+
if not isinstance(chk.output_payload, BaseModel)
|
|
221
|
+
else chk.output_payload.model_dump()
|
|
222
|
+
)
|
|
223
|
+
chk_list.append(
|
|
224
|
+
{
|
|
225
|
+
"stage_name": chk.stage_name,
|
|
226
|
+
"step_name": chk.step_name,
|
|
227
|
+
"status": chk_status,
|
|
228
|
+
"attempt_number": chk.attempt_number,
|
|
229
|
+
"duration_seconds": round(chk.duration_seconds, 3),
|
|
230
|
+
"output_payload": payload,
|
|
231
|
+
}
|
|
232
|
+
)
|
|
233
|
+
return chk_list
|
|
234
|
+
|
|
235
|
+
def _render_workflow_json(self, data: dict[str, Any]) -> None:
|
|
236
|
+
"""Output workflow status report as indented JSON."""
|
|
237
|
+
sys.stdout.write(json.dumps(data, indent=2, default=str) + "\n")
|
|
238
|
+
sys.stdout.flush()
|
|
239
|
+
|
|
240
|
+
def _render_workflow_markdown(self, data: dict[str, Any]) -> None:
|
|
241
|
+
"""Output workflow status report as Markdown."""
|
|
242
|
+
lines = [
|
|
243
|
+
f"### Workflow Run: {data['workflow_name']} (`{data['run_id']}`)",
|
|
244
|
+
"",
|
|
245
|
+
f"- **Status**: `{data['status']}`",
|
|
246
|
+
]
|
|
247
|
+
if data.get("current_stage"):
|
|
248
|
+
lines.append(f"- **Current Stage**: {data['current_stage']}")
|
|
249
|
+
if data.get("error_summary"):
|
|
250
|
+
lines.append(f"- **Error**: {data['error_summary']}")
|
|
251
|
+
lines.extend(
|
|
252
|
+
[
|
|
253
|
+
"",
|
|
254
|
+
"| Stage | Step | Status | Attempt | Duration (s) |",
|
|
255
|
+
"|---|---|---|---|---|",
|
|
256
|
+
]
|
|
257
|
+
)
|
|
258
|
+
for c in data.get("checkpoints", []):
|
|
259
|
+
lines.append(
|
|
260
|
+
f"| {c['stage_name']} | {c['step_name']} | {c['status']} | {c['attempt_number']} | {c['duration_seconds']} |"
|
|
261
|
+
)
|
|
262
|
+
sys.stdout.write("\n".join(lines) + "\n")
|
|
263
|
+
sys.stdout.flush()
|
|
264
|
+
|
|
265
|
+
def _render_workflow_plain(self, data: dict[str, Any]) -> None:
|
|
266
|
+
"""Output workflow status report as plain tab-delimited text."""
|
|
267
|
+
sys.stdout.write(f"run_id\t{data['run_id']}\n")
|
|
268
|
+
sys.stdout.write(f"workflow\t{data['workflow_name']}\n")
|
|
269
|
+
sys.stdout.write(f"status\t{data['status']}\n")
|
|
270
|
+
for c in data.get("checkpoints", []):
|
|
271
|
+
sys.stdout.write(
|
|
272
|
+
f"{c['stage_name']}\t{c['step_name']}\t{c['status']}\t{c['duration_seconds']}\n"
|
|
273
|
+
)
|
|
274
|
+
sys.stdout.flush()
|
|
275
|
+
|
|
276
|
+
def _render_workflow_table(self, data: dict[str, Any]) -> None:
|
|
277
|
+
"""Output workflow status report as interactive Rich table and panel."""
|
|
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:", str(data["run_id"]))
|
|
282
|
+
header_table.add_row("Workflow:", str(data["workflow_name"]))
|
|
283
|
+
header_table.add_row("Status:", f"[bold cyan]{data['status']}[/]")
|
|
284
|
+
if data.get("current_stage"):
|
|
285
|
+
header_table.add_row("Current Stage:", str(data["current_stage"]))
|
|
286
|
+
if data.get("error_summary"):
|
|
287
|
+
header_table.add_row("Error:", f"[bold red]{data['error_summary']}[/]")
|
|
288
|
+
|
|
289
|
+
self._console.print(
|
|
290
|
+
Panel(
|
|
291
|
+
header_table,
|
|
292
|
+
title=f"Workflow Run: {data['workflow_name']}",
|
|
293
|
+
expand=False,
|
|
294
|
+
)
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
chk_list = data.get("checkpoints", [])
|
|
298
|
+
if not chk_list:
|
|
299
|
+
self._console.print("[dim]No step checkpoints recorded yet.[/]")
|
|
300
|
+
return
|
|
301
|
+
|
|
302
|
+
table = Table(title="Step Checkpoints & Staged Artifacts", expand=True)
|
|
303
|
+
table.add_column("Stage", style="cyan")
|
|
304
|
+
table.add_column("Step", style="bold")
|
|
305
|
+
table.add_column("Status", justify="center")
|
|
306
|
+
table.add_column("Attempt", justify="right")
|
|
307
|
+
table.add_column("Duration (s)", justify="right")
|
|
308
|
+
table.add_column("Artifact / Output", style="dim")
|
|
309
|
+
|
|
310
|
+
for chk in chk_list:
|
|
311
|
+
chk_color = (
|
|
312
|
+
"[green]COMPLETED[/]"
|
|
313
|
+
if chk["status"] == "COMPLETED"
|
|
314
|
+
else (
|
|
315
|
+
"[red]FAILED[/]"
|
|
316
|
+
if chk["status"] == "FAILED"
|
|
317
|
+
else str(chk["status"])
|
|
318
|
+
)
|
|
319
|
+
)
|
|
320
|
+
payload_repr = str(chk["output_payload"])
|
|
321
|
+
if len(payload_repr) > 30:
|
|
322
|
+
payload_repr = payload_repr[:30] + "..."
|
|
323
|
+
|
|
324
|
+
table.add_row(
|
|
325
|
+
chk["stage_name"],
|
|
326
|
+
chk["step_name"],
|
|
327
|
+
chk_color,
|
|
328
|
+
str(chk["attempt_number"]),
|
|
329
|
+
f"{chk['duration_seconds']:.3f}",
|
|
330
|
+
payload_repr,
|
|
331
|
+
)
|
|
332
|
+
self._console.print(table)
|
|
333
|
+
|
|
334
|
+
def render_workflow_state(
|
|
335
|
+
self,
|
|
336
|
+
state: Any,
|
|
337
|
+
checkpoints: list[Any],
|
|
338
|
+
format_type: str | OutputFormat = OutputFormat.TABLE,
|
|
339
|
+
) -> None:
|
|
340
|
+
"""Render workflow execution state and checkpoints in the chosen format.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
state: WorkflowExecutionState instance.
|
|
344
|
+
checkpoints: List of CheckpointRecord instances.
|
|
345
|
+
format_type: Chosen output format string or OutputFormat enum.
|
|
346
|
+
"""
|
|
347
|
+
fmt = (
|
|
348
|
+
format_type.value
|
|
349
|
+
if isinstance(format_type, OutputFormat)
|
|
350
|
+
else str(format_type).lower().strip()
|
|
351
|
+
)
|
|
352
|
+
status_val = (
|
|
353
|
+
state.status.value if hasattr(state.status, "value") else str(state.status)
|
|
354
|
+
)
|
|
355
|
+
data = {
|
|
356
|
+
"run_id": state.run_id,
|
|
357
|
+
"workflow_name": state.workflow_name,
|
|
358
|
+
"status": status_val,
|
|
359
|
+
"current_stage": getattr(state, "current_stage", None),
|
|
360
|
+
"error_summary": getattr(state, "error_summary", None),
|
|
361
|
+
"checkpoints": self._serialize_checkpoints(checkpoints),
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if fmt == OutputFormat.JSON.value:
|
|
365
|
+
self._render_workflow_json(data)
|
|
366
|
+
elif fmt == OutputFormat.MARKDOWN.value:
|
|
367
|
+
self._render_workflow_markdown(data)
|
|
368
|
+
elif fmt == OutputFormat.PLAIN.value:
|
|
369
|
+
self._render_workflow_plain(data)
|
|
370
|
+
else:
|
|
371
|
+
self._render_workflow_table(data)
|
|
@@ -10,11 +10,12 @@ from pathlib import Path
|
|
|
10
10
|
|
|
11
11
|
import typer
|
|
12
12
|
from rich.console import Console
|
|
13
|
-
from rich.table import Table
|
|
14
13
|
|
|
15
14
|
from hexaqueue_cli.adapters.local import LocalClientAdapter
|
|
15
|
+
from hexaqueue_cli.adapters.presenter import CliPresenter
|
|
16
16
|
from hexaqueue_cli.domain.parser import parse_run_spec_from_file
|
|
17
17
|
from hexaqueue_cli.domain.session import get_default_session
|
|
18
|
+
from hexaqueue_cli.infra.options import format_option, resolve_format
|
|
18
19
|
from hexaqueue_core.domain.lifecycle import RunState
|
|
19
20
|
|
|
20
21
|
app = typer.Typer(help="Pipeline run management commands.")
|
|
@@ -27,6 +28,7 @@ def submit_cmd(
|
|
|
27
28
|
watch: bool = typer.Option(
|
|
28
29
|
False, "--watch", "-w", help="Watch run execution until completion"
|
|
29
30
|
),
|
|
31
|
+
format_type: str = format_option(),
|
|
30
32
|
) -> None:
|
|
31
33
|
"""Submit a DAG pipeline definition file."""
|
|
32
34
|
try:
|
|
@@ -35,41 +37,38 @@ def submit_cmd(
|
|
|
35
37
|
console.print(f"[bold red]Error parsing pipeline spec:[/] {e}")
|
|
36
38
|
raise typer.Exit(code=1) from e
|
|
37
39
|
|
|
40
|
+
resolved_fmt = resolve_format(format_type)
|
|
41
|
+
presenter = CliPresenter(console=console)
|
|
42
|
+
|
|
38
43
|
async def _run() -> None:
|
|
39
44
|
session = get_default_session()
|
|
40
45
|
await session.start()
|
|
41
46
|
client = LocalClientAdapter(session=session)
|
|
42
47
|
|
|
43
48
|
report = await client.submit_run(submission)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
49
|
+
if resolved_fmt in ("table", "rich"):
|
|
50
|
+
console.print(
|
|
51
|
+
f"[bold green]✓[/] Run '[bold cyan]{report.run_id}[/]' submitted ({report.total_jobs} jobs)"
|
|
52
|
+
)
|
|
53
|
+
else:
|
|
54
|
+
presenter.render_run_status(report, resolved_fmt)
|
|
55
|
+
|
|
56
|
+
if not watch:
|
|
57
|
+
return
|
|
47
58
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
report = await client.get_run_status(report.run_id)
|
|
59
|
+
with console.status(f"[bold blue]Executing run {report.run_id}...[/]"):
|
|
60
|
+
while report.state not in (RunState.DONE, RunState.BLOCKED):
|
|
61
|
+
await asyncio.sleep(0.1)
|
|
62
|
+
report = await client.get_run_status(report.run_id)
|
|
53
63
|
|
|
64
|
+
if resolved_fmt in ("table", "rich"):
|
|
54
65
|
outcome_str = (
|
|
55
66
|
f"[bold green]{report.outcome}[/]"
|
|
56
|
-
if report.outcome
|
|
67
|
+
if str(report.outcome) in ("COMPLETED", "SUCCEEDED")
|
|
57
68
|
else f"[bold red]{report.outcome}[/]"
|
|
58
69
|
)
|
|
59
70
|
console.print(f"Run completed with status: {outcome_str}")
|
|
60
|
-
|
|
61
|
-
table = Table(title=f"Run Summary: {report.run_id}")
|
|
62
|
-
table.add_column("Total", justify="center")
|
|
63
|
-
table.add_column("Completed", justify="center", style="green")
|
|
64
|
-
table.add_column("Failed", justify="center", style="red")
|
|
65
|
-
table.add_column("Pending", justify="center", style="yellow")
|
|
66
|
-
table.add_row(
|
|
67
|
-
str(report.total_jobs),
|
|
68
|
-
str(report.completed_jobs),
|
|
69
|
-
str(report.failed_jobs),
|
|
70
|
-
str(report.pending_jobs),
|
|
71
|
-
)
|
|
72
|
-
console.print(table)
|
|
71
|
+
presenter.render_run_status(report, resolved_fmt)
|
|
73
72
|
|
|
74
73
|
asyncio.run(_run())
|
|
75
74
|
|
|
@@ -15,14 +15,12 @@ from typing import Any
|
|
|
15
15
|
import typer
|
|
16
16
|
from hexaflow.adapters.storage.sqlite import SqliteStateStore
|
|
17
17
|
from hexaflow.domain.models import WorkflowDefinition
|
|
18
|
-
from hexaflow.domain.state import StepStatus, WorkflowStatus
|
|
19
18
|
from hexaflow.dsl.builder import Workflow
|
|
20
19
|
from rich.console import Console
|
|
21
|
-
from rich.panel import Panel
|
|
22
|
-
from rich.table import Table
|
|
23
20
|
|
|
21
|
+
from hexaqueue_cli.adapters.presenter import CliPresenter
|
|
22
|
+
from hexaqueue_cli.infra.options import format_option, resolve_format
|
|
24
23
|
from hexaqueue_workflow.adapters.engines.distributed import HexaqueueDistributedEngine
|
|
25
|
-
from hexaqueue_workflow.domain.models import ArtifactReference
|
|
26
24
|
|
|
27
25
|
app = typer.Typer(
|
|
28
26
|
name="workflow",
|
|
@@ -30,6 +28,7 @@ app = typer.Typer(
|
|
|
30
28
|
no_args_is_help=True,
|
|
31
29
|
)
|
|
32
30
|
console = Console()
|
|
31
|
+
presenter = CliPresenter(console=console)
|
|
33
32
|
|
|
34
33
|
|
|
35
34
|
def load_workflow_from_target(target: str) -> WorkflowDefinition:
|
|
@@ -137,6 +136,7 @@ def submit_cmd(
|
|
|
137
136
|
watch: bool = typer.Option(
|
|
138
137
|
False, "--watch", "-w", help="Watch workflow execution until completion"
|
|
139
138
|
),
|
|
139
|
+
format_type: str = format_option(),
|
|
140
140
|
) -> None:
|
|
141
141
|
"""Submit a workflow definition for distributed cluster execution.
|
|
142
142
|
|
|
@@ -145,21 +145,24 @@ def submit_cmd(
|
|
|
145
145
|
inputs: Optional JSON inputs string.
|
|
146
146
|
db_path: SQLite state store database path.
|
|
147
147
|
watch: Whether to block and watch until terminal completion.
|
|
148
|
+
format_type: Output presentation format.
|
|
148
149
|
"""
|
|
149
150
|
workflow = load_workflow_from_target(target)
|
|
150
151
|
initial_inputs = parse_inputs_arg(inputs)
|
|
152
|
+
resolved_fmt = resolve_format(format_type)
|
|
151
153
|
|
|
152
154
|
store = SqliteStateStore(db_path=db_path)
|
|
153
155
|
engine = HexaqueueDistributedEngine(state_store=store)
|
|
154
156
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
157
|
+
if resolved_fmt in ("table", "rich"):
|
|
158
|
+
console.print(
|
|
159
|
+
f"[bold green]Submitting workflow:[/] [cyan]{workflow.name}[/] "
|
|
160
|
+
f"({len(workflow.stages)} stages, {sum(len(s.steps) for s in workflow.stages)} steps)"
|
|
161
|
+
)
|
|
159
162
|
|
|
160
163
|
async def _execute() -> None:
|
|
161
164
|
state = await engine.run_async(workflow, initial_inputs=initial_inputs)
|
|
162
|
-
_render_workflow_state(state, store)
|
|
165
|
+
_render_workflow_state(state, store, resolved_fmt)
|
|
163
166
|
|
|
164
167
|
asyncio.run(_execute())
|
|
165
168
|
|
|
@@ -172,13 +175,16 @@ def status_cmd(
|
|
|
172
175
|
db_path: str = typer.Option(
|
|
173
176
|
".hexaflow/state.db", "--db", help="Path to SQLite state database"
|
|
174
177
|
),
|
|
178
|
+
format_type: str = format_option(),
|
|
175
179
|
) -> None:
|
|
176
180
|
"""Inspect status, checkpoints, and staged artifacts for a workflow run.
|
|
177
181
|
|
|
178
182
|
Args:
|
|
179
183
|
run_id: Run identifier to query.
|
|
180
184
|
db_path: SQLite state store database path.
|
|
185
|
+
format_type: Output presentation format.
|
|
181
186
|
"""
|
|
187
|
+
resolved_fmt = resolve_format(format_type)
|
|
182
188
|
store = SqliteStateStore(db_path=db_path)
|
|
183
189
|
state = store.get_run(run_id)
|
|
184
190
|
|
|
@@ -188,7 +194,7 @@ def status_cmd(
|
|
|
188
194
|
)
|
|
189
195
|
raise typer.Exit(code=1)
|
|
190
196
|
|
|
191
|
-
_render_workflow_state(state, store)
|
|
197
|
+
_render_workflow_state(state, store, resolved_fmt)
|
|
192
198
|
|
|
193
199
|
|
|
194
200
|
@app.command("resume")
|
|
@@ -207,6 +213,7 @@ def resume_cmd(
|
|
|
207
213
|
db_path: str = typer.Option(
|
|
208
214
|
".hexaflow/state.db", "--db", help="Path to SQLite state database"
|
|
209
215
|
),
|
|
216
|
+
format_type: str = format_option(),
|
|
210
217
|
) -> None:
|
|
211
218
|
"""Resume execution of a suspended workflow run from its latest checkpoints.
|
|
212
219
|
|
|
@@ -216,14 +223,17 @@ def resume_cmd(
|
|
|
216
223
|
inputs: Optional patch inputs.
|
|
217
224
|
skip_steps: Optional step names to skip.
|
|
218
225
|
db_path: SQLite state database path.
|
|
226
|
+
format_type: Output presentation format.
|
|
219
227
|
"""
|
|
220
228
|
workflow = load_workflow_from_target(target)
|
|
221
229
|
patch_inputs = parse_inputs_arg(inputs)
|
|
230
|
+
resolved_fmt = resolve_format(format_type)
|
|
222
231
|
|
|
223
232
|
store = SqliteStateStore(db_path=db_path)
|
|
224
233
|
engine = HexaqueueDistributedEngine(state_store=store)
|
|
225
234
|
|
|
226
|
-
|
|
235
|
+
if resolved_fmt in ("table", "rich"):
|
|
236
|
+
console.print(f"[bold yellow]Resuming workflow run:[/] [cyan]{run_id}[/]")
|
|
227
237
|
|
|
228
238
|
async def _resume() -> None:
|
|
229
239
|
state = await engine.resume_async(
|
|
@@ -232,7 +242,7 @@ def resume_cmd(
|
|
|
232
242
|
patch_inputs=patch_inputs,
|
|
233
243
|
skip_steps=set(skip_steps or ()),
|
|
234
244
|
)
|
|
235
|
-
_render_workflow_state(state, store)
|
|
245
|
+
_render_workflow_state(state, store, resolved_fmt)
|
|
236
246
|
|
|
237
247
|
asyncio.run(_resume())
|
|
238
248
|
|
|
@@ -244,6 +254,7 @@ def abort_cmd(
|
|
|
244
254
|
db_path: str = typer.Option(
|
|
245
255
|
".hexaflow/state.db", "--db", help="Path to SQLite state database"
|
|
246
256
|
),
|
|
257
|
+
format_type: str = format_option(),
|
|
247
258
|
) -> None:
|
|
248
259
|
"""Abort an active or suspended workflow, unwinding step compensations in reverse order.
|
|
249
260
|
|
|
@@ -251,97 +262,29 @@ def abort_cmd(
|
|
|
251
262
|
run_id: Run identifier to abort.
|
|
252
263
|
target: Workflow file specifier containing compensations.
|
|
253
264
|
db_path: SQLite state database path.
|
|
265
|
+
format_type: Output presentation format.
|
|
254
266
|
"""
|
|
255
267
|
workflow = load_workflow_from_target(target)
|
|
268
|
+
resolved_fmt = resolve_format(format_type)
|
|
256
269
|
store = SqliteStateStore(db_path=db_path)
|
|
257
270
|
engine = HexaqueueDistributedEngine(state_store=store)
|
|
258
271
|
|
|
259
|
-
|
|
272
|
+
if resolved_fmt in ("table", "rich"):
|
|
273
|
+
console.print(f"[bold red]Aborting workflow run:[/] [cyan]{run_id}[/]")
|
|
260
274
|
|
|
261
275
|
async def _abort() -> None:
|
|
262
276
|
state = await engine.abort_async(run_id=run_id, workflow=workflow)
|
|
263
|
-
_render_workflow_state(state, store)
|
|
277
|
+
_render_workflow_state(state, store, resolved_fmt)
|
|
264
278
|
|
|
265
279
|
asyncio.run(_abort())
|
|
266
280
|
|
|
267
281
|
|
|
268
|
-
def _render_workflow_state(
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
282
|
+
def _render_workflow_state(
|
|
283
|
+
state: Any, store: SqliteStateStore, format_type: str = "table"
|
|
284
|
+
) -> None:
|
|
285
|
+
"""Render workflow status summary and step checkpoint table."""
|
|
304
286
|
checkpoints = store.get_checkpoints(state.run_id)
|
|
305
|
-
|
|
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)
|
|
287
|
+
presenter.render_workflow_state(state, checkpoints, format_type=format_type)
|
|
345
288
|
|
|
346
289
|
|
|
347
290
|
__all__ = [
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Domain package for hexaqueue-cli."""
|
|
2
2
|
|
|
3
|
+
from hexaqueue_cli.domain.options import OutputFormat
|
|
3
4
|
from hexaqueue_cli.domain.parser import (
|
|
4
5
|
parse_run_spec_from_dict,
|
|
5
6
|
parse_run_spec_from_file,
|
|
@@ -13,6 +14,7 @@ from hexaqueue_cli.domain.session import (
|
|
|
13
14
|
__all__ = [
|
|
14
15
|
"get_default_session",
|
|
15
16
|
"LocalCliSession",
|
|
17
|
+
"OutputFormat",
|
|
16
18
|
"parse_run_spec_from_dict",
|
|
17
19
|
"parse_run_spec_from_file",
|
|
18
20
|
"set_default_session",
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Domain representations for CLI output presentation formats.
|
|
2
|
+
|
|
3
|
+
Notes/Architectural Intent:
|
|
4
|
+
Defines the canonical output format enumeration shared across CLI presenters,
|
|
5
|
+
command runners, and reporting bridges. Fulfills the cross-ecosystem CLI alignment invariant.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from enum import StrEnum
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"OutputFormat",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class OutputFormat(StrEnum):
|
|
18
|
+
"""Supported output presentation formats for CLI commands."""
|
|
19
|
+
|
|
20
|
+
AUTO = "auto"
|
|
21
|
+
JSON = "json"
|
|
22
|
+
MARKDOWN = "markdown"
|
|
23
|
+
PLAIN = "plain"
|
|
24
|
+
RICH = "rich"
|
|
25
|
+
TABLE = "table"
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""CLI option factories and formatting resolvers for Hexaqueue CLI applications.
|
|
2
|
+
|
|
3
|
+
Notes/Architectural Intent:
|
|
4
|
+
Provides standardized Typer option factories and stdout pipe-detection routines.
|
|
5
|
+
When format is 'auto', automatically routes to structured JSON when piped into
|
|
6
|
+
downstream Unix utilities (jq, grep) or styled Rich/table output when attached to an
|
|
7
|
+
interactive TTY. Aligns with the Hexa ecosystem CLI standards.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import sys
|
|
13
|
+
from enum import Enum
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
import typer
|
|
17
|
+
|
|
18
|
+
from hexaqueue_cli.domain.options import OutputFormat
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"format_option",
|
|
22
|
+
"resolve_format",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def resolve_format(
|
|
27
|
+
format_type: str | OutputFormat,
|
|
28
|
+
default_tty: str = "table",
|
|
29
|
+
default_pipe: str = "json",
|
|
30
|
+
) -> str:
|
|
31
|
+
"""Resolve an output format string, handling auto-detection for pipes.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
format_type: The format chosen by user or default ('auto', 'table', 'json', etc.).
|
|
35
|
+
default_tty: Default format when connected to an interactive terminal.
|
|
36
|
+
default_pipe: Default format when stdout is piped or redirected.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Normalized lowercase format string (e.g. 'table', 'json', 'rich').
|
|
40
|
+
|
|
41
|
+
Notes/Architectural Intent:
|
|
42
|
+
When format_type is 'auto', inspects sys.stdout.isatty() to choose between
|
|
43
|
+
structured machine-readable format for pipelines or visual format for humans.
|
|
44
|
+
"""
|
|
45
|
+
raw = (
|
|
46
|
+
format_type.value
|
|
47
|
+
if isinstance(format_type, Enum)
|
|
48
|
+
else str(format_type).lower().strip()
|
|
49
|
+
)
|
|
50
|
+
if raw == OutputFormat.AUTO.value:
|
|
51
|
+
if not sys.stdout.isatty():
|
|
52
|
+
return default_pipe
|
|
53
|
+
return default_tty
|
|
54
|
+
return raw
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def format_option(
|
|
58
|
+
default: str = "table",
|
|
59
|
+
help_text: str = "Output presentation format (table, json, markdown, rich, plain, auto).",
|
|
60
|
+
) -> Any:
|
|
61
|
+
"""Construct a standardized Typer option for format selection.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
default: Default format option value.
|
|
65
|
+
help_text: Help string to render in CLI usage catalogs.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Configured Typer Option specification.
|
|
69
|
+
|
|
70
|
+
Notes/Architectural Intent:
|
|
71
|
+
Standardizes '-f' / '--format' flag across all Hexaqueue CLI commands.
|
|
72
|
+
"""
|
|
73
|
+
return typer.Option(default, "-f", "--format", help=help_text)
|
|
@@ -8,11 +8,12 @@ import asyncio
|
|
|
8
8
|
|
|
9
9
|
import typer
|
|
10
10
|
from rich.console import Console
|
|
11
|
-
from rich.table import Table
|
|
12
11
|
|
|
13
12
|
from hexaqueue_cli.adapters.local import LocalClientAdapter
|
|
13
|
+
from hexaqueue_cli.adapters.presenter import CliPresenter
|
|
14
14
|
from hexaqueue_cli.commands.run import app as run_app
|
|
15
15
|
from hexaqueue_cli.commands.workflow import app as workflow_app
|
|
16
|
+
from hexaqueue_cli.infra.options import format_option, resolve_format
|
|
16
17
|
|
|
17
18
|
app = typer.Typer(
|
|
18
19
|
name="hq",
|
|
@@ -28,58 +29,24 @@ console = Console()
|
|
|
28
29
|
@app.command("status")
|
|
29
30
|
def status_cmd(
|
|
30
31
|
target_id: str = typer.Argument(..., help="Run ID or Job ID to inspect"),
|
|
32
|
+
format_type: str = format_option(),
|
|
31
33
|
) -> None:
|
|
32
34
|
"""Check status of a run or job."""
|
|
35
|
+
resolved_fmt = resolve_format(format_type)
|
|
36
|
+
presenter = CliPresenter(console=console)
|
|
33
37
|
|
|
34
38
|
async def _status() -> None:
|
|
35
39
|
client = LocalClientAdapter()
|
|
36
40
|
try:
|
|
37
41
|
report = await client.get_run_status(target_id)
|
|
38
|
-
|
|
39
|
-
table.add_column("Run ID", style="cyan")
|
|
40
|
-
table.add_column("State", style="magenta")
|
|
41
|
-
table.add_column(
|
|
42
|
-
"Outcome",
|
|
43
|
-
style="green"
|
|
44
|
-
if report.outcome in ("COMPLETED", "SUCCEEDED")
|
|
45
|
-
else "yellow",
|
|
46
|
-
)
|
|
47
|
-
table.add_column("Total", justify="center")
|
|
48
|
-
table.add_column("Completed", justify="center", style="green")
|
|
49
|
-
table.add_column("Failed", justify="center", style="red")
|
|
50
|
-
table.add_column("Pending", justify="center", style="yellow")
|
|
51
|
-
table.add_row(
|
|
52
|
-
report.run_id,
|
|
53
|
-
report.state.value
|
|
54
|
-
if hasattr(report.state, "value")
|
|
55
|
-
else str(report.state),
|
|
56
|
-
str(report.outcome) if report.outcome else "-",
|
|
57
|
-
str(report.total_jobs),
|
|
58
|
-
str(report.completed_jobs),
|
|
59
|
-
str(report.failed_jobs),
|
|
60
|
-
str(report.pending_jobs),
|
|
61
|
-
)
|
|
62
|
-
console.print(table)
|
|
42
|
+
presenter.render_run_status(report, resolved_fmt)
|
|
63
43
|
return
|
|
64
44
|
except Exception:
|
|
65
45
|
pass
|
|
66
46
|
|
|
67
47
|
try:
|
|
68
48
|
job = await client.get_job(target_id)
|
|
69
|
-
|
|
70
|
-
table.add_column("Job ID", style="cyan")
|
|
71
|
-
table.add_column("Run ID", style="blue")
|
|
72
|
-
table.add_column("Name")
|
|
73
|
-
table.add_column("State", style="magenta")
|
|
74
|
-
table.add_column("Outcome")
|
|
75
|
-
table.add_row(
|
|
76
|
-
job.id,
|
|
77
|
-
job.run_id,
|
|
78
|
-
job.name,
|
|
79
|
-
job.state.value if hasattr(job.state, "value") else str(job.state),
|
|
80
|
-
str(job.outcome) if job.outcome else "-",
|
|
81
|
-
)
|
|
82
|
-
console.print(table)
|
|
49
|
+
presenter.render_job(job, resolved_fmt)
|
|
83
50
|
except Exception as e:
|
|
84
51
|
console.print(f"[bold red]Error querying status for '{target_id}':[/] {e}")
|
|
85
52
|
raise typer.Exit(code=1) from e
|
|
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
|