Skip to content

Commit 4219b7c

Browse files
authored
Merge pull request #326 from nanotaboada/feature/aiocache
feature/aiocache
2 parents 585d2bc + 613bd66 commit 4219b7c

File tree

8 files changed

+66
-27
lines changed

8 files changed

+66
-27
lines changed

.vscode/launch.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@
77
"request": "launch",
88
"module": "uvicorn",
99
"args": ["main:app", "--reload", "--port", "9000"],
10-
"jinja": true
10+
"jinja": true,
11+
"serverReadyAction": {
12+
"action": "openExternally",
13+
"pattern": "Uvicorn running on .*:(\\d+)",
14+
"uriFormat": "http://localhost:%s/docs"
15+
}
1116
}
1217
]
1318
}

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ The following is a simplified dependency diagram of modules and main libraries:
2828
## Install
2929

3030
```console
31-
pip install --requirement requirements.txt
31+
pip install -r requirements.txt
32+
pip install -r requirements-lint.txt
33+
pip install -r requirements-test.txt
3234
```
3335

3436
## Start

main.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,24 @@
33
# ------------------------------------------------------------------------------
44

55
from contextlib import asynccontextmanager
6+
import logging
7+
from typing import AsyncIterator
68
from fastapi import FastAPI
7-
from fastapi_cache import FastAPICache
8-
from fastapi_cache.backends.inmemory import InMemoryBackend
99
from routes import player_route
1010

11+
# https://github.com/encode/uvicorn/issues/562
12+
UVICORN_LOGGER = "uvicorn.error"
13+
logger = logging.getLogger(UVICORN_LOGGER)
1114

