API - Add restock config to API /v1/watch/ json output #4099 (#4103)
Build and push containers / metadata (push) Waiting to run
Build and push containers / build-push-containers (push) Waiting to run
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Waiting to run
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Test the built package works basically. (push) Blocked by required conditions
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Blocked by required conditions
ChangeDetection.io Container Build Test / Build linux/amd64 (alpine) (push) Waiting to run
ChangeDetection.io Container Build Test / Build linux/arm64 (alpine) (push) Waiting to run
ChangeDetection.io Container Build Test / Build linux/amd64 (main) (push) Waiting to run
ChangeDetection.io Container Build Test / Build linux/arm/v7 (main) (push) Waiting to run
ChangeDetection.io Container Build Test / Build linux/arm/v8 (main) (push) Waiting to run
ChangeDetection.io Container Build Test / Build linux/arm64 (main) (push) Waiting to run
ChangeDetection.io App Test / lint-code (push) Waiting to run
ChangeDetection.io App Test / lint-translations (push) Waiting to run
ChangeDetection.io App Test / lint-template-i18n (push) Waiting to run
ChangeDetection.io App Test / test-application-3-10 (push) Blocked by required conditions
ChangeDetection.io App Test / test-application-3-11 (push) Blocked by required conditions
ChangeDetection.io App Test / test-application-3-12 (push) Blocked by required conditions
ChangeDetection.io App Test / test-application-3-13 (push) Blocked by required conditions
ChangeDetection.io App Test / test-application-3-14 (push) Blocked by required conditions

