Skip to content
Open
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
22 changes: 13 additions & 9 deletions .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,25 @@ on:
branches:
- main

permissions:
contents: read

jobs:
lint:
name: Linter
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Install Poetry
run: pipx install poetry
- name: Setup Python
id: setup-python
uses: actions/setup-python@v5
with:
python-version: '3.x'
python-version: '3.12'
cache: poetry
- name: Cache mypy cache
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: .mypy_cache
key: ${{ runner.os }}-mypy-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('poetry.lock') }}
Expand All @@ -41,14 +44,14 @@ jobs:
- lint
strategy:
matrix:
python: ["3.8", "3.9", "3.10", "3.11"]
python: ["3.10", "3.11", "3.12", "3.13", "3.14"]
fail-fast: false

name: "Test on Python ${{ matrix.python }}"
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Install Poetry
run: pipx install poetry
- uses: actions/setup-python@v5
Expand Down Expand Up @@ -79,19 +82,19 @@ jobs:
needs: [test-summary]
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install Poetry
run: pipx install poetry
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.x'
python-version: '3.12'
cache: poetry
- name: Build distributions
run:
make build
- name: Upload artifacts
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: dist
path: dist
Expand All @@ -108,9 +111,10 @@ jobs:
url: https://pypi.org/p/fastapi-cache2
permissions:
id-token: write
contents: read
steps:
- name: Download artifacts
uses: actions/download-artifact@v2
uses: actions/download-artifact@v4
with:
name: dist
path: dist
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/towncrier.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ jobs:
towncrier:
name: Towncrier
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
# skip if this is a bot or the PR has the skip-changelog label
# note that the towncrier check command can recognize a release PR, by looking
# for changes to the CHANGELOG.md file.
Expand Down
1 change: 1 addition & 0 deletions changelog.d/173.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Modernize dependency stack for redis-py 5.x/6.x and Prefect 3.x compatibility. Drop Python 3.8/3.9 support, require Python >=3.10.
8 changes: 4 additions & 4 deletions examples/in_memory/main.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# pyright: reportGeneralTypeIssues=false
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from typing import AsyncIterator, Dict, Optional

import pendulum
import uvicorn
Expand Down Expand Up @@ -94,9 +94,9 @@ async def handler_method(self):
# cache a Pydantic model instance; the return type annotation is required in this case
class Item(BaseModel):
name: str
description: Optional[str] = None
description: str | None = None
price: float
tax: Optional[float] = None
tax: float | None = None


