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.
- {pytonapi-2.0.2/pytonapi.egg-info → pytonapi-2.2.0}/PKG-INFO +38 -11
- {pytonapi-2.0.2 → pytonapi-2.2.0}/README.md +25 -9
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pyproject.toml +26 -4
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/__meta__.py +1 -1
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/client.py +37 -16
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/exceptions.py +43 -1
- pytonapi-2.2.0/pytonapi/py.typed +0 -0
- pytonapi-2.2.0/pytonapi/rest/client.py +156 -0
- pytonapi-2.2.0/pytonapi/rest/mixin.py +54 -0
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/models/__init__.py +12 -12
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/models/accounts.py +167 -147
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/models/blockchain.py +11 -17
- pytonapi-2.2.0/pytonapi/rest/models/emulation.py +43 -0
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/models/gasless.py +1 -1
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/models/multisig.py +1 -16
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/models/nft.py +2 -12
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/models/purchases.py +2 -8
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/models/wallet.py +0 -13
- pytonapi-2.2.0/pytonapi/rest/rotator.py +49 -0
- pytonapi-2.2.0/pytonapi/streaming/__init__.py +35 -0
- pytonapi-2.2.0/pytonapi/streaming/base.py +492 -0
- pytonapi-2.2.0/pytonapi/streaming/models.py +196 -0
- pytonapi-2.2.0/pytonapi/streaming/sse.py +159 -0
- pytonapi-2.2.0/pytonapi/streaming/ws.py +282 -0
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/types.py +15 -0
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/webhook/client.py +3 -5
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/webhook/dispatcher.py +5 -14
- {pytonapi-2.0.2 → pytonapi-2.2.0/pytonapi.egg-info}/PKG-INFO +38 -11
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi.egg-info/SOURCES.txt +2 -1
- pytonapi-2.2.0/pytonapi.egg-info/requires.txt +13 -0
- {pytonapi-2.0.2 → pytonapi-2.2.0}/tests/test_utils.py +0 -3
- pytonapi-2.0.2/pytonapi/__init__.py +0 -1
- pytonapi-2.0.2/pytonapi/rest/client.py +0 -97
- pytonapi-2.0.2/pytonapi/rest/mixin.py +0 -130
- pytonapi-2.0.2/pytonapi/rest/models/emulation.py +0 -21
- pytonapi-2.0.2/pytonapi/streaming/__init__.py +0 -19
- pytonapi-2.0.2/pytonapi/streaming/client.py +0 -94
- pytonapi-2.0.2/pytonapi/streaming/models.py +0 -34
- pytonapi-2.0.2/pytonapi/streaming/sse.py +0 -247
- pytonapi-2.0.2/pytonapi/streaming/ws.py +0 -232
- pytonapi-2.0.2/pytonapi.egg-info/requires.txt +0 -2
- {pytonapi-2.0.2 → pytonapi-2.2.0}/LICENSE +0 -0
- pytonapi-2.0.2/pytonapi/py.typed → pytonapi-2.2.0/pytonapi/__init__.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/cli.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/__init__.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/limiter.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/models/_enums.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/models/connect.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/models/dns.py +6 -6
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/models/events.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/models/extra_currency.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/models/jettons.py +5 -5
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/models/lite_server.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/models/rates.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/models/staking.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/models/storage.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/models/traces.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/models/utilities.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/resources/__init__.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/resources/_base.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/resources/accounts.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/resources/blockchain.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/resources/connect.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/resources/dns.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/resources/emulation.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/resources/events.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/resources/extra_currency.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/resources/gasless.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/resources/jettons.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/resources/lite_server.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/resources/multisig.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/resources/nft.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/resources/purchases.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/resources/rates.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/resources/staking.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/resources/storage.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/resources/traces.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/resources/utilities.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/rest/resources/wallet.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/utils.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/webhook/__init__.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi/webhook/models.py +0 -0
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi.egg-info/dependency_links.txt +0 -0
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi.egg-info/entry_points.txt +0 -0
- {pytonapi-2.0.2 → pytonapi-2.2.0}/pytonapi.egg-info/top_level.txt +0 -0
- {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
|
|
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.
|
|
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
|
[](https://github.com/nessshon/tonapi/blob/main/LICENSE)
|
|
36
47
|
[](https://tonviewer.com/UQCZq3_Vd21-4y4m7Wc-ej9NFOhh_qvdfAkAYAOHoQ__Ness)
|
|
37
48
|
|
|
38
|
-

|
|
39
50
|
|
|
40
51
|

|
|
41
52
|

|
|
@@ -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/)
|
|
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
|
[](https://github.com/nessshon/tonapi/blob/main/LICENSE)
|
|
7
7
|
[](https://tonviewer.com/UQCZq3_Vd21-4y4m7Wc-ej9NFOhh_qvdfAkAYAOHoQ__Ness)
|
|
8
8
|
|
|
9
|
-

|
|
10
10
|
|
|
11
11
|

|
|
12
12
|

|
|
@@ -14,9 +14,8 @@
|
|
|
14
14
|
|
|
15
15
|
### Python SDK for [TON API](https://tonapi.io)
|
|
16
16
|
|
|
17
|
-
Access TON blockchain data via REST API, real-time streaming, and webhooks.
|
|
18
|
-
API key required — obtain at [tonconsole.com](https://tonconsole.com/)
|
|
19
|
-
at [docs.tonconsole.com](https://docs.tonconsole.com/).
|
|
17
|
+
Access TON blockchain data via REST API, real-time streaming, and webhooks.
|
|
18
|
+
API key optional for REST, required for streaming and webhooks — obtain at [tonconsole.com](https://tonconsole.com/).
|
|
20
19
|
|
|
21
20
|
> For creating wallets, transferring TON, jettons, etc., use [tonutils](https://github.com/nessshon/tonutils).
|
|
22
21
|
|
|
@@ -35,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.
|
|
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"
|
|
@@ -4,13 +4,18 @@ import asyncio
|
|
|
4
4
|
import json
|
|
5
5
|
import typing as t
|
|
6
6
|
|
|
7
|
+
if t.TYPE_CHECKING:
|
|
8
|
+
import types
|
|
9
|
+
|
|
7
10
|
import aiohttp
|
|
11
|
+
from pydantic import TypeAdapter, ValidationError
|
|
8
12
|
|
|
9
13
|
from pytonapi.exceptions import (
|
|
10
14
|
TONAPIConnectionError,
|
|
11
15
|
TONAPIError,
|
|
12
16
|
TONAPIRetryLimitError,
|
|
13
17
|
TONAPISessionNotCreatedError,
|
|
18
|
+
TONAPIValidationError,
|
|
14
19
|
raise_for_status,
|
|
15
20
|
)
|
|
16
21
|
from pytonapi.types import (
|
|
@@ -21,6 +26,18 @@ from pytonapi.types import (
|
|
|
21
26
|
__all__ = ["BaseClient"]
|
|
22
27
|
|
|
23
28
|
T = t.TypeVar("T")
|
|
29
|
+
_Self = t.TypeVar("_Self", bound="BaseClient")
|
|
30
|
+
|
|
31
|
+
_adapter_cache: dict[t.Any, TypeAdapter[t.Any]] = {}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _get_adapter(model: t.Any) -> TypeAdapter[t.Any]:
|
|
35
|
+
"""Return a cached ``TypeAdapter`` for the given model."""
|
|
36
|
+
adapter = _adapter_cache.get(model)
|
|
37
|
+
if adapter is None:
|
|
38
|
+
adapter = TypeAdapter(model)
|
|
39
|
+
_adapter_cache[model] = adapter
|
|
40
|
+
return adapter
|
|
24
41
|
|
|
25
42
|
|
|
26
43
|
class BaseClient:
|
|
@@ -28,8 +45,8 @@ class BaseClient:
|
|
|
28
45
|
|
|
29
46
|
def __init__(
|
|
30
47
|
self,
|
|
31
|
-
api_key: str,
|
|
32
|
-
base_url: str,
|
|
48
|
+
api_key: str = "",
|
|
49
|
+
base_url: str = "",
|
|
33
50
|
*,
|
|
34
51
|
timeout: float = 10.0,
|
|
35
52
|
session: aiohttp.ClientSession | None = None,
|
|
@@ -39,7 +56,9 @@ class BaseClient:
|
|
|
39
56
|
) -> None:
|
|
40
57
|
"""Initialize the base HTTP client.
|
|
41
58
|
|
|
42
|
-
:param api_key: TONAPI key.
|
|
59
|
+
:param api_key: TONAPI key. Optional for REST — without a key
|
|
60
|
+
requests are throttled to ~0.24 RPS (1 per 4 seconds).
|
|
61
|
+
Get one at https://tonconsole.com/.
|
|
43
62
|
:param base_url: Base URL for all requests.
|
|
44
63
|
:param timeout: Request timeout in seconds.
|
|
45
64
|
:param session: Optional external ``aiohttp.ClientSession``.
|
|
@@ -60,7 +79,7 @@ class BaseClient:
|
|
|
60
79
|
self._is_external_session = session is not None
|
|
61
80
|
self._retry_policy = retry_policy
|
|
62
81
|
|
|
63
|
-
async def create_session(self) ->
|
|
82
|
+
async def create_session(self: _Self) -> _Self:
|
|
64
83
|
"""Create an ``aiohttp.ClientSession`` for making requests.
|
|
65
84
|
|
|
66
85
|
If an external session was provided via the ``session`` parameter,
|
|
@@ -85,9 +104,10 @@ class BaseClient:
|
|
|
85
104
|
"""
|
|
86
105
|
if self._session and not self._session.closed and not self._is_external_session:
|
|
87
106
|
await self._session.close()
|
|
107
|
+
await asyncio.sleep(0)
|
|
88
108
|
self._session = None
|
|
89
109
|
|
|
90
|
-
async def __aenter__(self) ->
|
|
110
|
+
async def __aenter__(self: _Self) -> _Self:
|
|
91
111
|
"""Enter the async context manager."""
|
|
92
112
|
await self.create_session()
|
|
93
113
|
return self
|
|
@@ -96,7 +116,7 @@ class BaseClient:
|
|
|
96
116
|
self,
|
|
97
117
|
exc_type: type[BaseException] | None,
|
|
98
118
|
exc_val: BaseException | None,
|
|
99
|
-
exc_tb:
|
|
119
|
+
exc_tb: types.TracebackType | None,
|
|
100
120
|
) -> None:
|
|
101
121
|
"""Exit the async context manager."""
|
|
102
122
|
await self.close_session()
|
|
@@ -106,10 +126,9 @@ class BaseClient:
|
|
|
106
126
|
|
|
107
127
|
:return: Merged headers dict.
|
|
108
128
|
"""
|
|
109
|
-
base = {
|
|
110
|
-
|
|
111
|
-
"
|
|
112
|
-
}
|
|
129
|
+
base: dict[str, str] = {"Accept": "application/json"}
|
|
130
|
+
if self._api_key:
|
|
131
|
+
base["Authorization"] = f"Bearer {self._api_key}"
|
|
113
132
|
base.update(self._headers)
|
|
114
133
|
return base
|
|
115
134
|
|
|
@@ -179,11 +198,7 @@ class BaseClient:
|
|
|
179
198
|
"""
|
|
180
199
|
url = f"{self._base_url}{path}"
|
|
181
200
|
if params:
|
|
182
|
-
params = {
|
|
183
|
-
k: str(v).lower() if isinstance(v, bool) else v
|
|
184
|
-
for k, v in params.items()
|
|
185
|
-
if v is not None
|
|
186
|
-
}
|
|
201
|
+
params = {k: str(v).lower() if isinstance(v, bool) else v for k, v in params.items() if v is not None}
|
|
187
202
|
if headers:
|
|
188
203
|
headers = {k: str(v) for k, v in headers.items() if v is not None}
|
|
189
204
|
|
|
@@ -283,4 +298,10 @@ class BaseClient:
|
|
|
283
298
|
data, _ = self._parse_body(text)
|
|
284
299
|
if data is None:
|
|
285
300
|
raise TONAPIError(f"Expected JSON response, got: {text}")
|
|
286
|
-
|
|
301
|
+
try:
|
|
302
|
+
return _get_adapter(response_model).validate_python(data)
|
|
303
|
+
except ValidationError as exc:
|
|
304
|
+
raise TONAPIValidationError(
|
|
305
|
+
model=response_model,
|
|
306
|
+
errors=exc.errors(),
|
|
307
|
+
) from exc
|
|
@@ -5,11 +5,14 @@ __all__ = [
|
|
|
5
5
|
"TONAPI_STATUS_TO_EXCEPTION",
|
|
6
6
|
"TONAPIBadRequestError",
|
|
7
7
|
"TONAPIClientError",
|
|
8
|
+
"TONAPIConflictError",
|
|
8
9
|
"TONAPIConnectionError",
|
|
9
10
|
"TONAPIConnectionLostError",
|
|
10
11
|
"TONAPIError",
|
|
11
12
|
"TONAPIForbiddenError",
|
|
13
|
+
"TONAPIGatewayTimeoutError",
|
|
12
14
|
"TONAPIInternalServerError",
|
|
15
|
+
"TONAPIMethodNotAllowedError",
|
|
13
16
|
"TONAPINotFoundError",
|
|
14
17
|
"TONAPINotImplementedError",
|
|
15
18
|
"TONAPIRetryLimitError",
|
|
@@ -19,6 +22,8 @@ __all__ = [
|
|
|
19
22
|
"TONAPIStreamingError",
|
|
20
23
|
"TONAPITooManyRequestsError",
|
|
21
24
|
"TONAPIUnauthorizedError",
|
|
25
|
+
"TONAPIUnprocessableError",
|
|
26
|
+
"TONAPIValidationError",
|
|
22
27
|
"raise_for_status",
|
|
23
28
|
]
|
|
24
29
|
|
|
@@ -81,6 +86,18 @@ class TONAPINotFoundError(TONAPIClientError):
|
|
|
81
86
|
"""HTTP 404 Not Found."""
|
|
82
87
|
|
|
83
88
|
|
|
89
|
+
class TONAPIMethodNotAllowedError(TONAPIClientError):
|
|
90
|
+
"""HTTP 405 Method Not Allowed."""
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class TONAPIConflictError(TONAPIClientError):
|
|
94
|
+
"""HTTP 409 Conflict."""
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class TONAPIUnprocessableError(TONAPIClientError):
|
|
98
|
+
"""HTTP 422 Unprocessable Entity."""
|
|
99
|
+
|
|
100
|
+
|
|
84
101
|
class TONAPITooManyRequestsError(TONAPIClientError):
|
|
85
102
|
"""HTTP 429 Too Many Requests."""
|
|
86
103
|
|
|
@@ -95,6 +112,27 @@ class TONAPINotImplementedError(TONAPIServerError):
|
|
|
95
112
|
"""HTTP 501 Not Implemented."""
|
|
96
113
|
|
|
97
114
|
|
|
115
|
+
class TONAPIGatewayTimeoutError(TONAPIServerError):
|
|
116
|
+
"""HTTP 504 Gateway Timeout."""
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class TONAPIValidationError(TONAPIError):
|
|
120
|
+
"""Response body did not match the expected Pydantic model."""
|
|
121
|
+
|
|
122
|
+
model: type
|
|
123
|
+
errors: list[t.Any]
|
|
124
|
+
|
|
125
|
+
def __init__(self, *, model: type, errors: list[t.Any]) -> None:
|
|
126
|
+
self.model = model
|
|
127
|
+
self.errors = errors
|
|
128
|
+
field_hints = ", ".join(f"{e.get('loc', '?')}: {e.get('msg', '')}" for e in errors[:3])
|
|
129
|
+
if len(errors) > 3:
|
|
130
|
+
field_hints += f" ... (+{len(errors) - 3} more)"
|
|
131
|
+
super().__init__(
|
|
132
|
+
f"Response validation failed for {model.__name__}: {field_hints}",
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
98
136
|
class TONAPIStreamingError(TONAPIError):
|
|
99
137
|
"""Streaming transport error."""
|
|
100
138
|
|
|
@@ -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)
|