Разложить сессию по запросу#
Не каждая сессия помещается в один заголовок Authorization. Веб-почта передаёт токен страницы
и адрес ящика в query каждого вызова, а ключ устройства отправляет заголовком. Все три значения
принадлежат одной сессии и появляются после одного входа.
Отметьте поля модели сессии маркером Placed. Схема, вход на первом защищённом вызове и
обновление после 401 строятся из этой модели.
# examples/docs/auth_placed.py
class MailSession(BaseModel):
token: Annotated[SecretStr, Placed.query("token")]
email: Annotated[str, Placed.query("email", secret=False)]
device: Annotated[SecretStr, Placed.header("X-Device")]
MAIL_SESSION = session_scheme(MailSession, name="mail-session")
@dataclass(frozen=True, slots=True, kw_only=True)
class ListFolders(HttpOperation[Folders]):
__http__ = Http.get("/api/devices")
class MailApi(AsyncApi):
security = MAIL_SESSION
folders = op(ListFolders)
session_scheme(MailSession) читает маркеры и возвращает схему. Роутер получает её через
security, а корень SDK связывает схему с учётными данными и сервисом входа одной строкой:
MAIL_SESSION.configure(credentials=..., service=...). Вместо credentials= можно передать
готовую session=, тогда вход не выполняется.
Маркер |
Куда уходит значение |
Тип поля |
|---|---|---|
|
параметр query |
строка или |
|
заголовок |
строка или |
|
поле тела запроса |
строка или |
secret=False снимает скрытие значения в диагностике. Оставьте его только для несекретных
полей, таких как адрес ящика.
Bearer() остаётся коротким написанием для Placed.header("Authorization", prefix="Bearer ").
Оба маркера живут на одной модели, если сервису нужны и токен в заголовке, и что-то ещё.
Куки маркером не размещают. Куки, которые поставил сайт, библиотека несёт сама, когда корень
объявил Cookies(...); об этом рассказывает руководство
о сессии на куках.
Запускаемый файл целиком
"""Place one session into the query and a header of every protected request."""
from __future__ import annotations
import asyncio
from dataclasses import dataclass
from typing import Annotated, Any, Self
import httpx
from pydantic import BaseModel, SecretStr
from eazy_sdk import AsyncApi, AsyncRoot, Http, HttpOperation, Identity, api_group, op
from eazy_sdk.auth import AuthContext, Placed, session_scheme
from eazy_sdk.handlers.httpx import AsyncHttpxHandler
from examples.docs.redirect_site import (
BASE_URL,
DEVICE_KEY,
MAILBOX,
PAGE_TOKEN,
PASSWORD,
RedirectSite,
)
class MailLogin(BaseModel):
username: str
password: SecretStr
class Folders(BaseModel):
folders: list[str]
# region docs: auth-placed-model
# examples/docs/auth_placed.py
class MailSession(BaseModel):
token: Annotated[SecretStr, Placed.query("token")]
email: Annotated[str, Placed.query("email", secret=False)]
device: Annotated[SecretStr, Placed.header("X-Device")]
MAIL_SESSION = session_scheme(MailSession, name="mail-session")
@dataclass(frozen=True, slots=True, kw_only=True)
class ListFolders(HttpOperation[Folders]):
__http__ = Http.get("/api/devices")
class MailApi(AsyncApi):
security = MAIL_SESSION
folders = op(ListFolders)
# endregion docs: auth-placed-model
class MailLoginService:
"""Stands in for a real login: the guide on cookie sessions declares its steps."""
async def acquire(self, credentials: MailLogin, context: AuthContext[Any]) -> MailSession:
return MailSession(
token=SecretStr(PAGE_TOKEN),
email=credentials.username,
device=SecretStr(DEVICE_KEY),
)
class MailSdk(AsyncRoot):
mail = api_group(MailApi)
@classmethod
def open(cls, site: RedirectSite, credentials: MailLogin) -> Self:
auth = MAIL_SESSION.configure(credentials=credentials, service=MailLoginService())
raw = httpx.AsyncClient(transport=httpx.MockTransport(site), headers={}, cookies={})
return cls.from_handler(
handler=AsyncHttpxHandler(raw, owns_client=True),
base_url=BASE_URL,
identity=Identity(auth=(auth,)),
)
@dataclass(slots=True)
class RecordingSite(RedirectSite):
"""The teaching site, remembering what the protected request carried."""
seen: str = ""
def __call__(self, request: httpx.Request) -> httpx.Response:
if request.url.path == "/api/devices":
self.seen = f"{request.url.query.decode()} | X-Device: {request.headers['x-device']}"
return RedirectSite.__call__(self, request)
async def main() -> None:
site = RecordingSite()
credentials = MailLogin(username=MAILBOX, password=SecretStr(PASSWORD))
async with MailSdk.open(site, credentials) as sdk:
folders = await sdk.mail.folders()
print(f"folders: {', '.join(folders.folders)}")
print(f"request: {site.seen}")
if __name__ == "__main__":
asyncio.run(main())
folders: Inbox, Sent
request: token=page-token-42&email=ada%40mail.example | X-Device: device-42
Сессия рядом с параметрами запроса#
Некоторые сайты передают сессию там же, где остальные параметры вызова: в query у чтения и в
форме у записи. Запишите оба места на одном поле через |:
Placed.body("token") | Placed.query("token").
Места читаются слева направо, и для каждой операции берётся первое, которое у неё есть. Query и
заголовки есть у любого запроса. Тело есть у операции, которая объявила плоские поля тела:
Form[...] или JsonField[...]. В такой операции значение сессии становится ещё одним полем
тела в её собственной кодировке, а в query не попадает. Операция без тела получает значение в
query.
Поля сессии встают в конец тела, после полей операции и значений Inject. Если сервер
чувствителен к порядку, задайте его целиком через Wire(order=FieldOrder(body=(...))).
Тело целиком из модели и тело из BodyProjection местом для сессии не считаются. Составное
тело (Part) не поддерживается: такая операция отклоняется до отправки запроса.
Когда сессия считается пригодной#
Сессия с пустым размещаемым значением в запрос не уходит. Пустая строка или None в
отмеченном поле означают, что отправлять нечего, и SDK выполняет вход заново. Поля без маркера на
пригодность не влияют. Срок действия по-прежнему задаёт ExpiresAt.
Что проверяется при объявлении#
session_scheme(...) отклоняет модель сразу, без запроса:
Объявление |
Причина отказа |
|---|---|
два поля с |
одно место запроса принимает одно поле |
|
одно место принимает одно значение |
два поля с |
заголовок авторизации один |
только |
значение некуда положить; отказ приходит на первом вызове операции |
Модель без единого маркера допустима только на сайте, который ведёт сессию на куках. В остальных случаях первый защищённый вызов заканчивается ошибкой настройки: такая сессия ничего не кладёт в запрос.