Coverage for navigaattori/meta.py: 85.35%
109 statements
« prev ^ index » next coverage.py v7.4.1, created at 2024-02-04 20:46:10 +00:00
« prev ^ index » next coverage.py v7.4.1, created at 2024-02-04 20:46:10 +00:00
1import copy
2import pathlib
3from typing import Union, no_type_check
5import yaml
7import navigaattori.liitos_meta as voc
8from navigaattori import ENCODING, log
11@no_type_check
12class Meta:
13 """Represent the metadata (including any imports)."""
15 def meta_top_has_content(self) -> None:
16 """Ensure we received a none empty top level metadata file to bootstrap from."""
17 if not self.meta_top_path.is_file() or not self.meta_top_path.stat().st_size:
18 self.state_code = 1
19 self.state_message = f'meta ({self.meta_top_path}) is no file or empty'
20 log.error(self.state_message)
22 def log_assessed_meta(self) -> None:
23 """Log out the meta we found."""
24 log.info(f'reporting current metadata starting from ({self.meta_top_path}) ...')
25 for key, aspect in self.metadata.items():
26 if not isinstance(aspect, dict) or not aspect: 26 ↛ 27line 26 didn't jump to line 27, because the condition on line 26 was never true
27 log.info(f'- {key} -> {aspect}')
28 else:
29 log.info(f'- {key} =>')
30 for this, that in aspect.items():
31 if not isinstance(that, dict) or not that:
32 log.info(f' + {this} -> {that}')
33 else:
34 log.info(f' + {this} =>')
35 for k, v in that.items():
36 if not isinstance(v, dict) or not v: 36 ↛ 39line 36 didn't jump to line 39, because the condition on line 36 was never false
37 log.info(f' * {k} -> {v}')
38 else:
39 log.info(f' * {k} =>')
40 for kf, vf in v.items():
41 log.info(f' - {kf} -> {vf}')
43 def load_meta_top(self) -> None:
44 """Load the top level meta data."""
45 with open(self.meta_top_path, 'rt', encoding=ENCODING) as handle:
46 data = yaml.safe_load(handle)
47 if not data:
48 self.state_code = 1
49 self.state_message = 'empty metadata?'
50 log.error(f'meta failed to load any entry from ({self.meta_top_path})')
51 return
52 peeled = data.get('document', []) if isinstance(data, dict) else []
53 if not peeled or 'document' not in data:
54 self.state_code = 1
55 self.state_message = 'missing expected top level key document - no metadata or wrong file?'
56 log.error(f'meta failed to load anything from ({self.meta_top_path})')
57 return
58 self.metadata = copy.deepcopy(data) # TODO(sthagen) belt and braces
59 log.info(f'top level metadata successfully loaded from ({self.meta_top_path}):')
60 self.log_assessed_meta()
62 def load_meta_import(self) -> None:
63 """Import any metadata if document/import found and valid."""
64 if 'import' in self.metadata['document']: 64 ↛ 80line 64 didn't jump to line 80, because the condition on line 64 was never false
65 base_meta_path = self.meta_top_base / self.metadata['document']['import']
66 log.info(f'- trying to import metadata from ({base_meta_path})')
67 if not base_meta_path.is_file() or not base_meta_path.stat().st_size: 67 ↛ 68line 67 didn't jump to line 68, because the condition on line 67 was never true
68 self.state_code = 1
69 self.state_message = 'missing expected top level key document - no metadata or wrong file?'
70 log.error(
71 f'metadata declares import of base data from ({base_meta_path.name})'
72 f' but failed to find non-empty base file at {base_meta_path}'
73 )
74 return
75 with open(base_meta_path, 'rt', encoding=ENCODING) as handle:
76 base_data = yaml.safe_load(handle)
77 for key, value in self.metadata['document']['patch'].items():
78 base_data['document']['common'][key] = value
79 self.metadata = base_data
80 log.info(f'metadata successfully loaded completely starting from ({self.meta_top_path}):')
81 self.log_assessed_meta()
83 def verify_token_use(self) -> None:
84 """Verify metadata uses only tokens from the liitos vocabulary."""
85 log.info(
86 f'verifying metadata starting from ({self.meta_top_path}) uses only tokens from the liitos vocabulary ...'
87 )
88 bad_tokens = []
89 common_tokens = sorted(self.metadata['document']['common'])
90 for token in common_tokens:
91 if token not in self.tokens: 91 ↛ 92line 91 didn't jump to line 92, because the condition on line 91 was never true
92 bad_tokens.append(token)
93 log.error(f'- unknown token ({token}) in metadata')
95 if bad_tokens: 95 ↛ 96line 95 didn't jump to line 96, because the condition on line 95 was never true
96 badness = len(bad_tokens)
97 tok_sin_plu = 'token' if badness == 1 else 'tokens'
98 self.state_code = 1
99 self.state_message = (
100 f'found {badness} invalid {tok_sin_plu} {tuple(sorted(bad_tokens))}'
101 f' in metadata loaded completely starting from ({self.meta_top_path})'
102 )
103 return
105 common_tokens_count = len(common_tokens)
106 tok_sin_plu = 'token' if common_tokens_count == 1 else 'tokens'
107 token_use = round(100.0 * common_tokens_count / len(self.tokens), 2)
108 log.info(f'metadata successfully verified {common_tokens_count} {tok_sin_plu} ({token_use}% of vocabulary)')
110 def __init__(self, meta_top_path: Union[str, pathlib.Path], options: dict[str, bool]):
111 self._options = options
112 self.quiet: bool = self._options.get('quiet', False)
113 self.strict: bool = self._options.get('strict', False)
114 self.verbose: bool = self._options.get('verbose', False)
115 self.guess: bool = self._options.get('guess', False)
116 self.meta_top_path: pathlib.Path = pathlib.Path(meta_top_path)
117 self.meta_top_base = self.meta_top_path.parent
118 self.metadata = {}
119 self.state_code = 0
120 self.state_message = ''
122 self.meta_top_has_content()
124 self.vocabulary = voc.load()
125 self.tokens = voc.tokens(self.vocabulary)
127 if not self.state_code:
128 self.load_meta_top()
130 if not self.state_code:
131 self.load_meta_import()
133 if not self.state_code:
134 self.verify_token_use()
136 if not self.state_code:
137 log.info(f'metadata from ({self.meta_top_path}) seems to be valid')
139 def is_valid(self) -> bool:
140 """Is the model valid?"""
141 return not self.state_code
143 def code_details(self) -> tuple[int, str]:
144 """Return an ordered pair of state code and message"""
145 return self.state_code, self.state_message
147 @no_type_check
148 def container(self):
149 """Return the metadata."""
150 return copy.deepcopy(self.metadata)