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
apps/cli/README.md ADDED
@@ -0,0 +1,147 @@
1
+ # Sovaria CLI
2
+
3
+ Operator CLI for gateway-backed Sovaria project and platform workflows.
4
+
5
+ ## Run
6
+
7
+ Examples below use `sov ...` command syntax.
8
+
9
+ If `sov` is installed on your PATH:
10
+
11
+ ```bash
12
+ sov --help
13
+ ```
14
+
15
+ From repo root without installation:
16
+
17
+ ```bash
18
+ python -m apps.cli.src.main --help
19
+ ```
20
+
21
+ ## Cross-SDK Happy Path Smoke
22
+
23
+ Use the repository smoke script to validate one shared happy path across:
24
+
25
+ - CLI auth + scoped SQL call
26
+ - TypeScript SDK SQL + engine health
27
+ - Python SDK SQL + engine health
28
+
29
+ ```bash
30
+ SOVARIA_URL=http://localhost:8000 \
31
+ SOVARIA_ORG_ID=<org_uuid> \
32
+ SOVARIA_PROJECT_ID=<project_uuid> \
33
+ SOVARIA_EMAIL=<owner_email> \
34
+ SOVARIA_PASSWORD='<owner_password>' \
35
+ scripts/cli_sdk_happy_path.sh
36
+ ```
37
+
38
+ If you already have valid CLI tokens, omit `SOVARIA_EMAIL` and `SOVARIA_PASSWORD`.
39
+
40
+ ## Core Command Groups
41
+
42
+ - `sov auth ...` auth login/logout
43
+ - `sov scope ...` default org/project/env scope
44
+ - `sov data ...` Atlas-Doc and Atlas-Rel
45
+ - `sov realtime ...` Pulse realtime and e2ee
46
+ - `sov policy ...` Guard policy management
47
+ - `sov audit ...` Echo audit and simulation
48
+ - `sov chrono ...` Chrono timeline/snapshot operations
49
+ - `sov fn ...` Forge function deploy/status/invoke
50
+ - `sov project ...` project config template + scaffold creation
51
+ - `sov api call ...` raw gateway API request
52
+
53
+ ## Project Bootstrap (Config JSON)
54
+
55
+ Create a template config file:
56
+
57
+ ```bash
58
+ sov project init-config my-app --output sovaria.config.json
59
+ ```
60
+
61
+ Then scaffold a project from that config:
62
+
63
+ ```bash
64
+ sov project create --config sovaria.config.json --dir .
65
+ ```
66
+
67
+ ## Function Sandbox Flow
68
+
69
+ Deploy a function bundle (zipped from local folder):
70
+
71
+ ```bash
72
+ sov fn deploy ./my-function --name hello --project <project_id> --note "add audit logging"
73
+ ```
74
+
75
+ Poll deployment status:
76
+
77
+ ```bash
78
+ sov fn status <deploy_id> --project <project_id>
79
+ ```
80
+
81
+ Invoke the deployed function in its project sandbox:
82
+
83
+ ```bash
84
+ sov fn invoke hello --project <project_id> --data '{"message":"hi"}'
85
+ ```
86
+
87
+ Run a simulation invocation (tracked separately from normal invoke):
88
+
89
+ ```bash
90
+ sov fn invoke hello --project <project_id> --simulate --data '{"message":"dry-run"}'
91
+ ```
92
+
93
+ Queue heavy async invocation and wait for completion:
94
+
95
+ ```bash
96
+ sov fn invoke hello --project <project_id> --execution-profile heavy --async --wait --data-file payload.json
97
+ ```
98
+
99
+ List function versions:
100
+
101
+ ```bash
102
+ sov fn versions hello --project <project_id>
103
+ ```
104
+
105
+ List functions with active versions + queue depth:
106
+
107
+ ```bash
108
+ sov fn functions --project <project_id>
109
+ ```
110
+
111
+ List invocation history:
112
+
113
+ ```bash
114
+ sov fn history hello --project <project_id> --mode invoke
115
+ ```
116
+
117
+ List durable async jobs:
118
+
119
+ ```bash
120
+ sov fn jobs hello --project <project_id> --status QUEUED
121
+ ```
122
+
123
+ Promote a specific function version:
124
+
125
+ ```bash
126
+ sov fn promote hello --project <project_id> --version 3 --reason "promote stable release"
127
+ ```
128
+
129
+ Rollback to previous active version:
130
+
131
+ ```bash
132
+ sov fn rollback hello --project <project_id> --reason "rollback regression"
133
+ ```
134
+
135
+ Diff two simulation invocations:
136
+
137
+ ```bash
138
+ sov fn sim-diff hello <left_invocation_id> <right_invocation_id> --project <project_id>
139
+ ```
140
+
141
+ ## Scope Requirement
142
+
143
+ Realtime and relational routes require org/project scope. You can pass scope per command (`--org --project --env`) or set defaults:
144
+
145
+ ```bash
146
+ sov scope set <org_id> <project_id> --env <env_id>
147
+ ```
apps/cli/__init__.py ADDED
File without changes
@@ -0,0 +1,3 @@
1
+ """Sovaria CLI — sov command."""
2
+
3
+ __version__ = "0.2.0"
@@ -0,0 +1 @@
1
+ """CLI commands package."""
@@ -0,0 +1,160 @@
1
+ """Platform admin commands (error traces, usage overview, transparency)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from urllib.parse import quote, urlencode
6
+
7
+ import typer
8
+
9
+ from apps.cli.src.utils.api_client import CLIRequestError, call_api
10
+ from apps.cli.src.utils.display import print_json
11
+
12
+ app = typer.Typer(help="Platform admin: error traces, usage, transparency")
13
+
14
+ error_traces_app = typer.Typer(help="Error trace inspection and resolution")
15
+ transparency_app = typer.Typer(help="Access transparency audit log")
16
+
17
+ app.add_typer(error_traces_app, name="error-traces")
18
+ app.add_typer(transparency_app, name="transparency")
19
+
20
+
21
+ @error_traces_app.command("list")
22
+ def error_traces_list(
23
+ service: str | None = typer.Option(None, "--service"),
24
+ project_id: str | None = typer.Option(None, "--project"),
25
+ error_class: str | None = typer.Option(None, "--error-class"),
26
+ severity: str | None = typer.Option(None, "--severity"),
27
+ resolved: bool | None = typer.Option(None, "--resolved/--unresolved"),
28
+ since: str | None = typer.Option(None, "--since", help="ISO-8601 lower bound"),
29
+ limit: int = typer.Option(50, "--limit", min=1, max=200),
30
+ profile: str | None = typer.Option(None, "--profile"),
31
+ ) -> None:
32
+ """List recent platform error traces."""
33
+ params: dict[str, str] = {"limit": str(limit)}
34
+ if service:
35
+ params["service"] = service
36
+ if project_id:
37
+ params["project_id"] = project_id
38
+ if error_class:
39
+ params["error_class"] = error_class
40
+ if severity:
41
+ params["severity"] = severity
42
+ if resolved is not None:
43
+ params["resolved"] = str(resolved).lower()
44
+ if since:
45
+ params["since"] = since
46
+ try:
47
+ result = call_api(
48
+ method="GET",
49
+ path="/api/v1/error-traces",
50
+ profile=profile,
51
+ require_scope=False,
52
+ params=params,
53
+ )
54
+ except CLIRequestError as exc:
55
+ typer.echo(str(exc), err=True)
56
+ raise typer.Exit(1) from exc
57
+ print_json(result)
58
+
59
+
60
+ @error_traces_app.command("get")
61
+ def error_traces_get(
62
+ trace_id: str = typer.Argument(..., help="Error trace ID"),
63
+ profile: str | None = typer.Option(None, "--profile"),
64
+ ) -> None:
65
+ """Fetch one error trace with ordered hops."""
66
+ try:
67
+ result = call_api(
68
+ method="GET",
69
+ path=f"/api/v1/error-traces/{quote(trace_id, safe='')}",
70
+ profile=profile,
71
+ require_scope=False,
72
+ )
73
+ except CLIRequestError as exc:
74
+ typer.echo(str(exc), err=True)
75
+ raise typer.Exit(1) from exc
76
+ print_json(result)
77
+
78
+
79
+ @error_traces_app.command("resolve")
80
+ def error_traces_resolve(
81
+ trace_id: str = typer.Argument(..., help="Error trace ID"),
82
+ resolved: bool = typer.Option(True, "--resolved/--unresolved"),
83
+ profile: str | None = typer.Option(None, "--profile"),
84
+ ) -> None:
85
+ """Mark an error trace resolved or unresolved."""
86
+ try:
87
+ result = call_api(
88
+ method="PATCH",
89
+ path=f"/api/v1/error-traces/{quote(trace_id, safe='')}/resolved",
90
+ profile=profile,
91
+ require_scope=False,
92
+ body={"resolved": resolved},
93
+ )
94
+ except CLIRequestError as exc:
95
+ typer.echo(str(exc), err=True)
96
+ raise typer.Exit(1) from exc
97
+ print_json(result)
98
+
99
+
100
+ @app.command("usage-overview")
101
+ def usage_overview(
102
+ period: str | None = typer.Option(None, "--period", help="Billing period YYYY-MM"),
103
+ limit: int = typer.Option(100, "--limit", min=1, max=500),
104
+ profile: str | None = typer.Option(None, "--profile"),
105
+ ) -> None:
106
+ """Fetch cross-tenant usage overview (platform admin)."""
107
+ params: dict[str, str] = {}
108
+ if period:
109
+ params["period"] = period
110
+ if limit != 100:
111
+ params["limit"] = str(limit)
112
+ try:
113
+ result = call_api(
114
+ method="GET",
115
+ path="/api/v1/admin/usage/overview",
116
+ profile=profile,
117
+ require_scope=False,
118
+ params=params or None,
119
+ )
120
+ except CLIRequestError as exc:
121
+ typer.echo(str(exc), err=True)
122
+ raise typer.Exit(1) from exc
123
+ print_json(result)
124
+
125
+
126
+ @transparency_app.command("list")
127
+ def transparency_list(
128
+ org_id: str = typer.Option(..., "--org", help="Organisation ID"),
129
+ project_id: str | None = typer.Option(None, "--project"),
130
+ event_type: str | None = typer.Option(None, "--event-type"),
131
+ since: str | None = typer.Option(None, "--since", help="ISO-8601 lower bound"),
132
+ limit: int = typer.Option(50, "--limit", min=1, max=200),
133
+ offset: int = typer.Option(0, "--offset", min=0),
134
+ profile: str | None = typer.Option(None, "--profile"),
135
+ ) -> None:
136
+ """List tenant-visible staff access transparency events."""
137
+ query: dict[str, str] = {
138
+ "org_id": org_id,
139
+ "limit": str(limit),
140
+ "offset": str(offset),
141
+ }
142
+ if project_id:
143
+ query["project_id"] = project_id
144
+ if event_type:
145
+ query["event_type"] = event_type
146
+ if since:
147
+ query["since"] = since
148
+ path = f"/api/v1/access-transparency?{urlencode(query)}"
149
+ try:
150
+ result = call_api(
151
+ method="GET",
152
+ path=path,
153
+ profile=profile,
154
+ org_id=org_id,
155
+ require_scope=False,
156
+ )
157
+ except CLIRequestError as exc:
158
+ typer.echo(str(exc), err=True)
159
+ raise typer.Exit(1) from exc
160
+ print_json(result)
@@ -0,0 +1,48 @@
1
+ """Raw API passthrough commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+
7
+ from apps.cli.src.utils.api_client import CLIRequestError, call_api, parse_json_arg, print_json
8
+
9
+ app = typer.Typer(help="Raw gateway API calls")
10
+
11
+
12
+ @app.command("call")
13
+ def call_command(
14
+ method: str = typer.Argument(..., help="HTTP method, e.g. GET/POST"),
15
+ path: str = typer.Argument(..., help="Gateway path, e.g. /api/v1/system/engines/health"),
16
+ body: str | None = typer.Option(None, "--body", help="JSON request body"),
17
+ query: str | None = typer.Option(None, "--query", help="JSON object query params"),
18
+ profile: str | None = typer.Option(None, "--profile"),
19
+ no_auth: bool = typer.Option(False, "--no-auth", help="Do not send Authorization header"),
20
+ require_scope: bool = typer.Option(False, "--require-scope"),
21
+ org_id: str | None = typer.Option(None, "--org"),
22
+ project_id: str | None = typer.Option(None, "--project"),
23
+ env_id: str | None = typer.Option(None, "--env"),
24
+ ) -> None:
25
+ """Call any gateway endpoint directly."""
26
+ try:
27
+ parsed_body = parse_json_arg(body, label="--body")
28
+ parsed_query = parse_json_arg(query, label="--query")
29
+ if parsed_query is not None and not isinstance(parsed_query, dict):
30
+ raise CLIRequestError("--query must be a JSON object")
31
+
32
+ result = call_api(
33
+ method=method,
34
+ path=path,
35
+ profile=profile,
36
+ body=parsed_body,
37
+ params={str(k): str(v) for k, v in (parsed_query or {}).items()},
38
+ require_auth=not no_auth,
39
+ require_scope=require_scope,
40
+ org_id=org_id,
41
+ project_id=project_id,
42
+ env_id=env_id,
43
+ )
44
+ except CLIRequestError as exc:
45
+ typer.echo(str(exc), err=True)
46
+ raise typer.Exit(1) from exc
47
+
48
+ typer.echo(print_json(result))
@@ -0,0 +1,181 @@
1
+ """Echo audit and simulation commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+
7
+ from apps.cli.src.utils.api_client import CLIRequestError, call_api, parse_json_arg, print_json
8
+
9
+ app = typer.Typer(help="Echo audit trail and simulation operations")
10
+
11
+
12
+ @app.command("append")
13
+ def append_audit(
14
+ event_type: str = typer.Option(..., "--event-type"),
15
+ actor_id: str = typer.Option(..., "--actor-id"),
16
+ source: str = typer.Option(..., "--source"),
17
+ action: str = typer.Option(..., "--action"),
18
+ resource_id: str | None = typer.Option(None, "--resource-id"),
19
+ metadata: str | None = typer.Option(None, "--metadata", help="JSON object"),
20
+ ip_address: str | None = typer.Option(None, "--ip-address"),
21
+ profile: str | None = typer.Option(None, "--profile"),
22
+ ) -> None:
23
+ """Append an immutable audit entry."""
24
+ try:
25
+ parsed_metadata = parse_json_arg(metadata, label="--metadata") if metadata else {}
26
+ if parsed_metadata is not None and not isinstance(parsed_metadata, dict):
27
+ raise CLIRequestError("--metadata must be a JSON object")
28
+ result = call_api(
29
+ method="POST",
30
+ path="/api/v1/audit/",
31
+ profile=profile,
32
+ body={
33
+ "event_type": event_type,
34
+ "actor_id": actor_id,
35
+ "resource_id": resource_id,
36
+ "source": source,
37
+ "action": action,
38
+ "metadata": parsed_metadata or {},
39
+ "ip_address": ip_address,
40
+ },
41
+ )
42
+ except CLIRequestError as exc:
43
+ typer.echo(str(exc), err=True)
44
+ raise typer.Exit(1) from exc
45
+
46
+ typer.echo(print_json(result))
47
+
48
+
49
+ @app.command("list")
50
+ def list_audit(
51
+ actor_id: str | None = typer.Option(None, "--actor-id"),
52
+ event_type: str | None = typer.Option(None, "--event-type"),
53
+ source: str | None = typer.Option(None, "--source"),
54
+ resource_id: str | None = typer.Option(None, "--resource-id"),
55
+ limit: int = typer.Option(50, "--limit"),
56
+ offset: int = typer.Option(0, "--offset"),
57
+ profile: str | None = typer.Option(None, "--profile"),
58
+ ) -> None:
59
+ """Query audit log entries."""
60
+ params = {
61
+ "limit": str(limit),
62
+ "offset": str(offset),
63
+ }
64
+ if actor_id:
65
+ params["actor_id"] = actor_id
66
+ if event_type:
67
+ params["event_type"] = event_type
68
+ if source:
69
+ params["source"] = source
70
+ if resource_id:
71
+ params["resource_id"] = resource_id
72
+
73
+ try:
74
+ result = call_api(
75
+ method="GET",
76
+ path="/api/v1/audit/",
77
+ profile=profile,
78
+ params=params,
79
+ )
80
+ except CLIRequestError as exc:
81
+ typer.echo(str(exc), err=True)
82
+ raise typer.Exit(1) from exc
83
+
84
+ typer.echo(print_json(result))
85
+
86
+
87
+ @app.command("verify")
88
+ def verify_chain(profile: str | None = typer.Option(None, "--profile")) -> None:
89
+ """Verify audit hash-chain integrity."""
90
+ try:
91
+ result = call_api(method="GET", path="/api/v1/audit/verify", profile=profile)
92
+ except CLIRequestError as exc:
93
+ typer.echo(str(exc), err=True)
94
+ raise typer.Exit(1) from exc
95
+
96
+ typer.echo(print_json(result))
97
+
98
+
99
+ @app.command("count")
100
+ def audit_count(profile: str | None = typer.Option(None, "--profile")) -> None:
101
+ """Return audit log entry count."""
102
+ try:
103
+ result = call_api(method="GET", path="/api/v1/audit/count", profile=profile)
104
+ except CLIRequestError as exc:
105
+ typer.echo(str(exc), err=True)
106
+ raise typer.Exit(1) from exc
107
+
108
+ typer.echo(print_json(result))
109
+
110
+
111
+ @app.command("sim-run")
112
+ def sim_run(
113
+ simulation_name: str = typer.Argument(...),
114
+ operations: str = typer.Argument(..., help="JSON array operations"),
115
+ context: str | None = typer.Option(None, "--context", help="JSON object"),
116
+ include_execution_tree: bool = typer.Option(False, "--include-execution-tree"),
117
+ profile: str | None = typer.Option(None, "--profile"),
118
+ ) -> None:
119
+ """Run an Echo phantom simulation."""
120
+ try:
121
+ parsed_ops = parse_json_arg(operations, label="operations")
122
+ if not isinstance(parsed_ops, list):
123
+ raise CLIRequestError("operations must be a JSON array")
124
+
125
+ parsed_context = parse_json_arg(context, label="--context") if context else {}
126
+ if parsed_context is not None and not isinstance(parsed_context, dict):
127
+ raise CLIRequestError("--context must be a JSON object")
128
+
129
+ result = call_api(
130
+ method="POST",
131
+ path="/api/v1/sim/run",
132
+ profile=profile,
133
+ body={
134
+ "simulation_name": simulation_name,
135
+ "operations": parsed_ops,
136
+ "echo_flag": True,
137
+ "include_execution_tree": include_execution_tree,
138
+ "context": parsed_context or {},
139
+ },
140
+ )
141
+ except CLIRequestError as exc:
142
+ typer.echo(str(exc), err=True)
143
+ raise typer.Exit(1) from exc
144
+
145
+ typer.echo(print_json(result))
146
+
147
+
148
+ @app.command("sim-plan")
149
+ def sim_plan(
150
+ simulation_name: str = typer.Argument(...),
151
+ operations: str = typer.Argument(..., help="JSON array operations"),
152
+ context: str | None = typer.Option(None, "--context", help="JSON object"),
153
+ profile: str | None = typer.Option(None, "--profile"),
154
+ ) -> None:
155
+ """Preview an Echo simulation plan."""
156
+ try:
157
+ parsed_ops = parse_json_arg(operations, label="operations")
158
+ if not isinstance(parsed_ops, list):
159
+ raise CLIRequestError("operations must be a JSON array")
160
+
161
+ parsed_context = parse_json_arg(context, label="--context") if context else {}
162
+ if parsed_context is not None and not isinstance(parsed_context, dict):
163
+ raise CLIRequestError("--context must be a JSON object")
164
+
165
+ result = call_api(
166
+ method="POST",
167
+ path="/api/v1/sim/plan",
168
+ profile=profile,
169
+ body={
170
+ "simulation_name": simulation_name,
171
+ "operations": parsed_ops,
172
+ "echo_flag": True,
173
+ "include_execution_tree": True,
174
+ "context": parsed_context or {},
175
+ },
176
+ )
177
+ except CLIRequestError as exc:
178
+ typer.echo(str(exc), err=True)
179
+ raise typer.Exit(1) from exc
180
+
181
+ typer.echo(print_json(result))