Refactor: Replace httpx with curl-cffi for HTTP requests and update dependencies

This commit is contained in:
bohd4nx
2026-07-05 16:13:16 +03:00
parent d78be012a3
commit fd31342d03
5 changed files with 22 additions and 33 deletions
+2 -3
View File
@@ -24,15 +24,14 @@ BASE_HEADERS: dict[str, str] = {
"content-type": "application/x-www-form-urlencoded; charset=UTF-8", "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
"origin": FRAGMENT_BASE_URL, "origin": FRAGMENT_BASE_URL,
"priority": "u=1, i", "priority": "u=1, i",
"sec-ch-ua": '"Google Chrome";v="147", "Not.A/Brand";v="8", "Chromium";v="147"', "sec-ch-ua": '"Not;A=Brand";v="8", "Chromium";v="150", "Google Chrome";v="150"',
"sec-ch-ua-mobile": "?1", "sec-ch-ua-mobile": "?1",
"sec-ch-ua-platform": '"Android"', "sec-ch-ua-platform": '"Android"',
"sec-fetch-dest": "empty", "sec-fetch-dest": "empty",
"sec-fetch-mode": "cors", "sec-fetch-mode": "cors",
"sec-fetch-site": "same-origin", "sec-fetch-site": "same-origin",
"user-agent": ( "user-agent": (
"Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) " "Mozilla/5.0 (Linux; Android 15; Pixel 9) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Mobile Safari/537.36"
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Mobile Safari/537.36"
), ),
"x-requested-with": "XMLHttpRequest", "x-requested-with": "XMLHttpRequest",
} }
+13 -23
View File
@@ -5,35 +5,25 @@ import random
import re import re
from typing import Any, cast from typing import Any, cast
import httpx from curl_cffi.requests import AsyncSession, Response
from pyfragment.core.constants import DEFAULT_TIMEOUT, FRAGMENT_BASE_URL from pyfragment.core.constants import FRAGMENT_BASE_URL
from pyfragment.exceptions import FragmentPageError, ParseError from pyfragment.exceptions import FragmentPageError, ParseError
async def get_fragment_hash( async def get_fragment_hash(
cookies: dict[str, Any], session: AsyncSession[Any], # type: ignore[type-arg]
headers: dict[str, str], headers: dict[str, str],
page_url: str, page_url: str,
timeout: float = DEFAULT_TIMEOUT,
) -> str: ) -> str:
page_headers = { # Derive the natural referer: strip the last path segment (e.g. /stars/buy → /stars)
k: v parent_url = page_url.rsplit("/", 1)[0] or FRAGMENT_BASE_URL
for k, v in headers.items()
if k not in ("accept", "accept-encoding", "content-type", "x-requested-with", "x-aj-referer")
}
page_headers.update(
{
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"referer": f"{FRAGMENT_BASE_URL}/",
"sec-fetch-dest": "document",
"sec-fetch-mode": "navigate",
"upgrade-insecure-requests": "1",
}
)
async with httpx.AsyncClient(cookies=cookies, timeout=timeout) as session: page_headers = {k: v for k, v in headers.items() if k not in ("content-type", "origin")}
response = await session.get(page_url, headers=page_headers) page_headers["referer"] = parent_url
page_headers["x-aj-referer"] = parent_url
response = await session.get(page_url, headers=page_headers)
if response.status_code != 200: if response.status_code != 200:
raise FragmentPageError(FragmentPageError.BAD_STATUS.format(status=response.status_code, url=page_url)) raise FragmentPageError(FragmentPageError.BAD_STATUS.format(status=response.status_code, url=page_url))
@@ -45,15 +35,15 @@ async def get_fragment_hash(
return match.group(1) return match.group(1)
def parse_json_response(response: httpx.Response, context: str) -> dict[str, Any]: def parse_json_response(response: Response, context: str) -> dict[str, Any]:
try: try:
return cast(dict[str, Any], response.json()) return cast(dict[str, Any], response.json()) # type: ignore[no-untyped-call]
except Exception as exc: except Exception as exc:
raise ParseError(ParseError.UNPARSEABLE.format(context=context, exc=exc)) from exc raise ParseError(ParseError.UNPARSEABLE.format(context=context, exc=exc)) from exc
async def fragment_request( async def fragment_request(
session: httpx.AsyncClient, session: AsyncSession[Any], # type: ignore[type-arg]
fragment_hash: str, fragment_hash: str,
headers: dict[str, str], headers: dict[str, str],
data: dict[str, Any], data: dict[str, Any],
+3 -3
View File
@@ -3,7 +3,7 @@ from __future__ import annotations
import logging import logging
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
import httpx from curl_cffi.requests import AsyncSession
from pyfragment.core.constants import BASE_HEADERS from pyfragment.core.constants import BASE_HEADERS
from pyfragment.core.transport import fragment_request, get_fragment_hash from pyfragment.core.transport import fragment_request, get_fragment_hash
@@ -27,8 +27,8 @@ async def raw_api_call(
call_headers = {**base, "referer": page_url, "x-aj-referer": page_url} call_headers = {**base, "referer": page_url, "x-aj-referer": page_url}
logger.debug("Starting Fragment API call '%s' on %s", method, page_url) logger.debug("Starting Fragment API call '%s' on %s", method, page_url)
try: try:
async with httpx.AsyncClient(cookies=cookies, timeout=timeout) as session: async with AsyncSession(cookies=cookies, timeout=timeout, impersonate="chrome") as session:
fragment_hash = await get_fragment_hash(cookies, call_headers, page_url, timeout) fragment_hash = await get_fragment_hash(session, call_headers, page_url)
response = await fragment_request(session, fragment_hash, call_headers, payload) response = await fragment_request(session, fragment_hash, call_headers, payload)
logger.debug("Completed Fragment API call '%s' with response keys: %s", method, sorted(response.keys())) logger.debug("Completed Fragment API call '%s' with response keys: %s", method, sorted(response.keys()))
return response return response
+1 -1
View File
@@ -34,7 +34,7 @@ classifiers = [
"Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Software Development :: Libraries :: Python Modules",
"Typing :: Typed", "Typing :: Typed",
] ]
dependencies = ["httpx>=0.25", "tonutils>=2.0.1"] dependencies = ["curl-cffi>=0.7", "tonutils>=2.0.1"]
[project.optional-dependencies] [project.optional-dependencies]
dev = ["pytest", "pytest-asyncio", "pytest-mock", "mypy", "ruff"] dev = ["pytest", "pytest-asyncio", "pytest-mock", "mypy", "ruff"]
+3 -3
View File
@@ -2,8 +2,8 @@
from unittest.mock import AsyncMock, MagicMock, patch from unittest.mock import AsyncMock, MagicMock, patch
import httpx
import pytest import pytest
from curl_cffi.requests import AsyncSession, Response
from pyfragment import FragmentClient, FragmentPageError from pyfragment import FragmentClient, FragmentPageError
from pyfragment.core.transport import fragment_request from pyfragment.core.transport import fragment_request
@@ -67,10 +67,10 @@ async def test_call_merges_extra_data(client: FragmentClient) -> None:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_fragment_request_non_200_raises() -> None: async def test_fragment_request_non_200_raises() -> None:
response = MagicMock(spec=httpx.Response) response = MagicMock(spec=Response)
response.status_code = 429 response.status_code = 429
session = AsyncMock(spec=httpx.AsyncClient) session = AsyncMock(spec=AsyncSession)
session.post = AsyncMock(return_value=response) session.post = AsyncMock(return_value=response)
with pytest.raises(FragmentPageError, match="429"): with pytest.raises(FragmentPageError, match="429"):