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
135 changes: 135 additions & 0 deletions examples/redis_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
"""An example of using Fast Cache Middleware with rout resolution and Redis storage.

to install using Redis, run this command: pip install fast-cache-middleware[redis]

Demonstrates:
1. Analysis of routes at the start of the application;
2. Extracting configuration cache from dependencies;
3. Automatic caching of GET requests in Redis;
4. Cache invalidation in case of modifying requests.
"""
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

можно дописать как ставить с екстрой, чтобы редис поставился


import logging
import time
import typing as tp

import uvicorn
from fastapi import FastAPI, HTTPException, Request
from pydantic import BaseModel, Field
from redis.asyncio import Redis # async only

from fast_cache_middleware import (
CacheConfig,
CacheDropConfig,
FastCacheMiddleware,
RedisStorage,
)

# Creating a Flash API application
app = FastAPI(title="FastCacheMiddleware Redis Example")
# Initializing Redis
redis = Redis(host="127.0.0.1", port=6379, db=0, decode_responses=True)

# Adding middleware - it will analyze the routes at the first request.
app.add_middleware(FastCacheMiddleware, storage=RedisStorage(redis_client=redis))


def custom_key_func(request: Request) -> str:
user_id = request.headers.get("user-id", "anonymous")
return f"{request.url.path}:user:{user_id}"


class User(BaseModel):
name: str
email: str


class FullUser(User):
user_id: int


class UserResponse(FullUser):
timestamp: float = Field(default_factory=time.time)


_USERS_STORAGE: tp.Dict[int, User] = {
1: User(name="John Doe", email="john.doe@example.com"),
2: User(name="Jane Doe", email="jane.doe@example.com"),
}


# Routers with different caching configurations


@app.get(
"/users/{user_id}",
dependencies=[CacheConfig(max_age=120, key_func=custom_key_func)],
)
async def get_user(user_id: int) -> UserResponse:
"""Getting a user with a custom caching key.

The cache key includes the user-id from the headers for personalization.
"""
user = _USERS_STORAGE.get(user_id)
if not user:
raise HTTPException(status_code=404, detail="User not found")

return UserResponse(user_id=user_id, name=user.name, email=user.email)


@app.get("/users", dependencies=[CacheConfig(max_age=120)])
async def get_users() -> tp.List[UserResponse]:
return [
UserResponse(user_id=user_id, name=user.name, email=user.email)
for user_id, user in _USERS_STORAGE.items()
]


@app.post("/users/{user_id}", dependencies=[CacheDropConfig(paths=["/users"])])
async def create_user(user_id: int, user_data: User) -> UserResponse:
"""Creating a user with a cache disability.

This POST request disables the cache for all /users/* paths.
"""
_USERS_STORAGE[user_id] = user_data

return UserResponse(user_id=user_id, name=user_data.name, email=user_data.email)


@app.delete("/users/{user_id}", dependencies=[CacheDropConfig(paths=["/users"])])
async def delete_user(user_id: int) -> UserResponse:
"""Deleting a user with a cache disability.

This DELETE request disables the cache for all /users/* paths.
"""
user = _USERS_STORAGE.get(user_id)
if not user:
raise HTTPException(status_code=404, detail="User not found")
del _USERS_STORAGE[user_id]

return UserResponse(user_id=user_id, name=user.name, email=user.email)


if __name__ == "__main__":
logging.basicConfig(
level=logging.DEBUG,
format="[-] %(asctime)s [%(levelname)s] %(module)s-%(lineno)d - %(message)s",
)

print("🚀 Running Fast Cache Middleware Redis Example...")
print("\n📋 Available endpoints:")
print(" GET /users/{user_id} - getting the user (2 min cache)")
print(" GET /users - list of users (2 min cache)")
print(" POST /users/{user_id} - user creation (disability /users)")
print(" DELETE /users/{user_id} - deleting a user (invalidation /users)")

print("\n💡 For testing purposes:")
print(" curl http://localhost:8000/users/1")
print(" curl http://localhost:8000/users")
print(
' curl -X POST http://localhost:8000/users/1 -H "Content-Type: application/json" -d \'{"name": "John", "email": "john@example.com"}\''
)
print(" curl -X DELETE http://localhost:8000/users/1")
print()

uvicorn.run(app, host="127.0.0.1", port=8000)
3 changes: 2 additions & 1 deletion fast_cache_middleware/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from .controller import Controller
from .depends import BaseCacheConfigDepends, CacheConfig, CacheDropConfig
from .middleware import FastCacheMiddleware
from .storages import BaseStorage, InMemoryStorage
from .storages import BaseStorage, InMemoryStorage, RedisStorage

__version__ = "1.0.0"

Expand All @@ -29,6 +29,7 @@
# Storages
"BaseStorage",
"InMemoryStorage",
"RedisStorage",
# Serialization
"BaseSerializer",
"DefaultSerializer",
Expand Down
44 changes: 36 additions & 8 deletions fast_cache_middleware/serializers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import json
from typing import Any, Callable, Dict, Optional, Tuple, TypeAlias, Union
from typing import Any, Dict, Tuple, TypeAlias, Union
from urllib.parse import urlparse

