atomicshop 2.14.4__py3-none-any.whl → 2.14.6__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.

Potentially problematic release.


This version of atomicshop might be problematic. Click here for more details.

atomicshop/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
1
  """Atomic Basic functions and classes to make developer life easier"""
2
2
 
3
3
  __author__ = "Den Kras"
4
- __version__ = '2.14.4'
4
+ __version__ = '2.14.6'
atomicshop/datetimes.py CHANGED
@@ -3,6 +3,7 @@ from datetime import timedelta
3
3
  import time
4
4
  import random
5
5
  import re
6
+ from typing import Union
6
7
 
7
8
 
8
9
  class MonthToNumber:
@@ -47,6 +48,28 @@ class MonthToNumber:
47
48
  'דצמבר': '12'}
48
49
 
49
50
 
51
+ # Mapping of datetime format specifiers to regex patterns
52
+ DATE_TIME_STRING_FORMAT_SPECIFIERS_TO_REGEX: dict = {
53
+ '%Y': r'\d{4}', # Year with century
54
+ '%m': r'\d{2}', # Month as a zero-padded decimal number
55
+ '%d': r'\d{2}', # Day of the month as a zero-padded decimal number
56
+ '%H': r'\d{2}', # Hour (24-hour clock) as a zero-padded decimal number
57
+ '%I': r'\d{2}', # Hour (12-hour clock) as a zero-padded decimal number
58
+ '%M': r'\d{2}', # Minute as a zero-padded decimal number
59
+ '%S': r'\d{2}', # Second as a zero-padded decimal number
60
+ '%f': r'\d{6}', # Microsecond as a decimal number, zero-padded on the left
61
+ '%j': r'\d{3}', # Day of the year as a zero-padded decimal number
62
+ '%U': r'\d{2}', # Week number of the year (Sunday as the first day of the week)
63
+ '%W': r'\d{2}', # Week number of the year (Monday as the first day of the week)
64
+ '%w': r'\d', # Weekday as a decimal number (0 = Sunday, 6 = Saturday)
65
+ '%y': r'\d{2}', # Year without century
66
+ '%p': r'(AM|PM)', # AM or PM
67
+ '%z': r'[+-]\d{4}', # UTC offset in the form ±HHMM
68
+ '%Z': r'[A-Z]+', # Time zone name
69
+ '%%': r'%' # Literal '%'
70
+ }
71
+
72
+
50
73
  def get_datetime_from_complex_string_by_pattern(complex_string: str, date_pattern: str) -> tuple[datetime, str, float]:
51
74
  """
52
75
  Function will get datetime object from a complex string by pattern.
@@ -71,6 +94,49 @@ def get_datetime_from_complex_string_by_pattern(complex_string: str, date_patter
71
94
  raise ValueError("No valid date found in the string")
72
95
 
73
96
 
97
+ def datetime_format_to_regex(format_str: str) -> str:
98
+ """
99
+ Convert a datetime format string to a regex pattern.
100
+
101
+ :param format_str: The datetime format string to convert.
102
+ :return: The regex pattern that matches the format string.
103
+
104
+ Example:
105
+ datetime_format_to_regex("%Y-%m-%d")
106
+
107
+ Output:
108
+ '^\\d{4}-\\d{2}-\\d{2}$'
109
+ """
110
+
111
+ # Escape all non-format characters
112
+ escaped_format_str = re.escape(format_str)
113
+
114
+ # Replace escaped format specifiers with their regex equivalents
115
+ for specifier, regex in DATE_TIME_STRING_FORMAT_SPECIFIERS_TO_REGEX.items():
116
+ escaped_format_str = escaped_format_str.replace(re.escape(specifier), regex)
117
+
118
+ # Return the full regex pattern with start and end anchors
119
+ return f"^{escaped_format_str}$"
120
+
121
+
122
+ def extract_datetime_format_from_string(complex_string: str) -> Union[str, None]:
123
+ """
124
+ Extract the datetime format from the suffix used in TimedRotatingFileHandler.
125
+
126
+ Args:
127
+ - suffix: The suffix string from the handler.
128
+
129
+ Returns:
130
+ - The datetime format string, or None if it cannot be determined.
131
+ """
132
+ # Regular expression to match datetime format components in the suffix
133
+ datetime_format_regex = r"%[a-zA-Z]"
134
+ matches = re.findall(datetime_format_regex, complex_string)
135
+ if matches:
136
+ return "".join(matches)
137
+ return None
138
+
139
+
74
140
  def convert_single_digit_to_zero_padded(string: str):
75
141
  """
