Skip to content

Commit 46693a8

Browse files
committed
feat!: Make HTTP client pluggable with abstract base classes
- Introduce `HttpClient` and `HttpClientAsync` ABCs that users can extend to provide custom HTTP client implementations via `ApifyClient(http_client=...)` and `ApifyClientAsync(http_client=...)` - Factor impit-specific logic into `ImpitHttpClient`/`ImpitHttpClientAsync`, keeping `_base.py` free of any `impit` dependency - Add `HttpResponse` protocol with full streaming support (`iter_bytes`, `aiter_bytes`, `read`, `aread`, `close`, `aclose`) - Export all public types (`HttpClient`, `HttpClientAsync`, `HttpResponse`, `ImpitHttpClient`, `ImpitHttpClientAsync`) from `apify_client`
1 parent 69571f0 commit 46693a8

File tree

16 files changed

+808
-161
lines changed

16 files changed

+808
-161
lines changed

src/apify_client/__init__.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,23 @@
11
from importlib import metadata
22

33
from ._apify_client import ApifyClient, ApifyClientAsync
4+
from ._http_clients import (
5+
HttpClient,
6+
HttpClientAsync,
7+
HttpResponse,
8+
ImpitHttpClient,
9+
ImpitHttpClientAsync,
10+
)
411

512
__version__ = metadata.version('apify-client')
613

7-
__all__ = ['ApifyClient', 'ApifyClientAsync', '__version__']
14+
__all__ = [
15+
'ApifyClient',
16+
'ApifyClientAsync',
17+
'HttpClient',
18+
'HttpClientAsync',
19+
'HttpResponse',
20+
'ImpitHttpClient',
21+
'ImpitHttpClientAsync',
22+
'__version__',
23+
]

src/apify_client/_apify_client.py

