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.
Files changed (32) hide show
  1. {buildai_cli-0.3.48 → buildai_cli-0.3.49}/.gitignore +1 -0
  2. {buildai_cli-0.3.48 → buildai_cli-0.3.49}/PKG-INFO +1 -1
  3. {buildai_cli-0.3.48 → buildai_cli-0.3.49}/cli/commands/db/query.py +3 -3
  4. {buildai_cli-0.3.48 → buildai_cli-0.3.49}/cli/commands/db/schema.py +3 -3
  5. {buildai_cli-0.3.48 → buildai_cli-0.3.49}/cli/commands/db/status.py +2 -2
  6. {buildai_cli-0.3.48 → buildai_cli-0.3.49}/cli/context.py +64 -35
  7. {buildai_cli-0.3.48 → buildai_cli-0.3.49}/cli/main.py +6 -1
  8. {buildai_cli-0.3.48 → buildai_cli-0.3.49}/pyproject.toml +1 -1
  9. {buildai_cli-0.3.48 → buildai_cli-0.3.49}/AGENTS.md +0 -0
  10. {buildai_cli-0.3.48 → buildai_cli-0.3.49}/CLAUDE.md +0 -0
  11. {buildai_cli-0.3.48 → buildai_cli-0.3.49}/buildai_bootstrap.py +0 -0
  12. {buildai_cli-0.3.48 → buildai_cli-0.3.49}/cli/__init__.py +0 -0
  13. {buildai_cli-0.3.48 → buildai_cli-0.3.49}/cli/_has_core.py +0 -0
  14. {buildai_cli-0.3.48 → buildai_cli-0.3.49}/cli/auth_local.py +0 -0
  15. {buildai_cli-0.3.48 → buildai_cli-0.3.49}/cli/auth_proxy.py +0 -0
  16. {buildai_cli-0.3.48 → buildai_cli-0.3.49}/cli/commands/__init__.py +0 -0
  17. {buildai_cli-0.3.48 → buildai_cli-0.3.49}/cli/commands/api_proxy.py +0 -0
  18. {buildai_cli-0.3.48 → buildai_cli-0.3.49}/cli/commands/auth.py +0 -0
  19. {buildai_cli-0.3.48 → buildai_cli-0.3.49}/cli/commands/db/__init__.py +0 -0
  20. {buildai_cli-0.3.48 → buildai_cli-0.3.49}/cli/commands/db/common.py +0 -0
  21. {buildai_cli-0.3.48 → buildai_cli-0.3.49}/cli/commands/db/migrate.py +0 -0
  22. {buildai_cli-0.3.48 → buildai_cli-0.3.49}/cli/commands/dev.py +0 -0
  23. {buildai_cli-0.3.48 → buildai_cli-0.3.49}/cli/commands/doctor.py +0 -0
  24. {buildai_cli-0.3.48 → buildai_cli-0.3.49}/cli/config.py +0 -0
  25. {buildai_cli-0.3.48 → buildai_cli-0.3.49}/cli/console.py +0 -0
  26. {buildai_cli-0.3.48 → buildai_cli-0.3.49}/cli/guard.py +0 -0
  27. {buildai_cli-0.3.48 → buildai_cli-0.3.49}/cli/internal_api.py +0 -0
  28. {buildai_cli-0.3.48 → buildai_cli-0.3.49}/cli/nl_query/__init__.py +0 -0
  29. {buildai_cli-0.3.48 → buildai_cli-0.3.49}/cli/nl_query/dataset_tools.py +0 -0
  30. {buildai_cli-0.3.48 → buildai_cli-0.3.49}/cli/ops_init.py +0 -0
  31. {buildai_cli-0.3.48 → buildai_cli-0.3.49}/cli/output.py +0 -0
  32. {buildai_cli-0.3.48 → buildai_cli-0.3.49}/cli/pagination.py +0 -0
@@ -29,6 +29,7 @@ node_modules/
29
29
  .next/
30
30
  .expect/
31
31
  out/
32
+ *.tsbuildinfo
32
33
 
33
34
  # IDE
34
35
  .idea/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: buildai-cli
3
- Version: 0.3.48
3
+ Version: 0.3.49
4
4
  Summary: Build AI CLI (Typer)
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: python-dotenv>=1.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 get_connection
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 directly against the selected database lane."""
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 get_connection(settings) as conn:
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 get_connection
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 normal DB connection path for schema introspection."""
26
+ """Use the direct inspection connection for schema introspection."""
27
27
 
28
- return get_connection(settings)
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 get_connection
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 get_connection(settings) as conn:
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 database connections and canonical DAL access.
1
+ """CLI context management for direct DB inspection and scoped DAL access.
2
2
 
3
- This module provides a unified interface for CLI commands to access the database,
4
- using infra.Database for AlloyDB (dev/prod) and direct asyncpg for test/CI.
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
- # Simple connection (most commands)
8
- async with get_connection(settings) as conn:
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 _stamp_cli_session_contract(
100
+ async def _stamp_base_cli_session_contract(
91
101
  conn: asyncpg.Connection,
92
102
  *,
93
- profile: str,
103
+ unrestricted: bool,
94
104
  ) -> None:
95
- """Stamp the canonical auth GUC contract for DB-direct CLI sessions."""
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("SELECT set_config('app.unrestricted', $1, false)", unrestricted)
99
- await conn.execute("SELECT set_config('app.allowed_organization_ids', $1, false)", "{}")
100
- await conn.execute("SELECT set_config('app.allowed_site_ids', $1, false)", "{}")
101
- await conn.execute("SELECT set_config('app.allowed_dataset_ids', $1, false)", "{}")
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 get_connection(
213
+ async def get_inspection_connection(
178
214
  settings: Settings | None = None,
179
215
  ) -> AsyncGenerator[asyncpg.Connection, None]:
180
216
  """
181
- Get a database connection for CLI commands.
217
+ Get a direct DB inspection connection for CLI commands.
182
218
 
183
- Handles routing between:
184
- - AlloyDB (development/production) - via infra.Database
185
- - Local PostgreSQL (test/CI) - direct asyncpg
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 get_connection(settings) as conn:
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 _stamp_cli_session_contract(conn, profile=resolved_profile)
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 _stamp_cli_session_contract(db.conn, profile=resolved_profile)
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 _stamp_cli_session_contract(db.conn, profile=resolved_profile)
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 _stamp_cli_session_contract(conn, profile=resolved_profile)
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 _stamp_cli_session_contract(db.conn, profile=resolved_profile)
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(None, "--profile", "-p", help="CLI scope profile."),
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:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "buildai-cli"
7
- version = "0.3.48"
7
+ version = "0.3.49"
8
8
  description = "Build AI CLI (Typer)"
9
9
  requires-python = ">=3.11"
10
10
  dependencies = [
File without changes
File without changes
File without changes
File without changes
File without changes