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/datetimes.py CHANGED
@@ -2,6 +2,8 @@ import datetime
2
2
  from datetime import timedelta
3
3
  import time
4
4
  import random
5
+ import re
6
+ from typing import Union
5
7
 
6
8
 
7
9
  class MonthToNumber:
@@ -46,6 +48,102 @@ class MonthToNumber:
46
48
  'דצמבר': '12'}
47
49
 
48
50
 
51
+ # Mapping of datetime format specifiers to regex patterns
52
+ DATE_TIME_STRING_FORMAT_SPECIFIERS_TO_REGEX: dict = {
53
+ '%Y': r'\d{4}', # Year with century
54
+ '%m': r'\d{2}', # Month as a zero-padded decimal number
55
+ '%d': r'\d{2}', # Day of the month as a zero-padded decimal number
56
+ '%H': r'\d{2}', # Hour (24-hour clock) as a zero-padded decimal number
57
+ '%I': r'\d{2}', # Hour (12-hour clock) as a zero-padded decimal number
58
+ '%M': r'\d{2}', # Minute as a zero-padded decimal number
59
+ '%S': r'\d{2}', # Second as a zero-padded decimal number
60
+ '%f': r'\d{6}', # Microsecond as a decimal number, zero-padded on the left
61
+ '%j': r'\d{3}', # Day of the year as a zero-padded decimal number
62
+ '%U': r'\d{2}', # Week number of the year (Sunday as the first day of the week)
63
+ '%W': r'\d{2}', # Week number of the year (Monday as the first day of the week)
64
+ '%w': r'\d', # Weekday as a decimal number (0 = Sunday, 6 = Saturday)
65
+ '%y': r'\d{2}', # Year without century
66
+ '%p': r'(AM|PM)', # AM or PM
67
+ '%z': r'[+-]\d{4}', # UTC offset in the form ±HHMM
68
+ '%Z': r'[A-Z]+', # Time zone name
69
+ '%%': r'%' # Literal '%'
70
+ }
71
+
72
+
73
+ def get_datetime_from_complex_string_by_pattern(
74
+ complex_string: str,
75
+ date_pattern: str
76
+ ) -> tuple[
77
+ Union[datetime.datetime, None],
78
+ Union[str, None],
79
+ Union[float, None]
80
+ ]:
81
+ """
82
+ Function will get datetime object from a complex string by pattern.
83
+
84
+ :param complex_string: string that contains date and time.
85
+ :param date_pattern: pattern that will be used to extract date and time from the string.
86
+ :return: tuple(datetime object, date string, timestamp float)
87
+ """
88
+
89
+ # Convert the date pattern to regex pattern
90
+ regex_pattern = re.sub(r'%[a-zA-Z]', r'\\d+', date_pattern)
91
+
92
+ # Find the date part in the file name using the regex pattern
93
+ date_str = re.search(regex_pattern, complex_string)
94
+
95
+ if date_str:
96
+ # Convert the date string to a datetime object based on the given pattern
97
+ date_obj = datetime.datetime.strptime(date_str.group(), date_pattern)
98
+ date_timestamp = date_obj.timestamp()
99
+ return date_obj, date_str.group(), date_timestamp
100
+ else:
101
+ return None, None, None
102
+
103
+
104
+ def datetime_format_to_regex(format_str: str) -> str:
105
+ """
106
+ Convert a datetime format string to a regex pattern.
107
+
108
+ :param format_str: The datetime format string to convert.
109
+ :return: The regex pattern that matches the format string.
110
+
111
+ Example:
112
+ datetime_format_to_regex("%Y-%m-%d")
113
+
114
+ Output:
115
+ '^\\d{4}-\\d{2}-\\d{2}$'
116
+ """
117
+
118
+ # Escape all non-format characters
119
+ escaped_format_str = re.escape(format_str)
120
+
121
+ # Replace escaped format specifiers with their regex equivalents
122
+ for specifier, regex in DATE_TIME_STRING_FORMAT_SPECIFIERS_TO_REGEX.items():
123
+ escaped_format_str = escaped_format_str.replace(re.escape(specifier), regex)
124
+
125
+ # Return the full regex pattern with start and end anchors
126
+ return f"^{escaped_format_str}$"
127
+
128
+
129
+ def extract_datetime_format_from_string(complex_string: str) -> Union[str, None]:
130
+ """
131
+ Extract the datetime format from the suffix used in TimedRotatingFileHandler.
132
+
133
+ Args:
134
+ - suffix: The suffix string from the handler.
135
+
136
+ Returns:
137
+ - The datetime format string, or None if it cannot be determined.
138
+ """
139
+ # Regular expression to match datetime format components in the suffix
140
+ datetime_format_regex = r"%[a-zA-Z]"
141
+ matches = re.findall(datetime_format_regex, complex_string)
142
+ if matches:
143
+ return "".join(matches)
144
+ return None
145
+
146
+
49
147
  def convert_single_digit_to_zero_padded(string: str):
