# Расширенный API Distribution

`Distribution` нужен, когда приложение самостоятельно управляет установкой, сессиями и профилями. Для обычных запросов достаточно `HttpClient` или `AsyncHttpClient`: они выполняют эту подготовку автоматически. Прямой выбор профиля не отменяет проверку его подписи и прав API-ключа.

`Distribution(*, state_dir, channel=None, bundle=None)` представляет постоянную установку. `state_dir` — её каталог; `channel` и `bundle` по умолчанию используют штатную поставку. Атрибуты `directory`, `platform`, `channel`, `bundle`, `installation_file` и `state_file` описывают размещение и платформу. `store` и `materializer` предоставляют доступ к хранилищу каталога и подготовке файлов; `trust` создаёт хранилище доверенных метаданных TUF при первом обращении. Для изменения установки используйте методы `Distribution`; прямое редактирование её хранилища не поддерживается. Для асинхронных сессий используются `open_async()` и `async_session()` того же объекта, отдельного класса `AsyncDistribution` нет.

| Вызов | Результат/назначение |
|---|---|
| prepare(now=None) | Заполняет пустое хранилище подписанным каталогом и файлами. Если файлы уже установлены, проверяет и возвращает действующий каталог. Не регистрирует установку и не загружает обновления поверх имеющегося содержимого; для обновления используйте `update()`. |
| enroll(api_key=...,now=None,auto_refresh=True) | Первичная регистрация новой установки; возвращает `Engine`. Не используйте для повторной регистрации уже существующей установки. |
| open(api_key=None,now=None,auto_refresh=False) | Открывает уже зарегистрированную установку и возвращает `Engine`. При ошибке не запускает новую регистрацию. |
| enroll_async / open_async | Обычные функции возвращают AsyncEngine; `async with`, без await перед фабрикой. |
| profile(engine,...) | Возвращает `NativeProfile`. Когда он больше не нужен, вызывающий код должен вызвать `release()`. Проще использовать `session()`, который освобождает профиль автоматически. |
| session(engine,...)/async_session(engine,...) | Контекстный менеджер Session/AsyncSession, автоматически владеющий профилем и освобождающий его. |
| navigation_context(url, *, selection=None) | Контекст открытия страницы. Без `selection` используется Chrome; передайте `ProfileSelection` выбранного профиля, чтобы получить соответствующий контекст Edge. |
| updater(engine,now=None) | Контекстный менеджер обновления; освобождает сетевые ресурсы при выходе. |
| update(engine,now=None) | Проверяет и устанавливает подписанное обновление. |
| recovery_updater(history_runtime_id=None) | Контекст координатора без открытого Engine для существующей истории. |
| recover(api_key=...,history_runtime_id=None,now=None) | Получает и проверяет актуальные данные для существующей установки с сохранением её идентификатора. Требует действующий API-ключ. |
| reconcile(history_runtime_id=None,now=None) | Определяет результат прерванного изменения установки и согласует её состояние, не меняя идентификатор. |

`now` у методов, которые его принимают, — целое число секунд Unix; `None` означает текущее время. Исключение: параметр Distribution.updater(now=...) в текущем коде не используется; время задаётся операции координатора либо через Distribution.update(now=...). Не подставляйте ложное время для обхода сроков. `auto_refresh` включает автоматическое продление разрешения на работу библиотеки. Новые профили этот параметр не загружает. Координатор `updater()` требует синхронный `Engine`, открытый этой же `Distribution`; операцию обновления завершите в отдельном синхронном контексте до открытия новой асинхронной сессии.

Параметры выбора профиля и сессии: `browser='chrome'`, `version=None`, `application='h2'`, `profile_id=None`, `now=None`. У сессии также есть `owner_id=1`, `ca_bundle=None`, `proxy=None`, `cookie_policy=None`.

Доступ к Pro-профилям определяется правами, полученными от сервиса лицензирования (`engine.managed_info().paid_profiles`). Явный `profile_id` не отменяет фильтры семейства, платформы, приложения и тарифа. Наибольшая подходящая версия выбирается, только если и `version`, и `profile_id` равны `None`; иначе применяется заданное ограничение. Реализацию транспорта определяет профиль, её не следует угадывать по номеру Chrome.

