atomicshop 2.16.42__py3-none-any.whl → 2.16.44__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.42'
4
+ __version__ = '2.16.44'
@@ -53,6 +53,30 @@ def convert_sequence_of_bytes_to_sequence_of_strings(byte_sequence: bytes) -> li
53
53
  return result
54
54
 
55
55
 
56
+ def convert_sequence_of_bytes_to_exact_string(
57
+ byte_sequence: bytes,
58
+ add_space_between_bytes: bool = False,
59
+ ) -> str:
60
+ """
61
+ Convert sequence of bytes to exact string.
62
+ Example: b'\xc0\x00' -> 'c000'
63
+
64
+ :param byte_sequence: bytes, sequence of bytes.
65
+ :param add_space_between_bytes: bool, add space between bytes.
66
+ Example: b'\xc0\x00' -> 'c0 00'
67
+ :return: string.
68
+ """
69
+
70
+ # Convert to hex string and format
71
+ byte_list: list = []
72
+ for byte in byte_sequence:
73
+ byte_list.append(f'{byte:02x}')
74
+
75
+ result = ''.join(byte_list)
76
+
77
+ return result
78
+
79
+
56
80
  def find_position(target: bytes, file_path: str = None, file_bytes: bytes = None, chunk_size: int = None, starting_position: int = 0) -> int:
57
81
  """
58
82
  Find position of the target bytes string in the file.
atomicshop/dns.py CHANGED
@@ -77,6 +77,17 @@ def get_default_dns_gateway() -> tuple[bool, list[str]]:
77
77
  return is_dynamic, dns_servers
78
78
 
79
79
 
80
+ def get_default_dns_gateway_with_dns_resolver() -> list[str]:
81
+ """
82
+ Get the default DNS gateway from the system using dns.resolver.
83
+ :return: tuple(is dynamic boolean, list of DNS server IPv4s).
84
+ """
85
+
86
+ resolver = dns.resolver.Resolver()
87
+ dns_servers = list(resolver.nameservers)
88
+ return dns_servers
89
+
90
+
80
91
  def set_connection_dns_gateway_static(
81
92
  dns_servers: list[str],
82
93
  connection_name: str = None,
@@ -75,7 +75,7 @@ def search_for_hyperlink_in_files(directory_path: str, hyperlink: str, relative_
75
75
 
76
76
  # Get all the docx files in the specified directory.
77
77
  files = filesystem.get_paths_from_directory(
78
- directory_path, get_file=True, file_name_check_pattern="*\.docx",
78
+ directory_path, get_file=True, file_name_check_pattern="*.docx",
79
79
  add_relative_directory=True, relative_file_name_as_directory=True)
80
80
 
81
81
  found_in_files: list = list()
atomicshop/http_parse.py CHANGED
@@ -2,7 +2,6 @@ from http.server import BaseHTTPRequestHandler
2
2
  from http.client import HTTPResponse
3
3
  import http
4
4
  from io import BytesIO
5
- import socket
6
5
 
7
6
 
8
7
  class HTTPRequestParse(BaseHTTPRequestHandler):
@@ -50,8 +49,8 @@ class HTTPRequestParse(BaseHTTPRequestHandler):
50
49
  """
51
50
 
52
51
  # noinspection PyMissingConstructor
53
- def __init__(self, request_text):
54
- self.request_text = request_text
52
+ def __init__(self, request_bytes: bytes):
53
+ self.request_bytes: bytes = request_bytes
55
54
 
56
55
  # noinspection PyTypeChecker
57
56
  self.rfile = None
@@ -107,7 +106,7 @@ class HTTPRequestParse(BaseHTTPRequestHandler):
107
106
  error: str = str()
108
107
  info: str = str()
109
108
 
110
- self.rfile = BytesIO(self.request_text)
109
+ self.rfile = BytesIO(self.request_bytes)
111
110
  self.raw_requestline = self.rfile.readline()
112
111
  self.error_code = self.error_message = None
113
112
  self.parse_request()
@@ -152,6 +151,7 @@ class FakeSocket:
152
151
  def __init__(self, response_bytes):
153
152
  self._file = BytesIO(response_bytes)
154
153
 
154
+ # noinspection PyUnusedLocal
155
155
  def makefile(self, mode='rb', buffering=-1) -> BytesIO:
156
156
  """
157
157
  Mimics the socket's makefile method, returning the BytesIO object.
@@ -100,15 +100,12 @@ def thread_worker_main(
100
100
  client_message.protocol = 'HTTP'
101
101
  client_message.request_raw_decoded = request_decoded
102
102
  print_api(request_parsing_info, logger=network_logger, logger_method='info')
103
- network_logger.info(f"Method: {request_decoded.command}")
104
- network_logger.info(f"Path: {request_decoded.path}")
105
- # If there was error - the request is really HTTP, but there's a problem with its structure.
103
+ network_logger.info(f"Method: {request_decoded.command} | Path: {request_decoded.path}")
106
104
  else:
107
- # client_message.error = request_parsing_error
108
- print_api(request_parsing_error, logger=network_logger, logger_method='error', color='yellow')
109
105
  # It doesn't matter if we have HTTP Parsing error, since the request may not be really HTTP, so it is OK
110
106
  # not to log it into statistics.
111
107
  # statistics_error_list.append(error_message)
108
+ print_api(request_parsing_error, logger=network_logger, logger_method='error', color='yellow')
112
109
 
113
110
  def finish_thread():
114
111
  # At this stage there could be several times that the same socket was used to the service server - we need to
@@ -204,8 +201,6 @@ def thread_worker_main(
204
201
  client_message.request_raw_bytes = client_received_raw_data
205
202
 
206
203
  parse_http()
207
- if client_message.protocol != 'HTTP':
208
- pass
209
204
 
210
205
  # Custom parser, should parse HTTP body or the whole message if not HTTP.
211
206
  parser_instance = parser(client_message)
@@ -19,6 +19,7 @@ from . import config_static, recs_files
19
19
 
20
20
  NETWORK_INTERFACE_IS_DYNAMIC: bool = bool()
21
21
  NETWORK_INTERFACE_IPV4_ADDRESS_LIST: list[str] = list()
22
+ IS_SET_DNS_GATEWAY: bool = False
22
23
  # noinspection PyTypeChecker
23
24
  RECS_PROCESS_INSTANCE: multiprocessing.Process = None
24
25
 
@@ -36,7 +37,7 @@ except win_console.NotWindowsConsoleError:
36
37
 
37
38
 
38
39
  def exit_cleanup():
39
- if permissions.is_admin():
40
+ if permissions.is_admin() and IS_SET_DNS_GATEWAY:
40
41
  is_dns_dynamic, current_dns_gateway = dns.get_default_dns_gateway()
41
42
  status_string = 'Dynamic' if is_dns_dynamic else 'Static'
42
43
  print_api.print_api(f'Current DNS Gateway: {status_string}, {current_dns_gateway}')
@@ -347,10 +348,14 @@ def mitm_server(config_file_path: str):
347
348
  dns_gateway_server_list = [base.DEFAULT_IPV4]
348
349
  set_dns_gateway = True
349
350
 
350
- # Get current network interface state.
351
- global NETWORK_INTERFACE_IS_DYNAMIC, NETWORK_INTERFACE_IPV4_ADDRESS_LIST
352
- NETWORK_INTERFACE_IS_DYNAMIC, NETWORK_INTERFACE_IPV4_ADDRESS_LIST = dns.get_default_dns_gateway()
353
351
  if set_dns_gateway:
352
+ global IS_SET_DNS_GATEWAY
353
+ IS_SET_DNS_GATEWAY = True
354
+
355
+ # Get current network interface state.
356
+ global NETWORK_INTERFACE_IS_DYNAMIC, NETWORK_INTERFACE_IPV4_ADDRESS_LIST
357
+ NETWORK_INTERFACE_IS_DYNAMIC, NETWORK_INTERFACE_IPV4_ADDRESS_LIST = dns.get_default_dns_gateway()
358
+
354
359
  # Set the DNS gateway to the specified one only if the DNS gateway is dynamic, or it is static but different
355
360
  # from the one specified in the configuration file.
356
361
  if (NETWORK_INTERFACE_IS_DYNAMIC or (not NETWORK_INTERFACE_IS_DYNAMIC and
atomicshop/web.py CHANGED
@@ -90,10 +90,13 @@ def get_page_bytes(
90
90
 
91
91
 
92
92
  def get_page_content(
93
- url: str, get_method: str = 'urllib', path: str = None,
93
+ url: str,
94
+ get_method: str = 'urllib',
95
+ path: str = None,
94
96
  playwright_pdf_format: str = 'A4',
95
97
  playwright_html_txt_convert_to_bytes: bool = True,
96
- print_kwargs: dict = None) -> any:
98
+ print_kwargs: dict = None
99
+ ) -> any:
97
100
  """
