tangle-cli 0.0.1a1__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 (48) hide show
  1. tangle_cli/__init__.py +19 -0
  2. tangle_cli/api_cli.py +787 -0
  3. tangle_cli/api_schema.py +633 -0
  4. tangle_cli/api_transport.py +461 -0
  5. tangle_cli/args_container.py +244 -0
  6. tangle_cli/artifacts.py +293 -0
  7. tangle_cli/artifacts_cli.py +108 -0
  8. tangle_cli/cli.py +57 -0
  9. tangle_cli/cli_helpers.py +116 -0
  10. tangle_cli/cli_options.py +52 -0
  11. tangle_cli/client.py +677 -0
  12. tangle_cli/component_from_func.py +1856 -0
  13. tangle_cli/component_generator.py +298 -0
  14. tangle_cli/component_inspector.py +494 -0
  15. tangle_cli/component_publisher.py +921 -0
  16. tangle_cli/components_cli.py +269 -0
  17. tangle_cli/dynamic_discovery_client.py +296 -0
  18. tangle_cli/generated_model_extensions.py +405 -0
  19. tangle_cli/generated_runtime.py +43 -0
  20. tangle_cli/handler.py +96 -0
  21. tangle_cli/hydration_trust.py +222 -0
  22. tangle_cli/logger.py +166 -0
  23. tangle_cli/models.py +407 -0
  24. tangle_cli/module_bundler.py +662 -0
  25. tangle_cli/openapi/__init__.py +0 -0
  26. tangle_cli/openapi/codegen.py +1090 -0
  27. tangle_cli/openapi/parser.py +77 -0
  28. tangle_cli/pipeline_dehydrator.py +720 -0
  29. tangle_cli/pipeline_hydrator.py +1785 -0
  30. tangle_cli/pipeline_run_annotations.py +41 -0
  31. tangle_cli/pipeline_run_details.py +203 -0
  32. tangle_cli/pipeline_run_manager.py +1994 -0
  33. tangle_cli/pipeline_run_search.py +712 -0
  34. tangle_cli/pipeline_runner.py +620 -0
  35. tangle_cli/pipeline_runs_cli.py +584 -0
  36. tangle_cli/pipelines.py +581 -0
  37. tangle_cli/pipelines_cli.py +271 -0
  38. tangle_cli/published_components_cli.py +373 -0
  39. tangle_cli/py.typed +0 -0
  40. tangle_cli/quickstart.py +110 -0
  41. tangle_cli/secrets.py +156 -0
  42. tangle_cli/secrets_cli.py +269 -0
  43. tangle_cli/utils.py +942 -0
  44. tangle_cli/version_manager.py +470 -0
  45. tangle_cli-0.0.1a1.dist-info/METADATA +561 -0
  46. tangle_cli-0.0.1a1.dist-info/RECORD +48 -0
  47. tangle_cli-0.0.1a1.dist-info/WHEEL +4 -0
  48. tangle_cli-0.0.1a1.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,293 @@
