Compare commits

..

3 Commits

Author SHA1 Message Date
dgtlmoon
a0ab1ab6be Update README.md 2025-08-23 23:56:35 +02:00
dgtlmoon
f4f716fffa More openAPI fixes 2025-08-23 23:53:06 +02:00
dgtlmoon
6e6136aaa7 Lets go openapi spec 2025-08-23 23:32:53 +02:00
25 changed files with 233 additions and 375 deletions

View File

@@ -15,10 +15,6 @@ jobs:
ruff check . --select E9,F63,F7,F82
# Complete check with errors treated as warnings
ruff check . --exit-zero
- name: Validate OpenAPI spec
run: |
pip install openapi-spec-validator
python3 -c "from openapi_spec_validator import validate_spec; import yaml; validate_spec(yaml.safe_load(open('docs/api-spec.yaml')))"
test-application-3-10:
needs: lint-code

View File

@@ -5,6 +5,7 @@ ARG PYTHON_VERSION=3.11
FROM python:${PYTHON_VERSION}-slim-bookworm AS builder
# See `cryptography` pin comment in requirements.txt
ARG CRYPTOGRAPHY_DONT_BUILD_RUST=1
RUN apt-get update && apt-get install -y --no-install-recommends \
g++ \
@@ -16,7 +17,6 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
libxslt-dev \
make \
patch \
pkg-config \
zlib1g-dev
RUN mkdir /install
@@ -26,14 +26,6 @@ COPY requirements.txt /requirements.txt
# Use cache mounts and multiple wheel sources for faster ARM builds
ENV PIP_CACHE_DIR=/tmp/pip-cache
# Help Rust find OpenSSL for cryptography package compilation on ARM
ENV PKG_CONFIG_PATH="/usr/lib/pkgconfig:/usr/lib/arm-linux-gnueabihf/pkgconfig:/usr/lib/aarch64-linux-gnu/pkgconfig"
ENV PKG_CONFIG_ALLOW_SYSTEM_CFLAGS=1
ENV OPENSSL_DIR="/usr"
ENV OPENSSL_LIB_DIR="/usr/lib/arm-linux-gnueabihf"
ENV OPENSSL_INCLUDE_DIR="/usr/include/openssl"
# Additional environment variables for cryptography Rust build
ENV CRYPTOGRAPHY_DONT_BUILD_RUST=1
RUN --mount=type=cache,target=/tmp/pip-cache \
pip install \
--extra-index-url https://www.piwheels.org/simple \

View File

@@ -2,7 +2,7 @@
# Read more https://github.com/dgtlmoon/changedetection.io/wiki
__version__ = '0.50.12'
__version__ = '0.50.10'
from changedetectionio.strtobool import strtobool
from json.decoder import JSONDecodeError

View File

@@ -3,7 +3,7 @@ from changedetectionio.strtobool import strtobool
from flask_restful import abort, Resource
from flask import request
import validators
from . import auth, validate_openapi_request
from . import auth
class Import(Resource):
@@ -12,7 +12,6 @@ class Import(Resource):
self.datastore = kwargs['datastore']
@auth.check_token
@validate_openapi_request('importWatches')
def post(self):
"""Import a list of watched URLs."""

View File

@@ -1,7 +1,9 @@
from flask_expects_json import expects_json
from flask_restful import Resource, abort
from flask_restful import Resource
from . import auth
from flask_restful import abort, Resource
from flask import request
from . import auth, validate_openapi_request
from . import auth
from . import schema_create_notification_urls, schema_delete_notification_urls
class Notifications(Resource):
@@ -10,7 +12,6 @@ class Notifications(Resource):
self.datastore = kwargs['datastore']
@auth.check_token
@validate_openapi_request('getNotifications')
def get(self):
"""Return Notification URL List."""
@@ -21,7 +22,6 @@ class Notifications(Resource):
}, 200
@auth.check_token
@validate_openapi_request('addNotifications')
@expects_json(schema_create_notification_urls)
def post(self):
"""Create Notification URLs."""
@@ -49,7 +49,6 @@ class Notifications(Resource):
return {'notification_urls': added_urls}, 201
@auth.check_token
@validate_openapi_request('replaceNotifications')
@expects_json(schema_create_notification_urls)
def put(self):
"""Replace Notification URLs."""
@@ -72,7 +71,6 @@ class Notifications(Resource):
return {'notification_urls': clean_urls}, 200
@auth.check_token
@validate_openapi_request('deleteNotifications')
@expects_json(schema_delete_notification_urls)
def delete(self):
"""Delete Notification URLs."""

