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.
Files changed (81) hide show
  1. {pytonapi-2.0.2/pytonapi.egg-info → pytonapi-2.1.0}/PKG-INFO +16 -6
  2. {pytonapi-2.0.2 → pytonapi-2.1.0}/README.md +14 -4
  3. {pytonapi-2.0.2 → pytonapi-2.1.0}/pyproject.toml +7 -2
  4. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/__meta__.py +1 -1
  5. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/client.py +37 -16
  6. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/exceptions.py +42 -0
  7. pytonapi-2.1.0/pytonapi/py.typed +0 -0
  8. pytonapi-2.1.0/pytonapi/rest/client.py +156 -0
  9. pytonapi-2.1.0/pytonapi/rest/mixin.py +54 -0
  10. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/models/__init__.py +12 -12
  11. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/models/accounts.py +167 -147
  12. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/models/blockchain.py +11 -17
  13. pytonapi-2.1.0/pytonapi/rest/models/emulation.py +43 -0
  14. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/models/gasless.py +1 -1
  15. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/models/multisig.py +1 -16
  16. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/models/nft.py +2 -12
  17. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/models/purchases.py +2 -8
  18. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/models/wallet.py +0 -13
  19. pytonapi-2.1.0/pytonapi/rest/rotator.py +49 -0
  20. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/streaming/client.py +5 -4
  21. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/streaming/models.py +0 -1
  22. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/streaming/sse.py +17 -10
  23. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/streaming/ws.py +2 -5
  24. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/types.py +15 -0
  25. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/webhook/client.py +3 -5
  26. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/webhook/dispatcher.py +5 -14
  27. {pytonapi-2.0.2 → pytonapi-2.1.0/pytonapi.egg-info}/PKG-INFO +16 -6
  28. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi.egg-info/SOURCES.txt +1 -0
  29. pytonapi-2.1.0/pytonapi.egg-info/requires.txt +2 -0
  30. {pytonapi-2.0.2 → pytonapi-2.1.0}/tests/test_utils.py +0 -3
  31. pytonapi-2.0.2/pytonapi/__init__.py +0 -1
  32. pytonapi-2.0.2/pytonapi/rest/client.py +0 -97
  33. pytonapi-2.0.2/pytonapi/rest/mixin.py +0 -130
  34. pytonapi-2.0.2/pytonapi/rest/models/emulation.py +0 -21
  35. pytonapi-2.0.2/pytonapi.egg-info/requires.txt +0 -2
  36. {pytonapi-2.0.2 → pytonapi-2.1.0}/LICENSE +0 -0
  37. pytonapi-2.0.2/pytonapi/py.typed → pytonapi-2.1.0/pytonapi/__init__.py +0 -0
  38. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/cli.py +0 -0
  39. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/__init__.py +0 -0
  40. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/limiter.py +0 -0
  41. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/models/_enums.py +0 -0
  42. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/models/connect.py +0 -0
  43. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/models/dns.py +6 -6
  44. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/models/events.py +0 -0
  45. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/models/extra_currency.py +0 -0
  46. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/models/jettons.py +5 -5
  47. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/models/lite_server.py +0 -0
  48. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/models/rates.py +0 -0
  49. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/models/staking.py +0 -0
  50. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/models/storage.py +0 -0
  51. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/models/traces.py +0 -0
  52. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/models/utilities.py +0 -0
  53. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/resources/__init__.py +0 -0
  54. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/resources/_base.py +0 -0
  55. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/resources/accounts.py +0 -0
  56. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/resources/blockchain.py +0 -0
  57. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/resources/connect.py +0 -0
  58. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/resources/dns.py +0 -0
  59. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/resources/emulation.py +0 -0
  60. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/resources/events.py +0 -0
  61. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/resources/extra_currency.py +0 -0
  62. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/resources/gasless.py +0 -0
  63. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/resources/jettons.py +0 -0
  64. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/resources/lite_server.py +0 -0
  65. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/resources/multisig.py +0 -0
  66. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/resources/nft.py +0 -0
  67. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/resources/purchases.py +0 -0
  68. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/resources/rates.py +0 -0
  69. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/resources/staking.py +0 -0
  70. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/resources/storage.py +0 -0
  71. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/resources/traces.py +0 -0
  72. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/resources/utilities.py +0 -0
  73. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/rest/resources/wallet.py +0 -0
  74. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/streaming/__init__.py +0 -0
  75. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/utils.py +0 -0
  76. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/webhook/__init__.py +0 -0
  77. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi/webhook/models.py +0 -0
  78. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi.egg-info/dependency_links.txt +0 -0
  79. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi.egg-info/entry_points.txt +0 -0
  80. {pytonapi-2.0.2 → pytonapi-2.1.0}/pytonapi.egg-info/top_level.txt +0 -0
  81. {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.2
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.4.1
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
  [![License](https://img.shields.io/github/license/nessshon/tonapi)](https://github.com/nessshon/tonapi/blob/main/LICENSE)
36
36
  [![Donate](https://img.shields.io/badge/Donate-TON-blue)](https://tonviewer.com/UQCZq3_Vd21-4y4m7Wc-ej9NFOhh_qvdfAkAYAOHoQ__Ness)
37
37
 
38
- ![Image](https://raw.githubusercontent.com/nessshon/tonapi/main/assets/banner.png)
38
+ ![Image](https://raw.githubusercontent.com/nessshon/tonapi/main/banner.png)
39
39
 
40
40
  ![Downloads](https://pepy.tech/badge/pytonapi)
41
41
  ![Downloads](https://pepy.tech/badge/pytonapi/month)
@@ -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/), docs
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
  [![License](https://img.shields.io/github/license/nessshon/tonapi)](https://github.com/nessshon/tonapi/blob/main/LICENSE)
7
7
  [![Donate](https://img.shields.io/badge/Donate-TON-blue)](https://tonviewer.com/UQCZq3_Vd21-4y4m7Wc-ej9NFOhh_qvdfAkAYAOHoQ__Ness)
8
8
 
9
- ![Image](https://raw.githubusercontent.com/nessshon/tonapi/main/assets/banner.png)
9
+ ![Image](https://raw.githubusercontent.com/nessshon/tonapi/main/banner.png)
10
10
 
11
11
  ![Downloads](https://pepy.tech/badge/pytonapi)
12
12
  ![Downloads](https://pepy.tech/badge/pytonapi/month)
@@ -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/), docs
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.4.1,<3.0",
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
@@ -3,6 +3,6 @@
3
3
  # This source code is licensed under the MIT License found in the
4
4
  # LICENSE file in the root directory of this source tree.
5
5
 
6
- __version__ = "2.0.2"
6
+ __version__ = "2.1.0"
7
7
  __author__ = "nessshon"
8
8
  __url__ = "https://github.com/nessshon/tonapi"
@@ -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. Get one at https://tonconsole.com/.
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) -> BaseClient:
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) -> BaseClient:
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: t.Any | None,
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
- "Authorization": f"Bearer {self._api_key}",
111
- "Accept": "application/json",
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
- return response_model.model_validate(data) # type: ignore[attr-defined]
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 JettonQuantity, Multisig, MultisigOrder, Risk
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, Metadata, Purchase
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,