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
atomicshop/filesystem.py CHANGED
@@ -9,12 +9,12 @@ from contextlib import contextmanager
9
9
  from typing import Literal, Union
10
10
  import tempfile
11
11
 
12
+ # noinspection PyPackageRequirements
12
13
  import psutil
13
14
 
14
- from .print_api import print_api, print_status_of_list
15
- from .basics import strings, list_of_dicts
15
+ from .basics import strings, list_of_dicts, list_of_classes
16
16
  from .file_io import file_io
17
- from . import hashing, datetimes
17
+ from . import hashing, datetimes, print_api
18
18
 
19
19
 
20
20
  WINDOWS_DIRECTORY_SPECIAL_CHARACTERS = ['<', '>', ':', '"', '/', '\\', '|', '?', '*']
@@ -65,14 +65,18 @@ FILE_NAME_REPLACEMENT_DICT: dict = {
65
65
  }
66
66
 
67
67
 
68
+ # class TimeCouldNotBeFoundInFileNameError(Exception):
69
+ # pass
70
+
71
+
68
72
  def get_home_directory(return_sudo_user: bool = False) -> str:
69
73
  """
70
74
  Returns the home directory of the current user or the user who invoked sudo.
71
75
 
72
76
  :param return_sudo_user: bool, if 'False', then the function will return the home directory of the user who invoked
73
77
  sudo (if the script was invoked with sudo).
74
- If 'True', then the function will return the home directory of the current user, doesn't matter if the script was
75
- invoked with sudo or not, if so home directory of the sudo user will be returned.
78
+ If 'True', then the function will return the home directory of the current user, doesn't matter if the script
79
+ was invoked with sudo or not, if so home directory of the sudo user will be returned.
76
80
  """
77
81
 
78
82
  def return_home_directory_of_current_user():
