veridret 1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- veridret/__init__.py +61 -0
- veridret/__main__.py +302 -0
- veridret/_cli.py +240 -0
- veridret/_comparison.py +95 -0
- veridret/_config.py +181 -0
- veridret/_logging.py +206 -0
- veridret/_plugin.py +40 -0
- veridret/_pydantic.py +98 -0
- veridret/_sw.py +320 -0
- veridret/_unset.py +86 -0
- veridret/_version_info.py +538 -0
- veridret/json.py +177 -0
- veridret/logging.py +326 -0
- veridret-1.dist-info/METADATA +24 -0
- veridret-1.dist-info/RECORD +18 -0
- veridret-1.dist-info/WHEEL +4 -0
- veridret-1.dist-info/entry_points.txt +2 -0
- veridret-1.dist-info/licenses/LICENSE.md +50 -0
veridret/__init__.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
VerIdRet (*Ver*sion-*Id*entifier *Ret*riever),
|
|
5
|
+
a program that retrieves the version identifiers
|
|
6
|
+
of the locally installed and upstream versions of software.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import functools
|
|
10
|
+
import os
|
|
11
|
+
import os.path
|
|
12
|
+
import typing
|
|
13
|
+
from typing import Final
|
|
14
|
+
|
|
15
|
+
__all__: Final = [
|
|
16
|
+
'get_config_dirs',
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
__version__: Final = '1'
|
|
20
|
+
|
|
21
|
+
PATH_LIST_SEP: Final = ':'
|
|
22
|
+
|
|
23
|
+
@typing.overload
|
|
24
|
+
def getenv_if_abs_path[T](key: str, default: T) -> str | T:
|
|
25
|
+
...
|
|
26
|
+
@typing.overload
|
|
27
|
+
def getenv_if_abs_path(key: str) -> str | None:
|
|
28
|
+
...
|
|
29
|
+
def getenv_if_abs_path[T](
|
|
30
|
+
key: str, default: T | None = None,
|
|
31
|
+
) -> str | T | None:
|
|
32
|
+
value = os.getenv(key)
|
|
33
|
+
if value is None or not os.path.isabs(value):
|
|
34
|
+
return default
|
|
35
|
+
return value
|
|
36
|
+
|
|
37
|
+
@functools.cache
|
|
38
|
+
def get_config_dirs() -> tuple[str, ...]:
|
|
39
|
+
"""
|
|
40
|
+
Get the paths to the program's configuration directories.
|
|
41
|
+
|
|
42
|
+
:return:
|
|
43
|
+
A tuple of the paths to the program's configuration directories,
|
|
44
|
+
in order of decreasing priority.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
config_dirs = os.getenv('VERIDRET_CONFIG')
|
|
48
|
+
if config_dirs is not None:
|
|
49
|
+
return tuple(config_dirs.split(PATH_LIST_SEP))
|
|
50
|
+
|
|
51
|
+
xdg_config_home = getenv_if_abs_path('XDG_CONFIG_HOME')
|
|
52
|
+
if xdg_config_home is None:
|
|
53
|
+
home_dir = getenv_if_abs_path('HOME', '/')
|
|
54
|
+
xdg_config_home = os.path.join(home_dir, '.config')
|
|
55
|
+
|
|
56
|
+
system_config_home = '/etc'
|
|
57
|
+
|
|
58
|
+
return tuple(
|
|
59
|
+
os.path.join(x, 'veridret')
|
|
60
|
+
for x in (xdg_config_home, system_config_home)
|
|
61
|
+
)
|
veridret/__main__.py
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
The program's entry point.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import dataclasses
|
|
8
|
+
import enum
|
|
9
|
+
import json
|
|
10
|
+
import io
|
|
11
|
+
import sys
|
|
12
|
+
import threading
|
|
13
|
+
from collections.abc import MutableSequence, Sequence
|
|
14
|
+
from contextlib import ExitStack
|
|
15
|
+
from queue import Queue, ShutDown as QueueShutDown
|
|
16
|
+
from typing import Any, Final, Never, TypeIs
|
|
17
|
+
|
|
18
|
+
import regex
|
|
19
|
+
|
|
20
|
+
import structlog
|
|
21
|
+
|
|
22
|
+
import veridret.json
|
|
23
|
+
from veridret._cli import parse as parse_args
|
|
24
|
+
from veridret._config import Config
|
|
25
|
+
from veridret._logging import init as init_logging
|
|
26
|
+
from veridret._sw import Config as SwConfig
|
|
27
|
+
from veridret._unset import Unset
|
|
28
|
+
from veridret._version_info import Fields as VersionInfoFields, VersionInfo
|
|
29
|
+
from veridret.logging import Logger
|
|
30
|
+
|
|
31
|
+
__all__: Final = [
|
|
32
|
+
'main',
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
g_logger: Final[Logger] = structlog.get_logger(logger_name=__name__)
|
|
36
|
+
|
|
37
|
+
def json_seq_dump(
|
|
38
|
+
obj: object, fp: io.Writer[str], **kwargs: Any,
|
|
39
|
+
) -> None:
|
|
40
|
+
fp.write('\N{RECORD SEPARATOR}')
|
|
41
|
+
json.dump(obj, fp, **kwargs)
|
|
42
|
+
fp.write('\n')
|
|
43
|
+
|
|
44
|
+
def sw_proc_thread_main_impl(
|
|
45
|
+
*,
|
|
46
|
+
queue: Queue[tuple[str, SwConfig]],
|
|
47
|
+
version_info_fields: VersionInfoFields,
|
|
48
|
+
output_lock: threading.Lock,
|
|
49
|
+
) -> bool:
|
|
50
|
+
name_field_mask = version_info_fields & VersionInfoFields.NAME
|
|
51
|
+
ok = True
|
|
52
|
+
|
|
53
|
+
while True:
|
|
54
|
+
try:
|
|
55
|
+
name, config = queue.get()
|
|
56
|
+
except QueueShutDown:
|
|
57
|
+
break
|
|
58
|
+
|
|
59
|
+
with ExitStack() as exit_stack:
|
|
60
|
+
exit_stack.callback(queue.task_done)
|
|
61
|
+
|
|
62
|
+
logger = g_logger.bind(
|
|
63
|
+
software={
|
|
64
|
+
'name': name,
|
|
65
|
+
'config': config,
|
|
66
|
+
},
|
|
67
|
+
requested_version_info_fields=version_info_fields,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
logger.info('processing software')
|
|
71
|
+
|
|
72
|
+
version_info = VersionInfo.compute(config, version_info_fields)
|
|
73
|
+
actual_fields = version_info.field_mask() | name_field_mask
|
|
74
|
+
if version_info_fields not in actual_fields:
|
|
75
|
+
ok = False
|
|
76
|
+
logger.error(
|
|
77
|
+
'cannot compute all requested version-info fields',
|
|
78
|
+
actual_version_info_fields=actual_fields,
|
|
79
|
+
cause='(previous error(s))',
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
version_info_with_name = {
|
|
83
|
+
'name': name if name_field_mask else Unset(),
|
|
84
|
+
'local': version_info.local,
|
|
85
|
+
'upstream': version_info.upstream,
|
|
86
|
+
'comparison': version_info.comparison,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
output_obj = {
|
|
90
|
+
k: v
|
|
91
|
+
for k, v in version_info_with_name.items()
|
|
92
|
+
if not isinstance(v, Unset)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
with output_lock:
|
|
97
|
+
json_seq_dump(
|
|
98
|
+
output_obj,
|
|
99
|
+
sys.stdout,
|
|
100
|
+
skipkeys=True,
|
|
101
|
+
separators=(',', ':'),
|
|
102
|
+
default=veridret.json.to_directly_encodable,
|
|
103
|
+
)
|
|
104
|
+
# pylint: disable-next=broad-exception-caught
|
|
105
|
+
except Exception as e:
|
|
106
|
+
ok = False
|
|
107
|
+
logger.error('cannot output version info', cause=e)
|
|
108
|
+
|
|
109
|
+
logger.info('done processing software')
|
|
110
|
+
|
|
111
|
+
return ok
|
|
112
|
+
|
|
113
|
+
def sw_proc_thread_main(
|
|
114
|
+
index: int,
|
|
115
|
+
*,
|
|
116
|
+
queue: Queue[tuple[str, SwConfig]],
|
|
117
|
+
oks: MutableSequence[bool],
|
|
118
|
+
version_info_fields: VersionInfoFields,
|
|
119
|
+
output_lock: threading.Lock,
|
|
120
|
+
) -> None:
|
|
121
|
+
oks[index] = sw_proc_thread_main_impl(
|
|
122
|
+
queue=queue,
|
|
123
|
+
version_info_fields=version_info_fields,
|
|
124
|
+
output_lock=output_lock,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
128
|
+
class SwProcessingThreadInfo:
|
|
129
|
+
threads: Sequence[threading.Thread]
|
|
130
|
+
oks: MutableSequence[bool]
|
|
131
|
+
|
|
132
|
+
def create_sw_proc_threads(
|
|
133
|
+
n: int,
|
|
134
|
+
queue: Queue[tuple[str, SwConfig]],
|
|
135
|
+
version_info_fields: VersionInfoFields,
|
|
136
|
+
) -> SwProcessingThreadInfo:
|
|
137
|
+
oks = [False] * n
|
|
138
|
+
output_lock = threading.Lock()
|
|
139
|
+
threads = tuple(
|
|
140
|
+
threading.Thread(
|
|
141
|
+
target=sw_proc_thread_main,
|
|
142
|
+
args=(i,),
|
|
143
|
+
kwargs={
|
|
144
|
+
'queue': queue,
|
|
145
|
+
'oks': oks,
|
|
146
|
+
'version_info_fields': version_info_fields,
|
|
147
|
+
'output_lock': output_lock,
|
|
148
|
+
},
|
|
149
|
+
)
|
|
150
|
+
for i in range(n)
|
|
151
|
+
)
|
|
152
|
+
return SwProcessingThreadInfo(threads=threads, oks=oks)
|
|
153
|
+
|
|
154
|
+
@dataclasses.dataclass(frozen=True)
|
|
155
|
+
class SoftwareNamePatternFilter:
|
|
156
|
+
pattern: regex.Pattern[str] | None = None
|
|
157
|
+
|
|
158
|
+
def __call__(self, name: str) -> bool:
|
|
159
|
+
return self.pattern is None or bool(self.pattern.search(name))
|
|
160
|
+
|
|
161
|
+
def load_and_enqueue_software(
|
|
162
|
+
queue: Queue[tuple[str, SwConfig]],
|
|
163
|
+
name_filter: SoftwareNamePatternFilter,
|
|
164
|
+
) -> bool:
|
|
165
|
+
logger = g_logger.bind()
|
|
166
|
+
|
|
167
|
+
ok = True
|
|
168
|
+
|
|
169
|
+
def sw_filter(
|
|
170
|
+
software: tuple[str, SwConfig | None] | None,
|
|
171
|
+
) -> TypeIs[tuple[str, SwConfig]]:
|
|
172
|
+
nonlocal ok
|
|
173
|
+
match software:
|
|
174
|
+
case name, config:
|
|
175
|
+
if not name_filter(name):
|
|
176
|
+
logger.debug(
|
|
177
|
+
'discarding software from processing',
|
|
178
|
+
software=name,
|
|
179
|
+
software_name_pattern=name_filter.pattern,
|
|
180
|
+
cause='software-name pattern mismatch',
|
|
181
|
+
)
|
|
182
|
+
return False
|
|
183
|
+
match config:
|
|
184
|
+
case SwConfig():
|
|
185
|
+
return True
|
|
186
|
+
case None:
|
|
187
|
+
ok = False
|
|
188
|
+
return False
|
|
189
|
+
case None:
|
|
190
|
+
ok = False
|
|
191
|
+
return False
|
|
192
|
+
|
|
193
|
+
logger.debug('enqueuing software for processing')
|
|
194
|
+
|
|
195
|
+
sw_iter = (x for x in SwConfig.load_all() if sw_filter(x))
|
|
196
|
+
for name, config in sw_iter:
|
|
197
|
+
sw_logger = logger.bind(
|
|
198
|
+
software={
|
|
199
|
+
'name': name,
|
|
200
|
+
'config': config,
|
|
201
|
+
},
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
sw_logger.debug(
|
|
205
|
+
'enqueuing software for processing',
|
|
206
|
+
)
|
|
207
|
+
queue.put((name, config))
|
|
208
|
+
sw_logger.debug(
|
|
209
|
+
'done enqueuing software for processing',
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
logger.debug('done enqueuing software for processing')
|
|
213
|
+
|
|
214
|
+
return ok
|
|
215
|
+
|
|
216
|
+
class ExitCode(enum.Enum):
|
|
217
|
+
SUCCESS = 0
|
|
218
|
+
FAILURE = 1
|
|
219
|
+
|
|
220
|
+
def __int__(self) -> int:
|
|
221
|
+
return self.value
|
|
222
|
+
|
|
223
|
+
def main() -> Never:
|
|
224
|
+
"""
|
|
225
|
+
Run the program
|
|
226
|
+
and then terminate the current process.
|
|
227
|
+
|
|
228
|
+
This function is the program's entry point
|
|
229
|
+
and terminates the current process upon completion.
|
|
230
|
+
If the program's command-line arguments are incorrect,
|
|
231
|
+
then the exit code is 2;
|
|
232
|
+
otherwise,
|
|
233
|
+
if program encounteres unrecoverable errors,
|
|
234
|
+
then the exit code is 1;
|
|
235
|
+
otherwise,
|
|
236
|
+
the exit code is 0.
|
|
237
|
+
|
|
238
|
+
:return: This function does not return.
|
|
239
|
+
"""
|
|
240
|
+
|
|
241
|
+
args = parse_args()
|
|
242
|
+
|
|
243
|
+
init_logging(
|
|
244
|
+
min_level=args.min_logging_level,
|
|
245
|
+
fmt=args.log_format,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
logger = g_logger.bind()
|
|
249
|
+
|
|
250
|
+
config = Config.load(
|
|
251
|
+
max_job_count=args.max_job_count,
|
|
252
|
+
job_queue_capacity=args.job_queue_capacity,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
sw_queue: Queue[tuple[str, SwConfig]] = Queue(
|
|
256
|
+
config.job_queue_capacity,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
sw_proc_thread_info = create_sw_proc_threads(
|
|
260
|
+
config.max_job_count, sw_queue, args.version_info_fields,
|
|
261
|
+
)
|
|
262
|
+
sw_proc_threads = sw_proc_thread_info.threads
|
|
263
|
+
sw_proc_thread_oks = sw_proc_thread_info.oks
|
|
264
|
+
|
|
265
|
+
logger.debug('starting software-processing threads')
|
|
266
|
+
for i, t in enumerate(sw_proc_threads):
|
|
267
|
+
logger.debug(
|
|
268
|
+
'starting software-processing thread',
|
|
269
|
+
software_processing_thread=t,
|
|
270
|
+
software_processing_thread_index=i,
|
|
271
|
+
)
|
|
272
|
+
t.start()
|
|
273
|
+
logger.debug('done starting software-processing threads')
|
|
274
|
+
|
|
275
|
+
sw_loading_ok = load_and_enqueue_software(
|
|
276
|
+
sw_queue,
|
|
277
|
+
SoftwareNamePatternFilter(args.software_name_pattern),
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
logger.debug('shutting down software queue')
|
|
281
|
+
sw_queue.shutdown()
|
|
282
|
+
|
|
283
|
+
logger.debug('joining software-processing threads')
|
|
284
|
+
for i, t in enumerate(sw_proc_threads):
|
|
285
|
+
logger.debug(
|
|
286
|
+
'joining software-processing thread',
|
|
287
|
+
software_processing_thread_index=i,
|
|
288
|
+
software_processing_thread=t,
|
|
289
|
+
)
|
|
290
|
+
t.join()
|
|
291
|
+
logger.debug('done joining software-processing threads')
|
|
292
|
+
|
|
293
|
+
ok = sw_loading_ok and all(sw_proc_thread_oks)
|
|
294
|
+
if not ok:
|
|
295
|
+
logger.error('one or more unrecoverable errors occurred')
|
|
296
|
+
|
|
297
|
+
exit_code = ExitCode.SUCCESS if ok else ExitCode.FAILURE
|
|
298
|
+
logger.info('exitting', exit_code=exit_code)
|
|
299
|
+
sys.exit(int(exit_code))
|
|
300
|
+
|
|
301
|
+
if __name__ == '__main__':
|
|
302
|
+
main()
|
veridret/_cli.py
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
The program's command-line interface.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import dataclasses
|
|
9
|
+
import enum
|
|
10
|
+
import functools
|
|
11
|
+
import logging
|
|
12
|
+
import typing
|
|
13
|
+
from typing import Final
|
|
14
|
+
|
|
15
|
+
import regex
|
|
16
|
+
|
|
17
|
+
import veridret
|
|
18
|
+
from veridret._logging import Format as LogFormat
|
|
19
|
+
from veridret._version_info import Fields as VersionInfoFields
|
|
20
|
+
from veridret.logging import Level as RealLoggingLevel
|
|
21
|
+
|
|
22
|
+
__all__: Final = [
|
|
23
|
+
'Args',
|
|
24
|
+
'parse',
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
def parse_regex(pattern: str) -> regex.Pattern[str]:
|
|
28
|
+
try:
|
|
29
|
+
return regex.compile(pattern)
|
|
30
|
+
except regex.error as e:
|
|
31
|
+
raise argparse.ArgumentTypeError(
|
|
32
|
+
f'invalid regular expression: {pattern!r}: {e}',
|
|
33
|
+
) from e
|
|
34
|
+
|
|
35
|
+
def parse_version_info_fields(fields: str) -> VersionInfoFields:
|
|
36
|
+
def acc_func(
|
|
37
|
+
acc: VersionInfoFields, index_char_pair: tuple[int, str],
|
|
38
|
+
) -> VersionInfoFields:
|
|
39
|
+
match index_char_pair:
|
|
40
|
+
case _, 'n':
|
|
41
|
+
field = VersionInfoFields.NAME
|
|
42
|
+
case _, 'l':
|
|
43
|
+
field = VersionInfoFields.LOCAL
|
|
44
|
+
case _, 'u':
|
|
45
|
+
field = VersionInfoFields.UPSTREAM
|
|
46
|
+
case _, 'c':
|
|
47
|
+
field = VersionInfoFields.COMPARISON
|
|
48
|
+
case i, c:
|
|
49
|
+
raise ValueError(f'invalid character at position {i}: {c!r}')
|
|
50
|
+
return acc | field
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
return functools.reduce(
|
|
54
|
+
acc_func,
|
|
55
|
+
enumerate(fields),
|
|
56
|
+
initial=VersionInfoFields(0),
|
|
57
|
+
)
|
|
58
|
+
except ValueError as e:
|
|
59
|
+
raise (
|
|
60
|
+
argparse.ArgumentTypeError(f'invalid FIELDS: {fields!r}: {e}')
|
|
61
|
+
) from e
|
|
62
|
+
|
|
63
|
+
class LoggingLevel(enum.Enum):
|
|
64
|
+
DEBUG = enum.auto()
|
|
65
|
+
INFO = enum.auto()
|
|
66
|
+
WARNING = enum.auto()
|
|
67
|
+
ERROR = enum.auto()
|
|
68
|
+
CRITICAL = enum.auto()
|
|
69
|
+
OFF = enum.auto()
|
|
70
|
+
|
|
71
|
+
@typing.override
|
|
72
|
+
def __str__(self) -> str:
|
|
73
|
+
return self.name.lower()
|
|
74
|
+
|
|
75
|
+
def get_logging_level(self) -> RealLoggingLevel | None:
|
|
76
|
+
match self:
|
|
77
|
+
case LoggingLevel.OFF:
|
|
78
|
+
return None
|
|
79
|
+
case _:
|
|
80
|
+
return getattr(logging, self.name)
|
|
81
|
+
|
|
82
|
+
def parse_enum_uppercase[T: enum.Enum](value: str, cls: type[T]) -> T:
|
|
83
|
+
try:
|
|
84
|
+
return cls[value.upper()]
|
|
85
|
+
except KeyError as e:
|
|
86
|
+
choices = ', '.join(repr(x.name.lower()) for x in cls)
|
|
87
|
+
raise argparse.ArgumentTypeError(
|
|
88
|
+
f'invalid choice: {value!r} (choose from {choices})',
|
|
89
|
+
) from e
|
|
90
|
+
|
|
91
|
+
@dataclasses.dataclass(kw_only=True)
|
|
92
|
+
class Args:
|
|
93
|
+
"""
|
|
94
|
+
The parsed arguments of the program.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
software_name_pattern: regex.Pattern[str] | None
|
|
98
|
+
version_info_fields: VersionInfoFields
|
|
99
|
+
max_job_count: int | None
|
|
100
|
+
job_queue_capacity: int | None
|
|
101
|
+
log_format: LogFormat
|
|
102
|
+
min_logging_level: RealLoggingLevel | None
|
|
103
|
+
|
|
104
|
+
def parse() -> Args:
|
|
105
|
+
"""
|
|
106
|
+
Parse the arguments to the program.
|
|
107
|
+
|
|
108
|
+
This function terminates the current process
|
|
109
|
+
if it cannot parse the program arguments into an ``Args``,
|
|
110
|
+
such as if the program arguments are incorrect
|
|
111
|
+
or if they include the command-line option ``--help``.
|
|
112
|
+
|
|
113
|
+
:return: The parsed program arguments.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
parser = argparse.ArgumentParser(
|
|
117
|
+
description='''
|
|
118
|
+
VerIdRet (Version-Identifier Retriever)
|
|
119
|
+
is a program that retrieves the version identifiers
|
|
120
|
+
of the locally installed and upstream versions of software.
|
|
121
|
+
The program outputs the version information
|
|
122
|
+
of each piece of software configured with it
|
|
123
|
+
to standard output as a record,
|
|
124
|
+
specifically a JSON object,
|
|
125
|
+
in a JSON stream of MIME type application/json-seq.
|
|
126
|
+
''',
|
|
127
|
+
)
|
|
128
|
+
parser.add_argument(
|
|
129
|
+
'-V', '--version',
|
|
130
|
+
action='version',
|
|
131
|
+
version=f'VerIdRet {veridret.__version__}',
|
|
132
|
+
help='''
|
|
133
|
+
show the program's version information and exit
|
|
134
|
+
''',
|
|
135
|
+
)
|
|
136
|
+
parser.add_argument(
|
|
137
|
+
'-n', '--name',
|
|
138
|
+
type=parse_regex,
|
|
139
|
+
metavar='PATTERN',
|
|
140
|
+
help='''
|
|
141
|
+
process only software any substring of whose name matches this pattern,
|
|
142
|
+
whose syntax and semantics
|
|
143
|
+
are those of the patterns in the Python package "regex",
|
|
144
|
+
overriding the default behavior
|
|
145
|
+
of processing all software regardless of name
|
|
146
|
+
''',
|
|
147
|
+
)
|
|
148
|
+
parser.add_argument(
|
|
149
|
+
'-f', '--fields',
|
|
150
|
+
type=parse_version_info_fields,
|
|
151
|
+
default='nluc',
|
|
152
|
+
help='''
|
|
153
|
+
what fields to place in the record,
|
|
154
|
+
a JSON object,
|
|
155
|
+
of each piece of software's version information
|
|
156
|
+
that is outputted to standard output;
|
|
157
|
+
each character of FIELDS represents
|
|
158
|
+
a field to include in the record,
|
|
159
|
+
the allowed characters being
|
|
160
|
+
'n' for the name of the software
|
|
161
|
+
as the field "name",
|
|
162
|
+
'l' for the locally installed version of the software
|
|
163
|
+
as the field "local",
|
|
164
|
+
'u' for the upstream version of the software
|
|
165
|
+
as the field "upstream",
|
|
166
|
+
and 'c' for the comparison,
|
|
167
|
+
of the locally installed to the upstream version of the software,
|
|
168
|
+
as the field "comparison";
|
|
169
|
+
each field occurs at most once in the record,
|
|
170
|
+
no matter how many times its character occurs in FIELDS;
|
|
171
|
+
if computing the value of a field specified in FIELDS fails,
|
|
172
|
+
then the record will not contain that field
|
|
173
|
+
(computing the value of the field "name" never fails)
|
|
174
|
+
[default: %(default)r]
|
|
175
|
+
''',
|
|
176
|
+
)
|
|
177
|
+
parser.add_argument(
|
|
178
|
+
'-j', '--jobs',
|
|
179
|
+
type=int,
|
|
180
|
+
metavar='N',
|
|
181
|
+
help='''
|
|
182
|
+
the maximum number of pieces of software to process in parallel;
|
|
183
|
+
overrides the setting 'max_job_count'
|
|
184
|
+
in the 'veridret.toml' configuration files
|
|
185
|
+
''',
|
|
186
|
+
)
|
|
187
|
+
parser.add_argument(
|
|
188
|
+
'-Q', '--queue-capacity',
|
|
189
|
+
type=int,
|
|
190
|
+
metavar='N',
|
|
191
|
+
help='''
|
|
192
|
+
the maximum number of unprocessed pieces of software
|
|
193
|
+
to have queued for eventual processing;
|
|
194
|
+
overrides the setting 'job_queue_capacity'
|
|
195
|
+
in the 'veridret.toml' configuration files
|
|
196
|
+
''',
|
|
197
|
+
)
|
|
198
|
+
parser.add_argument(
|
|
199
|
+
'-F', '--log-format',
|
|
200
|
+
metavar=f'{{{','.join(x.name.lower() for x in LogFormat)}}}',
|
|
201
|
+
type=lambda x: parse_enum_uppercase(x, LogFormat),
|
|
202
|
+
default=LogFormat.HUMAN,
|
|
203
|
+
help='''
|
|
204
|
+
the program's log's format;
|
|
205
|
+
case-insensitive;
|
|
206
|
+
'human' writes each log entry to standard error upon a new line
|
|
207
|
+
and formats the entry in a human-readable way;
|
|
208
|
+
'json' formats each log entry
|
|
209
|
+
as a JSON object of the form
|
|
210
|
+
{
|
|
211
|
+
"event": "<log-message>",
|
|
212
|
+
"level": "<logging-level>"
|
|
213
|
+
[, ...]
|
|
214
|
+
}
|
|
215
|
+
and writes such objects together to standard error
|
|
216
|
+
as a JSON stream of MIME type application/json-seq
|
|
217
|
+
[default: '%(default)s']
|
|
218
|
+
''',
|
|
219
|
+
)
|
|
220
|
+
parser.add_argument(
|
|
221
|
+
'-m', '--min-logging-level', '--logging-level',
|
|
222
|
+
metavar=f'{{{','.join(x.name.lower() for x in LoggingLevel)}}}',
|
|
223
|
+
type=lambda x: parse_enum_uppercase(x, LoggingLevel),
|
|
224
|
+
default=LoggingLevel.INFO,
|
|
225
|
+
help='''
|
|
226
|
+
the minimum logging level;
|
|
227
|
+
case-insensitive
|
|
228
|
+
[default: '%(default)s']
|
|
229
|
+
''',
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
args = parser.parse_args()
|
|
233
|
+
return Args(
|
|
234
|
+
software_name_pattern=args.name,
|
|
235
|
+
version_info_fields=args.fields,
|
|
236
|
+
max_job_count=args.jobs,
|
|
237
|
+
job_queue_capacity=args.queue_capacity,
|
|
238
|
+
log_format=args.log_format,
|
|
239
|
+
min_logging_level=args.min_logging_level.get_logging_level(),
|
|
240
|
+
)
|
veridret/_comparison.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
An enumeration representing
|
|
5
|
+
the result of an ordered or unordered comparison.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import enum
|
|
9
|
+
import typing
|
|
10
|
+
from typing import Any, Final
|
|
11
|
+
|
|
12
|
+
__all__: Final = [
|
|
13
|
+
'Comparison',
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
class Comparison(enum.Enum):
|
|
17
|
+
"""
|
|
18
|
+
An enumeration representing
|
|
19
|
+
the result of an ordered or unordered comparison.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
LT = enum.auto()
|
|
23
|
+
EQ = enum.auto()
|
|
24
|
+
GT = enum.auto()
|
|
25
|
+
NE = enum.auto()
|
|
26
|
+
|
|
27
|
+
@typing.override
|
|
28
|
+
def __str__(self) -> str:
|
|
29
|
+
"""
|
|
30
|
+
Convert this ``Comparison`` to a human-readable string.
|
|
31
|
+
|
|
32
|
+
:param self: This ``Comparison``.
|
|
33
|
+
:return: ``self.name``.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
return self.name
|
|
37
|
+
|
|
38
|
+
def __neg__(self) -> Comparison:
|
|
39
|
+
"""
|
|
40
|
+
Negate, or reflect, this ``Comparison``.
|
|
41
|
+
|
|
42
|
+
``Comparison.LT`` and ``Comparison.GT`` are each other's reflection
|
|
43
|
+
while ``Comparison.EQ`` and ``Comparison.NE``
|
|
44
|
+
are each their own reflection.
|
|
45
|
+
|
|
46
|
+
:param self: This ``Comparison``.
|
|
47
|
+
:return: The negation, or reflection, of ``self``.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
match self:
|
|
51
|
+
case Comparison.LT:
|
|
52
|
+
return Comparison.GT
|
|
53
|
+
case Comparison.EQ | Comparison.NE:
|
|
54
|
+
return self
|
|
55
|
+
case Comparison.GT:
|
|
56
|
+
return Comparison.LT
|
|
57
|
+
|
|
58
|
+
@staticmethod
|
|
59
|
+
def from_object_comparison(a: object, b: object) -> Comparison:
|
|
60
|
+
"""
|
|
61
|
+
Compute the ``Comparison`` value
|
|
62
|
+
corresponding to the comparison between two objects.
|
|
63
|
+
|
|
64
|
+
:param a: The left-hand object to compare.
|
|
65
|
+
:param b: The right-hand object to compare.
|
|
66
|
+
:return:
|
|
67
|
+
If ``a < b`` does not raise a ``TypeError``,
|
|
68
|
+
then,
|
|
69
|
+
if ``a < b`` is true,
|
|
70
|
+
then the function returns ``Comparison.LT``,
|
|
71
|
+
else,
|
|
72
|
+
if ``a == b`` is true,
|
|
73
|
+
then the function returns ``Comparison.EQ``,
|
|
74
|
+
else the function returns ``Comparison.GT``;
|
|
75
|
+
if ``a < b`` does raise a ``TypeError``,
|
|
76
|
+
then,
|
|
77
|
+
if ``a == b`` is true,
|
|
78
|
+
then the function returns ``Comparison.EQ``,
|
|
79
|
+
else the function returns ``Comparison.NE``.
|
|
80
|
+
:raise Exception:
|
|
81
|
+
If ``a < b`` or ``a == b`` raises an exception,
|
|
82
|
+
then that exception is propagated,
|
|
83
|
+
except for a ``TypeError`` raised by ``a < b``.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
lt = typing.cast(Any, a) < b
|
|
88
|
+
except TypeError:
|
|
89
|
+
return Comparison.EQ if a == b else Comparison.NE
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
Comparison.LT if lt
|
|
93
|
+
else Comparison.EQ if a == b
|
|
94
|
+
else Comparison.GT
|
|
95
|
+
)
|