This commit is contained in:
dgtlmoon
2026-04-29 20:10:47 +10:00
committed by GitHub
parent 44ac324a41
commit aadf8df7ae
4 changed files with 171 additions and 8 deletions
+21
View File
@@ -105,6 +105,27 @@ class Watch(Resource):
watch['viewed'] = watch_obj.viewed
watch['link'] = watch_obj.link
# Resolved processor config: tag override wins over watch-level config (mirrors restock processor logic)
import json
_restock_path = os.path.join(watch_obj.data_dir, 'restock_diff.json') if watch_obj.data_dir else None
restock_config = {}
if _restock_path and os.path.isfile(_restock_path):
try:
with open(_restock_path, 'r', encoding='utf-8') as _f:
restock_config = json.load(_f).get('restock_diff') or {}
except (json.JSONDecodeError, IOError) as e:
logger.warning(f"Failed to read restock_diff.json for watch {uuid}: {e}")
restock_source = 'watch'
tags = self.datastore.data['settings']['application'].get('tags', {})
for tag_uuid in (watch_obj.get('tags') or []):
tag = tags.get(tag_uuid, {})
if tag.get('overrides_watch'):
restock_config = dict(tag.get('processor_config_restock_diff') or {})
restock_source = f'tag:{tag_uuid}'
break
watch['processor_config_restock_diff'] = restock_config
watch['processor_config_restock_diff_source'] = restock_source
return watch
@auth.check_token
+95
View File
@@ -905,6 +905,101 @@ def test_api_restock_processor_config(client, live_server, measure_memory_usage,
delete_all_watches(client)
def test_api_watch_get_returns_resolved_restock_processor_config(client, live_server, measure_memory_usage, datastore_path):
"""
GET /api/v1/watch/{uuid} must include processor_config_restock_diff and
processor_config_restock_diff_source in the response.
Two cases:
- Watch-level config only: source == 'watch', config reflects the watch's own settings.
- Tag with overrides_watch=True: source == 'tag:<uuid>', config reflects the tag's settings
regardless of what the watch itself has stored.
"""
api_key = live_server.app.config['DATASTORE'].data['settings']['application'].get('api_access_token')
test_url = url_for('test_endpoint', _external=True)
# --- Case 1: watch-level config, no tag override ---
res = client.post(
url_for("createwatch"),
data=json.dumps({
"url": test_url,
"processor": "restock_diff",
"processor_config_restock_diff": {
"in_stock_processing": "all_changes",
"follow_price_changes": False,
"price_change_min": 1.23,
}
}),
headers={'content-type': 'application/json', 'x-api-key': api_key},
)
assert res.status_code == 201
watch_uuid = res.json.get('uuid')
res = client.get(url_for("watch", uuid=watch_uuid), headers={'x-api-key': api_key})
assert res.status_code == 200
data = res.json
assert 'processor_config_restock_diff' in data, "GET should include processor_config_restock_diff"
assert 'processor_config_restock_diff_source' in data, "GET should include processor_config_restock_diff_source"
assert data['processor_config_restock_diff_source'] == 'watch'
assert data['processor_config_restock_diff'].get('in_stock_processing') == 'all_changes'
assert data['processor_config_restock_diff'].get('follow_price_changes') == False
assert data['processor_config_restock_diff'].get('price_change_min') == 1.23
# --- Case 2: tag with overrides_watch=True overrides watch-level config ---
res = client.post(
url_for("tag"),
data=json.dumps({
"title": "Override tag",
"overrides_watch": True,
"processor_config_restock_diff": {
"in_stock_processing": "in_stock_only",
"follow_price_changes": True,
"price_change_min": 999.0,
}
}),
headers={'content-type': 'application/json', 'x-api-key': api_key},
)
assert res.status_code == 201
tag_uuid = res.json.get('uuid')
# Assign the tag to the watch
res = client.put(
url_for("watch", uuid=watch_uuid),
data=json.dumps({"tags": [tag_uuid]}),
headers={'content-type': 'application/json', 'x-api-key': api_key},
)
assert res.status_code == 200
res = client.get(url_for("watch", uuid=watch_uuid), headers={'x-api-key': api_key})
assert res.status_code == 200
data = res.json
assert data['processor_config_restock_diff_source'] == f'tag:{tag_uuid}', \
"Source should show the overriding tag UUID"
assert data['processor_config_restock_diff'].get('in_stock_processing') == 'in_stock_only', \
"Tag config should override watch-level config"
assert data['processor_config_restock_diff'].get('price_change_min') == 999.0, \
"Tag price_change_min should override watch-level value"
# processor_config_restock_diff is readonly — PUT attempts to set the resolved field should be
# silently ignored (the field is stripped before the watch is updated, same as other readOnly fields)
res = client.put(
url_for("watch", uuid=watch_uuid),
data=json.dumps({"processor_config_restock_diff": {"in_stock_processing": "off"}}),
headers={'content-type': 'application/json', 'x-api-key': api_key},
)
# PUT with processor_config_restock_diff is still valid (sets watch-level config),
# but the GET response continues to reflect the tag override
assert res.status_code == 200
res = client.get(url_for("watch", uuid=watch_uuid), headers={'x-api-key': api_key})
data = res.json
assert data['processor_config_restock_diff_source'] == f'tag:{tag_uuid}', \
"Tag override should still be active after PUT"
assert data['processor_config_restock_diff'].get('in_stock_processing') == 'in_stock_only', \
"Tag config should still win after PUT attempted to change watch-level config"
delete_all_watches(client)
def test_api_conflict_UI_password(client, live_server, measure_memory_usage, datastore_path):
+32 -1
View File
@@ -28,7 +28,7 @@ info:
For example: `x-api-key: YOUR_API_KEY`
version: 0.1.6
version: 0.1.7
contact:
name: ChangeDetection.io
url: https://github.com/dgtlmoon/changedetection.io
@@ -727,6 +727,37 @@ components:
description: Number of history snapshots available
readOnly: true
x-computed: true
processor_config_restock_diff:
type: object
readOnly: true
x-computed: true
description: |
Resolved restock/price processor config for this watch.
If a tag with `overrides_watch: true` is assigned to this watch, the tag's config is
returned here instead of the watch's own config. Use `processor_config_restock_diff_source`
to determine where the config originated.
properties:
in_stock_processing:
type: string
enum: [in_stock_only, all_changes, 'off']
follow_price_changes:
type: boolean
price_change_min:
type: [number, 'null']
price_change_max:
type: [number, 'null']
price_change_threshold_percent:
type: [number, 'null']
minimum: 0
maximum: 100
processor_config_restock_diff_source:
type: string
readOnly: true
x-computed: true
description: |
Indicates the origin of `processor_config_restock_diff`.
- `watch`: config comes from the watch itself
- `tag:<uuid>`: config is overridden by the tag with the given UUID
CreateWatch:
allOf:
+23 -7
View File
File diff suppressed because one or more lines are too long