atomicshop 2.15.11__py3-none-any.whl → 3.10.5__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.
Files changed (221) hide show
  1. atomicshop/__init__.py +1 -1
  2. atomicshop/{addons/mains → a_mains}/FACT/update_extract.py +3 -2
  3. atomicshop/a_mains/dns_gateway_setting.py +11 -0
  4. atomicshop/a_mains/get_local_tcp_ports.py +85 -0
  5. atomicshop/a_mains/github_wrapper.py +11 -0
  6. atomicshop/a_mains/install_ca_certificate.py +172 -0
  7. atomicshop/a_mains/process_from_port.py +119 -0
  8. atomicshop/a_mains/set_default_dns_gateway.py +90 -0
  9. atomicshop/a_mains/update_config_toml.py +38 -0
  10. atomicshop/basics/ansi_escape_codes.py +3 -1
  11. atomicshop/basics/argparse_template.py +2 -0
  12. atomicshop/basics/booleans.py +27 -30
  13. atomicshop/basics/bytes_arrays.py +43 -0
  14. atomicshop/basics/classes.py +149 -1
  15. atomicshop/basics/enums.py +2 -2
  16. atomicshop/basics/exceptions.py +5 -1
  17. atomicshop/basics/list_of_classes.py +29 -0
  18. atomicshop/basics/multiprocesses.py +374 -50
  19. atomicshop/basics/strings.py +72 -3
  20. atomicshop/basics/threads.py +14 -0
  21. atomicshop/basics/tracebacks.py +13 -3
  22. atomicshop/certificates.py +153 -52
  23. atomicshop/config_init.py +11 -6
  24. atomicshop/console_user_response.py +7 -14
  25. atomicshop/consoles.py +9 -0
  26. atomicshop/datetimes.py +1 -1
  27. atomicshop/diff_check.py +3 -3
  28. atomicshop/dns.py +128 -3
  29. atomicshop/etws/_pywintrace_fix.py +17 -0
  30. atomicshop/etws/trace.py +40 -42
  31. atomicshop/etws/traces/trace_dns.py +56 -44
  32. atomicshop/etws/traces/trace_tcp.py +130 -0
  33. atomicshop/file_io/csvs.py +27 -5
  34. atomicshop/file_io/docxs.py +34 -17
  35. atomicshop/file_io/file_io.py +31 -17
  36. atomicshop/file_io/jsons.py +49 -0
  37. atomicshop/file_io/tomls.py +139 -0
  38. atomicshop/filesystem.py +616 -291
  39. atomicshop/get_process_list.py +3 -3
  40. atomicshop/http_parse.py +149 -93
  41. atomicshop/ip_addresses.py +6 -1
  42. atomicshop/mitm/centered_settings.py +132 -0
  43. atomicshop/mitm/config_static.py +207 -0
  44. atomicshop/mitm/config_toml_editor.py +55 -0
  45. atomicshop/mitm/connection_thread_worker.py +875 -357
  46. atomicshop/mitm/engines/__parent/parser___parent.py +4 -17
  47. atomicshop/mitm/engines/__parent/recorder___parent.py +108 -51
  48. atomicshop/mitm/engines/__parent/requester___parent.py +116 -0
  49. atomicshop/mitm/engines/__parent/responder___parent.py +75 -114
  50. atomicshop/mitm/engines/__reference_general/parser___reference_general.py +10 -7
  51. atomicshop/mitm/engines/__reference_general/recorder___reference_general.py +5 -5
  52. atomicshop/mitm/engines/__reference_general/requester___reference_general.py +47 -0
  53. atomicshop/mitm/engines/__reference_general/responder___reference_general.py +95 -13
  54. atomicshop/mitm/engines/create_module_template.py +58 -14
  55. atomicshop/mitm/import_config.py +359 -139
  56. atomicshop/mitm/initialize_engines.py +160 -80
  57. atomicshop/mitm/message.py +64 -23
  58. atomicshop/mitm/mitm_main.py +892 -0
  59. atomicshop/mitm/recs_files.py +183 -0
  60. atomicshop/mitm/shared_functions.py +4 -10
  61. atomicshop/mitm/ssh_tester.py +82 -0
  62. atomicshop/mitm/statistic_analyzer.py +136 -40
  63. atomicshop/mitm/statistic_analyzer_helper/moving_average_helper.py +265 -83
  64. atomicshop/monitor/checks/dns.py +1 -1
  65. atomicshop/networks.py +671 -0
  66. atomicshop/on_exit.py +39 -9
  67. atomicshop/package_mains_processor.py +84 -0
  68. atomicshop/permissions/permissions.py +22 -0
  69. atomicshop/permissions/ubuntu_permissions.py +239 -0
  70. atomicshop/permissions/win_permissions.py +33 -0
  71. atomicshop/print_api.py +24 -42
  72. atomicshop/process.py +24 -6
  73. atomicshop/process_poller/process_pool.py +0 -1
  74. atomicshop/process_poller/simple_process_pool.py +204 -5
  75. atomicshop/python_file_patcher.py +1 -1
  76. atomicshop/python_functions.py +27 -75
  77. atomicshop/speech_recognize.py +8 -0
  78. atomicshop/ssh_remote.py +158 -172
  79. atomicshop/system_resource_monitor.py +61 -47
  80. atomicshop/system_resources.py +8 -8
  81. atomicshop/tempfiles.py +1 -2
  82. atomicshop/urls.py +6 -0
  83. atomicshop/venvs.py +28 -0
  84. atomicshop/versioning.py +27 -0
  85. atomicshop/web.py +98 -27
  86. atomicshop/web_apis/google_custom_search.py +44 -0
  87. atomicshop/web_apis/google_llm.py +188 -0
  88. atomicshop/websocket_parse.py +450 -0
  89. atomicshop/wrappers/certauthw/certauth.py +1 -0
  90. atomicshop/wrappers/cryptographyw.py +29 -8
  91. atomicshop/wrappers/ctyping/etw_winapi/const.py +97 -47
  92. atomicshop/wrappers/ctyping/etw_winapi/etw_functions.py +178 -49
  93. atomicshop/wrappers/ctyping/file_details_winapi.py +67 -0
  94. atomicshop/wrappers/ctyping/msi_windows_installer/cabs.py +2 -1
  95. atomicshop/wrappers/ctyping/msi_windows_installer/extract_msi_main.py +2 -2
  96. atomicshop/wrappers/ctyping/setup_device.py +466 -0
  97. atomicshop/wrappers/ctyping/win_console.py +39 -0
  98. atomicshop/wrappers/dockerw/dockerw.py +113 -2
  99. atomicshop/wrappers/elasticsearchw/config_basic.py +0 -12
  100. atomicshop/wrappers/elasticsearchw/elastic_infra.py +75 -0
  101. atomicshop/wrappers/elasticsearchw/elasticsearchw.py +2 -20
  102. atomicshop/wrappers/factw/get_file_data.py +12 -5
  103. atomicshop/wrappers/factw/install/install_after_restart.py +89 -5
  104. atomicshop/wrappers/factw/install/pre_install_and_install_before_restart.py +20 -14
  105. atomicshop/wrappers/githubw.py +537 -54
  106. atomicshop/wrappers/loggingw/consts.py +1 -1
  107. atomicshop/wrappers/loggingw/filters.py +23 -0
  108. atomicshop/wrappers/loggingw/formatters.py +12 -0
  109. atomicshop/wrappers/loggingw/handlers.py +214 -107
  110. atomicshop/wrappers/loggingw/loggers.py +19 -0
  111. atomicshop/wrappers/loggingw/loggingw.py +860 -22
  112. atomicshop/wrappers/loggingw/reading.py +134 -112
  113. atomicshop/wrappers/mongodbw/mongo_infra.py +31 -0
  114. atomicshop/wrappers/mongodbw/mongodbw.py +1324 -36
  115. atomicshop/wrappers/netshw.py +271 -0
  116. atomicshop/wrappers/playwrightw/engine.py +34 -19
  117. atomicshop/wrappers/playwrightw/infra.py +5 -0
  118. atomicshop/wrappers/playwrightw/javascript.py +7 -3
  119. atomicshop/wrappers/playwrightw/keyboard.py +14 -0
  120. atomicshop/wrappers/playwrightw/scenarios.py +172 -5
  121. atomicshop/wrappers/playwrightw/waits.py +9 -7
  122. atomicshop/wrappers/powershell_networking.py +80 -0
  123. atomicshop/wrappers/psutilw/processes.py +37 -1
  124. atomicshop/wrappers/psutilw/psutil_networks.py +85 -0
  125. atomicshop/wrappers/pyopensslw.py +9 -2
  126. atomicshop/wrappers/pywin32w/cert_store.py +116 -0
  127. atomicshop/wrappers/pywin32w/win_event_log/fetch.py +174 -0
  128. atomicshop/wrappers/pywin32w/win_event_log/subscribes/process_create.py +3 -105
  129. atomicshop/wrappers/pywin32w/win_event_log/subscribes/process_terminate.py +3 -57
  130. atomicshop/wrappers/pywin32w/wmis/msft_netipaddress.py +113 -0
  131. atomicshop/wrappers/pywin32w/wmis/win32_networkadapterconfiguration.py +259 -0
  132. atomicshop/wrappers/pywin32w/wmis/win32networkadapter.py +112 -0
  133. atomicshop/wrappers/pywin32w/wmis/wmi_helpers.py +236 -0
  134. atomicshop/wrappers/socketw/accepter.py +21 -7
  135. atomicshop/wrappers/socketw/certificator.py +216 -150
  136. atomicshop/wrappers/socketw/creator.py +190 -50
  137. atomicshop/wrappers/socketw/dns_server.py +491 -182
  138. atomicshop/wrappers/socketw/exception_wrapper.py +45 -52
  139. atomicshop/wrappers/socketw/process_getter.py +86 -0
  140. atomicshop/wrappers/socketw/receiver.py +144 -102
  141. atomicshop/wrappers/socketw/sender.py +65 -35
  142. atomicshop/wrappers/socketw/sni.py +334 -165
  143. atomicshop/wrappers/socketw/socket_base.py +134 -0
  144. atomicshop/wrappers/socketw/socket_client.py +137 -95
  145. atomicshop/wrappers/socketw/socket_server_tester.py +11 -7
  146. atomicshop/wrappers/socketw/socket_wrapper.py +717 -116
  147. atomicshop/wrappers/socketw/ssl_base.py +15 -14
  148. atomicshop/wrappers/socketw/statistics_csv.py +148 -17
  149. atomicshop/wrappers/sysmonw.py +1 -1
  150. atomicshop/wrappers/ubuntu_terminal.py +65 -26
  151. atomicshop/wrappers/win_auditw.py +189 -0
  152. atomicshop/wrappers/winregw/__init__.py +0 -0
  153. atomicshop/wrappers/winregw/winreg_installed_software.py +58 -0
  154. atomicshop/wrappers/winregw/winreg_network.py +232 -0
  155. {atomicshop-2.15.11.dist-info → atomicshop-3.10.5.dist-info}/METADATA +31 -51
  156. atomicshop-3.10.5.dist-info/RECORD +306 -0
  157. {atomicshop-2.15.11.dist-info → atomicshop-3.10.5.dist-info}/WHEEL +1 -1
  158. atomicshop/_basics_temp.py +0 -101
  159. atomicshop/a_installs/win/fibratus.py +0 -9
  160. atomicshop/a_installs/win/mongodb.py +0 -9
  161. atomicshop/a_installs/win/pycharm.py +0 -9
  162. atomicshop/addons/a_setup_scripts/install_psycopg2_ubuntu.sh +0 -3
  163. atomicshop/addons/a_setup_scripts/install_pywintrace_0.3.cmd +0 -2
  164. atomicshop/addons/mains/__pycache__/install_fibratus_windows.cpython-312.pyc +0 -0
  165. atomicshop/addons/mains/__pycache__/msi_unpacker.cpython-312.pyc +0 -0
  166. atomicshop/addons/mains/install_docker_rootless_ubuntu.py +0 -11
  167. atomicshop/addons/mains/install_docker_ubuntu_main_sudo.py +0 -11
  168. atomicshop/addons/mains/install_elastic_search_and_kibana_ubuntu.py +0 -10
  169. atomicshop/addons/mains/install_wsl_ubuntu_lts_admin.py +0 -9
  170. atomicshop/addons/package_setup/CreateWheel.cmd +0 -7
  171. atomicshop/addons/package_setup/Setup in Edit mode.cmd +0 -6
  172. atomicshop/addons/package_setup/Setup.cmd +0 -7
  173. atomicshop/archiver/_search_in_zip.py +0 -189
  174. atomicshop/archiver/archiver.py +0 -34
  175. atomicshop/archiver/search_in_archive.py +0 -250
  176. atomicshop/archiver/sevenz_app_w.py +0 -86
  177. atomicshop/archiver/sevenzs.py +0 -44
  178. atomicshop/archiver/zips.py +0 -293
  179. atomicshop/file_types.py +0 -24
  180. atomicshop/mitm/config_editor.py +0 -37
  181. atomicshop/mitm/engines/create_module_template_example.py +0 -13
  182. atomicshop/mitm/initialize_mitm_server.py +0 -268
  183. atomicshop/pbtkmultifile_argparse.py +0 -88
  184. atomicshop/permissions.py +0 -151
  185. atomicshop/script_as_string_processor.py +0 -38
  186. atomicshop/ssh_scripts/process_from_ipv4.py +0 -37
  187. atomicshop/ssh_scripts/process_from_port.py +0 -27
  188. atomicshop/wrappers/_process_wrapper_curl.py +0 -27
  189. atomicshop/wrappers/_process_wrapper_tar.py +0 -21
  190. atomicshop/wrappers/dockerw/install_docker.py +0 -209
  191. atomicshop/wrappers/elasticsearchw/infrastructure.py +0 -265
  192. atomicshop/wrappers/elasticsearchw/install_elastic.py +0 -232
  193. atomicshop/wrappers/ffmpegw.py +0 -125
  194. atomicshop/wrappers/fibratusw/install.py +0 -81
  195. atomicshop/wrappers/mongodbw/infrastructure.py +0 -53
  196. atomicshop/wrappers/mongodbw/install_mongodb.py +0 -190
  197. atomicshop/wrappers/msiw.py +0 -149
  198. atomicshop/wrappers/nodejsw/install_nodejs.py +0 -139
  199. atomicshop/wrappers/process_wrapper_pbtk.py +0 -16
  200. atomicshop/wrappers/psutilw/networks.py +0 -45
  201. atomicshop/wrappers/pycharmw.py +0 -81
  202. atomicshop/wrappers/socketw/base.py +0 -59
  203. atomicshop/wrappers/socketw/get_process.py +0 -107
  204. atomicshop/wrappers/wslw.py +0 -191
  205. atomicshop-2.15.11.dist-info/RECORD +0 -302
  206. /atomicshop/{addons/mains → a_mains}/FACT/factw_fact_extractor_docker_image_main_sudo.py +0 -0
  207. /atomicshop/{addons → a_mains/addons}/PlayWrightCodegen.cmd +0 -0
  208. /atomicshop/{addons → a_mains/addons}/ScriptExecution.cmd +0 -0
  209. /atomicshop/{addons → a_mains/addons}/inits/init_to_import_all_modules.py +0 -0
  210. /atomicshop/{addons → a_mains/addons}/process_list/ReadMe.txt +0 -0
  211. /atomicshop/{addons → a_mains/addons}/process_list/compile.cmd +0 -0
  212. /atomicshop/{addons → a_mains/addons}/process_list/compiled/Win10x64/process_list.dll +0 -0
  213. /atomicshop/{addons → a_mains/addons}/process_list/compiled/Win10x64/process_list.exp +0 -0
  214. /atomicshop/{addons → a_mains/addons}/process_list/compiled/Win10x64/process_list.lib +0 -0
  215. /atomicshop/{addons → a_mains/addons}/process_list/process_list.cpp +0 -0
  216. /atomicshop/{archiver → permissions}/__init__.py +0 -0
  217. /atomicshop/{wrappers/fibratusw → web_apis}/__init__.py +0 -0
  218. /atomicshop/wrappers/{nodejsw → pywin32w/wmis}/__init__.py +0 -0
  219. /atomicshop/wrappers/pywin32w/{wmi_win32process.py → wmis/win32process.py} +0 -0
  220. {atomicshop-2.15.11.dist-info → atomicshop-3.10.5.dist-info/licenses}/LICENSE.txt +0 -0
  221. {atomicshop-2.15.11.dist-info → atomicshop-3.10.5.dist-info}/top_level.txt +0 -0
