Skip to content

Commit 514eea7

Browse files
committed
- implement postgres connection pool
- adapt current sqlalchemy based methods to produce raw sql
1 parent 40984ed commit 514eea7

File tree

5 files changed

+63
-19
lines changed

5 files changed

+63
-19
lines changed

app/api/stuff.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from fastapi import APIRouter, Depends, HTTPException, status
1+
from fastapi import APIRouter, Depends, HTTPException, status, Request
2+
from fastapi.exceptions import ResponseValidationError
23
from sqlalchemy.exc import SQLAlchemyError
34
from sqlalchemy.ext.asyncio import AsyncSession
45

@@ -21,7 +22,6 @@ async def create_multi_stuff(
2122
db_session.add_all(stuff_instances)
2223
await db_session.commit()
2324
except SQLAlchemyError as ex:
24-
# logger.exception(ex)
2525
raise HTTPException(
2626
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=repr(ex)
2727
) from ex
@@ -43,10 +43,29 @@ async def create_stuff(
4343

4444
@router.get("/{name}", response_model=StuffResponse)
4545
async def find_stuff(
46+
request: Request,
4647
name: str,
48+
pool: bool = False,
4749
db_session: AsyncSession = Depends(get_db),
4850
):
49-
return await Stuff.find(db_session, name)
51+
try:
52+
if not pool:
53+
result = await Stuff.find(db_session, name)
54+
else:
55+
# execute the compiled SQL statement
56+
stmt = await Stuff.find(db_session, name, compile_sql=True)
57+
result = await request.app.postgres_pool.fetchrow(str(stmt))
58+
result = dict(result)
59+
except SQLAlchemyError as ex:
60+
raise HTTPException(
61+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=repr(ex)
62+
) from ex
63+
if not result:
64+
raise HTTPException(
65+
status_code=status.HTTP_404_NOT_FOUND,
66+
detail=f"Stuff with name {name} not found.",
67+
)
68+
return result
5069

5170

5271
@router.delete("/{name}")

app/config.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,5 +70,29 @@ def asyncpg_url(self) -> PostgresDsn:
7070
path=self.POSTGRES_DB,
7171
)
7272

73+
@computed_field
74+
@property
75+
def postgres_url(self) -> PostgresDsn:
76+
"""
77+
This is a computed field that generates a PostgresDsn URL
78+
79+
The URL is built using the MultiHostUrl.build method, which takes the following parameters:
80+
- scheme: The scheme of the URL. In this case, it is "postgres".
81+
- username: The username for the Postgres database, retrieved from the POSTGRES_USER environment variable.
82+
- password: The password for the Postgres database, retrieved from the POSTGRES_PASSWORD environment variable.
83+
- host: The host of the Postgres database, retrieved from the POSTGRES_HOST environment variable.
84+
- path: The path of the Postgres database, retrieved from the POSTGRES_DB environment variable.
85+
86+
Returns:
87+
PostgresDsn: The constructed PostgresDsn URL.
88+
"""
89+
return MultiHostUrl.build(
90+
scheme="postgres",
91+
username=self.POSTGRES_USER,
92+
password=self.POSTGRES_PASSWORD,
93+
host=self.POSTGRES_HOST,
94+
path=self.POSTGRES_DB,
95+
)
96+
7397

7498
settings = Settings()

app/main.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import asyncpg
12
from contextlib import asynccontextmanager
23

34
from fastapi import FastAPI, Depends
@@ -7,6 +8,7 @@
78
from app.api.nonsense import router as nonsense_router
89
from app.api.shakespeare import router as shakespeare_router
910
from app.api.stuff import router as stuff_router
11+
from app.config import settings as global_settings
1012
from app.utils.logging import AppLogger
1113
from app.api.user import router as user_router
1214
from app.api.health import router as health_router
@@ -21,15 +23,26 @@ async def lifespan(_app: FastAPI):
2123
# Load the redis connection
2224
_app.redis = await get_redis()
2325

26+
_postgres_dsn = global_settings.postgres_url.unicode_string()
27+
2428
try:
2529
# Initialize the cache with the redis connection
2630
redis_cache = await get_cache()
2731
FastAPICache.init(RedisBackend(redis_cache), prefix="fastapi-cache")
2832
logger.info(FastAPICache.get_cache_status_header())
33+
# Initialize the postgres connection pool
34+
_app.postgres_pool = await asyncpg.create_pool(
35+
dsn=_postgres_dsn,
36+
min_size=5,
37+
max_size=20,
38+
)
39+
logger.info(f"Postgres pool created: {_app.postgres_pool.get_idle_size()=}")
2940
yield
3041
finally:
3142
# close redis connection and release the resources
3243
await _app.redis.close()
44+
# close postgres connection pool and release the resources
45+
await _app.postgres_pool.close()
3346

3447

3548
app = FastAPI(title="Stuff And Nonsense API", version="0.6", lifespan=lifespan)

app/models/stuff.py

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,23 +24,12 @@ class Stuff(Base):
2424
)
2525

2626
@classmethod
27-
async def find(cls, db_session: AsyncSession, name: str):
28-
"""
29-
30-
:param db_session:
31-
:param name:
32-
:return:
33-
"""
27+
async def find(cls, db_session: AsyncSession, name: str, compile_sql: bool = False):
3428
stmt = select(cls).options(joinedload(cls.nonsense)).where(cls.name == name)
29+
if compile_sql:
30+
return stmt.compile(compile_kwargs={"literal_binds": True})
3531
result = await db_session.execute(stmt)
36-
instance = result.scalars().first()
37-
if instance is None:
38-
raise HTTPException(
39-
status_code=status.HTTP_404_NOT_FOUND,
40-
detail={"Not found": f"There is no record for name: {name}"},
41-
)
42-
else:
43-
return instance
32+
return result.scalars().first()
4433

4534

4635
class StuffFullOfNonsense(Base):

tests/api/test_stuff.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
import pytest
32
from fastapi import status
43
from httpx import AsyncClient

0 commit comments

Comments
 (0)