atomicshop 3.3.28__py3-none-any.whl → 3.4.0__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__ = '3.3.28'
4
+ __version__ = '3.4.0'
@@ -2,6 +2,8 @@ from datetime import datetime
2
2
  import threading
3
3
  import queue
4
4
  import socket
5
+ import ssl
6
+ from typing import Literal
5
7
 
6
8
  from ..wrappers.socketw import receiver, sender, socket_client, base
7
9
  from .. import websocket_parse, ip_addresses
@@ -187,13 +189,19 @@ def thread_worker_main(
187
189
  def create_requester_request(
188
190
  client_message: ClientMessage,
189
191
  sending_socket: socket.socket
190
- ) -> list[bytes]:
192
+ ) -> tuple[bytes, bool]:
193
+ request_received_raw: bytes = client_message.request_raw_bytes
191
194
  request_custom_raw: bytes = requester.create_request(client_message, sending_socket=sending_socket)
192
195
 
193
- # Output first 100 characters of the request.
194
- requester.logger.info(f"{request_custom_raw[0: 100]}...")
196
+ if request_custom_raw is None or request_received_raw == request_custom_raw:
197
+ is_requester_worked: bool = False
198
+ else:
199
+ is_requester_worked: bool = True
200
+
201
+ # Output first 100 characters of the request.
202
+ requester.logger.info(f"{request_custom_raw[0: 100]}...")
195
203
 
196
- return [request_custom_raw]
204
+ return request_custom_raw, is_requester_worked
197
205
 
198
206
  def create_responder_response(client_message: ClientMessage) -> list[bytes]:
199
207
  if client_message.action == 'service_connect':
