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.
Files changed (39) hide show
  1. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/__init__.py +5 -0
  2. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/_registry.py +78 -0
  3. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/assets/manifest/BANNER +5 -0
  4. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/assets/manifest/LICENSE +3 -0
  5. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/assets/manifest/README-PyPi.md +8 -0
  6. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/assets/manifest/RELEASE-NOTES +1 -0
  7. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/assets/manifest/THIRD-PARTY +3 -0
  8. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/assets/manifest/VERSION +1 -0
  9. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/cli_utils.py +993 -0
  10. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/shell.py +390 -0
  11. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/__init__.py +3 -0
  12. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/auth_group.py +197 -0
  13. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/cli.py +508 -0
  14. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/collection_group.py +458 -0
  15. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/config_utils.py +93 -0
  16. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/connection_group.py +159 -0
  17. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/examples_group.py +206 -0
  18. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/execution_utils.py +429 -0
  19. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/fn_group.py +1109 -0
  20. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/fnrun_group.py +285 -0
  21. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/group_group.py +184 -0
  22. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/metrics_group.py +323 -0
  23. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/permission_group.py +259 -0
  24. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/plan_group.py +324 -0
  25. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/project_group.py +170 -0
  26. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/role_group.py +630 -0
  27. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/settings_group.py +344 -0
  28. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/table_group.py +600 -0
  29. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/trigger_group.py +338 -0
  30. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/trx_group.py +329 -0
  31. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/user_group.py +200 -0
  32. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/worker_group.py +185 -0
  33. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/template.py +781 -0
  34. tabsdata_cli-2.0.0.data/purelib/tabsdatak/py.typed +0 -0
  35. tabsdata_cli-2.0.0.dist-info/METADATA +39 -0
  36. tabsdata_cli-2.0.0.dist-info/RECORD +39 -0
  37. tabsdata_cli-2.0.0.dist-info/WHEEL +5 -0
  38. tabsdata_cli-2.0.0.dist-info/licenses/src/main/python/tabsdatak/cli/assets/manifest/LICENSE +3 -0
  39. 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
+ )