tlsreq 0.1.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.
tlsreq/__init__.py ADDED
@@ -0,0 +1,18 @@
1
+ from .errors import BackendNotInstalled, TlsReqError, UnknownBackend, UnknownFingerprint
2
+ from .response import Response
3
+ from .session import AsyncSession, Session
4
+ from .utls_release import UTLS_RELEASE, UTLS_RELEASE_PAGE
5
+
6
+ __version__ = "0.1.0"
7
+ __all__ = [
8
+ "AsyncSession",
9
+ "Session",
10
+ "Response",
11
+ "TlsReqError",
12
+ "UnknownBackend",
13
+ "BackendNotInstalled",
14
+ "UnknownFingerprint",
15
+ "UTLS_RELEASE",
16
+ "UTLS_RELEASE_PAGE",
17
+ "__version__",
18
+ ]
@@ -0,0 +1,61 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Optional
4
+
5
+ from ..errors import UnknownBackend
6
+ from .base import AsyncBackend, SyncBackend
7
+
8
+ _ALIASES = {
9
+ "curl": "curl_cffi",
10
+ "cffi": "curl_cffi",
11
+ "nirequest": "niquests",
12
+ "nirequests": "niquests",
13
+ "nio": "niquests",
14
+ }
15
+
16
+
17
+ def _normalize_backend(name: str) -> str:
18
+ key = name.strip().lower().replace("-", "_")
19
+ return _ALIASES.get(key, key)
20
+
21
+
22
+ def create_sync_backend(
23
+ backend: str,
24
+ impersonate: Optional[str] = None,
25
+ **kwargs: Any,
26
+ ) -> SyncBackend:
27
+ name = _normalize_backend(backend)
28
+ if name == "curl_cffi":
29
+ from .curl_cffi import CurlCffiSync
30
+ return CurlCffiSync(impersonate=impersonate, **kwargs)
31
+ if name == "wreq":
32
+ from .wreq import WreqSync
33
+ return WreqSync(impersonate=impersonate, **kwargs)
34
+ if name == "niquests":
35
+ from .niquests import NiquestsSync
36
+ return NiquestsSync(impersonate=impersonate, **kwargs)
37
+ if name == "httpx":
38
+ from .httpx import HttpxSync
39
+ return HttpxSync(impersonate=impersonate, **kwargs)
40
+ raise UnknownBackend(f"未知 backend {backend!r}。可选: curl_cffi, wreq, niquests, httpx")
41
+
42
+
43
+ def create_async_backend(
44
+ backend: str,
45
+ impersonate: Optional[str] = None,
46
+ **kwargs: Any,
47
+ ) -> AsyncBackend:
48
+ name = _normalize_backend(backend)
49
+ if name == "curl_cffi":
50
+ from .curl_cffi import CurlCffiAsync
51
+ return CurlCffiAsync(impersonate=impersonate, **kwargs)
52
+ if name == "wreq":
53
+ from .wreq import WreqAsync
54
+ return WreqAsync(impersonate=impersonate, **kwargs)
55
+ if name == "niquests":
56
+ from .niquests import NiquestsAsync
57
+ return NiquestsAsync(impersonate=impersonate, **kwargs)
58
+ if name == "httpx":
59
+ from .httpx import HttpxAsync
60
+ return HttpxAsync(impersonate=impersonate, **kwargs)
61
+ raise UnknownBackend(f"未知 backend {backend!r}。可选: curl_cffi, wreq, niquests, httpx")
@@ -0,0 +1,122 @@
1
+ """httpcore/h2:Chrome 152 SETTINGS、连接窗口、HEADERS Priority。"""
2
+ from __future__ import annotations
3
+
4
+ import collections
5
+ from typing import Any
6
+
7
+ import h2.settings
8
+
9
+ from ..chrome_h2 import (
10
+ CONNECTION_WINDOW_INCREMENT,
11
+ ENABLE_PUSH,
12
+ H2_HOP_BY_HOP,
13
+ HEADER_TABLE_SIZE,
14
+ INITIAL_WINDOW_SIZE,
15
+ MAX_HEADER_LIST_SIZE,
16
+ PRIORITY_DEPENDS_ON,
17
+ PRIORITY_EXCLUSIVE,
18
+ PRIORITY_WEIGHT,
19
+ )
20
+
21
+ _PATCHED = False
22
+
23
+
24
+ def _chrome_local_settings() -> h2.settings.Settings:
25
+ settings = h2.settings.Settings(
26
+ client=True,
27
+ initial_values={
28
+ h2.settings.SettingCodes.HEADER_TABLE_SIZE: HEADER_TABLE_SIZE,
29
+ h2.settings.SettingCodes.ENABLE_PUSH: ENABLE_PUSH,
30
+ h2.settings.SettingCodes.INITIAL_WINDOW_SIZE: INITIAL_WINDOW_SIZE,
31
+ h2.settings.SettingCodes.MAX_HEADER_LIST_SIZE: MAX_HEADER_LIST_SIZE,
32
+ h2.settings.SettingCodes.MAX_CONCURRENT_STREAMS: 100,
33
+ },
34
+ )
35
+ del settings[h2.settings.SettingCodes.MAX_FRAME_SIZE]
36
+ del settings[h2.settings.SettingCodes.ENABLE_CONNECT_PROTOCOL]
37
+ del settings[h2.settings.SettingCodes.MAX_CONCURRENT_STREAMS]
38
+ return settings
39
+
40
+
41
+ def _restore_local_only(settings: h2.settings.Settings) -> None:
42
+ """SETTINGS 帧发出后再补回 h2/httpcore 内部会读的项,避免再发一帧。"""
43
+ codes = h2.settings.SettingCodes
44
+ settings._settings.setdefault(codes.MAX_CONCURRENT_STREAMS, collections.deque([100]))
45
+ settings._settings.setdefault(codes.MAX_FRAME_SIZE, collections.deque([16384]))
46
+
47
+
48
+ def patch_httpcore_chrome_h2() -> None:
49
+ global _PATCHED
50
+ if _PATCHED:
51
+ return
52
+
53
+ from httpcore._async.http2 import AsyncHTTP2Connection
54
+ from httpcore._async.http2 import has_body_headers as async_has_body_headers
55
+ from httpcore._sync.http2 import HTTP2Connection
56
+ from httpcore._sync.http2 import has_body_headers as sync_has_body_headers
57
+
58
+ async def async_send_connection_init(self: Any, request: Any) -> None:
59
+ self._h2_state.local_settings = _chrome_local_settings()
60
+ self._h2_state.initiate_connection()
61
+ _restore_local_only(self._h2_state.local_settings)
62
+ self._h2_state.increment_flow_control_window(CONNECTION_WINDOW_INCREMENT)
63
+ await self._write_outgoing_data(request)
64
+
65
+ def sync_send_connection_init(self: Any, request: Any) -> None:
66
+ self._h2_state.local_settings = _chrome_local_settings()
67
+ self._h2_state.initiate_connection()
68
+ _restore_local_only(self._h2_state.local_settings)
69
+ self._h2_state.increment_flow_control_window(CONNECTION_WINDOW_INCREMENT)
70
+ self._write_outgoing_data(request)
71
+
72
+ async def async_send_request_headers(self: Any, request: Any, stream_id: int) -> None:
73
+ end_stream = not async_has_body_headers(request)
74
+ authority = [value for key, value in request.headers if key.lower() == b"host"][0]
75
+ headers = [
76
+ (b":method", request.method),
77
+ (b":authority", authority),
78
+ (b":scheme", request.url.scheme),
79
+ (b":path", request.url.target),
80
+ ] + [
81
+ (key.lower(), value)
82
+ for key, value in request.headers
83
+ if key.lower() not in H2_HOP_BY_HOP
84
+ ]
85
+ self._h2_state.send_headers(
86
+ stream_id,
87
+ headers,
88
+ end_stream=end_stream,
89
+ priority_weight=PRIORITY_WEIGHT,
90
+ priority_depends_on=PRIORITY_DEPENDS_ON,
91
+ priority_exclusive=PRIORITY_EXCLUSIVE,
92
+ )
93
+ await self._write_outgoing_data(request)
94
+
95
+ def sync_send_request_headers(self: Any, request: Any, stream_id: int) -> None:
96
+ end_stream = not sync_has_body_headers(request)
97
+ authority = [value for key, value in request.headers if key.lower() == b"host"][0]
98
+ headers = [
99
+ (b":method", request.method),
100
+ (b":authority", authority),
101
+ (b":scheme", request.url.scheme),
102
+ (b":path", request.url.target),
103
+ ] + [
104
+ (key.lower(), value)
105
+ for key, value in request.headers
106
+ if key.lower() not in H2_HOP_BY_HOP
107
+ ]
108
+ self._h2_state.send_headers(
109
+ stream_id,
110
+ headers,
111
+ end_stream=end_stream,
112
+ priority_weight=PRIORITY_WEIGHT,
113
+ priority_depends_on=PRIORITY_DEPENDS_ON,
114
+ priority_exclusive=PRIORITY_EXCLUSIVE,
115
+ )
116
+ self._write_outgoing_data(request)
117
+
118
+ AsyncHTTP2Connection._send_connection_init = async_send_connection_init
119
+ AsyncHTTP2Connection._send_request_headers = async_send_request_headers
120
+ HTTP2Connection._send_connection_init = sync_send_connection_init
121
+ HTTP2Connection._send_request_headers = sync_send_request_headers
122
+ _PATCHED = True
@@ -0,0 +1,327 @@
1
+ """niquests HTTP/2 帧序 / HTTP/1 头与写合并,对齐 Chrome。"""
2
+ from __future__ import annotations
3
+
4
+ import inspect
5
+ from typing import Any
6
+
7
+ _H2_PATCHED = False
8
+ _H1_PATCHED = False
9
+
10
+ _HTTP1_KEEP_LOWER = frozenset({
11
+ b"sec-ch-ua",
12
+ b"sec-ch-ua-mobile",
13
+ b"sec-ch-ua-platform",
14
+ b"sec-fetch-site",
15
+ b"sec-fetch-mode",
16
+ b"sec-fetch-dest",
17
+ b"sec-fetch-user",
18
+ b"dnt",
19
+ b"priority",
20
+ })
21
+
22
+ _H2_FRAME_HEADER = 9
23
+ _H2_HEADERS = 0x01
24
+ _H2_SETTINGS = 0x04
25
+ _H2_ACK = 0x01
26
+
27
+
28
+ def _import_module(name: str) -> Any:
29
+ import sys
30
+
31
+ mod = sys.modules.get(name)
32
+ if mod is not None:
33
+ return mod
34
+ try:
35
+ __import__(name)
36
+ except ImportError:
37
+ return None
38
+ return sys.modules.get(name)
39
+
40
+
41
+ def _should_skip_h2_settings_wait(self: Any, event_type: Any) -> bool:
42
+ names = (
43
+ (getattr(event_type, "__name__", ""),)
44
+ if not isinstance(event_type, tuple)
45
+ else tuple(getattr(t, "__name__", "") for t in event_type)
46
+ )
47
+ svn = getattr(self._svn, "name", None) or getattr(self._svn, "value", self._svn)
48
+ return "HandshakeCompleted" in names and svn in {"h2", "HTTP/2.0"}
49
+
50
+
51
+ def _wrap_exchange_until(orig: Any) -> Any:
52
+ if inspect.iscoroutinefunction(orig):
53
+ async def __exchange_until(self: Any, event_type: Any, *args: Any, _orig: Any = orig, **kwargs: Any) -> Any:
54
+ if _should_skip_h2_settings_wait(self, event_type):
55
+ return []
56
+ return await _orig(self, event_type, *args, **kwargs)
57
+
58
+ return __exchange_until
59
+
60
+ def __exchange_until(self: Any, event_type: Any, *args: Any, _orig: Any = orig, **kwargs: Any) -> Any:
61
+ if _should_skip_h2_settings_wait(self, event_type):
62
+ return []
63
+ return _orig(self, event_type, *args, **kwargs)
64
+
65
+ return __exchange_until
66
+
67
+
68
+ def _iter_h2_frames(data: bytes) -> tuple[list[bytes], bytes]:
69
+ frames: list[bytes] = []
70
+ i = 0
71
+ n = len(data)
72
+ while i + _H2_FRAME_HEADER <= n:
73
+ length = int.from_bytes(data[i:i + 3], "big")
74
+ end = i + _H2_FRAME_HEADER + length
75
+ if end > n:
76
+ break
77
+ frames.append(data[i:end])
78
+ i = end
79
+ return frames, data[i:]
80
+
81
+
82
+ def _park_settings_ack(protocol: Any, data: bytes) -> bytes:
83
+ frames, rest = _iter_h2_frames(data)
84
+ kept: list[bytes] = []
85
+ pending = getattr(protocol, "_tlsreq_pending_ack", b"")
86
+ for frame in frames:
87
+ if frame[3] == _H2_SETTINGS and frame[4] & _H2_ACK:
88
+ pending += frame
89
+ continue
90
+ if frame[3] == _H2_HEADERS:
91
+ protocol._tlsreq_headers_sent = True
92
+ kept.append(frame)
93
+ protocol._tlsreq_pending_ack = pending
94
+ return b"".join(kept) + rest
95
+
96
+
97
+ def _wrap_bytes_received(orig: Any) -> Any:
98
+ def bytes_received(self: Any, data: bytes, _orig: Any = orig) -> None:
99
+ _orig(self, data)
100
+ buf = getattr(self._connection, "_data_to_send", None)
101
+ if buf:
102
+ self._connection._data_to_send = bytearray(_park_settings_ack(self, bytes(buf)))
103
+
104
+ return bytes_received
105
+
106
+
107
+ def _swap_python_hpack(connection: Any) -> None:
108
+ from jh2.hpack.hpack import Encoder as PyHpackEncoder
109
+
110
+ if type(connection.encoder).__module__ == "jh2.hpack.hpack":
111
+ return
112
+ connection.encoder = PyHpackEncoder()
113
+
114
+
115
+ def _wrap_protocol_init(orig: Any) -> Any:
116
+ def __init__(self: Any, *args: Any, _orig: Any = orig, **kwargs: Any) -> None:
117
+ _orig(self, *args, **kwargs)
118
+ _swap_python_hpack(self._connection)
119
+
120
+ return __init__
121
+
122
+
123
+ def _wrap_bytes_to_send(orig: Any) -> Any:
124
+ def bytes_to_send(self: Any, _orig: Any = orig) -> bytes:
125
+ already_flushed = getattr(self, "_tlsreq_headers_flushed", False)
126
+ data = _park_settings_ack(self, _orig(self))
127
+ if getattr(self, "_tlsreq_headers_sent", False):
128
+ self._tlsreq_headers_flushed = True
129
+ if already_flushed:
130
+ writes = getattr(self, "_tlsreq_post_headers_writes", 0) + 1
131
+ self._tlsreq_post_headers_writes = writes
132
+ pending = getattr(self, "_tlsreq_pending_ack", b"")
133
+ if pending and writes >= 2:
134
+ self._tlsreq_pending_ack = b""
135
+ return data + pending
136
+ return data
137
+
138
+ return bytes_to_send
139
+
140
+
141
+ def patch_http2_chrome_frames() -> None:
142
+ global _H2_PATCHED
143
+ if _H2_PATCHED:
144
+ return
145
+
146
+ priority_ok = False
147
+ for name in (
148
+ "niquests.packages.urllib3.contrib.hface.protocols.http2._h2",
149
+ "urllib3_future.contrib.hface.protocols.http2._h2",
150
+ "urllib3.contrib.hface.protocols.http2._h2",
151
+ ):
152
+ mod = _import_module(name)
153
+ cls = getattr(mod, "HTTP2ProtocolHyperImpl", None) if mod else None
154
+ if cls is None:
155
+ continue
156
+ if getattr(cls, "_tlsreq_h2_patched", False) or getattr(cls, "_chrome151_ack_held", False):
157
+ priority_ok = True
158
+ continue
159
+
160
+ def submit_headers(self: Any, stream_id: int, headers: Any, end_stream: bool = False) -> None:
161
+ self._connection.send_headers(
162
+ stream_id,
163
+ headers,
164
+ end_stream,
165
+ priority_weight=256,
166
+ priority_depends_on=0,
167
+ priority_exclusive=True,
168
+ )
169
+ self._open_stream_count += 1
170
+
171
+ cls.submit_headers = submit_headers
172
+ cls.bytes_received = _wrap_bytes_received(cls.bytes_received)
173
+ cls.bytes_to_send = _wrap_bytes_to_send(cls.bytes_to_send)
174
+ cls.__init__ = _wrap_protocol_init(cls.__init__)
175
+ cls._tlsreq_h2_patched = True
176
+ priority_ok = True
177
+
178
+ preface_ok = False
179
+ for name, cls_name, mangled in (
180
+ ("niquests.packages.urllib3.backend.hface", "HfaceBackend", "_HfaceBackend__exchange_until"),
181
+ ("urllib3_future.backend.hface", "HfaceBackend", "_HfaceBackend__exchange_until"),
182
+ ("urllib3.backend.hface", "HfaceBackend", "_HfaceBackend__exchange_until"),
183
+ ("niquests.packages.urllib3.backend._async.hface", "AsyncHfaceBackend", "_AsyncHfaceBackend__exchange_until"),
184
+ ("urllib3_future.backend._async.hface", "AsyncHfaceBackend", "_AsyncHfaceBackend__exchange_until"),
185
+ ("urllib3.backend._async.hface", "AsyncHfaceBackend", "_AsyncHfaceBackend__exchange_until"),
186
+ ):
187
+ mod = _import_module(name)
188
+ cls = getattr(mod, cls_name, None) if mod else None
189
+ if cls is None:
190
+ continue
191
+ if getattr(cls, "_tlsreq_skip_h2_settings_wait", False) or getattr(cls, "_chrome151_skip_h2_settings_wait", False):
192
+ preface_ok = True
193
+ continue
194
+ orig = getattr(cls, mangled, None)
195
+ if orig is None:
196
+ continue
197
+ setattr(cls, mangled, _wrap_exchange_until(orig))
198
+ cls._tlsreq_skip_h2_settings_wait = True
199
+ preface_ok = True
200
+
201
+ if not priority_ok or not preface_ok:
202
+ raise RuntimeError("could not patch niquests HTTP/2 to match Chrome frame order")
203
+ _H2_PATCHED = True
204
+
205
+
206
+ def _chrome_http1_header_name(name: bytes) -> bytes:
207
+ lower = name.lower().replace(b"_", b"-")
208
+ if lower in _HTTP1_KEEP_LOWER or lower.startswith(b"sec-"):
209
+ return lower
210
+ return b"-".join(part.capitalize() for part in lower.split(b"-"))
211
+
212
+
213
+ def _wrap_http1_bytes_to_send(orig: Any) -> Any:
214
+ def bytes_to_send(self: Any, _orig: Any = orig) -> bytes:
215
+ if getattr(self, "_tlsreq_hold", False):
216
+ return b""
217
+ return _orig(self)
218
+
219
+ return bytes_to_send
220
+
221
+
222
+ def _wrap_http1_submit_data(orig: Any) -> Any:
223
+ def submit_data(self: Any, stream_id: int, data: bytes, end_stream: bool = False, _orig: Any = orig) -> None:
224
+ _orig(self, stream_id, data, end_stream)
225
+ if end_stream:
226
+ self._tlsreq_hold = False
227
+
228
+ return submit_data
229
+
230
+
231
+ def _wrap_async_endheaders(orig: Any) -> Any:
232
+ async def endheaders(
233
+ self: Any,
234
+ message_body: Any = None,
235
+ *,
236
+ encode_chunked: bool = False,
237
+ expect_body_afterward: bool = False,
238
+ _orig: Any = orig,
239
+ ) -> Any:
240
+ from urllib3 import HttpVersion
241
+
242
+ proto = getattr(self, "_protocol", None)
243
+ if expect_body_afterward and getattr(self, "_svn", None) == HttpVersion.h11 and proto is not None:
244
+ proto._tlsreq_hold = True
245
+ return await _orig(
246
+ self,
247
+ message_body,
248
+ encode_chunked=encode_chunked,
249
+ expect_body_afterward=expect_body_afterward,
250
+ )
251
+
252
+ return endheaders
253
+
254
+
255
+ def patch_http1_chrome_headers() -> None:
256
+ global _H1_PATCHED
257
+ if _H1_PATCHED:
258
+ return
259
+
260
+ import h11
261
+
262
+ header_ok = False
263
+ for name in (
264
+ "niquests.packages.urllib3.contrib.hface.protocols.http1._h11",
265
+ "urllib3_future.contrib.hface.protocols.http1._h11",
266
+ "urllib3.contrib.hface.protocols.http1._h11",
267
+ ):
268
+ mod = _import_module(name)
269
+ if mod is None:
270
+ continue
271
+ cls = getattr(mod, "HTTP1ProtocolHyperImpl", None)
272
+ if cls is not None and not getattr(cls, "_tlsreq_h11_hold", False) and not getattr(cls, "_chrome151_h11_hold", False):
273
+ cls.bytes_to_send = _wrap_http1_bytes_to_send(cls.bytes_to_send)
274
+ cls.submit_data = _wrap_http1_submit_data(cls.submit_data)
275
+ cls._tlsreq_h11_hold = True
276
+ if getattr(mod, "_tlsreq_http1_headers", False) or getattr(mod, "_chrome151_http1_headers", False):
277
+ header_ok = True
278
+ continue
279
+
280
+ orig_headers_to_request = mod.headers_to_request
281
+
282
+ def headers_to_request(headers: Any, _orig: Any = orig_headers_to_request) -> Any:
283
+ request = _orig(headers)
284
+ present = {key.lower() for key, _value in request.headers}
285
+ if b"connection" in present:
286
+ return request
287
+ rewritten = list(request.headers)
288
+ insert_at = 0
289
+ for index, (key, _value) in enumerate(rewritten):
290
+ if key.lower() == b"host":
291
+ insert_at = index + 1
292
+ break
293
+ rewritten.insert(insert_at, (b"Connection", b"keep-alive"))
294
+ return h11.Request(
295
+ method=request.method,
296
+ headers=rewritten,
297
+ target=request.target,
298
+ )
299
+
300
+ def capitalize_header_name(header: bytes) -> bytes:
301
+ return _chrome_http1_header_name(header)
302
+
303
+ mod.capitalize_header_name = capitalize_header_name
304
+ mod.headers_to_request = headers_to_request
305
+ mod._tlsreq_http1_headers = True
306
+ header_ok = True
307
+
308
+ coalesce_ok = False
309
+ for name, cls_name in (
310
+ ("niquests.packages.urllib3.backend._async.hface", "AsyncHfaceBackend"),
311
+ ("urllib3_future.backend._async.hface", "AsyncHfaceBackend"),
312
+ ("urllib3.backend._async.hface", "AsyncHfaceBackend"),
313
+ ):
314
+ mod = _import_module(name)
315
+ cls = getattr(mod, cls_name, None) if mod else None
316
+ if cls is None:
317
+ continue
318
+ if getattr(cls, "_tlsreq_h11_coalesce", False) or getattr(cls, "_chrome151_h11_coalesce", False):
319
+ coalesce_ok = True
320
+ continue
321
+ cls.endheaders = _wrap_async_endheaders(cls.endheaders)
322
+ cls._tlsreq_h11_coalesce = True
323
+ coalesce_ok = True
324
+
325
+ if not header_ok or not coalesce_ok:
326
+ raise RuntimeError("could not patch niquests HTTP/1 to match Chrome request write")
327
+ _H1_PATCHED = True
@@ -0,0 +1,58 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Optional
4
+
5
+ from ..response import Response
6
+
7
+
8
+ def merge_extra(mapped: dict[str, Any], extra: Optional[dict[str, Any]]) -> dict[str, Any]:
9
+ if not extra:
10
+ return mapped
11
+ out = dict(mapped)
12
+ out.update(extra)
13
+ return out
14
+
15
+
16
+ def split_data(data: Any) -> dict[str, Any]:
17
+ """httpx: dict 走 data= 表单,str/bytes 走 content=。"""
18
+ if data is None:
19
+ return {}
20
+ if isinstance(data, (dict, list, tuple)):
21
+ return {"data": data}
22
+ return {"content": data}
23
+
24
+
25
+ class SyncBackend:
26
+ def request(self, method: str, url: str, **kwargs: Any) -> Response:
27
+ raise NotImplementedError
28
+
29
+ def close(self) -> None:
30
+ return None
31
+
32
+ def get_cookies(self) -> dict[str, str]:
33
+ return {}
34
+
35
+ def set_cookies(self, cookies: dict[str, str], url: Optional[str] = None) -> None:
36
+ return None
37
+
38
+ @property
39
+ def raw(self) -> Any:
40
+ raise NotImplementedError
41
+
42
+
43
+ class AsyncBackend:
44
+ async def request(self, method: str, url: str, **kwargs: Any) -> Response:
45
+ raise NotImplementedError
46
+
47
+ async def aclose(self) -> None:
48
+ return None
49
+
50
+ def get_cookies(self) -> dict[str, str]:
51
+ return {}
52
+
53
+ def set_cookies(self, cookies: dict[str, str], url: Optional[str] = None) -> None:
54
+ return None
55
+
56
+ @property
57
+ def raw(self) -> Any:
58
+ raise NotImplementedError