Skip to content

Commit 42950e0

Browse files
committed
ARQ Jobs queue integrated, documentation updated
1 parent 74e7406 commit 42950e0

File tree

10 files changed

+246
-35
lines changed

10 files changed

+246
-35
lines changed

README.md

Lines changed: 145 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
<p align="center">
77
<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="30%" height="auto">
8+
<img src="https://user-images.githubusercontent.com/43156212/277095260-ef5d4496-8290-4b18-99b2-0c0b5500504e.png" width="35%" height="auto">
99
</a>
1010
</p>
1111

@@ -16,26 +16,22 @@
1616
- [`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/)
1717
- [`SQLAlchemy 2.0`](https://docs.sqlalchemy.org/en/20/changelog/whatsnew_20.html): Python SQL toolkit and Object Relational Mapper
1818
- [`PostgreSQL`](https://www.postgresql.org): The World's Most Advanced Open Source Relational Database
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.
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.
2021

2122
## 1. Features
2223
- Fully async
2324
- Pydantic V2 and SQLAlchemy 2.0
2425
- User authentication with JWT
2526
- Easy redis caching
2627
- Easy client-side caching
28+
- ARQ integration for task queue
2729
- Easily extendable
2830
- Flexible
2931

30-
### 1.1 To do
31-
- [x] Redis cache
32-
- [ ] Arq job queues
33-
- [x] App settings (such as database connection, etc) only for what's inherited in core.config.Settings
34-
3532
## 2. Contents
3633
0. [About](#0-about)
3734
1. [Features](#1-features)
38-
1. [To do](#11-to-do)
3935
2. [Contents](#2-contents)
4036
3. [Usage](#3-usage)
4137
4. [Requirements](#4-requirements)
@@ -48,15 +44,17 @@
4844
7. [Creating the first superuser](#7-creating-the-first-superuser)
4945
8. [Database Migrations](#8-database-migrations)
5046
9. [Extending](#9-extending)
51-
1. [Database Model](#91-database-model)
52-
2. [SQLAlchemy Models](#92-sqlalchemy-model)
53-
3. [Pydantic Schemas](#93-pydantic-schemas)
54-
4. [Alembic Migrations](#94-alembic-migration)
55-
5. [CRUD](#95-crud)
56-
6. [Routes](#96-routes)
57-
7. [Caching](#97-caching)
58-
8. [More Advanced Caching](#98-more-advanced-caching)
59-
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)
6058
10. [Testing](#10-testing)
6159
11. [Contributing](#11-contributing)
6260
12. [References](#12-references)
@@ -140,10 +138,18 @@ REDIS_CACHE_PORT=6379
140138
141139
And for client-side caching:
142140
```
143-
# ------------- redis -------------
141+
# ------------- redis cache -------------
142+
REDIS_CACHE_HOST="your_host" # default localhost
143+
REDIS_CACHE_PORT=6379
144+
```
145+
146+
For ARQ Job Queues:
147+
```
148+
# ------------- redis queue -------------
144149
REDIS_CACHE_HOST="your_host" # default localhost
145150
REDIS_CACHE_PORT=6379
146151
```
152+
147153
___
148154
## 5. Running Databases With Docker:
149155
### 5.1 PostgreSQL (main database)
@@ -174,7 +180,7 @@ docker run -d \
174180

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

177-
### 5.2 Redis (for caching)
183+
### 5.2 Redis (for caching and job queue)
178184
Install docker if you don't have it yet, then run:
179185
```sh
180186
docker pull redis:alpine
@@ -227,11 +233,80 @@ poetry run alembic upgrade head
227233

228234
___
229235
## 9. Extending
230-
### 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
231306
Create the new entities and relationships and add them to the model
232307
![diagram](https://user-images.githubusercontent.com/43156212/274053323-31bbdb41-15bf-45f2-8c8e-0b04b71c5b0b.png)
233308

234-
### 9.2 SQLAlchemy Model
309+
### 9.3 SQLAlchemy Model
235310
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):
236311
```python
237312
from sqlalchemy import String, DateTime
@@ -249,7 +324,7 @@ class Entity(Base):
249324
...
250325
```
251326

252-
### 9.3 Pydantic Schemas
327+
### 9.4 Pydantic Schemas
253328
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:
254329
```python
255330
from typing import Annotated
@@ -289,7 +364,7 @@ class EntityDelete(BaseModel):
289364

290365
```
291366

292-
### 9.4 Alembic Migration
367+
### 9.5 Alembic Migration
293368
Then, while in the `src` folder, run Alembic migrations:
294369
```sh
295370
poetry run alembic revision --autogenerate
@@ -300,7 +375,7 @@ And to apply the migration
300375
poetry run alembic upgrade head
301376
```
302377

303-
### 9.5 CRUD
378+
### 9.6 CRUD
304379
Inside `app/crud`, create a new `crud_entities.py` inheriting from `CRUDBase` for each new entity:
305380
```python
306381
from app.crud.crud_base import CRUDBase
@@ -311,7 +386,7 @@ CRUDEntity = CRUDBase[Entity, EntityCreateInternal, EntityUpdate, EntityUpdateIn
311386
crud_entity = CRUDEntity(Entity)
312387
```
313388

314-
### 9.6 Routes
389+
### 9.7 Routes
315390
Inside `app/api/v1`, create a new `entities.py` file and create the desired routes
316391
```python
317392
from typing import Annotated
@@ -342,7 +417,7 @@ router = APIRouter(prefix="/v1") # this should be there already
342417
router.include_router(entity_router)
343418
```
344419

345-
### 9.7 Caching
420+
### 9.8 Caching
346421
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.
347422

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

391466
Passing resource_id_name is usually preferred.
392467

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

@@ -451,7 +526,48 @@ async def patch_post(
451526
#### Client-side Caching
452527
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).
453528

454-
### 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
455571
While in the `src` folder, run to start the application with uvicorn server:
456572
```sh
457573
poetry run uvicorn app.main:app --reload
@@ -491,7 +607,7 @@ Contributions are appreciated, even if just reporting bugs, documenting stuff or
491607
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)
492608
* [`Full Stack FastAPI and PostgreSQL`](https://github.com/tiangolo/full-stack-fastapi-postgresql) by @tiangolo himself
493609
* [`FastAPI Microservices`](https://github.com/Kludex/fastapi-microservices) by @kludex which heavily inspired this boilerplate
494-
* [`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
495611

496612
## 13. License
497613
[`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)