Skip to content

Commit a73aae1

Browse files
vdusekclaude
andcommitted
refactor: Standardize timeout handling across all resource clients
- Replace three inconsistent timeout constants (DEFAULT_TIMEOUT 360s, FAST_OPERATION_TIMEOUT 5s, STANDARD_OPERATION_TIMEOUT 30s) with a single DEFAULT_REQUEST_TIMEOUT (30s) that grows exponentially on retry - Add `timeout` parameter to all public resource client methods for user-controllable HTTP request timeouts - Add timeout parameter to base methods `_list()`, `_create()`, and `_get_or_create()` which previously lacked it - Rename domain-specific Actor/Task run timeout from `timeout` to `run_timeout` to avoid ambiguity with the HTTP request timeout Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 841d760 commit a73aae1

33 files changed

+1207
-375
lines changed

src/apify_client/_apify_client.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
DEFAULT_API_URL,
1010
DEFAULT_MAX_RETRIES,
1111
DEFAULT_MIN_DELAY_BETWEEN_RETRIES,
12-
DEFAULT_TIMEOUT,
12+
DEFAULT_REQUEST_TIMEOUT,
1313
)
1414
from apify_client._docs import docs_group
1515
from apify_client._http_clients import HttpClient, HttpClientAsync, ImpitHttpClient, ImpitHttpClientAsync
@@ -114,7 +114,7 @@ def __init__(
114114
api_public_url: str | None = DEFAULT_API_URL,
115115
max_retries: int = DEFAULT_MAX_RETRIES,
116116
min_delay_between_retries: timedelta = DEFAULT_MIN_DELAY_BETWEEN_RETRIES,
117-
timeout: timedelta = DEFAULT_TIMEOUT,
117+
timeout: timedelta = DEFAULT_REQUEST_TIMEOUT,
118118
headers: dict[str, str] | None = None,
119119
) -> None:
120120
"""Initialize the Apify API client.
@@ -455,7 +455,7 @@ def __init__(
455455
api_public_url: str | None = DEFAULT_API_URL,
456456
max_retries: int = DEFAULT_MAX_RETRIES,
457457
min_delay_between_retries: timedelta = DEFAULT_MIN_DELAY_BETWEEN_RETRIES,
458-
timeout: timedelta = DEFAULT_TIMEOUT,
458+
timeout: timedelta = DEFAULT_REQUEST_TIMEOUT,
459459
headers: dict[str, str] | None = None,
460460
) -> None:
461461
"""Initialize the Apify API client.

src/apify_client/_consts.py

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616
API_VERSION = 'v2'
1717
"""Current Apify API version."""
1818

19-
DEFAULT_TIMEOUT = timedelta(seconds=360)
20-
"""Default request timeout."""
19+
DEFAULT_REQUEST_TIMEOUT = timedelta(seconds=30)
20+
"""Default timeout for individual API requests."""
2121

2222
DEFAULT_MAX_RETRIES = 8
2323
"""Default maximum number of retries for failed requests."""
@@ -31,12 +31,6 @@
3131
DEFAULT_WAIT_WHEN_JOB_NOT_EXIST = timedelta(seconds=3)
3232
"""How long to wait for a job to exist before giving up."""
3333

