Metadata-Version: 2.5
Name: funora
Version: 0.0.1.dev2
Summary: Unofficial multi-language framework for the FunPay marketplace - Python SDK
Project-URL: Homepage, https://github.com/Funora-Develop
Project-URL: Repository, https://github.com/Funora-Develop/Funora-python
Author: Funora Contributors
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: funpay,marketplace,sdk,unofficial
Classifier: Development Status :: 2 - Pre-Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: httpx>=0.27
Requires-Dist: selectolax>=0.3.21
Provides-Extra: dev
Requires-Dist: hatchling>=1.27; extra == 'dev'
Requires-Dist: mypy>=1.11; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
Requires-Dist: pytest-cov>=6; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: pyyaml>=6; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Provides-Extra: docs
Requires-Dist: mkdocs-material>=9.5; extra == 'docs'
Requires-Dist: mkdocs>=1.6; extra == 'docs'
Description-Content-Type: text/markdown

<p align="center">
  <img src="https://raw.githubusercontent.com/Funora-Develop/.github/main/assets/funora-python.svg" width="76" height="76" alt="">
</p>

<h1 align="center">Funora для Python</h1>

<p align="center"><em>Эталонная реализация контракта Funora.</em></p>

<p align="center">
  <img alt="status" src="https://img.shields.io/badge/status-draft-6E7681?style=flat-square">
  <a href="https://pypi.org/project/funora/"><img alt="PyPI" src="https://img.shields.io/pypi/v/funora?style=flat-square"></a>
  <img alt="license" src="https://img.shields.io/badge/license-Apache--2.0-2F7D95?style=flat-square">
  <img alt="FunPay" src="https://img.shields.io/badge/FunPay-unofficial-B4501E?style=flat-square">
</p>

<p align="center"><a href="https://github.com/Funora-Develop/Funora-python/blob/v0.0.1.dev2/README.en.md">English</a></p>

---

