Files
trading-journal/backend/app.py

133 lines
4.9 KiB
Python
Raw Normal View History

from __future__ import annotations
2025-09-22 14:33:32 +02:00
import asyncio
import logging
2025-09-22 14:33:32 +02:00
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
from datetime import datetime, timezone
2025-09-11 18:24:36 +00:00
from fastapi import FastAPI, HTTPException, Request, status
from fastapi.responses import JSONResponse
2025-09-11 18:24:36 +00:00
2025-09-22 14:33:32 +02:00
import settings
2025-09-22 17:35:10 +02:00
from trading_journal import db, service
from trading_journal.db import Database
2025-09-22 23:07:28 +02:00
from trading_journal.dto import ExchangesBase, SessionsBase, SessionsCreate, UserCreate, UserLogin, UserRead
2025-09-11 18:24:36 +00:00
2025-09-22 14:33:32 +02:00
_db = db.create_database(settings.settings.database_url)
2025-09-11 18:24:36 +00:00
logging.basicConfig(
level=logging.WARNING,
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
)
logger = logging.getLogger(__name__)
2025-09-11 18:24:36 +00:00
2025-09-22 14:33:32 +02:00
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: # noqa: ARG001
await asyncio.to_thread(_db.init_db)
try:
yield
finally:
await asyncio.to_thread(_db.dispose)
2025-09-11 18:24:36 +00:00
2025-09-22 14:33:32 +02:00
app = FastAPI(lifespan=lifespan)
2025-09-22 17:35:10 +02:00
app.add_middleware(service.AuthMiddleWare)
app.state.db_factory = _db
2025-09-11 18:24:36 +00:00
2025-09-22 17:35:10 +02:00
@app.get(f"{settings.settings.api_base}/status")
2025-09-22 14:33:32 +02:00
async def get_status() -> dict[str, str]:
return {"status": "ok"}
2025-09-22 17:35:10 +02:00
@app.post(f"{settings.settings.api_base}/register")
async def register_user(request: Request, user_in: UserCreate) -> UserRead:
db_factory: Database = request.app.state.db_factory
def sync_work() -> UserRead:
with db_factory.get_session_ctx_manager() as db:
return service.register_user_service(db, user_in)
try:
user = await asyncio.to_thread(sync_work)
return JSONResponse(status_code=status.HTTP_201_CREATED, content=user.model_dump())
2025-09-22 17:35:10 +02:00
except service.UserAlreadyExistsError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) from e
2025-09-22 17:35:10 +02:00
except Exception as e:
logger.exception("Failed to register user: \n")
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Internal server error") from e
@app.post(f"{settings.settings.api_base}/login")
async def login(request: Request, user_in: UserLogin) -> SessionsBase:
db_factory: Database = request.app.state.db_factory
def sync_work() -> tuple[SessionsCreate, str] | None:
with db_factory.get_session_ctx_manager() as db:
return service.authenticate_user_service(db, user_in)
try:
result = await asyncio.to_thread(sync_work)
if result is None:
return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
content={"detail": "Invalid username or password, or user doesn't exist"},
)
session, token = result
session_return = SessionsBase(user_id=session.user_id)
response = JSONResponse(status_code=status.HTTP_200_OK, content=session_return.model_dump())
expires_sec = int((session.expires_at.replace(tzinfo=timezone.utc) - datetime.now(timezone.utc)).total_seconds())
response.set_cookie(
key="session_token",
value=token,
httponly=True,
secure=True,
samesite="lax",
max_age=expires_sec,
path="/",
)
except Exception as e:
logger.exception("Failed to login user: \n")
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Internal server error") from e
else:
return response
# Exchange
2025-09-22 23:07:28 +02:00
@app.post(f"{settings.settings.api_base}/exchanges")
async def create_exchange(request: Request, exchange_data: ExchangesBase) -> dict:
db_factory: Database = request.app.state.db_factory
def sync_work() -> ExchangesBase:
with db_factory.get_session_ctx_manager() as db:
return service.create_exchange_service(db, request.state.user_id, exchange_data.name, exchange_data.notes)
try:
exchange = await asyncio.to_thread(sync_work)
return JSONResponse(status_code=status.HTTP_201_CREATED, content=exchange.model_dump())
except service.ExchangeAlreadyExistsError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) from e
except Exception as e:
logger.exception("Failed to create exchange: \n")
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Internal server error") from e
2025-09-22 17:35:10 +02:00
2025-09-22 23:07:28 +02:00
@app.get(f"{settings.settings.api_base}/exchanges")
async def get_exchanges(request: Request) -> list[ExchangesBase]:
db_factory: Database = request.app.state.db_factory
2025-09-22 23:07:28 +02:00
def sync_work() -> list[ExchangesBase]:
with db_factory.get_session_ctx_manager() as db:
return service.get_exchanges_by_user_service(db, request.state.user_id)
try:
return await asyncio.to_thread(sync_work)
except Exception as e:
logger.exception("Failed to get exchanges: \n")
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Internal server error") from e
# Trade