View File

@@ -1,6 +1,6 @@
from flask_restful import Resource, abort
from flask import request
from . import auth, validate_openapi_request
from . import auth
class Search(Resource):
def __init__(self, **kwargs):
@@ -8,7 +8,6 @@ class Search(Resource):
self.datastore = kwargs['datastore']
@auth.check_token
@validate_openapi_request('searchWatches')
def get(self):
"""Search for watches by URL or title text."""
query = request.args.get('q', '').strip()

View File

@@ -1,5 +1,5 @@
from flask_restful import Resource
from . import auth, validate_openapi_request
from . import auth
class SystemInfo(Resource):
@@ -9,7 +9,6 @@ class SystemInfo(Resource):
self.update_q = kwargs['update_q']
@auth.check_token
@validate_openapi_request('getSystemInfo')
def get(self):
"""Return system info."""
import time

View File

@@ -7,7 +7,7 @@ from flask import request
from . import auth
# Import schemas from __init__.py
from . import schema_tag, schema_create_tag, schema_update_tag, validate_openapi_request
from . import schema_tag, schema_create_tag, schema_update_tag
class Tag(Resource):
@@ -19,7 +19,6 @@ class Tag(Resource):
# Get information about a single tag
# curl http://localhost:5000/api/v1/tag/<string:uuid>
@auth.check_token
@validate_openapi_request('getTag')
def get(self, uuid):
"""Get data for a single tag/group, toggle notification muting, or recheck all."""
from copy import deepcopy
@@ -51,7 +50,6 @@ class Tag(Resource):
return tag
@auth.check_token
@validate_openapi_request('deleteTag')
def delete(self, uuid):
"""Delete a tag/group and remove it from all watches."""
if not self.datastore.data['settings']['application']['tags'].get(uuid):
@@ -68,7 +66,6 @@ class Tag(Resource):
return 'OK', 204
@auth.check_token
@validate_openapi_request('updateTag')
@expects_json(schema_update_tag)
def put(self, uuid):
"""Update tag information."""
@@ -83,7 +80,6 @@ class Tag(Resource):
@auth.check_token
@validate_openapi_request('createTag')
# Only cares for {'title': 'xxxx'}
def post(self):
"""Create a single tag/group."""
@@ -104,7 +100,6 @@ class Tags(Resource):
self.datastore = kwargs['datastore']
@auth.check_token
@validate_openapi_request('listTags')
def get(self):
"""List tags/groups."""
result = {}

View File

@@ -11,7 +11,7 @@ from . import auth
import copy
# Import schemas from __init__.py
from . import schema, schema_create_watch, schema_update_watch, validate_openapi_request
from . import schema, schema_create_watch, schema_update_watch
class Watch(Resource):
@@ -25,7 +25,6 @@ class Watch(Resource):
# @todo - version2 - ?muted and ?paused should be able to be called together, return the watch struct not "OK"
# ?recheck=true
@auth.check_token
@validate_openapi_request('getWatch')
def get(self, uuid):
"""Get information about a single watch, recheck, pause, or mute."""
from copy import deepcopy
@@ -58,7 +57,6 @@ class Watch(Resource):
return watch
@auth.check_token
@validate_openapi_request('deleteWatch')
def delete(self, uuid):
"""Delete a watch and related history."""
if not self.datastore.data['watching'].get(uuid):
@@ -68,7 +66,6 @@ class Watch(Resource):
return 'OK', 204
@auth.check_token
@validate_openapi_request('updateWatch')
@expects_json(schema_update_watch)
def put(self, uuid):
"""Update watch information."""
@@ -94,7 +91,6 @@ class WatchHistory(Resource):
# Get a list of available history for a watch by UUID
# curl http://localhost:5000/api/v1/watch/<string:uuid>/history
@auth.check_token
@validate_openapi_request('getWatchHistory')
def get(self, uuid):
"""Get a list of all historical snapshots available for a watch."""
watch = self.datastore.data['watching'].get(uuid)
@@ -109,7 +105,6 @@ class WatchSingleHistory(Resource):
self.datastore = kwargs['datastore']
@auth.check_token
@validate_openapi_request('getWatchSnapshot')
def get(self, uuid, timestamp):
"""Get single snapshot from watch."""
watch = self.datastore.data['watching'].get(uuid)
@@ -143,7 +138,6 @@ class WatchFavicon(Resource):
self.datastore = kwargs['datastore']
@auth.check_token
@validate_openapi_request('getWatchFavicon')
def get(self, uuid):
"""Get favicon for a watch."""
watch = self.datastore.data['watching'].get(uuid)
@@ -178,7 +172,6 @@ class CreateWatch(Resource):
self.update_q = kwargs['update_q']
@auth.check_token
@validate_openapi_request('createWatch')
@expects_json(schema_create_watch)
def post(self):
"""Create a single watch."""
@@ -214,7 +207,6 @@ class CreateWatch(Resource):
return "Invalid or unsupported URL", 400
@auth.check_token
@validate_openapi_request('listWatches')
def get(self):
"""List watches."""
list = {}

