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,298 @@
1
+ """Generate Tangle component YAML files from local Python functions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any, Literal
7
+
8
+ import yaml
9
+
10
+ # Pin the default runtime image by digest so generated component YAML is reproducible.
11
+ # The tag documents the Python line; the digest pins the linux/amd64 image
12
+ # used by Tangle execution. Authors can still pass --image to choose a
13
+ # different runtime explicitly.
14
+ DEFAULT_CONTAINER_IMAGE = "python:3.12@sha256:b8163b64b37051de76577219aa4d5e9b95dc12a2e6c8cb438793c7adb3026016"
15
+
16
+
17
+ class ComponentGenerator:
18
+ """Generic Python-function component generation orchestration.
19
+
20
+ The heavy Python-function introspection and YAML construction lives in
21
+ :mod:`tangle_cli.component_from_func`. This class owns the surrounding
22
+ authoring workflow: dependency discovery, output-path derivation, existing
23
+ image reuse, partial-output cleanup, and logging. Downstreams should
24
+ subclass or compose this class rather than wrapping module globals.
25
+ """
26
+
27
+ default_container_image = DEFAULT_CONTAINER_IMAGE
28
+
29
+ def __init__(
30
+ self,
31
+ *,
32
+ logger: Any | None = None,
33
+ verbose: bool = False,
34
+ default_container_image: str | None = None,
35
+ ) -> None:
36
+ self.logger = logger
37
+ self.verbose = verbose
38
+ if default_container_image is not None:
39
+ self.default_container_image = default_container_image
40
+
41
+ def _log(self, message: str, *, err: bool = False) -> None:
42
+ if self.logger is not None:
43
+ log_method = getattr(self.logger, "error", None) if err else getattr(self.logger, "info", None)
44
+ if log_method is not None:
45
+ log_method(message)
46
+ return
47
+ if self.verbose:
48
+ print(message)
49
+
50
+ def find_dependencies_file(self, python_file: Path) -> Path | None:
51
+ """Find a dependency file for a Python component source file.
52
+
53
+ Looks for a component-specific TOML file next to the Python file, then a
54
+ ``pyproject.toml`` in the file's directory or up to three parent directories.
55
+ """
56
+
57
+ file_dir = python_file.parent
58
+ file_base = python_file.stem
59
+ toml_variations = [
60
+ file_dir / f"{file_base.replace('_', '-')}.toml",
61
+ file_dir / f"{file_base}.toml",
62
+ ]
63
+ for toml_file in toml_variations:
64
+ if toml_file.exists():
65
+ return toml_file
66
+
67
+ search_dirs = [
68
+ file_dir,
69
+ file_dir.parent,
70
+ file_dir.parent.parent,
71
+ file_dir.parent.parent.parent,
72
+ ]
73
+ for search_dir in search_dirs:
74
+ pyproject = search_dir / "pyproject.toml"
75
+ if pyproject.exists():
76
+ return pyproject
77
+ return None
78
+
79
+ def determine_output_path(
80
+ self,
81
+ input_file: Path,
82
+ output: Path | None = None,
83
+ output_is_dir: bool = False,
84
+ use_legacy_naming: bool = False,
85
+ ) -> Path:
86
+ """Determine the YAML output path for a generated component."""
87
+
88
+ base_name = input_file.stem.replace("_", "-")
89
+ if output:
90
+ output_name = base_name + ".yaml"
91
+ if output.is_dir() or output_is_dir or (not output.suffix and not output.exists()):
92
+ return output / output_name
93
+ return output
94
+
95
+ if use_legacy_naming:
96
+ legacy_name = input_file.stem + ".component.yaml"
97
+ output_dir = input_file.parent / "generated"
98
+ return output_dir / legacy_name
99
+
100
+ return input_file.parent / (base_name + ".yaml")
101
+
102
+ def extract_image_from_yaml(self, yaml_path: Path) -> str | None:
103
+ """Extract an existing component container image, if any."""
104
+
105
+ if not yaml_path.exists():
106
+ return None
107
+ try:
108
+ with yaml_path.open(encoding="utf-8") as f:
109
+ existing_yaml = yaml.safe_load(f)
110
+ impl = existing_yaml.get("implementation", {}) if isinstance(existing_yaml, dict) else {}
111
+ return impl.get("container", {}).get("image")
112
+ except Exception:
113
+ return None
114
+
115
+ def generate_component_yaml(
116
+ self,
117
+ *,
118
+ file_path: Path,
119
+ output_path: Path,
120
+ container_image: str,
121
+ function_name: str | None = None,
122
+ dependencies_from: Path | None = None,
123
+ mode: Literal["inline", "bundle"] = "inline",
124
+ custom_name: str | None = None,
125
+ custom_annotations: dict[str, str] | None = None,
126
+ strip_code: bool = False,
127
+ strip_source_path: bool = False,
128
+ resolve_root: Path | None = None,
129
+ emit_generation_annotations: bool = True,
130
+ ) -> bool:
131
+ """Generate component YAML from a Python function source file."""
132
+
133
+ from tangle_cli.component_from_func import generate_component_yaml
134
+
135
+ return generate_component_yaml(
136
+ file_path=file_path,
137
+ output_path=output_path,
138
+ container_image=container_image,
139
+ function_name=function_name,
140
+ dependencies_from=dependencies_from,
141
+ mode=mode,
142
+ custom_name=custom_name,
143
+ custom_annotations=custom_annotations,
144
+ strip_code=strip_code,
145
+ strip_source_path=strip_source_path,
146
+ resolve_root=resolve_root,
147
+ emit_generation_annotations=emit_generation_annotations,
148
+ )
149
+
150
+ def regenerate_yaml(
151
+ self,
152
+ python_file: Path,
153
+ output_path: Path | None = None,
154
+ function_name: str | None = None,
155
+ custom_name: str | None = None,
156
+ image: str | None = None,
157
+ dependencies_from: Path | None = None,
158
+ strip_code: bool = False,
159
+ strip_source_path: bool = False,
160
+ mode: str = "inline",
161
+ resolve_root: Path | None = None,
162
+ emit_generation_annotations: bool = True,
163
+ ) -> bool:
164
+ """Regenerate a YAML component from a Python function source file."""
165
+
166
+ if not python_file.exists():
167
+ self._log(f" ❌ File not found: {python_file}", err=True)
168
+ return False
169
+
170
+ final_output = output_path or self.determine_output_path(python_file)
171
+ resolved_image = image or self.extract_image_from_yaml(final_output) or self.default_container_image
172
+ deps_file = dependencies_from or self.find_dependencies_file(python_file)
173
+ if deps_file:
174
+ self._log(f" Found dependencies: {deps_file}")
175
+
176
+ final_output.parent.mkdir(parents=True, exist_ok=True)
177
+ return self.run_generation(
178
+ python_file=python_file,
179
+ final_output=final_output,
180
+ image=resolved_image,
181
+ func_name=function_name,
182
+ deps_file=deps_file,
183
+ custom_name=custom_name,
184
+ strip_code=strip_code,
185
+ strip_source_path=strip_source_path,
186
+ mode=mode,
187
+ resolve_root=resolve_root,
188
+ emit_generation_annotations=emit_generation_annotations,
189
+ )
190
+
191
+ def run_generation(
192
+ self,
193
+ *,
194
+ python_file: Path,
195
+ final_output: Path,
196
+ image: str,
197
+ func_name: str | None,
198
+ deps_file: Path | None,
199
+ custom_name: str | None,
200
+ strip_code: bool,
201
+ strip_source_path: bool,
202
+ mode: str = "inline",
203
+ resolve_root: Path | None = None,
204
+ emit_generation_annotations: bool = True,
205
+ ) -> bool:
206
+ """Execute component generation and clean up partial output on failure."""
207
+
208
+ try:
209
+ function_detail = f" function {func_name!r}" if func_name else ""
210
+ self._log(f" Generating component from {python_file.name}{function_detail}...")
211
+ success = self.generate_component_yaml(
212
+ file_path=python_file,
213
+ output_path=final_output,
214
+ container_image=image,
215
+ function_name=func_name,
216
+ dependencies_from=deps_file,
217
+ mode=mode, # type: ignore[arg-type]
218
+ custom_name=custom_name,
219
+ strip_code=strip_code,
220
+ strip_source_path=strip_source_path,
221
+ resolve_root=resolve_root,
222
+ emit_generation_annotations=emit_generation_annotations,
223
+ )
224
+ if not success:
225
+ self._log(" ❌ Failed to generate component", err=True)
226
+ return False
227
+ self._log(f" ✅ Generated: {final_output}")
228
+ return True
229
+ except Exception as exc:
230
+ if exc.__class__.__name__ == "AuthoringStripError":
231
+ if final_output.exists():
232
+ final_output.unlink()
233
+ raise
234
+ self._log(f" ❌ Error: {exc}", err=True)
235
+ if final_output.exists():
236
+ final_output.unlink()
237
+ return False
238
+
239
+
240
+ def find_dependencies_file(python_file: Path) -> Path | None:
241
+ """Find a dependency file for a Python component source file."""
242
+
243
+ return ComponentGenerator().find_dependencies_file(python_file)
244
+
245
+
246
+ def determine_output_path(
247
+ input_file: Path,
248
+ output: Path | None = None,
249
+ output_is_dir: bool = False,
250
+ use_legacy_naming: bool = False,
251
+ ) -> Path:
252
+ """Determine the YAML output path for a generated component."""
253
+
254
+ return ComponentGenerator().determine_output_path(
255
+ input_file,
256
+ output,
257
+ output_is_dir,
258
+ use_legacy_naming,
259
+ )
260
+
261
+
262
+ def regenerate_yaml(
263
+ python_file: Path,
264
+ output_path: Path | None = None,
265
+ function_name: str | None = None,
266
+ custom_name: str | None = None,
267
+ image: str | None = None,
268
+ dependencies_from: Path | None = None,
269
+ strip_code: bool = False,
270
+ strip_source_path: bool = False,
271
+ verbose: bool = False,
272
+ mode: str = "inline",
273
+ resolve_root: Path | None = None,
274
+ logger: Any | None = None,
275
+ ) -> bool:
276
+ """Regenerate a YAML component from a Python function source file."""
277
+
278
+ return ComponentGenerator(logger=logger, verbose=verbose).regenerate_yaml(
279
+ python_file=python_file,
280
+ output_path=output_path,
281
+ function_name=function_name,
282
+ custom_name=custom_name,
283
+ image=image,
284
+ dependencies_from=dependencies_from,
285
+ strip_code=strip_code,
286
+ strip_source_path=strip_source_path,
287
+ mode=mode,
288
+ resolve_root=resolve_root,
289
+ )
290
+
291
+
292
+ __all__ = [
293
+ "ComponentGenerator",
294
+ "DEFAULT_CONTAINER_IMAGE",
295
+ "determine_output_path",
296
+ "find_dependencies_file",
297
+ "regenerate_yaml",
298
+ ]