@@ -335,6 +343,260 @@ def thread_worker_main(
335
343
 
336
344
  return client_message
337
345
 
346
+ def receive_send_service_connect(
347
+ client_connection_message: ClientMessage,
348
+ sending_socket: ssl.SSLSocket | socket.socket
349
+ ) -> Literal['continue', 'return'] | None:
350
+
351
+ nonlocal exception_or_close_in_receiving_thread
352
+
353
+ client_message = client_connection_message
354
+
355
+ bytes_to_send_list: list[bytes] = create_responder_response(client_message)
356
+ print_api(f"Got responses from connect responder, count: [{len(bytes_to_send_list)}]",
357
+ logger=network_logger, logger_method='info')
358
+
359
+ # If the client message is the connection message, then we'll skip to the next iteration.
360
+ if not bytes_to_send_list:
361
+ return 'continue'
362
+
363
+ # is_socket_closed: bool = False
364
+ error_on_send: str = str()
365
+ for bytes_to_send_single in bytes_to_send_list:
366
+ client_message.reinitialize_dynamic_vars()
367
+ client_message.timestamp = datetime.now()
368
+ client_message.response_raw_bytes = bytes_to_send_single
369
+ client_message.action = 'service_responder'
370
+ record_and_statistics_write(client_message)
371
+
372
+ # Send the bytes back to the client socket.
373
+ error_on_send: str = sender.Sender(
374
+ ssl_socket=sending_socket, class_message=bytes_to_send_single,
375
+ logger=network_logger).send()
376
+
377
+ if error_on_send:
378
+ client_message.reinitialize_dynamic_vars()
379
+ client_message.errors.append(error_on_send)
380
+ client_message.timestamp = datetime.now()
381
+ client_message.action = 'client_send'
382
+ record_and_statistics_write(client_message)
383
+
384
+ if error_on_send:
385
+ exception_or_close_in_receiving_thread = True
386
+ finish_thread()
387
+ return 'return'
388
+
389
+ return None
390
+
391
+ def receive_send_client_offline(
392
+ client_message: ClientMessage,
393
+ receiving_socket: ssl.SSLSocket | socket.socket,
394
+ sending_socket: ssl.SSLSocket | socket.socket
395
+ ) -> Literal['return'] | None:
396
+ nonlocal exception_or_close_in_receiving_thread
397
+ nonlocal client_receive_count
398
+
399
+ client_receive_count += 1
400
+
401
+ network_logger.info(f"Initializing Receiver for Client cycle: {str(client_receive_count)}")
402
+ client_message.timestamp = datetime.now()
403
+
404
+ received_raw_data, is_socket_closed, error_message = receiver.Receiver(
405
+ ssl_socket=receiving_socket, logger=network_logger).receive()
406
+
407
+ process_client_raw_data(received_raw_data, error_message, client_message)
408
+ client_message.action = 'client_receive'
409
+
410
+ record_and_statistics_write(client_message)
411
+
412
+ # If there was an exception in the service thread, then receiving empty bytes doesn't mean that
413
+ # the socket was closed by the other side, it means that the service thread closed the socket.
414
+ if (received_raw_data == b'' or error_message) and exception_or_close_in_receiving_thread:
415
+ print_api("Both sockets are closed, breaking the loop", logger=network_logger,
416
+ logger_method='info')
417
+ return 'return'
418
+
419
+ # If the socket was closed on receive, and we're in offline mode, then we'll finish the thread right away.
420
+ # Since nothing more can be done, like responding to service or using requester.
421
+ if is_socket_closed:
422
+ exception_or_close_in_receiving_thread = True
423
+ finish_thread()
424
+ return 'return'
425
+
426
+ # Send to requester.
427
+ # THERE IS ALWAYS WILL BE ONLY ONE REQUEST FROM REQUESTER, SINCE THIS IS WHAT WE GOT FROM THE CLIENT.
428
+ request_custom_raw, is_requester_worked = create_requester_request(client_message, sending_socket=sending_socket)
429
+ # We will not process the raw data if requester didn't change anything.
430
+ if is_requester_worked:
431
+ client_message.reinitialize_dynamic_vars()
432
+ client_message.timestamp = datetime.now()
433
+ client_message.request_raw_bytes = request_custom_raw
434
+ client_message.action = 'client_requester'
435
+ process_client_raw_data(request_custom_raw, error_message, client_message)
436
+ record_and_statistics_write(client_message)
437
+
438
+ print_api("Offline Mode, sending to responder directly.", logger=network_logger,
439
+ logger_method='info')
440
+ bytes_to_send_list: list[bytes] = create_responder_response(client_message)
441
+
442
+ error_on_send: str = str()
443
+ for bytes_to_send_single in bytes_to_send_list:
444
+ client_message.reinitialize_dynamic_vars()
445
+ client_message.timestamp = datetime.now()
446
+ client_message.response_raw_bytes = bytes_to_send_single
447
+ client_message.action = 'client_responder_offline'
448
+ process_server_raw_data(bytes_to_send_single, '', client_message)
449
+ record_and_statistics_write(client_message)
450
+
451
+ error_on_send: str = sender.Sender(
452
+ ssl_socket=receiving_socket, class_message=bytes_to_send_single,
453
+ logger=network_logger).send()
454
+
455
+ if error_on_send:
456
+ client_message.reinitialize_dynamic_vars()
457
+ client_message.errors.append(error_on_send)
458
+ client_message.timestamp = datetime.now()
459
+ client_message.action = 'service_send'
460
+
461
+ record_and_statistics_write(client_message)
462
+
463
+ # If the socket was closed on message receive, then we'll break the loop only after send.
464
+ if is_socket_closed or error_on_send:
465
+ exception_or_close_in_receiving_thread = True
466
+ finish_thread()
467
+ return 'return'
468
+
469
+ return None
470
+
471
+ def receive_send_client(
472
+ client_message: ClientMessage,
473
+ receiving_socket: ssl.SSLSocket | socket.socket,
474
+ sending_socket: ssl.SSLSocket | socket.socket
475
+ ) -> Literal['return'] | None:
476
+
477
+ nonlocal exception_or_close_in_receiving_thread
478
+ nonlocal client_receive_count
479
+
480
+ client_receive_count += 1
481
+
482
+ network_logger.info(f"Initializing Receiver for Client cycle: {str(client_receive_count)}")
483
+
484
+ # Getting message from the client over the socket using specific class.
485
+ client_message.timestamp = datetime.now()
486
+ received_raw_data, is_socket_closed, error_message = receiver.Receiver(
487
+ ssl_socket=receiving_socket, logger=network_logger).receive()
488
+
489
+ process_client_raw_data(received_raw_data, error_message, client_message)
490
+ client_message.action = 'client_receive'
491
+
492
+ record_and_statistics_write(client_message)
493
+
494
+ # If there was an exception in the service thread, then receiving empty bytes doesn't mean that
495
+ # the socket was closed by the other side, it means that the service thread closed the socket.
496
+ if (received_raw_data == b'' or error_message) and exception_or_close_in_receiving_thread:
497
+ print_api("Both sockets are closed, breaking the loop", logger=network_logger,
498
+ logger_method='info')
499
+ return 'return'
500
+
501
+ # Send to requester.
502
+ # THERE IS ALWAYS WILL BE ONLY ONE REQUEST FROM REQUESTER, SINCE THIS IS WHAT WE GOT FROM THE CLIENT.
503
+ request_custom_raw, is_requester_worked = create_requester_request(client_message, sending_socket=sending_socket)
504
+ # We will not process the raw data if requester didn't change anything.
505
+ if is_requester_worked:
506
+ client_message.reinitialize_dynamic_vars()
507
+ client_message.timestamp = datetime.now()
508
+ client_message.request_raw_bytes = request_custom_raw
509
+ client_message.action = 'client_requester'
510
+ process_client_raw_data(request_custom_raw, error_message, client_message)
511
+ record_and_statistics_write(client_message)
512
+
513
+ error_on_send: str = sender.Sender(
514
+ ssl_socket=sending_socket, class_message=client_message.request_raw_bytes,
515
+ logger=network_logger).send()
516
+
517
+ if error_on_send:
518
+ client_message.reinitialize_dynamic_vars()
519
+ client_message.errors.append(error_on_send)
520
+ client_message.timestamp = datetime.now()
521
+ client_message.action = 'service_send'
522
+ record_and_statistics_write(client_message)
523
+
524
+ # If the socket was closed on message receive, then we'll break the loop only after send.
525
+ if is_socket_closed or error_on_send:
526
+ exception_or_close_in_receiving_thread = True
527
+ finish_thread()
528
+ return 'return'
529
+
530
+ return None
531
+
532
+ def receive_send_service(
533
+ client_message: ClientMessage,
534
+ receiving_socket: ssl.SSLSocket | socket.socket,
535
+ sending_socket: ssl.SSLSocket | socket.socket
536
+ ) -> Literal['return'] | None:
537
+
538
+ nonlocal exception_or_close_in_receiving_thread
539
+ nonlocal server_receive_count
540
+
541
+ server_receive_count += 1
542
+
543
+ network_logger.info(f"Initializing Receiver for Service cycle: {str(server_receive_count)}")
544
+
545
+ # Getting message from the client over the socket using specific class.
546
+ client_message.timestamp = datetime.now()
547
+ received_raw_data, is_socket_closed, error_message = receiver.Receiver(
548
+ ssl_socket=receiving_socket, logger=network_logger).receive()
549
+
550
+ process_server_raw_data(received_raw_data, error_message, client_message)
551
+ client_message.action = 'service_receive'
552
+ record_and_statistics_write(client_message)
553
+
554
+ # If there was an exception in the service thread, then receiving empty bytes doesn't mean that
555
+ # the socket was closed by the other side, it means that the service thread closed the socket.
556
+ if (received_raw_data == b'' or error_message) and exception_or_close_in_receiving_thread:
557
+ print_api("Both sockets are closed, breaking the loop", logger=network_logger,
558
+ logger_method='info')
559
+ return 'return'
560
+
561
+ # Now send it to requester/responder.
562
+ bytes_to_send_list: list[bytes] = create_responder_response(client_message)
563
+ print_api(f"Got responses from responder, count: [{len(bytes_to_send_list)}]",
564
+ logger=network_logger, logger_method='info')
565
+
566
+ # is_socket_closed: bool = False
567
+ error_on_send: str = str()
568
+ for bytes_to_send_single in bytes_to_send_list:
569
+ client_message.reinitialize_dynamic_vars()
570
+ client_message.timestamp = datetime.now()
571
+ client_message.response_raw_bytes = bytes_to_send_single
572
+
573
+ # This records the requester or responder output, only if it is not the same as the original
574
+ # message.
575
+ if bytes_to_send_single != received_raw_data:
576
+ client_message.action = 'service_responder'
577
+ record_and_statistics_write(client_message)
578
+
579
+ error_on_send: str = sender.Sender(
580
+ ssl_socket=sending_socket, class_message=bytes_to_send_single,
581
+ logger=network_logger).send()
582
+
583
+ if error_on_send:
584
+ client_message.reinitialize_dynamic_vars()
585
+ client_message.errors.append(error_on_send)
586
+ client_message.timestamp = datetime.now()
587
+ client_message.action = 'client_send'
588
+
589
+ record_and_statistics_write(client_message)
590
+
591
+ # If the socket was closed on message receive, then we'll break the loop only after send.
592
+ if is_socket_closed or error_on_send:
593
+ exception_or_close_in_receiving_thread = True
594
+ finish_thread()
595
+ return 'return'
596
+
597
+ return None
598
+
599
+
338
600
  def receive_send_start(
339
601
  receiving_socket,
340
602
  sending_socket = None,
@@ -360,146 +622,25 @@ def thread_worker_main(
360
622
  raise ValueError(f"Unknown side of the socket: {receiving_socket}")
361
623
 
362
624
  while True:
363
- is_socket_closed: bool = False
364
- # pass the socket connect to responder.
365
- if side == 'Service' and client_connection_message:
366
- client_message = client_connection_message
367
-
368
- bytes_to_send_list: list[bytes] = create_responder_response(client_message)
369
- print_api(f"Got responses from connect responder, count: [{len(bytes_to_send_list)}]", logger=network_logger,
370
- logger_method='info')
625
+ client_message.reinitialize_dynamic_vars()
371
626
 
372
- received_raw_data = None
373
- else:
374
- client_message.reinitialize_dynamic_vars()
375
-
376
- if side == 'Client':
377
- client_receive_count += 1
378
- current_count = client_receive_count
379
- else:
380
- server_receive_count += 1
381
- current_count = server_receive_count
382
-
383
- # Getting current time of message received, either from client or service.
384
- client_message.timestamp = datetime.now()
385
-
386
- # # No need to receive on service socket if we're in offline mode, because there is no service to connect to.
387
- # if config_static.MainConfig.offline and side == 'Service':
388
- # print_api("Offline Mode, skipping receiving on service socket.", logger=network_logger,
389
- # logger_method='info')
390
- # else:
391
-
392
- network_logger.info(
393
- f"Initializing Receiver for {side} cycle: {str(current_count)}")
394
-
395
- # Getting message from the client over the socket using specific class.
396
- received_raw_data, is_socket_closed, error_message = receiver.Receiver(
397
- ssl_socket=receiving_socket, logger=network_logger).receive()
398
-
399
- # In case of client socket, we'll process the raw data specifically for the client.
400
- if side == 'Client':
401
- process_client_raw_data(received_raw_data, error_message, client_message)
402
- client_message.action = 'client_receive'
403
- # In case of service socket, we'll process the raw data specifically for the service.
404
- else:
405
- process_server_raw_data(received_raw_data, error_message, client_message)
406
- client_message.action = 'service_receive'
407
-
408
- # If there was an exception in the service thread, then receiving empty bytes doesn't mean that
409
- # the socket was closed by the other side, it means that the service thread closed the socket.
410
- if (received_raw_data == b'' or error_message) and exception_or_close_in_receiving_thread:
411
- print_api("Both sockets are closed, breaking the loop", logger=network_logger,
412
- logger_method='info')
413
- return
414
-
415
- # We will record only if there was no closing signal, because if there was, it means that we initiated
416
- # the close on the opposite socket.
417
- record_and_statistics_write(client_message)
418
-
419
- # if is_socket_closed:
420
- # exception_or_close_in_receiving_thread = True
421
- # finish_thread()
422
- # return
423
-
424
- # Now send it to requester/responder.
425
- if side == 'Client':
426
- # Send to requester.
427
- bytes_to_send_list: list[bytes] = create_requester_request(
428
- client_message, sending_socket=sending_socket)
429
-
430
- # If we're in offline mode, then we'll put the request to the responder right away.
431
- if config_static.MainConfig.offline:
432
- print_api("Offline Mode, sending to responder directly.", logger=network_logger,
433
- logger_method='info')
434
- process_client_raw_data(bytes_to_send_list[0], error_message, client_message)
435
- bytes_to_send_list = create_responder_response(client_message)
436
- elif side == 'Service':
437
- bytes_to_send_list: list[bytes] = create_responder_response(client_message)
438
- print_api(f"Got responses from responder, count: [{len(bytes_to_send_list)}]",
439
- logger=network_logger,
440
- logger_method='info')
441
- else:
442
- raise ValueError(f"Unknown side [{side}] of the socket: {receiving_socket}")
443
-
444
-
445
- # If nothing was passed from the responder, and the client message is the connection message, then we'll skip to the next iteration.
446
- if not bytes_to_send_list and client_connection_message:
627
+ if side == 'Service' and client_connection_message:
628
+ result: Literal['continue', 'return'] | None = (
629
+ receive_send_service_connect(client_connection_message, sending_socket))
447
630
  client_connection_message = None
448
- continue
449
-
450
- # is_socket_closed: bool = False
451
- error_on_send: str = str()
452
- for bytes_to_send_single in bytes_to_send_list:
453
- client_message.reinitialize_dynamic_vars()
454
- client_message.timestamp = datetime.now()
455
-
456
- if side == 'Client':
457
- client_message.request_raw_bytes = bytes_to_send_single
458
- else:
459
- client_message.response_raw_bytes = bytes_to_send_single
460
-
461
- # This records the requester or responder output, only if it is not the same as the original
462
- # message.
463
- if bytes_to_send_single != received_raw_data:
464
- if side == 'Client':
465
- client_message.action = 'client_requester'
466
-
467
- if config_static.MainConfig.offline:
468
- client_message.action = 'client_responder_offline'
469
- elif side == 'Service':
470
- client_message.action = 'service_responder'
471
- record_and_statistics_write(client_message)
472
-
473
- # If we're in offline mode, it means we're in the client thread, and we'll send the
474
- # bytes back to the client socket.
475
- if config_static.MainConfig.offline:
476
- error_on_send: str = sender.Sender(
477
- ssl_socket=receiving_socket, class_message=bytes_to_send_single,
478
- logger=network_logger).send()
479
- else:
480
- error_on_send: str = sender.Sender(
481
- ssl_socket=sending_socket, class_message=bytes_to_send_single,
482
- logger=network_logger).send()
483
-
484
- if error_on_send:
485
- client_message.reinitialize_dynamic_vars()
486
- client_message.errors.append(error_on_send)
487
- client_message.timestamp = datetime.now()
488
- if side == 'Client':
489
- client_message.action = 'service_send'
490
- else:
491
- client_message.action = 'client_send'
492
-
493
- record_and_statistics_write(client_message)
494
-
495
- # If the socket was closed on message receive, then we'll break the loop only after send.
496
- if is_socket_closed or error_on_send:
497
- exception_or_close_in_receiving_thread = True
498
- finish_thread()
499
- return
631
+ if result == 'continue':
632
+ continue
633
+ elif side == 'Client' and config_static.MainConfig.offline:
634
+ result: Literal['return'] | None = receive_send_client_offline(client_message, receiving_socket, sending_socket)
635
+ elif side == 'Client':
636
+ result: Literal['return'] | None = receive_send_client(client_message, receiving_socket, sending_socket)
637
+ elif side == 'Service':
638
+ result: Literal['return'] | None = receive_send_service(client_message, receiving_socket, sending_socket)
639
+ else:
640
+ raise ValueError(f"Unknown side [{side}] of the socket: {receiving_socket}")
500
641
 
501
- # For next iteration to start in case this iteration was responsible to process connection message, we need to set it to None.
502
- client_connection_message = None
642
+ if result == 'return':
643
+ return
503
644
  except Exception as exc:
504
645
  # If the sockets were already closed, then there is nothing to do here besides log.
505
646
  # if (isinstance(exc, OSError) and exc.errno == 10038 and
@@ -602,7 +743,13 @@ def thread_worker_main(
602
743
  try:
603
744
  engine_name: str = recorder.engine_name
604
745
  client_ip, source_port = client_socket.getpeername()
605
- client_name: str = socket.gethostbyaddr(client_ip)[0]
746
+
747
+ try:
748
+ client_name: str = socket.gethostbyaddr(client_ip)[0]
749
+ # This can happen if the host changed IP address, but it wasn't propagated over DHCP.
750
+ except socket.herror:
751
+ client_name = ""
752
+
606
753
  client_name = client_name.lower()
607
754
  destination_port: int = client_socket.getsockname()[1]
608
755
  destination_port_str: str = str(destination_port)
@@ -112,5 +112,5 @@ class RequesterParent:
112
112
  def create_request(self, class_client_message: ClientMessage, **kwargs) -> bytes:
113
113
  """ This function should be overridden in the child class. """
114
114
 
115
- request_bytes: bytes = class_client_message.request_raw_bytes
115
+ request_bytes: bytes = None
116
116
  return request_bytes
@@ -773,7 +773,28 @@ def _loop_at_midnight_recs_archive(network_logger_name):
773
773
 
774
774
 
775
775
  def mitm_server_main(config_file_path: str, script_version: str):
776
+ # This is for Linux and MacOS.
777
+ signal.signal(signal.SIGTERM, _graceful_shutdown)
776
778
  signal.signal(signal.SIGINT, _graceful_shutdown)
779
+ # This is for Windows.
780
+ """
781
+ Example:
782
+ script = (Path(__file__).resolve().parent / "ServerTCPWithDNS.py").resolve()
783
+ p = subprocess.Popen(
784
+ [sys.executable, "-u", str(script)],
785
+ creationflags=subprocess.CREATE_NEW_PROCESS_GROUP,
786
+ # inherit console; do NOT use CREATE_NEW_CONSOLE
787
+ )
788
+ time.sleep(30)
789
+ p.send_signal(signal.CTRL_BREAK_EVENT)
790
+ try:
791
+ p.wait(timeout=5)
792
+ except subprocess.TimeoutExpired:
793
+ print("Graceful interrupt timed out; terminating")
794
+ p.terminate()
795
+ p.wait()
796
+ """
797
+ signal.signal(signal.SIGBREAK, _graceful_shutdown)
777
798
 
778
799
  try:
779
800
  # Main function should return integer with error code, 0 is successful.
@@ -163,11 +163,103 @@ class GitHubWrapper:
163
163
 
164
164
  self.build_links_from_user_and_repo()
165
165
 
166
- def check_github_domain(self, domain):
166
+ def check_github_domain(
167
+ self,
168
+ domain: str
169
+ ):
167
170
  if self.domain not in domain:
168
171
  print_api(
169
172
  f'This is not [{self.domain}] domain.', color="red", error_type=True)
170
173
 
174
+ def download_file(
175
+ self,
176
+ file_name: str,
177
+ target_dir: str
178
+ ) -> str:
179
+ """
180
+ Download a single repo file to a local directory.
181
+
182
+ :param file_name: string, Full repo-relative path to the file. Example:
183
+ "eng.traineddata"
184
+ "script\\English.script"
185
+ :param target_dir: string, Local directory to save into.
186
+
187
+ :return: The local path to the downloaded file.
188
+ """
189
+
190
+ # Normalize to GitHub path format
191
+ file_path = file_name.replace("\\", "/").strip("/")
192
+
193
+ headers = self._get_headers()
194
+ url = f"{self.contents_url}/{file_path}"
195
+ params = {"ref": self.branch}
196
+
197
+ resp = requests.get(url, headers=headers, params=params)
198
+ resp.raise_for_status()
199
+ item = resp.json()
200
+
201
+ # Expect a single file object
202
+ if isinstance(item, list) or item.get("type") != "file":
203
+ raise ValueError(f"'{file_name}' is not a file in branch '{self.branch}'.")
204
+
205
+ download_url = item.get("download_url")
206
+ if not download_url:
207
+ raise ValueError(f"Unable to obtain download URL for '{file_name}'.")
208
+
209
+ os.makedirs(target_dir, exist_ok=True)
210
+ local_name = item.get("name") or os.path.basename(file_path)
211
+
212
+ from .. import web # ensure available in your module structure
213
+ web.download(
214
+ file_url=download_url,
215
+ target_directory=target_dir,
216
+ file_name=local_name,
217
+ headers=headers,
218
+ )
219
+ return os.path.join(target_dir, local_name)
220
+
221
+ def download_directory(
222
+ self,
223
+ folder_name: str,
224
+ target_dir: str
225
+ ) -> None:
226
+ """
227
+ Recursively download a repo directory to a local directory.
228
+
229
+ :param folder_name: string, Repo-relative directory path to download (e.g., "tests/langs").
230
+ :param target_dir: string, Local directory to save the folder tree into.
231
+ """
232
+ headers = self._get_headers()
233
+ root_path = folder_name.replace("\\", "/").strip("/")
234
+
235
+ def _walk_dir(rel_path: str, local_dir: str) -> None:
236
+ contents_url = f"{self.contents_url}/{rel_path}" if rel_path else self.contents_url
237
+ params = {"ref": self.branch}
238
+
239
+ response = requests.get(contents_url, headers=headers, params=params)
240
+ response.raise_for_status()
241
+ items = response.json()
242
+
243
+ # If a file path was passed accidentally, delegate to download_file
244
+ if isinstance(items, dict) and items.get("type") == "file":
245
+ self.download_file(rel_path, local_dir)
246
+ return
247
+
248
+ if not isinstance(items, list):
249
+ raise ValueError(f"Unexpected response shape when listing '{rel_path or '/'}'.")
250
+
251
+ os.makedirs(local_dir, exist_ok=True)
252
+
253
+ for item in items:
254
+ name = item["name"]
255
+ if item["type"] == "file":
256
+ self.download_file(f"{rel_path}/{name}" if rel_path else name, local_dir)
257
+ elif item["type"] == "dir":
258
+ _walk_dir(f"{rel_path}/{name}" if rel_path else name, os.path.join(local_dir, name))
259
+ # ignore symlinks/submodules if present
260
+
261
+ _walk_dir(root_path, target_dir)
262
+
171
263
  def download_and_extract_branch(
172
264
  self,
173
265
  target_directory: str,
@@ -195,48 +287,6 @@ class GitHubWrapper:
195
287
  :return:
196
288
  """
197
289
 
198
- def download_file(file_url: str, target_dir: str, file_name: str, current_headers: dict) -> None:
199
- os.makedirs(target_dir, exist_ok=True)
200
-
201
- web.download(
202
- file_url=file_url,
203
- target_directory=target_dir,
204
- file_name=file_name,
205
- headers=current_headers
206
- )
207
-
208
- def download_directory(folder_path: str, target_dir: str, current_headers: dict) -> None:
209
- # Construct the API URL for the current folder.
210
- contents_url = f"{self.contents_url}/{folder_path}"
211
- params = {'ref': self.branch}
212
-
213
- response = requests.get(contents_url, headers=current_headers, params=params)
214
- response.raise_for_status()
215
-
216
- # Get the list of items (files and subdirectories) in the folder.
217
- items = response.json()
218
-
219
- # Ensure the local target directory exists.
220
- os.makedirs(target_dir, exist_ok=True)
221
-
222
- # Process each item.
223
- for item in items:
224
- local_item_path = os.path.join(target_dir, item['name'])
225
- if item['type'] == 'file':
226
- download_file(
227
- file_url=item['download_url'],
228
- target_dir=target_dir,
229
- file_name=item['name'],
230
- current_headers=current_headers
231
- )
232
- elif item['type'] == 'dir':
233
- # Recursively download subdirectories.
234
- download_directory(
235
- folder_path=f"{folder_path}/{item['name']}",
236
- target_dir=local_item_path,
237
- current_headers=current_headers
238
- )
239
-
240
290
  headers: dict = self._get_headers()
241
291
 
242
292
  if not download_each_file:
@@ -275,7 +325,7 @@ class GitHubWrapper:
275
325
  else:
276
326
  current_target_directory = os.path.join(target_directory, self.path)
277
327
 
278
- download_directory(self.path, current_target_directory, headers)
328
+ self.download_directory(self.path, current_target_directory)
279
329
 
280
330
  def get_releases_json(
281
331
  self,
@@ -530,8 +580,77 @@ class GitHubWrapper:
530
580
  commit_message = latest_commit.get("commit", {}).get("message", "")
531
581
  return commit_message
532
582
 
583
+ def list_files(
584
+ self,
585
+ pattern: str = "*",
586
+ recursive: bool = True,
587
+ path: str | None = None,
588
+ ) -> list[str]:
589
+ """
590
+ List files in the repository (or in a specific subfolder).
533
591
 
534
- def parse_github_args():
592
+ :param pattern: Glob-style pattern (e.g., "*.ex*", "*test*.py"). Matching is done
593
+ against the file's base name (not the full path).
594
+ :param recursive: If True, include files in all subfolders (returns full repo-relative
595
+ paths). If False, list only the immediate files in the chosen folder.
596
+ :param path: Optional subfolder to list from (e.g., "tests/langs"). If omitted,
597
+ uses self.path if set, otherwise the repo root.
598
+
599
+ :return: A list of repo-relative file paths that match the pattern.
600
+ """
601
+ headers = self._get_headers()
602
+ base_path = (path or self.path or "").strip("/")
603
+
604
+ if recursive:
605
+ # Use the Git Trees API to fetch all files in one call, then filter.
606
+ tree_url = f"{self.api_url}/git/trees/{self.branch}"
607
+ params = {"recursive": "1"}
608
+ resp = requests.get(tree_url, headers=headers, params=params)
609
+ resp.raise_for_status()
610
+ data = resp.json()
611
+
612
+ files = []
613
+ for entry in data.get("tree", []):
614
+ if entry.get("type") != "blob":
615
+ continue # only files
616
+ entry_path = entry.get("path", "")
617
+ # If a base_path was provided, keep only files under it
618
+ if base_path and not entry_path.startswith(base_path + "/") and entry_path != base_path:
619
+ continue
620
+ # Match pattern against the *file name* (basename)
621
+ if fnmatch.fnmatch(os.path.basename(entry_path), pattern):
622
+ files.append(entry_path)
623
+ return files
624
+
625
+ else:
626
+ # Non-recursive: use the Contents API to list a single directory.
627
+ # If base_path is empty, list the repo root.
628
+ if base_path:
629
+ contents_url = f"{self.contents_url}/{base_path}"
630
+ else:
631
+ contents_url = self.contents_url
632
+
633
+ params = {"ref": self.branch}
634
+ resp = requests.get(contents_url, headers=headers, params=params)
635
+ resp.raise_for_status()
636
+ items = resp.json()
637
+
638
+ # The Contents API returns a dict when the path points to a single file;
639
+ # normalize to a list to simplify handling.
640
+ if isinstance(items, dict):
641
+ items = [items]
642
+
643
+ files = []
644
+ for item in items:
645
+ if item.get("type") == "file":
646
+ name = item.get("name", "")
647
+ if fnmatch.fnmatch(name, pattern):
648
+ # item["path"] is the full repo-relative path we want to return
649
+ files.append(item.get("path", name))
650
+ return files
651
+
652
+
653
+ def _make_parser():
535
654
  import argparse
536
655
 
537
656
  parser = argparse.ArgumentParser(description='GitHub Wrapper')
@@ -562,7 +681,7 @@ def parse_github_args():
562
681
  '-db', '--download_branch', action='store_true', default=False,
563
682
  help='Sets if the branch will be downloaded. In conjunction with path, only the path will be downloaded.')
564
683
 
565
- return parser.parse_args()
684
+ return parser
566
685
 
567
686
 
568
687
  def github_wrapper_main(
@@ -571,8 +690,8 @@ def github_wrapper_main(
571
690
  path: str = None,
572
691
  target_directory: str = None,
573
692
  pat: str = None,
574
- get_latest_commit_json: bool = False,
575
693
  get_latest_commit_message: bool = False,
694
+ get_latest_commit_json: bool = False,
576
695
  download_branch: bool = False
577
696
  ):
578
697
  """
@@ -610,15 +729,7 @@ def github_wrapper_main(
610
729
 
611
730
 
612
731
  def github_wrapper_main_with_args():
613
- args = parse_github_args()
614
-
615
- return github_wrapper_main(
616
- repo_url=args.repo_url,
617
- branch=args.branch,
618
- path=args.path,
619
- target_directory=args.target_directory,
620
- pat=args.pat,
621
- get_latest_commit_json=args.get_latest_commit_json,
622
- get_latest_commit_message=args.get_latest_commit_message,
623
- download_branch=args.download_branch
624
- )
732
+ main_parser = _make_parser()
733
+ args = main_parser.parse_args()
734
+
735
+ return github_wrapper_main(**vars(args))
@@ -478,7 +478,7 @@ class DnsServer:
478
478
  dns_cached_request = False
479
479
  # Check if the received data request from client is already in the cache
480
480
  if client_data in self.dns_questions_to_answers_cache:
481
- # message = "!!! Question / Answer is already in the dictionary..."
481
+ # message = "!!! Request / Response is already in the dictionary..."
482
482
  # self.logger.info(message)
483
483
 
484
484
  # Get the response from the cached answers list
@@ -561,7 +561,7 @@ class DnsServer:
561
561
  f'{self.offline_route_ipv6}')
562
562
  )
563
563
 
564
- message = f"!!! Question / Answer is in offline mode returning " \
564
+ message = f"!!! Request / Response is in offline mode returning " \
565
565
  f"{self.offline_route_ipv6}."
566
566
  self.logger.info(message)
567
567
 
@@ -577,7 +577,7 @@ class DnsServer:
577
577
  elif qtype_string == "SRV" or qtype_string == "SOA" or qtype_string == "HTTPS":
578
578
  dns_built_response.add_answer(*RR.fromZone(self.offline_srv_answer))
579
579
 
580
- message = f"!!! Question / Answer is in offline mode returning: " \
580
+ message = f"!!! Request / Response is in offline mode returning: " \
581
581
  f"{self.offline_srv_answer}."
582
582
  self.logger.info(message)
583
583
  elif qtype_string == "ANY":
@@ -586,7 +586,7 @@ class DnsServer:
586
586
  self.offline_route_domain)
587
587
  )
588
588
 
589
- message = f"!!! Question / Answer is in offline mode returning " \
589
+ message = f"!!! Request / Response is in offline mode returning " \
590
590
  f"{self.offline_route_domain}."
591
591
  self.logger.info(message)
592
592
  else:
@@ -596,7 +596,7 @@ class DnsServer:
596
596
  " " + self.offline_route_ipv4)
597
597
  )
