Coverage for etiketti/discover.py: 80.00%
74 statements
« prev ^ index » next coverage.py v7.8.2, created at 2025-06-09 05:40:04 +00:00
« prev ^ index » next coverage.py v7.8.2, created at 2025-06-09 05:40:04 +00:00
1import hashlib
2import io
3import pathlib
4import platform
5import subprocess # nosec B404
6from typing import Any, Callable, Union, no_type_check
8import yaml
10from etiketti import (
11 DEFAULT_AUTHOR,
12 ENCODING,
13 LOG_SEPARATOR,
14 ContextType,
15 ConventionsType,
16 PathLike,
17 log,
18)
20CHUNK_SIZE = 2 << 15
23def get_producer() -> str:
24 """Assume the producer is fixed and retrieve the terse version repr from a --version call."""
25 version_text = 'Version unknown'
26 proc = subprocess.Popen(['lualatex', '--version'], stdout=subprocess.PIPE) # nosec B603, B607
27 for line in io.TextIOWrapper(proc.stdout, encoding='utf-8'): # type: ignore 27 ↛ 31line 27 didn't jump to line 31 because the loop on line 27 didn't complete
28 if line.startswith('This is LuaHBTeX, Version '): 28 ↛ 27line 28 didn't jump to line 27 because the condition on line 28 was always true
29 version_text = line.rstrip()
30 break
31 log.info(f'producer version banner: ({version_text})')
33 # Example: 'This is LuaHBTeX, Version 1.15.0 (TeX Live 2022)'
34 engine = 'lltx'
35 version = version_text.split('Version ', 1)[1].rstrip().replace(' (TeX Live ', '-txlv-').rstrip(')')
36 where = platform.platform().lower()
37 producer_version = f'{engine}-{version}-{where}'
38 log.info(f'- noting as: {producer_version=}')
39 log.info(LOG_SEPARATOR)
40 return producer_version
43def hash_file(path: pathlib.Path, hasher: Union[Callable[..., Any], None] = None) -> str:
44 """Return the SHA512 hex digest of the data from file."""
45 if hasher is None: 45 ↛ 47line 45 didn't jump to line 47 because the condition on line 45 was always true
46 hasher = hashlib.sha512
47 the_hash = hasher()
48 try:
49 with open(path, 'rb') as handle:
50 while chunk := handle.read(CHUNK_SIZE):
51 the_hash.update(chunk)
52 return the_hash.hexdigest()
53 except FileNotFoundError as err:
54 log.warn(f'hash file failed with: ({err})')
55 return 'error:plain:file-to-hash-not-found'
58@no_type_check
59def load_label_context(path: PathLike) -> ContextType:
60 """Load the label context providing prefix, site-id, and action-id."""
61 with open(path, 'rt', encoding=ENCODING) as handle:
62 return yaml.safe_load(handle)
65@no_type_check
66def extract_author(path: PathLike) -> str:
67 """Extract the author from the approvals file if DEFAULT_AUTHOR is false-like."""
68 if DEFAULT_AUTHOR: 68 ↛ 69line 68 didn't jump to line 69 because the condition on line 68 was never true
69 return DEFAULT_AUTHOR
70 try:
71 with open(path, 'rt', encoding=ENCODING) as handle:
72 approvals = yaml.safe_load(handle)
73 entries = approvals['approvals']
74 for entry in entries: 74 ↛ 79line 74 didn't jump to line 79 because the loop on line 74 didn't complete
75 if entry.get('role').lower() == 'author': 75 ↛ 74line 75 didn't jump to line 74 because the condition on line 75 was always true
76 return entry.get('name', '') or DEFAULT_AUTHOR
77 except FileNotFoundError as err:
78 log.warning(f'extract author failed with: ({err})')
79 return DEFAULT_AUTHOR
82def extract_meta_parts(path: PathLike) -> tuple[str, str, str]:
83 """Extract the title, subject, keywords in that order from the metadata file."""
84 try:
85 with open(path, 'rt', encoding=ENCODING) as handle:
86 metadata = yaml.safe_load(handle)
87 mapping = metadata['document']['common']
88 title = mapping.get('title', '').replace('\\\\', '').replace(' ', ' ').title()
89 subject = mapping.get('header_id', '')
90 if subject and subject.startswith('Issue,'): 90 ↛ 91line 90 didn't jump to line 91 because the condition on line 90 was never true
91 subject = mapping.get('header_issue_revision_combined_label', '')
92 else:
93 subject = ''
94 keywords = mapping.get('keywords_csl', '')
95 return title or '', subject or '', keywords or ''
96 except FileNotFoundError as err:
97 log.warning(f'extract meta parts failed with: ({err})')
98 return '', '', ''
101def load_conventions(context: ContextType, path: PathLike) -> ConventionsType:
102 """Derive conventions from path to input pdf file."""
103 in_pdf = pathlib.Path(path)
104 workspace = in_pdf.parent
105 names = context['label']
106 return {
107 'workspace-folder-path': workspace,
108 'approvals-yml-path': workspace / names.get('approvals-yml-name', 'approvals.yml'),
109 'metadata-yml-path': workspace / names.get('metadata-yml-name', 'metadata.yml'),
110 'bookmatter-tex-path': workspace / names.get('bookmatter-tex-name', 'bookmatter.tex'),
111 'document-tex-path': workspace / names.get('document-tex-name', 'document.tex'),
112 'driver-tex-path': workspace / names.get('driver-tex-name', 'driver.tex'),
113 'metadata-tex-path': workspace / names.get('metadata-tex-name', 'metadata.tex'),
114 'publisher-tex-path': workspace / names.get('publisher-tex-name', 'publisher.tex'),
115 'setup-tex-path': workspace / names.get('setup-tex-name', 'setup.tex'),
116 'this-tex-path': workspace / names.get('this-tex-name', 'this.tex'),
117 }