Initial tags API

This commit is contained in:
dgtlmoon
2025-03-23 12:10:43 +01:00
parent f29f89d078
commit 8e36e9ddc4
3 changed files with 328 additions and 0 deletions
+164
View File
@@ -24,6 +24,13 @@ schema_create_watch['required'] = ['url']
schema_update_watch = copy.deepcopy(schema)
schema_update_watch['additionalProperties'] = False
# Tag schema is also based on watch_base since Tag inherits from it
schema_tag = copy.deepcopy(schema)
schema_create_tag = copy.deepcopy(schema_tag)
schema_create_tag['required'] = ['title']
schema_update_tag = copy.deepcopy(schema_tag)
schema_update_tag['additionalProperties'] = False
class Watch(Resource):
def __init__(self, **kwargs):
# datastore is a black box dependency
@@ -307,6 +314,163 @@ class CreateWatch(Resource):
return list, 200
class Tag(Resource):
def __init__(self, **kwargs):
# datastore is a black box dependency
self.datastore = kwargs['datastore']
# Get information about a single tag
# curl http://localhost:5000/api/v1/tag/<string:uuid>
@auth.check_token
def get(self, uuid):
"""
@api {get} /api/v1/tag/:uuid Single tag - get data or toggle notification muting.
@apiDescription Retrieve tag information and set notification_muted status
@apiExample {curl} Example usage:
curl http://localhost:5000/api/v1/tag/cc0cfffa-f449-477b-83ea-0caafd1dc091 -H"x-api-key:813031b16330fe25e3780cf0325daa45"
curl "http://localhost:5000/api/v1/tag/cc0cfffa-f449-477b-83ea-0caafd1dc091?muted=muted" -H"x-api-key:813031b16330fe25e3780cf0325daa45"
@apiName Tag
@apiGroup Tag
@apiParam {uuid} uuid Tag unique ID.
@apiQuery {String} [muted] =`muted` or =`unmuted` , Sets the MUTE NOTIFICATIONS state
@apiSuccess (200) {String} OK When muted operation OR full JSON object of the tag
@apiSuccess (200) {JSON} TagJSON JSON Full JSON object of the tag
"""
from copy import deepcopy
tag = deepcopy(self.datastore.data['settings']['application']['tags'].get(uuid))
if not tag:
abort(404, message='No tag exists with the UUID of {}'.format(uuid))
if request.args.get('muted', '') == 'muted':
self.datastore.data['settings']['application']['tags'][uuid]['notification_muted'] = True
return "OK", 200
elif request.args.get('muted', '') == 'unmuted':
self.datastore.data['settings']['application']['tags'][uuid]['notification_muted'] = False
return "OK", 200
return tag
@auth.check_token
def delete(self, uuid):
"""
@api {delete} /api/v1/tag/:uuid Delete a tag and remove it from all watches
@apiExample {curl} Example usage:
curl http://localhost:5000/api/v1/tag/cc0cfffa-f449-477b-83ea-0caafd1dc091 -X DELETE -H"x-api-key:813031b16330fe25e3780cf0325daa45"
@apiParam {uuid} uuid Tag unique ID.
@apiName DeleteTag
@apiGroup Tag
@apiSuccess (200) {String} OK Was deleted
"""
if not self.datastore.data['settings']['application']['tags'].get(uuid):
abort(400, message='No tag exists with the UUID of {}'.format(uuid))
# Delete the tag, and any tag reference
del self.datastore.data['settings']['application']['tags'][uuid]
# Remove tag from all watches
for watch_uuid, watch in self.datastore.data['watching'].items():
if watch.get('tags') and uuid in watch['tags']:
watch['tags'].remove(uuid)
return 'OK', 204
@auth.check_token
@expects_json(schema_update_tag)
def put(self, uuid):
"""
@api {put} /api/v1/tag/:uuid Update tag information
@apiExample {curl} Example usage:
Update (PUT)
curl http://localhost:5000/api/v1/tag/cc0cfffa-f449-477b-83ea-0caafd1dc091 -X PUT -H"x-api-key:813031b16330fe25e3780cf0325daa45" -H "Content-Type: application/json" -d '{"title": "New Tag Title"}'
@apiDescription Updates an existing tag using JSON
@apiParam {uuid} uuid Tag unique ID.
@apiName UpdateTag
@apiGroup Tag
@apiSuccess (200) {String} OK Was updated
@apiSuccess (500) {String} ERR Some other error
"""
tag = self.datastore.data['settings']['application']['tags'].get(uuid)
if not tag:
abort(404, message='No tag exists with the UUID of {}'.format(uuid))
tag.update(request.json)
self.datastore.needs_write_urgent = True
return "OK", 200
class Tags(Resource):
def __init__(self, **kwargs):
# datastore is a black box dependency
self.datastore = kwargs['datastore']
@auth.check_token
@expects_json(schema_create_tag)
def post(self):
"""
@api {post} /api/v1/tag Create a single tag
@apiDescription Requires atleast `title` set
@apiExample {curl} Example usage:
curl http://localhost:5000/api/v1/tag -H"x-api-key:813031b16330fe25e3780cf0325daa45" -H "Content-Type: application/json" -d '{"title": "New Tag"}'
@apiName CreateTag
@apiGroup Tag
@apiSuccess (200) {String} OK Was created
@apiSuccess (500) {String} ERR Some other error
"""
json_data = request.get_json()
title = json_data['title'].strip()
if self.datastore.tag_exists_by_name(title):
return "Tag with this title already exists", 400
new_uuid = self.datastore.add_tag(name=title)
if new_uuid:
# Update other fields if provided
tag = self.datastore.data['settings']['application']['tags'].get(new_uuid)
for k, v in json_data.items():
if k != 'title' and hasattr(tag, k):
tag[k] = v
self.datastore.needs_write_urgent = True
return {'uuid': new_uuid}, 201
else:
return "Failed to create tag", 400
@auth.check_token
def get(self):
"""
@api {get} /api/v1/tag List tags
@apiDescription Return list of available tags
@apiExample {curl} Example usage:
curl http://localhost:5000/api/v1/tag -H"x-api-key:813031b16330fe25e3780cf0325daa45"
{
"cc0cfffa-f449-477b-83ea-0caafd1dc091": {
"title": "Tech News",
"notification_muted": false,
"date_created": 1677103794
},
"e6f5fd5c-dbfe-468b-b8f3-f9d6ff5ad69b": {
"title": "Shopping",
"notification_muted": true,
"date_created": 1676662819
}
}
@apiName ListTags
@apiGroup Tag Management
@apiSuccess (200) {String} OK JSON dict
"""
result = {}
for uuid, tag in self.datastore.data['settings']['application']['tags'].items():
result[uuid] = {
'title': tag.get('title', ''),
'notification_muted': tag.get('notification_muted', False),
'date_created': tag.get('date_created', 0)
}
return result, 200
class Import(Resource):
def __init__(self, **kwargs):
# datastore is a black box dependency
+6
View File
@@ -271,6 +271,12 @@ def changedetection_app(config=None, datastore_o=None):
'/api/v1/import',
resource_class_kwargs={'datastore': datastore})
watch_api.add_resource(api_v1.Tags, '/api/v1/tag',
resource_class_kwargs={'datastore': datastore})
watch_api.add_resource(api_v1.Tag, '/api/v1/tag/<string:uuid>',
resource_class_kwargs={'datastore': datastore})
# Setup cors headers to allow all domains
# https://flask-cors.readthedocs.io/en/latest/
# CORS(app)
+158
View File
@@ -0,0 +1,158 @@
#!/usr/bin/env python3
import time
from flask import url_for
from .util import live_server_setup, wait_for_all_checks
import json
import uuid
def is_valid_uuid(val):
try:
uuid.UUID(str(val))
return True
except ValueError:
return False
def test_setup(client, live_server, measure_memory_usage):
live_server_setup(live_server)
def test_api_tags(client, live_server, measure_memory_usage):
api_key = live_server.app.config['DATASTORE'].data['settings']['application'].get('api_access_token')
# Make sure we start with no tags
res = client.get(
url_for("tags.tags_overview_page")
)
assert b'No tags' in res.data
# Create a tag via API
res = client.post(
"/api/v1/tag",
data=json.dumps({"title": "Test Tag"}),
headers={'content-type': 'application/json', 'x-api-key': api_key},
follow_redirects=True
)
assert res.status_code == 201
assert is_valid_uuid(res.json.get('uuid'))
tag_uuid = res.json.get('uuid')
# List tags - should include our new tag
res = client.get(
"/api/v1/tag",
headers={'x-api-key': api_key}
)
assert res.status_code == 200
assert tag_uuid in res.json
assert res.json[tag_uuid]['title'] == 'Test Tag'
assert res.json[tag_uuid]['notification_muted'] == False
# Get single tag
res = client.get(
f"/api/v1/tag/{tag_uuid}",
headers={'x-api-key': api_key}
)
assert res.status_code == 200
assert res.json['title'] == 'Test Tag'
# Update tag
res = client.put(
f"/api/v1/tag/{tag_uuid}",
data=json.dumps({"title": "Updated Tag"}),
headers={'content-type': 'application/json', 'x-api-key': api_key}
)
assert res.status_code == 200
assert b'OK' in res.data
# Verify update worked
res = client.get(
f"/api/v1/tag/{tag_uuid}",
headers={'x-api-key': api_key}
)
assert res.status_code == 200
assert res.json['title'] == 'Updated Tag'
# Mute tag notifications
res = client.get(
f"/api/v1/tag/{tag_uuid}?muted=muted",
headers={'x-api-key': api_key}
)
assert res.status_code == 200
assert b'OK' in res.data
# Verify muted status
res = client.get(
f"/api/v1/tag/{tag_uuid}",
headers={'x-api-key': api_key}
)
assert res.status_code == 200
assert res.json['notification_muted'] == True
# Unmute tag
res = client.get(
f"/api/v1/tag/{tag_uuid}?muted=unmuted",
headers={'x-api-key': api_key}
)
assert res.status_code == 200
assert b'OK' in res.data
# Verify unmuted status
res = client.get(
f"/api/v1/tag/{tag_uuid}",
headers={'x-api-key': api_key}
)
assert res.status_code == 200
assert res.json['notification_muted'] == False
# Create a watch with the tag
test_url = url_for('test_endpoint', _external=True)
res = client.post(
"/api/v1/watch",
data=json.dumps({"url": test_url, "tag": "Updated Tag", "title": "Watch with tag"}),
headers={'content-type': 'application/json', 'x-api-key': api_key},
follow_redirects=True
)
assert res.status_code == 201
watch_uuid = res.json.get('uuid')
# Verify tag is associated with watch
res = client.get(
url_for("watch", uuid=watch_uuid),
headers={'x-api-key': api_key}
)
assert res.status_code == 200
assert tag_uuid in res.json.get('tags', [])
# Delete tag
res = client.delete(
f"/api/v1/tag/{tag_uuid}",
headers={'x-api-key': api_key}
)
assert res.status_code == 204
# Verify tag is gone
res = client.get(
"/api/v1/tag",
headers={'x-api-key': api_key}
)
assert res.status_code == 200
assert tag_uuid not in res.json
# Verify tag was removed from watch
res = client.get(
f"/api/v1/watch/{watch_uuid}",
headers={'x-api-key': api_key}
)
assert res.status_code == 200
assert tag_uuid not in res.json.get('tags', [])
# Delete the watch
res = client.delete(
f"/api/v1/watch/{watch_uuid}",
headers={'x-api-key': api_key},
)
assert res.status_code == 204