1 Commits

Author SHA1 Message Date
long2ice
b2143ef85e Update README.md 2025-06-30 08:20:56 +08:00
10 changed files with 586 additions and 789 deletions

View File

@@ -50,42 +50,13 @@ or
### Quick Start ### Quick Start
Using in-memory backend:
```python
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi_cache import FastAPICache
from fastapi_cache.backends.inmemory import InMemoryBackend
from fastapi_cache.decorator import cache
@asynccontextmanager
async def lifespan(app: FastAPI):
FastAPICache.init(InMemoryBackend(), prefix="fastapi-cache")
yield
app = FastAPI(lifespan=lifespan)
@cache()
async def get_cache():
return 1
@app.get("/")
@cache(expire=60)
async def index():
return dict(hello="world")
```
or [Redis](https://redis.io) backend:
```python ```python
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from fastapi import FastAPI from fastapi import FastAPI
from starlette.requests import Request
from starlette.responses import Response
from fastapi_cache import FastAPICache from fastapi_cache import FastAPICache
from fastapi_cache.backends.redis import RedisBackend from fastapi_cache.backends.redis import RedisBackend
@@ -95,7 +66,7 @@ from redis import asyncio as aioredis
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(_: FastAPI) -> AsyncIterator[None]:
redis = aioredis.from_url("redis://localhost") redis = aioredis.from_url("redis://localhost")
FastAPICache.init(RedisBackend(redis), prefix="fastapi-cache") FastAPICache.init(RedisBackend(redis), prefix="fastapi-cache")
yield yield
@@ -114,6 +85,7 @@ async def get_cache():
async def index(): async def index():
return dict(hello="world") return dict(hello="world")
``` ```
### Initialization ### Initialization
First you must call `FastAPICache.init` during startup FastAPI startup; this is where you set global configuration. First you must call `FastAPICache.init` during startup FastAPI startup; this is where you set global configuration.
@@ -260,3 +232,7 @@ xdg-open htmlcov/index.html
## License ## License
This project is licensed under the [Apache-2.0](https://github.com/long2ice/fastapi-cache/blob/master/LICENSE) License. This project is licensed under the [Apache-2.0](https://github.com/long2ice/fastapi-cache/blob/master/LICENSE) License.
## Sponsor
[![Powered by DartNode](https://dartnode.com/branding/DN-Open-Source-sm.png)](https://dartnode.com "Powered by DartNode - Free VPS for Open Source")

View File

@@ -1 +0,0 @@
Delete method support for memcached backend (from @xodiumx)

View File

@@ -19,11 +19,4 @@ class MemcachedBackend(Backend):
await self.mcache.set(key.encode(), value, exptime=expire or 0) 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: Optional[str] = None, key: Optional[str] = None) -> int:
is_deleted = False raise NotImplementedError
if key:
is_deleted = await self.mcache.delete(key=key.encode())
else:
await self.mcache.flush_all()
is_deleted = True
return int(is_deleted)

View File

@@ -90,7 +90,7 @@ def cache(
key_builder: Optional[KeyBuilder] = None, key_builder: Optional[KeyBuilder] = None,
namespace: str = "", namespace: str = "",
injected_dependency_namespace: str = "__fastapi_cache", injected_dependency_namespace: str = "__fastapi_cache",
) -> Callable[[Union[Callable[P, Awaitable[R]], Callable[P, R]]], Callable[P, Awaitable[Union[R, Response]]]]: ) -> Callable[[Callable[P, Awaitable[R]]], Callable[P, Awaitable[Union[R, Response]]]]:
""" """
cache all function cache all function
:param injected_dependency_namespace: :param injected_dependency_namespace:
@@ -114,7 +114,7 @@ def cache(
) )
def wrapper( def wrapper(
func: Union[Callable[P, Awaitable[R]], Callable[P, R]] func: Callable[P, Awaitable[R]]
) -> Callable[P, Awaitable[Union[R, Response]]]: ) -> Callable[P, Awaitable[Union[R, Response]]]:
# get_typed_signature ensures that any forward references are resolved first # get_typed_signature ensures that any forward references are resolved first
wrapped_signature = get_typed_signature(func) wrapped_signature = get_typed_signature(func)
@@ -142,7 +142,7 @@ def cache(
# unintuitively, we have to await once here, so that caller # unintuitively, we have to await once here, so that caller
# does not have to await twice. See # does not have to await twice. See
# https://stackoverflow.com/a/59268198/532513 # https://stackoverflow.com/a/59268198/532513
return await func(*args, **kwargs) # type: ignore[no-any-return] return await func(*args, **kwargs)
else: else:
# sync, wrap in thread and return async # sync, wrap in thread and return async
# see above why we have to await even although caller also awaits. # see above why we have to await even although caller also awaits.
@@ -221,8 +221,7 @@ def cache(
return response return response
result = cast(R, coder.decode_as_type(cached, type_=return_type)) result = cast(R, coder.decode_as_type(cached, type_=return_type))
if response and isinstance(result, Response):
result.headers.update(response.headers)
return result return result
inner.__signature__ = _augment_signature(wrapped_signature, *to_inject) # type: ignore[attr-defined] inner.__signature__ = _augment_signature(wrapped_signature, *to_inject) # type: ignore[attr-defined]

1255
poetry.lock generated

File diff suppressed because one or more lines are too long

View File

@@ -23,17 +23,17 @@ importlib-metadata = { version = "^6.6.0", python = "<3.8" }
pendulum = "^3.0.0" pendulum = "^3.0.0"
aiomcache = { version = "^0.8.2", optional = true } aiomcache = { version = "^0.8.2", optional = true }
aiobotocore = {version = "^2.13.1", optional = true} aiobotocore = {version = "^2.13.1", optional = true}
redis = {version = "^5.2.0", extras = ["redis"]} redis = {version = "^5.0.8", extras = ["redis"]}
[tool.poetry.group.linting] [tool.poetry.group.linting]
optional = true optional = true
[tool.poetry.group.linting.dependencies] [tool.poetry.group.linting.dependencies]
mypy = { version = "^1.13.0", python = "^3.10" } mypy = { version = "^1.2.0", python = "^3.10" }
pyright = { version = "^1.1.388", python = "^3.10" } pyright = { version = "^1.1.373", python = "^3.10" }
types-aiobotocore = { extras = ["dynamodb"], version = "^2.5.0.post2", python = "^3.10" } types-aiobotocore = { extras = ["dynamodb"], version = "^2.5.0.post2", python = "^3.10" }
types-redis = { version = "^4.5.4.2", python = "^3.10" } types-redis = { version = "^4.5.4.2", python = "^3.10" }
ruff = { version = ">=0.3.2", python = "^3.10" } ruff = { version = ">=0.0.267,<0.1.2", python = "^3.10" }
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
pytest = "*" pytest = "*"
@@ -96,9 +96,9 @@ pythonVersion = "3.8"
addopts = "-p no:warnings" addopts = "-p no:warnings"
[tool.ruff] [tool.ruff]
ignore = ["E501"]
line-length = 80 line-length = 80
lint.ignore = ["E501"] select = [
lint.select = [
"B", # flake8-bugbear "B", # flake8-bugbear
"C4", # flake8-comprehensions "C4", # flake8-comprehensions
"E", # pycodestyle errors "E", # pycodestyle errors

View File

@@ -1,10 +1,10 @@
[tool.ruff] [tool.ruff]
extend = "../pyproject.toml" extend = "../pyproject.toml"
lint.extend-select = [ extend-select = [
"PT", # flake8-pytest-style "PT", # flake8-pytest-style
] ]
lint.ignore = ["S101"] ignore = ["S101"]
[tool.ruff.lint.isort] [tool.ruff.isort]
known-first-party = ["examples", "fastapi_cache"] known-first-party = ["examples", "fastapi_cache"]

View File

@@ -1,8 +1,8 @@
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, Dict, List, Optional, Type from typing import Any, Optional, Tuple, Type
import pytest import pytest
from pydantic import BaseModel from pydantic import BaseModel, ValidationError
from fastapi_cache.coder import JsonCoder, PickleCoder from fastapi_cache.coder import JsonCoder, PickleCoder
@@ -41,21 +41,16 @@ def test_pickle_coder(value: Any) -> None:
assert decoded_value == value assert decoded_value == value
# vicchi: 2024/11/09 - some values commented out until #460 is resolved
@pytest.mark.parametrize( @pytest.mark.parametrize(
("value", "return_type"), ("value", "return_type"),
[ [
(1, int),
(1, None), (1, None),
("some_string", str),
("some_string", None), ("some_string", None),
# ((1, 2), Tuple[int, int]), ((1, 2), Tuple[int, int]),
([1, 2, 3], List[int]),
([1, 2, 3], None), ([1, 2, 3], None),
({"some_key": 1, "other_key": 2}, Dict[str, int]),
({"some_key": 1, "other_key": 2}, None), ({"some_key": 1, "other_key": 2}, None),
# (DCItem(name="foo", price=42.0, description="some dataclass item", tax=0.2), DCItem), (DCItem(name="foo", price=42.0, description="some dataclass item", tax=0.2), DCItem),
# (PDItem(name="foo", price=42.0, description="some pydantic item", tax=0.2), PDItem), (PDItem(name="foo", price=42.0, description="some pydantic item", tax=0.2), PDItem),
], ],
) )
def test_json_coder(value: Any, return_type: Type[Any]) -> None: def test_json_coder(value: Any, return_type: Type[Any]) -> None:
@@ -65,8 +60,7 @@ def test_json_coder(value: Any, return_type: Type[Any]) -> None:
assert decoded_value == value assert decoded_value == value
# vicchi: 2024/11/09 - test commented out until #460 is resolved def test_json_coder_validation_error() -> None:
# def test_json_coder_validation_error() -> None: invalid = b'{"name": "incomplete"}'
# invalid = b'{"name": "incomplete"}' with pytest.raises(ValidationError):
# with pytest.raises(ValidationError): JsonCoder.decode_as_type(invalid, type_=PDItem)
# JsonCoder.decode_as_type(invalid, type_=PDItem)

View File

@@ -1,5 +1,4 @@
import time import time
from http import HTTPStatus
from typing import Any, Generator from typing import Any, Generator
import pendulum import pendulum
@@ -23,17 +22,19 @@ def test_datetime() -> None:
response = client.get("/datetime") response = client.get("/datetime")
assert response.headers.get("X-FastAPI-Cache") == "MISS" assert response.headers.get("X-FastAPI-Cache") == "MISS"
now = response.json().get("now") now = response.json().get("now")
now_ = pendulum.parse(now) now_ = pendulum.now()
assert pendulum.parse(now) == now_
response = client.get("/datetime") response = client.get("/datetime")
assert response.headers.get("X-FastAPI-Cache") == "HIT" assert response.headers.get("X-FastAPI-Cache") == "HIT"
now = response.json().get("now") now = response.json().get("now")
assert pendulum.parse(now) == now_ assert pendulum.parse(now) == now_
time.sleep(3) time.sleep(3)
response = client.get("/datetime") response = client.get("/datetime")
assert response.headers.get("X-FastAPI-Cache") == "MISS"
now = response.json().get("now") now = response.json().get("now")
assert response.headers.get("X-FastAPI-Cache") == "MISS"
now = pendulum.parse(now) now = pendulum.parse(now)
assert now != now_ assert now != now_
assert now == pendulum.now()
def test_date() -> None: def test_date() -> None:
@@ -99,13 +100,11 @@ def test_pydantic_model() -> None:
def test_non_get() -> None: def test_non_get() -> None:
with TestClient(app) as client: with TestClient(app) as client:
response = client.put("/cached_put") response = client.put("/cached_put")
assert response.status_code == HTTPStatus.METHOD_NOT_ALLOWED
assert "X-FastAPI-Cache" not in response.headers assert "X-FastAPI-Cache" not in response.headers
assert response.json() != {"value": 1} assert response.json() == {"value": 1}
response = client.put("/cached_put") response = client.put("/cached_put")
assert response.status_code == HTTPStatus.METHOD_NOT_ALLOWED
assert "X-FastAPI-Cache" not in response.headers assert "X-FastAPI-Cache" not in response.headers
assert response.json() != {"value": 2} assert response.json() == {"value": 2}
def test_alternate_injected_namespace() -> None: def test_alternate_injected_namespace() -> None:

View File

@@ -30,7 +30,7 @@ skip_install = true
commands_pre = commands_pre =
poetry install --no-root --with=linting --sync --all-extras poetry install --no-root --with=linting --sync --all-extras
commands = commands =
ruff check . ruff check --show-source .
mypy mypy
pyright pyright