Skip to main content

s-accountpoolkit

Зрелый пул аккаунтов/сессий поверх s-librarykit — доменный слой над SessionStore/SessionPool/RateLimiter/RotatingAuth/antibot-транспортами. Обобщение gemini-balancer в переиспользуемый кит.

import-имя accountpoolkit · dist-имя s-accountpoolkit (как s-librarykit → librarykit).

Что даёт (сверх librarykit)

  • Account со статус-машинойavailable / cooldown / quota_exhausted / disabled / blocked с причинами и таймстемпами (не строка+is_active).
  • Quota / подпискиQuotaData union (dual-window weekly+5h / header-based / JWT-claims), приоритет ULTRA > PRO > FREE, model-level изоляция.
  • Таксономия подписок по семействам (accountpoolkit.subscription) — провайдеры одного вендора делят подписку, но тиры разные при общей оси free/paid: google (Plus/Pro/Ultra 5x·20x), openai (Go/Plus/Edu/Pro 5x·20x), anthropic (Pro/Max 5x·20x), telegram (Premium). Каждый тир несёт грубый SubscriptionTier для селектора; у Accountplan/subscription_expires_at/project (срок подписки + ярлык-проект).
  • Rate-limit + circuit-breaker — разбор 429/403/5xx/Retry-After, exp-backoff по причинам, cooldown отдельно от CB-open, авто-recovery.
  • Selector — двухслойный (жёсткий eligibility-фильтр → Power-of-Two-Choices), приоритет-каскад подписка→квота→health→reset, sticky-сессии, slow-start.
  • OAuth-ротация — single-flight (double-checked-locking) на аккаунт, атомарный rolling-refresh, fallback-цепочка OAuth-клиентов, invalid_grant → карантин, проактивный фоновый рефреш, keyring-экспорт токена.
  • Egress-пул — привязка аккаунт↔прокси (sticky IP) vs глобальный пул, стратегии (RR/Weighted/LeastConn/Priority/P2C), health-check, настоящий per-proxy circuit-breaker/quarantine.
  • Общий вход у поставщика личности (SSO) — вход в Google хранится ОДИН раз и переиспользуется всеми сервисами и пулами; отдельное состояние «сессия сервиса жива, вход отозван»; продление одно на всех (см. ниже).
  • Журнал событий идентичности — append-only история аккаунта (выдача, вход, продление, ротация, операция, отказ по лимиту, челлендж, бан, перелогин, смена выхода) с выходом/устройством/уликой и выборкой «что предшествовало бану». Без секретов by-design; отказ журнала не роняет работу (см. ниже).
  • Риск аккаунта одним числом — показатель из журнала (свежее весит больше, бан ≠ отказ по лимиту) со слагаемыми, из которых он сложился, и третьим состоянием «истории не хватает». Селектор уточняется им осознанно — флагом, с режимом наблюдения (см. ниже).
  • Темп бизнес-операций — аккаунт не совершает операций чаще человеческого темпа, заданного для РОДА операции (отправка, публикация, поиск, экспорт, вход): границы в час и в сутки, промежуток между двумя подряд, пауза с разбросом. Считается по журналу, работает проактивно (не совершить раньше срока, а не отреагировать на 429), включается осознанно — флагом, с режимом наблюдения (см. ниже).
  • Устройство аккаунта (персона) — одно устройство на аккаунт: заводится при первой выдаче, лежит в реестре, переживает перезапуск и версионируется (corekit.persona). Выдача отдаёт персону ВМЕСТЕ с адресом выхода и сводит их между собой; смена устройства — событие журнала (см. ниже).
  • Выход по требованию — тип выхода (datacenter | residential | mobile + честное «неизвестно»), ASN и страна в модели прокси/инбаунда; дорогой выход выдаётся только когда его просят (см. ниже).
  • Import/export — версионированный конверт, идемпотентный upsert, чтение чужих сторов.
  • Headless AccountService-фасад + clikit CLI (json-by-default); опц. REST admin ([rest], accountpoolkit.rest.create_admin_app) + cloudflared quick-tunnel (accountpoolkit.tunnel, нужен бинарь cloudflared на PATH — не pip-пакет).

Секреты — только через librarykit SessionStore (envelope KEK/DEK) / SecretStore (keyring+fallback). Никакого plaintext.

Провайдер-специфика (gemini / codex / …) — через AccountProvider Protocol-плагины; ядро её не знает.

