tonutils 2.0.1b2__py3-none-any.whl → 2.0.1b4__py3-none-any.whl

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 (91) hide show
  1. tonutils/__init__.py +0 -2
  2. tonutils/__meta__.py +1 -1
  3. tonutils/cli.py +111 -0
  4. tonutils/clients/__init__.py +7 -11
  5. tonutils/clients/adnl/__init__.py +7 -3
  6. tonutils/clients/adnl/balancer.py +362 -168
  7. tonutils/clients/adnl/client.py +203 -67
  8. tonutils/clients/adnl/provider/config.py +24 -25
  9. tonutils/clients/adnl/provider/models.py +4 -0
  10. tonutils/clients/adnl/provider/provider.py +203 -160
  11. tonutils/clients/adnl/provider/transport.py +44 -33
  12. tonutils/clients/adnl/provider/workers/base.py +0 -2
  13. tonutils/clients/adnl/provider/workers/pinger.py +1 -1
  14. tonutils/clients/adnl/provider/workers/reader.py +3 -2
  15. tonutils/clients/adnl/{provider/builder.py → utils.py} +62 -2
  16. tonutils/clients/http/__init__.py +11 -8
  17. tonutils/clients/http/balancer.py +75 -63
  18. tonutils/clients/http/clients/__init__.py +13 -0
  19. tonutils/clients/http/clients/chainstack.py +48 -0
  20. tonutils/clients/http/clients/quicknode.py +47 -0
  21. tonutils/clients/http/clients/tatum.py +56 -0
  22. tonutils/clients/http/{tonapi/client.py → clients/tonapi.py} +31 -31
  23. tonutils/clients/http/{toncenter/client.py → clients/toncenter.py} +59 -48
  24. tonutils/clients/http/providers/__init__.py +4 -0
  25. tonutils/clients/http/providers/base.py +201 -0
  26. tonutils/clients/http/providers/response.py +85 -0
  27. tonutils/clients/http/providers/tonapi/__init__.py +3 -0
  28. tonutils/clients/http/{tonapi → providers/tonapi}/models.py +1 -0
  29. tonutils/clients/http/providers/tonapi/provider.py +125 -0
  30. tonutils/clients/http/providers/toncenter/__init__.py +3 -0
  31. tonutils/clients/http/{toncenter → providers/toncenter}/models.py +1 -0
  32. tonutils/clients/http/providers/toncenter/provider.py +119 -0
  33. tonutils/clients/http/utils.py +140 -0
  34. tonutils/clients/limiter.py +115 -0
  35. tonutils/contracts/__init__.py +4 -0
  36. tonutils/contracts/base.py +33 -20
  37. tonutils/contracts/dns/methods.py +2 -2
  38. tonutils/contracts/jetton/methods.py +2 -2
  39. tonutils/contracts/nft/methods.py +2 -2
  40. tonutils/contracts/nft/tlb.py +1 -1
  41. tonutils/{protocols/contract.py → contracts/protocol.py} +29 -29
  42. tonutils/contracts/telegram/methods.py +2 -2
  43. tonutils/contracts/vanity/vanity.py +1 -1
  44. tonutils/contracts/wallet/__init__.py +2 -0
  45. tonutils/contracts/wallet/base.py +3 -3
  46. tonutils/contracts/wallet/messages.py +1 -1
  47. tonutils/contracts/wallet/methods.py +2 -2
  48. tonutils/{protocols/wallet.py → contracts/wallet/protocol.py} +35 -35
  49. tonutils/contracts/wallet/versions/v5.py +3 -3
  50. tonutils/exceptions.py +146 -228
  51. tonutils/tonconnect/__init__.py +0 -0
  52. tonutils/tools/__init__.py +6 -0
  53. tonutils/tools/block_scanner/__init__.py +26 -0
  54. tonutils/tools/block_scanner/annotations.py +23 -0
  55. tonutils/tools/block_scanner/dispatcher.py +141 -0
  56. tonutils/tools/block_scanner/events.py +31 -0
  57. tonutils/tools/block_scanner/scanner.py +315 -0
  58. tonutils/tools/block_scanner/traversal.py +96 -0
  59. tonutils/tools/block_scanner/where.py +151 -0
  60. tonutils/tools/status_monitor/__init__.py +3 -0
  61. tonutils/tools/status_monitor/console.py +157 -0
  62. tonutils/tools/status_monitor/models.py +27 -0
  63. tonutils/tools/status_monitor/monitor.py +295 -0
  64. tonutils/types.py +125 -2
  65. tonutils/utils.py +3 -3
  66. {tonutils-2.0.1b2.dist-info → tonutils-2.0.1b4.dist-info}/METADATA +2 -5
  67. tonutils-2.0.1b4.dist-info/RECORD +108 -0
  68. tonutils-2.0.1b4.dist-info/entry_points.txt +2 -0
  69. tonutils/clients/adnl/provider/limiter.py +0 -56
  70. tonutils/clients/adnl/stack.py +0 -64
  71. tonutils/clients/http/chainstack/__init__.py +0 -4
  72. tonutils/clients/http/chainstack/client.py +0 -63
  73. tonutils/clients/http/chainstack/provider.py +0 -44
  74. tonutils/clients/http/quicknode/__init__.py +0 -4
  75. tonutils/clients/http/quicknode/client.py +0 -60
  76. tonutils/clients/http/quicknode/provider.py +0 -42
  77. tonutils/clients/http/tatum/__init__.py +0 -4
  78. tonutils/clients/http/tatum/client.py +0 -66
  79. tonutils/clients/http/tatum/provider.py +0 -53
  80. tonutils/clients/http/tonapi/__init__.py +0 -4
  81. tonutils/clients/http/tonapi/provider.py +0 -150
  82. tonutils/clients/http/tonapi/stack.py +0 -71
  83. tonutils/clients/http/toncenter/__init__.py +0 -4
  84. tonutils/clients/http/toncenter/provider.py +0 -145
  85. tonutils/clients/http/toncenter/stack.py +0 -73
  86. tonutils/protocols/__init__.py +0 -9
  87. tonutils-2.0.1b2.dist-info/RECORD +0 -98
  88. /tonutils/{protocols/client.py → clients/protocol.py} +0 -0
  89. {tonutils-2.0.1b2.dist-info → tonutils-2.0.1b4.dist-info}/WHEEL +0 -0
  90. {tonutils-2.0.1b2.dist-info → tonutils-2.0.1b4.dist-info}/licenses/LICENSE +0 -0
  91. {tonutils-2.0.1b2.dist-info → tonutils-2.0.1b4.dist-info}/top_level.txt +0 -0
