tonutils 2.0.1b6__py3-none-any.whl → 2.0.1b7__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 (33) hide show
  1. tonutils/__meta__.py +1 -1
  2. tonutils/clients/adnl/balancer.py +132 -355
  3. tonutils/clients/adnl/client.py +32 -202
  4. tonutils/clients/adnl/mixin.py +268 -0
  5. tonutils/clients/adnl/provider/provider.py +61 -16
  6. tonutils/clients/adnl/provider/transport.py +13 -4
  7. tonutils/clients/adnl/provider/workers/pinger.py +1 -1
  8. tonutils/clients/adnl/utils.py +5 -5
  9. tonutils/clients/base.py +52 -92
  10. tonutils/clients/http/balancer.py +93 -90
  11. tonutils/clients/http/clients/tatum.py +1 -0
  12. tonutils/clients/http/clients/tonapi.py +12 -24
  13. tonutils/clients/http/clients/toncenter.py +15 -33
  14. tonutils/clients/http/provider/base.py +75 -60
  15. tonutils/clients/http/provider/models.py +1 -1
  16. tonutils/clients/http/provider/tonapi.py +0 -5
  17. tonutils/clients/http/provider/toncenter.py +4 -8
  18. tonutils/clients/protocol.py +6 -6
  19. tonutils/contracts/base.py +32 -32
  20. tonutils/contracts/protocol.py +9 -9
  21. tonutils/contracts/wallet/base.py +5 -5
  22. tonutils/contracts/wallet/versions/v5.py +2 -2
  23. tonutils/exceptions.py +29 -13
  24. tonutils/tools/block_scanner/__init__.py +5 -1
  25. tonutils/tools/block_scanner/scanner.py +1 -1
  26. tonutils/tools/status_monitor/monitor.py +6 -6
  27. tonutils/types.py +2 -2
  28. {tonutils-2.0.1b6.dist-info → tonutils-2.0.1b7.dist-info}/METADATA +3 -18
  29. {tonutils-2.0.1b6.dist-info → tonutils-2.0.1b7.dist-info}/RECORD +33 -32
  30. {tonutils-2.0.1b6.dist-info → tonutils-2.0.1b7.dist-info}/WHEEL +0 -0
  31. {tonutils-2.0.1b6.dist-info → tonutils-2.0.1b7.dist-info}/entry_points.txt +0 -0
  32. {tonutils-2.0.1b6.dist-info → tonutils-2.0.1b7.dist-info}/licenses/LICENSE +0 -0
  33. {tonutils-2.0.1b6.dist-info → tonutils-2.0.1b7.dist-info}/top_level.txt +0 -0
@@ -19,8 +19,9 @@ from tonutils.exceptions import (
19
19
  ProviderResponseError,
20
20
  RunGetMethodError,
21
21
  ProviderTimeoutError,
22
+ NotConnectedError,
22
23
  )
23
- from tonutils.types import ClientType, ContractStateInfo, NetworkGlobalID
24
+ from tonutils.types import ClientType, ContractInfo, NetworkGlobalID
24
25
 
25
26
  _T = t.TypeVar("_T")
26
27
 
@@ -73,14 +74,14 @@ class HttpBalancer(BaseClient):
73
74
 
74
75
  :param network: Target TON network (mainnet or testnet)
75
76
  :param clients: List of HTTP BaseClient instances to balance between
76
- :param request_timeout: Maximum total time in seconds for a balancer operation,
77
+ :param request_timeout: Maximum total time in seconds for a balancer method,
77
78
  including all failover attempts across providers
