@malamute/ai-rules 1.0.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +272 -121
- package/bin/cli.js +5 -2
- package/configs/_shared/CLAUDE.md +52 -149
- package/configs/_shared/rules/conventions/documentation.md +324 -0
- package/configs/_shared/rules/conventions/git.md +265 -0
- package/configs/_shared/rules/conventions/npm.md +80 -0
- package/configs/_shared/{.claude/rules → rules/conventions}/performance.md +1 -1
- package/configs/_shared/rules/conventions/principles.md +334 -0
- package/configs/_shared/rules/devops/ci-cd.md +262 -0
- package/configs/_shared/rules/devops/docker.md +275 -0
- package/configs/_shared/rules/devops/nx.md +194 -0
- package/configs/_shared/rules/domain/backend/api-design.md +203 -0
- package/configs/_shared/rules/lang/csharp/async.md +220 -0
- package/configs/_shared/rules/lang/csharp/csharp.md +314 -0
- package/configs/_shared/rules/lang/csharp/linq.md +210 -0
- package/configs/_shared/rules/lang/python/async.md +337 -0
- package/configs/_shared/rules/lang/python/celery.md +476 -0
- package/configs/_shared/rules/lang/python/config.md +339 -0
- package/configs/{python/.claude/rules → _shared/rules/lang/python}/database/sqlalchemy.md +6 -1
- package/configs/_shared/rules/lang/python/deployment.md +523 -0
- package/configs/_shared/rules/lang/python/error-handling.md +330 -0
- package/configs/_shared/rules/lang/python/migrations.md +421 -0
- package/configs/_shared/rules/lang/python/python.md +172 -0
- package/configs/_shared/rules/lang/python/repository.md +383 -0
- package/configs/{python/.claude/rules → _shared/rules/lang/python}/testing.md +2 -69
- package/configs/_shared/rules/lang/typescript/async.md +447 -0
- package/configs/_shared/rules/lang/typescript/generics.md +356 -0
- package/configs/_shared/rules/lang/typescript/typescript.md +212 -0
- package/configs/_shared/rules/quality/error-handling.md +48 -0
- package/configs/_shared/rules/quality/logging.md +45 -0
- package/configs/_shared/rules/quality/observability.md +240 -0
- package/configs/_shared/rules/quality/testing-patterns.md +65 -0
- package/configs/_shared/rules/security/secrets-management.md +222 -0
- package/configs/_shared/skills/analysis/explore/SKILL.md +257 -0
- package/configs/_shared/skills/analysis/security-audit/SKILL.md +184 -0
- package/configs/_shared/skills/dev/api-endpoint/SKILL.md +126 -0
- package/configs/_shared/{.claude/commands/generate-tests.md → skills/dev/generate-tests/SKILL.md} +6 -0
- package/configs/_shared/{.claude/commands/fix-issue.md → skills/git/fix-issue/SKILL.md} +6 -0
- package/configs/_shared/{.claude/commands/review-pr.md → skills/git/review-pr/SKILL.md} +6 -0
- package/configs/_shared/skills/infra/deploy/SKILL.md +139 -0
- package/configs/_shared/skills/infra/docker/SKILL.md +95 -0
- package/configs/_shared/skills/infra/migration/SKILL.md +158 -0
- package/configs/_shared/skills/nx/nx-affected/SKILL.md +72 -0
- package/configs/_shared/skills/nx/nx-lib/SKILL.md +375 -0
- package/configs/angular/CLAUDE.md +24 -216
- package/configs/angular/{.claude/rules → rules/core}/components.md +69 -15
- package/configs/angular/rules/core/resource.md +285 -0
- package/configs/angular/rules/core/signals.md +323 -0
- package/configs/angular/rules/http.md +338 -0
- package/configs/angular/rules/routing.md +291 -0
- package/configs/angular/rules/ssr.md +312 -0
- package/configs/angular/rules/state/signal-store.md +408 -0
- package/configs/angular/{.claude/rules → rules/state}/state.md +2 -2
- package/configs/angular/{.claude/rules → rules}/testing.md +7 -7
- package/configs/angular/rules/ui/aria.md +422 -0
- package/configs/angular/rules/ui/forms.md +424 -0
- package/configs/angular/rules/ui/pipes-directives.md +335 -0
- package/configs/angular/{.claude/settings.json → settings.json} +3 -0
- package/configs/dotnet/CLAUDE.md +53 -286
- package/configs/dotnet/rules/background-services.md +552 -0
- package/configs/dotnet/rules/configuration.md +426 -0
- package/configs/dotnet/rules/ddd.md +447 -0
- package/configs/dotnet/rules/dependency-injection.md +343 -0
- package/configs/dotnet/rules/mediatr.md +320 -0
- package/configs/dotnet/rules/middleware.md +489 -0
- package/configs/dotnet/rules/result-pattern.md +363 -0
- package/configs/dotnet/rules/validation.md +388 -0
- package/configs/dotnet/settings.json +29 -0
- package/configs/fastapi/CLAUDE.md +144 -0
- package/configs/fastapi/rules/background-tasks.md +254 -0
- package/configs/fastapi/rules/dependencies.md +170 -0
- package/configs/{python/.claude → fastapi}/rules/fastapi.md +61 -1
- package/configs/fastapi/rules/lifespan.md +274 -0
- package/configs/fastapi/rules/middleware.md +229 -0
- package/configs/fastapi/rules/pydantic.md +433 -0
- package/configs/fastapi/rules/responses.md +251 -0
- package/configs/fastapi/rules/routers.md +202 -0
- package/configs/fastapi/rules/security.md +222 -0
- package/configs/fastapi/rules/testing.md +251 -0
- package/configs/fastapi/rules/websockets.md +298 -0
- package/configs/fastapi/settings.json +35 -0
- package/configs/flask/CLAUDE.md +166 -0
- package/configs/flask/rules/blueprints.md +208 -0
- package/configs/flask/rules/cli.md +285 -0
- package/configs/flask/rules/configuration.md +281 -0
- package/configs/flask/rules/context.md +238 -0
- package/configs/flask/rules/error-handlers.md +278 -0
- package/configs/flask/rules/extensions.md +278 -0
- package/configs/flask/rules/flask.md +171 -0
- package/configs/flask/rules/marshmallow.md +206 -0
- package/configs/flask/rules/security.md +267 -0
- package/configs/flask/rules/testing.md +284 -0
- package/configs/flask/settings.json +35 -0
- package/configs/nestjs/CLAUDE.md +57 -215
- package/configs/nestjs/rules/common-patterns.md +300 -0
- package/configs/nestjs/rules/filters.md +376 -0
- package/configs/nestjs/rules/interceptors.md +317 -0
- package/configs/nestjs/rules/middleware.md +321 -0
- package/configs/nestjs/{.claude/rules → rules}/modules.md +26 -0
- package/configs/nestjs/rules/pipes.md +351 -0
- package/configs/nestjs/rules/websockets.md +451 -0
- package/configs/nestjs/settings.json +31 -0
- package/configs/nextjs/CLAUDE.md +69 -331
- package/configs/nextjs/rules/api-routes.md +358 -0
- package/configs/nextjs/rules/authentication.md +355 -0
- package/configs/nextjs/{.claude/rules → rules}/components.md +52 -0
- package/configs/nextjs/rules/data-fetching.md +249 -0
- package/configs/nextjs/rules/database.md +400 -0
- package/configs/nextjs/rules/middleware.md +303 -0
- package/configs/nextjs/rules/routing.md +324 -0
- package/configs/nextjs/rules/seo.md +350 -0
- package/configs/nextjs/rules/server-actions.md +353 -0
- package/configs/nextjs/{.claude/rules → rules}/state/zustand.md +6 -6
- package/configs/nextjs/{.claude/settings.json → settings.json} +7 -0
- package/package.json +24 -9
- package/src/cli.js +218 -0
- package/src/config.js +63 -0
- package/src/index.js +4 -0
- package/src/installer.js +414 -0
- package/src/merge.js +109 -0
- package/src/tech-config.json +45 -0
- package/src/utils.js +88 -0
- package/configs/dotnet/.claude/settings.json +0 -9
- package/configs/nestjs/.claude/settings.json +0 -15
- package/configs/python/.claude/rules/flask.md +0 -332
- package/configs/python/.claude/settings.json +0 -18
- package/configs/python/CLAUDE.md +0 -273
- package/src/install.js +0 -315
- /package/configs/_shared/{.claude/rules → rules/domain/frontend}/accessibility.md +0 -0
- /package/configs/_shared/{.claude/rules → rules/security}/security.md +0 -0
- /package/configs/_shared/{.claude/skills → skills/dev}/debug/SKILL.md +0 -0
- /package/configs/_shared/{.claude/skills → skills/dev}/learning/SKILL.md +0 -0
- /package/configs/_shared/{.claude/skills → skills/dev}/spec/SKILL.md +0 -0
- /package/configs/_shared/{.claude/skills → skills/git}/review/SKILL.md +0 -0
- /package/configs/dotnet/{.claude/rules → rules}/api.md +0 -0
- /package/configs/dotnet/{.claude/rules → rules}/architecture.md +0 -0
- /package/configs/dotnet/{.claude/rules → rules}/database/efcore.md +0 -0
- /package/configs/dotnet/{.claude/rules → rules}/testing.md +0 -0
- /package/configs/nestjs/{.claude/rules → rules}/auth.md +0 -0
- /package/configs/nestjs/{.claude/rules → rules}/database/prisma.md +0 -0
- /package/configs/nestjs/{.claude/rules → rules}/database/typeorm.md +0 -0
- /package/configs/nestjs/{.claude/rules → rules}/testing.md +0 -0
- /package/configs/nestjs/{.claude/rules → rules}/validation.md +0 -0
- /package/configs/nextjs/{.claude/rules → rules}/state/redux-toolkit.md +0 -0
- /package/configs/nextjs/{.claude/rules → rules}/testing.md +0 -0
|
@@ -0,0 +1,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
|
+
```
|