Coverage for liitos/__init__.py: 86.81%

81 statements  

« prev     ^ index     » next       coverage.py v7.10.2, created at 2025-08-11 15:51:33 +00:00

1"""Splice (Finnish liitos) contributions.""" 

2 

3import datetime as dti 

4import logging 

5import os 

6import pathlib 

7import shellingham # type: ignore 

8from typing import Union, no_type_check 

9 

10# [[[fill git_describe()]]] 

11__version__ = '2025.8.11+parent.gd451cf84' 

12# [[[end]]] (checksum: 1f8e192582df6747d71dfe4747e7f6f9) 

13__version_info__ = tuple( 

14 e if '-' not in e else e.split('-')[0] for part in __version__.split('+') for e in part.split('.') if e != 'parent' 

15) 

16 

17APP_ALIAS = str(pathlib.Path(__file__).parent.name) 

18APP_ENV = APP_ALIAS.upper() 

19APP_NAME = locals()['__doc__'] 

20DEBUG = bool(os.getenv(f'{APP_ENV}_DEBUG', '')) 

21VERBOSE = bool(os.getenv(f'{APP_ENV}_VERBOSE', '')) 

22QUIET = True # HACK A DID ACK - the new normal 

23STRICT = bool(os.getenv(f'{APP_ENV}_STRICT', '')) 

24ENCODING = 'utf-8' 

25ENCODING_ERRORS_POLICY = 'ignore' 

26DEFAULT_CONFIG_NAME = f'.{APP_ALIAS}.json' 

27 

28APP_VERSION = __version__ 

29COMMA = ',' 

30DEFAULT_LF_ONLY = 'YES' 

31FILTER_CS_LIST = 'mermaid-filter' 

32FROM_FORMAT_SPEC = 'markdown' 

33LATEX_PAYLOAD_NAME = 'document.tex' 

34log = logging.getLogger() # Module level logger is sufficient 

35LOG_FOLDER = pathlib.Path('logs') 

36LOG_FILE = f'{APP_ALIAS}.log' 

37LOG_PATH = pathlib.Path(LOG_FOLDER, LOG_FILE) if LOG_FOLDER.is_dir() else pathlib.Path(LOG_FILE) 

38LOG_LEVEL = logging.INFO 

39LOG_SEPARATOR = '- ' * 80 

40 

41DEFAULT_STRUCTURE_NAME = 'structure.yml' 

42KEY_APPROVALS = 'approvals' 

43KEY_BIND = 'bind' 

44KEY_CHANGES = 'changes' 

45KEY_LAYOUT = 'layout' 

46KEY_META = 'meta' 

47KEYS_REQUIRED = (KEY_APPROVALS, KEY_BIND, KEY_CHANGES, KEY_META) 

48 

49CONTEXT: dict[str, str] = {} 

50KNOWN_APPROVALS_STRATEGIES = ('south', 'east') 

51APPROVALS_STRATEGY = os.getenv('LIITOS_APPROVALS_STRATEGY', '').lower() 

52 

53OptionsType = dict[str, Union[bool, str, None]] 

54PathLike = Union[str, pathlib.Path] 

55PathLikeOrBool = Union[PathLike, bool] 

56ExternalsType = dict[str, dict[str, PathLikeOrBool]] 

57 

58try: 

59 SHELL = shellingham.detect_shell() 

60except shellingham.ShellDetectionFailure: 

61 SHELL = ('', 'echo') 

62 

63TOOL_VERSION_COMMAND_MAP = { 

64 'etiketti': { 

65 'command': 'etiketti --version', 

66 'banner': 'Label and document the pdf file (data protection and identity)', 

67 }, 

68 'exiftool': { 

69 'command': 'exiftool -ver', 

70 'banner': 'Change and list EXIF attributes', 

71 }, 

72 'foran': { 

73 'command': 'foran version', 

74 'banner': 'Inspect local git status (or detect that there is no repository)', 

75 }, 

76 'git': { 

77 'command': 'git --version', 

78 'banner': 'Version control system (git)', 

79 }, 

80 'liitos': { 

81 'command': 'liitos version', 

82 'banner': 'Process the markdown documentation to produce PDF', 

83 }, 

84 'lualatex': { 

85 'command': 'lualatex --version', 

86 'banner': 'Process LaTeX to produce PDF', 

87 }, 

88 'mermaid': { 

89 'command': 'npm view mermaid', 

90 'banner': 'Mermaid for rendering diagrams from textual representations', 

91 }, 

92 'mermaid-filter': { 

93 'command': 'npm view mermaid-filter', 

94 'banner': 'Pandoc filter for mermaid diagrams (rasterized version for PDF)', 

95 }, 

96 'navigaattori': { 

97 'command': 'navigaattori version', 

98 'banner': 'Discover publication structural information from tree', 

99 }, 

100 'node': { 

101 'command': 'node --version', 

102 'banner': 'Node server for executing some tools', 

103 }, 

104 'npm': { 

105 'command': 'npm --version', 

106 'banner': 'Node package manager for inspecting versions of some node based tools', 

107 }, 

108 'pandoc': { 

109 'command': 'pandoc --version', 

110 'banner': 'Pandoc for transforming markdown to LaTeX', 

111 }, 

112 'pandoc-filter': { 

113 'command': 'npm view pandoc-filter', 

114 'banner': 'Pandoc base filter for mermaid diagram tools (interface module to pandoc filtering)', 

115 }, 

116 'pdfinfo': { 

117 'command': 'pdfinfo -v', 

118 'banner': 'Show PDF file information', 

119 }, 

120 'puppeteer': { 

121 'command': 'npm view puppeteer', 

122 'banner': 'Headless browser driver used by svgexport', 

123 }, 

124 'python': { 

125 'command': 'python -V', 

126 'banner': 'Python driving it all', 

127 }, 

128 'shell': { 

129 'command': f'{SHELL[1]} --version', 

130 'banner': 'The shell under which this process executes', 

131 }, 

132 'svgexport': { 

133 'command': 'npm view svgexport', 

134 'banner': 'Export SVG to PNG (rasterized version for inclusion in PDF)', 

135 }, 

136 'taksonomia': { 

137 'command': 'taksonomia --version', 

138 'banner': 'Assess and document the inventory of folders and files', 

139 }, 

140} 

