web3 7.0.0b2__py3-none-any.whl → 7.7.0__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 (144) hide show
  1. ens/__init__.py +13 -2
  2. ens/_normalization.py +4 -4
  3. ens/async_ens.py +27 -15
  4. ens/base_ens.py +3 -1
  5. ens/contract_data.py +2 -2
  6. ens/ens.py +10 -7
  7. ens/exceptions.py +16 -29
  8. ens/specs/nf.json +1 -1
  9. ens/specs/normalization_spec.json +1 -1
  10. ens/utils.py +24 -32
  11. web3/__init__.py +23 -12
  12. web3/_utils/abi.py +157 -263
  13. web3/_utils/async_transactions.py +34 -20
  14. web3/_utils/batching.py +217 -0
  15. web3/_utils/blocks.py +6 -2
  16. web3/_utils/caching/__init__.py +12 -0
  17. web3/_utils/caching/caching_utils.py +433 -0
  18. web3/_utils/caching/request_caching_validation.py +287 -0
  19. web3/_utils/compat/__init__.py +2 -3
  20. web3/_utils/contract_sources/compile_contracts.py +1 -1
  21. web3/_utils/contract_sources/contract_data/ambiguous_function_contract.py +42 -0
  22. web3/_utils/contract_sources/contract_data/arrays_contract.py +3 -3
  23. web3/_utils/contract_sources/contract_data/bytes_contracts.py +5 -5
  24. web3/_utils/contract_sources/contract_data/constructor_contracts.py +7 -7
  25. web3/_utils/contract_sources/contract_data/contract_caller_tester.py +3 -3
  26. web3/_utils/contract_sources/contract_data/emitter_contract.py +3 -3
  27. web3/_utils/contract_sources/contract_data/event_contracts.py +50 -5
  28. web3/_utils/contract_sources/contract_data/extended_resolver.py +3 -3
  29. web3/_utils/contract_sources/contract_data/fallback_function_contract.py +3 -3
  30. web3/_utils/contract_sources/contract_data/function_name_tester_contract.py +3 -3
  31. web3/_utils/contract_sources/contract_data/math_contract.py +3 -3
  32. web3/_utils/contract_sources/contract_data/offchain_lookup.py +3 -3
  33. web3/_utils/contract_sources/contract_data/offchain_resolver.py +3 -3
  34. web3/_utils/contract_sources/contract_data/panic_errors_contract.py +3 -3
  35. web3/_utils/contract_sources/contract_data/payable_tester.py +3 -3
  36. web3/_utils/contract_sources/contract_data/receive_function_contracts.py +5 -5
  37. web3/_utils/contract_sources/contract_data/reflector_contracts.py +3 -3
  38. web3/_utils/contract_sources/contract_data/revert_contract.py +3 -3
  39. web3/_utils/contract_sources/contract_data/simple_resolver.py +3 -3
  40. web3/_utils/contract_sources/contract_data/storage_contract.py +3 -3
  41. web3/_utils/contract_sources/contract_data/string_contract.py +3 -3
  42. web3/_utils/contract_sources/contract_data/tuple_contracts.py +5 -5
  43. web3/_utils/contracts.py +172 -220
  44. web3/_utils/datatypes.py +5 -1
  45. web3/_utils/decorators.py +6 -1
  46. web3/_utils/empty.py +1 -1
  47. web3/_utils/encoding.py +16 -12
  48. web3/_utils/error_formatters_utils.py +5 -3
  49. web3/_utils/events.py +78 -72
  50. web3/_utils/fee_utils.py +1 -3
  51. web3/_utils/filters.py +24 -22
  52. web3/_utils/formatters.py +2 -2
  53. web3/_utils/http.py +8 -2
  54. web3/_utils/http_session_manager.py +314 -0
  55. web3/_utils/math.py +14 -15
  56. web3/_utils/method_formatters.py +161 -34
  57. web3/_utils/module.py +2 -1
  58. web3/_utils/module_testing/__init__.py +3 -2
  59. web3/_utils/module_testing/eth_module.py +736 -583
  60. web3/_utils/module_testing/go_ethereum_debug_module.py +128 -0
  61. web3/_utils/module_testing/module_testing_utils.py +81 -24
  62. web3/_utils/module_testing/persistent_connection_provider.py +702 -220
  63. web3/_utils/module_testing/utils.py +114 -33
  64. web3/_utils/module_testing/web3_module.py +438 -17
  65. web3/_utils/normalizers.py +13 -11
  66. web3/_utils/rpc_abi.py +10 -22
  67. web3/_utils/threads.py +8 -7
  68. web3/_utils/transactions.py +32 -25
  69. web3/_utils/type_conversion.py +5 -1
  70. web3/_utils/validation.py +20 -17
  71. web3/beacon/__init__.py +5 -0
  72. web3/beacon/api_endpoints.py +3 -0
  73. web3/beacon/async_beacon.py +29 -6
  74. web3/beacon/beacon.py +24 -6
  75. web3/contract/__init__.py +7 -0
  76. web3/contract/async_contract.py +285 -82
  77. web3/contract/base_contract.py +556 -258
  78. web3/contract/contract.py +295 -84
  79. web3/contract/utils.py +251 -55
  80. web3/datastructures.py +49 -34
  81. web3/eth/__init__.py +7 -0
  82. web3/eth/async_eth.py +89 -69
  83. web3/eth/base_eth.py +7 -3
  84. web3/eth/eth.py +43 -66
  85. web3/exceptions.py +158 -83
  86. web3/gas_strategies/time_based.py +8 -6
  87. web3/geth.py +53 -184
  88. web3/main.py +77 -17
  89. web3/manager.py +362 -95
  90. web3/method.py +43 -15
  91. web3/middleware/__init__.py +17 -0
  92. web3/middleware/attrdict.py +12 -22
  93. web3/middleware/base.py +55 -2
  94. web3/middleware/filter.py +45 -23
  95. web3/middleware/formatting.py +6 -3
  96. web3/middleware/names.py +4 -1
  97. web3/middleware/signing.py +15 -6
  98. web3/middleware/stalecheck.py +2 -1
  99. web3/module.py +61 -25
  100. web3/providers/__init__.py +21 -0
  101. web3/providers/async_base.py +87 -32
  102. web3/providers/base.py +77 -32
  103. web3/providers/eth_tester/__init__.py +5 -0
  104. web3/providers/eth_tester/defaults.py +2 -55
  105. web3/providers/eth_tester/main.py +41 -15
  106. web3/providers/eth_tester/middleware.py +16 -17
  107. web3/providers/ipc.py +41 -17
  108. web3/providers/legacy_websocket.py +26 -1
  109. web3/providers/persistent/__init__.py +7 -0
  110. web3/providers/persistent/async_ipc.py +61 -121
  111. web3/providers/persistent/persistent.py +323 -16
  112. web3/providers/persistent/persistent_connection.py +54 -5
  113. web3/providers/persistent/request_processor.py +136 -56
  114. web3/providers/persistent/subscription_container.py +56 -0
  115. web3/providers/persistent/subscription_manager.py +233 -0
  116. web3/providers/persistent/websocket.py +29 -92
  117. web3/providers/rpc/__init__.py +5 -0
  118. web3/providers/rpc/async_rpc.py +73 -18
  119. web3/providers/rpc/rpc.py +73 -30
  120. web3/providers/rpc/utils.py +1 -13
  121. web3/scripts/install_pre_releases.py +33 -0
  122. web3/scripts/parse_pygeth_version.py +16 -0
  123. web3/testing.py +4 -4
  124. web3/tracing.py +9 -5
  125. web3/types.py +141 -74
  126. web3/utils/__init__.py +64 -5
  127. web3/utils/abi.py +790 -10
  128. web3/utils/address.py +8 -0
  129. web3/utils/async_exception_handling.py +20 -11
  130. web3/utils/caching.py +34 -4
  131. web3/utils/exception_handling.py +9 -12
  132. web3/utils/subscriptions.py +285 -0
  133. {web3-7.0.0b2.dist-info → web3-7.7.0.dist-info}/LICENSE +1 -1
  134. web3-7.7.0.dist-info/METADATA +130 -0
  135. web3-7.7.0.dist-info/RECORD +171 -0
  136. {web3-7.0.0b2.dist-info → web3-7.7.0.dist-info}/WHEEL +1 -1
  137. web3/_utils/caching.py +0 -155
  138. web3/_utils/contract_sources/contract_data/address_reflector.py +0 -29
  139. web3/_utils/module_testing/go_ethereum_personal_module.py +0 -300
  140. web3/_utils/request.py +0 -265
  141. web3-7.0.0b2.dist-info/METADATA +0 -106
  142. web3-7.0.0b2.dist-info/RECORD +0 -163
  143. /web3/_utils/{function_identifiers.py → abi_element_identifiers.py} +0 -0
  144. {web3-7.0.0b2.dist-info → web3-7.7.0.dist-info}/top_level.txt +0 -0