View File

@@ -1,9 +1,4 @@
import copy
import yaml
import functools
from flask import request, abort
from openapi_core import OpenAPI
from openapi_core.contrib.flask import FlaskOpenAPIRequest
from . import api_schema
from ..model import watch_base
@@ -13,7 +8,6 @@ schema = api_schema.build_watch_json_schema(watch_base_config)
schema_create_watch = copy.deepcopy(schema)
schema_create_watch['required'] = ['url']
del schema_create_watch['properties']['last_viewed']
schema_update_watch = copy.deepcopy(schema)
schema_update_watch['additionalProperties'] = False
@@ -31,38 +25,6 @@ schema_create_notification_urls['required'] = ['notification_urls']
schema_delete_notification_urls = copy.deepcopy(schema_notification_urls)
schema_delete_notification_urls['required'] = ['notification_urls']
# Load OpenAPI spec for validation
_openapi_spec = None
def get_openapi_spec():
global _openapi_spec
if _openapi_spec is None:
import os
spec_path = os.path.join(os.path.dirname(__file__), '../../docs/api-spec.yaml')
with open(spec_path, 'r') as f:
spec_dict = yaml.safe_load(f)
_openapi_spec = OpenAPI.from_dict(spec_dict)
return _openapi_spec
def validate_openapi_request(operation_id):
"""Decorator to validate incoming requests against OpenAPI spec."""
def decorator(f):
@functools.wraps(f)
def wrapper(*args, **kwargs):
try:
spec = get_openapi_spec()
openapi_request = FlaskOpenAPIRequest(request)
result = spec.unmarshal_request(openapi_request)
if result.errors:
abort(400, message=f"OpenAPI validation failed: {result.errors}")
return f(*args, **kwargs)
except Exception as e:
# If OpenAPI validation fails, log but don't break existing functionality
print(f"OpenAPI validation warning for {operation_id}: {e}")
return f(*args, **kwargs)
return wrapper
return decorator
# Import all API resources
from .Watch import Watch, WatchHistory, WatchSingleHistory, CreateWatch, WatchFavicon
from .Tags import Tags, Tag

View File

@@ -78,13 +78,6 @@ def build_watch_json_schema(d):
]:
schema['properties'][v]['anyOf'].append({'type': 'string', "maxLength": 5000})
for v in ['last_viewed']:
schema['properties'][v] = {
"type": "integer",
"description": "Unix timestamp in seconds of the last time the watch was viewed.",
"minimum": 0
}
# None or Boolean
schema['properties']['track_ldjson_price_data']['anyOf'].append({'type': 'boolean'})

View File

@@ -203,7 +203,7 @@ nav
<div class="tab-pane-inner" id="api">
<h4>API Access</h4>
<p>Drive your changedetection.io via API, More about <a href="https://changedetection.io/docs/api_v1/index.html">API access and examples here</a>.</p>
<p>Drive your changedetection.io via API, More about <a href="https://github.com/dgtlmoon/changedetection.io/wiki/API-Reference">API access here</a></p>
<div class="pure-control-group">
{{ render_checkbox_field(form.application.form.api_access_token_enabled) }}

View File