50
148
  """
51
149
  Function will check if string is a single character digit and will add zero in front of it.
atomicshop/diff_check.py CHANGED
@@ -1,9 +1,13 @@
1
+ import datetime
1
2
  from pathlib import Path
2
- from typing import Union
3
+ from typing import Union, Literal
4
+ import json
5
+ import queue
3
6
 
7
+ from . import filesystem, timer
4
8
  from .file_io import file_io, jsons
5
9
  from .print_api import print_api
6
- from .basics import list_of_dicts
10
+ from .basics import list_of_dicts, dicts
7
11
 
8
12
 
9
13
  class DiffChecker:
@@ -25,10 +29,21 @@ class DiffChecker:
25
29
  self,
26
30
  check_object: any = None,
27
31
  check_object_display_name: str = None,
28
- aggregation: bool = False,
29
32
  input_file_path: str = None,
30
33
  input_file_write_only: bool = True,
31
- return_first_cycle: bool = True
34
+ return_first_cycle: bool = True,
35
+ operation_type: Literal[
36
+ 'new_objects',
37
+ 'hit_statistics',
38
+ 'all_objects',
39
+ 'single_object'] = None,
40
+ hit_statistics_input_file_rotation_cycle_hours: Union[
41
+ float,
42
+ Literal['midnight'],
43
+ None] = None,
44
+ hit_statistics_enable_queue: bool = False,
45
+ new_objects_hours_then_difference: float = None
46
+
32
47
  ):
33
48
  """
34
49
  :param check_object: any, object to check if it changed.
@@ -38,10 +53,10 @@ class DiffChecker:
38
53
  The object is 'None' by default, since there are objects that are needed to be provided in the
39
54
  function input for that object. So, not always you know what your object type during class initialization.
40
55
  :param check_object_display_name: string, name of the object to display in the message.
41
- If not specified, the 'check_object' will be displayed.
42
- :param aggregation: boolean, if True, the object will be aggregated with other objects in the list of objects.
43
- Meaning, that the object will be checked against the existing objects in the list, and if it is not
44
- in the list, it will be added to the list. If it is in the list, it will be ignored.
56
+ If not specified, the provided 'check_object' will be displayed.
57
+ #:param aggregation: boolean, if True, the object will be aggregated with other objects in the list of objects.
58
+ # Meaning, that the object will be checked against the existing objects in the list, and if it is not
59
+ # in the list, it will be added to the list. If it is in the list, it will be ignored.
45
60
  :param input_file_path: string, full file path for storing input file for current state of objects,
46
61
  to check later if this state isn't updated. If this variable is left empty, all the content will be saved
47
62
  in memory and input file will not be used.
@@ -62,27 +77,193 @@ class DiffChecker:
62
77
 
63
78
  True: return updated dictionary on first cycle. This is the default.
64
79
  False: don't return updated dictionary on first cycle.
