pytonapi 2.0.2__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.2/pytonapi.egg-info → pytonapi-2.1.0}/PKG-INFO +16 -6
- {pytonapi-2.0.2 → pytonapi-2.1.0}/README.md +14 -4
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pyproject.toml +7 -2
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/__meta__.py +1 -1
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/client.py +37 -16
- {pytonapi-2.0.2 → 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.2 → pytonapi-2.1.0}/pytonapi/rest/models/__init__.py +12 -12
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/models/accounts.py +167 -147
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/models/blockchain.py +11 -17
- pytonapi-2.1.0/pytonapi/rest/models/emulation.py +43 -0
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/models/gasless.py +1 -1
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/models/multisig.py +1 -16
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/models/nft.py +2 -12
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/models/purchases.py +2 -8
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/models/wallet.py +0 -13
- pytonapi-2.1.0/pytonapi/rest/rotator.py +49 -0
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/streaming/client.py +5 -4
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/streaming/models.py +0 -1
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/streaming/sse.py +17 -10
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/streaming/ws.py +2 -5
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/types.py +15 -0
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/webhook/client.py +3 -5
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/webhook/dispatcher.py +5 -14
- {pytonapi-2.0.2 → pytonapi-2.1.0/pytonapi.egg-info}/PKG-INFO +16 -6
- {pytonapi-2.0.2 → 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.2 → pytonapi-2.1.0}/tests/test_utils.py +0 -3
- pytonapi-2.0.2/pytonapi/__init__.py +0 -1
- pytonapi-2.0.2/pytonapi/rest/client.py +0 -97
- pytonapi-2.0.2/pytonapi/rest/mixin.py +0 -130
- pytonapi-2.0.2/pytonapi/rest/models/emulation.py +0 -21
- pytonapi-2.0.2/pytonapi.egg-info/requires.txt +0 -2
- {pytonapi-2.0.2 → pytonapi-2.1.0}/LICENSE +0 -0
- pytonapi-2.0.2/pytonapi/py.typed → pytonapi-2.1.0/pytonapi/__init__.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/cli.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/__init__.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/limiter.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/models/_enums.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/models/connect.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/models/dns.py +6 -6
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/models/events.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/models/extra_currency.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/models/jettons.py +5 -5
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/models/lite_server.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/models/rates.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/models/staking.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/models/storage.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/models/traces.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/models/utilities.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/resources/__init__.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/resources/_base.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/resources/accounts.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/resources/blockchain.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/resources/connect.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/resources/dns.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/resources/emulation.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/resources/events.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/resources/extra_currency.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/resources/gasless.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/resources/jettons.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/resources/lite_server.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/resources/multisig.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/resources/nft.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/resources/purchases.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/resources/rates.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/resources/staking.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/resources/storage.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/resources/traces.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/resources/utilities.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/resources/wallet.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/streaming/__init__.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/utils.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/webhook/__init__.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/webhook/models.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi.egg-info/dependency_links.txt +0 -0
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi.egg-info/entry_points.txt +0 -0
- {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi.egg-info/top_level.txt +0 -0
- {pytonapi-2.0.2 → 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,7 +24,7 @@ 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
|
|
@@ -35,7 +35,7 @@ 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
|

|
|
@@ -43,9 +43,8 @@ Dynamic: license-file
|
|
|
43
43
|
|
|
44
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**
|
|
@@ -6,7 +6,7 @@
|
|
|
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
|

|
|
@@ -14,9 +14,8 @@
|
|
|
14
14
|
|
|
15
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",
|
|
@@ -153,4 +153,9 @@ enable_error_code = [
|
|
|
153
153
|
"ignore-without-code",
|
|
154
154
|
"redundant-cast",
|
|
155
155
|
"truthy-bool",
|
|
156
|
-
]
|
|
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,
|
|
@@ -47,9 +48,11 @@ from pytonapi.rest.models.accounts import (
|
|
|
47
48
|
JettonSwapAction,
|
|
48
49
|
JettonTransferAction,
|
|
49
50
|
LiquidityDepositAction,
|
|
51
|
+
Metadata,
|
|
50
52
|
Multisigs,
|
|
51
53
|
NftItemTransferAction,
|
|
52
54
|
NftPurchaseAction,
|
|
55
|
+
Price,
|
|
53
56
|
Protocol,
|
|
54
57
|
PurchaseAction,
|
|
55
58
|
Refund,
|
|
@@ -86,7 +89,6 @@ from pytonapi.rest.models.blockchain import (
|
|
|
86
89
|
ConfigProposalSetup,
|
|
87
90
|
CreditPhase,
|
|
88
91
|
Error,
|
|
89
|
-
ExtraCurrency,
|
|
90
92
|
GasLimitPrices,
|
|
91
93
|
JettonBridgeParams,
|
|
92
94
|
JettonBridgePrices,
|
|
@@ -124,7 +126,7 @@ from pytonapi.rest.models.dns import (
|
|
|
124
126
|
PictureDNS,
|
|
125
127
|
WalletDNS,
|
|
126
128
|
)
|
|
127
|
-
from pytonapi.rest.models.emulation import DecodedMessage, DecodedRawMessage
|
|
129
|
+
from pytonapi.rest.models.emulation import DecodedMessage, DecodedRawMessage, JettonQuantity, MessageConsequences, Risk
|
|
128
130
|
from pytonapi.rest.models.events import Event, ValueFlow
|
|
129
131
|
from pytonapi.rest.models.extra_currency import EcPreview
|
|
130
132
|
from pytonapi.rest.models.gasless import GaslessConfig, GaslessTx, SignRawMessage, SignRawParams
|
|
@@ -137,7 +139,7 @@ from pytonapi.rest.models.jettons import (
|
|
|
137
139
|
ScaledUI,
|
|
138
140
|
)
|
|
139
141
|
from pytonapi.rest.models.lite_server import BlockRaw, InitStateRaw
|
|
140
|
-
from pytonapi.rest.models.multisig import
|
|
142
|
+
from pytonapi.rest.models.multisig import Multisig, MultisigOrder
|
|
141
143
|
from pytonapi.rest.models.nft import (
|
|
142
144
|
ImagePreview,
|
|
143
145
|
NftApprovedBy,
|
|
@@ -147,17 +149,15 @@ from pytonapi.rest.models.nft import (
|
|
|
147
149
|
NftItems,
|
|
148
150
|
NftOperation,
|
|
149
151
|
NftOperations,
|
|
150
|
-
Price,
|
|
151
152
|
Sale,
|
|
152
153
|
)
|
|
153
|
-
from pytonapi.rest.models.purchases import AccountPurchases,
|
|
154
|
+
from pytonapi.rest.models.purchases import AccountPurchases, Purchase
|
|
154
155
|
from pytonapi.rest.models.rates import ChartPoints, MarketTonRates, TokenRates
|
|
155
156
|
from pytonapi.rest.models.staking import AccountStaking, AccountStakingInfo, ApyHistory, PoolImplementation, PoolInfo
|
|
156
157
|
from pytonapi.rest.models.storage import StorageProvider
|
|
157
158
|
from pytonapi.rest.models.traces import Trace
|
|
158
159
|
from pytonapi.rest.models.utilities import ServiceStatus
|
|
159
160
|
from pytonapi.rest.models.wallet import (
|
|
160
|
-
MessageConsequences,
|
|
161
161
|
Seqno,
|
|
162
162
|
Wallet,
|
|
163
163
|
WalletPlugin,
|
|
@@ -189,6 +189,7 @@ _models_to_rebuild = [
|
|
|
189
189
|
EncryptedComment,
|
|
190
190
|
ExecGetMethodArg,
|
|
191
191
|
ExtraCurrencies,
|
|
192
|
+
ExtraCurrency,
|
|
192
193
|
ExtraCurrencyTransferAction,
|
|
193
194
|
FlawedJettonTransferAction,
|
|
194
195
|
FoundAccounts,
|
|
@@ -203,9 +204,11 @@ _models_to_rebuild = [
|
|
|
203
204
|
JettonTransferAction,
|
|
204
205
|
JettonsBalances,
|
|
205
206
|
LiquidityDepositAction,
|
|
207
|
+
Metadata,
|
|
206
208
|
Multisigs,
|
|
207
209
|
NftItemTransferAction,
|
|
208
210
|
NftPurchaseAction,
|
|
211
|
+
Price,
|
|
209
212
|
Protocol,
|
|
210
213
|
PurchaseAction,
|
|
211
214
|
Refund,
|
|
@@ -240,7 +243,6 @@ _models_to_rebuild = [
|
|
|
240
243
|
ConfigProposalSetup,
|
|
241
244
|
CreditPhase,
|
|
242
245
|
Error,
|
|
243
|
-
ExtraCurrency,
|
|
244
246
|
GasLimitPrices,
|
|
245
247
|
JettonBridgeParams,
|
|
246
248
|
JettonBridgePrices,
|
|
@@ -277,6 +279,9 @@ _models_to_rebuild = [
|
|
|
277
279
|
WalletDNS,
|
|
278
280
|
DecodedMessage,
|
|
279
281
|
DecodedRawMessage,
|
|
282
|
+
JettonQuantity,
|
|
283
|
+
MessageConsequences,
|
|
284
|
+
Risk,
|
|
280
285
|
Event,
|
|
281
286
|
ValueFlow,
|
|
282
287
|
EcPreview,
|
|
@@ -292,10 +297,8 @@ _models_to_rebuild = [
|
|
|
292
297
|
ScaledUI,
|
|
293
298
|
BlockRaw,
|
|
294
299
|
InitStateRaw,
|
|
295
|
-
JettonQuantity,
|
|
296
300
|
Multisig,
|
|
297
301
|
MultisigOrder,
|
|
298
|
-
Risk,
|
|
299
302
|
ImagePreview,
|
|
300
303
|
NftCollection,
|
|
301
304
|
NftCollections,
|
|
@@ -303,10 +306,8 @@ _models_to_rebuild = [
|
|
|
303
306
|
NftItems,
|
|
304
307
|
NftOperation,
|
|
305
308
|
NftOperations,
|
|
306
|
-
Price,
|
|
307
309
|
Sale,
|
|
308
310
|
AccountPurchases,
|
|
309
|
-
Metadata,
|
|
310
311
|
Purchase,
|
|
311
312
|
MarketTonRates,
|
|
312
313
|
TokenRates,
|
|
@@ -318,7 +319,6 @@ _models_to_rebuild = [
|
|
|
318
319
|
StorageProvider,
|
|
319
320
|
Trace,
|
|
320
321
|
ServiceStatus,
|
|
321
|
-
MessageConsequences,
|
|
322
322
|
Seqno,
|
|
323
323
|
Wallet,
|
|
324
324
|
WalletPlugin,
|