@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.
Files changed (145) hide show
  1. package/README.md +272 -121
  2. package/bin/cli.js +5 -2
  3. package/configs/_shared/CLAUDE.md +52 -149
  4. package/configs/_shared/rules/conventions/documentation.md +324 -0
  5. package/configs/_shared/rules/conventions/git.md +265 -0
  6. package/configs/_shared/rules/conventions/npm.md +80 -0
  7. package/configs/_shared/{.claude/rules → rules/conventions}/performance.md +1 -1
  8. package/configs/_shared/rules/conventions/principles.md +334 -0
  9. package/configs/_shared/rules/devops/ci-cd.md +262 -0
  10. package/configs/_shared/rules/devops/docker.md +275 -0
  11. package/configs/_shared/rules/devops/nx.md +194 -0
  12. package/configs/_shared/rules/domain/backend/api-design.md +203 -0
  13. package/configs/_shared/rules/lang/csharp/async.md +220 -0
  14. package/configs/_shared/rules/lang/csharp/csharp.md +314 -0
  15. package/configs/_shared/rules/lang/csharp/linq.md +210 -0
  16. package/configs/_shared/rules/lang/python/async.md +337 -0
  17. package/configs/_shared/rules/lang/python/celery.md +476 -0
  18. package/configs/_shared/rules/lang/python/config.md +339 -0
  19. package/configs/{python/.claude/rules → _shared/rules/lang/python}/database/sqlalchemy.md +6 -1
  20. package/configs/_shared/rules/lang/python/deployment.md +523 -0
  21. package/configs/_shared/rules/lang/python/error-handling.md +330 -0
  22. package/configs/_shared/rules/lang/python/migrations.md +421 -0
  23. package/configs/_shared/rules/lang/python/python.md +172 -0
  24. package/configs/_shared/rules/lang/python/repository.md +383 -0
  25. package/configs/{python/.claude/rules → _shared/rules/lang/python}/testing.md +2 -69
  26. package/configs/_shared/rules/lang/typescript/async.md +447 -0
  27. package/configs/_shared/rules/lang/typescript/generics.md +356 -0
  28. package/configs/_shared/rules/lang/typescript/typescript.md +212 -0
  29. package/configs/_shared/rules/quality/error-handling.md +48 -0
  30. package/configs/_shared/rules/quality/logging.md +45 -0
  31. package/configs/_shared/rules/quality/observability.md +240 -0
  32. package/configs/_shared/rules/quality/testing-patterns.md +65 -0
  33. package/configs/_shared/rules/security/secrets-management.md +222 -0
  34. package/configs/_shared/skills/analysis/explore/SKILL.md +257 -0
  35. package/configs/_shared/skills/analysis/security-audit/SKILL.md +184 -0
  36. package/configs/_shared/skills/dev/api-endpoint/SKILL.md +126 -0
  37. package/configs/_shared/{.claude/commands/generate-tests.md → skills/dev/generate-tests/SKILL.md} +6 -0
  38. package/configs/_shared/{.claude/commands/fix-issue.md → skills/git/fix-issue/SKILL.md} +6 -0
  39. package/configs/_shared/{.claude/commands/review-pr.md → skills/git/review-pr/SKILL.md} +6 -0
  40. package/configs/_shared/skills/infra/deploy/SKILL.md +139 -0
  41. package/configs/_shared/skills/infra/docker/SKILL.md +95 -0
  42. package/configs/_shared/skills/infra/migration/SKILL.md +158 -0
  43. package/configs/_shared/skills/nx/nx-affected/SKILL.md +72 -0
  44. package/configs/_shared/skills/nx/nx-lib/SKILL.md +375 -0
  45. package/configs/angular/CLAUDE.md +24 -216
  46. package/configs/angular/{.claude/rules → rules/core}/components.md +69 -15
  47. package/configs/angular/rules/core/resource.md +285 -0
  48. package/configs/angular/rules/core/signals.md +323 -0
  49. package/configs/angular/rules/http.md +338 -0
  50. package/configs/angular/rules/routing.md +291 -0
  51. package/configs/angular/rules/ssr.md +312 -0
  52. package/configs/angular/rules/state/signal-store.md +408 -0
  53. package/configs/angular/{.claude/rules → rules/state}/state.md +2 -2
  54. package/configs/angular/{.claude/rules → rules}/testing.md +7 -7
  55. package/configs/angular/rules/ui/aria.md +422 -0
  56. package/configs/angular/rules/ui/forms.md +424 -0
  57. package/configs/angular/rules/ui/pipes-directives.md +335 -0
  58. package/configs/angular/{.claude/settings.json → settings.json} +3 -0
  59. package/configs/dotnet/CLAUDE.md +53 -286
  60. package/configs/dotnet/rules/background-services.md +552 -0
  61. package/configs/dotnet/rules/configuration.md +426 -0
  62. package/configs/dotnet/rules/ddd.md +447 -0
  63. package/configs/dotnet/rules/dependency-injection.md +343 -0
  64. package/configs/dotnet/rules/mediatr.md +320 -0
  65. package/configs/dotnet/rules/middleware.md +489 -0
  66. package/configs/dotnet/rules/result-pattern.md +363 -0
  67. package/configs/dotnet/rules/validation.md +388 -0
  68. package/configs/dotnet/settings.json +29 -0
  69. package/configs/fastapi/CLAUDE.md +144 -0
  70. package/configs/fastapi/rules/background-tasks.md +254 -0
  71. package/configs/fastapi/rules/dependencies.md +170 -0
  72. package/configs/{python/.claude → fastapi}/rules/fastapi.md +61 -1
  73. package/configs/fastapi/rules/lifespan.md +274 -0
  74. package/configs/fastapi/rules/middleware.md +229 -0
  75. package/configs/fastapi/rules/pydantic.md +433 -0
  76. package/configs/fastapi/rules/responses.md +251 -0
  77. package/configs/fastapi/rules/routers.md +202 -0
  78. package/configs/fastapi/rules/security.md +222 -0
  79. package/configs/fastapi/rules/testing.md +251 -0
  80. package/configs/fastapi/rules/websockets.md +298 -0
  81. package/configs/fastapi/settings.json +35 -0
  82. package/configs/flask/CLAUDE.md +166 -0
  83. package/configs/flask/rules/blueprints.md +208 -0
  84. package/configs/flask/rules/cli.md +285 -0
  85. package/configs/flask/rules/configuration.md +281 -0
  86. package/configs/flask/rules/context.md +238 -0
  87. package/configs/flask/rules/error-handlers.md +278 -0
  88. package/configs/flask/rules/extensions.md +278 -0
  89. package/configs/flask/rules/flask.md +171 -0
  90. package/configs/flask/rules/marshmallow.md +206 -0
  91. package/configs/flask/rules/security.md +267 -0
  92. package/configs/flask/rules/testing.md +284 -0
  93. package/configs/flask/settings.json +35 -0
  94. package/configs/nestjs/CLAUDE.md +57 -215
  95. package/configs/nestjs/rules/common-patterns.md +300 -0
  96. package/configs/nestjs/rules/filters.md +376 -0
  97. package/configs/nestjs/rules/interceptors.md +317 -0
  98. package/configs/nestjs/rules/middleware.md +321 -0
  99. package/configs/nestjs/{.claude/rules → rules}/modules.md +26 -0
  100. package/configs/nestjs/rules/pipes.md +351 -0
  101. package/configs/nestjs/rules/websockets.md +451 -0
  102. package/configs/nestjs/settings.json +31 -0
  103. package/configs/nextjs/CLAUDE.md +69 -331
  104. package/configs/nextjs/rules/api-routes.md +358 -0
  105. package/configs/nextjs/rules/authentication.md +355 -0
  106. package/configs/nextjs/{.claude/rules → rules}/components.md +52 -0
  107. package/configs/nextjs/rules/data-fetching.md +249 -0
  108. package/configs/nextjs/rules/database.md +400 -0
  109. package/configs/nextjs/rules/middleware.md +303 -0
  110. package/configs/nextjs/rules/routing.md +324 -0
  111. package/configs/nextjs/rules/seo.md +350 -0
  112. package/configs/nextjs/rules/server-actions.md +353 -0
  113. package/configs/nextjs/{.claude/rules → rules}/state/zustand.md +6 -6
  114. package/configs/nextjs/{.claude/settings.json → settings.json} +7 -0
  115. package/package.json +24 -9
  116. package/src/cli.js +218 -0
  117. package/src/config.js +63 -0
  118. package/src/index.js +4 -0
  119. package/src/installer.js +414 -0
  120. package/src/merge.js +109 -0
  121. package/src/tech-config.json +45 -0
  122. package/src/utils.js +88 -0
  123. package/configs/dotnet/.claude/settings.json +0 -9
  124. package/configs/nestjs/.claude/settings.json +0 -15
  125. package/configs/python/.claude/rules/flask.md +0 -332
  126. package/configs/python/.claude/settings.json +0 -18
  127. package/configs/python/CLAUDE.md +0 -273
  128. package/src/install.js +0 -315
  129. /package/configs/_shared/{.claude/rules → rules/domain/frontend}/accessibility.md +0 -0
  130. /package/configs/_shared/{.claude/rules → rules/security}/security.md +0 -0
  131. /package/configs/_shared/{.claude/skills → skills/dev}/debug/SKILL.md +0 -0
  132. /package/configs/_shared/{.claude/skills → skills/dev}/learning/SKILL.md +0 -0
  133. /package/configs/_shared/{.claude/skills → skills/dev}/spec/SKILL.md +0 -0
  134. /package/configs/_shared/{.claude/skills → skills/git}/review/SKILL.md +0 -0
  135. /package/configs/dotnet/{.claude/rules → rules}/api.md +0 -0
  136. /package/configs/dotnet/{.claude/rules → rules}/architecture.md +0 -0
  137. /package/configs/dotnet/{.claude/rules → rules}/database/efcore.md +0 -0
  138. /package/configs/dotnet/{.claude/rules → rules}/testing.md +0 -0
  139. /package/configs/nestjs/{.claude/rules → rules}/auth.md +0 -0
  140. /package/configs/nestjs/{.claude/rules → rules}/database/prisma.md +0 -0
  141. /package/configs/nestjs/{.claude/rules → rules}/database/typeorm.md +0 -0
  142. /package/configs/nestjs/{.claude/rules → rules}/testing.md +0 -0
  143. /package/configs/nestjs/{.claude/rules → rules}/validation.md +0 -0
  144. /package/configs/nextjs/{.claude/rules → rules}/state/redux-toolkit.md +0 -0
  145. /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
+ ```