from starlette.requests import Request
from starlette.responses import Response
Expand All @@ -10,7 +11,7 @@


class BaseSerializer:
def dumps(
async def dumps(
self, response: Response, request: Request, metadata: Metadata
) -> Union[str, bytes]:
raise NotImplementedError()
Expand All @@ -24,8 +25,36 @@ def is_binary(self) -> bool:


class JSONSerializer(BaseSerializer):
def dumps(self, response: Response, request: Request, metadata: Metadata) -> str:
raise NotImplementedError() # fixme: bad implementation now, maybe async?
async def dumps(
self, response: Response, request: Request, metadata: Metadata
) -> Union[str, bytes]:
body_bytes = await request.body()
request_data = {
"method": request.method,
"url": str(request.url),
"headers": dict(request.headers),
"body": (
body_bytes.decode("utf-8", errors="ignore") if body_bytes else None
),
}

response_data = {
"status_code": response.status_code,
"headers": dict(response.headers),
"content": (
bytes(response.body).decode("utf-8", errors="ignore")
if response.body
else None
),
}

payload = {
"request": request_data,
"response": response_data,
"metadata": metadata,
}

return json.dumps(payload)

def loads(self, data: Union[str, bytes]) -> StoredResponse:
if isinstance(data, bytes):
Expand All @@ -48,16 +77,15 @@ def loads(self, data: Union[str, bytes]) -> StoredResponse:
# Restore Request - create mock object for compatibility
request_data = parsed["request"]

# Create minimal scope for Request
from urllib.parse import urlparse

parsed_url = urlparse(request_data["url"])
scope = {
"type": "http",
"method": request_data["method"],
"path": parsed_url.path,
"query_string": parsed_url.query.encode() if parsed_url.query else b"",
"headers": [[k.encode(), v.encode()] for k, v in request_data["headers"]],
"headers": [
[k.encode(), v.encode()] for k, v in request_data["headers"].items()
],
}

# Create empty receive function
Expand Down
3 changes: 3 additions & 0 deletions fast_cache_middleware/storages/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .base_storage import BaseStorage
from .in_memory_storage import InMemoryStorage
from .redis_storage import RedisStorage
45 changes: 45 additions & 0 deletions fast_cache_middleware/storages/base_storage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import re
from typing import Optional, Tuple, TypeAlias, Union

from starlette.requests import Request
from starlette.responses import Response

from fast_cache_middleware.exceptions import StorageError
from fast_cache_middleware.serializers import BaseSerializer, JSONSerializer, Metadata

StoredResponse: TypeAlias = Tuple[Response, Request, Metadata]


class BaseStorage:
"""Base class for cache storage.

Args:
serializer: Serializer for converting Response/Request to string/bytes
ttl: Cache lifetime in seconds. None for permanent storage
"""

def __init__(
self,
serializer: Optional[BaseSerializer] = None,
ttl: Optional[Union[int, float]] = None,
) -> None:
self._serializer = serializer or JSONSerializer()

if ttl is not None and ttl <= 0:
raise StorageError("TTL must be positive")

self._ttl = ttl

async def store(
self, key: str, response: Response, request: Request, metadata: Metadata
) -> None:
raise NotImplementedError()

async def retrieve(self, key: str) -> Optional[StoredResponse]:
raise NotImplementedError()

async def remove(self, path: re.Pattern) -> None:
raise NotImplementedError()

async def close(self) -> None:
raise NotImplementedError()
Original file line number Diff line number Diff line change
Expand Up @@ -2,55 +2,17 @@
import re
import time
from collections import OrderedDict
from typing import Any, Dict, Optional, Tuple, Union
from typing import Dict, Optional, Union

from starlette.requests import Request
from starlette.responses import Response
from typing_extensions import TypeAlias

from .exceptions import StorageError
from .serializers import BaseSerializer, JSONSerializer, Metadata
from fast_cache_middleware.exceptions import StorageError
from fast_cache_middleware.serializers import BaseSerializer, Metadata

logger = logging.getLogger(__name__)

# Define type for stored response
StoredResponse: TypeAlias = Tuple[Response, Request, Metadata]


# Define base class for cache storage
class BaseStorage:
"""Base class for cache storage.

Args:
serializer: Serializer for converting Response/Request to string/bytes
ttl: Cache lifetime in seconds. None for permanent storage
"""

def __init__(
self,
serializer: Optional[BaseSerializer] = None,
ttl: Optional[Union[int, float]] = None,
) -> None:
self._serializer = serializer or JSONSerializer()

if ttl is not None and ttl <= 0:
raise StorageError("TTL must be positive")

self._ttl = ttl

async def store(
self, key: str, response: Response, request: Request, metadata: Metadata
) -> None:
raise NotImplementedError()
from .base_storage import BaseStorage, StoredResponse

async def retrieve(self, key: str) -> Optional[StoredResponse]:
raise NotImplementedError()

async def remove(self, path: re.Pattern) -> None:
raise NotImplementedError()

async def close(self) -> None:
raise NotImplementedError()
logger = logging.getLogger(__name__)


class InMemoryStorage(BaseStorage):
Expand Down
Loading