tungoai 0.2.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 (63) hide show
  1. tungoai-0.2.0/.gitignore +10 -0
  2. tungoai-0.2.0/CHANGELOG.md +39 -0
  3. tungoai-0.2.0/LICENSE +21 -0
  4. tungoai-0.2.0/PKG-INFO +477 -0
  5. tungoai-0.2.0/README.md +437 -0
  6. tungoai-0.2.0/conformance/vectors.json +3696 -0
  7. tungoai-0.2.0/examples/README.md +11 -0
  8. tungoai-0.2.0/examples/async_client.py +26 -0
  9. tungoai-0.2.0/examples/configuration.py +40 -0
  10. tungoai-0.2.0/examples/errors.py +29 -0
  11. tungoai-0.2.0/examples/money.py +20 -0
  12. tungoai-0.2.0/examples/pagination.py +22 -0
  13. tungoai-0.2.0/examples/quick_start.py +13 -0
  14. tungoai-0.2.0/examples/retries.py +26 -0
  15. tungoai-0.2.0/examples/streaming.py +32 -0
  16. tungoai-0.2.0/examples/webhooks.py +72 -0
  17. tungoai-0.2.0/pyproject.toml +126 -0
  18. tungoai-0.2.0/src/tungoai/__init__.py +132 -0
  19. tungoai-0.2.0/src/tungoai/_base_client.py +1351 -0
  20. tungoai-0.2.0/src/tungoai/_client.py +707 -0
  21. tungoai-0.2.0/src/tungoai/_exceptions.py +274 -0
  22. tungoai-0.2.0/src/tungoai/_models.py +284 -0
  23. tungoai-0.2.0/src/tungoai/_money.py +109 -0
  24. tungoai-0.2.0/src/tungoai/_pagination.py +181 -0
  25. tungoai-0.2.0/src/tungoai/_response.py +103 -0
  26. tungoai-0.2.0/src/tungoai/_sse.py +126 -0
  27. tungoai-0.2.0/src/tungoai/_streaming.py +456 -0
  28. tungoai-0.2.0/src/tungoai/_types.py +53 -0
  29. tungoai-0.2.0/src/tungoai/_version.py +4 -0
  30. tungoai-0.2.0/src/tungoai/_webhooks.py +186 -0
  31. tungoai-0.2.0/src/tungoai/py.typed +0 -0
  32. tungoai-0.2.0/src/tungoai/resources/__init__.py +45 -0
  33. tungoai-0.2.0/src/tungoai/resources/api_keys.py +253 -0
  34. tungoai-0.2.0/src/tungoai/resources/automations.py +1805 -0
  35. tungoai-0.2.0/src/tungoai/resources/batches.py +701 -0
  36. tungoai-0.2.0/src/tungoai/resources/chat.py +962 -0
  37. tungoai-0.2.0/src/tungoai/resources/companies.py +144 -0
  38. tungoai-0.2.0/src/tungoai/resources/esf.py +239 -0
  39. tungoai-0.2.0/src/tungoai/resources/feed.py +217 -0
  40. tungoai-0.2.0/src/tungoai/resources/forms.py +425 -0
  41. tungoai-0.2.0/src/tungoai/resources/onec.py +107 -0
  42. tungoai-0.2.0/src/tungoai/resources/platform.py +609 -0
  43. tungoai-0.2.0/src/tungoai/resources/proposals.py +535 -0
  44. tungoai-0.2.0/src/tungoai/resources/webhooks.py +1040 -0
  45. tungoai-0.2.0/src/tungoai/resources/workspaces.py +100 -0
  46. tungoai-0.2.0/src/tungoai/types/__init__.py +991 -0
  47. tungoai-0.2.0/src/tungoai/types/_models.py +7713 -0
  48. tungoai-0.2.0/src/tungoai/types/params.py +474 -0
  49. tungoai-0.2.0/tests/__init__.py +0 -0
  50. tungoai-0.2.0/tests/conftest.py +48 -0
  51. tungoai-0.2.0/tests/fixtures.json +4965 -0
  52. tungoai-0.2.0/tests/mock_server.py +235 -0
  53. tungoai-0.2.0/tests/support.py +146 -0
  54. tungoai-0.2.0/tests/test_async_only.py +75 -0
  55. tungoai-0.2.0/tests/test_client_async.py +1184 -0
  56. tungoai-0.2.0/tests/test_client_sync.py +1184 -0
  57. tungoai-0.2.0/tests/test_conformance.py +306 -0
  58. tungoai-0.2.0/tests/test_examples.py +116 -0
  59. tungoai-0.2.0/tests/test_models.py +207 -0
  60. tungoai-0.2.0/tests/test_operations.py +3256 -0
  61. tungoai-0.2.0/tests/test_package.py +110 -0
  62. tungoai-0.2.0/tests/test_streaming_async.py +312 -0
  63. tungoai-0.2.0/tests/test_streaming_sync.py +312 -0
