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.
- atomicshop/__init__.py +1 -1
- atomicshop/{addons/mains → a_mains}/FACT/update_extract.py +3 -2
- atomicshop/a_mains/addons/process_list/compile.cmd +7 -0
- atomicshop/a_mains/addons/process_list/compiled/Win10x64/process_list.dll +0 -0
- atomicshop/a_mains/addons/process_list/compiled/Win10x64/process_list.exp +0 -0
- atomicshop/a_mains/addons/process_list/compiled/Win10x64/process_list.lib +0 -0
- atomicshop/{addons → a_mains/addons}/process_list/process_list.cpp +8 -1
- atomicshop/a_mains/dns_gateway_setting.py +11 -0
- atomicshop/a_mains/get_local_tcp_ports.py +85 -0
- atomicshop/a_mains/github_wrapper.py +11 -0
- atomicshop/a_mains/install_ca_certificate.py +172 -0
- atomicshop/{addons/mains → a_mains}/msi_unpacker.py +3 -1
- atomicshop/a_mains/process_from_port.py +119 -0
- atomicshop/a_mains/set_default_dns_gateway.py +90 -0
- atomicshop/a_mains/update_config_toml.py +38 -0
- atomicshop/appointment_management.py +5 -3
- atomicshop/basics/ansi_escape_codes.py +3 -1
- atomicshop/basics/argparse_template.py +2 -0
- atomicshop/basics/booleans.py +27 -30
- atomicshop/basics/bytes_arrays.py +43 -0
- atomicshop/basics/classes.py +149 -1
- atomicshop/basics/dicts.py +12 -0
- atomicshop/basics/enums.py +2 -2
- atomicshop/basics/exceptions.py +5 -1
- atomicshop/basics/list_of_classes.py +29 -0
- atomicshop/basics/list_of_dicts.py +69 -5
- atomicshop/basics/lists.py +14 -0
- atomicshop/basics/multiprocesses.py +374 -50
- atomicshop/basics/package_module.py +10 -0
- atomicshop/basics/strings.py +160 -7
- atomicshop/basics/threads.py +14 -0
- atomicshop/basics/tracebacks.py +13 -4
- atomicshop/certificates.py +153 -52
- atomicshop/config_init.py +12 -7
- atomicshop/console_user_response.py +7 -14
- atomicshop/consoles.py +9 -0
- atomicshop/datetimes.py +98 -0
- atomicshop/diff_check.py +340 -40
- atomicshop/dns.py +128 -12
- atomicshop/etws/_pywintrace_fix.py +17 -0
- atomicshop/etws/const.py +38 -0
- atomicshop/etws/providers.py +21 -0
- atomicshop/etws/sessions.py +43 -0
- atomicshop/etws/trace.py +168 -0
- atomicshop/etws/traces/trace_dns.py +162 -0
- atomicshop/etws/traces/trace_sysmon_process_creation.py +126 -0
- atomicshop/etws/traces/trace_tcp.py +130 -0
- atomicshop/file_io/csvs.py +222 -24
- atomicshop/file_io/docxs.py +35 -18
- atomicshop/file_io/file_io.py +35 -19
- atomicshop/file_io/jsons.py +49 -0
- atomicshop/file_io/tomls.py +139 -0
- atomicshop/filesystem.py +864 -293
- atomicshop/get_process_list.py +133 -0
- atomicshop/{process_name_cmd.py → get_process_name_cmd_dll.py} +52 -19
- atomicshop/http_parse.py +149 -93
- atomicshop/ip_addresses.py +6 -1
- atomicshop/mitm/centered_settings.py +132 -0
- atomicshop/mitm/config_static.py +207 -0
- atomicshop/mitm/config_toml_editor.py +55 -0
- atomicshop/mitm/connection_thread_worker.py +875 -357
- atomicshop/mitm/engines/__parent/parser___parent.py +4 -17
- atomicshop/mitm/engines/__parent/recorder___parent.py +108 -51
- atomicshop/mitm/engines/__parent/requester___parent.py +116 -0
- atomicshop/mitm/engines/__parent/responder___parent.py +75 -114
- atomicshop/mitm/engines/__reference_general/parser___reference_general.py +10 -7
- atomicshop/mitm/engines/__reference_general/recorder___reference_general.py +5 -5
- atomicshop/mitm/engines/__reference_general/requester___reference_general.py +47 -0
- atomicshop/mitm/engines/__reference_general/responder___reference_general.py +95 -13
- atomicshop/mitm/engines/create_module_template.py +58 -14
- atomicshop/mitm/import_config.py +359 -139
- atomicshop/mitm/initialize_engines.py +160 -74
- atomicshop/mitm/message.py +64 -23
- atomicshop/mitm/mitm_main.py +892 -0
- atomicshop/mitm/recs_files.py +183 -0
- atomicshop/mitm/shared_functions.py +4 -10
- atomicshop/mitm/ssh_tester.py +82 -0
- atomicshop/mitm/statistic_analyzer.py +257 -166
- atomicshop/mitm/statistic_analyzer_helper/analyzer_helper.py +136 -0
- atomicshop/mitm/statistic_analyzer_helper/moving_average_helper.py +525 -0
- atomicshop/monitor/change_monitor.py +96 -120
- atomicshop/monitor/checks/dns.py +139 -70
- atomicshop/monitor/checks/file.py +77 -0
- atomicshop/monitor/checks/network.py +81 -77
- atomicshop/monitor/checks/process_running.py +33 -34
- atomicshop/monitor/checks/url.py +94 -0
- atomicshop/networks.py +671 -0
- atomicshop/on_exit.py +205 -0
- atomicshop/package_mains_processor.py +84 -0
- atomicshop/permissions/permissions.py +22 -0
- atomicshop/permissions/ubuntu_permissions.py +239 -0
- atomicshop/permissions/win_permissions.py +33 -0
- atomicshop/print_api.py +24 -41
- atomicshop/process.py +63 -17
- atomicshop/process_poller/__init__.py +0 -0
- atomicshop/process_poller/pollers/__init__.py +0 -0
- atomicshop/process_poller/pollers/psutil_pywin32wmi_dll.py +95 -0
- atomicshop/process_poller/process_pool.py +207 -0
- atomicshop/process_poller/simple_process_pool.py +311 -0
- atomicshop/process_poller/tracer_base.py +45 -0
- atomicshop/process_poller/tracers/__init__.py +0 -0
- atomicshop/process_poller/tracers/event_log.py +46 -0
- atomicshop/process_poller/tracers/sysmon_etw.py +68 -0
- atomicshop/python_file_patcher.py +1 -1
- atomicshop/python_functions.py +27 -75
- atomicshop/question_answer_engine.py +2 -2
- atomicshop/scheduling.py +24 -5
- atomicshop/sound.py +4 -2
- atomicshop/speech_recognize.py +8 -0
- atomicshop/ssh_remote.py +158 -172
- atomicshop/startup/__init__.py +0 -0
- atomicshop/startup/win/__init__.py +0 -0
- atomicshop/startup/win/startup_folder.py +53 -0
- atomicshop/startup/win/task_scheduler.py +119 -0
- atomicshop/system_resource_monitor.py +61 -46
- atomicshop/system_resources.py +8 -8
- atomicshop/tempfiles.py +1 -2
- atomicshop/timer.py +30 -11
- atomicshop/urls.py +41 -0
- atomicshop/venvs.py +28 -0
- atomicshop/versioning.py +27 -0
- atomicshop/web.py +110 -25
- atomicshop/web_apis/__init__.py +0 -0
- atomicshop/web_apis/google_custom_search.py +44 -0
- atomicshop/web_apis/google_llm.py +188 -0
- atomicshop/websocket_parse.py +450 -0
- atomicshop/wrappers/certauthw/certauth.py +1 -0
- atomicshop/wrappers/cryptographyw.py +29 -8
- atomicshop/wrappers/ctyping/etw_winapi/__init__.py +0 -0
- atomicshop/wrappers/ctyping/etw_winapi/const.py +335 -0
- atomicshop/wrappers/ctyping/etw_winapi/etw_functions.py +393 -0
- atomicshop/wrappers/ctyping/file_details_winapi.py +67 -0
- atomicshop/wrappers/ctyping/msi_windows_installer/cabs.py +2 -1
- atomicshop/wrappers/ctyping/msi_windows_installer/extract_msi_main.py +13 -9
- atomicshop/wrappers/ctyping/msi_windows_installer/tables.py +35 -0
- atomicshop/wrappers/ctyping/setup_device.py +466 -0
- atomicshop/wrappers/ctyping/win_console.py +39 -0
- atomicshop/wrappers/dockerw/dockerw.py +113 -2
- atomicshop/wrappers/elasticsearchw/config_basic.py +0 -12
- atomicshop/wrappers/elasticsearchw/elastic_infra.py +75 -0
- atomicshop/wrappers/elasticsearchw/elasticsearchw.py +2 -20
- atomicshop/wrappers/factw/get_file_data.py +12 -5
- atomicshop/wrappers/factw/install/install_after_restart.py +89 -5
- atomicshop/wrappers/factw/install/pre_install_and_install_before_restart.py +20 -14
- atomicshop/wrappers/factw/postgresql/firmware.py +4 -6
- atomicshop/wrappers/githubw.py +583 -51
- atomicshop/wrappers/loggingw/consts.py +49 -0
- atomicshop/wrappers/loggingw/filters.py +102 -0
- atomicshop/wrappers/loggingw/formatters.py +58 -71
- atomicshop/wrappers/loggingw/handlers.py +459 -40
- atomicshop/wrappers/loggingw/loggers.py +19 -0
- atomicshop/wrappers/loggingw/loggingw.py +1010 -178
- atomicshop/wrappers/loggingw/reading.py +344 -19
- atomicshop/wrappers/mongodbw/__init__.py +0 -0
- atomicshop/wrappers/mongodbw/mongo_infra.py +31 -0
- atomicshop/wrappers/mongodbw/mongodbw.py +1432 -0
- atomicshop/wrappers/netshw.py +271 -0
- atomicshop/wrappers/playwrightw/engine.py +34 -19
- atomicshop/wrappers/playwrightw/infra.py +5 -0
- atomicshop/wrappers/playwrightw/javascript.py +7 -3
- atomicshop/wrappers/playwrightw/keyboard.py +14 -0
- atomicshop/wrappers/playwrightw/scenarios.py +172 -5
- atomicshop/wrappers/playwrightw/waits.py +9 -7
- atomicshop/wrappers/powershell_networking.py +80 -0
- atomicshop/wrappers/psutilw/processes.py +81 -0
- atomicshop/wrappers/psutilw/psutil_networks.py +85 -0
- atomicshop/wrappers/psutilw/psutilw.py +9 -0
- atomicshop/wrappers/pyopensslw.py +9 -2
- atomicshop/wrappers/pywin32w/__init__.py +0 -0
- atomicshop/wrappers/pywin32w/cert_store.py +116 -0
- atomicshop/wrappers/pywin32w/console.py +34 -0
- atomicshop/wrappers/pywin32w/win_event_log/__init__.py +0 -0
- atomicshop/wrappers/pywin32w/win_event_log/fetch.py +174 -0
- atomicshop/wrappers/pywin32w/win_event_log/subscribe.py +212 -0
- atomicshop/wrappers/pywin32w/win_event_log/subscribes/__init__.py +0 -0
- atomicshop/wrappers/pywin32w/win_event_log/subscribes/process_create.py +57 -0
- atomicshop/wrappers/pywin32w/win_event_log/subscribes/process_terminate.py +49 -0
- atomicshop/wrappers/pywin32w/win_event_log/subscribes/schannel_logging.py +97 -0
- atomicshop/wrappers/pywin32w/winshell.py +19 -0
- atomicshop/wrappers/pywin32w/wmis/__init__.py +0 -0
- atomicshop/wrappers/pywin32w/wmis/msft_netipaddress.py +113 -0
- atomicshop/wrappers/pywin32w/wmis/win32_networkadapterconfiguration.py +259 -0
- atomicshop/wrappers/pywin32w/wmis/win32networkadapter.py +112 -0
- atomicshop/wrappers/pywin32w/wmis/wmi_helpers.py +236 -0
- atomicshop/wrappers/socketw/accepter.py +21 -7
- atomicshop/wrappers/socketw/certificator.py +216 -150
- atomicshop/wrappers/socketw/creator.py +190 -50
- atomicshop/wrappers/socketw/dns_server.py +500 -173
- atomicshop/wrappers/socketw/exception_wrapper.py +45 -52
- atomicshop/wrappers/socketw/process_getter.py +86 -0
- atomicshop/wrappers/socketw/receiver.py +144 -102
- atomicshop/wrappers/socketw/sender.py +65 -35
- atomicshop/wrappers/socketw/sni.py +334 -165
- atomicshop/wrappers/socketw/socket_base.py +134 -0
- atomicshop/wrappers/socketw/socket_client.py +137 -95
- atomicshop/wrappers/socketw/socket_server_tester.py +14 -9
- atomicshop/wrappers/socketw/socket_wrapper.py +717 -116
- atomicshop/wrappers/socketw/ssl_base.py +15 -14
- atomicshop/wrappers/socketw/statistics_csv.py +148 -17
- atomicshop/wrappers/sysmonw.py +157 -0
- atomicshop/wrappers/ubuntu_terminal.py +65 -26
- atomicshop/wrappers/win_auditw.py +189 -0
- atomicshop/wrappers/winregw/__init__.py +0 -0
- atomicshop/wrappers/winregw/winreg_installed_software.py +58 -0
- atomicshop/wrappers/winregw/winreg_network.py +232 -0
- {atomicshop-2.11.47.dist-info → atomicshop-3.10.5.dist-info}/METADATA +31 -49
- atomicshop-3.10.5.dist-info/RECORD +306 -0
- {atomicshop-2.11.47.dist-info → atomicshop-3.10.5.dist-info}/WHEEL +1 -1
- atomicshop/_basics_temp.py +0 -101
- atomicshop/addons/a_setup_scripts/install_psycopg2_ubuntu.sh +0 -3
- atomicshop/addons/a_setup_scripts/install_pywintrace_0.3.cmd +0 -2
- atomicshop/addons/mains/install_docker_rootless_ubuntu.py +0 -11
- atomicshop/addons/mains/install_docker_ubuntu_main_sudo.py +0 -11
- atomicshop/addons/mains/install_elastic_search_and_kibana_ubuntu.py +0 -10
- atomicshop/addons/mains/install_wsl_ubuntu_lts_admin.py +0 -9
- atomicshop/addons/package_setup/CreateWheel.cmd +0 -7
- atomicshop/addons/package_setup/Setup in Edit mode.cmd +0 -6
- atomicshop/addons/package_setup/Setup.cmd +0 -7
- atomicshop/addons/process_list/compile.cmd +0 -2
- atomicshop/addons/process_list/compiled/Win10x64/process_list.dll +0 -0
- atomicshop/addons/process_list/compiled/Win10x64/process_list.exp +0 -0
- atomicshop/addons/process_list/compiled/Win10x64/process_list.lib +0 -0
- atomicshop/archiver/_search_in_zip.py +0 -189
- atomicshop/archiver/archiver.py +0 -34
- atomicshop/archiver/search_in_archive.py +0 -250
- atomicshop/archiver/sevenz_app_w.py +0 -86
- atomicshop/archiver/sevenzs.py +0 -44
- atomicshop/archiver/zips.py +0 -293
- atomicshop/etw/dns_trace.py +0 -118
- atomicshop/etw/etw.py +0 -61
- atomicshop/file_types.py +0 -24
- atomicshop/mitm/engines/create_module_template_example.py +0 -13
- atomicshop/mitm/initialize_mitm_server.py +0 -240
- atomicshop/monitor/checks/hash.py +0 -44
- atomicshop/monitor/checks/hash_checks/file.py +0 -55
- atomicshop/monitor/checks/hash_checks/url.py +0 -62
- atomicshop/pbtkmultifile_argparse.py +0 -88
- atomicshop/permissions.py +0 -110
- atomicshop/process_poller.py +0 -237
- atomicshop/script_as_string_processor.py +0 -38
- atomicshop/ssh_scripts/process_from_ipv4.py +0 -37
- atomicshop/ssh_scripts/process_from_port.py +0 -27
- atomicshop/wrappers/_process_wrapper_curl.py +0 -27
- atomicshop/wrappers/_process_wrapper_tar.py +0 -21
- atomicshop/wrappers/dockerw/install_docker.py +0 -209
- atomicshop/wrappers/elasticsearchw/infrastructure.py +0 -265
- atomicshop/wrappers/elasticsearchw/install_elastic.py +0 -232
- atomicshop/wrappers/ffmpegw.py +0 -125
- atomicshop/wrappers/loggingw/checks.py +0 -20
- atomicshop/wrappers/nodejsw/install_nodejs.py +0 -139
- atomicshop/wrappers/process_wrapper_pbtk.py +0 -16
- atomicshop/wrappers/socketw/base.py +0 -59
- atomicshop/wrappers/socketw/get_process.py +0 -107
- atomicshop/wrappers/wslw.py +0 -191
- atomicshop-2.11.47.dist-info/RECORD +0 -251
- /atomicshop/{addons/mains → a_mains}/FACT/factw_fact_extractor_docker_image_main_sudo.py +0 -0
- /atomicshop/{addons → a_mains/addons}/PlayWrightCodegen.cmd +0 -0
- /atomicshop/{addons → a_mains/addons}/ScriptExecution.cmd +0 -0
- /atomicshop/{addons/mains → a_mains/addons}/inits/init_to_import_all_modules.py +0 -0
- /atomicshop/{addons → a_mains/addons}/process_list/ReadMe.txt +0 -0
- /atomicshop/{addons/mains → a_mains}/search_for_hyperlinks_in_docx.py +0 -0
- /atomicshop/{archiver → etws}/__init__.py +0 -0
- /atomicshop/{etw → etws/traces}/__init__.py +0 -0
- /atomicshop/{monitor/checks/hash_checks → mitm/statistic_analyzer_helper}/__init__.py +0 -0
- /atomicshop/{wrappers/nodejsw → permissions}/__init__.py +0 -0
- /atomicshop/wrappers/pywin32w/{wmi_win32process.py → wmis/win32process.py} +0 -0
- {atomicshop-2.11.47.dist-info → atomicshop-3.10.5.dist-info/licenses}/LICENSE.txt +0 -0
- {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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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.
|
|
153
|
-
return self.
|
|
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
|
-
|
|
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
|
-
#
|
|
173
|
-
self.
|
|
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
|
-
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
|
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).
|