sovaria-cli 0.6.0__tar.gz → 0.7.0__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.6.0 → sovaria_cli-0.7.0}/PKG-INFO +1 -1
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/__init__.py +1 -1
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/db.py +1 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/import_cmd.py +8 -2
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/keys.py +7 -6
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/link_cmd.py +26 -2
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/oidc_providers.py +2 -2
- sovaria_cli-0.7.0/apps/cli/src/commands/orgs_projects.py +167 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/vector.py +3 -2
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/main.py +4 -2
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/pyproject.toml +1 -1
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/.gitignore +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/README.md +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/__init__.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/__init__.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/admin.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/ai_credits.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/ai_data.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/alerts.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/analytics.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/api.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/api_versions.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/audit.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/auth_enterprise.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/auth_product.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/auth_settings.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/billing.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/branches.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/builds.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/chrono.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/collections.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/compliance.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/connect.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/context.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/crashes.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/dashboard.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/data.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/deploy.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/distribute.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/env.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/experiments.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/fn_platform.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/frontends.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/gen.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/governance.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/hosting.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/iam.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/init_cmd.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/inspect.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/knowledge.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/local.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/login.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/logs.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/mcp.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/messaging.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/metrics.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/org.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/org_key.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/passkeys.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/perf.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/policy.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/project.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/push.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/queues.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/realtime.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/remote_config.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/rules.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/schema.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/scope.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/secrets.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/shell.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/sim.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/simulate.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/storage.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/templates.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/test_cmd.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/local/docker-compose.yml +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/local/initdb/001_axis_tables.sql +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/local/initdb/002_axis_base_schema.sql +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/local/initdb/003_guard_policies.sql +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/local/initdb/010_local_stack.sql +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/utils/__init__.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/utils/api_client.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/utils/api_helpers.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/utils/auth_store.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/utils/config.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/utils/display.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/utils/forge_local.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/utils/importers.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/utils/local_function_server.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/utils/local_stack.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/utils/log_tail.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/utils/loopback_host.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/utils/macho_symbols.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/utils/migrations.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/utils/mobile_types.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/utils/output_mode.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/utils/premium_display.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/utils/project_context.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/utils/schema_introspect.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/utils/scope_picker.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/utils/sim_offline.py +0 -0
- {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/utils/theme.py +0 -0
|
@@ -972,6 +972,7 @@ def _dry_run_in_order(items: list[tuple[Path, dict]], scope: dict[str, str | Non
|
|
|
972
972
|
print_success(f"All {len(items)} pending migration(s) apply cleanly in order. Nothing was changed.")
|
|
973
973
|
|
|
974
974
|
|
|
975
|
+
@app.command("push", help="Apply pending migrations (same as `sov db migrations push`, like `supabase db push`).")
|
|
975
976
|
@migrations_app.command("push")
|
|
976
977
|
def migrations_push(
|
|
977
978
|
directory: str | None = typer.Option(
|
|
@@ -543,7 +543,7 @@ def import_firebase(
|
|
|
543
543
|
if mapping and not dry_run:
|
|
544
544
|
Path(_UID_MAP).write_text(json.dumps(mapping, indent=2))
|
|
545
545
|
print_success(f"Users: {len(users)} read" + (f"; uid map in {_UID_MAP}" if mapping else ""))
|
|
546
|
-
elif "auth" in parts
|
|
546
|
+
elif "auth" in parts:
|
|
547
547
|
print_warning("Auth skipped: pass --auth-export (firebase auth:export users.json --format=json)")
|
|
548
548
|
|
|
549
549
|
if "firestore" in parts and sa:
|
|
@@ -562,8 +562,14 @@ def import_firebase(
|
|
|
562
562
|
report["rules"] = suggestions
|
|
563
563
|
Path("sovaria-rules-report.md").write_text(_rules_markdown(suggestions))
|
|
564
564
|
print_success(f"Rules: {len(suggestions)} rule(s) mapped in sovaria-rules-report.md (review, then add in Policies)")
|
|
565
|
-
if ("firestore" in parts or "storage" in parts) and not sa
|
|
565
|
+
if ("firestore" in parts or "storage" in parts) and not sa:
|
|
566
566
|
print_warning("Firestore/Storage skipped: pass --service-account")
|
|
567
|
+
if "rules" in parts and not rules and only:
|
|
568
|
+
print_warning("Rules skipped: pass --rules firestore.rules")
|
|
569
|
+
if not ({"auth", "firestore", "storage", "rules"} & set(report)):
|
|
570
|
+
typer.echo("Nothing was imported: pass --auth-export, --service-account and/or --rules "
|
|
571
|
+
"(see `sov import firebase --help`).", err=True)
|
|
572
|
+
raise typer.Exit(1)
|
|
567
573
|
except CLIRequestError as exc:
|
|
568
574
|
report["error"] = str(exc)
|
|
569
575
|
_write_report(report)
|
|
@@ -6,7 +6,7 @@ from urllib.parse import quote
|
|
|
6
6
|
|
|
7
7
|
import typer
|
|
8
8
|
|
|
9
|
-
from apps.cli.src.utils.api_client import CLIRequestError, call_api
|
|
9
|
+
from apps.cli.src.utils.api_client import CLIRequestError, call_api
|
|
10
10
|
from apps.cli.src.utils.display import handle_cli_error, print_json, print_success, print_warning
|
|
11
11
|
from apps.cli.src.utils.premium_display import run_with_spinner
|
|
12
12
|
|
|
@@ -49,7 +49,8 @@ def create_key(
|
|
|
49
49
|
name: str = typer.Option(..., "--name", help="Human-readable key name"),
|
|
50
50
|
role: str = typer.Option("viewer", "--role", help="admin, editor or viewer (read-only)"),
|
|
51
51
|
expires: str = typer.Option("never", "--expires", help="never, 30d, 90d or 1y"),
|
|
52
|
-
metadata: str | None = typer.Option(None, "--metadata",
|
|
52
|
+
metadata: str | None = typer.Option(None, "--metadata", hidden=True,
|
|
53
|
+
help="Not stored by the server (kept so old scripts still run)"),
|
|
53
54
|
profile: str | None = typer.Option(None, "--profile"),
|
|
54
55
|
org_id: str | None = typer.Option(None, "--org"),
|
|
55
56
|
project_id: str | None = typer.Option(None, "--project"),
|
|
@@ -72,9 +73,9 @@ def create_key(
|
|
|
72
73
|
expires = expires.strip().lower()
|
|
73
74
|
if expires not in _EXPIRIES:
|
|
74
75
|
raise typer.BadParameter("choose never, 30d, 90d or 1y", param_hint="--expires")
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
if metadata:
|
|
77
|
+
# API keys have no metadata field: the server dropped it silently (p32). Say so instead.
|
|
78
|
+
print_warning("--metadata is ignored: API keys don't store metadata (use --name to label the key)")
|
|
78
79
|
try:
|
|
79
80
|
result = run_with_spinner(
|
|
80
81
|
f"Creating API key '{name}'…",
|
|
@@ -86,7 +87,7 @@ def create_key(
|
|
|
86
87
|
project_id=project_id,
|
|
87
88
|
env_id=env_id,
|
|
88
89
|
require_scope=True,
|
|
89
|
-
body={"name": name, "kind": "custom", "role": role, "expires_in": expires
|
|
90
|
+
body={"name": name, "kind": "custom", "role": role, "expires_in": expires},
|
|
90
91
|
),
|
|
91
92
|
success="API key created",
|
|
92
93
|
)
|
|
@@ -7,7 +7,8 @@ from pathlib import Path
|
|
|
7
7
|
import typer
|
|
8
8
|
|
|
9
9
|
from apps.cli.src.utils.api_client import CLIRequestError
|
|
10
|
-
from apps.cli.src.utils.
|
|
10
|
+
from apps.cli.src.utils.auth_store import load_tokens
|
|
11
|
+
from apps.cli.src.utils.display import handle_cli_error, handle_command_error, print_success, print_warning
|
|
11
12
|
from apps.cli.src.utils.premium_display import (
|
|
12
13
|
print_brand_panel,
|
|
13
14
|
print_next_steps,
|
|
@@ -15,7 +16,7 @@ from apps.cli.src.utils.premium_display import (
|
|
|
15
16
|
run_with_spinner,
|
|
16
17
|
)
|
|
17
18
|
from apps.cli.src.utils.project_context import find_project_config, link_project_to_profile
|
|
18
|
-
from apps.cli.src.utils.scope_picker import interactive_pick_scope
|
|
19
|
+
from apps.cli.src.utils.scope_picker import fetch_projects, interactive_pick_scope
|
|
19
20
|
|
|
20
21
|
app = typer.Typer(help="Link local project to Sovaria cloud", invoke_without_command=True)
|
|
21
22
|
|
|
@@ -28,6 +29,27 @@ _LINK_NEXT_STEPS = [
|
|
|
28
29
|
]
|
|
29
30
|
|
|
30
31
|
|
|
32
|
+
def _verify_project(org_id: str, project_id: str, profile: str | None) -> None:
|
|
33
|
+
"""Refuse to link a project the signed-in user can't reach (a typo used to be written as-is and every later
|
|
34
|
+
command failed). Logged out (CI with a key, offline), the IDs are linked unchecked, with a warning."""
|
|
35
|
+
if not load_tokens(profile=profile):
|
|
36
|
+
print_warning("Not logged in: the org/project IDs were not checked (run `sov login` to check them).")
|
|
37
|
+
return
|
|
38
|
+
try:
|
|
39
|
+
projects = fetch_projects(org_id, profile)
|
|
40
|
+
except CLIRequestError as exc:
|
|
41
|
+
if exc.status_code in (401, 403, 404, 422):
|
|
42
|
+
typer.echo(f"You can't access organisation {org_id} ({exc}). Check --org, or run `sov orgs`.", err=True)
|
|
43
|
+
raise typer.Exit(1) from None
|
|
44
|
+
print_warning(f"Couldn't check the project ({exc}); linking anyway.")
|
|
45
|
+
return
|
|
46
|
+
ids = {str(p.get("node_id") or p.get("id") or "").lower() for p in projects}
|
|
47
|
+
if project_id.strip().lower() not in ids:
|
|
48
|
+
typer.echo(f"Project {project_id} isn't in organisation {org_id}. Run `sov projects {org_id}` to list them.",
|
|
49
|
+
err=True)
|
|
50
|
+
raise typer.Exit(1)
|
|
51
|
+
|
|
52
|
+
|
|
31
53
|
@app.callback()
|
|
32
54
|
def link_project(
|
|
33
55
|
org_id: str | None = typer.Option(None, "--org", help="Organisation UUID"),
|
|
@@ -87,6 +109,8 @@ def link_project(
|
|
|
87
109
|
typer.echo("No sovaria.config.json found. Run: sov init", err=True)
|
|
88
110
|
raise typer.Exit(1)
|
|
89
111
|
|
|
112
|
+
_verify_project(resolved_org, resolved_project, profile)
|
|
113
|
+
|
|
90
114
|
try:
|
|
91
115
|
path = link_project_to_profile(
|
|
92
116
|
org_id=resolved_org,
|
|
@@ -175,13 +175,13 @@ def login_url(
|
|
|
175
175
|
"Building OIDC login URL…",
|
|
176
176
|
lambda: call_api(
|
|
177
177
|
method="GET",
|
|
178
|
-
path=f"/api/v1/auth/
|
|
178
|
+
path=f"/api/v1/auth/sso/{quote(provider_id, safe='')}/authorize",
|
|
179
179
|
profile=profile,
|
|
180
180
|
org_id=resolved_org,
|
|
181
181
|
project_id=resolved_project,
|
|
182
182
|
env_id=env_id,
|
|
183
183
|
require_scope=True,
|
|
184
|
-
params={"project_id": resolved_project},
|
|
184
|
+
params={"project_id": resolved_project, "mode": "json"},
|
|
185
185
|
),
|
|
186
186
|
success="Login URL ready",
|
|
187
187
|
)
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""`sov orgs` and `sov projects`: list (the old forms keep working), `sov orgs get`, `sov projects create`.
|
|
2
|
+
|
|
3
|
+
`sov projects create` does what the console's New project dialog does: the project node, its owner edge, the
|
|
4
|
+
primary environment, and the database. It sends a region only when you pass `--region`; otherwise the server uses
|
|
5
|
+
the organization's default region (Organization settings › General), and Sovaria's default when the org has none.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import sys
|
|
11
|
+
from typing import Any
|
|
12
|
+
from urllib.parse import quote
|
|
13
|
+
|
|
14
|
+
import typer
|
|
15
|
+
from typer.core import TyperGroup
|
|
16
|
+
|
|
17
|
+
from apps.cli.src.commands import context as context_mod
|
|
18
|
+
from apps.cli.src.utils.api_client import CLIRequestError, call_api
|
|
19
|
+
from apps.cli.src.utils.api_helpers import require_login
|
|
20
|
+
from apps.cli.src.utils.config import get_scope
|
|
21
|
+
from apps.cli.src.utils.display import handle_cli_error, print_json, print_success, print_warning
|
|
22
|
+
from apps.cli.src.utils.output_mode import wants_json
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class _ListByDefault(TyperGroup):
|
|
26
|
+
"""`sov projects` / `sov projects <org_id>` run `list`; named subcommands run as usual."""
|
|
27
|
+
|
|
28
|
+
def resolve_command(self, ctx: Any, args: list[str]) -> Any:
|
|
29
|
+
if args and args[0] not in self.commands and not args[0].startswith("-"):
|
|
30
|
+
return "list", self.commands["list"], args
|
|
31
|
+
return super().resolve_command(ctx, args)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
orgs_app = typer.Typer(cls=_ListByDefault, invoke_without_command=True,
|
|
35
|
+
help="Your organisations: list (default) and get (with the default region)")
|
|
36
|
+
projects_app = typer.Typer(cls=_ListByDefault, invoke_without_command=True,
|
|
37
|
+
help="Projects: list (default) and create (in the org's default region unless --region)")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@orgs_app.callback()
|
|
41
|
+
def _orgs_default(ctx: typer.Context, profile: str | None = typer.Option(None, "--profile")) -> None:
|
|
42
|
+
if ctx.invoked_subcommand is None:
|
|
43
|
+
context_mod.list_orgs(profile=profile)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@projects_app.callback()
|
|
47
|
+
def _projects_default(ctx: typer.Context, profile: str | None = typer.Option(None, "--profile")) -> None:
|
|
48
|
+
if ctx.invoked_subcommand is None:
|
|
49
|
+
context_mod.list_projects(org_id=None, profile=profile)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
orgs_app.command("list")(context_mod.list_orgs)
|
|
53
|
+
projects_app.command("list")(context_mod.list_projects)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _settings(node: dict[str, Any]) -> dict[str, Any]:
|
|
57
|
+
for key in ("metadata_json", "metadata"):
|
|
58
|
+
meta = node.get(key)
|
|
59
|
+
if isinstance(meta, dict) and isinstance(meta.get("settings"), dict):
|
|
60
|
+
return meta["settings"]
|
|
61
|
+
return {}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def org_default_region(org_id: str, profile: str | None) -> str | None:
|
|
65
|
+
"""The organization's default region (Organization settings › General), or None when it has none."""
|
|
66
|
+
node = call_api(method="GET", path=f"/api/v1/org/{quote(org_id, safe='')}", profile=profile)
|
|
67
|
+
if not isinstance(node, dict):
|
|
68
|
+
return None
|
|
69
|
+
return str(_settings(node).get("default_region") or "").strip() or None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@orgs_app.command("get")
|
|
73
|
+
def get_org(
|
|
74
|
+
org_id: str = typer.Argument(..., help="Organisation ID"),
|
|
75
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
76
|
+
) -> None:
|
|
77
|
+
"""An organisation, with its default region (where new projects go unless you pick one)."""
|
|
78
|
+
require_login(profile)
|
|
79
|
+
try:
|
|
80
|
+
node = call_api(method="GET", path=f"/api/v1/org/{quote(org_id, safe='')}", profile=profile)
|
|
81
|
+
except CLIRequestError as exc:
|
|
82
|
+
handle_cli_error(exc)
|
|
83
|
+
return
|
|
84
|
+
region = str(_settings(node if isinstance(node, dict) else {}).get("default_region") or "").strip()
|
|
85
|
+
if wants_json():
|
|
86
|
+
print_json({**(node if isinstance(node, dict) else {}), "default_region": region or None})
|
|
87
|
+
return
|
|
88
|
+
print_json(node, title=f"Organisation {org_id[:8]}…")
|
|
89
|
+
typer.echo(f"Default region: {region or 'not set (new projects use Sovaria’s default region)'}")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _node_id(data: Any, step: str) -> str:
|
|
93
|
+
node_id = str((data or {}).get("node_id") or (data or {}).get("id") or "") if isinstance(data, dict) else ""
|
|
94
|
+
if not node_id:
|
|
95
|
+
raise CLIRequestError(f"{step}: the server returned no id")
|
|
96
|
+
return node_id
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@projects_app.command("create")
|
|
100
|
+
def create_project(
|
|
101
|
+
name: str = typer.Argument(..., help="Project name"),
|
|
102
|
+
org_id: str | None = typer.Option(None, "--org", help="Organisation ID (default: your current scope)"),
|
|
103
|
+
region: str | None = typer.Option(
|
|
104
|
+
None, "--region", help="Region for the database; omit to use the organisation's default region"),
|
|
105
|
+
env_name: str = typer.Option("production", "--env-name", help="Name of the primary environment"),
|
|
106
|
+
db_password_stdin: bool = typer.Option(
|
|
107
|
+
False, "--db-password-stdin", help="Read the database password (12–72 characters) from stdin; random otherwise"),
|
|
108
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
109
|
+
) -> None:
|
|
110
|
+
"""Create a cloud project: the project, its primary environment and its database (like the console)."""
|
|
111
|
+
require_login(profile)
|
|
112
|
+
org = (org_id or get_scope(profile=profile).get("org_id") or "").strip()
|
|
113
|
+
if not org:
|
|
114
|
+
typer.echo("Pass --org, or pick a scope first (sov scope pick).", err=True)
|
|
115
|
+
raise typer.Exit(1)
|
|
116
|
+
password = sys.stdin.readline().rstrip("\n") if db_password_stdin else ""
|
|
117
|
+
if db_password_stdin and not 12 <= len(password) <= 72:
|
|
118
|
+
raise typer.BadParameter("the database password must be 12–72 characters", param_hint="--db-password-stdin")
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
default_region = None if region else org_default_region(org, profile)
|
|
122
|
+
except CLIRequestError as exc:
|
|
123
|
+
handle_cli_error(exc)
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
created: dict[str, str] = {}
|
|
127
|
+
try:
|
|
128
|
+
project = call_api(method="POST", path="/api/v1/org", profile=profile, body={
|
|
129
|
+
"type": "PROJECT", "name": name.strip(), "metadata": {"org_id": org},
|
|
130
|
+
**({"database_password": password} if password else {}),
|
|
131
|
+
})
|
|
132
|
+
created["project_id"] = _node_id(project, "Creating the project")
|
|
133
|
+
call_api(method="POST", path="/api/v1/edge", profile=profile, body={
|
|
134
|
+
"source_id": org, "target_id": created["project_id"], "relation_type": "OWNS", "role": "OWNER"})
|
|
135
|
+
env = call_api(method="POST", path="/api/v1/org", profile=profile, body={
|
|
136
|
+
"type": "ENV", "name": env_name.strip() or "production", "metadata": {"db_type": "hybrid"}})
|
|
137
|
+
created["env_id"] = _node_id(env, "Creating the environment")
|
|
138
|
+
call_api(method="POST", path="/api/v1/edge", profile=profile, body={
|
|
139
|
+
"source_id": created["project_id"], "target_id": created["env_id"], "relation_type": "HAS_ENV",
|
|
140
|
+
"role": "PRIMARY"})
|
|
141
|
+
# No region unless asked: the server places the database in the org's default region.
|
|
142
|
+
database = call_api(
|
|
143
|
+
method="POST", path=f"/api/v1/governance/tenants/{quote(org, safe='')}/databases/provision",
|
|
144
|
+
profile=profile, timeout=120,
|
|
145
|
+
body={"project_id": created["project_id"], "tier": "dedicated", **({"region": region} if region else {})})
|
|
146
|
+
except CLIRequestError as exc:
|
|
147
|
+
if created.get("project_id"):
|
|
148
|
+
print_warning(f"Stopped part way: project {created['project_id']} exists"
|
|
149
|
+
+ (f" with environment {created['env_id']}" if created.get("env_id") else "")
|
|
150
|
+
+ ". Finish it in the console, or remove it: sov api call DELETE /api/v1/org/"
|
|
151
|
+
+ created["project_id"])
|
|
152
|
+
handle_cli_error(exc)
|
|
153
|
+
return
|
|
154
|
+
|
|
155
|
+
placed = (database or {}).get("region") if isinstance(database, dict) else None
|
|
156
|
+
where = region or placed or default_region
|
|
157
|
+
source = ("--region" if region else "the organisation's default region" if default_region
|
|
158
|
+
else "Sovaria's default region (the organisation has none set)")
|
|
159
|
+
out = {"org_id": org, **created, "name": name.strip(), "env_name": env_name, "region": where,
|
|
160
|
+
"region_source": source, "database": database}
|
|
161
|
+
if wants_json():
|
|
162
|
+
print_json(out)
|
|
163
|
+
return
|
|
164
|
+
print_success(f"Project created: {created['project_id']}")
|
|
165
|
+
typer.echo(f"Environment: {env_name} ({created['env_id']})")
|
|
166
|
+
typer.echo(f"Region: {where or 'chosen by the server'} — from {source}")
|
|
167
|
+
typer.echo(f"Next: sov link --org {org} --project {created['project_id']}")
|
|
@@ -121,7 +121,8 @@ def app_search(
|
|
|
121
121
|
@app.command("reembed")
|
|
122
122
|
def reembed(
|
|
123
123
|
collection: str = typer.Argument(..., help="Collection name"),
|
|
124
|
-
force: bool = typer.Option(False, "--force",
|
|
124
|
+
force: bool = typer.Option(False, "--force", hidden=True,
|
|
125
|
+
help="Not needed: an explicit re-embed always covers the whole collection (uses quota)"),
|
|
125
126
|
org_id: str | None = typer.Option(None, "--org"),
|
|
126
127
|
project_id: str | None = typer.Option(None, "--project"),
|
|
127
128
|
env_id: str | None = typer.Option(None, "--env"),
|
|
@@ -131,7 +132,7 @@ def reembed(
|
|
|
131
132
|
result = run_with_spinner(
|
|
132
133
|
f"Re-embedding '{collection}'…",
|
|
133
134
|
lambda: _call("POST", f"/api/v1/collections/{quote(collection, safe='')}/reembed", org_id=org_id,
|
|
134
|
-
project_id=project_id, env_id=env_id, profile=profile
|
|
135
|
+
project_id=project_id, env_id=env_id, profile=profile),
|
|
135
136
|
success="Re-embedded",
|
|
136
137
|
)
|
|
137
138
|
print_json(result)
|
|
@@ -221,8 +221,10 @@ app.add_typer(shell_mod.app, name="shell", help="Interactive Sovaria REPL")
|
|
|
221
221
|
app.command("login")(login_mod.login)
|
|
222
222
|
app.command("logout")(login_mod.logout)
|
|
223
223
|
app.command("whoami")(context_mod.whoami)
|
|
224
|
-
|
|
225
|
-
|
|
224
|
+
from apps.cli.src.commands import orgs_projects as orgs_projects_mod # noqa: E402
|
|
225
|
+
|
|
226
|
+
app.add_typer(orgs_projects_mod.orgs_app, name="orgs")
|
|
227
|
+
app.add_typer(orgs_projects_mod.projects_app, name="projects")
|
|
226
228
|
app.command("context")(context_mod.context)
|
|
227
229
|
app.command("dashboard")(dashboard_mod.show_dashboard)
|
|
228
230
|
app.command("connect")(connect_mod.show_connect)
|
|
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
|
|
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
|