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.
- ens/__init__.py +13 -2
- ens/_normalization.py +4 -4
- ens/async_ens.py +27 -15
- ens/base_ens.py +3 -1
- ens/contract_data.py +2 -2
- ens/ens.py +10 -7
- ens/exceptions.py +16 -29
- ens/specs/nf.json +1 -1
- ens/specs/normalization_spec.json +1 -1
- ens/utils.py +24 -32
- web3/__init__.py +23 -12
- web3/_utils/abi.py +157 -263
- web3/_utils/async_transactions.py +34 -20
- web3/_utils/batching.py +217 -0
- web3/_utils/blocks.py +6 -2
- web3/_utils/caching/__init__.py +12 -0
- web3/_utils/caching/caching_utils.py +433 -0
- web3/_utils/caching/request_caching_validation.py +287 -0
- web3/_utils/compat/__init__.py +2 -3
- web3/_utils/contract_sources/compile_contracts.py +1 -1
- web3/_utils/contract_sources/contract_data/ambiguous_function_contract.py +42 -0
- web3/_utils/contract_sources/contract_data/arrays_contract.py +3 -3
- web3/_utils/contract_sources/contract_data/bytes_contracts.py +5 -5
- web3/_utils/contract_sources/contract_data/constructor_contracts.py +7 -7
- web3/_utils/contract_sources/contract_data/contract_caller_tester.py +3 -3
- web3/_utils/contract_sources/contract_data/emitter_contract.py +3 -3
- web3/_utils/contract_sources/contract_data/event_contracts.py +50 -5
- web3/_utils/contract_sources/contract_data/extended_resolver.py +3 -3
- web3/_utils/contract_sources/contract_data/fallback_function_contract.py +3 -3
- web3/_utils/contract_sources/contract_data/function_name_tester_contract.py +3 -3
- web3/_utils/contract_sources/contract_data/math_contract.py +3 -3
- web3/_utils/contract_sources/contract_data/offchain_lookup.py +3 -3
- web3/_utils/contract_sources/contract_data/offchain_resolver.py +3 -3
- web3/_utils/contract_sources/contract_data/panic_errors_contract.py +3 -3
- web3/_utils/contract_sources/contract_data/payable_tester.py +3 -3
- web3/_utils/contract_sources/contract_data/receive_function_contracts.py +5 -5
- web3/_utils/contract_sources/contract_data/reflector_contracts.py +3 -3
- web3/_utils/contract_sources/contract_data/revert_contract.py +3 -3
- web3/_utils/contract_sources/contract_data/simple_resolver.py +3 -3
- web3/_utils/contract_sources/contract_data/storage_contract.py +3 -3
- web3/_utils/contract_sources/contract_data/string_contract.py +3 -3
- web3/_utils/contract_sources/contract_data/tuple_contracts.py +5 -5
- web3/_utils/contracts.py +172 -220
- web3/_utils/datatypes.py +5 -1
- web3/_utils/decorators.py +6 -1
- web3/_utils/empty.py +1 -1
- web3/_utils/encoding.py +16 -12
- web3/_utils/error_formatters_utils.py +5 -3
- web3/_utils/events.py +78 -72
- web3/_utils/fee_utils.py +1 -3
- web3/_utils/filters.py +24 -22
- web3/_utils/formatters.py +2 -2
- web3/_utils/http.py +8 -2
- web3/_utils/http_session_manager.py +314 -0
- web3/_utils/math.py +14 -15
- web3/_utils/method_formatters.py +161 -34
- web3/_utils/module.py +2 -1
- web3/_utils/module_testing/__init__.py +3 -2
- web3/_utils/module_testing/eth_module.py +736 -583
- web3/_utils/module_testing/go_ethereum_debug_module.py +128 -0
- web3/_utils/module_testing/module_testing_utils.py +81 -24
- web3/_utils/module_testing/persistent_connection_provider.py +702 -220
- web3/_utils/module_testing/utils.py +114 -33
- web3/_utils/module_testing/web3_module.py +438 -17
- web3/_utils/normalizers.py +13 -11
- web3/_utils/rpc_abi.py +10 -22
- web3/_utils/threads.py +8 -7
- web3/_utils/transactions.py +32 -25
- web3/_utils/type_conversion.py +5 -1
- web3/_utils/validation.py +20 -17
- web3/beacon/__init__.py +5 -0
- web3/beacon/api_endpoints.py +3 -0
- web3/beacon/async_beacon.py +29 -6
- web3/beacon/beacon.py +24 -6
- web3/contract/__init__.py +7 -0
- web3/contract/async_contract.py +285 -82
- web3/contract/base_contract.py +556 -258
- web3/contract/contract.py +295 -84
- web3/contract/utils.py +251 -55
- web3/datastructures.py +49 -34
- web3/eth/__init__.py +7 -0
- web3/eth/async_eth.py +89 -69
- web3/eth/base_eth.py +7 -3
- web3/eth/eth.py +43 -66
- web3/exceptions.py +158 -83
- web3/gas_strategies/time_based.py +8 -6
- web3/geth.py +53 -184
- web3/main.py +77 -17
- web3/manager.py +362 -95
- web3/method.py +43 -15
- web3/middleware/__init__.py +17 -0
- web3/middleware/attrdict.py +12 -22
- web3/middleware/base.py +55 -2
- web3/middleware/filter.py +45 -23
- web3/middleware/formatting.py +6 -3
- web3/middleware/names.py +4 -1
- web3/middleware/signing.py +15 -6
- web3/middleware/stalecheck.py +2 -1
- web3/module.py +61 -25
- web3/providers/__init__.py +21 -0
- web3/providers/async_base.py +87 -32
- web3/providers/base.py +77 -32
- web3/providers/eth_tester/__init__.py +5 -0
- web3/providers/eth_tester/defaults.py +2 -55
- web3/providers/eth_tester/main.py +41 -15
- web3/providers/eth_tester/middleware.py +16 -17
- web3/providers/ipc.py +41 -17
- web3/providers/legacy_websocket.py +26 -1
- web3/providers/persistent/__init__.py +7 -0
- web3/providers/persistent/async_ipc.py +61 -121
- web3/providers/persistent/persistent.py +323 -16
- web3/providers/persistent/persistent_connection.py +54 -5
- web3/providers/persistent/request_processor.py +136 -56
- web3/providers/persistent/subscription_container.py +56 -0
- web3/providers/persistent/subscription_manager.py +233 -0
- web3/providers/persistent/websocket.py +29 -92
- web3/providers/rpc/__init__.py +5 -0
- web3/providers/rpc/async_rpc.py +73 -18
- web3/providers/rpc/rpc.py +73 -30
- web3/providers/rpc/utils.py +1 -13
- web3/scripts/install_pre_releases.py +33 -0
- web3/scripts/parse_pygeth_version.py +16 -0
- web3/testing.py +4 -4
- web3/tracing.py +9 -5
- web3/types.py +141 -74
- web3/utils/__init__.py +64 -5
- web3/utils/abi.py +790 -10
- web3/utils/address.py +8 -0
- web3/utils/async_exception_handling.py +20 -11
- web3/utils/caching.py +34 -4
- web3/utils/exception_handling.py +9 -12
- web3/utils/subscriptions.py +285 -0
- {web3-7.0.0b2.dist-info → web3-7.7.0.dist-info}/LICENSE +1 -1
- web3-7.7.0.dist-info/METADATA +130 -0
- web3-7.7.0.dist-info/RECORD +171 -0
- {web3-7.0.0b2.dist-info → web3-7.7.0.dist-info}/WHEEL +1 -1
- web3/_utils/caching.py +0 -155
- web3/_utils/contract_sources/contract_data/address_reflector.py +0 -29
- web3/_utils/module_testing/go_ethereum_personal_module.py +0 -300
- web3/_utils/request.py +0 -265
- web3-7.0.0b2.dist-info/METADATA +0 -106
- web3-7.0.0b2.dist-info/RECORD +0 -163
- /web3/_utils/{function_identifiers.py → abi_element_identifiers.py} +0 -0
- {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
|
|
36
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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:
|
|
45
|
-
|
|
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[
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
|
173
|
+
raise Web3ValueError("Subscription response must have params field")
|
|
140
174
|
if "subscription" not in response["params"]:
|
|
141
|
-
raise
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
#
|
|
362
|
+
# cache methods
|
|
286
363
|
|
|
287
|
-
def
|
|
288
|
-
|
|
289
|
-
|
|
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 =
|
|
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()
|