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
|
@@ -1,5 +1,65 @@
|
|
|
1
1
|
{%- if cookiecutter.enable_session_management and cookiecutter.use_jwt %}
|
|
2
|
-
{%- if cookiecutter.use_postgresql %}
|
|
2
|
+
{%- if cookiecutter.use_postgresql and cookiecutter.use_sqlmodel %}
|
|
3
|
+
"""Session database model for tracking user sessions using SQLModel."""
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
from sqlalchemy import Column, DateTime, ForeignKey, String, Text
|
|
9
|
+
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
|
|
10
|
+
from sqlmodel import Field, Relationship, SQLModel
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Session(SQLModel, table=True):
|
|
14
|
+
"""User session model for tracking active login sessions."""
|
|
15
|
+
|
|
16
|
+
__tablename__ = "sessions"
|
|
17
|
+
|
|
18
|
+
id: uuid.UUID = Field(
|
|
19
|
+
default_factory=uuid.uuid4,
|
|
20
|
+
sa_column=Column(PG_UUID(as_uuid=True), primary_key=True),
|
|
21
|
+
)
|
|
22
|
+
user_id: uuid.UUID = Field(
|
|
23
|
+
sa_column=Column(
|
|
24
|
+
PG_UUID(as_uuid=True),
|
|
25
|
+
ForeignKey("users.id", ondelete="CASCADE"),
|
|
26
|
+
nullable=False,
|
|
27
|
+
),
|
|
28
|
+
)
|
|
29
|
+
refresh_token_hash: str = Field(
|
|
30
|
+
sa_column=Column(String(255), nullable=False, index=True),
|
|
31
|
+
)
|
|
32
|
+
device_name: str | None = Field(default=None, max_length=255)
|
|
33
|
+
device_type: str | None = Field(default=None, max_length=50)
|
|
34
|
+
ip_address: str | None = Field(default=None, max_length=45)
|
|
35
|
+
user_agent: str | None = Field(default=None, sa_column=Column(Text, nullable=True))
|
|
36
|
+
is_active: bool = Field(default=True)
|
|
37
|
+
created_at: datetime = Field(
|
|
38
|
+
default_factory=datetime.utcnow,
|
|
39
|
+
sa_column=Column(DateTime(timezone=True), nullable=False),
|
|
40
|
+
)
|
|
41
|
+
last_used_at: datetime = Field(
|
|
42
|
+
default_factory=datetime.utcnow,
|
|
43
|
+
sa_column=Column(DateTime(timezone=True), nullable=False),
|
|
44
|
+
)
|
|
45
|
+
expires_at: datetime = Field(
|
|
46
|
+
sa_column=Column(DateTime(timezone=True), nullable=False),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Relationship
|
|
50
|
+
user: "User" = Relationship(back_populates="sessions")
|
|
51
|
+
|
|
52
|
+
def __repr__(self) -> str:
|
|
53
|
+
return f"<Session(id={self.id}, user_id={self.user_id}, device={self.device_name})>"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# Forward reference for type hints
|
|
57
|
+
from typing import TYPE_CHECKING
|
|
58
|
+
if TYPE_CHECKING:
|
|
59
|
+
from app.db.models.user import User
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
{%- elif cookiecutter.use_postgresql %}
|
|
3
63
|
"""Session database model for tracking user sessions."""
|
|
4
64
|
|
|
5
65
|
import uuid
|
|
@@ -44,6 +104,63 @@ class Session(Base):
|
|
|
44
104
|
return f"<Session(id={self.id}, user_id={self.user_id}, device={self.device_name})>"
|
|
45
105
|
|
|
46
106
|
|
|
107
|
+
{%- elif cookiecutter.use_sqlite and cookiecutter.use_sqlmodel %}
|
|
108
|
+
"""Session database model for tracking user sessions using SQLModel."""
|
|
109
|
+
|
|
110
|
+
import uuid
|
|
111
|
+
from datetime import datetime
|
|
112
|
+
|
|
113
|
+
from sqlalchemy import Column, DateTime, ForeignKey, String, Text
|
|
114
|
+
from sqlmodel import Field, Relationship, SQLModel
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class Session(SQLModel, table=True):
|
|
118
|
+
"""User session model for tracking active login sessions."""
|
|
119
|
+
|
|
120
|
+
__tablename__ = "sessions"
|
|
121
|
+
|
|
122
|
+
id: str = Field(
|
|
123
|
+
default_factory=lambda: str(uuid.uuid4()),
|
|
124
|
+
sa_column=Column(String(36), primary_key=True),
|
|
125
|
+
)
|
|
126
|
+
user_id: str = Field(
|
|
127
|
+
sa_column=Column(
|
|
128
|
+
String(36),
|
|
129
|
+
ForeignKey("users.id", ondelete="CASCADE"),
|
|
130
|
+
nullable=False,
|
|
131
|
+
),
|
|
132
|
+
)
|
|
133
|
+
refresh_token_hash: str = Field(
|
|
134
|
+
sa_column=Column(String(255), nullable=False, index=True),
|
|
135
|
+
)
|
|
136
|
+
device_name: str | None = Field(default=None, max_length=255)
|
|
137
|
+
device_type: str | None = Field(default=None, max_length=50)
|
|
138
|
+
ip_address: str | None = Field(default=None, max_length=45)
|
|
139
|
+
user_agent: str | None = Field(default=None, sa_column=Column(Text, nullable=True))
|
|
140
|
+
is_active: bool = Field(default=True)
|
|
141
|
+
created_at: datetime = Field(
|
|
142
|
+
default_factory=datetime.utcnow,
|
|
143
|
+
sa_column=Column(DateTime, nullable=False),
|
|
144
|
+
)
|
|
145
|
+
last_used_at: datetime = Field(
|
|
146
|
+
default_factory=datetime.utcnow,
|
|
147
|
+
sa_column=Column(DateTime, nullable=False),
|
|
148
|
+
)
|
|
149
|
+
expires_at: datetime = Field(sa_column=Column(DateTime, nullable=False))
|
|
150
|
+
|
|
151
|
+
# Relationship
|
|
152
|
+
user: "User" = Relationship(back_populates="sessions")
|
|
153
|
+
|
|
154
|
+
def __repr__(self) -> str:
|
|
155
|
+
return f"<Session(id={self.id}, user_id={self.user_id}, device={self.device_name})>"
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# Forward reference for type hints
|
|
159
|
+
from typing import TYPE_CHECKING
|
|
160
|
+
if TYPE_CHECKING:
|
|
161
|
+
from app.db.models.user import User
|
|
162
|
+
|
|
163
|
+
|
|
47
164
|
{%- elif cookiecutter.use_sqlite %}
|
|
48
165
|
"""Session database model for tracking user sessions."""
|
|
49
166
|
|
|
@@ -1,4 +1,83 @@
|
|
|
1
|
-
{%- if cookiecutter.use_jwt and cookiecutter.use_postgresql %}
|
|
1
|
+
{%- if cookiecutter.use_jwt and cookiecutter.use_postgresql and cookiecutter.use_sqlmodel %}
|
|
2
|
+
"""User database model using SQLModel."""
|
|
3
|
+
|
|
4
|
+
import uuid
|
|
5
|
+
from enum import Enum
|
|
6
|
+
{%- if cookiecutter.enable_session_management %}
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
{%- endif %}
|
|
9
|
+
|
|
10
|
+
from sqlalchemy import Column, String
|
|
11
|
+
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
|
|
12
|
+
from sqlmodel import Field, Relationship, SQLModel
|
|
13
|
+
|
|
14
|
+
from app.db.base import TimestampMixin
|
|
15
|
+
|
|
16
|
+
{%- if cookiecutter.enable_session_management %}
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from app.db.models.session import Session
|
|
19
|
+
{%- endif %}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class UserRole(str, Enum):
|
|
23
|
+
"""User role enumeration.
|
|
24
|
+
|
|
25
|
+
Roles hierarchy (higher includes lower permissions):
|
|
26
|
+
- ADMIN: Full system access, can manage users and settings
|
|
27
|
+
- USER: Standard user access
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
ADMIN = "admin"
|
|
31
|
+
USER = "user"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class User(TimestampMixin, SQLModel, table=True):
|
|
35
|
+
"""User model."""
|
|
36
|
+
|
|
37
|
+
__tablename__ = "users"
|
|
38
|
+
|
|
39
|
+
id: uuid.UUID = Field(
|
|
40
|
+
default_factory=uuid.uuid4,
|
|
41
|
+
sa_column=Column(PG_UUID(as_uuid=True), primary_key=True),
|
|
42
|
+
)
|
|
43
|
+
email: str = Field(
|
|
44
|
+
sa_column=Column(String(255), unique=True, index=True, nullable=False),
|
|
45
|
+
)
|
|
46
|
+
hashed_password: str | None = Field(default=None, max_length=255)
|
|
47
|
+
full_name: str | None = Field(default=None, max_length=255)
|
|
48
|
+
is_active: bool = Field(default=True)
|
|
49
|
+
is_superuser: bool = Field(default=False)
|
|
50
|
+
role: str = Field(default=UserRole.USER.value, max_length=50)
|
|
51
|
+
{%- if cookiecutter.enable_oauth %}
|
|
52
|
+
oauth_provider: str | None = Field(default=None, max_length=50)
|
|
53
|
+
oauth_id: str | None = Field(default=None, max_length=255)
|
|
54
|
+
{%- endif %}
|
|
55
|
+
|
|
56
|
+
{%- if cookiecutter.enable_session_management %}
|
|
57
|
+
|
|
58
|
+
# Relationship to sessions
|
|
59
|
+
sessions: list["Session"] = Relationship(back_populates="user")
|
|
60
|
+
{%- endif %}
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def user_role(self) -> UserRole:
|
|
64
|
+
"""Get role as enum."""
|
|
65
|
+
return UserRole(self.role)
|
|
66
|
+
|
|
67
|
+
def has_role(self, required_role: UserRole) -> bool:
|
|
68
|
+
"""Check if user has the required role or higher.
|
|
69
|
+
|
|
70
|
+
Admin role has access to everything.
|
|
71
|
+
"""
|
|
72
|
+
if self.role == UserRole.ADMIN.value:
|
|
73
|
+
return True
|
|
74
|
+
return self.role == required_role.value
|
|
75
|
+
|
|
76
|
+
def __repr__(self) -> str:
|
|
77
|
+
return f"<User(id={self.id}, email={self.email}, role={self.role})>"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
{%- elif cookiecutter.use_jwt and cookiecutter.use_postgresql %}
|
|
2
81
|
"""User database model."""
|
|
3
82
|
|
|
4
83
|
import uuid
|
|
@@ -76,6 +155,84 @@ class User(Base, TimestampMixin):
|
|
|
76
155
|
return f"<User(id={self.id}, email={self.email}, role={self.role})>"
|
|
77
156
|
|
|
78
157
|
|
|
158
|
+
{%- elif cookiecutter.use_jwt and cookiecutter.use_sqlite and cookiecutter.use_sqlmodel %}
|
|
159
|
+
"""User database model using SQLModel."""
|
|
160
|
+
|
|
161
|
+
import uuid
|
|
162
|
+
from enum import Enum
|
|
163
|
+
{%- if cookiecutter.enable_session_management %}
|
|
164
|
+
from typing import TYPE_CHECKING
|
|
165
|
+
{%- endif %}
|
|
166
|
+
|
|
167
|
+
from sqlalchemy import Column, String
|
|
168
|
+
from sqlmodel import Field, Relationship, SQLModel
|
|
169
|
+
|
|
170
|
+
from app.db.base import TimestampMixin
|
|
171
|
+
|
|
172
|
+
{%- if cookiecutter.enable_session_management %}
|
|
173
|
+
if TYPE_CHECKING:
|
|
174
|
+
from app.db.models.session import Session
|
|
175
|
+
{%- endif %}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class UserRole(str, Enum):
|
|
179
|
+
"""User role enumeration.
|
|
180
|
+
|
|
181
|
+
Roles hierarchy (higher includes lower permissions):
|
|
182
|
+
- ADMIN: Full system access, can manage users and settings
|
|
183
|
+
- USER: Standard user access
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
ADMIN = "admin"
|
|
187
|
+
USER = "user"
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class User(TimestampMixin, SQLModel, table=True):
|
|
191
|
+
"""User model."""
|
|
192
|
+
|
|
193
|
+
__tablename__ = "users"
|
|
194
|
+
|
|
195
|
+
id: str = Field(
|
|
196
|
+
default_factory=lambda: str(uuid.uuid4()),
|
|
197
|
+
sa_column=Column(String(36), primary_key=True),
|
|
198
|
+
)
|
|
199
|
+
email: str = Field(
|
|
200
|
+
sa_column=Column(String(255), unique=True, index=True, nullable=False),
|
|
201
|
+
)
|
|
202
|
+
hashed_password: str | None = Field(default=None, max_length=255)
|
|
203
|
+
full_name: str | None = Field(default=None, max_length=255)
|
|
204
|
+
is_active: bool = Field(default=True)
|
|
205
|
+
is_superuser: bool = Field(default=False)
|
|
206
|
+
role: str = Field(default=UserRole.USER.value, max_length=50)
|
|
207
|
+
{%- if cookiecutter.enable_oauth %}
|
|
208
|
+
oauth_provider: str | None = Field(default=None, max_length=50)
|
|
209
|
+
oauth_id: str | None = Field(default=None, max_length=255)
|
|
210
|
+
{%- endif %}
|
|
211
|
+
|
|
212
|
+
{%- if cookiecutter.enable_session_management %}
|
|
213
|
+
|
|
214
|
+
# Relationship to sessions
|
|
215
|
+
sessions: list["Session"] = Relationship(back_populates="user")
|
|
216
|
+
{%- endif %}
|
|
217
|
+
|
|
218
|
+
@property
|
|
219
|
+
def user_role(self) -> UserRole:
|
|
220
|
+
"""Get role as enum."""
|
|
221
|
+
return UserRole(self.role)
|
|
222
|
+
|
|
223
|
+
def has_role(self, required_role: UserRole) -> bool:
|
|
224
|
+
"""Check if user has the required role or higher.
|
|
225
|
+
|
|
226
|
+
Admin role has access to everything.
|
|
227
|
+
"""
|
|
228
|
+
if self.role == UserRole.ADMIN.value:
|
|
229
|
+
return True
|
|
230
|
+
return self.role == required_role.value
|
|
231
|
+
|
|
232
|
+
def __repr__(self) -> str:
|
|
233
|
+
return f"<User(id={self.id}, email={self.email}, role={self.role})>"
|
|
234
|
+
|
|
235
|
+
|
|
79
236
|
{%- elif cookiecutter.use_jwt and cookiecutter.use_sqlite %}
|
|
80
237
|
"""User database model."""
|
|
81
238
|
|
|
@@ -1,5 +1,92 @@
|
|
|
1
1
|
{%- if cookiecutter.enable_webhooks and cookiecutter.use_database %}
|
|
2
|
-
{%- if cookiecutter.use_postgresql %}
|
|
2
|
+
{%- if cookiecutter.use_postgresql and cookiecutter.use_sqlmodel %}
|
|
3
|
+
"""Webhook database models using SQLModel (PostgreSQL async)."""
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from enum import Enum
|
|
8
|
+
|
|
9
|
+
from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, Text
|
|
10
|
+
from sqlalchemy.dialects.postgresql import ARRAY, UUID as PG_UUID
|
|
11
|
+
from sqlmodel import Field, Relationship, SQLModel
|
|
12
|
+
|
|
13
|
+
from app.db.base import TimestampMixin
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class WebhookEventType(str, Enum):
|
|
17
|
+
"""Webhook event types."""
|
|
18
|
+
|
|
19
|
+
# User events
|
|
20
|
+
USER_CREATED = "user.created"
|
|
21
|
+
USER_UPDATED = "user.updated"
|
|
22
|
+
USER_DELETED = "user.deleted"
|
|
23
|
+
|
|
24
|
+
# Custom events (extend as needed)
|
|
25
|
+
ITEM_CREATED = "item.created"
|
|
26
|
+
ITEM_UPDATED = "item.updated"
|
|
27
|
+
ITEM_DELETED = "item.deleted"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Webhook(TimestampMixin, SQLModel, table=True):
|
|
31
|
+
"""Webhook subscription model."""
|
|
32
|
+
|
|
33
|
+
__tablename__ = "webhooks"
|
|
34
|
+
|
|
35
|
+
id: uuid.UUID = Field(
|
|
36
|
+
default_factory=uuid.uuid4,
|
|
37
|
+
sa_column=Column(PG_UUID(as_uuid=True), primary_key=True),
|
|
38
|
+
)
|
|
39
|
+
name: str = Field(max_length=255)
|
|
40
|
+
url: str = Field(max_length=2048)
|
|
41
|
+
secret: str = Field(max_length=255)
|
|
42
|
+
events: list[str] = Field(sa_column=Column(ARRAY(String), nullable=False))
|
|
43
|
+
is_active: bool = Field(default=True)
|
|
44
|
+
description: str | None = Field(default=None, sa_column=Column(Text, nullable=True))
|
|
45
|
+
|
|
46
|
+
{%- if cookiecutter.use_jwt %}
|
|
47
|
+
user_id: uuid.UUID | None = Field(
|
|
48
|
+
default=None,
|
|
49
|
+
sa_column=Column(PG_UUID(as_uuid=True), ForeignKey("users.id"), nullable=True),
|
|
50
|
+
)
|
|
51
|
+
{%- endif %}
|
|
52
|
+
|
|
53
|
+
# Relationship to delivery logs
|
|
54
|
+
deliveries: list["WebhookDelivery"] = Relationship(
|
|
55
|
+
back_populates="webhook",
|
|
56
|
+
sa_relationship_kwargs={"cascade": "all, delete-orphan"},
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class WebhookDelivery(SQLModel, table=True):
|
|
61
|
+
"""Webhook delivery log model."""
|
|
62
|
+
|
|
63
|
+
__tablename__ = "webhook_deliveries"
|
|
64
|
+
|
|
65
|
+
id: uuid.UUID = Field(
|
|
66
|
+
default_factory=uuid.uuid4,
|
|
67
|
+
sa_column=Column(PG_UUID(as_uuid=True), primary_key=True),
|
|
68
|
+
)
|
|
69
|
+
webhook_id: uuid.UUID = Field(
|
|
70
|
+
sa_column=Column(PG_UUID(as_uuid=True), ForeignKey("webhooks.id"), nullable=False),
|
|
71
|
+
)
|
|
72
|
+
event_type: str = Field(max_length=100)
|
|
73
|
+
payload: str = Field(sa_column=Column(Text, nullable=False))
|
|
74
|
+
response_status: int | None = Field(default=None)
|
|
75
|
+
response_body: str | None = Field(default=None, sa_column=Column(Text, nullable=True))
|
|
76
|
+
error_message: str | None = Field(default=None, sa_column=Column(Text, nullable=True))
|
|
77
|
+
attempt_count: int = Field(default=1)
|
|
78
|
+
success: bool = Field(default=False)
|
|
79
|
+
created_at: datetime = Field(sa_column=Column(DateTime, nullable=False))
|
|
80
|
+
delivered_at: datetime | None = Field(
|
|
81
|
+
default=None,
|
|
82
|
+
sa_column=Column(DateTime, nullable=True),
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Relationship
|
|
86
|
+
webhook: "Webhook" = Relationship(back_populates="deliveries")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
{%- elif cookiecutter.use_postgresql %}
|
|
3
90
|
"""Webhook database models (PostgreSQL async)."""
|
|
4
91
|
|
|
5
92
|
import uuid
|
|
@@ -82,9 +169,106 @@ class WebhookDelivery(Base):
|
|
|
82
169
|
webhook: Mapped["Webhook"] = relationship("Webhook", back_populates="deliveries")
|
|
83
170
|
|
|
84
171
|
|
|
172
|
+
{%- elif cookiecutter.use_sqlite and cookiecutter.use_sqlmodel %}
|
|
173
|
+
"""Webhook database models using SQLModel (SQLite sync)."""
|
|
174
|
+
|
|
175
|
+
import json
|
|
176
|
+
import uuid
|
|
177
|
+
from datetime import datetime
|
|
178
|
+
from enum import Enum
|
|
179
|
+
|
|
180
|
+
from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, Text
|
|
181
|
+
from sqlmodel import Field, Relationship, SQLModel
|
|
182
|
+
|
|
183
|
+
from app.db.base import TimestampMixin
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class WebhookEventType(str, Enum):
|
|
187
|
+
"""Webhook event types."""
|
|
188
|
+
|
|
189
|
+
# User events
|
|
190
|
+
USER_CREATED = "user.created"
|
|
191
|
+
USER_UPDATED = "user.updated"
|
|
192
|
+
USER_DELETED = "user.deleted"
|
|
193
|
+
|
|
194
|
+
# Custom events (extend as needed)
|
|
195
|
+
ITEM_CREATED = "item.created"
|
|
196
|
+
ITEM_UPDATED = "item.updated"
|
|
197
|
+
ITEM_DELETED = "item.deleted"
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class Webhook(TimestampMixin, SQLModel, table=True):
|
|
201
|
+
"""Webhook subscription model."""
|
|
202
|
+
|
|
203
|
+
__tablename__ = "webhooks"
|
|
204
|
+
|
|
205
|
+
id: str = Field(
|
|
206
|
+
default_factory=lambda: str(uuid.uuid4()),
|
|
207
|
+
sa_column=Column(String(36), primary_key=True),
|
|
208
|
+
)
|
|
209
|
+
name: str = Field(max_length=255)
|
|
210
|
+
url: str = Field(max_length=2048)
|
|
211
|
+
secret: str = Field(max_length=255)
|
|
212
|
+
# Store events as JSON string for SQLite
|
|
213
|
+
events_json: str = Field(sa_column=Column(Text, nullable=False))
|
|
214
|
+
is_active: bool = Field(default=True)
|
|
215
|
+
description: str | None = Field(default=None, sa_column=Column(Text, nullable=True))
|
|
216
|
+
|
|
217
|
+
{%- if cookiecutter.use_jwt %}
|
|
218
|
+
user_id: str | None = Field(
|
|
219
|
+
default=None,
|
|
220
|
+
sa_column=Column(String(36), ForeignKey("users.id"), nullable=True),
|
|
221
|
+
)
|
|
222
|
+
{%- endif %}
|
|
223
|
+
|
|
224
|
+
deliveries: list["WebhookDelivery"] = Relationship(
|
|
225
|
+
back_populates="webhook",
|
|
226
|
+
sa_relationship_kwargs={"cascade": "all, delete-orphan"},
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
@property
|
|
230
|
+
def events(self) -> list[str]:
|
|
231
|
+
"""Parse events from JSON string."""
|
|
232
|
+
return json.loads(self.events_json) if self.events_json else []
|
|
233
|
+
|
|
234
|
+
@events.setter
|
|
235
|
+
def events(self, value: list[str]) -> None:
|
|
236
|
+
"""Store events as JSON string."""
|
|
237
|
+
self.events_json = json.dumps(value)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class WebhookDelivery(SQLModel, table=True):
|
|
241
|
+
"""Webhook delivery log model."""
|
|
242
|
+
|
|
243
|
+
__tablename__ = "webhook_deliveries"
|
|
244
|
+
|
|
245
|
+
id: str = Field(
|
|
246
|
+
default_factory=lambda: str(uuid.uuid4()),
|
|
247
|
+
sa_column=Column(String(36), primary_key=True),
|
|
248
|
+
)
|
|
249
|
+
webhook_id: str = Field(
|
|
250
|
+
sa_column=Column(String(36), ForeignKey("webhooks.id"), nullable=False),
|
|
251
|
+
)
|
|
252
|
+
event_type: str = Field(max_length=100)
|
|
253
|
+
payload: str = Field(sa_column=Column(Text, nullable=False))
|
|
254
|
+
response_status: int | None = Field(default=None)
|
|
255
|
+
response_body: str | None = Field(default=None, sa_column=Column(Text, nullable=True))
|
|
256
|
+
error_message: str | None = Field(default=None, sa_column=Column(Text, nullable=True))
|
|
257
|
+
attempt_count: int = Field(default=1)
|
|
258
|
+
success: bool = Field(default=False)
|
|
259
|
+
created_at: datetime = Field(sa_column=Column(DateTime, nullable=False))
|
|
260
|
+
delivered_at: datetime | None = Field(
|
|
261
|
+
default=None,
|
|
262
|
+
sa_column=Column(DateTime, nullable=True),
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
webhook: "Webhook" = Relationship(back_populates="deliveries")
|
|
266
|
+
|
|
267
|
+
|
|
85
268
|
{%- elif cookiecutter.use_sqlite %}
|
|
86
269
|
"""Webhook database models (SQLite sync)."""
|
|
87
270
|
|
|
271
|
+
import json
|
|
88
272
|
import uuid
|
|
89
273
|
from datetime import datetime
|
|
90
274
|
from enum import Enum
|
|
@@ -138,13 +322,11 @@ class Webhook(Base, TimestampMixin):
|
|
|
138
322
|
@property
|
|
139
323
|
def events(self) -> list[str]:
|
|
140
324
|
"""Parse events from JSON string."""
|
|
141
|
-
import json
|
|
142
325
|
return json.loads(self.events_json) if self.events_json else []
|
|
143
326
|
|
|
144
327
|
@events.setter
|
|
145
328
|
def events(self, value: list[str]) -> None:
|
|
146
329
|
"""Store events as JSON string."""
|
|
147
|
-
import json
|
|
148
330
|
self.events_json = json.dumps(value)
|
|
149
331
|
|
|
150
332
|
|
|
@@ -17,7 +17,9 @@ from fastapi_pagination import add_pagination
|
|
|
17
17
|
from app.api.exception_handlers import register_exception_handlers
|
|
18
18
|
from app.api.router import api_router
|
|
19
19
|
from app.core.config import settings
|
|
20
|
+
{%- if cookiecutter.enable_logfire %}
|
|
20
21
|
from app.core.logfire_setup import instrument_app, setup_logfire
|
|
22
|
+
{%- endif %}
|
|
21
23
|
from app.core.middleware import RequestIDMiddleware
|
|
22
24
|
|
|
23
25
|
{%- if cookiecutter.enable_redis %}
|
|
@@ -39,7 +41,9 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[{% if cookiecutter.enable_red
|
|
|
39
41
|
See: https://asgi.readthedocs.io/en/latest/specs/lifespan.html#lifespan-state
|
|
40
42
|
"""
|
|
41
43
|
# === Startup ===
|
|
44
|
+
{%- if cookiecutter.enable_logfire %}
|
|
42
45
|
setup_logfire()
|
|
46
|
+
{%- endif %}
|
|
43
47
|
|
|
44
48
|
{%- if cookiecutter.use_postgresql and cookiecutter.enable_logfire and cookiecutter.logfire_database %}
|
|
45
49
|
from app.core.logfire_setup import instrument_asyncpg
|
|
@@ -71,7 +75,7 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[{% if cookiecutter.enable_red
|
|
|
71
75
|
await redis_client.connect()
|
|
72
76
|
{%- endif %}
|
|
73
77
|
|
|
74
|
-
{%- if cookiecutter.enable_caching %}
|
|
78
|
+
{%- if cookiecutter.enable_caching and cookiecutter.enable_redis %}
|
|
75
79
|
from app.core.cache import setup_cache
|
|
76
80
|
setup_cache(redis_client)
|
|
77
81
|
{%- endif %}
|
|
@@ -178,7 +182,7 @@ def create_app() -> FastAPI:
|
|
|
178
182
|
|
|
179
183
|
app = FastAPI(
|
|
180
184
|
title=settings.PROJECT_NAME,
|
|
181
|
-
summary="FastAPI application with Logfire observability",
|
|
185
|
+
summary="FastAPI application{% if cookiecutter.enable_logfire %} with Logfire observability{% endif %}",
|
|
182
186
|
description="""
|
|
183
187
|
{{ cookiecutter.project_description }}
|
|
184
188
|
|
|
@@ -233,8 +237,10 @@ def create_app() -> FastAPI:
|
|
|
233
237
|
{%- endif %}
|
|
234
238
|
)
|
|
235
239
|
|
|
240
|
+
{%- if cookiecutter.enable_logfire %}
|
|
236
241
|
# Logfire instrumentation
|
|
237
242
|
instrument_app(app)
|
|
243
|
+
{%- endif %}
|
|
238
244
|
|
|
239
245
|
# Request ID middleware (for request correlation/debugging)
|
|
240
246
|
app.add_middleware(RequestIDMiddleware)
|
|
@@ -263,6 +269,27 @@ def create_app() -> FastAPI:
|
|
|
263
269
|
sentry_sdk.init(dsn=settings.SENTRY_DSN, enable_tracing=True)
|
|
264
270
|
{%- endif %}
|
|
265
271
|
|
|
272
|
+
{%- if cookiecutter.enable_prometheus %}
|
|
273
|
+
|
|
274
|
+
# Prometheus metrics
|
|
275
|
+
from prometheus_fastapi_instrumentator import Instrumentator
|
|
276
|
+
|
|
277
|
+
instrumentator = Instrumentator(
|
|
278
|
+
should_group_status_codes=True,
|
|
279
|
+
should_ignore_untemplated=True,
|
|
280
|
+
should_respect_env_var=True,
|
|
281
|
+
should_instrument_requests_inprogress=True,
|
|
282
|
+
excluded_handlers=["/health", "/health/ready", "/health/live", settings.PROMETHEUS_METRICS_PATH],
|
|
283
|
+
inprogress_name="http_requests_inprogress",
|
|
284
|
+
inprogress_labels=True,
|
|
285
|
+
)
|
|
286
|
+
instrumentator.instrument(app).expose(
|
|
287
|
+
app,
|
|
288
|
+
endpoint=settings.PROMETHEUS_METRICS_PATH,
|
|
289
|
+
include_in_schema=settings.PROMETHEUS_INCLUDE_IN_SCHEMA,
|
|
290
|
+
)
|
|
291
|
+
{%- endif %}
|
|
292
|
+
|
|
266
293
|
{%- if cookiecutter.enable_rate_limiting %}
|
|
267
294
|
|
|
268
295
|
# Rate limiting
|
|
@@ -11,9 +11,15 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
11
11
|
from sqlalchemy.orm import Session
|
|
12
12
|
{%- endif %}
|
|
13
13
|
|
|
14
|
+
{%- if cookiecutter.use_sqlmodel %}
|
|
15
|
+
from sqlmodel import SQLModel
|
|
16
|
+
|
|
17
|
+
ModelType = TypeVar("ModelType", bound=SQLModel)
|
|
18
|
+
{%- else %}
|
|
14
19
|
from app.db.base import Base
|
|
15
20
|
|
|
16
21
|
ModelType = TypeVar("ModelType", bound=Base)
|
|
22
|
+
{%- endif %}
|
|
17
23
|
CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel)
|
|
18
24
|
UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel)
|
|
19
25
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
{%- if cookiecutter.use_postgresql %}
|
|
3
3
|
"""Session repository (PostgreSQL async)."""
|
|
4
4
|
|
|
5
|
-
from datetime import datetime
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
6
|
from uuid import UUID
|
|
7
7
|
|
|
8
8
|
from sqlalchemy import select, update
|
|
@@ -74,7 +74,7 @@ async def update_last_used(db: AsyncSession, session_id: UUID) -> None:
|
|
|
74
74
|
await db.execute(
|
|
75
75
|
update(Session)
|
|
76
76
|
.where(Session.id == session_id)
|
|
77
|
-
.values(last_used_at=datetime.
|
|
77
|
+
.values(last_used_at=datetime.now(UTC))
|
|
78
78
|
)
|
|
79
79
|
await db.flush()
|
|
80
80
|
|
|
@@ -113,7 +113,7 @@ async def deactivate_by_refresh_token_hash(db: AsyncSession, token_hash: str) ->
|
|
|
113
113
|
{%- elif cookiecutter.use_sqlite %}
|
|
114
114
|
"""Session repository (SQLite sync)."""
|
|
115
115
|
|
|
116
|
-
from datetime import datetime
|
|
116
|
+
from datetime import UTC, datetime
|
|
117
117
|
|
|
118
118
|
from sqlalchemy import select, update
|
|
119
119
|
from sqlalchemy.orm import Session as DBSession
|
|
@@ -184,7 +184,7 @@ def update_last_used(db: DBSession, session_id: str) -> None:
|
|
|
184
184
|
db.execute(
|
|
185
185
|
update(Session)
|
|
186
186
|
.where(Session.id == session_id)
|
|
187
|
-
.values(last_used_at=datetime.
|
|
187
|
+
.values(last_used_at=datetime.now(UTC))
|
|
188
188
|
)
|
|
189
189
|
db.flush()
|
|
190
190
|
|