mammoth-cli 1.0.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.
- mammoth_cli/__init__.py +14 -0
- mammoth_cli/__main__.py +27 -0
- mammoth_cli/app.py +565 -0
- mammoth_cli/bundled_skill/mammoth-cli/SKILL.md +109 -0
- mammoth_cli/bundled_skill/mammoth-cli/references/auth.md +27 -0
- mammoth_cli/bundled_skill/mammoth-cli/references/input.md +24 -0
- mammoth_cli/bundled_skill/mammoth-cli/references/jobs-drafts.md +35 -0
- mammoth_cli/bundled_skill/mammoth-cli/references/machine-output.md +30 -0
- mammoth_cli/bundled_skill/mammoth-cli/references/recovery.md +28 -0
- mammoth_cli/bundled_skill/mammoth-cli/references/safety.md +26 -0
- mammoth_cli/commands/__init__.py +29 -0
- mammoth_cli/commands/activity.py +123 -0
- mammoth_cli/commands/addon.py +173 -0
- mammoth_cli/commands/agent.py +199 -0
- mammoth_cli/commands/ai.py +274 -0
- mammoth_cli/commands/annotation.py +166 -0
- mammoth_cli/commands/auth.py +567 -0
- mammoth_cli/commands/automation.py +245 -0
- mammoth_cli/commands/batch.py +204 -0
- mammoth_cli/commands/billing.py +462 -0
- mammoth_cli/commands/browse.py +151 -0
- mammoth_cli/commands/capability.py +43 -0
- mammoth_cli/commands/client_app.py +233 -0
- mammoth_cli/commands/completion.py +82 -0
- mammoth_cli/commands/config.py +309 -0
- mammoth_cli/commands/connector.py +464 -0
- mammoth_cli/commands/context.py +205 -0
- mammoth_cli/commands/dashboard.py +419 -0
- mammoth_cli/commands/data_app.py +239 -0
- mammoth_cli/commands/dataset.py +332 -0
- mammoth_cli/commands/doctor.py +90 -0
- mammoth_cli/commands/external_key.py +163 -0
- mammoth_cli/commands/file.py +243 -0
- mammoth_cli/commands/folder.py +224 -0
- mammoth_cli/commands/job.py +147 -0
- mammoth_cli/commands/notification.py +201 -0
- mammoth_cli/commands/parameter.py +284 -0
- mammoth_cli/commands/project.py +327 -0
- mammoth_cli/commands/registry.py +587 -0
- mammoth_cli/commands/report.py +61 -0
- mammoth_cli/commands/schedule.py +214 -0
- mammoth_cli/commands/schema.py +271 -0
- mammoth_cli/commands/skill.py +59 -0
- mammoth_cli/commands/snippet.py +200 -0
- mammoth_cli/commands/support.py +800 -0
- mammoth_cli/commands/template.py +134 -0
- mammoth_cli/commands/trash.py +115 -0
- mammoth_cli/commands/user.py +190 -0
- mammoth_cli/commands/view.py +1143 -0
- mammoth_cli/commands/view_ops.py +596 -0
- mammoth_cli/commands/webhook.py +235 -0
- mammoth_cli/commands/workflow.py +340 -0
- mammoth_cli/commands/workspace.py +359 -0
- mammoth_cli/context/__init__.py +1 -0
- mammoth_cli/context/credentials.py +223 -0
- mammoth_cli/context/endpoint.py +57 -0
- mammoth_cli/context/profiles.py +370 -0
- mammoth_cli/context/resolver.py +240 -0
- mammoth_cli/contracts/__init__.py +1 -0
- mammoth_cli/contracts/auth.py +32 -0
- mammoth_cli/errors/__init__.py +1 -0
- mammoth_cli/errors/envelope.py +144 -0
- mammoth_cli/manifest/__init__.py +1 -0
- mammoth_cli/manifest/loader.py +96 -0
- mammoth_cli/messages/__init__.py +1 -0
- mammoth_cli/output/__init__.py +1 -0
- mammoth_cli/output/envelope.py +42 -0
- mammoth_cli/output/normalize.py +84 -0
- mammoth_cli/output/policy.py +71 -0
- mammoth_cli/output/render.py +83 -0
- mammoth_cli/py.typed +0 -0
- mammoth_cli/runtime/__init__.py +0 -0
- mammoth_cli/runtime/confirm.py +124 -0
- mammoth_cli/runtime/executor.py +126 -0
- mammoth_cli/runtime/input_loader.py +140 -0
- mammoth_cli/runtime/invocation.py +87 -0
- mammoth_cli/runtime/options.py +172 -0
- mammoth_cli/runtime/session.py +82 -0
- mammoth_cli/runtime/strict.py +240 -0
- mammoth_cli/runtime/validate.py +120 -0
- mammoth_cli/services/__init__.py +1 -0
- mammoth_cli/services/argspec.py +301 -0
- mammoth_cli/services/coerce.py +172 -0
- mammoth_cli/services/conditions.py +85 -0
- mammoth_cli/services/dispatch.py +63 -0
- mammoth_cli/services/factory.py +45 -0
- mammoth_cli/services/input_fields.py +44 -0
- mammoth_cli/services/mapping.py +74 -0
- mammoth_cli/services/openapi_types.py +166 -0
- mammoth_cli/services/positionals.py +408 -0
- mammoth_cli/services/protocol.py +123 -0
- mammoth_cli/services/sdk_service.py +274 -0
- mammoth_cli/services/testing.py +153 -0
- mammoth_cli/services/type_system.py +427 -0
- mammoth_cli/skills/__init__.py +1 -0
- mammoth_cli/skills/installer.py +321 -0
- mammoth_cli/testing.py +55 -0
- mammoth_cli-1.0.0.dist-info/METADATA +92 -0
- mammoth_cli-1.0.0.dist-info/RECORD +150 -0
- mammoth_cli-1.0.0.dist-info/WHEEL +4 -0
- mammoth_cli-1.0.0.dist-info/entry_points.txt +3 -0
- mammoth_cli-1.0.0.dist-info/licenses/LICENSE +23 -0
- spec/manifests/_sdk_introspection.json +1459 -0
- spec/manifests/commands/activity.yaml +72 -0
- spec/manifests/commands/addon.yaml +239 -0
- spec/manifests/commands/agent.yaml +192 -0
- spec/manifests/commands/ai.yaml +152 -0
- spec/manifests/commands/annotation.yaml +191 -0
- spec/manifests/commands/auth.yaml +101 -0
- spec/manifests/commands/automation.yaml +273 -0
- spec/manifests/commands/batch.yaml +262 -0
- spec/manifests/commands/billing.yaml +813 -0
- spec/manifests/commands/browse.yaml +145 -0
- spec/manifests/commands/capability.yaml +74 -0
- spec/manifests/commands/client-app.yaml +193 -0
- spec/manifests/commands/completion.yaml +68 -0
- spec/manifests/commands/config.yaml +134 -0
- spec/manifests/commands/connector.yaml +944 -0
- spec/manifests/commands/context.yaml +101 -0
- spec/manifests/commands/dashboard.yaml +3595 -0
- spec/manifests/commands/data-app.yaml +485 -0
- spec/manifests/commands/dataset.yaml +580 -0
- spec/manifests/commands/doctor.yaml +35 -0
- spec/manifests/commands/external-key.yaml +151 -0
- spec/manifests/commands/file.yaml +341 -0
- spec/manifests/commands/folder.yaml +334 -0
- spec/manifests/commands/job.yaml +151 -0
- spec/manifests/commands/notification.yaml +188 -0
- spec/manifests/commands/parameter.yaml +527 -0
- spec/manifests/commands/project.yaml +669 -0
- spec/manifests/commands/report.yaml +36 -0
- spec/manifests/commands/schedule.yaml +194 -0
- spec/manifests/commands/schema.yaml +74 -0
- spec/manifests/commands/skill.yaml +167 -0
- spec/manifests/commands/snippet.yaml +311 -0
- spec/manifests/commands/support.yaml +1805 -0
- spec/manifests/commands/template.yaml +191 -0
- spec/manifests/commands/trash.yaml +105 -0
- spec/manifests/commands/user.yaml +280 -0
- spec/manifests/commands/version.yaml +35 -0
- spec/manifests/commands/view.yaml +4232 -0
- spec/manifests/commands/webhook.yaml +261 -0
- spec/manifests/commands/workflow.yaml +622 -0
- spec/manifests/commands/workspace.yaml +705 -0
- spec/manifests/openapi-operations.yaml +8853 -0
- spec/manifests/schema-v1.json +212 -0
- spec/manifests/sdk-catalog.source.yaml +4069 -0
- spec/manifests/sdk-methods.yaml +6971 -0
- spec/openapi/metadata.json +10 -0
- spec/openapi/openapi.json +1 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
"""Handlers for the ``dashboard`` command family (workspace-scoped, no project).
|
|
2
|
+
|
|
3
|
+
Dashboards are identified either by an integer ``dashboard_id`` (most commands)
|
|
4
|
+
or by a share ``url`` slug (the ``*-by-url`` commands). None of the backing SDK
|
|
5
|
+
signatures accept a ``project_id``, so handlers never resolve or forward one.
|
|
6
|
+
Every handler dispatches through the generic
|
|
7
|
+
:meth:`~mammoth_cli.services.protocol.MammothService.call` seam to the public
|
|
8
|
+
SDK method named by the command's reviewed manifest ``sdk_symbol``.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from mammoth_cli.errors.envelope import (
|
|
16
|
+
CODE_INVALID_ARGUMENT,
|
|
17
|
+
CODE_MISSING_ARGUMENT,
|
|
18
|
+
CODE_MISSING_FIELD,
|
|
19
|
+
CODE_SDK_SYMBOL_UNRESOLVED,
|
|
20
|
+
EXIT_USAGE,
|
|
21
|
+
CliError,
|
|
22
|
+
)
|
|
23
|
+
from mammoth_cli.manifest.loader import command_by_id
|
|
24
|
+
from mammoth_cli.runtime.confirm import (
|
|
25
|
+
POLICY_CONFIRM_TARGET,
|
|
26
|
+
POLICY_NONE,
|
|
27
|
+
POLICY_PROMPT_OR_YES,
|
|
28
|
+
POLICY_YES_ALWAYS,
|
|
29
|
+
enforce_confirmation,
|
|
30
|
+
)
|
|
31
|
+
from mammoth_cli.runtime.invocation import Invocation
|
|
32
|
+
from mammoth_cli.runtime.session import open_service
|
|
33
|
+
from mammoth_cli.services.argspec import arg_spec
|
|
34
|
+
|
|
35
|
+
HandlerResult = tuple[Any, dict[str, Any]]
|
|
36
|
+
|
|
37
|
+
# Wait policies for which a job-shaped dashboard response must be resolved to
|
|
38
|
+
# its completed result (honoring --job-timeout) rather than returned raw. The
|
|
39
|
+
# dashboard SDK methods -- generated and handwritten alike -- never wait
|
|
40
|
+
# internally, so the CLI handler is the only place this can happen.
|
|
41
|
+
_JOB_WAIT_POLICIES = frozenset({"always_wait", "start_or_wait"})
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _resolve_job(service: Any, invocation: Invocation, data: Any) -> Any:
|
|
45
|
+
"""Wait for a job-shaped response when the command's wait policy opts in.
|
|
46
|
+
|
|
47
|
+
``wait_if_job`` is a no-op for payloads that are not a recognized job
|
|
48
|
+
reference, so this is safe to apply to any dashboard response: it blocks
|
|
49
|
+
(honoring ``--job-timeout``) only when the reviewed ``wait_policy`` is
|
|
50
|
+
wait-capable *and* the server actually returned a job handle.
|
|
51
|
+
"""
|
|
52
|
+
record = command_by_id(invocation.command_id)
|
|
53
|
+
if record is None or record.get("wait_policy") not in _JOB_WAIT_POLICIES:
|
|
54
|
+
return data
|
|
55
|
+
if hasattr(data, "model_dump"):
|
|
56
|
+
data = data.model_dump(mode="json")
|
|
57
|
+
return service.wait_if_job(data)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _symbol(invocation: Invocation) -> str:
|
|
61
|
+
"""Return the reviewed backing SDK symbol for this command."""
|
|
62
|
+
record = command_by_id(invocation.command_id)
|
|
63
|
+
if record is None or not record.get("sdk_symbol"):
|
|
64
|
+
raise CliError(
|
|
65
|
+
code=CODE_SDK_SYMBOL_UNRESOLVED,
|
|
66
|
+
message=f"No SDK symbol is recorded for '{invocation.command_id}'.",
|
|
67
|
+
exit_status=EXIT_USAGE,
|
|
68
|
+
)
|
|
69
|
+
return str(record["sdk_symbol"])
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _string_positional(invocation: Invocation) -> str | None:
|
|
73
|
+
"""Return the first positional argument, or None if absent."""
|
|
74
|
+
return invocation.extra_args[0] if invocation.extra_args else None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _require_str_positional(invocation: Invocation, name: str) -> str:
|
|
78
|
+
"""Return the first positional argument, or raise ``missing_argument``."""
|
|
79
|
+
value = _string_positional(invocation)
|
|
80
|
+
if value is None:
|
|
81
|
+
raise CliError(
|
|
82
|
+
code=CODE_MISSING_ARGUMENT,
|
|
83
|
+
message=f"This command requires a {name} argument.",
|
|
84
|
+
exit_status=EXIT_USAGE,
|
|
85
|
+
hint=f"Pass the {name} as a positional argument.",
|
|
86
|
+
)
|
|
87
|
+
return value
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _int_positional(invocation: Invocation, name: str) -> int | None:
|
|
91
|
+
"""Parse the first positional argument as an int, or return None if absent."""
|
|
92
|
+
if not invocation.extra_args:
|
|
93
|
+
return None
|
|
94
|
+
raw = invocation.extra_args[0]
|
|
95
|
+
try:
|
|
96
|
+
return int(raw)
|
|
97
|
+
except ValueError as exc:
|
|
98
|
+
raise CliError(
|
|
99
|
+
code=CODE_INVALID_ARGUMENT,
|
|
100
|
+
message=f"The {name} argument '{raw}' is not an integer.",
|
|
101
|
+
exit_status=EXIT_USAGE,
|
|
102
|
+
) from exc
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _require_int_positional(invocation: Invocation, name: str) -> int:
|
|
106
|
+
"""Parse the first positional argument as an int, or raise ``missing_argument``."""
|
|
107
|
+
value = _int_positional(invocation, name)
|
|
108
|
+
if value is None:
|
|
109
|
+
raise CliError(
|
|
110
|
+
code=CODE_MISSING_ARGUMENT,
|
|
111
|
+
message=f"This command requires a {name} argument.",
|
|
112
|
+
exit_status=EXIT_USAGE,
|
|
113
|
+
hint=f"Pass the {name} as a positional argument.",
|
|
114
|
+
)
|
|
115
|
+
return value
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _require_int_positional_at(invocation: Invocation, name: str, index: int) -> int:
|
|
119
|
+
"""Parse the positional at ``index`` as an integer."""
|
|
120
|
+
if len(invocation.extra_args) <= index:
|
|
121
|
+
raise CliError(
|
|
122
|
+
code=CODE_MISSING_ARGUMENT,
|
|
123
|
+
message=f"This command requires a {name} argument.",
|
|
124
|
+
exit_status=EXIT_USAGE,
|
|
125
|
+
hint=f"Pass the {name} as a positional argument.",
|
|
126
|
+
)
|
|
127
|
+
raw = invocation.extra_args[index]
|
|
128
|
+
try:
|
|
129
|
+
return int(raw)
|
|
130
|
+
except ValueError as exc:
|
|
131
|
+
raise CliError(
|
|
132
|
+
code=CODE_INVALID_ARGUMENT,
|
|
133
|
+
message=f"The {name} argument '{raw}' is not an integer.",
|
|
134
|
+
exit_status=EXIT_USAGE,
|
|
135
|
+
) from exc
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _require_field(document: dict[str, Any] | None, field: str) -> Any:
|
|
139
|
+
"""Return a required field from the ``--input`` document, or raise usage."""
|
|
140
|
+
if document is None or field not in document:
|
|
141
|
+
raise CliError(
|
|
142
|
+
code=CODE_MISSING_FIELD,
|
|
143
|
+
message=f"This command requires the '{field}' input field.",
|
|
144
|
+
exit_status=EXIT_USAGE,
|
|
145
|
+
hint=f"Pass it via --input, for example: --input '{{\"{field}\": ...}}'.",
|
|
146
|
+
)
|
|
147
|
+
return document[field]
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _forward_optional(
|
|
151
|
+
document: dict[str, Any], kwargs: dict[str, Any], fields: tuple[str, ...]
|
|
152
|
+
) -> None:
|
|
153
|
+
"""Copy any of ``fields`` present in ``document`` into ``kwargs``."""
|
|
154
|
+
for field in fields:
|
|
155
|
+
if field in document:
|
|
156
|
+
kwargs[field] = document[field]
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _meta(invocation: Invocation, workspace_id: int) -> dict[str, Any]:
|
|
160
|
+
"""Build the common envelope metadata for a dashboard command (no project scope)."""
|
|
161
|
+
return {
|
|
162
|
+
"profile": invocation.profile,
|
|
163
|
+
"workspace_id": workspace_id,
|
|
164
|
+
"project_id": None,
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def dashboard_list(invocation: Invocation) -> HandlerResult:
|
|
169
|
+
"""List dashboards in the active workspace."""
|
|
170
|
+
kwargs = {"project_id": invocation.project} if invocation.project is not None else {}
|
|
171
|
+
with open_service(invocation) as (service, auth):
|
|
172
|
+
data = service.call(_symbol(invocation), **kwargs)
|
|
173
|
+
return data, _meta(invocation, auth.workspace_id)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def dashboard_get(invocation: Invocation) -> HandlerResult:
|
|
177
|
+
"""Get one dashboard by id."""
|
|
178
|
+
dashboard_id = _require_int_positional(invocation, "dashboard id")
|
|
179
|
+
with open_service(invocation) as (service, auth):
|
|
180
|
+
data = service.call(_symbol(invocation), dashboard_id=dashboard_id)
|
|
181
|
+
return data, _meta(invocation, auth.workspace_id)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def dashboard_get_by_url(invocation: Invocation) -> HandlerResult:
|
|
185
|
+
"""Get one dashboard by its share url slug."""
|
|
186
|
+
url = _require_str_positional(invocation, "url")
|
|
187
|
+
with open_service(invocation) as (service, auth):
|
|
188
|
+
data = service.call(_symbol(invocation), url=url)
|
|
189
|
+
return data, _meta(invocation, auth.workspace_id)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def dashboard_analytics(invocation: Invocation) -> HandlerResult:
|
|
193
|
+
"""Get analytics for one dashboard by id."""
|
|
194
|
+
dashboard_id = _require_int_positional(invocation, "dashboard id")
|
|
195
|
+
with open_service(invocation) as (service, auth):
|
|
196
|
+
data = service.call(_symbol(invocation), dashboard_id=dashboard_id)
|
|
197
|
+
return data, _meta(invocation, auth.workspace_id)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def dashboard_data_draft(invocation: Invocation) -> HandlerResult:
|
|
201
|
+
"""Run a SQL query against a dashboard's draft data. ``sql`` comes from ``--input``."""
|
|
202
|
+
dashboard_id = _require_int_positional(invocation, "dashboard id")
|
|
203
|
+
document = invocation.load_input()
|
|
204
|
+
sql = _require_field(document, "sql")
|
|
205
|
+
with open_service(invocation) as (service, auth):
|
|
206
|
+
data = service.call(_symbol(invocation), dashboard_id=dashboard_id, sql=sql)
|
|
207
|
+
data = _resolve_job(service, invocation, data)
|
|
208
|
+
return data, _meta(invocation, auth.workspace_id)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def dashboard_data_published(invocation: Invocation) -> HandlerResult:
|
|
212
|
+
"""Run a SQL query against a dashboard's published data. ``sql`` from ``--input``."""
|
|
213
|
+
dashboard_id = _require_int_positional(invocation, "dashboard id")
|
|
214
|
+
document = invocation.load_input()
|
|
215
|
+
sql = _require_field(document, "sql")
|
|
216
|
+
with open_service(invocation) as (service, auth):
|
|
217
|
+
data = service.call(_symbol(invocation), dashboard_id=dashboard_id, sql=sql)
|
|
218
|
+
data = _resolve_job(service, invocation, data)
|
|
219
|
+
return data, _meta(invocation, auth.workspace_id)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def dashboard_job_by_url(invocation: Invocation) -> HandlerResult:
|
|
223
|
+
"""Get a job's status for a dashboard by URL and job-id positionals."""
|
|
224
|
+
url = _require_str_positional(invocation, "url")
|
|
225
|
+
job_id = _require_int_positional_at(invocation, "job id", 1)
|
|
226
|
+
with open_service(invocation) as (service, auth):
|
|
227
|
+
data = service.call(_symbol(invocation), url=url, job_id=job_id)
|
|
228
|
+
return data, _meta(invocation, auth.workspace_id)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def dashboard_published_data_by_url(invocation: Invocation) -> HandlerResult:
|
|
232
|
+
"""Run a published-data request for a dashboard by url. ``body`` from ``--input``."""
|
|
233
|
+
url = _require_str_positional(invocation, "url")
|
|
234
|
+
document = invocation.load_input()
|
|
235
|
+
body = _require_field(document, "body")
|
|
236
|
+
with open_service(invocation) as (service, auth):
|
|
237
|
+
data = service.call(_symbol(invocation), url=url, body=body)
|
|
238
|
+
data = _resolve_job(service, invocation, data)
|
|
239
|
+
return data, _meta(invocation, auth.workspace_id)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def dashboard_widget_data(invocation: Invocation) -> HandlerResult:
|
|
243
|
+
"""Run a widget-data request for a dashboard by id. ``body`` from ``--input``."""
|
|
244
|
+
dashboard_id = _require_int_positional(invocation, "dashboard id")
|
|
245
|
+
document = invocation.load_input()
|
|
246
|
+
body = _require_field(document, "body")
|
|
247
|
+
with open_service(invocation) as (service, auth):
|
|
248
|
+
data = service.call(_symbol(invocation), dashboard_id=dashboard_id, body=body)
|
|
249
|
+
data = _resolve_job(service, invocation, data)
|
|
250
|
+
return data, _meta(invocation, auth.workspace_id)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def dashboard_widget_data_by_url(invocation: Invocation) -> HandlerResult:
|
|
254
|
+
"""Run a widget-data request for a dashboard by url. ``body`` from ``--input``."""
|
|
255
|
+
url = _require_str_positional(invocation, "url")
|
|
256
|
+
document = invocation.load_input()
|
|
257
|
+
body = _require_field(document, "body")
|
|
258
|
+
with open_service(invocation) as (service, auth):
|
|
259
|
+
data = service.call(_symbol(invocation), url=url, body=body)
|
|
260
|
+
data = _resolve_job(service, invocation, data)
|
|
261
|
+
return data, _meta(invocation, auth.workspace_id)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def dashboard_source_list(invocation: Invocation) -> HandlerResult:
|
|
265
|
+
"""List dashboard sources available in the active workspace."""
|
|
266
|
+
with open_service(invocation) as (service, auth):
|
|
267
|
+
data = service.call(_symbol(invocation))
|
|
268
|
+
return data, _meta(invocation, auth.workspace_id)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def dashboard_create(invocation: Invocation) -> HandlerResult:
|
|
272
|
+
"""Create a dashboard. Intent comes from a positional or the ``intent`` field."""
|
|
273
|
+
document = invocation.load_input() or {}
|
|
274
|
+
intent = _string_positional(invocation) or document.get("intent")
|
|
275
|
+
if not intent:
|
|
276
|
+
raise CliError(
|
|
277
|
+
code=CODE_MISSING_ARGUMENT,
|
|
278
|
+
message="A dashboard intent is required.",
|
|
279
|
+
exit_status=EXIT_USAGE,
|
|
280
|
+
hint="Pass the intent as a positional argument or an 'intent' input field.",
|
|
281
|
+
)
|
|
282
|
+
source = _require_field(document, "source")
|
|
283
|
+
kwargs: dict[str, Any] = {"intent": intent, "source": source}
|
|
284
|
+
_forward_optional(document, kwargs, ("enable_filters", "enable_pages"))
|
|
285
|
+
with open_service(invocation) as (service, auth):
|
|
286
|
+
data = service.call(_symbol(invocation), **kwargs)
|
|
287
|
+
return data, _meta(invocation, auth.workspace_id)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def dashboard_update(invocation: Invocation) -> HandlerResult:
|
|
291
|
+
"""Apply a JSON Patch to one dashboard. Dashboard id is positional."""
|
|
292
|
+
dashboard_id = _require_int_positional(invocation, "dashboard id")
|
|
293
|
+
document = invocation.load_input()
|
|
294
|
+
patch = _require_field(document, "patch")
|
|
295
|
+
with open_service(invocation) as (service, auth):
|
|
296
|
+
data = service.call(_symbol(invocation), dashboard_id=dashboard_id, patch=patch)
|
|
297
|
+
return data, _meta(invocation, auth.workspace_id)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def dashboard_action(invocation: Invocation) -> HandlerResult:
|
|
301
|
+
"""Apply a lifecycle action to a dashboard. Always requires ``--yes``."""
|
|
302
|
+
dashboard_id = _require_int_positional(invocation, "dashboard id")
|
|
303
|
+
document = invocation.load_input()
|
|
304
|
+
action = _require_field(document, "action")
|
|
305
|
+
enforce_confirmation(
|
|
306
|
+
invocation,
|
|
307
|
+
policy=POLICY_YES_ALWAYS,
|
|
308
|
+
action=f"apply an action to dashboard {dashboard_id}",
|
|
309
|
+
)
|
|
310
|
+
kwargs: dict[str, Any] = {"dashboard_id": dashboard_id, "action": action}
|
|
311
|
+
assert document is not None
|
|
312
|
+
_forward_optional(document, kwargs, ("params_enabled", "params_view_id"))
|
|
313
|
+
with open_service(invocation) as (service, auth):
|
|
314
|
+
data = service.call(_symbol(invocation), **kwargs)
|
|
315
|
+
return data, _meta(invocation, auth.workspace_id)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def dashboard_share(invocation: Invocation) -> HandlerResult:
|
|
319
|
+
"""Share a dashboard. Always requires ``--yes``."""
|
|
320
|
+
dashboard_id = _require_int_positional(invocation, "dashboard id")
|
|
321
|
+
document = invocation.load_input()
|
|
322
|
+
type_of_auth = _require_field(document, "type_of_auth")
|
|
323
|
+
enforce_confirmation(
|
|
324
|
+
invocation,
|
|
325
|
+
policy=POLICY_YES_ALWAYS,
|
|
326
|
+
action=f"share dashboard {dashboard_id}",
|
|
327
|
+
)
|
|
328
|
+
kwargs: dict[str, Any] = {"dashboard_id": dashboard_id, "type_of_auth": type_of_auth}
|
|
329
|
+
assert document is not None
|
|
330
|
+
_forward_optional(document, kwargs, ("users",))
|
|
331
|
+
with open_service(invocation) as (service, auth):
|
|
332
|
+
data = service.call(_symbol(invocation), **kwargs)
|
|
333
|
+
return data, _meta(invocation, auth.workspace_id)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def dashboard_cancel_generation(invocation: Invocation) -> HandlerResult:
|
|
337
|
+
"""Cancel an in-progress dashboard generation."""
|
|
338
|
+
dashboard_id = _require_int_positional(invocation, "dashboard id")
|
|
339
|
+
with open_service(invocation) as (service, auth):
|
|
340
|
+
data = service.call(_symbol(invocation), dashboard_id=dashboard_id)
|
|
341
|
+
return data, _meta(invocation, auth.workspace_id)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def dashboard_restore(invocation: Invocation) -> HandlerResult:
|
|
345
|
+
"""Restore a trashed dashboard."""
|
|
346
|
+
dashboard_id = _require_int_positional(invocation, "dashboard id")
|
|
347
|
+
with open_service(invocation) as (service, auth):
|
|
348
|
+
data = service.call(_symbol(invocation), dashboard_id=dashboard_id)
|
|
349
|
+
data = _resolve_job(service, invocation, data)
|
|
350
|
+
return data, _meta(invocation, auth.workspace_id)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def dashboard_trash(invocation: Invocation) -> HandlerResult:
|
|
354
|
+
"""Move one dashboard to the trash (reversible)."""
|
|
355
|
+
dashboard_id = _require_int_positional(invocation, "dashboard id")
|
|
356
|
+
with open_service(invocation) as (service, auth):
|
|
357
|
+
data = service.call(_symbol(invocation), dashboard_id=dashboard_id)
|
|
358
|
+
data = _resolve_job(service, invocation, data)
|
|
359
|
+
return data, _meta(invocation, auth.workspace_id)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def dashboard_delete(invocation: Invocation) -> HandlerResult:
|
|
363
|
+
"""Permanently delete one dashboard by id. Prompt or ``--yes`` required."""
|
|
364
|
+
dashboard_id = _require_int_positional(invocation, "dashboard id")
|
|
365
|
+
enforce_confirmation(
|
|
366
|
+
invocation, policy=POLICY_PROMPT_OR_YES, action=f"delete dashboard {dashboard_id}"
|
|
367
|
+
)
|
|
368
|
+
with open_service(invocation) as (service, auth):
|
|
369
|
+
data = service.call(_symbol(invocation), dashboard_id=dashboard_id)
|
|
370
|
+
return data, _meta(invocation, auth.workspace_id)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def generated_dashboard(invocation: Invocation) -> HandlerResult:
|
|
374
|
+
"""Dispatch a generated dashboard command through its reviewed manifest.
|
|
375
|
+
|
|
376
|
+
Positionals have already been typed by the shared positional resolver and
|
|
377
|
+
structured input is recursively validated by :meth:`Invocation.load_input`.
|
|
378
|
+
The manifest remains authoritative for both the backing SDK method and the
|
|
379
|
+
mutation confirmation policy.
|
|
380
|
+
"""
|
|
381
|
+
record = command_by_id(invocation.command_id)
|
|
382
|
+
if record is None:
|
|
383
|
+
raise CliError(
|
|
384
|
+
code=CODE_SDK_SYMBOL_UNRESOLVED,
|
|
385
|
+
message=f"No command manifest exists for '{invocation.command_id}'.",
|
|
386
|
+
exit_status=EXIT_USAGE,
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
document = invocation.load_input() or {}
|
|
390
|
+
kwargs = dict(document)
|
|
391
|
+
kwargs.update(invocation.positionals)
|
|
392
|
+
|
|
393
|
+
spec = arg_spec(_symbol(invocation))
|
|
394
|
+
if spec is not None:
|
|
395
|
+
missing = [
|
|
396
|
+
field.name for field in spec.fields if field.required and field.name not in kwargs
|
|
397
|
+
]
|
|
398
|
+
if missing:
|
|
399
|
+
field = missing[0]
|
|
400
|
+
raise CliError(
|
|
401
|
+
code=CODE_MISSING_FIELD,
|
|
402
|
+
message=f"This command requires the '{field}' input field.",
|
|
403
|
+
exit_status=EXIT_USAGE,
|
|
404
|
+
hint=f"Pass it via --input, for example: --input '{{\"{field}\": ...}}'.",
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
policy = str(record.get("confirmation") or POLICY_NONE)
|
|
408
|
+
target = next((str(value) for value in invocation.positionals.values()), None)
|
|
409
|
+
enforce_confirmation(
|
|
410
|
+
invocation,
|
|
411
|
+
policy=policy,
|
|
412
|
+
action=invocation.command_id.replace(".", " "),
|
|
413
|
+
target=target if policy == POLICY_CONFIRM_TARGET else None,
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
with open_service(invocation) as (service, auth):
|
|
417
|
+
data = service.call(_symbol(invocation), **kwargs)
|
|
418
|
+
data = _resolve_job(service, invocation, data)
|
|
419
|
+
return data, _meta(invocation, auth.workspace_id)
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""Handlers for the ``data-app`` command family.
|
|
2
|
+
|
|
3
|
+
Data apps are standalone resources: no reviewed manifest signature in this
|
|
4
|
+
family takes a ``project_id``, and ``list`` is the only command that accepts a
|
|
5
|
+
``workspace_id`` at all — and per the SDK client convention it is never
|
|
6
|
+
forwarded explicitly, since the authenticated client already carries the
|
|
7
|
+
workspace. The active project (if any) is still reported in the envelope
|
|
8
|
+
metadata for context, mirroring the read handlers in
|
|
9
|
+
:mod:`mammoth_cli.commands.file`. Every ``data_app_id`` comes from the first
|
|
10
|
+
CLI positional argument; a nested per-app identifier (a job id or a user's
|
|
11
|
+
email) comes from the second. Handlers dispatch through the generic
|
|
12
|
+
:meth:`~mammoth_cli.services.protocol.MammothService.call` seam to the public
|
|
13
|
+
SDK method named by the command's reviewed manifest ``sdk_symbol``.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from mammoth_cli.errors.envelope import (
|
|
21
|
+
CODE_INVALID_ARGUMENT,
|
|
22
|
+
CODE_MISSING_ARGUMENT,
|
|
23
|
+
CODE_MISSING_FIELD,
|
|
24
|
+
CODE_SDK_SYMBOL_UNRESOLVED,
|
|
25
|
+
EXIT_USAGE,
|
|
26
|
+
CliError,
|
|
27
|
+
)
|
|
28
|
+
from mammoth_cli.manifest.loader import command_by_id
|
|
29
|
+
from mammoth_cli.runtime.confirm import POLICY_PROMPT_OR_YES, enforce_confirmation
|
|
30
|
+
from mammoth_cli.runtime.invocation import Invocation
|
|
31
|
+
from mammoth_cli.runtime.session import open_service, resolved_project
|
|
32
|
+
|
|
33
|
+
HandlerResult = tuple[Any, dict[str, Any]]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _symbol(invocation: Invocation) -> str:
|
|
37
|
+
"""Return the reviewed backing SDK symbol for this command."""
|
|
38
|
+
record = command_by_id(invocation.command_id)
|
|
39
|
+
if record is None or not record.get("sdk_symbol"):
|
|
40
|
+
raise CliError(
|
|
41
|
+
code=CODE_SDK_SYMBOL_UNRESOLVED,
|
|
42
|
+
message=f"No SDK symbol is recorded for '{invocation.command_id}'.",
|
|
43
|
+
exit_status=EXIT_USAGE,
|
|
44
|
+
)
|
|
45
|
+
return str(record["sdk_symbol"])
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _int_positional_at(invocation: Invocation, index: int, name: str) -> int | None:
|
|
49
|
+
"""Parse the positional argument at ``index`` as an int, or return None."""
|
|
50
|
+
if len(invocation.extra_args) <= index:
|
|
51
|
+
return None
|
|
52
|
+
raw = invocation.extra_args[index]
|
|
53
|
+
try:
|
|
54
|
+
return int(raw)
|
|
55
|
+
except ValueError as exc:
|
|
56
|
+
raise CliError(
|
|
57
|
+
code=CODE_INVALID_ARGUMENT,
|
|
58
|
+
message=f"The {name} argument '{raw}' is not an integer.",
|
|
59
|
+
exit_status=EXIT_USAGE,
|
|
60
|
+
) from exc
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _require_int_positional_at(invocation: Invocation, index: int, name: str) -> int:
|
|
64
|
+
"""Return the required positional argument at ``index`` as an int, or raise."""
|
|
65
|
+
value = _int_positional_at(invocation, index, name)
|
|
66
|
+
if value is None:
|
|
67
|
+
raise CliError(
|
|
68
|
+
code=CODE_MISSING_ARGUMENT,
|
|
69
|
+
message=f"This command requires a {name} argument.",
|
|
70
|
+
exit_status=EXIT_USAGE,
|
|
71
|
+
hint=f"Pass the {name} as a positional argument.",
|
|
72
|
+
)
|
|
73
|
+
return value
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _require_int_positional(invocation: Invocation, name: str) -> int:
|
|
77
|
+
"""Return the required first positional argument as an int, or raise."""
|
|
78
|
+
return _require_int_positional_at(invocation, 0, name)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _require_string_positional_at(invocation: Invocation, index: int, name: str) -> str:
|
|
82
|
+
"""Return the required positional argument at ``index``, or raise usage."""
|
|
83
|
+
if len(invocation.extra_args) <= index:
|
|
84
|
+
raise CliError(
|
|
85
|
+
code=CODE_MISSING_ARGUMENT,
|
|
86
|
+
message=f"This command requires a {name} argument.",
|
|
87
|
+
exit_status=EXIT_USAGE,
|
|
88
|
+
hint=f"Pass the {name} as a positional argument.",
|
|
89
|
+
)
|
|
90
|
+
return invocation.extra_args[index]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _require_field(document: dict[str, Any] | None, field: str) -> Any:
|
|
94
|
+
"""Return a required field from the ``--input`` document, or raise usage."""
|
|
95
|
+
if document is None or field not in document:
|
|
96
|
+
raise CliError(
|
|
97
|
+
code=CODE_MISSING_FIELD,
|
|
98
|
+
message=f"This command requires the '{field}' input field.",
|
|
99
|
+
exit_status=EXIT_USAGE,
|
|
100
|
+
hint=f"Pass it via --input, for example: --input '{{\"{field}\": ...}}'.",
|
|
101
|
+
)
|
|
102
|
+
return document[field]
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _forward_optional(
|
|
106
|
+
document: dict[str, Any], kwargs: dict[str, Any], fields: tuple[str, ...]
|
|
107
|
+
) -> None:
|
|
108
|
+
"""Copy each of ``fields`` present in ``document`` into ``kwargs``, unchanged."""
|
|
109
|
+
for field in fields:
|
|
110
|
+
if field in document:
|
|
111
|
+
kwargs[field] = document[field]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _meta(invocation: Invocation, workspace_id: int, project_id: int | None) -> dict[str, Any]:
|
|
115
|
+
"""Build the common envelope metadata for a data-app command."""
|
|
116
|
+
return {
|
|
117
|
+
"profile": invocation.profile,
|
|
118
|
+
"workspace_id": workspace_id,
|
|
119
|
+
"project_id": project_id,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def data_app_list(invocation: Invocation) -> HandlerResult:
|
|
124
|
+
"""List data apps. The workspace is always the authenticated client's own."""
|
|
125
|
+
with open_service(invocation) as (service, auth):
|
|
126
|
+
data = service.call(_symbol(invocation))
|
|
127
|
+
return data, _meta(invocation, auth.workspace_id, resolved_project(invocation))
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def data_app_get(invocation: Invocation) -> HandlerResult:
|
|
131
|
+
"""Get one data app by id."""
|
|
132
|
+
data_app_id = _require_int_positional(invocation, "data app id")
|
|
133
|
+
with open_service(invocation) as (service, auth):
|
|
134
|
+
data = service.call(_symbol(invocation), data_app_id=data_app_id)
|
|
135
|
+
return data, _meta(invocation, auth.workspace_id, resolved_project(invocation))
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def data_app_create(invocation: Invocation) -> HandlerResult:
|
|
139
|
+
"""Create a data app. The creation payload is the required ``body`` field."""
|
|
140
|
+
document = invocation.load_input()
|
|
141
|
+
body = _require_field(document, "body")
|
|
142
|
+
with open_service(invocation) as (service, auth):
|
|
143
|
+
data = service.call(_symbol(invocation), body=body)
|
|
144
|
+
return data, _meta(invocation, auth.workspace_id, resolved_project(invocation))
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def data_app_update(invocation: Invocation) -> HandlerResult:
|
|
148
|
+
"""Update a data app's settings. Data app id is positional; body is required input."""
|
|
149
|
+
data_app_id = _require_int_positional(invocation, "data app id")
|
|
150
|
+
document = invocation.load_input()
|
|
151
|
+
body = _require_field(document, "body")
|
|
152
|
+
with open_service(invocation) as (service, auth):
|
|
153
|
+
data = service.call(_symbol(invocation), data_app_id=data_app_id, body=body)
|
|
154
|
+
return data, _meta(invocation, auth.workspace_id, resolved_project(invocation))
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def data_app_delete(invocation: Invocation) -> HandlerResult:
|
|
158
|
+
"""Permanently delete one data app by id. Prompt or ``--yes`` required."""
|
|
159
|
+
data_app_id = _require_int_positional(invocation, "data app id")
|
|
160
|
+
enforce_confirmation(
|
|
161
|
+
invocation, policy=POLICY_PROMPT_OR_YES, action=f"delete data app {data_app_id}"
|
|
162
|
+
)
|
|
163
|
+
with open_service(invocation) as (service, auth):
|
|
164
|
+
data = service.call(_symbol(invocation), data_app_id=data_app_id)
|
|
165
|
+
return data, _meta(invocation, auth.workspace_id, resolved_project(invocation))
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def data_app_active_job(invocation: Invocation) -> HandlerResult:
|
|
169
|
+
"""Get the currently active job for a data app, if any."""
|
|
170
|
+
data_app_id = _require_int_positional(invocation, "data app id")
|
|
171
|
+
with open_service(invocation) as (service, auth):
|
|
172
|
+
data = service.call(_symbol(invocation), data_app_id=data_app_id)
|
|
173
|
+
return data, _meta(invocation, auth.workspace_id, resolved_project(invocation))
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def data_app_job(invocation: Invocation) -> HandlerResult:
|
|
177
|
+
"""Get one job for a data app. Data app id and job id are both positional."""
|
|
178
|
+
data_app_id = _require_int_positional_at(invocation, 0, "data app id")
|
|
179
|
+
job_id = _require_int_positional_at(invocation, 1, "job id")
|
|
180
|
+
with open_service(invocation) as (service, auth):
|
|
181
|
+
data = service.call(_symbol(invocation), data_app_id=data_app_id, job_id=job_id)
|
|
182
|
+
return data, _meta(invocation, auth.workspace_id, resolved_project(invocation))
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def data_app_pipeline_changes(invocation: Invocation) -> HandlerResult:
|
|
186
|
+
"""Get pending pipeline changes for a data app's source dataview."""
|
|
187
|
+
data_app_id = _require_int_positional(invocation, "data app id")
|
|
188
|
+
with open_service(invocation) as (service, auth):
|
|
189
|
+
data = service.call(_symbol(invocation), data_app_id=data_app_id)
|
|
190
|
+
return data, _meta(invocation, auth.workspace_id, resolved_project(invocation))
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def data_app_share(invocation: Invocation) -> HandlerResult:
|
|
194
|
+
"""Share a data app with a user. Data app id is positional; body is required input."""
|
|
195
|
+
data_app_id = _require_int_positional(invocation, "data app id")
|
|
196
|
+
document = invocation.load_input()
|
|
197
|
+
body = _require_field(document, "body")
|
|
198
|
+
with open_service(invocation) as (service, auth):
|
|
199
|
+
data = service.call(_symbol(invocation), data_app_id=data_app_id, body=body)
|
|
200
|
+
return data, _meta(invocation, auth.workspace_id, resolved_project(invocation))
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def data_app_upload(invocation: Invocation) -> HandlerResult:
|
|
204
|
+
"""Upload a file to a data app. Data app id is positional; file is required input."""
|
|
205
|
+
data_app_id = _require_int_positional(invocation, "data app id")
|
|
206
|
+
document = invocation.load_input()
|
|
207
|
+
file = _require_field(document, "file")
|
|
208
|
+
kwargs: dict[str, Any] = {"data_app_id": data_app_id, "file": file}
|
|
209
|
+
assert document is not None
|
|
210
|
+
_forward_optional(document, kwargs, ("append_to_ds_id",))
|
|
211
|
+
with open_service(invocation) as (service, auth):
|
|
212
|
+
data = service.call(_symbol(invocation), **kwargs)
|
|
213
|
+
return data, _meta(invocation, auth.workspace_id, resolved_project(invocation))
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def data_app_user_list(invocation: Invocation) -> HandlerResult:
|
|
217
|
+
"""List users a data app is shared with."""
|
|
218
|
+
data_app_id = _require_int_positional(invocation, "data app id")
|
|
219
|
+
with open_service(invocation) as (service, auth):
|
|
220
|
+
data = service.call(_symbol(invocation), data_app_id=data_app_id)
|
|
221
|
+
return data, _meta(invocation, auth.workspace_id, resolved_project(invocation))
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def data_app_user_remove(invocation: Invocation) -> HandlerResult:
|
|
225
|
+
"""Remove a shared user from a data app. Prompt or ``--yes`` required.
|
|
226
|
+
|
|
227
|
+
The data app id is the first positional argument; the user's email is the
|
|
228
|
+
second.
|
|
229
|
+
"""
|
|
230
|
+
data_app_id = _require_int_positional_at(invocation, 0, "data app id")
|
|
231
|
+
email = _require_string_positional_at(invocation, 1, "email")
|
|
232
|
+
enforce_confirmation(
|
|
233
|
+
invocation,
|
|
234
|
+
policy=POLICY_PROMPT_OR_YES,
|
|
235
|
+
action=f"remove user {email} from data app {data_app_id}",
|
|
236
|
+
)
|
|
237
|
+
with open_service(invocation) as (service, auth):
|
|
238
|
+
data = service.call(_symbol(invocation), data_app_id=data_app_id, email=email)
|
|
239
|
+
return data, _meta(invocation, auth.workspace_id, resolved_project(invocation))
|