Skip to content

Commit bbe7ca9

Browse files
authored
feat: TaskIQ Integration option (#23)
1 parent a3feaea commit bbe7ca9

18 files changed

Lines changed: 316 additions & 36 deletions

File tree

fastapi_forge/dtos.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,7 @@ class ProjectSpec(_Base):
295295
use_builtin_auth: bool = False
296296
use_redis: bool = False
297297
use_rabbitmq: bool = False
298+
use_taskiq: bool = False
298299
models: list[Model] = []
299300

300301
@model_validator(mode="after")
@@ -329,6 +330,18 @@ def _validate_models(self) -> Self:
329330
msg = "Only one model can be an auth user."
330331
raise ValueError(msg)
331332

333+
if self.use_taskiq and not (self.use_redis and self.use_rabbitmq):
334+
missing = []
335+
if not self.use_rabbitmq:
336+
missing.append("RabbitMQ")
337+
if not self.use_redis:
338+
missing.append("Redis")
339+
340+
if missing:
341+
raise ValueError(
342+
"TaskIQ is enabled, but the following are missing and required "
343+
f"for its operation: {', '.join(missing)}."
344+
)
332345
return self
333346

334347
@model_validator(mode="after")

fastapi_forge/example-projects/game_zone.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ project:
55
use_builtin_auth: true
66
use_redis: true
77
use_rabbitmq: true
8+
use_taskiq: true
89

910
models:
1011
- name: auth_user

fastapi_forge/frontend/panels/project_config_panel.py

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ def __init__(self):
2222
super().__init__(value=True, elevated=False, bottom_corner=True)
2323
self._build()
2424
self._bind_state_to_ui()
25+
self._update_taskiq_state()
2526

2627
def _build(self) -> None:
2728
with (
@@ -83,16 +84,16 @@ def _build(self) -> None:
8384
self.use_rabbitmq = ui.checkbox(
8485
"RabbitMQ",
8586
value=state.use_rabbitmq,
87+
on_change=self._update_taskiq_state,
8688
).classes("w-full")
8789

8890
with ui.column().classes("w-full gap-2"):
8991
ui.label("Task Queues").classes("text-lg font-bold")
90-
self.use_taskiq = (
91-
ui.checkbox("Taskiq")
92-
.classes("w-full")
93-
.tooltip("Coming soon!")
94-
.set_enabled(False)
95-
)
92+
self.use_taskiq = ui.checkbox(
93+
"Taskiq",
94+
value=state.use_taskiq,
95+
on_change=self._update_taskiq_state,
96+
).classes("w-full")
9697
self.use_celery = (
9798
ui.checkbox("Celery")
9899
.classes("w-full")
@@ -123,6 +124,7 @@ def _build(self) -> None:
123124
self.use_redis = ui.checkbox(
124125
"Redis",
125126
value=state.use_redis,
127+
on_change=self._update_taskiq_state,
126128
).classes("w-full")
127129

128130
with ui.column().classes("w-full gap-2"):
@@ -141,7 +143,23 @@ def _bind_state_to_ui(self) -> None:
141143
self.use_postgres.bind_value_to(state, "use_postgres")
142144
self.use_alembic.bind_value_to(state, "use_alembic")
143145
self.use_builtin_auth.bind_value_to(state, "use_builtin_auth")
144-
self.use_rabbitmq.bind_value_to(state, "use_rabbitmq")
146+
self.use_rabbitmq.bind_value_to(state, "use_rabbitmq").on(
147+
"change", self._update_taskiq_state
148+
)
149+
self.use_redis.bind_value_to(state, "use_redis").on(
150+
"change", self._update_taskiq_state
151+
)
152+
self.use_taskiq.bind_value_to(state, "use_taskiq")
153+
154+
def _update_taskiq_state(self, *_) -> None:
155+
"""Enable or disable Taskiq based on Redis and RabbitMQ."""
156+
self.use_taskiq.set_enabled(self.use_redis.value and self.use_rabbitmq.value)
157+
if (
158+
not (self.use_redis.value and self.use_rabbitmq.value)
159+
and self.use_taskiq.value
160+
):
161+
self.use_taskiq.value = False
162+
state.use_taskiq = False
145163

146164
def _handle_builtin_auth_change(self, event: ValueChangeEventArguments) -> None:
147165
"""Handle JWT Auth checkbox changes"""
@@ -245,6 +263,7 @@ async def _create_project(self) -> None:
245263
state.use_builtin_auth = self.use_builtin_auth.value
246264
state.use_redis = self.use_redis.value
247265
state.use_rabbitmq = self.use_rabbitmq.value
266+
state.use_taskiq = self.use_taskiq.value
248267

249268
project_spec = state.get_project_spec()
250269
await build_project(project_spec)

fastapi_forge/frontend/state.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class ProjectState(BaseModel):
3737
use_builtin_auth: bool = False
3838
use_redis: bool = False
3939
use_rabbitmq: bool = False
40+
use_taskiq: bool = False
4041

4142
def initialize_from_project(self, project: ProjectSpec) -> None:
4243
"""Initialize state from an existing project specification."""
@@ -46,6 +47,7 @@ def initialize_from_project(self, project: ProjectSpec) -> None:
4647
self.use_builtin_auth = project.use_builtin_auth
4748
self.use_redis = project.use_redis
4849
self.use_rabbitmq = project.use_rabbitmq
50+
self.use_taskiq = project.use_taskiq
4951
self.models = project.models.copy()
5052

5153
self._trigger_ui_refresh()
@@ -111,6 +113,7 @@ def get_project_spec(self) -> ProjectSpec:
111113
use_builtin_auth=self.use_builtin_auth,
112114
use_redis=self.use_redis,
113115
use_rabbitmq=self.use_rabbitmq,
116+
use_taskiq=self.use_taskiq,
114117
models=self.models,
115118
)
116119

fastapi_forge/template/cookiecutter.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
"use_rabbitmq": {
1818
"default": true
1919
},
20+
"use_taskiq": {
21+
"default": true
22+
},
2023
"models": {
2124
"models": []
2225
},

fastapi_forge/template/{{cookiecutter.project_name}}/docker-compose.yaml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
services:
22
api:
3+
&api
34
build:
45
context: .
56
image: {{ cookiecutter.project_name }}:latest
@@ -71,6 +72,19 @@ services:
7172
path: pyproject.toml
7273
- action: rebuild
7374
path: uv.lock
75+
{% if cookiecutter.use_taskiq %}
76+
taskiq-worker:
77+
<<: *api
78+
container_name: game_zone-taskiq-worker
79+
ports: []
80+
command: [ taskiq, worker, -fsd, src.services.taskiq.broker:broker, -w, "1", --max-fails, "1"]
81+
82+
taskiq-scheduler:
83+
<<: *api
84+
container_name: game_zone-taskiq-scheduler
85+
ports: []
86+
command: [ taskiq, scheduler, -fsd, src.services.taskiq.scheduler:scheduler ]
87+
{%- endif %}
7488
{% if cookiecutter.use_postgres %}
7589
postgres:
7690
image: postgres:13.8-bullseye
@@ -108,7 +122,7 @@ services:
108122
{% endif %}
109123
{%- if cookiecutter.use_rabbitmq -%}
110124
rabbitmq:
111-
image: rabbitmq:3-management
125+
image: rabbitmq:3.8.27-management-alpine
112126
container_name: {{ cookiecutter.project_name }}-rabbitmq
113127
ports:
114128
- "15672:15672"

fastapi_forge/template/{{cookiecutter.project_name}}/forge-config.yaml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,17 @@ paths:
2828
enabled: {{cookiecutter.use_rabbitmq | lower}}
2929
paths:
3030
- src/services/rabbitmq
31-
- src/routes/demo_routes.py
31+
32+
use_taskiq:
33+
enabled: {{cookiecutter.use_taskiq | lower}}
34+
paths:
35+
- src/services/taskiq
3236

3337
constants:
3438
requires_all:
3539
- use_builtin_auth
3640
- use_rabbitmq
3741
paths:
38-
- src/constants.py
42+
- src/constants.py
43+
44+

fastapi_forge/template/{{cookiecutter.project_name}}/pyproject.toml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@ dependencies = [
1414
"ruff>=0.9.4",
1515
{%- if cookiecutter.use_postgres -%}
1616
"sqlalchemy[asyncio]>=2.0.37",
17-
{% endif %}
18-
{%- if cookiecutter.use_postgres -%}
1917
"asyncpg>=0.30.0",
2018
{% endif %}
2119
{%- if cookiecutter.use_builtin_auth -%}
@@ -31,10 +29,18 @@ dependencies = [
3129
"factory-boy>=3.3.3",
3230
{%- if cookiecutter.use_redis -%}
3331
"redis>=5.2.1",
32+
"fakeredis>=2.28.1",
3433
{% endif %}
3534
{%- if cookiecutter.use_rabbitmq -%}
3635
"aio-pika>=9.5.5",
3736
{%- endif %}
37+
{%- if cookiecutter.use_taskiq -%}
38+
"taskiq>=0.11.16",
39+
"taskiq-aio-pika>=0.4.1",
40+
"taskiq-redis>=1.0.4",
41+
"taskiq-fastapi>=0.3.4",
42+
"orjson>=3.10.16",
43+
{%- endif %}
3844
]
3945

4046
[tool.pytest.ini_options]
@@ -64,7 +70,9 @@ ignore = [
6470
#### specific rules
6571
"A001",
6672
"A002",
73+
"ARG002",
6774
"ARG001",
75+
"B008",
6876
"B904",
6977
"BLE001",
7078
"D100",

fastapi_forge/template/{{cookiecutter.project_name}}/src/main.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
from src.services.rabbitmq import rabbitmq_lifetime
1717
from src.constants import QUEUE_CONFIGS
1818
{% endif %}
19+
{% if cookiecutter.use_taskiq %}
20+
from src.services.taskiq import taskiq_lifetime
21+
{% endif %}
1922

2023
@asynccontextmanager
2124
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
@@ -29,6 +32,9 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
2932
{%- if cookiecutter.use_rabbitmq -%}
3033
await rabbitmq_lifetime.setup_rabbitmq(app, configs=QUEUE_CONFIGS)
3134
{% endif %}
35+
{%- if cookiecutter.use_taskiq %}
36+
await taskiq_lifetime.setup_taskiq()
37+
{% endif %}
3238

3339
yield
3440

@@ -41,6 +47,9 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
4147
{%- if cookiecutter.use_rabbitmq -%}
4248
await rabbitmq_lifetime.shutdown_rabbitmq(app)
4349
{% endif %}
50+
{%- if cookiecutter.use_taskiq %}
51+
await taskiq_lifetime.shutdown_taskiq()
52+
{% endif %}
4453

4554

4655
def get_app() -> FastAPI:

fastapi_forge/template/{{cookiecutter.project_name}}/src/routes/demo_routes.py

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@
77
from pydantic import BaseModel
88
from typing import Any
99

10+
{% if cookiecutter.use_taskiq %}
11+
from datetime import UTC, datetime, timedelta
12+
from src.services.taskiq import tasks
13+
from src.services.taskiq.scheduler import redis_source
14+
{% endif %}
15+
1016
router = APIRouter(prefix="/demo")
1117

1218
{% if cookiecutter.use_rabbitmq %}
@@ -17,10 +23,8 @@ class RabbitMQDemoMessage(BaseModel):
1723

1824
{% if cookiecutter.use_redis %}
1925
@router.post("/set-redis")
20-
async def set_redis_value(key: str, value: str, redis: GetRedis,) -> dict[str, Any]:
26+
async def set_redis_value(key: str, value: str, redis: GetRedis,) -> None:
2127
await redis.set(key, value)
22-
return {"message": "Value set successfully", "key": key, "value": value}
23-
2428

2529
@router.get("/get-redis")
2630
async def get_redis_value(key: str, redis: GetRedis,) -> dict[str, Any]:
@@ -35,11 +39,22 @@ async def get_redis_value(key: str, redis: GetRedis,) -> dict[str, Any]:
3539
async def send_rabbitmq_message(
3640
message: RabbitMQDemoMessage,
3741
rabbitmq: GetRabbitMQ,
38-
) -> dict[str, Any]:
42+
) -> None:
3943
await rabbitmq.send_demo_message(message)
40-
return {
41-
"message": "RabbitMQ message sent successfully",
42-
"key": message.key,
43-
"value": message.value,
44-
}
44+
{% endif %}
45+
46+
{% if cookiecutter.use_taskiq %}
47+
@router.post("/taskiq-kiq")
48+
async def kick_taskiq_message() -> None:
49+
await tasks.demo_task.kiq(hello="hello taskiq", world="world taskiq")
50+
51+
52+
@router.post("/taskiq-scheduled")
53+
async def schedule_taskiq_message(delay_seconds: int = 10) -> None:
54+
await tasks.demo_task.schedule_by_time(
55+
redis_source,
56+
datetime.now(UTC) + timedelta(seconds=delay_seconds),
57+
hello="hello taskiq scheduled",
58+
world="world taskiq scheduled",
59+
)
4560
{% endif %}

0 commit comments

Comments
 (0)