@@ -209,16 +213,6 @@ def get_list_of_directories_in_file_path(
209
213
  return directory_list
210
214
 
211
215
 
212
- def get_file_name(file_path: str) -> str:
213
- """
214
- The function will return file name of the file.
215
-
216
- :param file_path: string, full file path.
217
- :return: string.
218
- """
219
- return str(Path(file_path).name)
220
-
221
-
222
216
  def check_absolute_path(filesystem_path) -> bool:
223
217
  """
224
218
  The function checks if the path provided is a full path (absolute) or relative.
@@ -240,21 +234,11 @@ def check_absolute_path___add_full(filesystem_path: str, full_path_to_add: str)
240
234
  """
241
235
 
242
236
  if not check_absolute_path(filesystem_path):
243
- return f'{full_path_to_add}{os.sep}{remove_first_separator(filesystem_path)}'
237
+ return f'{full_path_to_add}{os.sep}{remove_last_separator(filesystem_path)}'
244
238
  else:
245
239
  return filesystem_path
246
240
 
247
241
 
248
- def check_file_existence(file_path: str) -> bool:
249
- """This will be removed in future versions. Use 'is_file_exists' instead."""
250
- return is_file_exists(file_path)
251
-
252
-
253
- def check_directory_existence(directory_path: str) -> bool:
254
- """This will be removed in future versions. Use 'is_directory_exists' instead."""
255
- return is_directory_exists(directory_path)
256
-
257
-
258
242
  def is_file_exists(file_path: str) -> bool:
259
243
  """
260
244
  Function to check if the path is a file.
@@ -295,16 +279,20 @@ def remove_file(file_path: str, **kwargs) -> bool:
295
279
 
296
280
  try:
297
281
  os.remove(file_path)
298
- print_api(f'File Removed: {file_path}')
282
+ print_api.print_api(f'File Removed: {file_path}')
299
283
  return True
300
284
  # Since the file doesn't exist, we actually don't care, since we want to remove it anyway.
301
285
  except FileNotFoundError:
302
286
  message = f'File Removal Failed, File non-existent: {file_path}'
303
- print_api(message, error_type=True, logger_method='critical', **kwargs)
287
+ print_api.print_api(message, error_type=True, logger_method='critical', **kwargs)
304
288
  return False
305
289
 
306
290
 
307
- def remove_directory(directory_path: str, force_readonly: bool = False, print_kwargs: dict = None) -> bool:
291
+ def remove_directory(
292
+ directory_path: str,
293
+ force_readonly: bool = False,
294
+ print_kwargs: dict = None
295
+ ) -> bool:
308
296
  """
309
297
  Remove directory if it exists.
310
298
 
@@ -336,15 +324,69 @@ def remove_directory(directory_path: str, force_readonly: bool = False, print_kw
336
324
  shutil.rmtree(directory_path, onerror=remove_readonly)
337
325
  else:
338
326
  shutil.rmtree(directory_path)
339
- print_api(f'Directory Removed: {directory_path}', **print_kwargs)
327
+ print_api.print_api(f'Directory Removed: {directory_path}', **print_kwargs)
340
328
  return True
341
329
  # Since the directory doesn't exist, we actually don't care, since we want to remove it anyway.
342
330
  except FileNotFoundError:
343
331
  message = f'Directory Removal Failed, Directory non-existent: {directory_path}'
344
- print_api(message, error_type=True, logger_method='critical', **print_kwargs)
332
+ print_api.print_api(message, error_type=True, logger_method='critical', **print_kwargs)
345
333
  return False
346
334
 
347
335
 
336
+ def clear_directory(directory: str) -> tuple[list[str], list[str]]:
337
+ """
338
+ The function will clear the directory of all files and subdirectories.
339
+ :param directory:
340
+ :return: tuple of lists of removed file paths and removed directory paths.
341
+ """
342
+
343
+ file_paths: list = []
344
+ directory_paths: list = []
345
+ # Iterate through all files and subdirectories in the directory
346
+ for item in os.listdir(directory):
347
+ item_path = os.path.join(directory, item)
348
+ # If it's a file, remove it
349
+ if os.path.isfile(item_path) or os.path.islink(item_path): # Handle symbolic links too
350
+ os.remove(item_path)
351
+ file_paths.append(item_path)
352
+ # If it's a directory, remove it and its contents
353
+ elif os.path.isdir(item_path):
354
+ shutil.rmtree(item_path)
355
+ directory_paths.append(item_path)
356
+
357
+ return file_paths, directory_paths
358
+
359
+
360
+ def remove_empty_directories(directory_path: str) -> list[str]:
361
+ """
362
+ Recursively removes empty directories in the given path, including the given path if it is empty.
363
+
364
+ :param directory_path: The starting directory path to check and remove empty directories.
365
+ """
366
+ if not os.path.isdir(directory_path):
367
+ # print(f"Path '{directory_path}' is not a directory or does not exist.")
368
+ return []
369
+
370
+ removed_directories: list = []
371
+ # Iterate through the directory contents
372
+ for root, dirs, files in os.walk(directory_path, topdown=False):
373
+ for directory in dirs:
374
+ dir_path = os.path.join(root, directory)
375
+ # Check if the directory is empty
376
+ if not os.listdir(dir_path):
377
+ os.rmdir(dir_path)
378
+ removed_directories.append(dir_path)
379
+ # print(f"Removed empty directory: {dir_path}")
380
+
381
+ # Finally, check if the top-level directory is empty
382
+ if not os.listdir(directory_path):
383
+ os.rmdir(directory_path)
384
+ removed_directories.append(directory_path)
385
+ # print(f"Removed top-level empty directory: {path}")
386
+
387
+ return removed_directories
388
+
389
+
348
390
  def create_directory(directory_fullpath: str):
349
391
  # Create directory if non-existent.
350
392
  # The library is used to create folder if it doesn't exist and won't raise exception if it does
@@ -353,41 +395,143 @@ def create_directory(directory_fullpath: str):
353
395
  pathlib.Path(directory_fullpath).mkdir(parents=True, exist_ok=True)
354
396
 
355
397
 
356
- def rename_file(source_file_path: str, target_file_path: str) -> None:
398
+ def rename_file(file_path: str, new_file_name: str) -> None:
357
399
  """
358
400
  The function renames file from source to target.
359
401
 
360
- :param source_file_path: string, full path to source file.
361
- :param target_file_path: string, full path to target file.
402
+ :param file_path: string, full path to file that will be renamed.
403
+ :param new_file_name: string, new name of the file. No path should be included.
362
404
 
363
405
  :return: None
364
406
  """
365
407
 
408
+ renamed_file_path = str(Path(file_path).parent) + os.sep + new_file_name
409
+
366
410
  # Rename file.
367
- os.rename(source_file_path, target_file_path)
411
+ os.rename(file_path, renamed_file_path)
368
412
 
369
413
 
370
- @contextmanager
371
- def temporary_rename(file_path: str, temp_file_path) -> None:
414
+ def rename_file_with_special_characters(
415
+ file_path: str,
416
+ rename_dictionary: dict = None,
417
+ ) -> str:
372
418
  """
373
- The function will rename the file to temporary name and then rename it back to original name.
419
+ The function will rename the file to replace special characters from the file name with '_'.
420
+ If the file already exists, then the function will add a number to the end of the file name.
374
421
 
375
422
  :param file_path: string, full path to file.
376
- :param temp_file_path: string, temporary name to rename the file to.
377
- :return: None.
423
+ :param rename_dictionary: dictionary, with special characters to replace.
424
+ If not specified, the default dictionary will be used: FILE_NAME_REPLACEMENT_DICT.
425
+ Example:
426
+ rename_dictionary = {
427
+ '$': '_',
428
+ ' ': '_'
429
+ }
430
+ :return: string, full path to file with special characters renamed.
431
+ """
432
+
433
+ if rename_dictionary is None:
434
+ rename_dictionary = FILE_NAME_REPLACEMENT_DICT
435
+
436
+ # Get the file name without extension
437
+ file_stem: str = str(Path(file_path).stem)
438
+ file_extension: str = str(Path(file_path).suffix)
439
+
440
+ # Remove special characters from the file name
441
+ new_file_stem = strings.replace_strings_with_values_from_dict(
442
+ string_to_replace=file_stem, dictionary=rename_dictionary)
443
+
444
+ # Rename the file
445
+ renamed_file_path = str(Path(file_path).parent) + os.sep + new_file_stem + file_extension
446
+
447
+ counter: int = 1
448
+ if os.path.isfile(renamed_file_path):
449
+ while True:
450
+ new_file_stem = f'{new_file_stem}_{counter}'
451
+ renamed_file_path = str(Path(file_path).parent) + os.sep + new_file_stem + file_extension
452
+ if not os.path.isfile(renamed_file_path):
453
+ break
454
+ counter += 1
455
+
456
+ os.rename(file_path, renamed_file_path)
457
+
458
+ return renamed_file_path
459
+
460
+
461
+ def rename_files_and_directories_with_special_characters(
462
+ base_path: str,
463
+ rename_dictionary: dict = None
464
+ ) -> None:
465
+ """
466
+ Recursively renames all files and directories in the given directory to rename special characters.
378
467
 
379
- Usage:
380
- original_file = 'example.txt'
381
- temporary_file = 'temp_example.txt'
468
+ :param base_path: str, the base directory to start processing.
469
+ :param rename_dictionary: dictionary, with special characters to replace.
470
+ If not specified, the default dictionary will be used: FILE_NAME_REPLACEMENT_DICT.
471
+ Example:
472
+ rename_dictionary = {
473
+ '$': '_',
474
+ ' ': '_'
475
+ }
476
+ """
477
+
478
+ def sanitize_name(name: str) -> str:
479
+ nonlocal rename_dictionary
480
+ """
481
+ Helper function to replace special characters in a string using a dictionary.
482
+ """
483
+ for key, value in rename_dictionary.items():
484
+ name = name.replace(key, value)
485
+ return name
382
486
 
383
- with temporary_rename(original_file, temporary_file):
384
- # Inside this block, the file exists as 'temp_example.txt'
385
- print(f"File is temporarily renamed to {temporary_file}")
386
- # Perform operations with the temporarily named file here
487
+ if rename_dictionary is None:
488
+ rename_dictionary = FILE_NAME_REPLACEMENT_DICT
387
489
 
388
- # Outside the block, it's back to 'example.txt'
389
- print(f"File is renamed back to {original_file}")
490
+ # Walk through the directory tree in reverse to ensure we rename files before directories
491
+ for root, dirs, files in os.walk(base_path, topdown=False):
492
+ # Rename files in the current directory
493
+ for file_name in files:
494
+ old_path = Path(root) / file_name
495
+ sanitized_name = sanitize_name(file_name)
496
+ new_path = Path(root) / sanitized_name
497
+
498
+ if sanitized_name != file_name: # Rename only if the name changed
499
+ # print(f"Renaming file: {old_path} -> {new_path}")
500
+ os.rename(old_path, new_path)
501
+
502
+ # Rename directories in the current directory
503
+ for dir_name in dirs:
504
+ old_path = Path(root) / dir_name
505
+ sanitized_name = sanitize_name(dir_name)
506
+ new_path = Path(root) / sanitized_name
507
+
508
+ if sanitized_name != dir_name: # Rename only if the name changed
509
+ # print(f"Renaming directory: {old_path} -> {new_path}")
510
+ os.rename(old_path, new_path)
511
+
512
+
513
+ @contextmanager
514
+ def temporary_rename(file_path: str, temp_file_path) -> None:
515
+ # noinspection GrazieInspection
390
516
  """
517
+ The function will rename the file to temporary name and then rename it back to original name.
518
+
519
+ :param file_path: string, full path to file.
520
+ :param temp_file_path: string, temporary name to rename the file to.
521
+ :return: None.
522
+
523
+ Usage:
524
+ original_file = 'example.txt'
525
+ temporary_file = 'temp_example.txt'
526
+
527
+ with temporary_rename(original_file, temporary_file):
528
+ # Inside this block, the file exists as 'temp_example.txt'
529
+ print(f"File is temporarily renamed to {temporary_file}")
530
+ # Perform operations with the temporarily named file here
531
+
532
+ # Outside the block, it's back to 'example.txt'
533
+ print(f"File is renamed back to {original_file}")
534
+ """
391
535
 
392
536
  original_name = file_path
393
537
  try:
@@ -436,17 +580,33 @@ def temporary_change_working_directory(new_working_directory: str) -> None:
436
580
  os.chdir(original_working_directory)
437
581
 
438
582
 
439
- def move_file(source_file_path: str, target_file_path: str, overwrite: bool = True) -> None:
583
+ def move_file(source_file_path: str, target_directory: str, overwrite: bool = True) -> None:
440
584
  """
441
585
  The function moves file from source to target.
442
586
 
443
587
  :param source_file_path: string, full path to source file.
444
- :param target_file_path: string, full path to target file.
588
+ :param target_directory: string, full path to target directory.
445
589
  :param overwrite: boolean, if 'False', then the function will not overwrite the file if it exists.
446
590
 
591
+ Example:
592
+ Before move:
593
+ Source file = 'C:/Users/user1/Downloads/file-to-move.txt'
594
+ Move to directory = 'C:/Users/user1/Documents'
595
+
596
+ Move:
597
+ source_file_path = 'C:/Users/user1/Downloads/file-to-move.txt'
598
+ target_directory = 'C:/Users/user1/Documents'
599
+ move_file(source_file_path, target_file_path)
600
+
601
+ After move:
602
+ 'C:/Users/user1/Downloads'
603
+ 'C:/Users/user1/Documents/file-to-move.txt'
604
+
447
605
  :return: None
448
606
  """
449
607
 
608
+ target_file_path = target_directory + os.sep + Path(source_file_path).name
609
+
450
610
  # Check if 'no_overwrite' is set to 'True' and if the file exists.
451
611
  if not overwrite:
452
612
  if is_file_exists(target_file_path):
@@ -469,12 +629,17 @@ def move_folder(source_directory: str, target_directory: str, overwrite: bool =
469
629
  ------------------------------
470
630
 
471
631
  Example:
472
- source_directory = 'C:/Users/user1/Downloads/folder-to-move'
473
- target_directory = 'C:/Users/user1/Documents'
474
- move_folder(source_directory, target_directory)
632
+ Before move:
633
+ Source folder = 'C:/Users/user1/Downloads/folder-to-move'
634
+ Move to directory = 'C:/Users/user1/Documents'
635
+
636
+ Move:
637
+ source_directory = 'C:/Users/user1/Downloads/folder-to-move'
638
+ target_directory = 'C:/Users/user1/Documents'
639
+ move_folder(source_directory, target_directory)
475
640
 
476
- Result path of the 'folder-to-move' will be:
477
- 'C:/Users/user1/Documents/folder-to-move'
641
+ Result path of the 'folder-to-move' will be:
642
+ 'C:/Users/user1/Documents/folder-to-move'
478
643
 
479
644
  """
480
645
 
@@ -487,14 +652,14 @@ def move_folder(source_directory: str, target_directory: str, overwrite: bool =
487
652
  shutil.move(source_directory, target_directory)
488
653
 
489
654
 
490
- def move_files_from_folder_to_folder(
655
+ def move_top_level_files_from_folder_to_folder(
491
656
  source_directory: str,
492
657
  target_directory: str,
493
658
  overwrite: bool = True
494
659
  ):
495
660
  """
496
- The function is currently non-recursive and not tested with directories inside the source directories.
497
- The function will move all the files from source directory to target directory overwriting existing files.
661
+ The function is non-recursive to move only top level files from source directory to target directory
662
+ overwriting existing files.
498
663
 
499
664
  :param source_directory: string, full path to source directory.
500
665
  :param target_directory: string, full path to target directory.
@@ -502,26 +667,50 @@ def move_files_from_folder_to_folder(
502
667
  """
503
668
 
504
669
  # Iterate over each item in the source directory
505
- for item in os.listdir(source_directory):
506
- # Construct full file path
507
- source_item = os.path.join(source_directory, item)
508
- destination_item = os.path.join(target_directory, item)
670
+ top_level_files: list[str] = get_paths_from_directory(
671
+ directory_path=source_directory, get_file=True, recursive=False, simple_list=True)
509
672
 
673
+ for source_item in top_level_files:
510
674
  # Move each item to the destination directory
511
- move_file(source_file_path=source_item, target_file_path=destination_item, overwrite=overwrite)
675
+ move_file(source_file_path=source_item, target_directory=target_directory, overwrite=overwrite)
512
676
 
513
- # # Get all file names without full paths in source folder.
514
- # file_list_in_source: list = get_file_paths_from_directory(source_directory)
515
- #
516
- # # Iterate through all the files.
517
- # for file_path in file_list_in_source:
518
- # # Move the file from source to target.
519
- # if file_path['relative_dir']:
520
- # create_directory(target_directory + os.sep + file_path['relative_dir'])
521
- # relative_file_path: str = file_path['relative_dir'] + os.sep + Path(file_path['path']).name
522
- # else:
523
- # relative_file_path: str = Path(file_path['path']).name
524
- # move_file(file_path['path'], target_directory + os.sep + relative_file_path)
677
+
678
+ def move_folder_contents_to_folder(
679
+ source_directory: str,
680
+ target_directory: str,
681
+ overwrite: bool = True
682
+ ):
683
+ """
684
+ The function moves all the contents of the source directory to the target directory.
685
+ If target directory is inside the source directory, this folder will be skipped.
686
+
687
+ :param source_directory: string, full path to source directory.
688
+ :param target_directory: string, full path to target directory.
689
+ :param overwrite: boolean, if 'True', then the function will overwrite the files if they exist.
690
+ """
691
+
692
+ # Make sure the destination directory exists, if not create it
693
+ os.makedirs(target_directory, exist_ok=True)
694
+
695
+ # Move contents of the source directory to the destination directory
696
+ for item in os.listdir(source_directory):
697
+ s = os.path.join(source_directory, item)
698
+ d = os.path.join(target_directory, item)
699
+
700
+ # if the target directory is inside the source directory, skip it
701
+ if os.path.abspath(target_directory).startswith(os.path.abspath(s)):
702
+ continue
703
+
704
+ if os.path.isdir(s):
705
+ if os.path.exists(d) and not overwrite:
706
+ raise FileExistsError(f"Directory already exists: {d}. Skipping due to overwrite=False.")
707
+ else:
708
+ shutil.move(s, d)
709
+ else:
710
+ if os.path.exists(d) and not overwrite:
711
+ raise FileExistsError(f"File {d} already exists. Skipping due to overwrite=False.")
712
+ else:
713
+ shutil.move(s, d)
525
714
 
526
715
 
527
716
  def copy_file(
@@ -601,49 +790,101 @@ def copy_files_from_folder_to_folder(source_directory: str, target_directory: st
601
790
  shutil.copy2(s, d)
602
791
 
603
792
 
604
- def get_directory_paths_from_directory(
605
- directory_path: str,
606
- recursive: bool = True
607
- ) -> list:
608
- """
609
- Recursive, by option.
610
- The function receives a filesystem directory as string, scans it recursively for directories and returns list of
611
- full paths to that directory (including).
612
-
613
- :param directory_path: string to full path to directory on the filesystem to scan.
614
- :param recursive: boolean.
615
- 'True', then the function will scan recursively in subdirectories.
616
- 'False', then the function will scan only in the directory that was passed.
617
-
618
- :return: list of all found directory names with full paths.
619
- """
620
-
621
- # Define locals.
622
- directory_list: list = list()
623
-
624
- # "Walk" over all the directories and subdirectories - make list of full directory paths inside the directory
625
- # recursively.
626
- for dirpath, subdirs, files in os.walk(directory_path):
627
- # Iterate through all the directory names that were found in the folder.
628
- for directory in subdirs:
629
- # Get full directory path.
630
- directory_list.append(os.path.join(dirpath, directory))
631
-
632
- if not recursive:
633
- break
634
-
635
- return directory_list
793
+ class AtomicPath:
794
+ def __init__(self, path: str):
795
+ self.path: str = path
796
+
797
+ self.is_file: bool = os.path.isfile(path)
798
+ self.is_directory: bool = os.path.isdir(path)
799
+ self.name: str = Path(path).name
800
+
801
+ self.queried_directory: str = ''
802
+ # noinspection PyTypeChecker
803
+ self.last_modified: float = None
804
+ self.relative_dir: str = ''
805
+ self.binary: bytes = b''
806
+ self.hash: str = ''
807
+ self.datetime_datetime = None
808
+ self.datetime_string: str = ''
809
+ # noinspection PyTypeChecker
810
+ self.datetime_float: float = None
811
+ self.datetime_format: str = ''
812
+
813
+ def __str__(self):
814
+ return self.path
815
+
816
+ def update(
817
+ self,
818
+ path: str = None,
819
+ datetime_format: str = None,
820
+ update_datetime: bool = False,
821
+ update_last_modified: bool = False,
822
+ update_binary: bool = False,
823
+ update_hash: bool = False
824
+ ):
825
+ if path:
826
+ if path != self.path:
827
+ self.queried_directory = ''
828
+ self.last_modified = None
829
+ self.relative_dir = ''
830
+ self.binary = b''
831
+ self.hash = ''
832
+ self.datetime_datetime = None
833
+ self.datetime_string = ''
834
+ self.datetime_float = None
835
+ self.datetime_format = ''
836
+
837
+ self.path = path
838
+ self.is_file = os.path.isfile(path)
839
+ self.is_directory = os.path.isdir(path)
840
+ self.name = Path(path).name
841
+
842
+ # Update the datetime format only if it is provided without the update_datetime boolean.
843
+ # Since, we don't want this variable if there is no relation between the datetime format and the filename.
844
+ # If the user want to put it manually, then we will not stop him, but this case is useless if filename
845
+ # doesn't contain the datetime.
846
+ if datetime_format and not update_datetime:
847
+ self.datetime_format = datetime_format
848
+
849
+ if update_datetime and not datetime_format and not self.datetime_format:
850
+ raise ValueError('If "update_datetime" is True, then "datetime_format" must be provided.')
851
+
852
+ if update_datetime:
853
+ self.datetime_datetime, self.datetime_string, self.datetime_float = (
854
+ datetimes.get_datetime_from_complex_string_by_pattern(self.name, datetime_format))
855
+ # If the provided datetime format is correct, then we will update the datetime format.
856
+ if self.datetime_string:
857
+ self.datetime_format = datetime_format
858
+
859
+ if update_last_modified:
860
+ self.last_modified = get_file_modified_time(self.path)
861
+
862
+ if update_binary:
863
+ self.binary = file_io.read_file(self.path, file_mode='rb', stdout=False)
864
+
865
+ if update_hash:
866
+ if self.binary:
867
+ self.hash = hashing.hash_bytes(self.binary)
868
+ else:
869
+ self.hash = hashing.hash_file(self.path)
636
870
 
637
871
 
638
- def get_file_paths_from_directory(
872
+ def get_paths_from_directory(
639
873
  directory_path: str,
874
+ simple_list: bool = False,
875
+ get_file: bool = False,
876
+ get_directory: bool = False,
640
877
  recursive: bool = True,
641
878
  file_name_check_pattern: str = '*',
879
+ datetime_format: str = None,
880
+ specific_date: str = None,
642
881
  add_relative_directory: bool = False,
643
882
  relative_file_name_as_directory: bool = False,
644
883
  add_last_modified_time: bool = False,
645
- sort_by_last_modified_time: bool = False
646
- ):
884
+ sort_by_last_modified_time: bool = False,
885
+ add_file_binary: bool = False,
886
+ add_file_hash: bool = False,
887
+ ) -> Union[list[AtomicPath], list[str]]:
647
888
  """
648
889
  Recursive, by option.
649
890
  The function receives a filesystem directory as string, scans it recursively for files and returns list of
@@ -652,15 +893,23 @@ def get_file_paths_from_directory(
652
893
  of that tuple.
653
894
 
654
895
  :param directory_path: string to full path to directory on the filesystem to scan.
896
+ :param simple_list: boolean, if 'True', then the function will return only full file paths.
897
+ :param get_file: boolean, if 'True', then the function will return files.
898
+ :param get_directory: boolean, if 'True', then the function will return directories.
655
899
  :param recursive: boolean.
656
900
  'True', then the function will scan recursively in subdirectories.
657
901
  'False', then the function will scan only in the directory that was passed.
658
902
  :param file_name_check_pattern: string, if specified, the function will return only files that match the pattern.
659
903
  The string can contain part of file name to check or full file name with extension.
660
904
  Can contain wildcards.
661
- If you need to specify a "." in the pattern, you need to escape it with a backslash:
662
- Example: "*\.txt" will return all files with the extension ".txt".
663
- While "*.txt" will return all files that contain "txt" in the name.
905
+ :param datetime_format: datetime format string pattern to match the date in the file name.
906
+ If specified, the function will get the files by the date pattern.
907
+
908
+ Example:
909
+ datetime_format = '%Y-%m-%d'
910
+ :param specific_date: Specific date to get the file path.
911
+ If specified, the function will get the file by the specific date.
912
+ Meaning that 'datetime_format' must be specified.
664
913
  :param add_relative_directory: boolean, if
665
914
  'True', then the function will add relative directory to the output list.
666
915
  In this case the output list will contain dictionaries with keys 'path' and 'relative_dir'.
@@ -672,47 +921,72 @@ def get_file_paths_from_directory(
672
921
  to the output list.
673
922
  :param sort_by_last_modified_time: boolean, if 'True', then the function will sort the output list by last
674
923
  modified time of the file.
924
+ :param add_file_binary: boolean, if 'True', then the function will add binary content of the file to each file
925
+ object of the output list.
926
+ :param add_file_hash: boolean, if 'True', then the function will add hash of the file to each file object of the
927
+ output list.
675
928
 
676
929
  :return: list of all found filenames with full file paths, list with relative folders to file excluding the
677
930
  main folder.
678
931
  """
679
932
 
680
- def get_file():
933
+ def get_path(file_or_directory: str):
681
934
  """
682
935
  Function gets the full file path, adds it to the found 'object_list' and gets the relative path to that
683
936
  file, against the main path to directory that was passed to the parent function.
684
937
  """
685
938
 
686
- file_path: str = os.path.join(dirpath, file)
939
+ if strings.match_pattern_against_string(file_name_check_pattern, file_or_directory):
940
+ file_or_dir_path: str = os.path.join(dir_path, file_or_directory)
687
941
 
688
- if not add_relative_directory and not add_last_modified_time:
689
- file_result: str = file_path
690
- else:
691
- file_result: dict = dict()
942
+ if simple_list:
943
+ object_list.append(file_or_dir_path)
944
+ return
692
945
 
693
- # Get full file path of the file.
694
- file_result['file_path'] = file_path
946
+ path_object: AtomicPath = AtomicPath(path=file_or_dir_path)
947
+ path_object.queried_directory = directory_path
695
948
 
696
949
  if add_relative_directory:
697
950
  # if 'relative_file_name_as_directory' was passed.
698
951
  if relative_file_name_as_directory:
699
952
  # Output the path with filename.
700
- file_result['relative_dir'] = _get_relative_output_path_from_input_path(
701
- directory_path, dirpath, file)
953
+ path_object.relative_dir = _get_relative_output_path_from_input_path(
954
+ directory_path, dir_path, file_or_directory)
702
955
  # if 'relative_file_name_as_directory' wasn't passed.
703
956
  else:
704
957
  # Output the path without filename.
705
- file_result['relative_dir'] = _get_relative_output_path_from_input_path(directory_path, dirpath)
958
+ path_object.relative_dir = _get_relative_output_path_from_input_path(
959
+ directory_path, dir_path)
706
960
 
707
961
  # Remove separator from the beginning if exists.
708
- file_result['relative_dir'] = file_result['relative_dir'].removeprefix(os.sep)
962
+ path_object.relative_dir = path_object.relative_dir.removeprefix(os.sep)
709
963
 
710
964
  # If 'add_last_modified_time' was passed.
711
965
  if add_last_modified_time:
712
966
  # Get last modified time of the file.
713
- file_result['last_modified'] = get_file_modified_time(file_result['file_path'])
967
+ path_object.update(update_last_modified=True)
968
+
969
+ if datetime_format:
970
+ # Get the datetime object from the file name by the date format pattern.
971
+ path_object.update(datetime_format=datetime_format, update_datetime=True)
972
+ # If the datetime string is empty, then the file doesn't contain the date in the filename.
973
+ if not path_object.datetime_string:
974
+ return
975
+
976
+ if specific_date:
977
+ if path_object.datetime_string != specific_date:
978
+ return
714
979
 
715
- object_list.append(file_result)
980
+ object_list.append(path_object)
981
+
982
+ if get_file and get_directory:
983
+ raise ValueError('Parameters "get_file" and "get_directory" cannot be both "True".')
984
+ elif not get_file and not get_directory:
985
+ raise ValueError('Parameters "get_file" and "get_directory" cannot be both "False".')
986
+
987
+ if get_directory and (add_file_binary or add_file_hash):
988
+ raise ValueError(
989
+ 'While "get_directory" is True, Parameters "add_file_binary" or "add_file_hash" cannot be "True".')
716
990
 
717
991
  if sort_by_last_modified_time and not add_last_modified_time:
718
992
  raise ValueError('Parameter "sort_by_last_modified_time" cannot be "True" if parameter '
@@ -721,18 +995,25 @@ def get_file_paths_from_directory(
721
995
  raise ValueError('Parameter "relative_file_name_as_directory" cannot be "True" if parameter '
722
996
  '"add_relative_directory" is not "True".')
723
997
 
998
+ if not datetime_format and specific_date:
999
+ raise ValueError('If "specific_date" is specified, "datetime_format" must be specified.')
1000
+
724
1001
  # === Function main ================
725
1002
  # Define locals.
726
1003
  object_list: list = list()
727
1004
 
728
1005
  # "Walk" over all the directories and subdirectories - make list of full file paths inside the directory
729
1006
  # recursively.
730
- for dirpath, subdirs, files in os.walk(directory_path):
731
- # Iterate through all the file names that were found in the folder.
732
- for file in files:
733
- # If 'file_name_check_pattern' was passed.
734
- if strings.match_pattern_against_string(file_name_check_pattern, file):
735
- get_file()
1007
+ for dir_path, sub_dirs, files in os.walk(directory_path):
1008
+ if get_file:
1009
+ # Iterate through all the file names that were found in the folder.
1010
+ for path in files:
1011
+ # If 'file_name_check_pattern' was passed.
1012
+ get_path(path)
1013
+ elif get_directory:
1014
+ # Iterate through all the directory names that were found in the folder.
1015
+ for path in sub_dirs:
1016
+ get_path(path)
736
1017
 
737
1018
  if not recursive:
738
1019
  break
@@ -740,7 +1021,34 @@ def get_file_paths_from_directory(
740
1021
  # If 'sort_by_last_modified_time' was passed.
741
1022
  if sort_by_last_modified_time:
742
1023
  # Sort the list by last modified time.
743
- object_list = list_of_dicts.sort_by_keys(object_list, key_list=['last_modified'])
1024
+ object_list = list_of_classes.sort_by_attributes(object_list, attribute_list=['last_modified'])
1025
+
1026
+ if add_file_binary or add_file_hash:
1027
+ if add_file_binary and not add_file_hash:
1028
+ prefix_string = 'Reading Binary of File: '
1029
+ elif add_file_hash and not add_file_binary:
1030
+ prefix_string = 'Reading Hash of File: '
1031
+ elif add_file_binary and add_file_hash:
1032
+ prefix_string = 'Reading Binary and Hash of File: '
1033
+ else:
1034
+ prefix_string = 'Reading File: '
1035
+
1036
+ for file_index, file_path in enumerate(object_list):
1037
+ print_api.print_status_of_list(
1038
+ list_instance=object_list, prefix_string=prefix_string, current_state=(file_index + 1))
1039
+
1040
+ # If 'add_binary' was passed.
1041
+ if add_file_binary and file_path.is_file:
1042
+ # Get binary content of the file.
1043
+ object_list[file_index].binary = file_io.read_file(file_path.path, file_mode='rb', stdout=False)
1044
+
1045
+ # If 'add_file_hash' was passed.
1046
+ if add_file_hash and file_path.is_file:
1047
+ # Get hash of the file.
1048
+ if file_path.binary:
1049
+ object_list[file_index].hash = hashing.hash_bytes(file_path.binary)
1050
+ else:
1051
+ object_list[file_index].hash = hashing.hash_file(file_path.path)
744
1052
 
745
1053
  return object_list
746
1054
 
@@ -815,17 +1123,6 @@ def remove_last_separator(directory_path: str) -> str:
815
1123
  return directory_path.removesuffix(os.sep)
816
1124
 
817
1125
 
818
- def remove_first_separator(filesystem_path: str) -> str:
819
- """
820
- The function removes the first character in 'filesystem_path' if it is a separator returning the processed string.
821
- If the first character is not a separator, nothing is happening.
822
-
823
- :param filesystem_path:
824
- :return:
825
- """
826
- return filesystem_path.removesuffix(os.sep)
827
-
828
-
829
1126
  def add_last_separator(filesystem_path: str) -> str:
830
1127
  """
831
1128
  The function adds a separator to the end of the path if it doesn't exist.
@@ -861,14 +1158,14 @@ def get_files_and_folders(directory_path: str, string_contains: str = str()):
861
1158
  return files_folders_list
862
1159
 
863
1160
 
864
- def get_file_modified_time(file_path: str) -> float:
1161
+ def get_file_modified_time(file_or_dir_path: str) -> float:
865
1162
  """
866
1163
  The function returns the time of last modification of the file in seconds since the epoch.
867
1164
 
868
- :param file_path: string, full path to file.
1165
+ :param file_or_dir_path: string, full path to file or directory.
869
1166
  :return: float, time of last modification of the file in seconds since the epoch.
870
1167
  """
871
- return os.path.getmtime(file_path)
1168
+ return os.path.getmtime(file_or_dir_path)
872
1169
 
873
1170
 
874
1171
  def change_last_modified_date_of_file(file_path: str, new_date: float) -> None:
@@ -890,43 +1187,6 @@ def change_last_modified_date_of_file(file_path: str, new_date: float) -> None:
890
1187
  os.utime(file_path, (new_date, new_date))
891
1188
 
892
1189
 
893
- def get_file_hashes_from_directory(directory_path: str, recursive: bool = False, add_binary: bool = False) -> list:
894
- """
895
- The function scans a directory for files and returns a list of dictionaries with file path and hash of the file.
896
- Binary option can be specified.
897
-
898
- :param directory_path: string, of full path to directory you want to return file names of.
899
- :param recursive: boolean.
900
- 'True', then the function will scan recursively in subdirectories.
901
- 'False', then the function will scan only in the directory that was passed.
902
- :param add_binary: boolean, if 'True', then the function will add the binary of the file to the output list.
903
-
904
- :return: list of dicts with full file paths, hashes and binaries (if specified).
905
- """
906
-
907
- # Get all the files.
908
- file_paths_list = get_file_paths_from_directory(directory_path, recursive=recursive)
909
-
910
- # Create a list of dictionaries, each dictionary is a file with its hash.
911
- files: list = list()
912
- for file_index, file_path in enumerate(file_paths_list):
913
- print_status_of_list(
914
- list_instance=file_paths_list, prefix_string=f'Reading File: ', current_state=(file_index + 1))
915
-
916
- file_info: dict = dict()
917
- file_info['path'] = file_path['path']
918
-
919
- if add_binary:
920
- file_info['binary'] = file_io.read_file(file_path['path'], file_mode='rb', stdout=False)
921
- file_info['hash'] = hashing.hash_bytes(file_info['binary'])
922
- else:
923
- file_info['hash'] = hashing.hash_file(file_path['path'])
924
-
925
- files.append(file_info)
926
-
927
- return files
928
-
929
-
930
1190
  def find_duplicates_by_hash(
931
1191
  directory_path: str,
932
1192
  recursive: bool = False,
@@ -945,33 +1205,34 @@ def find_duplicates_by_hash(
945
1205
  """
946
1206
 
947
1207
  # Get all the files.
948
- files: list = get_file_hashes_from_directory(directory_path, recursive=recursive, add_binary=add_binary)
1208
+ files: list = get_paths_from_directory(
1209
+ directory_path, get_file=True, recursive=recursive, add_file_binary=add_binary)
949
1210
 
950
1211
  same_hash_files: list = list()
951
1212
  # Check if there are files that have exactly the same hash.
952
- for file_dict in files:
1213
+ for atomic_path in files:
953
1214
  # Create a list of files that have the same hash for current 'firmware'.
954
1215
  current_run_list: list = list()
955
- for file_dict_compare in files:
1216
+ for atomic_path_compare in files:
956
1217
  # Add all the 'firmware_compare' that have the same hash to the list.
957
- if (file_dict['hash'] == file_dict_compare['hash'] and
958
- file_dict['path'] != file_dict_compare['path']):
1218
+ if (atomic_path.hash == atomic_path_compare.hash and
1219
+ atomic_path.path != atomic_path_compare.path):
959
1220
  # Check if current 'firmware' is already in the 'same_hash_files' list. If not, add 'firmware_compare'
960
1221
  # to the 'current_run_list'.
961
1222
  if not any(list_of_dicts.is_value_exist_in_key(
962
- list_of_dicts=test_hash, key='path', value_to_match=file_dict['path']) for
1223
+ list_of_dicts=test_hash, key='path', value_to_match=atomic_path.path) for
963
1224
  test_hash in same_hash_files):
964
1225
  current_run_list.append({
965
- 'path': file_dict_compare['path'],
966
- 'hash': file_dict_compare['hash']
1226
+ 'path': atomic_path_compare.path,
1227
+ 'hash': atomic_path_compare.hash
967
1228
  })
968
1229
 
969
1230
  if current_run_list:
970
1231
  # After the iteration of the 'firmware_compare' finished and the list is not empty, add the 'firmware'
971
1232
  # to the list.
972
1233
  current_run_list.append({
973
- 'path': file_dict['path'],
974
- 'hash': file_dict['hash']
1234
+ 'path': atomic_path.path,
1235
+ 'hash': atomic_path.hash
975
1236
  })
976
1237
  same_hash_files.append(current_run_list)
977
1238
 
@@ -1052,39 +1313,40 @@ def get_directory_size(directory_path: str):
1052
1313
 
1053
1314
 
1054
1315
  def get_subpaths_between(start_path: str, end_path: str) -> list[str]:
1316
+ # noinspection GrazieInspection
1055
1317
  """
1056
- Get the subpaths between two paths.
1057
- :param start_path: string, start path.
1058
- :param end_path: string, end path.
1059
- :return:
1318
+ Get the subpaths between two paths.
1319
+ :param start_path: string, start path.
1320
+ :param end_path: string, end path.
1321
+ :return:
1060
1322
 
1061
- Example Linux:
1062
- start_path = '/test/1'
1063
- end_path = '/test/1/2/3/4'
1323
+ Example Linux:
1324
+ start_path = '/test/1'
1325
+ end_path = '/test/1/2/3/4'
1064
1326
 
1065
- subpaths = get_subpaths_between(start_path, end_path)
1327
+ subpaths = get_subpaths_between(start_path, end_path)
1066
1328
 
1067
- subpaths = [
1068
- '/test/1'
1069
- '/test/1/2',
1070
- '/test/1/2/3',
1071
- '/test/1/2/3/4',
1072
- ]
1329
+ subpaths = [
1330
+ '/test/1'
1331
+ '/test/1/2',
1332
+ '/test/1/2/3',
1333
+ '/test/1/2/3/4',
1334
+ ]
1073
1335
 
1074
1336
 
1075
- Example Windows:
1076
- start_path = 'C:\\test\\1'
1077
- end_path = 'C:\\test\\1\\2\\3\\4'
1337
+ Example Windows:
1338
+ start_path = 'C:\\test\\1'
1339
+ end_path = 'C:\\test\\1\\2\\3\\4'
1078
1340
 
1079
- subpaths = get_subpaths_between(start_path, end_path)
1341
+ subpaths = get_subpaths_between(start_path, end_path)
1080
1342
 
1081
- subpaths = [
1082
- 'C:\\test\\1',
1083
- 'C:\\test\\1\\2',
1084
- 'C:\\test\\1\\2\\3',
1085
- 'C:\\test\\1\\2\\3\\4',
1086
- ]
1087
- """
1343
+ subpaths = [
1344
+ 'C:\\test\\1',
1345
+ 'C:\\test\\1\\2',
1346
+ 'C:\\test\\1\\2\\3',
1347
+ 'C:\\test\\1\\2\\3\\4',
1348
+ ]
1349
+ """
1088
1350
 
1089
1351
  # Detect slash type based on the input (default to forward slash)
1090
1352
  slash_type = "\\" if "\\" in start_path else "/"
@@ -1125,13 +1387,13 @@ def get_subpaths_between(start_path: str, end_path: str) -> list[str]:
1125
1387
  # else:
1126
1388
  # raise ValueError("Start path must be a parent of the end path")
1127
1389
  #
1128
- # # Reverse the list so it goes from start to end.
1390
+ # # Reverse the list, so it goes from start to end.
1129
1391
  # subpaths.reverse()
1130
1392
  #
1131
1393
  # return subpaths
1132
1394
 
1133
1395
 
1134
- def create_dict_of_paths_list(list_of_paths: list) -> dict:
1396
+ def create_dict_of_paths_list(list_of_paths: list) -> list:
1135
1397
  """
1136
1398
  The function receives a list of paths and returns a dictionary with keys as the paths and values as the
1137
1399
  subpaths of the key path.
@@ -1167,7 +1429,7 @@ def create_dict_of_paths_list(list_of_paths: list) -> dict:
1167
1429
  :return: dictionary.
1168
1430
  """
1169
1431
 
1170
- structure = []
1432
+ structure: list = []
1171
1433
  for path in list_of_paths:
1172
1434
  create_dict_of_path(path, structure)
1173
1435
  return structure
@@ -1175,70 +1437,20 @@ def create_dict_of_paths_list(list_of_paths: list) -> dict:
1175
1437
 
1176
1438
  def create_dict_of_path(
1177
1439
  path: str,
1178
- # structure_dict: dict,
1179
1440
  structure_list: list,
1180
- add_data_to_entry: any = None,
1181
- add_data_key: str = 'addon',
1182
- parent_entry: str = None
1441
+ add_data_to_entry: list[dict[str, any]] = None
1183
1442
  ):
1184
1443
  """
1185
1444
  The function receives a path and a list, and adds the path to the list.
1186
-
1187
1445
  Check the working example from 'create_dict_of_paths_list' function.
1188
1446
 
1189
1447
  :param path: string, path.
1190
1448
  :param structure_list: list to add the path to.
1191
- :param add_data_to_entry: any, data to add to the entry.
1192
- :param add_data_key: string, key to add the data to.
1193
- :param parent_entry: string, for internal use to pass the current parent entry.
1449
+ :param add_data_to_entry: a list of dicts with data to add to the entry.
1450
+ dict format: {key: data}
1194
1451
  :return:
1195
1452
  """
1196
1453
 
1197
- # # Normalize path for cross-platform compatibility
1198
- # normalized_path = path.replace("\\", "/")
1199
- # parts = normalized_path.strip("/").split("/")
1200
- # current_level = structure_dict
1201
- #
1202
- # for part in parts[:-1]: # Iterate through the directories
1203
- # # If the part is not already a key in the current level of the structure, add it
1204
- # if part not in current_level:
1205
- # current_level[part] = {}
1206
- # current_level = current_level[part]
1207
- #
1208
- # # Create the entry for the file with additional data
1209
- # file_entry = {"entry": parts[-1], add_data_key: add_data_to_entry}
1210
- #
1211
- # # We're adding file entries under numeric keys.
1212
- # if isinstance(current_level, dict) and all(isinstance(key, int) for key in current_level.keys()):
1213
- # current_level[len(current_level)] = file_entry
1214
- # else:
1215
- # # Handle cases where there's a mix of numeric keys and directory names
1216
- # # Find the next available numeric key
1217
- # next_key = max([key if isinstance(key, int) else -1 for key in current_level.keys()], default=-1) + 1
1218
- # current_level[next_key] = file_entry
1219
-
1220
- # entries_key_name = "__entries__"
1221
- #
1222
- # # Normalize path for cross-platform compatibility
1223
- # normalized_path = path.replace("\\", "/")
1224
- # parts = normalized_path.strip("/").split("/")
1225
- # current_level = structure_dict
1226
- #
1227
- # for part in parts[:-1]: # Navigate through or create directory structure
1228
- # if part not in current_level:
1229
- # current_level[part] = {}
1230
- # current_level = current_level[part]
1231
- #
1232
- # # Create the entry for the file with additional data
1233
- # file_entry = {"entry": parts[-1], add_data_key: add_data_to_entry}
1234
- #
1235
- # # If the current level (final directory) does not have an "entries" key for files, create it
1236
- # if entries_key_name not in current_level:
1237
- # current_level[entries_key_name] = []
1238
- #
1239
- # # Append the file entry to the list associated with the "entries" key
1240
- # current_level[entries_key_name].append(file_entry)
1241
-
1242
1454
  # Normalize path for cross-platform compatibility
1243
1455
  normalized_path = path.replace("\\", "/")
1244
1456
  parts = normalized_path.strip("/").split("/")
@@ -1246,28 +1458,36 @@ def create_dict_of_path(
1246
1458
  current_level = structure_list
1247
1459
 
1248
1460
  for i, part in enumerate(parts):
1249
- # Determine if this is the last part (a file)
1461
+ # Determine if this is the last part (a file or final component of the path)
1250
1462
  is_last_part = (i == len(parts) - 1)
1251
1463
 
1252
1464
  # Try to find an existing entry for this part
1253
1465
  existing_entry = next((item for item in current_level if item["entry"] == part), None)
1254
1466
 
1255
1467
  if existing_entry is None:
1256
- # For the last part, add the additional data; for directories, just create the structure
1257
- if is_last_part:
1258
- new_entry = {"entry": part, add_data_key: add_data_to_entry, "included": []}
1259
- else:
1260
- new_entry = {"entry": part, "included": []}
1468
+ # Create a new entry
1469
+ new_entry = {"entry": part, "included": []}
1470
+
1471
+ # Add additional data if it's the last part
1472
+ if is_last_part and add_data_to_entry:
1473
+ for data_dict in add_data_to_entry:
1474
+ new_entry.update(data_dict)
1261
1475
 
1262
1476
  current_level.append(new_entry)
1477
+
1263
1478
  # Only update current_level if it's not the last part
1264
1479
  if not is_last_part:
1265
1480
  current_level = new_entry["included"]
1266
1481
  else:
1267
- # If it's not the last part and the entry exists, navigate deeper
1482
+ # If the entry exists and it's not the last part, navigate deeper
1268
1483
  if not is_last_part:
1269
1484
  current_level = existing_entry["included"]
1270
1485
 
1486
+ # If the entry exists and it's the last part, update with additional data
1487
+ if is_last_part and add_data_to_entry:
1488
+ for data_dict in add_data_to_entry:
1489
+ existing_entry.update(data_dict)
1490
+
1271
1491
 
1272
1492
  def list_open_files_in_directory(directory):
1273
1493
  """
@@ -1284,6 +1504,7 @@ def list_open_files_in_directory(directory):
1284
1504
  proc_open_files = proc.open_files()
1285
1505
  for file in proc_open_files:
1286
1506
  if file.path.startswith(directory):
1507
+ # noinspection PyUnresolvedReferences
1287
1508
  open_files.append((proc.info['pid'], proc.info['name'], file.path))
1288
1509
  except (psutil.AccessDenied, psutil.NoSuchProcess):
1289
1510
  # Ignore processes that can't be accessed
@@ -1362,7 +1583,13 @@ def is_file_open_by_process(file_path: str) -> bool:
1362
1583
  return False
1363
1584
 
1364
1585
 
1365
- def get_download_directory(place: Literal['temp', 'script', 'working'] = 'temp', script_path: str = None) -> str:
1586
+ def get_download_directory(
1587
+ place: Literal[
1588
+ 'temp',
1589
+ 'script',
1590
+ 'working'] = 'temp',
1591
+ script_path: str = None
1592
+ ) -> str:
1366
1593
  """
1367
1594
  The function returns the default download directory based on place.
1368
1595
 
@@ -1394,11 +1621,12 @@ def backup_folder(directory_path: str, backup_directory: str) -> None:
1394
1621
  """
1395
1622
  Backup the specified directory.
1396
1623
 
1397
- :param directory_path: The directory path to backup.
1398
- :param backup_directory: The directory to backup the directory to.
1624
+ :param directory_path: The directory path to back up.
1625
+ :param backup_directory: The directory to back up the directory to.
1399
1626
 
1400
1627
  Example:
1401
- backup_folder(directory_path='C:\\Users\\user1\\Downloads\\folder1', backup_directory='C:\\Users\\user1\\Downloads\\backup')
1628
+ backup_folder(
1629
+ directory_path='C:\\Users\\user1\\Downloads\\folder1', backup_directory='C:\\Users\\user1\\Downloads\\backup')
1402
1630
 
1403
1631
  Backed up folder will be moved to 'C:\\Users\\user1\\Downloads\\backup' with timestamp in the name.
1404
1632
  Final path will look like: 'C:\\Users\\user1\\Downloads\\backup\\20231003-120000-000000_folder1'
@@ -1456,7 +1684,7 @@ def backup_file(
1456
1684
  else:
1457
1685
  file_name: str = f"{file_name_no_extension}_{timestamp}{file_extension}"
1458
1686
  backup_file_path: str = str(Path(backup_directory) / file_name)
1459
- move_file(file_path, backup_file_path)
1687
+ rename_file(file_path, file_name)
1460
1688
 
1461
1689
  return backup_file_path
1462
1690
  else:
@@ -1470,8 +1698,105 @@ def find_file(file_name: str, directory_path: str):
1470
1698
  :param directory_path: string, The directory to search in.
1471
1699
  :return:
1472
1700
  """
1473
- for dirpath, dirnames, filenames in os.walk(directory_path):
1701
+ for dir_path, dir_names, filenames in os.walk(directory_path):
1474
1702
  for filename in filenames:
1475
1703
  if filename == file_name:
1476
- return os.path.join(dirpath, filename)
1704
+ return os.path.join(dir_path, filename)
1477
1705
  return None
1706
+
1707
+
1708
+ def create_ubuntu_desktop_shortcut(
1709
+ file_path: str = None,
1710
+ shortcut_name: str = None,
1711
+ command: str = None,
1712
+ working_directory: str = None,
1713
+ icon_path: str = None,
1714
+ terminal: bool = False,
1715
+ comment: str = "Shortcut to execute the Python script",
1716
+ categories: str = "Utility",
1717
+ set_executable: bool = False,
1718
+ set_trusted: bool = False,
1719
+ set_xfce_exe_checksum: bool = False
1720
+ ):
1721
+ """
1722
+ Create a desktop shortcut on Ubuntu.
1723
+ Either file_path or command must be specified.
1724
+
1725
+ :param file_path: string, The file_path to execute when the shortcut is clicked.
1726
+ Example2: '/path/to/script.sh'
1727
+ :param shortcut_name: string, The name of the shortcut.
1728
+ Example: 'My Python Script'
1729
+ Result: 'My Python Script.desktop'
1730
+ :param command: string, The command to execute when the shortcut is clicked.
1731
+ Example: 'python3 /path/to/script.py'
1732
+ :param working_directory: string, The working directory for the command.
1733
+ If None, the command will be executed in the same script's directory.
1734
+ :param icon_path: string, The path to the icon file.
1735
+ :param terminal: boolean, If True, the command will be executed in a terminal.
1736
+ :param comment: string, A comment to describe the shortcut.
1737
+ :param categories: string, The categories of the shortcut.
1738
+ :param set_executable: boolean, If True, the shortcut will be made executable.
1739
+ :param set_trusted: boolean, If True, the shortcut will be marked as trusted.
1740
+ This is needed for GNOME.
1741
+ :param set_xfce_exe_checksum: boolean, If True, the shortcut will be made safe executable for XFCE.
1742
+
1743
+ :return: None
1744
+ """
1745
+
1746
+ if not file_path and not command:
1747
+ raise ValueError("Either 'file_path' or 'command' must be specified.")
1748
+ if command and file_path:
1749
+ raise ValueError("Only one of 'file_path' or 'command' can be specified.")
1750
+ if command and not shortcut_name:
1751
+ raise ValueError("The 'shortcut_name' must be specified when 'command' is used.")
1752
+
1753
+ from .permissions import ubuntu_permissions
1754
+
1755
+ # Get the user's directory.
1756
+ desktop_dir = os.path.expanduser("~/Desktop")
1757
+
1758
+ if not working_directory and file_path:
1759
+ working_directory = os.path.dirname(file_path)
1760
+
1761
+ if not shortcut_name:
1762
+ shortcut_name: str = Path(file_path).stem
1763
+
1764
+ if command:
1765
+ executable: str = command
1766
+ elif file_path:
1767
+ executable: str = file_path
1768
+ else:
1769
+ raise ValueError("Either 'file_path' or 'command' must be specified.")
1770
+
1771
+ # Full path to the .desktop file
1772
+ shortcut_path = os.path.join(desktop_dir, f"{shortcut_name}.desktop")
1773
+
1774
+ # Generate the content for the .desktop file
1775
+ desktop_entry = [
1776
+ "[Desktop Entry]",
1777
+ "Version=1.0",
1778
+ "Type=Application",
1779
+ f"Name={shortcut_name}",
1780
+ f"Exec={executable}",
1781
+ f"Path={working_directory}" if working_directory else "",
1782
+ f"Icon={icon_path}" if icon_path else "",
1783
+ f"Terminal={'true' if terminal else 'false'}",
1784
+ f"Comment={comment}",
1785
+ f"Categories={categories};",
1786
+ ]
1787
+
1788
+ # Write the .desktop file
1789
+ with open(shortcut_path, "w") as shortcut_file:
1790
+ shortcut_file.write("\n".join(line for line in desktop_entry if line)) # Skip empty lines
1791
+
1792
+ # Make the .desktop file executable
1793
+ if set_executable:
1794
+ ubuntu_permissions.set_executable(shortcut_path)
1795
+
1796
+ # Mark the .desktop file as trusted
1797
+ if set_trusted:
1798
+ ubuntu_permissions.set_trusted_executable(shortcut_path)
1799
+
1800
+ # Make the .desktop file safe executable for XFCE
1801
+ if set_xfce_exe_checksum:
1802
+ ubuntu_permissions.set_xfce_exe_checksum(shortcut_path)