80
+ :param operation_type: string, type of operation to perform. The type must be one of the following:
81
+ 'new_objects': will only store the new objects in the input file.
82
+ 'hit_statistics': will only store the statistics of the entries in the input file.
83
+ The file will be rotated after the specified time in the 'input_file_rotation_cycle' variable, if
84
+ it is specified.
85
+ 'all_objects': disable the DiffChecker features, meaning any new entries will be emitted as is.
86
+ 'single_object': will store the object as is, without any comparison. Meaning, that the object will be
87
+ compared only to itself, and if it changes, it will be updated.
88
+ None: Nothing will be done, you will get an exception.
89
+ :param hit_statistics_input_file_rotation_cycle_hours:
90
+ float, the amount of hours the input file will be rotated in the 'hit_statistics' operation type.
91
+ str, (only 'midnight' is valid), the input file will be rotated daily at midnight.
92
+ This is valid only for the 'hit_statistics' operation type.
93
+ :param hit_statistics_enable_queue: boolean, if True, the statistics queue will be enabled for the 'hit_statistics'
94
+ operation type. You can use this queue to process the statistics in another thread.
95
+
96
+ Example:
97
+ diff_checker = DiffChecker(
98
+ check_object_display_name='List of Dicts',
99
+ input_file_path='D:\\input\\list_of_dicts.json',
100
+ input_file_write_only=True,
101
+ return_first_cycle=True,
102
+ operation_type='hit_statistics',
103
+ hit_statistics_input_file_rotation_cycle_hours='midnight',
104
+ hit_statistics_enable_queue=True)
105
+
106
+ def process_statistics_queue():
107
+ while True:
108
+ statistics = diff_checker.statistics_queue.get()
109
+ print(statistics)
110
+
111
+ thread = threading.Thread(target=process_statistics_queue)
112
+ thread.daemon = True
113
+ thread.start()
114
+
115
+ <... Your checking operation for the object ...>
116
+ :param new_objects_hours_then_difference: float, This is only for the 'new_objects' operation type.
117
+ If the object is not in the list of objects, it will be added to the list.
118
+ If the object is in the list of objects, it will be ignored.
119
+ After the specified amount of hours, new objects will not be added to the input file list, so each new
120
+ object will be outputted from the function. This is useful for checking new objects that are not
121
+ supposed to be in the list of objects, but you want to know about them.
122
+
123
+ --------------------------------------------------
124
+
125
+ Working example:
126
+ from atomicshop import diff_check
127
+
128
+
129
+ # Example of checking list of dicts.
130
+ check_list_of_dicts = [
131
+ {'name': 'John', 'age': 25},
132
+ {'name': 'Alice', 'age': 30}
133
+ ]
134
+
135
+ diff_checker = diff_check.DiffChecker(
136
+ check_object=check_list_of_dicts,
137
+ check_object_display_name='List of Dicts',
138
+ operation_type='new_objects'
139
+ input_file_path='D:\\input\\list_of_dicts.json',
140
+ input_file_write_only=True,
141
+ return_first_cycle=True
142
+ )
143
+
144
+ result, message = diff_checker.check_list_of_dicts(
145
+ sort_by_keys=['name']
146
+ )
147
+
148
+ # If result is not None, it means that the object was updated.
149
+ if result:
150
+ print(message)
151
+
152
+ --------------------------------------------------
153
+
154
+ Working example when you need to aggregate a list of dicts, meaning only new entries will be added to the list:
155
+ from atomicshop import diff_check
156
+
157
+
158
+ diff_checker = diff_check.DiffChecker(
159
+ check_object_display_name='List of Dicts',
160
+ input_file_path='D:\\input\\list_of_dicts.json',
161
+ input_file_write_only=True,
162
+ return_first_cycle=True,
163
+ operation_type='new_objects'
164
+ )
165
+
166
+ # Example of checking list of dicts.
167
+ check_list_of_dicts = [
168
+ {'name': 'John', 'age': 25},
169
+ {'name': 'Alice', 'age': 30}
170
+ ]
171
+
172
+ diff_checker.check_object = check_list_of_dicts
173
+ result, message = diff_checker.check_list_of_dicts()
174
+
175
+ # If result is not None, it means that the object was updated.
176
+ if result:
177
+ print(message)
178
+
179
+
180
+ check_list_of_dicts = [
181
+ {'name': 'John', 'age': 25},
182
+ {'name': 'Jessie', 'age': 50}
183
+ ]
184
+
185
+ diff_checker.check_object = check_list_of_dicts
186
+ result, message = diff_checker.check_list_of_dicts()
187
+
188
+ if result:
189
+ print(message)
65
190
  """
66
191
 
67
- # 'check_object' can be none, so checking if it not equals empty string.
68
- if check_object == "":
69
- raise ValueError("[check_object] option can't be empty string.")
70
-
71
192
  self.check_object = check_object
72
193
  self.check_object_display_name = check_object_display_name
73
- self.aggregation: bool = aggregation
74
194
  self.input_file_path: str = input_file_path
75
195
  self.input_file_write_only: bool = input_file_write_only
76
196
  self.return_first_cycle: bool = return_first_cycle
77
-
78
- if not self.check_object_display_name:
79
- self.check_object_display_name = self.check_object
197
+ self.operation_type = operation_type
198
+ self.hit_statistics_input_file_rotation_cycle_hours = hit_statistics_input_file_rotation_cycle_hours
199
+ self.hit_statistics_enable_queue = hit_statistics_enable_queue
200
+ self.new_objects_hours_then_difference: float = new_objects_hours_then_difference
80
201
 
81
202
  # Previous content.
82
203
  self.previous_content: Union['list', 'str', None] = None
83
204
  # The format the file will be saved as (not used as extension): txt, json.
84
205
  self.save_as: str = str()
85
206
 
207
+ self.statistics_queue = None
208
+ self.previous_day = None
209
+ self.new_objects_seconds_then_difference: Union[float, None] = None
210
+ self.timer = None
211
+
212
+ def initiate_before_action(self):
213
+ """
214
+ This function will be called before the actual checking of the object.
215
+ If you change any attribute of the class, you will need to call this function again.
216
+ """
217
+ # 'check_object' can be none, so checking if it not equals empty string.
218
+ if self.check_object == "":
219
+ raise ValueError("[check_object] option can't be empty string.")
220
+
221
+ if (self.operation_type and self.operation_type not in
222
+ ['new_objects', 'hit_statistics', 'all_objects', 'single_object']):
223
+ raise ValueError(f"[operation_type] must be one of the following: "
224
+ f"'new_objects', 'hit_statistics', 'all_objects', 'single_object'.")
225
+
226
+ if self.hit_statistics_input_file_rotation_cycle_hours and self.operation_type != 'hit_statistics':
227
+ raise ValueError("[input_file_rotation_cycle] can be specified only for 'hit_statistics' operation type.")
228
+
229
+ if self.hit_statistics_enable_queue and self.operation_type != 'hit_statistics':
230
+ raise ValueError("[hit_statistics_enable_queue] can be specified only for 'hit_statistics' operation type.")
231
+
232
+ if (self.hit_statistics_input_file_rotation_cycle_hours and
233
+ not isinstance(self.hit_statistics_input_file_rotation_cycle_hours, float) and
234
+ not isinstance(self.hit_statistics_input_file_rotation_cycle_hours, int) and
235
+ self.hit_statistics_input_file_rotation_cycle_hours != 'midnight'):
236
+ raise ValueError("[input_file_rotation_cycle] must be float, int or 'midnight' str.")
237
+
238
+ if self.new_objects_hours_then_difference and self.operation_type != 'new_objects':
239
+ raise ValueError(
240
+ "[new_objects_hours_then_difference] can be specified only for 'new_objects' operation type.")
241
+
242
+ if not self.check_object_display_name:
243
+ self.check_object_display_name = self.check_object
244
+
245
+ # If the input file rotation cycle is set and the statistics queue is enabled, we will create the queue.
246
+ if self.hit_statistics_input_file_rotation_cycle_hours and self.hit_statistics_enable_queue:
247
+ # You can use this queue to process the statistics in another thread.
248
+ self.statistics_queue = queue.Queue()
249
+ else:
250
+ self.statistics_queue = None
251
+
252
+ # If the input file rotation cycle is set to midnight, we will store the previous day as today.
253
+ if self.hit_statistics_input_file_rotation_cycle_hours == 'midnight':
254
+ self.previous_day = datetime.datetime.now().strftime('%d')
255
+ else:
256
+ self.previous_day = None
257
+
258
+ if self.new_objects_hours_then_difference and self.operation_type != 'new_objects':
259
+ raise ValueError(
260
+ "The 'new_objects_hours_then_difference' variable must be set for 'new_objects' operation type.")
261
+
262
+ if self.new_objects_hours_then_difference:
263
+ self.new_objects_seconds_then_difference = self.new_objects_hours_then_difference * 60 * 60
264
+ self.timer = timer.Timer()
265
+ self.timer.start()
266
+
86
267
  def check_string(self, print_kwargs: dict = None):
87
268
  """