@@ -2,9 +2,12 @@ from typing import (
2
2
  TYPE_CHECKING,
3
3
  Any,
4
4
  Dict,
5
+ Union,
6
+ cast,
5
7
  )
6
8
 
7
9
  from web3.types import (
10
+ FormattedEthSubscriptionResponse,
8
11
  RPCEndpoint,
9
12
  RPCResponse,
10
13
  )
@@ -16,6 +19,9 @@ if TYPE_CHECKING:
16
19
  from web3.manager import ( # noqa: F401
17
20
  _AsyncPersistentMessageStream,
18
21
  )
22
+ from web3.providers.persistent import ( # noqa: F401
23
+ PersistentConnectionProvider,
24
+ )
19
25
 
20
26
 
21
27
  class PersistentConnection:
@@ -26,17 +32,60 @@ class PersistentConnection:
26
32
 
27
33
  def __init__(self, w3: "AsyncWeb3"):
28
34
  self._manager = w3.manager
35
+ self.provider = cast("PersistentConnectionProvider", self._manager.provider)
29
36
 
30
- # -- public methods -- #
31
37
  @property
32
38
  def subscriptions(self) -> Dict[str, Any]:
39
+ """
40
+ Return the active subscriptions on the persistent connection.
41
+
42
+ :return: The active subscriptions on the persistent connection.
43
+ :rtype: Dict[str, Any]
44
+ """
33
45
  return self._manager._request_processor.active_subscriptions