Установка

uv add s-accountpoolkit          # ядро
uv add "s-accountpoolkit[egress]"  # + antibot health-транспорт для egress-пула
uv add "s-accountpoolkit[cli]"     # + CLI `accountpool` (json-by-default)

import-имя — accountpoolkit. AccountService — headless-фасад над всеми слоями:

import accountpoolkit as apk
from librarykit.sessions import SessionStore

store = apk.AccountStore(SessionStore(root=None, encrypt=True), social="myservice")
pool = apk.AccountPool(store, tracker=apk.RateLimitTracker())
svc = apk.AccountService(store=store, pool=pool)

choice = await svc.acquire(require_tier=apk.SubscriptionTier.PRO)
if choice:
    ...  # запрос через choice.proxy (egress) + choice.account (device);
    # секреты хранятся отдельно (envelope): await store.load_creds(choice.ref)
    await svc.report(choice.ref, response=resp)  # 429/5xx → cooldown/circuit-breaker

Общий вход у поставщика личности (SSO)

Аккаунт, заведённый «через Google», держится на ДВУХ входах сразу: сессии самого сервиса и входе у Google. Сроки жизни у них разные, и умирают они порознь — ChatGPT продолжает отвечать, когда войти в Google уже нельзя. Пока состояний было два, этот случай прятался в «всё хорошо» и всплывал в час, когда вход понадобился.

Вход хранится один раз. SsoLogin — запись по ключу «поставщик + кого он узнаёт» (google + почта). Тело входа лежит в общем хранилище сессий по каноническому адресу sso_session_ref(...) — профиль sso, а не профиль потребителя, иначе у каждого профиля завелась бы своя копия. Аккаунты на него ССЫЛАЮТСЯ: Identity.sso_login_id в реестре и пара «провайдер + почта» в строке состояния сессии. Копии на навык нет ни одной.

Три состояния вместо двух (SessionHealthSnapshot.auth_state):

состояние что произошло что чинить
healthy сессия сервиса жива, плохих улик о входе нет ничего
sso_expired сессия сервиса ЖИВА, вход у поставщика МЁРТВ восстановить вход; сервис не трогать, работа идёт
unauthenticated мертва сама сессия сервиса перелогин в сервис
unknown сервис недоступен / лимит / капча переспросить позже

logged_out ставится ТОЛЬКО по прямой улике: поля формы входа (identifierId, Passwd) или ответ 401/403 от поставщика. Ссылка «войти» на странице и сетевой сбой уликами не считаются — по догадке выключаются рабочие аккаунты. Улика хранится рядом с вердиктом и уходит в отчёт.

Продление одно на всех. Человек входит ОДИН раз; поколение входа растёт, и каждый навык узнаёт об этом сам — его сессия отстала от поколения, а новое тело уже лежит в общем хранилище:

from accountpoolkit.domain import SsoEvidence
from accountpoolkit.services import SsoService

sso = SsoService(session_factory, store=db_store)
sso.attach("google", "me@gmail.com", service="gemini", account="me@gmail.com", profile=uid)
sso.attach("google", "me@gmail.com", service="chatgpt", account="me@gmail.com", profile=uid)

# наблюдение по улике — одно на ВСЕ сервисы, вход-то один
sso.observe("google", "me@gmail.com", SsoEvidence(status_code=401))   # → logged_out

# вход перехвачен заново: тело уезжает в общее хранилище, поколение +1
await sso.publish("google", "me@gmail.com", storage_state)

# навык спрашивает про СВОЮ сессию и подхватывает общий вход — человека не зовут
if sso.needs_resync("google", "me@gmail.com", service="gemini",
                    account="me@gmail.com", profile=uid):
    body = await sso.body("google", "me@gmail.com")     # один источник на всех
    ...                                                  # пере-минт сессии сервиса
    sso.mark_synced("google", "me@gmail.com", service="gemini",
                    account="me@gmail.com", profile=uid)

Аккаунт с мёртвым входом НЕ выключается из выдачи: работа идёт, и отсечь его значило бы сломать её. Чинить надо вход, а не сервис.

await sso.adopt(...) дополнительно помечает сессию в ИНДЕКСЕ хранилища (credential_group — задел librarykit «один логин ⇒ несколько сетей»), чтобы ответ на «кто ляжет вместе с этим входом» был один, а не два расходящихся.

Журнал событий идентичности (что предшествовало бану)

