zenmoney-python 0.7.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. zenmoney_python-0.7.0/.dockerignore +21 -0
  2. zenmoney_python-0.7.0/CHANGELOG.md +63 -0
  3. zenmoney_python-0.7.0/Dockerfile +27 -0
  4. zenmoney_python-0.7.0/LICENSE +21 -0
  5. zenmoney_python-0.7.0/MANIFEST.in +11 -0
  6. zenmoney_python-0.7.0/PKG-INFO +224 -0
  7. zenmoney_python-0.7.0/README.md +198 -0
  8. zenmoney_python-0.7.0/docs/TASK.md +63 -0
  9. zenmoney_python-0.7.0/docs/architecture.md +44 -0
  10. zenmoney_python-0.7.0/docs/basic-operations.md +70 -0
  11. zenmoney_python-0.7.0/docs/coverage.md +61 -0
  12. zenmoney_python-0.7.0/docs/importer-contract-audit.md +36 -0
  13. zenmoney_python-0.7.0/docs/live-results.md +112 -0
  14. zenmoney_python-0.7.0/docs/live-validation.md +48 -0
  15. zenmoney_python-0.7.0/docs/publication.md +71 -0
  16. zenmoney_python-0.7.0/examples/operations.py +21 -0
  17. zenmoney_python-0.7.0/pyproject.toml +42 -0
  18. zenmoney_python-0.7.0/requirements.build.txt +5 -0
  19. zenmoney_python-0.7.0/scripts/build.sh +5 -0
  20. zenmoney_python-0.7.0/scripts/live-check.sh +19 -0
  21. zenmoney_python-0.7.0/scripts/live_basic.py +165 -0
  22. zenmoney_python-0.7.0/scripts/live_categories.py +132 -0
  23. zenmoney_python-0.7.0/scripts/live_validation.py +360 -0
  24. zenmoney_python-0.7.0/scripts/release_artifacts.py +46 -0
  25. zenmoney_python-0.7.0/scripts/test.sh +7 -0
  26. zenmoney_python-0.7.0/setup.cfg +4 -0
  27. zenmoney_python-0.7.0/src/zenmoney_api/__init__.py +11 -0
  28. zenmoney_python-0.7.0/src/zenmoney_api/client.py +167 -0
  29. zenmoney_python-0.7.0/src/zenmoney_api/entities.py +63 -0
  30. zenmoney_python-0.7.0/src/zenmoney_api/errors.py +21 -0
  31. zenmoney_python-0.7.0/src/zenmoney_api/models.py +180 -0
  32. zenmoney_python-0.7.0/src/zenmoney_api/oauth.py +169 -0
  33. zenmoney_python-0.7.0/src/zenmoney_api/protocol.py +219 -0
  34. zenmoney_python-0.7.0/src/zenmoney_api/py.typed +0 -0
  35. zenmoney_python-0.7.0/src/zenmoney_api/transactions.py +148 -0
  36. zenmoney_python-0.7.0/src/zenmoney_python.egg-info/PKG-INFO +224 -0
  37. zenmoney_python-0.7.0/src/zenmoney_python.egg-info/SOURCES.txt +49 -0
  38. zenmoney_python-0.7.0/src/zenmoney_python.egg-info/dependency_links.txt +1 -0
  39. zenmoney_python-0.7.0/src/zenmoney_python.egg-info/requires.txt +8 -0
  40. zenmoney_python-0.7.0/src/zenmoney_python.egg-info/top_level.txt +1 -0
  41. zenmoney_python-0.7.0/tests/conftest.py +18 -0
  42. zenmoney_python-0.7.0/tests/test_api.py +286 -0
  43. zenmoney_python-0.7.0/tests/test_client.py +62 -0
  44. zenmoney_python-0.7.0/tests/test_entities.py +46 -0
  45. zenmoney_python-0.7.0/tests/test_live_acceptance.py +124 -0
  46. zenmoney_python-0.7.0/tests/test_live_basic.py +105 -0
  47. zenmoney_python-0.7.0/tests/test_live_categories.py +41 -0
  48. zenmoney_python-0.7.0/tests/test_oauth_lifecycle.py +158 -0
  49. zenmoney_python-0.7.0/tests/test_universal.py +153 -0
  50. zenmoney_python-0.7.0/tests/test_unreviewed_delivery.py +78 -0
  51. zenmoney_python-0.7.0/tests/typing_contract.py +44 -0
