Skip to content

Commit a5130ca

Browse files
authored
Merge pull request a-luna#34 from a-luna/patch-release/v0.2.3
v0.2.3
2 parents fa065fe + dc12793 commit a5130ca

File tree

5 files changed

+33
-20
lines changed

5 files changed

+33
-20
lines changed

README.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,26 @@
1-
## `fastapi-redis-cache`
1+
## fastapi-redis-cache
22

33
[![PyPI version](https://badge.fury.io/py/fastapi-redis-cache.svg)](https://badge.fury.io/py/fastapi-redis-cache)
44
![PyPI - Downloads](https://img.shields.io/pypi/dm/fastapi-redis-cache?color=%234DC71F)
55
![PyPI - License](https://img.shields.io/pypi/l/fastapi-redis-cache?color=%25234DC71F)
66
![PyPI - Python Version](https://img.shields.io/pypi/pyversions/fastapi-redis-cache)
77
[![Maintainability](https://api.codeclimate.com/v1/badges/ec0b1d7afb21bd8c23dc/maintainability)](https://codeclimate.com/github/a-luna/fastapi-redis-cache/maintainability)
8-
[![Test Coverage](https://api.codeclimate.com/v1/badges/ec0b1d7afb21bd8c23dc/test_coverage)](https://codeclimate.com/github/a-luna/fastapi-redis-cache/test_coverage)
8+
[![codecov](https://codecov.io/gh/a-luna/fastapi-redis-cache/branch/main/graph/badge.svg?token=dUaILJcgWY)](https://codecov.io/gh/a-luna/fastapi-redis-cache)
99

10-
### Features
10+
## Features
1111

1212
- Cache response data for async and non-async path operation functions.
1313
- Lifetime of cached data is configured separately for each API endpoint.
1414
- Requests with `Cache-Control` header containing `no-cache` or `no-store` are handled correctly (all caching behavior is disabled).
1515
- Requests with `If-None-Match` header will receive a response with status `304 NOT MODIFIED` if `ETag` for requested resource matches header value.
1616

17-
### Installation
17+
## Installation
1818

1919
`pip install fastapi-redis-cache`
2020

21-
### Usage
21+
## Usage
2222

23-
#### Initialize Redis
23+
### Initialize Redis
2424

2525
Create a `FastApiRedisCache` instance when your application starts by [defining an event handler for the `"startup"` event](https://fastapi.tiangolo.com/advanced/events/) as shown below:
2626

@@ -54,7 +54,7 @@ After creating the instance, you must call the `init` method. The only required
5454
- `ignore_arg_types` (`List[Type[object]]`) — Cache keys are created (in part) by combining the name and value of each argument used to invoke a path operation function. If any of the arguments have no effect on the response (such as a `Request` or `Response` object), including their type in this list will ignore those arguments when the key is created. (_Optional_, defaults to `[Request, Response]`)
5555
- The example shown here includes the `sqlalchemy.orm.Session` type, if your project uses SQLAlchemy as a dependency ([as demonstrated in the FastAPI docs](https://fastapi.tiangolo.com/tutorial/sql-databases/)), you should include `Session` in `ignore_arg_types` in order for cache keys to be created correctly ([More info](#cache-keys)).
5656

57-
#### `@cache` Decorator
57+
### `@cache` Decorator
5858

5959
Decorating a path function with `@cache` enables caching for the endpoint. **Response data is only cached for `GET` operations**, decorating path functions for other HTTP method types will have no effect. If no arguments are provided, responses will be set to expire after one year, which, historically, is the correct way to mark data that "never expires".
6060

@@ -137,7 +137,7 @@ def partial_cache_two_hours(response: Response):
137137
return {"success": True, "message": "this data should be cached for two hours"}
138138
```
139139

140-
#### Response Headers
140+
### Response Headers
141141

142142
Below is an example HTTP response for the `/dynamic_data` endpoint. The `cache-control`, `etag`, `expires`, and `x-fastapi-cache` headers are added because of the `@cache` decorator:
143143

@@ -167,14 +167,14 @@ If this request was made from a web browser, and a request for the same resource
167167

168168
Similarly, if a request is sent with the `cache-control` header containing `no-cache` or `no-store`, all caching behavior will be disabled and the response will be generated and sent as if endpoint had not been decorated with `@cache`.
169169

170-
#### Cache Keys
170+
### Cache Keys
171171

172172
Consider the `/get_user` API route defined below. This is the first path function we have seen where the response depends on the value of an argument (`user_id: int`). This is a typical CRUD operation where `user_id` is used to retrieve a `User` record from a database. The API route also includes a dependency that injects a `Session` object (`db`) into the function, [per the instructions from the FastAPI docs](https://fastapi.tiangolo.com/tutorial/sql-databases/#create-a-dependency):
173173

174174
```python
175175
@app.get("/get_user", response_model=schemas.User)
176176
@cache(expire=3600)
177-
def get_item(user_id: int, db: Session = Depends(get_db)):
177+
def get_user(user_id: int, db: Session = Depends(get_db)):
178178
return db.query(models.User).filter(models.User.id == user_id).first()
179179
```
180180

@@ -227,7 +227,7 @@ INFO: 127.0.0.1:50761 - "GET /get_user?user_id=1 HTTP/1.1" 200 OK
227227

228228
Now, every request for the same `user_id` generates the same key value (`myapi-cache:api.get_user(user_id=1)`). As expected, the first request adds the key/value pair to the cache, and each subsequent request retrieves the value from the cache based on the key.
229229

230-
#### Cache Keys Pt 2.
230+
### Cache Keys Pt 2.
231231

232232
What about this situation? You create a custom dependency for your API that performs input validation, but you can't ignore it because _**it does**_ have an effect on the response data. There's a simple solution for that, too.
233233

@@ -267,6 +267,6 @@ class MLBGameDate:
267267

268268
Please note the `__str__` method that overrides the default behavior. This way, instead of `<MLBGameDate object at 0x11c7e35e0>`, the value will be formatted as, for example, `2019-05-09`. You can use this strategy whenever you have an argument that has en effect on the response data but converting that argument to a string results in a value containing the object's memory location.
269269

270-
### Questions/Contributions
270+
## Questions/Contributions
271271

272272
If you have any questions, please open an issue. Any suggestions and contributions are absolutely welcome. This is still a very small and young project, I plan on adding a feature roadmap and further documentation in the near future.

requirements-dev.txt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
black==20.8b1
22
coverage==5.5
3-
fakeredis==1.5.0
4-
flake8==3.9.1
3+
fakeredis==1.5.2
4+
flake8==3.9.2
55
isort==5.8.0
6-
pytest==6.2.3
7-
pytest-cov==2.11.1
6+
pytest==6.2.4
7+
pytest-cov==2.12.1
88
pytest-flake8==1.0.7
99
pytest-random-order==1.0.4
1010
requests==2.25.1

requirements.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
fastapi==0.63.0
2-
pydantic==1.8.1
1+
fastapi==0.65.2
2+
pydantic==1.8.2
33
redis==3.5.3
4-
uvicorn==0.13.4
4+
uvicorn==0.14.0

src/fastapi_redis_cache/client.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,13 @@ def requested_resource_not_modified(self, request: Request, cached_data: str) ->
110110
return self.get_etag(cached_data) in check_etags
111111

112112
def add_to_cache(self, key: str, value: Dict, expire: int) -> bool:
113+
if not isinstance(value, dict): # pragma: no cover
114+
if self.hasmethod(value, 'dict'):
115+
value = value.dict()
116+
else:
117+
message = f"Object of type {type(value)} is not JSON-serializable"
118+
self.log(RedisEvent.FAILED_TO_CACHE_KEY, msg=message, key=key)
119+
return False
113120
cached = self.redis.set(name=key, value=serialize_json(value), ex=expire)
114121
if cached:
115122
self.log(RedisEvent.KEY_ADDED_TO_CACHE, key=key)
@@ -151,3 +158,9 @@ def get_etag(cached_data: Union[str, bytes, Dict]) -> str:
151158
def get_log_time():
152159
"""Get a timestamp to include with a log message."""
153160
return datetime.now().strftime(LOG_TIMESTAMP)
161+
162+
@staticmethod
163+
def hasmethod(obj, method_name):
164+
"""Return True if obj.method_name exists and is callable. Otherwise, return False."""
165+
obj_method = getattr(obj, method_name, None)
166+
return callable(obj_method) if obj_method else False

src/fastapi_redis_cache/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
# flake8: noqa
2-
__version_info__ = ("0", "2", "2") # pragma: no cover
2+
__version_info__ = ("0", "2", "3") # pragma: no cover
33
__version__ = ".".join(__version_info__) # pragma: no cover

0 commit comments

Comments
 (0)