141 

142ToolKey = str 

143 

144EXTERNALS: ExternalsType = { 

145 'bookmatter': { 

146 'id': 'templates/bookmatter.tex.in', 

147 'is_custom': False, 

148 }, 

149 'driver': { 

150 'id': 'templates/driver.tex.in', 

151 'is_custom': False, 

152 }, 

153 'publisher': { 

154 'id': 'templates/publisher.tex.in', 

155 'is_custom': False, 

156 }, 

157 'metadata': { 

158 'id': 'templates/metadata.tex.in', 

159 'is_custom': False, 

160 }, 

161 'setup': { 

162 'id': 'templates/setup.tex.in', 

163 'is_custom': False, 

164 }, 

165} 

166 

167BOOKMATTER_TEMPLATE = os.getenv('LIITOS_BOOKMATTER_TEMPLATE', '') 

168if BOOKMATTER_TEMPLATE: 168 ↛ 169line 168 didn't jump to line 169 because the condition on line 168 was never true

169 EXTERNALS['bookmatter'] = {'id': BOOKMATTER_TEMPLATE, 'is_custom': True} 

170 

171DRIVER_TEMPLATE = os.getenv('LIITOS_DRIVER_TEMPLATE', '') 

172if DRIVER_TEMPLATE: 172 ↛ 173line 172 didn't jump to line 173 because the condition on line 172 was never true

173 EXTERNALS['driver'] = {'id': DRIVER_TEMPLATE, 'is_custom': True} 

174 

175METADATA_TEMPLATE = os.getenv('LIITOS_METADATA_TEMPLATE', '') 

176if METADATA_TEMPLATE: 176 ↛ 177line 176 didn't jump to line 177 because the condition on line 176 was never true

177 EXTERNALS['metadata'] = {'id': METADATA_TEMPLATE, 'is_custom': True} 

178 

179PUBLISHER_TEMPLATE = os.getenv('LIITOS_PUBLISHER_TEMPLATE', '') 

180if PUBLISHER_TEMPLATE: 180 ↛ 181line 180 didn't jump to line 181 because the condition on line 180 was never true

181 EXTERNALS['publisher'] = {'id': PUBLISHER_TEMPLATE, 'is_custom': True} 

182 

183SETUP_TEMPLATE = os.getenv('LIITOS_SETUP_TEMPLATE', '') 

184if SETUP_TEMPLATE: 184 ↛ 185line 184 didn't jump to line 185 because the condition on line 184 was never true

185 EXTERNALS['setup'] = {'id': SETUP_TEMPLATE, 'is_custom': True} 

186 

187TS_FORMAT_LOG = '%Y-%m-%dT%H:%M:%S' 

188TS_FORMAT_PAYLOADS = '%Y-%m-%d %H:%M:%S.%f UTC' 

189 

190__all__: list[str] = [ 

191 'APP_ALIAS', 

192 'APP_ENV', 

193 'APP_VERSION', 

194 'APPROVALS_STRATEGY', 

195 'DEBUG', 

196 'DEFAULT_STRUCTURE_NAME', 

197 'ENCODING', 

198 'EXTERNALS', 

199 'ExternalsType', 

200 'CONTEXT', 

201 'FILTER_CS_LIST', 

202 'FROM_FORMAT_SPEC', 

203 'KEY_APPROVALS', 

204 'KEY_BIND', 

205 'KEY_CHANGES', 

206 'KEY_LAYOUT', 

207 'KEY_META', 

208 'KEYS_REQUIRED', 

209 'KNOWN_APPROVALS_STRATEGIES', 

210 'LATEX_PAYLOAD_NAME', 

211 'LOG_SEPARATOR', 

212 'OptionsType', 

213 'PathLike', 

214 'QUIET', 

215 'TOOL_VERSION_COMMAND_MAP', 

216 'ToolKey', 

217 'TS_FORMAT_PAYLOADS', 

218 'log', 

219 'parse_csl', 

220] 

221 

222 

223def parse_csl(csl: str) -> list[str]: 

224 """DRY.""" 

225 return [fmt.strip() for fmt in csl.split(COMMA) if fmt.strip()] 

226 

227 

228@no_type_check 

229def formatTime_RFC3339(self, record, datefmt=None): # noqa 

230 """HACK A DID ACK we could inject .astimezone() to localize ...""" 

231 return dti.datetime.fromtimestamp(record.created, dti.timezone.utc).isoformat() # pragma: no cover 

232 

233 

234@no_type_check 

235def init_logger(name=None, level=None): 

236 """Initialize module level logger""" 

237 global log # pylint: disable=global-statement 

238 

239 log_format = { 

240 'format': '%(asctime)s %(levelname)s [%(name)s]: %(message)s', 

241 'datefmt': TS_FORMAT_LOG, 

242 # 'filename': LOG_PATH, 

243 'level': LOG_LEVEL if level is None else level, 

244 } 

245 logging.Formatter.formatTime = formatTime_RFC3339 

246 logging.basicConfig(**log_format) 

247 log = logging.getLogger(APP_ENV if name is None else name) 

248 log.propagate = True 

249 

250 

251init_logger(name=APP_ENV, level=logging.DEBUG if DEBUG else None)