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