Metadata-Version: 2.4
Name: zenmoney-python
Version: 0.7.0
Summary: Independent sync and async Python client for the public ZenMoney API
License-Expression: MIT
Project-URL: Repository, https://github.com/vaslv/zenmoney-python
Project-URL: Issues, https://github.com/vaslv/zenmoney-python/issues
Project-URL: Documentation, https://github.com/vaslv/zenmoney-python/blob/main/README.md
Keywords: zenmoney,finance,api,asyncio
Classifier: Development Status :: 4 - Beta
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Typing :: Typed
Classifier: Operating System :: OS Independent
Requires-Python: >=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: httpx<0.29,>=0.28
Requires-Dist: simplejson<4,>=3.19
Provides-Extra: test
Requires-Dist: pytest<10,>=8; extra == "test"
Requires-Dist: pytest-cov<8,>=6; extra == "test"
Requires-Dist: mypy<2,>=1.15; extra == "test"
Requires-Dist: types-simplejson<4,>=3.19; extra == "test"
Dynamic: license-file

# zenmoney-python

Самостоятельная Python-библиотека публичного HTTP API ZenMoney. Проект находится
рядом с ZenImport и развивается независимо. Distribution: `zenmoney-python`,
импорт: `zenmoney_api`, версия 0.7.0. Python 3.12+, HTTPX 0.28.x, simplejson для точных JSON-чисел.

## Возможности

- Синхронный и асинхронный клиент: полная и инкрементальная синхронизация diff,
  forceFetch, создание/обновление/удаление пользовательских сущностей.
- Счета, категории, получатели, операции, бюджеты, напоминания и планируемые
  операции; чтение системных справочников. Типы полей для редактора в `models`.
- Функции создания дохода, расхода, перевода, долговой операции; сохранение
  оригинальных сумм/валют, нескольких категорий и неизвестных полей при обновлении.
- Suggest: один объект или список; OAuth code exchange и явное обновление токена.
- Без автоматических повторов записи, скрытого хранения данных или зависимости
  от UI/банков. Единственный публичный namespace — `zenmoney_api`.

Область покрытия и честные ограничения: [docs/coverage.md](docs/coverage.md).
Сценарии расходов, возвратов, долгов и переводов: [docs/basic-operations.md](docs/basic-operations.md).

## Установка

