diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bf7649..5abcb47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ We follow [Semantic Versions](https://semver.org/). ## WIP +## Version 0.3.1 + +### Fixes + +- Removed `TypeAlias` import from `typing_extensions` for better compatibility with Python 3.8 and 3.9, as `TypeAlias` is now part of `typing` in Python 3.10+. +- Replaced the `RedisBackend` alias with a direct use of `Union[Redis, AsyncRedis]` in the `backend` parameter of the `__init__` method for backward compatibility. + +### Misc + +- Updated README with improved examples for: + - **Using middleware for FastAPI**: Enhanced to include proper error handling with `JSONResponse` for 429 responses, providing more clarity and correctness. + - **Using middleware for Django**: Added a class-based middleware example leveraging `MiddlewareMixin` for better integration with Django's request/response lifecycle. Also demonstrated an example with DRF's `APIView`. + ## Version 0.3.0 ### Features diff --git a/README.md b/README.md index bb9dd79..495d8e5 100644 --- a/README.md +++ b/README.md @@ -125,22 +125,31 @@ async with async_limiter: ```python from fastapi import FastAPI, HTTPException, Request +from fastapi.responses import JSONResponse from ratelimit_io import RatelimitIO, RatelimitIOError, LimitSpec from redis.asyncio import Redis as AsyncRedis app = FastAPI() redis_client = AsyncRedis(host="localhost", port=6379) -limiter = RatelimitIO(backend=redis_client, is_incoming=True) +limiter = RatelimitIO( + backend=redis_client, + base_limit=LimitSpec(requests=5, seconds=1), + is_incoming=True +) @app.middleware("http") async def ratelimit_middleware(request: Request, call_next): try: - await limiter.a_wait(f"user:{request.client.host}", LimitSpec(10, seconds=60)) - except RatelimitIOError as e: - raise HTTPException(status_code=e.status_code, detail=e.detail) - return await call_next(request) + response = await call_next(request) + return response + except RatelimitIOError as exc: + return JSONResponse( + status_code=exc.status_code, + content={"detail": exc.detail} + ) @app.get("/fetch") +@limit async def fetch_data(): return {"message": "Request succeeded!"} ``` @@ -149,20 +158,39 @@ async def fetch_data(): ```python from django.http import JsonResponse -from ratelimit_io import RatelimitIO, RatelimitIOError, LimitSpec +from django.utils.deprecation import MiddlewareMixin +from ratelimit_io import RatelimitIO, LimitSpec, RatelimitIOError from redis import Redis +from rest_framework import status +from rest_framework.response import Response +from rest_framework.views import APIView -redis_client = Redis(host="localhost", port=6379) -limiter = RatelimitIO(backend=redis_client, is_incoming=True) -def ratelimit_middleware(get_response): - def middleware(request): - try: - limiter.wait(f"user:{request.META['REMOTE_ADDR']}", LimitSpec(10, seconds=60)) - except RatelimitIOError as e: - return JsonResponse({"error": e.detail}, status=e.status_code) - return get_response(request) - return middleware +redis = Redis("localhost", port=6379) +limit = RatelimitIO( + backend=redis, + base_limit=LimitSpec(requests=5, seconds=1), + is_incoming=True, +) + + +class RatelimitMiddleware(MiddlewareMixin): + def process_exception(self, request, exception): + if isinstance(exception, RatelimitIOError): + return JsonResponse( + {"detail": exception.detail}, + status=exception.status_code, + ) + return None + + +class Foo(APIView): + permission_classes = () + + @limit + def get(self, request, *args, **kwargs): + return Response(data={"message": "ok"}, status=status.HTTP_200_OK) + ``` ### Flask Example diff --git a/pyproject.toml b/pyproject.toml index be2668f..0389f1f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "ratelimit-io" -version = "0.3.0" +version = "0.3.1" description = "Flexible bidirectional rate-limiting library with redis backend" authors = ["Galushko Bogdan "] license = "MIT" diff --git a/ratelimit_io/rate_limit.py b/ratelimit_io/rate_limit.py index 28a1dd4..05dce9b 100644 --- a/ratelimit_io/rate_limit.py +++ b/ratelimit_io/rate_limit.py @@ -10,7 +10,6 @@ from redis import Redis from redis.asyncio import Redis as AsyncRedis from redis.exceptions import NoScriptError -from typing_extensions import TypeAlias class RatelimitIOError(Exception): @@ -78,15 +77,12 @@ def __str__(self) -> str: return f"{self.requests}/{self.total_seconds()}s" -RedisBackend: TypeAlias = Union[Redis, AsyncRedis] - - class RatelimitIO: """Rate limiter for managing incoming and outgoing request limits.""" def __init__( self, - backend: RedisBackend, + backend: Union[Redis, AsyncRedis], is_incoming: bool = False, base_url: Optional[str] = None, base_limit: Optional[LimitSpec] = None,