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
2634This project is a continuation of
@@ -179,23 +187,7 @@ async def get_immutable_data():
179187
180188Response data for the API endpoint at ` /immutable_data ` will be cached by the
181189Redis 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
200192In most situations, response data must expire in a much shorter period of time
201193than 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
270214The 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
450266If you have any questions, please open an issue. Any suggestions and
451267contributions are absolutely welcome. This is still a very small and young
452268project, I plan on adding a feature roadmap and further documentation in the
453269near 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