tribunal-kit 3.0.0 → 3.1.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/.agent/ARCHITECTURE.md +99 -99
- package/.agent/GEMINI.md +52 -52
- package/.agent/agents/accessibility-reviewer.md +187 -220
- package/.agent/agents/ai-code-reviewer.md +199 -233
- package/.agent/agents/backend-specialist.md +215 -238
- package/.agent/agents/code-archaeologist.md +161 -181
- package/.agent/agents/database-architect.md +184 -207
- package/.agent/agents/debugger.md +191 -218
- package/.agent/agents/dependency-reviewer.md +103 -136
- package/.agent/agents/devops-engineer.md +218 -238
- package/.agent/agents/documentation-writer.md +201 -221
- package/.agent/agents/explorer-agent.md +160 -180
- package/.agent/agents/frontend-reviewer.md +160 -194
- package/.agent/agents/frontend-specialist.md +248 -237
- package/.agent/agents/game-developer.md +48 -52
- package/.agent/agents/logic-reviewer.md +116 -149
- package/.agent/agents/mobile-developer.md +200 -223
- package/.agent/agents/mobile-reviewer.md +162 -195
- package/.agent/agents/orchestrator.md +181 -211
- package/.agent/agents/penetration-tester.md +157 -174
- package/.agent/agents/performance-optimizer.md +183 -203
- package/.agent/agents/performance-reviewer.md +178 -211
- package/.agent/agents/product-manager.md +142 -162
- package/.agent/agents/product-owner.md +6 -25
- package/.agent/agents/project-planner.md +142 -162
- package/.agent/agents/qa-automation-engineer.md +225 -242
- package/.agent/agents/security-auditor.md +174 -194
- package/.agent/agents/seo-specialist.md +193 -213
- package/.agent/agents/sql-reviewer.md +161 -194
- package/.agent/agents/supervisor-agent.md +184 -203
- package/.agent/agents/swarm-worker-contracts.md +17 -17
- package/.agent/agents/swarm-worker-registry.md +46 -46
- package/.agent/agents/test-coverage-reviewer.md +160 -193
- package/.agent/agents/test-engineer.md +0 -21
- package/.agent/agents/type-safety-reviewer.md +175 -208
- package/.agent/patterns/generator.md +9 -9
- package/.agent/patterns/inversion.md +12 -12
- package/.agent/patterns/pipeline.md +9 -9
- package/.agent/patterns/reviewer.md +13 -13
- package/.agent/patterns/tool-wrapper.md +9 -9
- package/.agent/rules/GEMINI.md +63 -63
- package/.agent/scripts/compress_skills.py +167 -0
- package/.agent/scripts/consolidate_skills.py +173 -0
- package/.agent/scripts/deep_compress.py +202 -0
- package/.agent/scripts/minify_context.py +80 -0
- package/.agent/scripts/security_scan.py +1 -1
- package/.agent/scripts/strip_tribunal.py +41 -0
- package/.agent/skills/agent-organizer/SKILL.md +92 -126
- package/.agent/skills/agentic-patterns/SKILL.md +0 -70
- package/.agent/skills/ai-prompt-injection-defense/SKILL.md +126 -160
- package/.agent/skills/api-patterns/SKILL.md +123 -215
- package/.agent/skills/api-security-auditor/SKILL.md +143 -177
- package/.agent/skills/app-builder/SKILL.md +326 -50
- package/.agent/skills/app-builder/templates/SKILL.md +13 -15
- package/.agent/skills/app-builder/templates/astro-static/TEMPLATE.md +16 -16
- package/.agent/skills/app-builder/templates/chrome-extension/TEMPLATE.md +22 -22
- package/.agent/skills/app-builder/templates/cli-tool/TEMPLATE.md +18 -18
- package/.agent/skills/app-builder/templates/electron-desktop/TEMPLATE.md +20 -20
- package/.agent/skills/app-builder/templates/express-api/TEMPLATE.md +17 -17
- package/.agent/skills/app-builder/templates/flutter-app/TEMPLATE.md +18 -18
- package/.agent/skills/app-builder/templates/monorepo-turborepo/TEMPLATE.md +21 -21
- package/.agent/skills/app-builder/templates/nextjs-fullstack/TEMPLATE.md +19 -19
- package/.agent/skills/app-builder/templates/nextjs-saas/TEMPLATE.md +26 -26
- package/.agent/skills/app-builder/templates/nextjs-static/TEMPLATE.md +26 -26
- package/.agent/skills/app-builder/templates/nuxt-app/TEMPLATE.md +19 -19
- package/.agent/skills/app-builder/templates/python-fastapi/TEMPLATE.md +18 -18
- package/.agent/skills/app-builder/templates/react-native-app/TEMPLATE.md +20 -20
- package/.agent/skills/appflow-wireframe/SKILL.md +87 -121
- package/.agent/skills/architecture/SKILL.md +82 -252
- package/.agent/skills/authentication-best-practices/SKILL.md +139 -173
- package/.agent/skills/bash-linux/SKILL.md +120 -154
- package/.agent/skills/behavioral-modes/SKILL.md +8 -69
- package/.agent/skills/brainstorming/SKILL.md +428 -104
- package/.agent/skills/building-native-ui/SKILL.md +143 -174
- package/.agent/skills/clean-code/SKILL.md +323 -360
- package/.agent/skills/code-review-checklist/SKILL.md +0 -62
- package/.agent/skills/config-validator/SKILL.md +107 -141
- package/.agent/skills/csharp-developer/SKILL.md +468 -528
- package/.agent/skills/database-design/SKILL.md +104 -369
- package/.agent/skills/deployment-procedures/SKILL.md +111 -145
- package/.agent/skills/devops-engineer/SKILL.md +295 -332
- package/.agent/skills/devops-incident-responder/SKILL.md +79 -113
- package/.agent/skills/doc.md +5 -5
- package/.agent/skills/documentation-templates/SKILL.md +19 -63
- package/.agent/skills/edge-computing/SKILL.md +123 -157
- package/.agent/skills/extract-design-system/SKILL.md +100 -134
- package/.agent/skills/framer-motion-expert/SKILL.md +111 -855
- package/.agent/skills/frontend-design/SKILL.md +151 -499
- package/.agent/skills/game-design-expert/SKILL.md +71 -105
- package/.agent/skills/game-engineering-expert/SKILL.md +88 -122
- package/.agent/skills/geo-fundamentals/SKILL.md +89 -124
- package/.agent/skills/github-operations/SKILL.md +279 -314
- package/.agent/skills/gsap-expert/SKILL.md +119 -826
- package/.agent/skills/i18n-localization/SKILL.md +104 -138
- package/.agent/skills/intelligent-routing/SKILL.md +159 -127
- package/.agent/skills/lint-and-validate/SKILL.md +8 -52
- package/.agent/skills/llm-engineering/SKILL.md +344 -357
- package/.agent/skills/local-first/SKILL.md +120 -154
- package/.agent/skills/mcp-builder/SKILL.md +84 -118
- package/.agent/skills/mobile-design/SKILL.md +213 -219
- package/.agent/skills/motion-engineering/SKILL.md +184 -0
- package/.agent/skills/nextjs-react-expert/SKILL.md +99 -698
- package/.agent/skills/nodejs-best-practices/SKILL.md +498 -559
- package/.agent/skills/observability/SKILL.md +293 -330
- package/.agent/skills/parallel-agents/SKILL.md +88 -122
- package/.agent/skills/performance-profiling/SKILL.md +217 -254
- package/.agent/skills/plan-writing/SKILL.md +84 -118
- package/.agent/skills/platform-engineer/SKILL.md +89 -123
- package/.agent/skills/playwright-best-practices/SKILL.md +128 -162
- package/.agent/skills/powershell-windows/SKILL.md +112 -146
- package/.agent/skills/python-patterns/SKILL.md +7 -35
- package/.agent/skills/python-pro/SKILL.md +148 -754
- package/.agent/skills/react-specialist/SKILL.md +123 -827
- package/.agent/skills/readme-builder/SKILL.md +15 -85
- package/.agent/skills/realtime-patterns/SKILL.md +269 -304
- package/.agent/skills/red-team-tactics/SKILL.md +10 -51
- package/.agent/skills/rust-pro/SKILL.md +623 -701
- package/.agent/skills/seo-fundamentals/SKILL.md +120 -154
- package/.agent/skills/server-management/SKILL.md +156 -190
- package/.agent/skills/shadcn-ui-expert/SKILL.md +172 -206
- package/.agent/skills/skill-creator/SKILL.md +18 -58
- package/.agent/skills/sql-pro/SKILL.md +579 -633
- package/.agent/skills/supabase-postgres-best-practices/SKILL.md +28 -68
- package/.agent/skills/swiftui-expert/SKILL.md +142 -176
- package/.agent/skills/systematic-debugging/SKILL.md +84 -118
- package/.agent/skills/tailwind-patterns/SKILL.md +516 -576
- package/.agent/skills/tdd-workflow/SKILL.md +103 -137
- package/.agent/skills/test-result-analyzer/SKILL.md +33 -73
- package/.agent/skills/testing-patterns/SKILL.md +512 -573
- package/.agent/skills/trend-researcher/SKILL.md +30 -71
- package/.agent/skills/ui-ux-pro-max/SKILL.md +0 -41
- package/.agent/skills/ui-ux-researcher/SKILL.md +51 -91
- package/.agent/skills/vue-expert/SKILL.md +127 -866
- package/.agent/skills/vulnerability-scanner/SKILL.md +354 -269
- package/.agent/skills/web-accessibility-auditor/SKILL.md +159 -193
- package/.agent/skills/web-design-guidelines/SKILL.md +17 -61
- package/.agent/skills/webapp-testing/SKILL.md +111 -145
- package/.agent/skills/whimsy-injector/SKILL.md +58 -132
- package/.agent/skills/workflow-optimizer/SKILL.md +28 -68
- package/.agent/workflows/api-tester.md +151 -151
- package/.agent/workflows/audit.md +127 -138
- package/.agent/workflows/brainstorm.md +110 -110
- package/.agent/workflows/changelog.md +112 -112
- package/.agent/workflows/create.md +124 -124
- package/.agent/workflows/debug.md +165 -189
- package/.agent/workflows/deploy.md +180 -189
- package/.agent/workflows/enhance.md +128 -151
- package/.agent/workflows/fix.md +114 -135
- package/.agent/workflows/generate.md +12 -4
- package/.agent/workflows/migrate.md +160 -160
- package/.agent/workflows/orchestrate.md +168 -168
- package/.agent/workflows/performance-benchmarker.md +114 -123
- package/.agent/workflows/plan.md +173 -173
- package/.agent/workflows/preview.md +80 -80
- package/.agent/workflows/refactor.md +161 -183
- package/.agent/workflows/review-ai.md +101 -129
- package/.agent/workflows/review.md +116 -116
- package/.agent/workflows/session.md +94 -94
- package/.agent/workflows/status.md +79 -79
- package/.agent/workflows/strengthen-skills.md +138 -139
- package/.agent/workflows/swarm.md +179 -179
- package/.agent/workflows/test.md +189 -211
- package/.agent/workflows/tribunal-backend.md +93 -113
- package/.agent/workflows/tribunal-database.md +94 -115
- package/.agent/workflows/tribunal-frontend.md +95 -118
- package/.agent/workflows/tribunal-full.md +92 -133
- package/.agent/workflows/tribunal-mobile.md +94 -119
- package/.agent/workflows/tribunal-performance.md +109 -133
- package/.agent/workflows/ui-ux-pro-max.md +122 -143
- package/package.json +1 -1
- package/.agent/skills/api-patterns/api-style.md +0 -42
- package/.agent/skills/api-patterns/auth.md +0 -24
- package/.agent/skills/api-patterns/documentation.md +0 -26
- package/.agent/skills/api-patterns/graphql.md +0 -41
- package/.agent/skills/api-patterns/rate-limiting.md +0 -31
- package/.agent/skills/api-patterns/response.md +0 -37
- package/.agent/skills/api-patterns/rest.md +0 -40
- package/.agent/skills/api-patterns/security-testing.md +0 -122
- package/.agent/skills/api-patterns/trpc.md +0 -41
- package/.agent/skills/api-patterns/versioning.md +0 -22
- package/.agent/skills/app-builder/agent-coordination.md +0 -71
- package/.agent/skills/app-builder/feature-building.md +0 -53
- package/.agent/skills/app-builder/project-detection.md +0 -34
- package/.agent/skills/app-builder/scaffolding.md +0 -118
- package/.agent/skills/app-builder/tech-stack.md +0 -40
- package/.agent/skills/architecture/context-discovery.md +0 -43
- package/.agent/skills/architecture/examples.md +0 -94
- package/.agent/skills/architecture/pattern-selection.md +0 -68
- package/.agent/skills/architecture/patterns-reference.md +0 -50
- package/.agent/skills/architecture/trade-off-analysis.md +0 -77
- package/.agent/skills/brainstorming/dynamic-questioning.md +0 -360
- package/.agent/skills/database-design/database-selection.md +0 -43
- package/.agent/skills/database-design/indexing.md +0 -39
- package/.agent/skills/database-design/migrations.md +0 -48
- package/.agent/skills/database-design/optimization.md +0 -36
- package/.agent/skills/database-design/orm-selection.md +0 -30
- package/.agent/skills/database-design/schema-design.md +0 -56
- package/.agent/skills/frontend-design/animation-guide.md +0 -331
- package/.agent/skills/frontend-design/color-system.md +0 -329
- package/.agent/skills/frontend-design/decision-trees.md +0 -418
- package/.agent/skills/frontend-design/motion-graphics.md +0 -306
- package/.agent/skills/frontend-design/typography-system.md +0 -363
- package/.agent/skills/frontend-design/ux-psychology.md +0 -1116
- package/.agent/skills/frontend-design/visual-effects.md +0 -383
- package/.agent/skills/intelligent-routing/router-manifest.md +0 -65
- package/.agent/skills/mobile-design/decision-trees.md +0 -516
- package/.agent/skills/mobile-design/mobile-backend.md +0 -491
- package/.agent/skills/mobile-design/mobile-color-system.md +0 -420
- package/.agent/skills/mobile-design/mobile-debugging.md +0 -122
- package/.agent/skills/mobile-design/mobile-design-thinking.md +0 -357
- package/.agent/skills/mobile-design/mobile-navigation.md +0 -458
- package/.agent/skills/mobile-design/mobile-performance.md +0 -767
- package/.agent/skills/mobile-design/mobile-testing.md +0 -356
- package/.agent/skills/mobile-design/mobile-typography.md +0 -433
- package/.agent/skills/mobile-design/platform-android.md +0 -666
- package/.agent/skills/mobile-design/platform-ios.md +0 -561
- package/.agent/skills/mobile-design/touch-psychology.md +0 -537
- package/.agent/skills/nextjs-react-expert/1-async-eliminating-waterfalls.md +0 -312
- package/.agent/skills/nextjs-react-expert/2-bundle-bundle-size-optimization.md +0 -240
- package/.agent/skills/nextjs-react-expert/3-server-server-side-performance.md +0 -490
- package/.agent/skills/nextjs-react-expert/4-client-client-side-data-fetching.md +0 -264
- package/.agent/skills/nextjs-react-expert/5-rerender-re-render-optimization.md +0 -581
- package/.agent/skills/nextjs-react-expert/6-rendering-rendering-performance.md +0 -432
- package/.agent/skills/nextjs-react-expert/7-js-javascript-performance.md +0 -684
- package/.agent/skills/nextjs-react-expert/8-advanced-advanced-patterns.md +0 -150
- package/.agent/skills/vulnerability-scanner/checklists.md +0 -121
|
@@ -1,169 +1,83 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: python-pro
|
|
3
|
-
description:
|
|
3
|
+
description: Python 3.12+ specialist. FastAPI, Pydantic v2, asyncio, modern types, pytest. Use when building Python APIs, data pipelines, automation, or any Python code.
|
|
4
4
|
allowed-tools: Read, Write, Edit, Glob, Grep
|
|
5
|
-
version:
|
|
6
|
-
last-updated: 2026-
|
|
7
|
-
applies-to-model: gemini-2.5-pro, claude-3-7-sonnet
|
|
5
|
+
version: 3.1.0
|
|
6
|
+
last-updated: 2026-04-06
|
|
8
7
|
---
|
|
9
8
|
|
|
10
|
-
# Python
|
|
9
|
+
# Python 3.12+ — Dense Reference
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
## Hallucination Traps (Read First)
|
|
12
|
+
- ❌ `from typing import List, Dict, Optional, Union` → ✅ `list[str]`, `dict[k,v]`, `X | None`, `X | Y` (Python 3.10+)
|
|
13
|
+
- ❌ `user.dict()` / `user.json()` / `UserCreate.parse_obj()` → ✅ Pydantic v2: `model_dump()`, `model_dump_json()`, `model_validate()`
|
|
14
|
+
- ❌ Pydantic `class Config: orm_mode = True` → ✅ `model_config = {"from_attributes": True}`
|
|
15
|
+
- ❌ `@validator` / `@root_validator` → ✅ `@field_validator` / `@model_validator`
|
|
16
|
+
- ❌ `@app.on_event("startup")` → ✅ `lifespan` context manager (deprecated)
|
|
17
|
+
- ❌ `import requests` in async code → ✅ `httpx.AsyncClient()` (requests BLOCKS the event loop)
|
|
18
|
+
- ❌ `asyncio.run()` inside running loop → ✅ `await` directly or use `loop.create_task()`
|
|
19
|
+
- ❌ `except Exception as e: pass` → ✅ always log or re-raise
|
|
14
20
|
|
|
15
21
|
---
|
|
16
22
|
|
|
17
|
-
##
|
|
18
|
-
|
|
19
|
-
### Function Signatures (Mandatory)
|
|
20
|
-
|
|
21
|
-
```python
|
|
22
|
-
# ✅ Every public function MUST have full type hints
|
|
23
|
-
def calculate_tax(amount: float, rate: float = 0.08) -> float:
|
|
24
|
-
"""Calculate tax for a given amount."""
|
|
25
|
-
return amount * rate
|
|
26
|
-
|
|
27
|
-
# ✅ Collections use built-in generics (Python 3.9+)
|
|
28
|
-
def process_items(items: list[str]) -> dict[str, int]:
|
|
29
|
-
return {item: len(item) for item in items}
|
|
30
|
-
|
|
31
|
-
# ❌ HALLUCINATION TRAP: Do NOT import from typing for built-in generics
|
|
32
|
-
# ❌ from typing import List, Dict, Tuple, Set ← LEGACY (Python 3.8)
|
|
33
|
-
# ✅ Use list[str], dict[str, int], tuple[int, ...], set[str] directly
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
### Union Types & Optional
|
|
23
|
+
## Type System (3.12+)
|
|
37
24
|
|
|
38
25
|
```python
|
|
39
|
-
#
|
|
40
|
-
def
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
# ❌ HALLUCINATION TRAP: Do NOT use Optional[X] or Union[X, Y]
|
|
45
|
-
# ❌ from typing import Optional, Union ← LEGACY
|
|
46
|
-
# ✅ Use X | None and X | Y directly (Python 3.10+)
|
|
47
|
-
|
|
48
|
-
# Multiple return types
|
|
49
|
-
def parse_value(raw: str) -> int | float | None:
|
|
50
|
-
try:
|
|
51
|
-
return int(raw)
|
|
52
|
-
except ValueError:
|
|
53
|
-
try:
|
|
54
|
-
return float(raw)
|
|
55
|
-
except ValueError:
|
|
56
|
-
return None
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
### TypedDict
|
|
60
|
-
|
|
61
|
-
```python
|
|
62
|
-
from typing import TypedDict, NotRequired
|
|
26
|
+
# Built-in generics (3.9+) — no typing imports needed for basic types
|
|
27
|
+
def process(items: list[str]) -> dict[str, int]: ...
|
|
28
|
+
def find(user_id: int) -> User | None: ... # 3.10+ union
|
|
29
|
+
def parse(raw: str) -> int | float | None: ...
|
|
63
30
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
role: NotRequired[str]
|
|
69
|
-
|
|
70
|
-
def create_user(data: UserPayload) -> User:
|
|
71
|
-
# TypedDict gives you autocomplete and type checking on dict keys
|
|
72
|
-
return User(name=data["name"], email=data["email"])
|
|
73
|
-
|
|
74
|
-
# Usage:
|
|
75
|
-
payload: UserPayload = {"name": "Alice", "email": "alice@example.com"}
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
### Protocol (Structural Typing)
|
|
31
|
+
# Generic syntax (3.12+)
|
|
32
|
+
def first[T](items: list[T]) -> T | None:
|
|
33
|
+
return items[0] if items else None
|
|
34
|
+
type Point = tuple[float, float] # 3.12+ type alias
|
|
79
35
|
|
|
80
|
-
|
|
36
|
+
# Protocol (structural typing — duck typing with types)
|
|
81
37
|
from typing import Protocol, runtime_checkable
|
|
82
|
-
|
|
83
38
|
@runtime_checkable
|
|
84
39
|
class Renderable(Protocol):
|
|
85
40
|
def render(self) -> str: ...
|
|
86
41
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def render(self) -> str:
|
|
93
|
-
return '{"type": "export"}'
|
|
94
|
-
|
|
95
|
-
# Both satisfy Renderable WITHOUT inheriting from it
|
|
96
|
-
def display(item: Renderable) -> None:
|
|
97
|
-
print(item.render())
|
|
98
|
-
|
|
99
|
-
display(HTMLWidget()) # ✅ works — has render() -> str
|
|
100
|
-
display(JSONExporter()) # ✅ works — has render() -> str
|
|
101
|
-
|
|
102
|
-
# Protocol is Duck Typing with type safety
|
|
103
|
-
# Use Protocol INSTEAD of ABC when you want structural (not nominal) typing
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
### Generic Functions & Classes
|
|
42
|
+
# TypedDict — typed dict with optional keys
|
|
43
|
+
from typing import TypedDict, NotRequired
|
|
44
|
+
class UserPayload(TypedDict):
|
|
45
|
+
name: str; email: str
|
|
46
|
+
age: NotRequired[int] # optional key
|
|
107
47
|
|
|
108
|
-
|
|
48
|
+
# ParamSpec — preserve signatures in decorators
|
|
109
49
|
from typing import TypeVar, ParamSpec
|
|
110
50
|
from collections.abc import Callable
|
|
111
|
-
|
|
112
|
-
T = TypeVar("T")
|
|
113
|
-
P = ParamSpec("P")
|
|
114
|
-
|
|
115
|
-
# Generic function
|
|
116
|
-
def first(items: list[T]) -> T | None:
|
|
117
|
-
return items[0] if items else None
|
|
118
|
-
|
|
119
|
-
# ParamSpec — preserve function signatures in decorators
|
|
51
|
+
T = TypeVar("T"); P = ParamSpec("P")
|
|
120
52
|
def with_logging(func: Callable[P, T]) -> Callable[P, T]:
|
|
121
53
|
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
122
|
-
print(f"Calling {func.__name__}")
|
|
123
54
|
result = func(*args, **kwargs)
|
|
124
|
-
print(f"Result: {result}")
|
|
125
55
|
return result
|
|
126
56
|
return wrapper
|
|
127
|
-
|
|
128
|
-
@with_logging
|
|
129
|
-
def add(a: int, b: int) -> int:
|
|
130
|
-
return a + b
|
|
131
|
-
|
|
132
|
-
# Python 3.12+ — Simplified generic syntax (PEP 695)
|
|
133
|
-
def first_item[T](items: list[T]) -> T | None: # 3.12+ syntax
|
|
134
|
-
return items[0] if items else None
|
|
135
|
-
|
|
136
|
-
type Point = tuple[float, float] # 3.12+ type alias syntax
|
|
137
57
|
```
|
|
138
58
|
|
|
139
59
|
---
|
|
140
60
|
|
|
141
|
-
## Pydantic v2
|
|
142
|
-
|
|
143
|
-
### Models
|
|
61
|
+
## Pydantic v2
|
|
144
62
|
|
|
145
63
|
```python
|
|
146
64
|
from pydantic import BaseModel, Field, field_validator, model_validator
|
|
147
|
-
from datetime import datetime
|
|
148
65
|
from enum import Enum
|
|
149
66
|
|
|
150
67
|
class Role(str, Enum):
|
|
151
|
-
ADMIN = "admin"
|
|
152
|
-
USER = "user"
|
|
153
|
-
MODERATOR = "moderator"
|
|
68
|
+
ADMIN = "admin"; USER = "user"
|
|
154
69
|
|
|
155
70
|
class UserCreate(BaseModel):
|
|
156
71
|
name: str = Field(..., min_length=2, max_length=100)
|
|
157
72
|
email: str = Field(..., pattern=r"^[\w.-]+@[\w.-]+\.\w+$")
|
|
158
73
|
age: int = Field(..., ge=13, le=120)
|
|
159
74
|
role: Role = Role.USER
|
|
160
|
-
tags: list[str] = Field(default_factory=list
|
|
75
|
+
tags: list[str] = Field(default_factory=list)
|
|
161
76
|
|
|
162
77
|
@field_validator("name")
|
|
163
78
|
@classmethod
|
|
164
|
-
def
|
|
165
|
-
if not v[0].isupper():
|
|
166
|
-
raise ValueError("Name must start with uppercase")
|
|
79
|
+
def name_titlecase(cls, v: str) -> str:
|
|
80
|
+
if not v[0].isupper(): raise ValueError("Name must start with uppercase")
|
|
167
81
|
return v.strip()
|
|
168
82
|
|
|
169
83
|
@model_validator(mode="after")
|
|
@@ -173,526 +87,170 @@ class UserCreate(BaseModel):
|
|
|
173
87
|
return self
|
|
174
88
|
|
|
175
89
|
class UserResponse(BaseModel):
|
|
176
|
-
id: int
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
# ❌ class Config:
|
|
186
|
-
# ❌ orm_mode = True ← REMOVED in v2
|
|
187
|
-
# ✅ model_config = {"from_attributes": True}
|
|
188
|
-
|
|
189
|
-
# ❌ HALLUCINATION TRAP: Pydantic v2 validators use @field_validator
|
|
190
|
-
# NOT @validator (v1) or @root_validator (v1)
|
|
191
|
-
# @field_validator replaces @validator
|
|
192
|
-
# @model_validator replaces @root_validator
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
### Serialization
|
|
196
|
-
|
|
197
|
-
```python
|
|
198
|
-
user = UserCreate(name="Alice", email="alice@test.com", age=30)
|
|
199
|
-
|
|
200
|
-
# Serialize to dict
|
|
201
|
-
data = user.model_dump() # ✅ v2
|
|
202
|
-
# ❌ user.dict() ← REMOVED in v2
|
|
203
|
-
|
|
204
|
-
# Serialize to JSON
|
|
205
|
-
json_str = user.model_json_schema() # schema
|
|
206
|
-
json_str = user.model_dump_json() # data as JSON string
|
|
207
|
-
# ❌ user.json() ← REMOVED in v2
|
|
208
|
-
|
|
209
|
-
# Parse from dict
|
|
210
|
-
user = UserCreate.model_validate({"name": "Bob", "email": "bob@test.com", "age": 25})
|
|
211
|
-
# ❌ UserCreate.parse_obj({...}) ← REMOVED in v2
|
|
212
|
-
|
|
213
|
-
# Parse from JSON
|
|
214
|
-
user = UserCreate.model_validate_json('{"name": "Bob", ...}')
|
|
215
|
-
# ❌ UserCreate.parse_raw(...) ← REMOVED in v2
|
|
90
|
+
id: int; name: str; email: str
|
|
91
|
+
model_config = {"from_attributes": True} # ORM mode (was orm_mode=True in v1)
|
|
92
|
+
|
|
93
|
+
# Serialization
|
|
94
|
+
user.model_dump() # ✅ (was .dict())
|
|
95
|
+
user.model_dump_json() # ✅ (was .json())
|
|
96
|
+
user.model_dump(exclude={"password"}, mode="json")
|
|
97
|
+
UserCreate.model_validate({"name": "Alice", "email": "a@b.com", "age": 30}) # ✅ (was parse_obj)
|
|
98
|
+
UserCreate.model_validate_json('{"name": "Bob", ...}')
|
|
216
99
|
```
|
|
217
100
|
|
|
218
101
|
---
|
|
219
102
|
|
|
220
|
-
## FastAPI
|
|
221
|
-
|
|
222
|
-
### Basic Route Structure
|
|
103
|
+
## FastAPI
|
|
223
104
|
|
|
224
105
|
```python
|
|
225
106
|
from fastapi import FastAPI, HTTPException, Depends, Query, Path, status
|
|
226
|
-
from
|
|
107
|
+
from contextlib import asynccontextmanager
|
|
108
|
+
|
|
109
|
+
@asynccontextmanager
|
|
110
|
+
async def lifespan(app: FastAPI):
|
|
111
|
+
await init_db(); await redis.connect() # startup
|
|
112
|
+
yield
|
|
113
|
+
await redis.close() # shutdown
|
|
114
|
+
|
|
115
|
+
app = FastAPI(title="My API", version="1.0.0", lifespan=lifespan)
|
|
227
116
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
)
|
|
234
|
-
|
|
235
|
-
app.add_middleware(
|
|
236
|
-
CORSMiddleware,
|
|
237
|
-
allow_origins=["https://myapp.com"], # ❌ NEVER use ["*"] in production
|
|
238
|
-
allow_credentials=True,
|
|
239
|
-
allow_methods=["GET", "POST", "PUT", "DELETE"],
|
|
240
|
-
allow_headers=["*"],
|
|
241
|
-
)
|
|
117
|
+
# CORS — never "*" in production
|
|
118
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
119
|
+
app.add_middleware(CORSMiddleware,
|
|
120
|
+
allow_origins=["https://myapp.com"], # ❌ NEVER ["*"]
|
|
121
|
+
allow_credentials=True, allow_methods=["GET","POST","PUT","DELETE"], allow_headers=["*"])
|
|
242
122
|
|
|
123
|
+
# Routes
|
|
243
124
|
@app.get("/users", response_model=list[UserResponse])
|
|
244
|
-
async def list_users(
|
|
245
|
-
|
|
246
|
-
limit: int = Query(20, ge=1, le=100),
|
|
247
|
-
role: Role | None = Query(None),
|
|
248
|
-
) -> list[UserResponse]:
|
|
249
|
-
query = select(User)
|
|
250
|
-
if role:
|
|
251
|
-
query = query.where(User.role == role)
|
|
252
|
-
users = await db.execute(query.offset(skip).limit(limit))
|
|
253
|
-
return users.scalars().all()
|
|
254
|
-
|
|
255
|
-
@app.get("/users/{user_id}", response_model=UserResponse)
|
|
256
|
-
async def get_user(user_id: int = Path(..., ge=1)) -> UserResponse:
|
|
257
|
-
user = await db.get(User, user_id)
|
|
258
|
-
if not user:
|
|
259
|
-
raise HTTPException(status_code=404, detail="User not found")
|
|
260
|
-
return user
|
|
125
|
+
async def list_users(skip: int = Query(0, ge=0), limit: int = Query(20, le=100)) -> list[UserResponse]:
|
|
126
|
+
return await db.execute(select(User).offset(skip).limit(limit))
|
|
261
127
|
|
|
262
128
|
@app.post("/users", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
|
|
263
129
|
async def create_user(payload: UserCreate) -> UserResponse:
|
|
264
130
|
user = User(**payload.model_dump())
|
|
265
|
-
db.add(user)
|
|
266
|
-
await db.commit()
|
|
267
|
-
await db.refresh(user)
|
|
131
|
+
db.add(user); await db.commit(); await db.refresh(user)
|
|
268
132
|
return user
|
|
269
|
-
```
|
|
270
133
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
```python
|
|
274
|
-
from fastapi import Depends
|
|
275
|
-
from sqlalchemy.ext.asyncio import AsyncSession
|
|
276
|
-
|
|
277
|
-
# Database session dependency
|
|
134
|
+
# Dependency Injection
|
|
278
135
|
async def get_db() -> AsyncGenerator[AsyncSession, None]:
|
|
279
136
|
async with async_session() as session:
|
|
280
|
-
try:
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
# Auth dependency
|
|
286
|
-
async def get_current_user(
|
|
287
|
-
token: str = Depends(oauth2_scheme),
|
|
288
|
-
db: AsyncSession = Depends(get_db),
|
|
289
|
-
) -> User:
|
|
137
|
+
try: yield session
|
|
138
|
+
finally: await session.close()
|
|
139
|
+
|
|
140
|
+
async def get_current_user(token: str = Depends(oauth2_scheme), db: AsyncSession = Depends(get_db)) -> User:
|
|
290
141
|
payload = decode_jwt(token)
|
|
291
142
|
user = await db.get(User, payload["sub"])
|
|
292
|
-
if not user:
|
|
293
|
-
raise HTTPException(status_code=401, detail="Invalid credentials")
|
|
143
|
+
if not user: raise HTTPException(status_code=401, detail="Invalid credentials")
|
|
294
144
|
return user
|
|
295
145
|
|
|
296
|
-
# Permission dependency
|
|
297
146
|
def require_role(role: Role):
|
|
298
147
|
async def checker(user: User = Depends(get_current_user)) -> User:
|
|
299
|
-
if user.role != role:
|
|
300
|
-
raise HTTPException(status_code=403, detail="Insufficient permissions")
|
|
148
|
+
if user.role != role: raise HTTPException(status_code=403, detail="Forbidden")
|
|
301
149
|
return user
|
|
302
150
|
return checker
|
|
303
151
|
|
|
304
|
-
#
|
|
305
|
-
@app.delete("/users/{user_id}")
|
|
306
|
-
async def delete_user(
|
|
307
|
-
user_id: int,
|
|
308
|
-
db: AsyncSession = Depends(get_db),
|
|
309
|
-
admin: User = Depends(require_role(Role.ADMIN)),
|
|
310
|
-
) -> dict:
|
|
311
|
-
user = await db.get(User, user_id)
|
|
312
|
-
if not user:
|
|
313
|
-
raise HTTPException(status_code=404, detail="Not found")
|
|
314
|
-
await db.delete(user)
|
|
315
|
-
await db.commit()
|
|
316
|
-
return {"deleted": user_id}
|
|
317
|
-
```
|
|
318
|
-
|
|
319
|
-
### Background Tasks & Lifespan
|
|
320
|
-
|
|
321
|
-
```python
|
|
322
|
-
from contextlib import asynccontextmanager
|
|
152
|
+
# Background Tasks
|
|
323
153
|
from fastapi import BackgroundTasks
|
|
324
|
-
|
|
325
|
-
# Lifespan (replaces @app.on_event — which is deprecated)
|
|
326
|
-
@asynccontextmanager
|
|
327
|
-
async def lifespan(app: FastAPI):
|
|
328
|
-
# Startup
|
|
329
|
-
await init_db()
|
|
330
|
-
await redis.connect()
|
|
331
|
-
print("App started")
|
|
332
|
-
yield
|
|
333
|
-
# Shutdown
|
|
334
|
-
await redis.close()
|
|
335
|
-
print("App stopped")
|
|
336
|
-
|
|
337
|
-
app = FastAPI(lifespan=lifespan)
|
|
338
|
-
|
|
339
|
-
# ❌ HALLUCINATION TRAP: @app.on_event("startup") is DEPRECATED in FastAPI
|
|
340
|
-
# ✅ Use the lifespan context manager instead
|
|
341
|
-
|
|
342
|
-
# Background tasks
|
|
343
154
|
@app.post("/orders")
|
|
344
|
-
async def create_order(
|
|
345
|
-
order: OrderCreate,
|
|
346
|
-
background_tasks: BackgroundTasks,
|
|
347
|
-
) -> OrderResponse:
|
|
155
|
+
async def create_order(order: OrderCreate, bg: BackgroundTasks) -> OrderResponse:
|
|
348
156
|
result = await save_order(order)
|
|
349
|
-
|
|
350
|
-
background_tasks.add_task(update_inventory, result.items)
|
|
157
|
+
bg.add_task(send_email, result.email)
|
|
351
158
|
return result
|
|
352
|
-
```
|
|
353
|
-
|
|
354
|
-
### Error Handling
|
|
355
159
|
|
|
356
|
-
|
|
357
|
-
from fastapi import Request
|
|
160
|
+
# Exception handlers
|
|
358
161
|
from fastapi.responses import JSONResponse
|
|
359
|
-
|
|
360
|
-
class AppError(Exception):
|
|
361
|
-
def __init__(self, message: str, status_code: int = 400):
|
|
362
|
-
self.message = message
|
|
363
|
-
self.status_code = status_code
|
|
364
|
-
|
|
365
162
|
@app.exception_handler(AppError)
|
|
366
|
-
async def
|
|
367
|
-
return JSONResponse(
|
|
368
|
-
status_code=exc.status_code,
|
|
369
|
-
content={"error": exc.message, "path": str(request.url)},
|
|
370
|
-
)
|
|
371
|
-
|
|
372
|
-
@app.exception_handler(Exception)
|
|
373
|
-
async def unhandled_error_handler(request: Request, exc: Exception) -> JSONResponse:
|
|
374
|
-
# Log the full traceback — critical for debugging
|
|
375
|
-
import traceback
|
|
376
|
-
traceback.print_exc()
|
|
377
|
-
return JSONResponse(
|
|
378
|
-
status_code=500,
|
|
379
|
-
content={"error": "Internal server error"},
|
|
380
|
-
)
|
|
163
|
+
async def app_error(request: Request, exc: AppError) -> JSONResponse:
|
|
164
|
+
return JSONResponse(status_code=exc.status_code, content={"error": exc.message})
|
|
381
165
|
```
|
|
382
166
|
|
|
383
167
|
---
|
|
384
168
|
|
|
385
|
-
## Async
|
|
386
|
-
|
|
387
|
-
### asyncio Patterns
|
|
169
|
+
## Async Patterns
|
|
388
170
|
|
|
389
171
|
```python
|
|
390
|
-
import asyncio
|
|
391
|
-
import httpx
|
|
172
|
+
import asyncio, httpx
|
|
392
173
|
|
|
393
|
-
#
|
|
394
|
-
async def
|
|
174
|
+
# Parallel calls — await all simultaneously
|
|
175
|
+
async def fetch_all() -> tuple:
|
|
395
176
|
async with httpx.AsyncClient() as client:
|
|
396
|
-
users, posts
|
|
397
|
-
client.get("
|
|
398
|
-
client.get("https://api.example.com/posts"),
|
|
399
|
-
client.get("https://api.example.com/analytics"),
|
|
177
|
+
users, posts = await asyncio.gather(
|
|
178
|
+
client.get("/users"), client.get("/posts")
|
|
400
179
|
)
|
|
401
|
-
return users.json(), posts.json()
|
|
402
|
-
|
|
403
|
-
#
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
#
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
async
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
results = []
|
|
424
|
-
async with asyncio.TaskGroup() as tg:
|
|
425
|
-
task1 = tg.create_task(fetch_users())
|
|
426
|
-
task2 = tg.create_task(fetch_posts())
|
|
427
|
-
# Both tasks guaranteed to complete or all cancel on error
|
|
428
|
-
return task1.result(), task2.result()
|
|
429
|
-
```
|
|
430
|
-
|
|
431
|
-
### Async Files & Databases
|
|
432
|
-
|
|
433
|
-
```python
|
|
434
|
-
import aiofiles
|
|
435
|
-
|
|
436
|
-
# ✅ Async file I/O
|
|
437
|
-
async def read_config(path: str) -> dict:
|
|
438
|
-
async with aiofiles.open(path, "r") as f:
|
|
439
|
-
content = await f.read()
|
|
440
|
-
return json.loads(content)
|
|
441
|
-
|
|
442
|
-
# ❌ HALLUCINATION TRAP: Do NOT use open() in async contexts
|
|
443
|
-
# open() blocks the event loop — use aiofiles instead
|
|
444
|
-
|
|
445
|
-
# Blocking code in async context — use to_thread
|
|
446
|
-
import asyncio
|
|
447
|
-
|
|
448
|
-
async def process_image(path: str) -> bytes:
|
|
449
|
-
# CPU-bound work — offload to thread pool
|
|
450
|
-
return await asyncio.to_thread(heavy_image_processing, path)
|
|
451
|
-
```
|
|
452
|
-
|
|
453
|
-
---
|
|
454
|
-
|
|
455
|
-
## Dataclasses & Patterns
|
|
456
|
-
|
|
457
|
-
### Modern Dataclasses
|
|
458
|
-
|
|
459
|
-
```python
|
|
460
|
-
from dataclasses import dataclass, field
|
|
461
|
-
from datetime import datetime
|
|
462
|
-
|
|
463
|
-
@dataclass(frozen=True, slots=True) # immutable + memory efficient
|
|
464
|
-
class Point:
|
|
465
|
-
x: float
|
|
466
|
-
y: float
|
|
467
|
-
|
|
468
|
-
def distance_to(self, other: "Point") -> float:
|
|
469
|
-
return ((self.x - other.x) ** 2 + (self.y - other.y) ** 2) ** 0.5
|
|
470
|
-
|
|
471
|
-
@dataclass(slots=True)
|
|
472
|
-
class Task:
|
|
473
|
-
title: str
|
|
474
|
-
description: str = ""
|
|
475
|
-
tags: list[str] = field(default_factory=list)
|
|
476
|
-
created_at: datetime = field(default_factory=datetime.now)
|
|
477
|
-
_internal: str = field(default="", repr=False, init=False)
|
|
478
|
-
|
|
479
|
-
# ✅ slots=True — faster attribute access, less memory
|
|
480
|
-
# ✅ frozen=True — immutable (hashable, safe for sets/dicts)
|
|
481
|
-
# ✅ field(default_factory=list) — never use mutable defaults
|
|
482
|
-
|
|
483
|
-
# ❌ HALLUCINATION TRAP: NEVER use mutable default arguments
|
|
484
|
-
# ❌ def __init__(self, tags: list[str] = []): ← BUG (shared reference)
|
|
485
|
-
# ✅ def __init__(self, tags: list[str] | None = None):
|
|
486
|
-
# self.tags = tags or []
|
|
487
|
-
```
|
|
488
|
-
|
|
489
|
-
### Pattern Matching (3.10+)
|
|
490
|
-
|
|
491
|
-
```python
|
|
492
|
-
# Structural pattern matching — NOT just a switch statement
|
|
493
|
-
def handle_command(command: dict) -> str:
|
|
494
|
-
match command:
|
|
495
|
-
case {"action": "create", "name": str(name), "type": str(kind)}:
|
|
496
|
-
return f"Creating {kind}: {name}"
|
|
497
|
-
|
|
498
|
-
case {"action": "delete", "id": int(item_id)}:
|
|
499
|
-
return f"Deleting item {item_id}"
|
|
500
|
-
|
|
501
|
-
case {"action": "list", "filter": {"status": str(status)}}:
|
|
502
|
-
return f"Listing items with status: {status}"
|
|
503
|
-
|
|
504
|
-
case {"action": "list"}:
|
|
505
|
-
return "Listing all items"
|
|
506
|
-
|
|
507
|
-
case _:
|
|
508
|
-
raise ValueError(f"Unknown command: {command}")
|
|
509
|
-
|
|
510
|
-
# With guards
|
|
511
|
-
def classify_response(status_code: int) -> str:
|
|
512
|
-
match status_code:
|
|
513
|
-
case code if 200 <= code < 300:
|
|
514
|
-
return "success"
|
|
515
|
-
case 404:
|
|
516
|
-
return "not_found"
|
|
517
|
-
case code if 400 <= code < 500:
|
|
518
|
-
return "client_error"
|
|
519
|
-
case code if 500 <= code < 600:
|
|
520
|
-
return "server_error"
|
|
521
|
-
case _:
|
|
522
|
-
return "unknown"
|
|
180
|
+
return users.json(), posts.json()
|
|
181
|
+
|
|
182
|
+
# Timeout
|
|
183
|
+
async with asyncio.timeout(5.0): # 3.11+ (was asyncio.wait_for)
|
|
184
|
+
result = await slow_operation()
|
|
185
|
+
|
|
186
|
+
# Semaphore — limit concurrent ops
|
|
187
|
+
sem = asyncio.Semaphore(10)
|
|
188
|
+
async def limited_fetch(url: str) -> dict:
|
|
189
|
+
async with sem:
|
|
190
|
+
async with httpx.AsyncClient() as client:
|
|
191
|
+
return (await client.get(url)).json()
|
|
192
|
+
|
|
193
|
+
# Producer-Consumer
|
|
194
|
+
async def producer(q: asyncio.Queue[str]):
|
|
195
|
+
for item in data: await q.put(item)
|
|
196
|
+
await q.put(None) # sentinel
|
|
197
|
+
|
|
198
|
+
async def consumer(q: asyncio.Queue[str]):
|
|
199
|
+
while (item := await q.get()) is not None:
|
|
200
|
+
await process(item)
|
|
201
|
+
q.task_done()
|
|
523
202
|
```
|
|
524
203
|
|
|
525
204
|
---
|
|
526
205
|
|
|
527
|
-
##
|
|
206
|
+
## Error Handling
|
|
528
207
|
|
|
529
208
|
```python
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
# Usage:
|
|
544
|
-
with timer("Database query"):
|
|
545
|
-
results = db.execute(query)
|
|
546
|
-
|
|
547
|
-
# Async context manager
|
|
209
|
+
# NEVER silently swallow exceptions
|
|
210
|
+
try: result = await risky_op()
|
|
211
|
+
except SpecificError as e: logger.error("Failed: %s", e); raise
|
|
212
|
+
except Exception: logger.exception("Unexpected"); raise
|
|
213
|
+
|
|
214
|
+
# Custom exceptions with context
|
|
215
|
+
class ServiceError(Exception):
|
|
216
|
+
def __init__(self, msg: str, code: int = 500, context: dict | None = None):
|
|
217
|
+
super().__init__(msg)
|
|
218
|
+
self.code = code; self.context = context or {}
|
|
219
|
+
|
|
220
|
+
# Context managers for cleanup
|
|
221
|
+
from contextlib import asynccontextmanager
|
|
548
222
|
@asynccontextmanager
|
|
549
|
-
async def
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
except Exception:
|
|
554
|
-
await db.rollback()
|
|
555
|
-
raise
|
|
556
|
-
finally:
|
|
557
|
-
await db.close()
|
|
558
|
-
|
|
559
|
-
# Usage:
|
|
560
|
-
async with managed_transaction(session) as db:
|
|
561
|
-
db.add(user)
|
|
562
|
-
```
|
|
563
|
-
|
|
564
|
-
---
|
|
565
|
-
|
|
566
|
-
## Decorators
|
|
567
|
-
|
|
568
|
-
```python
|
|
569
|
-
import functools
|
|
570
|
-
import time
|
|
571
|
-
from collections.abc import Callable
|
|
572
|
-
from typing import ParamSpec, TypeVar
|
|
573
|
-
|
|
574
|
-
P = ParamSpec("P")
|
|
575
|
-
T = TypeVar("T")
|
|
576
|
-
|
|
577
|
-
# Retry decorator with exponential backoff
|
|
578
|
-
def retry(max_attempts: int = 3, delay: float = 1.0):
|
|
579
|
-
def decorator(func: Callable[P, T]) -> Callable[P, T]:
|
|
580
|
-
@functools.wraps(func)
|
|
581
|
-
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
582
|
-
last_error: Exception | None = None
|
|
583
|
-
for attempt in range(max_attempts):
|
|
584
|
-
try:
|
|
585
|
-
return func(*args, **kwargs)
|
|
586
|
-
except Exception as e:
|
|
587
|
-
last_error = e
|
|
588
|
-
if attempt < max_attempts - 1:
|
|
589
|
-
sleep_time = delay * (2 ** attempt)
|
|
590
|
-
time.sleep(sleep_time)
|
|
591
|
-
raise last_error # type: ignore[misc]
|
|
592
|
-
return wrapper
|
|
593
|
-
return decorator
|
|
594
|
-
|
|
595
|
-
@retry(max_attempts=3, delay=0.5)
|
|
596
|
-
def flaky_api_call(url: str) -> dict:
|
|
597
|
-
response = httpx.get(url)
|
|
598
|
-
response.raise_for_status()
|
|
599
|
-
return response.json()
|
|
600
|
-
|
|
601
|
-
# Cache decorator (Python 3.9+ — replaces lru_cache for methods)
|
|
602
|
-
from functools import cache
|
|
603
|
-
|
|
604
|
-
@cache # unbounded cache (use lru_cache(maxsize=N) to limit)
|
|
605
|
-
def fibonacci(n: int) -> int:
|
|
606
|
-
if n < 2:
|
|
607
|
-
return n
|
|
608
|
-
return fibonacci(n - 1) + fibonacci(n - 2)
|
|
223
|
+
async def managed_connection():
|
|
224
|
+
conn = await db.connect()
|
|
225
|
+
try: yield conn
|
|
226
|
+
finally: await conn.close()
|
|
609
227
|
```
|
|
610
228
|
|
|
611
229
|
---
|
|
612
230
|
|
|
613
|
-
## Testing
|
|
614
|
-
|
|
615
|
-
### Test Structure
|
|
231
|
+
## Testing (pytest)
|
|
616
232
|
|
|
617
233
|
```python
|
|
618
234
|
import pytest
|
|
619
235
|
from httpx import AsyncClient, ASGITransport
|
|
620
|
-
from unittest.mock import AsyncMock, patch
|
|
621
236
|
|
|
622
|
-
# Fixtures
|
|
623
237
|
@pytest.fixture
|
|
624
|
-
def
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
@pytest.fixture
|
|
628
|
-
async def async_client(app: FastAPI) -> AsyncGenerator[AsyncClient, None]:
|
|
629
|
-
transport = ASGITransport(app=app)
|
|
630
|
-
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
|
631
|
-
yield client
|
|
632
|
-
|
|
633
|
-
# ❌ HALLUCINATION TRAP: Do NOT use TestClient for async FastAPI tests
|
|
634
|
-
# TestClient is synchronous. Use httpx.AsyncClient with ASGITransport.
|
|
635
|
-
|
|
636
|
-
# Basic test
|
|
637
|
-
def test_calculate_tax():
|
|
638
|
-
assert calculate_tax(100, 0.08) == pytest.approx(8.0)
|
|
639
|
-
|
|
640
|
-
# Async test
|
|
641
|
-
@pytest.mark.anyio # or @pytest.mark.asyncio
|
|
642
|
-
async def test_create_user(async_client: AsyncClient):
|
|
643
|
-
response = await async_client.post("/users", json={
|
|
644
|
-
"name": "Alice",
|
|
645
|
-
"email": "alice@test.com",
|
|
646
|
-
"age": 30,
|
|
647
|
-
})
|
|
648
|
-
assert response.status_code == 201
|
|
649
|
-
data = response.json()
|
|
650
|
-
assert data["name"] == "Alice"
|
|
651
|
-
|
|
652
|
-
# Parametrize
|
|
653
|
-
@pytest.mark.parametrize("input_val,expected", [
|
|
654
|
-
(0, "zero"),
|
|
655
|
-
(1, "positive"),
|
|
656
|
-
(-1, "negative"),
|
|
657
|
-
])
|
|
658
|
-
def test_classify(input_val: int, expected: str):
|
|
659
|
-
assert classify(input_val) == expected
|
|
660
|
-
|
|
661
|
-
# Mocking
|
|
662
|
-
async def test_external_api_call():
|
|
663
|
-
with patch("myapp.services.httpx.AsyncClient.get", new_callable=AsyncMock) as mock:
|
|
664
|
-
mock.return_value = httpx.Response(200, json={"data": "mocked"})
|
|
665
|
-
result = await fetch_external_data()
|
|
666
|
-
assert result == {"data": "mocked"}
|
|
667
|
-
mock.assert_called_once()
|
|
668
|
-
|
|
669
|
-
# Exception testing
|
|
670
|
-
def test_invalid_age_raises():
|
|
671
|
-
with pytest.raises(ValueError, match="Admins must be 18"):
|
|
672
|
-
UserCreate(name="Kid", email="kid@test.com", age=15, role=Role.ADMIN)
|
|
673
|
-
```
|
|
674
|
-
|
|
675
|
-
### conftest.py Patterns
|
|
676
|
-
|
|
677
|
-
```python
|
|
678
|
-
# conftest.py — shared fixtures across all tests
|
|
679
|
-
import pytest
|
|
680
|
-
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
|
681
|
-
|
|
682
|
-
@pytest.fixture(scope="session")
|
|
683
|
-
def engine():
|
|
684
|
-
return create_async_engine("sqlite+aiosqlite:///:memory:")
|
|
238
|
+
async def client():
|
|
239
|
+
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as c:
|
|
240
|
+
yield c
|
|
685
241
|
|
|
686
|
-
@pytest.
|
|
687
|
-
async def
|
|
688
|
-
|
|
689
|
-
|
|
242
|
+
@pytest.mark.anyio
|
|
243
|
+
async def test_create_user(client: AsyncClient):
|
|
244
|
+
r = await client.post("/users", json={"name": "Alice", "email": "a@b.com", "age": 25})
|
|
245
|
+
assert r.status_code == 201
|
|
246
|
+
assert r.json()["name"] == "Alice"
|
|
690
247
|
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
async
|
|
695
|
-
await
|
|
248
|
+
# Fixtures with factories (avoid fixtures that return complex data directly)
|
|
249
|
+
@pytest.fixture
|
|
250
|
+
def make_user(db_session):
|
|
251
|
+
async def _make(name="Alice", role="user"):
|
|
252
|
+
return await User.create(db=db_session, name=name, role=role)
|
|
253
|
+
return _make
|
|
696
254
|
```
|
|
697
255
|
|
|
698
256
|
---
|
|
@@ -700,180 +258,16 @@ async def db_session(engine) -> AsyncGenerator[AsyncSession, None]:
|
|
|
700
258
|
## Project Structure
|
|
701
259
|
|
|
702
260
|
```
|
|
703
|
-
|
|
704
|
-
├──
|
|
705
|
-
├──
|
|
706
|
-
│
|
|
707
|
-
│
|
|
708
|
-
│
|
|
709
|
-
│
|
|
710
|
-
│
|
|
711
|
-
│
|
|
712
|
-
│ ├── services/ ← Business logic
|
|
713
|
-
│ ├── repositories/ ← Database access layer
|
|
714
|
-
│ └── utils/ ← Shared utilities
|
|
261
|
+
my-api/
|
|
262
|
+
├── app/
|
|
263
|
+
│ ├── main.py # FastAPI app + lifespan
|
|
264
|
+
│ ├── models/ # SQLAlchemy ORM models
|
|
265
|
+
│ ├── schemas/ # Pydantic request/response models
|
|
266
|
+
│ ├── routers/ # APIRouter groups
|
|
267
|
+
│ ├── services/ # Business logic (no FastAPI imports)
|
|
268
|
+
│ ├── dependencies.py # Shared Depends() callables
|
|
269
|
+
│ └── config.py # Settings via pydantic-settings
|
|
715
270
|
├── tests/
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
│ ├── test_services/
|
|
719
|
-
│ └── test_models/
|
|
720
|
-
├── alembic/ ← Database migrations
|
|
721
|
-
│ └── versions/
|
|
722
|
-
└── .env ← Environment variables (git-ignored)
|
|
271
|
+
├── alembic/ # Migrations
|
|
272
|
+
└── pyproject.toml
|
|
723
273
|
```
|
|
724
|
-
|
|
725
|
-
### Settings with pydantic-settings
|
|
726
|
-
|
|
727
|
-
```python
|
|
728
|
-
from pydantic_settings import BaseSettings
|
|
729
|
-
from pydantic import Field
|
|
730
|
-
|
|
731
|
-
class Settings(BaseSettings):
|
|
732
|
-
database_url: str = Field(..., alias="DATABASE_URL")
|
|
733
|
-
redis_url: str = Field("redis://localhost:6379", alias="REDIS_URL")
|
|
734
|
-
jwt_secret: str = Field(..., alias="JWT_SECRET")
|
|
735
|
-
debug: bool = False
|
|
736
|
-
environment: str = "development"
|
|
737
|
-
|
|
738
|
-
model_config = {"env_file": ".env", "env_file_encoding": "utf-8"}
|
|
739
|
-
|
|
740
|
-
# ❌ HALLUCINATION TRAP: pydantic-settings is a SEPARATE package in v2
|
|
741
|
-
# pip install pydantic-settings
|
|
742
|
-
# ❌ from pydantic import BaseSettings ← REMOVED from pydantic v2 core
|
|
743
|
-
# ✅ from pydantic_settings import BaseSettings
|
|
744
|
-
|
|
745
|
-
settings = Settings() # reads from .env and environment variables
|
|
746
|
-
```
|
|
747
|
-
|
|
748
|
-
---
|
|
749
|
-
|
|
750
|
-
## Common Idioms
|
|
751
|
-
|
|
752
|
-
### Comprehensions & Generators
|
|
753
|
-
|
|
754
|
-
```python
|
|
755
|
-
# Dict comprehension with filtering
|
|
756
|
-
active_users = {u.id: u.name for u in users if u.is_active}
|
|
757
|
-
|
|
758
|
-
# Set comprehension
|
|
759
|
-
unique_domains = {email.split("@")[1] for email in emails}
|
|
760
|
-
|
|
761
|
-
# Generator (lazy — doesn't load all into memory)
|
|
762
|
-
def read_large_file(path: str):
|
|
763
|
-
with open(path) as f:
|
|
764
|
-
for line in f:
|
|
765
|
-
yield line.strip()
|
|
766
|
-
|
|
767
|
-
# Walrus operator (:=) — assign and test
|
|
768
|
-
while chunk := f.read(8192):
|
|
769
|
-
process(chunk)
|
|
770
|
-
|
|
771
|
-
# zip with strict (Python 3.10+)
|
|
772
|
-
for name, score in zip(names, scores, strict=True):
|
|
773
|
-
# Raises ValueError if lengths differ
|
|
774
|
-
print(f"{name}: {score}")
|
|
775
|
-
```
|
|
776
|
-
|
|
777
|
-
### Exception Handling
|
|
778
|
-
|
|
779
|
-
```python
|
|
780
|
-
# ✅ CORRECT: Specific exceptions with context
|
|
781
|
-
import logging
|
|
782
|
-
|
|
783
|
-
logger = logging.getLogger(__name__)
|
|
784
|
-
|
|
785
|
-
async def fetch_user(user_id: int) -> User:
|
|
786
|
-
try:
|
|
787
|
-
response = await client.get(f"/users/{user_id}")
|
|
788
|
-
response.raise_for_status()
|
|
789
|
-
return User.model_validate(response.json())
|
|
790
|
-
except httpx.HTTPStatusError as e:
|
|
791
|
-
if e.response.status_code == 404:
|
|
792
|
-
raise UserNotFoundError(f"User {user_id} not found") from e
|
|
793
|
-
logger.error("API error: %s", e.response.text)
|
|
794
|
-
raise
|
|
795
|
-
except httpx.RequestError as e:
|
|
796
|
-
logger.error("Network error fetching user %d: %s", user_id, e)
|
|
797
|
-
raise ServiceUnavailableError("User service unreachable") from e
|
|
798
|
-
|
|
799
|
-
# ❌ NEVER: Bare except or broad Exception catch
|
|
800
|
-
# ❌ except: ← catches SystemExit, KeyboardInterrupt
|
|
801
|
-
# ❌ except Exception: ← too broad
|
|
802
|
-
# ❌ except Exception as e: pass ← silent failure (WORST)
|
|
803
|
-
```
|
|
804
|
-
|
|
805
|
-
---
|
|
806
|
-
|
|
807
|
-
## Output Format
|
|
808
|
-
|
|
809
|
-
When this skill produces or reviews code, structure your output as follows:
|
|
810
|
-
|
|
811
|
-
```
|
|
812
|
-
━━━ Python Pro Report ━━━━━━━━━━━━━━━━━━━━━━━━
|
|
813
|
-
Skill: Python Pro
|
|
814
|
-
Python Ver: 3.12+
|
|
815
|
-
Scope: [N files · N functions]
|
|
816
|
-
─────────────────────────────────────────────────
|
|
817
|
-
✅ Passed: [checks that passed, or "All clean"]
|
|
818
|
-
⚠️ Warnings: [non-blocking issues, or "None"]
|
|
819
|
-
❌ Blocked: [blocking issues requiring fix, or "None"]
|
|
820
|
-
─────────────────────────────────────────────────
|
|
821
|
-
VBC status: PENDING → VERIFIED
|
|
822
|
-
Evidence: [test output / lint pass / compile success]
|
|
823
|
-
```
|
|
824
|
-
|
|
825
|
-
**VBC (Verification-Before-Completion) is mandatory.**
|
|
826
|
-
Do not mark status as VERIFIED until concrete terminal evidence is provided.
|
|
827
|
-
|
|
828
|
-
---
|
|
829
|
-
|
|
830
|
-
## 🤖 LLM-Specific Traps
|
|
831
|
-
|
|
832
|
-
AI coding assistants often fall into specific bad habits when generating Python code. These are strictly forbidden:
|
|
833
|
-
|
|
834
|
-
1. **Legacy Type Imports:** Do NOT import `List`, `Dict`, `Tuple`, `Set`, `Optional`, `Union` from `typing`. Use `list[str]`, `dict[str, int]`, `tuple[int, ...]`, `X | None`, `X | Y` directly (Python 3.10+).
|
|
835
|
-
2. **Pydantic v1 Syntax:** Do NOT use `.dict()`, `.json()`, `.parse_obj()`, `@validator`, `@root_validator`, or `class Config:`. Use `.model_dump()`, `.model_dump_json()`, `.model_validate()`, `@field_validator`, `@model_validator`, and `model_config = {}`.
|
|
836
|
-
3. **`BaseSettings` from Pydantic Core:** `BaseSettings` was moved to the `pydantic-settings` package in v2. Import from `pydantic_settings`, not `pydantic`.
|
|
837
|
-
4. **Synchronous I/O in Async:** Never use `requests`, `open()`, or blocking `time.sleep()` inside `async` functions. Use `httpx`, `aiofiles`, and `asyncio.sleep()`.
|
|
838
|
-
5. **Mutable Default Arguments:** Never use `def func(items=[])` or `def func(data={})`. Use `None` and initialize inside the function body.
|
|
839
|
-
6. **Bare `except:`:** Never catch all exceptions. Always catch specific exception types. Never silence errors with `except: pass`.
|
|
840
|
-
7. **`@app.on_event("startup")`:** This is deprecated in FastAPI. Use the `lifespan` context manager.
|
|
841
|
-
8. **`TestClient` for Async Tests:** Use `httpx.AsyncClient` with `ASGITransport` for async FastAPI tests, not the synchronous `TestClient`.
|
|
842
|
-
9. **String-Interpolated SQL:** Never use f-strings or `.format()` to build SQL queries. Always use parameterized queries or ORM methods.
|
|
843
|
-
10. **Missing `from __future__ import annotations`:** If targeting Python 3.9 with 3.10+ type syntax, you need this import. For 3.10+, it's not needed.
|
|
844
|
-
|
|
845
|
-
---
|
|
846
|
-
|
|
847
|
-
## 🏛️ Tribunal Integration (Anti-Hallucination)
|
|
848
|
-
|
|
849
|
-
**Slash command: `/tribunal-backend`**
|
|
850
|
-
**Active reviewers: `logic` · `security` · `dependency` · `type-safety`**
|
|
851
|
-
|
|
852
|
-
### ❌ Forbidden AI Tropes
|
|
853
|
-
|
|
854
|
-
1. **Blind Assumptions:** Never make an assumption without documenting it clearly with `# VERIFY: [reason]`.
|
|
855
|
-
2. **Silent Degradation:** Catching and suppressing exceptions without logging.
|
|
856
|
-
3. **Context Amnesia:** Forgetting the user's Python version, framework, or package manager constraints.
|
|
857
|
-
4. **Over-Engineering:** Creating abstractions, factory patterns, or metaclasses where a simple function suffices.
|
|
858
|
-
|
|
859
|
-
### ✅ Pre-Flight Self-Audit
|
|
860
|
-
|
|
861
|
-
Review these questions before confirming output:
|
|
862
|
-
```
|
|
863
|
-
✅ Are all function signatures fully typed (params + return)?
|
|
864
|
-
✅ Did I use modern syntax (X | None, not Optional[X])?
|
|
865
|
-
✅ Did I use Pydantic v2 methods (.model_dump, not .dict)?
|
|
866
|
-
✅ Is I/O properly awaited or offloaded with asyncio.to_thread?
|
|
867
|
-
✅ Did I catch specific exceptions (not bare except)?
|
|
868
|
-
✅ Did I use parameterized queries (not f-string SQL)?
|
|
869
|
-
✅ Are mutable defaults avoided (no list/dict as default args)?
|
|
870
|
-
✅ Did I use the lifespan pattern (not @app.on_event)?
|
|
871
|
-
✅ Is the code PEP 8 / black / ruff compliant?
|
|
872
|
-
✅ Did I write pytest tests with proper fixtures?
|
|
873
|
-
```
|
|
874
|
-
|
|
875
|
-
### 🛑 Verification-Before-Completion (VBC) Protocol
|
|
876
|
-
|
|
877
|
-
**CRITICAL:** You must follow a strict "evidence-based closeout" state machine.
|
|
878
|
-
- ❌ **Forbidden:** Ending your task because the code "looks pythonic" or lacks syntax errors.
|
|
879
|
-
- ✅ **Required:** You are explicitly forbidden from completing your task without providing **concrete terminal/test evidence** (e.g., passing `pytest` logs, `mypy --strict` success, or `ruff check` pass) proving the code actually runs correctly.
|