atomicshop 2.11.49__py3-none-any.whl → 2.12.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.

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.11.49'
4
+ __version__ = '2.12.1'
@@ -0,0 +1,9 @@
1
+ from atomicshop.wrappers.fibratusw import install
2
+
3
+
4
+ def main():
5
+ install.install_fibratus(remove_file_after_installation=True)
6
+
7
+
8
+ if __name__ == '__main__':
9
+ main()
@@ -5,7 +5,7 @@ from .print_api import print_api
5
5
  from .basics.lists import remove_duplicates
6
6
  from .datetimes import convert_single_digit_to_zero_padded, create_date_range_for_year, \
7
7
  create_date_range_for_year_month
8
- from .file_io.csvs import read_csv_to_list
8
+ from .file_io import csvs
9
9
 
10
10
 
11
11
  class AppointmentManager:
@@ -38,7 +38,8 @@ class AppointmentManager:
38
38
  def read_latest_date_csv(self):
39
39
  try:
40
40
  # Read the csv to list of dicts.
41
- csv_list, _ = read_csv_to_list(file_path=self.latest_date_to_check_filepath, raise_exception=True)
41
+ csv_list, _ = csvs.read_csv_to_list_of_dicts_by_header(
42
+ file_path=self.latest_date_to_check_filepath, raise_exception=True)
42
43
  # It has only 1 line, so get it to dict.
43
44
  latest_date_dict = csv_list[0]
44
45
 
@@ -100,7 +101,8 @@ class BlacklistEngine:
100
101
  def read_blacklist_csv(self) -> None:
101
102
  try:
102
103
  # Read the csv to list of dicts.
103
- csv_list, _ = read_csv_to_list(file_path=self.blacklist_dates_filepath, raise_exception=True)
104
+ csv_list, _ = csvs.read_csv_to_list_of_dicts_by_header(
105
+ file_path=self.blacklist_dates_filepath, raise_exception=True)
104
106
 
105
107
  daterange = None
106
108
  # Iterate through all the rows.
@@ -6,14 +6,25 @@ from . import file_io
6
6
 
7
7
 
8
8
  @read_file_decorator
9
- def read_csv_to_list(file_path: str,
10
- file_mode: str = 'r',
11
- encoding=None,
12
- header: list = None,
13
- file_object=None,
14
- **kwargs) -> Tuple[List, List | None]:
9
+ def read_csv_to_list_of_dicts_by_header(
10
+ file_path: str,
11
+ file_mode: str = 'r',
12
+ encoding=None,
13
+ header: list = None,
14
+ file_object=None,
15
+ **kwargs
16
+ ) -> Tuple[List, List | None]:
15
17
  """
16
18
  Function to read csv file and output its contents as list of dictionaries for each row.
19
+ Each key of the dictionary is a header field.
20
+
21
+ Example:
22
+ CSV file:
23
+ name,age,city
24
+ John,25,New York
25
+
26
+ Output:
27
+ [{'name': 'John', 'age': '25', 'city': 'New York'}]
17
28
 
18
29
  :param file_path: String with full file path to json file.
19
30
  :param file_mode: string, file reading mode. Examples: 'r', 'rb'. Default is 'r'.
@@ -39,25 +50,68 @@ def read_csv_to_list(file_path: str,
39
50
  return csv_list, header
40
51
 
41
52
 
42
- def write_list_to_csv(csv_list: list, csv_filepath: str) -> None:
53
+ @read_file_decorator
54
+ def read_csv_to_list_of_lists(
55
+ file_path: str,
56
+ file_mode: str = 'r',
57
+ encoding=None,
58
+ exclude_header_from_content: bool = False,
59
+ file_object=None,
60
+ **kwargs
61
+ ) -> Tuple[List, List | None]:
62
+ """
63
+ Function to read csv file and output its contents as list of lists for each row.
64
+
65
+ Example:
66
+ CSV file:
67
+ name,age,city
68
+ John,25,New York
69
+
70
+ Output:
71
+ [['name', 'age', 'city'], ['John', '25', 'New York']]
72
+
73
+ :param file_path: String with full file path to json file.
74
+ :param file_mode: string, file reading mode. Examples: 'r', 'rb'. Default is 'r'.
75
+ :param encoding: string, encoding of the file. Default is 'None'.
76
+ :param exclude_header_from_content: Boolean, if True, the header will be excluded from the content.
77
+ :param file_object: file object of the 'open()' function in the decorator. Decorator executes the 'with open()'
78
+ statement and passes to this function. That's why the default is 'None', since we get it from the decorator.
79
+ :param kwargs: Keyword arguments for 'read_file' function.
80
+ :return: list.
81
+ """
82
+
83
+ # Read CSV file to list of lists.
84
+ csv_reader = csv.reader(file_object)
85
+
86
+ csv_list = list(csv_reader)
87
+ header = csv_list[0]
88
+
89
+ if exclude_header_from_content:
90
+ csv_list.pop(0)
91
+
92
+ return csv_list, header
93
+
94
+
95
+ def write_list_to_csv(file_path: str, content_list: list, mode: str = 'w') -> None:
43
96
  """
44
97
  Function to write list object that each iteration of it contains dict object with same keys and different values.
45
98
 
46
- :param csv_list: List object that each iteration contains dictionary with same keys and different values.
47
- :param csv_filepath: Full file path to CSV file.
99
+ :param file_path: Full file path to CSV file.
100
+ :param content_list: List object that each iteration contains dictionary with same keys and different values.
101
+ :param mode: String, file writing mode. Default is 'w'.
48
102
  :return: None.
