atomicshop 2.11.47__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 (268) hide show
  1. atomicshop/__init__.py +1 -1
  2. atomicshop/{addons/mains → a_mains}/FACT/update_extract.py +3 -2
  3. atomicshop/a_mains/addons/process_list/compile.cmd +7 -0
  4. atomicshop/a_mains/addons/process_list/compiled/Win10x64/process_list.dll +0 -0
  5. atomicshop/a_mains/addons/process_list/compiled/Win10x64/process_list.exp +0 -0
  6. atomicshop/a_mains/addons/process_list/compiled/Win10x64/process_list.lib +0 -0
  7. atomicshop/{addons → a_mains/addons}/process_list/process_list.cpp +8 -1
  8. atomicshop/a_mains/dns_gateway_setting.py +11 -0
  9. atomicshop/a_mains/get_local_tcp_ports.py +85 -0
  10. atomicshop/a_mains/github_wrapper.py +11 -0
  11. atomicshop/a_mains/install_ca_certificate.py +172 -0
  12. atomicshop/{addons/mains → a_mains}/msi_unpacker.py +3 -1
  13. atomicshop/a_mains/process_from_port.py +119 -0
  14. atomicshop/a_mains/set_default_dns_gateway.py +90 -0
  15. atomicshop/a_mains/update_config_toml.py +38 -0
  16. atomicshop/appointment_management.py +5 -3
  17. atomicshop/basics/ansi_escape_codes.py +3 -1
  18. atomicshop/basics/argparse_template.py +2 -0
  19. atomicshop/basics/booleans.py +27 -30
  20. atomicshop/basics/bytes_arrays.py +43 -0
  21. atomicshop/basics/classes.py +149 -1
  22. atomicshop/basics/dicts.py +12 -0
  23. atomicshop/basics/enums.py +2 -2
  24. atomicshop/basics/exceptions.py +5 -1
  25. atomicshop/basics/list_of_classes.py +29 -0
  26. atomicshop/basics/list_of_dicts.py +69 -5
  27. atomicshop/basics/lists.py +14 -0
  28. atomicshop/basics/multiprocesses.py +374 -50
  29. atomicshop/basics/package_module.py +10 -0
  30. atomicshop/basics/strings.py +160 -7
  31. atomicshop/basics/threads.py +14 -0
  32. atomicshop/basics/tracebacks.py +13 -4
  33. atomicshop/certificates.py +153 -52
  34. atomicshop/config_init.py +12 -7
  35. atomicshop/console_user_response.py +7 -14
  36. atomicshop/consoles.py +9 -0
  37. atomicshop/datetimes.py +98 -0
  38. atomicshop/diff_check.py +340 -40
  39. atomicshop/dns.py +128 -12
  40. atomicshop/etws/_pywintrace_fix.py +17 -0
  41. atomicshop/etws/const.py +38 -0
  42. atomicshop/etws/providers.py +21 -0
  43. atomicshop/etws/sessions.py +43 -0
  44. atomicshop/etws/trace.py +168 -0
  45. atomicshop/etws/traces/trace_dns.py +162 -0
  46. atomicshop/etws/traces/trace_sysmon_process_creation.py +126 -0
  47. atomicshop/etws/traces/trace_tcp.py +130 -0
  48. atomicshop/file_io/csvs.py +222 -24
  49. atomicshop/file_io/docxs.py +35 -18
  50. atomicshop/file_io/file_io.py +35 -19
  51. atomicshop/file_io/jsons.py +49 -0
  52. atomicshop/file_io/tomls.py +139 -0
  53. atomicshop/filesystem.py +864 -293
  54. atomicshop/get_process_list.py +133 -0
  55. atomicshop/{process_name_cmd.py → get_process_name_cmd_dll.py} +52 -19
  56. atomicshop/http_parse.py +149 -93
  57. atomicshop/ip_addresses.py +6 -1
  58. atomicshop/mitm/centered_settings.py +132 -0
  59. atomicshop/mitm/config_static.py +207 -0
  60. atomicshop/mitm/config_toml_editor.py +55 -0
  61. atomicshop/mitm/connection_thread_worker.py +875 -357
  62. atomicshop/mitm/engines/__parent/parser___parent.py +4 -17
  63. atomicshop/mitm/engines/__parent/recorder___parent.py +108 -51
  64. atomicshop/mitm/engines/__parent/requester___parent.py +116 -0
  65. atomicshop/mitm/engines/__parent/responder___parent.py +75 -114
  66. atomicshop/mitm/engines/__reference_general/parser___reference_general.py +10 -7
  67. atomicshop/mitm/engines/__reference_general/recorder___reference_general.py +5 -5
  68. atomicshop/mitm/engines/__reference_general/requester___reference_general.py +47 -0
  69. atomicshop/mitm/engines/__reference_general/responder___reference_general.py +95 -13
  70. atomicshop/mitm/engines/create_module_template.py +58 -14
  71. atomicshop/mitm/import_config.py +359 -139
  72. atomicshop/mitm/initialize_engines.py +160 -74
  73. atomicshop/mitm/message.py +64 -23
  74. atomicshop/mitm/mitm_main.py +892 -0
  75. atomicshop/mitm/recs_files.py +183 -0
  76. atomicshop/mitm/shared_functions.py +4 -10
  77. atomicshop/mitm/ssh_tester.py +82 -0
  78. atomicshop/mitm/statistic_analyzer.py +257 -166
  79. atomicshop/mitm/statistic_analyzer_helper/analyzer_helper.py +136 -0
  80. atomicshop/mitm/statistic_analyzer_helper/moving_average_helper.py +525 -0
  81. atomicshop/monitor/change_monitor.py +96 -120
  82. atomicshop/monitor/checks/dns.py +139 -70
  83. atomicshop/monitor/checks/file.py +77 -0
  84. atomicshop/monitor/checks/network.py +81 -77
  85. atomicshop/monitor/checks/process_running.py +33 -34
  86. atomicshop/monitor/checks/url.py +94 -0
  87. atomicshop/networks.py +671 -0
  88. atomicshop/on_exit.py +205 -0
  89. atomicshop/package_mains_processor.py +84 -0
  90. atomicshop/permissions/permissions.py +22 -0
  91. atomicshop/permissions/ubuntu_permissions.py +239 -0
  92. atomicshop/permissions/win_permissions.py +33 -0
  93. atomicshop/print_api.py +24 -41
  94. atomicshop/process.py +63 -17
  95. atomicshop/process_poller/__init__.py +0 -0
  96. atomicshop/process_poller/pollers/__init__.py +0 -0
  97. atomicshop/process_poller/pollers/psutil_pywin32wmi_dll.py +95 -0
  98. atomicshop/process_poller/process_pool.py +207 -0
  99. atomicshop/process_poller/simple_process_pool.py +311 -0
  100. atomicshop/process_poller/tracer_base.py +45 -0
  101. atomicshop/process_poller/tracers/__init__.py +0 -0
  102. atomicshop/process_poller/tracers/event_log.py +46 -0
  103. atomicshop/process_poller/tracers/sysmon_etw.py +68 -0
  104. atomicshop/python_file_patcher.py +1 -1
  105. atomicshop/python_functions.py +27 -75
  106. atomicshop/question_answer_engine.py +2 -2
  107. atomicshop/scheduling.py +24 -5
  108. atomicshop/sound.py +4 -2
  109. atomicshop/speech_recognize.py +8 -0
  110. atomicshop/ssh_remote.py +158 -172
  111. atomicshop/startup/__init__.py +0 -0
  112. atomicshop/startup/win/__init__.py +0 -0
  113. atomicshop/startup/win/startup_folder.py +53 -0
  114. atomicshop/startup/win/task_scheduler.py +119 -0
  115. atomicshop/system_resource_monitor.py +61 -46
  116. atomicshop/system_resources.py +8 -8
  117. atomicshop/tempfiles.py +1 -2
  118. atomicshop/timer.py +30 -11
  119. atomicshop/urls.py +41 -0
  120. atomicshop/venvs.py +28 -0
  121. atomicshop/versioning.py +27 -0
  122. atomicshop/web.py +110 -25
  123. atomicshop/web_apis/__init__.py +0 -0
  124. atomicshop/web_apis/google_custom_search.py +44 -0
  125. atomicshop/web_apis/google_llm.py +188 -0
  126. atomicshop/websocket_parse.py +450 -0
  127. atomicshop/wrappers/certauthw/certauth.py +1 -0
  128. atomicshop/wrappers/cryptographyw.py +29 -8
  129. atomicshop/wrappers/ctyping/etw_winapi/__init__.py +0 -0
  130. atomicshop/wrappers/ctyping/etw_winapi/const.py +335 -0
  131. atomicshop/wrappers/ctyping/etw_winapi/etw_functions.py +393 -0
  132. atomicshop/wrappers/ctyping/file_details_winapi.py +67 -0
  133. atomicshop/wrappers/ctyping/msi_windows_installer/cabs.py +2 -1
  134. atomicshop/wrappers/ctyping/msi_windows_installer/extract_msi_main.py +13 -9
  135. atomicshop/wrappers/ctyping/msi_windows_installer/tables.py +35 -0
  136. atomicshop/wrappers/ctyping/setup_device.py +466 -0
  137. atomicshop/wrappers/ctyping/win_console.py +39 -0
  138. atomicshop/wrappers/dockerw/dockerw.py +113 -2
  139. atomicshop/wrappers/elasticsearchw/config_basic.py +0 -12
  140. atomicshop/wrappers/elasticsearchw/elastic_infra.py +75 -0
  141. atomicshop/wrappers/elasticsearchw/elasticsearchw.py +2 -20
  142. atomicshop/wrappers/factw/get_file_data.py +12 -5
  143. atomicshop/wrappers/factw/install/install_after_restart.py +89 -5
  144. atomicshop/wrappers/factw/install/pre_install_and_install_before_restart.py +20 -14
  145. atomicshop/wrappers/factw/postgresql/firmware.py +4 -6
  146. atomicshop/wrappers/githubw.py +583 -51
  147. atomicshop/wrappers/loggingw/consts.py +49 -0
  148. atomicshop/wrappers/loggingw/filters.py +102 -0
  149. atomicshop/wrappers/loggingw/formatters.py +58 -71
  150. atomicshop/wrappers/loggingw/handlers.py +459 -40
  151. atomicshop/wrappers/loggingw/loggers.py +19 -0
  152. atomicshop/wrappers/loggingw/loggingw.py +1010 -178
  153. atomicshop/wrappers/loggingw/reading.py +344 -19
  154. atomicshop/wrappers/mongodbw/__init__.py +0 -0
  155. atomicshop/wrappers/mongodbw/mongo_infra.py +31 -0
  156. atomicshop/wrappers/mongodbw/mongodbw.py +1432 -0
  157. atomicshop/wrappers/netshw.py +271 -0
  158. atomicshop/wrappers/playwrightw/engine.py +34 -19
  159. atomicshop/wrappers/playwrightw/infra.py +5 -0
  160. atomicshop/wrappers/playwrightw/javascript.py +7 -3
  161. atomicshop/wrappers/playwrightw/keyboard.py +14 -0
  162. atomicshop/wrappers/playwrightw/scenarios.py +172 -5
  163. atomicshop/wrappers/playwrightw/waits.py +9 -7
  164. atomicshop/wrappers/powershell_networking.py +80 -0
  165. atomicshop/wrappers/psutilw/processes.py +81 -0
  166. atomicshop/wrappers/psutilw/psutil_networks.py +85 -0
  167. atomicshop/wrappers/psutilw/psutilw.py +9 -0
  168. atomicshop/wrappers/pyopensslw.py +9 -2
  169. atomicshop/wrappers/pywin32w/__init__.py +0 -0
  170. atomicshop/wrappers/pywin32w/cert_store.py +116 -0
  171. atomicshop/wrappers/pywin32w/console.py +34 -0
  172. atomicshop/wrappers/pywin32w/win_event_log/__init__.py +0 -0
  173. atomicshop/wrappers/pywin32w/win_event_log/fetch.py +174 -0
  174. atomicshop/wrappers/pywin32w/win_event_log/subscribe.py +212 -0
  175. atomicshop/wrappers/pywin32w/win_event_log/subscribes/__init__.py +0 -0
  176. atomicshop/wrappers/pywin32w/win_event_log/subscribes/process_create.py +57 -0
  177. atomicshop/wrappers/pywin32w/win_event_log/subscribes/process_terminate.py +49 -0
  178. atomicshop/wrappers/pywin32w/win_event_log/subscribes/schannel_logging.py +97 -0
  179. atomicshop/wrappers/pywin32w/winshell.py +19 -0
  180. atomicshop/wrappers/pywin32w/wmis/__init__.py +0 -0
  181. atomicshop/wrappers/pywin32w/wmis/msft_netipaddress.py +113 -0
  182. atomicshop/wrappers/pywin32w/wmis/win32_networkadapterconfiguration.py +259 -0
  183. atomicshop/wrappers/pywin32w/wmis/win32networkadapter.py +112 -0
  184. atomicshop/wrappers/pywin32w/wmis/wmi_helpers.py +236 -0
  185. atomicshop/wrappers/socketw/accepter.py +21 -7
  186. atomicshop/wrappers/socketw/certificator.py +216 -150
  187. atomicshop/wrappers/socketw/creator.py +190 -50
  188. atomicshop/wrappers/socketw/dns_server.py +500 -173
  189. atomicshop/wrappers/socketw/exception_wrapper.py +45 -52
  190. atomicshop/wrappers/socketw/process_getter.py +86 -0
  191. atomicshop/wrappers/socketw/receiver.py +144 -102
  192. atomicshop/wrappers/socketw/sender.py +65 -35
  193. atomicshop/wrappers/socketw/sni.py +334 -165
  194. atomicshop/wrappers/socketw/socket_base.py +134 -0
  195. atomicshop/wrappers/socketw/socket_client.py +137 -95
  196. atomicshop/wrappers/socketw/socket_server_tester.py +14 -9
  197. atomicshop/wrappers/socketw/socket_wrapper.py +717 -116
  198. atomicshop/wrappers/socketw/ssl_base.py +15 -14
  199. atomicshop/wrappers/socketw/statistics_csv.py +148 -17
  200. atomicshop/wrappers/sysmonw.py +157 -0
  201. atomicshop/wrappers/ubuntu_terminal.py +65 -26
  202. atomicshop/wrappers/win_auditw.py +189 -0
  203. atomicshop/wrappers/winregw/__init__.py +0 -0
  204. atomicshop/wrappers/winregw/winreg_installed_software.py +58 -0
  205. atomicshop/wrappers/winregw/winreg_network.py +232 -0
  206. {atomicshop-2.11.47.dist-info → atomicshop-3.10.5.dist-info}/METADATA +31 -49
  207. atomicshop-3.10.5.dist-info/RECORD +306 -0
  208. {atomicshop-2.11.47.dist-info → atomicshop-3.10.5.dist-info}/WHEEL +1 -1
  209. atomicshop/_basics_temp.py +0 -101
  210. atomicshop/addons/a_setup_scripts/install_psycopg2_ubuntu.sh +0 -3
  211. atomicshop/addons/a_setup_scripts/install_pywintrace_0.3.cmd +0 -2
  212. atomicshop/addons/mains/install_docker_rootless_ubuntu.py +0 -11
  213. atomicshop/addons/mains/install_docker_ubuntu_main_sudo.py +0 -11
  214. atomicshop/addons/mains/install_elastic_search_and_kibana_ubuntu.py +0 -10
  215. atomicshop/addons/mains/install_wsl_ubuntu_lts_admin.py +0 -9
  216. atomicshop/addons/package_setup/CreateWheel.cmd +0 -7
  217. atomicshop/addons/package_setup/Setup in Edit mode.cmd +0 -6
  218. atomicshop/addons/package_setup/Setup.cmd +0 -7
  219. atomicshop/addons/process_list/compile.cmd +0 -2
  220. atomicshop/addons/process_list/compiled/Win10x64/process_list.dll +0 -0
  221. atomicshop/addons/process_list/compiled/Win10x64/process_list.exp +0 -0
  222. atomicshop/addons/process_list/compiled/Win10x64/process_list.lib +0 -0
  223. atomicshop/archiver/_search_in_zip.py +0 -189
  224. atomicshop/archiver/archiver.py +0 -34
  225. atomicshop/archiver/search_in_archive.py +0 -250
  226. atomicshop/archiver/sevenz_app_w.py +0 -86
  227. atomicshop/archiver/sevenzs.py +0 -44
  228. atomicshop/archiver/zips.py +0 -293
  229. atomicshop/etw/dns_trace.py +0 -118
  230. atomicshop/etw/etw.py +0 -61
  231. atomicshop/file_types.py +0 -24
  232. atomicshop/mitm/engines/create_module_template_example.py +0 -13
  233. atomicshop/mitm/initialize_mitm_server.py +0 -240
  234. atomicshop/monitor/checks/hash.py +0 -44
  235. atomicshop/monitor/checks/hash_checks/file.py +0 -55
  236. atomicshop/monitor/checks/hash_checks/url.py +0 -62
  237. atomicshop/pbtkmultifile_argparse.py +0 -88
  238. atomicshop/permissions.py +0 -110
  239. atomicshop/process_poller.py +0 -237
  240. atomicshop/script_as_string_processor.py +0 -38
  241. atomicshop/ssh_scripts/process_from_ipv4.py +0 -37
  242. atomicshop/ssh_scripts/process_from_port.py +0 -27
  243. atomicshop/wrappers/_process_wrapper_curl.py +0 -27
  244. atomicshop/wrappers/_process_wrapper_tar.py +0 -21
  245. atomicshop/wrappers/dockerw/install_docker.py +0 -209
  246. atomicshop/wrappers/elasticsearchw/infrastructure.py +0 -265
  247. atomicshop/wrappers/elasticsearchw/install_elastic.py +0 -232
  248. atomicshop/wrappers/ffmpegw.py +0 -125
  249. atomicshop/wrappers/loggingw/checks.py +0 -20
  250. atomicshop/wrappers/nodejsw/install_nodejs.py +0 -139
  251. atomicshop/wrappers/process_wrapper_pbtk.py +0 -16
  252. atomicshop/wrappers/socketw/base.py +0 -59
  253. atomicshop/wrappers/socketw/get_process.py +0 -107
  254. atomicshop/wrappers/wslw.py +0 -191
  255. atomicshop-2.11.47.dist-info/RECORD +0 -251
  256. /atomicshop/{addons/mains → a_mains}/FACT/factw_fact_extractor_docker_image_main_sudo.py +0 -0
  257. /atomicshop/{addons → a_mains/addons}/PlayWrightCodegen.cmd +0 -0
  258. /atomicshop/{addons → a_mains/addons}/ScriptExecution.cmd +0 -0
  259. /atomicshop/{addons/mains → a_mains/addons}/inits/init_to_import_all_modules.py +0 -0
  260. /atomicshop/{addons → a_mains/addons}/process_list/ReadMe.txt +0 -0
  261. /atomicshop/{addons/mains → a_mains}/search_for_hyperlinks_in_docx.py +0 -0
  262. /atomicshop/{archiver → etws}/__init__.py +0 -0
  263. /atomicshop/{etw → etws/traces}/__init__.py +0 -0
  264. /atomicshop/{monitor/checks/hash_checks → mitm/statistic_analyzer_helper}/__init__.py +0 -0
  265. /atomicshop/{wrappers/nodejsw → permissions}/__init__.py +0 -0
  266. /atomicshop/wrappers/pywin32w/{wmi_win32process.py → wmis/win32process.py} +0 -0
  267. {atomicshop-2.11.47.dist-info → atomicshop-3.10.5.dist-info/licenses}/LICENSE.txt +0 -0
  268. {atomicshop-2.11.47.dist-info → atomicshop-3.10.5.dist-info}/top_level.txt +0 -0