```python
from duckonet import Distribution

async def fetch_existing(state_dir, url):
    dist = Distribution(state_dir=state_dir)
    async with dist.open_async() as engine:
        async with dist.async_session(engine, application='h2') as session:
            return await session.request('GET', url, context=dist.navigation_context(url))
```

## Выбор профиля из каталога

`ProfileQuery(*,family,platform,application,version=None,major_range=None,profile_id=None,tier=None,random=False)`.
Все аргументы передаются по имени:

| Параметр | Значение |
|---|---|
| family | Семейство браузера: `chrome` или `edge`. |
| platform | `windows` или `linux`. |
| application | `h1`, `h2`, `h3`, `h2_only`, `h3_only`; их назначение описано в [Client](legacy.html). Значение `auto` здесь не принимается. |
| version | Полная версия или `None`. Нельзя сочетать с `major_range`. |
| major_range | Включительный диапазон основных версий: кортеж двух целых чисел от 1 до 100000; начало не больше конца. |
| profile_id | Точный идентификатор или `None`. Остальные фильтры продолжают действовать. |
| tier | `None`, `free` или `paid`. В API значение `paid` соответствует Pro. |
| random | `False` по умолчанию. Без `version`, `major_range` и `profile_id` требуется `True`. Случайный выбор происходит один раз, а не перед каждым запросом. |

`catalog_choices(store, now=..., native_id=..., query=...)` возвращает кортеж подходящих записей без случайного выбора. Если их нет, возникает `CATALOG_SELECTION_ABSENT`. `select_catalog_profile()` возвращает `ProfileSelection`: обычный выбор требует ровно одну запись, случайный выбирает один индекс однократно.

`verify_catalog_selection()` проверяет неизменность области, поколения и списка и возвращает выбранный `ProfileRecord`; повторного выбора не делает. `selection.profile` — свойство. `selection.describe()` возвращает описание выбора. При открытии профиля подписи и права проверяются отдельно.

Ошибка `CATALOG_SELECTION_ABSENT` означает, что ни одна запись не соответствует всем фильтрам. При `CATALOG_SELECTION_AMBIGUOUS` уточните версию или ID либо явно разрешите случайный выбор. Если каталог изменился и выбор устарел, получите выбор заново из текущего каталога.

```python
import time
from duckonet import Distribution, ProfileQuery, catalog_choices

def available_profiles(state_dir):
    dist = Distribution(state_dir=state_dir)
    with dist.open() as engine:
        query = ProfileQuery(family='chrome', platform=dist.platform,
            application='h2', tier='paid' if engine.managed_info().paid_profiles else 'free',
            random=True)
        rows = catalog_choices(dist.store, now=int(time.time()),
                               native_id=engine.catalog_native_id, query=query)
        return [(row.id, row.version) for row in rows]
```

## Прямая работа с профилем

Этот раздел нужен интеграциям, которые самостоятельно загружают и компилируют описание профиля. Для обычной сессии достаточно `Distribution.session()`.

Для схемы 2 `map_catalog_profile_v2(receipt, *, profile_id, native_id)` преобразует структуру без подтверждения её происхождения. `compile_selected_profile()` требует `authentication` и `trusted_application_keys`; `admission` может быть `None`.

Для схемы 3 `compile_selected_modular_profile()` принимает `program`, `profile_v7`, `signature`, `wire_policy`, `request_policy` и использует существующий `Engine`. У `AsyncEngine` есть публичный одноимённый метод.

`load_modular_profile_inputs(path)` загружает описание сборки профиля и два закреплённых файла — подпись и каноническое представление. Проверяет схему, размеры, хеши и идентификаторы. `inputs.as_kwargs()` предназначен для компилятора профиля, не для HTTP-запроса. Подпись и права проверяет нативное ядро. Файлы должны соответствовать выбранному поколению и ID профиля.

Каталог схемы 1 можно читать и разбирать, но нельзя использовать для выполнения запросов: `NativeMapping.require_executable()` и `compile_catalog_profile()` отклоняют исполнение. Это номера форматов данных, а не версии публично выпускавшейся библиотеки. `Distribution` использует подписанные модульные профили схемы 3.

## Разбор собственных настроек профиля

