Metadata-Version: 2.5
Name: s-sessionkit
Version: 0.1.1
Summary: Владение сессией аккаунта: адрес, порт хранилища, окно записи, четыре раскладки как слои и порт селектора. Один дом вместо четырёх — носители (файловое дерево, база) объявляют себя entry-points, а движок задаётся строкой подключения.
Author: Dmitry
License: MIT
Keywords: accounts,cookies,selector,session,storage,write-window
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries
Requires-Python: >=3.11
Requires-Dist: s-authkit-contracts<0.2,>=0.1.0
Requires-Dist: s-corekit>=0.0.14
Provides-Extra: dev
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: ruff>=0.8; extra == 'dev'
Provides-Extra: live
Requires-Dist: s-accountpoolkit<0.7,>=0.6.0; extra == 'live'
Requires-Dist: s-authkit-client<0.2,>=0.1.0; extra == 'live'
Requires-Dist: s-librarykit<0.9,>=0.7.25; extra == 'live'
Description-Content-Type: text/markdown

# sessionkit

> Владение сессией аккаунта: **дверь**, адрес, порт хранилища, окно записи,
> четыре раскладки как слои, порт селектора. Один дом вместо четырёх — где
> сессия лежит, решает кит, а не потребитель.

## Зачем

«Сессия аккаунта» физически живёт сразу в нескольких местах: файловым деревом
(`s-authkit-client`, envelope KEK/DEK), строкой в общей базе
(`s-accountpoolkit`, документ хранится запечатанным целиком) и профилем
браузера как кэшем узнавания устройства. Какое из них главное, до сих пор
решала переменная окружения, прочитанная посторонним китом.

У этого две цены. Первая: одна команда пишет туда, где другая ничего не
находит. Вторая, дороже: у сессии нет владельца — спросить «где она на самом
деле» не у кого, и спор «почему навык не видит вход» решается догадками.

Этот кит — владелец. Носители объявляют себя ему сами (entry-points), он
находит общую базу сам, выбирает носитель и **говорит, кого выбрал и почему**.
Здоровье аккаунта пишется одним словом (`authkit_contracts.status.Status`) и
только под окном записи.

## Как спрашивают

```python
import sessionkit

address = sessionkit.SessionAddress(service="gemini", account="me@example.com")
try:
    state = sessionkit.load_state(address)          # None — входа ещё не было
    sessionkit.save_state(address, state or {"cookies": []})
    with sessionkit.write_window(address) as window:   # здоровье — только под окном
        sessionkit.mark(address, status=sessionkit.Status.LIVE, window=window)

    choice = sessionkit.where()                     # КТО ответил и ПОЧЕМУ
    print(choice.name, choice.reason)
    for verdict in choice.verdicts:
        print(" ", verdict)                         # включая тех, кто отказался
except sessionkit.StoreUnavailable as refusal:
    print(refusal)  # носителей в установке нет — отказ называет, какой пакет ставить
```

`mark` без окна — `WriteWindowRequired`, а не молчаливая запись: статус в
индексе расходился с телом сессии ровно потому, что его писали полтора десятка
мест мимо единой отметки.

Разбор полётов «почему выбрали не то» — `sessionkit.explain()`: опрашивает всех
кандидатов, а не останавливается на первом согласившемся.

Что умеет выбранное хранилище сверх обязательного минимума (аренда на минт,
набор аккаунтов, запись без слияния) — `sessionkit.capabilities()`. Способность
спрашивают, а не предполагают: предположение оборачивается тишиной на месте
защиты.

## Адрес

Шесть координат, ровно те же, которыми адресует хранилище
(`corekit.dto.SessionRef`):

| координата | вопрос | умолчание |
|---|---|---|
| `tenant` | чья сессия в общем хосте | `local` |
| `profile` | чей это набор входов (рабочий, личный) | `default` |
| `service` | к какому сервису | обязательна |
| `instance` | какой ИМЕННО вход сервиса (портал, национальный домен) | пусто |
| `account` | под каким аккаунтом | `default` |
| `stage` | боевая сессия или черновик записи трафика | `live` |

`instance` — не украшение: без него два разных портала Битрикса одного аккаунта
получают одно имя, а хранилище считает уникальность по шести координатам — и
запись уходит не туда, откуда читали.

## Где что лежит

`sessionkit.locate` отвечает на вопрос «где» один раз на всех:

