From 3a21b16a5510009ab69ddb11228387381c07ac7c Mon Sep 17 00:00:00 2001 From: ghostfaccee Date: Tue, 7 Jul 2026 11:57:37 +0900 Subject: [PATCH] feat(redis, cache): caching of get requests has been added --- app/api/v1/endpoints/habits.py | 6 +++++ app/api/v1/endpoints/stats.py | 2 ++ app/infrastructure/redis/cache.py | 41 +++++++++++++++++++++++++++++++ app/infrastructure/redis/redis.py | 24 ------------------ app/main.py | 4 ++- readme.md | 2 ++ requirements.txt | 7 ++++++ tests/conftest.py | 4 ++- 8 files changed, 64 insertions(+), 26 deletions(-) create mode 100644 app/infrastructure/redis/cache.py delete mode 100644 app/infrastructure/redis/redis.py diff --git a/app/api/v1/endpoints/habits.py b/app/api/v1/endpoints/habits.py index b5e2066..ab95a59 100644 --- a/app/api/v1/endpoints/habits.py +++ b/app/api/v1/endpoints/habits.py @@ -4,10 +4,12 @@ from app.services.habit_service import HabitService from app.schemas.habit import HabitCreate, HabitUpdate, HabitResponse from app.middlewares.rate_limit.limiter import limiter +from app.infrastructure.redis.cache import CacheService router = APIRouter() @router.get('/habits', response_model = List[HabitResponse]) +@CacheService.cached(expire = 50) @limiter.limit('5/minute') async def get_all(request: Request, service: HabitService = Depends(get_habit_service)): return await service.get_all() @@ -15,9 +17,11 @@ async def get_all(request: Request, service: HabitService = Depends(get_habit_se @router.post('/habit/create', response_model = HabitResponse, status_code = status.HTTP_201_CREATED) @limiter.limit('5/minute') async def create(request: Request, data: HabitCreate, service: HabitService = Depends(get_habit_service)): + await CacheService.clear() return await service.create(data) @router.get('/habit/{habit_id}', response_model = HabitResponse) +@CacheService.cached(expire = 50) @limiter.limit('5/minute') async def get_by_id(request: Request, habit_id: int, service: HabitService = Depends(get_habit_service)): return await service.get_by_id(habit_id) @@ -26,10 +30,12 @@ async def get_by_id(request: Request, habit_id: int, service: HabitService = Dep @limiter.limit('5/minute') async def delete_by_id(request: Request, habit_id: int, service: HabitService = Depends(get_habit_service)): await service.delete(habit_id) + await CacheService.clear() return None @router.put('/habit/{habit_id}', response_model = HabitResponse) @limiter.limit('5/minute') async def update(request: Request, habit_id: int, data: HabitUpdate, service: HabitService = Depends(get_habit_service)): + await CacheService.clear() return await service.update(habit_id, data) diff --git a/app/api/v1/endpoints/stats.py b/app/api/v1/endpoints/stats.py index e457d4b..d573159 100644 --- a/app/api/v1/endpoints/stats.py +++ b/app/api/v1/endpoints/stats.py @@ -3,10 +3,12 @@ from app.services.stats_service import StatsService from app.schemas.stats import StatsResponse from app.middlewares.rate_limit.limiter import limiter +from app.infrastructure.redis.cache import CacheService router = APIRouter() @router.get('/stats/{habit_id}', response_model = StatsResponse) +@CacheService.cached(expire = 50) @limiter.limit('5/minute') async def get(request: Request, habit_id: int, service: StatsService = Depends(get_stats_service)): return await service.get_stats(habit_id) \ No newline at end of file diff --git a/app/infrastructure/redis/cache.py b/app/infrastructure/redis/cache.py new file mode 100644 index 0000000..1ea2293 --- /dev/null +++ b/app/infrastructure/redis/cache.py @@ -0,0 +1,41 @@ +from fastapi_cache import FastAPICache +from fastapi_cache.decorator import cache +from fastapi_cache.backends.redis import RedisBackend +import redis.asyncio as redis +from app.core.config import settings + +class CacheService: + _enabled = True + + @classmethod + def disable(cls): + cls._enabled = False + + @classmethod + async def init(cls): + if not cls._enabled: + return + + redis_client = redis.from_url( + settings.REDIS_URL, + decode_responses=True, + max_connections=10 + ) + FastAPICache.init( + RedisBackend(redis_client), + prefix="trackit-cache" + ) + + @staticmethod + def cached(expire: int = 60): + def decorator(func): + if not CacheService._enabled: + return func + + return cache(expire=expire)(func) + return decorator + + @staticmethod + async def clear(): + if CacheService._enabled: + await FastAPICache.clear(namespace="trackit-cache") \ No newline at end of file diff --git a/app/infrastructure/redis/redis.py b/app/infrastructure/redis/redis.py deleted file mode 100644 index ed5902d..0000000 --- a/app/infrastructure/redis/redis.py +++ /dev/null @@ -1,24 +0,0 @@ -# Redis client for caching and storing temporary data. Currently not used in the project and reserved for future innovations. - -import redis.asyncio as redis -from app.core.config import settings - -class RedisClient: - _client = None - - @classmethod - async def get_client(cls) -> redis.Redis: - if cls._client is None: - cls._client = redis.from_url( - settings.REDIS_URL, - decode_responses = True, - max_connections = 10 - ) - return cls._client - - @classmethod - async def close(cls): - if cls._client: - await cls._client.aclose() - cls._client = None - diff --git a/app/main.py b/app/main.py index a4266bc..c4928a1 100644 --- a/app/main.py +++ b/app/main.py @@ -9,12 +9,14 @@ from app.middlewares.logging.logging_middleware import LoggingMiddleware from app.api import router from app.core.logger import logger +from app.infrastructure.redis.cache import CacheService @asynccontextmanager async def lifespan(app: FastAPI): async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) - logger.info('Application started') + await CacheService.init() + logger.info('Application started') yield app = FastAPI(lifespan = lifespan) diff --git a/readme.md b/readme.md index 94bca97..0f5ad43 100644 --- a/readme.md +++ b/readme.md @@ -9,6 +9,8 @@ API for tracking habits. Written in FastAPI with asynchronous database operation [![FastAPI](https://img.shields.io/badge/FastAPI-009485.svg?logo=fastapi&logoColor=white)](#) [![Pytest](https://img.shields.io/badge/Pytest-fff?logo=pytest&logoColor=000)](#) [![Docker](https://img.shields.io/badge/Docker-2496ED?logo=docker&logoColor=fff)](#) +[![Redis](https://img.shields.io/badge/Redis-%23DD0031.svg?logo=redis&logoColor=white)](#) +[![Postgres](https://img.shields.io/badge/Postgres-%23316192.svg?logo=postgresql&logoColor=white)](#) ## Installation and launch (Ubuntu) ``` diff --git a/requirements.txt b/requirements.txt index f617fef..631a8fd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,15 +8,19 @@ certifi==2026.6.17 click==8.4.2 Deprecated==1.3.1 fastapi==0.138.1 +fastapi-cache2==0.2.2 greenlet==3.5.3 h11==0.16.0 httpcore==1.0.9 httpx==0.28.1 idna==3.18 iniconfig==2.3.0 +Jinja2==3.1.6 limits==5.8.0 loguru==0.7.3 +MarkupSafe==3.0.3 packaging==26.2 +pendulum==3.2.0 pluggy==1.6.0 pydantic==2.13.4 pydantic-settings==2.14.2 @@ -24,13 +28,16 @@ pydantic_core==2.46.4 Pygments==2.20.0 pytest==9.1.1 pytest-asyncio==1.4.0 +python-dateutil==2.9.0.post0 python-dotenv==1.2.2 redis==8.0.1 +six==1.17.0 slowapi==0.1.10 sniffio==1.3.1 SQLAlchemy==2.0.51 starlette==1.3.1 typing-inspection==0.4.2 typing_extensions==4.15.0 +tzdata==2026.2 uvicorn==0.49.0 wrapt==2.2.2 diff --git a/tests/conftest.py b/tests/conftest.py index 476935f..f659ecb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,6 +3,9 @@ from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker from app.core.database import Base, get_db +from app.infrastructure.redis.cache import CacheService +CacheService.disable() + import app.middlewares.rate_limit.limiter as rate_limit_module from slowapi import Limiter from slowapi.util import get_remote_address @@ -12,7 +15,6 @@ def decorator(func): return func return decorator -original_limiter_class = Limiter rate_limit_module.limiter = Limiter( key_func=get_remote_address, default_limits=["100/minute"]