|
# retoor <retoor@molodetz.nl>
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import AsyncIterator
|
|
|
|
import httpx
|
|
from curl_cffi import CurlHttpVersion
|
|
from curl_cffi.requests import AsyncSession
|
|
from curl_cffi.requests.exceptions import RequestException, Timeout
|
|
|
|
IMPERSONATE_TARGET: str = "chrome146"
|
|
|
|
DEFAULT_TIMEOUT_SECONDS: float = 30.0
|
|
|
|
STRIP_REQUEST_HEADERS: frozenset[str] = frozenset(
|
|
{
|
|
"host",
|
|
"connection",
|
|
"proxy-connection",
|
|
"content-length",
|
|
"transfer-encoding",
|
|
"user-agent",
|
|
"accept-encoding",
|
|
}
|
|
)
|
|
|
|
STRIP_RESPONSE_HEADERS: frozenset[str] = frozenset(
|
|
{
|
|
"content-encoding",
|
|
"content-length",
|
|
"transfer-encoding",
|
|
"connection",
|
|
}
|
|
)
|
|
|
|
HTTP_VERSION_LABELS: dict[int, bytes] = {
|
|
int(CurlHttpVersion.V1_0): b"HTTP/1.0",
|
|
int(CurlHttpVersion.V1_1): b"HTTP/1.1",
|
|
int(CurlHttpVersion.V2_0): b"HTTP/2",
|
|
int(CurlHttpVersion.V2TLS): b"HTTP/2",
|
|
int(CurlHttpVersion.V2_PRIOR_KNOWLEDGE): b"HTTP/2",
|
|
int(CurlHttpVersion.V3): b"HTTP/3",
|
|
int(CurlHttpVersion.V3ONLY): b"HTTP/3",
|
|
}
|
|
|
|
|
|
def http_version_for(url: httpx.URL):
|
|
if url.scheme == "http":
|
|
return CurlHttpVersion.V1_1
|
|
return None
|
|
|
|
|
|
def resolve_timeout(request: httpx.Request) -> float:
|
|
extension = request.extensions.get("timeout") or {}
|
|
for key in ("read", "connect", "pool"):
|
|
value = extension.get(key)
|
|
if isinstance(value, (int, float)):
|
|
return float(value)
|
|
return DEFAULT_TIMEOUT_SECONDS
|
|
|
|
|
|
class CurlResponseStream(httpx.AsyncByteStream):
|
|
def __init__(self, response: object) -> None:
|
|
self._response = response
|
|
|
|
async def __aiter__(self) -> AsyncIterator[bytes]:
|
|
async for chunk in self._response.aiter_content():
|
|
yield chunk
|
|
|
|
async def aclose(self) -> None:
|
|
await self._response.aclose()
|
|
|
|
|
|
class CurlTransport(httpx.AsyncBaseTransport):
|
|
def __init__(self, *, impersonate: str = IMPERSONATE_TARGET, verify: bool = True) -> None:
|
|
self._session = AsyncSession()
|
|
self._impersonate = impersonate
|
|
self._verify = verify
|
|
|
|
async def handle_async_request(self, request: httpx.Request) -> httpx.Response:
|
|
headers = {
|
|
key: value
|
|
for key, value in request.headers.items()
|
|
if key.lower() not in STRIP_REQUEST_HEADERS
|
|
}
|
|
body = await request.aread()
|
|
extra = {}
|
|
version = http_version_for(request.url)
|
|
if version is not None:
|
|
extra["http_version"] = version
|
|
try:
|
|
response = await self._session.request(
|
|
request.method,
|
|
str(request.url),
|
|
headers=headers,
|
|
data=body or None,
|
|
impersonate=self._impersonate,
|
|
verify=self._verify,
|
|
stream=True,
|
|
allow_redirects=False,
|
|
timeout=resolve_timeout(request),
|
|
**extra,
|
|
)
|
|
except Timeout as exc:
|
|
raise httpx.ConnectTimeout(str(exc), request=request) from exc
|
|
except RequestException as exc:
|
|
raise httpx.ConnectError(str(exc), request=request) from exc
|
|
|
|
response_headers = [
|
|
(key, value)
|
|
for key, value in response.headers.items()
|
|
if key.lower() not in STRIP_RESPONSE_HEADERS
|
|
]
|
|
http_version = HTTP_VERSION_LABELS.get(int(response.http_version), b"HTTP/2")
|
|
return httpx.Response(
|
|
status_code=response.status_code,
|
|
headers=response_headers,
|
|
stream=CurlResponseStream(response),
|
|
extensions={"http_version": http_version},
|
|
request=request,
|
|
)
|
|
|
|
async def aclose(self) -> None:
|
|
await self._session.close()
|