web3 7.0.0b7__py3-none-any.whl → 7.0.0b9__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 (50) hide show
  1. ens/async_ens.py +16 -7
  2. ens/base_ens.py +3 -1
  3. ens/exceptions.py +2 -7
  4. ens/utils.py +0 -17
  5. web3/_utils/abi.py +100 -257
  6. web3/_utils/compat/__init__.py +1 -0
  7. web3/_utils/contracts.py +116 -205
  8. web3/_utils/encoding.py +4 -5
  9. web3/_utils/events.py +28 -33
  10. web3/_utils/fee_utils.py +2 -2
  11. web3/_utils/filters.py +2 -5
  12. web3/_utils/http_session_manager.py +30 -7
  13. web3/_utils/method_formatters.py +21 -3
  14. web3/_utils/module_testing/eth_module.py +61 -39
  15. web3/_utils/module_testing/module_testing_utils.py +51 -10
  16. web3/_utils/module_testing/persistent_connection_provider.py +46 -16
  17. web3/_utils/module_testing/web3_module.py +8 -8
  18. web3/_utils/normalizers.py +10 -8
  19. web3/_utils/validation.py +5 -7
  20. web3/contract/async_contract.py +18 -17
  21. web3/contract/base_contract.py +116 -80
  22. web3/contract/contract.py +16 -17
  23. web3/contract/utils.py +86 -55
  24. web3/eth/async_eth.py +1 -2
  25. web3/eth/eth.py +1 -2
  26. web3/exceptions.py +28 -9
  27. web3/gas_strategies/time_based.py +4 -0
  28. web3/manager.py +68 -23
  29. web3/middleware/filter.py +3 -3
  30. web3/middleware/signing.py +6 -1
  31. web3/module.py +1 -1
  32. web3/providers/persistent/async_ipc.py +34 -79
  33. web3/providers/persistent/persistent.py +76 -7
  34. web3/providers/persistent/persistent_connection.py +47 -5
  35. web3/providers/persistent/websocket.py +19 -59
  36. web3/types.py +5 -45
  37. web3/utils/__init__.py +48 -4
  38. web3/utils/abi.py +575 -10
  39. web3/utils/caching.py +24 -0
  40. {web3-7.0.0b7.dist-info → web3-7.0.0b9.dist-info}/METADATA +14 -8
  41. {web3-7.0.0b7.dist-info → web3-7.0.0b9.dist-info}/RECORD +45 -50
  42. {web3-7.0.0b7.dist-info → web3-7.0.0b9.dist-info}/WHEEL +1 -1
  43. web3/tools/benchmark/__init__.py +0 -0
  44. web3/tools/benchmark/main.py +0 -190
  45. web3/tools/benchmark/node.py +0 -120
  46. web3/tools/benchmark/reporting.py +0 -39
  47. web3/tools/benchmark/utils.py +0 -69
  48. /web3/_utils/{function_identifiers.py → abi_element_identifiers.py} +0 -0
  49. {web3-7.0.0b7.dist-info → web3-7.0.0b9.dist-info}/LICENSE +0 -0
  50. {web3-7.0.0b7.dist-info → web3-7.0.0b9.dist-info}/top_level.txt +0 -0