88
269
  The function will check file content for change by hashing it and comparing the hash.
@@ -119,6 +300,11 @@ class DiffChecker:
119
300
  return self._handle_input_file(sort_by_keys, print_kwargs=print_kwargs)
120
301
 
121
302
  def _handle_input_file(self, sort_by_keys=None, print_kwargs: dict = None):
303
+ # This point is the first one that is shared between the processing functions, so now we can check
304
+ # if the 'operation_type' is set.
305
+ if not self.operation_type:
306
+ raise ValueError("[operation_type] must be specified.")
307
+
122
308
  # If 'input_file_path' was specified, this means that the input file will be created for storing
123
309
  # content of the function to compare.
124
310
  if self.input_file_path:
@@ -130,14 +316,15 @@ class DiffChecker:
130
316
  try:
131
317
  if self.save_as == 'txt':
132
318
  self.previous_content = file_io.read_file(
133
- self.input_file_path, stderr=False, **print_kwargs)
319
+ self.input_file_path, stdout=False, stderr=False, **(print_kwargs or {}))
134
320
  elif self.save_as == 'json':
135
321
  self.previous_content = jsons.read_json_file(
136
- self.input_file_path, stderr=False, **print_kwargs)
322
+ self.input_file_path, stdout=False, stderr=False, **(print_kwargs or {}))
137
323
  except FileNotFoundError as except_object:
