Coverage for liitos/cli.py: 87.68%

151 statements  

« prev     ^ index     » next       coverage.py v7.4.1, created at 2024-02-13 17:41:25 +00:00

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

2 

3import datetime as dti 

4import logging 

5import os 

6import pathlib 

7import sys 

8from typing import Union 

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

34 

35app = typer.Typer( 

36 add_completion=False, 

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

38 no_args_is_help=True, 

39) 

40 

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) 

111 

112 

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

129 

130 

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

150 

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

159 

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 never false

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 ) 

169 

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 ) 

176 

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 

193 

194 

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 

214 

215 return sys.exit( 

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

217 ) 

218 

219 

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 never false

237 log.error(message) 

238 return 2 

239 

240 return sys.exit( 

241 sig.weave(doc_root=doc, structure_name=structure, target_key=target, facet_key=facet, options=options) 

242 ) 

243 

244 

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 never false

262 log.error(message) 

263 return 2 

264 

265 return sys.exit( 

266 chg.weave(doc_root=doc, structure_name=structure, target_key=target, facet_key=facet, options=options) 

267 ) 

268 

269 

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 never false

288 log.error(message) 

289 return 2 

290 

291 return sys.exit( 

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

293 ) 

294 

295 

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) 

328 

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) 

335 

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) 

342 

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) 

349 

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) 

356 

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) 

361 

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) 

372 

373 

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) 

384 

385 return sys.exit(0) 

386 

387 

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

397 

398 

399@app.command('version') 

400def app_version() -> None: 

401 """ 

402 Display the application version and exit. 

403 """ 

404 callback(True)