buildai-cli 0.3.48__tar.gz → 0.3.49__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.48 → buildai_cli-0.3.49}/.gitignore +1 -0
- {buildai_cli-0.3.48 → buildai_cli-0.3.49}/PKG-INFO +1 -1
- {buildai_cli-0.3.48 → buildai_cli-0.3.49}/cli/commands/db/query.py +3 -3
- {buildai_cli-0.3.48 → buildai_cli-0.3.49}/cli/commands/db/schema.py +3 -3
- {buildai_cli-0.3.48 → buildai_cli-0.3.49}/cli/commands/db/status.py +2 -2
- {buildai_cli-0.3.48 → buildai_cli-0.3.49}/cli/context.py +64 -35
- {buildai_cli-0.3.48 → buildai_cli-0.3.49}/cli/main.py +6 -1
- {buildai_cli-0.3.48 → buildai_cli-0.3.49}/pyproject.toml +1 -1
- {buildai_cli-0.3.48 → buildai_cli-0.3.49}/AGENTS.md +0 -0
- {buildai_cli-0.3.48 → buildai_cli-0.3.49}/CLAUDE.md +0 -0
- {buildai_cli-0.3.48 → buildai_cli-0.3.49}/buildai_bootstrap.py +0 -0
- {buildai_cli-0.3.48 → buildai_cli-0.3.49}/cli/__init__.py +0 -0
- {buildai_cli-0.3.48 → buildai_cli-0.3.49}/cli/_has_core.py +0 -0
- {buildai_cli-0.3.48 → buildai_cli-0.3.49}/cli/auth_local.py +0 -0
- {buildai_cli-0.3.48 → buildai_cli-0.3.49}/cli/auth_proxy.py +0 -0
- {buildai_cli-0.3.48 → buildai_cli-0.3.49}/cli/commands/__init__.py +0 -0
- {buildai_cli-0.3.48 → buildai_cli-0.3.49}/cli/commands/api_proxy.py +0 -0
- {buildai_cli-0.3.48 → buildai_cli-0.3.49}/cli/commands/auth.py +0 -0
- {buildai_cli-0.3.48 → buildai_cli-0.3.49}/cli/commands/db/__init__.py +0 -0
- {buildai_cli-0.3.48 → buildai_cli-0.3.49}/cli/commands/db/common.py +0 -0
- {buildai_cli-0.3.48 → buildai_cli-0.3.49}/cli/commands/db/migrate.py +0 -0
- {buildai_cli-0.3.48 → buildai_cli-0.3.49}/cli/commands/dev.py +0 -0
- {buildai_cli-0.3.48 → buildai_cli-0.3.49}/cli/commands/doctor.py +0 -0
- {buildai_cli-0.3.48 → buildai_cli-0.3.49}/cli/config.py +0 -0
- {buildai_cli-0.3.48 → buildai_cli-0.3.49}/cli/console.py +0 -0
- {buildai_cli-0.3.48 → buildai_cli-0.3.49}/cli/guard.py +0 -0
- {buildai_cli-0.3.48 → buildai_cli-0.3.49}/cli/internal_api.py +0 -0
- {buildai_cli-0.3.48 → buildai_cli-0.3.49}/cli/nl_query/__init__.py +0 -0
- {buildai_cli-0.3.48 → buildai_cli-0.3.49}/cli/nl_query/dataset_tools.py +0 -0
- {buildai_cli-0.3.48 → buildai_cli-0.3.49}/cli/ops_init.py +0 -0
- {buildai_cli-0.3.48 → buildai_cli-0.3.49}/cli/output.py +0 -0
- {buildai_cli-0.3.48 → buildai_cli-0.3.49}/cli/pagination.py +0 -0
|
@@ -10,7 +10,7 @@ from infra.settings import Settings
|
|
|
10
10
|
from typing_extensions import Annotated
|
|
11
11
|
|
|
12
12
|
from cli.console import console, dim, error, show_sql, warning
|
|
13
|
-
from cli.context import
|
|
13
|
+
from cli.context import get_inspection_connection
|
|
14
14
|
from cli.output import Format, format_option, output
|
|
15
15
|
|
|
16
16
|
QUERY_HINTS = {
|
|
@@ -120,7 +120,7 @@ def query(
|
|
|
120
120
|
typer.Option("--write", help="Allow write queries (requires internal_admin profile)"),
|
|
121
121
|
] = False,
|
|
122
122
|
) -> None:
|
|
123
|
-
"""Execute SQL
|
|
123
|
+
"""Execute SQL against the selected database lane in inspection mode."""
|
|
124
124
|
|
|
125
125
|
settings: Settings = ctx.obj["settings"]
|
|
126
126
|
|
|
@@ -157,7 +157,7 @@ def query(
|
|
|
157
157
|
|
|
158
158
|
show_sql(sql)
|
|
159
159
|
|
|
160
|
-
async with
|
|
160
|
+
async with get_inspection_connection(settings) as conn:
|
|
161
161
|
try:
|
|
162
162
|
if _is_select_query(sql):
|
|
163
163
|
result = await conn.fetch(sql)
|
|
@@ -13,7 +13,7 @@ from rich.table import Table
|
|
|
13
13
|
from typing_extensions import Annotated
|
|
14
14
|
|
|
15
15
|
from cli.console import console, success, warning
|
|
16
|
-
from cli.context import
|
|
16
|
+
from cli.context import get_inspection_connection
|
|
17
17
|
|
|
18
18
|
app = typer.Typer(
|
|
19
19
|
name="schema",
|
|
@@ -23,9 +23,9 @@ app = typer.Typer(
|
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
def _schema_connection(settings):
|
|
26
|
-
"""Use the
|
|
26
|
+
"""Use the direct inspection connection for schema introspection."""
|
|
27
27
|
|
|
28
|
-
return
|
|
28
|
+
return get_inspection_connection(settings)
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
@dataclass
|
|
@@ -12,7 +12,7 @@ from rich.panel import Panel
|
|
|
12
12
|
from rich.table import Table
|
|
13
13
|
|
|
14
14
|
from cli.console import console, error, success, warning
|
|
15
|
-
from cli.context import
|
|
15
|
+
from cli.context import get_inspection_connection
|
|
16
16
|
|
|
17
17
|
from .common import DatabaseStatus, get_table_row_count
|
|
18
18
|
|
|
@@ -29,7 +29,7 @@ def status(ctx: typer.Context) -> None:
|
|
|
29
29
|
)
|
|
30
30
|
|
|
31
31
|
try:
|
|
32
|
-
async with
|
|
32
|
+
async with get_inspection_connection(settings) as conn:
|
|
33
33
|
status_obj.connected = True
|
|
34
34
|
migration_audit = await audit_migration_tracking(
|
|
35
35
|
conn,
|
|
@@ -1,19 +1,22 @@
|
|
|
1
|
-
"""CLI context management for
|
|
1
|
+
"""CLI context management for direct DB inspection and scoped DAL access.
|
|
2
2
|
|
|
3
|
-
This module
|
|
4
|
-
|
|
3
|
+
This module exposes two intentionally different database access modes:
|
|
4
|
+
|
|
5
|
+
- ``get_inspection_connection()`` for direct operator/developer inspection.
|
|
6
|
+
This uses the selected DB principal plus one explicit internal inspection
|
|
7
|
+
session contract so RLS-backed tables stay visible without pretending to be
|
|
8
|
+
a tenant request.
|
|
9
|
+
- ``get_cli_context()`` for DAL-driven commands that do want the CLI-owned
|
|
10
|
+
scoped session contract.
|
|
5
11
|
|
|
6
12
|
Usage:
|
|
7
|
-
#
|
|
8
|
-
async with
|
|
13
|
+
# Direct DB inspection
|
|
14
|
+
async with get_inspection_connection(settings) as conn:
|
|
9
15
|
result = await conn.fetch("SELECT 1")
|
|
10
16
|
|
|
11
17
|
# With canonical DAL context (for commands that use dal functions)
|
|
12
18
|
async with get_cli_context(settings) as (db, ctx):
|
|
13
19
|
datasets = await dal.external.datasets.list_datasets(ctx)
|
|
14
|
-
|
|
15
|
-
# For raw SQL commands, use get_connection()
|
|
16
|
-
# For commands that benefit from dal, use get_cli_context()
|
|
17
20
|
"""
|
|
18
21
|
|
|
19
22
|
import os
|
|
@@ -35,6 +38,13 @@ logger = get_logger(__name__)
|
|
|
35
38
|
# Environment variable to force local PostgreSQL (for test/CI)
|
|
36
39
|
USE_LOCAL_DB = os.getenv("USE_LOCAL_DB", "false").lower() == "true"
|
|
37
40
|
_ZERO_UUID = "00000000-0000-0000-0000-000000000000"
|
|
41
|
+
_EMPTY_SCOPE_IDS = "{}"
|
|
42
|
+
_SCOPE_GUCS = (
|
|
43
|
+
"app.allowed_organization_ids",
|
|
44
|
+
"app.allowed_site_ids",
|
|
45
|
+
"app.allowed_source_ids",
|
|
46
|
+
"app.allowed_dataset_ids",
|
|
47
|
+
)
|
|
38
48
|
_READ_SUFFIXES = (".read", ".search", ".query", ".introspection")
|
|
39
49
|
_READ_PROFILE_SCOPES = frozenset(
|
|
40
50
|
scope
|
|
@@ -87,18 +97,44 @@ class AdminConnectionConfig:
|
|
|
87
97
|
password: str = ""
|
|
88
98
|
|
|
89
99
|
|
|
90
|
-
async def
|
|
100
|
+
async def _stamp_base_cli_session_contract(
|
|
91
101
|
conn: asyncpg.Connection,
|
|
92
102
|
*,
|
|
93
|
-
|
|
103
|
+
unrestricted: bool,
|
|
94
104
|
) -> None:
|
|
95
|
-
"""Stamp the
|
|
96
|
-
unrestricted = "true" if profile in _UNRESTRICTED_CLI_PROFILES else "false"
|
|
105
|
+
"""Stamp the common CLI session GUCs and clear all known resource scopes."""
|
|
97
106
|
await conn.execute("SELECT set_config('app.principal_id', $1, false)", _ZERO_UUID)
|
|
98
|
-
await conn.execute(
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
107
|
+
await conn.execute(
|
|
108
|
+
"SELECT set_config('app.unrestricted', $1, false)",
|
|
109
|
+
"true" if unrestricted else "false",
|
|
110
|
+
)
|
|
111
|
+
for setting_name in _SCOPE_GUCS:
|
|
112
|
+
await conn.execute(
|
|
113
|
+
f"SELECT set_config('{setting_name}', $1, false)",
|
|
114
|
+
_EMPTY_SCOPE_IDS,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
async def _stamp_internal_inspection_session_contract(conn: asyncpg.Connection) -> None:
|
|
119
|
+
"""Stamp the explicit unrestricted contract for direct CLI inspection work.
|
|
120
|
+
|
|
121
|
+
``buildai db ...`` commands are internal inspection tools, not request-parity
|
|
122
|
+
emulators. The DB principal still caps what the caller can mutate, while the
|
|
123
|
+
unrestricted flag ensures RLS-backed tables answer truthfully for reads.
|
|
124
|
+
"""
|
|
125
|
+
await _stamp_base_cli_session_contract(conn, unrestricted=True)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
async def _stamp_scoped_cli_session_contract(
|
|
129
|
+
conn: asyncpg.Connection,
|
|
130
|
+
*,
|
|
131
|
+
profile: str,
|
|
132
|
+
) -> None:
|
|
133
|
+
"""Stamp the CLI-owned scoped session contract for DAL-oriented commands."""
|
|
134
|
+
await _stamp_base_cli_session_contract(
|
|
135
|
+
conn,
|
|
136
|
+
unrestricted=profile in _UNRESTRICTED_CLI_PROFILES,
|
|
137
|
+
)
|
|
102
138
|
|
|
103
139
|
|
|
104
140
|
def resolve_admin_connection_config(
|
|
@@ -174,15 +210,16 @@ async def open_admin_database(settings: Settings) -> Database:
|
|
|
174
210
|
|
|
175
211
|
|
|
176
212
|
@asynccontextmanager
|
|
177
|
-
async def
|
|
213
|
+
async def get_inspection_connection(
|
|
178
214
|
settings: Settings | None = None,
|
|
179
215
|
) -> AsyncGenerator[asyncpg.Connection, None]:
|
|
180
216
|
"""
|
|
181
|
-
Get a
|
|
217
|
+
Get a direct DB inspection connection for CLI commands.
|
|
182
218
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
219
|
+
This path is for ``buildai db query/status/schema`` style inspection. It
|
|
220
|
+
does not emulate request-scoped visibility. Instead it stamps the single
|
|
221
|
+
explicit internal inspection contract so RLS-backed tables remain visible
|
|
222
|
+
under the chosen DB principal.
|
|
186
223
|
|
|
187
224
|
Args:
|
|
188
225
|
settings: Optional Settings instance. Uses cached settings if not provided.
|
|
@@ -191,26 +228,23 @@ async def get_connection(
|
|
|
191
228
|
asyncpg.Connection for executing queries
|
|
192
229
|
|
|
193
230
|
Usage:
|
|
194
|
-
async with
|
|
231
|
+
async with get_inspection_connection(settings) as conn:
|
|
195
232
|
result = await conn.fetch("SELECT 1")
|
|
196
233
|
"""
|
|
197
|
-
from cli.config import resolve_cli_profile
|
|
198
|
-
|
|
199
234
|
if settings is None:
|
|
200
235
|
settings = get_settings()
|
|
201
|
-
resolved_profile = resolve_cli_profile()
|
|
202
236
|
|
|
203
237
|
if USE_LOCAL_DB or settings.is_test:
|
|
204
238
|
# Test/CI: use local PostgreSQL
|
|
205
239
|
async with _local_connection(settings) as conn:
|
|
206
|
-
await
|
|
240
|
+
await _stamp_internal_inspection_session_contract(conn)
|
|
207
241
|
yield conn
|
|
208
242
|
else:
|
|
209
243
|
# Development/Production: use AlloyDB via infra.Database
|
|
210
244
|
db = Database.from_settings(settings)
|
|
211
245
|
try:
|
|
212
246
|
await db.connect()
|
|
213
|
-
await
|
|
247
|
+
await _stamp_internal_inspection_session_contract(db.conn)
|
|
214
248
|
yield db.conn
|
|
215
249
|
finally:
|
|
216
250
|
await db.close()
|
|
@@ -219,8 +253,6 @@ async def get_connection(
|
|
|
219
253
|
@asynccontextmanager
|
|
220
254
|
async def get_admin_connection(
|
|
221
255
|
settings: Settings | None = None,
|
|
222
|
-
*,
|
|
223
|
-
profile: str | None = None,
|
|
224
256
|
) -> AsyncGenerator[asyncpg.Connection, None]:
|
|
225
257
|
"""Get a connection using the canonical admin DB identity for this lane.
|
|
226
258
|
|
|
@@ -228,15 +260,12 @@ async def get_admin_connection(
|
|
|
228
260
|
deployment profile should decide the DB principal instead of the caller's
|
|
229
261
|
personal IAM mapping.
|
|
230
262
|
"""
|
|
231
|
-
from cli.config import resolve_cli_profile
|
|
232
|
-
|
|
233
263
|
if settings is None:
|
|
234
264
|
settings = get_settings()
|
|
235
|
-
resolved_profile = resolve_cli_profile(profile)
|
|
236
265
|
|
|
237
266
|
db = await open_admin_database(settings)
|
|
238
267
|
try:
|
|
239
|
-
await
|
|
268
|
+
await _stamp_internal_inspection_session_contract(db.conn)
|
|
240
269
|
yield db.conn
|
|
241
270
|
finally:
|
|
242
271
|
await db.close()
|
|
@@ -305,7 +334,7 @@ async def get_cli_context(
|
|
|
305
334
|
if USE_LOCAL_DB or settings.is_test:
|
|
306
335
|
# Test/CI: wrap local connection in context
|
|
307
336
|
async with _local_connection(settings) as conn:
|
|
308
|
-
await
|
|
337
|
+
await _stamp_scoped_cli_session_contract(conn, profile=resolved_profile)
|
|
309
338
|
ctx = Context.for_cli(conn, scopes=scopes_for_cli_profile(resolved_profile))
|
|
310
339
|
yield None, ctx
|
|
311
340
|
else:
|
|
@@ -313,7 +342,7 @@ async def get_cli_context(
|
|
|
313
342
|
db = Database.from_settings(settings)
|
|
314
343
|
try:
|
|
315
344
|
await db.connect()
|
|
316
|
-
await
|
|
345
|
+
await _stamp_scoped_cli_session_contract(db.conn, profile=resolved_profile)
|
|
317
346
|
ctx = Context.for_cli(db, scopes=scopes_for_cli_profile(resolved_profile))
|
|
318
347
|
yield db, ctx
|
|
319
348
|
finally:
|
|
@@ -281,7 +281,12 @@ def db_callback(
|
|
|
281
281
|
),
|
|
282
282
|
auth: str = typer.Option(None, "--auth", "-a", help="Auth method: iam or password."),
|
|
283
283
|
user: str = typer.Option(None, "--user", "-u", help="Override database user."),
|
|
284
|
-
profile: str | None = typer.Option(
|
|
284
|
+
profile: str | None = typer.Option(
|
|
285
|
+
None,
|
|
286
|
+
"--profile",
|
|
287
|
+
"-p",
|
|
288
|
+
help="Auth workflow profile.",
|
|
289
|
+
),
|
|
285
290
|
write: bool = typer.Option(False, "--write", help="Allow write operations."),
|
|
286
291
|
verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose logging."),
|
|
287
292
|
) -> None:
|
|
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
|