"""Application entry point: wires slices, cross-cutting request rules, and JSON error handling.""" import logging import psycopg2.errors from flask import Flask, Response, g, jsonify, request from flask.typing import ResponseReturnValue from werkzeug.exceptions import HTTPException import auth import books import db import notes from validation import ApiError log = logging.getLogger(__name__) MUTATING_METHODS = {"POST", "PUT", "PATCH", "DELETE"} def create_app() -> Flask: logging.basicConfig( level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s" ) app = Flask(__name__) app.config["MAX_CONTENT_LENGTH"] = 1024 * 1024 db.init_app(app) auth.init_app(app) app.register_blueprint(books.bp) app.register_blueprint(notes.bp) @app.get("/api/health") @auth.allow_anonymous def health() -> ResponseReturnValue: return {"status": "ok"} @app.before_request def require_json_for_mutations() -> None: if request.method in MUTATING_METHODS and not request.is_json: raise ApiError( 400, "Request body must be JSON with Content-Type: application/json" ) @app.after_request def log_request(response: Response) -> Response: log.info( "%s %s -> %s user=%s", request.method, request.path, response.status_code, g.get("user_id"), ) return response @app.errorhandler(ApiError) def handle_api_error(error: ApiError) -> ResponseReturnValue: body = {"error": error.message} if error.field: body["field"] = error.field return jsonify(body), error.status @app.errorhandler(HTTPException) def handle_http_error(error: HTTPException) -> ResponseReturnValue: return jsonify(error=error.description), error.code or 500 @app.errorhandler(psycopg2.errors.CheckViolation) def handle_check_violation( error: psycopg2.errors.CheckViolation, ) -> ResponseReturnValue: return ( jsonify( error=f"Value breaks data rule '{error.diag.constraint_name}'; correct it and retry" ), 400, ) @app.errorhandler(Exception) def handle_unexpected(_error: Exception) -> ResponseReturnValue: log.exception("Unhandled error on %s %s", request.method, request.path) return jsonify(error="Unexpected server error"), 500 return app if __name__ == "__main__": create_app().run(host="127.0.0.1", port=5000)