Coverage for liitos/gather.py: 84.47%
185 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-28 20:14:46 +00:00
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-28 20:14:46 +00:00
1"""Gather the structure and discover the content."""
3import os
4import pathlib
5from typing import Dict, List, Set, Tuple, Union
7import yaml
9from liitos import (
10 DEFAULT_STRUCTURE_NAME,
11 KEY_APPROVALS,
12 KEY_BIND,
13 KEY_CHANGES,
14 KEY_LAYOUT,
15 KEY_META,
16 KEYS_REQUIRED,
17 ENCODING,
18 OptionsType,
19 log,
20)
22PathLike = Union[str, pathlib.Path]
24Approvals = Dict[str, Union[List[str], List[List[str]]]]
25Assets = Dict[str, Dict[str, Dict[str, str]]]
26Binder = List[str]
27Changes = Dict[str, Union[List[str], List[List[str]]]]
28Layout = Dict[str, str]
29Meta = Dict[str, str]
30Structure = Dict[str, List[Dict[str, str]]]
31Targets = Set[str]
32Facets = Dict[str, Targets]
33Payload = Union[Approvals, Binder, Changes, Meta]
34Verification = Tuple[bool, str]
37def load_structure(path: PathLike = DEFAULT_STRUCTURE_NAME) -> Structure:
38 """Load the structure information and content links from the YAML file per convention."""
39 with open(path, 'rt', encoding=ENCODING) as handle:
40 return yaml.safe_load(handle) # type: ignore
43def targets(structure: Structure) -> Targets:
44 """Extract the targets from the given structure information item."""
45 return set(target for target in structure)
48def facets(structure: Structure) -> Facets:
49 """Extract the facets per target from the given structure information item."""
50 return {target: set(facet for facet_data in cnt for facet in facet_data) for target, cnt in structure.items()}
53def assets(structure: Structure) -> Assets:
54 """Map the assets to facets of targets."""
55 return {t: {f: asset for fd in cnt for f, asset in fd.items()} for t, cnt in structure.items()} # type: ignore
58def verify_target(name: str, target_set: Targets) -> Verification:
59 """Verify presence of target yielding predicate and message (in case of failure)."""
60 return (True, '') if name in target_set else (False, f'target ({name}) not in {sorted(target_set)}')
63def verify_facet(name: str, target: str, facet_map: Facets) -> Verification:
64 """Verify presence of facet for target yielding predicate and message (in case of failure)."""
65 if name in facet_map[target]:
66 return True, ''
67 return False, f'facet ({name}) of target ({target}) not in {sorted(facet_map[target])}'
70def error_context(
71 payload: Payload,
72 label: str,
73 facet: str,
74 target: str,
75 path: PathLike,
76 err: Union[FileNotFoundError, KeyError, ValueError],
77) -> Tuple[Payload, str]:
78 """Provide harmonized context for the error situation as per parameters."""
79 if isinstance(err, FileNotFoundError):
80 return payload, f'{label} link not found at ({path}) or invalid for facet ({facet}) of target ({target})'
81 if isinstance(err, KeyError):
82 return [], f'{label} not found in assets for facet ({facet}) of target ({target})'
83 if isinstance(err, ValueError):
84 return [], f'{label} requires json or yaml format in assets for facet ({facet}) of target ({target})'
85 raise NotImplementedError(f'error context not implemented for error ({err})')
88def load_binder(facet: str, target: str, path: PathLike) -> Tuple[Binder, str]:
89 """Yield the binder for facet of target from path and message (in case of failure)."""
90 try:
91 with open(path, 'rt', encoding=ENCODING) as handle:
92 return [line.strip() for line in handle.readlines() if line.strip()], ''
93 except FileNotFoundError as err:
94 return error_context([], 'Binder', facet, target, path, err) # type: ignore
97def binder(facet: str, target: str, asset_struct: Assets) -> Tuple[Binder, str]:
98 """Yield the binder for facet of target from link in assets and message (in case of failure)."""
99 try:
100 path = pathlib.Path(asset_struct[target][facet][KEY_BIND])
101 except KeyError as err:
102 return error_context([], 'Binder', facet, target, '', err) # type: ignore
103 return load_binder(facet, target, path)
106def load_layout(facet: str, target: str, path: PathLike) -> Tuple[Meta, str]:
107 """Yield the layout for facet of target from path and message (in case of failure)."""
108 try:
109 with open(path, 'rt', encoding=ENCODING) as handle:
110 return yaml.safe_load(handle), ''
111 except FileNotFoundError as err:
112 return error_context({}, 'Metadata', facet, target, path, err) # type: ignore
115def layout(facet: str, target: str, asset_struct: Assets) -> Tuple[Meta, str]:
116 """Yield the layout for facet of target from link in assets and message (in case of failure)."""
117 try:
118 path = pathlib.Path(asset_struct[target][facet][KEY_LAYOUT])
119 except KeyError as err:
120 return error_context({}, 'Layout', facet, target, '', err) # type: ignore
121 return load_layout(facet, target, path)
124def load_meta(facet: str, target: str, path: PathLike) -> Tuple[Meta, str]:
125 """Yield the metadata for facet of target from path and message (in case of failure)."""
126 try:
127 with open(path, 'rt', encoding=ENCODING) as handle:
128 return yaml.safe_load(handle), ''
129 except FileNotFoundError as err:
130 return error_context({}, 'Metadata', facet, target, path, err) # type: ignore
133def meta(facet: str, target: str, asset_struct: Assets) -> Tuple[Meta, str]:
134 """Yield the metadata for facet of target from link in assets and message (in case of failure)."""
135 try:
136 path = pathlib.Path(asset_struct[target][facet][KEY_META])
137 except KeyError as err:
138 return error_context({}, 'Metadata', facet, target, '', err) # type: ignore
139 return load_meta(facet, target, path)
142def load_approvals(facet: str, target: str, path: PathLike) -> Tuple[Approvals, str]:
143 """Yield the approvals for facet of target from path and message (in case of failure)."""
144 if str(path).lower().endswith('json'):
145 return error_context(
146 {}, 'Approvals', facet, target, path, ValueError('please transform approvals from json to yaml format')
147 ) # type: ignore
148 elif str(path).lower().endswith(('yaml', 'yml')): 148 ↛ 155line 148 didn't jump to line 155 because the condition on line 148 was always true
149 try:
150 with open(path, 'rt', encoding=ENCODING) as handle:
151 return yaml.safe_load(handle), ''
152 except FileNotFoundError as err:
153 return error_context({}, 'Approvals', facet, target, path, err) # type: ignore
155 return error_context({}, 'Approvals', facet, target, path, ValueError('json or yaml required')) # type: ignore
158def approvals(facet: str, target: str, asset_struct: Assets) -> Tuple[Approvals, str]:
159 """Yield the approvals for facet of target from link in assets and message (in case of failure)."""
160 try:
161 path = pathlib.Path(asset_struct[target][facet][KEY_APPROVALS])
162 except KeyError as err:
163 return error_context({}, 'Approvals', facet, target, '', err) # type: ignore
164 return load_approvals(facet, target, path)
167def load_changes(facet: str, target: str, path: PathLike) -> Tuple[Approvals, str]:
168 """Yield the changes for facet of target from path and message (in case of failure)."""
169 if str(path).lower().endswith('json'):
170 return error_context(
171 {}, 'Changes', facet, target, path, ValueError('please transform changes from json to yaml format')
172 ) # type: ignore
173 elif str(path).lower().endswith(('yaml', 'yml')):
174 try:
175 with open(path, 'rt', encoding=ENCODING) as handle:
176 return yaml.safe_load(handle), ''
177 except FileNotFoundError as err:
178 return error_context({}, 'Changes', facet, target, path, err) # type: ignore
180 return error_context({}, 'Changes', facet, target, path, ValueError('json or yaml required')) # type: ignore
183def changes(facet: str, target: str, asset_struct: Assets) -> Tuple[Changes, str]:
184 """Yield the changes for facet of target from link in assets and message (in case of failure)."""
185 try:
186 path = pathlib.Path(asset_struct[target][facet][KEY_CHANGES])
187 except KeyError as err:
188 return error_context({}, 'Changes', facet, target, '', err) # type: ignore
189 return load_changes(facet, target, path)
192def verify_asset_keys(facet: str, target: str, asset_struct: Assets) -> Verification:
193 """Verify presence of required keys for facet of target yielding predicate and message (in case of failure)."""
194 if all(key in asset_struct[target][facet] for key in KEYS_REQUIRED):
195 return True, ''
196 return False, f'keys in {sorted(KEYS_REQUIRED)} for facet ({facet}) of target ({target}) are missing'
199def verify_asset_links(facet: str, target: str, asset_struct: Assets) -> Verification:
200 """Verify presence of asset links for facet of target yielding predicate and message (in case of failure)."""
201 predicate, message = verify_asset_keys(facet, target, asset_struct)
202 if not predicate:
203 return predicate, message
204 for key in KEYS_REQUIRED:
205 link = pathlib.Path(asset_struct[target][facet][key])
206 log.debug(f' + verifying: {pathlib.Path.cwd() / link}')
207 if not link.is_file() or not link.stat().st_size:
208 return False, f'{key} asset link ({link}) for facet ({facet}) of target ({target}) is invalid'
209 return True, ''
212ASSET_KEY_ACTION = {
213 KEY_APPROVALS: approvals,
214 KEY_BIND: binder,
215 KEY_CHANGES: changes,
216 KEY_LAYOUT: layout,
217 KEY_META: meta,
218}
221def verify_assets(facet: str, target: str, asset_struct: Assets) -> Verification:
222 """Verify assets for facet of target yielding predicate and message (in case of failure)."""
223 predicate, message = verify_asset_links(facet, target, asset_struct)
224 if not predicate:
225 return predicate, message
226 for key, action in ASSET_KEY_ACTION.items(): 226 ↛ 230line 226 didn't jump to line 230 because the loop on line 226 didn't complete
227 asset, message = action(facet, target, asset_struct)
228 if not asset:
229 return False, f'{key} asset for facet ({facet}) of target ({target}) is invalid'
230 return True, ''
233def prelude(
234 doc_root: Union[str, pathlib.Path], structure_name: str, target_key: str, facet_key: str, command: str
235) -> tuple[Structure, Assets]:
236 """DRY."""
237 doc_root = pathlib.Path(doc_root)
238 idem = os.getcwd()
239 os.chdir(doc_root)
240 job_description = (
241 f'facet ({facet_key}) of target ({target_key}) with structure map ({structure_name})'
242 f' in document root ({doc_root}) coming from ({idem})'
243 )
244 log.info(f'executing prelude of command ({command}) for {job_description}')
245 structure = load_structure(structure_name)
246 asset_map = assets(structure)
247 return structure, asset_map
250def verify(
251 doc_root: Union[str, pathlib.Path],
252 structure_name: str,
253 target_key: str,
254 facet_key: str,
255 options: OptionsType,
256) -> int:
257 """Drive the verification."""
258 doc_root = pathlib.Path(doc_root)
259 os.chdir(doc_root)
260 facet = facet_key
261 target = target_key
262 structure_name = structure_name
263 job_description = (
264 f'facet ({facet}) for target ({target}) with structure map ({structure_name})' f' in document root ({doc_root})'
265 )
266 log.info(f'starting verification of {job_description}')
267 structure = load_structure(structure_name)
268 target_set = targets(structure)
269 facet_map = facets(structure)
270 asset_map = assets(structure)
272 predicate, message = verify_target(target, target_set)
273 if not predicate: 273 ↛ 274line 273 didn't jump to line 274 because the condition on line 273 was never true
274 log.error(f'failed verification with: {message}')
275 return 1
276 log.info(f'- target ({target}) OK')
278 predicate, message = verify_facet(facet, target, facet_map)
279 if not predicate: 279 ↛ 280line 279 didn't jump to line 280 because the condition on line 279 was never true
280 log.error(f'failed verification with: {message}')
281 return 1
282 log.info(f'- facet ({facet}) of target ({target}) OK')
284 predicate, message = verify_assets(facet, target, asset_map)
285 if not predicate: 285 ↛ 288line 285 didn't jump to line 288 because the condition on line 285 was always true
286 log.error(f'failed verification with: {message}')
287 return 1
288 log.info(f'- assets ({", ".join(sorted(KEYS_REQUIRED))}) for facet ({facet}) of target ({target}) OK')
290 signatures_path = asset_map[target][facet][KEY_APPROVALS]
291 log.info(f'loading signatures from {signatures_path=}')
292 signatures = load_approvals(facet, target, signatures_path)
293 log.info(f'{signatures=}')
294 history_path = asset_map[target][facet][KEY_CHANGES]
295 log.info(f'loading history from {history_path=}')
296 history = load_changes(facet, target, history_path)
297 log.info(f'{history=}')
299 layout_path = asset_map[target][facet][KEY_LAYOUT]
300 log.info(f'loading layout from {layout_path=}')
301 design = load_layout(facet, target, layout_path)
302 log.info(f'{design=}')
304 metadata_path = asset_map[target][facet][KEY_META]
305 log.info(f'loading metadata from {metadata_path=}')
306 info = load_meta(facet, target, metadata_path)
308 log.info(f'{info=}')
309 log.info('successful verification')
310 return 0