pytonapi 2.0.2__tar.gz → 2.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. {pytonapi-2.0.2/pytonapi.egg-info → pytonapi-2.2.0}/PKG-INFO +38 -11
  2. {pytonapi-2.0.2 → pytonapi-2.2.0}/README.md +25 -9
  3. {pytonapi-2.0.2 → pytonapi-2.2.0}/pyproject.toml +26 -4
  4. {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/__meta__.py +1 -1
  5. {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/client.py +37 -16
  6. {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/exceptions.py +43 -1
  7. pytonapi-2.2.0/pytonapi/py.typed +0 -0
  8. pytonapi-2.2.0/pytonapi/rest/client.py +156 -0
  9. pytonapi-2.2.0/pytonapi/rest/mixin.py +54 -0
  10. {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/models/__init__.py +12 -12
  11. {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/models/accounts.py +167 -147
  12. {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/models/blockchain.py +11 -17
  13. pytonapi-2.2.0/pytonapi/rest/models/emulation.py +43 -0
  14. {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/models/gasless.py +1 -1
  15. {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/models/multisig.py +1 -16
  16. {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/models/nft.py +2 -12
  17. {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/models/purchases.py +2 -8
  18. {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/models/wallet.py +0 -13
  19. pytonapi-2.2.0/pytonapi/rest/rotator.py +49 -0
  20. pytonapi-2.2.0/pytonapi/streaming/__init__.py +35 -0
  21. pytonapi-2.2.0/pytonapi/streaming/base.py +492 -0
  22. pytonapi-2.2.0/pytonapi/streaming/models.py +196 -0
  23. pytonapi-2.2.0/pytonapi/streaming/sse.py +159 -0
  24. pytonapi-2.2.0/pytonapi/streaming/ws.py +282 -0
  25. {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/types.py +15 -0
  26. {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/webhook/client.py +3 -5
  27. {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/webhook/dispatcher.py +5 -14
  28. {pytonapi-2.0.2 → pytonapi-2.2.0/pytonapi.egg-info}/PKG-INFO +38 -11
  29. {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi.egg-info/SOURCES.txt +2 -1
  30. pytonapi-2.2.0/pytonapi.egg-info/requires.txt +13 -0
  31. {pytonapi-2.0.2 → pytonapi-2.2.0}/tests/test_utils.py +0 -3
  32. pytonapi-2.0.2/pytonapi/__init__.py +0 -1
  33. pytonapi-2.0.2/pytonapi/rest/client.py +0 -97
  34. pytonapi-2.0.2/pytonapi/rest/mixin.py +0 -130
  35. pytonapi-2.0.2/pytonapi/rest/models/emulation.py +0 -21
  36. pytonapi-2.0.2/pytonapi/streaming/__init__.py +0 -19
  37. pytonapi-2.0.2/pytonapi/streaming/client.py +0 -94
  38. pytonapi-2.0.2/pytonapi/streaming/models.py +0 -34
  39. pytonapi-2.0.2/pytonapi/streaming/sse.py +0 -247
  40. pytonapi-2.0.2/pytonapi/streaming/ws.py +0 -232
  41. pytonapi-2.0.2/pytonapi.egg-info/requires.txt +0 -2
  42. {pytonapi-2.0.2 → pytonapi-2.2.0}/LICENSE +0 -0
  43. pytonapi-2.0.2/pytonapi/py.typed → pytonapi-2.2.0/pytonapi/__init__.py +0 -0
  44. {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/cli.py +0 -0
  45. {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/__init__.py +0 -0
  46. {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/limiter.py +0 -0
  47. {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/models/_enums.py +0 -0
  48. {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/models/connect.py +0 -0
  49. {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/models/dns.py +6 -6
  50. {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/models/events.py +0 -0
  51. {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/models/extra_currency.py +0 -0
  52. {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/models/jettons.py +5 -5
  53. {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/models/lite_server.py +0 -0
  54. {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/models/rates.py +0 -0
  55. {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/models/staking.py +0 -0
  56. {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/models/storage.py +0 -0
  57. {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/models/traces.py +0 -0
  58. {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/models/utilities.py +0 -0
  59. {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/resources/__init__.py +0 -0
  60. {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/resources/_base.py +0 -0
  61. {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/resources/accounts.py +0 -0
  62. {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/resources/blockchain.py +0 -0
  63. {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/resources/connect.py +0 -0
  64. {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/resources/dns.py +0 -0
  65. {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/resources/emulation.py +0 -0
  66. {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/resources/events.py +0 -0
  67. {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/resources/extra_currency.py +0 -0
  68. {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/resources/gasless.py +0 -0
  69. {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/resources/jettons.py +0 -0
  70. {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/resources/lite_server.py +0 -0
  71. {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/resources/multisig.py +0 -0
  72. {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/resources/nft.py +0 -0
  73. {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/resources/purchases.py +0 -0
  74. {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/resources/rates.py +0 -0
  75. {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/resources/staking.py +0 -0
  76. {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/resources/storage.py +0 -0
  77. {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/resources/traces.py +0 -0
  78. {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/resources/utilities.py +0 -0
  79. {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/resources/wallet.py +0 -0
  80. {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/utils.py +0 -0
  81. {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/webhook/__init__.py +0 -0
  82. {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/webhook/models.py +0 -0
  83. {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi.egg-info/dependency_links.txt +0 -0
  84. {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi.egg-info/entry_points.txt +0 -0
  85. {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi.egg-info/top_level.txt +0 -0
  86. {pytonapi-2.0.2 → pytonapi-2.2.0}/setup.cfg +0 -0
@@ -1,12 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytonapi
3
- Version: 2.0.2
3
+ Version: 2.2.0
4
4
  Summary: Python SDK for TONAPI — REST API, streaming, and webhooks for TON blockchain.
5
5
  Author: nessshon
6
6
  Maintainer: nessshon
7
7
  License-Expression: MIT
8
8
  Project-URL: Homepage, https://github.com/nessshon/tonapi/
9
9
  Project-URL: Examples, https://github.com/nessshon/tonapi/tree/main/examples/
10
+ Project-URL: Documentation, https://tonapi.ness.su/
10
11
  Keywords: AsyncIO,REST API,SDK,TON,TON blockchain,TONAPI,The Open Network,blockchain,crypto,streaming,webhooks
11
12
  Classifier: Development Status :: 4 - Beta
12
13
  Classifier: Environment :: Console
@@ -24,7 +25,17 @@ Requires-Python: <3.15,>=3.10
24
25
  Description-Content-Type: text/markdown
25
26
  License-File: LICENSE
26
27
  Requires-Dist: aiohttp>=3.9.0
27
- Requires-Dist: pydantic<3.0,>=2.4.1
28
+ Requires-Dist: pydantic<3.0,>=2.0
29
+ Provides-Extra: dev
30
+ Requires-Dist: environs>=11.0.0; extra == "dev"
31
+ Requires-Dist: fastapi>=0.115.0; extra == "dev"
32
+ Requires-Dist: jinja2>=3.1; extra == "dev"
33
+ Requires-Dist: mypy>=1.19.0; extra == "dev"
34
+ Requires-Dist: pyyaml>=6.0; extra == "dev"
35
+ Requires-Dist: pytest>=8.0; extra == "dev"
36
+ Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
37
+ Requires-Dist: ruff>=0.8.0; extra == "dev"
38
+ Requires-Dist: uvicorn>=0.34.0; extra == "dev"
28
39
  Dynamic: license-file
29
40
 
30
41
  # 📦 TON API
@@ -35,7 +46,7 @@ Dynamic: license-file
35
46
  [![License](https://img.shields.io/github/license/nessshon/tonapi)](https://github.com/nessshon/tonapi/blob/main/LICENSE)
36
47
  [![Donate](https://img.shields.io/badge/Donate-TON-blue)](https://tonviewer.com/UQCZq3_Vd21-4y4m7Wc-ej9NFOhh_qvdfAkAYAOHoQ__Ness)
37
48
 
38
- ![Image](https://raw.githubusercontent.com/nessshon/tonapi/main/assets/banner.png)
49
+ ![Image](https://raw.githubusercontent.com/nessshon/tonapi/main/banner.png)
39
50
 
40
51
  ![Downloads](https://pepy.tech/badge/pytonapi)
41
52
  ![Downloads](https://pepy.tech/badge/pytonapi/month)
@@ -43,9 +54,8 @@ Dynamic: license-file
43
54
 
44
55
  ### Python SDK for [TON API](https://tonapi.io)
45
56
 
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/).
57
+ Access TON blockchain data via REST API, real-time streaming, and webhooks.
58
+ API key optional for REST, required for streaming and webhooks — obtain at [tonconsole.com](https://tonconsole.com/).
49
59
 
50
60
  > For creating wallets, transferring TON, jettons, etc., use [tonutils](https://github.com/nessshon/tonutils).
51
61
 
@@ -64,15 +74,37 @@ at [docs.tonconsole.com](https://docs.tonconsole.com/).
64
74
  pip install pytonapi
65
75
  ```
66
76
 
77
+ [Claude Code plugin](https://github.com/nessshon/tonapi/blob/main/skills/tonapi/README.md):
78
+ ```
79
+ /plugin marketplace add nessshon/claude-plugins
80
+ /plugin install tonapi@nessshon-plugins
81
+ ```
82
+
83
+ ## Documentation
84
+
85
+ [Documentation](https://tonapi.ness.su/) — API reference, streaming, and webhooks guides.
86
+ [llms.txt](https://tonapi.ness.su/llms.txt) — auto-generated machine-readable docs for AI tools.
87
+
67
88
  ## Examples
68
89
 
69
90
  **REST API**
70
91
 
71
92
  - [Get account info](https://github.com/nessshon/tonapi/blob/main/examples/get_account_info.py)
72
93
  - [Get account transactions](https://github.com/nessshon/tonapi/blob/main/examples/get_account_transactions.py)
94
+ - [Get jetton info](https://github.com/nessshon/tonapi/blob/main/examples/get_jetton_info.py)
73
95
  - [Get NFTs by owner](https://github.com/nessshon/tonapi/blob/main/examples/get_nft_by_owner.py)
74
96
  - [Get NFTs by collection](https://github.com/nessshon/tonapi/blob/main/examples/get_nft_by_collection.py)
75
97
 
98
+ **Emulation & Sending**
99
+
100
+ - [Send message](https://github.com/nessshon/tonapi/blob/main/examples/send_message.py)
101
+ - [Emulate message](https://github.com/nessshon/tonapi/blob/main/examples/emulate_message.py)
102
+
103
+ **Transfers** (requires [tonutils](https://github.com/nessshon/tonutils))
104
+
105
+ - [Transfer TON](https://github.com/nessshon/tonapi/blob/main/examples/transfer_ton.py)
106
+ - [Gasless transfer](https://github.com/nessshon/tonapi/blob/main/examples/transfer_gasless.py)
107
+
76
108
  **Streaming** (SSE & WebSocket)
77
109
 
78
110
  - [SSE subscriptions](https://github.com/nessshon/tonapi/blob/main/examples/streaming_sse.py)
@@ -83,11 +115,6 @@ pip install pytonapi
83
115
  - [FastAPI webhook server](https://github.com/nessshon/tonapi/blob/main/examples/webhook_fastapi.py)
84
116
  - [aiohttp webhook server](https://github.com/nessshon/tonapi/blob/main/examples/webhook_aiohttp.py)
85
117
 
86
- **Transfers** (requires [tonutils](https://github.com/nessshon/tonutils))
87
-
88
- - [Transfer TON](https://github.com/nessshon/tonapi/blob/main/examples/transfer_ton.py)
89
- - [Gasless transfer](https://github.com/nessshon/tonapi/blob/main/examples/transfer_gasless.py)
90
-
91
118
  ## License
92
119
 
93
120
  This repository is distributed under the [MIT License](https://github.com/nessshon/tonapi/blob/main/LICENSE).
@@ -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,15 +34,37 @@ 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**
41
51
 
42
52
  - [Get account info](https://github.com/nessshon/tonapi/blob/main/examples/get_account_info.py)
43
53
  - [Get account transactions](https://github.com/nessshon/tonapi/blob/main/examples/get_account_transactions.py)
54
+ - [Get jetton info](https://github.com/nessshon/tonapi/blob/main/examples/get_jetton_info.py)
44
55
  - [Get NFTs by owner](https://github.com/nessshon/tonapi/blob/main/examples/get_nft_by_owner.py)
45
56
  - [Get NFTs by collection](https://github.com/nessshon/tonapi/blob/main/examples/get_nft_by_collection.py)
46
57
 
58
+ **Emulation & Sending**
59
+
60
+ - [Send message](https://github.com/nessshon/tonapi/blob/main/examples/send_message.py)
61
+ - [Emulate message](https://github.com/nessshon/tonapi/blob/main/examples/emulate_message.py)
62
+
63
+ **Transfers** (requires [tonutils](https://github.com/nessshon/tonutils))
64
+
65
+ - [Transfer TON](https://github.com/nessshon/tonapi/blob/main/examples/transfer_ton.py)
66
+ - [Gasless transfer](https://github.com/nessshon/tonapi/blob/main/examples/transfer_gasless.py)
67
+
47
68
  **Streaming** (SSE & WebSocket)
48
69
 
49
70
  - [SSE subscriptions](https://github.com/nessshon/tonapi/blob/main/examples/streaming_sse.py)
@@ -54,11 +75,6 @@ pip install pytonapi
54
75
  - [FastAPI webhook server](https://github.com/nessshon/tonapi/blob/main/examples/webhook_fastapi.py)
55
76
  - [aiohttp webhook server](https://github.com/nessshon/tonapi/blob/main/examples/webhook_aiohttp.py)
56
77
 
57
- **Transfers** (requires [tonutils](https://github.com/nessshon/tonutils))
58
-
59
- - [Transfer TON](https://github.com/nessshon/tonapi/blob/main/examples/transfer_ton.py)
60
- - [Gasless transfer](https://github.com/nessshon/tonapi/blob/main/examples/transfer_gasless.py)
61
-
62
78
  ## License
63
79
 
64
80
  This repository is distributed under the [MIT License](https://github.com/nessshon/tonapi/blob/main/LICENSE).
@@ -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",
@@ -53,6 +53,20 @@ pytonapi = "pytonapi.cli:main"
53
53
  [project.urls]
54
54
  Homepage = "https://github.com/nessshon/tonapi/"
55
55
  Examples = "https://github.com/nessshon/tonapi/tree/main/examples/"
56
+ Documentation = "https://tonapi.ness.su/"
57
+
58
+ [project.optional-dependencies]
59
+ dev = [
60
+ "environs>=11.0.0",
61
+ "fastapi>=0.115.0",
62
+ "jinja2>=3.1",
63
+ "mypy>=1.19.0",
64
+ "pyyaml>=6.0",
65
+ "pytest>=8.0",
66
+ "pytest-asyncio>=0.24",
67
+ "ruff>=0.8.0",
68
+ "uvicorn>=0.34.0",
69
+ ]
56
70
 
57
71
  [tool.setuptools.packages.find]
58
72
  include = ["pytonapi", "pytonapi.*"]
@@ -128,10 +142,10 @@ ignore = [
128
142
  [tool.ruff.lint.per-file-ignores]
129
143
  "__init__.py" = ["F401"]
130
144
  "pytonapi/rest/models/*" = ["D101"]
131
- "tests/*" = ["D101", "D102", "T20"]
132
- "tests/rest/fixtures.py" = ["E501"]
133
145
  "codegen/*" = ["T20"]
134
146
  "examples/*" = ["D", "T20", "RUF006", "RUF059"]
147
+ "tests/*" = ["D101", "D102", "D103", "T20"]
148
+ "tests/rest/fixtures.py" = ["E501"]
135
149
 
136
150
  [tool.ruff.lint.isort]
137
151
  known-first-party = ["pytonapi"]
@@ -153,4 +167,12 @@ enable_error_code = [
153
167
  "ignore-without-code",
154
168
  "redundant-cast",
155
169
  "truthy-bool",
156
- ]
170
+ ]
171
+
172
+ [[tool.mypy.overrides]]
173
+ module = "tests.*"
174
+ disallow_untyped_defs = false
175
+ disallow_incomplete_defs = false
176
+
177
+ [tool.pytest.ini_options]
178
+ asyncio_mode = "auto"
@@ -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.2.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
 
@@ -149,8 +187,8 @@ class TONAPIRetryLimitError(TONAPIError):
149
187
 
150
188
  STREAMING_RECOVERABLE: t.Final[tuple[type[TONAPIError], ...]] = (
151
189
  TONAPIServerError,
152
- TONAPITooManyRequestsError,
153
190
  TONAPIStreamingError,
191
+ TONAPITooManyRequestsError,
154
192
  )
155
193
 
156
194
  TONAPI_STATUS_TO_EXCEPTION: t.Final[dict[int, type[TONAPIStatusError]]] = {
@@ -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)