598
598
 
599
- message = f"!!! Question / Answer is in offline mode returning " \
599
+ message = f"!!! Request / Response is in offline mode returning " \
600
600
  f"{self.offline_route_ipv4}."
601
601
  self.logger.info(message)
602
602
  # Values error means in most cases that you create wrong response
@@ -1,3 +1,4 @@
1
+ import socket
1
2
  import ssl
2
3
  import logging
3
4
  from pathlib import Path
@@ -12,12 +13,12 @@ from . import base
12
13
  class Sender:
13
14
  def __init__(
14
15
  self,
15
- ssl_socket: ssl.SSLSocket,
16
+ ssl_socket: ssl.SSLSocket | socket.socket,
16
17
  class_message: bytes,
17
18
  logger: logging.Logger = None
18
19
  ):
19
20
  self.class_message: bytes = class_message
20
- self.ssl_socket: ssl.SSLSocket = ssl_socket
21
+ self.ssl_socket: ssl.SSLSocket | socket.socket = ssl_socket
21
22
 
22
23
  if logger:
23
24
  # Create child logger for the provided logger with the module's name.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: atomicshop
3
- Version: 3.3.28
3
+ Version: 3.4.0
4
4
  Summary: Atomic functions and classes to make developer life easier
5
5
  Author: Denis Kras
