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.
Files changed (67) hide show
  1. deepcell_cli/__init__.py +12 -0
  2. deepcell_cli/__main__.py +5 -0
  3. deepcell_cli/_findings.py +84 -0
  4. deepcell_cli/capabilities.py +560 -0
  5. deepcell_cli/capability-contract.json +15622 -0
  6. deepcell_cli/client.py +503 -0
  7. deepcell_cli/commands/__init__.py +1 -0
  8. deepcell_cli/commands/_batch_input.py +29 -0
  9. deepcell_cli/commands/_datatypes.py +56 -0
  10. deepcell_cli/commands/_negative_args.py +133 -0
  11. deepcell_cli/commands/_swapped_args.py +153 -0
  12. deepcell_cli/commands/_version_display.py +40 -0
  13. deepcell_cli/commands/_write_opts.py +139 -0
  14. deepcell_cli/commands/account.py +123 -0
  15. deepcell_cli/commands/auth.py +610 -0
  16. deepcell_cli/commands/changes.py +307 -0
  17. deepcell_cli/commands/deck.py +594 -0
  18. deepcell_cli/commands/defs.py +3890 -0
  19. deepcell_cli/commands/describe.py +902 -0
  20. deepcell_cli/commands/doc.py +529 -0
  21. deepcell_cli/commands/doctor.py +257 -0
  22. deepcell_cli/commands/download.py +36 -0
  23. deepcell_cli/commands/edit.py +384 -0
  24. deepcell_cli/commands/example.py +161 -0
  25. deepcell_cli/commands/export.py +81 -0
  26. deepcell_cli/commands/export_docx.py +57 -0
  27. deepcell_cli/commands/export_pdf.py +66 -0
  28. deepcell_cli/commands/export_pptx.py +45 -0
  29. deepcell_cli/commands/files.py +386 -0
  30. deepcell_cli/commands/grep.py +90 -0
  31. deepcell_cli/commands/guide.py +431 -0
  32. deepcell_cli/commands/help_cmd.py +348 -0
  33. deepcell_cli/commands/impact.py +382 -0
  34. deepcell_cli/commands/import_cmd.py +208 -0
  35. deepcell_cli/commands/ingest.py +110 -0
  36. deepcell_cli/commands/merge.py +399 -0
  37. deepcell_cli/commands/query.py +718 -0
  38. deepcell_cli/commands/reasoning.py +2981 -0
  39. deepcell_cli/commands/ref.py +279 -0
  40. deepcell_cli/commands/replace.py +326 -0
  41. deepcell_cli/commands/rules.py +206 -0
  42. deepcell_cli/commands/share.py +186 -0
  43. deepcell_cli/commands/sync.py +804 -0
  44. deepcell_cli/commands/upgrade.py +185 -0
  45. deepcell_cli/commands/variant.py +353 -0
  46. deepcell_cli/commands/version.py +445 -0
  47. deepcell_cli/commands/viewer.py +54 -0
  48. deepcell_cli/commands/workspace.py +101 -0
  49. deepcell_cli/config.py +352 -0
  50. deepcell_cli/context.py +187 -0
  51. deepcell_cli/errors.py +141 -0
  52. deepcell_cli/logging_setup.py +161 -0
  53. deepcell_cli/main.py +518 -0
  54. deepcell_cli/mcp_server.py +906 -0
  55. deepcell_cli/oauth_provider.py +580 -0
  56. deepcell_cli/output.py +503 -0
  57. deepcell_cli/revision.py +164 -0
  58. deepcell_cli/stages.py +223 -0
  59. deepcell_cli/surface.py +628 -0
  60. deepcell_cli/sync_state.py +120 -0
  61. deepcell_cli/upgrade_check.py +399 -0
  62. deepcell_cli/xml_replace.py +89 -0
  63. deepcell_cli-0.6.1.dist-info/METADATA +264 -0
  64. deepcell_cli-0.6.1.dist-info/RECORD +67 -0
  65. deepcell_cli-0.6.1.dist-info/WHEEL +5 -0
  66. deepcell_cli-0.6.1.dist-info/entry_points.txt +3 -0
  67. 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")