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

1"""Command line interface for splice (Finnish liitos) contributions.""" 

2 

3import copy 

4import datetime as dti 

5import logging 

6import os 

7import pathlib 

8import sys 

9 

10import typer 

11 

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) 

36 

37app = typer.Typer( 

38 add_completion=False, 

39 context_settings={'help_option_names': ['-h', '--help']}, 

40 no_args_is_help=True, 

41) 

42 

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) 

113 

114 

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

131 

132 

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', '', {} 

152 

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()})', '', {} 

161 

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 ) 

171 

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 ) 

178 

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 

197 

198 

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 

218 

219 return sys.exit( 

220 gat.verify(doc_root=doc, structure_name=structure, target_key=target, facet_key=facet, options=options) 

221 ) 

222 

223 

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 

243 

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 ) 

254 

255 

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 

275 

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 ) 

286 

287 

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 

308 

309 return sys.exit( 

310 cat.concatenate(doc_root=doc, structure_name=structure, target_key=target, facet_key=facet, options=options) 

311 ) 

312 

313 

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) 

359 

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) 

366 

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) 

376 

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) 

385 

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) 

394 

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) 

399 

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) 

410 

411 

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) 

422 

423 return sys.exit(0) 

424 

425 

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

435 

436 

437@app.command('version') 

438def app_version() -> None: 

439 """ 

440 Display the application version and exit. 

441 """ 

442 callback(True)