6
6
  License-Expression: MIT
@@ -1,4 +1,4 @@
1
- atomicshop/__init__.py,sha256=7_vQ7ANOGXMGTntv4aMk8d8oVSTFzWJBQnvcLQHYyFw,123
1
+ atomicshop/__init__.py,sha256=ixOmnAbr1hvPV-ANWmylWbdUrobDsVq3iPHZfp7dYM4,122
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
@@ -126,11 +126,11 @@ atomicshop/file_io/xmls.py,sha256=zh3SuK-dNaFq2NDNhx6ivcf4GYCfGM8M10PcEwDSpxk,21
126
126
  atomicshop/mitm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
127
127
  atomicshop/mitm/config_static.py,sha256=KzO8DjZjRHfkQMYSIGTkW4jLNPzMR8visTqs1H6ZQ-U,8926
128
128
  atomicshop/mitm/config_toml_editor.py,sha256=2p1CMcktWRR_NW-SmyDwylu63ad5e0-w1QPMa8ZLDBw,1635
129
- atomicshop/mitm/connection_thread_worker.py,sha256=zPgDoIrZw6iIfjt_vwAYBanjOz23DMUZaWsBtHPUm34,33298
129
+ atomicshop/mitm/connection_thread_worker.py,sha256=NPHizpPJOaYjP05EEGxEOOKHhgbe4CSzUuk7WTPgZd4,38858
130
130
  atomicshop/mitm/import_config.py,sha256=7aLfKqflc3ZnzKc2_Y4T0eenzQpKG94M0r-PaVwF99M,18881
