mirror of
https://github.com/long2ice/fastapi-cache.git
synced 2026-03-25 04:57:54 +00:00
Merge pull request #128 from mjpieters/namespaced_injection
Inject dependencies using a namespace
This commit is contained in:
13
README.md
13
README.md
@@ -98,9 +98,22 @@ expire | int, states a caching time in seconds
|
|||||||
namespace | str, namespace to use to store certain cache items
|
namespace | str, namespace to use to store certain cache items
|
||||||
coder | which coder to use, e.g. JsonCoder
|
coder | which coder to use, e.g. JsonCoder
|
||||||
key_builder | which key builder to use, default to builtin
|
key_builder | which key builder to use, default to builtin
|
||||||
|
injected_dependency_namespace | prefix for injected dependency keywords, defaults to `__fastapi_cache`.
|
||||||
|
|
||||||
You can also use `cache` as decorator like other cache tools to cache common function result.
|
You can also use `cache` as decorator like other cache tools to cache common function result.
|
||||||
|
|
||||||
|
### Injected Request and Response dependencies
|
||||||
|
|
||||||
|
The `cache` decorator adds dependencies for the `Request` and `Response` objects, so that it can
|
||||||
|
add cache control headers to the outgoing response, and return a 304 Not Modified response when
|
||||||
|
the incoming request has a matching If-Non-Match header. This only happens if the decorated
|
||||||
|
endpoint doesn't already list these objects directly.
|
||||||
|
|
||||||
|
The keyword arguments for these extra dependencies are named
|
||||||
|
`__fastapi_cache_request` and `__fastapi_cache_response` to minimize collisions.
|
||||||
|
Use the `injected_dependency_namespace` argument to `@cache()` to change the
|
||||||
|
prefix used if those names would clash anyway.
|
||||||
|
|
||||||
|
|
||||||
### Supported data types
|
### Supported data types
|
||||||
|
|
||||||
|
|||||||
@@ -106,6 +106,17 @@ async def uncached_put():
|
|||||||
return {"value": put_ret}
|
return {"value": put_ret}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/namespaced_injection")
|
||||||
|
@cache(namespace="test", expire=5, injected_dependency_namespace="monty_python")
|
||||||
|
def namespaced_injection(
|
||||||
|
__fastapi_cache_request: int = 42, __fastapi_cache_response: int = 17
|
||||||
|
) -> dict[str, int]:
|
||||||
|
return {
|
||||||
|
"__fastapi_cache_request": __fastapi_cache_request,
|
||||||
|
"__fastapi_cache_response": __fastapi_cache_response,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@app.on_event("startup")
|
@app.on_event("startup")
|
||||||
async def startup():
|
async def startup():
|
||||||
FastAPICache.init(InMemoryBackend())
|
FastAPICache.init(InMemoryBackend())
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import inspect
|
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
from inspect import Parameter, Signature, isawaitable, iscoroutinefunction
|
||||||
from typing import Awaitable, Callable, Optional, Type, TypeVar
|
from typing import Awaitable, Callable, Optional, Type, TypeVar
|
||||||
|
|
||||||
if sys.version_info >= (3, 10):
|
if sys.version_info >= (3, 10):
|
||||||
@@ -10,7 +10,7 @@ else:
|
|||||||
from typing_extensions import ParamSpec
|
from typing_extensions import ParamSpec
|
||||||
|
|
||||||
from fastapi.concurrency import run_in_threadpool
|
from fastapi.concurrency import run_in_threadpool
|
||||||
from fastapi.dependencies.utils import get_typed_return_annotation
|
from fastapi.dependencies.utils import get_typed_return_annotation, get_typed_signature
|
||||||
from starlette.requests import Request
|
from starlette.requests import Request
|
||||||
from starlette.responses import Response
|
from starlette.responses import Response
|
||||||
|
|
||||||
@@ -24,34 +24,32 @@ P = ParamSpec("P")
|
|||||||
R = TypeVar("R")
|
R = TypeVar("R")
|
||||||
|
|
||||||
|
|
||||||
def _augment_signature(
|
def _augment_signature(signature: Signature, *extra: Parameter) -> Signature:
|
||||||
signature: inspect.Signature, add_request: bool, add_response: bool
|
if not extra:
|
||||||
) -> inspect.Signature:
|
|
||||||
if not (add_request or add_response):
|
|
||||||
return signature
|
return signature
|
||||||
|
|
||||||
parameters = list(signature.parameters.values())
|
parameters = list(signature.parameters.values())
|
||||||
variadic_keyword_params = []
|
variadic_keyword_params = []
|
||||||
while parameters and parameters[-1].kind is inspect.Parameter.VAR_KEYWORD:
|
while parameters and parameters[-1].kind is Parameter.VAR_KEYWORD:
|
||||||
variadic_keyword_params.append(parameters.pop())
|
variadic_keyword_params.append(parameters.pop())
|
||||||
|
|
||||||
if add_request:
|
return signature.replace(parameters=[*parameters, *extra, *variadic_keyword_params])
|
||||||
parameters.append(
|
|
||||||
inspect.Parameter(
|
|
||||||
name="request",
|
def _locate_param(sig: Signature, dep: Parameter, to_inject: list[Parameter]) -> Parameter:
|
||||||
annotation=Request,
|
"""Locate an existing parameter in the decorated endpoint
|
||||||
kind=inspect.Parameter.KEYWORD_ONLY,
|
|
||||||
),
|
If not found, returns the injectable parameter, and adds it to the to_inject list.
|
||||||
|
|
||||||
|
"""
|
||||||
|
param = next(
|
||||||
|
(param for param in sig.parameters.values() if param.annotation is dep.annotation),
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
if add_response:
|
if param is None:
|
||||||
parameters.append(
|
to_inject.append(dep)
|
||||||
inspect.Parameter(
|
param = dep
|
||||||
name="response",
|
return param
|
||||||
annotation=Response,
|
|
||||||
kind=inspect.Parameter.KEYWORD_ONLY,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return signature.replace(parameters=[*parameters, *variadic_keyword_params])
|
|
||||||
|
|
||||||
|
|
||||||
def cache(
|
def cache(
|
||||||
@@ -59,6 +57,7 @@ def cache(
|
|||||||
coder: Optional[Type[Coder]] = None,
|
coder: Optional[Type[Coder]] = None,
|
||||||
key_builder: Optional[KeyBuilder] = None,
|
key_builder: Optional[KeyBuilder] = None,
|
||||||
namespace: Optional[str] = "",
|
namespace: Optional[str] = "",
|
||||||
|
injected_dependency_namespace: str = "__fastapi_cache",
|
||||||
) -> Callable[[Callable[P, Awaitable[R]]], Callable[P, Awaitable[R]]]:
|
) -> Callable[[Callable[P, Awaitable[R]]], Callable[P, Awaitable[R]]]:
|
||||||
"""
|
"""
|
||||||
cache all function
|
cache all function
|
||||||
@@ -70,16 +69,23 @@ def cache(
|
|||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
injected_request = Parameter(
|
||||||
|
name=f"{injected_dependency_namespace}_request",
|
||||||
|
annotation=Request,
|
||||||
|
kind=Parameter.KEYWORD_ONLY,
|
||||||
|
)
|
||||||
|
injected_response = Parameter(
|
||||||
|
name=f"{injected_dependency_namespace}_response",
|
||||||
|
annotation=Response,
|
||||||
|
kind=Parameter.KEYWORD_ONLY,
|
||||||
|
)
|
||||||
|
|
||||||
def wrapper(func: Callable[P, Awaitable[R]]) -> Callable[P, Awaitable[R]]:
|
def wrapper(func: Callable[P, Awaitable[R]]) -> Callable[P, Awaitable[R]]:
|
||||||
signature = inspect.signature(func)
|
# get_typed_signature ensures that any forward references are resolved first
|
||||||
request_param = next(
|
wrapped_signature = get_typed_signature(func)
|
||||||
(param for param in signature.parameters.values() if param.annotation is Request),
|
to_inject: list[Parameter] = []
|
||||||
None,
|
request_param = _locate_param(wrapped_signature, injected_request, to_inject)
|
||||||
)
|
response_param = _locate_param(wrapped_signature, injected_response, to_inject)
|
||||||
response_param = next(
|
|
||||||
(param for param in signature.parameters.values() if param.annotation is Response),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
return_type = get_typed_return_annotation(func)
|
return_type = get_typed_return_annotation(func)
|
||||||
|
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
@@ -90,14 +96,13 @@ def cache(
|
|||||||
|
|
||||||
async def ensure_async_func(*args: P.args, **kwargs: P.kwargs) -> R:
|
async def ensure_async_func(*args: P.args, **kwargs: P.kwargs) -> R:
|
||||||
"""Run cached sync functions in thread pool just like FastAPI."""
|
"""Run cached sync functions in thread pool just like FastAPI."""
|
||||||
# if the wrapped function does NOT have request or response in its function signature,
|
# if the wrapped function does NOT have request or response in
|
||||||
# make sure we don't pass them in as keyword arguments
|
# its function signature, make sure we don't pass them in as
|
||||||
if not request_param:
|
# keyword arguments
|
||||||
kwargs.pop("request", None)
|
kwargs.pop(injected_request.name, None)
|
||||||
if not response_param:
|
kwargs.pop(injected_response.name, None)
|
||||||
kwargs.pop("response", None)
|
|
||||||
|
|
||||||
if inspect.iscoroutinefunction(func):
|
if iscoroutinefunction(func):
|
||||||
# async, return as is.
|
# async, return as is.
|
||||||
# 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
|
||||||
@@ -109,8 +114,8 @@ def cache(
|
|||||||
return await run_in_threadpool(func, *args, **kwargs)
|
return await run_in_threadpool(func, *args, **kwargs)
|
||||||
|
|
||||||
copy_kwargs = kwargs.copy()
|
copy_kwargs = kwargs.copy()
|
||||||
request: Optional[Request] = copy_kwargs.pop("request", None)
|
request: Optional[Request] = copy_kwargs.pop(request_param.name, None)
|
||||||
response: Optional[Response] = copy_kwargs.pop("response", None)
|
response: Optional[Response] = copy_kwargs.pop(response_param.name, None)
|
||||||
if (
|
if (
|
||||||
request and request.headers.get("Cache-Control") in ("no-store", "no-cache")
|
request and request.headers.get("Cache-Control") in ("no-store", "no-cache")
|
||||||
) or not FastAPICache.get_enable():
|
) or not FastAPICache.get_enable():
|
||||||
@@ -129,7 +134,7 @@ def cache(
|
|||||||
args=args,
|
args=args,
|
||||||
kwargs=copy_kwargs,
|
kwargs=copy_kwargs,
|
||||||
)
|
)
|
||||||
if inspect.isawaitable(cache_key):
|
if isawaitable(cache_key):
|
||||||
cache_key = await cache_key
|
cache_key = await cache_key
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -178,9 +183,7 @@ def cache(
|
|||||||
response.headers["ETag"] = etag
|
response.headers["ETag"] = etag
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
inner.__signature__ = _augment_signature(
|
inner.__signature__ = _augment_signature(wrapped_signature, *to_inject)
|
||||||
signature, request_param is None, response_param is None
|
|
||||||
)
|
|
||||||
return inner
|
return inner
|
||||||
|
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|||||||
@@ -94,3 +94,9 @@ def test_non_get() -> None:
|
|||||||
assert response.json() == {"value": 1}
|
assert response.json() == {"value": 1}
|
||||||
response = client.put("/uncached_put")
|
response = client.put("/uncached_put")
|
||||||
assert response.json() == {"value": 2}
|
assert response.json() == {"value": 2}
|
||||||
|
|
||||||
|
|
||||||
|
def test_alternate_injected_namespace() -> None:
|
||||||
|
with TestClient(app) as client:
|
||||||
|
response = client.get("/namespaced_injection")
|
||||||
|
assert response.json() == {"__fastapi_cache_request": 42, "__fastapi_cache_response": 17}
|
||||||
|
|||||||
Reference in New Issue
Block a user