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,233 @@
1
+ """Handlers for the ``client-app`` command family.
2
+
3
+ Client apps are workspace-level API credentials: none of the reviewed manifest
4
+ signatures in this family take a ``project_id`` argument, so no handler here
5
+ requires an active project. The (optional) active project is still reported
6
+ in the envelope metadata for context, mirroring the read handlers in
7
+ :mod:`mammoth_cli.commands.file`. The family's resource id is ``client_key``,
8
+ a string identifier — never a secret — read from the first CLI positional.
9
+ No command in this family accepts application secret material as input at
10
+ all: ``create`` only takes a name and description, and ``update`` forwards an
11
+ opaque ``patch_request`` document from ``--input``, never a positional.
12
+ Handlers dispatch through the generic
13
+ :meth:`~mammoth_cli.services.protocol.MammothService.call` seam to the public
14
+ SDK method named by the command's reviewed manifest ``sdk_symbol``.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from typing import Any
20
+
21
+ from mammoth_cli.errors.envelope import (
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 (
30
+ POLICY_CONFIRM_TARGET,
31
+ POLICY_PROMPT_OR_YES,
32
+ enforce_confirmation,
33
+ )
34
+ from mammoth_cli.runtime.invocation import Invocation
35
+ from mammoth_cli.runtime.session import open_service, resolved_project
36
+
37
+ HandlerResult = tuple[Any, dict[str, Any]]
38
+
39
+
40
+ def _symbol(invocation: Invocation) -> str:
41
+ """Return the reviewed backing SDK symbol for this command.
42
+
43
+ Args:
44
+ invocation: The current command's resolved global options.
45
+
46
+ Returns:
47
+ The dotted SDK symbol recorded in the command's manifest entry.
48
+
49
+ Raises:
50
+ CliError: ``sdk_symbol_unresolved`` when no symbol is recorded.
51
+ """
52
+ record = command_by_id(invocation.command_id)
53
+ if record is None or not record.get("sdk_symbol"):
54
+ raise CliError(
55
+ code=CODE_SDK_SYMBOL_UNRESOLVED,
56
+ message=f"No SDK symbol is recorded for '{invocation.command_id}'.",
57
+ exit_status=EXIT_USAGE,
58
+ )
59
+ return str(record["sdk_symbol"])
60
+
61
+
62
+ def _string_positional(invocation: Invocation) -> str | None:
63
+ """Return the first positional argument, or None if absent.
64
+
65
+ Args:
66
+ invocation: The current command's resolved global options.
67
+
68
+ Returns:
69
+ The first extra argument, or None when there is none.
70
+ """
71
+ return invocation.extra_args[0] if invocation.extra_args else None
72
+
73
+
74
+ def _require_string_positional(invocation: Invocation, name: str) -> str:
75
+ """Return the required first positional argument, or raise usage.
76
+
77
+ Args:
78
+ invocation: The current command's resolved global options.
79
+ name: A human-readable name for the argument, used in error text.
80
+
81
+ Returns:
82
+ The first extra argument.
83
+
84
+ Raises:
85
+ CliError: ``missing_argument`` when no positional was given.
86
+ """
87
+ value = _string_positional(invocation)
88
+ if value is None:
89
+ raise CliError(
90
+ code=CODE_MISSING_ARGUMENT,
91
+ message=f"This command requires a {name} argument.",
92
+ exit_status=EXIT_USAGE,
93
+ hint=f"Pass the {name} as a positional argument.",
94
+ )
95
+ return value
96
+
97
+
98
+ def _require_field(document: dict[str, Any] | None, field: str) -> Any:
99
+ """Return a required field from the ``--input`` document, or raise usage.
100
+
101
+ Args:
102
+ document: The parsed ``--input`` request document, or None.
103
+ field: The required field name.
104
+
105
+ Returns:
106
+ The field's value.
107
+
108
+ Raises:
109
+ CliError: ``missing_field`` when the document or field is absent.
110
+ """
111
+ if document is None or field not in document:
112
+ raise CliError(
113
+ code=CODE_MISSING_FIELD,
114
+ message=f"This command requires the '{field}' input field.",
115
+ exit_status=EXIT_USAGE,
116
+ hint=f"Pass it via --input, for example: --input '{{\"{field}\": ...}}'.",
117
+ )
118
+ return document[field]
119
+
120
+
121
+ def _forward_optional(
122
+ document: dict[str, Any], kwargs: dict[str, Any], fields: tuple[str, ...]
123
+ ) -> None:
124
+ """Copy each of ``fields`` present in ``document`` into ``kwargs``, unchanged.
125
+
126
+ Args:
127
+ document: The parsed ``--input`` request document.
128
+ kwargs: The keyword arguments accumulator to update in place.
129
+ fields: The optional field names to forward when present.
130
+ """
131
+ for field in fields:
132
+ if field in document:
133
+ kwargs[field] = document[field]
134
+
135
+
136
+ def _meta(invocation: Invocation, workspace_id: int, project_id: int | None) -> dict[str, Any]:
137
+ """Build the common envelope metadata for a client-app command.
138
+
139
+ Args:
140
+ invocation: The current command's resolved global options.
141
+ workspace_id: The authenticated client's resolved workspace id.
142
+ project_id: The active project id, or None when unset.
143
+
144
+ Returns:
145
+ The envelope metadata mapping.
146
+ """
147
+ return {
148
+ "profile": invocation.profile,
149
+ "workspace_id": workspace_id,
150
+ "project_id": project_id,
151
+ }
152
+
153
+
154
+ def client_app_list(invocation: Invocation) -> HandlerResult:
155
+ """List client apps in the active workspace (requires an admin role)."""
156
+ document = invocation.load_input() or {}
157
+ kwargs: dict[str, Any] = {}
158
+ _forward_optional(document, kwargs, ("limit", "offset", "fields", "sort"))
159
+ with open_service(invocation) as (service, auth):
160
+ data = service.call(_symbol(invocation), **kwargs)
161
+ return data, _meta(invocation, auth.workspace_id, resolved_project(invocation))
162
+
163
+
164
+ def client_app_get(invocation: Invocation) -> HandlerResult:
165
+ """Get one client app by its client key."""
166
+ client_key = _require_string_positional(invocation, "client key")
167
+ document = invocation.load_input() or {}
168
+ kwargs: dict[str, Any] = {"client_key": client_key}
169
+ _forward_optional(document, kwargs, ("fields",))
170
+ with open_service(invocation) as (service, auth):
171
+ data = service.call(_symbol(invocation), **kwargs)
172
+ return data, _meta(invocation, auth.workspace_id, resolved_project(invocation))
173
+
174
+
175
+ def client_app_create(invocation: Invocation) -> HandlerResult:
176
+ """Create a client app. Name comes from a positional or the ``app_name`` field.
177
+
178
+ High-impact: this mints a new set of API credentials in the workspace, so
179
+ it requires ``--yes --confirm APP_NAME``. No secret material is ever
180
+ accepted as input here; any tokens are returned by the API, not supplied.
181
+ """
182
+ document = invocation.load_input() or {}
183
+ app_name = _string_positional(invocation) or document.get("app_name")
184
+ if not app_name:
185
+ raise CliError(
186
+ code=CODE_MISSING_ARGUMENT,
187
+ message="A client app name is required.",
188
+ exit_status=EXIT_USAGE,
189
+ hint="Pass the name as a positional argument or an 'app_name' input field.",
190
+ )
191
+ enforce_confirmation(
192
+ invocation,
193
+ policy=POLICY_CONFIRM_TARGET,
194
+ action=f"create client app '{app_name}'",
195
+ target=str(app_name),
196
+ )
197
+ kwargs: dict[str, Any] = {"app_name": app_name}
198
+ _forward_optional(document, kwargs, ("description",))
199
+ with open_service(invocation) as (service, auth):
200
+ data = service.call(_symbol(invocation), **kwargs)
201
+ return data, _meta(invocation, auth.workspace_id, resolved_project(invocation))
202
+
203
+
204
+ def client_app_update(invocation: Invocation) -> HandlerResult:
205
+ """Apply a patch request to one client app. Client key is positional.
206
+
207
+ High-impact: requires ``--yes --confirm CLIENT_KEY``. The patch document
208
+ is a required ``--input`` field (``patch_request``); no secret material
209
+ is ever taken from a positional argument.
210
+ """
211
+ client_key = _require_string_positional(invocation, "client key")
212
+ document = invocation.load_input()
213
+ patch_request = _require_field(document, "patch_request")
214
+ enforce_confirmation(
215
+ invocation,
216
+ policy=POLICY_CONFIRM_TARGET,
217
+ action=f"update client app {client_key}",
218
+ target=str(client_key),
219
+ )
220
+ with open_service(invocation) as (service, auth):
221
+ data = service.call(_symbol(invocation), client_key=client_key, patch_request=patch_request)
222
+ return data, _meta(invocation, auth.workspace_id, resolved_project(invocation))
223
+
224
+
225
+ def client_app_delete(invocation: Invocation) -> HandlerResult:
226
+ """Permanently delete one client app by its client key. Prompt or ``--yes`` required."""
227
+ client_key = _require_string_positional(invocation, "client key")
228
+ enforce_confirmation(
229
+ invocation, policy=POLICY_PROMPT_OR_YES, action=f"delete client app {client_key}"
230
+ )
231
+ with open_service(invocation) as (service, auth):
232
+ data = service.call(_symbol(invocation), client_key=client_key)
233
+ return data, _meta(invocation, auth.workspace_id, resolved_project(invocation))
@@ -0,0 +1,82 @@
1
+ """The ``completion`` commands: show or install shell tab-completion.
2
+
3
+ ``completion show`` prints the activation snippet for a shell (deterministic,
4
+ side-effect free). ``completion install`` appends that snippet to the shell's
5
+ startup file, idempotently, and reports the path. Both resolve the shell from a
6
+ positional argument, the ``--input`` ``shell`` field, or the ``$SHELL``
7
+ environment variable, and never prompt.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import os
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+ from mammoth_cli.errors.envelope import EXIT_USAGE, CliError
17
+ from mammoth_cli.runtime.invocation import Invocation
18
+
19
+ HandlerResult = tuple[Any, dict[str, Any]]
20
+
21
+ # click/Typer complete-var for the ``mammoth`` program.
22
+ _COMPLETE_VAR = "_MAMMOTH_COMPLETE"
23
+ _SUPPORTED = ("bash", "zsh", "fish")
24
+
25
+
26
+ def _snippet(shell: str) -> str:
27
+ if shell == "fish":
28
+ return f"{_COMPLETE_VAR}=fish_source mammoth | source"
29
+ return f'eval "$({_COMPLETE_VAR}={shell}_source mammoth)"'
30
+
31
+
32
+ def _rc_path(shell: str) -> Path:
33
+ home = Path.home()
34
+ if shell == "bash":
35
+ return home / ".bashrc"
36
+ if shell == "zsh":
37
+ return home / ".zshrc"
38
+ return home / ".config" / "fish" / "config.fish"
39
+
40
+
41
+ def _resolve_shell(invocation: Invocation) -> str:
42
+ document = invocation.load_input() or {}
43
+ candidate = (
44
+ (invocation.extra_args[0] if invocation.extra_args else None)
45
+ or document.get("shell")
46
+ or os.path.basename(os.environ.get("SHELL", ""))
47
+ )
48
+ shell = str(candidate).strip().lower()
49
+ if shell not in _SUPPORTED:
50
+ raise CliError(
51
+ code="unsupported_shell",
52
+ message=f"Cannot generate completion for shell '{candidate or ''}'.",
53
+ exit_status=EXIT_USAGE,
54
+ hint=f"Pass one of: {', '.join(_SUPPORTED)}.",
55
+ )
56
+ return shell
57
+
58
+
59
+ def completion_show(invocation: Invocation) -> HandlerResult:
60
+ """Print the completion activation snippet for a shell."""
61
+ shell = _resolve_shell(invocation)
62
+ return {"shell": shell, "script": _snippet(shell)}, {}
63
+
64
+
65
+ def completion_install(invocation: Invocation) -> HandlerResult:
66
+ """Append the completion snippet to the shell startup file, idempotently."""
67
+ shell = _resolve_shell(invocation)
68
+ snippet = _snippet(shell)
69
+ rc_path = _rc_path(shell)
70
+ rc_path.parent.mkdir(parents=True, exist_ok=True)
71
+ existing = rc_path.read_text(encoding="utf-8") if rc_path.exists() else ""
72
+ added = snippet not in existing
73
+ if added:
74
+ prefix = "" if existing.endswith("\n") or not existing else "\n"
75
+ with rc_path.open("a", encoding="utf-8") as handle:
76
+ handle.write(f"{prefix}# Mammoth CLI completion\n{snippet}\n")
77
+ return {
78
+ "shell": shell,
79
+ "path": str(rc_path),
80
+ "added": added,
81
+ "detail": "installed" if added else "already present",
82
+ }, {}
@@ -0,0 +1,309 @@
1
+ """Bespoke `config` command family: get, set, list, path.
2
+
3
+ Manages non-secret, profile-scoped configuration: the default output mode,
4
+ the timeout family, the one-label server prefix, and the active project id.
5
+ All local; no command in this family makes a network call.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any
11
+
12
+ import typer
13
+
14
+ from mammoth_cli.context import profiles
15
+ from mammoth_cli.context.endpoint import resolve_base_url
16
+ from mammoth_cli.errors.envelope import (
17
+ CODE_INVALID_CONFIG_VALUE,
18
+ CODE_PROFILE_NOT_FOUND,
19
+ EXIT_USAGE,
20
+ CliError,
21
+ )
22
+ from mammoth_cli.output.policy import VALID_OUTPUTS
23
+ from mammoth_cli.runtime import executor
24
+ from mammoth_cli.runtime import options as go
25
+ from mammoth_cli.runtime.invocation import Invocation
26
+
27
+ #: The timeout-family setting keys, shared between the settable-key registry
28
+ #: and the numeric-parsing branch below.
29
+ _TIMEOUT_KEYS = ("timeout", "job_timeout", "pipeline_timeout")
30
+ _PROFILE_KEYS = ("server_prefix", "project")
31
+ _SETTING_KEYS = ("output", *_TIMEOUT_KEYS)
32
+ ALL_CONFIG_KEYS = tuple(sorted(_PROFILE_KEYS + _SETTING_KEYS))
33
+
34
+
35
+ def _profile_name(invocation: Invocation) -> str:
36
+ return invocation.profile or profiles.get_selected()
37
+
38
+
39
+ def _unknown_key_error(key: str) -> CliError:
40
+ return CliError(
41
+ code="unknown_config_key",
42
+ message=f"'{key}' is not a recognized configuration key.",
43
+ exit_status=EXIT_USAGE,
44
+ hint=f"Use one of: {', '.join(ALL_CONFIG_KEYS)}.",
45
+ )
46
+
47
+
48
+ def _profile_not_found_error(profile_name: str) -> CliError:
49
+ return CliError(
50
+ code=CODE_PROFILE_NOT_FOUND,
51
+ message=f"No profile named '{profile_name}' exists yet.",
52
+ exit_status=EXIT_USAGE,
53
+ hint="Run 'mammoth auth login' first.",
54
+ recovery_commands=["mammoth auth login"],
55
+ )
56
+
57
+
58
+ def _validate_key(key: str) -> None:
59
+ if key not in _PROFILE_KEYS and key not in _SETTING_KEYS:
60
+ raise _unknown_key_error(key)
61
+
62
+
63
+ def _get_value(profile_name: str, key: str) -> str | int | None:
64
+ if key in _SETTING_KEYS:
65
+ return profiles.get_setting(profile_name, key)
66
+ record = profiles.get_profile(profile_name)
67
+ if record is None:
68
+ return None
69
+ if key == "project":
70
+ return record.project_id
71
+ return record.server_prefix
72
+
73
+
74
+ def _require_profile(profile_name: str) -> profiles.ProfileRecord:
75
+ existing = profiles.get_profile(profile_name)
76
+ if existing is None:
77
+ raise _profile_not_found_error(profile_name)
78
+ return existing
79
+
80
+
81
+ def _set_value(profile_name: str, key: str, value: str) -> str | int | float:
82
+ if key == "output":
83
+ if value not in VALID_OUTPUTS:
84
+ raise CliError(
85
+ code=CODE_INVALID_CONFIG_VALUE,
86
+ message=f"'{value}' is not a valid --output mode.",
87
+ exit_status=EXIT_USAGE,
88
+ hint=f"Use one of: {', '.join(VALID_OUTPUTS)}.",
89
+ )
90
+ profiles.set_setting(profile_name, key, value)
91
+ return value
92
+
93
+ if key in _TIMEOUT_KEYS:
94
+ try:
95
+ parsed = float(value)
96
+ except ValueError as exc:
97
+ raise CliError(
98
+ code=CODE_INVALID_CONFIG_VALUE,
99
+ message=f"'{value}' is not a number.",
100
+ exit_status=EXIT_USAGE,
101
+ ) from exc
102
+ if parsed <= 0:
103
+ raise CliError(
104
+ code=CODE_INVALID_CONFIG_VALUE,
105
+ message="Timeouts must be positive.",
106
+ exit_status=EXIT_USAGE,
107
+ )
108
+ profiles.set_setting(profile_name, key, value)
109
+ return parsed
110
+
111
+ if key == "project":
112
+ try:
113
+ project_id = int(value)
114
+ except ValueError as exc:
115
+ raise CliError(
116
+ code=CODE_INVALID_CONFIG_VALUE,
117
+ message=f"'{value}' is not an integer project id.",
118
+ exit_status=EXIT_USAGE,
119
+ ) from exc
120
+ if project_id <= 0:
121
+ raise CliError(
122
+ code=CODE_INVALID_CONFIG_VALUE,
123
+ message="project must be a positive integer.",
124
+ exit_status=EXIT_USAGE,
125
+ )
126
+ existing = _require_profile(profile_name)
127
+ profiles.save_profile(
128
+ profiles.ProfileRecord(
129
+ name=existing.name,
130
+ workspace_id=existing.workspace_id,
131
+ server_prefix=existing.server_prefix,
132
+ project_id=project_id,
133
+ )
134
+ )
135
+ return project_id
136
+
137
+ if key == "server_prefix":
138
+ resolve_base_url(value)
139
+ existing = _require_profile(profile_name)
140
+ profiles.save_profile(
141
+ profiles.ProfileRecord(
142
+ name=existing.name,
143
+ workspace_id=existing.workspace_id,
144
+ server_prefix=value,
145
+ project_id=existing.project_id,
146
+ )
147
+ )
148
+ return value
149
+
150
+ raise _unknown_key_error(key)
151
+
152
+
153
+ def config_get(
154
+ output: str = go.output_option(),
155
+ profile: str | None = go.profile_option(),
156
+ project: int | None = go.project_option(),
157
+ timeout: float | None = go.timeout_option(),
158
+ job_timeout: float | None = go.job_timeout_option(),
159
+ pipeline_timeout: float | None = go.pipeline_timeout_option(),
160
+ color: str = go.color_option(),
161
+ no_input: bool = go.no_input_option(),
162
+ no_progress: bool = go.no_progress_option(),
163
+ debug: bool = go.debug_option(),
164
+ input_file: str | None = go.input_file_option(),
165
+ input_format: str | None = go.input_format_option(),
166
+ key: str = typer.Argument(..., help="Configuration key."),
167
+ ) -> None:
168
+ """Get one profile-scoped configuration value."""
169
+ invocation = go.make_invocation(
170
+ "config.get",
171
+ output=output,
172
+ profile=profile,
173
+ project=project,
174
+ timeout=timeout,
175
+ job_timeout=job_timeout,
176
+ pipeline_timeout=pipeline_timeout,
177
+ color=color,
178
+ no_input=no_input,
179
+ no_progress=no_progress,
180
+ debug=debug,
181
+ input_file=input_file,
182
+ input_format=input_format,
183
+ )
184
+
185
+ def producer() -> tuple[Any, dict[str, Any]]:
186
+ _validate_key(key)
187
+ profile_name = _profile_name(invocation)
188
+ value = _get_value(profile_name, key)
189
+ return {"key": key, "value": value, "profile": profile_name}, {"profile": profile_name}
190
+
191
+ executor.run(invocation.command_id, invocation.output, producer)
192
+
193
+
194
+ def config_set(
195
+ output: str = go.output_option(),
196
+ profile: str | None = go.profile_option(),
197
+ project: int | None = go.project_option(),
198
+ timeout: float | None = go.timeout_option(),
199
+ job_timeout: float | None = go.job_timeout_option(),
200
+ pipeline_timeout: float | None = go.pipeline_timeout_option(),
201
+ color: str = go.color_option(),
202
+ no_input: bool = go.no_input_option(),
203
+ no_progress: bool = go.no_progress_option(),
204
+ debug: bool = go.debug_option(),
205
+ input_file: str | None = go.input_file_option(),
206
+ input_format: str | None = go.input_format_option(),
207
+ key: str = typer.Argument(..., help="Configuration key."),
208
+ value: str = typer.Argument(..., help="New value."),
209
+ ) -> None:
210
+ """Set one profile-scoped configuration value."""
211
+ invocation = go.make_invocation(
212
+ "config.set",
213
+ output=output,
214
+ profile=profile,
215
+ project=project,
216
+ timeout=timeout,
217
+ job_timeout=job_timeout,
218
+ pipeline_timeout=pipeline_timeout,
219
+ color=color,
220
+ no_input=no_input,
221
+ no_progress=no_progress,
222
+ debug=debug,
223
+ input_file=input_file,
224
+ input_format=input_format,
225
+ )
226
+
227
+ def producer() -> tuple[Any, dict[str, Any]]:
228
+ _validate_key(key)
229
+ profile_name = _profile_name(invocation)
230
+ stored = _set_value(profile_name, key, value)
231
+ return {"key": key, "value": stored, "profile": profile_name}, {"profile": profile_name}
232
+
233
+ executor.run(invocation.command_id, invocation.output, producer)
234
+
235
+
236
+ def config_list(
237
+ output: str = go.output_option(),
238
+ profile: str | None = go.profile_option(),
239
+ project: int | None = go.project_option(),
240
+ timeout: float | None = go.timeout_option(),
241
+ job_timeout: float | None = go.job_timeout_option(),
242
+ pipeline_timeout: float | None = go.pipeline_timeout_option(),
243
+ color: str = go.color_option(),
244
+ no_input: bool = go.no_input_option(),
245
+ no_progress: bool = go.no_progress_option(),
246
+ debug: bool = go.debug_option(),
247
+ input_file: str | None = go.input_file_option(),
248
+ input_format: str | None = go.input_format_option(),
249
+ ) -> None:
250
+ """List every profile-scoped configuration value."""
251
+ invocation = go.make_invocation(
252
+ "config.list",
253
+ output=output,
254
+ profile=profile,
255
+ project=project,
256
+ timeout=timeout,
257
+ job_timeout=job_timeout,
258
+ pipeline_timeout=pipeline_timeout,
259
+ color=color,
260
+ no_input=no_input,
261
+ no_progress=no_progress,
262
+ debug=debug,
263
+ input_file=input_file,
264
+ input_format=input_format,
265
+ )
266
+
267
+ def producer() -> tuple[Any, dict[str, Any]]:
268
+ profile_name = _profile_name(invocation)
269
+ values = {key: _get_value(profile_name, key) for key in ALL_CONFIG_KEYS}
270
+ return {"profile": profile_name, "values": values}, {"profile": profile_name}
271
+
272
+ executor.run(invocation.command_id, invocation.output, producer)
273
+
274
+
275
+ def config_path(
276
+ output: str = go.output_option(),
277
+ profile: str | None = go.profile_option(),
278
+ project: int | None = go.project_option(),
279
+ timeout: float | None = go.timeout_option(),
280
+ job_timeout: float | None = go.job_timeout_option(),
281
+ pipeline_timeout: float | None = go.pipeline_timeout_option(),
282
+ color: str = go.color_option(),
283
+ no_input: bool = go.no_input_option(),
284
+ no_progress: bool = go.no_progress_option(),
285
+ debug: bool = go.debug_option(),
286
+ input_file: str | None = go.input_file_option(),
287
+ input_format: str | None = go.input_format_option(),
288
+ ) -> None:
289
+ """Print the path to the non-secret profiles file."""
290
+ invocation = go.make_invocation(
291
+ "config.path",
292
+ output=output,
293
+ profile=profile,
294
+ project=project,
295
+ timeout=timeout,
296
+ job_timeout=job_timeout,
297
+ pipeline_timeout=pipeline_timeout,
298
+ color=color,
299
+ no_input=no_input,
300
+ no_progress=no_progress,
301
+ debug=debug,
302
+ input_file=input_file,
303
+ input_format=input_format,
304
+ )
305
+
306
+ def producer() -> tuple[Any, dict[str, Any]]:
307
+ return {"profiles_path": str(profiles.profiles_path())}, {}
308
+
309
+ executor.run(invocation.command_id, invocation.output, producer)