buildai-cli 0.3.34__tar.gz → 0.3.36__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.36}/.gitignore +5 -0
- buildai_cli-0.3.36/CLAUDE 2.md +35 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.36}/CLAUDE.md +1 -3
- {buildai_cli-0.3.34 → buildai_cli-0.3.36}/PKG-INFO +1 -1
- {buildai_cli-0.3.34 → buildai_cli-0.3.36}/cli/commands/database.py +59 -92
- buildai_cli-0.3.36/cli/commands/jobs.py +193 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.36}/cli/commands/medoid.py +2 -2
- {buildai_cli-0.3.34 → buildai_cli-0.3.36}/cli/config.py +0 -18
- {buildai_cli-0.3.34 → buildai_cli-0.3.36}/cli/context.py +4 -19
- {buildai_cli-0.3.34 → buildai_cli-0.3.36}/cli/main.py +1 -6
- {buildai_cli-0.3.34 → buildai_cli-0.3.36}/cli/ops_init.py +5 -122
- {buildai_cli-0.3.34 → buildai_cli-0.3.36}/pyproject.toml +1 -1
- buildai_cli-0.3.34/cli/commands/dev.py +0 -279
- buildai_cli-0.3.34/cli/commands/jobs.py +0 -344
- buildai_cli-0.3.34/cli/commands/permissions.py +0 -160
- buildai_cli-0.3.34/cli/dev_context.py +0 -365
- {buildai_cli-0.3.34 → buildai_cli-0.3.36}/AGENTS.md +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.36}/cli/__init__.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.36}/cli/_has_core.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.36}/cli/commands/__init__.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.36}/cli/commands/api_proxy.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.36}/cli/commands/assets_cli.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.36}/cli/commands/auth_lite.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.36}/cli/commands/clips.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.36}/cli/commands/embed.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.36}/cli/commands/external.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.36}/cli/commands/inference.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.36}/cli/commands/keys.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.36}/cli/commands/operations.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.36}/cli/commands/partners.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.36}/cli/commands/projection.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.36}/cli/commands/query.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.36}/cli/commands/query_api.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.36}/cli/commands/reports.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.36}/cli/commands/schema.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.36}/cli/commands/search.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.36}/cli/commands/stats.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.36}/cli/commands/sync/ddl.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.36}/cli/commands/sync/models.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.36}/cli/commands/sync/queries.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.36}/cli/console.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.36}/cli/guard.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.36}/cli/nl_query/__init__.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.36}/cli/nl_query/dataset_tools.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.36}/cli/output.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.36}/cli/pagination.py +0 -0
- {buildai_cli-0.3.34 → buildai_cli-0.3.36}/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,35 @@
|
|
|
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
|
+
buildai admin --write database migrate all # Run migrations
|
|
20
|
+
buildai admin database diff --from preview --to production # Migration delta
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Guards
|
|
24
|
+
|
|
25
|
+
- `admin` subcommands require workspace install + gcloud IAM.
|
|
26
|
+
- Writes require `--write` flag.
|
|
27
|
+
- Production migrations prompt for confirmation.
|
|
28
|
+
- Worktree app/runtime targeting comes from explicit env vars and the repo Makefile, not a saved CLI context layer.
|
|
29
|
+
- `buildai admin --env` selects the explicit DB lane: `production`, `preview`, `dev`.
|
|
30
|
+
- Do not confuse `--env preview` with any old local preset names. The CLI no longer manages worktree deployment profiles.
|
|
31
|
+
|
|
32
|
+
## Reference
|
|
33
|
+
|
|
34
|
+
CLI mode guide: `docs/how-to/choose-buildai-mode.md`
|
|
35
|
+
Database roles: `docs/database-roles.md`
|
|
@@ -16,8 +16,6 @@ buildai query "SELECT count(*) FROM core.clips" # API-backed
|
|
|
16
16
|
buildai admin query "SELECT count(*) FROM core.clips" # DB-direct
|
|
17
17
|
buildai admin schema tables # Schema introspection
|
|
18
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
19
|
buildai admin --write database migrate all # Run migrations
|
|
22
20
|
buildai admin database status # Migration status
|
|
23
21
|
```
|
|
@@ -27,7 +25,7 @@ buildai admin database status # Migration status
|
|
|
27
25
|
- `admin` subcommands require workspace install + gcloud IAM.
|
|
28
26
|
- Writes require `--write` flag.
|
|
29
27
|
- Production migrations prompt for confirmation.
|
|
30
|
-
- Worktree app/runtime targeting comes from
|
|
28
|
+
- Worktree app/runtime targeting comes from explicit env vars and the repo Makefile, not a saved CLI context layer.
|
|
31
29
|
- `buildai admin --env` selects the explicit DB lane: `production`, `dev`.
|
|
32
30
|
|
|
33
31
|
## Reference
|
|
@@ -12,7 +12,6 @@ All database operations under one command group:
|
|
|
12
12
|
buildai admin database discover # Retired legacy discovery flow
|
|
13
13
|
buildai admin database status # Overall database health summary
|
|
14
14
|
buildai admin database roles # Role management
|
|
15
|
-
buildai admin database roles-setup # Apply least-privilege roles/grants
|
|
16
15
|
buildai admin database audit # Permission auditing
|
|
17
16
|
buildai admin database seed # Run seed data scripts
|
|
18
17
|
"""
|
|
@@ -26,9 +25,7 @@ from typing import Any, Sequence
|
|
|
26
25
|
|
|
27
26
|
import asyncpg
|
|
28
27
|
import typer
|
|
29
|
-
from
|
|
30
|
-
from infra.migration_tracking import audit_migration_tracking, load_tracker_policy
|
|
31
|
-
from infra.settings import Settings, switch_environment
|
|
28
|
+
from api.service_catalog import list_api_services
|
|
32
29
|
from rich.panel import Panel
|
|
33
30
|
from rich.syntax import Syntax
|
|
34
31
|
from rich.table import Table
|
|
@@ -62,6 +59,8 @@ from cli.context import (
|
|
|
62
59
|
resolve_admin_connection_config,
|
|
63
60
|
)
|
|
64
61
|
from cli.guard import require_write
|
|
62
|
+
from infra.migration_tracking import audit_migration_tracking, load_tracker_policy
|
|
63
|
+
from infra.settings import Settings, switch_environment
|
|
65
64
|
|
|
66
65
|
# =============================================================================
|
|
67
66
|
# Constants
|
|
@@ -84,7 +83,7 @@ async def _get_admin_connection(settings: Settings):
|
|
|
84
83
|
|
|
85
84
|
Resolution order:
|
|
86
85
|
1. ALLOYDB_ADMIN_IAM_USER_* env vars (explicit override)
|
|
87
|
-
2.
|
|
86
|
+
2. explicit ALLOYDB_ADMIN_IAM_USER_* / ALLOYDB_ADMIN_IMPERSONATE_SA_* env wiring
|
|
88
87
|
3. ALLOYDB_PSW_PROD password fallback
|
|
89
88
|
"""
|
|
90
89
|
return await open_admin_database(settings)
|
|
@@ -227,6 +226,60 @@ class DatabaseStatus:
|
|
|
227
226
|
return "READY FOR TESTING"
|
|
228
227
|
|
|
229
228
|
|
|
229
|
+
@dataclass(frozen=True)
|
|
230
|
+
class _RuntimeDbTarget:
|
|
231
|
+
service_slug: str
|
|
232
|
+
gcp_service_account_email: str
|
|
233
|
+
alloydb_iam_user: str
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _runtime_db_targets() -> list[_RuntimeDbTarget]:
|
|
237
|
+
"""Return the current runtime principals that should exist as AlloyDB IAM users."""
|
|
238
|
+
api_targets = [
|
|
239
|
+
_RuntimeDbTarget(
|
|
240
|
+
service_slug=service.service_slug,
|
|
241
|
+
gcp_service_account_email=service.runtime_service_account,
|
|
242
|
+
alloydb_iam_user=service.runtime_db_user,
|
|
243
|
+
)
|
|
244
|
+
for service in list_api_services()
|
|
245
|
+
]
|
|
246
|
+
worker_targets = [
|
|
247
|
+
_RuntimeDbTarget(
|
|
248
|
+
service_slug="mcp",
|
|
249
|
+
gcp_service_account_email="mcp-sa@data-470400.iam.gserviceaccount.com",
|
|
250
|
+
alloydb_iam_user="mcp-sa@data-470400.iam",
|
|
251
|
+
),
|
|
252
|
+
_RuntimeDbTarget(
|
|
253
|
+
service_slug="frame-extractor",
|
|
254
|
+
gcp_service_account_email="frame-extractor-sa@data-470400.iam.gserviceaccount.com",
|
|
255
|
+
alloydb_iam_user="frame-extractor-sa@data-470400.iam",
|
|
256
|
+
),
|
|
257
|
+
_RuntimeDbTarget(
|
|
258
|
+
service_slug="frame-embed",
|
|
259
|
+
gcp_service_account_email="frame-embed-sa@data-470400.iam.gserviceaccount.com",
|
|
260
|
+
alloydb_iam_user="frame-embed-sa@data-470400.iam",
|
|
261
|
+
),
|
|
262
|
+
_RuntimeDbTarget(
|
|
263
|
+
service_slug="frame-inference",
|
|
264
|
+
gcp_service_account_email="frame-inference-sa@data-470400.iam.gserviceaccount.com",
|
|
265
|
+
alloydb_iam_user="frame-inference-sa@data-470400.iam",
|
|
266
|
+
),
|
|
267
|
+
_RuntimeDbTarget(
|
|
268
|
+
service_slug="migrations-runner",
|
|
269
|
+
gcp_service_account_email="migrations-runner-sa@data-470400.iam.gserviceaccount.com",
|
|
270
|
+
alloydb_iam_user="migrations-runner-sa@data-470400.iam",
|
|
271
|
+
),
|
|
272
|
+
]
|
|
273
|
+
seen: set[str] = set()
|
|
274
|
+
targets: list[_RuntimeDbTarget] = []
|
|
275
|
+
for target in [*api_targets, *worker_targets]:
|
|
276
|
+
if target.gcp_service_account_email in seen:
|
|
277
|
+
continue
|
|
278
|
+
seen.add(target.gcp_service_account_email)
|
|
279
|
+
targets.append(target)
|
|
280
|
+
return targets
|
|
281
|
+
|
|
282
|
+
|
|
230
283
|
def _migration_requires_system_admin(path: Path) -> bool:
|
|
231
284
|
for line in path.read_text(encoding="utf-8").splitlines()[:10]:
|
|
232
285
|
if line.strip().lower() == "-- requires-system-admin: true":
|
|
@@ -1392,90 +1445,6 @@ def roles(ctx: typer.Context) -> None:
|
|
|
1392
1445
|
asyncio.run(run())
|
|
1393
1446
|
|
|
1394
1447
|
|
|
1395
|
-
@app.command("roles-setup")
|
|
1396
|
-
def roles_setup(
|
|
1397
|
-
ctx: typer.Context,
|
|
1398
|
-
dry_run: Annotated[bool, typer.Option("--dry-run", help="Show SQL without executing")] = False,
|
|
1399
|
-
as_admin: Annotated[
|
|
1400
|
-
bool,
|
|
1401
|
-
typer.Option(
|
|
1402
|
-
"--as-admin",
|
|
1403
|
-
help="Connect as postgres user via password auth (requires ALLOYDB_PSW_PROD)",
|
|
1404
|
-
),
|
|
1405
|
-
] = False,
|
|
1406
|
-
yes: Annotated[
|
|
1407
|
-
bool, typer.Option("--yes", "-y", help="Skip confirmation for production")
|
|
1408
|
-
] = False,
|
|
1409
|
-
) -> None:
|
|
1410
|
-
"""
|
|
1411
|
-
Apply least-privilege role and grant setup.
|
|
1412
|
-
|
|
1413
|
-
Uses scripts/ops/create-db-roles-leastpriv.sql.
|
|
1414
|
-
"""
|
|
1415
|
-
settings: Settings = ctx.obj["settings"]
|
|
1416
|
-
|
|
1417
|
-
if not dry_run:
|
|
1418
|
-
require_write(ctx, "Database role setup")
|
|
1419
|
-
|
|
1420
|
-
sql_path = REPO_ROOT / "scripts" / "ops" / "create-db-roles-leastpriv.sql"
|
|
1421
|
-
|
|
1422
|
-
if not sql_path.exists():
|
|
1423
|
-
error(f"SQL file not found: {sql_path}")
|
|
1424
|
-
raise typer.Exit(1)
|
|
1425
|
-
|
|
1426
|
-
sql_content = sql_path.read_text()
|
|
1427
|
-
|
|
1428
|
-
if dry_run:
|
|
1429
|
-
info("SQL to execute:")
|
|
1430
|
-
console.print(Syntax(sql_content, "sql", theme="monokai", line_numbers=False))
|
|
1431
|
-
return
|
|
1432
|
-
|
|
1433
|
-
if settings.is_production and not yes:
|
|
1434
|
-
warning("This will modify PRODUCTION database roles!")
|
|
1435
|
-
if not typer.confirm("Are you sure you want to continue?"):
|
|
1436
|
-
raise typer.Abort()
|
|
1437
|
-
|
|
1438
|
-
async def run() -> None:
|
|
1439
|
-
db = None
|
|
1440
|
-
connection_context = None
|
|
1441
|
-
using_owner_role = False
|
|
1442
|
-
|
|
1443
|
-
try:
|
|
1444
|
-
if as_admin:
|
|
1445
|
-
info("Connecting as postgres (password auth)...")
|
|
1446
|
-
db = await _get_admin_connection(settings)
|
|
1447
|
-
conn = db.conn
|
|
1448
|
-
else:
|
|
1449
|
-
connection_context = get_connection(settings)
|
|
1450
|
-
conn = await connection_context.__aenter__()
|
|
1451
|
-
using_owner_role = await _set_owner_role(conn)
|
|
1452
|
-
if using_owner_role:
|
|
1453
|
-
dim("Using role: buildai_owner")
|
|
1454
|
-
else:
|
|
1455
|
-
dim("Role buildai_owner not available, using default permissions")
|
|
1456
|
-
|
|
1457
|
-
await conn.execute(sql_content)
|
|
1458
|
-
success("Role setup completed.")
|
|
1459
|
-
|
|
1460
|
-
except Exception as e:
|
|
1461
|
-
error(f"Role setup failed: {e}")
|
|
1462
|
-
raise typer.Exit(1)
|
|
1463
|
-
|
|
1464
|
-
finally:
|
|
1465
|
-
if using_owner_role:
|
|
1466
|
-
try:
|
|
1467
|
-
await conn.execute("RESET ROLE")
|
|
1468
|
-
except Exception:
|
|
1469
|
-
pass
|
|
1470
|
-
|
|
1471
|
-
if db is not None:
|
|
1472
|
-
await db.close()
|
|
1473
|
-
elif connection_context is not None:
|
|
1474
|
-
await connection_context.__aexit__(None, None, None)
|
|
1475
|
-
|
|
1476
|
-
asyncio.run(run())
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
1448
|
@app.command()
|
|
1480
1449
|
def audit(
|
|
1481
1450
|
ctx: typer.Context,
|
|
@@ -1502,7 +1471,7 @@ def audit(
|
|
|
1502
1471
|
|
|
1503
1472
|
# Check IAM roles
|
|
1504
1473
|
info("Checking GCP IAM roles...")
|
|
1505
|
-
runtime_targets =
|
|
1474
|
+
runtime_targets = _runtime_db_targets()
|
|
1506
1475
|
required_roles = ["roles/alloydb.client", "roles/alloydb.databaseUser"]
|
|
1507
1476
|
|
|
1508
1477
|
iam_table = Table(title="IAM Role Audit")
|
|
@@ -1516,8 +1485,6 @@ def audit(
|
|
|
1516
1485
|
for target in runtime_targets:
|
|
1517
1486
|
sa_email = target.gcp_service_account_email
|
|
1518
1487
|
db_login = target.alloydb_iam_user
|
|
1519
|
-
if sa_email is None or db_login is None:
|
|
1520
|
-
continue
|
|
1521
1488
|
sa_name = sa_email.split("@", 1)[0]
|
|
1522
1489
|
row = [sa_name, db_login]
|
|
1523
1490
|
|
|
@@ -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",
|
|
@@ -7,8 +7,6 @@ import os
|
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
from typing import Any
|
|
9
9
|
|
|
10
|
-
from cli.dev_context import load_dev_context
|
|
11
|
-
|
|
12
10
|
CONFIG_DIR = Path.home() / ".buildai"
|
|
13
11
|
CREDENTIALS_PATH = CONFIG_DIR / "credentials.json"
|
|
14
12
|
DEFAULT_API_URL = "https://api.build.ai"
|
|
@@ -41,13 +39,6 @@ def clear_credential() -> None:
|
|
|
41
39
|
CREDENTIALS_PATH.unlink()
|
|
42
40
|
|
|
43
41
|
|
|
44
|
-
def _workspace_context() -> dict[str, Any] | None:
|
|
45
|
-
try:
|
|
46
|
-
return load_dev_context()
|
|
47
|
-
except Exception:
|
|
48
|
-
return None
|
|
49
|
-
|
|
50
|
-
|
|
51
42
|
def _clean_str(value: Any) -> str | None:
|
|
52
43
|
if not isinstance(value, str):
|
|
53
44
|
return None
|
|
@@ -74,11 +65,6 @@ def resolve_api_url(*, include_workspace: bool = True) -> str:
|
|
|
74
65
|
env_api_url = os.getenv("BUILDAI_API_URL")
|
|
75
66
|
if env_api_url:
|
|
76
67
|
return env_api_url.rstrip("/")
|
|
77
|
-
if include_workspace:
|
|
78
|
-
workspace_context = _workspace_context()
|
|
79
|
-
workspace_api_url = _clean_str((workspace_context or {}).get("api_url"))
|
|
80
|
-
if workspace_api_url:
|
|
81
|
-
return workspace_api_url.rstrip("/")
|
|
82
68
|
api_url = load_credentials().get("api_url")
|
|
83
69
|
if isinstance(api_url, str) and api_url:
|
|
84
70
|
return api_url.rstrip("/")
|
|
@@ -109,8 +95,4 @@ def resolve_cli_profile(explicit: str | None = None) -> str:
|
|
|
109
95
|
env_profile = _clean_str(os.getenv("BUILDAI_CLI_PROFILE"))
|
|
110
96
|
if env_profile:
|
|
111
97
|
return env_profile
|
|
112
|
-
workspace_context = _workspace_context()
|
|
113
|
-
workspace_profile = _clean_str((workspace_context or {}).get("cli_access_profile"))
|
|
114
|
-
if workspace_profile:
|
|
115
|
-
return workspace_profile
|
|
116
98
|
return "internal_viewer"
|
|
@@ -22,9 +22,9 @@ from dataclasses import dataclass
|
|
|
22
22
|
from typing import TYPE_CHECKING, AsyncGenerator
|
|
23
23
|
|
|
24
24
|
import asyncpg
|
|
25
|
-
from infra.settings import Settings, get_settings
|
|
26
25
|
|
|
27
26
|
from infra import Database, get_logger
|
|
27
|
+
from infra.settings import Settings, get_settings
|
|
28
28
|
|
|
29
29
|
if TYPE_CHECKING:
|
|
30
30
|
from dal.context import Context
|
|
@@ -38,7 +38,6 @@ _UNRESTRICTED_CLI_PROFILES = frozenset(
|
|
|
38
38
|
{
|
|
39
39
|
"internal_admin",
|
|
40
40
|
"internal_viewer",
|
|
41
|
-
"analyst",
|
|
42
41
|
"developer",
|
|
43
42
|
"operator",
|
|
44
43
|
"ml_engineer",
|
|
@@ -82,25 +81,12 @@ def resolve_admin_connection_config(
|
|
|
82
81
|
) -> AdminConnectionConfig | None:
|
|
83
82
|
"""Resolve the canonical admin DB login for this environment, if one exists.
|
|
84
83
|
|
|
85
|
-
Resolution order is explicit env overrides first, then the
|
|
86
|
-
|
|
87
|
-
privileged database commands.
|
|
84
|
+
Resolution order is explicit env overrides first, then the production
|
|
85
|
+
password fallback already used by privileged database commands.
|
|
88
86
|
"""
|
|
89
|
-
from infra.deployment_profiles import resolve_deployment_profile
|
|
90
|
-
|
|
91
87
|
admin_iam_user = settings.effective_alloydb_admin_iam_user
|
|
92
88
|
admin_impersonate_sa = settings.effective_alloydb_admin_impersonate_sa
|
|
93
89
|
|
|
94
|
-
if not admin_iam_user or not admin_impersonate_sa:
|
|
95
|
-
try:
|
|
96
|
-
profile_name = os.environ.get("BUILDAI_DEPLOYMENT_PROFILE", "production")
|
|
97
|
-
profile = resolve_deployment_profile(profile_name)
|
|
98
|
-
if profile.database_admin:
|
|
99
|
-
admin_iam_user = admin_iam_user or profile.database_admin.iam_user
|
|
100
|
-
admin_impersonate_sa = admin_impersonate_sa or profile.database_admin.impersonate_sa
|
|
101
|
-
except (ValueError, KeyError):
|
|
102
|
-
pass
|
|
103
|
-
|
|
104
90
|
if admin_iam_user and admin_impersonate_sa:
|
|
105
91
|
return AdminConnectionConfig(
|
|
106
92
|
user=admin_iam_user,
|
|
@@ -137,8 +123,7 @@ async def open_admin_database(settings: Settings) -> Database:
|
|
|
137
123
|
if config is None:
|
|
138
124
|
raise RuntimeError(
|
|
139
125
|
"Admin credentials not configured for this environment. "
|
|
140
|
-
"Set ALLOYDB_ADMIN_IAM_USER_* / ALLOYDB_ADMIN_IMPERSONATE_SA_
|
|
141
|
-
"define database_admin in deployment_profiles.yaml."
|
|
126
|
+
"Set ALLOYDB_ADMIN_IAM_USER_* / ALLOYDB_ADMIN_IMPERSONATE_SA_*."
|
|
142
127
|
)
|
|
143
128
|
|
|
144
129
|
ip_type = "PRIVATE" if settings.use_private_ip else "PUBLIC"
|
|
@@ -18,7 +18,6 @@ from cli.commands.api_proxy import api
|
|
|
18
18
|
from cli.commands.assets_cli import app as assets_app
|
|
19
19
|
from cli.commands.auth_lite import login, logout, whoami
|
|
20
20
|
from cli.commands.clips import app as clips_app
|
|
21
|
-
from cli.commands.dev import app as dev_app
|
|
22
21
|
from cli.commands.inference import app as inference_app
|
|
23
22
|
from cli.commands.jobs import app as jobs_app
|
|
24
23
|
from cli.commands.query_api import query
|
|
@@ -208,7 +207,6 @@ Build AI CLI — query data, run inference, sign media URLs.
|
|
|
208
207
|
|
|
209
208
|
Get started:
|
|
210
209
|
uv tool install buildai-cli Install the standalone CLI
|
|
211
|
-
uv run buildai dev use local Set this worktree's dev target
|
|
212
210
|
buildai login Authenticate (opens browser)
|
|
213
211
|
buildai whoami Show your identity and permissions
|
|
214
212
|
buildai query "SELECT count(*) FROM core.clips"
|
|
@@ -253,7 +251,6 @@ app.command("whoami")(whoami)
|
|
|
253
251
|
app.command("logout")(logout)
|
|
254
252
|
app.command("query")(query)
|
|
255
253
|
|
|
256
|
-
app.add_typer(dev_app, name="dev")
|
|
257
254
|
app.add_typer(clips_app, name="clips")
|
|
258
255
|
app.add_typer(assets_app, name="assets")
|
|
259
256
|
app.add_typer(inference_app, name="inference")
|
|
@@ -275,7 +272,7 @@ def admin_callback(
|
|
|
275
272
|
None,
|
|
276
273
|
"--env",
|
|
277
274
|
"-e",
|
|
278
|
-
help="Target environment. Defaults to APP_ENV, then
|
|
275
|
+
help="Target environment. Defaults to APP_ENV, then production.",
|
|
279
276
|
),
|
|
280
277
|
auth: str = typer.Option(None, "--auth", "-a", help="Auth method: iam or password."),
|
|
281
278
|
user: str = typer.Option(None, "--user", "-u", help="Override database user."),
|
|
@@ -306,7 +303,6 @@ if has_core():
|
|
|
306
303
|
from cli.commands.medoid import app as medoid_app
|
|
307
304
|
from cli.commands.operations import app as operations_app
|
|
308
305
|
from cli.commands.partners import app as partners_app
|
|
309
|
-
from cli.commands.permissions import app as permissions_app
|
|
310
306
|
from cli.commands.projection import app as projection_app
|
|
311
307
|
from cli.commands.query import query as admin_query
|
|
312
308
|
from cli.commands.schema import app as schema_app
|
|
@@ -321,7 +317,6 @@ if has_core():
|
|
|
321
317
|
admin_app.add_typer(embed_app, name="embeddings")
|
|
322
318
|
admin_app.add_typer(database_app, name="database")
|
|
323
319
|
admin_app.add_typer(external_app, name="external")
|
|
324
|
-
admin_app.add_typer(permissions_app, name="permissions")
|
|
325
320
|
admin_app.add_typer(projection_app, name="projection")
|
|
326
321
|
admin_app.add_typer(medoid_app, name="medoid")
|
|
327
322
|
|