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,405 @@
|
|
|
1
|
+
"""
|
|
2
|
+
apps.cli.src.commands.storage — Vault storage management commands.
|
|
3
|
+
|
|
4
|
+
Commands:
|
|
5
|
+
sov storage buckets List all buckets
|
|
6
|
+
sov storage create-bucket Create a new bucket
|
|
7
|
+
sov storage ls List objects in a bucket
|
|
8
|
+
sov storage upload Upload a file to a bucket
|
|
9
|
+
sov storage download Download an object
|
|
10
|
+
sov storage sign-download Mint a signed download URL
|
|
11
|
+
sov storage sign-upload Mint a signed upload URL
|
|
12
|
+
sov storage rm Delete an object
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from urllib.parse import quote
|
|
19
|
+
|
|
20
|
+
import httpx
|
|
21
|
+
import typer
|
|
22
|
+
|
|
23
|
+
from apps.cli.src.utils.api_client import CLIRequestError, build_request_context, call_api
|
|
24
|
+
from apps.cli.src.utils.display import print_json, print_success
|
|
25
|
+
|
|
26
|
+
app = typer.Typer(help="Object storage management")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _object_path(bucket: str, object_path: str) -> str:
|
|
30
|
+
encoded = "/".join(quote(part, safe="") for part in object_path.strip("/").split("/"))
|
|
31
|
+
return f"/api/v1/store/objects/{quote(bucket, safe='')}/{encoded}"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@app.command("buckets")
|
|
35
|
+
def list_buckets(
|
|
36
|
+
project_id: str | None = typer.Option(None, "--project", help="Project ID"),
|
|
37
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
38
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
39
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
40
|
+
) -> None:
|
|
41
|
+
"""List all storage buckets."""
|
|
42
|
+
try:
|
|
43
|
+
result = call_api(
|
|
44
|
+
method="GET",
|
|
45
|
+
path="/api/v1/store/buckets",
|
|
46
|
+
profile=profile,
|
|
47
|
+
org_id=org_id,
|
|
48
|
+
project_id=project_id,
|
|
49
|
+
env_id=env_id,
|
|
50
|
+
require_scope=True,
|
|
51
|
+
)
|
|
52
|
+
except CLIRequestError as exc:
|
|
53
|
+
typer.echo(str(exc), err=True)
|
|
54
|
+
raise typer.Exit(1) from exc
|
|
55
|
+
|
|
56
|
+
if not isinstance(result, list):
|
|
57
|
+
print_json(result)
|
|
58
|
+
return
|
|
59
|
+
for bucket in result:
|
|
60
|
+
vis = "public" if bucket.get("public") else "private"
|
|
61
|
+
typer.echo(f" {bucket['name']} ({vis}) objects={bucket.get('object_count', 0)}")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@app.command("create-bucket")
|
|
65
|
+
def create_bucket(
|
|
66
|
+
name: str = typer.Argument(..., help="Bucket name"),
|
|
67
|
+
project_id: str | None = typer.Option(None, "--project", help="Project ID"),
|
|
68
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
69
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
70
|
+
public: bool = typer.Option(False, "--public", help="Make bucket public"),
|
|
71
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
72
|
+
) -> None:
|
|
73
|
+
"""Create a new storage bucket."""
|
|
74
|
+
try:
|
|
75
|
+
call_api(
|
|
76
|
+
method="POST",
|
|
77
|
+
path="/api/v1/store/buckets",
|
|
78
|
+
profile=profile,
|
|
79
|
+
org_id=org_id,
|
|
80
|
+
project_id=project_id,
|
|
81
|
+
env_id=env_id,
|
|
82
|
+
require_scope=True,
|
|
83
|
+
body={"name": name, "public": public},
|
|
84
|
+
)
|
|
85
|
+
except CLIRequestError as exc:
|
|
86
|
+
typer.echo(str(exc), err=True)
|
|
87
|
+
raise typer.Exit(1) from exc
|
|
88
|
+
print_success(f"Bucket '{name}' created")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@app.command("ls")
|
|
92
|
+
def list_objects(
|
|
93
|
+
bucket: str = typer.Argument(..., help="Bucket name"),
|
|
94
|
+
project_id: str | None = typer.Option(None, "--project", help="Project ID"),
|
|
95
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
96
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
97
|
+
prefix: str = typer.Option("", "--prefix", help="Filter by prefix"),
|
|
98
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
99
|
+
) -> None:
|
|
100
|
+
"""List objects in a bucket."""
|
|
101
|
+
params = {"prefix": prefix} if prefix else None
|
|
102
|
+
try:
|
|
103
|
+
result = call_api(
|
|
104
|
+
method="GET",
|
|
105
|
+
path=f"/api/v1/store/objects/{quote(bucket, safe='')}",
|
|
106
|
+
profile=profile,
|
|
107
|
+
org_id=org_id,
|
|
108
|
+
project_id=project_id,
|
|
109
|
+
env_id=env_id,
|
|
110
|
+
require_scope=True,
|
|
111
|
+
params=params,
|
|
112
|
+
)
|
|
113
|
+
except CLIRequestError as exc:
|
|
114
|
+
typer.echo(str(exc), err=True)
|
|
115
|
+
raise typer.Exit(1) from exc
|
|
116
|
+
|
|
117
|
+
if not isinstance(result, list):
|
|
118
|
+
print_json(result)
|
|
119
|
+
return
|
|
120
|
+
for obj in result:
|
|
121
|
+
size_kb = obj.get("size", 0) / 1024
|
|
122
|
+
typer.echo(f" {obj['name']} {size_kb:.1f}KB {obj.get('content_type', '-')}")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@app.command("upload")
|
|
126
|
+
def upload(
|
|
127
|
+
bucket: str = typer.Argument(..., help="Bucket name"),
|
|
128
|
+
file_path: str = typer.Argument(..., help="Local file path"),
|
|
129
|
+
project_id: str | None = typer.Option(None, "--project", help="Project ID"),
|
|
130
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
131
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
132
|
+
object_path: str | None = typer.Option(None, "--name", help="Remote object path (defaults to filename)"),
|
|
133
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
134
|
+
) -> None:
|
|
135
|
+
"""Upload a file to a bucket."""
|
|
136
|
+
path = Path(file_path)
|
|
137
|
+
if not path.exists():
|
|
138
|
+
typer.echo(f"File not found: {file_path}", err=True)
|
|
139
|
+
raise typer.Exit(1)
|
|
140
|
+
remote_name = object_path or path.name
|
|
141
|
+
try:
|
|
142
|
+
api_url, headers = build_request_context(
|
|
143
|
+
profile=profile,
|
|
144
|
+
org_id=org_id,
|
|
145
|
+
project_id=project_id,
|
|
146
|
+
env_id=env_id,
|
|
147
|
+
require_scope=True,
|
|
148
|
+
)
|
|
149
|
+
with open(path, "rb") as handle:
|
|
150
|
+
response = httpx.post(
|
|
151
|
+
f"{api_url}{_object_path(bucket, remote_name)}",
|
|
152
|
+
headers=headers,
|
|
153
|
+
files={"file": (remote_name, handle)},
|
|
154
|
+
timeout=120,
|
|
155
|
+
)
|
|
156
|
+
response.raise_for_status()
|
|
157
|
+
except CLIRequestError as exc:
|
|
158
|
+
typer.echo(str(exc), err=True)
|
|
159
|
+
raise typer.Exit(1) from exc
|
|
160
|
+
except httpx.HTTPStatusError as exc:
|
|
161
|
+
typer.echo(f"Error: {exc.response.text}", err=True)
|
|
162
|
+
raise typer.Exit(1) from exc
|
|
163
|
+
print_success(f"Uploaded: {remote_name} -> {bucket}")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@app.command("download")
|
|
167
|
+
def download(
|
|
168
|
+
bucket: str = typer.Argument(..., help="Bucket name"),
|
|
169
|
+
object_path: str = typer.Argument(..., help="Remote object path"),
|
|
170
|
+
output: Path = typer.Option(..., "--output", "-o", help="Local output file path"),
|
|
171
|
+
project_id: str | None = typer.Option(None, "--project"),
|
|
172
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
173
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
174
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
175
|
+
) -> None:
|
|
176
|
+
"""Download an object to a local file."""
|
|
177
|
+
try:
|
|
178
|
+
api_url, headers = build_request_context(
|
|
179
|
+
profile=profile,
|
|
180
|
+
org_id=org_id,
|
|
181
|
+
project_id=project_id,
|
|
182
|
+
env_id=env_id,
|
|
183
|
+
require_scope=True,
|
|
184
|
+
)
|
|
185
|
+
response = httpx.get(
|
|
186
|
+
f"{api_url}{_object_path(bucket, object_path)}",
|
|
187
|
+
headers=headers,
|
|
188
|
+
timeout=120,
|
|
189
|
+
)
|
|
190
|
+
response.raise_for_status()
|
|
191
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
192
|
+
output.write_bytes(response.content)
|
|
193
|
+
except CLIRequestError as exc:
|
|
194
|
+
typer.echo(str(exc), err=True)
|
|
195
|
+
raise typer.Exit(1) from exc
|
|
196
|
+
except httpx.HTTPStatusError as exc:
|
|
197
|
+
typer.echo(f"Error: {exc.response.text}", err=True)
|
|
198
|
+
raise typer.Exit(1) from exc
|
|
199
|
+
print_success(f"Downloaded to {output}")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@app.command("sign-download")
|
|
203
|
+
def sign_download(
|
|
204
|
+
bucket: str = typer.Argument(...),
|
|
205
|
+
object_path: str = typer.Argument(...),
|
|
206
|
+
expires_in: int = typer.Option(3600, "--expires-in", help="TTL seconds"),
|
|
207
|
+
project_id: str | None = typer.Option(None, "--project"),
|
|
208
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
209
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
210
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
211
|
+
) -> None:
|
|
212
|
+
"""Mint a time-limited signed download URL."""
|
|
213
|
+
try:
|
|
214
|
+
result = call_api(
|
|
215
|
+
method="POST",
|
|
216
|
+
path=f"{_object_path(bucket, object_path)}/sign-download",
|
|
217
|
+
profile=profile,
|
|
218
|
+
org_id=org_id,
|
|
219
|
+
project_id=project_id,
|
|
220
|
+
env_id=env_id,
|
|
221
|
+
require_scope=True,
|
|
222
|
+
body={"expires_in": expires_in},
|
|
223
|
+
)
|
|
224
|
+
except CLIRequestError as exc:
|
|
225
|
+
typer.echo(str(exc), err=True)
|
|
226
|
+
raise typer.Exit(1) from exc
|
|
227
|
+
print_json(result)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@app.command("sign-upload")
|
|
231
|
+
def sign_upload(
|
|
232
|
+
bucket: str = typer.Argument(...),
|
|
233
|
+
object_path: str = typer.Argument(...),
|
|
234
|
+
expires_in: int = typer.Option(3600, "--expires-in", help="TTL seconds"),
|
|
235
|
+
project_id: str | None = typer.Option(None, "--project"),
|
|
236
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
237
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
238
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
239
|
+
) -> None:
|
|
240
|
+
"""Mint a time-limited signed upload URL."""
|
|
241
|
+
try:
|
|
242
|
+
result = call_api(
|
|
243
|
+
method="POST",
|
|
244
|
+
path=f"{_object_path(bucket, object_path)}/sign-upload",
|
|
245
|
+
profile=profile,
|
|
246
|
+
org_id=org_id,
|
|
247
|
+
project_id=project_id,
|
|
248
|
+
env_id=env_id,
|
|
249
|
+
require_scope=True,
|
|
250
|
+
body={"expires_in": expires_in},
|
|
251
|
+
)
|
|
252
|
+
except CLIRequestError as exc:
|
|
253
|
+
typer.echo(str(exc), err=True)
|
|
254
|
+
raise typer.Exit(1) from exc
|
|
255
|
+
print_json(result)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
@app.command("rm")
|
|
259
|
+
def delete_object(
|
|
260
|
+
bucket: str = typer.Argument(..., help="Bucket name"),
|
|
261
|
+
object_path: str = typer.Argument(..., help="Object path to delete"),
|
|
262
|
+
project_id: str | None = typer.Option(None, "--project", help="Project ID"),
|
|
263
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
264
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
265
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
266
|
+
) -> None:
|
|
267
|
+
"""Delete an object from a bucket."""
|
|
268
|
+
try:
|
|
269
|
+
call_api(
|
|
270
|
+
method="DELETE",
|
|
271
|
+
path=_object_path(bucket, object_path),
|
|
272
|
+
profile=profile,
|
|
273
|
+
org_id=org_id,
|
|
274
|
+
project_id=project_id,
|
|
275
|
+
env_id=env_id,
|
|
276
|
+
require_scope=True,
|
|
277
|
+
)
|
|
278
|
+
except CLIRequestError as exc:
|
|
279
|
+
typer.echo(str(exc), err=True)
|
|
280
|
+
raise typer.Exit(1) from exc
|
|
281
|
+
print_success(f"Deleted: {bucket}/{object_path}")
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
@app.command("bucket-get")
|
|
285
|
+
def bucket_get(
|
|
286
|
+
name: str = typer.Argument(..., help="Bucket name"),
|
|
287
|
+
project_id: str | None = typer.Option(None, "--project"),
|
|
288
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
289
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
290
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
291
|
+
) -> None:
|
|
292
|
+
"""Fetch bucket metadata."""
|
|
293
|
+
try:
|
|
294
|
+
result = call_api(
|
|
295
|
+
method="GET",
|
|
296
|
+
path=f"/api/v1/store/buckets/{quote(name, safe='')}",
|
|
297
|
+
profile=profile,
|
|
298
|
+
org_id=org_id,
|
|
299
|
+
project_id=project_id,
|
|
300
|
+
env_id=env_id,
|
|
301
|
+
require_scope=True,
|
|
302
|
+
)
|
|
303
|
+
except CLIRequestError as exc:
|
|
304
|
+
typer.echo(str(exc), err=True)
|
|
305
|
+
raise typer.Exit(1) from exc
|
|
306
|
+
print_json(result)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
@app.command("bucket-update")
|
|
310
|
+
def bucket_update(
|
|
311
|
+
name: str = typer.Argument(..., help="Bucket name"),
|
|
312
|
+
public: bool | None = typer.Option(None, "--public/--private"),
|
|
313
|
+
file_size_limit: int | None = typer.Option(None, "--file-size-limit"),
|
|
314
|
+
allowed_mime_types: str | None = typer.Option(
|
|
315
|
+
None, "--allowed-mime-types", help="Comma-separated MIME patterns"
|
|
316
|
+
),
|
|
317
|
+
project_id: str | None = typer.Option(None, "--project"),
|
|
318
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
319
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
320
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
321
|
+
) -> None:
|
|
322
|
+
"""Update bucket settings."""
|
|
323
|
+
body: dict[str, object] = {}
|
|
324
|
+
if public is not None:
|
|
325
|
+
body["public"] = public
|
|
326
|
+
if file_size_limit is not None:
|
|
327
|
+
body["file_size_limit"] = file_size_limit
|
|
328
|
+
if allowed_mime_types is not None:
|
|
329
|
+
body["allowed_mime_types"] = [
|
|
330
|
+
part.strip() for part in allowed_mime_types.split(",") if part.strip()
|
|
331
|
+
]
|
|
332
|
+
if not body:
|
|
333
|
+
raise typer.BadParameter("Provide at least one field to update")
|
|
334
|
+
try:
|
|
335
|
+
result = call_api(
|
|
336
|
+
method="PATCH",
|
|
337
|
+
path=f"/api/v1/store/buckets/{quote(name, safe='')}",
|
|
338
|
+
profile=profile,
|
|
339
|
+
org_id=org_id,
|
|
340
|
+
project_id=project_id,
|
|
341
|
+
env_id=env_id,
|
|
342
|
+
require_scope=True,
|
|
343
|
+
body=body,
|
|
344
|
+
)
|
|
345
|
+
except CLIRequestError as exc:
|
|
346
|
+
typer.echo(str(exc), err=True)
|
|
347
|
+
raise typer.Exit(1) from exc
|
|
348
|
+
print_json(result)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
@app.command("bucket-delete")
|
|
352
|
+
def bucket_delete(
|
|
353
|
+
name: str = typer.Argument(..., help="Bucket name"),
|
|
354
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"),
|
|
355
|
+
project_id: str | None = typer.Option(None, "--project"),
|
|
356
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
357
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
358
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
359
|
+
) -> None:
|
|
360
|
+
"""Delete a storage bucket and all objects."""
|
|
361
|
+
if not yes and not typer.confirm(f"Delete bucket '{name}' and all objects?"):
|
|
362
|
+
raise typer.Exit(0)
|
|
363
|
+
try:
|
|
364
|
+
result = call_api(
|
|
365
|
+
method="DELETE",
|
|
366
|
+
path=f"/api/v1/store/buckets/{quote(name, safe='')}",
|
|
367
|
+
profile=profile,
|
|
368
|
+
org_id=org_id,
|
|
369
|
+
project_id=project_id,
|
|
370
|
+
env_id=env_id,
|
|
371
|
+
require_scope=True,
|
|
372
|
+
)
|
|
373
|
+
except CLIRequestError as exc:
|
|
374
|
+
typer.echo(str(exc), err=True)
|
|
375
|
+
raise typer.Exit(1) from exc
|
|
376
|
+
print_json(result)
|
|
377
|
+
print_success(f"Deleted bucket '{name}'")
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
@app.command("move")
|
|
381
|
+
def move_object(
|
|
382
|
+
from_path: str = typer.Option(..., "--from", help="Source path bucket/object"),
|
|
383
|
+
to_path: str = typer.Option(..., "--to", help="Destination path bucket/object"),
|
|
384
|
+
project_id: str | None = typer.Option(None, "--project"),
|
|
385
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
386
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
387
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
388
|
+
) -> None:
|
|
389
|
+
"""Move or rename an object within storage."""
|
|
390
|
+
try:
|
|
391
|
+
result = call_api(
|
|
392
|
+
method="POST",
|
|
393
|
+
path="/api/v1/store/objects/move",
|
|
394
|
+
profile=profile,
|
|
395
|
+
org_id=org_id,
|
|
396
|
+
project_id=project_id,
|
|
397
|
+
env_id=env_id,
|
|
398
|
+
require_scope=True,
|
|
399
|
+
body={"from_path": from_path, "to_path": to_path},
|
|
400
|
+
)
|
|
401
|
+
except CLIRequestError as exc:
|
|
402
|
+
typer.echo(str(exc), err=True)
|
|
403
|
+
raise typer.Exit(1) from exc
|
|
404
|
+
print_json(result)
|
|
405
|
+
print_success(f"Moved {from_path} -> {to_path}")
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Nexus vector / embedding 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, parse_json_arg, print_json
|
|
10
|
+
|
|
11
|
+
app = typer.Typer(help="Vector search and embeddings (Nexus engine)")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@app.command("embed")
|
|
15
|
+
def embed(
|
|
16
|
+
text: list[str] = typer.Argument(..., help="One or more texts to embed"),
|
|
17
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
18
|
+
project_id: str | None = typer.Option(None, "--project"),
|
|
19
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
20
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
21
|
+
) -> None:
|
|
22
|
+
"""Generate embeddings for text inputs."""
|
|
23
|
+
try:
|
|
24
|
+
result = call_api(
|
|
25
|
+
method="POST",
|
|
26
|
+
path="/api/v1/embed",
|
|
27
|
+
profile=profile,
|
|
28
|
+
org_id=org_id,
|
|
29
|
+
project_id=project_id,
|
|
30
|
+
env_id=env_id,
|
|
31
|
+
require_scope=True,
|
|
32
|
+
body={"texts": text},
|
|
33
|
+
)
|
|
34
|
+
except CLIRequestError as exc:
|
|
35
|
+
typer.echo(str(exc), err=True)
|
|
36
|
+
raise typer.Exit(1) from exc
|
|
37
|
+
typer.echo(print_json(result))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@app.command("upsert")
|
|
41
|
+
def upsert(
|
|
42
|
+
collection: str = typer.Argument(..., help="Collection name"),
|
|
43
|
+
docs_json: str = typer.Option(..., "--docs", help='JSON array: [{"text":"...", "id":"..."}]'),
|
|
44
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
45
|
+
project_id: str | None = typer.Option(None, "--project"),
|
|
46
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
47
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
48
|
+
) -> None:
|
|
49
|
+
"""Embed and upsert documents into a vector collection."""
|
|
50
|
+
docs = parse_json_arg(docs_json, label="--docs")
|
|
51
|
+
try:
|
|
52
|
+
result = call_api(
|
|
53
|
+
method="POST",
|
|
54
|
+
path="/api/v1/vectors/upsert",
|
|
55
|
+
profile=profile,
|
|
56
|
+
org_id=org_id,
|
|
57
|
+
project_id=project_id,
|
|
58
|
+
env_id=env_id,
|
|
59
|
+
require_scope=True,
|
|
60
|
+
body={"collection": collection, "docs": docs},
|
|
61
|
+
)
|
|
62
|
+
except CLIRequestError as exc:
|
|
63
|
+
typer.echo(str(exc), err=True)
|
|
64
|
+
raise typer.Exit(1) from exc
|
|
65
|
+
typer.echo(print_json(result))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@app.command("search")
|
|
69
|
+
def search(
|
|
70
|
+
collection: str = typer.Argument(..., help="Collection name"),
|
|
71
|
+
query: str = typer.Argument(..., help="Search query text"),
|
|
72
|
+
top_k: int = typer.Option(5, "--top-k", min=1, max=100),
|
|
73
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
74
|
+
project_id: str | None = typer.Option(None, "--project"),
|
|
75
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
76
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
77
|
+
) -> None:
|
|
78
|
+
"""Semantic search within a vector collection."""
|
|
79
|
+
try:
|
|
80
|
+
result = call_api(
|
|
81
|
+
method="POST",
|
|
82
|
+
path="/api/v1/vectors/search",
|
|
83
|
+
profile=profile,
|
|
84
|
+
org_id=org_id,
|
|
85
|
+
project_id=project_id,
|
|
86
|
+
env_id=env_id,
|
|
87
|
+
require_scope=True,
|
|
88
|
+
body={"collection": collection, "query": query, "top_k": top_k, "metadata_filter": {}},
|
|
89
|
+
)
|
|
90
|
+
except CLIRequestError as exc:
|
|
91
|
+
typer.echo(str(exc), err=True)
|
|
92
|
+
raise typer.Exit(1) from exc
|
|
93
|
+
typer.echo(print_json(result))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@app.command("collections")
|
|
97
|
+
def collections(
|
|
98
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
99
|
+
project_id: str | None = typer.Option(None, "--project"),
|
|
100
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
101
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
102
|
+
) -> None:
|
|
103
|
+
"""List vector collections in the current project scope."""
|
|
104
|
+
try:
|
|
105
|
+
result = call_api(
|
|
106
|
+
method="GET",
|
|
107
|
+
path="/api/v1/collections",
|
|
108
|
+
profile=profile,
|
|
109
|
+
org_id=org_id,
|
|
110
|
+
project_id=project_id,
|
|
111
|
+
env_id=env_id,
|
|
112
|
+
require_scope=True,
|
|
113
|
+
)
|
|
114
|
+
except CLIRequestError as exc:
|
|
115
|
+
typer.echo(str(exc), err=True)
|
|
116
|
+
raise typer.Exit(1) from exc
|
|
117
|
+
typer.echo(print_json(result))
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@app.command("records")
|
|
121
|
+
def records(
|
|
122
|
+
collection: str = typer.Argument(..., help="Collection name"),
|
|
123
|
+
limit: int | None = typer.Option(None, "--limit"),
|
|
124
|
+
offset: int | None = typer.Option(None, "--offset"),
|
|
125
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
126
|
+
project_id: str | None = typer.Option(None, "--project"),
|
|
127
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
128
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
129
|
+
) -> None:
|
|
130
|
+
"""List records in a vector collection."""
|
|
131
|
+
params: dict[str, str] = {}
|
|
132
|
+
if limit is not None:
|
|
133
|
+
params["limit"] = str(limit)
|
|
134
|
+
if offset is not None:
|
|
135
|
+
params["offset"] = str(offset)
|
|
136
|
+
try:
|
|
137
|
+
result = call_api(
|
|
138
|
+
method="GET",
|
|
139
|
+
path=f"/api/v1/collections/{quote(collection, safe='')}/records",
|
|
140
|
+
profile=profile,
|
|
141
|
+
org_id=org_id,
|
|
142
|
+
project_id=project_id,
|
|
143
|
+
env_id=env_id,
|
|
144
|
+
require_scope=True,
|
|
145
|
+
params=params or None,
|
|
146
|
+
)
|
|
147
|
+
except CLIRequestError as exc:
|
|
148
|
+
typer.echo(str(exc), err=True)
|
|
149
|
+
raise typer.Exit(1) from exc
|
|
150
|
+
typer.echo(print_json(result))
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@app.command("delete-record")
|
|
154
|
+
def delete_record(
|
|
155
|
+
collection: str = typer.Argument(..., help="Collection name"),
|
|
156
|
+
record_id: str = typer.Argument(..., help="Record ID"),
|
|
157
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
158
|
+
project_id: str | None = typer.Option(None, "--project"),
|
|
159
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
160
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
161
|
+
) -> None:
|
|
162
|
+
"""Delete one record from a vector collection."""
|
|
163
|
+
try:
|
|
164
|
+
result = call_api(
|
|
165
|
+
method="DELETE",
|
|
166
|
+
path=(
|
|
167
|
+
f"/api/v1/collections/{quote(collection, safe='')}/records/"
|
|
168
|
+
f"{quote(record_id, safe='')}"
|
|
169
|
+
),
|
|
170
|
+
profile=profile,
|
|
171
|
+
org_id=org_id,
|
|
172
|
+
project_id=project_id,
|
|
173
|
+
env_id=env_id,
|
|
174
|
+
require_scope=True,
|
|
175
|
+
)
|
|
176
|
+
except CLIRequestError as exc:
|
|
177
|
+
typer.echo(str(exc), err=True)
|
|
178
|
+
raise typer.Exit(1) from exc
|
|
179
|
+
typer.echo(print_json(result))
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@app.command("delete-collection")
|
|
183
|
+
def delete_collection(
|
|
184
|
+
collection: str = typer.Argument(..., help="Collection name"),
|
|
185
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
186
|
+
project_id: str | None = typer.Option(None, "--project"),
|
|
187
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
188
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
189
|
+
) -> None:
|
|
190
|
+
"""Delete an entire vector collection."""
|
|
191
|
+
try:
|
|
192
|
+
result = call_api(
|
|
193
|
+
method="DELETE",
|
|
194
|
+
path=f"/api/v1/collections/{quote(collection, safe='')}",
|
|
195
|
+
profile=profile,
|
|
196
|
+
org_id=org_id,
|
|
197
|
+
project_id=project_id,
|
|
198
|
+
env_id=env_id,
|
|
199
|
+
require_scope=True,
|
|
200
|
+
)
|
|
201
|
+
except CLIRequestError as exc:
|
|
202
|
+
typer.echo(str(exc), err=True)
|
|
203
|
+
raise typer.Exit(1) from exc
|
|
204
|
+
typer.echo(print_json(result))
|