35 Commits

Author SHA1 Message Date
long2ice
9928f4cda0 Add enable param to init 2021-10-28 15:52:21 +08:00
long2ice
4faa5b7101 update workflows 2021-10-19 11:56:51 +08:00
long2ice
c3be2eca19 Fix default json coder for datetime. 2021-10-09 16:51:05 +08:00
long2ice
8a8eb395ec Merge pull request #38 from joeflack4/docs-minorFix
Fix: README.md: Corrected some syntactically incorrect commands
2021-09-24 10:23:25 +08:00
Joe Flack
e397dcb16b Updated readme.md
Corrected some syntactically incorrect commands
2021-09-23 16:10:31 -04:00
long2ice
37a2fa85db update CONVERTERS 2021-09-17 10:19:56 +08:00
long2ice
6888c10d6c update deps 2021-08-29 22:33:50 +08:00
long2ice
943935870d Update FUNDING.yml 2021-08-26 20:34:50 +08:00
long2ice
46c7ada364 Fix ci 2021-07-26 16:37:19 +08:00
long2ice
767241be41 - Fix redis cache.
- Encode key builder.
2021-07-26 16:33:22 +08:00
long2ice
de1bde39fd update version 2021-07-23 09:38:47 +08:00
long2ice
8490ad36f0 Merge pull request #25 from dveleztx/master
This fixes #24.
2021-07-23 09:38:04 +08:00
David Velez
57fe4ce24b Updated changelog to 1.4 from 1.5 2021-07-22 10:54:10 -05:00
David Velez
3dc2b53e41 Updated changelog and version for the project. 2021-07-22 10:47:02 -05:00
David Velez
2dd37b09ab This fixes #24. Looking at aioredis library, the client.py keyword for set is now 'ex', not 'expire'. Tested this fix and fast-cache now works without issue. 2021-07-22 10:17:52 -05:00
long2ice
0bc8c6c20e Merge pull request #22 from heliumbrain/master
Adapt to aioredis 2.0
2021-07-16 10:02:10 +08:00
heliumbrain
9e3c9816c5 Update README.md
Updated readme to reflect the changes in aioredis 2.0
2021-07-15 23:15:01 +02:00
long2ice
7c7aa26a88 update deps 2021-07-15 10:00:27 +08:00
long2ice
1edb0ba1fe Create FUNDING.yml 2021-04-30 16:49:36 +08:00
long2ice
7c2007847f Merge pull request #8 from rushilsrivastava/patch-2
Use FastAPI's built in jsonable_encoder
2021-04-30 16:48:07 +08:00
long2ice
80de421a2a Merge pull request #18 from Agri1988/master
add timezone serializer to coder.JsonEncoder for datetime.datetime instances
2021-04-29 09:36:48 +08:00
alexandr.yagolovich
eb55b01be9 timezone serializer to coder.JsonEncoder for datetime.datetime instances 2021-04-28 20:44:35 +03:00
long2ice
8573eeace6 fix ci 2021-03-20 14:47:46 +08:00
long2ice
1d0c245a70 - Fix default expire for memcached. (#13)
- Update default key builder. (#12)
2021-03-20 14:42:29 +08:00
long2ice
c665189d90 Merge pull request #7 from shershen08/patch-1
Update README.md
2021-01-12 09:50:25 +08:00
Mikhail Kuznetcov
ba7276ba98 Update README.md
add other params description
2021-01-11 17:37:39 +01:00
Rushil Srivastava
30e5246cf5 Use FastAPI's built in jsonable_encoder 2021-01-11 03:11:35 -08:00
Mikhail Kuznetcov
cdae610432 Update README.md
add explanation about expire
2021-01-10 16:26:41 +01:00
long2ice
9157412d6a Merge pull request #5 from rushilsrivastava/patch-1
Add minimum dependencies version
2021-01-10 19:59:15 +08:00
Rushil Srivastava
a9b0b9d913 add min dependencies 2021-01-10 03:53:26 -08:00
long2ice
fec3c78291 bug fix 2021-01-06 20:00:58 +08:00
long2ice
361a25857b bug fix 2021-01-06 19:57:16 +08:00
long2ice
6f5d4900a9 bug fix 2021-01-06 14:41:46 +08:00
long2ice
7f7252d151 bug fix 2021-01-06 10:44:22 +08:00
long2ice
c9e03ed9af update pypi.yml 2021-01-06 10:38:30 +08:00
15 changed files with 489 additions and 361 deletions

2
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,2 @@
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,11 +11,13 @@ jobs:
- uses: actions/setup-python@v1
with:
python-version: '3.x'
- uses: dschep/install-poetry-action@v1.3
- 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
uses: pypa/gh-action-pypi-publish@master
with:
user: __token__
password: ${{ secrets.pypi_password }}
password: ${{ secrets.pypi_password }}

View File

@@ -2,6 +2,26 @@
## 0.1
### 0.1.7
- Fix default json coder for datetime.
- Add `enable` param to `init`.
### 0.1.6
- Fix redis cache.
- Encode key builder.
### 0.1.5
- Fix setting expire for redis (#24)
- Update expire key
### 0.1.4
- Fix default expire for memcached. (#13)
- Update default key builder. (#12)
### 0.1.3
- Fix cache key builder.

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,18 +8,21 @@ 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)
@poetry run bandit -r $(checkfiles)
test: deps
$(py_warn) pytest
$(py_warn) poetry run pytest
build: deps
build: clean deps
@poetry build
clean:
@rm -rf ./dist
ci: check test

View File

@@ -30,13 +30,13 @@
or
```shell
> pip install fastapi-cache2[redis]
> pip install "fastapi-cache2[redis]"
```
or
```shell
> pip install fastapi-cache2[memcache]
> pip install "fastapi-cache2[memcache]"
```
## Usage
@@ -69,7 +69,7 @@ async def index(request: Request, response: Response):
@app.on_event("startup")
async def startup():
redis = await aioredis.create_redis_pool("redis://localhost", encoding="utf8")
redis = aioredis.from_url("redis://localhost", encoding="utf8", decode_responses=True)
FastAPICache.init(RedisBackend(redis), prefix="fastapi-cache")
```
@@ -82,6 +82,14 @@ Firstly you must call `FastAPICache.init` on startup event of `fastapi`, there a
If you want cache `fastapi` response transparently, you can use `cache` as decorator between router decorator and view function and must pass `request` as param of view function.
Parameter | type, description
------------ | -------------
expire | int, states a caching time in seconds
namespace | str, namespace to use to store certain cache items
coder | which coder to use, e.g. JsonCoder
key_builder | which key builder to use, default to builtin
And if you want use `ETag` and `Cache-Control` features, you must pass `response` param also.
You can also use `cache` as decorator like other cache tools to cache common function result.

View File

@@ -1,10 +1,13 @@
from datetime import date, datetime
import aioredis
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.inmemory import InMemoryBackend
from fastapi_cache.backends.redis import RedisBackend
from fastapi_cache.decorator import cache
app = FastAPI()
@@ -20,7 +23,7 @@ async def get_ret():
@app.get("/")
@cache(namespace="test", expire=2)
@cache(namespace="test", expire=20)
async def index(request: Request, response: Response):
return dict(ret=await get_ret())
@@ -30,9 +33,22 @@ 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():
FastAPICache.init(InMemoryBackend(), prefix="fastapi-cache")
redis = aioredis.from_url(url="redis://localhost")
FastAPICache.init(RedisBackend(redis), prefix="fastapi-cache")
if __name__ == "__main__":

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

@@ -16,7 +16,7 @@ class MemcachedBackend(Backend):
return await self.mcache.get(key, key.encode())
async def set(self, key: str, value: str, expire: int = None):
return await self.mcache.set(key.encode(), value.encode(), exptime=expire)
return await self.mcache.set(key.encode(), value.encode(), exptime=expire or 0)
async def clear(self, namespace: str = None, key: str = None):
raise NotImplementedError

View File

@@ -10,20 +10,18 @@ class RedisBackend(Backend):
self.redis = redis
async def get_with_ttl(self, key: str) -> Tuple[int, str]:
p = self.redis.pipeline()
p.ttl(key)
p.get(key)
return await p.execute()
async with self.redis.pipeline(transaction=True) as pipe:
return await (pipe.ttl(key).get(key).execute())
async def get(self, key) -> str:
return await self.redis.get(key)
async def set(self, key: str, value: str, expire: int = None):
return await self.redis.set(key, value, expire=expire)
return await self.redis.set(key, value, ex=expire)
async def clear(self, namespace: str = None, key: str = None) -> int:
if namespace:
lua = f"for i, name in ipairs(redis.call('KEYS', '{namespace}:*')) do redis.call('DEL', name); end"
return await self.redis.eval(lua)
return await self.redis.eval(lua, numkeys=0)
elif key:
return await self.redis.delete(key)

View File

@@ -4,11 +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,
}
@@ -16,13 +17,13 @@ CONVERTERS = {
class JsonEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, datetime.datetime):
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:
return super().default(obj)
return jsonable_encoder(obj)
def object_hook(obj):

View File

@@ -26,17 +26,22 @@ def cache(
nonlocal coder
nonlocal expire
nonlocal key_builder
request = kwargs.get("request")
if request.headers.get("Cache-Control") == "no-store":
copy_kwargs = kwargs.copy()
request = copy_kwargs.pop("request", None)
response = copy_kwargs.pop("response", None)
if (
request and request.headers.get("Cache-Control") == "no-store"
) or not FastAPICache.get_enable():
return await func(*args, **kwargs)
coder = coder or FastAPICache.get_coder()
expire = expire or FastAPICache.get_expire()
key_builder = key_builder or FastAPICache.get_key_builder()
request = kwargs.pop("request", None)
response = kwargs.pop("response", None)
backend = FastAPICache.get_backend()
cache_key = key_builder(func, namespace, args=args, kwargs=kwargs)
cache_key = key_builder(
func, namespace, request=request, response=response, args=args, kwargs=copy_kwargs
)
ttl, ret = await backend.get_with_ttl(cache_key)
if not request:
if ret is not None:
@@ -49,7 +54,6 @@ def cache(
return await func(request, *args, **kwargs)
if_none_match = request.headers.get("if-none-match")
if ret is not None:
response = kwargs.get("response")
if response:
response.headers["Cache-Control"] = f"max-age={ttl}"
etag = f"W/{hash(ret)}"

View File

@@ -1,3 +1,4 @@
import hashlib
from typing import Optional
from starlette.requests import Request
@@ -14,6 +15,11 @@ def default_key_builder(
):
from fastapi_cache import FastAPICache
prefix = FastAPICache.get_prefix()
cache_key = f"{prefix}:{namespace}:{func.__module__}:{func.__name__}:{args}:{kwargs}"
prefix = f"{FastAPICache.get_prefix()}:{namespace}:"
cache_key = (
prefix
+ hashlib.md5( # nosec:B303
f"{func.__module__}:{func.__name__}:{args}:{kwargs}".encode()
).hexdigest()
)
return cache_key

679
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.3"
version = "0.1.7"
description = "Cache for FastAPI"
authors = ["long2ice <long2ice@gmail.com>"]
license = "Apache-2.0"
@@ -18,14 +18,14 @@ include = ["LICENSE", "README.md"]
python = "^3.7"
fastapi = "*"
uvicorn = "*"
aioredis = {version = "*", optional = true}
aioredis = {version = "^2.0", optional = true}
aiomcache = {version = "*", optional = true}
python-dateutil = "*"
pendulum = "*"
[tool.poetry.dev-dependencies]
flake8 = "*"
isort = "*"
black = "^19.10b0"
black = "*"
pytest = "*"
bandit = "*"
@@ -37,3 +37,7 @@ build-backend = "poetry.masonry.api"
redis = ["aioredis"]
memcache = ["aiomcache"]
all = ["aioredis","aiomcache"]
[tool.black]
line-length = 100
target-version = ['py36', 'py37', 'py38', 'py39']