composable-data-stack 0.4.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.
- cli/__init__.py +1 -0
- cli/diagnostics.py +13 -0
- cli/graph.py +58 -0
- cli/image_updates.py +326 -0
- cli/image_verification.py +484 -0
- cli/loader.py +180 -0
- cli/main.py +1656 -0
- cli/overlay.py +239 -0
- cli/planner.py +618 -0
- cli/preflight.py +418 -0
- cli/renderer.py +791 -0
- cli/resolver.py +28 -0
- cli/resources/__init__.py +1 -0
- cli/resources/rule-schema.json +274 -0
- cli/resources/rule-set.json +919 -0
- cli/secrets.py +169 -0
- cli/security.py +768 -0
- cli/security_common.py +41 -0
- cli/state.py +112 -0
- cli/up_runner.py +257 -0
- cli/validator.py +570 -0
- composable_data_stack-0.4.0.dist-info/METADATA +872 -0
- composable_data_stack-0.4.0.dist-info/RECORD +27 -0
- composable_data_stack-0.4.0.dist-info/WHEEL +5 -0
- composable_data_stack-0.4.0.dist-info/entry_points.txt +2 -0
- composable_data_stack-0.4.0.dist-info/licenses/LICENSE +201 -0
- composable_data_stack-0.4.0.dist-info/top_level.txt +1 -0
cli/overlay.py
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# cli/overlay.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .diagnostics import Diagnostic
|
|
8
|
+
from .loader import _is_within, load_yaml_file
|
|
9
|
+
from .validator import validate_loaded_profile
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _duplicate_module_ids(modules: list[dict[str, Any]]) -> set[str]:
|
|
13
|
+
seen: set[str] = set()
|
|
14
|
+
dupes: set[str] = set()
|
|
15
|
+
for m in modules:
|
|
16
|
+
if not isinstance(m, dict):
|
|
17
|
+
continue
|
|
18
|
+
mid = m.get("id")
|
|
19
|
+
if mid is None:
|
|
20
|
+
continue
|
|
21
|
+
if mid in seen:
|
|
22
|
+
dupes.add(mid)
|
|
23
|
+
seen.add(mid)
|
|
24
|
+
return dupes
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _merge_value(
|
|
28
|
+
base: Any,
|
|
29
|
+
overlay: Any,
|
|
30
|
+
base_source: str,
|
|
31
|
+
overlay_source: str,
|
|
32
|
+
path: str,
|
|
33
|
+
provenance: dict[str, str],
|
|
34
|
+
) -> Any:
|
|
35
|
+
if isinstance(base, dict) and isinstance(overlay, dict):
|
|
36
|
+
result: dict[str, Any] = {}
|
|
37
|
+
for key, value in base.items():
|
|
38
|
+
child_path = f"{path}.{key}" if path else key
|
|
39
|
+
result[key] = value
|
|
40
|
+
provenance.setdefault(child_path, base_source)
|
|
41
|
+
for key, value in overlay.items():
|
|
42
|
+
child_path = f"{path}.{key}" if path else key
|
|
43
|
+
if key in result:
|
|
44
|
+
result[key] = _merge_value(
|
|
45
|
+
result[key], value, base_source, overlay_source, child_path, provenance
|
|
46
|
+
)
|
|
47
|
+
else:
|
|
48
|
+
result[key] = value
|
|
49
|
+
provenance[child_path] = overlay_source
|
|
50
|
+
return result
|
|
51
|
+
|
|
52
|
+
provenance[path] = overlay_source
|
|
53
|
+
return overlay
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _merge_modules(
|
|
57
|
+
base_modules: list[dict[str, Any]],
|
|
58
|
+
overlay_modules: list[dict[str, Any]],
|
|
59
|
+
base_source: str,
|
|
60
|
+
overlay_source: str,
|
|
61
|
+
provenance: dict[str, str],
|
|
62
|
+
) -> list[dict[str, Any]]:
|
|
63
|
+
by_id: dict[str, dict[str, Any]] = {}
|
|
64
|
+
order: list[str] = []
|
|
65
|
+
|
|
66
|
+
for module in base_modules:
|
|
67
|
+
mid = module["id"]
|
|
68
|
+
by_id[mid] = module
|
|
69
|
+
order.append(mid)
|
|
70
|
+
provenance[f"spec.modules[{mid}]"] = base_source
|
|
71
|
+
|
|
72
|
+
for module in overlay_modules:
|
|
73
|
+
mid = module["id"]
|
|
74
|
+
if mid in by_id:
|
|
75
|
+
by_id[mid] = _merge_value(
|
|
76
|
+
by_id[mid], module, base_source, overlay_source, f"spec.modules[{mid}]", provenance
|
|
77
|
+
)
|
|
78
|
+
else:
|
|
79
|
+
by_id[mid] = module
|
|
80
|
+
order.append(mid)
|
|
81
|
+
provenance[f"spec.modules[{mid}]"] = overlay_source
|
|
82
|
+
|
|
83
|
+
return [by_id[mid] for mid in order]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def resolve_profile(
|
|
87
|
+
profile_path: str,
|
|
88
|
+
environment: str | None = None,
|
|
89
|
+
) -> tuple[dict[str, Any] | None, dict[str, str], list[Diagnostic]]:
|
|
90
|
+
"""
|
|
91
|
+
Loads and, if an environment is selected, merges the profile at
|
|
92
|
+
profile_path with profiles/<name>/environments/<environment>.yaml.
|
|
93
|
+
|
|
94
|
+
Returns (resolved_profile, provenance, diagnostics). provenance maps
|
|
95
|
+
dotted config paths (and "spec.modules[<id>]" for whole module entries)
|
|
96
|
+
to the source file responsible for that value. resolved_profile is
|
|
97
|
+
None if resolution or validation failed (see diagnostics).
|
|
98
|
+
|
|
99
|
+
environment=None (the default) reproduces the exact behavior of
|
|
100
|
+
load_yaml_file + validate_loaded_profile on the base profile alone,
|
|
101
|
+
standalone profiles are unaffected by this resolver existing.
|
|
102
|
+
"""
|
|
103
|
+
profile_file = Path(profile_path)
|
|
104
|
+
base, diagnostics = load_yaml_file(profile_file)
|
|
105
|
+
if base is None:
|
|
106
|
+
return None, {}, diagnostics
|
|
107
|
+
|
|
108
|
+
if environment is None:
|
|
109
|
+
diagnostics += validate_loaded_profile(base, profile_file)
|
|
110
|
+
provenance = {}
|
|
111
|
+
return (base, provenance, diagnostics) if not any(
|
|
112
|
+
d.level == "error" for d in diagnostics
|
|
113
|
+
) else (None, provenance, diagnostics)
|
|
114
|
+
|
|
115
|
+
profile_dir = profile_file.parent
|
|
116
|
+
environments_dir = profile_dir / "environments"
|
|
117
|
+
overlay_file = environments_dir / f"{environment}.yaml"
|
|
118
|
+
|
|
119
|
+
if not _is_within(overlay_file, environments_dir):
|
|
120
|
+
diagnostics.append(
|
|
121
|
+
Diagnostic(
|
|
122
|
+
level="error",
|
|
123
|
+
code="E090",
|
|
124
|
+
message=f'Environment "{environment}" resolves outside the profile\'s environments/ directory.',
|
|
125
|
+
path="environment",
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
return None, {}, diagnostics
|
|
129
|
+
|
|
130
|
+
if not overlay_file.is_file():
|
|
131
|
+
diagnostics.append(
|
|
132
|
+
Diagnostic(
|
|
133
|
+
level="error",
|
|
134
|
+
code="E091",
|
|
135
|
+
message=f'Unknown environment "{environment}": {overlay_file} does not exist.',
|
|
136
|
+
path="environment",
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
return None, {}, diagnostics
|
|
140
|
+
|
|
141
|
+
overlay, overlay_diags = load_yaml_file(overlay_file)
|
|
142
|
+
diagnostics += overlay_diags
|
|
143
|
+
if overlay is None:
|
|
144
|
+
return None, {}, diagnostics
|
|
145
|
+
|
|
146
|
+
base_source = str(profile_file)
|
|
147
|
+
overlay_source = str(overlay_file)
|
|
148
|
+
|
|
149
|
+
base_spec = base.get("spec", {})
|
|
150
|
+
base_modules = base_spec.get("modules", []) if isinstance(base_spec, dict) else []
|
|
151
|
+
overlay_spec = overlay.get("spec", {})
|
|
152
|
+
overlay_modules = overlay_spec.get("modules", []) if isinstance(overlay_spec, dict) else []
|
|
153
|
+
|
|
154
|
+
for label, modules in (("base profile", base_modules), (f"overlay {overlay_source}", overlay_modules)):
|
|
155
|
+
if not isinstance(modules, list):
|
|
156
|
+
diagnostics.append(
|
|
157
|
+
Diagnostic(
|
|
158
|
+
level="error",
|
|
159
|
+
code="E093",
|
|
160
|
+
message=f"spec.modules in {label} must be a list, got {type(modules).__name__}.",
|
|
161
|
+
path="spec.modules",
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
continue
|
|
165
|
+
|
|
166
|
+
non_dict_indices = [i for i, m in enumerate(modules) if not isinstance(m, dict)]
|
|
167
|
+
if non_dict_indices:
|
|
168
|
+
diagnostics.append(
|
|
169
|
+
Diagnostic(
|
|
170
|
+
level="error",
|
|
171
|
+
code="E093",
|
|
172
|
+
message=(
|
|
173
|
+
f"Module entr{'y' if len(non_dict_indices) == 1 else 'ies'} in {label} "
|
|
174
|
+
f"must be a mapping, not a scalar/list, at index {non_dict_indices}."
|
|
175
|
+
),
|
|
176
|
+
path="spec.modules",
|
|
177
|
+
)
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
if any(d.level == "error" for d in diagnostics):
|
|
181
|
+
return None, {}, diagnostics
|
|
182
|
+
|
|
183
|
+
for label, modules in (("base profile", base_modules), (f"overlay {overlay_source}", overlay_modules)):
|
|
184
|
+
missing_id = [i for i, m in enumerate(modules) if not m.get("id")]
|
|
185
|
+
if missing_id:
|
|
186
|
+
diagnostics.append(
|
|
187
|
+
Diagnostic(
|
|
188
|
+
level="error",
|
|
189
|
+
code="E093",
|
|
190
|
+
message=f"Module entr{'y' if len(missing_id) == 1 else 'ies'} in {label} missing required 'id' at index {missing_id}.",
|
|
191
|
+
path="spec.modules",
|
|
192
|
+
)
|
|
193
|
+
)
|
|
194
|
+
if any(d.level == "error" for d in diagnostics):
|
|
195
|
+
return None, {}, diagnostics
|
|
196
|
+
|
|
197
|
+
for label, modules in (("base profile", base_modules), (f"overlay {overlay_source}", overlay_modules)):
|
|
198
|
+
dupes = _duplicate_module_ids(modules)
|
|
199
|
+
if dupes:
|
|
200
|
+
diagnostics.append(
|
|
201
|
+
Diagnostic(
|
|
202
|
+
level="error",
|
|
203
|
+
code="E093",
|
|
204
|
+
message=f"Duplicate module id(s) in {label}: {sorted(dupes)}.",
|
|
205
|
+
path="spec.modules",
|
|
206
|
+
)
|
|
207
|
+
)
|
|
208
|
+
if any(d.level == "error" for d in diagnostics):
|
|
209
|
+
return None, {}, diagnostics
|
|
210
|
+
|
|
211
|
+
provenance: dict[str, str] = {}
|
|
212
|
+
|
|
213
|
+
base_without_modules = dict(base)
|
|
214
|
+
base_spec_val = base.get("spec", {})
|
|
215
|
+
base_spec_without_modules = (
|
|
216
|
+
{k: v for k, v in base_spec_val.items() if k != "modules"}
|
|
217
|
+
if isinstance(base_spec_val, dict)
|
|
218
|
+
else {}
|
|
219
|
+
)
|
|
220
|
+
base_without_modules["spec"] = base_spec_without_modules
|
|
221
|
+
|
|
222
|
+
overlay_without_modules = dict(overlay)
|
|
223
|
+
if "spec" in overlay and isinstance(overlay.get("spec"), dict):
|
|
224
|
+
overlay_spec_without_modules = {k: v for k, v in overlay["spec"].items() if k != "modules"}
|
|
225
|
+
overlay_without_modules["spec"] = overlay_spec_without_modules
|
|
226
|
+
|
|
227
|
+
merged = _merge_value(
|
|
228
|
+
base_without_modules, overlay_without_modules, base_source, overlay_source, "", provenance
|
|
229
|
+
)
|
|
230
|
+
merged.setdefault("spec", {})
|
|
231
|
+
merged["spec"]["modules"] = _merge_modules(
|
|
232
|
+
base_modules, overlay_modules, base_source, overlay_source, provenance
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
diagnostics += validate_loaded_profile(merged, profile_file)
|
|
236
|
+
if any(d.level == "error" for d in diagnostics):
|
|
237
|
+
return None, provenance, diagnostics
|
|
238
|
+
|
|
239
|
+
return merged, provenance, diagnostics
|