buildai-cli 0.3.43__tar.gz → 0.3.45__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.43 → buildai_cli-0.3.45}/PKG-INFO +1 -1
- {buildai_cli-0.3.43 → buildai_cli-0.3.45}/cli/auth_local.py +12 -14
- {buildai_cli-0.3.43 → buildai_cli-0.3.45}/cli/commands/auth.py +29 -131
- {buildai_cli-0.3.43 → buildai_cli-0.3.45}/cli/main.py +1 -1
- {buildai_cli-0.3.43 → buildai_cli-0.3.45}/cli/ops_init.py +22 -3
- {buildai_cli-0.3.43 → buildai_cli-0.3.45}/pyproject.toml +1 -1
- {buildai_cli-0.3.43 → buildai_cli-0.3.45}/.gitignore +0 -0
- {buildai_cli-0.3.43 → buildai_cli-0.3.45}/AGENTS.md +0 -0
- {buildai_cli-0.3.43 → buildai_cli-0.3.45}/CLAUDE.md +0 -0
- {buildai_cli-0.3.43 → buildai_cli-0.3.45}/buildai_bootstrap.py +0 -0
- {buildai_cli-0.3.43 → buildai_cli-0.3.45}/cli/__init__.py +0 -0
- {buildai_cli-0.3.43 → buildai_cli-0.3.45}/cli/_has_core.py +0 -0
- {buildai_cli-0.3.43 → buildai_cli-0.3.45}/cli/commands/__init__.py +0 -0
- {buildai_cli-0.3.43 → buildai_cli-0.3.45}/cli/commands/api_proxy.py +0 -0
- {buildai_cli-0.3.43 → buildai_cli-0.3.45}/cli/commands/db/__init__.py +0 -0
- {buildai_cli-0.3.43 → buildai_cli-0.3.45}/cli/commands/db/common.py +0 -0
- {buildai_cli-0.3.43 → buildai_cli-0.3.45}/cli/commands/db/migrate.py +0 -0
- {buildai_cli-0.3.43 → buildai_cli-0.3.45}/cli/commands/db/query.py +0 -0
- {buildai_cli-0.3.43 → buildai_cli-0.3.45}/cli/commands/db/schema.py +0 -0
- {buildai_cli-0.3.43 → buildai_cli-0.3.45}/cli/commands/db/status.py +0 -0
- {buildai_cli-0.3.43 → buildai_cli-0.3.45}/cli/commands/dev.py +0 -0
- {buildai_cli-0.3.43 → buildai_cli-0.3.45}/cli/commands/doctor.py +0 -0
- {buildai_cli-0.3.43 → buildai_cli-0.3.45}/cli/config.py +0 -0
- {buildai_cli-0.3.43 → buildai_cli-0.3.45}/cli/console.py +0 -0
- {buildai_cli-0.3.43 → buildai_cli-0.3.45}/cli/context.py +0 -0
- {buildai_cli-0.3.43 → buildai_cli-0.3.45}/cli/guard.py +0 -0
- {buildai_cli-0.3.43 → buildai_cli-0.3.45}/cli/internal_api.py +0 -0
- {buildai_cli-0.3.43 → buildai_cli-0.3.45}/cli/nl_query/__init__.py +0 -0
- {buildai_cli-0.3.43 → buildai_cli-0.3.45}/cli/nl_query/dataset_tools.py +0 -0
- {buildai_cli-0.3.43 → buildai_cli-0.3.45}/cli/output.py +0 -0
- {buildai_cli-0.3.43 → buildai_cli-0.3.45}/cli/pagination.py +0 -0
|
@@ -95,18 +95,12 @@ class ResolvedLocalAuthProfile:
|
|
|
95
95
|
f'export ALLOYDB_IAM_AUTH_{suffix}="true"',
|
|
96
96
|
]
|
|
97
97
|
)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
]
|
|
105
|
-
)
|
|
106
|
-
else:
|
|
107
|
-
exports.append(
|
|
108
|
-
f'export ALLOYDB_RUNTIME_IMPERSONATE_SA_{suffix}="{self.target_service_account}"'
|
|
109
|
-
)
|
|
98
|
+
exports.extend(
|
|
99
|
+
[
|
|
100
|
+
f'export ALLOYDB_RUNTIME_IMPERSONATE_SA_{suffix}="{self.target_service_account}"',
|
|
101
|
+
f'export ALLOYDB_USE_AUTH_PROXY_{suffix}="false"',
|
|
102
|
+
]
|
|
103
|
+
)
|
|
110
104
|
elif self.name == "db-admin-local" and self.target_db_user and self.target_service_account:
|
|
111
105
|
exports.extend(
|
|
112
106
|
[
|
|
@@ -200,8 +194,12 @@ def sanctioned_auth_profiles() -> tuple[LocalAuthProfile, ...]:
|
|
|
200
194
|
LocalAuthProfile(
|
|
201
195
|
name="engineers-dev",
|
|
202
196
|
audience="engineers",
|
|
203
|
-
summary="Use the
|
|
204
|
-
purpose=
|
|
197
|
+
summary="Use the engineer-facing read-only DB lane.",
|
|
198
|
+
purpose=(
|
|
199
|
+
"Use this for direct CLI database reads and schema inspection through the "
|
|
200
|
+
"engineer-facing service-account identity. Local API runtime parity should "
|
|
201
|
+
"use the service-runtime profiles instead."
|
|
202
|
+
),
|
|
205
203
|
),
|
|
206
204
|
LocalAuthProfile(
|
|
207
205
|
name="api-runtime-like",
|
|
@@ -3,11 +3,8 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import sys
|
|
6
|
-
import time
|
|
7
|
-
import webbrowser
|
|
8
6
|
from typing import Any
|
|
9
7
|
|
|
10
|
-
import httpx
|
|
11
8
|
import typer
|
|
12
9
|
from rich.table import Table
|
|
13
10
|
|
|
@@ -21,7 +18,7 @@ from cli.auth_local import (
|
|
|
21
18
|
sanctioned_auth_profiles,
|
|
22
19
|
)
|
|
23
20
|
from cli.config import clear_credential, resolve_api_url, save_cli_config
|
|
24
|
-
from cli.console import
|
|
21
|
+
from cli.console import data_console, error, info, status_console, success
|
|
25
22
|
from cli.internal_api import get_internal_api_client
|
|
26
23
|
from cli.output import Format, auto_format, format_option, render
|
|
27
24
|
|
|
@@ -37,57 +34,6 @@ def _api_base_url() -> str:
|
|
|
37
34
|
return resolve_api_url()
|
|
38
35
|
|
|
39
36
|
|
|
40
|
-
def _raise_for_status(resp: httpx.Response) -> None:
|
|
41
|
-
"""Promote API login errors into user-facing Typer validation errors."""
|
|
42
|
-
|
|
43
|
-
try:
|
|
44
|
-
payload = resp.json()
|
|
45
|
-
except Exception:
|
|
46
|
-
payload = None
|
|
47
|
-
|
|
48
|
-
message = None
|
|
49
|
-
if isinstance(payload, dict):
|
|
50
|
-
if isinstance(payload.get("detail"), str):
|
|
51
|
-
message = payload["detail"]
|
|
52
|
-
elif isinstance(payload.get("error"), dict):
|
|
53
|
-
message = payload["error"].get("message")
|
|
54
|
-
if not message:
|
|
55
|
-
message = resp.text or f"HTTP {resp.status_code}"
|
|
56
|
-
raise typer.BadParameter(message)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def _start_device_login(client: httpx.Client, client_name: str) -> dict[str, Any]:
|
|
60
|
-
"""Kick off the device-flow login used by the public API credential plane."""
|
|
61
|
-
|
|
62
|
-
resp = client.post(
|
|
63
|
-
f"{_api_base_url()}/v1/auth/cli/start",
|
|
64
|
-
json={"client_name": client_name},
|
|
65
|
-
)
|
|
66
|
-
if resp.status_code >= 400:
|
|
67
|
-
_raise_for_status(resp)
|
|
68
|
-
return resp.json()
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def _poll_device_login(client: httpx.Client, device_code: str) -> dict[str, Any]:
|
|
72
|
-
"""Poll the public API until the device-flow login is approved or expires."""
|
|
73
|
-
|
|
74
|
-
resp = client.get(
|
|
75
|
-
f"{_api_base_url()}/v1/auth/cli/poll",
|
|
76
|
-
params={"device_code": device_code},
|
|
77
|
-
)
|
|
78
|
-
if resp.status_code == 429:
|
|
79
|
-
retry_after = resp.headers.get("Retry-After")
|
|
80
|
-
if retry_after:
|
|
81
|
-
try:
|
|
82
|
-
time.sleep(max(float(retry_after), 0.0))
|
|
83
|
-
except ValueError:
|
|
84
|
-
pass
|
|
85
|
-
return {"status": "pending"}
|
|
86
|
-
if resp.status_code >= 400:
|
|
87
|
-
_raise_for_status(resp)
|
|
88
|
-
return resp.json()
|
|
89
|
-
|
|
90
|
-
|
|
91
37
|
def _store_token(token: str, *, profile: str | None = None) -> None:
|
|
92
38
|
"""Persist a public API token in the normal CLI credentials location."""
|
|
93
39
|
|
|
@@ -95,69 +41,16 @@ def _store_token(token: str, *, profile: str | None = None) -> None:
|
|
|
95
41
|
success("Saved CLI credentials to ~/.buildai/credentials.json")
|
|
96
42
|
|
|
97
43
|
|
|
98
|
-
def
|
|
99
|
-
|
|
100
|
-
client_name: str = "buildai-cli",
|
|
101
|
-
open_browser: bool = True,
|
|
102
|
-
profile: str | None = None,
|
|
103
|
-
) -> None:
|
|
104
|
-
"""Run the browser-capable device flow used for Build AI API auth."""
|
|
105
|
-
|
|
106
|
-
with httpx.Client(timeout=15.0) as client:
|
|
107
|
-
try:
|
|
108
|
-
start = _start_device_login(client, client_name)
|
|
109
|
-
except typer.BadParameter as exc:
|
|
110
|
-
error(str(exc))
|
|
111
|
-
raise typer.Exit(1) from exc
|
|
112
|
-
|
|
113
|
-
verification_url = start["verification_url"]
|
|
114
|
-
user_code = start["user_code"]
|
|
115
|
-
expires_in = int(start["expires_in"])
|
|
116
|
-
poll_interval = max(int(start["poll_interval"]), 1)
|
|
117
|
-
|
|
118
|
-
console.print(f" code : [cyan]{user_code}[/cyan]")
|
|
119
|
-
console.print(f" verify : [cyan]{verification_url}[/cyan]")
|
|
120
|
-
info("Approve this login in the browser, then the CLI will finish automatically.")
|
|
121
|
-
|
|
122
|
-
if open_browser:
|
|
123
|
-
try:
|
|
124
|
-
if webbrowser.open(verification_url):
|
|
125
|
-
info("Opened verification URL in your browser.")
|
|
126
|
-
else:
|
|
127
|
-
warning("Could not open your browser automatically.")
|
|
128
|
-
except Exception:
|
|
129
|
-
warning("Could not open your browser automatically.")
|
|
130
|
-
|
|
131
|
-
deadline = time.monotonic() + expires_in + poll_interval
|
|
132
|
-
while time.monotonic() < deadline:
|
|
133
|
-
try:
|
|
134
|
-
poll = _poll_device_login(client, start["device_code"])
|
|
135
|
-
except typer.BadParameter as exc:
|
|
136
|
-
error(str(exc))
|
|
137
|
-
raise typer.Exit(1) from exc
|
|
138
|
-
|
|
139
|
-
status = poll.get("status")
|
|
140
|
-
if status == "approved":
|
|
141
|
-
token = poll.get("token")
|
|
142
|
-
if not token:
|
|
143
|
-
error("CLI login completed without a token.")
|
|
144
|
-
raise typer.Exit(1)
|
|
145
|
-
_store_token(token, profile=profile)
|
|
146
|
-
return
|
|
147
|
-
if status == "pending":
|
|
148
|
-
time.sleep(poll_interval)
|
|
149
|
-
continue
|
|
150
|
-
if status == "expired":
|
|
151
|
-
error("CLI login expired before approval.")
|
|
152
|
-
raise typer.Exit(1)
|
|
153
|
-
if status == "consumed":
|
|
154
|
-
error("CLI login token was already consumed.")
|
|
155
|
-
raise typer.Exit(1)
|
|
156
|
-
error(f"Unexpected CLI login status: {status!r}")
|
|
157
|
-
raise typer.Exit(1)
|
|
44
|
+
def _prompt_for_token() -> str:
|
|
45
|
+
"""Prompt for an existing API key instead of pretending a browser flow exists."""
|
|
158
46
|
|
|
159
|
-
|
|
160
|
-
|
|
47
|
+
info("Build AI CLI login stores an existing API key.")
|
|
48
|
+
status_console.print("[dim]Get one from data.build.ai -> Developer -> Create API key.[/dim]")
|
|
49
|
+
value = typer.prompt("Paste Build AI API key", hide_input=True).strip()
|
|
50
|
+
if not value:
|
|
51
|
+
error("Expected a non-empty API key.")
|
|
52
|
+
raise typer.Exit(1)
|
|
53
|
+
return value
|
|
161
54
|
|
|
162
55
|
|
|
163
56
|
def _profile_rows() -> list[dict[str, Any]]:
|
|
@@ -290,33 +183,38 @@ def _emit_report(
|
|
|
290
183
|
|
|
291
184
|
@app.command("login")
|
|
292
185
|
def login(
|
|
186
|
+
api_key: str | None = typer.Option(
|
|
187
|
+
None,
|
|
188
|
+
"--api-key",
|
|
189
|
+
help="Store this API key directly instead of prompting.",
|
|
190
|
+
hidden=True,
|
|
191
|
+
),
|
|
293
192
|
token: bool = typer.Option(
|
|
294
193
|
False,
|
|
295
194
|
"--token",
|
|
296
|
-
help="Read
|
|
297
|
-
),
|
|
298
|
-
client_name: str = typer.Option(
|
|
299
|
-
"buildai-cli",
|
|
300
|
-
"--client-name",
|
|
301
|
-
help="Client name sent to the API for device-login requests.",
|
|
302
|
-
),
|
|
303
|
-
open_browser: bool = typer.Option(
|
|
304
|
-
True,
|
|
305
|
-
"--open-browser/--no-open-browser",
|
|
306
|
-
help="Open the verification URL in your browser during device login.",
|
|
195
|
+
help="Read an API key from stdin instead of prompting.",
|
|
307
196
|
),
|
|
308
197
|
profile: str | None = typer.Option(None, "--profile", hidden=True),
|
|
309
198
|
) -> None:
|
|
310
|
-
"""
|
|
199
|
+
"""Store an existing Build AI API key for API-backed CLI commands."""
|
|
200
|
+
|
|
201
|
+
if api_key and token:
|
|
202
|
+
error("Choose either --api-key or --token, not both.")
|
|
203
|
+
raise typer.Exit(1)
|
|
204
|
+
|
|
205
|
+
if api_key:
|
|
206
|
+
_store_token(api_key.strip(), profile=profile)
|
|
207
|
+
return
|
|
311
208
|
|
|
312
209
|
if token:
|
|
313
210
|
value = sys.stdin.read().strip()
|
|
314
211
|
if not value:
|
|
315
|
-
error("Expected
|
|
212
|
+
error("Expected an API key on stdin.")
|
|
316
213
|
raise typer.Exit(1)
|
|
317
214
|
_store_token(value, profile=profile)
|
|
318
215
|
return
|
|
319
|
-
|
|
216
|
+
|
|
217
|
+
_store_token(_prompt_for_token(), profile=profile)
|
|
320
218
|
|
|
321
219
|
|
|
322
220
|
@app.command("whoami")
|
|
@@ -224,7 +224,7 @@ Build AI CLI — auth, dev, and database utilities.
|
|
|
224
224
|
|
|
225
225
|
Get started:
|
|
226
226
|
uv tool install buildai-cli Install the standalone CLI
|
|
227
|
-
buildai auth login
|
|
227
|
+
buildai auth login Paste or pipe an API key
|
|
228
228
|
buildai auth whoami Inspect API and local auth state
|
|
229
229
|
buildai dev db info Print the local DB recipe for CLI and desktop tools
|
|
230
230
|
buildai doctor auth Diagnose local auth before DB work
|
|
@@ -34,15 +34,32 @@ def _parse_env(value: str) -> str:
|
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
def _auth_env_prefix(settings_app_env: object) -> str:
|
|
37
|
-
|
|
37
|
+
normalized = str(getattr(settings_app_env, "value", settings_app_env)).strip().lower()
|
|
38
|
+
if normalized == "production":
|
|
38
39
|
return "PROD"
|
|
39
|
-
if
|
|
40
|
+
if normalized == "development":
|
|
40
41
|
return "DEV"
|
|
41
|
-
if
|
|
42
|
+
if normalized == "test":
|
|
42
43
|
return "DEV"
|
|
43
44
|
return "PREVIEW"
|
|
44
45
|
|
|
45
46
|
|
|
47
|
+
def _apply_default_db_target(*, env_prefix: str) -> None:
|
|
48
|
+
"""Seed the canonical production DB target when the shell provided none.
|
|
49
|
+
|
|
50
|
+
The engineer/operator CLI should not require folklore env setup before it
|
|
51
|
+
can even attempt the sanctioned impersonation lane. Defaulting the DB
|
|
52
|
+
target here keeps `buildai db ...` aligned with the current single-database
|
|
53
|
+
reality while still letting explicit env overrides win.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
os.environ.setdefault("ALLOYDB_CLUSTER", "buildai-india")
|
|
57
|
+
os.environ.setdefault("ALLOYDB_INSTANCE", "buildai-india-primary")
|
|
58
|
+
os.environ.setdefault("ALLOYDB_REGION", "asia-south1")
|
|
59
|
+
os.environ.setdefault("DB_NAME", "buildai_production")
|
|
60
|
+
os.environ.setdefault(f"ALLOYDB_IAM_AUTH_{env_prefix}", "true")
|
|
61
|
+
|
|
62
|
+
|
|
46
63
|
def init_ops_context(ctx: typer.Context):
|
|
47
64
|
"""Heavy DB/auth/observability init — called lazily by ops-plane commands.
|
|
48
65
|
|
|
@@ -95,6 +112,7 @@ def init_ops_context(ctx: typer.Context):
|
|
|
95
112
|
# --- resolve auth -------------------------------------------------------
|
|
96
113
|
auth_info: list[str] = []
|
|
97
114
|
env_prefix = _auth_env_prefix(settings.app_env)
|
|
115
|
+
_apply_default_db_target(env_prefix=env_prefix)
|
|
98
116
|
|
|
99
117
|
default_user = settings.effective_db_user
|
|
100
118
|
default_use_iam = settings.effective_use_iam_auth
|
|
@@ -143,6 +161,7 @@ def init_ops_context(ctx: typer.Context):
|
|
|
143
161
|
os.environ[f"ALLOYDB_RUNTIME_IMPERSONATE_SA_{env_prefix}"] = (
|
|
144
162
|
"engineers-dev-sa@data-470400.iam.gserviceaccount.com"
|
|
145
163
|
)
|
|
164
|
+
os.environ[f"ALLOYDB_USE_AUTH_PROXY_{env_prefix}"] = "false"
|
|
146
165
|
auth_info.append("impersonate=engineers-dev-sa")
|
|
147
166
|
|
|
148
167
|
# Reload settings with overrides
|
|
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
|