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.
- apps/cli/README.md +147 -0
- apps/cli/__init__.py +0 -0
- apps/cli/src/__init__.py +3 -0
- apps/cli/src/commands/__init__.py +1 -0
- apps/cli/src/commands/admin.py +160 -0
- apps/cli/src/commands/api.py +48 -0
- apps/cli/src/commands/audit.py +181 -0
- apps/cli/src/commands/auth_product.py +187 -0
- apps/cli/src/commands/billing.py +513 -0
- apps/cli/src/commands/chrono.py +248 -0
- apps/cli/src/commands/context.py +99 -0
- apps/cli/src/commands/data.py +272 -0
- apps/cli/src/commands/db.py +528 -0
- apps/cli/src/commands/deploy.py +1099 -0
- apps/cli/src/commands/governance.py +491 -0
- apps/cli/src/commands/iam.py +436 -0
- apps/cli/src/commands/init_cmd.py +177 -0
- apps/cli/src/commands/keys.py +120 -0
- apps/cli/src/commands/login.py +842 -0
- apps/cli/src/commands/org.py +157 -0
- apps/cli/src/commands/policy.py +317 -0
- apps/cli/src/commands/project.py +130 -0
- apps/cli/src/commands/realtime.py +364 -0
- apps/cli/src/commands/scope.py +42 -0
- apps/cli/src/commands/shell.py +415 -0
- apps/cli/src/commands/sim.py +178 -0
- apps/cli/src/commands/storage.py +405 -0
- apps/cli/src/commands/vector.py +204 -0
- apps/cli/src/main.py +169 -0
- apps/cli/src/utils/__init__.py +1 -0
- apps/cli/src/utils/api_client.py +329 -0
- apps/cli/src/utils/auth_store.py +299 -0
- apps/cli/src/utils/config.py +185 -0
- apps/cli/src/utils/display.py +62 -0
- apps/cli/src/utils/migrations.py +69 -0
- apps/cli/src/utils/project_context.py +124 -0
- apps/cli/tests/test_auth_store_profiles.py +103 -0
- apps/cli/tests/test_cli_security.py +110 -0
- apps/cli/tests/test_cli_smoke_paths.py +279 -0
- apps/cli/tests/test_config_profiles.py +55 -0
- apps/cli/tests/test_main_startup.py +29 -0
- apps/cli/tests/test_project_and_commands.py +196 -0
- sovaria_cli-0.2.0.dist-info/METADATA +170 -0
- sovaria_cli-0.2.0.dist-info/RECORD +46 -0
- sovaria_cli-0.2.0.dist-info/WHEEL +4 -0
- sovaria_cli-0.2.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
"""Interactive Sovaria shell REPL (`sov shell`)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import cmd
|
|
6
|
+
import shlex
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from apps.cli.src.utils.api_client import CLIRequestError, call_api
|
|
12
|
+
from apps.cli.src.utils.auth_store import load_tokens
|
|
13
|
+
from apps.cli.src.utils.display import print_banner, print_error, print_json, print_sql_result, print_success
|
|
14
|
+
from apps.cli.src.utils.project_context import project_status
|
|
15
|
+
|
|
16
|
+
app = typer.Typer(help="Interactive Sovaria terminal", invoke_without_command=True)
|
|
17
|
+
|
|
18
|
+
_SHELL_COMMANDS = (
|
|
19
|
+
"help",
|
|
20
|
+
"whoami",
|
|
21
|
+
"status",
|
|
22
|
+
"scope",
|
|
23
|
+
"sql",
|
|
24
|
+
"sim",
|
|
25
|
+
"keys",
|
|
26
|
+
"policy",
|
|
27
|
+
"vector",
|
|
28
|
+
"billing",
|
|
29
|
+
"fn",
|
|
30
|
+
"db",
|
|
31
|
+
"sessions",
|
|
32
|
+
"api",
|
|
33
|
+
"exit",
|
|
34
|
+
"quit",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
_HELP_LINES = """
|
|
38
|
+
Commands:
|
|
39
|
+
help Show this help
|
|
40
|
+
whoami Current authenticated user
|
|
41
|
+
status Local project link + scope
|
|
42
|
+
scope Show resolved org/project/env
|
|
43
|
+
sql <query> Run SQL (Atlas-Rel)
|
|
44
|
+
sim discover | runs | run <id>
|
|
45
|
+
keys list List project API keys
|
|
46
|
+
policy list | check --action A --resource R
|
|
47
|
+
vector collections List vector collections
|
|
48
|
+
billing usage Current billing usage
|
|
49
|
+
fn list List Forge functions
|
|
50
|
+
fn logs <name> Recent function logs
|
|
51
|
+
db migrations list --local Local migration files
|
|
52
|
+
sessions List your login sessions
|
|
53
|
+
api GET /path Raw gateway call
|
|
54
|
+
exit | quit Leave the shell
|
|
55
|
+
|
|
56
|
+
Tips:
|
|
57
|
+
• Tab completes command names; ↑/↓ history when readline is available
|
|
58
|
+
• Scope auto-resolves from sovaria.config.json — run `sov init` first
|
|
59
|
+
• Multi-word SQL: sql "SELECT * FROM users LIMIT 5"
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _setup_readline() -> None:
|
|
64
|
+
"""Enable tab completion and persistent history when readline exists."""
|
|
65
|
+
try:
|
|
66
|
+
import readline
|
|
67
|
+
except ImportError:
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
def _completer(text: str, state: int) -> str | None:
|
|
71
|
+
options = [cmd_name for cmd_name in _SHELL_COMMANDS if cmd_name.startswith(text)]
|
|
72
|
+
if state < len(options):
|
|
73
|
+
return options[state]
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
readline.set_completer(_completer)
|
|
77
|
+
readline.parse_and_bind("tab: complete")
|
|
78
|
+
history_path = Path.home() / ".sovaria" / "shell_history"
|
|
79
|
+
history_path.parent.mkdir(parents=True, exist_ok=True)
|
|
80
|
+
try:
|
|
81
|
+
readline.read_history_file(str(history_path))
|
|
82
|
+
except OSError:
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
import atexit
|
|
86
|
+
|
|
87
|
+
def _save_history() -> None:
|
|
88
|
+
try:
|
|
89
|
+
readline.write_history_file(str(history_path))
|
|
90
|
+
except OSError:
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
atexit.register(_save_history)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class SovariaShell(cmd.Cmd):
|
|
97
|
+
intro = ""
|
|
98
|
+
prompt = "sov> "
|
|
99
|
+
doc_header = "Sovaria interactive commands"
|
|
100
|
+
|
|
101
|
+
def __init__(self, *, profile: str | None = None) -> None:
|
|
102
|
+
super().__init__()
|
|
103
|
+
self.profile = profile
|
|
104
|
+
|
|
105
|
+
def precmd(self, line: str) -> str:
|
|
106
|
+
return line.strip()
|
|
107
|
+
|
|
108
|
+
def do_help(self, arg: str) -> None:
|
|
109
|
+
if arg:
|
|
110
|
+
super().do_help(arg)
|
|
111
|
+
return
|
|
112
|
+
typer.echo(_HELP_LINES)
|
|
113
|
+
|
|
114
|
+
def do_exit(self, arg: str) -> bool:
|
|
115
|
+
typer.echo("Bye.")
|
|
116
|
+
return True
|
|
117
|
+
|
|
118
|
+
def do_quit(self, arg: str) -> bool:
|
|
119
|
+
return self.do_exit(arg)
|
|
120
|
+
|
|
121
|
+
def do_whoami(self, arg: str) -> None:
|
|
122
|
+
try:
|
|
123
|
+
result = call_api(method="GET", path="/api/v1/auth/me", profile=self.profile)
|
|
124
|
+
print_json(result)
|
|
125
|
+
except CLIRequestError as exc:
|
|
126
|
+
print_error(str(exc))
|
|
127
|
+
|
|
128
|
+
def do_status(self, arg: str) -> None:
|
|
129
|
+
print_json(project_status())
|
|
130
|
+
|
|
131
|
+
def do_scope(self, arg: str) -> None:
|
|
132
|
+
status = project_status()
|
|
133
|
+
scope = status.get("scope") if isinstance(status, dict) else {}
|
|
134
|
+
if isinstance(scope, dict):
|
|
135
|
+
typer.echo(f"org_id : {scope.get('org_id') or '-'}")
|
|
136
|
+
typer.echo(f"project_id : {scope.get('project_id') or '-'}")
|
|
137
|
+
typer.echo(f"env_id : {scope.get('env_id') or '-'}")
|
|
138
|
+
else:
|
|
139
|
+
print_json(status)
|
|
140
|
+
|
|
141
|
+
def do_sql(self, arg: str) -> None:
|
|
142
|
+
if not arg:
|
|
143
|
+
typer.echo("Usage: sql SELECT ...")
|
|
144
|
+
return
|
|
145
|
+
try:
|
|
146
|
+
result = call_api(
|
|
147
|
+
method="POST",
|
|
148
|
+
path="/api/v1/sql/query",
|
|
149
|
+
profile=self.profile,
|
|
150
|
+
body={"sql": arg, "txid": 0},
|
|
151
|
+
require_scope=True,
|
|
152
|
+
)
|
|
153
|
+
print_sql_result(result)
|
|
154
|
+
except CLIRequestError as exc:
|
|
155
|
+
print_error(str(exc))
|
|
156
|
+
|
|
157
|
+
def do_sim(self, arg: str) -> None:
|
|
158
|
+
parts = shlex.split(arg)
|
|
159
|
+
if not parts:
|
|
160
|
+
typer.echo("Usage: sim discover | runs | run <scenario_id>")
|
|
161
|
+
return
|
|
162
|
+
sub = parts[0]
|
|
163
|
+
try:
|
|
164
|
+
if sub == "discover":
|
|
165
|
+
print_json(
|
|
166
|
+
call_api(
|
|
167
|
+
method="POST",
|
|
168
|
+
path="/api/v1/sim/discover",
|
|
169
|
+
profile=self.profile,
|
|
170
|
+
require_scope=True,
|
|
171
|
+
body={},
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
elif sub == "runs":
|
|
175
|
+
limit = "20"
|
|
176
|
+
if len(parts) >= 3 and parts[1] == "--limit":
|
|
177
|
+
limit = parts[2]
|
|
178
|
+
print_json(
|
|
179
|
+
call_api(
|
|
180
|
+
method="GET",
|
|
181
|
+
path="/api/v1/sim/runs",
|
|
182
|
+
profile=self.profile,
|
|
183
|
+
require_scope=True,
|
|
184
|
+
params={"limit": limit, "offset": "0"},
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
elif sub == "run" and len(parts) >= 2:
|
|
188
|
+
print_json(
|
|
189
|
+
call_api(
|
|
190
|
+
method="POST",
|
|
191
|
+
path="/api/v1/sim/run",
|
|
192
|
+
profile=self.profile,
|
|
193
|
+
require_scope=True,
|
|
194
|
+
body={"scenario_id": parts[1]},
|
|
195
|
+
)
|
|
196
|
+
)
|
|
197
|
+
else:
|
|
198
|
+
typer.echo("Usage: sim discover | runs [--limit N] | run <scenario_id>")
|
|
199
|
+
except CLIRequestError as exc:
|
|
200
|
+
print_error(str(exc))
|
|
201
|
+
|
|
202
|
+
def do_keys(self, arg: str) -> None:
|
|
203
|
+
parts = shlex.split(arg)
|
|
204
|
+
if parts and parts[0] != "list":
|
|
205
|
+
typer.echo("Usage: keys list")
|
|
206
|
+
return
|
|
207
|
+
try:
|
|
208
|
+
print_json(
|
|
209
|
+
call_api(
|
|
210
|
+
method="GET",
|
|
211
|
+
path="/api/v1/apikeys",
|
|
212
|
+
profile=self.profile,
|
|
213
|
+
require_scope=True,
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
except CLIRequestError as exc:
|
|
217
|
+
print_error(str(exc))
|
|
218
|
+
|
|
219
|
+
def do_policy(self, arg: str) -> None:
|
|
220
|
+
parts = shlex.split(arg)
|
|
221
|
+
if not parts:
|
|
222
|
+
typer.echo("Usage: policy list | policy check --action A --resource R")
|
|
223
|
+
return
|
|
224
|
+
try:
|
|
225
|
+
if parts[0] == "list":
|
|
226
|
+
print_json(
|
|
227
|
+
call_api(
|
|
228
|
+
method="GET",
|
|
229
|
+
path="/api/v1/policies/",
|
|
230
|
+
profile=self.profile,
|
|
231
|
+
require_scope=True,
|
|
232
|
+
)
|
|
233
|
+
)
|
|
234
|
+
elif parts[0] == "check":
|
|
235
|
+
action = ""
|
|
236
|
+
resource = ""
|
|
237
|
+
idx = 1
|
|
238
|
+
while idx < len(parts):
|
|
239
|
+
if parts[idx] == "--action" and idx + 1 < len(parts):
|
|
240
|
+
action = parts[idx + 1]
|
|
241
|
+
idx += 2
|
|
242
|
+
elif parts[idx] == "--resource" and idx + 1 < len(parts):
|
|
243
|
+
resource = parts[idx + 1]
|
|
244
|
+
idx += 2
|
|
245
|
+
else:
|
|
246
|
+
idx += 1
|
|
247
|
+
if not action or not resource:
|
|
248
|
+
typer.echo("Usage: policy check --action sql:select --resource users")
|
|
249
|
+
return
|
|
250
|
+
status = project_status()
|
|
251
|
+
scope = status.get("scope") if isinstance(status, dict) else {}
|
|
252
|
+
org_id = str((scope or {}).get("org_id") or "")
|
|
253
|
+
project_id = str((scope or {}).get("project_id") or "")
|
|
254
|
+
print_json(
|
|
255
|
+
call_api(
|
|
256
|
+
method="POST",
|
|
257
|
+
path="/api/v1/policies/check",
|
|
258
|
+
profile=self.profile,
|
|
259
|
+
body={
|
|
260
|
+
"org_id": org_id,
|
|
261
|
+
"project_id": project_id,
|
|
262
|
+
"action": action,
|
|
263
|
+
"resource": resource,
|
|
264
|
+
"context": {},
|
|
265
|
+
},
|
|
266
|
+
require_scope=True,
|
|
267
|
+
)
|
|
268
|
+
)
|
|
269
|
+
else:
|
|
270
|
+
typer.echo("Usage: policy list | policy check --action A --resource R")
|
|
271
|
+
except CLIRequestError as exc:
|
|
272
|
+
print_error(str(exc))
|
|
273
|
+
|
|
274
|
+
def do_vector(self, arg: str) -> None:
|
|
275
|
+
parts = shlex.split(arg)
|
|
276
|
+
if not parts or parts[0] != "collections":
|
|
277
|
+
typer.echo("Usage: vector collections")
|
|
278
|
+
return
|
|
279
|
+
try:
|
|
280
|
+
print_json(
|
|
281
|
+
call_api(
|
|
282
|
+
method="GET",
|
|
283
|
+
path="/api/v1/vector/collections",
|
|
284
|
+
profile=self.profile,
|
|
285
|
+
require_scope=True,
|
|
286
|
+
)
|
|
287
|
+
)
|
|
288
|
+
except CLIRequestError as exc:
|
|
289
|
+
print_error(str(exc))
|
|
290
|
+
|
|
291
|
+
def do_billing(self, arg: str) -> None:
|
|
292
|
+
parts = shlex.split(arg)
|
|
293
|
+
if not parts or parts[0] != "usage":
|
|
294
|
+
typer.echo("Usage: billing usage")
|
|
295
|
+
return
|
|
296
|
+
try:
|
|
297
|
+
print_json(
|
|
298
|
+
call_api(
|
|
299
|
+
method="GET",
|
|
300
|
+
path="/api/v1/billing/usage",
|
|
301
|
+
profile=self.profile,
|
|
302
|
+
require_scope=True,
|
|
303
|
+
)
|
|
304
|
+
)
|
|
305
|
+
except CLIRequestError as exc:
|
|
306
|
+
print_error(str(exc))
|
|
307
|
+
|
|
308
|
+
def do_fn(self, arg: str) -> None:
|
|
309
|
+
parts = shlex.split(arg)
|
|
310
|
+
if not parts:
|
|
311
|
+
typer.echo("Usage: fn list | fn logs <name>")
|
|
312
|
+
return
|
|
313
|
+
try:
|
|
314
|
+
if parts[0] == "list":
|
|
315
|
+
print_json(
|
|
316
|
+
call_api(
|
|
317
|
+
method="GET",
|
|
318
|
+
path="/api/v1/fn/functions",
|
|
319
|
+
profile=self.profile,
|
|
320
|
+
require_scope=True,
|
|
321
|
+
params={"limit": "50", "offset": "0"},
|
|
322
|
+
)
|
|
323
|
+
)
|
|
324
|
+
elif parts[0] == "logs" and len(parts) >= 2:
|
|
325
|
+
from urllib.parse import quote
|
|
326
|
+
|
|
327
|
+
name = quote(parts[1], safe="")
|
|
328
|
+
print_json(
|
|
329
|
+
call_api(
|
|
330
|
+
method="GET",
|
|
331
|
+
path=f"/api/v1/fn/functions/{name}/logs",
|
|
332
|
+
profile=self.profile,
|
|
333
|
+
require_scope=True,
|
|
334
|
+
params={"limit": "20", "offset": "0"},
|
|
335
|
+
)
|
|
336
|
+
)
|
|
337
|
+
else:
|
|
338
|
+
typer.echo("Usage: fn list | fn logs <name>")
|
|
339
|
+
except CLIRequestError as exc:
|
|
340
|
+
print_error(str(exc))
|
|
341
|
+
|
|
342
|
+
def do_db(self, arg: str) -> None:
|
|
343
|
+
parts = shlex.split(arg)
|
|
344
|
+
if len(parts) >= 3 and parts[0] == "migrations" and parts[1] == "list" and parts[2] == "--local":
|
|
345
|
+
from apps.cli.src.utils.migrations import list_local_migrations, load_applied_state
|
|
346
|
+
|
|
347
|
+
applied = load_applied_state()
|
|
348
|
+
for path in list_local_migrations():
|
|
349
|
+
marker = "✓" if path.name in applied else " "
|
|
350
|
+
typer.echo(f" [{marker}] {path.name}")
|
|
351
|
+
return
|
|
352
|
+
typer.echo("Usage: db migrations list --local")
|
|
353
|
+
|
|
354
|
+
def do_sessions(self, arg: str) -> None:
|
|
355
|
+
if arg.strip():
|
|
356
|
+
typer.echo("Usage: sessions")
|
|
357
|
+
return
|
|
358
|
+
try:
|
|
359
|
+
print_json(call_api(method="GET", path="/api/v1/auth/sessions", profile=self.profile))
|
|
360
|
+
except CLIRequestError as exc:
|
|
361
|
+
print_error(str(exc))
|
|
362
|
+
|
|
363
|
+
def do_api(self, arg: str) -> None:
|
|
364
|
+
parts = shlex.split(arg)
|
|
365
|
+
if len(parts) < 2:
|
|
366
|
+
typer.echo("Usage: api GET /api/v1/...")
|
|
367
|
+
return
|
|
368
|
+
method, path = parts[0].upper(), parts[1]
|
|
369
|
+
try:
|
|
370
|
+
print_json(
|
|
371
|
+
call_api(
|
|
372
|
+
method=method,
|
|
373
|
+
path=path,
|
|
374
|
+
profile=self.profile,
|
|
375
|
+
require_scope=True,
|
|
376
|
+
)
|
|
377
|
+
)
|
|
378
|
+
except CLIRequestError as exc:
|
|
379
|
+
print_error(str(exc))
|
|
380
|
+
|
|
381
|
+
def default(self, line: str) -> None:
|
|
382
|
+
if line.startswith("SELECT") or line.startswith("select") or line.startswith("WITH"):
|
|
383
|
+
self.do_sql(line)
|
|
384
|
+
return
|
|
385
|
+
typer.echo(f"Unknown command: {line.split()[0]}. Type `help`.")
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
@app.callback()
|
|
389
|
+
def shell(
|
|
390
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
391
|
+
) -> None:
|
|
392
|
+
"""Launch an interactive Sovaria REPL with SQL, sim, keys, policy, and more."""
|
|
393
|
+
if not load_tokens(profile=profile):
|
|
394
|
+
print_error("Not logged in. Run: sov login")
|
|
395
|
+
raise typer.Exit(1)
|
|
396
|
+
|
|
397
|
+
_setup_readline()
|
|
398
|
+
|
|
399
|
+
status = project_status()
|
|
400
|
+
name = status.get("project_name") if isinstance(status, dict) else None
|
|
401
|
+
print_banner("Sovaria Shell", name or "Interactive project terminal")
|
|
402
|
+
if isinstance(status, dict) and status.get("linked"):
|
|
403
|
+
scope = status.get("scope") or {}
|
|
404
|
+
typer.echo(
|
|
405
|
+
f"Linked: {scope.get('org_id', '?')} / {scope.get('project_id', '?')}"
|
|
406
|
+
)
|
|
407
|
+
print_success("Scope resolved from sovaria.config.json")
|
|
408
|
+
else:
|
|
409
|
+
typer.echo("No local project link. Run `sov init` or `sov project link`.")
|
|
410
|
+
typer.echo("Type `help` for commands.\n")
|
|
411
|
+
|
|
412
|
+
try:
|
|
413
|
+
SovariaShell(profile=profile).cmdloop()
|
|
414
|
+
except KeyboardInterrupt:
|
|
415
|
+
typer.echo("\nBye.")
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Phase 2 simulation commands (Echo engine)."""
|
|
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="Scenario discovery, runs, and comparisons")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@app.command("discover")
|
|
16
|
+
def discover(
|
|
17
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
18
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
19
|
+
project_id: str | None = typer.Option(None, "--project"),
|
|
20
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
21
|
+
) -> None:
|
|
22
|
+
"""Discover runnable simulation scenarios for the linked project."""
|
|
23
|
+
try:
|
|
24
|
+
result = call_api(
|
|
25
|
+
method="POST",
|
|
26
|
+
path="/api/v1/sim/discover",
|
|
27
|
+
profile=profile,
|
|
28
|
+
org_id=org_id,
|
|
29
|
+
project_id=project_id,
|
|
30
|
+
env_id=env_id,
|
|
31
|
+
require_scope=True,
|
|
32
|
+
body={},
|
|
33
|
+
)
|
|
34
|
+
except CLIRequestError as exc:
|
|
35
|
+
typer.echo(str(exc), err=True)
|
|
36
|
+
raise typer.Exit(1) from exc
|
|
37
|
+
print_json(result)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@app.command("runs")
|
|
41
|
+
def list_runs(
|
|
42
|
+
limit: int = typer.Option(20, "--limit"),
|
|
43
|
+
offset: int = typer.Option(0, "--offset"),
|
|
44
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
45
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
46
|
+
project_id: str | None = typer.Option(None, "--project"),
|
|
47
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
48
|
+
) -> None:
|
|
49
|
+
"""List simulation runs for the current project."""
|
|
50
|
+
try:
|
|
51
|
+
result = call_api(
|
|
52
|
+
method="GET",
|
|
53
|
+
path="/api/v1/sim/runs",
|
|
54
|
+
profile=profile,
|
|
55
|
+
org_id=org_id,
|
|
56
|
+
project_id=project_id,
|
|
57
|
+
env_id=env_id,
|
|
58
|
+
require_scope=True,
|
|
59
|
+
params={"limit": str(limit), "offset": str(offset)},
|
|
60
|
+
)
|
|
61
|
+
except CLIRequestError as exc:
|
|
62
|
+
typer.echo(str(exc), err=True)
|
|
63
|
+
raise typer.Exit(1) from exc
|
|
64
|
+
print_json(result)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@app.command("get")
|
|
68
|
+
def get_run(
|
|
69
|
+
run_id: str = typer.Argument(..., help="Simulation run ID"),
|
|
70
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
71
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
72
|
+
project_id: str | None = typer.Option(None, "--project"),
|
|
73
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
74
|
+
) -> None:
|
|
75
|
+
"""Fetch one simulation run by ID."""
|
|
76
|
+
try:
|
|
77
|
+
result = call_api(
|
|
78
|
+
method="GET",
|
|
79
|
+
path=f"/api/v1/sim/runs/{quote(run_id, safe='')}",
|
|
80
|
+
profile=profile,
|
|
81
|
+
org_id=org_id,
|
|
82
|
+
project_id=project_id,
|
|
83
|
+
env_id=env_id,
|
|
84
|
+
require_scope=True,
|
|
85
|
+
)
|
|
86
|
+
except CLIRequestError as exc:
|
|
87
|
+
typer.echo(str(exc), err=True)
|
|
88
|
+
raise typer.Exit(1) from exc
|
|
89
|
+
print_json(result)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@app.command("compare")
|
|
93
|
+
def compare_runs(
|
|
94
|
+
target_run_id: str = typer.Argument(..., help="Run to inspect"),
|
|
95
|
+
baseline_run_id: str = typer.Argument(..., help="Baseline run"),
|
|
96
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
97
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
98
|
+
project_id: str | None = typer.Option(None, "--project"),
|
|
99
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
100
|
+
) -> None:
|
|
101
|
+
"""Compare two simulation runs."""
|
|
102
|
+
try:
|
|
103
|
+
result = call_api(
|
|
104
|
+
method="GET",
|
|
105
|
+
path=(
|
|
106
|
+
f"/api/v1/sim/runs/{quote(target_run_id, safe='')}/compare/"
|
|
107
|
+
f"{quote(baseline_run_id, safe='')}"
|
|
108
|
+
),
|
|
109
|
+
profile=profile,
|
|
110
|
+
org_id=org_id,
|
|
111
|
+
project_id=project_id,
|
|
112
|
+
env_id=env_id,
|
|
113
|
+
require_scope=True,
|
|
114
|
+
)
|
|
115
|
+
except CLIRequestError as exc:
|
|
116
|
+
typer.echo(str(exc), err=True)
|
|
117
|
+
raise typer.Exit(1) from exc
|
|
118
|
+
print_json(result)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@app.command("run")
|
|
122
|
+
def run_scenario(
|
|
123
|
+
scenario_id: str = typer.Argument(..., help="Scenario ID from `sov sim discover`"),
|
|
124
|
+
payload: str | None = typer.Option(None, "--payload", help="JSON object payload"),
|
|
125
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
126
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
127
|
+
project_id: str | None = typer.Option(None, "--project"),
|
|
128
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
129
|
+
) -> None:
|
|
130
|
+
"""Execute a discovered simulation scenario."""
|
|
131
|
+
body = parse_json_arg(payload, label="--payload") if payload else {}
|
|
132
|
+
if body is not None and not isinstance(body, dict):
|
|
133
|
+
raise typer.BadParameter("--payload must be a JSON object")
|
|
134
|
+
try:
|
|
135
|
+
result = call_api(
|
|
136
|
+
method="POST",
|
|
137
|
+
path="/api/v1/sim/run",
|
|
138
|
+
profile=profile,
|
|
139
|
+
org_id=org_id,
|
|
140
|
+
project_id=project_id,
|
|
141
|
+
env_id=env_id,
|
|
142
|
+
require_scope=True,
|
|
143
|
+
body={"scenario_id": scenario_id, **(body or {})},
|
|
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("plan")
|
|
152
|
+
def plan_scenario(
|
|
153
|
+
scenario_id: str = typer.Argument(..., help="Scenario ID"),
|
|
154
|
+
payload: str | None = typer.Option(None, "--payload", help="JSON object payload"),
|
|
155
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
156
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
157
|
+
project_id: str | None = typer.Option(None, "--project"),
|
|
158
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
159
|
+
) -> None:
|
|
160
|
+
"""Dry-run a scenario and return the execution plan."""
|
|
161
|
+
body = parse_json_arg(payload, label="--payload") if payload else {}
|
|
162
|
+
if body is not None and not isinstance(body, dict):
|
|
163
|
+
raise typer.BadParameter("--payload must be a JSON object")
|
|
164
|
+
try:
|
|
165
|
+
result = call_api(
|
|
166
|
+
method="POST",
|
|
167
|
+
path="/api/v1/sim/plan",
|
|
168
|
+
profile=profile,
|
|
169
|
+
org_id=org_id,
|
|
170
|
+
project_id=project_id,
|
|
171
|
+
env_id=env_id,
|
|
172
|
+
require_scope=True,
|
|
173
|
+
body={"scenario_id": scenario_id, **(body or {})},
|
|
174
|
+
)
|
|
175
|
+
except CLIRequestError as exc:
|
|
176
|
+
typer.echo(str(exc), err=True)
|
|
177
|
+
raise typer.Exit(1) from exc
|
|
178
|
+
print_json(result)
|