138
324
  message = f"Input File [{Path(except_object.filename).name}] doesn't exist - Will create new one."
139
- print_api(message, color='yellow', **print_kwargs)
140
- pass
325
+ print_api(message, color='yellow', **(print_kwargs or {}))
326
+ if not self.input_file_write_only:
327
+ self.previous_content = list()
141
328
 
142
329
  # get the content of current function.
143
330
  if isinstance(self.check_object, list):
@@ -149,10 +336,112 @@ class DiffChecker:
149
336
  result = None
150
337
  message = f'First Cycle on Object: {self.check_object_display_name}'
151
338
 
152
- if self.aggregation:
153
- return self._aggregation_handling(current_content, result, message, sort_by_keys=sort_by_keys, print_kwargs=print_kwargs)
339
+ if self.operation_type == 'all_objects':
340
+ return self._no_diffcheck_handling(
341
+ current_content, result, message, print_kwargs=print_kwargs)
342
+
343
+ if self.operation_type == 'hit_statistics':
344
+ return self._hit_statistics_only_handling(
345
+ current_content, result, message, sort_by_keys, print_kwargs=print_kwargs)
346
+
347
+ if self.operation_type == 'new_objects':
348
+ return self._aggregation_handling(
349
+ current_content, result, message, sort_by_keys=sort_by_keys, print_kwargs=print_kwargs)
350
+
351
+ if self.operation_type == 'single_object':
352
+ return self._singular_object_handling(current_content, result, message, print_kwargs=print_kwargs)
353
+
354
+ def _no_diffcheck_handling(self, current_content, result, message, print_kwargs: dict = None):
355
+ # if not self.previous_content:
356
+ # self.previous_content = []
357
+
358
+ self.previous_content.append(f"{datetime.datetime.now()},{current_content}")
359
+
360
+ result = {
361
+ 'object': self.check_object_display_name,
362
+ 'entry': current_content
363
+ }
364
+
365
+ message = f"Object: {result['object']} | Entry: {result['entry']}"
366
+
367
+ # If 'input_file_path' was specified by the user, it means that we will use the input file to save
368
+ # our known content there for next iterations to compare.
369
+ if self.input_file_path:
370
+ if self.save_as == 'txt':
371
+ # noinspection PyTypeChecker
372
+ file_io.write_file(self.previous_content, self.input_file_path, **(print_kwargs or {}))
373
+ elif self.save_as == 'json':
374
+ jsons.write_json_file(
375
+ self.previous_content, self.input_file_path, use_default_indent=True, **(print_kwargs or {}))
376
+
377
+ return result, message
378
+
379
+ def _hit_statistics_only_handling(self, current_content, result, message, sort_by_keys, print_kwargs: dict = None):
380
+ if self.hit_statistics_input_file_rotation_cycle_hours == 'midnight':
381
+ # If the current time is midnight, we will rotate the file.
382
+ # Get current date.
383
+ current_date = datetime.datetime.now().strftime('%d')
384
+ # If current date is different from previous date it means it is a new day, rotate the file.
385
+ if current_date != self.previous_day:
386
+ input_file_statistics = None
387
+ try:
388
+ # Read the latest statistics from the input file.
389
+ input_file_statistics = jsons.read_json_file(self.input_file_path)
390
+ # Basically, this means that the input statistics file doesn't exist yet, and no events hit
391
+ # yet and new day has come, so it doesn't matter, since there are no statistics to rotate the file.
392
+ except FileNotFoundError:
393
+ pass
394
+
395
+ if input_file_statistics:
396
+ # Rename the file.
397
+ filesystem.backup_file(
398
+ self.input_file_path, str(Path(self.input_file_path).parent), timestamp_as_prefix=False)
399
+ # Update the previous date.
400
+ self.previous_day = current_date
401
+
402
+ previous_day_date_object = (datetime.datetime.now() - datetime.timedelta(days=1)).date()
403
+ # Put the statistics in the queue to be processed.
404
+ if self.statistics_queue:
405
+ self.statistics_queue.put((input_file_statistics, previous_day_date_object))
406
+
407
+ self.previous_content = {}
154
408
  else:
