@malamute/ai-rules 1.0.0 → 1.3.0
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.
- package/README.md +272 -121
- package/bin/cli.js +5 -2
- package/configs/_shared/CLAUDE.md +52 -149
- package/configs/_shared/rules/conventions/documentation.md +324 -0
- package/configs/_shared/rules/conventions/git.md +265 -0
- package/configs/_shared/rules/conventions/npm.md +80 -0
- package/configs/_shared/{.claude/rules → rules/conventions}/performance.md +1 -1
- package/configs/_shared/rules/conventions/principles.md +334 -0
- package/configs/_shared/rules/devops/ci-cd.md +262 -0
- package/configs/_shared/rules/devops/docker.md +275 -0
- package/configs/_shared/rules/devops/nx.md +194 -0
- package/configs/_shared/rules/domain/backend/api-design.md +203 -0
- package/configs/_shared/rules/lang/csharp/async.md +220 -0
- package/configs/_shared/rules/lang/csharp/csharp.md +314 -0
- package/configs/_shared/rules/lang/csharp/linq.md +210 -0
- package/configs/_shared/rules/lang/python/async.md +337 -0
- package/configs/_shared/rules/lang/python/celery.md +476 -0
- package/configs/_shared/rules/lang/python/config.md +339 -0
- package/configs/{python/.claude/rules → _shared/rules/lang/python}/database/sqlalchemy.md +6 -1
- package/configs/_shared/rules/lang/python/deployment.md +523 -0
- package/configs/_shared/rules/lang/python/error-handling.md +330 -0
- package/configs/_shared/rules/lang/python/migrations.md +421 -0
- package/configs/_shared/rules/lang/python/python.md +172 -0
- package/configs/_shared/rules/lang/python/repository.md +383 -0
- package/configs/{python/.claude/rules → _shared/rules/lang/python}/testing.md +2 -69
- package/configs/_shared/rules/lang/typescript/async.md +447 -0
- package/configs/_shared/rules/lang/typescript/generics.md +356 -0
- package/configs/_shared/rules/lang/typescript/typescript.md +212 -0
- package/configs/_shared/rules/quality/error-handling.md +48 -0
- package/configs/_shared/rules/quality/logging.md +45 -0
- package/configs/_shared/rules/quality/observability.md +240 -0
- package/configs/_shared/rules/quality/testing-patterns.md +65 -0
- package/configs/_shared/rules/security/secrets-management.md +222 -0
- package/configs/_shared/skills/analysis/explore/SKILL.md +257 -0
- package/configs/_shared/skills/analysis/security-audit/SKILL.md +184 -0
- package/configs/_shared/skills/dev/api-endpoint/SKILL.md +126 -0
- package/configs/_shared/{.claude/commands/generate-tests.md → skills/dev/generate-tests/SKILL.md} +6 -0
- package/configs/_shared/{.claude/commands/fix-issue.md → skills/git/fix-issue/SKILL.md} +6 -0
- package/configs/_shared/{.claude/commands/review-pr.md → skills/git/review-pr/SKILL.md} +6 -0
- package/configs/_shared/skills/infra/deploy/SKILL.md +139 -0
- package/configs/_shared/skills/infra/docker/SKILL.md +95 -0
- package/configs/_shared/skills/infra/migration/SKILL.md +158 -0
- package/configs/_shared/skills/nx/nx-affected/SKILL.md +72 -0
- package/configs/_shared/skills/nx/nx-lib/SKILL.md +375 -0
- package/configs/angular/CLAUDE.md +24 -216
- package/configs/angular/{.claude/rules → rules/core}/components.md +69 -15
- package/configs/angular/rules/core/resource.md +285 -0
- package/configs/angular/rules/core/signals.md +323 -0
- package/configs/angular/rules/http.md +338 -0
- package/configs/angular/rules/routing.md +291 -0
- package/configs/angular/rules/ssr.md +312 -0
- package/configs/angular/rules/state/signal-store.md +408 -0
- package/configs/angular/{.claude/rules → rules/state}/state.md +2 -2
- package/configs/angular/{.claude/rules → rules}/testing.md +7 -7
- package/configs/angular/rules/ui/aria.md +422 -0
- package/configs/angular/rules/ui/forms.md +424 -0
- package/configs/angular/rules/ui/pipes-directives.md +335 -0
- package/configs/angular/{.claude/settings.json → settings.json} +3 -0
- package/configs/dotnet/CLAUDE.md +53 -286
- package/configs/dotnet/rules/background-services.md +552 -0
- package/configs/dotnet/rules/configuration.md +426 -0
- package/configs/dotnet/rules/ddd.md +447 -0
- package/configs/dotnet/rules/dependency-injection.md +343 -0
- package/configs/dotnet/rules/mediatr.md +320 -0
- package/configs/dotnet/rules/middleware.md +489 -0
- package/configs/dotnet/rules/result-pattern.md +363 -0
- package/configs/dotnet/rules/validation.md +388 -0
- package/configs/dotnet/settings.json +29 -0
- package/configs/fastapi/CLAUDE.md +144 -0
- package/configs/fastapi/rules/background-tasks.md +254 -0
- package/configs/fastapi/rules/dependencies.md +170 -0
- package/configs/{python/.claude → fastapi}/rules/fastapi.md +61 -1
- package/configs/fastapi/rules/lifespan.md +274 -0
- package/configs/fastapi/rules/middleware.md +229 -0
- package/configs/fastapi/rules/pydantic.md +433 -0
- package/configs/fastapi/rules/responses.md +251 -0
- package/configs/fastapi/rules/routers.md +202 -0
- package/configs/fastapi/rules/security.md +222 -0
- package/configs/fastapi/rules/testing.md +251 -0
- package/configs/fastapi/rules/websockets.md +298 -0
- package/configs/fastapi/settings.json +35 -0
- package/configs/flask/CLAUDE.md +166 -0
- package/configs/flask/rules/blueprints.md +208 -0
- package/configs/flask/rules/cli.md +285 -0
- package/configs/flask/rules/configuration.md +281 -0
- package/configs/flask/rules/context.md +238 -0
- package/configs/flask/rules/error-handlers.md +278 -0
- package/configs/flask/rules/extensions.md +278 -0
- package/configs/flask/rules/flask.md +171 -0
- package/configs/flask/rules/marshmallow.md +206 -0
- package/configs/flask/rules/security.md +267 -0
- package/configs/flask/rules/testing.md +284 -0
- package/configs/flask/settings.json +35 -0
- package/configs/nestjs/CLAUDE.md +57 -215
- package/configs/nestjs/rules/common-patterns.md +300 -0
- package/configs/nestjs/rules/filters.md +376 -0
- package/configs/nestjs/rules/interceptors.md +317 -0
- package/configs/nestjs/rules/middleware.md +321 -0
- package/configs/nestjs/{.claude/rules → rules}/modules.md +26 -0
- package/configs/nestjs/rules/pipes.md +351 -0
- package/configs/nestjs/rules/websockets.md +451 -0
- package/configs/nestjs/settings.json +31 -0
- package/configs/nextjs/CLAUDE.md +69 -331
- package/configs/nextjs/rules/api-routes.md +358 -0
- package/configs/nextjs/rules/authentication.md +355 -0
- package/configs/nextjs/{.claude/rules → rules}/components.md +52 -0
- package/configs/nextjs/rules/data-fetching.md +249 -0
- package/configs/nextjs/rules/database.md +400 -0
- package/configs/nextjs/rules/middleware.md +303 -0
- package/configs/nextjs/rules/routing.md +324 -0
- package/configs/nextjs/rules/seo.md +350 -0
- package/configs/nextjs/rules/server-actions.md +353 -0
- package/configs/nextjs/{.claude/rules → rules}/state/zustand.md +6 -6
- package/configs/nextjs/{.claude/settings.json → settings.json} +7 -0
- package/package.json +24 -9
- package/src/cli.js +218 -0
- package/src/config.js +63 -0
- package/src/index.js +4 -0
- package/src/installer.js +414 -0
- package/src/merge.js +109 -0
- package/src/tech-config.json +45 -0
- package/src/utils.js +88 -0
- package/configs/dotnet/.claude/settings.json +0 -9
- package/configs/nestjs/.claude/settings.json +0 -15
- package/configs/python/.claude/rules/flask.md +0 -332
- package/configs/python/.claude/settings.json +0 -18
- package/configs/python/CLAUDE.md +0 -273
- package/src/install.js +0 -315
- /package/configs/_shared/{.claude/rules → rules/domain/frontend}/accessibility.md +0 -0
- /package/configs/_shared/{.claude/rules → rules/security}/security.md +0 -0
- /package/configs/_shared/{.claude/skills → skills/dev}/debug/SKILL.md +0 -0
- /package/configs/_shared/{.claude/skills → skills/dev}/learning/SKILL.md +0 -0
- /package/configs/_shared/{.claude/skills → skills/dev}/spec/SKILL.md +0 -0
- /package/configs/_shared/{.claude/skills → skills/git}/review/SKILL.md +0 -0
- /package/configs/dotnet/{.claude/rules → rules}/api.md +0 -0
- /package/configs/dotnet/{.claude/rules → rules}/architecture.md +0 -0
- /package/configs/dotnet/{.claude/rules → rules}/database/efcore.md +0 -0
- /package/configs/dotnet/{.claude/rules → rules}/testing.md +0 -0
- /package/configs/nestjs/{.claude/rules → rules}/auth.md +0 -0
- /package/configs/nestjs/{.claude/rules → rules}/database/prisma.md +0 -0
- /package/configs/nestjs/{.claude/rules → rules}/database/typeorm.md +0 -0
- /package/configs/nestjs/{.claude/rules → rules}/testing.md +0 -0
- /package/configs/nestjs/{.claude/rules → rules}/validation.md +0 -0
- /package/configs/nextjs/{.claude/rules → rules}/state/redux-toolkit.md +0 -0
- /package/configs/nextjs/{.claude/rules → rules}/testing.md +0 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "**/*.py"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# FastAPI WebSocket Patterns
|
|
7
|
+
|
|
8
|
+
## Basic WebSocket
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
from fastapi import WebSocket, WebSocketDisconnect
|
|
12
|
+
|
|
13
|
+
@app.websocket("/ws")
|
|
14
|
+
async def websocket_endpoint(websocket: WebSocket):
|
|
15
|
+
await websocket.accept()
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
while True:
|
|
19
|
+
data = await websocket.receive_text()
|
|
20
|
+
await websocket.send_text(f"Echo: {data}")
|
|
21
|
+
except WebSocketDisconnect:
|
|
22
|
+
print("Client disconnected")
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Connection Manager
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from typing import List
|
|
29
|
+
|
|
30
|
+
class ConnectionManager:
|
|
31
|
+
def __init__(self):
|
|
32
|
+
self.active_connections: list[WebSocket] = []
|
|
33
|
+
|
|
34
|
+
async def connect(self, websocket: WebSocket):
|
|
35
|
+
await websocket.accept()
|
|
36
|
+
self.active_connections.append(websocket)
|
|
37
|
+
|
|
38
|
+
def disconnect(self, websocket: WebSocket):
|
|
39
|
+
self.active_connections.remove(websocket)
|
|
40
|
+
|
|
41
|
+
async def send_personal_message(self, message: str, websocket: WebSocket):
|
|
42
|
+
await websocket.send_text(message)
|
|
43
|
+
|
|
44
|
+
async def broadcast(self, message: str):
|
|
45
|
+
for connection in self.active_connections:
|
|
46
|
+
await connection.send_text(message)
|
|
47
|
+
|
|
48
|
+
manager = ConnectionManager()
|
|
49
|
+
|
|
50
|
+
@app.websocket("/ws/{client_id}")
|
|
51
|
+
async def websocket_endpoint(websocket: WebSocket, client_id: str):
|
|
52
|
+
await manager.connect(websocket)
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
while True:
|
|
56
|
+
data = await websocket.receive_text()
|
|
57
|
+
await manager.broadcast(f"Client {client_id}: {data}")
|
|
58
|
+
except WebSocketDisconnect:
|
|
59
|
+
manager.disconnect(websocket)
|
|
60
|
+
await manager.broadcast(f"Client {client_id} left")
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Room-Based Connections
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from collections import defaultdict
|
|
67
|
+
|
|
68
|
+
class RoomManager:
|
|
69
|
+
def __init__(self):
|
|
70
|
+
self.rooms: dict[str, list[WebSocket]] = defaultdict(list)
|
|
71
|
+
|
|
72
|
+
async def join_room(self, room: str, websocket: WebSocket):
|
|
73
|
+
await websocket.accept()
|
|
74
|
+
self.rooms[room].append(websocket)
|
|
75
|
+
|
|
76
|
+
def leave_room(self, room: str, websocket: WebSocket):
|
|
77
|
+
if websocket in self.rooms[room]:
|
|
78
|
+
self.rooms[room].remove(websocket)
|
|
79
|
+
if not self.rooms[room]:
|
|
80
|
+
del self.rooms[room]
|
|
81
|
+
|
|
82
|
+
async def broadcast_to_room(self, room: str, message: str, exclude: WebSocket = None):
|
|
83
|
+
for connection in self.rooms[room]:
|
|
84
|
+
if connection != exclude:
|
|
85
|
+
await connection.send_text(message)
|
|
86
|
+
|
|
87
|
+
room_manager = RoomManager()
|
|
88
|
+
|
|
89
|
+
@app.websocket("/ws/room/{room_id}")
|
|
90
|
+
async def room_websocket(websocket: WebSocket, room_id: str):
|
|
91
|
+
await room_manager.join_room(room_id, websocket)
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
while True:
|
|
95
|
+
data = await websocket.receive_text()
|
|
96
|
+
await room_manager.broadcast_to_room(room_id, data, exclude=websocket)
|
|
97
|
+
except WebSocketDisconnect:
|
|
98
|
+
room_manager.leave_room(room_id, websocket)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Authentication
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from fastapi import Query, status
|
|
105
|
+
|
|
106
|
+
async def get_user_from_token(token: str) -> User | None:
|
|
107
|
+
# Verify JWT token
|
|
108
|
+
try:
|
|
109
|
+
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
|
|
110
|
+
return await get_user(payload["sub"])
|
|
111
|
+
except JWTError:
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
@app.websocket("/ws")
|
|
115
|
+
async def websocket_endpoint(
|
|
116
|
+
websocket: WebSocket,
|
|
117
|
+
token: str = Query(...),
|
|
118
|
+
):
|
|
119
|
+
user = await get_user_from_token(token)
|
|
120
|
+
|
|
121
|
+
if not user:
|
|
122
|
+
await websocket.close(code=status.WS_1008_POLICY_VIOLATION)
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
await websocket.accept()
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
while True:
|
|
129
|
+
data = await websocket.receive_text()
|
|
130
|
+
await websocket.send_text(f"Hello {user.name}: {data}")
|
|
131
|
+
except WebSocketDisconnect:
|
|
132
|
+
pass
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## JSON Messages
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
from pydantic import BaseModel
|
|
139
|
+
|
|
140
|
+
class WSMessage(BaseModel):
|
|
141
|
+
type: str
|
|
142
|
+
payload: dict
|
|
143
|
+
|
|
144
|
+
class WSResponse(BaseModel):
|
|
145
|
+
type: str
|
|
146
|
+
data: dict
|
|
147
|
+
|
|
148
|
+
@app.websocket("/ws")
|
|
149
|
+
async def websocket_endpoint(websocket: WebSocket):
|
|
150
|
+
await websocket.accept()
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
while True:
|
|
154
|
+
raw_data = await websocket.receive_json()
|
|
155
|
+
message = WSMessage(**raw_data)
|
|
156
|
+
|
|
157
|
+
if message.type == "ping":
|
|
158
|
+
response = WSResponse(type="pong", data={})
|
|
159
|
+
elif message.type == "subscribe":
|
|
160
|
+
# Handle subscription
|
|
161
|
+
response = WSResponse(type="subscribed", data=message.payload)
|
|
162
|
+
else:
|
|
163
|
+
response = WSResponse(type="error", data={"message": "Unknown type"})
|
|
164
|
+
|
|
165
|
+
await websocket.send_json(response.model_dump())
|
|
166
|
+
except WebSocketDisconnect:
|
|
167
|
+
pass
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Pub/Sub with Redis
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
import aioredis
|
|
174
|
+
import asyncio
|
|
175
|
+
|
|
176
|
+
class PubSubManager:
|
|
177
|
+
def __init__(self, redis_url: str):
|
|
178
|
+
self.redis_url = redis_url
|
|
179
|
+
self.pubsub = None
|
|
180
|
+
self.redis = None
|
|
181
|
+
|
|
182
|
+
async def connect(self):
|
|
183
|
+
self.redis = await aioredis.from_url(self.redis_url)
|
|
184
|
+
self.pubsub = self.redis.pubsub()
|
|
185
|
+
|
|
186
|
+
async def subscribe(self, channel: str):
|
|
187
|
+
await self.pubsub.subscribe(channel)
|
|
188
|
+
|
|
189
|
+
async def publish(self, channel: str, message: str):
|
|
190
|
+
await self.redis.publish(channel, message)
|
|
191
|
+
|
|
192
|
+
async def listen(self):
|
|
193
|
+
async for message in self.pubsub.listen():
|
|
194
|
+
if message["type"] == "message":
|
|
195
|
+
yield message["data"].decode()
|
|
196
|
+
|
|
197
|
+
pubsub = PubSubManager("redis://localhost")
|
|
198
|
+
|
|
199
|
+
@app.websocket("/ws/subscribe/{channel}")
|
|
200
|
+
async def subscribe_websocket(websocket: WebSocket, channel: str):
|
|
201
|
+
await websocket.accept()
|
|
202
|
+
await pubsub.connect()
|
|
203
|
+
await pubsub.subscribe(channel)
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
# Task to receive from WebSocket
|
|
207
|
+
async def receive():
|
|
208
|
+
while True:
|
|
209
|
+
data = await websocket.receive_text()
|
|
210
|
+
await pubsub.publish(channel, data)
|
|
211
|
+
|
|
212
|
+
# Task to send from Redis
|
|
213
|
+
async def send():
|
|
214
|
+
async for message in pubsub.listen():
|
|
215
|
+
await websocket.send_text(message)
|
|
216
|
+
|
|
217
|
+
await asyncio.gather(receive(), send())
|
|
218
|
+
except WebSocketDisconnect:
|
|
219
|
+
pass
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Heartbeat / Keep-Alive
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
import asyncio
|
|
226
|
+
|
|
227
|
+
@app.websocket("/ws")
|
|
228
|
+
async def websocket_endpoint(websocket: WebSocket):
|
|
229
|
+
await websocket.accept()
|
|
230
|
+
|
|
231
|
+
async def send_heartbeat():
|
|
232
|
+
while True:
|
|
233
|
+
try:
|
|
234
|
+
await asyncio.sleep(30)
|
|
235
|
+
await websocket.send_json({"type": "ping"})
|
|
236
|
+
except:
|
|
237
|
+
break
|
|
238
|
+
|
|
239
|
+
heartbeat_task = asyncio.create_task(send_heartbeat())
|
|
240
|
+
|
|
241
|
+
try:
|
|
242
|
+
while True:
|
|
243
|
+
data = await websocket.receive_json()
|
|
244
|
+
|
|
245
|
+
if data.get("type") == "pong":
|
|
246
|
+
continue # Heartbeat response
|
|
247
|
+
|
|
248
|
+
# Handle other messages
|
|
249
|
+
await process_message(data)
|
|
250
|
+
except WebSocketDisconnect:
|
|
251
|
+
heartbeat_task.cancel()
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## Rate Limiting WebSocket
|
|
255
|
+
|
|
256
|
+
```python
|
|
257
|
+
from collections import defaultdict
|
|
258
|
+
import time
|
|
259
|
+
|
|
260
|
+
class WebSocketRateLimiter:
|
|
261
|
+
def __init__(self, max_messages: int = 10, window: int = 1):
|
|
262
|
+
self.max_messages = max_messages
|
|
263
|
+
self.window = window
|
|
264
|
+
self.messages: dict[WebSocket, list[float]] = defaultdict(list)
|
|
265
|
+
|
|
266
|
+
def is_allowed(self, websocket: WebSocket) -> bool:
|
|
267
|
+
now = time.time()
|
|
268
|
+
|
|
269
|
+
# Clean old messages
|
|
270
|
+
self.messages[websocket] = [
|
|
271
|
+
t for t in self.messages[websocket]
|
|
272
|
+
if now - t < self.window
|
|
273
|
+
]
|
|
274
|
+
|
|
275
|
+
if len(self.messages[websocket]) >= self.max_messages:
|
|
276
|
+
return False
|
|
277
|
+
|
|
278
|
+
self.messages[websocket].append(now)
|
|
279
|
+
return True
|
|
280
|
+
|
|
281
|
+
rate_limiter = WebSocketRateLimiter(max_messages=10, window=1)
|
|
282
|
+
|
|
283
|
+
@app.websocket("/ws")
|
|
284
|
+
async def websocket_endpoint(websocket: WebSocket):
|
|
285
|
+
await websocket.accept()
|
|
286
|
+
|
|
287
|
+
try:
|
|
288
|
+
while True:
|
|
289
|
+
data = await websocket.receive_text()
|
|
290
|
+
|
|
291
|
+
if not rate_limiter.is_allowed(websocket):
|
|
292
|
+
await websocket.send_json({"error": "Rate limit exceeded"})
|
|
293
|
+
continue
|
|
294
|
+
|
|
295
|
+
await process_and_respond(websocket, data)
|
|
296
|
+
except WebSocketDisconnect:
|
|
297
|
+
pass
|
|
298
|
+
```
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(python *)",
|
|
5
|
+
"Bash(python3 *)",
|
|
6
|
+
"Bash(uvicorn *)",
|
|
7
|
+
"Bash(gunicorn *)",
|
|
8
|
+
"Bash(fastapi *)",
|
|
9
|
+
"Bash(pytest *)",
|
|
10
|
+
"Bash(ruff *)",
|
|
11
|
+
"Bash(mypy *)",
|
|
12
|
+
"Bash(alembic *)",
|
|
13
|
+
"Bash(uv *)",
|
|
14
|
+
"Bash(poetry *)",
|
|
15
|
+
"Bash(pip *)",
|
|
16
|
+
"Bash(pip3 *)",
|
|
17
|
+
"Read",
|
|
18
|
+
"Edit",
|
|
19
|
+
"Write"
|
|
20
|
+
],
|
|
21
|
+
"deny": [
|
|
22
|
+
"Bash(git push *)",
|
|
23
|
+
"Bash(git push)",
|
|
24
|
+
"Bash(rm -rf *)",
|
|
25
|
+
"Read(.env)",
|
|
26
|
+
"Read(.env.*)",
|
|
27
|
+
"Read(**/secrets/**)",
|
|
28
|
+
"Read(**/*.pem)"
|
|
29
|
+
]
|
|
30
|
+
},
|
|
31
|
+
"env": {
|
|
32
|
+
"PYTHONDONTWRITEBYTECODE": "1",
|
|
33
|
+
"PYTHONUNBUFFERED": "1"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# Flask Project Guidelines
|
|
2
|
+
|
|
3
|
+
@../_shared/CLAUDE.md
|
|
4
|
+
|
|
5
|
+
## Stack
|
|
6
|
+
|
|
7
|
+
- Python 3.12+
|
|
8
|
+
- Flask 3.0+
|
|
9
|
+
- SQLAlchemy 2.0+ (sync or async)
|
|
10
|
+
- Marshmallow for validation
|
|
11
|
+
- pytest for testing
|
|
12
|
+
- uv or poetry for dependencies
|
|
13
|
+
|
|
14
|
+
## Architecture
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
src/app/
|
|
18
|
+
├── __init__.py # App factory
|
|
19
|
+
├── config.py # Configuration classes
|
|
20
|
+
├── extensions.py # Flask extensions (db, migrate, etc.)
|
|
21
|
+
├── [domain]/ # Feature blueprints
|
|
22
|
+
│ ├── __init__.py # Blueprint registration
|
|
23
|
+
│ ├── routes.py # Route handlers
|
|
24
|
+
│ ├── schemas.py # Marshmallow schemas
|
|
25
|
+
│ ├── models.py # SQLAlchemy models
|
|
26
|
+
│ ├── services.py # Business logic
|
|
27
|
+
│ └── repository.py # Data access
|
|
28
|
+
├── core/ # Shared utilities
|
|
29
|
+
│ ├── exceptions.py
|
|
30
|
+
│ └── security.py
|
|
31
|
+
└── common/
|
|
32
|
+
├── models.py # Base models
|
|
33
|
+
└── schemas.py # Shared schemas
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Flask Patterns
|
|
37
|
+
|
|
38
|
+
### Application Factory
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from flask import Flask
|
|
42
|
+
from app.extensions import db, migrate
|
|
43
|
+
from app.config import config
|
|
44
|
+
|
|
45
|
+
def create_app(config_name: str = "development") -> Flask:
|
|
46
|
+
app = Flask(__name__)
|
|
47
|
+
app.config.from_object(config[config_name])
|
|
48
|
+
|
|
49
|
+
# Initialize extensions
|
|
50
|
+
db.init_app(app)
|
|
51
|
+
migrate.init_app(app, db)
|
|
52
|
+
|
|
53
|
+
# Register blueprints
|
|
54
|
+
from app.users import users_bp
|
|
55
|
+
app.register_blueprint(users_bp, url_prefix="/api/v1/users")
|
|
56
|
+
|
|
57
|
+
# Register error handlers
|
|
58
|
+
register_error_handlers(app)
|
|
59
|
+
|
|
60
|
+
return app
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Blueprints
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from flask import Blueprint, request, jsonify
|
|
67
|
+
from app.users.schemas import UserCreateSchema, UserResponseSchema
|
|
68
|
+
from app.users.services import UserService
|
|
69
|
+
|
|
70
|
+
users_bp = Blueprint("users", __name__)
|
|
71
|
+
|
|
72
|
+
@users_bp.route("/", methods=["POST"])
|
|
73
|
+
def create_user():
|
|
74
|
+
schema = UserCreateSchema()
|
|
75
|
+
data = schema.load(request.get_json())
|
|
76
|
+
|
|
77
|
+
user = UserService.create(data)
|
|
78
|
+
|
|
79
|
+
return jsonify(UserResponseSchema().dump(user)), 201
|
|
80
|
+
|
|
81
|
+
@users_bp.route("/<int:user_id>")
|
|
82
|
+
def get_user(user_id: int):
|
|
83
|
+
user = UserService.get_by_id(user_id)
|
|
84
|
+
if not user:
|
|
85
|
+
return jsonify({"error": "User not found"}), 404
|
|
86
|
+
return jsonify(UserResponseSchema().dump(user))
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Extensions
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
# extensions.py
|
|
93
|
+
from flask_sqlalchemy import SQLAlchemy
|
|
94
|
+
from flask_migrate import Migrate
|
|
95
|
+
from flask_jwt_extended import JWTManager
|
|
96
|
+
|
|
97
|
+
db = SQLAlchemy()
|
|
98
|
+
migrate = Migrate()
|
|
99
|
+
jwt = JWTManager()
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Marshmallow Schemas
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from marshmallow import Schema, fields, validate, post_load
|
|
106
|
+
|
|
107
|
+
class UserCreateSchema(Schema):
|
|
108
|
+
email = fields.Email(required=True)
|
|
109
|
+
password = fields.Str(required=True, validate=validate.Length(min=8))
|
|
110
|
+
name = fields.Str(required=True, validate=validate.Length(min=1, max=100))
|
|
111
|
+
|
|
112
|
+
class UserResponseSchema(Schema):
|
|
113
|
+
id = fields.Int(dump_only=True)
|
|
114
|
+
email = fields.Email()
|
|
115
|
+
name = fields.Str()
|
|
116
|
+
created_at = fields.DateTime(dump_only=True)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## SQLAlchemy 2.0
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
123
|
+
from app.extensions import db
|
|
124
|
+
|
|
125
|
+
class User(db.Model):
|
|
126
|
+
__tablename__ = "users"
|
|
127
|
+
|
|
128
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
129
|
+
email: Mapped[str] = mapped_column(unique=True, index=True)
|
|
130
|
+
hashed_password: Mapped[str]
|
|
131
|
+
name: Mapped[str] = mapped_column(db.String(100))
|
|
132
|
+
is_active: Mapped[bool] = mapped_column(default=True)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Error Handling
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
from flask import jsonify
|
|
139
|
+
from werkzeug.exceptions import HTTPException
|
|
140
|
+
|
|
141
|
+
def register_error_handlers(app):
|
|
142
|
+
@app.errorhandler(HTTPException)
|
|
143
|
+
def handle_http_error(error):
|
|
144
|
+
return jsonify({
|
|
145
|
+
"error": error.name,
|
|
146
|
+
"message": error.description,
|
|
147
|
+
}), error.code
|
|
148
|
+
|
|
149
|
+
@app.errorhandler(ValidationError)
|
|
150
|
+
def handle_validation_error(error):
|
|
151
|
+
return jsonify({
|
|
152
|
+
"error": "Validation Error",
|
|
153
|
+
"details": error.messages,
|
|
154
|
+
}), 400
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Commands
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
flask run # Dev server
|
|
161
|
+
flask db upgrade # Run migrations
|
|
162
|
+
flask db migrate -m "message" # Generate migration
|
|
163
|
+
pytest # Run tests
|
|
164
|
+
pytest --cov=app # Coverage
|
|
165
|
+
ruff check . && ruff format . # Lint + format
|
|
166
|
+
```
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "**/*.py"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Flask Blueprint Patterns
|
|
7
|
+
|
|
8
|
+
## Basic Blueprint
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
# GOOD - organized blueprint
|
|
12
|
+
from flask import Blueprint, request, jsonify
|
|
13
|
+
|
|
14
|
+
users_bp = Blueprint("users", __name__)
|
|
15
|
+
|
|
16
|
+
@users_bp.route("/", methods=["GET"])
|
|
17
|
+
def list_users():
|
|
18
|
+
"""List all users."""
|
|
19
|
+
users = UserService.get_all()
|
|
20
|
+
return jsonify(UserSchema(many=True).dump(users))
|
|
21
|
+
|
|
22
|
+
@users_bp.route("/", methods=["POST"])
|
|
23
|
+
def create_user():
|
|
24
|
+
"""Create a new user."""
|
|
25
|
+
schema = UserCreateSchema()
|
|
26
|
+
data = schema.load(request.get_json())
|
|
27
|
+
user = UserService.create(data)
|
|
28
|
+
return jsonify(UserSchema().dump(user)), 201
|
|
29
|
+
|
|
30
|
+
@users_bp.route("/<int:user_id>")
|
|
31
|
+
def get_user(user_id: int):
|
|
32
|
+
"""Get user by ID."""
|
|
33
|
+
user = UserService.get_by_id(user_id)
|
|
34
|
+
if not user:
|
|
35
|
+
return jsonify({"error": "User not found"}), 404
|
|
36
|
+
return jsonify(UserSchema().dump(user))
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Blueprint Registration
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
# app/__init__.py
|
|
43
|
+
from flask import Flask
|
|
44
|
+
|
|
45
|
+
def create_app(config_name: str = "development") -> Flask:
|
|
46
|
+
app = Flask(__name__)
|
|
47
|
+
app.config.from_object(config[config_name])
|
|
48
|
+
|
|
49
|
+
# Register blueprints
|
|
50
|
+
from app.users import users_bp
|
|
51
|
+
from app.auth import auth_bp
|
|
52
|
+
from app.products import products_bp
|
|
53
|
+
|
|
54
|
+
app.register_blueprint(users_bp, url_prefix="/api/v1/users")
|
|
55
|
+
app.register_blueprint(auth_bp, url_prefix="/api/v1/auth")
|
|
56
|
+
app.register_blueprint(products_bp, url_prefix="/api/v1/products")
|
|
57
|
+
|
|
58
|
+
return app
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Blueprint with Resources
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
# app/users/__init__.py
|
|
65
|
+
from flask import Blueprint
|
|
66
|
+
|
|
67
|
+
users_bp = Blueprint("users", __name__)
|
|
68
|
+
|
|
69
|
+
# Import routes after blueprint creation to avoid circular imports
|
|
70
|
+
from app.users import routes # noqa: E402, F401
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
# app/users/routes.py
|
|
75
|
+
from app.users import users_bp
|
|
76
|
+
from app.users.schemas import UserSchema, UserCreateSchema
|
|
77
|
+
from app.users.services import UserService
|
|
78
|
+
|
|
79
|
+
@users_bp.route("/", methods=["GET"])
|
|
80
|
+
def list_users():
|
|
81
|
+
...
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Nested Blueprints
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
# API versioning with nested blueprints
|
|
88
|
+
api_v1 = Blueprint("api_v1", __name__, url_prefix="/api/v1")
|
|
89
|
+
api_v2 = Blueprint("api_v2", __name__, url_prefix="/api/v2")
|
|
90
|
+
|
|
91
|
+
# Register child blueprints
|
|
92
|
+
api_v1.register_blueprint(users_bp, url_prefix="/users")
|
|
93
|
+
api_v1.register_blueprint(products_bp, url_prefix="/products")
|
|
94
|
+
|
|
95
|
+
api_v2.register_blueprint(users_v2_bp, url_prefix="/users")
|
|
96
|
+
|
|
97
|
+
# Register with app
|
|
98
|
+
app.register_blueprint(api_v1)
|
|
99
|
+
app.register_blueprint(api_v2)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Blueprint-Specific Error Handlers
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
users_bp = Blueprint("users", __name__)
|
|
106
|
+
|
|
107
|
+
@users_bp.errorhandler(404)
|
|
108
|
+
def user_not_found(error):
|
|
109
|
+
return jsonify({"error": "User not found"}), 404
|
|
110
|
+
|
|
111
|
+
@users_bp.errorhandler(ValidationError)
|
|
112
|
+
def validation_error(error):
|
|
113
|
+
return jsonify({"error": "Validation failed", "details": error.messages}), 400
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Blueprint Hooks
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
users_bp = Blueprint("users", __name__)
|
|
120
|
+
|
|
121
|
+
@users_bp.before_request
|
|
122
|
+
def before_user_request():
|
|
123
|
+
"""Run before every request to this blueprint."""
|
|
124
|
+
# Verify API key, log request, etc.
|
|
125
|
+
if not verify_api_key(request.headers.get("X-API-Key")):
|
|
126
|
+
return jsonify({"error": "Invalid API key"}), 401
|
|
127
|
+
|
|
128
|
+
@users_bp.after_request
|
|
129
|
+
def after_user_request(response):
|
|
130
|
+
"""Run after every request to this blueprint."""
|
|
131
|
+
response.headers["X-Blueprint"] = "users"
|
|
132
|
+
return response
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Blueprint URL Builders
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
from flask import url_for
|
|
139
|
+
|
|
140
|
+
# Build URL for blueprint route
|
|
141
|
+
url = url_for("users.get_user", user_id=1) # /api/v1/users/1
|
|
142
|
+
|
|
143
|
+
# With external URL
|
|
144
|
+
url = url_for("users.get_user", user_id=1, _external=True)
|
|
145
|
+
# https://example.com/api/v1/users/1
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Blueprint Templates
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
# Blueprint with templates
|
|
152
|
+
users_bp = Blueprint(
|
|
153
|
+
"users",
|
|
154
|
+
__name__,
|
|
155
|
+
template_folder="templates",
|
|
156
|
+
static_folder="static",
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
@users_bp.route("/profile")
|
|
160
|
+
def profile():
|
|
161
|
+
return render_template("users/profile.html") # From blueprint's templates/
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Blueprint Context Processor
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
@users_bp.context_processor
|
|
168
|
+
def user_context():
|
|
169
|
+
"""Add variables to all templates rendered by this blueprint."""
|
|
170
|
+
return {
|
|
171
|
+
"current_user": get_current_user(),
|
|
172
|
+
"user_count": User.query.count(),
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Class-Based Views with Blueprints
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
from flask.views import MethodView
|
|
180
|
+
|
|
181
|
+
class UserAPI(MethodView):
|
|
182
|
+
def get(self, user_id: int | None = None):
|
|
183
|
+
if user_id is None:
|
|
184
|
+
return jsonify(UserSchema(many=True).dump(User.query.all()))
|
|
185
|
+
user = User.query.get_or_404(user_id)
|
|
186
|
+
return jsonify(UserSchema().dump(user))
|
|
187
|
+
|
|
188
|
+
def post(self):
|
|
189
|
+
data = UserCreateSchema().load(request.get_json())
|
|
190
|
+
user = UserService.create(data)
|
|
191
|
+
return jsonify(UserSchema().dump(user)), 201
|
|
192
|
+
|
|
193
|
+
def put(self, user_id: int):
|
|
194
|
+
user = User.query.get_or_404(user_id)
|
|
195
|
+
data = UserUpdateSchema().load(request.get_json())
|
|
196
|
+
user = UserService.update(user, data)
|
|
197
|
+
return jsonify(UserSchema().dump(user))
|
|
198
|
+
|
|
199
|
+
def delete(self, user_id: int):
|
|
200
|
+
user = User.query.get_or_404(user_id)
|
|
201
|
+
UserService.delete(user)
|
|
202
|
+
return "", 204
|
|
203
|
+
|
|
204
|
+
# Register view
|
|
205
|
+
user_view = UserAPI.as_view("user_api")
|
|
206
|
+
users_bp.add_url_rule("/", view_func=user_view, methods=["GET", "POST"])
|
|
207
|
+
users_bp.add_url_rule("/<int:user_id>", view_func=user_view, methods=["GET", "PUT", "DELETE"])
|
|
208
|
+
```
|