atomicshop/filesystem.py CHANGED
@@ -6,11 +6,15 @@ import shutil
6
6
  import stat
7
7
  import errno
8
8
  from contextlib import contextmanager
9
+ from typing import Literal, Union
10
+ import tempfile
9
11
 
10
- from .print_api import print_api, print_status_of_list
11
- from .basics import strings, list_of_dicts
12
+ # noinspection PyPackageRequirements
13
+ import psutil
14
+
15
+ from .basics import strings, list_of_dicts, list_of_classes
12
16
  from .file_io import file_io
13
- from . import hashing
17
+ from . import hashing, datetimes, print_api
14
18
 
15
19
 
16
20
  WINDOWS_DIRECTORY_SPECIAL_CHARACTERS = ['<', '>', ':', '"', '/', '\\', '|', '?', '*']
@@ -61,14 +65,18 @@ FILE_NAME_REPLACEMENT_DICT: dict = {
61
65
  }
62
66
 
63
67
 
68
+ # class TimeCouldNotBeFoundInFileNameError(Exception):
69
+ # pass
70
+
71
+
64
72
  def get_home_directory(return_sudo_user: bool = False) -> str:
65
73
  """
66
74
  Returns the home directory of the current user or the user who invoked sudo.
67
75
 
68
76
  :param return_sudo_user: bool, if 'False', then the function will return the home directory of the user who invoked
69
77
  sudo (if the script was invoked with sudo).
70
- If 'True', then the function will return the home directory of the current user, doesn't matter if the script was
71
- 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.
72
80
  """
