dataspring-cli 0.3.0__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.
cli/generated.py ADDED
@@ -0,0 +1,1297 @@
1
+ """GENERATED by `python -m services.cli_generate`; do not edit.
2
+
3
+ The dispatch registry as Typer commands (decision D19, step 4): one
4
+ command per flat tool (``dataspring <key> --<field> ...``) and one group
5
+ per edit family (``dataspring <key> <verb> --<field> ...``), each flag a
6
+ field of the entry's model in ``cli/contract.py``, help text the models'
7
+ own. ``services/cli_generate.py`` is the generator and the only place that
8
+ knows how a model becomes a command; ``tests/test_cli_generated.py``
9
+ asserts this file equals its output. ``cli/runtime.py`` is what every
10
+ command calls: the body from the flags (and ``--from-file``), validated
11
+ against the model, the manifest skew hint, ``POST /api/dispatch/<key>``.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from pathlib import Path
17
+ from typing import Annotated, Optional
18
+
19
+ import typer
20
+
21
+ from cli import contract
22
+ from cli.runtime import J, run_tool
23
+
24
+ FromFile = Annotated[
25
+ Optional[Path],
26
+ typer.Option("--from-file", help="A YAML or JSON file holding the fields; flags given on the command line override its keys"),
27
+ ]
28
+ Output = Annotated[
29
+ Optional[Path],
30
+ typer.Option("--output", "-o", help="Write the answer's document (`content`) or rendered file (`data_base64`) here instead of printing the JSON answer"),
31
+ ]
32
+
33
+ #: The contract model of every registry key, for the alias layer and the tests.
34
+ MODELS: dict[str, type[contract.DispatchParams]] = {
35
+ 'dashboard_edit': contract.DashboardEditParams,
36
+ 'widget_edit': contract.WidgetEditParams,
37
+ 'page_edit': contract.PageEditParams,
38
+ 'section_edit': contract.SectionEditParams,
39
+ 'business_context_edit': contract.BusinessContextEditParams,
40
+ 'semantic_model_edit': contract.SemanticModelEditParams,
41
+ 'metric_edit': contract.MetricEditParams,
42
+ 'quick_metric_edit': contract.QuickMetricEditParams,
43
+ 'report_edit': contract.ReportEditParams,
44
+ 'learned_edit': contract.LearnedEditParams,
45
+ 'verified_query_edit': contract.VerifiedQueryEditParams,
46
+ 'warehouse_edit': contract.WarehouseEditParams,
47
+ 'query_metrics': contract.QueryMetricsParams,
48
+ 'explain_query': contract.ExplainQueryParams,
49
+ 'update_context': contract.UpdateContextParams,
50
+ 'export_data': contract.ExportDataParams,
51
+ 'import_manifest': contract.ImportManifestParams,
52
+ 'render_dashboard': contract.RenderDashboardParams,
53
+ 'render_widget': contract.RenderWidgetParams,
54
+ 'switch_organization': contract.SwitchOrganizationParams,
55
+ 'submit_error_report': contract.SubmitErrorReportParams,
56
+ 'secret_edit': contract.SecretEditParams,
57
+ 'run_sql': contract.RunSqlParams,
58
+ 'datacore_edit': contract.DatacoreEditParams,
59
+ 'datacore_run': contract.DatacoreRunParams,
60
+ }
61
+
62
+ #: The verbs of every edit family, in the order the action union declares them.
63
+ VERBS: dict[str, tuple[str, ...]] = {
64
+ 'dashboard_edit': ('create', 'update', 'delete', 'duplicate', 'share', 'set_controls'),
65
+ 'widget_edit': ('add', 'update', 'remove', 'reorder', 'move', 'copy', 'swap'),
66
+ 'page_edit': ('create', 'delete', 'rename', 'reorder'),
67
+ 'section_edit': ('create', 'delete', 'rename', 'move'),
68
+ 'business_context_edit': ('get', 'show_size', 'set', 'append'),
69
+ 'semantic_model_edit': ('create', 'update', 'delete', 'preview'),
70
+ 'metric_edit': ('create', 'update', 'delete', 'preview', 'impact'),
71
+ 'quick_metric_edit': ('create', 'update', 'delete'),
72
+ 'report_edit': ('create', 'update', 'delete'),
73
+ 'learned_edit': ('list', 'undo'),
74
+ 'verified_query_edit': ('list', 'record', 'delete'),
75
+ 'warehouse_edit': ('activate', 'add_external', 'remove'),
76
+ 'secret_edit': ('set', 'reveal'),
77
+ 'datacore_edit': ('apply', 'patch', 'delete'),
78
+ 'datacore_run': ('check', 'deploy', 'run', 'reset'),
79
+ }
80
+
81
+
82
+ # ---------------------------------------------------------------------------
83
+ # dashboard_edit
84
+ # ---------------------------------------------------------------------------
85
+
86
+
87
+ dashboard_edit = typer.Typer(name='dashboard_edit', help='Edit dashboards via a single dispatch tool with a discriminated action.', no_args_is_help=True)
88
+
89
+
90
+ @dashboard_edit.command('create', short_help='Create a new dashboard.')
91
+ def dashboard_edit__create(
92
+ title: Annotated[str, typer.Option('--title', help='Dashboard title')],
93
+ description: Annotated[Optional[str], typer.Option('--description', help='Optional description')] = None,
94
+ visibility: Annotated[Optional[str], typer.Option('--visibility', help='"private" (only you) or "org" (shared with organization) (one of: private, org) (default: private)')] = None,
95
+ from_file: FromFile = None,
96
+ output: Output = None,
97
+ ) -> None:
98
+ """Create a new dashboard."""
99
+ run_tool('dashboard_edit', contract.DashboardEditParams, 'create', {'title': title, 'description': description, 'visibility': visibility}, from_file=from_file, output=output)
100
+
101
+
102
+ @dashboard_edit.command('update', short_help="Update an existing dashboard's metadata (title/description/visibility).")
103
+ def dashboard_edit__update(
104
+ id: Annotated[str, typer.Option('--id', help='Dashboard ID')],
105
+ title: Annotated[Optional[str], typer.Option('--title', help='title')] = None,
106
+ description: Annotated[Optional[str], typer.Option('--description', help='description')] = None,
107
+ visibility: Annotated[Optional[str], typer.Option('--visibility', help='visibility (one of: private, org)')] = None,
108
+ expected_version: Annotated[Optional[int], typer.Option('--expected-version', help='Optimistic lock — if provided, update fails on mismatch. Use the version from get_dashboard to prevent overwriting concurrent changes.')] = None,
109
+ from_file: FromFile = None,
110
+ output: Output = None,
111
+ ) -> None:
112
+ """Update an existing dashboard's metadata (title/description/visibility)."""
113
+ run_tool('dashboard_edit', contract.DashboardEditParams, 'update', {'id': id, 'title': title, 'description': description, 'visibility': visibility, 'expected_version': expected_version}, from_file=from_file, output=output)
114
+
115
+
116
+ @dashboard_edit.command('delete', short_help='Delete a dashboard. Cannot be undone.')
117
+ def dashboard_edit__delete(
118
+ id: Annotated[str, typer.Option('--id', help='Dashboard ID')],
119
+ from_file: FromFile = None,
120
+ output: Output = None,
121
+ ) -> None:
122
+ """Delete a dashboard. Cannot be undone."""
123
+ run_tool('dashboard_edit', contract.DashboardEditParams, 'delete', {'id': id}, from_file=from_file, output=output)
124
+
125
+
126
+ @dashboard_edit.command('duplicate', short_help='Duplicate a dashboard with all pages, sections, and widgets (new IDs).')
127
+ def dashboard_edit__duplicate(
128
+ id: Annotated[str, typer.Option('--id', help='Source dashboard ID')],
129
+ new_title: Annotated[Optional[str], typer.Option('--new-title', help='Title for the copy (default: "Copy of {original}")')] = None,
130
+ from_file: FromFile = None,
131
+ output: Output = None,
132
+ ) -> None:
133
+ """Duplicate a dashboard with all pages, sections, and widgets (new IDs)."""
134
+ run_tool('dashboard_edit', contract.DashboardEditParams, 'duplicate', {'id': id, 'new_title': new_title}, from_file=from_file, output=output)
135
+
136
+
137
+ @dashboard_edit.command('share', short_help='Toggle dashboard visibility between private and shared (org).')
138
+ def dashboard_edit__share(
139
+ id: Annotated[str, typer.Option('--id', help='Dashboard ID')],
140
+ visibility: Annotated[str, typer.Option('--visibility', help='visibility (one of: private, org)')],
141
+ from_file: FromFile = None,
142
+ output: Output = None,
143
+ ) -> None:
144
+ """Toggle dashboard visibility between private and shared (org)."""
145
+ run_tool('dashboard_edit', contract.DashboardEditParams, 'share', {'id': id, 'visibility': visibility}, from_file=from_file, output=output)
146
+
147
+
148
+ @dashboard_edit.command('set_controls', short_help="Update a dashboard's date-range, grain, and comparison controls.")
149
+ def dashboard_edit__set_controls(
150
+ id: Annotated[str, typer.Option('--id', help='Dashboard ID')],
151
+ date_range: Annotated[Optional[str], typer.Option('--date-range-json', help='Date range config. Relative: {"mode": "relative", "preset": "last_30_days"}. Absolute: {"mode": "absolute", "start_date": "YYYY-MM-DD", "end_date": "YYYY-MM-DD"}. (JSON, or @path to a .json/.yaml file)')] = None,
152
+ grain: Annotated[Optional[str], typer.Option('--grain', help='grain (one of: day, week, month, quarter, year)')] = None,
153
+ comparison: Annotated[Optional[str], typer.Option('--comparison', help='comparison (one of: none, previous_period, same_period_last_year)')] = None,
154
+ from_file: FromFile = None,
155
+ output: Output = None,
156
+ ) -> None:
157
+ """Update a dashboard's date-range, grain, and comparison controls."""
158
+ run_tool('dashboard_edit', contract.DashboardEditParams, 'set_controls', {'id': id, 'date_range': J(date_range), 'grain': grain, 'comparison': comparison}, from_file=from_file, output=output)
159
+
160
+
161
+ # ---------------------------------------------------------------------------
162
+ # widget_edit
163
+ # ---------------------------------------------------------------------------
164
+
165
+
166
+ widget_edit = typer.Typer(name='widget_edit', help='Edit dashboard widgets via a single dispatch tool.', no_args_is_help=True)
167
+
168
+
169
+ @widget_edit.command('add', short_help='Add a widget to a dashboard section.')
170
+ def widget_edit__add(
171
+ dashboard_id: Annotated[str, typer.Option('--dashboard-id', help='dashboard id')],
172
+ widget: Annotated[Optional[str], typer.Option('--widget-json', help='Widget definition. Required: type (kpi|area_chart|bar_chart|line_chart|table|donut|heatmap), title, query. Optional: format, width (1-10), time_scope (range|latest|latest_complete), pivot (for table widgets). (JSON, or @path to a .json/.yaml file)')],
173
+ page_index: Annotated[Optional[int], typer.Option('--page-index', help='page index (default: 0)')] = None,
174
+ section_index: Annotated[Optional[int], typer.Option('--section-index', help='section index (default: 0)')] = None,
175
+ expected_version: Annotated[Optional[int], typer.Option('--expected-version', help='expected version')] = None,
176
+ from_file: FromFile = None,
177
+ output: Output = None,
178
+ ) -> None:
179
+ """Add a widget to a dashboard section."""
180
+ run_tool('widget_edit', contract.WidgetEditParams, 'add', {'dashboard_id': dashboard_id, 'widget': J(widget), 'page_index': page_index, 'section_index': section_index, 'expected_version': expected_version}, from_file=from_file, output=output)
181
+
182
+
183
+ @widget_edit.command('update', short_help="Update a widget's fields (title, type, query, format, width, time_scope, pivot).")
184
+ def widget_edit__update(
185
+ dashboard_id: Annotated[str, typer.Option('--dashboard-id', help='dashboard id')],
186
+ page_index: Annotated[int, typer.Option('--page-index', help='page index')],
187
+ section_index: Annotated[int, typer.Option('--section-index', help='section index')],
188
+ widget_index: Annotated[int, typer.Option('--widget-index', help='widget index')],
189
+ updates: Annotated[Optional[str], typer.Option('--updates-json', help='Fields to patch — only provided keys are changed (JSON, or @path to a .json/.yaml file)')],
190
+ expected_version: Annotated[Optional[int], typer.Option('--expected-version', help='expected version')] = None,
191
+ from_file: FromFile = None,
192
+ output: Output = None,
193
+ ) -> None:
194
+ """Update a widget's fields (title, type, query, format, width, time_scope, pivot)."""
195
+ run_tool('widget_edit', contract.WidgetEditParams, 'update', {'dashboard_id': dashboard_id, 'page_index': page_index, 'section_index': section_index, 'widget_index': widget_index, 'updates': J(updates), 'expected_version': expected_version}, from_file=from_file, output=output)
196
+
197
+
198
+ @widget_edit.command('remove', short_help='Remove a widget from a dashboard section (irreversible).')
199
+ def widget_edit__remove(
200
+ dashboard_id: Annotated[str, typer.Option('--dashboard-id', help='dashboard id')],
201
+ page_index: Annotated[int, typer.Option('--page-index', help='page index')],
202
+ section_index: Annotated[int, typer.Option('--section-index', help='section index')],
203
+ widget_index: Annotated[int, typer.Option('--widget-index', help='widget index')],
204
+ expected_version: Annotated[Optional[int], typer.Option('--expected-version', help='expected version')] = None,
205
+ from_file: FromFile = None,
206
+ output: Output = None,
207
+ ) -> None:
208
+ """Remove a widget from a dashboard section (irreversible)."""
209
+ run_tool('widget_edit', contract.WidgetEditParams, 'remove', {'dashboard_id': dashboard_id, 'page_index': page_index, 'section_index': section_index, 'widget_index': widget_index, 'expected_version': expected_version}, from_file=from_file, output=output)
210
+
211
+
212
+ @widget_edit.command('reorder', short_help='Reorder widgets within a section. All widget IDs in the section must be listed.')
213
+ def widget_edit__reorder(
214
+ dashboard_id: Annotated[str, typer.Option('--dashboard-id', help='dashboard id')],
215
+ page_index: Annotated[int, typer.Option('--page-index', help='page index')],
216
+ section_index: Annotated[int, typer.Option('--section-index', help='section index')],
217
+ widget_ids: Annotated[list[str], typer.Option('--widget-ids', help='Full list of widget IDs in desired order')],
218
+ expected_version: Annotated[Optional[int], typer.Option('--expected-version', help='expected version')] = None,
219
+ from_file: FromFile = None,
220
+ output: Output = None,
221
+ ) -> None:
222
+ """Reorder widgets within a section. All widget IDs in the section must be listed."""
223
+ run_tool('widget_edit', contract.WidgetEditParams, 'reorder', {'dashboard_id': dashboard_id, 'page_index': page_index, 'section_index': section_index, 'widget_ids': widget_ids, 'expected_version': expected_version}, from_file=from_file, output=output)
224
+
225
+
226
+ @widget_edit.command('move', short_help='Move a widget to a different page/section (keeps the same ID).')
227
+ def widget_edit__move(
228
+ dashboard_id: Annotated[str, typer.Option('--dashboard-id', help='dashboard id')],
229
+ widget_id: Annotated[str, typer.Option('--widget-id', help='widget id')],
230
+ target_page_id: Annotated[str, typer.Option('--target-page-id', help='target page id')],
231
+ target_section_index: Annotated[int, typer.Option('--target-section-index', help='target section index')],
232
+ position: Annotated[Optional[int], typer.Option('--position', help='Position within target section (None = append)')] = None,
233
+ from_file: FromFile = None,
234
+ output: Output = None,
235
+ ) -> None:
236
+ """Move a widget to a different page/section (keeps the same ID)."""
237
+ run_tool('widget_edit', contract.WidgetEditParams, 'move', {'dashboard_id': dashboard_id, 'widget_id': widget_id, 'target_page_id': target_page_id, 'target_section_index': target_section_index, 'position': position}, from_file=from_file, output=output)
238
+
239
+
240
+ @widget_edit.command('copy', short_help='Copy a widget with a new ID.')
241
+ def widget_edit__copy(
242
+ dashboard_id: Annotated[str, typer.Option('--dashboard-id', help='dashboard id')],
243
+ widget_id: Annotated[str, typer.Option('--widget-id', help='widget id')],
244
+ target_page_id: Annotated[str, typer.Option('--target-page-id', help='target page id')],
245
+ target_section_index: Annotated[int, typer.Option('--target-section-index', help='target section index')],
246
+ new_title: Annotated[Optional[str], typer.Option('--new-title', help='new title')] = None,
247
+ from_file: FromFile = None,
248
+ output: Output = None,
249
+ ) -> None:
250
+ """Copy a widget with a new ID."""
251
+ run_tool('widget_edit', contract.WidgetEditParams, 'copy', {'dashboard_id': dashboard_id, 'widget_id': widget_id, 'target_page_id': target_page_id, 'target_section_index': target_section_index, 'new_title': new_title}, from_file=from_file, output=output)
252
+
253
+
254
+ @widget_edit.command('swap', short_help='Swap the positions of two widgets (may be in different sections).')
255
+ def widget_edit__swap(
256
+ dashboard_id: Annotated[str, typer.Option('--dashboard-id', help='dashboard id')],
257
+ widget_id_1: Annotated[str, typer.Option('--widget-id-1', help='widget id 1')],
258
+ widget_id_2: Annotated[str, typer.Option('--widget-id-2', help='widget id 2')],
259
+ from_file: FromFile = None,
260
+ output: Output = None,
261
+ ) -> None:
262
+ """Swap the positions of two widgets (may be in different sections)."""
263
+ run_tool('widget_edit', contract.WidgetEditParams, 'swap', {'dashboard_id': dashboard_id, 'widget_id_1': widget_id_1, 'widget_id_2': widget_id_2}, from_file=from_file, output=output)
264
+
265
+
266
+ # ---------------------------------------------------------------------------
267
+ # page_edit
268
+ # ---------------------------------------------------------------------------
269
+
270
+
271
+ page_edit = typer.Typer(name='page_edit', help='Edit dashboard pages via a single dispatch tool.', no_args_is_help=True)
272
+
273
+
274
+ @page_edit.command('create', short_help='`page_edit` create.')
275
+ def page_edit__create(
276
+ dashboard_id: Annotated[str, typer.Option('--dashboard-id', help='dashboard id')],
277
+ title: Annotated[str, typer.Option('--title', help='title')],
278
+ position: Annotated[Optional[int], typer.Option('--position', help='Insert position (None = append)')] = None,
279
+ from_file: FromFile = None,
280
+ output: Output = None,
281
+ ) -> None:
282
+ """`page_edit` create."""
283
+ run_tool('page_edit', contract.PageEditParams, 'create', {'dashboard_id': dashboard_id, 'title': title, 'position': position}, from_file=from_file, output=output)
284
+
285
+
286
+ @page_edit.command('delete', short_help='`page_edit` delete.')
287
+ def page_edit__delete(
288
+ dashboard_id: Annotated[str, typer.Option('--dashboard-id', help='dashboard id')],
289
+ page_id: Annotated[str, typer.Option('--page-id', help='page id')],
290
+ cascade: Annotated[Optional[bool], typer.Option('--cascade/--no-cascade', help='Delete even if page has widgets')] = None,
291
+ from_file: FromFile = None,
292
+ output: Output = None,
293
+ ) -> None:
294
+ """`page_edit` delete."""
295
+ run_tool('page_edit', contract.PageEditParams, 'delete', {'dashboard_id': dashboard_id, 'page_id': page_id, 'cascade': cascade}, from_file=from_file, output=output)
296
+
297
+
298
+ @page_edit.command('rename', short_help='`page_edit` rename.')
299
+ def page_edit__rename(
300
+ dashboard_id: Annotated[str, typer.Option('--dashboard-id', help='dashboard id')],
301
+ page_id: Annotated[str, typer.Option('--page-id', help='page id')],
302
+ title: Annotated[str, typer.Option('--title', help='title')],
303
+ from_file: FromFile = None,
304
+ output: Output = None,
305
+ ) -> None:
306
+ """`page_edit` rename."""
307
+ run_tool('page_edit', contract.PageEditParams, 'rename', {'dashboard_id': dashboard_id, 'page_id': page_id, 'title': title}, from_file=from_file, output=output)
308
+
309
+
310
+ @page_edit.command('reorder', short_help='`page_edit` reorder.')
311
+ def page_edit__reorder(
312
+ dashboard_id: Annotated[str, typer.Option('--dashboard-id', help='dashboard id')],
313
+ page_ids: Annotated[list[str], typer.Option('--page-ids', help='Full list of page IDs in desired order')],
314
+ from_file: FromFile = None,
315
+ output: Output = None,
316
+ ) -> None:
317
+ """`page_edit` reorder."""
318
+ run_tool('page_edit', contract.PageEditParams, 'reorder', {'dashboard_id': dashboard_id, 'page_ids': page_ids}, from_file=from_file, output=output)
319
+
320
+
321
+ # ---------------------------------------------------------------------------
322
+ # section_edit
323
+ # ---------------------------------------------------------------------------
324
+
325
+
326
+ section_edit = typer.Typer(name='section_edit', help='Edit dashboard sections via a single dispatch tool.', no_args_is_help=True)
327
+
328
+
329
+ @section_edit.command('create', short_help='`section_edit` create.')
330
+ def section_edit__create(
331
+ dashboard_id: Annotated[str, typer.Option('--dashboard-id', help='dashboard id')],
332
+ page_id: Annotated[str, typer.Option('--page-id', help='page id')],
333
+ title: Annotated[Optional[str], typer.Option('--title', help='title')] = None,
334
+ position: Annotated[Optional[int], typer.Option('--position', help='position')] = None,
335
+ from_file: FromFile = None,
336
+ output: Output = None,
337
+ ) -> None:
338
+ """`section_edit` create."""
339
+ run_tool('section_edit', contract.SectionEditParams, 'create', {'dashboard_id': dashboard_id, 'page_id': page_id, 'title': title, 'position': position}, from_file=from_file, output=output)
340
+
341
+
342
+ @section_edit.command('delete', short_help='`section_edit` delete.')
343
+ def section_edit__delete(
344
+ dashboard_id: Annotated[str, typer.Option('--dashboard-id', help='dashboard id')],
345
+ page_id: Annotated[str, typer.Option('--page-id', help='page id')],
346
+ section_index: Annotated[int, typer.Option('--section-index', help='section index')],
347
+ cascade: Annotated[Optional[bool], typer.Option('--cascade/--no-cascade', help='cascade')] = None,
348
+ from_file: FromFile = None,
349
+ output: Output = None,
350
+ ) -> None:
351
+ """`section_edit` delete."""
352
+ run_tool('section_edit', contract.SectionEditParams, 'delete', {'dashboard_id': dashboard_id, 'page_id': page_id, 'section_index': section_index, 'cascade': cascade}, from_file=from_file, output=output)
353
+
354
+
355
+ @section_edit.command('rename', short_help='`section_edit` rename.')
356
+ def section_edit__rename(
357
+ dashboard_id: Annotated[str, typer.Option('--dashboard-id', help='dashboard id')],
358
+ page_id: Annotated[str, typer.Option('--page-id', help='page id')],
359
+ section_index: Annotated[int, typer.Option('--section-index', help='section index')],
360
+ title: Annotated[str, typer.Option('--title', help='New title (None to clear)')],
361
+ from_file: FromFile = None,
362
+ output: Output = None,
363
+ ) -> None:
364
+ """`section_edit` rename."""
365
+ run_tool('section_edit', contract.SectionEditParams, 'rename', {'dashboard_id': dashboard_id, 'page_id': page_id, 'section_index': section_index, 'title': title}, from_file=from_file, output=output)
366
+
367
+
368
+ @section_edit.command('move', short_help='`section_edit` move.')
369
+ def section_edit__move(
370
+ dashboard_id: Annotated[str, typer.Option('--dashboard-id', help='dashboard id')],
371
+ source_page_id: Annotated[str, typer.Option('--source-page-id', help='source page id')],
372
+ section_index: Annotated[int, typer.Option('--section-index', help='section index')],
373
+ target_page_id: Annotated[str, typer.Option('--target-page-id', help='target page id')],
374
+ target_position: Annotated[Optional[int], typer.Option('--target-position', help='target position')] = None,
375
+ from_file: FromFile = None,
376
+ output: Output = None,
377
+ ) -> None:
378
+ """`section_edit` move."""
379
+ run_tool('section_edit', contract.SectionEditParams, 'move', {'dashboard_id': dashboard_id, 'source_page_id': source_page_id, 'section_index': section_index, 'target_page_id': target_page_id, 'target_position': target_position}, from_file=from_file, output=output)
380
+
381
+
382
+ # ---------------------------------------------------------------------------
383
+ # business_context_edit
384
+ # ---------------------------------------------------------------------------
385
+
386
+
387
+ business_context_edit = typer.Typer(name='business_context_edit', help="Edit the org's cross-cutting business_context document.", no_args_is_help=True)
388
+
389
+
390
+ @business_context_edit.command('get', short_help='Read the current org business_context document.')
391
+ def business_context_edit__get(
392
+ from_file: FromFile = None,
393
+ output: Output = None,
394
+ ) -> None:
395
+ """Read the current org business_context document."""
396
+ run_tool('business_context_edit', contract.BusinessContextEditParams, 'get', {}, from_file=from_file, output=output)
397
+
398
+
399
+ @business_context_edit.command('show_size', short_help='Report current size and remaining room within the 2000-byte UTF-8 cap.')
400
+ def business_context_edit__show_size(
401
+ from_file: FromFile = None,
402
+ output: Output = None,
403
+ ) -> None:
404
+ """Report current size and remaining room within the 2000-byte UTF-8 cap."""
405
+ run_tool('business_context_edit', contract.BusinessContextEditParams, 'show_size', {}, from_file=from_file, output=output)
406
+
407
+
408
+ @business_context_edit.command('set', short_help="Replace the org's business_context with ``content`` (admin/owner).")
409
+ def business_context_edit__set(
410
+ content: Annotated[str, typer.Option('--content', help='Full document content to save')],
411
+ from_file: FromFile = None,
412
+ output: Output = None,
413
+ ) -> None:
414
+ """Replace the org's business_context with ``content`` (admin/owner)."""
415
+ run_tool('business_context_edit', contract.BusinessContextEditParams, 'set', {'content': content}, from_file=from_file, output=output)
416
+
417
+
418
+ @business_context_edit.command('append', short_help='Append ``content`` to the existing business_context (admin/owner).')
419
+ def business_context_edit__append(
420
+ content: Annotated[str, typer.Option('--content', help='Text to append to the current document')],
421
+ from_file: FromFile = None,
422
+ output: Output = None,
423
+ ) -> None:
424
+ """Append ``content`` to the existing business_context (admin/owner).
425
+
426
+ Reads the current value, concatenates, and writes the combined text.
427
+ Useful for incremental edits where the agent doesn't already have
428
+ the prior content in scope.
429
+ """
430
+ run_tool('business_context_edit', contract.BusinessContextEditParams, 'append', {'content': content}, from_file=from_file, output=output)
431
+
432
+
433
+ # ---------------------------------------------------------------------------
434
+ # semantic_model_edit
435
+ # ---------------------------------------------------------------------------
436
+
437
+
438
+ semantic_model_edit = typer.Typer(name='semantic_model_edit', help='Edit semantic models in the org manifest. Admin/owner only for writes.', no_args_is_help=True)
439
+
440
+
441
+ @semantic_model_edit.command('create', short_help='`semantic_model_edit` create.')
442
+ def semantic_model_edit__create(
443
+ model_data: Annotated[Optional[str], typer.Option('--model-data-json', help='Full semantic-model definition (name, measures, dimensions, entities) (JSON, or @path to a .json/.yaml file)')],
444
+ expect_version: Annotated[Optional[str], typer.Option('--expect-version', help="Managed tenants only: the datacore version (draft snapshot id) this edit was written against, from the previous edit's `version` or dataspring://datacore/files; refused with the current one when stale")] = None,
445
+ from_file: FromFile = None,
446
+ output: Output = None,
447
+ ) -> None:
448
+ """`semantic_model_edit` create."""
449
+ run_tool('semantic_model_edit', contract.SemanticModelEditParams, 'create', {'model_data': J(model_data), 'expect_version': expect_version}, from_file=from_file, output=output)
450
+
451
+
452
+ @semantic_model_edit.command('update', short_help='`semantic_model_edit` update.')
453
+ def semantic_model_edit__update(
454
+ name: Annotated[str, typer.Option('--name', help='name')],
455
+ updates: Annotated[Optional[str], typer.Option('--updates-json', help='updates (JSON, or @path to a .json/.yaml file)')],
456
+ confirmed: Annotated[Optional[bool], typer.Option('--confirmed/--no-confirmed', help='set true only after the user confirmed a redefinition of a NATIVE entity whose impact you showed them; has no effect on imported entities')] = None,
457
+ expect_version: Annotated[Optional[str], typer.Option('--expect-version', help="Managed tenants only: the datacore version (draft snapshot id) this edit was written against, from the previous edit's `version` or dataspring://datacore/files; refused with the current one when stale")] = None,
458
+ from_file: FromFile = None,
459
+ output: Output = None,
460
+ ) -> None:
461
+ """`semantic_model_edit` update."""
462
+ run_tool('semantic_model_edit', contract.SemanticModelEditParams, 'update', {'name': name, 'updates': J(updates), 'confirmed': confirmed, 'expect_version': expect_version}, from_file=from_file, output=output)
463
+
464
+
465
+ @semantic_model_edit.command('delete', short_help='`semantic_model_edit` delete.')
466
+ def semantic_model_edit__delete(
467
+ name: Annotated[str, typer.Option('--name', help='name')],
468
+ expect_version: Annotated[Optional[str], typer.Option('--expect-version', help="Managed tenants only: the datacore version (draft snapshot id) this edit was written against, from the previous edit's `version` or dataspring://datacore/files; refused with the current one when stale")] = None,
469
+ from_file: FromFile = None,
470
+ output: Output = None,
471
+ ) -> None:
472
+ """`semantic_model_edit` delete."""
473
+ run_tool('semantic_model_edit', contract.SemanticModelEditParams, 'delete', {'name': name, 'expect_version': expect_version}, from_file=from_file, output=output)
474
+
475
+
476
+ @semantic_model_edit.command('preview', short_help='Validate a semantic model without writing it or touching the warehouse.')
477
+ def semantic_model_edit__preview(
478
+ model_data: Annotated[Optional[str], typer.Option('--model-data-json', help='Full semantic-model definition to validate (name, measures, dimensions, entities) (JSON, or @path to a .json/.yaml file)')],
479
+ from_file: FromFile = None,
480
+ output: Output = None,
481
+ ) -> None:
482
+ """Validate a semantic model without writing it or touching the warehouse."""
483
+ run_tool('semantic_model_edit', contract.SemanticModelEditParams, 'preview', {'model_data': J(model_data)}, from_file=from_file, output=output)
484
+
485
+
486
+ # ---------------------------------------------------------------------------
487
+ # metric_edit
488
+ # ---------------------------------------------------------------------------
489
+
490
+
491
+ metric_edit = typer.Typer(name='metric_edit', help='Edit metrics in the org manifest. Admin/owner only for writes.', no_args_is_help=True)
492
+
493
+
494
+ @metric_edit.command('create', short_help='`metric_edit` create.')
495
+ def metric_edit__create(
496
+ metric_data: Annotated[Optional[str], typer.Option('--metric-data-json', help='Metric definition (name, type, type_params) (JSON, or @path to a .json/.yaml file)')],
497
+ expect_version: Annotated[Optional[str], typer.Option('--expect-version', help="Managed tenants only: the datacore version (draft snapshot id) this edit was written against, from the previous edit's `version` or dataspring://datacore/files; refused with the current one when stale")] = None,
498
+ from_file: FromFile = None,
499
+ output: Output = None,
500
+ ) -> None:
501
+ """`metric_edit` create."""
502
+ run_tool('metric_edit', contract.MetricEditParams, 'create', {'metric_data': J(metric_data), 'expect_version': expect_version}, from_file=from_file, output=output)
503
+
504
+
505
+ @metric_edit.command('update', short_help='`metric_edit` update.')
506
+ def metric_edit__update(
507
+ name: Annotated[str, typer.Option('--name', help='name')],
508
+ updates: Annotated[Optional[str], typer.Option('--updates-json', help='updates (JSON, or @path to a .json/.yaml file)')],
509
+ confirmed: Annotated[Optional[bool], typer.Option('--confirmed/--no-confirmed', help='set true only after the user confirmed a redefinition of a NATIVE entity whose impact you showed them; has no effect on imported entities')] = None,
510
+ expect_version: Annotated[Optional[str], typer.Option('--expect-version', help="Managed tenants only: the datacore version (draft snapshot id) this edit was written against, from the previous edit's `version` or dataspring://datacore/files; refused with the current one when stale")] = None,
511
+ from_file: FromFile = None,
512
+ output: Output = None,
513
+ ) -> None:
514
+ """`metric_edit` update."""
515
+ run_tool('metric_edit', contract.MetricEditParams, 'update', {'name': name, 'updates': J(updates), 'confirmed': confirmed, 'expect_version': expect_version}, from_file=from_file, output=output)
516
+
517
+
518
+ @metric_edit.command('delete', short_help='`metric_edit` delete.')
519
+ def metric_edit__delete(
520
+ name: Annotated[str, typer.Option('--name', help='name')],
521
+ expect_version: Annotated[Optional[str], typer.Option('--expect-version', help="Managed tenants only: the datacore version (draft snapshot id) this edit was written against, from the previous edit's `version` or dataspring://datacore/files; refused with the current one when stale")] = None,
522
+ from_file: FromFile = None,
523
+ output: Output = None,
524
+ ) -> None:
525
+ """`metric_edit` delete."""
526
+ run_tool('metric_edit', contract.MetricEditParams, 'delete', {'name': name, 'expect_version': expect_version}, from_file=from_file, output=output)
527
+
528
+
529
+ @metric_edit.command('preview', short_help='`metric_edit` preview.')
530
+ def metric_edit__preview(
531
+ metric_data: Annotated[Optional[str], typer.Option('--metric-data-json', help='Metric definition to preview (JSON, or @path to a .json/.yaml file)')],
532
+ sample_query: Annotated[Optional[str], typer.Option('--sample-query-json', help='Optional query params (start_date, end_date, grain, limit) (JSON, or @path to a .json/.yaml file)')] = None,
533
+ from_file: FromFile = None,
534
+ output: Output = None,
535
+ ) -> None:
536
+ """`metric_edit` preview."""
537
+ run_tool('metric_edit', contract.MetricEditParams, 'preview', {'metric_data': J(metric_data), 'sample_query': J(sample_query)}, from_file=from_file, output=output)
538
+
539
+
540
+ @metric_edit.command('impact', short_help='What applying ``updates`` to this metric would touch, without writing.')
541
+ def metric_edit__impact(
542
+ name: Annotated[str, typer.Option('--name', help='name')],
543
+ updates: Annotated[Optional[str], typer.Option('--updates-json', help='The updates whose impact to assess (JSON, or @path to a .json/.yaml file)')],
544
+ from_file: FromFile = None,
545
+ output: Output = None,
546
+ ) -> None:
547
+ """What applying ``updates`` to this metric would touch, without writing."""
548
+ run_tool('metric_edit', contract.MetricEditParams, 'impact', {'name': name, 'updates': J(updates)}, from_file=from_file, output=output)
549
+
550
+
551
+ # ---------------------------------------------------------------------------
552
+ # quick_metric_edit
553
+ # ---------------------------------------------------------------------------
554
+
555
+
556
+ quick_metric_edit = typer.Typer(name='quick_metric_edit', help='Edit user-defined quick metrics (arithmetic expressions over existing metrics).', no_args_is_help=True)
557
+
558
+
559
+ @quick_metric_edit.command('create', short_help='`quick_metric_edit` create.')
560
+ def quick_metric_edit__create(
561
+ name: Annotated[str, typer.Option('--name', help='Metric name (e.g., "revenue_per_order")')],
562
+ expression: Annotated[str, typer.Option('--expression', help='Arithmetic expression over existing metrics (e.g., "total_revenue / order_count")')],
563
+ description: Annotated[Optional[str], typer.Option('--description', help='description')] = None,
564
+ from_file: FromFile = None,
565
+ output: Output = None,
566
+ ) -> None:
567
+ """`quick_metric_edit` create."""
568
+ run_tool('quick_metric_edit', contract.QuickMetricEditParams, 'create', {'name': name, 'expression': expression, 'description': description}, from_file=from_file, output=output)
569
+
570
+
571
+ @quick_metric_edit.command('update', short_help='`quick_metric_edit` update.')
572
+ def quick_metric_edit__update(
573
+ metric_id: Annotated[str, typer.Option('--metric-id', help='metric id')],
574
+ name: Annotated[Optional[str], typer.Option('--name', help='name')] = None,
575
+ expression: Annotated[Optional[str], typer.Option('--expression', help='expression')] = None,
576
+ description: Annotated[Optional[str], typer.Option('--description', help='description')] = None,
577
+ from_file: FromFile = None,
578
+ output: Output = None,
579
+ ) -> None:
580
+ """`quick_metric_edit` update."""
581
+ run_tool('quick_metric_edit', contract.QuickMetricEditParams, 'update', {'metric_id': metric_id, 'name': name, 'expression': expression, 'description': description}, from_file=from_file, output=output)
582
+
583
+
584
+ @quick_metric_edit.command('delete', short_help='`quick_metric_edit` delete.')
585
+ def quick_metric_edit__delete(
586
+ metric_id: Annotated[str, typer.Option('--metric-id', help='metric id')],
587
+ from_file: FromFile = None,
588
+ output: Output = None,
589
+ ) -> None:
590
+ """`quick_metric_edit` delete."""
591
+ run_tool('quick_metric_edit', contract.QuickMetricEditParams, 'delete', {'metric_id': metric_id}, from_file=from_file, output=output)
592
+
593
+
594
+ # ---------------------------------------------------------------------------
595
+ # report_edit
596
+ # ---------------------------------------------------------------------------
597
+
598
+
599
+ report_edit = typer.Typer(name='report_edit', help='Edit scheduled reports (recurring email delivery of dashboards or queries).', no_args_is_help=True)
600
+
601
+
602
+ @report_edit.command('create', short_help='`report_edit` create.')
603
+ def report_edit__create(
604
+ name: Annotated[str, typer.Option('--name', help='name')],
605
+ frequency_type: Annotated[str, typer.Option('--frequency-type', help='frequency type (one of: daily, weekly, monthly)')],
606
+ time: Annotated[str, typer.Option('--time', help='HH:MM in UTC, 24-hour')],
607
+ recipients: Annotated[list[str], typer.Option('--recipients', help='recipients')],
608
+ dashboard_id: Annotated[Optional[str], typer.Option('--dashboard-id', help='For dashboard reports (PDF/PNG)')] = None,
609
+ metrics: Annotated[Optional[list[str]], typer.Option('--metrics', help='For query reports (CSV/JSON)')] = None,
610
+ dimensions: Annotated[Optional[list[str]], typer.Option('--dimensions', help='dimensions')] = None,
611
+ format: Annotated[Optional[str], typer.Option('--format', help='"pdf"|"png" for dashboards, "csv"|"json" for queries (default: pdf)')] = None,
612
+ day_of_week: Annotated[Optional[int], typer.Option('--day-of-week', help='0=Mon … 6=Sun (weekly)')] = None,
613
+ day_of_month: Annotated[Optional[int], typer.Option('--day-of-month', help='1-28 (monthly)')] = None,
614
+ from_file: FromFile = None,
615
+ output: Output = None,
616
+ ) -> None:
617
+ """`report_edit` create."""
618
+ run_tool('report_edit', contract.ReportEditParams, 'create', {'name': name, 'frequency_type': frequency_type, 'time': time, 'recipients': recipients, 'dashboard_id': dashboard_id, 'metrics': metrics, 'dimensions': dimensions, 'format': format, 'day_of_week': day_of_week, 'day_of_month': day_of_month}, from_file=from_file, output=output)
619
+
620
+
621
+ @report_edit.command('update', short_help='`report_edit` update.')
622
+ def report_edit__update(
623
+ schedule_id: Annotated[str, typer.Option('--schedule-id', help='schedule id')],
624
+ name: Annotated[Optional[str], typer.Option('--name', help='name')] = None,
625
+ enabled: Annotated[Optional[bool], typer.Option('--enabled/--no-enabled', help='Set False to pause without deleting')] = None,
626
+ frequency_type: Annotated[Optional[str], typer.Option('--frequency-type', help='frequency type (one of: daily, weekly, monthly)')] = None,
627
+ time: Annotated[Optional[str], typer.Option('--time', help='time')] = None,
628
+ day_of_week: Annotated[Optional[int], typer.Option('--day-of-week', help='day of week')] = None,
629
+ day_of_month: Annotated[Optional[int], typer.Option('--day-of-month', help='day of month')] = None,
630
+ recipients: Annotated[Optional[list[str]], typer.Option('--recipients', help='recipients')] = None,
631
+ from_file: FromFile = None,
632
+ output: Output = None,
633
+ ) -> None:
634
+ """`report_edit` update."""
635
+ run_tool('report_edit', contract.ReportEditParams, 'update', {'schedule_id': schedule_id, 'name': name, 'enabled': enabled, 'frequency_type': frequency_type, 'time': time, 'day_of_week': day_of_week, 'day_of_month': day_of_month, 'recipients': recipients}, from_file=from_file, output=output)
636
+
637
+
638
+ @report_edit.command('delete', short_help='`report_edit` delete.')
639
+ def report_edit__delete(
640
+ schedule_id: Annotated[str, typer.Option('--schedule-id', help='schedule id')],
641
+ from_file: FromFile = None,
642
+ output: Output = None,
643
+ ) -> None:
644
+ """`report_edit` delete."""
645
+ run_tool('report_edit', contract.ReportEditParams, 'delete', {'schedule_id': schedule_id}, from_file=from_file, output=output)
646
+
647
+
648
+ # ---------------------------------------------------------------------------
649
+ # learned_edit
650
+ # ---------------------------------------------------------------------------
651
+
652
+
653
+ learned_edit = typer.Typer(name='learned_edit', help='Browse and undo the learned trail — what DataSpring learned for this org.', no_args_is_help=True)
654
+
655
+
656
+ @learned_edit.command('list', short_help='List what DataSpring learned for this org (plus your personal entries).')
657
+ def learned_edit__list(
658
+ limit: Annotated[Optional[int], typer.Option('--limit', help='limit (default: 20)')] = None,
659
+ include_reverted: Annotated[Optional[bool], typer.Option('--include-reverted/--no-include-reverted', help='include reverted')] = None,
660
+ from_file: FromFile = None,
661
+ output: Output = None,
662
+ ) -> None:
663
+ """List what DataSpring learned for this org (plus your personal entries)."""
664
+ run_tool('learned_edit', contract.LearnedEditParams, 'list', {'limit': limit, 'include_reverted': include_reverted}, from_file=from_file, output=output)
665
+
666
+
667
+ @learned_edit.command('undo', short_help='Revert one learning (or, with no id, your most recent reversible one).')
668
+ def learned_edit__undo(
669
+ learning_id: Annotated[Optional[str], typer.Option('--learning-id', help='Learning to revert; omit for your most recent reversible entry')] = None,
670
+ from_file: FromFile = None,
671
+ output: Output = None,
672
+ ) -> None:
673
+ """Revert one learning (or, with no id, your most recent reversible one)."""
674
+ run_tool('learned_edit', contract.LearnedEditParams, 'undo', {'learning_id': learning_id}, from_file=from_file, output=output)
675
+
676
+
677
+ # ---------------------------------------------------------------------------
678
+ # verified_query_edit
679
+ # ---------------------------------------------------------------------------
680
+
681
+
682
+ verified_query_edit = typer.Typer(name='verified_query_edit', help='Curate verified queries — confirmed (question, query) pairs.', no_args_is_help=True)
683
+
684
+
685
+ @verified_query_edit.command('list', short_help="The org's verified queries plus your own personal ones.")
686
+ def verified_query_edit__list(
687
+ limit: Annotated[Optional[int], typer.Option('--limit', help='limit (default: 20)')] = None,
688
+ from_file: FromFile = None,
689
+ output: Output = None,
690
+ ) -> None:
691
+ """The org's verified queries plus your own personal ones."""
692
+ run_tool('verified_query_edit', contract.VerifiedQueryEditParams, 'list', {'limit': limit}, from_file=from_file, output=output)
693
+
694
+
695
+ @verified_query_edit.command('record', short_help='Record one confirmed (question, query) pair.')
696
+ def verified_query_edit__record(
697
+ question: Annotated[str, typer.Option('--question', help="The user's question, verbatim")],
698
+ params: Annotated[Optional[str], typer.Option('--params-json', help='The query that answers it, in the query_metrics wire shape: metrics, dimensions, grain, start_date, end_date, where, order_by, limit (JSON, or @path to a .json/.yaml file)')],
699
+ note: Annotated[Optional[str], typer.Option('--note', help='One line on WHY this is the right query for that question')] = None,
700
+ scope: Annotated[Optional[str], typer.Option('--scope', help="'user' (the default) records it in this user's personal set; 'org' curates it for everyone and requires admin/owner (one of: org, user) (default: user)")] = None,
701
+ from_file: FromFile = None,
702
+ output: Output = None,
703
+ ) -> None:
704
+ """Record one confirmed (question, query) pair."""
705
+ run_tool('verified_query_edit', contract.VerifiedQueryEditParams, 'record', {'question': question, 'params': J(params), 'note': note, 'scope': scope}, from_file=from_file, output=output)
706
+
707
+
708
+ @verified_query_edit.command('delete', short_help='Remove one verified query by id.')
709
+ def verified_query_edit__delete(
710
+ id: Annotated[str, typer.Option('--id', help='id')],
711
+ from_file: FromFile = None,
712
+ output: Output = None,
713
+ ) -> None:
714
+ """Remove one verified query by id."""
715
+ run_tool('verified_query_edit', contract.VerifiedQueryEditParams, 'delete', {'id': id}, from_file=from_file, output=output)
716
+
717
+
718
+ # ---------------------------------------------------------------------------
719
+ # warehouse_edit
720
+ # ---------------------------------------------------------------------------
721
+
722
+
723
+ warehouse_edit = typer.Typer(name='warehouse_edit', help='Manage the org\'s warehouses (admin/owner). Read them with ``dataspring://warehouses`` / ``read(kind="warehouses")``: each entry has id, kind (external | managed), label, active flag, manifest counts and import time.', no_args_is_help=True)
724
+
725
+
726
+ @warehouse_edit.command('activate', short_help="Make one warehouse the org's default: every query that names no ``warehouse`` runs against it.")
727
+ def warehouse_edit__activate(
728
+ id: Annotated[str, typer.Option('--id', help="Warehouse id, e.g. 'managed' or 'external'")],
729
+ from_file: FromFile = None,
730
+ output: Output = None,
731
+ ) -> None:
732
+ """Make one warehouse the org's default: every query that names no
733
+ ``warehouse`` runs against it.
734
+ """
735
+ run_tool('warehouse_edit', contract.WarehouseEditParams, 'activate', {'id': id}, from_file=from_file, output=output)
736
+
737
+
738
+ @warehouse_edit.command('add_external', short_help='Register a customer-owned BigQuery project as an external warehouse.')
739
+ def warehouse_edit__add_external(
740
+ label: Annotated[str, typer.Option('--label', help='Display name, e.g. "Noon\'s BigQuery"')],
741
+ project: Annotated[str, typer.Option('--project', help="The customer's GCP project id")],
742
+ dataset: Annotated[str, typer.Option('--dataset', help='The dataset the semantic manifest points at')],
743
+ location: Annotated[str, typer.Option('--location', help="BigQuery location of that dataset, e.g. 'europe-north2'")],
744
+ key_secret: Annotated[str, typer.Option('--key-secret', help="NAME of the Secret Manager secret holding the customer's service-account key (t-<org>-external-bigquery-key). Never the key itself.")],
745
+ id: Annotated[Optional[str], typer.Option('--id', help="Warehouse id to create (default 'external') (default: external)")] = None,
746
+ from_file: FromFile = None,
747
+ output: Output = None,
748
+ ) -> None:
749
+ """Register a customer-owned BigQuery project as an external warehouse.
750
+
751
+ The customer's key is NOT an argument: it lives in Secret Manager in the
752
+ data project and is only named here (``key_secret``).
753
+ """
754
+ run_tool('warehouse_edit', contract.WarehouseEditParams, 'add_external', {'label': label, 'project': project, 'dataset': dataset, 'location': location, 'key_secret': key_secret, 'id': id}, from_file=from_file, output=output)
755
+
756
+
757
+ @warehouse_edit.command('remove', short_help='Delete a warehouse and its manifest. Refused for the active warehouse and for the managed one.')
758
+ def warehouse_edit__remove(
759
+ id: Annotated[str, typer.Option('--id', help='Warehouse id to remove')],
760
+ from_file: FromFile = None,
761
+ output: Output = None,
762
+ ) -> None:
763
+ """Delete a warehouse and its manifest. Refused for the active warehouse
764
+ and for the managed one.
765
+ """
766
+ run_tool('warehouse_edit', contract.WarehouseEditParams, 'remove', {'id': id}, from_file=from_file, output=output)
767
+
768
+
769
+ # ---------------------------------------------------------------------------
770
+ # query_metrics
771
+ # ---------------------------------------------------------------------------
772
+
773
+
774
+ def query_metrics(
775
+ metrics: Annotated[list[str], typer.Option('--metrics', help="List of metric names to query (e.g., ['total_revenue', 'order_count'])")],
776
+ dimensions: Annotated[Optional[list[str]], typer.Option('--dimensions', help="Dimensions to group by. Use qualified names from dataspring://dimensions (e.g., ['customer__segment', 'order__region'])")] = None,
777
+ grain: Annotated[Optional[str], typer.Option('--grain', help="Time granularity: 'day', 'week', 'month', 'quarter', or 'year'")] = None,
778
+ start_date: Annotated[Optional[str], typer.Option('--start-date', help='Start date in YYYY-MM-DD format')] = None,
779
+ end_date: Annotated[Optional[str], typer.Option('--end-date', help='End date in YYYY-MM-DD format')] = None,
780
+ limit: Annotated[Optional[int], typer.Option('--limit', help='Maximum number of rows to return')] = None,
781
+ order_by: Annotated[Optional[str], typer.Option('--order-by', help="Column to sort by, append ' desc' for descending (e.g., 'total_revenue desc')")] = None,
782
+ where: Annotated[Optional[list[str]], typer.Option('--where', help='Row filters in MetricFlow\'s constraint syntax, one string each, e.g. "{{ Dimension(\'customer__segment\') }} NOT IN (\'Direct\')". Use qualified names from dataspring://dimensions - an unresolvable name comes back as an error whose suggestion names the qualified form. `where` decides WHICH ROWS the numbers cover; `dimensions` breaks the numbers OUT by a column; send both for a breakdown of a filtered slice. A `where` on a dimension also overrides the user\'s standing filter on that same dimension for this call.')] = None,
783
+ warehouse: Annotated[Optional[str], typer.Option('--warehouse', help="Which of the org's warehouses to run against - an id from dataspring://warehouses ('managed', 'external', ...). Omit for the active one. During a migration ask both and compare.")] = None,
784
+ suggest_visualization: Annotated[Optional[bool], typer.Option('--suggest-visualization/--no-suggest-visualization', help='Also return a suggested visualization type for the result.')] = None,
785
+ format: Annotated[Optional[str], typer.Option('--format', help="'json' (default) answers rows in `data`; 'csv' answers the same result as one CSV document in `content` (header row first) for a file or a spreadsheet, with `columns` and `row_count` alongside. (one of: json, csv) (default: json)")] = None,
786
+ from_file: FromFile = None,
787
+ output: Output = None,
788
+ ) -> None:
789
+ """Fetch metric data from the warehouse.
790
+
791
+ Consult ``list_metrics`` first: each metric's description is
792
+ authoritative for unit, source rollup, filter rules, and caveats — apply
793
+ them unless the user explicitly overrides. Segment the query around a
794
+ description's temporal discontinuity (system migration, ETL bug fix) and
795
+ say so; follow a derived-dimension pointer; refuse honestly rather than
796
+ query the closest-named metric when the catalog can't answer.
797
+
798
+ ``where`` decides WHICH ROWS the numbers cover; ``dimensions`` breaks
799
+ those numbers OUT by a column. Send both for a breakdown of a slice.
800
+ ``warehouse`` picks one of the org's warehouses (see
801
+ ``dataspring://warehouses``); omit it for the active one.
802
+ ``format="csv"`` returns the result as one CSV document in ``content``
803
+ instead of rows, for a file or a spreadsheet.
804
+
805
+ If the response carries ``applied_defaults`` (the query was rewritten
806
+ from saved preferences), state every entry's ``echo_text`` in the
807
+ answer, next to the number it changed ("Revenue $94k, excluding Direct —
808
+ your standing filter") — a defaulted number is not the unqualified one.
809
+ A ``where`` you send on the same dimension overrides that standing
810
+ filter and nothing is reported for it — say that too.
811
+
812
+ Examples:
813
+ - Monthly revenue: metrics=['total_revenue'], grain='month', start_date='2024-01-01'
814
+ - Revenue by region: metrics=['total_revenue'], dimensions=['region']
815
+ - Top 10: metrics=['total_revenue'], dimensions=['customer_name'], order_by='total_revenue desc', limit=10
816
+ """
817
+ run_tool('query_metrics', contract.QueryMetricsParams, None, {'metrics': metrics, 'dimensions': dimensions, 'grain': grain, 'start_date': start_date, 'end_date': end_date, 'limit': limit, 'order_by': order_by, 'where': where, 'warehouse': warehouse, 'suggest_visualization': suggest_visualization, 'format': format}, from_file=from_file, output=output)
818
+
819
+
820
+ # ---------------------------------------------------------------------------
821
+ # explain_query
822
+ # ---------------------------------------------------------------------------
823
+
824
+
825
+ def explain_query(
826
+ metrics: Annotated[list[str], typer.Option('--metrics', help="List of metric names to query (e.g., ['total_revenue', 'order_count'])")],
827
+ dimensions: Annotated[Optional[list[str]], typer.Option('--dimensions', help="Dimensions to group by. Use qualified names from dataspring://dimensions (e.g., ['customer__segment', 'order__region'])")] = None,
828
+ grain: Annotated[Optional[str], typer.Option('--grain', help="Time granularity: 'day', 'week', 'month', 'quarter', or 'year'")] = None,
829
+ start_date: Annotated[Optional[str], typer.Option('--start-date', help='Start date in YYYY-MM-DD format')] = None,
830
+ end_date: Annotated[Optional[str], typer.Option('--end-date', help='End date in YYYY-MM-DD format')] = None,
831
+ limit: Annotated[Optional[int], typer.Option('--limit', help='Maximum number of rows to return')] = None,
832
+ order_by: Annotated[Optional[str], typer.Option('--order-by', help="Column to sort by, append ' desc' for descending (e.g., 'total_revenue desc')")] = None,
833
+ where: Annotated[Optional[list[str]], typer.Option('--where', help='Row filters in MetricFlow\'s constraint syntax, one string each, e.g. "{{ Dimension(\'customer__segment\') }} NOT IN (\'Direct\')". Use qualified names from dataspring://dimensions - an unresolvable name comes back as an error whose suggestion names the qualified form. `where` decides WHICH ROWS the numbers cover; `dimensions` breaks the numbers OUT by a column; send both for a breakdown of a filtered slice. A `where` on a dimension also overrides the user\'s standing filter on that same dimension for this call.')] = None,
834
+ warehouse: Annotated[Optional[str], typer.Option('--warehouse', help="Which of the org's warehouses to run against - an id from dataspring://warehouses ('managed', 'external', ...). Omit for the active one. During a migration ask both and compare.")] = None,
835
+ from_file: FromFile = None,
836
+ output: Output = None,
837
+ ) -> None:
838
+ """Show what a query computes without running it: the resolved metrics and
839
+ dimensions, the effective time window, the filters (including any standing
840
+ preference folded in) and the compiled SQL. Use it when the user asks what
841
+ you actually computed, disputes a number, or before proposing a correction.
842
+ ``warehouse`` picks one of the org's warehouses; omit it for the active one.
843
+ """
844
+ run_tool('explain_query', contract.ExplainQueryParams, None, {'metrics': metrics, 'dimensions': dimensions, 'grain': grain, 'start_date': start_date, 'end_date': end_date, 'limit': limit, 'order_by': order_by, 'where': where, 'warehouse': warehouse}, from_file=from_file, output=output)
845
+
846
+
847
+ # ---------------------------------------------------------------------------
848
+ # update_context
849
+ # ---------------------------------------------------------------------------
850
+
851
+
852
+ def update_context(
853
+ updates: Annotated[Optional[str], typer.Option('--updates-json', help='Fields to update; only these change. Presentation: default_currency, default_grain, decimal_places, preferred_chart_type. Lists and maps REPLACE what is stored: favorite_metrics, standing_filters ([{dimension, operator: in|not_in|eq|neq, values}]), metric_substitutions ({asked: preferred}), default_segment ({dimension, value} or null). (JSON, or @path to a .json/.yaml file)')],
854
+ from_file: FromFile = None,
855
+ output: Output = None,
856
+ ) -> None:
857
+ """Update user preferences (merge, not replace).
858
+
859
+ Use this when you learn a user preference in conversation. Only
860
+ provided fields are updated; others remain unchanged.
861
+
862
+ Args:
863
+ updates: Fields to update. Supported fields:
864
+ - default_currency, default_grain, decimal_places,
865
+ preferred_chart_type: presentation prefs.
866
+ - favorite_metrics: list of metric names. REPLACES the stored
867
+ list — read current favourites first (``dataspring://context`` /
868
+ ``get_context``) and send them back with the new one, or the
869
+ ones left out are dropped.
870
+ - standing_filters: filters applied to every query, as
871
+ [{"dimension": "customer__segment", "operator": "not_in",
872
+ "values": ["Direct"]}]. Operators: in, not_in, eq, neq
873
+ (eq/neq take exactly one value). REPLACES the stored list.
874
+ - metric_substitutions: {asked_metric: preferred_metric}, e.g.
875
+ {"revenue": "total_revenue_ex_vat"} — what to query when the
876
+ user asks by that word. REPLACES the stored map.
877
+ - default_segment: {"dimension": ..., "value": ...} (or null to
878
+ clear) — the slice to assume when the user names none.
879
+
880
+ Dimension/metric names in the last three must exist in this org's
881
+ semantic layer, or the write is rejected, naming them. Bare
882
+ dimension names are stored qualified.
883
+
884
+ Example: update_context({"default_currency": "EUR", "default_grain": "month"})
885
+ """
886
+ run_tool('update_context', contract.UpdateContextParams, None, {'updates': J(updates)}, from_file=from_file, output=output)
887
+
888
+
889
+ # ---------------------------------------------------------------------------
890
+ # export_data
891
+ # ---------------------------------------------------------------------------
892
+
893
+
894
+ def export_data(
895
+ format: Annotated[Optional[str], typer.Option('--format', help="Output format: 'csv' or 'json'. (one of: csv, json) (default: csv)")] = None,
896
+ metrics: Annotated[Optional[list[str]], typer.Option('--metrics', help='Metric names to query (query export).')] = None,
897
+ dimensions: Annotated[Optional[list[str]], typer.Option('--dimensions', help='Dimensions to group by (query export).')] = None,
898
+ grain: Annotated[Optional[str], typer.Option('--grain', help="Time granularity: 'day', 'week', 'month', 'quarter' or 'year' (query export).")] = None,
899
+ start_date: Annotated[Optional[str], typer.Option('--start-date', help='Start date, YYYY-MM-DD (query export).')] = None,
900
+ end_date: Annotated[Optional[str], typer.Option('--end-date', help='End date, YYYY-MM-DD (query export).')] = None,
901
+ dashboard_id: Annotated[Optional[str], typer.Option('--dashboard-id', help='Dashboard id for a dashboard export; takes precedence over the query fields.')] = None,
902
+ output_path: Annotated[Optional[str], typer.Option('--output-path', help='If given, save to this file path instead of returning the content.')] = None,
903
+ warehouse: Annotated[Optional[str], typer.Option('--warehouse', help="One of the org's warehouses (see dataspring://warehouses); omit for the active one (query export only).")] = None,
904
+ from_file: FromFile = None,
905
+ output: Output = None,
906
+ ) -> None:
907
+ """Export query results or dashboard data to CSV or JSON.
908
+
909
+ You can either:
910
+ - Export query results by specifying metrics, dimensions, etc.
911
+ - Export all data from a dashboard by specifying dashboard_id
912
+
913
+ Args:
914
+ format: Output format - "csv" or "json" (default: csv)
915
+ metrics: List of metric names to query (for query export)
916
+ dimensions: Dimensions to group by (for query export)
917
+ grain: Time granularity (for query export)
918
+ start_date: Start date YYYY-MM-DD (for query export)
919
+ end_date: End date YYYY-MM-DD (for query export)
920
+ dashboard_id: Dashboard ID (for dashboard export, overrides query params)
921
+ output_path: If provided, save to this file path
922
+ warehouse: One of the org's warehouses (see dataspring://warehouses);
923
+ omit for the active one (query export only)
924
+
925
+ Returns the data content (CSV or JSON) or file path if saved.
926
+ """
927
+ run_tool('export_data', contract.ExportDataParams, None, {'format': format, 'metrics': metrics, 'dimensions': dimensions, 'grain': grain, 'start_date': start_date, 'end_date': end_date, 'dashboard_id': dashboard_id, 'output_path': output_path, 'warehouse': warehouse}, from_file=from_file, output=output)
928
+
929
+
930
+ # ---------------------------------------------------------------------------
931
+ # import_manifest
932
+ # ---------------------------------------------------------------------------
933
+
934
+
935
+ def import_manifest(
936
+ manifest_data: Annotated[Optional[str], typer.Option('--manifest-data-json', help='The semantic manifest to import (semantic models, metrics), as a JSON object. (JSON, or @path to a .json/.yaml file)')],
937
+ warehouse: Annotated[Optional[str], typer.Option('--warehouse', help="Which of the org's warehouses the manifest belongs to (see dataspring://warehouses); omit for the active one.")] = None,
938
+ from_file: FromFile = None,
939
+ output: Output = None,
940
+ ) -> None:
941
+ """Import a semantic manifest using replace semantics.
942
+
943
+ The manifest is the source of truth for the imported namespace. Items
944
+ absent from the upload are hard-deleted. User-created (DataSpring UI/MCP)
945
+ metrics and models are never touched. A name collision with a
946
+ user-created item is reported as a conflict; the user-created version
947
+ wins and the upload entry is skipped.
948
+
949
+ The upload is rejected if the uploaded manifest is internally
950
+ inconsistent or would leave the org with unresolved metric→measure /
951
+ metric→metric / quick-metric references.
952
+
953
+ ``warehouse`` names which of the org's warehouses the manifest belongs
954
+ to (each has its own); omit it for the active one. Requires admin or
955
+ owner role.
956
+ """
957
+ run_tool('import_manifest', contract.ImportManifestParams, None, {'manifest_data': J(manifest_data), 'warehouse': warehouse}, from_file=from_file, output=output)
958
+
959
+
960
+ # ---------------------------------------------------------------------------
961
+ # render_dashboard
962
+ # ---------------------------------------------------------------------------
963
+
964
+
965
+ def render_dashboard(
966
+ dashboard_id: Annotated[str, typer.Option('--dashboard-id', help='ID of the dashboard to render.')],
967
+ format: Annotated[Optional[str], typer.Option('--format', help="'app' for an interactive iframe payload (default), 'pdf' or 'png' for a static export. (one of: pdf, png, app) (default: app)")] = None,
968
+ page_id: Annotated[Optional[str], typer.Option('--page-id', help='(pdf/png only) One page to render; default: all pages.')] = None,
969
+ width: Annotated[Optional[int], typer.Option('--width', help='(pdf/png only) Viewport width in pixels. (default: 1200)')] = None,
970
+ height: Annotated[Optional[int], typer.Option('--height', help='(pdf/png only) Viewport height in pixels. (default: 800)')] = None,
971
+ output_path: Annotated[Optional[str], typer.Option('--output-path', help='(pdf/png only) Save to this file path instead of returning base64.')] = None,
972
+ warehouse: Annotated[Optional[str], typer.Option('--warehouse', help="One of the org's warehouses (see dataspring://warehouses) the widgets query; omit for the active one.")] = None,
973
+ from_file: FromFile = None,
974
+ output: Output = None,
975
+ ) -> None:
976
+ """Render a dashboard.
977
+
978
+ Modes:
979
+ - **app** (default): returns an interactive iframe payload that hosts
980
+ with the MCP UI extension (Claude.ai, Claude Code, `fastmcp dev apps`)
981
+ render inline. Hosts without UI support receive the JSON payload and
982
+ can open ``embed_url`` in a browser tab.
983
+ - **pdf** / **png**: headless-Chrome static export (legacy path).
984
+
985
+ Args:
986
+ dashboard_id: ID of the dashboard to render
987
+ format: "app" for interactive iframe (default), "pdf"/"png" for
988
+ static export.
989
+ page_id: (pdf/png only) Optional page ID to render (default: all pages)
990
+ width: (pdf/png only) Viewport width in pixels (default: 1200)
991
+ height: (pdf/png only) Viewport height in pixels (default: 800)
992
+ output_path: (pdf/png only) If provided, save to this file path instead
993
+ of returning base64.
994
+ warehouse: One of the org's warehouses (see dataspring://warehouses)
995
+ the widgets query; omit for the active one.
996
+
997
+ App mode is iframed by hosts that advertise the MCP UI extension.
998
+ PDF/PNG modes require headless Chrome.
999
+ """
1000
+ run_tool('render_dashboard', contract.RenderDashboardParams, None, {'dashboard_id': dashboard_id, 'format': format, 'page_id': page_id, 'width': width, 'height': height, 'output_path': output_path, 'warehouse': warehouse}, from_file=from_file, output=output)
1001
+
1002
+
1003
+ # ---------------------------------------------------------------------------
1004
+ # render_widget
1005
+ # ---------------------------------------------------------------------------
1006
+
1007
+
1008
+ def render_widget(
1009
+ dashboard_id: Annotated[str, typer.Option('--dashboard-id', help='ID of the dashboard containing the widget.')],
1010
+ widget_id: Annotated[str, typer.Option('--widget-id', help='ID of the widget to render.')],
1011
+ format: Annotated[Optional[str], typer.Option('--format', help="'app' for an interactive iframe payload (default), 'png' for a static snapshot. (one of: png, app) (default: app)")] = None,
1012
+ width: Annotated[Optional[int], typer.Option('--width', help='(png only) Viewport width in pixels. (default: 600)')] = None,
1013
+ height: Annotated[Optional[int], typer.Option('--height', help='(png only) Viewport height in pixels. (default: 400)')] = None,
1014
+ output_path: Annotated[Optional[str], typer.Option('--output-path', help='(png only) Save to this file path instead of returning base64.')] = None,
1015
+ warehouse: Annotated[Optional[str], typer.Option('--warehouse', help="One of the org's warehouses (see dataspring://warehouses) the widget queries; omit for the active one.")] = None,
1016
+ from_file: FromFile = None,
1017
+ output: Output = None,
1018
+ ) -> None:
1019
+ """Render a single widget.
1020
+
1021
+ Modes:
1022
+ - **app** (default): interactive iframe payload; hosts with the MCP UI
1023
+ extension render it inline.
1024
+ - **png**: headless-Chrome static snapshot as base64 PNG.
1025
+
1026
+ Args:
1027
+ dashboard_id: ID of the dashboard containing the widget
1028
+ widget_id: ID of the widget to render
1029
+ format: "app" for interactive iframe (default), "png" for static snapshot.
1030
+ width: (png only) Viewport width in pixels (default: 600)
1031
+ height: (png only) Viewport height in pixels (default: 400)
1032
+ output_path: (png only) Save to this file path instead of returning base64.
1033
+ warehouse: One of the org's warehouses (see dataspring://warehouses)
1034
+ the widget queries; omit for the active one.
1035
+
1036
+ App mode is iframed by hosts that advertise the MCP UI extension.
1037
+ PNG mode requires headless Chrome.
1038
+ """
1039
+ run_tool('render_widget', contract.RenderWidgetParams, None, {'dashboard_id': dashboard_id, 'widget_id': widget_id, 'format': format, 'width': width, 'height': height, 'output_path': output_path, 'warehouse': warehouse}, from_file=from_file, output=output)
1040
+
1041
+
1042
+ # ---------------------------------------------------------------------------
1043
+ # switch_organization
1044
+ # ---------------------------------------------------------------------------
1045
+
1046
+
1047
+ def switch_organization(
1048
+ org_id: Annotated[str, typer.Option('--org-id', help='The organization ID to switch to (see read(kind="organizations")).')],
1049
+ from_file: FromFile = None,
1050
+ output: Output = None,
1051
+ ) -> None:
1052
+ """Switch this Claude Code (stdio) session or CLI login to a different
1053
+ organization: it becomes the account's default org and the next call
1054
+ runs in it. Browse ids via ``read(kind="organizations")``.
1055
+
1056
+ Over HTTP (Claude.ai and other remote hosts) an organization is chosen
1057
+ per connection by ``?org=<id>`` on the MCP URL, never by a call, so this
1058
+ tool is refused there: the error names the exact URL to connect with for
1059
+ the target org.
1060
+
1061
+ Raises:
1062
+ DispatchError: the org does not exist, or the switch failed. This one
1063
+ fails loudly rather than returning ``{"error": …}`` — a surface
1064
+ that silently kept the old org would answer the next question
1065
+ against the wrong warehouse.
1066
+ """
1067
+ run_tool('switch_organization', contract.SwitchOrganizationParams, None, {'org_id': org_id}, from_file=from_file, output=output)
1068
+
1069
+
1070
+ # ---------------------------------------------------------------------------
1071
+ # submit_error_report
1072
+ # ---------------------------------------------------------------------------
1073
+
1074
+
1075
+ def submit_error_report(
1076
+ command: Annotated[str, typer.Option('--command', help="The tool, resource, or action that failed (e.g. 'query_metrics', 'dataspring://dashboards', 'dashboard_edit').")],
1077
+ error_message: Annotated[str, typer.Option('--error-message', help='The exact error message received.')],
1078
+ expected: Annotated[Optional[str], typer.Option('--expected', help='What you expected to happen.')] = None,
1079
+ context_info: Annotated[Optional[str], typer.Option('--context-info', help='What you were trying to achieve.')] = None,
1080
+ source: Annotated[Optional[str], typer.Option('--source', help="Which surface sent the report: 'mcp', 'cli' or 'api'. (default: mcp)")] = None,
1081
+ from_file: FromFile = None,
1082
+ output: Output = None,
1083
+ ) -> None:
1084
+ """Submit an error report to help improve DataSpring.
1085
+
1086
+ Use this when a tool returns an unexpected error or incorrect result.
1087
+
1088
+ Args:
1089
+ command: The tool, resource, or action that failed (e.g. 'query_metrics', 'dataspring://dashboards', 'dashboard_edit')
1090
+ error_message: The exact error message received
1091
+ expected: What you expected to happen (optional)
1092
+ context_info: What you were trying to achieve (optional)
1093
+ """
1094
+ run_tool('submit_error_report', contract.SubmitErrorReportParams, None, {'command': command, 'error_message': error_message, 'expected': expected, 'context_info': context_info, 'source': source}, from_file=from_file, output=output)
1095
+
1096
+
1097
+ # ---------------------------------------------------------------------------
1098
+ # secret_edit
1099
+ # ---------------------------------------------------------------------------
1100
+
1101
+
1102
+ secret_edit = typer.Typer(name='secret_edit', help='Customer secrets, both directions, as one-time LINKS (admin/owner). A value never passes through a model: hand the person the ``link`` (valid 15 minutes, this organization only, works once).', no_args_is_help=True)
1103
+
1104
+
1105
+ @secret_edit.command('set', short_help='Get a one-time link where a PERSON pastes a credential. The value never comes through here; the same name rotates a credential.')
1106
+ def secret_edit__set(
1107
+ name: Annotated[str, typer.Option('--name', help='The secret to set or rotate: t-<org>-<connection>-<field> for your organization, lower-case (t-acme-kanpla-api_key, t-acme-external-bigquery-key). A connection file names it.')],
1108
+ from_file: FromFile = None,
1109
+ output: Output = None,
1110
+ ) -> None:
1111
+ """Get a one-time link where a PERSON pastes a credential. The value
1112
+ never comes through here; the same name rotates a credential.
1113
+ """
1114
+ run_tool('secret_edit', contract.SecretEditParams, 'set', {'name': name}, from_file=from_file, output=output)
1115
+
1116
+
1117
+ @secret_edit.command('reveal', short_help='Get a one-time link that shows a credential DataSpring ISSUED to this organization (the Airbyte destination key today), once.')
1118
+ def secret_edit__reveal(
1119
+ name: Annotated[Optional[str], typer.Option('--name', help='The issued credential, by secret name. Omit for the Airbyte destination key (t-<org>-airbyte-key). Only issued credentials can be revealed, never ones you gave DataSpring.')] = None,
1120
+ from_file: FromFile = None,
1121
+ output: Output = None,
1122
+ ) -> None:
1123
+ """Get a one-time link that shows a credential DataSpring ISSUED to this
1124
+ organization (the Airbyte destination key today), once.
1125
+ """
1126
+ run_tool('secret_edit', contract.SecretEditParams, 'reveal', {'name': name}, from_file=from_file, output=output)
1127
+
1128
+
1129
+ # ---------------------------------------------------------------------------
1130
+ # run_sql
1131
+ # ---------------------------------------------------------------------------
1132
+
1133
+
1134
+ def run_sql(
1135
+ sql: Annotated[str, typer.Option('--sql', help='One SQL statement (BigQuery Standard SQL on a managed warehouse). Name tables as <org>_marts.<table>, <org>_core.<table> or <org>_staging.<table>; the identity it runs as can read those and nothing else, so a write or a reach outside them is refused by the warehouse.')],
1136
+ warehouse: Annotated[Optional[str], typer.Option('--warehouse', help="One of the org's warehouses (see dataspring://warehouses); omit for the active one.")] = None,
1137
+ max_rows: Annotated[Optional[int], typer.Option('--max-rows', help='Rows to return at most (default 1000, cap 10000). Aggregate in SQL rather than paging. (default: 1000)')] = None,
1138
+ format: Annotated[Optional[str], typer.Option('--format', help="'json' answers rows in `data`; 'csv' or 'markdown' answer one document in `content`. (one of: json, csv, markdown) (default: json)")] = None,
1139
+ dry_run: Annotated[Optional[bool], typer.Option('--dry-run/--no-dry-run', help='Only estimate the bytes the statement would process (and check it parses); run nothing.')] = None,
1140
+ from_file: FromFile = None,
1141
+ output: Output = None,
1142
+ ) -> None:
1143
+ """Run ONE read-only SQL statement against the org's warehouse and get
1144
+ the rows back, ungoverned.
1145
+
1146
+ Metrics first: ``list_metrics`` / ``query_metrics`` answer with the
1147
+ org's own definitions (units, filters, caveats) and are what a number
1148
+ in an answer should come from. Use ``run_sql`` for what the semantic
1149
+ layer does not expose: a column it has no dimension for, a row-level
1150
+ look at a mart, a join the models do not make, checking a load. Say in
1151
+ the answer that such numbers are not metric definitions (every result
1152
+ carries ``governed: false`` and a note).
1153
+
1154
+ On a managed warehouse the statement runs as the tenant's read-only
1155
+ identity (``t-<org>-reader@``: marts, core and staging, nothing else),
1156
+ so any write, export or reach outside those datasets is refused by
1157
+ BigQuery itself. It is dry-run first and refused over the byte cap;
1158
+ the timeout and ``max_rows`` bound it further. On an external
1159
+ warehouse it runs with the customer's own key. Every call is logged
1160
+ with the statement and the caller.
1161
+ """
1162
+ run_tool('run_sql', contract.RunSqlParams, None, {'sql': sql, 'warehouse': warehouse, 'max_rows': max_rows, 'format': format, 'dry_run': dry_run}, from_file=from_file, output=output)
1163
+
1164
+
1165
+ # ---------------------------------------------------------------------------
1166
+ # datacore_edit
1167
+ # ---------------------------------------------------------------------------
1168
+
1169
+
1170
+ datacore_edit = typer.Typer(name='datacore_edit', help='Edit the datacore workspace: connections, pipelines and the dbt project of a managed tenant, as files. ``kind`` picks the family:', no_args_is_help=True)
1171
+
1172
+
1173
+ @datacore_edit.command('apply', short_help='Write one workspace file whole (create or replace): a connection or pipeline as a ``document`` (validated against its schema and the catalog), a dbt project file as ``content``.')
1174
+ def datacore_edit__apply(
1175
+ kind: Annotated[str, typer.Option('--kind', help='connection (admin), pipeline (admin) or model (member) (one of: connection, pipeline, model)')],
1176
+ name: Annotated[str, typer.Option('--name', help='For kind connection or pipeline: the name, i.e. the file name without .yaml (connections/<name>.yaml, pipelines/<name>.yaml). For kind model: the path under models/, the dbt project (models/models/marts/orders.sql, models/models/staging/schema.yml, models/seeds/x.csv, models/dbt_project.yml); no traversal, .sql .yml .yaml .csv .md .txt .json, never target/ or dbt_packages/')],
1177
+ document: Annotated[Optional[str], typer.Option('--document-json', help='kind connection: the whole document (connection.schema.json: kind, base_url, auth with secret NAMES). kind pipeline: the whole document (pipeline.schema.json: a catalog `connector` with config, secrets, overrides; or a `connection` reference, an inline `source` or named `sources`, with `resources`; schedule, checks, then). Secrets are named, never valued; every name must exist (JSON, or @path to a .json/.yaml file)')] = None,
1178
+ content: Annotated[Optional[str], typer.Option('--content', help='kind model: the whole file content')] = None,
1179
+ expect_version: Annotated[Optional[str], typer.Option('--expect-version', help='The datacore version this edit was written against: the `version` the previous edit returned, or dataspring://datacore/files. An edit against a stale version is refused with the current one. Omit to skip the check.')] = None,
1180
+ from_file: FromFile = None,
1181
+ output: Output = None,
1182
+ ) -> None:
1183
+ """Write one workspace file whole (create or replace): a connection or
1184
+ pipeline as a ``document`` (validated against its schema and the
1185
+ catalog), a dbt project file as ``content``.
1186
+ """
1187
+ run_tool('datacore_edit', contract.DatacoreEditParams, 'apply', {'kind': kind, 'name': name, 'document': J(document), 'content': content, 'expect_version': expect_version}, from_file=from_file, output=output)
1188
+
1189
+
1190
+ @datacore_edit.command('patch', short_help='Apply a JSON merge patch (RFC 7386: a key set to null is removed) to an existing YAML file: a connection, a pipeline (a schedule, a window, a check), or a YAML file under models/ (not SQL).')
1191
+ def datacore_edit__patch(
1192
+ kind: Annotated[str, typer.Option('--kind', help='connection (admin), pipeline (admin) or model (member) (one of: connection, pipeline, model)')],
1193
+ name: Annotated[str, typer.Option('--name', help='For kind connection or pipeline: the name, i.e. the file name without .yaml (connections/<name>.yaml, pipelines/<name>.yaml). For kind model: the path under models/, the dbt project (models/models/marts/orders.sql, models/models/staging/schema.yml, models/seeds/x.csv, models/dbt_project.yml); no traversal, .sql .yml .yaml .csv .md .txt .json, never target/ or dbt_packages/')],
1194
+ patch: Annotated[Optional[str], typer.Option('--patch-json', help='JSON merge patch over the current document (JSON, or @path to a .json/.yaml file)')],
1195
+ expect_version: Annotated[Optional[str], typer.Option('--expect-version', help='The datacore version this edit was written against: the `version` the previous edit returned, or dataspring://datacore/files. An edit against a stale version is refused with the current one. Omit to skip the check.')] = None,
1196
+ from_file: FromFile = None,
1197
+ output: Output = None,
1198
+ ) -> None:
1199
+ """Apply a JSON merge patch (RFC 7386: a key set to null is removed) to an
1200
+ existing YAML file: a connection, a pipeline (a schedule, a window, a
1201
+ check), or a YAML file under models/ (not SQL).
1202
+ """
1203
+ run_tool('datacore_edit', contract.DatacoreEditParams, 'patch', {'kind': kind, 'name': name, 'patch': J(patch), 'expect_version': expect_version}, from_file=from_file, output=output)
1204
+
1205
+
1206
+ @datacore_edit.command('delete', short_help='Remove one workspace file.')
1207
+ def datacore_edit__delete(
1208
+ kind: Annotated[str, typer.Option('--kind', help='connection (admin), pipeline (admin) or model (member) (one of: connection, pipeline, model)')],
1209
+ name: Annotated[str, typer.Option('--name', help='For kind connection or pipeline: the name, i.e. the file name without .yaml (connections/<name>.yaml, pipelines/<name>.yaml). For kind model: the path under models/, the dbt project (models/models/marts/orders.sql, models/models/staging/schema.yml, models/seeds/x.csv, models/dbt_project.yml); no traversal, .sql .yml .yaml .csv .md .txt .json, never target/ or dbt_packages/')],
1210
+ expect_version: Annotated[Optional[str], typer.Option('--expect-version', help='The datacore version this edit was written against: the `version` the previous edit returned, or dataspring://datacore/files. An edit against a stale version is refused with the current one. Omit to skip the check.')] = None,
1211
+ from_file: FromFile = None,
1212
+ output: Output = None,
1213
+ ) -> None:
1214
+ """Remove one workspace file."""
1215
+ run_tool('datacore_edit', contract.DatacoreEditParams, 'delete', {'kind': kind, 'name': name, 'expect_version': expect_version}, from_file=from_file, output=output)
1216
+
1217
+
1218
+ # ---------------------------------------------------------------------------
1219
+ # datacore_run
1220
+ # ---------------------------------------------------------------------------
1221
+
1222
+
1223
+ datacore_run = typer.Typer(name='datacore_run', help="The datacore's check-then-deploy loop and its operations.", no_args_is_help=True)
1224
+
1225
+
1226
+ @datacore_run.command('check', short_help='Check the current draft; no arguments beyond an optional note.')
1227
+ def datacore_run__check(
1228
+ note: Annotated[Optional[str], typer.Option('--note', help='Optional note, recorded on the check run')] = None,
1229
+ from_file: FromFile = None,
1230
+ output: Output = None,
1231
+ ) -> None:
1232
+ """Check the current draft; no arguments beyond an optional note."""
1233
+ run_tool('datacore_run', contract.DatacoreRunParams, 'check', {'note': note}, from_file=from_file, output=output)
1234
+
1235
+
1236
+ @datacore_run.command('deploy', short_help='`datacore_run` deploy.')
1237
+ def datacore_run__deploy(
1238
+ snapshot: Annotated[Optional[str], typer.Option('--snapshot', help='Omit to promote the current draft (needs a green check bound to it). A previously deployed snapshot id rolls back to it')] = None,
1239
+ note: Annotated[Optional[str], typer.Option('--note', help='Why, recorded on the learned trail')] = None,
1240
+ from_file: FromFile = None,
1241
+ output: Output = None,
1242
+ ) -> None:
1243
+ """`datacore_run` deploy."""
1244
+ run_tool('datacore_run', contract.DatacoreRunParams, 'deploy', {'snapshot': snapshot, 'note': note}, from_file=from_file, output=output)
1245
+
1246
+
1247
+ @datacore_run.command('run', short_help='`datacore_run` run.')
1248
+ def datacore_run__run(
1249
+ pipeline: Annotated[str, typer.Option('--pipeline', help='Pipeline name (a deployed pipelines/<name>.yaml)')],
1250
+ window: Annotated[Optional[str], typer.Option('--window-json', help="A date window to load instead of the pipeline's own; wider than backfill_chunk becomes a chunked backfill (JSON object with keys: start, end, or @path to a .json/.yaml file)")] = None,
1251
+ cursor: Annotated[Optional[str], typer.Option('--cursor-json', help='{from: <value>}: a cursor to re-pull from (JSON object with keys: from, or @path to a .json/.yaml file)')] = None,
1252
+ backfill_chunk: Annotated[Optional[str], typer.Option('--backfill-chunk', help='Chunk size for a windowed run: 7d, 2w or 1M (calendar months, the default)')] = None,
1253
+ from_file: FromFile = None,
1254
+ output: Output = None,
1255
+ ) -> None:
1256
+ """`datacore_run` run."""
1257
+ run_tool('datacore_run', contract.DatacoreRunParams, 'run', {'pipeline': pipeline, 'window': J(window), 'cursor': J(cursor), 'backfill_chunk': backfill_chunk}, from_file=from_file, output=output)
1258
+
1259
+
1260
+ @datacore_run.command('reset', short_help='`datacore_run` reset.')
1261
+ def datacore_run__reset(
1262
+ pipeline: Annotated[str, typer.Option('--pipeline', help='The pipeline whose dlt state and raw tables to remove')],
1263
+ approve: Annotated[Optional[bool], typer.Option('--approve/--no-approve', help='Must be true: the reset is irreversible. Ask the user first')] = None,
1264
+ from_file: FromFile = None,
1265
+ output: Output = None,
1266
+ ) -> None:
1267
+ """`datacore_run` reset."""
1268
+ run_tool('datacore_run', contract.DatacoreRunParams, 'reset', {'pipeline': pipeline, 'approve': approve}, from_file=from_file, output=output)
1269
+
1270
+
1271
+ def mount(app: typer.Typer) -> None:
1272
+ """Add every generated command to ``app``: flat tools at the root, families as groups."""
1273
+ app.add_typer(dashboard_edit, name='dashboard_edit')
1274
+ app.add_typer(widget_edit, name='widget_edit')
1275
+ app.add_typer(page_edit, name='page_edit')
1276
+ app.add_typer(section_edit, name='section_edit')
1277
+ app.add_typer(business_context_edit, name='business_context_edit')
1278
+ app.add_typer(semantic_model_edit, name='semantic_model_edit')
1279
+ app.add_typer(metric_edit, name='metric_edit')
1280
+ app.add_typer(quick_metric_edit, name='quick_metric_edit')
1281
+ app.add_typer(report_edit, name='report_edit')
1282
+ app.add_typer(learned_edit, name='learned_edit')
1283
+ app.add_typer(verified_query_edit, name='verified_query_edit')
1284
+ app.add_typer(warehouse_edit, name='warehouse_edit')
1285
+ app.command('query_metrics', short_help='Fetch metric data from the warehouse.')(query_metrics)
1286
+ app.command('explain_query', short_help="Show what a query computes without running it: the resolved metrics and dimensions, the effective time window, the filters (including any standing preference folded in) and the compiled SQL. Use it when the user asks what you actually computed, disputes a number, or before proposing a correction. ``warehouse`` picks one of the org's warehouses; omit it for the active one.")(explain_query)
1287
+ app.command('update_context', short_help='Update user preferences (merge, not replace).')(update_context)
1288
+ app.command('export_data', short_help='Export query results or dashboard data to CSV or JSON.')(export_data)
1289
+ app.command('import_manifest', short_help='Import a semantic manifest using replace semantics.')(import_manifest)
1290
+ app.command('render_dashboard', short_help='Render a dashboard.')(render_dashboard)
1291
+ app.command('render_widget', short_help='Render a single widget.')(render_widget)
1292
+ app.command('switch_organization', short_help='Switch this Claude Code (stdio) session or CLI login to a different organization: it becomes the account\'s default org and the next call runs in it. Browse ids via ``read(kind="organizations")``.')(switch_organization)
1293
+ app.command('submit_error_report', short_help='Submit an error report to help improve DataSpring.')(submit_error_report)
1294
+ app.add_typer(secret_edit, name='secret_edit')
1295
+ app.command('run_sql', short_help="Run ONE read-only SQL statement against the org's warehouse and get the rows back, ungoverned.")(run_sql)
1296
+ app.add_typer(datacore_edit, name='datacore_edit')
1297
+ app.add_typer(datacore_run, name='datacore_run')