Coverage for muuntaa/__init__.py: 93.94%
56 statements
« prev ^ index » next coverage.py v7.6.0, created at 2024-07-21 12:16:47 +00:00
« prev ^ index » next coverage.py v7.6.0, created at 2024-07-21 12:16:47 +00:00
1"""Convert (Finnish: muuntaa) CVRF v1.2 XML to CSAF v2.0 JSON documents."""
3import datetime as dti
4import logging
5import os
6import pathlib
7import re
8import sys
9from typing import Union, no_type_check
11# [[[fill git_describe()]]]
12__version__ = '2024.1.9+parent.abadcafe'
13# [[[end]]]
14__version_info__ = tuple(
15 e if '-' not in e else e.split('-')[0] for part in __version__.split('+') for e in part.split('.') if e != 'parent'
16)
18APP_ALIAS = str(pathlib.Path(__file__).parent.name)
19APP_ENV = APP_ALIAS.upper()
20APP_NAME = locals()['__doc__']
21DEBUG = bool(os.getenv(f'{APP_ENV}_DEBUG', ''))
22ENCODING = 'utf-8'
23ENCODING_ERRORS_POLICY = 'ignore'
24DEFAULT_CONFIG_NAME = f'.{APP_ALIAS}.yml'
25log = logging.getLogger() # Module level logger is sufficient
26LOG_FOLDER = pathlib.Path('logs')
27LOG_FILE = f'{APP_ALIAS}.log'
28LOG_PATH = pathlib.Path(LOG_FOLDER, LOG_FILE) if LOG_FOLDER.is_dir() else pathlib.Path(LOG_FILE)
29LOG_LEVEL = logging.INFO
30VERSION = __version__
31VERSION_DOTTED_TRIPLE = '.'.join(__version_info__[:3])
32TS_FORMAT_LOG = '%Y-%m-%dT%H:%M:%S'
33BOOLEAN_KEYS = ('force', 'fix_insert_current_version_into_revision_history')
34INPUT_FILE_KEY = 'input_file'
35NOW_CODE = 'now'
36OVERWRITABLE_KEYS = [
37 'fix_insert_current_version_into_revision_history',
38 'force_insert_default_reference_category',
39 'remove_CVSS_values_without_vector',
40 'force',
41]
42CSAF_FILE_SUFFIX = '.json'
44# Semantic version is defined in version_t definition.
45# Cf. https://docs.oasis-open.org/csaf/csaf/v2.0/csaf-v2.0.html#3111-version-type
46# and section 9.1.5 Conformance Clause 5: CVRF CSAF converter
47VERSION_PATTERN = re.compile(
48 r'^((0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)'
49 r'(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)'
50 r'(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))'
51 r'?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)$'
52)
54ConfigType = dict[str, Union[None, bool, int, float, str]]
55LogLevel = int
56Pathlike = Union[pathlib.Path, str]
57ScopedMessage = tuple[LogLevel, str]
58ScopedMessages = list[ScopedMessage]
59WriterOptions = Union[None, dict[str, Union[bool, int]]]
62def cleanse_id(id_string: str) -> str:
63 """Strips spaces and linebreaks from the ID string and logs a warning if the ID string was changed."""
64 if (cleansed := id_string.strip().replace('\r', '').replace('\n', '')) != id_string: 64 ↛ 65line 64 didn't jump to line 65 because the condition on line 64 was never true
65 logging.warning('The ID string contained leading/trailing whitespace or linebreaks. These were removed.')
66 return cleansed
69def integer_tuple(text: str) -> tuple[int, ...]:
70 """Convert a string of dotted integers into tuple of integers"""
71 try:
72 return tuple(int(part) for part in text.split('.'))
73 except ValueError:
74 return (sys.maxsize,)
77__all__: list[str] = [
78 'APP_ALIAS',
79 'APP_ENV',
80 'APP_NAME',
81 'BOOLEAN_KEYS',
82 'CSAF_FILE_SUFFIX',
83 'DEBUG',
84 'DEFAULT_CONFIG_NAME',
85 'ConfigType',
86 'ENCODING',
87 'ENCODING_ERRORS_POLICY',
88 'INPUT_FILE_KEY',
89 'LogLevel',
90 'NOW_CODE',
91 'OVERWRITABLE_KEYS',
92 'Pathlike',
93 'ScopedMessage',
94 'ScopedMessages',
95 'VERSION',
96 'VERSION_DOTTED_TRIPLE',
97 'VERSION_PATTERN',
98 'WriterOptions',
99 'cleanse_id',
100 'integer_tuple',
101 'log',
102]
105@no_type_check
106def formatTime_RFC3339(self, record, datefmt=None): # noqa
107 """HACK A DID ACK we could inject .astimezone() to localize ..."""
108 return dti.datetime.fromtimestamp(record.created, dti.timezone.utc).isoformat() # pragma: no cover
111@no_type_check
112def init_logger(name=None, level=None):
113 """Initialize module level logger"""
114 global log # pylint: disable=global-statement
116 log_format = {
117 'format': '%(asctime)s %(levelname)s [%(name)s]: %(message)s',
118 'datefmt': TS_FORMAT_LOG,
119 # 'filename': LOG_PATH,
120 'level': LOG_LEVEL if level is None else level,
121 }
122 logging.Formatter.formatTime = formatTime_RFC3339
123 logging.basicConfig(**log_format)
124 log = logging.getLogger(APP_ENV if name is None else name)
125 log.propagate = True
128init_logger(name=APP_ENV, level=logging.DEBUG if DEBUG else None)