web3 7.11.1__py3-none-any.whl → 7.12.1__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.
@@ -1,4 +1,5 @@
1
1
  import asyncio
2
+ import contextvars
2
3
  import itertools
3
4
  import logging
4
5
  from typing import (
@@ -61,6 +62,9 @@ if TYPE_CHECKING:
61
62
  AsyncWeb3,
62
63
  WebSocketProvider,
63
64
  )
65
+ from web3._utils.batching import ( # noqa: F401
66
+ RequestBatcher,
67
+ )
64
68
  from web3.providers.persistent import ( # noqa: F401
65
69
  RequestProcessor,
66
70
  )
@@ -94,12 +98,19 @@ class AsyncBaseProvider:
94
98
  self.cache_allowed_requests = cache_allowed_requests
95
99
  self.cacheable_requests = cacheable_requests or CACHEABLE_REQUESTS
96
100
  self.request_cache_validation_threshold = request_cache_validation_threshold
97
- self._is_batching: bool = False
101
+
102
+ self._batching_context: contextvars.ContextVar[
103
+ Optional["RequestBatcher[Any]"]
104
+ ] = contextvars.ContextVar("batching_context", default=None)
98
105
  self._batch_request_func_cache: Tuple[
99
106
  Tuple[Middleware, ...],
100
107
  Callable[..., Coroutine[Any, Any, Union[List[RPCResponse], RPCResponse]]],
101
108
  ] = (None, None)
102
109
 
110
+ @property
111
+ def _is_batching(self) -> bool:
112
+ return self._batching_context.get() is not None
113
+
103
114
  async def request_func(
104
115
  self, async_w3: "AsyncWeb3", middleware_onion: MiddlewareOnion
105
116
  ) -> Callable[..., Coroutine[Any, Any, RPCResponse]]:
@@ -240,3 +251,6 @@ class AsyncJSONBaseProvider(AsyncBaseProvider):
240
251
  )
241
252
  + b"]"
242
253
  )
