Skip to content

Commit 5ad75cb

Browse files
authored
Merge pull request #153 from grillazz/152-implement-acyncpg-coon-pool-poc
152 implement acyncpg coon pool poc
2 parents 40984ed + c8efc05 commit 5ad75cb

File tree

5 files changed

+103
-21
lines changed

5 files changed

+103
-21
lines changed

app/api/stuff.py

Lines changed: 49 additions & 4 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
@@ -42,11 +42,56 @@ async def create_stuff(
4242

4343

4444
@router.get("/{name}", response_model=StuffResponse)
45-
async def find_stuff(
45+
async def find_stuff(name: str, db_session: AsyncSession = Depends(get_db)):
46+
result = await Stuff.find(db_session, name)
47+
if not result:
48+
raise HTTPException(
49+
status_code=status.HTTP_404_NOT_FOUND,
50+
detail=f"Stuff with name {name} not found.",
51+
)
52+
return result
53+
54+
55+
@router.get("/pool/{name}", response_model=StuffResponse)
56+
async def find_stuff_pool(
57+
request: Request,
4658
name: str,
4759
db_session: AsyncSession = Depends(get_db),
4860
):
49-
return await Stuff.find(db_session, name)
61+
"""
62+
Asynchronous function to find a specific 'Stuff' object in the database using a connection pool.
63+
64+
This function compiles an SQL statement to find a 'Stuff' object by its name, executes the statement
65+
using a connection from the application's connection pool, and returns the result as a dictionary.
66+
If the 'Stuff' object is not found, it raises an HTTPException with a 404 status code.
67+
If an SQLAlchemyError occurs during the execution of the SQL statement, it raises an HTTPException
68+
with a 422 status code.
69+
70+
Args:
71+
request (Request): The incoming request. Used to access the application's connection pool.
72+
name (str): The name of the 'Stuff' object to find.
73+
db_session (AsyncSession): The database session. Used to compile the SQL statement.
74+
75+
Returns:
76+
dict: The found 'Stuff' object as a dictionary.
77+
78+
Raises:
79+
HTTPException: If the 'Stuff' object is not found or an SQLAlchemyError occurs.
80+
"""
81+
try:
82+
stmt = await Stuff.find(db_session, name, compile_sql=True)
83+
result = await request.app.postgres_pool.fetchrow(str(stmt))
84+
result = dict(result)
85+
except SQLAlchemyError as ex:
86+
raise HTTPException(
87+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=repr(ex)
88+
) from ex
89+
if not result:
90+
raise HTTPException(
91+
status_code=status.HTTP_404_NOT_FOUND,
92+
detail=f"Stuff with name {name} not found.",
93+
)
94+
return result
5095

5196

5297
@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: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,20 @@
99
from app.models.base import Base
1010
from app.models.nonsense import Nonsense
1111

12+
from functools import wraps
13+
14+
15+
def compile_sql_or_scalar(func):
16+
@wraps(func)
17+
async def wrapper(cls, db_session, name, compile_sql=False, *args, **kwargs):
18+
stmt = await func(cls, db_session, name, *args, **kwargs)
19+
if compile_sql:
20+
return stmt.compile(compile_kwargs={"literal_binds": True})
21+
result = await db_session.execute(stmt)
22+
return result.scalars().first()
23+
24+
return wrapper
25+
1226

1327
class Stuff(Base):
1428
__tablename__ = "stuff"
@@ -24,23 +38,10 @@ class Stuff(Base):
2438
)
2539

2640
@classmethod
27-
async def find(cls, db_session: AsyncSession, name: str):
28-
"""
29-
30-
:param db_session:
31-
:param name:
32-
:return:
33-
"""
41+
@compile_sql_or_scalar
42+
async def find(cls, db_session: AsyncSession, name: str, compile_sql=False):
3443
stmt = select(cls).options(joinedload(cls.nonsense)).where(cls.name == name)
35-
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
44+
return stmt
4445

4546

4647
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)