34-
FAST_OPERATION_TIMEOUT = timedelta(seconds=5)
35-
"""Timeout for fast, idempotent operations (e.g., GET, DELETE)."""
36-
37-
STANDARD_OPERATION_TIMEOUT = timedelta(seconds=30)
38-
"""Timeout for operations that may take longer (e.g., list operations, batch operations)."""
39-
4034
TERMINAL_STATUSES = frozenset(
4135
{
4236
ActorJobStatus.SUCCEEDED,

src/apify_client/_http_clients/_base.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
1111
from urllib.parse import urlencode
1212

13-
from apify_client._consts import DEFAULT_MAX_RETRIES, DEFAULT_MIN_DELAY_BETWEEN_RETRIES, DEFAULT_TIMEOUT
13+
from apify_client._consts import DEFAULT_MAX_RETRIES, DEFAULT_MIN_DELAY_BETWEEN_RETRIES, DEFAULT_REQUEST_TIMEOUT
1414
from apify_client._docs import docs_group
1515
from apify_client._statistics import ClientStatistics
1616
from apify_client._utils import to_seconds
@@ -85,7 +85,7 @@ def __init__(
8585
self,
8686
*,
8787
token: str | None = None,
88-
timeout: timedelta = DEFAULT_TIMEOUT,
88+
timeout: timedelta = DEFAULT_REQUEST_TIMEOUT,
8989
max_retries: int = DEFAULT_MAX_RETRIES,
9090
min_delay_between_retries: timedelta = DEFAULT_MIN_DELAY_BETWEEN_RETRIES,
9191
statistics: ClientStatistics | None = None,
@@ -240,7 +240,6 @@ def call(
240240
ApifyApiError: If the request fails after all retries or returns a non-retryable error status.
241241
ValueError: If both json and data are provided.
242242
"""
243-
...
244243

245244

246245
@docs_group('HTTP clients')
@@ -283,4 +282,3 @@ async def call(
283282
ApifyApiError: If the request fails after all retries or returns a non-retryable error status.
284283
ValueError: If both json and data are provided.
285284
"""
286-
...

src/apify_client/_http_clients/_impit.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
import impit
1212

13-
from apify_client._consts import DEFAULT_MAX_RETRIES, DEFAULT_MIN_DELAY_BETWEEN_RETRIES, DEFAULT_TIMEOUT
13+
from apify_client._consts import DEFAULT_MAX_RETRIES, DEFAULT_MIN_DELAY_BETWEEN_RETRIES, DEFAULT_REQUEST_TIMEOUT
1414
from apify_client._docs import docs_group
1515
from apify_client._http_clients import HttpClient, HttpClientAsync
1616
from apify_client._logging import log_context, logger_name
@@ -58,7 +58,7 @@ def __init__(
5858
self,
5959
*,
6060
token: str | None = None,
61-
timeout: timedelta = DEFAULT_TIMEOUT,
61+
timeout: timedelta = DEFAULT_REQUEST_TIMEOUT,
6262
max_retries: int = DEFAULT_MAX_RETRIES,
6363
min_delay_between_retries: timedelta = DEFAULT_MIN_DELAY_BETWEEN_RETRIES,
6464
statistics: ClientStatistics | None = None,
@@ -284,7 +284,7 @@ def __init__(
284284
self,
285285
*,
286286
token: str | None = None,
287-
timeout: timedelta = DEFAULT_TIMEOUT,
287+
timeout: timedelta = DEFAULT_REQUEST_TIMEOUT,
288288
max_retries: int = DEFAULT_MAX_RETRIES,
289289
min_delay_between_retries: timedelta = DEFAULT_MIN_DELAY_BETWEEN_RETRIES,
290290
statistics: ClientStatistics | None = None,

src/apify_client/_resource_clients/_resource_client.py

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@
66
from functools import cached_property
77
from typing import TYPE_CHECKING, Any
88

9-
from apify_client._consts import DEFAULT_WAIT_FOR_FINISH, DEFAULT_WAIT_WHEN_JOB_NOT_EXIST, TERMINAL_STATUSES
9+
from apify_client._consts import (
10+
DEFAULT_REQUEST_TIMEOUT,
11+
DEFAULT_WAIT_FOR_FINISH,
12+
DEFAULT_WAIT_WHEN_JOB_NOT_EXIST,
13+
TERMINAL_STATUSES,
14+
)
1015
from apify_client._docs import docs_group
1116
from apify_client._internal_models import ActorJobResponse
1217
from apify_client._logging import WithLogDetailsClient
@@ -192,7 +197,7 @@ def __init__(
192197
params=params,
193198
)
194199

195-
def _get(self, *, timeout: timedelta | None = None) -> dict | None:
200+
def _get(self, *, timeout: timedelta | None = DEFAULT_REQUEST_TIMEOUT) -> dict | None:
196201
"""Perform a GET request for this resource, returning the parsed response or None if not found."""
197202
try:
198203
response = self._http_client.call(
@@ -206,7 +211,7 @@ def _get(self, *, timeout: timedelta | None = None) -> dict | None:
206211
catch_not_found_or_throw(exc)
207212
return None
208213

209-
def _update(self, *, timeout: timedelta | None = None, **kwargs: Any) -> dict:
214+
def _update(self, *, timeout: timedelta | None = DEFAULT_REQUEST_TIMEOUT, **kwargs: Any) -> dict:
210215
"""Perform a PUT request to update this resource with the given fields."""
211216
response = self._http_client.call(
212217
url=self._build_url(),
@@ -217,7 +222,7 @@ def _update(self, *, timeout: timedelta | None = None, **kwargs: Any) -> dict:
217222
)
218223
return response_to_dict(response)
219224

220-
def _delete(self, *, timeout: timedelta | None = None) -> None:
225+
def _delete(self, *, timeout: timedelta | None = DEFAULT_REQUEST_TIMEOUT) -> None:
221226
"""Perform a DELETE request to delete this resource, ignoring 404 errors."""
222227
try:
223228
self._http_client.call(
@@ -229,32 +234,41 @@ def _delete(self, *, timeout: timedelta | None = None) -> None:
229234
except ApifyApiError as exc:
230235
catch_not_found_or_throw(exc)
231236

232-
def _list(self, **kwargs: Any) -> dict:
237+
def _list(self, *, timeout: timedelta | None = DEFAULT_REQUEST_TIMEOUT, **kwargs: Any) -> dict:
233238
"""Perform a GET request to list resources."""
234239
response = self._http_client.call(
235240
url=self._build_url(),
236241
method='GET',
237242
params=self._build_params(**kwargs),
243+
timeout=timeout,
238244
)
239245
return response_to_dict(response)
240246

241-
def _create(self, **kwargs: Any) -> dict:
247+
def _create(self, *, timeout: timedelta | None = DEFAULT_REQUEST_TIMEOUT, **kwargs: Any) -> dict:
242248
"""Perform a POST request to create a resource."""
243249
response = self._http_client.call(
244250
url=self._build_url(),
245251
method='POST',
246252
params=self._build_params(),
247253
json=self._clean_json_payload(kwargs),
254+
timeout=timeout,
248255
)
249256
return response_to_dict(response)
250257

251-
def _get_or_create(self, *, name: str | None = None, resource_fields: dict | None = None) -> dict:
258+
def _get_or_create(
259+
self,
260+
*,
261+
name: str | None = None,
262+
resource_fields: dict | None = None,
263+
timeout: timedelta | None = DEFAULT_REQUEST_TIMEOUT,
264+
) -> dict:
252265
"""Perform a POST request to get or create a named resource."""
253266
response = self._http_client.call(
254267
url=self._build_url(),
255268
method='POST',
256269
params=self._build_params(name=name),
257270
json=self._clean_json_payload(resource_fields) if resource_fields is not None else None,
271+
timeout=timeout,
258272
)
259273
return response_to_dict(response)
260274

@@ -263,6 +277,7 @@ def _wait_for_finish(
263277
url: str,
264278
params: dict,
265279
wait_duration: timedelta | None = None,
280+
timeout: timedelta | None = None,
266281
) -> dict | None:
267282
"""Wait synchronously for an Actor job (run or build) to finish.
268283
@@ -273,6 +288,7 @@ def _wait_for_finish(
273288
url: Full URL to the job endpoint.
274289
params: Base query parameters to include in each request.
275290
wait_duration: Maximum time to wait (None = indefinite).
291+
timeout: Timeout for each HTTP request (None = no timeout).
276292
277293
Returns:
278294
Job data dict when finished, or None if job doesn't exist after
@@ -298,6 +314,7 @@ def _wait_for_finish(
298314
url=url,
299315
method='GET',
300316
params={**params, 'waitForFinish': wait_for_finish},
317+
timeout=timeout,
301318
)
302319
result = response_to_dict(response)
303320
actor_job_response = ActorJobResponse.model_validate(result)
@@ -359,7 +376,7 @@ def __init__(
359376
params=params,
360377
)
361378

362-
async def _get(self, *, timeout: timedelta | None = None) -> dict | None:
379+
async def _get(self, *, timeout: timedelta | None = DEFAULT_REQUEST_TIMEOUT) -> dict | None:
363380
"""Perform a GET request for this resource, returning the parsed response or None if not found."""
364381
try:
365382
response = await self._http_client.call(
@@ -373,7 +390,7 @@ async def _get(self, *, timeout: timedelta | None = None) -> dict | None:
373390
catch_not_found_or_throw(exc)
374391
return None
375392

376-
async def _update(self, *, timeout: timedelta | None = None, **kwargs: Any) -> dict:
393+
async def _update(self, *, timeout: timedelta | None = DEFAULT_REQUEST_TIMEOUT, **kwargs: Any) -> dict:
377394
"""Perform a PUT request to update this resource with the given fields."""
378395
response = await self._http_client.call(
379396
url=self._build_url(),
@@ -384,7 +401,7 @@ async def _update(self, *, timeout: timedelta | None = None, **kwargs: Any) -> d
384401
)
385402
return response_to_dict(response)
386403

387-
async def _delete(self, *, timeout: timedelta | None = None) -> None:
404+
async def _delete(self, *, timeout: timedelta | None = DEFAULT_REQUEST_TIMEOUT) -> None:
388405
"""Perform a DELETE request to delete this resource, ignoring 404 errors."""
389406
try:
390407
await self._http_client.call(
@@ -396,32 +413,41 @@ async def _delete(self, *, timeout: timedelta | None = None) -> None:
396413
except ApifyApiError as exc:
397414
catch_not_found_or_throw(exc)
398415

399-
async def _list(self, **kwargs: Any) -> dict:
416+
async def _list(self, *, timeout: timedelta | None = DEFAULT_REQUEST_TIMEOUT, **kwargs: Any) -> dict:
400417
"""Perform a GET request to list resources."""
401418
response = await self._http_client.call(
402419
url=self._build_url(),
403420
method='GET',
404421
params=self._build_params(**kwargs),
422+
timeout=timeout,
405423
)
406424
return response_to_dict(response)
407425

408-
async def _create(self, **kwargs: Any) -> dict:
426+
async def _create(self, *, timeout: timedelta | None = DEFAULT_REQUEST_TIMEOUT, **kwargs: Any) -> dict:
409427
"""Perform a POST request to create a resource."""
410428
response = await self._http_client.call(
411429
url=self._build_url(),
412430
method='POST',
413431
params=self._build_params(),
414432
json=self._clean_json_payload(kwargs),
433+
timeout=timeout,
415434
)
416435
return response_to_dict(response)
417436

418-
async def _get_or_create(self, *, name: str | None = None, resource_fields: dict | None = None) -> dict:
437+
async def _get_or_create(
438+
self,
439+
*,
440+
name: str | None = None,
441+
resource_fields: dict | None = None,
442+
timeout: timedelta | None = DEFAULT_REQUEST_TIMEOUT,
443+
) -> dict:
419444
"""Perform a POST request to get or create a named resource."""
420445
response = await self._http_client.call(
421446
url=self._build_url(),
422447
method='POST',
423448
params=self._build_params(name=name),
424449
json=self._clean_json_payload(resource_fields) if resource_fields is not None else None,
450+
timeout=timeout,
425451
)
426452
return response_to_dict(response)
427453

@@ -430,6 +456,7 @@ async def _wait_for_finish(
430456
url: str,
431457
params: dict,
432458
wait_duration: timedelta | None = None,
459+
timeout: timedelta | None = None,
433460
) -> dict | None:
434461
"""Wait asynchronously for an Actor job (run or build) to finish.
435462
@@ -440,6 +467,7 @@ async def _wait_for_finish(
440467
url: Full URL to the job endpoint.
441468
params: Base query parameters to include in each request.
442469
wait_duration: Maximum time to wait (None = indefinite).
470+
timeout: Timeout for each HTTP request (None = no timeout).
443471
444472
Returns:
445473
Job data dict when finished, or None if job doesn't exist after
@@ -465,6 +493,7 @@ async def _wait_for_finish(
465493
url=url,
466494
method='GET',
467495
params={**params, 'waitForFinish': wait_for_finish},
496+
timeout=timeout,
468497
)
469498
result = response_to_dict(response)
470499
actor_job_response = ActorJobResponse.model_validate(result)

0 commit comments

Comments
 (0)