73
81
 
74
82
  def return_home_directory_of_current_user():
@@ -113,6 +121,22 @@ def get_working_directory() -> str:
113
121
  return str(Path.cwd())
114
122
 
115
123
 
124
+ def get_temp_directory() -> str:
125
+ """
126
+ The function returns temporary directory of the system.
127
+
128
+ :return: string.
129
+ """
130
+
131
+ # Get the temporary directory in 8.3 format
132
+ short_temp_dir = tempfile.gettempdir()
133
+
134
+ # Convert to the long path name
135
+ long_temp_dir = str(Path(short_temp_dir).resolve())
136
+
137
+ return long_temp_dir
138
+
139
+
116
140
  def get_file_directory(file_path: str) -> str:
117
141
  """
118
142
  The function will return directory of the file.
@@ -189,16 +213,6 @@ def get_list_of_directories_in_file_path(
189
213
  return directory_list
190
214
 
191
215
 
192
- def get_file_name(file_path: str) -> str:
193
- """
194
- The function will return file name of the file.
195
-
196
- :param file_path: string, full file path.
197
- :return: string.
198
- """
199
- return str(Path(file_path).name)
200
-
201
-
202
216
  def check_absolute_path(filesystem_path) -> bool:
203
217
  """
204
218
  The function checks if the path provided is a full path (absolute) or relative.
@@ -220,12 +234,12 @@ def check_absolute_path___add_full(filesystem_path: str, full_path_to_add: str)
220
234
  """
221
235
 
222
236
  if not check_absolute_path(filesystem_path):
223
- 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)}'
224
238
  else:
225
239
  return filesystem_path
226
240
 
227
241
 
228
- def check_file_existence(file_path: str) -> bool:
242
+ def is_file_exists(file_path: str) -> bool:
229
243
  """
230
244
  Function to check if the path is a file.
231
245
 
@@ -240,7 +254,7 @@ def check_file_existence(file_path: str) -> bool:
240
254
  return False