@@ -1,20 +1,32 @@
1
1
  import requests
2
2
  import fnmatch
3
+ import os
4
+ import tempfile
5
+ from typing import Literal
3
6
 
4
- from .. import web, urls
7
+ from .. import web, urls, filesystem
5
8
  from ..print_api import print_api
6
- from ..basics import strings
9
+
10
+
11
+ class MoreThanOneReleaseFoundError(Exception):
12
+ pass
13
+
14
+ class NoReleaseFoundError(Exception):
15
+ pass
7
16
 
8
17
 
9
18
  class GitHubWrapper:
10
- # You also can use '.tar.gz' as extension.
11
19
  def __init__(
12
20
  self,
13
21
  user_name: str = None,
14
22
  repo_name: str = None,
15
23
  repo_url: str = None,
16
- branch: str = 'master',
17
- branch_file_extension: str = '.zip'
24
+ branch: str = 'main',
25
+ path: str = None,
26
+ pat: str = None,
27
+ branch_file_extension: Literal[
28
+ 'zip',
29
+ 'tar.gz'] = 'zip'
18
30
  ):
19
31
  """
20
32
  This class is a wrapper for GitHub repositories. It can download the branch file from the repository and extract
@@ -27,7 +39,11 @@ class GitHubWrapper:
27
39
  You can provide the full url to the repository directly and then extract the user_name and repo_name from it
28
40
  with the 'build_links_from_repo_url' function.
29
41
  :param branch: str, the branch name. The default is 'master'.
30
- :param branch_file_extension: str, the branch file extension. The default is '.zip'.
42
+ :param path: str, the path to the file/folder inside the repo that we'll do certain actions on.
43
+ Actions example: get_latest_commit_comment, download_path_from_branch.
44
+ :param pat: str, the personal access token to the repo.
45
+ :param branch_file_extension: str, the branch file extension. The default is 'zip'.
46
+ You also can use 'tar.gz' as extension.
31
47
 
32
48
  ================================================================================================================
33
49
  Usage to download the 'master' branch file:
@@ -62,7 +78,7 @@ class GitHubWrapper:
62
78
  Usage to download the latest release where the file name is 'test_file.zip':
63
79
  git_wrapper = GitHubWrapper(user_name='user_name', repo_name='repo_name')
64
80
  git_wrapper.download_and_extract_latest_release(
65
- target_directory='target_directory', string_pattern='test_*.zip')
81
+ target_directory='target_directory', asset_pattern='test_*.zip')
66
82
  ================================================================================================================
67
83
  Usage to get the latest release json:
68
84
  git_wrapper = GitHubWrapper(user_name='user_name', repo_name='repo_name')
@@ -77,17 +93,32 @@ class GitHubWrapper:
77
93
  self.repo_name: str = repo_name
78
94
  self.repo_url: str = repo_url
79
95
  self.branch: str = branch
96
+ self.path: str = path
97
+ self.pat: str = pat
80
98
  self.branch_file_extension: str = branch_file_extension
81
99
 
82
100
  # Default variables.
83
- self.archive_directory: str = 'archive'
84
- self.branch_file_name: str = f'{self.branch}{self.branch_file_extension}'
101
+ if self.branch_file_extension == 'zip':
102
+ self.branch_type_directory: str = 'zipball'
103
+ elif self.branch_file_extension == 'tar.gz':
104
+ self.branch_type_directory: str = 'tarball'
105
+ else:
106
+ raise ValueError(f"Unsupported branch file extension: {self.branch_file_extension}")
107
+
85
108
  self.domain: str = 'github.com'
86
109
 
87
110
  # Initialize variables.
88
111
  self.branch_download_link: str = str()
89
112
  self.branch_downloaded_folder_name: str = str()
113
+ self.branch_file_name: str = str()
114
+ self.api_url: str = str()
90
115
  self.latest_release_json_url: str = str()
116
+ self.commits_url: str = str()
117
+ self.contents_url: str = str()
118
+
119
+ self.releases_url: str = str()
120
+ self.releases_per_page: int = 100
121
+ self.releases_starting_page: int = 1
91
122
 
92
123
  if self.user_name and self.repo_name and not self.repo_url:
93
124
  self.build_links_from_user_and_repo()
@@ -95,21 +126,35 @@ class GitHubWrapper:
95
126
  if self.repo_url and not self.user_name and not self.repo_name:
96
127
  self.build_links_from_repo_url()
97
128
 
129
+ def _get_headers(self) -> dict:
130
+ """
131
+ Returns headers for the GitHub API requests. If a personal access token (PAT) is provided, it adds the
132
+ 'Authorization' header.
133
+ """
134
+ headers = {}
135
+ if self.pat:
136
+ headers['Authorization'] = f'token {self.pat}'
137
+ return headers
138
+
98
139
  def build_links_from_user_and_repo(self, **kwargs):
99
140
  if not self.user_name or not self.repo_name:
100
- message = "'user_name' or 'repo_name' is empty."
101
- print_api(message, color="red", error_type=True, **kwargs)
141
+ raise ValueError("'user_name' or 'repo_name' is empty.")
102
142
 
103
143
  self.repo_url = f'https://{self.domain}/{self.user_name}/{self.repo_name}'
104
- self.branch_download_link = f'{self.repo_url}/{self.archive_directory}/{self.branch_file_name}'
105
144
  self.branch_downloaded_folder_name = f'{self.repo_name}-{self.branch}'
106
- self.latest_release_json_url: str = \
107
- f'https://api.{self.domain}/repos/{self.user_name}/{self.repo_name}/releases/latest'
145
+ self.branch_file_name: str = f'{self.repo_name}-{self.branch}.{self.branch_file_extension}'
146
+
147
+ self.api_url = f'https://api.{self.domain}/repos/{self.user_name}/{self.repo_name}'
148
+
149
+ self.latest_release_json_url: str = f'{self.api_url}/releases/latest'
150
+ self.releases_url: str = f'{self.api_url}/releases'
151
+ self.commits_url: str = f'{self.api_url}/commits'
152
+ self.contents_url: str = f'{self.api_url}/contents'
153
+ self.branch_download_link = f'{self.api_url}/{self.branch_type_directory}/{self.branch}'
108
154
 
109
155
  def build_links_from_repo_url(self, **kwargs):
110
156
  if not self.repo_url:
111
- message = "'repo_url' is empty."
112
- print_api(message, color="red", error_type=True, **kwargs)
157
+ raise ValueError("'repo_url' is empty.")
113
158
 
114
159
  repo_url_parsed = urls.url_parser(self.repo_url)
115
160
  self.check_github_domain(repo_url_parsed['netloc'])
@@ -118,50 +163,297 @@ class GitHubWrapper:
118
163
 
119
164
  self.build_links_from_user_and_repo()
120
165
 
121
- def check_github_domain(self, domain):
166
+ def check_github_domain(
167
+ self,
168
+ domain: str
169
+ ):
122
170
  if self.domain not in domain:
123
171
  print_api(
124
172
  f'This is not [{self.domain}] domain.', color="red", error_type=True)
125
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
+
126
263
  def download_and_extract_branch(
127
264
  self,
128
265
  target_directory: str,
129
266
  archive_remove_first_directory: bool = False,
267
+ download_each_file: bool = False,
130
268
  **kwargs
131
269
  ):
132
270
  """
