red64-cli 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/parseArgs.d.ts.map +1 -1
- package/dist/cli/parseArgs.js +5 -0
- package/dist/cli/parseArgs.js.map +1 -1
- package/dist/components/init/CompleteStep.d.ts.map +1 -1
- package/dist/components/init/CompleteStep.js +2 -2
- package/dist/components/init/CompleteStep.js.map +1 -1
- package/dist/components/init/TestCheckStep.d.ts +16 -0
- package/dist/components/init/TestCheckStep.d.ts.map +1 -0
- package/dist/components/init/TestCheckStep.js +120 -0
- package/dist/components/init/TestCheckStep.js.map +1 -0
- package/dist/components/init/index.d.ts +1 -0
- package/dist/components/init/index.d.ts.map +1 -1
- package/dist/components/init/index.js +1 -0
- package/dist/components/init/index.js.map +1 -1
- package/dist/components/init/types.d.ts +9 -0
- package/dist/components/init/types.d.ts.map +1 -1
- package/dist/components/screens/InitScreen.d.ts.map +1 -1
- package/dist/components/screens/InitScreen.js +69 -6
- package/dist/components/screens/InitScreen.js.map +1 -1
- package/dist/components/screens/StartScreen.d.ts.map +1 -1
- package/dist/components/screens/StartScreen.js +89 -3
- package/dist/components/screens/StartScreen.js.map +1 -1
- package/dist/services/ConfigService.d.ts +1 -0
- package/dist/services/ConfigService.d.ts.map +1 -1
- package/dist/services/ConfigService.js.map +1 -1
- package/dist/services/ProjectDetector.d.ts +28 -0
- package/dist/services/ProjectDetector.d.ts.map +1 -0
- package/dist/services/ProjectDetector.js +236 -0
- package/dist/services/ProjectDetector.js.map +1 -0
- package/dist/services/TestRunner.d.ts +46 -0
- package/dist/services/TestRunner.d.ts.map +1 -0
- package/dist/services/TestRunner.js +85 -0
- package/dist/services/TestRunner.js.map +1 -0
- package/dist/services/index.d.ts +2 -0
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +2 -0
- package/dist/services/index.js.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/framework/agents/claude/.claude/agents/red64/spec-impl.md +131 -2
- package/framework/agents/claude/.claude/commands/red64/spec-impl.md +24 -0
- package/framework/agents/codex/.codex/agents/red64/spec-impl.md +131 -2
- package/framework/agents/codex/.codex/commands/red64/spec-impl.md +24 -0
- package/framework/stacks/generic/feedback.md +80 -0
- package/framework/stacks/nextjs/accessibility.md +437 -0
- package/framework/stacks/nextjs/api.md +431 -0
- package/framework/stacks/nextjs/coding-style.md +282 -0
- package/framework/stacks/nextjs/commenting.md +226 -0
- package/framework/stacks/nextjs/components.md +411 -0
- package/framework/stacks/nextjs/conventions.md +333 -0
- package/framework/stacks/nextjs/css.md +310 -0
- package/framework/stacks/nextjs/error-handling.md +442 -0
- package/framework/stacks/nextjs/feedback.md +124 -0
- package/framework/stacks/nextjs/migrations.md +332 -0
- package/framework/stacks/nextjs/models.md +362 -0
- package/framework/stacks/nextjs/queries.md +410 -0
- package/framework/stacks/nextjs/responsive.md +338 -0
- package/framework/stacks/nextjs/tech-stack.md +177 -0
- package/framework/stacks/nextjs/test-writing.md +475 -0
- package/framework/stacks/nextjs/validation.md +467 -0
- package/framework/stacks/python/api.md +468 -0
- package/framework/stacks/python/authentication.md +342 -0
- package/framework/stacks/python/code-quality.md +283 -0
- package/framework/stacks/python/code-refactoring.md +315 -0
- package/framework/stacks/python/coding-style.md +462 -0
- package/framework/stacks/python/conventions.md +399 -0
- package/framework/stacks/python/error-handling.md +512 -0
- package/framework/stacks/python/feedback.md +92 -0
- package/framework/stacks/python/implement-ai-llm.md +468 -0
- package/framework/stacks/python/migrations.md +388 -0
- package/framework/stacks/python/models.md +399 -0
- package/framework/stacks/python/python.md +232 -0
- package/framework/stacks/python/queries.md +451 -0
- package/framework/stacks/python/structure.md +245 -58
- package/framework/stacks/python/tech.md +92 -35
- package/framework/stacks/python/testing.md +380 -0
- package/framework/stacks/python/validation.md +471 -0
- package/framework/stacks/rails/authentication.md +176 -0
- package/framework/stacks/rails/code-quality.md +287 -0
- package/framework/stacks/rails/code-refactoring.md +299 -0
- package/framework/stacks/rails/feedback.md +130 -0
- package/framework/stacks/rails/implement-ai-llm-with-rubyllm.md +342 -0
- package/framework/stacks/rails/rails.md +301 -0
- package/framework/stacks/rails/rails8-best-practices.md +498 -0
- package/framework/stacks/rails/rails8-css.md +573 -0
- package/framework/stacks/rails/structure.md +140 -0
- package/framework/stacks/rails/tech.md +108 -0
- package/framework/stacks/react/code-quality.md +521 -0
- package/framework/stacks/react/components.md +625 -0
- package/framework/stacks/react/data-fetching.md +586 -0
- package/framework/stacks/react/feedback.md +110 -0
- package/framework/stacks/react/forms.md +694 -0
- package/framework/stacks/react/performance.md +640 -0
- package/framework/stacks/react/product.md +22 -9
- package/framework/stacks/react/state-management.md +472 -0
- package/framework/stacks/react/structure.md +351 -44
- package/framework/stacks/react/tech.md +219 -30
- package/framework/stacks/react/testing.md +690 -0
- package/package.json +1 -1
- package/framework/stacks/node/product.md +0 -27
- package/framework/stacks/node/structure.md +0 -82
- package/framework/stacks/node/tech.md +0 -63
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
# Error Handling Patterns
|
|
2
|
+
|
|
3
|
+
Structured error handling for Python applications with FastAPI, custom exceptions, and observability.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Philosophy
|
|
8
|
+
|
|
9
|
+
- **Fail fast**: Validate inputs early, raise immediately on invalid state
|
|
10
|
+
- **Typed exceptions**: Custom hierarchy over generic `Exception`
|
|
11
|
+
- **Centralized handling**: Exception handlers at API boundary, not scattered try/except
|
|
12
|
+
- **Structured logging**: Machine-readable logs with context, not print statements
|
|
13
|
+
- **User-safe messages**: Never expose stack traces or internal details to clients
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Custom Exception Hierarchy
|
|
18
|
+
|
|
19
|
+
### Base Exceptions
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
# app/exceptions.py
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AppError(Exception):
|
|
27
|
+
"""Base exception for all application errors."""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
message: str,
|
|
32
|
+
code: str = "INTERNAL_ERROR",
|
|
33
|
+
status_code: int = 500,
|
|
34
|
+
details: dict[str, Any] | None = None,
|
|
35
|
+
):
|
|
36
|
+
super().__init__(message)
|
|
37
|
+
self.message = message
|
|
38
|
+
self.code = code
|
|
39
|
+
self.status_code = status_code
|
|
40
|
+
self.details = details or {}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class NotFoundError(AppError):
|
|
44
|
+
def __init__(self, resource: str, identifier: str):
|
|
45
|
+
super().__init__(
|
|
46
|
+
message=f"{resource} not found: {identifier}",
|
|
47
|
+
code="NOT_FOUND",
|
|
48
|
+
status_code=404,
|
|
49
|
+
details={"resource": resource, "identifier": identifier},
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class ConflictError(AppError):
|
|
54
|
+
def __init__(self, message: str, details: dict[str, Any] | None = None):
|
|
55
|
+
super().__init__(message=message, code="CONFLICT", status_code=409, details=details)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class ValidationError(AppError):
|
|
59
|
+
def __init__(self, message: str, field_errors: dict[str, str] | None = None):
|
|
60
|
+
super().__init__(
|
|
61
|
+
message=message,
|
|
62
|
+
code="VALIDATION_ERROR",
|
|
63
|
+
status_code=422,
|
|
64
|
+
details={"field_errors": field_errors or {}},
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class AuthenticationError(AppError):
|
|
69
|
+
def __init__(self, message: str = "Authentication required"):
|
|
70
|
+
super().__init__(message=message, code="UNAUTHENTICATED", status_code=401)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class AuthorizationError(AppError):
|
|
74
|
+
def __init__(self, message: str = "Insufficient permissions"):
|
|
75
|
+
super().__init__(message=message, code="FORBIDDEN", status_code=403)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class ExternalServiceError(AppError):
|
|
79
|
+
def __init__(self, service: str, message: str):
|
|
80
|
+
super().__init__(
|
|
81
|
+
message=f"External service error ({service}): {message}",
|
|
82
|
+
code="EXTERNAL_SERVICE_ERROR",
|
|
83
|
+
status_code=502,
|
|
84
|
+
details={"service": service},
|
|
85
|
+
)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Usage in Services
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
class UserService:
|
|
92
|
+
async def get_user(self, user_id: int) -> User:
|
|
93
|
+
user = await self.repo.get(user_id)
|
|
94
|
+
if user is None:
|
|
95
|
+
raise NotFoundError("User", str(user_id))
|
|
96
|
+
return user
|
|
97
|
+
|
|
98
|
+
async def create_user(self, data: CreateUserRequest) -> User:
|
|
99
|
+
existing = await self.repo.get_by_email(data.email)
|
|
100
|
+
if existing:
|
|
101
|
+
raise ConflictError(
|
|
102
|
+
"Email already registered",
|
|
103
|
+
details={"email": data.email},
|
|
104
|
+
)
|
|
105
|
+
return await self.repo.save(User(**data.model_dump()))
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## FastAPI Exception Handlers
|
|
111
|
+
|
|
112
|
+
### Centralized Handler Registration
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
# app/middleware/error_handler.py
|
|
116
|
+
import structlog
|
|
117
|
+
from fastapi import FastAPI, Request
|
|
118
|
+
from fastapi.responses import JSONResponse
|
|
119
|
+
from pydantic import ValidationError as PydanticValidationError
|
|
120
|
+
|
|
121
|
+
from app.exceptions import AppError
|
|
122
|
+
|
|
123
|
+
logger = structlog.get_logger()
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def register_error_handlers(app: FastAPI) -> None:
|
|
127
|
+
@app.exception_handler(AppError)
|
|
128
|
+
async def app_error_handler(request: Request, exc: AppError) -> JSONResponse:
|
|
129
|
+
logger.warning(
|
|
130
|
+
"app_error",
|
|
131
|
+
code=exc.code,
|
|
132
|
+
message=exc.message,
|
|
133
|
+
status_code=exc.status_code,
|
|
134
|
+
path=request.url.path,
|
|
135
|
+
**exc.details,
|
|
136
|
+
)
|
|
137
|
+
return JSONResponse(
|
|
138
|
+
status_code=exc.status_code,
|
|
139
|
+
content={
|
|
140
|
+
"error": {
|
|
141
|
+
"code": exc.code,
|
|
142
|
+
"message": exc.message,
|
|
143
|
+
"details": exc.details,
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
@app.exception_handler(PydanticValidationError)
|
|
149
|
+
async def pydantic_error_handler(
|
|
150
|
+
request: Request, exc: PydanticValidationError,
|
|
151
|
+
) -> JSONResponse:
|
|
152
|
+
return JSONResponse(
|
|
153
|
+
status_code=422,
|
|
154
|
+
content={
|
|
155
|
+
"error": {
|
|
156
|
+
"code": "VALIDATION_ERROR",
|
|
157
|
+
"message": "Request validation failed",
|
|
158
|
+
"details": {"errors": exc.errors()},
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
@app.exception_handler(Exception)
|
|
164
|
+
async def unhandled_error_handler(request: Request, exc: Exception) -> JSONResponse:
|
|
165
|
+
logger.exception(
|
|
166
|
+
"unhandled_error",
|
|
167
|
+
path=request.url.path,
|
|
168
|
+
method=request.method,
|
|
169
|
+
error=str(exc),
|
|
170
|
+
)
|
|
171
|
+
return JSONResponse(
|
|
172
|
+
status_code=500,
|
|
173
|
+
content={
|
|
174
|
+
"error": {
|
|
175
|
+
"code": "INTERNAL_ERROR",
|
|
176
|
+
"message": "An unexpected error occurred",
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Registration in App
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
# app/main.py
|
|
186
|
+
from app.middleware.error_handler import register_error_handlers
|
|
187
|
+
|
|
188
|
+
app = FastAPI()
|
|
189
|
+
register_error_handlers(app)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## Structured Error Response
|
|
195
|
+
|
|
196
|
+
### Consistent Format
|
|
197
|
+
|
|
198
|
+
All error responses follow the same structure:
|
|
199
|
+
|
|
200
|
+
```json
|
|
201
|
+
{
|
|
202
|
+
"error": {
|
|
203
|
+
"code": "NOT_FOUND",
|
|
204
|
+
"message": "User not found: 42",
|
|
205
|
+
"details": {
|
|
206
|
+
"resource": "User",
|
|
207
|
+
"identifier": "42"
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Error Response Schema
|
|
214
|
+
|
|
215
|
+
```python
|
|
216
|
+
# app/schemas/error.py
|
|
217
|
+
from pydantic import BaseModel
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class ErrorDetail(BaseModel):
|
|
221
|
+
code: str
|
|
222
|
+
message: str
|
|
223
|
+
details: dict | None = None
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class ErrorResponse(BaseModel):
|
|
227
|
+
error: ErrorDetail
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Use in OpenAPI docs:
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
@router.get(
|
|
234
|
+
"/users/{user_id}",
|
|
235
|
+
responses={
|
|
236
|
+
404: {"model": ErrorResponse, "description": "User not found"},
|
|
237
|
+
},
|
|
238
|
+
)
|
|
239
|
+
async def get_user(user_id: int) -> UserResponse:
|
|
240
|
+
...
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Fail-Fast Validation
|
|
246
|
+
|
|
247
|
+
### Early Returns
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
async def publish_post(self, post_id: int, user: User) -> Post:
|
|
251
|
+
post = await self.repo.get(post_id)
|
|
252
|
+
if post is None:
|
|
253
|
+
raise NotFoundError("Post", str(post_id))
|
|
254
|
+
if post.user_id != user.id:
|
|
255
|
+
raise AuthorizationError("Cannot publish another user's post")
|
|
256
|
+
if post.status == "published":
|
|
257
|
+
raise ConflictError("Post is already published")
|
|
258
|
+
if not post.title or not post.body:
|
|
259
|
+
raise ValidationError("Post must have title and body to publish")
|
|
260
|
+
|
|
261
|
+
post.status = "published"
|
|
262
|
+
post.published_at = datetime.now(timezone.utc)
|
|
263
|
+
await self.repo.save(post)
|
|
264
|
+
return post
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Guard Clauses in Utilities
|
|
268
|
+
|
|
269
|
+
```python
|
|
270
|
+
def parse_pagination(page: int, per_page: int) -> tuple[int, int]:
|
|
271
|
+
if page < 1:
|
|
272
|
+
raise ValidationError("Page must be >= 1", field_errors={"page": "Must be positive"})
|
|
273
|
+
if per_page < 1 or per_page > 100:
|
|
274
|
+
raise ValidationError(
|
|
275
|
+
"per_page must be between 1 and 100",
|
|
276
|
+
field_errors={"per_page": "Must be 1-100"},
|
|
277
|
+
)
|
|
278
|
+
return page, per_page
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
## Retry Strategies with tenacity
|
|
284
|
+
|
|
285
|
+
### External Service Calls
|
|
286
|
+
|
|
287
|
+
```python
|
|
288
|
+
from tenacity import (
|
|
289
|
+
retry,
|
|
290
|
+
stop_after_attempt,
|
|
291
|
+
wait_exponential,
|
|
292
|
+
retry_if_exception_type,
|
|
293
|
+
)
|
|
294
|
+
import httpx
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
class PaymentClient:
|
|
298
|
+
@retry(
|
|
299
|
+
stop=stop_after_attempt(3),
|
|
300
|
+
wait=wait_exponential(multiplier=1, min=1, max=10),
|
|
301
|
+
retry=retry_if_exception_type((httpx.TimeoutException, httpx.HTTPStatusError)),
|
|
302
|
+
reraise=True,
|
|
303
|
+
)
|
|
304
|
+
async def charge(self, amount_cents: int, token: str) -> dict:
|
|
305
|
+
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
306
|
+
response = await client.post(
|
|
307
|
+
"https://api.payments.example.com/charges",
|
|
308
|
+
json={"amount": amount_cents, "token": token},
|
|
309
|
+
)
|
|
310
|
+
response.raise_for_status()
|
|
311
|
+
return response.json()
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### Custom Retry with Logging
|
|
315
|
+
|
|
316
|
+
```python
|
|
317
|
+
from tenacity import before_sleep_log, after_log
|
|
318
|
+
import logging
|
|
319
|
+
|
|
320
|
+
logger = logging.getLogger(__name__)
|
|
321
|
+
|
|
322
|
+
@retry(
|
|
323
|
+
stop=stop_after_attempt(3),
|
|
324
|
+
wait=wait_exponential(multiplier=1, min=2, max=30),
|
|
325
|
+
before_sleep=before_sleep_log(logger, logging.WARNING),
|
|
326
|
+
after=after_log(logger, logging.INFO),
|
|
327
|
+
)
|
|
328
|
+
async def fetch_external_data(url: str) -> dict:
|
|
329
|
+
...
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### When NOT to Retry
|
|
333
|
+
|
|
334
|
+
| Scenario | Retry? | Reason |
|
|
335
|
+
|---|---|---|
|
|
336
|
+
| Network timeout | Yes | Transient failure |
|
|
337
|
+
| 5xx server error | Yes | Server may recover |
|
|
338
|
+
| 4xx client error | No | Request is wrong, retrying won't help |
|
|
339
|
+
| Authentication failure | No | Credentials are invalid |
|
|
340
|
+
| Validation error | No | Input is invalid |
|
|
341
|
+
| Database constraint violation | No | Data conflict, not transient |
|
|
342
|
+
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
## Structured Logging with structlog
|
|
346
|
+
|
|
347
|
+
### Configuration
|
|
348
|
+
|
|
349
|
+
```python
|
|
350
|
+
# app/logging_config.py
|
|
351
|
+
import structlog
|
|
352
|
+
|
|
353
|
+
structlog.configure(
|
|
354
|
+
processors=[
|
|
355
|
+
structlog.contextvars.merge_contextvars,
|
|
356
|
+
structlog.stdlib.filter_by_level,
|
|
357
|
+
structlog.stdlib.add_logger_name,
|
|
358
|
+
structlog.stdlib.add_log_level,
|
|
359
|
+
structlog.stdlib.PositionalArgumentsFormatter(),
|
|
360
|
+
structlog.processors.TimeStamper(fmt="iso"),
|
|
361
|
+
structlog.processors.StackInfoRenderer(),
|
|
362
|
+
structlog.processors.format_exc_info,
|
|
363
|
+
structlog.processors.UnicodeDecoder(),
|
|
364
|
+
structlog.processors.JSONRenderer(), # JSON in production
|
|
365
|
+
],
|
|
366
|
+
wrapper_class=structlog.stdlib.BoundLogger,
|
|
367
|
+
context_class=dict,
|
|
368
|
+
logger_factory=structlog.stdlib.LoggerFactory(),
|
|
369
|
+
)
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### Logging with Context
|
|
373
|
+
|
|
374
|
+
```python
|
|
375
|
+
import structlog
|
|
376
|
+
|
|
377
|
+
logger = structlog.get_logger()
|
|
378
|
+
|
|
379
|
+
async def create_order(self, user_id: int, items: list[dict]) -> Order:
|
|
380
|
+
log = logger.bind(user_id=user_id, item_count=len(items))
|
|
381
|
+
log.info("creating_order")
|
|
382
|
+
|
|
383
|
+
try:
|
|
384
|
+
order = await self._build_order(user_id, items)
|
|
385
|
+
log.info("order_created", order_id=order.id, total=order.total_cents)
|
|
386
|
+
return order
|
|
387
|
+
except ExternalServiceError:
|
|
388
|
+
log.error("order_creation_failed", reason="payment_service_unavailable")
|
|
389
|
+
raise
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
### Request Context Middleware
|
|
393
|
+
|
|
394
|
+
```python
|
|
395
|
+
import uuid
|
|
396
|
+
import structlog
|
|
397
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
398
|
+
|
|
399
|
+
class RequestContextMiddleware(BaseHTTPMiddleware):
|
|
400
|
+
async def dispatch(self, request, call_next):
|
|
401
|
+
request_id = request.headers.get("X-Request-ID", str(uuid.uuid4()))
|
|
402
|
+
structlog.contextvars.clear_contextvars()
|
|
403
|
+
structlog.contextvars.bind_contextvars(
|
|
404
|
+
request_id=request_id,
|
|
405
|
+
method=request.method,
|
|
406
|
+
path=request.url.path,
|
|
407
|
+
)
|
|
408
|
+
response = await call_next(request)
|
|
409
|
+
response.headers["X-Request-ID"] = request_id
|
|
410
|
+
return response
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
---
|
|
414
|
+
|
|
415
|
+
## Error Context Propagation
|
|
416
|
+
|
|
417
|
+
### Chaining Exceptions
|
|
418
|
+
|
|
419
|
+
```python
|
|
420
|
+
try:
|
|
421
|
+
result = await external_client.fetch(resource_id)
|
|
422
|
+
except httpx.HTTPError as exc:
|
|
423
|
+
raise ExternalServiceError("resource-api", str(exc)) from exc
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
The `from exc` preserves the original traceback for debugging while presenting a clean error to the caller.
|
|
427
|
+
|
|
428
|
+
### Adding Context to Errors
|
|
429
|
+
|
|
430
|
+
```python
|
|
431
|
+
async def process_batch(items: list[dict]) -> list[Result]:
|
|
432
|
+
results = []
|
|
433
|
+
for i, item in enumerate(items):
|
|
434
|
+
try:
|
|
435
|
+
result = await process_item(item)
|
|
436
|
+
results.append(result)
|
|
437
|
+
except AppError as exc:
|
|
438
|
+
exc.details["batch_index"] = i
|
|
439
|
+
exc.details["item_id"] = item.get("id")
|
|
440
|
+
raise
|
|
441
|
+
return results
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
---
|
|
445
|
+
|
|
446
|
+
## Result Pattern (Alternative to Exceptions)
|
|
447
|
+
|
|
448
|
+
### For Expected Failures
|
|
449
|
+
|
|
450
|
+
```python
|
|
451
|
+
from dataclasses import dataclass
|
|
452
|
+
from typing import Generic, TypeVar
|
|
453
|
+
|
|
454
|
+
T = TypeVar("T")
|
|
455
|
+
|
|
456
|
+
@dataclass(frozen=True)
|
|
457
|
+
class Ok(Generic[T]):
|
|
458
|
+
value: T
|
|
459
|
+
is_ok: bool = True
|
|
460
|
+
|
|
461
|
+
@dataclass(frozen=True)
|
|
462
|
+
class Err:
|
|
463
|
+
error: str
|
|
464
|
+
code: str = "ERROR"
|
|
465
|
+
is_ok: bool = False
|
|
466
|
+
|
|
467
|
+
type Result[T] = Ok[T] | Err
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
### Usage
|
|
471
|
+
|
|
472
|
+
```python
|
|
473
|
+
async def create_user(self, data: CreateUserRequest) -> Result[User]:
|
|
474
|
+
existing = await self.repo.get_by_email(data.email)
|
|
475
|
+
if existing:
|
|
476
|
+
return Err("Email already registered", code="DUPLICATE_EMAIL")
|
|
477
|
+
|
|
478
|
+
user = User(**data.model_dump())
|
|
479
|
+
await self.repo.save(user)
|
|
480
|
+
return Ok(user)
|
|
481
|
+
|
|
482
|
+
# Caller
|
|
483
|
+
result = await service.create_user(data)
|
|
484
|
+
if result.is_ok:
|
|
485
|
+
return UserResponse.model_validate(result.value)
|
|
486
|
+
else:
|
|
487
|
+
raise ConflictError(result.error)
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
### When to Use Each
|
|
491
|
+
|
|
492
|
+
| Pattern | Use Case |
|
|
493
|
+
|---|---|
|
|
494
|
+
| Exceptions | Unexpected failures, infrastructure errors, auth failures |
|
|
495
|
+
| Result | Expected business logic outcomes (duplicate email, insufficient funds) |
|
|
496
|
+
|
|
497
|
+
---
|
|
498
|
+
|
|
499
|
+
## Anti-Patterns
|
|
500
|
+
|
|
501
|
+
| Anti-Pattern | Problem | Correct Approach |
|
|
502
|
+
|---|---|---|
|
|
503
|
+
| Bare `except:` | Catches `SystemExit`, `KeyboardInterrupt` | Catch specific exceptions |
|
|
504
|
+
| `except Exception: pass` | Silently swallows errors | Log and re-raise or handle |
|
|
505
|
+
| Returning error strings | No type safety, easy to ignore | Use Result type or raise |
|
|
506
|
+
| Stack traces in API responses | Security risk, bad UX | Return error codes and messages |
|
|
507
|
+
| Try/except around every function | Hard to read, hides flow | Centralized handlers |
|
|
508
|
+
| Generic `HTTPException(500)` | No error classification | Use typed exception hierarchy |
|
|
509
|
+
|
|
510
|
+
---
|
|
511
|
+
|
|
512
|
+
_Errors are data. Classify them, log them with context, and present them consistently. Never swallow exceptions silently._
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Feedback Configuration
|
|
2
|
+
|
|
3
|
+
Project-specific commands for automated feedback during Python implementation.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Test Commands
|
|
8
|
+
|
|
9
|
+
Commands to run tests during implementation. The agent will use these to verify code changes.
|
|
10
|
+
|
|
11
|
+
```yaml
|
|
12
|
+
# Primary test command (REQUIRED)
|
|
13
|
+
test: uv run pytest
|
|
14
|
+
|
|
15
|
+
# Test with coverage report
|
|
16
|
+
test_coverage: uv run pytest --cov=src --cov-report=term-missing
|
|
17
|
+
|
|
18
|
+
# Run specific test file (use {file} as placeholder)
|
|
19
|
+
test_file: uv run pytest {file} -v
|
|
20
|
+
|
|
21
|
+
# Run tests matching pattern
|
|
22
|
+
test_pattern: uv run pytest -k "{pattern}"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Linting Commands
|
|
28
|
+
|
|
29
|
+
Commands for code quality checks.
|
|
30
|
+
|
|
31
|
+
```yaml
|
|
32
|
+
# Primary lint command (Ruff for speed)
|
|
33
|
+
lint: uv run ruff check .
|
|
34
|
+
|
|
35
|
+
# Lint with auto-fix
|
|
36
|
+
lint_fix: uv run ruff check . --fix
|
|
37
|
+
|
|
38
|
+
# Type checking
|
|
39
|
+
type_check: uv run mypy src/
|
|
40
|
+
|
|
41
|
+
# Format check
|
|
42
|
+
format_check: uv run ruff format --check .
|
|
43
|
+
|
|
44
|
+
# Format fix
|
|
45
|
+
format_fix: uv run ruff format .
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Development Server
|
|
51
|
+
|
|
52
|
+
Commands for starting the development server (required for UI verification).
|
|
53
|
+
|
|
54
|
+
```yaml
|
|
55
|
+
# Start dev server (FastAPI)
|
|
56
|
+
dev_server: uv run uvicorn src.app.main:app --reload
|
|
57
|
+
|
|
58
|
+
# Dev server port
|
|
59
|
+
dev_port: 8000
|
|
60
|
+
|
|
61
|
+
# Dev server base URL
|
|
62
|
+
dev_url: http://localhost:8000
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## UI Verification
|
|
68
|
+
|
|
69
|
+
Settings for agent-browser UI verification.
|
|
70
|
+
|
|
71
|
+
```yaml
|
|
72
|
+
# Enable UI verification (typically false for API-only projects)
|
|
73
|
+
ui_verification_enabled: false
|
|
74
|
+
|
|
75
|
+
# Default wait time after navigation (milliseconds)
|
|
76
|
+
navigation_wait: 2000
|
|
77
|
+
|
|
78
|
+
# Screenshot directory
|
|
79
|
+
screenshot_dir: /tmp/ui-captures
|
|
80
|
+
|
|
81
|
+
# API documentation URL (for API projects)
|
|
82
|
+
api_docs_url: http://localhost:8000/docs
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Notes
|
|
88
|
+
|
|
89
|
+
- Uses `uv` as the package manager (faster than pip)
|
|
90
|
+
- Ruff replaces flake8, isort, and black for linting/formatting
|
|
91
|
+
- pytest-asyncio runs async tests automatically with `asyncio_mode = "auto"`
|
|
92
|
+
- For API-only projects, UI verification is disabled by default
|