241
255
 
242
256
 
243
- def check_directory_existence(directory_path: str) -> bool:
257
+ def is_directory_exists(directory_path: str) -> bool:
244
258
  """
245
259
  Function to check if a path is a directory.
246
260
 
@@ -265,16 +279,20 @@ def remove_file(file_path: str, **kwargs) -> bool:
265
279
 
266
280
  try:
267
281
  os.remove(file_path)
268
- print_api(f'File Removed: {file_path}')
282
+ print_api.print_api(f'File Removed: {file_path}')
269
283
  return True
270
284
  # Since the file doesn't exist, we actually don't care, since we want to remove it anyway.
271
285
  except FileNotFoundError:
272
286
  message = f'File Removal Failed, File non-existent: {file_path}'
273
- print_api(message, error_type=True, logger_method='critical', **kwargs)
287
+ print_api.print_api(message, error_type=True, logger_method='critical', **kwargs)
274
288
  return False
275
289
 
276
290
 
277
- 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:
278
296
  """
279
297
  Remove directory if it exists.
280
298
 
@@ -306,15 +324,69 @@ def remove_directory(directory_path: str, force_readonly: bool = False, print_kw
306
324
  shutil.rmtree(directory_path, onerror=remove_readonly)
307
325
  else:
308
326
  shutil.rmtree(directory_path)
309
- print_api(f'Directory Removed: {directory_path}', **print_kwargs)
327
+ print_api.print_api(f'Directory Removed: {directory_path}', **print_kwargs)
310
328
  return True
311
329
  # Since the directory doesn't exist, we actually don't care, since we want to remove it anyway.
312
330
  except FileNotFoundError:
313
331
  message = f'Directory Removal Failed, Directory non-existent: {directory_path}'
314
- 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)
315
333
  return False
316
334
 
317
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
+
318
390
  def create_directory(directory_fullpath: str):
319
391
  # Create directory if non-existent.
320
392
  # The library is used to create folder if it doesn't exist and won't raise exception if it does
@@ -323,41 +395,143 @@ def create_directory(directory_fullpath: str):
323
395
  pathlib.Path(directory_fullpath).mkdir(parents=True, exist_ok=True)
324
396
 
325
397
 
326
- def rename_file(source_file_path: str, target_file_path: str) -> None:
398
+ def rename_file(file_path: str, new_file_name: str) -> None:
327
399
  """
328
400
  The function renames file from source to target.
329
401
 
330
- :param source_file_path: string, full path to source file.
331
- :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.
332
404
 
333
405
  :return: None
334
406
  """
335
407
 
408
+ renamed_file_path = str(Path(file_path).parent) + os.sep + new_file_name
409
+
336
410
  # Rename file.
337
- os.rename(source_file_path, target_file_path)
411
+ os.rename(file_path, renamed_file_path)
338
412
 
339
413
 
340
- @contextmanager
341
- 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:
342
418
  """
343
- 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.
344
421
 
345
422
  :param file_path: string, full path to file.
346
- :param temp_file_path: string, temporary name to rename the file to.
347
- :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.
467
+
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
486
+
487
+ if rename_dictionary is None:
488
+ rename_dictionary = FILE_NAME_REPLACEMENT_DICT
489
+
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
348
497
 
349
- Usage:
350
- original_file = 'example.txt'
351
- temporary_file = 'temp_example.txt'
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)
352
501
 
353
- with temporary_rename(original_file, temporary_file):
354
- # Inside this block, the file exists as 'temp_example.txt'
355
- print(f"File is temporarily renamed to {temporary_file}")
356
- # Perform operations with the temporarily named file here
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
357
507
 
358
- # Outside the block, it's back to 'example.txt'
359
- print(f"File is renamed back to {original_file}")
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
360
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
+ """
361
535
 
362
536
  original_name = file_path
363
537
  try:
@@ -406,34 +580,86 @@ def temporary_change_working_directory(new_working_directory: str) -> None:
406
580
  os.chdir(original_working_directory)
407
581
 
408
582
 
409
- 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:
410
584
  """
411
585
  The function moves file from source to target.
412
586
 
413
587
  :param source_file_path: string, full path to source file.
414
- :param target_file_path: string, full path to target file.
588
+ :param target_directory: string, full path to target directory.
415
589
  :param overwrite: boolean, if 'False', then the function will not overwrite the file if it exists.
416
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
+
417
605
  :return: None
418
606
  """
419
607
 
608
+ target_file_path = target_directory + os.sep + Path(source_file_path).name
609
+
420
610
  # Check if 'no_overwrite' is set to 'True' and if the file exists.
421
611
  if not overwrite:
422
- if check_file_existence(target_file_path):
612
+ if is_file_exists(target_file_path):
423
613
  raise FileExistsError(f'File already exists: {target_file_path}')
424
614
 
425
615
  # Move file.
426
616
  shutil.move(source_file_path, target_file_path)
427
617
 
428
618
 
429
- def move_files_from_folder_to_folder(
619
+ def move_folder(source_directory: str, target_directory: str, overwrite: bool = True) -> None:
620
+ """
621
+ The function moves folder from source to target.
622
+
623
+ :param source_directory: string, full path to source directory.
624
+ :param target_directory: string, full path to target directory.
625
+ :param overwrite: boolean, if 'False', then the function will not overwrite the directory if it exists.
626
+
627
+ :return: None
628
+
629
+ ------------------------------
630
+
631
+ Example:
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)
640
+
641
+ Result path of the 'folder-to-move' will be:
642
+ 'C:/Users/user1/Documents/folder-to-move'
643
+
644
+ """
645
+
646
+ # Check if 'overwrite' is set to 'True' and if the directory exists.
647
+ if not overwrite:
648
+ if is_directory_exists(target_directory):
649
+ raise FileExistsError(f'Directory already exists: {target_directory}')
650
+
651
+ # Move directory.
652
+ shutil.move(source_directory, target_directory)
653
+
654
+
655
+ def move_top_level_files_from_folder_to_folder(
430
656
  source_directory: str,
431
657
  target_directory: str,
432
658
  overwrite: bool = True
433
659
  ):
434
660
  """
435
- The function is currently non-recursive and not tested with directories inside the source directories.
436
- 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.
437
663
 
438
664
  :param source_directory: string, full path to source directory.
439
665
  :param target_directory: string, full path to target directory.
@@ -441,26 +667,50 @@ def move_files_from_folder_to_folder(
441
667
  """
442
668
 
443
669
  # Iterate over each item in the source directory
444
- for item in os.listdir(source_directory):
445
- # Construct full file path
446
- source_item = os.path.join(source_directory, item)
447
- 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)
448
672
 
673
+ for source_item in top_level_files:
449
674
  # Move each item to the destination directory
450
- 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)
451
676
 
452
- # # Get all file names without full paths in source folder.
453
- # file_list_in_source: list = get_file_paths_from_directory(source_directory)
454
- #
455
- # # Iterate through all the files.
456
- # for file_path in file_list_in_source:
457
- # # Move the file from source to target.
458
- # if file_path['relative_dir']:
459
- # create_directory(target_directory + os.sep + file_path['relative_dir'])
460
- # relative_file_path: str = file_path['relative_dir'] + os.sep + Path(file_path['path']).name
461
- # else:
462
- # relative_file_path: str = Path(file_path['path']).name
463
- # 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)
464
714
 
465
715
 