133
271
  This function will download the branch file from GitHub, extract the file and remove the file, leaving
134
272
  only the extracted folder.
273
+ If the 'path' was specified during the initialization of the class, only the path will be downloaded.
135
274
 
136
- :param target_directory:
137
- :param archive_remove_first_directory: boolean, sets if archive extract function will extract the archive
275
+ :param target_directory: str, the target directory to download the branch/path.
276
+ :param archive_remove_first_directory: boolean, available only if 'path' was not specified during the initialization
277
+ Sets if archive extract function will extract the archive
138
278
  without first directory in the archive. Check reference in the
139
- 'archiver.zip.extract_archive_with_zipfile' function.
279
+ 'dkarchiver.arch_wrappers.zips.extract_archive_with_zipfile' function.
280
+ :param download_each_file: bool, available only if 'path' was specified during the initialization of the class.
281
+ Sets if each file will be downloaded separately.
282
+
283
+ True: Meaning the directory '/home/user/Downloads/files/' will be created and each file will be downloaded
284
+ ('file1.txt', 'file2.txt', 'file3.txt') separately to this directory.
285
+ False: The branch file will be downloaded to temp directory then the provided path
286
+ will be extracted from there, then the downloaded branch directory will be removed.
140
287
  :return:
141
288
  """
142
289
 
143
- # Download the repo to current working directory, extract and remove the archive.
144
- web.download_and_extract_file(
145
- file_url=self.branch_download_link,
146
- target_directory=target_directory,
147
- archive_remove_first_directory=archive_remove_first_directory,
148
- **kwargs)
290
+ headers: dict = self._get_headers()
291
+
292
+ if not download_each_file:
293
+ if self.path:
294
+ download_target_directory = tempfile.mkdtemp()
295
+ current_archive_remove_first_directory = True
296
+ else:
297
+ download_target_directory = target_directory
298
+ current_archive_remove_first_directory = archive_remove_first_directory
299
+
300
+ # Download the repo to current working directory, extract and remove the archive.
301
+ web.download_and_extract_file(
302
+ file_url=self.branch_download_link,
303
+ file_name=self.branch_file_name,
304
+ target_directory=download_target_directory,
305
+ archive_remove_first_directory=current_archive_remove_first_directory,
306
+ headers=headers,
307
+ **kwargs)
308
+
309
+ if self.path:
310
+ source_path: str = f"{download_target_directory}{os.sep}{self.path}"
311
+
312
+ if not archive_remove_first_directory:
313
+ target_directory = os.path.join(target_directory, self.path)
314
+ filesystem.create_directory(target_directory)
315
+
316
+ # Move the path to the target directory.
317
+ filesystem.move_folder_contents_to_folder(
318
+ source_path, target_directory)
319
+
320
+ # Remove the downloaded branch directory.
321
+ filesystem.remove_directory(download_target_directory)
322
+ else:
323
+ if archive_remove_first_directory:
324
+ current_target_directory = target_directory
325
+ else:
326
+ current_target_directory = os.path.join(target_directory, self.path)
327
+
328
+ self.download_directory(self.path, current_target_directory)
329
+
330
+ def get_releases_json(
331
+ self,
332
+ asset_pattern: str = None,
333
+ latest: bool = False,
334
+ per_page: int = None,
335
+ starting_page: int = None,
336
+ all_assets: bool = False,
337
+ ):
338
+ """
339
+ This function will get the releases json.
340
+ :param asset_pattern: str, the string pattern to search in the asset names of releases. Wildcards can be used.
341
+ If there is a match, the release will be added to the result list.
342
+ :param latest: bool, if True, will get only the latest release.
343
+ If 'asset_pattern' is provided, 'latest' will find the latest release matching the pattern.
344
+ Of course if you want to get it from all releases, you must set 'all_assets' to True.
345
+ :param per_page: int, the number of releases per page. Default is 100.
346
+ :param starting_page: int, the starting page number. Default is 1.
347
+ :param all_assets: bool, if True, will get all releases matching the pattern across all pages
348
+ OR all assets if no pattern is provided.
349
+ :return:
350
+ """
351
+
352
+ # If 'latest' is True and no 'asset_pattern' is provided, we only need to get 1 release from page 1.
353
+ # No need to get more assets than the first one.
354
+ if latest and not asset_pattern:
355
+ per_page = 1
356
+ starting_page = 1
357
+ all_assets = False
358
+ # In all other cases, get the releases according to the provided parameters or defaults.
359
+ else:
360
+ if not per_page:
361
+ per_page = self.releases_per_page
362
+
363
+ if not starting_page:
364
+ starting_page = self.releases_starting_page
365
+
366
+ headers: dict = self._get_headers()
367
+
368
+ params: dict = {
369
+ 'per_page': per_page,
370
+ 'page': starting_page
371
+ }
372
+
373
+ all_releases = []
374
+ while True:
375
+ response = requests.get(self.releases_url, headers=headers, params=params)
376
+ releases = response.json()
377
+ # If no releases found on current page, there will be none on the next as well, break the loop.
378
+ if not releases:
379
+ break
380
+
381
+ # If 'asset_pattern' is provided, filter releases to only those that have matching assets.
382
+ if asset_pattern:
383
+ for release in releases:
384
+ assets = release.get('assets', [])
385
+ matching_assets = [asset for asset in assets if fnmatch.fnmatch(asset.get('name', ''), asset_pattern)]
386
+ if matching_assets:
387
+ all_releases.append(release)
388
+
389
+ if latest:
390
+ return all_releases
391
+ else:
392
+ all_releases.extend(releases)
393
+
394
+ if not all_assets:
395
+ break
396
+
397
+ params['page'] += 1
398
+
399
+ return all_releases
400
+
401
+ def get_latest_release_json(
402
+ self,
403
+ asset_pattern: str = None
404
+ ) -> dict:
405
+ """
406
+ This function will get the latest releases json.
407
+ :param asset_pattern: str, the string pattern to search in the asset names of releases. Wildcards can be used.
408
+ If there is a match, the release will be added to the result list.
409
+ :return: dict, the latest release json.
410
+ """
411
+
412
+ if asset_pattern:
413
+ releases = self.get_releases_json(
414
+ asset_pattern=asset_pattern,
415
+ latest=True,
416
+ all_assets=True
417
+ )
418
+ else:
419
+ releases = self.get_releases_json(latest=True)
420
+
421
+ if not releases:
422
+ return {}
423
+ else:
424
+ return releases[0]
425
+
426
+ def get_latest_release_version(
427
+ self,
428
+ asset_pattern: str = None
429
+ ) -> str:
430
+ """
431
+ This function will get the latest release version number.
432
+
433
+ :param asset_pattern: str, the string pattern to search in the asset names of releases. Wildcards can be used.
434
+ If there is a match, the release will be added to the result list.
435
+ :return: str, the latest release version number.
436
+ """
437
+
438
+ latest_release_json: dict = self.get_latest_release_json(asset_pattern=asset_pattern)
439
+ latest_release_version: str = latest_release_json['tag_name']
440
+ return latest_release_version
149
441
 
150
442
  def get_latest_release_url(
151
443
  self,
152
- string_pattern: str,
444
+ asset_pattern: str,
153
445
  exclude_string: str = None,
154
446
  **kwargs):
155
447
  """
