Skip to content

Commit

Permalink
Merge pull request #34 from a-luna/patch-release/v0.2.3
Browse files Browse the repository at this point in the history
v0.2.3
  • Loading branch information
a-luna authored Jun 14, 2021
2 parents fa065fe + dc12793 commit a5130ca
Show file tree
Hide file tree
Showing 5 changed files with 33 additions and 20 deletions.
24 changes: 12 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
## `fastapi-redis-cache`
## fastapi-redis-cache

[![PyPI version](https://badge.fury.io/py/fastapi-redis-cache.svg)](https://badge.fury.io/py/fastapi-redis-cache)
![PyPI - Downloads](https://img.shields.io/pypi/dm/fastapi-redis-cache?color=%234DC71F)
![PyPI - License](https://img.shields.io/pypi/l/fastapi-redis-cache?color=%25234DC71F)
![PyPI - Python Version](https://img.shields.io/pypi/pyversions/fastapi-redis-cache)
[![Maintainability](https://api.codeclimate.com/v1/badges/ec0b1d7afb21bd8c23dc/maintainability)](https://codeclimate.com/github/a-luna/fastapi-redis-cache/maintainability)
[![Test Coverage](https://api.codeclimate.com/v1/badges/ec0b1d7afb21bd8c23dc/test_coverage)](https://codeclimate.com/github/a-luna/fastapi-redis-cache/test_coverage)
[![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)

### Features
## Features

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

### Installation
## Installation

`pip install fastapi-redis-cache`

### Usage
## Usage

#### Initialize Redis
### Initialize Redis

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:

Expand Down Expand Up @@ -54,7 +54,7 @@ After creating the instance, you must call the `init` method. The only required
- `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]`)
- 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)).

#### `@cache` Decorator
### `@cache` Decorator

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".

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

#### Response Headers
### Response Headers

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:

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

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`.

#### Cache Keys
### Cache Keys

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):

```python
@app.get("/get_user", response_model=schemas.User)
@cache(expire=3600)
def get_item(user_id: int, db: Session = Depends(get_db)):
def get_user(user_id: int, db: Session = Depends(get_db)):
return db.query(models.User).filter(models.User.id == user_id).first()
```

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

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.

#### Cache Keys Pt 2.
### Cache Keys Pt 2.

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.

Expand Down Expand Up @@ -267,6 +267,6 @@ class MLBGameDate:

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.

### Questions/Contributions
## Questions/Contributions

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.
8 changes: 4 additions & 4 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
black==20.8b1
coverage==5.5
fakeredis==1.5.0
flake8==3.9.1
fakeredis==1.5.2
flake8==3.9.2
isort==5.8.0
pytest==6.2.3
pytest-cov==2.11.1
pytest==6.2.4
pytest-cov==2.12.1
pytest-flake8==1.0.7
pytest-random-order==1.0.4
requests==2.25.1
6 changes: 3 additions & 3 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
fastapi==0.63.0
pydantic==1.8.1
fastapi==0.65.2
pydantic==1.8.2
redis==3.5.3
uvicorn==0.13.4
uvicorn==0.14.0
13 changes: 13 additions & 0 deletions src/fastapi_redis_cache/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,13 @@ def requested_resource_not_modified(self, request: Request, cached_data: str) ->
return self.get_etag(cached_data) in check_etags

def add_to_cache(self, key: str, value: Dict, expire: int) -> bool:
if not isinstance(value, dict): # pragma: no cover
if self.hasmethod(value, 'dict'):
value = value.dict()
else:
message = f"Object of type {type(value)} is not JSON-serializable"
self.log(RedisEvent.FAILED_TO_CACHE_KEY, msg=message, key=key)
return False
cached = self.redis.set(name=key, value=serialize_json(value), ex=expire)
if cached:
self.log(RedisEvent.KEY_ADDED_TO_CACHE, key=key)
Expand Down Expand Up @@ -151,3 +158,9 @@ def get_etag(cached_data: Union[str, bytes, Dict]) -> str:
def get_log_time():
"""Get a timestamp to include with a log message."""
return datetime.now().strftime(LOG_TIMESTAMP)

@staticmethod
def hasmethod(obj, method_name):
"""Return True if obj.method_name exists and is callable. Otherwise, return False."""
obj_method = getattr(obj, method_name, None)
return callable(obj_method) if obj_method else False
2 changes: 1 addition & 1 deletion src/fastapi_redis_cache/version.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# flake8: noqa
__version_info__ = ("0", "2", "2") # pragma: no cover
__version_info__ = ("0", "2", "3") # pragma: no cover
__version__ = ".".join(__version_info__) # pragma: no cover

0 comments on commit a5130ca

Please sign in to comment.