34
46
 
35
- async def send(self, method: RPCEndpoint, params: Any) -> RPCResponse:
36
- return await self._manager.send(method, params)
47
+ async def make_request(self, method: RPCEndpoint, params: Any) -> RPCResponse:
48
+ """
49
+ Make a request to the persistent connection and return the response. This method
50
+ does not process the response as it would when invoking a method via the
51
+ appropriate module on the `AsyncWeb3` instance,
52
+ e.g. `w3.eth.get_block("latest")`.
53
+
54
+ :param method: The RPC method, e.g. `eth_getBlockByNumber`.
55
+ :param params: The RPC method parameters, e.g. `["0x1337", False]`.
56
+
57
+ :return: The unprocessed response from the persistent connection.
58
+ :rtype: RPCResponse
59
+ """
60
+ return await self.provider.make_request(method, params)
61
+
62
+ async def send(self, method: RPCEndpoint, params: Any) -> None:
63
+ """
64
+ Send a raw, unprocessed message to the persistent connection.
37
65
 
38
- async def recv(self) -> Any:
39
- return await self._manager._get_next_message()
66
+ :param method: The RPC method, e.g. `eth_getBlockByNumber`.
67
+ :param params: The RPC method parameters, e.g. `["0x1337", False]`.
68
+
69
+ :return: None
70
+ """
71
+ await self._manager.send(method, params)
72
+
73
+ async def recv(self) -> Union[RPCResponse, FormattedEthSubscriptionResponse]:
74
+ """
75
+ Receive the next unprocessed response for a request from the persistent
76
+ connection.
77
+
78
+ :return: The next unprocessed response for a request from the persistent
79
+ connection.
80
+ :rtype: Union[RPCResponse, FormattedEthSubscriptionResponse]
81
+ """
82
+ return await self._manager.recv()
40
83
 
41
84
  def process_subscriptions(self) -> "_AsyncPersistentMessageStream":
85
+ """
86
+ Asynchronous iterator that yields messages from the subscription message stream.
87
+
88
+ :return: The subscription message stream.
89
+ :rtype: _AsyncPersistentMessageStream
90
+ """
42
91
  return self._manager._persistent_message_stream()
@@ -1,22 +1,39 @@
1
1
  import asyncio
2
- from copy import (
3
- copy,
4
- )
2
+ import sys
5
3
  from typing import (
6
4
  TYPE_CHECKING,
7
5
  Any,
8
6
  Callable,
9
7
  Dict,
8
+ Generic,
10
9
  Optional,
11
10
  Tuple,
11
+ TypeVar,
12
+ Union,
13
+ )
14
+
15
+ from eth_utils.toolz import (
16
+ compose,
12
17
  )
13
18
 
19
+ from web3._utils.batching import (
20
+ BATCH_REQUEST_ID,
21
+ )
14
22
  from web3._utils.caching import (
15
23
  RequestInformation,
16
24
  generate_cache_key,
17
25
  )
26
+ from web3.exceptions import (
27
+ SubscriptionProcessingFinished,
28
+ TaskNotRunning,
29
+ Web3ValueError,
30
+ )
31
+ from web3.providers.persistent.subscription_manager import (
32
+ SubscriptionContainer,
33
+ )
18
34
  from web3.types import (
19
35
  RPCEndpoint,
36
+ RPCId,
20
37
  RPCResponse,
21
38
  )
22
39
  from web3.utils import (
@@ -28,22 +45,55 @@ if TYPE_CHECKING:
28
45
  PersistentConnectionProvider,
29
46
  )