@@ -44,16 +44,12 @@ def construct_blueprint(datastore: ChangeDetectionStore, update_q, queuedWatchMe
# Sort by last_changed and add the uuid which is usually the key..
sorted_watches = []
with_errors = request.args.get('with_errors') == "1"
unread_only = request.args.get('unread') == "1"
errored_count = 0
search_q = request.args.get('q').strip().lower() if request.args.get('q') else False
for uuid, watch in datastore.data['watching'].items():
if with_errors and not watch.get('last_error'):
continue
if unread_only and (watch.viewed or watch.last_changed == 0) :
continue
if active_tag_uuid and not active_tag_uuid in watch['tags']:
continue
if watch.get('last_error'):

View File

@@ -245,9 +245,6 @@ document.addEventListener('DOMContentLoaded', function() {
<a href="{{url_for('ui.mark_all_viewed', tag=active_tag_uuid) }}" class="pure-button button-tag " id="mark-all-viewed">Mark all viewed in '{{active_tag.title}}'</a>
</li>
{%- endif -%}
<li id="post-list-unread" class="{%- if has_unviewed -%}has-unviewed{%- endif -%}" style="display: none;" >
<a href="{{url_for('watchlist.index', unread=1, tag=request.args.get('tag')) }}" class="pure-button button-tag">Unread</a>
</li>
<li>
<a href="{{ url_for('ui.form_watch_checknow', tag=active_tag_uuid, with_errors=request.args.get('with_errors',0)) }}" class="pure-button button-tag" id="recheck-all">Recheck
all {% if active_tag_uuid %} in '{{active_tag.title}}'{%endif%}</a>

View File

@@ -251,7 +251,8 @@ class perform_site_check(difference_detection_processor):
update_obj["last_check_status"] = self.fetcher.get_last_status_code()
# 615 Extract text by regex
extract_text = list(dict.fromkeys(watch.get('extract_text', []) + self.datastore.get_tag_overrides_for_watch(uuid=watch.get('uuid'), attr='extract_text')))
extract_text = watch.get('extract_text', [])
extract_text += self.datastore.get_tag_overrides_for_watch(uuid=watch.get('uuid'), attr='extract_text')
if len(extract_text) > 0:
regex_matched_output = []
for s_re in extract_text:
@@ -310,7 +311,8 @@ class perform_site_check(difference_detection_processor):
############ Blocking rules, after checksum #################
blocked = False
trigger_text = list(dict.fromkeys(watch.get('trigger_text', []) + self.datastore.get_tag_overrides_for_watch(uuid=watch.get('uuid'), attr='trigger_text')))
trigger_text = watch.get('trigger_text', [])
trigger_text += self.datastore.get_tag_overrides_for_watch(uuid=watch.get('uuid'), attr='trigger_text')
if len(trigger_text):
# Assume blocked
blocked = True
@@ -324,7 +326,8 @@ class perform_site_check(difference_detection_processor):
if result:
blocked = False
text_should_not_be_present = list(dict.fromkeys(watch.get('text_should_not_be_present', []) + self.datastore.get_tag_overrides_for_watch(uuid=watch.get('uuid'), attr='text_should_not_be_present')))
text_should_not_be_present = watch.get('text_should_not_be_present', [])
text_should_not_be_present += self.datastore.get_tag_overrides_for_watch(uuid=watch.get('uuid'), attr='text_should_not_be_present')
if len(text_should_not_be_present):
# If anything matched, then we should block a change from happening
result = html_tools.strip_ignore_text(content=str(stripped_text_from_html),

View File

@@ -153,7 +153,6 @@ $(document).ready(function () {
// Tabs at bottom of list
$('#post-list-mark-views').toggleClass("has-unviewed", general_stats.has_unviewed);
$('#post-list-unread').toggleClass("has-unviewed", general_stats.has_unviewed);
$('#post-list-with-errors').toggleClass("has-error", general_stats.count_errors !== 0)
$('#post-list-with-errors a').text(`With errors (${ general_stats.count_errors })`);

View File

@@ -24,9 +24,6 @@ body.checking-now {
#post-list-mark-views.has-unviewed {
display: inline-block !important;
}
#post-list-unread.has-unviewed {
display: inline-block !important;
}
}

View File

@@ -1130,12 +1130,11 @@ ul {
}
#realtime-conn-error {
position: fixed;
position: absolute;
bottom: 0;
left: 0;
left: 30px;
background: var(--color-warning);
padding: 10px;
font-size: 0.8rem;
color: #fff;
opacity: 0.8;
}

File diff suppressed because one or more lines are too long

View File

@@ -236,7 +236,7 @@
<script src="{{url_for('static_content', group='js', filename='toggle-theme.js')}}" defer></script>
<div id="checking-now-fixed-tab" style="display: none;"><span class="spinner"></span><span>&nbsp;Checking now</span></div>
<div id="realtime-conn-error" style="display:none">Real-time updates offline</div>
<div id="realtime-conn-error" style="display:none">Offline</div>
</body>
</html>

View File

@@ -311,7 +311,7 @@ def test_api_watch_PUT_update(client, live_server, measure_memory_usage):
"value": "." # contains anything
}
],
"conditions_match_logic": "ALL",
"conditions_match_logic": "ALL"
}
),
headers={'content-type': 'application/json', 'x-api-key': api_key},
@@ -328,7 +328,6 @@ def test_api_watch_PUT_update(client, live_server, measure_memory_usage):
)
watch_uuid = list(res.json.keys())[0]
assert not res.json[watch_uuid].get('viewed'), 'A newly created watch can only be unviewed'
# Check in the edit page just to be sure
res = client.get(
@@ -342,12 +341,7 @@ def test_api_watch_PUT_update(client, live_server, measure_memory_usage):
res = client.put(
url_for("watch", uuid=watch_uuid),
headers={'x-api-key': api_key, 'content-type': 'application/json'},
data=json.dumps({
"title": "new title",
'time_between_check': {'minutes': 552},
'headers': {'cookie': 'all eaten'},
'last_viewed': int(time.time())
}),
data=json.dumps({"title": "new title", 'time_between_check': {'minutes': 552}, 'headers': {'cookie': 'all eaten'}}),
)
assert res.status_code == 200, "HTTP PUT update was sent OK"
@@ -357,7 +351,6 @@ def test_api_watch_PUT_update(client, live_server, measure_memory_usage):
headers={'x-api-key': api_key}
)
assert res.json.get('title') == 'new title'
assert res.json.get('viewed'), 'With the timestamp greater than "changed" a watch can be updated to viewed'
# Check in the edit page just to be sure
res = client.get(
@@ -390,7 +383,7 @@ def test_api_watch_PUT_update(client, live_server, measure_memory_usage):
def test_api_import(client, live_server, measure_memory_usage):
api_key = live_server.app.config['DATASTORE'].data['settings']['application'].get('api_access_token')
res = client.post(

View File

@@ -84,7 +84,6 @@ services:
# Comment out ports: when using behind a reverse proxy , enable networks: etc.
# Mac users! Use "127.0.0.1:5050:5000" (port 5050) so theres no conflict with Airplay etc. (https://github.com/dgtlmoon/changedetection.io/issues/3401)
ports:
- 127.0.0.1:5000:5000
restart: unless-stopped

View File

@@ -6,7 +6,7 @@ info:
REST API for managing Page watches, Group tags, and Notifications.
changedetection.io can be driven by its built in simple API, in the examples below you will also find `curl` command line and `python` examples to help you get started faster.
changedetection.io can be driven by its built in simple API, in the examples below you will also find `curl` command line examples to help you.
## Where to find my API key?
@@ -28,7 +28,7 @@ info:
For example: `x-api-key: YOUR_API_KEY`
version: 0.1.0
version: 1.0.0
contact:
name: ChangeDetection.io
url: https://github.com/dgtlmoon/changedetection.io
@@ -89,8 +89,6 @@ tags:
Configure global notification endpoints that can be used across all your watches. Supports various
notification services including email, Discord, Slack, webhooks, and many other popular platforms.
These settings serve as defaults that can be overridden at the individual watch or tag level.
The notification syntax uses [https://github.com/caronc/apprise](https://github.com/caronc/apprise).
- name: Search
description: |
@@ -119,9 +117,14 @@ components:
Enter your API key in the "Authorize" button above to automatically populate all code examples.
schemas:
WatchBase:
Watch:
type: object
properties:
uuid:
type: string
format: uuid
description: Unique identifier for the watch
readOnly: true
url:
type: string
format: uri
@@ -129,11 +132,11 @@ components:
maxLength: 5000
title:
type: string
description: Custom title for the web page change monitor (watch)
description: Custom title for the watch
maxLength: 5000
tag:
type: string
description: Tag UUID to associate with this web page change monitor (watch)
description: Tag UUID to associate with this watch
maxLength: 5000
tags:
type: array
@@ -142,7 +145,7 @@ components:
description: Array of tag UUIDs
paused:
type: boolean
description: Whether the web page change monitor (watch) is paused
description: Whether the watch is paused
muted:
type: boolean
description: Whether notifications are muted
@@ -192,7 +195,7 @@ components:
type: array
items:
type: string
description: Notification URLs for this web page change monitor (watch)
description: Notification URLs for this watch
notification_title:
type: string
description: Custom notification title
@@ -224,40 +227,25 @@ components:
maxLength: 5000
required: [operation, selector, optional_value]
description: Browser automation steps
last_checked:
type: integer
description: Unix timestamp of last check
readOnly: true
last_changed:
type: integer
description: Unix timestamp of last change
readOnly: true
last_error:
type: string
description: Last error message
readOnly: true
required:
- url
Watch:
allOf:
- $ref: '#/components/schemas/WatchBase'
- type: object
properties:
uuid:
type: string
format: uuid
description: Unique identifier for the web page change monitor (watch)
readOnly: true
last_checked:
type: integer
description: Unix timestamp of last check
readOnly: true
last_changed:
type: integer
description: Unix timestamp of last change
readOnly: true
last_error:
type: string
description: Last error message
readOnly: true
last_viewed:
type: integer
description: Unix timestamp in seconds of the last time the watch was viewed. Setting it to a value higher than `last_changed` in the "Update watch" endpoint marks the watch as viewed.
minimum: 0
CreateWatch:
allOf:
- $ref: '#/components/schemas/WatchBase'
- type: 'object'
- $ref: '#/components/schemas/Watch'
- type: object
required:
- url
@@ -277,7 +265,7 @@ components:
type: array
items:
type: string
description: Default notification URLs for web page change monitors (watches) with this tag
description: Default notification URLs for watches with this tag
notification_muted:
type: boolean
description: Whether notifications are muted for this tag
@@ -301,7 +289,7 @@ components:
properties:
watch_count:
type: integer
description: Total number of web page change monitors (watches)
description: Total number of watches
tag_count:
type: integer
description: Total number of tags
@@ -319,7 +307,7 @@ components:
type: object
additionalProperties:
$ref: '#/components/schemas/Watch'
description: Dictionary of matching web page change monitors (watches) keyed by UUID
description: Dictionary of matching watches keyed by UUID
WatchHistory:
type: object
@@ -338,10 +326,9 @@ components:
paths:
/watch:
get:
operationId: listWatches
tags: [Watch Management]
summary: List all watches
description: Return concise list of available web page change monitors (watches) and basic info
description: Return concise list of available watches and basic info
x-code-samples:
- lang: 'curl'
source: |
@@ -401,10 +388,9 @@ paths:
last_checked: 1640998800
last_changed: 1640995200
post:
operationId: createWatch
tags: [Watch Management]
summary: Create a new watch
description: Create a single web page change monitor (watch). Requires at least 'url' to be set.
description: Create a single watch. Requires at least 'url' to be set.
x-code-samples:
- lang: 'curl'
source: |
@@ -450,7 +436,7 @@ paths:
hours: 1
responses:
'200':
description: Web page change monitor (watch) created successfully
description: Watch created successfully
content:
text/plain:
schema:
@@ -465,10 +451,10 @@ paths:
/watch/{uuid}:
get:
operationId: getWatch
operationId: getSingleWatch
tags: [Watch Management]
summary: Get single watch
description: Retrieve web page change monitor (watch) information and set muted/paused status. Returns the FULL Watch JSON.
description: Retrieve watch information and set muted/paused status. Returns the FULL Watch JSON.
x-code-samples:
- lang: 'curl'
source: |
@@ -486,13 +472,13 @@ paths:
- name: uuid
in: path
required: true
description: Web page change monitor (watch) unique ID
description: Watch unique ID
schema:
type: string
format: uuid
- name: recheck
in: query
description: Recheck this web page change monitor (watch)
description: Recheck this watch
schema:
type: string
enum: ["1", "true"]
@@ -520,7 +506,7 @@ paths:
type: string
example: "OK"
'404':
description: Web page change monitor (watch) not found
description: Watch not found
content:
application/json:
schema:
@@ -530,7 +516,7 @@ paths:
operationId: updateWatch
tags: [Watch Management]
summary: Update watch
description: Update an existing web page change monitor (watch) using JSON. Accepts the same structure as returned in [get single watch information](#operation/getWatch).
description: Update an existing watch using JSON. Accepts the same structure as returned in [get single watch information](#operation/getSingleWatch).
x-code-samples:
- lang: 'curl'
source: |
@@ -563,7 +549,7 @@ paths:
- name: uuid
in: path
required: true
description: Web page change monitor (watch) unique ID
description: Watch unique ID
schema:
type: string
format: uuid
@@ -575,7 +561,7 @@ paths:
$ref: '#/components/schemas/Watch'
responses:
'200':
description: Web page change monitor (watch) updated successfully
description: Watch updated successfully
content:
text/plain:
schema:
@@ -585,10 +571,9 @@ paths:
description: Server error
delete:
operationId: deleteWatch
tags: [Watch Management]
tags: [Watch Management]
summary: Delete watch
description: Delete a web page change monitor (watch) and all related history
description: Delete a watch and all related history
x-code-samples:
- lang: 'curl'
source: |
@@ -606,13 +591,13 @@ paths:
- name: uuid
in: path
required: true
description: Web page change monitor (watch) unique ID
description: Watch unique ID
schema:
type: string
format: uuid
responses:
'200':
description: Web page change monitor (watch) deleted successfully
description: Watch deleted successfully
content:
text/plain:
schema:
@@ -621,10 +606,9 @@ paths:
/watch/{uuid}/history:
get:
operationId: getWatchHistory
tags: [Watch History]
summary: Get watch history
description: Get a list of all historical snapshots available for a web page change monitor (watch)
description: Get a list of all historical snapshots available for a watch
x-code-samples:
- lang: 'curl'
source: |
@@ -642,7 +626,7 @@ paths:
- name: uuid
in: path
required: true
description: Web page change monitor (watch) unique ID
description: Watch unique ID
schema:
type: string
format: uuid
@@ -657,14 +641,13 @@ paths:
"1640995200": "/path/to/snapshot1.txt"
"1640998800": "/path/to/snapshot2.txt"
'404':
description: Web page change monitor (watch) not found
description: Watch not found
/watch/{uuid}/history/{timestamp}:
get:
operationId: getWatchSnapshot
tags: [Snapshots]
summary: Get single snapshot
description: Get single snapshot from web page change monitor (watch). Use 'latest' for the most recent snapshot.
description: Get single snapshot from watch. Use 'latest' for the most recent snapshot.
x-code-samples:
- lang: 'curl'
source: |
@@ -683,7 +666,7 @@ paths:
- name: uuid
in: path
required: true
description: Web page change monitor (watch) unique ID
description: Watch unique ID
schema:
type: string
format: uuid
@@ -714,10 +697,9 @@ paths:
/watch/{uuid}/favicon:
get:
operationId: getWatchFavicon
tags: [Favicon]
summary: Get watch favicon
description: Get the favicon for a web page change monitor (watch) as displayed in the watch overview list.
description: Get the favicon for a watch as displayed in the watch overview list.
x-code-samples:
- lang: 'curl'
source: |
@@ -737,7 +719,7 @@ paths:
- name: uuid
in: path
required: true
description: Web page change monitor (watch) unique ID
description: Watch unique ID
schema:
type: string
format: uuid
@@ -754,7 +736,6 @@ paths:
/tags:
get:
operationId: listTags
tags: [Group / Tag Management]
summary: List all tags
description: Return list of available tags/groups
@@ -791,62 +772,11 @@ paths:
notification_urls: ["discord://webhook_id/webhook_token"]
notification_muted: false
/tag:
post:
operationId: createTag
tags: [Group / Tag Management]
summary: Create tag
description: Create a single tag/group
x-code-samples:
- lang: 'curl'
source: |
curl -X POST "http://localhost:5000/api/v1/tag" \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Important Sites"
}'
- lang: 'Python'
source: |
import requests
headers = {
'x-api-key': 'YOUR_API_KEY',
'Content-Type': 'application/json'
}
data = {'title': 'Important Sites'}
response = requests.post('http://localhost:5000/api/v1/tag',
headers=headers, json=data)
print(response.json())
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/Tag'
example:
title: "Important Sites"
responses:
'201':
description: Tag created successfully
content:
application/json:
schema:
type: object
properties:
uuid:
type: string
format: uuid
description: UUID of the created tag
'400':
description: Invalid or unsupported tag
/tag/{uuid}:
get:
operationId: getTag
tags: [Group / Tag Management]
summary: Get single tag
description: Retrieve tag information, set notification_muted status, recheck all web page change monitors (watches) in tag.
description: Retrieve tag information, set notification_muted status, recheck all in tag.
x-code-samples:
- lang: 'curl'
source: |
@@ -876,7 +806,7 @@ paths:
enum: [muted, unmuted]
- name: recheck
in: query
description: Queue all web page change monitors (watches) with this tag for recheck
description: Queue all watches with this tag for recheck
schema:
type: string
enum: ["true"]
@@ -895,7 +825,6 @@ paths:
description: Tag not found
put:
operationId: updateTag
tags: [Group / Tag Management]
summary: Update tag
description: Update an existing tag using JSON
@@ -946,10 +875,9 @@ paths:
description: Server error
delete:
operationId: deleteTag
tags: [Group / Tag Management]
summary: Delete tag
description: Delete a tag/group and remove it from all web page change monitors (watches)
description: Delete a tag/group and remove it from all watches
x-code-samples:
- lang: 'curl'
source: |
@@ -975,10 +903,48 @@ paths:
'200':
description: Tag deleted successfully
post:
tags: [Group / Tag Management]
summary: Create tag
description: Create a single tag/group
x-code-samples:
- lang: 'curl'
source: |
curl -X POST "http://localhost:5000/api/v1/tag/550e8400-e29b-41d4-a716-446655440000" \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Important Sites"
}'
- lang: 'Python'
source: |
import requests
headers = {
'x-api-key': 'YOUR_API_KEY',
'Content-Type': 'application/json'
}
tag_uuid = '550e8400-e29b-41d4-a716-446655440000'
data = {'title': 'Important Sites'}
response = requests.post(f'http://localhost:5000/api/v1/tag/{tag_uuid}',
headers=headers, json=data)
print(response.text)
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/Tag'
example:
title: "Important Sites"
responses:
'200':
description: Tag created successfully
'500':
description: Server error
/notifications:
get:
operationId: getNotifications
tags: [Notifications]
summary: Get notification URLs
description: Return the notification URL list from the configuration
@@ -1003,7 +969,6 @@ paths:
$ref: '#/components/schemas/NotificationUrls'
post:
operationId: addNotifications
tags: [Notifications]
summary: Add notification URLs
description: Add one or more notification URLs to the configuration
@@ -1057,7 +1022,6 @@ paths:
description: Invalid input
put:
operationId: replaceNotifications
tags: [Notifications]
summary: Replace notification URLs
description: Replace all notification URLs with the provided list (can be empty)
@@ -1105,7 +1069,6 @@ paths:
description: Invalid input
delete:
operationId: deleteNotifications
tags: [Notifications]
summary: Delete notification URLs
description: Delete one or more notification URLs from the configuration
@@ -1150,10 +1113,9 @@ paths:
/search:
get:
operationId: searchWatches
tags: [Search]
summary: Search watches
description: Search web page change monitors (watches) by URL or title text
description: Search watches by URL or title text
x-code-samples:
- lang: 'curl'
source: |
@@ -1205,7 +1167,6 @@ paths:
/import:
post:
operationId: importWatches
tags: [Import]
summary: Import watch URLs
description: Import a list of URLs to monitor. Accepts line-separated URLs in request body.
@@ -1231,17 +1192,17 @@ paths:
parameters:
- name: tag_uuids
in: query
description: Tag UUID to apply to imported web page change monitors (watches)
description: Tag UUID to apply to imported watches
schema:
type: string
- name: tag
in: query
description: Tag name to apply to imported web page change monitors (watches)
description: Tag name to apply to imported watches
schema:
type: string
- name: proxy
in: query
description: Proxy key to use for imported web page change monitors (watches)
description: Proxy key to use for imported watches
schema:
type: string
- name: dedupe
@@ -1276,7 +1237,6 @@ paths:
/systeminfo:
get:
operationId: getSystemInfo
tags: [System Information]
summary: Get system information
description: Return information about the current system state
@@ -1303,4 +1263,4 @@ paths:
watch_count: 42
tag_count: 5
uptime: "2 days, 3:45:12"
version: "0.50.10"
version: "0.50.10"

File diff suppressed because one or more lines are too long

View File

@@ -41,16 +41,13 @@ jsonpath-ng~=1.5.3
# Notification library
apprise==1.9.3
# - Needed for apprise/spush, and maybe others? hopefully doesnt trigger a rust compile.
# - Requires extra wheel for rPi, adds build time for arm/v8 which is not in piwheels
# Pinned to 43.0.1 for ARM compatibility (45.x may not have pre-built ARM wheels)
# Also pinned because dependabot wants specific versions
cryptography==44.0.1
# apprise mqtt https://github.com/dgtlmoon/changedetection.io/issues/315
# use any version other than 2.0.x due to https://github.com/eclipse/paho.mqtt.python/issues/814
paho-mqtt!=2.0.*
# Requires extra wheel for rPi
#cryptography~=42.0.8
# Used for CSS filtering
beautifulsoup4>=4.0.0
@@ -92,9 +89,6 @@ pytest-flask ~=1.2
# Anything 4.0 and up but not 5.0
jsonschema ~= 4.0
# OpenAPI validation support
openapi-core[flask] >= 0.19.0
loguru