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