Coverage for liitos/cli.py: 86.36%
154 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"""Command line interface for splice (Finnish liitos) contributions."""
3import copy
4import datetime as dti
5import logging
6import os
7import pathlib
8import sys
10import typer
12import liitos.approvals as sig
13import liitos.changes as chg
14import liitos.concat as cat
15import liitos.eject as eje
16import liitos.gather as gat
17import liitos.meta as met
18import liitos.render as ren
19import liitos.tools as too
20from liitos import (
21 APP_NAME,
22 APP_VERSION,
23 APPROVALS_STRATEGY,
24 DEBUG,
25 DEFAULT_STRUCTURE_NAME,
26 EXTERNALS,
27 FILTER_CS_LIST,
28 FROM_FORMAT_SPEC,
29 KNOWN_APPROVALS_STRATEGIES,
30 LOG_SEPARATOR,
31 QUIET,
32 TOOL_VERSION_COMMAND_MAP,
33 TS_FORMAT_PAYLOADS,
34 OptionsType,
35 log,
36)
38app = typer.Typer(
39 add_completion=False,
40 context_settings={'help_option_names': ['-h', '--help']},
41 no_args_is_help=True,
42)
44DocumentRoot = typer.Option(
45 '',
46 '-d',
47 '--document-root',
48 help='Root of the document tree to visit. Optional\n(default: positional tree root value)',
49)
50StructureName = typer.Option(
51 DEFAULT_STRUCTURE_NAME,
52 '-s',
53 '--structure',
54 help='structure mapping file (default: {gat.DEFAULT_STRUCTURE_NAME})',
55)
56TargetName = typer.Option(
57 '',
58 '-t',
59 '--target',
60 help='target document key',
61)
62FacetName = typer.Option(
63 '',
64 '-f',
65 '--facet',
66 help='facet key of target document',
67)
68FromFormatSpec = typer.Option(
69 FROM_FORMAT_SPEC,
70 '--from-format-spec',
71 help='from format specification handed over to pandoc',
72)
73FilterCSList = typer.Option(
74 'DEFAULT_FILTER',
75 '-F',
76 '--filters',
77 help='comma separated list of filters handed over to pandoc (in order) or empty to apply no filter',
78)
79Verbosity = typer.Option(
80 False,
81 '-v',
82 '--verbose',
83 help='Verbose output (default is False)',
84)
85Strictness = typer.Option(
86 False,
87 '--strict',
88 help='Ouput noisy warnings on console (default is False)',
89)
90OutputPath = typer.Option(
91 '',
92 '-o',
93 '--output-path',
94 help='Path to output unambiguous content to - like when ejecting a template',
95)
96LabelCall = typer.Option(
97 '',
98 '-l',
99 '--label',
100 help='optional label call to execute',
101)
102PatchTables = typer.Option(
103 False,
104 '-p',
105 '--patch-tables',
106 help='Patch tables EXPERIMENTAL (default is False)',
107)
108ApprovalsStrategy = typer.Option(
109 '',
110 '-a',
111 '--approvals-strategy',
112 help=f'optional approvals layout strategy in ({", ".join(KNOWN_APPROVALS_STRATEGIES)})',
113)
116@app.callback(invoke_without_command=True)
117def callback(
118 version: bool = typer.Option(
119 False,
120 '-V',
121 '--version',
122 help='Display the application version and exit',
123 is_eager=True,
124 )
125) -> None:
126 """
127 Splice (Finnish liitos) contributions.
128 """
129 if version:
130 typer.echo(f'{APP_NAME} version {APP_VERSION}')
131 raise typer.Exit()
134def _verify_call_vector(
135 doc_root: str,
136 doc_root_pos: str,
137 verbose: bool,
138 strict: bool,
139 label: str = '',
140 patch_tables: bool = False,
141 from_format_spec: str = FROM_FORMAT_SPEC,
142 filter_cs_list: str = '',
143 approvals_strategy: str = '',
144) -> tuple[int, str, str, OptionsType]:
145 """DRY"""
146 if DEBUG: 146 ↛ 147line 146 didn't jump to line 147 because the condition on line 146 was never true
147 logging.getLogger().setLevel(logging.DEBUG)
148 elif verbose:
149 logging.getLogger().setLevel(logging.INFO)
150 elif QUIET and not verbose and not strict: 150 ↛ 153line 150 didn't jump to line 153 because the condition on line 150 was always true
151 logging.getLogger().setLevel(logging.WARNING)
153 log.debug(f'verifier received: {locals()}')
154 doc = doc_root.strip()
155 if not doc and doc_root_pos:
156 doc = doc_root_pos
157 if not doc:
158 print('Document tree root required', file=sys.stderr)
159 return 2, 'Document tree root required', '', {}
161 doc_root_path = pathlib.Path(doc)
162 if doc_root_path.exists():
163 if not doc_root_path.is_dir(): 163 ↛ 164line 163 didn't jump to line 164 because the condition on line 163 was never true
164 print(f'requested tree root at ({doc}) is not a folder reachable from ({os.getcwd()})', file=sys.stderr)
165 return 2, f'requested tree root at ({doc}) is not a folder reachable from ({os.getcwd()})', '', {}
166 else:
167 print(f'requested tree root at ({doc}) does not exist as seen from ({os.getcwd()})', file=sys.stderr)
168 return 2, f'requested tree root at ({doc}) does not exist as seen from ({os.getcwd()})', '', {}
170 if not approvals_strategy:
171 approvals_strategy = APPROVALS_STRATEGY
172 log.info(f'Using value from environment for approvals strategy (APPROVALS_STRATEGY) == ({approvals_strategy})')
173 if not approvals_strategy: 173 ↛ 180line 173 didn't jump to line 180 because the condition on line 173 was always true
174 approvals_strategy = KNOWN_APPROVALS_STRATEGIES[0]
175 log.info(
176 'No preference in environment for approvals strategy (APPROVALS_STRATEGY)'
177 f' using default ({approvals_strategy})'
178 )
180 if approvals_strategy not in KNOWN_APPROVALS_STRATEGIES: 180 ↛ 181line 180 didn't jump to line 181 because the condition on line 180 was never true
181 approvals_strategy = KNOWN_APPROVALS_STRATEGIES[0]
182 log.info(
183 'Value in environment for approvals strategy (APPROVALS_STRATEGY)'
184 f' not in ({", ".join(KNOWN_APPROVALS_STRATEGIES)}) - using default ({approvals_strategy})'
185 )
187 options: OptionsType = {
188 'quiet': QUIET and not verbose and not strict,
189 'strict': strict,
190 'verbose': verbose,
191 'label': label,
192 'patch_tables': patch_tables,
193 'from_format_spec': from_format_spec if from_format_spec else FROM_FORMAT_SPEC,
194 'filter_cs_list': filter_cs_list if filter_cs_list != 'DEFAULT_FILTER' else FILTER_CS_LIST,
195 'approvals_strategy': approvals_strategy,
196 'table_caption_below': None,
197 'table_uglify': None,
198 }
199 log.debug(f'Post verifier: {options=}')
200 return 0, '', doc, options
203@app.command('verify')
204def verify( # noqa
205 doc_root_pos: str = typer.Argument(''),
206 doc_root: str = DocumentRoot,
207 structure: str = StructureName,
208 target: str = TargetName,
209 facet: str = FacetName,
210 verbose: bool = Verbosity,
211 strict: bool = Strictness,
212) -> int:
213 """
214 Verify the structure definition against the file system.
215 """
216 code, message, doc, options = _verify_call_vector(
217 doc_root=doc_root, doc_root_pos=doc_root_pos, verbose=verbose, strict=strict
218 )
219 if code:
220 log.error(message)
221 return code
223 return sys.exit(
224 gat.verify(doc_root=doc, structure_name=structure, target_key=target, facet_key=facet, options=options)
225 )
228@app.command('approvals')
229def approvals( # noqa
230 doc_root_pos: str = typer.Argument(''),
231 doc_root: str = DocumentRoot,
232 structure: str = StructureName,
233 target: str = TargetName,
234 facet: str = FacetName,
235 verbose: bool = Verbosity,
236 strict: bool = Strictness,
237) -> int:
238 """
239 Weave in the approvals for facet of target within document root.
240 """
241 code, message, doc, options = _verify_call_vector(
242 doc_root=doc_root, doc_root_pos=doc_root_pos, verbose=verbose, strict=strict
243 )
244 if code: 244 ↛ 248line 244 didn't jump to line 248 because the condition on line 244 was always true
245 log.error(message)
246 return 2
248 return sys.exit(
249 sig.weave(
250 doc_root=doc,
251 structure_name=structure,
252 target_key=target,
253 facet_key=facet,
254 options=options,
255 externals=EXTERNALS,
256 )
257 )
260@app.command('changes')
261def changes( # noqa
262 doc_root_pos: str = typer.Argument(''),
263 doc_root: str = DocumentRoot,
264 structure: str = StructureName,
265 target: str = TargetName,
266 facet: str = FacetName,
267 verbose: bool = Verbosity,
268 strict: bool = Strictness,
269) -> int:
270 """
271 Weave in the changes for facet of target within document root.
272 """
273 code, message, doc, options = _verify_call_vector(
274 doc_root=doc_root, doc_root_pos=doc_root_pos, verbose=verbose, strict=strict
275 )
276 if code: 276 ↛ 280line 276 didn't jump to line 280 because the condition on line 276 was always true
277 log.error(message)
278 return 2
280 return sys.exit(
281 chg.weave(
282 doc_root=doc,
283 structure_name=structure,
284 target_key=target,
285 facet_key=facet,
286 options=options,
287 externals=EXTERNALS,
288 )
289 )
292@app.command('concat')
293def concat( # noqa
294 *,
295 doc_root_pos: str = typer.Argument(''),
296 doc_root: str = DocumentRoot,
297 structure: str = StructureName,
298 target: str = TargetName,
299 facet: str = FacetName,
300 verbose: bool = Verbosity,
301 strict: bool = Strictness,
302) -> int:
303 """
304 Concatenate the markdown tree for facet of target within render/pdf below document root.
305 """
306 code, message, doc, options = _verify_call_vector(
307 doc_root=doc_root, doc_root_pos=doc_root_pos, verbose=verbose, strict=strict
308 )
309 if code: 309 ↛ 313line 309 didn't jump to line 313 because the condition on line 309 was always true
310 log.error(message)
311 return 2
313 return sys.exit(
314 cat.concatenate(doc_root=doc, structure_name=structure, target_key=target, facet_key=facet, options=options)
315 )
318@app.command('render')
319def render( # noqa
320 doc_root_pos: str = typer.Argument(''),
321 doc_root: str = DocumentRoot,
322 structure: str = StructureName,
323 target: str = TargetName,
324 facet: str = FacetName,
325 label: str = LabelCall,
326 verbose: bool = Verbosity,
327 strict: bool = Strictness,
328 patch_tables: bool = PatchTables,
329 from_format_spec: str = FromFormatSpec,
330 filter_cs_list: str = FilterCSList,
331 approvals_strategy: str = ApprovalsStrategy,
332) -> int:
333 """
334 Render the markdown tree for facet of target within render/pdf below document root.
335 \n
336 For ejected / customized templates set matching environment variables to the paths:\n
337 \n
338 - LIITOS_BOOKMATTER_TEMPLATE (for title page incl. approvals table)\n
339 - LIITOS_PUBLISHER_TEMPLATE (for publisher page incl. changes and proprietary info)\n
340 - LIITOS_METADATA_TEMPLATE (values to required known keys used on LaTeX level)\n
341 - LIITOS_SETUP_TEMPLATE (general layout template)\n
342 - DRIVER_TEMPLATE (template for general structure)\n
343 \n
344 To not insert PDF digital signature fields set the environment variable\n
345 LIITOS_NO_DIG_SIG_FIELDS to something truthy.\n
346 In case the LaTeX package digital-signature-fields is not found some placeholder\n
347 will be inserted when allowing the insert of such fields in the approvals table.
348 """
349 code, message, doc, options = _verify_call_vector(
350 doc_root=doc_root,
351 doc_root_pos=doc_root_pos,
352 verbose=verbose,
353 strict=strict,
354 label=label,
355 patch_tables=patch_tables,
356 from_format_spec=from_format_spec,
357 filter_cs_list=filter_cs_list,
358 approvals_strategy=approvals_strategy,
359 )
360 if code:
361 log.error(message)
362 return sys.exit(code)
364 start_time = dti.datetime.now(tz=dti.timezone.utc)
365 start_ts = start_time.strftime(TS_FORMAT_PAYLOADS)
366 log.info(f'Start timestamp ({start_ts})')
367 code = cat.concatenate(doc_root=doc, structure_name=structure, target_key=target, facet_key=facet, options=options)
368 if code: 368 ↛ 369line 368 didn't jump to line 369 because the condition on line 368 was never true
369 return sys.exit(code)
371 idem = os.getcwd()
372 doc = '../../'
373 log.info(f'before met.weave(): {os.getcwd()} set doc ({doc})')
374 externals = copy.deepcopy(EXTERNALS)
375 code = met.weave(
376 doc_root=doc, structure_name=structure, target_key=target, facet_key=facet, options=options, externals=externals
377 )
378 if code: 378 ↛ 379line 378 didn't jump to line 379 because the condition on line 378 was never true
379 return sys.exit(code)
381 log.info(f'before sig.weave(): {os.getcwd()} set doc ({doc})')
382 os.chdir(idem)
383 log.info(f'relocated for sig.weave(): {os.getcwd()} with doc ({doc})')
384 code = sig.weave(
385 doc_root=doc, structure_name=structure, target_key=target, facet_key=facet, options=options, externals=externals
386 )
387 if code: 387 ↛ 388line 387 didn't jump to line 388 because the condition on line 387 was never true
388 return sys.exit(code)
390 log.info(f'before chg.weave(): {os.getcwd()} set doc ({doc})')
391 os.chdir(idem)
392 log.info(f'relocated for chg.weave(): {os.getcwd()} with doc ({doc})')
393 code = chg.weave(
394 doc_root=doc, structure_name=structure, target_key=target, facet_key=facet, options=options, externals=externals
395 )
396 if code: 396 ↛ 397line 396 didn't jump to line 397 because the condition on line 396 was never true
397 return sys.exit(code)
399 log.info(f'before chg.weave(): {os.getcwd()} set doc ({doc})')
400 os.chdir(idem)
401 log.info(f'relocated for chg.weave(): {os.getcwd()} with doc ({doc})')
402 code = ren.der(doc_root=doc, structure_name=structure, target_key=target, facet_key=facet, options=options)
404 end_time = dti.datetime.now(tz=dti.timezone.utc)
405 end_ts = end_time.strftime(TS_FORMAT_PAYLOADS)
406 duration_secs = (end_time - start_time).total_seconds()
407 log.info(f'End timestamp ({end_ts})')
408 acted = 'Rendered'
409 if code == 0xFADECAFE: 409 ↛ 410line 409 didn't jump to line 410 because the condition on line 409 was never true
410 acted = 'Did not render'
411 code = 0 # HACK A DID ACK
412 log.info(f'{acted} {target} document for {facet} at {doc} in {duration_secs} secs')
413 return sys.exit(code)
416@app.command('report')
417def report() -> int:
418 """
419 Report on the environment.
420 """
421 log.info(LOG_SEPARATOR)
422 log.info('inspecting environment (tool version information):')
423 for tool_key in TOOL_VERSION_COMMAND_MAP:
424 too.report(tool_key)
425 log.info(LOG_SEPARATOR)
427 return sys.exit(0)
430@app.command('eject')
431def eject( # noqa
432 that: str = typer.Argument(''),
433 out: str = OutputPath,
434) -> int:
435 """
436 Eject a template. Enter unique part to retrieve, any unknown word to obtain the list of known templates.
437 """
438 return sys.exit(eje.this(thing=that, out=out))
441@app.command('version')
442def app_version() -> None:
443 """
444 Display the application version and exit.
445 """
446 callback(True)