sovaria-cli 0.2.3__tar.gz → 0.2.5__tar.gz
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.
- {sovaria_cli-0.2.3 → sovaria_cli-0.2.5}/PKG-INFO +1 -1
- {sovaria_cli-0.2.3 → sovaria_cli-0.2.5}/apps/cli/src/__init__.py +1 -1
- {sovaria_cli-0.2.3 → sovaria_cli-0.2.5}/apps/cli/src/commands/db.py +162 -16
- {sovaria_cli-0.2.3 → sovaria_cli-0.2.5}/apps/cli/tests/test_main_startup.py +1 -1
- {sovaria_cli-0.2.3 → sovaria_cli-0.2.5}/apps/cli/tests/test_project_and_commands.py +61 -0
- {sovaria_cli-0.2.3 → sovaria_cli-0.2.5}/pyproject.toml +1 -1
- {sovaria_cli-0.2.3 → sovaria_cli-0.2.5}/.github/workflows/ci.yml +0 -0
- {sovaria_cli-0.2.3 → sovaria_cli-0.2.5}/.github/workflows/publish.yml +0 -0
- {sovaria_cli-0.2.3 → sovaria_cli-0.2.5}/.gitignore +0 -0
- {sovaria_cli-0.2.3 → sovaria_cli-0.2.5}/.python-version +0 -0
- {sovaria_cli-0.2.3 → sovaria_cli-0.2.5}/Makefile +0 -0
- {sovaria_cli-0.2.3 → sovaria_cli-0.2.5}/apps/cli/README.md +0 -0
- {sovaria_cli-0.2.3 → sovaria_cli-0.2.5}/apps/cli/__init__.py +0 -0
- {sovaria_cli-0.2.3 → sovaria_cli-0.2.5}/apps/cli/src/commands/__init__.py +0 -0
- {sovaria_cli-0.2.3 → sovaria_cli-0.2.5}/apps/cli/src/commands/admin.py +0 -0
- {sovaria_cli-0.2.3 → sovaria_cli-0.2.5}/apps/cli/src/commands/api.py +0 -0
- {sovaria_cli-0.2.3 → sovaria_cli-0.2.5}/apps/cli/src/commands/audit.py +0 -0
- {sovaria_cli-0.2.3 → sovaria_cli-0.2.5}/apps/cli/src/commands/auth_product.py +0 -0
- {sovaria_cli-0.2.3 → sovaria_cli-0.2.5}/apps/cli/src/commands/billing.py +0 -0
- {sovaria_cli-0.2.3 → sovaria_cli-0.2.5}/apps/cli/src/commands/chrono.py +0 -0
- {sovaria_cli-0.2.3 → sovaria_cli-0.2.5}/apps/cli/src/commands/context.py +0 -0
- {sovaria_cli-0.2.3 → sovaria_cli-0.2.5}/apps/cli/src/commands/data.py +0 -0
- {sovaria_cli-0.2.3 → sovaria_cli-0.2.5}/apps/cli/src/commands/deploy.py +0 -0
- {sovaria_cli-0.2.3 → sovaria_cli-0.2.5}/apps/cli/src/commands/governance.py +0 -0
- {sovaria_cli-0.2.3 → sovaria_cli-0.2.5}/apps/cli/src/commands/iam.py +0 -0
- {sovaria_cli-0.2.3 → sovaria_cli-0.2.5}/apps/cli/src/commands/init_cmd.py +0 -0
- {sovaria_cli-0.2.3 → sovaria_cli-0.2.5}/apps/cli/src/commands/keys.py +0 -0
- {sovaria_cli-0.2.3 → sovaria_cli-0.2.5}/apps/cli/src/commands/login.py +0 -0
- {sovaria_cli-0.2.3 → sovaria_cli-0.2.5}/apps/cli/src/commands/org.py +0 -0
- {sovaria_cli-0.2.3 → sovaria_cli-0.2.5}/apps/cli/src/commands/policy.py +0 -0
- {sovaria_cli-0.2.3 → sovaria_cli-0.2.5}/apps/cli/src/commands/project.py +0 -0
- {sovaria_cli-0.2.3 → sovaria_cli-0.2.5}/apps/cli/src/commands/realtime.py +0 -0
- {sovaria_cli-0.2.3 → sovaria_cli-0.2.5}/apps/cli/src/commands/scope.py +0 -0
- {sovaria_cli-0.2.3 → sovaria_cli-0.2.5}/apps/cli/src/commands/shell.py +0 -0
- {sovaria_cli-0.2.3 → sovaria_cli-0.2.5}/apps/cli/src/commands/sim.py +0 -0
- {sovaria_cli-0.2.3 → sovaria_cli-0.2.5}/apps/cli/src/commands/storage.py +0 -0
- {sovaria_cli-0.2.3 → sovaria_cli-0.2.5}/apps/cli/src/commands/vector.py +0 -0
- {sovaria_cli-0.2.3 → sovaria_cli-0.2.5}/apps/cli/src/main.py +0 -0
- {sovaria_cli-0.2.3 → sovaria_cli-0.2.5}/apps/cli/src/utils/__init__.py +0 -0
- {sovaria_cli-0.2.3 → sovaria_cli-0.2.5}/apps/cli/src/utils/api_client.py +0 -0
- {sovaria_cli-0.2.3 → sovaria_cli-0.2.5}/apps/cli/src/utils/auth_store.py +0 -0
- {sovaria_cli-0.2.3 → sovaria_cli-0.2.5}/apps/cli/src/utils/config.py +0 -0
- {sovaria_cli-0.2.3 → sovaria_cli-0.2.5}/apps/cli/src/utils/display.py +0 -0
- {sovaria_cli-0.2.3 → sovaria_cli-0.2.5}/apps/cli/src/utils/migrations.py +0 -0
- {sovaria_cli-0.2.3 → sovaria_cli-0.2.5}/apps/cli/src/utils/project_context.py +0 -0
- {sovaria_cli-0.2.3 → sovaria_cli-0.2.5}/apps/cli/src/utils/scope_picker.py +0 -0
- {sovaria_cli-0.2.3 → sovaria_cli-0.2.5}/apps/cli/tests/test_auth_store_profiles.py +0 -0
- {sovaria_cli-0.2.3 → sovaria_cli-0.2.5}/apps/cli/tests/test_cli_security.py +0 -0
- {sovaria_cli-0.2.3 → sovaria_cli-0.2.5}/apps/cli/tests/test_cli_smoke_paths.py +0 -0
- {sovaria_cli-0.2.3 → sovaria_cli-0.2.5}/apps/cli/tests/test_config_profiles.py +0 -0
- {sovaria_cli-0.2.3 → sovaria_cli-0.2.5}/apps/cli/tests/test_scope_picker.py +0 -0
- {sovaria_cli-0.2.3 → sovaria_cli-0.2.5}/pytest.ini +0 -0
- {sovaria_cli-0.2.3 → sovaria_cli-0.2.5}/requirements-dev.txt +0 -0
- {sovaria_cli-0.2.3 → sovaria_cli-0.2.5}/requirements.txt +0 -0
- {sovaria_cli-0.2.3 → sovaria_cli-0.2.5}/scripts/cli_sdk_happy_path.sh +0 -0
|
@@ -18,6 +18,143 @@ from apps.cli.src.utils.display import print_json, print_sql_result
|
|
|
18
18
|
|
|
19
19
|
app = typer.Typer(help="Database (SQL) management")
|
|
20
20
|
|
|
21
|
+
_INTERNAL_SQL_TABLES = ("sov_documents",)
|
|
22
|
+
_LIST_TABLES_SQL = (
|
|
23
|
+
"SELECT table_name FROM information_schema.tables "
|
|
24
|
+
"WHERE table_schema = current_schema() "
|
|
25
|
+
"AND table_type = 'BASE TABLE' "
|
|
26
|
+
f"AND table_name NOT IN ({', '.join(repr(name) for name in _INTERNAL_SQL_TABLES)}) "
|
|
27
|
+
"ORDER BY table_name;"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _begin_tx(
|
|
32
|
+
*,
|
|
33
|
+
profile: str | None,
|
|
34
|
+
org_id: str | None,
|
|
35
|
+
project_id: str | None,
|
|
36
|
+
env_id: str | None,
|
|
37
|
+
) -> int:
|
|
38
|
+
result = call_api(
|
|
39
|
+
method="POST",
|
|
40
|
+
path="/api/v1/data/rel/transaction/begin",
|
|
41
|
+
profile=profile,
|
|
42
|
+
org_id=org_id,
|
|
43
|
+
project_id=project_id,
|
|
44
|
+
env_id=env_id,
|
|
45
|
+
require_scope=True,
|
|
46
|
+
)
|
|
47
|
+
txid = result.get("txid") if isinstance(result, dict) else None
|
|
48
|
+
if not isinstance(txid, int) or txid < 1:
|
|
49
|
+
raise CLIRequestError(f"Invalid txid from transaction begin: {result!r}")
|
|
50
|
+
return txid
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _commit_tx(
|
|
54
|
+
txid: int,
|
|
55
|
+
*,
|
|
56
|
+
profile: str | None,
|
|
57
|
+
org_id: str | None,
|
|
58
|
+
project_id: str | None,
|
|
59
|
+
env_id: str | None,
|
|
60
|
+
) -> None:
|
|
61
|
+
call_api(
|
|
62
|
+
method="POST",
|
|
63
|
+
path="/api/v1/data/rel/transaction/commit",
|
|
64
|
+
profile=profile,
|
|
65
|
+
body={"txid": txid},
|
|
66
|
+
org_id=org_id,
|
|
67
|
+
project_id=project_id,
|
|
68
|
+
env_id=env_id,
|
|
69
|
+
require_scope=True,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _rollback_tx(
|
|
74
|
+
txid: int,
|
|
75
|
+
*,
|
|
76
|
+
profile: str | None,
|
|
77
|
+
org_id: str | None,
|
|
78
|
+
project_id: str | None,
|
|
79
|
+
env_id: str | None,
|
|
80
|
+
) -> None:
|
|
81
|
+
try:
|
|
82
|
+
call_api(
|
|
83
|
+
method="POST",
|
|
84
|
+
path="/api/v1/data/rel/transaction/rollback",
|
|
85
|
+
profile=profile,
|
|
86
|
+
body={"txid": txid},
|
|
87
|
+
org_id=org_id,
|
|
88
|
+
project_id=project_id,
|
|
89
|
+
env_id=env_id,
|
|
90
|
+
require_scope=True,
|
|
91
|
+
)
|
|
92
|
+
except CLIRequestError:
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _run_sql_query(
|
|
97
|
+
sql: str,
|
|
98
|
+
*,
|
|
99
|
+
profile: str | None,
|
|
100
|
+
org_id: str | None,
|
|
101
|
+
project_id: str | None,
|
|
102
|
+
env_id: str | None,
|
|
103
|
+
) -> dict:
|
|
104
|
+
scope = {
|
|
105
|
+
"profile": profile,
|
|
106
|
+
"org_id": org_id,
|
|
107
|
+
"project_id": project_id,
|
|
108
|
+
"env_id": env_id,
|
|
109
|
+
}
|
|
110
|
+
txid = _begin_tx(**scope)
|
|
111
|
+
try:
|
|
112
|
+
result = call_api(
|
|
113
|
+
method="POST",
|
|
114
|
+
path="/api/v1/sql/query",
|
|
115
|
+
profile=profile,
|
|
116
|
+
body={"sql": sql, "txid": txid},
|
|
117
|
+
org_id=org_id,
|
|
118
|
+
project_id=project_id,
|
|
119
|
+
env_id=env_id,
|
|
120
|
+
require_scope=True,
|
|
121
|
+
)
|
|
122
|
+
_commit_tx(txid, **scope)
|
|
123
|
+
return result if isinstance(result, dict) else {"rows": result}
|
|
124
|
+
except Exception:
|
|
125
|
+
_rollback_tx(txid, **scope)
|
|
126
|
+
raise
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _fetch_live_tables(
|
|
130
|
+
*,
|
|
131
|
+
profile: str | None,
|
|
132
|
+
org_id: str | None,
|
|
133
|
+
project_id: str | None,
|
|
134
|
+
env_id: str | None,
|
|
135
|
+
) -> list[str]:
|
|
136
|
+
result = _run_sql_query(
|
|
137
|
+
_LIST_TABLES_SQL,
|
|
138
|
+
profile=profile,
|
|
139
|
+
org_id=org_id,
|
|
140
|
+
project_id=project_id,
|
|
141
|
+
env_id=env_id,
|
|
142
|
+
)
|
|
143
|
+
rows = result.get("rows") if isinstance(result, dict) else None
|
|
144
|
+
if not isinstance(rows, list):
|
|
145
|
+
return []
|
|
146
|
+
tables: list[str] = []
|
|
147
|
+
for row in rows:
|
|
148
|
+
if isinstance(row, list) and row:
|
|
149
|
+
name = str(row[0]).strip()
|
|
150
|
+
elif isinstance(row, dict):
|
|
151
|
+
name = str(row.get("table_name") or row.get("TABLE_NAME") or "").strip()
|
|
152
|
+
else:
|
|
153
|
+
continue
|
|
154
|
+
if name and name not in _INTERNAL_SQL_TABLES:
|
|
155
|
+
tables.append(name)
|
|
156
|
+
return tables
|
|
157
|
+
|
|
21
158
|
|
|
22
159
|
@app.command("tables")
|
|
23
160
|
def list_tables(
|
|
@@ -25,22 +162,37 @@ def list_tables(
|
|
|
25
162
|
project_id: str | None = typer.Option(None, "--project", help="Project ID"),
|
|
26
163
|
env_id: str | None = typer.Option(None, "--env"),
|
|
27
164
|
profile: str | None = typer.Option(None, "--profile"),
|
|
165
|
+
migrations: bool = typer.Option(
|
|
166
|
+
False,
|
|
167
|
+
"--migrations",
|
|
168
|
+
help="Show migration log only (legacy behaviour)",
|
|
169
|
+
),
|
|
28
170
|
) -> None:
|
|
29
|
-
"""List relational tables
|
|
171
|
+
"""List live relational tables in the linked project database."""
|
|
30
172
|
try:
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
173
|
+
if migrations:
|
|
174
|
+
result = call_api(
|
|
175
|
+
method="GET",
|
|
176
|
+
path="/api/v1/data/rel/migrations",
|
|
177
|
+
profile=profile,
|
|
178
|
+
org_id=org_id,
|
|
179
|
+
project_id=project_id,
|
|
180
|
+
env_id=env_id,
|
|
181
|
+
require_scope=True,
|
|
182
|
+
)
|
|
183
|
+
print_json(result)
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
tables = _fetch_live_tables(
|
|
34
187
|
profile=profile,
|
|
35
188
|
org_id=org_id,
|
|
36
189
|
project_id=project_id,
|
|
37
190
|
env_id=env_id,
|
|
38
|
-
require_scope=True,
|
|
39
191
|
)
|
|
192
|
+
print_json({"tables": tables, "total": len(tables), "source": "information_schema"})
|
|
40
193
|
except CLIRequestError as exc:
|
|
41
194
|
typer.echo(str(exc), err=True)
|
|
42
195
|
raise typer.Exit(1) from exc
|
|
43
|
-
print_json(result)
|
|
44
196
|
|
|
45
197
|
|
|
46
198
|
@app.command("sql")
|
|
@@ -97,15 +249,12 @@ def _execute_sql(
|
|
|
97
249
|
env_id: str | None,
|
|
98
250
|
) -> None:
|
|
99
251
|
try:
|
|
100
|
-
result =
|
|
101
|
-
|
|
102
|
-
path="/api/v1/sql/query",
|
|
252
|
+
result = _run_sql_query(
|
|
253
|
+
sql,
|
|
103
254
|
profile=profile,
|
|
104
|
-
body={"sql": sql, "txid": 0},
|
|
105
255
|
org_id=org_id,
|
|
106
256
|
project_id=project_id,
|
|
107
257
|
env_id=env_id,
|
|
108
|
-
require_scope=True,
|
|
109
258
|
)
|
|
110
259
|
except CLIRequestError as exc:
|
|
111
260
|
typer.echo(str(exc), err=True)
|
|
@@ -473,11 +622,8 @@ def migrations_push(
|
|
|
473
622
|
continue
|
|
474
623
|
typer.echo(f"Applying {path.name} …")
|
|
475
624
|
try:
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
path="/api/v1/sql/query",
|
|
479
|
-
body={"sql": sql, "txid": 0},
|
|
480
|
-
require_scope=True,
|
|
625
|
+
_run_sql_query(
|
|
626
|
+
sql,
|
|
481
627
|
**_scope_opts(profile, org_id, project_id, env_id),
|
|
482
628
|
)
|
|
483
629
|
except CLIRequestError as exc:
|
|
@@ -56,6 +56,36 @@ def test_resolve_scope_requires_linked_project(monkeypatch: pytest.MonkeyPatch)
|
|
|
56
56
|
)
|
|
57
57
|
|
|
58
58
|
|
|
59
|
+
def test_db_tables_uses_information_schema(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
60
|
+
from apps.cli.src.commands import db
|
|
61
|
+
|
|
62
|
+
captured: dict[str, Any] = {}
|
|
63
|
+
calls: list[tuple[str, str]] = []
|
|
64
|
+
|
|
65
|
+
def _fake_call_api(**kwargs: Any) -> dict[str, Any]:
|
|
66
|
+
method = str(kwargs.get("method", ""))
|
|
67
|
+
path = str(kwargs.get("path", ""))
|
|
68
|
+
calls.append((method, path))
|
|
69
|
+
if path.endswith("/transaction/begin"):
|
|
70
|
+
return {"txid": 1, "snapshot_txid": 1}
|
|
71
|
+
if path.endswith("/sql/query"):
|
|
72
|
+
captured.update(kwargs)
|
|
73
|
+
return {"rows": [["users"], ["orders"]]}
|
|
74
|
+
if path.endswith("/transaction/commit"):
|
|
75
|
+
return {"ok": True}
|
|
76
|
+
return {}
|
|
77
|
+
|
|
78
|
+
monkeypatch.setattr(db, "call_api", _fake_call_api)
|
|
79
|
+
db.list_tables(profile=None, org_id=None, project_id=None, env_id=None, migrations=False)
|
|
80
|
+
|
|
81
|
+
assert captured["method"] == "POST"
|
|
82
|
+
assert captured["path"] == "/api/v1/sql/query"
|
|
83
|
+
assert "information_schema.tables" in captured["body"]["sql"]
|
|
84
|
+
assert captured["body"]["txid"] == 1
|
|
85
|
+
assert ("POST", "/api/v1/data/rel/transaction/begin") in calls
|
|
86
|
+
assert ("POST", "/api/v1/data/rel/transaction/commit") in calls
|
|
87
|
+
|
|
88
|
+
|
|
59
89
|
def test_sim_discover_path_smoke(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
60
90
|
from apps.cli.src.commands import sim
|
|
61
91
|
|
|
@@ -194,3 +224,34 @@ def test_storage_buckets_uses_call_api(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
|
194
224
|
assert captured["path"] == "/api/v1/store/buckets"
|
|
195
225
|
assert captured["require_scope"] is True
|
|
196
226
|
assert any("bucket-a" in line for line in output)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def test_fetch_live_tables_uses_transaction(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
230
|
+
from apps.cli.src.commands import db
|
|
231
|
+
|
|
232
|
+
calls: list[tuple[str, str]] = []
|
|
233
|
+
|
|
234
|
+
def _fake_call_api(**kwargs: Any) -> dict[str, Any]:
|
|
235
|
+
method = str(kwargs.get("method", ""))
|
|
236
|
+
path = str(kwargs.get("path", ""))
|
|
237
|
+
calls.append((method, path))
|
|
238
|
+
if path.endswith("/transaction/begin"):
|
|
239
|
+
return {"txid": 7, "snapshot_txid": 7}
|
|
240
|
+
if path.endswith("/sql/query"):
|
|
241
|
+
return {"rows": [["users"], ["posts"]]}
|
|
242
|
+
if path.endswith("/transaction/commit"):
|
|
243
|
+
return {"ok": True}
|
|
244
|
+
return {}
|
|
245
|
+
|
|
246
|
+
monkeypatch.setattr(db, "call_api", _fake_call_api)
|
|
247
|
+
tables = db._fetch_live_tables(
|
|
248
|
+
profile=None,
|
|
249
|
+
org_id="org-1",
|
|
250
|
+
project_id="proj-1",
|
|
251
|
+
env_id=None,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
assert tables == ["users", "posts"]
|
|
255
|
+
assert ("POST", "/api/v1/data/rel/transaction/begin") in calls
|
|
256
|
+
assert ("POST", "/api/v1/sql/query") in calls
|
|
257
|
+
assert ("POST", "/api/v1/data/rel/transaction/commit") in calls
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|