Skip to content

Commit e912bb3

Browse files
committed
refactor!: support for async code
1 parent b8626e9 commit e912bb3

File tree

6 files changed

+132
-111
lines changed

6 files changed

+132
-111
lines changed

.coveragerc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[run]
2+
concurrency = thread,gevent

data/player_database.py

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,38 @@
22
# Database
33
# ------------------------------------------------------------------------------
44

5-
from sqlalchemy import create_engine
6-
from sqlalchemy.orm import scoped_session, sessionmaker, declarative_base
5+
import logging
6+
from typing import AsyncGenerator
7+
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
8+
from sqlalchemy.orm import sessionmaker, declarative_base
79

8-
DATABASE_URL = "sqlite:///./data/players-sqlite3.db"
10+
DATABASE_URL = "sqlite+aiosqlite:///./data/players-sqlite3.db"
11+
12+
logger = logging.getLogger("uvicorn")
13+
logging.getLogger("sqlalchemy.engine.Engine").handlers = logger.handlers
14+
15+
async_engine = create_async_engine(
16+
DATABASE_URL,
17+
connect_args={"check_same_thread": False},
18+
echo=True
19+
)
20+
21+
async_sessionmaker = sessionmaker(
22+
bind=async_engine,
23+
class_=AsyncSession,
24+
autocommit=False,
25+
autoflush=False
26+
)
927

10-
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
11-
OrmSession = scoped_session(sessionmaker(autocommit=False, autoflush=False, bind=engine))
1228
Base = declarative_base()
29+
30+
31+
async def generate_async_session() -> AsyncGenerator[AsyncSession, None]:
32+
"""
33+
Dependency function to yield an async SQLAlchemy ORM session.
34+
35+
Yields:
36+
AsyncSession: An instance of an async SQLAlchemy ORM session.
37+
"""
38+
async with async_sessionmaker() as async_session:
39+
yield async_session

main.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,16 @@
1010

1111

1212
@asynccontextmanager
13-
async def lifespan(app: FastAPI):
13+
async def lifespan_context_manager(_):
14+
"""
15+
Context manager for the FastAPI app lifespan.
16+
17+
Initializes FastAPICache with an InMemoryBackend for the duration of the app's lifespan.
18+
"""
1419
FastAPICache.init(InMemoryBackend())
1520
yield
1621