tonutils/exceptions.py CHANGED
@@ -1,294 +1,212 @@
1
+ import asyncio
1
2
  import typing as t
2
3
 
3
- from pytoniq_core import Address
4
-
5
4
  __all__ = [
6
- "TonutilsException",
7
- "ClientNotConnectedError",
8
- "NotRefreshedError",
9
- "ContractError",
5
+ "TonutilsError",
6
+ "TransportError",
7
+ "ProviderError",
10
8
  "ClientError",
11
- "AdnlServerError",
12
- "AdnlProviderError",
13
- "AdnlProviderConnectError",
14
- "AdnlProviderClosedError",
15
- "AdnlProviderResponseError",
16
- "AdnlProviderMissingBlockError",
17
- "AdnlBalancerConnectionError",
18
- "RateLimitExceededError",
9
+ "ContractError",
10
+ "BalancerError",
11
+ "NotConnectedError",
12
+ "ProviderTimeoutError",
13
+ "ProviderResponseError",
14
+ "RetryLimitError",
19
15
  "RunGetMethodError",
20
- "AdnlHandshakeError",
21
- "AdnlTransportError",
22
- "AdnlTransportStateError",
23
- "AdnlTransportCipherError",
24
- "AdnlTransportFrameError",
16
+ "StateNotLoadedError",
17
+ "CDN_CHALLENGE_MARKERS",
25
18
  ]