98
101
  Function returns the page content from the given URL.
99
102
 
@@ -0,0 +1,415 @@
1
+ from typing import Union, Generator
2
+ import logging
3
+
4
+ from websockets.server import ServerProtocol
5
+ from websockets.client import ClientProtocol
6
+ from websockets.extensions.permessage_deflate import PerMessageDeflate, ServerPerMessageDeflateFactory, ClientPerMessageDeflateFactory
7
+ from websockets.http11 import Request, Response
8
+ from websockets.frames import Frame, Opcode
9
+ from websockets.uri import parse_uri
10
+ from websockets.exceptions import InvalidHeaderValue
11
+ from websockets.protocol import OPEN
12
+ from websockets.streams import StreamReader
13
+ from websockets.exceptions import ProtocolError, PayloadTooBig
14
+
15
+
16
+ class WebsocketParseWrongOpcode(Exception):
17
+ pass
18
+
19
+
20
+ def create_byte_http_response(
21
+ byte_http_request: Union[bytes, bytearray],
22
+ enable_logging: bool = False
23
+ ) -> bytes:
24
+ """
25
+ Create a byte HTTP response from a byte HTTP request.
26
+
27
+ Parameters:
28
+ - byte_http_request (bytes, bytearray): The byte HTTP request.
29
+ - enable_logging (bool): Whether to enable logging.
30
+
31
+ Returns:
32
+ - bytes: The byte HTTP response.
33
+ """
34
+
35
+ # Set up extensions
36
+ permessage_deflate_factory = ServerPerMessageDeflateFactory()
37
+
38
+ # Create the protocol instance
39
+ protocol = ServerProtocol(
40
+ extensions=[permessage_deflate_factory],
41
+ )
42
+ # At this state the protocol.state is State.CONNECTING
43
+
44
+ if enable_logging:
45
+ logging.basicConfig(level=logging.DEBUG)
46
+ protocol.logger.setLevel(logging.DEBUG)
47
+
48
+
49
+ protocol.receive_data(byte_http_request)
50
+ events = protocol.events_received()
51
+ event = events[0]
52
+ if isinstance(event, Request):
53
+ # Accept the handshake.
54
+ # After the response is sent, it means the handshake was successful, the protocol.state is State.OPEN
55
+ # Only after this state we can parse frames.
56
+ response = protocol.accept(event)
57
+ return response.serialize()
58
+ else:
59
+ raise ValueError("The event is not a Request object.")
60
+
61
+
62
+ def parse_frame_bytes(data_bytes: bytes):
63
+ # Define the read_exact function
64
+ def read_exact(n: int) -> Generator[None, None, bytes]:
65
+ return reader.read_exact(n)
66
+
67
+ # Helper function to run generator-based coroutines
68
+ def run_coroutine(coroutine):
69
+ try:
70
+ while True:
71
+ next(coroutine)
72
+ except StopIteration as e:
73
+ return e.value
74
+ except Exception as e:
75
+ raise e # Re-raise exceptions to be handled by the caller
76
+
77
+ # Function to parse frames
78
+ def parse_frame(mask: bool):
79
+ try:
80
+ # Use Frame.parse to parse the frame
81
+ frame_parser = Frame.parse(
82
+ read_exact,
83
+ mask=mask, # Client frames are masked
84
+ max_size=None,
85
+ extensions=[permessage_deflate], # Include the extension unconditionally
86
+ )
87
+ current_frame = run_coroutine(frame_parser)
88
+ except EOFError as e:
89
+ # Not enough data to parse a complete frame
90
+ raise e
91
+ except (ProtocolError, PayloadTooBig) as e:
92
+ print("Error parsing frame:", e)
93
+ raise e
94
+ except Exception as e:
95
+ print("Error parsing frame:", e)
96
+ raise e
97
+ return current_frame
98
+
99
+ def process_frame(current_frame):
100
+ if current_frame.opcode == Opcode.TEXT:
101
+ message = current_frame.data.decode('utf-8', errors='replace')
102
+ return message
103
+ elif current_frame.opcode == Opcode.BINARY:
104
+ return current_frame.data
105
+ elif current_frame.opcode == Opcode.CLOSE:
106
+ print("Received close frame")
107
+ elif current_frame.opcode == Opcode.PING:
108
+ print("Received ping")
109
+ elif current_frame.opcode == Opcode.PONG:
110
+ print("Received pong")
111
+ else:
112
+ raise WebsocketParseWrongOpcode("Received unknown frame with opcode:", current_frame.opcode)
113
+
114
+ # Create the StreamReader instance
115
+ reader = StreamReader()
116
+
117
+ # Instantiate the permessage-deflate extension
118
+ permessage_deflate = PerMessageDeflate(
119
+ remote_no_context_takeover=False,
120
+ local_no_context_takeover=False,
121
+ remote_max_window_bits=15,
122
+ local_max_window_bits=15,
123
+ )
124
+
125
+ masked = is_frame_masked(data_bytes)
126
+
127
+ # Feed the data into the reader
128
+ reader.feed_data(data_bytes)
129
+
130
+ # Parse and process frames
131
+ frame = parse_frame(masked)
132
+ result = process_frame(frame)
133
+ return result
134
+
135
+
136
+ def create_websocket_frame(
137
+ data: Union[str, bytes, bytearray],
138
+ deflate: bool = False,
139
+ mask: bool = False,
140
+ opcode: int = None
141
+ ) -> bytes:
142
+ """
143
+ Create a WebSocket frame with the given data, optionally applying
144
+ permessage-deflate compression and masking.
145
+
146
+ Parameters:
147
+ - data (str, bytes, bytearray): The payload data.
148
+ If str, it will be encoded to bytes using UTF-8.
149
+ - deflate (bool): Whether to apply permessage-deflate compression.
150
+ - mask (bool): Whether to apply masking to the frame.
151
+ - opcode (int): The opcode of the frame. If not provided, it will be
152
+ determined based on the type of data.
153
+ Example:
154
+ from websockets.frames import Opcode
155
+ Opcode.TEXT, Opcode.BINARY, Opcode.CLOSE, Opcode.PING, Opcode.PONG.
156
+
157
+ Returns:
158
+ - bytes: The serialized WebSocket frame ready to be sent.
159
+ """
160
+
161
+ # Determine the opcode if not provided
162
+ if opcode is None:
163
+ if isinstance(data, str):
164
+ opcode = Opcode.TEXT
165
+ elif isinstance(data, (bytes, bytearray)):
166
+ opcode = Opcode.BINARY
167
+ else:
168
+ raise TypeError("Data must be of type str, bytes, or bytearray.")
169
+ else:
170
+ if not isinstance(opcode, int):
171
+ raise TypeError("Opcode must be an integer.")
172
+ if not isinstance(data, (str, bytes, bytearray)):
173
+ raise TypeError("Data must be of type str, bytes, or bytearray.")
174
+
175
+ # Encode string data if necessary
176
+ if isinstance(data, str):
177
+ payload = data.encode('utf-8')
178
+ else:
179
+ payload = bytes(data)
180
+
181
+ # Create the Frame instance
182
+ frame = Frame(opcode=opcode, data=payload)
183
+
184
+ # Set up extensions if deflate is True
185
+ extensions = []
186
+ if deflate:
187
+ permessage_deflate = PerMessageDeflate(
188
+ remote_no_context_takeover=False,
189
+ local_no_context_takeover=False,
190
+ remote_max_window_bits=15,
191
+ local_max_window_bits=15,
192
+ )
193
+ extensions.append(permessage_deflate)
194
+
195
+ # Serialize the frame with the specified options
196
+ try:
197
+ frame_bytes = frame.serialize(
198
+ mask=mask,
199
+ extensions=extensions,
200
+ )
201
+ except Exception as e:
202
+ raise RuntimeError(f"Error serializing frame: {e}")
203
+
204
+ return frame_bytes
205
+
206
+
207
+ def is_frame_masked(frame_bytes):
208
+ """
209
+ Determine whether a WebSocket frame is masked.
210
+
211
+ Parameters:
212
+ - frame_bytes (bytes): The raw bytes of the WebSocket frame.
213
+
214
+ Returns:
215
+ - bool: True if the frame is masked, False otherwise.
216
+ """
217
+ if len(frame_bytes) < 2:
218
+ raise ValueError("Frame is too short to determine masking.")
219
+
220
+ # The second byte of the frame header contains the MASK bit
221
+ second_byte = frame_bytes[1]
222
+
223
+ # The MASK bit is the most significant bit (MSB) of the second byte
224
+ mask_bit = (second_byte & 0x80) != 0 # 0x80 is 1000 0000 in binary
225
+
226
+ return mask_bit
227
+
228
+
229
+ def is_frame_deflated(frame_bytes):
230
+ """
231
+ Determine whether a WebSocket frame is deflated (compressed).
232
+
233
+ Parameters:
234
+ - frame_bytes (bytes): The raw bytes of the WebSocket frame.
235
+
236
+ Returns:
237
+ - bool: True if the frame is deflated (compressed), False otherwise.
238
+ """
239
+ if len(frame_bytes) < 1:
240
+ raise ValueError("Frame is too short to determine deflation status.")
241
+
242
+ # The first byte of the frame header contains the RSV1 bit
243
+ first_byte = frame_bytes[0]
244
+
245
+ # The RSV1 bit is the second most significant bit (bit 6)
246
+ rsv1 = (first_byte & 0x40) != 0 # 0x40 is 0100 0000 in binary
247
+
248
+ return rsv1
249
+
250
+
251
+ class _WebsocketRequestParse:
252
+ """
253
+ THIS IS ONLY FOR THE REFERENCE IT IS NOT CURRENTLY USED OR SHOULD BE USED.
254
+ Parse the websocket request and return the data
255
+ """
256
+ def __init__(
257
+ self,
258
+ enable_logging: bool = False,
259
+ ):
260
+ """
261
+ Initialize the websocket parser.
262
+
263
+ :param enable_logging: bool: Enable logging for the websocket protocol.
264
+ """
265
+ # noinspection PyTypeChecker
266
+ self.request_bytes: bytes = None
267
+
268
+ # Set up extensions
269
+ permessage_deflate_factory = ServerPerMessageDeflateFactory()
270
+
271
+ # Create the protocol instance
272
+ self.protocol = ServerProtocol(
273
+ extensions=[permessage_deflate_factory],
274
+ )
275
+ # At this state the protocol.state is State.CONNECTING
276
+
277
+ if enable_logging:
278
+ logging.basicConfig(level=logging.DEBUG)
279
+ self.protocol.logger.setLevel(logging.DEBUG)
280
+
281
+ def parse(
282
+ self,
283
+ request_bytes: bytes
284
+ ) -> Union[str, bytes, Request]:
285
+ """
286
+ Parse the websocket request and return the data
287
+
288
+ :param request_bytes: bytes: The raw bytes of the websocket request.
289
+ :return: Request: The parsed request object.
290
+ """
291
+
292
+ self.protocol.receive_data(request_bytes)
293
+ events = self.protocol.events_received()
294
+ for event in events:
295
+ if isinstance(event, Request):
296
+ # Accept the handshake.
297
+ # After the response is sent, it means the handshake was successful, the protocol.state is State.OPEN
298
+ # Only after this state we can parse frames.
299
+ response = self.protocol.accept(event)
300
+ self.protocol.send_response(response)
301
+ return event
302
+ elif isinstance(event, Frame):
303
+ frame = event
304
+ if frame.opcode == Opcode.TEXT:
305
+ message = frame.data.decode('utf-8')
306
+ return message
307
+ elif frame.opcode == Opcode.BINARY:
308
+ return frame.data
309
+
310
+ """
311
+ # Handle control frames, these are here for the future references.
312
+ elif frame.opcode == Opcode.CLOSE:
313
+ close_info = Close.parse(frame.data)
314
+ print(f"Connection closed by client: {close_info.code}, {close_info.reason}")
315
+ # Send a close frame in response if not already sent
316
+ if self.protocol.state == self.protocol.OPEN:
317
+ self.protocol.send_close()
318
+ elif frame.opcode == Opcode.PING:
319
+ # Respond to ping with pong
320
+ self.protocol.send_pong(frame.data)
321
+ elif frame.opcode == Opcode.PONG:
322
+ print("Received pong")
323
+ """
324
+
325
+
326
+ class _WebsocketResponseParse:
327
+ """
328
+ THIS IS ONLY FOR THE REFERENCE IT IS NOT CURRENTLY USED OR SHOULD BE USED.
329
+ Parse the websocket response and return the data
330
+ """
331
+ def __init__(
332
+ self,
333
+ enable_logging: bool = False,
334
+ ):
335
+ """
336
+ Initialize the websocket parser.
337
+
338
+ :param enable_logging: bool: Enable logging for the websocket protocol.
339
+ """
340
+ # noinspection PyTypeChecker
341
+ self.response_bytes: bytes = None
342
+
343
+ # Set up extensions
344
+ permessage_deflate_factory = ClientPerMessageDeflateFactory()
345
+
346
+ # Parse the WebSocket URI.
347
+ # Since we're parsing the response, we don't need the URI, but the protocol object requires it.
348
+ # So we will just use a dummy URI.
349
+ wsuri = parse_uri('ws://example.com/websocket')
350
+
351
+ # Create the protocol instance
352
+ self.protocol = ClientProtocol(
353
+ wsuri,
354
+ extensions=[permessage_deflate_factory],
355
+ )
356
+
357
+ if enable_logging:
358
+ logging.basicConfig(level=logging.DEBUG)
359
+ # self.protocol.logger.setLevel(logging.DEBUG)
360
+ self.protocol.debug = True
361
+
362
+ # Perform the handshake and emulate the connection and request sending.
363
+ request = self.protocol.connect()
364
+ self.protocol.send_request(request)
365
+ _ = self.protocol.data_to_send()
366
+ # At this state the protocol.state is State.CONNECTING
367
+
368
+ def parse(
369
+ self,
370
+ response_bytes: bytes
371
+ ) -> Union[str, bytes, Response]:
372
+ """
373
+ Parse the websocket response and return the data
374
+
375
+ :param response_bytes: bytes: The raw bytes of the websocket response.
376
+ :return: The parsed response.
377
+ """
378
+
379
+ self.protocol.receive_data(response_bytes)
380
+ events = self.protocol.events_received()
381
+ for event in events:
382
+ if isinstance(event, Response):
383
+ # Accept the handshake.
384
+ # After the response is sent, it means the handshake was successful, the protocol.state is State.OPEN
385
+ # Only after this state we can parse frames.
386
+ try:
387
+ self.protocol.process_response(event)
388
+ except InvalidHeaderValue as e:
389
+ headers = event.headers
390
+ self.protocol.extensions = self.protocol.process_extensions(headers)
391
+ self.protocol.subprotocol = self.protocol.process_subprotocol(headers)
392
+ self.protocol.state = OPEN
393
+ return event
394
+ elif isinstance(event, Frame):
395
+ frame = event
396
+ if frame.opcode == Opcode.TEXT:
397
+ message = frame.data.decode('utf-8')
398
+ return message
399
+ elif frame.opcode == Opcode.BINARY:
400
+ return frame.data
401
+
402
+ """
403
+ # Handle control frames, these are here for the future references.
404
+ elif frame.opcode == Opcode.CLOSE:
405
+ close_info = Close.parse(frame.data)
406
+ print(f"Connection closed by client: {close_info.code}, {close_info.reason}")
407
+ # Send a close frame in response if not already sent
408
+ if self.protocol.state == self.protocol.OPEN:
409
+ self.protocol.send_close()
410
+ elif frame.opcode == Opcode.PING:
411
+ # Respond to ping with pong
412
+ self.protocol.send_pong(frame.data)
413
+ elif frame.opcode == Opcode.PONG:
414
+ print("Received pong")
415
+ """
@@ -1,14 +1,48 @@
1
1
  from typing import Union, Literal
