sovaria-cli 0.2.0__py3-none-any.whl

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 (46) hide show
  1. apps/cli/README.md +147 -0
  2. apps/cli/__init__.py +0 -0
  3. apps/cli/src/__init__.py +3 -0
  4. apps/cli/src/commands/__init__.py +1 -0
  5. apps/cli/src/commands/admin.py +160 -0
  6. apps/cli/src/commands/api.py +48 -0
  7. apps/cli/src/commands/audit.py +181 -0
  8. apps/cli/src/commands/auth_product.py +187 -0
  9. apps/cli/src/commands/billing.py +513 -0
  10. apps/cli/src/commands/chrono.py +248 -0
  11. apps/cli/src/commands/context.py +99 -0
  12. apps/cli/src/commands/data.py +272 -0
  13. apps/cli/src/commands/db.py +528 -0
  14. apps/cli/src/commands/deploy.py +1099 -0
  15. apps/cli/src/commands/governance.py +491 -0
  16. apps/cli/src/commands/iam.py +436 -0
  17. apps/cli/src/commands/init_cmd.py +177 -0
  18. apps/cli/src/commands/keys.py +120 -0
  19. apps/cli/src/commands/login.py +842 -0
  20. apps/cli/src/commands/org.py +157 -0
  21. apps/cli/src/commands/policy.py +317 -0
  22. apps/cli/src/commands/project.py +130 -0
  23. apps/cli/src/commands/realtime.py +364 -0
  24. apps/cli/src/commands/scope.py +42 -0
  25. apps/cli/src/commands/shell.py +415 -0
  26. apps/cli/src/commands/sim.py +178 -0
  27. apps/cli/src/commands/storage.py +405 -0
  28. apps/cli/src/commands/vector.py +204 -0
  29. apps/cli/src/main.py +169 -0
  30. apps/cli/src/utils/__init__.py +1 -0
  31. apps/cli/src/utils/api_client.py +329 -0
  32. apps/cli/src/utils/auth_store.py +299 -0
  33. apps/cli/src/utils/config.py +185 -0
  34. apps/cli/src/utils/display.py +62 -0
  35. apps/cli/src/utils/migrations.py +69 -0
  36. apps/cli/src/utils/project_context.py +124 -0
  37. apps/cli/tests/test_auth_store_profiles.py +103 -0
  38. apps/cli/tests/test_cli_security.py +110 -0
  39. apps/cli/tests/test_cli_smoke_paths.py +279 -0
  40. apps/cli/tests/test_config_profiles.py +55 -0
  41. apps/cli/tests/test_main_startup.py +29 -0
  42. apps/cli/tests/test_project_and_commands.py +196 -0
  43. sovaria_cli-0.2.0.dist-info/METADATA +170 -0
  44. sovaria_cli-0.2.0.dist-info/RECORD +46 -0
  45. sovaria_cli-0.2.0.dist-info/WHEEL +4 -0
  46. sovaria_cli-0.2.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,528 @@