49
103
  """
50
104
 
51
- with open(csv_filepath, mode='w') as csv_file:
105
+ with open(file_path, mode=mode) as csv_file:
52
106
  # Create header from keys of the first dictionary in list.
53
- header = csv_list[0].keys()
107
+ header = content_list[0].keys()
54
108
  # Create CSV writer.
55
109
  writer = csv.DictWriter(csv_file, fieldnames=header, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL)
56
110
 
57
111
  # Write header.
58
112
  writer.writeheader()
59
113
  # Write list of dits as rows.
60
- writer.writerows(csv_list)
114
+ writer.writerows(content_list)
61
115
 
62
116
 
63
117
  def get_header(file_path: str, print_kwargs: dict = None) -> list:
atomicshop/filesystem.py CHANGED
@@ -6,6 +6,8 @@ import shutil
6
6
  import stat
7
7
  import errno
8
8
  from contextlib import contextmanager
9
+ from typing import Literal
10
+ import tempfile
9
11
 
10
12
  import psutil
11
13
 
@@ -115,6 +117,22 @@ def get_working_directory() -> str:
115
117
  return str(Path.cwd())
116
118
 
117
119
 
120
+ def get_temp_directory() -> str:
121
+ """
122
+ The function returns temporary directory of the system.
123
+
124
+ :return: string.
125
+ """
126
+
127
+ # Get the temporary directory in 8.3 format
128
+ short_temp_dir = tempfile.gettempdir()
129
+
130
+ # Convert to the long path name
131
+ long_temp_dir = str(Path(short_temp_dir).resolve())
132
+
133
+ return long_temp_dir
134
+
135
+
118
136
  def get_file_directory(file_path: str) -> str:
119
137
  """
120
138
  The function will return directory of the file.
@@ -1273,3 +1291,31 @@ def is_file_open_by_process(file_path: str) -> bool:
1273
1291
  continue
1274
1292
 
1275
1293
  return False
1294
+
1295
+
1296
+ def get_download_directory(place: Literal['temp', 'script', 'working'] = 'temp', script_path: str = None) -> str:
1297
+ """
1298
+ The function returns the default download directory based on place.
1299
+
1300
+ :param place: string,
1301
+ 'temp', then the function will return the temporary directory.
1302
+ 'script', then the function will return the directory of the script.
1303
+ 'working', then the function will return the working directory.
1304
+ :param script_path: string, full path to the script.
1305
+ :return: string, full path to the default download directory.
1306
+ """
1307
+
1308
+ if place == 'script' and script_path is None:
1309
+ raise ValueError("Script path must be specified if place is 'script'.")
1310
+
1311
+ # Get the download directory based on the operating system
1312
+ if place == 'script':
1313
+ download_directory = get_file_directory(script_path)
1314
+ elif place == 'working':
1315
+ download_directory = get_working_directory()
1316
+ elif place == 'temp':
1317
+ download_directory = get_temp_directory()
1318
+ else:
1319
+ raise ValueError("Invalid place specified.")
1320
+
1321
+ return download_directory
@@ -155,7 +155,9 @@ def analyze(main_file_path: str):
155
155
 
156
156
  # Get the content from statistics files.
157
157
  statistics_content: list = reading.get_logs(
158
- config['statistic_files_path'], pattern='statistics*.csv', log_type='csv',
158
+ config['statistic_files_path'],
159
+ pattern='statistics*.csv',
160
+ log_type='csv',
159
161
  )
160
162
 
161
163
  # Initialize loop.
atomicshop/permissions.py CHANGED
@@ -108,3 +108,32 @@ def expand_user_path(user_name, path):
108
108
  pwnam = pwd.getpwnam(user_name)
109
109
  home_dir = pwnam.pw_dir
110
110
  return path.replace("~", home_dir)