26
19
 
27
20
 
28
- class TonutilsException(Exception):
29
- """Base exception for all tonutils-specific errors."""
21
+ class TonutilsError(Exception):
22
+ """Base exception for tonutils."""
30
23
 
31
- @classmethod
32
- def _obj_name(cls, obj: t.Union[object, type, str]) -> str:
33
- """
34
- Resolve a human-readable class name from an object or type.
35
24
 
36
- :param obj: Instance, class or string name
37
- :return: Resolved name used in error messages
38
- """
39
- if isinstance(obj, type):
40
- return obj.__name__
41
- if isinstance(obj, str):
42
- return obj
43
- return obj.__class__.__name__
25
+ class TransportError(TonutilsError):
26
+ """Transport-level failure with structured context.
44
27
 
28
+ Covers: TCP connect, ADNL handshake, send/recv, crypto failures.
45
29
 
46
- class ClientNotConnectedError(TonutilsException):
30
+ :param endpoint: Server address as "host:port"
31
+ :param operation: What was attempted ("connect", "handshake", "send", "recv")
32
+ :param reason: Why it failed ("timeout 2.0s", "connection refused", etc.)
47
33
  """
48
- Raised when a client method is called without an active connection.
49
34
 
50
- This usually indicates that connect() or an async context manager
51
- was not used before making network requests.
52
- """
35
+ endpoint: str
36
+ operation: str
37
+ reason: str
53
38
 
54
- def __init__(self, obj: t.Union[object, type, str]) -> None:
55
- name = self._obj_name(obj)
56
- super().__init__(
57
- f"`{name}` is not connected.\n"
58
- f"Use `async with {name}(...) as client:` "
59
- f"or call `await {name}(...).connect()` before making requests."
60
- )
39
+ def __init__(
40
+ self,
41
+ *,
42
+ endpoint: str,
43
+ operation: str,
44
+ reason: str,
45
+ ) -> None:
46
+ self.endpoint = endpoint
47
+ self.operation = operation
48
+ self.reason = reason
49
+ super().__init__(f"{operation} failed at {endpoint}: {reason}")
61
50
 
62
51
 
63
- class NotRefreshedError(TonutilsException):
64
- """
65
- Raised when accessing derived state before an explicit refresh.
52
+ class ProviderError(TonutilsError):
53
+ """Raise on provider-level failures (protocol, parsing, session/state)."""
66
54
 
67
- Typical usage is for contract wrappers that require refresh()
68
- to be called before accessing state_info or derived properties.
69
- """
70
55
 
71
- def __init__(self, obj: t.Union[object, type, str], attr: str) -> None:
72
- name = self._obj_name(obj)
73
- super().__init__(
74
- f"Access to `{attr}` is not allowed.\n"
75
- f"Call `await {name}.refresh()` before accessing `{attr}`."
76
- )
56
+ class ClientError(TonutilsError):
57
+ """Raise on client misuse, validation errors, or unsupported operations."""
77
58
 
78
59
 
79
- class ContractError(TonutilsException):
80
- """
81
- Generic error related to smart contract helpers.
60
+ class BalancerError(TonutilsError):
61
+ """Raise on balancer failures (no alive backends, failover exhausted)."""
82
62
 
83
- Used for configuration issues, invalid versions and similar
84
- contract wrapper problems.
85
- """
86
63
 
87
- def __init__(self, obj: t.Union[object, type, str], message: str) -> None:
88
- super().__init__(f"{self._obj_name(obj)}: {message}.")
64
+ class NotConnectedError(TonutilsError, RuntimeError):
65
+ """Raise when an operation requires an active connection."""
89
66
 
67
+ endpoint: t.Optional[str]
90
68
 
91
- class ClientError(TonutilsException):
92
- """
93
- Base error for client-side failures.
69
+ def __init__(self, endpoint: t.Optional[str] = None) -> None:
70
+ self.endpoint = endpoint
71
+ if endpoint:
72
+ super().__init__(f"not connected to {endpoint}")
73
+ else:
74
+ super().__init__("not connected")
94
75
 
