Merge branch 'master' into master

This commit is contained in:
Lucca Marques
2022-08-08 12:40:33 -03:00
committed by GitHub
18 changed files with 996 additions and 459 deletions

2
.github/FUNDING.yml vendored
View File

@@ -1,2 +1,2 @@
custom: ["https://sponsor.long2ice.cn"]
custom: ["https://sponsor.long2ice.io"]

View File

@@ -8,9 +8,8 @@ jobs:
- uses: actions/setup-python@v2
with:
python-version: "3.x"
- name: Install and configure Poetry
uses: snok/install-poetry@v1.1.1
with:
virtualenvs-create: false
- uses: abatilo/actions-poetry@v2.1.3
- name: Config poetry
run: poetry config experimental.new-installer false
- name: CI
run: make ci

View File

@@ -11,10 +11,9 @@ jobs:
- uses: actions/setup-python@v1
with:
python-version: '3.x'
- name: Install and configure Poetry
uses: snok/install-poetry@v1.1.1
with:
virtualenvs-create: false
- uses: abatilo/actions-poetry@v2.1.3
- name: Config poetry
run: poetry config experimental.new-installer false
- name: Build dists
run: make build
- name: Pypi Publish

View File

@@ -2,6 +2,19 @@
## 0.1
### 0.1.9
- Replace `aioredis` with `redis-py`.
### 0.1.8
- Support `dynamodb` backend.
### 0.1.7
- Fix default json coder for datetime.
- Add `enable` param to `init`.
### 0.1.6
- Fix redis cache.

View File

@@ -1,19 +1,6 @@
checkfiles = fastapi_cache/ examples/ tests/
black_opts = -l 100 -t py38
py_warn = PYTHONDEVMODE=1
help:
@echo "FastAPI-Cache development makefile"
@echo
@echo "usage: make <target>"
@echo "Targets:"
@echo " up Ensure dev/test dependencies are updated"
@echo " deps Ensure dev/test dependencies are installed"
@echo " check Checks that build is sane"
@echo " test Runs all tests"
@echo " style Auto-formats the code"
@echo " build Build package"
up:
@poetry update
@@ -21,16 +8,15 @@ deps:
@poetry install --no-root -E all
style: deps
@isort -src $(checkfiles)
@black $(black_opts) $(checkfiles)
@poetry run isort -src $(checkfiles)
@poetry run black $(checkfiles)
check: deps
@black --check $(black_opts) $(checkfiles) || (echo "Please run 'make style' to auto-fix style issues" && false)
@flake8 $(checkfiles)
@bandit -r $(checkfiles)
@poetry run black $(checkfiles) || (echo "Please run 'make style' to auto-fix style issues" && false)
@poetry run flake8 $(checkfiles)
test: deps
$(py_warn) pytest
$(py_warn) poetry run pytest
build: clean deps
@poetry build

View File

@@ -7,11 +7,11 @@
## Introduction
`fastapi-cache` is a tool to cache fastapi response and function result, with backends support `redis` and `memcache`.
`fastapi-cache` is a tool to cache fastapi response and function result, with backends support `redis`, `memcache`, and `dynamodb`.
## Features
- Support `redis` and `memcache` and `in-memory` backends.
- Support `redis`, `memcache`, `dynamodb`, and `in-memory` backends.
- Easily integration with `fastapi`.
- Support http cache like `ETag` and `Cache-Control`.
@@ -20,6 +20,7 @@
- `asyncio` environment.
- `redis` if use `RedisBackend`.
- `memcache` if use `MemcacheBackend`.
- `aiobotocore` if use `DynamoBackend`.
## Install
@@ -30,13 +31,19 @@
or
```shell
> pip install fastapi-cache2[redis]
> pip install "fastapi-cache2[redis]"
```
or
```shell
> pip install fastapi-cache2[memcache]
> pip install "fastapi-cache2[memcache]"
```
or
```shell
> pip install "fastapi-cache2[dynamodb]"
```
## Usage

View File

View File

@@ -1,11 +1,12 @@
import aioredis
from datetime import date, datetime
import uvicorn
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
from fastapi_cache.backends.inmemory import InMemoryBackend
from fastapi_cache.decorator import cache
app = FastAPI()
@@ -31,10 +32,21 @@ async def clear():
return await FastAPICache.clear(namespace="test")
@app.get("/date")
@cache(namespace="test", expire=20)
async def get_data(request: Request, response: Response):
return date.today()
@app.get("/datetime")
@cache(namespace="test", expire=20)
async def get_datetime(request: Request, response: Response):
return datetime.now()
@app.on_event("startup")
async def startup():
redis = aioredis.from_url(url="redis://localhost")
FastAPICache.init(RedisBackend(redis), prefix="fastapi-cache")
FastAPICache.init(InMemoryBackend())
if __name__ == "__main__":