156
448
  This function will return the latest release url.
157
- :param string_pattern: str, the string pattern to search in the latest release. Wildcards can be used.
449
+ :param asset_pattern: str, the string pattern to search in the latest release. Wildcards can be used.
158
450
  :param exclude_string: str, the string to exclude from the search. No wildcards can be used.
159
451
  :param kwargs: dict, the print arguments for the 'print_api' function.
160
452
  :return: str, the latest release url.
161
453
  """
162
454
 
163
455
  # Get the 'assets' key of the latest release json.
164
- github_latest_releases_list = self.get_the_latest_release_json()['assets']
456
+ github_latest_releases_list = self.get_latest_release_json()['assets']
165
457
 
166
458
  # Get only download urls of the latest releases.
167
459
  download_urls: list = list()
@@ -174,44 +466,51 @@ class GitHubWrapper:
174
466
  if exclude_string in download_url:
175
467
  download_urls.remove(download_url)
176
468
 
177
- # Find urls against 'string_pattern'.
178
- found_urls = fnmatch.filter(download_urls, string_pattern)
469
+ # Find urls against 'asset_pattern'.
470
+ found_urls: list = fnmatch.filter(download_urls, asset_pattern)
179
471
 
180
472
  # If more than 1 url answer the criteria, we can't download it. The user must be more specific in his input
181
473
  # strings.
182
474
  if len(found_urls) > 1:
183
475
  message = f'More than 1 result found in JSON response, try changing search string or extension.\n' \
184
476
  f'{found_urls}'
185
- print_api(message, color="red", error_type=True, **kwargs)
186
-
187
- return found_urls[0]
477
+ raise MoreThanOneReleaseFoundError(message)
478
+ elif len(found_urls) == 0:
479
+ message = f'No result found in JSON response, try changing search string or extension.'
480
+ raise NoReleaseFoundError(message)
481
+ else:
482
+ return found_urls[0]
188
483
 
189
484
  def download_latest_release(
190
485
  self,
191
486
  target_directory: str,
192
- string_pattern: str,
487
+ asset_pattern: str,
193
488
  exclude_string: str = None,
194
- **kwargs):
489
+ **kwargs
490
+ ) -> str:
195
491
  """