111
+
112
+
113
+ def unblock_file_windows(file_path):
114
+ """
115
+ Unblock a file on Windows. This is used to unblock files downloaded from the internet.
116
+ When you Right-click then navigate to Properties, you will see the Unblock checkbox.
117
+ :param file_path:
118
+ :return:
119
+ """
120
+ try:
121
+ subprocess.run(["powershell", "-Command", f"Unblock-File -Path '{file_path}'"], check=True)
122
+ print(f"Successfully unblocked the file: {file_path}")
123
+ except subprocess.CalledProcessError as e:
124
+ print(f"Failed to unblock the file: {file_path}\nError: {e}")
125
+
126
+
127
+ def get_command_to_run_as_admin_windows(command: str) -> str:
128
+ """
129
+ Function returns the command to run a command as administrator on Windows.
130
+ :param command: str, command to run.
131
+ :return: str, command to run as administrator.
132
+ """
133
+
134
+ executable = command.split()[0]
135
+ command = (
136
+ f"powershell -Command "
137
+ f"\"Start-Process {executable} -ArgumentList '{' '.join(command.split()[1:])}' -Verb RunAs\"")
138
+
139
+ return command
atomicshop/web.py CHANGED
@@ -174,6 +174,7 @@ def download(file_url: str, target_directory: str, file_name: str = None, **kwar
174
174
  file_path: str = f'{target_directory}{os.sep}{file_name}'
175
175
 
176
176
  print_api(f'Downloading: {file_url}', **kwargs)
177
+ print_api(f'To: {file_path}', **kwargs)
177
178
 
178
179
  # In order to use 'urllib.request', it is not enough to 'import urllib', you need to 'import urllib.request'.
179
180
  # Open the URL for data gathering.
@@ -245,6 +245,6 @@ def save_firmware_uids_as_csv(
245
245
 
246
246
  # Save UIDs as CSV file.
247
247
  file_path = directory_path + os.sep + 'uids.csv'
248
- csvs.write_list_to_csv(export_list, file_path)
248
+ csvs.write_list_to_csv(file_path, export_list)
249
249
 
250
250
  return None
@@ -511,6 +511,6 @@ def save_firmware_uids_as_csv(
511
511
 
512
512
  # Save UIDs as CSV file.
513
513
  file_path = directory_path + os.sep + 'uids.csv'
514
- csvs.write_list_to_csv(export_list, file_path)
514
+ csvs.write_list_to_csv(file_path, export_list)
515
515
 
516
516
  return None
File without changes
@@ -0,0 +1,81 @@
1
+ import os.path
2
+ import subprocess
3
+ import time
4
+ from typing import Literal
5
+
6
+ from .. import githubw, msiw
7
+ from ... import filesystem
8
+ from ...print_api import print_api
9
+
10
+
11
+ DEFAULT_INSTALLATION_EXE_PATH = r"C:\Program Files\Fibratus\Bin\fibratus.exe"
12
+ WAIT_SECONDS_FOR_EXECUTABLE_TO_APPEAR_AFTER_INSTALLATION: float = 10
13
+
14
+
15
+ def install_fibratus(
16
+ installation_file_download_directory: str = None,
17
+ place_to_download_file: Literal['working', 'temp', 'script'] = 'temp',
18
+ remove_file_after_installation: bool = False
19
+ ):
20
+ """
21
+ Download latest release from GitHub and install Fibratus.
22
+ :param installation_file_download_directory: Directory to download the installation file. If None, the download
23
+ directory will be automatically determined, by the 'place_to_download_file' parameter.
24
+ :param place_to_download_file: Where to download the installation file.
25
+ 'working' is the working directory of the script.
26
+ 'temp' is the temporary directory.
27
+ 'script' is the directory of the script.
28
+ :param remove_file_after_installation: Whether to remove the installation file after installation.
29
+ :return:
30
+ """
31
+
32
+ if not installation_file_download_directory:
33
+ installation_file_download_directory = filesystem.get_download_directory(
34
+ place=place_to_download_file, script_path=__file__)
35
+
36
+ github_wrapper = githubw.GitHubWrapper(user_name='rabbitstack', repo_name='fibratus')
37
+ fibratus_setup_file_path: str = github_wrapper.download_latest_release(
38
+ target_directory=installation_file_download_directory,
39
+ string_pattern='*fibratus-*-amd64.msi',
40
+ exclude_string='slim')
41
+
42
+ # Install the MSI file
43
+ msiw.install_msi(
44
+ msi_path=fibratus_setup_file_path, exit_on_error=True, as_admin=True)
45
+
46
+ count = 0
47
+ while count != WAIT_SECONDS_FOR_EXECUTABLE_TO_APPEAR_AFTER_INSTALLATION:
48
+ if os.path.isfile(DEFAULT_INSTALLATION_EXE_PATH):
49
+ break
50
+ count += 1
51
+ time.sleep(1)
52
+
53
+ if count == WAIT_SECONDS_FOR_EXECUTABLE_TO_APPEAR_AFTER_INSTALLATION:
54
+ message = \
55
+ (f"Fibratus installation failed. The executable was not found after "
56
+ f"{str(WAIT_SECONDS_FOR_EXECUTABLE_TO_APPEAR_AFTER_INSTALLATION)} seconds.\n"
57
+ f"{DEFAULT_INSTALLATION_EXE_PATH}")
58
+ print_api(message, color="red")
59
+
60
+ result = None
61
+ # Check if the installation was successful
62
+ try:
63
+ result = subprocess.run([DEFAULT_INSTALLATION_EXE_PATH], capture_output=True, text=True)
64
+ except FileNotFoundError:
65
+ print_api("Fibratus executable not found.", color="red")
66
+
67
+ if result:
68
+ if result.returncode == 0:
69
+ print_api("Fibratus installed successfully. Please restart.", color="green")
70
+ else:
71
+ print_api("Fibratus installation failed.", color="red")
72
+ print_api(result.stderr)
73
+ raise Exception("Fibratus installation failed.")
74
+ else:
75
+ print_api("Fibratus executable not found.", color="red")
76
+
77
+ # Wait for the installation to finish before removing the file.
78
+ time.sleep(5)
79
+
80
+ if remove_file_after_installation:
81
+ filesystem.remove_file(fibratus_setup_file_path)
@@ -3,6 +3,7 @@ import fnmatch
3
3
 
4
4
  from .. import web, urls
5
5
  from ..print_api import print_api
6
+ from ..basics import strings
6
7
 
7
8
 
8
9
  class GitHubWrapper:
@@ -146,22 +147,17 @@ class GitHubWrapper:
146
147
  archive_remove_first_directory=archive_remove_first_directory,
147
148
  **kwargs)
148
149
 
149
- def download_and_extract_latest_release(
150
+ def get_latest_release_url(
150
151
  self,
151
- target_directory: str,
152
152
  string_pattern: str,
153
- archive_remove_first_directory: bool = False,
153
+ exclude_string: str = None,
154
154
  **kwargs):
155
155
  """
156
- This function will download the latest release from the GitHub repository, extract the file and remove the file,
157
- leaving only the extracted folder.
158
- :param target_directory: str, the target directory to download and extract the file.
156
+ This function will return the latest release url.
159
157
  :param string_pattern: str, the string pattern to search in the latest release. Wildcards can be used.
160
- :param archive_remove_first_directory: bool, sets if archive extract function will extract the archive
161
- without first directory in the archive. Check reference in the
162
- 'archiver.zip.extract_archive_with_zipfile' function.
158
+ :param exclude_string: str, the string to exclude from the search. No wildcards can be used.
163
159
  :param kwargs: dict, the print arguments for the 'print_api' function.
164
- :return:
160
+ :return: str, the latest release url.
165
161
  """
166
162
 
167
163
  # Get the 'assets' key of the latest release json.
@@ -172,6 +168,12 @@ class GitHubWrapper:
172
168
  for single_dict in github_latest_releases_list:
173
169
  download_urls.append(single_dict['browser_download_url'])
174
170
 
171
+ # Exclude urls against 'exclude_string'.
172
+ if exclude_string:
173
+ for download_url in download_urls:
174
+ if exclude_string in download_url:
175
+ download_urls.remove(download_url)
176
+
175
177
  # Find urls against 'string_pattern'.
176
178
  found_urls = fnmatch.filter(download_urls, string_pattern)
177
179
 
@@ -182,8 +184,55 @@ class GitHubWrapper:
182
184
  f'{found_urls}'
183
185
  print_api(message, color="red", error_type=True, **kwargs)
184
186
 
187
+ return found_urls[0]
188
+
189
+ def download_latest_release(
190
+ self,
191
+ target_directory: str,
192
+ string_pattern: str,
193
+ exclude_string: str = None,
194
+ **kwargs):
195
+ """
196
+ This function will download the latest release from the GitHub repository.
197
+ :param target_directory: str, the target directory to download the file.
198
+ :param string_pattern: str, the string pattern to search in the latest release. Wildcards can be used.
199
+ :param exclude_string: str, the string to exclude from the search. No wildcards can be used.
200
+ The 'excluded_string' will be filtered before the 'string_pattern' entries.
201
+ :param kwargs: dict, the print arguments for the 'print_api' function.
202
+ :return:
203
+ """
204
+
205
+ # Get the latest release url.
206
+ found_url = self.get_latest_release_url(string_pattern=string_pattern, exclude_string=exclude_string, **kwargs)
207
+
208
+ downloaded_file_path = web.download(file_url=found_url, target_directory=target_directory, **kwargs)
209
+ return downloaded_file_path
210
+
211
+ def download_and_extract_latest_release(
212
+ self,
213
+ target_directory: str,
214
+ string_pattern: str,
215
+ exclude_string: str = None,
216
+ archive_remove_first_directory: bool = False,
217
+ **kwargs):
218
+ """
219
+ This function will download the latest release from the GitHub repository, extract the file and remove the file,
220
+ leaving only the extracted folder.
221
+ :param target_directory: str, the target directory to download and extract the file.
222
+ :param string_pattern: str, the string pattern to search in the latest release. Wildcards can be used.
223
+ :param exclude_string: str, the string to exclude from the search. No wildcards can be used.
224
+ :param archive_remove_first_directory: bool, sets if archive extract function will extract the archive
225
+ without first directory in the archive. Check reference in the
226
+ 'archiver.zip.extract_archive_with_zipfile' function.
227
+ :param kwargs: dict, the print arguments for the 'print_api' function.
228
+ :return:
229
+ """
230
+
231
+ # Get the latest release url.
232
+ found_url = self.get_latest_release_url(string_pattern=string_pattern, exclude_string=exclude_string, **kwargs)
233
+
185
234
  web.download_and_extract_file(
186
- file_url=found_urls[0],
235
+ file_url=found_url,
187
236
  target_directory=target_directory,
188
237
  archive_remove_first_directory=archive_remove_first_directory,
189
238
  **kwargs)
@@ -1,12 +1,84 @@
1
1
  import os
2
2
  from typing import Literal
3
+ from pathlib import Path
3
4
 
4
5
  from ... import filesystem, datetimes
5
6
  from ...file_io import csvs
6
7
 
7
8
 
9
+ def get_logs_paths(
10
+ log_files_directory_path: str = None,
11
+ log_file_path: str = None,
12
+ pattern: str = '*.*',
13
+ log_type: Literal['csv'] = 'csv',
14
+ latest_only: bool = False,
15
+ previous_day_only: bool = False
16
+ ):
17
+ """
18
+ This function gets the logs file paths from the directory. Supports rotating files to get the logs by time.
19
+
20
+ :param log_files_directory_path: Path to the log files. If specified, the function will get all the files from the
21
+ directory by the 'pattern'.
22
+ :param log_file_path: Path to the log file. If specified, the function will get the file and all the rotated logs
23
+ associated with this file. The 'pattern' will become the file name using the file name and extension.
24
+
25
+ Example:
26
+ log_file_path = 'C:/logs/test_log.csv'
27
+
28
+ # The function will get all the files that start with 'test_log' and have '.csv' extension:
29
+ pattern = 'test_log*.csv'
30
+
31
+ # The 'log_files_directory_path' will also be taken from the 'log_file_path':
32
+ log_files_directory_path = 'C:/logs'
33
+ :param pattern: Pattern to match the log files names.
34
+ Default pattern will match all the files.
35
+ :param log_type: Type of log to get.
36
+ :param latest_only: Boolean, if True, only the latest log file path will be returned.
37
+ :param previous_day_only: Boolean, if True, only the log file path from the previous day will be returned.
38
+ """
39
+
40
+ if not log_files_directory_path and not log_file_path:
41
+ raise ValueError('Either "log_files_directory_path" or "log_file_path" must be specified.')
42
+ elif log_files_directory_path and log_file_path:
43
+ raise ValueError('Both "log_files_directory_path" and "log_file_path" cannot be specified at the same time.')
44
+
45
+ if log_type != 'csv':
46
+ raise ValueError('Only "csv" log type is supported.')
47
+
48
+ if latest_only and previous_day_only:
49
+ raise ValueError('Both "latest_only" and "previous_day_only" cannot be True at the same time.')
50
+
51
+ # If log file path is specified, get the pattern from the file name.
52
+ if log_file_path:
53
+ # Build the pattern.
54
+ log_file_name: str = Path(log_file_path).stem
55
+ log_file_extension: str = Path(log_file_path).suffix
56
+ pattern = f'{log_file_name}*{log_file_extension}'
57
+
58
+ # Get the directory path from the file path.
59
+ log_files_directory_path = Path(log_file_path).parent
60
+
61
+ # Get all the log file paths by the pattern.
62
+ logs_files: list = filesystem.get_file_paths_from_directory(
63
+ log_files_directory_path, file_name_check_pattern=pattern,
64
+ add_last_modified_time=True, sort_by_last_modified_time=True)
65
+
66
+ if latest_only:
67
+ logs_files = [logs_files[-1]]
68
+
69
+ if previous_day_only:
70
+ # Check if there is a previous day log file.
71
+ if len(logs_files) == 1:
72
+ logs_files = []
73
+ else:
74
+ logs_files = [logs_files[-2]]
75
+
76
+ return logs_files
77
+
78
+
8
79
  def get_logs(
9
- path: str,
80
+ log_files_directory_path: str = None,
81
+ log_file_path: str = None,
10
82
  pattern: str = '*.*',
11
83
  log_type: Literal['csv'] = 'csv',
12
84
  header_type_of_files: Literal['first', 'all'] = 'first',
@@ -17,7 +89,8 @@ def get_logs(
17
89
  """
18
90
  This function gets the logs from the log files. Supports rotating files to get the logs by time.
19
91
 
20
- :param path: Path to the log files.
92
+ :param log_files_directory_path: Path to the log files. Check the 'get_logs_paths' function for more details.
93
+ :param log_file_path: Path to the log file. Check the 'get_logs_paths' function for more details.
21
94
  :param pattern: Pattern to match the log files names.
22
95
  Default pattern will match all the files.
23
96
  :param log_type: Type of log to get.
@@ -36,9 +109,13 @@ def get_logs(
36
109
  if remove_logs and move_to_path:
37
110
  raise ValueError('Both "remove_logs" and "move_to_path" cannot be True/specified at the same time.')
38
111
 
39
- logs_files: list = filesystem.get_file_paths_from_directory(
40
- path, file_name_check_pattern=pattern,
41
- add_last_modified_time=True, sort_by_last_modified_time=True)
112
+ if header_type_of_files not in ['first', 'all']:
113
+ raise ValueError('Only "first" and "all" header types are supported.')
114
+
115
+ # Get all the log file paths by the pattern.
116
+ logs_files: list = get_logs_paths(
117
+ log_files_directory_path=log_files_directory_path, log_file_path=log_file_path,
118
+ pattern=pattern, log_type=log_type)
42
119
 
43
120
  # Read all the logs.
44
121
  logs_content: list = list()
@@ -46,12 +123,12 @@ def get_logs(
46
123
  for single_file in logs_files:
47
124
  if log_type == 'csv':
48
125
  if header_type_of_files == 'all':
49
- csv_content, _ = csvs.read_csv_to_list(single_file['file_path'], **print_kwargs)
126
+ csv_content, _ = csvs.read_csv_to_list_of_dicts_by_header(single_file['file_path'], **print_kwargs)
50
127
  logs_content.extend(csv_content)
51
128
  elif header_type_of_files == 'first':
52
129
  # The function gets empty header to read it from the CSV file, the returns the header that it read.
53
130
  # Then each time the header is fed once again to the function.
54
- csv_content, header = csvs.read_csv_to_list(single_file['file_path'], header=header, **print_kwargs)
131
+ csv_content, header = csvs.read_csv_to_list_of_dicts_by_header(single_file['file_path'], header=header, **print_kwargs)
55
132
  # Any way the first file will be read with header.
56
133
  logs_content.extend(csv_content)
57
134
 
@@ -0,0 +1,60 @@
1
+ import subprocess
2
+
3
+ from ..print_api import print_api
4
+ from .. import permissions
5
+
6
+
7
+ ERROR_CODES = {
8
+ '1603': 'The App is already installed or Insufficient permissions',
9
+ '1619': 'This installation package could not be opened. Verify that the package exists and that you can '
10
+ 'install it manually, also check the installation command line switches'
11
+ }
12
+
13
+
14
+ def install_msi(
15
+ msi_path,
16
+ silent_no_gui=True,
17
+ silent_progress_bar=False,
18
+ no_restart=True,
19
+ as_admin=False,
20
+ exit_on_error=False,
21
+ print_kwargs=None):
22
+ """
23
+ Install an MSI file silently.
24
+ :param msi_path: str, path to the MSI file.
25
+ :param silent_no_gui: bool, whether to run the installation silently, without showing GUI.
26
+ :param silent_progress_bar: bool, whether to show a progress bar during silent installation.
27
+ :param no_restart: bool, whether to restart the computer after installation.
28
+ :param as_admin: bool, whether to run the installation as administrator.
29
+ :param exit_on_error: bool, whether to exit the script if the installation fails.
30
+ :param print_kwargs: dict, print_api kwargs.
31
+ :return:
32
+ """
33
+
34
+ if silent_progress_bar and silent_no_gui:
35
+ raise ValueError("silent_progress_bar and silent_no_gui cannot be both True.")
36
+
37
+ # Define the msiexec command
38
+ command = f'msiexec /i "{msi_path}"'
39
+
40
+ if silent_no_gui:
41
+ command = f"{command} /qn"
42
+ if silent_progress_bar:
43
+ command = f"{command} /qb"
44
+ if no_restart:
45
+ command = f"{command} /norestart"
46
+
47
+ if as_admin:
48
+ command = permissions.get_command_to_run_as_admin_windows(command)
49
+
50
+ # Run the command
51
+ result = subprocess.run(command, capture_output=True, shell=True, text=True)
52
+
53
+ # Check the result
54
+ if result.returncode == 0:
55
+ print_api("MSI Installation completed.", color="green", **(print_kwargs or {}))
56
+ else:
57
+ message = f"Installation failed. Return code: {result.returncode}\n{ERROR_CODES.get(str(result.returncode), '')}"
58
+ print_api(message, color="red", **(print_kwargs or {}))
59
+ if exit_on_error:
60
+ exit()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: atomicshop
3
- Version: 2.11.49
3
+ Version: 2.12.1
4
4
  Summary: Atomic functions and classes to make developer life easier
5
5
  Author: Denis Kras
6
6
  License: MIT License
@@ -1,8 +1,8 @@
1
- atomicshop/__init__.py,sha256=qgi85ZKKeYx9w0YfZF0iWyjf6YkZavOGGnHhYdTgoP4,124
1
+ atomicshop/__init__.py,sha256=EvosCo6MVhZ-HXgV5FtdTdD7o9rDV_EL4_xqhrsoJaw,123
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
- atomicshop/appointment_management.py,sha256=N3wVGJgrqJfsj_lqiRfaL3FxMEe57by5Stzanh189mk,7263
5
+ atomicshop/appointment_management.py,sha256=BsYH_PClTGLVazcuNjt30--hpXKYjSmHp1R1iQbM4Hc,7330
6
6
  atomicshop/certificates.py,sha256=J-cmd6Rpq3zZyzsOH-GcdqIXdg2UwM8_E9mg7XtUph8,3787
7
7
  atomicshop/command_line_processing.py,sha256=u5yT9Ger_cu7ni5ID0VFlRbVD46ARHeNC9tRM-_YXrQ,1038
8
8
  atomicshop/config_init.py,sha256=z2RXD_mw9nQlAOpuGry1h9QT-2LhNscXgGAktN3dCVQ,2497
@@ -14,7 +14,7 @@ atomicshop/dns.py,sha256=bNZOo5jVPzq7OT2qCPukXoK3zb1oOsyaelUwQEyK1SA,2500
14
14
  atomicshop/domains.py,sha256=Rxu6JhhMqFZRcoFs69IoEd1PtYca0lMCG6F1AomP7z4,3197
15
15
  atomicshop/emails.py,sha256=I0KyODQpIMEsNRi9YWSOL8EUPBiWyon3HRdIuSj3AEU,1410
16
16
  atomicshop/file_types.py,sha256=-0jzQMRlmU1AP9DARjk-HJm1tVE22E6ngP2mRblyEjY,763
17
- atomicshop/filesystem.py,sha256=M4x2jTPMOyvwxJD9hJzgJ6BgF2Z9SIDf2Ye4ImXvXtY,47268
17
+ atomicshop/filesystem.py,sha256=JVWoOSkm-lfh11nBMlrP0w_YnrTFnJ5noYLtoN5Nf5o,48809
18
18
  atomicshop/functions.py,sha256=pK8hoCE9z61PtWCxQJsda7YAphrLH1wxU5x-1QJP-sY,499
19
19
  atomicshop/hashing.py,sha256=Le8qGFyt3_wX-zGTeQShz7L2HL_b6nVv9PnawjglyHo,3474
20
20
  atomicshop/http_parse.py,sha256=nrf2rZcprLqtW8HVrV7TCZ1iTBcWRRy-mXIlAOzcaJs,9703
@@ -22,7 +22,7 @@ atomicshop/inspect_wrapper.py,sha256=sGRVQhrJovNygHTydqJj0hxES-aB2Eg9KbIk3G31apw
22
22
  atomicshop/ip_addresses.py,sha256=Hvi4TumEFoTEpKWaq5WNF-YzcRzt24IxmNgv-Mgax1s,1190
23
23
  atomicshop/keyboard_press.py,sha256=1W5kRtOB75fulVx-uF2yarBhW0_IzdI1k73AnvXstk0,452
24
24
  atomicshop/pbtkmultifile_argparse.py,sha256=aEk8nhvoQVu-xyfZosK3ma17CwIgOjzO1erXXdjwtS4,4574
25
- atomicshop/permissions.py,sha256=441sSs7QBX3cuFIEG8U-RiiDwXE5XL-PESpRbZLKqqw,3047
25
+ atomicshop/permissions.py,sha256=pGynX57FqFdCW2Y6dE1T0oqL7ujagMAABw7nPHxi2IQ,4094
26
26
  atomicshop/print_api.py,sha256=DhbCQd0MWZZ5GYEk4oTu1opRFC-b31g1VWZgTGewG2Y,11568
27
27
  atomicshop/process.py,sha256=kOLrpUb5T5QN9ZvpGOjXyo7Kivrc14A9gcw9lvNMidI,15670
28
28
  atomicshop/process_name_cmd.py,sha256=TNAK6kQZm5JKWzEW6QLqVHEG98ZLNDQiSS4YwDk8V8c,3830
@@ -44,7 +44,7 @@ atomicshop/timer.py,sha256=KxBBgVM8po6pUJDW8TgY1UXj0iiDmRmL5XDCq0VHAfU,1670
44
44
  atomicshop/urls.py,sha256=CQl1j1kjEVDlAuYJqYD9XxPF1SUSgrmG8PjlcXNEKsQ,597
45
45
  atomicshop/uuids.py,sha256=JSQdm3ZTJiwPQ1gYe6kU0TKS_7suwVrHc8JZDGYlydM,2214
46
46
  atomicshop/virtualization.py,sha256=LPP4vjE0Vr10R6DA4lqhfX_WaNdDGRAZUW0Am6VeGco,494
47
- atomicshop/web.py,sha256=7WPV6Q4xZX7bByEeCl2VAfPlyMY42BpR_byVX0Gu7Js,11071
47
+ atomicshop/web.py,sha256=Ks_4F02MUqGvFRC-gs2H_NHk3jf1XIzrf_Q_NFb3re4,11116
48
48
  atomicshop/addons/PlayWrightCodegen.cmd,sha256=Z5cnllsyXD4F1W2h-WLEnyFkg5nZy0-hTGHRWXVOuW4,173
49
49
  atomicshop/addons/ScriptExecution.cmd,sha256=8iC-uHs9MX9qUD_C2M7n9Xw4MZvwOfxT8H5v3hluVps,93
50
50
  atomicshop/addons/a_setup_scripts/install_psycopg2_ubuntu.sh,sha256=lM7LkXQ2AxfFzDGyzSOfIS_zpg9bAD1k3JJ-qu5CdH8,81
@@ -52,11 +52,13 @@ atomicshop/addons/a_setup_scripts/install_pywintrace_0.3.cmd,sha256=lEP_o6rWcBFU
52
52
  atomicshop/addons/mains/install_docker_rootless_ubuntu.py,sha256=9IPNtGZYjfy1_n6ZRt7gWz9KZgR6XCgevjqq02xk-o0,281
53
53
  atomicshop/addons/mains/install_docker_ubuntu_main_sudo.py,sha256=JzayxeyKDtiuT4Icp2L2LyFRbx4wvpyN_bHLfZ-yX5E,281
54
54
  atomicshop/addons/mains/install_elastic_search_and_kibana_ubuntu.py,sha256=yRB-l1zBxdiN6av-FwNkhcBlaeu4zrDPjQ0uPGgpK2I,244
55
+ atomicshop/addons/mains/install_fibratus_windows.py,sha256=TU4e9gdZ_zI73C40uueJ59pD3qmN-UFGdX5GFoVf6cM,179
55
56
  atomicshop/addons/mains/install_wsl_ubuntu_lts_admin.py,sha256=PrvZ4hMuadzj2GYKRZSwyNayJUuaSnCF9nV6ORqoPdo,123
56
57
  atomicshop/addons/mains/msi_unpacker.py,sha256=XAJdEqs-3s9JqIgHpGRL-HxLKpFMXdrlXmq2Is2Pyfk,164
57
58
  atomicshop/addons/mains/search_for_hyperlinks_in_docx.py,sha256=HkIdo_Sz9nPbbbJf1mwfwFkyI7vkvpH8qiIkuYopN4w,529
58
59
  atomicshop/addons/mains/FACT/factw_fact_extractor_docker_image_main_sudo.py,sha256=DDKX3Wp2SmzMCEtCIEOUbEKMob2ZQ7VEQGLEf9uYXrs,320
59
60
  atomicshop/addons/mains/FACT/update_extract.py,sha256=H3VsdhlA7xxK5lI_nyrWUdk8GNZXbEUVR_K9cJ4ECAw,506
61
+ atomicshop/addons/mains/__pycache__/install_fibratus_windows.cpython-312.pyc,sha256=u92dFjDrTbZBIti9B3ttna33Jg1ZSeMYhTiupdfklt4,549
60
62
  atomicshop/addons/mains/inits/init_to_import_all_modules.py,sha256=piyFjkqtNbM9PT2p8aGcatI615517XEQHgU9kDFwseY,559
61
63
  atomicshop/addons/package_setup/CreateWheel.cmd,sha256=hq9aWBSH6iffYlZyaCNrFlA0vxMh3j1k8DQE8IARQuA,189
62
64
  atomicshop/addons/package_setup/Setup in Edit mode.cmd,sha256=299RsExjR8Mup6YyC6rW0qF8lnwa3uIzwk_gYg_R_Ss,176
@@ -102,7 +104,7 @@ atomicshop/etw/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
102
104
  atomicshop/etw/dns_trace.py,sha256=RaREpwJETAMZSd1Lhbg0sO3ugBMw3y1fSKdvP5NfTqM,5189
103
105
  atomicshop/etw/etw.py,sha256=xVJNbfCq4KgRfsDnul6CrIdAMl9xRBixZ-hUyqiB2g4,2403
104
106
  atomicshop/file_io/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
105
- atomicshop/file_io/csvs.py,sha256=4R4Kij8FmxNwXFjDtlF_A0flAk0Hj5nZKlEnqC5VxgQ,3125
107
+ atomicshop/file_io/csvs.py,sha256=FwLTHFdngcgEpf-qviDrHpt7qT_QWtNGAR_RKvYZlpI,4816
106
108
  atomicshop/file_io/docxs.py,sha256=6tcYFGp0vRsHR47VwcRqwhdt2DQOwrAUYhrwN996n9U,5117
107
109
  atomicshop/file_io/file_io.py,sha256=FR84ihjGlr7Eqejo-_js4nBICVst31axD0bwX19S2eM,6385
108
110
  atomicshop/file_io/jsons.py,sha256=q9ZU8slBKnHLrtn3TnbK1qxrRpj5ZvCm6AlsFzoANjo,5303
@@ -116,7 +118,7 @@ atomicshop/mitm/initialize_engines.py,sha256=UGdT5DKYNri3MNOxESP7oeSxYiUDrVilJ4j
116
118
  atomicshop/mitm/initialize_mitm_server.py,sha256=aXNZlRu1_RGjC7lagvs2Q8rjQiygxYucy-U4C_SBnsk,13871
117
119
  atomicshop/mitm/message.py,sha256=u2U2f2SOHdBNU-6r1Ik2W14ai2EOwxUV4wVfGZA098k,1732
118
120
  atomicshop/mitm/shared_functions.py,sha256=PaK_sbnEA5zo9k2ktEOKLmvo-6wRUunxzSNRr41uXIQ,1924
119
- atomicshop/mitm/statistic_analyzer.py,sha256=1g5l6X-NbnHvh_TREJRumTDWgE4ixUNJ8pKGneKcf4Y,23524
121
+ atomicshop/mitm/statistic_analyzer.py,sha256=K6HN7iKMthpEZYmVS1aa0jpW2g5Owq4Jl-mZIQzxWYo,23542
120
122
  atomicshop/mitm/engines/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
121
123
  atomicshop/mitm/engines/create_module_template.py,sha256=tRjVSm1sD6FzML71Qbuwvita0qsusdFGm8NZLsZ-XMs,4853
122
124
  atomicshop/mitm/engines/create_module_template_example.py,sha256=X5xhvbV6-g9jU_bQVhf_crZmaH50LRWz3bS-faQ18ds,489
@@ -147,7 +149,8 @@ atomicshop/wrappers/astw.py,sha256=VkYfkfyc_PJLIOxByT6L7B8uUmKY6-I8XGZl4t_z828,4
147
149
  atomicshop/wrappers/configparserw.py,sha256=JwDTPjZoSrv44YKwIRcjyUnpN-FjgXVfMqMK_tJuSgU,22800
148
150
  atomicshop/wrappers/cryptographyw.py,sha256=H5NaHHDkr97QYhUrHFO9vY218u8k3N3Zgh6bQRnicUE,13140
149
151
  atomicshop/wrappers/ffmpegw.py,sha256=wcq0ZnAe0yajBOuTKZCCaKI7CDBjkq7FAgdW5IsKcVE,6031
150
- atomicshop/wrappers/githubw.py,sha256=mQGtj6up1HIvjOD2t0bmOWjLooJLYvuIa7d7H-tknrw,9998
152
+ atomicshop/wrappers/githubw.py,sha256=AQcFuT5mvDUNT_cI31MwkJ7srdhMtttF8FyXS8vs5cU,12270
153
+ atomicshop/wrappers/msiw.py,sha256=1kEs9T3eJYGzEpEBXaq8OZxlCnV4gnIR4zhmCsQiKvY,2201
151
154
  atomicshop/wrappers/numpyw.py,sha256=sBV4gSKyr23kXTalqAb1oqttzE_2XxBooCui66jbAqc,1025
152
155
  atomicshop/wrappers/olefilew.py,sha256=biD5m58rogifCYmYhJBrAFb9O_Bn_spLek_9HofLeYE,2051
153
156
  atomicshop/wrappers/pipw.py,sha256=mu4jnHkSaYNfpBiLZKMZxEX_E2LqW5BVthMZkblPB_c,1317
@@ -193,23 +196,25 @@ atomicshop/wrappers/factw/install/pre_install_and_install_before_restart.py,sha2
193
196
  atomicshop/wrappers/factw/postgresql/__init__.py,sha256=xMBn2d3Exo23IPP2F_9-SXmOlhFbwWDgS9KwozSTjA0,162
194
197
  atomicshop/wrappers/factw/postgresql/analysis.py,sha256=2Rxzy2jyq3zEKIo53z8VkjuslKE_i5mq2ZpmJAvyd6U,716
195
198
  atomicshop/wrappers/factw/postgresql/file_object.py,sha256=VRiCXnsd6yDbnsE-TEKYPC-gkAgFVkE6rygRrJLQShI,713
196
- atomicshop/wrappers/factw/postgresql/firmware.py,sha256=_fiRv8biUxazfiCFXpqYDUgZHSNe4cgtZAKO69nIZ4M,10759
199
+ atomicshop/wrappers/factw/postgresql/firmware.py,sha256=wnohSnSOCmlTUCzzHIIPGkRrnownlGKgIFyhhdhNEoA,10759
197
200
  atomicshop/wrappers/factw/postgresql/fw_files.py,sha256=P1jq4AAZa7fygWdEZtFJOnfz4tyqmPpvFzEMDKrCRkU,1291
198
201
  atomicshop/wrappers/factw/postgresql/included_files.py,sha256=sn5YhLkrsvjhrVSA8O8YUNfbqR9STprSuQGEnHsK0jE,1025
199
202
  atomicshop/wrappers/factw/postgresql/virtual_file_path.py,sha256=iR68A_My_ohgRcYdueMaQF9EHOgBRN3bIi8Nq59g3kc,1098
200
203
  atomicshop/wrappers/factw/rest/__init__.py,sha256=MuzZDJ38myxmwLhNhHIsDk0DXkcNbsB_t4R4SSYl--Y,150
201
204
  atomicshop/wrappers/factw/rest/binary_search.py,sha256=AXMFTma3awymrSlE8T1MSV8Q-PCqk586WBDlBr4TbR4,826
202
205
  atomicshop/wrappers/factw/rest/file_object.py,sha256=E_CA9lYpUqpxPDJ8c9dAqQAkJq8NafTecKa3q3EKr40,3218
203
- atomicshop/wrappers/factw/rest/firmware.py,sha256=FezneouU1lUO9uZ6_8ZQNxr4MDlFIoTbBgjIZiNo3_k,20387
206
+ atomicshop/wrappers/factw/rest/firmware.py,sha256=MEdrupbZbjsAsCpTlSast2Y610WbXWCtuDT75rr0g3g,20387
204
207
  atomicshop/wrappers/factw/rest/router.py,sha256=fdGok5ESBxcZHIBgM93l4yTPRGoeooQNsrPWIETieGk,710
205
208
  atomicshop/wrappers/factw/rest/statistics.py,sha256=vznwzKP1gEF7uXz3HsuV66BU9wrp73N_eFqpFpye9Qw,653
206
209
  atomicshop/wrappers/factw/rest/status.py,sha256=4O3xS1poafwyUiLDkhyx4oMMe4PBwABuRPpOMnMKgIU,641
210
+ atomicshop/wrappers/fibratusw/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
211
+ atomicshop/wrappers/fibratusw/install.py,sha256=PLVymDe0HuOvU0r2lje8BkQAgtiOWEeRO7n-1zKuL7A,3287
207
212
  atomicshop/wrappers/loggingw/checks.py,sha256=AGFsTsLxHQd1yAraa5popqLaGO9VM0KpcPGuSLn5ptU,719
208
213
  atomicshop/wrappers/loggingw/formatters.py,sha256=mUtcJJfmhLNrwUVYShXTmdu40dBaJu4TS8FiuTXI7ys,7189
209
214
  atomicshop/wrappers/loggingw/handlers.py,sha256=qm5Fbu8eDmlstMduUe5nKUlJU5IazFkSnQizz8Qt2os,5479
210
215
  atomicshop/wrappers/loggingw/loggers.py,sha256=DHOOTAtqkwn1xgvLHSkOiBm6yFGNuQy1kvbhG-TDog8,2374
211
216
  atomicshop/wrappers/loggingw/loggingw.py,sha256=v9WAseZXB50LluT9rIUcRvvevg2nLVKPgz3dbGejfV0,12151
212
- atomicshop/wrappers/loggingw/reading.py,sha256=xs7L6Jo-vedrhCVP7m-cJo0VhWmoSoK86avR4Tm0kG4,3675
217
+ atomicshop/wrappers/loggingw/reading.py,sha256=XKQVggjleXqS-sjY8q7o_xzMBhWDdJO0A1d4DDE2rDA,7183
213
218
  atomicshop/wrappers/nodejsw/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
214
219
  atomicshop/wrappers/nodejsw/install_nodejs.py,sha256=QZg-R2iTQt7kFb8wNtnTmwraSGwvUs34JIasdbNa7ZU,5154
215
220
  atomicshop/wrappers/playwrightw/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -244,8 +249,8 @@ atomicshop/wrappers/socketw/socket_server_tester.py,sha256=AhpurHJmP2kgzHaUbq5ey
244
249
  atomicshop/wrappers/socketw/socket_wrapper.py,sha256=aXBwlEIJhFT0-c4i8iNlFx2It9VpCEpsv--5Oqcpxao,11624
245
250
  atomicshop/wrappers/socketw/ssl_base.py,sha256=k4V3gwkbq10MvOH4btU4onLX2GNOsSfUAdcHmL1rpVE,2274
246
251
  atomicshop/wrappers/socketw/statistics_csv.py,sha256=t3dtDEfN47CfYVi0CW6Kc2QHTEeZVyYhc57IYYh5nmA,826
247
- atomicshop-2.11.49.dist-info/LICENSE.txt,sha256=lLU7EYycfYcK2NR_1gfnhnRC8b8ccOTElACYplgZN88,1094
248
- atomicshop-2.11.49.dist-info/METADATA,sha256=2Q8mFvwf5Wx4Hyhu2Ngv8Hr2U4hfvkBFGiNpaVdSkg4,10448
249
- atomicshop-2.11.49.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
250
- atomicshop-2.11.49.dist-info/top_level.txt,sha256=EgKJB-7xcrAPeqTRF2laD_Np2gNGYkJkd4OyXqpJphA,11
251
- atomicshop-2.11.49.dist-info/RECORD,,
252
+ atomicshop-2.12.1.dist-info/LICENSE.txt,sha256=lLU7EYycfYcK2NR_1gfnhnRC8b8ccOTElACYplgZN88,1094
253
+ atomicshop-2.12.1.dist-info/METADATA,sha256=TaPxIyYLWTGyaS4RhB9JdRuElraHbe0umXI6faSh9lE,10447
254
+ atomicshop-2.12.1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
255
+ atomicshop-2.12.1.dist-info/top_level.txt,sha256=EgKJB-7xcrAPeqTRF2laD_Np2gNGYkJkd4OyXqpJphA,11
256
+ atomicshop-2.12.1.dist-info/RECORD,,