mirror of
https://github.com/long2ice/fastapi-cache.git
synced 2026-03-25 13:07:53 +00:00
Merge pull request #133 from mjpieters/correct_json_decode_return
Clean up type annotations
This commit is contained in:
7
.github/workflows/ci.yml
vendored
7
.github/workflows/ci.yml
vendored
@@ -2,12 +2,17 @@ name: ci
|
|||||||
on: [ push, pull_request ]
|
on: [ push, pull_request ]
|
||||||
jobs:
|
jobs:
|
||||||
ci:
|
ci:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python: ["3.7", "3.8", "3.9", "3.10", "3.11"]
|
||||||
|
|
||||||
|
name: "Test on Python ${{ matrix.python }}"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-python@v4
|
- uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: "3.x"
|
python-version: "${{ matrix.python }}"
|
||||||
- name: Install and configure Poetry
|
- name: Install and configure Poetry
|
||||||
run: |
|
run: |
|
||||||
pip install -U pip poetry
|
pip install -U pip poetry
|
||||||
|
|||||||
2
Makefile
2
Makefile
@@ -14,6 +14,8 @@ style: deps
|
|||||||
check: deps
|
check: deps
|
||||||
@black $(checkfiles) || (echo "Please run 'make style' to auto-fix style issues" && false)
|
@black $(checkfiles) || (echo "Please run 'make style' to auto-fix style issues" && false)
|
||||||
@flake8 $(checkfiles)
|
@flake8 $(checkfiles)
|
||||||
|
@mypy ${checkfiles}
|
||||||
|
@pyright ${checkfiles}
|
||||||
|
|
||||||
test: deps
|
test: deps
|
||||||
$(py_warn) pytest
|
$(py_warn) pytest
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
# pyright: reportGeneralTypeIssues=false
|
||||||
|
from typing import Dict, Optional
|
||||||
|
|
||||||
import pendulum
|
import pendulum
|
||||||
import uvicorn
|
import uvicorn
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
@@ -84,9 +87,9 @@ app.get("/method")(cache(namespace="test")(instance.handler_method))
|
|||||||
# cache a Pydantic model instance; the return type annotation is required in this case
|
# cache a Pydantic model instance; the return type annotation is required in this case
|
||||||
class Item(BaseModel):
|
class Item(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
description: str | None = None
|
description: Optional[str] = None
|
||||||
price: float
|
price: float
|
||||||
tax: float | None = None
|
tax: Optional[float] = None
|
||||||
|
|
||||||
|
|
||||||
@app.get("/pydantic_instance")
|
@app.get("/pydantic_instance")
|
||||||
@@ -110,7 +113,7 @@ async def uncached_put():
|
|||||||
@cache(namespace="test", expire=5, injected_dependency_namespace="monty_python")
|
@cache(namespace="test", expire=5, injected_dependency_namespace="monty_python")
|
||||||
def namespaced_injection(
|
def namespaced_injection(
|
||||||
__fastapi_cache_request: int = 42, __fastapi_cache_response: int = 17
|
__fastapi_cache_request: int = 42, __fastapi_cache_response: int = 17
|
||||||
) -> dict[str, int]:
|
) -> Dict[str, int]:
|
||||||
return {
|
return {
|
||||||
"__fastapi_cache_request": __fastapi_cache_request,
|
"__fastapi_cache_request": __fastapi_cache_request,
|
||||||
"__fastapi_cache_response": __fastapi_cache_response,
|
"__fastapi_cache_response": __fastapi_cache_response,
|
||||||
@@ -123,4 +126,4 @@ async def startup():
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
uvicorn.run("main:app", debug=True, reload=True)
|
uvicorn.run("main:app", reload=True)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# pyright: reportGeneralTypeIssues=false
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import pendulum
|
import pendulum
|
||||||
@@ -87,4 +88,4 @@ async def startup():
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
uvicorn.run("main:app", debug=True, reload=True)
|
uvicorn.run("main:app", reload=True)
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
import datetime
|
import datetime
|
||||||
from typing import Optional, Tuple
|
from typing import TYPE_CHECKING, Optional, Tuple
|
||||||
|
|
||||||
from aiobotocore.client import AioBaseClient
|
from aiobotocore.client import AioBaseClient
|
||||||
from aiobotocore.session import get_session, AioSession
|
from aiobotocore.session import AioSession, get_session
|
||||||
|
|
||||||
from fastapi_cache.backends import Backend
|
from fastapi_cache.backends import Backend
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from types_aiobotocore_dynamodb import DynamoDBClient
|
||||||
|
else:
|
||||||
|
DynamoDBClient = AioBaseClient
|
||||||
|
|
||||||
|
|
||||||
class DynamoBackend(Backend):
|
class DynamoBackend(Backend):
|
||||||
"""
|
"""
|
||||||
@@ -25,14 +30,18 @@ class DynamoBackend(Backend):
|
|||||||
>> FastAPICache.init(dynamodb)
|
>> FastAPICache.init(dynamodb)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
client: DynamoDBClient
|
||||||
|
session: AioSession
|
||||||
|
table_name: str
|
||||||
|
region: Optional[str]
|
||||||
|
|
||||||
def __init__(self, table_name: str, region: Optional[str] = None) -> None:
|
def __init__(self, table_name: str, region: Optional[str] = None) -> None:
|
||||||
self.session: AioSession = get_session()
|
self.session: AioSession = get_session()
|
||||||
self.client: Optional[AioBaseClient] = None # Needs async init
|
|
||||||
self.table_name = table_name
|
self.table_name = table_name
|
||||||
self.region = region
|
self.region = region
|
||||||
|
|
||||||
async def init(self) -> None:
|
async def init(self) -> None:
|
||||||
self.client = await self.session.create_client(
|
self.client = await self.session.create_client( # pyright: ignore[reportUnknownMemberType]
|
||||||
"dynamodb", region_name=self.region
|
"dynamodb", region_name=self.region
|
||||||
).__aenter__()
|
).__aenter__()
|
||||||
|
|
||||||
@@ -60,6 +69,7 @@ class DynamoBackend(Backend):
|
|||||||
response = await self.client.get_item(TableName=self.table_name, Key={"key": {"S": key}})
|
response = await self.client.get_item(TableName=self.table_name, Key={"key": {"S": key}})
|
||||||
if "Item" in response:
|
if "Item" in response:
|
||||||
return response["Item"].get("value", {}).get("B")
|
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: Optional[int] = None) -> None:
|
||||||
ttl = (
|
ttl = (
|
||||||
|
|||||||
@@ -13,18 +13,18 @@ class RedisBackend(Backend):
|
|||||||
|
|
||||||
async def get_with_ttl(self, key: str) -> Tuple[int, Optional[bytes]]:
|
async def get_with_ttl(self, key: str) -> Tuple[int, Optional[bytes]]:
|
||||||
async with self.redis.pipeline(transaction=not self.is_cluster) as pipe:
|
async with self.redis.pipeline(transaction=not self.is_cluster) as pipe:
|
||||||
return await pipe.ttl(key).get(key).execute()
|
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) -> Optional[bytes]:
|
||||||
return await self.redis.get(key)
|
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: Optional[int] = None) -> None:
|
||||||
return await self.redis.set(key, value, ex=expire)
|
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: Optional[str] = None, key: Optional[str] = None) -> int:
|
||||||
if namespace:
|
if namespace:
|
||||||
lua = f"for i, name in ipairs(redis.call('KEYS', '{namespace}:*')) do redis.call('DEL', name); end"
|
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)
|
return await self.redis.eval(lua, numkeys=0) # type: ignore[union-attr,no-any-return]
|
||||||
elif key:
|
elif key:
|
||||||
return await self.redis.delete(key)
|
return await self.redis.delete(key) # type: ignore[union-attr]
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@@ -8,28 +8,31 @@ import pendulum
|
|||||||
from fastapi.encoders import jsonable_encoder
|
from fastapi.encoders import jsonable_encoder
|
||||||
from pydantic import BaseConfig, ValidationError, fields
|
from pydantic import BaseConfig, ValidationError, fields
|
||||||
from starlette.responses import JSONResponse
|
from starlette.responses import JSONResponse
|
||||||
from starlette.templating import _TemplateResponse as TemplateResponse
|
from starlette.templating import (
|
||||||
|
_TemplateResponse as TemplateResponse, # pyright: ignore[reportPrivateUsage]
|
||||||
|
)
|
||||||
|
|
||||||
_T = TypeVar("_T")
|
_T = TypeVar("_T", bound=type)
|
||||||
|
|
||||||
|
|
||||||
CONVERTERS: dict[str, Callable[[str], Any]] = {
|
CONVERTERS: Dict[str, Callable[[str], Any]] = {
|
||||||
"date": lambda x: pendulum.parse(x, exact=True),
|
# Pendulum 3.0.0 adds parse to __all__, at which point these ignores can be removed
|
||||||
"datetime": lambda x: pendulum.parse(x, exact=True),
|
"date": lambda x: pendulum.parse(x, exact=True), # type: ignore[attr-defined]
|
||||||
|
"datetime": lambda x: pendulum.parse(x, exact=True), # type: ignore[attr-defined]
|
||||||
"decimal": Decimal,
|
"decimal": Decimal,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class JsonEncoder(json.JSONEncoder):
|
class JsonEncoder(json.JSONEncoder):
|
||||||
def default(self, obj: Any) -> Any:
|
def default(self, o: Any) -> Any:
|
||||||
if isinstance(obj, datetime.datetime):
|
if isinstance(o, datetime.datetime):
|
||||||
return {"val": str(obj), "_spec_type": "datetime"}
|
return {"val": str(o), "_spec_type": "datetime"}
|
||||||
elif isinstance(obj, datetime.date):
|
elif isinstance(o, datetime.date):
|
||||||
return {"val": str(obj), "_spec_type": "date"}
|
return {"val": str(o), "_spec_type": "date"}
|
||||||
elif isinstance(obj, Decimal):
|
elif isinstance(o, Decimal):
|
||||||
return {"val": str(obj), "_spec_type": "decimal"}
|
return {"val": str(o), "_spec_type": "decimal"}
|
||||||
else:
|
else:
|
||||||
return jsonable_encoder(obj)
|
return jsonable_encoder(o)
|
||||||
|
|
||||||
|
|
||||||
def object_hook(obj: Any) -> Any:
|
def object_hook(obj: Any) -> Any:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import logging
|
|||||||
import sys
|
import sys
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from inspect import Parameter, Signature, isawaitable, iscoroutinefunction
|
from inspect import Parameter, Signature, isawaitable, iscoroutinefunction
|
||||||
from typing import Awaitable, Callable, Optional, Type, TypeVar
|
from typing import Awaitable, Callable, List, Optional, Type, TypeVar, Union, cast
|
||||||
|
|
||||||
if sys.version_info >= (3, 10):
|
if sys.version_info >= (3, 10):
|
||||||
from typing import ParamSpec
|
from typing import ParamSpec
|
||||||
@@ -29,14 +29,14 @@ def _augment_signature(signature: Signature, *extra: Parameter) -> Signature:
|
|||||||
return signature
|
return signature
|
||||||
|
|
||||||
parameters = list(signature.parameters.values())
|
parameters = list(signature.parameters.values())
|
||||||
variadic_keyword_params = []
|
variadic_keyword_params: List[Parameter] = []
|
||||||
while parameters and parameters[-1].kind is 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())
|
||||||
|
|
||||||
return signature.replace(parameters=[*parameters, *extra, *variadic_keyword_params])
|
return signature.replace(parameters=[*parameters, *extra, *variadic_keyword_params])
|
||||||
|
|
||||||
|
|
||||||
def _locate_param(sig: Signature, dep: Parameter, to_inject: list[Parameter]) -> Parameter:
|
def _locate_param(sig: Signature, dep: Parameter, to_inject: List[Parameter]) -> Parameter:
|
||||||
"""Locate an existing parameter in the decorated endpoint
|
"""Locate an existing parameter in the decorated endpoint
|
||||||
|
|
||||||
If not found, returns the injectable parameter, and adds it to the to_inject list.
|
If not found, returns the injectable parameter, and adds it to the to_inject list.
|
||||||
@@ -56,9 +56,9 @@ def cache(
|
|||||||
expire: Optional[int] = None,
|
expire: Optional[int] = None,
|
||||||
coder: Optional[Type[Coder]] = None,
|
coder: Optional[Type[Coder]] = None,
|
||||||
key_builder: Optional[KeyBuilder] = None,
|
key_builder: Optional[KeyBuilder] = None,
|
||||||
namespace: Optional[str] = "",
|
namespace: str = "",
|
||||||
injected_dependency_namespace: str = "__fastapi_cache",
|
injected_dependency_namespace: str = "__fastapi_cache",
|
||||||
) -> Callable[[Callable[P, Awaitable[R]]], Callable[P, Awaitable[R]]]:
|
) -> Callable[[Callable[P, Awaitable[R]]], Callable[P, Awaitable[Union[R, Response]]]]:
|
||||||
"""
|
"""
|
||||||
cache all function
|
cache all function
|
||||||
:param namespace:
|
:param namespace:
|
||||||
@@ -80,16 +80,16 @@ def cache(
|
|||||||
kind=Parameter.KEYWORD_ONLY,
|
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[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)
|
||||||
to_inject: list[Parameter] = []
|
to_inject: List[Parameter] = []
|
||||||
request_param = _locate_param(wrapped_signature, injected_request, to_inject)
|
request_param = _locate_param(wrapped_signature, injected_request, to_inject)
|
||||||
response_param = _locate_param(wrapped_signature, injected_response, to_inject)
|
response_param = _locate_param(wrapped_signature, injected_response, to_inject)
|
||||||
return_type = get_typed_return_annotation(func)
|
return_type = get_typed_return_annotation(func)
|
||||||
|
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
async def inner(*args: P.args, **kwargs: P.kwargs) -> R:
|
async def inner(*args: P.args, **kwargs: P.kwargs) -> Union[R, Response]:
|
||||||
nonlocal coder
|
nonlocal coder
|
||||||
nonlocal expire
|
nonlocal expire
|
||||||
nonlocal key_builder
|
nonlocal key_builder
|
||||||
@@ -111,11 +111,11 @@ def cache(
|
|||||||
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.
|
||||||
return await run_in_threadpool(func, *args, **kwargs)
|
return await run_in_threadpool(func, *args, **kwargs) # type: ignore[arg-type]
|
||||||
|
|
||||||
copy_kwargs = kwargs.copy()
|
copy_kwargs = kwargs.copy()
|
||||||
request: Optional[Request] = copy_kwargs.pop(request_param.name, None)
|
request: Optional[Request] = copy_kwargs.pop(request_param.name, None) # type: ignore[assignment]
|
||||||
response: Optional[Response] = copy_kwargs.pop(response_param.name, None)
|
response: Optional[Response] = copy_kwargs.pop(response_param.name, None) # type: ignore[assignment]
|
||||||
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():
|
||||||
@@ -137,17 +137,18 @@ def cache(
|
|||||||
)
|
)
|
||||||
if isawaitable(cache_key):
|
if isawaitable(cache_key):
|
||||||
cache_key = await cache_key
|
cache_key = await cache_key
|
||||||
|
assert isinstance(cache_key, str)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ttl, ret = await backend.get_with_ttl(cache_key)
|
ttl, cached = await backend.get_with_ttl(cache_key)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"Error retrieving cache key '{cache_key}' from backend:", exc_info=True
|
f"Error retrieving cache key '{cache_key}' from backend:", exc_info=True
|
||||||
)
|
)
|
||||||
ttl, ret = 0, None
|
ttl, cached = 0, None
|
||||||
if not request:
|
if not request:
|
||||||
if ret is not None:
|
if cached is not None:
|
||||||
return coder.decode_as_type(ret, type_=return_type)
|
return cast(R, coder.decode_as_type(cached, type_=return_type))
|
||||||
ret = await ensure_async_func(*args, **kwargs)
|
ret = await ensure_async_func(*args, **kwargs)
|
||||||
try:
|
try:
|
||||||
await backend.set(cache_key, coder.encode(ret), expire)
|
await backend.set(cache_key, coder.encode(ret), expire)
|
||||||
@@ -161,15 +162,15 @@ def cache(
|
|||||||
return await ensure_async_func(*args, **kwargs)
|
return await ensure_async_func(*args, **kwargs)
|
||||||
|
|
||||||
if_none_match = request.headers.get("if-none-match")
|
if_none_match = request.headers.get("if-none-match")
|
||||||
if ret is not None:
|
if cached is not None:
|
||||||
if response:
|
if response:
|
||||||
response.headers["Cache-Control"] = f"max-age={ttl}"
|
response.headers["Cache-Control"] = f"max-age={ttl}"
|
||||||
etag = f"W/{hash(ret)}"
|
etag = f"W/{hash(cached)}"
|
||||||
if if_none_match == etag:
|
if if_none_match == etag:
|
||||||
response.status_code = 304
|
response.status_code = 304
|
||||||
return response
|
return response
|
||||||
response.headers["ETag"] = etag
|
response.headers["ETag"] = etag
|
||||||
return coder.decode_as_type(ret, type_=return_type)
|
return cast(R, coder.decode_as_type(cached, type_=return_type))
|
||||||
|
|
||||||
ret = await ensure_async_func(*args, **kwargs)
|
ret = await ensure_async_func(*args, **kwargs)
|
||||||
encoded_ret = coder.encode(ret)
|
encoded_ret = coder.encode(ret)
|
||||||
@@ -179,12 +180,13 @@ def cache(
|
|||||||
except Exception:
|
except Exception:
|
||||||
logger.warning(f"Error setting cache key '{cache_key}' in backend:", exc_info=True)
|
logger.warning(f"Error setting cache key '{cache_key}' in backend:", exc_info=True)
|
||||||
|
|
||||||
|
if response:
|
||||||
response.headers["Cache-Control"] = f"max-age={expire}"
|
response.headers["Cache-Control"] = f"max-age={expire}"
|
||||||
etag = f"W/{hash(encoded_ret)}"
|
etag = f"W/{hash(encoded_ret)}"
|
||||||
response.headers["ETag"] = etag
|
response.headers["ETag"] = etag
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
inner.__signature__ = _augment_signature(wrapped_signature, *to_inject)
|
inner.__signature__ = _augment_signature(wrapped_signature, *to_inject) # type: ignore[attr-defined]
|
||||||
return inner
|
return inner
|
||||||
|
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import hashlib
|
import hashlib
|
||||||
from typing import Any, Callable, Optional
|
from typing import Any, Callable, Dict, Optional, Tuple
|
||||||
|
|
||||||
from starlette.requests import Request
|
from starlette.requests import Request
|
||||||
from starlette.responses import Response
|
from starlette.responses import Response
|
||||||
@@ -8,10 +8,11 @@ from starlette.responses import Response
|
|||||||
def default_key_builder(
|
def default_key_builder(
|
||||||
func: Callable[..., Any],
|
func: Callable[..., Any],
|
||||||
namespace: str = "",
|
namespace: str = "",
|
||||||
|
*,
|
||||||
request: Optional[Request] = None,
|
request: Optional[Request] = None,
|
||||||
response: Optional[Response] = None,
|
response: Optional[Response] = None,
|
||||||
args: Optional[tuple[Any, ...]] = None,
|
args: Tuple[Any, ...],
|
||||||
kwargs: Optional[dict[str, Any]] = None,
|
kwargs: Dict[str, Any],
|
||||||
) -> str:
|
) -> str:
|
||||||
cache_key = hashlib.md5( # nosec:B303
|
cache_key = hashlib.md5( # nosec:B303
|
||||||
f"{func.__module__}:{func.__name__}:{args}:{kwargs}".encode()
|
f"{func.__module__}:{func.__name__}:{args}:{kwargs}".encode()
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
from typing import Any, Awaitable, Callable, Optional, Protocol, Union
|
from typing import Any, Awaitable, Callable, Dict, Optional, Tuple, Union
|
||||||
|
|
||||||
from starlette.requests import Request
|
from starlette.requests import Request
|
||||||
from starlette.responses import Response
|
from starlette.responses import Response
|
||||||
|
from typing_extensions import Protocol
|
||||||
|
|
||||||
|
|
||||||
_Func = Callable[..., Any]
|
_Func = Callable[..., Any]
|
||||||
@@ -10,12 +11,12 @@ _Func = Callable[..., Any]
|
|||||||
class KeyBuilder(Protocol):
|
class KeyBuilder(Protocol):
|
||||||
def __call__(
|
def __call__(
|
||||||
self,
|
self,
|
||||||
_function: _Func,
|
__function: _Func,
|
||||||
_namespace: str = ...,
|
__namespace: str = ...,
|
||||||
*,
|
*,
|
||||||
request: Optional[Request] = ...,
|
request: Optional[Request] = ...,
|
||||||
response: Optional[Response] = ...,
|
response: Optional[Response] = ...,
|
||||||
args: tuple[Any, ...] = ...,
|
args: Tuple[Any, ...],
|
||||||
kwargs: dict[str, Any] = ...,
|
kwargs: Dict[str, Any],
|
||||||
) -> Union[Awaitable[str], str]:
|
) -> Union[Awaitable[str], str]:
|
||||||
...
|
...
|
||||||
|
|||||||
691
poetry.lock
generated
691
poetry.lock
generated
File diff suppressed because one or more lines are too long
@@ -22,7 +22,7 @@ redis = { version = "^4.2.0rc1", optional = true }
|
|||||||
aiomcache = { version = "*", optional = true }
|
aiomcache = { version = "*", optional = true }
|
||||||
pendulum = "*"
|
pendulum = "*"
|
||||||
aiobotocore = { version = "^1.4.1", optional = true }
|
aiobotocore = { version = "^1.4.1", optional = true }
|
||||||
typing-extensions = { version = ">=4.1.0", markers = "python_version < \"3.10\"" }
|
typing-extensions = { version = ">=4.1.0" }
|
||||||
aiohttp = { version = ">=3.8.3", markers = "python_version >= \"3.11\"" }
|
aiohttp = { version = ">=3.8.3", markers = "python_version >= \"3.11\"" }
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
@@ -33,6 +33,10 @@ pytest = "*"
|
|||||||
requests = "*"
|
requests = "*"
|
||||||
coverage = "^6.5.0"
|
coverage = "^6.5.0"
|
||||||
httpx = "*"
|
httpx = "*"
|
||||||
|
mypy = "^1.2.0"
|
||||||
|
types-redis = "^4.5.4.2"
|
||||||
|
pyright = "^1.1.306"
|
||||||
|
types-aiobotocore = { extras = ["dynamodb"], version = "^2.5.0.post2" }
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry>=0.12"]
|
requires = ["poetry>=0.12"]
|
||||||
@@ -47,3 +51,31 @@ all = ["redis", "aiomcache", "aiobotocore"]
|
|||||||
[tool.black]
|
[tool.black]
|
||||||
line-length = 100
|
line-length = 100
|
||||||
target-version = ['py36', 'py37', 'py38', 'py39']
|
target-version = ['py36', 'py37', 'py38', 'py39']
|
||||||
|
|
||||||
|
[tool.mypy]
|
||||||
|
files = ["fastapi_cache", "examples", "tests"]
|
||||||
|
python_version = "3.7"
|
||||||
|
# equivalent of --strict
|
||||||
|
warn_unused_configs = true
|
||||||
|
disallow_any_generics = true
|
||||||
|
disallow_subclassing_any = true
|
||||||
|
disallow_untyped_calls = true
|
||||||
|
disallow_untyped_defs = true
|
||||||
|
disallow_incomplete_defs = true
|
||||||
|
check_untyped_defs = true
|
||||||
|
disallow_untyped_decorators = true
|
||||||
|
warn_redundant_casts = true
|
||||||
|
warn_unused_ignores = true
|
||||||
|
warn_return_any = true
|
||||||
|
no_implicit_reexport = true
|
||||||
|
strict_equality = true
|
||||||
|
strict_concatenate = true
|
||||||
|
|
||||||
|
[[tool.mypy.overrides]]
|
||||||
|
module = "examples.*.main"
|
||||||
|
ignore_errors = true
|
||||||
|
|
||||||
|
[tool.pyright]
|
||||||
|
include = ["fastapi_cache", "tests", "examples"]
|
||||||
|
strict = ["fastapi_cache", "tests"]
|
||||||
|
pythonVersion = "3.7"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional, Tuple, Type
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from pydantic import BaseModel, ValidationError
|
from pydantic import BaseModel, ValidationError
|
||||||
@@ -46,14 +46,14 @@ def test_pickle_coder(value: Any) -> None:
|
|||||||
[
|
[
|
||||||
(1, None),
|
(1, None),
|
||||||
("some_string", None),
|
("some_string", None),
|
||||||
((1, 2), tuple[int, int]),
|
((1, 2), Tuple[int, int]),
|
||||||
([1, 2, 3], None),
|
([1, 2, 3], None),
|
||||||
({"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) -> None:
|
def test_json_coder(value: Any, return_type: Type[Any]) -> None:
|
||||||
encoded_value = JsonCoder.encode(value)
|
encoded_value = JsonCoder.encode(value)
|
||||||
assert isinstance(encoded_value, bytes)
|
assert isinstance(encoded_value, bytes)
|
||||||
decoded_value = JsonCoder.decode_as_type(encoded_value, type_=return_type)
|
decoded_value = JsonCoder.decode_as_type(encoded_value, type_=return_type)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import time
|
import time
|
||||||
from typing import Generator
|
from typing import Generator, Any
|
||||||
|
|
||||||
import pendulum
|
import pendulum
|
||||||
import pytest
|
import pytest
|
||||||
@@ -11,7 +11,7 @@ from fastapi_cache.backends.inmemory import InMemoryBackend
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def init_cache() -> Generator:
|
def init_cache() -> Generator[Any, Any, None]:
|
||||||
FastAPICache.init(InMemoryBackend())
|
FastAPICache.init(InMemoryBackend())
|
||||||
yield
|
yield
|
||||||
FastAPICache.reset()
|
FastAPICache.reset()
|
||||||
@@ -21,34 +21,34 @@ def test_datetime() -> None:
|
|||||||
with TestClient(app) as client:
|
with TestClient(app) as client:
|
||||||
response = client.get("/datetime")
|
response = client.get("/datetime")
|
||||||
now = response.json().get("now")
|
now = response.json().get("now")
|
||||||
now_ = pendulum.now().replace(microsecond=0)
|
now_ = pendulum.now().replace(microsecond=0) # type: ignore[no-untyped-call]
|
||||||
assert pendulum.parse(now).replace(microsecond=0) == now_
|
assert pendulum.parse(now).replace(microsecond=0) == now_ # type: ignore[attr-defined]
|
||||||
response = client.get("/datetime")
|
response = client.get("/datetime")
|
||||||
now = response.json().get("now")
|
now = response.json().get("now")
|
||||||
assert pendulum.parse(now).replace(microsecond=0) == now_
|
assert pendulum.parse(now).replace(microsecond=0) == now_ # type: ignore[attr-defined]
|
||||||
time.sleep(3)
|
time.sleep(3)
|
||||||
response = client.get("/datetime")
|
response = client.get("/datetime")
|
||||||
now = response.json().get("now")
|
now = response.json().get("now")
|
||||||
now = pendulum.parse(now).replace(microsecond=0)
|
now = pendulum.parse(now).replace(microsecond=0) # type: ignore[attr-defined]
|
||||||
assert now != now_
|
assert now != now_
|
||||||
assert now == pendulum.now().replace(microsecond=0)
|
assert now == pendulum.now().replace(microsecond=0) # type: ignore[no-untyped-call]
|
||||||
|
|
||||||
|
|
||||||
def test_date() -> None:
|
def test_date() -> None:
|
||||||
"""Test path function without request or response arguments."""
|
"""Test path function without request or response arguments."""
|
||||||
with TestClient(app) as client:
|
with TestClient(app) as client:
|
||||||
response = client.get("/date")
|
response = client.get("/date")
|
||||||
assert pendulum.parse(response.json()) == pendulum.today()
|
assert pendulum.parse(response.json()) == pendulum.today() # type: ignore[attr-defined]
|
||||||
|
|
||||||
# do it again to test cache
|
# do it again to test cache
|
||||||
response = client.get("/date")
|
response = client.get("/date")
|
||||||
assert pendulum.parse(response.json()) == pendulum.today()
|
assert pendulum.parse(response.json()) == pendulum.today() # type: ignore[attr-defined]
|
||||||
|
|
||||||
# now test with cache disabled, as that's a separate code path
|
# now test with cache disabled, as that's a separate code path
|
||||||
FastAPICache._enable = False
|
FastAPICache._enable = False # pyright: ignore[reportPrivateUsage]
|
||||||
response = client.get("/date")
|
response = client.get("/date")
|
||||||
assert pendulum.parse(response.json()) == pendulum.today()
|
assert pendulum.parse(response.json()) == pendulum.today() # type: ignore[attr-defined]
|
||||||
FastAPICache._enable = True
|
FastAPICache._enable = True # pyright: ignore[reportPrivateUsage]
|
||||||
|
|
||||||
|
|
||||||
def test_sync() -> None:
|
def test_sync() -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user