qod-cli 0.3.7__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.
@@ -0,0 +1,58 @@
1
+ import typer
2
+
3
+ from ._run import call
4
+
5
+ app = typer.Typer(help="Node lifecycle and statement inspection.")
6
+ statement_app = typer.Typer(help="Statement control.")
7
+
8
+ TENANT = typer.Option(..., "--tenant")
9
+ DB = typer.Option(..., "--db", help="Tenant database name.")
10
+ POOL = typer.Option(..., "--pool")
11
+ NODE = typer.Option(..., "--node-id")
12
+
13
+
14
+ def _key(tenant: str, db: str, pool: str, node_id: str) -> dict:
15
+ return {"tenant": tenant, "tenantDb": db, "pool": pool, "nodeId": node_id}
16
+
17
+
18
+ @app.command()
19
+ def quarantine(ctx: typer.Context, tenant: str = TENANT, db: str = DB, pool: str = POOL, node_id: str = NODE):
20
+ call(ctx, "POST", "/api/node/quarantine", body=_key(tenant, db, pool, node_id))
21
+
22
+
23
+ @app.command()
24
+ def unquarantine(ctx: typer.Context, tenant: str = TENANT, db: str = DB, pool: str = POOL, node_id: str = NODE):
25
+ call(ctx, "POST", "/api/node/unquarantine", body=_key(tenant, db, pool, node_id))
26
+
27
+
28
+ @app.command()
29
+ def restart(ctx: typer.Context, tenant: str = TENANT, db: str = DB, pool: str = POOL, node_id: str = NODE):
30
+ call(ctx, "POST", "/api/node/restart", body=_key(tenant, db, pool, node_id))
31
+
32
+
33
+ @app.command("set-max-concurrent")
34
+ def set_max_concurrent(
35
+ ctx: typer.Context,
36
+ tenant: str = TENANT,
37
+ db: str = DB,
38
+ pool: str = POOL,
39
+ node_id: str = NODE,
40
+ max_: int = typer.Option(..., "--max"),
41
+ ):
42
+ call(ctx, "POST", "/api/node/setMaxConcurrent", body={**_key(tenant, db, pool, node_id), "max": max_})
43
+
44
+
45
+ @app.command()
46
+ def statements(ctx: typer.Context, limit: int = typer.Option(None, "--limit")):
47
+ """Recent statement history, newest first."""
48
+ call(ctx, "GET", "/api/node/statements", params={"limit": limit})
49
+
50
+
51
+ @app.command("active-statements")
52
+ def active_statements(ctx: typer.Context):
53
+ call(ctx, "GET", "/api/node/active-statements")
54
+
55
+
56
+ @statement_app.command()
57
+ def kill(ctx: typer.Context, statement_id: str = typer.Argument(..., metavar="ID")):
58
+ call(ctx, "POST", "/api/statement/kill", body={"id": statement_id})
@@ -0,0 +1,137 @@
1
+ import typer
2
+
3
+ from ._run import call
4
+
5
+ app = typer.Typer(help="Pool lifecycle and pool-level access grants.")
6
+ permission_app = typer.Typer(help="Pool access grants for users and groups.")
7
+ app.add_typer(permission_app, name="permission")
8
+
9
+ TENANT = typer.Option(..., "--tenant")
10
+ DB = typer.Option(..., "--db", help="Tenant database name.")
11
+ POOL = typer.Option(..., "--pool")
12
+
13
+
14
+ def _key(tenant: str, db: str, pool: str) -> dict:
15
+ return {"tenant": tenant, "tenantDb": db, "pool": pool}
16
+
17
+
18
+ @app.command("list")
19
+ def list_(ctx: typer.Context):
20
+ call(ctx, "GET", "/api/pool/list")
21
+
22
+
23
+ @app.command()
24
+ def create(
25
+ ctx: typer.Context,
26
+ tenant: str = TENANT,
27
+ db: str = DB,
28
+ pool: str = POOL,
29
+ size: int = typer.Option(..., "--size"),
30
+ writeonly: int = typer.Option(0, "--writeonly"),
31
+ readonly: int = typer.Option(0, "--readonly"),
32
+ dual: int = typer.Option(0, "--dual"),
33
+ idle_timeout_sec: int = typer.Option(-1, "--idle-timeout-sec"),
34
+ max_concurrent_per_node: int = typer.Option(0, "--max-concurrent-per-node"),
35
+ disabled: bool = typer.Option(False, "--disabled"),
36
+ init_sql: str = typer.Option(None, "--init-sql"),
37
+ cpu: str = typer.Option("", "--cpu"),
38
+ memory: str = typer.Option("", "--memory"),
39
+ pod_template_file: typer.FileText = typer.Option(None, "--pod-template-file"),
40
+ ):
41
+ body = {
42
+ **_key(tenant, db, pool),
43
+ "size": size,
44
+ "roleDistribution": {"writeonly": writeonly, "readonly": readonly, "dual": dual},
45
+ "idleTimeoutSec": idle_timeout_sec,
46
+ "maxConcurrentPerNode": max_concurrent_per_node,
47
+ "disabled": disabled,
48
+ "cpu": cpu,
49
+ "memory": memory,
50
+ "podTemplateYaml": pod_template_file.read() if pod_template_file else "",
51
+ }
52
+ if init_sql is not None:
53
+ body["initSql"] = init_sql
54
+ call(ctx, "POST", "/api/pool/create", body=body)
55
+
56
+
57
+ @app.command()
58
+ def scale(
59
+ ctx: typer.Context,
60
+ tenant: str = TENANT,
61
+ db: str = DB,
62
+ pool: str = POOL,
63
+ target_size: int = typer.Option(..., "--target-size"),
64
+ writeonly: int = typer.Option(0, "--writeonly"),
65
+ readonly: int = typer.Option(0, "--readonly"),
66
+ dual: int = typer.Option(0, "--dual"),
67
+ force: bool = typer.Option(False, "--force"),
68
+ ):
69
+ call(
70
+ ctx,
71
+ "POST",
72
+ "/api/pool/scale",
73
+ body={
74
+ **_key(tenant, db, pool),
75
+ "targetSize": target_size,
76
+ "roleDistribution": {"writeonly": writeonly, "readonly": readonly, "dual": dual},
77
+ "force": force,
78
+ },
79
+ )
80
+
81
+
82
+ @app.command()
83
+ def stop(ctx: typer.Context, tenant: str = TENANT, db: str = DB, pool: str = POOL, force: bool = typer.Option(False, "--force")):
84
+ call(ctx, "POST", "/api/pool/stop", body={**_key(tenant, db, pool), "force": force})
85
+
86
+
87
+ @app.command()
88
+ def delete(ctx: typer.Context, tenant: str = TENANT, db: str = DB, pool: str = POOL, force: bool = typer.Option(False, "--force")):
89
+ call(ctx, "POST", "/api/pool/delete", body={**_key(tenant, db, pool), "force": force})
90
+
91
+
92
+ @app.command("set-disabled")
93
+ def set_disabled(ctx: typer.Context, tenant: str = TENANT, db: str = DB, pool: str = POOL, disabled: bool = typer.Option(..., "--disabled/--enabled")):
94
+ call(ctx, "POST", "/api/pool/setDisabled", body={**_key(tenant, db, pool), "disabled": disabled})
95
+
96
+
97
+ @app.command("set-resources")
98
+ def set_resources(ctx: typer.Context, tenant: str = TENANT, db: str = DB, pool: str = POOL, cpu: str = typer.Option(..., "--cpu"), memory: str = typer.Option(..., "--memory")):
99
+ call(ctx, "POST", "/api/pool/setResources", body={**_key(tenant, db, pool), "cpu": cpu, "memory": memory})
100
+
101
+
102
+ @app.command("set-pod-template")
103
+ def set_pod_template(ctx: typer.Context, tenant: str = TENANT, db: str = DB, pool: str = POOL, file: typer.FileText = typer.Option(..., "--file")):
104
+ call(ctx, "POST", "/api/pool/setPodTemplate", body={**_key(tenant, db, pool), "podTemplateYaml": file.read()})
105
+
106
+
107
+ @permission_app.command("list")
108
+ def permission_list(
109
+ ctx: typer.Context,
110
+ tenant: str = typer.Option(None, "--tenant"),
111
+ user_id: str = typer.Option(None, "--user-id"),
112
+ group_id: str = typer.Option(None, "--group-id"),
113
+ ):
114
+ call(ctx, "GET", "/api/pool/permission/list", params={"tenant": tenant, "userId": user_id, "groupId": group_id})
115
+
116
+
117
+ @permission_app.command("grant")
118
+ def permission_grant(
119
+ ctx: typer.Context,
120
+ tenant: str = typer.Option(..., "--tenant"),
121
+ pool_id: str = typer.Option(None, "--pool-id", help="Omit = every pool in the tenant."),
122
+ user_id: str = typer.Option(None, "--user-id"),
123
+ group_id: str = typer.Option(None, "--group-id", help="Exactly one of --user-id / --group-id."),
124
+ ):
125
+ body: dict = {"tenant": tenant}
126
+ if pool_id is not None:
127
+ body["poolId"] = pool_id
128
+ if user_id is not None:
129
+ body["userId"] = user_id
130
+ if group_id is not None:
131
+ body["groupId"] = group_id
132
+ call(ctx, "POST", "/api/pool/permission/grant", body=body)
133
+
134
+
135
+ @permission_app.command("revoke")
136
+ def permission_revoke(ctx: typer.Context, grant_id: str = typer.Argument(..., metavar="ID")):
137
+ call(ctx, "POST", "/api/pool/permission/revoke", body={"id": grant_id})
@@ -0,0 +1,150 @@
1
+ import typer
2
+
3
+ from ._run import call
4
+
5
+ app = typer.Typer(help="Roles, table permissions, and row/column policies.")
6
+ permission_app = typer.Typer(help="Table permissions attached to a role.")
7
+ row_policy_app = typer.Typer(help="Row-level security predicates.")
8
+ column_policy_app = typer.Typer(help="Column-level security policies.")
9
+ app.add_typer(permission_app, name="permission")
10
+ app.add_typer(row_policy_app, name="row-policy")
11
+ app.add_typer(column_policy_app, name="column-policy")
12
+
13
+
14
+ @app.command("list")
15
+ def list_(ctx: typer.Context, tenant: str = typer.Option(..., "--tenant")):
16
+ call(ctx, "GET", "/api/role/list", params={"tenant": tenant})
17
+
18
+
19
+ @app.command()
20
+ def create(
21
+ ctx: typer.Context,
22
+ tenant: str = typer.Option(..., "--tenant"),
23
+ name: str = typer.Option(..., "--name"),
24
+ description: str = typer.Option(None, "--description"),
25
+ ):
26
+ body: dict = {"tenant": tenant, "name": name}
27
+ if description is not None:
28
+ body["description"] = description
29
+ call(ctx, "POST", "/api/role/create", body=body)
30
+
31
+
32
+ @app.command()
33
+ def delete(ctx: typer.Context, role_id: str = typer.Argument(..., metavar="ID")):
34
+ call(ctx, "POST", "/api/role/delete", body={"id": role_id})
35
+
36
+
37
+ @permission_app.command("list")
38
+ def permission_list(ctx: typer.Context, role_id: str = typer.Option(..., "--role-id")):
39
+ call(ctx, "GET", "/api/role/permission/list", params={"roleId": role_id})
40
+
41
+
42
+ @permission_app.command("grant")
43
+ def permission_grant(
44
+ ctx: typer.Context,
45
+ role_id: str = typer.Option(..., "--role-id"),
46
+ verb: str = typer.Option(..., "--verb", help="RO|RW|DDL|ALL"),
47
+ catalog: str = typer.Option("*", "--catalog"),
48
+ schema: str = typer.Option("*", "--schema"),
49
+ table: str = typer.Option("*", "--table"),
50
+ ):
51
+ call(
52
+ ctx,
53
+ "POST",
54
+ "/api/role/permission/grant",
55
+ body={"roleId": role_id, "catalog": catalog, "schema": schema, "table": table, "verb": verb},
56
+ )
57
+
58
+
59
+ @permission_app.command("revoke")
60
+ def permission_revoke(ctx: typer.Context, permission_id: str = typer.Argument(..., metavar="ID")):
61
+ call(ctx, "POST", "/api/role/permission/revoke", body={"id": permission_id})
62
+
63
+
64
+ @row_policy_app.command("list")
65
+ def row_policy_list(ctx: typer.Context, role_id: str = typer.Option(..., "--role-id")):
66
+ call(ctx, "GET", "/api/role/row-policy/list", params={"roleId": role_id})
67
+
68
+
69
+ @row_policy_app.command("create")
70
+ def row_policy_create(
71
+ ctx: typer.Context,
72
+ role_id: str = typer.Option(..., "--role-id"),
73
+ catalog: str = typer.Option(..., "--catalog"),
74
+ schema: str = typer.Option(..., "--schema"),
75
+ table: str = typer.Option(..., "--table"),
76
+ predicate_sql: str = typer.Option(..., "--predicate-sql"),
77
+ ):
78
+ call(
79
+ ctx,
80
+ "POST",
81
+ "/api/role/row-policy/create",
82
+ body={
83
+ "roleId": role_id,
84
+ "catalogName": catalog,
85
+ "schemaName": schema,
86
+ "tableName": table,
87
+ "predicateSql": predicate_sql,
88
+ },
89
+ )
90
+
91
+
92
+ @row_policy_app.command("update")
93
+ def row_policy_update(
94
+ ctx: typer.Context,
95
+ policy_id: str = typer.Argument(..., metavar="ID"),
96
+ predicate_sql: str = typer.Option(..., "--predicate-sql"),
97
+ ):
98
+ call(ctx, "POST", "/api/role/row-policy/update", body={"id": policy_id, "predicateSql": predicate_sql})
99
+
100
+
101
+ @row_policy_app.command("delete")
102
+ def row_policy_delete(ctx: typer.Context, policy_id: str = typer.Argument(..., metavar="ID")):
103
+ call(ctx, "POST", "/api/role/row-policy/delete", body={"id": policy_id})
104
+
105
+
106
+ @column_policy_app.command("list")
107
+ def column_policy_list(ctx: typer.Context, role_id: str = typer.Option(..., "--role-id")):
108
+ call(ctx, "GET", "/api/role/column-policy/list", params={"roleId": role_id})
109
+
110
+
111
+ @column_policy_app.command("create")
112
+ def column_policy_create(
113
+ ctx: typer.Context,
114
+ role_id: str = typer.Option(..., "--role-id"),
115
+ catalog: str = typer.Option(..., "--catalog"),
116
+ schema: str = typer.Option(..., "--schema"),
117
+ table: str = typer.Option(..., "--table"),
118
+ column: str = typer.Option(..., "--column"),
119
+ action: str = typer.Option(..., "--action"),
120
+ transform_sql: str = typer.Option(None, "--transform-sql"),
121
+ ):
122
+ body: dict = {
123
+ "roleId": role_id,
124
+ "catalogName": catalog,
125
+ "schemaName": schema,
126
+ "tableName": table,
127
+ "columnName": column,
128
+ "action": action,
129
+ }
130
+ if transform_sql is not None:
131
+ body["transformSql"] = transform_sql
132
+ call(ctx, "POST", "/api/role/column-policy/create", body=body)
133
+
134
+
135
+ @column_policy_app.command("update")
136
+ def column_policy_update(
137
+ ctx: typer.Context,
138
+ policy_id: str = typer.Argument(..., metavar="ID"),
139
+ action: str = typer.Option(..., "--action"),
140
+ transform_sql: str = typer.Option(None, "--transform-sql"),
141
+ ):
142
+ body: dict = {"id": policy_id, "action": action}
143
+ if transform_sql is not None:
144
+ body["transformSql"] = transform_sql
145
+ call(ctx, "POST", "/api/role/column-policy/update", body=body)
146
+
147
+
148
+ @column_policy_app.command("delete")
149
+ def column_policy_delete(ctx: typer.Context, policy_id: str = typer.Argument(..., metavar="ID")):
150
+ call(ctx, "POST", "/api/role/column-policy/delete", body={"id": policy_id})
@@ -0,0 +1,86 @@
1
+ import typer
2
+
3
+ from ..sql import SqlClient, render_table
4
+
5
+
6
+ def repl(client, mode: str, input_fn=input) -> None:
7
+ try:
8
+ import readline # noqa: F401 (line editing + history; pyreadline3 provides it on Windows)
9
+ except ImportError:
10
+ pass
11
+ import sys
12
+
13
+ buffer: list[str] = []
14
+ while True:
15
+ prompt = "qod> " if not buffer else "...> "
16
+ try:
17
+ line = input_fn(prompt)
18
+ except EOFError:
19
+ break
20
+ except KeyboardInterrupt:
21
+ buffer.clear()
22
+ print(file=sys.stderr)
23
+ continue
24
+ if not buffer and line.strip() == "\\q":
25
+ break
26
+ buffer.append(line)
27
+ if not line.rstrip().endswith(";"):
28
+ continue
29
+ statement = "\n".join(buffer)
30
+ buffer.clear()
31
+ try:
32
+ render_table(client.query(statement), mode)
33
+ except Exception as exc:
34
+ print(f"error: {exc}", file=sys.stderr)
35
+
36
+
37
+ def _mode(ctx: typer.Context, csv_flag: bool) -> str:
38
+ if csv_flag and ctx.obj.json_output:
39
+ raise typer.BadParameter("--csv and --json are mutually exclusive")
40
+ return "csv" if csv_flag else "json" if ctx.obj.json_output else "table"
41
+
42
+
43
+ def _connect(ctx: typer.Context) -> SqlClient:
44
+ settings = ctx.obj.settings
45
+ if not settings.sql_user:
46
+ raise typer.BadParameter("no SQL user configured; run qod login or set QOD_USER")
47
+ if not settings.sql_password:
48
+ settings.sql_password = typer.prompt(f"Password for {settings.sql_user}", hide_input=True)
49
+ return SqlClient.connect(settings)
50
+
51
+
52
+ def sql(
53
+ ctx: typer.Context,
54
+ statement: str = typer.Argument(None, help="SQL to run; omit for the interactive REPL."),
55
+ csv_flag: bool = typer.Option(False, "--csv", help="CSV to stdout."),
56
+ tenant: str = typer.Option(None, "--tenant", help="Override the profile tenant."),
57
+ pool: str = typer.Option(None, "--pool", help="Override the profile pool."),
58
+ superuser: bool = typer.Option(None, "--superuser/--no-superuser"),
59
+ ):
60
+ """Run SQL against the FlightSQL edge."""
61
+ settings = ctx.obj.settings
62
+ if tenant is not None:
63
+ settings.tenant = tenant
64
+ if pool is not None:
65
+ settings.pool = pool
66
+ if superuser is not None:
67
+ settings.superuser = superuser
68
+ mode = _mode(ctx, csv_flag)
69
+ if statement is None:
70
+ try:
71
+ with _connect(ctx) as client:
72
+ repl(client, mode)
73
+ except (typer.BadParameter, typer.Exit):
74
+ raise
75
+ except Exception as exc:
76
+ typer.echo(f"error: {exc}", err=True)
77
+ raise typer.Exit(1)
78
+ return
79
+ try:
80
+ with _connect(ctx) as client:
81
+ render_table(client.query(statement), mode)
82
+ except (typer.BadParameter, typer.Exit):
83
+ raise
84
+ except Exception as exc: # ADBC raises driver-specific exception types
85
+ typer.echo(f"error: {exc}", err=True)
86
+ raise typer.Exit(1)
@@ -0,0 +1,47 @@
1
+ import typer
2
+
3
+ from ._run import call
4
+
5
+ app = typer.Typer(help="Snapshot tags (create, delete, protect).")
6
+
7
+ TENANT = typer.Option(..., "--tenant")
8
+ DB = typer.Option(..., "--db")
9
+ NAME = typer.Option(..., "--name")
10
+
11
+
12
+ @app.command()
13
+ def create(
14
+ ctx: typer.Context,
15
+ tenant: str = TENANT,
16
+ db: str = DB,
17
+ name: str = NAME,
18
+ snapshot_id: int = typer.Option(..., "--snapshot-id"),
19
+ protected: bool = typer.Option(False, "--protected"),
20
+ ):
21
+ call(
22
+ ctx,
23
+ "POST",
24
+ "/api/catalog/tag/create",
25
+ body={"tenant": tenant, "tenantDb": db, "name": name, "snapshotId": snapshot_id, "isProtected": protected},
26
+ )
27
+
28
+
29
+ @app.command()
30
+ def delete(ctx: typer.Context, tenant: str = TENANT, db: str = DB, name: str = NAME):
31
+ call(ctx, "POST", "/api/catalog/tag/delete", body={"tenant": tenant, "tenantDb": db, "name": name})
32
+
33
+
34
+ @app.command()
35
+ def protect(
36
+ ctx: typer.Context,
37
+ tenant: str = TENANT,
38
+ db: str = DB,
39
+ name: str = NAME,
40
+ protected: bool = typer.Option(..., "--protected/--unprotected"),
41
+ ):
42
+ call(
43
+ ctx,
44
+ "POST",
45
+ "/api/catalog/tag/protect",
46
+ body={"tenant": tenant, "tenantDb": db, "name": name, "isProtected": protected},
47
+ )
@@ -0,0 +1,95 @@
1
+ import typer
2
+
3
+ from ._run import call
4
+
5
+ audit_app = typer.Typer(help="Audit log.")
6
+ history_app = typer.Typer(help="Statement history and trends.")
7
+
8
+
9
+ @audit_app.command("list")
10
+ def audit_list(
11
+ ctx: typer.Context,
12
+ family: str = typer.Option(None, "--family"),
13
+ tenant: str = typer.Option(None, "--tenant"),
14
+ actor: str = typer.Option(None, "--actor"),
15
+ action: str = typer.Option(None, "--action"),
16
+ q: str = typer.Option(None, "--q", help="Free-text filter."),
17
+ from_: str = typer.Option(None, "--from"),
18
+ to: str = typer.Option(None, "--to"),
19
+ limit: int = typer.Option(None, "--limit"),
20
+ before: str = typer.Option(None, "--before", help="Keyset pagination token."),
21
+ no_tenant: bool = typer.Option(None, "--no-tenant", help="Only rows without a tenant."),
22
+ ):
23
+ call(
24
+ ctx,
25
+ "GET",
26
+ "/api/audit/list",
27
+ params={
28
+ "family": family, "tenant": tenant, "actor": actor, "action": action, "q": q,
29
+ "from": from_, "to": to, "limit": limit, "before": before, "noTenant": no_tenant,
30
+ },
31
+ )
32
+
33
+
34
+ @audit_app.command()
35
+ def actions(ctx: typer.Context):
36
+ """Distinct audit action names, for filter values."""
37
+ call(ctx, "GET", "/api/audit/actions")
38
+
39
+
40
+ def usage(
41
+ ctx: typer.Context,
42
+ from_: str = typer.Option(None, "--from"),
43
+ to: str = typer.Option(None, "--to"),
44
+ group_by: str = typer.Option(None, "--group-by"),
45
+ tenant: str = typer.Option(None, "--tenant"),
46
+ pool: str = typer.Option(None, "--pool"),
47
+ ):
48
+ """Usage accounting rollups."""
49
+ call(
50
+ ctx,
51
+ "GET",
52
+ "/api/usage",
53
+ params={"from": from_, "to": to, "groupBy": group_by, "tenant": tenant, "pool": pool},
54
+ )
55
+
56
+
57
+ @history_app.command()
58
+ def statements(
59
+ ctx: typer.Context,
60
+ from_: str = typer.Option(None, "--from"),
61
+ to: str = typer.Option(None, "--to"),
62
+ tenant: str = typer.Option(None, "--tenant"),
63
+ pool: str = typer.Option(None, "--pool"),
64
+ user: str = typer.Option(None, "--user"),
65
+ status: str = typer.Option(None, "--status"),
66
+ q: str = typer.Option(None, "--q"),
67
+ limit: int = typer.Option(None, "--limit"),
68
+ before: str = typer.Option(None, "--before"),
69
+ ):
70
+ call(
71
+ ctx,
72
+ "GET",
73
+ "/api/history/statements",
74
+ params={
75
+ "from": from_, "to": to, "tenant": tenant, "pool": pool, "user": user,
76
+ "status": status, "q": q, "limit": limit, "before": before,
77
+ },
78
+ )
79
+
80
+
81
+ @history_app.command()
82
+ def trends(
83
+ ctx: typer.Context,
84
+ granularity: str = typer.Option(None, "--granularity"),
85
+ from_: str = typer.Option(None, "--from"),
86
+ to: str = typer.Option(None, "--to"),
87
+ tenant: str = typer.Option(None, "--tenant"),
88
+ pool: str = typer.Option(None, "--pool"),
89
+ ):
90
+ call(
91
+ ctx,
92
+ "GET",
93
+ "/api/history/trends",
94
+ params={"granularity": granularity, "from": from_, "to": to, "tenant": tenant, "pool": pool},
95
+ )
@@ -0,0 +1,61 @@
1
+ import typer
2
+
3
+ from ._run import call, kv_pairs
4
+
5
+ app = typer.Typer(help="Tenant CRUD.")
6
+
7
+
8
+ @app.command("list")
9
+ def list_(ctx: typer.Context):
10
+ call(ctx, "GET", "/api/tenant/list")
11
+
12
+
13
+ @app.command()
14
+ def create(
15
+ ctx: typer.Context,
16
+ tenant_id: str = typer.Argument(..., metavar="ID", help="Lowercase slug, e.g. acme."),
17
+ display_name: str = typer.Option("", "--display-name"),
18
+ auth_provider: str = typer.Option("db", "--auth-provider", help="db|keycloak|google|azure|aws"),
19
+ auth_config: list[str] = typer.Option(None, "--auth-config", help="KEY=VALUE, repeatable."),
20
+ ):
21
+ call(
22
+ ctx,
23
+ "POST",
24
+ "/api/tenant/create",
25
+ body={
26
+ "id": tenant_id,
27
+ "displayName": display_name,
28
+ "authProvider": auth_provider,
29
+ "authConfig": kv_pairs(auth_config),
30
+ },
31
+ )
32
+
33
+
34
+ @app.command()
35
+ def delete(ctx: typer.Context, name: str = typer.Argument(...)):
36
+ """Delete a tenant (must have no pools)."""
37
+ call(ctx, "POST", "/api/tenant/delete", body={"name": name})
38
+
39
+
40
+ @app.command("set-disabled")
41
+ def set_disabled(
42
+ ctx: typer.Context,
43
+ name: str = typer.Argument(...),
44
+ disabled: bool = typer.Option(..., "--disabled/--enabled"),
45
+ ):
46
+ call(ctx, "POST", "/api/tenant/setDisabled", body={"name": name, "disabled": disabled})
47
+
48
+
49
+ @app.command("set-auth")
50
+ def set_auth(
51
+ ctx: typer.Context,
52
+ name: str = typer.Argument(...),
53
+ auth_provider: str = typer.Option(..., "--auth-provider"),
54
+ auth_config: list[str] = typer.Option(None, "--auth-config", help="KEY=VALUE, repeatable."),
55
+ ):
56
+ call(
57
+ ctx,
58
+ "POST",
59
+ "/api/tenant/setAuth",
60
+ body={"name": name, "authProvider": auth_provider, "authConfig": kv_pairs(auth_config)},
61
+ )