78
79
  """
79
80
  self.network = network
80
81
 
81
82
  self._clients: t.List[BaseClient] = []
82
83
  self._states: t.List[HttpClientState] = []
83
- self.__init_clients(clients)
84
+ self._init_clients(clients)
84
85
 
85
86
  self._rr = cycle(self._clients)
86
87
 
@@ -89,31 +90,14 @@ class HttpBalancer(BaseClient):
89
90
 
90
91
  self._request_timeout = request_timeout
91
92
 
92
- def __init_clients(
93
- self,
94
- clients: t.List[BaseClient],
95
- ) -> None:
96
- """Validate and register input HTTP clients."""
97
- for client in clients:
98
- if client.TYPE != ClientType.HTTP:
99
- raise ClientError(
100
- "HttpBalancer can work only with HTTP clients, "
101
- f"got {client.__class__.__name__}."
102
- )
103
-
104
- if (
105
- isinstance(client, QuicknodeClient)
106
- and self.network == NetworkGlobalID.TESTNET
107
- ):
108
- raise ClientError(
109
- "QuickNode HTTP client does not support testnet network."
110
- )
111
-
112
- client.network = self.network
113
- state = HttpClientState(client=client)
93
+ @property
94
+ def connected(self) -> bool:
95
+ """
96
+ Check whether at least one underlying client is connected.
114
97
 
115
- self._clients.append(client)
116
- self._states.append(state)
98
+ :return: True if any client is connected, otherwise False
99
+ """
100
+ return any(c.connected for c in self._clients)
117
101
 
118
102
  @property
119
103
  def provider(self) -> t.Any:
@@ -125,14 +109,17 @@ class HttpBalancer(BaseClient):
125
109
  c = self._pick_client()
126
110
  return c.provider
127
111
 
128
- @property
129
- def is_connected(self) -> bool:
130
- """
131
- Check whether at least one underlying client is connected.
112
+ async def connect(self) -> None:
113
+ await asyncio.gather(
114
+ *(state.client.connect() for state in self._states),
115
+ return_exceptions=True,
116
+ )
132
117
 
133
- :return: True if any client is connected, otherwise False
134
- """
135
- return any(c.is_connected for c in self._clients)
118
+ async def close(self) -> None:
119
+ await asyncio.gather(
120
+ *(state.client.close() for state in self._states),
121
+ return_exceptions=True,
122
+ )
136
123
 
137
124
  @property
138
125
  def clients(self) -> t.Tuple[BaseClient, ...]:
@@ -154,7 +141,8 @@ class HttpBalancer(BaseClient):
154
141
  return tuple(
155
142
  state.client
156
143
  for state in self._states
157
- if state.retry_after is None or state.retry_after <= now
144
+ if state.client.connected
145
+ and (state.retry_after is None or state.retry_after <= now)
158
146
  )
159
147
 
160
148
  @property
@@ -171,24 +159,31 @@ class HttpBalancer(BaseClient):
171
159
  if state.retry_after is not None and state.retry_after > now
172
160
  )
173
161
 
174
- async def __aenter__(self) -> HttpBalancer:
175
- """
176
- Enter async context manager and connect all underlying clients.
177
-
178
- :return: Self instance with initialized connections
179
- """
180
- await self.connect()
181
- return self
182
-
183
- async def __aexit__(
162
+ def _init_clients(
184
163
  self,
185
- exc_type: t.Optional[t.Type[BaseException]],
186
- exc_value: t.Optional[BaseException],
187
- traceback: t.Optional[t.Any],
164
+ clients: t.List[BaseClient],
188
165
  ) -> None:
189
- """Exit async context manager and close all underlying clients."""
190
- with suppress(asyncio.CancelledError):
191
- await self.close()
166
+ """Validate and register input HTTP clients."""
167
+ for client in clients:
168
+ if client.TYPE != ClientType.HTTP:
169
+ raise ClientError(
170
+ "HttpBalancer can work only with HTTP clients, "
171
+ f"got {client.__class__.__name__}."
172
+ )
173
+
174
+ if (
175
+ isinstance(client, QuicknodeClient)
176
+ and self.network == NetworkGlobalID.TESTNET
177
+ ):
178
+ raise ClientError(
179
+ "QuickNode HTTP client does not support testnet network."
180
+ )
181
+
182
+ client.network = self.network
183
+ state = HttpClientState(client=client)
184
+
185
+ self._clients.append(client)
186
+ self._states.append(state)
192
187
 
193
188
  def _pick_client(self) -> BaseClient:
194
189
  """
