@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,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
+ ```
@@ -0,0 +1,206 @@
1
+ # Marshmallow Validation Rules
2
+
3
+ ## Activation
4
+
5
+ ```yaml
6
+ paths:
7
+ - "**/*.py"
8
+ ```
9
+
10
+ ## Schema Definition
11
+
12
+ ```python
13
+ # GOOD - Clear schema with validation
14
+ from marshmallow import Schema, fields, validate, validates, ValidationError, post_load
15
+
16
+ class UserCreateSchema(Schema):
17
+ email = fields.Email(required=True)
18
+ password = fields.Str(required=True, validate=validate.Length(min=8))
19
+ name = fields.Str(required=True, validate=validate.Length(min=1, max=100))
20
+ age = fields.Int(validate=validate.Range(min=0, max=150))
21
+
22
+ class UserResponseSchema(Schema):
23
+ id = fields.Int(dump_only=True)
24
+ email = fields.Email()
25
+ name = fields.Str()
26
+ created_at = fields.DateTime(dump_only=True)
27
+
28
+ # BAD - No validation
29
+ class UserSchema(Schema):
30
+ email = fields.Str() # Should be fields.Email
31
+ password = fields.Str() # No length validation
32
+ ```
33
+
34
+ ## Custom Validation
35
+
36
+ ```python
37
+ # GOOD - Field-level validation
38
+ class UserSchema(Schema):
39
+ username = fields.Str(required=True)
40
+
41
+ @validates("username")
42
+ def validate_username(self, value):
43
+ if not value.isalnum():
44
+ raise ValidationError("Username must be alphanumeric")
45
+ if len(value) < 3:
46
+ raise ValidationError("Username must be at least 3 characters")
47
+
48
+ # GOOD - Cross-field validation
49
+ from marshmallow import validates_schema
50
+
51
+ class PasswordChangeSchema(Schema):
52
+ password = fields.Str(required=True)
53
+ password_confirm = fields.Str(required=True)
54
+
55
+ @validates_schema
56
+ def validate_passwords_match(self, data, **kwargs):
57
+ if data.get("password") != data.get("password_confirm"):
58
+ raise ValidationError("Passwords must match", field_name="password_confirm")
59
+ ```
60
+
61
+ ## Load/Dump Hooks
62
+
63
+ ```python
64
+ from marshmallow import pre_load, post_load, post_dump
65
+
66
+ class UserSchema(Schema):
67
+ email = fields.Email(required=True)
68
+ name = fields.Str(required=True)
69
+
70
+ @pre_load
71
+ def normalize_email(self, data, **kwargs):
72
+ if "email" in data:
73
+ data["email"] = data["email"].lower().strip()
74
+ return data
75
+
76
+ @post_load
77
+ def make_user(self, data, **kwargs):
78
+ return User(**data)
79
+
80
+ @post_dump
81
+ def remove_nulls(self, data, **kwargs):
82
+ return {k: v for k, v in data.items() if v is not None}
83
+ ```
84
+
85
+ ## Nested Schemas
86
+
87
+ ```python
88
+ # GOOD - Nested relationships
89
+ class AddressSchema(Schema):
90
+ street = fields.Str(required=True)
91
+ city = fields.Str(required=True)
92
+ country = fields.Str(required=True)
93
+
94
+ class UserSchema(Schema):
95
+ name = fields.Str(required=True)
96
+ address = fields.Nested(AddressSchema)
97
+ addresses = fields.List(fields.Nested(AddressSchema))
98
+
99
+ # GOOD - Self-referential (e.g., comments with replies)
100
+ class CommentSchema(Schema):
101
+ id = fields.Int(dump_only=True)
102
+ text = fields.Str(required=True)
103
+ replies = fields.List(fields.Nested("self", exclude=("replies",)))
104
+ ```
105
+
106
+ ## Partial Loading
107
+
108
+ ```python
109
+ # GOOD - Partial updates
110
+ class UserUpdateSchema(Schema):
111
+ email = fields.Email()
112
+ name = fields.Str(validate=validate.Length(min=1, max=100))
113
+
114
+ @users_bp.route("/<int:user_id>", methods=["PATCH"])
115
+ def update_user(user_id: int):
116
+ schema = UserUpdateSchema()
117
+ data = schema.load(request.get_json(), partial=True)
118
+ user = UserService.update(user_id, data)
119
+ return jsonify(UserResponseSchema().dump(user))
120
+ ```
121
+
122
+ ## Schema Inheritance
123
+
124
+ ```python
125
+ # GOOD - Base schema with common fields
126
+ class BaseSchema(Schema):
127
+ class Meta:
128
+ strict = True
129
+
130
+ class TimestampMixin:
131
+ created_at = fields.DateTime(dump_only=True)
132
+ updated_at = fields.DateTime(dump_only=True)
133
+
134
+ class UserSchema(BaseSchema, TimestampMixin):
135
+ id = fields.Int(dump_only=True)
136
+ email = fields.Email(required=True)
137
+ name = fields.Str(required=True)
138
+ ```
139
+
140
+ ## Error Handling
141
+
142
+ ```python
143
+ # GOOD - Centralized error handling
144
+ from marshmallow import ValidationError
145
+ from flask import jsonify
146
+
147
+ @app.errorhandler(ValidationError)
148
+ def handle_validation_error(error):
149
+ return jsonify({
150
+ "error": "Validation Error",
151
+ "details": error.messages,
152
+ }), 400
153
+
154
+ # In routes
155
+ @users_bp.route("/", methods=["POST"])
156
+ def create_user():
157
+ schema = UserCreateSchema()
158
+ try:
159
+ data = schema.load(request.get_json())
160
+ except ValidationError as e:
161
+ return jsonify({"errors": e.messages}), 400
162
+
163
+ user = UserService.create(data)
164
+ return jsonify(UserResponseSchema().dump(user)), 201
165
+ ```
166
+
167
+ ## SQLAlchemy Integration
168
+
169
+ ```python
170
+ # GOOD - With marshmallow-sqlalchemy
171
+ from marshmallow_sqlalchemy import SQLAlchemyAutoSchema
172
+
173
+ class UserSchema(SQLAlchemyAutoSchema):
174
+ class Meta:
175
+ model = User
176
+ include_relationships = True
177
+ load_instance = True
178
+ exclude = ("hashed_password",)
179
+
180
+ # Manual approach (more control)
181
+ class UserSchema(Schema):
182
+ class Meta:
183
+ load_instance = True
184
+
185
+ id = fields.Int(dump_only=True)
186
+ email = fields.Email(required=True)
187
+
188
+ @post_load
189
+ def make_user(self, data, **kwargs):
190
+ return User(**data)
191
+ ```
192
+
193
+ ## Many Parameter
194
+
195
+ ```python
196
+ # GOOD - Serialize/deserialize collections
197
+ schema = UserSchema()
198
+
199
+ # Single object
200
+ user_data = schema.dump(user)
201
+ user = schema.load(data)
202
+
203
+ # Multiple objects
204
+ users_data = schema.dump(users, many=True)
205
+ users = schema.load(data_list, many=True)
206
+ ```