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.
Files changed (81) hide show
  1. {pytonapi-2.0.1/pytonapi.egg-info → pytonapi-2.1.0}/PKG-INFO +18 -8
  2. {pytonapi-2.0.1 → pytonapi-2.1.0}/README.md +16 -6
  3. {pytonapi-2.0.1 → pytonapi-2.1.0}/pyproject.toml +10 -4
  4. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/__meta__.py +1 -1
  5. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/client.py +37 -16
  6. {pytonapi-2.0.1 → 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.1 → pytonapi-2.1.0}/pytonapi/rest/models/__init__.py +7 -8
  11. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/models/accounts.py +147 -143
  12. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/models/blockchain.py +6 -12
  13. pytonapi-2.1.0/pytonapi/rest/models/emulation.py +43 -0
  14. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/models/gasless.py +1 -1
  15. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/models/multisig.py +1 -16
  16. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/models/wallet.py +0 -13
  17. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/resources/accounts.py +14 -14
  18. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/resources/blockchain.py +6 -2
  19. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/resources/events.py +6 -3
  20. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/resources/jettons.py +1 -2
  21. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/resources/rates.py +1 -2
  22. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/resources/staking.py +2 -2
  23. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/resources/traces.py +1 -2
  24. pytonapi-2.1.0/pytonapi/rest/rotator.py +49 -0
  25. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/streaming/client.py +5 -4
  26. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/streaming/models.py +0 -1
  27. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/streaming/sse.py +17 -10
  28. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/streaming/ws.py +2 -5
  29. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/types.py +15 -0
  30. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/utils.py +32 -16
  31. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/webhook/client.py +3 -5
  32. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/webhook/dispatcher.py +5 -14
  33. {pytonapi-2.0.1 → pytonapi-2.1.0/pytonapi.egg-info}/PKG-INFO +18 -8
  34. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi.egg-info/SOURCES.txt +1 -0
  35. pytonapi-2.1.0/pytonapi.egg-info/requires.txt +2 -0
  36. {pytonapi-2.0.1 → pytonapi-2.1.0}/tests/test_utils.py +0 -3
  37. pytonapi-2.0.1/pytonapi/__init__.py +0 -1
  38. pytonapi-2.0.1/pytonapi/rest/client.py +0 -97
  39. pytonapi-2.0.1/pytonapi/rest/mixin.py +0 -130
  40. pytonapi-2.0.1/pytonapi/rest/models/emulation.py +0 -21
  41. pytonapi-2.0.1/pytonapi.egg-info/requires.txt +0 -2
  42. {pytonapi-2.0.1 → pytonapi-2.1.0}/LICENSE +0 -0
  43. /pytonapi-2.0.1/pytonapi/py.typed → /pytonapi-2.1.0/pytonapi/__init__.py +0 -0
  44. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/cli.py +0 -0
  45. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/__init__.py +0 -0
  46. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/limiter.py +0 -0
  47. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/models/_enums.py +0 -0
  48. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/models/connect.py +0 -0
  49. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/models/dns.py +0 -0
  50. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/models/events.py +0 -0
  51. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/models/extra_currency.py +0 -0
  52. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/models/jettons.py +0 -0
  53. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/models/lite_server.py +0 -0
  54. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/models/nft.py +0 -0
  55. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/models/purchases.py +0 -0
  56. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/models/rates.py +0 -0
  57. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/models/staking.py +0 -0
  58. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/models/storage.py +0 -0
  59. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/models/traces.py +0 -0
  60. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/models/utilities.py +0 -0
  61. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/resources/__init__.py +0 -0
  62. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/resources/_base.py +0 -0
  63. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/resources/connect.py +0 -0
  64. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/resources/dns.py +0 -0
  65. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/resources/emulation.py +0 -0
  66. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/resources/extra_currency.py +0 -0
  67. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/resources/gasless.py +0 -0
  68. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/resources/lite_server.py +0 -0
  69. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/resources/multisig.py +0 -0
  70. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/resources/nft.py +0 -0
  71. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/resources/purchases.py +0 -0
  72. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/resources/storage.py +0 -0
  73. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/resources/utilities.py +0 -0
  74. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/rest/resources/wallet.py +0 -0
  75. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/streaming/__init__.py +0 -0
  76. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/webhook/__init__.py +0 -0
  77. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi/webhook/models.py +0 -0
  78. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi.egg-info/dependency_links.txt +0 -0
  79. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi.egg-info/entry_points.txt +0 -0
  80. {pytonapi-2.0.1 → pytonapi-2.1.0}/pytonapi.egg-info/top_level.txt +0 -0
  81. {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.1
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.4.1
27
+ Requires-Dist: pydantic<3.0,>=2.0
28
28
  Dynamic: license-file
29
29
 
30
- # 📦 PyTONAPI
30
+ # 📦 TON API
31
31
 
32
32
  [![TON](https://img.shields.io/badge/TON-grey?logo=TON&logoColor=40AEF0)](https://ton.org)
33
33
  ![Python Versions](https://img.shields.io/badge/Python-3.10%20--%203.14-black?color=FFE873&labelColor=3776AB)
@@ -35,17 +35,16 @@ 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)
42
42
  ![Downloads](https://pepy.tech/badge/pytonapi/week)
43
43
 
44
- ### Python SDK for [TONAPI](https://tonapi.io)
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**
@@ -1,4 +1,4 @@
1
- # 📦 PyTONAPI
1
+ # 📦 TON API
2
2
 
3
3
  [![TON](https://img.shields.io/badge/TON-grey?logo=TON&logoColor=40AEF0)](https://ton.org)
4
4
  ![Python Versions](https://img.shields.io/badge/Python-3.10%20--%203.14-black?color=FFE873&labelColor=3776AB)
@@ -6,17 +6,16 @@
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)
13
13
  ![Downloads](https://pepy.tech/badge/pytonapi/week)
14
14
 
15
- ### Python SDK for [TONAPI](https://tonapi.io)
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",
@@ -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
@@ -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.1"
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,
@@ -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 JettonQuantity, Multisig, MultisigOrder, Risk
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,