У аккаунта есть ИСТОРИЯ, а не только счётчики и мгновенное состояние. Журнал append-only живёт в том же хранилище, что реестр, и отвечает на вопрос, ради которого заведён: что происходило перед баном.

from accountpoolkit import IdentityJournal

journal = IdentityJournal.open()          # или Gate(...).journal

# ЦКП: события до последнего бана И сам бан последней строкой
for e in journal.before_ban(identity_id=17, limit=20):
    print(e.at, e.kind, e.outcome, e.code, e.egress_host or "—", e.egress_country)

# лента за окно (по аккаунту реестра или по имени аккаунта)
journal.feed(identity_id=17, since=..., limit=100)
journal.feed(account="me@gmail.com", service="gemini-chat")

Из CLI (json-by-default):

accountpool journal before-ban --identity-id 17 --limit 20
accountpool journal feed --account me@gmail.com --hours 24 --kind limit

Событие отвечает на «кто, когда, чем и с каким исходом»: момент (UTC), аккаунт и сервис, сессия и проект-потребитель, ВЫХОД (хост/страна/ASN ноды), устройство (ссылка — заполняется разметкой устройств), род (EventKind: выдача, освобождение, вход, продление, ротация токена, операция, отказ по лимиту, челлендж, бан, перелогин, смена выхода), исход (EventOutcome) и улику — МАШИННЫЙ КОД из уже существующих словарей (LimitReason, HealthState, SsoState, AccessState).

Секретов в журнале нет и быть не может. Не по дисциплине пишущего, а по устройству: свободного поля под «тело ответа» или «подробности» в контракте не существует, а улика и приметы проходят фильтр формы и длины — токен, кука и заголовок авторизации туда не пролезают. Журнал append-only: попавший в него секрет остался бы там навсегда.

Журнал не важнее работы. Его отказ никогда не роняет горячий путь: выдача аккаунта происходит и тогда, когда записать событие не удалось.

Ретенция — 90 дней (ACCOUNTPOOL_JOURNAL_RETENTION_DAYS, ноль — «не убирать»). Столько живёт окно расследования: бан выясняется не сразу, а всплеск надо сравнивать с несколькими нормальными циклами недельных лимитов; дальше квартала — уже архив, а не расследование.

Риск аккаунта одним числом

Журнал даёт историю, а показатель риска сводит её к ОДНОМУ сравнимому числу: насколько рискованно идти этим аккаунтом прямо сейчас. Раньше на этот вопрос отвечать было нечем — признаки описывали момент (жив ли, сколько осталось, сколько подряд упало), а рискованность это свойство истории.

from accountpoolkit import IdentityJournal
from accountpoolkit.services import RiskScorer

risk = RiskScorer(IdentityJournal.open()).assess(identity_id=17, account="me@gmail.com")
print(risk.score, risk.level)   # 63.4 high
print(risk.explain())           # me@gmail.com: риск 63.4 (high) по 41 событиям за 30 дн
                                # — ban 60.0 (1), challenge 2.4 (1), limit 1.0 (12)
accountpool journal risk --account me@gmail.com

Как считается: свежее весит больше (полураспад 14 дней — бан вчера и бан три месяца назад это разные вещи), роды весят по-разному (бан 60, челлендж 15, отказ по лимиту 1 — лимит про исчерпание, а не про риск), сумма обрезается сотней. Рядом с числом всегда едут слагаемые (components): род, сколько событий, вклад в баллах, когда случилось последнее и какие коды-улики встретились — иначе показателем нельзя пользоваться при расследовании.

«Нет данных» — не «низкий риск». У свежего аккаунта уровень unknown, а не low: он непроверен, а не безупречен. Плохие улики при этом перебивают нехватку данных — единственное событие-бан даёт high, а не «мало данных».

Связь с селектором — осознанная. По умолчанию учёт риска ВЫКЛЮЧЕН и не делает ни одного лишнего запроса. Включается флагом ACCOUNTPOOL_RISK_AWARE_SELECTOR: shadow — считать и логировать расхождения, не трогая боевой выдачи; on — уточнять каскад (полоса риска встаёт между health и reset). Отсечения по риску нет: рискованный аккаунт идёт позже, но остаётся кандидатом.

Темп бизнес-операций (не чаще, чем это делает человек)

