Skip to content

Commit c0259bc

Browse files
committed
Add in-memory support.
1 parent 800032c commit c0259bc

File tree

8 files changed

+237
-203
lines changed

8 files changed

+237
-203
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# ChangeLog
2+
3+
## 0.1
4+
5+
### 0.1.1
6+
7+
- Add in-memory support.
8+
9+
### 0.1.0
10+
11+
- First version release.

README.md

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
## Features
1313

14-
- Support `redis` and `memcache` .
14+
- Support `redis` and `memcache` and `in-memory` backends.
1515
- Easily integration with `fastapi`.
1616
- Support http cache like `ETag` and `Cache-Control`.
1717

@@ -23,6 +23,12 @@
2323

2424
## Install
2525

26+
```shell
27+
> pip install fastapi-cache2
28+
```
29+
30+
or
31+
2632
```shell
2733
> pip install fastapi-cache2[redis]
2834
```
@@ -39,14 +45,13 @@ or
3945

4046
```python
4147
import aioredis
42-
import uvicorn
4348
from fastapi import FastAPI
4449
from starlette.requests import Request
4550
from starlette.responses import Response
4651

4752
from fastapi_cache import FastAPICache
4853
from fastapi_cache.backends.redis import RedisBackend
49-
from fastapi_cache.decorator import cache_response, cache
54+
from fastapi_cache.decorator import cache
5055

5156
app = FastAPI()
5257

@@ -57,7 +62,7 @@ async def get_cache():
5762

5863

5964
@app.get("/")
60-
@cache_response(expire=60)
65+
@cache(expire=60)
6166
async def index(request: Request, response: Response):
6267
return dict(hello="world")
6368

@@ -69,23 +74,21 @@ async def startup():
6974

7075
```
7176

72-
### Use `cache_response`
77+
### Use `cache` decorator
7378

74-
If you want cache `fastapi` response transparently, you can use `cache_response` as decorator between router decorator and view function and must pass `request` as param of view function.
79+
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.
7580

7681
And if you want use `ETag` and `Cache-Control` features, you must pass `response` param also.
7782

78-
### Use `cache`
79-
80-
You can use `cache` as decorator like other cache tools to cache common function result.
83+
You can also use `cache` as decorator like other cache tools to cache common function result.
8184

8285
### Custom coder
8386

8487
By default use `JsonCoder`, you can write custom coder to encode and decode cache result, just need inherit `fastapi_cache.coder.Coder`.
8588

