Рецепты¶
Готовые паттерны для частых задач.
Общий кортеж ошибок авторизации¶
Ошибки токена/авторизации обычно одни и те же на многих роутах. Вынесите их в кортеж и распаковывайте маркером:
from typing import Final
TOKEN_ERRORS: Final = (RequiredTokenError, InvalidTokenError, WrongTokenTypeError)
@router.get("/me")
def me(user: Annotated[User, Depends(current_user)]) -> Annotated[User, Raises[*TOKEN_ERRORS]]: ...
Все три кода на 401 автоматически станут discriminated oneOf-union.
Ошибки зависимостей без ручной декларации¶
Если ошибки авторизации живут в зависимостях (Depends), включите auto=True — и не декларируйте их вовсе:
router = with_errors(APIRouter(), auto=True)
def current_user(token: Annotated[str, Depends(oauth)]) -> User:
if invalid(token):
raise InvalidTokenError("плохой токен") # найдётся автоматически
...
@router.get("/me")
def me(user: Annotated[User, Depends(current_user)]) -> User:
return user # 401 из зависимости попадёт в responses сам
Фабрика get_or_404¶
Частый паттерн — общий хелпер, поднимающий переданный класс ошибки:
def get_or_404[M](value: M | None, *, error: type[BaseError[Any]]) -> M:
if value is None:
raise error("не найдено")
return value
@router.get("/items/{item_id}")
def get_item(item_id: int) -> Annotated[Item, Raises[NotFoundError]]:
item = get_or_404(repo.find(item_id), error=NotFoundError)
return item
Обходчик (auto и check_raises) понимает эту фабрику: класс ошибки, переданный аргументом вызова, засчитывается как потенциально поднимаемый.
Тестирование контрактов ошибок¶
Одна строка в тесте гарантирует, что декларации не разошлись с кодом:
from fastapi_typed_errors import check_raises
def test_error_contracts() -> None:
assert check_raises(app).ok
Полное тело ответа тоже легко проверить через TestClient:
def test_not_found() -> None:
response = TestClient(app).get("/items/0")
assert response.status_code == 404
assert response.json() == {"code": "NOT_FOUND", "detail": "нет"}
Приложение целиком¶
Оберните роутер приложения, чтобы декораторы app.get(...) тоже понимали Raises:
app = FastAPI()
app.add_exception_handler(BaseError, handle_base_error)
with_errors(app.router, auto=True)
@app.get("/health")
def health() -> dict[str, str]:
return {"status": "ok"}
Дальше: Ограничения — что вне области видимости и почему.