Технический темп кит держал давно — RPM/RPD, окна квот, circuit-breaker. Всё это про КАНАЛ: «сто запросов в минуту» не говорит ничего о том, нормально ли для человека опубликовать сорок постов подряд. А признаки современная защита строит вокруг БИЗНЕС-ОПЕРАЦИЙ: подделать один заголовок дешевле, чем весь жизненный цикл аккаунта, — поэтому проверяют жизненный цикл.

from accountpoolkit import IdentityJournal, OperationPacer

pacer = OperationPacer(IdentityJournal.open())

план = pacer.plan("publish", identity_id=17, account="me@vk.com")
print(план.explain())
# me@vk.com: publish — ждать 1123.4 сек (hour_budget); за час 10/10,
# за сутки 23/40; режим on

pacer.wait("publish", identity_id=17)     # выждать (в корутине — asyncio.sleep)
...                                       # сделать операцию
pacer.note("publish", identity_id=17)     # отметить в журнале

Род операции — данные, а не набор if. Кит знает из коробки пять родов — send (45/час, 300/сутки), publish (10/40), search (120/800), export (6/30), login (3/10) — с минимальным промежутком между двумя подряд. Границы перебиваются конструктором или переменной окружения ACCOUNTPOOL_OPERATION_TEMPO (publish=6/25/90, my_export=2/8/300), а незнакомый род получает осторожное умолчание: навык, заведший свою операцию, узнаёт об этом не остановкой работы.

Отклонения нет — есть отсрочка. План говорит «подожди столько-то», а не «нельзя»: отказ вернулся бы вызывающему ошибкой, ошибка ушла бы в ретрай, а ретрай — это ещё более плотный темп.

Пауза с разбросом. Ровно N операций в час через равные промежутки — сам по себе машинный признак; пауза гуляет в пределах +35% (разброс только вверх — пауза короче границы перестала бы быть границей).

Считается по журналу, который уже пишется. Род операции едет машинным кодом в IdentityEvent.code события operation, выборка дешёвая (только моменты, по индексу, окно — сутки). Второго счётчика в памяти нет: он не пережил бы перезапуск, а история переживает.

Включение осознанное. По умолчанию темп ВЫКЛЮЧЕН и не делает ни одного запроса. ACCOUNTPOOL_OPERATION_PACE=shadow — считать и показывать, что было бы отложено (plan.advised_seconds, pacer.last_shadow), ничего не откладывая; on — откладывать. Поломка подсчёта даёт честное «можно сразу» (pace_unavailable) — как и у журнала, темп не важнее работы.

Выход по требованию (тип и ASN)

У выхода есть тип (datacenter | residential | mobile плюс честное unknown), ASN и страна: именно их защита видит на краю, ещё до первого запроса. Выход выдаётся ПО ТРЕБОВАНИЮ:

mgr.choose()                        # требования нет → дешёвый (датацентр)
mgr.choose(require="residential")   # нужен домашний адрес → резидентский/мобильный
mgr.bind(identity_id, "nl-1", require="residential")   # мимо требования → ValueError

Резидентские прокси стоят денег, поэтому умолчание их не расходует: дорогой выход выдаётся по явному требованию, а не «на всякий случай». Неразмеченная нода (unknown) требование дороже датацентра НЕ закрывает — отказ честнее подмены. Разметка задаётся при заведении инбаунда (add(..., egress_kind=…, asn=…)) и переживает рестарт.

Устройство аккаунта (персона)

Выдача отдаёт не только адрес выхода, но и устройство, которым этот аккаунт представляется:

granted = await gate.acquire("проект", "gemini-chat")
granted.proxy_endpoint      # socks5h://127.0.0.1:10801 — чем ходим
granted.persona.persona_id  # dev-d648b243a3e8 — КЕМ приходим (стабильно)
granted.persona.version     # 3 — поколение заявления
granted.persona.timezone    # Europe/Helsinki — часы из страны выхода

Одно устройство на аккаунт, а не на вызов. Раньше выдача отдавала только адрес, устройство до потребителя не доезжало вовсе, и один аккаунт представлялся разными клиентами при одном и том же IP. Для скоринга устройства это худший из рисунков: стабильная сеть при плавающем клиенте читается как угон сессии. Персона лежит в строке device (имя и поколение — колонки, слои — JSON) и одна на все сервисы аккаунта: у человека один ноутбук на все сайты.

Обновление — это bump, а не подмена. Обновился Chrome, сменилась платформа, переехал выход — растёт version, persona_id остаётся. Смена имени означала бы для защиты нового посетителя: потерянное узнавание устройства и подтверждение входа на ровном месте.