1215
@asynccontextmanager
13-
async def lifespan_context_manager(_):
16+
async def lifespan(_: FastAPI) -> AsyncIterator[None]:
17+
""""
18+
Lifespan event handler for FastAPI.
1419
"""
15-
Context manager for the FastAPI app lifespan.
16-
17-
Initializes FastAPICache with an InMemoryBackend for the duration of the app's lifespan.
18-
"""
19-
FastAPICache.init(InMemoryBackend())
20+
logger.info("Lifespan event handler execution complete.")
2021
yield
2122

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

postman_collections/python-samples-fastapi-restful.postman_collection.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
}
2323
},
2424
"url": {
25-
"raw": "http://localhost:9000/players",
25+
"raw": "http://localhost:9000/players/",
2626
"protocol": "http",
2727
"host": ["localhost"],
2828
"port": "9000",
@@ -50,7 +50,7 @@
5050
}
5151
},
5252
"url": {
53-
"raw": "http://localhost:9000/players",
53+
"raw": "http://localhost:9000/players/",
5454
"protocol": "http",
5555
"host": ["localhost"],
5656
"port": "9000",

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# https://fastapi.tiangolo.com/#standard-dependencies
22
fastapi[standard]==0.115.12
3-
fastapi-cache2==0.2.2
43
SQLAlchemy==2.0.40
54
aiosqlite==0.21.0
5+
aiocache==0.12.3

routes/player_route.py

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,19 @@
33
# ------------------------------------------------------------------------------
44

55
from typing import List
6-
from fastapi import APIRouter, Body, Depends, HTTPException, status, Path
6+
from fastapi import APIRouter, Body, Depends, HTTPException, status, Path, Response
77
from sqlalchemy.ext.asyncio import AsyncSession
8-
from fastapi_cache import FastAPICache
9-
from fastapi_cache.decorator import cache
8+
from aiocache import SimpleMemoryCache
109

1110
from data.player_database import generate_async_session
1211
from models.player_model import PlayerModel
1312
from services import player_service
1413

1514
api_router = APIRouter()
15+
simple_memory_cache = SimpleMemoryCache()
1616

17-
CACHING_TIME_IN_SECONDS = 600
17+
CACHE_KEY = "players"
18+
CACHE_TTL = 600 # 10 minutes
1819

1920
# POST -------------------------------------------------------------------------
2021

@@ -43,7 +44,7 @@ async def post_async(
4344
if player:
4445
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
4546
await player_service.create_async(async_session, player_model)
46-
await FastAPICache.clear()
47+
await simple_memory_cache.clear(CACHE_KEY)
4748

4849
# GET --------------------------------------------------------------------------
4950

@@ -55,8 +56,8 @@ async def post_async(
5556
summary="Retrieves a collection of Players",
5657
tags=["Players"]
5758
)
58-
@cache(expire=CACHING_TIME_IN_SECONDS)
5959
async def get_all_async(
60+
response: Response,
6061
async_session: AsyncSession = Depends(generate_async_session)
6162
):
6263
"""
@@ -68,7 +69,12 @@ async def get_all_async(
6869
Returns:
6970
List[PlayerModel]: A list of Pydantic models representing all players.
7071
"""
71-
players = await player_service.retrieve_all_async(async_session)
72+
players = await simple_memory_cache.get(CACHE_KEY)
73+
response.headers["X-Cache"] = "HIT"
74+
if not players:
75+
players = await player_service.retrieve_all_async(async_session)
76+
await simple_memory_cache.set(CACHE_KEY, players, ttl=CACHE_TTL)
77+
response.headers["X-Cache"] = "MISS"
7278
return players
7379

7480

@@ -79,7 +85,6 @@ async def get_all_async(
7985
summary="Retrieves a Player by its Id",
8086
tags=["Players"]
8187
)
82-
@cache(expire=CACHING_TIME_IN_SECONDS)
8388
async def get_by_id_async(
8489
player_id: int = Path(..., title="The ID of the Player"),
8590
async_session: AsyncSession = Depends(generate_async_session)
@@ -110,7 +115,6 @@ async def get_by_id_async(
110115
summary="Retrieves a Player by its Squad Number",
111116
tags=["Players"]
112117
)
113-
@cache(expire=CACHING_TIME_IN_SECONDS)
114118
async def get_by_squad_number_async(
115119
squad_number: int = Path(..., title="The Squad Number of the Player"),
116120
async_session: AsyncSession = Depends(generate_async_session)
@@ -162,7 +166,7 @@ async def put_async(
162166
if not player:
163167
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
164168
await player_service.update_async(async_session, player_model)
165-
await FastAPICache.clear()
169+
await simple_memory_cache.clear(CACHE_KEY)
166170

167171
# DELETE -----------------------------------------------------------------------
168172

@@ -191,4 +195,4 @@ async def delete_async(
191195
if not player:
192196
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
193197
await player_service.delete_async(async_session, player_id)
194-
await FastAPICache.clear()
198+
await simple_memory_cache.clear(CACHE_KEY)

tests/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
warnings.filterwarnings("ignore", category=DeprecationWarning)
88

99

10-
@pytest.fixture(scope="module")
10+
@pytest.fixture(scope="function")
1111
def client():
1212
with TestClient(app) as test_client:
1313
yield test_client

tests/test_main.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,33 @@
1010
# GET /players/ ----------------------------------------------------------------
1111

1212

13+
def test_given_get_when_request_is_initial_then_response_header_x_cache_should_be_miss(client):
14+
"""
15+
Given GET /players/
16+
when request is initial
17+
then response Header X-Cache value should be MISS
18+
"""
19+
# Act
20+
response = client.get(PATH)
21+
22+
# Assert
23+
assert "X-Cache" in response.headers
24+
assert response.headers.get("X-Cache") == "MISS"
25+
26+
def test_given_get_when_request_is_subsequent_then_response_header_x_cache_should_be_hit(client):
27+
"""
28+
Given GET /players/
29+
when request is subsequent
30+
then response Header X-Cache should be HIT
31+
"""
32+
# Act
33+
client.get(PATH) # initial
34+
response = client.get(PATH) # subsequent (cached)
35+
36+
# Assert
37+
assert "X-Cache" in response.headers
38+
assert response.headers.get("X-Cache") == "HIT"
39+
1340
def test_given_get_when_request_path_has_no_id_then_response_status_code_should_be_200_ok(client):
1441
"""
1542
Given GET /players/
@@ -26,7 +53,7 @@ def test_given_get_when_request_path_has_no_id_then_response_body_should_be_coll
2653
"""
2754
Given GET /players/
2855
when request path has no ID
29-
then response Status Code should be collection of players
56+
then response Body should be collection of players
3057
"""
3158
# Act
3259
response = client.get(PATH)

0 commit comments

Comments
 (0)