76
142
  Function will check if string is a single character digit and will add zero in front of it.
@@ -0,0 +1,37 @@
1
+ import configparser
2
+
3
+
4
+ class CategoryNotFoundInConfigError(Exception):
5
+ pass
6
+
7
+
8
+ def edit_property(category: str, category_property: str, value: str, config_file_path: str) -> None:
9
+ """
10
+ Edit a property in the config file.
11
+
12
+ :param category: str, Category in the config file.
13
+ :param category_property: str, Property in the category.
14
+ :param value: str, Value to set to the property.
15
+ :param config_file_path: str, Path to the config file.
16
+
17
+ :return: None.
18
+
19
+ -----------
20
+
21
+ Config Example:
22
+ [category]
23
+ category_property = value
24
+ """
25
+ config = configparser.ConfigParser()
26
+ config.read(config_file_path)
27
+
28
+ if category not in config:
29
+ raise CategoryNotFoundInConfigError(f"Category '{category}' not found in the config file.")
30
+
31
+ # Change the value of the property if it is different from the current value.
32
+ current_value = config[category][category_property]
33
+ if current_value != value:
34
+ config[category][category_property] = value
35
+
36
+ with open(config_file_path, "w") as configfile:
37
+ config.write(configfile)
@@ -83,8 +83,14 @@ class ModuleCategory:
83
83
 
84
84
  # Initiating logger for each engine by its name
85
85
  # initiate_logger(current_module.engine_name, log_file_extension)
86
- loggingw.get_logger_with_stream_handler_and_timedfilehandler(
87
- logger_name=self.engine_name, directory_path=logs_path, disable_duplicate_ms=True)
86
+ loggingw.create_logger(
87
+ logger_name=self.engine_name,
88
+ directory_path=logs_path,
89
+ add_stream=True,
90
+ add_timedfile=True,
91
+ formatter_streamhandler='DEFAULT',
92
+ formatter_filehandler='DEFAULT'
93
+ )
88
94
 
89
95
 
90
96
  # Assigning external class object by message domain received from client. If the domain is not in the list,
@@ -1,5 +1,7 @@
1
1
  import os
2
+ import sys
2
3
  import threading
4
+ import time
3
5
 
4
6
  # Importing atomicshop package to get the version of the package.
5
7
  import atomicshop
@@ -10,7 +12,8 @@ from .connection_thread_worker import thread_worker_main
10
12
  from .. import filesystem, queues
11
13
  from ..python_functions import get_current_python_version_string, check_python_version_compliance
12
14
  from ..wrappers.socketw.socket_wrapper import SocketWrapper
13
- from ..wrappers.socketw.dns_server import DnsServer
15
+ from ..wrappers.socketw import dns_server
16
+ from ..wrappers.psutilw import networks
14
17
  from ..basics import dicts_nested
15
18
  from ..wrappers.loggingw import loggingw
16
19
  from ..print_api import print_api
@@ -41,8 +44,15 @@ def initialize_mitm_server(config_static):
41
44
  config['certificates']['sni_server_certificate_from_server_socket_download_directory'])
42
45
 
43
46
  # Create a logger that will log messages to file, Initiate System logger.
44
- system_logger = loggingw.get_logger_with_stream_handler_and_timedfilehandler(
45
- "system", config['log']['logs_path'], disable_duplicate_ms=True)
47
+ logger_name = "system"
48
+ system_logger = loggingw.create_logger(
49
+ logger_name=logger_name,
50
+ file_path=f'{config['log']['logs_path']}{os.sep}{logger_name}.txt',
51
+ add_stream=True,
52
+ add_timedfile=True,
53
+ formatter_streamhandler='DEFAULT',
54
+ formatter_filehandler='DEFAULT'
55
+ )
46
56
 
47
57
  # Writing first log.
48
58
  system_logger.info("======================================")
@@ -175,14 +185,24 @@ def initialize_mitm_server(config_static):
175
185
  config_static.CONFIG_EXTENDED['certificates']['domains_all_times'] = list(domains_engine_list_full)
176
186
 
177
187
  # Creating Statistics logger.
178
- statistics_logger = loggingw.get_logger_with_stream_handler_and_timedfilehandler(
179
- logger_name="statistics", directory_path=config['log']['logs_path'],
180
- file_extension=config_static.CSV_EXTENSION, formatter_message_only=True, header=STATISTICS_HEADER
188
+ statistics_logger = loggingw.create_logger(
189
+ logger_name="statistics",
190
+ directory_path=config['log']['logs_path'],
191
+ add_timedfile=True,
192
+ formatter_filehandler='MESSAGE',
193
+ file_type='csv',
194
+ header=STATISTICS_HEADER
181
195
  )
182
196
 
183
197
  network_logger_name = "network"
184
- network_logger = loggingw.get_logger_with_stream_handler_and_timedfilehandler(
185
- logger_name=network_logger_name, directory_path=config['log']['logs_path'], disable_duplicate_ms=True)
198
+ network_logger = loggingw.create_logger(
199
+ logger_name=network_logger_name,
200
+ directory_path=config['log']['logs_path'],
201
+ add_stream=True,
202
+ add_timedfile=True,
203
+ formatter_streamhandler='DEFAULT',
204
+ formatter_filehandler='DEFAULT'
205
+ )
186
206
  system_logger.info(f"Loaded network logger: {network_logger}")
187
207
 
188
208
  # Initiate Listener logger, which is a child of network logger, so he uses the same settings and handlers
@@ -194,19 +214,41 @@ def initialize_mitm_server(config_static):
194
214
 
195
215
  # === Initialize DNS module ====================================================================================
196
216
  if config['dns']['enable_dns_server']:
217
+ # Check if the DNS server port is in use.
218
+ port_in_use = networks.get_processes_using_port_list([config['dns']['listening_port']])
219
+ if port_in_use:
220
+ for port, process_info in port_in_use.items():
221
+ message = f"Port [{port}] is already in use by process: {process_info}"
222
+ print_api(message, error_type=True, logger_method='critical', logger=system_logger)
223
+
224
+ # Wait for the message to be printed and saved to file.
225
+ time.sleep(1)
226
+ sys.exit(1)
227
+
197
228
  # before executing TCP sockets and after executing 'network' logger.
198
- dns_server = DnsServer(config)
229
+ dns_server_instance = dns_server.DnsServer(config)
199
230
  # Passing the engine domain list to DNS server to work with.
200
231
  # 'list' function re-initializes the current list, or else it will be the same instance object.
201
- dns_server.domain_list = list(domains_engine_list_full)
232
+ dns_server_instance.domain_list = list(domains_engine_list_full)
202
233
 
203
- dns_server.request_domain_queue = domain_queue
234
+ dns_server_instance.request_domain_queue = domain_queue
204
235
  # Initiate the thread.
205
- threading.Thread(target=dns_server.start).start()
236
+ dns_thread = threading.Thread(target=dns_server_instance.start)
237
+ dns_thread.daemon = True
238
+ dns_thread.start()
206
239
 
207
240
  # === EOF Initialize DNS module ================================================================================
208
241
  # === Initialize TCP Server ====================================================================================
209
242
  if config['tcp']['enable_tcp_server']:
243
+ port_in_use = networks.get_processes_using_port_list(config['tcp']['listening_port_list'])
244
+ if port_in_use:
245
+ for port, process_info in port_in_use.items():
246
+ print_api(f"Port [{port}] is already in use by process: {process_info}", logger=system_logger,
247
+ error_type=True, logger_method='critical')
248
+ # Wait for the message to be printed and saved to file.
249
+ time.sleep(1)
250
+ sys.exit(1)
251
+
210
252
  socket_wrapper = SocketWrapper(
211
253
  config=dicts_nested.merge(config, config_static.CONFIG_EXTENDED), logger=listener_logger,
212
254
  statistics_logger=statistics_logger)
atomicshop/print_api.py CHANGED
@@ -99,15 +99,16 @@ def print_api(
99
99
  # If 'logger.error' should be outputted to console, and 'color' wasn't selected, then set color to 'yellow'.
100
100
  if logger_method == 'error' and not color:
101
101
  color = 'yellow'
102
- error_type = True
103
102
  # If 'logger.critical' should be outputted to console, and 'color' wasn't selected, then set color to 'red'.
104
103
  elif logger_method == 'critical' and not color:
105
104
  color = 'red'
106
- error_type = True
107
105
 
108
106
  if color:
109
107
  message = get_colors_basic_dict(color) + message + ColorsBasic.END
110
108
 
109
+ if logger_method == 'error' or logger_method == 'critical':
110
+ error_type = True
111
+
111
112
  # If exception was raised and 'stderr=True'.
112
113
  if sys.exc_info()[0] is not None and stderr:
113
114
  # If 'traceback' is set to 'True', we'll output traceback of exception.
@@ -1,4 +1,5 @@
1
1
  import logging
2
+ import time
2
3
 
3
4
 
4
5
  # Log formatter, means how the log will look inside the file
@@ -8,32 +9,9 @@ import logging
8
9
 
9
10
  # ".40" truncating the string to only 40 characters. Example: %(message).250s
10
11
 
11
- # Adding '%(asctime)s.%(msecs)06f' will print milliseconds as well as nanoseconds:
12
- # 2022-02-17 15:15:51,913.335562
13
- # If you don't use custom 'datefmt' in your 'setFormatter' function,
14
- # it will print duplicate milliseconds:
15
- # 2022-02-17 15:15:51,913.913.335562
16
- # The setting should be like:
17
- # file_handler.setFormatter(logging.Formatter(log_formatter_file, datefmt='%Y-%m-%d,%H:%M:%S'))
18
- # 's' stands for string. 'd' stands for digits, a.k.a. 'int'. 'f' stands for float.
19
-
20
- # Old tryouts:
21
- # log_formatter_file: str = f"%(asctime)s.%(msecs)06f | " \
22
- # log_formatter_file: str = f"%(asctime)s.%(msecs) | " \
23
- # f"%(levelname)-{len(log_header_level)}s | " \
24
- # f"%(name)-{len(log_header_logger)}s | " \
25
- # f"%(filename)-{len(log_header_script)}s : " \
26
- # f"%(lineno)-{len(log_header_line)}d | " \
27
- # "%(threadName)s: %(message)s"
28
- # log_formatter_file: str = "{asctime}.{msecs:0<3.0f} | " \
29
- # log_formatter_file: str = "{asctime}.{msecs:0>3.0f}.{msecs:0>.6f} | " \
30
-
31
- # Old tryouts for reference:
32
- # file_formatter = logging.Formatter(log_formatter_file, style='{')
33
- # file_formatter.default_time_format = '%Y-%m-%d %H:%M:%S'
34
- # file_formatter.default_msec_format = '%s,%03d'
35
- # file_formatter.default_msec_format = '%s,%03f'
36
12
 
13
+ DEFAULT_STREAM_FORMATTER: str = "%(levelname)s | %(threadName)s | %(name)s | %(message)s"
14
+ DEFAULT_MESSAGE_FORMATTER: str = "%(message)s"
37
15
 
38
16
  FORMAT_ELEMENT_TO_HEADER: dict = {
39
17
  'asctime': 'Event Time [Y-M-D H:M:S]',
@@ -56,7 +34,7 @@ FORMAT_ELEMENT_TO_HEADER: dict = {
56
34
  }
57
35
 
58
36
  DEFAULT_FORMATTER_TXT_FILE: str = \
59
- "{asctime},{msecs:013.9f} | " \
37
+ "{asctime} | " \
60
38
  "{levelname:<" + f"{len(FORMAT_ELEMENT_TO_HEADER['levelname'])}" + "s} | " \
61
39
  "{name:<" + f"{len(FORMAT_ELEMENT_TO_HEADER['name'])}" + "s} | " \
62
40
  "{filename:<" + f"{len(FORMAT_ELEMENT_TO_HEADER['filename'])}" + "s} : " \
@@ -64,7 +42,53 @@ DEFAULT_FORMATTER_TXT_FILE: str = \
64
42
  "{threadName} | {message}"
65
43
 
66
44
  DEFAULT_FORMATTER_CSV_FILE: str = \
67
- '\"{asctime}.{msecs:010.6f}\",{levelname},{name},{filename},{lineno},{threadName},\"{message}\"'
45
+ '\"{asctime}\",{levelname},{name},{filename},{lineno},{threadName},\"{message}\"'
46
+
47
+
48
+ class NanosecondsFormatter(logging.Formatter):
49
+ def __init__(self, fmt=None, datefmt=None, style='%', use_nanoseconds=False):
50
+ super().__init__(fmt, datefmt, style)
51
+ self.use_nanoseconds = use_nanoseconds
52
+
53
+ def formatTime(self, record, datefmt=None):
54
+ ct = self.converter(record.created)
55
+
56
+ if datefmt:
57
+ # Remove unsupported %f from datefmt if present
58
+ if '%f' in datefmt:
59
+ datefmt = datefmt.replace('%f', '')
60
+ self.use_nanoseconds = True
61
+ else:
62
+ # Default time format if datefmt is not provided
63
+ datefmt = '%Y-%m-%d %H:%M:%S'
64
+
65
+ s = time.strftime(datefmt, ct)
66
+
67
+ if self.use_nanoseconds:
68
+ # Calculate nanoseconds from the fractional part of the timestamp
69
+ nanoseconds = f'{record.created:.9f}'.split('.')[1]
70
+ # Return the formatted string with nanoseconds appended
71
+ return f'{s}.{nanoseconds}'
72
+ else:
73
+ return s
74
+
75
+
76
+
77
+
78
+
79
+ # if datefmt is None:
80
+ # # Use the default behavior if no datefmt is provided
81
+ # return super().formatTime(record, datefmt)
82
+ # elif '%f' in datefmt:
83
+ # # Format the time up to seconds
84
+ # base_time = time.strftime(datefmt.replace('%f', ''), ct)
85
+ # # Calculate nanoseconds from the fractional part of the timestamp
86
+ # nanoseconds = f'{record.created:.9f}'.split('.')[1]
87
+ # # Return the formatted string with nanoseconds appended
88
+ # return base_time + nanoseconds
89
+ # else:
90
+ # # Use the provided datefmt if it doesn't include %f
91
+ # return time.strftime(datefmt, ct)
68
92
 
69
93
 
70
94
  class FormatterProcessor:
@@ -150,7 +174,11 @@ class FormatterProcessor:
150
174
 
151
175
 
152
176
  def get_logging_formatter_from_string(
153
- formatter: str, style=None, datefmt=None, disable_duplicate_ms: bool = False) -> logging.Formatter:
177
+ formatter: str,
178
+ style=None,
179
+ datefmt=None,
180
+ use_nanoseconds: bool = False
181
+ ) -> logging.Formatter:
154
182
  """
155
183
  Function to get the logging formatter from the string.
156
184
 
@@ -160,12 +188,12 @@ def get_logging_formatter_from_string(
160
188
  '%': will use the '%' style.
161
189
  '{': will use the '{' style.
162
190
  :param datefmt: string, date format of 'asctime' element. Default is None.
163
- :param disable_duplicate_ms: bool, if True, will disable the duplicate milliseconds in the 'asctime' element.
164
- Example: If we're using '%(asctime)s.%(msecs)06f' msecs value in our time stamp, we need to use custom
165
- 'datefmt' to get rid of the additional duplicate milliseconds:
166
- Instead of '2022-02-17 15:15:51,913.913.335562' print '2022-02-17 15:15:51,913.335562'
167
- The problem with this method is that milliseconds aren't adjusted to 3 digits with zeroes (like 1 = 001).
168
- We can use the regular strftime format: datefmt='%Y-%m-%d,%H:%M:%S:%f'
191
+ We use custom formatter that can process the date format with nanoseconds:
192
+ '%Y-%m-%d %H:%M:%S.%f' -> '2021-01-01 00:00:00.000000000'
193
+ :param use_nanoseconds: bool, if set to True, the formatter will use nanoseconds instead of milliseconds.
194
+ This will print 'asctime' in the following format: '2021-01-01 00:00:00.000000000', instead of
195
+ '2021-01-01 00:00:00.000'.
196
+
169
197
  :return: logging.Formatter, formatter.
170
198
  """
171
199
 
@@ -173,10 +201,8 @@ def get_logging_formatter_from_string(
173
201
  if not style:
174
202
  style = FormatterProcessor(formatter).get_style()['style']
175
203
 
176
- # The regular 'datefmt' is '%Y-%m-%d,%H:%M:%S:%f'. If we want to use it with milliseconds 'msecs' element,
177
- # we need to disable the duplicate milliseconds.
178
- if disable_duplicate_ms:
179
- datefmt = '%Y-%m-%d,%H:%M:%S'
180
-
181
204
  # Create the logging formatter.
182
- return logging.Formatter(formatter, style=style, datefmt=datefmt)
205
+ if use_nanoseconds or '%f' in datefmt:
206
+ return NanosecondsFormatter(formatter, style=style, datefmt=datefmt, use_nanoseconds=use_nanoseconds)
207
+ else:
208
+ return logging.Formatter(formatter, style=style, datefmt=datefmt)