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
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from concurrent.futures import (
|
|
3
|
+
ThreadPoolExecutor,
|
|
4
|
+
)
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import threading
|
|
8
|
+
import time
|
|
9
|
+
from typing import (
|
|
10
|
+
Any,
|
|
11
|
+
Dict,
|
|
12
|
+
List,
|
|
13
|
+
Optional,
|
|
14
|
+
Union,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from aiohttp import (
|
|
18
|
+
ClientResponse,
|
|
19
|
+
ClientSession,
|
|
20
|
+
ClientTimeout,
|
|
21
|
+
TCPConnector,
|
|
22
|
+
)
|
|
23
|
+
from eth_typing import (
|
|
24
|
+
URI,
|
|
25
|
+
)
|
|
26
|
+
import requests
|
|
27
|
+
|
|
28
|
+
from web3._utils.async_caching import (
|
|
29
|
+
async_lock,
|
|
30
|
+
)
|
|
31
|
+
from web3._utils.caching import (
|
|
32
|
+
generate_cache_key,
|
|
33
|
+
)
|
|
34
|
+
from web3._utils.http import (
|
|
35
|
+
DEFAULT_HTTP_TIMEOUT,
|
|
36
|
+
)
|
|
37
|
+
from web3.exceptions import (
|
|
38
|
+
TimeExhausted,
|
|
39
|
+
)
|
|
40
|
+
from web3.utils.caching import (
|
|
41
|
+
SimpleCache,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class HTTPSessionManager:
|
|
46
|
+
logger = logging.getLogger("web3._utils.http_session_manager.HTTPSessionManager")
|
|
47
|
+
_lock: threading.Lock = threading.Lock()
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
cache_size: int = 100,
|
|
52
|
+
session_pool_max_workers: int = 5,
|
|
53
|
+
) -> None:
|
|
54
|
+
self.session_cache = SimpleCache(cache_size)
|
|
55
|
+
self.session_pool = ThreadPoolExecutor(max_workers=session_pool_max_workers)
|
|
56
|
+
|
|
57
|
+
@staticmethod
|
|
58
|
+
def get_default_http_endpoint() -> URI:
|
|
59
|
+
return URI(os.environ.get("WEB3_HTTP_PROVIDER_URI", "http://localhost:8545"))
|
|
60
|
+
|
|
61
|
+
def cache_and_return_session(
|
|
62
|
+
self,
|
|
63
|
+
endpoint_uri: URI,
|
|
64
|
+
session: requests.Session = None,
|
|
65
|
+
request_timeout: Optional[float] = None,
|
|
66
|
+
) -> requests.Session:
|
|
67
|
+
# cache key should have a unique thread identifier
|
|
68
|
+
cache_key = generate_cache_key(f"{threading.get_ident()}:{endpoint_uri}")
|
|
69
|
+
|
|
70
|
+
cached_session = self.session_cache.get_cache_entry(cache_key)
|
|
71
|
+
if cached_session is not None:
|
|
72
|
+
# If read from cache yields a session, no need to lock; return the session.
|
|
73
|
+
# Sync is a bit simpler in this way since a `requests.Session` doesn't
|
|
74
|
+
# really "close" in the same way that an async `ClientSession` does.
|
|
75
|
+
# When "closed", it still uses http / https adapters successfully if a
|
|
76
|
+
# request is made.
|
|
77
|
+
return cached_session
|
|
78
|
+
|
|
79
|
+
if session is None:
|
|
80
|
+
session = requests.Session()
|
|
81
|
+
|
|
82
|
+
with self._lock:
|
|
83
|
+
cached_session, evicted_items = self.session_cache.cache(cache_key, session)
|
|
84
|
+
self.logger.debug(f"Session cached: {endpoint_uri}, {cached_session}")
|
|
85
|
+
|
|
86
|
+
if evicted_items is not None:
|
|
87
|
+
evicted_sessions = evicted_items.values()
|
|
88
|
+
for evicted_session in evicted_sessions:
|
|
89
|
+
self.logger.debug(
|
|
90
|
+
"Session cache full. Session evicted from cache: "
|
|
91
|
+
f"{evicted_session}",
|
|
92
|
+
)
|
|
93
|
+
threading.Timer(
|
|
94
|
+
# If `request_timeout` is `None`, don't wait forever for the closing
|
|
95
|
+
# session to finish the request. Instead, wait over the default timeout.
|
|
96
|
+
request_timeout or DEFAULT_HTTP_TIMEOUT + 0.1,
|
|
97
|
+
self._close_evicted_sessions,
|
|
98
|
+
args=[evicted_sessions],
|
|
99
|
+
).start()
|
|
100
|
+
|
|
101
|
+
return cached_session
|
|
102
|
+
|
|
103
|
+
def get_response_from_get_request(
|
|
104
|
+
self, endpoint_uri: URI, *args: Any, **kwargs: Any
|
|
105
|
+
) -> requests.Response:
|
|
106
|
+
kwargs.setdefault("timeout", DEFAULT_HTTP_TIMEOUT)
|
|
107
|
+
session = self.cache_and_return_session(
|
|
108
|
+
endpoint_uri, request_timeout=kwargs["timeout"]
|
|
109
|
+
)
|
|
110
|
+
response = session.get(endpoint_uri, *args, **kwargs)
|
|
111
|
+
return response
|
|
112
|
+
|
|
113
|
+
def json_make_get_request(
|
|
114
|
+
self, endpoint_uri: URI, *args: Any, **kwargs: Any
|
|
115
|
+
) -> Dict[str, Any]:
|
|
116
|
+
response = self.get_response_from_get_request(endpoint_uri, *args, **kwargs)
|
|
117
|
+
response.raise_for_status()
|
|
118
|
+
return response.json()
|
|
119
|
+
|
|
120
|
+
def get_response_from_post_request(
|
|
121
|
+
self, endpoint_uri: URI, *args: Any, **kwargs: Any
|
|
122
|
+
) -> requests.Response:
|
|
123
|
+
session = self.cache_and_return_session(
|
|
124
|
+
endpoint_uri, request_timeout=kwargs["timeout"]
|
|
125
|
+
)
|
|
126
|
+
return session.post(endpoint_uri, *args, **kwargs)
|
|
127
|
+
|
|
128
|
+
def make_post_request(
|
|
129
|
+
self, endpoint_uri: URI, data: Union[bytes, Dict[str, Any]], **kwargs: Any
|
|
130
|
+
) -> bytes:
|
|
131
|
+
kwargs.setdefault("timeout", DEFAULT_HTTP_TIMEOUT)
|
|
132
|
+
kwargs.setdefault("stream", False)
|
|
133
|
+
|
|
134
|
+
start = time.time()
|
|
135
|
+
timeout = kwargs["timeout"]
|
|
136
|
+
|
|
137
|
+
with self.get_response_from_post_request(
|
|
138
|
+
endpoint_uri, data=data, **kwargs
|
|
139
|
+
) as response:
|
|
140
|
+
response.raise_for_status()
|
|
141
|
+
if kwargs.get("stream"):
|
|
142
|
+
return self._handle_streaming_response(response, start, timeout)
|
|
143
|
+
else:
|
|
144
|
+
return response.content
|
|
145
|
+
|
|
146
|
+
def _handle_streaming_response(
|
|
147
|
+
self, response: requests.Response, start: float, timeout: float
|
|
148
|
+
) -> bytes:
|
|
149
|
+
response_body = b""
|
|
150
|
+
for data in response.iter_content():
|
|
151
|
+
response_body += data
|
|
152
|
+
# Manually manage timeout so streaming responses time out
|
|
153
|
+
# rather than resetting the timeout each time a response comes back
|
|
154
|
+
if (time.time() - start) > timeout:
|
|
155
|
+
raise TimeExhausted
|
|
156
|
+
return response_body
|
|
157
|
+
|
|
158
|
+
def _close_evicted_sessions(self, evicted_sessions: List[requests.Session]) -> None:
|
|
159
|
+
for evicted_session in evicted_sessions:
|
|
160
|
+
evicted_session.close()
|
|
161
|
+
self.logger.debug(f"Closed evicted session: {evicted_session}")
|
|
162
|
+
|
|
163
|
+
# -- async -- #
|
|
164
|
+
|
|
165
|
+
async def async_cache_and_return_session(
|
|
166
|
+
self,
|
|
167
|
+
endpoint_uri: URI,
|
|
168
|
+
session: Optional[ClientSession] = None,
|
|
169
|
+
request_timeout: Optional[ClientTimeout] = None,
|
|
170
|
+
) -> ClientSession:
|
|
171
|
+
# cache key should have a unique thread identifier
|
|
172
|
+
cache_key = generate_cache_key(f"{threading.get_ident()}:{endpoint_uri}")
|
|
173
|
+
|
|
174
|
+
evicted_items = None
|
|
175
|
+
async with async_lock(self.session_pool, self._lock):
|
|
176
|
+
if cache_key not in self.session_cache:
|
|
177
|
+
if session is None:
|
|
178
|
+
session = ClientSession(
|
|
179
|
+
raise_for_status=True,
|
|
180
|
+
connector=TCPConnector(
|
|
181
|
+
force_close=True, enable_cleanup_closed=True
|
|
182
|
+
),
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
cached_session, evicted_items = self.session_cache.cache(
|
|
186
|
+
cache_key, session
|
|
187
|
+
)
|
|
188
|
+
self.logger.debug(
|
|
189
|
+
f"Async session cached: {endpoint_uri}, {cached_session}"
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
else:
|
|
193
|
+
# get the cached session
|
|
194
|
+
cached_session = self.session_cache.get_cache_entry(cache_key)
|
|
195
|
+
session_is_closed = cached_session.closed
|
|
196
|
+
session_loop_is_closed = cached_session._loop.is_closed()
|
|
197
|
+
|
|
198
|
+
warning = (
|
|
199
|
+
"Async session was closed"
|
|
200
|
+
if session_is_closed
|
|
201
|
+
else (
|
|
202
|
+
"Loop was closed for async session"
|
|
203
|
+
if session_loop_is_closed
|
|
204
|
+
else None
|
|
205
|
+
)
|
|
206
|
+
)
|
|
207
|
+
if warning:
|
|
208
|
+
self.logger.debug(
|
|
209
|
+
f"{warning}: {endpoint_uri}, {cached_session}. "
|
|
210
|
+
f"Creating and caching a new async session for uri."
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
self.session_cache._data.pop(cache_key)
|
|
214
|
+
if not session_is_closed:
|
|
215
|
+
# if loop was closed but not the session, close the session
|
|
216
|
+
await cached_session.close()
|
|
217
|
+
self.logger.debug(
|
|
218
|
+
f"Async session closed and evicted from cache: {cached_session}"
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
# replace stale session with a new session at the cache key
|
|
222
|
+
_session = ClientSession(
|
|
223
|
+
raise_for_status=True,
|
|
224
|
+
connector=TCPConnector(
|
|
225
|
+
force_close=True, enable_cleanup_closed=True
|
|
226
|
+
),
|
|
227
|
+
)
|
|
228
|
+
cached_session, evicted_items = self.session_cache.cache(
|
|
229
|
+
cache_key, _session
|
|
230
|
+
)
|
|
231
|
+
self.logger.debug(
|
|
232
|
+
f"Async session cached: {endpoint_uri}, {cached_session}"
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
if evicted_items is not None:
|
|
236
|
+
# At this point the evicted sessions are already popped out of the cache and
|
|
237
|
+
# just stored in the `evicted_sessions` dict. So we can kick off a future
|
|
238
|
+
# task to close them and it should be safe to pop out of the lock here.
|
|
239
|
+
evicted_sessions = list(evicted_items.values())
|
|
240
|
+
for evicted_session in evicted_sessions:
|
|
241
|
+
self.logger.debug(
|
|
242
|
+
"Async session cache full. Session evicted from cache: "
|
|
243
|
+
f"{evicted_session}",
|
|
244
|
+
)
|
|
245
|
+
# Kick off an asyncio `Task` to close the evicted sessions. In the case
|
|
246
|
+
# that the cache filled very quickly and some sessions have been evicted
|
|
247
|
+
# before their original request has been made, we set the timer to a bit
|
|
248
|
+
# more than the `request_timeout` for a call. This should make it so that
|
|
249
|
+
# any call from an evicted session can still be made before the session
|
|
250
|
+
# is closed.
|
|
251
|
+
asyncio.create_task(
|
|
252
|
+
self._async_close_evicted_sessions(
|
|
253
|
+
# if `ClientTimeout.total` is `None`, don't wait forever for the
|
|
254
|
+
# closing session to finish the request. Instead, use the default
|
|
255
|
+
# timeout.
|
|
256
|
+
request_timeout.total or DEFAULT_HTTP_TIMEOUT + 0.1,
|
|
257
|
+
evicted_sessions,
|
|
258
|
+
)
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
return cached_session
|
|
262
|
+
|
|
263
|
+
async def async_get_response_from_get_request(
|
|
264
|
+
self, endpoint_uri: URI, *args: Any, **kwargs: Any
|
|
265
|
+
) -> ClientResponse:
|
|
266
|
+
kwargs.setdefault("timeout", ClientTimeout(DEFAULT_HTTP_TIMEOUT))
|
|
267
|
+
session = await self.async_cache_and_return_session(
|
|
268
|
+
endpoint_uri, request_timeout=kwargs["timeout"]
|
|
269
|
+
)
|
|
270
|
+
response = await session.get(endpoint_uri, *args, **kwargs)
|
|
271
|
+
return response
|
|
272
|
+
|
|
273
|
+
async def async_json_make_get_request(
|
|
274
|
+
self, endpoint_uri: URI, *args: Any, **kwargs: Any
|
|
275
|
+
) -> Dict[str, Any]:
|
|
276
|
+
response = await self.async_get_response_from_get_request(
|
|
277
|
+
endpoint_uri, *args, **kwargs
|
|
278
|
+
)
|
|
279
|
+
response.raise_for_status()
|
|
280
|
+
return await response.json()
|
|
281
|
+
|
|
282
|
+
async def async_get_response_from_post_request(
|
|
283
|
+
self, endpoint_uri: URI, *args: Any, **kwargs: Any
|
|
284
|
+
) -> ClientResponse:
|
|
285
|
+
kwargs.setdefault("timeout", ClientTimeout(DEFAULT_HTTP_TIMEOUT))
|
|
286
|
+
session = await self.async_cache_and_return_session(
|
|
287
|
+
endpoint_uri, request_timeout=kwargs["timeout"]
|
|
288
|
+
)
|
|
289
|
+
response = await session.post(endpoint_uri, *args, **kwargs)
|
|
290
|
+
return response
|
|
291
|
+
|
|
292
|
+
async def async_make_post_request(
|
|
293
|
+
self, endpoint_uri: URI, data: Union[bytes, Dict[str, Any]], **kwargs: Any
|
|
294
|
+
) -> bytes:
|
|
295
|
+
response = await self.async_get_response_from_post_request(
|
|
296
|
+
endpoint_uri, data=data, **kwargs
|
|
297
|
+
)
|
|
298
|
+
response.raise_for_status()
|
|
299
|
+
return await response.read()
|
|
300
|
+
|
|
301
|
+
async def _async_close_evicted_sessions(
|
|
302
|
+
self, timeout: float, evicted_sessions: List[ClientSession]
|
|
303
|
+
) -> None:
|
|
304
|
+
await asyncio.sleep(timeout)
|
|
305
|
+
|
|
306
|
+
for evicted_session in evicted_sessions:
|
|
307
|
+
await evicted_session.close()
|
|
308
|
+
self.logger.debug(f"Closed evicted async session: {evicted_session}")
|
|
309
|
+
|
|
310
|
+
if any(not evicted_session.closed for evicted_session in evicted_sessions):
|
|
311
|
+
self.logger.warning(
|
|
312
|
+
"Some evicted async sessions were not properly closed: "
|
|
313
|
+
f"{evicted_sessions}"
|
|
314
|
+
)
|
web3/_utils/math.py
CHANGED
|
@@ -5,6 +5,7 @@ from typing import (
|
|
|
5
5
|
|
|
6
6
|
from web3.exceptions import (
|
|
7
7
|
InsufficientData,
|
|
8
|
+
Web3ValueError,
|
|
8
9
|
)
|
|
9
10
|
|
|
10
11
|
|
|
@@ -17,23 +18,21 @@ def percentile(
|
|
|
17
18
|
f"Expected a sequence of at least 1 integers, got {values!r}"
|
|
18
19
|
)
|
|
19
20
|
if percentile is None:
|
|
20
|
-
raise
|
|
21
|
+
raise Web3ValueError(f"Expected a percentile choice, got {percentile}")
|
|
22
|
+
if percentile < 0 or percentile > 100:
|
|
23
|
+
raise Web3ValueError("percentile must be in the range [0, 100]")
|
|
21
24
|
|
|
22
25
|
sorted_values = sorted(values)
|
|
23
26
|
|
|
24
|
-
|
|
25
|
-
if
|
|
26
|
-
|
|
27
|
-
if index < 0:
|
|
28
|
-
return sorted_values[0]
|
|
29
|
-
else:
|
|
30
|
-
index = rank
|
|
27
|
+
index = len(values) * percentile / 100 - 1
|
|
28
|
+
if index < 0:
|
|
29
|
+
return sorted_values[0]
|
|
31
30
|
|
|
32
|
-
|
|
31
|
+
fractional = index % 1
|
|
32
|
+
if fractional == 0:
|
|
33
33
|
return sorted_values[int(index)]
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
return lower + fractional * (higher - lower)
|
|
34
|
+
|
|
35
|
+
integer = int(index - fractional)
|
|
36
|
+
lower = sorted_values[integer]
|
|
37
|
+
higher = sorted_values[integer + 1]
|
|
38
|
+
return lower + fractional * (higher - lower)
|