buildai-cli 0.3.150__tar.gz → 0.3.151__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.150 → buildai_cli-0.3.151}/Dockerfile.rescaling-migration-jobs +1 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/Dockerfile.rescaling-migration-jobs.dockerignore +3 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/PKG-INFO +1 -1
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/commands/egoexo.py +2 -0
- buildai_cli-0.3.151/cli/commands/egoexo_config.py +557 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/pyproject.toml +1 -1
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/.gitignore +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/AGENTS.md +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/CLAUDE.md +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/buildai_bootstrap.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/__init__.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/_has_core.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/auth_local.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/commands/__init__.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/commands/api_proxy.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/commands/auth.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/commands/db/__init__.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/commands/db/broker.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/commands/db/common.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/commands/db/migrate.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/commands/db/query.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/commands/db/rescaling.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/commands/db/schema.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/commands/db/status.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/commands/db/tunnel.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/commands/dev.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/commands/doctor.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/commands/ego_frame_search.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/commands/gigcamera.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/commands/ingest.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/commands/ingest_docs.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/commands/intake.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/commands/label_replay_recovery.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/commands/labeling.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/commands/processing.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/commands/rescaling.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/commands/spec.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/commands/spec_pr.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/config.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/console.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/context.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/db_broker.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/guard.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/internal_api.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/labeling_onboarding.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/main.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/nl_query/__init__.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/nl_query/dataset_tools.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/ops_init.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/output.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/pagination.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/rescaling_migration/__init__.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/rescaling_migration/canonical.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/rescaling_migration/export.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/rescaling_migration/feed_proofs.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/rescaling_migration/fence.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/rescaling_migration/importer.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/rescaling_migration/jobs.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/rescaling_migration/management.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/rescaling_migration/orchestrator.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/rescaling_migration/probes.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/rescaling_migration/reconcile.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/rescaling_migration/run_record.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/rescaling_migration/runtime.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/rescaling_migration/sql/legacy_export_closeout.sql +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/rescaling_migration/sql/legacy_export_prepare.sql +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/rescaling_migration/sql/legacy_fence.sql +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/rescaling_migration/sql/legacy_unfreeze.sql +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/rescaling_migration/sql/v2_export_closeout.sql +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/rescaling_migration/sql/v2_export_prepare.sql +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/rescaling_migration/sql/v2_fence.sql +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/rescaling_migration/sql/v2_unfreeze.sql +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/rescaling_migration/storage.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/rescaling_migration/transform.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/rls_guard.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/rescaling_logical_backup/__init__.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/rescaling_logical_backup/__main__.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/rescaling_migration_job/__init__.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/rescaling_migration_job/__main__.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/rescaling_recovery_extractor/__init__.py +0 -0
- {buildai_cli-0.3.150 → buildai_cli-0.3.151}/rescaling_recovery_extractor/__main__.py +0 -0
|
@@ -20,6 +20,7 @@ WORKDIR /app
|
|
|
20
20
|
# uv must see every workspace member declaration to honor the checked-in lockfile.
|
|
21
21
|
COPY pyproject.toml uv.lock ./
|
|
22
22
|
COPY core/pyproject.toml core/pyproject.toml
|
|
23
|
+
COPY packages/egoexo/pyproject.toml packages/egoexo/pyproject.toml
|
|
23
24
|
COPY services/pyproject.toml services/pyproject.toml
|
|
24
25
|
COPY apps/buildai/buildai-api/pyproject.toml apps/buildai/buildai-api/pyproject.toml
|
|
25
26
|
COPY apps/buildai/buildai-cli/pyproject.toml apps/buildai/buildai-cli/pyproject.toml
|
|
@@ -10,10 +10,12 @@ import httpx
|
|
|
10
10
|
import typer
|
|
11
11
|
|
|
12
12
|
from cli.commands.api_proxy import _build_url
|
|
13
|
+
from cli.commands.egoexo_config import app as config_app
|
|
13
14
|
from cli.config import resolve_api_url, resolve_credential
|
|
14
15
|
from cli.console import error
|
|
15
16
|
|
|
16
17
|
app = typer.Typer(help="Inspect EgoExo verdict-plane outcomes.", no_args_is_help=True)
|
|
18
|
+
app.add_typer(config_app, name="config")
|
|
17
19
|
|
|
18
20
|
|
|
19
21
|
def _request(path: str, *, query_params: list[str] | None = None) -> None:
|
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
"""Operator commands for the EgoExo runtime configuration store.
|
|
2
|
+
|
|
3
|
+
The store is one JSON object in the capture bucket. The processing API reads it
|
|
4
|
+
every thirty seconds and lets each stored field override the matching environment
|
|
5
|
+
value. These commands read the object, show the effective configuration, and
|
|
6
|
+
write new revisions. Every write needs ``--write --reason``. Reads never change
|
|
7
|
+
anything.
|
|
8
|
+
|
|
9
|
+
The commands need the workspace ``egoexo`` package and the Google Cloud Storage
|
|
10
|
+
client, so run them from the repo with ``uv run buildai egoexo config ...``.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import sys
|
|
17
|
+
import tempfile
|
|
18
|
+
from dataclasses import replace
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
import typer
|
|
23
|
+
|
|
24
|
+
from cli.console import error, info
|
|
25
|
+
|
|
26
|
+
app = typer.Typer(
|
|
27
|
+
help="Show and change the EgoExo runtime configuration store.", no_args_is_help=True
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
DEPLOYMENT_RELATIVE = Path("infra/production/services/processing-api")
|
|
31
|
+
LOCAL_JSON_MAX_BYTES = 1024 * 1024
|
|
32
|
+
ASSET_PREFIX = "derived/runtime-assets/"
|
|
33
|
+
|
|
34
|
+
URI_OPTION = typer.Option(
|
|
35
|
+
None,
|
|
36
|
+
"--uri",
|
|
37
|
+
envvar="EGOEXO_RUNTIME_CONFIG_URI",
|
|
38
|
+
help="Runtime config object. Default: the production object in gs://buildai-ego-exo.",
|
|
39
|
+
)
|
|
40
|
+
DEPLOYMENT_OPTION = typer.Option(
|
|
41
|
+
None,
|
|
42
|
+
"--deployment",
|
|
43
|
+
help=(
|
|
44
|
+
"Checked-in deployment directory whose egoexo-recipe.json and "
|
|
45
|
+
"egoexo-trusted-assets.json are the environment fallback shown for env fields. "
|
|
46
|
+
"Default: infra/production/services/processing-api when run inside the repo."
|
|
47
|
+
),
|
|
48
|
+
)
|
|
49
|
+
WRITE_OPTION = typer.Option(False, "--write", help="Write the revision. Omit for a dry run.")
|
|
50
|
+
REASON_OPTION = typer.Option(
|
|
51
|
+
None, "--reason", help="Why this revision exists. Required with --write."
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _runtime_config():
|
|
56
|
+
"""Import the store module lazily so installed clients without it get a clear message."""
|
|
57
|
+
try:
|
|
58
|
+
from egoexo.runtime.pipeline import runtime_config
|
|
59
|
+
except ImportError as exc:
|
|
60
|
+
error(
|
|
61
|
+
"'buildai egoexo config' needs the workspace egoexo package and the GCS client.\n"
|
|
62
|
+
" Run it from the repo: uv run buildai egoexo config ..."
|
|
63
|
+
)
|
|
64
|
+
raise typer.Exit(1) from exc
|
|
65
|
+
return runtime_config
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _store(uri: str | None):
|
|
69
|
+
"""Build a store on the real GCS adapter. Tests replace this function."""
|
|
70
|
+
rc = _runtime_config()
|
|
71
|
+
from egoexo.adapters.gcs.publication import GCSStorage
|
|
72
|
+
|
|
73
|
+
return rc.RuntimeConfigStore(uri or rc.DEFAULT_RUNTIME_CONFIG_URI, storage=GCSStorage())
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _author() -> str:
|
|
77
|
+
"""Return the active gcloud account. It is recorded as the revision author."""
|
|
78
|
+
from cli.auth_local import _gcloud_value
|
|
79
|
+
|
|
80
|
+
account = _gcloud_value("account")
|
|
81
|
+
if not account:
|
|
82
|
+
error(
|
|
83
|
+
"No active gcloud account. Run `gcloud auth login` first; the account is "
|
|
84
|
+
"recorded as the revision author."
|
|
85
|
+
)
|
|
86
|
+
raise typer.Exit(1)
|
|
87
|
+
return account
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _deployment_dir(explicit: Path | None) -> Path | None:
|
|
91
|
+
"""Return the checked-in deployment directory, or None when it is not available."""
|
|
92
|
+
if explicit is not None:
|
|
93
|
+
if not explicit.is_dir():
|
|
94
|
+
error(f"Deployment directory not found: {explicit}")
|
|
95
|
+
raise typer.Exit(1)
|
|
96
|
+
return explicit
|
|
97
|
+
for root in (Path.cwd(), *Path.cwd().parents):
|
|
98
|
+
candidate = root / DEPLOYMENT_RELATIVE
|
|
99
|
+
if (candidate / "egoexo-recipe.json").is_file():
|
|
100
|
+
return candidate
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _load_json_file(path: Path) -> Any:
|
|
105
|
+
"""Read a bounded local JSON file."""
|
|
106
|
+
if not path.is_file():
|
|
107
|
+
error(f"File not found: {path}")
|
|
108
|
+
raise typer.Exit(1)
|
|
109
|
+
data = path.read_bytes()
|
|
110
|
+
if len(data) > LOCAL_JSON_MAX_BYTES:
|
|
111
|
+
error(f"{path} exceeds {LOCAL_JSON_MAX_BYTES} bytes")
|
|
112
|
+
raise typer.Exit(1)
|
|
113
|
+
try:
|
|
114
|
+
return json.loads(data)
|
|
115
|
+
except ValueError as exc:
|
|
116
|
+
error(f"{path} is not valid JSON: {exc}")
|
|
117
|
+
raise typer.Exit(1) from exc
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _deployment_env(deployment: Path | None):
|
|
121
|
+
"""Build the environment fallback from checked-in files. Flags stay unknown."""
|
|
122
|
+
rc = _runtime_config()
|
|
123
|
+
if deployment is None:
|
|
124
|
+
return rc.EnvironmentConfig(), None
|
|
125
|
+
recipe = deployment / "egoexo-recipe.json"
|
|
126
|
+
assets = deployment / "egoexo-trusted-assets.json"
|
|
127
|
+
return (
|
|
128
|
+
rc.EnvironmentConfig(
|
|
129
|
+
recipe_json=recipe.read_text() if recipe.is_file() else None,
|
|
130
|
+
trusted_assets_json=assets.read_text() if assets.is_file() else "[]",
|
|
131
|
+
),
|
|
132
|
+
deployment,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _read_base(store):
|
|
137
|
+
"""Read the current document once. Returns (document, generation, problem)."""
|
|
138
|
+
try:
|
|
139
|
+
document, generation = store.read()
|
|
140
|
+
return document, generation, None
|
|
141
|
+
except (ValueError, TypeError, KeyError) as exc:
|
|
142
|
+
generation = str(store.storage.metadata(store.uri)["generation"])
|
|
143
|
+
return None, generation, f"{type(exc).__name__}: {exc}"
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _emit(payload: dict[str, Any]) -> None:
|
|
147
|
+
sys.stdout.write(json.dumps(payload, indent=2, default=str) + "\n")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _recipe_summary(recipe: dict[str, Any]) -> dict[str, Any]:
|
|
151
|
+
stages = {}
|
|
152
|
+
for stage, spec in sorted(recipe.get("preset", {}).items()):
|
|
153
|
+
entry: dict[str, Any] = {"method": spec.get("method")}
|
|
154
|
+
if "config" in spec:
|
|
155
|
+
entry["config_uri"] = spec["config"].get("uri")
|
|
156
|
+
for key in ("frames_per_job", "selection", "selections"):
|
|
157
|
+
if key in spec:
|
|
158
|
+
entry[key] = spec[key]
|
|
159
|
+
stages[stage] = entry
|
|
160
|
+
return {
|
|
161
|
+
key: recipe.get(key)
|
|
162
|
+
for key in ("enabled", "gpu_enabled", "bucket", "max_pages", "max_plans")
|
|
163
|
+
if key in recipe
|
|
164
|
+
} | {"stages": stages}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _task_class_summary(classes: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
168
|
+
keys = (
|
|
169
|
+
"name",
|
|
170
|
+
"machine_type",
|
|
171
|
+
"provisioning_model",
|
|
172
|
+
"max_active_tasks",
|
|
173
|
+
"max_tasks_per_capture",
|
|
174
|
+
"runtime_seconds",
|
|
175
|
+
"image",
|
|
176
|
+
)
|
|
177
|
+
return [{key: item.get(key) for key in keys if key in item} for item in classes]
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _asset_summary(assets: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
181
|
+
return [{"role": a.get("role"), "uri": a.get("uri"), "sha256": a.get("sha256")} for a in assets]
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
ENV_NOTE = "environment value lives in the processing API deployment; not readable locally"
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@app.command("show")
|
|
188
|
+
def show(
|
|
189
|
+
uri: str | None = URI_OPTION,
|
|
190
|
+
deployment: Path | None = DEPLOYMENT_OPTION,
|
|
191
|
+
full: bool = typer.Option(False, "--full", help="Print full values instead of summaries."),
|
|
192
|
+
) -> None:
|
|
193
|
+
"""Show the effective configuration, the source of each field, and the store generation."""
|
|
194
|
+
rc = _runtime_config()
|
|
195
|
+
store = _store(uri)
|
|
196
|
+
document, generation, problem = _read_base(store)
|
|
197
|
+
env, deployment_dir = _deployment_env(_deployment_dir(deployment))
|
|
198
|
+
loaded = rc.LoadedDocument(document, generation) if document is not None else None
|
|
199
|
+
effective = rc.merge_effective(env, loaded)
|
|
200
|
+
fields: dict[str, Any] = {}
|
|
201
|
+
|
|
202
|
+
def value_or_none(name: str, raw: str | None, summarize):
|
|
203
|
+
if raw is None:
|
|
204
|
+
return None
|
|
205
|
+
parsed = json.loads(raw)
|
|
206
|
+
return parsed if full else summarize(parsed)
|
|
207
|
+
|
|
208
|
+
for name, raw, summarize, filename in (
|
|
209
|
+
("recipe", effective.recipe_json, _recipe_summary, "egoexo-recipe.json"),
|
|
210
|
+
(
|
|
211
|
+
"trusted_assets",
|
|
212
|
+
effective.trusted_assets_json,
|
|
213
|
+
_asset_summary,
|
|
214
|
+
"egoexo-trusted-assets.json",
|
|
215
|
+
),
|
|
216
|
+
):
|
|
217
|
+
source = effective.source(name)
|
|
218
|
+
entry: dict[str, Any] = {"source": source}
|
|
219
|
+
if source == "store" or deployment_dir is not None:
|
|
220
|
+
entry["value"] = value_or_none(name, raw, summarize)
|
|
221
|
+
if source == "env":
|
|
222
|
+
entry["note"] = (
|
|
223
|
+
f"environment value read from {deployment_dir / filename}"
|
|
224
|
+
if deployment_dir is not None
|
|
225
|
+
else ENV_NOTE
|
|
226
|
+
)
|
|
227
|
+
fields[name] = entry
|
|
228
|
+
source = effective.source("task_classes")
|
|
229
|
+
fields["task_classes"] = {"source": source}
|
|
230
|
+
if source == "store":
|
|
231
|
+
fields["task_classes"]["value"] = value_or_none(
|
|
232
|
+
"task_classes", effective.task_classes_json, _task_class_summary
|
|
233
|
+
)
|
|
234
|
+
else:
|
|
235
|
+
fields["task_classes"]["note"] = ENV_NOTE + " (rendered by terraform task_classes.tf)"
|
|
236
|
+
fields["flags"] = {
|
|
237
|
+
name: (
|
|
238
|
+
{"source": "store", "value": getattr(effective, name)}
|
|
239
|
+
if effective.source(name) == "store"
|
|
240
|
+
else {"source": "env", "note": ENV_NOTE}
|
|
241
|
+
)
|
|
242
|
+
for name in rc.FLAG_NAMES
|
|
243
|
+
}
|
|
244
|
+
_emit(
|
|
245
|
+
{
|
|
246
|
+
"uri": store.uri,
|
|
247
|
+
"generation": None if generation == "0" else generation,
|
|
248
|
+
"revision": (
|
|
249
|
+
{
|
|
250
|
+
"author": document.author,
|
|
251
|
+
"reason": document.reason,
|
|
252
|
+
"created_at": document.created_at,
|
|
253
|
+
"previous_generation": document.previous_generation,
|
|
254
|
+
}
|
|
255
|
+
if document is not None
|
|
256
|
+
else None
|
|
257
|
+
),
|
|
258
|
+
"problem": problem,
|
|
259
|
+
"fields": fields,
|
|
260
|
+
}
|
|
261
|
+
)
|
|
262
|
+
if problem:
|
|
263
|
+
raise typer.Exit(1)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _revise(store, *, write: bool, reason: str | None, change: dict[str, Any], mutate) -> None:
|
|
267
|
+
"""Validate a new revision, print the plan, and write it only with --write --reason."""
|
|
268
|
+
rc = _runtime_config()
|
|
269
|
+
document, generation, problem = _read_base(store)
|
|
270
|
+
base = document if document is not None else rc.RuntimeConfigDocument()
|
|
271
|
+
revised = mutate(base)
|
|
272
|
+
try:
|
|
273
|
+
revised.validate()
|
|
274
|
+
except (ValueError, TypeError, KeyError) as exc:
|
|
275
|
+
error(f"Rejected by the production parser: {type(exc).__name__}: {exc}")
|
|
276
|
+
raise typer.Exit(1) from exc
|
|
277
|
+
plan = {
|
|
278
|
+
"uri": store.uri,
|
|
279
|
+
"expected_generation": None if generation == "0" else generation,
|
|
280
|
+
"current_problem": problem,
|
|
281
|
+
"change": change,
|
|
282
|
+
"overrides_after": {
|
|
283
|
+
"recipe": revised.recipe is not None,
|
|
284
|
+
"trusted_assets": revised.trusted_assets is not None,
|
|
285
|
+
"task_classes": revised.task_classes is not None,
|
|
286
|
+
"flags": dict(revised.flags),
|
|
287
|
+
},
|
|
288
|
+
}
|
|
289
|
+
if not write:
|
|
290
|
+
_emit(plan | {"written": False})
|
|
291
|
+
info('Dry run. Nothing was written. Re-run with --write --reason "<text>".')
|
|
292
|
+
return
|
|
293
|
+
if not reason or not reason.strip():
|
|
294
|
+
error("--reason is required with --write.")
|
|
295
|
+
raise typer.Exit(1)
|
|
296
|
+
author = _author()
|
|
297
|
+
try:
|
|
298
|
+
new_generation = store.write(
|
|
299
|
+
revised, author=author, reason=reason, expected_generation=generation
|
|
300
|
+
)
|
|
301
|
+
except rc.GenerationMismatch as exc:
|
|
302
|
+
error("The object changed since it was read. Re-run the command to read it again.")
|
|
303
|
+
raise typer.Exit(1) from exc
|
|
304
|
+
_emit(
|
|
305
|
+
plan
|
|
306
|
+
| {
|
|
307
|
+
"written": True,
|
|
308
|
+
"generation": new_generation,
|
|
309
|
+
"history_uri": store.history_uri(new_generation),
|
|
310
|
+
"author": author,
|
|
311
|
+
"reason": reason,
|
|
312
|
+
}
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
@app.command("set-recipe")
|
|
317
|
+
def set_recipe(
|
|
318
|
+
file: Path = typer.Argument(..., help="Recipe JSON, the same document the API loads."),
|
|
319
|
+
uri: str | None = URI_OPTION,
|
|
320
|
+
write: bool = WRITE_OPTION,
|
|
321
|
+
reason: str | None = REASON_OPTION,
|
|
322
|
+
) -> None:
|
|
323
|
+
"""Store a recipe that overrides EGOEXO_CAPTURE_RECIPE_JSON."""
|
|
324
|
+
recipe = _load_json_file(file)
|
|
325
|
+
_revise(
|
|
326
|
+
_store(uri),
|
|
327
|
+
write=write,
|
|
328
|
+
reason=reason,
|
|
329
|
+
change={"field": "recipe", "file": str(file)},
|
|
330
|
+
mutate=lambda base: replace(base, recipe=recipe),
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
@app.command("set-task-classes")
|
|
335
|
+
def set_task_classes(
|
|
336
|
+
file: Path = typer.Argument(..., help="Task class list JSON, one to three classes."),
|
|
337
|
+
uri: str | None = URI_OPTION,
|
|
338
|
+
write: bool = WRITE_OPTION,
|
|
339
|
+
reason: str | None = REASON_OPTION,
|
|
340
|
+
) -> None:
|
|
341
|
+
"""Store task classes that override BUILDAI_EGOEXO_ASSIGNED_TASK_CLASSES_JSON."""
|
|
342
|
+
classes = _load_json_file(file)
|
|
343
|
+
_revise(
|
|
344
|
+
_store(uri),
|
|
345
|
+
write=write,
|
|
346
|
+
reason=reason,
|
|
347
|
+
change={"field": "task_classes", "file": str(file)},
|
|
348
|
+
mutate=lambda base: replace(base, task_classes=classes),
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
@app.command("set-trusted-assets")
|
|
353
|
+
def set_trusted_assets(
|
|
354
|
+
file: Path = typer.Argument(..., help="Trusted asset list JSON with pinned identities."),
|
|
355
|
+
uri: str | None = URI_OPTION,
|
|
356
|
+
write: bool = WRITE_OPTION,
|
|
357
|
+
reason: str | None = REASON_OPTION,
|
|
358
|
+
) -> None:
|
|
359
|
+
"""Store trusted assets that override BUILDAI_EGOEXO_CAPTURE_TRUSTED_ASSETS_JSON."""
|
|
360
|
+
assets = _load_json_file(file)
|
|
361
|
+
_revise(
|
|
362
|
+
_store(uri),
|
|
363
|
+
write=write,
|
|
364
|
+
reason=reason,
|
|
365
|
+
change={"field": "trusted_assets", "file": str(file)},
|
|
366
|
+
mutate=lambda base: replace(base, trusted_assets=assets),
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
@app.command("set-flag")
|
|
371
|
+
def set_flag(
|
|
372
|
+
name: str = typer.Argument(
|
|
373
|
+
...,
|
|
374
|
+
help="assigned_dispatch_enabled, auto_advance_enabled or native_discovery_enabled.",
|
|
375
|
+
),
|
|
376
|
+
value: str = typer.Argument(..., help="true or false."),
|
|
377
|
+
uri: str | None = URI_OPTION,
|
|
378
|
+
write: bool = WRITE_OPTION,
|
|
379
|
+
reason: str | None = REASON_OPTION,
|
|
380
|
+
) -> None:
|
|
381
|
+
"""Store one lane flag that overrides its environment variable."""
|
|
382
|
+
rc = _runtime_config()
|
|
383
|
+
if name not in rc.FLAG_NAMES:
|
|
384
|
+
error(f"Unknown flag {name!r}. Choose one of: {', '.join(rc.FLAG_NAMES)}")
|
|
385
|
+
raise typer.Exit(1)
|
|
386
|
+
if value.lower() not in {"true", "false"}:
|
|
387
|
+
error("Flag value must be true or false.")
|
|
388
|
+
raise typer.Exit(1)
|
|
389
|
+
enabled = value.lower() == "true"
|
|
390
|
+
_revise(
|
|
391
|
+
_store(uri),
|
|
392
|
+
write=write,
|
|
393
|
+
reason=reason,
|
|
394
|
+
change={"field": f"flags.{name}", "value": enabled},
|
|
395
|
+
mutate=lambda base: replace(base, flags=dict(base.flags) | {name: enabled}),
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
@app.command("clear")
|
|
400
|
+
def clear(
|
|
401
|
+
field: str = typer.Argument(
|
|
402
|
+
..., help="recipe, trusted_assets, task_classes, or a flag name to hand back to env."
|
|
403
|
+
),
|
|
404
|
+
uri: str | None = URI_OPTION,
|
|
405
|
+
write: bool = WRITE_OPTION,
|
|
406
|
+
reason: str | None = REASON_OPTION,
|
|
407
|
+
) -> None:
|
|
408
|
+
"""Remove one stored override so the environment value applies again."""
|
|
409
|
+
rc = _runtime_config()
|
|
410
|
+
if field in rc.VALUE_FIELDS:
|
|
411
|
+
mutate = lambda base: replace(base, **{field: None}) # noqa: E731
|
|
412
|
+
elif field in rc.FLAG_NAMES:
|
|
413
|
+
mutate = lambda base: replace( # noqa: E731
|
|
414
|
+
base, flags={k: v for k, v in base.flags.items() if k != field}
|
|
415
|
+
)
|
|
416
|
+
else:
|
|
417
|
+
error(f"Unknown field {field!r}. Choose one of: {', '.join(rc.FIELD_NAMES)}")
|
|
418
|
+
raise typer.Exit(1)
|
|
419
|
+
_revise(
|
|
420
|
+
_store(uri),
|
|
421
|
+
write=write,
|
|
422
|
+
reason=reason,
|
|
423
|
+
change={"field": field, "value": "environment"},
|
|
424
|
+
mutate=mutate,
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _asset_group(uri: str) -> str | None:
|
|
429
|
+
"""Return the <group> segment of derived/runtime-assets/<group>/<sha256>/<name>."""
|
|
430
|
+
marker = uri.find(ASSET_PREFIX)
|
|
431
|
+
if marker < 0:
|
|
432
|
+
return None
|
|
433
|
+
parts = uri[marker + len(ASSET_PREFIX) :].split("/")
|
|
434
|
+
return parts[0] if len(parts) == 3 and parts[0] else None
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
@app.command("pin-config-asset")
|
|
438
|
+
def pin_config_asset(
|
|
439
|
+
stage: str = typer.Option(..., "--stage", help="Recipe stage whose config changes."),
|
|
440
|
+
file: Path = typer.Option(..., "--file", help="Local JSON config to upload."),
|
|
441
|
+
group: str | None = typer.Option(
|
|
442
|
+
None, "--group", help="Asset group. Default: the group of the stage's current config."
|
|
443
|
+
),
|
|
444
|
+
name: str | None = typer.Option(
|
|
445
|
+
None, "--name", help="Object name under the SHA256 folder. Default: the file name."
|
|
446
|
+
),
|
|
447
|
+
uri: str | None = URI_OPTION,
|
|
448
|
+
deployment: Path | None = DEPLOYMENT_OPTION,
|
|
449
|
+
write: bool = WRITE_OPTION,
|
|
450
|
+
reason: str | None = REASON_OPTION,
|
|
451
|
+
) -> None:
|
|
452
|
+
"""Upload a content-addressed stage config and pin it in the recipe and trusted assets.
|
|
453
|
+
|
|
454
|
+
The object lands at derived/runtime-assets/<group>/<sha256>/<name>. After the
|
|
455
|
+
upload the command reads it back and checks generation, size, CRC32C and
|
|
456
|
+
SHA256. The recipe's stage config, the method config_sha256, and the trusted
|
|
457
|
+
asset list change together in one new store revision.
|
|
458
|
+
"""
|
|
459
|
+
_runtime_config()
|
|
460
|
+
from egoexo.adapters.gcs import publication
|
|
461
|
+
|
|
462
|
+
_load_json_file(file)
|
|
463
|
+
store = _store(uri)
|
|
464
|
+
document, generation, problem = _read_base(store)
|
|
465
|
+
env, deployment_dir = _deployment_env(_deployment_dir(deployment))
|
|
466
|
+
if document is not None and document.recipe is not None:
|
|
467
|
+
recipe = json.loads(json.dumps(document.recipe))
|
|
468
|
+
recipe_source = "store"
|
|
469
|
+
elif env.recipe_json:
|
|
470
|
+
recipe = json.loads(env.recipe_json)
|
|
471
|
+
recipe_source = f"env ({deployment_dir / 'egoexo-recipe.json'})"
|
|
472
|
+
else:
|
|
473
|
+
error("No recipe in the store and no --deployment recipe to start from.")
|
|
474
|
+
raise typer.Exit(1)
|
|
475
|
+
if document is not None and document.trusted_assets is not None:
|
|
476
|
+
assets = json.loads(json.dumps(document.trusted_assets))
|
|
477
|
+
else:
|
|
478
|
+
assets = json.loads(env.trusted_assets_json)
|
|
479
|
+
spec = recipe.get("preset", {}).get(stage)
|
|
480
|
+
if not isinstance(spec, dict) or not isinstance(spec.get("method"), dict):
|
|
481
|
+
error(f"Stage {stage!r} is not in the recipe preset.")
|
|
482
|
+
raise typer.Exit(1)
|
|
483
|
+
old = spec.get("config")
|
|
484
|
+
group = group or (_asset_group(old["uri"]) if isinstance(old, dict) else None)
|
|
485
|
+
if not group or "/" in group:
|
|
486
|
+
error("Pass --group; the stage has no current config to take the group from.")
|
|
487
|
+
raise typer.Exit(1)
|
|
488
|
+
name = name or file.name
|
|
489
|
+
if "/" in name or name in {"", ".", ".."}:
|
|
490
|
+
error("--name must be a plain file name.")
|
|
491
|
+
raise typer.Exit(1)
|
|
492
|
+
size, sha, crc = publication.identity(file)
|
|
493
|
+
target = f"gs://{recipe['bucket']}/{ASSET_PREFIX}{group}/{sha}/{name}"
|
|
494
|
+
change = {
|
|
495
|
+
"stage": stage,
|
|
496
|
+
"recipe_source": recipe_source,
|
|
497
|
+
"file": str(file),
|
|
498
|
+
"target_uri": target,
|
|
499
|
+
"sha256": sha,
|
|
500
|
+
"size_bytes": size,
|
|
501
|
+
"crc32c_base64": crc,
|
|
502
|
+
"replaces_config_uri": old["uri"] if isinstance(old, dict) else None,
|
|
503
|
+
}
|
|
504
|
+
if not write:
|
|
505
|
+
_emit(
|
|
506
|
+
{
|
|
507
|
+
"uri": store.uri,
|
|
508
|
+
"expected_generation": None if generation == "0" else generation,
|
|
509
|
+
"current_problem": problem,
|
|
510
|
+
"change": change,
|
|
511
|
+
"written": False,
|
|
512
|
+
}
|
|
513
|
+
)
|
|
514
|
+
info('Dry run. Nothing was uploaded or written. Re-run with --write --reason "<text>".')
|
|
515
|
+
return
|
|
516
|
+
if not reason or not reason.strip():
|
|
517
|
+
error("--reason is required with --write.")
|
|
518
|
+
raise typer.Exit(1)
|
|
519
|
+
with tempfile.TemporaryDirectory() as scratch:
|
|
520
|
+
published = publication.publish(store.storage, target, file, Path(scratch))
|
|
521
|
+
check = Path(scratch) / "readback.json"
|
|
522
|
+
store.storage.download(target, published.generation, check, size)
|
|
523
|
+
if publication.identity(check) != (size, sha, crc):
|
|
524
|
+
error("Readback of the uploaded object did not match the local file.")
|
|
525
|
+
raise typer.Exit(1)
|
|
526
|
+
ref = {
|
|
527
|
+
"role": "producer_config",
|
|
528
|
+
"uri": target,
|
|
529
|
+
"generation": published.generation,
|
|
530
|
+
"size_bytes": size,
|
|
531
|
+
"sha256": sha,
|
|
532
|
+
"sha256_verification": "declared",
|
|
533
|
+
"crc32c_base64": crc,
|
|
534
|
+
}
|
|
535
|
+
spec["config"] = ref
|
|
536
|
+
spec["method"] = dict(spec["method"]) | {"config_sha256": sha}
|
|
537
|
+
replaced = False
|
|
538
|
+
for index, asset in enumerate(assets):
|
|
539
|
+
if (
|
|
540
|
+
isinstance(old, dict)
|
|
541
|
+
and asset.get("uri") == old["uri"]
|
|
542
|
+
and asset.get("role") == ref["role"]
|
|
543
|
+
):
|
|
544
|
+
assets[index] = ref
|
|
545
|
+
replaced = True
|
|
546
|
+
if not replaced and not any(
|
|
547
|
+
a.get("uri") == target and a.get("role") == ref["role"] for a in assets
|
|
548
|
+
):
|
|
549
|
+
assets.append(ref)
|
|
550
|
+
change |= {"uploaded_generation": published.generation, "trusted_asset_replaced": replaced}
|
|
551
|
+
_revise(
|
|
552
|
+
store,
|
|
553
|
+
write=True,
|
|
554
|
+
reason=reason,
|
|
555
|
+
change=change,
|
|
556
|
+
mutate=lambda base: replace(base, recipe=recipe, trusted_assets=assets),
|
|
557
|
+
)
|
|
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
|
|
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
|
{buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/rescaling_migration/sql/legacy_export_closeout.sql
RENAMED
|
File without changes
|
{buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/rescaling_migration/sql/legacy_export_prepare.sql
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/rescaling_migration/sql/v2_export_closeout.sql
RENAMED
|
File without changes
|
{buildai_cli-0.3.150 → buildai_cli-0.3.151}/cli/rescaling_migration/sql/v2_export_prepare.sql
RENAMED
|
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
|