deepcell-cli 0.6.1__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.
- deepcell_cli/__init__.py +12 -0
- deepcell_cli/__main__.py +5 -0
- deepcell_cli/_findings.py +84 -0
- deepcell_cli/capabilities.py +560 -0
- deepcell_cli/capability-contract.json +15622 -0
- deepcell_cli/client.py +503 -0
- deepcell_cli/commands/__init__.py +1 -0
- deepcell_cli/commands/_batch_input.py +29 -0
- deepcell_cli/commands/_datatypes.py +56 -0
- deepcell_cli/commands/_negative_args.py +133 -0
- deepcell_cli/commands/_swapped_args.py +153 -0
- deepcell_cli/commands/_version_display.py +40 -0
- deepcell_cli/commands/_write_opts.py +139 -0
- deepcell_cli/commands/account.py +123 -0
- deepcell_cli/commands/auth.py +610 -0
- deepcell_cli/commands/changes.py +307 -0
- deepcell_cli/commands/deck.py +594 -0
- deepcell_cli/commands/defs.py +3890 -0
- deepcell_cli/commands/describe.py +902 -0
- deepcell_cli/commands/doc.py +529 -0
- deepcell_cli/commands/doctor.py +257 -0
- deepcell_cli/commands/download.py +36 -0
- deepcell_cli/commands/edit.py +384 -0
- deepcell_cli/commands/example.py +161 -0
- deepcell_cli/commands/export.py +81 -0
- deepcell_cli/commands/export_docx.py +57 -0
- deepcell_cli/commands/export_pdf.py +66 -0
- deepcell_cli/commands/export_pptx.py +45 -0
- deepcell_cli/commands/files.py +386 -0
- deepcell_cli/commands/grep.py +90 -0
- deepcell_cli/commands/guide.py +431 -0
- deepcell_cli/commands/help_cmd.py +348 -0
- deepcell_cli/commands/impact.py +382 -0
- deepcell_cli/commands/import_cmd.py +208 -0
- deepcell_cli/commands/ingest.py +110 -0
- deepcell_cli/commands/merge.py +399 -0
- deepcell_cli/commands/query.py +718 -0
- deepcell_cli/commands/reasoning.py +2981 -0
- deepcell_cli/commands/ref.py +279 -0
- deepcell_cli/commands/replace.py +326 -0
- deepcell_cli/commands/rules.py +206 -0
- deepcell_cli/commands/share.py +186 -0
- deepcell_cli/commands/sync.py +804 -0
- deepcell_cli/commands/upgrade.py +185 -0
- deepcell_cli/commands/variant.py +353 -0
- deepcell_cli/commands/version.py +445 -0
- deepcell_cli/commands/viewer.py +54 -0
- deepcell_cli/commands/workspace.py +101 -0
- deepcell_cli/config.py +352 -0
- deepcell_cli/context.py +187 -0
- deepcell_cli/errors.py +141 -0
- deepcell_cli/logging_setup.py +161 -0
- deepcell_cli/main.py +518 -0
- deepcell_cli/mcp_server.py +906 -0
- deepcell_cli/oauth_provider.py +580 -0
- deepcell_cli/output.py +503 -0
- deepcell_cli/revision.py +164 -0
- deepcell_cli/stages.py +223 -0
- deepcell_cli/surface.py +628 -0
- deepcell_cli/sync_state.py +120 -0
- deepcell_cli/upgrade_check.py +399 -0
- deepcell_cli/xml_replace.py +89 -0
- deepcell_cli-0.6.1.dist-info/METADATA +264 -0
- deepcell_cli-0.6.1.dist-info/RECORD +67 -0
- deepcell_cli-0.6.1.dist-info/WHEEL +5 -0
- deepcell_cli-0.6.1.dist-info/entry_points.txt +3 -0
- deepcell_cli-0.6.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""``deepcell rules`` — invariants scoped to the selected work shape.
|
|
2
|
+
|
|
3
|
+
A sibling of ``deepcell guide``, split out because rules must be selected for
|
|
4
|
+
the work shape before writing and verified before done. See
|
|
5
|
+
docs/cli-agent-surface.md §4.
|
|
6
|
+
|
|
7
|
+
The listing leads with enforcement rather than description: what an agent needs
|
|
8
|
+
from the index is which invariants a linter will catch for it, and which it
|
|
9
|
+
must verify itself. Lint findings print the id of the rule they enforce, so
|
|
10
|
+
``rules R2`` is the other half of a failure the agent has already hit.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import click
|
|
16
|
+
|
|
17
|
+
from deepcell_cli.context import Ctx, pass_ctx
|
|
18
|
+
from deepcell_cli.output import output, print_plain
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _format_enforcement(rule: dict) -> str:
|
|
22
|
+
"""One-line enforcement summary for the listing."""
|
|
23
|
+
if rule.get("enforcement") != "lint":
|
|
24
|
+
return "review"
|
|
25
|
+
codes = ", ".join(rule.get("lint") or [])
|
|
26
|
+
command = rule.get("command")
|
|
27
|
+
return f"{command} — {codes}" if command else codes
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _format_scope(rule: dict) -> str:
|
|
31
|
+
scopes = list(rule.get("surfaces") or [])
|
|
32
|
+
if rule.get("pack"):
|
|
33
|
+
scopes.append(f"pack:{rule['pack']}")
|
|
34
|
+
return ",".join(scopes) if scopes else "universal"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _selection_notes(
|
|
38
|
+
rules: list[dict], *, surfaces: str | None, pack: str | None
|
|
39
|
+
) -> list[str]:
|
|
40
|
+
"""What a `--surfaces` / `--pack` selection did NOT add, one line each.
|
|
41
|
+
|
|
42
|
+
Computed from the rows rather than asked of the server: every row carries
|
|
43
|
+
its `surfaces` and `pack`, so a selected surface that no row names, or a
|
|
44
|
+
pack no row belongs to, is visible in the answer already. Nothing is
|
|
45
|
+
said when the selection worked — the scope column shows that.
|
|
46
|
+
"""
|
|
47
|
+
notes: list[str] = []
|
|
48
|
+
selected = [s.strip() for s in (surfaces or "").split(",") if s.strip()]
|
|
49
|
+
if selected:
|
|
50
|
+
covered = {s for r in rules for s in (r.get("surfaces") or [])}
|
|
51
|
+
missing = [s for s in selected if s not in covered]
|
|
52
|
+
if missing:
|
|
53
|
+
rest = (
|
|
54
|
+
"universal rules follow"
|
|
55
|
+
if len(missing) == len(selected)
|
|
56
|
+
else "universal rules and the other selected surfaces' rules follow"
|
|
57
|
+
)
|
|
58
|
+
notes.append(f"No {'/'.join(missing)}-specific rules; {rest}.")
|
|
59
|
+
if pack and not any(r.get("pack") == pack for r in rules):
|
|
60
|
+
notes.append(
|
|
61
|
+
f"The {pack} pack contributes no rules to this selection — a pack "
|
|
62
|
+
"rule also needs its surface selected; `deepcell rules --all` "
|
|
63
|
+
"shows every rule's scope."
|
|
64
|
+
)
|
|
65
|
+
return notes
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@click.command()
|
|
69
|
+
@click.argument("rule_id", required=False)
|
|
70
|
+
@click.option(
|
|
71
|
+
"--pack",
|
|
72
|
+
default=None,
|
|
73
|
+
help="Add this domain pack's rules when their selected surface applies.",
|
|
74
|
+
)
|
|
75
|
+
@click.option(
|
|
76
|
+
"--surfaces",
|
|
77
|
+
default=None,
|
|
78
|
+
metavar="LIST",
|
|
79
|
+
help="Comma-separated selected surfaces: reasoning,spreadsheet,document,deck. "
|
|
80
|
+
"Omit for universal rules only.",
|
|
81
|
+
)
|
|
82
|
+
@click.option(
|
|
83
|
+
"--all",
|
|
84
|
+
"all_rules",
|
|
85
|
+
is_flag=True,
|
|
86
|
+
help="Every rule, whatever work shape it applies to. Overrides "
|
|
87
|
+
"--surfaces/--pack.",
|
|
88
|
+
)
|
|
89
|
+
@click.option(
|
|
90
|
+
"--full",
|
|
91
|
+
is_flag=True,
|
|
92
|
+
help="Print every applicable rule in full, not just the index — one call, "
|
|
93
|
+
"no per-rule round trips.",
|
|
94
|
+
)
|
|
95
|
+
@pass_ctx
|
|
96
|
+
def rules(
|
|
97
|
+
ctx: Ctx,
|
|
98
|
+
rule_id: str | None,
|
|
99
|
+
pack: str | None,
|
|
100
|
+
surfaces: str | None,
|
|
101
|
+
all_rules: bool,
|
|
102
|
+
full: bool,
|
|
103
|
+
) -> None:
|
|
104
|
+
"""The invariants applicable to a .deepcell work shape.
|
|
105
|
+
|
|
106
|
+
\b
|
|
107
|
+
Universal rules: deepcell rules
|
|
108
|
+
For a work shape: deepcell rules --surfaces reasoning,document
|
|
109
|
+
Finance grid: deepcell rules --surfaces spreadsheet --pack finance
|
|
110
|
+
Every rule: deepcell rules --all
|
|
111
|
+
Read one: deepcell rules R2
|
|
112
|
+
|
|
113
|
+
Rule ids are stable — a lint finding cites the rule it enforces, so the id
|
|
114
|
+
printed by a failure is the argument to pass here.
|
|
115
|
+
"""
|
|
116
|
+
if rule_id:
|
|
117
|
+
if pack or surfaces or all_rules:
|
|
118
|
+
raise click.UsageError(
|
|
119
|
+
"--all/--surfaces/--pack select a rulebook; omit them when "
|
|
120
|
+
"reading one id."
|
|
121
|
+
)
|
|
122
|
+
data = ctx.client.get(f"/rules/{rule_id}")
|
|
123
|
+
if ctx.fmt == "json":
|
|
124
|
+
output(data, ctx.fmt)
|
|
125
|
+
return
|
|
126
|
+
# A non-dict here means the API returned something this command cannot
|
|
127
|
+
# read; print it rather than crashing on .get().
|
|
128
|
+
if not isinstance(data, dict):
|
|
129
|
+
print_plain(str(data))
|
|
130
|
+
return
|
|
131
|
+
body = data.get("body", "")
|
|
132
|
+
header = f"{data.get('id')}. {data.get('title')}"
|
|
133
|
+
lines = [header, "=" * len(header), ""]
|
|
134
|
+
lines.append(f"Enforcement: {_format_enforcement(data)}")
|
|
135
|
+
lines.append(f"Applies to: {_format_scope(data)}")
|
|
136
|
+
see_also = data.get("see_also") or []
|
|
137
|
+
if see_also:
|
|
138
|
+
lines.append(f"See also: {' · '.join(see_also)}")
|
|
139
|
+
lines += ["", str(body).rstrip()]
|
|
140
|
+
print_plain("\n".join(lines))
|
|
141
|
+
return
|
|
142
|
+
|
|
143
|
+
params = {
|
|
144
|
+
key: value
|
|
145
|
+
for key, value in {
|
|
146
|
+
"surfaces": None if all_rules else surfaces,
|
|
147
|
+
"pack": None if all_rules else pack,
|
|
148
|
+
"all": "true" if all_rules else None,
|
|
149
|
+
}.items()
|
|
150
|
+
if value
|
|
151
|
+
}
|
|
152
|
+
data = ctx.client.get("/rules", params=params or None)
|
|
153
|
+
rule_list = data.get("rules", []) if isinstance(data, dict) else data
|
|
154
|
+
|
|
155
|
+
if ctx.fmt == "json":
|
|
156
|
+
output(data, ctx.fmt)
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
# Say when a selection added nothing, before printing what it did not
|
|
160
|
+
# narrow. `--surfaces deck` returns exactly the universal set — no rule is
|
|
161
|
+
# deck-scoped — and `--pack research` returns it too, and both printed
|
|
162
|
+
# that set with nothing on screen saying the selector had no effect. A
|
|
163
|
+
# reader takes silence as "these are the deck rules".
|
|
164
|
+
notes = (
|
|
165
|
+
_selection_notes(rule_list, surfaces=surfaces, pack=pack)
|
|
166
|
+
if isinstance(rule_list, list) and not all_rules
|
|
167
|
+
else []
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# --full is the applicable rulebook. It is a flag rather than the default
|
|
171
|
+
# because the default has to match what `--help` promises and what
|
|
172
|
+
# `deepcell guide` does with no argument: print an index.
|
|
173
|
+
if full and isinstance(data, dict) and data.get("markdown"):
|
|
174
|
+
print_plain("\n".join(notes + [""] + [data["markdown"]]) if notes else data["markdown"])
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
if not isinstance(rule_list, list) or not rule_list:
|
|
178
|
+
# No rules to tabulate — say so plainly rather than rendering the raw
|
|
179
|
+
# envelope as a one-row table, which is what `output()` would do.
|
|
180
|
+
print_plain(
|
|
181
|
+
"No rules available. The rule library is empty or could not be "
|
|
182
|
+
"read on the server."
|
|
183
|
+
)
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
id_width = max(len(str(r.get("id", ""))) for r in rule_list)
|
|
187
|
+
title_width = max(len(str(r.get("title", ""))) for r in rule_list)
|
|
188
|
+
lines = notes + [""] if notes else []
|
|
189
|
+
lines += [
|
|
190
|
+
f"{str(r.get('id', '')):<{id_width}} {str(r.get('title', '')):<{title_width}} "
|
|
191
|
+
f"{_format_scope(r):<30} {_format_enforcement(r)}".rstrip()
|
|
192
|
+
for r in rule_list
|
|
193
|
+
]
|
|
194
|
+
unenforced = [r["id"] for r in rule_list if r.get("enforcement") != "lint"]
|
|
195
|
+
lines += ["", "Read a rule: deepcell rules <id>"]
|
|
196
|
+
if not all_rules:
|
|
197
|
+
# Without this the listing is silently partial: a bare call shows the
|
|
198
|
+
# universal rules only, and nothing on screen says the rest exist.
|
|
199
|
+
lines[-1:] = [
|
|
200
|
+
"Select work surfaces: deepcell rules --surfaces reasoning,document",
|
|
201
|
+
"Every rule: deepcell rules --all",
|
|
202
|
+
"Read a rule: deepcell rules <id>",
|
|
203
|
+
]
|
|
204
|
+
if unenforced:
|
|
205
|
+
lines.append(f"Nothing lints {', '.join(unenforced)} — verify these yourself.")
|
|
206
|
+
print_plain("\n".join(lines))
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""Share-link commands: create, list, revoke.
|
|
2
|
+
|
|
3
|
+
Thin pass-through to the sharing router (``/workspaces/{slug}/shares`` +
|
|
4
|
+
``DELETE .../shares/{id}``). ``share create`` prints the frontend viewer URL
|
|
5
|
+
so a model can be opened in the browser by anyone with the link — including
|
|
6
|
+
straight from an anonymous session (view-only links, server-enforced expiry).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
|
|
13
|
+
from deepcell_cli.config import frontend_base_url, is_anonymous_session
|
|
14
|
+
from deepcell_cli.context import Ctx, pass_ctx
|
|
15
|
+
from deepcell_cli.output import echo_success, output
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@click.group()
|
|
19
|
+
def share() -> None:
|
|
20
|
+
"""Create and manage browser share links for .deepcell files.
|
|
21
|
+
|
|
22
|
+
A share link gives anyone with the URL access to one file in the web
|
|
23
|
+
viewer — view-only by default, no sign-in needed to look. View-only links
|
|
24
|
+
can be created without an account (they expire after a few days); edit
|
|
25
|
+
links and password protection require one (`deepcell login`).
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@share.command("create")
|
|
30
|
+
@click.argument("filename")
|
|
31
|
+
@click.option(
|
|
32
|
+
"--permission",
|
|
33
|
+
type=click.Choice(["view", "edit"]),
|
|
34
|
+
default="view",
|
|
35
|
+
show_default=True,
|
|
36
|
+
help="Access level for the link. Edit links require an account.",
|
|
37
|
+
)
|
|
38
|
+
@click.option(
|
|
39
|
+
"--expires-days",
|
|
40
|
+
type=click.IntRange(1, 365),
|
|
41
|
+
default=None,
|
|
42
|
+
help=(
|
|
43
|
+
"Days until the link expires. Every link expires; omit this to take the "
|
|
44
|
+
"server default (90 days, or 7 for a password-less --permission edit "
|
|
45
|
+
"link). Larger values are clamped to that cap."
|
|
46
|
+
),
|
|
47
|
+
)
|
|
48
|
+
@click.option(
|
|
49
|
+
"--expires-hours",
|
|
50
|
+
type=click.IntRange(1, 365 * 24),
|
|
51
|
+
default=None,
|
|
52
|
+
help=(
|
|
53
|
+
"The same lifetime in hours, for a link that should outlive a meeting "
|
|
54
|
+
"and not a day. Clamped against the same caps as --expires-days; pass "
|
|
55
|
+
"one or the other, not both."
|
|
56
|
+
),
|
|
57
|
+
)
|
|
58
|
+
@click.option("--password", default=None, help="Password-protect the link (account required).")
|
|
59
|
+
@click.option("--label", default=None, help="Label to recognize the link in `share list`.")
|
|
60
|
+
@pass_ctx
|
|
61
|
+
def share_create(
|
|
62
|
+
ctx: Ctx,
|
|
63
|
+
filename: str,
|
|
64
|
+
permission: str,
|
|
65
|
+
expires_days: int | None,
|
|
66
|
+
expires_hours: int | None,
|
|
67
|
+
password: str | None,
|
|
68
|
+
label: str | None,
|
|
69
|
+
) -> None:
|
|
70
|
+
"""Create a share link for FILENAME and print its viewer URL."""
|
|
71
|
+
slug = ctx.require_workspace()
|
|
72
|
+
if expires_days is not None and expires_hours is not None:
|
|
73
|
+
# Not a precedence rule: two stated lifetimes mean only one of them is
|
|
74
|
+
# what the caller meant, and silently preferring either hands out a
|
|
75
|
+
# capability that lives longer or shorter than they asked for.
|
|
76
|
+
raise click.UsageError(
|
|
77
|
+
"Pass --expires-days or --expires-hours, not both — they are the "
|
|
78
|
+
"same lifetime in different units."
|
|
79
|
+
)
|
|
80
|
+
body: dict = {"filename": filename, "permission": permission}
|
|
81
|
+
if expires_days is not None:
|
|
82
|
+
body["expires_in_days"] = expires_days
|
|
83
|
+
if expires_hours is not None:
|
|
84
|
+
body["expires_in_hours"] = expires_hours
|
|
85
|
+
if password:
|
|
86
|
+
body["password"] = password
|
|
87
|
+
if label:
|
|
88
|
+
body["label"] = label
|
|
89
|
+
row = ctx.client.post(f"/workspaces/{slug}/shares", json=body)
|
|
90
|
+
|
|
91
|
+
url = f"{frontend_base_url()}/share/{row['share_token']}"
|
|
92
|
+
if ctx.fmt == "json":
|
|
93
|
+
# Structured consumers (MCP forces json) need token/id/url as data,
|
|
94
|
+
# not scraped out of the pretty two-line output below.
|
|
95
|
+
output({**row, "url": url}, "json")
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
click.echo(url)
|
|
99
|
+
details = [f"Permission: {row['permission']}"]
|
|
100
|
+
if row.get("expires_at"):
|
|
101
|
+
details.append(f"Expires: {row['expires_at']}")
|
|
102
|
+
if row.get("has_password"):
|
|
103
|
+
details.append("Password-protected")
|
|
104
|
+
if row.get("id"):
|
|
105
|
+
# The id is the only input `share revoke` accepts — without it here,
|
|
106
|
+
# revoking requires a `share list -f json` round-trip.
|
|
107
|
+
details.append(f"Id: {row['id']}")
|
|
108
|
+
click.echo(" · ".join(details))
|
|
109
|
+
if not is_anonymous_session():
|
|
110
|
+
# The share link is for handing off; the owner's own browsing should
|
|
111
|
+
# go through the authenticated workbench (stderr keeps stdout to the
|
|
112
|
+
# URL + details lines only, so `head -1` still yields the URL).
|
|
113
|
+
click.echo(
|
|
114
|
+
f"For your own editing, `deepcell viewer {filename}` opens your workbench.",
|
|
115
|
+
err=True,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _share_row_plain(row: dict) -> str:
|
|
120
|
+
"""One scannable line per link: id, permission, filename, URL, annotations.
|
|
121
|
+
|
|
122
|
+
The generic list formatter used to reduce rows to bare filenames, leaving
|
|
123
|
+
plain mode with no path to a revocable id or a reconstructable URL.
|
|
124
|
+
"""
|
|
125
|
+
parts = [
|
|
126
|
+
str(row.get("id", "")),
|
|
127
|
+
str(row.get("permission", "")),
|
|
128
|
+
str(row.get("filename", "")),
|
|
129
|
+
f"{frontend_base_url()}/share/{row.get('share_token', '')}",
|
|
130
|
+
]
|
|
131
|
+
notes: list[str] = []
|
|
132
|
+
if row.get("label"):
|
|
133
|
+
notes.append(f'label "{row["label"]}"')
|
|
134
|
+
if row.get("expires_at"):
|
|
135
|
+
notes.append(f"expires {row['expires_at']}")
|
|
136
|
+
if row.get("has_password"):
|
|
137
|
+
notes.append("password")
|
|
138
|
+
if not row.get("is_active", True):
|
|
139
|
+
notes.append("revoked")
|
|
140
|
+
if row.get("view_count"):
|
|
141
|
+
notes.append(f"{row['view_count']} views")
|
|
142
|
+
line = " ".join(p for p in parts if p)
|
|
143
|
+
if notes:
|
|
144
|
+
line += f" ({', '.join(notes)})"
|
|
145
|
+
return line
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _share_row_with_url(row: dict) -> dict:
|
|
149
|
+
"""Add the same usable viewer URL exposed by plain and create output."""
|
|
150
|
+
token = row.get("share_token")
|
|
151
|
+
if not token:
|
|
152
|
+
return row
|
|
153
|
+
return {**row, "url": f"{frontend_base_url()}/share/{token}"}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@share.command("list")
|
|
157
|
+
@click.option("--file", "filename", default=None, help="Only links for this file.")
|
|
158
|
+
@pass_ctx
|
|
159
|
+
def share_list(ctx: Ctx, filename: str | None) -> None:
|
|
160
|
+
"""List active share links in the workspace."""
|
|
161
|
+
slug = ctx.require_workspace()
|
|
162
|
+
params = {"filename": filename} if filename else None
|
|
163
|
+
data = ctx.client.get(f"/workspaces/{slug}/shares", params=params)
|
|
164
|
+
if isinstance(data, list):
|
|
165
|
+
data = [
|
|
166
|
+
_share_row_with_url(row) if isinstance(row, dict) else row
|
|
167
|
+
for row in data
|
|
168
|
+
]
|
|
169
|
+
if ctx.fmt == "plain" and isinstance(data, list):
|
|
170
|
+
if not data:
|
|
171
|
+
click.echo("(no share links)")
|
|
172
|
+
for row in data:
|
|
173
|
+
if isinstance(row, dict):
|
|
174
|
+
click.echo(_share_row_plain(row))
|
|
175
|
+
return
|
|
176
|
+
output(data, ctx.fmt)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@share.command("revoke")
|
|
180
|
+
@click.argument("share_id")
|
|
181
|
+
@pass_ctx
|
|
182
|
+
def share_revoke(ctx: Ctx, share_id: str) -> None:
|
|
183
|
+
"""Revoke a share link by its id (see `share list`)."""
|
|
184
|
+
slug = ctx.require_workspace()
|
|
185
|
+
ctx.client.delete(f"/workspaces/{slug}/shares/{share_id}")
|
|
186
|
+
echo_success("Share link revoked.")
|