196
492
  This function will download the latest release from the GitHub repository.
197
493
  :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.
494
+ :param asset_pattern: str, the string pattern to search in the latest release. Wildcards can be used.
199
495
  :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.
496
+ The 'excluded_string' will be filtered before the 'asset_pattern' entries.
201
497
  :param kwargs: dict, the print arguments for the 'print_api' function.
202
- :return:
498
+ :return: str, the downloaded file path.
203
499
  """
204
500
 
501
+ headers: dict = self._get_headers()
502
+
205
503
  # Get the latest release url.
206
- found_url = self.get_latest_release_url(string_pattern=string_pattern, exclude_string=exclude_string, **kwargs)
504
+ found_url = self.get_latest_release_url(asset_pattern=asset_pattern, exclude_string=exclude_string, **kwargs)
207
505
 
208
- downloaded_file_path = web.download(file_url=found_url, target_directory=target_directory, **kwargs)
506
+ downloaded_file_path = web.download(
507
+ file_url=found_url, target_directory=target_directory, headers=headers, **kwargs)
209
508
  return downloaded_file_path
210
509
 
211
510
  def download_and_extract_latest_release(
212
511
  self,
213
512
  target_directory: str,
214
- string_pattern: str,
513
+ asset_pattern: str,
215
514
  exclude_string: str = None,
216
515
  archive_remove_first_directory: bool = False,
217
516
  **kwargs):
@@ -219,35 +518,219 @@ class GitHubWrapper:
219
518
  This function will download the latest release from the GitHub repository, extract the file and remove the file,
220
519
  leaving only the extracted folder.
221
520
  :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.
521
+ :param asset_pattern: str, the string pattern to search in the latest release. Wildcards can be used.
223
522
  :param exclude_string: str, the string to exclude from the search. No wildcards can be used.
224
523
  :param archive_remove_first_directory: bool, sets if archive extract function will extract the archive
225
524
  without first directory in the archive. Check reference in the
226
- 'archiver.zip.extract_archive_with_zipfile' function.
525
+ 'dkarchiver.arch_wrappers.zips.extract_archive_with_zipfile' function.
227
526
  :param kwargs: dict, the print arguments for the 'print_api' function.
228
527
  :return:
229
528
  """
