mirror of
https://github.com/long2ice/fastapi-cache.git
synced 2026-03-25 13:07:53 +00:00
Compare commits
1 Commits
integratio
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
771ee9a161 |
40
README.md
40
README.md
@@ -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.
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
Delete method support for memcached backend (from @xodiumx)
|
|
||||||
@@ -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)
|
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
1303
poetry.lock
generated
1303
poetry.lock
generated
File diff suppressed because one or more lines are too long
@@ -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 = "*"
|
||||||
@@ -47,7 +47,7 @@ towncrier = "^22.12.0"
|
|||||||
optional = true
|
optional = true
|
||||||
|
|
||||||
[tool.poetry.group.distributing.dependencies]
|
[tool.poetry.group.distributing.dependencies]
|
||||||
twine = { version = "^4.0.2", python = "^3.10" }
|
twine = { version = ">=4.0.2,<7.0.0", python = "^3.10" }
|
||||||
|
|
||||||
[tool.poetry.extras]
|
[tool.poetry.extras]
|
||||||
redis = ["redis"]
|
redis = ["redis"]
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user