95
- Used for issues related to specific client implementations
96
- (HTTP, ADNL, balancers, etc.).
97
- """
98
76
 
77
+ class ProviderTimeoutError(ProviderError, asyncio.TimeoutError):
78
+ """Raise when a provider operation exceeds its timeout.
99
79
 
100
- class AdnlServerError(ClientError):
80
+ :param timeout: Timeout in seconds.
81
+ :param endpoint: Endpoint identifier (URL or host:port).
82
+ :param operation: Operation label (e.g. "request", "connect").
101
83
  """
102
- Lite-server reported an internal error while processing a request.
103
84
 
104
- Wraps lite-server error code and message as returned by ADNL.
105
- """
85
+ timeout: float
86
+ endpoint: str
87
+ operation: str
106
88
 
107
- def __init__(self, code: int, message: str) -> None:
108
- """
109
- :param code: Lite-server error code
110
- :param message: Lite-server error message
111
- """
112
- super().__init__(f"Lite-server crashed with `{code}` code. Message: {message}.")
113
- self.message = message
114
- self.code = code
89
+ def __init__(self, *, timeout: float, endpoint: str, operation: str) -> None:
90
+ self.timeout = timeout
91
+ self.endpoint = endpoint
92
+ self.operation = operation
93
+ super().__init__(f"{operation} timed out after {timeout}s at {endpoint}")
115
94
 
116
95
 
117
- class AdnlProviderError(ClientError):
118
- """
119
- Base error for ADNL provider failures.
96
+ class ProviderResponseError(ProviderError):
97
+ """Raise when a backend returns an error response.
120
98
 
121
- Includes additional context about the lite-server host and port.
99
+ :param code: Backend code (HTTP status or lite-server code).
100
+ :param message: Backend error description.
101
+ :param endpoint: Endpoint identifier (URL or host:port).
122
102
  """
123
103
 
124
- def __init__(self, message: str, host: str, port: int) -> None:
125
- """
126
- :param message: Error description
127
- :param host: Lite-server host
128
- :param port: Lite-server port
129
- """
130
- full_message = f"{message} ({host}:{port})."
131
- super().__init__(full_message)
132
- self.host = host
133
- self.port = port
134
-
135
-
136
- class AdnlProviderConnectError(AdnlProviderError):
137
- """
138
- Failed to establish an ADNL connection to the lite-server.
104
+ code: int
105
+ message: str
106
+ endpoint: str
139
107
 
140
- Wraps network or handshake errors that occur during connect().
141
- """
142
-
143
- def __init__(self, host: str, port: int, message: str) -> None:
144
- super().__init__(
145
- f"Failed to connect: {message}.",
146
- host=host,
147
- port=port,
148
- )
108
+ def __init__(self, *, code: int, message: str, endpoint: str) -> None:
109
+ self.code = code
110
+ self.message = message
111
+ self.endpoint = endpoint
112
+ super().__init__(f"request failed with code {code} at {endpoint}: {message}")
149
113
 
150
114
 
151
- class AdnlProviderClosedError(AdnlProviderError):
152
- """
153
- ADNL provider was closed while waiting for a response.
115
+ class RetryLimitError(ProviderError):
116
+ """Raise when retry policy is exhausted for a matched rule.
154
117
 
155
- Typically raised when transport is torn down during an in-flight
156
- request or when the remote peer closes connection unexpectedly.
118
+ :param attempts: Attempts already performed for the matched rule.
119
+ :param max_attempts: Maximum attempts allowed by the matched rule.
120
+ :param last_error: Last provider error that triggered a retry.
157
121
  """
158
122
 
159
- def __init__(self, host: str, port: int) -> None:
160
- super().__init__(
161
- "Provider closed while waiting response.",
162
- host=host,
163
- port=port,
164
- )
165
-
123
+ attempts: int
124
+ max_attempts: int
125
+ last_error: ProviderError
166
126
 