> **Неофициальный проект.** Funora не аффилирована с FunPay, не одобрена ею и никак с ней не связана.
> Работает с приватным веб-интерфейсом, который может измениться в любой момент без предупреждения.
> Использование может привести к блокировке аккаунта и заморозке средств - этот риск несёте вы.
> Прочитайте [DISCLAIMER.md](https://github.com/Funora-Develop/Funora-python/blob/v0.0.1.dev2/DISCLAIMER.md) прежде, чем строить на этом то, что приносит вам деньги.

## Тестовый pre-alpha: `0.0.1.dev2`

Установка из [PyPI](https://pypi.org/project/funora/0.0.1.dev2/):

```bash
python -m pip install "funora==0.0.1.dev2"
```

Установка и ограничения описаны в [заметке о выпуске](https://github.com/Funora-Develop/Funora-python/blob/v0.0.1.dev2/docs/pre-alpha.md).
Контракт пока имеет статус draft; сборка пакета не означает проверку всех
операций на действующем аккаунте.
Новые версии на PyPI выходят на крупных этапах; текущая доработка идёт в ветках и PR.

Реализованы и проверяются тестами тридцать пять операций: двадцать четыре чтения и одиннадцать записей - отправка текста и картинки, отметка прочтения, отзыв и его снятие, правка цены лота, поднятие предложений, включение и выключение лота, смена валюты интерфейса и возврат по заказу.

**Руководство: [docs/index.md](https://github.com/Funora-Develop/Funora-python/blob/v0.0.1.dev2/docs/index.md).** Оно собирается в сайт
(`mkdocs serve`) и проверяется тем же прогоном, что и код: примеры разбираются
интерпретатором, ссылки разрешаются, а каждая упомянутая операция ищется на
настоящем клиенте.

## Что это

Python SDK для FunPay. Здесь протокол прорабатывается первым; остальные языки
реализуют его заново по спецификации, а не портируют этот код построчно.

```python
from funora import Client, EnvSecretProvider

# Секрет берётся из FUNORA_GOLDEN_KEY и в коде не появляется ни разу.
with Client(EnvSecretProvider()) as client:
    page = client.orders.list()
    for order in page.rows():
        print(order.order_id, order.description_text)
```

То же асинхронно. Фасада два, ядро одно: нормативный порядок шагов, политика
повторов, расход бюджета и правила курсора написаны один раз и обоим достаются
готовыми. Перевод бота сводится к `await`.

```python
from funora import AsyncClient, EnvSecretProvider

async with AsyncClient(EnvSecretProvider()) as client:
    page = await client.orders.list()
    for order in page.rows():
        print(order.order_id, order.description_text)
```

## Что уже работает

| Операция | Возвращает |
|---|---|
| `client.orders.list()` | список продаж сокращёнными записями |
| `client.orders.get(order_id)` | один заказ целиком |
| `client.orders.details(*ids)` | заказы структурно: сумма числом, валюта кодом, стороны порознь |
| `client.orders.refund(order_id)` | средства возвращены покупателю |
| `client.chats.list()` | список диалогов |
| `client.chats.thread(node_id)` | переписку с определением происхождения сообщений |
| `client.chats.send_text(node_id, text)` | квитанцию отправки с исходом |
| `client.chats.mark_read(node_id)` | диалог помечен прочитанным |
| `client.chats.send_image(node_id, content, ...)` | картинка в переписке |
| `client.chats.buyer_viewing(node_id, *buyer_ids)` | что покупатель смотрит сейчас |
| `client.lots.list_own(node_id)` | свои лоты раздела с идентификаторами предложений |
| `client.lots.form(node_id, offer_id)` | форму правки лота и признак показа в выдаче |
| `client.lots.update_price(...)` | лот с новой ценой, всё прочее нетронутым |
| `client.lots.promote(game_id, node_id)` | поднятие всех предложений раздела |
| `client.lots.calculate_prices(node_id, price)` | что заплатит покупатель |
| `client.lots.activate(...)` | лот в выдаче |
| `client.lots.deactivate(...)` | лот снят с выдачи |
| `client.lots.showcase(user_id)` | витрину продавца разделами |
| `client.market.offers(node_id)` | публичные предложения раздела: чужие цены и продавцы |
| `client.market.snapshot(node_id)` | снимок выдачи для сравнения во времени |
| `client.market.chips(node_id)` | второй рынок: предложения по количеству |
| `client.reviews.get(user_id, rating=None, cursor=None)` | страница отзывов, отбор по оценке 1..5 и курсор продолжения |
| `client.reviews.leave(order_id, rating=..., text=...)` | отзыв к заказу |
| `client.reviews.remove(order_id)` | отзыв снят |
| `client.account.get()` | личность аккаунта |
| `client.account.refresh()` | её же, перечитанную |
| `client.account.health()` | пригодность сессии |
| `client.account.balance()` | баланс и операции |
| `client.account.switch_currency(code)` | валюта показа сменена |
| `client.account.capabilities()` | что из объявленного доступно |
| `client.catalog.categories(refresh=False)` | разделы площадки с кэшем |
| `client.catalog.search(query)` | публичный поиск игр и разделов; полнота совпадений не подтверждена |
| `client.catalog.field_schema(section_id)` | поля фильтров раздела, варианты выбора и диапазоны |
| `client.chats.history_before(node_id, cursor=...)` | предыдущие сообщения и сохраняемый курсор |
| `client.market.calculate_chip_prices(game_id, price)` | расчёт цены на рынке по количеству |

## Реакция за секунды, а не за минуты

У площадки есть собственный канал обновлений - `POST /runner/`, промежуток пять
секунд, - и наблюдение его слушает. Пока канал молчит, страницы не читаются
вовсе; сказал «изменилось» - читаются немедленно. Прежде изменение замечалось за
время опроса: от трёх секунд при активности до двух минут в тишине.

Из канала берётся ОДНО решение: изменилось что-нибудь или нет. События
по-прежнему собираются чтением страниц, тем же кодом и с теми же гарантиями.
Так и задумано: поведения канала при истёкшей сессии и при исчерпании предела не
наблюдал никто, а сигналу верить не нужно - ошибка в одну сторону стоит лишнего
чтения страниц, в другую ловится сторожевым сроком в две минуты.

Непонятный ответ канала не роняет наблюдение: оно возвращается к опросу страниц
и говорит об этом в журнал. Выключить быстрый путь целиком -
`client.watch(router, use_channel=False)`.

Для операций записи существенны исход запроса и сохранность прежнего состояния.

**Отправка текста** - [глава в руководстве](https://github.com/Funora-Develop/Funora-python/blob/v0.0.1.dev2/docs/guide/sending.md): исходов у неё
три, а не два, и третий - «неизвестно» - это то, ради чего глава написана.

**Правка цены** - [глава про лоты](https://github.com/Funora-Develop/Funora-python/blob/v0.0.1.dev2/docs/guide/lots.md): отправляется прочитанное
целиком, меняется ровно одно поле, а прежняя цена ложится в долговечный журнал
раньше, чем уходит запрос. Без файла состояния операция отказывает: у площадки
нет ни истории цен, ни отката, и «как было» знает только наша запись.

Поверх этого есть слой бота: `funora.bot`. Он даёт очередь исходящих, в которую
можно класть **из любого потока** - скажем, из обработчика телеграм-бота, - а
отправляет тот же поток, что ведёт наблюдение. Прямой вызов из чужого потока
портит счёт ограничителя молча, и потому отвергается вслух.

Есть и вторая очередь - **каталог с файлами**, для телеграм-бота, поднятого
ОТДЕЛЬНОЙ командой: до очереди в памяти он не дотягивается ничем. Задание,
взятое умершим процессом, повторно не отправляется никогда - его судьба
неизвестна, и решает о нём человек. Как это выглядит целиком - в [главе про
бота](https://github.com/Funora-Develop/Funora-python/blob/v0.0.1.dev2/docs/guide/bot.md).

Полный реестр незавершённых механизмов с причинами лежит в
`Funora-spec/spec/conformance/not-implemented.yaml`. Он включает ограничения
планировщика, недостающие наблюдения и ещё не исполняемые части общего контракта.
Наличие всех сервисных методов не означает, что весь межъязыковой контракт завершён.

## Чего SDK пока не умеет

- Вывод средств не реализован.
- Догрузка длинных списков заказов и операций счёта: семантика `continue`
  ещё не установлена. Предыдущие сообщения чата читаются отдельной операцией.
- Восстановление пропущенных событий канала по позиции: сейчас используются
  повторное чтение страниц и сохранённые курсоры наблюдения.
- Отмена уже отправленного запроса ради более приоритетного.

Публичное чтение рынка уже использует отдельный транспорт без секрета аккаунта.
Общий сетевой бюджет и пауза после HTTP 429 сохраняются.

Читаются состояния заказа `paid`, `closed` и `refunded`. Другие носители
сохраняются как ненаблюдённое значение; выдачу нужно разрешать по конкретному
состоянию, а не по условию «не закрыт». Сообщение в переписке не подтверждает оплату.

`orders.details()` читает сумму числом и код валюты из структурного ответа.
Точное время нельзя восстановить там, где площадка даёт только текст для показа.

Возврат по заказу уже реализован: доступность формы проверяется перед запросом,
а результат определяется по ответу. Проверка этой реализации на записанных
ответах не заменяет проверку на действующем тестовом аккаунте.

Подробности: [границы SDK](https://github.com/Funora-Develop/Funora-python/blob/v0.0.1.dev2/docs/limits.md),
[план наблюдений](https://github.com/Funora-Develop/Funora-python/blob/v0.0.1.dev2/docs/observation-plan.md).

## Как устроено

Три решения, которые видно в первом же вызове.

**Результат - страница, а не список.** Записи получают методом `rows()`, и при
неполноте нужен явный `accept_incomplete=True`. Молча отданный неполный список
неотличим от полного, и обработчик примет решение по данным, которых нет.

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

**Механические части порождаются из спецификации.** Ошибки, возможности,
политики повторов, бюджет и таблица соответствия вердиктов ошибкам не пишутся
руками ни в одном из шести SDK. Сборка падает, если порождённое отстало от
источника.

Подробнее - в [docs/architecture.md](https://github.com/Funora-Develop/Funora-python/blob/v0.0.1.dev2/docs/architecture.md), а как этим
пользоваться - в [руководстве](https://github.com/Funora-Develop/Funora-python/blob/v0.0.1.dev2/docs/index.md).

## Наблюдения за протоколом

Пакет содержит инструмент `funora-observe`, которым собраны все факты о
протоколе, на которых стоит спецификация. Он сохраняет структурный скелет
страницы: разметка целиком, текст и значения атрибутов заменены подписями.

- [docs/observations.md](https://github.com/Funora-Develop/Funora-python/blob/v0.0.1.dev2/docs/observations.md) - что установлено и как проверить.
- [docs/protocol-questions.md](https://github.com/Funora-Develop/Funora-python/blob/v0.0.1.dev2/docs/protocol-questions.md) - что осталось открытым.
- [docs/limits.md](https://github.com/Funora-Develop/Funora-python/blob/v0.0.1.dev2/docs/limits.md) - чего Funora не умеет и почему это не чинится кодом.
- [docs/observing.md](https://github.com/Funora-Develop/Funora-python/blob/v0.0.1.dev2/docs/observing.md) - как снять наблюдение самому.
- [tests/fixtures/pages/README.md](https://github.com/Funora-Develop/Funora-python/blob/v0.0.1.dev2/tests/fixtures/pages/README.md) - формат
  снимков и почему их можно публиковать.

## Проект целиком

Funora - это один контракт, реализованный нативно на нескольких языках. Меняется язык,
но не ментальная модель: `Client`, сервисы, события, роутер и
таксономия ошибок означают одно и то же везде.

| Репозиторий | Что это | Статус |
|---|---|---|
| [Funora](https://github.com/Funora-Develop/Funora) | Один контракт, один набор тестовых векторов, нативный SDK на каждый язык. | `design` |
| [Funora-spec](https://github.com/Funora-Develop/Funora-spec) | Канонический контракт, который реализует каждый SDK. | `design` |
| [Funora-codegen](https://github.com/Funora-Develop/Funora-codegen) | Генерирует скучную повторяющуюся часть каждого SDK. | `design` |
| [Funora-conformance](https://github.com/Funora-Develop/Funora-conformance) | Тестовый контракт между языками. | `design` |
| [Funora-python](https://github.com/Funora-Develop/Funora-python) | Эталонная реализация контракта Funora. | `draft` |
| [Funora-javascript](https://github.com/Funora-Develop/Funora-javascript) | Исходник на TypeScript, на выходе JavaScript и декларации типов. | `planned` |
| [Funora-java](https://github.com/Funora-Develop/Funora-java) | Java SDK. | `planned` |
| [Funora-dotnet](https://github.com/Funora-Develop/Funora-dotnet) | .NET SDK. | `planned` |
| [Funora-cpp](https://github.com/Funora-Develop/Funora-cpp) | C++ SDK. | `planned` |
| [Funora-c](https://github.com/Funora-Develop/Funora-c) | C SDK - самый узкий контракт в проекте. | `planned` |
| [Funora-docs](https://github.com/Funora-Develop/Funora-docs) | Документация всех SDK из одного источника. | `design` |
| [Funora-examples](https://github.com/Funora-Develop/Funora-examples) | Сквозные примеры, которые реально прогоняет CI. | `planned` |

## Участие в разработке

Сначала прочитайте [CONTRIBUTING.md](https://github.com/Funora-Develop/.github/blob/main/CONTRIBUTING.md).

Полезнее всего сейчас три вещи.

Снимки страниц в состояниях, которых у нас нет: заказ в возврате или споре,
непрочитанный диалог, длинный список с постраничной навигацией. Каждый такой
снимок закрывает пункт в [docs/protocol-questions.md](https://github.com/Funora-Develop/Funora-python/blob/v0.0.1.dev2/docs/protocol-questions.md).

Разбор спецификации в [Funora-spec](https://github.com/Funora-Develop/Funora-spec):
она проверяется употреблением, и первая же попытка её применить дала восемнадцать
мест, где она противоречила сама себе.

Реализация операций чтения по уже написанным правилам извлечения.

## Безопасность

Никогда не вставляйте сессионный ключ, сырой HTML со страницы под авторизацией или содержимое
личной переписки в публичный issue. Сессионный ключ FunPay - это доступ ко всему аккаунту.
Сообщайте приватно через [Security Advisories](https://github.com/Funora-Develop/Funora/security/advisories/new),
подробности - в [SECURITY.md](https://github.com/Funora-Develop/.github/blob/main/SECURITY.md).

## Лицензия

[Apache-2.0](https://github.com/Funora-Develop/Funora-python/blob/v0.0.1.dev2/LICENSE) © Funora Contributors