8689
```python
8790
@app.get("/")
88-
@cache_response(expire=60,coder=JsonCoder)
91+
@cache(expire=60,coder=JsonCoder)
8992
async def index(request: Request, response: Response):
9093
return dict(hello="world")
9194
```
@@ -106,11 +109,15 @@ def my_key_builder(
106109
return cache_key
107110

108111
@app.get("/")
109-
@cache_response(expire=60,coder=JsonCoder,key_builder=my_key_builder)
112+
@cache(expire=60,coder=JsonCoder,key_builder=my_key_builder)
110113
async def index(request: Request, response: Response):
111114
return dict(hello="world")
112115
```
113116

117+
### InMemoryBackend
118+
119+
`InMemoryBackend` only support in single node instead of distributed environment.
120+
114121
## License
115122

116123
This project is licensed under the [Apache-2.0](https://github.com/long2ice/fastapi-cache/blob/master/LICENSE) License.

examples/main.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,33 @@
1-
import aioredis
21
import uvicorn
32
from fastapi import FastAPI
43
from starlette.requests import Request
54
from starlette.responses import Response
65

76
from fastapi_cache import FastAPICache
8-
from fastapi_cache.backends.redis import RedisBackend
9-
from fastapi_cache.decorator import cache, cache_response
7+
from fastapi_cache.backends.inmemory import InMemoryBackend
8+
from fastapi_cache.decorator import cache
109

1110
app = FastAPI()
1211

12+
ret = 0
1313

14-
@cache()
15-
async def get_cache():
16-
return 1
14+
15+
@cache(expire=1)
16+
async def get_ret():
17+
global ret
18+
ret = ret + 1
19+
return ret
1720

1821

1922
@app.get("/")
20-
@cache_response(expire=60)
23+
@cache(expire=2)
2124
async def index(request: Request, response: Response):
22-
return dict(hello="world")
25+
return dict(ret=await get_ret())
2326

2427

2528
@app.on_event("startup")
2629
async def startup():
27-
redis = await aioredis.create_redis_pool("redis://localhost", encoding="utf8")
28-
FastAPICache.init(RedisBackend(redis), prefix="fastapi-cache")
30+
FastAPICache.init(InMemoryBackend(), prefix="fastapi-cache")
2931

3032

3133
if __name__ == "__main__":

fastapi_cache/__init__.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
class FastAPICache:
22
_backend = None
33
_prefix = None
4+
_expire = None
5+
_init = False
46

57
@classmethod
6-
def init(cls, backend, prefix: str = ""):
8+
def init(cls, backend, prefix: str = "", expire: int = None):
9+
if cls._init:
10+
return
11+
cls._init = True
712
cls._backend = backend
813
cls._prefix = prefix
14+
cls._expire = expire
915

1016
@classmethod
1117
def get_backend(cls):
@@ -14,5 +20,8 @@ def get_backend(cls):
1420

1521
@classmethod
1622
def get_prefix(cls):
17-
assert cls._prefix, "You must call init first!" # nosec: B101
1823
return cls._prefix
24+
25+
@classmethod
26+
def get_expire(cls):
27+
return cls._expire

fastapi_cache/backends/inmemory.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import time
2+
from dataclasses import dataclass
3+
from threading import Lock
4+
from typing import Dict, Optional, Tuple
5+
6+
from fastapi_cache.backends import Backend
7+
8+
9+
@dataclass
10+
class Value:
11+
data: str
12+
ttl_ts: int
13+
14+
15+
class InMemoryBackend(Backend):
16+
_store: Dict[str, Value] = {}
17+
_lock = Lock()
18+
19+
@property
20+
def _now(self) -> int:
21+
return int(time.time())
22+
23+
def _get(self, key: str):
24+
v = self._store.get(key)
25+
if v:
26+
if v.ttl_ts < self._now:
27+
del self._store[key]
28+
else:
29+
return v
30+
31+
async def get_with_ttl(self, key: str) -> Tuple[int, Optional[str]]:
32+
with self._lock:
33+
v = self._get(key)
34+
if v:
35+
return v.ttl_ts - self._now, v.data
36+
return 0, None
37+
38+
async def get(self, key: str) -> str:
39+
with self._lock:
40+
v = self._get(key)
41+
if v:
42+
return v.data
43+
44+
async def set(self, key: str, value: str, expire: int = None):
45+
with self._lock:
46+
self._store[key] = Value(value, self._now + expire)

fastapi_cache/decorator.py

Lines changed: 10 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -39,45 +39,19 @@ def cache(
3939
def wrapper(func):
4040
@wraps(func)
4141
async def inner(*args, **kwargs):
42+
request = kwargs.get("request")
4243
backend = FastAPICache.get_backend()
4344
cache_key = key_builder(func, namespace, *args, **kwargs)
44-
ret = await backend.get(cache_key)
45-
if ret is not None:
46-
return coder.decode(ret)
47-
48-
ret = await func(*args, **kwargs)
49-
await backend.set(cache_key, coder.encode(ret), expire)
50-
return ret
51-
52-
return inner
53-
54-
return wrapper
55-
56-
57-
def cache_response(
58-
expire: int = None,
59-
coder: Type[Coder] = JsonCoder,
60-
key_builder: Callable = default_key_builder,
61-
namespace: Optional[str] = "",
62-
):
63-
"""
64-
cache fastapi response
65-
:param namespace:
66-
:param expire:
67-
:param coder:
68-
:param key_builder:
69-
:return:
70-
"""
45+
ttl, ret = await backend.get_with_ttl(cache_key)
46+
if not request:
47+
if ret is not None:
48+
return coder.decode(ret)
49+
ret = await func(*args, **kwargs)
50+
await backend.set(cache_key, coder.encode(ret), expire or FastAPICache.get_expire())
51+
return ret
7152

72-
def wrapper(func):
73-
@wraps(func)
74-
async def inner(request: Request, *args, **kwargs):
7553
if request.method != "GET":
7654
return await func(request, *args, **kwargs)
77-
78-
backend = FastAPICache.get_backend()
79-
cache_key = key_builder(func, namespace, request, *args, **kwargs)
80-
ttl, ret = await backend.get_with_ttl(cache_key)
8155
if_none_match = request.headers.get("if-none-match")
8256
if ret is not None:
8357
response = kwargs.get("response")
@@ -90,8 +64,8 @@ async def inner(request: Request, *args, **kwargs):
9064
response.headers["ETag"] = etag
9165
return coder.decode(ret)
9266

93-
ret = await func(request, *args, **kwargs)
94-
await backend.set(cache_key, coder.encode(ret), expire)
67+
ret = await func(*args, **kwargs)
68+
await backend.set(cache_key, coder.encode(ret), expire or FastAPICache.get_expire())
9569
return ret
9670

9771
return inner

0 commit comments

Comments
 (0)