tabsdata-cli 2.0.0__cp312-abi3-win_amd64.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.
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/__init__.py +5 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/_registry.py +78 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/assets/manifest/BANNER +5 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/assets/manifest/LICENSE +3 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/assets/manifest/README-PyPi.md +8 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/assets/manifest/RELEASE-NOTES +1 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/assets/manifest/THIRD-PARTY +3 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/assets/manifest/VERSION +1 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/cli_utils.py +993 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/shell.py +390 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/__init__.py +3 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/auth_group.py +197 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/cli.py +508 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/collection_group.py +458 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/config_utils.py +93 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/connection_group.py +159 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/examples_group.py +206 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/execution_utils.py +429 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/fn_group.py +1109 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/fnrun_group.py +285 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/group_group.py +184 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/metrics_group.py +323 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/permission_group.py +259 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/plan_group.py +324 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/project_group.py +170 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/role_group.py +630 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/settings_group.py +344 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/table_group.py +600 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/trigger_group.py +338 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/trx_group.py +329 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/user_group.py +200 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/worker_group.py +185 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/template.py +781 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/py.typed +0 -0
- tabsdata_cli-2.0.0.dist-info/METADATA +39 -0
- tabsdata_cli-2.0.0.dist-info/RECORD +39 -0
- tabsdata_cli-2.0.0.dist-info/WHEEL +5 -0
- tabsdata_cli-2.0.0.dist-info/licenses/src/main/python/tabsdatak/cli/assets/manifest/LICENSE +3 -0
- tabsdata_cli-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright 2026 Tabsdata Inc.
|
|
3
|
+
#
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
import rich_click as click
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
from tabsdatak.cli._registry import cli_command
|
|
13
|
+
from tabsdatak.cli.cli_utils import (
|
|
14
|
+
format_option,
|
|
15
|
+
hint_common_solutions,
|
|
16
|
+
make_console,
|
|
17
|
+
proj_option,
|
|
18
|
+
render,
|
|
19
|
+
verify_login_or_prompt,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from tabsdatak.sdk.tabsdata_server import TabsdataServer
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _split(value: str | None) -> list[str]:
|
|
27
|
+
"""Comma-separated list parser for repeatable filter options."""
|
|
28
|
+
if not value:
|
|
29
|
+
return []
|
|
30
|
+
return [v.strip() for v in value.split(",") if v.strip()]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _render_summary(data: dict) -> None:
|
|
34
|
+
"""Single condensed table: one row per
|
|
35
|
+
`(dim_entity, group, status)`, magnitudes summed across buckets.
|
|
36
|
+
"""
|
|
37
|
+
dim = (data.get("dim") or "").lower()
|
|
38
|
+
unit = data.get("unit", "?")
|
|
39
|
+
start_ms = data.get("start_ms")
|
|
40
|
+
end_ms = data.get("end_ms")
|
|
41
|
+
click.echo(f"dim={dim or '?'} unit={unit} window=[{start_ms}, {end_ms})")
|
|
42
|
+
|
|
43
|
+
nodes = data.get("nodes") or []
|
|
44
|
+
if not nodes:
|
|
45
|
+
click.echo("(no activity in window)")
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
is_collection_dim = dim == "collection"
|
|
49
|
+
|
|
50
|
+
agg: dict[tuple[str, str, str], dict] = {}
|
|
51
|
+
for n in nodes:
|
|
52
|
+
coll_id = (n.get("collection_id") or "") if is_collection_dim else ""
|
|
53
|
+
coll_name = (n.get("collection_name") or "") if is_collection_dim else ""
|
|
54
|
+
grp_id = n.get("group_id") or ""
|
|
55
|
+
grp_name = n.get("group_name") or ""
|
|
56
|
+
status = n.get("status") or ""
|
|
57
|
+
dim_key = coll_id if is_collection_dim else grp_id
|
|
58
|
+
key = (dim_key, grp_id, status)
|
|
59
|
+
row = agg.setdefault(
|
|
60
|
+
key,
|
|
61
|
+
{
|
|
62
|
+
"collection_name": coll_name,
|
|
63
|
+
"group_name": grp_name,
|
|
64
|
+
"fn_runs": 0,
|
|
65
|
+
"transactions": 0,
|
|
66
|
+
"tables_in": 0,
|
|
67
|
+
"rows_in": 0,
|
|
68
|
+
"bytes_in": 0,
|
|
69
|
+
"tables_out": 0,
|
|
70
|
+
"rows_out": 0,
|
|
71
|
+
"bytes_out": 0,
|
|
72
|
+
},
|
|
73
|
+
)
|
|
74
|
+
row["fn_runs"] += int(n.get("fn_runs") or 0)
|
|
75
|
+
row["transactions"] += int(n.get("transactions") or 0)
|
|
76
|
+
row["tables_in"] += int(n.get("table_versions_in") or 0)
|
|
77
|
+
row["rows_in"] += int(n.get("row_count_in") or 0)
|
|
78
|
+
row["bytes_in"] += int(n.get("data_bytes_in") or 0)
|
|
79
|
+
row["tables_out"] += int(n.get("table_versions_out") or 0)
|
|
80
|
+
row["rows_out"] += int(n.get("row_count_out") or 0)
|
|
81
|
+
row["bytes_out"] += int(n.get("data_bytes_out") or 0)
|
|
82
|
+
|
|
83
|
+
show_bytes = any(r["bytes_in"] or r["bytes_out"] for r in agg.values())
|
|
84
|
+
|
|
85
|
+
table = Table(title=None, show_lines=False)
|
|
86
|
+
if is_collection_dim:
|
|
87
|
+
table.add_column("Collection", style="cyan", no_wrap=True)
|
|
88
|
+
table.add_column("Group", no_wrap=True)
|
|
89
|
+
else:
|
|
90
|
+
table.add_column("Group", style="cyan", no_wrap=True)
|
|
91
|
+
table.add_column("Status", no_wrap=True)
|
|
92
|
+
table.add_column("Runs", justify="right")
|
|
93
|
+
table.add_column("Txns", justify="right")
|
|
94
|
+
table.add_column("Tables in", justify="right")
|
|
95
|
+
table.add_column("Rows in", justify="right")
|
|
96
|
+
if show_bytes:
|
|
97
|
+
table.add_column("Bytes in", justify="right")
|
|
98
|
+
table.add_column("Tables out", justify="right")
|
|
99
|
+
table.add_column("Rows out", justify="right")
|
|
100
|
+
if show_bytes:
|
|
101
|
+
table.add_column("Bytes out", justify="right")
|
|
102
|
+
|
|
103
|
+
# Stable order: by dim_id then status.
|
|
104
|
+
for key in sorted(agg.keys()):
|
|
105
|
+
dim_key, grp_id, status = key
|
|
106
|
+
r = agg[key]
|
|
107
|
+
cells: list[str] = []
|
|
108
|
+
if is_collection_dim:
|
|
109
|
+
cells.append(r["collection_name"] or dim_key or "-")
|
|
110
|
+
cells.append(r["group_name"] or grp_id)
|
|
111
|
+
else:
|
|
112
|
+
cells.append(r["group_name"] or dim_key)
|
|
113
|
+
cells.extend([
|
|
114
|
+
status,
|
|
115
|
+
str(r["fn_runs"]),
|
|
116
|
+
str(r["transactions"]),
|
|
117
|
+
str(r["tables_in"]),
|
|
118
|
+
str(r["rows_in"]),
|
|
119
|
+
])
|
|
120
|
+
if show_bytes:
|
|
121
|
+
cells.append(str(r["bytes_in"]))
|
|
122
|
+
cells.extend([str(r["tables_out"]), str(r["rows_out"])])
|
|
123
|
+
if show_bytes:
|
|
124
|
+
cells.append(str(r["bytes_out"]))
|
|
125
|
+
table.add_row(*cells)
|
|
126
|
+
|
|
127
|
+
make_console().print(table)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@cli_command
|
|
131
|
+
@click.group()
|
|
132
|
+
def metrics():
|
|
133
|
+
"""Operational metrics over a window."""
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@metrics.command()
|
|
137
|
+
@proj_option
|
|
138
|
+
@click.option(
|
|
139
|
+
"--dim",
|
|
140
|
+
type=click.Choice(["collection", "group"], case_sensitive=False),
|
|
141
|
+
default=None,
|
|
142
|
+
help="Breakout dimension. Defaults to `collection` server-side.",
|
|
143
|
+
)
|
|
144
|
+
@click.option(
|
|
145
|
+
"--unit",
|
|
146
|
+
type=click.Choice(
|
|
147
|
+
["hour", "day", "week", "month", "quarter", "year"], case_sensitive=False
|
|
148
|
+
),
|
|
149
|
+
default=None,
|
|
150
|
+
help="Window unit. Defaults to `day` server-side.",
|
|
151
|
+
)
|
|
152
|
+
@click.option(
|
|
153
|
+
"--align",
|
|
154
|
+
type=click.Choice(["calendar", "rolling"], case_sensitive=False),
|
|
155
|
+
default=None,
|
|
156
|
+
help="`calendar` (default) snaps the start of the window to the "
|
|
157
|
+
"calendar-unit boundary containing `from` (week/month/quarter/"
|
|
158
|
+
"year-to-date); `rolling` is a fixed `span*unit`-long window "
|
|
159
|
+
"ending exactly at `from`.",
|
|
160
|
+
)
|
|
161
|
+
@click.option(
|
|
162
|
+
"--from",
|
|
163
|
+
"from_ms",
|
|
164
|
+
type=int,
|
|
165
|
+
default=None,
|
|
166
|
+
help="Right edge of the window (UTC epoch ms, exclusive). "
|
|
167
|
+
"Defaults to server-now. Pass a past instant to look at a past "
|
|
168
|
+
"window.",
|
|
169
|
+
)
|
|
170
|
+
@click.option(
|
|
171
|
+
"--span",
|
|
172
|
+
type=int,
|
|
173
|
+
default=None,
|
|
174
|
+
help="Window width in `unit`s. Default 1.",
|
|
175
|
+
)
|
|
176
|
+
@click.option(
|
|
177
|
+
"--collection",
|
|
178
|
+
default=None,
|
|
179
|
+
help="Comma-separated collection ids to include.",
|
|
180
|
+
)
|
|
181
|
+
@click.option(
|
|
182
|
+
"--collection-exclude",
|
|
183
|
+
default=None,
|
|
184
|
+
help="Comma-separated collection ids to exclude.",
|
|
185
|
+
)
|
|
186
|
+
@click.option(
|
|
187
|
+
"--group",
|
|
188
|
+
default=None,
|
|
189
|
+
help="Comma-separated group ids to include.",
|
|
190
|
+
)
|
|
191
|
+
@click.option(
|
|
192
|
+
"--group-exclude",
|
|
193
|
+
default=None,
|
|
194
|
+
help="Comma-separated group ids to exclude.",
|
|
195
|
+
)
|
|
196
|
+
@click.option(
|
|
197
|
+
"--status",
|
|
198
|
+
default=None,
|
|
199
|
+
help="Comma-separated function run statuses to include (e.g. `C,D,E`).",
|
|
200
|
+
)
|
|
201
|
+
@format_option
|
|
202
|
+
@click.pass_context
|
|
203
|
+
def show(
|
|
204
|
+
ctx: click.Context,
|
|
205
|
+
dim: str,
|
|
206
|
+
unit: str,
|
|
207
|
+
align: str,
|
|
208
|
+
from_ms: int,
|
|
209
|
+
span: int,
|
|
210
|
+
collection: str,
|
|
211
|
+
collection_exclude: str,
|
|
212
|
+
group: str,
|
|
213
|
+
group_exclude: str,
|
|
214
|
+
status: str,
|
|
215
|
+
):
|
|
216
|
+
"""Per-collection (or per-group) operational summary over the window.
|
|
217
|
+
|
|
218
|
+
Renders one row per `(dim_entity, status)` with `Runs / Txns /
|
|
219
|
+
Tables in / Rows in / Tables out / Rows out` (bytes columns added
|
|
220
|
+
when any non-zero). Edges are dropped from the rendering — they
|
|
221
|
+
belong in a flow visualization; use `-o json` for the raw shape.
|
|
222
|
+
"""
|
|
223
|
+
verify_login_or_prompt(ctx)
|
|
224
|
+
try:
|
|
225
|
+
server: TabsdataServer = ctx.obj["tabsdataserver"]
|
|
226
|
+
data = server.metrics(
|
|
227
|
+
dim=dim.lower() if dim else None,
|
|
228
|
+
unit=unit.lower() if unit else None,
|
|
229
|
+
align=align.lower() if align else None,
|
|
230
|
+
from_ms=from_ms,
|
|
231
|
+
span=span,
|
|
232
|
+
collection=_split(collection) or None,
|
|
233
|
+
collection_exclude=_split(collection_exclude) or None,
|
|
234
|
+
group=_split(group) or None,
|
|
235
|
+
group_exclude=_split(group_exclude) or None,
|
|
236
|
+
status=_split(status) or None,
|
|
237
|
+
)
|
|
238
|
+
render(ctx, data=data, table=lambda: _render_summary(data))
|
|
239
|
+
except Exception as e:
|
|
240
|
+
hint_common_solutions(ctx, e)
|
|
241
|
+
raise click.ClickException(f"Failed to fetch metrics: {e}")
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
@metrics.command()
|
|
245
|
+
@proj_option
|
|
246
|
+
@click.option(
|
|
247
|
+
"--collection",
|
|
248
|
+
default=None,
|
|
249
|
+
help="Comma-separated collection ids to include.",
|
|
250
|
+
)
|
|
251
|
+
@click.option(
|
|
252
|
+
"--collection-exclude",
|
|
253
|
+
default=None,
|
|
254
|
+
help="Comma-separated collection ids to exclude.",
|
|
255
|
+
)
|
|
256
|
+
@click.option(
|
|
257
|
+
"--group",
|
|
258
|
+
default=None,
|
|
259
|
+
help="Comma-separated group ids to include.",
|
|
260
|
+
)
|
|
261
|
+
@click.option(
|
|
262
|
+
"--group-exclude",
|
|
263
|
+
default=None,
|
|
264
|
+
help="Comma-separated group ids to exclude.",
|
|
265
|
+
)
|
|
266
|
+
@click.option(
|
|
267
|
+
"--status",
|
|
268
|
+
default=None,
|
|
269
|
+
help=(
|
|
270
|
+
"Comma-separated non-terminal function run statuses to include (e.g. `S,R,H`)."
|
|
271
|
+
),
|
|
272
|
+
)
|
|
273
|
+
@format_option
|
|
274
|
+
@click.pass_context
|
|
275
|
+
def inflight(
|
|
276
|
+
ctx: click.Context,
|
|
277
|
+
collection: str,
|
|
278
|
+
collection_exclude: str,
|
|
279
|
+
group: str,
|
|
280
|
+
group_exclude: str,
|
|
281
|
+
status: str,
|
|
282
|
+
):
|
|
283
|
+
"""Point-in-time `/inflight` — non-terminal LFRs per
|
|
284
|
+
(collection, group, status).
|
|
285
|
+
"""
|
|
286
|
+
verify_login_or_prompt(ctx)
|
|
287
|
+
try:
|
|
288
|
+
server: TabsdataServer = ctx.obj["tabsdataserver"]
|
|
289
|
+
data = server.inflight(
|
|
290
|
+
collection=_split(collection) or None,
|
|
291
|
+
collection_exclude=_split(collection_exclude) or None,
|
|
292
|
+
group=_split(group) or None,
|
|
293
|
+
group_exclude=_split(group_exclude) or None,
|
|
294
|
+
status=_split(status) or None,
|
|
295
|
+
)
|
|
296
|
+
rows = data.get("rows") or []
|
|
297
|
+
|
|
298
|
+
def _table():
|
|
299
|
+
if not rows:
|
|
300
|
+
click.echo("(no inflight runs)")
|
|
301
|
+
return
|
|
302
|
+
table = Table(title="Inflight")
|
|
303
|
+
table.add_column("Collection", style="cyan", no_wrap=True)
|
|
304
|
+
table.add_column("Group", no_wrap=True)
|
|
305
|
+
table.add_column("Status", no_wrap=True)
|
|
306
|
+
table.add_column("Count", justify="right")
|
|
307
|
+
for r in rows:
|
|
308
|
+
cid = str(r.get("collection_id") or "")
|
|
309
|
+
gid = str(r.get("group_id") or "")
|
|
310
|
+
cname = r.get("collection_name") or cid
|
|
311
|
+
gname = r.get("group_name") or gid
|
|
312
|
+
table.add_row(
|
|
313
|
+
cname,
|
|
314
|
+
gname,
|
|
315
|
+
str(r.get("status", "")),
|
|
316
|
+
str(r.get("count", 0)),
|
|
317
|
+
)
|
|
318
|
+
make_console().print(table)
|
|
319
|
+
|
|
320
|
+
render(ctx, data=data, table=_table)
|
|
321
|
+
except Exception as e:
|
|
322
|
+
hint_common_solutions(ctx, e)
|
|
323
|
+
raise click.ClickException(f"Failed to fetch inflight: {e}")
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright 2026 Tabsdata Inc.
|
|
3
|
+
#
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
import rich_click as click
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
from tabsdatak.cli._registry import cli_command
|
|
13
|
+
from tabsdatak.cli.cli_utils import (
|
|
14
|
+
MutuallyExclusiveOption,
|
|
15
|
+
format_option,
|
|
16
|
+
hint_common_solutions,
|
|
17
|
+
logical_prompt,
|
|
18
|
+
make_console,
|
|
19
|
+
render,
|
|
20
|
+
verify_login_or_prompt,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# noinspection PyProtectedMember
|
|
24
|
+
from tabsdatak.sdk._permission_types import (
|
|
25
|
+
RBAC_ONLY_WILDCARD_MESSAGE,
|
|
26
|
+
SIMPLE_PROJECT_PERMISSION_TYPES,
|
|
27
|
+
SIMPLE_RBAC_ONLY_WILDCARD_TYPES,
|
|
28
|
+
SIMPLE_SYSTEM_PERMISSION_TYPES,
|
|
29
|
+
VALID_SIMPLE_PERMISSION_TYPES,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from tabsdatak.sdk.tabsdata_server import TabsdataServer
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _validate_permission(perm: str, project: str) -> None:
|
|
37
|
+
"""Client-side validation matching the server's facade rules.
|
|
38
|
+
|
|
39
|
+
- The org-wide read/develop wildcards are RBAC Mode View only.
|
|
40
|
+
- Project-scoped permissions require ``--proj``; system-scoped
|
|
41
|
+
permissions forbid it.
|
|
42
|
+
"""
|
|
43
|
+
perm = perm.lower()
|
|
44
|
+
if perm in SIMPLE_RBAC_ONLY_WILDCARD_TYPES:
|
|
45
|
+
raise click.ClickException(RBAC_ONLY_WILDCARD_MESSAGE)
|
|
46
|
+
if perm not in VALID_SIMPLE_PERMISSION_TYPES:
|
|
47
|
+
raise click.ClickException(
|
|
48
|
+
f"'{perm}' is an invalid permission. The valid values are: "
|
|
49
|
+
f"{', '.join(VALID_SIMPLE_PERMISSION_TYPES)}."
|
|
50
|
+
)
|
|
51
|
+
if perm in SIMPLE_PROJECT_PERMISSION_TYPES and not project:
|
|
52
|
+
raise click.ClickException(
|
|
53
|
+
f"Permission '{perm}' is project-scoped and requires --proj. "
|
|
54
|
+
"The project-scoped permissions are: "
|
|
55
|
+
f"{', '.join(SIMPLE_PROJECT_PERMISSION_TYPES)}."
|
|
56
|
+
)
|
|
57
|
+
if perm in SIMPLE_SYSTEM_PERMISSION_TYPES and project:
|
|
58
|
+
raise click.ClickException(
|
|
59
|
+
f"Permission '{perm}' is system-scoped and does not take a "
|
|
60
|
+
"--proj. The system-scoped permissions are: "
|
|
61
|
+
f"{', '.join(SIMPLE_SYSTEM_PERMISSION_TYPES)}. "
|
|
62
|
+
"Please remove the --proj option."
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@cli_command
|
|
67
|
+
@click.group()
|
|
68
|
+
def permission():
|
|
69
|
+
"""Permission management commands (Simple Security Mode)"""
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@permission.command()
|
|
73
|
+
@click.option(
|
|
74
|
+
"--user",
|
|
75
|
+
"-u",
|
|
76
|
+
help="User to grant the permission to.",
|
|
77
|
+
)
|
|
78
|
+
@click.option(
|
|
79
|
+
"--permission",
|
|
80
|
+
help=(
|
|
81
|
+
"Permission to grant. Valid values are "
|
|
82
|
+
f"{', '.join(VALID_SIMPLE_PERMISSION_TYPES)}. The project-scoped "
|
|
83
|
+
"permissions ("
|
|
84
|
+
f"{', '.join(SIMPLE_PROJECT_PERMISSION_TYPES)}) require --proj; the "
|
|
85
|
+
"system-scoped ones ("
|
|
86
|
+
f"{', '.join(SIMPLE_SYSTEM_PERMISSION_TYPES)}) must not carry --proj."
|
|
87
|
+
),
|
|
88
|
+
)
|
|
89
|
+
@click.option(
|
|
90
|
+
"--proj",
|
|
91
|
+
"project",
|
|
92
|
+
help=(
|
|
93
|
+
"Project the permission applies to. Required for project-scoped "
|
|
94
|
+
"permissions, forbidden for system-scoped ones."
|
|
95
|
+
),
|
|
96
|
+
)
|
|
97
|
+
@click.pass_context
|
|
98
|
+
def grant(ctx: click.Context, user: str, permission: str, project: str):
|
|
99
|
+
"""Grant a permission to a user"""
|
|
100
|
+
verify_login_or_prompt(ctx)
|
|
101
|
+
user = user or logical_prompt(ctx, "User to grant the permission to")
|
|
102
|
+
permission = permission or logical_prompt(
|
|
103
|
+
ctx,
|
|
104
|
+
"Permission to grant. Valid values are "
|
|
105
|
+
f"'{', '.join(VALID_SIMPLE_PERMISSION_TYPES)}'",
|
|
106
|
+
)
|
|
107
|
+
_validate_permission(permission, project)
|
|
108
|
+
try:
|
|
109
|
+
if project:
|
|
110
|
+
click.echo(
|
|
111
|
+
f"Granting permission '{permission}' to user '{user}' on "
|
|
112
|
+
f"project '{project}'"
|
|
113
|
+
)
|
|
114
|
+
else:
|
|
115
|
+
click.echo(f"Granting permission '{permission}' to user '{user}'")
|
|
116
|
+
click.echo("-" * 10)
|
|
117
|
+
server: TabsdataServer = ctx.obj["tabsdataserver"]
|
|
118
|
+
server.grant_permission(user, permission, project)
|
|
119
|
+
click.echo("Permission granted successfully")
|
|
120
|
+
except Exception as e:
|
|
121
|
+
hint_common_solutions(ctx, e)
|
|
122
|
+
raise click.ClickException(f"Failed to grant permission: {e}")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@permission.command()
|
|
126
|
+
@click.option(
|
|
127
|
+
"--user",
|
|
128
|
+
"-u",
|
|
129
|
+
help="User to revoke the permission from.",
|
|
130
|
+
)
|
|
131
|
+
@click.option(
|
|
132
|
+
"--permission",
|
|
133
|
+
help=(
|
|
134
|
+
"Permission to revoke. Valid values are "
|
|
135
|
+
f"{', '.join(VALID_SIMPLE_PERMISSION_TYPES)}. The project-scoped "
|
|
136
|
+
"permissions ("
|
|
137
|
+
f"{', '.join(SIMPLE_PROJECT_PERMISSION_TYPES)}) require --proj; the "
|
|
138
|
+
"system-scoped ones ("
|
|
139
|
+
f"{', '.join(SIMPLE_SYSTEM_PERMISSION_TYPES)}) must not carry --proj."
|
|
140
|
+
),
|
|
141
|
+
)
|
|
142
|
+
@click.option(
|
|
143
|
+
"--proj",
|
|
144
|
+
"project",
|
|
145
|
+
help=(
|
|
146
|
+
"Project the permission applies to. Required for project-scoped "
|
|
147
|
+
"permissions, forbidden for system-scoped ones."
|
|
148
|
+
),
|
|
149
|
+
)
|
|
150
|
+
@click.pass_context
|
|
151
|
+
def revoke(ctx: click.Context, user: str, permission: str, project: str):
|
|
152
|
+
"""Revoke a permission from a user"""
|
|
153
|
+
verify_login_or_prompt(ctx)
|
|
154
|
+
user = user or logical_prompt(ctx, "User to revoke the permission from")
|
|
155
|
+
permission = permission or logical_prompt(
|
|
156
|
+
ctx,
|
|
157
|
+
"Permission to revoke. Valid values are "
|
|
158
|
+
f"'{', '.join(VALID_SIMPLE_PERMISSION_TYPES)}'",
|
|
159
|
+
)
|
|
160
|
+
_validate_permission(permission, project)
|
|
161
|
+
try:
|
|
162
|
+
if project:
|
|
163
|
+
click.echo(
|
|
164
|
+
f"Revoking permission '{permission}' from user '{user}' on "
|
|
165
|
+
f"project '{project}'"
|
|
166
|
+
)
|
|
167
|
+
else:
|
|
168
|
+
click.echo(f"Revoking permission '{permission}' from user '{user}'")
|
|
169
|
+
click.echo("-" * 10)
|
|
170
|
+
server: TabsdataServer = ctx.obj["tabsdataserver"]
|
|
171
|
+
server.revoke_permission(user, permission, project)
|
|
172
|
+
click.echo("Permission revoked successfully")
|
|
173
|
+
except Exception as e:
|
|
174
|
+
hint_common_solutions(ctx, e)
|
|
175
|
+
raise click.ClickException(f"Failed to revoke permission: {e}")
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@permission.command(name="list")
|
|
179
|
+
@click.option(
|
|
180
|
+
"--user",
|
|
181
|
+
"-u",
|
|
182
|
+
help="List the effective flat permissions of this user.",
|
|
183
|
+
cls=MutuallyExclusiveOption,
|
|
184
|
+
mutually_exclusive=["project"],
|
|
185
|
+
)
|
|
186
|
+
@click.option(
|
|
187
|
+
"--proj",
|
|
188
|
+
"project",
|
|
189
|
+
help="List everyone with authority on this project.",
|
|
190
|
+
cls=MutuallyExclusiveOption,
|
|
191
|
+
mutually_exclusive=["user"],
|
|
192
|
+
)
|
|
193
|
+
@format_option
|
|
194
|
+
@click.pass_context
|
|
195
|
+
def list_permissions(ctx: click.Context, user: str, project: str):
|
|
196
|
+
"""List a user's permissions, or who has access to a project"""
|
|
197
|
+
verify_login_or_prompt(ctx)
|
|
198
|
+
if not user and not project:
|
|
199
|
+
raise click.ClickException(
|
|
200
|
+
"Provide either --user (a user's effective permissions) or "
|
|
201
|
+
"--proj (everyone with authority on a project)."
|
|
202
|
+
)
|
|
203
|
+
try:
|
|
204
|
+
server: TabsdataServer = ctx.obj["tabsdataserver"]
|
|
205
|
+
if user:
|
|
206
|
+
_list_by_user(ctx, server, user)
|
|
207
|
+
else:
|
|
208
|
+
_list_by_project(ctx, server, project)
|
|
209
|
+
except click.ClickException:
|
|
210
|
+
raise
|
|
211
|
+
except Exception as e:
|
|
212
|
+
hint_common_solutions(ctx, e)
|
|
213
|
+
raise click.ClickException(f"Failed to list permissions: {e}")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _list_by_user(ctx: click.Context, server: TabsdataServer, user: str):
|
|
217
|
+
permissions = server.list_permissions_by_user(user)
|
|
218
|
+
|
|
219
|
+
def _table():
|
|
220
|
+
table = Table(title="Permissions")
|
|
221
|
+
table.add_column("Permission", style="cyan", no_wrap=True)
|
|
222
|
+
table.add_column("Project")
|
|
223
|
+
for perm in permissions:
|
|
224
|
+
table.add_row(perm.permission, perm.project if perm.project else "-")
|
|
225
|
+
click.echo()
|
|
226
|
+
make_console().print(table)
|
|
227
|
+
click.echo(f"Number of permissions: {len(permissions)}")
|
|
228
|
+
click.echo()
|
|
229
|
+
|
|
230
|
+
render(
|
|
231
|
+
ctx,
|
|
232
|
+
data=[{"permission": p.permission, "project": p.project} for p in permissions],
|
|
233
|
+
table=_table,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _list_by_project(ctx: click.Context, server: TabsdataServer, project: str):
|
|
238
|
+
access = server.list_permissions_by_project(project)
|
|
239
|
+
|
|
240
|
+
def _table():
|
|
241
|
+
table = Table(title="Access")
|
|
242
|
+
table.add_column("User", style="cyan", no_wrap=True)
|
|
243
|
+
table.add_column("Permission")
|
|
244
|
+
table.add_column("On")
|
|
245
|
+
for entry in access:
|
|
246
|
+
table.add_row(entry.user, entry.permission, entry.source)
|
|
247
|
+
click.echo()
|
|
248
|
+
make_console().print(table)
|
|
249
|
+
click.echo(f"Number of grants: {len(access)}")
|
|
250
|
+
click.echo()
|
|
251
|
+
|
|
252
|
+
render(
|
|
253
|
+
ctx,
|
|
254
|
+
data=[
|
|
255
|
+
{"user": e.user, "permission": e.permission, "source": e.source}
|
|
256
|
+
for e in access
|
|
257
|
+
],
|
|
258
|
+
table=_table,
|
|
259
|
+
)
|