@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,238 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "**/*.py"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Flask Context Management
|
|
7
|
+
|
|
8
|
+
## Application Context
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
from flask import current_app, g
|
|
12
|
+
|
|
13
|
+
# GOOD - use application context
|
|
14
|
+
with app.app_context():
|
|
15
|
+
db.create_all()
|
|
16
|
+
current_app.logger.info("Database initialized")
|
|
17
|
+
|
|
18
|
+
# In CLI commands
|
|
19
|
+
@app.cli.command("init-db")
|
|
20
|
+
def init_db():
|
|
21
|
+
# Already in app context
|
|
22
|
+
db.create_all()
|
|
23
|
+
click.echo("Database initialized")
|
|
24
|
+
|
|
25
|
+
# BAD - access outside context
|
|
26
|
+
db.create_all() # RuntimeError: Working outside of application context
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Request Context
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from flask import request, session, g
|
|
33
|
+
|
|
34
|
+
# Automatically pushed during requests
|
|
35
|
+
@app.route("/")
|
|
36
|
+
def index():
|
|
37
|
+
user_agent = request.headers.get("User-Agent")
|
|
38
|
+
user_id = session.get("user_id")
|
|
39
|
+
return f"UA: {user_agent}"
|
|
40
|
+
|
|
41
|
+
# Test request context
|
|
42
|
+
with app.test_request_context("/users?page=2"):
|
|
43
|
+
assert request.path == "/users"
|
|
44
|
+
assert request.args["page"] == "2"
|
|
45
|
+
url = url_for("users.list_users")
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## The `g` Object
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from flask import g
|
|
52
|
+
|
|
53
|
+
# GOOD - store per-request data in g
|
|
54
|
+
@app.before_request
|
|
55
|
+
def load_user():
|
|
56
|
+
user_id = session.get("user_id")
|
|
57
|
+
if user_id:
|
|
58
|
+
g.user = User.query.get(user_id)
|
|
59
|
+
else:
|
|
60
|
+
g.user = None
|
|
61
|
+
|
|
62
|
+
@app.route("/profile")
|
|
63
|
+
def profile():
|
|
64
|
+
if g.user is None:
|
|
65
|
+
return redirect(url_for("auth.login"))
|
|
66
|
+
return render_template("profile.html", user=g.user)
|
|
67
|
+
|
|
68
|
+
# GOOD - lazy loading with g
|
|
69
|
+
def get_db():
|
|
70
|
+
if "db" not in g:
|
|
71
|
+
g.db = connect_to_database()
|
|
72
|
+
return g.db
|
|
73
|
+
|
|
74
|
+
@app.teardown_appcontext
|
|
75
|
+
def close_db(exception):
|
|
76
|
+
db = g.pop("db", None)
|
|
77
|
+
if db is not None:
|
|
78
|
+
db.close()
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Request Hooks
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
# Before first request (deprecated in Flask 2.3+)
|
|
85
|
+
# Use app.before_request or lifespan pattern instead
|
|
86
|
+
|
|
87
|
+
@app.before_request
|
|
88
|
+
def before_every_request():
|
|
89
|
+
"""Run before every request."""
|
|
90
|
+
g.start_time = time.time()
|
|
91
|
+
g.request_id = request.headers.get("X-Request-ID", str(uuid.uuid4()))
|
|
92
|
+
|
|
93
|
+
@app.after_request
|
|
94
|
+
def after_every_request(response):
|
|
95
|
+
"""Run after every request (even on error)."""
|
|
96
|
+
duration = time.time() - g.start_time
|
|
97
|
+
response.headers["X-Request-ID"] = g.request_id
|
|
98
|
+
response.headers["X-Response-Time"] = f"{duration:.4f}s"
|
|
99
|
+
return response
|
|
100
|
+
|
|
101
|
+
@app.teardown_request
|
|
102
|
+
def teardown_request(exception):
|
|
103
|
+
"""Run at the end of request, for cleanup."""
|
|
104
|
+
if exception:
|
|
105
|
+
app.logger.error(f"Request failed: {exception}")
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Blueprint-Specific Hooks
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
users_bp = Blueprint("users", __name__)
|
|
112
|
+
|
|
113
|
+
@users_bp.before_request
|
|
114
|
+
def before_user_request():
|
|
115
|
+
"""Only runs for requests to this blueprint."""
|
|
116
|
+
g.service = UserService(db.session)
|
|
117
|
+
|
|
118
|
+
@users_bp.after_request
|
|
119
|
+
def after_user_request(response):
|
|
120
|
+
"""Only runs for requests to this blueprint."""
|
|
121
|
+
return response
|
|
122
|
+
|
|
123
|
+
# App-wide hooks still run for blueprint requests
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Context Locals with werkzeug
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from werkzeug.local import LocalStack, LocalProxy
|
|
130
|
+
|
|
131
|
+
# Custom context local
|
|
132
|
+
_request_ctx_stack = LocalStack()
|
|
133
|
+
|
|
134
|
+
def get_current_request_id():
|
|
135
|
+
ctx = _request_ctx_stack.top
|
|
136
|
+
if ctx is not None:
|
|
137
|
+
return ctx.request_id
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
request_id = LocalProxy(get_current_request_id)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Async Context (Flask 2.0+)
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from flask import Flask
|
|
147
|
+
import asyncio
|
|
148
|
+
|
|
149
|
+
app = Flask(__name__)
|
|
150
|
+
|
|
151
|
+
@app.route("/async")
|
|
152
|
+
async def async_route():
|
|
153
|
+
# Can use async/await in routes
|
|
154
|
+
result = await some_async_operation()
|
|
155
|
+
return jsonify(result)
|
|
156
|
+
|
|
157
|
+
# Context is preserved in async functions
|
|
158
|
+
@app.route("/async-context")
|
|
159
|
+
async def async_with_context():
|
|
160
|
+
# current_app, g, request all work
|
|
161
|
+
app.logger.info("Async route called")
|
|
162
|
+
g.async_data = await fetch_data()
|
|
163
|
+
return jsonify(g.async_data)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Testing Contexts
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
import pytest
|
|
170
|
+
|
|
171
|
+
@pytest.fixture
|
|
172
|
+
def app():
|
|
173
|
+
app = create_app("testing")
|
|
174
|
+
return app
|
|
175
|
+
|
|
176
|
+
@pytest.fixture
|
|
177
|
+
def client(app):
|
|
178
|
+
return app.test_client()
|
|
179
|
+
|
|
180
|
+
@pytest.fixture
|
|
181
|
+
def app_context(app):
|
|
182
|
+
with app.app_context():
|
|
183
|
+
yield
|
|
184
|
+
|
|
185
|
+
@pytest.fixture
|
|
186
|
+
def request_context(app):
|
|
187
|
+
with app.test_request_context():
|
|
188
|
+
yield
|
|
189
|
+
|
|
190
|
+
def test_with_app_context(app_context):
|
|
191
|
+
# current_app is available
|
|
192
|
+
assert current_app.config["TESTING"]
|
|
193
|
+
|
|
194
|
+
def test_with_request_context(request_context):
|
|
195
|
+
# request, g are available
|
|
196
|
+
g.user = User(name="Test")
|
|
197
|
+
assert g.user.name == "Test"
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Pushing Contexts Manually
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
# For background tasks, CLI commands, etc.
|
|
204
|
+
def background_task(app, user_id):
|
|
205
|
+
with app.app_context():
|
|
206
|
+
user = User.query.get(user_id)
|
|
207
|
+
send_email(user)
|
|
208
|
+
|
|
209
|
+
# Thread-safe context pushing
|
|
210
|
+
from threading import Thread
|
|
211
|
+
|
|
212
|
+
def run_in_thread(app):
|
|
213
|
+
def wrapper():
|
|
214
|
+
with app.app_context():
|
|
215
|
+
do_work()
|
|
216
|
+
|
|
217
|
+
thread = Thread(target=wrapper)
|
|
218
|
+
thread.start()
|
|
219
|
+
return thread
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Context Processor
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
@app.context_processor
|
|
226
|
+
def inject_globals():
|
|
227
|
+
"""Inject variables into all templates."""
|
|
228
|
+
return {
|
|
229
|
+
"current_year": datetime.now().year,
|
|
230
|
+
"app_name": current_app.config["APP_NAME"],
|
|
231
|
+
"user": g.get("user"),
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
# In templates
|
|
235
|
+
# {{ current_year }}
|
|
236
|
+
# {{ app_name }}
|
|
237
|
+
# {% if user %}Hello {{ user.name }}{% endif %}
|
|
238
|
+
```
|
|
@@ -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
|
+
```
|