Готовые wheel, sdist и SHA256SUMS доступны в [GitHub Releases](https://github.com/vaslv/zenmoney-python/releases).
Установка версии 0.7.0 из GitHub (Python 3.12+):

```sh
python -m pip install "https://github.com/vaslv/zenmoney-python/releases/download/v0.7.0/zenmoney_python-0.7.0-py3-none-any.whl"
```

Пакет ещё не загружен в PyPI. Установка из исходников: `python -m pip install .`.
Для собственной сборки выполните `sh scripts/build.sh`: в `dist/` появятся wheel,
sdist и SHA256SUMS. Twine проверяет метаданные; wheel собирается из sdist.
Имя репозитория и дистрибутива — `zenmoney-python`, Python-import — `zenmoney_api`.
Прежнее локальное имя `zenmoney-api-client` больше не используется; перед
переходом в существующем окружении удалите старый дистрибутив или создайте чистое
окружение, чтобы два пакета не владели одним namespace.

Порядок первого GitHub-релиза и интеграции приложений: [docs/publication.md](docs/publication.md).

## Чтение

```python
import os
from zenmoney_api import Client

with Client(os.environ['ZENMONEY_TOKEN']) as api:
    initial = api.read_diff()  # cursor=0 — первоначальная синхронизация
    # Сначала атомарно сохраните сущности и удаления, затем курсор в своей БД.
    cursor = initial['serverTimestamp']
    changes = api.read_diff(cursor)
    accounts = api.read_diff(cursor, force_fetch=['account'])
```

Клиент сохраняет неизвестные поля в словарях; `Diff.raw_json` содержит исходный
ответ. Целые числа читаются как `int`, десятичные — как `Decimal`. Для денег
используйте `Decimal('1.23')` или целые числа VND: при отправке они остаются
JSON-числами без округления через float. `float` принимается, но уже возникшую
в нём погрешность SDK исправить не может. NaN/Infinity и не-JSON объекты отклоняются
до запроса. SDK не пересчитывает валюты. Обычный `json.dumps` не сериализует Decimal;
для исходного ответа используйте `Diff.raw_json`, для подготовленных объектов —
`zenmoney_api.protocol.encode_json`.

```python
from zenmoney_api import AsyncClient

async def load(token):
    async with AsyncClient(token) as api:
        return await api.read_diff()
```

Используйте контекстный менеджер либо `close()` / `aclose()` для освобождения
соединений. Закрытый клиент не используется повторно: он вызывает `ClientClosedError`;
создайте новый экземпляр. `is_closed` показывает состояние. Один экземпляр
асинхронного клиента используйте в одном event loop. Параметр `timeout` принимает
секунды, `httpx.Timeout` или явно отключённый таймаут `None`.

## Подготовка и отправка

```python
from zenmoney_api import transactions as tx

operation = tx.expense(user=123, account='account-id', instrument=1,
    amount=100000, date='2026-10-13', categories=['category-id'], payee='Shop')
# Сохраните operation и его UUID до отправки. Повтор не должен создавать новый UUID.
# Выполнить запись, когда вызывающее приложение готово:
# result = api.upsert('transaction', [operation], cursor)
# Затем повторно получите данные и проверьте наличие/содержимое операции.
```

`transaction(...)` формирует общий объект; `income`, `expense`, `transfer`, `debt`
упрощают типовые случаи. Примеры: [examples/operations.py](examples/operations.py).
`update(fetched_object, tag=[...])` копирует полный объект с сохранением полей.

`api.diff(cursor, changes={'account': [...], 'tag': [...], 'transaction': [...]},
deletions=[...])` позволяет передать несколько типов сущностей одним запросом.
`upsert(kind, entities, cursor)` — удобная обёртка.
Для новых объектов каталога есть полные формы с актуальными полями:
`entities.account(user=..., title=..., instrument=...)`, `entities.tag(...)`,
`entities.merchant(...)`. Полученный объект можно передать в upsert.
Для loan/deposit задайте параметры срока и процентов; системный debt-счёт
следует получать из diff, а не создавать повторно. **Передавайте полные сущности,
не patch**. Низкоуровневый интерфейс проверяет структуру, идентификаторы, timestamps
и JSON, но не подменяет серверную проверку всех полей. Ограничения/пакетирование
определяет вызывающее приложение, скрытого лимита в 20 записей в общем SDK нет.

`deletion(kind, id, user=..., now=...)` создаёт удаление по ID. Для бюджета с
составным ключом используйте `clear_budget` и `upsert('budget', ...)`.
Мягкое удаление операции — `update(operation, deleted=True)`.

`api.suggest({'payee': 'Shop'})` или список объектов возвращает подсказки API,
не изменяя записи.

## OAuth (необязательно)

Для diff/suggest достаточно готового Bearer-токена в Client/AsyncClient.
Регистрация приложения и собственный OAuth не требуются для использования SDK.
Методы ниже предназначены для владельцев уже выданных реквизитов приложения;
их живая проверка не является условием приёмки основного API.


```python
import secrets
from zenmoney_api import OAuthClient, callback_code

with OAuthClient('registered-client-id', 'client-secret', 'https://your-app/callback') as oauth:
    state = secrets.token_urlsafe(32)  # сохраните в сессии до открытия URL
    url = oauth.authorization_url(state=state)
    # code = callback_code(received_callback_url, expected_state=state)
    # token = oauth.exchange_code(code)
    # if token.refresh_token is not None:
    #     refreshed = oauth.refresh(token.refresh_token)
```

Для async есть независимый `AsyncOAuthClient` с теми же аргументами:
используйте `async with`, а `exchange_code` и `refresh` вызывайте с `await`.
OAuth-клиенты переиспользуют HTTPX-клиент и переданный транспорт между запросами.
При работе без контекстного менеджера вызовите `close()` / `await aclose()`.
Оба способа освобождают открытый HTTPX-клиент и его транспорт; повторное закрытие
безопасно. После закрытия `is_closed` равен `True`, а обмен кода, обновление токена
и повторный вход в контекст вызывают `ClientClosedError`. Создайте новый экземпляр
для следующего сеанса. `AsyncOAuthClient` используйте в рамках одного event loop.
SDK не поднимает callback-сервер, не регистрирует приложение, не хранит токены
и не обновляет их автоматически. Если refresh-ответ не содержит нового
refresh_token, приложение сохраняет прежний. `Token.__repr__` скрывает токены.
State проверяется до обмена code; привязка callback к сессии — задача приложения.

## Ошибки и подтверждение записи

`HTTPError.status_code`, `TransportError`, `ResponseError` не включают тело ответа
и токен. HTTP 200 не гарантирует принятия всех изменений: более новый `changed`
на сервере может победить. После записи требуется отдельное чтение и сверка;
при неопределённом результате нельзя создавать другой UUID и слепо повторять.
Синхронизация БД, пользовательское подтверждение, журнал отправок и reconciliation
принадлежат приложению, а не SDK.

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

```sh
sh scripts/test.sh
sh scripts/build.sh
```

Тесты используют установленный wheel без дерева исходников, без сети и без
настоящих токенов. Есть проверка реального HTTP на локальном тестовом сервере.
Тестовый образ проверяет строгую типизацию mypy, типовые примеры использования
публичного API и выводит покрытие. Сборка требует доступа к реестру образов и PyPI.

## Переход с 0.4

Версия 0.5 намеренно меняет контракт: `zenimport_zenmoney` удалён, десятичные
числа ответа теперь Decimal, закрытый клиент не открывается повторно. Для записи
используйте `diff`/`upsert`, для типовых объектов — `transactions`.
Старые приложения следует адаптировать после приёмки SDK; их ограничения
не определяют дизайн библиотеки.
[Архитектура](docs/architecture.md) · [Покрытие API](docs/coverage.md) ·
[Проверка на настоящем API](docs/live-validation.md) · [История](CHANGELOG.md).


## Живая приёмка

[Результаты проверки на тестовом аккаунте](docs/live-results.md).
Для собственного отдельного тестового аккаунта скопируйте `.env.example` в `.env`
и заполните ZENMONEY_TEST_TOKEN. Шаблон содержит также все необязательные переменные
OAuth. Файл не попадает в Git и образ Docker.

`sh scripts/live-check.sh` выполняет только чтение. `--write` явно включает запись
искусственных данных и требует первоначально пустого отдельного аккаунта;
`--soft-delete` и `--cleanup` работают только с UUID сохранённого тестового журнала.
После ошибки автоматического повтора нет. `--resume` разрешает только повторную
сверку и продолжение подтверждённых шагов, без повторения неизвестной записи.
Завершённый очищенный журнал не используется для нового запуска.
Для нового запуска задайте другой путь `--journal /state/new-run.json`.

`--write --basic --journal /state/basic-run.json` проверяет согласованный базовый
набор операций, баланс после каждого шага, долговые движения и отсутствие дубля
при явной записи того же UUID. `--cleanup --journal /state/basic-run.json`
убирает тестовые объекты; `--inspect` выводит только названия отличающихся полей.