131
131
  atomicshop/mitm/initialize_engines.py,sha256=qzz5jzh_lKC03bI1w5ebngVXo1K-RV3poAyW-nObyqo,11042
132
132
  atomicshop/mitm/message.py,sha256=CDhhm4BTuZE7oNZCjvIZ4BuPOW4MuIzQLOg91hJaxDI,3065
133
- atomicshop/mitm/mitm_main.py,sha256=C1lZr5-1yRUCtPgs8mBnt_bD7W1fNOweI45OpmlSOII,39501
133
+ atomicshop/mitm/mitm_main.py,sha256=vjdK18ix3oH3thTgCi5qlAL13Bw_PgHaLGH2D9xic8w,40229
134
134
  atomicshop/mitm/recs_files.py,sha256=tv8XFhYZMkBv4DauvpiAdPgvSo0Bcm1CghnmwO7dx8M,5018
135
135
  atomicshop/mitm/shared_functions.py,sha256=0lzeyINd44sVEfFbahJxQmz6KAMWbYrW5ou3UYfItvw,1777
136
136
  atomicshop/mitm/statistic_analyzer.py,sha256=EC9g21ocOsFzNfntV-nZHSGtrS1-Kxb0QDSGWS5FuNA,28942
@@ -140,7 +140,7 @@ atomicshop/mitm/engines/create_module_template_main_example.py,sha256=LeQ44Rp2Gi
140
140
  atomicshop/mitm/engines/__parent/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
