Skip to content

Commit 0b8ab15

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 7895a4e commit 0b8ab15

File tree

16 files changed

+793
-153
lines changed

16 files changed

+793
-153
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: 70 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@
1212
DEFAULT_MIN_DELAY_BETWEEN_RETRIES,
1313
DEFAULT_TIMEOUT,
1414
)
15-
from apify_client._http_clients import HttpClient, HttpClientAsync
15+
from apify_client._http_clients import (
16+
HttpClient,
17+
HttpClientAsync,
18+
ImpitHttpClient,
19+
ImpitHttpClientAsync,
20+
)
1621
from apify_client._resource_clients import (
1722
ActorClient,
1823
ActorClientAsync,
@@ -94,6 +99,7 @@ def __init__(
9499
min_delay_between_retries: timedelta = DEFAULT_MIN_DELAY_BETWEEN_RETRIES,
95100
timeout: timedelta = DEFAULT_TIMEOUT,
96101
headers: dict[str, str] | None = None,
102+
http_client: HttpClient | None = None,
97103
) -> None:
98104
"""Initialize a new instance.
99105
@@ -103,17 +109,23 @@ def __init__(
103109
be an internal URL that is not globally accessible, in such case `api_public_url` should be set as well.
104110
api_public_url: The globally accessible URL of the Apify API server. It should be set only if the `api_url`
105111
is an internal URL that is not globally accessible. Defaults to https://api.apify.com.
106-
max_retries: How many times to retry a failed request at most.
112+
max_retries: How many times to retry a failed request at most. Only used when `http_client` is not
113+
provided.
107114
min_delay_between_retries: How long will the client wait between retrying requests
108-
(increases exponentially from this value).
109-
timeout: The socket timeout of the HTTP requests sent to the Apify API.
110-
headers: Additional HTTP headers to include in all API requests.
115+
(increases exponentially from this value). Only used when `http_client` is not provided.
116+
timeout: The socket timeout of the HTTP requests sent to the Apify API. Only used when `http_client`
117+
is not provided.
118+
headers: Additional HTTP headers to include in all API requests. Only used when `http_client` is not
119+
provided.
120+
http_client: A custom HTTP client instance extending `HttpClient`. When provided, the `max_retries`,
121+
`min_delay_between_retries`, `timeout`, and `headers` parameters are ignored, as the custom
122+
client is responsible for its own configuration.
111123
"""
112124
# We need to do this because of mocking in tests and default mutable arguments.
113125
api_url = DEFAULT_API_URL if api_url is None else api_url
114126
api_public_url = DEFAULT_API_URL if api_public_url is None else api_public_url
115127

116-
if headers:
128+
if headers and not http_client:
117129
self._check_custom_headers(headers)
118130

119131
self._token = token
@@ -128,14 +140,17 @@ def __init__(
128140
self._statistics = ClientStatistics()
129141
"""Collector for client request statistics."""
130142

131-
self._http_client = HttpClient(
132-
token=self._token,
133-
timeout=timeout,
134-
max_retries=max_retries,
135-
min_delay_between_retries=min_delay_between_retries,
136-
statistics=self._statistics,
137-
headers=headers,
138-
)
143+
if http_client is not None:
144+
self._http_client: HttpClient = http_client
145+
else:
146+
self._http_client = ImpitHttpClient(
147+
token=self._token,
148+
timeout=timeout,
149+
max_retries=max_retries,
150+
min_delay_between_retries=min_delay_between_retries,
151+
statistics=self._statistics,
152+
headers=headers,
153+
)
139154
"""HTTP client used to communicate with the Apify API."""
140155

141156
self._client_registry = ClientRegistry(
@@ -198,6 +213,15 @@ def token(self) -> str | None:
198213
"""The Apify API token used by the client."""
199214
return self._token
200215

216+
@property
217+
def http_client(self) -> HttpClient:
218+
"""The HTTP client instance used for API communication.
219+
220+
Returns the custom HTTP client if one was provided during initialization,
221+
or the default `ImpitHttpClient` otherwise.
222+
"""
223+
return self._http_client
224+
201225
def actor(self, actor_id: str) -> ActorClient:
202226
"""Retrieve the sub-client for manipulating a single Actor.
203227
@@ -355,6 +379,7 @@ def __init__(
355379
min_delay_between_retries: timedelta = DEFAULT_MIN_DELAY_BETWEEN_RETRIES,
356380
timeout: timedelta = DEFAULT_TIMEOUT,
357381
headers: dict[str, str] | None = None,
382+
http_client: HttpClientAsync | None = None,
358383
) -> None:
359384
"""Initialize a new instance.
360385
@@ -364,17 +389,23 @@ def __init__(
364389
be an internal URL that is not globally accessible, in such case `api_public_url` should be set as well.
365390
api_public_url: The globally accessible URL of the Apify API server. It should be set only if the `api_url`
366391
is an internal URL that is not globally accessible. Defaults to https://api.apify.com.
367-
max_retries: How many times to retry a failed request at most.
392+
max_retries: How many times to retry a failed request at most. Only used when `http_client` is not
393+
provided.
368394
min_delay_between_retries: How long will the client wait between retrying requests
369-
(increases exponentially from this value).
370-
timeout: The socket timeout of the HTTP requests sent to the Apify API.
371-
headers: Additional HTTP headers to include in all API requests.
395+
(increases exponentially from this value). Only used when `http_client` is not provided.
396+
timeout: The socket timeout of the HTTP requests sent to the Apify API. Only used when `http_client`
397+
is not provided.
398+
headers: Additional HTTP headers to include in all API requests. Only used when `http_client` is not
399+
provided.
400+
http_client: A custom HTTP client instance extending `HttpClientAsync`. When provided, the `max_retries`,
401+
`min_delay_between_retries`, `timeout`, and `headers` parameters are ignored, as the custom
402+
client is responsible for its own configuration.
372403
"""
373404
# We need to do this because of mocking in tests and default mutable arguments.
374405
api_url = DEFAULT_API_URL if api_url is None else api_url
375406
api_public_url = DEFAULT_API_URL if api_public_url is None else api_public_url
376407

377-
if headers:
408+
if headers and not http_client:
378409
self._check_custom_headers(headers)
379410

380411
self._token = token
@@ -389,14 +420,17 @@ def __init__(
389420
self._statistics = ClientStatistics()
390421
"""Collector for client request statistics."""
391422

392-
self._http_client = HttpClientAsync(
393-
token=self._token,
394-
timeout=timeout,
395-
max_retries=max_retries,
396-
min_delay_between_retries=min_delay_between_retries,
397-
statistics=self._statistics,
398-
headers=headers,
399-
)
423+
if http_client is not None:
424+
self._http_client: HttpClientAsync = http_client
425+
else:
426+
self._http_client = ImpitHttpClientAsync(
427+
token=self._token,
428+
timeout=timeout,
429+
max_retries=max_retries,
430+
min_delay_between_retries=min_delay_between_retries,
431+
statistics=self._statistics,
432+
headers=headers,
433+
)
400434
"""HTTP client used to communicate with the Apify API."""
401435

402436
self._client_registry = ClientRegistryAsync(
@@ -459,6 +493,15 @@ def token(self) -> str | None:
459493
"""The Apify API token used by the client."""
460494
return self._token
461495

496+
@property
497+
def http_client(self) -> HttpClientAsync:
498+
"""The HTTP client instance used for API communication.
499+
500+
Returns the custom HTTP client if one was provided during initialization,
501+
or the default `ImpitHttpClientAsync` otherwise.
502+
"""
503+
return self._http_client
504+
462505
def actor(self, actor_id: str) -> ActorClientAsync:
463506
"""Retrieve the sub-client for manipulating a single Actor.
464507
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)