fastapi-fullstack 0.1.7__py3-none-any.whl → 0.1.15__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fastapi_fullstack-0.1.7.dist-info → fastapi_fullstack-0.1.15.dist-info}/METADATA +9 -2
- {fastapi_fullstack-0.1.7.dist-info → fastapi_fullstack-0.1.15.dist-info}/RECORD +71 -55
- fastapi_gen/__init__.py +6 -1
- fastapi_gen/cli.py +9 -0
- fastapi_gen/config.py +154 -2
- fastapi_gen/generator.py +34 -14
- fastapi_gen/prompts.py +172 -31
- fastapi_gen/template/VARIABLES.md +33 -4
- fastapi_gen/template/cookiecutter.json +10 -0
- fastapi_gen/template/hooks/post_gen_project.py +87 -2
- fastapi_gen/template/{{cookiecutter.project_slug}}/.env.prod.example +9 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/.gitlab-ci.yml +178 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/CLAUDE.md +3 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/README.md +334 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/.env.example +32 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/alembic/env.py +10 -1
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/admin.py +1 -1
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/__init__.py +31 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/crewai_assistant.py +563 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/deepagents_assistant.py +526 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/langchain_assistant.py +4 -3
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/langgraph_assistant.py +371 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/agent.py +1472 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/oauth.py +3 -7
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/commands/cleanup.py +2 -2
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/commands/seed.py +7 -2
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/config.py +44 -7
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/__init__.py +7 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/base.py +42 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/conversation.py +262 -1
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/item.py +76 -1
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/session.py +118 -1
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/user.py +158 -1
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/webhook.py +185 -3
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/main.py +29 -2
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/base.py +6 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/session.py +4 -4
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/conversation.py +9 -9
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/session.py +6 -6
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/webhook.py +7 -7
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/__init__.py +1 -1
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/arq_app.py +165 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/tasks/__init__.py +10 -1
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/pyproject.toml +40 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/test_metrics.py +53 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_agents.py +2 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.dev.yml +6 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.prod.yml +100 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.yml +39 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/.env.example +5 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/chat-container.tsx +28 -1
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/index.ts +1 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/message-item.tsx +22 -4
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/message-list.tsx +23 -3
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/tool-approval-dialog.tsx +138 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-chat.ts +242 -18
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-local-chat.ts +242 -17
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/constants.ts +1 -1
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/types/chat.ts +57 -1
- fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/configmap.yaml +63 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/deployment.yaml +242 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/ingress.yaml +44 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/kustomization.yaml +28 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/namespace.yaml +12 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/secret.yaml +59 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/service.yaml +23 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/nginx/nginx.conf +225 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/nginx/ssl/.gitkeep +18 -0
- {fastapi_fullstack-0.1.7.dist-info → fastapi_fullstack-0.1.15.dist-info}/WHEEL +0 -0
- {fastapi_fullstack-0.1.7.dist-info → fastapi_fullstack-0.1.15.dist-info}/entry_points.txt +0 -0
- {fastapi_fullstack-0.1.7.dist-info → fastapi_fullstack-0.1.15.dist-info}/licenses/LICENSE +0 -0
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
Contains business logic for conversation, message, and tool call operations.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from datetime import datetime
|
|
7
|
+
from datetime import UTC, datetime
|
|
8
8
|
from uuid import UUID
|
|
9
9
|
|
|
10
10
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
@@ -264,7 +264,7 @@ class ConversationService:
|
|
|
264
264
|
tool_call_id=data.tool_call_id,
|
|
265
265
|
tool_name=data.tool_name,
|
|
266
266
|
args=data.args,
|
|
267
|
-
started_at=data.started_at or datetime.
|
|
267
|
+
started_at=data.started_at or datetime.now(UTC),
|
|
268
268
|
)
|
|
269
269
|
|
|
270
270
|
async def complete_tool_call(
|
|
@@ -282,7 +282,7 @@ class ConversationService:
|
|
|
282
282
|
self.db,
|
|
283
283
|
db_tool_call=tool_call,
|
|
284
284
|
result=data.result,
|
|
285
|
-
completed_at=data.completed_at or datetime.
|
|
285
|
+
completed_at=data.completed_at or datetime.now(UTC),
|
|
286
286
|
success=data.success,
|
|
287
287
|
)
|
|
288
288
|
|
|
@@ -293,7 +293,7 @@ class ConversationService:
|
|
|
293
293
|
Contains business logic for conversation, message, and tool call operations.
|
|
294
294
|
"""
|
|
295
295
|
|
|
296
|
-
from datetime import datetime
|
|
296
|
+
from datetime import UTC, datetime
|
|
297
297
|
|
|
298
298
|
from sqlalchemy.orm import Session
|
|
299
299
|
|
|
@@ -550,7 +550,7 @@ class ConversationService:
|
|
|
550
550
|
tool_call_id=data.tool_call_id,
|
|
551
551
|
tool_name=data.tool_name,
|
|
552
552
|
args=data.args,
|
|
553
|
-
started_at=data.started_at or datetime.
|
|
553
|
+
started_at=data.started_at or datetime.now(UTC),
|
|
554
554
|
)
|
|
555
555
|
|
|
556
556
|
def complete_tool_call(
|
|
@@ -568,7 +568,7 @@ class ConversationService:
|
|
|
568
568
|
self.db,
|
|
569
569
|
db_tool_call=tool_call,
|
|
570
570
|
result=data.result,
|
|
571
|
-
completed_at=data.completed_at or datetime.
|
|
571
|
+
completed_at=data.completed_at or datetime.now(UTC),
|
|
572
572
|
success=data.success,
|
|
573
573
|
)
|
|
574
574
|
|
|
@@ -579,7 +579,7 @@ class ConversationService:
|
|
|
579
579
|
Contains business logic for conversation, message, and tool call operations.
|
|
580
580
|
"""
|
|
581
581
|
|
|
582
|
-
from datetime import datetime
|
|
582
|
+
from datetime import UTC, datetime
|
|
583
583
|
|
|
584
584
|
from app.core.exceptions import NotFoundError
|
|
585
585
|
from app.db.models.conversation import Conversation, Message, ToolCall
|
|
@@ -823,7 +823,7 @@ class ConversationService:
|
|
|
823
823
|
tool_call_id=data.tool_call_id,
|
|
824
824
|
tool_name=data.tool_name,
|
|
825
825
|
args=data.args,
|
|
826
|
-
started_at=data.started_at or datetime.
|
|
826
|
+
started_at=data.started_at or datetime.now(UTC),
|
|
827
827
|
)
|
|
828
828
|
|
|
829
829
|
async def complete_tool_call(
|
|
@@ -840,7 +840,7 @@ class ConversationService:
|
|
|
840
840
|
return await conversation_repo.complete_tool_call(
|
|
841
841
|
db_tool_call=tool_call,
|
|
842
842
|
result=data.result,
|
|
843
|
-
completed_at=data.completed_at or datetime.
|
|
843
|
+
completed_at=data.completed_at or datetime.now(UTC),
|
|
844
844
|
success=data.success,
|
|
845
845
|
)
|
|
846
846
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"""Session service (PostgreSQL async)."""
|
|
4
4
|
|
|
5
5
|
import hashlib
|
|
6
|
-
from datetime import datetime, timedelta
|
|
6
|
+
from datetime import UTC, datetime, timedelta
|
|
7
7
|
from uuid import UUID
|
|
8
8
|
|
|
9
9
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
@@ -64,7 +64,7 @@ class SessionService:
|
|
|
64
64
|
) -> Session:
|
|
65
65
|
"""Create a new session for a user."""
|
|
66
66
|
device_name, device_type = _parse_user_agent(user_agent)
|
|
67
|
-
expires_at = datetime.
|
|
67
|
+
expires_at = datetime.now(UTC) + timedelta(minutes=settings.REFRESH_TOKEN_EXPIRE_MINUTES)
|
|
68
68
|
|
|
69
69
|
return await session_repo.create(
|
|
70
70
|
self.db,
|
|
@@ -86,7 +86,7 @@ class SessionService:
|
|
|
86
86
|
token_hash = _hash_token(refresh_token)
|
|
87
87
|
session = await session_repo.get_by_refresh_token_hash(self.db, token_hash)
|
|
88
88
|
|
|
89
|
-
if session and session.expires_at > datetime.
|
|
89
|
+
if session and session.expires_at > datetime.now(UTC):
|
|
90
90
|
await session_repo.update_last_used(self.db, session.id)
|
|
91
91
|
return session
|
|
92
92
|
|
|
@@ -115,7 +115,7 @@ class SessionService:
|
|
|
115
115
|
"""Session service (SQLite sync)."""
|
|
116
116
|
|
|
117
117
|
import hashlib
|
|
118
|
-
from datetime import datetime, timedelta
|
|
118
|
+
from datetime import UTC, datetime, timedelta
|
|
119
119
|
|
|
120
120
|
from sqlalchemy.orm import Session as DBSession
|
|
121
121
|
|
|
@@ -175,7 +175,7 @@ class SessionService:
|
|
|
175
175
|
) -> Session:
|
|
176
176
|
"""Create a new session for a user."""
|
|
177
177
|
device_name, device_type = _parse_user_agent(user_agent)
|
|
178
|
-
expires_at = datetime.
|
|
178
|
+
expires_at = datetime.now(UTC) + timedelta(minutes=settings.REFRESH_TOKEN_EXPIRE_MINUTES)
|
|
179
179
|
|
|
180
180
|
return session_repo.create(
|
|
181
181
|
self.db,
|
|
@@ -197,7 +197,7 @@ class SessionService:
|
|
|
197
197
|
token_hash = _hash_token(refresh_token)
|
|
198
198
|
session = session_repo.get_by_refresh_token_hash(self.db, token_hash)
|
|
199
199
|
|
|
200
|
-
if session and session.expires_at > datetime.
|
|
200
|
+
if session and session.expires_at > datetime.now(UTC):
|
|
201
201
|
session_repo.update_last_used(self.db, session.id)
|
|
202
202
|
return session
|
|
203
203
|
|
|
@@ -6,7 +6,7 @@ import hashlib
|
|
|
6
6
|
import hmac
|
|
7
7
|
import json
|
|
8
8
|
import secrets
|
|
9
|
-
from datetime import datetime
|
|
9
|
+
from datetime import UTC, datetime
|
|
10
10
|
from uuid import UUID
|
|
11
11
|
|
|
12
12
|
import httpx
|
|
@@ -89,7 +89,7 @@ class WebhookService:
|
|
|
89
89
|
|
|
90
90
|
test_payload = {
|
|
91
91
|
"event": "webhook.test",
|
|
92
|
-
"timestamp": datetime.
|
|
92
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
|
93
93
|
"data": {"message": "This is a test webhook delivery"},
|
|
94
94
|
}
|
|
95
95
|
|
|
@@ -106,7 +106,7 @@ class WebhookService:
|
|
|
106
106
|
|
|
107
107
|
payload = {
|
|
108
108
|
"event": event_type,
|
|
109
|
-
"timestamp": datetime.
|
|
109
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
|
110
110
|
"data": data,
|
|
111
111
|
}
|
|
112
112
|
|
|
@@ -159,7 +159,7 @@ class WebhookService:
|
|
|
159
159
|
delivery.response_status = response.status_code
|
|
160
160
|
delivery.response_body = response.text[:10000] # Limit size
|
|
161
161
|
delivery.success = 200 <= response.status_code < 300
|
|
162
|
-
delivery.delivered_at = datetime.
|
|
162
|
+
delivery.delivered_at = datetime.now(UTC)
|
|
163
163
|
|
|
164
164
|
logfire.info(
|
|
165
165
|
"Webhook delivered",
|
|
@@ -228,7 +228,7 @@ import hashlib
|
|
|
228
228
|
import hmac
|
|
229
229
|
import json
|
|
230
230
|
import secrets
|
|
231
|
-
from datetime import datetime
|
|
231
|
+
from datetime import UTC, datetime
|
|
232
232
|
|
|
233
233
|
import httpx
|
|
234
234
|
import logfire
|
|
@@ -306,7 +306,7 @@ class WebhookService:
|
|
|
306
306
|
|
|
307
307
|
payload = {
|
|
308
308
|
"event": event_type,
|
|
309
|
-
"timestamp": datetime.
|
|
309
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
|
310
310
|
"data": data,
|
|
311
311
|
}
|
|
312
312
|
|
|
@@ -356,7 +356,7 @@ class WebhookService:
|
|
|
356
356
|
delivery.response_status = response.status_code
|
|
357
357
|
delivery.response_body = response.text[:10000]
|
|
358
358
|
delivery.success = 200 <= response.status_code < 300
|
|
359
|
-
delivery.delivered_at = datetime.
|
|
359
|
+
delivery.delivered_at = datetime.now(UTC)
|
|
360
360
|
|
|
361
361
|
except Exception as e:
|
|
362
362
|
delivery.error_message = str(e)
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
{%- if cookiecutter.use_arq %}
|
|
2
|
+
"""ARQ (Async Redis Queue) application configuration."""
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from arq import cron
|
|
9
|
+
from arq.connections import RedisSettings
|
|
10
|
+
|
|
11
|
+
from app.core.config import settings
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def startup(ctx: dict[str, Any]) -> None:
|
|
17
|
+
"""Initialize resources on worker startup."""
|
|
18
|
+
logger.info("ARQ worker starting up...")
|
|
19
|
+
# Add any startup initialization here
|
|
20
|
+
# e.g., database connections, external clients
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
async def shutdown(ctx: dict[str, Any]) -> None:
|
|
24
|
+
"""Cleanup resources on worker shutdown."""
|
|
25
|
+
logger.info("ARQ worker shutting down...")
|
|
26
|
+
# Add any cleanup here
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# === Example Tasks ===
|
|
30
|
+
# Tasks are defined as regular async functions
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async def example_task(ctx: dict[str, Any], message: str) -> dict[str, Any]:
|
|
34
|
+
"""
|
|
35
|
+
Example task that processes a message.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
ctx: ARQ context dictionary (contains redis connection, job info, etc.)
|
|
39
|
+
message: Message to process
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Result dictionary with processed message
|
|
43
|
+
"""
|
|
44
|
+
logger.info(f"Processing message: {message}")
|
|
45
|
+
|
|
46
|
+
# Simulate async work
|
|
47
|
+
await asyncio.sleep(1)
|
|
48
|
+
|
|
49
|
+
result = {
|
|
50
|
+
"status": "completed",
|
|
51
|
+
"message": f"Processed: {message}",
|
|
52
|
+
"job_id": ctx.get("job_id"),
|
|
53
|
+
}
|
|
54
|
+
logger.info(f"Task completed: {result}")
|
|
55
|
+
return result
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
async def long_running_task(ctx: dict[str, Any], duration: int = 10) -> dict[str, Any]:
|
|
59
|
+
"""
|
|
60
|
+
Example long-running async task.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
ctx: ARQ context dictionary
|
|
64
|
+
duration: Duration in seconds
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Result dictionary
|
|
68
|
+
"""
|
|
69
|
+
logger.info(f"Starting long-running task for {duration} seconds")
|
|
70
|
+
|
|
71
|
+
for i in range(duration):
|
|
72
|
+
await asyncio.sleep(1)
|
|
73
|
+
logger.info(f"Progress: {i + 1}/{duration}")
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
"status": "completed",
|
|
77
|
+
"duration": duration,
|
|
78
|
+
"job_id": ctx.get("job_id"),
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
async def send_email_task(
|
|
83
|
+
ctx: dict[str, Any], to: str, subject: str, body: str
|
|
84
|
+
) -> dict[str, Any]:
|
|
85
|
+
"""
|
|
86
|
+
Example email sending task.
|
|
87
|
+
|
|
88
|
+
Replace with actual email sending logic (e.g., using aiosmtplib, sendgrid, etc.)
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
ctx: ARQ context dictionary
|
|
92
|
+
to: Recipient email
|
|
93
|
+
subject: Email subject
|
|
94
|
+
body: Email body
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Result dictionary
|
|
98
|
+
"""
|
|
99
|
+
logger.info(f"Sending email to {to}: {subject}")
|
|
100
|
+
|
|
101
|
+
# TODO: Implement actual email sending
|
|
102
|
+
# Example with aiosmtplib:
|
|
103
|
+
# import aiosmtplib
|
|
104
|
+
# ...
|
|
105
|
+
|
|
106
|
+
# Simulate sending
|
|
107
|
+
await asyncio.sleep(0.5)
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
"status": "sent",
|
|
111
|
+
"to": to,
|
|
112
|
+
"subject": subject,
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# === Scheduled Task (runs periodically) ===
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
async def scheduled_example(ctx: dict[str, Any]) -> dict[str, Any]:
|
|
120
|
+
"""Example scheduled task that runs every minute."""
|
|
121
|
+
logger.info("Running scheduled example task")
|
|
122
|
+
return await example_task(ctx, "scheduled")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# === Worker Settings ===
|
|
126
|
+
# This class is used by the ARQ CLI: arq app.worker.arq_app.WorkerSettings
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class WorkerSettings:
|
|
130
|
+
"""ARQ Worker configuration."""
|
|
131
|
+
|
|
132
|
+
# Redis connection settings
|
|
133
|
+
redis_settings = RedisSettings(
|
|
134
|
+
host=settings.ARQ_REDIS_HOST,
|
|
135
|
+
port=settings.ARQ_REDIS_PORT,
|
|
136
|
+
password=settings.ARQ_REDIS_PASSWORD or None,
|
|
137
|
+
database=settings.ARQ_REDIS_DB,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Register task functions
|
|
141
|
+
functions = [
|
|
142
|
+
example_task,
|
|
143
|
+
long_running_task,
|
|
144
|
+
send_email_task,
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
# Scheduled/cron jobs
|
|
148
|
+
cron_jobs = [
|
|
149
|
+
cron(scheduled_example, minute={0, 15, 30, 45}), # Every 15 minutes
|
|
150
|
+
# cron(scheduled_example, minute=0, hour=0), # Daily at midnight
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
# Worker lifecycle hooks
|
|
154
|
+
on_startup = startup
|
|
155
|
+
on_shutdown = shutdown
|
|
156
|
+
|
|
157
|
+
# Worker settings
|
|
158
|
+
max_jobs = 10 # Maximum concurrent jobs
|
|
159
|
+
job_timeout = 300 # Job timeout in seconds (5 minutes)
|
|
160
|
+
keep_result = 3600 # Keep results for 1 hour
|
|
161
|
+
poll_delay = 0.5 # Polling delay in seconds
|
|
162
|
+
queue_read_limit = 100 # Number of jobs to read at once
|
|
163
|
+
{%- else %}
|
|
164
|
+
# ARQ not enabled for this project
|
|
165
|
+
{%- endif %}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
{%- if cookiecutter.use_celery or cookiecutter.use_taskiq %}
|
|
1
|
+
{%- if cookiecutter.use_celery or cookiecutter.use_taskiq or cookiecutter.use_arq %}
|
|
2
2
|
"""Background tasks."""
|
|
3
3
|
|
|
4
4
|
{%- if cookiecutter.use_celery %}
|
|
@@ -10,6 +10,11 @@ from app.worker.tasks.taskiq_examples import example_task as taskiq_example_task
|
|
|
10
10
|
from app.worker.tasks.taskiq_examples import long_running_task as taskiq_long_running_task
|
|
11
11
|
{%- endif %}
|
|
12
12
|
|
|
13
|
+
{%- if cookiecutter.use_arq %}
|
|
14
|
+
from app.worker.arq_app import example_task as arq_example_task
|
|
15
|
+
from app.worker.arq_app import long_running_task as arq_long_running_task
|
|
16
|
+
{%- endif %}
|
|
17
|
+
|
|
13
18
|
__all__ = [
|
|
14
19
|
{%- if cookiecutter.use_celery %}
|
|
15
20
|
"example_task",
|
|
@@ -19,6 +24,10 @@ __all__ = [
|
|
|
19
24
|
"taskiq_example_task",
|
|
20
25
|
"taskiq_long_running_task",
|
|
21
26
|
{%- endif %}
|
|
27
|
+
{%- if cookiecutter.use_arq %}
|
|
28
|
+
"arq_example_task",
|
|
29
|
+
"arq_long_running_task",
|
|
30
|
+
{%- endif %}
|
|
22
31
|
]
|
|
23
32
|
{%- else %}
|
|
24
33
|
# Background tasks not enabled
|
|
@@ -31,7 +31,11 @@ dependencies = [
|
|
|
31
31
|
"logfire[{{ logfire_extras | join(',') }}]>=2.0.0",
|
|
32
32
|
{%- endif %}
|
|
33
33
|
{%- if cookiecutter.use_postgresql %}
|
|
34
|
+
{%- if cookiecutter.use_sqlmodel %}
|
|
35
|
+
"sqlmodel>=0.0.22",
|
|
36
|
+
{%- else %}
|
|
34
37
|
"sqlalchemy[asyncio]>=2.0.0",
|
|
38
|
+
{%- endif %}
|
|
35
39
|
"asyncpg>=0.30.0",
|
|
36
40
|
"psycopg2-binary>=2.9.0",
|
|
37
41
|
"alembic>=1.14.0",
|
|
@@ -42,7 +46,11 @@ dependencies = [
|
|
|
42
46
|
"beanie>=1.27.0",
|
|
43
47
|
{%- endif %}
|
|
44
48
|
{%- if cookiecutter.use_sqlite %}
|
|
49
|
+
{%- if cookiecutter.use_sqlmodel %}
|
|
50
|
+
"sqlmodel>=0.0.22",
|
|
51
|
+
{%- else %}
|
|
45
52
|
"sqlalchemy>=2.0.0",
|
|
53
|
+
{%- endif %}
|
|
46
54
|
"alembic>=1.14.0",
|
|
47
55
|
{%- endif %}
|
|
48
56
|
{%- if cookiecutter.use_jwt %}
|
|
@@ -69,6 +77,9 @@ dependencies = [
|
|
|
69
77
|
{%- if cookiecutter.enable_sentry %}
|
|
70
78
|
"sentry-sdk[fastapi]>=2.18.0",
|
|
71
79
|
{%- endif %}
|
|
80
|
+
{%- if cookiecutter.enable_prometheus %}
|
|
81
|
+
"prometheus-fastapi-instrumentator>=7.0.0",
|
|
82
|
+
{%- endif %}
|
|
72
83
|
{%- if cookiecutter.enable_admin_panel %}
|
|
73
84
|
"sqladmin>=0.19.0",
|
|
74
85
|
{%- if cookiecutter.admin_require_auth %}
|
|
@@ -111,6 +122,35 @@ dependencies = [
|
|
|
111
122
|
{%- endif %}
|
|
112
123
|
"langgraph>=0.2.0",
|
|
113
124
|
{%- endif %}
|
|
125
|
+
{%- if cookiecutter.enable_ai_agent and cookiecutter.use_langgraph %}
|
|
126
|
+
"langchain-core>=0.3.0",
|
|
127
|
+
{%- if cookiecutter.use_openai %}
|
|
128
|
+
"langchain-openai>=0.3.0",
|
|
129
|
+
{%- endif %}
|
|
130
|
+
{%- if cookiecutter.use_anthropic %}
|
|
131
|
+
"langchain-anthropic>=0.3.0",
|
|
132
|
+
{%- endif %}
|
|
133
|
+
"langgraph>=0.2.0",
|
|
134
|
+
"langgraph-checkpoint>=2.0.0",
|
|
135
|
+
{%- endif %}
|
|
136
|
+
{%- if cookiecutter.enable_ai_agent and cookiecutter.use_crewai %}
|
|
137
|
+
"crewai>=1.0.0",
|
|
138
|
+
{%- if cookiecutter.use_openai %}
|
|
139
|
+
"langchain-openai>=0.3.0",
|
|
140
|
+
{%- endif %}
|
|
141
|
+
{%- if cookiecutter.use_anthropic %}
|
|
142
|
+
"langchain-anthropic>=0.3.0",
|
|
143
|
+
{%- endif %}
|
|
144
|
+
{%- endif %}
|
|
145
|
+
{%- if cookiecutter.enable_ai_agent and cookiecutter.use_deepagents %}
|
|
146
|
+
"deepagents>=0.1.0",
|
|
147
|
+
{%- if cookiecutter.use_openai %}
|
|
148
|
+
"langchain-openai>=0.3.0",
|
|
149
|
+
{%- endif %}
|
|
150
|
+
{%- if cookiecutter.use_anthropic %}
|
|
151
|
+
"langchain-anthropic>=0.3.0",
|
|
152
|
+
{%- endif %}
|
|
153
|
+
{%- endif %}
|
|
114
154
|
{%- if cookiecutter.logfire_httpx or cookiecutter.enable_file_storage %}
|
|
115
155
|
"httpx>=0.27.0",
|
|
116
156
|
{%- endif %}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{%- if cookiecutter.enable_prometheus %}
|
|
2
|
+
"""Prometheus metrics endpoint tests."""
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
from httpx import AsyncClient
|
|
6
|
+
|
|
7
|
+
from app.core.config import settings
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.mark.anyio
|
|
11
|
+
async def test_metrics_endpoint(client: AsyncClient):
|
|
12
|
+
"""Test that /metrics endpoint returns Prometheus metrics."""
|
|
13
|
+
response = await client.get(settings.PROMETHEUS_METRICS_PATH)
|
|
14
|
+
assert response.status_code == 200
|
|
15
|
+
assert response.headers["content-type"].startswith("text/plain")
|
|
16
|
+
|
|
17
|
+
# Check for standard prometheus-fastapi-instrumentator metrics
|
|
18
|
+
content = response.text
|
|
19
|
+
assert "http_requests_total" in content
|
|
20
|
+
assert "http_request_duration_seconds" in content
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.mark.anyio
|
|
24
|
+
async def test_metrics_increments_on_request(client: AsyncClient):
|
|
25
|
+
"""Test that metrics are incremented after making requests."""
|
|
26
|
+
# Make a health check request first
|
|
27
|
+
await client.get(f"{settings.API_V1_STR}/health")
|
|
28
|
+
|
|
29
|
+
# Then check metrics
|
|
30
|
+
response = await client.get(settings.PROMETHEUS_METRICS_PATH)
|
|
31
|
+
assert response.status_code == 200
|
|
32
|
+
|
|
33
|
+
content = response.text
|
|
34
|
+
# Should have recorded the health check request
|
|
35
|
+
assert "http_requests_total" in content
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@pytest.mark.anyio
|
|
39
|
+
async def test_metrics_excluded_from_own_metrics(client: AsyncClient):
|
|
40
|
+
"""Test that /metrics endpoint doesn't count itself in metrics."""
|
|
41
|
+
# Make multiple requests to /metrics
|
|
42
|
+
for _ in range(3):
|
|
43
|
+
await client.get(settings.PROMETHEUS_METRICS_PATH)
|
|
44
|
+
|
|
45
|
+
response = await client.get(settings.PROMETHEUS_METRICS_PATH)
|
|
46
|
+
content = response.text
|
|
47
|
+
|
|
48
|
+
# The /metrics endpoint should be excluded from instrumentation
|
|
49
|
+
# so we shouldn't see it in the metrics path labels
|
|
50
|
+
assert f'path="{settings.PROMETHEUS_METRICS_PATH}"' not in content
|
|
51
|
+
{%- else %}
|
|
52
|
+
# Prometheus metrics are not enabled for this project
|
|
53
|
+
{%- endif %}
|
|
@@ -58,6 +58,7 @@ class TestAssistantAgent:
|
|
|
58
58
|
assert agent.temperature == 0.5
|
|
59
59
|
assert agent.system_prompt == "Custom prompt"
|
|
60
60
|
|
|
61
|
+
@patch.dict("os.environ", {"OPENAI_API_KEY": "test-key"})
|
|
61
62
|
@patch("app.agents.assistant.OpenAIProvider")
|
|
62
63
|
@patch("app.agents.assistant.OpenAIChatModel")
|
|
63
64
|
def test_agent_property_creates_agent(self, mock_model, mock_provider):
|
|
@@ -67,6 +68,7 @@ class TestAssistantAgent:
|
|
|
67
68
|
assert agent._agent is not None
|
|
68
69
|
mock_model.assert_called_once()
|
|
69
70
|
|
|
71
|
+
@patch.dict("os.environ", {"OPENAI_API_KEY": "test-key"})
|
|
70
72
|
@patch("app.agents.assistant.OpenAIProvider")
|
|
71
73
|
@patch("app.agents.assistant.OpenAIChatModel")
|
|
72
74
|
def test_agent_property_caches_agent(self, mock_model, mock_provider):
|
|
@@ -50,6 +50,12 @@ services:
|
|
|
50
50
|
{%- endif %}
|
|
51
51
|
{%- endif %}
|
|
52
52
|
restart: unless-stopped
|
|
53
|
+
{%- if cookiecutter.enable_prometheus %}
|
|
54
|
+
labels:
|
|
55
|
+
- "prometheus.scrape=true"
|
|
56
|
+
- "prometheus.port={{ cookiecutter.backend_port }}"
|
|
57
|
+
- "prometheus.path=/metrics"
|
|
58
|
+
{%- endif %}
|
|
53
59
|
|
|
54
60
|
{%- if cookiecutter.use_postgresql %}
|
|
55
61
|
|
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
# with Traefik reverse proxy
|
|
5
5
|
{%- elif cookiecutter.include_traefik_labels %}
|
|
6
6
|
# with external Traefik (connect to traefik-public network)
|
|
7
|
+
{%- elif cookiecutter.include_nginx_service %}
|
|
8
|
+
# with Nginx reverse proxy
|
|
9
|
+
{%- elif cookiecutter.include_nginx_config %}
|
|
10
|
+
# with external Nginx (config template provided)
|
|
7
11
|
{%- else %}
|
|
8
12
|
# without reverse proxy (ports exposed directly)
|
|
9
13
|
{%- endif %}
|
|
@@ -24,6 +28,19 @@
|
|
|
24
28
|
# 1. Create external traefik-public network: docker network create traefik-public
|
|
25
29
|
# 2. Run your Traefik instance connected to traefik-public network
|
|
26
30
|
# 3. Configure .env.prod with DOMAIN
|
|
31
|
+
{%- elif cookiecutter.include_nginx_service %}
|
|
32
|
+
#
|
|
33
|
+
# Prerequisites:
|
|
34
|
+
# 1. Place SSL certificates in nginx/ssl/ directory:
|
|
35
|
+
# - nginx/ssl/cert.pem (certificate)
|
|
36
|
+
# - nginx/ssl/key.pem (private key)
|
|
37
|
+
# 2. Configure .env.prod with DOMAIN
|
|
38
|
+
# 3. Ensure ports 80 and 443 are available
|
|
39
|
+
{%- elif cookiecutter.include_nginx_config %}
|
|
40
|
+
#
|
|
41
|
+
# Prerequisites:
|
|
42
|
+
# 1. Configure your external Nginx using nginx/nginx.conf as reference
|
|
43
|
+
# 2. Configure .env.prod with DOMAIN
|
|
27
44
|
{%- endif %}
|
|
28
45
|
|
|
29
46
|
services:
|
|
@@ -82,6 +99,36 @@ services:
|
|
|
82
99
|
memory: 256M
|
|
83
100
|
{%- endif %}
|
|
84
101
|
|
|
102
|
+
{%- if cookiecutter.include_nginx_service %}
|
|
103
|
+
nginx:
|
|
104
|
+
image: nginx:alpine
|
|
105
|
+
container_name: {{ cookiecutter.project_slug }}_nginx
|
|
106
|
+
ports:
|
|
107
|
+
- "80:80"
|
|
108
|
+
- "443:443"
|
|
109
|
+
volumes:
|
|
110
|
+
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
|
111
|
+
- ./nginx/ssl:/etc/nginx/ssl:ro
|
|
112
|
+
networks:
|
|
113
|
+
- backend-internal
|
|
114
|
+
depends_on:
|
|
115
|
+
- app
|
|
116
|
+
{%- if cookiecutter.use_frontend %}
|
|
117
|
+
- frontend
|
|
118
|
+
{%- endif %}
|
|
119
|
+
healthcheck:
|
|
120
|
+
test: ["CMD", "nginx", "-t"]
|
|
121
|
+
interval: 30s
|
|
122
|
+
timeout: 10s
|
|
123
|
+
retries: 3
|
|
124
|
+
restart: unless-stopped
|
|
125
|
+
deploy:
|
|
126
|
+
resources:
|
|
127
|
+
limits:
|
|
128
|
+
cpus: '0.5'
|
|
129
|
+
memory: 128M
|
|
130
|
+
{%- endif %}
|
|
131
|
+
|
|
85
132
|
app:
|
|
86
133
|
build:
|
|
87
134
|
context: ./backend
|
|
@@ -120,6 +167,10 @@ services:
|
|
|
120
167
|
- "traefik.http.routers.{{ cookiecutter.project_slug }}-api.middlewares={{ cookiecutter.project_slug }}-security-headers@docker"
|
|
121
168
|
- "traefik.http.services.{{ cookiecutter.project_slug }}-api.loadbalancer.server.port={{ cookiecutter.backend_port }}"
|
|
122
169
|
- "traefik.docker.network=traefik-public"
|
|
170
|
+
{%- elif cookiecutter.include_nginx_service %}
|
|
171
|
+
networks:
|
|
172
|
+
- backend-internal
|
|
173
|
+
# No ports exposed - nginx handles external traffic
|
|
123
174
|
{%- else %}
|
|
124
175
|
networks:
|
|
125
176
|
- backend-internal
|
|
@@ -289,6 +340,10 @@ services:
|
|
|
289
340
|
- "traefik.http.routers.{{ cookiecutter.project_slug }}-flower.tls.certresolver=letsencrypt"
|
|
290
341
|
- "traefik.http.services.{{ cookiecutter.project_slug }}-flower.loadbalancer.server.port=5555"
|
|
291
342
|
- "traefik.docker.network=traefik-public"
|
|
343
|
+
{%- elif cookiecutter.include_nginx_service %}
|
|
344
|
+
networks:
|
|
345
|
+
- backend-internal
|
|
346
|
+
# No ports exposed - nginx handles external traffic
|
|
292
347
|
{%- else %}
|
|
293
348
|
networks:
|
|
294
349
|
- backend-internal
|
|
@@ -372,6 +427,45 @@ services:
|
|
|
372
427
|
memory: 128M
|
|
373
428
|
{%- endif %}
|
|
374
429
|
|
|
430
|
+
{%- if cookiecutter.use_arq %}
|
|
431
|
+
|
|
432
|
+
arq_worker:
|
|
433
|
+
build:
|
|
434
|
+
context: ./backend
|
|
435
|
+
dockerfile: Dockerfile
|
|
436
|
+
container_name: {{ cookiecutter.project_slug }}_arq_worker
|
|
437
|
+
command: arq app.worker.arq_app.WorkerSettings
|
|
438
|
+
env_file:
|
|
439
|
+
- .env.prod
|
|
440
|
+
- ./backend/.env
|
|
441
|
+
environment:
|
|
442
|
+
- DEBUG=false
|
|
443
|
+
{%- if cookiecutter.use_postgresql %}
|
|
444
|
+
- POSTGRES_HOST=db
|
|
445
|
+
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
|
446
|
+
{%- endif %}
|
|
447
|
+
- REDIS_HOST=redis
|
|
448
|
+
- REDIS_PASSWORD=${REDIS_PASSWORD}
|
|
449
|
+
- ARQ_REDIS_HOST=redis
|
|
450
|
+
- ARQ_REDIS_PORT=6379
|
|
451
|
+
- ARQ_REDIS_DB=2
|
|
452
|
+
networks:
|
|
453
|
+
- backend-internal
|
|
454
|
+
depends_on:
|
|
455
|
+
redis:
|
|
456
|
+
condition: service_healthy
|
|
457
|
+
{%- if cookiecutter.use_postgresql %}
|
|
458
|
+
db:
|
|
459
|
+
condition: service_healthy
|
|
460
|
+
{%- endif %}
|
|
461
|
+
restart: unless-stopped
|
|
462
|
+
deploy:
|
|
463
|
+
resources:
|
|
464
|
+
limits:
|
|
465
|
+
cpus: '0.5'
|
|
466
|
+
memory: 256M
|
|
467
|
+
{%- endif %}
|
|
468
|
+
|
|
375
469
|
{%- if cookiecutter.use_frontend %}
|
|
376
470
|
|
|
377
471
|
frontend:
|
|
@@ -394,7 +488,13 @@ services:
|
|
|
394
488
|
- "traefik.http.routers.{{ cookiecutter.project_slug }}-frontend.middlewares={{ cookiecutter.project_slug }}-security-headers@docker"
|
|
395
489
|
- "traefik.http.services.{{ cookiecutter.project_slug }}-frontend.loadbalancer.server.port={{ cookiecutter.frontend_port }}"
|
|
396
490
|
- "traefik.docker.network=traefik-public"
|
|
491
|
+
{%- elif cookiecutter.include_nginx_service %}
|
|
492
|
+
networks:
|
|
493
|
+
- backend-internal
|
|
494
|
+
# No ports exposed - nginx handles external traffic
|
|
397
495
|
{%- else %}
|
|
496
|
+
networks:
|
|
497
|
+
- backend-internal
|
|
398
498
|
ports:
|
|
399
499
|
- "{{ cookiecutter.frontend_port }}:{{ cookiecutter.frontend_port }}"
|
|
400
500
|
{%- endif %}
|