254
+
255
+ def encode_batch_request_dicts(self, request_dicts: List[RPCRequest]) -> bytes:
256
+ return b"[" + b",".join(self.encode_rpc_dict(d) for d in request_dicts) + b"]"
web3/providers/auto.py CHANGED
@@ -3,6 +3,7 @@ from typing import (
3
3
  Any,
4
4
  Callable,
5
5
  Dict,
6
+ List,
6
7
  Optional,
7
8
  Sequence,
8
9
  Tuple,
@@ -21,9 +22,9 @@ from web3.exceptions import (
21
22
  CannotHandleRequest,
22
23
  )
23
24
  from web3.providers import (
24
- BaseProvider,
25
25
  HTTPProvider,
26
26
  IPCProvider,
27
+ JSONBaseProvider,
27
28
  LegacyWebSocketProvider,
28
29
  )
29
30
  from web3.types import (
@@ -35,7 +36,7 @@ HTTP_SCHEMES = {"http", "https"}
35
36
  WS_SCHEMES = {"ws", "wss"}
36
37
 
37
38
 
38
- def load_provider_from_environment() -> BaseProvider:
39
+ def load_provider_from_environment() -> Optional[JSONBaseProvider]:
39
40
  uri_string = URI(os.environ.get("WEB3_PROVIDER_URI", ""))
40
41
  if not uri_string:
41
42
  return None
@@ -45,7 +46,7 @@ def load_provider_from_environment() -> BaseProvider:
45
46
 
46
47
  def load_provider_from_uri(
47
48
  uri_string: URI, headers: Optional[Dict[str, Tuple[str, str]]] = None
48
- ) -> BaseProvider:
49
+ ) -> JSONBaseProvider:
49
50
  uri = urlparse(uri_string)
50
51
  if uri.scheme == "file":
51
52
  return IPCProvider(uri.path)
@@ -60,7 +61,7 @@ def load_provider_from_uri(
60
61
  )
61
62
 
62
63
 
63
- class AutoProvider(BaseProvider):
64
+ class AutoProvider(JSONBaseProvider):
64
65
  default_providers = (
65
66
  load_provider_from_environment,
66
67
  IPCProvider,
@@ -72,7 +73,7 @@ class AutoProvider(BaseProvider):
72
73
  def __init__(
73
74
  self,
74
75
  potential_providers: Optional[
75
- Sequence[Union[Callable[..., BaseProvider], Type[BaseProvider]]]
76
+ Sequence[Union[Callable[..., JSONBaseProvider], Type[JSONBaseProvider]]]
76
77
  ] = None,
77
78
  ) -> None:
78
79
  """
@@ -83,6 +84,7 @@ class AutoProvider(BaseProvider):
83
84
  in an attempt to find an active node. The list will default to
84
85
  :attribute:`default_providers`.
85
86
  """
87
+ super().__init__()
86
88
  if potential_providers:
87
89
  self._potential_providers = potential_providers
88
90
  else:
@@ -94,6 +96,14 @@ class AutoProvider(BaseProvider):
94
96
  except OSError:
95
97
  return self._proxy_request(method, params, use_cache=False)
96
98
 
99
+ def make_batch_request(
100
+ self, requests: List[Tuple[RPCEndpoint, Any]]
101
+ ) -> Union[List[RPCResponse], RPCResponse]:
102
+ try:
103
+ return self._proxy_batch_request(requests)
104
+ except OSError:
105
+ return self._proxy_batch_request(requests, use_cache=False)
106
+
97
107
  def is_connected(self, show_traceback: bool = False) -> bool:
98
108
  provider = self._get_active_provider(use_cache=True)
99
109
  return provider is not None and provider.is_connected(show_traceback)
@@ -110,7 +120,19 @@ class AutoProvider(BaseProvider):
110
120
 
111
121
  return provider.make_request(method, params)
112
122
 
113
- def _get_active_provider(self, use_cache: bool) -> Optional[BaseProvider]:
123
+ def _proxy_batch_request(
124
+ self, requests: List[Tuple[RPCEndpoint, Any]], use_cache: bool = True
125
+ ) -> Union[List[RPCResponse], RPCResponse]:
126
+ provider = self._get_active_provider(use_cache)
127
+ if provider is None:
128
+ raise CannotHandleRequest(
129
+ "Could not discover provider while making batch request: "
130
+ f"requests:{requests}\n"
131
+ )
132
+
133
+ return provider.make_batch_request(requests)
134
+
135
+ def _get_active_provider(self, use_cache: bool) -> Optional[JSONBaseProvider]:
114
136
  if use_cache and self._active_provider is not None:
115
137
  return self._active_provider
116
138
 
web3/providers/base.py CHANGED
@@ -1,3 +1,4 @@
1
+ import contextvars
1
2
  import itertools
2
3
  import logging
3
4
  import threading
@@ -50,6 +51,9 @@ from web3.utils import (
50
51
 
51
52
  if TYPE_CHECKING:
52
53
  from web3 import Web3 # noqa: F401
54
+ from web3._utils.batching import (
55
+ RequestBatcher,
56
+ )
53
57
 
54
58
 
55
59
  class BaseProvider:
@@ -81,6 +85,20 @@ class BaseProvider:
81
85
  self.cacheable_requests = cacheable_requests or CACHEABLE_REQUESTS
82
86
  self.request_cache_validation_threshold = request_cache_validation_threshold
83
87
 
88
+ self._batching_context: contextvars.ContextVar[
89
+ Optional["RequestBatcher[Any]"]
90
+ ] = contextvars.ContextVar("batching_context", default=None)
91
+ self._batch_request_func_cache: Tuple[
92
+ Tuple[Middleware, ...], Callable[..., Union[List[RPCResponse], RPCResponse]]
93
+ ] = (None, None)
94
+
95
+ @property
96
+ def _is_batching(self) -> bool:
97
+ """
98
+ Check if the provider is currently batching requests.
99
+ """
100
+ return self._batching_context.get() is not None
101
+
84
102
  def request_func(
85
103
  self, w3: "Web3", middleware_onion: MiddlewareOnion
86
104
  ) -> Callable[..., RPCResponse]:
@@ -120,11 +138,6 @@ class JSONBaseProvider(BaseProvider):
120
138
  super().__init__(**kwargs)
121
139
  self.request_counter = itertools.count()
122
140
 
123
- self._is_batching: bool = False
124
- self._batch_request_func_cache: Tuple[
125
- Tuple[Middleware, ...], Callable[..., Union[List[RPCResponse], RPCResponse]]
126
- ] = (None, None)
127
-
128
141
  def encode_rpc_request(self, method: RPCEndpoint, params: Any) -> bytes:
129
142
  rpc_dict = {
130
143
  "jsonrpc": "2.0",
web3/providers/ipc.py CHANGED
@@ -30,7 +30,6 @@ from web3.types import (
30
30
  )
31
31
 
32
32
  from .._utils.batching import (
33
- batching_context,
34
33
  sort_batch_response_by_response_ids,
35
34
  )
36
35
  from .._utils.caching import (
@@ -60,7 +59,7 @@ def get_ipc_socket(ipc_path: str, timeout: float = 2.0) -> socket.socket:
60
59
  return sock
61
60
 
62
61
 
63
- class PersistantSocket:
62
+ class PersistentSocket:
64
63
  sock = None
65
64
 
66
65
  def __init__(self, ipc_path: str) -> None:
@@ -158,7 +157,7 @@ class IPCProvider(JSONBaseProvider):
158
157
 
159
158
  self.timeout = timeout
160
159
  self._lock = threading.Lock()
161
- self._socket = PersistantSocket(self.ipc_path)
160
+ self._socket = PersistentSocket(self.ipc_path)
162
161
 
163
162
  def __str__(self) -> str:
164
163
  return f"<{self.__class__.__name__} {self.ipc_path}>"
@@ -197,16 +196,15 @@ class IPCProvider(JSONBaseProvider):
197
196
  @handle_request_caching
198
197
  def make_request(self, method: RPCEndpoint, params: Any) -> RPCResponse:
199
198
  self.logger.debug(
200
- f"Making request IPC. Path: {self.ipc_path}, Method: {method}"
199
+ "Making request IPC. Path: %s, Method: %s", self.ipc_path, method
201
200
  )
202
201
  request = self.encode_rpc_request(method, params)
203
202
  return self._make_request(request)
204
203
 
205
- @batching_context
206
204
  def make_batch_request(
207
205
  self, requests: List[Tuple[RPCEndpoint, Any]]
208
206
  ) -> List[RPCResponse]:
209
- self.logger.debug(f"Making batch request IPC. Path: {self.ipc_path}")
207
+ self.logger.debug("Making batch request IPC. Path: %s", self.ipc_path)
210
208
  request_data = self.encode_batch_rpc_request(requests)
211
209
  response = cast(List[RPCResponse], self._make_request(request_data))
212
210
  return sort_batch_response_by_response_ids(response)
@@ -27,7 +27,6 @@ from websockets.legacy.client import (
27
27
  )
28
28
 
29
29
  from web3._utils.batching import (
30
- batching_context,
31
30
  sort_batch_response_by_response_ids,
32
31
  )
33
32
  from web3._utils.caching import (
@@ -136,7 +135,7 @@ class LegacyWebSocketProvider(JSONBaseProvider):
136
135
  @handle_request_caching
137
136
  def make_request(self, method: RPCEndpoint, params: Any) -> RPCResponse:
138
137
  self.logger.debug(
139
- f"Making request WebSocket. URI: {self.endpoint_uri}, " f"Method: {method}"
138
+ "Making request WebSocket. URI: %s, Method: %s", self.endpoint_uri, method
140
139
  )
141
140
  request_data = self.encode_rpc_request(method, params)
142
141
  future = asyncio.run_coroutine_threadsafe(
@@ -144,13 +143,13 @@ class LegacyWebSocketProvider(JSONBaseProvider):
144
143
  )
145
144
  return future.result()
146
145
 
147
- @batching_context
148
146
  def make_batch_request(
149
147
  self, requests: List[Tuple[RPCEndpoint, Any]]
150
148
  ) -> List[RPCResponse]:
151
149
  self.logger.debug(
152
- f"Making batch request WebSocket. URI: {self.endpoint_uri}, "
153
- f"Methods: {requests}"
150
+ "Making batch request WebSocket. URI: %s, Methods: %s",
151
+ self.endpoint_uri,
152
+ requests,
154
153
  )
155
154
  request_data = self.encode_batch_rpc_request(requests)
156
155
  future = asyncio.run_coroutine_threadsafe(
@@ -24,7 +24,6 @@ from websockets import (
24
24
 
25
25
  from web3._utils.batching import (
26
26
  BATCH_REQUEST_ID,
27
- async_batching_context,
28
27
  sort_batch_response_by_response_ids,
29
28
  )
30
29
  from web3._utils.caching import (
@@ -71,16 +70,16 @@ class PersistentConnectionProvider(AsyncJSONBaseProvider, ABC):
71
70
 
72
71
  _send_func_cache: Tuple[
73
72
  Optional[int], Optional[Callable[..., Coroutine[Any, Any, RPCRequest]]]
74
- ] = (
75
- None,
76
- None,
77
- )
73
+ ] = (None, None)
78
74
  _recv_func_cache: Tuple[
79
75
  Optional[int], Optional[Callable[..., Coroutine[Any, Any, RPCResponse]]]
80
- ] = (
81
- None,
82
- None,
83
- )
76
+ ] = (None, None)
77
+ _send_batch_func_cache: Tuple[
78
+ Optional[int], Optional[Callable[..., Coroutine[Any, Any, List[RPCRequest]]]]
79
+ ] = (None, None)
80
+ _recv_batch_func_cache: Tuple[
81
+ Optional[int], Optional[Callable[..., Coroutine[Any, Any, List[RPCResponse]]]]
82
+ ] = (None, None)
84
83
 
85
84
  def __init__(
86
85
  self,
@@ -98,13 +97,14 @@ class PersistentConnectionProvider(AsyncJSONBaseProvider, ABC):
98
97
  request_information_cache_size=request_information_cache_size,
99
98
  )
100
99
  self._message_listener_task: Optional["asyncio.Task[None]"] = None
101
- self._batch_request_counter: Optional[int] = None
102
100
  self._listen_event: asyncio.Event = asyncio.Event()
103
101
  self._max_connection_retries = max_connection_retries
104
102
 
105
103
  self.request_timeout = request_timeout
106
104
  self.silence_listener_task_exceptions = silence_listener_task_exceptions
107
105
 
106
+ # -- cached middleware request/response functions -- #
107
+
108
108
  async def send_func(
109
109
  self, async_w3: "AsyncWeb3", middleware_onion: "MiddlewareOnion"
110
110
  ) -> Callable[..., Coroutine[Any, Any, RPCRequest]]:
@@ -155,6 +155,60 @@ class PersistentConnectionProvider(AsyncJSONBaseProvider, ABC):
155
155
 
156
156
  return self._recv_func_cache[1]
157
157
 
158
+ async def send_batch_func(
159
+ self, async_w3: "AsyncWeb3", middleware_onion: "MiddlewareOnion"
160
+ ) -> Callable[..., Coroutine[Any, Any, List[RPCRequest]]]:
161
+ middleware = middleware_onion.as_tuple_of_middleware()
162
+ cache_key = hash(tuple(id(mw) for mw in middleware))
163
+
164
+ if cache_key != self._send_batch_func_cache[0]:
165
+
166
+ async def send_func(
167
+ requests: List[Tuple[RPCEndpoint, Any]]
168
+ ) -> List[RPCRequest]:
169
+ for mw in middleware:
170
+ initialized = mw(async_w3)
171
+ requests = [
172
+ await initialized.async_request_processor(method, params)
173
+ for (method, params) in requests
174
+ ]
175
+ return await self.send_batch_request(requests)
176
+
177
+ self._send_batch_func_cache = (cache_key, send_func)
178
+
179
+ return self._send_batch_func_cache[1]
180
+
181
+ async def recv_batch_func(
182
+ self, async_w3: "AsyncWeb3", middleware_onion: "MiddlewareOnion"
183
+ ) -> Callable[..., Coroutine[Any, Any, List[RPCResponse]]]:
184
+ middleware = middleware_onion.as_tuple_of_middleware()
185
+ cache_key = hash(tuple(id(mw) for mw in middleware))
186
+
187
+ if cache_key != self._recv_batch_func_cache[0]:
188
+
189
+ async def recv_function(
190
+ rpc_requests: List[RPCRequest],
191
+ ) -> List[RPCResponse]:
192
+ methods = [rpc_request["method"] for rpc_request in rpc_requests]
193
+ responses = await self.recv_for_batch_request(rpc_requests)
194
+ for mw in reversed(middleware):
195
+ if not isinstance(responses, list):
196
+ # RPC errors return only one response with the error object
197
+ return responses
198
+
199
+ initialized = mw(async_w3)
200
+ responses = [
201
+ await initialized.async_response_processor(m, r)
202
+ for m, r in zip(methods, responses)
203
+ ]
204
+ return responses
205
+
206
+ self._recv_batch_func_cache = (cache_key, recv_function)
207
+
208
+ return self._recv_batch_func_cache[1]
209
+
210
+ # -- connection management -- #
211
+
158
212
  def get_endpoint_uri_or_ipc_path(self) -> str:
159
213
  if hasattr(self, "endpoint_uri"):
160
214
  return str(self.endpoint_uri)
@@ -167,6 +221,7 @@ class PersistentConnectionProvider(AsyncJSONBaseProvider, ABC):
167
221
  )
168
222
 
169
223
  async def connect(self) -> None:
224
+ endpoint = self.get_endpoint_uri_or_ipc_path()
170
225
  _connection_attempts = 0
171
226
  _backoff_rate_change = 1.75
172
227
  _backoff_time = 1.75
@@ -174,9 +229,7 @@ class PersistentConnectionProvider(AsyncJSONBaseProvider, ABC):
174
229
  while _connection_attempts != self._max_connection_retries:
175
230
  try:
176
231
  _connection_attempts += 1
177
- self.logger.info(
178
- f"Connecting to: {self.get_endpoint_uri_or_ipc_path()}"
179
- )
232
+ self.logger.info("Connecting to: %s", endpoint)
180
233
  await self._provider_specific_connect()
181
234
  self._message_listener_task = asyncio.create_task(
182
235
  self._message_listener()
@@ -184,19 +237,18 @@ class PersistentConnectionProvider(AsyncJSONBaseProvider, ABC):
184
237
  self._message_listener_task.add_done_callback(
185
238
  self._message_listener_callback
186
239
  )
187
- self.logger.info(
188
- f"Successfully connected to: {self.get_endpoint_uri_or_ipc_path()}"
189
- )
240
+ self.logger.info("Successfully connected to: %s", endpoint)
190
241
  break
191
242
  except (WebSocketException, OSError) as e:
192
243
  if _connection_attempts == self._max_connection_retries:
193
244
  raise ProviderConnectionError(
194
- f"Could not connect to: {self.get_endpoint_uri_or_ipc_path()}. "
245
+ f"Could not connect to: {endpoint}. "
195
246
  f"Retries exceeded max of {self._max_connection_retries}."
196
247
  ) from e
197
248
  self.logger.info(
198
- f"Could not connect to: {self.get_endpoint_uri_or_ipc_path()}. "
199
- f"Retrying in {round(_backoff_time, 1)} seconds.",
249
+ "Could not connect to: %s. Retrying in %s seconds.",
250
+ endpoint,
251
+ round(_backoff_time, 1),
200
252
  exc_info=True,
201
253
  )
202
254
  await asyncio.sleep(_backoff_time)
@@ -217,9 +269,12 @@ class PersistentConnectionProvider(AsyncJSONBaseProvider, ABC):
217
269
  await self._provider_specific_disconnect()
218
270
  self._request_processor.clear_caches()
219
271
  self.logger.info(
220
- f"Successfully disconnected from: {self.get_endpoint_uri_or_ipc_path()}"
272
+ "Successfully disconnected from: %s",
273
+ self.get_endpoint_uri_or_ipc_path(),
221
274
  )
222
275
 
276
+ # -- request methods -- #
277
+
223
278
  @async_handle_send_caching
224
279
  async def send_request(self, method: RPCEndpoint, params: Any) -> RPCRequest:
225
280
  request_dict = self.form_request(method, params)
@@ -238,20 +293,33 @@ class PersistentConnectionProvider(AsyncJSONBaseProvider, ABC):
238
293
  rpc_request = await self.send_request(method, params)
239
294
  return await self.recv_for_request(rpc_request)
240
295
 
241
- @async_batching_context
242
- async def make_batch_request(
296
+ # -- batch requests -- #
297
+
298
+ async def send_batch_request(
243
299
  self, requests: List[Tuple[RPCEndpoint, Any]]
244
- ) -> List[RPCResponse]:
245
- request_data = self.encode_batch_rpc_request(requests)
300
+ ) -> List[RPCRequest]:
301
+ request_dicts = [
302
+ self.form_request(method, params) for (method, params) in requests
303
+ ]
304
+ request_data = self.encode_batch_request_dicts(request_dicts)
246
305
  await self.socket_send(request_data)
306
+ return request_dicts
247
307
 
248
- # breakpoint()
308
+ async def recv_for_batch_request(
309
+ self, _request_dicts: List[RPCRequest]
310
+ ) -> List[RPCResponse]:
249
311
  response = cast(
250
312
  List[RPCResponse],
251
313
  await self._get_response_for_request_id(BATCH_REQUEST_ID),
252
314
  )
253
315
  return response
254
316
 
317
+ async def make_batch_request(
318
+ self, requests: List[Tuple[RPCEndpoint, Any]]
319
+ ) -> List[RPCResponse]:
320
+ request_dicts = await self.send_batch_request(requests)
321
+ return await self.recv_for_batch_request(request_dicts)
322
+
255
323
  # -- abstract methods -- #
256
324
 
257
325
  @abstractmethod
@@ -320,26 +388,26 @@ class PersistentConnectionProvider(AsyncJSONBaseProvider, ABC):
320
388
  Check the request response cache for any errors not tied to current requests
321
389
  and raise them if found.
322
390
  """
323
- if not self._is_batching:
324
- for (
325
- response
326
- ) in self._request_processor._request_response_cache._data.values():
327
- if isinstance(response, dict):
391
+ for response in self._request_processor._request_response_cache._data.values():
392
+ if isinstance(response, dict):
393
+ if "id" not in response:
394
+ validate_rpc_response_and_raise_if_error(
395
+ cast(RPCResponse, response), None, logger=self.logger
396
+ )
397
+ else:
328
398
  request = self._request_processor._request_information_cache.get_cache_entry( # noqa: E501
329
399
  generate_cache_key(response["id"])
330
400
  )
331
401
  if "error" in response and request is None:
332
- # if we find an error response in the cache without a
333
- # corresponding request, raise the error
334
402
  validate_rpc_response_and_raise_if_error(
335
403
  cast(RPCResponse, response), None, logger=self.logger
336
404
  )
337
405
 
338
406
  async def _message_listener(self) -> None:
339
407
  self.logger.info(
340
- f"{self.__class__.__qualname__} listener background task started. Storing "
341
- "all messages in appropriate request processor queues / caches to be "
342
- "processed."
408
+ "%s listener background task started. Storing all messages in "
409
+ "appropriate request processor queues / caches to be processed.",
410
+ self.__class__.__qualname__,
343
411
  )
344
412
  while True:
345
413
  # the use of sleep(0) seems to be the most efficient way to yield control
@@ -363,8 +431,8 @@ class PersistentConnectionProvider(AsyncJSONBaseProvider, ABC):
363
431
  self._raise_stray_errors_from_cache()
364
432
  except PersistentConnectionClosedOK as e:
365
433
  self.logger.info(
366
- "Message listener background task has ended gracefully: "
367
- f"{e.user_message}"
434
+ "Message listener background task has ended gracefully: %s",
435
+ e.user_message,
368
436
  )
369
437
  # trigger a return to end the listener task and initiate the callback fn
370
438
  return
@@ -382,8 +450,9 @@ class PersistentConnectionProvider(AsyncJSONBaseProvider, ABC):
382
450
  """
383
451
  self.logger.error(
384
452
  "Exception caught in listener, error logging and keeping "
385
- "listener background task alive."
386
- f"\n error={e.__class__.__name__}: {e}"
453
+ "listener background task alive.\n error=%s: %s",
454
+ e.__class__.__name__,
455
+ e,
387
456
  )
388
457
 
389
458
  def _handle_listener_task_exceptions(self) -> None:
@@ -416,7 +485,8 @@ class PersistentConnectionProvider(AsyncJSONBaseProvider, ABC):
416
485
 
417
486
  if request_cache_key in self._request_processor._request_response_cache:
418
487
  self.logger.debug(
419
- f"Popping response for id {request_id} from cache."
488
+ "Popping response for id %s from cache.",
489
+ request_id,
420
490
  )
421
491
  popped_response = await self._request_processor.pop_raw_response(
422
492
  cache_key=request_cache_key,