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

1import copy 

2import pathlib 

3from typing import Union, no_type_check 

4 

5import yaml 

6 

7import navigaattori.liitos_meta as voc 

8from navigaattori import ENCODING, log 

9 

10 

11@no_type_check 

12class Meta: 

13 """Represent the metadata (including any imports).""" 

14 

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) 

21 

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}') 

42 

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() 

61 

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() 

82 

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') 

94 

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 

104 

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)') 

109 

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 = '' 

121 

122 self.meta_top_has_content() 

123 

124 self.vocabulary = voc.load() 

125 self.tokens = voc.tokens(self.vocabulary) 

126 

127 if not self.state_code: 

128 self.load_meta_top() 

129 

130 if not self.state_code: 

131 self.load_meta_import() 

132 

133 if not self.state_code: 

134 self.verify_token_use() 

135 

136 if not self.state_code: 

137 log.info(f'metadata from ({self.meta_top_path}) seems to be valid') 

138 

139 def is_valid(self) -> bool: 

140 """Is the model valid?""" 

141 return not self.state_code 

142 

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 

146 

147 @no_type_check 

148 def container(self): 

149 """Return the metadata.""" 

150 return copy.deepcopy(self.metadata)