File size: 1,936 Bytes
4c83ab6
 
 
 
0a367ef
 
4c83ab6
 
b96346f
0a367ef
 
 
 
4c83ab6
 
 
 
 
 
 
 
 
 
b96346f
 
 
 
 
 
 
 
 
4c83ab6
 
 
 
 
 
 
 
b96346f
4c83ab6
 
 
 
 
 
 
 
 
0a367ef
4c83ab6
 
 
0a367ef
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import logging
from contextlib import asynccontextmanager
from typing import AsyncGenerator

from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse

from app.config import settings
from app.database import init_db
from app.lib.graph.auth import GraphAuthProvider
from app.lib.graph.client import GraphClient
from app.routes.health import router as health_router
from app.routes.webhook import router as webhook_router

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(name)s: %(message)s",
)
logger = logging.getLogger(__name__)


@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator:
    """Minimal lifespan — sets up the DB and Graph client only.

    Subscription bootstrap and APScheduler jobs live in the standalone
    `app.scheduler_main` process so that the FastAPI server can run with
    multiple uvicorn workers without duplicating cron work or racing on
    subscription creation. The webhook handler reads the active
    subscription's clientState from the DB on each request — it does not
    need a live subscription_id on app.state.
    """
    # --- Startup ---
    await init_db(settings.database_path)

    auth = GraphAuthProvider(settings)
    client = GraphClient(auth)
    app.state.client = client
    app.state.settings = settings

    logger.info("FastAPI ready — webhook receiver online")

    yield

    # --- Shutdown ---
    await client.aclose()
    logger.info("Shutdown complete")


app = FastAPI(title="RCMEmail", version="1.0.0", lifespan=lifespan)
app.include_router(health_router)
app.include_router(webhook_router)


@app.exception_handler(Exception)
async def _global_exception_handler(request: Request, exc: Exception) -> JSONResponse:
    logger.exception("Unhandled exception for %s %s", request.method, request.url.path)
    return JSONResponse(status_code=500, content={"detail": "Internal server error"})