Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions app/api/v1/endpoints/habits.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,24 @@
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()

@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)
Expand All @@ -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)

2 changes: 2 additions & 0 deletions app/api/v1/endpoints/stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
41 changes: 41 additions & 0 deletions app/infrastructure/redis/cache.py
Original file line number Diff line number Diff line change
@@ -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")
24 changes: 0 additions & 24 deletions app/infrastructure/redis/redis.py

This file was deleted.

4 changes: 3 additions & 1 deletion app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
```
Expand Down
7 changes: 7 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,36 @@ 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
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
4 changes: 3 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"]
Expand Down