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,491 @@
1
+ """Nexus governance plane commands (regions, tenants, databases, metrics)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from urllib.parse import quote
6
+
7
+ import typer
8
+
9
+ from apps.cli.src.utils.api_client import CLIRequestError, call_api, parse_json_arg
10
+ from apps.cli.src.utils.display import print_json
11
+
12
+ app = typer.Typer(help="Platform governance: regions, tenants, databases, metrics")
13
+
14
+
15
+ def _gov_path(*parts: str) -> str:
16
+ return "/api/v1/governance/" + "/".join(quote(part, safe="") for part in parts)
17
+
18
+
19
+ @app.command("regions")
20
+ def list_regions(
21
+ profile: str | None = typer.Option(None, "--profile"),
22
+ ) -> None:
23
+ """List configured governance regions."""
24
+ try:
25
+ result = call_api(
26
+ method="GET",
27
+ path="/api/v1/governance/regions",
28
+ profile=profile,
29
+ require_scope=False,
30
+ )
31
+ except CLIRequestError as exc:
32
+ typer.echo(str(exc), err=True)
33
+ raise typer.Exit(1) from exc
34
+ print_json(result)
35
+
36
+
37
+ @app.command("regions-available")
38
+ def regions_available(
39
+ size: str = typer.Option("micro", "--size", help="Compute size filter"),
40
+ plan: str | None = typer.Option(None, "--plan", help="Optional plan filter"),
41
+ profile: str | None = typer.Option(None, "--profile"),
42
+ ) -> None:
43
+ """List regions with available capacity for provisioning."""
44
+ params: dict[str, str] = {"size": size}
45
+ if plan:
46
+ params["plan"] = plan
47
+ try:
48
+ result = call_api(
49
+ method="GET",
50
+ path="/api/v1/governance/regions/available",
51
+ profile=profile,
52
+ require_scope=False,
53
+ params=params,
54
+ )
55
+ except CLIRequestError as exc:
56
+ typer.echo(str(exc), err=True)
57
+ raise typer.Exit(1) from exc
58
+ print_json(result)
59
+
60
+
61
+ @app.command("region-patch")
62
+ def region_patch(
63
+ region_id: str = typer.Argument(..., help="Region ID"),
64
+ label: str | None = typer.Option(None, "--label"),
65
+ status: str | None = typer.Option(None, "--status"),
66
+ is_default: bool | None = typer.Option(None, "--default/--no-default"),
67
+ profile: str | None = typer.Option(None, "--profile"),
68
+ ) -> None:
69
+ """Patch governance region metadata."""
70
+ body: dict[str, object] = {}
71
+ if label is not None:
72
+ body["label"] = label
73
+ if status is not None:
74
+ body["status"] = status
75
+ if is_default is not None:
76
+ body["is_default"] = is_default
77
+ if not body:
78
+ raise typer.BadParameter("Provide at least one of --label, --status, or --default")
79
+ try:
80
+ result = call_api(
81
+ method="PATCH",
82
+ path=_gov_path("regions", region_id),
83
+ profile=profile,
84
+ require_scope=False,
85
+ body=body,
86
+ )
87
+ except CLIRequestError as exc:
88
+ typer.echo(str(exc), err=True)
89
+ raise typer.Exit(1) from exc
90
+ print_json(result)
91
+
92
+
93
+ @app.command("tenants")
94
+ def list_tenants(
95
+ profile: str | None = typer.Option(None, "--profile"),
96
+ ) -> None:
97
+ """List registered tenants."""
98
+ try:
99
+ result = call_api(
100
+ method="GET",
101
+ path="/api/v1/governance/tenants",
102
+ profile=profile,
103
+ require_scope=False,
104
+ )
105
+ except CLIRequestError as exc:
106
+ typer.echo(str(exc), err=True)
107
+ raise typer.Exit(1) from exc
108
+ print_json(result)
109
+
110
+
111
+ @app.command("tenant-get")
112
+ def tenant_get(
113
+ org_id: str = typer.Argument(..., help="Organisation / tenant ID"),
114
+ profile: str | None = typer.Option(None, "--profile"),
115
+ ) -> None:
116
+ """Fetch one tenant record."""
117
+ try:
118
+ result = call_api(
119
+ method="GET",
120
+ path=_gov_path("tenants", org_id),
121
+ profile=profile,
122
+ require_scope=False,
123
+ )
124
+ except CLIRequestError as exc:
125
+ typer.echo(str(exc), err=True)
126
+ raise typer.Exit(1) from exc
127
+ print_json(result)
128
+
129
+
130
+ @app.command("tenant-status")
131
+ def tenant_status(
132
+ org_id: str = typer.Argument(..., help="Organisation / tenant ID"),
133
+ status: str = typer.Option(..., "--status", help="active | suspended | deleted"),
134
+ profile: str | None = typer.Option(None, "--profile"),
135
+ ) -> None:
136
+ """Update tenant lifecycle status."""
137
+ try:
138
+ result = call_api(
139
+ method="POST",
140
+ path=_gov_path("tenants", org_id, "status"),
141
+ profile=profile,
142
+ require_scope=False,
143
+ body={"status": status},
144
+ )
145
+ except CLIRequestError as exc:
146
+ typer.echo(str(exc), err=True)
147
+ raise typer.Exit(1) from exc
148
+ print_json(result)
149
+
150
+
151
+ @app.command("tenant-databases")
152
+ def tenant_databases(
153
+ org_id: str = typer.Argument(..., help="Organisation / tenant ID"),
154
+ profile: str | None = typer.Option(None, "--profile"),
155
+ ) -> None:
156
+ """List databases provisioned for a tenant."""
157
+ try:
158
+ result = call_api(
159
+ method="GET",
160
+ path=_gov_path("tenants", org_id, "databases"),
161
+ profile=profile,
162
+ require_scope=False,
163
+ )
164
+ except CLIRequestError as exc:
165
+ typer.echo(str(exc), err=True)
166
+ raise typer.Exit(1) from exc
167
+ print_json(result)
168
+
169
+
170
+ @app.command("db-provision")
171
+ def db_provision(
172
+ org_id: str = typer.Argument(..., help="Organisation / tenant ID"),
173
+ project_id: str = typer.Option(..., "--project", help="Project ID to provision"),
174
+ tier: str = typer.Option("dedicated", "--tier"),
175
+ compute_size: str | None = typer.Option(None, "--compute-size"),
176
+ region: str | None = typer.Option(None, "--region"),
177
+ profile: str | None = typer.Option(None, "--profile"),
178
+ ) -> None:
179
+ """Provision a dedicated database for a tenant project."""
180
+ body: dict[str, object] = {"project_id": project_id, "tier": tier}
181
+ if compute_size:
182
+ body["compute_size"] = compute_size
183
+ if region:
184
+ body["region"] = region
185
+ try:
186
+ result = call_api(
187
+ method="POST",
188
+ path=_gov_path("tenants", org_id, "databases", "provision"),
189
+ profile=profile,
190
+ require_scope=False,
191
+ body=body,
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
+ @app.command("db-connection")
200
+ def db_connection(
201
+ org_id: str = typer.Argument(..., help="Organisation / tenant ID"),
202
+ project_id: str = typer.Argument(..., help="Project ID"),
203
+ profile: str | None = typer.Option(None, "--profile"),
204
+ ) -> None:
205
+ """Fetch database connection metadata (password redacted)."""
206
+ try:
207
+ result = call_api(
208
+ method="GET",
209
+ path=_gov_path("tenants", org_id, "databases", project_id, "connection"),
210
+ profile=profile,
211
+ require_scope=False,
212
+ )
213
+ except CLIRequestError as exc:
214
+ typer.echo(str(exc), err=True)
215
+ raise typer.Exit(1) from exc
216
+ print_json(result)
217
+
218
+
219
+ @app.command("db-connection-reveal")
220
+ def db_connection_reveal(
221
+ org_id: str = typer.Argument(..., help="Organisation / tenant ID"),
222
+ project_id: str = typer.Argument(..., help="Project ID"),
223
+ grant_id: str = typer.Option(..., "--grant-id", help="Active JIT grant ID"),
224
+ profile: str | None = typer.Option(None, "--profile"),
225
+ ) -> None:
226
+ """Reveal database password using an active JIT grant."""
227
+ try:
228
+ result = call_api(
229
+ method="POST",
230
+ path=_gov_path("tenants", org_id, "databases", project_id, "connection", "reveal"),
231
+ profile=profile,
232
+ require_scope=False,
233
+ body={"grant_id": grant_id},
234
+ )
235
+ except CLIRequestError as exc:
236
+ typer.echo(str(exc), err=True)
237
+ raise typer.Exit(1) from exc
238
+ print_json(result)
239
+
240
+
241
+ @app.command("db-pause")
242
+ def db_pause(
243
+ org_id: str = typer.Argument(..., help="Organisation / tenant ID"),
244
+ project_id: str = typer.Argument(..., help="Project ID"),
245
+ profile: str | None = typer.Option(None, "--profile"),
246
+ ) -> None:
247
+ """Pause a tenant database."""
248
+ try:
249
+ result = call_api(
250
+ method="POST",
251
+ path=_gov_path("tenants", org_id, "databases", project_id, "pause"),
252
+ profile=profile,
253
+ require_scope=False,
254
+ body={},
255
+ )
256
+ except CLIRequestError as exc:
257
+ typer.echo(str(exc), err=True)
258
+ raise typer.Exit(1) from exc
259
+ print_json(result)
260
+
261
+
262
+ @app.command("db-resume")
263
+ def db_resume(
264
+ org_id: str = typer.Argument(..., help="Organisation / tenant ID"),
265
+ project_id: str = typer.Argument(..., help="Project ID"),
266
+ profile: str | None = typer.Option(None, "--profile"),
267
+ ) -> None:
268
+ """Resume a paused tenant database."""
269
+ try:
270
+ result = call_api(
271
+ method="POST",
272
+ path=_gov_path("tenants", org_id, "databases", project_id, "resume"),
273
+ profile=profile,
274
+ require_scope=False,
275
+ body={},
276
+ )
277
+ except CLIRequestError as exc:
278
+ typer.echo(str(exc), err=True)
279
+ raise typer.Exit(1) from exc
280
+ print_json(result)
281
+
282
+
283
+ @app.command("db-resize")
284
+ def db_resize(
285
+ org_id: str = typer.Argument(..., help="Organisation / tenant ID"),
286
+ project_id: str = typer.Argument(..., help="Project ID"),
287
+ compute_size: str = typer.Option(..., "--compute-size", help="Target compute size"),
288
+ profile: str | None = typer.Option(None, "--profile"),
289
+ ) -> None:
290
+ """Resize a tenant database compute profile."""
291
+ try:
292
+ result = call_api(
293
+ method="POST",
294
+ path=_gov_path("tenants", org_id, "databases", project_id, "resize"),
295
+ profile=profile,
296
+ require_scope=False,
297
+ body={"compute_size": compute_size},
298
+ )
299
+ except CLIRequestError as exc:
300
+ typer.echo(str(exc), err=True)
301
+ raise typer.Exit(1) from exc
302
+ print_json(result)
303
+
304
+
305
+ @app.command("db-backups")
306
+ def db_backups(
307
+ org_id: str = typer.Argument(..., help="Organisation / tenant ID"),
308
+ project_id: str = typer.Argument(..., help="Project ID"),
309
+ limit: int = typer.Option(5, "--limit", min=1, max=100),
310
+ profile: str | None = typer.Option(None, "--profile"),
311
+ ) -> None:
312
+ """List database backups for a tenant project."""
313
+ try:
314
+ result = call_api(
315
+ method="GET",
316
+ path=_gov_path("tenants", org_id, "databases", project_id, "backups"),
317
+ profile=profile,
318
+ require_scope=False,
319
+ params={"limit": str(limit)},
320
+ )
321
+ except CLIRequestError as exc:
322
+ typer.echo(str(exc), err=True)
323
+ raise typer.Exit(1) from exc
324
+ print_json(result)
325
+
326
+
327
+ @app.command("db-backup-trigger")
328
+ def db_backup_trigger(
329
+ org_id: str = typer.Argument(..., help="Organisation / tenant ID"),
330
+ project_id: str = typer.Argument(..., help="Project ID"),
331
+ profile: str | None = typer.Option(None, "--profile"),
332
+ ) -> None:
333
+ """Trigger a manual database backup."""
334
+ try:
335
+ result = call_api(
336
+ method="POST",
337
+ path=_gov_path("tenants", org_id, "databases", project_id, "backups"),
338
+ profile=profile,
339
+ require_scope=False,
340
+ body={},
341
+ )
342
+ except CLIRequestError as exc:
343
+ typer.echo(str(exc), err=True)
344
+ raise typer.Exit(1) from exc
345
+ print_json(result)
346
+
347
+
348
+ @app.command("db-delete")
349
+ def db_delete(
350
+ org_id: str = typer.Argument(..., help="Organisation / tenant ID"),
351
+ project_id: str = typer.Argument(..., help="Project ID"),
352
+ snapshot: bool = typer.Option(True, "--snapshot/--no-snapshot"),
353
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"),
354
+ profile: str | None = typer.Option(None, "--profile"),
355
+ ) -> None:
356
+ """Delete a tenant database (optionally after snapshot)."""
357
+ if not yes and not typer.confirm(f"Delete database for project '{project_id}'?"):
358
+ raise typer.Exit(0)
359
+ params = {"snapshot": "true" if snapshot else "false"}
360
+ try:
361
+ result = call_api(
362
+ method="DELETE",
363
+ path=_gov_path("tenants", org_id, "databases", project_id),
364
+ profile=profile,
365
+ require_scope=False,
366
+ params=params,
367
+ )
368
+ except CLIRequestError as exc:
369
+ typer.echo(str(exc), err=True)
370
+ raise typer.Exit(1) from exc
371
+ print_json(result)
372
+
373
+
374
+ @app.command("admin-databases")
375
+ def admin_databases(
376
+ profile: str | None = typer.Option(None, "--profile"),
377
+ ) -> None:
378
+ """List all tenant databases (platform admin)."""
379
+ try:
380
+ result = call_api(
381
+ method="GET",
382
+ path="/api/v1/governance/admin/databases",
383
+ profile=profile,
384
+ require_scope=False,
385
+ )
386
+ except CLIRequestError as exc:
387
+ typer.echo(str(exc), err=True)
388
+ raise typer.Exit(1) from exc
389
+ print_json(result)
390
+
391
+
392
+ @app.command("hosts")
393
+ def list_hosts(
394
+ region_id: str | None = typer.Option(None, "--region-id"),
395
+ profile: str | None = typer.Option(None, "--profile"),
396
+ ) -> None:
397
+ """List governance hosts."""
398
+ params = {"region_id": region_id} if region_id else None
399
+ try:
400
+ result = call_api(
401
+ method="GET",
402
+ path="/api/v1/governance/hosts",
403
+ profile=profile,
404
+ require_scope=False,
405
+ params=params,
406
+ )
407
+ except CLIRequestError as exc:
408
+ typer.echo(str(exc), err=True)
409
+ raise typer.Exit(1) from exc
410
+ print_json(result)
411
+
412
+
413
+ @app.command("host-patch")
414
+ def host_patch(
415
+ host_id: str = typer.Argument(..., help="Host ID"),
416
+ status: str | None = typer.Option(None, "--status"),
417
+ address: str | None = typer.Option(None, "--address"),
418
+ total_vcpu: float | None = typer.Option(None, "--total-vcpu"),
419
+ total_memory_mb: int | None = typer.Option(None, "--total-memory-mb"),
420
+ total_storage_gb: int | None = typer.Option(None, "--total-storage-gb"),
421
+ profile: str | None = typer.Option(None, "--profile"),
422
+ ) -> None:
423
+ """Patch governance host metadata."""
424
+ body: dict[str, object] = {}
425
+ if status is not None:
426
+ body["status"] = status
427
+ if address is not None:
428
+ body["address"] = address
429
+ if total_vcpu is not None:
430
+ body["total_vcpu"] = total_vcpu
431
+ if total_memory_mb is not None:
432
+ body["total_memory_mb"] = total_memory_mb
433
+ if total_storage_gb is not None:
434
+ body["total_storage_gb"] = total_storage_gb
435
+ if not body:
436
+ raise typer.BadParameter("Provide at least one patch field")
437
+ try:
438
+ result = call_api(
439
+ method="PATCH",
440
+ path=_gov_path("hosts", host_id),
441
+ profile=profile,
442
+ require_scope=False,
443
+ body=body,
444
+ )
445
+ except CLIRequestError as exc:
446
+ typer.echo(str(exc), err=True)
447
+ raise typer.Exit(1) from exc
448
+ print_json(result)
449
+
450
+
451
+ @app.command("metrics-overview")
452
+ def metrics_overview(
453
+ profile: str | None = typer.Option(None, "--profile"),
454
+ ) -> None:
455
+ """Fetch platform metrics overview."""
456
+ try:
457
+ result = call_api(
458
+ method="GET",
459
+ path="/api/v1/governance/metrics/overview",
460
+ profile=profile,
461
+ require_scope=False,
462
+ )
463
+ except CLIRequestError as exc:
464
+ typer.echo(str(exc), err=True)
465
+ raise typer.Exit(1) from exc
466
+ print_json(result)
467
+
468
+
469
+ @app.command("project-metrics")
470
+ def project_metrics(
471
+ project_id: str = typer.Argument(..., help="Project ID"),
472
+ since: str = typer.Option("24", "--since", help="Hours or ISO timestamp"),
473
+ limit: int = typer.Option(500, "--limit", min=1, max=5000),
474
+ profile: str | None = typer.Option(None, "--profile"),
475
+ ) -> None:
476
+ """Fetch governance metrics for one project."""
477
+ params: dict[str, str] = {"since": since}
478
+ if limit != 500:
479
+ params["limit"] = str(limit)
480
+ try:
481
+ result = call_api(
482
+ method="GET",
483
+ path=_gov_path("metrics", "projects", project_id),
484
+ profile=profile,
485
+ require_scope=False,
486
+ params=params,
487
+ )
488
+ except CLIRequestError as exc:
489
+ typer.echo(str(exc), err=True)
490
+ raise typer.Exit(1) from exc
491
+ print_json(result)