466
716
  def copy_file(
@@ -482,7 +732,7 @@ def copy_file(
482
732
 
483
733
  # Check if 'no_overwrite' is set to 'True' and if the file exists.
484
734
  if no_overwrite:
485
- if check_file_existence(target_file_path):
735
+ if is_file_exists(target_file_path):
486
736
  raise FileExistsError(f'File already exists: {target_file_path}')
487
737
 
488
738
  # Copy file.
@@ -505,56 +755,136 @@ def copy_directory(source_directory: str, target_directory: str, overwrite: bool
505
755
 
506
756
  # Check if 'overwrite' is set to 'True' and if the directory exists.
507
757
  if overwrite:
508
- if check_directory_existence(target_directory):
758
+ if is_directory_exists(target_directory):
509
759
  remove_directory(target_directory)
510
760
 
511
761
  # Copy directory.
512
762
  shutil.copytree(source_directory, target_directory)
513
763
 
514
764
 
515
- def get_directory_paths_from_directory(
516
- directory_path: str,
517
- recursive: bool = True
518
- ) -> list:
765
+ def copy_files_from_folder_to_folder(source_directory: str, target_directory: str, overwrite: bool = False) -> None:
519
766
  """
520
- Recursive, by option.
521
- The function receives a filesystem directory as string, scans it recursively for directories and returns list of
522
- full paths to that directory (including).
767
+ The function will copy all the files from source directory to target directory.
523
768
 
524
- :param directory_path: string to full path to directory on the filesystem to scan.
525
- :param recursive: boolean.
526
- 'True', then the function will scan recursively in subdirectories.
527
- 'False', then the function will scan only in the directory that was passed.
528
-
529
- :return: list of all found directory names with full paths.
769
+ :param source_directory: string, full path to source directory.
770
+ :param target_directory: string, full path to target directory.
771
+ :param overwrite: boolean, if 'True', then the function will overwrite the files if they exist.
530
772
  """
773
+ # Make sure the destination directory exists, if not create it
774
+ os.makedirs(target_directory, exist_ok=True)
531
775
 
532
- # Define locals.
533
- directory_list: list = list()
534
-
535
- # "Walk" over all the directories and subdirectories - make list of full directory paths inside the directory
536
- # recursively.
537
- for dirpath, subdirs, files in os.walk(directory_path):
538
- # Iterate through all the directory names that were found in the folder.
539
- for directory in subdirs:
540
- # Get full directory path.
541
- directory_list.append(os.path.join(dirpath, directory))
542
-
543
- if not recursive:
544
- break
776
+ # Copy contents of the source directory to the destination directory
777
+ for item in os.listdir(source_directory):
778
+ s = os.path.join(source_directory, item)
779
+ d = os.path.join(target_directory, item)
545
780
 
546
- return directory_list
781
+ if os.path.isdir(s):
782
+ if os.path.exists(d) and not overwrite:
783
+ print(f"Directory {d} already exists. Skipping due to overwrite=False.")
784
+ else:
785
+ shutil.copytree(s, d, dirs_exist_ok=overwrite)
786
+ else:
787
+ if os.path.exists(d) and not overwrite:
788
+ print(f"File {d} already exists. Skipping due to overwrite=False.")
789
+ else:
790
+ shutil.copy2(s, d)
791
+
792
+
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)
547
870
 
548
871
 
549
- def get_file_paths_from_directory(
872
+ def get_paths_from_directory(
550
873
  directory_path: str,
874
+ simple_list: bool = False,
875
+ get_file: bool = False,
876
+ get_directory: bool = False,
551
877
  recursive: bool = True,
552
878
  file_name_check_pattern: str = '*',
879
+ datetime_format: str = None,
880
+ specific_date: str = None,
553
881
  add_relative_directory: bool = False,
554
882
  relative_file_name_as_directory: bool = False,
555
883
  add_last_modified_time: bool = False,
556
- sort_by_last_modified_time: bool = False
557
- ):
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]]:
558
888
  """
559
889
  Recursive, by option.
560
890
  The function receives a filesystem directory as string, scans it recursively for files and returns list of
@@ -563,15 +893,23 @@ def get_file_paths_from_directory(
563
893
  of that tuple.
564
894
 
565
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.
566
899
  :param recursive: boolean.
567
900
  'True', then the function will scan recursively in subdirectories.
568
901
  'False', then the function will scan only in the directory that was passed.
569
902
  :param file_name_check_pattern: string, if specified, the function will return only files that match the pattern.
570
903
  The string can contain part of file name to check or full file name with extension.
571
904
  Can contain wildcards.
572
- If you need to specify a "." in the pattern, you need to escape it with a backslash:
573
- Example: "*\.txt" will return all files with the extension ".txt".
574
- 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.
575
913
  :param add_relative_directory: boolean, if
576
914
  'True', then the function will add relative directory to the output list.
577
915
  In this case the output list will contain dictionaries with keys 'path' and 'relative_dir'.
@@ -583,47 +921,72 @@ def get_file_paths_from_directory(
583
921
  to the output list.
584
922
  :param sort_by_last_modified_time: boolean, if 'True', then the function will sort the output list by last
585
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.
586
928
 
587
929
  :return: list of all found filenames with full file paths, list with relative folders to file excluding the
588
930
  main folder.
589
931
  """
590
932
 
591
- def get_file():
933
+ def get_path(file_or_directory: str):
592
934
  """
593
935
  Function gets the full file path, adds it to the found 'object_list' and gets the relative path to that
594
936
  file, against the main path to directory that was passed to the parent function.
595
937
  """
596
938
 
597
- 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)
598
941
 
599
- if not add_relative_directory and not add_last_modified_time:
600
- file_result: str = file_path
601
- else:
602
- file_result: dict = dict()
942
+ if simple_list:
943
+ object_list.append(file_or_dir_path)
944
+ return
603
945
 
604
- # Get full file path of the file.
605
- file_result['file_path'] = file_path
946
+ path_object: AtomicPath = AtomicPath(path=file_or_dir_path)
947
+ path_object.queried_directory = directory_path
606
948
 
607
949
  if add_relative_directory:
608
950
  # if 'relative_file_name_as_directory' was passed.
609
951
  if relative_file_name_as_directory:
610
952
  # Output the path with filename.
611
- file_result['relative_dir'] = _get_relative_output_path_from_input_path(
612
- directory_path, dirpath, file)
953
+ path_object.relative_dir = _get_relative_output_path_from_input_path(
954
+ directory_path, dir_path, file_or_directory)
613
955
  # if 'relative_file_name_as_directory' wasn't passed.
614
956
  else:
615
957
  # Output the path without filename.
616
- 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)
617
960
 
618
961
  # Remove separator from the beginning if exists.
619
- file_result['relative_dir'] = file_result['relative_dir'].removeprefix(os.sep)
962
+ path_object.relative_dir = path_object.relative_dir.removeprefix(os.sep)
620
963
 
621
964
  # If 'add_last_modified_time' was passed.
622
965
  if add_last_modified_time:
623
966
  # Get last modified time of the file.
624
- 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
625
975
 
626
- object_list.append(file_result)
976
+ if specific_date:
977
+ if path_object.datetime_string != specific_date:
978
+ return
979
+
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".')
627
990
 
628
991
  if sort_by_last_modified_time and not add_last_modified_time:
629
992
  raise ValueError('Parameter "sort_by_last_modified_time" cannot be "True" if parameter '
@@ -632,18 +995,25 @@ def get_file_paths_from_directory(
632
995
  raise ValueError('Parameter "relative_file_name_as_directory" cannot be "True" if parameter '
633
996
  '"add_relative_directory" is not "True".')
634
997
 
998
+ if not datetime_format and specific_date:
999
+ raise ValueError('If "specific_date" is specified, "datetime_format" must be specified.')
1000
+
635
1001
  # === Function main ================
636
1002
  # Define locals.
637
1003
  object_list: list = list()
638
1004
 
639
1005
  # "Walk" over all the directories and subdirectories - make list of full file paths inside the directory
640
1006
  # recursively.
641
- for dirpath, subdirs, files in os.walk(directory_path):
642
- # Iterate through all the file names that were found in the folder.
643
- for file in files:
644
- # If 'file_name_check_pattern' was passed.
645
- if strings.match_pattern_against_string(file_name_check_pattern, file):
646
- 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)
647
1017
 
648
1018
  if not recursive:
649
1019
  break
@@ -651,7 +1021,34 @@ def get_file_paths_from_directory(
651
1021
  # If 'sort_by_last_modified_time' was passed.
652
1022
  if sort_by_last_modified_time:
653
1023
  # Sort the list by last modified time.
654
- 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)
655
1052
 
656
1053
  return object_list
657
1054
 
@@ -726,17 +1123,6 @@ def remove_last_separator(directory_path: str) -> str:
726
1123
  return directory_path.removesuffix(os.sep)
727
1124
 
728
1125
 
729
- def remove_first_separator(filesystem_path: str) -> str:
730
- """
731
- The function removes the first character in 'filesystem_path' if it is a separator returning the processed string.
732
- If the first character is not a separator, nothing is happening.
733
-
734
- :param filesystem_path:
735
- :return:
736
- """
737
- return filesystem_path.removesuffix(os.sep)
738
-
739
-
740
1126
  def add_last_separator(filesystem_path: str) -> str:
741
1127
  """
742
1128
  The function adds a separator to the end of the path if it doesn't exist.
@@ -772,14 +1158,14 @@ def get_files_and_folders(directory_path: str, string_contains: str = str()):
772
1158
  return files_folders_list
773
1159
 
774
1160
 
775
- def get_file_modified_time(file_path: str) -> float:
1161
+ def get_file_modified_time(file_or_dir_path: str) -> float:
776
1162
  """
777
1163
  The function returns the time of last modification of the file in seconds since the epoch.
778
1164
 
779
- :param file_path: string, full path to file.
1165
+ :param file_or_dir_path: string, full path to file or directory.
780
1166
  :return: float, time of last modification of the file in seconds since the epoch.
781
1167
  """
782
- return os.path.getmtime(file_path)
1168
+ return os.path.getmtime(file_or_dir_path)
783
1169
 
784
1170
 
785
1171
  def change_last_modified_date_of_file(file_path: str, new_date: float) -> None:
@@ -801,43 +1187,6 @@ def change_last_modified_date_of_file(file_path: str, new_date: float) -> None:
801
1187
  os.utime(file_path, (new_date, new_date))
802
1188
 
803
1189
 
804
- def get_file_hashes_from_directory(directory_path: str, recursive: bool = False, add_binary: bool = False) -> list:
805
- """
806
- The function scans a directory for files and returns a list of dictionaries with file path and hash of the file.
807
- Binary option can be specified.
808
-
809
- :param directory_path: string, of full path to directory you want to return file names of.
810
- :param recursive: boolean.
811
- 'True', then the function will scan recursively in subdirectories.
812
- 'False', then the function will scan only in the directory that was passed.
813
- :param add_binary: boolean, if 'True', then the function will add the binary of the file to the output list.
814
-
815
- :return: list of dicts with full file paths, hashes and binaries (if specified).
816
- """
817
-
818
- # Get all the files.
819
- file_paths_list = get_file_paths_from_directory(directory_path, recursive=recursive)
820
-
821
- # Create a list of dictionaries, each dictionary is a file with its hash.
822
- files: list = list()
823
- for file_index, file_path in enumerate(file_paths_list):
824
- print_status_of_list(
825
- list_instance=file_paths_list, prefix_string=f'Reading File: ', current_state=(file_index + 1))
826
-
827
- file_info: dict = dict()
828
- file_info['path'] = file_path['path']
829
-
830
- if add_binary:
831
- file_info['binary'] = file_io.read_file(file_path['path'], file_mode='rb', stdout=False)
832
- file_info['hash'] = hashing.hash_bytes(file_info['binary'])
833
- else:
834
- file_info['hash'] = hashing.hash_file(file_path['path'])
835
-
836
- files.append(file_info)
837
-
838
- return files
839
-
840
-
841
1190
  def find_duplicates_by_hash(
842
1191
  directory_path: str,
843
1192
  recursive: bool = False,
@@ -856,33 +1205,34 @@ def find_duplicates_by_hash(
856
1205
  """
857
1206
 
858
1207
  # Get all the files.
859
- 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)
860
1210
 
861
1211
  same_hash_files: list = list()
862
1212
  # Check if there are files that have exactly the same hash.
863
- for file_dict in files:
1213
+ for atomic_path in files:
864
1214
  # Create a list of files that have the same hash for current 'firmware'.
865
1215
  current_run_list: list = list()
866
- for file_dict_compare in files:
1216
+ for atomic_path_compare in files:
867
1217
  # Add all the 'firmware_compare' that have the same hash to the list.
868
- if (file_dict['hash'] == file_dict_compare['hash'] and
869
- 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):
870
1220
  # Check if current 'firmware' is already in the 'same_hash_files' list. If not, add 'firmware_compare'
871
1221
  # to the 'current_run_list'.
872
1222
  if not any(list_of_dicts.is_value_exist_in_key(
873
- 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
874
1224
  test_hash in same_hash_files):
875
1225
  current_run_list.append({
876
- 'path': file_dict_compare['path'],
877
- 'hash': file_dict_compare['hash']
1226
+ 'path': atomic_path_compare.path,
1227
+ 'hash': atomic_path_compare.hash
878
1228
  })
879
1229
 
880
1230
  if current_run_list:
881
1231
  # After the iteration of the 'firmware_compare' finished and the list is not empty, add the 'firmware'
882
1232
  # to the list.
883
1233
  current_run_list.append({
884
- 'path': file_dict['path'],
885
- 'hash': file_dict['hash']
1234
+ 'path': atomic_path.path,
1235
+ 'hash': atomic_path.hash
886
1236
  })
887
1237
  same_hash_files.append(current_run_list)
888
1238
 
@@ -963,39 +1313,40 @@ def get_directory_size(directory_path: str):
963
1313
 
964
1314
 
965
1315
  def get_subpaths_between(start_path: str, end_path: str) -> list[str]:
1316
+ # noinspection GrazieInspection
966
1317
  """
967
- Get the subpaths between two paths.
968
- :param start_path: string, start path.
969
- :param end_path: string, end path.
970
- :return:
1318
+ Get the subpaths between two paths.
1319
+ :param start_path: string, start path.
1320
+ :param end_path: string, end path.
1321
+ :return:
971
1322
 
972
- Example Linux:
973
- start_path = '/test/1'
974
- end_path = '/test/1/2/3/4'
1323
+ Example Linux:
1324
+ start_path = '/test/1'
1325
+ end_path = '/test/1/2/3/4'
975
1326
 
976
- subpaths = get_subpaths_between(start_path, end_path)
1327
+ subpaths = get_subpaths_between(start_path, end_path)
977
1328
 
978
- subpaths = [
979
- '/test/1'
980
- '/test/1/2',
981
- '/test/1/2/3',
982
- '/test/1/2/3/4',
983
- ]
1329
+ subpaths = [
1330
+ '/test/1'
1331
+ '/test/1/2',
1332
+ '/test/1/2/3',
1333
+ '/test/1/2/3/4',
1334
+ ]
984
1335
 
985
1336
 
986
- Example Windows:
987
- start_path = 'C:\\test\\1'
988
- 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'
989
1340
 
990
- subpaths = get_subpaths_between(start_path, end_path)
1341
+ subpaths = get_subpaths_between(start_path, end_path)
991
1342
 
992
- subpaths = [
993
- 'C:\\test\\1',
994
- 'C:\\test\\1\\2',
995
- 'C:\\test\\1\\2\\3',
996
- 'C:\\test\\1\\2\\3\\4',
997
- ]
998
- """
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
+ """
999
1350
 
1000
1351
  # Detect slash type based on the input (default to forward slash)
1001
1352
  slash_type = "\\" if "\\" in start_path else "/"
@@ -1036,13 +1387,13 @@ def get_subpaths_between(start_path: str, end_path: str) -> list[str]:
1036
1387
  # else:
1037
1388
  # raise ValueError("Start path must be a parent of the end path")
1038
1389
  #
1039
- # # Reverse the list so it goes from start to end.
1390
+ # # Reverse the list, so it goes from start to end.
1040
1391
  # subpaths.reverse()
1041
1392
  #
1042
1393
  # return subpaths
1043
1394
 
1044
1395
 
1045
- def create_dict_of_paths_list(list_of_paths: list) -> dict:
1396
+ def create_dict_of_paths_list(list_of_paths: list) -> list:
1046
1397
  """
1047
1398
  The function receives a list of paths and returns a dictionary with keys as the paths and values as the
1048
1399
  subpaths of the key path.
@@ -1078,7 +1429,7 @@ def create_dict_of_paths_list(list_of_paths: list) -> dict:
1078
1429
  :return: dictionary.
1079
1430
  """
1080
1431
 
1081
- structure = []
1432
+ structure: list = []
1082
1433
  for path in list_of_paths:
1083
1434
  create_dict_of_path(path, structure)
1084
1435
  return structure
@@ -1086,70 +1437,20 @@ def create_dict_of_paths_list(list_of_paths: list) -> dict:
1086
1437
 
1087
1438
  def create_dict_of_path(
1088
1439
  path: str,
1089
- # structure_dict: dict,
1090
1440
  structure_list: list,
1091
- add_data_to_entry: any = None,
1092
- add_data_key: str = 'addon',
1093
- parent_entry: str = None
1441
+ add_data_to_entry: list[dict[str, any]] = None
1094
1442
  ):
1095
1443
  """
1096
1444
  The function receives a path and a list, and adds the path to the list.
1097
-
1098
1445
  Check the working example from 'create_dict_of_paths_list' function.
1099
1446
 
1100
1447
  :param path: string, path.
1101
1448
  :param structure_list: list to add the path to.
1102
- :param add_data_to_entry: any, data to add to the entry.
1103
- :param add_data_key: string, key to add the data to.
1104
- :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}
1105
1451
  :return:
