Refresh examples for modern stack; promote AGENTS.md to canonical agent ruleset (#89)
* Refresh examples for modern stack; promote AGENTS.md to canonical agent ruleset Replaces stale code in README.md (python-jose, async_asgi_testclient, json_encoders, contradictory `Field(ge=18, default=None)`, unused `model_validator` import) with current equivalents (PyJWT, httpx + ASGITransport, `@field_serializer`). Adds a top-of-file pointer to AGENTS.md, a tested-versions line, and inline notes for `Annotated[T, Depends(...)]` and SQLAlchemy 2.0 async without rewriting every example. Adds a `dependency_overrides` testing subsection and a BackgroundTasks vs. task-queue decision table. Rewrites AGENTS.md as a terse, machine-readable mirror: compatibility matrix at the top, Do/Don't blocks throughout, an explicit anti-patterns table covering common AI-agent mistakes, and a quick-reference table. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Sync code-only fixes into README_ZH.md Mirrors the safe code-only changes from the parent commit into the Chinese translation: - python-jose -> PyJWT (import + exception class) in both dependency examples - async_asgi_testclient -> httpx.AsyncClient + ASGITransport (and fixes the previously broken example that mixed both imports) - Pydantic json_encoders -> @field_serializer for the CustomModel example - Field(ge=18, default=None) -> Field(ge=18) (the constraint and the default contradict each other) - Removes unused `model_validator` import No prose changes. Existing Chinese descriptions still accurately describe the new code. Sections newly added in README.md (BackgroundTasks vs task queue, dependency_overrides, the agent banner, the version line, and the Annotated/SQLAlchemy 2.0 inline notes) are not ported here because they require translation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Trim docs/changelog voice from refresh Pass over the refresh to keep the article in best-practices voice, not docs or changelog: - Drop the "Note on syntax" blockquote in README's Dependencies section. It hedged ("Both styles work. New code should prefer Annotated.") while every example below kept the default-arg form. Removing the callout returns the section to its original shape; a full Annotated migration can be its own PR. - Remove the "json_encoders is deprecated in Pydantic v2" inline explainer. The code change already says it. - Rewrite the databases blockquote into a single in-voice line ("For new projects, reach for SQLAlchemy 2.0's async API...") instead of an apologetic disclaimer. - Collapse the "Earlier versions of this article recommended async_asgi_testclient" blockquote into a direct sentence. - Drop the "this file wins — README is narrative, this is the spec" precedence line in AGENTS.md. The two files are peers; neither outranks the other. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
51
README_ZH.md
51
README_ZH.md
@@ -222,7 +222,7 @@ class UserBase(BaseModel):
|
||||
first_name: str = Field(min_length=1, max_length=128)
|
||||
username: str = Field(min_length=1, max_length=128, pattern="^[A-Za-z0-9-_]+$")
|
||||
email: EmailStr
|
||||
age: int = Field(ge=18, default=None) # 必须大于或等于18
|
||||
age: int = Field(ge=18) # 必须大于或等于18
|
||||
favorite_band: MusicBand | None = None # 只允许输入"AEROSMITH"、"QUEEN"、"AC/DC"值
|
||||
website: AnyUrl | None = None
|
||||
```
|
||||
@@ -233,22 +233,22 @@ class UserBase(BaseModel):
|
||||
|
||||
```python
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
def datetime_to_gmt_str(dt: datetime) -> str:
|
||||
if not dt.tzinfo:
|
||||
dt = dt.replace(tzinfo=ZoneInfo("UTC"))
|
||||
|
||||
return dt.strftime("%Y-%m-%dT%H:%M:%S%z")
|
||||
from pydantic import BaseModel, ConfigDict, field_serializer
|
||||
|
||||
class CustomModel(BaseModel):
|
||||
model_config = ConfigDict(
|
||||
json_encoders={datetime: datetime_to_gmt_str},
|
||||
populate_by_name=True,
|
||||
)
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
@field_serializer("*", when_used="json", check_fields=False)
|
||||
def _serialize_datetimes(self, value: Any) -> Any:
|
||||
if isinstance(value, datetime):
|
||||
if value.tzinfo is None:
|
||||
value = value.replace(tzinfo=ZoneInfo("UTC"))
|
||||
return value.strftime("%Y-%m-%dT%H:%M:%S%z")
|
||||
return value
|
||||
|
||||
def serializable_dict(self, **kwargs):
|
||||
"""返回仅包含可序列化字段的字典。"""
|
||||
@@ -285,7 +285,7 @@ class AuthConfig(BaseSettings):
|
||||
auth_settings = AuthConfig()
|
||||
|
||||
# src.config
|
||||
from pydantic import PostgresDsn, RedisDsn, model_validator
|
||||
from pydantic import PostgresDsn, RedisDsn
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
from src.constants import Environment
|
||||
@@ -356,7 +356,8 @@ async def get_post_reviews(post: dict[str, Any] = Depends(valid_post_id)):
|
||||
```python
|
||||
# dependencies.py
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from jose import JWTError, jwt
|
||||
import jwt # PyJWT
|
||||
from jwt.exceptions import InvalidTokenError
|
||||
|
||||
async def valid_post_id(post_id: UUID4) -> dict[str, Any]:
|
||||
post = await service.get_by_id(post_id)
|
||||
@@ -370,7 +371,7 @@ async def parse_jwt_data(
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
payload = jwt.decode(token, "JWT_SECRET", algorithms=["HS256"])
|
||||
except JWTError:
|
||||
except InvalidTokenError:
|
||||
raise InvalidCredentials()
|
||||
|
||||
return {"user_id": payload["id"]}
|
||||
@@ -408,7 +409,8 @@ async def get_user_post(post: dict[str, Any] = Depends(valid_owned_post)):
|
||||
# dependencies.py
|
||||
from fastapi import BackgroundTasks
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from jose import JWTError, jwt
|
||||
import jwt # PyJWT
|
||||
from jwt.exceptions import InvalidTokenError
|
||||
|
||||
async def valid_post_id(post_id: UUID4) -> Mapping:
|
||||
post = await service.get_by_id(post_id)
|
||||
@@ -422,7 +424,7 @@ async def parse_jwt_data(
|
||||
) -> dict:
|
||||
try:
|
||||
payload = jwt.decode(token, "JWT_SECRET", algorithms=["HS256"])
|
||||
except JWTError:
|
||||
except InvalidTokenError:
|
||||
raise InvalidCredentials()
|
||||
|
||||
return {"user_id": payload["id"]}
|
||||
@@ -804,20 +806,21 @@ async def get_creator_posts(creator: dict[str, Any] = Depends(valid_creator_id))
|
||||
使用数据库编写集成测试很可能在将来导致混乱的事件循环错误。立即设置异步测试客户端,例如[httpx](https://github.com/encode/starlette/issues/652)
|
||||
|
||||
```python
|
||||
from typing import AsyncGenerator
|
||||
|
||||
import pytest
|
||||
from async_asgi_testclient import TestClient
|
||||
from httpx import AsyncClient, ASGITransport
|
||||
|
||||
from src.main import app # inited FastAPI app
|
||||
|
||||
@pytest.fixture
|
||||
async def client() -> AsyncGenerator[TestClient, None]:
|
||||
host, port = "127.0.0.1", "9000"
|
||||
|
||||
async with AsyncClient(transport=ASGITransport(app=app, client=(host, port)), base_url="http://test") as client:
|
||||
yield client
|
||||
async def client() -> AsyncGenerator[AsyncClient, None]:
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as ac:
|
||||
yield ac
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_post(client: TestClient):
|
||||
async def test_create_post(client: AsyncClient):
|
||||
resp = await client.post("/posts")
|
||||
|
||||
assert resp.status_code == 201
|
||||
|
||||
Reference in New Issue
Block a user