mirror of
https://github.com/long2ice/fastapi-cache.git
synced 2026-03-25 04:57:54 +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
|
||||
|
||||
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
|
||||
from collections.abc import AsyncIterator
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import Response
|
||||
|
||||
from fastapi_cache import FastAPICache
|
||||
from fastapi_cache.backends.redis import RedisBackend
|
||||
@@ -95,7 +66,7 @@ from redis import asyncio as aioredis
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
async def lifespan(_: FastAPI) -> AsyncIterator[None]:
|
||||
redis = aioredis.from_url("redis://localhost")
|
||||
FastAPICache.init(RedisBackend(redis), prefix="fastapi-cache")
|
||||
yield
|
||||
@@ -114,6 +85,7 @@ async def get_cache():
|
||||
async def index():
|
||||
return dict(hello="world")
|
||||
```
|
||||
|
||||
### Initialization
|
||||
|
||||
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)
|
||||
|
||||
async def clear(self, namespace: Optional[str] = None, key: Optional[str] = None) -> int:
|
||||
is_deleted = False
|
||||
if key:
|
||||
is_deleted = await self.mcache.delete(key=key.encode())
|
||||
else:
|
||||
await self.mcache.flush_all()
|
||||
is_deleted = True
|
||||
|
||||
return int(is_deleted)
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -90,7 +90,7 @@ def cache(
|
||||
key_builder: Optional[KeyBuilder] = None,
|
||||
namespace: str = "",
|
||||
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
|
||||
:param injected_dependency_namespace:
|
||||
@@ -114,7 +114,7 @@ def cache(
|
||||
)
|
||||
|
||||
def wrapper(
|
||||
func: Union[Callable[P, Awaitable[R]], Callable[P, R]]
|
||||
func: Callable[P, Awaitable[R]]
|
||||
) -> Callable[P, Awaitable[Union[R, Response]]]:
|
||||
# get_typed_signature ensures that any forward references are resolved first
|
||||
wrapped_signature = get_typed_signature(func)
|
||||
@@ -142,7 +142,7 @@ def cache(
|
||||
# unintuitively, we have to await once here, so that caller
|
||||
# does not have to await twice. See
|
||||
# https://stackoverflow.com/a/59268198/532513
|
||||
return await func(*args, **kwargs) # type: ignore[no-any-return]
|
||||
return await func(*args, **kwargs)
|
||||
else:
|
||||
# sync, wrap in thread and return async
|
||||
# see above why we have to await even although caller also awaits.
|
||||
@@ -221,8 +221,7 @@ def cache(
|
||||
return response
|
||||
|
||||
result = cast(R, coder.decode_as_type(cached, type_=return_type))
|
||||
if response and isinstance(result, Response):
|
||||
result.headers.update(response.headers)
|
||||
|
||||
return result
|
||||
|
||||
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"
|
||||
aiomcache = { version = "^0.8.2", 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]
|
||||
optional = true
|
||||
|
||||
[tool.poetry.group.linting.dependencies]
|
||||
mypy = { version = "^1.13.0", python = "^3.10" }
|
||||
pyright = { version = "^1.1.388", python = "^3.10" }
|
||||
mypy = { version = "^1.2.0", python = "^3.10" }
|
||||
pyright = { version = "^1.1.373", 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" }
|
||||
ruff = { version = ">=0.3.2", python = "^3.10" }
|
||||
ruff = { version = ">=0.0.267,<0.1.2", python = "^3.10" }
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pytest = "*"
|
||||
@@ -47,7 +47,7 @@ towncrier = "^22.12.0"
|
||||
optional = true
|
||||
|
||||
[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]
|
||||
redis = ["redis"]
|
||||
@@ -96,9 +96,9 @@ pythonVersion = "3.8"
|
||||
addopts = "-p no:warnings"
|
||||
|
||||
[tool.ruff]
|
||||
ignore = ["E501"]
|
||||
line-length = 80
|
||||
lint.ignore = ["E501"]
|
||||
lint.select = [
|
||||
select = [
|
||||
"B", # flake8-bugbear
|
||||
"C4", # flake8-comprehensions
|
||||
"E", # pycodestyle errors
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
[tool.ruff]
|
||||
extend = "../pyproject.toml"
|
||||
lint.extend-select = [
|
||||
extend-select = [
|
||||
"PT", # flake8-pytest-style
|
||||
]
|
||||
lint.ignore = ["S101"]
|
||||
ignore = ["S101"]
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
[tool.ruff.isort]
|
||||
known-first-party = ["examples", "fastapi_cache"]
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, List, Optional, Type
|
||||
from typing import Any, Optional, Tuple, Type
|
||||
|
||||
import pytest
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, ValidationError
|
||||
|
||||
from fastapi_cache.coder import JsonCoder, PickleCoder
|
||||
|
||||
@@ -41,21 +41,16 @@ def test_pickle_coder(value: Any) -> None:
|
||||
assert decoded_value == value
|
||||
|
||||
|
||||
# vicchi: 2024/11/09 - some values commented out until #460 is resolved
|
||||
@pytest.mark.parametrize(
|
||||
("value", "return_type"),
|
||||
[
|
||||
(1, int),
|
||||
(1, None),
|
||||
("some_string", str),
|
||||
("some_string", None),
|
||||
# ((1, 2), Tuple[int, int]),
|
||||
([1, 2, 3], List[int]),
|
||||
((1, 2), Tuple[int, int]),
|
||||
([1, 2, 3], None),
|
||||
({"some_key": 1, "other_key": 2}, Dict[str, int]),
|
||||
({"some_key": 1, "other_key": 2}, None),
|
||||
# (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),
|
||||
(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),
|
||||
],
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
# vicchi: 2024/11/09 - test commented out until #460 is resolved
|
||||
# def test_json_coder_validation_error() -> None:
|
||||
# invalid = b'{"name": "incomplete"}'
|
||||
# with pytest.raises(ValidationError):
|
||||
# JsonCoder.decode_as_type(invalid, type_=PDItem)
|
||||
def test_json_coder_validation_error() -> None:
|
||||
invalid = b'{"name": "incomplete"}'
|
||||
with pytest.raises(ValidationError):
|
||||
JsonCoder.decode_as_type(invalid, type_=PDItem)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import time
|
||||
from http import HTTPStatus
|
||||
from typing import Any, Generator
|
||||
|
||||
import pendulum
|
||||
@@ -23,17 +22,19 @@ def test_datetime() -> None:
|
||||
response = client.get("/datetime")
|
||||
assert response.headers.get("X-FastAPI-Cache") == "MISS"
|
||||
now = response.json().get("now")
|
||||
now_ = pendulum.parse(now)
|
||||
now_ = pendulum.now()
|
||||
assert pendulum.parse(now) == now_
|
||||
response = client.get("/datetime")
|
||||
assert response.headers.get("X-FastAPI-Cache") == "HIT"
|
||||
now = response.json().get("now")
|
||||
assert pendulum.parse(now) == now_
|
||||
time.sleep(3)
|
||||
response = client.get("/datetime")
|
||||
assert response.headers.get("X-FastAPI-Cache") == "MISS"
|
||||
now = response.json().get("now")
|
||||
assert response.headers.get("X-FastAPI-Cache") == "MISS"
|
||||
now = pendulum.parse(now)
|
||||
assert now != now_
|
||||
assert now == pendulum.now()
|
||||
|
||||
|
||||
def test_date() -> None:
|
||||
@@ -99,13 +100,11 @@ def test_pydantic_model() -> None:
|
||||
def test_non_get() -> None:
|
||||
with TestClient(app) as client:
|
||||
response = client.put("/cached_put")
|
||||
assert response.status_code == HTTPStatus.METHOD_NOT_ALLOWED
|
||||
assert "X-FastAPI-Cache" not in response.headers
|
||||
assert response.json() != {"value": 1}
|
||||
assert response.json() == {"value": 1}
|
||||
response = client.put("/cached_put")
|
||||
assert response.status_code == HTTPStatus.METHOD_NOT_ALLOWED
|
||||
assert "X-FastAPI-Cache" not in response.headers
|
||||
assert response.json() != {"value": 2}
|
||||
assert response.json() == {"value": 2}
|
||||
|
||||
|
||||
def test_alternate_injected_namespace() -> None:
|
||||
|
||||
Reference in New Issue
Block a user