mirror of
https://github.com/dgtlmoon/changedetection.io.git
synced 2026-05-08 02:30:37 +00:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 362cc40e90 | |||
| e25387f588 | |||
| e4bc048280 | |||
| 2839a4276e | |||
| 5759a28d89 |
@@ -11,8 +11,8 @@ jobs:
|
||||
- name: Lint with Ruff
|
||||
run: |
|
||||
pip install ruff
|
||||
# Check for syntax errors and undefined names
|
||||
ruff check . --select E9,F63,F7,F82
|
||||
# Check for syntax errors and undefined names, and gettext misuse
|
||||
ruff check . --select E9,F63,F7,F82,INT
|
||||
# Complete check with errors treated as warnings
|
||||
ruff check . --exit-zero
|
||||
- name: Validate OpenAPI spec
|
||||
|
||||
+5
-1
@@ -20,10 +20,11 @@ exclude = [
|
||||
select = [
|
||||
"B", # flake8-bugbear
|
||||
"B9",
|
||||
"C",
|
||||
"C",
|
||||
"E", # pycodestyle
|
||||
"F", # Pyflakes
|
||||
"I", # isort
|
||||
"INT", # flake8-gettext
|
||||
"N", # pep8-naming
|
||||
"UP", # pyupgrade
|
||||
"W", # pycodestyle
|
||||
@@ -43,6 +44,9 @@ ignore = [
|
||||
[lint.mccabe]
|
||||
max-complexity = 12
|
||||
|
||||
[lint.flake8-gettext]
|
||||
extend-function-names = ["_l", "lazy_gettext", "pgettext", "npgettext"]
|
||||
|
||||
[format]
|
||||
indent-style = "space"
|
||||
quote-style = "preserve"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
# Read more https://github.com/dgtlmoon/changedetection.io/wiki
|
||||
# Semver means never use .01, or 00. Should be .1.
|
||||
__version__ = '0.55.2'
|
||||
__version__ = '0.55.3'
|
||||
|
||||
from changedetectionio.strtobool import strtobool
|
||||
from json.decoder import JSONDecodeError
|
||||
|
||||
@@ -283,6 +283,8 @@ def construct_blueprint(datastore: ChangeDetectionStore):
|
||||
# Check cache — keyed by version pair + prompt hash (invalidates if prompt changes)
|
||||
cached = watch.get_llm_diff_summary(from_version, to_version, prompt=cache_prompt)
|
||||
if cached:
|
||||
import time
|
||||
datastore.set_last_viewed(uuid, int(time.time()))
|
||||
return jsonify({'summary': cached, 'error': None, 'cached': True})
|
||||
|
||||
# Check global monthly token budget before making an LLM call
|
||||
@@ -316,6 +318,8 @@ def construct_blueprint(datastore: ChangeDetectionStore):
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not cache llm summary for {uuid}: {e}")
|
||||
|
||||
import time
|
||||
datastore.set_last_viewed(uuid, int(time.time()))
|
||||
return jsonify({'summary': summary, 'error': None, 'cached': False})
|
||||
|
||||
@diff_blueprint.route("/diff/<uuid_str:uuid>/extract", methods=['GET'])
|
||||
|
||||
@@ -65,6 +65,9 @@ def notification_format_align_with_apprise(n_format : str):
|
||||
:return:
|
||||
"""
|
||||
|
||||
if not n_format:
|
||||
return NotifyFormat.TEXT.value
|
||||
|
||||
if n_format.startswith('html'):
|
||||
# Apprise only knows 'html' not 'htmlcolor' etc, which shouldnt matter here
|
||||
n_format = NotifyFormat.HTML.value
|
||||
|
||||
@@ -29,7 +29,7 @@ def _check_cascading_vars(datastore, var_name, watch):
|
||||
v = watch.get(var_name)
|
||||
if v and not watch.get('notification_muted'):
|
||||
if var_name == 'notification_format' and v == USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH:
|
||||
return datastore.data['settings']['application'].get('notification_format')
|
||||
return datastore.data['settings']['application'].get('notification_format') or default_notification_format
|
||||
|
||||
return v
|
||||
|
||||
@@ -457,7 +457,7 @@ Thanks - Your omniscient changedetection.io installation.
|
||||
'notification_body': body,
|
||||
'notification_format': _check_cascading_vars(self.datastore, 'notification_format', watch),
|
||||
})
|
||||
n_object['markup_text_links_to_html_links'] = n_object.get('notification_format').startswith('html')
|
||||
n_object['markup_text_links_to_html_links'] = (n_object.get('notification_format') or '').startswith('html')
|
||||
|
||||
if len(watch['notification_urls']):
|
||||
n_object['notification_urls'] = watch['notification_urls']
|
||||
@@ -506,7 +506,7 @@ Thanks - Your omniscient changedetection.io installation.
|
||||
'notification_body': body,
|
||||
'notification_format': _check_cascading_vars(self.datastore, 'notification_format', watch),
|
||||
})
|
||||
n_object['markup_text_links_to_html_links'] = n_object.get('notification_format').startswith('html')
|
||||
n_object['markup_text_links_to_html_links'] = (n_object.get('notification_format') or '').startswith('html')
|
||||
|
||||
if len(watch['notification_urls']):
|
||||
n_object['notification_urls'] = watch['notification_urls']
|
||||
|
||||
@@ -336,6 +336,58 @@ def test_hardcoded_fallback_when_nothing_set(
|
||||
delete_all_watches(client)
|
||||
|
||||
|
||||
def test_llm_summary_ajax_sets_last_viewed(
|
||||
client, live_server, measure_memory_usage, datastore_path):
|
||||
"""
|
||||
Calling /diff/<uuid>/llm-summary via AJAX should mark the watch as viewed
|
||||
(set last_viewed) for both fresh and cached responses.
|
||||
"""
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
_configure_llm(client)
|
||||
ds = client.application.config.get('DATASTORE')
|
||||
|
||||
test_url = url_for('test_endpoint', content_type='text/html', content='v1', _external=True)
|
||||
uuid = ds.add_watch(url=test_url)
|
||||
watch = ds.data['watching'][uuid]
|
||||
|
||||
watch.save_history_blob('old content\n', '4000000000', 'snap-old')
|
||||
watch.save_history_blob('new content\n', '4000000001', 'snap-new')
|
||||
|
||||
assert watch['last_viewed'] == 0, "last_viewed should start at 0"
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.choices = [MagicMock()]
|
||||
mock_response.choices[0].message.content = 'Content changed from old to new.'
|
||||
mock_response.usage = MagicMock(total_tokens=50, prompt_tokens=40, completion_tokens=10)
|
||||
|
||||
with patch('litellm.completion', return_value=mock_response):
|
||||
res = client.get(
|
||||
url_for('ui.ui_diff.diff_llm_summary', uuid=uuid,
|
||||
from_version='4000000000', to_version='4000000001'),
|
||||
)
|
||||
|
||||
assert res.status_code == 200
|
||||
data = res.get_json()
|
||||
assert data['summary'] == 'Content changed from old to new.'
|
||||
assert watch['last_viewed'] > 0, "last_viewed should be set after fresh LLM summary"
|
||||
|
||||
# Reset and verify the cached path also sets last_viewed
|
||||
watch['last_viewed'] = 0
|
||||
with patch('litellm.completion', return_value=mock_response):
|
||||
res2 = client.get(
|
||||
url_for('ui.ui_diff.diff_llm_summary', uuid=uuid,
|
||||
from_version='4000000000', to_version='4000000001'),
|
||||
)
|
||||
|
||||
assert res2.status_code == 200
|
||||
data2 = res2.get_json()
|
||||
assert data2.get('cached') is True
|
||||
assert watch['last_viewed'] > 0, "last_viewed should be set even when returning cached summary"
|
||||
|
||||
delete_all_watches(client)
|
||||
|
||||
|
||||
def test_global_default_saved_and_loaded_via_settings_form(
|
||||
client, live_server, measure_memory_usage, datastore_path):
|
||||
"""
|
||||
|
||||
@@ -541,6 +541,39 @@ def test_single_send_test_notification_on_watch(client, live_server, measure_mem
|
||||
assert 'Current snapshot: Example text: example test' in x
|
||||
os.unlink(os.path.join(datastore_path, "notification.txt"))
|
||||
|
||||
# Regression test for #4119 - sending a test notification with 'System default' format caused a crash
|
||||
def test_send_test_notification_with_system_default_format(client, live_server, measure_memory_usage, datastore_path):
|
||||
|
||||
set_original_response(datastore_path=datastore_path)
|
||||
if os.path.isfile(os.path.join(datastore_path, "notification.txt")):
|
||||
os.unlink(os.path.join(datastore_path, "notification.txt"))
|
||||
|
||||
test_notification_url = url_for('test_notification_endpoint', _external=True).replace('http://', 'post://') + "?status_code=204"
|
||||
|
||||
test_url = url_for('test_endpoint', _external=True)
|
||||
uuid = client.application.config.get('DATASTORE').add_watch(url=test_url)
|
||||
client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
|
||||
wait_for_all_checks(client)
|
||||
|
||||
# New watches default to USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH.
|
||||
# The JS sends this value verbatim from the select; it must not crash.
|
||||
res = client.post(
|
||||
url_for("ui.ui_notification.ajax_callback_send_notification_test") + f"/{uuid}",
|
||||
data={
|
||||
"notification_urls": test_notification_url,
|
||||
"notification_body": default_notification_body,
|
||||
"notification_title": default_notification_title,
|
||||
"notification_format": USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH,
|
||||
},
|
||||
follow_redirects=True
|
||||
)
|
||||
|
||||
assert res.status_code != 400
|
||||
assert res.status_code != 500
|
||||
|
||||
client.get(url_for("ui.form_delete", uuid="all"), follow_redirects=True)
|
||||
|
||||
|
||||
def _test_color_notifications(client, notification_body_token, datastore_path):
|
||||
|
||||
set_original_response(datastore_path=datastore_path)
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: changedetection.io 0.55.2\n"
|
||||
"Project-Id-Version: changedetection.io 0.55.3\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2026-04-28 15:22+1000\n"
|
||||
"POT-Creation-Date: 2026-04-28 15:26+1000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
||||
+9
-3
@@ -99,6 +99,12 @@ pytest-mock ~=3.15
|
||||
|
||||
# OpenAPI validation support
|
||||
openapi-core[flask] ~= 0.23
|
||||
# openapi-spec-validator (pulled in by openapi-core) requires jsonschema>=4.24.0.
|
||||
# litellm 1.83.1–1.83.14 exact-pin jsonschema==4.23.0, which is below that floor —
|
||||
# the two can never coexist. Without this pin, pip walks back through ~14 litellm
|
||||
# patch releases before finding 1.83.0 (jsonschema>=4.23.0,<5.0.0, accepts 4.24.x).
|
||||
# Pinning >=4.24.0 here lets the resolver reject incompatible litellm versions immediately.
|
||||
jsonschema>=4.24.0,<5.0.0
|
||||
|
||||
loguru
|
||||
|
||||
@@ -120,7 +126,7 @@ greenlet >= 3.0.3
|
||||
# Default SOCKETIO_MODE=threading is recommended for better compatibility
|
||||
gevent
|
||||
|
||||
referencing # Don't pin — jsonschema-path (required by openapi-core>=0.18) caps referencing<0.37.0, so pinning 0.37.0 forces openapi-core back to 0.17.2. Revisit once jsonschema-path>=0.3.5 relaxes the cap.
|
||||
referencing==0.37.0 # jsonschema-path>=0.4.x allows <0.38.0; 0.37.0 is current latest
|
||||
|
||||
# For conditions
|
||||
panzi-json-logic
|
||||
@@ -131,7 +137,7 @@ price-parser
|
||||
|
||||
# Lightweight MIME type detection (saves ~14MB memory vs python-magic/libmagic)
|
||||
# Used for detecting correct favicon type and content-type detection
|
||||
puremagic
|
||||
puremagic<2.0 # 2.x requires Python >=3.12; unpin once 3.10/3.11 support is dropped
|
||||
|
||||
# Scheduler - Windows seemed to miss a lot of default timezone info (even "UTC" !)
|
||||
tzdata
|
||||
@@ -141,7 +147,7 @@ tzdata
|
||||
pluggy ~= 1.6
|
||||
|
||||
# LLM intent-based change evaluation (multi-provider via litellm)
|
||||
litellm>=1.40.0
|
||||
litellm>=1.40.0,<1.83.1 # 1.83.1–1.83.14 exact-pin jsonschema==4.23.0, conflicting with openapi-spec-validator's >=4.24.0 floor; re-evaluate when litellm fixes this
|
||||
# BM25 relevance trimming for large snapshots (pure Python, no ML)
|
||||
rank-bm25>=0.2.2
|
||||
|
||||
|
||||
Reference in New Issue
Block a user