1106
1452
  """
1107
1453
 
1108
- # # Normalize path for cross-platform compatibility
1109
- # normalized_path = path.replace("\\", "/")
1110
- # parts = normalized_path.strip("/").split("/")
1111
- # current_level = structure_dict
1112
- #
1113
- # for part in parts[:-1]: # Iterate through the directories
1114
- # # If the part is not already a key in the current level of the structure, add it
1115
- # if part not in current_level:
1116
- # current_level[part] = {}
1117
- # current_level = current_level[part]
1118
- #
1119
- # # Create the entry for the file with additional data
1120
- # file_entry = {"entry": parts[-1], add_data_key: add_data_to_entry}
1121
- #
1122
- # # We're adding file entries under numeric keys.
1123
- # if isinstance(current_level, dict) and all(isinstance(key, int) for key in current_level.keys()):
1124
- # current_level[len(current_level)] = file_entry
1125
- # else:
1126
- # # Handle cases where there's a mix of numeric keys and directory names
1127
- # # Find the next available numeric key
1128
- # next_key = max([key if isinstance(key, int) else -1 for key in current_level.keys()], default=-1) + 1
1129
- # current_level[next_key] = file_entry
1130
-
1131
- # entries_key_name = "__entries__"
1132
- #
1133
- # # Normalize path for cross-platform compatibility
1134
- # normalized_path = path.replace("\\", "/")
1135
- # parts = normalized_path.strip("/").split("/")
1136
- # current_level = structure_dict
1137
- #
1138
- # for part in parts[:-1]: # Navigate through or create directory structure
1139
- # if part not in current_level:
1140
- # current_level[part] = {}
1141
- # current_level = current_level[part]
1142
- #
1143
- # # Create the entry for the file with additional data
1144
- # file_entry = {"entry": parts[-1], add_data_key: add_data_to_entry}
1145
- #
1146
- # # If the current level (final directory) does not have an "entries" key for files, create it
1147
- # if entries_key_name not in current_level:
1148
- # current_level[entries_key_name] = []
1149
- #
1150
- # # Append the file entry to the list associated with the "entries" key
1151
- # current_level[entries_key_name].append(file_entry)
1152
-
1153
1454
  # Normalize path for cross-platform compatibility
1154
1455
  normalized_path = path.replace("\\", "/")
1155
1456
  parts = normalized_path.strip("/").split("/")
@@ -1157,75 +1458,345 @@ def create_dict_of_path(
1157
1458
  current_level = structure_list
1158
1459
 
1159
1460
  for i, part in enumerate(parts):
1160
- # 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)
1161
1462
  is_last_part = (i == len(parts) - 1)
1162
1463
 
1163
1464
  # Try to find an existing entry for this part
1164
1465
  existing_entry = next((item for item in current_level if item["entry"] == part), None)
1165
1466
 
1166
1467
  if existing_entry is None:
1167
- # For the last part, add the additional data; for directories, just create the structure
1168
- if is_last_part:
1169
- new_entry = {"entry": part, add_data_key: add_data_to_entry, "included": []}
1170
- else:
1171
- 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)
1172
1475
 
1173
1476
  current_level.append(new_entry)
1477
+
1174
1478
  # Only update current_level if it's not the last part
1175
1479
  if not is_last_part:
1176
1480
  current_level = new_entry["included"]
1177
1481
  else:
1178
- # 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
1179
1483
  if not is_last_part:
1180
1484
  current_level = existing_entry["included"]
1181
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
+
1491
+
1492
+ def list_open_files_in_directory(directory):
1493
+ """
1494
+ The function lists all open files by any processes in the directory.
1495
+ :param directory:
1496
+ :return:
1497
+ """
1498
+ open_files: list = []
1499
+
1500
+ # Iterate over all running processes
1501
+ for proc in psutil.process_iter(['pid', 'name']):
1502
+ try:
1503
+ # List open files for the process
1504
+ proc_open_files = proc.open_files()
1505
+ for file in proc_open_files:
1506
+ if file.path.startswith(directory):
1507
+ # noinspection PyUnresolvedReferences
1508
+ open_files.append((proc.info['pid'], proc.info['name'], file.path))
1509
+ except (psutil.AccessDenied, psutil.NoSuchProcess):
1510
+ # Ignore processes that can't be accessed
1511
+ continue
1512
+
1513
+ return open_files
1182
1514
 
1183
- def is_any_file_in_directory_locked(directory_path: str) -> bool:
1515
+
1516
+ def is_any_file_in_directory_opened_by_process(directory_path: str) -> bool:
1184
1517
  """
1185
- The function checks if any file in the directory is locked (if the file is currently being written to that directory
1186
- it will be locked for reading). Basically it opens a handle for read and write and if it fails, then the file is
1187
- locked.
1518
+ The function checks if any file in the directory is opened in any process using psutil.
1188
1519
 
1189
1520
  :param directory_path: string, full path to directory.
1190
- :return: boolean, if 'True', then at least one file in the directory is locked.
1191
- """
1521
+ :return: boolean, if 'True', then at least one file in the directory is opened by a process.
1522
+ """
1523
+
1524
+ # for filename in os.listdir(directory_path):
1525
+ # file_path = os.path.join(directory_path, filename)
1526
+ # if os.path.isfile(file_path):
1527
+ # return is_file_locked(file_path)
1528
+ # return False
1529
+
1530
+ # Iterate over all running processes
1531
+ for proc in psutil.process_iter(['pid', 'name']):
1532
+ try:
1533
+ # List open files for the process
1534
+ proc_open_files = proc.open_files()
1535
+ for file in proc_open_files:
1536
+ if file.path.startswith(directory_path):
1537
+ return True
1538
+ except (psutil.AccessDenied, psutil.NoSuchProcess):
1539
+ # Ignore processes that can't be accessed
1540
+ continue
1192
1541
 
1193
- for filename in os.listdir(directory_path):
1194
- file_path = os.path.join(directory_path, filename)
1195
- if os.path.isfile(file_path):
1196
- return is_file_locked(file_path)
1197
1542
  return False