Персона сводится с выходом, и молча это не делается. Часы принадлежат машине, а машина стоит там, откуда приходит запрос: пояс, спорящий со страной выхода, — ошибка уровня «ходить нельзя». Такое расхождение ЧИНИТСЯ (выход переставили мы) — с поднятием поколения и записью device_switch в журнал. А расхождение, которое сменой места не лечится (окно больше экрана, десктопная платформа при мобильном признаке, рукопожатие, отставшее от заголовков), — PersonaConflict: чинить его значило бы подменить само устройство.

Чем мы выглядим (версия браузера, язык) кит НЕ пинит — это знает netkit, одно место на систему; наблюдение можно задать явно:

from accountpoolkit.services import Gate, PersonaService, PersonaTraits

gate = Gate(persona=PersonaService(traits=PersonaTraits(browser_version="152.0.8100.10")))

Статус

Ядро стабильно: статус-машина аккаунта, quota/подписки, rate-limit + circuit-breaker, OAuth-ротация (single-flight), egress-пул, import/export, AccountService-фасад + CLI. Провайдер-плагины подключаются через entry-points accountpoolkit.providers.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

s_accountpoolkit-0.5.22.tar.gz (688.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

s_accountpoolkit-0.5.22-py3-none-any.whl (478.3 kB view details)

Uploaded Python 3

File details

Details for the file s_accountpoolkit-0.5.22.tar.gz.

File metadata

  • Download URL: s_accountpoolkit-0.5.22.tar.gz
  • Upload date:
  • Size: 688.5 kB
  • Tags: Source
  • Uploaded using 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}

File hashes

Hashes for s_accountpoolkit-0.5.22.tar.gz
Algorithm Hash digest
SHA256 a10d6a38032839c4d1f3dc840967f3bff2895d1d9d5cf6f5bc7c955a6e66119f
MD5 e8cf52f1ba5b22973b991207d0ef8f5e
BLAKE2b-256 a2072f9226301e8cdc172afd4d827da0412c97c8fde81de12b15e33cb2ff9161

See more details on using hashes here.

File details

Details for the file s_accountpoolkit-0.5.22-py3-none-any.whl.

File metadata

  • Download URL: s_accountpoolkit-0.5.22-py3-none-any.whl
  • Upload date:
  • Size: 478.3 kB
  • Tags: Python 3
  • Uploaded using 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}

File hashes

Hashes for s_accountpoolkit-0.5.22-py3-none-any.whl
Algorithm Hash digest
SHA256 70690398f4a30a7819b9a0617a6c44b4c28f6a92793e4c6e5bafb93671b1eb10
MD5 4ae63bc4cc170947a642aed41882fb3b
BLAKE2b-256 15e324dcb5f7a6e01faaa63099821aa7082074877734c6f40573f15d7041d85f

See more details on using hashes here.

Release history Release notifications | RSS feed

0.5.28

2 files

0.5.27

2 files

0.5.26

2 files

0.5.25

2 files

0.5.23

2 files

This release

0.5.22 This release

2 files

0.5.21

2 files

0.5.20

2 files

0.5.19

2 files

0.5.18

2 files

0.5.14

2 files

0.5.13

2 files

0.5.12

2 files

0.5.11

2 files

0.5.10

2 files

0.5.9

2 files

0.5.8

2 files

0.5.7

2 files

0.5.6

2 files

0.5.5

2 files

0.5.4

2 files

0.5.3

2 files

0.5.2

2 files

0.5.1

2 files

0.4.9

2 files

0.4.8

2 files

0.4.7

2 files

0.4.6

2 files

0.4.5

2 files

0.4.4

2 files

0.4.3

2 files

0.4.2

2 files

0.4.1

2 files

0.4.0

2 files

0.3.12

2 files

0.3.11

2 files

0.3.10

2 files

0.3.9

2 files

0.3.8

2 files

0.3.7

2 files

0.3.6

2 files

0.3.5

2 files

0.3.4

2 files

0.3.3

2 files

0.3.2

2 files

0.3.1

2 files

0.3.0

2 files

0.2.0

2 files

0.1.1

2 files

0.1.0

2 files

0.0.6

2 files

0.0.5

2 files

0.0.4

2 files

0.0.3

2 files

0.0.2

2 files

0.0.1

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page