@@ -201,8 +196,10 @@ class HttpBalancer(BaseClient):
201
196
 
202
197
  :return: Selected BaseClient instance
203
198
  """
204
- alive = list(self.alive_clients)
199
+ if not self.connected:
200
+ raise NotConnectedError(component=self.__class__.__name__)
205
201
 
202
+ alive = list(self.alive_clients)
206
203
  height_candidates: t.List[
207
204
  t.Tuple[
208
205
  float,
@@ -223,7 +220,9 @@ class HttpBalancer(BaseClient):
223
220
  height_candidates.append((wait, state.error_count, state))
224
221
 
225
222
  if not height_candidates:
226
- raise BalancerError("no available HTTP clients")
223
+ raise BalancerError(
224
+ "http balancer has no available clients (all in cooldown or not connected)"
225
+ )
227
226
 
228
227
  height_candidates.sort(key=lambda x: (x[0], x[1]))
229
228
  best_wait, best_err, _ = height_candidates[0]
@@ -286,75 +285,89 @@ class HttpBalancer(BaseClient):
286
285
  async def _with_failover(
287
286
  self,
288
287
  func: t.Callable[[BaseClient], t.Awaitable[_T]],
288
+ method: str,
289
289
  ) -> _T:
290
290
  """
291
- Execute a client operation with automatic failover.
291
+ Execute a client method with automatic failover.
292
292
 
293
293
  Iterates through available clients until one succeeds
294
294
  or all clients fail.
295
295
 
296
- :param func: Callable performing an operation using a client
296
+ :param func: Callable performing an method using a client
297
297
  :return: Result of the successful invocation