@app.get("/pydantic_instance")
Expand Down Expand Up @@ -129,7 +129,7 @@ async def cached_put():
@cache(namespace="test", expire=5, injected_dependency_namespace="monty_python") # pyright: ignore[reportArgumentType]
def namespaced_injection(
__fastapi_cache_request: int = 42, __fastapi_cache_response: int = 17
) -> Dict[str, int]:
) -> dict[str, int]:
return {
"__fastapi_cache_request": __fastapi_cache_request,
"__fastapi_cache_response": __fastapi_cache_response,
Expand Down
7 changes: 3 additions & 4 deletions examples/redis/main.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# pyright: reportGeneralTypeIssues=false
import time
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from typing import AsyncIterator

import pendulum
import redis.asyncio as redis
import uvicorn
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
Expand All @@ -13,12 +14,10 @@
from fastapi_cache.backends.redis import RedisBackend
from fastapi_cache.coder import PickleCoder
from fastapi_cache.decorator import cache
from redis.asyncio.connection import ConnectionPool
from starlette.requests import Request
from starlette.responses import JSONResponse, Response

import redis.asyncio as redis
from redis.asyncio.connection import ConnectionPool


@asynccontextmanager
async def lifespan(_: FastAPI) -> AsyncIterator[None]:
Expand Down
24 changes: 12 additions & 12 deletions fastapi_cache/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from importlib.metadata import version
from typing import ClassVar, Optional, Type
from typing import ClassVar

from fastapi_cache.coder import Coder, JsonCoder
from fastapi_cache.key_builder import default_key_builder
Expand All @@ -17,22 +17,22 @@


class FastAPICache:
_backend: ClassVar[Optional[Backend]] = None
_prefix: ClassVar[Optional[str]] = None
_expire: ClassVar[Optional[int]] = None
_backend: ClassVar[Backend | None] = None
_prefix: ClassVar[str | None] = None
_expire: ClassVar[int | None] = None
_init: ClassVar[bool] = False
_coder: ClassVar[Optional[Type[Coder]]] = None
_key_builder: ClassVar[Optional[KeyBuilder]] = None
_cache_status_header: ClassVar[Optional[str]] = None
_coder: ClassVar[type[Coder] | None] = None
_key_builder: ClassVar[KeyBuilder | None] = None
_cache_status_header: ClassVar[str | None] = None
_enable: ClassVar[bool] = True

@classmethod
def init(
cls,
backend: Backend,
prefix: str = "",
expire: Optional[int] = None,
coder: Type[Coder] = JsonCoder,
expire: int | None = None,
coder: type[Coder] = JsonCoder,
key_builder: KeyBuilder = default_key_builder,
cache_status_header: str = "X-FastAPI-Cache",
enable: bool = True,
Expand Down Expand Up @@ -70,11 +70,11 @@ def get_prefix(cls) -> str:
return cls._prefix

@classmethod
def get_expire(cls) -> Optional[int]:
def get_expire(cls) -> int | None:
return cls._expire

@classmethod
def get_coder(cls) -> Type[Coder]:
def get_coder(cls) -> type[Coder]:
assert cls._coder, "You must call init first!" # noqa: S101
return cls._coder

Expand All @@ -94,7 +94,7 @@ def get_enable(cls) -> bool:

@classmethod
async def clear(
cls, namespace: Optional[str] = None, key: Optional[str] = None
cls, namespace: str | None = None, key: str | None = None
) -> int:
assert ( # noqa: S101
cls._backend and cls._prefix is not None
Expand Down
31 changes: 20 additions & 11 deletions fastapi_cache/backends/dynamodb.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import datetime
from typing import TYPE_CHECKING, Optional, Tuple
from typing import TYPE_CHECKING

from aiobotocore.client import AioBaseClient
from aiobotocore.session import AioSession, get_session
Expand Down Expand Up @@ -33,9 +33,9 @@ class DynamoBackend(Backend):
client: DynamoDBClient
session: AioSession
table_name: str
region: Optional[str]
region: str | None

def __init__(self, table_name: str, region: Optional[str] = None) -> None:
def __init__(self, table_name: str, region: str | None = None) -> None:
self.session: AioSession = get_session()
self.table_name = table_name
self.region = region
Expand All @@ -46,10 +46,12 @@ async def init(self) -> None:
).__aenter__()

async def close(self) -> None:
self.client = await self.client.__aexit__(None, None, None)
self.client = await self.client.__aexit__(None, None, None) # type: ignore[assignment,func-returns-value]

async def get_with_ttl(self, key: str) -> Tuple[int, Optional[bytes]]:
response = await self.client.get_item(TableName=self.table_name, Key={"key": {"S": key}})
async def get_with_ttl(self, key: str) -> tuple[int, bytes | None]:
response = await self.client.get_item(
TableName=self.table_name, Key={"key": {"S": key}}
)

if "Item" in response:
value = response["Item"].get("value", {}).get("B")
Expand All @@ -65,20 +67,25 @@ async def get_with_ttl(self, key: str) -> Tuple[int, Optional[bytes]]:

return 0, None

async def get(self, key: str) -> Optional[bytes]:
response = await self.client.get_item(TableName=self.table_name, Key={"key": {"S": key}})
async def get(self, key: str) -> bytes | None:
response = await self.client.get_item(
TableName=self.table_name, Key={"key": {"S": key}}
)
if "Item" in response:
return response["Item"].get("value", {}).get("B")
return None

async def set(self, key: str, value: bytes, expire: Optional[int] = None) -> None:
async def set(
self, key: str, value: bytes, expire: int | None = None
) -> None:
ttl = (
{
"ttl": {
"N": str(
int(
(
datetime.datetime.now() + datetime.timedelta(seconds=expire)
datetime.datetime.now()
+ datetime.timedelta(seconds=expire)
).timestamp()
)
)
Expand All @@ -99,5 +106,7 @@ async def set(self, key: str, value: bytes, expire: Optional[int] = None) -> Non
},
)

async def clear(self, namespace: Optional[str] = None, key: Optional[str] = None) -> int:
async def clear(
self, namespace: str | None = None, key: str | None = None
) -> int:
raise NotImplementedError
13 changes: 6 additions & 7 deletions fastapi_cache/backends/inmemory.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import time
from asyncio import Lock
from dataclasses import dataclass
from typing import Dict, Optional, Tuple

from fastapi_cache.types import Backend

Expand All @@ -13,14 +12,14 @@ class Value:


class InMemoryBackend(Backend):
_store: Dict[str, Value] = {}
_store: dict[str, Value] = {}
_lock = Lock()

@property
def _now(self) -> int:
return int(time.time())

def _get(self, key: str) -> Optional[Value]:
def _get(self, key: str) -> Value | None:
v = self._store.get(key)
if v:
if v.ttl_ts < self._now:
Expand All @@ -29,25 +28,25 @@ def _get(self, key: str) -> Optional[Value]:
return v
return None

async def get_with_ttl(self, key: str) -> Tuple[int, Optional[bytes]]:
async def get_with_ttl(self, key: str) -> tuple[int, bytes | None]:
async with self._lock:
v = self._get(key)
if v:
return v.ttl_ts - self._now, v.data
return 0, None

async def get(self, key: str) -> Optional[bytes]:
async def get(self, key: str) -> bytes | None:
async with self._lock:
v = self._get(key)
if v:
return v.data
return None

async def set(self, key: str, value: bytes, expire: Optional[int] = None) -> None:
async def set(self, key: str, value: bytes, expire: int | None = None) -> None:
async with self._lock:
self._store[key] = Value(value, self._now + (expire or 0))

async def clear(self, namespace: Optional[str] = None, key: Optional[str] = None) -> int:
async def clear(self, namespace: str | None = None, key: str | None = None) -> int:
count = 0
if namespace:
keys = list(self._store.keys())
Expand Down
9 changes: 4 additions & 5 deletions fastapi_cache/backends/memcached.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from typing import Optional, Tuple

from aiomcache import Client

Expand All @@ -9,14 +8,14 @@ class MemcachedBackend(Backend):
def __init__(self, mcache: Client):
self.mcache = mcache

async def get_with_ttl(self, key: str) -> Tuple[int, Optional[bytes]]:
async def get_with_ttl(self, key: str) -> tuple[int, bytes | None]:
return 3600, await self.get(key)

async def get(self, key: str) -> Optional[bytes]:
async def get(self, key: str) -> bytes | None:
return await self.mcache.get(key.encode())

async def set(self, key: str, value: bytes, expire: Optional[int] = None) -> None:
async def set(self, key: str, value: bytes, expire: int | None = None) -> None:
await self.mcache.set(key.encode(), value, exptime=expire or 0)

async def clear(self, namespace: Optional[str] = None, key: Optional[str] = None) -> int:
async def clear(self, namespace: str | None = None, key: str | None = None) -> int:
raise NotImplementedError
10 changes: 5 additions & 5 deletions fastapi_cache/backends/redis.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Optional, Tuple, Union
from typing import Union

from redis.asyncio.client import Redis
from redis.asyncio.cluster import RedisCluster
Expand All @@ -11,17 +11,17 @@ def __init__(self, redis: Union["Redis[bytes]", "RedisCluster[bytes]"]):
self.redis = redis
self.is_cluster: bool = isinstance(redis, RedisCluster)

async def get_with_ttl(self, key: str) -> Tuple[int, Optional[bytes]]:
async def get_with_ttl(self, key: str) -> tuple[int, bytes | None]:
async with self.redis.pipeline(transaction=not self.is_cluster) as pipe:
return await pipe.ttl(key).get(key).execute() # type: ignore[union-attr,no-any-return]

async def get(self, key: str) -> Optional[bytes]:
async def get(self, key: str) -> bytes | None:
return await self.redis.get(key) # type: ignore[union-attr]

async def set(self, key: str, value: bytes, expire: Optional[int] = None) -> None:
async def set(self, key: str, value: bytes, expire: int | None = None) -> None:
await self.redis.set(key, value, ex=expire) # type: ignore[union-attr]

async def clear(self, namespace: Optional[str] = None, key: Optional[str] = None) -> int:
async def clear(self, namespace: str | None = None, key: str | None = None) -> int:
if namespace:
lua = f"for i, name in ipairs(redis.call('KEYS', '{namespace}:*')) do redis.call('DEL', name); end"
return await self.redis.eval(lua, numkeys=0) # type: ignore[union-attr,no-any-return]
Expand Down
Loading