Собрать SDK для нескольких сервисов#

Один корень, несколько адресов.

Адрес сервиса является частью контракта роутера. Общий клиент может обслуживать разные hosts, сохраняя единые transport policy и состояние handler.

# examples/docs/multi_service.py
class BillingService:
    base_url = "https://billing.mail.example"


@dataclass(frozen=True, slots=True, kw_only=True)
class GetBook(HttpOperation[Result]):
    __http__ = Http.get("/books/{book_id}")
    book_id: Path[int]


@dataclass(frozen=True, slots=True, kw_only=True)
class CreateCard(HttpOperation[Result]):
    __http__ = Http.post("/cards", success={201: Result})
    label: JsonField[str]


class BooksApi(SyncApi):
    get = op(GetBook)


class CardsApi(BillingService, SyncApi):
    create = op(CreateCard)


class ShopSdk(SyncRoot):
    books = api_group(BooksApi)
    cards = api_group(CardsApi)


def main() -> None:
    site = GuideSite()
    with guide_client(site) as client:
        sdk = ShopSdk(
            client,
            bindings=(
                bind(BillingService, base_url="https://stage-billing.mail.example"),
            ),
        )
        book = sdk.books.get(book_id=7)
        card = sdk.cards.create(label="main")
    print(f"book: {book.value}")
    print(f"card: {card.value}")
    print(*site.calls, sep="\n")

BooksApi наследует адрес клиента. CardsApi получает адрес от BillingService, а bind(...) заменяет его для stage-сборки без изменения декларации API.

Файл целиком
"""Compose two service routers into one SDK root."""

from dataclasses import dataclass

from pydantic import BaseModel

from eazy_sdk import (
    Http,
    HttpOperation,
    JsonField,
    Path,
    SyncApi,
    SyncRoot,
    api_group,
    bind,
    op,
)
from examples.docs.guide_site import GuideSite, guide_client


class Result(BaseModel):
    value: str


# region docs: multi-service
# examples/docs/multi_service.py
class BillingService:
    base_url = "https://billing.mail.example"


@dataclass(frozen=True, slots=True, kw_only=True)
class GetBook(HttpOperation[Result]):
    __http__ = Http.get("/books/{book_id}")
    book_id: Path[int]


@dataclass(frozen=True, slots=True, kw_only=True)
class CreateCard(HttpOperation[Result]):
    __http__ = Http.post("/cards", success={201: Result})
    label: JsonField[str]


class BooksApi(SyncApi):
    get = op(GetBook)


class CardsApi(BillingService, SyncApi):
    create = op(CreateCard)


class ShopSdk(SyncRoot):
    books = api_group(BooksApi)
    cards = api_group(CardsApi)


def main() -> None:
    site = GuideSite()
    with guide_client(site) as client:
        sdk = ShopSdk(
            client,
            bindings=(
                bind(BillingService, base_url="https://stage-billing.mail.example"),
            ),
        )
        book = sdk.books.get(book_id=7)
        card = sdk.cards.create(label="main")
    print(f"book: {book.value}")
    print(f"card: {card.value}")
    print(*site.calls, sep="\n")
# endregion docs: multi-service


if __name__ == "__main__":
    main()
book: SDK Patterns
card: card-42
GET api.mail.example/books/7
POST stage-billing.mail.example/cards

Порядок выбора адреса#

Приоритет

Источник

1

Абсолютный URL операции

2

base_url из наиболее точного bind(...)

3

base_url роутера или его миксина

4

base_url клиента

Отдельный клиент нужен, когда сервису требуется другой proxy, TLS-профиль, handler, timeout или собственная guard-сессия. Передайте его через bind(Router, client=...); в остальных случаях новый клиент только раздробит cookies, connection pool и управляемое состояние без пользы.

Контрактные defaults роутера включают security, signing, crypto, wire, errors и base_url. Операция может заменить правила, а security=None, signing=() и crypto=None явно отключают соответствующее наследование.

Root(client) не владеет переданным клиентом. Root.from_handler(...) создаёт и закрывает его сам, а клиент из bind(..., client=...) всегда остаётся во владении сборки.

Дальше#