Skip to content

Commit cca3869

Browse files
Mantisusvdusek
andauthored
refactor!: Replace httpx with impit (#560)
### Description - Replace the usage of HTTPX with Impit ### Issues - Closes: #558 --------- Co-authored-by: Vlada Dusek <[email protected]>
1 parent 991b40a commit cca3869

File tree

9 files changed

+37
-77
lines changed

9 files changed

+37
-77
lines changed

pyproject.toml

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,14 @@ dependencies = [
3939
"crawlee@git+https://github.com/apify/crawlee-python.git@master",
4040
"cachetools>=5.5.0",
4141
"cryptography>=42.0.0",
42-
"httpx>=0.27.0",
4342
# TODO: ensure compatibility with the latest version of lazy-object-proxy
4443
# https://github.com/apify/apify-sdk-python/issues/460
44+
"impit>=0.5.3",
4545
"lazy-object-proxy<1.11.0",
4646
"more_itertools>=10.2.0",
4747
"typing-extensions>=4.1.0",
4848
"websockets>=14.0",
49+
"yarl>=1.18.0",
4950
]
5051

5152
[project.optional-dependencies]
@@ -81,7 +82,6 @@ dev = [
8182
"types-cachetools~=6.0.0.20250525",
8283
"uvicorn[standard]",
8384
"werkzeug~=3.1.0", # Werkzeug is used by httpserver
84-
"yarl~=1.20.0", # yarl is used by crawlee
8585
]
8686

8787
[tool.hatch.build.targets.wheel]
@@ -213,12 +213,12 @@ exclude = []
213213

214214
[[tool.mypy.overrides]]
215215
module = [
216-
'bs4',
217-
'lazy_object_proxy',
218-
'nest_asyncio',
219-
'playwright.*',
220-
'scrapy.*',
221-
'selenium.*',
216+
'bs4', # Documentation
217+
'httpx', # Documentation
218+
'lazy_object_proxy', # Untyped and stubs not available
219+
'playwright.*', # Documentation
220+
'scrapy.*', # Untyped and stubs not available
221+
'selenium.*', # Documentation
222222
]
223223
ignore_missing_imports = true
224224

src/apify/_proxy_configuration.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
from __future__ import annotations
22

33
import ipaddress
4+
import json
45
import re
56
from dataclasses import dataclass, field
67
from re import Pattern
78
from typing import TYPE_CHECKING, Any
89
from urllib.parse import urljoin, urlparse
910

10-
import httpx
11+
import impit
12+
from yarl import URL
1113

1214
from apify_shared.consts import ApifyEnvVars
1315
from crawlee.proxy_configuration import ProxyConfiguration as CrawleeProxyConfiguration
@@ -231,7 +233,7 @@ async def new_proxy_info(
231233
return None
232234

233235
if self._uses_apify_proxy:
234-
parsed_url = httpx.URL(proxy_info.url)
236+
parsed_url = URL(proxy_info.url)
235237
username = self._get_username(session_id)
236238

237239
return ProxyInfo(
@@ -275,11 +277,11 @@ async def _check_access(self) -> None:
275277
return
276278

277279
status = None
278-
async with httpx.AsyncClient(proxy=proxy_info.url, timeout=10) as client:
280+
async with impit.AsyncClient(proxy=proxy_info.url, timeout=10) as client:
279281
for _ in range(2):
280282
try:
281283
response = await client.get(proxy_status_url)
282-
status = response.json()
284+
status = json.loads(response.text)
283285
break
284286
except Exception: # noqa: S110
285287
# retry on connection errors

src/apify/log.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,6 @@ def _configure_logging() -> None:
2727
else:
2828
apify_client_logger.setLevel(level)
2929

30-
# Silence HTTPX logger unless debug logging is requested
31-
httpx_logger = logging.getLogger('httpx')
32-
if level > logging.DEBUG:
33-
httpx_logger.setLevel(logging.WARNING)
34-
else:
35-
httpx_logger.setLevel(level)
36-
3730
# Use configured log level for apify logger
3831
apify_logger = logging.getLogger('apify')
3932
configure_logger(apify_logger, remove_old_handlers=True)

src/apify/scrapy/_logging_config.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
# Define logger names.
1212
_PRIMARY_LOGGERS = ['apify', 'apify_client', 'scrapy']
13-
_SUPPLEMENTAL_LOGGERS = ['filelock', 'hpack', 'httpcore', 'httpx', 'protego', 'twisted']
13+
_SUPPLEMENTAL_LOGGERS = ['filelock', 'hpack', 'httpcore', 'protego', 'twisted']
1414
_ALL_LOGGERS = _PRIMARY_LOGGERS + _SUPPLEMENTAL_LOGGERS
1515

1616

@@ -37,9 +37,6 @@ def initialize_logging() -> None:
3737
for logger_name in [None, *_ALL_LOGGERS]:
3838
_configure_logger(logger_name, logging_level, handler)
3939

40-
# Set the 'httpx' logger to a less verbose level.
41-
logging.getLogger('httpx').setLevel('WARNING')
42-
4340
# Monkey-patch Scrapy's logging configuration to re-apply our settings.
4441
original_configure_logging = scrapy_logging.configure_logging
4542

tests/integration/conftest.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -96,14 +96,9 @@ def apify_token() -> str:
9696
return api_token
9797

9898

99-
@pytest.fixture
99+
@pytest.fixture(scope='session')
100100
def apify_client_async(apify_token: str) -> ApifyClientAsync:
101-
"""Create an instance of the ApifyClientAsync.
102-
103-
This fixture can't be session-scoped, because then you start getting `RuntimeError: Event loop is closed` errors,
104-
because `httpx.AsyncClient` in `ApifyClientAsync` tries to reuse the same event loop across requests,
105-
but `pytest-asyncio` closes the event loop after each test, and uses a new one for the next test.
106-
"""
101+
"""Create an instance of the ApifyClientAsync."""
107102
api_url = os.getenv(_API_URL_ENV_VAR)
108103

109104
return ApifyClientAsync(apify_token, api_url=api_url)

tests/unit/actor/test_actor_create_proxy_configuration.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def patched_apify_client(apify_client_async_patcher: ApifyClientAsyncPatcher) ->
2525
return ApifyClientAsync()
2626

2727

28-
@pytest.mark.usefixtures('patched_httpx_client')
28+
@pytest.mark.usefixtures('patched_impit_client')
2929
async def test_basic_proxy_configuration_creation(
3030
monkeypatch: pytest.MonkeyPatch,
3131
httpserver: HTTPServer,
@@ -68,7 +68,7 @@ def request_handler(request: Request, response: Response) -> Response:
6868
await Actor.exit()
6969

7070

71-
@pytest.mark.usefixtures('patched_httpx_client')
71+
@pytest.mark.usefixtures('patched_impit_client')
7272
async def test_proxy_configuration_with_actor_proxy_input(
7373
monkeypatch: pytest.MonkeyPatch,
7474
httpserver: HTTPServer,

tests/unit/conftest.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from logging import getLogger
88
from typing import TYPE_CHECKING, Any, get_type_hints
99

10-
import httpx
10+
import impit
1111
import pytest
1212
from pytest_httpserver import HTTPServer
1313

@@ -193,14 +193,15 @@ def httpserver(make_httpserver: HTTPServer) -> Iterator[HTTPServer]:
193193

194194

195195
@pytest.fixture
196-
def patched_httpx_client(monkeypatch: pytest.MonkeyPatch) -> Iterator[None]:
197-
"""Patch httpx client to drop proxy settings."""
196+
def patched_impit_client(monkeypatch: pytest.MonkeyPatch) -> Iterator[None]:
197+
"""Patch impit client to drop proxy settings."""
198198

199-
class ProxylessAsyncClient(httpx.AsyncClient):
200-
def __init__(self, *args: Any, **kwargs: Any) -> None:
201-
kwargs.pop('proxy', None)
202-
super().__init__(*args, **kwargs)
199+
original_async_client = impit.AsyncClient
203200

204-
monkeypatch.setattr(httpx, 'AsyncClient', ProxylessAsyncClient)
201+
def proxyless_async_client(*args: Any, **kwargs: Any) -> impit.AsyncClient:
202+
kwargs.pop('proxy', None)
203+
return original_async_client(*args, **kwargs)
204+
205+
monkeypatch.setattr(impit, 'AsyncClient', proxyless_async_client)
205206
yield
206207
monkeypatch.undo()

tests/unit/test_proxy_configuration.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,7 @@ async def test_new_proxy_info_rotating_urls_with_sessions() -> None:
377377
assert proxy_info.url == proxy_urls[0]
378378

379379

380-
@pytest.mark.usefixtures('patched_httpx_client')
380+
@pytest.mark.usefixtures('patched_impit_client')
381381
async def test_initialize_with_valid_configuration(
382382
monkeypatch: pytest.MonkeyPatch,
383383
httpserver: HTTPServer,
@@ -420,7 +420,7 @@ async def test_initialize_without_password_or_token() -> None:
420420
await proxy_configuration.initialize()
421421

422422

423-
@pytest.mark.usefixtures('patched_httpx_client')
423+
@pytest.mark.usefixtures('patched_impit_client')
424424
async def test_initialize_with_manual_password(monkeypatch: pytest.MonkeyPatch, httpserver: HTTPServer) -> None:
425425
dummy_proxy_status_url = str(httpserver.url_for('/')).removesuffix('/')
426426
monkeypatch.setenv(ApifyEnvVars.PROXY_STATUS_URL.value, dummy_proxy_status_url)
@@ -442,7 +442,7 @@ async def test_initialize_with_manual_password(monkeypatch: pytest.MonkeyPatch,
442442
assert proxy_configuration.is_man_in_the_middle is False
443443

444444

445-
@pytest.mark.usefixtures('patched_httpx_client')
445+
@pytest.mark.usefixtures('patched_impit_client')
446446
async def test_initialize_prefering_password_from_env_over_calling_api(
447447
monkeypatch: pytest.MonkeyPatch,
448448
httpserver: HTTPServer,
@@ -471,7 +471,7 @@ async def test_initialize_prefering_password_from_env_over_calling_api(
471471
assert len(patched_apify_client.calls['user']['get']) == 0 # type: ignore[attr-defined]
472472

473473

474-
@pytest.mark.usefixtures('patched_httpx_client')
474+
@pytest.mark.usefixtures('patched_impit_client')
475475
@pytest.mark.skip(reason='There are issues with log propagation to caplog, see issue #462.')
476476
async def test_initialize_with_manual_password_different_than_user_one(
477477
monkeypatch: pytest.MonkeyPatch,
@@ -506,7 +506,7 @@ async def test_initialize_with_manual_password_different_than_user_one(
506506
assert 'The Apify Proxy password you provided belongs to a different user' in caplog.records[0].message
507507

508508

509-
@pytest.mark.usefixtures('patched_httpx_client')
509+
@pytest.mark.usefixtures('patched_impit_client')
510510
async def test_initialize_when_not_connected(monkeypatch: pytest.MonkeyPatch, httpserver: HTTPServer) -> None:
511511
dummy_connection_error = 'DUMMY_CONNECTION_ERROR'
512512
dummy_proxy_status_url = str(httpserver.url_for('/')).removesuffix('/')

uv.lock

Lines changed: 4 additions & 32 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)