From 63a8802f32e08259b4e56055fc02548bfb6dc099 Mon Sep 17 00:00:00 2001 From: dgtlmoon Date: Fri, 21 Mar 2025 11:00:35 +0100 Subject: [PATCH] Tidy up model def and clean up API endpoint --- changedetectionio/api/api_v1.py | 36 ++-- changedetectionio/model/__init__.py | 246 ++++++++++++++-------------- changedetectionio/store.py | 2 +- changedetectionio/tests/test_api.py | 5 +- 4 files changed, 146 insertions(+), 143 deletions(-) diff --git a/changedetectionio/api/api_v1.py b/changedetectionio/api/api_v1.py index fc09bb1b..9e03f2e2 100644 --- a/changedetectionio/api/api_v1.py +++ b/changedetectionio/api/api_v1.py @@ -12,11 +12,10 @@ import copy # See docs/README.md for rebuilding the docs/apidoc information from . import api_schema -from ..model import watch_base +from ..model import schema as watch_schema # Build a JSON Schema atleast partially based on our Watch model -watch_base_config = watch_base() -schema = api_schema.build_watch_json_schema(watch_base_config) +schema = api_schema.build_watch_json_schema(watch_schema) schema_create_watch = copy.deepcopy(schema) schema_create_watch['required'] = ['url'] @@ -53,9 +52,9 @@ class Watch(Resource): @apiSuccess (200) {JSON} WatchJSON JSON Full JSON object of the watch """ from copy import deepcopy - watch = deepcopy(self.datastore.data['watching'].get(uuid)) + watch = self.datastore.data['watching'].get(uuid) if not watch: - abort(404, message='No watch exists with the UUID of {}'.format(uuid)) + abort(404, message=f'No watch exists with the UUID of {uuid}') if request.args.get('recheck'): self.update_q.put(queuedWatchMetaData.PrioritizedItem(priority=1, item={'uuid': uuid})) @@ -73,13 +72,16 @@ class Watch(Resource): self.datastore.data['watching'].get(uuid).unmute() return "OK", 200 - # Return without history, get that via another API call - # Properties are not returned as a JSON, so add the required props manually - watch['history_n'] = watch.history_n - # attr .last_changed will check for the last written text snapshot on change - watch['last_changed'] = watch.last_changed - watch['viewed'] = watch.viewed - return watch + + response = dict(watch.get_data()) + + # Add properties that aren't included in the standard dictionary items (they are properties/attr) + response['history_n'] = watch.history_n + response['last_changed'] = watch.last_changed + response['viewed'] = watch.viewed + response['title'] = watch.get('title') + + return response @auth.check_token def delete(self, uuid): @@ -114,16 +116,16 @@ class Watch(Resource): @apiSuccess (200) {String} OK Was updated @apiSuccess (500) {String} ERR Some other error """ - watch = self.datastore.data['watching'].get(uuid) - if not watch: - abort(404, message='No watch exists with the UUID of {}'.format(uuid)) + + if not self.datastore.data['watching'].get(uuid): + abort(404, message=f'No watch exists with the UUID of {uuid}') if request.json.get('proxy'): plist = self.datastore.proxy_list if not request.json.get('proxy') in plist: - return "Invalid proxy choice, currently supported proxies are '{}'".format(', '.join(plist)), 400 + return f"Invalid proxy choice, currently supported proxies are '{', '.join(plist)}'", 400 - watch.update(request.json) + self.datastore.data['watching'][uuid].update(request.json) return "OK", 200 diff --git a/changedetectionio/model/__init__.py b/changedetectionio/model/__init__.py index ad926c13..fc7c5795 100644 --- a/changedetectionio/model/__init__.py +++ b/changedetectionio/model/__init__.py @@ -1,134 +1,138 @@ import os import uuid +from copy import deepcopy from changedetectionio import strtobool from changedetectionio.notification import default_notification_format_for_watch +schema = { + # Custom notification content + # Re #110, so then if this is set to None, we know to use the default value instead + # Requires setting to None on submit if it's the same as the default + # Should be all None by default, so we use the system default in this case. + 'body': None, + 'browser_steps': [], + 'browser_steps_last_error_step': None, + 'check_count': 0, + 'check_unique_lines': False, # On change-detected, compare against all history if its something new + 'consecutive_filter_failures': 0, # Every time the CSS/xPath filter cannot be located, reset when all is fine. + 'content-type': None, + 'date_created': None, + 'extract_text': [], # Extract text by regex after filters + 'extract_title_as_title': False, + 'fetch_backend': 'system', # plaintext, playwright etc + 'fetch_time': 0.0, + 'filter_failure_notification_send': strtobool(os.getenv('FILTER_FAILURE_NOTIFICATION_SEND_DEFAULT', 'True')), + 'filter_text_added': True, + 'filter_text_removed': True, + 'filter_text_replaced': True, + 'follow_price_changes': True, + 'has_ldjson_price_data': None, + 'headers': {}, # Extra headers to send + 'ignore_text': [], # List of text to ignore when calculating the comparison checksum + 'in_stock_only': True, # Only trigger change on going to instock from out-of-stock + 'include_filters': [], + 'last_checked': 0, + 'last_error': False, + 'last_viewed': 0, # history key value of the last viewed via the [diff] link + 'method': 'GET', + 'notification_alert_count': 0, + 'notification_body': None, + 'notification_format': default_notification_format_for_watch, + 'notification_muted': False, + 'notification_screenshot': False, # Include the latest screenshot if available and supported by the apprise URL + 'notification_title': None, + 'notification_urls': [], # List of URLs to add to the notification Queue (Usually AppRise) + 'paused': False, + 'previous_md5': False, + 'previous_md5_before_filters': False, # Used for skipping changedetection entirely + 'processor': 'text_json_diff', # could be restock_diff or others from .processors + 'price_change_threshold_percent': None, + 'proxy': None, # Preferred proxy connection + 'remote_server_reply': None, # From 'server' reply header + 'sort_text_alphabetically': False, + 'subtractive_selectors': [], + 'tag': '', # Old system of text name for a tag, to be removed + 'tags': [], # list of UUIDs to App.Tags + 'text_should_not_be_present': [], # Text that should not present + 'time_between_check': {'weeks': None, 'days': None, 'hours': None, 'minutes': None, 'seconds': None}, + 'time_between_check_use_default': True, + "time_schedule_limit": { + "enabled": False, + "monday": { + "enabled": True, + "start_time": "00:00", + "duration": { + "hours": "24", + "minutes": "00" + } + }, + "tuesday": { + "enabled": True, + "start_time": "00:00", + "duration": { + "hours": "24", + "minutes": "00" + } + }, + "wednesday": { + "enabled": True, + "start_time": "00:00", + "duration": { + "hours": "24", + "minutes": "00" + } + }, + "thursday": { + "enabled": True, + "start_time": "00:00", + "duration": { + "hours": "24", + "minutes": "00" + } + }, + "friday": { + "enabled": True, + "start_time": "00:00", + "duration": { + "hours": "24", + "minutes": "00" + } + }, + "saturday": { + "enabled": True, + "start_time": "00:00", + "duration": { + "hours": "24", + "minutes": "00" + } + }, + "sunday": { + "enabled": True, + "start_time": "00:00", + "duration": { + "hours": "24", + "minutes": "00" + } + }, + }, + 'title': None, + 'track_ldjson_price_data': None, + 'trim_text_whitespace': False, + 'remove_duplicate_lines': False, + 'trigger_text': [], # List of text or regex to wait for until a change is detected + 'url': '', + 'uuid': None, + 'webdriver_delay': None, + 'webdriver_js_execute_code': None, # Run before change-detection +} + class watch_base(dict): def __init__(self, *arg, **kw): # Initialize internal data storage - self.__data = { - # Custom notification content - # Re #110, so then if this is set to None, we know to use the default value instead - # Requires setting to None on submit if it's the same as the default - # Should be all None by default, so we use the system default in this case. - 'body': None, - 'browser_steps': [], - 'browser_steps_last_error_step': None, - 'check_count': 0, - 'check_unique_lines': False, # On change-detected, compare against all history if its something new - 'consecutive_filter_failures': 0, # Every time the CSS/xPath filter cannot be located, reset when all is fine. - 'content-type': None, - 'date_created': None, - 'extract_text': [], # Extract text by regex after filters - 'extract_title_as_title': False, - 'fetch_backend': 'system', # plaintext, playwright etc - 'fetch_time': 0.0, - 'filter_failure_notification_send': strtobool(os.getenv('FILTER_FAILURE_NOTIFICATION_SEND_DEFAULT', 'True')), - 'filter_text_added': True, - 'filter_text_removed': True, - 'filter_text_replaced': True, - 'follow_price_changes': True, - 'has_ldjson_price_data': None, - 'headers': {}, # Extra headers to send - 'ignore_text': [], # List of text to ignore when calculating the comparison checksum - 'in_stock_only': True, # Only trigger change on going to instock from out-of-stock - 'include_filters': [], - 'last_checked': 0, - 'last_error': False, - 'last_viewed': 0, # history key value of the last viewed via the [diff] link - 'method': 'GET', - 'notification_alert_count': 0, - 'notification_body': None, - 'notification_format': default_notification_format_for_watch, - 'notification_muted': False, - 'notification_screenshot': False, # Include the latest screenshot if available and supported by the apprise URL - 'notification_title': None, - 'notification_urls': [], # List of URLs to add to the notification Queue (Usually AppRise) - 'paused': False, - 'previous_md5': False, - 'previous_md5_before_filters': False, # Used for skipping changedetection entirely - 'processor': 'text_json_diff', # could be restock_diff or others from .processors - 'price_change_threshold_percent': None, - 'proxy': None, # Preferred proxy connection - 'remote_server_reply': None, # From 'server' reply header - 'sort_text_alphabetically': False, - 'subtractive_selectors': [], - 'tag': '', # Old system of text name for a tag, to be removed - 'tags': [], # list of UUIDs to App.Tags - 'text_should_not_be_present': [], # Text that should not present - 'time_between_check': {'weeks': None, 'days': None, 'hours': None, 'minutes': None, 'seconds': None}, - 'time_between_check_use_default': True, - "time_schedule_limit": { - "enabled": False, - "monday": { - "enabled": True, - "start_time": "00:00", - "duration": { - "hours": "24", - "minutes": "00" - } - }, - "tuesday": { - "enabled": True, - "start_time": "00:00", - "duration": { - "hours": "24", - "minutes": "00" - } - }, - "wednesday": { - "enabled": True, - "start_time": "00:00", - "duration": { - "hours": "24", - "minutes": "00" - } - }, - "thursday": { - "enabled": True, - "start_time": "00:00", - "duration": { - "hours": "24", - "minutes": "00" - } - }, - "friday": { - "enabled": True, - "start_time": "00:00", - "duration": { - "hours": "24", - "minutes": "00" - } - }, - "saturday": { - "enabled": True, - "start_time": "00:00", - "duration": { - "hours": "24", - "minutes": "00" - } - }, - "sunday": { - "enabled": True, - "start_time": "00:00", - "duration": { - "hours": "24", - "minutes": "00" - } - }, - }, - 'title': None, - 'track_ldjson_price_data': None, - 'trim_text_whitespace': False, - 'remove_duplicate_lines': False, - 'trigger_text': [], # List of text or regex to wait for until a change is detected - 'url': '', - 'uuid': None, - 'webdriver_delay': None, - 'webdriver_js_execute_code': None, # Run before change-detection - } + + self.__data = deepcopy(schema) # Initialize as empty dict but maintain dict interface super(watch_base, self).__init__() diff --git a/changedetectionio/store.py b/changedetectionio/store.py index f0fb76ed..86fb31bd 100644 --- a/changedetectionio/store.py +++ b/changedetectionio/store.py @@ -410,7 +410,7 @@ class ChangeDetectionStore: logger.remove() logger.add(sys.stderr) - logger.critical("Shutting down datastore thread") + logger.info("Shutting down datastore thread") return if self.needs_write or self.needs_write_urgent: diff --git a/changedetectionio/tests/test_api.py b/changedetectionio/tests/test_api.py index 097133fe..584ca7e7 100644 --- a/changedetectionio/tests/test_api.py +++ b/changedetectionio/tests/test_api.py @@ -57,8 +57,7 @@ def test_setup(client, live_server, measure_memory_usage): def test_api_simple(client, live_server, measure_memory_usage): -# live_server_setup(live_server) - + #live_server_setup(live_server) api_key = extract_api_key_from_UI(client) # Create a watch @@ -291,7 +290,6 @@ def test_access_denied(client, live_server, measure_memory_usage): assert b"Settings updated." in res.data def test_api_watch_PUT_update(client, live_server, measure_memory_usage): - #live_server_setup(live_server) api_key = extract_api_key_from_UI(client) @@ -373,7 +371,6 @@ def test_api_watch_PUT_update(client, live_server, measure_memory_usage): def test_api_import(client, live_server, measure_memory_usage): - #live_server_setup(live_server) api_key = extract_api_key_from_UI(client) res = client.post(