230
529
 
530
+ headers: dict = self._get_headers()
531
+
231
532
  # Get the latest release url.
232
- found_url = self.get_latest_release_url(string_pattern=string_pattern, exclude_string=exclude_string, **kwargs)
533
+ found_url = self.get_latest_release_url(asset_pattern=asset_pattern, exclude_string=exclude_string, **kwargs)
233
534
 
234
535
  web.download_and_extract_file(
235
536
  file_url=found_url,
236
537
  target_directory=target_directory,
237
538
  archive_remove_first_directory=archive_remove_first_directory,
539
+ headers=headers,
238
540
  **kwargs)
239
541
 
240
- def get_the_latest_release_json(self):
542
+ def get_latest_commit(self) -> dict:
241
543
  """
242
- This function will get the latest releases json.
243
- :return:
544
+ This function retrieves the latest commit on the specified branch.
545
+ It uses the GitHub API endpoint for commits.
546
+
547
+ :return: dict, the latest commit data.
244
548
  """
245
- response = requests.get(self.latest_release_json_url)
246
- return response.json()
247
549
 
248
- def get_the_latest_release_version_number(self):
550
+ headers: dict = self._get_headers()
551
+
552
+ # Use query parameters to filter commits by branch (sha) and limit results to 1
553
+ params: dict = {
554
+ 'sha': self.branch,
555
+ 'per_page': 1
556
+ }
557
+
558
+ if self.path:
559
+ params['path'] = self.path
560
+
561
+ response = requests.get(self.commits_url, headers=headers, params=params)
562
+ response.raise_for_status() # Raises an HTTPError if the HTTP request returned an unsuccessful status code.
563
+
564
+ commits = response.json()
565
+ if not commits:
566
+ return {}
567
+
568
+ latest_commit = commits[0]
569
+ return latest_commit
570
+
571
+ def get_latest_commit_message(self):
249
572
  """
250
- This function will get the latest release version number.
251
- :return:
573
+ This function retrieves the commit message (comment) of the latest commit on the specified branch.
574
+ It uses the GitHub API endpoint for commits.
575
+
576
+ :return: str, the commit message of the latest commit.
577
+ """
578
+
579
+ latest_commit: dict = self.get_latest_commit()
580
+
581
+ commit_message = latest_commit.get("commit", {}).get("message", "")
582
+ return commit_message
583
+
584
+ def list_files(
585
+ self,
586
+ pattern: str = "*",
587
+ recursive: bool = True,
588
+ path: str | None = None,
589
+ ) -> list[str]:
590
+ """
591
+ List files in the repository (or in a specific subfolder).
592
+
593
+ :param pattern: Glob-style pattern (e.g., "*.ex*", "*test*.py"). Matching is done
594
+ against the file's base name (not the full path).
595
+ :param recursive: If True, include files in all subfolders (returns full repo-relative
596
+ paths). If False, list only the immediate files in the chosen folder.
597
+ :param path: Optional subfolder to list from (e.g., "tests/langs"). If omitted,
598
+ uses self.path if set, otherwise the repo root.
599
+
600
+ :return: A list of repo-relative file paths that match the pattern.
252
601
  """