1198
1543
 
1199
1544
 
1200
- def is_any_file_locked_in_list(file_paths_list: list[str]) -> bool:
1545
+ def is_any_file_in_list_open_by_process(file_paths_list: list[str]) -> bool:
1201
1546
  """
1202
- The function checks if any file in the list is locked (if the file is currently being written to it will be locked
1203
- for reading). Basically it opens a handle for read and write and if it fails, then the file is locked.
1547
+ The function checks if any file in the list is opened by any running process.
1204
1548
 
1205
1549
  :param file_paths_list: list of strings, full paths to files.
1206
1550
  :return: boolean, if 'True', then at least one file in the list is locked.
1207
1551
  """
1208
1552
 
1209
1553
  for file_path in file_paths_list:
1210
- if is_file_locked(file_path):
1554
+ if is_file_open_by_process(file_path):
1211
1555
  return True
1212
1556
  return False
1213
1557
 
1214
1558
 
1215
- def is_file_locked(file_path: str) -> bool:
1559
+ def is_file_open_by_process(file_path: str) -> bool:
1216
1560
  """
1217
- The function checks if the file is locked (if the file is currently being written to it will be locked for reading).
1218
- Basically it opens a handle for read and write and if it fails, then the file is locked.
1561
+ The function checks if the file is opened in any of the running processes.
1219
1562
 
1220
1563
  :param file_path: string, full path to file.
1221
1564
  :return: boolean, if 'True', then the file is locked.
1222
1565
  """
1223
1566
 
1224
- try:
1225
- # Attempt to open the file exclusively
1226
- with open(file_path, 'rb+') as f:
1227
- pass
1228
- except IOError:
1229
- # If we can't open the file, it might be in the process of being copied
1230
- return True
1567
+ # If the file doesn't exist, or it is not a file, it's not locked.
1568
+ if not os.path.isfile(file_path):
1569
+ return False
1570
+
1571
+ # Iterate over all running processes
1572
+ for proc in psutil.process_iter(['pid', 'name']):
1573
+ try:
1574
+ # List open files for the process
1575
+ proc_open_files = proc.open_files()
1576
+ for file in proc_open_files:
1577
+ if file.path == file_path:
1578
+ return True
1579
+ except (psutil.AccessDenied, psutil.NoSuchProcess):
1580
+ # Ignore processes that can't be accessed
1581
+ continue
1582
+
1231
1583
  return False
1584
+
1585
+
1586
+ def get_download_directory(
1587
+ place: Literal[
1588
+ 'temp',
1589
+ 'script',
1590
+ 'working'] = 'temp',
1591
+ script_path: str = None
1592
+ ) -> str:
1593
+ """
1594
+ The function returns the default download directory based on place.
1595
+
1596
+ :param place: string,
1597
+ 'temp', then the function will return the temporary directory.
1598
+ 'script', then the function will return the directory of the script.
1599
+ 'working', then the function will return the working directory.
1600
+ :param script_path: string, full path to the script.
1601
+ :return: string, full path to the default download directory.
1602
+ """
1603
+
1604
+ if place == 'script' and script_path is None:
1605
+ raise ValueError("Script path must be specified if place is 'script'.")
1606
+
1607
+ # Get the download directory based on the operating system
1608
+ if place == 'script':
1609
+ download_directory = get_file_directory(script_path)
1610
+ elif place == 'working':
1611
+ download_directory = get_working_directory()
1612
+ elif place == 'temp':
1613
+ download_directory = get_temp_directory()
1614
+ else:
1615
+ raise ValueError("Invalid place specified.")
1616
+
1617
+ return download_directory
1618
+
1619
+
1620
+ def backup_folder(directory_path: str, backup_directory: str) -> None:
1621
+ """
1622
+ Backup the specified directory.
1623
+
1624
+ :param directory_path: The directory path to back up.
1625
+ :param backup_directory: The directory to back up the directory to.
1626
+
1627
+ Example:
1628
+ backup_folder(
1629
+ directory_path='C:\\Users\\user1\\Downloads\\folder1', backup_directory='C:\\Users\\user1\\Downloads\\backup')
1630
+
1631
+ Backed up folder will be moved to 'C:\\Users\\user1\\Downloads\\backup' with timestamp in the name.
1632
+ Final path will look like: 'C:\\Users\\user1\\Downloads\\backup\\20231003-120000-000000_folder1'
1633
+ """
1634
+
1635
+ if is_directory_exists(directory_path):
1636
+ timestamp: str = datetimes.TimeFormats().get_current_formatted_time_filename_stamp(True)
1637
+ directory_name = Path(directory_path).name
1638
+ backup_directory_path: str = str(Path(backup_directory) / f"{timestamp}_{directory_name}")
1639
+ create_directory(backup_directory_path)
1640
+ move_folder(directory_path, backup_directory_path)
1641
+
1642
+
1643
+ def backup_file(
1644
+ file_path: str,
1645
+ backup_directory: str,
1646
+ timestamp_as_prefix: bool = False
1647
+ ) -> Union[str, None]:
1648
+ """
1649
+ Backup the specified file.
1650
+
1651
+ :param file_path: The file path to back up.
1652
+ :param backup_directory: The directory to back up the file to.
1653
+ :param timestamp_as_prefix: boolean, if
1654
+ True, then the timestamp will be added as a prefix to the file name.
1655
+ False, then the timestamp will be added as a suffix to the file name.
1656
+ -----------------------------------------
1657
+ Example:
1658
+ backup_file(
1659
+ file_path='C:\\Users\\user1\\Downloads\\file.txt',
1660
+ backup_directory='C:\\Users\\user1\\Downloads\\backup',
1661
+ timestamp_as_prefix=True
1662
+ )
1663
+
1664
+ Backed up file will be moved to 'C:\\Users\\user1\\Downloads\\backup' with timestamp in the name.
1665
+ Final path will look like: 'C:\\Users\\user1\\Downloads\\backup\\20231003-120000-000000_file.txt'
1666
+ ---------------------------------------------
1667
+ Example when timestamp_as_prefix is False:
1668
+ backup_file(
1669
+ file_path='C:\\Users\\user1\\Downloads\\file.txt',
1670
+ backup_directory='C:\\Users\\user1\\Downloads\\backup',
1671
+ timestamp_as_prefix=False
1672
+ )
1673
+
1674
+ Backed up file will be moved to 'C:\\Users\\user1\\Downloads\\backup' with timestamp in the name.
1675
+ Final path will look like: 'C:\\Users\\user1\\Downloads\\backup\\file_20231003-120000-000000.txt'
1676
+ """
1677
+
1678
+ if is_file_exists(file_path):
1679
+ timestamp: str = datetimes.TimeFormats().get_current_formatted_time_filename_stamp(True)
1680
+ file_name_no_extension = Path(file_path).stem
1681
+ file_extension = Path(file_path).suffix
1682
+ if timestamp_as_prefix:
1683
+ file_name: str = f"{timestamp}_{file_name_no_extension}{file_extension}"
1684
+ else:
1685
+ file_name: str = f"{file_name_no_extension}_{timestamp}{file_extension}"
1686
+ backup_file_path: str = str(Path(backup_directory) / file_name)
1687
+ rename_file(file_path, file_name)
1688
+
1689
+ return backup_file_path
1690
+ else:
1691
+ return None
1692
+
1693
+
1694
+ def find_file(file_name: str, directory_path: str):
1695
+ """
1696
+ The function finds the file in the directory recursively.
1697
+ :param file_name: string, The name of the file to find.
1698
+ :param directory_path: string, The directory to search in.
1699
+ :return:
1700
+ """
1701
+ for dir_path, dir_names, filenames in os.walk(directory_path):
1702
+ for filename in filenames:
1703
+ if filename == file_name:
1704
+ return os.path.join(dir_path, filename)
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)