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,567 @@
1
+ """Bespoke `auth` command family: login, status, logout.
2
+
3
+ Authentication has exactly three required inputs (API key, API secret,
4
+ workspace id) and one optional input (server prefix, default ``app-eu``).
5
+ These commands never accept a secret as an ordinary command-line value: the
6
+ API key and secret come from a hidden TTY prompt, the four documented
7
+ ``MAMMOTH_*`` environment variables, or a permission-checked JSON/YAML
8
+ document read through the shared ``--input`` option.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import os
15
+ import stat
16
+ import sys
17
+ from pathlib import Path
18
+ from typing import Any, cast
19
+
20
+ import typer
21
+ import yaml
22
+ from pydantic import ValidationError
23
+
24
+ from mammoth_cli.context import credentials, profiles
25
+ from mammoth_cli.context.endpoint import resolve_base_url
26
+ from mammoth_cli.context.resolver import (
27
+ ENV_API_KEY,
28
+ ENV_API_SECRET,
29
+ ENV_SERVER_PREFIX,
30
+ ENV_WORKSPACE_ID,
31
+ ResolvedAuth,
32
+ resolve_auth,
33
+ )
34
+ from mammoth_cli.contracts.auth import LoginRequest
35
+ from mammoth_cli.errors.envelope import (
36
+ CODE_CONFIRMATION_DECLINED,
37
+ CODE_CONFIRMATION_REQUIRED,
38
+ CODE_INPUT_FORMAT_REQUIRED,
39
+ CODE_INVALID_INPUT_DOCUMENT,
40
+ CODE_INVALID_INPUT_FORMAT,
41
+ CODE_INVALID_WORKSPACE_ID,
42
+ EXIT_USAGE,
43
+ CliError,
44
+ )
45
+ from mammoth_cli.output.policy import resolve_policy
46
+ from mammoth_cli.runtime import executor
47
+ from mammoth_cli.runtime import options as go
48
+ from mammoth_cli.runtime.invocation import Invocation
49
+ from mammoth_cli.services import factory as service_factory
50
+
51
+ _STORAGE_MODES = ("auto", "keyring", "file")
52
+
53
+
54
+ def _format_from_suffix(path: Path) -> str:
55
+ """Detect the JSON/YAML document format from a file suffix.
56
+
57
+ Args:
58
+ path: The document path.
59
+
60
+ Returns:
61
+ ``"json"`` or ``"yaml"``.
62
+
63
+ Raises:
64
+ CliError: ``invalid_input_format`` when the suffix is not recognized.
65
+ """
66
+ suffix = path.suffix.lower()
67
+ if suffix == ".json":
68
+ return "json"
69
+ if suffix in (".yaml", ".yml"):
70
+ return "yaml"
71
+ raise CliError(
72
+ code=CODE_INVALID_INPUT_FORMAT,
73
+ message=f"Cannot detect the document format from '{path.name}'.",
74
+ exit_status=EXIT_USAGE,
75
+ hint="Pass --input-format json or --input-format yaml.",
76
+ )
77
+
78
+
79
+ def _check_file_permissions(path: Path) -> None:
80
+ """Reject a login document readable by group or other users.
81
+
82
+ Args:
83
+ path: The document path.
84
+
85
+ Raises:
86
+ CliError: ``insecure_input_file`` on POSIX when the file is group- or
87
+ other-readable.
88
+ """
89
+ if os.name != "posix":
90
+ return
91
+ mode = stat.S_IMODE(path.stat().st_mode)
92
+ if mode & (stat.S_IRWXG | stat.S_IRWXO):
93
+ raise CliError(
94
+ code="insecure_input_file",
95
+ message=f"'{path}' is readable by group or other users.",
96
+ exit_status=EXIT_USAGE,
97
+ hint=f"Restrict its permissions, for example: chmod 600 {path}",
98
+ )
99
+
100
+
101
+ def _load_login_document(path_or_dash: str, input_format: str | None) -> dict[str, Any]:
102
+ """Read and parse the `auth login --input` document.
103
+
104
+ Args:
105
+ path_or_dash: A file path, or ``"-"`` for stdin.
106
+ input_format: ``"json"`` or ``"yaml"``; required for stdin, otherwise
107
+ detected from the file suffix when omitted.
108
+
109
+ Returns:
110
+ The parsed document as a plain dict.
111
+
112
+ Raises:
113
+ CliError: On a missing file, an insecure file, a missing
114
+ ``--input-format`` for stdin, an unsupported format, or a
115
+ document that does not parse to an object.
116
+ """
117
+ if path_or_dash == "-":
118
+ if input_format is None:
119
+ raise CliError(
120
+ code=CODE_INPUT_FORMAT_REQUIRED,
121
+ message="Reading the login document from stdin requires --input-format.",
122
+ exit_status=EXIT_USAGE,
123
+ hint="Pass --input-format json or --input-format yaml.",
124
+ )
125
+ text = sys.stdin.read()
126
+ fmt = input_format
127
+ else:
128
+ path = Path(path_or_dash)
129
+ if not path.exists():
130
+ raise CliError(
131
+ code="input_file_not_found",
132
+ message=f"'{path}' does not exist.",
133
+ exit_status=EXIT_USAGE,
134
+ )
135
+ _check_file_permissions(path)
136
+ text = path.read_text(encoding="utf-8")
137
+ fmt = input_format or _format_from_suffix(path)
138
+
139
+ if fmt not in ("json", "yaml"):
140
+ raise CliError(
141
+ code=CODE_INVALID_INPUT_FORMAT,
142
+ message=f"'{fmt}' is not a supported input format.",
143
+ exit_status=EXIT_USAGE,
144
+ hint="Use json or yaml.",
145
+ )
146
+ try:
147
+ loaded = json.loads(text) if fmt == "json" else yaml.safe_load(text)
148
+ except (json.JSONDecodeError, yaml.YAMLError) as exc:
149
+ raise CliError(
150
+ code=CODE_INVALID_INPUT_DOCUMENT,
151
+ message=f"The login document is not valid {fmt}.",
152
+ exit_status=EXIT_USAGE,
153
+ hint="Provide a well-formed JSON or YAML object.",
154
+ ) from exc
155
+ if not isinstance(loaded, dict):
156
+ raise CliError(
157
+ code=CODE_INVALID_INPUT_DOCUMENT,
158
+ message="The login document must be a JSON/YAML object.",
159
+ exit_status=EXIT_USAGE,
160
+ )
161
+ return loaded
162
+
163
+
164
+ def _validate_login_document(document: dict[str, Any]) -> LoginRequest:
165
+ """Validate a raw login document against :class:`LoginRequest`.
166
+
167
+ Args:
168
+ document: The parsed document.
169
+
170
+ Returns:
171
+ The validated :class:`LoginRequest`.
172
+
173
+ Raises:
174
+ CliError: ``invalid_login_document`` with sanitized (secret-free)
175
+ field errors.
176
+ """
177
+ try:
178
+ return LoginRequest.model_validate(document)
179
+ except ValidationError as exc:
180
+ errors = [
181
+ {"loc": ".".join(str(part) for part in error["loc"]), "type": error["type"]}
182
+ for error in exc.errors()
183
+ ]
184
+ raise CliError(
185
+ code="invalid_login_document",
186
+ message="The login request document failed validation.",
187
+ exit_status=EXIT_USAGE,
188
+ details={"errors": errors},
189
+ ) from exc
190
+
191
+
192
+ def _read_env_login() -> tuple[str, str, int | None, str | None]:
193
+ """Read credentials from the four documented ``MAMMOTH_*`` variables.
194
+
195
+ Returns:
196
+ ``(api_key, api_secret, workspace_id, server_prefix)``. Workspace id
197
+ and server prefix are None when their variable is unset.
198
+
199
+ Raises:
200
+ CliError: ``missing_env_credentials`` when the API key or secret
201
+ variable is unset.
202
+ """
203
+ api_key = os.environ.get(ENV_API_KEY)
204
+ api_secret = os.environ.get(ENV_API_SECRET)
205
+ workspace_raw = os.environ.get(ENV_WORKSPACE_ID)
206
+ server_prefix = os.environ.get(ENV_SERVER_PREFIX)
207
+ if not api_key or not api_secret:
208
+ raise CliError(
209
+ code="missing_env_credentials",
210
+ message="MAMMOTH_API_KEY and MAMMOTH_API_SECRET must be set for --from-env.",
211
+ exit_status=EXIT_USAGE,
212
+ )
213
+ workspace_id: int | None = None
214
+ if workspace_raw:
215
+ try:
216
+ workspace_id = int(workspace_raw)
217
+ except ValueError as exc:
218
+ raise CliError(
219
+ code=CODE_INVALID_WORKSPACE_ID,
220
+ message="MAMMOTH_WORKSPACE_ID must be an integer.",
221
+ exit_status=EXIT_USAGE,
222
+ hint="Set MAMMOTH_WORKSPACE_ID to a numeric workspace id.",
223
+ ) from exc
224
+ return api_key, api_secret, workspace_id, server_prefix
225
+
226
+
227
+ def _run_login(
228
+ invocation: Invocation,
229
+ *,
230
+ workspace: int | None,
231
+ server_prefix: str | None,
232
+ storage: str,
233
+ from_env: bool,
234
+ ) -> tuple[dict[str, Any], dict[str, Any]]:
235
+ """Validate, connection-check, and persist one `auth login` invocation."""
236
+ if storage not in _STORAGE_MODES:
237
+ raise CliError(
238
+ code="invalid_storage_mode",
239
+ message=f"'{storage}' is not a valid --storage value.",
240
+ exit_status=EXIT_USAGE,
241
+ hint=f"Use one of: {', '.join(_STORAGE_MODES)}.",
242
+ )
243
+ if from_env and invocation.input_file is not None:
244
+ raise CliError(
245
+ code="invalid_input_mode",
246
+ message="--from-env and --input are mutually exclusive.",
247
+ exit_status=EXIT_USAGE,
248
+ )
249
+
250
+ policy = resolve_policy(
251
+ output=invocation.output,
252
+ no_input=invocation.no_input,
253
+ no_progress=invocation.no_progress,
254
+ is_tty=sys.stdin.isatty(),
255
+ color=invocation.color,
256
+ )
257
+
258
+ if from_env:
259
+ api_key, api_secret, env_workspace, env_prefix = _read_env_login()
260
+ effective_workspace = workspace if workspace is not None else env_workspace
261
+ effective_prefix = server_prefix if server_prefix is not None else env_prefix
262
+ elif invocation.input_file is not None:
263
+ document = _load_login_document(invocation.input_file, invocation.input_format)
264
+ request = _validate_login_document(document)
265
+ api_key, api_secret = request.api_key, request.api_secret
266
+ effective_workspace = workspace if workspace is not None else request.workspace_id
267
+ effective_prefix = server_prefix if server_prefix is not None else request.server_prefix
268
+ elif not policy.prompts_disabled:
269
+ api_key = typer.prompt("API key", hide_input=True)
270
+ api_secret = typer.prompt("API secret", hide_input=True)
271
+ effective_workspace = workspace
272
+ effective_prefix = server_prefix
273
+ else:
274
+ raise CliError(
275
+ code="login_input_required",
276
+ message="Non-interactive login requires --from-env or --input.",
277
+ exit_status=EXIT_USAGE,
278
+ hint="Pass --from-env, or --input FILE|- (with --input-format for stdin).",
279
+ )
280
+
281
+ if effective_workspace is None or effective_workspace <= 0:
282
+ raise CliError(
283
+ code=CODE_INVALID_WORKSPACE_ID,
284
+ message="A positive --workspace id is required.",
285
+ exit_status=EXIT_USAGE,
286
+ )
287
+ if not api_key or not api_secret:
288
+ raise CliError(
289
+ code="invalid_credentials",
290
+ message="The API key and secret must be non-empty.",
291
+ exit_status=EXIT_USAGE,
292
+ )
293
+
294
+ resolved_base_url = resolve_base_url(effective_prefix)
295
+
296
+ resolved_auth = ResolvedAuth(
297
+ api_key=api_key,
298
+ api_secret=api_secret,
299
+ workspace_id=effective_workspace,
300
+ base_url=resolved_base_url,
301
+ )
302
+ service = service_factory.build_service(resolved_auth, timeout=invocation.timeout)
303
+ try:
304
+ service.check_connection()
305
+ finally:
306
+ service.close()
307
+
308
+ profile_name = invocation.profile or profiles.DEFAULT_PROFILE_NAME
309
+ profiles.validate_profile_name(profile_name)
310
+ existing = profiles.get_profile(profile_name)
311
+ record = profiles.ProfileRecord(
312
+ name=profile_name,
313
+ workspace_id=effective_workspace,
314
+ server_prefix=effective_prefix,
315
+ project_id=existing.project_id if existing is not None else None,
316
+ )
317
+ profiles.save_profile(record)
318
+ storage_used = credentials.store_credentials(
319
+ profile_name,
320
+ api_key,
321
+ api_secret,
322
+ storage=cast(credentials.StorageMode, storage),
323
+ interactive=not policy.prompts_disabled,
324
+ )
325
+ profiles.set_selected(profile_name)
326
+
327
+ data = {
328
+ "profile": profile_name,
329
+ "workspace_id": effective_workspace,
330
+ "base_url": resolved_base_url,
331
+ "storage": storage_used,
332
+ }
333
+ return data, {"profile": profile_name, "workspace_id": effective_workspace}
334
+
335
+
336
+ def auth_login(
337
+ output: str = go.output_option(),
338
+ profile: str | None = go.profile_option(),
339
+ project: int | None = go.project_option(),
340
+ timeout: float | None = go.timeout_option(),
341
+ job_timeout: float | None = go.job_timeout_option(),
342
+ pipeline_timeout: float | None = go.pipeline_timeout_option(),
343
+ color: str = go.color_option(),
344
+ no_input: bool = go.no_input_option(),
345
+ no_progress: bool = go.no_progress_option(),
346
+ debug: bool = go.debug_option(),
347
+ input_file: str | None = go.input_file_option(),
348
+ input_format: str | None = go.input_format_option(),
349
+ workspace: int | None = typer.Option(
350
+ None,
351
+ "--workspace",
352
+ "-w",
353
+ help="Workspace id. Required unless --from-env or --input supplies it.",
354
+ ),
355
+ server_prefix: str | None = typer.Option(
356
+ None, "--server-prefix", help="Server prefix (one DNS label). Default 'app-eu'."
357
+ ),
358
+ storage: str = typer.Option(
359
+ "auto", "--storage", help="Credential storage backend.", metavar="auto|keyring|file"
360
+ ),
361
+ from_env: bool = typer.Option(
362
+ False,
363
+ "--from-env",
364
+ help="Read credentials from MAMMOTH_API_KEY/MAMMOTH_API_SECRET/MAMMOTH_WORKSPACE_ID.",
365
+ ),
366
+ ) -> None:
367
+ """Log in and store one profile's credentials.
368
+
369
+ Performs a lightweight connection check before saving anything; a failed
370
+ check leaves existing profile state unchanged.
371
+ """
372
+ invocation = go.make_invocation(
373
+ "auth.login",
374
+ output=output,
375
+ profile=profile,
376
+ project=project,
377
+ timeout=timeout,
378
+ job_timeout=job_timeout,
379
+ pipeline_timeout=pipeline_timeout,
380
+ color=color,
381
+ no_input=no_input,
382
+ no_progress=no_progress,
383
+ debug=debug,
384
+ input_file=input_file,
385
+ input_format=input_format,
386
+ )
387
+
388
+ def producer() -> tuple[Any, dict[str, Any]]:
389
+ return _run_login(
390
+ invocation,
391
+ workspace=workspace,
392
+ server_prefix=server_prefix,
393
+ storage=storage,
394
+ from_env=from_env,
395
+ )
396
+
397
+ executor.run(invocation.command_id, invocation.output, producer)
398
+
399
+
400
+ def _run_status(invocation: Invocation, *, check: bool) -> tuple[dict[str, Any], dict[str, Any]]:
401
+ """Report local (or, with `--check`, live) authentication state."""
402
+ profile_name = invocation.profile or profiles.get_selected()
403
+ record = profiles.get_profile(profile_name)
404
+ has_creds = credentials.has_credentials(profile_name)
405
+
406
+ if record is not None:
407
+ endpoint = resolve_base_url(record.server_prefix)
408
+ workspace_id: int | None = record.workspace_id
409
+ else:
410
+ endpoint = resolve_base_url(None)
411
+ workspace_id = None
412
+
413
+ data: dict[str, Any] = {
414
+ "profile": profile_name,
415
+ "workspace_id": workspace_id,
416
+ "endpoint": endpoint,
417
+ "has_credentials": has_creds,
418
+ "checked": False,
419
+ "connected": None,
420
+ }
421
+
422
+ if check:
423
+ auth = resolve_auth(invocation)
424
+ service = service_factory.build_service(auth, timeout=invocation.timeout)
425
+ try:
426
+ service.check_connection()
427
+ data["connected"] = True
428
+ finally:
429
+ service.close()
430
+ data["checked"] = True
431
+
432
+ return data, {"profile": profile_name, "workspace_id": workspace_id}
433
+
434
+
435
+ def auth_status(
436
+ output: str = go.output_option(),
437
+ profile: str | None = go.profile_option(),
438
+ project: int | None = go.project_option(),
439
+ timeout: float | None = go.timeout_option(),
440
+ job_timeout: float | None = go.job_timeout_option(),
441
+ pipeline_timeout: float | None = go.pipeline_timeout_option(),
442
+ color: str = go.color_option(),
443
+ no_input: bool = go.no_input_option(),
444
+ no_progress: bool = go.no_progress_option(),
445
+ debug: bool = go.debug_option(),
446
+ input_file: str | None = go.input_file_option(),
447
+ input_format: str | None = go.input_format_option(),
448
+ check: bool = typer.Option(False, "--check", help="Perform a live connection check."),
449
+ ) -> None:
450
+ """Report local authentication state for one profile.
451
+
452
+ Makes no network call unless `--check` is given.
453
+ """
454
+ invocation = go.make_invocation(
455
+ "auth.status",
456
+ output=output,
457
+ profile=profile,
458
+ project=project,
459
+ timeout=timeout,
460
+ job_timeout=job_timeout,
461
+ pipeline_timeout=pipeline_timeout,
462
+ color=color,
463
+ no_input=no_input,
464
+ no_progress=no_progress,
465
+ debug=debug,
466
+ input_file=input_file,
467
+ input_format=input_format,
468
+ )
469
+
470
+ def producer() -> tuple[Any, dict[str, Any]]:
471
+ return _run_status(invocation, check=check)
472
+
473
+ executor.run(invocation.command_id, invocation.output, producer)
474
+
475
+
476
+ def _run_logout(
477
+ invocation: Invocation, *, all_profiles: bool, yes: bool
478
+ ) -> tuple[dict[str, Any], dict[str, Any]]:
479
+ """Remove one profile's (or every profile's) stored credentials."""
480
+ if all_profiles and invocation.profile is not None:
481
+ raise CliError(
482
+ code="invalid_argument_combination",
483
+ message="--all and --profile are mutually exclusive.",
484
+ exit_status=EXIT_USAGE,
485
+ )
486
+
487
+ policy = resolve_policy(
488
+ output=invocation.output,
489
+ no_input=invocation.no_input,
490
+ no_progress=invocation.no_progress,
491
+ is_tty=sys.stdin.isatty(),
492
+ color=invocation.color,
493
+ )
494
+ if not yes:
495
+ if policy.prompts_disabled:
496
+ raise CliError(
497
+ code=CODE_CONFIRMATION_REQUIRED,
498
+ message="auth logout requires --yes in non-interactive mode.",
499
+ exit_status=EXIT_USAGE,
500
+ hint="Re-run with --yes.",
501
+ )
502
+ if not typer.confirm("Remove the stored credentials?", default=False):
503
+ raise CliError(
504
+ code=CODE_CONFIRMATION_DECLINED,
505
+ message="Logout was not confirmed.",
506
+ exit_status=EXIT_USAGE,
507
+ )
508
+
509
+ removed: list[str] = []
510
+ if all_profiles:
511
+ # Iterate raw profile names so an unparseable legacy profile (for
512
+ # example one with an unsupported base_url) is still cleaned up rather
513
+ # than blocking the very command meant to remove it.
514
+ for name in profiles.list_profile_names():
515
+ if credentials.delete_credentials(name):
516
+ removed.append(name)
517
+ profiles.delete_profile(name)
518
+ else:
519
+ profile_name = invocation.profile or profiles.get_selected()
520
+ if credentials.delete_credentials(profile_name):
521
+ removed.append(profile_name)
522
+ profiles.delete_profile(profile_name)
523
+
524
+ data = {"removed_profiles": sorted(removed), "all": all_profiles}
525
+ return data, {}
526
+
527
+
528
+ def auth_logout(
529
+ output: str = go.output_option(),
530
+ profile: str | None = go.profile_option(),
531
+ project: int | None = go.project_option(),
532
+ timeout: float | None = go.timeout_option(),
533
+ job_timeout: float | None = go.job_timeout_option(),
534
+ pipeline_timeout: float | None = go.pipeline_timeout_option(),
535
+ color: str = go.color_option(),
536
+ no_input: bool = go.no_input_option(),
537
+ no_progress: bool = go.no_progress_option(),
538
+ debug: bool = go.debug_option(),
539
+ input_file: str | None = go.input_file_option(),
540
+ input_format: str | None = go.input_format_option(),
541
+ all_profiles: bool = typer.Option(False, "--all", help="Remove every profile."),
542
+ yes: bool = typer.Option(False, "--yes", help="Confirm the removal without a prompt."),
543
+ ) -> None:
544
+ """Remove one profile's (or every profile's) stored credentials.
545
+
546
+ Idempotent: removing a profile that does not exist succeeds.
547
+ """
548
+ invocation = go.make_invocation(
549
+ "auth.logout",
550
+ output=output,
551
+ profile=profile,
552
+ project=project,
553
+ timeout=timeout,
554
+ job_timeout=job_timeout,
555
+ pipeline_timeout=pipeline_timeout,
556
+ color=color,
557
+ no_input=no_input,
558
+ no_progress=no_progress,
559
+ debug=debug,
560
+ input_file=input_file,
561
+ input_format=input_format,
562
+ )
563
+
564
+ def producer() -> tuple[Any, dict[str, Any]]:
565
+ return _run_logout(invocation, all_profiles=all_profiles, yes=yes)
566
+
567
+ executor.run(invocation.command_id, invocation.output, producer)