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.
Files changed (47) hide show
  1. {buildai_cli-0.3.34 → buildai_cli-0.3.35}/.gitignore +5 -0
  2. buildai_cli-0.3.35/CLAUDE 2.md +37 -0
  3. {buildai_cli-0.3.34 → buildai_cli-0.3.35}/PKG-INFO +1 -1
  4. buildai_cli-0.3.35/cli/commands/jobs.py +193 -0
  5. {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/commands/medoid.py +2 -2
  6. {buildai_cli-0.3.34 → buildai_cli-0.3.35}/pyproject.toml +1 -1
  7. buildai_cli-0.3.34/cli/commands/jobs.py +0 -344
  8. {buildai_cli-0.3.34 → buildai_cli-0.3.35}/AGENTS.md +0 -0
  9. {buildai_cli-0.3.34 → buildai_cli-0.3.35}/CLAUDE.md +0 -0
  10. {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/__init__.py +0 -0
  11. {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/_has_core.py +0 -0
  12. {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/commands/__init__.py +0 -0
  13. {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/commands/api_proxy.py +0 -0
  14. {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/commands/assets_cli.py +0 -0
  15. {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/commands/auth_lite.py +0 -0
  16. {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/commands/clips.py +0 -0
  17. {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/commands/database.py +0 -0
  18. {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/commands/dev.py +0 -0
  19. {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/commands/embed.py +0 -0
  20. {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/commands/external.py +0 -0
  21. {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/commands/inference.py +0 -0
  22. {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/commands/keys.py +0 -0
  23. {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/commands/operations.py +0 -0
  24. {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/commands/partners.py +0 -0
  25. {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/commands/permissions.py +0 -0
  26. {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/commands/projection.py +0 -0
  27. {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/commands/query.py +0 -0
  28. {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/commands/query_api.py +0 -0
  29. {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/commands/reports.py +0 -0
  30. {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/commands/schema.py +0 -0
  31. {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/commands/search.py +0 -0
  32. {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/commands/stats.py +0 -0
  33. {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/commands/sync/ddl.py +0 -0
  34. {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/commands/sync/models.py +0 -0
  35. {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/commands/sync/queries.py +0 -0
  36. {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/config.py +0 -0
  37. {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/console.py +0 -0
  38. {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/context.py +0 -0
  39. {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/dev_context.py +0 -0
  40. {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/guard.py +0 -0
  41. {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/main.py +0 -0
  42. {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/nl_query/__init__.py +0 -0
  43. {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/nl_query/dataset_tools.py +0 -0
  44. {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/ops_init.py +0 -0
  45. {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/output.py +0 -0
  46. {buildai_cli-0.3.34 → buildai_cli-0.3.35}/cli/pagination.py +0 -0
  47. {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`
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: buildai-cli
3
- Version: 0.3.34
3
+ Version: 0.3.35
4
4
  Summary: Build AI CLI (Typer)
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: buildai-data
@@ -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 services.load_config import load_service_config, resolve_profile
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 = load_service_config("media_embedding")
303
+ embedding_config = load_runtime_profile("embedding_runtime")
304
304
  batch_config = resolve_profile(
305
305
  embedding_config,
306
306
  job_type="compute_medoids",
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "buildai-cli"
7
- version = "0.3.34"
7
+ version = "0.3.35"
8
8
  description = "Build AI CLI (Typer)"
9
9
  requires-python = ">=3.11"
10
10
  dependencies = [
@@ -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