17 Commits

Author SHA1 Message Date
Jinlong Peng
15576b482a Merge remote-tracking branch 'origin/master' 2022-08-07 21:11:55 +08:00
Jinlong Peng
f80bfdb18d upgrade deps 2022-08-07 21:11:47 +08:00
long2ice
aaed438d8f Merge pull request #69 from jegork/feature/support-cache-for-normal-functions
Add support for normal def functions
2022-08-01 15:05:16 +08:00
Jegor Kitskerkin
d6c52408d2 Add support for normal def functions 2022-08-01 00:06:39 +02:00
long2ice
8c92cc59ae Fix test 2022-06-26 19:18:34 +08:00
long2ice
824e2e145f Replace aioredis with redis-py 2022-06-17 11:01:47 +08:00
long2ice
7fa54d311f Merge remote-tracking branch 'origin/master' 2022-06-10 08:43:42 +08:00
long2ice
9582e04d43 update deps 2022-06-10 08:43:35 +08:00
long2ice
fd8cf2da11 Merge pull request #60 from cnkailyn/master
bugfix: '+' is more prior than 'or'
2022-04-24 11:44:25 +08:00
kailyn
2f1b1409b9 bugfix: '+' is more prior than 'or' 2022-04-24 11:19:20 +08:00
long2ice
269c1ca616 update deps 2022-03-30 14:19:53 +08:00
long2ice
81d2bf2cc6 update version and changelog 2021-11-12 09:37:02 +08:00
long2ice
70f53566aa Merge pull request #40 from jimtheyounger/feature/add-dynamodb-backend
Add dynamodb backend support
2021-10-29 10:45:28 +08:00
Jimmy
11f01a21f5 Remove unused variable assignment 2021-10-07 17:34:06 +02:00
Jimmy
cdcfdc6ae6 Apply styling 2021-10-06 10:10:22 +02:00
Jimmy
a40c54e9e7 Update README & add usage 2021-09-29 16:22:04 +02:00
Jimmy
d67797a1d5 Add DynamoDB backend 2021-09-29 16:14:28 +02:00
11 changed files with 831 additions and 446 deletions

View File

@@ -2,6 +2,14 @@
## 0.1 ## 0.1
### 0.1.9
- Replace `aioredis` with `redis-py`.
### 0.1.8
- Support `dynamodb` backend.
### 0.1.7 ### 0.1.7
- Fix default json coder for datetime. - Fix default json coder for datetime.

View File

@@ -14,7 +14,6 @@ style: deps
check: deps check: deps
@poetry run black $(checkfiles) || (echo "Please run 'make style' to auto-fix style issues" && false) @poetry run black $(checkfiles) || (echo "Please run 'make style' to auto-fix style issues" && false)
@poetry run flake8 $(checkfiles) @poetry run flake8 $(checkfiles)
@poetry run bandit -r $(checkfiles)
test: deps test: deps
$(py_warn) poetry run pytest $(py_warn) poetry run pytest

View File

@@ -7,11 +7,11 @@
## Introduction ## 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 ## Features
- Support `redis` and `memcache` and `in-memory` backends. - Support `redis`, `memcache`, `dynamodb`, and `in-memory` backends.
- Easily integration with `fastapi`. - Easily integration with `fastapi`.
- Support http cache like `ETag` and `Cache-Control`. - Support http cache like `ETag` and `Cache-Control`.
@@ -20,6 +20,7 @@
- `asyncio` environment. - `asyncio` environment.
- `redis` if use `RedisBackend`. - `redis` if use `RedisBackend`.
- `memcache` if use `MemcacheBackend`. - `memcache` if use `MemcacheBackend`.
- `aiobotocore` if use `DynamoBackend`.
## Install ## Install
@@ -39,6 +40,12 @@ or
> pip install "fastapi-cache2[memcache]" > pip install "fastapi-cache2[memcache]"
``` ```
or
```shell
> pip install "fastapi-cache2[dynamodb]"
```
## Usage ## Usage
### Quick Start ### Quick Start

View File

@@ -1,8 +1,10 @@
from datetime import date, datetime from datetime import date, datetime
import time
import aioredis import redis.asyncio as redis
import uvicorn import uvicorn
from fastapi import FastAPI from fastapi import FastAPI
from redis.asyncio.connection import ConnectionPool
from starlette.requests import Request from starlette.requests import Request
from starlette.responses import Response from starlette.responses import Response
@@ -39,6 +41,13 @@ async def get_data(request: Request, response: Response):
return date.today() 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") @app.get("/datetime")
@cache(namespace="test", expire=20) @cache(namespace="test", expire=20)
async def get_datetime(request: Request, response: Response): async def get_datetime(request: Request, response: Response):
@@ -47,8 +56,9 @@ async def get_datetime(request: Request, response: Response):
@app.on_event("startup") @app.on_event("startup")
async def startup(): async def startup():
redis = aioredis.from_url(url="redis://localhost") pool = ConnectionPool.from_url(url="redis://localhost")
FastAPICache.init(RedisBackend(redis), prefix="fastapi-cache") r = redis.Redis(connection_pool=pool)
FastAPICache.init(RedisBackend(r), prefix="fastapi-cache")
if __name__ == "__main__": if __name__ == "__main__":

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 def set(self, key: str, value: str, expire: int = None):
async with self._lock: 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: async def clear(self, namespace: str = None, key: str = None) -> int:
count = 0 count = 0

View File

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

View File

@@ -64,4 +64,4 @@ class PickleCoder(Coder):
@classmethod @classmethod
def decode(cls, value: Any): 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 import asyncio
from typing import Callable, Optional, Type from functools import wraps, partial
import inspect
from typing import TYPE_CHECKING, Callable, Optional, Type
from fastapi_cache import FastAPICache from fastapi_cache import FastAPICache
from fastapi_cache.coder import Coder from fastapi_cache.coder import Coder
if TYPE_CHECKING:
import concurrent.futures
def cache( def cache(
expire: int = None, expire: int = None,
coder: Type[Coder] = None, coder: Type[Coder] = None,
key_builder: Callable = None, key_builder: Callable = None,
namespace: Optional[str] = "", namespace: Optional[str] = "",
executor: Optional["concurrent.futures.Executor"] = None,
): ):
""" """
cache all function cache all function
@@ -17,6 +23,8 @@ def cache(
:param expire: :param expire:
:param coder: :param coder:
:param key_builder: :param key_builder:
:param executor:
:return: :return:
""" """
@@ -63,7 +71,12 @@ def cache(
response.headers["ETag"] = etag response.headers["ETag"] = etag
return coder.decode(ret) return coder.decode(ret)
if inspect.iscoroutinefunction(func):
ret = await func(*args, **kwargs) 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()) await backend.set(cache_key, coder.encode(ret), expire or FastAPICache.get_expire())
return ret return ret

1111
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

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