analogcli 0.1.4__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.
- anabrid/cloud/cli/__init__.py +0 -0
- anabrid/cloud/cli/client.py +385 -0
- anabrid/cloud/cli/commands/__init__.py +0 -0
- anabrid/cloud/cli/commands/compile.py +164 -0
- anabrid/cloud/cli/commands/info.py +97 -0
- anabrid/cloud/cli/commands/run.py +116 -0
- anabrid/cloud/cli/commands/status.py +20 -0
- anabrid/cloud/cli/commands/task/__init__.py +29 -0
- anabrid/cloud/cli/commands/task/delete.py +22 -0
- anabrid/cloud/cli/commands/task/info.py +66 -0
- anabrid/cloud/cli/commands/task/list.py +82 -0
- anabrid/cloud/cli/commands/task/logs.py +120 -0
- anabrid/cloud/cli/commands/task/result.py +64 -0
- anabrid/cloud/cli/commands/task/revoke.py +23 -0
- anabrid/cloud/cli/commands/task/search.py +238 -0
- anabrid/cloud/cli/commands/task/status.py +54 -0
- anabrid/cloud/cli/commands/task/wait.py +67 -0
- anabrid/cloud/cli/context.py +18 -0
- anabrid/cloud/cli/entity_tree.py +110 -0
- anabrid/cloud/cli/main.py +52 -0
- anabrid/cloud/cli/utils.py +421 -0
- analogcli-0.1.4.dist-info/METADATA +276 -0
- analogcli-0.1.4.dist-info/RECORD +25 -0
- analogcli-0.1.4.dist-info/WHEEL +4 -0
- analogcli-0.1.4.dist-info/entry_points.txt +5 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""CLI command for executing a compiled module on a physical device."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
import json
|
|
5
|
+
from ..context import get_client
|
|
6
|
+
from ..utils import (
|
|
7
|
+
handle_api_error,
|
|
8
|
+
load_binary_file,
|
|
9
|
+
wait_for_task_with_progress,
|
|
10
|
+
display_task_logs,
|
|
11
|
+
display_task_output,
|
|
12
|
+
console,
|
|
13
|
+
)
|
|
14
|
+
from anabrid.cloud.models.enums import TaskStateEnum
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@click.command()
|
|
18
|
+
@click.argument("module_file", type=click.Path(exists=True))
|
|
19
|
+
@click.option(
|
|
20
|
+
"--sample-rate",
|
|
21
|
+
default=10000,
|
|
22
|
+
type=int,
|
|
23
|
+
show_default=True,
|
|
24
|
+
help="ADC samples drawn per second",
|
|
25
|
+
)
|
|
26
|
+
@click.option(
|
|
27
|
+
"--op-time",
|
|
28
|
+
default=0.1,
|
|
29
|
+
type=float,
|
|
30
|
+
show_default=True,
|
|
31
|
+
help="Wall-clock integration time in seconds",
|
|
32
|
+
)
|
|
33
|
+
@click.option(
|
|
34
|
+
"--device",
|
|
35
|
+
help="Device name to run on. If omitted, the first available LUCIDAC is used.",
|
|
36
|
+
)
|
|
37
|
+
@click.option(
|
|
38
|
+
"--output",
|
|
39
|
+
"-o",
|
|
40
|
+
default=None,
|
|
41
|
+
help="Save run result JSON to this file path",
|
|
42
|
+
)
|
|
43
|
+
@click.option(
|
|
44
|
+
"--follow/--no-follow",
|
|
45
|
+
default=True,
|
|
46
|
+
show_default=True,
|
|
47
|
+
help="Wait for the task to finish; with --no-follow only the UUID is printed.",
|
|
48
|
+
)
|
|
49
|
+
@click.pass_context
|
|
50
|
+
@handle_api_error
|
|
51
|
+
def run(
|
|
52
|
+
ctx: click.Context,
|
|
53
|
+
module_file: str,
|
|
54
|
+
sample_rate: int,
|
|
55
|
+
op_time: float,
|
|
56
|
+
device: str | None,
|
|
57
|
+
output: str | None,
|
|
58
|
+
follow: bool,
|
|
59
|
+
):
|
|
60
|
+
"""Execute a compiled module on a physical LUCIDAC device.
|
|
61
|
+
|
|
62
|
+
MODULE_FILE should be a compiled protobuf configuration blob (.apb).
|
|
63
|
+
|
|
64
|
+
Example:
|
|
65
|
+
analogcli run compiled.apb --sample-rate 20000 --op-time 0.5 --device lucidac-01
|
|
66
|
+
"""
|
|
67
|
+
module_bytes = load_binary_file(module_file)
|
|
68
|
+
|
|
69
|
+
with get_client(ctx) as client:
|
|
70
|
+
if not device:
|
|
71
|
+
devices = client.get_devices()
|
|
72
|
+
lucidacs = [
|
|
73
|
+
d for d in devices if "lucidac" in d.name.lower() and d.available
|
|
74
|
+
]
|
|
75
|
+
if not lucidacs:
|
|
76
|
+
console.print("[red]No available LUCIDAC device found[/red]")
|
|
77
|
+
return
|
|
78
|
+
device = lucidacs[0].name
|
|
79
|
+
if follow:
|
|
80
|
+
console.print(f"[yellow]Using device: {device}[/yellow]")
|
|
81
|
+
|
|
82
|
+
if follow:
|
|
83
|
+
console.print(f"[blue]Submitting run task to device {device}...[/blue]")
|
|
84
|
+
task_id = client.submit_run_task(
|
|
85
|
+
device_name=device,
|
|
86
|
+
module=module_bytes,
|
|
87
|
+
sample_rate=sample_rate,
|
|
88
|
+
op_time=op_time,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
if not follow:
|
|
92
|
+
click.echo(task_id)
|
|
93
|
+
click.echo(f"Task submitted: {task_id}", err=True)
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
console.print(f"[green]Task submitted with ID: {task_id}[/green]")
|
|
97
|
+
|
|
98
|
+
final_status = wait_for_task_with_progress(client, task_id, "Running on device")
|
|
99
|
+
|
|
100
|
+
log_response = client.get_task_log(task_id)
|
|
101
|
+
display_task_logs(log_response.lines)
|
|
102
|
+
|
|
103
|
+
if final_status.state == TaskStateEnum.SUCCEEDED:
|
|
104
|
+
console.print("\n[bold green]Task completed successfully![/bold green]")
|
|
105
|
+
|
|
106
|
+
result = client.get_task_result(task_id)
|
|
107
|
+
console.print()
|
|
108
|
+
display_task_output(result)
|
|
109
|
+
|
|
110
|
+
if output:
|
|
111
|
+
with open(output, "w") as f:
|
|
112
|
+
json.dump(result, f, indent=2)
|
|
113
|
+
console.print(f"[green]Result saved to {output}[/green]")
|
|
114
|
+
else:
|
|
115
|
+
error = final_status.error or "(no detail)"
|
|
116
|
+
console.print(f"\n[bold red]Task failed: {error}[/bold red]")
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""CLI command for displaying system and device status."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from ..context import get_client
|
|
5
|
+
from ..utils import handle_api_error, display_system_info
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@click.command()
|
|
9
|
+
@click.pass_context
|
|
10
|
+
@handle_api_error
|
|
11
|
+
def status(ctx: click.Context):
|
|
12
|
+
"""Display system status and all connected devices.
|
|
13
|
+
|
|
14
|
+
Shows the hub domain, version, uptime, and a table of available
|
|
15
|
+
and unavailable devices with their current queue lengths.
|
|
16
|
+
"""
|
|
17
|
+
with get_client(ctx) as client:
|
|
18
|
+
system_info = client.get_system_status()
|
|
19
|
+
devices = client.get_devices()
|
|
20
|
+
display_system_info(system_info, devices)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Task management commands registered under ``analogcli task``."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from .delete import task_delete
|
|
6
|
+
from .info import task_info
|
|
7
|
+
from .list import task_list
|
|
8
|
+
from .logs import task_logs
|
|
9
|
+
from .result import task_result
|
|
10
|
+
from .revoke import task_revoke
|
|
11
|
+
from .search import task_search
|
|
12
|
+
from .status import task_status
|
|
13
|
+
from .wait import task_wait
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@click.group(name="task")
|
|
17
|
+
def task() -> None:
|
|
18
|
+
"""Operations on tasks (list / search / info / status / logs / result / wait / revoke / delete)."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
task.add_command(task_list, name="list")
|
|
22
|
+
task.add_command(task_search, name="search")
|
|
23
|
+
task.add_command(task_info, name="info")
|
|
24
|
+
task.add_command(task_status, name="status")
|
|
25
|
+
task.add_command(task_logs, name="logs")
|
|
26
|
+
task.add_command(task_result, name="result")
|
|
27
|
+
task.add_command(task_wait, name="wait")
|
|
28
|
+
task.add_command(task_revoke, name="revoke")
|
|
29
|
+
task.add_command(task_delete, name="delete")
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""``analogcli task delete`` — permanently delete a task and its blobs."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from ...context import get_client
|
|
6
|
+
from ...utils import handle_api_error
|
|
7
|
+
|
|
8
|
+
__all__ = ["task_delete", "get_client"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.command()
|
|
12
|
+
@click.argument("task_id")
|
|
13
|
+
@click.pass_context
|
|
14
|
+
@handle_api_error
|
|
15
|
+
def task_delete(ctx: click.Context, task_id: str) -> None:
|
|
16
|
+
"""Permanently delete *task_id* and its associated blobs.
|
|
17
|
+
|
|
18
|
+
Stdout stays empty on success; a 409 state conflict is mapped to
|
|
19
|
+
exit code 4 by the ``handle_api_error`` decorator.
|
|
20
|
+
"""
|
|
21
|
+
with get_client(ctx) as client:
|
|
22
|
+
client.delete_task(task_id)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""``analogcli task info`` — detailed task view."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from rich.panel import Panel
|
|
5
|
+
|
|
6
|
+
from ...context import get_client
|
|
7
|
+
from ...utils import (
|
|
8
|
+
OutputMode,
|
|
9
|
+
console,
|
|
10
|
+
dump_task_json,
|
|
11
|
+
format_timestamp,
|
|
12
|
+
handle_api_error,
|
|
13
|
+
pick_output_mode,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@click.command()
|
|
18
|
+
@click.argument("task_id")
|
|
19
|
+
@click.option(
|
|
20
|
+
"--format",
|
|
21
|
+
"format_",
|
|
22
|
+
type=click.Choice(["text", "json"]),
|
|
23
|
+
default=None,
|
|
24
|
+
help="Force the output format; defaults to auto-detect from TTY.",
|
|
25
|
+
)
|
|
26
|
+
@click.pass_context
|
|
27
|
+
@handle_api_error
|
|
28
|
+
def task_info(ctx: click.Context, task_id: str, format_: str | None) -> None:
|
|
29
|
+
"""Show detailed information for a single task."""
|
|
30
|
+
with get_client(ctx) as client:
|
|
31
|
+
detail = client.get_task_detail(task_id)
|
|
32
|
+
|
|
33
|
+
mode = pick_output_mode(ctx, format_, follow=False)
|
|
34
|
+
if mode == OutputMode.JSON:
|
|
35
|
+
dump_task_json(detail)
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
type_val = detail.type
|
|
39
|
+
type_str = type_val.value if hasattr(type_val, "value") else str(type_val)
|
|
40
|
+
state_val = detail.state
|
|
41
|
+
state_str = state_val.value if hasattr(state_val, "value") else str(state_val)
|
|
42
|
+
created_str = format_timestamp(detail.created_at)
|
|
43
|
+
|
|
44
|
+
lines = [
|
|
45
|
+
f"[cyan]ID:[/cyan] {detail.id}",
|
|
46
|
+
f"[cyan]Type:[/cyan] {type_str}",
|
|
47
|
+
f"[cyan]State:[/cyan] {state_str}",
|
|
48
|
+
f"[cyan]Created:[/cyan] {created_str}",
|
|
49
|
+
]
|
|
50
|
+
if getattr(detail, "error", None):
|
|
51
|
+
lines.append(f"[red]Error:[/red] {detail.error}")
|
|
52
|
+
if hasattr(detail, "backend"):
|
|
53
|
+
backend = detail.backend
|
|
54
|
+
backend_str = backend.value if hasattr(backend, "value") else str(backend)
|
|
55
|
+
lines.append(f"[cyan]Backend:[/cyan] {backend_str}")
|
|
56
|
+
lines.append(f"[cyan]k0:[/cyan] {detail.k0}")
|
|
57
|
+
if hasattr(detail, "device_name"):
|
|
58
|
+
lines.append(f"[cyan]Device:[/cyan] {detail.device_name}")
|
|
59
|
+
lines.append(f"[cyan]Sample rate:[/cyan] {detail.sample_rate}")
|
|
60
|
+
lines.append(f"[cyan]Op time:[/cyan] {detail.op_time}")
|
|
61
|
+
if hasattr(detail, "has_result"):
|
|
62
|
+
lines.append(f"[cyan]Has result:[/cyan] {detail.has_result}")
|
|
63
|
+
if hasattr(detail, "has_log"):
|
|
64
|
+
lines.append(f"[cyan]Has log:[/cyan] {detail.has_log}")
|
|
65
|
+
|
|
66
|
+
console.print(Panel("\n".join(lines), title=f"Task {detail.id}"))
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""``analogcli task list`` — paginated, filterable task listing."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from ...context import get_client
|
|
6
|
+
from ...utils import (
|
|
7
|
+
OutputMode,
|
|
8
|
+
dump_task_json,
|
|
9
|
+
handle_api_error,
|
|
10
|
+
pick_output_mode,
|
|
11
|
+
print_task_table,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@click.command()
|
|
16
|
+
@click.option(
|
|
17
|
+
"--state",
|
|
18
|
+
default=None,
|
|
19
|
+
help="Filter tasks by state (e.g. QUEUED, INPROGRESS, SUCCEEDED).",
|
|
20
|
+
)
|
|
21
|
+
@click.option(
|
|
22
|
+
"--type",
|
|
23
|
+
"task_type",
|
|
24
|
+
default=None,
|
|
25
|
+
help="Filter tasks by type (e.g. compile, run).",
|
|
26
|
+
)
|
|
27
|
+
@click.option(
|
|
28
|
+
"--limit",
|
|
29
|
+
default=None,
|
|
30
|
+
type=int,
|
|
31
|
+
help="Maximum number of tasks to return.",
|
|
32
|
+
)
|
|
33
|
+
@click.option(
|
|
34
|
+
"--offset",
|
|
35
|
+
default=None,
|
|
36
|
+
type=int,
|
|
37
|
+
help="Number of tasks to skip before the page.",
|
|
38
|
+
)
|
|
39
|
+
@click.option(
|
|
40
|
+
"--ids-only",
|
|
41
|
+
is_flag=True,
|
|
42
|
+
default=False,
|
|
43
|
+
help="Emit one UUID per line on stdout (overrides --format).",
|
|
44
|
+
)
|
|
45
|
+
@click.option(
|
|
46
|
+
"--format",
|
|
47
|
+
"format_",
|
|
48
|
+
type=click.Choice(["text", "json"]),
|
|
49
|
+
default=None,
|
|
50
|
+
help="Force the output format; defaults to auto-detect from TTY.",
|
|
51
|
+
)
|
|
52
|
+
@click.pass_context
|
|
53
|
+
@handle_api_error
|
|
54
|
+
def task_list(
|
|
55
|
+
ctx: click.Context,
|
|
56
|
+
state: str | None,
|
|
57
|
+
task_type: str | None,
|
|
58
|
+
limit: int | None,
|
|
59
|
+
offset: int | None,
|
|
60
|
+
ids_only: bool,
|
|
61
|
+
format_: str | None,
|
|
62
|
+
) -> None:
|
|
63
|
+
"""List tasks visible to the current API key."""
|
|
64
|
+
with get_client(ctx) as client:
|
|
65
|
+
tasks = client.list_tasks(
|
|
66
|
+
state=state,
|
|
67
|
+
type=task_type,
|
|
68
|
+
limit=limit,
|
|
69
|
+
offset=offset,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
if ids_only:
|
|
73
|
+
for task in tasks:
|
|
74
|
+
task_id = task.id if hasattr(task, "id") else task["id"]
|
|
75
|
+
click.echo(str(task_id))
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
mode = pick_output_mode(ctx, format_, follow=False)
|
|
79
|
+
if mode == OutputMode.JSON:
|
|
80
|
+
dump_task_json(tasks)
|
|
81
|
+
else:
|
|
82
|
+
print_task_table(tasks)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""``analogcli task logs`` — fetch (and optionally follow) a task's log output."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from rich.panel import Panel
|
|
7
|
+
|
|
8
|
+
from ...context import get_client
|
|
9
|
+
from ...utils import (
|
|
10
|
+
OutputMode,
|
|
11
|
+
console,
|
|
12
|
+
dump_task_json,
|
|
13
|
+
handle_api_error,
|
|
14
|
+
pick_output_mode,
|
|
15
|
+
print_task_logs_plain,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = ["task_logs", "get_client", "time", "POLL_INTERVAL"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
POLL_INTERVAL: float = 1.0
|
|
22
|
+
"""Seconds between polls in ``--follow`` mode. Patched to 0 in tests."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
_TERMINAL_STATE_VALUES = frozenset({"succeeded", "failed", "cancelled"})
|
|
26
|
+
"""Lowercase wire-level values considered terminal for follow-mode polling."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _state_value(state: object) -> str:
|
|
30
|
+
"""Return the lowercase string value of a state enum or raw string."""
|
|
31
|
+
raw = getattr(state, "value", state)
|
|
32
|
+
return str(raw).lower()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _pick_logs_output_mode(
|
|
36
|
+
ctx: click.Context | None, format_: str | None
|
|
37
|
+
) -> OutputMode:
|
|
38
|
+
"""Pick the output mode for ``task logs`` snapshot output.
|
|
39
|
+
|
|
40
|
+
Logs are text-native: the natural downstream consumer of piped log
|
|
41
|
+
output is ``grep``, not ``jq``. That makes the JSON-by-default
|
|
42
|
+
behaviour of the shared ``pick_output_mode`` (designed for
|
|
43
|
+
structured commands like ``task list``) the wrong choice here. This
|
|
44
|
+
helper overrides the auto-detect branch to default to TEXT off-TTY
|
|
45
|
+
while still honouring an explicit ``--format`` verbatim, so the
|
|
46
|
+
user can opt into JSON when they want it.
|
|
47
|
+
"""
|
|
48
|
+
if format_ is not None:
|
|
49
|
+
return pick_output_mode(ctx, format_, follow=False)
|
|
50
|
+
if _stdout_is_tty():
|
|
51
|
+
return OutputMode.RICH
|
|
52
|
+
return OutputMode.TEXT
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _stdout_is_tty() -> bool:
|
|
56
|
+
"""Return True if ``sys.stdout`` reports being attached to a TTY."""
|
|
57
|
+
import sys
|
|
58
|
+
|
|
59
|
+
isatty = getattr(sys.stdout, "isatty", None)
|
|
60
|
+
if isatty is None:
|
|
61
|
+
return False
|
|
62
|
+
try:
|
|
63
|
+
return bool(isatty())
|
|
64
|
+
except Exception:
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@click.command()
|
|
69
|
+
@click.argument("task_id")
|
|
70
|
+
@click.option(
|
|
71
|
+
"--follow",
|
|
72
|
+
is_flag=True,
|
|
73
|
+
default=False,
|
|
74
|
+
help="Continuously poll for new log lines until the task reaches a terminal state.",
|
|
75
|
+
)
|
|
76
|
+
@click.option(
|
|
77
|
+
"--format",
|
|
78
|
+
"format_",
|
|
79
|
+
type=click.Choice(["text", "json"]),
|
|
80
|
+
default=None,
|
|
81
|
+
help="Force the output format; defaults to auto-detect from TTY.",
|
|
82
|
+
)
|
|
83
|
+
@click.pass_context
|
|
84
|
+
@handle_api_error
|
|
85
|
+
def task_logs(
|
|
86
|
+
ctx: click.Context, task_id: str, follow: bool, format_: str | None
|
|
87
|
+
) -> None:
|
|
88
|
+
"""Show the log output for *task_id*.
|
|
89
|
+
|
|
90
|
+
Snapshot mode (default): fetch once and render to stdout. On a TTY the
|
|
91
|
+
log lines appear in a Rich panel; off a TTY the raw newline-joined
|
|
92
|
+
lines are printed so ``grep`` works.
|
|
93
|
+
|
|
94
|
+
With ``--follow``: poll periodically and emit only newly appended lines
|
|
95
|
+
until the task reaches a terminal state.
|
|
96
|
+
"""
|
|
97
|
+
with get_client(ctx) as client:
|
|
98
|
+
if not follow:
|
|
99
|
+
log = client.get_task_log(task_id)
|
|
100
|
+
lines = log.lines
|
|
101
|
+
mode = _pick_logs_output_mode(ctx, format_)
|
|
102
|
+
if mode == OutputMode.JSON:
|
|
103
|
+
dump_task_json(log)
|
|
104
|
+
elif mode == OutputMode.TEXT:
|
|
105
|
+
print_task_logs_plain(lines)
|
|
106
|
+
else:
|
|
107
|
+
console.print(Panel("\n".join(lines), title="Task logs"))
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
printed = 0
|
|
111
|
+
while True:
|
|
112
|
+
status = client.get_task_status(task_id)
|
|
113
|
+
log = client.get_task_log(task_id)
|
|
114
|
+
lines = log.lines
|
|
115
|
+
for line in lines[printed:]:
|
|
116
|
+
click.echo(line)
|
|
117
|
+
printed = len(lines)
|
|
118
|
+
if _state_value(status.state) in _TERMINAL_STATE_VALUES:
|
|
119
|
+
break
|
|
120
|
+
time.sleep(POLL_INTERVAL)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""``analogcli task result`` — fetch a task's result payload (compile bytes or run JSON)."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from ...context import get_client
|
|
9
|
+
from ...utils import handle_api_error
|
|
10
|
+
|
|
11
|
+
__all__ = ["task_result", "get_client"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _to_jsonable(payload: object) -> object:
|
|
15
|
+
"""Return a JSON-serialisable representation of *payload*.
|
|
16
|
+
|
|
17
|
+
Pydantic models are dumped via ``model_dump(mode="json")``; everything
|
|
18
|
+
else is returned unchanged so ``json.dumps(..., default=str)`` can
|
|
19
|
+
handle leaf types.
|
|
20
|
+
"""
|
|
21
|
+
if hasattr(payload, "model_dump"):
|
|
22
|
+
return payload.model_dump(mode="json")
|
|
23
|
+
return payload
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@click.command()
|
|
27
|
+
@click.argument("task_id")
|
|
28
|
+
@click.option(
|
|
29
|
+
"-o",
|
|
30
|
+
"--output",
|
|
31
|
+
"output",
|
|
32
|
+
type=click.Path(dir_okay=False, writable=True),
|
|
33
|
+
default=None,
|
|
34
|
+
help="Write the result to FILE instead of stdout.",
|
|
35
|
+
)
|
|
36
|
+
@click.pass_context
|
|
37
|
+
@handle_api_error
|
|
38
|
+
def task_result(ctx: click.Context, task_id: str, output: str | None) -> None:
|
|
39
|
+
"""Fetch the result payload for *task_id*.
|
|
40
|
+
|
|
41
|
+
Compile tasks return a binary protobuf blob; run tasks return a JSON
|
|
42
|
+
document. The discrimination is driven by the wire-level content-type
|
|
43
|
+
returned from ``client.get_task_result``.
|
|
44
|
+
|
|
45
|
+
When ``-o FILE`` is given the payload is written to that path; otherwise
|
|
46
|
+
it is written to stdout (binary for compile, JSON for run).
|
|
47
|
+
"""
|
|
48
|
+
with get_client(ctx) as client:
|
|
49
|
+
payload = client.get_task_result(task_id)
|
|
50
|
+
|
|
51
|
+
if isinstance(payload, bytes):
|
|
52
|
+
if output is not None:
|
|
53
|
+
with open(output, "wb") as f:
|
|
54
|
+
f.write(payload)
|
|
55
|
+
else:
|
|
56
|
+
sys.stdout.buffer.write(payload)
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
jsonable = _to_jsonable(payload)
|
|
60
|
+
if output is not None:
|
|
61
|
+
with open(output, "w") as f:
|
|
62
|
+
json.dump(jsonable, f, indent=2, default=str)
|
|
63
|
+
else:
|
|
64
|
+
click.echo(json.dumps(jsonable, indent=2, default=str))
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""``analogcli task revoke`` — request cancellation of a queued/running task."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from ...context import get_client
|
|
6
|
+
from ...utils import handle_api_error
|
|
7
|
+
|
|
8
|
+
__all__ = ["task_revoke", "get_client"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.command()
|
|
12
|
+
@click.argument("task_id")
|
|
13
|
+
@click.pass_context
|
|
14
|
+
@handle_api_error
|
|
15
|
+
def task_revoke(ctx: click.Context, task_id: str) -> None:
|
|
16
|
+
"""Request cancellation of *task_id*.
|
|
17
|
+
|
|
18
|
+
Wire-level endpoint is unchanged (``POST /tasks/{id}/cancel``).
|
|
19
|
+
Stdout stays empty on success; a 409 state conflict is mapped to
|
|
20
|
+
exit code 4 by the ``handle_api_error`` decorator.
|
|
21
|
+
"""
|
|
22
|
+
with get_client(ctx) as client:
|
|
23
|
+
client.cancel_task(task_id)
|