From 3c0222c8261106adc8afa4ba73d5b2ef1ea16156 Mon Sep 17 00:00:00 2001 From: Tupui <23188539+tupui@users.noreply.github.com> Date: Mon, 19 Aug 2024 18:10:21 +0200 Subject: [PATCH] Start backend for events --- tansu/pyproject.toml | 8 ++- .../tansu/events/{models.py => api_models.py} | 6 ++ tansu/src/tansu/events/app.py | 69 +++++++++++++++++++ tansu/src/tansu/events/consume.py | 8 +-- tansu/src/tansu/events/log.py | 2 +- tansu/src/tansu/events/main.py | 8 +++ tansu/src/tansu/events/routers/__init__.py | 0 tansu/src/tansu/events/routers/events.py | 27 ++++++++ tansu/src/tansu/events/routers/system.py | 11 +++ 9 files changed, 133 insertions(+), 6 deletions(-) rename tansu/src/tansu/events/{models.py => api_models.py} (89%) create mode 100644 tansu/src/tansu/events/app.py create mode 100644 tansu/src/tansu/events/main.py create mode 100644 tansu/src/tansu/events/routers/__init__.py create mode 100644 tansu/src/tansu/events/routers/events.py create mode 100644 tansu/src/tansu/events/routers/system.py diff --git a/tansu/pyproject.toml b/tansu/pyproject.toml index 254c0b6..d186492 100644 --- a/tansu/pyproject.toml +++ b/tansu/pyproject.toml @@ -49,10 +49,16 @@ test = [ "pytest", "pytest-asyncio", "locust", + "respx", +] + +backend = [ + "fastapi", + "uvicorn[standard]", ] dev = [ - "tansu[test]", + "tansu[test,backend]", "pre-commit", "hatch", "ruff", diff --git a/tansu/src/tansu/events/models.py b/tansu/src/tansu/events/api_models.py similarity index 89% rename from tansu/src/tansu/events/models.py rename to tansu/src/tansu/events/api_models.py index 7b2b6b8..c35c8ce 100644 --- a/tansu/src/tansu/events/models.py +++ b/tansu/src/tansu/events/api_models.py @@ -38,3 +38,9 @@ class Event(BaseModel): value: SCValNative_ model_config = dict(arbitrary_types_allowed=True) + + +class EventRequest(BaseModel): + project_key: str + action: str + limit: int = 1000 diff --git a/tansu/src/tansu/events/app.py b/tansu/src/tansu/events/app.py new file mode 100644 index 0000000..1afecfa --- /dev/null +++ b/tansu/src/tansu/events/app.py @@ -0,0 +1,69 @@ +"""FastAPI app. + +Connect all routers to the app and add error handling. +""" + +from fastapi import FastAPI, Request +from fastapi.responses import JSONResponse +from fastapi.middleware import Middleware +from starlette.middleware.cors import CORSMiddleware + +from tansu.events.log import logger +from tansu.events.routers import system, events + +ORIGINS = [ + "https://testnet.tansu.dev", + "https://app.tansu.dev", +] + + +async def unhandled_exception_handler(request: Request, exc: Exception) -> JSONResponse: + """Log all unhandled exceptions.""" + message = f"Internal API error: {exc}" + logger.error(message) + return JSONResponse( + status_code=500, + content={"message": "Internal API error. The error was reported to our team."}, + ) + + +def create_app(debug: bool = False): + app = FastAPI( + title="Tansu - events backend", + debug=debug, + description="", + version="1.0.0", + middleware=[ + Middleware( + CORSMiddleware, + **{ + "allow_origins": ORIGINS, + "allow_credentials": True, + "allow_methods": ["GET", "POST", "DELETE", "OPTIONS"], + "allow_headers": ["*"], + }, + ), + ], + ) + + app.add_exception_handler(Exception, unhandled_exception_handler) + + app.include_router(system.router, tags=["system"]) + app.include_router(events.router, tags=["events"]) + + logger.info("Tansu RPC is running...") + + return app + + +if __name__ == "__main__": + import uvicorn + + uvicorn.run( + "tansu.event.main:app", + host="127.0.0.1", + reload=True, + port=8080, + access_log=True, + log_level="debug", + ) diff --git a/tansu/src/tansu/events/consume.py b/tansu/src/tansu/events/consume.py index ec54cac..96d31e5 100644 --- a/tansu/src/tansu/events/consume.py +++ b/tansu/src/tansu/events/consume.py @@ -1,16 +1,16 @@ import sqlalchemy -from tansu.events import models +from tansu.events import api_models from tansu.events.database import db_models from tansu.events.log import logger @sqlalchemy.event.listens_for(db_models.Event, "after_insert") def event_handler(mapper, connection, target: db_models.Event): - event = models.Event( + event = api_models.Event( project_key=target.project_key, action=target.action, value=target.value ) - logger.info( - f"Event listener: {event.project_key} :: {event.action} :: {event.value}" + logger.debug( + f"Event listener: {event.project_key} :: {event.action} :: {event.value} :: {event.ledger}" ) diff --git a/tansu/src/tansu/events/log.py b/tansu/src/tansu/events/log.py index 54b5e9b..f5d33ba 100644 --- a/tansu/src/tansu/events/log.py +++ b/tansu/src/tansu/events/log.py @@ -1,3 +1,3 @@ import logging -logger = logging.getLogger("soroban-versioning-events-events") +logger = logging.getLogger("tansu-events") diff --git a/tansu/src/tansu/events/main.py b/tansu/src/tansu/events/main.py new file mode 100644 index 0000000..67eda05 --- /dev/null +++ b/tansu/src/tansu/events/main.py @@ -0,0 +1,8 @@ +"""Main entry point for the FastAPI server.""" + +import os +from tansu.events.app import create_app + +env = os.getenv("ENV", "production") +debug = True if env == "testing" else False +app = create_app(debug=debug) diff --git a/tansu/src/tansu/events/routers/__init__.py b/tansu/src/tansu/events/routers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tansu/src/tansu/events/routers/events.py b/tansu/src/tansu/events/routers/events.py new file mode 100644 index 0000000..f053386 --- /dev/null +++ b/tansu/src/tansu/events/routers/events.py @@ -0,0 +1,27 @@ +from fastapi import APIRouter + +from tansu.events.database import SessionFactory, db_models +from tansu.events import api_models + +router = APIRouter() + + +@router.post("/events") +async def events(request: api_models.EventRequest) -> list[api_models.Event | None]: + async with SessionFactory() as session: + events_ = ( + session.query(db_models.Event) + .where(db_models.Event.project_key == request.project_key) + .where(db_models.Event.action == request.action) + .limit(request.limit) + .all() + ) + + result = [ + api_models.Event( + project_key=event_.project_key, action=event_.action, value=event_.value + ) + for event_ in events_ + ] + + return result diff --git a/tansu/src/tansu/events/routers/system.py b/tansu/src/tansu/events/routers/system.py new file mode 100644 index 0000000..7471e0c --- /dev/null +++ b/tansu/src/tansu/events/routers/system.py @@ -0,0 +1,11 @@ +from fastapi import APIRouter +from fastapi.responses import PlainTextResponse + +router = APIRouter() + + +@router.get("/favicon.ico") +@router.get("/health") +@router.get("/") +async def health_check() -> PlainTextResponse: + return PlainTextResponse("OK")