|
7 | 7 | - [`Pydantic V2`](https://docs.pydantic.dev/2.4/): the most widely used data validation library for Python, now rewritten in Rust [`(5x to 50x speed improvement)`](https://docs.pydantic.dev/latest/blog/pydantic-v2-alpha/) |
8 | 8 | - [`SQLAlchemy 2.0`](https://docs.sqlalchemy.org/en/20/changelog/whatsnew_20.html): Python SQL toolkit and Object Relational Mapper |
9 | 9 | - [`PostgreSQL`](https://www.postgresql.org): The World's Most Advanced Open Source Relational Database |
| 10 | +- [`Redis`](https://redis.io): The open source, in-memory data store used by millions of developers as a database, cache, streaming engine, and message broker. |
10 | 11 |
|
11 | 12 | ## 1. Features |
12 | 13 | - Fully async |
13 | 14 | - Pydantic V2 and SQLAlchemy 2.0 |
14 | 15 | - User authentication with JWT |
| 16 | + - Easy redis caching |
15 | 17 | - Easily extendable |
16 | 18 | - Flexible |
17 | 19 |
|
18 | 20 | ### 1.1 To do |
19 | | -- [ ] Redis cache |
20 | | -- [ ] Google SSO |
| 21 | +- [x] Redis cache |
21 | 22 | - [ ] Arq job queues |
| 23 | +- [ ] App settings (such as database connection, etc) only for what's inherited in core.config.Settings |
22 | 24 |
|
23 | 25 | ## 2. Contents |
24 | 26 | 0. [About](#0-about) |
|
29 | 31 | 4. [Requirements](#4-requirements) |
30 | 32 | 1. [Packages](#41-packages) |
31 | 33 | 2. [Environment Variables](#42-environment-variables) |
32 | | -5. [Running PostgreSQL with docker](#5-running-postgresql-with-docker) |
| 34 | +5. [Running Databases With Docker](#5-running-databases-with-docker) |
| 35 | + 1. [PostgreSQL](#51-postgresql-main-database) |
| 36 | + 2. [Redis](#52-redis-for-caching) |
33 | 37 | 6. [Running the api](#6-running-the-api) |
34 | 38 | 7. [Creating the first superuser](#7-creating-the-first-superuser) |
35 | 39 | 8. [Database Migrations](#8-database-migrations) |
|
40 | 44 | 4. [Alembic Migrations](#94-alembic-migration) |
41 | 45 | 5. [CRUD](#95-crud) |
42 | 46 | 6. [Routes](#96-routes) |
43 | | - 7. [Running](#97-running) |
| 47 | + 7. [Caching](#97-caching) |
| 48 | + 8. [More Advanced Caching](#98-more-advanced-caching) |
| 49 | + 9. [Running](#99-running) |
44 | 50 | 10. [Testing](#10-testing) |
45 | 51 | 11. [Contributing](#11-contributing) |
46 | 52 | 12. [References](#12-references) |
@@ -116,8 +122,15 @@ ADMIN_USERNAME="your_username" |
116 | 122 | ADMIN_PASSWORD="your_password" |
117 | 123 | ``` |
118 | 124 |
|
| 125 | +Optionally, for redis caching: |
| 126 | +``` |
| 127 | +# ------------- redis ------------- |
| 128 | +REDIS_CACHE_HOST="your_host" # default localhost |
| 129 | +REDIS_CACHE_PORT=6379 |
| 130 | +``` |
119 | 131 | ___ |
120 | | -## 5. Running PostgreSQL with docker: |
| 132 | +## 5. Running Databases With Docker: |
| 133 | +### 5.1 PostgreSQL (main database) |
121 | 134 | Install docker if you don't have it yet, then run: |
122 | 135 | ```sh |
123 | 136 | docker pull postgres |
@@ -145,6 +158,29 @@ docker run -d \ |
145 | 158 |
|
146 | 159 | [`If you didn't create the .env variables yet, click here.`](#environment-variables) |
147 | 160 |
|
| 161 | +### 5.2 Redis (for caching) |
| 162 | +Install docker if you don't have it yet, then run: |
| 163 | +```sh |
| 164 | +docker pull redis:alpine |
| 165 | +``` |
| 166 | + |
| 167 | +And pick the name and port, replacing the fields: |
| 168 | +```sh |
| 169 | +docker run -d \ |
| 170 | + --name {NAME} \ |
| 171 | + -p {PORT}:{PORT} \ |
| 172 | +redis:alpine |
| 173 | +``` |
| 174 | + |
| 175 | +Such as |
| 176 | +```sh |
| 177 | +docker run -d \ |
| 178 | + --name redis \ |
| 179 | + -p 6379:6379 \ |
| 180 | +redis:alpine |
| 181 | +``` |
| 182 | + |
| 183 | +[`If you didn't create the .env variables yet, click here.`](#environment-variables) |
148 | 184 | ___ |
149 | 185 | ## 6. Running the api |
150 | 186 | While in the **src** folder, run to start the application with uvicorn server: |
@@ -290,7 +326,109 @@ router = APIRouter(prefix="/v1") # this should be there already |
290 | 326 | router.include_router(entity_router) |
291 | 327 | ``` |
292 | 328 |
|
293 | | -### 9.7 Running |
| 329 | +### 9.7 Caching |
| 330 | +The cache decorator allows you to cache the results of FastAPI endpoint functions, enhancing response times and reducing the load on your application by storing and retrieving data in a cache. |
| 331 | + |
| 332 | +Caching the response of an endpoint is really simple, just apply the cache decorator to the endpoint function. |
| 333 | +Note that you should always pass request as a variable to your endpoint function. |
| 334 | +```python |
| 335 | +... |
| 336 | +from app.core.cache import cache |
| 337 | + |
| 338 | +@app.get("/sample/{my_id}") |
| 339 | +@cache( |
| 340 | + key_prefix="sample_data", |
| 341 | + expiration=3600, |
| 342 | + resource_id_name="my_id" |
| 343 | +) |
| 344 | +async def sample_endpoint(request: Request, my_id: int): |
| 345 | + # Endpoint logic here |
| 346 | + return {"data": "my_data"} |
| 347 | +``` |
| 348 | + |
| 349 | +The way it works is: |
| 350 | +- the data is saved in redis with the following cache key: "sample_data:{my_id}" |
| 351 | +- then the the time to expire is set as 3600 seconds (that's the default) |
| 352 | + |
| 353 | +Another option is not passing the resource_id_name, but passing the resource_id_type (default int): |
| 354 | +```python |
| 355 | +... |
| 356 | +from app.core.cache import cache |
| 357 | + |
| 358 | +@app.get("/sample/{my_id}") |
| 359 | +@cache( |
| 360 | + key_prefix="sample_data", |
| 361 | + resource_id_type=int |
| 362 | +) |
| 363 | +async def sample_endpoint(request: Request, my_id: int): |
| 364 | + # Endpoint logic here |
| 365 | + return {"data": "my_data"} |
| 366 | +``` |
| 367 | +In this case, what will happen is: |
| 368 | +- the resource_id will be inferred from the keyword arguments (my_id in this case) |
| 369 | +- the data is saved in redis with the following cache key: "sample_data:{my_id}" |
| 370 | +- then the the time to expire is set as 3600 seconds (that's the default) |
| 371 | + |
| 372 | +Passing resource_id_name is usually preferred. |
| 373 | + |
| 374 | +### 9.8 More Advanced Caching |
| 375 | +The behaviour of the `cache` decorator changes based on the request method of your endpoint. |
| 376 | +It caches the result if you are passing it to a **GET** endpoint, and it invalidates the cache with this key_prefix and id if passed to other endpoints (**PATCH**, **DELETE**). |
| 377 | + |
| 378 | +If you also want to invalidate cache with a different key, you can use the decorator with the "to_invalidate_extra" variable. |
| 379 | + |
| 380 | +In the following example, I want to invalidate the cache for a certain user_id, since I'm deleting it, but I also want to invalidate the cache for the list of users, so it will not be out of sync. |
| 381 | + |
| 382 | +```python |
| 383 | +# The cache here will be saved as "{username}_posts:{username}": |
| 384 | +@router.get("/{username}/posts", response_model=List[PostRead]) |
| 385 | +@cache(key_prefix="{username}_posts", resource_id_name="username") |
| 386 | +async def read_posts( |
| 387 | + request: Request, |
| 388 | + username: str, |
| 389 | + db: Annotated[AsyncSession, Depends(async_get_db)] |
| 390 | +): |
| 391 | + ... |
| 392 | + |
| 393 | +... |
| 394 | + |
| 395 | +# I'll invalidate the cache for the former endpoint by just passing the key_prefix and id as a dictionary: |
| 396 | +@router.delete("/{username}/post/{id}") |
| 397 | +@cache( |
| 398 | + "{username}_post_cache", |
| 399 | + resource_id_name="id", |
| 400 | + to_invalidate_extra={"{username}_posts": "{username}"} # Now it will also invalidate the cache with id "{username}_posts:{username}" |
| 401 | +) |
| 402 | +async def erase_post( |
| 403 | + request: Request, |
| 404 | + username: str, |
| 405 | + id: int, |
| 406 | + current_user: Annotated[UserRead, Depends(get_current_user)], |
| 407 | + db: Annotated[AsyncSession, Depends(async_get_db)] |
| 408 | +): |
| 409 | + ... |
| 410 | + |
| 411 | +# And now I'll also invalidate when I update the user: |
| 412 | +@router.patch("/{username}/post/{id}", response_model=PostRead) |
| 413 | +@cache( |
| 414 | + "{username}_post_cache", |
| 415 | + resource_id_name="id", |
| 416 | + to_invalidate_extra={"{username}_posts": "{username}"} |
| 417 | +) |
| 418 | +async def patch_post( |
| 419 | + request: Request, |
| 420 | + username: str, |
| 421 | + id: int, |
| 422 | + values: PostUpdate, |
| 423 | + current_user: Annotated[UserRead, Depends(get_current_user)], |
| 424 | + db: Annotated[AsyncSession, Depends(async_get_db)] |
| 425 | +): |
| 426 | + ... |
| 427 | +``` |
| 428 | + |
| 429 | +Note that this will not work for **GET** requests. |
| 430 | + |
| 431 | +### 9.9 Running |
294 | 432 | While in the **src** folder, run to start the application with uvicorn server: |
295 | 433 | ```sh |
296 | 434 | poetry run uvicorn app.main:app --reload |
|
0 commit comments