2
2
  from pathlib import Path
3
3
 
4
- from .... import process, filesystem
4
+ from .... import process, print_api
5
5
  from .. import config_install
6
6
 
7
7
 
8
+ PLUGIN_LIST: list = [
9
+ 'qemu_exec',
10
+ 'binwalk',
11
+ 'users_and_passwords',
12
+ 'kernel_config',
13
+ 'cve_lookup',
14
+ 'crypto_hints',
15
+ 'input_vectors',
16
+ 'cwe_checker',
17
+ 'linter',
18
+ 'ip_and_uri_finder',
19
+ 'device_tree',
20
+ 'file_system_metadata',
21
+ 'ipc',
22
+ 'software_components',
23
+ 'architecture_detection',
24
+ 'known_vulnerabilities'
25
+ ]
26
+
27
+
28
+ INSTALLING_STRINGS: list = ['Installing', 'plugin']
29
+ FINISHED_INSTALLING_STRINGS: list = ['Finished installing', 'plugin']
30
+ LOG_FINISHED_STRING: str = 'installation complete'
31
+
32
+
8
33
  def install_after_restart(
9
34
  installation_directory: str,
10
- install_type: Union[None, Literal['backend', 'frontend', 'db']] = None
11
- ):
35
+ install_type: Union[
36
+ None,
37
+ Literal['backend', 'frontend', 'db']] = None,
38
+ log_level: Union[
39
+ None,
40
+ Literal['DEBUG', 'INFO', 'WARNING', 'ERROR']] = None,
41
+ log_file: Union[
42
+ None,
43
+ str] = None,
44
+ analyze_log: bool = False
45
+ ) -> int:
12
46
  """
13
47
  This function will continue the installation the FACT_core after the restart of the computer.
14
48
 
@@ -20,7 +54,13 @@ def install_after_restart(
20
54
  --backend: Distributed setup, Install the FACT_core backend.
21
55
  --frontend: Distributed setup, Install the FACT_core frontend.
22
56
  --db: Distributed setup, Install the FACT_core database.
23
- :return:
57
+ :param log_level: string, the log level to use for the installation.
58
+ The same as using the '--log-level' parameter in the 'install.py' script.
59
+ The default is 'INFO' in the 'install.py' script.
60
+ :param log_file: string, the log file to use for the installation.
61
+ The same as using the '--log-file' parameter in the 'install.py' script.
62
+ :param analyze_log: bool, if True, the log file will be analyzed for plugin installation errors.
63
+ :return: int, 0 if the installation was successful, otherwise 1.
24
64
  """