17-
app = FastAPI(lifespan=lifespan,
22+
app = FastAPI(lifespan=lifespan_context_manager,
1823
title="python-samples-fastapi-restful",
1924
description="🧪 Proof of Concept for a RESTful API made with Python 3 and FastAPI",
2025
version="1.0.0",)

requirements.txt

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,48 @@
11
aiohttp==3.9.5
22
aiosignal==1.3.1
3+
aiosqlite==0.20.0
34
annotated-types==0.7.0
45
anyio==4.4.0
6+
asttokens==2.4.1
57
attrs==23.2.0
8+
backcall==0.2.0
9+
bleach==6.1.0
610
certifi==2024.7.4
711
charset-normalizer==3.3.2
812
click==8.1.7
9-
colorclass==2.2.2
1013
coverage==7.6.0
14+
decorator==5.1.1
15+
defusedxml==0.7.1
1116
dnspython==2.6.1
12-
docopt==0.6.2
1317
email_validator==2.2.0
1418
fastapi==0.111.1
1519
fastapi-cache2==0.2.1
1620
fastapi-cli==0.0.4
21+
fastjsonschema==2.20.0
1722
flake8==7.1.0
1823
frozenlist==1.4.1
24+
gevent==24.2.1
1925
greenlet==3.0.3
2026
h11==0.14.0
2127
httpcore==1.0.5
2228
httptools==0.6.1
2329
httpx==0.27.0
2430
idna==3.7
25-
ijson==3.3.0
2631
iniconfig==2.0.0
32+
jedi==0.19.1
2733
Jinja2==3.1.4
34+
jsonschema==4.23.0
35+
jsonschema-specifications==2023.12.1
2836
markdown-it-py==3.0.0
2937
MarkupSafe==2.1.5
3038
mccabe==0.7.0
3139
mdurl==0.1.2
3240
multidict==6.0.5
3341
orjson==3.10.6
3442
packaging==24.1
43+
parso==0.8.4
3544
pendulum==3.0.0
3645
pluggy==1.5.0
37-
pur==7.3.2
3846
pycodestyle==2.12.0
3947
pydantic==2.8.2
4048
pydantic_core
@@ -48,10 +56,13 @@ python-dateutil==2.9.0.post0
4856
python-dotenv==1.0.1
4957
python-multipart==0.0.9
5058
PyYAML==6.0.1
59+
pyzmq==26.0.3
60+
referencing==0.35.1
5161
requests==2.32.3
5262
responses==0.25.3
5363
rfc3986==2.0.0
5464
rich==13.7.1
65+
rpds-py==0.19.1
5566
setuptools==71.1.0
5667
shellingham==1.5.4
5768
six==1.16.0
@@ -60,9 +71,8 @@ SQLAlchemy==2.0.31
6071
starlette
6172
stdlib-list==0.10.0
6273
termcolor==2.4.0
63-
terminaltables==3.1.10
6474
time-machine==2.14.2
65-
tree-sitter==0.22.3
75+
tornado==6.4.1
6676
typer==0.12.3
6777
typing_extensions==4.12.2
6878
tzdata==2024.1
@@ -71,5 +81,9 @@ urllib3==2.2.2
7181
uvicorn==0.30.3
7282
uvloop==0.19.0
7383
watchfiles==0.22.0
84+
wcwidth==0.2.13
85+
webencodings==0.5.1
7486
websockets==12.0
7587
yarl==1.9.4
88+
zope.event==5.0
89+
zope.interface==6.4.post2

routes/player_route.py

Lines changed: 38 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -4,33 +4,18 @@
44

55
from typing import List
66
from fastapi import APIRouter, Body, Depends, HTTPException, status, Path
7-
from sqlalchemy.orm import Session
7+
from sqlalchemy.ext.asyncio import AsyncSession
88
from fastapi_cache import FastAPICache
99
from fastapi_cache.decorator import cache
10-
from data.player_database import OrmSession
10+
11+
from data.player_database import generate_async_session
1112
from models.player_model import PlayerModel
1213
from services import player_service
1314

1415
api_router = APIRouter()
1516

1617
CACHING_TIME_IN_SECONDS = 600
1718

18-
# https://fastapi.tiangolo.com/tutorial/sql-databases/#create-a-dependency
19-
20-
21-
def get_orm_session():
22-
"""
23-
Dependency function to yield a scoped SQLAlchemy ORM session.
24-
25-
Yields:
26-
OrmSession: An instance of a scoped SQLAlchemy ORM session.
27-
"""
28-
orm_session = OrmSession()
29-
try:
30-
yield orm_session
31-
finally:
32-
orm_session.close()
33-
3419
# POST -------------------------------------------------------------------------
3520

3621

@@ -40,28 +25,25 @@ def get_orm_session():
4025
summary="Creates a new Player",
4126
tags=["Players"]
4227
)
43-
def post(
28+
async def post(
4429
player_model: PlayerModel = Body(...),
45-
orm_session: Session = Depends(get_orm_session)
30+
async_session: AsyncSession = Depends(generate_async_session)
4631
):
4732
"""
4833
Endpoint to create a new player.
4934
5035
Args:
51-
player_model (PlayerModel): The data model representing a Player.
52-
orm_session (Session): The SQLAlchemy ORM session.
36+
player_model (PlayerModel): The Pydantic model representing the Player to create.
37+
async_session (AsyncSession): The async version of a SQLAlchemy ORM session.
5338
5439
Raises:
5540
HTTPException: HTTP 409 Conflict error if the Player already exists.
5641
"""
57-
player = player_service.retrieve_by_id(orm_session, player_model.id)
58-
42+
player = await player_service.retrieve_by_id(async_session, player_model.id)
5943
if player:
6044
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
61-
62-
player_service.create(orm_session, player_model)
63-
64-
FastAPICache.clear()
45+
await player_service.create(async_session, player_model)
46+
await FastAPICache.clear()
6547

6648
# GET --------------------------------------------------------------------------
6749

@@ -74,20 +56,19 @@ def post(
7456
tags=["Players"]
7557
)
7658
@cache(expire=CACHING_TIME_IN_SECONDS)
77-
def get_all(
78-
orm_session: Session = Depends(get_orm_session)
59+
async def get_all(
60+
async_session: AsyncSession = Depends(generate_async_session)
7961
):
8062
"""
8163
Endpoint to retrieve all players.
8264
8365
Args:
84-
orm_session (Session): The SQLAlchemy ORM session.
66+
async_session (AsyncSession): The async version of a SQLAlchemy ORM session.
8567
8668
Returns:
87-
List[PlayerModel]: A list of data models representing all players.
69+
List[PlayerModel]: A list of Pydantic models representing all players.
8870
"""
89-
players = player_service.retrieve_all(orm_session)
90-
71+
players = await player_service.retrieve_all(async_session)
9172
return players
9273

9374

@@ -99,28 +80,26 @@ def get_all(
9980
tags=["Players"]
10081
)
10182
@cache(expire=CACHING_TIME_IN_SECONDS)
102-
def get_by_id(
83+
async def get_by_id(
10384
player_id: int = Path(..., title="The ID of the Player"),
104-
orm_session: Session = Depends(get_orm_session)
85+
async_session: AsyncSession = Depends(generate_async_session)
10586
):
10687
"""
10788
Endpoint to retrieve a Player by its ID.
10889
10990
Args:
11091
player_id (int): The ID of the Player to retrieve.
111-
orm_session (Session): The SQLAlchemy ORM session.
92+
async_session (AsyncSession): The async version of a SQLAlchemy ORM session.
11293
11394
Returns:
114-
PlayerModel: A data model representing the Player.
95+
PlayerModel: The Pydantic model representing the matching Player.
11596
11697
Raises:
11798
HTTPException: Not found error if the Player with the specified ID does not exist.
11899
"""
119-
player = player_service.retrieve_by_id(orm_session, player_id)
120-
100+
player = await player_service.retrieve_by_id(async_session, player_id)
121101
if not player:
122102
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
123-
124103
return player
125104

126105

@@ -132,28 +111,26 @@ def get_by_id(
132111
tags=["Players"]
133112
)
134113
@cache(expire=CACHING_TIME_IN_SECONDS)
135-
def get_by_squad_number(
114+
async def get_by_squad_number(
136115
squad_number: int = Path(..., title="The Squad Number of the Player"),
137-
orm_session: Session = Depends(get_orm_session)
116+
async_session: AsyncSession = Depends(generate_async_session)
138117
):
139118
"""
140119
Endpoint to retrieve a Player by its Squad Number.
141120
142121
Args:
143122
squad_number (int): The Squad Number of the Player to retrieve.
144-
orm_session (Session): SQLAlchemy ORM session.
123+
async_session (AsyncSession): The async version of a SQLAlchemy ORM session.
145124
146125
Returns:
147-
PlayerModel: A data model representing the Player.
126+
PlayerModel: The Pydantic model representing the matching Player.
148127
149128
Raises:
150129
HTTPException: HTTP 404 Not Found error if the Player with the specified Squad Number does not exist.
151130
"""
152-
player = player_service.retrieve_by_squad_number(orm_session, squad_number)
153-
131+
player = await player_service.retrieve_by_squad_number(async_session, squad_number)
154132
if not player:
155133
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
156-
157134
return player
158135

159136
# PUT --------------------------------------------------------------------------
@@ -165,30 +142,27 @@ def get_by_squad_number(
165142
summary="Updates an existing Player",
166143
tags=["Players"]
167144
)
168-
def put(
145+
async def put(
169146
player_id: int = Path(..., title="The ID of the Player"),
170147
player_model: PlayerModel = Body(...),
171-
orm_session: Session = Depends(get_orm_session)
148+
async_session: AsyncSession = Depends(generate_async_session)
172149
):
173150
"""
174151
Endpoint to entirely update an existing Player.
175152
176153
Args:
177154
player_id (int): The ID of the Player to update.
178-
player_model (PlayerModel): The data model representing the Player to update.
179-
orm_session (Session): The SQLAlchemy ORM session.
155+
player_model (PlayerModel): The Pydantic model representing the Player to update.
156+
async_session (AsyncSession): The async version of a SQLAlchemy ORM session.
180157
181158
Raises:
182159
HTTPException: HTTP 404 Not Found error if the Player with the specified ID does not exist.
183160
"""
184-
player = player_service.retrieve_by_id(orm_session, player_id)
185-
161+
player = await player_service.retrieve_by_id(async_session, player_id)
186162
if not player:
187163
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
188-
189-
player_service.update(orm_session, player_model)
190-
191-
FastAPICache.clear()
164+
await player_service.update(async_session, player_model)
165+
await FastAPICache.clear()
192166

193167
# DELETE -----------------------------------------------------------------------
194168

@@ -199,25 +173,22 @@ def put(
199173
summary="Deletes an existing Player",
200174
tags=["Players"]
201175
)
202-
def delete(
176+
async def delete(
203177
player_id: int = Path(..., title="The ID of the Player"),
204-
orm_session: Session = Depends(get_orm_session)
178+
async_session: AsyncSession = Depends(generate_async_session)
205179
):
206180
"""
207181
Endpoint to delete an existing Player.
208182
209183
Args:
210184
player_id (int): The ID of the Player to delete.
211-
orm_session (Session): The SQLAlchemy ORM session.
185+
async_session (AsyncSession): The async version of a SQLAlchemy ORM session.
212186
213187
Raises:
214188
HTTPException: HTTP 404 Not Found error if the Player with the specified ID does not exist.
215189
"""
216-
player = player_service.retrieve_by_id(orm_session, player_id)
217-
190+
player = await player_service.retrieve_by_id(async_session, player_id)
218191
if not player:
219192
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
220-
221-
player_service.delete(orm_session, player_id)
222-
223-
FastAPICache.clear()
193+
await player_service.delete(async_session, player_id)
194+
await FastAPICache.clear()

0 commit comments

Comments
 (0)