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

1"""Convert (Finnish: muuntaa) CVRF v1.2 XML to CSAF v2.0 JSON documents.""" 

2 

3import datetime as dti 

4import logging 

5import os 

6import pathlib 

7import re 

8import sys 

9from typing import Union, no_type_check 

10 

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) 

17 

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' 

43 

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) 

53 

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

60 

61 

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 

67 

68 

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

75 

76 

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] 

103 

104 

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 

109 

110 

111@no_type_check 

112def init_logger(name=None, level=None): 

113 """Initialize module level logger""" 

114 global log # pylint: disable=global-statement 

115 

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 

126 

127 

128init_logger(name=APP_ENV, level=logging.DEBUG if DEBUG else None)