167
- class AdnlProviderResponseError(AdnlProviderError):
168
- """
169
- Received an invalid or malformed response from lite-server.
170
-
171
- Raised when the ADNL response payload does not match the expected
172
- structure or type.
173
- """
127
+ def __init__(
128
+ self,
129
+ *,
130
+ attempts: int,
131
+ max_attempts: int,
132
+ last_error: ProviderError,
133
+ ) -> None:
134
+ self.attempts = attempts
135
+ self.max_attempts = max_attempts
136
+ self.last_error = last_error
137
+ super().__init__(f"retry exhausted ({attempts}/{max_attempts}): {last_error}")
174
138
 
175
- def __init__(self, host: str, port: int) -> None:
176
- super().__init__(
177
- "Invalid response from provider.",
178
- host=host,
179
- port=port,
180
- )
181
139
 
140
+ class ContractError(ClientError):
141
+ """Raise when a contract wrapper operation fails.
182
142
 
183
- class AdnlProviderMissingBlockError(AdnlProviderError):
143
+ :param target: Contract instance or contract class related to the failure.
144
+ :param message: Human-readable error message.
184
145
  """
185
- Lite-server reported that a requested block is missing.
186
146
 
187
- Used for specific lite-server error codes that indicate
188
- absence of the requested block.
189
- """
147
+ target: t.Any
148
+ message: str
190
149
 
191
- def __init__(self, attempts: int, host: str, port: int, message: str) -> None:
192
- super().__init__(
193
- f"Cannot load block after {attempts} attempts: {message}.",
194
- host=host,
195
- port=port,
150
+ def __init__(self, target: t.Any, message: str) -> None:
151
+ self.target = target
152
+ self.message = message
153
+ name = (
154
+ target.__name__ if isinstance(target, type) else target.__class__.__name__
196
155
  )
197
- self.attempts = attempts
156
+ super().__init__(f"{name}: {message}")
198
157
 
199
158
 
200
- class AdnlBalancerConnectionError(ClientError):
201
- """
202
- All ADNL lite-server providers failed to connect or process a request.
159
+ class StateNotLoadedError(ContractError):
160
+ """Raise when a contract wrapper requires state that is not loaded.
203
161
 
204
- Raised by AdnlBalancer when no healthy providers remain.
162
+ :param contract: Contract instance related to the failure.
163
+ :param missing: Missing field name (e.g. "state_info", "state_data").
205
164
  """
206
165
 
166
+ missing: str
207
167
 
208
- class RateLimitExceededError(ClientError):
209
- """
210
- Request was retried multiple times but rate limits could not be bypassed.
211
-
212
- Raised after exhausting configured retry attempts.
213
- """
214
-
215
- def __init__(self, attempts: int) -> None:
216
- """
217
- :param attempts: Number of attempts performed before giving up
218
- """
219
- super().__init__(f"Rate limit exceeded after `{attempts}` attempts.")
220
- self.attempts = attempts
168
+ def __init__(self, contract: t.Any, *, missing: str) -> None:
169
+ self.missing = missing
170
+ name = contract.__class__.__name__
171
+ super().__init__(contract, f"{missing} is not loaded. Call {name}.refresh().")
221
172
 
222
173
 
223
174
  class RunGetMethodError(ClientError):
224
- """
225
- get-method execution failed with a non-zero exit code.
175
+ """Raise when a contract get-method returns a non-zero TVM exit code.
226
176
 
