Skip to content

Commit b12f409

Browse files
authored
Merge pull request #14 from igormagalhaesr/arq-queue
Arq queue
2 parents 6e5c9b5 + 42950e0 commit b12f409

File tree

10 files changed

+259
-38
lines changed

10 files changed

+259
-38
lines changed

README.md

Lines changed: 158 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,37 @@
1-
# FastAPI-boilerplate
2-
>A template to speed your FastAPI development up.
1+
<h1 align="center"> Fast FastAPI boilerplate</h1>
2+
<p align="center" markdown=1>
3+
<i>Yet another template to speed your FastAPI development up.</i>
4+
</p>
5+
6+
<p align="center">
7+
<a href="https://github.com/igormagalhaesr/FastAPI-boilerplate">
8+
<img src="https://user-images.githubusercontent.com/43156212/277095260-ef5d4496-8290-4b18-99b2-0c0b5500504e.png" width="35%" height="auto">
9+
</a>
10+
</p>
11+
312

413
## 0. About
514
**FastAPI boilerplate** creates an extendable async API using FastAPI, Pydantic V2, SQLAlchemy 2.0 and PostgreSQL:
615
- [`FastAPI`](https://fastapi.tiangolo.com): modern Python web framework for building APIs
716
- [`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/)
817
- [`SQLAlchemy 2.0`](https://docs.sqlalchemy.org/en/20/changelog/whatsnew_20.html): Python SQL toolkit and Object Relational Mapper
918
- [`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.
19+
- [`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
20+
- [`ARQ`](https://arq-docs.helpmanual.io) Job queues and RPC in python with asyncio and redis.
1121

1222
## 1. Features
1323
- Fully async
1424
- Pydantic V2 and SQLAlchemy 2.0
1525
- User authentication with JWT
1626
- Easy redis caching
1727
- Easy client-side caching
28+
- ARQ integration for task queue
1829
- Easily extendable
1930
- Flexible
2031

21-
### 1.1 To do
22-
- [x] Redis cache
23-
- [ ] Arq job queues
24-
- [x] App settings (such as database connection, etc) only for what's inherited in core.config.Settings
25-
2632
## 2. Contents
2733
0. [About](#0-about)
2834
1. [Features](#1-features)
29-
1. [To do](#11-to-do)
3035
2. [Contents](#2-contents)
3136
3. [Usage](#3-usage)
3237
4. [Requirements](#4-requirements)
@@ -39,15 +44,17 @@
3944
7. [Creating the first superuser](#7-creating-the-first-superuser)
4045
8. [Database Migrations](#8-database-migrations)
4146
9. [Extending](#9-extending)
42-
1. [Database Model](#91-database-model)
43-
2. [SQLAlchemy Models](#92-sqlalchemy-model)
44-
3. [Pydantic Schemas](#93-pydantic-schemas)
45-
4. [Alembic Migrations](#94-alembic-migration)
46-
5. [CRUD](#95-crud)
47-
6. [Routes](#96-routes)
48-
7. [Caching](#97-caching)
49-
8. [More Advanced Caching](#98-more-advanced-caching)
50-
9. [Running](#99-running)
47+
1. [Project Structure](#91-project-structure)
48+
2. [Database Model](#92-database-model)
49+
3. [SQLAlchemy Models](#93-sqlalchemy-model)
50+
4. [Pydantic Schemas](#94-pydantic-schemas)
51+
5. [Alembic Migrations](#95-alembic-migration)
52+
6. [CRUD](#96-crud)
53+
7. [Routes](#97-routes)
54+
8. [Caching](#98-caching)
55+
9. [More Advanced Caching](#99-more-advanced-caching)
56+
10. [ARQ Job Queues](#910-arq-job-queues)
57+
11. [Running](#911-running)
5158
10. [Testing](#10-testing)
5259
11. [Contributing](#11-contributing)
5360
12. [References](#12-references)
@@ -68,7 +75,7 @@ Then install poetry:
6875
pip install poetry
6976
```
7077

71-
In the **src** directory, run to install required packages:
78+
In the `src` directory, run to install required packages:
7279
```sh
7380
poetry install
7481
```
@@ -131,10 +138,18 @@ REDIS_CACHE_PORT=6379
131138
132139
And for client-side caching:
133140
```
134-
# ------------- redis -------------
141+
# ------------- redis cache -------------
135142
REDIS_CACHE_HOST="your_host" # default localhost
136143
REDIS_CACHE_PORT=6379
137144
```
145+
146+
For ARQ Job Queues:
147+
```
148+
# ------------- redis queue -------------
149+
REDIS_CACHE_HOST="your_host" # default localhost
150+
REDIS_CACHE_PORT=6379
151+
```
152+
138153
___
139154
## 5. Running Databases With Docker:
140155
### 5.1 PostgreSQL (main database)
@@ -165,7 +180,7 @@ docker run -d \
165180

166181
[`If you didn't create the .env variables yet, click here.`](#environment-variables)
167182

168-
### 5.2 Redis (for caching)
183+
### 5.2 Redis (for caching and job queue)
169184
Install docker if you don't have it yet, then run:
170185
```sh
171186
docker pull redis:alpine
@@ -218,11 +233,80 @@ poetry run alembic upgrade head
218233

219234
___
220235
## 9. Extending
221-
### 9.1 Database Model
236+
### 9.1 Project Structure
237+
```sh
238+
.
239+
├── .env # Environment variables file for configuration and secrets.
240+
├── __init__.py # An initialization file for the package.
241+
├── alembic.ini # Configuration file for Alembic (database migration tool).
242+
├── app # Main application directory.
243+
│ ├── __init__.py # Initialization file for the app package.
244+
│ ├── api # Folder containing API-related logic.
245+
│ │ ├── __init__.py # Initialization file for the api package.
246+
│ │ ├── dependencies.py # Defines dependencies that can be reused across the API endpoints.
247+
│ │ ├── exceptions.py # Contains custom exceptions for the API.
248+
│ │ └── v1 # Version 1 of the API.
249+
│ │ ├── __init__.py # Initialization file for the v1 package.
250+
│ │ ├── login.py # API routes related to user login.
251+
│ │ ├── posts.py # API routes related to posts.
252+
│ │ ├── tasks.py # API routes related to background tasks.
253+
│ │ └── users.py # API routes related to user management.
254+
│ │
255+
│ ├── core # Core utilities and configurations for the application.
256+
│ │ ├── __init__.py # Initialization file for the core package.
257+
│ │ ├── cache.py # Utilities related to caching.
258+
│ │ ├── config.py # Application configuration settings.
259+
│ │ ├── database.py # Database connectivity and session management.
260+
│ │ ├── exceptions.py # Contains core custom exceptions for the application.
261+
│ │ ├── models.py # Base models for the application.
262+
│ │ ├── queue.py # Utilities related to task queues.
263+
│ │ └── security.py # Security utilities like password hashing and token generation.
264+
│ │
265+
│ ├── crud # CRUD operations for the application.
266+
│ │ ├── __init__.py # Initialization file for the crud package.
267+
│ │ ├── crud_base.py # Base CRUD operations class that can be extended by other CRUD modules.
268+
│ │ ├── crud_posts.py # CRUD operations for posts.
269+
│ │ └── crud_users.py # CRUD operations for users.
270+
│ │
271+
│ ├── main.py # Entry point for the FastAPI application.
272+
│ │
273+
│ ├── models # ORM models for the application.
274+
│ │ ├── __init__.py # Initialization file for the models package.
275+
│ │ ├── post.py # ORM model for posts.
276+
│ │ └── user.py # ORM model for users.
277+
│ │
278+
│ ├── schemas # Pydantic schemas for data validation.
279+
│ │ ├── __init__.py # Initialization file for the schemas package.
280+
│ │ ├── job.py # Schemas related to background jobs.
281+
│ │ ├── post.py # Schemas related to posts.
282+
│ │ └── user.py # Schemas related to users.
283+
│ │
284+
│ └── worker.py # Worker script for handling background tasks.
285+
286+
├── migrations # Directory for Alembic migrations.
287+
│ ├── README # General info and guidelines for migrations.
288+
│ ├── env.py # Environment configurations for Alembic.
289+
│ ├── script.py.mako # Template script for migration generation.
290+
│ └── versions # Folder containing individual migration scripts.
291+
│ └── README.MD # Readme for the versions directory.
292+
293+
├── poetry.lock # Lock file for Poetry, ensuring consistent dependencies.
294+
├── pyproject.toml # Configuration file for Poetry, lists project dependencies.
295+
├── scripts # Utility scripts for the project.
296+
│ └── create_first_superuser.py # Script to create the first superuser in the application.
297+
298+
└── tests # Directory containing all the tests.
299+
├── __init__.py # Initialization file for the tests package.
300+
├── conftest.py # Configuration and fixtures for pytest.
301+
├── helper.py # Helper functions for writing tests.
302+
└── test_user.py # Tests related to the user model and endpoints.
303+
```
304+
305+
### 9.2 Database Model
222306
Create the new entities and relationships and add them to the model
223307
![diagram](https://user-images.githubusercontent.com/43156212/274053323-31bbdb41-15bf-45f2-8c8e-0b04b71c5b0b.png)
224308

225-
### 9.2 SQLAlchemy Model
309+
### 9.3 SQLAlchemy Model
226310
Inside `app/models`, create a new `entity.py` for each new entity (replacing entity with the name) and define the attributes according to [SQLAlchemy 2.0 standards](https://docs.sqlalchemy.org/en/20/orm/mapping_styles.html#orm-mapping-styles):
227311
```python
228312
from sqlalchemy import String, DateTime
@@ -240,7 +324,7 @@ class Entity(Base):
240324
...
241325
```
242326

243-
### 9.3 Pydantic Schemas
327+
### 9.4 Pydantic Schemas
244328
Inside `app/schemas`, create a new `entity.py` for for each new entity (replacing entity with the name) and create the schemas according to [Pydantic V2](https://docs.pydantic.dev/latest/#pydantic-examples) standards:
245329
```python
246330
from typing import Annotated
@@ -280,7 +364,7 @@ class EntityDelete(BaseModel):
280364

281365
```
282366

283-
### 9.4 Alembic Migration
367+
### 9.5 Alembic Migration
284368
Then, while in the `src` folder, run Alembic migrations:
285369
```sh
286370
poetry run alembic revision --autogenerate
@@ -291,7 +375,7 @@ And to apply the migration
291375
poetry run alembic upgrade head
292376
```
293377

294-
### 9.5 CRUD
378+
### 9.6 CRUD
295379
Inside `app/crud`, create a new `crud_entities.py` inheriting from `CRUDBase` for each new entity:
296380
```python
297381
from app.crud.crud_base import CRUDBase
@@ -302,7 +386,7 @@ CRUDEntity = CRUDBase[Entity, EntityCreateInternal, EntityUpdate, EntityUpdateIn
302386
crud_entity = CRUDEntity(Entity)
303387
```
304388

305-
### 9.6 Routes
389+
### 9.7 Routes
306390
Inside `app/api/v1`, create a new `entities.py` file and create the desired routes
307391
```python
308392
from typing import Annotated
@@ -333,7 +417,7 @@ router = APIRouter(prefix="/v1") # this should be there already
333417
router.include_router(entity_router)
334418
```
335419

336-
### 9.7 Caching
420+
### 9.8 Caching
337421
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.
338422

339423
Caching the response of an endpoint is really simple, just apply the `cache` decorator to the endpoint function.
@@ -381,7 +465,7 @@ In this case, what will happen is:
381465

382466
Passing resource_id_name is usually preferred.
383467

384-
### 9.8 More Advanced Caching
468+
### 9.9 More Advanced Caching
385469
The behaviour of the `cache` decorator changes based on the request method of your endpoint.
386470
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**).
387471

@@ -437,11 +521,53 @@ async def patch_post(
437521
```
438522

439523
> **Warning**
440-
> Note that this will not work for **GET** requests.
524+
> Note that adding `to_invalidate_extra` will not work for **GET** requests.
441525
526+
#### Client-side Caching
442527
For `client-side caching`, all you have to do is let the `Settings` class defined in `app/core/config.py` inherit from the `ClientSideCacheSettings` class. You can set the `CLIENT_CACHE_MAX_AGE` value in `.env,` it defaults to 60 (seconds).
443528

444-
### 9.9 Running
529+
### 9.10 ARQ Job Queues
530+
Create the background task in `app/worker.py`:
531+
```python
532+
...
533+
# -------- background tasks --------
534+
async def sample_background_task(ctx, name: str) -> str:
535+
await asyncio.sleep(5)
536+
return f"Task {name} is complete!"
537+
```
538+
539+
Then add the function to the `WorkerSettings` class `functions` variable:
540+
```python
541+
# -------- class --------
542+
...
543+
class WorkerSettings:
544+
functions = [sample_background_task]
545+
...
546+
```
547+
548+
Add the task to be enqueued in a **POST** endpoint and get the info in a **GET**:
549+
```python
550+
...
551+
@router.post("/task", response_model=Job, status_code=201)
552+
async def create_task(message: str):
553+
job = await queue.pool.enqueue_job("sample_background_task", message)
554+
return {"id": job.job_id}
555+
556+
557+
@router.get("/task/{task_id}")
558+
async def get_task(task_id: str):
559+
job = ArqJob(task_id, queue.pool)
560+
return await job.info()
561+
562+
```
563+
564+
And finally run the worker in parallel to your fastapi application.
565+
While in the `src` folder:
566+
```sh
567+
poetry run arq app.worker.WorkerSettings
568+
```
569+
570+
### 9.11 Running
445571
While in the `src` folder, run to start the application with uvicorn server:
446572
```sh
447573
poetry run uvicorn app.main:app --reload
@@ -481,7 +607,7 @@ Contributions are appreciated, even if just reporting bugs, documenting stuff or
481607
This project was inspired by a few projects, it's based on them with things changed to the way I like (and pydantic, sqlalchemy updated)
482608
* [`Full Stack FastAPI and PostgreSQL`](https://github.com/tiangolo/full-stack-fastapi-postgresql) by @tiangolo himself
483609
* [`FastAPI Microservices`](https://github.com/Kludex/fastapi-microservices) by @kludex which heavily inspired this boilerplate
484-
* [`Async Web API with FastAPI + SQLAlchemy 2.0`](https://github.com/rhoboro/async-fastapi-sqlalchemy)
610+
* [`Async Web API with FastAPI + SQLAlchemy 2.0`](https://github.com/rhoboro/async-fastapi-sqlalchemy) for sqlalchemy 2.0 ORM examples
485611

486612
## 13. License
487613
[`MIT`](LICENSE.md)

src/app/api/v1/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
from app.api.v1.login import router as login_router
44
from app.api.v1.users import router as users_router
55
from app.api.v1.posts import router as posts_router
6+
from app.api.v1.tasks import router as tasks_router
67

78
router = APIRouter(prefix="/v1")
89
router.include_router(login_router)
910
router.include_router(users_router)
1011
router.include_router(posts_router)
12+
router.include_router(tasks_router)

src/app/api/v1/tasks.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from arq.jobs import Job as ArqJob
2+
from fastapi import APIRouter, HTTPException
3+
4+
from app.core import queue
5+
from app.schemas.job import Job
6+
7+
router = APIRouter(prefix="/tasks", tags=["Tasks"])
8+
9+
10+
@router.post("/task", response_model=Job, status_code=201)
11+
async def create_task(message: str):
12+
job = await queue.pool.enqueue_job("sample_background_task", message)
13+
return {"id": job.job_id}
14+
15+
16+
@router.get("/task/{task_id}")
17+
async def get_task(task_id: str):
18+
job = ArqJob(task_id, queue.pool)
19+
return await job.info()

src/app/api/v1/users.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from fastapi import Request
66
import fastapi
77

8-
from app.schemas.user import UserCreate, UserCreateInternal, UserUpdate, UserRead, UserBase
8+
from app.schemas.user import UserCreate, UserCreateInternal, UserUpdate, UserRead
99
from app.api.dependencies import get_current_user, get_current_superuser
1010
from app.core.database import async_get_db
1111
from app.core.security import get_password_hash

src/app/core/config.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,14 +76,20 @@ class ClientSideCacheSettings(BaseSettings):
7676
CLIENT_CACHE_MAX_AGE: int = config("CLIENT_CACHE_MAX_AGE", default=60)
7777

7878

79+
class RedisQueueSettings(BaseSettings):
80+
REDIS_QUEUE_HOST: str = config("REDIS_QUEUE_HOST", default="localhost")
81+
REDIS_QUEUE_PORT: str = config("REDIS_QUEUE_PORT", default=6379)
82+
83+
7984
class Settings(
8085
AppSettings,
8186
PostgresSettings,
8287
CryptSettings,
8388
FirstUserSettings,
8489
TestSettings,
8590
RedisCacheSettings,
86-
ClientSideCacheSettings
91+
ClientSideCacheSettings,
92+
RedisQueueSettings
8793
):
8894
pass
8995

src/app/core/queue.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from arq.connections import ArqRedis
2+
3+
pool: ArqRedis | None = None

0 commit comments

Comments
 (0)