@@ -0,0 +1,21 @@
1
+ *
2
+ !pyproject.toml
3
+ !README.md
4
+ !CHANGELOG.md
5
+ !LICENSE
6
+ !MANIFEST.in
7
+ !requirements.build.txt
8
+ !docs/
9
+ !docs/**
10
+ !examples/
11
+ !examples/**
12
+ !src/
13
+ !src/**
14
+ !tests/
15
+ !tests/**
16
+ **/__pycache__
17
+ !scripts/
18
+ !scripts/*.py
19
+ !scripts/*.sh
20
+ !Dockerfile
21
+ !.dockerignore
@@ -0,0 +1,63 @@
1
+ # Изменения
2
+
3
+ ## 0.7.0 — 17 октября 2026
4
+
5
+ - Дистрибутив переименован в zenmoney-python; импорт zenmoney_api сохранён.
6
+ - Добавлены wheel/sdist, проверка Twine, контроль содержимого и SHA256SUMS.
7
+ - Подготовлен CI, который проверяет установленный пакет и сохраняет сборки.
8
+ - Автоматической публикации, push и создания тегов нет.
9
+
10
+ ## 0.6.1 — 16 октября 2026 (подготовлено локально)
11
+
12
+ - OAuthClient и AsyncOAuthClient сохраняют HTTPX-транспорт между обменом кода
13
+ и обновлением токена. Добавлены close/aclose, контекстные менеджеры и is_closed;
14
+ закрытый экземпляр отклоняет запросы с ClientClosedError.
15
+ - Добавлены регрессии sync/async с транспортом, недоступным после закрытия,
16
+ и типизированные примеры управления OAuth-клиентом.
17
+ - Docker-проверки: 260 тестов, строгий mypy, покрытие 99%. Настоящий API
18
+ не вызывался; пакет не опубликован.
19
+
20
+ ## Проверки 0.6.0 — 13 октября 2026
21
+
22
+ - Подтверждены настоящим API 18 базовых операций, включая возвраты, разбиение
23
+ расхода, все направления долгов, переводы и суммы VND.
24
+ - Добавлена проверка баланса после каждого шага, долговых движений и повторного
25
+ upsert одного UUID без дубля; документированы правила возвратов и переводов.
26
+ - OAuth-регистрация исключена из обязательной приёмки с готовым Bearer-токеном.
27
+ - Усилены локальные проверки сценариев приёмки, диагностики и очистки.
28
+
29
+ ## 0.6.0 — 13 октября 2026
30
+
31
+ - Проведена живая приёмка на отдельном тестовом аккаунте; тестовые данные убраны.
32
+ - Добавлены текущие поля Account, Tag, Merchant, Budget и ReminderMarker.
33
+ - Добавлены полные формы каталога entities.account/tag/merchant, учитывающие
34
+ обязательные поля API, отсутствующие в старой wiki.
35
+ - Добавлен явный Docker-сценарий приёмки с локальным журналом, привязкой пользователя,
36
+ остановкой после неподтверждённой записи и удалением только своих UUID.
37
+ - Шаблон всех переменных тестового подключения; токен остаётся локально вне Git.
38
+
39
+ ## 0.5.0 — 13 октября 2026
40
+
41
+ - Удалены namespace zenimport_zenmoney и тесты прикладных ограничений.
42
+ - Дизайн SDK независим от требований совместимости ZenImport.
43
+ - Decimal сериализуется точно как JSON-число и используется для десятичных ответов.
44
+ - Явный lifecycle: закрытые клиенты не открываются повторно, есть is_closed.
45
+ - AsyncOAuthClient больше не наследует синхронный OAuthClient.
46
+ - Типизированные публичные методы и параметры builders; строгая проверка mypy.
47
+ - Поддержка Mapping/Sequence, копирование запросов, предсказуемые ошибки JSON.
48
+ - Реальный UTF-8 проверяется до разбора; бесконечные/circular значения запрещены.
49
+
50
+ ## 0.4.0 — 13 октября 2026
51
+
52
+ - Выделен самостоятельный проект ZenMoneyAPI.
53
+ - Добавлен namespace zenmoney_api: sync/async diff, CRUD через diff и suggest.
54
+ - Добавлены типы всех документированных сущностей, builders переводов/долгов,
55
+ обновление полных объектов с сохранением неизвестных полей, удаления и бюджет.
56
+ - OAuth code/refresh, state validation; явное управление токенами.
57
+ - Совместимость zenimport_zenmoney сохранена для прежнего backend.
58
+ - Автономная Docker-сборка wheel, контрактные и локальные HTTP-тесты.
59
+
60
+ ## Исходная версия 0.3.0
61
+
62
+ Извлечена из ZenImport. Чтение diff, ограниченная запись категорий и обычных
63
+ операций; полный набор nullable-полей создания, исправляющий HTTP 400.
@@ -0,0 +1,27 @@
1
+ FROM python:3.12-slim-bookworm@sha256:54c85f3c47607a77f32adec749d3c81d1348bf25833671f512b26a9b6d778cb3 AS wheel
2
+ WORKDIR /build
3
+ COPY requirements.build.txt ./
4
+ RUN python -m pip install --no-cache-dir -r requirements.build.txt
5
+ # Allowlisted context: source/docs only, never local secrets or state.
6
+ COPY . .
7
+ ARG SOURCE_DATE_EPOCH=0
8
+ ENV SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH
9
+ RUN python -m build --no-isolation --outdir /artifacts \
10
+ && python -m twine check --strict /artifacts/* \
11
+ && python scripts/release_artifacts.py /artifacts \
12
+ && python -m pip wheel --no-cache-dir --wheel-dir /wheels '.[test]' \
13
+ && cp /artifacts/*.whl /wheels/
14
+
15
+ FROM scratch AS artifact
16
+ COPY --from=wheel /artifacts/ /
17
+
18
+ FROM python:3.12-slim-bookworm@sha256:54c85f3c47607a77f32adec749d3c81d1348bf25833671f512b26a9b6d778cb3 AS test
19
+ ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 COVERAGE_FILE=/tmp/.coverage
20
+ COPY --from=wheel /wheels /wheels
21
+ RUN python -m pip install --no-cache-dir --no-index --find-links=/wheels 'zenmoney-python[test]' \
22
+ && python -m pip check && rm -r /wheels
23
+ WORKDIR /tests
24
+ COPY tests ./
25
+ COPY scripts/live_validation.py scripts/live_basic.py scripts/live_categories.py /tools/
26
+ USER 10001:10001
27
+ CMD ["sh", "-c", "python -m mypy --strict --cache-dir=/tmp/mypy -p zenmoney_api && python -m mypy --strict --cache-dir=/tmp/mypy /tests/typing_contract.py && python -m pytest -q -p no:cacheprovider --cov=zenmoney_api --cov-report=term-missing /tests"]
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Andrei Vasilev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,11 @@
1
+ include README.md CHANGELOG.md
2
+ include Dockerfile .dockerignore requirements.build.txt
3
+ recursive-include docs *.md
4
+ recursive-include examples *.py
5
+ recursive-include tests *.py
6
+ recursive-include scripts *.py *.sh
7
+ global-exclude *.pyc
8
+ global-exclude .env
9
+ global-exclude .env.*
10
+ prune .local
11
+ prune .idea
@@ -0,0 +1,224 @@
1
+ Metadata-Version: 2.4
2
+ Name: zenmoney-python
3
+ Version: 0.7.0
4
+ Summary: Independent sync and async Python client for the public ZenMoney API
5
+ License-Expression: MIT
6
+ Project-URL: Repository, https://github.com/vaslv/zenmoney-python
7
+ Project-URL: Issues, https://github.com/vaslv/zenmoney-python/issues
8
+ Project-URL: Documentation, https://github.com/vaslv/zenmoney-python/blob/main/README.md
9
+ Keywords: zenmoney,finance,api,asyncio
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Typing :: Typed
14
+ Classifier: Operating System :: OS Independent
15
+ Requires-Python: >=3.12
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: httpx<0.29,>=0.28
19
+ Requires-Dist: simplejson<4,>=3.19
20
+ Provides-Extra: test
21
+ Requires-Dist: pytest<10,>=8; extra == "test"
22
+ Requires-Dist: pytest-cov<8,>=6; extra == "test"
23
+ Requires-Dist: mypy<2,>=1.15; extra == "test"
24
+ Requires-Dist: types-simplejson<4,>=3.19; extra == "test"
25
+ Dynamic: license-file
26
+
27
+ # zenmoney-python
28
+
29
+ Самостоятельная Python-библиотека публичного HTTP API ZenMoney. Проект находится
30
+ рядом с ZenImport и развивается независимо. Distribution: `zenmoney-python`,
31
+ импорт: `zenmoney_api`, версия 0.7.0. Python 3.12+, HTTPX 0.28.x, simplejson для точных JSON-чисел.
32
+
33
+ ## Возможности
34
+
35
+ - Синхронный и асинхронный клиент: полная и инкрементальная синхронизация diff,
36
+ forceFetch, создание/обновление/удаление пользовательских сущностей.
37
+ - Счета, категории, получатели, операции, бюджеты, напоминания и планируемые
38
+ операции; чтение системных справочников. Типы полей для редактора в `models`.
39
+ - Функции создания дохода, расхода, перевода, долговой операции; сохранение
40
+ оригинальных сумм/валют, нескольких категорий и неизвестных полей при обновлении.
41
+ - Suggest: один объект или список; OAuth code exchange и явное обновление токена.
42
+ - Без автоматических повторов записи, скрытого хранения данных или зависимости
43
+ от UI/банков. Единственный публичный namespace — `zenmoney_api`.
44
+
45
+ Область покрытия и честные ограничения: [docs/coverage.md](docs/coverage.md).
46
+ Сценарии расходов, возвратов, долгов и переводов: [docs/basic-operations.md](docs/basic-operations.md).
47
+
48
+ ## Установка
49
+
50
+ Готовые wheel, sdist и SHA256SUMS доступны в [GitHub Releases](https://github.com/vaslv/zenmoney-python/releases).
51
+ Установка версии 0.7.0 из GitHub (Python 3.12+):
52
+
53
+ ```sh
54
+ python -m pip install "https://github.com/vaslv/zenmoney-python/releases/download/v0.7.0/zenmoney_python-0.7.0-py3-none-any.whl"
55
+ ```
56
+
57
+ Пакет ещё не загружен в PyPI. Установка из исходников: `python -m pip install .`.
58
+ Для собственной сборки выполните `sh scripts/build.sh`: в `dist/` появятся wheel,
59
+ sdist и SHA256SUMS. Twine проверяет метаданные; wheel собирается из sdist.
60
+ Имя репозитория и дистрибутива — `zenmoney-python`, Python-import — `zenmoney_api`.
61
+ Прежнее локальное имя `zenmoney-api-client` больше не используется; перед
62
+ переходом в существующем окружении удалите старый дистрибутив или создайте чистое
63
+ окружение, чтобы два пакета не владели одним namespace.
64
+
65
+ Порядок первого GitHub-релиза и интеграции приложений: [docs/publication.md](docs/publication.md).
66
+
67
+ ## Чтение
68
+
69
+ ```python
70
+ import os
71
+ from zenmoney_api import Client
72
+
73
+ with Client(os.environ['ZENMONEY_TOKEN']) as api:
74
+ initial = api.read_diff() # cursor=0 — первоначальная синхронизация
75
+ # Сначала атомарно сохраните сущности и удаления, затем курсор в своей БД.
76
+ cursor = initial['serverTimestamp']
77
+ changes = api.read_diff(cursor)
78
+ accounts = api.read_diff(cursor, force_fetch=['account'])
79
+ ```
80
+
81
+ Клиент сохраняет неизвестные поля в словарях; `Diff.raw_json` содержит исходный
82
+ ответ. Целые числа читаются как `int`, десятичные — как `Decimal`. Для денег
83
+ используйте `Decimal('1.23')` или целые числа VND: при отправке они остаются
84
+ JSON-числами без округления через float. `float` принимается, но уже возникшую
85
+ в нём погрешность SDK исправить не может. NaN/Infinity и не-JSON объекты отклоняются
86
+ до запроса. SDK не пересчитывает валюты. Обычный `json.dumps` не сериализует Decimal;
87
+ для исходного ответа используйте `Diff.raw_json`, для подготовленных объектов —
88
+ `zenmoney_api.protocol.encode_json`.
89
+
90
+ ```python
91
+ from zenmoney_api import AsyncClient
92
+
93
+ async def load(token):
94
+ async with AsyncClient(token) as api:
95
+ return await api.read_diff()
96
+ ```
97
+
98
+ Используйте контекстный менеджер либо `close()` / `aclose()` для освобождения
99
+ соединений. Закрытый клиент не используется повторно: он вызывает `ClientClosedError`;
100
+ создайте новый экземпляр. `is_closed` показывает состояние. Один экземпляр
101
+ асинхронного клиента используйте в одном event loop. Параметр `timeout` принимает
102
+ секунды, `httpx.Timeout` или явно отключённый таймаут `None`.
103
+
104
+ ## Подготовка и отправка
105
+
106
+ ```python
107
+ from zenmoney_api import transactions as tx
108
+
109
+ operation = tx.expense(user=123, account='account-id', instrument=1,
110
+ amount=100000, date='2026-10-13', categories=['category-id'], payee='Shop')
111
+ # Сохраните operation и его UUID до отправки. Повтор не должен создавать новый UUID.
112
+ # Выполнить запись, когда вызывающее приложение готово:
113
+ # result = api.upsert('transaction', [operation], cursor)
114
+ # Затем повторно получите данные и проверьте наличие/содержимое операции.
115
+ ```
116
+
117
+ `transaction(...)` формирует общий объект; `income`, `expense`, `transfer`, `debt`
118
+ упрощают типовые случаи. Примеры: [examples/operations.py](examples/operations.py).
119
+ `update(fetched_object, tag=[...])` копирует полный объект с сохранением полей.
120
+
121
+ `api.diff(cursor, changes={'account': [...], 'tag': [...], 'transaction': [...]},
122
+ deletions=[...])` позволяет передать несколько типов сущностей одним запросом.
123
+ `upsert(kind, entities, cursor)` — удобная обёртка.
124
+ Для новых объектов каталога есть полные формы с актуальными полями:
125
+ `entities.account(user=..., title=..., instrument=...)`, `entities.tag(...)`,
126
+ `entities.merchant(...)`. Полученный объект можно передать в upsert.
127
+ Для loan/deposit задайте параметры срока и процентов; системный debt-счёт
128
+ следует получать из diff, а не создавать повторно. **Передавайте полные сущности,
129
+ не patch**. Низкоуровневый интерфейс проверяет структуру, идентификаторы, timestamps
130
+ и JSON, но не подменяет серверную проверку всех полей. Ограничения/пакетирование
131
+ определяет вызывающее приложение, скрытого лимита в 20 записей в общем SDK нет.
132
+
133
+ `deletion(kind, id, user=..., now=...)` создаёт удаление по ID. Для бюджета с
134
+ составным ключом используйте `clear_budget` и `upsert('budget', ...)`.
135
+ Мягкое удаление операции — `update(operation, deleted=True)`.
136
+
137
+ `api.suggest({'payee': 'Shop'})` или список объектов возвращает подсказки API,
138
+ не изменяя записи.
139
+
140
+ ## OAuth (необязательно)
141
+
142
+ Для diff/suggest достаточно готового Bearer-токена в Client/AsyncClient.
143
+ Регистрация приложения и собственный OAuth не требуются для использования SDK.
144
+ Методы ниже предназначены для владельцев уже выданных реквизитов приложения;
145
+ их живая проверка не является условием приёмки основного API.
146
+
147
+
148
+ ```python
149
+ import secrets
150
+ from zenmoney_api import OAuthClient, callback_code
151
+
152
+ with OAuthClient('registered-client-id', 'client-secret', 'https://your-app/callback') as oauth:
153
+ state = secrets.token_urlsafe(32) # сохраните в сессии до открытия URL
154
+ url = oauth.authorization_url(state=state)
155
+ # code = callback_code(received_callback_url, expected_state=state)
156
+ # token = oauth.exchange_code(code)
157
+ # if token.refresh_token is not None:
158
+ # refreshed = oauth.refresh(token.refresh_token)
159
+ ```
160
+
161
+ Для async есть независимый `AsyncOAuthClient` с теми же аргументами:
162
+ используйте `async with`, а `exchange_code` и `refresh` вызывайте с `await`.
163
+ OAuth-клиенты переиспользуют HTTPX-клиент и переданный транспорт между запросами.
164
+ При работе без контекстного менеджера вызовите `close()` / `await aclose()`.
165
+ Оба способа освобождают открытый HTTPX-клиент и его транспорт; повторное закрытие
166
+ безопасно. После закрытия `is_closed` равен `True`, а обмен кода, обновление токена
167
+ и повторный вход в контекст вызывают `ClientClosedError`. Создайте новый экземпляр
168
+ для следующего сеанса. `AsyncOAuthClient` используйте в рамках одного event loop.
169
+ SDK не поднимает callback-сервер, не регистрирует приложение, не хранит токены
170
+ и не обновляет их автоматически. Если refresh-ответ не содержит нового
171
+ refresh_token, приложение сохраняет прежний. `Token.__repr__` скрывает токены.
172
+ State проверяется до обмена code; привязка callback к сессии — задача приложения.
173
+
174
+ ## Ошибки и подтверждение записи
175
+
176
+ `HTTPError.status_code`, `TransportError`, `ResponseError` не включают тело ответа
177
+ и токен. HTTP 200 не гарантирует принятия всех изменений: более новый `changed`
178
+ на сервере может победить. После записи требуется отдельное чтение и сверка;
179
+ при неопределённом результате нельзя создавать другой UUID и слепо повторять.
180
+ Синхронизация БД, пользовательское подтверждение, журнал отправок и reconciliation
181
+ принадлежат приложению, а не SDK.
182
+
183
+ ## Разработка и Docker
184
+
185
+ ```sh
186
+ sh scripts/test.sh
187
+ sh scripts/build.sh
188
+ ```
189
+
190
+ Тесты используют установленный wheel без дерева исходников, без сети и без
191
+ настоящих токенов. Есть проверка реального HTTP на локальном тестовом сервере.
192
+ Тестовый образ проверяет строгую типизацию mypy, типовые примеры использования
193
+ публичного API и выводит покрытие. Сборка требует доступа к реестру образов и PyPI.
194
+
195
+ ## Переход с 0.4
196
+
197
+ Версия 0.5 намеренно меняет контракт: `zenimport_zenmoney` удалён, десятичные
198
+ числа ответа теперь Decimal, закрытый клиент не открывается повторно. Для записи
199
+ используйте `diff`/`upsert`, для типовых объектов — `transactions`.
200
+ Старые приложения следует адаптировать после приёмки SDK; их ограничения
201
+ не определяют дизайн библиотеки.
202
+ [Архитектура](docs/architecture.md) · [Покрытие API](docs/coverage.md) ·
203
+ [Проверка на настоящем API](docs/live-validation.md) · [История](CHANGELOG.md).
204
+
205
+
206
+ ## Живая приёмка
207
+
208
+ [Результаты проверки на тестовом аккаунте](docs/live-results.md).
209
+ Для собственного отдельного тестового аккаунта скопируйте `.env.example` в `.env`
210
+ и заполните ZENMONEY_TEST_TOKEN. Шаблон содержит также все необязательные переменные
211
+ OAuth. Файл не попадает в Git и образ Docker.
212
+
213
+ `sh scripts/live-check.sh` выполняет только чтение. `--write` явно включает запись
214
+ искусственных данных и требует первоначально пустого отдельного аккаунта;
215
+ `--soft-delete` и `--cleanup` работают только с UUID сохранённого тестового журнала.
216
+ После ошибки автоматического повтора нет. `--resume` разрешает только повторную
217
+ сверку и продолжение подтверждённых шагов, без повторения неизвестной записи.
218
+ Завершённый очищенный журнал не используется для нового запуска.
219
+ Для нового запуска задайте другой путь `--journal /state/new-run.json`.
220
+
221
+ `--write --basic --journal /state/basic-run.json` проверяет согласованный базовый
222
+ набор операций, баланс после каждого шага, долговые движения и отсутствие дубля
223
+ при явной записи того же UUID. `--cleanup --journal /state/basic-run.json`
224
+ убирает тестовые объекты; `--inspect` выводит только названия отличающихся полей.
@@ -0,0 +1,198 @@
1
+ # zenmoney-python
2
+
3
+ Самостоятельная Python-библиотека публичного HTTP API ZenMoney. Проект находится
4
+ рядом с ZenImport и развивается независимо. Distribution: `zenmoney-python`,
5
+ импорт: `zenmoney_api`, версия 0.7.0. Python 3.12+, HTTPX 0.28.x, simplejson для точных JSON-чисел.
6
+
7
+ ## Возможности
8
+
9
+ - Синхронный и асинхронный клиент: полная и инкрементальная синхронизация diff,
10
+ forceFetch, создание/обновление/удаление пользовательских сущностей.
11
+ - Счета, категории, получатели, операции, бюджеты, напоминания и планируемые
12
+ операции; чтение системных справочников. Типы полей для редактора в `models`.
13
+ - Функции создания дохода, расхода, перевода, долговой операции; сохранение
14
+ оригинальных сумм/валют, нескольких категорий и неизвестных полей при обновлении.
15
+ - Suggest: один объект или список; OAuth code exchange и явное обновление токена.
16
+ - Без автоматических повторов записи, скрытого хранения данных или зависимости
17
+ от UI/банков. Единственный публичный namespace — `zenmoney_api`.
18
+
19
+ Область покрытия и честные ограничения: [docs/coverage.md](docs/coverage.md).
20
+ Сценарии расходов, возвратов, долгов и переводов: [docs/basic-operations.md](docs/basic-operations.md).
21
+
22
+ ## Установка
23
+
24
+ Готовые wheel, sdist и SHA256SUMS доступны в [GitHub Releases](https://github.com/vaslv/zenmoney-python/releases).
25
+ Установка версии 0.7.0 из GitHub (Python 3.12+):
26
+
27
+ ```sh
28
+ python -m pip install "https://github.com/vaslv/zenmoney-python/releases/download/v0.7.0/zenmoney_python-0.7.0-py3-none-any.whl"
29
+ ```
30
+
31
+ Пакет ещё не загружен в PyPI. Установка из исходников: `python -m pip install .`.
32
+ Для собственной сборки выполните `sh scripts/build.sh`: в `dist/` появятся wheel,
33
+ sdist и SHA256SUMS. Twine проверяет метаданные; wheel собирается из sdist.
34
+ Имя репозитория и дистрибутива — `zenmoney-python`, Python-import — `zenmoney_api`.
35
+ Прежнее локальное имя `zenmoney-api-client` больше не используется; перед
36
+ переходом в существующем окружении удалите старый дистрибутив или создайте чистое
37
+ окружение, чтобы два пакета не владели одним namespace.
38
+
39
+ Порядок первого GitHub-релиза и интеграции приложений: [docs/publication.md](docs/publication.md).
40
+
41
+ ## Чтение
42
+
43
+ ```python
44
+ import os
45
+ from zenmoney_api import Client
46
+
47
+ with Client(os.environ['ZENMONEY_TOKEN']) as api:
48
+ initial = api.read_diff() # cursor=0 — первоначальная синхронизация
49
+ # Сначала атомарно сохраните сущности и удаления, затем курсор в своей БД.
50
+ cursor = initial['serverTimestamp']
51
+ changes = api.read_diff(cursor)
52
+ accounts = api.read_diff(cursor, force_fetch=['account'])
53
+ ```
54
+
55
+ Клиент сохраняет неизвестные поля в словарях; `Diff.raw_json` содержит исходный
56
+ ответ. Целые числа читаются как `int`, десятичные — как `Decimal`. Для денег
57
+ используйте `Decimal('1.23')` или целые числа VND: при отправке они остаются
58
+ JSON-числами без округления через float. `float` принимается, но уже возникшую
59
+ в нём погрешность SDK исправить не может. NaN/Infinity и не-JSON объекты отклоняются
60
+ до запроса. SDK не пересчитывает валюты. Обычный `json.dumps` не сериализует Decimal;
61
+ для исходного ответа используйте `Diff.raw_json`, для подготовленных объектов —
62
+ `zenmoney_api.protocol.encode_json`.
63
+
64
+ ```python
65
+ from zenmoney_api import AsyncClient
66
+
67
+ async def load(token):
68
+ async with AsyncClient(token) as api:
69
+ return await api.read_diff()
70
+ ```
71
+
72
+ Используйте контекстный менеджер либо `close()` / `aclose()` для освобождения
73
+ соединений. Закрытый клиент не используется повторно: он вызывает `ClientClosedError`;
74
+ создайте новый экземпляр. `is_closed` показывает состояние. Один экземпляр
75
+ асинхронного клиента используйте в одном event loop. Параметр `timeout` принимает
76
+ секунды, `httpx.Timeout` или явно отключённый таймаут `None`.
77
+
78
+ ## Подготовка и отправка
79
+
80
+ ```python
81
+ from zenmoney_api import transactions as tx
82
+
83
+ operation = tx.expense(user=123, account='account-id', instrument=1,
84
+ amount=100000, date='2026-10-13', categories=['category-id'], payee='Shop')
85
+ # Сохраните operation и его UUID до отправки. Повтор не должен создавать новый UUID.
86
+ # Выполнить запись, когда вызывающее приложение готово:
87
+ # result = api.upsert('transaction', [operation], cursor)
88
+ # Затем повторно получите данные и проверьте наличие/содержимое операции.
89
+ ```
90
+
91
+ `transaction(...)` формирует общий объект; `income`, `expense`, `transfer`, `debt`
92
+ упрощают типовые случаи. Примеры: [examples/operations.py](examples/operations.py).
93
+ `update(fetched_object, tag=[...])` копирует полный объект с сохранением полей.
94
+
95
+ `api.diff(cursor, changes={'account': [...], 'tag': [...], 'transaction': [...]},
96
+ deletions=[...])` позволяет передать несколько типов сущностей одним запросом.
97
+ `upsert(kind, entities, cursor)` — удобная обёртка.
98
+ Для новых объектов каталога есть полные формы с актуальными полями:
99
+ `entities.account(user=..., title=..., instrument=...)`, `entities.tag(...)`,
100
+ `entities.merchant(...)`. Полученный объект можно передать в upsert.
101
+ Для loan/deposit задайте параметры срока и процентов; системный debt-счёт
102
+ следует получать из diff, а не создавать повторно. **Передавайте полные сущности,
103
+ не patch**. Низкоуровневый интерфейс проверяет структуру, идентификаторы, timestamps
104
+ и JSON, но не подменяет серверную проверку всех полей. Ограничения/пакетирование
105
+ определяет вызывающее приложение, скрытого лимита в 20 записей в общем SDK нет.
106
+
107
+ `deletion(kind, id, user=..., now=...)` создаёт удаление по ID. Для бюджета с
108
+ составным ключом используйте `clear_budget` и `upsert('budget', ...)`.
109
+ Мягкое удаление операции — `update(operation, deleted=True)`.
110
+
111
+ `api.suggest({'payee': 'Shop'})` или список объектов возвращает подсказки API,
112
+ не изменяя записи.
113
+
114
+ ## OAuth (необязательно)
115
+
116
+ Для diff/suggest достаточно готового Bearer-токена в Client/AsyncClient.
117
+ Регистрация приложения и собственный OAuth не требуются для использования SDK.
118
+ Методы ниже предназначены для владельцев уже выданных реквизитов приложения;
119
+ их живая проверка не является условием приёмки основного API.
120
+
121
+
122
+ ```python
123
+ import secrets
124
+ from zenmoney_api import OAuthClient, callback_code
125
+
126
+ with OAuthClient('registered-client-id', 'client-secret', 'https://your-app/callback') as oauth:
127
+ state = secrets.token_urlsafe(32) # сохраните в сессии до открытия URL
128
+ url = oauth.authorization_url(state=state)
129
+ # code = callback_code(received_callback_url, expected_state=state)
130
+ # token = oauth.exchange_code(code)
131
+ # if token.refresh_token is not None:
132
+ # refreshed = oauth.refresh(token.refresh_token)
133
+ ```
134
+
135
+ Для async есть независимый `AsyncOAuthClient` с теми же аргументами:
136
+ используйте `async with`, а `exchange_code` и `refresh` вызывайте с `await`.
137
+ OAuth-клиенты переиспользуют HTTPX-клиент и переданный транспорт между запросами.
138
+ При работе без контекстного менеджера вызовите `close()` / `await aclose()`.
139
+ Оба способа освобождают открытый HTTPX-клиент и его транспорт; повторное закрытие
140
+ безопасно. После закрытия `is_closed` равен `True`, а обмен кода, обновление токена
141
+ и повторный вход в контекст вызывают `ClientClosedError`. Создайте новый экземпляр
142
+ для следующего сеанса. `AsyncOAuthClient` используйте в рамках одного event loop.
143
+ SDK не поднимает callback-сервер, не регистрирует приложение, не хранит токены
144
+ и не обновляет их автоматически. Если refresh-ответ не содержит нового
145
+ refresh_token, приложение сохраняет прежний. `Token.__repr__` скрывает токены.
146
+ State проверяется до обмена code; привязка callback к сессии — задача приложения.
147
+
148
+ ## Ошибки и подтверждение записи
149
+
150
+ `HTTPError.status_code`, `TransportError`, `ResponseError` не включают тело ответа
151
+ и токен. HTTP 200 не гарантирует принятия всех изменений: более новый `changed`
152
+ на сервере может победить. После записи требуется отдельное чтение и сверка;
153
+ при неопределённом результате нельзя создавать другой UUID и слепо повторять.
154
+ Синхронизация БД, пользовательское подтверждение, журнал отправок и reconciliation
155
+ принадлежат приложению, а не SDK.
156
+
157
+ ## Разработка и Docker
158
+
159
+ ```sh
160
+ sh scripts/test.sh
161
+ sh scripts/build.sh
162
+ ```
163
+
164
+ Тесты используют установленный wheel без дерева исходников, без сети и без
165
+ настоящих токенов. Есть проверка реального HTTP на локальном тестовом сервере.
166
+ Тестовый образ проверяет строгую типизацию mypy, типовые примеры использования
167
+ публичного API и выводит покрытие. Сборка требует доступа к реестру образов и PyPI.
168
+
169
+ ## Переход с 0.4
170
+
171
+ Версия 0.5 намеренно меняет контракт: `zenimport_zenmoney` удалён, десятичные
172
+ числа ответа теперь Decimal, закрытый клиент не открывается повторно. Для записи
173
+ используйте `diff`/`upsert`, для типовых объектов — `transactions`.
174
+ Старые приложения следует адаптировать после приёмки SDK; их ограничения
175
+ не определяют дизайн библиотеки.
176
+ [Архитектура](docs/architecture.md) · [Покрытие API](docs/coverage.md) ·
177
+ [Проверка на настоящем API](docs/live-validation.md) · [История](CHANGELOG.md).
178
+
179
+
180
+ ## Живая приёмка
181
+
182
+ [Результаты проверки на тестовом аккаунте](docs/live-results.md).
183
+ Для собственного отдельного тестового аккаунта скопируйте `.env.example` в `.env`
184
+ и заполните ZENMONEY_TEST_TOKEN. Шаблон содержит также все необязательные переменные
185
+ OAuth. Файл не попадает в Git и образ Docker.
186
+
187
+ `sh scripts/live-check.sh` выполняет только чтение. `--write` явно включает запись
188
+ искусственных данных и требует первоначально пустого отдельного аккаунта;
189
+ `--soft-delete` и `--cleanup` работают только с UUID сохранённого тестового журнала.
190
+ После ошибки автоматического повтора нет. `--resume` разрешает только повторную
191
+ сверку и продолжение подтверждённых шагов, без повторения неизвестной записи.
192
+ Завершённый очищенный журнал не используется для нового запуска.
193
+ Для нового запуска задайте другой путь `--journal /state/new-run.json`.
194
+
195
+ `--write --basic --journal /state/basic-run.json` проверяет согласованный базовый
196
+ набор операций, баланс после каждого шага, долговые движения и отсутствие дубля
197
+ при явной записи того же UUID. `--cleanup --journal /state/basic-run.json`
198
+ убирает тестовые объекты; `--inspect` выводит только названия отличающихся полей.
@@ -0,0 +1,63 @@
1
+ # Задача и порядок работы
2
+
3
+ Развивать самостоятельную универсальную Python-библиотеку всего документированного
4
+ публичного API ZenMoney. Совместимость с прежним backend/Chrome не ограничивает
5
+ дизайн. К рефакторингу основного проекта возвращаемся после приёмки SDK.
6
+
7
+ ## Реализовано
8
+
9
+ - [x] Отдельный проект, локальный Git, wheel и независимые Docker-тесты.
10
+ - [x] Единственный публичный namespace zenmoney_api; старый адаптер удалён.
11
+ - [x] Diff всех сущностей, пользовательская запись/удаление и Suggest.
12
+ - [x] Доходы/расходы, переводы, долги, несколько категорий, валютные поля.
13
+ - [x] Sync/async клиенты, явное владение ресурсами, без скрытых повторов.
14
+ - [x] Независимые sync/async OAuth code/refresh и проверка state.
15
+ - [x] Decimal без потери точности; проверка конечных JSON-чисел.
16
+ - [x] Сохранение дополнительных полей и самостоятельные типы аргументов/результатов.
17
+ - [x] Структурная валидация, ошибки, лимиты ответа и реальный локальный HTTP.
18
+ - [x] Строгая проверка типов установленного пакета и примеров клиента в Docker.
19
+
20
+ ## Проверка после код-ревью — 16 октября 2026
21
+
22
+ - [x] Исправлено преждевременное закрытие OAuth-транспорта между code и refresh.
23
+ - [x] Добавлены close/aclose, контекстные менеджеры и проверка закрытого состояния.
24
+ - [x] Docker: 260 тестов, строгий mypy для пакета и публичных примеров, покрытие 99%.
25
+ - [x] Версия повышена до 0.6.1 для отдельного wheel и обновления vendor backend.
26
+
27
+ Проверки использовали искусственные ответы и локальный HTTP; настоящий API
28
+ и пользовательские данные не затрагивались. Публикация и Git-теги не выполнялись.
29
+
30
+ ## Живая приёмка
31
+
32
+ - OAuth code/refresh — необязательная возможность, проверенная локально.
33
+ Живая проверка без реквизитов приложения недоступна и исключена из обязательной
34
+ приёмки; готового Bearer-токена достаточно для основного API.
35
+ - [x] Базовые виды записи на тестовом аккаунте: счета cash, категории, переводы,
36
+ долги, напоминания, бюджеты; повторное чтение и сверка результатов.
37
+ - [x] Отдельная приёмка 18 базовых операций: полный/частичный возврат покупки,
38
+ разделение расхода, все направления долгов, переводы и VND; балансы,
39
+ долговые движения и повтор того же UUID без дубля.
40
+
41
+ Результаты и оставшиеся границы: live-results.md.
42
+ Подготовленный сценарий: live-validation.md. Проверки diff/suggest требуют только тестового
43
+ аккаунта и готового токена; локальные тесты не заменяют их результат.
44
+
45
+ ## После приёмки
46
+
47
+ Выбрать принятую версию SDK, затем рефакторить backend ZenImport под новый контракт,
48
+ включая Decimal, управление ресурсами, сущности и обработку результатов записи.
49
+ Переделывать SDK под ограничения старого приложения не требуется.
50
+ Отдельное выделение Chrome/банковских адаптеров можно рассмотреть позже.
51
+
52
+ ## Подготовка первого GitHub-релиза — 17 октября 2026
53
+
54
+ Имя дистрибутива приведено к имени созданного репозитория: `zenmoney-python`
55
+ (по уточнению пользователя), версия 0.7.0, импорт `zenmoney_api` неизменен.
56
+ Добавлены MIT из GitHub, метаданные, wheel/sdist/SHA256SUMS, Twine и CI без
57
+ публикации. Команды и порядок первого релиза: [publication.md](publication.md).
58
+ Origin настроен; push, теги и публикация требуют отдельного разрешения.
59
+
60
+ Проверка 0.7.0 в Docker: 260 passed, mypy strict, покрытие 99%; wheel/sdist
61
+ прошли Twine strict и проверку содержимого, лицензии и типов. Финансовый код SDK
62
+ не менялся. Gitleaks 8.30.1 и отдельная проверка известных локальных токенов
63
+ не нашли секретов в прежней Git-истории; токены не выводились.