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.
- qod_cli/__init__.py +1 -0
- qod_cli/commands/__init__.py +0 -0
- qod_cli/commands/_run.py +35 -0
- qod_cli/commands/auth.py +67 -0
- qod_cli/commands/catalog.py +173 -0
- qod_cli/commands/config_cmd.py +17 -0
- qod_cli/commands/database.py +69 -0
- qod_cli/commands/federation.py +82 -0
- qod_cli/commands/group.py +28 -0
- qod_cli/commands/maintenance.py +81 -0
- qod_cli/commands/manifest.py +34 -0
- qod_cli/commands/membership.py +50 -0
- qod_cli/commands/node.py +58 -0
- qod_cli/commands/pool.py +137 -0
- qod_cli/commands/role.py +150 -0
- qod_cli/commands/sql_cmd.py +86 -0
- qod_cli/commands/tag.py +47 -0
- qod_cli/commands/telemetry.py +95 -0
- qod_cli/commands/tenant.py +61 -0
- qod_cli/commands/user.py +62 -0
- qod_cli/config.py +107 -0
- qod_cli/main.py +96 -0
- qod_cli/output.py +66 -0
- qod_cli/rest.py +73 -0
- qod_cli/sql.py +80 -0
- qod_cli-0.3.7.dist-info/METADATA +17 -0
- qod_cli-0.3.7.dist-info/RECORD +29 -0
- qod_cli-0.3.7.dist-info/WHEEL +4 -0
- qod_cli-0.3.7.dist-info/entry_points.txt +2 -0
qod_cli/commands/node.py
ADDED
|
@@ -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})
|
qod_cli/commands/pool.py
ADDED
|
@@ -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})
|
qod_cli/commands/role.py
ADDED
|
@@ -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)
|
qod_cli/commands/tag.py
ADDED
|
@@ -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
|
+
)
|