mirror of
https://github.com/dgtlmoon/changedetection.io.git
synced 2026-04-30 06:47:03 +00:00
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
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:
@@ -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
|
||||
|
||||
@@ -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
@@ -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
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user