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,187 @@
|
|
|
1
|
+
"""Project-scoped product auth admin commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TypedDict
|
|
6
|
+
from urllib.parse import quote
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from apps.cli.src.utils.api_client import CLIRequestError, call_api, print_json
|
|
11
|
+
from apps.cli.src.utils.config import get_scope
|
|
12
|
+
|
|
13
|
+
app = typer.Typer(help="Manage product users within an org/project scope")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ScopeArgs(TypedDict):
|
|
17
|
+
org_id: str | None
|
|
18
|
+
project_id: str | None
|
|
19
|
+
env_id: str | None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _scope_args(
|
|
23
|
+
org_id: str | None,
|
|
24
|
+
project_id: str | None,
|
|
25
|
+
env_id: str | None,
|
|
26
|
+
) -> ScopeArgs:
|
|
27
|
+
return {
|
|
28
|
+
"org_id": org_id,
|
|
29
|
+
"project_id": project_id,
|
|
30
|
+
"env_id": env_id,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _project_auth_path(profile: str | None, project_id: str | None, suffix: str) -> str:
|
|
35
|
+
resolved_project = (project_id or get_scope(profile).get("project_id") or "").strip()
|
|
36
|
+
if not resolved_project:
|
|
37
|
+
raise CLIRequestError(
|
|
38
|
+
"Missing required project scope. Set --project or run: sov scope set <org_id> <project_id>"
|
|
39
|
+
)
|
|
40
|
+
return f"/api/v1/projects/{quote(resolved_project, safe='')}/auth{suffix}"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@app.command("users")
|
|
44
|
+
def list_users(
|
|
45
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
46
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
47
|
+
project_id: str | None = typer.Option(None, "--project"),
|
|
48
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
49
|
+
) -> None:
|
|
50
|
+
"""List product users for the resolved org/project scope."""
|
|
51
|
+
try:
|
|
52
|
+
result = call_api(
|
|
53
|
+
method="GET",
|
|
54
|
+
path=_project_auth_path(profile, project_id, "/users"),
|
|
55
|
+
profile=profile,
|
|
56
|
+
require_scope=True,
|
|
57
|
+
**_scope_args(org_id, project_id, env_id),
|
|
58
|
+
)
|
|
59
|
+
except CLIRequestError as exc:
|
|
60
|
+
typer.echo(str(exc), err=True)
|
|
61
|
+
raise typer.Exit(1) from exc
|
|
62
|
+
typer.echo(print_json(result))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@app.command("ban")
|
|
66
|
+
def ban_user(
|
|
67
|
+
user_id: str = typer.Argument(..., help="Product user id to ban/unban"),
|
|
68
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
69
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
70
|
+
project_id: str | None = typer.Option(None, "--project"),
|
|
71
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
72
|
+
) -> None:
|
|
73
|
+
"""Toggle a product user's active state in scope."""
|
|
74
|
+
safe_user_id = quote(user_id, safe="")
|
|
75
|
+
try:
|
|
76
|
+
result = call_api(
|
|
77
|
+
method="POST",
|
|
78
|
+
path=_project_auth_path(profile, project_id, f"/users/{safe_user_id}/ban"),
|
|
79
|
+
profile=profile,
|
|
80
|
+
require_scope=True,
|
|
81
|
+
**_scope_args(org_id, project_id, env_id),
|
|
82
|
+
)
|
|
83
|
+
except CLIRequestError as exc:
|
|
84
|
+
typer.echo(str(exc), err=True)
|
|
85
|
+
raise typer.Exit(1) from exc
|
|
86
|
+
typer.echo(print_json(result))
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@app.command("delete")
|
|
90
|
+
def delete_user(
|
|
91
|
+
user_id: str = typer.Argument(..., help="Product user id to delete"),
|
|
92
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
93
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
94
|
+
project_id: str | None = typer.Option(None, "--project"),
|
|
95
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
96
|
+
) -> None:
|
|
97
|
+
"""Delete a product user credential in scope."""
|
|
98
|
+
safe_user_id = quote(user_id, safe="")
|
|
99
|
+
try:
|
|
100
|
+
result = call_api(
|
|
101
|
+
method="DELETE",
|
|
102
|
+
path=_project_auth_path(profile, project_id, f"/users/{safe_user_id}"),
|
|
103
|
+
profile=profile,
|
|
104
|
+
require_scope=True,
|
|
105
|
+
**_scope_args(org_id, project_id, env_id),
|
|
106
|
+
)
|
|
107
|
+
except CLIRequestError as exc:
|
|
108
|
+
typer.echo(str(exc), err=True)
|
|
109
|
+
raise typer.Exit(1) from exc
|
|
110
|
+
typer.echo(print_json(result))
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@app.command("roles")
|
|
114
|
+
def get_roles(
|
|
115
|
+
user_id: str = typer.Argument(..., help="Product user id"),
|
|
116
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
117
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
118
|
+
project_id: str | None = typer.Option(None, "--project"),
|
|
119
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
120
|
+
) -> None:
|
|
121
|
+
"""Show a product user's app roles in scope."""
|
|
122
|
+
safe_user_id = quote(user_id, safe="")
|
|
123
|
+
try:
|
|
124
|
+
result = call_api(
|
|
125
|
+
method="GET",
|
|
126
|
+
path=_project_auth_path(profile, project_id, f"/users/{safe_user_id}/roles"),
|
|
127
|
+
profile=profile,
|
|
128
|
+
require_scope=True,
|
|
129
|
+
**_scope_args(org_id, project_id, env_id),
|
|
130
|
+
)
|
|
131
|
+
except CLIRequestError as exc:
|
|
132
|
+
typer.echo(str(exc), err=True)
|
|
133
|
+
raise typer.Exit(1) from exc
|
|
134
|
+
typer.echo(print_json(result))
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@app.command("roles-set")
|
|
138
|
+
def set_roles(
|
|
139
|
+
user_id: str = typer.Argument(..., help="Product user id"),
|
|
140
|
+
roles: list[str] = typer.Argument(..., help="Roles to assign"),
|
|
141
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
142
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
143
|
+
project_id: str | None = typer.Option(None, "--project"),
|
|
144
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
145
|
+
) -> None:
|
|
146
|
+
"""Replace a product user's app roles in scope."""
|
|
147
|
+
safe_user_id = quote(user_id, safe="")
|
|
148
|
+
try:
|
|
149
|
+
result = call_api(
|
|
150
|
+
method="PUT",
|
|
151
|
+
path=_project_auth_path(profile, project_id, f"/users/{safe_user_id}/roles"),
|
|
152
|
+
profile=profile,
|
|
153
|
+
body={"roles": roles},
|
|
154
|
+
require_scope=True,
|
|
155
|
+
**_scope_args(org_id, project_id, env_id),
|
|
156
|
+
)
|
|
157
|
+
except CLIRequestError as exc:
|
|
158
|
+
typer.echo(str(exc), err=True)
|
|
159
|
+
raise typer.Exit(1) from exc
|
|
160
|
+
typer.echo(print_json(result))
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@app.command("create")
|
|
164
|
+
def create_user(
|
|
165
|
+
email: str = typer.Option(..., "--email"),
|
|
166
|
+
password: str = typer.Option(..., "--password"),
|
|
167
|
+
name: str | None = typer.Option(None, "--name", help="Display name (defaults to email local-part)"),
|
|
168
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
169
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
170
|
+
project_id: str | None = typer.Option(None, "--project"),
|
|
171
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
172
|
+
) -> None:
|
|
173
|
+
"""Create a product user in the current project scope."""
|
|
174
|
+
display_name = name or email.split("@", maxsplit=1)[0]
|
|
175
|
+
try:
|
|
176
|
+
result = call_api(
|
|
177
|
+
method="POST",
|
|
178
|
+
path=_project_auth_path(profile, project_id, "/users"),
|
|
179
|
+
profile=profile,
|
|
180
|
+
require_scope=True,
|
|
181
|
+
**_scope_args(org_id, project_id, env_id),
|
|
182
|
+
body={"name": display_name, "email": email, "password": password},
|
|
183
|
+
)
|
|
184
|
+
except CLIRequestError as exc:
|
|
185
|
+
typer.echo(str(exc), err=True)
|
|
186
|
+
raise typer.Exit(1) from exc
|
|
187
|
+
typer.echo(print_json(result))
|
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
"""Ledger billing commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from apps.cli.src.utils.api_client import CLIRequestError, call_api, print_json
|
|
8
|
+
|
|
9
|
+
app = typer.Typer(help="Billing and subscription commands (Ledger engine)")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@app.command("plans")
|
|
13
|
+
def list_plans(
|
|
14
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
15
|
+
) -> None:
|
|
16
|
+
"""List public billing plans."""
|
|
17
|
+
try:
|
|
18
|
+
result = call_api(
|
|
19
|
+
method="GET",
|
|
20
|
+
path="/api/v1/plans",
|
|
21
|
+
profile=profile,
|
|
22
|
+
require_auth=False,
|
|
23
|
+
)
|
|
24
|
+
except CLIRequestError as exc:
|
|
25
|
+
typer.echo(str(exc), err=True)
|
|
26
|
+
raise typer.Exit(1) from exc
|
|
27
|
+
typer.echo(print_json(result))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@app.command("plan")
|
|
31
|
+
def get_plan(
|
|
32
|
+
name: str = typer.Argument(..., help="Plan name"),
|
|
33
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
34
|
+
) -> None:
|
|
35
|
+
"""Fetch one billing plan by name."""
|
|
36
|
+
from urllib.parse import quote
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
result = call_api(
|
|
40
|
+
method="GET",
|
|
41
|
+
path=f"/api/v1/plans/{quote(name, safe='')}",
|
|
42
|
+
profile=profile,
|
|
43
|
+
require_auth=False,
|
|
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("subscription")
|
|
52
|
+
def get_subscription(
|
|
53
|
+
org_id: str = typer.Argument(..., help="Organisation ID"),
|
|
54
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
55
|
+
) -> None:
|
|
56
|
+
"""Fetch the active subscription for an organisation."""
|
|
57
|
+
from urllib.parse import quote
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
result = call_api(
|
|
61
|
+
method="GET",
|
|
62
|
+
path=f"/api/v1/subscriptions/{quote(org_id, safe='')}",
|
|
63
|
+
profile=profile,
|
|
64
|
+
require_scope=False,
|
|
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("set-subscription")
|
|
73
|
+
def set_subscription(
|
|
74
|
+
org_id: str = typer.Argument(..., help="Organisation ID"),
|
|
75
|
+
plan: str = typer.Option(..., "--plan", help="Plan name"),
|
|
76
|
+
billing_cycle: str = typer.Option("monthly", "--cycle", help="monthly or annual"),
|
|
77
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
78
|
+
) -> None:
|
|
79
|
+
"""Create or update an organisation subscription."""
|
|
80
|
+
from urllib.parse import quote
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
result = call_api(
|
|
84
|
+
method="PUT",
|
|
85
|
+
path=f"/api/v1/subscriptions/{quote(org_id, safe='')}",
|
|
86
|
+
profile=profile,
|
|
87
|
+
require_scope=False,
|
|
88
|
+
body={"plan": plan, "billing_cycle": billing_cycle, "metadata": {}},
|
|
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("cancel-subscription")
|
|
97
|
+
def cancel_subscription(
|
|
98
|
+
org_id: str = typer.Argument(..., help="Organisation ID"),
|
|
99
|
+
immediate: bool = typer.Option(False, "--immediate", help="Cancel immediately"),
|
|
100
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
101
|
+
) -> None:
|
|
102
|
+
"""Cancel an organisation subscription."""
|
|
103
|
+
from urllib.parse import quote
|
|
104
|
+
|
|
105
|
+
params = {"immediate": "true"} if immediate else None
|
|
106
|
+
try:
|
|
107
|
+
result = call_api(
|
|
108
|
+
method="DELETE",
|
|
109
|
+
path=f"/api/v1/subscriptions/{quote(org_id, safe='')}",
|
|
110
|
+
profile=profile,
|
|
111
|
+
require_scope=False,
|
|
112
|
+
params=params,
|
|
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("usage")
|
|
121
|
+
def get_usage(
|
|
122
|
+
org_id: str = typer.Argument(..., help="Organisation ID"),
|
|
123
|
+
period: str | None = typer.Option(None, "--period", help="Usage period"),
|
|
124
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
125
|
+
) -> None:
|
|
126
|
+
"""Fetch usage counters for an organisation."""
|
|
127
|
+
from urllib.parse import quote
|
|
128
|
+
|
|
129
|
+
params = {"period": period} if period else None
|
|
130
|
+
try:
|
|
131
|
+
result = call_api(
|
|
132
|
+
method="GET",
|
|
133
|
+
path=f"/api/v1/usage/{quote(org_id, safe='')}",
|
|
134
|
+
profile=profile,
|
|
135
|
+
require_scope=False,
|
|
136
|
+
params=params,
|
|
137
|
+
)
|
|
138
|
+
except CLIRequestError as exc:
|
|
139
|
+
typer.echo(str(exc), err=True)
|
|
140
|
+
raise typer.Exit(1) from exc
|
|
141
|
+
typer.echo(print_json(result))
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@app.command("usage-history")
|
|
145
|
+
def usage_history(
|
|
146
|
+
org_id: str = typer.Argument(..., help="Organisation ID"),
|
|
147
|
+
months: int = typer.Option(6, "--months", min=1, max=24),
|
|
148
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
149
|
+
) -> None:
|
|
150
|
+
"""Fetch historical usage for an organisation."""
|
|
151
|
+
from urllib.parse import quote
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
result = call_api(
|
|
155
|
+
method="GET",
|
|
156
|
+
path=f"/api/v1/usage/{quote(org_id, safe='')}/history",
|
|
157
|
+
profile=profile,
|
|
158
|
+
require_scope=False,
|
|
159
|
+
params={"months": str(months)},
|
|
160
|
+
)
|
|
161
|
+
except CLIRequestError as exc:
|
|
162
|
+
typer.echo(str(exc), err=True)
|
|
163
|
+
raise typer.Exit(1) from exc
|
|
164
|
+
typer.echo(print_json(result))
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@app.command("subscriptions")
|
|
168
|
+
def list_subscriptions(
|
|
169
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
170
|
+
) -> None:
|
|
171
|
+
"""List all subscriptions (platform admin)."""
|
|
172
|
+
try:
|
|
173
|
+
result = call_api(
|
|
174
|
+
method="GET",
|
|
175
|
+
path="/api/v1/subscriptions",
|
|
176
|
+
profile=profile,
|
|
177
|
+
require_scope=False,
|
|
178
|
+
)
|
|
179
|
+
except CLIRequestError as exc:
|
|
180
|
+
typer.echo(str(exc), err=True)
|
|
181
|
+
raise typer.Exit(1) from exc
|
|
182
|
+
typer.echo(print_json(result))
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@app.command("invoices")
|
|
186
|
+
def list_invoices(
|
|
187
|
+
org_id: str = typer.Argument(..., help="Organisation ID"),
|
|
188
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
189
|
+
) -> None:
|
|
190
|
+
"""List invoices for an organisation."""
|
|
191
|
+
from urllib.parse import quote
|
|
192
|
+
|
|
193
|
+
try:
|
|
194
|
+
result = call_api(
|
|
195
|
+
method="GET",
|
|
196
|
+
path=f"/api/v1/invoices/{quote(org_id, safe='')}",
|
|
197
|
+
profile=profile,
|
|
198
|
+
require_scope=False,
|
|
199
|
+
)
|
|
200
|
+
except CLIRequestError as exc:
|
|
201
|
+
typer.echo(str(exc), err=True)
|
|
202
|
+
raise typer.Exit(1) from exc
|
|
203
|
+
typer.echo(print_json(result))
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@app.command("compute")
|
|
207
|
+
def get_compute(
|
|
208
|
+
org_id: str = typer.Argument(..., help="Organisation ID"),
|
|
209
|
+
period: str | None = typer.Option(None, "--period", help="Billing period YYYY-MM"),
|
|
210
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
211
|
+
) -> None:
|
|
212
|
+
"""Fetch compute usage breakdown for an organisation."""
|
|
213
|
+
from urllib.parse import quote
|
|
214
|
+
|
|
215
|
+
params = {"period": period} if period else None
|
|
216
|
+
try:
|
|
217
|
+
result = call_api(
|
|
218
|
+
method="GET",
|
|
219
|
+
path=f"/api/v1/compute/{quote(org_id, safe='')}",
|
|
220
|
+
profile=profile,
|
|
221
|
+
require_scope=False,
|
|
222
|
+
params=params,
|
|
223
|
+
)
|
|
224
|
+
except CLIRequestError as exc:
|
|
225
|
+
typer.echo(str(exc), err=True)
|
|
226
|
+
raise typer.Exit(1) from exc
|
|
227
|
+
typer.echo(print_json(result))
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@app.command("build-invoice")
|
|
231
|
+
def build_invoice(
|
|
232
|
+
org_id: str = typer.Argument(..., help="Organisation ID"),
|
|
233
|
+
period: str | None = typer.Option(None, "--period", help="Billing period YYYY-MM"),
|
|
234
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
235
|
+
) -> None:
|
|
236
|
+
"""Build an invoice for an organisation billing period."""
|
|
237
|
+
from urllib.parse import quote
|
|
238
|
+
|
|
239
|
+
params = {"period": period} if period else None
|
|
240
|
+
try:
|
|
241
|
+
result = call_api(
|
|
242
|
+
method="POST",
|
|
243
|
+
path=f"/api/v1/invoices/{quote(org_id, safe='')}/build",
|
|
244
|
+
profile=profile,
|
|
245
|
+
require_scope=False,
|
|
246
|
+
params=params,
|
|
247
|
+
body={},
|
|
248
|
+
)
|
|
249
|
+
except CLIRequestError as exc:
|
|
250
|
+
typer.echo(str(exc), err=True)
|
|
251
|
+
raise typer.Exit(1) from exc
|
|
252
|
+
typer.echo(print_json(result))
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
@app.command("checkout")
|
|
256
|
+
def checkout(
|
|
257
|
+
plan: str = typer.Option(..., "--plan", help="Plan name (pro|ultra)"),
|
|
258
|
+
billing_cycle: str = typer.Option("monthly", "--cycle", help="monthly or annual"),
|
|
259
|
+
customer_email: str | None = typer.Option(None, "--email"),
|
|
260
|
+
provider: str | None = typer.Option(None, "--provider", help="stripe or razorpay"),
|
|
261
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
262
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
263
|
+
) -> None:
|
|
264
|
+
"""Create a hosted billing checkout session."""
|
|
265
|
+
body: dict[str, object] = {"plan": plan, "billing_cycle": billing_cycle}
|
|
266
|
+
if customer_email:
|
|
267
|
+
body["customer_email"] = customer_email
|
|
268
|
+
if provider:
|
|
269
|
+
body["provider"] = provider
|
|
270
|
+
try:
|
|
271
|
+
result = call_api(
|
|
272
|
+
method="POST",
|
|
273
|
+
path="/api/v1/billing/checkout",
|
|
274
|
+
profile=profile,
|
|
275
|
+
org_id=org_id,
|
|
276
|
+
require_scope=False,
|
|
277
|
+
body=body,
|
|
278
|
+
)
|
|
279
|
+
except CLIRequestError as exc:
|
|
280
|
+
typer.echo(str(exc), err=True)
|
|
281
|
+
raise typer.Exit(1) from exc
|
|
282
|
+
typer.echo(print_json(result))
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
@app.command("provider")
|
|
286
|
+
def billing_provider(
|
|
287
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
288
|
+
) -> None:
|
|
289
|
+
"""Show the active billing payment provider."""
|
|
290
|
+
try:
|
|
291
|
+
result = call_api(
|
|
292
|
+
method="GET",
|
|
293
|
+
path="/api/v1/billing/provider",
|
|
294
|
+
profile=profile,
|
|
295
|
+
require_scope=False,
|
|
296
|
+
)
|
|
297
|
+
except CLIRequestError as exc:
|
|
298
|
+
typer.echo(str(exc), err=True)
|
|
299
|
+
raise typer.Exit(1) from exc
|
|
300
|
+
typer.echo(print_json(result))
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
@app.command("fx")
|
|
304
|
+
def billing_fx(
|
|
305
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
306
|
+
) -> None:
|
|
307
|
+
"""Fetch billing FX rates."""
|
|
308
|
+
try:
|
|
309
|
+
result = call_api(
|
|
310
|
+
method="GET",
|
|
311
|
+
path="/api/v1/billing/fx",
|
|
312
|
+
profile=profile,
|
|
313
|
+
require_scope=False,
|
|
314
|
+
)
|
|
315
|
+
except CLIRequestError as exc:
|
|
316
|
+
typer.echo(str(exc), err=True)
|
|
317
|
+
raise typer.Exit(1) from exc
|
|
318
|
+
typer.echo(print_json(result))
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
@app.command("overage")
|
|
322
|
+
def billing_overage(
|
|
323
|
+
org_id: str = typer.Argument(..., help="Organisation ID"),
|
|
324
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
325
|
+
) -> None:
|
|
326
|
+
"""Fetch overage billing details for an organisation."""
|
|
327
|
+
from urllib.parse import quote
|
|
328
|
+
|
|
329
|
+
try:
|
|
330
|
+
result = call_api(
|
|
331
|
+
method="GET",
|
|
332
|
+
path=f"/api/v1/billing/overage/{quote(org_id, safe='')}",
|
|
333
|
+
profile=profile,
|
|
334
|
+
require_scope=False,
|
|
335
|
+
)
|
|
336
|
+
except CLIRequestError as exc:
|
|
337
|
+
typer.echo(str(exc), err=True)
|
|
338
|
+
raise typer.Exit(1) from exc
|
|
339
|
+
typer.echo(print_json(result))
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
@app.command("open-invoice")
|
|
343
|
+
def open_invoice(
|
|
344
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
345
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
346
|
+
) -> None:
|
|
347
|
+
"""Fetch the caller's open invoice for the scoped organisation."""
|
|
348
|
+
try:
|
|
349
|
+
result = call_api(
|
|
350
|
+
method="GET",
|
|
351
|
+
path="/api/v1/billing/invoices/open",
|
|
352
|
+
profile=profile,
|
|
353
|
+
org_id=org_id,
|
|
354
|
+
require_scope=False,
|
|
355
|
+
)
|
|
356
|
+
except CLIRequestError as exc:
|
|
357
|
+
typer.echo(str(exc), err=True)
|
|
358
|
+
raise typer.Exit(1) from exc
|
|
359
|
+
typer.echo(print_json(result))
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
@app.command("pay-invoice")
|
|
363
|
+
def pay_invoice(
|
|
364
|
+
org_id: str = typer.Argument(..., help="Organisation ID"),
|
|
365
|
+
period: str = typer.Argument(..., help="Invoice period YYYY-MM"),
|
|
366
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
367
|
+
) -> None:
|
|
368
|
+
"""Pay an invoice for an organisation billing period."""
|
|
369
|
+
from urllib.parse import quote
|
|
370
|
+
|
|
371
|
+
try:
|
|
372
|
+
result = call_api(
|
|
373
|
+
method="POST",
|
|
374
|
+
path=f"/api/v1/billing/invoices/{quote(org_id, safe='')}/{quote(period, safe='')}/pay",
|
|
375
|
+
profile=profile,
|
|
376
|
+
require_scope=False,
|
|
377
|
+
body={},
|
|
378
|
+
)
|
|
379
|
+
except CLIRequestError as exc:
|
|
380
|
+
typer.echo(str(exc), err=True)
|
|
381
|
+
raise typer.Exit(1) from exc
|
|
382
|
+
typer.echo(print_json(result))
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
razorpay_app = typer.Typer(help="Razorpay checkout helpers")
|
|
386
|
+
app.add_typer(razorpay_app, name="razorpay")
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
@razorpay_app.command("create-order")
|
|
390
|
+
def razorpay_create_order(
|
|
391
|
+
plan: str = typer.Option(..., "--plan", help="Plan name (pro|ultra)"),
|
|
392
|
+
billing_cycle: str = typer.Option("monthly", "--cycle", help="monthly or annual"),
|
|
393
|
+
customer_email: str | None = typer.Option(None, "--email"),
|
|
394
|
+
account_id: str | None = typer.Option(None, "--account"),
|
|
395
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
396
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
397
|
+
) -> None:
|
|
398
|
+
"""Create a Razorpay order for self-serve checkout."""
|
|
399
|
+
body: dict[str, object] = {"plan": plan, "billing_cycle": billing_cycle}
|
|
400
|
+
if customer_email:
|
|
401
|
+
body["customer_email"] = customer_email
|
|
402
|
+
if account_id:
|
|
403
|
+
body["account_id"] = account_id
|
|
404
|
+
try:
|
|
405
|
+
result = call_api(
|
|
406
|
+
method="POST",
|
|
407
|
+
path="/api/v1/billing/razorpay/create-order",
|
|
408
|
+
profile=profile,
|
|
409
|
+
org_id=org_id,
|
|
410
|
+
require_scope=False,
|
|
411
|
+
body=body,
|
|
412
|
+
)
|
|
413
|
+
except CLIRequestError as exc:
|
|
414
|
+
typer.echo(str(exc), err=True)
|
|
415
|
+
raise typer.Exit(1) from exc
|
|
416
|
+
typer.echo(print_json(result))
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
@razorpay_app.command("verify")
|
|
420
|
+
def razorpay_verify(
|
|
421
|
+
order_id: str = typer.Option(..., "--order-id"),
|
|
422
|
+
payment_id: str = typer.Option(..., "--payment-id"),
|
|
423
|
+
signature: str = typer.Option(..., "--signature"),
|
|
424
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
425
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
426
|
+
) -> None:
|
|
427
|
+
"""Verify a Razorpay payment signature and activate the plan."""
|
|
428
|
+
try:
|
|
429
|
+
result = call_api(
|
|
430
|
+
method="POST",
|
|
431
|
+
path="/api/v1/billing/razorpay/verify-payment",
|
|
432
|
+
profile=profile,
|
|
433
|
+
org_id=org_id,
|
|
434
|
+
require_scope=False,
|
|
435
|
+
body={
|
|
436
|
+
"razorpay_order_id": order_id,
|
|
437
|
+
"razorpay_payment_id": payment_id,
|
|
438
|
+
"razorpay_signature": signature,
|
|
439
|
+
},
|
|
440
|
+
)
|
|
441
|
+
except CLIRequestError as exc:
|
|
442
|
+
typer.echo(str(exc), err=True)
|
|
443
|
+
raise typer.Exit(1) from exc
|
|
444
|
+
typer.echo(print_json(result))
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
account_app = typer.Typer(help="Billing account lifecycle")
|
|
448
|
+
app.add_typer(account_app, name="account")
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
@account_app.command("get")
|
|
452
|
+
def account_get(
|
|
453
|
+
account_id: str = typer.Argument(..., help="Billing account ID"),
|
|
454
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
455
|
+
) -> None:
|
|
456
|
+
"""Fetch a billing account."""
|
|
457
|
+
from urllib.parse import quote
|
|
458
|
+
|
|
459
|
+
try:
|
|
460
|
+
result = call_api(
|
|
461
|
+
method="GET",
|
|
462
|
+
path=f"/api/v1/accounts/{quote(account_id, safe='')}",
|
|
463
|
+
profile=profile,
|
|
464
|
+
require_scope=False,
|
|
465
|
+
)
|
|
466
|
+
except CLIRequestError as exc:
|
|
467
|
+
typer.echo(str(exc), err=True)
|
|
468
|
+
raise typer.Exit(1) from exc
|
|
469
|
+
typer.echo(print_json(result))
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
@account_app.command("cancel")
|
|
473
|
+
def account_cancel(
|
|
474
|
+
account_id: str = typer.Argument(..., help="Billing account ID"),
|
|
475
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
476
|
+
) -> None:
|
|
477
|
+
"""Cancel a billing account subscription."""
|
|
478
|
+
from urllib.parse import quote
|
|
479
|
+
|
|
480
|
+
try:
|
|
481
|
+
result = call_api(
|
|
482
|
+
method="POST",
|
|
483
|
+
path=f"/api/v1/accounts/{quote(account_id, safe='')}/cancel",
|
|
484
|
+
profile=profile,
|
|
485
|
+
require_scope=False,
|
|
486
|
+
body={},
|
|
487
|
+
)
|
|
488
|
+
except CLIRequestError as exc:
|
|
489
|
+
typer.echo(str(exc), err=True)
|
|
490
|
+
raise typer.Exit(1) from exc
|
|
491
|
+
typer.echo(print_json(result))
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
@account_app.command("resume")
|
|
495
|
+
def account_resume(
|
|
496
|
+
account_id: str = typer.Argument(..., help="Billing account ID"),
|
|
497
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
498
|
+
) -> None:
|
|
499
|
+
"""Resume a cancelled billing account."""
|
|
500
|
+
from urllib.parse import quote
|
|
501
|
+
|
|
502
|
+
try:
|
|
503
|
+
result = call_api(
|
|
504
|
+
method="POST",
|
|
505
|
+
path=f"/api/v1/accounts/{quote(account_id, safe='')}/resume",
|
|
506
|
+
profile=profile,
|
|
507
|
+
require_scope=False,
|
|
508
|
+
body={},
|
|
509
|
+
)
|
|
510
|
+
except CLIRequestError as exc:
|
|
511
|
+
typer.echo(str(exc), err=True)
|
|
512
|
+
raise typer.Exit(1) from exc
|
|
513
|
+
typer.echo(print_json(result))
|