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.
Files changed (71) hide show
  1. {fastapi_fullstack-0.1.7.dist-info → fastapi_fullstack-0.1.15.dist-info}/METADATA +9 -2
  2. {fastapi_fullstack-0.1.7.dist-info → fastapi_fullstack-0.1.15.dist-info}/RECORD +71 -55
  3. fastapi_gen/__init__.py +6 -1
  4. fastapi_gen/cli.py +9 -0
  5. fastapi_gen/config.py +154 -2
  6. fastapi_gen/generator.py +34 -14
  7. fastapi_gen/prompts.py +172 -31
  8. fastapi_gen/template/VARIABLES.md +33 -4
  9. fastapi_gen/template/cookiecutter.json +10 -0
  10. fastapi_gen/template/hooks/post_gen_project.py +87 -2
  11. fastapi_gen/template/{{cookiecutter.project_slug}}/.env.prod.example +9 -0
  12. fastapi_gen/template/{{cookiecutter.project_slug}}/.gitlab-ci.yml +178 -0
  13. fastapi_gen/template/{{cookiecutter.project_slug}}/CLAUDE.md +3 -0
  14. fastapi_gen/template/{{cookiecutter.project_slug}}/README.md +334 -0
  15. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/.env.example +32 -0
  16. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/alembic/env.py +10 -1
  17. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/admin.py +1 -1
  18. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/__init__.py +31 -0
  19. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/crewai_assistant.py +563 -0
  20. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/deepagents_assistant.py +526 -0
  21. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/langchain_assistant.py +4 -3
  22. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/langgraph_assistant.py +371 -0
  23. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/agent.py +1472 -0
  24. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/oauth.py +3 -7
  25. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/commands/cleanup.py +2 -2
  26. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/commands/seed.py +7 -2
  27. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/config.py +44 -7
  28. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/__init__.py +7 -0
  29. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/base.py +42 -0
  30. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/conversation.py +262 -1
  31. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/item.py +76 -1
  32. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/session.py +118 -1
  33. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/user.py +158 -1
  34. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/webhook.py +185 -3
  35. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/main.py +29 -2
  36. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/base.py +6 -0
  37. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/session.py +4 -4
  38. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/conversation.py +9 -9
  39. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/session.py +6 -6
  40. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/webhook.py +7 -7
  41. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/__init__.py +1 -1
  42. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/arq_app.py +165 -0
  43. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/tasks/__init__.py +10 -1
  44. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/pyproject.toml +40 -0
  45. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/test_metrics.py +53 -0
  46. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_agents.py +2 -0
  47. fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.dev.yml +6 -0
  48. fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.prod.yml +100 -0
  49. fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.yml +39 -0
  50. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/.env.example +5 -0
  51. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/chat-container.tsx +28 -1
  52. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/index.ts +1 -0
  53. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/message-item.tsx +22 -4
  54. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/message-list.tsx +23 -3
  55. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/tool-approval-dialog.tsx +138 -0
  56. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-chat.ts +242 -18
  57. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-local-chat.ts +242 -17
  58. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/constants.ts +1 -1
  59. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/types/chat.ts +57 -1
  60. fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/configmap.yaml +63 -0
  61. fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/deployment.yaml +242 -0
  62. fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/ingress.yaml +44 -0
  63. fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/kustomization.yaml +28 -0
  64. fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/namespace.yaml +12 -0
  65. fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/secret.yaml +59 -0
  66. fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/service.yaml +23 -0
  67. fastapi_gen/template/{{cookiecutter.project_slug}}/nginx/nginx.conf +225 -0
  68. fastapi_gen/template/{{cookiecutter.project_slug}}/nginx/ssl/.gitkeep +18 -0
  69. {fastapi_fullstack-0.1.7.dist-info → fastapi_fullstack-0.1.15.dist-info}/WHEEL +0 -0
  70. {fastapi_fullstack-0.1.7.dist-info → fastapi_fullstack-0.1.15.dist-info}/entry_points.txt +0 -0
  71. {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.utcnow())
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.utcnow())
187
+ .values(last_used_at=datetime.now(UTC))
188
188
  )
189
189
  db.flush()
190
190