pytonapi 2.0.1__tar.gz → 2.1.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.
- {pytonapi-2.0.1/pytonapi.egg-info → pytonapi-2.1.0}/PKG-INFO +18 -8
- {pytonapi-2.0.1 → pytonapi-2.1.0}/README.md +16 -6
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pyproject.toml +10 -4
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/__meta__.py +1 -1
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/client.py +37 -16
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/exceptions.py +42 -0
- pytonapi-2.1.0/pytonapi/py.typed +0 -0
- pytonapi-2.1.0/pytonapi/rest/client.py +156 -0
- pytonapi-2.1.0/pytonapi/rest/mixin.py +54 -0
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/models/__init__.py +7 -8
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/models/accounts.py +147 -143
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/models/blockchain.py +6 -12
- pytonapi-2.1.0/pytonapi/rest/models/emulation.py +43 -0
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/models/gasless.py +1 -1
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/models/multisig.py +1 -16
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/models/wallet.py +0 -13
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/resources/accounts.py +14 -14
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/resources/blockchain.py +6 -2
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/resources/events.py +6 -3
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/resources/jettons.py +1 -2
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/resources/rates.py +1 -2
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/resources/staking.py +2 -2
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/resources/traces.py +1 -2
- pytonapi-2.1.0/pytonapi/rest/rotator.py +49 -0
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/streaming/client.py +5 -4
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/streaming/models.py +0 -1
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/streaming/sse.py +17 -10
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/streaming/ws.py +2 -5
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/types.py +15 -0
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/utils.py +32 -16
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/webhook/client.py +3 -5
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/webhook/dispatcher.py +5 -14
- {pytonapi-2.0.1 → pytonapi-2.1.0/pytonapi.egg-info}/PKG-INFO +18 -8
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi.egg-info/SOURCES.txt +1 -0
- pytonapi-2.1.0/pytonapi.egg-info/requires.txt +2 -0
- {pytonapi-2.0.1 → pytonapi-2.1.0}/tests/test_utils.py +0 -3
- pytonapi-2.0.1/pytonapi/__init__.py +0 -1
- pytonapi-2.0.1/pytonapi/rest/client.py +0 -97
- pytonapi-2.0.1/pytonapi/rest/mixin.py +0 -130
- pytonapi-2.0.1/pytonapi/rest/models/emulation.py +0 -21
- pytonapi-2.0.1/pytonapi.egg-info/requires.txt +0 -2
- {pytonapi-2.0.1 → pytonapi-2.1.0}/LICENSE +0 -0
- /pytonapi-2.0.1/pytonapi/py.typed → /pytonapi-2.1.0/pytonapi/__init__.py +0 -0
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/cli.py +0 -0
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/__init__.py +0 -0
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/limiter.py +0 -0
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/models/_enums.py +0 -0
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/models/connect.py +0 -0
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/models/dns.py +0 -0
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/models/events.py +0 -0
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/models/extra_currency.py +0 -0
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/models/jettons.py +0 -0
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/models/lite_server.py +0 -0
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/models/nft.py +0 -0
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/models/purchases.py +0 -0
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/models/rates.py +0 -0
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/models/staking.py +0 -0
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/models/storage.py +0 -0
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/models/traces.py +0 -0
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/models/utilities.py +0 -0
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/resources/__init__.py +0 -0
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/resources/_base.py +0 -0
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/resources/connect.py +0 -0
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/resources/dns.py +0 -0
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/resources/emulation.py +0 -0
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/resources/extra_currency.py +0 -0
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/resources/gasless.py +0 -0
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/resources/lite_server.py +0 -0
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/resources/multisig.py +0 -0
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/resources/nft.py +0 -0
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/resources/purchases.py +0 -0
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/resources/storage.py +0 -0
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/resources/utilities.py +0 -0
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/resources/wallet.py +0 -0
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/streaming/__init__.py +0 -0
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/webhook/__init__.py +0 -0
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/webhook/models.py +0 -0
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi.egg-info/dependency_links.txt +0 -0
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi.egg-info/entry_points.txt +0 -0
- {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi.egg-info/top_level.txt +0 -0
- {pytonapi-2.0.1 → pytonapi-2.1.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pytonapi
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: Python SDK for TONAPI — REST API, streaming, and webhooks for TON blockchain.
|
|
5
5
|
Author: nessshon
|
|
6
6
|
Maintainer: nessshon
|
|
@@ -24,10 +24,10 @@ Requires-Python: <3.15,>=3.10
|
|
|
24
24
|
Description-Content-Type: text/markdown
|
|
25
25
|
License-File: LICENSE
|
|
26
26
|
Requires-Dist: aiohttp>=3.9.0
|
|
27
|
-
Requires-Dist: pydantic<3.0,>=2.
|
|
27
|
+
Requires-Dist: pydantic<3.0,>=2.0
|
|
28
28
|
Dynamic: license-file
|
|
29
29
|
|
|
30
|
-
# 📦
|
|
30
|
+
# 📦 TON API
|
|
31
31
|
|
|
32
32
|
[](https://ton.org)
|
|
33
33
|

|
|
@@ -35,17 +35,16 @@ Dynamic: license-file
|
|
|
35
35
|
[](https://github.com/nessshon/tonapi/blob/main/LICENSE)
|
|
36
36
|
[](https://tonviewer.com/UQCZq3_Vd21-4y4m7Wc-ej9NFOhh_qvdfAkAYAOHoQ__Ness)
|
|
37
37
|
|
|
38
|
-

|
|
39
39
|
|
|
40
40
|

|
|
41
41
|

|
|
42
42
|

|
|
43
43
|
|
|
44
|
-
### Python SDK for [
|
|
44
|
+
### Python SDK for [TON API](https://tonapi.io)
|
|
45
45
|
|
|
46
|
-
Access TON blockchain data via REST API, real-time streaming, and webhooks.
|
|
47
|
-
API key required — obtain at [tonconsole.com](https://tonconsole.com/)
|
|
48
|
-
at [docs.tonconsole.com](https://docs.tonconsole.com/).
|
|
46
|
+
Access TON blockchain data via REST API, real-time streaming, and webhooks.
|
|
47
|
+
API key optional for REST, required for streaming and webhooks — obtain at [tonconsole.com](https://tonconsole.com/).
|
|
49
48
|
|
|
50
49
|
> For creating wallets, transferring TON, jettons, etc., use [tonutils](https://github.com/nessshon/tonutils).
|
|
51
50
|
|
|
@@ -64,6 +63,17 @@ at [docs.tonconsole.com](https://docs.tonconsole.com/).
|
|
|
64
63
|
pip install pytonapi
|
|
65
64
|
```
|
|
66
65
|
|
|
66
|
+
[Claude Code plugin](https://github.com/nessshon/tonapi/blob/main/skills/tonapi/README.md):
|
|
67
|
+
```
|
|
68
|
+
/plugin marketplace add nessshon/claude-plugins
|
|
69
|
+
/plugin install tonapi@nessshon-plugins
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Documentation
|
|
73
|
+
|
|
74
|
+
[Documentation](https://tonapi.ness.su/) — API reference, streaming, and webhooks guides.
|
|
75
|
+
[llms.txt](https://tonapi.ness.su/llms.txt) — auto-generated machine-readable docs for AI tools.
|
|
76
|
+
|
|
67
77
|
## Examples
|
|
68
78
|
|
|
69
79
|
**REST API**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# 📦
|
|
1
|
+
# 📦 TON API
|
|
2
2
|
|
|
3
3
|
[](https://ton.org)
|
|
4
4
|

|
|
@@ -6,17 +6,16 @@
|
|
|
6
6
|
[](https://github.com/nessshon/tonapi/blob/main/LICENSE)
|
|
7
7
|
[](https://tonviewer.com/UQCZq3_Vd21-4y4m7Wc-ej9NFOhh_qvdfAkAYAOHoQ__Ness)
|
|
8
8
|
|
|
9
|
-

|
|
10
10
|
|
|
11
11
|

|
|
12
12
|

|
|
13
13
|

|
|
14
14
|
|
|
15
|
-
### Python SDK for [
|
|
15
|
+
### Python SDK for [TON API](https://tonapi.io)
|
|
16
16
|
|
|
17
|
-
Access TON blockchain data via REST API, real-time streaming, and webhooks.
|
|
18
|
-
API key required — obtain at [tonconsole.com](https://tonconsole.com/)
|
|
19
|
-
at [docs.tonconsole.com](https://docs.tonconsole.com/).
|
|
17
|
+
Access TON blockchain data via REST API, real-time streaming, and webhooks.
|
|
18
|
+
API key optional for REST, required for streaming and webhooks — obtain at [tonconsole.com](https://tonconsole.com/).
|
|
20
19
|
|
|
21
20
|
> For creating wallets, transferring TON, jettons, etc., use [tonutils](https://github.com/nessshon/tonutils).
|
|
22
21
|
|
|
@@ -35,6 +34,17 @@ at [docs.tonconsole.com](https://docs.tonconsole.com/).
|
|
|
35
34
|
pip install pytonapi
|
|
36
35
|
```
|
|
37
36
|
|
|
37
|
+
[Claude Code plugin](https://github.com/nessshon/tonapi/blob/main/skills/tonapi/README.md):
|
|
38
|
+
```
|
|
39
|
+
/plugin marketplace add nessshon/claude-plugins
|
|
40
|
+
/plugin install tonapi@nessshon-plugins
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Documentation
|
|
44
|
+
|
|
45
|
+
[Documentation](https://tonapi.ness.su/) — API reference, streaming, and webhooks guides.
|
|
46
|
+
[llms.txt](https://tonapi.ness.su/llms.txt) — auto-generated machine-readable docs for AI tools.
|
|
47
|
+
|
|
38
48
|
## Examples
|
|
39
49
|
|
|
40
50
|
**REST API**
|
|
@@ -14,7 +14,7 @@ maintainers = [
|
|
|
14
14
|
]
|
|
15
15
|
dependencies = [
|
|
16
16
|
"aiohttp>=3.9.0",
|
|
17
|
-
"pydantic>=2.
|
|
17
|
+
"pydantic>=2.0,<3.0",
|
|
18
18
|
]
|
|
19
19
|
keywords = [
|
|
20
20
|
"AsyncIO",
|
|
@@ -104,8 +104,6 @@ select = [
|
|
|
104
104
|
"RUF",
|
|
105
105
|
]
|
|
106
106
|
ignore = [
|
|
107
|
-
# line too long
|
|
108
|
-
"E501",
|
|
109
107
|
# docstring in public module
|
|
110
108
|
"D100",
|
|
111
109
|
# docstring in public package
|
|
@@ -116,6 +114,8 @@ ignore = [
|
|
|
116
114
|
"D107",
|
|
117
115
|
# blank line before class docstring (conflicts D211)
|
|
118
116
|
"D203",
|
|
117
|
+
# 1 blank line required between summary and description (multi-line summary)
|
|
118
|
+
"D205",
|
|
119
119
|
# multi-line summary second line (conflicts D212)
|
|
120
120
|
"D213",
|
|
121
121
|
# first word capitalization (false positive for proper nouns like dApp)
|
|
@@ -129,6 +129,7 @@ ignore = [
|
|
|
129
129
|
"__init__.py" = ["F401"]
|
|
130
130
|
"pytonapi/rest/models/*" = ["D101"]
|
|
131
131
|
"tests/*" = ["D101", "D102", "T20"]
|
|
132
|
+
"tests/rest/fixtures.py" = ["E501"]
|
|
132
133
|
"codegen/*" = ["T20"]
|
|
133
134
|
"examples/*" = ["D", "T20", "RUF006", "RUF059"]
|
|
134
135
|
|
|
@@ -152,4 +153,9 @@ enable_error_code = [
|
|
|
152
153
|
"ignore-without-code",
|
|
153
154
|
"redundant-cast",
|
|
154
155
|
"truthy-bool",
|
|
155
|
-
]
|
|
156
|
+
]
|
|
157
|
+
|
|
158
|
+
[[tool.mypy.overrides]]
|
|
159
|
+
module = "tests.*"
|
|
160
|
+
disallow_untyped_defs = false
|
|
161
|
+
disallow_incomplete_defs = false
|
|
@@ -4,13 +4,18 @@ import asyncio
|
|
|
4
4
|
import json
|
|
5
5
|
import typing as t
|
|
6
6
|
|
|
7
|
+
if t.TYPE_CHECKING:
|
|
8
|
+
import types
|
|
9
|
+
|
|
7
10
|
import aiohttp
|
|
11
|
+
from pydantic import TypeAdapter, ValidationError
|
|
8
12
|
|
|
9
13
|
from pytonapi.exceptions import (
|
|
10
14
|
TONAPIConnectionError,
|
|
11
15
|
TONAPIError,
|
|
12
16
|
TONAPIRetryLimitError,
|
|
13
17
|
TONAPISessionNotCreatedError,
|
|
18
|
+
TONAPIValidationError,
|
|
14
19
|
raise_for_status,
|
|
15
20
|
)
|
|
16
21
|
from pytonapi.types import (
|
|
@@ -21,6 +26,18 @@ from pytonapi.types import (
|
|
|
21
26
|
__all__ = ["BaseClient"]
|
|
22
27
|
|
|
23
28
|
T = t.TypeVar("T")
|
|
29
|
+
_Self = t.TypeVar("_Self", bound="BaseClient")
|
|
30
|
+
|
|
31
|
+
_adapter_cache: dict[t.Any, TypeAdapter[t.Any]] = {}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _get_adapter(model: t.Any) -> TypeAdapter[t.Any]:
|
|
35
|
+
"""Return a cached ``TypeAdapter`` for the given model."""
|
|
36
|
+
adapter = _adapter_cache.get(model)
|
|
37
|
+
if adapter is None:
|
|
38
|
+
adapter = TypeAdapter(model)
|
|
39
|
+
_adapter_cache[model] = adapter
|
|
40
|
+
return adapter
|
|
24
41
|
|
|
25
42
|
|
|
26
43
|
class BaseClient:
|
|
@@ -28,8 +45,8 @@ class BaseClient:
|
|
|
28
45
|
|
|
29
46
|
def __init__(
|
|
30
47
|
self,
|
|
31
|
-
api_key: str,
|
|
32
|
-
base_url: str,
|
|
48
|
+
api_key: str = "",
|
|
49
|
+
base_url: str = "",
|
|
33
50
|
*,
|
|
34
51
|
timeout: float = 10.0,
|
|
35
52
|
session: aiohttp.ClientSession | None = None,
|
|
@@ -39,7 +56,9 @@ class BaseClient:
|
|
|
39
56
|
) -> None:
|
|
40
57
|
"""Initialize the base HTTP client.
|
|
41
58
|
|
|
42
|
-
:param api_key: TONAPI key.
|
|
59
|
+
:param api_key: TONAPI key. Optional for REST — without a key
|
|
60
|
+
requests are throttled to ~0.24 RPS (1 per 4 seconds).
|
|
61
|
+
Get one at https://tonconsole.com/.
|
|
43
62
|
:param base_url: Base URL for all requests.
|
|
44
63
|
:param timeout: Request timeout in seconds.
|
|
45
64
|
:param session: Optional external ``aiohttp.ClientSession``.
|
|
@@ -60,7 +79,7 @@ class BaseClient:
|
|
|
60
79
|
self._is_external_session = session is not None
|
|
61
80
|
self._retry_policy = retry_policy
|
|
62
81
|
|
|
63
|
-
async def create_session(self) ->
|
|
82
|
+
async def create_session(self: _Self) -> _Self:
|
|
64
83
|
"""Create an ``aiohttp.ClientSession`` for making requests.
|
|
65
84
|
|
|
66
85
|
If an external session was provided via the ``session`` parameter,
|
|
@@ -85,9 +104,10 @@ class BaseClient:
|
|
|
85
104
|
"""
|
|
86
105
|
if self._session and not self._session.closed and not self._is_external_session:
|
|
87
106
|
await self._session.close()
|
|
107
|
+
await asyncio.sleep(0)
|
|
88
108
|
self._session = None
|
|
89
109
|
|
|
90
|
-
async def __aenter__(self) ->
|
|
110
|
+
async def __aenter__(self: _Self) -> _Self:
|
|
91
111
|
"""Enter the async context manager."""
|
|
92
112
|
await self.create_session()
|
|
93
113
|
return self
|
|
@@ -96,7 +116,7 @@ class BaseClient:
|
|
|
96
116
|
self,
|
|
97
117
|
exc_type: type[BaseException] | None,
|
|
98
118
|
exc_val: BaseException | None,
|
|
99
|
-
exc_tb:
|
|
119
|
+
exc_tb: types.TracebackType | None,
|
|
100
120
|
) -> None:
|
|
101
121
|
"""Exit the async context manager."""
|
|
102
122
|
await self.close_session()
|
|
@@ -106,10 +126,9 @@ class BaseClient:
|
|
|
106
126
|
|
|
107
127
|
:return: Merged headers dict.
|
|
108
128
|
"""
|
|
109
|
-
base = {
|
|
110
|
-
|
|
111
|
-
"
|
|
112
|
-
}
|
|
129
|
+
base: dict[str, str] = {"Accept": "application/json"}
|
|
130
|
+
if self._api_key:
|
|
131
|
+
base["Authorization"] = f"Bearer {self._api_key}"
|
|
113
132
|
base.update(self._headers)
|
|
114
133
|
return base
|
|
115
134
|
|
|
@@ -179,11 +198,7 @@ class BaseClient:
|
|
|
179
198
|
"""
|
|
180
199
|
url = f"{self._base_url}{path}"
|
|
181
200
|
if params:
|
|
182
|
-
params = {
|
|
183
|
-
k: str(v).lower() if isinstance(v, bool) else v
|
|
184
|
-
for k, v in params.items()
|
|
185
|
-
if v is not None
|
|
186
|
-
}
|
|
201
|
+
params = {k: str(v).lower() if isinstance(v, bool) else v for k, v in params.items() if v is not None}
|
|
187
202
|
if headers:
|
|
188
203
|
headers = {k: str(v) for k, v in headers.items() if v is not None}
|
|
189
204
|
|
|
@@ -283,4 +298,10 @@ class BaseClient:
|
|
|
283
298
|
data, _ = self._parse_body(text)
|
|
284
299
|
if data is None:
|
|
285
300
|
raise TONAPIError(f"Expected JSON response, got: {text}")
|
|
286
|
-
|
|
301
|
+
try:
|
|
302
|
+
return _get_adapter(response_model).validate_python(data)
|
|
303
|
+
except ValidationError as exc:
|
|
304
|
+
raise TONAPIValidationError(
|
|
305
|
+
model=response_model,
|
|
306
|
+
errors=exc.errors(),
|
|
307
|
+
) from exc
|
|
@@ -5,11 +5,14 @@ __all__ = [
|
|
|
5
5
|
"TONAPI_STATUS_TO_EXCEPTION",
|
|
6
6
|
"TONAPIBadRequestError",
|
|
7
7
|
"TONAPIClientError",
|
|
8
|
+
"TONAPIConflictError",
|
|
8
9
|
"TONAPIConnectionError",
|
|
9
10
|
"TONAPIConnectionLostError",
|
|
10
11
|
"TONAPIError",
|
|
11
12
|
"TONAPIForbiddenError",
|
|
13
|
+
"TONAPIGatewayTimeoutError",
|
|
12
14
|
"TONAPIInternalServerError",
|
|
15
|
+
"TONAPIMethodNotAllowedError",
|
|
13
16
|
"TONAPINotFoundError",
|
|
14
17
|
"TONAPINotImplementedError",
|
|
15
18
|
"TONAPIRetryLimitError",
|
|
@@ -19,6 +22,8 @@ __all__ = [
|
|
|
19
22
|
"TONAPIStreamingError",
|
|
20
23
|
"TONAPITooManyRequestsError",
|
|
21
24
|
"TONAPIUnauthorizedError",
|
|
25
|
+
"TONAPIUnprocessableError",
|
|
26
|
+
"TONAPIValidationError",
|
|
22
27
|
"raise_for_status",
|
|
23
28
|
]
|
|
24
29
|
|
|
@@ -81,6 +86,18 @@ class TONAPINotFoundError(TONAPIClientError):
|
|
|
81
86
|
"""HTTP 404 Not Found."""
|
|
82
87
|
|
|
83
88
|
|
|
89
|
+
class TONAPIMethodNotAllowedError(TONAPIClientError):
|
|
90
|
+
"""HTTP 405 Method Not Allowed."""
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class TONAPIConflictError(TONAPIClientError):
|
|
94
|
+
"""HTTP 409 Conflict."""
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class TONAPIUnprocessableError(TONAPIClientError):
|
|
98
|
+
"""HTTP 422 Unprocessable Entity."""
|
|
99
|
+
|
|
100
|
+
|
|
84
101
|
class TONAPITooManyRequestsError(TONAPIClientError):
|
|
85
102
|
"""HTTP 429 Too Many Requests."""
|
|
86
103
|
|
|
@@ -95,6 +112,27 @@ class TONAPINotImplementedError(TONAPIServerError):
|
|
|
95
112
|
"""HTTP 501 Not Implemented."""
|
|
96
113
|
|
|
97
114
|
|
|
115
|
+
class TONAPIGatewayTimeoutError(TONAPIServerError):
|
|
116
|
+
"""HTTP 504 Gateway Timeout."""
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class TONAPIValidationError(TONAPIError):
|
|
120
|
+
"""Response body did not match the expected Pydantic model."""
|
|
121
|
+
|
|
122
|
+
model: type
|
|
123
|
+
errors: list[t.Any]
|
|
124
|
+
|
|
125
|
+
def __init__(self, *, model: type, errors: list[t.Any]) -> None:
|
|
126
|
+
self.model = model
|
|
127
|
+
self.errors = errors
|
|
128
|
+
field_hints = ", ".join(f"{e.get('loc', '?')}: {e.get('msg', '')}" for e in errors[:3])
|
|
129
|
+
if len(errors) > 3:
|
|
130
|
+
field_hints += f" ... (+{len(errors) - 3} more)"
|
|
131
|
+
super().__init__(
|
|
132
|
+
f"Response validation failed for {model.__name__}: {field_hints}",
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
98
136
|
class TONAPIStreamingError(TONAPIError):
|
|
99
137
|
"""Streaming transport error."""
|
|
100
138
|
|
|
@@ -158,9 +196,13 @@ TONAPI_STATUS_TO_EXCEPTION: t.Final[dict[int, type[TONAPIStatusError]]] = {
|
|
|
158
196
|
401: TONAPIUnauthorizedError,
|
|
159
197
|
403: TONAPIForbiddenError,
|
|
160
198
|
404: TONAPINotFoundError,
|
|
199
|
+
405: TONAPIMethodNotAllowedError,
|
|
200
|
+
409: TONAPIConflictError,
|
|
201
|
+
422: TONAPIUnprocessableError,
|
|
161
202
|
429: TONAPITooManyRequestsError,
|
|
162
203
|
500: TONAPIInternalServerError,
|
|
163
204
|
501: TONAPINotImplementedError,
|
|
205
|
+
504: TONAPIGatewayTimeoutError,
|
|
164
206
|
}
|
|
165
207
|
|
|
166
208
|
|
|
File without changes
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typing as t
|
|
4
|
+
|
|
5
|
+
import aiohttp
|
|
6
|
+
|
|
7
|
+
from pytonapi.client import BaseClient
|
|
8
|
+
from pytonapi.exceptions import TONAPITooManyRequestsError
|
|
9
|
+
from pytonapi.rest.limiter import RateLimiter
|
|
10
|
+
from pytonapi.rest.mixin import ResourcesMixin
|
|
11
|
+
from pytonapi.rest.rotator import KeyRotator
|
|
12
|
+
from pytonapi.types import (
|
|
13
|
+
DEFAULT_RETRY_POLICY,
|
|
14
|
+
NETWORK_BASE_URLS,
|
|
15
|
+
ApiKey,
|
|
16
|
+
Network,
|
|
17
|
+
RetryPolicy,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = ["TonapiRestClient"]
|
|
21
|
+
|
|
22
|
+
T = t.TypeVar("T")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TonapiRestClient(BaseClient, ResourcesMixin):
|
|
26
|
+
"""Async client for the TONAPI."""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
api_key: str | ApiKey | list[ApiKey] = "",
|
|
31
|
+
network: Network = Network.MAINNET,
|
|
32
|
+
*,
|
|
33
|
+
base_url: str | None = None,
|
|
34
|
+
timeout: float = 10.0,
|
|
35
|
+
session: aiohttp.ClientSession | None = None,
|
|
36
|
+
headers: dict[str, str] | None = None,
|
|
37
|
+
cookies: dict[str, str] | None = None,
|
|
38
|
+
rps_limit: int | None = None,
|
|
39
|
+
rps_period: float | None = None,
|
|
40
|
+
retry_policy: RetryPolicy | None = DEFAULT_RETRY_POLICY,
|
|
41
|
+
) -> None:
|
|
42
|
+
"""Initialize the TONAPI client.
|
|
43
|
+
|
|
44
|
+
:param api_key: TONAPI key, ``ApiKey`` with per-key rate limit,
|
|
45
|
+
or a list of ``ApiKey`` for automatic rotation on HTTP 429.
|
|
46
|
+
Optional — without a key requests are throttled to ~0.24 RPS
|
|
47
|
+
(1 per 4 seconds). Get one at https://tonconsole.com/.
|
|
48
|
+
:param network: Target network (``Network.MAINNET`` or ``Network.TESTNET``).
|
|
49
|
+
:param base_url: Custom base URL (overrides ``network``).
|
|
50
|
+
:param timeout: Request timeout in seconds.
|
|
51
|
+
:param session: Optional external ``aiohttp.ClientSession``.
|
|
52
|
+
When provided, the client will not close it — the caller
|
|
53
|
+
is responsible for managing its lifecycle.
|
|
54
|
+
:param headers: Additional HTTP headers sent with every request.
|
|
55
|
+
:param cookies: Additional cookies sent with every request.
|
|
56
|
+
:param rps_limit: Maximum requests per second.
|
|
57
|
+
Used only when ``api_key`` is a plain string.
|
|
58
|
+
``None`` (default) — auto: ``1`` RPS without a key,
|
|
59
|
+
disabled with a key. ``0`` — explicitly disabled.
|
|
60
|
+
:param rps_period: Rate-limiter window in seconds.
|
|
61
|
+
Used only when ``api_key`` is a plain string.
|
|
62
|
+
``None`` (default) — ``4.0`` s when auto-limiting
|
|
63
|
+
without a key, ``1.0`` s when ``rps_limit`` is set
|
|
64
|
+
explicitly.
|
|
65
|
+
:param retry_policy: Retry policy, or ``None`` to disable retries.
|
|
66
|
+
"""
|
|
67
|
+
if isinstance(api_key, list):
|
|
68
|
+
self._key_rotator: KeyRotator | None = KeyRotator(api_key) if api_key else None
|
|
69
|
+
initial_key = api_key[0].key if api_key else ""
|
|
70
|
+
self._rate_limiter: RateLimiter | None = None
|
|
71
|
+
elif isinstance(api_key, ApiKey):
|
|
72
|
+
self._key_rotator = None
|
|
73
|
+
initial_key = api_key.key
|
|
74
|
+
self._rate_limiter = (
|
|
75
|
+
RateLimiter(rps=api_key.rps_limit, period=api_key.rps_period) if api_key.rps_limit > 0 else None
|
|
76
|
+
)
|
|
77
|
+
else:
|
|
78
|
+
self._key_rotator = None
|
|
79
|
+
initial_key = api_key
|
|
80
|
+
if rps_limit is None:
|
|
81
|
+
self._rate_limiter = RateLimiter(rps=1, period=rps_period or 4.0) if not api_key else None
|
|
82
|
+
elif rps_limit > 0:
|
|
83
|
+
self._rate_limiter = RateLimiter(rps=rps_limit, period=rps_period or 1.0)
|
|
84
|
+
else:
|
|
85
|
+
self._rate_limiter = None
|
|
86
|
+
|
|
87
|
+
super().__init__(
|
|
88
|
+
api_key=initial_key,
|
|
89
|
+
base_url=base_url or NETWORK_BASE_URLS[network],
|
|
90
|
+
timeout=timeout,
|
|
91
|
+
session=session,
|
|
92
|
+
headers=headers,
|
|
93
|
+
cookies=cookies,
|
|
94
|
+
retry_policy=retry_policy,
|
|
95
|
+
)
|
|
96
|
+
ResourcesMixin.__init__(self, self)
|
|
97
|
+
|
|
98
|
+
async def request(
|
|
99
|
+
self,
|
|
100
|
+
method: str,
|
|
101
|
+
path: str,
|
|
102
|
+
*,
|
|
103
|
+
params: dict[str, t.Any] | None = None,
|
|
104
|
+
body: t.Any | None = None,
|
|
105
|
+
headers: dict[str, t.Any] | None = None,
|
|
106
|
+
response_model: type[T] | None = None,
|
|
107
|
+
) -> t.Any:
|
|
108
|
+
"""Execute an HTTP request with retry and rate limiting.
|
|
109
|
+
|
|
110
|
+
When multiple ``ApiKey`` instances are configured, rotates to the
|
|
111
|
+
next key after all retries for the current key are exhausted on
|
|
112
|
+
HTTP 429. Each key uses its own ``RateLimiter``.
|
|
113
|
+
|
|
114
|
+
:param method: HTTP method (``GET``, ``POST``, etc.).
|
|
115
|
+
:param path: API path.
|
|
116
|
+
:param params: Query parameters.
|
|
117
|
+
:param body: JSON request body.
|
|
118
|
+
:param headers: Additional request headers.
|
|
119
|
+
:param response_model: Pydantic model to parse response into.
|
|
120
|
+
:return: Parsed model instance, raw dict, or ``None``.
|
|
121
|
+
"""
|
|
122
|
+
if self._key_rotator is None:
|
|
123
|
+
if self._rate_limiter:
|
|
124
|
+
await self._rate_limiter.acquire()
|
|
125
|
+
return await super().request(
|
|
126
|
+
method,
|
|
127
|
+
path,
|
|
128
|
+
params=params,
|
|
129
|
+
body=body,
|
|
130
|
+
headers=headers,
|
|
131
|
+
response_model=response_model,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
last_exc: TONAPITooManyRequestsError | None = None
|
|
135
|
+
for _ in range(len(self._key_rotator)):
|
|
136
|
+
limiter = self._key_rotator.current_limiter
|
|
137
|
+
if limiter:
|
|
138
|
+
await limiter.acquire()
|
|
139
|
+
key_headers = {
|
|
140
|
+
**(headers or {}),
|
|
141
|
+
"Authorization": f"Bearer {self._key_rotator.current_key}",
|
|
142
|
+
}
|
|
143
|
+
try:
|
|
144
|
+
return await super().request(
|
|
145
|
+
method,
|
|
146
|
+
path,
|
|
147
|
+
params=params,
|
|
148
|
+
body=body,
|
|
149
|
+
headers=key_headers,
|
|
150
|
+
response_model=response_model,
|
|
151
|
+
)
|
|
152
|
+
except TONAPITooManyRequestsError as exc:
|
|
153
|
+
last_exc = exc
|
|
154
|
+
self._key_rotator.rotate()
|
|
155
|
+
|
|
156
|
+
raise last_exc # type: ignore[misc]
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# This file is auto-generated. Do not edit manually.
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typing as t
|
|
6
|
+
|
|
7
|
+
from pytonapi.rest.resources.accounts import AccountsResource
|
|
8
|
+
from pytonapi.rest.resources.blockchain import BlockchainResource
|
|
9
|
+
from pytonapi.rest.resources.connect import ConnectResource
|
|
10
|
+
from pytonapi.rest.resources.dns import DNSResource
|
|
11
|
+
from pytonapi.rest.resources.emulation import EmulationResource
|
|
12
|
+
from pytonapi.rest.resources.events import EventsResource
|
|
13
|
+
from pytonapi.rest.resources.extra_currency import ExtraCurrencyResource
|
|
14
|
+
from pytonapi.rest.resources.gasless import GaslessResource
|
|
15
|
+
from pytonapi.rest.resources.jettons import JettonsResource
|
|
16
|
+
from pytonapi.rest.resources.lite_server import LiteServerResource
|
|
17
|
+
from pytonapi.rest.resources.multisig import MultisigResource
|
|
18
|
+
from pytonapi.rest.resources.nft import NFTResource
|
|
19
|
+
from pytonapi.rest.resources.purchases import PurchasesResource
|
|
20
|
+
from pytonapi.rest.resources.rates import RatesResource
|
|
21
|
+
from pytonapi.rest.resources.staking import StakingResource
|
|
22
|
+
from pytonapi.rest.resources.storage import StorageResource
|
|
23
|
+
from pytonapi.rest.resources.traces import TracesResource
|
|
24
|
+
from pytonapi.rest.resources.utilities import UtilitiesResource
|
|
25
|
+
from pytonapi.rest.resources.wallet import WalletResource
|
|
26
|
+
|
|
27
|
+
if t.TYPE_CHECKING:
|
|
28
|
+
from pytonapi.rest.client import TonapiRestClient
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ResourcesMixin:
|
|
32
|
+
"""Mixin that exposes all API resources as properties."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, client: TonapiRestClient) -> None:
|
|
35
|
+
self._client = client
|
|
36
|
+
self.accounts = AccountsResource(client)
|
|
37
|
+
self.blockchain = BlockchainResource(client)
|
|
38
|
+
self.connect = ConnectResource(client)
|
|
39
|
+
self.dns = DNSResource(client)
|
|
40
|
+
self.emulation = EmulationResource(client)
|
|
41
|
+
self.events = EventsResource(client)
|
|
42
|
+
self.extra_currency = ExtraCurrencyResource(client)
|
|
43
|
+
self.gasless = GaslessResource(client)
|
|
44
|
+
self.jettons = JettonsResource(client)
|
|
45
|
+
self.lite_server = LiteServerResource(client)
|
|
46
|
+
self.multisig = MultisigResource(client)
|
|
47
|
+
self.nft = NFTResource(client)
|
|
48
|
+
self.purchases = PurchasesResource(client)
|
|
49
|
+
self.rates = RatesResource(client)
|
|
50
|
+
self.staking = StakingResource(client)
|
|
51
|
+
self.storage = StorageResource(client)
|
|
52
|
+
self.traces = TracesResource(client)
|
|
53
|
+
self.utilities = UtilitiesResource(client)
|
|
54
|
+
self.wallet = WalletResource(client)
|
|
@@ -33,6 +33,7 @@ from pytonapi.rest.models.accounts import (
|
|
|
33
33
|
EncryptedComment,
|
|
34
34
|
ExecGetMethodArg,
|
|
35
35
|
ExtraCurrencies,
|
|
36
|
+
ExtraCurrency,
|
|
36
37
|
ExtraCurrencyTransferAction,
|
|
37
38
|
FlawedJettonTransferAction,
|
|
38
39
|
FoundAccounts,
|
|
@@ -88,7 +89,6 @@ from pytonapi.rest.models.blockchain import (
|
|
|
88
89
|
ConfigProposalSetup,
|
|
89
90
|
CreditPhase,
|
|
90
91
|
Error,
|
|
91
|
-
ExtraCurrency,
|
|
92
92
|
GasLimitPrices,
|
|
93
93
|
JettonBridgeParams,
|
|
94
94
|
JettonBridgePrices,
|
|
@@ -126,7 +126,7 @@ from pytonapi.rest.models.dns import (
|
|
|
126
126
|
PictureDNS,
|
|
127
127
|
WalletDNS,
|
|
128
128
|
)
|
|
129
|
-
from pytonapi.rest.models.emulation import DecodedMessage, DecodedRawMessage
|
|
129
|
+
from pytonapi.rest.models.emulation import DecodedMessage, DecodedRawMessage, JettonQuantity, MessageConsequences, Risk
|
|
130
130
|
from pytonapi.rest.models.events import Event, ValueFlow
|
|
131
131
|
from pytonapi.rest.models.extra_currency import EcPreview
|
|
132
132
|
from pytonapi.rest.models.gasless import GaslessConfig, GaslessTx, SignRawMessage, SignRawParams
|
|
@@ -139,7 +139,7 @@ from pytonapi.rest.models.jettons import (
|
|
|
139
139
|
ScaledUI,
|
|
140
140
|
)
|
|
141
141
|
from pytonapi.rest.models.lite_server import BlockRaw, InitStateRaw
|
|
142
|
-
from pytonapi.rest.models.multisig import
|
|
142
|
+
from pytonapi.rest.models.multisig import Multisig, MultisigOrder
|
|
143
143
|
from pytonapi.rest.models.nft import (
|
|
144
144
|
ImagePreview,
|
|
145
145
|
NftApprovedBy,
|
|
@@ -158,7 +158,6 @@ from pytonapi.rest.models.storage import StorageProvider
|
|
|
158
158
|
from pytonapi.rest.models.traces import Trace
|
|
159
159
|
from pytonapi.rest.models.utilities import ServiceStatus
|
|
160
160
|
from pytonapi.rest.models.wallet import (
|
|
161
|
-
MessageConsequences,
|
|
162
161
|
Seqno,
|
|
163
162
|
Wallet,
|
|
164
163
|
WalletPlugin,
|
|
@@ -190,6 +189,7 @@ _models_to_rebuild = [
|
|
|
190
189
|
EncryptedComment,
|
|
191
190
|
ExecGetMethodArg,
|
|
192
191
|
ExtraCurrencies,
|
|
192
|
+
ExtraCurrency,
|
|
193
193
|
ExtraCurrencyTransferAction,
|
|
194
194
|
FlawedJettonTransferAction,
|
|
195
195
|
FoundAccounts,
|
|
@@ -243,7 +243,6 @@ _models_to_rebuild = [
|
|
|
243
243
|
ConfigProposalSetup,
|
|
244
244
|
CreditPhase,
|
|
245
245
|
Error,
|
|
246
|
-
ExtraCurrency,
|
|
247
246
|
GasLimitPrices,
|
|
248
247
|
JettonBridgeParams,
|
|
249
248
|
JettonBridgePrices,
|
|
@@ -280,6 +279,9 @@ _models_to_rebuild = [
|
|
|
280
279
|
WalletDNS,
|
|
281
280
|
DecodedMessage,
|
|
282
281
|
DecodedRawMessage,
|
|
282
|
+
JettonQuantity,
|
|
283
|
+
MessageConsequences,
|
|
284
|
+
Risk,
|
|
283
285
|
Event,
|
|
284
286
|
ValueFlow,
|
|
285
287
|
EcPreview,
|
|
@@ -295,10 +297,8 @@ _models_to_rebuild = [
|
|
|
295
297
|
ScaledUI,
|
|
296
298
|
BlockRaw,
|
|
297
299
|
InitStateRaw,
|
|
298
|
-
JettonQuantity,
|
|
299
300
|
Multisig,
|
|
300
301
|
MultisigOrder,
|
|
301
|
-
Risk,
|
|
302
302
|
ImagePreview,
|
|
303
303
|
NftCollection,
|
|
304
304
|
NftCollections,
|
|
@@ -319,7 +319,6 @@ _models_to_rebuild = [
|
|
|
319
319
|
StorageProvider,
|
|
320
320
|
Trace,
|
|
321
321
|
ServiceStatus,
|
|
322
|
-
MessageConsequences,
|
|
323
322
|
Seqno,
|
|
324
323
|
Wallet,
|
|
325
324
|
WalletPlugin,
|