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,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.")