30
47
 
48
+ T = TypeVar("T")
49
+
50
+ # TODO: This is an ugly hack for python 3.8. Remove this after we drop support for it
51
+ # and use `asyncio.Queue[T]` type directly in the `TaskReliantQueue` class.
52
+ if sys.version_info >= (3, 9):
53
+
54
+ class _TaskReliantQueue(asyncio.Queue[T], Generic[T]):
55
+ pass
56
+
57
+ else:
58
+
59
+ class _TaskReliantQueue(asyncio.Queue, Generic[T]): # type: ignore
60
+ pass
61
+
62
+
63
+ class TaskReliantQueue(_TaskReliantQueue[T]):
64
+ """
65
+ A queue that relies on a task to be running to process items in the queue.
66
+ """
67
+
68
+ async def get(self) -> T:
69
+ item = await super().get()
70
+ if isinstance(item, Exception):
71
+ # if the item is an exception, raise it so the task can handle this case
72
+ # more gracefully
73
+ raise item
74
+ return item
75
+
31
76
 
32
77
  class RequestProcessor:
33
78
  _subscription_queue_synced_with_ws_stream: bool = False
34
79
 
80
+ # set by the subscription manager when it is initialized
81
+ _subscription_container: Optional[SubscriptionContainer] = None
82
+
35
83
  def __init__(
36
84
  self,
37
85
  provider: "PersistentConnectionProvider",
38
86
  subscription_response_queue_size: int = 500,
39
87
  ) -> None:
40
88
  self._provider = provider
41
-
42
89
  self._request_information_cache: SimpleCache = SimpleCache(500)
43
90
  self._request_response_cache: SimpleCache = SimpleCache(500)
44
- self._subscription_response_queue: asyncio.Queue[RPCResponse] = asyncio.Queue(
45
- maxsize=subscription_response_queue_size
46
- )
91
+ self._subscription_response_queue: TaskReliantQueue[
92
+ Union[RPCResponse, TaskNotRunning]
93
+ ] = TaskReliantQueue(maxsize=subscription_response_queue_size)
94
+ self._handler_subscription_queue: TaskReliantQueue[
95
+ Union[RPCResponse, TaskNotRunning, SubscriptionProcessingFinished]
96
+ ] = TaskReliantQueue(maxsize=subscription_response_queue_size)
47
97
 
48
98
  @property
49
99
  def active_subscriptions(self) -> Dict[str, Any]:
@@ -57,9 +107,14 @@ class RequestProcessor:
57
107
 
58
108
  def cache_request_information(
59
109
  self,
110
+ request_id: Optional[RPCId],
60
111
  method: RPCEndpoint,
61
112
  params: Any,
62
- response_formatters: Tuple[Callable[..., Any], ...],
113
+ response_formatters: Tuple[
114
+ Union[Dict[str, Callable[..., Any]], Callable[..., Any]],
115
+ Callable[..., Any],
116
+ Callable[..., Any],
117
+ ],
63
118
  ) -> Optional[str]:
64
119
  cached_requests_key = generate_cache_key((method, params))
65
120
  if cached_requests_key in self._provider._request_cache._data:
@@ -73,13 +128,16 @@ class RequestProcessor:
73
128
  )
74
129
  return None
75
130
 
76
- # copy the request counter and find the next request id without incrementing
77
- # since this is done when / if the request is successfully sent
78
- request_id = next(copy(self._provider.request_counter))
79
- cache_key = generate_cache_key(request_id)
80
-
81
- self._bump_cache_if_key_present(cache_key, request_id)
131
+ if request_id is None:
132
+ if not self._provider._is_batching:
133
+ raise Web3ValueError(
134
+ "Request id must be provided when not batching requests."
135
+ )
136
+ # the _batch_request_counter is set when entering the context manager
137
+ request_id = self._provider._batch_request_counter
138
+ self._provider._batch_request_counter += 1
82
139
 
140
+ cache_key = generate_cache_key(request_id)
83
141
  request_info = RequestInformation(
84
142
  method,
85
143
  params,
@@ -95,30 +153,6 @@ class RequestProcessor:
95
153
  )
96
154
  return cache_key
97
155
 
