From a807f899f562cb09298f5554b1ee3c8542caecde Mon Sep 17 00:00:00 2001 From: Santiago Lo Coco Date: Tue, 5 Nov 2024 18:42:26 +0100 Subject: [PATCH] Update API, fix bugs and add more methods --- docker/Dockerfile | 2 -- src/.env.dev | 4 ++- src/api.py | 84 ++++++++++++++++++++++++++++++++--------------- src/app.py | 7 ++-- src/config.py | 2 ++ src/db.py | 56 ++++++++++++++++++++++++++----- src/register.py | 26 ++++++++++++--- src/utils.py | 15 +++++++++ 8 files changed, 151 insertions(+), 45 deletions(-) create mode 100644 src/utils.py diff --git a/docker/Dockerfile b/docker/Dockerfile index db92dd2..8301766 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -8,8 +8,6 @@ RUN pip install --no-cache-dir -r requirements.txt COPY . . -EXPOSE 5000 - STOPSIGNAL SIGTERM CMD ["python", "app.py"] diff --git a/src/.env.dev b/src/.env.dev index 9b5c367..4130309 100644 --- a/src/.env.dev +++ b/src/.env.dev @@ -3,4 +3,6 @@ API_HOST=room7200.local SERVICE_NAME=room7200 SERVICE_TYPE=_http._tcp.local. SERVICE_IP=192.168.137.1 -SERVICE_PORT=5000 \ No newline at end of file +SERVICE_PORT=5000 +DEFAULT_ENDPOINTS=true +LOG_LEVEL=WARN \ No newline at end of file diff --git a/src/api.py b/src/api.py index 45f141c..9624ac2 100644 --- a/src/api.py +++ b/src/api.py @@ -1,33 +1,54 @@ -import socket - from flask import Blueprint, abort, jsonify, request -from config import API_HOST -from db import get_endpoint_by_id, get_endpoints_from_db, update_endpoint_in_db +from db import ( + add_endpoint_to_db, + delete_endpoint_from_db, + get_endpoint_by_id, + get_endpoint_by_url, + get_endpoints_from_db, + update_endpoint_in_db, +) +from utils import is_valid_url api = Blueprint("api", __name__) -@api.route("/api/ip", methods=["GET"]) -def get_ip(): - try: - ip_address = socket.gethostbyname(API_HOST) - return jsonify({"ip": ip_address}), 200 - except socket.gaierror: - return jsonify({"error": f"Could not resolve IP address for {API_HOST}"}), 404 - - @api.route("/api/endpoints", methods=["GET"]) def get_endpoints(): endpoints = get_endpoints_from_db() - return jsonify(endpoints) + return jsonify(endpoints), 200 + + +@api.route("/api/endpoints", methods=["POST"]) +def create_endpoint(): + data = request.get_json() + new_url = data.get("url") + + if new_url is None or not is_valid_url(new_url): + return jsonify({"status": "error", "message": "Invalid or missing URL"}), 400 + + existing_endpoint = get_endpoint_by_url(new_url) + if existing_endpoint: + return ( + jsonify( + { + "status": "error", + "message": "Endpoint already exists", + "id": existing_endpoint["id"], + } + ), + 409, + ) + + endpoint_id = add_endpoint_to_db(new_url) + return jsonify({"id": endpoint_id, "url": new_url}), 201 @api.route("/api/endpoints/", methods=["GET"]) def get_endpoint(endpoint_id): endpoint = get_endpoint_by_id(endpoint_id) if endpoint: - return jsonify(endpoint) + return jsonify(endpoint), 200 else: abort(404) @@ -37,16 +58,25 @@ def update_endpoint(endpoint_id): data = request.get_json() new_url = data.get("url") - if new_url: - update_endpoint_in_db(endpoint_id, new_url) - return ( - jsonify( - { - "status": "success", - "message": f"Endpoint {endpoint_id} updated to {new_url}", - } - ), - 200, - ) + if not new_url or not is_valid_url(new_url): + return jsonify({"status": "error", "message": "Invalid or missing URL"}), 400 + + updated = update_endpoint_in_db(endpoint_id, new_url) + if updated: + return "", 204 else: - return jsonify({"status": "error", "message": "No new endpoint provided."}), 400 + return jsonify({"status": "error", "message": "Endpoint not found"}), 404 + + +@api.route("/api/endpoints/", methods=["DELETE"]) +def delete_endpoint(endpoint_id): + deleted = delete_endpoint_from_db(endpoint_id) + if deleted: + return "", 204 + else: + return jsonify({"status": "error", "message": "Endpoint not found"}), 404 + + +@api.route("/api/health", methods=["GET"]) +def health(): + return jsonify({"status": "ok"}) diff --git a/src/app.py b/src/app.py index d38e7a1..8a47d95 100644 --- a/src/app.py +++ b/src/app.py @@ -1,3 +1,4 @@ +import logging import signal import sys @@ -5,7 +6,7 @@ from flask import Flask, render_template from waitress import serve from api import api -from config import API_HOST, DATABASE_URI +from config import API_HOST, DATABASE_URI, LOG_LEVEL, SERVICE_IP, SERVICE_PORT from db import get_endpoints_from_db, init_db from register import register_service, unregister_service @@ -32,9 +33,11 @@ def homepage(): if __name__ == "__main__": + logging.basicConfig(level=logging.getLevelName(LOG_LEVEL)) + register_service() try: - serve(app, host="0.0.0.0", port=5000) + serve(app, host=SERVICE_IP, port=SERVICE_PORT) finally: unregister_service() diff --git a/src/config.py b/src/config.py index 7dd78c6..55255d9 100644 --- a/src/config.py +++ b/src/config.py @@ -10,3 +10,5 @@ SERVICE_NAME = os.getenv("SERVICE_NAME") SERVICE_TYPE = os.getenv("SERVICE_TYPE") SERVICE_IP = os.getenv("SERVICE_IP") SERVICE_PORT = os.getenv("SERVICE_PORT") +DEFAULT_ENDPOINTS = os.getenv("DEFAULT_ENDPOINTS") +LOG_LEVEL = os.getenv("LOG_LEVEL") diff --git a/src/db.py b/src/db.py index 4760002..7be0e05 100644 --- a/src/db.py +++ b/src/db.py @@ -1,6 +1,8 @@ +import logging + from flask_sqlalchemy import SQLAlchemy -from config import API_HOST +from config import API_HOST, DEFAULT_ENDPOINTS db = SQLAlchemy() @@ -14,7 +16,8 @@ def init_db(app): db.init_app(app) with app.app_context(): db.create_all() - init_default_endpoints() + if DEFAULT_ENDPOINTS: + init_default_endpoints() def init_default_endpoints(): @@ -28,6 +31,7 @@ def init_default_endpoints(): updated = False for endpoint in default_endpoints: if endpoint.id not in existing_endpoints: + logging.info(f"Adding default endpoint {endpoint.url}") db.session.add(endpoint) updated = True @@ -40,13 +44,49 @@ def get_endpoints_from_db(): return [{"id": endpoint.id, "url": endpoint.url} for endpoint in endpoints] +def add_endpoint_to_db(url): + new_endpoint = Endpoint(url=url) + db.session.add(new_endpoint) + db.session.commit() + logging.info(f"Added new endpoint {url}") + return new_endpoint.id + + +# def update_endpoint_in_db(endpoint_id, new_url): +# with db.session.begin(): +# endpoint = Endpoint.query.get(endpoint_id) +# if endpoint: +# endpoint.url = new_url +# db.session.add(endpoint) +# return True +# else: +# db.session.add(Endpoint(id=endpoint_id, url=new_url)) +# return False + + def update_endpoint_in_db(endpoint_id, new_url): - with db.session.begin(): - endpoint = Endpoint.query.get(endpoint_id) - if endpoint: - endpoint.url = new_url - else: - db.session.add(Endpoint(id=endpoint_id, url=new_url)) + endpoint = Endpoint.query.get(endpoint_id) + if endpoint: + endpoint.url = new_url + db.session.commit() + logging.info(f"Updated endpoint {endpoint_id} to {new_url}") + return True + return False + + +def get_endpoint_by_url(url): + endpoint = Endpoint.query.filter_by(url=url).first() + return {"id": endpoint.id, "url": endpoint.url} if endpoint else None + + +def delete_endpoint_from_db(endpoint_id): + endpoint = Endpoint.query.get(endpoint_id) + if endpoint: + db.session.delete(endpoint) + db.session.commit() + logging.info(f"Deleted endpoint {endpoint_id}") + return True + return False def get_endpoint_by_id(endpoint_id): diff --git a/src/register.py b/src/register.py index 6d9b093..fed6e70 100644 --- a/src/register.py +++ b/src/register.py @@ -1,3 +1,5 @@ +import logging + from zeroconf import ServiceInfo, Zeroconf from config import SERVICE_IP, SERVICE_NAME, SERVICE_PORT, SERVICE_TYPE @@ -15,11 +17,25 @@ info = ServiceInfo( def register_service(): - zeroconf.register_service(info) - print(f"Service {SERVICE_NAME}.{SERVICE_TYPE} registered") + try: + zeroconf.register_service(info) + logging.info(f"Service {SERVICE_NAME}.{SERVICE_TYPE} registered") + logging.info( + f"Service is available at http://{SERVICE_NAME}.local:{SERVICE_PORT}" + ) + except Exception: + logging.error(f"Failed to register service {SERVICE_NAME}.{SERVICE_TYPE}") + logging.error( + f"Please add {SERVICE_NAME}.local to /etc/hosts or to your DNS server" + ) + pass def unregister_service(): - zeroconf.unregister_service(info) - print(f"Service {SERVICE_NAME}.{SERVICE_TYPE} unregistered") - zeroconf.close() + try: + zeroconf.unregister_service(info) + logging.info(f"Service {SERVICE_NAME}.{SERVICE_TYPE} unregistered") + zeroconf.close() + except Exception: + logging.error(f"Failed to unregister service {SERVICE_NAME}.{SERVICE_TYPE}") + pass diff --git a/src/utils.py b/src/utils.py new file mode 100644 index 0000000..e6740c0 --- /dev/null +++ b/src/utils.py @@ -0,0 +1,15 @@ +import re + + +def is_valid_url(url): + url_regex = re.compile( + r"^(https?):\/\/" + r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|" + r"localhost|" + r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|" + r"\[?[A-F0-9]*:[A-F0-9:]+\]?)" + r"(?::\d+)?" + r"(?:\/[^\s]*)?$", + re.IGNORECASE, + ) + return re.match(url_regex, url) is not None