298
298
  """
299
299
 
300
300
  async def _run() -> _T:
301
+ if not self.connected:
302
+ raise NotConnectedError(
303
+ component=self.__class__.__name__,
304
+ operation=method,
305
+ )
306
+
301
307
  last_exc: t.Optional[BaseException] = None
308
+ attempts = 0
302
309
 
303
310
  for _ in range(len(self._clients)):
304
311
  if not self.alive_clients:
305
312
  break
306
313
 
307
314
  client = self._pick_client()
315
+ attempts += 1
308
316
 
309
317
  try:
310
318
  result = await func(client)
311
-
312
319
  except RunGetMethodError:
313
320
  raise
314
321
  except ProviderResponseError as e:
315
- is_rate_limit = e.code == 429
316
- self._mark_error(client, is_rate_limit=is_rate_limit)
322
+ self._mark_error(client, is_rate_limit=(e.code == 429))
317
323
  last_exc = e
318
324
  continue
319
325
  except (TransportError, ProviderError) as e:
320
326
  self._mark_error(client, is_rate_limit=False)
321
327
  last_exc = e
322
328
  continue
329
+ else:
330
+ self._mark_success(client)
331
+ return result
323
332
 
324
- self._mark_success(client)
325
- return result
326
-
327
- if last_exc is not None:
328
- raise last_exc
333
+ if last_exc is None:
334
+ raise BalancerError(
335
+ "http balancer has no available clients (all in cooldown or not connected)"
336
+ )
329
337
 
330
- raise ClientError("all HTTP clients failed to process request.")
338
+ raise BalancerError(
339
+ f"http failover exhausted after {attempts} attempt(s)"
340
+ ) from last_exc
331
341
 
332
342
  try:
333
343
  return await asyncio.wait_for(_run(), timeout=self._request_timeout)
334
344
  except asyncio.TimeoutError as exc:
335
345
  raise ProviderTimeoutError(
336
346
  timeout=self._request_timeout,
337
- endpoint="http balancer",
338
- operation="failover request",
347
+ endpoint=self.__class__.__name__,
348
+ operation="request",
339
349
  ) from exc
340
350
 
341
- async def _send_boc(self, boc: str) -> None:
351
+ async def _send_message(self, boc: str) -> None:
342
352
  async def _call(client: BaseClient) -> None:
343
- return await client._send_boc(boc)
353
+ return await client._send_message(boc)
344
354
 
345
- return await self._with_failover(_call)
355
+ method = "send_message"
356
+ return await self._with_failover(_call, method)
346
357
 
347
- async def _get_blockchain_config(self) -> t.Dict[int, t.Any]:
358
+ async def _get_config(self) -> t.Dict[int, t.Any]:
348
359
  async def _call(client: BaseClient) -> t.Dict[int, t.Any]:
349
- return await client._get_blockchain_config()
360
+ return await client._get_config()
350
361
 
351
- return await self._with_failover(_call)
362
+ method = "get_config"
363
+ return await self._with_failover(_call, method)
352
364
 
353
- async def _get_contract_info(self, address: str) -> ContractStateInfo:
354
- async def _call(client: BaseClient) -> ContractStateInfo:
355
- return await client._get_contract_info(address)
365
+ async def _get_info(self, address: str) -> ContractInfo:
366
+ async def _call(client: BaseClient) -> ContractInfo:
367
+ return await client._get_info(address)
356
368
 
357
- return await self._with_failover(_call)
369
+ method = "get_info"
370
+ return await self._with_failover(_call, method)
358
371
 
359
372
  async def _get_transactions(
360
373
  self,
@@ -371,7 +384,8 @@ class HttpBalancer(BaseClient):
371
384
  to_lt=to_lt,
372
385
  )
373
386
 
374
- return await self._with_failover(_call)
387
+ method = "get_transactions"
388
+ return await self._with_failover(_call, method)
375
389
 
376
390
  async def _run_get_method(
377
391
  self,
@@ -386,16 +400,5 @@ class HttpBalancer(BaseClient):
386
400
  stack=stack,
387
401
  )
388
402
 
389
- return await self._with_failover(_call)
390
-
391
- async def connect(self) -> None:
392
- await asyncio.gather(
393
- *(state.client.connect() for state in self._states),
394
- return_exceptions=True,
395
- )
396
-
397
- async def close(self) -> None:
398
- await asyncio.gather(
399
- *(state.client.close() for state in self._states),
400
- return_exceptions=True,
401
- )
403
+ method = "run_get_method"
404
+ return await self._with_failover(_call, method)
@@ -42,6 +42,7 @@ class TatumClient(ToncenterClient):
42
42
  NetworkGlobalID.TESTNET: "https://ton-testnet.gateway.tatum.io",
43
43
  }
44
44
  base_url = base_url or urls.get(network)
45
+
45
46
  super().__init__(
46
47
  network=network,
47
48
  api_key=api_key,
@@ -13,7 +13,7 @@ from tonutils.exceptions import ClientError, RunGetMethodError
13
13
  from tonutils.types import (
14
14
  ClientType,
15
15
  ContractState,
16
- ContractStateInfo,
16
+ ContractInfo,
17
17
  NetworkGlobalID,
18
18
  RetryPolicy,
19
19
  )
@@ -72,31 +72,25 @@ class TonapiClient(BaseClient):
72
72
  )
73
73
 
74
74
  @property
75
- def provider(self) -> TonapiHttpProvider:
76
- return self._provider
77
-
78
- @property
79
- def is_connected(self) -> bool:
75
+ def connected(self) -> bool:
80
76
  session = self._provider.session
81
77
  return session is not None and not session.closed
82
78
 
83
- async def __aenter__(self) -> TonapiClient:
79
+ @property
80
+ def provider(self) -> TonapiHttpProvider:
81
+ return self._provider
82
+
83
+ async def connect(self) -> None:
84
84
  await self._provider.connect()
85
- return self
86
85
 
87
- async def __aexit__(
88
- self,
89
- exc_type: t.Optional[t.Type[BaseException]],
90
- exc_value: t.Optional[BaseException],
91
- traceback: t.Optional[t.Any],
92
- ) -> None:
86
+ async def close(self) -> None:
93
87
  await self._provider.close()
94
88
 
95
- async def _send_boc(self, boc: str) -> None:
89
+ async def _send_message(self, boc: str) -> None:
96
90
  payload = BlockchainMessagePayload(boc=boc)
97
91
  return await self.provider.blockchain_message(payload=payload)
98
92
 
99
- async def _get_blockchain_config(self) -> t.Dict[int, t.Any]:
93
+ async def _get_config(self) -> t.Dict[int, t.Any]:
100
94
  result = await self.provider.blockchain_config()
101
95
 
102
96
  if result.raw is None:
@@ -106,10 +100,10 @@ class TonapiClient(BaseClient):
106
100
  config_slice = config_cell.begin_parse()
107
101
  return parse_stack_config(config_slice)
108
102
 
109
- async def _get_contract_info(self, address: str) -> ContractStateInfo:
103
+ async def _get_info(self, address: str) -> ContractInfo:
110
104
  result = await self.provider.blockchain_account(address)
111
105
 
112
- contract_info = ContractStateInfo(
106
+ contract_info = ContractInfo(
113
107
  balance=result.balance,
114
108
  state=ContractState(result.status),
115
109
  last_transaction_lt=result.last_transaction_lt,
@@ -165,9 +159,3 @@ class TonapiClient(BaseClient):
165
159
  )
166
160
 
167
161
  return decode_tonapi_stack(result.stack or [])
168
-
169
- async def connect(self) -> None:
170
- await self._provider.connect()
171
-
172
- async def close(self) -> None:
173
- await self._provider.close()
@@ -14,7 +14,7 @@ from tonutils.exceptions import ClientError, RunGetMethodError
14
14
  from tonutils.types import (
15
15
  ClientType,
16
16
  ContractState,
17
- ContractStateInfo,
17
+ ContractInfo,
18
18
  NetworkGlobalID,
19
19
  RetryPolicy,
20
20
  )
@@ -70,56 +70,44 @@ class ToncenterClient(BaseClient):
70
70
  )
71
71
 
72
72
  @property
73
- def provider(self) -> ToncenterHttpProvider:
74
- return self._provider
75
-
76
- @property
77
- def is_connected(self) -> bool:
73
+ def connected(self) -> bool:
78
74
  session = self._provider.session
79
75
  return session is not None and not session.closed
80
76
 
81
- async def __aenter__(self) -> ToncenterClient:
77
+ @property
78
+ def provider(self) -> ToncenterHttpProvider:
79
+ return self._provider
80
+
81
+ async def connect(self) -> None:
82
82
  await self._provider.connect()
83
- return self
84
83
 
85
- async def __aexit__(
86
- self,
87
- exc_type: t.Optional[t.Type[BaseException]],
88
- exc_value: t.Optional[BaseException],
89
- traceback: t.Optional[t.Any],
90
- ) -> None:
84
+ async def close(self) -> None:
91
85
  await self._provider.close()
92
86
 
93
- async def _send_boc(self, boc: str) -> None:
87
+ async def _send_message(self, boc: str) -> None:
94
88
  payload = SendBocPayload(boc=boc)
95
89
  return await self.provider.send_boc(payload=payload)
96
90
 
97
- async def _get_blockchain_config(self) -> t.Dict[int, t.Any]:
91
+ async def _get_config(self) -> t.Dict[int, t.Any]:
98
92
  request = await self.provider.get_config_all()
99
93
 
100
94
  if request.result is None:
101
- raise ClientError(
102
- "Invalid get_config_all response: missing 'result' field."
103
- )
95
+ raise ClientError("Invalid get_config response: missing `result`.")
104
96
 
105
97
  if request.result.config is None:
106
- raise ClientError(
107
- "Invalid config response: missing 'config' section in result."
108
- )
98
+ raise ClientError("Invalid config response: missing `config` in `result`.")
109
99
 
110
100
  if request.result.config.bytes is None:
111
- raise ClientError(
112
- "Invalid config response: missing 'bytes' field in 'config' section."
113
- )
101
+ raise ClientError("Invalid config response: missing `config.bytes`.")
114
102
 
115
103
  config_cell = Cell.one_from_boc(request.result.config.bytes)
116
104
  config_slice = config_cell.begin_parse()
117
105
  return parse_stack_config(config_slice)
118
106
 
119
- async def _get_contract_info(self, address: str) -> ContractStateInfo:
107
+ async def _get_info(self, address: str) -> ContractInfo:
120
108
  request = await self.provider.get_address_information(address)
121
109
 
122
- contract_info = ContractStateInfo(
110
+ contract_info = ContractInfo(
123
111
  balance=int(request.result.balance),
124
112
  state=ContractState(request.result.state),
125
113
  )
@@ -241,9 +229,3 @@ class ToncenterClient(BaseClient):
241
229
  )
242
230
 
243
231
  return decode_toncenter_stack(request.result.stack or [])
244
-
245
- async def connect(self) -> None:
246
- await self._provider.connect()
247
-
248
- async def close(self) -> None:
249
- await self._provider.close()
@@ -5,6 +5,7 @@ import json
5
5
  import typing as t
6
6
 
7
7
  import aiohttp
8
+ from pydantic import BaseModel, ValidationError
8
9
 
9
10
  from tonutils.clients.limiter import RateLimiter
10
11
  from tonutils.exceptions import (
@@ -13,6 +14,8 @@ from tonutils.exceptions import (
13
14
  ProviderTimeoutError,
14
15
  RetryLimitError,
15
16
  CDN_CHALLENGE_MARKERS,
17
+ TransportError,
18
+ ProviderError,
16
19
  )
17
20
  from tonutils.types import RetryPolicy
18
21
 
@@ -65,17 +68,77 @@ class HttpProvider:
65
68
  return self._session
66
69
 
67
70
  @property
68
- def is_connected(self) -> bool:
71
+ def connected(self) -> bool:
69
72
  """Check whether the provider session is initialized and open."""
70
73
  return self._session is not None and not self._session.closed
71
74
 
75
+ @staticmethod
76
+ def _model(model: t.Type[BaseModel], data: t.Any) -> t.Any:
77
+ try:
78
+ return model.model_validate(data)
79
+ except ValidationError as e:
80
+ raise ProviderError(
81
+ f"invalid response: {model.__name__} validation failed"
82
+ ) from e
83
+
84
+ async def send_http_request(
85
+ self,
86
+ method: str,
87
+ path: str,
88
+ *,
89
+ params: t.Any = None,
90
+ json_data: t.Any = None,
91
+ ) -> t.Any:
92
+ """Send an HTTP request with retry handling.
93
+
94
+ On provider error, retries the request according to the retry policy
95
+ matched by error code and message. If no rule matches, or retry attempts
96
+ are exhausted, the error is raised.
97
+
98
+ :param method: HTTP method.
99
+ :param path: Endpoint path relative to base_url.
100
+ :param params: Optional query parameters.
101
+ :param json_data: Optional JSON body.
102
+ :return: Parsed response payload.
103
+ """
104
+ attempts: t.Dict[int, int] = {}
105
+
106
+ while True:
107
+ try:
108
+ return await self._send_once(
109
+ method,
110
+ path,
111
+ params=params,
112
+ json_data=json_data,
113
+ )
114
+ except ProviderResponseError as e:
115
+ policy = self._retry_policy
116
+ if policy is None:
117
+ raise
118
+
119
+ rule = policy.rule_for(e.code, e.message)
120
+ if rule is None:
121
+ raise
122
+
123
+ key = id(rule)
124
+ attempts[key] = attempts.get(key, 0) + 1
125
+
126
+ if attempts[key] >= rule.attempts:
127
+ raise RetryLimitError(
128
+ attempts=attempts[key],
129
+ max_attempts=rule.attempts,
130
+ last_error=e,
131
+ ) from e
132
+
133
+ await asyncio.sleep(rule.delay(attempts[key] - 1))
134
+
72
135
  async def connect(self) -> None:
73
136
  """Initialize HTTP session if not already connected."""
74
- if self.is_connected:
137
+ if self.connected:
75
138
  return
76
139
 
77
140
  async with self._connect_lock:
78
- if self.is_connected:
141
+ if self.connected:
79
142
  return
80
143
 
81
144
  self._session = aiohttp.ClientSession(
@@ -139,8 +202,12 @@ class HttpProvider:
139
202
  :param json_data: Optional JSON body.
140
203
  :return: Parsed response payload.
141
204
  """
