Skip to content

Commit

Permalink
Merge pull request #21 from a-luna/patch-release/v0.1.4
Browse files Browse the repository at this point in the history
v0.1.4
  • Loading branch information
a-luna authored Apr 25, 2021
2 parents bcbf263 + 3245617 commit 472a638
Show file tree
Hide file tree
Showing 4 changed files with 54 additions and 8 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
venv
build
dist
coverage_html
.vscode
.tox
Expand Down
56 changes: 50 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,15 @@ After creating the instance, you must call the `init` method. The only required

#### `@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 1 year, which, historically, is the correct way to mark data that "never expires".
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".

```python
# WILL NOT be cached
@app.get("/data_no_cache")
def get_data():
return {"success": True, "message": "this is the data you requested"}
return {"success": True, "message": "this data is not cacheable, for... you know, reasons"}

# Will be cached
# Will be cached for one year
@app.get("/immutable_data")
@cache()
async def get_immutable_data():
Expand All @@ -87,6 +87,7 @@ The log messages show two successful (**`200 OK`**) responses to the same reques
If data for an API endpoint needs to expire, you can specify the number of seconds before it is deleted by Redis using the `expire_after_seconds` parameter:

```python
# Will be cached for thirty seconds
@app.get("/dynamic_data")
@cache(expire_after_seconds=30)
def get_dynamic_data(request: Request, response: Response):
Expand All @@ -95,7 +96,7 @@ def get_dynamic_data(request: Request, response: Response):

#### Response Headers

Below is the HTTP response for the `/dynamic_data` endpoint. The `cache-control`, `etag`, `expires`, and `x-fastapi-cache` headers are added because of the `@cache` decorator:
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:

```console
$ http "http://127.0.0.1:8000/dynamic_data"
Expand All @@ -115,9 +116,9 @@ $ http "http://127.0.0.1:8000/dynamic_data"
}
```

- The `x-fastapi-cache` header field indicates that this response was found in the Redis cache (a.k.a. a `Hit`).
- The `x-fastapi-cache` header field indicates that this response was found in the Redis cache (a.k.a. a `Hit`). The only other possible value for this field is `Miss`.
- The `expires` field and `max-age` value in the `cache-control` field indicate that this response will be considered fresh for 29 seconds. This is expected since `expire_after_seconds=30` was specified in the `@cache` decorator.
- The `etag` field is an identifier that is computed by converting the response data to a string and applying a hash function. If a request containing the `if-none-match` header is received, the `etag` value will be used to determine if the requested resource has been modified.
- The `etag` field is an identifier that is created by converting the response data to a string and applying a hash function. If a request containing the `if-none-match` header is received, the `etag` value will be used to determine if the requested resource has been modified.

If this request was made from a web browser, and a request for the same resource was sent before the cached response expires, the browser would automatically serve the cached version and the request would never even be sent to the FastAPI server.

Expand Down Expand Up @@ -183,6 +184,49 @@ INFO:fastapi_redis_cache.client: 04/23/2021 07:04:12 PM | KEY_FOUND_IN_CACHE: ke
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.

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.

Here is an endpoint from one of my projects:

```python
@router.get("/scoreboard", response_model=ScoreboardSchema)
@cache()
def get_scoreboard_for_date(
game_date: MLBGameDate = Depends(), db: Session = Depends(get_db)
):
return get_scoreboard_data_for_date(db, game_date.date)
```

The `game_date` argument is a `MLBGameDate` type. This is a custom type that parses the value from the querystring to a date, and determines if the parsed date is valid by checking if it is within a certain range. The implementation for `MLBGameDate` is given below:


```python
class MLBGameDate:
def __init__(
self,
game_date: str = Query(..., description="Date as a string in YYYYMMDD format"),
db: Session = Depends(get_db),
):
try:
parsed_date = parse_date(game_date)
except ValueError as ex:
raise HTTPException(status_code=400, detail=ex.message)
result = Season.is_date_in_season(db, parsed_date)
if result.failure:
raise HTTPException(status_code=400, detail=result.error)
self.date = parsed_date
self.season = convert_season_to_dict(result.value)

def __str__(self):
return self.date.strftime("%Y-%m-%d")
```

Please note the custom `__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

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.
2 changes: 1 addition & 1 deletion src/fastapi_redis_cache/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ async def inner_wrapper(*args, **kwargs):
ttl, in_cache = redis_cache.check_cache(key)
if in_cache:
if redis_cache.requested_resource_not_modified(request, in_cache):
response.status_code = HTTPStatus.NOT_MODIFIED
response.status_code = int(HTTPStatus.NOT_MODIFIED)
return response
cached_data = redis_cache.deserialize_json(in_cache)
redis_cache.set_response_headers(response, cache_hit=True, response_data=cached_data, ttl=ttl)
Expand Down
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", "1", "3") # pragma: no cover
__version_info__ = ("0", "1", "4") # pragma: no cover
__version__ = ".".join(__version_info__) # pragma: no cover

0 comments on commit 472a638

Please sign in to comment.