Coverage for polar/app.py: 82%
112 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-12-05 15:52 +0000
« prev ^ index » next coverage.py v7.10.6, created at 2025-12-05 15:52 +0000
1import contextlib 1b
2from collections.abc import AsyncIterator 1b
3from typing import TypedDict 1b
5import structlog 1b
6from fastapi import FastAPI 1b
7from fastapi.routing import APIRoute 1b
9from polar import worker # noqa 1b
10from polar.api import router 1b
11from polar.auth.middlewares import AuthSubjectMiddleware 1b
12from polar.backoffice import app as backoffice_app 1b
13from polar.checkout import ip_geolocation 1b
14from polar.config import settings 1b
15from polar.exception_handlers import add_exception_handlers 1b
16from polar.health.endpoints import router as health_router 1b
17from polar.kit.cors import CORSConfig, CORSMatcherMiddleware, Scope 1b
18from polar.kit.db.postgres import ( 1b
19 AsyncEngine,
20 AsyncSessionMaker,
21 Engine,
22 SyncSessionMaker,
23 create_async_sessionmaker,
24 create_sync_sessionmaker,
25)
26from polar.logfire import ( 1b
27 configure_logfire,
28 instrument_fastapi,
29 instrument_httpx,
30 instrument_sqlalchemy,
31)
32from polar.logging import Logger 1b
33from polar.logging import configure as configure_logging 1b
34from polar.middlewares import ( 1b
35 FlushEnqueuedWorkerJobsMiddleware,
36 LogCorrelationIdMiddleware,
37 PathRewriteMiddleware,
38 SandboxResponseHeaderMiddleware,
39)
40from polar.oauth2.endpoints.well_known import router as well_known_router 1b
41from polar.oauth2.exception_handlers import OAuth2Error, oauth2_error_exception_handler 1b
42from polar.openapi import OPENAPI_PARAMETERS, APITag, set_openapi_generator 1b
43from polar.postgres import ( 1b
44 AsyncSessionMiddleware,
45 create_async_engine,
46 create_async_read_engine,
47 create_sync_engine,
48)
49from polar.posthog import configure_posthog 1b
50from polar.redis import Redis, create_redis 1b
51from polar.sentry import configure_sentry 1b
52from polar.webhook.webhooks import document_webhooks 1b
54from . import rate_limit 1b
56log: Logger = structlog.get_logger() 1b
59def configure_cors(app: FastAPI) -> None: 1b
60 configs: list[CORSConfig] = []
62 # Polar frontend CORS configuration
63 if settings.CORS_ORIGINS: 63 ↛ 78line 63 didn't jump to line 78 because the condition on line 63 was always true
65 def polar_frontend_matcher(origin: str, scope: Scope) -> bool:
66 return origin in settings.CORS_ORIGINS
68 polar_frontend_config = CORSConfig(
69 polar_frontend_matcher,
70 allow_origins=[str(origin) for origin in settings.CORS_ORIGINS],
71 allow_credentials=True, # Cookies are allowed, but only there!
72 allow_methods=["*"],
73 allow_headers=["*"],
74 )
75 configs.append(polar_frontend_config)
77 # External API calls CORS configuration
78 api_config = CORSConfig(
79 lambda origin, scope: True,
80 allow_origins=["*"],
81 allow_credentials=False, # No cookies allowed
82 allow_methods=["*"],
83 allow_headers=["Authorization"], # Allow Authorization header to pass tokens
84 )
85 configs.append(api_config)
87 app.add_middleware(CORSMatcherMiddleware, configs=configs)
90def generate_unique_openapi_id(route: APIRoute) -> str: 1b
91 parts = [str(tag) for tag in route.tags if tag not in APITag] + [route.name]
92 return ":".join(parts)
95class State(TypedDict): 1b
96 async_engine: AsyncEngine 1b
97 async_sessionmaker: AsyncSessionMaker 1b
98 async_read_engine: AsyncEngine 1b
99 async_read_sessionmaker: AsyncSessionMaker 1b
100 sync_engine: Engine 1b
101 sync_sessionmaker: SyncSessionMaker 1b
103 redis: Redis 1b
104 ip_geolocation_client: ip_geolocation.IPGeolocationClient | None 1b
107@contextlib.asynccontextmanager 1b
108async def lifespan(app: FastAPI) -> AsyncIterator[State]: 1b
109 log.info("Starting Polar API")
111 async_engine = async_read_engine = create_async_engine("app")
112 async_sessionmaker = async_read_sessionmaker = create_async_sessionmaker(
113 async_engine
114 )
115 instrument_engines = [async_engine.sync_engine]
117 if settings.is_read_replica_configured(): 117 ↛ 122line 117 didn't jump to line 122 because the condition on line 117 was always true
118 async_read_engine = create_async_read_engine("app")
119 async_read_sessionmaker = create_async_sessionmaker(async_read_engine)
120 instrument_engines.append(async_read_engine.sync_engine)
122 sync_engine = create_sync_engine("app")
123 sync_sessionmaker = create_sync_sessionmaker(sync_engine)
124 instrument_engines.append(sync_engine)
125 instrument_sqlalchemy(instrument_engines)
127 redis = create_redis("app")
129 try:
130 ip_geolocation_client = None # Disabled for testing
131 except FileNotFoundError:
132 log.info(
133 "IP geolocation database not found. "
134 "Checkout won't automatically geolocate IPs."
135 )
136 ip_geolocation_client = None
138 log.info("Polar API started")
140 yield {
141 "async_engine": async_engine,
142 "async_sessionmaker": async_sessionmaker,
143 "async_read_engine": async_read_engine,
144 "async_read_sessionmaker": async_read_sessionmaker,
145 "sync_engine": sync_engine,
146 "sync_sessionmaker": sync_sessionmaker,
147 "redis": redis,
148 "ip_geolocation_client": ip_geolocation_client,
149 }
151 await redis.close(True)
152 await async_engine.dispose()
153 if async_read_engine is not async_engine:
154 await async_read_engine.dispose()
155 sync_engine.dispose()
156 if ip_geolocation_client is not None:
157 ip_geolocation_client.close()
159 log.info("Polar API stopped")
162def create_app() -> FastAPI: 1b
163 app = FastAPI(
164 generate_unique_id_function=generate_unique_openapi_id,
165 lifespan=lifespan,
166 **OPENAPI_PARAMETERS,
167 )
169 if settings.is_sandbox(): 169 ↛ 170line 169 didn't jump to line 170 because the condition on line 169 was never true
170 app.add_middleware(SandboxResponseHeaderMiddleware)
171 if not settings.is_testing(): 171 ↛ 176line 171 didn't jump to line 176 because the condition on line 171 was always true
172 app.add_middleware(rate_limit.get_middleware)
173 app.add_middleware(AuthSubjectMiddleware)
174 app.add_middleware(FlushEnqueuedWorkerJobsMiddleware)
175 app.add_middleware(AsyncSessionMiddleware)
176 app.add_middleware(PathRewriteMiddleware, pattern=r"^/api/v1", replacement="/v1")
177 app.add_middleware(LogCorrelationIdMiddleware)
179 configure_cors(app)
181 add_exception_handlers(app)
182 app.add_exception_handler(OAuth2Error, oauth2_error_exception_handler) # pyright: ignore
184 # /.well-known
185 app.include_router(well_known_router)
187 # /healthz
188 app.include_router(health_router)
190 if settings.BACKOFFICE_HOST is None: 190 ↛ 193line 190 didn't jump to line 193 because the condition on line 190 was always true
191 app.mount("/backoffice", backoffice_app)
192 else:
193 app.host(settings.BACKOFFICE_HOST, backoffice_app)
195 app.include_router(router)
196 document_webhooks(app)
198 return app
201configure_sentry() 1b
202configure_logfire("server") 1ba
203configure_logging(logfire=True)
204configure_posthog()
206app = create_app()
207set_openapi_generator(app)
208instrument_fastapi(app)
209instrument_httpx()