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