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,307 @@
|
|
|
1
|
+
"""Document change review: list, diff, revert.
|
|
2
|
+
|
|
3
|
+
Mirrors ``GET/POST /workspaces/{slug}/changes*`` one-for-one, the way every
|
|
4
|
+
command under this package mirrors a router. `log`/`diff`/`restore` in
|
|
5
|
+
`version.py` answer "what commits exist"; these answer "what *changes* landed,
|
|
6
|
+
by whom, and can I undo one" — the same grouped feed the browser's Changes
|
|
7
|
+
navigator reads, so a CLI user and a browser user see the same history.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
|
|
14
|
+
import click
|
|
15
|
+
|
|
16
|
+
from deepcell_cli.errors import APIError
|
|
17
|
+
from deepcell_cli.context import Ctx, pass_ctx
|
|
18
|
+
from deepcell_cli.output import (
|
|
19
|
+
echo_error, echo_info, echo_success, output, output_mutation,
|
|
20
|
+
)
|
|
21
|
+
from deepcell_cli.commands.version import _print_semantic_diff
|
|
22
|
+
|
|
23
|
+
_REVISION_RE = re.compile(r"^[0-9a-fA-F]{7,64}$")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class RevisionParam(click.ParamType):
|
|
27
|
+
"""A commit sha, checked here rather than at the router.
|
|
28
|
+
|
|
29
|
+
These three endpoints declare ``pattern=^[0-9a-fA-F]{7,64}$`` on their
|
|
30
|
+
revision query params — unlike `/versions/diff`, which takes a bare string.
|
|
31
|
+
Forwarding `HEAD` (or `main`, or a 4-character short sha) therefore bought
|
|
32
|
+
a pydantic 422 that quoted the regex back at the user:
|
|
33
|
+
|
|
34
|
+
Error: request validation failed:
|
|
35
|
+
- from_revision: String should match pattern '^[0-9a-fA-F]{7,64}$'
|
|
36
|
+
|
|
37
|
+
A regex is not an answer to "what should I have typed", and `HEAD` is the
|
|
38
|
+
first thing anyone with git in their fingers reaches for. Same reasoning as
|
|
39
|
+
the `IntRange(1, 200)` on ``--limit``: mirror the constraint the endpoint
|
|
40
|
+
already declares, and spend the error message on the way out.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
name = "revision"
|
|
44
|
+
|
|
45
|
+
def convert(self, value, param, ctx): # noqa: D102
|
|
46
|
+
if _REVISION_RE.match(value):
|
|
47
|
+
return value
|
|
48
|
+
self.fail(
|
|
49
|
+
f"{value!r} is not a revision. Pass a commit sha — 7 to 64 hex "
|
|
50
|
+
"characters, as printed by `deepcell changes list` and "
|
|
51
|
+
"`deepcell log`. Names like HEAD, main or a tag are not resolved "
|
|
52
|
+
"here.",
|
|
53
|
+
param,
|
|
54
|
+
ctx,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
REVISION = RevisionParam()
|
|
59
|
+
|
|
60
|
+
_SOURCE_LABELS = {
|
|
61
|
+
"workbench-agent": "agent",
|
|
62
|
+
"cli": "CLI",
|
|
63
|
+
"browser": "browser",
|
|
64
|
+
"import": "import",
|
|
65
|
+
"restore": "restore",
|
|
66
|
+
"revert": "undo",
|
|
67
|
+
"api": "API",
|
|
68
|
+
"excel-addin": "Excel add-in",
|
|
69
|
+
"document": "document",
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@click.group()
|
|
74
|
+
def changes() -> None:
|
|
75
|
+
"""Review document changes from every writer (agent, CLI, browser, import).
|
|
76
|
+
|
|
77
|
+
\b
|
|
78
|
+
deepcell changes list # newest changes in the workspace
|
|
79
|
+
deepcell changes list model.deepcell # only changes touching one file
|
|
80
|
+
deepcell changes diff <base> <head> # what one change actually did
|
|
81
|
+
deepcell changes revert <base> <head> # undo it as a new commit
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@changes.command("list")
|
|
86
|
+
@click.argument("filename_pos", metavar="[FILENAME]", required=False)
|
|
87
|
+
@click.option("--file", "filename", default=None, help="Filter by filename.")
|
|
88
|
+
@click.option(
|
|
89
|
+
"-n",
|
|
90
|
+
"--limit",
|
|
91
|
+
default=20,
|
|
92
|
+
# The endpoint declares ge=1, le=200; without this the CLI forwards 500 and
|
|
93
|
+
# the user gets a raw pydantic 422 instead of a usage error.
|
|
94
|
+
type=click.IntRange(1, 200),
|
|
95
|
+
help="Number of changes to show (1-200).",
|
|
96
|
+
)
|
|
97
|
+
@click.option(
|
|
98
|
+
"--since",
|
|
99
|
+
"since_revision",
|
|
100
|
+
default=None,
|
|
101
|
+
type=REVISION,
|
|
102
|
+
help="Only changes that moved the branch forward from this revision.",
|
|
103
|
+
)
|
|
104
|
+
@pass_ctx
|
|
105
|
+
def list_changes(
|
|
106
|
+
ctx: Ctx,
|
|
107
|
+
filename_pos: str | None,
|
|
108
|
+
filename: str | None,
|
|
109
|
+
limit: int,
|
|
110
|
+
since_revision: str | None,
|
|
111
|
+
) -> None:
|
|
112
|
+
"""List document changes, newest first.
|
|
113
|
+
|
|
114
|
+
Consecutive commits from one agent run or one CLI invocation are grouped
|
|
115
|
+
into a single change, so `base_revision`..`head_revision` is the range you
|
|
116
|
+
pass to `changes diff` and `changes revert`.
|
|
117
|
+
"""
|
|
118
|
+
if filename_pos and filename:
|
|
119
|
+
raise click.UsageError("Pass FILENAME positionally or via --file, not both.")
|
|
120
|
+
filename = filename_pos or filename
|
|
121
|
+
slug = ctx.require_workspace()
|
|
122
|
+
params: dict = {"max_count": limit}
|
|
123
|
+
if filename:
|
|
124
|
+
params["filename"] = filename
|
|
125
|
+
if since_revision:
|
|
126
|
+
params["since_revision"] = since_revision
|
|
127
|
+
|
|
128
|
+
data = ctx.client.get(f"/workspaces/{slug}/changes", params=params)
|
|
129
|
+
|
|
130
|
+
if ctx.fmt != "plain":
|
|
131
|
+
output(data, ctx.fmt)
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
if data.get("resync_required"):
|
|
135
|
+
# Two situations, opposite remedies. Overflow means the revision was
|
|
136
|
+
# fine and more changes had landed than were asked for — one agent run
|
|
137
|
+
# of 21 commits does it against this command's default of 20 — and
|
|
138
|
+
# saying "unreachable" there sent the user to fix a revision that was
|
|
139
|
+
# never the problem. Anything else is a cursor this history cannot walk
|
|
140
|
+
# from (too far back, unknown, rewritten, not an ancestor), and the
|
|
141
|
+
# server does not say which, so neither do we.
|
|
142
|
+
if data.get("resync_reason") == "overflow":
|
|
143
|
+
echo_info(
|
|
144
|
+
f"More than {limit} changes since that revision. Re-run with a "
|
|
145
|
+
f"larger -n to see them all."
|
|
146
|
+
)
|
|
147
|
+
else:
|
|
148
|
+
echo_info(
|
|
149
|
+
"Cannot list changes from that revision — it is unreachable or "
|
|
150
|
+
"too far back. Re-run without --since for the current history."
|
|
151
|
+
)
|
|
152
|
+
return
|
|
153
|
+
entries = data.get("changes") or []
|
|
154
|
+
if not entries:
|
|
155
|
+
echo_info("No changes yet.")
|
|
156
|
+
return
|
|
157
|
+
for entry in entries:
|
|
158
|
+
_print_change(entry)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
_TITLE_KEYS = {
|
|
162
|
+
"revert": lambda p: f"Undo of {p.get('revision', '?')}",
|
|
163
|
+
"untitled": lambda p: "Document update",
|
|
164
|
+
"syncFromCli": lambda p: "Synced from the terminal",
|
|
165
|
+
"batchEdit": lambda p: f"Edited {p['detail']}" if p.get("detail") else "Edited values",
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _change_title(entry: dict) -> str:
|
|
170
|
+
"""What to print for a change whose subject this server wrote.
|
|
171
|
+
|
|
172
|
+
`Revert change a1b2c3d4` and `[batch-edit] Revenue[FY2025]` are commit
|
|
173
|
+
bookkeeping, not a description anybody typed. The server says which of the
|
|
174
|
+
two it is via `title_key`; an unknown key falls back to the raw subject, so
|
|
175
|
+
an older CLI against a newer server keeps exactly what it had.
|
|
176
|
+
"""
|
|
177
|
+
key = entry.get("title_key")
|
|
178
|
+
render = _TITLE_KEYS.get(key) if key else None
|
|
179
|
+
if render:
|
|
180
|
+
return render(entry.get("title_params") or {})
|
|
181
|
+
return entry.get("title") or "(no title)"
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _print_change(entry: dict) -> None:
|
|
185
|
+
"""One change as a three-line block: title, provenance, revision range."""
|
|
186
|
+
source = _SOURCE_LABELS.get(entry.get("source", ""), entry.get("source", "?"))
|
|
187
|
+
click.echo(click.style(_change_title(entry), bold=True))
|
|
188
|
+
detail = f" {source} · {entry.get('author', '?')} · {entry.get('timestamp', '?')}"
|
|
189
|
+
if entry.get("command"):
|
|
190
|
+
detail += f" · {entry['command']}"
|
|
191
|
+
click.echo(detail)
|
|
192
|
+
base = entry.get("base_revision")
|
|
193
|
+
head = entry.get("head_revision") or "?"
|
|
194
|
+
files = entry.get("files_changed") or []
|
|
195
|
+
click.echo(
|
|
196
|
+
f" {(base[:8] if base else '(root)')}..{head[:8]} · "
|
|
197
|
+
+ (", ".join(str(f) for f in files) if files else "no files")
|
|
198
|
+
)
|
|
199
|
+
click.echo("")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@changes.command("diff")
|
|
203
|
+
@click.argument("base_revision", type=REVISION)
|
|
204
|
+
@click.argument("head_revision", required=False, type=REVISION)
|
|
205
|
+
@click.option("--file", "filename", default=None, help="Filter diff by filename.")
|
|
206
|
+
@pass_ctx
|
|
207
|
+
def diff_change(
|
|
208
|
+
ctx: Ctx,
|
|
209
|
+
base_revision: str,
|
|
210
|
+
head_revision: str | None,
|
|
211
|
+
filename: str | None,
|
|
212
|
+
) -> None:
|
|
213
|
+
"""Show what one change did, semantically.
|
|
214
|
+
|
|
215
|
+
\b
|
|
216
|
+
deepcell changes diff abc1234 # that revision to HEAD
|
|
217
|
+
deepcell changes diff abc1234 def5678 # one change's exact range
|
|
218
|
+
"""
|
|
219
|
+
slug = ctx.require_workspace()
|
|
220
|
+
params: dict = {"from_revision": base_revision}
|
|
221
|
+
if head_revision:
|
|
222
|
+
params["to_revision"] = head_revision
|
|
223
|
+
if filename:
|
|
224
|
+
params["filename"] = filename
|
|
225
|
+
|
|
226
|
+
try:
|
|
227
|
+
data = ctx.client.get(f"/workspaces/{slug}/changes/diff", params=params)
|
|
228
|
+
except APIError as e:
|
|
229
|
+
if e.status_code == 400:
|
|
230
|
+
echo_error(e.detail)
|
|
231
|
+
raise SystemExit(1) from e
|
|
232
|
+
raise
|
|
233
|
+
|
|
234
|
+
if ctx.fmt != "plain":
|
|
235
|
+
# The list, not the envelope: `output`'s table path wraps a dict as a
|
|
236
|
+
# single row, which put the repr of every change in one cell.
|
|
237
|
+
output(data.get("files") if ctx.fmt == "table" else data, ctx.fmt)
|
|
238
|
+
return
|
|
239
|
+
|
|
240
|
+
files = data.get("files") or []
|
|
241
|
+
if not files:
|
|
242
|
+
echo_info("No differences found.")
|
|
243
|
+
return
|
|
244
|
+
for entry in files:
|
|
245
|
+
click.echo(
|
|
246
|
+
click.style(
|
|
247
|
+
f"--- {entry.get('file_path', '')} "
|
|
248
|
+
f"({entry.get('change_type', 'modified')})",
|
|
249
|
+
bold=True,
|
|
250
|
+
)
|
|
251
|
+
)
|
|
252
|
+
if entry.get("semantic_diff"):
|
|
253
|
+
_print_semantic_diff(entry["semantic_diff"])
|
|
254
|
+
else:
|
|
255
|
+
# A semantic diff needs BOTH sides of a `.deepcell` file, so an
|
|
256
|
+
# added or deleted document — and any non-`.deepcell` file in a
|
|
257
|
+
# mixed commit — has none. Saying so beats printing a bare header
|
|
258
|
+
# and exiting 0, which reads as "this change did nothing".
|
|
259
|
+
echo_info(
|
|
260
|
+
" No semantic diff for this file "
|
|
261
|
+
"(added, deleted, or not a .deepcell document)."
|
|
262
|
+
)
|
|
263
|
+
click.echo("")
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
@changes.command("revert")
|
|
267
|
+
@click.argument("base_revision", type=REVISION)
|
|
268
|
+
@click.argument("head_revision", type=REVISION)
|
|
269
|
+
@click.option("-y", "--yes", is_flag=True, help="Skip confirmation prompt.")
|
|
270
|
+
@pass_ctx
|
|
271
|
+
def revert_change(
|
|
272
|
+
ctx: Ctx,
|
|
273
|
+
base_revision: str,
|
|
274
|
+
head_revision: str,
|
|
275
|
+
yes: bool,
|
|
276
|
+
) -> None:
|
|
277
|
+
"""Undo one change by committing its inverse onto the current tip.
|
|
278
|
+
|
|
279
|
+
This is not a restore: work committed *after* the change survives, as long
|
|
280
|
+
as it touched different content. If it overlaps, nothing is committed and
|
|
281
|
+
the command reports the conflict — resolve it by editing, not by reverting.
|
|
282
|
+
Reverting is itself a new commit, so nothing is lost for good.
|
|
283
|
+
"""
|
|
284
|
+
slug = ctx.require_workspace()
|
|
285
|
+
if not yes:
|
|
286
|
+
# After `require_workspace`, not before: approving a revert and only
|
|
287
|
+
# then being told there is no workspace wastes the one question this
|
|
288
|
+
# command asks, and trains people to type -y.
|
|
289
|
+
click.confirm(
|
|
290
|
+
f"Revert the change {base_revision[:8]}..{head_revision[:8]}?",
|
|
291
|
+
abort=True,
|
|
292
|
+
)
|
|
293
|
+
try:
|
|
294
|
+
data = ctx.client.post(
|
|
295
|
+
f"/workspaces/{slug}/changes/revert",
|
|
296
|
+
json={"base_revision": base_revision, "head_revision": head_revision},
|
|
297
|
+
)
|
|
298
|
+
except APIError as e:
|
|
299
|
+
if e.status_code == 409:
|
|
300
|
+
# The server refuses rather than committing a half-undo; say which
|
|
301
|
+
# files collided so the next step is obvious.
|
|
302
|
+
echo_error(e.detail)
|
|
303
|
+
raise SystemExit(1) from e
|
|
304
|
+
raise
|
|
305
|
+
|
|
306
|
+
output_mutation(data, ctx.fmt, plain_key="sha")
|
|
307
|
+
echo_success(f"Reverted {head_revision[:8]} as a new revision")
|