155
- return self._non_aggregation_handling(current_content, result, message, print_kwargs=print_kwargs)
409
+ raise NotImplementedError("This feature is not implemented yet.")
410
+
411
+ # Convert the dictionary entry to string, since we will use it as a key in the dictionary.
412
+ current_entry = json.dumps(current_content[0])
413
+
414
+ if not self.previous_content:
415
+ self.previous_content = {}
416
+
417
+ if not self.previous_content.get(current_entry):
418
+ self.previous_content[current_entry] = 1
419
+ else:
420
+ self.previous_content[current_entry] += 1
421
+
422
+ result = {
423
+ 'object': self.check_object_display_name,
424
+ 'entry': current_entry,
425
+ 'count': self.previous_content[current_entry]
426
+ }
427
+
428
+ message = f"Object: {result['object']} | Entry: {result['entry']} | Count: {result['count']}"
429
+
430
+ # Sort the dictionary by count of entries.
431
+ self.previous_content = dicts.sort_by_values(self.previous_content, reverse=True)
432
+
433
+ # If 'input_file_path' was specified by the user, it means that we will use the input file to save
434
+ # our known content there for next iterations to compare.
435
+ if self.input_file_path:
436
+ if self.save_as == 'txt':
437
+ # noinspection PyTypeChecker
438
+ file_io.write_file(self.previous_content, self.input_file_path, stdout=False, **(print_kwargs or {}))
439
+ elif self.save_as == 'json':
440
+ jsons.write_json_file(
441
+ self.previous_content, self.input_file_path, use_default_indent=True, stdout=False,
442
+ **(print_kwargs or {}))
443
+
444
+ return result, message
156
445
 
