Skip to content

Commit dedeee7

Browse files
committed
update README
Signed-off-by: Grant Ramsay <seapagan@gmail.com>
1 parent af2a74b commit dedeee7

File tree

1 file changed

+22
-203
lines changed

1 file changed

+22
-203
lines changed

README.md

Lines changed: 22 additions & 203 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,27 @@
88
![PyPI - License](https://img.shields.io/pypi/l/fastapi-redis-cache-reborn?color=%25234DC71F)
99
![PyPI - Downloads](https://img.shields.io/pypi/dm/fastapi-redis-cache-reborn?color=%234DC71F)
1010

11+
- [Documentation Site](#documentation-site)
1112
- [Migrating from `fastapi-redis-cache`](#migrating-from-fastapi-redis-cache)
1213
- [Features](#features)
1314
- [Installation](#installation)
1415
- [Usage](#usage)
1516
- [Redis Server](#redis-server)
1617
- [Initialize Redis in your FastAPI application](#initialize-redis-in-your-fastapi-application)
1718
- [`@cache` Decorator](#cache-decorator)
18-
- [Response Headers](#response-headers)
1919
- [Pre-defined Lifetimes](#pre-defined-lifetimes)
20-
- [Cache Keys](#cache-keys)
21-
- [Cache Keys Pt 2](#cache-keys-pt-2)
2220
- [Questions/Contributions](#questionscontributions)
2321

22+
## Documentation Site
23+
24+
There is now a documentation site at
25+
<https://seapagan.github.io/fastapi-redis-cache-reborn/> that is generated from
26+
the `docs` folder in this repository. The site is built using
27+
[MkDocs](https://www.mkdocs.org/) and the [Material for
28+
MkDocs](https://squidfunk.github.io/mkdocs-material/) theme. The documentation
29+
is a work in progress, but I will be adding more content as time goes on, and
30+
cutting down the README file to be more concise.
31+
2432
## Migrating from `fastapi-redis-cache`
2533

2634
This project is a continuation of
@@ -179,23 +187,7 @@ async def get_immutable_data():
179187

180188
Response data for the API endpoint at `/immutable_data` will be cached by the
181189
Redis server. Log messages are written to standard output whenever a response is
182-
added to or retrieved from the cache:
183-
184-
```console
185-
INFO:fastapi_redis_cache:| 04/21/2021 12:26:26 AM | CONNECT_BEGIN: Attempting to connect to Redis server...
186-
INFO:fastapi_redis_cache:| 04/21/2021 12:26:26 AM | CONNECT_SUCCESS: Redis client is connected to server.
187-
INFO:fastapi_redis_cache:| 04/21/2021 12:26:34 AM | KEY_ADDED_TO_CACHE: key=api.get_immutable_data()
188-
INFO: 127.0.0.1:61779 - "GET /immutable_data HTTP/1.1" 200 OK
189-
INFO:fastapi_redis_cache:| 04/21/2021 12:26:45 AM | KEY_FOUND_IN_CACHE: key=api.get_immutable_data()
190-
INFO: 127.0.0.1:61779 - "GET /immutable_data HTTP/1.1" 200 OK
191-
```
192-
193-
The log messages show two successful (**`200 OK`**) responses to the same
194-
request (**`GET /immutable_data`**). The first request executed the
195-
`get_immutable_data` function and stored the result in Redis under key
196-
`api.get_immutable_data()`. The second request _**did not**_ execute the
197-
`get_immutable_data` function, instead the cached result was retrieved and sent
198-
as the response.
190+
added to or retrieved from the cache.
199191

200192
In most situations, response data must expire in a much shorter period of time
201193
than one year. Using the `expire` parameter, You can specify the number of
@@ -209,62 +201,14 @@ def get_dynamic_data(request: Request, response: Response):
209201
return {"success": True, "message": "this data should only be cached temporarily"}
210202
```
211203

212-
> **NOTE!** `expire` can be either an `int` value or `timedelta` object. When
204+
[!NOTE]
205+
> `expire` can be either an `int` value or `timedelta` object. When
213206
> the TTL is very short (like the example above) this results in a decorator
214207
> that is expressive and requires minimal effort to parse visually. For
215208
> durations an hour or longer (e.g., `@cache(expire=86400)`), IMHO, using a
216209
> `timedelta` object is much easier to grok
217210
> (`@cache(expire=timedelta(days=1))`).
218211
219-
#### Response Headers
220-
221-
A response from the `/dynamic_data` endpoint showing all header values is given
222-
below:
223-
224-
```console
225-
$ http "http://127.0.0.1:8000/dynamic_data"
226-
HTTP/1.1 200 OK
227-
cache-control: max-age=29
228-
content-length: 72
229-
content-type: application/json
230-
date: Wed, 21 Apr 2021 07:54:33 GMT
231-
etag: W/-5480454928453453778
232-
expires: Wed, 21 Apr 2021 07:55:03 GMT
233-
server: uvicorn
234-
x-fastapi-cache: Hit
235-
236-
{
237-
"message": "this data should only be cached temporarily",
238-
"success": true
239-
}
240-
```
241-
242-
- The `x-fastapi-cache` header field indicates that this response was found in
243-
the Redis cache (a.k.a. a `Hit`). The only other possible value for this field
244-
is `Miss`.
245-
- The `expires` field and `max-age` value in the `cache-control` field indicate
246-
that this response will be considered fresh for 29 seconds. This is expected
247-
since `expire=30` was specified in the `@cache` decorator.
248-
- The `etag` field is an identifier that is created by converting the response
249-
data to a string and applying a hash function. If a request containing the
250-
`if-none-match` header is received, any `etag` value(s) included in the
251-
request will be used to determine if the data requested is the same as the
252-
data stored in the cache. If they are the same, a `304 NOT MODIFIED` response
253-
will be sent. If they are not the same, the cached data will be sent with a
254-
`200 OK` response.
255-
256-
These header fields are used by your web browser's cache to avoid sending
257-
unnecessary requests. After receiving the response shown above, if a user
258-
requested the same resource before the `expires` time, the browser wouldn't send
259-
a request to the FastAPI server. Instead, the cached response would be served
260-
directly from disk.
261-
262-
Of course, this assumes that the browser is configured to perform caching. If
263-
the browser sends a request with the `cache-control` header containing
264-
`no-cache` or `no-store`, the `cache-control`, `etag`, `expires`, and
265-
`x-fastapi-cache` response header fields will not be included and the response
266-
data will not be stored in Redis.
267-
268212
#### Pre-defined Lifetimes
269213

270214
The decorators listed below define several common durations and can be used in
@@ -311,143 +255,18 @@ def partial_cache_two_hours(response: Response):
311255
return {"success": True, "message": "this data should be cached for two hours"}
312256
```
313257

314-
### Cache Keys
315-
316-
Consider the `/get_user` API route defined below. This is the first path
317-
function we have seen where the response depends on the value of an argument
318-
(`id: int`). This is a typical CRUD operation where `id` is used to retrieve a
319-
`User` record from a database. The API route also includes a dependency that
320-
injects a `Session` object (`db`) into the function, [per the instructions from
321-
the FastAPI
322-
docs](https://fastapi.tiangolo.com/tutorial/sql-databases/#create-a-dependency):
323-
324-
```python
325-
@app.get("/get_user", response_model=schemas.User)
326-
@cache(expire=3600)
327-
def get_user(id: int, db: Session = Depends(get_db)):
328-
return db.query(models.User).filter(models.User.id == id).first()
329-
```
330-
331-
In the [Initialize Redis](#initialize-redis-in-your-fastapi-application) section
332-
of this document, the `FastApiRedisCache.init` method was called with
333-
`ignore_arg_types=[Request, Response, Session]`. Why is it necessary to include
334-
`Session` in this list?
335-
336-
Before we can answer that question, we must understand how a cache key is
337-
created. If the following request was received: `GET /get_user?id=1`, the cache
338-
key generated would be `myapi-cache:api.get_user(id=1)`.
339-
340-
The source of each value used to construct this cache key is given below:
341-
342-
1) The optional `prefix` value provided as an argument to the
343-
`FastApiRedisCache.init` method => `"myapi-cache"`.
344-
2) The module containing the path function => `"api"`.
345-
3) The name of the path function => `"get_user"`.
346-
4) The name and value of all arguments to the path function **EXCEPT for
347-
arguments with a type that exists in** `ignore_arg_types` => `"id=1"`.
348-
349-
Since `Session` is included in `ignore_arg_types`, the `db` argument was not
350-
included in the cache key when **Step 4** was performed.
351-
352-
If `Session` had not been included in `ignore_arg_types`, caching would be
353-
completely broken. To understand why this is the case, see if you can figure out
354-
what is happening in the log messages below:
355-
356-
```console
357-
INFO:uvicorn.error:Application startup complete.
358-
INFO:fastapi_redis_cache.client: 04/23/2021 07:04:12 PM | KEY_ADDED_TO_CACHE: key=myapi-cache:api.get_user(id=1,db=<sqlalchemy.orm.session.Session object at 0x11b9fe550>)
359-
INFO: 127.0.0.1:50761 - "GET /get_user?id=1 HTTP/1.1" 200 OK
360-
INFO:fastapi_redis_cache.client: 04/23/2021 07:04:15 PM | KEY_ADDED_TO_CACHE: key=myapi-cache:api.get_user(id=1,db=<sqlalchemy.orm.session.Session object at 0x11c7f73a0>)
361-
INFO: 127.0.0.1:50761 - "GET /get_user?id=1 HTTP/1.1" 200 OK
362-
INFO:fastapi_redis_cache.client: 04/23/2021 07:04:17 PM | KEY_ADDED_TO_CACHE: key=myapi-cache:api.get_user(id=1,db=<sqlalchemy.orm.session.Session object at 0x11c7e35e0>)
363-
INFO: 127.0.0.1:50761 - "GET /get_user?id=1 HTTP/1.1" 200 OK
364-
```
365-
366-
The log messages indicate that three requests were received for the same
367-
endpoint, with the same arguments (`GET /get_user?id=1`). However, the cache key
368-
that is created is different for each request:
369-
370-
```console
371-
KEY_ADDED_TO_CACHE: key=myapi-cache:api.get_user(id=1,db=<sqlalchemy.orm.session.Session object at 0x11b9fe550>
372-
KEY_ADDED_TO_CACHE: key=myapi-cache:api.get_user(id=1,db=<sqlalchemy.orm.session.Session object at 0x11c7f73a0>
373-
KEY_ADDED_TO_CACHE: key=myapi-cache:api.get_user(id=1,db=<sqlalchemy.orm.session.Session object at 0x11c7e35e0>
374-
```
375-
376-
The value of each argument is added to the cache key by calling `str(arg)`. The
377-
`db` object includes the memory location when converted to a string, causing the
378-
same response data to be cached under three different keys! This is obviously
379-
not what we want.
380-
381-
The correct behavior (with `Session` included in `ignore_arg_types`) is shown
382-
below:
383-
384-
```console
385-
INFO:uvicorn.error:Application startup complete.
386-
INFO:fastapi_redis_cache.client: 04/23/2021 07:04:12 PM | KEY_ADDED_TO_CACHE: key=myapi-cache:api.get_user(id=1)
387-
INFO: 127.0.0.1:50761 - "GET /get_user?id=1 HTTP/1.1" 200 OK
388-
INFO:fastapi_redis_cache.client: 04/23/2021 07:04:12 PM | KEY_FOUND_IN_CACHE: key=myapi-cache:api.get_user(id=1)
389-
INFO: 127.0.0.1:50761 - "GET /get_user?id=1 HTTP/1.1" 200 OK
390-
INFO:fastapi_redis_cache.client: 04/23/2021 07:04:12 PM | KEY_FOUND_IN_CACHE: key=myapi-cache:api.get_user(id=1)
391-
INFO: 127.0.0.1:50761 - "GET /get_user?id=1 HTTP/1.1" 200 OK
392-
```
393-
394-
Now, every request for the same `id` generates the same key value
395-
(`myapi-cache:api.get_user(id=1)`). As expected, the first request adds the
396-
key/value pair to the cache, and each subsequent request retrieves the value
397-
from the cache based on the key.
398-
399-
### Cache Keys Pt 2
400-
401-
What about this situation? You create a custom dependency for your API that
402-
performs input validation, but you can't ignore it because _**it does**_ have an
403-
effect on the response data. There's a simple solution for that, too.
404-
405-
Here is an endpoint from one of my projects:
406-
407-
```python
408-
@router.get("/scoreboard", response_model=ScoreboardSchema)
409-
@cache()
410-
def get_scoreboard_for_date(
411-
game_date: MLBGameDate = Depends(), db: Session = Depends(get_db)
412-
):
413-
return get_scoreboard_data_for_date(db, game_date.date)
414-
```
415-
416-
The `game_date` argument is a `MLBGameDate` type. This is a custom type that
417-
parses the value from the querystring to a date, and determines if the parsed
418-
date is valid by checking if it is within a certain range. The implementation
419-
for `MLBGameDate` is given below:
420-
421-
```python
422-
class MLBGameDate:
423-
def __init__(
424-
self,
425-
game_date: str = Query(..., description="Date as a string in YYYYMMDD format"),
426-
db: Session = Depends(get_db),
427-
):
428-
try:
429-
parsed_date = parse_date(game_date)
430-
except ValueError as ex:
431-
raise HTTPException(status_code=400, detail=ex.message)
432-
result = Season.is_date_in_season(db, parsed_date)
433-
if result.failure:
434-
raise HTTPException(status_code=400, detail=result.error)
435-
self.date = parsed_date
436-
self.season = convert_season_to_dict(result.value)
437-
438-
def __str__(self):
439-
return self.date.strftime("%Y-%m-%d")
440-
```
441-
442-
Please note the `__str__` method that overrides the default behavior. This way,
443-
instead of `<MLBGameDate object at 0x11c7e35e0>`, the value will be formatted
444-
as, for example, `2019-05-09`. You can use this strategy whenever you have an
445-
argument that has en effect on the response data but converting that argument to
446-
a string results in a value containing the object's memory location.
258+
[!TIP]
259+
> Please read the full documentation on the [website][website] for more
260+
> information on the `@cache` decorator and the pre-defined lifetimes.
261+
> There is also a section on [cache keys][cache-keys] that explains how the
262+
> cache keys are generated and how to use them properly.
447263
448264
## Questions/Contributions
449265

450266
If you have any questions, please open an issue. Any suggestions and
451267
contributions are absolutely welcome. This is still a very small and young
452268
project, I plan on adding a feature roadmap and further documentation in the
453269
near future.
270+
271+
[website]: https://seapagan.github.io/fastapi-redis-cache-reborn/
272+
[cache-keys]: https://seapagan.github.io/fastapi-redis-cache-reborn/usage/#cache-keys

0 commit comments

Comments
 (0)