25
65
 
26
66
  install_command: str = 'python3 "' + str(Path(installation_directory, config_install.INSTALL_FILE_PATH)) + '"'
@@ -28,8 +68,52 @@ def install_after_restart(
28
68
  if install_type:
29
69
  install_command = install_command + ' --' + install_type
30
70
 
71
+ if log_level:
72
+ install_command = install_command + ' --log_level ' + log_level
73
+
74
+ if log_file:
75
+ install_command = install_command + ' --log_file "' + log_file + '"'
76
+
31
77
  # Install the FACT_core repo.
32
78
  process.execute_with_live_output(cmd=install_command, verbose=True)
79
+
80
+ # Analyze the log file for errors.
81
+ if analyze_log and (install_type == 'backend' or install_type is None):
82
+ if not log_file:
83
+ log_file = str(Path.cwd() / config_install.INSTALL_LOG_FILE_NAME)
84
+
85
+ return analyze_log_file(log_file=log_file)
86
+
33
87
  # Remove the FACT_core installation log.
34
- working_directory_path: str = filesystem.get_working_directory()
88
+ # working_directory_path: str = filesystem.get_working_directory()
35
89
  # filesystem.remove_file(str(Path(working_directory_path, config_install.INSTALL_LOG_FILE_NAME)))
90
+
91
+ return 0
92
+
93
+
94
+ def analyze_log_file(log_file: str):
95
+ """
96
+ This function will analyze the log file for plugin installation errors.
97
+ :param log_file:
98
+ :return:
99
+ """
100
+
101
+ with open(log_file, 'r') as file:
102
+ log_content: str = file.read()
103
+
104
+ for plugin in PLUGIN_LIST:
105
+ if f'{FINISHED_INSTALLING_STRINGS[0]} {plugin} {FINISHED_INSTALLING_STRINGS[1]}' not in log_content:
106
+ message = (f'Error: [{plugin}] installation failed.\n'
107
+ f'Check the log file: {log_file}\n'
108
+ f'Exiting...')
109
+ print_api.print_api(message, color='red')
110
+ return 1
111
+
112
+ if LOG_FINISHED_STRING not in log_content:
113
+ message = (f'Error: Installation failed.\n'
114
+ f'Check the log file: {log_file}\n'
115
+ f'Exiting...')
116
+ print_api.print_api(message, color='red')
117
+ return 1
118
+
119
+ return 0
@@ -174,6 +174,7 @@ class MongoDBWrapper:
174
174
  self,
175
175
  collection_name: str,
176
176
  filter_query: dict = None,
177
+ projection: dict = None,
177
178
  page: int = None,
178
179
  items: int = None,
179
180
  sort: dict[str, Literal[
@@ -192,6 +193,7 @@ class MongoDBWrapper:
192
193
  filter_query = None
193
194
 
194
195
  CHECK MORE EXAMPLES IN THE DOCSTRING OF THE FUNCTION 'find' BELOW which is not in this class.
196
+ :param projection: dict, the only fields to return or exclude.
195
197
  :param page: int, the page number (Optional).
196
198
  The results are filtered after results are fetched from db.
197
199
  :param items: int, the number of results per page (Optional).
@@ -229,7 +231,8 @@ class MongoDBWrapper:
229
231
 
230
232
  entries: list[dict] = find(
231
233
  database=self.db, collection_name=collection_name,
232
- filter_query=filter_query, page=page, items=items, sort=sort,
234
+ filter_query=filter_query, projection=projection,
235
+ page=page, items=items, sort=sort,
233
236
  convert_object_id_to_str=convert_object_id_to_str, key_convert_to_dict=keys_convert_to_dict,
234
237
  mongo_client=self.client, close_client=False)
235
238
 
@@ -827,7 +830,7 @@ def find(
827
830
 
828
831
  entries: list[dict] = list(collection_items)
829
832
 
830
- if convert_object_id_to_str:
833
+ if entries and convert_object_id_to_str and '_id' in entries[0]:
831
834
  for entry_index, entry in enumerate(entries):
832
835
  entries[entry_index]['_id'] = str(entry['_id'])
833
836
 
@@ -44,8 +44,12 @@ def get_page_text_content(page) -> str:
44
44
  :return: string, text content of the page.
45
45
  """
46
46
 
47
- text_content: str = page.evaluate('''() => {
48
- return document.body.innerText;
49
- }''')
47
+ # Full javascript.
48
+ # text_content: str = page.evaluate('''() => {
49
+ # return document.body.innerText;
50
+ # }''')
51
+
52
+ # Short javascript.
53
+ text_content: str = page.evaluate("document.body.innerText")
50
54
 
51
55
  return text_content
@@ -43,8 +43,12 @@ def get_text_from_html_tag(url: str, tag_name: str, attribute: str, value: str)
43
43
 
44
44
 
45
45
  def get_page_content(
46
- url: str, page_format: str = 'html', path: str = None, print_kwargs: dict = None, pdf_format: str = 'A4',
47
- html_txt_convert_to_bytes: bool = True
46
+ url: str,
47
+ page_format: str = 'html',
48
+ path: str = None,
49
+ pdf_format: str = 'A4',
50
+ html_txt_convert_to_bytes: bool = True,
51
+ print_kwargs: dict = None
48
52
  ) -> any:
49
53
  """
50
54
  The function receives playwright engine and page object, navigates to URL, gets page content in specified format,
@@ -57,10 +61,10 @@ def get_page_content(
57
61
  'png' - returns png binary.
58
62
  'jpeg' - returns jpeg binary.
59
63
  :param path: string of path to save the file to. Default is None.
60
- :param print_kwargs: dict, that contains all the arguments for 'print_api' function.
61
64
  :param pdf_format: string of pdf format, applicable only if 'page_format=pdf'. Default is 'A4'.
62
65
  :param html_txt_convert_to_bytes: boolean, applicable only if 'page_format=html' or 'page_format=txt'.
63
66
  Default is True.
67
+ :param print_kwargs: dict, that contains all the arguments for 'print_api' function.
64
68
 
65
69
  :return: any page content in specified format.
66
70
  """
@@ -95,8 +99,13 @@ def get_page_content(
95
99
 
96
100
 
97
101
  def get_page_content_in_thread(
98
- url: str, page_format: str = 'html', path: str = None, print_kwargs: dict = None, pdf_format: str = 'A4',
99
- html_txt_convert_to_bytes: bool = True):
102
+ url: str,
103
+ page_format: str = 'html',
104
+ path: str = None,
105
+ pdf_format: str = 'A4',
106
+ html_txt_convert_to_bytes: bool = True,
107
+ print_kwargs: dict = None
108
+ ):
100
109
  """
101
110
  The function uses 'threads.thread_wrap_var' function in order to wrap the function 'get_page_content' and
102
111
  execute it in a thread with arguments and return the result.
@@ -144,8 +144,23 @@ def get_default_dns_gateway() -> tuple[bool, list[str]]:
144
144
  :return: tuple(is dynamic boolean, list of DNS server IPv4s).
145
145
  """
146
146
 
147
+ def get_current_interface_status(current_interface_settings: dict) -> tuple[bool, list[str]]:
148
+ if current_interface_settings['NameServer']:
149
+ result = (False, current_interface_settings['NameServer'].split(','))
150
+ else:
151
+ result = (True, current_interface_settings['DhcpNameServer'].split(','))
152
+
153
+ return result
154
+
155
+
147
156
  # Get current default IPv4 address of the interface that is being used for internet.
148
157
  default_ipv4_address: str = socket.gethostbyname(socket.gethostname())
158
+ # If the default IPv4 address is localhost, then it could mean that the system is not connected to the internet.
159
+ # Or there is no network adapter at all.
160
+ default_dns_gateway_list: list[str] = []
161
+ if default_ipv4_address == '127.0.0.1':
162
+ from ... import dns
163
+ default_dns_gateway_list = dns.get_default_dns_gateway_with_dns_resolver()
149
164
 
150
165
  # Get all network interface settings from the registry.
151
166
  all_interfaces_configurations = get_network_interfaces_settings()
@@ -153,22 +168,36 @@ def get_default_dns_gateway() -> tuple[bool, list[str]]:
153
168
  # Find the interface that has this IPv4 assigned.
154
169
  function_result: tuple = tuple()
155
170
  for interface_guid, interface_settings in all_interfaces_configurations.items():
156
- current_interface_static_ipv4_address: list = interface_settings.get('IPAddress', None)
157
- current_interface_dynamic_ipv4_address: str = interface_settings.get('DhcpIPAddress', None)
158
-
159
- static_and_ip_match: bool = (
160
- current_interface_static_ipv4_address and
161
- current_interface_static_ipv4_address[0] == default_ipv4_address)
162
- dynamic_and_ip_match: bool = (
163
- current_interface_dynamic_ipv4_address and
164
- current_interface_dynamic_ipv4_address == default_ipv4_address)
165
- if static_and_ip_match or dynamic_and_ip_match:
166
- if interface_settings['NameServer']:
167
- function_result = (False, interface_settings['NameServer'].split(','))
168
- else:
169
- function_result = (True, interface_settings['DhcpNameServer'].split(','))
170
-
171
- break
171
+ if not interface_settings:
172
+ continue
173
+
174
+ if ' ' in interface_settings['NameServer']:
175
+ interface_settings['NameServer'] = interface_settings['NameServer'].replace(' ', ',')
176
+ if ' ' in interface_settings['DhcpNameServer']:
177
+ interface_settings['DhcpNameServer'] = interface_settings['DhcpNameServer'].replace(' ', ',')
178
+
179
+ if not default_dns_gateway_list:
180
+ current_interface_static_ipv4_address: list = interface_settings.get('IPAddress', None)
181
+ current_interface_dynamic_ipv4_address: str = interface_settings.get('DhcpIPAddress', None)
182
+
183
+ static_and_ip_match: bool = (
184
+ current_interface_static_ipv4_address and
185
+ current_interface_static_ipv4_address[0] == default_ipv4_address)
186
+ dynamic_and_ip_match: bool = (
187
+ current_interface_dynamic_ipv4_address and
188
+ current_interface_dynamic_ipv4_address == default_ipv4_address)
189
+ if static_and_ip_match or dynamic_and_ip_match:
190
+ function_result = get_current_interface_status(interface_settings)
191
+
192
+ break
193
+ else:
194
+ current_interface_name_server_list: list[str] = interface_settings['NameServer'].split(',')
195
+ current_interface_dhcp_name_server_list: list[str] = interface_settings['DhcpNameServer'].split(',')
196
+ if (current_interface_name_server_list == default_dns_gateway_list or
197
+ current_interface_dhcp_name_server_list == default_dns_gateway_list):
198
+ function_result = get_current_interface_status(interface_settings)
199
+
200
+ break
172
201
 
173
202
  # noinspection PyTypeChecker
174
203
  return function_result
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: atomicshop
3
- Version: 2.16.42
3
+ Version: 2.16.44
4
4
  Summary: Atomic functions and classes to make developer life easier
5
5
  Author: Denis Kras
6
6
  License: MIT License
@@ -60,6 +60,7 @@ Requires-Dist: SoundCard
60
60
  Requires-Dist: soundfile
61
61
  Requires-Dist: SpeechRecognition
62
62
  Requires-Dist: tldextract
63
+ Requires-Dist: websockets
63
64
  Requires-Dist: pywin32 ; platform_system == "Windows"
64
65
 
65
66
  <h1 align="center">Atomic Workshop</h1>
@@ -1,4 +1,4 @@
1
- atomicshop/__init__.py,sha256=tWZJJS59JBaRpS5l3fKPYI9YQJf0dM6cQNGppw5sjoo,124
1
+ atomicshop/__init__.py,sha256=cTkolJcyCusHq9bBDXgphXYHAv5YCLiWysUXbZegGZc,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
@@ -10,7 +10,7 @@ atomicshop/console_output.py,sha256=AOSJjrRryE97PAGtgDL03IBtWSi02aNol8noDnW3k6M,
10
10
  atomicshop/console_user_response.py,sha256=31HIy9QGXa7f-GVR8MzJauQ79E_ZqAeagF3Ks4GGdDU,3234
11
11
  atomicshop/datetimes.py,sha256=IQZ66lmta-ZqxYbyHzm_9eugbJFSilXK1e0kfMgoXGg,18371
12
12
  atomicshop/diff_check.py,sha256=Q9RCqRa-jEgo7Fujx08_JTuZ6qcgttMI6aNYB6zN9Ik,27173
13
- atomicshop/dns.py,sha256=3AWZFczxwuYPyVG2r_gXrmHOyLYaqWjO7QYQk9Heghk,6484
13
+ atomicshop/dns.py,sha256=5Gimq_WY2arqg7BeGmR7P--fGfnH0Dsh8lrOt_H0jRY,6817
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
@@ -19,7 +19,7 @@ atomicshop/functions.py,sha256=pK8hoCE9z61PtWCxQJsda7YAphrLH1wxU5x-1QJP-sY,499
19
19
  atomicshop/get_process_list.py,sha256=8cxb7gKe9sl4R6H2yMi8J6oe-RkonTvCdKjRFqi-Fs4,6075
20
20
  atomicshop/get_process_name_cmd_dll.py,sha256=CtaSp3mgxxJKCCVW8BLx6BJNx4giCklU_T7USiCEwfc,5162
21
21
  atomicshop/hashing.py,sha256=Le8qGFyt3_wX-zGTeQShz7L2HL_b6nVv9PnawjglyHo,3474
22
- atomicshop/http_parse.py,sha256=bIrJODFmumfI6uSwWkTtjtJWS-o40kn_eg7J9Mmwr20,10002
22
+ atomicshop/http_parse.py,sha256=WoUoz5HeiIjPzFQYn7d2aeuAKa0kVWvrswyKMZT6VUg,10039
23
23
  atomicshop/inspect_wrapper.py,sha256=sGRVQhrJovNygHTydqJj0hxES-aB2Eg9KbIk3G31apw,11429
24
24
  atomicshop/ip_addresses.py,sha256=Hvi4TumEFoTEpKWaq5WNF-YzcRzt24IxmNgv-Mgax1s,1190
25
25
  atomicshop/keyboard_press.py,sha256=1W5kRtOB75fulVx-uF2yarBhW0_IzdI1k73AnvXstk0,452
@@ -44,7 +44,8 @@ atomicshop/timer.py,sha256=7Zw1KRV0acHCRATMnanyX2MLBb63Hc-6us3rCZ9dNlY,2345
44
44
  atomicshop/urls.py,sha256=aJ0NGS9qqaKeqjkkWBs80jaBBg6MYBiPuLIyPGxscVc,1557
45
45
  atomicshop/uuids.py,sha256=JSQdm3ZTJiwPQ1gYe6kU0TKS_7suwVrHc8JZDGYlydM,2214
46
46
  atomicshop/virtualization.py,sha256=LPP4vjE0Vr10R6DA4lqhfX_WaNdDGRAZUW0Am6VeGco,494
47
- atomicshop/web.py,sha256=DkpdUYiwRu_Du5J8LlUyxLj-NAEnDLfEwfVlSI22trM,11642
47
+ atomicshop/web.py,sha256=GLdTXgMxg1_0UQaXC4bOvARVyuFg7SPIeJdsCHV8rNE,11662
48
+ atomicshop/websocket_parse.py,sha256=PygRqEFnqEcPx3PNRtJoZcKN-mXvT_zighzTJR_B-Tc,14934
48
49
  atomicshop/a_installs/ubuntu/docker_rootless.py,sha256=9IPNtGZYjfy1_n6ZRt7gWz9KZgR6XCgevjqq02xk-o0,281
49
50
  atomicshop/a_installs/ubuntu/docker_sudo.py,sha256=JzayxeyKDtiuT4Icp2L2LyFRbx4wvpyN_bHLfZ-yX5E,281
50
51
  atomicshop/a_installs/ubuntu/elastic_search_and_kibana.py,sha256=yRB-l1zBxdiN6av-FwNkhcBlaeu4zrDPjQ0uPGgpK2I,244
@@ -84,7 +85,7 @@ atomicshop/basics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU
84
85
  atomicshop/basics/ansi_escape_codes.py,sha256=WtIkm-BjSZS5J5irDUdAMBNvdX-qXFZcTX98jcBMpJE,3140
85
86
  atomicshop/basics/argparse_template.py,sha256=horwgSf3MX1ZgRnYxtmmQuz9OU_vKrKggF65gmjlmfg,5836
86
87
  atomicshop/basics/booleans.py,sha256=V36NaMf8AffhCom_ovQeOZlYcdtGyIcQwWKki6h7O0M,1745
87
- atomicshop/basics/bytes_arrays.py,sha256=WvSRDhIGt1ywF95t-yNgpxLm1nlZUbM1Dz6QckcyE8Y,5915
88
+ atomicshop/basics/bytes_arrays.py,sha256=tU7KF0UAFSErGNcan4Uz1GJxeIYVRuUu9sHVZHgyNnw,6542
88
89
  atomicshop/basics/classes.py,sha256=T0Bm13hKvkXG3med68ptL7XuoWiCi3TE-K5TMINDlrY,10655
89
90
  atomicshop/basics/dicts.py,sha256=DeYHIh940pMMBrFhpXt4dsigFVYzTrlqWymNo4Pq_Js,14049
90
91
  atomicshop/basics/dicts_nested.py,sha256=StYxYnYPa0SEJr1lmEwAv5zfERWWqoULeyG8e0zRAwE,4107
@@ -116,7 +117,7 @@ atomicshop/etws/traces/trace_dns.py,sha256=WvOZm7KNdP4r6ofkZhUGi9WjtYlkV3mUp_yxi
116
117
  atomicshop/etws/traces/trace_sysmon_process_creation.py,sha256=OM-bkK38uYMwWLZKNOTDa0Xdk3sO6sqsxoMUIiPvm5g,4656
117
118
  atomicshop/file_io/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
118
119
  atomicshop/file_io/csvs.py,sha256=jBdm3_z5cMyvxLxJnGcybUAptHAbyL0r0tlLqY0sdTQ,9327
119
- atomicshop/file_io/docxs.py,sha256=ffJhnmM_WyD8mCoq2dGdpfahdIrGTPy96QVlH5EWjeI,5754
120
+ atomicshop/file_io/docxs.py,sha256=3ctQ9JiGx8K8EYeKWiuraLtqhilW1qk1cZX9lHv0usk,5753
120
121
  atomicshop/file_io/file_io.py,sha256=5Kl0P6vF4GQVdwew1lzHLb-db9qiMvDjTgccbi5P-zk,7167
121
122
  atomicshop/file_io/jsons.py,sha256=q9ZU8slBKnHLrtn3TnbK1qxrRpj5ZvCm6AlsFzoANjo,5303
122
123
  atomicshop/file_io/tomls.py,sha256=ol8EvQPf9sryTmZUf1v55BYSUQ6ml7HVVBHpNKbsIlA,9768
@@ -125,11 +126,11 @@ atomicshop/file_io/xmls.py,sha256=zh3SuK-dNaFq2NDNhx6ivcf4GYCfGM8M10PcEwDSpxk,21
125
126
  atomicshop/mitm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
126
127
  atomicshop/mitm/config_static.py,sha256=ROAtbibSWSsF3BraUbhu-QO3MPIFqYY5KUKgsQbiSkk,7813
127
128
  atomicshop/mitm/config_toml_editor.py,sha256=2p1CMcktWRR_NW-SmyDwylu63ad5e0-w1QPMa8ZLDBw,1635
128
- atomicshop/mitm/connection_thread_worker.py,sha256=q_h4rRHupuUMwGsHEyKUhOLCtIAEHDB4loLrxApzQvQ,17859
129
+ atomicshop/mitm/connection_thread_worker.py,sha256=DxHCgDtCbSQCIX80WK9hJhp3v37pMjmm-mr3y6tyJMY,17581
129
130
  atomicshop/mitm/import_config.py,sha256=0Ij14aISTllTOiWYJpIUMOWobQqGofD6uafui5uWllE,9272
130
131
  atomicshop/mitm/initialize_engines.py,sha256=VyJE8QnzlgD3QbX5inz5o6rC3zQ3is9CeTL7-B10g1w,8292
131
132
  atomicshop/mitm/message.py,sha256=URR5JKSuAT8XmGIkyprEjlPW2GW4ef_gfUz_GgcFseE,2184
132
- atomicshop/mitm/mitm_main.py,sha256=tT6U4UuJ1PgNs__gDkExSR_qIbCjdBPkOQl8RPMMC5c,23224
133
+ atomicshop/mitm/mitm_main.py,sha256=5c-9oxBiLueTbZr4Dyd4EEOorEUix5vSWxX9p5O1fBs,23375
133
134
  atomicshop/mitm/recs_files.py,sha256=mMyO1kPB-VkS_pbWCDhZHKdbWzlPbYSout61QuzHOao,3077
134
135
  atomicshop/mitm/shared_functions.py,sha256=l6oEyv4ug5D_03V3QLADYoocbcL2Ml_dYVW2WKM21l4,1818
135
136
  atomicshop/mitm/statistic_analyzer.py,sha256=5_sAYGX2Xunzo_pS2W5WijNCwr_BlGJbbOO462y_wN4,27533
@@ -228,7 +229,7 @@ atomicshop/wrappers/factw/fact_extractor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5
228
229
  atomicshop/wrappers/factw/fact_extractor/docker_image.py,sha256=2FyYjnw8gxFNwISQ83OwH-iGivkFi6EAluyCZ0loHEQ,2501
229
230
  atomicshop/wrappers/factw/fact_extractor/get_extractor.py,sha256=2mfOAftHIlCcGt1s7MWdq7DsDCuI6wX3MtvcEZ4SK-0,756
230
231
  atomicshop/wrappers/factw/install/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
231
- atomicshop/wrappers/factw/install/install_after_restart.py,sha256=-VXC3KDX2BzF0oi0ELCmfch55vLk-3t16KxlmYyUGD8,1560
232
+ atomicshop/wrappers/factw/install/install_after_restart.py,sha256=cr8E9BAEdpJXJ1VdyXr85byU0PvE3tPV0uPALQOmiKg,4318
232
233
  atomicshop/wrappers/factw/install/pre_install_and_install_before_restart.py,sha256=GFsO9MTH0czKoxkiPJtjalilUwsmFLBCcx9Znv37S4M,5945
233
234
  atomicshop/wrappers/factw/postgresql/__init__.py,sha256=xMBn2d3Exo23IPP2F_9-SXmOlhFbwWDgS9KwozSTjA0,162
234
235
  atomicshop/wrappers/factw/postgresql/analysis.py,sha256=2Rxzy2jyq3zEKIo53z8VkjuslKE_i5mq2ZpmJAvyd6U,716
@@ -257,7 +258,7 @@ atomicshop/wrappers/mongodbw/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NM
257
258
  atomicshop/wrappers/mongodbw/install_mongodb_ubuntu.py,sha256=pmI9AwWJ2cv5h8GionSpSJkllg6kfp0M381pk6y4Y5U,4015
258
259
  atomicshop/wrappers/mongodbw/install_mongodb_win.py,sha256=3ZPqrXxj3lC-PnAKGXclylLuOqsbyXYeUpb5iGjdeUU,6626
259
260
  atomicshop/wrappers/mongodbw/mongo_infra.py,sha256=IjEF0jPzQz866MpTm7rnksnyyWQeUT_B2h2DA9ryAio,2034
260
- atomicshop/wrappers/mongodbw/mongodbw.py,sha256=Vxsw0lNaJxAVzQWRx_9dNQXKTIKjMyZnx9FbYbINkBk,52435
261
+ atomicshop/wrappers/mongodbw/mongodbw.py,sha256=IkEw86QFyVRU-5p5s6_6yupvSxmaQxr59GKNgSEkAm4,52617
261
262
  atomicshop/wrappers/nodejsw/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
262
263
  atomicshop/wrappers/nodejsw/install_nodejs.py,sha256=TKGa3jSlSqZTL2NA0nMkWDFtlkz7rxGGn44ywCg7MN8,5228
263
264
  atomicshop/wrappers/playwrightw/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -266,11 +267,11 @@ atomicshop/wrappers/playwrightw/base.py,sha256=WeRpx8otdXuKSr-BjY-uCJTze21kbPpfi
266
267
  atomicshop/wrappers/playwrightw/combos.py,sha256=215y7wugyyBrFK9_0WutnMXsF1jqJ2PLm9eHEa2PMz0,7145
267
268
  atomicshop/wrappers/playwrightw/engine.py,sha256=fapOeaqSMNoxJucTvhzp30cKfKNSVe45drhZJcJ-fOk,15191
268
269
  atomicshop/wrappers/playwrightw/infra.py,sha256=ncp62l6CgAgLz4lqBtKEWZO_6ES8cqnbzJBov-tlpBo,97
269
- atomicshop/wrappers/playwrightw/javascript.py,sha256=CL3tVE9C4SVvy76FEgGCl77B-xziCG6JZR5Ed8IfcSo,1112
270
+ atomicshop/wrappers/playwrightw/javascript.py,sha256=_bW7CAtm0Y8IHYrAalg5HpPFnk6juiqicmkvZ9dITaA,1235
270
271
  atomicshop/wrappers/playwrightw/keyboard.py,sha256=zN3YddGO-qUkn6C0CRVFejP4cTuaUwXLDNFhFREjERY,422
271
272
  atomicshop/wrappers/playwrightw/locators.py,sha256=6wsLywZxDuii7mwv-zQsRbqQC8r7j96Bma5b5_7ZoVo,2411
272
273
  atomicshop/wrappers/playwrightw/mouse.py,sha256=-2FZbQtjgH7tdXWld6ZPGqlKFUdf5in0ujN0hewxa50,656
273
- atomicshop/wrappers/playwrightw/scenarios.py,sha256=Wz7aVYfG7K4fuSe_TUAc1jhFXVq5jYvZKbDtvqUiONc,5236
274
+ atomicshop/wrappers/playwrightw/scenarios.py,sha256=OzI3SV0QgazRwMZ0hMEopDHUYG-aygBSxZ50w78lIP8,5310
274
275
  atomicshop/wrappers/playwrightw/waits.py,sha256=PBFdz_PoM7Fo7O8hLqMrxNPzBEYgPoXwZceFFCGGeu8,7182
275
276
  atomicshop/wrappers/psutilw/cpus.py,sha256=w6LPBMINqS-T_X8vzdYkLS2Wzuve28Ydp_GafTCngrc,236
276
277
  atomicshop/wrappers/psutilw/disks.py,sha256=3ZSVoommKH1TWo37j_83frB-NqXF4Nf5q5mBCX8G4jE,9221
@@ -313,9 +314,9 @@ atomicshop/wrappers/socketw/socket_wrapper.py,sha256=WtylpezgIIBuz-A6PfM0hO1sm9E
313
314
  atomicshop/wrappers/socketw/ssl_base.py,sha256=kmiif84kMhBr5yjQW17p935sfjR5JKG0LxIwBA4iVvU,2275
314
315
  atomicshop/wrappers/socketw/statistics_csv.py,sha256=w1AH-zf4mBuT4euf28UKij9ihM-b1BRU9Qfby0QDdqI,2957
315
316
  atomicshop/wrappers/winregw/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
316
- atomicshop/wrappers/winregw/winreg_network.py,sha256=4tIBitgT4X_y76k3T1ychRu85KNRflLfjFumSmoj7es,7138
317
- atomicshop-2.16.42.dist-info/LICENSE.txt,sha256=lLU7EYycfYcK2NR_1gfnhnRC8b8ccOTElACYplgZN88,1094
318
- atomicshop-2.16.42.dist-info/METADATA,sha256=PPS3bp3ZqUTDio5PNmC2vlny1J9gavc8P4HHXhrnaQE,10473
319
- atomicshop-2.16.42.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
320
- atomicshop-2.16.42.dist-info/top_level.txt,sha256=EgKJB-7xcrAPeqTRF2laD_Np2gNGYkJkd4OyXqpJphA,11
321
- atomicshop-2.16.42.dist-info/RECORD,,
317
+ atomicshop/wrappers/winregw/winreg_network.py,sha256=ChnVG8ZecKJ-DMF8nUHRiifWJq2M4slEwKat6FBfPfE,8685
318
+ atomicshop-2.16.44.dist-info/LICENSE.txt,sha256=lLU7EYycfYcK2NR_1gfnhnRC8b8ccOTElACYplgZN88,1094
319
+ atomicshop-2.16.44.dist-info/METADATA,sha256=yeA-HD5CtX5FhTCrJG9ylhYHECkHvJL_ai4gaOnilY4,10500
320
+ atomicshop-2.16.44.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
321
+ atomicshop-2.16.44.dist-info/top_level.txt,sha256=EgKJB-7xcrAPeqTRF2laD_Np2gNGYkJkd4OyXqpJphA,11
322
+ atomicshop-2.16.44.dist-info/RECORD,,