pytonapi 2.1.0__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.1.0/pytonapi.egg-info → pytonapi-2.2.0}/PKG-INFO +23 -6
- {pytonapi-2.1.0 → pytonapi-2.2.0}/README.md +11 -5
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pyproject.toml +20 -3
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/__meta__.py +1 -1
- 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.1.0 → pytonapi-2.2.0/pytonapi.egg-info}/PKG-INFO +23 -6
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi.egg-info/SOURCES.txt +1 -1
- pytonapi-2.2.0/pytonapi.egg-info/requires.txt +13 -0
- pytonapi-2.1.0/pytonapi/streaming/__init__.py +0 -19
- pytonapi-2.1.0/pytonapi/streaming/client.py +0 -95
- pytonapi-2.1.0/pytonapi/streaming/models.py +0 -33
- pytonapi-2.1.0/pytonapi/streaming/sse.py +0 -254
- pytonapi-2.1.0/pytonapi/streaming/ws.py +0 -229
- pytonapi-2.1.0/pytonapi.egg-info/requires.txt +0 -2
- {pytonapi-2.1.0 → pytonapi-2.2.0}/LICENSE +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/__init__.py +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/cli.py +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/client.py +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/exceptions.py +1 -1
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/py.typed +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/rest/__init__.py +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/rest/client.py +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/rest/limiter.py +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/rest/mixin.py +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/rest/models/__init__.py +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/rest/models/_enums.py +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/rest/models/accounts.py +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/rest/models/blockchain.py +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/rest/models/connect.py +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/rest/models/dns.py +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/rest/models/emulation.py +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/rest/models/events.py +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/rest/models/extra_currency.py +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/rest/models/gasless.py +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/rest/models/jettons.py +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/rest/models/lite_server.py +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/rest/models/multisig.py +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/rest/models/nft.py +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/rest/models/purchases.py +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/rest/models/rates.py +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/rest/models/staking.py +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/rest/models/storage.py +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/rest/models/traces.py +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/rest/models/utilities.py +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/rest/models/wallet.py +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/rest/resources/__init__.py +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/rest/resources/_base.py +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/rest/resources/accounts.py +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/rest/resources/blockchain.py +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/rest/resources/connect.py +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/rest/resources/dns.py +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/rest/resources/emulation.py +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/rest/resources/events.py +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/rest/resources/extra_currency.py +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/rest/resources/gasless.py +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/rest/resources/jettons.py +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/rest/resources/lite_server.py +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/rest/resources/multisig.py +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/rest/resources/nft.py +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/rest/resources/purchases.py +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/rest/resources/rates.py +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/rest/resources/staking.py +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/rest/resources/storage.py +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/rest/resources/traces.py +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/rest/resources/utilities.py +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/rest/resources/wallet.py +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/rest/rotator.py +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/types.py +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/utils.py +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/webhook/__init__.py +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/webhook/client.py +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/webhook/dispatcher.py +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi/webhook/models.py +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi.egg-info/dependency_links.txt +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi.egg-info/entry_points.txt +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/pytonapi.egg-info/top_level.txt +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/setup.cfg +0 -0
- {pytonapi-2.1.0 → pytonapi-2.2.0}/tests/test_utils.py +0 -0
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pytonapi
|
|
3
|
-
Version: 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
|
|
@@ -25,6 +26,16 @@ Description-Content-Type: text/markdown
|
|
|
25
26
|
License-File: LICENSE
|
|
26
27
|
Requires-Dist: aiohttp>=3.9.0
|
|
27
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
|
|
@@ -80,9 +91,20 @@ pip install pytonapi
|
|
|
80
91
|
|
|
81
92
|
- [Get account info](https://github.com/nessshon/tonapi/blob/main/examples/get_account_info.py)
|
|
82
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)
|
|
83
95
|
- [Get NFTs by owner](https://github.com/nessshon/tonapi/blob/main/examples/get_nft_by_owner.py)
|
|
84
96
|
- [Get NFTs by collection](https://github.com/nessshon/tonapi/blob/main/examples/get_nft_by_collection.py)
|
|
85
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
|
+
|
|
86
108
|
**Streaming** (SSE & WebSocket)
|
|
87
109
|
|
|
88
110
|
- [SSE subscriptions](https://github.com/nessshon/tonapi/blob/main/examples/streaming_sse.py)
|
|
@@ -93,11 +115,6 @@ pip install pytonapi
|
|
|
93
115
|
- [FastAPI webhook server](https://github.com/nessshon/tonapi/blob/main/examples/webhook_fastapi.py)
|
|
94
116
|
- [aiohttp webhook server](https://github.com/nessshon/tonapi/blob/main/examples/webhook_aiohttp.py)
|
|
95
117
|
|
|
96
|
-
**Transfers** (requires [tonutils](https://github.com/nessshon/tonutils))
|
|
97
|
-
|
|
98
|
-
- [Transfer TON](https://github.com/nessshon/tonapi/blob/main/examples/transfer_ton.py)
|
|
99
|
-
- [Gasless transfer](https://github.com/nessshon/tonapi/blob/main/examples/transfer_gasless.py)
|
|
100
|
-
|
|
101
118
|
## License
|
|
102
119
|
|
|
103
120
|
This repository is distributed under the [MIT License](https://github.com/nessshon/tonapi/blob/main/LICENSE).
|
|
@@ -51,9 +51,20 @@ pip install pytonapi
|
|
|
51
51
|
|
|
52
52
|
- [Get account info](https://github.com/nessshon/tonapi/blob/main/examples/get_account_info.py)
|
|
53
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)
|
|
54
55
|
- [Get NFTs by owner](https://github.com/nessshon/tonapi/blob/main/examples/get_nft_by_owner.py)
|
|
55
56
|
- [Get NFTs by collection](https://github.com/nessshon/tonapi/blob/main/examples/get_nft_by_collection.py)
|
|
56
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
|
+
|
|
57
68
|
**Streaming** (SSE & WebSocket)
|
|
58
69
|
|
|
59
70
|
- [SSE subscriptions](https://github.com/nessshon/tonapi/blob/main/examples/streaming_sse.py)
|
|
@@ -64,11 +75,6 @@ pip install pytonapi
|
|
|
64
75
|
- [FastAPI webhook server](https://github.com/nessshon/tonapi/blob/main/examples/webhook_fastapi.py)
|
|
65
76
|
- [aiohttp webhook server](https://github.com/nessshon/tonapi/blob/main/examples/webhook_aiohttp.py)
|
|
66
77
|
|
|
67
|
-
**Transfers** (requires [tonutils](https://github.com/nessshon/tonutils))
|
|
68
|
-
|
|
69
|
-
- [Transfer TON](https://github.com/nessshon/tonapi/blob/main/examples/transfer_ton.py)
|
|
70
|
-
- [Gasless transfer](https://github.com/nessshon/tonapi/blob/main/examples/transfer_gasless.py)
|
|
71
|
-
|
|
72
78
|
## License
|
|
73
79
|
|
|
74
80
|
This repository is distributed under the [MIT License](https://github.com/nessshon/tonapi/blob/main/LICENSE).
|
|
@@ -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"]
|
|
@@ -158,4 +172,7 @@ enable_error_code = [
|
|
|
158
172
|
[[tool.mypy.overrides]]
|
|
159
173
|
module = "tests.*"
|
|
160
174
|
disallow_untyped_defs = false
|
|
161
|
-
disallow_incomplete_defs = false
|
|
175
|
+
disallow_incomplete_defs = false
|
|
176
|
+
|
|
177
|
+
[tool.pytest.ini_options]
|
|
178
|
+
asyncio_mode = "auto"
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from pytonapi.streaming.models import (
|
|
2
|
+
AccountState,
|
|
3
|
+
AccountStateNotification,
|
|
4
|
+
ActionsNotification,
|
|
5
|
+
ActionType,
|
|
6
|
+
ConnectionState,
|
|
7
|
+
EventType,
|
|
8
|
+
Finality,
|
|
9
|
+
JettonsNotification,
|
|
10
|
+
JettonWallet,
|
|
11
|
+
StreamNotification,
|
|
12
|
+
TraceInvalidatedNotification,
|
|
13
|
+
TraceNotification,
|
|
14
|
+
TransactionsNotification,
|
|
15
|
+
)
|
|
16
|
+
from pytonapi.streaming.sse import TonapiSSE
|
|
17
|
+
from pytonapi.streaming.ws import TonapiWebSocket
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"AccountState",
|
|
21
|
+
"AccountStateNotification",
|
|
22
|
+
"ActionType",
|
|
23
|
+
"ActionsNotification",
|
|
24
|
+
"ConnectionState",
|
|
25
|
+
"EventType",
|
|
26
|
+
"Finality",
|
|
27
|
+
"JettonWallet",
|
|
28
|
+
"JettonsNotification",
|
|
29
|
+
"StreamNotification",
|
|
30
|
+
"TonapiSSE",
|
|
31
|
+
"TonapiWebSocket",
|
|
32
|
+
"TraceInvalidatedNotification",
|
|
33
|
+
"TraceNotification",
|
|
34
|
+
"TransactionsNotification",
|
|
35
|
+
]
|
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import abc
|
|
4
|
+
import asyncio
|
|
5
|
+
import inspect
|
|
6
|
+
import typing as t
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
import aiohttp
|
|
10
|
+
|
|
11
|
+
from pytonapi.client import BaseClient
|
|
12
|
+
from pytonapi.exceptions import (
|
|
13
|
+
STREAMING_RECOVERABLE,
|
|
14
|
+
TONAPIConnectionLostError,
|
|
15
|
+
TONAPIError,
|
|
16
|
+
)
|
|
17
|
+
from pytonapi.streaming.models import (
|
|
18
|
+
NOTIFICATION_MODEL_MAP,
|
|
19
|
+
ActionsNotification,
|
|
20
|
+
ConnectionState,
|
|
21
|
+
EventType,
|
|
22
|
+
Finality,
|
|
23
|
+
StreamNotification,
|
|
24
|
+
_FinalityMixin,
|
|
25
|
+
)
|
|
26
|
+
from pytonapi.types import (
|
|
27
|
+
DEFAULT_RECONNECT_POLICY,
|
|
28
|
+
NETWORK_BASE_URLS,
|
|
29
|
+
Network,
|
|
30
|
+
ReconnectPolicy,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
_F = t.TypeVar("_F", bound=t.Callable[..., t.Any])
|
|
34
|
+
|
|
35
|
+
_FINALITY_ORDER: t.Final[dict[str, int]] = {
|
|
36
|
+
"pending": 0,
|
|
37
|
+
"confirmed": 1,
|
|
38
|
+
"finalized": 2,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
_SUBSCRIBABLE: t.Final[frozenset[str]] = frozenset(
|
|
42
|
+
{
|
|
43
|
+
EventType.TRANSACTIONS,
|
|
44
|
+
EventType.ACTIONS,
|
|
45
|
+
EventType.TRACE,
|
|
46
|
+
EventType.ACCOUNT_STATE_CHANGE,
|
|
47
|
+
EventType.JETTONS_CHANGE,
|
|
48
|
+
}
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(slots=True)
|
|
53
|
+
class _Handler:
|
|
54
|
+
callback: t.Callable[..., t.Any]
|
|
55
|
+
min_finality: Finality | None = None
|
|
56
|
+
action_types: list[str] | None = None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class StreamingBase(BaseClient, abc.ABC):
|
|
60
|
+
"""Abstract base for streaming transports (SSE / WebSocket)."""
|
|
61
|
+
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
api_key: str,
|
|
65
|
+
network: Network,
|
|
66
|
+
*,
|
|
67
|
+
base_url: str | None = None,
|
|
68
|
+
session: aiohttp.ClientSession | None = None,
|
|
69
|
+
headers: dict[str, str] | None = None,
|
|
70
|
+
reconnect_policy: ReconnectPolicy | None = None,
|
|
71
|
+
on_state_change: t.Callable[[ConnectionState], t.Any] | None = None,
|
|
72
|
+
) -> None:
|
|
73
|
+
super().__init__(
|
|
74
|
+
api_key=api_key,
|
|
75
|
+
base_url=base_url or NETWORK_BASE_URLS[network],
|
|
76
|
+
session=session,
|
|
77
|
+
headers=headers,
|
|
78
|
+
timeout=0.0,
|
|
79
|
+
retry_policy=None,
|
|
80
|
+
)
|
|
81
|
+
self._reconnect_policy = reconnect_policy or DEFAULT_RECONNECT_POLICY
|
|
82
|
+
self._on_state_change = on_state_change
|
|
83
|
+
self._handlers: dict[str, list[_Handler]] = {}
|
|
84
|
+
self._stop: asyncio.Event | None = None
|
|
85
|
+
self._state: ConnectionState = ConnectionState.IDLE
|
|
86
|
+
self._subscribed_event = asyncio.Event()
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def state(self) -> ConnectionState:
|
|
90
|
+
"""Current connection state."""
|
|
91
|
+
return self._state
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def is_subscribed(self) -> bool:
|
|
95
|
+
"""``True`` if currently subscribed and receiving notifications."""
|
|
96
|
+
return self._state == ConnectionState.SUBSCRIBED
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def is_connecting(self) -> bool:
|
|
100
|
+
"""``True`` if establishing initial connection."""
|
|
101
|
+
return self._state == ConnectionState.CONNECTING
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def is_reconnecting(self) -> bool:
|
|
105
|
+
"""``True`` if reconnecting after a connection loss."""
|
|
106
|
+
return self._state == ConnectionState.RECONNECTING
|
|
107
|
+
|
|
108
|
+
async def wait_subscribed(self, timeout: float | None = None) -> None:
|
|
109
|
+
"""Wait until the transport reaches ``SUBSCRIBED`` state.
|
|
110
|
+
|
|
111
|
+
:param timeout: Maximum seconds to wait, or ``None`` for no limit.
|
|
112
|
+
:raises asyncio.TimeoutError: If *timeout* is reached before subscribing.
|
|
113
|
+
"""
|
|
114
|
+
if self._state == ConnectionState.SUBSCRIBED:
|
|
115
|
+
return
|
|
116
|
+
await asyncio.wait_for(self._subscribed_event.wait(), timeout=timeout)
|
|
117
|
+
|
|
118
|
+
async def _set_state(self, new: ConnectionState) -> None:
|
|
119
|
+
if self._state == new:
|
|
120
|
+
return
|
|
121
|
+
if self._state == ConnectionState.SUBSCRIBED:
|
|
122
|
+
self._subscribed_event.clear()
|
|
123
|
+
self._state = new
|
|
124
|
+
if new == ConnectionState.SUBSCRIBED:
|
|
125
|
+
self._subscribed_event.set()
|
|
126
|
+
if self._on_state_change is not None:
|
|
127
|
+
result = self._on_state_change(new)
|
|
128
|
+
if inspect.isawaitable(result):
|
|
129
|
+
await result
|
|
130
|
+
|
|
131
|
+
@t.overload
|
|
132
|
+
def _register(
|
|
133
|
+
self,
|
|
134
|
+
notification_type: str,
|
|
135
|
+
callback: _F,
|
|
136
|
+
*,
|
|
137
|
+
min_finality: Finality | str | None = ...,
|
|
138
|
+
action_types: list[str] | None = ...,
|
|
139
|
+
) -> _F: ...
|
|
140
|
+
|
|
141
|
+
@t.overload
|
|
142
|
+
def _register(
|
|
143
|
+
self,
|
|
144
|
+
notification_type: str,
|
|
145
|
+
callback: None = ...,
|
|
146
|
+
*,
|
|
147
|
+
min_finality: Finality | str | None = ...,
|
|
148
|
+
action_types: list[str] | None = ...,
|
|
149
|
+
) -> t.Callable[[_F], _F]: ...
|
|
150
|
+
|
|
151
|
+
def _register(
|
|
152
|
+
self,
|
|
153
|
+
notification_type: str,
|
|
154
|
+
callback: _F | None = None,
|
|
155
|
+
*,
|
|
156
|
+
min_finality: Finality | str | None = None,
|
|
157
|
+
action_types: list[str] | None = None,
|
|
158
|
+
) -> _F | t.Callable[[_F], _F]:
|
|
159
|
+
def decorator(fn: _F) -> _F:
|
|
160
|
+
parsed = Finality(min_finality) if isinstance(min_finality, str) else min_finality
|
|
161
|
+
self._handlers.setdefault(notification_type, []).append(
|
|
162
|
+
_Handler(
|
|
163
|
+
callback=fn,
|
|
164
|
+
min_finality=parsed,
|
|
165
|
+
action_types=(
|
|
166
|
+
[a.value if hasattr(a, "value") else a for a in action_types] if action_types else None
|
|
167
|
+
),
|
|
168
|
+
),
|
|
169
|
+
)
|
|
170
|
+
return fn
|
|
171
|
+
|
|
172
|
+
return decorator(callback) if callback is not None else decorator
|
|
173
|
+
|
|
174
|
+
@t.overload
|
|
175
|
+
def on_transactions(self, callback: _F, *, min_finality: Finality | str | None = ...) -> _F: ...
|
|
176
|
+
|
|
177
|
+
@t.overload
|
|
178
|
+
def on_transactions(
|
|
179
|
+
self, callback: None = ..., *, min_finality: Finality | str | None = ...
|
|
180
|
+
) -> t.Callable[[_F], _F]: ...
|
|
181
|
+
|
|
182
|
+
def on_transactions(
|
|
183
|
+
self,
|
|
184
|
+
callback: _F | None = None,
|
|
185
|
+
*,
|
|
186
|
+
min_finality: Finality | str | None = None,
|
|
187
|
+
) -> _F | t.Callable[[_F], _F]:
|
|
188
|
+
"""Register a handler for ``transactions`` notifications."""
|
|
189
|
+
return self._register(EventType.TRANSACTIONS, callback, min_finality=min_finality)
|
|
190
|
+
|
|
191
|
+
@t.overload
|
|
192
|
+
def on_actions(
|
|
193
|
+
self, callback: _F, *, min_finality: Finality | str | None = ..., action_types: list[str] | None = ...
|
|
194
|
+
) -> _F: ...
|
|
195
|
+
|
|
196
|
+
@t.overload
|
|
197
|
+
def on_actions(
|
|
198
|
+
self, callback: None = ..., *, min_finality: Finality | str | None = ..., action_types: list[str] | None = ...
|
|
199
|
+
) -> t.Callable[[_F], _F]: ...
|
|
200
|
+
|
|
201
|
+
def on_actions(
|
|
202
|
+
self,
|
|
203
|
+
callback: _F | None = None,
|
|
204
|
+
*,
|
|
205
|
+
min_finality: Finality | str | None = None,
|
|
206
|
+
action_types: list[str] | None = None,
|
|
207
|
+
) -> _F | t.Callable[[_F], _F]:
|
|
208
|
+
"""Register a handler for ``actions`` notifications."""
|
|
209
|
+
return self._register(
|
|
210
|
+
EventType.ACTIONS,
|
|
211
|
+
callback,
|
|
212
|
+
min_finality=min_finality,
|
|
213
|
+
action_types=action_types,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
@t.overload
|
|
217
|
+
def on_traces(self, callback: _F, *, min_finality: Finality | str | None = ...) -> _F: ...
|
|
218
|
+
|
|
219
|
+
@t.overload
|
|
220
|
+
def on_traces(self, callback: None = ..., *, min_finality: Finality | str | None = ...) -> t.Callable[[_F], _F]: ...
|
|
221
|
+
|
|
222
|
+
def on_traces(
|
|
223
|
+
self,
|
|
224
|
+
callback: _F | None = None,
|
|
225
|
+
*,
|
|
226
|
+
min_finality: Finality | str | None = None,
|
|
227
|
+
) -> _F | t.Callable[[_F], _F]:
|
|
228
|
+
"""Register a handler for ``trace`` notifications."""
|
|
229
|
+
return self._register(EventType.TRACE, callback, min_finality=min_finality)
|
|
230
|
+
|
|
231
|
+
@t.overload
|
|
232
|
+
def on_account_states(self, callback: _F, *, min_finality: Finality | str | None = ...) -> _F: ...
|
|
233
|
+
|
|
234
|
+
@t.overload
|
|
235
|
+
def on_account_states(
|
|
236
|
+
self, callback: None = ..., *, min_finality: Finality | str | None = ...
|
|
237
|
+
) -> t.Callable[[_F], _F]: ...
|
|
238
|
+
|
|
239
|
+
def on_account_states(
|
|
240
|
+
self,
|
|
241
|
+
callback: _F | None = None,
|
|
242
|
+
*,
|
|
243
|
+
min_finality: Finality | str | None = None,
|
|
244
|
+
) -> _F | t.Callable[[_F], _F]:
|
|
245
|
+
"""Register a handler for ``account_state_change`` notifications."""
|
|
246
|
+
if min_finality is not None:
|
|
247
|
+
raw = min_finality.value if isinstance(min_finality, Finality) else min_finality
|
|
248
|
+
if raw == Finality.PENDING:
|
|
249
|
+
raise ValueError(
|
|
250
|
+
"account_state_change events are only emitted at confirmed and finalized finality levels"
|
|
251
|
+
)
|
|
252
|
+
return self._register(EventType.ACCOUNT_STATE_CHANGE, callback, min_finality=min_finality)
|
|
253
|
+
|
|
254
|
+
@t.overload
|
|
255
|
+
def on_jettons(self, callback: _F, *, min_finality: Finality | str | None = ...) -> _F: ...
|
|
256
|
+
|
|
257
|
+
@t.overload
|
|
258
|
+
def on_jettons(
|
|
259
|
+
self, callback: None = ..., *, min_finality: Finality | str | None = ...
|
|
260
|
+
) -> t.Callable[[_F], _F]: ...
|
|
261
|
+
|
|
262
|
+
def on_jettons(
|
|
263
|
+
self,
|
|
264
|
+
callback: _F | None = None,
|
|
265
|
+
*,
|
|
266
|
+
min_finality: Finality | str | None = None,
|
|
267
|
+
) -> _F | t.Callable[[_F], _F]:
|
|
268
|
+
"""Register a handler for ``jettons_change`` notifications."""
|
|
269
|
+
if min_finality is not None:
|
|
270
|
+
raw = min_finality.value if isinstance(min_finality, Finality) else min_finality
|
|
271
|
+
if raw == Finality.PENDING:
|
|
272
|
+
raise ValueError("jettons_change events are only emitted at confirmed and finalized finality levels")
|
|
273
|
+
return self._register(EventType.JETTONS_CHANGE, callback, min_finality=min_finality)
|
|
274
|
+
|
|
275
|
+
@t.overload
|
|
276
|
+
def on_trace_invalidated(self, callback: _F) -> _F: ...
|
|
277
|
+
|
|
278
|
+
@t.overload
|
|
279
|
+
def on_trace_invalidated(self, callback: None = ...) -> t.Callable[[_F], _F]: ...
|
|
280
|
+
|
|
281
|
+
def on_trace_invalidated(
|
|
282
|
+
self,
|
|
283
|
+
callback: _F | None = None,
|
|
284
|
+
) -> _F | t.Callable[[_F], _F]:
|
|
285
|
+
"""Register a handler for ``trace_invalidated`` notifications."""
|
|
286
|
+
return self._register(EventType.TRACE_INVALIDATED, callback)
|
|
287
|
+
|
|
288
|
+
def _build_subscription(
|
|
289
|
+
self,
|
|
290
|
+
addresses: list[str] | None,
|
|
291
|
+
trace_external_hash_norms: list[str] | None,
|
|
292
|
+
) -> dict[str, t.Any]:
|
|
293
|
+
types = sorted(k for k in self._handlers if k in _SUBSCRIBABLE)
|
|
294
|
+
if not types:
|
|
295
|
+
raise ValueError("No subscribable notification handlers registered")
|
|
296
|
+
|
|
297
|
+
fin = Finality.FINALIZED
|
|
298
|
+
for handlers in self._handlers.values():
|
|
299
|
+
for h in handlers:
|
|
300
|
+
if h.min_finality is not None and _FINALITY_ORDER[h.min_finality] < _FINALITY_ORDER[fin]:
|
|
301
|
+
fin = h.min_finality
|
|
302
|
+
|
|
303
|
+
action_types: list[str] | None = None
|
|
304
|
+
ah = self._handlers.get(EventType.ACTIONS, [])
|
|
305
|
+
if ah and all(h.action_types is not None for h in ah):
|
|
306
|
+
merged = {at for h in ah for at in (h.action_types or [])}
|
|
307
|
+
action_types = sorted(merged) if merged else None
|
|
308
|
+
|
|
309
|
+
non_trace = [et for et in types if et != EventType.TRACE]
|
|
310
|
+
if non_trace and not addresses:
|
|
311
|
+
raise ValueError(
|
|
312
|
+
f"addresses are required for event types: {non_trace}. Only 'trace' subscriptions can omit addresses.",
|
|
313
|
+
)
|
|
314
|
+
if EventType.TRACE in types and not trace_external_hash_norms:
|
|
315
|
+
raise ValueError("trace_external_hash_norms are required for trace subscriptions")
|
|
316
|
+
|
|
317
|
+
result: dict[str, t.Any] = {"types": types, "min_finality": fin}
|
|
318
|
+
if addresses:
|
|
319
|
+
result["addresses"] = addresses
|
|
320
|
+
if trace_external_hash_norms:
|
|
321
|
+
result["trace_external_hash_norms"] = trace_external_hash_norms
|
|
322
|
+
if action_types:
|
|
323
|
+
result["action_types"] = action_types
|
|
324
|
+
return result
|
|
325
|
+
|
|
326
|
+
async def _dispatch(self, notification: StreamNotification) -> None:
|
|
327
|
+
for handler in self._handlers.get(notification.type, []):
|
|
328
|
+
if handler.min_finality is not None and isinstance(notification, _FinalityMixin):
|
|
329
|
+
n_order = _FINALITY_ORDER.get(getattr(notification, "finality", ""), -1)
|
|
330
|
+
if n_order < 0:
|
|
331
|
+
continue
|
|
332
|
+
h_order = _FINALITY_ORDER.get(handler.min_finality.value, 0)
|
|
333
|
+
if n_order < h_order:
|
|
334
|
+
continue
|
|
335
|
+
|
|
336
|
+
if isinstance(notification, ActionsNotification) and handler.action_types is not None:
|
|
337
|
+
wanted = set(handler.action_types)
|
|
338
|
+
if not any(a.get("type") in wanted for a in notification.actions):
|
|
339
|
+
continue
|
|
340
|
+
|
|
341
|
+
result = handler.callback(notification)
|
|
342
|
+
if inspect.isawaitable(result):
|
|
343
|
+
await result
|
|
344
|
+
|
|
345
|
+
async def subscribe(
|
|
346
|
+
self,
|
|
347
|
+
*,
|
|
348
|
+
addresses: list[str] | None = None,
|
|
349
|
+
trace_external_hash_norms: list[str] | None = None,
|
|
350
|
+
types: list[str] | None = None,
|
|
351
|
+
min_finality: Finality | str = Finality.FINALIZED,
|
|
352
|
+
include_address_book: bool = False,
|
|
353
|
+
include_metadata: bool = False,
|
|
354
|
+
action_types: list[str] | None = None,
|
|
355
|
+
supported_action_types: list[str] | None = None,
|
|
356
|
+
stop: asyncio.Event | None = None,
|
|
357
|
+
) -> t.AsyncGenerator[StreamNotification, None]:
|
|
358
|
+
"""Low-level subscription generator.
|
|
359
|
+
|
|
360
|
+
Opens one connection, yields typed notifications. Reconnects automatically.
|
|
361
|
+
For high-level usage prefer ``on_*()`` decorators + ``start()``.
|
|
362
|
+
"""
|
|
363
|
+
non_trace = [et for et in (types or []) if et != "trace"]
|
|
364
|
+
if non_trace and not addresses:
|
|
365
|
+
raise ValueError(
|
|
366
|
+
f"addresses are required for event types: {non_trace}. Only 'trace' subscriptions can omit addresses.",
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
params: dict[str, t.Any] = {
|
|
370
|
+
"min_finality": min_finality.value if isinstance(min_finality, Finality) else min_finality,
|
|
371
|
+
"include_address_book": include_address_book,
|
|
372
|
+
"include_metadata": include_metadata,
|
|
373
|
+
}
|
|
374
|
+
if addresses:
|
|
375
|
+
params["addresses"] = addresses
|
|
376
|
+
if trace_external_hash_norms:
|
|
377
|
+
params["trace_external_hash_norms"] = trace_external_hash_norms
|
|
378
|
+
if types:
|
|
379
|
+
params["types"] = [et.value if hasattr(et, "value") else et for et in types]
|
|
380
|
+
if action_types:
|
|
381
|
+
params["action_types"] = [a.value if hasattr(a, "value") else a for a in action_types]
|
|
382
|
+
if supported_action_types:
|
|
383
|
+
params["supported_action_types"] = supported_action_types
|
|
384
|
+
|
|
385
|
+
async for data in self._stream_with_reconnect(params, stop):
|
|
386
|
+
model = NOTIFICATION_MODEL_MAP.get(data.get("type", ""))
|
|
387
|
+
if model is not None:
|
|
388
|
+
yield model.model_validate(data) # type: ignore[misc]
|
|
389
|
+
|
|
390
|
+
async def _stream_with_reconnect(
|
|
391
|
+
self,
|
|
392
|
+
params: dict[str, t.Any],
|
|
393
|
+
stop: asyncio.Event | None = None,
|
|
394
|
+
) -> t.AsyncGenerator[dict[str, t.Any], None]:
|
|
395
|
+
attempt = 0
|
|
396
|
+
await self._set_state(ConnectionState.CONNECTING)
|
|
397
|
+
|
|
398
|
+
while not (stop and stop.is_set()):
|
|
399
|
+
try:
|
|
400
|
+
async for data in self._open_stream(params, stop):
|
|
401
|
+
attempt = 0
|
|
402
|
+
yield data
|
|
403
|
+
except TONAPIError as exc:
|
|
404
|
+
if not isinstance(exc, STREAMING_RECOVERABLE):
|
|
405
|
+
await self._set_state(ConnectionState.IDLE)
|
|
406
|
+
raise
|
|
407
|
+
except (aiohttp.ClientError, OSError):
|
|
408
|
+
pass
|
|
409
|
+
|
|
410
|
+
if stop and stop.is_set():
|
|
411
|
+
await self._set_state(ConnectionState.IDLE)
|
|
412
|
+
return
|
|
413
|
+
|
|
414
|
+
attempt += 1
|
|
415
|
+
if self._reconnect_policy.max_reconnects != -1 and attempt > self._reconnect_policy.max_reconnects:
|
|
416
|
+
await self._set_state(ConnectionState.IDLE)
|
|
417
|
+
raise TONAPIConnectionLostError(attempts=attempt)
|
|
418
|
+
|
|
419
|
+
await self._set_state(ConnectionState.RECONNECTING)
|
|
420
|
+
await asyncio.sleep(self._reconnect_policy.delay_for_attempt(attempt - 1))
|
|
421
|
+
|
|
422
|
+
@abc.abstractmethod
|
|
423
|
+
async def _open_stream(
|
|
424
|
+
self,
|
|
425
|
+
params: dict[str, t.Any],
|
|
426
|
+
stop: asyncio.Event | None = None,
|
|
427
|
+
) -> t.AsyncGenerator[dict[str, t.Any], None]:
|
|
428
|
+
"""Open a transport-specific streaming connection and yield raw notification dicts."""
|
|
429
|
+
raise NotImplementedError
|
|
430
|
+
yield # type: ignore[unreachable]
|
|
431
|
+
|
|
432
|
+
async def start(
|
|
433
|
+
self,
|
|
434
|
+
addresses: list[str] | None = None,
|
|
435
|
+
*,
|
|
436
|
+
trace_external_hash_norms: list[str] | None = None,
|
|
437
|
+
include_address_book: bool = False,
|
|
438
|
+
include_metadata: bool = False,
|
|
439
|
+
supported_action_types: list[str] | None = None,
|
|
440
|
+
) -> None:
|
|
441
|
+
"""Start the streaming transport: create session, subscribe, and dispatch.
|
|
442
|
+
|
|
443
|
+
Blocks until ``stop()`` is called or a fatal error occurs.
|
|
444
|
+
Creates an internal aiohttp session on entry and closes it on exit
|
|
445
|
+
(unless an external session was provided via the constructor).
|
|
446
|
+
|
|
447
|
+
:param addresses: Wallet/contract addresses to monitor, in any form.
|
|
448
|
+
:param trace_external_hash_norms: Trace hashes for trace subscriptions.
|
|
449
|
+
:param include_address_book: Include DNS-resolved names in notifications.
|
|
450
|
+
:param include_metadata: Include token metadata in notifications.
|
|
451
|
+
:param supported_action_types: Advertise client-supported action types.
|
|
452
|
+
"""
|
|
453
|
+
if self._stop is not None:
|
|
454
|
+
raise RuntimeError("start() is already active")
|
|
455
|
+
|
|
456
|
+
sub = self._build_subscription(addresses, trace_external_hash_norms)
|
|
457
|
+
self._stop = asyncio.Event()
|
|
458
|
+
await self.create_session()
|
|
459
|
+
try:
|
|
460
|
+
async for notification in self.subscribe(
|
|
461
|
+
addresses=sub.get("addresses"),
|
|
462
|
+
trace_external_hash_norms=sub.get("trace_external_hash_norms"),
|
|
463
|
+
types=sub.get("types"),
|
|
464
|
+
min_finality=sub["min_finality"],
|
|
465
|
+
action_types=sub.get("action_types"),
|
|
466
|
+
include_address_book=include_address_book,
|
|
467
|
+
include_metadata=include_metadata,
|
|
468
|
+
supported_action_types=supported_action_types,
|
|
469
|
+
stop=self._stop,
|
|
470
|
+
):
|
|
471
|
+
await self._dispatch(notification)
|
|
472
|
+
finally:
|
|
473
|
+
self._stop = None
|
|
474
|
+
await self.close_session()
|
|
475
|
+
|
|
476
|
+
async def stop(self) -> None:
|
|
477
|
+
"""Stop the streaming transport and release resources.
|
|
478
|
+
|
|
479
|
+
Signals the dispatch loop to exit, closes the aiohttp session
|
|
480
|
+
(unless externally provided), and resets the connection state.
|
|
481
|
+
Safe to call multiple times.
|
|
482
|
+
"""
|
|
483
|
+
if self._stop is not None:
|
|
484
|
+
self._stop.set()
|
|
485
|
+
await self.close_session()
|
|
486
|
+
await self._set_state(ConnectionState.IDLE)
|
|
487
|
+
|
|
488
|
+
async def __aenter__(self) -> t.NoReturn:
|
|
489
|
+
raise TypeError("Streaming transports do not support 'async with'. Use start() and stop() instead.")
|
|
490
|
+
|
|
491
|
+
async def __aexit__(self, *args: t.Any) -> None:
|
|
492
|
+
pass
|