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.
- tangle_cli/__init__.py +19 -0
- tangle_cli/api_cli.py +787 -0
- tangle_cli/api_schema.py +633 -0
- tangle_cli/api_transport.py +461 -0
- tangle_cli/args_container.py +244 -0
- tangle_cli/artifacts.py +293 -0
- tangle_cli/artifacts_cli.py +108 -0
- tangle_cli/cli.py +57 -0
- tangle_cli/cli_helpers.py +116 -0
- tangle_cli/cli_options.py +52 -0
- tangle_cli/client.py +677 -0
- tangle_cli/component_from_func.py +1856 -0
- tangle_cli/component_generator.py +298 -0
- tangle_cli/component_inspector.py +494 -0
- tangle_cli/component_publisher.py +921 -0
- tangle_cli/components_cli.py +269 -0
- tangle_cli/dynamic_discovery_client.py +296 -0
- tangle_cli/generated_model_extensions.py +405 -0
- tangle_cli/generated_runtime.py +43 -0
- tangle_cli/handler.py +96 -0
- tangle_cli/hydration_trust.py +222 -0
- tangle_cli/logger.py +166 -0
- tangle_cli/models.py +407 -0
- tangle_cli/module_bundler.py +662 -0
- tangle_cli/openapi/__init__.py +0 -0
- tangle_cli/openapi/codegen.py +1090 -0
- tangle_cli/openapi/parser.py +77 -0
- tangle_cli/pipeline_dehydrator.py +720 -0
- tangle_cli/pipeline_hydrator.py +1785 -0
- tangle_cli/pipeline_run_annotations.py +41 -0
- tangle_cli/pipeline_run_details.py +203 -0
- tangle_cli/pipeline_run_manager.py +1994 -0
- tangle_cli/pipeline_run_search.py +712 -0
- tangle_cli/pipeline_runner.py +620 -0
- tangle_cli/pipeline_runs_cli.py +584 -0
- tangle_cli/pipelines.py +581 -0
- tangle_cli/pipelines_cli.py +271 -0
- tangle_cli/published_components_cli.py +373 -0
- tangle_cli/py.typed +0 -0
- tangle_cli/quickstart.py +110 -0
- tangle_cli/secrets.py +156 -0
- tangle_cli/secrets_cli.py +269 -0
- tangle_cli/utils.py +942 -0
- tangle_cli/version_manager.py +470 -0
- tangle_cli-0.0.1a1.dist-info/METADATA +561 -0
- tangle_cli-0.0.1a1.dist-info/RECORD +48 -0
- tangle_cli-0.0.1a1.dist-info/WHEEL +4 -0
- tangle_cli-0.0.1a1.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
"""Version bumping for Tangle component YAML and Python source files."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ast
|
|
6
|
+
import re
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import yaml
|
|
12
|
+
|
|
13
|
+
from tangle_cli import utils
|
|
14
|
+
from tangle_cli.component_from_func import extract_file_metadata, find_function_in_source
|
|
15
|
+
from tangle_cli.component_generator import ComponentGenerator
|
|
16
|
+
from tangle_cli.logger import Logger, get_default_logger
|
|
17
|
+
|
|
18
|
+
ReferenceContentGetter = Callable[[str], str | None]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class VersionManager:
|
|
22
|
+
"""Manage version updates for component YAML and Python source files."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, logger: Logger | None = None) -> None:
|
|
25
|
+
self.log = logger or get_default_logger()
|
|
26
|
+
|
|
27
|
+
def parse_version(self, version_str: str) -> tuple[int, ...]:
|
|
28
|
+
"""Parse a major/minor[/patch] version string into integer parts."""
|
|
29
|
+
|
|
30
|
+
parts = str(version_str).strip().strip("\"'").split(".")
|
|
31
|
+
if len(parts) == 1:
|
|
32
|
+
return (int(parts[0]), 0)
|
|
33
|
+
if len(parts) == 2:
|
|
34
|
+
return (int(parts[0]), int(parts[1]))
|
|
35
|
+
return (int(parts[0]), int(parts[1]), int(parts[2]))
|
|
36
|
+
|
|
37
|
+
def increment_version(self, version_str: str) -> str:
|
|
38
|
+
"""Increment patch for x.y.z versions, otherwise increment minor."""
|
|
39
|
+
|
|
40
|
+
parts = self.parse_version(version_str)
|
|
41
|
+
if len(parts) == 3:
|
|
42
|
+
return f"{parts[0]}.{parts[1]}.{parts[2] + 1}"
|
|
43
|
+
return f"{parts[0]}.{parts[1] + 1}"
|
|
44
|
+
|
|
45
|
+
def _get_yaml_version(self, content: str) -> str | None:
|
|
46
|
+
try:
|
|
47
|
+
data = yaml.safe_load(content)
|
|
48
|
+
return utils.get_version_from_data(data)
|
|
49
|
+
except Exception:
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
def update_yaml_file(
|
|
53
|
+
self,
|
|
54
|
+
file_path: str,
|
|
55
|
+
new_version: str | None = None,
|
|
56
|
+
reference_content_getter: ReferenceContentGetter | None = None,
|
|
57
|
+
update_timestamp: bool = False,
|
|
58
|
+
) -> bool:
|
|
59
|
+
"""Update version metadata in a YAML component file."""
|
|
60
|
+
|
|
61
|
+
with open(file_path, encoding="utf-8") as f:
|
|
62
|
+
content = f.read()
|
|
63
|
+
data = yaml.safe_load(content) or {}
|
|
64
|
+
old_version = utils.get_version_from_data(data)
|
|
65
|
+
|
|
66
|
+
if new_version is None:
|
|
67
|
+
ref_version = None
|
|
68
|
+
if reference_content_getter:
|
|
69
|
+
ref_content = reference_content_getter(file_path)
|
|
70
|
+
if ref_content:
|
|
71
|
+
ref_version = self._get_yaml_version(ref_content)
|
|
72
|
+
if ref_version:
|
|
73
|
+
new_version = self.increment_version(ref_version)
|
|
74
|
+
self.log.info(f" 📊 Reference version: {ref_version} → bumping to {new_version}")
|
|
75
|
+
if new_version is None:
|
|
76
|
+
if old_version:
|
|
77
|
+
new_version = self.increment_version(old_version)
|
|
78
|
+
self.log.info(f" 📊 Local version: {old_version} → bumping to {new_version}")
|
|
79
|
+
else:
|
|
80
|
+
new_version = "0.1"
|
|
81
|
+
self.log.info(" 📝 No existing version - using 0.1")
|
|
82
|
+
else:
|
|
83
|
+
parts = self.parse_version(new_version)
|
|
84
|
+
new_version = ".".join(str(part) for part in parts)
|
|
85
|
+
|
|
86
|
+
self.log.info(f" {Path(file_path).name}:")
|
|
87
|
+
self.log.info(f" Current version: {old_version or 'none'}")
|
|
88
|
+
self.log.info(f" New version: {new_version}")
|
|
89
|
+
|
|
90
|
+
if not isinstance(data, dict):
|
|
91
|
+
self.log.warn(" ⚠️ Could not update YAML - root value is not a mapping")
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
if "metadata" not in data or data["metadata"] is None:
|
|
95
|
+
data["metadata"] = {}
|
|
96
|
+
if not isinstance(data["metadata"], dict):
|
|
97
|
+
self.log.warn(" ⚠️ Could not update YAML - metadata is not a mapping")
|
|
98
|
+
return False
|
|
99
|
+
if "annotations" not in data["metadata"] or data["metadata"]["annotations"] is None:
|
|
100
|
+
data["metadata"]["annotations"] = {}
|
|
101
|
+
if not isinstance(data["metadata"]["annotations"], dict):
|
|
102
|
+
self.log.warn(" ⚠️ Could not update YAML - metadata.annotations is not a mapping")
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
annotations = data["metadata"]["annotations"]
|
|
106
|
+
annotations["version"] = new_version
|
|
107
|
+
data.pop("version", None)
|
|
108
|
+
if update_timestamp:
|
|
109
|
+
timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
110
|
+
self.log.info(f" Timestamp: {timestamp}")
|
|
111
|
+
annotations["updated_at"] = timestamp
|
|
112
|
+
else:
|
|
113
|
+
existing_timestamp = data.get("updated_at") or annotations.get("updated_at")
|
|
114
|
+
if existing_timestamp:
|
|
115
|
+
annotations["updated_at"] = existing_timestamp
|
|
116
|
+
data.pop("updated_at", None)
|
|
117
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
|
118
|
+
f.write(utils.dump_yaml(data))
|
|
119
|
+
|
|
120
|
+
self.log.info(" ✅ Updated")
|
|
121
|
+
return True
|
|
122
|
+
|
|
123
|
+
def update_python_file(
|
|
124
|
+
self,
|
|
125
|
+
python_file: str,
|
|
126
|
+
new_version: str | None = None,
|
|
127
|
+
reference_content_getter: ReferenceContentGetter | None = None,
|
|
128
|
+
update_timestamp: bool = False,
|
|
129
|
+
function_name: str | None = None,
|
|
130
|
+
) -> bool:
|
|
131
|
+
"""Update a Python component function docstring Metadata section."""
|
|
132
|
+
|
|
133
|
+
python_path = Path(python_file)
|
|
134
|
+
if function_name and not _has_exact_public_function(python_path, function_name):
|
|
135
|
+
self.log.warn(f" ⚠️ Function '{function_name}' not found in {python_path.name}")
|
|
136
|
+
return False
|
|
137
|
+
metadata, actual_func_name = extract_file_metadata(python_path, function_name)
|
|
138
|
+
if not actual_func_name:
|
|
139
|
+
self.log.warn(f" ⚠️ No function found in {python_path.name}")
|
|
140
|
+
return False
|
|
141
|
+
|
|
142
|
+
current_version = metadata.get("version")
|
|
143
|
+
if new_version:
|
|
144
|
+
final_version = new_version
|
|
145
|
+
else:
|
|
146
|
+
ref_version = None
|
|
147
|
+
if reference_content_getter:
|
|
148
|
+
ref_content = reference_content_getter(python_file)
|
|
149
|
+
if ref_content:
|
|
150
|
+
import tempfile
|
|
151
|
+
|
|
152
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as tmp:
|
|
153
|
+
tmp.write(ref_content)
|
|
154
|
+
tmp_path = Path(tmp.name)
|
|
155
|
+
try:
|
|
156
|
+
ref_metadata, _ = extract_file_metadata(tmp_path, actual_func_name)
|
|
157
|
+
ref_version = ref_metadata.get("version")
|
|
158
|
+
finally:
|
|
159
|
+
tmp_path.unlink()
|
|
160
|
+
if ref_version:
|
|
161
|
+
final_version = self.increment_version(ref_version)
|
|
162
|
+
self.log.info(f" 📊 Reference version: {ref_version} → bumping to {final_version}")
|
|
163
|
+
elif current_version:
|
|
164
|
+
final_version = self.increment_version(current_version)
|
|
165
|
+
self.log.info(f" 📊 Local version: {current_version} → bumping to {final_version}")
|
|
166
|
+
else:
|
|
167
|
+
final_version = "0.1"
|
|
168
|
+
self.log.info(" 📝 No existing version - using 0.1")
|
|
169
|
+
|
|
170
|
+
current_timestamp = (
|
|
171
|
+
datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
172
|
+
if update_timestamp
|
|
173
|
+
else None
|
|
174
|
+
)
|
|
175
|
+
self.log.info(f" {python_path.name}:")
|
|
176
|
+
self.log.info(f" Current version: {current_version or 'none'}")
|
|
177
|
+
self.log.info(f" New version: {final_version}")
|
|
178
|
+
if current_timestamp:
|
|
179
|
+
self.log.info(f" Timestamp: {current_timestamp}")
|
|
180
|
+
|
|
181
|
+
with open(python_file, encoding="utf-8") as f:
|
|
182
|
+
content = f.read()
|
|
183
|
+
new_content = self._update_function_docstring_metadata(
|
|
184
|
+
python_path,
|
|
185
|
+
content,
|
|
186
|
+
actual_func_name,
|
|
187
|
+
final_version,
|
|
188
|
+
current_timestamp,
|
|
189
|
+
)
|
|
190
|
+
if new_content == content:
|
|
191
|
+
self.log.warn(" ⚠️ Could not update docstring - no Metadata section found")
|
|
192
|
+
return False
|
|
193
|
+
with open(python_file, "w", encoding="utf-8") as f:
|
|
194
|
+
f.write(new_content)
|
|
195
|
+
self.log.info(" ✅ Updated")
|
|
196
|
+
return True
|
|
197
|
+
|
|
198
|
+
def _update_function_docstring_metadata(
|
|
199
|
+
self,
|
|
200
|
+
python_path: Path,
|
|
201
|
+
content: str,
|
|
202
|
+
function_name: str,
|
|
203
|
+
version: str,
|
|
204
|
+
timestamp: str | None = None,
|
|
205
|
+
) -> str:
|
|
206
|
+
_, func_node = find_function_in_source(python_path, function_name)
|
|
207
|
+
if not func_node or not func_node.body:
|
|
208
|
+
return content
|
|
209
|
+
doc_node = func_node.body[0]
|
|
210
|
+
value = getattr(doc_node, "value", None)
|
|
211
|
+
if not (
|
|
212
|
+
getattr(doc_node, "lineno", None)
|
|
213
|
+
and getattr(doc_node, "end_lineno", None)
|
|
214
|
+
and isinstance(getattr(value, "value", None), str)
|
|
215
|
+
):
|
|
216
|
+
return content
|
|
217
|
+
|
|
218
|
+
lines = content.splitlines(keepends=True)
|
|
219
|
+
start = doc_node.lineno - 1
|
|
220
|
+
end = doc_node.end_lineno
|
|
221
|
+
docstring_source = "".join(lines[start:end])
|
|
222
|
+
updated_docstring = self._update_docstring_metadata(docstring_source, version, timestamp)
|
|
223
|
+
if updated_docstring == docstring_source:
|
|
224
|
+
return content
|
|
225
|
+
return "".join([*lines[:start], updated_docstring, *lines[end:]])
|
|
226
|
+
|
|
227
|
+
def _update_docstring_metadata(
|
|
228
|
+
self,
|
|
229
|
+
content: str,
|
|
230
|
+
version: str,
|
|
231
|
+
timestamp: str | None = None,
|
|
232
|
+
) -> str:
|
|
233
|
+
metadata_pattern = re.compile(
|
|
234
|
+
r"(Metadata:\s*\n)"
|
|
235
|
+
r"(\s+)"
|
|
236
|
+
r"(?:.*?\n)*?"
|
|
237
|
+
r"(?=\s*(?:Args:|Returns:|Raises:|Yields:|Note:|Example:|\"\"\"|\'\'\')|\Z)",
|
|
238
|
+
re.IGNORECASE | re.MULTILINE,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
def replace_metadata(match: re.Match) -> str:
|
|
242
|
+
header = match.group(1)
|
|
243
|
+
indent = match.group(2)
|
|
244
|
+
result = f"{header}{indent}version: {version}\n"
|
|
245
|
+
if timestamp:
|
|
246
|
+
result += f"{indent}updated_at: {timestamp}\n"
|
|
247
|
+
return result
|
|
248
|
+
|
|
249
|
+
return metadata_pattern.sub(replace_metadata, content, count=1)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _has_exact_public_function(python_path: Path, function_name: str) -> bool:
|
|
253
|
+
"""Return whether *python_path* defines exactly this public function."""
|
|
254
|
+
|
|
255
|
+
try:
|
|
256
|
+
tree = ast.parse(python_path.read_text(encoding="utf-8"))
|
|
257
|
+
except (OSError, SyntaxError):
|
|
258
|
+
return False
|
|
259
|
+
return any(
|
|
260
|
+
isinstance(node, ast.FunctionDef) and node.name == function_name and not node.name.startswith("_")
|
|
261
|
+
for node in ast.iter_child_nodes(tree)
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _common_generation_dir(yaml_path: Path, annotations: dict[str, str]) -> Path | None:
|
|
266
|
+
component_yaml_path = annotations.get("component_yaml_path")
|
|
267
|
+
if not component_yaml_path:
|
|
268
|
+
return None
|
|
269
|
+
yaml_rel = Path(component_yaml_path)
|
|
270
|
+
if yaml_rel.is_absolute():
|
|
271
|
+
return None
|
|
272
|
+
common_dir = yaml_path.resolve().parent
|
|
273
|
+
for part in yaml_rel.parent.parts:
|
|
274
|
+
if part not in ("", "."):
|
|
275
|
+
common_dir = common_dir.parent
|
|
276
|
+
return common_dir
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _resolve_annotated_path(yaml_path: Path, annotations: dict[str, str], annotation_key: str) -> Path | None:
|
|
280
|
+
raw_path = annotations.get(annotation_key)
|
|
281
|
+
if not raw_path:
|
|
282
|
+
return None
|
|
283
|
+
annotated_path = Path(raw_path)
|
|
284
|
+
if annotated_path.is_absolute():
|
|
285
|
+
return annotated_path if annotated_path.exists() else None
|
|
286
|
+
|
|
287
|
+
candidates: list[Path] = []
|
|
288
|
+
common_dir = _common_generation_dir(yaml_path, annotations)
|
|
289
|
+
if common_dir:
|
|
290
|
+
candidates.append(common_dir / annotated_path)
|
|
291
|
+
candidates.append(yaml_path.parent / annotated_path)
|
|
292
|
+
|
|
293
|
+
seen: set[Path] = set()
|
|
294
|
+
for candidate in candidates:
|
|
295
|
+
resolved = candidate.resolve()
|
|
296
|
+
if resolved in seen:
|
|
297
|
+
continue
|
|
298
|
+
seen.add(resolved)
|
|
299
|
+
if resolved.exists():
|
|
300
|
+
return resolved
|
|
301
|
+
return None
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _resolve_python_source_path(yaml_path: Path, annotations: dict[str, str]) -> Path | None:
|
|
305
|
+
"""Resolve a component YAML's annotated Python source path.
|
|
306
|
+
|
|
307
|
+
New generated YAML records both ``python_original_code_path`` and
|
|
308
|
+
``component_yaml_path`` relative to a common ancestor. Older YAML may store
|
|
309
|
+
only the source basename, sometimes beside the YAML or under a sibling
|
|
310
|
+
``sources`` directory. Try the structured common-ancestor form first, then
|
|
311
|
+
legacy locations.
|
|
312
|
+
"""
|
|
313
|
+
|
|
314
|
+
raw_python_path = annotations.get("python_original_code_path")
|
|
315
|
+
if not raw_python_path:
|
|
316
|
+
return None
|
|
317
|
+
|
|
318
|
+
python_path = Path(raw_python_path)
|
|
319
|
+
if python_path.is_absolute():
|
|
320
|
+
return python_path if python_path.exists() else None
|
|
321
|
+
|
|
322
|
+
candidates: list[Path] = []
|
|
323
|
+
common_dir = _common_generation_dir(yaml_path, annotations)
|
|
324
|
+
if common_dir:
|
|
325
|
+
candidates.append(common_dir / python_path)
|
|
326
|
+
|
|
327
|
+
yaml_dir = yaml_path.parent
|
|
328
|
+
candidates.extend(
|
|
329
|
+
[
|
|
330
|
+
yaml_dir / python_path,
|
|
331
|
+
yaml_dir / "sources" / python_path.name,
|
|
332
|
+
yaml_dir / python_path.name,
|
|
333
|
+
]
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
seen: set[Path] = set()
|
|
337
|
+
for candidate in candidates:
|
|
338
|
+
resolved = candidate.resolve()
|
|
339
|
+
if resolved in seen:
|
|
340
|
+
continue
|
|
341
|
+
seen.add(resolved)
|
|
342
|
+
if resolved.exists():
|
|
343
|
+
return resolved
|
|
344
|
+
return None
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def bump_version(
|
|
348
|
+
yaml_file: str | Path,
|
|
349
|
+
set_version: str | None = None,
|
|
350
|
+
reference_content_getter: ReferenceContentGetter | None = None,
|
|
351
|
+
update_timestamp: bool = False,
|
|
352
|
+
logger: Logger | None = None,
|
|
353
|
+
) -> dict[str, str | None]:
|
|
354
|
+
"""Bump component version in a YAML file.
|
|
355
|
+
|
|
356
|
+
If the YAML references a local Python source via
|
|
357
|
+
``metadata.annotations.python_original_code_path``, updates that source and
|
|
358
|
+
regenerates the YAML. Otherwise updates YAML metadata directly.
|
|
359
|
+
"""
|
|
360
|
+
|
|
361
|
+
log = logger or get_default_logger()
|
|
362
|
+
yaml_path = Path(yaml_file)
|
|
363
|
+
if not yaml_path.exists():
|
|
364
|
+
log.error(f"❌ File not found: {yaml_file}")
|
|
365
|
+
return {"status": "failed", "error": f"File not found: {yaml_file}"}
|
|
366
|
+
if yaml_path.suffix not in [".yaml", ".yml"]:
|
|
367
|
+
log.error(f"❌ Not a YAML file: {yaml_file}")
|
|
368
|
+
return {"status": "failed", "error": f"Not a YAML file: {yaml_file}"}
|
|
369
|
+
|
|
370
|
+
version_manager = VersionManager(logger=log)
|
|
371
|
+
with open(yaml_path, encoding="utf-8") as f:
|
|
372
|
+
yaml_content = yaml.safe_load(f) or {}
|
|
373
|
+
old_version = utils.get_version_from_data(yaml_content)
|
|
374
|
+
|
|
375
|
+
annotations: dict[str, str] = {}
|
|
376
|
+
metadata = yaml_content.get("metadata") if isinstance(yaml_content, dict) else None
|
|
377
|
+
if isinstance(metadata, dict) and isinstance(metadata.get("annotations"), dict):
|
|
378
|
+
annotations = metadata["annotations"]
|
|
379
|
+
python_path = annotations.get("python_original_code_path")
|
|
380
|
+
has_original_code = "python_original_code" in annotations
|
|
381
|
+
generation_function_name = annotations.get("tangle_cli_generation_function_name")
|
|
382
|
+
generation_mode = annotations.get("tangle_cli_generation_mode") or (
|
|
383
|
+
"bundle" if annotations.get("bundled_modules") else "inline"
|
|
384
|
+
)
|
|
385
|
+
if generation_mode not in {"inline", "bundle"}:
|
|
386
|
+
error = f"Unsupported generation mode: {generation_mode}"
|
|
387
|
+
log.error(f"❌ {error}")
|
|
388
|
+
return {"status": "failed", "yaml_file": str(yaml_path), "error": error}
|
|
389
|
+
custom_name = (
|
|
390
|
+
yaml_content.get("name")
|
|
391
|
+
if isinstance(yaml_content, dict) and isinstance(yaml_content.get("name"), str)
|
|
392
|
+
else None
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
dependencies_from = None
|
|
396
|
+
if annotations.get("tangle_cli_generation_dependencies_from"):
|
|
397
|
+
dependencies_from = _resolve_annotated_path(
|
|
398
|
+
yaml_path,
|
|
399
|
+
annotations,
|
|
400
|
+
"tangle_cli_generation_dependencies_from",
|
|
401
|
+
)
|
|
402
|
+
if dependencies_from is None:
|
|
403
|
+
error = f"Dependency file not found: {annotations['tangle_cli_generation_dependencies_from']}"
|
|
404
|
+
log.error(f"❌ {error}")
|
|
405
|
+
return {"status": "failed", "yaml_file": str(yaml_path), "error": error}
|
|
406
|
+
|
|
407
|
+
resolve_root = None
|
|
408
|
+
if annotations.get("tangle_cli_generation_resolve_root"):
|
|
409
|
+
resolve_root = _resolve_annotated_path(
|
|
410
|
+
yaml_path,
|
|
411
|
+
annotations,
|
|
412
|
+
"tangle_cli_generation_resolve_root",
|
|
413
|
+
)
|
|
414
|
+
if resolve_root is None:
|
|
415
|
+
error = f"Resolve root not found: {annotations['tangle_cli_generation_resolve_root']}"
|
|
416
|
+
log.error(f"❌ {error}")
|
|
417
|
+
return {"status": "failed", "yaml_file": str(yaml_path), "error": error}
|
|
418
|
+
|
|
419
|
+
if python_path:
|
|
420
|
+
python_full_path = _resolve_python_source_path(yaml_path, annotations)
|
|
421
|
+
if python_full_path:
|
|
422
|
+
log.info(f" 📍 Found Python source: {python_full_path.name}")
|
|
423
|
+
success = version_manager.update_python_file(
|
|
424
|
+
str(python_full_path),
|
|
425
|
+
new_version=set_version,
|
|
426
|
+
reference_content_getter=reference_content_getter,
|
|
427
|
+
update_timestamp=update_timestamp,
|
|
428
|
+
function_name=generation_function_name,
|
|
429
|
+
)
|
|
430
|
+
if success:
|
|
431
|
+
log.info(" 🔄 Regenerating YAML...")
|
|
432
|
+
success = ComponentGenerator(logger=log).regenerate_yaml(
|
|
433
|
+
python_full_path,
|
|
434
|
+
output_path=yaml_path,
|
|
435
|
+
function_name=generation_function_name,
|
|
436
|
+
custom_name=custom_name,
|
|
437
|
+
dependencies_from=dependencies_from,
|
|
438
|
+
strip_code=not has_original_code,
|
|
439
|
+
mode=generation_mode,
|
|
440
|
+
resolve_root=resolve_root,
|
|
441
|
+
)
|
|
442
|
+
else:
|
|
443
|
+
log.error(f"❌ Python source not found: {python_path}")
|
|
444
|
+
return {
|
|
445
|
+
"status": "failed",
|
|
446
|
+
"yaml_file": str(yaml_path),
|
|
447
|
+
"error": f"Python source not found: {python_path}",
|
|
448
|
+
}
|
|
449
|
+
else:
|
|
450
|
+
success = version_manager.update_yaml_file(
|
|
451
|
+
str(yaml_path),
|
|
452
|
+
new_version=set_version,
|
|
453
|
+
reference_content_getter=reference_content_getter,
|
|
454
|
+
update_timestamp=update_timestamp,
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
if not success:
|
|
458
|
+
return {"status": "failed", "yaml_file": str(yaml_path), "error": "Version update failed"}
|
|
459
|
+
|
|
460
|
+
with open(yaml_path, encoding="utf-8") as f:
|
|
461
|
+
new_version = utils.get_version_from_data(yaml.safe_load(f))
|
|
462
|
+
return {
|
|
463
|
+
"status": "success",
|
|
464
|
+
"yaml_file": str(yaml_path),
|
|
465
|
+
"old_version": old_version,
|
|
466
|
+
"new_version": new_version,
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
__all__ = ["ReferenceContentGetter", "VersionManager", "bump_version"]
|