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,248 @@
|
|
|
1
|
+
"""Chrono timeline engine commands."""
|
|
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, print_json
|
|
10
|
+
|
|
11
|
+
app = typer.Typer(help="Chrono timeline and snapshot operations")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@app.command("commit")
|
|
15
|
+
def commit(
|
|
16
|
+
timeline: str = typer.Option("main", "--timeline"),
|
|
17
|
+
message: str = typer.Option(..., "--message"),
|
|
18
|
+
author: str = typer.Option(..., "--author"),
|
|
19
|
+
event_type: str = typer.Option("update", "--event-type"),
|
|
20
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
21
|
+
) -> None:
|
|
22
|
+
"""Create a new chronon commit."""
|
|
23
|
+
try:
|
|
24
|
+
result = call_api(
|
|
25
|
+
method="POST",
|
|
26
|
+
path="/api/v1/chrono/commit",
|
|
27
|
+
profile=profile,
|
|
28
|
+
body={
|
|
29
|
+
"timeline": timeline,
|
|
30
|
+
"message": message,
|
|
31
|
+
"author": author,
|
|
32
|
+
"event_type": event_type,
|
|
33
|
+
},
|
|
34
|
+
)
|
|
35
|
+
except CLIRequestError as exc:
|
|
36
|
+
typer.echo(str(exc), err=True)
|
|
37
|
+
raise typer.Exit(1) from exc
|
|
38
|
+
typer.echo(print_json(result))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@app.command("timeline-list")
|
|
42
|
+
def timeline_list(profile: str | None = typer.Option(None, "--profile")) -> None:
|
|
43
|
+
"""List timelines."""
|
|
44
|
+
try:
|
|
45
|
+
result = call_api(method="GET", path="/api/v1/chrono/timelines", profile=profile)
|
|
46
|
+
except CLIRequestError as exc:
|
|
47
|
+
typer.echo(str(exc), err=True)
|
|
48
|
+
raise typer.Exit(1) from exc
|
|
49
|
+
typer.echo(print_json(result))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@app.command("timeline-spawn")
|
|
53
|
+
def timeline_spawn(
|
|
54
|
+
name: str = typer.Argument(...),
|
|
55
|
+
source_hex_id: str | None = typer.Option(None, "--source-hex-id"),
|
|
56
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
57
|
+
) -> None:
|
|
58
|
+
"""Spawn a new timeline from a source head."""
|
|
59
|
+
try:
|
|
60
|
+
result = call_api(
|
|
61
|
+
method="POST",
|
|
62
|
+
path="/api/v1/chrono/timelines/spawn",
|
|
63
|
+
profile=profile,
|
|
64
|
+
body={"name": name, "source_hex_id": source_hex_id},
|
|
65
|
+
)
|
|
66
|
+
except CLIRequestError as exc:
|
|
67
|
+
typer.echo(str(exc), err=True)
|
|
68
|
+
raise typer.Exit(1) from exc
|
|
69
|
+
typer.echo(print_json(result))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@app.command("timeline-delete")
|
|
73
|
+
def timeline_delete(
|
|
74
|
+
name: str = typer.Argument(...),
|
|
75
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
76
|
+
) -> None:
|
|
77
|
+
"""Delete a timeline."""
|
|
78
|
+
try:
|
|
79
|
+
result = call_api(
|
|
80
|
+
method="DELETE",
|
|
81
|
+
path=f"/api/v1/chrono/timelines/{quote(name, safe='')}",
|
|
82
|
+
profile=profile,
|
|
83
|
+
)
|
|
84
|
+
except CLIRequestError as exc:
|
|
85
|
+
typer.echo(str(exc), err=True)
|
|
86
|
+
raise typer.Exit(1) from exc
|
|
87
|
+
typer.echo(print_json(result))
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@app.command("timeline-jump")
|
|
91
|
+
def timeline_jump(
|
|
92
|
+
name: str = typer.Argument(...),
|
|
93
|
+
target_hex: str = typer.Option(..., "--target-hex"),
|
|
94
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
95
|
+
) -> None:
|
|
96
|
+
"""Force-jump a timeline to a specific head."""
|
|
97
|
+
try:
|
|
98
|
+
result = call_api(
|
|
99
|
+
method="POST",
|
|
100
|
+
path=f"/api/v1/chrono/timelines/{quote(name, safe='')}/jump",
|
|
101
|
+
profile=profile,
|
|
102
|
+
body={"target_hex": target_hex},
|
|
103
|
+
)
|
|
104
|
+
except CLIRequestError as exc:
|
|
105
|
+
typer.echo(str(exc), err=True)
|
|
106
|
+
raise typer.Exit(1) from exc
|
|
107
|
+
typer.echo(print_json(result))
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@app.command("timeline-history")
|
|
111
|
+
def timeline_history(
|
|
112
|
+
name: str = typer.Argument(...),
|
|
113
|
+
limit: int = typer.Option(50, "--limit"),
|
|
114
|
+
offset: int = typer.Option(0, "--offset"),
|
|
115
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
116
|
+
) -> None:
|
|
117
|
+
"""Show timeline history."""
|
|
118
|
+
try:
|
|
119
|
+
result = call_api(
|
|
120
|
+
method="GET",
|
|
121
|
+
path=f"/api/v1/chrono/timelines/{quote(name, safe='')}/history",
|
|
122
|
+
profile=profile,
|
|
123
|
+
params={"limit": str(limit), "offset": str(offset)},
|
|
124
|
+
)
|
|
125
|
+
except CLIRequestError as exc:
|
|
126
|
+
typer.echo(str(exc), err=True)
|
|
127
|
+
raise typer.Exit(1) from exc
|
|
128
|
+
typer.echo(print_json(result))
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@app.command("topology")
|
|
132
|
+
def topology(
|
|
133
|
+
timeline: str = typer.Option("main", "--timeline"),
|
|
134
|
+
limit: int = typer.Option(50, "--limit"),
|
|
135
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
136
|
+
) -> None:
|
|
137
|
+
"""Show timeline topology graph."""
|
|
138
|
+
try:
|
|
139
|
+
result = call_api(
|
|
140
|
+
method="GET",
|
|
141
|
+
path="/api/v1/chrono/timelines/topology",
|
|
142
|
+
profile=profile,
|
|
143
|
+
params={"timeline": timeline, "limit": str(limit)},
|
|
144
|
+
)
|
|
145
|
+
except CLIRequestError as exc:
|
|
146
|
+
typer.echo(str(exc), err=True)
|
|
147
|
+
raise typer.Exit(1) from exc
|
|
148
|
+
typer.echo(print_json(result))
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@app.command("converge")
|
|
152
|
+
def converge(
|
|
153
|
+
target: str = typer.Option(..., "--target"),
|
|
154
|
+
source: str = typer.Option(..., "--source"),
|
|
155
|
+
author: str = typer.Option(..., "--author"),
|
|
156
|
+
message: str = typer.Option(..., "--message"),
|
|
157
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
158
|
+
) -> None:
|
|
159
|
+
"""Converge source timeline into target."""
|
|
160
|
+
try:
|
|
161
|
+
result = call_api(
|
|
162
|
+
method="POST",
|
|
163
|
+
path="/api/v1/chrono/timelines/converge",
|
|
164
|
+
profile=profile,
|
|
165
|
+
body={
|
|
166
|
+
"target": target,
|
|
167
|
+
"source": source,
|
|
168
|
+
"author": author,
|
|
169
|
+
"message": message,
|
|
170
|
+
},
|
|
171
|
+
)
|
|
172
|
+
except CLIRequestError as exc:
|
|
173
|
+
typer.echo(str(exc), err=True)
|
|
174
|
+
raise typer.Exit(1) from exc
|
|
175
|
+
typer.echo(print_json(result))
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@app.command("variance")
|
|
179
|
+
def variance(
|
|
180
|
+
old_chronon_hex: str = typer.Option(..., "--old"),
|
|
181
|
+
new_chronon_hex: str = typer.Option(..., "--new"),
|
|
182
|
+
domain_filter: str | None = typer.Option(None, "--domain-filter"),
|
|
183
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
184
|
+
) -> None:
|
|
185
|
+
"""Calculate variance between chronons."""
|
|
186
|
+
try:
|
|
187
|
+
result = call_api(
|
|
188
|
+
method="POST",
|
|
189
|
+
path="/api/v1/chrono/variance",
|
|
190
|
+
profile=profile,
|
|
191
|
+
body={
|
|
192
|
+
"old_chronon_hex": old_chronon_hex,
|
|
193
|
+
"new_chronon_hex": new_chronon_hex,
|
|
194
|
+
"domain_filter": domain_filter,
|
|
195
|
+
},
|
|
196
|
+
)
|
|
197
|
+
except CLIRequestError as exc:
|
|
198
|
+
typer.echo(str(exc), err=True)
|
|
199
|
+
raise typer.Exit(1) from exc
|
|
200
|
+
typer.echo(print_json(result))
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@app.command("snapshot-create")
|
|
204
|
+
def snapshot_create(
|
|
205
|
+
message: str = typer.Option(..., "--message"),
|
|
206
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
207
|
+
) -> None:
|
|
208
|
+
"""Create a Chrono snapshot."""
|
|
209
|
+
try:
|
|
210
|
+
result = call_api(
|
|
211
|
+
method="POST",
|
|
212
|
+
path="/api/v1/chrono/snapshots/create",
|
|
213
|
+
profile=profile,
|
|
214
|
+
body={"message": message},
|
|
215
|
+
)
|
|
216
|
+
except CLIRequestError as exc:
|
|
217
|
+
typer.echo(str(exc), err=True)
|
|
218
|
+
raise typer.Exit(1) from exc
|
|
219
|
+
typer.echo(print_json(result))
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@app.command("snapshot-list")
|
|
223
|
+
def snapshot_list(profile: str | None = typer.Option(None, "--profile")) -> None:
|
|
224
|
+
"""List Chrono snapshots."""
|
|
225
|
+
try:
|
|
226
|
+
result = call_api(method="GET", path="/api/v1/chrono/snapshots", profile=profile)
|
|
227
|
+
except CLIRequestError as exc:
|
|
228
|
+
typer.echo(str(exc), err=True)
|
|
229
|
+
raise typer.Exit(1) from exc
|
|
230
|
+
typer.echo(print_json(result))
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@app.command("snapshot-restore")
|
|
234
|
+
def snapshot_restore(
|
|
235
|
+
snapshot_id: int = typer.Argument(...),
|
|
236
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
237
|
+
) -> None:
|
|
238
|
+
"""Restore a snapshot by id."""
|
|
239
|
+
try:
|
|
240
|
+
result = call_api(
|
|
241
|
+
method="POST",
|
|
242
|
+
path=f"/api/v1/chrono/snapshots/{snapshot_id}/restore",
|
|
243
|
+
profile=profile,
|
|
244
|
+
)
|
|
245
|
+
except CLIRequestError as exc:
|
|
246
|
+
typer.echo(str(exc), err=True)
|
|
247
|
+
raise typer.Exit(1) from exc
|
|
248
|
+
typer.echo(print_json(result))
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""
|
|
2
|
+
apps.cli.src.commands.context — Show current user/org context.
|
|
3
|
+
|
|
4
|
+
Phase 1 §9 Sprint 3.2: `sov context` prints who you are and what
|
|
5
|
+
resources you have access to, by querying the Axis context endpoint
|
|
6
|
+
through the Gateway.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
import typer
|
|
13
|
+
|
|
14
|
+
from apps.cli.src.utils.auth_store import load_tokens
|
|
15
|
+
from apps.cli.src.utils.config import get_api_url
|
|
16
|
+
|
|
17
|
+
app = typer.Typer()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@app.command("whoami")
|
|
21
|
+
def whoami(
|
|
22
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
23
|
+
) -> None:
|
|
24
|
+
"""Print the currently authenticated user."""
|
|
25
|
+
tokens = load_tokens(profile=profile)
|
|
26
|
+
if not tokens:
|
|
27
|
+
typer.echo("Not logged in. Run: sov login", err=True)
|
|
28
|
+
raise typer.Exit(1)
|
|
29
|
+
|
|
30
|
+
api_url = get_api_url(profile)
|
|
31
|
+
try:
|
|
32
|
+
resp = httpx.get(
|
|
33
|
+
f"{api_url}/api/v1/auth/me",
|
|
34
|
+
headers={"Authorization": f"Bearer {tokens['access_token']}"},
|
|
35
|
+
timeout=10,
|
|
36
|
+
)
|
|
37
|
+
resp.raise_for_status()
|
|
38
|
+
data = resp.json()
|
|
39
|
+
typer.echo(f"user_id : {data.get('user_id', '-')}")
|
|
40
|
+
typer.echo(f"email : {data.get('email', '-')}")
|
|
41
|
+
typer.echo(f"roles : {', '.join(data.get('roles', []))}")
|
|
42
|
+
typer.echo(f"lane : {data.get('actor_lane', '-')}")
|
|
43
|
+
typer.echo(f"subject : {data.get('auth_subject_type', '-')}")
|
|
44
|
+
except httpx.HTTPStatusError as exc:
|
|
45
|
+
if exc.response.status_code == 401:
|
|
46
|
+
typer.echo("Session expired - run: sov login", err=True)
|
|
47
|
+
else:
|
|
48
|
+
typer.echo(f"Error: {exc.response.text}", err=True)
|
|
49
|
+
raise typer.Exit(1) from exc
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@app.command("context")
|
|
53
|
+
def context(
|
|
54
|
+
resource_id: str = typer.Argument(..., help="Node ID of the resource (ORG / PROJECT / ENV)"),
|
|
55
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
56
|
+
) -> None:
|
|
57
|
+
"""
|
|
58
|
+
Show the access roles the current user has on a specific resource.
|
|
59
|
+
|
|
60
|
+
Example:
|
|
61
|
+
sov context proj-abc123
|
|
62
|
+
"""
|
|
63
|
+
tokens = load_tokens(profile=profile)
|
|
64
|
+
if not tokens:
|
|
65
|
+
typer.echo("Not logged in. Run: sov login", err=True)
|
|
66
|
+
raise typer.Exit(1)
|
|
67
|
+
|
|
68
|
+
api_url = get_api_url(profile)
|
|
69
|
+
try:
|
|
70
|
+
# First resolve user_id from the access token by calling whoami.
|
|
71
|
+
me = httpx.get(
|
|
72
|
+
f"{api_url}/api/v1/auth/me",
|
|
73
|
+
headers={"Authorization": f"Bearer {tokens['access_token']}"},
|
|
74
|
+
timeout=10,
|
|
75
|
+
)
|
|
76
|
+
me.raise_for_status()
|
|
77
|
+
me_data = me.json()
|
|
78
|
+
user_id = me_data.get("user_id") or me_data.get("sub") or me_data.get("id")
|
|
79
|
+
if not user_id:
|
|
80
|
+
typer.echo("Could not determine user ID from /auth/me response", err=True)
|
|
81
|
+
raise typer.Exit(1)
|
|
82
|
+
|
|
83
|
+
resp = httpx.get(
|
|
84
|
+
f"{api_url}/api/v1/context/{user_id}/{resource_id}",
|
|
85
|
+
headers={"Authorization": f"Bearer {tokens['access_token']}"},
|
|
86
|
+
timeout=10,
|
|
87
|
+
)
|
|
88
|
+
resp.raise_for_status()
|
|
89
|
+
data = resp.json()
|
|
90
|
+
roles = data.get("roles", [])
|
|
91
|
+
if roles:
|
|
92
|
+
typer.echo(f"You have the following roles on {resource_id}:")
|
|
93
|
+
for role in roles:
|
|
94
|
+
typer.echo(f" • {role}")
|
|
95
|
+
else:
|
|
96
|
+
typer.echo(f"No access to resource {resource_id}")
|
|
97
|
+
except httpx.HTTPStatusError as exc:
|
|
98
|
+
typer.echo(f"Error: {exc.response.text}", err=True)
|
|
99
|
+
raise typer.Exit(1) from exc
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"""Data engine commands (Atlas-Doc + Atlas-Rel)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
from urllib.parse import quote
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from apps.cli.src.utils.api_client import CLIRequestError, call_api, parse_json_arg, print_json
|
|
11
|
+
|
|
12
|
+
app = typer.Typer(help="Atlas-Doc and Atlas-Rel operations")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _safe_doc_path(collection: str, document_id: str) -> str:
|
|
16
|
+
return f"{quote(collection.strip('/'), safe='/')}/{quote(document_id, safe='')}"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@app.command("doc-put")
|
|
20
|
+
def doc_put(
|
|
21
|
+
collection: str = typer.Argument(..., help="Collection path"),
|
|
22
|
+
document_id: str = typer.Argument(..., help="Document key"),
|
|
23
|
+
data: str = typer.Argument(..., help="JSON object document body"),
|
|
24
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
25
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
26
|
+
project_id: str | None = typer.Option(None, "--project"),
|
|
27
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
28
|
+
) -> None:
|
|
29
|
+
"""Write a plain JSON document."""
|
|
30
|
+
try:
|
|
31
|
+
payload = parse_json_arg(data, label="data")
|
|
32
|
+
if not isinstance(payload, dict):
|
|
33
|
+
raise CLIRequestError("data must be a JSON object")
|
|
34
|
+
path = _safe_doc_path(collection, document_id)
|
|
35
|
+
result = call_api(
|
|
36
|
+
method="POST",
|
|
37
|
+
path=f"/api/v1/data/doc/{path}",
|
|
38
|
+
profile=profile,
|
|
39
|
+
body={"data": payload},
|
|
40
|
+
require_scope=False,
|
|
41
|
+
org_id=org_id,
|
|
42
|
+
project_id=project_id,
|
|
43
|
+
env_id=env_id,
|
|
44
|
+
)
|
|
45
|
+
except CLIRequestError as exc:
|
|
46
|
+
typer.echo(str(exc), err=True)
|
|
47
|
+
raise typer.Exit(1) from exc
|
|
48
|
+
typer.echo(print_json(result))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@app.command("doc-get")
|
|
52
|
+
def doc_get(
|
|
53
|
+
collection: str = typer.Argument(...),
|
|
54
|
+
document_id: str = typer.Argument(...),
|
|
55
|
+
txid: int = typer.Option(0, "--txid", help="Optional historical txid"),
|
|
56
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
57
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
58
|
+
project_id: str | None = typer.Option(None, "--project"),
|
|
59
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
60
|
+
) -> None:
|
|
61
|
+
"""Read a plain JSON document."""
|
|
62
|
+
try:
|
|
63
|
+
path = _safe_doc_path(collection, document_id)
|
|
64
|
+
params = {"txid": str(txid)} if txid > 0 else None
|
|
65
|
+
result = call_api(
|
|
66
|
+
method="GET",
|
|
67
|
+
path=f"/api/v1/data/doc/{path}",
|
|
68
|
+
profile=profile,
|
|
69
|
+
params=params,
|
|
70
|
+
require_scope=False,
|
|
71
|
+
org_id=org_id,
|
|
72
|
+
project_id=project_id,
|
|
73
|
+
env_id=env_id,
|
|
74
|
+
)
|
|
75
|
+
except CLIRequestError as exc:
|
|
76
|
+
typer.echo(str(exc), err=True)
|
|
77
|
+
raise typer.Exit(1) from exc
|
|
78
|
+
typer.echo(print_json(result))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@app.command("doc-delete")
|
|
82
|
+
def doc_delete(
|
|
83
|
+
collection: str = typer.Argument(...),
|
|
84
|
+
document_id: str = typer.Argument(...),
|
|
85
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
86
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
87
|
+
project_id: str | None = typer.Option(None, "--project"),
|
|
88
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
89
|
+
) -> None:
|
|
90
|
+
"""Delete a document."""
|
|
91
|
+
try:
|
|
92
|
+
path = _safe_doc_path(collection, document_id)
|
|
93
|
+
result = call_api(
|
|
94
|
+
method="DELETE",
|
|
95
|
+
path=f"/api/v1/data/doc/{path}",
|
|
96
|
+
profile=profile,
|
|
97
|
+
require_scope=False,
|
|
98
|
+
org_id=org_id,
|
|
99
|
+
project_id=project_id,
|
|
100
|
+
env_id=env_id,
|
|
101
|
+
)
|
|
102
|
+
except CLIRequestError as exc:
|
|
103
|
+
typer.echo(str(exc), err=True)
|
|
104
|
+
raise typer.Exit(1) from exc
|
|
105
|
+
typer.echo(print_json(result))
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@app.command("doc-scan")
|
|
109
|
+
def doc_scan(
|
|
110
|
+
prefix: str | None = typer.Option(None, "--prefix"),
|
|
111
|
+
start_key: str | None = typer.Option(None, "--start-key"),
|
|
112
|
+
end_key: str | None = typer.Option(None, "--end-key"),
|
|
113
|
+
limit: int = typer.Option(100, "--limit"),
|
|
114
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
115
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
116
|
+
project_id: str | None = typer.Option(None, "--project"),
|
|
117
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
118
|
+
) -> None:
|
|
119
|
+
"""Scan Atlas-Doc by prefix/range."""
|
|
120
|
+
body: dict[str, Any] = {"limit": limit}
|
|
121
|
+
if prefix is not None:
|
|
122
|
+
body["prefix"] = prefix
|
|
123
|
+
if start_key is not None:
|
|
124
|
+
body["start_key"] = start_key
|
|
125
|
+
if end_key is not None:
|
|
126
|
+
body["end_key"] = end_key
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
result = call_api(
|
|
130
|
+
method="POST",
|
|
131
|
+
path="/api/v1/data/query/scan",
|
|
132
|
+
profile=profile,
|
|
133
|
+
body=body,
|
|
134
|
+
require_scope=False,
|
|
135
|
+
org_id=org_id,
|
|
136
|
+
project_id=project_id,
|
|
137
|
+
env_id=env_id,
|
|
138
|
+
)
|
|
139
|
+
except CLIRequestError as exc:
|
|
140
|
+
typer.echo(str(exc), err=True)
|
|
141
|
+
raise typer.Exit(1) from exc
|
|
142
|
+
typer.echo(print_json(result))
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@app.command("sql")
|
|
146
|
+
def sql_query(
|
|
147
|
+
sql: str = typer.Argument(..., help="SQL statement"),
|
|
148
|
+
txid: int = typer.Option(0, "--txid"),
|
|
149
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
150
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
151
|
+
project_id: str | None = typer.Option(None, "--project"),
|
|
152
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
153
|
+
) -> None:
|
|
154
|
+
"""Run SQL query through Atlas-Rel."""
|
|
155
|
+
try:
|
|
156
|
+
result = call_api(
|
|
157
|
+
method="POST",
|
|
158
|
+
path="/api/v1/sql/query",
|
|
159
|
+
profile=profile,
|
|
160
|
+
body={"sql": sql, "txid": txid},
|
|
161
|
+
require_scope=True,
|
|
162
|
+
org_id=org_id,
|
|
163
|
+
project_id=project_id,
|
|
164
|
+
env_id=env_id,
|
|
165
|
+
)
|
|
166
|
+
except CLIRequestError as exc:
|
|
167
|
+
typer.echo(str(exc), err=True)
|
|
168
|
+
raise typer.Exit(1) from exc
|
|
169
|
+
typer.echo(print_json(result))
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@app.command("table-create")
|
|
173
|
+
def table_create(
|
|
174
|
+
table_name: str = typer.Argument(...),
|
|
175
|
+
columns: str = typer.Argument(..., help="JSON array of column definitions"),
|
|
176
|
+
foreign_keys: str | None = typer.Option(None, "--foreign-keys", help="JSON array"),
|
|
177
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
178
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
179
|
+
project_id: str | None = typer.Option(None, "--project"),
|
|
180
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
181
|
+
) -> None:
|
|
182
|
+
"""Create a relational table."""
|
|
183
|
+
try:
|
|
184
|
+
parsed_columns = parse_json_arg(columns, label="columns")
|
|
185
|
+
parsed_fk = parse_json_arg(foreign_keys, label="--foreign-keys") if foreign_keys else []
|
|
186
|
+
if not isinstance(parsed_columns, list):
|
|
187
|
+
raise CLIRequestError("columns must be a JSON array")
|
|
188
|
+
if parsed_fk is not None and not isinstance(parsed_fk, list):
|
|
189
|
+
raise CLIRequestError("--foreign-keys must be a JSON array")
|
|
190
|
+
|
|
191
|
+
result = call_api(
|
|
192
|
+
method="POST",
|
|
193
|
+
path="/api/v1/data/rel/create",
|
|
194
|
+
profile=profile,
|
|
195
|
+
body={
|
|
196
|
+
"table_name": table_name,
|
|
197
|
+
"columns": parsed_columns,
|
|
198
|
+
"foreign_keys": parsed_fk or [],
|
|
199
|
+
},
|
|
200
|
+
require_scope=True,
|
|
201
|
+
org_id=org_id,
|
|
202
|
+
project_id=project_id,
|
|
203
|
+
env_id=env_id,
|
|
204
|
+
)
|
|
205
|
+
except CLIRequestError as exc:
|
|
206
|
+
typer.echo(str(exc), err=True)
|
|
207
|
+
raise typer.Exit(1) from exc
|
|
208
|
+
|
|
209
|
+
typer.echo(print_json(result))
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@app.command("table-drop")
|
|
213
|
+
def table_drop(
|
|
214
|
+
table_name: str = typer.Argument(...),
|
|
215
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
216
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
217
|
+
project_id: str | None = typer.Option(None, "--project"),
|
|
218
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
219
|
+
) -> None:
|
|
220
|
+
"""Drop a relational table."""
|
|
221
|
+
try:
|
|
222
|
+
result = call_api(
|
|
223
|
+
method="POST",
|
|
224
|
+
path="/api/v1/data/rel/drop",
|
|
225
|
+
profile=profile,
|
|
226
|
+
body={"table_name": table_name},
|
|
227
|
+
require_scope=True,
|
|
228
|
+
org_id=org_id,
|
|
229
|
+
project_id=project_id,
|
|
230
|
+
env_id=env_id,
|
|
231
|
+
)
|
|
232
|
+
except CLIRequestError as exc:
|
|
233
|
+
typer.echo(str(exc), err=True)
|
|
234
|
+
raise typer.Exit(1) from exc
|
|
235
|
+
|
|
236
|
+
typer.echo(print_json(result))
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@app.command("table-migrations")
|
|
240
|
+
def table_migrations(
|
|
241
|
+
table_name: str | None = typer.Option(None, "--table"),
|
|
242
|
+
limit: int = typer.Option(100, "--limit"),
|
|
243
|
+
offset: int = typer.Option(0, "--offset"),
|
|
244
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
245
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
246
|
+
project_id: str | None = typer.Option(None, "--project"),
|
|
247
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
248
|
+
) -> None:
|
|
249
|
+
"""List migration log entries."""
|
|
250
|
+
params = {
|
|
251
|
+
"limit": str(limit),
|
|
252
|
+
"offset": str(offset),
|
|
253
|
+
}
|
|
254
|
+
if table_name:
|
|
255
|
+
params["table_name"] = table_name
|
|
256
|
+
|
|
257
|
+
try:
|
|
258
|
+
result = call_api(
|
|
259
|
+
method="GET",
|
|
260
|
+
path="/api/v1/data/rel/migrations",
|
|
261
|
+
profile=profile,
|
|
262
|
+
params=params,
|
|
263
|
+
require_scope=True,
|
|
264
|
+
org_id=org_id,
|
|
265
|
+
project_id=project_id,
|
|
266
|
+
env_id=env_id,
|
|
267
|
+
)
|
|
268
|
+
except CLIRequestError as exc:
|
|
269
|
+
typer.echo(str(exc), err=True)
|
|
270
|
+
raise typer.Exit(1) from exc
|
|
271
|
+
|
|
272
|
+
typer.echo(print_json(result))
|