-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.py
More file actions
112 lines (87 loc) · 3.6 KB
/
app.py
File metadata and controls
112 lines (87 loc) · 3.6 KB
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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
from boot import app_boot
from src.config import Config
from src.utils import now
from fastapi import FastAPI, Header, HTTPException, Depends, Request, Response
from fastapi.responses import JSONResponse
from fastapi.middleware.cors import CORSMiddleware
from contextlib import asynccontextmanager
from src.utils.performance import performance_tracker
import logging
from typing import AsyncGenerator
from src.models.api_request import ApiRequest
from slowapi import _rate_limit_exceeded_handler
from slowapi.errors import RateLimitExceeded
from src.utils.rate_limiter import limiter
logger = logging.getLogger(__name__)
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
"""
Lifespan context manager for FastAPI application.
Handles startup and shutdown events.
"""
# Startup
await app_boot()
logger.info(f"Starting API in {Config.APP_MODE} mode")
yield # App is running
# Shutdown
# await processor.cleanup()
logger.info("Shutting down API")
def get_allowed_origins():
origins_str = Config.ALLOWED_ORIGINS
if not origins_str: # Handle empty/None case
return ["http://localhost:3000"] if Config.APP_MODE != "production" else []
origins = [origin.strip() for origin in origins_str.split(",")]
return [origin for origin in origins if origin]
allowed_origins = get_allowed_origins()
if Config.APP_MODE == "production" and not allowed_origins:
raise ValueError("ALLOWED_ORIGINS must be set in production")
app = FastAPI(title=f"{Config.APP_NAME} API", lifespan=lifespan)
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
app.add_middleware(
CORSMiddleware,
allow_origins=allowed_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
async def verify_api_key(x_api_key: str | None=Header(None)):
if Config.HTTP_SECRET is None:
raise HTTPException(status_code=500, detail="API key not configured")
if x_api_key is None:
raise HTTPException(status_code=401, detail="X-API-KEY header is missing")
# Use secure comparison to prevent timing attacks
import secrets
if not secrets.compare_digest(x_api_key, Config.HTTP_SECRET):
raise HTTPException(status_code=401, detail="Invalid API key")
return x_api_key
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc):
logger.error(f"Unhandled exception: {exc}", exc_info=True)
return JSONResponse(status_code=500, content={"error": "Internal server error"})
@app.get("/health")
@limiter.limit("2/minute")
async def health(request: Request, response: Response):
return JSONResponse(content={
"status": "healthy",
"mode": Config.APP_MODE,
"timestamp": now().isoformat(),
"boot_time": performance_tracker.get_boot_time()
}, status_code=200)
@app.post("/test")
@limiter.limit("5/minute")
async def test_fn(request: ApiRequest, response: Response, api_key: str=Depends(verify_api_key)):
try:
# Simulate some processing logic
response = {
"success": True,
"result": {
"message": "Test function executed successfully",
"org_id": request.org_id
}
}
if not response["success"]:
return JSONResponse(content={"error": response["error"]}, status_code=400)
return JSONResponse(content=response["result"], status_code=200)
except Exception as e:
return JSONResponse(content={"error": str(e)}, status_code=400)