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

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

37 

38app = typer.Typer( 

39 add_completion=False, 

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

41 no_args_is_help=True, 

42) 

43 

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) 

114 

115 

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

132 

133 

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) 

152 

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

160 

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

169 

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 ) 

179 

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 ) 

186 

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 

201 

202 

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 

222 

223 return sys.exit( 

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

225 ) 

226 

227 

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 

247 

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 ) 

258 

259 

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 

279 

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 ) 

290 

291 

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 

312 

313 return sys.exit( 

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

315 ) 

316 

317 

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) 

363 

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) 

370 

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) 

380 

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) 

389 

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) 

398 

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) 

403 

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) 

414 

415 

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) 

426 

427 return sys.exit(0) 

428 

429 

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

439 

440 

441@app.command('version') 

442def app_version() -> None: 

443 """ 

444 Display the application version and exit. 

445 """ 

446 callback(True)