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,408 @@
|
|
|
1
|
+
"""Single code-derived source of truth for a command's positional arguments.
|
|
2
|
+
|
|
3
|
+
A command's positionals are the leading, non-``--input`` tokens it accepts on
|
|
4
|
+
the command line (view ids, dataset ids, project names, and the like). Before
|
|
5
|
+
this module they were invisible to the system: the manifest hardcoded an empty
|
|
6
|
+
list, Typer declared no ``Argument``s, and every handler re-derived its own
|
|
7
|
+
positionals imperatively from ``extra_args``. Five consumers each carried a
|
|
8
|
+
private, drift-prone copy.
|
|
9
|
+
|
|
10
|
+
This module resolves a command's positionals **once**, from the command's
|
|
11
|
+
backing SDK signature (via :mod:`mammoth_cli.services.argspec`) plus a rule keyed
|
|
12
|
+
on the symbol's class, with a small authored override catalog for the cases the
|
|
13
|
+
signature cannot express. Every consumer -- the manifest builder, the runnable
|
|
14
|
+
``agent_example``, the docs generator, the Typer registration, and the handlers
|
|
15
|
+
-- reads the same :func:`resolve_positionals` result, so they cannot disagree.
|
|
16
|
+
|
|
17
|
+
The derivation is intentionally conservative: ``int``/``str`` scalars only, never
|
|
18
|
+
multi-valued, and it emits nothing it cannot justify from the signature. Anything
|
|
19
|
+
ambiguous lives in :data:`POSITIONAL_OVERRIDES`, guarded by the real-code drift
|
|
20
|
+
test rather than trusted.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
from functools import cache
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
from mammoth_cli.manifest.loader import command_by_id
|
|
30
|
+
from mammoth_cli.services.argspec import arg_spec
|
|
31
|
+
|
|
32
|
+
# Field names that identify a resource (and so become a positional locator).
|
|
33
|
+
_IDENTITY_SUFFIXES = ("_id", "_key")
|
|
34
|
+
_IDENTITY_NAMES = ("id", "key", "url")
|
|
35
|
+
|
|
36
|
+
# Fields that are always sourced from a global option or resolved context, never
|
|
37
|
+
# a positional. ``project_id`` comes from ``--project`` or the active project for
|
|
38
|
+
# every command except the ``project`` family (which overrides it back in, since
|
|
39
|
+
# the project *is* the resource those commands act on).
|
|
40
|
+
_SOURCED_ELSEWHERE = frozenset({"project_id"})
|
|
41
|
+
|
|
42
|
+
# Modules whose methods take the dataview as the receiver (``self``); the id is
|
|
43
|
+
# therefore not a signature parameter and must be synthesized.
|
|
44
|
+
_VIEW_METHOD_MODULE = "mammoth.view"
|
|
45
|
+
_MIXIN_MODULE_PREFIX = "mammoth._mixins"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class PositionalSpec:
|
|
50
|
+
"""One positional argument of a CLI command, derived from the SDK signature.
|
|
51
|
+
|
|
52
|
+
Attributes:
|
|
53
|
+
name: snake_case argument name; also the key under which the resolved
|
|
54
|
+
value is stored on :attr:`mammoth_cli.runtime.invocation.Invocation.positionals`.
|
|
55
|
+
type: The scalar the value represents; ``int`` or ``str`` only.
|
|
56
|
+
required: Whether the argument must be supplied on the command line. An
|
|
57
|
+
optional positional may legitimately be absent (filled from
|
|
58
|
+
``--input`` or a resolved context by the handler).
|
|
59
|
+
help: One-line help shown in the Typer Arguments panel and the docs.
|
|
60
|
+
falls_back_to_field: When set, an omitted optional positional is filled
|
|
61
|
+
from this ``--input`` document field by the handler; this is how the
|
|
62
|
+
"positional OR ``--input`` field" commands stay dual-sourced.
|
|
63
|
+
fills_sdk_param: The backing SDK parameter this positional supplies when
|
|
64
|
+
its name differs from that parameter (e.g. the ``project_id``
|
|
65
|
+
positional fills the ``project`` argument of ``ProjectsAPI.get``).
|
|
66
|
+
Such a parameter is positional-sourced, so it must be excluded from
|
|
67
|
+
the accepted ``--input`` fields even though its name never appears in
|
|
68
|
+
the positional list. Defaults to ``name``.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
name: str
|
|
72
|
+
type: type[int] | type[str]
|
|
73
|
+
required: bool
|
|
74
|
+
help: str
|
|
75
|
+
falls_back_to_field: str | None = None
|
|
76
|
+
fills_sdk_param: str | None = None
|
|
77
|
+
example_value: str | None = None
|
|
78
|
+
"""A concrete, resolvable value for the generated runnable example.
|
|
79
|
+
|
|
80
|
+
Left ``None`` for ordinary locators, whose example placeholder is a generic
|
|
81
|
+
``123``/``example`` (never validated at build time). Set only where the
|
|
82
|
+
generated example must actually *run* to exit zero -- the CLI-only discovery
|
|
83
|
+
commands (``schema get``/``capability get``), whose id is looked up against a
|
|
84
|
+
real, offline catalog, so the placeholder must be a genuine command/operation
|
|
85
|
+
id rather than the literal ``example`` (which resolves to nothing)."""
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def metavar(self) -> str:
|
|
89
|
+
"""UPPER_SNAKE placeholder used in help, examples, and docs."""
|
|
90
|
+
return self.name.upper()
|
|
91
|
+
|
|
92
|
+
def as_manifest(self) -> dict[str, Any]:
|
|
93
|
+
"""Serialize to the manifest/schema JSON shape (type as ``'int'``/``'str'``)."""
|
|
94
|
+
return {
|
|
95
|
+
"name": self.name,
|
|
96
|
+
"type": self.type.__name__,
|
|
97
|
+
"required": self.required,
|
|
98
|
+
"metavar": self.metavar,
|
|
99
|
+
"falls_back_to_field": self.falls_back_to_field,
|
|
100
|
+
"help": self.help,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _optional_project_id() -> tuple[PositionalSpec, ...]:
|
|
105
|
+
"""The shared ``project`` family locator: an optional project-id positional.
|
|
106
|
+
|
|
107
|
+
Every ``project`` command that acts on a single project accepts the project
|
|
108
|
+
id as an optional positional, falling back to the active project when
|
|
109
|
+
omitted. That dual-sourcing is invisible to the SDK signature (the id is
|
|
110
|
+
``project``/``project_id`` and looks required), so it is authored here.
|
|
111
|
+
"""
|
|
112
|
+
return (
|
|
113
|
+
PositionalSpec(
|
|
114
|
+
name="project_id",
|
|
115
|
+
type=int,
|
|
116
|
+
required=False,
|
|
117
|
+
help="ID of the project to act on; defaults to the active project.",
|
|
118
|
+
# ``project get`` names this argument ``project`` in the SDK; every
|
|
119
|
+
# other project command names it ``project_id``. Declaring the alias
|
|
120
|
+
# keeps the resource id positional-sourced (never an --input field)
|
|
121
|
+
# regardless of the backing parameter's name.
|
|
122
|
+
fills_sdk_param="project",
|
|
123
|
+
),
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# Commands whose positionals the signature cannot express correctly. Each entry
|
|
128
|
+
# replaces the derivation wholesale; the drift test proves the union is right.
|
|
129
|
+
POSITIONAL_OVERRIDES: dict[str, tuple[PositionalSpec, ...]] = {
|
|
130
|
+
# project family: the project id is an optional positional (falls back to the
|
|
131
|
+
# active project). The SDK signature marks it required or omits it, so the
|
|
132
|
+
# dual-sourced optional locator is authored here.
|
|
133
|
+
"project.get": _optional_project_id(),
|
|
134
|
+
"project.pending-changes": _optional_project_id(),
|
|
135
|
+
"project.resource-status": _optional_project_id(),
|
|
136
|
+
"project.resource-dependencies": _optional_project_id(),
|
|
137
|
+
"project.publish-credentials": _optional_project_id(),
|
|
138
|
+
"project.update": _optional_project_id(),
|
|
139
|
+
"project.delete": _optional_project_id(),
|
|
140
|
+
"project.sample-flow": _optional_project_id(),
|
|
141
|
+
"project.checkpoint.list": _optional_project_id(),
|
|
142
|
+
"project.data-check.list": _optional_project_id(),
|
|
143
|
+
"project.user.add": _optional_project_id(),
|
|
144
|
+
"project.user.remove": _optional_project_id(),
|
|
145
|
+
"project.user.update": _optional_project_id(),
|
|
146
|
+
# project create takes the name as an optional positional or a ``name`` field.
|
|
147
|
+
"project.create": (
|
|
148
|
+
PositionalSpec(
|
|
149
|
+
name="name",
|
|
150
|
+
type=str,
|
|
151
|
+
required=False,
|
|
152
|
+
help="Name of the new project; or pass it via the 'name' input field.",
|
|
153
|
+
falls_back_to_field="name",
|
|
154
|
+
),
|
|
155
|
+
),
|
|
156
|
+
# project commands sourced entirely from ``--input`` or the active workspace.
|
|
157
|
+
"project.list": (),
|
|
158
|
+
"project.bulk-delete": (),
|
|
159
|
+
"project.bulk-update": (),
|
|
160
|
+
# ``data-app user remove`` takes the shared user's email as a required second
|
|
161
|
+
# positional. ``email`` is not an identity-named signature parameter, so the
|
|
162
|
+
# derivation stops after ``data_app_id``; the handler reads it positionally
|
|
163
|
+
# (never from ``--input``), so it is authored here as a required locator.
|
|
164
|
+
"data-app.user.remove": (
|
|
165
|
+
PositionalSpec(
|
|
166
|
+
name="data_app_id",
|
|
167
|
+
type=int,
|
|
168
|
+
required=True,
|
|
169
|
+
help="ID of the data app.",
|
|
170
|
+
),
|
|
171
|
+
PositionalSpec(
|
|
172
|
+
name="email",
|
|
173
|
+
type=str,
|
|
174
|
+
required=True,
|
|
175
|
+
help="Email address of the shared user to remove.",
|
|
176
|
+
),
|
|
177
|
+
),
|
|
178
|
+
# ``ai condition generate`` / ``ai expression generate`` take the dataset id
|
|
179
|
+
# as a positional. The SDK signature leads with the required ``intent`` (a
|
|
180
|
+
# non-identity ``str``), so the derivation stops before ``dataset_id`` and
|
|
181
|
+
# emits nothing -- leaving the command uninvokable (the positional was
|
|
182
|
+
# unregistered) while ``dataset_id`` was still advertised as an --input field
|
|
183
|
+
# the handler never reads. Author the id locator here; ``intent`` (and
|
|
184
|
+
# ``mode`` for the expression variant) remain required --input fields.
|
|
185
|
+
"ai.condition.generate": (
|
|
186
|
+
PositionalSpec(
|
|
187
|
+
name="dataset_id",
|
|
188
|
+
type=int,
|
|
189
|
+
required=True,
|
|
190
|
+
help="ID of the dataset to generate a condition for.",
|
|
191
|
+
),
|
|
192
|
+
),
|
|
193
|
+
"ai.expression.generate": (
|
|
194
|
+
PositionalSpec(
|
|
195
|
+
name="dataset_id",
|
|
196
|
+
type=int,
|
|
197
|
+
required=True,
|
|
198
|
+
help="ID of the dataset to generate an expression for.",
|
|
199
|
+
),
|
|
200
|
+
),
|
|
201
|
+
# ``support`` create/register verbs take their identifying name/email as a
|
|
202
|
+
# positional OR the matching ``--input`` field (handler
|
|
203
|
+
# ``_require_positional_or_field``). Without a registered optional positional
|
|
204
|
+
# the strict validator rejects the positional form outright, so only the
|
|
205
|
+
# ``--input`` form is invokable and the documented dual-sourcing is a half
|
|
206
|
+
# promise. Author an optional locator that falls back to the field -- the
|
|
207
|
+
# same shape as ``project create``.
|
|
208
|
+
**{
|
|
209
|
+
command: (
|
|
210
|
+
PositionalSpec(
|
|
211
|
+
name=field,
|
|
212
|
+
type=str,
|
|
213
|
+
required=False,
|
|
214
|
+
help=f"{label}; or pass it via the '{field}' input field.",
|
|
215
|
+
falls_back_to_field=field,
|
|
216
|
+
),
|
|
217
|
+
)
|
|
218
|
+
for command, field, label in (
|
|
219
|
+
("support.connector.create", "name", "Name of the new connector"),
|
|
220
|
+
("support.connector-profile.create", "name", "Name of the new connector profile"),
|
|
221
|
+
("support.feature.create", "name", "Name of the new feature"),
|
|
222
|
+
("support.feature-profile.create", "name", "Name of the new feature profile"),
|
|
223
|
+
("support.plan.create", "name", "Name of the new plan"),
|
|
224
|
+
("support.workspace.create", "name", "Name of the new workspace"),
|
|
225
|
+
("support.user.register", "email", "Email of the user to register"),
|
|
226
|
+
("support.user.update", "email", "Email of the user to update"),
|
|
227
|
+
)
|
|
228
|
+
},
|
|
229
|
+
# ``schema get`` / ``capability get`` are CLI-only discovery commands with no
|
|
230
|
+
# backing SDK signature (their ``sdk_symbol`` is a meta-reference), so the
|
|
231
|
+
# derivation emits nothing. Their handlers read the single id positionally
|
|
232
|
+
# (``registry._require_arg`` -> ``extra_args[0]``), so without a registered
|
|
233
|
+
# positional the strict validator rejects the id and the documented command
|
|
234
|
+
# is uninvokable. Author the required locator here.
|
|
235
|
+
"schema.get": (
|
|
236
|
+
PositionalSpec(
|
|
237
|
+
name="command_id",
|
|
238
|
+
type=str,
|
|
239
|
+
required=True,
|
|
240
|
+
help="Command id to fetch the schema for (e.g. view.transform.bulk-replace).",
|
|
241
|
+
# The generated example is executed offline against the real command
|
|
242
|
+
# catalog (see the discovery-example subprocess test), so its
|
|
243
|
+
# placeholder must be a genuine, resolvable command id -- the generic
|
|
244
|
+
# ``example`` would resolve to nothing and exit non-zero.
|
|
245
|
+
example_value="view.transform.bulk-replace",
|
|
246
|
+
),
|
|
247
|
+
),
|
|
248
|
+
"capability.get": (
|
|
249
|
+
PositionalSpec(
|
|
250
|
+
name="operation_id",
|
|
251
|
+
type=str,
|
|
252
|
+
required=True,
|
|
253
|
+
help="Operation id to fetch the capability record for (e.g. AddTask).",
|
|
254
|
+
# A genuine, resolvable operation id, for the same reason as above.
|
|
255
|
+
example_value="AddTask",
|
|
256
|
+
),
|
|
257
|
+
),
|
|
258
|
+
# ``folder delete`` takes a single folder id positionally; the handler wraps
|
|
259
|
+
# it into the SDK's ``folder_ids`` list. The signature leads with the
|
|
260
|
+
# required ``folder_ids`` (a ``list[int]``, not a scalar identity), so the
|
|
261
|
+
# derivation emits nothing -- the command was uninvokable and ``folder_ids``
|
|
262
|
+
# was advertised as an --input field the handler ignores. Author the scalar
|
|
263
|
+
# locator and mark it as filling ``folder_ids`` so that SDK parameter is
|
|
264
|
+
# excluded from the advertised --input fields.
|
|
265
|
+
"folder.delete": (
|
|
266
|
+
PositionalSpec(
|
|
267
|
+
name="folder_id",
|
|
268
|
+
type=int,
|
|
269
|
+
required=True,
|
|
270
|
+
help="ID of the folder to delete.",
|
|
271
|
+
fills_sdk_param="folder_ids",
|
|
272
|
+
),
|
|
273
|
+
),
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _module_of(sdk_symbol: str) -> str:
|
|
278
|
+
"""Return the module portion of a ``module.Class.method`` SDK symbol."""
|
|
279
|
+
parts = sdk_symbol.rsplit(".", 2)
|
|
280
|
+
return parts[0] if len(parts) == 3 else sdk_symbol
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _is_view_method(sdk_symbol: str) -> bool:
|
|
284
|
+
"""Whether the symbol is a :class:`~mammoth.view.View` (or mixin) method."""
|
|
285
|
+
module = _module_of(sdk_symbol)
|
|
286
|
+
return module == _VIEW_METHOD_MODULE or module.startswith(_MIXIN_MODULE_PREFIX)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _is_identity_name(name: str) -> bool:
|
|
290
|
+
"""Whether a field name identifies a resource (so becomes a locator)."""
|
|
291
|
+
return name in _IDENTITY_NAMES or name.endswith(_IDENTITY_SUFFIXES)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _identity_type(name: str, annotation: Any) -> type[int] | type[str]:
|
|
295
|
+
"""Return the declared scalar type, falling back only when unresolved."""
|
|
296
|
+
if annotation is int:
|
|
297
|
+
return int
|
|
298
|
+
if annotation is str:
|
|
299
|
+
return str
|
|
300
|
+
if name == "id" or name.endswith("_id"):
|
|
301
|
+
return int
|
|
302
|
+
return str
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _identity_help(name: str) -> str:
|
|
306
|
+
"""Synthesize one-line help for an identity-named locator field."""
|
|
307
|
+
if name == "id":
|
|
308
|
+
return "Identifier of the resource."
|
|
309
|
+
if name == "key":
|
|
310
|
+
return "Key identifying the resource."
|
|
311
|
+
if name == "url":
|
|
312
|
+
return "URL slug identifying the resource."
|
|
313
|
+
if name.endswith("_id"):
|
|
314
|
+
return f"ID of the {name[:-3].replace('_', ' ')}."
|
|
315
|
+
return f"Key identifying the {name[:-4].replace('_', ' ')}."
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def derive_positionals(command_id: str, sdk_symbol: str) -> tuple[PositionalSpec, ...]:
|
|
319
|
+
"""Derive a command's positionals from its backing SDK signature.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
command_id: The manifest command id (used only for diagnostics).
|
|
323
|
+
sdk_symbol: The command's reviewed backing SDK symbol.
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
The derived positional specs, in command-line order. A view-method emits
|
|
327
|
+
a single synthesized ``view_id`` locator (the dataview receiver); every
|
|
328
|
+
other symbol emits the leading contiguous run of required, identity-named
|
|
329
|
+
signature parameters that are not sourced from a global option.
|
|
330
|
+
"""
|
|
331
|
+
if _is_view_method(sdk_symbol):
|
|
332
|
+
return (
|
|
333
|
+
PositionalSpec(
|
|
334
|
+
name="view_id",
|
|
335
|
+
type=int,
|
|
336
|
+
required=True,
|
|
337
|
+
help="ID of the view to act on.",
|
|
338
|
+
),
|
|
339
|
+
)
|
|
340
|
+
spec = arg_spec(sdk_symbol)
|
|
341
|
+
if spec is None:
|
|
342
|
+
return ()
|
|
343
|
+
positionals: list[PositionalSpec] = []
|
|
344
|
+
for field in spec.fields:
|
|
345
|
+
if not field.required:
|
|
346
|
+
break
|
|
347
|
+
if field.name in _SOURCED_ELSEWHERE:
|
|
348
|
+
break
|
|
349
|
+
if not _is_identity_name(field.name):
|
|
350
|
+
break
|
|
351
|
+
positionals.append(
|
|
352
|
+
PositionalSpec(
|
|
353
|
+
name=field.name,
|
|
354
|
+
type=_identity_type(field.name, field.annotation),
|
|
355
|
+
required=True,
|
|
356
|
+
help=_identity_help(field.name),
|
|
357
|
+
)
|
|
358
|
+
)
|
|
359
|
+
return tuple(positionals)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def positionals_for(command_id: str, sdk_symbol: str | None) -> tuple[PositionalSpec, ...]:
|
|
363
|
+
"""Resolve a command's positionals from an explicitly supplied SDK symbol.
|
|
364
|
+
|
|
365
|
+
This is the build-time entry point: :mod:`scripts.build_manifests` already
|
|
366
|
+
holds the reviewed ``command_id`` and ``sdk_symbol`` in scope, so it passes
|
|
367
|
+
them directly rather than reading the (in-progress) manifest.
|
|
368
|
+
|
|
369
|
+
Args:
|
|
370
|
+
command_id: The manifest command id.
|
|
371
|
+
sdk_symbol: The command's reviewed backing SDK symbol, or None.
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
The command's positional specs (an override when catalogued, else the
|
|
375
|
+
signature derivation, else an empty tuple).
|
|
376
|
+
"""
|
|
377
|
+
if command_id in POSITIONAL_OVERRIDES:
|
|
378
|
+
return POSITIONAL_OVERRIDES[command_id]
|
|
379
|
+
if not sdk_symbol:
|
|
380
|
+
return ()
|
|
381
|
+
return derive_positionals(command_id, sdk_symbol)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _symbol_for(command_id: str) -> str | None:
|
|
385
|
+
"""Return a command's backing SDK symbol from the loaded manifest."""
|
|
386
|
+
record = command_by_id(command_id)
|
|
387
|
+
if record is None:
|
|
388
|
+
return None
|
|
389
|
+
symbol = record.get("sdk_symbol")
|
|
390
|
+
return str(symbol) if symbol else None
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
@cache
|
|
394
|
+
def resolve_positionals(command_id: str) -> tuple[PositionalSpec, ...]:
|
|
395
|
+
"""Resolve a command's positionals at runtime, by command id.
|
|
396
|
+
|
|
397
|
+
This is the runtime entry point every non-build consumer uses (Typer
|
|
398
|
+
registration, handlers, the docs generator, the drift test). It reads the
|
|
399
|
+
command's reviewed ``sdk_symbol`` from the loaded manifest, then defers to
|
|
400
|
+
:func:`positionals_for`, so it always agrees with the generated manifest.
|
|
401
|
+
|
|
402
|
+
Args:
|
|
403
|
+
command_id: The manifest command id.
|
|
404
|
+
|
|
405
|
+
Returns:
|
|
406
|
+
The command's positional specs.
|
|
407
|
+
"""
|
|
408
|
+
return positionals_for(command_id, _symbol_for(command_id))
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Typed protocol for the Mammoth service boundary used by CLI commands.
|
|
2
|
+
|
|
3
|
+
Every bespoke command depends on this protocol, never on
|
|
4
|
+
:class:`mammoth.client.MammothClient` directly, so command tests can supply a
|
|
5
|
+
typed fake with no network access. The surface is intentionally minimal; it
|
|
6
|
+
grows as later command families are implemented.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any, Protocol
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MammothService(Protocol):
|
|
15
|
+
"""The typed operations the auth/project command families need."""
|
|
16
|
+
|
|
17
|
+
def call(self, sdk_symbol: str, /, **kwargs: Any) -> Any:
|
|
18
|
+
"""Invoke the public SDK method named by ``sdk_symbol``.
|
|
19
|
+
|
|
20
|
+
This is the generic seam every manifest-driven command uses: the
|
|
21
|
+
command supplies the reviewed ``sdk_symbol`` and validated keyword
|
|
22
|
+
arguments, and the service resolves and calls the matching public
|
|
23
|
+
method, mapping any SDK exception to a stable
|
|
24
|
+
:class:`~mammoth_cli.errors.envelope.CliError`.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
sdk_symbol: The dotted symbol naming the backing public SDK method.
|
|
28
|
+
**kwargs: Keyword arguments forwarded to the method.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
The raw SDK return value (typically a mapping).
|
|
32
|
+
"""
|
|
33
|
+
...
|
|
34
|
+
|
|
35
|
+
def wait_if_job(self, response: Any) -> Any:
|
|
36
|
+
"""Wait for an SDK response containing a recognized async job reference."""
|
|
37
|
+
...
|
|
38
|
+
|
|
39
|
+
def call_view(self, view_id: int, method: str, /, **kwargs: Any) -> Any:
|
|
40
|
+
"""Invoke a public method on a rich View object.
|
|
41
|
+
|
|
42
|
+
Resolves the dataview into a ``View`` (via the public client) and calls
|
|
43
|
+
the named public transformation/draft method. A ``condition`` keyword
|
|
44
|
+
holding a condition spec (mapping or list) is compiled to the SDK's
|
|
45
|
+
condition object inside the service, so command modules never import
|
|
46
|
+
the SDK's condition builder. A non-callable attribute (for example the
|
|
47
|
+
``is_draft_mode`` property) is returned as-is.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
view_id: The dataview id to resolve into a View.
|
|
51
|
+
method: The public View method (or property) name to invoke.
|
|
52
|
+
**kwargs: Keyword arguments forwarded to the method.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
The raw method return value (typically a job/result mapping).
|
|
56
|
+
"""
|
|
57
|
+
...
|
|
58
|
+
|
|
59
|
+
def check_connection(self) -> dict[str, Any]:
|
|
60
|
+
"""Perform a lightweight authenticated call and return its raw result.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
The raw response used as the connection probe.
|
|
64
|
+
"""
|
|
65
|
+
...
|
|
66
|
+
|
|
67
|
+
def list_projects(self, limit: int = 100, offset: int = 0) -> dict[str, Any]:
|
|
68
|
+
"""List projects in the current workspace.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
limit: Maximum number of results.
|
|
72
|
+
offset: Number of leading results to skip.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
The raw project-list response.
|
|
76
|
+
"""
|
|
77
|
+
...
|
|
78
|
+
|
|
79
|
+
def get_project(self, project_id: int) -> dict[str, Any]:
|
|
80
|
+
"""Get one project by id.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
project_id: The project id.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
The raw project response.
|
|
87
|
+
"""
|
|
88
|
+
...
|
|
89
|
+
|
|
90
|
+
def create_project(self, name: str, **kwargs: Any) -> dict[str, Any]:
|
|
91
|
+
"""Create a project.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
name: The project name.
|
|
95
|
+
**kwargs: Additional creation options.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
The raw created-project response.
|
|
99
|
+
"""
|
|
100
|
+
...
|
|
101
|
+
|
|
102
|
+
def delete_project(self, project_id: int) -> dict[str, Any]:
|
|
103
|
+
"""Delete a project.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
project_id: The project id.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
The raw deletion response.
|
|
110
|
+
"""
|
|
111
|
+
...
|
|
112
|
+
|
|
113
|
+
def close(self) -> None:
|
|
114
|
+
"""Release any held resources (for example, the HTTP session)."""
|
|
115
|
+
...
|
|
116
|
+
|
|
117
|
+
def __enter__(self) -> MammothService:
|
|
118
|
+
"""Enter the service as a context manager."""
|
|
119
|
+
...
|
|
120
|
+
|
|
121
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
122
|
+
"""Close the service on context-manager exit."""
|
|
123
|
+
...
|