View File

65
examples/redis/main.py Normal file
View File

@@ -0,0 +1,65 @@
from datetime import date, datetime
import time
import redis.asyncio as redis
import uvicorn
from fastapi import FastAPI
from redis.asyncio.connection import ConnectionPool
from starlette.requests import Request
from starlette.responses import Response
from fastapi_cache import FastAPICache
from fastapi_cache.backends.redis import RedisBackend
from fastapi_cache.decorator import cache
app = FastAPI()
ret = 0
@cache(namespace="test", expire=1)
async def get_ret():
global ret
ret = ret + 1
return ret
@app.get("/")
@cache(namespace="test", expire=20)
async def index(request: Request, response: Response):
return dict(ret=await get_ret())
@app.get("/clear")
async def clear():
return await FastAPICache.clear(namespace="test")
@app.get("/date")
@cache(namespace="test", expire=20)
async def get_data(request: Request, response: Response):
return date.today()
@app.get("/blocking")
@cache(namespace="test", expire=20)
def blocking(request: Request, response: Response):
time.sleep(5)
return dict(ret=get_ret())
@app.get("/datetime")
@cache(namespace="test", expire=20)
async def get_datetime(request: Request, response: Response):
return datetime.now()
@app.on_event("startup")
async def startup():
pool = ConnectionPool.from_url(url="redis://localhost")
r = redis.Redis(connection_pool=pool)
FastAPICache.init(RedisBackend(r), prefix="fastapi-cache")
if __name__ == "__main__":
uvicorn.run("main:app", debug=True, reload=True)

View File

@@ -11,6 +11,7 @@ class FastAPICache:
_init = False
_coder = None
_key_builder = None
_enable = True
@classmethod
def init(
@@ -20,6 +21,7 @@ class FastAPICache:
expire: int = None,
coder: Coder = JsonCoder,
key_builder: Callable = default_key_builder,
enable: bool = True,
):
if cls._init:
return
@@ -29,6 +31,7 @@ class FastAPICache:
cls._expire = expire
cls._coder = coder
cls._key_builder = key_builder
cls._enable = enable
@classmethod
def get_backend(cls):
@@ -51,6 +54,10 @@ class FastAPICache:
def get_key_builder(cls):
return cls._key_builder
@classmethod
def get_enable(cls):
return cls._enable
@classmethod
async def clear(cls, namespace: str = None, key: str = None):
namespace = cls._prefix + ":" + namespace if namespace else None

View File

@@ -0,0 +1,92 @@
import datetime
from typing import Tuple
from aiobotocore.session import get_session
from fastapi_cache.backends import Backend
class DynamoBackend(Backend):
"""
Amazon DynamoDB backend provider
This backend requires an existing table within your AWS environment to be passed during
backend init. If ttl is going to be used, this needs to be manually enabled on the table
using the `ttl` key. Dynamo will take care of deleting outdated objects, but this is not
instant so don't be alarmed when they linger around for a bit.
As with all AWS clients, credentials will be taken from the environment. Check the AWS SDK
for more information.
Usage:
>> dynamodb = DynamoBackend(table_name="your-cache", region="eu-west-1")
>> await dynamodb.init()
>> FastAPICache.init(dynamodb)
"""
def __init__(self, table_name, region=None):
self.session = get_session()
self.client = None # Needs async init
self.table_name = table_name
self.region = region
async def init(self):
self.client = await self.session.create_client(
"dynamodb", region_name=self.region
).__aenter__()
async def close(self):
self.client = await self.client.__aexit__(None, None, None)
async def get_with_ttl(self, key: str) -> Tuple[int, str]:
response = await self.client.get_item(TableName=self.table_name, Key={"key": {"S": key}})
if "Item" in response:
value = response["Item"].get("value", {}).get("S")
ttl = response["Item"].get("ttl", {}).get("N")
if not ttl:
return -1, value
# It's only eventually consistent so we need to check ourselves
expire = int(ttl) - int(datetime.datetime.now().timestamp())
if expire > 0:
return expire, value
return 0, None
async def get(self, key) -> str:
response = await self.client.get_item(TableName=self.table_name, Key={"key": {"S": key}})
if "Item" in response:
return response["Item"].get("value", {}).get("S")
async def set(self, key: str, value: str, expire: int = None):
ttl = (
{
"ttl": {
"N": str(
int(
(
datetime.datetime.now() + datetime.timedelta(seconds=expire)
).timestamp()
)
)
}
}
if expire
else {}
)
await self.client.put_item(
TableName=self.table_name,
Item={
**{
"key": {"S": key},
"value": {"S": value},
},
**ttl,
},
)
async def clear(self, namespace: str = None, key: str = None) -> int:
raise NotImplementedError

View File