98
- def _bump_cache_if_key_present(self, cache_key: str, request_id: int) -> None:
99
- """
100
- If the cache key is present in the cache, bump the cache key and request id
101
- by one to make room for the new request. This behavior is necessary when a
102
- request is made but inner requests, say to `eth_estimateGas` if the `gas` is
103
- missing, are made before the original request is sent.
104
- """
105
- if cache_key in self._request_information_cache:
106
- original_request_info = self._request_information_cache.get_cache_entry(
107
- cache_key
108
- )
109
- bump = generate_cache_key(request_id + 1)
110
-
111
- # recursively bump the cache if the new key is also present
112
- self._bump_cache_if_key_present(bump, request_id + 1)
113
-
114
- self._provider.logger.debug(
115
- "Caching internal request. Bumping original request in cache:\n"
116
- f" request_id=[{request_id}] -> [{request_id + 1}],\n"
117
- f" cache_key=[{cache_key}] -> [{bump}],\n"
118
- f" request_info={original_request_info.__dict__}"
119
- )
120
- self._request_information_cache.cache(bump, original_request_info)
121
-
122
156
  def pop_cached_request_information(
123
157
  self, cache_key: str
124
158
  ) -> Optional[RequestInformation]:
@@ -136,9 +170,9 @@ class RequestProcessor:
136
170
  ) -> RequestInformation:
137
171
  if "method" in response and response["method"] == "eth_subscription":
138
172
  if "params" not in response:
139
- raise ValueError("Subscription response must have params field")
173
+ raise Web3ValueError("Subscription response must have params field")
140
174
  if "subscription" not in response["params"]:
141
- raise ValueError(
175
+ raise Web3ValueError(
142
176
  "Subscription response params must have subscription field"
143
177
  )
144
178
 
@@ -150,9 +184,8 @@ class RequestProcessor:
150
184
  # i.e. subscription request information remains in the cache
151
185
  self._request_information_cache.get_cache_entry(cache_key)
152
186
  )
153
-
154
187
  else:
155
- # retrieve the request info from the cache using the request id
188
+ # retrieve the request info from the cache using the response id
156
189
  cache_key = generate_cache_key(response["id"])
157
190
  if response in self._provider._request_cache._data.values():
