# 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()