adapterkit
Тонкий коннектор сетевых адаптеров поверх s-librarykit. adapterkit отвечает на один вопрос: «как подключить сетевой адаптер к приложению». Он даёт декларативный контракт плагина, реестр с автодискавери через entry-points и базовый фасад адаптера — а весь сетевой движок (transport/auth/retry/errmap/ pagination/sessions/antibot/browser) реэкспортирует из librarykit, не дублируя его. Пишется один раз, переиспользуется любым доменным пакетом.
librarykit ← КОРЕНЬ: весь сетевой движок (transport/auth/retry/errmap/
pagination/sessions/antibot/browser)
▲
adapterkit ← ЭТОТ КИТ: контракт NetworkAdapter + registry (entry-points) +
BaseAdapter + orchestration_api.
Остальное — тонкий реэкспорт-шим из librarykit.
▲
домен ← конкретные адаптеры (endpoint-таблица + мапперы на сеть)
Зависимости направлены только внутрь: adapterkit зависит только от
librarykit, но НЕ от домена и НЕ от clikit (онион-граф
librarykit <- adapterkit <- clikit: clikit — слой ВЫШЕ, adapterkit его не
импортит). Адаптеры кодируются против
стабильных typing.Protocol из adapterkit.contract (структурный контракт, а не
наследование от домена). Композиция конкретных реализаций — единственный
composition root на приложение.
Установка
uv add s-adapterkit
Имя дистрибутива — s-adapterkit, имя для импорта — adapterkit. librarykit
подтянется автоматически как транзитивная зависимость.
Опциональные extra:
uv add "s-adapterkit[browser]" # Playwright — browser-login
uv add "s-adapterkit[antibot]" # curl-cffi — JA3-impersonate
uv add "s-adapterkit[oauth]" # authlib — OAuth2-flows
Быстрый старт
Описать адаптер декларативно и зарегистрировать его в реестре:
from adapterkit import BaseAdapter, Endpoint, register_adapter
class TwitterAdapter(BaseAdapter):
api_version = 1
endpoints = {
"search": Endpoint(name="search", method="GET", path="/2/tweets/search/recent"),
}
register_adapter("twitter", TwitterAdapter)
Либо отдать адаптер на автодискавери — объявить entry-point в своём pyproject.toml,
и любой потребитель adapterkit подхватит его без явного импорта:
[project.entry-points."adapterkit.adapters"]
twitter = "my_package.adapter:TwitterAdapter"
from adapterkit import discover_adapters, get_adapter_class
discover_adapters() # загрузить все плагины из entry-points
cls = get_adapter_class("twitter") # получить класс по имени сервиса
Карта модулей
| Модуль | Назначение | Реализация |
|---|---|---|
contract.py |
граничные Protocol (NetworkAdapter/Transport/Auth/ErrorMapper/Paginator/SessionStoreProtocol) + DTO (Endpoint/RequestSpec/SessionRef/Creds) + ADAPTER_API_VERSION/MIN_SUPPORTED_API_VERSION |
контракт коннектора |
registry.py |
AdapterRegistry + автодискавери через entry-points adapterkit.adapters, ленивая загрузка, ручная регистрация |
код коннектора |
base.py |
BaseAdapter (описание запроса build_request + исполнение execute) + ресурс-под-сервисы (ContentResource/CommentsResource/MetricsResource/SearchResource) — Stripe-стиль фасад |
код коннектора |
throttle.py |
ядро само троттлит плагин по его метаданным: @ratelimit, ServiceThrottle, ManagedExecutor, шов ExecutionContext |
код коннектора |
orchestration_api.py |
тонкий registry-driven API: onboard_all / health_check_all (без импортов домена) |
код коннектора |
onboarding_contract.py |
онбординг/health-контракты (LoginMode/OnboardingProtocol/HealthProtocol, api_version 2) |
реэкспорт librarykit.protocols |
errors.py |
единая иерархия ошибок | реэкспорт librarykit.errors |
retry.py |
header-driven RetryPolicy |
реэкспорт librarykit.retry |
transport.py / client.py |
HttpxTransport + choke-point HttpClient |
реэкспорт librarykit.transport |
auth.py |
TokenAuth/OAuth2Auth/CookieSessionAuth/BrowserLoginAuth |
реэкспорт librarykit.auth |
errmap.py |
декларативная карта ответ → доменная ошибка | реэкспорт librarykit.errmap |
pagination.py |
CursorPaginator (offset/cursor/page) |
реэкспорт librarykit.pagination |
sessions.py |
envelope-шифрованный SessionStore |
реэкспорт librarykit.sessions |
antibot.py |
выбор транспорта Tier 0-4 (curl-cffi JA3 / CDP) | ленивый реэкспорт librarykit.antibot |
browser.py |
warm/cold-login (требует extra browser) |
ленивый реэкспорт librarykit.browser |
endpoints.py |
EndpointRegistry — адреса скрытого API как данные, проба живости |
код коннектора |
clientkit/ |
СБОРЩИК клиента по декларации сервиса (TOML): WhiteApiClient/HiddenApiClient, каталог имён сервисов (clientkit.identity), адреса-кандидаты поверх EndpointRegistry (clientkit._candidates) — extras client/client-hidden (переехал из отдельного кита clientkit, волна 5 #2710) |
код коннектора |
Всё, что помечено «реэкспорт», — тонкий shim: единая реализация живёт в librarykit,
adapterkit лишь предоставляет её под привычным именем. Собственный код коннектора —
только contract/registry/base/throttle/orchestration_api.
Ядро само держит лимиты плагина
Плагин объявляет лимиты ОДНОЙ строкой метаданных и не пишет кода лимитов, ретраев и удержания сессии — очередь запросов и exponential backoff делает ядро:
from adapterkit import BaseAdapter, Endpoint, ratelimit
@ratelimit(calls=2, period=1) # ← всё, что плагин пишет про лимиты
class ExampleAdapter(BaseAdapter):
service = "example"
endpoints = {"get_item": Endpoint("get_item", "GET", "/items/{item_id}",
required_params=("item_id",))}
Полная инструкция (формы декларации, оси квот QuotaScope, инварианты, чек-лист для
кодогенератора) — docs/PLUGIN_LIMITS.md. Адаптер БЕЗ
объявленных лимитов работает ровно как раньше: исполнитель ядра не собирается, запросы
уходят в клиент напрямую.
Плагины-способности: единая группа skillery.plugins
Кроме ПОЛНОГО адаптера сети кит несёт вторую, независимую ось подключения —
capability: одна способность, один-два метода, ресурсы приходят аргументом
ctx: ExecutionContext (плагин не читает окружение).
from adapterkit import PluginRegistry, PublisherProtocol
registry = PluginRegistry()
registry.capabilities() # имена БЕЗ импорта чужих пакетов
registry.plugins_supporting(PublisherProtocol) # {capability_id: КЛАСС}, без инстанцирования
Формы (PluginProtocol / PublisherProtocol / TranscriberProtocol / IngestProtocol /
AskProtocol / DeepResearchProtocol),
версия-гейт PLUGIN_API_VERSION, мост AdapterCapability поверх существующего адаптера
и чек-лист публикации — docs/PLUGIN_CONTRACT.md. Прежние
группы (adapterkit.adapters, transcribe.channels) не переименованы и работают как
раньше — новая ось добавлена рядом, а не вместо.
ask-способность в три строки + отбор по виду без импортов
Чат-навык объявляет ask наследованием (AskCapability) или фабрикой
(ask_capability(fn, service=…)) — capability_id вида <service>_ask и версия
контракта приезжают сами. По суффиксу имени реестр отбирает ask-способности из
метаданных, за ноль ep.load(): список «кто умеет отвечать» не стоит импорта
шести чат-пакетов с браузерными зависимостями.
registry.capabilities_supporting("ask") # ['chatgpt_ask', 'gemini_ask', …] — 0 загрузок
registry.plugins_supporting("ask") # классы; грузятся только подошедшие по имени
Conformance-набор adapterkit.testing.AskConformanceTests ловит то, чего не ловит
форма: ask не реализован (унаследована заглушка), ask синхронный, лишний
обязательный аргумент, имя вне соглашения —
docs/ASK_CAPABILITY.md.
Эндпоинты скрытого API — данные, а не константы в коде
Операция несёт СПИСОК адресов-кандидатов (rpcid/URL) с приоритетом, формой
пагинации, живостью и датой последней успешной проверки; вызов идёт по ИМЕНИ
операции. Смерть rpcid лечится правкой декларации.
registry = EndpointRegistry.from_data(json.loads(path.read_text()))
candidate = registry.resolve("conversation_history") # мёртвые адреса пропущены
probe = await probe_endpoint(registry.get("conversation_history"), call, session=state)
probe_endpoint судит только на подтверждённо живой сессии (иначе один
разлогин пометит мёртвыми все адреса разом), мёртвый адрес даёт ENDPOINT_DEAD,
а не «протухла сессия» — docs/ENDPOINT_ADVISOR.md.
Способность локально ИЛИ за сетью — решает реестр
Потребитель не знает, где живёт плагин: реестр по ОДНОМУ capability_id отдаёт
либо локальный класс, либо remote-прокси с идентичным интерфейсом.
registry.bind_remote("vk_transcriber", transport, protocol=TranscriberProtocol)
# ↑ единственная строка сборки хоста; код вызова НЕ меняется:
await registry.get("vk_transcriber")().transcribe(ctx, "audio.mp3", lang="ru")
Дефолт — локальный (до bind_remote поведение реестра не отличается от прежнего).
Лимиты применяются на стороне СЕРВИСА (иначе у каждого клиента своя квота), ключ
идемпотентности едет в конверте вызова. Сериализация контекста, границы безопасности
и что осталось до реального HTTP — docs/REMOTE_CAPABILITY.md.
Ядро не тянет EXTENSIONS (ленивые слои)
Антибот и браузер — тяжёлые опциональные слои (extras antibot/browser), поэтому
import adapterkit их не загружает: реэкспорт идёт через module-level
__getattr__ (PEP 562) и срабатывает на первом обращении к имени. Практически:
import adapterkit # librarykit.antibot / .browser НЕ загружены
adapterkit.HttpClient # ядро — как раньше
adapterkit.CurlCffiTransport # ← вот здесь подгрузится librarykit.antibot
from adapterkit.browser import warm_or_autologin # ← и здесь librarykit.browser
Публичный API не изменился: from adapterkit import CurlCffiTransport,
from adapterkit.antibot import ..., from adapterkit.browser import ...,
from adapterkit import * и dir(adapterkit) работают идентично. Инвариант
закреплён fitness-тестами в tests/test_lazy_extensions.py (замер sys.modules
в отдельном интерпретаторе).
Архитектурные гейты
Канон держится инструментом, а не договорённостью. Онион-граф, ленивая граница ЯДРО/РАСШИРЕНИЯ и отсутствие скрытых зависимостей проверяются одной командой:
uv run --extra dev lint-imports
Базовый слой — зрелый import-linter;
два измерения, которых у него нет, добавлены его же механизмом плагинов
(adapterkit.gates.contracts):
eager_forbidden— ядро не поднимаетlibrarykit.browser/librarykit.antibotв момент импорта. Ленивый доступ (импорт внутри функции, PEP 562__getattr__,if TYPE_CHECKING:) нарушением не считается — встроенныйforbiddenэтого не различает;declared_dependencies— всё импортируемое объявлено вpyproject.toml(dependenciesлибо любой extra).
Рядом — conformance-наборы, которые чужой репозиторий подключает тремя строками:
AdapterContractTests (контракт NetworkAdapter) и PluginConformanceTests
(контракт способности: capability_id = имя entry-point, ctx первым
аргументом, лимиты метаданными, никакой инфраструктуры внутри плагина).
Полное описание, инструкция «как подключить у себя» и таблица «сработало —
что делать» — docs/ARCHITECTURE_GATES.md.
Новый навык за одну команду
python -m adapterkit new-skill myskill --capability publish
Порождает пакет, который уже соблюдает канон: три слоя луковицы на каждую
способность (mechanics/ атомы → use_cases/ сценарий → capabilities/
граница плагина), ctx-аргумент, лимиты через @ratelimit, entry-point в группе
skillery.plugins под тем же именем, что capability_id, шесть гейтов в
pyproject.toml и готовый conformance-тест. Сгенерированный навык проходит
pytest, ruff и lint-imports без единой правки — это закреплено тестами
скаффолда.
Способности: publish, transcribe, ingest, ask, deep_research. Зачем
слои и какие гейты их держат (с живым примером VK-транскрибации) —
docs/ONION_LAYERS.md.
Разработка
uv sync --extra dev
uv run --extra dev pytest -q
uv run --extra dev ruff check adapterkit
uv run --extra dev lint-imports
librarykit тянется из публичной группы
семейство китов. Для локальной
правки кита временно укажите path-источник в [tool.uv.sources] (см. комментарий
в pyproject.toml) и выполните uv lock --upgrade.
Лицензия
MIT © 2026 Dmitry.
Metadata
Release files for s-adapterkit 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| s_adapterkit-0.2.0.tar.gz | 573.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| s_adapterkit-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 861.4 kB
Release files / s_adapterkit-0.2.0.tar.gz
| Download URL | s_adapterkit-0.2.0.tar.gz |
|---|---|
| Size | 573.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b88e946e11a89d0bfe1fed1f5097892f970d5fe3b5fbd64bf2c67b0510929a9e
|
|
BLAKE2b-256 checksum How to use checksums |
fdcb790bc9ff76824e9f49ebb28e36facb072fd2e73c84ba1341e89e1e728aff
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.9.30 {"installer":{"name":"uv","version":"0.9.30","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"12","id":"bookworm","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / s_adapterkit-0.2.0-py3-none-any.whl
| Download URL | s_adapterkit-0.2.0-py3-none-any.whl |
|---|---|
| Size | 287.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c1ebe8f9a7315c0cb35aa6961a51db6f1dda8b5acbc75c0276b477a30841b5ca
|
|
BLAKE2b-256 checksum How to use checksums |
0e1d9011b352fbf677fcdb0fe38b8d26c732606f2e01fd2ebf73a5ed77e463c0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.9.30 {"installer":{"name":"uv","version":"0.9.30","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"12","id":"bookworm","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|