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,238 @@
|
|
|
1
|
+
"""``analogcli task search`` — cross-user task search, report table + CSV export."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from datetime import datetime, timedelta, timezone
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from ...context import get_client
|
|
9
|
+
from ...utils import (
|
|
10
|
+
OutputMode,
|
|
11
|
+
handle_api_error,
|
|
12
|
+
pick_output_mode,
|
|
13
|
+
print_search_table,
|
|
14
|
+
write_task_csv,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
# Column sets for the report table and CSV export.
|
|
18
|
+
THIN_COLUMNS = ["id", "user_id", "type", "state", "created_at", "error"]
|
|
19
|
+
RICH_COLUMNS = THIN_COLUMNS + [
|
|
20
|
+
"device_name",
|
|
21
|
+
"backend",
|
|
22
|
+
"sample_rate",
|
|
23
|
+
"op_time",
|
|
24
|
+
"k0",
|
|
25
|
+
"has_result",
|
|
26
|
+
"has_log",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
# Per-request page size when gathering all matches across pages.
|
|
30
|
+
PAGE_SIZE = 1000
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@click.command()
|
|
34
|
+
@click.option("--user-id", "user_id", multiple=True,
|
|
35
|
+
help="Filter by owning api_key (honoured only for system keys). Repeatable.")
|
|
36
|
+
@click.option("--me", is_flag=True, default=False,
|
|
37
|
+
help="Shortcut for --user-id $ANALOGCLOUD_API_KEY (your own jobs). "
|
|
38
|
+
"Combines with any --user-id values given.")
|
|
39
|
+
@click.option("--device", "device_name", multiple=True,
|
|
40
|
+
help="Filter by device name (matches run jobs). Repeatable.")
|
|
41
|
+
@click.option("--device-class", "device_class", multiple=True,
|
|
42
|
+
help="Filter by device class: LUCIDAC, REDAC or SIMULATOR. Repeatable.")
|
|
43
|
+
@click.option("--type", "task_type", multiple=True,
|
|
44
|
+
help="Filter by task type: compile or run. Repeatable.")
|
|
45
|
+
@click.option("--state", multiple=True,
|
|
46
|
+
help="Filter by latest state, e.g. QUEUED, INPROGRESS, SUCCEEDED. Repeatable.")
|
|
47
|
+
@click.option("--from", "from_", default=None,
|
|
48
|
+
help="Window start as an ISO 8601 datetime, e.g. 2026-05-01T00:00:00 "
|
|
49
|
+
"(default: 24h before --to). Mutually exclusive with --last-*.")
|
|
50
|
+
@click.option("--to", "to", default=None,
|
|
51
|
+
help="Window end as an ISO 8601 datetime, e.g. 2026-05-08T00:00:00 "
|
|
52
|
+
"(default: now). Window capped at 90 days. Mutually exclusive with --last-*.")
|
|
53
|
+
@click.option("--last-h", "last_h", default=None, type=int,
|
|
54
|
+
help="Show the last N hours (window = now-Nh .. now). "
|
|
55
|
+
"Excludes --from/--to and the other --last-* options.")
|
|
56
|
+
@click.option("--last-d", "last_d", default=None, type=int,
|
|
57
|
+
help="Show the last N days. Excludes --from/--to and the other --last-* options.")
|
|
58
|
+
@click.option("--last-m", "last_m", default=None, type=int,
|
|
59
|
+
help="Show the last N minutes. Excludes --from/--to and the other --last-* options.")
|
|
60
|
+
@click.option("--limit", default=None, type=int,
|
|
61
|
+
help="Cap the total number of jobs returned (default: all matches).")
|
|
62
|
+
@click.option("--recursive", is_flag=True, default=False,
|
|
63
|
+
help="Enrich each job with full detail (device/backend/...) via per-job lookups.")
|
|
64
|
+
@click.option("--csv", "csv_path", default=None, type=click.Path(dir_okay=False, writable=True),
|
|
65
|
+
help="Also write the result set to this path as CSV.")
|
|
66
|
+
@click.option("--ids-only", is_flag=True, default=False,
|
|
67
|
+
help="Emit one UUID per line on stdout (overrides --format).")
|
|
68
|
+
@click.option("--format", "format_", type=click.Choice(["text", "json"]), default=None,
|
|
69
|
+
help="Force the output format; defaults to auto-detect from TTY.")
|
|
70
|
+
@click.pass_context
|
|
71
|
+
@handle_api_error
|
|
72
|
+
def task_search(
|
|
73
|
+
ctx: click.Context,
|
|
74
|
+
user_id: tuple[str, ...],
|
|
75
|
+
me: bool,
|
|
76
|
+
device_name: tuple[str, ...],
|
|
77
|
+
device_class: tuple[str, ...],
|
|
78
|
+
task_type: tuple[str, ...],
|
|
79
|
+
state: tuple[str, ...],
|
|
80
|
+
from_: str | None,
|
|
81
|
+
to: str | None,
|
|
82
|
+
last_h: int | None,
|
|
83
|
+
last_d: int | None,
|
|
84
|
+
last_m: int | None,
|
|
85
|
+
limit: int | None,
|
|
86
|
+
recursive: bool,
|
|
87
|
+
csv_path: str | None,
|
|
88
|
+
ids_only: bool,
|
|
89
|
+
format_: str | None,
|
|
90
|
+
) -> None:
|
|
91
|
+
"""Search tasks and render a report table, optionally exporting CSV.
|
|
92
|
+
|
|
93
|
+
System keys search across all users (and may filter by ``--user-id``);
|
|
94
|
+
non-system keys are scoped to their own tasks. All matches are gathered
|
|
95
|
+
across pages up to ``--limit``.
|
|
96
|
+
|
|
97
|
+
\b
|
|
98
|
+
Time window — pick ONE style:
|
|
99
|
+
* --from / --to with ISO 8601 datetimes (e.g. 2026-05-01T00:00:00).
|
|
100
|
+
A bare datetime is read as UTC; a value with an offset is converted.
|
|
101
|
+
* --last-h / --last-d / --last-m for a window ending "now". These are
|
|
102
|
+
mutually exclusive with --from/--to and with each other.
|
|
103
|
+
|
|
104
|
+
\b
|
|
105
|
+
Examples:
|
|
106
|
+
# explicit ISO 8601 window (one week)
|
|
107
|
+
analogcli task search --from 2026-05-01T00:00:00 --to 2026-05-08T00:00:00
|
|
108
|
+
# from a fixed start up to the current moment; generate the ISO
|
|
109
|
+
# timestamp for "now" with the Linux `date` command:
|
|
110
|
+
analogcli task search --from 2026-05-01T00:00:00 --to "$(date -u +%Y-%m-%dT%H:%M:%S)"
|
|
111
|
+
# the last 6 hours / 7 days / 90 minutes
|
|
112
|
+
analogcli task search --last-h 6
|
|
113
|
+
analogcli task search --last-d 7
|
|
114
|
+
analogcli task search --last-m 90
|
|
115
|
+
# multiple owners (system key) — repeat the flag
|
|
116
|
+
analogcli task search --user-id alice-key --user-id bob-key
|
|
117
|
+
# just your own jobs (handy for a system key, which sees everyone by default)
|
|
118
|
+
analogcli task search --me
|
|
119
|
+
"""
|
|
120
|
+
# Resolve the time window: --last-* is a convenience over --from/--to and
|
|
121
|
+
# may not be mixed with them or with each other.
|
|
122
|
+
last_opts = {"--last-h": last_h, "--last-d": last_d, "--last-m": last_m}
|
|
123
|
+
given_last = {name: val for name, val in last_opts.items() if val is not None}
|
|
124
|
+
if len(given_last) > 1:
|
|
125
|
+
raise click.UsageError(
|
|
126
|
+
"Only one of --last-h / --last-d / --last-m may be used at a time."
|
|
127
|
+
)
|
|
128
|
+
if given_last and (from_ is not None or to is not None):
|
|
129
|
+
raise click.UsageError(
|
|
130
|
+
"--last-h / --last-d / --last-m cannot be combined with --from / --to."
|
|
131
|
+
)
|
|
132
|
+
if given_last:
|
|
133
|
+
deltas = {
|
|
134
|
+
"--last-h": timedelta(hours=last_h or 0),
|
|
135
|
+
"--last-d": timedelta(days=last_d or 0),
|
|
136
|
+
"--last-m": timedelta(minutes=last_m or 0),
|
|
137
|
+
}
|
|
138
|
+
(name,) = given_last
|
|
139
|
+
if given_last[name] < 0:
|
|
140
|
+
raise click.UsageError(f"{name} must not be negative.")
|
|
141
|
+
now = datetime.now(timezone.utc)
|
|
142
|
+
to = now.isoformat()
|
|
143
|
+
from_ = (now - deltas[name]).isoformat()
|
|
144
|
+
|
|
145
|
+
_to_list = lambda t: list(t) if t else None # noqa: E731
|
|
146
|
+
|
|
147
|
+
uid_filter = _to_list(user_id)
|
|
148
|
+
dev_filter = _to_list(device_name)
|
|
149
|
+
cls_filter = _to_list(device_class)
|
|
150
|
+
type_filter = _to_list(task_type)
|
|
151
|
+
state_filter = _to_list(state)
|
|
152
|
+
|
|
153
|
+
columns = RICH_COLUMNS if recursive else THIN_COLUMNS
|
|
154
|
+
|
|
155
|
+
collected = []
|
|
156
|
+
offset = 0
|
|
157
|
+
|
|
158
|
+
with get_client(ctx) as client:
|
|
159
|
+
# --me folds the caller's own api_key into the user_id filter (deduped,
|
|
160
|
+
# order-preserving). For a non-system key the server force-scopes to
|
|
161
|
+
# the caller anyway, so this is a no-op there; it matters for a system
|
|
162
|
+
# key that would otherwise see every user's jobs.
|
|
163
|
+
if me:
|
|
164
|
+
uid_filter = list(dict.fromkeys((uid_filter or []) + [client.api_key]))
|
|
165
|
+
|
|
166
|
+
# Pagination loop — gather all matching pages up to limit.
|
|
167
|
+
while True:
|
|
168
|
+
remaining = None if limit is None else limit - len(collected)
|
|
169
|
+
if remaining is not None and remaining <= 0:
|
|
170
|
+
break
|
|
171
|
+
req_limit = PAGE_SIZE if remaining is None else min(PAGE_SIZE, remaining)
|
|
172
|
+
resp = client.search_tasks(
|
|
173
|
+
user_id=uid_filter,
|
|
174
|
+
device_name=dev_filter,
|
|
175
|
+
device_class=cls_filter,
|
|
176
|
+
type=type_filter,
|
|
177
|
+
state=state_filter,
|
|
178
|
+
from_=from_,
|
|
179
|
+
to=to,
|
|
180
|
+
limit=req_limit,
|
|
181
|
+
offset=offset,
|
|
182
|
+
)
|
|
183
|
+
collected.extend(resp.items)
|
|
184
|
+
if resp.next_offset is None:
|
|
185
|
+
break
|
|
186
|
+
offset = resp.next_offset
|
|
187
|
+
|
|
188
|
+
if limit is not None:
|
|
189
|
+
collected = collected[:limit]
|
|
190
|
+
|
|
191
|
+
# Optionally enrich each item with per-job detail.
|
|
192
|
+
details = {}
|
|
193
|
+
if recursive:
|
|
194
|
+
for item in collected:
|
|
195
|
+
details[item.id] = client.get_task_detail(str(item.id))
|
|
196
|
+
|
|
197
|
+
# Build flat row dicts.
|
|
198
|
+
def _attr_val(obj, name: str) -> str:
|
|
199
|
+
"""Return a string rendering of *name* on *obj*; None or missing → ''."""
|
|
200
|
+
val = getattr(obj, name, None)
|
|
201
|
+
if val is None:
|
|
202
|
+
return ""
|
|
203
|
+
if hasattr(val, "value"):
|
|
204
|
+
return str(val.value)
|
|
205
|
+
return str(val)
|
|
206
|
+
|
|
207
|
+
rows = []
|
|
208
|
+
for item in collected:
|
|
209
|
+
row = {
|
|
210
|
+
"id": str(item.id),
|
|
211
|
+
"user_id": item.user_id,
|
|
212
|
+
"type": item.type.value,
|
|
213
|
+
"state": item.state.value,
|
|
214
|
+
"created_at": item.created_at.isoformat(),
|
|
215
|
+
"error": item.error or "",
|
|
216
|
+
}
|
|
217
|
+
if recursive:
|
|
218
|
+
detail = details.get(item.id)
|
|
219
|
+
for col in ["device_name", "backend", "sample_rate", "op_time", "k0", "has_result", "has_log"]:
|
|
220
|
+
row[col] = _attr_val(detail, col) if detail is not None else ""
|
|
221
|
+
rows.append(row)
|
|
222
|
+
|
|
223
|
+
# --ids-only overrides --format.
|
|
224
|
+
if ids_only:
|
|
225
|
+
for item in collected:
|
|
226
|
+
click.echo(str(item.id))
|
|
227
|
+
if csv_path is not None:
|
|
228
|
+
write_task_csv(rows, csv_path, columns)
|
|
229
|
+
return
|
|
230
|
+
|
|
231
|
+
mode = pick_output_mode(ctx, format_, follow=False)
|
|
232
|
+
if mode == OutputMode.JSON:
|
|
233
|
+
click.echo(json.dumps(rows, indent=2, default=str))
|
|
234
|
+
else:
|
|
235
|
+
print_search_table(rows, columns)
|
|
236
|
+
|
|
237
|
+
if csv_path is not None:
|
|
238
|
+
write_task_csv(rows, csv_path, columns)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""``analogcli task status`` — current state of a single task."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from rich.table import Table
|
|
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_status(ctx: click.Context, task_id: str, format_: str | None) -> None:
|
|
29
|
+
"""Show the current state and state history of a single task."""
|
|
30
|
+
with get_client(ctx) as client:
|
|
31
|
+
status_obj = client.get_task_status(task_id)
|
|
32
|
+
|
|
33
|
+
mode = pick_output_mode(ctx, format_, follow=False)
|
|
34
|
+
if mode == OutputMode.JSON:
|
|
35
|
+
dump_task_json(status_obj)
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
state_val = status_obj.state
|
|
39
|
+
state_str = state_val.value if hasattr(state_val, "value") else str(state_val)
|
|
40
|
+
console.print(f"[cyan]ID:[/cyan] {status_obj.id}")
|
|
41
|
+
console.print(f"[cyan]State:[/cyan] {state_str}")
|
|
42
|
+
if status_obj.error:
|
|
43
|
+
console.print(f"[red]Error:[/red] {status_obj.error}")
|
|
44
|
+
|
|
45
|
+
table = Table(title="State history")
|
|
46
|
+
table.add_column("Timestamp", style="white")
|
|
47
|
+
table.add_column("State", style="green")
|
|
48
|
+
for entry in status_obj.state_history:
|
|
49
|
+
entry_state = entry.state
|
|
50
|
+
entry_state_str = (
|
|
51
|
+
entry_state.value if hasattr(entry_state, "value") else str(entry_state)
|
|
52
|
+
)
|
|
53
|
+
table.add_row(format_timestamp(entry.timestamp), entry_state_str)
|
|
54
|
+
console.print(table)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""``analogcli task wait`` — poll a task until terminal; exit code encodes the state."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from ...context import get_client
|
|
9
|
+
from ...utils import handle_api_error
|
|
10
|
+
|
|
11
|
+
__all__ = ["task_wait", "get_client", "time", "_now"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _now() -> float:
|
|
15
|
+
"""Return a monotonic timestamp in seconds for elapsed-time computation.
|
|
16
|
+
|
|
17
|
+
Wrapped in a helper so tests can patch a single, named symbol on this
|
|
18
|
+
module rather than monkey-patching ``time.monotonic`` globally.
|
|
19
|
+
"""
|
|
20
|
+
return time.monotonic()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@click.command()
|
|
24
|
+
@click.argument("task_id")
|
|
25
|
+
@click.option(
|
|
26
|
+
"--poll-interval",
|
|
27
|
+
"poll_interval",
|
|
28
|
+
type=float,
|
|
29
|
+
default=1.0,
|
|
30
|
+
show_default=True,
|
|
31
|
+
help="Seconds between status polls.",
|
|
32
|
+
)
|
|
33
|
+
@click.option(
|
|
34
|
+
"--timeout",
|
|
35
|
+
"timeout",
|
|
36
|
+
type=float,
|
|
37
|
+
default=None,
|
|
38
|
+
help="Abort polling after this many seconds and exit 124.",
|
|
39
|
+
)
|
|
40
|
+
@click.pass_context
|
|
41
|
+
@handle_api_error
|
|
42
|
+
def task_wait(
|
|
43
|
+
ctx: click.Context,
|
|
44
|
+
task_id: str,
|
|
45
|
+
poll_interval: float,
|
|
46
|
+
timeout: float | None,
|
|
47
|
+
) -> None:
|
|
48
|
+
"""Wait until *task_id* reaches a terminal state.
|
|
49
|
+
|
|
50
|
+
Stdout stays empty; the terminal state is carried by the exit code:
|
|
51
|
+
0 = SUCCEEDED, 1 = FAILED, 2 = CANCELLED, 124 = timeout.
|
|
52
|
+
"""
|
|
53
|
+
start = _now()
|
|
54
|
+
with get_client(ctx) as client:
|
|
55
|
+
while True:
|
|
56
|
+
status = client.get_task_status(task_id)
|
|
57
|
+
raw_state = getattr(status.state, "value", status.state)
|
|
58
|
+
state = str(raw_state).lower()
|
|
59
|
+
if state == "succeeded":
|
|
60
|
+
sys.exit(0)
|
|
61
|
+
if state == "failed":
|
|
62
|
+
sys.exit(1)
|
|
63
|
+
if state == "cancelled":
|
|
64
|
+
sys.exit(2)
|
|
65
|
+
if timeout is not None and (_now() - start) >= timeout:
|
|
66
|
+
sys.exit(124)
|
|
67
|
+
time.sleep(poll_interval)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import click
|
|
3
|
+
from .client import AnalogCloudClient
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_client(ctx: click.Context) -> AnalogCloudClient:
|
|
7
|
+
"""Create and return an Analog Cloud client with context configuration."""
|
|
8
|
+
config = ctx.obj["client_config"]
|
|
9
|
+
|
|
10
|
+
# Read API key from environment
|
|
11
|
+
api_key = os.environ.get("ANALOGCLOUD_API_KEY")
|
|
12
|
+
if not api_key:
|
|
13
|
+
raise click.ClickException(
|
|
14
|
+
"ANALOGCLOUD_API_KEY environment variable is not set. "
|
|
15
|
+
"Please set it to your Analog Cloud API key."
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
return AnalogCloudClient(**config, api_key=api_key)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Entity-blob -> pybrid ``Computer`` construction and ASCII-tree rendering.
|
|
2
|
+
|
|
3
|
+
The helpers in this module let the CLI render a device's entity tree without
|
|
4
|
+
a live hardware connection. The entity blob is the raw ``pb.Module`` bytes
|
|
5
|
+
returned by the server's ``/api/v1/devices/{name}/entity`` endpoint.
|
|
6
|
+
|
|
7
|
+
Pybrid is imported lazily inside each function so callers that do not need
|
|
8
|
+
tree rendering (e.g. ``analogcli info --short``) don't pay the pybrid import
|
|
9
|
+
cost.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import TYPE_CHECKING
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from pybrid.redac.computer import REDAC as Computer
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
_SUPPORTED_DEVICE_TYPES = frozenset({"LUCIDAC", "REDAC"})
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def build_computer_from_entity(entity_bytes: bytes, device_type: str) -> "Computer":
|
|
24
|
+
"""Deserialize an entity blob into a populated pybrid ``Computer``.
|
|
25
|
+
|
|
26
|
+
The server stores the entity as a ``pb.File`` envelope wrapping the
|
|
27
|
+
raw ``pb.Module`` (see ``devices._fetch_entity``); this helper parses
|
|
28
|
+
the envelope, takes ``.module``, instantiates a ``LUCIDACController``
|
|
29
|
+
or ``REDACController`` based on *device_type*, walks the module's
|
|
30
|
+
entity-specification items through a ``LUCIDACDeserializer``, and
|
|
31
|
+
attaches the resulting carriers to the controller's ``.computer``.
|
|
32
|
+
No network or hardware access is performed.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
entity_bytes: Raw serialized ``pb.File`` bytes (as returned by
|
|
36
|
+
the server's device-entity endpoint).
|
|
37
|
+
device_type: ``"LUCIDAC"`` or ``"REDAC"`` - drives the
|
|
38
|
+
controller-class selection.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
A pybrid ``Computer`` (``LUCIDAC`` or ``REDAC`` subclass) with
|
|
42
|
+
carriers populated from the entity blob.
|
|
43
|
+
|
|
44
|
+
Raises:
|
|
45
|
+
ValueError: If *device_type* is not a recognised physical-device
|
|
46
|
+
type (e.g. ``"SIMULATOR"``) or if *entity_bytes* cannot be
|
|
47
|
+
parsed as a ``pb.File``.
|
|
48
|
+
"""
|
|
49
|
+
if device_type not in _SUPPORTED_DEVICE_TYPES:
|
|
50
|
+
raise ValueError(
|
|
51
|
+
f"Unsupported device_type {device_type!r}; "
|
|
52
|
+
f"expected one of {sorted(_SUPPORTED_DEVICE_TYPES)}"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
import pybrid.base.proto.main_pb2 as pb
|
|
56
|
+
from pybrid.lucidac.controller import Controller as LUCIDACController
|
|
57
|
+
from pybrid.lucidac.protocol.serializer import LUCIDACDeserializer
|
|
58
|
+
from pybrid.redac.controller import Controller as REDACController
|
|
59
|
+
from pybrid.redac.device import Device
|
|
60
|
+
from pybrid.redac.entities import Path
|
|
61
|
+
|
|
62
|
+
file_msg = pb.File()
|
|
63
|
+
try:
|
|
64
|
+
file_msg.ParseFromString(entity_bytes)
|
|
65
|
+
except Exception as exc:
|
|
66
|
+
raise ValueError(f"entity_bytes is not a valid pb.File: {exc}") from exc
|
|
67
|
+
module = file_msg.module
|
|
68
|
+
|
|
69
|
+
controller_cls = LUCIDACController if device_type == "LUCIDAC" else REDACController
|
|
70
|
+
controller = controller_cls()
|
|
71
|
+
|
|
72
|
+
deserializer = LUCIDACDeserializer()
|
|
73
|
+
for item in module.items:
|
|
74
|
+
if not item.HasField("entity_specification"):
|
|
75
|
+
continue
|
|
76
|
+
entity = item.entity_specification.entity
|
|
77
|
+
try:
|
|
78
|
+
root_path = Path.parse(entity.id.strip("/"))
|
|
79
|
+
except Exception:
|
|
80
|
+
root_path = Path.make()
|
|
81
|
+
|
|
82
|
+
result = deserializer.deserialize_specification(entity, root_path)
|
|
83
|
+
carriers = result.carriers if isinstance(result, Device) else [result]
|
|
84
|
+
for carrier in carriers:
|
|
85
|
+
controller.computer.add_carrier(carrier)
|
|
86
|
+
|
|
87
|
+
return controller.computer
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def render_entity_tree(entity_bytes: bytes, device_type: str) -> str:
|
|
91
|
+
"""Return the pybrid ``TreeDisplay`` rendering of an entity blob.
|
|
92
|
+
|
|
93
|
+
Equivalent to building a ``Computer`` via :func:`build_computer_from_entity`
|
|
94
|
+
and running ``TreeDisplay.render(computer)``. The return value is the
|
|
95
|
+
same ASCII tree that ``pybrid display`` prints against a live device.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
entity_bytes: Raw serialized ``pb.Module`` bytes.
|
|
99
|
+
device_type: ``"LUCIDAC"`` or ``"REDAC"``.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
A non-empty ASCII tree string.
|
|
103
|
+
|
|
104
|
+
Raises:
|
|
105
|
+
ValueError: Propagated from :func:`build_computer_from_entity`.
|
|
106
|
+
"""
|
|
107
|
+
from pybrid.redac.display import TreeDisplay
|
|
108
|
+
|
|
109
|
+
computer = build_computer_from_entity(entity_bytes, device_type)
|
|
110
|
+
return TreeDisplay.render(computer)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""CLI entry point — registers all sub-commands."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from .commands import compile, info, run, status
|
|
6
|
+
from .commands.task import task
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@click.group()
|
|
10
|
+
@click.option(
|
|
11
|
+
"-u",
|
|
12
|
+
"--base-url",
|
|
13
|
+
default="https://redac.anabrid.com",
|
|
14
|
+
help="Hostname of the Analog Cloud API server",
|
|
15
|
+
show_default=True,
|
|
16
|
+
)
|
|
17
|
+
@click.option(
|
|
18
|
+
"--timeout",
|
|
19
|
+
default=30.0,
|
|
20
|
+
type=float,
|
|
21
|
+
help="Request timeout in seconds",
|
|
22
|
+
show_default=True,
|
|
23
|
+
)
|
|
24
|
+
@click.pass_context
|
|
25
|
+
def cli(ctx: click.Context, base_url: str, timeout: float):
|
|
26
|
+
"""Analog Cloud CLI - Command-line interface for the Analog Cloud system.
|
|
27
|
+
|
|
28
|
+
This CLI provides access to Analog Cloud's analog computing devices through
|
|
29
|
+
a REST API interface. You can submit tasks, monitor their progress,
|
|
30
|
+
and retrieve results.
|
|
31
|
+
|
|
32
|
+
Common workflow:
|
|
33
|
+
1. Check system status: analogcli status
|
|
34
|
+
2. Compile an ODE: analogcli compile my_ode.txt device.entity
|
|
35
|
+
3. Run on a device: analogcli run compiled.apb --device lucidac-01
|
|
36
|
+
"""
|
|
37
|
+
ctx.ensure_object(dict)
|
|
38
|
+
ctx.obj["client_config"] = {
|
|
39
|
+
"base_url": base_url,
|
|
40
|
+
"timeout": timeout,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
cli.add_command(status.status)
|
|
45
|
+
cli.add_command(info.info)
|
|
46
|
+
cli.add_command(run.run)
|
|
47
|
+
cli.add_command(compile.compile)
|
|
48
|
+
cli.add_command(task)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
if __name__ == "__main__":
|
|
52
|
+
cli()
|