diff --git a/docker-compose.yml b/docker-compose.yml index 6d661e5..70ae366 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,6 +20,7 @@ services: - redis volumes: - ./src/app:/code/app + - ./src/.env:/code/.env worker: build: @@ -33,6 +34,7 @@ services: - redis volumes: - ./src/app:/code/app + - ./src/.env:/code/.env db: image: postgres:13 diff --git a/pyproject.toml b/pyproject.toml index 4e368f2..8047dcf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,27 +10,27 @@ packages = [{ include = "src" }] [tool.poetry.dependencies] python = "^3.11" python-dotenv = "^1.0.0" -pydantic = { extras = ["email"], version = "^2.4.1" } +pydantic = { extras = ["email"], version = "^2.6.1" } fastapi = "^0.109.1" -uvicorn = "^0.23.2" -uvloop = "^0.17.0" -httptools = "^0.6.0" +uvicorn = "^0.27.0" +uvloop = "^0.19.0" +httptools = "^0.6.1" uuid = "^1.30" -alembic = "^1.12.0" -asyncpg = "^0.28.0" +alembic = "^1.13.1" +asyncpg = "^0.29.0" SQLAlchemy-Utils = "^0.41.1" python-jose = "^3.3.0" -SQLAlchemy = "^2.0.21" +SQLAlchemy = "^2.0.25" pytest = "^7.4.2" -python-multipart = "^0.0.7" +python-multipart = "^0.0.9" greenlet = "^2.0.2" -httpx = "^0.25.0" +httpx = "^0.26.0" pydantic-settings = "^2.0.3" redis = "^5.0.1" arq = "^0.25.0" gunicorn = "^21.2.0" bcrypt = "^4.1.1" -fastcrud = "^0.4.0" +fastcrud = "^0.5.0" [build-system] diff --git a/src/app/core/setup.py b/src/app/core/setup.py index c2ceadc..c126cd3 100644 --- a/src/app/core/setup.py +++ b/src/app/core/setup.py @@ -1,3 +1,5 @@ +from collections.abc import AsyncGenerator, Callable +from contextlib import _AsyncGeneratorContextManager, asynccontextmanager from typing import Any import anyio @@ -68,6 +70,50 @@ async def set_threadpool_tokens(number_of_tokens: int = 100) -> None: limiter.total_tokens = number_of_tokens +def lifespan_factory( + settings: ( + DatabaseSettings + | RedisCacheSettings + | AppSettings + | ClientSideCacheSettings + | RedisQueueSettings + | RedisRateLimiterSettings + | EnvironmentSettings + ), + create_tables_on_start: bool = True, +) -> Callable[[FastAPI], _AsyncGeneratorContextManager[Any]]: + """Factory to create a lifespan async context manager for a FastAPI app.""" + + @asynccontextmanager + async def lifespan(app: FastAPI) -> AsyncGenerator: + await set_threadpool_tokens() + + if isinstance(settings, DatabaseSettings) and create_tables_on_start: + await create_tables() + + if isinstance(settings, RedisCacheSettings): + await create_redis_cache_pool() + + if isinstance(settings, RedisQueueSettings): + await create_redis_queue_pool() + + if isinstance(settings, RedisRateLimiterSettings): + await create_redis_rate_limit_pool() + + yield + + if isinstance(settings, RedisCacheSettings): + await close_redis_cache_pool() + + if isinstance(settings, RedisQueueSettings): + await close_redis_queue_pool() + + if isinstance(settings, RedisRateLimiterSettings): + await close_redis_rate_limit_pool() + + return lifespan + + # -------------- application -------------- def create_application( router: APIRouter, @@ -136,30 +182,13 @@ def create_application( if isinstance(settings, EnvironmentSettings): kwargs.update({"docs_url": None, "redoc_url": None, "openapi_url": None}) - application = FastAPI(**kwargs) - - # --- application created --- - application.include_router(router) - application.add_event_handler("startup", set_threadpool_tokens) + lifespan = lifespan_factory(settings, create_tables_on_start=create_tables_on_start) - if isinstance(settings, DatabaseSettings) and create_tables_on_start: - application.add_event_handler("startup", create_tables) - - if isinstance(settings, RedisCacheSettings): - application.add_event_handler("startup", create_redis_cache_pool) - application.add_event_handler("shutdown", close_redis_cache_pool) + application = FastAPI(lifespan=lifespan, **kwargs) if isinstance(settings, ClientSideCacheSettings): application.add_middleware(ClientCacheMiddleware, max_age=settings.CLIENT_CACHE_MAX_AGE) - if isinstance(settings, RedisQueueSettings): - application.add_event_handler("startup", create_redis_queue_pool) - application.add_event_handler("shutdown", close_redis_queue_pool) - - if isinstance(settings, RedisRateLimiterSettings): - application.add_event_handler("startup", create_redis_rate_limit_pool) - application.add_event_handler("shutdown", close_redis_rate_limit_pool) - if isinstance(settings, EnvironmentSettings): if settings.ENVIRONMENT != EnvironmentOption.PRODUCTION: docs_router = APIRouter() @@ -181,4 +210,4 @@ async def openapi() -> dict[str, Any]: application.include_router(docs_router) - return application + return application