@@ -0,0 +1,10 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ .venv-*/
5
+ dist/
6
+ build/
7
+ *.egg-info/
8
+ .pytest_cache/
9
+ .mypy_cache/
10
+ .ruff_cache/
@@ -0,0 +1,39 @@
1
+ # Changelog
2
+
3
+ All notable changes to the Tungo SDK for Python are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ <!-- Generated by tungo-sdkgen: edit changelogReleases in the generator, not this file. -->
9
+
10
+ ## [Unreleased]
11
+
12
+ ## [0.2.0] - 2026-11-12
13
+
14
+ First production-ready release. It contains breaking changes compared with 0.1.0; 0.1.0 should not be used.
15
+
16
+ ### Changed
17
+
18
+ - The distribution is renamed from `tungo-sdk` to `tungoai`, and the import package from `tungo` to `tungoai`.
19
+ - Requests and responses use typed models generated from the API contract.
20
+ - Errors are classified by the `error.type` of the API error envelope, with the HTTP status as a fallback; the `hint` field, which the API never sent, is removed.
21
+ - Retries happen only for requests that are safe to repeat (idempotent methods, or requests carrying an `Idempotency-Key`), with jittered exponential backoff that honours `retry-after-ms` and `Retry-After`.
22
+ - Timeouts cover the whole request, are configurable per client and per request, and combine with caller-supplied cancellation.
23
+ - Server-sent events are delivered incrementally; closing or abandoning a stream closes the connection.
24
+ - List methods return typed pages and offer an auto-paging iterator.
25
+ - Webhook verification enforces the timestamp tolerance against the system clock and reports every failure as a typed error.
26
+ - Money helpers parse decimal strings exactly; tiyn amounts are 64-bit integers.
27
+ - The default base URL is `https://api.tungo.kz`; it can be set with `TUNGO_BASE_URL`.
28
+ - Requires Python 3.10 or newer.
29
+ - New clients `Tungo` (synchronous) and `AsyncTungo` (asynchronous), built on httpx, with the same resources, pagination and streaming.
30
+ - Responses are pydantic models; request parameters are named keyword arguments checked by mypy and pyright.
31
+ - `with_raw_response` gives the status, headers, request id and rate-limit information of a response.
32
+
33
+ ## [0.1.0] - 2026-11-09
34
+
35
+ Initial preview. Not suitable for production use.
36
+
37
+ [Unreleased]: https://github.com/TungoAI/tungo-sdk-python/compare/v0.2.0...HEAD
38
+ [0.2.0]: https://github.com/TungoAI/tungo-sdk-python/compare/v0.1.0...v0.2.0
39
+ [0.1.0]: https://github.com/TungoAI/tungo-sdk-python/releases/tag/v0.1.0
tungoai-0.2.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tungo AI (https://tungo.kz)
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.
tungoai-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,477 @@
1
+ Metadata-Version: 2.5
2
+ Name: tungoai
3
+ Version: 0.2.0
4
+ Summary: Official Python library for the Tungo Platform API
5
+ Project-URL: Homepage, https://github.com/TungoAI/tungo-sdk-python
6
+ Project-URL: Repository, https://github.com/TungoAI/tungo-sdk-python
7
+ Project-URL: Issues, https://github.com/TungoAI/tungo-sdk-python/issues
8
+ Project-URL: Changelog, https://github.com/TungoAI/tungo-sdk-python/blob/main/CHANGELOG.md
9
+ Author: Tungo
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: accounting,api,kazakhstan,sdk,tungo
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.10
26
+ Requires-Dist: anyio<5,>=4.4
27
+ Requires-Dist: httpx<1,>=0.27
28
+ Requires-Dist: pydantic<3,>=2.9
29
+ Requires-Dist: typing-extensions<5,>=4.10
30
+ Provides-Extra: dev
31
+ Requires-Dist: build>=1.2; extra == 'dev'
32
+ Requires-Dist: mypy>=1.13; extra == 'dev'
33
+ Requires-Dist: pyright>=1.1.390; extra == 'dev'
34
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
35
+ Requires-Dist: pytest>=8.3; extra == 'dev'
36
+ Requires-Dist: ruff>=0.8; extra == 'dev'
37
+ Requires-Dist: trio>=0.27; extra == 'dev'
38
+ Requires-Dist: twine>=6.1; extra == 'dev'
39
+ Description-Content-Type: text/markdown
40
+
41
+ <!-- Generated by tungo-sdkgen. Do not edit: the file is rewritten on every generation. -->
42
+
43
+ <div align="center">
44
+
45
+ <picture>
46
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/TungoAI/.github/main/assets/logo02.svg" />
47
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/TungoAI/.github/main/assets/logo03.svg" />
48
+ <img alt="Tungo AI" src="https://raw.githubusercontent.com/TungoAI/.github/main/assets/logo03.svg" width="240" />
49
+ </picture>
50
+
51
+ # Tungo SDK for Python
52
+
53
+ **The official Python library for the Tungo Platform API**
54
+
55
+ [![PyPI](https://img.shields.io/pypi/v/tungoai)](https://pypi.org/project/tungoai/)
56
+ [![CI](https://github.com/TungoAI/tungo-sdk-python/actions/workflows/ci.yml/badge.svg)](https://github.com/TungoAI/tungo-sdk-python/actions/workflows/ci.yml)
57
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/TungoAI/tungo-sdk-python/blob/main/LICENSE)
58
+
59
+ **English** · [Русский](https://github.com/TungoAI/tungo-sdk-python/blob/main/README.ru.md) · [Қазақша](https://github.com/TungoAI/tungo-sdk-python/blob/main/README.kk.md)
60
+
61
+ [tungo.kz](https://tungo.kz) · [Quick start](#quick-start) · [API reference](#api-reference) · [Changelog](https://github.com/TungoAI/tungo-sdk-python/blob/main/CHANGELOG.md) · [Issues](https://github.com/TungoAI/tungo-sdk-python/issues)
62
+
63
+ </div>
64
+
65
+ ---
66
+
67
+ The official Python library for the Tungo Platform API (`https://api.tungo.kz`): the AI accountant, 1C integration, bank statements, e-invoices, tax forms and automations of [Tungo AI](https://tungo.kz).
68
+
69
+ It is generated from the API's OpenAPI contract, so its methods and models always match the contract it was built from.
70
+
71
+ ## Requirements
72
+
73
+ Python 3.10 or newer. The library depends on [httpx](https://www.python-httpx.org/) and [pydantic](https://docs.pydantic.dev/) 2 (plus `anyio` and `typing-extensions`, which those two already bring in). It ships type information (`py.typed`) and is checked with `mypy --strict` and pyright in strict mode.
74
+
75
+ ## Installation
76
+
77
+ ```sh
78
+ pip install tungoai
79
+ ```
80
+
81
+ The import package has the same name: `import tungoai`.
82
+
83
+ ## Quick start
84
+
85
+ ```python
86
+ from tungoai import Tungo
87
+
88
+ with Tungo() as client: # reads TUNGO_API_KEY from the environment
89
+ companies = client.companies.list()
90
+ for company in companies.data:
91
+ print(company.id, company.name)
92
+
93
+ turn = client.ask(companies.data[0].id, "How much VAT is due this quarter?")
94
+ print(turn.answer)
95
+ ```
96
+
97
+ ## Authentication and configuration
98
+
99
+ Create one client and share it: it is immutable and safe to use from several threads or tasks. Every option can be passed explicitly; three of them fall back to environment variables.
100
+
101
+ | Option | Environment variable | Default |
102
+ | --- | --- | --- |
103
+ | API key | `TUNGO_API_KEY` | none |
104
+ | Base URL | `TUNGO_BASE_URL` | `https://api.tungo.kz` |
105
+ | Webhook secret | `TUNGO_WEBHOOK_SECRET` | none |
106
+ | Timeout | | 60 s (600 s for non-streaming chat turns) |
107
+ | Maximum retries | | 2 |
108
+ | Default headers, HTTP client | | none, built-in |
109
+
110
+ The API key is sent only to operations that require it; public endpoints and incoming hooks never receive it, and it never appears in error messages or debug output. Calling an authenticated operation without a key fails locally, before any request is made.
111
+
112
+ Every method also accepts per-request options: timeout, maximum retries, extra headers, an idempotency key and cancellation.
113
+
114
+ ```python
115
+ import os
116
+
117
+ import httpx
118
+
119
+ from tungoai import Tungo
120
+
121
+ http_client = httpx.Client(limits=httpx.Limits(max_connections=10)) # proxies, mTLS, custom transports
122
+ client = Tungo(
123
+ api_key=os.environ["TUNGO_API_KEY"], # default: the TUNGO_API_KEY environment variable
124
+ base_url=os.environ.get("TUNGO_BASE_URL"), # default: TUNGO_BASE_URL, then https://api.tungo.kz
125
+ webhook_secret=os.environ.get("TUNGO_WEBHOOK_SECRET"), # default: TUNGO_WEBHOOK_SECRET
126
+ timeout=30.0, # seconds for a whole ordinary request; default 60
127
+ long_running_timeout=900.0, # seconds for a non-streaming chat turn; default 600
128
+ max_retries=3, # default 2
129
+ default_headers={"X-App-Name": "billing"},
130
+ http_client=http_client,
131
+ )
132
+ company_id = client.companies.list().data[0].id
133
+
134
+ # Options of one request.
135
+ company = client.companies.get(
136
+ company_id,
137
+ request_options={"timeout": 5.0, "max_retries": 0, "headers": {"X-Trace-Id": "trace-1"}},
138
+ )
139
+
140
+ # A copy of the client with other settings; it shares the connection pool.
141
+ patient = client.with_options(timeout=120.0)
142
+ print(patient.companies.get(company_id).name)
143
+
144
+ # Status, headers, request id and rate limit of a response.
145
+ response = client.companies.with_raw_response.get(company_id)
146
+ print(response.status_code, response.request_id, response.rate_limit.remaining)
147
+ print(response.parse().name)
148
+
149
+ client.close()
150
+ http_client.close() # the SDK never closes a client it did not create
151
+ ```
152
+
153
+ `Tungo` is the synchronous client and `AsyncTungo` the asynchronous one. Both accept the same options (`http_client` is an `httpx.Client` or an `httpx.AsyncClient`), are immutable after construction and release their connection pool with `close()` / `aclose()` or a `with` block. In asynchronous code a call is cancelled the usual way, by cancelling its task; the timeout stays in force.
154
+
155
+ `timeout` covers ordinary calls and `long_running_timeout` the non-streaming chat turns (`chat.create`, `chat.answer`, `ask`): setting `timeout=30` does not cut a chat turn to 30 s. A `timeout` in `request_options` wins over both. A malformed API key (a line break, a space, a control or non-ASCII character inside it) is refused when the client is created, with a `ConfigError` that does not show the key.
156
+
157
+ ## Errors
158
+
159
+ Every failure is a subtype of the SDK's base error. An HTTP error response becomes an API error chosen by the `error.type` field of the response body, falling back to the HTTP status when the body is not the API's error envelope:
160
+
161
+ | `error.type` | Status |
162
+ | --- | --- |
163
+ | `invalid_request_error` | 400 |
164
+ | `authentication_error` | 401 |
165
+ | `payment_required_error` | 402 |
166
+ | `permission_error` | 403 |
167
+ | `not_found_error` | 404 |
168
+ | `conflict_error` | 409 |
169
+ | `gone_error` | 410 |
170
+ | `limit_error` | 429 |
171
+ | `api_error` | 500 |
172
+ | `unavailable_error` | 503 |
173
+
174
+ An API error carries the status, type, code, message, request id, field errors, the parsed retry-after delay, the response headers and the raw body. Problems that happen without a response have their own types: connection error, timeout, decode error (a successful response whose body is not valid), and configuration or validation error (raised before any request).
175
+
176
+ ```python
177
+ import tungoai
178
+ from tungoai import Tungo
179
+
180
+ client = Tungo()
181
+ try:
182
+ client.companies.get("co_missing")
183
+ except tungoai.NotFoundError as error:
184
+ print(error.status, error.type, error.code, error.message, error.request_id)
185
+ except tungoai.InvalidRequestError as error:
186
+ for item in error.fields:
187
+ print(item.path, item.code, item.message)
188
+ except tungoai.LimitError as error:
189
+ print("retry after", error.retry_after, "s; the limit resets at", error.extra.get("resets_at"))
190
+ except tungoai.APIError as error: # any other error response
191
+ print(error.status, error.headers.get("x-request-id"), error.body)
192
+ except tungoai.APITimeoutError:
193
+ print("the request timed out")
194
+ except tungoai.APIConnectionError as error:
195
+ print("network problem:", error.__cause__)
196
+ except tungoai.DecodeError as error:
197
+ print("unexpected response body:", error.body)
198
+ except tungoai.ConfigError as error: # raised before any request, e.g. no API key
199
+ print(error)
200
+ finally:
201
+ client.close()
202
+ ```
203
+
204
+ Every error derives from `tungoai.TungoError`. `APITimeoutError` and `WaitTimeoutError` also derive from the built-in `TimeoutError`; `RequestValidationError` and `MoneyError` from `ValueError`. The original exception, when there is one, is available as `__cause__`.
205
+
206
+ An error that a chat turn reports inside a successful response (an `error` event of a stream, or `errors` of `ask`) is raised by the helpers that collect a turn as `ChatStreamError`. It is not an `APIError` and has no HTTP status; its `type` is the same `error.type` (`limit_error`, `api_error`, ...), next to `code`, `message`, `request_id`, `extra`, and `result` or `turn` with what was received. The TypeScript SDK throws the `APIError` subclass of that `error.type` (for example `LimitError`) with an undefined `status` for the same situation.
207
+
208
+ ## Retries and timeouts
209
+
210
+ A request is retried up to 2 times (3 attempts) on connection errors, timeouts and the statuses 408, 429, 500, 502, 503 and 504, but only when repeating it is safe: `GET`, `HEAD`, `PUT` and `DELETE` requests, and `POST` or `PATCH` requests that carry an `Idempotency-Key`. For the operations whose contract declares that header the SDK generates a random key and reuses it for every attempt; you can pass your own.
211
+
212
+ The delay between attempts is `min(0.5 s × 2^attempt, 8 s)` with random jitter. A positive `retry-after-ms` or `Retry-After` response header overrides it; if the server asks for more than 60 s the SDK does not wait and raises the error with the delay filled in.
213
+
214
+ The timeout covers the whole request including reading the body. Streaming calls have no total timeout, only a connect timeout and an idle timeout of 120 s. Cancellation supplied by the caller is combined with the timeout.
215
+
216
+ ```python
217
+ from tungoai import Tungo
218
+
219
+ client = Tungo(max_retries=5, timeout=20.0) # for every request of this client
220
+ company_id = client.companies.list().data[0].id
221
+ proposal_id = client.proposals.list(company_id).data[0].id
222
+
223
+ # For one request. approve declares Idempotency-Key, so the SDK would generate a key anyway;
224
+ # pass your own to make a repeated call from your side safe too.
225
+ approved = client.proposals.approve(
226
+ company_id,
227
+ proposal_id,
228
+ note="Checked against the contract",
229
+ request_options={"idempotency_key": f"approve-{proposal_id}", "max_retries": 1, "timeout": 10.0},
230
+ )
231
+ print(approved.to_dict())
232
+
233
+ # A POST that does not declare the header is never retried unless you give it a key.
234
+ client.proposals.reject(
235
+ company_id, proposal_id, reason="Duplicate", request_options={"idempotency_key": f"reject-{proposal_id}"}
236
+ )
237
+
238
+ client.close()
239
+ ```
240
+
241
+ A request is retried only when repeating it is safe: GET, HEAD, PUT and DELETE, or any request that carries an `Idempotency-Key` (generated for the operations that declare it, passed as `idempotency_key`, or put into the headers by you). A `Retry-After` of zero, or a date that has already passed, does not trigger an immediate retry: the computed backoff applies.
242
+
243
+ The timeout is one deadline for the whole request. The asynchronous client enforces it exactly. The synchronous client gives each phase (connecting, sending, waiting for the headers, reading the body) only the time that is left and checks the deadline after every chunk of the body; a server that goes silent in the middle of a body is cut off when the time that was left at the start of the body runs out.
244
+
245
+ ## Pagination
246
+
247
+ List operations use cursor pagination (`{ data, next_cursor }`). The plain method returns one typed page that knows whether there is a next page and can fetch it. The auto-paging variant is a lazy iterator: it requests the next page only when needed, carries your filters over, and stops fetching as soon as you stop iterating.
248
+
249
+ ```python
250
+ from tungoai import Tungo
251
+
252
+ client = Tungo()
253
+ company_id = client.companies.list().data[0].id
254
+
255
+ # One page: the items, the cursor and the whole typed response.
256
+ page = client.proposals.list(company_id, status="awaiting_approval", limit=50)
257
+ print(len(page.data), page.next_cursor, page.body.total)
258
+ if page.has_next_page():
259
+ page = page.get_next_page() # same filters, next cursor
260
+
261
+ # Every item: iterating a page requests the following pages only when they are needed.
262
+ for proposal in client.proposals.list(company_id, status="awaiting_approval", limit=50):
263
+ print(proposal.id, proposal.title, proposal.total.formatted)
264
+ if proposal.blocker_count > 0:
265
+ break # no further page is requested
266
+
267
+ client.close()
268
+ ```
269
+
270
+ A cursor that the server has already returned in the same walk (the previous one or any earlier one) raises `PaginationError` instead of looping forever.
271
+
272
+ ## Streaming
273
+
274
+ Streaming operations use server-sent events and deliver typed events as they arrive. Leaving the loop early or closing the stream closes the connection. A stream request is retried only before its first byte; an error response is raised as the same typed API error as for ordinary calls. A chat stream that ends before its final event is reported as an error, never as a finished answer.
275
+
276
+ ```python
277
+ from tungoai import ChatStreamError, DecodeError, Tungo
278
+ from tungoai.types import ChatEventDone, ChatEventText, ChatEventTool
279
+
280
+ client = Tungo()
281
+ company_id = client.companies.list().data[0].id
282
+
283
+ # Events as they arrive. Leaving the "with" block closes the connection.
284
+ with client.chat.create_stream(company_id, message="How much VAT is due this quarter?") as stream:
285
+ print("dialog", stream.dialog_id, "turn", stream.turn_no)
286
+ for event in stream:
287
+ if isinstance(event, ChatEventText):
288
+ print(event.delta, end="", flush=True)
289
+ elif isinstance(event, ChatEventTool):
290
+ print(f"\n[{event.name}: {event.status}]")
291
+ elif isinstance(event, ChatEventDone):
292
+ print("\nstop reason:", event.stop_reason)
293
+
294
+ # Or let the SDK collect the turn. An "error" event of the turn is raised as ChatStreamError,
295
+ # and a stream that was cut before its "done" event as DecodeError: never a half answer.
296
+ try:
297
+ result = client.chat.create_stream(company_id, message="And for the year?").get_final_result()
298
+ print(result.text, result.stop_reason, result.cost_tiyn)
299
+ except ChatStreamError as error:
300
+ print(error.type, error.code, error.message, error.request_id)
301
+ except DecodeError as error:
302
+ print("the stream was cut:", error.result.text if error.result else "")
303
+
304
+ client.close()
305
+ ```
306
+
307
+ Every streaming operation of the API is available: `chat.create_stream`, `chat.answer_stream`, `chat.list_events`, `feed.list_events` and `batches.list_events`. Events are typed models; an event type this version does not know arrives as `UnknownVariant`, and heart-beats are skipped. `stream.iter_sse()` gives the raw server-sent events. `request_options={"stream_idle_timeout": 300}` changes the idle timeout of one call.
308
+
309
+ Leaving a `for` loop over a stream early (`break`, `return`, an exception) closes the connection: at once on CPython with the synchronous client, at the next step of the event loop with the asynchronous one. A `with` block, or `close()` / `aclose()`, makes the moment explicit on every interpreter. If you pull events with `next(stream)` instead of a loop, close the stream yourself.
310
+
311
+ `get_final_result()` never returns a half answer: a stream that ends without its `done` event raises `DecodeError` (`error.result` holds what arrived). A successful answer to a streaming call that is not `text/event-stream` raises `DecodeError` too, instead of looking like an empty stream. Plain iteration simply ends when the stream ends.
312
+
313
+ ## Webhooks
314
+
315
+ Outgoing webhooks are signed according to the Standard Webhooks specification (Svix). The verify helper takes the raw request body and the headers (`webhook-*` or `svix-*`), checks the HMAC-SHA256 signature in constant time and the timestamp within 300 s of the system clock, and returns the typed event. Any problem is reported as a webhook verification error. If no secret is passed, the client's webhook secret is used.
316
+
317
+ The send-hook helper posts an event to an incoming hook (`/hooks/{token}`) and, when given a secret, signs the exact bytes it sends in the `X-Signature: sha256=<hex>` header. The API key is never sent to hooks.
318
+
319
+ ```python
320
+ from collections.abc import Mapping
321
+
322
+ import tungoai
323
+ from tungoai import Tungo, WebhookVerificationError
324
+ from tungoai.types import WebhookEventProposalWritten
325
+
326
+ client = Tungo() # the webhook secret comes from TUNGO_WEBHOOK_SECRET, or pass webhook_secret=...
327
+
328
+
329
+ def handle_webhook(raw_body: bytes, headers: Mapping[str, str]) -> int:
330
+ """Call it from your HTTP handler with the raw request body and the request headers.
331
+
332
+ Returns the HTTP status to answer with.
333
+ """
334
+ try:
335
+ event = client.verify_webhook(raw_body, headers)
336
+ except WebhookVerificationError as error:
337
+ print("rejected:", error.reason)
338
+ return 400
339
+ if isinstance(event, WebhookEventProposalWritten):
340
+ print("written to 1C:", event.data.proposal_id)
341
+ return 204
342
+
343
+
344
+ # Send an event to an incoming hook. With a secret the exact bytes sent are signed (X-Signature).
345
+ accepted = client.send_hook(
346
+ "th_example",
347
+ {"id": "order_1532", "amount_tiyn": 4_500_000},
348
+ secret="hksec_example",
349
+ event_id="order_1532",
350
+ )
351
+ print(accepted.to_dict())
352
+
353
+ # Check a hook signature yourself (for example in a test double of the hook endpoint).
354
+ body = b'{"id":"order_1532"}'
355
+ assert tungoai.verify_hook_signature(body, tungoai.hook_signature_header(body, "hksec_example"), "hksec_example")
356
+ ```
357
+
358
+ `tungoai.verify_webhook(body, headers, secret)` is the same check without a client. The reason of a failure is in `WebhookVerificationError.reason`. `client.automations.post_hook` and `client.send_hook` are the same call.
359
+
360
+ ## Money
361
+
362
+ The API expresses amounts in tiyn (1/100 of a tenge) as 64-bit integers. The money helper converts decimal strings and exact decimal values to tiyn without floating-point error, rounding half away from zero (`"1.005"` → `101`), and formats tiyn for display (`4500000` → `45 000,00 ₸`). Invalid input raises a typed error.
363
+
364
+ ```python
365
+ from decimal import Decimal
366
+
367
+ from tungoai import MoneyError, TungoMoney, is_valid_bin_iin
368
+
369
+ print(TungoMoney.to_tiyn("45000.50")) # 4500050
370
+ print(TungoMoney.to_tiyn(Decimal("1.005"))) # 101: half away from zero, no float error
371
+ print(TungoMoney.to_tiyn(1.005)) # 101: a float goes through its shortest decimal form
372
+ print(TungoMoney.format_tiyn(4_500_000)) # 45 000,00 ₸
373
+ print(TungoMoney.format_tiyn(-50, "USD")) # -0,50 $
374
+
375
+ try:
376
+ TungoMoney.to_tiyn("12,5")
377
+ except MoneyError as error:
378
+ print(error)
379
+
380
+ print(is_valid_bin_iin("990140000017")) # True
381
+ ```
382
+
383
+ `TungoMoney.to_tiyn` accepts `str`, `decimal.Decimal`, `int` (whole tenge) and `float`. Tiyn are 64-bit integers in the API: `format_tiyn` raises `MoneyError` for a value outside that range, like every Tungo SDK.
384
+
385
+ ## Async usage
386
+
387
+ ```python
388
+ import asyncio
389
+
390
+ from tungoai import AsyncTungo
391
+ from tungoai.types import ChatEventText
392
+
393
+
394
+ async def main() -> None:
395
+ async with AsyncTungo() as client:
396
+ companies = await client.companies.list()
397
+ company_id = companies.data[0].id
398
+
399
+ proposals = await client.proposals.list(company_id, limit=50)
400
+ async for proposal in proposals: # requests the following pages as needed
401
+ print(proposal.id, proposal.title)
402
+
403
+ async with await client.chat.create_stream(company_id, message="What is new today?") as stream:
404
+ async for event in stream:
405
+ if isinstance(event, ChatEventText):
406
+ print(event.delta, end="")
407
+
408
+
409
+ asyncio.run(main())
410
+ ```
411
+
412
+ ## Types
413
+
414
+ Responses are [pydantic](https://docs.pydantic.dev/) models from `tungoai.types`: read fields as attributes, `model.to_dict()` returns the JSON object as the API sent it. Models are forward compatible: unknown fields are kept in `model.model_extra`, enumerations accept values added later, and a tagged union (for example `ChatEvent` or `WebhookEvent`) yields `UnknownVariant` for a tag this version does not know, so narrow with `isinstance`. A field that is absent from the response and one that is `null` are both `None`; `model.model_fields_set` tells them apart.
415
+
416
+ Responses are decoded leniently, requests strictly. If a response does not match the contract (a field that must not be `null` is `null`, a field changed its type), the SDK does not fail the call: the model is built without validation, every part that does match is still a typed model, the part that does not is left as the API sent it, and `to_dict()` returns the original JSON. So a value can differ from its annotation after such a drift; the `tungoai` logger reports it at DEBUG level. The same holds for the events of a stream: one odd event does not end the turn. A body that is not JSON, or JSON of the wrong kind altogether (an array where an object is expected), still raises `DecodeError`.
417
+
418
+ Request parameters are keyword arguments. Leave a parameter out to not send it; pass `None` to send JSON `null` where the API allows it. Nested objects are plain dictionaries typed by `tungoai.types.params`.
419
+
420
+ To call an endpoint this version does not cover, build a `tungoai.RequestSpec` and pass it to `client.request`.
421
+
422
+ ## Helpers
423
+
424
+ `client.ask` sends one chat message and waits for the whole turn. `client.upload_bank_statement(company_id, file_name, content)` takes the bytes of the file, or a string: the base64 of the file by default, or the statement text itself with `text=True` (the same rule as in the TypeScript SDK). `client.wait_for_run`, `client.run_scenario_and_wait` and `client.wait_for_batch` poll with a pause that grows from 1 s to 5 s until the run or job is final; they raise `WaitTimeoutError` after `timeout` seconds (10 minutes by default).
425
+
426
+ ## Logging and secrets
427
+
428
+ The SDK logs to the `tungoai` logger at DEBUG level: the method, the path, the status and the request id, never header values or the API key, and the path of an incoming hook is logged as `/hooks/***`.
429
+
430
+ httpx, the HTTP library underneath, writes its own line for every request to the `httpx` logger at INFO level, with the full URL. The URL of an incoming hook contains its token, which is a secret. For the requests the SDK sends, it replaces that URL with `/hooks/***` in httpx's line (a filter on the `httpx` logger that touches nothing else). Anything else that records URLs on its own (a custom transport, a proxy, an APM agent) is outside the SDK's reach: treat hook URLs as secrets there.
431
+
432
+ ## API reference
433
+
434
+ Generated from the Tungo Platform API contract: **13 resources, 82 methods, 22 webhook event types**.
435
+
436
+ | Resource | Description | Methods |
437
+ | --- | --- | --- |
438
+ | `client.workspaces` | Organisations (workspaces) the API key can access | `list` |
439
+ | `client.companies` | Companies of an organisation and their profiles | `list` · `get` |
440
+ | `client.platform` | Platform balance, ledger, usage reports, limits, audit log and model rates | `list_usage_rates` · `get_account` · `get_audit` · `get_ledger` · `list_limits` · `list_requests` · `get_usage` |
441
+ | `client.api_keys` | The current API key and its usage statistics | `get_current` · `get_current_usage` · `get_usage` |
442
+ | `client.webhooks` | Outgoing webhooks of an organisation (Svix): endpoints, event catalogue, deliveries and signature check | `list_endpoints` · `create_endpoint` · `get_endpoint` · `update_endpoint` · `delete_endpoint` · `list_endpoint_attempts` · `rotate_endpoint_secret` · `test_endpoint` · `list_event_types` · `list_messages` · `list_message_attempts` · `resend_message` · `get_usage` |
443
+ | `client.chat` | Conversations with the AI accountant: questions, streaming chat (SSE), token counting and history | `list` · `create` · `create_stream` · `count_tokens` · `get` · `update` · `delete` · `answer` · `answer_stream` · `list_events` · `stop` |
444
+ | `client.proposals` | Documents proposed by the AI accountant: review, approval for posting to 1C, and rejection | `list` · `approve_batch` · `get` · `approve` · `reject` |
445
+ | `client.feed` | The company's feed of events and proposals (pages and a live SSE stream) | `get` · `list_events` |
446
+ | `client.batches` | Processing batches for bank statements, incoming e-invoices and DirectBank | `list` · `create_bank_statement` · `create_esf_purchases` · `get` · `list_events` · `stop` · `get_summary` · `list_statements` · `poll_directbank` |
447
+ | `client.esf` | Incoming and outgoing electronic invoices (the ESF system) | `get_incoming` · `get_outgoing` |
448
+ | `client.forms` | Tax forms (FNO 100.00 and others): building, ISNA checks and data gaps | `list` · `build` · `list_data_gaps` · `get_isna` · `list_notes` |
449
+ | `client.onec` | Status of the company's connection to its 1C database | `get_status` |
450
+ | `client.automations` | Company automations: incoming hooks, scenarios, step chains, runs and the AI autonomy level | `post_hook` · `list_hooks` · `create_hook` · `get_hook` · `update_hook` · `delete_hook` · `rotate_hook` · `test_hook` · `list_runs` · `get_run` · `cancel_run` · `retry_run` · `list_scenarios` · `create_scenario` · `get_scenario` · `put_scenario` · `update_scenario` · `delete_scenario` · `run_scenario` · `get_state` · `put_state` · `get_autonomy` · `put_autonomy` |
451
+
452
+ ## Other SDKs
453
+
454
+ The same API is available in ten languages. All SDKs are generated from one contract and behave the same way.
455
+
456
+ | Language | Repository | Package |
457
+ | --- | --- | --- |
458
+ | TypeScript | [tungo-sdk-typescript](https://github.com/TungoAI/tungo-sdk-typescript) | `@tungoai/sdk` |
459
+ | **Python** | [tungo-sdk-python](https://github.com/TungoAI/tungo-sdk-python) | `tungoai` |
460
+ | Go | [tungo-sdk-go](https://github.com/TungoAI/tungo-sdk-go) | `github.com/TungoAI/tungo-sdk-go` |
461
+ | Rust | [tungo-sdk-rust](https://github.com/TungoAI/tungo-sdk-rust) | `tungo-sdk` |
462
+ | PHP | [tungo-sdk-php](https://github.com/TungoAI/tungo-sdk-php) | `tungo/sdk` |
463
+ | Java | [tungo-sdk-java](https://github.com/TungoAI/tungo-sdk-java) | `kz.tungo:tungo-sdk` |
464
+ | Kotlin | [tungo-sdk-kotlin](https://github.com/TungoAI/tungo-sdk-kotlin) | `kz.tungo:tungo-sdk-kotlin` |
465
+ | Swift | [tungo-sdk-swift](https://github.com/TungoAI/tungo-sdk-swift) | `TungoSDK` |
466
+ | Ruby | [tungo-sdk-ruby](https://github.com/TungoAI/tungo-sdk-ruby) | `tungo-sdk` |
467
+ | .NET | [tungo-sdk-dotnet](https://github.com/TungoAI/tungo-sdk-dotnet) | `Tungo.Sdk` |
468
+
469
+ ## Versioning and support
470
+
471
+ This library follows [Semantic Versioning](https://semver.org/). While the major version is 0, a minor release may contain breaking changes; they are always listed in the [changelog](https://github.com/TungoAI/tungo-sdk-python/blob/main/CHANGELOG.md). This README describes version 0.2.0.
472
+
473
+ The code is generated: please report problems and ideas in the [issue tracker](https://github.com/TungoAI/tungo-sdk-python/issues) rather than opening pull requests against generated files. To report a vulnerability, follow the [security policy](https://github.com/TungoAI/tungo-sdk-python/blob/main/SECURITY.md).
474
+
475
+ ## License
476
+
477
+ [MIT](https://github.com/TungoAI/tungo-sdk-python/blob/main/LICENSE)