@@ -43,7 +43,7 @@ class InMemoryBackend(Backend):
async def set(self, key: str, value: str, expire: int = None):
async with self._lock:
self._store[key] = Value(value, self._now + expire)
self._store[key] = Value(value, self._now + (expire or 0))
async def clear(self, namespace: str = None, key: str = None) -> int:
count = 0

View File

@@ -1,6 +1,6 @@
from typing import Tuple
from aioredis import Redis
from redis.asyncio.client import Redis
from fastapi_cache.backends import Backend

View File

@@ -4,12 +4,12 @@ import pickle # nosec:B403
from decimal import Decimal
from typing import Any
import dateutil.parser
import pendulum
from fastapi.encoders import jsonable_encoder
CONVERTERS = {
"date": dateutil.parser.parse,
"datetime": dateutil.parser.parse,
"date": lambda x: pendulum.parse(x, exact=True),
"datetime": lambda x: pendulum.parse(x, exact=True),
"decimal": Decimal,
}
@@ -17,12 +17,9 @@ CONVERTERS = {
class JsonEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, datetime.datetime):
if obj.tzinfo:
return {"val": obj.strftime("%Y-%m-%d %H:%M:%S%z"), "_spec_type": "datetime"}
else:
return {"val": obj.strftime("%Y-%m-%d %H:%M:%S"), "_spec_type": "datetime"}
return {"val": str(obj), "_spec_type": "datetime"}
elif isinstance(obj, datetime.date):
return {"val": obj.strftime("%Y-%m-%d"), "_spec_type": "date"}
return {"val": str(obj), "_spec_type": "date"}
elif isinstance(obj, Decimal):
return {"val": str(obj), "_spec_type": "decimal"}
else:
@@ -67,4 +64,4 @@ class PickleCoder(Coder):
@classmethod
def decode(cls, value: Any):
return pickle.loads(value) # nosec:B403
return pickle.loads(value) # nosec:B403,B301

View File

@@ -1,15 +1,21 @@
from functools import wraps
from typing import Callable, Optional, Type
import asyncio
from functools import wraps, partial
import inspect
from typing import TYPE_CHECKING, Callable, Optional, Type
from fastapi_cache import FastAPICache
from fastapi_cache.coder import Coder
if TYPE_CHECKING:
import concurrent.futures
def cache(
expire: int = None,
coder: Type[Coder] = None,
key_builder: Callable = None,
namespace: Optional[str] = "",
executor: Optional["concurrent.futures.Executor"] = None,
):
"""
cache all function
@@ -17,6 +23,8 @@ def cache(
:param expire:
:param coder:
:param key_builder:
:param executor:
:return:
"""
@@ -29,7 +37,10 @@ def cache(
copy_kwargs = kwargs.copy()
request = copy_kwargs.pop("request", None)
response = copy_kwargs.pop("response", None)
if request and request.headers.get("Cache-Control") in ("no-store", "no-cache"):
if (
request and request.headers.get("Cache-Control") in ("no-store", "no-cache")
) or not FastAPICache.get_enable():
return await func(*args, **kwargs)
coder = coder or FastAPICache.get_coder()
@@ -61,7 +72,12 @@ def cache(
response.headers["ETag"] = etag
return coder.decode(ret)
if inspect.iscoroutinefunction(func):
ret = await func(*args, **kwargs)
else:
loop = asyncio.get_event_loop()
ret = await loop.run_in_executor(executor, partial(func, *args, **kwargs))
await backend.set(cache_key, coder.encode(ret), expire or FastAPICache.get_expire())
return ret

1139
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "fastapi-cache2"
version = "0.1.6"
version = "0.1.9"
description = "Cache for FastAPI"
authors = ["long2ice <long2ice@gmail.com>"]
license = "Apache-2.0"
@@ -18,22 +18,27 @@ include = ["LICENSE", "README.md"]
python = "^3.7"
fastapi = "*"
uvicorn = "*"
aioredis = {version = ">=2.0.0b1", optional = true}
redis = { version = "^4.2.0rc1", optional = true }
aiomcache = { version = "*", optional = true }
python-dateutil = "*"
pendulum = "*"
aiobotocore = { version = "^1.4.1", optional = true }
[tool.poetry.dev-dependencies]
flake8 = "*"
isort = "*"
black = "^19.10b0"
black = "*"
pytest = "*"
bandit = "*"
[build-system]
requires = ["poetry>=0.12"]
build-backend = "poetry.masonry.api"
[tool.poetry.extras]
redis = ["aioredis"]
redis = ["redis"]
memcache = ["aiomcache"]
all = ["aioredis","aiomcache"]
dynamodb = ["aiobotocore"]
all = ["redis", "aiomcache", "aiobotocore"]
[tool.black]
line-length = 100
target-version = ['py36', 'py37', 'py38', 'py39']