227
- Raised when lite-server returns exit_code != 0 for a runSmcMethod call.
177
+ :param address: Contract address (string form).
178
+ :param method_name: Get-method name.
179
+ :param exit_code: TVM exit code.
228
180
  """
229
181
 
230
- def __init__(self, address: Address, method_name: str, exit_code: int) -> None:
231
- """
232
- :param address: Contract address on which get-method was executed
233
- :param method_name: Name of the get-method
234
- :param exit_code: Non-zero TVM exit code returned by the method
235
- """
236
- super().__init__(
237
- f"Get method `{method_name}` on `{address.to_str()}` "
238
- f"failed with `{exit_code}` exit code."
239
- )
182
+ address: str
183
+ method_name: str
184
+ exit_code: int
185
+
186
+ def __init__(self, *, address: str, method_name: str, exit_code: int) -> None:
187
+ self.address = address
240
188
  self.method_name = method_name
241
189
  self.exit_code = exit_code
242
- self.address = address
243
-
244
-
245
- class AdnlTransportError(TonutilsException):
246
- """
247
- Base error for raw ADNL transport failures.
248
-
249
- Covers handshake, cipher initialization, framing and state issues.
250
- """
251
-
252
-
253
- class AdnlHandshakeError(AdnlTransportError):
254
- """
255
- ADNL handshake failed during initial connection.
256
-
257
- Raised when the remote side closes the connection or does not
258
- respond within the expected timeout.
259
- """
260
-
261
-
262
- class AdnlTransportStateError(AdnlTransportError):
263
- """
264
- Invalid internal state of the ADNL transport.
265
-
266
- Raised when required transport components (reader, writer, cipher, etc.)
267
- are not initialized or used incorrectly.
268
- """
269
-
270
- def __init__(self, message: str) -> None:
271
- super().__init__(f"ADNL transport state error: {message}.")
272
-
273
-
274
- class AdnlTransportCipherError(AdnlTransportError):
275
- """
276
- ADNL cipher was used before being initialized.
277
-
278
- Raised when trying to encrypt or decrypt frames without
279
- a valid session cipher.
280
- """
281
-
282
- def __init__(self, direction: str) -> None:
283
- super().__init__(f"ADNL {direction} cipher is not initialized.")
284
-
285
-
286
- class AdnlTransportFrameError(AdnlTransportError):
287
- """
288
- Malformed or invalid ADNL frame was received.
190
+ super().__init__(
191
+ f"get-method '{method_name}' failed for {address} with exit code {exit_code}"
192
+ )
289
193
 
