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,594 @@
|
|
|
1
|
+
"""``deepcell deck`` — the Deck surface's front door.
|
|
2
|
+
|
|
3
|
+
A .deepcell file connects four surfaces: Reasoning (ideas), Spreadsheet
|
|
4
|
+
(calculations), Document (prose) and Deck (slides). Three of them had an
|
|
5
|
+
authoring command group. Deck had none — twelve deck and slide ops sat in
|
|
6
|
+
``test_op_reachability.py``'s ``_DELIBERATELY_UNREACHABLE`` under one shared
|
|
7
|
+
justification:
|
|
8
|
+
|
|
9
|
+
the payloads carry whole HTML/CSS documents — flags are the wrong shape
|
|
10
|
+
|
|
11
|
+
Checked against the committed ``openapi.json`` the reachability test itself
|
|
12
|
+
reads from, that is true of **three** of the twelve. ``add_presentation_slide``
|
|
13
|
+
carries ``html``, ``set_presentation_slide_html`` carries ``html``, and
|
|
14
|
+
``set_presentation_deck_style`` carries ``style``. Those three stay on ``defs
|
|
15
|
+
apply``, where a heredoc is the right shape for a document-sized argument.
|
|
16
|
+
|
|
17
|
+
The other nine take ids, names and indices — ``deckId``, ``slideId``,
|
|
18
|
+
``newName``, ``toIndex`` — which is exactly what flags are for. They are wired
|
|
19
|
+
here. The allowlist's own header says why this is the right direction:
|
|
20
|
+
*"Adding an entry is a deliberate act: prefer wiring the op up."*
|
|
21
|
+
|
|
22
|
+
So: **structure here, content through `defs apply`.** Creating a deck, naming
|
|
23
|
+
it, ordering its slides, removing one, and pointing a binding at a value are
|
|
24
|
+
all structural. Writing the slide's HTML is not.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
from typing import Any
|
|
30
|
+
|
|
31
|
+
import click
|
|
32
|
+
|
|
33
|
+
from deepcell_cli.commands.defs import _DefsGroup, _apply
|
|
34
|
+
from deepcell_cli.context import Ctx, pass_ctx
|
|
35
|
+
|
|
36
|
+
#: Mirrors ``UpsertPresentationBindingOp.bindingKind`` in
|
|
37
|
+
#: ``backend/jingwei_api/routers/defs/models/_presentation.py``, which declares
|
|
38
|
+
#: it as a ``Literal``. Declared here as a ``Choice`` so a typo is refused
|
|
39
|
+
#: locally rather than coming back as a 422 — the same standard
|
|
40
|
+
#: ``test_deck_op_shape.py`` applies to the required fields. That test pins
|
|
41
|
+
#: both tuples to the committed schema, so neither can drift silently.
|
|
42
|
+
BINDING_KINDS = (
|
|
43
|
+
"value", "series", "chart", "document_stat", "reasoning", "source",
|
|
44
|
+
"htmlblock",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
#: Mirrors the same op's ``metric``. These count elements of the document's
|
|
48
|
+
#: provenance and reasoning graphs; they are not derived from the values the
|
|
49
|
+
#: binding reads, which is what this flag's help used to say.
|
|
50
|
+
BINDING_METRICS = (
|
|
51
|
+
"dataSourceCount",
|
|
52
|
+
"claimCount",
|
|
53
|
+
"assumptionCount",
|
|
54
|
+
"evidenceCount",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
#: Mirrors ``AddPresentationDeckOp.aspectRatio``, which is an enum with a
|
|
58
|
+
#: server-side default of ``16:9``. Free text here meant `--aspect-ratio 16:10`
|
|
59
|
+
#: reached the server as a 422 the CLI already had everything to refuse.
|
|
60
|
+
ASPECT_RATIOS = ("16:9", "4:3")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _slug(ctx: Ctx, workspace_slug: str | None) -> str:
|
|
64
|
+
return workspace_slug or ctx.require_workspace()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@click.group(cls=_DefsGroup)
|
|
68
|
+
def deck() -> None:
|
|
69
|
+
"""Build and reorder decks and slides — the Deck surface.
|
|
70
|
+
|
|
71
|
+
\b
|
|
72
|
+
Structure lives here: deepcell deck add-slide report.deepcell --deck d1 --name "Outlook"
|
|
73
|
+
Content goes through: deepcell defs apply report.deepcell --ops '[{"kind": "set_presentation_slide_html", ...}]'
|
|
74
|
+
|
|
75
|
+
A slide's HTML and a deck's CSS are document-sized arguments, so they stay
|
|
76
|
+
on `defs apply` rather than becoming shell flags. Everything structural —
|
|
77
|
+
creating, naming, ordering, deleting, binding — is here.
|
|
78
|
+
|
|
79
|
+
Every command here posts to the same endpoint `defs` does, so every one
|
|
80
|
+
takes `--dry-run` to pre-flight it and `-m` to name the commit it writes.
|
|
81
|
+
|
|
82
|
+
Read `deepcell guide present/decks` for how a deck stays linked to the
|
|
83
|
+
values it presents.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# ── Decks ───────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@deck.command("add")
|
|
91
|
+
@click.argument("filename")
|
|
92
|
+
@click.option("--deck-id", default=None, help="Deck id. Omit to let the server assign one.")
|
|
93
|
+
@click.option("--name", required=True, help="Display name for the deck.")
|
|
94
|
+
@click.option(
|
|
95
|
+
"--aspect-ratio",
|
|
96
|
+
default=None,
|
|
97
|
+
type=click.Choice(ASPECT_RATIOS),
|
|
98
|
+
help="Slide aspect ratio. Omit for the format default (16:9).",
|
|
99
|
+
)
|
|
100
|
+
@click.option("--index", type=int, default=None, help="Position among decks (0-based).")
|
|
101
|
+
@click.option("--revision", default=None, help="Expected revision SHA for optimistic locking.")
|
|
102
|
+
@click.option("--workspace", "workspace_slug", help="Override active workspace.")
|
|
103
|
+
@pass_ctx
|
|
104
|
+
def add_deck(
|
|
105
|
+
ctx: Ctx,
|
|
106
|
+
filename: str,
|
|
107
|
+
deck_id: str | None,
|
|
108
|
+
name: str,
|
|
109
|
+
aspect_ratio: str | None,
|
|
110
|
+
index: int | None,
|
|
111
|
+
revision: str | None,
|
|
112
|
+
workspace_slug: str | None,
|
|
113
|
+
) -> None:
|
|
114
|
+
"""Add a Deck to a .deepcell file.
|
|
115
|
+
|
|
116
|
+
The deck starts empty. Add slides with `deepcell deck add-slide`, then set
|
|
117
|
+
each slide's HTML with `defs apply`.
|
|
118
|
+
"""
|
|
119
|
+
op: dict[str, Any] = {"name": name}
|
|
120
|
+
if deck_id is not None:
|
|
121
|
+
op["deckId"] = deck_id
|
|
122
|
+
if aspect_ratio is not None:
|
|
123
|
+
op["aspectRatio"] = aspect_ratio
|
|
124
|
+
if index is not None:
|
|
125
|
+
op["index"] = index
|
|
126
|
+
_apply(
|
|
127
|
+
ctx,
|
|
128
|
+
_slug(ctx, workspace_slug),
|
|
129
|
+
filename,
|
|
130
|
+
[{"kind": "add_presentation_deck", **op}],
|
|
131
|
+
revision=revision,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@deck.command("rename")
|
|
136
|
+
@click.argument("filename")
|
|
137
|
+
@click.argument("deck_id")
|
|
138
|
+
@click.argument("new_name")
|
|
139
|
+
@click.option("--revision", default=None, help="Expected revision SHA for optimistic locking.")
|
|
140
|
+
@click.option("--workspace", "workspace_slug", help="Override active workspace.")
|
|
141
|
+
@pass_ctx
|
|
142
|
+
def rename_deck(
|
|
143
|
+
ctx: Ctx,
|
|
144
|
+
filename: str,
|
|
145
|
+
deck_id: str,
|
|
146
|
+
new_name: str,
|
|
147
|
+
revision: str | None,
|
|
148
|
+
workspace_slug: str | None,
|
|
149
|
+
) -> None:
|
|
150
|
+
"""Change a deck's display NAME. The deckId is unchanged.
|
|
151
|
+
|
|
152
|
+
Renaming the id instead would redirect every reference that points at it.
|
|
153
|
+
"""
|
|
154
|
+
_apply(
|
|
155
|
+
ctx,
|
|
156
|
+
_slug(ctx, workspace_slug),
|
|
157
|
+
filename,
|
|
158
|
+
[
|
|
159
|
+
{
|
|
160
|
+
"kind": "rename_presentation_deck",
|
|
161
|
+
"deckId": deck_id,
|
|
162
|
+
"newName": new_name,
|
|
163
|
+
}
|
|
164
|
+
],
|
|
165
|
+
revision=revision,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@deck.command("reorder")
|
|
170
|
+
@click.argument("filename")
|
|
171
|
+
@click.argument("deck_id")
|
|
172
|
+
@click.argument("to_index", type=int)
|
|
173
|
+
@click.option("--revision", default=None, help="Expected revision SHA for optimistic locking.")
|
|
174
|
+
@click.option("--workspace", "workspace_slug", help="Override active workspace.")
|
|
175
|
+
@pass_ctx
|
|
176
|
+
def reorder_decks(
|
|
177
|
+
ctx: Ctx,
|
|
178
|
+
filename: str,
|
|
179
|
+
deck_id: str,
|
|
180
|
+
to_index: int,
|
|
181
|
+
revision: str | None,
|
|
182
|
+
workspace_slug: str | None,
|
|
183
|
+
) -> None:
|
|
184
|
+
"""Move a deck to TO_INDEX in the deck order (0-based)."""
|
|
185
|
+
_apply(
|
|
186
|
+
ctx,
|
|
187
|
+
_slug(ctx, workspace_slug),
|
|
188
|
+
filename,
|
|
189
|
+
[
|
|
190
|
+
{
|
|
191
|
+
"kind": "reorder_presentation_decks",
|
|
192
|
+
"deckId": deck_id,
|
|
193
|
+
"toIndex": to_index,
|
|
194
|
+
}
|
|
195
|
+
],
|
|
196
|
+
revision=revision,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@deck.command("delete")
|
|
201
|
+
@click.argument("filename")
|
|
202
|
+
@click.argument("deck_id")
|
|
203
|
+
@click.option("--revision", default=None, help="Expected revision SHA for optimistic locking.")
|
|
204
|
+
@click.option("--workspace", "workspace_slug", help="Override active workspace.")
|
|
205
|
+
@pass_ctx
|
|
206
|
+
def delete_deck(
|
|
207
|
+
ctx: Ctx,
|
|
208
|
+
filename: str,
|
|
209
|
+
deck_id: str,
|
|
210
|
+
revision: str | None,
|
|
211
|
+
workspace_slug: str | None,
|
|
212
|
+
) -> None:
|
|
213
|
+
"""Delete a deck and every slide on it.
|
|
214
|
+
|
|
215
|
+
This removes the slides' HTML with them. Pass --dry-run first if you are
|
|
216
|
+
not sure which deck the id names — `deepcell describe` lists them.
|
|
217
|
+
"""
|
|
218
|
+
_apply(
|
|
219
|
+
ctx,
|
|
220
|
+
_slug(ctx, workspace_slug),
|
|
221
|
+
filename,
|
|
222
|
+
[{"kind": "delete_presentation_deck", "deckId": deck_id}],
|
|
223
|
+
revision=revision,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
# ── Slides ──────────────────────────────────────────────────
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@deck.command("add-slide")
|
|
231
|
+
@click.argument("filename")
|
|
232
|
+
@click.option("--deck", "deck_id", required=True, help="Deck to add the slide to.")
|
|
233
|
+
@click.option("--slide-id", default=None, help="Slide id. Omit to let the server assign one.")
|
|
234
|
+
@click.option("--name", required=True, help="Display name for the slide.")
|
|
235
|
+
@click.option("--index", type=int, default=None, help="Position within the deck (0-based).")
|
|
236
|
+
@click.option("--revision", default=None, help="Expected revision SHA for optimistic locking.")
|
|
237
|
+
@click.option("--workspace", "workspace_slug", help="Override active workspace.")
|
|
238
|
+
@pass_ctx
|
|
239
|
+
def add_slide(
|
|
240
|
+
ctx: Ctx,
|
|
241
|
+
filename: str,
|
|
242
|
+
deck_id: str,
|
|
243
|
+
slide_id: str | None,
|
|
244
|
+
name: str,
|
|
245
|
+
index: int | None,
|
|
246
|
+
revision: str | None,
|
|
247
|
+
workspace_slug: str | None,
|
|
248
|
+
) -> None:
|
|
249
|
+
"""Add an empty slide to a deck.
|
|
250
|
+
|
|
251
|
+
The slide's HTML is a document-sized argument, so this command never takes
|
|
252
|
+
it. Set it afterwards, or create and fill the slide in one op — the same
|
|
253
|
+
`add_presentation_slide` op also accepts `html` on `defs apply`. Start
|
|
254
|
+
the body from a `deepcell ref deck-layout` entry: class-only HTML on the
|
|
255
|
+
vocabulary every style pack defines.
|
|
256
|
+
|
|
257
|
+
\b
|
|
258
|
+
deepcell defs apply FILE --ops '[{"kind": "set_presentation_slide_html",
|
|
259
|
+
"deckId": "d1", "slideId": "s1", "html": "<section>...</section>"}]'
|
|
260
|
+
deepcell defs apply FILE --ops '[{"kind": "add_presentation_slide",
|
|
261
|
+
"deckId": "d1", "slideId": "s2", "name": "Risks", "html": "<section>...</section>"}]'
|
|
262
|
+
"""
|
|
263
|
+
op: dict[str, Any] = {"deckId": deck_id, "name": name}
|
|
264
|
+
if slide_id is not None:
|
|
265
|
+
op["slideId"] = slide_id
|
|
266
|
+
if index is not None:
|
|
267
|
+
op["index"] = index
|
|
268
|
+
_apply(
|
|
269
|
+
ctx,
|
|
270
|
+
_slug(ctx, workspace_slug),
|
|
271
|
+
filename,
|
|
272
|
+
[{"kind": "add_presentation_slide", **op}],
|
|
273
|
+
revision=revision,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
@deck.command("rename-slide")
|
|
278
|
+
@click.argument("filename")
|
|
279
|
+
@click.argument("slide_id")
|
|
280
|
+
@click.argument("new_name")
|
|
281
|
+
@click.option("--deck", "deck_id", required=True, help="Deck the slide belongs to.")
|
|
282
|
+
@click.option("--revision", default=None, help="Expected revision SHA for optimistic locking.")
|
|
283
|
+
@click.option("--workspace", "workspace_slug", help="Override active workspace.")
|
|
284
|
+
@pass_ctx
|
|
285
|
+
def rename_slide(
|
|
286
|
+
ctx: Ctx,
|
|
287
|
+
filename: str,
|
|
288
|
+
slide_id: str,
|
|
289
|
+
new_name: str,
|
|
290
|
+
deck_id: str,
|
|
291
|
+
revision: str | None,
|
|
292
|
+
workspace_slug: str | None,
|
|
293
|
+
) -> None:
|
|
294
|
+
"""Change a slide's display NAME. The slideId is unchanged."""
|
|
295
|
+
_apply(
|
|
296
|
+
ctx,
|
|
297
|
+
_slug(ctx, workspace_slug),
|
|
298
|
+
filename,
|
|
299
|
+
[
|
|
300
|
+
{
|
|
301
|
+
"kind": "rename_presentation_slide",
|
|
302
|
+
"deckId": deck_id,
|
|
303
|
+
"slideId": slide_id,
|
|
304
|
+
"newName": new_name,
|
|
305
|
+
}
|
|
306
|
+
],
|
|
307
|
+
revision=revision,
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
@deck.command("reorder-slide")
|
|
312
|
+
@click.argument("filename")
|
|
313
|
+
@click.argument("slide_id")
|
|
314
|
+
@click.argument("to_index", type=int)
|
|
315
|
+
@click.option("--deck", "deck_id", required=True, help="Deck the slide belongs to.")
|
|
316
|
+
@click.option("--revision", default=None, help="Expected revision SHA for optimistic locking.")
|
|
317
|
+
@click.option("--workspace", "workspace_slug", help="Override active workspace.")
|
|
318
|
+
@pass_ctx
|
|
319
|
+
def reorder_slide(
|
|
320
|
+
ctx: Ctx,
|
|
321
|
+
filename: str,
|
|
322
|
+
slide_id: str,
|
|
323
|
+
to_index: int,
|
|
324
|
+
deck_id: str,
|
|
325
|
+
revision: str | None,
|
|
326
|
+
workspace_slug: str | None,
|
|
327
|
+
) -> None:
|
|
328
|
+
"""Move a slide to TO_INDEX within its deck (0-based)."""
|
|
329
|
+
_apply(
|
|
330
|
+
ctx,
|
|
331
|
+
_slug(ctx, workspace_slug),
|
|
332
|
+
filename,
|
|
333
|
+
[
|
|
334
|
+
{
|
|
335
|
+
"kind": "reorder_presentation_slides",
|
|
336
|
+
"deckId": deck_id,
|
|
337
|
+
"slideId": slide_id,
|
|
338
|
+
"toIndex": to_index,
|
|
339
|
+
}
|
|
340
|
+
],
|
|
341
|
+
revision=revision,
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
@deck.command("delete-slide")
|
|
346
|
+
@click.argument("filename")
|
|
347
|
+
@click.argument("slide_id")
|
|
348
|
+
@click.option("--deck", "deck_id", required=True, help="Deck the slide belongs to.")
|
|
349
|
+
@click.option("--revision", default=None, help="Expected revision SHA for optimistic locking.")
|
|
350
|
+
@click.option("--workspace", "workspace_slug", help="Override active workspace.")
|
|
351
|
+
@pass_ctx
|
|
352
|
+
def delete_slide(
|
|
353
|
+
ctx: Ctx,
|
|
354
|
+
filename: str,
|
|
355
|
+
slide_id: str,
|
|
356
|
+
deck_id: str,
|
|
357
|
+
revision: str | None,
|
|
358
|
+
workspace_slug: str | None,
|
|
359
|
+
) -> None:
|
|
360
|
+
"""Delete one slide, and the HTML on it."""
|
|
361
|
+
_apply(
|
|
362
|
+
ctx,
|
|
363
|
+
_slug(ctx, workspace_slug),
|
|
364
|
+
filename,
|
|
365
|
+
[
|
|
366
|
+
{
|
|
367
|
+
"kind": "delete_presentation_slide",
|
|
368
|
+
"deckId": deck_id,
|
|
369
|
+
"slideId": slide_id,
|
|
370
|
+
}
|
|
371
|
+
],
|
|
372
|
+
revision=revision,
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
# ── Bindings ────────────────────────────────────────────────
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
@deck.command("bind")
|
|
380
|
+
@click.argument("filename")
|
|
381
|
+
@click.option("--deck", "deck_id", required=True, help="Deck the binding lives on.")
|
|
382
|
+
@click.option("--binding-id", required=True, help="Binding id. Reusing one updates it in place.")
|
|
383
|
+
@click.option(
|
|
384
|
+
"--binding-kind",
|
|
385
|
+
required=True,
|
|
386
|
+
type=click.Choice(BINDING_KINDS),
|
|
387
|
+
help="What the binding renders. Decides which address flag it takes.",
|
|
388
|
+
)
|
|
389
|
+
@click.option(
|
|
390
|
+
"--field",
|
|
391
|
+
default=None,
|
|
392
|
+
help="Which text of the addressed thing to render, not an address itself.",
|
|
393
|
+
)
|
|
394
|
+
@click.option("--ref", default=None, help="Single coordinate the binding reads.")
|
|
395
|
+
@click.option(
|
|
396
|
+
"--refs",
|
|
397
|
+
default=None,
|
|
398
|
+
help=(
|
|
399
|
+
"Comma-separated coordinates — the address a `series` binding takes. "
|
|
400
|
+
"Single-quote an item id that contains a comma."
|
|
401
|
+
),
|
|
402
|
+
)
|
|
403
|
+
@click.option(
|
|
404
|
+
"--metric",
|
|
405
|
+
default=None,
|
|
406
|
+
type=click.Choice(BINDING_METRICS),
|
|
407
|
+
help="For --binding-kind document_stat only: which element type to count.",
|
|
408
|
+
)
|
|
409
|
+
@click.option(
|
|
410
|
+
"--number-format",
|
|
411
|
+
default=None,
|
|
412
|
+
help=(
|
|
413
|
+
"Excel-style number pattern the value renders with — `#,##0.0`, `0.0%`, "
|
|
414
|
+
"`#,##0;(#,##0)`. It labels the stored value and never divides it."
|
|
415
|
+
),
|
|
416
|
+
)
|
|
417
|
+
@click.option("--prefix", default=None, help="Text rendered before the value.")
|
|
418
|
+
@click.option("--suffix", default=None, help="Text rendered after the value.")
|
|
419
|
+
@click.option("--revision", default=None, help="Expected revision SHA for optimistic locking.")
|
|
420
|
+
@click.option("--workspace", "workspace_slug", help="Override active workspace.")
|
|
421
|
+
@pass_ctx
|
|
422
|
+
def bind(
|
|
423
|
+
ctx: Ctx,
|
|
424
|
+
filename: str,
|
|
425
|
+
deck_id: str,
|
|
426
|
+
binding_id: str,
|
|
427
|
+
binding_kind: str,
|
|
428
|
+
field: str | None,
|
|
429
|
+
ref: str | None,
|
|
430
|
+
refs: str | None,
|
|
431
|
+
metric: str | None,
|
|
432
|
+
number_format: str | None,
|
|
433
|
+
prefix: str | None,
|
|
434
|
+
suffix: str | None,
|
|
435
|
+
revision: str | None,
|
|
436
|
+
workspace_slug: str | None,
|
|
437
|
+
) -> None:
|
|
438
|
+
"""Point a slide at a live value, or update an existing binding.
|
|
439
|
+
|
|
440
|
+
A binding is what keeps a slide honest: the number on the slide is read
|
|
441
|
+
from the model rather than typed, so changing the model changes the deck.
|
|
442
|
+
Create and update are one command — passing an existing --binding-id
|
|
443
|
+
updates it in place.
|
|
444
|
+
|
|
445
|
+
\b
|
|
446
|
+
Which address flag the kind takes — the server rejects the other one:
|
|
447
|
+
series --refs (comma-separated, ordered)
|
|
448
|
+
document_stat no address; --metric instead
|
|
449
|
+
value / chart / reasoning / source
|
|
450
|
+
--ref
|
|
451
|
+
htmlblock --ref deepcell:doc/<docId>#<blockId>
|
|
452
|
+
|
|
453
|
+
An `htmlblock` shows a designed exhibit authored in a document's prose —
|
|
454
|
+
a margin bridge, a comparison card. It addresses a `doc` because that is
|
|
455
|
+
where a block lives; the block stays one thing in one place, so editing
|
|
456
|
+
the memo changes the slide.
|
|
457
|
+
|
|
458
|
+
`--field` selects which text an already-addressed thing renders
|
|
459
|
+
(`label` / `body` / `excerpt` on reasoning, `title` / `locator` on a
|
|
460
|
+
source). It is not an address.
|
|
461
|
+
"""
|
|
462
|
+
op: dict[str, Any] = {
|
|
463
|
+
"deckId": deck_id,
|
|
464
|
+
"bindingId": binding_id,
|
|
465
|
+
"bindingKind": binding_kind,
|
|
466
|
+
}
|
|
467
|
+
for key, value in (
|
|
468
|
+
("field", field),
|
|
469
|
+
("ref", ref),
|
|
470
|
+
("metric", metric),
|
|
471
|
+
("numberFormat", number_format),
|
|
472
|
+
("prefix", prefix),
|
|
473
|
+
("suffix", suffix),
|
|
474
|
+
):
|
|
475
|
+
if value is not None:
|
|
476
|
+
op[key] = value
|
|
477
|
+
if refs is not None:
|
|
478
|
+
# Verbatim, and deliberately so. Two things are true here and the
|
|
479
|
+
# first draft got both wrong.
|
|
480
|
+
#
|
|
481
|
+
# It is a STRING on the wire: `UpsertPresentationBindingOp.refs` is
|
|
482
|
+
# `Optional[str]` and its comment calls it "an ordered comma-separated
|
|
483
|
+
# list". Sending a split list 422'd on every `--refs` call, which a
|
|
484
|
+
# mocked test cannot see — hence the schema validation in
|
|
485
|
+
# `test_deck_op_shape.py`.
|
|
486
|
+
#
|
|
487
|
+
# And the splitting is not ours to do. A comma is legal *inside* an
|
|
488
|
+
# item id when the id is single-quoted
|
|
489
|
+
# (`deepcell:cell/'Cost,Net'[FY26]`), so the separator can only be
|
|
490
|
+
# found by a quote-aware scan — `core.refs.binding_ref.split_ref_list`,
|
|
491
|
+
# which the file validator and the render builder also call. Splitting
|
|
492
|
+
# and rejoining here strips the space out of `'Cost, Net'` and changes
|
|
493
|
+
# which item the reference names.
|
|
494
|
+
op["refs"] = refs
|
|
495
|
+
_apply(
|
|
496
|
+
ctx,
|
|
497
|
+
_slug(ctx, workspace_slug),
|
|
498
|
+
filename,
|
|
499
|
+
[{"kind": "upsert_presentation_binding", **op}],
|
|
500
|
+
revision=revision,
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
@deck.command("unbind")
|
|
505
|
+
@click.argument("filename")
|
|
506
|
+
@click.argument("binding_id")
|
|
507
|
+
@click.option("--deck", "deck_id", required=True, help="Deck the binding lives on.")
|
|
508
|
+
@click.option("--revision", default=None, help="Expected revision SHA for optimistic locking.")
|
|
509
|
+
@click.option("--workspace", "workspace_slug", help="Override active workspace.")
|
|
510
|
+
@pass_ctx
|
|
511
|
+
def unbind(
|
|
512
|
+
ctx: Ctx,
|
|
513
|
+
filename: str,
|
|
514
|
+
binding_id: str,
|
|
515
|
+
deck_id: str,
|
|
516
|
+
revision: str | None,
|
|
517
|
+
workspace_slug: str | None,
|
|
518
|
+
) -> None:
|
|
519
|
+
"""Remove a binding, leaving whatever the slide's HTML says in its place."""
|
|
520
|
+
_apply(
|
|
521
|
+
ctx,
|
|
522
|
+
_slug(ctx, workspace_slug),
|
|
523
|
+
filename,
|
|
524
|
+
[
|
|
525
|
+
{
|
|
526
|
+
"kind": "delete_presentation_binding",
|
|
527
|
+
"deckId": deck_id,
|
|
528
|
+
"bindingId": binding_id,
|
|
529
|
+
}
|
|
530
|
+
],
|
|
531
|
+
revision=revision,
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def _parse_role_assignments(pairs: tuple[str, ...]) -> dict[str, str]:
|
|
536
|
+
roles: dict[str, str] = {}
|
|
537
|
+
for pair in pairs:
|
|
538
|
+
role, sep, colour = pair.partition("=")
|
|
539
|
+
if not sep or not role.strip() or not colour.strip():
|
|
540
|
+
raise click.BadParameter(f"expected ROLE=#RRGGBB, got {pair!r}", param_hint="--set")
|
|
541
|
+
roles[role.strip()] = colour.strip()
|
|
542
|
+
return roles
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
@deck.command("brand")
|
|
546
|
+
@click.argument("filename")
|
|
547
|
+
@click.option("--deck", "deck_id", required=True, help="Deck to recolour.")
|
|
548
|
+
@click.option(
|
|
549
|
+
"--set",
|
|
550
|
+
"assignments",
|
|
551
|
+
multiple=True,
|
|
552
|
+
metavar="ROLE=#RRGGBB",
|
|
553
|
+
help="Colour-role override, repeatable: --set accent=#0A5C3B --set ink=#111111. "
|
|
554
|
+
"Roles are the pack's colour names — accent, ink, series-1 … (`deepcell ref deck-style/hooks`).",
|
|
555
|
+
)
|
|
556
|
+
@click.option("--clear", is_flag=True, help="Remove the brand; the pack's own palette shows again.")
|
|
557
|
+
@click.option("--revision", default=None, help="Expected revision SHA for optimistic locking.")
|
|
558
|
+
@click.option("--workspace", "workspace_slug", help="Override active workspace.")
|
|
559
|
+
@pass_ctx
|
|
560
|
+
def brand(
|
|
561
|
+
ctx: Ctx,
|
|
562
|
+
filename: str,
|
|
563
|
+
deck_id: str,
|
|
564
|
+
assignments: tuple[str, ...],
|
|
565
|
+
clear: bool,
|
|
566
|
+
revision: str | None,
|
|
567
|
+
workspace_slug: str | None,
|
|
568
|
+
) -> None:
|
|
569
|
+
"""Recolour a deck by ROLE without touching its stylesheet.
|
|
570
|
+
|
|
571
|
+
A style pack paints through colour roles (`accent`, `ink`, `series-1` …,
|
|
572
|
+
the `dc-` custom properties it declares). A brand is a set of overrides for those roles, written
|
|
573
|
+
as `<Brand accent="#0A5C3B"/>` and emitted after the pack's CSS by every
|
|
574
|
+
renderer, so `deck brand` and a later pack swap compose: the brand
|
|
575
|
+
survives the restyle. The whole brand is replaced on each call — resend
|
|
576
|
+
every role you want kept, or `--clear` to drop it.
|
|
577
|
+
"""
|
|
578
|
+
if clear and assignments:
|
|
579
|
+
raise click.UsageError("--clear takes no --set; pass one or the other.")
|
|
580
|
+
if not clear and not assignments:
|
|
581
|
+
raise click.UsageError("Pass at least one --set ROLE=#RRGGBB, or --clear.")
|
|
582
|
+
_apply(
|
|
583
|
+
ctx,
|
|
584
|
+
_slug(ctx, workspace_slug),
|
|
585
|
+
filename,
|
|
586
|
+
[
|
|
587
|
+
{
|
|
588
|
+
"kind": "set_presentation_deck_brand",
|
|
589
|
+
"deckId": deck_id,
|
|
590
|
+
"roles": {} if clear else _parse_role_assignments(assignments),
|
|
591
|
+
}
|
|
592
|
+
],
|
|
593
|
+
revision=revision,
|
|
594
|
+
)
|