web3/manager.py CHANGED
@@ -20,9 +20,6 @@ from eth_utils.toolz import (
20
20
  from hexbytes import (
21
21
  HexBytes,
22
22
  )
23
- from websockets.exceptions import (
24
- ConnectionClosedOK,
25
- )
26
23
 
27
24
  from web3._utils.batching import (
28
25
  RequestBatcher,
@@ -40,6 +37,7 @@ from web3.exceptions import (
40
37
  BadResponseFormat,
41
38
  MethodUnavailable,
42
39
  ProviderConnectionError,
40
+ RequestTimedOut,
43
41
  TaskNotRunning,
44
42
  Web3RPCError,
45
43
  Web3TypeError,
@@ -92,6 +90,13 @@ if TYPE_CHECKING:
92
90
 
93
91
 
94
92
  NULL_RESPONSES = [None, HexBytes("0x"), "0x"]
93
+ KNOWN_REQUEST_TIMEOUT_MESSAGING = {
94
+ # Note: It's important to be very explicit here and not too broad. We don't want
95
+ # to accidentally catch a message that is not for a request timeout. In the worst
96
+ # case, we raise something more generic like `Web3RPCError`. JSON-RPC unfortunately
97
+ # has not standardized error codes for request timeouts.
98
+ "request timed out", # go-ethereum
99
+ }
95
100
  METHOD_NOT_FOUND = -32601
96
101
 
97
102
 
@@ -188,6 +193,7 @@ def _validate_response(
188
193
  response, 'Response must include either "error" or "result".'
189
194
  )
190
195
  elif "error" in response:
196
+ web3_rpc_error: Optional[Web3RPCError] = None
191
197
  error = response["error"]
192
198
 
193
199
  # raise the error when the value is a string
@@ -198,6 +204,13 @@ def _validate_response(
198
204
  "JSON-RPC 2.0 specification.",
199
205
  )
200
206
 
207
+ # errors must include a message
208
+ error_message = error.get("message")
209
+ if not isinstance(error_message, str):
210
+ _raise_bad_response_format(
211
+ response, 'error["message"] is required and must be a string value.'
212
+ )
213
+
201
214
  # errors must include an integer code
202
215
  code = error.get("code")
203
216
  if not isinstance(code, int):
@@ -205,7 +218,7 @@ def _validate_response(
205
218
  response, 'error["code"] is required and must be an integer value.'
206
219
  )
207
220
  elif code == METHOD_NOT_FOUND:
208
- exception = MethodUnavailable(
221
+ web3_rpc_error = MethodUnavailable(
209
222
  repr(error),
210
223
  rpc_response=response,
211
224
  user_message=(
@@ -214,20 +227,26 @@ def _validate_response(
214
227
  "currently enabled."
215
228
  ),
216
229
  )
217
- logger.error(exception.user_message)
218
- logger.debug(f"RPC error response: {response}")
219
- raise exception
220
-
221
- # errors must include a message
222
- error_message = error.get("message")
223
- if not isinstance(error_message, str):
224
- _raise_bad_response_format(
225
- response, 'error["message"] is required and must be a string value.'
230
+ elif any(
231
+ # parse specific timeout messages
232
+ timeout_str in error_message.lower()
233
+ for timeout_str in KNOWN_REQUEST_TIMEOUT_MESSAGING
234
+ ):
235
+ web3_rpc_error = RequestTimedOut(
236
+ repr(error),
237
+ rpc_response=response,
238
+ user_message=(
239
+ "The request timed out. Check the connection to your node and "
240
+ "try again."
241
+ ),
226
242
  )
227
243
 
228
- apply_error_formatters(error_formatters, response)
244
+ if web3_rpc_error is None:
245
+ # if no condition was met above, raise a more generic `Web3RPCError`
246
+ web3_rpc_error = Web3RPCError(repr(error), rpc_response=response)
247
+
248
+ response = apply_error_formatters(error_formatters, response)
229
249
 
230
- web3_rpc_error = Web3RPCError(repr(error), rpc_response=response)
231
250
  logger.error(web3_rpc_error.user_message)
232
251
  logger.debug(f"RPC error response: {response}")
233
252
  raise web3_rpc_error
@@ -472,22 +491,50 @@ class RequestManager:
472
491
 
473
492
  # -- persistent connection -- #
474
493
 
475
- async def send(self, method: RPCEndpoint, params: Any) -> RPCResponse:
494
+ async def socket_request(self, method: RPCEndpoint, params: Any) -> RPCResponse:
476
495
  provider = cast(PersistentConnectionProvider, self._provider)
477
496
  request_func = await provider.request_func(
478
497
  cast("AsyncWeb3", self.w3), cast("MiddlewareOnion", self.middleware_onion)
479
498
  )
480
499
  self.logger.debug(
481
- "Making request to open socket connection: "
500
+ "Making request to open socket connection and waiting for response: "
482
501
  f"{provider.get_endpoint_uri_or_ipc_path()}, method: {method}"
483
502
  )
484
503
  response = await request_func(method, params)
485
504
  return await self._process_response(response)
486
505
 
506
+ async def send(self, method: RPCEndpoint, params: Any) -> None:
507
+ provider = cast(PersistentConnectionProvider, self._provider)
508
+ # run through the request processors of the middleware
509
+ for mw_class in self.middleware_onion.as_tuple_of_middleware():
510
+ mw = mw_class(self.w3)
511
+ method, params = mw.request_processor(method, params)
512
+
513
+ self.logger.debug(
514
+ "Sending request to open socket connection: "
515
+ f"{provider.get_endpoint_uri_or_ipc_path()}, method: {method}"
516
+ )
517
+ await provider.socket_send(provider.encode_rpc_request(method, params))
518
+
519
+ async def recv(self) -> RPCResponse:
520
+ provider = cast(PersistentConnectionProvider, self._provider)
521
+ self.logger.debug(
522
+ "Getting next response from open socket connection: "
523
+ f"{provider.get_endpoint_uri_or_ipc_path()}"
524
+ )
525
+ # pop from the queue since the listener task is responsible for reading
526
+ # directly from the socket
527
+ request_response_cache = self._request_processor._request_response_cache
528
+ _key, response = await request_response_cache.async_await_and_popitem(
529
+ last=False,
530
+ timeout=provider.request_timeout,
531
+ )
532
+ return await self._process_response(response)
533
+
487
534
  def _persistent_message_stream(self) -> "_AsyncPersistentMessageStream":
488
535
  return _AsyncPersistentMessageStream(self)
489
536
 
490
- async def _get_next_message(self) -> Any:
537
+ async def _get_next_message(self) -> RPCResponse:
491
538
  return await self._message_stream().__anext__()
492
539
 
493
540
  async def _message_stream(self) -> AsyncGenerator[RPCResponse, None]:
@@ -515,12 +562,13 @@ class RequestManager:
515
562
  # if response is an active subscription response, process it
516
563
  yield await self._process_response(response)
517
564
  except TaskNotRunning:
565
+ await asyncio.sleep(0)
518
566
  self._provider._handle_listener_task_exceptions()
519
567
  self.logger.error(
520
568
  "Message listener background task has stopped unexpectedly. "
521
569
  "Stopping message stream."
522
570
  )
523
- raise StopAsyncIteration
571
+ return
524
572
 
525
573
  async def _process_response(self, response: RPCResponse) -> RPCResponse:
526
574
  provider = cast(PersistentConnectionProvider, self._provider)
@@ -586,7 +634,4 @@ class _AsyncPersistentMessageStream:
586
634
  return self
587
635
 
588
636
  async def __anext__(self) -> RPCResponse:
589
- try:
590
- return await self.manager._get_next_message()
591
- except ConnectionClosedOK:
592
- raise StopAsyncIteration
637
+ return await self.manager._get_next_message()
web3/middleware/filter.py CHANGED
@@ -167,7 +167,7 @@ def iter_latest_block(
167
167
 
168
168
  while True:
169
169
  latest_block = w3.eth.block_number
170
- if is_bounded_range and latest_block > to_block:
170
+ if is_bounded_range and latest_block > cast(BlockNumber, to_block):
171
171
  yield None
172
172
  # No new blocks since last iteration.
173
173
  if _last is not None and _last == latest_block:
@@ -255,7 +255,7 @@ class RequestLogs:
255
255
  if from_block is None or from_block == "latest":
256
256
  self._from_block = BlockNumber(w3.eth.block_number + 1)
257
257
  elif is_string(from_block) and is_hex(from_block):
258
- self._from_block = BlockNumber(hex_to_integer(from_block))
258
+ self._from_block = BlockNumber(hex_to_integer(cast(HexStr, from_block)))
259
259
  else:
260
260
  self._from_block = from_block
261
261
  self._to_block = to_block
@@ -272,7 +272,7 @@ class RequestLogs:
272
272
  elif self._to_block == "latest":
273
273
  to_block = self.w3.eth.block_number
274
274
  elif is_string(self._to_block) and is_hex(self._to_block):
275
- to_block = BlockNumber(hex_to_integer(self._to_block))
275
+ to_block = BlockNumber(hex_to_integer(cast(HexStr, self._to_block)))
276
276
  else:
277
277
  to_block = self._to_block
278
278
 
@@ -19,6 +19,9 @@ from eth_account import (
19
19
  from eth_account.signers.local import (
20
20
  LocalAccount,
21
21
  )
22
+ from eth_account.types import (
23
+ TransactionDictType as EthAccountTxParams,
24
+ )
22
25
  from eth_keys.datatypes import (
23
26
  PrivateKey,
24
27
  )
@@ -211,7 +214,9 @@ class SignAndSendRawMiddlewareBuilder(Web3MiddlewareBuilder):
211
214
  return method, params
212
215
  else:
213
216
  account = self._accounts[to_checksum_address(tx_from)]
214
- raw_tx = account.sign_transaction(filled_transaction).raw_transaction
217
+ raw_tx = account.sign_transaction(
218
+ cast(EthAccountTxParams, filled_transaction)
219
+ ).raw_transaction
215
220
 
216
221
  return (
217
222
  RPCEndpoint("eth_sendRawTransaction"),
web3/module.py CHANGED
@@ -138,7 +138,7 @@ def retrieve_async_method_call_fn(
138
138
 
139
139
  try:
140
140
  method_str = cast(RPCEndpoint, method_str)
141
- return await async_w3.manager.send(method_str, params)
141
+ return await async_w3.manager.socket_request(method_str, params)
142
142
  except Exception as e:
143
143
  if (
144
144
  cache_key is not None
@@ -11,11 +11,9 @@ from pathlib import (
11
11
  import sys
12
12
  from typing import (
13
13
  Any,
14
- List,
15
14
  Optional,
16
15
  Tuple,
17
16
  Union,
18
- cast,
19
17
  )
20
18
 
21
19
  from eth_utils import (
@@ -23,20 +21,12 @@ from eth_utils import (
23
21
  )
24
22
 
25
23
  from web3.types import (
26
- RPCEndpoint,
27
24
  RPCResponse,
28
25
  )
29
26
 
30
27
  from . import (
31
28
  PersistentConnectionProvider,
32
29
  )
33
- from ..._utils.batching import (
34
- BATCH_REQUEST_ID,
35
- sort_batch_response_by_response_ids,
36
- )
37
- from ..._utils.caching import (
38
- async_handle_request_caching,
39
- )
40
30
  from ...exceptions import (
41
31
  ProviderConnectionError,
42
32
  Web3TypeError,
@@ -91,12 +81,7 @@ class AsyncIPCProvider(PersistentConnectionProvider):
91
81
  return False
92
82
 
93
83
  try:
94
- request_data = self.encode_rpc_request(
95
- RPCEndpoint("web3_clientVersions"), []
96
- )
97
- self._writer.write(request_data)
98
- current_request_id = json.loads(request_data)["id"]
99
- await self._get_response_for_request_id(current_request_id, timeout=2)
84
+ await self.make_request("web3_clientVersion", [])
100
85
  return True
101
86
  except (OSError, ProviderConnectionError) as e:
102
87
  if show_traceback:
@@ -105,55 +90,33 @@ class AsyncIPCProvider(PersistentConnectionProvider):
105
90
  )
106
91
  return False
107
92
 
108
- async def _provider_specific_connect(self) -> None:
109
- self._reader, self._writer = await async_get_ipc_socket(self.ipc_path)
110
-
111
- async def _provider_specific_disconnect(self) -> None:
112
- if self._writer and not self._writer.is_closing():
113
- self._writer.close()
114
- await self._writer.wait_closed()
115
- self._writer = None
116
- if self._reader:
117
- self._reader = None
118
-
119
- async def _reset_socket(self) -> None:
120
- self._writer.close()
121
- await self._writer.wait_closed()
122
- self._reader, self._writer = await async_get_ipc_socket(self.ipc_path)
123
-
124
- @async_handle_request_caching
125
- async def make_request(self, method: RPCEndpoint, params: Any) -> RPCResponse:
93
+ async def socket_send(self, request_data: bytes) -> None:
126
94
  if self._writer is None:
127
95
  raise ProviderConnectionError(
128
96
  "Connection to ipc socket has not been initiated for the provider."
129
97
  )
130
98
 
131
- request_data = self.encode_rpc_request(method, params)
132
- try:
133
- self._writer.write(request_data)
134
- await self._writer.drain()
135
- except OSError as e:
136
- # Broken pipe
137
- if e.errno == errno.EPIPE:
138
- # one extra attempt, then give up
139
- await self._reset_socket()
140
- self._writer.write(request_data)
141
- await self._writer.drain()
99
+ return await asyncio.wait_for(
100
+ self._socket_send(request_data), timeout=self.request_timeout
101
+ )
142
102
 
143
- current_request_id = json.loads(request_data)["id"]
144
- response = await self._get_response_for_request_id(current_request_id)
103
+ async def socket_recv(self) -> RPCResponse:
104
+ while True:
105
+ # yield to the event loop to allow other tasks to run
106
+ await asyncio.sleep(0)
145
107
 
146
- return response
108
+ try:
109
+ response, pos = self._decoder.raw_decode(self._raw_message)
110
+ self._raw_message = self._raw_message[pos:].lstrip()
111
+ return response
112
+ except JSONDecodeError:
113
+ # read more data from the socket if the current raw message is
114
+ # incomplete
115
+ self._raw_message += to_text(await self._reader.read(4096)).lstrip()
147
116
 
148
- async def make_batch_request(
149
- self, requests: List[Tuple[RPCEndpoint, Any]]
150
- ) -> List[RPCResponse]:
151
- if self._writer is None:
152
- raise ProviderConnectionError(
153
- "Connection to ipc socket has not been initiated for the provider."
154
- )
117
+ # -- private methods -- #
155
118
 
156
- request_data = self.encode_batch_rpc_request(requests)
119
+ async def _socket_send(self, request_data: bytes) -> None:
157
120
  try:
158
121
  self._writer.write(request_data)
159
122
  await self._writer.drain()
@@ -165,32 +128,24 @@ class AsyncIPCProvider(PersistentConnectionProvider):
165
128
  self._writer.write(request_data)
166
129
  await self._writer.drain()
167
130
 
168
- response = cast(
169
- List[RPCResponse], await self._get_response_for_request_id(BATCH_REQUEST_ID)
170
- )
171
- return response
172
-
173
- async def _provider_specific_message_listener(self) -> None:
174
- self._raw_message += to_text(await self._reader.read(4096)).lstrip()
131
+ async def _reset_socket(self) -> None:
132
+ self._writer.close()
133
+ await self._writer.wait_closed()
134
+ self._reader, self._writer = await async_get_ipc_socket(self.ipc_path)
175
135
 
176
- while self._raw_message:
177
- try:
178
- response, pos = self._decoder.raw_decode(self._raw_message)
179
- except JSONDecodeError:
180
- break
136
+ async def _provider_specific_connect(self) -> None:
137
+ self._reader, self._writer = await async_get_ipc_socket(self.ipc_path)
181
138
 
182
- if isinstance(response, list):
183
- response = sort_batch_response_by_response_ids(response)
139
+ async def _provider_specific_disconnect(self) -> None:
140
+ if self._writer and not self._writer.is_closing():
141
+ self._writer.close()
142
+ await self._writer.wait_closed()
143
+ self._writer = None
144
+ if self._reader:
145
+ self._reader = None
184
146
 
185
- is_subscription = (
186
- response.get("method") == "eth_subscription"
187
- if not isinstance(response, list)
188
- else False
189
- )
190
- await self._request_processor.cache_raw_response(
191
- response, subscription=is_subscription
192
- )
193
- self._raw_message = self._raw_message[pos:].lstrip()
147
+ async def _provider_specific_socket_reader(self) -> RPCResponse:
148
+ return await self.socket_recv()
194
149
 
195
150
  def _error_log_listener_task_exception(self, e: Exception) -> None:
196
151
  super()._error_log_listener_task_exception(e)
@@ -1,13 +1,17 @@
1
1
  from abc import (
2
2
  ABC,
3
+ abstractmethod,
3
4
  )
4
5
  import asyncio
6
+ import json
5
7
  import logging
6
8
  from typing import (
7
9
  Any,
8
10
  List,
9
11
  Optional,
12
+ Tuple,
10
13
  Union,
14
+ cast,
11
15
  )
12
16
 
13
17
  from websockets import (
@@ -15,13 +19,20 @@ from websockets import (
15
19
  WebSocketException,
16
20
  )
17
21
 
22
+ from web3._utils.batching import (
23
+ BATCH_REQUEST_ID,
24
+ sort_batch_response_by_response_ids,
25
+ )
18
26
  from web3._utils.caching import (
27
+ async_handle_request_caching,
19
28
  generate_cache_key,
20
29
  )
21
30
  from web3.exceptions import (
31
+ PersistentConnectionClosedOK,
22
32
  ProviderConnectionError,
23
33
  TaskNotRunning,
24
34
  TimeExhausted,
35
+ Web3AttributeError,
25
36
  )
26
37
  from web3.providers.async_base import (
27
38
  AsyncJSONBaseProvider,
@@ -30,6 +41,7 @@ from web3.providers.persistent.request_processor import (
30
41
  RequestProcessor,
31
42
  )
32
43
  from web3.types import (
44
+ RPCEndpoint,
33
45
  RPCId,
34
46
  RPCResponse,
35
47
  )
@@ -70,7 +82,7 @@ class PersistentConnectionProvider(AsyncJSONBaseProvider, ABC):
70
82
  elif hasattr(self, "ipc_path"):
71
83
  return str(self.ipc_path)
72
84
  else:
73
- raise AttributeError(
85
+ raise Web3AttributeError(
74
86
  "`PersistentConnectionProvider` must have either `endpoint_uri` or "
75
87
  "`ipc_path` attribute."
76
88
  )
@@ -128,6 +140,44 @@ class PersistentConnectionProvider(AsyncJSONBaseProvider, ABC):
128
140
  f"Successfully disconnected from: {self.get_endpoint_uri_or_ipc_path()}"
129
141
  )
130
142
 
143
+ @async_handle_request_caching
144
+ async def make_request(self, method: RPCEndpoint, params: Any) -> RPCResponse:
145
+ request_data = self.encode_rpc_request(method, params)
146
+ await self.socket_send(request_data)
147
+
148
+ current_request_id = json.loads(request_data)["id"]
149
+ response = await self._get_response_for_request_id(current_request_id)
150
+
151
+ return response
152
+
153
+ async def make_batch_request(
154
+ self, requests: List[Tuple[RPCEndpoint, Any]]
155
+ ) -> List[RPCResponse]:
156
+ request_data = self.encode_batch_rpc_request(requests)
157
+ await self.socket_send(request_data)
158
+
159
+ response = cast(
160
+ List[RPCResponse], await self._get_response_for_request_id(BATCH_REQUEST_ID)
161
+ )
162
+ return response
163
+
164
+ # -- abstract methods -- #
165
+
166
+ @abstractmethod
167
+ async def socket_send(self, request_data: bytes) -> None:
168
+ """
169
+ Send an encoded RPC request to the provider over the persistent connection.
170
+ """
171
+ raise NotImplementedError("Must be implemented by subclasses")
172
+
173
+ @abstractmethod
174
+ async def socket_recv(self) -> RPCResponse:
175
+ """
176
+ Receive, decode, and return an RPC response from the provider over the
177
+ persistent connection.
178
+ """
179
+ raise NotImplementedError("Must be implemented by subclasses")
180
+
131
181
  # -- private methods -- #
132
182
 
133
183
  async def _provider_specific_connect(self) -> None:
@@ -136,7 +186,7 @@ class PersistentConnectionProvider(AsyncJSONBaseProvider, ABC):
136
186
  async def _provider_specific_disconnect(self) -> None:
137
187
  raise NotImplementedError("Must be implemented by subclasses")
138
188
 
139
- async def _provider_specific_message_listener(self) -> None:
189
+ async def _provider_specific_socket_reader(self) -> RPCResponse:
140
190
  raise NotImplementedError("Must be implemented by subclasses")
141
191
 
142
192
  def _message_listener_callback(
@@ -158,8 +208,28 @@ class PersistentConnectionProvider(AsyncJSONBaseProvider, ABC):
158
208
  # the use of sleep(0) seems to be the most efficient way to yield control
159
209
  # back to the event loop to share the loop with other tasks.
160
210
  await asyncio.sleep(0)
211
+
161
212
  try:
162
- await self._provider_specific_message_listener()
213
+ response = await self._provider_specific_socket_reader()
214
+
215
+ if isinstance(response, list):
216
+ response = sort_batch_response_by_response_ids(response)
217
+
218
+ subscription = (
219
+ response.get("method") == "eth_subscription"
220
+ if not isinstance(response, list)
221
+ else False
222
+ )
223
+ await self._request_processor.cache_raw_response(
224
+ response, subscription=subscription
225
+ )
226
+ except PersistentConnectionClosedOK as e:
227
+ self.logger.info(
228
+ "Message listener background task has ended gracefully: "
229
+ f"{e.user_message}"
230
+ )
231
+ # trigger a return to end the listener task and initiate the callback fn
232
+ return
163
233
  except Exception as e:
164
234
  if not self.silence_listener_task_exceptions:
165
235
  raise e
@@ -202,10 +272,6 @@ class PersistentConnectionProvider(AsyncJSONBaseProvider, ABC):
202
272
  request_cache_key = generate_cache_key(request_id)
203
273
 
204
274
  while True:
205
- # check if an exception was recorded in the listener task and raise it
206
- # in the main loop if so
207
- self._handle_listener_task_exceptions()
208
-
209
275
  if request_cache_key in self._request_processor._request_response_cache:
210
276
  self.logger.debug(
211
277
  f"Popping response for id {request_id} from cache."
@@ -215,6 +281,9 @@ class PersistentConnectionProvider(AsyncJSONBaseProvider, ABC):
215
281
  )
216
282
  return popped_response
217
283
  else:
284
+ # check if an exception was recorded in the listener task and raise
285
+ # it in the main loop if so
286
+ self._handle_listener_task_exceptions()
218
287
  await asyncio.sleep(0)
219
288
 
220
289
  try:
@@ -27,16 +27,58 @@ class PersistentConnection:
27
27
  def __init__(self, w3: "AsyncWeb3"):
28
28
  self._manager = w3.manager
29
29
 
30
- # -- public methods -- #
31
30
  @property
32
31
  def subscriptions(self) -> Dict[str, Any]:
32
+ """
33
+ Return the active subscriptions on the persistent connection.
34
+
35
+ :return: The active subscriptions on the persistent connection.
36
+ :rtype: Dict[str, Any]
37
+ """
33
38
  return self._manager._request_processor.active_subscriptions
34
39
 
35
- async def send(self, method: RPCEndpoint, params: Any) -> RPCResponse:
36
- return await self._manager.send(method, params)
40
+ async def make_request(self, method: RPCEndpoint, params: Any) -> RPCResponse:
41
+ """
42
+ Make a request to the persistent connection and return the response. This method
43
+ does not process the response as it would when invoking a method via the
44
+ appropriate module on the `AsyncWeb3` instance,
45
+ e.g. `w3.eth.get_block("latest")`.
46
+
47
+ :param method: The RPC method, e.g. `eth_getBlockByNumber`.
48
+ :param params: The RPC method parameters, e.g. `["0x1337", False]`.
49
+
50
+ :return: The processed response from the persistent connection.
51
+ :rtype: RPCResponse
52
+ """
53
+ return await self._manager.socket_request(method, params)
54
+
55
+ async def send(self, method: RPCEndpoint, params: Any) -> None:
56
+ """
57
+ Send a raw, unprocessed message to the persistent connection.
37
58
 
38
- async def recv(self) -> Any:
39
- return await self._manager._get_next_message()
59
+ :param method: The RPC method, e.g. `eth_getBlockByNumber`.
60
+ :param params: The RPC method parameters, e.g. `["0x1337", False]`.
61
+
62
+ :return: None
63
+ """
64
+ await self._manager.send(method, params)
65
+
66
+ async def recv(self) -> RPCResponse:
67
+ """
68
+ Receive the next unprocessed response for a request from the persistent
69
+ connection.
70
+
71
+ :return: The next unprocessed response for a request from the persistent
72
+ connection.
73
+ :rtype: RPCResponse
74
+ """
75
+ return await self._manager.recv()
40
76
 
41
77
  def process_subscriptions(self) -> "_AsyncPersistentMessageStream":
78
+ """
79
+ Asynchronous iterator that yields messages from the subscription message stream.
80
+
81
+ :return: The subscription message stream.
82
+ :rtype: _AsyncPersistentMessageStream
83
+ """
42
84
  return self._manager._persistent_message_stream()