buildai-cli 0.3.80__tar.gz → 0.3.82__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 (38) hide show
  1. {buildai_cli-0.3.80 → buildai_cli-0.3.82}/PKG-INFO +1 -1
  2. buildai_cli-0.3.82/cli/commands/grid.py +440 -0
  3. {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/main.py +3 -0
  4. {buildai_cli-0.3.80 → buildai_cli-0.3.82}/pyproject.toml +1 -1
  5. {buildai_cli-0.3.80 → buildai_cli-0.3.82}/.gitignore +0 -0
  6. {buildai_cli-0.3.80 → buildai_cli-0.3.82}/AGENTS.md +0 -0
  7. {buildai_cli-0.3.80 → buildai_cli-0.3.82}/CLAUDE.md +0 -0
  8. {buildai_cli-0.3.80 → buildai_cli-0.3.82}/buildai_bootstrap.py +0 -0
  9. {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/__init__.py +0 -0
  10. {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/_has_core.py +0 -0
  11. {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/auth_local.py +0 -0
  12. {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/commands/__init__.py +0 -0
  13. {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/commands/api_proxy.py +0 -0
  14. {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/commands/auth.py +0 -0
  15. {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/commands/db/__init__.py +0 -0
  16. {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/commands/db/broker.py +0 -0
  17. {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/commands/db/common.py +0 -0
  18. {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/commands/db/migrate.py +0 -0
  19. {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/commands/db/query.py +0 -0
  20. {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/commands/db/schema.py +0 -0
  21. {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/commands/db/status.py +0 -0
  22. {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/commands/db/tunnel.py +0 -0
  23. {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/commands/dev.py +0 -0
  24. {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/commands/doctor.py +0 -0
  25. {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/commands/egoexo.py +0 -0
  26. {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/commands/gigcamera.py +0 -0
  27. {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/commands/processing.py +0 -0
  28. {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/config.py +0 -0
  29. {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/console.py +0 -0
  30. {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/context.py +0 -0
  31. {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/db_broker.py +0 -0
  32. {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/guard.py +0 -0
  33. {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/internal_api.py +0 -0
  34. {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/nl_query/__init__.py +0 -0
  35. {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/nl_query/dataset_tools.py +0 -0
  36. {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/ops_init.py +0 -0
  37. {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/output.py +0 -0
  38. {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/pagination.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: buildai-cli
3
- Version: 0.3.80
3
+ Version: 0.3.82
4
4
  Summary: Build AI CLI (Typer)
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: httpx>=0.27.0
@@ -0,0 +1,440 @@
1
+ """Workspace commands for the GCS-native Build AI grid builder."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import UTC, datetime
6
+ from os import getenv
7
+ from pathlib import Path
8
+ from typing import Literal
9
+ from uuid import uuid4
10
+
11
+ import typer
12
+
13
+ from cli.console import error, info, success
14
+
15
+ app = typer.Typer(
16
+ help="Build manifest-native video grids in gs://buildai-grids.", no_args_is_help=True
17
+ )
18
+
19
+ ZoomMode = Literal["auto", "always", "never"]
20
+ RunMode = Literal["local", "batch"]
21
+ DEFAULT_GRID_BUILDER_BATCH_IMAGE = (
22
+ "us-central1-docker.pkg.dev/data-470400/buildai-services/grid-builder:latest"
23
+ )
24
+
25
+
26
+ @app.command("validate")
27
+ def validate_manifest(
28
+ manifest: str = typer.Option(..., "--manifest", "-m", help="Local path or gs:// manifest URI."),
29
+ ) -> None:
30
+ """Validate a grid manifest without writing outputs."""
31
+
32
+ manifest_model, _state = _load_manifest(manifest)
33
+ success(
34
+ f"valid grid manifest: {manifest_model.grid_size}x{manifest_model.grid_size} "
35
+ f"({len(manifest_model.clips)} clips)"
36
+ )
37
+
38
+
39
+ @app.command("plan")
40
+ def plan_grid(
41
+ manifest: str = typer.Option(..., "--manifest", "-m", help="Local path or gs:// manifest URI."),
42
+ grid_key: str | None = typer.Option(
43
+ None, "--grid-key", help="Stable grid key for output prefix."
44
+ ),
45
+ zoom_out: ZoomMode = typer.Option("auto", "--zoom-out", help="auto, always, or never."),
46
+ write: bool = typer.Option(False, "--write", help="Write normalized manifest and plan to GCS."),
47
+ ) -> None:
48
+ """Create the deterministic render plan for a grid manifest."""
49
+
50
+ render_plan, state = _plan_from_manifest(
51
+ manifest, grid_key=grid_key, zoom_out=zoom_out, pin_sources=write
52
+ )
53
+ _print_plan(render_plan)
54
+ if write:
55
+ _write_plan(render_plan, state)
56
+ success(f"wrote plan: {render_plan.render_prefix}/plan.json")
57
+
58
+
59
+ @app.command("build")
60
+ def build_grid(
61
+ manifest: str = typer.Option(..., "--manifest", "-m", help="Local path or gs:// manifest URI."),
62
+ grid_key: str | None = typer.Option(
63
+ None, "--grid-key", help="Stable grid key for output prefix."
64
+ ),
65
+ zoom_out: ZoomMode = typer.Option("auto", "--zoom-out", help="auto, always, or never."),
66
+ mode: RunMode = typer.Option("batch", "--mode", help="local or batch."),
67
+ workers: int = typer.Option(
68
+ 1,
69
+ "--workers",
70
+ "-w",
71
+ min=1,
72
+ help="Batch task count; local mode runs one resumable worker loop.",
73
+ ),
74
+ work_dir: Path = typer.Option(
75
+ Path(".grid-builder-work"), "--work-dir", help="Local scratch directory."
76
+ ),
77
+ encoder: str = typer.Option("hevc_nvenc", "--encoder", help="FFmpeg video encoder."),
78
+ image: str | None = typer.Option(
79
+ None,
80
+ "--image",
81
+ help="Batch worker image URI; defaults to the repo-owned grid-builder image.",
82
+ ),
83
+ project_id: str | None = typer.Option(
84
+ None, "--project-id", help="GCP project for Batch submission."
85
+ ),
86
+ region: str = typer.Option("us-central1", "--region", help="Batch region."),
87
+ service_account: str | None = typer.Option(
88
+ None, "--service-account", help="Batch worker service account."
89
+ ),
90
+ network: str | None = typer.Option(None, "--network", help="Batch VPC network."),
91
+ subnetwork: str | None = typer.Option(None, "--subnetwork", help="Batch VPC subnetwork."),
92
+ write: bool = typer.Option(False, "--write", help="Required for GCS mutations and rendering."),
93
+ ) -> None:
94
+ """Write a plan and execute or submit the grid render."""
95
+
96
+ if not write:
97
+ error("grid build mutates gs://buildai-grids; rerun with --write")
98
+ raise typer.Exit(1)
99
+
100
+ render_plan, state = _plan_from_manifest(
101
+ manifest,
102
+ grid_key=grid_key,
103
+ zoom_out=zoom_out,
104
+ pin_sources=True,
105
+ workers=workers,
106
+ )
107
+ _write_plan(render_plan, state)
108
+ if mode == "batch":
109
+ job_name = _submit_batch(
110
+ render_plan=render_plan,
111
+ image=image,
112
+ project_id=project_id,
113
+ region=region,
114
+ service_account=service_account,
115
+ network=network,
116
+ subnetwork=subnetwork,
117
+ workers=workers,
118
+ encoder=encoder,
119
+ )
120
+ success(f"submitted Batch job: {job_name}")
121
+ else:
122
+ summary = _execute_local(render_plan, state, work_dir=work_dir, encoder=encoder)
123
+ success(
124
+ f"local render pass complete: {len(summary.completed)} completed, "
125
+ f"{len(summary.skipped)} skipped"
126
+ )
127
+ info(render_plan.render_prefix)
128
+
129
+
130
+ @app.command("resume")
131
+ def resume_grid(
132
+ render_prefix: str = typer.Argument(..., help="Render prefix under gs://buildai-grids."),
133
+ mode: RunMode = typer.Option("batch", "--mode", help="local or batch."),
134
+ workers: int = typer.Option(
135
+ 1,
136
+ "--workers",
137
+ "-w",
138
+ min=1,
139
+ help="Batch task count; local mode runs one resumable worker loop.",
140
+ ),
141
+ work_dir: Path = typer.Option(Path(".grid-builder-work"), "--work-dir"),
142
+ encoder: str = typer.Option("hevc_nvenc", "--encoder", help="FFmpeg video encoder."),
143
+ image: str | None = typer.Option(
144
+ None,
145
+ "--image",
146
+ help="Batch worker image URI; defaults to the repo-owned grid-builder image.",
147
+ ),
148
+ project_id: str | None = typer.Option(
149
+ None, "--project-id", help="GCP project for Batch submission."
150
+ ),
151
+ region: str = typer.Option("us-central1", "--region", help="Batch region."),
152
+ service_account: str | None = typer.Option(
153
+ None, "--service-account", help="Batch worker service account."
154
+ ),
155
+ network: str | None = typer.Option(None, "--network", help="Batch VPC network."),
156
+ subnetwork: str | None = typer.Option(None, "--subnetwork", help="Batch VPC subnetwork."),
157
+ write: bool = typer.Option(False, "--write", help="Required for GCS mutations and rendering."),
158
+ ) -> None:
159
+ """Resume a previously planned render, adopting completed receipts."""
160
+
161
+ if not write:
162
+ error("grid resume mutates gs://buildai-grids; rerun with --write")
163
+ raise typer.Exit(1)
164
+ state = _service("GcsState")()
165
+ render_plan = _load_plan_from_prefix(render_prefix, state)
166
+ if mode == "batch":
167
+ job_name = _submit_batch(
168
+ render_plan=render_plan,
169
+ image=image,
170
+ project_id=project_id,
171
+ region=region,
172
+ service_account=service_account,
173
+ network=network,
174
+ subnetwork=subnetwork,
175
+ workers=workers,
176
+ encoder=encoder,
177
+ )
178
+ success(f"submitted Batch job: {job_name}")
179
+ else:
180
+ summary = _execute_local(render_plan, state, work_dir=work_dir, encoder=encoder)
181
+ success(
182
+ f"local resume pass complete: {len(summary.completed)} completed, "
183
+ f"{len(summary.skipped)} skipped"
184
+ )
185
+
186
+
187
+ @app.command("status")
188
+ def grid_status(
189
+ render_prefix: str = typer.Argument(..., help="Render prefix under gs://buildai-grids."),
190
+ ) -> None:
191
+ """Show receipt status for a planned render."""
192
+
193
+ state = _service("GcsState")()
194
+ render_plan = _load_plan_from_prefix(render_prefix, state)
195
+ executor_cls = _service("GridTaskExecutor")
196
+ executor = executor_cls(
197
+ plan=render_plan,
198
+ state=state,
199
+ work_dir=Path(".grid-builder-status"),
200
+ owner="status",
201
+ handler=_noop_handler,
202
+ )
203
+ complete = sum(1 for task in render_plan.tasks if executor.receipt_is_valid(task))
204
+ pending = len(render_plan.tasks) - complete
205
+ info(f"render: {render_plan.render_id}")
206
+ info(f"grid: {render_plan.grid_key}")
207
+ info(f"tasks: {complete} complete, {pending} pending")
208
+ info(f"zoom_out: {'enabled' if render_plan.zoom_out_enabled else 'disabled'}")
209
+
210
+
211
+ @app.command("verify")
212
+ def verify_grid(
213
+ render_prefix: str = typer.Argument(..., help="Render prefix under gs://buildai-grids."),
214
+ ) -> None:
215
+ """Verify that every planned task has a valid receipt."""
216
+
217
+ state = _service("GcsState")()
218
+ render_plan = _load_plan_from_prefix(render_prefix, state)
219
+ executor_cls = _service("GridTaskExecutor")
220
+ executor = executor_cls(
221
+ plan=render_plan,
222
+ state=state,
223
+ work_dir=Path(".grid-builder-verify"),
224
+ owner="verify",
225
+ handler=_noop_handler,
226
+ )
227
+ missing = [task.task_id for task in render_plan.tasks if not executor.receipt_is_valid(task)]
228
+ if missing:
229
+ error(f"{len(missing)} tasks missing valid receipts")
230
+ for task_id in missing[:20]:
231
+ error(f" {task_id}")
232
+ raise typer.Exit(1)
233
+ success("all grid builder task receipts are valid")
234
+
235
+
236
+ def _load_manifest(manifest_uri: str, state=None):
237
+ """Load a local or GCS manifest while keeping service imports lazy."""
238
+
239
+ GridManifest = _service("GridManifest")
240
+ if manifest_uri.startswith("gs://"):
241
+ state = state or _service("GcsState")()
242
+ return GridManifest.model_validate(state.read_json(manifest_uri)), state
243
+ return GridManifest.model_validate_json(Path(manifest_uri).read_text(encoding="utf-8")), state
244
+
245
+
246
+ def _plan_from_manifest(
247
+ manifest_uri: str,
248
+ *,
249
+ grid_key: str | None,
250
+ zoom_out: ZoomMode,
251
+ pin_sources: bool,
252
+ workers: int = 1,
253
+ ):
254
+ """Normalize a manifest and build a render plan."""
255
+
256
+ state = _service("GcsState")() if pin_sources or manifest_uri.startswith("gs://") else None
257
+ manifest_model, state = _load_manifest(manifest_uri, state=state)
258
+ normalize_manifest = _service("normalize_manifest")
259
+ build_render_plan = _service("build_render_plan")
260
+ RenderProfile = _service("RenderProfile")
261
+ ZoomOutMode = _service("ZoomOutMode")
262
+ resolver = state if pin_sources else None
263
+ normalized = normalize_manifest(manifest_model, resolver=resolver, grid_key=grid_key)
264
+ profile = RenderProfile(zoom_out_mode=ZoomOutMode(zoom_out), worker_count=workers)
265
+ return build_render_plan(normalized, profile), state
266
+
267
+
268
+ def _write_plan(render_plan, state) -> None:
269
+ """Write immutable plan sidecars under the render prefix."""
270
+
271
+ if state is None:
272
+ raise RuntimeError("GCS state is required before writing a grid render plan")
273
+ sidecars = {
274
+ "manifest.normalized.json": render_plan.manifest,
275
+ "profile.json": render_plan.profile,
276
+ "plan.json": render_plan,
277
+ "metadata/status.json": {
278
+ "schema_key": "buildai.grid_builder.status",
279
+ "schema_version": "1.0",
280
+ "render_id": render_plan.render_id,
281
+ "grid_key": render_plan.grid_key,
282
+ "task_count": len(render_plan.tasks),
283
+ "zoom_out_enabled": render_plan.zoom_out_enabled,
284
+ },
285
+ }
286
+ for relative_path, payload in sidecars.items():
287
+ try:
288
+ state.upload_json(
289
+ f"{render_plan.render_prefix}/{relative_path}", payload, create_only=True
290
+ )
291
+ except FileExistsError:
292
+ pass
293
+
294
+
295
+ def _execute_local(render_plan, state, *, work_dir: Path, encoder: str):
296
+ """Run the worker loop locally for a render plan."""
297
+
298
+ if state is None:
299
+ raise RuntimeError("GCS state is required before executing a grid render")
300
+ GridTaskExecutor = _service("GridTaskExecutor")
301
+ GridRenderHandler = _service("GridRenderHandler")
302
+ executor = GridTaskExecutor(
303
+ plan=render_plan,
304
+ state=state,
305
+ work_dir=work_dir / render_plan.render_id,
306
+ owner="local#0",
307
+ handler=GridRenderHandler(encoder=encoder),
308
+ )
309
+ return executor.execute_until_complete(poll_interval_sec=2.0)
310
+
311
+
312
+ def _load_plan_from_prefix(render_prefix: str, state):
313
+ """Load a render plan from its GCS render prefix."""
314
+
315
+ RenderPlan = _service("RenderPlan")
316
+ return RenderPlan.model_validate(state.read_json(f"{render_prefix.rstrip('/')}/plan.json"))
317
+
318
+
319
+ def _submit_batch(
320
+ *,
321
+ render_plan,
322
+ image: str | None,
323
+ project_id: str | None,
324
+ region: str,
325
+ service_account: str | None,
326
+ network: str | None,
327
+ subnetwork: str | None,
328
+ workers: int,
329
+ encoder: str,
330
+ ) -> str:
331
+ """Submit a Cloud Batch worker fleet for a grid render plan."""
332
+
333
+ resolved_image = _grid_builder_batch_image(image)
334
+ if not all([project_id, service_account, network, subnetwork]):
335
+ error("--mode=batch requires --project-id, --service-account, --network, and --subnetwork")
336
+ raise typer.Exit(1)
337
+ from infra.batch import BatchClient, BatchJobConfig, ProvisioningModel
338
+
339
+ job_id = _batch_job_id(render_plan.render_id)
340
+ client = BatchClient(
341
+ project_id=project_id,
342
+ region=region,
343
+ vpc_network=network,
344
+ vpc_subnetwork=subnetwork,
345
+ service_account=service_account,
346
+ )
347
+ config = BatchJobConfig(
348
+ image=resolved_image,
349
+ entrypoint="python",
350
+ commands=[
351
+ "-m",
352
+ "services.grid_builder",
353
+ "worker",
354
+ "--plan-uri",
355
+ f"{render_plan.render_prefix}/plan.json",
356
+ "--work-dir",
357
+ "/tmp/grid-builder",
358
+ "--encoder",
359
+ encoder,
360
+ ],
361
+ machine_type="g2-standard-4",
362
+ gpu_type="nvidia-l4",
363
+ gpu_count=1,
364
+ cpu_milli=4000,
365
+ memory_mib=16384,
366
+ boot_disk_gb=500,
367
+ service_account=service_account,
368
+ )
369
+ return client.create_job(
370
+ job_id=job_id,
371
+ task_count=workers,
372
+ config=config,
373
+ provisioning_model=ProvisioningModel.SPOT,
374
+ labels={"buildai-grid": "true", "render-id": render_plan.render_id},
375
+ )
376
+
377
+
378
+ def _grid_builder_batch_image(image: str | None) -> str:
379
+ """Resolve the worker image while allowing operator overrides."""
380
+
381
+ return image or getenv("GRID_BUILDER_BATCH_IMAGE") or DEFAULT_GRID_BUILDER_BATCH_IMAGE
382
+
383
+
384
+ def _print_plan(render_plan) -> None:
385
+ """Print the compact plan summary users need before launch."""
386
+
387
+ info(f"render_id: {render_plan.render_id}")
388
+ info(f"render_prefix: {render_plan.render_prefix}")
389
+ info(f"lod_sizes: {', '.join(str(size) for size in render_plan.lod_sizes)}")
390
+ info(f"zoom_out: {'enabled' if render_plan.zoom_out_enabled else 'disabled'}")
391
+ info(f"tasks: {len(render_plan.tasks)}")
392
+
393
+
394
+ def _noop_handler(_task, _context) -> Path:
395
+ """Satisfy executor construction for receipt-only status commands."""
396
+
397
+ raise RuntimeError("status and verify commands do not execute render tasks")
398
+
399
+
400
+ def _service(name: str):
401
+ """Import service symbols lazily so the standalone CLI remains lightweight."""
402
+
403
+ try:
404
+ import services.grid_builder as grid_builder
405
+ except ImportError as exc:
406
+ error("grid commands require a workspace install: run with `uv run buildai` from the repo")
407
+ raise typer.Exit(1) from exc
408
+
409
+ if name in {
410
+ "GridManifest",
411
+ "RenderPlan",
412
+ "RenderProfile",
413
+ "build_render_plan",
414
+ "normalize_manifest",
415
+ }:
416
+ return getattr(grid_builder, name)
417
+ if name == "GcsState":
418
+ from services.grid_builder.gcs_state import GcsState
419
+
420
+ return GcsState
421
+ if name == "GridTaskExecutor":
422
+ from services.grid_builder.executor import GridTaskExecutor
423
+
424
+ return GridTaskExecutor
425
+ if name == "GridRenderHandler":
426
+ from services.grid_builder.render_tasks import GridRenderHandler
427
+
428
+ return GridRenderHandler
429
+ if name == "ZoomOutMode":
430
+ from services.grid_builder.planner import ZoomOutMode
431
+
432
+ return ZoomOutMode
433
+ raise KeyError(name)
434
+
435
+
436
+ def _batch_job_id(render_id: str) -> str:
437
+ """Return a unique Cloud Batch job id while preserving render traceability."""
438
+
439
+ attempt = datetime.now(UTC).strftime("%Y%m%d%H%M%S")
440
+ return f"grid-{render_id}-{attempt}-{uuid4().hex[:8]}"
@@ -38,6 +38,7 @@ def _bootstrap_repo_paths() -> None:
38
38
  return
39
39
 
40
40
  for package_root in (
41
+ repo_root,
41
42
  repo_root / "core",
42
43
  repo_root / "apps" / "buildai-api" / "shared",
43
44
  ):
@@ -319,10 +320,12 @@ def db_callback(
319
320
  if has_core():
320
321
  from cli.commands.db import app as db_app
321
322
  from cli.commands.gigcamera import app as gigcamera_app
323
+ from cli.commands.grid import app as grid_app
322
324
 
323
325
  db_app.callback()(db_callback)
324
326
  app.add_typer(db_app, name="db")
325
327
  app.add_typer(gigcamera_app, name="gigcamera")
328
+ app.add_typer(grid_app, name="grid")
326
329
 
327
330
 
328
331
  def main() -> None:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "buildai-cli"
7
- version = "0.3.80"
7
+ version = "0.3.82"
8
8
  description = "Build AI CLI (Typer)"
9
9
  requires-python = ">=3.11"
10
10
  dependencies = [
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes