atomicshop 2.16.31__py3-none-any.whl → 2.16.32__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.16.31'
4
+ __version__ = '2.16.32'
@@ -1,47 +1,35 @@
1
- def check_3_booleans_when_only_1_can_be_true(
2
- boolean1: tuple,
3
- boolean2: tuple,
4
- boolean3: tuple
1
+ def is_only_1_true_in_list(
2
+ booleans_list_of_tuples: list[tuple],
3
+ raise_if_all_false: bool = True
5
4
  ) -> None:
6
5
  """
7
- Check if only one boolean can be 'True' from 3 booleans
8
-
9
- :param boolean1: tuple, (value, string name of the setting you want to print to the user to be aware of).
10
- :param boolean2: tuple, (value, string name of the setting you want to print to the user to be aware of).
11
- :param boolean3: tuple, (value, string name of the setting you want to print to the user to be aware of).
12
- :return:
13
-
14
- ------------------------------------------------
15
-
16
- Example:
17
- check_3_booleans_when_only_1_can_be_true(
18
- (self.config['section']['default_usage'], 'default_usage'),
19
- (self.config['section']['create_usage'], 'create_usage'),
20
- (self.config['section']['custom_usage'], 'custom_usage'))
6
+ Check if only one boolean can be 'True' from a list of booleans
7
+ :param booleans_list_of_tuples: list of tuples, Structure:
8
+ [(value, string name of the setting you want to print to the user to be aware of), ...]
9
+ :param raise_if_all_false: bool, If True, exception will be raised if all booleans are False.
10
+ :return: None
21
11
  """
22
12
 
23
- check_if_3_booleans_are_false(boolean1, boolean2, boolean3)
24
- check_if_2_booleans_are_true(boolean1, boolean2)
25
- check_if_2_booleans_are_true(boolean1, boolean3)
26
- check_if_2_booleans_are_true(boolean2, boolean3)
27
-
28
-
29
- def check_if_3_booleans_are_false(boolean1: tuple, boolean2: tuple, boolean3: tuple):
30
- if not boolean1[0] and not boolean2[0] and not boolean3[0]:
31
- message = f"All the boolean settings in config ini file were set to 'False',\n" \
32
- f"You need at least one 'True':\n" \
33
- f"{boolean1[1]}={boolean1[0]}\n" \
34
- f"{boolean2[1]}={boolean2[0]}\n" \
35
- f"{boolean3[1]}={boolean3[0]}"
36
- raise ValueError(message)
37
-
38
-
39
- def check_if_2_booleans_are_true(boolean1: tuple, boolean2: tuple) -> None:
40
- if boolean1[0] and boolean2[0]:
41
- message = f"Only one configuration can be 'True':\n" \
42
- f"{boolean1[1]}={boolean1[0]}\n" \
43
- f"{boolean2[1]}={boolean2[0]}\n"
44
- raise ValueError(message)
13
+ # Filter to get all the `True` conditions and their associated names
14
+ true_conditions = [name for value, name in booleans_list_of_tuples if value]
15
+
16
+ # Count the number of True values
17
+ true_count = len(true_conditions)
18
+
19
+ if true_count == 1:
20
+ # Only one value is True, which is acceptable
21
+ # print(f"Only one condition is True: {true_conditions[0]}.")
22
+ pass
23
+ elif true_count > 1:
24
+ # More than one value is True, raise an exception
25
+ raise ValueError(f"Multiple conditions are True: {', '.join(true_conditions)}.")
26
+ elif true_count == 0 and raise_if_all_false:
27
+ # None of the values are True, and the user does not want to ignore this case
28
+ raise ValueError("No conditions are True, and raise_if_all_false is set to True.")
29
+ else:
30
+ # If no True values and no_raise_if_all_false is True, just pass silently
31
+ # print("No conditions are True (but raise_if_all_false is set to False).")
32
+ pass
45
33
 
46
34
 
47
35
  def convert_string_to_bool(string: str) -> bool:
atomicshop/config_init.py CHANGED
@@ -2,7 +2,7 @@ import os
2
2
 
3
3
  from .file_io import tomls
4
4
  from . import filesystem
5
- from .print_api import print_api
5
+ from . import print_api
6
6
 
7
7
  CONFIG_FILE_NAME = 'config.toml'
8
8
  CONFIG: dict = dict()
@@ -45,7 +45,7 @@ def write_config(
45
45
  'CONFIG_FILE_NAME'.
46
46
  :param print_message: boolean, if True, the function will print the message about the created config file.
47
47
  Also, it will wait for the user to press Enter to exit the script.
48
- If False, the function will not print anything and will not exit..
48
+ If False, the function will not print anything and will not exit.
49
49
  :return:
50
50
  """
51
51
 
@@ -62,7 +62,7 @@ def write_config(
62
62
  tomls.write_toml_file(config, f'{script_directory}{os.sep}{config_file_name}')
63
63
 
64
64
  if print_message:
65
- print_api(f"Created config file: {config_file_path}", color="yellow")
66
- print_api(f"You need to fill it with details.", color="yellow")
65
+ print_api.print_api(f"Created config file: {config_file_path}", color="yellow")
66
+ print_api.print_api(f"You need to fill it with details.", color="yellow")
67
67
  input("Press Enter to exit.")
68
68
  exit()
@@ -2,11 +2,10 @@ import csv
2
2
  import io
3
3
  from typing import Tuple, List
4
4
 
5
- from .file_io import read_file_decorator
6
5
  from . import file_io
7
6
 
8
7
 
9
- @read_file_decorator
8
+ @file_io.read_file_decorator
10
9
  def read_csv_to_list_of_dicts_by_header(
11
10
  file_path: str,
12
11
  file_mode: str = 'r',
@@ -53,7 +52,7 @@ def read_csv_to_list_of_dicts_by_header(
53
52
  return csv_list, header
54
53
 
55
54
 
56
- @read_file_decorator
55
+ @file_io.read_file_decorator
57
56
  def read_csv_to_list_of_lists(
58
57
  file_path: str,
59
58
  file_mode: str = 'r',
@@ -2,7 +2,7 @@ from typing import Union
2
2
  import functools
3
3
 
4
4
  from .. import print_api
5
- from ..inspect_wrapper import get_target_function_default_args_and_combine_with_current
5
+ from .. import inspect_wrapper
6
6
 
7
7
 
8
8
  def get_write_file_mode_string_from_overwrite_bool(overwrite: bool) -> str:
@@ -17,7 +17,8 @@ def write_file_decorator(function_name):
17
17
  def wrapper_write_file_decorator(*args, **kwargs):
18
18
  # Put 'args' into 'kwargs' with appropriate key.
19
19
  # args, kwargs = put_args_to_kwargs(function_name, *args, **kwargs)
20
- args, kwargs = get_target_function_default_args_and_combine_with_current(function_name, *args, **kwargs)
20
+ args, kwargs = inspect_wrapper.get_target_function_default_args_and_combine_with_current(
21
+ function_name, *args, **kwargs)
21
22
 
22
23
  print_api.print_api(message=f"Writing file: {kwargs['file_path']}", **kwargs)
23
24
 
@@ -51,7 +52,8 @@ def read_file_decorator(function_name):
51
52
  def wrapper_read_file_decorator(*args, **kwargs):
52
53
  # Put 'args' into 'kwargs' with appropriate key.
53
54
  # args, kwargs = put_args_to_kwargs(function_name, *args, **kwargs)
54
- args, kwargs = get_target_function_default_args_and_combine_with_current(function_name, *args, **kwargs)
55
+ args, kwargs = inspect_wrapper.get_target_function_default_args_and_combine_with_current(
56
+ function_name, *args, **kwargs)
55
57
 
56
58
  continue_loop: bool = True
57
59
  while continue_loop:
@@ -2,9 +2,9 @@ import os
2
2
  from datetime import datetime
3
3
  import json
4
4
 
5
- from ...shared_functions import build_module_names, create_custom_logger, get_json
5
+ from ...shared_functions import build_module_names, create_custom_logger
6
6
  from ... import message, recs_files
7
- from .... import filesystem, urls
7
+ from .... import filesystem
8
8
  from ....file_io import file_io
9
9
 
10
10
 
@@ -46,21 +46,6 @@ class RecorderParent:
46
46
  # Build the record path with file name
47
47
  self.build_record_path_to_engine()
48
48
 
49
- # Define empty 'http_path'.
50
- http_path: str = str()
51
- # If 'self.class_client_message.request_raw_decoded.path' will be undefined, exception will raise.
52
- # This will happen if the message is not HTTP.
53
- try:
54
- # Parse the url to components.
55
- http_path_parsed = urls.url_parser(self.class_client_message.request_raw_decoded.path)
56
- # Get only directories.
57
- http_path_directories_string = '-'.join(http_path_parsed['directories'])
58
- # Add '_' character before 'http_path' to look better on the file name.
59
- http_path = f'_{http_path_directories_string}'
60
- # If 'self.class_client_message.request_raw_decoded.path' is not defined, we'll pass the exception.
61
- except Exception:
62
- pass
63
-
64
49
  # If HTTP Path is not defined, 'http_path' will be empty, and it will not interfere with file name.
65
50
  self.record_file_path: str = \
66
51
  self.engine_record_path + os.sep + \
@@ -88,7 +73,6 @@ class RecorderParent:
88
73
  # Convert the requests and responses to hex.
89
74
  self.convert_messages()
90
75
  # Get the message in dict / JSON format
91
- # record_message = get_json(self.class_client_message)
92
76
  record_message_dict: dict = dict(self.class_client_message)
93
77
  recorded_message_json_string = json.dumps(record_message_dict)
94
78
 
@@ -93,19 +93,20 @@ def check_configurations() -> int:
93
93
  print_api(message, color='red')
94
94
  return 1
95
95
 
96
- if config_static.DNSServer.set_default_dns_gateway or \
97
- config_static.DNSServer.set_default_dns_gateway_to_localhost or \
98
- config_static.DNSServer.set_default_dns_gateway_to_default_interface_ipv4:
99
- try:
100
- booleans.check_3_booleans_when_only_1_can_be_true(
96
+ try:
97
+ booleans.is_only_1_true_in_list(
98
+ booleans_list_of_tuples=[
101
99
  (config_static.DNSServer.set_default_dns_gateway, '[dns][set_default_dns_gateway]'),
102
100
  (config_static.DNSServer.set_default_dns_gateway_to_localhost,
103
101
  '[dns][set_default_dns_gateway_to_localhost]'),
104
102
  (config_static.DNSServer.set_default_dns_gateway_to_default_interface_ipv4,
105
- '[dns][set_default_dns_gateway_to_default_interface_ipv4]'))
106
- except ValueError as e:
107
- print_api(str(e), color='red')
108
- return 1
103
+ '[dns][set_default_dns_gateway_to_default_interface_ipv4]')
104
+ ],
105
+ raise_if_all_false=False
106
+ )
107
+ except ValueError as e:
108
+ print_api(str(e), color='red')
109
+ return 1
109
110
 
110
111
  if (config_static.DNSServer.set_default_dns_gateway or
111
112
  config_static.DNSServer.set_default_dns_gateway_to_localhost or
@@ -10,6 +10,7 @@ from ..permissions import permissions
10
10
  from ..python_functions import get_current_python_version_string, check_python_version_compliance
11
11
  from ..wrappers.socketw import socket_wrapper, dns_server, base
12
12
  from ..wrappers.loggingw import loggingw
13
+ from ..wrappers.ctyping import win_console
13
14
 
14
15
  from .initialize_engines import ModuleCategory
15
16
  from .connection_thread_worker import thread_worker_main
@@ -28,6 +29,12 @@ EXCEPTIONS_CSV_LOGGER_HEADER: str = 'time,exception'
28
29
  MITM_ERROR_LOGGER: loggingw.ExceptionCsvLogger = None
29
30
 
30
31
 
32
+ try:
33
+ win_console.disable_quick_edit()
34
+ except win_console.NotWindowsConsoleError:
35
+ pass
36
+
37
+
31
38
  def exit_cleanup():
32
39
  if permissions.is_admin():
33
40
  is_dns_dynamic, current_dns_gateway = dns.get_default_dns_gateway()
@@ -1,13 +1,15 @@
1
1
  import datetime
2
2
  import os
3
3
  import multiprocessing
4
+ from pathlib import Path
4
5
 
5
6
  from ..archiver import zips
6
7
  from .. import filesystem
8
+ from .. wrappers.loggingw import consts
7
9
 
8
10
 
9
- REC_FILE_DATE_TIME_FORMAT: str = "%Y_%m_%d-%H_%M_%S_%f"
10
- REC_FILE_DATE_FORMAT: str = REC_FILE_DATE_TIME_FORMAT.split('-')[0]
11
+ REC_FILE_DATE_TIME_FORMAT: str = f'{consts.DEFAULT_ROTATING_SUFFIXES_FROM_WHEN["S"]}_%f'
12
+ REC_FILE_DATE_FORMAT: str = REC_FILE_DATE_TIME_FORMAT.split('_')[0]
11
13
 
12
14
 
13
15
  def recs_archiver(recs_directory: str) -> list:
@@ -38,9 +38,3 @@ def create_custom_logger():
38
38
  logger_name = f'{config_static.MainConfig.LOGGER_NAME}.{engine_logger_part}'
39
39
 
40
40
  return loggingw.get_logger_with_level(logger_name)
41
-
42
-
43
- def get_json(obj):
44
- """ Convert any nested object to json / dict and values to string as is """
45
-
46
- return json.dumps(obj, default=dicts.convert_complex_object_to_dict)
@@ -354,7 +354,8 @@ def deviation_calculator_by_moving_average(
354
354
  summary: bool = False,
355
355
  output_file_path: str = None,
356
356
  output_file_type: Literal['json', 'csv'] = 'json',
357
- convert_sizes_lists_and_ma_data_to_string: bool = False
357
+ convert_sizes_lists_and_ma_data_to_string: bool = False,
358
+ skip_total_count_less_than: int = None
358
359
  ) -> Union[list, None]:
359
360
  """
360
361
  This function is the main function for the moving average calculator.
@@ -386,6 +387,12 @@ def deviation_calculator_by_moving_average(
386
387
  :param output_file_type: string, the type of the output file. 'json' or 'csv'.
387
388
  :param convert_sizes_lists_and_ma_data_to_string: bool, if True, the 'request_sizes', 'response_sizes' and 'ma_data'
388
389
  will be converted to string. This is useful when writing to files, so the view will be more readable.
390
+ :param skip_total_count_less_than: integer, if specified, the deviation calculation will be skipped
391
+ if the total count is less than this number.
392
+ This means that if we have moving average window of 7 days, on the 8th day, the deviation will be calculated
393
+ only if the total count of the checked type
394
+ (request average size, response average, request count, response count)
395
+ is greater or equal to this number.
389
396
  -----------------------------
390
397
  :return: the deviation list of dicts.
391
398
 
@@ -427,7 +434,8 @@ def deviation_calculator_by_moving_average(
427
434
  by_type,
428
435
  moving_average_window_days,
429
436
  top_bottom_deviation_percentage,
430
- get_deviation_for_last_day_only
437
+ get_deviation_for_last_day_only,
438
+ skip_total_count_less_than
431
439
  )
432
440
 
433
441
  if deviation_list:
@@ -484,6 +492,9 @@ def deviation_calculator_by_moving_average_main():
484
492
  parser.add_argument(
485
493
  '-p', '--percentage', type=float, required=True,
486
494
  help='Percentage of deviation from moving average. Example: 0.1 for 10%%.')
495
+ parser.add_argument(
496
+ '-slt', '--skip_total_count_less_than', type=int, required=False,
497
+ help='An integer to skip the deviation calculation if the total count is less than this number.')
487
498
 
488
499
  return parser.parse_args()
489
500
 
@@ -517,7 +528,8 @@ def deviation_calculator_by_moving_average_main():
517
528
  summary=summary,
518
529
  output_file_path=args.output_file,
519
530
  output_file_type=args.output_type,
520
- convert_sizes_lists_and_ma_data_to_string=convert_sizes_lists_and_ma_data_to_string
531
+ convert_sizes_lists_and_ma_data_to_string=convert_sizes_lists_and_ma_data_to_string,
532
+ skip_total_count_less_than=args.skip_total_count_less_than
521
533
  )
522
534
 
523
535
  return 0
@@ -14,6 +14,7 @@ def calculate_moving_average(
14
14
  moving_average_window_days,
15
15
  top_bottom_deviation_percentage: float,
16
16
  get_deviation_for_last_day_only: bool = False,
17
+ skip_total_count_less_than: int = None,
17
18
  print_kwargs: dict = None
18
19
  ) -> list:
19
20
  """
@@ -37,6 +38,7 @@ def calculate_moving_average(
37
38
  Files 01 to 05 will be used for moving average and the file 06 for deviation.
38
39
  Meaning the average calculated for 2021-01-06 will be compared to the values moving average of 2021-01-01
39
40
  to 2021-01-05.
41
+ :param skip_total_count_less_than: integer, if the total count is less than this number, skip the deviation.
40
42
  :param print_kwargs: dict, the print_api arguments.
41
43
  """
42
44
 
@@ -55,7 +57,7 @@ def calculate_moving_average(
55
57
  statistics_content: dict = {}
56
58
  # Read each file to its day.
57
59
  for log_atomic_path in logs_paths:
58
- date_string = log_atomic_path.datetime_string
60
+ date_string: str = log_atomic_path.datetime_string
59
61
  statistics_content[date_string] = {}
60
62
 
61
63
  statistics_content[date_string]['file'] = log_atomic_path
@@ -65,11 +67,11 @@ def calculate_moving_average(
65
67
  statistics_content[date_string]['content'] = log_file_content
66
68
  statistics_content[date_string]['header'] = log_file_header
67
69
 
68
- statistics_content[date_string]['content_no_errors'] = get_content_without_errors(log_file_content)
70
+ statistics_content[date_string]['content_no_useless'] = get_content_without_useless(log_file_content)
69
71
 
70
72
  # Get the data dictionary from the statistics content.
71
73
  statistics_content[date_string]['statistics_daily'] = compute_statistics_from_content(
72
- statistics_content[date_string]['content_no_errors'], by_type)
74
+ statistics_content[date_string]['content_no_useless'], by_type)
73
75
 
74
76
  moving_average_dict: dict = compute_moving_averages_from_average_statistics(
75
77
  statistics_content,
@@ -85,12 +87,12 @@ def calculate_moving_average(
85
87
 
86
88
  # Find deviation from the moving average to the bottom or top by specified percentage.
87
89
  deviation_list: list = find_deviation_from_moving_average(
88
- statistics_content, top_bottom_deviation_percentage)
90
+ statistics_content, top_bottom_deviation_percentage, skip_total_count_less_than)
89
91
 
90
92
  return deviation_list
91
93
 
92
94
 
93
- def get_content_without_errors(content: list) -> list:
95
+ def get_content_without_useless(content: list) -> list:
94
96
  """
95
97
  This function gets the 'statistics.csv' file content without errors from the 'content' list.
96
98
 
@@ -101,7 +103,7 @@ def get_content_without_errors(content: list) -> list:
101
103
  traffic_statistics_without_errors: list = []
102
104
  for line in content:
103
105
  # Skip empty lines, headers and errors.
104
- if line['host'] == 'host' or line['command'] == '':
106
+ if line['host'] == 'host' or (line['request_size_bytes'] == '' and line['response_size_bytes'] == ''):
105
107
  continue
106
108
 
107
109
  traffic_statistics_without_errors.append(line)
@@ -152,16 +154,13 @@ def get_data_dict_from_statistics_content(
152
154
  try:
153
155
  request_size_bytes = line['request_size_bytes']
154
156
  response_size_bytes = line['response_size_bytes']
155
- if request_size_bytes == '':
156
- request_size_bytes = '0'
157
- if response_size_bytes == '':
158
- response_size_bytes = '0'
159
-
160
- hosts_requests_responses[type_to_check]['request_sizes'].append(int(request_size_bytes))
161
- hosts_requests_responses[type_to_check]['response_sizes'].append(int(response_size_bytes))
162
- except ValueError:
157
+ if request_size_bytes != '':
158
+ hosts_requests_responses[type_to_check]['request_sizes'].append(int(request_size_bytes))
159
+ if response_size_bytes != '':
160
+ hosts_requests_responses[type_to_check]['response_sizes'].append(int(response_size_bytes))
161
+ except ValueError as e:
163
162
  print_api(line, color='yellow')
164
- raise
163
+ raise e
165
164
 
166
165
  return hosts_requests_responses
167
166
 
@@ -175,13 +174,15 @@ def compute_statistics_from_data_dict(data_dict: dict):
175
174
  """
176
175
 
177
176
  for host, host_dict in data_dict.items():
178
- count = len(host_dict['request_sizes'])
179
- avg_request_size = statistics.mean(host_dict['request_sizes']) if count > 0 else 0
180
- median_request_size = statistics.median(host_dict['request_sizes']) if count > 0 else 0
181
- avg_response_size = statistics.mean(host_dict['response_sizes']) if count > 0 else 0
182
- median_response_size = statistics.median(host_dict['response_sizes']) if count > 0 else 0
183
-
184
- data_dict[host]['count'] = count
177
+ count_requests = len(host_dict['request_sizes'])
178
+ count_responses = len(host_dict['response_sizes'])
179
+ avg_request_size = statistics.mean(host_dict['request_sizes']) if count_requests > 0 else 0
180
+ median_request_size = statistics.median(host_dict['request_sizes']) if count_requests > 0 else 0
181
+ avg_response_size = statistics.mean(host_dict['response_sizes']) if count_responses > 0 else 0
182
+ median_response_size = statistics.median(host_dict['response_sizes']) if count_responses > 0 else 0
183
+
184
+ data_dict[host]['count_requests'] = count_requests
185
+ data_dict[host]['count_responses'] = count_responses
185
186
  data_dict[host]['avg_request_size'] = avg_request_size
186
187
  data_dict[host]['median_request_size'] = median_request_size
187
188
  data_dict[host]['avg_response_size'] = avg_response_size
@@ -251,14 +252,16 @@ def compute_average_for_current_day_from_past_x_days(
251
252
  for host, host_dict in statistics_daily.items():
252
253
  if host not in moving_average:
253
254
  moving_average[host] = {
254
- 'counts': [],
255
+ 'all_request_counts': [],
256
+ 'all_response_counts': [],
255
257
  'avg_request_sizes': [],
256
258
  'avg_response_sizes': [],
257
259
  'median_request_sizes': [],
258
260
  'median_response_sizes': []
259
261
  }
260
262
 
261
- moving_average[host]['counts'].append(int(host_dict['count']))
263
+ moving_average[host]['all_request_counts'].append(int(host_dict['count_requests']))
264
+ moving_average[host]['all_response_counts'].append(int(host_dict['count_responses']))
262
265
  moving_average[host]['avg_request_sizes'].append(float(host_dict['avg_request_size']))
263
266
  moving_average[host]['avg_response_sizes'].append(float(host_dict['avg_response_size']))
264
267
  moving_average[host]['median_request_sizes'].append(float(host_dict['median_request_size']))
@@ -267,21 +270,26 @@ def compute_average_for_current_day_from_past_x_days(
267
270
  # Compute the moving average.
268
271
  moving_average_results: dict = {}
269
272
  for host, host_dict in moving_average.items():
270
- ma_count = statistics.mean(host_dict['counts'])
273
+ ma_request_count = statistics.mean(host_dict['all_request_counts'])
274
+ ma_response_count = statistics.mean(host_dict['all_response_counts'])
271
275
  ma_request_size = statistics.mean(host_dict['avg_request_sizes'])
272
276
  ma_response_size = statistics.mean(host_dict['avg_response_sizes'])
273
- mm_count = statistics.median(host_dict['counts'])
277
+ mm_request_count = statistics.median(host_dict['all_request_counts'])
278
+ mm_response_count = statistics.median(host_dict['all_response_counts'])
274
279
  mm_request_size = statistics.median(host_dict['median_request_sizes'])
275
280
  mm_response_size = statistics.median(host_dict['median_response_sizes'])
276
281
 
277
282
  moving_average_results[host] = {
278
- 'ma_count': ma_count,
283
+ 'ma_request_count': ma_request_count,
284
+ 'ma_response_count': ma_response_count,
279
285
  'ma_request_size': ma_request_size,
280
286
  'ma_response_size': ma_response_size,
281
- 'mm_count': mm_count,
287
+ 'mm_request_count': mm_request_count,
288
+ 'mm_response_count': mm_response_count,
282
289
  'mm_request_size': mm_request_size,
283
290
  'mm_response_size': mm_response_size,
284
- 'counts': host_dict['counts'],
291
+ 'all_request_counts': host_dict['all_request_counts'],
292
+ 'all_response_counts': host_dict['all_response_counts'],
285
293
  'avg_request_sizes': host_dict['avg_request_sizes'],
286
294
  'avg_response_sizes': host_dict['avg_response_sizes'],
287
295
  'median_request_sizes': host_dict['median_request_sizes'],
@@ -293,7 +301,8 @@ def compute_average_for_current_day_from_past_x_days(
293
301
 
294
302
  def find_deviation_from_moving_average(
295
303
  statistics_content: dict,
296
- top_bottom_deviation_percentage: float
304
+ top_bottom_deviation_percentage: float,
305
+ skip_total_count_less_than: int = None
297
306
  ) -> list:
298
307
  """
299
308
  This function finds the deviation from the moving average to the bottom or top by specified percentage.
@@ -301,12 +310,13 @@ def find_deviation_from_moving_average(
301
310
  :param statistics_content: dict, the statistics content dictionary.
302
311
  :param top_bottom_deviation_percentage: float, the percentage of deviation from the moving average to the top or
303
312
  bottom.
313
+ :param skip_total_count_less_than: integer, if the total count is less than this number, skip the deviation.
304
314
  :return: list, the deviation list.
305
315
  """
306
316
 
307
317
  def _check_deviation(
308
- check_type: Literal['count', 'avg_request_size', 'avg_response_size'],
309
- ma_check_type: Literal['ma_count', 'ma_request_size', 'ma_response_size'],
318
+ check: Literal['count', 'avg'],
319
+ traffic_direction: Literal['request', 'response'],
310
320
  day_statistics_content_dict: dict,
311
321
  moving_averages_dict: dict
312
322
  ):
@@ -316,6 +326,19 @@ def find_deviation_from_moving_average(
316
326
 
317
327
  nonlocal message
318
328
 
329
+ if check == 'count':
330
+ check_type = f'{check}_{traffic_direction}s'
331
+ ma_check_type = f'ma_{traffic_direction}_{check}'
332
+ median_type_string = check_type
333
+ moving_median_type_string = f'mm_{traffic_direction}_{check}'
334
+ elif check == 'avg':
335
+ check_type = f'{check}_{traffic_direction}_size'
336
+ ma_check_type = f'ma_{traffic_direction}_size'
337
+ median_type_string = f'median_{traffic_direction}_size'
338
+ moving_median_type_string = f'mm_{traffic_direction}_size'
339
+ else:
340
+ raise ValueError(f'Invalid check: {check}')
341
+
319
342
  host_moving_average_by_type = moving_averages_dict[host][ma_check_type]
320
343
  check_type_moving_by_percent = (
321
344
  host_moving_average_by_type * top_bottom_deviation_percentage)
@@ -338,29 +361,32 @@ def find_deviation_from_moving_average(
338
361
  if deviation_type:
339
362
  message = f'[{check_type}] is [{deviation_type}] the moving average.'
340
363
 
341
- # Get the right moving median.
342
- if check_type == 'count':
343
- median_type_string: str = 'count'
344
- moving_median_type_string: str = 'mm_count'
345
- else:
346
- median_type_string: str = check_type.replace('avg', 'median')
347
- moving_median_type_string: str = check_type.replace('avg', 'mm')
348
-
349
364
  # The median and the total count are None for the count, Since they are the count.
350
- if check_type == 'count':
365
+ if 'count' in check_type:
351
366
  total_entries_averaged = None
352
367
  median_size = None
353
368
  else:
354
- total_entries_averaged = day_statistics_content_dict['count']
369
+ total_entries_averaged = day_statistics_content_dict[f'count_{traffic_direction}s']
355
370
  median_size = day_statistics_content_dict[median_type_string]
356
371
 
372
+ value = day_statistics_content_dict[check_type]
373
+
374
+ # If the total count is less than the specified number, skip the deviation.
375
+ if skip_total_count_less_than:
376
+ if total_entries_averaged:
377
+ if total_entries_averaged < skip_total_count_less_than:
378
+ return
379
+ else:
380
+ if value < skip_total_count_less_than:
381
+ return
382
+
357
383
  moving_median_size = moving_averages_dict[host][moving_median_type_string]
358
384
 
359
385
  deviation_list.append({
360
386
  'day': day,
361
387
  'host': host,
362
388
  'message': message,
363
- 'value': day_statistics_content_dict[check_type],
389
+ 'value': value,
364
390
  'ma_value': host_moving_average_by_type,
365
391
  'check_type': check_type,
366
392
  'percentage': top_bottom_deviation_percentage,
@@ -411,10 +437,12 @@ def find_deviation_from_moving_average(
411
437
  continue
412
438
 
413
439
  _check_deviation(
414
- 'count', 'ma_count', host_dict, previous_day_moving_average_dict)
440
+ 'count', 'request', host_dict, previous_day_moving_average_dict)
441
+ _check_deviation(
442
+ 'count', 'response', host_dict, previous_day_moving_average_dict)
415
443
  _check_deviation(
416
- 'avg_request_size', 'ma_request_size', host_dict, previous_day_moving_average_dict)
444
+ 'avg', 'request', host_dict, previous_day_moving_average_dict)
417
445
  _check_deviation(
418
- 'avg_response_size', 'ma_response_size', host_dict, previous_day_moving_average_dict)
446
+ 'avg', 'response', host_dict, previous_day_moving_average_dict)
419
447
 
420
448
  return deviation_list
atomicshop/print_api.py CHANGED
@@ -2,7 +2,6 @@ import sys
2
2
  import logging
3
3
 
4
4
  from .basics import ansi_escape_codes
5
- from .wrappers.loggingw import loggingw
6
5
  from .basics import tracebacks
7
6
 
8
7
 
@@ -74,6 +73,7 @@ def print_api(
74
73
 
75
74
  # Inner functions already get all the local variables of the main function.
76
75
  def print_or_logger():
76
+ from .wrappers.loggingw import loggingw
77
77
  nonlocal message
78
78
  nonlocal color
79
79
  nonlocal traceback_string
@@ -128,7 +128,6 @@ def print_api(
128
128
  if print_end == '\n':
129
129
  if stdcolor and color is not None:
130
130
  # Use logger to output message.
131
- # with loggingw.temporary_change_logger_stream_handler_color(logger, color=color):
132
131
  with loggingw.temporary_change_logger_stream_handler_emit_color(logger, color):
133
132
  getattr(logger, logger_method)(message)
134
133
  else:
@@ -118,51 +118,12 @@ class SystemResourceMonitor:
118
118
  :return:
119
119
  """
120
120
 
121
- def run_check_system_resources(
122
- interval, get_cpu, get_memory, get_disk_io_bytes, get_disk_files_count, get_disk_busy_time,
123
- get_disk_used_percent, calculate_maximum_changed_disk_io, maximum_disk_io, queue_list, manager_dict):
124
- """
125
- Continuously update the system resources in the shared results dictionary.
126
- This function runs in a separate process.
127
- """
128
-
129
- while self.running:
130
- # Get the results of the system resources check function and store them in
131
- # temporary results' dictionary.
132
- results = system_resources.check_system_resources(
133
- interval=interval, get_cpu=get_cpu, get_memory=get_memory,
134
- get_disk_io_bytes=get_disk_io_bytes, get_disk_files_count=get_disk_files_count,
135
- get_disk_busy_time=get_disk_busy_time, get_disk_used_percent=get_disk_used_percent)
136
-
137
- if calculate_maximum_changed_disk_io:
138
- if results['disk_io_read'] > maximum_disk_io['read_bytes_per_sec']:
139
- maximum_disk_io['read_bytes_per_sec'] = results['disk_io_read']
140
- if results['disk_io_write'] > maximum_disk_io['write_bytes_per_sec']:
141
- maximum_disk_io['write_bytes_per_sec'] = results['disk_io_write']
142
- if results['disk_files_count_read'] > maximum_disk_io['read_files_count_per_sec']:
143
- maximum_disk_io['read_files_count_per_sec'] = results['disk_files_count_read']
144
- if results['disk_files_count_write'] > maximum_disk_io['write_files_count_per_sec']:
145
- maximum_disk_io['write_files_count_per_sec'] = results['disk_files_count_write']
146
- results['maximum_disk_io'] = maximum_disk_io
147
-
148
- if queue_list is not None:
149
- for queue in queue_list:
150
- queue.put(results)
151
-
152
- # Update the shared results dictionary with the temporary results' dictionary.
153
- # This is done in separate steps to avoid overwriting the special 'multiprocessing.Manager.dict' object.
154
- # So we update the shared results dictionary with the temporary results' dictionary.
155
- if manager_dict is not None:
156
- manager_dict.update(results)
157
-
158
- self.results = results
159
-
160
121
  if print_kwargs is None:
161
122
  print_kwargs = {}
162
123
 
163
124
  if self.thread is None:
164
125
  self.running = True
165
- self.thread = threading.Thread(target=run_check_system_resources, args=(
126
+ self.thread = threading.Thread(target=self.run_check_system_resources, args=(
166
127
  self.interval, self.get_cpu, self.get_memory, self.get_disk_io_bytes, self.get_disk_files_count,
167
128
  self.get_disk_busy_time, self.get_disk_used_percent, self.calculate_maximum_changed_disk_io,
168
129
  self.maximum_disk_io, self.queue_list, self.manager_dict))
@@ -171,6 +132,46 @@ class SystemResourceMonitor:
171
132
  else:
172
133
  print_api.print_api("Monitoring is already running.", color='yellow', **print_kwargs)
173
134
 
135
+ def run_check_system_resources(
136
+ self,
137
+ interval, get_cpu, get_memory, get_disk_io_bytes, get_disk_files_count, get_disk_busy_time,
138
+ get_disk_used_percent, calculate_maximum_changed_disk_io, maximum_disk_io, queue_list, manager_dict):
139
+ """
140
+ Continuously update the system resources in the shared results dictionary.
141
+ This function runs in a separate process.
142
+ """
143
+
144
+ while self.running:
145
+ # Get the results of the system resources check function and store them in
146
+ # temporary results' dictionary.
147
+ results = system_resources.check_system_resources(
148
+ interval=interval, get_cpu=get_cpu, get_memory=get_memory,
149
+ get_disk_io_bytes=get_disk_io_bytes, get_disk_files_count=get_disk_files_count,
150
+ get_disk_busy_time=get_disk_busy_time, get_disk_used_percent=get_disk_used_percent)
151
+
152
+ if calculate_maximum_changed_disk_io:
153
+ if results['disk_io_read'] > maximum_disk_io['read_bytes_per_sec']:
154
+ maximum_disk_io['read_bytes_per_sec'] = results['disk_io_read']
155
+ if results['disk_io_write'] > maximum_disk_io['write_bytes_per_sec']:
156
+ maximum_disk_io['write_bytes_per_sec'] = results['disk_io_write']
157
+ if results['disk_files_count_read'] > maximum_disk_io['read_files_count_per_sec']:
158
+ maximum_disk_io['read_files_count_per_sec'] = results['disk_files_count_read']
159
+ if results['disk_files_count_write'] > maximum_disk_io['write_files_count_per_sec']:
160
+ maximum_disk_io['write_files_count_per_sec'] = results['disk_files_count_write']
161
+ results['maximum_disk_io'] = maximum_disk_io
162
+
163
+ if queue_list is not None:
164
+ for queue in queue_list:
165
+ queue.put(results)
166
+
167
+ # Update the shared results dictionary with the temporary results' dictionary.
168
+ # This is done in separate steps to avoid overwriting the special 'multiprocessing.Manager.dict' object.
169
+ # So we update the shared results dictionary with the temporary results' dictionary.
170
+ if manager_dict is not None:
171
+ manager_dict.update(results)
172
+
173
+ self.results = results
174
+
174
175
  def get_results(self) -> dict:
175
176
  """
176
177
  Retrieve the latest results.
@@ -0,0 +1,39 @@
1
+ import ctypes
2
+
3
+
4
+ class NotWindowsConsoleError(Exception):
5
+ pass
6
+
7
+
8
+ # Define QuickEdit mode bit (0x0040)
9
+ ENABLE_QUICK_EDIT = 0x0040
10
+
11
+
12
+ def disable_quick_edit():
13
+ """
14
+ Disables QuickEdit mode in the Windows Command Prompt.
15
+ This prevents the console from being paused when the user selects text.
16
+ NO ADMIN REQUIRED
17
+ """
18
+
19
+ kernel32 = ctypes.windll.kernel32
20
+ h_stdin = kernel32.GetStdHandle(-10) # -10 is STD_INPUT_HANDLE
21
+
22
+ # Get current console mode
23
+ mode = ctypes.c_uint()
24
+ if kernel32.GetConsoleMode(h_stdin, ctypes.byref(mode)) == 0:
25
+ try:
26
+ raise ctypes.WinError()
27
+ except OSError as e:
28
+ # This means that the code is not running in console window.
29
+ if e.errno == 9 and e.winerror == 6:
30
+ raise NotWindowsConsoleError("This code is not running in a Windows console.")
31
+ else:
32
+ raise e
33
+
34
+ # Disable QuickEdit Mode by clearing the corresponding bit
35
+ mode.value &= ~ENABLE_QUICK_EDIT
36
+
37
+ # Set the new console mode
38
+ if kernel32.SetConsoleMode(h_stdin, mode) == 0:
39
+ raise ctypes.WinError()
@@ -8,7 +8,7 @@ import threading
8
8
  from . import loggers, handlers
9
9
  from ...file_io import csvs
10
10
  from ...basics import tracebacks, ansi_escape_codes
11
- from ...import print_api
11
+ from ... import print_api
12
12
 
13
13
 
14
14
  class LoggingwLoggerAlreadyExistsError(Exception):
@@ -13,6 +13,7 @@ def get_logs_paths(
13
13
  date_format: str = None,
14
14
  latest_only: bool = False,
15
15
  previous_day_only: bool = False,
16
+ yesterday_only: bool = False,
16
17
  specific_date: str = None
17
18
  ) -> list[filesystem.AtomicPath]:
18
19
  """
@@ -37,24 +38,37 @@ def get_logs_paths(
37
38
  date_format = '%Y-%m-%d'
38
39
  :param latest_only: Boolean, if True, only the latest log file path will be returned.
39
40
  :param previous_day_only: Boolean, if True, only the log file path from the previous day will be returned.
41
+ :param yesterday_only: Boolean, if True, only the log file path from yesterday will be returned.
42
+ There's a difference between 'previous_day_only' and 'yesterday_only'.
43
+ 'previous_day_only' will get the log file from the previous day in the list of files that were found.
44
+ Since that doesn't guarantee that the log file from the previous day is yesterday, we have 'yesterday_only'.
40
45
  :param specific_date: Specific date to get the log file path.
41
46
  If specified, the function will get the log file by the specific date.
42
47
  Meaning that 'date_format' must be specified.
43
-
44
48
  """
45
49
 
46
- if latest_only or previous_day_only or specific_date:
47
- booleans.check_3_booleans_when_only_1_can_be_true(
48
- (latest_only, 'latest_only'),
49
- (previous_day_only, 'previous_day_only'),
50
- (specific_date, 'specific_date'))
50
+ booleans.is_only_1_true_in_list(
51
+ booleans_list_of_tuples=[
52
+ (latest_only, 'latest_only'),
53
+ (previous_day_only, 'previous_day_only'),
54
+ (yesterday_only, 'yesterday_only'),
55
+ (specific_date, 'specific_date'),
56
+ ],
57
+ raise_if_all_false=False
58
+ )
51
59
 
52
60
  if not date_format and specific_date:
53
61
  raise ValueError('If "specific_date" is specified, "date_format" must be specified.')
54
62
 
55
63
  # Get the file_name_pattern from the file name. Build the file_name_pattern.
64
+ # For some reason if the file name will be '.zip', then the file stem will be '.zip' and the extension will be ''.
56
65
  log_file_name: str = Path(log_file_path).stem
57
66
  log_file_extension: str = Path(log_file_path).suffix
67
+
68
+ if not log_file_extension and '.' in log_file_name:
69
+ log_file_name, log_file_extension = log_file_name.rsplit('.')
70
+ log_file_extension = f'.{log_file_extension}'
71
+
58
72
  file_name_pattern: str = f'{log_file_name}*{log_file_extension}'
59
73
 
60
74
  # Get the directory path from the file path.
@@ -111,15 +125,18 @@ def get_logs_paths(
111
125
  if logs_files:
112
126
  if latest_only:
113
127
  logs_files = [logs_files[-1]]
114
-
115
- if previous_day_only:
116
- # Check if there is a previous day log file.
128
+ elif previous_day_only:
117
129
  if len(logs_files) == 1:
118
130
  logs_files = []
119
131
  else:
120
132
  logs_files = [logs_files[-2]]
121
-
122
- if specific_date:
133
+ elif yesterday_only:
134
+ # Get yesterday's date.
135
+ yesterday_date_string = (datetime.datetime.now() - datetime.timedelta(days=1)).strftime(date_format)
136
+ # Check if there is a yesterday log file.
137
+ logs_files = [single_file
138
+ for single_file in logs_files if single_file.datetime_string == yesterday_date_string]
139
+ elif specific_date:
123
140
  # Check if there is a specific date log file.
124
141
  logs_files = [single_file for single_file in logs_files if single_file.datetime_string == specific_date]
125
142
 
@@ -394,9 +394,14 @@ def find(
394
394
  query: dict = None,
395
395
  page: int = None,
396
396
  items: int = None,
397
- sorting: dict[str, Literal[
398
- 'asc', 'desc',
399
- 1, -1]] = None,
397
+ sorting: Union[
398
+ dict[str, Literal[
399
+ 'asc', 'desc',
400
+ 'ASC', 'DESC',
401
+ 1, -1]],
402
+ list[tuple[
403
+ str, Literal[1, -1]]],
404
+ None] = None,
400
405
  convert_object_id_to_str: bool = False,
401
406
  key_convert_to_dict: list[str] = None,
402
407
  mongo_client: pymongo.MongoClient = None,
@@ -445,29 +450,40 @@ def find(
445
450
  $nin: Will search for a value not in a list of values.
446
451
  Example for searching for a value that is not in a list of values:
447
452
  query = {'field_name': {'$nin': ['value1', 'value2', 'value3']}}
448
-
453
+ $exists: Will search for entries where the field exists or not.
454
+ Example for searching for entries where the field exists:
455
+ query = {'field_name': {'$exists': True}}
456
+ Example for searching for entries where the field does not exist:
457
+ query = {'field_name': {'$exists': False}}
458
+ $ne: Will search for entries where the field is not equal to the value.
459
+ Example for searching for entries where the field is not equal to the value:
460
+ query = {'field_name': {'$ne': 'value'}}
449
461
 
450
462
  :param page: int, the page number (Optional).
451
463
  :param items: int, the number of results per page (Optional).
452
- :param sorting: dict, the name of the field and the order to sort the containers by.
453
- You can use several fields to sort the containers by several fields.
454
- In this case the containers will be sorted by the first field, then by the second field, etc.
455
- You can also use only singular field to sort the containers by only one field.
456
- Usage:
457
- {
458
- field_name: order
459
- }
460
- Example:
461
- {
462
- 'vendor': 'asc',
463
- 'model': 'desc'
464
- }
464
+ :param sorting: dict or list of tuples:
465
+ dict, the name of the field and the order to sort the containers by.
466
+ You can use several fields to sort the containers by several fields.
467
+ In this case the containers will be sorted by the first field, then by the second field, etc.
468
+ You can also use only singular field to sort the containers by only one field.
469
+ Usage:
470
+ {
471
+ field_name: order
472
+ }
473
+ Example:
474
+ {
475
+ 'vendor': 'asc',
476
+ 'model': 'desc'
477
+ }
465
478
 
466
- Or example using integers:
467
- {
468
- 'vendor': 1,
469
- 'model': -1
470
- }
479
+ Or example using integers:
480
+ {
481
+ 'vendor': 1,
482
+ 'model': -1
483
+ }
484
+
485
+ list of tuples, each tuple will contain [0] string of the field name and [1] the integer value of the order
486
+ to sort by, this is pymongo default, 1 for ascending and -1 for descending.
471
487
  :param convert_object_id_to_str: bool, if True, the '_id' field will be converted to a string.
472
488
  The '_id' field is an ObjectId type, which is a complex object, it can be converted to a string for simpler
473
489
  processing.
@@ -486,9 +502,9 @@ def find(
486
502
  elif items and not page:
487
503
  page = 1
488
504
 
489
- if sorting:
505
+ if sorting and isinstance(sorting, dict):
490
506
  for key_to_sort_by, order in sorting.items():
491
- if order not in ['asc', 'desc', 1, -1]:
507
+ if order.lower() not in ['asc', 'desc', 1, -1]:
492
508
  raise ValueError("The order must be 'asc', 'desc', 1 or -1.")
493
509
 
494
510
  if not mongo_client:
@@ -510,13 +526,16 @@ def find(
510
526
 
511
527
  if sorting:
512
528
  sorting_list_of_tuples: list[tuple[str, int]] = []
513
- for key_to_sort_by, order in sorting.items():
514
- if order == 'asc':
515
- order = pymongo.ASCENDING
516
- elif order == 'desc':
517
- order = pymongo.DESCENDING
518
-
519
- sorting_list_of_tuples.append((key_to_sort_by, order))
529
+ if isinstance(sorting, dict):
530
+ for key_to_sort_by, order in sorting.items():
531
+ if order.lower() == 'asc':
532
+ order = pymongo.ASCENDING
533
+ elif order.lower() == 'desc':
534
+ order = pymongo.DESCENDING
535
+
536
+ sorting_list_of_tuples.append((key_to_sort_by, order))
537
+ elif sorting and isinstance(sorting, list):
538
+ sorting_list_of_tuples = sorting
520
539
 
521
540
  collection_items = collection_items.sort(sorting_list_of_tuples)
522
541
 
@@ -84,10 +84,13 @@ def install_nodejs_ubuntu(
84
84
  :return:
85
85
  """
86
86
 
87
- booleans.check_3_booleans_when_only_1_can_be_true(
88
- (install_latest_version, 'install_latest_version'),
89
- (install_lts, 'install_lts'),
90
- (install_by_version_number, 'install_by_version_number')
87
+ booleans.is_only_1_true_in_list(
88
+ booleans_list_of_tuples=[
89
+ (install_latest_version, 'install_latest_version'),
90
+ (install_lts, 'install_lts'),
91
+ (install_by_version_number, 'install_by_version_number')
92
+ ],
93
+ raise_if_all_false=True
91
94
  )
92
95
 
93
96
  # Check if Node.js is already installed.
@@ -261,11 +261,14 @@ class DnsServer:
261
261
 
262
262
  def test_config(self):
263
263
  try:
264
- booleans.check_3_booleans_when_only_1_can_be_true(
265
- (self.resolve_to_tcp_server_only_tcp_resolve_domains,
266
- 'resolve_to_tcp_server_only_tcp_resolve_domains'),
267
- (self.resolve_to_tcp_server_all_domains, 'resolve_to_tcp_server_all_domains'),
268
- (self.resolve_regular, 'resolve_regular')
264
+ booleans.is_only_1_true_in_list(
265
+ booleans_list_of_tuples=[
266
+ (self.resolve_to_tcp_server_only_tcp_resolve_domains,
267
+ 'resolve_to_tcp_server_only_tcp_resolve_domains'),
268
+ (self.resolve_to_tcp_server_all_domains, 'resolve_to_tcp_server_all_domains'),
269
+ (self.resolve_regular, 'resolve_regular')
270
+ ],
271
+ raise_if_all_false=True
269
272
  )
270
273
  except ValueError as e:
271
274
  raise DnsConfigurationValuesError(e)
@@ -263,11 +263,15 @@ class SocketWrapper:
263
263
  raise SocketWrapperConfigurationValuesError(message)
264
264
 
265
265
  try:
266
- booleans.check_3_booleans_when_only_1_can_be_true(
267
- (self.default_server_certificate_usage, 'default_server_certificate_usage'),
268
- (self.sni_create_server_certificate_for_each_domain,
269
- 'sni_create_server_certificate_for_each_domain'),
270
- (self.custom_server_certificate_usage, 'custom_server_certificate_usage'))
266
+ booleans.is_only_1_true_in_list(
267
+ booleans_list_of_tuples=[
268
+ (self.default_server_certificate_usage, 'default_server_certificate_usage'),
269
+ (self.sni_create_server_certificate_for_each_domain,
270
+ 'sni_create_server_certificate_for_each_domain'),
271
+ (self.custom_server_certificate_usage, 'custom_server_certificate_usage')
272
+ ],
273
+ raise_if_all_false=True
274
+ )
271
275
  except ValueError as e:
272
276
  raise SocketWrapperConfigurationValuesError(str(e))
273
277
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: atomicshop
3
- Version: 2.16.31
3
+ Version: 2.16.32
4
4
  Summary: Atomic functions and classes to make developer life easier
5
5
  Author: Denis Kras
6
6
  License: MIT License
@@ -1,11 +1,11 @@
1
- atomicshop/__init__.py,sha256=sCusnYw5eAOy-azyQImMCB9etQnC6WjnQr70CnWZluk,124
1
+ atomicshop/__init__.py,sha256=CTcwR9Fze6eeaSRpctGxH5Awdz0-la2J3f5qJFeP0jA,124
2
2
  atomicshop/_basics_temp.py,sha256=6cu2dd6r2dLrd1BRNcVDKTHlsHs_26Gpw8QS6v32lQ0,3699
3
3
  atomicshop/_create_pdf_demo.py,sha256=Yi-PGZuMg0RKvQmLqVeLIZYadqEZwUm-4A9JxBl_vYA,3713
4
4
  atomicshop/_patch_import.py,sha256=ENp55sKVJ0e6-4lBvZnpz9PQCt3Otbur7F6aXDlyje4,6334
5
5
  atomicshop/appointment_management.py,sha256=BsYH_PClTGLVazcuNjt30--hpXKYjSmHp1R1iQbM4Hc,7330
6
6
  atomicshop/certificates.py,sha256=MEGj7t3Nt2CHE7yzXrvFTLCOKZG9tJ6Ok5JC2BsFRis,7603
7
7
  atomicshop/command_line_processing.py,sha256=u5yT9Ger_cu7ni5ID0VFlRbVD46ARHeNC9tRM-_YXrQ,1038
8
- atomicshop/config_init.py,sha256=BSxc2FhytQPv06g5z9wbAXuA6oYCAsAJLxu_mTExhwI,2491
8
+ atomicshop/config_init.py,sha256=50kD2lXP8sgwPekcmAbfADcY46YvXkF-6XIdA7W_638,2501
9
9
  atomicshop/console_output.py,sha256=AOSJjrRryE97PAGtgDL03IBtWSi02aNol8noDnW3k6M,4667
10
10
  atomicshop/console_user_response.py,sha256=31HIy9QGXa7f-GVR8MzJauQ79E_ZqAeagF3Ks4GGdDU,3234
11
11
  atomicshop/datetimes.py,sha256=IQZ66lmta-ZqxYbyHzm_9eugbJFSilXK1e0kfMgoXGg,18371
@@ -25,7 +25,7 @@ atomicshop/ip_addresses.py,sha256=Hvi4TumEFoTEpKWaq5WNF-YzcRzt24IxmNgv-Mgax1s,11
25
25
  atomicshop/keyboard_press.py,sha256=1W5kRtOB75fulVx-uF2yarBhW0_IzdI1k73AnvXstk0,452
26
26
  atomicshop/on_exit.py,sha256=Rpg2SaF0aginuO7JYwA49YJYnS8F6K2jUqhjH65WzuU,6889
27
27
  atomicshop/pbtkmultifile_argparse.py,sha256=aEk8nhvoQVu-xyfZosK3ma17CwIgOjzO1erXXdjwtS4,4574
28
- atomicshop/print_api.py,sha256=OT4C7wsCDB3ZvXPWNTwWCIjDF6lRxlYQVINKNa7Oi1s,11254
28
+ atomicshop/print_api.py,sha256=REkd1W3--g1Av4_nH3M2CvQCIEDecfsT7spMXgIRUJE,11158
29
29
  atomicshop/process.py,sha256=PeLvyixXaCfftdUF3oMbohI1L4MdLtvQVDx2V1Tz_Rk,16662
30
30
  atomicshop/python_file_patcher.py,sha256=-uhbUX-um5k-If_XXuOfCr8wMzZ3QE6h9N8xGWw6W_o,5486
31
31
  atomicshop/python_functions.py,sha256=zJg4ogUwECxrDD7xdDN5JikIUctITM5lsyabr_ZNsRw,4435
@@ -37,7 +37,7 @@ atomicshop/sound.py,sha256=tHiQQbFBk7EYN3pAfGNcxfF9oNsoYnZgu9z9iq8hxQE,24352
37
37
  atomicshop/speech_recognize.py,sha256=55-dIjgkpF93mvJnJuxSFuft5H5eRvGNlUj9BeIOZxk,5903
38
38
  atomicshop/ssh_remote.py,sha256=Mxixqs2-xGy1bhbcP0LKqjxKTNPz1Gmzz8PzO8aLB4c,17345
39
39
  atomicshop/sys_functions.py,sha256=MTBxRve5bh58SPvhX3gMiGqHlSBuI_rdNN1NnnBBWqI,906
40
- atomicshop/system_resource_monitor.py,sha256=JW3Wo0u109_tfBcjR1yJWAFAURT80cHsZ3hLHwM__vs,13740
40
+ atomicshop/system_resource_monitor.py,sha256=5sM3Ad4IZI8ahpKM3Wu4B0a3TD-N2Y795_kSkIv94lk,13632
41
41
  atomicshop/system_resources.py,sha256=iKUvVSaXR47inmr3cTYsgNfclT38dRia2oupnlhIpK4,9290
42
42
  atomicshop/tempfiles.py,sha256=uq1ve2WlWehZ3NOTXJnpBBMt6HyCdBufqedF0HyzA6k,2517
43
43
  atomicshop/timer.py,sha256=7Zw1KRV0acHCRATMnanyX2MLBb63Hc-6us3rCZ9dNlY,2345
@@ -82,7 +82,7 @@ atomicshop/archiver/zips.py,sha256=0Z_1MWs7YRiCBVpyaG8llnzRguHSO4R51KDMN3FJZt8,1
82
82
  atomicshop/basics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
83
83
  atomicshop/basics/ansi_escape_codes.py,sha256=WtIkm-BjSZS5J5irDUdAMBNvdX-qXFZcTX98jcBMpJE,3140
84
84
  atomicshop/basics/argparse_template.py,sha256=horwgSf3MX1ZgRnYxtmmQuz9OU_vKrKggF65gmjlmfg,5836
85
- atomicshop/basics/booleans.py,sha256=QM0pibMmEKRKtBlpW9M5hkfD4S4rzirde0f8TSTnScE,2198
85
+ atomicshop/basics/booleans.py,sha256=V36NaMf8AffhCom_ovQeOZlYcdtGyIcQwWKki6h7O0M,1745
86
86
  atomicshop/basics/bytes_arrays.py,sha256=WvSRDhIGt1ywF95t-yNgpxLm1nlZUbM1Dz6QckcyE8Y,5915
87
87
  atomicshop/basics/classes.py,sha256=T0Bm13hKvkXG3med68ptL7XuoWiCi3TE-K5TMINDlrY,10655
88
88
  atomicshop/basics/dicts.py,sha256=DeYHIh940pMMBrFhpXt4dsigFVYzTrlqWymNo4Pq_Js,14049
@@ -114,9 +114,9 @@ atomicshop/etws/traces/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3
114
114
  atomicshop/etws/traces/trace_dns.py,sha256=WvOZm7KNdP4r6ofkZhUGi9WjtYlkV3mUp_yxita3Qg4,6399
115
115
  atomicshop/etws/traces/trace_sysmon_process_creation.py,sha256=OM-bkK38uYMwWLZKNOTDa0Xdk3sO6sqsxoMUIiPvm5g,4656
116
116
  atomicshop/file_io/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
117
- atomicshop/file_io/csvs.py,sha256=XFZ-kvg2nWLXUpFh_FyPwOXB_TYLFRrHpS5hWMblTi8,9353
117
+ atomicshop/file_io/csvs.py,sha256=jBdm3_z5cMyvxLxJnGcybUAptHAbyL0r0tlLqY0sdTQ,9327
118
118
  atomicshop/file_io/docxs.py,sha256=ffJhnmM_WyD8mCoq2dGdpfahdIrGTPy96QVlH5EWjeI,5754
119
- atomicshop/file_io/file_io.py,sha256=hovI_WFn3YixQDAXLulZG8GxW7whog-AC5KeK-03-xI,7164
119
+ atomicshop/file_io/file_io.py,sha256=5Kl0P6vF4GQVdwew1lzHLb-db9qiMvDjTgccbi5P-zk,7167
120
120
  atomicshop/file_io/jsons.py,sha256=q9ZU8slBKnHLrtn3TnbK1qxrRpj5ZvCm6AlsFzoANjo,5303
121
121
  atomicshop/file_io/tomls.py,sha256=ol8EvQPf9sryTmZUf1v55BYSUQ6ml7HVVBHpNKbsIlA,9768
122
122
  atomicshop/file_io/xlsxs.py,sha256=v_dyg9GD4LqgWi6wA1QuWRZ8zG4ZwB6Dz52ytdcmmmI,2184
@@ -125,19 +125,19 @@ atomicshop/mitm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
125
125
  atomicshop/mitm/config_static.py,sha256=ROAtbibSWSsF3BraUbhu-QO3MPIFqYY5KUKgsQbiSkk,7813
126
126
  atomicshop/mitm/config_toml_editor.py,sha256=2p1CMcktWRR_NW-SmyDwylu63ad5e0-w1QPMa8ZLDBw,1635
127
127
  atomicshop/mitm/connection_thread_worker.py,sha256=fybUBcZckgF7TC_P1z2yIYGH6ATX7jQEfsQSBuddt2s,16531
128
- atomicshop/mitm/import_config.py,sha256=_nu8mgA-M4s6dZ8_QWx3x0aVb75upvsCuX_PIUg4X2w,8345
128
+ atomicshop/mitm/import_config.py,sha256=ZKQXxbtjVqzN9fpRrMwPNQREecH06RG8F_nXZAKTUJM,8182
129
129
  atomicshop/mitm/initialize_engines.py,sha256=VyJE8QnzlgD3QbX5inz5o6rC3zQ3is9CeTL7-B10g1w,8292
130
130
  atomicshop/mitm/message.py,sha256=URR5JKSuAT8XmGIkyprEjlPW2GW4ef_gfUz_GgcFseE,2184
131
- atomicshop/mitm/mitm_main.py,sha256=wEW0UAqxnn9kI4oO5zyNtG04Glmi3hR-C-0W6SpxVWY,22446
132
- atomicshop/mitm/recs_files.py,sha256=VjgtJF7vy365mBjctwB2bpDYoLMkEeogzF8kbb01dAk,2977
133
- atomicshop/mitm/shared_functions.py,sha256=jjCDZVQCwQ8hf9QNMe3T8W3ISkfZo4Mm2HtXOJLZYgI,1999
134
- atomicshop/mitm/statistic_analyzer.py,sha256=mBmwEe68WAjnIskGndQTRldnsAsDHuaOW0O7UXlM3Pc,26702
131
+ atomicshop/mitm/mitm_main.py,sha256=ICQS8-4-owDhPUIQohymLShzgCZwgq7jUocZHX1J3Zo,22592
132
+ atomicshop/mitm/recs_files.py,sha256=mMyO1kPB-VkS_pbWCDhZHKdbWzlPbYSout61QuzHOao,3077
133
+ atomicshop/mitm/shared_functions.py,sha256=l6oEyv4ug5D_03V3QLADYoocbcL2Ml_dYVW2WKM21l4,1818
134
+ atomicshop/mitm/statistic_analyzer.py,sha256=5_sAYGX2Xunzo_pS2W5WijNCwr_BlGJbbOO462y_wN4,27533
135
135
  atomicshop/mitm/engines/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
136
136
  atomicshop/mitm/engines/create_module_template.py,sha256=tRjVSm1sD6FzML71Qbuwvita0qsusdFGm8NZLsZ-XMs,4853
137
137
  atomicshop/mitm/engines/create_module_template_example.py,sha256=X5xhvbV6-g9jU_bQVhf_crZmaH50LRWz3bS-faQ18ds,489
138
138
  atomicshop/mitm/engines/__parent/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
139
139
  atomicshop/mitm/engines/__parent/parser___parent.py,sha256=RK2wviepP0oeq7zuLpgkvqvTJtc0r0a7hDGWdV0dGc4,657
140
- atomicshop/mitm/engines/__parent/recorder___parent.py,sha256=xPc_5BGDubPfgNa4z8qBtm8MacDlas_yQNkfNsji46k,4677
140
+ atomicshop/mitm/engines/__parent/recorder___parent.py,sha256=xJ3YVa8XYwcKqCwUIEXf6_UGnCbpRsvoicUNe0WxMfs,3782
141
141
  atomicshop/mitm/engines/__parent/responder___parent.py,sha256=7WQeR3UmMnN74bDwn-0nz2OfhXJ3-ClXpNGUFZ7wJUE,12004
142
142
  atomicshop/mitm/engines/__reference_general/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
143
143
  atomicshop/mitm/engines/__reference_general/parser___reference_general.py,sha256=57MEPZMAjTO6xBDZ-yt6lgGJyqRrP0Do5Gk_cgCiPns,2998
@@ -145,7 +145,7 @@ atomicshop/mitm/engines/__reference_general/recorder___reference_general.py,sha2
145
145
  atomicshop/mitm/engines/__reference_general/responder___reference_general.py,sha256=IUyQYMPeEhIARfALWiKPFeXagSQD6lRzAxUdi4ZIT88,7010
146
146
  atomicshop/mitm/statistic_analyzer_helper/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
147
147
  atomicshop/mitm/statistic_analyzer_helper/analyzer_helper.py,sha256=pk6L1t1ea1kvlBoR9QEJptOmaX-mumhwLsP2GCKukbk,5920
148
- atomicshop/mitm/statistic_analyzer_helper/moving_average_helper.py,sha256=XkJBAKD20j4rBpTWuhRJG3ONDWsktOh3PfOqzVDSORo,17883
148
+ atomicshop/mitm/statistic_analyzer_helper/moving_average_helper.py,sha256=Fw-1lH1NOTinMsJnCbCgMHfdoH5Vav388puKm3DmdEs,19746
149
149
  atomicshop/monitor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
150
150
  atomicshop/monitor/change_monitor.py,sha256=K5NlVp99XIDDPnQQMdru4BDmua_DtcDIhVAzkTOvD5s,7673
151
151
  atomicshop/monitor/checks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -194,6 +194,7 @@ atomicshop/wrappers/certauthw/certauth.py,sha256=hKedW0DOWlEigSNm8wu4SqHkCQsGJ1t
194
194
  atomicshop/wrappers/certauthw/certauthw.py,sha256=4WvhjANI7Kzqrr_nKmtA8Kf7B6rute_5wfP65gwQrjw,8082
195
195
  atomicshop/wrappers/ctyping/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
196
196
  atomicshop/wrappers/ctyping/process_winapi.py,sha256=QcXL-ETtlSSkoT8F7pYle97ubGWsjYp8cx8HxkVMgAc,2762
197
+ atomicshop/wrappers/ctyping/win_console.py,sha256=uTtjkz9rY559AaV0dhyZYUSSEe9cn6Du2DgurdMtX-M,1158
197
198
  atomicshop/wrappers/ctyping/etw_winapi/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
198
199
  atomicshop/wrappers/ctyping/etw_winapi/const.py,sha256=stZHZ7tSiSAs04ikr7uH-Td_yBXxsF-bp2Q0F3K2fsM,9543
199
200
  atomicshop/wrappers/ctyping/etw_winapi/etw_functions.py,sha256=Iwd0wIuoxpjMaaOfZZtT1bPtDTsMO8jjItBE5bvkocM,11546
@@ -249,14 +250,14 @@ atomicshop/wrappers/loggingw/filters.py,sha256=48UVhJHemCS0agXmQP8dHvAHM8r9DFphJ
249
250
  atomicshop/wrappers/loggingw/formatters.py,sha256=ZY12IokVY1G_Wzn2Zlv9qjK-e8CtIK6yUgUfPHvH2BU,5802
250
251
  atomicshop/wrappers/loggingw/handlers.py,sha256=vxaSSnlJGs9NKJvYROKtNjaFTqePdHy0sz-GwN5aNPw,19035
251
252
  atomicshop/wrappers/loggingw/loggers.py,sha256=mmM__XR3W4QC82wbsDRG_M4_0JYGGEP0Qn0WCOSp-go,2910
252
- atomicshop/wrappers/loggingw/loggingw.py,sha256=hHCCNJTCI45vGMoNFqQQip5svqffh4-vJ1AnZ7yeE0M,21329
253
- atomicshop/wrappers/loggingw/reading.py,sha256=ERBSiQbEksySKpXpu2E_6k9dZ6MPH95ZIsmdjWW9MUE,16436
253
+ atomicshop/wrappers/loggingw/loggingw.py,sha256=64r5XZSAwJ5GfkN7JqAvuLFlJRdf79n0jr_FriaaaCw,21330
254
+ atomicshop/wrappers/loggingw/reading.py,sha256=sCNlgqLNH5XdKqOOjjEox7CvViMHzs6h7-hwCnx4NKk,17566
254
255
  atomicshop/wrappers/mongodbw/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
255
256
  atomicshop/wrappers/mongodbw/install_mongodb.py,sha256=3ZPqrXxj3lC-PnAKGXclylLuOqsbyXYeUpb5iGjdeUU,6626
256
257
  atomicshop/wrappers/mongodbw/mongo_infra.py,sha256=IjEF0jPzQz866MpTm7rnksnyyWQeUT_B2h2DA9ryAio,2034
257
- atomicshop/wrappers/mongodbw/mongodbw.py,sha256=v_5YYgbgT0F-k2I-hDHQCMPhpry8OonsPclubJk2NG0,31509
258
+ atomicshop/wrappers/mongodbw/mongodbw.py,sha256=oM2pS-M0EI7HhewWY0ri_Ri9U5GOBo0CehPmo4Yas3o,32736
258
259
  atomicshop/wrappers/nodejsw/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
259
- atomicshop/wrappers/nodejsw/install_nodejs.py,sha256=QZg-R2iTQt7kFb8wNtnTmwraSGwvUs34JIasdbNa7ZU,5154
260
+ atomicshop/wrappers/nodejsw/install_nodejs.py,sha256=TKGa3jSlSqZTL2NA0nMkWDFtlkz7rxGGn44ywCg7MN8,5228
260
261
  atomicshop/wrappers/playwrightw/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
261
262
  atomicshop/wrappers/playwrightw/_tryouts.py,sha256=l1BLkFsiIMNlgv7nfZd1XGEvXQkIQkIcg48__9OaC00,4920
262
263
  atomicshop/wrappers/playwrightw/base.py,sha256=WeRpx8otdXuKSr-BjY-uCJTze21kbPpfitoOjKQz5-g,9818
@@ -296,7 +297,7 @@ atomicshop/wrappers/socketw/accepter.py,sha256=hZZKVYlF3LOHQJsSIEKXZUf6QXXWm-Atq
296
297
  atomicshop/wrappers/socketw/base.py,sha256=evoOIxg5Xff3THJnrVX00D5HobaOpDp6_e_gso7TJmA,2191
297
298
  atomicshop/wrappers/socketw/certificator.py,sha256=3CpQKtcW68FSbH6LVSEZTqWBS6Yg_-3K0x4nFkId4UY,12236
298
299
  atomicshop/wrappers/socketw/creator.py,sha256=3_OraDkw2DAWZfoSdY3svCGMOIxpjLEEY7NxWd7M5P4,9873
299
- atomicshop/wrappers/socketw/dns_server.py,sha256=F-t2ZOrMAS0hZ5AaoTsIEbe7jj0tUathp8gs4CTNDqM,48967
300
+ atomicshop/wrappers/socketw/dns_server.py,sha256=VHV6s7vd0zqqW3dhE6li-260YRzmEB5ZUXqYJ9p0vVA,49069
300
301
  atomicshop/wrappers/socketw/exception_wrapper.py,sha256=B-X5SHLSUIWToihH2MKnOB1F4A81_X0DpLLfnYKYbEc,7067
301
302
  atomicshop/wrappers/socketw/get_process.py,sha256=zKEqh98cB9UDLFhtxVpperfXsCjyIMNANHilDD06p0U,6094
302
303
  atomicshop/wrappers/socketw/receiver.py,sha256=XVvWOoeCo3vA0O5p19ryi-hcDIyx382WNG7WzMNVeYk,9322
@@ -304,13 +305,13 @@ atomicshop/wrappers/socketw/sender.py,sha256=5HPrgTS2pA1g-jbG1TUtR7drHT1Z_8UevlR
304
305
  atomicshop/wrappers/socketw/sni.py,sha256=J1kPnQ77XwKN1pO5aOI1c_VfhuivCm95OOaQxMpPuZ0,17627
305
306
  atomicshop/wrappers/socketw/socket_client.py,sha256=XC-YaqA1wu0rvWQ9Q99DWLxcycKPkPc72pSnflzalfo,20320
306
307
  atomicshop/wrappers/socketw/socket_server_tester.py,sha256=Qobmh4XV8ZxLUaw-eW4ESKAbeSLecCKn2OWFzMhadk0,6420
307
- atomicshop/wrappers/socketw/socket_wrapper.py,sha256=jOIux6eedupRtiEKQeaPSBt7xJbJixpBbK1zNcQUDqU,35451
308
+ atomicshop/wrappers/socketw/socket_wrapper.py,sha256=WtylpezgIIBuz-A6PfM0hO1sm9Exd4j3qhDXcFc74-E,35567
308
309
  atomicshop/wrappers/socketw/ssl_base.py,sha256=kmiif84kMhBr5yjQW17p935sfjR5JKG0LxIwBA4iVvU,2275
309
310
  atomicshop/wrappers/socketw/statistics_csv.py,sha256=w1AH-zf4mBuT4euf28UKij9ihM-b1BRU9Qfby0QDdqI,2957
310
311
  atomicshop/wrappers/winregw/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
311
312
  atomicshop/wrappers/winregw/winreg_network.py,sha256=bQ8Jql8bVGBJ0dt3VQ56lga_1LBOMLI3Km_otvvbU6c,7138
312
- atomicshop-2.16.31.dist-info/LICENSE.txt,sha256=lLU7EYycfYcK2NR_1gfnhnRC8b8ccOTElACYplgZN88,1094
313
- atomicshop-2.16.31.dist-info/METADATA,sha256=qf8gXgqyW9JhWKob2yEkM118Kt1OcfQyVeqTrz9nj-0,10473
314
- atomicshop-2.16.31.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
315
- atomicshop-2.16.31.dist-info/top_level.txt,sha256=EgKJB-7xcrAPeqTRF2laD_Np2gNGYkJkd4OyXqpJphA,11
316
- atomicshop-2.16.31.dist-info/RECORD,,
313
+ atomicshop-2.16.32.dist-info/LICENSE.txt,sha256=lLU7EYycfYcK2NR_1gfnhnRC8b8ccOTElACYplgZN88,1094
314
+ atomicshop-2.16.32.dist-info/METADATA,sha256=9y8KcvM3GJNgsSJQ2aDxHl6YGVSkAV6KWlkAO3t9HTQ,10473
315
+ atomicshop-2.16.32.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
316
+ atomicshop-2.16.32.dist-info/top_level.txt,sha256=EgKJB-7xcrAPeqTRF2laD_Np2gNGYkJkd4OyXqpJphA,11
317
+ atomicshop-2.16.32.dist-info/RECORD,,