- корень сессий — `SESSIONS_HOME`, иначе `~/.sessions`;
- общая база **находится сама** в `<корень>/sessions.db`; `SESSIONS_DB_URL` —
  переопределение (другой сервер, другой тенант), а не условие работы;
- ключ шифрования имеет **три** состояния: файл (`SESSIONS_DB_KEY_FILE` или
  `<корень>/.session-db.key`), явное «хранить открыто»
  (`SESSIONS_DB_PLAINTEXT=1`) и «не задан» — последнее НЕ равно «открыто».

Порядок опроса: сначала база, потом дерево. Если общая база заведена, то она и
есть сессия; читать в этот момент дерево значит завести второй носитель — обе
копии выглядят рабочими, а ротацию получает только та, через которую сходили.

## Носители: объявляются сами

Кит не называет ни одной реализации по имени — это держит `.importlinter`.
Пакет, умеющий хранить сессии, записывается в группу entry-points
`sessionkit.stores`:

```toml
[project.entry-points."sessionkit.stores"]
file = "authkit_client.sessions:store_provider"    # s-authkit-client
db   = "accountpoolkit.session_db:store_provider"  # s-accountpoolkit
```

Провайдер — функция `dsn -> хранилище | StoreOffer | None`. Носителя нет —
`StoreProviderMissing` с рецептом установки (`pip install s-authkit-client` /
`s-accountpoolkit`) и перечнем объявленных. Явная `register_provider` под тем же
именем сильнее объявленной пакетом: подмена в тесте остаётся подменой.

## Окно записи

`sessionkit.write_window(address)` — окно записи на два слоя (лок внутри
процесса + файл между процессами) со сроком и держателем; ждёт не дольше 5 с.
Всё, что меняет тело или здоровье сессии, идёт внутри. Нужно там, где вход
ВЫПУСКАЮТ: у сервисов вида Google одна сессия на аккаунт, и второй
одновременный вход инвалидирует первый. Слово «аренда» (lease) отсюда ушло —
в портфеле оно означало ещё аренду на минт в базе и JWS-пропуск leasekit;
`sessionkit.lease` живёт один релиз как алиас с `DeprecationWarning`.

## Четыре раскладки — слои одной двери

`sessionkit.layouts` (переезд `librarykit.session_layers`): общая база,
шифрованное дерево, снимок рядом с профилем движка, файл в папке навыка —
опрашиваются по старшинству. `read()` возвращает тело **вместе с уликой**, кто
ответил; `write()` пишет в канон, а без канона — обратно в тот же слой (нового
носителя не заводит); `migration_plan()`/`migrate(confirm=True)` — перенос
только с показанной дельтой. `python -m sessionkit.layouts [--plan]` — всё
одним перечнем.

## Селектор — порт

`sessionkit.select`: `Candidate`, `Policy`, `Choice`, протокол `Selector`
(`pick/candidates/report/release`) и чистая `choose(candidates, policy,
cursor=, rng=)` — фильтр → каскад приоритета → Power-of-Two-Choices. Ядро
(`priority_key`, `power_of_two_choices`, `health_from`, `ratio`, `reset_epoch`,
`is_dead_health`) переехало из `accountpoolkit.selection_core` с теми же
сигнатурами; пулы librarykit/accountpoolkit становятся адаптерами.

## Что живёт МИМО двери

Честный список (подробности — в `CHANGELOG.md`): отпечаток выхода
`egress_fingerprint.json`, персона устройства (`corekit.persona`), секреты,
профиль браузера, а также координаты `instance`/`stage`, которые файловое дерево
пока игнорирует. Дверь отдаёт тело сессии; всё перечисленное адресуется своими
способами и ждёт своей очереди.

## Разработка

```sh
uv sync --extra dev --extra live
uv run --extra dev --extra live pytest -q
uv run --extra dev ruff check sessionkit tests
```

Набор `live` ставит НАСТОЯЩИЕ хранилища: утверждение «обе реализации доступны
через дверь» подделкой не доказывается — она подтверждает лишь то, что мы её так
и написали. Без набора живые тесты пропускаются с внятной причиной.

## Статус

- **Type**: `kit`
- **Status**: `active`
- **Priority**: P0
- **Slug**: `sessionkit` · **Prefix**: `ses`

```sh
atlas project get sessionkit
```

Физический layout:

- Storage: `_storage/sessionkit/`
- Junction: `Products\sessionkit` → `_storage/sessionkit`