157
446
  def _aggregation_handling(self, current_content, result, message, sort_by_keys, print_kwargs: dict = None):
158
447
  if current_content[0] not in self.previous_content:
@@ -163,35 +452,46 @@ class DiffChecker:
163
452
  'object': self.check_object_display_name,
164
453
  'old': list(self.previous_content),
165
454
  'updated': current_content
166
- # 'type': self.object_type
167
455
  }
168
456
 
169
457
  # f"Type: {result['type']} | "
170
458
  message = f"Object: {result['object']} | Old: {result['old']} | Updated: {result['updated']}"
171
459
 
172
- # Make known content the current, since it is updated.
173
- self.previous_content.extend(current_content)
460
+ # If the time has passed, we will stop updating the input file.
461
+ if ((self.new_objects_seconds_then_difference and
462
+ self.new_objects_seconds_then_difference > self.timer.measure())
463
+ or not self.new_objects_hours_then_difference):
174
464
 
175
- # Sort list of dicts by specified list of keys.
176
- if sort_by_keys:
177
- self.previous_content = list_of_dicts.sort_by_keys(
178
- self.previous_content, sort_by_keys, case_insensitive=True)
465
+ result['time_passed'] = False
179
466
 
180
- # If 'input_file_path' was specified by the user, it means that we will use the input file to save
181
- # our known content there for next iterations to compare.
182
- if self.input_file_path:
183
- if self.save_as == 'txt':
184
- # noinspection PyTypeChecker
185
- file_io.write_file(self.previous_content, self.input_file_path, **print_kwargs)
186
- elif self.save_as == 'json':
187
- jsons.write_json_file(
188
- self.previous_content, self.input_file_path, use_default_indent=True, **print_kwargs)
467
+ # Make known content the current, since it is updated.
468
+ self.previous_content.extend(current_content)
469
+
470
+ # Sort list of dicts by specified list of keys.
471
+ if sort_by_keys:
472
+ self.previous_content = list_of_dicts.sort_by_keys(
473
+ self.previous_content, sort_by_keys, case_insensitive=True)
474
+
475
+ # If 'input_file_path' was specified by the user, it means that we will use the input file to save
476
+ # our known content there for next iterations to compare.
477
+ if self.input_file_path:
478
+ if self.save_as == 'txt':
479
+ # noinspection PyTypeChecker
480
+ file_io.write_file(self.previous_content, self.input_file_path, **(print_kwargs or {}))
481
+ elif self.save_as == 'json':
482
+ jsons.write_json_file(
483
+ self.previous_content, self.input_file_path, use_default_indent=True, **(print_kwargs or {}))
484
+ else:
485
+ result['time_passed'] = True
486
+ # We will stop the timer, since the time has passed, the 'measure' method will return the last measure
487
+ # when the timer was running.
488
+ self.timer.stop()
189
489
  else:
190
490
  message = f"Object didn't change: {self.check_object_display_name}"
191
491
 
192
492
  return result, message
193
493
 
194
- def _non_aggregation_handling(self, current_content, result, message, print_kwargs):
494
+ def _singular_object_handling(self, current_content, result, message, print_kwargs):
195
495
  if self.previous_content != current_content:
196
496
  # If known content is not empty (if it is, it means it is the first iteration, and we don't have the input
197
497
  # file, so we don't need to update the 'result', since there is nothing to compare yet).