1
+ """
2
+ apps.cli.src.commands.db — Database management commands.
3
+
4
+ Commands:
5
+ sov db tables List migration log / tables
6
+ sov db create-table Create a new table
7
+ sov db sql Execute SQL (supports --interactive)
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+
14
+ import typer
15
+
16
+ from apps.cli.src.utils.api_client import CLIRequestError, call_api, parse_json_arg
17
+ from apps.cli.src.utils.display import print_json, print_sql_result
18
+
19
+ app = typer.Typer(help="Database (SQL) management")
20
+
21
+
22
+ @app.command("tables")
23
+ def list_tables(
24
+ org_id: str | None = typer.Option(None, "--org", help="Organization ID"),
25
+ project_id: str | None = typer.Option(None, "--project", help="Project ID"),
26
+ env_id: str | None = typer.Option(None, "--env"),
27
+ profile: str | None = typer.Option(None, "--profile"),
28
+ ) -> None:
29
+ """List relational tables / migration log for the linked project."""
30
+ try:
31
+ result = call_api(
32
+ method="GET",
33
+ path="/api/v1/data/rel/migrations",
34
+ profile=profile,
35
+ org_id=org_id,
36
+ project_id=project_id,
37
+ env_id=env_id,
38
+ require_scope=True,
39
+ )
40
+ except CLIRequestError as exc:
41
+ typer.echo(str(exc), err=True)
42
+ raise typer.Exit(1) from exc
43
+ print_json(result)
44
+
45
+
46
+ @app.command("sql")
47
+ def run_sql(
48
+ query: str | None = typer.Argument(None, help="SQL query to execute"),
49
+ interactive: bool = typer.Option(False, "--interactive", "-i", help="Interactive SQL REPL"),
50
+ org_id: str | None = typer.Option(None, "--org", help="Organization ID"),
51
+ project_id: str | None = typer.Option(None, "--project", help="Project ID"),
52
+ env_id: str | None = typer.Option(None, "--env"),
53
+ profile: str | None = typer.Option(None, "--profile"),
54
+ ) -> None:
55
+ """Execute SQL against Atlas-Rel. Use --interactive for a mini psql-style loop."""
56
+ if interactive:
57
+ typer.echo("Interactive SQL (\\q or exit to quit)")
58
+ while True:
59
+ try:
60
+ line = typer.prompt("sql", prompt_suffix="> ")
61
+ except (EOFError, KeyboardInterrupt):
62
+ typer.echo("")
63
+ break
64
+ stripped = line.strip()
65
+ if not stripped:
66
+ continue
67
+ if stripped.lower() in {"\\q", "exit", "quit"}:
68
+ break
69
+ _execute_sql(
70
+ stripped,
71
+ profile=profile,
72
+ org_id=org_id,
73
+ project_id=project_id,
74
+ env_id=env_id,
75
+ )
76
+ return
77
+
78
+ if not query:
79
+ typer.echo("Provide a SQL query or use --interactive", err=True)
80
+ raise typer.Exit(1)
81
+
82
+ _execute_sql(
83
+ query,
84
+ profile=profile,
85
+ org_id=org_id,
86
+ project_id=project_id,
87
+ env_id=env_id,
88
+ )
89
+
90
+
91
+ def _execute_sql(
92
+ sql: str,
93
+ *,
94
+ profile: str | None,
95
+ org_id: str | None,
96
+ project_id: str | None,
97
+ env_id: str | None,
98
+ ) -> None:
99
+ try:
100
+ result = call_api(
101
+ method="POST",
102
+ path="/api/v1/sql/query",
103
+ profile=profile,
104
+ body={"sql": sql, "txid": 0},
105
+ org_id=org_id,
106
+ project_id=project_id,
107
+ env_id=env_id,
108
+ require_scope=True,
109
+ )
110
+ except CLIRequestError as exc:
111
+ typer.echo(str(exc), err=True)
112
+ raise typer.Exit(1) from exc
113
+ print_sql_result(result)
114
+
115
+
116
+ @app.command("create-table")
117
+ def create_table(
118
+ name: str = typer.Argument(..., help="Table name"),
119
+ columns_json: str = typer.Argument(
120
+ ...,
121
+ help='Columns as JSON array, e.g. \'[{"name":"id","type":"SERIAL","primary_key":true}]\'',
122
+ ),
123
+ foreign_keys: str | None = typer.Option(None, "--foreign-keys", help="JSON array of FK defs"),
124
+ org_id: str | None = typer.Option(None, "--org", help="Organization ID"),
125
+ project_id: str | None = typer.Option(None, "--project", help="Project ID"),
126
+ env_id: str | None = typer.Option(None, "--env"),
127
+ profile: str | None = typer.Option(None, "--profile"),
128
+ ) -> None:
129
+ """Create a new relational table."""
130
+ try:
131
+ columns = parse_json_arg(columns_json, label="columns")
132
+ parsed_fk = parse_json_arg(foreign_keys, label="--foreign-keys") if foreign_keys else []
133
+ if not isinstance(columns, list):
134
+ raise CLIRequestError("columns must be a JSON array")
135
+ if parsed_fk is not None and not isinstance(parsed_fk, list):
136
+ raise CLIRequestError("--foreign-keys must be a JSON array")
137
+ result = call_api(
138
+ method="POST",
139
+ path="/api/v1/data/rel/create",
140
+ profile=profile,
141
+ body={
142
+ "table_name": name,
143
+ "columns": columns,
144
+ "foreign_keys": parsed_fk or [],
145
+ },
146
+ org_id=org_id,
147
+ project_id=project_id,
148
+ env_id=env_id,
149
+ require_scope=True,
150
+ )
151
+ except CLIRequestError as exc:
152
+ typer.echo(str(exc), err=True)
153
+ raise typer.Exit(1) from exc
154
+ print_json(result)
155
+ typer.echo(f"Table '{name}' created successfully")
156
+
157
+
158
+ # ── Transaction subcommands ───────────────────────────────────────────────────
159
+
160
+ tx_app = typer.Typer(help="SQL transaction control")
161
+ app.add_typer(tx_app, name="tx")
162
+
163
+
164
+ def _scope_opts(
165
+ profile: str | None,
166
+ org_id: str | None,
167
+ project_id: str | None,
168
+ env_id: str | None,
169
+ ) -> dict[str, str | None]:
170
+ return {
171
+ "profile": profile,
172
+ "org_id": org_id,
173
+ "project_id": project_id,
174
+ "env_id": env_id,
175
+ }
176
+
177
+
178
+ @tx_app.command("begin")
179
+ def tx_begin(
180
+ profile: str | None = typer.Option(None, "--profile"),
181
+ org_id: str | None = typer.Option(None, "--org"),
182
+ project_id: str | None = typer.Option(None, "--project"),
183
+ env_id: str | None = typer.Option(None, "--env"),
184
+ ) -> None:
185
+ """Begin a relational transaction and return txid."""
186
+ try:
187
+ result = call_api(
188
+ method="POST",
189
+ path="/api/v1/data/rel/transaction/begin",
190
+ require_scope=True,
191
+ **_scope_opts(profile, org_id, project_id, env_id),
192
+ )
193
+ except CLIRequestError as exc:
194
+ typer.echo(str(exc), err=True)
195
+ raise typer.Exit(1) from exc
196
+ print_json(result)
197
+
198
+
199
+ @tx_app.command("commit")
200
+ def tx_commit(
201
+ txid: int = typer.Option(..., "--txid", help="Transaction id from `sov db tx begin`"),
202
+ profile: str | None = typer.Option(None, "--profile"),
203
+ org_id: str | None = typer.Option(None, "--org"),
204
+ project_id: str | None = typer.Option(None, "--project"),
205
+ env_id: str | None = typer.Option(None, "--env"),
206
+ ) -> None:
207
+ """Commit an open transaction."""
208
+ try:
209
+ result = call_api(
210
+ method="POST",
211
+ path="/api/v1/data/rel/transaction/commit",
212
+ body={"txid": txid},
213
+ require_scope=True,
214
+ **_scope_opts(profile, org_id, project_id, env_id),
215
+ )
216
+ except CLIRequestError as exc:
217
+ typer.echo(str(exc), err=True)
218
+ raise typer.Exit(1) from exc
219
+ print_json(result)
220
+
221
+
222
+ @tx_app.command("rollback")
223
+ def tx_rollback(
224
+ txid: int = typer.Option(..., "--txid", help="Transaction id from `sov db tx begin`"),
225
+ profile: str | None = typer.Option(None, "--profile"),
226
+ org_id: str | None = typer.Option(None, "--org"),
227
+ project_id: str | None = typer.Option(None, "--project"),
228
+ env_id: str | None = typer.Option(None, "--env"),
229
+ ) -> None:
230
+ """Rollback an open transaction."""
231
+ try:
232
+ result = call_api(
233
+ method="POST",
234
+ path="/api/v1/data/rel/transaction/rollback",
235
+ body={"txid": txid},
236
+ require_scope=True,
237
+ **_scope_opts(profile, org_id, project_id, env_id),
238
+ )
239
+ except CLIRequestError as exc:
240
+ typer.echo(str(exc), err=True)
241
+ raise typer.Exit(1) from exc
242
+ print_json(result)
243
+
244
+
245
+ # ── Column / FK DDL ───────────────────────────────────────────────────────────
246
+
247
+ column_app = typer.Typer(help="Column DDL operations")
248
+ app.add_typer(column_app, name="column")
249
+
250
+
251
+ @column_app.command("add")
252
+ def column_add(
253
+ table: str = typer.Argument(..., help="Table name"),
254
+ name: str = typer.Argument(..., help="Column name"),
255
+ data_type: str = typer.Option(..., "--type", help="SQL data type, e.g. TEXT or INTEGER"),
256
+ nullable: bool = typer.Option(True, "--nullable/--not-null"),
257
+ unique: bool = typer.Option(False, "--unique"),
258
+ default: str | None = typer.Option(None, "--default"),
259
+ profile: str | None = typer.Option(None, "--profile"),
260
+ org_id: str | None = typer.Option(None, "--org"),
261
+ project_id: str | None = typer.Option(None, "--project"),
262
+ env_id: str | None = typer.Option(None, "--env"),
263
+ ) -> None:
264
+ """Add a column to an existing table."""
265
+ body: dict[str, object] = {
266
+ "table_name": table,
267
+ "column_name": name,
268
+ "data_type": data_type,
269
+ "is_nullable": nullable,
270
+ "is_unique": unique,
271
+ }
272
+ if default is not None:
273
+ body["default_value"] = default
274
+ try:
275
+ result = call_api(
276
+ method="POST",
277
+ path="/api/v1/data/rel/column/add",
278
+ body=body,
279
+ require_scope=True,
280
+ **_scope_opts(profile, org_id, project_id, env_id),
281
+ )
282
+ except CLIRequestError as exc:
283
+ typer.echo(str(exc), err=True)
284
+ raise typer.Exit(1) from exc
285
+ print_json(result)
286
+
287
+
288
+ @column_app.command("drop")
289
+ def column_drop(
290
+ table: str = typer.Argument(...),
291
+ name: str = typer.Argument(..., help="Column name"),
292
+ profile: str | None = typer.Option(None, "--profile"),
293
+ org_id: str | None = typer.Option(None, "--org"),
294
+ project_id: str | None = typer.Option(None, "--project"),
295
+ env_id: str | None = typer.Option(None, "--env"),
296
+ ) -> None:
297
+ """Drop a column from a table."""
298
+ try:
299
+ result = call_api(
300
+ method="POST",
301
+ path="/api/v1/data/rel/column/drop",
302
+ body={"table_name": table, "column_name": name},
303
+ require_scope=True,
304
+ **_scope_opts(profile, org_id, project_id, env_id),
305
+ )
306
+ except CLIRequestError as exc:
307
+ typer.echo(str(exc), err=True)
308
+ raise typer.Exit(1) from exc
309
+ print_json(result)
310
+
311
+
312
+ fk_app = typer.Typer(help="Foreign key DDL operations")
313
+ app.add_typer(fk_app, name="fk")
314
+
315
+
316
+ @fk_app.command("add")
317
+ def fk_add(
318
+ table: str = typer.Argument(...),
319
+ column: str = typer.Argument(...),
320
+ ref_table: str = typer.Option(..., "--ref-table"),
321
+ ref_column: str = typer.Option(..., "--ref-column"),
322
+ profile: str | None = typer.Option(None, "--profile"),
323
+ org_id: str | None = typer.Option(None, "--org"),
324
+ project_id: str | None = typer.Option(None, "--project"),
325
+ env_id: str | None = typer.Option(None, "--env"),
326
+ ) -> None:
327
+ """Add a foreign key between tables."""
328
+ try:
329
+ result = call_api(
330
+ method="POST",
331
+ path="/api/v1/data/rel/foreign-key/add",
332
+ body={
333
+ "table_name": table,
334
+ "column_name": column,
335
+ "referenced_table": ref_table,
336
+ "referenced_column": ref_column,
337
+ },
338
+ require_scope=True,
339
+ **_scope_opts(profile, org_id, project_id, env_id),
340
+ )
341
+ except CLIRequestError as exc:
342
+ typer.echo(str(exc), err=True)
343
+ raise typer.Exit(1) from exc
344
+ print_json(result)
345
+
346
+
347
+ @fk_app.command("drop")
348
+ def fk_drop(
349
+ table: str = typer.Argument(...),
350
+ column: str = typer.Argument(...),
351
+ ref_table: str = typer.Option(..., "--ref-table"),
352
+ ref_column: str = typer.Option(..., "--ref-column"),
353
+ profile: str | None = typer.Option(None, "--profile"),
354
+ org_id: str | None = typer.Option(None, "--org"),
355
+ project_id: str | None = typer.Option(None, "--project"),
356
+ env_id: str | None = typer.Option(None, "--env"),
357
+ ) -> None:
358
+ """Drop a foreign key."""
359
+ try:
360
+ result = call_api(
361
+ method="POST",
362
+ path="/api/v1/data/rel/foreign-key/drop",
363
+ body={
364
+ "table_name": table,
365
+ "column_name": column,
366
+ "referenced_table": ref_table,
367
+ "referenced_column": ref_column,
368
+ },
369
+ require_scope=True,
370
+ **_scope_opts(profile, org_id, project_id, env_id),
371
+ )
372
+ except CLIRequestError as exc:
373
+ typer.echo(str(exc), err=True)
374
+ raise typer.Exit(1) from exc
375
+ print_json(result)
376
+
377
+
378
+ # ── Migrations push / pull ────────────────────────────────────────────────────
379
+
380
+ migrations_app = typer.Typer(help="Local SQL migration files")
381
+ app.add_typer(migrations_app, name="migrations")
382
+
383
+
384
+ @migrations_app.command("list")
385
+ def migrations_list(
386
+ table: str | None = typer.Option(None, "--table"),
387
+ limit: int = typer.Option(100, "--limit"),
388
+ offset: int = typer.Option(0, "--offset"),
389
+ local: bool = typer.Option(False, "--local", help="List local .sql files only"),
390
+ profile: str | None = typer.Option(None, "--profile"),
391
+ org_id: str | None = typer.Option(None, "--org"),
392
+ project_id: str | None = typer.Option(None, "--project"),
393
+ env_id: str | None = typer.Option(None, "--env"),
394
+ ) -> None:
395
+ """List remote migration log entries or local migration files."""
396
+ from apps.cli.src.utils.migrations import list_local_migrations, load_applied_state
397
+
398
+ if local:
399
+ files = list_local_migrations()
400
+ applied = load_applied_state()
401
+ for path in files:
402
+ marker = "✓" if path.name in applied else " "
403
+ typer.echo(f" [{marker}] {path.name}")
404
+ typer.echo(f"\n{len(files)} local file(s), {len(applied)} applied locally")
405
+ return
406
+
407
+ params: dict[str, str] = {"limit": str(limit), "offset": str(offset)}
408
+ if table:
409
+ params["table_name"] = table
410
+ try:
411
+ result = call_api(
412
+ method="GET",
413
+ path="/api/v1/data/rel/migrations",
414
+ params=params,
415
+ require_scope=True,
416
+ **_scope_opts(profile, org_id, project_id, env_id),
417
+ )
418
+ except CLIRequestError as exc:
419
+ typer.echo(str(exc), err=True)
420
+ raise typer.Exit(1) from exc
421
+ print_json(result)
422
+
423
+
424
+ @migrations_app.command("push")
425
+ def migrations_push(
426
+ directory: str | None = typer.Option(
427
+ None,
428
+ "--dir",
429
+ help="Migration directory (default: sovaria/migrations under linked project)",
430
+ ),
431
+ dry_run: bool = typer.Option(False, "--dry-run", help="Show pending files without executing"),
432
+ profile: str | None = typer.Option(None, "--profile"),
433
+ org_id: str | None = typer.Option(None, "--org"),
434
+ project_id: str | None = typer.Option(None, "--project"),
435
+ env_id: str | None = typer.Option(None, "--env"),
436
+ ) -> None:
437
+ """Apply pending local .sql migration files in sorted order."""
438
+ from pathlib import Path
439
+
440
+ from apps.cli.src.utils.display import print_success, print_warning
441
+ from apps.cli.src.utils.migrations import (
442
+ list_local_migrations,
443
+ load_applied_state,
444
+ migration_name,
445
+ migrations_dir,
446
+ save_applied_state,
447
+ )
448
+
449
+ folder = Path(directory) if directory else migrations_dir()
450
+ files = list_local_migrations(folder)
451
+ if not files:
452
+ print_warning(f"No .sql files in {folder}. Create migrations there first.")
453
+ raise typer.Exit(0)
454
+
455
+ applied = load_applied_state()
456
+ pending = [path for path in files if migration_name(path) not in applied]
457
+ if not pending:
458
+ print_success("All local migrations already applied.")
459
+ raise typer.Exit(0)
460
+
461
+ typer.echo(f"Pending migrations ({len(pending)}):")
462
+ for path in pending:
463
+ typer.echo(f" • {path.name}")
464
+
465
+ if dry_run:
466
+ raise typer.Exit(0)
467
+
468
+ for path in pending:
469
+ sql = path.read_text(encoding="utf-8").strip()
470
+ if not sql:
471
+ print_warning(f"Skipping empty file: {path.name}")
472
+ applied.add(migration_name(path))
473
+ continue
474
+ typer.echo(f"Applying {path.name} …")
475
+ try:
476
+ call_api(
477
+ method="POST",
478
+ path="/api/v1/sql/query",
479
+ body={"sql": sql, "txid": 0},
480
+ require_scope=True,
481
+ **_scope_opts(profile, org_id, project_id, env_id),
482
+ )
483
+ except CLIRequestError as exc:
484
+ typer.echo(str(exc), err=True)
485
+ raise typer.Exit(1) from exc
486
+ applied.add(migration_name(path))
487
+
488
+ state_file = save_applied_state(applied)
489
+ print_success(f"Applied {len(pending)} migration(s). State: {state_file}")
490
+
491
+
492
+ @migrations_app.command("pull")
493
+ def migrations_pull(
494
+ output: str | None = typer.Option(
495
+ None,
496
+ "--output",
497
+ "-o",
498
+ help="Write remote migration log JSON to this path",
499
+ ),
500
+ profile: str | None = typer.Option(None, "--profile"),
501
+ org_id: str | None = typer.Option(None, "--org"),
502
+ project_id: str | None = typer.Option(None, "--project"),
503
+ env_id: str | None = typer.Option(None, "--env"),
504
+ ) -> None:
505
+ """Fetch remote migration log (schema change history) as JSON."""
506
+ from pathlib import Path
507
+
508
+ from apps.cli.src.utils.migrations import migrations_dir
509
+
510
+ try:
511
+ result = call_api(
512
+ method="GET",
513
+ path="/api/v1/data/rel/migrations",
514
+ params={"limit": "500", "offset": "0"},
515
+ require_scope=True,
516
+ **_scope_opts(profile, org_id, project_id, env_id),
517
+ )
518
+ except CLIRequestError as exc:
519
+ typer.echo(str(exc), err=True)
520
+ raise typer.Exit(1) from exc
521
+
522
+ if output:
523
+ out_path = Path(output)
524
+ else:
525
+ out_path = migrations_dir() / "remote_migrations.json"
526
+ out_path.parent.mkdir(parents=True, exist_ok=True)
527
+ out_path.write_text(json.dumps(result, indent=2, sort_keys=True, default=str) + "\n")
528
+ typer.echo(f"Wrote migration log to {out_path}")