`Fingerprint(*, name, program)` и `CustomProfileRegistry` описывают собственные механические настройки без подтверждения браузерного соответствия. Они не заменяют подписанный профиль публичного выпуска. Аргументы `Fingerprint` передаются по имени; `fingerprint.canonical_bytes` и `.sha256` — свойства.

`registry.register(name, fingerprint)` возвращает новый реестр, не меняя прежний. Пространство имён выбирается явно: `builtin`, `json`, `fingerprint` или `custom`. Библиотека не пытается угадать по строке путь к файлу или адрес загрузки.

```python
from pathlib import Path
from duckonet import parse_fingerprint_json, CustomProfileRegistry, resolve_profile_input

def describe_custom(path):
    fp = parse_fingerprint_json(Path(path).read_bytes())
    registry = CustomProfileRegistry().register('local-example', fp)
    value = resolve_profile_input('local-example', namespace='custom', registry=registry)
    return value.describe()
```

Формат JSON: схема `duckonet.fingerprint/1`, поля `assurance=caller_metadata_unverified`, `name` и `program`. Предел — 262144 байта UTF-8. Неизвестные и повторные поля, числа с плавающей точкой и NaN запрещены. Реестр содержит до 128 записей без повторяющихся имён.

Пространство `builtin` содержит базовые настройки транспорта для низкоуровневых интеграций. Оно не является каталогом доступных браузеров. Чтобы выбрать поддерживаемую версию браузера для запросов, используйте `browser` и `version` клиента либо `Distribution.profile()`.

`ResolvedProfileInput.open_session()` работает только с синхронным немодульным `Engine` и `TransportConfig`. Модульный Engine отклоняет такой профиль с `PROFILE_INPUT_MODULAR_UNSIGNED`. Пример выше только разбирает и описывает настройки: он не отправляет запрос и не устанавливает браузерный профиль. Для выполнения запросов через `Distribution` выберите подписанный профиль.

## Настройка канала поставки

Для стандартного сервиса оставьте `channel=None`. Функция `default_release_channel()` возвращает настройки адресов и доверия, поставляемые с пакетом. Собственный канал задаётся объектом `ReleaseChannel`; все его аргументы именованные.

| Аргумент | Назначение |
|---|---|
| catalog_keys | Непустое отображение идентификаторов ключей в объекты `Ed25519PublicKey` для проверки каталога. |
| bootstrap | Байты исходного доверенного корневого документа TUF. |
| bootstrap_sha256 | SHA256 этих байтов; несовпадение отклоняется. |
| repository_base | Базовый адрес репозитория подписанных метаданных. |
| artifact_base | Базовый адрес файлов компонентов. |
| pairing_origin | Адрес сервиса регистрации; по умолчанию `https://duckolab.com`. |
| permission_origin | Адрес сервиса прав; по умолчанию `https://api.duckolab.com`. |
| available | `True` по умолчанию. При `False` начальная подготовка использует файлы из `bundle`; это не отключает проверку подписей и не предоставляет доступ без регистрации. |

Доверенные ключи и исходный корневой документ должны приходить из вашего доверенного канала поставки. Не подставляйте данные произвольного сервера вместо доверенных настроек только ради устранения ошибки проверки.

## Ошибки первого запуска и восстановление

При прерывании первой регистрации создайте новый клиент с тем же API-ключом и `state_dir`. Библиотека продолжит сохранённую операцию, в том числе если подтверждение успело истечь. Сбрасывать каталог установки не нужно.

После временной сетевой ошибки повторите запуск клиента с прежним `state_dir`. Не удаляйте каталог и не вызывайте `enroll()` после неудачного `open()`. Если доступ отозван или права истекли, сначала восстановите действующие права ключа. Повреждённое состояние не исправляется повторной регистрацией. `recover()` сохраняет идентификатор установки и защиту от отката; `reconcile()` завершает разбор операции с неопределённым результатом записи. Обе функции проверяют подписи, время и права и могут отказать.

Повтор запуска библиотеки и повтор HTTP-запроса — разные действия. Не повторяйте POST бесконечно при неизвестном результате: целевой сервер мог уже выполнить операцию.

## Обновление между прогонами

Distribution.profile() выбирает профиль по явным фильтрам и не запускает цикл переключения прогонов. Для version="last" используйте HttpClient/AsyncHttpClient и их run(), как описано в [руководстве обновлений](profile-updates.html).