Lines changed: 72 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,12 @@
1313
DEFAULT_TIMEOUT,
1414
)
1515
from apify_client._docs import docs_group
16-
from apify_client._http_clients import HttpClient, HttpClientAsync
16+
from apify_client._http_clients import (
17+
HttpClient,
18+
HttpClientAsync,
19+
ImpitHttpClient,
20+
ImpitHttpClientAsync,
21+
)
1722
from apify_client._resource_clients import (
1823
ActorClient,
1924
ActorClientAsync,
@@ -118,6 +123,7 @@ def __init__(
118123
min_delay_between_retries: timedelta = DEFAULT_MIN_DELAY_BETWEEN_RETRIES,
119124
timeout: timedelta = DEFAULT_TIMEOUT,
120125
headers: dict[str, str] | None = None,
126+
http_client: HttpClient | None = None,
121127
) -> None:
122128
"""Initialize the Apify API client.
123129
@@ -129,16 +135,23 @@ def __init__(
129135
as well.
130136
api_public_url: The globally accessible URL of the Apify API server. Should be set only if `api_url`
131137
is an internal URL that is not globally accessible. Defaults to https://api.apify.com.
132-
max_retries: Maximum number of retry attempts for failed requests.
133-
min_delay_between_retries: Minimum delay between retries (increases exponentially with each attempt).
134-
timeout: Timeout for HTTP requests sent to the Apify API.
135-
headers: Additional HTTP headers to include in all API requests.
138+
max_retries: How many times to retry a failed request at most. Only used when `http_client` is not
139+
provided.
140+
min_delay_between_retries: How long will the client wait between retrying requests
141+
(increases exponentially from this value). Only used when `http_client` is not provided.
142+
timeout: The socket timeout of the HTTP requests sent to the Apify API. Only used when `http_client`
143+
is not provided.
144+
headers: Additional HTTP headers to include in all API requests. Only used when `http_client` is not
145+
provided.
146+
http_client: A custom HTTP client instance extending `HttpClient`. When provided, the `max_retries`,
147+
`min_delay_between_retries`, `timeout`, and `headers` parameters are ignored, as the custom
148+
client is responsible for its own configuration.
136149
"""
137150
# We need to do this because of mocking in tests and default mutable arguments.
138151
api_url = DEFAULT_API_URL if api_url is None else api_url
139152
api_public_url = DEFAULT_API_URL if api_public_url is None else api_public_url
140153

141-
if headers:
154+
if headers and not http_client:
142155
self._check_custom_headers(headers)
143156

144157
self._token = token
@@ -153,14 +166,17 @@ def __init__(
153166
self._statistics = ClientStatistics()
154167
"""Collector for client request statistics."""
155168

156-
self._http_client = HttpClient(
157-
token=self._token,
158-
timeout=timeout,
159-
max_retries=max_retries,
160-
min_delay_between_retries=min_delay_between_retries,
161-
statistics=self._statistics,
162-
headers=headers,
163-
)
169+
if http_client is not None:
170+
self._http_client: HttpClient = http_client
171+
else:
172+
self._http_client = ImpitHttpClient(
173+
token=self._token,
174+
timeout=timeout,
175+
max_retries=max_retries,
176+
min_delay_between_retries=min_delay_between_retries,
177+
statistics=self._statistics,
178+
headers=headers,
179+
)
164180
"""HTTP client used to communicate with the Apify API."""
165181

166182
self._client_registry = ClientRegistry(
@@ -221,6 +237,15 @@ def token(self) -> str | None:
221237
"""The Apify API token used by the client."""
222238
return self._token
223239

240+
@property
241+
def http_client(self) -> HttpClient:
242+
"""The HTTP client instance used for API communication.
243+
244+
Returns the custom HTTP client if one was provided during initialization,
245+
or the default `ImpitHttpClient` otherwise.
246+
"""
247+
return self._http_client
248+
224249
def actor(self, actor_id: str) -> ActorClient:
225250
"""Get the sub-client for a specific Actor.
226251
@@ -412,6 +437,7 @@ def __init__(
412437
min_delay_between_retries: timedelta = DEFAULT_MIN_DELAY_BETWEEN_RETRIES,
413438
timeout: timedelta = DEFAULT_TIMEOUT,
414439
headers: dict[str, str] | None = None,
440+
http_client: HttpClientAsync | None = None,
415441
) -> None:
416442
"""Initialize the Apify API client.
417443
@@ -423,16 +449,23 @@ def __init__(
423449
as well.
424450
api_public_url: The globally accessible URL of the Apify API server. Should be set only if `api_url`
425451
is an internal URL that is not globally accessible. Defaults to https://api.apify.com.
426-
max_retries: Maximum number of retry attempts for failed requests.
427-
min_delay_between_retries: Minimum delay between retries (increases exponentially with each attempt).
428-
timeout: Timeout for HTTP requests sent to the Apify API.
429-
headers: Additional HTTP headers to include in all API requests.
452+
max_retries: How many times to retry a failed request at most. Only used when `http_client` is not
453+
provided.
454+
min_delay_between_retries: How long will the client wait between retrying requests
455+
(increases exponentially from this value). Only used when `http_client` is not provided.
456+
timeout: The socket timeout of the HTTP requests sent to the Apify API. Only used when `http_client`
457+
is not provided.
458+
headers: Additional HTTP headers to include in all API requests. Only used when `http_client` is not
459+
provided.
460+
http_client: A custom HTTP client instance extending `HttpClientAsync`. When provided, the `max_retries`,
461+
`min_delay_between_retries`, `timeout`, and `headers` parameters are ignored, as the custom
462+
client is responsible for its own configuration.
430463
"""
431464
# We need to do this because of mocking in tests and default mutable arguments.
432465
api_url = DEFAULT_API_URL if api_url is None else api_url
433466
api_public_url = DEFAULT_API_URL if api_public_url is None else api_public_url
434467

435-
if headers:
468+
if headers and not http_client:
436469
self._check_custom_headers(headers)
437470

438471
self._token = token
@@ -447,14 +480,17 @@ def __init__(
447480
self._statistics = ClientStatistics()
448481
"""Collector for client request statistics."""
449482

450-
self._http_client = HttpClientAsync(
451-
token=self._token,
452-
timeout=timeout,
453-
max_retries=max_retries,
454-
min_delay_between_retries=min_delay_between_retries,
455-
statistics=self._statistics,
456-
headers=headers,
457-
)
483+
if http_client is not None:
484+
self._http_client: HttpClientAsync = http_client
485+
else:
486+
self._http_client = ImpitHttpClientAsync(
487+
token=self._token,
488+
timeout=timeout,
489+
max_retries=max_retries,
490+
min_delay_between_retries=min_delay_between_retries,
491+
statistics=self._statistics,
492+
headers=headers,
493+
)
458494
"""HTTP client used to communicate with the Apify API."""
459495

460496
self._client_registry = ClientRegistryAsync(
@@ -515,6 +551,15 @@ def token(self) -> str | None:
515551
"""The Apify API token used by the client."""
516552
return self._token
517553

554+
@property
555+
def http_client(self) -> HttpClientAsync:
556+
"""The HTTP client instance used for API communication.
557+
558+
Returns the custom HTTP client if one was provided during initialization,
559+
or the default `ImpitHttpClientAsync` otherwise.
560+
"""
561+
return self._http_client
562+
518563
def actor(self, actor_id: str) -> ActorClientAsync:
519564
"""Get the sub-client for a specific Actor.
520565
Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1-
from ._http_client import HttpClient, HttpClientAsync
1+
from ._base import HttpClient, HttpClientAsync, HttpResponse
2+
from ._impit import ImpitHttpClient, ImpitHttpClientAsync
23

34
__all__ = [
45
'HttpClient',
56
'HttpClientAsync',
7+
'HttpResponse',
8+
'ImpitHttpClient',
9+
'ImpitHttpClientAsync',
610
]

0 commit comments

Comments
 (0)