142
- if not self.is_connected:
143
- raise NotConnectedError()
205
+ if not self.connected:
206
+ raise NotConnectedError(
207
+ component="HttpProvider",
208
+ endpoint=self._base_url,
209
+ operation=f"{method} {path}",
210
+ )
144
211
 
145
212
  assert self._session is not None
146
213
  url = f"{self._base_url}/{path.lstrip('/')}"
@@ -166,65 +233,13 @@ class HttpProvider:
166
233
  endpoint=url,
167
234
  operation="http request",
168
235
  ) from exc
169
-
170
236
  except aiohttp.ClientError as exc:
171
- raise ProviderResponseError(
172
- code=0,
173
- message=str(exc),
237
+ raise TransportError(
174
238
  endpoint=url,
239
+ operation="http request",
240
+ reason=str(exc),
175
241
  ) from exc
176
242
 
177
- async def send_http_request(
178
- self,
179
- method: str,
180
- path: str,
181
- *,
182
- params: t.Any = None,
183
- json_data: t.Any = None,
184
- ) -> t.Any:
185
- """Send an HTTP request with retry handling.
186
-
187
- On provider error, retries the request according to the retry policy
188
- matched by error code and message. If no rule matches, or retry attempts
189
- are exhausted, the error is raised.
190
-
191
- :param method: HTTP method.
192
- :param path: Endpoint path relative to base_url.
193
- :param params: Optional query parameters.
194
- :param json_data: Optional JSON body.
195
- :return: Parsed response payload.
196
- """
197
- attempts: t.Dict[int, int] = {}
198
-
199
- while True:
200
- try:
201
- return await self._send_once(
202
- method,
203
- path,
204
- params=params,
205
- json_data=json_data,
206
- )
207
- except ProviderResponseError as e:
208
- policy = self._retry_policy
209
- if policy is None:
210
- raise
211
-
212
- rule = policy.rule_for(e.code, e.message)
213
- if rule is None:
214
- raise
215
-
216
- key = id(rule)
217
- attempts[key] = attempts.get(key, 0) + 1
218
-
219
- if attempts[key] >= rule.attempts:
220
- raise RetryLimitError(
221
- attempts=attempts[key],
222
- max_attempts=rule.attempts,
223
- last_error=e,
224
- ) from e
225
-
226
- await asyncio.sleep(rule.delay(attempts[key] - 1))
227
-
228
243
  @classmethod
229
244
  def _detect_proxy_error(
230
245
  cls,
@@ -140,7 +140,7 @@ class RunGetMethodPayload(BaseModel):
140
140
  stack: t.List[t.Any]
141
141
 
142
142
 
143
- class RunGetMethodResul(BaseModel):
143
+ class RunGetMethodResult(BaseModel):
144
144
  """Response wrapper for /runGetMethod."""
145
145
 
146
146
  result: t.Optional[_GetMethod] = None
@@ -3,7 +3,6 @@ from __future__ import annotations
3
3
  import typing as t
4
4
 
5
5
  import aiohttp
6
- from pydantic import BaseModel
7
6
 
8
7
  from tonutils.clients.http.provider.base import HttpProvider
9
8
  from tonutils.clients.http.provider.models import (
@@ -50,10 +49,6 @@ class TonapiHttpProvider(HttpProvider):
50
49
  retry_policy=retry_policy,
51
50
  )
52
51
 
53
- @staticmethod
54
- def _model(model: t.Type[BaseModel], data: t.Any) -> t.Any:
55
- return model.model_validate(data)
56
-
57
52
  async def blockchain_message(
58
53
  self,
59
54
  payload: BlockchainMessagePayload,