141
141
  atomicshop/mitm/engines/__parent/parser___parent.py,sha256=HHaCXhScl3OlPjz6eUxsDpJaZyk6BNuDMc9xCkeo2Ws,661
142
142
  atomicshop/mitm/engines/__parent/recorder___parent.py,sha256=D99cbpMneY9STSAPETa6eIxyfs_Q9etRYxm2dosA-DI,6203
143
- atomicshop/mitm/engines/__parent/requester___parent.py,sha256=j3QYOfQFEPSzIEWihkssNcfaLWC8cpdpi-ciPgjKNBc,5126
143
+ atomicshop/mitm/engines/__parent/requester___parent.py,sha256=-S817ERzs1pdY2rXiQBnG4eB4sm2jsy2wycpvuiDlpI,5092
144
144
  atomicshop/mitm/engines/__parent/responder___parent.py,sha256=mtiS_6ej9nxT9UhAQR4ftMqnqL-j_kO3u8KEaoEaI9k,9495
145
145
  atomicshop/mitm/engines/__reference_general/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
146
146
  atomicshop/mitm/engines/__reference_general/parser___reference_general.py,sha256=57MEPZMAjTO6xBDZ-yt6lgGJyqRrP0Do5Gk_cgCiPns,2998
@@ -187,7 +187,7 @@ atomicshop/wrappers/astw.py,sha256=VkYfkfyc_PJLIOxByT6L7B8uUmKY6-I8XGZl4t_z828,4
187
187
  atomicshop/wrappers/configparserw.py,sha256=JwDTPjZoSrv44YKwIRcjyUnpN-FjgXVfMqMK_tJuSgU,22800