290
- Raised when frame length, structure or checksum validation fails.
291
- """
292
194
 
293
- def __init__(self, reason: str) -> None:
294
- super().__init__(f"Invalid ADNL frame: {reason}.")
195
+ CDN_CHALLENGE_MARKERS: t.Dict[str, str] = {
196
+ # Cloudflare
197
+ "cloudflare": "Cloudflare protection triggered or blocked the request.",
198
+ "cf-ray": "Cloudflare intermediate error (cf-ray header detected).",
199
+ "just a moment": "Cloudflare browser verification page.",
200
+ "checking your browser": "Cloudflare browser verification page.",
201
+ "attention required": "Cloudflare challenge page.",
202
+ "captcha": "Cloudflare CAPTCHA challenge.",
203
+ # Other CDNs / proxies
204
+ "akamai": "Akamai CDN blocked or intercepted the request.",
205
+ "fastly": "Fastly CDN error response detected.",
206
+ "varnish": "Varnish cache/CDN interference.",
207
+ "nginx": "Reverse proxy (nginx) error response.",
208
+ # Upstream failures
209
+ "502 bad gateway": "Bad gateway from upstream or proxy.",
210
+ "503 service unavailable": "Service temporarily unavailable (proxy or CDN).",
211
+ "ddos": "Possible DDoS protection or mitigation page.",
212
+ }
File without changes
@@ -0,0 +1,6 @@
1
+ from . import block_scanner, status_monitor
2
+
3
+ __all__ = [
4
+ "block_scanner",
5
+ "status_monitor",
6
+ ]
@@ -0,0 +1,26 @@
1
+ from .events import (
2
+ BlockEvent,
3
+ TransactionEvent,
4
+ TransactionsEvent,
5
+ )
6
+ from .scanner import BlockScanner
7
+ from .where import (
8
+ Where,
9
+ comment,
10
+ destination,
11
+ opcode,
12
+ sender,
13
+ )
14
+
15
+
16
+ __all__ = [
17
+ "BlockScanner",
18
+ "BlockEvent",
19
+ "TransactionEvent",
20
+ "TransactionsEvent",
21
+ "Where",
22
+ "comment",
23
+ "destination",
24
+ "opcode",
25
+ "sender",
26
+ ]
@@ -0,0 +1,23 @@
1
+ import typing as t
2
+
3
+ from tonutils.tools.block_scanner.events import (
4
+ BlockEvent,
5
+ EventBase,
6
+ TransactionEvent,
7
+ TransactionsEvent,
8
+ )
9
+
10
+ TEvent = t.TypeVar("TEvent", bound=EventBase)
11
+
12
+ Handler = t.Callable[[TEvent], t.Awaitable[None]]
13
+ Where = t.Callable[[TEvent], t.Union[bool, t.Awaitable[bool]]]
14
+
15
+ BlockWhere = t.Callable[[BlockEvent], t.Union[bool, t.Awaitable[bool]]]
16
+ TransactionWhere = t.Callable[[TransactionEvent], t.Union[bool, t.Awaitable[bool]]]
17
+ TransactionsWhere = t.Callable[[TransactionsEvent], t.Union[bool, t.Awaitable[bool]]]
18
+
19
+ AnyHandler = t.Callable[[EventBase], t.Awaitable[None]]
20
+ AnyWhere = t.Callable[[EventBase], t.Union[bool, t.Awaitable[bool]]]
21
+
22
+ HandlerEntry = t.Tuple[AnyHandler, t.Optional[AnyWhere]]
23
+ Decorator = t.Callable[[Handler[TEvent]], Handler[TEvent]]
@@ -0,0 +1,141 @@
1
+ import asyncio
2
+ import inspect
3
+ import traceback
4
+ import typing as t
5
+
6
+ from tonutils.tools.block_scanner.annotations import (
7
+ AnyHandler,
8
+ AnyWhere,
9
+ Decorator,
10
+ Handler,
11
+ HandlerEntry,
12
+ TEvent,
13
+ Where,
14
+ )
15
+ from tonutils.tools.block_scanner.events import EventBase
16
+
17
+
18
+ class EventDispatcher:
19
+ """Dispatches events to registered handlers asynchronously."""
20
+
21
+ def __init__(self, max_concurrency: int = 1000) -> None:
22
+ """
23
+ Initialize EventDispatcher.
24
+
25
+ :param max_concurrency: maximum number of concurrent handler tasks.
26
+ """
27
+ self._handlers: t.Dict[t.Type[EventBase], t.List[HandlerEntry]] = {}
28
+ self._sem = asyncio.Semaphore(max(1, max_concurrency))
29
+ self._tasks: t.Set[asyncio.Task[None]] = set()
30
+ self._closed = False
31
+
32
+ def register(
33
+ self,
34
+ event_type: t.Type[TEvent],
35
+ handler: Handler[TEvent],
36
+ *,
37
+ where: t.Optional[Where[TEvent]] = None,
38
+ ) -> None:
39
+ """
40
+ Register a handler for a specific event type.
41
+
42
+ :param event_type: subclass of EventBase to handle.
43
+ :param handler: callable receiving the event.
44
+ :param where: optional filter predicate. Handler is invoked only if predicate returns True.
45
+ """
46
+ if not callable(handler):
47
+ raise TypeError("handler must be callable")
48
+
49
+ entry: HandlerEntry = (
50
+ t.cast(AnyHandler, handler),
51
+ t.cast(t.Optional[AnyWhere], where),
52
+ )
53
+ self._handlers.setdefault(event_type, []).append(entry)
54
+
55
+ def on(
56
+ self,
57
+ event_type: t.Type[TEvent],
58
+ *,
59
+ where: t.Optional[Where[TEvent]] = None,
60
+ ) -> Decorator[TEvent]:
61
+ """
62
+ Decorator to register a handler for an event type.
63
+
64
+ :param event_type: event class to handle.
65
+ :param where: optional filter predicate.
66
+ :return: Decorator that registers the handler.
67
+ """
68
+
69
+ def decorator(fn: Handler[TEvent]) -> Handler[TEvent]:
70
+ self.register(event_type=event_type, handler=fn, where=where)
71
+ return fn
72
+
73
+ return decorator
74
+
75
+ def _iter_handlers(self, event: EventBase) -> t.Sequence[HandlerEntry]:
76
+ """Return all handlers matching the type of `event`."""
77
+ out: t.List[HandlerEntry] = []
78
+ for tp in type(event).mro():
79
+ if tp is EventBase:
80
+ break
81
+ entries = self._handlers.get(t.cast(t.Type[EventBase], tp))
82
+ if entries:
83
+ out.extend(entries)
84
+ return out
85
+
86
+ def _on_task_done(self, task: asyncio.Task[None]) -> None:
87
+ """Callback to handle task completion and print exceptions."""
88
+ self._tasks.discard(task)
89
+ try:
90
+ exc = task.exception()
91
+ except asyncio.CancelledError:
92
+ return
93
+ if exc is not None:
94
+ traceback.print_exception(type(exc), exc, exc.__traceback__)
95
+
96
+ async def _run_task(
97
+ self,
98
+ handler: AnyHandler,
99
+ event: EventBase,
100
+ where: t.Optional[AnyWhere] = None,
101
+ ) -> None:
102
+ """
103
+ Run a single handler task with optional 'where' filtering.
104
+
105
+ :param handler: async callable to execute.
106
+ :param event: event instance to pass.
107
+ :param where: optional predicate, skip handler if False.
108
+ """
109
+ async with self._sem:
110
+ if where is not None:
111
+ result = where(event)
112
+ if inspect.isawaitable(result):
113
+ result = await result
114
+ if not result:
115
+ return
116
+ await handler(event)
117
+
118
+ def emit(self, event: EventBase) -> None:
119
+ """
120
+ Emit an event to all matching handlers.
121
+
122
+ Handlers are executed asynchronously.
123
+ """
124
+ if self._closed:
125
+ return
126
+
127
+ for handler, where in self._iter_handlers(event):
128
+ task = asyncio.create_task(self._run_task(handler, event, where))
129
+ self._tasks.add(task)
130
+ task.add_done_callback(self._on_task_done)
131
+
132
+ async def aclose(self) -> None:
133
+ """
134
+ Close the dispatcher and wait for all running handler tasks.
135
+
136
+ After calling, no new events will be dispatched.
137
+ """
138
+ self._closed = True
139
+ if self._tasks:
140
+ await asyncio.gather(*self._tasks, return_exceptions=True)
141
+ self._tasks.clear()
@@ -0,0 +1,31 @@
1
+ import typing as t
2
+ from dataclasses import dataclass, field
3
+
4
+ from pytoniq_core import Transaction
5
+ from pytoniq_core.tl import BlockIdExt
6
+
7
+ from tonutils.clients import LiteBalancer, LiteClient
8
+
9
+
10
+ @dataclass(frozen=True, slots=True)
11
+ class EventBase:
12
+ client: t.Union[LiteBalancer, LiteClient]
13
+ mc_block: BlockIdExt
14
+ context: t.Dict[str, t.Any]
15
+
16
+
17
+ @dataclass(frozen=True, slots=True)
18
+ class BlockEvent(EventBase):
19
+ block: BlockIdExt
20
+
21
+
22
+ @dataclass(frozen=True, slots=True)
23
+ class TransactionEvent(EventBase):
24
+ block: BlockIdExt
25
+ transaction: Transaction
26
+
27
+
28
+ @dataclass(frozen=True, slots=True)
29
+ class TransactionsEvent(EventBase):
30
+ block: BlockIdExt
31
+ transactions: t.List[Transaction] = field(default_factory=list)