158
191
  request_info = (
@@ -181,6 +214,33 @@ class RequestProcessor:
181
214
 
182
215
  return request_info
183
216
 
217
+ def append_result_formatter_for_request(
218
+ self, request_id: int, result_formatter: Callable[..., Any]
219
+ ) -> None:
220
+ cache_key = generate_cache_key(request_id)
221
+ cached_request_info_for_id: RequestInformation = (
222
+ self._request_information_cache.get_cache_entry(cache_key)
223
+ )
224
+ if cached_request_info_for_id is not None:
225
+ (
226
+ current_result_formatters,
227
+ error_formatters,
228
+ null_result_formatters,
229
+ ) = cached_request_info_for_id.response_formatters
230
+ cached_request_info_for_id.response_formatters = (
231
+ compose(
232
+ result_formatter,
233
+ current_result_formatters,
234
+ ),
235
+ error_formatters,
236
+ null_result_formatters,
237
+ )
238
+ else:
239
+ self._provider.logger.debug(
240
+ f"No cached request info for response id `{request_id}`. Cannot "
241
+ f"append response formatter for response."
242
+ )
243
+
184
244
  def append_middleware_response_processor(
185
245
  self,
186
246
  response: RPCResponse,
@@ -215,7 +275,7 @@ class RequestProcessor:
215
275
  ) -> None:
216
276
  if subscription:
217
277
  if self._subscription_response_queue.full():
218
- self._provider.logger.info(
278
+ self._provider.logger.debug(
219
279
  "Subscription queue is full. Waiting for provider to consume "
220
280
  "messages before caching."
221
281
  )
@@ -225,7 +285,26 @@ class RequestProcessor:
225
285
  self._provider.logger.debug(
226
286
  f"Caching subscription response:\n response={raw_response}"
227
287
  )
228
- await self._subscription_response_queue.put(raw_response)
288
+ subscription_id = raw_response.get("params", {}).get("subscription")
289
+ sub_container = self._subscription_container
290
+ if sub_container and sub_container.get_handler_subscription_by_id(
291
+ subscription_id
292
+ ):
293
+ # if the subscription has a handler, put it in the handler queue
294
+ await self._handler_subscription_queue.put(raw_response)
295
+ else:
296
+ # otherwise, put it in the subscription response queue so a response
297
+ # can be yielded by the message stream
298
+ await self._subscription_response_queue.put(raw_response)
299
+ elif isinstance(raw_response, list):
300
+ # Since only one batch should be in the cache at all times, we use a
301
+ # constant cache key for the batch response.
302
+ cache_key = generate_cache_key(BATCH_REQUEST_ID)
303
+ self._provider.logger.debug(
304
+ f"Caching batch response:\n cache_key={cache_key},\n"
305
+ f" response={raw_response}"
306
+ )
307
+ self._request_response_cache.cache(cache_key, raw_response)
229
308
  else:
230
309
  response_id = raw_response.get("id")
231
310
  cache_key = generate_cache_key(response_id)
@@ -235,19 +314,17 @@ class RequestProcessor:
235
314
  )
236
315
  self._request_response_cache.cache(cache_key, raw_response)
237
316
 
238
- def pop_raw_response(
317
+ async def pop_raw_response(
239
318
  self, cache_key: str = None, subscription: bool = False
240
319
  ) -> Any:
241
320
  if subscription:
242
321
  qsize = self._subscription_response_queue.qsize()
243
- if qsize == 0:
244
- return None
322
+ raw_response = await self._subscription_response_queue.get()
245
323
 
246
324
  if not self._provider._listen_event.is_set():
247
325
  self._provider._listen_event.set()
248
326
 
249
- raw_response = self._subscription_response_queue.get_nowait()
250
- if qsize == 1:
327
+ if qsize == 0:
251
328
  if not self._subscription_queue_synced_with_ws_stream:
252
329
  self._subscription_queue_synced_with_ws_stream = True
253
330
  self._provider.logger.info(
@@ -268,7 +345,7 @@ class RequestProcessor:
268
345
  )
269
346
  else:
270
347
  if not cache_key:
271
- raise ValueError(
348
+ raise Web3ValueError(
272
349
  "Must provide cache key when popping a non-subscription response."
273
350
  )
274
351
 
@@ -282,15 +359,18 @@ class RequestProcessor:
282
359
 
283
360
  return raw_response
284
361
 
285
- # request processor class methods
362
+ # cache methods
286
363
 
287
- def clear_caches(self) -> None:
288
- """
289
- Clear the request processor caches.
290
- """
364
+ def _reset_handler_subscription_queue(self) -> None:
365
+ self._handler_subscription_queue = TaskReliantQueue(
366
+ maxsize=self._handler_subscription_queue.maxsize
367
+ )
291
368
 
369
+ def clear_caches(self) -> None:
370
+ """Clear the request processor caches."""
292
371
  self._request_information_cache.clear()
293
372
  self._request_response_cache.clear()
294
- self._subscription_response_queue = asyncio.Queue(
373
+ self._subscription_response_queue = TaskReliantQueue(
295
374
  maxsize=self._subscription_response_queue.maxsize
296
375
  )
376
+ self._reset_handler_subscription_queue()
@@ -0,0 +1,56 @@
1
+ from typing import (
2
+ Any,
3
+ Dict,
4
+ Iterator,
5
+ List,
6
+ Optional,
7
+ )
8
+
9
+ from eth_typing import (
10
+ HexStr,
11
+ )
12
+
13
+ from web3.utils import (
14
+ EthSubscription,
15
+ )
16
+
17
+
18
+ class SubscriptionContainer:
19
+ def __init__(self) -> None:
20
+ self.subscriptions: List[EthSubscription[Any]] = []
21
+ self.subscriptions_by_id: Dict[HexStr, EthSubscription[Any]] = {}
22
+ self.subscriptions_by_label: Dict[str, EthSubscription[Any]] = {}
23
+
24
+ def __len__(self) -> int:
25
+ return len(self.subscriptions)
26
+
27
+ def __iter__(self) -> Iterator[EthSubscription[Any]]:
28
+ return iter(self.subscriptions)
29
+
30
+ def add_subscription(self, subscription: EthSubscription[Any]) -> None:
31
+ self.subscriptions.append(subscription)
32
+ self.subscriptions_by_id[subscription.id] = subscription
33
+ self.subscriptions_by_label[subscription.label] = subscription
34
+
35
+ def remove_subscription(self, subscription: EthSubscription[Any]) -> None:
36
+ self.subscriptions.remove(subscription)
37
+ self.subscriptions_by_id.pop(subscription.id)
38
+ self.subscriptions_by_label.pop(subscription.label)
39
+
40
+ def get_by_id(self, sub_id: HexStr) -> EthSubscription[Any]:
41
+ return self.subscriptions_by_id.get(sub_id)
42
+
43
+ def get_by_label(self, label: str) -> EthSubscription[Any]:
44
+ return self.subscriptions_by_label.get(label)
45
+
46
+ @property
47
+ def handler_subscriptions(self) -> List[EthSubscription[Any]]:
48
+ return [sub for sub in self.subscriptions if sub._handler is not None]
49
+
50
+ def get_handler_subscription_by_id(
51
+ self, sub_id: HexStr
52
+ ) -> Optional[EthSubscription[Any]]:
53
+ sub = self.get_by_id(sub_id)
54
+ if sub and sub._handler:
55
+ return sub
56
+ return None
@@ -0,0 +1,233 @@
1
+ import asyncio
2
+ import logging
3
+ from typing import (
4
+ TYPE_CHECKING,
5
+ Any,
6
+ List,
7
+ Sequence,
8
+ Union,
9
+ cast,
10
+ overload,
11
+ )
12
+
13
+ from eth_typing import (
14
+ HexStr,
15
+ )
16
+
17
+ from web3.exceptions import (
18
+ SubscriptionProcessingFinished,
19
+ TaskNotRunning,
20
+ Web3TypeError,
21
+ Web3ValueError,
22
+ )
23
+ from web3.providers.persistent.subscription_container import (
24
+ SubscriptionContainer,
25
+ )
26
+ from web3.types import (
27
+ FormattedEthSubscriptionResponse,
28
+ RPCResponse,
29
+ )
30
+ from web3.utils.subscriptions import (
31
+ EthSubscription,
32
+ EthSubscriptionContext,
33
+ )
34
+
35
+ if TYPE_CHECKING:
36
+ from web3 import AsyncWeb3 # noqa: F401
37
+ from web3.providers.persistent import ( # noqa: F401
38
+ PersistentConnectionProvider,
39
+ RequestProcessor,
40
+ )
41
+
42
+
43
+ class SubscriptionManager:
44
+ """
45
+ The ``SubscriptionManager`` is responsible for subscribing, unsubscribing, and
46
+ managing all active subscriptions for an ``AsyncWeb3`` instance. It is also
47
+ used for processing all subscriptions that have handler functions.
48
+ """
49
+
50
+ logger: logging.Logger = logging.getLogger(
51
+ "web3.providers.persistent.subscription_manager"
52
+ )
53
+ total_handler_calls: int = 0
54
+
55
+ def __init__(self, w3: "AsyncWeb3") -> None:
56
+ self._w3 = w3
57
+ self._provider = cast("PersistentConnectionProvider", w3.provider)
58
+ self._subscription_container = SubscriptionContainer()
59
+
60
+ # share the subscription container with the request processor so it can separate
61
+ # subscriptions into different queues based on ``sub._handler`` presence
62
+ self._provider._request_processor._subscription_container = (
63
+ self._subscription_container
64
+ )
65
+
66
+ def _add_subscription(self, subscription: EthSubscription[Any]) -> None:
67
+ self._subscription_container.add_subscription(subscription)
68
+
69
+ def _remove_subscription(self, subscription: EthSubscription[Any]) -> None:
70
+ self._subscription_container.remove_subscription(subscription)
71
+
72
+ @property
73
+ def subscriptions(self) -> List[EthSubscription[Any]]:
74
+ return self._subscription_container.subscriptions
75
+
76
+ def get_by_id(self, sub_id: HexStr) -> EthSubscription[Any]:
77
+ return self._subscription_container.get_by_id(sub_id)
78
+
79
+ def get_by_label(self, label: str) -> EthSubscription[Any]:
80
+ return self._subscription_container.get_by_label(label)
81
+
82
+ @overload
83
+ async def subscribe(self, subscriptions: EthSubscription[Any]) -> HexStr:
84
+ ...
85
+
86
+ @overload
87
+ async def subscribe(
88
+ self, subscriptions: Sequence[EthSubscription[Any]]
89
+ ) -> List[HexStr]:
90
+ ...
91
+
92
+ async def subscribe(
93
+ self, subscriptions: Union[EthSubscription[Any], Sequence[EthSubscription[Any]]]
94
+ ) -> Union[HexStr, List[HexStr]]:
95
+ """
96
+ Used to subscribe to a single or multiple subscriptions.
97
+
98
+ :param subscriptions: A single subscription or a sequence of subscriptions.
99
+ :type subscriptions: Union[EthSubscription, Sequence[EthSubscription]]
100
+ :return:
101
+ """
102
+ if isinstance(subscriptions, EthSubscription):
103
+ if (
104
+ subscriptions.label
105
+ in self._subscription_container.subscriptions_by_label
106
+ ):
107
+ raise Web3ValueError(
108
+ "Subscription label already exists. Subscriptions must have "
109
+ f"unique labels.\n label: {subscriptions.label}"
110
+ )
111
+
112
+ subscriptions.manager = self
113
+ sub_id = await self._w3.eth._subscribe(*subscriptions.subscription_params)
114
+ subscriptions._id = sub_id
115
+ self._add_subscription(subscriptions)
116
+ self.logger.info(
117
+ "Successfully subscribed to subscription:\n "
118
+ f"label: {subscriptions.label}\n id: {sub_id}"
119
+ )
120
+ return sub_id
121
+ elif isinstance(subscriptions, Sequence):
122
+ if len(subscriptions) == 0:
123
+ raise Web3ValueError("No subscriptions provided.")
124
+
125
+ sub_ids: List[HexStr] = []
126
+ for sub in subscriptions:
127
+ await self.subscribe(sub)
128
+ return sub_ids
129
+ raise Web3TypeError("Expected a Subscription or a sequence of Subscriptions.")
130
+
131
+ async def unsubscribe(self, subscription: EthSubscription[Any]) -> bool:
132
+ """
133
+ Used to unsubscribe from a subscription.
134
+
135
+ :param subscription: The subscription to unsubscribe from.
136
+ :type subscription: EthSubscription
137
+ :return: ``True`` if unsubscribing was successful, ``False`` otherwise.
138
+ :rtype: bool
139
+ """
140
+ if subscription not in self.subscriptions:
141
+ raise Web3ValueError(
142
+ "Subscription not found or is not being managed by the subscription "
143
+ f"manager.\n label: {subscription.label}\n id: {subscription._id}"
144
+ )
145
+ if await self._w3.eth._unsubscribe(subscription.id):
146
+ self._remove_subscription(subscription)
147
+ self.logger.info(
148
+ "Successfully unsubscribed from subscription:\n "
149
+ f"label: {subscription.label}\n id: {subscription.id}"
150
+ )
151
+ if len(self._subscription_container.handler_subscriptions) == 0:
152
+ queue = self._provider._request_processor._handler_subscription_queue
153
+ await queue.put(SubscriptionProcessingFinished())
154
+ return True
155
+ return False
156
+
157
+ async def unsubscribe_all(self) -> bool:
158
+ """
159
+ Used to unsubscribe from all subscriptions that are being managed by the
160
+ subscription manager.
161
+
162
+ :return: ``True`` if unsubscribing was successful, ``False`` otherwise.
163
+ :rtype: bool
164
+ """
165
+ unsubscribed = [
166
+ await self.unsubscribe(sub) for sub in self.subscriptions.copy()
167
+ ]
168
+ if all(unsubscribed):
169
+ self.logger.info("Successfully unsubscribed from all subscriptions.")
170
+ return True
171
+ else:
172
+ if len(self.subscriptions) > 0:
173
+ self.logger.warning(
174
+ "Failed to unsubscribe from all subscriptions. Some subscriptions "
175
+ f"are still active.\n subscriptions={self.subscriptions}"
176
+ )
177
+ return False
178
+
179
+ async def handle_subscriptions(self, run_forever: bool = False) -> None:
180
+ """
181
+ Used to handle all subscriptions that have handlers. The method will run until
182
+ all subscriptions that have handler functions are unsubscribed from or, if
183
+ ``run_forever`` is set to ``True``, it will run indefinitely.
184
+
185
+ :param run_forever: If ``True``, the method will run indefinitely.
186
+ :type run_forever: bool
187
+ :return: None
188
+ """
189
+ if not self._subscription_container.handler_subscriptions:
190
+ self.logger.warning(
191
+ "No handler subscriptions found. Subscription handler did not run."
192
+ )
193
+ return
194
+
195
+ queue = self._provider._request_processor._handler_subscription_queue
196
+ try:
197
+ while run_forever or self._subscription_container.handler_subscriptions:
198
+ response = cast(RPCResponse, await queue.get())
199
+ formatted_sub_response = cast(
200
+ FormattedEthSubscriptionResponse,
201
+ await self._w3.manager._process_response(response),
202
+ )
203
+
204
+ # if the subscription was unsubscribed from, the response won't be
205
+ # formatted because we lost the request information
206
+ sub_id = formatted_sub_response.get("subscription")
207
+ sub = self._subscription_container.get_handler_subscription_by_id(
208
+ sub_id
209
+ )
210
+ if sub:
211
+ await sub._handler(
212
+ EthSubscriptionContext(
213
+ self._w3,
214
+ sub,
215
+ formatted_sub_response["result"],
216
+ **sub._handler_context,
217
+ )
218
+ )
219
+ except SubscriptionProcessingFinished:
220
+ self.logger.info(
221
+ "All handler subscriptions have been unsubscribed from. "
222
+ "Stopping subscription handling."
223
+ )
224
+ except TaskNotRunning:
225
+ await asyncio.sleep(0)
226
+ self._provider._handle_listener_task_exceptions()
227
+ self.logger.error(
228
+ "Message listener background task for the provider has stopped "
229
+ "unexpectedly. Stopping subscription handling."
230
+ )
231
+
232
+ # no active handler subscriptions, clear the handler subscription queue
233
+ self._provider._request_processor._reset_handler_subscription_queue()