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.
Files changed (150) hide show
  1. mammoth_cli/__init__.py +14 -0
  2. mammoth_cli/__main__.py +27 -0
  3. mammoth_cli/app.py +565 -0
  4. mammoth_cli/bundled_skill/mammoth-cli/SKILL.md +109 -0
  5. mammoth_cli/bundled_skill/mammoth-cli/references/auth.md +27 -0
  6. mammoth_cli/bundled_skill/mammoth-cli/references/input.md +24 -0
  7. mammoth_cli/bundled_skill/mammoth-cli/references/jobs-drafts.md +35 -0
  8. mammoth_cli/bundled_skill/mammoth-cli/references/machine-output.md +30 -0
  9. mammoth_cli/bundled_skill/mammoth-cli/references/recovery.md +28 -0
  10. mammoth_cli/bundled_skill/mammoth-cli/references/safety.md +26 -0
  11. mammoth_cli/commands/__init__.py +29 -0
  12. mammoth_cli/commands/activity.py +123 -0
  13. mammoth_cli/commands/addon.py +173 -0
  14. mammoth_cli/commands/agent.py +199 -0
  15. mammoth_cli/commands/ai.py +274 -0
  16. mammoth_cli/commands/annotation.py +166 -0
  17. mammoth_cli/commands/auth.py +567 -0
  18. mammoth_cli/commands/automation.py +245 -0
  19. mammoth_cli/commands/batch.py +204 -0
  20. mammoth_cli/commands/billing.py +462 -0
  21. mammoth_cli/commands/browse.py +151 -0
  22. mammoth_cli/commands/capability.py +43 -0
  23. mammoth_cli/commands/client_app.py +233 -0
  24. mammoth_cli/commands/completion.py +82 -0
  25. mammoth_cli/commands/config.py +309 -0
  26. mammoth_cli/commands/connector.py +464 -0
  27. mammoth_cli/commands/context.py +205 -0
  28. mammoth_cli/commands/dashboard.py +419 -0
  29. mammoth_cli/commands/data_app.py +239 -0
  30. mammoth_cli/commands/dataset.py +332 -0
  31. mammoth_cli/commands/doctor.py +90 -0
  32. mammoth_cli/commands/external_key.py +163 -0
  33. mammoth_cli/commands/file.py +243 -0
  34. mammoth_cli/commands/folder.py +224 -0
  35. mammoth_cli/commands/job.py +147 -0
  36. mammoth_cli/commands/notification.py +201 -0
  37. mammoth_cli/commands/parameter.py +284 -0
  38. mammoth_cli/commands/project.py +327 -0
  39. mammoth_cli/commands/registry.py +587 -0
  40. mammoth_cli/commands/report.py +61 -0
  41. mammoth_cli/commands/schedule.py +214 -0
  42. mammoth_cli/commands/schema.py +271 -0
  43. mammoth_cli/commands/skill.py +59 -0
  44. mammoth_cli/commands/snippet.py +200 -0
  45. mammoth_cli/commands/support.py +800 -0
  46. mammoth_cli/commands/template.py +134 -0
  47. mammoth_cli/commands/trash.py +115 -0
  48. mammoth_cli/commands/user.py +190 -0
  49. mammoth_cli/commands/view.py +1143 -0
  50. mammoth_cli/commands/view_ops.py +596 -0
  51. mammoth_cli/commands/webhook.py +235 -0
  52. mammoth_cli/commands/workflow.py +340 -0
  53. mammoth_cli/commands/workspace.py +359 -0
  54. mammoth_cli/context/__init__.py +1 -0
  55. mammoth_cli/context/credentials.py +223 -0
  56. mammoth_cli/context/endpoint.py +57 -0
  57. mammoth_cli/context/profiles.py +370 -0
  58. mammoth_cli/context/resolver.py +240 -0
  59. mammoth_cli/contracts/__init__.py +1 -0
  60. mammoth_cli/contracts/auth.py +32 -0
  61. mammoth_cli/errors/__init__.py +1 -0
  62. mammoth_cli/errors/envelope.py +144 -0
  63. mammoth_cli/manifest/__init__.py +1 -0
  64. mammoth_cli/manifest/loader.py +96 -0
  65. mammoth_cli/messages/__init__.py +1 -0
  66. mammoth_cli/output/__init__.py +1 -0
  67. mammoth_cli/output/envelope.py +42 -0
  68. mammoth_cli/output/normalize.py +84 -0
  69. mammoth_cli/output/policy.py +71 -0
  70. mammoth_cli/output/render.py +83 -0
  71. mammoth_cli/py.typed +0 -0
  72. mammoth_cli/runtime/__init__.py +0 -0
  73. mammoth_cli/runtime/confirm.py +124 -0
  74. mammoth_cli/runtime/executor.py +126 -0
  75. mammoth_cli/runtime/input_loader.py +140 -0
  76. mammoth_cli/runtime/invocation.py +87 -0
  77. mammoth_cli/runtime/options.py +172 -0
  78. mammoth_cli/runtime/session.py +82 -0
  79. mammoth_cli/runtime/strict.py +240 -0
  80. mammoth_cli/runtime/validate.py +120 -0
  81. mammoth_cli/services/__init__.py +1 -0
  82. mammoth_cli/services/argspec.py +301 -0
  83. mammoth_cli/services/coerce.py +172 -0
  84. mammoth_cli/services/conditions.py +85 -0
  85. mammoth_cli/services/dispatch.py +63 -0
  86. mammoth_cli/services/factory.py +45 -0
  87. mammoth_cli/services/input_fields.py +44 -0
  88. mammoth_cli/services/mapping.py +74 -0
  89. mammoth_cli/services/openapi_types.py +166 -0
  90. mammoth_cli/services/positionals.py +408 -0
  91. mammoth_cli/services/protocol.py +123 -0
  92. mammoth_cli/services/sdk_service.py +274 -0
  93. mammoth_cli/services/testing.py +153 -0
  94. mammoth_cli/services/type_system.py +427 -0
  95. mammoth_cli/skills/__init__.py +1 -0
  96. mammoth_cli/skills/installer.py +321 -0
  97. mammoth_cli/testing.py +55 -0
  98. mammoth_cli-1.0.0.dist-info/METADATA +92 -0
  99. mammoth_cli-1.0.0.dist-info/RECORD +150 -0
  100. mammoth_cli-1.0.0.dist-info/WHEEL +4 -0
  101. mammoth_cli-1.0.0.dist-info/entry_points.txt +3 -0
  102. mammoth_cli-1.0.0.dist-info/licenses/LICENSE +23 -0
  103. spec/manifests/_sdk_introspection.json +1459 -0
  104. spec/manifests/commands/activity.yaml +72 -0
  105. spec/manifests/commands/addon.yaml +239 -0
  106. spec/manifests/commands/agent.yaml +192 -0
  107. spec/manifests/commands/ai.yaml +152 -0
  108. spec/manifests/commands/annotation.yaml +191 -0
  109. spec/manifests/commands/auth.yaml +101 -0
  110. spec/manifests/commands/automation.yaml +273 -0
  111. spec/manifests/commands/batch.yaml +262 -0
  112. spec/manifests/commands/billing.yaml +813 -0
  113. spec/manifests/commands/browse.yaml +145 -0
  114. spec/manifests/commands/capability.yaml +74 -0
  115. spec/manifests/commands/client-app.yaml +193 -0
  116. spec/manifests/commands/completion.yaml +68 -0
  117. spec/manifests/commands/config.yaml +134 -0
  118. spec/manifests/commands/connector.yaml +944 -0
  119. spec/manifests/commands/context.yaml +101 -0
  120. spec/manifests/commands/dashboard.yaml +3595 -0
  121. spec/manifests/commands/data-app.yaml +485 -0
  122. spec/manifests/commands/dataset.yaml +580 -0
  123. spec/manifests/commands/doctor.yaml +35 -0
  124. spec/manifests/commands/external-key.yaml +151 -0
  125. spec/manifests/commands/file.yaml +341 -0
  126. spec/manifests/commands/folder.yaml +334 -0
  127. spec/manifests/commands/job.yaml +151 -0
  128. spec/manifests/commands/notification.yaml +188 -0
  129. spec/manifests/commands/parameter.yaml +527 -0
  130. spec/manifests/commands/project.yaml +669 -0
  131. spec/manifests/commands/report.yaml +36 -0
  132. spec/manifests/commands/schedule.yaml +194 -0
  133. spec/manifests/commands/schema.yaml +74 -0
  134. spec/manifests/commands/skill.yaml +167 -0
  135. spec/manifests/commands/snippet.yaml +311 -0
  136. spec/manifests/commands/support.yaml +1805 -0
  137. spec/manifests/commands/template.yaml +191 -0
  138. spec/manifests/commands/trash.yaml +105 -0
  139. spec/manifests/commands/user.yaml +280 -0
  140. spec/manifests/commands/version.yaml +35 -0
  141. spec/manifests/commands/view.yaml +4232 -0
  142. spec/manifests/commands/webhook.yaml +261 -0
  143. spec/manifests/commands/workflow.yaml +622 -0
  144. spec/manifests/commands/workspace.yaml +705 -0
  145. spec/manifests/openapi-operations.yaml +8853 -0
  146. spec/manifests/schema-v1.json +212 -0
  147. spec/manifests/sdk-catalog.source.yaml +4069 -0
  148. spec/manifests/sdk-methods.yaml +6971 -0
  149. spec/openapi/metadata.json +10 -0
  150. 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
+ ...