188
188
  atomicshop/wrappers/cryptographyw.py,sha256=QEUpDn8vUvMg3ADz6-4oC2kbDNC_woDlw7C0zU7qFVM,14233
189
189
  atomicshop/wrappers/ffmpegw.py,sha256=wcq0ZnAe0yajBOuTKZCCaKI7CDBjkq7FAgdW5IsKcVE,6031
190
- atomicshop/wrappers/githubw.py,sha256=BEEJuWd1eBfiv1FSgb76D4nEBRpFU9j37lztn-KbYa4,27548
190
+ atomicshop/wrappers/githubw.py,sha256=95RAbxomp4GA9-7B1pk6q3aYOmrCO0vkHyhSPgr3n3Q,31924
191
191
  atomicshop/wrappers/netshw.py,sha256=8WE_576XiiHykwFuE-VkCx5CydMpFlztX4frlEteCtI,6350
192
192
  atomicshop/wrappers/numpyw.py,sha256=sBV4gSKyr23kXTalqAb1oqttzE_2XxBooCui66jbAqc,1025
193
193
  atomicshop/wrappers/olefilew.py,sha256=biD5m58rogifCYmYhJBrAFb9O_Bn_spLek_9HofLeYE,2051
@@ -300,11 +300,11 @@ atomicshop/wrappers/socketw/accepter.py,sha256=4I9ORugRDvwaqSzm_gWSjZnRwQGY8hDTl
300
300
  atomicshop/wrappers/socketw/base.py,sha256=EcosGkD8VzgBY3GeIHDSG29ThQfXwg3-GQPmBTAqTdw,3048
