agentic-team-templates 0.12.1 → 0.13.2
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 +1 -0
- package/package.json +1 -1
- package/src/index.js +9 -5
- package/src/index.test.js +2 -1
- package/templates/python-expert/.cursorrules/async-python.md +214 -0
- package/templates/python-expert/.cursorrules/overview.md +174 -0
- package/templates/python-expert/.cursorrules/patterns-and-idioms.md +251 -0
- package/templates/python-expert/.cursorrules/performance.md +208 -0
- package/templates/python-expert/.cursorrules/testing.md +238 -0
- package/templates/python-expert/.cursorrules/tooling.md +240 -0
- package/templates/python-expert/.cursorrules/type-system.md +203 -0
- package/templates/python-expert/.cursorrules/web-and-apis.md +231 -0
- package/templates/python-expert/CLAUDE.md +264 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# Python Tooling
|
|
2
|
+
|
|
3
|
+
Modern Python tooling is fast, integrated, and opinionated. Use it all.
|
|
4
|
+
|
|
5
|
+
## Project Configuration
|
|
6
|
+
|
|
7
|
+
### pyproject.toml (Single Source of Truth)
|
|
8
|
+
|
|
9
|
+
```toml
|
|
10
|
+
[project]
|
|
11
|
+
name = "mypackage"
|
|
12
|
+
version = "1.0.0"
|
|
13
|
+
requires-python = ">=3.12"
|
|
14
|
+
dependencies = [
|
|
15
|
+
"fastapi>=0.110",
|
|
16
|
+
"pydantic>=2.0",
|
|
17
|
+
"sqlalchemy>=2.0",
|
|
18
|
+
"httpx>=0.27",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.optional-dependencies]
|
|
22
|
+
dev = [
|
|
23
|
+
"pytest>=8.0",
|
|
24
|
+
"pytest-asyncio>=0.23",
|
|
25
|
+
"pytest-cov>=5.0",
|
|
26
|
+
"mypy>=1.10",
|
|
27
|
+
"ruff>=0.4",
|
|
28
|
+
"pre-commit>=3.7",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.scripts]
|
|
32
|
+
myapp = "mypackage.cli:main"
|
|
33
|
+
|
|
34
|
+
[build-system]
|
|
35
|
+
requires = ["hatchling"]
|
|
36
|
+
build-backend = "hatchling.build"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## uv (Package Manager)
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# uv is the modern Python package manager — fast, reliable, replaces pip/pip-tools/venv
|
|
43
|
+
uv venv # Create virtual environment
|
|
44
|
+
uv pip install -e ".[dev]" # Install with dev dependencies
|
|
45
|
+
uv pip compile pyproject.toml -o requirements.lock # Lock dependencies
|
|
46
|
+
uv run pytest # Run in the project's environment
|
|
47
|
+
uv tool install ruff # Install CLI tools globally
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Ruff (Linter + Formatter)
|
|
51
|
+
|
|
52
|
+
```toml
|
|
53
|
+
# pyproject.toml
|
|
54
|
+
[tool.ruff]
|
|
55
|
+
target-version = "py312"
|
|
56
|
+
line-length = 100
|
|
57
|
+
|
|
58
|
+
[tool.ruff.lint]
|
|
59
|
+
select = [
|
|
60
|
+
"E", # pycodestyle errors
|
|
61
|
+
"W", # pycodestyle warnings
|
|
62
|
+
"F", # pyflakes
|
|
63
|
+
"I", # isort
|
|
64
|
+
"N", # pep8-naming
|
|
65
|
+
"UP", # pyupgrade
|
|
66
|
+
"B", # flake8-bugbear
|
|
67
|
+
"SIM", # flake8-simplify
|
|
68
|
+
"TCH", # flake8-type-checking
|
|
69
|
+
"RUF", # ruff-specific rules
|
|
70
|
+
"S", # flake8-bandit (security)
|
|
71
|
+
"DTZ", # flake8-datetimez
|
|
72
|
+
"PT", # flake8-pytest-style
|
|
73
|
+
"ERA", # eradicate (commented-out code)
|
|
74
|
+
"ARG", # flake8-unused-arguments
|
|
75
|
+
"PTH", # flake8-use-pathlib
|
|
76
|
+
"PERF", # perflint
|
|
77
|
+
]
|
|
78
|
+
ignore = [
|
|
79
|
+
"E501", # line length handled by formatter
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
[tool.ruff.lint.per-file-ignores]
|
|
83
|
+
"tests/**" = ["S101"] # Allow assert in tests
|
|
84
|
+
|
|
85
|
+
[tool.ruff.format]
|
|
86
|
+
quote-style = "double"
|
|
87
|
+
indent-style = "space"
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
ruff check . # Lint
|
|
92
|
+
ruff check --fix . # Lint with auto-fix
|
|
93
|
+
ruff format . # Format
|
|
94
|
+
ruff format --check . # Check formatting
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## mypy
|
|
98
|
+
|
|
99
|
+
```toml
|
|
100
|
+
[tool.mypy]
|
|
101
|
+
python_version = "3.12"
|
|
102
|
+
strict = true
|
|
103
|
+
warn_return_any = true
|
|
104
|
+
warn_unused_configs = true
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
mypy . # Type check everything
|
|
109
|
+
mypy --strict . # Strictest mode
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## pytest
|
|
113
|
+
|
|
114
|
+
```toml
|
|
115
|
+
[tool.pytest.ini_options]
|
|
116
|
+
testpaths = ["tests"]
|
|
117
|
+
asyncio_mode = "auto"
|
|
118
|
+
addopts = [
|
|
119
|
+
"-ra", # Show summary of all non-passing tests
|
|
120
|
+
"--strict-markers", # Undefined markers are errors
|
|
121
|
+
"--strict-config", # Warn about unknown config options
|
|
122
|
+
"-x", # Stop on first failure during development
|
|
123
|
+
]
|
|
124
|
+
markers = [
|
|
125
|
+
"integration: marks integration tests",
|
|
126
|
+
"slow: marks slow tests",
|
|
127
|
+
]
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
pytest # Run all tests
|
|
132
|
+
pytest tests/unit/ # Run subset
|
|
133
|
+
pytest -k "test_create" # Run by name pattern
|
|
134
|
+
pytest --cov=mypackage --cov-report=html # Coverage
|
|
135
|
+
pytest -m "not integration" # Skip integration tests
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Coverage
|
|
139
|
+
|
|
140
|
+
```toml
|
|
141
|
+
[tool.coverage.run]
|
|
142
|
+
source = ["src/mypackage"]
|
|
143
|
+
branch = true
|
|
144
|
+
|
|
145
|
+
[tool.coverage.report]
|
|
146
|
+
fail_under = 80
|
|
147
|
+
show_missing = true
|
|
148
|
+
exclude_lines = [
|
|
149
|
+
"pragma: no cover",
|
|
150
|
+
"if TYPE_CHECKING:",
|
|
151
|
+
"if __name__ == .__main__.",
|
|
152
|
+
"@overload",
|
|
153
|
+
"raise NotImplementedError",
|
|
154
|
+
]
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Pre-commit
|
|
158
|
+
|
|
159
|
+
```yaml
|
|
160
|
+
# .pre-commit-config.yaml
|
|
161
|
+
repos:
|
|
162
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
163
|
+
rev: v0.4.0
|
|
164
|
+
hooks:
|
|
165
|
+
- id: ruff
|
|
166
|
+
args: [--fix]
|
|
167
|
+
- id: ruff-format
|
|
168
|
+
|
|
169
|
+
- repo: https://github.com/pre-commit/mirrors-mypy
|
|
170
|
+
rev: v1.10.0
|
|
171
|
+
hooks:
|
|
172
|
+
- id: mypy
|
|
173
|
+
additional_dependencies: [types-requests]
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Makefile
|
|
177
|
+
|
|
178
|
+
```makefile
|
|
179
|
+
.PHONY: check test lint format typecheck
|
|
180
|
+
|
|
181
|
+
check: format lint typecheck test
|
|
182
|
+
|
|
183
|
+
format:
|
|
184
|
+
ruff format .
|
|
185
|
+
|
|
186
|
+
lint:
|
|
187
|
+
ruff check .
|
|
188
|
+
|
|
189
|
+
typecheck:
|
|
190
|
+
mypy .
|
|
191
|
+
|
|
192
|
+
test:
|
|
193
|
+
pytest --cov=src/mypackage --cov-report=term-missing
|
|
194
|
+
|
|
195
|
+
test-all:
|
|
196
|
+
pytest -m "" --cov=src/mypackage
|
|
197
|
+
|
|
198
|
+
clean:
|
|
199
|
+
find . -type d -name __pycache__ -exec rm -rf {} +
|
|
200
|
+
find . -type f -name "*.pyc" -delete
|
|
201
|
+
rm -rf .pytest_cache .mypy_cache .ruff_cache dist build *.egg-info
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## CI/CD
|
|
205
|
+
|
|
206
|
+
```yaml
|
|
207
|
+
# .github/workflows/ci.yml
|
|
208
|
+
name: CI
|
|
209
|
+
on: [push, pull_request]
|
|
210
|
+
|
|
211
|
+
jobs:
|
|
212
|
+
check:
|
|
213
|
+
runs-on: ubuntu-latest
|
|
214
|
+
strategy:
|
|
215
|
+
matrix:
|
|
216
|
+
python-version: ["3.12", "3.13"]
|
|
217
|
+
steps:
|
|
218
|
+
- uses: actions/checkout@v4
|
|
219
|
+
- uses: astral-sh/setup-uv@v3
|
|
220
|
+
- run: uv venv && uv pip install -e ".[dev]"
|
|
221
|
+
- run: ruff format --check .
|
|
222
|
+
- run: ruff check .
|
|
223
|
+
- run: mypy .
|
|
224
|
+
- run: pytest --cov --cov-report=xml
|
|
225
|
+
|
|
226
|
+
security:
|
|
227
|
+
runs-on: ubuntu-latest
|
|
228
|
+
steps:
|
|
229
|
+
- uses: actions/checkout@v4
|
|
230
|
+
- run: pip install pip-audit
|
|
231
|
+
- run: pip-audit .
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## Dependency Hygiene
|
|
235
|
+
|
|
236
|
+
- **Pin for applications**: exact versions in lock file
|
|
237
|
+
- **Range for libraries**: `>=1.0,<2.0` in pyproject.toml
|
|
238
|
+
- **Audit regularly**: `pip-audit` for security advisories
|
|
239
|
+
- **Minimize dependencies**: every dep is attack surface and maintenance burden
|
|
240
|
+
- **Prefer stdlib**: `pathlib` over `os.path`, `dataclasses` over attrs (for simple cases), `tomllib` over `toml`
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# Python Type System
|
|
2
|
+
|
|
3
|
+
Modern Python is typed Python. Type hints enable tooling, prevent bugs, and serve as living documentation. `mypy --strict` is the baseline.
|
|
4
|
+
|
|
5
|
+
## Core Typing
|
|
6
|
+
|
|
7
|
+
### Function Signatures
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from collections.abc import Sequence, Mapping, Callable, Awaitable, Iterator
|
|
11
|
+
from typing import Any, TypeVar, overload
|
|
12
|
+
|
|
13
|
+
# All parameters and return types annotated
|
|
14
|
+
def process_items(
|
|
15
|
+
items: Sequence[Item],
|
|
16
|
+
*,
|
|
17
|
+
transform: Callable[[Item], Result],
|
|
18
|
+
max_retries: int = 3,
|
|
19
|
+
) -> list[Result]:
|
|
20
|
+
...
|
|
21
|
+
|
|
22
|
+
# Use None return type explicitly
|
|
23
|
+
def log_event(event: Event) -> None:
|
|
24
|
+
logger.info("event", extra={"event": event})
|
|
25
|
+
|
|
26
|
+
# Async functions
|
|
27
|
+
async def fetch_user(user_id: str) -> User | None:
|
|
28
|
+
...
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Modern Syntax (3.10+)
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
# Union types with |
|
|
35
|
+
def parse(value: str | int) -> Data:
|
|
36
|
+
...
|
|
37
|
+
|
|
38
|
+
# Optional is just T | None
|
|
39
|
+
def find_user(email: str) -> User | None:
|
|
40
|
+
...
|
|
41
|
+
|
|
42
|
+
# Built-in generics — no need to import List, Dict, etc.
|
|
43
|
+
names: list[str] = []
|
|
44
|
+
config: dict[str, Any] = {}
|
|
45
|
+
coordinates: tuple[float, float] = (0.0, 0.0)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### TypeVar and Generics
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from typing import TypeVar, Generic
|
|
52
|
+
|
|
53
|
+
T = TypeVar("T")
|
|
54
|
+
|
|
55
|
+
# Generic container
|
|
56
|
+
class Stack(Generic[T]):
|
|
57
|
+
def __init__(self) -> None:
|
|
58
|
+
self._items: list[T] = []
|
|
59
|
+
|
|
60
|
+
def push(self, item: T) -> None:
|
|
61
|
+
self._items.append(item)
|
|
62
|
+
|
|
63
|
+
def pop(self) -> T:
|
|
64
|
+
if not self._items:
|
|
65
|
+
raise IndexError("pop from empty stack")
|
|
66
|
+
return self._items.pop()
|
|
67
|
+
|
|
68
|
+
# Bounded TypeVar
|
|
69
|
+
from typing import SupportsFloat
|
|
70
|
+
N = TypeVar("N", bound=SupportsFloat)
|
|
71
|
+
|
|
72
|
+
def average(values: Sequence[N]) -> float:
|
|
73
|
+
return sum(float(v) for v in values) / len(values)
|
|
74
|
+
|
|
75
|
+
# Python 3.12+ syntax
|
|
76
|
+
def first[T](items: Sequence[T]) -> T:
|
|
77
|
+
return items[0]
|
|
78
|
+
|
|
79
|
+
class Stack[T]:
|
|
80
|
+
...
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Protocol (Structural Subtyping)
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from typing import Protocol, runtime_checkable
|
|
87
|
+
|
|
88
|
+
# Define what you need, not what you depend on
|
|
89
|
+
class Renderable(Protocol):
|
|
90
|
+
def render(self) -> str: ...
|
|
91
|
+
|
|
92
|
+
class HTMLWidget:
|
|
93
|
+
def render(self) -> str:
|
|
94
|
+
return "<div>widget</div>"
|
|
95
|
+
|
|
96
|
+
# HTMLWidget satisfies Renderable without inheriting from it
|
|
97
|
+
def display(item: Renderable) -> None:
|
|
98
|
+
print(item.render())
|
|
99
|
+
|
|
100
|
+
display(HTMLWidget()) # Works — structural match
|
|
101
|
+
|
|
102
|
+
# runtime_checkable for isinstance() checks
|
|
103
|
+
@runtime_checkable
|
|
104
|
+
class Closeable(Protocol):
|
|
105
|
+
def close(self) -> None: ...
|
|
106
|
+
|
|
107
|
+
if isinstance(resource, Closeable):
|
|
108
|
+
resource.close()
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### TypedDict
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from typing import TypedDict, Required, NotRequired
|
|
115
|
+
|
|
116
|
+
class UserDict(TypedDict):
|
|
117
|
+
id: str
|
|
118
|
+
email: str
|
|
119
|
+
name: Required[str]
|
|
120
|
+
bio: NotRequired[str]
|
|
121
|
+
|
|
122
|
+
# Useful for JSON responses, config dicts, and legacy code
|
|
123
|
+
# Prefer dataclasses/Pydantic for new code
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Literal and Final
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from typing import Literal, Final
|
|
130
|
+
|
|
131
|
+
# Restrict to specific values
|
|
132
|
+
def set_log_level(level: Literal["debug", "info", "warn", "error"]) -> None:
|
|
133
|
+
...
|
|
134
|
+
|
|
135
|
+
# Constants that shouldn't be reassigned
|
|
136
|
+
MAX_RETRIES: Final = 3
|
|
137
|
+
API_VERSION: Final[str] = "v2"
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Overloads
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
from typing import overload
|
|
144
|
+
|
|
145
|
+
@overload
|
|
146
|
+
def get(key: str, default: None = None) -> str | None: ...
|
|
147
|
+
@overload
|
|
148
|
+
def get(key: str, default: str) -> str: ...
|
|
149
|
+
|
|
150
|
+
def get(key: str, default: str | None = None) -> str | None:
|
|
151
|
+
value = store.get(key)
|
|
152
|
+
return value if value is not None else default
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## mypy Configuration
|
|
156
|
+
|
|
157
|
+
```toml
|
|
158
|
+
# pyproject.toml
|
|
159
|
+
[tool.mypy]
|
|
160
|
+
python_version = "3.12"
|
|
161
|
+
strict = true
|
|
162
|
+
warn_return_any = true
|
|
163
|
+
warn_unused_configs = true
|
|
164
|
+
disallow_untyped_defs = true
|
|
165
|
+
disallow_incomplete_defs = true
|
|
166
|
+
check_untyped_defs = true
|
|
167
|
+
no_implicit_optional = true
|
|
168
|
+
warn_redundant_casts = true
|
|
169
|
+
warn_unused_ignores = true
|
|
170
|
+
|
|
171
|
+
# Per-module overrides for third-party libs without stubs
|
|
172
|
+
[[tool.mypy.overrides]]
|
|
173
|
+
module = "some_untyped_lib.*"
|
|
174
|
+
ignore_missing_imports = true
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Anti-Patterns
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
# Never: Any as a crutch
|
|
181
|
+
def process(data: Any) -> Any: # This is just untyped Python with extra steps
|
|
182
|
+
...
|
|
183
|
+
|
|
184
|
+
# Never: type: ignore without explanation
|
|
185
|
+
result = sketchy_call() # type: ignore
|
|
186
|
+
# Better:
|
|
187
|
+
result = sketchy_call() # type: ignore[no-untyped-call] # library lacks stubs
|
|
188
|
+
|
|
189
|
+
# Never: Mutable default in typed signatures
|
|
190
|
+
def append_to(item: str, target: list[str] = []) -> list[str]: # BUG
|
|
191
|
+
...
|
|
192
|
+
# Fix:
|
|
193
|
+
def append_to(item: str, target: list[str] | None = None) -> list[str]:
|
|
194
|
+
if target is None:
|
|
195
|
+
target = []
|
|
196
|
+
target.append(item)
|
|
197
|
+
return target
|
|
198
|
+
|
|
199
|
+
# Never: cast() to lie to the type checker
|
|
200
|
+
from typing import cast
|
|
201
|
+
user = cast(User, random_dict) # This doesn't validate anything at runtime
|
|
202
|
+
# Use Pydantic or manual validation instead
|
|
203
|
+
```
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# Python Web and APIs
|
|
2
|
+
|
|
3
|
+
Patterns for building production-grade web services and APIs in Python.
|
|
4
|
+
|
|
5
|
+
## FastAPI
|
|
6
|
+
|
|
7
|
+
### Application Structure
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from fastapi import FastAPI, Depends, HTTPException, status
|
|
11
|
+
from contextlib import asynccontextmanager
|
|
12
|
+
|
|
13
|
+
@asynccontextmanager
|
|
14
|
+
async def lifespan(app: FastAPI):
|
|
15
|
+
# Startup
|
|
16
|
+
app.state.db = await create_pool(settings.database_url)
|
|
17
|
+
yield
|
|
18
|
+
# Shutdown
|
|
19
|
+
await app.state.db.close()
|
|
20
|
+
|
|
21
|
+
app = FastAPI(
|
|
22
|
+
title="My Service",
|
|
23
|
+
version="1.0.0",
|
|
24
|
+
lifespan=lifespan,
|
|
25
|
+
)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Dependency Injection
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from fastapi import Depends
|
|
32
|
+
from typing import Annotated
|
|
33
|
+
|
|
34
|
+
async def get_db(request: Request) -> AsyncGenerator[Database, None]:
|
|
35
|
+
async with request.app.state.db.acquire() as conn:
|
|
36
|
+
yield Database(conn)
|
|
37
|
+
|
|
38
|
+
async def get_current_user(
|
|
39
|
+
token: Annotated[str, Depends(oauth2_scheme)],
|
|
40
|
+
db: Annotated[Database, Depends(get_db)],
|
|
41
|
+
) -> User:
|
|
42
|
+
user = await db.get_user_by_token(token)
|
|
43
|
+
if user is None:
|
|
44
|
+
raise HTTPException(status_code=401, detail="Invalid token")
|
|
45
|
+
return user
|
|
46
|
+
|
|
47
|
+
# Type alias for common dependencies
|
|
48
|
+
CurrentUser = Annotated[User, Depends(get_current_user)]
|
|
49
|
+
DB = Annotated[Database, Depends(get_db)]
|
|
50
|
+
|
|
51
|
+
@app.get("/users/me")
|
|
52
|
+
async def get_me(user: CurrentUser) -> UserResponse:
|
|
53
|
+
return UserResponse.model_validate(user)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Request/Response Models
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from pydantic import BaseModel, Field, EmailStr, ConfigDict
|
|
60
|
+
|
|
61
|
+
class CreateUserRequest(BaseModel):
|
|
62
|
+
model_config = ConfigDict(strict=True)
|
|
63
|
+
|
|
64
|
+
name: str = Field(min_length=1, max_length=200)
|
|
65
|
+
email: EmailStr
|
|
66
|
+
role: Literal["admin", "user"] = "user"
|
|
67
|
+
|
|
68
|
+
class UserResponse(BaseModel):
|
|
69
|
+
model_config = ConfigDict(from_attributes=True)
|
|
70
|
+
|
|
71
|
+
id: str
|
|
72
|
+
name: str
|
|
73
|
+
email: str
|
|
74
|
+
created_at: datetime
|
|
75
|
+
|
|
76
|
+
class PaginatedResponse(BaseModel, Generic[T]):
|
|
77
|
+
items: list[T]
|
|
78
|
+
total: int
|
|
79
|
+
page: int
|
|
80
|
+
per_page: int
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Error Handling
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from fastapi import Request
|
|
87
|
+
from fastapi.responses import JSONResponse
|
|
88
|
+
|
|
89
|
+
class AppError(Exception):
|
|
90
|
+
def __init__(self, message: str, status_code: int = 500) -> None:
|
|
91
|
+
self.message = message
|
|
92
|
+
self.status_code = status_code
|
|
93
|
+
|
|
94
|
+
class NotFoundError(AppError):
|
|
95
|
+
def __init__(self, entity: str, id: str) -> None:
|
|
96
|
+
super().__init__(f"{entity} not found: {id}", status_code=404)
|
|
97
|
+
|
|
98
|
+
class ValidationError(AppError):
|
|
99
|
+
def __init__(self, message: str) -> None:
|
|
100
|
+
super().__init__(message, status_code=400)
|
|
101
|
+
|
|
102
|
+
@app.exception_handler(AppError)
|
|
103
|
+
async def app_error_handler(request: Request, exc: AppError) -> JSONResponse:
|
|
104
|
+
return JSONResponse(
|
|
105
|
+
status_code=exc.status_code,
|
|
106
|
+
content={"error": exc.message},
|
|
107
|
+
)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Middleware
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
114
|
+
import time
|
|
115
|
+
|
|
116
|
+
class TimingMiddleware(BaseHTTPMiddleware):
|
|
117
|
+
async def dispatch(self, request: Request, call_next):
|
|
118
|
+
start = time.monotonic()
|
|
119
|
+
response = await call_next(request)
|
|
120
|
+
duration = time.monotonic() - start
|
|
121
|
+
response.headers["X-Process-Time"] = f"{duration:.4f}"
|
|
122
|
+
return response
|
|
123
|
+
|
|
124
|
+
app.add_middleware(TimingMiddleware)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Django Patterns
|
|
128
|
+
|
|
129
|
+
### Fat Models, Thin Views
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
# Business logic lives in models and services, not views
|
|
133
|
+
class Order(models.Model):
|
|
134
|
+
status = models.CharField(max_length=20)
|
|
135
|
+
total = models.DecimalField(max_digits=10, decimal_places=2)
|
|
136
|
+
|
|
137
|
+
def can_cancel(self) -> bool:
|
|
138
|
+
return self.status in ("pending", "confirmed")
|
|
139
|
+
|
|
140
|
+
def cancel(self) -> None:
|
|
141
|
+
if not self.can_cancel():
|
|
142
|
+
raise ValueError(f"Cannot cancel order in {self.status} state")
|
|
143
|
+
self.status = "cancelled"
|
|
144
|
+
self.save()
|
|
145
|
+
|
|
146
|
+
# Views are thin — delegate to models/services
|
|
147
|
+
class OrderCancelView(View):
|
|
148
|
+
def post(self, request, order_id):
|
|
149
|
+
order = get_object_or_404(Order, id=order_id)
|
|
150
|
+
try:
|
|
151
|
+
order.cancel()
|
|
152
|
+
except ValueError as e:
|
|
153
|
+
return JsonResponse({"error": str(e)}, status=400)
|
|
154
|
+
return JsonResponse({"status": "cancelled"})
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### QuerySet Optimization
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
# Select only needed fields
|
|
161
|
+
users = User.objects.only("id", "name", "email")
|
|
162
|
+
|
|
163
|
+
# Prefetch related objects to avoid N+1 queries
|
|
164
|
+
orders = Order.objects.select_related("user").prefetch_related("items")
|
|
165
|
+
|
|
166
|
+
# Use exists() instead of count() for boolean checks
|
|
167
|
+
if Order.objects.filter(user=user, status="pending").exists():
|
|
168
|
+
...
|
|
169
|
+
|
|
170
|
+
# Use iterator() for large querysets
|
|
171
|
+
for user in User.objects.all().iterator(chunk_size=1000):
|
|
172
|
+
process(user)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Configuration
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
from pydantic_settings import BaseSettings
|
|
179
|
+
|
|
180
|
+
class Settings(BaseSettings):
|
|
181
|
+
model_config = ConfigDict(env_file=".env", env_file_encoding="utf-8")
|
|
182
|
+
|
|
183
|
+
database_url: str
|
|
184
|
+
redis_url: str = "redis://localhost:6379"
|
|
185
|
+
debug: bool = False
|
|
186
|
+
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR"] = "INFO"
|
|
187
|
+
allowed_origins: list[str] = ["http://localhost:3000"]
|
|
188
|
+
|
|
189
|
+
# Validate at import time — fail fast
|
|
190
|
+
settings = Settings()
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Observability
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
import structlog
|
|
197
|
+
|
|
198
|
+
# Structured logging
|
|
199
|
+
logger = structlog.get_logger()
|
|
200
|
+
|
|
201
|
+
logger.info(
|
|
202
|
+
"request_completed",
|
|
203
|
+
method=request.method,
|
|
204
|
+
path=request.url.path,
|
|
205
|
+
status=response.status_code,
|
|
206
|
+
duration_ms=round(duration * 1000, 2),
|
|
207
|
+
request_id=request.state.request_id,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# Never log sensitive data (passwords, tokens, PII)
|
|
211
|
+
# Never log at ERROR for expected conditions (404, validation failures)
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Anti-Patterns
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
# Never: Business logic in views/routes
|
|
218
|
+
@app.post("/orders")
|
|
219
|
+
async def create_order(data: OrderInput, db: DB) -> OrderResponse:
|
|
220
|
+
# Don't put 50 lines of business logic here
|
|
221
|
+
# Delegate to a service
|
|
222
|
+
return await order_service.create(data)
|
|
223
|
+
|
|
224
|
+
# Never: Raw SQL without parameterization
|
|
225
|
+
cursor.execute(f"SELECT * FROM users WHERE id = '{user_id}'") # SQL INJECTION
|
|
226
|
+
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,)) # Safe
|
|
227
|
+
|
|
228
|
+
# Never: Synchronous HTTP calls in async handlers
|
|
229
|
+
response = requests.get(url) # Blocks the event loop
|
|
230
|
+
response = await httpx_client.get(url) # Non-blocking
|
|
231
|
+
```
|