Skip to content

Cookbook

Capturing HTTP status code

Status code can be «mixed» into a model by using a combination of the Mixin and StatusCode markers:

status_code.py
import pytest
import sys

if sys.version_info < (3, 9):
    pytest.skip("HTTP 418 requires Python 3.9 or higher")

from http import HTTPStatus
from typing import Protocol

from httpx import Client
from pydantic import BaseModel
from typing_extensions import Annotated

from combadge.core.markers import Mixin
from combadge.support.http.markers import StatusCode, http_method, path
from combadge.support.httpx.backends.sync import HttpxBackend


class Response(BaseModel):
    my_status_code: HTTPStatus


class SupportsHttpbin(Protocol):
    @http_method("GET")
    @path("/status/418")
    def get_teapot(self) -> Annotated[Response, Mixin(StatusCode("my_status_code"))]:
        raise NotImplementedError


backend = HttpxBackend(Client(base_url="https://httpbin.org"), raise_for_status=False)
service = backend[SupportsHttpbin]

response = service.get_teapot()
assert response.my_status_code == HTTPStatus.IM_A_TEAPOT

Do not repeat response annotations

Consider the following example. We have a service with 2 methods: foo() and bar().

foo() declares its own response and error models:

class Foo(SuccessfulResponse):
    ...


class FooError(ErrorResponse):
    status_code: Literal[HTTPStatus.IM_A_TEAPOT]

And so does bar():

class Bar(SuccessfulResponse):
    ...


class BarError(ErrorResponse):
    status_code: Literal[HTTPStatus.BAD_REQUEST]

In addition, they both share the following common error models:

class InternalServerError(ErrorResponse):
    status_code: Literal[HTTPStatus.INTERNAL_SERVER_ERROR]


class ServiceUnavailable(ErrorResponse):
    status_code: Literal[HTTPStatus.SERVICE_UNAVAILABLE]

A head-on approach would be to declare the interface as follows:

class Service(Protocol):
    def foo(self) -> Annotated[
        Union[Foo, FooError, InternalServerError, ServiceUnavailable],
        Mixin(StatusCode()),
    ]:
        ...

    def bar(self) -> Annotated[
        Union[Foo, BarError, InternalServerError, ServiceUnavailable],
        Mixin(StatusCode()),
    ]:
        ...

However, that way we would get a bunch of repetitive annotations. It is possible to extract the shared parts using the combination TypeAlias and TypeVar:

ResponseT = TypeVar("ResponseT")
"""Method-specific response type."""

ErrorT = TypeVar("ErrorT")
"""Method-specific error type."""

CommonError: TypeAlias = Union[InternalServerError, ServiceUnavailable]
"""Any common error for all the methods."""

Response: TypeAlias = Annotated[
    Union[ResponseT, ErrorT, CommonError],
    Mixin(StatusCode()),
]


class Service(Protocol):
    def foo(self) -> Response[Foo, FooError]:
        ...

    def bar(self) -> Response[Bar, BarError]:
        ...

Fields for simple requests

There are cases when having a request model is undesired. For example, when a call takes a handful of simple parameters of scalar types.

You can map such parameters with the Field marker, which would mark them as separate root fields of the payload:

field.py
from httpx import Client
from pydantic import BaseModel
from typing_extensions import Annotated, Protocol

from combadge.core.binder import bind
from combadge.support.http.markers import Field, http_method, path
from combadge.support.httpx.backends.sync import HttpxBackend


class Response(BaseModel):
    data: str


class SupportsHttpbin(Protocol):
    @http_method("POST")
    @path("/anything")
    def post(
        self,
        *,
        foo: Annotated[str, Field("foobar")] = "quuuuux",
    ) -> Response:
        raise NotImplementedError


backend = HttpxBackend(Client(base_url="https://httpbin.org"))
service = bind(SupportsHttpbin, backend)

response = service.post()
assert response.data == r"""{"foobar": "quuuuux"}"""

Parameter validation

Combadge supports @pydantic.validate_call via the @wrap_with marker:

from combadge.core.markers.method import wrap_with


class SupportsWttrIn(Protocol):
    @wrap_with(validate_call)
    def get_weather(self, ...) -> ...:
        ...