@malamute/ai-rules 1.0.0 → 1.2.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 +270 -121
- package/bin/cli.js +5 -2
- package/configs/_shared/.claude/rules/conventions/documentation.md +324 -0
- package/configs/_shared/.claude/rules/conventions/git.md +265 -0
- package/configs/_shared/.claude/rules/{performance.md → conventions/performance.md} +1 -1
- package/configs/_shared/.claude/rules/conventions/principles.md +334 -0
- package/configs/_shared/.claude/rules/devops/ci-cd.md +262 -0
- package/configs/_shared/.claude/rules/devops/docker.md +275 -0
- package/configs/_shared/.claude/rules/devops/nx.md +194 -0
- package/configs/_shared/.claude/rules/domain/backend/api-design.md +203 -0
- package/configs/_shared/.claude/rules/lang/csharp/async.md +220 -0
- package/configs/_shared/.claude/rules/lang/csharp/csharp.md +314 -0
- package/configs/_shared/.claude/rules/lang/csharp/linq.md +210 -0
- package/configs/_shared/.claude/rules/lang/python/async.md +337 -0
- package/configs/_shared/.claude/rules/lang/python/celery.md +476 -0
- package/configs/_shared/.claude/rules/lang/python/config.md +339 -0
- package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/database/sqlalchemy.md +6 -1
- package/configs/_shared/.claude/rules/lang/python/deployment.md +523 -0
- package/configs/_shared/.claude/rules/lang/python/error-handling.md +330 -0
- package/configs/_shared/.claude/rules/lang/python/migrations.md +421 -0
- package/configs/_shared/.claude/rules/lang/python/python.md +172 -0
- package/configs/_shared/.claude/rules/lang/python/repository.md +383 -0
- package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/testing.md +2 -69
- package/configs/_shared/.claude/rules/lang/typescript/async.md +447 -0
- package/configs/_shared/.claude/rules/lang/typescript/generics.md +356 -0
- package/configs/_shared/.claude/rules/lang/typescript/typescript.md +212 -0
- package/configs/_shared/.claude/rules/quality/error-handling.md +48 -0
- package/configs/_shared/.claude/rules/quality/logging.md +45 -0
- package/configs/_shared/.claude/rules/quality/observability.md +240 -0
- package/configs/_shared/.claude/rules/quality/testing-patterns.md +65 -0
- package/configs/_shared/.claude/rules/security/secrets-management.md +222 -0
- package/configs/_shared/.claude/skills/analysis/explore/SKILL.md +257 -0
- package/configs/_shared/.claude/skills/analysis/security-audit/SKILL.md +184 -0
- package/configs/_shared/.claude/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/.claude/skills/infra/deploy/SKILL.md +139 -0
- package/configs/_shared/.claude/skills/infra/docker/SKILL.md +95 -0
- package/configs/_shared/.claude/skills/infra/migration/SKILL.md +158 -0
- package/configs/_shared/.claude/skills/nx/nx-affected/SKILL.md +72 -0
- package/configs/_shared/.claude/skills/nx/nx-lib/SKILL.md +375 -0
- package/configs/_shared/CLAUDE.md +52 -149
- package/configs/angular/.claude/rules/{components.md → core/components.md} +69 -15
- package/configs/angular/.claude/rules/core/resource.md +285 -0
- package/configs/angular/.claude/rules/core/signals.md +323 -0
- package/configs/angular/.claude/rules/http.md +338 -0
- package/configs/angular/.claude/rules/routing.md +291 -0
- package/configs/angular/.claude/rules/ssr.md +312 -0
- package/configs/angular/.claude/rules/state/signal-store.md +408 -0
- package/configs/angular/.claude/rules/{state.md → state/state.md} +2 -2
- package/configs/angular/.claude/rules/testing.md +7 -7
- package/configs/angular/.claude/rules/ui/aria.md +422 -0
- package/configs/angular/.claude/rules/ui/forms.md +424 -0
- package/configs/angular/.claude/rules/ui/pipes-directives.md +335 -0
- package/configs/angular/.claude/settings.json +1 -0
- package/configs/angular/.claude/skills/ngrx-slice/SKILL.md +362 -0
- package/configs/angular/.claude/skills/signal-store/SKILL.md +445 -0
- package/configs/angular/CLAUDE.md +24 -216
- package/configs/dotnet/.claude/rules/background-services.md +552 -0
- package/configs/dotnet/.claude/rules/configuration.md +426 -0
- package/configs/dotnet/.claude/rules/ddd.md +447 -0
- package/configs/dotnet/.claude/rules/dependency-injection.md +343 -0
- package/configs/dotnet/.claude/rules/mediatr.md +320 -0
- package/configs/dotnet/.claude/rules/middleware.md +489 -0
- package/configs/dotnet/.claude/rules/result-pattern.md +363 -0
- package/configs/dotnet/.claude/rules/validation.md +388 -0
- package/configs/dotnet/.claude/settings.json +21 -3
- package/configs/dotnet/CLAUDE.md +53 -286
- package/configs/fastapi/.claude/rules/background-tasks.md +254 -0
- package/configs/fastapi/.claude/rules/dependencies.md +170 -0
- package/configs/{python → fastapi}/.claude/rules/fastapi.md +61 -1
- package/configs/fastapi/.claude/rules/lifespan.md +274 -0
- package/configs/fastapi/.claude/rules/middleware.md +229 -0
- package/configs/fastapi/.claude/rules/pydantic.md +433 -0
- package/configs/fastapi/.claude/rules/responses.md +251 -0
- package/configs/fastapi/.claude/rules/routers.md +202 -0
- package/configs/fastapi/.claude/rules/security.md +222 -0
- package/configs/fastapi/.claude/rules/testing.md +251 -0
- package/configs/fastapi/.claude/rules/websockets.md +298 -0
- package/configs/fastapi/.claude/settings.json +33 -0
- package/configs/fastapi/CLAUDE.md +144 -0
- package/configs/flask/.claude/rules/blueprints.md +208 -0
- package/configs/flask/.claude/rules/cli.md +285 -0
- package/configs/flask/.claude/rules/configuration.md +281 -0
- package/configs/flask/.claude/rules/context.md +238 -0
- package/configs/flask/.claude/rules/error-handlers.md +278 -0
- package/configs/flask/.claude/rules/extensions.md +278 -0
- package/configs/flask/.claude/rules/flask.md +171 -0
- package/configs/flask/.claude/rules/marshmallow.md +206 -0
- package/configs/flask/.claude/rules/security.md +267 -0
- package/configs/flask/.claude/rules/testing.md +284 -0
- package/configs/flask/.claude/settings.json +33 -0
- package/configs/flask/CLAUDE.md +166 -0
- package/configs/nestjs/.claude/rules/common-patterns.md +300 -0
- package/configs/nestjs/.claude/rules/filters.md +376 -0
- package/configs/nestjs/.claude/rules/interceptors.md +317 -0
- package/configs/nestjs/.claude/rules/middleware.md +321 -0
- package/configs/nestjs/.claude/rules/modules.md +26 -0
- package/configs/nestjs/.claude/rules/pipes.md +351 -0
- package/configs/nestjs/.claude/rules/websockets.md +451 -0
- package/configs/nestjs/.claude/settings.json +16 -2
- package/configs/nestjs/CLAUDE.md +57 -215
- package/configs/nextjs/.claude/rules/api-routes.md +358 -0
- package/configs/nextjs/.claude/rules/authentication.md +355 -0
- package/configs/nextjs/.claude/rules/components.md +52 -0
- package/configs/nextjs/.claude/rules/data-fetching.md +249 -0
- package/configs/nextjs/.claude/rules/database.md +400 -0
- package/configs/nextjs/.claude/rules/middleware.md +303 -0
- package/configs/nextjs/.claude/rules/routing.md +324 -0
- package/configs/nextjs/.claude/rules/seo.md +350 -0
- package/configs/nextjs/.claude/rules/server-actions.md +353 -0
- package/configs/nextjs/.claude/rules/state/zustand.md +6 -6
- package/configs/nextjs/.claude/settings.json +5 -0
- package/configs/nextjs/CLAUDE.md +69 -331
- package/package.json +23 -9
- package/src/cli.js +220 -0
- package/src/config.js +29 -0
- package/src/index.js +13 -0
- package/src/installer.js +361 -0
- package/src/merge.js +116 -0
- package/src/tech-config.json +29 -0
- package/src/utils.js +96 -0
- 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/{accessibility.md → domain/frontend/accessibility.md} +0 -0
- /package/configs/_shared/.claude/rules/{security.md → security/security.md} +0 -0
- /package/configs/_shared/.claude/skills/{debug → dev/debug}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{learning → dev/learning}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{spec → dev/spec}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{review → git/review}/SKILL.md +0 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "**/*.py"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Flask Error Handling
|
|
7
|
+
|
|
8
|
+
## HTTP Error Handlers
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
from flask import jsonify, render_template
|
|
12
|
+
from werkzeug.exceptions import HTTPException
|
|
13
|
+
|
|
14
|
+
# JSON API error handler
|
|
15
|
+
@app.errorhandler(HTTPException)
|
|
16
|
+
def handle_http_exception(error):
|
|
17
|
+
return jsonify({
|
|
18
|
+
"error": error.name,
|
|
19
|
+
"message": error.description,
|
|
20
|
+
"status_code": error.code,
|
|
21
|
+
}), error.code
|
|
22
|
+
|
|
23
|
+
# Specific error handlers
|
|
24
|
+
@app.errorhandler(404)
|
|
25
|
+
def not_found(error):
|
|
26
|
+
if request.accept_mimetypes.accept_json:
|
|
27
|
+
return jsonify({"error": "Not found"}), 404
|
|
28
|
+
return render_template("errors/404.html"), 404
|
|
29
|
+
|
|
30
|
+
@app.errorhandler(500)
|
|
31
|
+
def internal_error(error):
|
|
32
|
+
db.session.rollback() # Rollback failed transaction
|
|
33
|
+
return jsonify({"error": "Internal server error"}), 500
|
|
34
|
+
|
|
35
|
+
@app.errorhandler(429)
|
|
36
|
+
def rate_limit_exceeded(error):
|
|
37
|
+
return jsonify({
|
|
38
|
+
"error": "Rate limit exceeded",
|
|
39
|
+
"retry_after": error.description,
|
|
40
|
+
}), 429
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Custom Exception Classes
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
class AppException(Exception):
|
|
47
|
+
"""Base exception for application errors."""
|
|
48
|
+
status_code = 500
|
|
49
|
+
error_code = "INTERNAL_ERROR"
|
|
50
|
+
message = "An unexpected error occurred"
|
|
51
|
+
|
|
52
|
+
def __init__(self, message: str = None, payload: dict = None):
|
|
53
|
+
super().__init__()
|
|
54
|
+
self.message = message or self.message
|
|
55
|
+
self.payload = payload
|
|
56
|
+
|
|
57
|
+
def to_dict(self) -> dict:
|
|
58
|
+
rv = {
|
|
59
|
+
"error": self.error_code,
|
|
60
|
+
"message": self.message,
|
|
61
|
+
}
|
|
62
|
+
if self.payload:
|
|
63
|
+
rv["details"] = self.payload
|
|
64
|
+
return rv
|
|
65
|
+
|
|
66
|
+
class NotFoundError(AppException):
|
|
67
|
+
status_code = 404
|
|
68
|
+
error_code = "NOT_FOUND"
|
|
69
|
+
message = "Resource not found"
|
|
70
|
+
|
|
71
|
+
class ValidationError(AppException):
|
|
72
|
+
status_code = 400
|
|
73
|
+
error_code = "VALIDATION_ERROR"
|
|
74
|
+
message = "Validation failed"
|
|
75
|
+
|
|
76
|
+
class UnauthorizedError(AppException):
|
|
77
|
+
status_code = 401
|
|
78
|
+
error_code = "UNAUTHORIZED"
|
|
79
|
+
message = "Authentication required"
|
|
80
|
+
|
|
81
|
+
class ForbiddenError(AppException):
|
|
82
|
+
status_code = 403
|
|
83
|
+
error_code = "FORBIDDEN"
|
|
84
|
+
message = "Access denied"
|
|
85
|
+
|
|
86
|
+
class ConflictError(AppException):
|
|
87
|
+
status_code = 409
|
|
88
|
+
error_code = "CONFLICT"
|
|
89
|
+
message = "Resource already exists"
|
|
90
|
+
|
|
91
|
+
# Register handler
|
|
92
|
+
@app.errorhandler(AppException)
|
|
93
|
+
def handle_app_exception(error):
|
|
94
|
+
response = jsonify(error.to_dict())
|
|
95
|
+
response.status_code = error.status_code
|
|
96
|
+
return response
|
|
97
|
+
|
|
98
|
+
# Usage
|
|
99
|
+
@users_bp.route("/<int:user_id>")
|
|
100
|
+
def get_user(user_id: int):
|
|
101
|
+
user = User.query.get(user_id)
|
|
102
|
+
if not user:
|
|
103
|
+
raise NotFoundError(f"User {user_id} not found")
|
|
104
|
+
return jsonify(UserSchema().dump(user))
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Marshmallow Validation Errors
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from marshmallow import ValidationError as MarshmallowValidationError
|
|
111
|
+
|
|
112
|
+
@app.errorhandler(MarshmallowValidationError)
|
|
113
|
+
def handle_validation_error(error):
|
|
114
|
+
return jsonify({
|
|
115
|
+
"error": "VALIDATION_ERROR",
|
|
116
|
+
"message": "Input validation failed",
|
|
117
|
+
"details": error.messages,
|
|
118
|
+
}), 400
|
|
119
|
+
|
|
120
|
+
# Usage
|
|
121
|
+
@users_bp.route("/", methods=["POST"])
|
|
122
|
+
def create_user():
|
|
123
|
+
schema = UserCreateSchema()
|
|
124
|
+
data = schema.load(request.get_json()) # Raises ValidationError if invalid
|
|
125
|
+
user = UserService.create(data)
|
|
126
|
+
return jsonify(UserSchema().dump(user)), 201
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## SQLAlchemy Errors
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
|
|
133
|
+
|
|
134
|
+
@app.errorhandler(IntegrityError)
|
|
135
|
+
def handle_integrity_error(error):
|
|
136
|
+
db.session.rollback()
|
|
137
|
+
|
|
138
|
+
# Parse constraint violation
|
|
139
|
+
if "unique constraint" in str(error.orig).lower():
|
|
140
|
+
return jsonify({
|
|
141
|
+
"error": "DUPLICATE_ENTRY",
|
|
142
|
+
"message": "A record with this value already exists",
|
|
143
|
+
}), 409
|
|
144
|
+
|
|
145
|
+
return jsonify({
|
|
146
|
+
"error": "DATABASE_ERROR",
|
|
147
|
+
"message": "Database constraint violation",
|
|
148
|
+
}), 400
|
|
149
|
+
|
|
150
|
+
@app.errorhandler(SQLAlchemyError)
|
|
151
|
+
def handle_db_error(error):
|
|
152
|
+
db.session.rollback()
|
|
153
|
+
app.logger.error(f"Database error: {error}")
|
|
154
|
+
return jsonify({
|
|
155
|
+
"error": "DATABASE_ERROR",
|
|
156
|
+
"message": "A database error occurred",
|
|
157
|
+
}), 500
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Logging Errors
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
import logging
|
|
164
|
+
import traceback
|
|
165
|
+
|
|
166
|
+
@app.errorhandler(Exception)
|
|
167
|
+
def handle_unexpected_error(error):
|
|
168
|
+
# Log full traceback
|
|
169
|
+
app.logger.error(
|
|
170
|
+
"Unhandled exception",
|
|
171
|
+
extra={
|
|
172
|
+
"error": str(error),
|
|
173
|
+
"traceback": traceback.format_exc(),
|
|
174
|
+
"path": request.path,
|
|
175
|
+
"method": request.method,
|
|
176
|
+
"user_id": g.get("user_id"),
|
|
177
|
+
},
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
# Return generic error to client
|
|
181
|
+
if app.debug:
|
|
182
|
+
return jsonify({
|
|
183
|
+
"error": "INTERNAL_ERROR",
|
|
184
|
+
"message": str(error),
|
|
185
|
+
"traceback": traceback.format_exc(),
|
|
186
|
+
}), 500
|
|
187
|
+
|
|
188
|
+
return jsonify({
|
|
189
|
+
"error": "INTERNAL_ERROR",
|
|
190
|
+
"message": "An unexpected error occurred",
|
|
191
|
+
}), 500
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Blueprint Error Handlers
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
users_bp = Blueprint("users", __name__)
|
|
198
|
+
|
|
199
|
+
# Only handles errors from this blueprint
|
|
200
|
+
@users_bp.errorhandler(404)
|
|
201
|
+
def user_not_found(error):
|
|
202
|
+
return jsonify({
|
|
203
|
+
"error": "USER_NOT_FOUND",
|
|
204
|
+
"message": "The requested user was not found",
|
|
205
|
+
}), 404
|
|
206
|
+
|
|
207
|
+
# App-level handler is fallback
|
|
208
|
+
@app.errorhandler(404)
|
|
209
|
+
def generic_not_found(error):
|
|
210
|
+
return jsonify({
|
|
211
|
+
"error": "NOT_FOUND",
|
|
212
|
+
"message": "Resource not found",
|
|
213
|
+
}), 404
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Error Response Format
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
from dataclasses import dataclass
|
|
220
|
+
from typing import Any
|
|
221
|
+
|
|
222
|
+
@dataclass
|
|
223
|
+
class ErrorResponse:
|
|
224
|
+
error: str
|
|
225
|
+
message: str
|
|
226
|
+
status_code: int
|
|
227
|
+
details: dict[str, Any] | None = None
|
|
228
|
+
request_id: str | None = None
|
|
229
|
+
|
|
230
|
+
def to_dict(self) -> dict:
|
|
231
|
+
data = {
|
|
232
|
+
"error": self.error,
|
|
233
|
+
"message": self.message,
|
|
234
|
+
}
|
|
235
|
+
if self.details:
|
|
236
|
+
data["details"] = self.details
|
|
237
|
+
if self.request_id:
|
|
238
|
+
data["request_id"] = self.request_id
|
|
239
|
+
return data
|
|
240
|
+
|
|
241
|
+
def error_response(
|
|
242
|
+
error: str,
|
|
243
|
+
message: str,
|
|
244
|
+
status_code: int,
|
|
245
|
+
details: dict = None,
|
|
246
|
+
) -> tuple:
|
|
247
|
+
response = ErrorResponse(
|
|
248
|
+
error=error,
|
|
249
|
+
message=message,
|
|
250
|
+
status_code=status_code,
|
|
251
|
+
details=details,
|
|
252
|
+
request_id=g.get("request_id"),
|
|
253
|
+
)
|
|
254
|
+
return jsonify(response.to_dict()), status_code
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## Abort with Custom Response
|
|
258
|
+
|
|
259
|
+
```python
|
|
260
|
+
from flask import abort
|
|
261
|
+
|
|
262
|
+
@users_bp.route("/<int:user_id>")
|
|
263
|
+
def get_user(user_id: int):
|
|
264
|
+
user = User.query.get(user_id)
|
|
265
|
+
if not user:
|
|
266
|
+
abort(404, description="User not found")
|
|
267
|
+
return jsonify(UserSchema().dump(user))
|
|
268
|
+
|
|
269
|
+
# Or with custom response
|
|
270
|
+
from werkzeug.exceptions import NotFound
|
|
271
|
+
|
|
272
|
+
@users_bp.route("/<int:user_id>")
|
|
273
|
+
def get_user(user_id: int):
|
|
274
|
+
user = User.query.get(user_id)
|
|
275
|
+
if not user:
|
|
276
|
+
raise NotFound(f"User with ID {user_id} not found")
|
|
277
|
+
return jsonify(UserSchema().dump(user))
|
|
278
|
+
```
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "**/*.py"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Flask Extensions Patterns
|
|
7
|
+
|
|
8
|
+
## Extension Initialization
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
# GOOD - Lazy initialization (extensions.py)
|
|
12
|
+
from flask_sqlalchemy import SQLAlchemy
|
|
13
|
+
from flask_migrate import Migrate
|
|
14
|
+
from flask_jwt_extended import JWTManager
|
|
15
|
+
from flask_cors import CORS
|
|
16
|
+
from flask_mail import Mail
|
|
17
|
+
|
|
18
|
+
db = SQLAlchemy()
|
|
19
|
+
migrate = Migrate()
|
|
20
|
+
jwt = JWTManager()
|
|
21
|
+
cors = CORS()
|
|
22
|
+
mail = Mail()
|
|
23
|
+
|
|
24
|
+
# In factory (app/__init__.py)
|
|
25
|
+
def create_app(config_name: str = "development") -> Flask:
|
|
26
|
+
app = Flask(__name__)
|
|
27
|
+
app.config.from_object(config[config_name])
|
|
28
|
+
|
|
29
|
+
# Initialize extensions
|
|
30
|
+
db.init_app(app)
|
|
31
|
+
migrate.init_app(app, db)
|
|
32
|
+
jwt.init_app(app)
|
|
33
|
+
cors.init_app(app)
|
|
34
|
+
mail.init_app(app)
|
|
35
|
+
|
|
36
|
+
return app
|
|
37
|
+
|
|
38
|
+
# BAD - Eager initialization
|
|
39
|
+
from flask import Flask
|
|
40
|
+
from flask_sqlalchemy import SQLAlchemy
|
|
41
|
+
|
|
42
|
+
app = Flask(__name__)
|
|
43
|
+
db = SQLAlchemy(app) # Can't use different configs!
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Flask-SQLAlchemy
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from app.extensions import db
|
|
50
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
51
|
+
|
|
52
|
+
class User(db.Model):
|
|
53
|
+
__tablename__ = "users"
|
|
54
|
+
|
|
55
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
56
|
+
email: Mapped[str] = mapped_column(unique=True, index=True)
|
|
57
|
+
name: Mapped[str] = mapped_column(db.String(100))
|
|
58
|
+
is_active: Mapped[bool] = mapped_column(default=True)
|
|
59
|
+
|
|
60
|
+
# Relationships
|
|
61
|
+
orders: Mapped[list["Order"]] = db.relationship(back_populates="user")
|
|
62
|
+
|
|
63
|
+
# Usage in routes
|
|
64
|
+
@users_bp.route("/")
|
|
65
|
+
def list_users():
|
|
66
|
+
users = db.session.scalars(db.select(User).where(User.is_active)).all()
|
|
67
|
+
return jsonify(UserSchema(many=True).dump(users))
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Flask-Migrate
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
# migrations/env.py is auto-generated
|
|
74
|
+
# Commands:
|
|
75
|
+
# flask db init - Initialize migrations
|
|
76
|
+
# flask db migrate -m "Add users table"
|
|
77
|
+
# flask db upgrade - Apply migrations
|
|
78
|
+
# flask db downgrade - Rollback last migration
|
|
79
|
+
# flask db current - Show current revision
|
|
80
|
+
# flask db history - Show migration history
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Flask-JWT-Extended
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from flask_jwt_extended import (
|
|
87
|
+
create_access_token,
|
|
88
|
+
create_refresh_token,
|
|
89
|
+
jwt_required,
|
|
90
|
+
current_user,
|
|
91
|
+
get_jwt_identity,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Configure in factory
|
|
95
|
+
app.config["JWT_SECRET_KEY"] = os.environ["JWT_SECRET_KEY"]
|
|
96
|
+
app.config["JWT_ACCESS_TOKEN_EXPIRES"] = timedelta(hours=1)
|
|
97
|
+
app.config["JWT_REFRESH_TOKEN_EXPIRES"] = timedelta(days=30)
|
|
98
|
+
|
|
99
|
+
# User loader callback
|
|
100
|
+
@jwt.user_lookup_loader
|
|
101
|
+
def user_lookup_callback(_jwt_header, jwt_data):
|
|
102
|
+
identity = jwt_data["sub"]
|
|
103
|
+
return User.query.get(identity)
|
|
104
|
+
|
|
105
|
+
# Login endpoint
|
|
106
|
+
@auth_bp.route("/login", methods=["POST"])
|
|
107
|
+
def login():
|
|
108
|
+
data = LoginSchema().load(request.get_json())
|
|
109
|
+
user = User.query.filter_by(email=data["email"]).first()
|
|
110
|
+
|
|
111
|
+
if not user or not user.check_password(data["password"]):
|
|
112
|
+
return jsonify({"error": "Invalid credentials"}), 401
|
|
113
|
+
|
|
114
|
+
access_token = create_access_token(identity=user.id)
|
|
115
|
+
refresh_token = create_refresh_token(identity=user.id)
|
|
116
|
+
|
|
117
|
+
return jsonify({
|
|
118
|
+
"access_token": access_token,
|
|
119
|
+
"refresh_token": refresh_token,
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
# Protected endpoint
|
|
123
|
+
@users_bp.route("/me")
|
|
124
|
+
@jwt_required()
|
|
125
|
+
def get_current_user():
|
|
126
|
+
return jsonify(UserSchema().dump(current_user))
|
|
127
|
+
|
|
128
|
+
# Refresh token
|
|
129
|
+
@auth_bp.route("/refresh", methods=["POST"])
|
|
130
|
+
@jwt_required(refresh=True)
|
|
131
|
+
def refresh():
|
|
132
|
+
identity = get_jwt_identity()
|
|
133
|
+
access_token = create_access_token(identity=identity)
|
|
134
|
+
return jsonify({"access_token": access_token})
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Flask-CORS
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
from flask_cors import CORS
|
|
141
|
+
|
|
142
|
+
# Global CORS
|
|
143
|
+
cors.init_app(app, resources={
|
|
144
|
+
r"/api/*": {
|
|
145
|
+
"origins": ["https://example.com"],
|
|
146
|
+
"methods": ["GET", "POST", "PUT", "DELETE"],
|
|
147
|
+
"allow_headers": ["Authorization", "Content-Type"],
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
# Blueprint-specific CORS
|
|
152
|
+
CORS(users_bp, origins=["https://admin.example.com"])
|
|
153
|
+
|
|
154
|
+
# Route-specific
|
|
155
|
+
from flask_cors import cross_origin
|
|
156
|
+
|
|
157
|
+
@app.route("/public")
|
|
158
|
+
@cross_origin(origins="*")
|
|
159
|
+
def public_endpoint():
|
|
160
|
+
return jsonify({"public": True})
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Flask-Mail
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
from flask_mail import Message
|
|
167
|
+
from app.extensions import mail
|
|
168
|
+
|
|
169
|
+
def send_welcome_email(user: User):
|
|
170
|
+
msg = Message(
|
|
171
|
+
subject="Welcome!",
|
|
172
|
+
sender="noreply@example.com",
|
|
173
|
+
recipients=[user.email],
|
|
174
|
+
)
|
|
175
|
+
msg.body = f"Hello {user.name}, welcome to our platform!"
|
|
176
|
+
msg.html = render_template("emails/welcome.html", user=user)
|
|
177
|
+
|
|
178
|
+
mail.send(msg)
|
|
179
|
+
|
|
180
|
+
# Async email sending
|
|
181
|
+
from threading import Thread
|
|
182
|
+
|
|
183
|
+
def send_async_email(app, msg):
|
|
184
|
+
with app.app_context():
|
|
185
|
+
mail.send(msg)
|
|
186
|
+
|
|
187
|
+
def send_email_async(msg):
|
|
188
|
+
Thread(target=send_async_email, args=(current_app._get_current_object(), msg)).start()
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Flask-Caching
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
from flask_caching import Cache
|
|
195
|
+
|
|
196
|
+
cache = Cache()
|
|
197
|
+
|
|
198
|
+
# Configuration
|
|
199
|
+
app.config["CACHE_TYPE"] = "redis"
|
|
200
|
+
app.config["CACHE_REDIS_URL"] = os.environ["REDIS_URL"]
|
|
201
|
+
app.config["CACHE_DEFAULT_TIMEOUT"] = 300
|
|
202
|
+
|
|
203
|
+
cache.init_app(app)
|
|
204
|
+
|
|
205
|
+
# Usage
|
|
206
|
+
@users_bp.route("/<int:user_id>")
|
|
207
|
+
@cache.cached(timeout=60, key_prefix="user")
|
|
208
|
+
def get_user(user_id: int):
|
|
209
|
+
user = User.query.get_or_404(user_id)
|
|
210
|
+
return jsonify(UserSchema().dump(user))
|
|
211
|
+
|
|
212
|
+
# Memoize (cache with arguments)
|
|
213
|
+
@cache.memoize(timeout=300)
|
|
214
|
+
def get_user_stats(user_id: int) -> dict:
|
|
215
|
+
return calculate_stats(user_id)
|
|
216
|
+
|
|
217
|
+
# Clear cache
|
|
218
|
+
cache.delete("user")
|
|
219
|
+
cache.delete_memoized(get_user_stats, user_id)
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Flask-Limiter
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
from flask_limiter import Limiter
|
|
226
|
+
from flask_limiter.util import get_remote_address
|
|
227
|
+
|
|
228
|
+
limiter = Limiter(
|
|
229
|
+
key_func=get_remote_address,
|
|
230
|
+
default_limits=["100 per hour"],
|
|
231
|
+
storage_uri="redis://localhost:6379",
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
limiter.init_app(app)
|
|
235
|
+
|
|
236
|
+
# Route-specific limits
|
|
237
|
+
@auth_bp.route("/login", methods=["POST"])
|
|
238
|
+
@limiter.limit("5 per minute")
|
|
239
|
+
def login():
|
|
240
|
+
...
|
|
241
|
+
|
|
242
|
+
# Blueprint limits
|
|
243
|
+
limiter.limit("50 per hour")(users_bp)
|
|
244
|
+
|
|
245
|
+
# Exempt from limits
|
|
246
|
+
@app.route("/health")
|
|
247
|
+
@limiter.exempt
|
|
248
|
+
def health():
|
|
249
|
+
return jsonify({"status": "ok"})
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## Flask-Login (Session-Based)
|
|
253
|
+
|
|
254
|
+
```python
|
|
255
|
+
from flask_login import LoginManager, login_user, logout_user, login_required, current_user
|
|
256
|
+
|
|
257
|
+
login_manager = LoginManager()
|
|
258
|
+
login_manager.login_view = "auth.login"
|
|
259
|
+
|
|
260
|
+
@login_manager.user_loader
|
|
261
|
+
def load_user(user_id):
|
|
262
|
+
return User.query.get(int(user_id))
|
|
263
|
+
|
|
264
|
+
# Login
|
|
265
|
+
@auth_bp.route("/login", methods=["POST"])
|
|
266
|
+
def login():
|
|
267
|
+
user = authenticate(request.form["email"], request.form["password"])
|
|
268
|
+
if user:
|
|
269
|
+
login_user(user, remember=True)
|
|
270
|
+
return redirect(url_for("main.index"))
|
|
271
|
+
return render_template("login.html", error="Invalid credentials")
|
|
272
|
+
|
|
273
|
+
# Protected route
|
|
274
|
+
@main_bp.route("/dashboard")
|
|
275
|
+
@login_required
|
|
276
|
+
def dashboard():
|
|
277
|
+
return render_template("dashboard.html", user=current_user)
|
|
278
|
+
```
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# Flask Rules
|
|
2
|
+
|
|
3
|
+
## Activation
|
|
4
|
+
|
|
5
|
+
```yaml
|
|
6
|
+
paths:
|
|
7
|
+
- "**/*.py"
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Application Factory
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
# GOOD - Application factory pattern
|
|
14
|
+
def create_app(config_name: str = "development") -> Flask:
|
|
15
|
+
app = Flask(__name__)
|
|
16
|
+
app.config.from_object(config[config_name])
|
|
17
|
+
|
|
18
|
+
db.init_app(app)
|
|
19
|
+
register_blueprints(app)
|
|
20
|
+
|
|
21
|
+
return app
|
|
22
|
+
|
|
23
|
+
# BAD - Global app instance
|
|
24
|
+
app = Flask(__name__) # Hard to test, can't have multiple configs
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Blueprints
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
# GOOD - Organized blueprints
|
|
31
|
+
from flask import Blueprint
|
|
32
|
+
|
|
33
|
+
users_bp = Blueprint("users", __name__)
|
|
34
|
+
|
|
35
|
+
@users_bp.route("/", methods=["GET"])
|
|
36
|
+
def list_users():
|
|
37
|
+
...
|
|
38
|
+
|
|
39
|
+
# Register in factory
|
|
40
|
+
app.register_blueprint(users_bp, url_prefix="/api/v1/users")
|
|
41
|
+
|
|
42
|
+
# BAD - All routes in one file
|
|
43
|
+
@app.route("/api/v1/users")
|
|
44
|
+
@app.route("/api/v1/posts")
|
|
45
|
+
@app.route("/api/v1/comments") # Messy, hard to maintain
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Request Handling
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
# GOOD - Validate input with schemas
|
|
52
|
+
from marshmallow import ValidationError
|
|
53
|
+
|
|
54
|
+
@users_bp.route("/", methods=["POST"])
|
|
55
|
+
def create_user():
|
|
56
|
+
try:
|
|
57
|
+
data = UserCreateSchema().load(request.get_json())
|
|
58
|
+
except ValidationError as e:
|
|
59
|
+
return jsonify({"errors": e.messages}), 400
|
|
60
|
+
|
|
61
|
+
user = user_service.create(data)
|
|
62
|
+
return jsonify(UserResponseSchema().dump(user)), 201
|
|
63
|
+
|
|
64
|
+
# BAD - Direct access without validation
|
|
65
|
+
@users_bp.route("/", methods=["POST"])
|
|
66
|
+
def create_user():
|
|
67
|
+
data = request.get_json()
|
|
68
|
+
user = User(email=data["email"]) # No validation!
|
|
69
|
+
db.session.add(user)
|
|
70
|
+
db.session.commit()
|
|
71
|
+
return jsonify(user.to_dict())
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Context Management
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
# GOOD - Use application context
|
|
78
|
+
with app.app_context():
|
|
79
|
+
db.create_all()
|
|
80
|
+
|
|
81
|
+
# GOOD - Use test request context
|
|
82
|
+
with app.test_request_context():
|
|
83
|
+
url = url_for("users.get_user", user_id=1)
|
|
84
|
+
|
|
85
|
+
# BAD - Access outside context
|
|
86
|
+
db.create_all() # RuntimeError: No application context
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Configuration
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
# GOOD - Class-based config
|
|
93
|
+
class Config:
|
|
94
|
+
SECRET_KEY = os.environ.get("SECRET_KEY", "dev-key")
|
|
95
|
+
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
|
96
|
+
|
|
97
|
+
class DevelopmentConfig(Config):
|
|
98
|
+
DEBUG = True
|
|
99
|
+
SQLALCHEMY_DATABASE_URI = "sqlite:///dev.db"
|
|
100
|
+
|
|
101
|
+
class ProductionConfig(Config):
|
|
102
|
+
DEBUG = False
|
|
103
|
+
SQLALCHEMY_DATABASE_URI = os.environ["DATABASE_URL"]
|
|
104
|
+
|
|
105
|
+
config = {
|
|
106
|
+
"development": DevelopmentConfig,
|
|
107
|
+
"production": ProductionConfig,
|
|
108
|
+
"testing": TestingConfig,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
# BAD - Hardcoded config
|
|
112
|
+
app.config["SECRET_KEY"] = "hardcoded-secret" # Never do this
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Testing
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
# GOOD - Test client fixture
|
|
119
|
+
import pytest
|
|
120
|
+
|
|
121
|
+
@pytest.fixture
|
|
122
|
+
def app():
|
|
123
|
+
app = create_app("testing")
|
|
124
|
+
return app
|
|
125
|
+
|
|
126
|
+
@pytest.fixture
|
|
127
|
+
def client(app):
|
|
128
|
+
return app.test_client()
|
|
129
|
+
|
|
130
|
+
@pytest.fixture
|
|
131
|
+
def db_session(app):
|
|
132
|
+
with app.app_context():
|
|
133
|
+
db.create_all()
|
|
134
|
+
yield db.session
|
|
135
|
+
db.drop_all()
|
|
136
|
+
|
|
137
|
+
def test_create_user(client):
|
|
138
|
+
response = client.post("/api/v1/users", json={
|
|
139
|
+
"email": "test@example.com",
|
|
140
|
+
"password": "password123",
|
|
141
|
+
"name": "Test User",
|
|
142
|
+
})
|
|
143
|
+
assert response.status_code == 201
|
|
144
|
+
assert response.json["email"] == "test@example.com"
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Extensions Pattern
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
# GOOD - Lazy initialization
|
|
151
|
+
# extensions.py
|
|
152
|
+
from flask_sqlalchemy import SQLAlchemy
|
|
153
|
+
from flask_migrate import Migrate
|
|
154
|
+
|
|
155
|
+
db = SQLAlchemy()
|
|
156
|
+
migrate = Migrate()
|
|
157
|
+
|
|
158
|
+
# __init__.py
|
|
159
|
+
def create_app():
|
|
160
|
+
app = Flask(__name__)
|
|
161
|
+
db.init_app(app)
|
|
162
|
+
migrate.init_app(app, db)
|
|
163
|
+
return app
|
|
164
|
+
|
|
165
|
+
# BAD - Eager initialization
|
|
166
|
+
from flask import Flask
|
|
167
|
+
from flask_sqlalchemy import SQLAlchemy
|
|
168
|
+
|
|
169
|
+
app = Flask(__name__)
|
|
170
|
+
db = SQLAlchemy(app) # Can't use different configs
|
|
171
|
+
```
|