253
- return self.get_the_latest_release_json()['tag_name']
602
+ headers = self._get_headers()
603
+ base_path = (path or self.path or "").strip("/")
604
+
605
+ if recursive:
606
+ # Use the Git Trees API to fetch all files in one call, then filter.
607
+ tree_url = f"{self.api_url}/git/trees/{self.branch}"
608
+ params = {"recursive": "1"}
609
+ resp = requests.get(tree_url, headers=headers, params=params)
610
+ resp.raise_for_status()
611
+ data = resp.json()
612
+
613
+ files = []
614
+ for entry in data.get("tree", []):
615
+ if entry.get("type") != "blob":
616
+ continue # only files
617
+ entry_path = entry.get("path", "")
618
+ # If a base_path was provided, keep only files under it
619
+ if base_path and not entry_path.startswith(base_path + "/") and entry_path != base_path:
620
+ continue
621
+ # Match pattern against the *file name* (basename)
622
+ if fnmatch.fnmatch(os.path.basename(entry_path), pattern):
623
+ files.append(entry_path)
624
+ return files
625
+
626
+ else:
627
+ # Non-recursive: use the Contents API to list a single directory.
628
+ # If base_path is empty, list the repo root.
629
+ if base_path:
630
+ contents_url = f"{self.contents_url}/{base_path}"
631
+ else:
632
+ contents_url = self.contents_url
633
+
634
+ params = {"ref": self.branch}
635
+ resp = requests.get(contents_url, headers=headers, params=params)
636
+ resp.raise_for_status()
637
+ items = resp.json()
638
+
639
+ # The Contents API returns a dict when the path points to a single file;
640
+ # normalize to a list to simplify handling.
641
+ if isinstance(items, dict):
642
+ items = [items]
643
+
644
+ files = []
645
+ for item in items:
646
+ if item.get("type") == "file":
647
+ name = item.get("name", "")
648
+ if fnmatch.fnmatch(name, pattern):
649
+ # item["path"] is the full repo-relative path we want to return
650
+ files.append(item.get("path", name))
651
+ return files
652
+
653
+
654
+ def _make_parser():
655
+ import argparse
656
+
657
+ parser = argparse.ArgumentParser(description='GitHub Wrapper')
658
+ parser.add_argument(
659
+ '-u', '--repo_url', type=str, required=True,
660
+ help='The repository url. Example: https://github.com/{user_name}/{repo_name}')
661
+ parser.add_argument(
662
+ '-b', '--branch', type=str, required=True,
663
+ help='The branch name. The specific branch from the repo you want to operate on.')
664
+ parser.add_argument(
665
+ '-p', '--path', type=str, default=None,
666
+ help="The path to the file/folder inside the repo that we'll do certain actions on.\n"
667
+ "Available actions: get_latest_commit_message, download_path_from_branch.")
668
+ parser.add_argument(
669
+ '-t', '--target_directory', type=str, default=None,
670
+ help='The target directory to download the file/folder.'
671
+ )
672
+ parser.add_argument(
673
+ '--pat', type=str, default=None,
674
+ help='The personal access token to the repo.')
675
+ parser.add_argument(
676
+ '-glcm', '--get_latest_commit_message', action='store_true', default=False,
677
+ help='Sets if the latest commit message comment will be printed.')
678
+ parser.add_argument(
679
+ '-glcj', '--get_latest_commit_json', action='store_true', default=False,
680
+ help='Sets if the latest commit json will be printed.')
681
+ parser.add_argument(
682
+ '-db', '--download_branch', action='store_true', default=False,
683
+ help='Sets if the branch will be downloaded. In conjunction with path, only the path will be downloaded.')
684
+
685
+ return parser
686
+
687
+
688
+ def github_wrapper_main(
689
+ repo_url: str,
690
+ branch: str,
691
+ path: str = None,
692
+ target_directory: str = None,
693
+ pat: str = None,
694
+ get_latest_commit_message: bool = False,
695
+ get_latest_commit_json: bool = False,
696
+ download_branch: bool = False
697
+ ):
698
+ """
699
+ This function is the main function for the GitHubWrapper class.
700
+ :param repo_url: str, the repository url.
701
+ Example: https://github.com/{user_name}/{repo_name}
702
+ :param branch: str, the branch name. The specific branch from the repo you want to operate on.
703
+ :param path: str, the path to the file/folder for which the commit message should be retrieved.
704
+ :param target_directory: str, the target directory to download the file/folder.
705
+ :param pat: str, the personal access token to the repo.
706
+ :param get_latest_commit_json: bool, sets if the latest commit json will be printed.
707
+ :param get_latest_commit_message: bool, sets if the latest commit message comment will be printed.
708
+ :param download_branch: bool, sets if the branch will be downloaded. In conjunction with path, only the path will be
709
+ downloaded.
710
+ :return:
711
+ """
712
+
713
+ git_wrapper = GitHubWrapper(repo_url=repo_url, branch=branch, path=path, pat=pat)
714
+
715
+ if get_latest_commit_message:
716
+ commit_comment = git_wrapper.get_latest_commit_message()
717
+ print_api(commit_comment)
718
+ return 0
719
+
720
+ if get_latest_commit_json:
721
+ latest_commit_json = git_wrapper.get_latest_commit()
722
+ print_api(latest_commit_json)
723
+ return 0
724
+
725
+ if download_branch:
726
+ git_wrapper.download_and_extract_branch(
727
+ target_directory=target_directory, download_each_file=False, archive_remove_first_directory=True)
728
+
729
+ return 0
730
+
731
+
732
+ def github_wrapper_main_with_args():
733
+ main_parser = _make_parser()
734
+ args = main_parser.parse_args()
735
+
736
+ return github_wrapper_main(**vars(args))