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; отказ журнала не роняет работу (см. ниже).
  • Риск аккаунта одним числом — показатель из журнала (свежее весит больше, бан ≠ отказ по лимиту) со слагаемыми, из которых он сложился, и третьим состоянием «истории не хватает». Селектор уточняется им осознанно — флагом, с режимом наблюдения (см. ниже).
  • Выход по требованию — тип выхода (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). Отсечения по риску нет: рискованный аккаунт идёт позже, но остаётся кандидатом.

Выход по требованию (тип и 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=…)) и переживает рестарт.

Статус

Ядро стабильно: статус-машина аккаунта, 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.18.tar.gz (615.0 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.18-py3-none-any.whl (438.8 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: s_accountpoolkit-0.5.18.tar.gz
  • Upload date:
  • Size: 615.0 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.18.tar.gz
Algorithm Hash digest
SHA256 24fdf5a41a3b4cfa41eadb6145015d5156668f0f8df89ff93e3b6e4d864ea362
MD5 d057677560161b5cf31b1af309dacdc3
BLAKE2b-256 2820138ad3cdf7ac263559164c025843100ccbe238038b8d2cb065fafc0ac55c

See more details on using hashes here.

File details

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

File metadata

  • Download URL: s_accountpoolkit-0.5.18-py3-none-any.whl
  • Upload date:
  • Size: 438.8 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.18-py3-none-any.whl
Algorithm Hash digest
SHA256 b448ace6248f81462f3d78ee67fd497bfa6dda60f34d224ecaad539103470b9e
MD5 82146aafc69e4d1ad74bedd0c3ee9612
BLAKE2b-256 c2c5e5e545186dff72b88fdebf3fa95b42609d13bf30db8683af50fe6ac33df1

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

0.5.22

2 files

0.5.21

2 files

0.5.20

2 files

0.5.19

2 files

This release

0.5.18 This release

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