buildai-cli 0.3.34__tar.gz → 0.3.35__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.
- {buildai_cli-0.3.34 → buildai_cli-0.3.35}/.gitignore +5 -0
- buildai_cli-0.3.35/CLAUDE 2.md +37 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.35}/PKG-INFO +1 -1
- buildai_cli-0.3.35/cli/commands/jobs.py +193 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/commands/medoid.py +2 -2
- {buildai_cli-0.3.34 → buildai_cli-0.3.35}/pyproject.toml +1 -1
- buildai_cli-0.3.34/cli/commands/jobs.py +0 -344
- {buildai_cli-0.3.34 → buildai_cli-0.3.35}/AGENTS.md +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.35}/CLAUDE.md +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/__init__.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/_has_core.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/commands/__init__.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/commands/api_proxy.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/commands/assets_cli.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/commands/auth_lite.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/commands/clips.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/commands/database.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/commands/dev.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/commands/embed.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/commands/external.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/commands/inference.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/commands/keys.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/commands/operations.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/commands/partners.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/commands/permissions.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/commands/projection.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/commands/query.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/commands/query_api.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/commands/reports.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/commands/schema.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/commands/search.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/commands/stats.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/commands/sync/ddl.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/commands/sync/models.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/commands/sync/queries.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/config.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/console.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/context.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/dev_context.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/guard.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/main.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/nl_query/__init__.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/nl_query/dataset_tools.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/ops_init.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/output.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/pagination.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/sdk_client.py +0 -0
|
@@ -27,6 +27,7 @@ test-results/
|
|
|
27
27
|
# Node
|
|
28
28
|
node_modules/
|
|
29
29
|
.next/
|
|
30
|
+
.expect/
|
|
30
31
|
out/
|
|
31
32
|
|
|
32
33
|
# IDE
|
|
@@ -70,6 +71,7 @@ reference/
|
|
|
70
71
|
scripts/output/
|
|
71
72
|
scripts/dead_assets.json
|
|
72
73
|
scripts/dead_assets.ids.txt
|
|
74
|
+
scripts/figure_h264_1000h/
|
|
73
75
|
|
|
74
76
|
# Service account keys
|
|
75
77
|
*-sa-key.json
|
|
@@ -85,3 +87,6 @@ scripts/dead_assets.ids.txt
|
|
|
85
87
|
*.ts.net.key
|
|
86
88
|
test-buildai-data/
|
|
87
89
|
.claude/worktrees/
|
|
90
|
+
|
|
91
|
+
# Generated multi-service app factory (local dev only)
|
|
92
|
+
_combined_factory.py
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# BuildAI CLI
|
|
2
|
+
|
|
3
|
+
Typer-based CLI with two modes: standalone (PyPI, API-backed) and workspace (repo-local, DB-direct).
|
|
4
|
+
|
|
5
|
+
## Two Planes
|
|
6
|
+
|
|
7
|
+
| Mode | Install | Auth | DB Access |
|
|
8
|
+
|------|---------|------|-----------|
|
|
9
|
+
| Standalone (`buildai`) | `uv tool install buildai-cli` | API key / JWT | Through API |
|
|
10
|
+
| Workspace (`uv run buildai`) | Repo-local editable install | IAM / password | Direct via `admin` |
|
|
11
|
+
|
|
12
|
+
## Key Commands
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
buildai query "SELECT count(*) FROM core.clips" # API-backed
|
|
16
|
+
buildai admin query "SELECT count(*) FROM core.clips" # DB-direct
|
|
17
|
+
buildai admin schema tables # Schema introspection
|
|
18
|
+
buildai admin schema describe core.clips # Table details
|
|
19
|
+
uv run buildai dev use local # Set this worktree's deployment profile
|
|
20
|
+
uv run buildai dev current # Show resolved worktree target
|
|
21
|
+
buildai admin --write database migrate all # Run migrations
|
|
22
|
+
buildai admin database diff --from preview --to production # Migration delta
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Guards
|
|
26
|
+
|
|
27
|
+
- `admin` subcommands require workspace install + gcloud IAM.
|
|
28
|
+
- Writes require `--write` flag.
|
|
29
|
+
- Production migrations prompt for confirmation.
|
|
30
|
+
- Worktree app/runtime targeting comes from `uv run buildai dev use <profile>`.
|
|
31
|
+
- `buildai admin --env` selects the explicit DB lane: `production`, `preview`, `dev`.
|
|
32
|
+
- Do not confuse `--env preview` with the deployment profiles `local`, `preview`, or `staging`.
|
|
33
|
+
|
|
34
|
+
## Reference
|
|
35
|
+
|
|
36
|
+
CLI mode guide: `docs/how-to/choose-buildai-mode.md`
|
|
37
|
+
Database roles: `docs/database-roles.md`
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""CLI commands for jobs (SDK-backed data-plane)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import itertools
|
|
6
|
+
import time
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from cli.console import info, success, warning
|
|
12
|
+
from cli.output import Format, format_option, output
|
|
13
|
+
from cli.sdk_client import get_sdk_client
|
|
14
|
+
|
|
15
|
+
app = typer.Typer(
|
|
16
|
+
name="jobs",
|
|
17
|
+
help="Manage processing jobs.",
|
|
18
|
+
no_args_is_help=True,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _job_row(job: Any) -> dict[str, Any]:
|
|
23
|
+
return {
|
|
24
|
+
"job_id": job.job_id,
|
|
25
|
+
"job_type": job.job_type,
|
|
26
|
+
"status": job.status,
|
|
27
|
+
"progress_pct": getattr(job, "progress_pct", 0.0),
|
|
28
|
+
"total_items": getattr(job, "total_items", None),
|
|
29
|
+
"chunk_count": getattr(job, "chunk_count", None),
|
|
30
|
+
"created_at": getattr(job, "created_at", None),
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _launch_job(
|
|
35
|
+
client: Any,
|
|
36
|
+
job_id: str,
|
|
37
|
+
*,
|
|
38
|
+
max_tasks: int,
|
|
39
|
+
spot: bool,
|
|
40
|
+
platform: str,
|
|
41
|
+
region: str | None,
|
|
42
|
+
cpu: str,
|
|
43
|
+
memory: str,
|
|
44
|
+
) -> Any:
|
|
45
|
+
return client.jobs.submit(
|
|
46
|
+
job_id,
|
|
47
|
+
max_tasks=max_tasks,
|
|
48
|
+
spot=spot,
|
|
49
|
+
platform=platform,
|
|
50
|
+
region=region,
|
|
51
|
+
cpu=cpu,
|
|
52
|
+
memory=memory,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _watch_job(client: Any, job_id: str, *, interval: float) -> None:
|
|
57
|
+
last_line: str | None = None
|
|
58
|
+
while True:
|
|
59
|
+
progress = client.jobs.progress(job_id)
|
|
60
|
+
line = (
|
|
61
|
+
f"{progress.status} "
|
|
62
|
+
f"chunks={progress.chunks_succeeded + progress.chunks_failed}/{progress.total_chunks} "
|
|
63
|
+
f"items={progress.items_succeeded} ok/{progress.items_failed} failed "
|
|
64
|
+
f"items_per_sec={progress.items_per_second} "
|
|
65
|
+
f"chunks_per_sec={progress.chunks_per_second} "
|
|
66
|
+
f"eta_s={progress.eta_seconds}"
|
|
67
|
+
)
|
|
68
|
+
if line != last_line:
|
|
69
|
+
info(f"{job_id}: {line}")
|
|
70
|
+
last_line = line
|
|
71
|
+
if progress.status in {"succeeded", "failed", "cancelled"}:
|
|
72
|
+
if progress.status != "succeeded":
|
|
73
|
+
failures = client.jobs.failures(job_id, page_size=5)
|
|
74
|
+
if failures.data:
|
|
75
|
+
for failure in failures.data:
|
|
76
|
+
warning(
|
|
77
|
+
f"{failure.entity_id} | "
|
|
78
|
+
f"{failure.error_code or 'error'} | "
|
|
79
|
+
f"{failure.error_message or ''}"
|
|
80
|
+
)
|
|
81
|
+
return
|
|
82
|
+
time.sleep(interval)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@app.command("list")
|
|
86
|
+
def jobs_list(
|
|
87
|
+
status: str | None = typer.Option(None, "--status", "-s", help="Filter by status"),
|
|
88
|
+
type: str | None = typer.Option(None, "--type", "-t", help="Filter by job type"),
|
|
89
|
+
limit: int = typer.Option(20, "--limit", "-n"),
|
|
90
|
+
format: Format | None = format_option(),
|
|
91
|
+
) -> None:
|
|
92
|
+
"""List processing jobs."""
|
|
93
|
+
client = get_sdk_client()
|
|
94
|
+
results = list(
|
|
95
|
+
itertools.islice(
|
|
96
|
+
client.jobs.iter(status=status, type=type),
|
|
97
|
+
limit,
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
output(
|
|
101
|
+
[_job_row(job) for job in results],
|
|
102
|
+
format=format,
|
|
103
|
+
columns=["job_id", "job_type", "status", "progress_pct", "total_items", "chunk_count"],
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@app.command("get")
|
|
108
|
+
def jobs_get(
|
|
109
|
+
job_id: str = typer.Argument(..., help="Manifest ID"),
|
|
110
|
+
format: Format | None = format_option(),
|
|
111
|
+
) -> None:
|
|
112
|
+
"""Get details for a single job."""
|
|
113
|
+
client = get_sdk_client()
|
|
114
|
+
job = client.jobs.get(job_id)
|
|
115
|
+
output(job, format=format)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@app.command("submit")
|
|
119
|
+
def jobs_submit(
|
|
120
|
+
ctx: typer.Context,
|
|
121
|
+
job_id: str = typer.Argument(..., help="Manifest ID"),
|
|
122
|
+
max_tasks: int = typer.Option(750, "--max-tasks"),
|
|
123
|
+
spot: bool = typer.Option(True, "--spot/--no-spot"),
|
|
124
|
+
platform: str = typer.Option("auto", "--platform"),
|
|
125
|
+
region: str | None = typer.Option(None, "--region"),
|
|
126
|
+
cpu: str = typer.Option("8", "--cpu"),
|
|
127
|
+
memory: str = typer.Option("8Gi", "--memory"),
|
|
128
|
+
watch: bool = typer.Option(False, "--watch"),
|
|
129
|
+
interval: float = typer.Option(5.0, "--interval"),
|
|
130
|
+
) -> None:
|
|
131
|
+
"""Submit an existing manifest to execution."""
|
|
132
|
+
del ctx # API enforces permissions via token scope
|
|
133
|
+
client = get_sdk_client()
|
|
134
|
+
result = _launch_job(
|
|
135
|
+
client,
|
|
136
|
+
job_id,
|
|
137
|
+
max_tasks=max_tasks,
|
|
138
|
+
spot=spot,
|
|
139
|
+
platform=platform,
|
|
140
|
+
region=region,
|
|
141
|
+
cpu=cpu,
|
|
142
|
+
memory=memory,
|
|
143
|
+
)
|
|
144
|
+
success(
|
|
145
|
+
f"Submitted {job_id} on {result.platform} "
|
|
146
|
+
f"as {result.batch_job_name} with {result.task_count} tasks"
|
|
147
|
+
)
|
|
148
|
+
if watch:
|
|
149
|
+
_watch_job(client, job_id, interval=interval)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@app.command("watch")
|
|
153
|
+
def jobs_watch(
|
|
154
|
+
job_id: str = typer.Argument(..., help="Manifest ID"),
|
|
155
|
+
interval: float = typer.Option(5.0, "--interval"),
|
|
156
|
+
) -> None:
|
|
157
|
+
"""Watch a job until completion."""
|
|
158
|
+
client = get_sdk_client()
|
|
159
|
+
_watch_job(client, job_id, interval=interval)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@app.command("retry")
|
|
163
|
+
def jobs_retry(
|
|
164
|
+
ctx: typer.Context,
|
|
165
|
+
job_id: str = typer.Argument(..., help="Manifest ID"),
|
|
166
|
+
dry_run: bool = typer.Option(False, "--dry-run"),
|
|
167
|
+
) -> None:
|
|
168
|
+
"""Retry a failed job."""
|
|
169
|
+
if dry_run:
|
|
170
|
+
info(f"Would retry job {job_id}")
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
del ctx # API enforces permissions via token scope
|
|
174
|
+
client = get_sdk_client()
|
|
175
|
+
client.jobs.retry(job_id)
|
|
176
|
+
success(f"Retried job {job_id}")
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@app.command("cancel")
|
|
180
|
+
def jobs_cancel(
|
|
181
|
+
ctx: typer.Context,
|
|
182
|
+
job_id: str = typer.Argument(..., help="Manifest ID"),
|
|
183
|
+
dry_run: bool = typer.Option(False, "--dry-run"),
|
|
184
|
+
) -> None:
|
|
185
|
+
"""Cancel a running job."""
|
|
186
|
+
if dry_run:
|
|
187
|
+
info(f"Would cancel job {job_id}")
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
del ctx # API enforces permissions via token scope
|
|
191
|
+
client = get_sdk_client()
|
|
192
|
+
client.jobs.cancel(job_id)
|
|
193
|
+
success(f"Cancelled job {job_id}")
|
|
@@ -239,7 +239,7 @@ async def _compute(
|
|
|
239
239
|
) -> None:
|
|
240
240
|
from dal.processing import jobs
|
|
241
241
|
|
|
242
|
-
from
|
|
242
|
+
from processing_runtime.profile_catalog import load_runtime_profile, resolve_profile
|
|
243
243
|
|
|
244
244
|
settings = ctx.obj["settings"]
|
|
245
245
|
|
|
@@ -300,7 +300,7 @@ async def _compute(
|
|
|
300
300
|
raise typer.Exit(1)
|
|
301
301
|
|
|
302
302
|
# Get GPU config based on data size for display
|
|
303
|
-
embedding_config =
|
|
303
|
+
embedding_config = load_runtime_profile("embedding_runtime")
|
|
304
304
|
batch_config = resolve_profile(
|
|
305
305
|
embedding_config,
|
|
306
306
|
job_type="compute_medoids",
|
|
@@ -1,344 +0,0 @@
|
|
|
1
|
-
"""CLI commands for jobs (SDK-backed data-plane)."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import itertools
|
|
6
|
-
import time
|
|
7
|
-
from typing import Any
|
|
8
|
-
|
|
9
|
-
import typer
|
|
10
|
-
|
|
11
|
-
from cli.console import error, info, success, warning
|
|
12
|
-
from cli.output import Format, format_option, output
|
|
13
|
-
from cli.sdk_client import get_sdk_client
|
|
14
|
-
|
|
15
|
-
app = typer.Typer(
|
|
16
|
-
name="jobs",
|
|
17
|
-
help="Manage processing jobs.",
|
|
18
|
-
no_args_is_help=True,
|
|
19
|
-
)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def _job_row(job: Any) -> dict[str, Any]:
|
|
23
|
-
return {
|
|
24
|
-
"job_id": job.job_id,
|
|
25
|
-
"job_type": job.job_type,
|
|
26
|
-
"status": job.status,
|
|
27
|
-
"progress_pct": getattr(job, "progress_pct", 0.0),
|
|
28
|
-
"total_items": getattr(job, "total_items", None),
|
|
29
|
-
"chunk_count": getattr(job, "chunk_count", None),
|
|
30
|
-
"created_at": getattr(job, "created_at", None),
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def _parse_exists_filter(raw: str) -> dict[str, Any]:
|
|
35
|
-
"""Parse --exists flag value like 'descriptions:model=gemini-2.0-flash'."""
|
|
36
|
-
if ":" not in raw:
|
|
37
|
-
raise typer.BadParameter(f"Invalid --exists format: {raw!r}. Expected 'source:col=val,...'")
|
|
38
|
-
source, rest = raw.split(":", 1)
|
|
39
|
-
conditions: dict[str, str | list[str]] = {}
|
|
40
|
-
for pair in rest.split(","):
|
|
41
|
-
if "=" not in pair:
|
|
42
|
-
raise typer.BadParameter(f"Invalid condition in --exists: {pair!r}. Expected 'col=val'")
|
|
43
|
-
col, val = pair.split("=", 1)
|
|
44
|
-
col = col.strip()
|
|
45
|
-
val = val.strip()
|
|
46
|
-
if col in conditions:
|
|
47
|
-
existing = conditions[col]
|
|
48
|
-
if isinstance(existing, list):
|
|
49
|
-
existing.append(val)
|
|
50
|
-
else:
|
|
51
|
-
conditions[col] = [existing, val]
|
|
52
|
-
else:
|
|
53
|
-
conditions[col] = val
|
|
54
|
-
return {"source": source.strip(), "conditions": conditions}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def _query(
|
|
58
|
-
*,
|
|
59
|
-
factory: str | None,
|
|
60
|
-
collection: str | None,
|
|
61
|
-
worker_ids: list[str] | None,
|
|
62
|
-
limit: int | None,
|
|
63
|
-
exists: list[str] | None = None,
|
|
64
|
-
where: list[str] | None = None,
|
|
65
|
-
) -> dict[str, Any]:
|
|
66
|
-
q: dict[str, Any] = {}
|
|
67
|
-
if factory:
|
|
68
|
-
q["factory_id"] = factory
|
|
69
|
-
if collection:
|
|
70
|
-
q["collection_id"] = collection
|
|
71
|
-
if worker_ids:
|
|
72
|
-
q["worker_ids"] = worker_ids
|
|
73
|
-
if limit is not None:
|
|
74
|
-
q["limit"] = limit
|
|
75
|
-
if exists:
|
|
76
|
-
q["exists"] = [_parse_exists_filter(e) for e in exists]
|
|
77
|
-
if where:
|
|
78
|
-
q["where"] = where
|
|
79
|
-
return q
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
# Backward compat
|
|
83
|
-
_selector = _query
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def _launch_job(
|
|
87
|
-
client: Any,
|
|
88
|
-
job_id: str,
|
|
89
|
-
*,
|
|
90
|
-
max_tasks: int,
|
|
91
|
-
spot: bool,
|
|
92
|
-
platform: str,
|
|
93
|
-
region: str | None,
|
|
94
|
-
cpu: str,
|
|
95
|
-
memory: str,
|
|
96
|
-
) -> Any:
|
|
97
|
-
return client.jobs.submit(
|
|
98
|
-
job_id,
|
|
99
|
-
max_tasks=max_tasks,
|
|
100
|
-
spot=spot,
|
|
101
|
-
platform=platform,
|
|
102
|
-
region=region,
|
|
103
|
-
cpu=cpu,
|
|
104
|
-
memory=memory,
|
|
105
|
-
)
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
def _watch_job(client: Any, job_id: str, *, interval: float) -> None:
|
|
109
|
-
last_line: str | None = None
|
|
110
|
-
while True:
|
|
111
|
-
progress = client.jobs.progress(job_id)
|
|
112
|
-
line = (
|
|
113
|
-
f"{progress.status} "
|
|
114
|
-
f"chunks={progress.chunks_succeeded + progress.chunks_failed}/{progress.total_chunks} "
|
|
115
|
-
f"items={progress.items_succeeded} ok/{progress.items_failed} failed "
|
|
116
|
-
f"items_per_sec={progress.items_per_second} "
|
|
117
|
-
f"chunks_per_sec={progress.chunks_per_second} "
|
|
118
|
-
f"eta_s={progress.eta_seconds}"
|
|
119
|
-
)
|
|
120
|
-
if line != last_line:
|
|
121
|
-
info(f"{job_id}: {line}")
|
|
122
|
-
last_line = line
|
|
123
|
-
if progress.status in {"succeeded", "failed", "cancelled"}:
|
|
124
|
-
if progress.status != "succeeded":
|
|
125
|
-
failures = client.jobs.failures(job_id, page_size=5)
|
|
126
|
-
if failures.data:
|
|
127
|
-
for failure in failures.data:
|
|
128
|
-
warning(
|
|
129
|
-
f"{failure.entity_id} | "
|
|
130
|
-
f"{failure.error_code or 'error'} | "
|
|
131
|
-
f"{failure.error_message or ''}"
|
|
132
|
-
)
|
|
133
|
-
return
|
|
134
|
-
time.sleep(interval)
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
@app.command("list")
|
|
138
|
-
def jobs_list(
|
|
139
|
-
status: str | None = typer.Option(None, "--status", "-s", help="Filter by status"),
|
|
140
|
-
type: str | None = typer.Option(None, "--type", "-t", help="Filter by job type"),
|
|
141
|
-
limit: int = typer.Option(20, "--limit", "-n"),
|
|
142
|
-
format: Format | None = format_option(),
|
|
143
|
-
) -> None:
|
|
144
|
-
"""List processing jobs."""
|
|
145
|
-
client = get_sdk_client()
|
|
146
|
-
results = list(
|
|
147
|
-
itertools.islice(
|
|
148
|
-
client.jobs.iter(status=status, type=type),
|
|
149
|
-
limit,
|
|
150
|
-
)
|
|
151
|
-
)
|
|
152
|
-
output(
|
|
153
|
-
[_job_row(job) for job in results],
|
|
154
|
-
format=format,
|
|
155
|
-
columns=["job_id", "job_type", "status", "progress_pct", "total_items", "chunk_count"],
|
|
156
|
-
)
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
@app.command("get")
|
|
160
|
-
def jobs_get(
|
|
161
|
-
job_id: str = typer.Argument(..., help="Manifest ID"),
|
|
162
|
-
format: Format | None = format_option(),
|
|
163
|
-
) -> None:
|
|
164
|
-
"""Get details for a single job."""
|
|
165
|
-
client = get_sdk_client()
|
|
166
|
-
job = client.jobs.get(job_id)
|
|
167
|
-
output(job, format=format)
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
@app.command("submit")
|
|
171
|
-
def jobs_submit(
|
|
172
|
-
ctx: typer.Context,
|
|
173
|
-
job_id: str = typer.Argument(..., help="Manifest ID"),
|
|
174
|
-
max_tasks: int = typer.Option(750, "--max-tasks"),
|
|
175
|
-
spot: bool = typer.Option(True, "--spot/--no-spot"),
|
|
176
|
-
platform: str = typer.Option("auto", "--platform"),
|
|
177
|
-
region: str | None = typer.Option(None, "--region"),
|
|
178
|
-
cpu: str = typer.Option("8", "--cpu"),
|
|
179
|
-
memory: str = typer.Option("8Gi", "--memory"),
|
|
180
|
-
watch: bool = typer.Option(False, "--watch"),
|
|
181
|
-
interval: float = typer.Option(5.0, "--interval"),
|
|
182
|
-
) -> None:
|
|
183
|
-
"""Submit an existing manifest to execution."""
|
|
184
|
-
del ctx # API enforces permissions via token scope
|
|
185
|
-
client = get_sdk_client()
|
|
186
|
-
result = _launch_job(
|
|
187
|
-
client,
|
|
188
|
-
job_id,
|
|
189
|
-
max_tasks=max_tasks,
|
|
190
|
-
spot=spot,
|
|
191
|
-
platform=platform,
|
|
192
|
-
region=region,
|
|
193
|
-
cpu=cpu,
|
|
194
|
-
memory=memory,
|
|
195
|
-
)
|
|
196
|
-
success(
|
|
197
|
-
f"Submitted {job_id} on {result.platform} "
|
|
198
|
-
f"as {result.batch_job_name} with {result.task_count} tasks"
|
|
199
|
-
)
|
|
200
|
-
if watch:
|
|
201
|
-
_watch_job(client, job_id, interval=interval)
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
@app.command("watch")
|
|
205
|
-
def jobs_watch(
|
|
206
|
-
job_id: str = typer.Argument(..., help="Manifest ID"),
|
|
207
|
-
interval: float = typer.Option(5.0, "--interval"),
|
|
208
|
-
) -> None:
|
|
209
|
-
"""Watch a job until completion."""
|
|
210
|
-
client = get_sdk_client()
|
|
211
|
-
_watch_job(client, job_id, interval=interval)
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
@app.command("backlog")
|
|
215
|
-
def jobs_backlog(
|
|
216
|
-
job_type: str = typer.Argument(..., help="extract-frames"),
|
|
217
|
-
factory: str | None = typer.Option(None, "--factory"),
|
|
218
|
-
collection: str | None = typer.Option(None, "--collection", "-c"),
|
|
219
|
-
format: Format | None = format_option(),
|
|
220
|
-
) -> None:
|
|
221
|
-
"""Inspect extractor backlog."""
|
|
222
|
-
client = get_sdk_client()
|
|
223
|
-
if job_type == "extract-frames":
|
|
224
|
-
backlog = client.jobs.extract_frames_backlog(factory_id=factory, collection_id=collection)
|
|
225
|
-
else:
|
|
226
|
-
error(f"Unknown backlog type: {job_type}")
|
|
227
|
-
raise typer.Exit(1)
|
|
228
|
-
output(backlog, format=format)
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
@app.command("extract-frames")
|
|
232
|
-
def jobs_extract_frames(
|
|
233
|
-
ctx: typer.Context,
|
|
234
|
-
factory: str | None = typer.Option(None, "--factory"),
|
|
235
|
-
collection: str | None = typer.Option(None, "--collection", "-c"),
|
|
236
|
-
worker_ids: list[str] | None = typer.Option(None, "--worker-id"),
|
|
237
|
-
limit: int | None = typer.Option(None, "--limit"),
|
|
238
|
-
exists: list[str] | None = typer.Option(
|
|
239
|
-
None, "--exists", help="EXISTS filter (source:col=val,...)"
|
|
240
|
-
),
|
|
241
|
-
where: list[str] | None = typer.Option(None, "--where", help="Raw SQL WHERE fragment"),
|
|
242
|
-
chunk_size: int | None = typer.Option(None, "--chunk-size"),
|
|
243
|
-
frame_extraction_timeout_sec: int = typer.Option(300, "--timeout-sec"),
|
|
244
|
-
num_frames: int = typer.Option(10, "--num-frames"),
|
|
245
|
-
frame_height: int = typer.Option(256, "--frame-height"),
|
|
246
|
-
frame_jpeg_quality: int = typer.Option(4, "--frame-jpeg-quality"),
|
|
247
|
-
max_downloads_per_worker: int = typer.Option(3, "--max-downloads-per-worker"),
|
|
248
|
-
max_clips_in_flight: int = typer.Option(4, "--max-clips-in-flight"),
|
|
249
|
-
max_seek_ffmpeg_processes_per_clip: int = typer.Option(
|
|
250
|
-
8, "--max-seek-ffmpeg-processes-per-clip"
|
|
251
|
-
),
|
|
252
|
-
max_uploads_per_clip: int = typer.Option(10, "--max-uploads-per-clip"),
|
|
253
|
-
ffmpeg_threads_per_process: int = typer.Option(1, "--ffmpeg-threads-per-process"),
|
|
254
|
-
launch: bool = typer.Option(True, "--launch/--no-launch"),
|
|
255
|
-
max_tasks: int = typer.Option(20, "--max-tasks"),
|
|
256
|
-
spot: bool = typer.Option(False, "--spot/--no-spot"),
|
|
257
|
-
platform: str = typer.Option("batch", "--platform"),
|
|
258
|
-
region: str | None = typer.Option(None, "--region"),
|
|
259
|
-
cpu: str = typer.Option("8", "--cpu"),
|
|
260
|
-
memory: str = typer.Option("8Gi", "--memory"),
|
|
261
|
-
watch: bool = typer.Option(True, "--watch"),
|
|
262
|
-
interval: float = typer.Option(5.0, "--interval"),
|
|
263
|
-
) -> None:
|
|
264
|
-
"""Queue canonical frame extraction, optionally launch immediately."""
|
|
265
|
-
del ctx # API enforces permissions via token scope
|
|
266
|
-
client = get_sdk_client()
|
|
267
|
-
backlog = client.jobs.extract_frames_backlog(factory_id=factory, collection_id=collection)
|
|
268
|
-
info(
|
|
269
|
-
f"Extractor backlog clips={backlog.total_clips} "
|
|
270
|
-
f"active_manifest={backlog.active_manifest_id}"
|
|
271
|
-
)
|
|
272
|
-
# Build exists filters from CLI format
|
|
273
|
-
exists_filters = [_parse_exists_filter(e) for e in exists] if exists else None
|
|
274
|
-
job = client.jobs.submit_extract_frames(
|
|
275
|
-
factory_id=factory,
|
|
276
|
-
collection_id=collection,
|
|
277
|
-
worker_ids=worker_ids,
|
|
278
|
-
limit=limit,
|
|
279
|
-
exists_filters=exists_filters,
|
|
280
|
-
where_clauses=where,
|
|
281
|
-
chunk_size=chunk_size,
|
|
282
|
-
frame_extraction_timeout_sec=frame_extraction_timeout_sec,
|
|
283
|
-
num_frames=num_frames,
|
|
284
|
-
frame_height=frame_height,
|
|
285
|
-
frame_jpeg_quality=frame_jpeg_quality,
|
|
286
|
-
max_downloads_per_worker=max_downloads_per_worker,
|
|
287
|
-
max_clips_in_flight=max_clips_in_flight,
|
|
288
|
-
max_seek_ffmpeg_processes_per_clip=max_seek_ffmpeg_processes_per_clip,
|
|
289
|
-
max_uploads_per_clip=max_uploads_per_clip,
|
|
290
|
-
ffmpeg_threads_per_process=ffmpeg_threads_per_process,
|
|
291
|
-
)
|
|
292
|
-
success(f"Queued extractor manifest {job.job_id}")
|
|
293
|
-
if not launch:
|
|
294
|
-
return
|
|
295
|
-
result = _launch_job(
|
|
296
|
-
client,
|
|
297
|
-
job.job_id,
|
|
298
|
-
max_tasks=max_tasks,
|
|
299
|
-
spot=spot,
|
|
300
|
-
platform=platform,
|
|
301
|
-
region=region,
|
|
302
|
-
cpu=cpu,
|
|
303
|
-
memory=memory,
|
|
304
|
-
)
|
|
305
|
-
success(
|
|
306
|
-
f"Submitted extractor {job.job_id} on {result.platform} "
|
|
307
|
-
f"as {result.batch_job_name} with {result.task_count} tasks"
|
|
308
|
-
)
|
|
309
|
-
if watch:
|
|
310
|
-
_watch_job(client, job.job_id, interval=interval)
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
@app.command("retry")
|
|
314
|
-
def jobs_retry(
|
|
315
|
-
ctx: typer.Context,
|
|
316
|
-
job_id: str = typer.Argument(..., help="Manifest ID"),
|
|
317
|
-
dry_run: bool = typer.Option(False, "--dry-run"),
|
|
318
|
-
) -> None:
|
|
319
|
-
"""Retry a failed job."""
|
|
320
|
-
if dry_run:
|
|
321
|
-
info(f"Would retry job {job_id}")
|
|
322
|
-
return
|
|
323
|
-
|
|
324
|
-
del ctx # API enforces permissions via token scope
|
|
325
|
-
client = get_sdk_client()
|
|
326
|
-
client.jobs.retry(job_id)
|
|
327
|
-
success(f"Retried job {job_id}")
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
@app.command("cancel")
|
|
331
|
-
def jobs_cancel(
|
|
332
|
-
ctx: typer.Context,
|
|
333
|
-
job_id: str = typer.Argument(..., help="Manifest ID"),
|
|
334
|
-
dry_run: bool = typer.Option(False, "--dry-run"),
|
|
335
|
-
) -> None:
|
|
336
|
-
"""Cancel a running job."""
|
|
337
|
-
if dry_run:
|
|
338
|
-
info(f"Would cancel job {job_id}")
|
|
339
|
-
return
|
|
340
|
-
|
|
341
|
-
del ctx # API enforces permissions via token scope
|
|
342
|
-
client = get_sdk_client()
|
|
343
|
-
client.jobs.cancel(job_id)
|
|
344
|
-
success(f"Cancelled job {job_id}")
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|