301
301
  atomicshop/wrappers/socketw/certificator.py,sha256=mtWPJ_ew3OSwt0-1W4jaoco1VIY4NRCrMv3mDUxb_Cc,12418
302
302
  atomicshop/wrappers/socketw/creator.py,sha256=hHq8frKQtqZ1-Xfdm2kAsxqtsLFxXKDNwgGKdVKV6yg,16192
303
- atomicshop/wrappers/socketw/dns_server.py,sha256=GOYMvHvS6Fx7s-DRygGqO7_o8_Qt9on3HmKxgOSznRE,55956
303
+ atomicshop/wrappers/socketw/dns_server.py,sha256=UHq1a3NVdOrclEOOQIe-wNtIgbF8DFeNXsobvtoM1U8,55961
304
304
  atomicshop/wrappers/socketw/exception_wrapper.py,sha256=_p98OdOaKYSMqJ23pHLXBUA7NkbVmpgqcSJAdWr6wwc,7560
305
305
  atomicshop/wrappers/socketw/get_process.py,sha256=aJC-_qFUv3NgWCSUzDI72E4z8_-VTZE9NVZ0CwUoNlM,5698
306
306
  atomicshop/wrappers/socketw/receiver.py,sha256=9B3MvcDqr4C3x2fsnjG5SQognd1wRqsBgikxZa0wXG8,8243
307
- atomicshop/wrappers/socketw/sender.py,sha256=aX_K8l_rHjd5AWb8bi5mt8-YTkMYVRDB6DnPqK_XDUE,4754
307
+ atomicshop/wrappers/socketw/sender.py,sha256=5ecHUlz4Sxt4oWevBFfy33jQLRXmmVLOF34njfvSbxY,4801
308
308
  atomicshop/wrappers/socketw/sni.py,sha256=uj6KKYKmSrzXcKBhVLaHQhYn1wNfIUpdnmcvn21V9iE,18176
309
309
  atomicshop/wrappers/socketw/socket_client.py,sha256=WWIiCxUX9irN9aWzJ6-1xrXNB_iv_diq3ha1yrWsNGU,22671
310
310
  atomicshop/wrappers/socketw/socket_server_tester.py,sha256=Qobmh4XV8ZxLUaw-eW4ESKAbeSLecCKn2OWFzMhadk0,6420
@@ -314,8 +314,8 @@ atomicshop/wrappers/socketw/statistics_csv.py,sha256=_gA8bMX6Sw_UCXKi2y9wNAwlqif
314
314
  atomicshop/wrappers/winregw/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
315
315
  atomicshop/wrappers/winregw/winreg_installed_software.py,sha256=Qzmyktvob1qp6Tjk2DjLfAqr_yXV0sgWzdMW_9kwNjY,2345
316
316
  atomicshop/wrappers/winregw/winreg_network.py,sha256=ih0BVNwByLvf9F_Lac4EdmDYYJA3PzMvmG0PieDZrsE,9905
317
- atomicshop-3.3.28.dist-info/licenses/LICENSE.txt,sha256=lLU7EYycfYcK2NR_1gfnhnRC8b8ccOTElACYplgZN88,1094
318
- atomicshop-3.3.28.dist-info/METADATA,sha256=mpQNhoVSvg9_AcIj-Qot4Kpvk_LrNOXujfo9ctQbzbA,9318
319
- atomicshop-3.3.28.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
320
- atomicshop-3.3.28.dist-info/top_level.txt,sha256=EgKJB-7xcrAPeqTRF2laD_Np2gNGYkJkd4OyXqpJphA,11
321
- atomicshop-3.3.28.dist-info/RECORD,,
317
+ atomicshop-3.4.0.dist-info/licenses/LICENSE.txt,sha256=lLU7EYycfYcK2NR_1gfnhnRC8b8ccOTElACYplgZN88,1094
318
+ atomicshop-3.4.0.dist-info/METADATA,sha256=7L3q8e0dAu6OukGb_M71h6kx-aLtHEr7E710wUkSUpI,9317
319
+ atomicshop-3.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
320
+ atomicshop-3.4.0.dist-info/top_level.txt,sha256=EgKJB-7xcrAPeqTRF2laD_Np2gNGYkJkd4OyXqpJphA,11
321
+ atomicshop-3.4.0.dist-info/RECORD,,