- implement postgres connection pool

- adapt current sqlalchemy based methods to produce raw sql
This commit is contained in:
Jakub Miazek 2024-05-11 16:03:33 +02:00
parent 40984ed7e3
commit 514eea7231
5 changed files with 63 additions and 19 deletions

View File

@ -1,4 +1,5 @@
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status, Request
from fastapi.exceptions import ResponseValidationError
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@ -21,7 +22,6 @@ async def create_multi_stuff(
db_session.add_all(stuff_instances) db_session.add_all(stuff_instances)
await db_session.commit() await db_session.commit()
except SQLAlchemyError as ex: except SQLAlchemyError as ex:
# logger.exception(ex)
raise HTTPException( raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=repr(ex) status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=repr(ex)
) from ex ) from ex
@ -43,10 +43,29 @@ async def create_stuff(
@router.get("/{name}", response_model=StuffResponse) @router.get("/{name}", response_model=StuffResponse)
async def find_stuff( async def find_stuff(
request: Request,
name: str, name: str,
pool: bool = False,
db_session: AsyncSession = Depends(get_db), db_session: AsyncSession = Depends(get_db),
): ):
return await Stuff.find(db_session, name) try:
if not pool:
result = await Stuff.find(db_session, name)
else:
# execute the compiled SQL statement
stmt = await Stuff.find(db_session, name, compile_sql=True)
result = await request.app.postgres_pool.fetchrow(str(stmt))
result = dict(result)
except SQLAlchemyError as ex:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=repr(ex)
) from ex
if not result:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Stuff with name {name} not found.",
)
return result
@router.delete("/{name}") @router.delete("/{name}")

View File

@ -70,5 +70,29 @@ class Settings(BaseSettings):
path=self.POSTGRES_DB, path=self.POSTGRES_DB,
) )
@computed_field
@property
def postgres_url(self) -> PostgresDsn:
"""
This is a computed field that generates a PostgresDsn URL
The URL is built using the MultiHostUrl.build method, which takes the following parameters:
- scheme: The scheme of the URL. In this case, it is "postgres".
- username: The username for the Postgres database, retrieved from the POSTGRES_USER environment variable.
- password: The password for the Postgres database, retrieved from the POSTGRES_PASSWORD environment variable.
- host: The host of the Postgres database, retrieved from the POSTGRES_HOST environment variable.
- path: The path of the Postgres database, retrieved from the POSTGRES_DB environment variable.
Returns:
PostgresDsn: The constructed PostgresDsn URL.
"""
return MultiHostUrl.build(
scheme="postgres",
username=self.POSTGRES_USER,
password=self.POSTGRES_PASSWORD,
host=self.POSTGRES_HOST,
path=self.POSTGRES_DB,
)
settings = Settings() settings = Settings()

View File

@ -1,3 +1,4 @@
import asyncpg
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from fastapi import FastAPI, Depends from fastapi import FastAPI, Depends
@ -7,6 +8,7 @@ from fastapi_cache.backends.redis import RedisBackend
from app.api.nonsense import router as nonsense_router from app.api.nonsense import router as nonsense_router
from app.api.shakespeare import router as shakespeare_router from app.api.shakespeare import router as shakespeare_router
from app.api.stuff import router as stuff_router from app.api.stuff import router as stuff_router
from app.config import settings as global_settings
from app.utils.logging import AppLogger from app.utils.logging import AppLogger
from app.api.user import router as user_router from app.api.user import router as user_router
from app.api.health import router as health_router from app.api.health import router as health_router
@ -21,15 +23,26 @@ async def lifespan(_app: FastAPI):
# Load the redis connection # Load the redis connection
_app.redis = await get_redis() _app.redis = await get_redis()
_postgres_dsn = global_settings.postgres_url.unicode_string()
try: try:
# Initialize the cache with the redis connection # Initialize the cache with the redis connection
redis_cache = await get_cache() redis_cache = await get_cache()
FastAPICache.init(RedisBackend(redis_cache), prefix="fastapi-cache") FastAPICache.init(RedisBackend(redis_cache), prefix="fastapi-cache")
logger.info(FastAPICache.get_cache_status_header()) logger.info(FastAPICache.get_cache_status_header())
# Initialize the postgres connection pool
_app.postgres_pool = await asyncpg.create_pool(
dsn=_postgres_dsn,
min_size=5,
max_size=20,
)
logger.info(f"Postgres pool created: {_app.postgres_pool.get_idle_size()=}")
yield yield
finally: finally:
# close redis connection and release the resources # close redis connection and release the resources
await _app.redis.close() await _app.redis.close()
# close postgres connection pool and release the resources
await _app.postgres_pool.close()
app = FastAPI(title="Stuff And Nonsense API", version="0.6", lifespan=lifespan) app = FastAPI(title="Stuff And Nonsense API", version="0.6", lifespan=lifespan)

View File

@ -24,23 +24,12 @@ class Stuff(Base):
) )
@classmethod @classmethod
async def find(cls, db_session: AsyncSession, name: str): async def find(cls, db_session: AsyncSession, name: str, compile_sql: bool = False):
"""
:param db_session:
:param name:
:return:
"""
stmt = select(cls).options(joinedload(cls.nonsense)).where(cls.name == name) stmt = select(cls).options(joinedload(cls.nonsense)).where(cls.name == name)
if compile_sql:
return stmt.compile(compile_kwargs={"literal_binds": True})
result = await db_session.execute(stmt) result = await db_session.execute(stmt)
instance = result.scalars().first() return result.scalars().first()
if instance is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail={"Not found": f"There is no record for name: {name}"},
)
else:
return instance
class StuffFullOfNonsense(Base): class StuffFullOfNonsense(Base):

View File

@ -1,4 +1,3 @@
import pytest import pytest
from fastapi import status from fastapi import status
from httpx import AsyncClient from httpx import AsyncClient