1
+ """Read-only artifact lookup helpers for Tangle pipeline runs.
2
+
3
+ This module intentionally resolves artifact metadata only. It does not fetch
4
+ signed URLs, download remote objects, write local files, or mutate artifacts.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import asdict, dataclass, field, is_dataclass
10
+ from typing import Any, Protocol
11
+
12
+ from .handler import TangleCliHandler
13
+
14
+
15
+ class ArtifactClient(Protocol):
16
+ """Subset of the static API client used for read-only artifact lookup."""
17
+
18
+ def get_run_details(self, run_id: str) -> Any: ...
19
+
20
+ def get_execution_details(self, execution_id: str) -> Any: ...
21
+
22
+ def artifacts_get(self, artifact_id: str) -> Any: ...
23
+
24
+
25
+ @dataclass
26
+ class ArtifactComponentQuery:
27
+ """Filter for selecting artifacts by component name or digest."""
28
+
29
+ name: str | None = None
30
+ digest: str | None = None
31
+ outputs: list[str] = field(default_factory=list)
32
+
33
+
34
+ @dataclass
35
+ class ArtifactInfo:
36
+ """Resolved artifact metadata from GET /api/artifacts/{id}.
37
+
38
+ This dataclass intentionally lives in this native-free module. Generated
39
+ response objects are accepted structurally via ``from_response`` so no
40
+ ``tangle_api`` import is required.
41
+ """
42
+
43
+ id: str
44
+ uri: str
45
+ key: str = ""
46
+ total_size: int = 0
47
+ is_dir: bool = False
48
+ hash: str | None = None
49
+ created_at: str | None = None
50
+ error: str | None = None
51
+ local_path: str | None = None
52
+
53
+ @classmethod
54
+ def from_dict(cls, data: dict[str, Any], key: str = "") -> ArtifactInfo:
55
+ ad = data.get("artifact_data", {})
56
+ return cls(
57
+ id=data.get("id", ""),
58
+ uri=_mapping_or_attr(ad, "uri", ""),
59
+ key=key,
60
+ total_size=_mapping_or_attr(ad, "total_size", 0),
61
+ is_dir=_mapping_or_attr(ad, "is_dir", False),
62
+ hash=_optional_str(_mapping_or_attr(ad, "hash")),
63
+ created_at=_optional_str(_mapping_or_attr(ad, "created_at")),
64
+ )
65
+
66
+ @classmethod
67
+ def from_response(cls, response: Any, *, key: str = "") -> ArtifactInfo:
68
+ """Create a flattened artifact DTO from a generated or duck-typed response."""
69
+
70
+ artifact_data = getattr(response, "artifact_data", None)
71
+ total_size = _mapping_or_attr(artifact_data, "total_size", 0)
72
+ is_dir = _mapping_or_attr(artifact_data, "is_dir", False)
73
+ return cls(
74
+ id=str(getattr(response, "id", "") or ""),
75
+ uri=str(_mapping_or_attr(artifact_data, "uri", "") or ""),
76
+ key=key,
77
+ total_size=total_size if isinstance(total_size, int) else 0,
78
+ is_dir=is_dir if isinstance(is_dir, bool) else False,
79
+ hash=_optional_str(_mapping_or_attr(artifact_data, "hash")),
80
+ created_at=_optional_str(_mapping_or_attr(artifact_data, "created_at")),
81
+ )
82
+
83
+
84
+ class ArtifactManager(TangleCliHandler):
85
+ """Read-only artifact metadata manager.
86
+
87
+ Downstream packages can inject an already-authenticated client or a lazy
88
+ ``client_factory`` (for example, one that applies provider auth). The manager
89
+ keeps the same read-only constraints as the module-level helpers: it never
90
+ downloads artifact contents, signs URLs, writes files, or mutates artifacts.
91
+ """
92
+
93
+ def __init__(
94
+ self,
95
+ client: ArtifactClient | None = None,
96
+ *,
97
+ client_factory: Any | None = None,
98
+ logger: Any | None = None,
99
+ **kwargs: Any,
100
+ ) -> None:
101
+ super().__init__(client=client, client_factory=client_factory, logger=logger, **kwargs)
102
+
103
+ def collect_artifacts(
104
+ self,
105
+ execution: Any,
106
+ tasks_query: dict[str, list[str]],
107
+ components_query: list[ArtifactComponentQuery],
108
+ prefix: str = "",
109
+ ) -> dict[str, str]:
110
+ """Collect artifact IDs by walking an enriched execution tree."""
111
+
112
+ artifact_ids: dict[str, str] = {}
113
+ task_spec = _mapping_or_attr(execution, "task_spec")
114
+ graph_tasks = _mapping_or_attr(task_spec, "graph_tasks", {})
115
+ if not isinstance(graph_tasks, dict):
116
+ return artifact_ids
117
+
118
+ for task_name, child_task in graph_tasks.items():
119
+ task_name = str(task_name)
120
+ key_prefix = f"{prefix}{task_name}" if prefix else task_name
121
+ output_filters: list[list[str]] = []
122
+
123
+ for query_name in (task_name, key_prefix):
124
+ if query_name in tasks_query:
125
+ output_filters.append(tasks_query[query_name])
126
+ break
127
+
128
+ child_digest = _mapping_or_attr(child_task, "digest")
129
+ child_name = _mapping_or_attr(child_task, "name")
130
+ for component in components_query:
131
+ if (component.digest and child_digest == component.digest) or (
132
+ component.name and child_name == component.name
133
+ ):
134
+ output_filters.append(component.outputs)
135
+
136
+ out_artifacts = _artifact_id_map(_mapping_or_attr(child_task, "execution_output_artifacts", {}))
137
+ if output_filters and out_artifacts:
138
+ include_all = any(not output_filter for output_filter in output_filters)
139
+ requested_outputs = {
140
+ output_name
141
+ for output_filter in output_filters
142
+ for output_name in output_filter
143
+ }
144
+ for output_name, artifact_id in out_artifacts.items():
145
+ if include_all or output_name in requested_outputs:
146
+ artifact_ids[f"{key_prefix}/{output_name}"] = artifact_id
147
+
148
+ if _mapping_or_attr(child_task, "is_graph", False):
149
+ child_executions = _mapping_or_attr(execution, "child_executions", {})
150
+ child_execution = child_executions.get(task_name) if isinstance(child_executions, dict) else None
151
+ if child_execution:
152
+ artifact_ids.update(
153
+ self.collect_artifacts(
154
+ child_execution,
155
+ tasks_query,
156
+ components_query,
157
+ prefix=f"{key_prefix}/",
158
+ )
159
+ )
160
+
161
+ return artifact_ids
162
+
163
+ def collect_execution_artifacts(
164
+ self,
165
+ execution_ids: dict[str, list[str]],
166
+ ) -> dict[str, str]:
167
+ """Collect artifact IDs directly from execution IDs."""
168
+
169
+ artifact_ids: dict[str, str] = {}
170
+ client = self._require_client()
171
+ for execution_id, output_filter in execution_ids.items():
172
+ execution = client.get_execution_details(execution_id)
173
+ output_artifacts = _artifact_id_map(_mapping_or_attr(execution, "output_artifacts", {}))
174
+ for output_name, artifact_id in output_artifacts.items():
175
+ if not output_filter or output_name in output_filter:
176
+ artifact_ids[f"{execution_id}/{output_name}"] = artifact_id
177
+ return artifact_ids
178
+
179
+ def get_artifacts(
180
+ self,
181
+ run_id: str,
182
+ query: dict[str, Any],
183
+ ) -> dict[str, ArtifactInfo]:
184
+ """Get artifact metadata for tasks/components in a pipeline run.
185
+
186
+ Query keys:
187
+ - ``tasks``: ``{<task_name>: [<output_names>]}``
188
+ - ``components``: ``[{"name"|"digest": ..., "outputs": [...]}]``
189
+ - ``executions``: ``{<execution_id>: [<output_names>]}``
190
+ - ``artifact_ids``: ``[<artifact_id>, ...]``
191
+
192
+ Empty output lists mean all outputs. Per-artifact lookup failures are
193
+ returned as ``ArtifactInfo(error=...)`` entries instead of failing the
194
+ whole command.
195
+ """
196
+
197
+ artifact_ids: dict[str, str] = {}
198
+
199
+ for artifact_id in query.get("artifact_ids", []) or []:
200
+ artifact_ids[str(artifact_id)] = str(artifact_id)
201
+
202
+ executions_query = query.get("executions", {}) or {}
203
+ if executions_query:
204
+ artifact_ids.update(self.collect_execution_artifacts(executions_query))
205
+
206
+ tasks_query = query.get("tasks", {}) or {}
207
+ components_query_raw = query.get("components", []) or []
208
+ if tasks_query or components_query_raw:
209
+ details = self._require_client().get_run_details(run_id)
210
+ execution = _mapping_or_attr(details, "execution")
211
+ if not execution:
212
+ raise RuntimeError("No execution details found for run")
213
+ artifact_ids.update(
214
+ self.collect_artifacts(
215
+ execution,
216
+ tasks_query,
217
+ _component_queries(components_query_raw),
218
+ )
219
+ )
220
+
221
+ artifacts: dict[str, ArtifactInfo] = {}
222
+ for key, artifact_id in artifact_ids.items():
223
+ try:
224
+ response = self._require_client().artifacts_get(artifact_id)
225
+ artifacts[key] = _artifact_info_from_response(response, artifact_id=artifact_id, key=key)
226
+ except Exception as exc:
227
+ artifacts[key] = ArtifactInfo(id=artifact_id, uri="", key=key, error=str(exc))
228
+
229
+ return artifacts
230
+
231
+ @staticmethod
232
+ def serialize_artifacts(artifacts: dict[str, ArtifactInfo]) -> list[dict[str, Any]]:
233
+ """Serialize artifact dict to a JSON-friendly list, dropping ``None`` fields."""
234
+
235
+ result: list[dict[str, Any]] = []
236
+ for artifact in artifacts.values():
237
+ data = asdict(artifact) if is_dataclass(artifact) else dict(artifact)
238
+ result.append({key: value for key, value in data.items() if value is not None})
239
+ return result
240
+
241
+
242
+ def _mapping_or_attr(value: Any, key: str, default: Any = None) -> Any:
243
+ if isinstance(value, dict):
244
+ return value.get(key, default)
245
+ return getattr(value, key, default)
246
+
247
+
248
+ def _optional_str(value: Any) -> str | None:
249
+ if value is None:
250
+ return None
251
+ return str(value)
252
+
253
+
254
+ def _artifact_id_map(raw_artifacts: Any) -> dict[str, str]:
255
+ """Normalize API artifact maps to ``{output_name: artifact_id}``."""
256
+
257
+ if not isinstance(raw_artifacts, dict):
258
+ return {}
259
+
260
+ artifact_ids: dict[str, str] = {}
261
+ for output_name, value in raw_artifacts.items():
262
+ if isinstance(value, str):
263
+ artifact_ids[str(output_name)] = value
264
+ elif isinstance(value, dict) and value.get("id"):
265
+ artifact_ids[str(output_name)] = str(value["id"])
266
+ elif getattr(value, "id", None):
267
+ artifact_ids[str(output_name)] = str(value.id)
268
+ return artifact_ids
269
+
270
+
271
+ def _component_queries(raw_components: list[dict[str, Any]]) -> list[ArtifactComponentQuery]:
272
+ return [
273
+ ArtifactComponentQuery(
274
+ name=component.get("name"),
275
+ digest=component.get("digest"),
276
+ outputs=component.get("outputs") or [],
277
+ )
278
+ for component in raw_components
279
+ ]
280
+
281
+
282
+ def _artifact_info_from_response(response: Any, *, artifact_id: str, key: str) -> ArtifactInfo:
283
+ if isinstance(response, dict):
284
+ return ArtifactInfo.from_dict(response, key=key)
285
+ return ArtifactInfo.from_response(response, key=key)
286
+
287
+
288
+ __all__ = [
289
+ "ArtifactClient",
290
+ "ArtifactComponentQuery",
291
+ "ArtifactInfo",
292
+ "ArtifactManager",
293
+ ]
@@ -0,0 +1,108 @@
1
+ """`tangle sdk artifacts` read-only artifact commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated, Any
6
+
7
+ from cyclopts import App, Parameter
8
+
9
+ from .cli_helpers import (
10
+ LazyTangleApiClient,
11
+ api_arg_specs,
12
+ include_env_credentials_for_args,
13
+ load_args_or_exit,
14
+ print_json,
15
+ )
16
+ from .cli_options import (
17
+ AuthHeaderOption,
18
+ BaseUrlOption,
19
+ ConfigOption,
20
+ HeaderOption,
21
+ LogTypeOption,
22
+ TokenOption,
23
+ )
24
+ from .logger import logger_for_log_type
25
+
26
+ QueryOption = Annotated[
27
+ str | None,
28
+ Parameter(
29
+ name="--query",
30
+ alias="-q",
31
+ help=(
32
+ "JSON query with optional keys: "
33
+ "'tasks', 'components', 'executions', and 'artifact_ids'. "
34
+ "Empty output lists mean all outputs."
35
+ ),
36
+ ),
37
+ ]
38
+
39
+ app = App(
40
+ name="artifacts",
41
+ help="Read artifact metadata for Tangle pipeline runs.",
42
+ )
43
+
44
+
45
+ @app.command(name="get")
46
+ def artifacts_get(
47
+ run_id: str | None = None,
48
+ *,
49
+ query: QueryOption = None,
50
+ base_url: BaseUrlOption = None,
51
+ token: TokenOption = None,
52
+ auth_header: AuthHeaderOption = None,
53
+ header: HeaderOption = None,
54
+ config: ConfigOption = None,
55
+ log_type: LogTypeOption = "console",
56
+ ) -> None:
57
+ """Get artifact metadata for tasks/components in a pipeline run."""
58
+
59
+ all_args = load_args_or_exit(
60
+ config,
61
+ run_id=("run_id", run_id, None, False, True),
62
+ query=("query", query, None, True, True),
63
+ log_type=(log_type, "console"),
64
+ **api_arg_specs(
65
+ base_url=base_url,
66
+ token=token,
67
+ auth_header=auth_header,
68
+ header=header,
69
+ ),
70
+ )
71
+
72
+ results: list[dict[str, Any]] = []
73
+ for args in all_args:
74
+ logger, finalize_logs = logger_for_log_type(args.log_type)
75
+ try:
76
+ client = LazyTangleApiClient(
77
+ base_url=args.base_url,
78
+ token=args.token,
79
+ auth_header=args.auth_header,
80
+ header=args.header,
81
+ include_env_credentials=include_env_credentials_for_args(args, base_url),
82
+ command_name="artifact commands",
83
+ )
84
+ if require_available := getattr(client, "require_available", None):
85
+ require_available()
86
+ from .artifacts import ArtifactManager
87
+
88
+ manager = ArtifactManager(client=client)
89
+ try:
90
+ artifacts = manager.get_artifacts(args.run_id, args.query)
91
+ except RuntimeError as exc:
92
+ print_json({"status": "error", "error": str(exc)})
93
+ raise SystemExit(1) from exc
94
+
95
+ results.append(
96
+ {
97
+ "status": "success",
98
+ "run_id": args.run_id,
99
+ "count": len(artifacts),
100
+ "artifacts": ArtifactManager.serialize_artifacts(artifacts),
101
+ }
102
+ )
103
+ finally:
104
+ finalize_logs()
105
+
106
+ print_json(
107
+ results[0] if len(results) == 1 else {"status": "success", "results": results}
108
+ )
tangle_cli/cli.py ADDED
@@ -0,0 +1,57 @@
1
+ from cyclopts import App
2
+
3
+ from . import (
4
+ __version__,
5
+ api_cli,
6
+ artifacts_cli,
7
+ components_cli,
8
+ pipeline_runs_cli,
9
+ pipelines_cli,
10
+ published_components_cli,
11
+ quickstart,
12
+ secrets_cli,
13
+ )
14
+
15
+
16
+ def version() -> None:
17
+ """Print the installed tangle-cli package version."""
18
+
19
+ print(__version__)
20
+
21
+
22
+ def build_sdk_app() -> App:
23
+ """Build the SDK command group."""
24
+
25
+ sdk_app = App(
26
+ name="sdk",
27
+ help="Work with local Tangle SDK resources and scaffolding.",
28
+ )
29
+ sdk_app.command(artifacts_cli.app)
30
+ sdk_app.command(components_cli.app)
31
+ sdk_app.command(pipelines_cli.app)
32
+ sdk_app.command(pipeline_runs_cli.app)
33
+ sdk_app.command(published_components_cli.app)
34
+ sdk_app.command(secrets_cli.app)
35
+ return sdk_app
36
+
37
+
38
+ def build_app() -> App:
39
+ """Build the root CLI app lazily for the current invocation."""
40
+
41
+ app = App(
42
+ help="CLI for Tangle, the open-source ML pipeline orchestration platform.",
43
+ version=__version__,
44
+ )
45
+ app.command(name="version")(version)
46
+ app.command(quickstart.app)
47
+ app.command(api_cli.build_app())
48
+ app.command(build_sdk_app())
49
+ return app
50
+
51
+
52
+ def main() -> None:
53
+ build_app()()
54
+
55
+
56
+ if __name__ == "__main__":
57
+ main()
@@ -0,0 +1,116 @@
1
+ """Shared helpers for Tangle CLI command modules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import pathlib
7
+ from typing import Any
8
+
9
+ from .args_container import ArgsContainer, ConfigFileError
10
+
11
+
12
+ def load_args_or_exit(config: str | None, **kwargs: Any) -> list[ArgsContainer]:
13
+ """Load ArgsContainer values from CLI/config specs, exiting with CLI errors."""
14
+
15
+ try:
16
+ return ArgsContainer.load(config, **kwargs)
17
+ except ConfigFileError as exc:
18
+ raise SystemExit(f"Config error: {exc}") from exc
19
+
20
+
21
+ def print_json(payload: object) -> None:
22
+ """Print a stable pretty JSON payload for CLI output."""
23
+
24
+ print(json.dumps(payload, indent=2, sort_keys=True))
25
+
26
+
27
+ def load_config_or_exit(config: str | None) -> dict[str, object]:
28
+ """Load the first YAML/JSON config mapping for commands with custom merging."""
29
+
30
+ if config is None:
31
+ return {}
32
+ try:
33
+ configs = ArgsContainer._load_config_file(config)
34
+ except ConfigFileError as exc:
35
+ raise SystemExit(f"Config error: {exc}") from exc
36
+ return configs[0] if configs else {}
37
+
38
+
39
+ def optional_path(value: str | pathlib.Path | object | None) -> pathlib.Path | None:
40
+ """Convert a CLI/config path value to Path when present."""
41
+
42
+ if isinstance(value, pathlib.Path):
43
+ return value
44
+ if isinstance(value, str):
45
+ return pathlib.Path(value)
46
+ return None
47
+
48
+
49
+ def api_arg_specs(
50
+ *,
51
+ base_url: str | None = None,
52
+ token: str | None = None,
53
+ auth_header: str | None = None,
54
+ header: list[str] | None = None,
55
+ ) -> dict[str, tuple[Any, ...]]:
56
+ """Build ArgsContainer specs for common API connection options."""
57
+
58
+ return {
59
+ "base_url": (base_url, None),
60
+ "token": (token, None),
61
+ "auth_header": (auth_header, None),
62
+ "header": (header, None),
63
+ }
64
+
65
+
66
+ class LazyTangleApiClient:
67
+ """Instantiate the generated API client only when a command uses it.
68
+
69
+ Importing CLI modules must stay native-free so local-only commands can run
70
+ without the generated ``tangle_api`` package. This proxy delays importing and
71
+ constructing ``TangleApiClient`` until an API method is actually accessed,
72
+ while keeping CLI-friendly error wording in the CLI helper layer.
73
+ """
74
+
75
+ def __init__(self, *, command_name: str, **client_kwargs: Any) -> None:
76
+ self.command_name = command_name
77
+ self.client_kwargs = client_kwargs
78
+ self._client: Any | None = None
79
+
80
+ def _get_client(self) -> Any:
81
+ if self._client is None:
82
+ try:
83
+ from .api_transport import DEFAULT_TIMEOUT_SECONDS
84
+ from .client import TangleApiClient
85
+ except ModuleNotFoundError as exc:
86
+ if exc.name == "tangle_api":
87
+ raise SystemExit(
88
+ "Native generated Tangle API bindings are required for "
89
+ f"{self.command_name}. Install tangle-cli[native] or provide "
90
+ "a local tangle_api.generated package."
91
+ ) from exc
92
+ raise
93
+
94
+ kwargs = dict(self.client_kwargs)
95
+ kwargs.setdefault("timeout", DEFAULT_TIMEOUT_SECONDS)
96
+ self._client = TangleApiClient(**kwargs)
97
+ return self._client
98
+
99
+ def require_available(self) -> None:
100
+ """Materialize the client so CLI commands fail before native helper imports."""
101
+
102
+ self._get_client()
103
+
104
+ def __getattr__(self, name: str) -> Any:
105
+ return getattr(self._get_client(), name)
106
+
107
+
108
+ def include_env_credentials_for_args(args: ArgsContainer, cli_base_url: str | None) -> bool:
109
+ """Suppress ambient credentials when base_url came from config, not CLI.
110
+
111
+ Explicit config/CLI token/auth/header values remain present on *args* and are
112
+ passed through by callers. This helper only controls environment fallback.
113
+ """
114
+
115
+ config_base_url = getattr(args, "_config", {}).get("base_url")
116
+ return not (cli_base_url is None and config_base_url is not None)
@@ -0,0 +1,52 @@
1
+ """Shared Cyclopts option annotations for Tangle CLI commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated
6
+
7
+ from cyclopts import Parameter
8
+
9
+ from .api_transport import DEFAULT_API_URL
10
+
11
+ BaseUrlOption = Annotated[
12
+ str | None,
13
+ Parameter(
14
+ help=(
15
+ "Tangle API base URL. Defaults to TANGLE_API_URL, then "
16
+ f"{DEFAULT_API_URL}."
17
+ )
18
+ ),
19
+ ]
20
+ TokenOption = Annotated[
21
+ str | None,
22
+ Parameter(help="Bearer token. Defaults to TANGLE_API_TOKEN."),
23
+ ]
24
+ AuthHeaderOption = Annotated[
25
+ str | None,
26
+ Parameter(
27
+ help=(
28
+ "Authorization header value, e.g. 'Bearer TOKEN' or 'Basic BASE64'. "
29
+ "Defaults to TANGLE_API_AUTH_HEADER or TANGLE_AUTH_HEADER."
30
+ )
31
+ ),
32
+ ]
33
+ HeaderOption = Annotated[
34
+ list[str] | None,
35
+ Parameter(
36
+ name="--header",
37
+ alias="-H",
38
+ help=(
39
+ "Custom request header as 'Name: value'. Repeat for multiple. "
40
+ "Applied after TANGLE_API_HEADERS."
41
+ ),
42
+ negative_iterable=(),
43
+ ),
44
+ ]
45
+ ConfigOption = Annotated[
46
+ str | None,
47
+ Parameter(help="YAML/JSON config file providing command defaults."),
48
+ ]
49
+ LogTypeOption = Annotated[
50
+ str,
51
+ Parameter(help="Log output: console, none, file."),
52
+ ]