Skip to content

Commit 87911c4

Browse files
committed
Updated exp backoff to increse timeout as well.
Updated some clients endpoints with spefici timeouts. Added tests. TODO: Add async tests. Make some deal with Mypy
1 parent 3059a17 commit 87911c4

File tree

6 files changed

+220
-22
lines changed

6 files changed

+220
-22
lines changed

src/apify_client/_http_client.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ def call(
143143
json: JSONSerializable | None = None,
144144
stream: bool | None = None,
145145
parse_response: bool | None = True,
146+
timeout_secs: int | None = None,
146147
) -> httpx.Response:
147148
log_context.method.set(method)
148149
log_context.url.set(url)
@@ -170,6 +171,16 @@ def _make_request(stop_retrying: Callable, attempt: int) -> httpx.Response:
170171
params=params,
171172
content=content,
172173
)
174+
175+
# Increase timeout with each attempt. Max timeout is bounded by the client timeout.
176+
timeout = min(self.timeout_secs, (timeout_secs or self.timeout_secs) * 2 ** (attempt - 1))
177+
request.extensions['timeout'] = {
178+
'connect': timeout,
179+
'pool': timeout,
180+
'read': timeout,
181+
'write': timeout,
182+
}
183+
173184
response = httpx_client.send(
174185
request=request,
175186
stream=stream or False,
@@ -225,6 +236,7 @@ async def call(
225236
json: JSONSerializable | None = None,
226237
stream: bool | None = None,
227238
parse_response: bool | None = True,
239+
timeout_secs: int | None = None,
228240
) -> httpx.Response:
229241
log_context.method.set(method)
230242
log_context.url.set(url)
@@ -249,6 +261,16 @@ async def _make_request(stop_retrying: Callable, attempt: int) -> httpx.Response
249261
params=params,
250262
content=content,
251263
)
264+
265+
# Increase timeout with each attempt. Max timeout is bounded by the client timeout.
266+
timeout = min(self.timeout_secs, (timeout_secs or self.timeout_secs) * 2 ** (attempt - 1))
267+
request.extensions['timeout'] = {
268+
'connect': timeout,
269+
'pool': timeout,
270+
'read': timeout,
271+
'write': timeout,
272+
}
273+
252274
response = await httpx_async_client.send(
253275
request=request,
254276
stream=stream or False,

src/apify_client/clients/base/resource_client.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,13 @@
1111
class ResourceClient(BaseClient):
1212
"""Base class for sub-clients manipulating a single resource."""
1313

14-
def _get(self) -> dict | None:
14+
def _get(self, timeout_secs: int | None = None) -> dict | None:
1515
try:
1616
response = self.http_client.call(
1717
url=self.url,
1818
method='GET',
1919
params=self._params(),
20+
timeout_secs=timeout_secs,
2021
)
2122

2223
return parse_date_fields(pluck_data(response.json()))
@@ -26,22 +27,24 @@ def _get(self) -> dict | None:
2627

2728
return None
2829

29-
def _update(self, updated_fields: dict) -> dict:
30+
def _update(self, updated_fields: dict, timeout_secs: int | None = None) -> dict:
3031
response = self.http_client.call(
3132
url=self._url(),
3233
method='PUT',
3334
params=self._params(),
3435
json=updated_fields,
36+
timeout_secs=timeout_secs,
3537
)
3638

3739
return parse_date_fields(pluck_data(response.json()))
3840

39-
def _delete(self) -> None:
41+
def _delete(self, timeout_secs: int | None = None) -> None:
4042
try:
4143
self.http_client.call(
4244
url=self._url(),
4345
method='DELETE',
4446
params=self._params(),
47+
timeout_secs=timeout_secs,
4548
)
4649

4750
except ApifyApiError as exc:
@@ -52,12 +55,13 @@ def _delete(self) -> None:
5255
class ResourceClientAsync(BaseClientAsync):
5356
"""Base class for async sub-clients manipulating a single resource."""
5457

55-
async def _get(self) -> dict | None:
58+
async def _get(self, timeout_secs: int | None = None) -> dict | None:
5659
try:
5760
response = await self.http_client.call(
5861
url=self.url,
5962
method='GET',
6063
params=self._params(),
64+
timeout_secs=timeout_secs,
6165
)
6266

6367
return parse_date_fields(pluck_data(response.json()))
@@ -67,22 +71,24 @@ async def _get(self) -> dict | None:
6771

6872
return None
6973

70-
async def _update(self, updated_fields: dict) -> dict:
74+
async def _update(self, updated_fields: dict, timeout_secs: int | None = None) -> dict:
7175
response = await self.http_client.call(
7276
url=self._url(),
7377
method='PUT',
7478
params=self._params(),
7579
json=updated_fields,
80+
timeout_secs=timeout_secs,
7681
)
7782

7883
return parse_date_fields(pluck_data(response.json()))
7984

80-
async def _delete(self) -> None:
85+
async def _delete(self, timeout_secs: int | None = None) -> None:
8186
try:
8287
await self.http_client.call(
8388
url=self._url(),
8489
method='DELETE',
8590
params=self._params(),
91+
timeout_secs=timeout_secs,
8692
)
8793

8894
except ApifyApiError as exc:

src/apify_client/clients/resource_clients/dataset.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
import httpx
1818
from apify_shared.types import JSONSerializable
1919

20+
_SMALL_TIMEOUT = 5 # For fast and common actions. Suitable for idempotent actions.
21+
_MEDIUM_TIMEOUT = 30 # For actions that may take longer.
22+
2023

2124
class DatasetClient(ResourceClient):
2225
"""Sub-client for manipulating a single dataset."""
@@ -34,7 +37,7 @@ def get(self) -> dict | None:
3437
Returns:
3538
The retrieved dataset, or None, if it does not exist.
3639
"""
37-
return self._get()
40+
return self._get(timeout_secs=_SMALL_TIMEOUT)
3841

3942
def update(self, *, name: str | None = None) -> dict:
4043
"""Update the dataset with specified fields.
@@ -49,14 +52,14 @@ def update(self, *, name: str | None = None) -> dict:
4952
"""
5053
updated_fields = {'name': name}
5154

52-
return self._update(filter_out_none_values_recursively(updated_fields))
55+
return self._update(filter_out_none_values_recursively(updated_fields), timeout_secs=_SMALL_TIMEOUT)
5356

5457
def delete(self) -> None:
5558
"""Delete the dataset.
5659
5760
https://docs.apify.com/api/v2#/reference/datasets/dataset/delete-dataset
5861
"""
59-
return self._delete()
62+
return self._delete(timeout_secs=_SMALL_TIMEOUT)
6063

6164
def list_items(
6265
self,
@@ -539,6 +542,7 @@ def push_items(self, items: JSONSerializable) -> None:
539542
params=self._params(),
540543
data=data,
541544
json=json,
545+
timeout_secs=_MEDIUM_TIMEOUT,
542546
)
543547

544548
def get_statistics(self) -> dict | None:
@@ -554,6 +558,7 @@ def get_statistics(self) -> dict | None:
554558
url=self._url('statistics'),
555559
method='GET',
556560
params=self._params(),
561+
timeout_secs=_SMALL_TIMEOUT,
557562
)
558563
return pluck_data(response.json())
559564
except ApifyApiError as exc:
@@ -578,7 +583,7 @@ async def get(self) -> dict | None:
578583
Returns:
579584
The retrieved dataset, or None, if it does not exist.
580585
"""
581-
return await self._get()
586+
return await self._get(timeout_secs=_SMALL_TIMEOUT)
582587

583588
async def update(self, *, name: str | None = None) -> dict:
584589
"""Update the dataset with specified fields.
@@ -593,14 +598,14 @@ async def update(self, *, name: str | None = None) -> dict:
593598
"""
594599
updated_fields = {'name': name}
595600

596-
return await self._update(filter_out_none_values_recursively(updated_fields))
601+
return await self._update(filter_out_none_values_recursively(updated_fields), timeout_secs=_SMALL_TIMEOUT)
597602

598603
async def delete(self) -> None:
599604
"""Delete the dataset.
600605
601606
https://docs.apify.com/api/v2#/reference/datasets/dataset/delete-dataset
602607
"""
603-
return await self._delete()
608+
return await self._delete(timeout_secs=_SMALL_TIMEOUT)
604609

605610
async def list_items(
606611
self,
@@ -990,6 +995,7 @@ async def push_items(self, items: JSONSerializable) -> None:
990995
params=self._params(),
991996
data=data,
992997
json=json,
998+
timeout_secs=_MEDIUM_TIMEOUT,
993999
)
9941000

9951001
async def get_statistics(self) -> dict | None:
@@ -1005,6 +1011,7 @@ async def get_statistics(self) -> dict | None:
10051011
url=self._url('statistics'),
10061012
method='GET',
10071013
params=self._params(),
1014+
timeout_secs=_SMALL_TIMEOUT,
10081015
)
10091016
return pluck_data(response.json())
10101017
except ApifyApiError as exc:

src/apify_client/clients/resource_clients/key_value_store.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
if TYPE_CHECKING:
1414
from collections.abc import AsyncIterator, Iterator
1515

16+
_SMALL_TIMEOUT = 5 # For fast and common actions. Suitable for idempotent actions.
17+
_MEDIUM_TIMEOUT = 30 # For actions that may take longer.
18+
1619

1720
class KeyValueStoreClient(ResourceClient):
1821
"""Sub-client for manipulating a single key-value store."""
@@ -30,7 +33,7 @@ def get(self) -> dict | None:
3033
Returns:
3134
The retrieved key-value store, or None if it does not exist.
3235
"""
33-
return self._get()
36+
return self._get(timeout_secs=_SMALL_TIMEOUT)
3437

3538
def update(self, *, name: str | None = None) -> dict:
3639
"""Update the key-value store with specified fields.
@@ -54,7 +57,7 @@ def delete(self) -> None:
5457
5558
https://docs.apify.com/api/v2#/reference/key-value-stores/store-object/delete-store
5659
"""
57-
return self._delete()
60+
return self._delete(timeout_secs=_SMALL_TIMEOUT)
5861

5962
def list_keys(self, *, limit: int | None = None, exclusive_start_key: str | None = None) -> dict:
6063
"""List the keys in the key-value store.
@@ -74,6 +77,7 @@ def list_keys(self, *, limit: int | None = None, exclusive_start_key: str | None
7477
url=self._url('keys'),
7578
method='GET',
7679
params=request_params,
80+
timeout_secs=_MEDIUM_TIMEOUT,
7781
)
7882

7983
return parse_date_fields(pluck_data(response.json()))
@@ -236,6 +240,7 @@ def delete_record(self, key: str) -> None:
236240
url=self._url(f'records/{key}'),
237241
method='DELETE',
238242
params=self._params(),
243+
timeout_secs=_SMALL_TIMEOUT,
239244
)
240245

241246

@@ -255,7 +260,7 @@ async def get(self) -> dict | None:
255260
Returns:
256261
The retrieved key-value store, or None if it does not exist.
257262
"""
258-
return await self._get()
263+
return await self._get(timeout_secs=_SMALL_TIMEOUT)
259264

260265
async def update(self, *, name: str | None = None) -> dict:
261266
"""Update the key-value store with specified fields.
@@ -279,7 +284,7 @@ async def delete(self) -> None:
279284
280285
https://docs.apify.com/api/v2#/reference/key-value-stores/store-object/delete-store
281286
"""
282-
return await self._delete()
287+
return await self._delete(timeout_secs=_SMALL_TIMEOUT)
283288

284289
async def list_keys(self, *, limit: int | None = None, exclusive_start_key: str | None = None) -> dict:
285290
"""List the keys in the key-value store.
@@ -299,6 +304,7 @@ async def list_keys(self, *, limit: int | None = None, exclusive_start_key: str
299304
url=self._url('keys'),
300305
method='GET',
301306
params=request_params,
307+
timeout_secs=_MEDIUM_TIMEOUT,
302308
)
303309

304310
return parse_date_fields(pluck_data(response.json()))
@@ -440,4 +446,5 @@ async def delete_record(self, key: str) -> None:
440446
url=self._url(f'records/{key}'),
441447
method='DELETE',
442448
params=self._params(),
449+
timeout_secs=_SMALL_TIMEOUT,
443450
)

0 commit comments

Comments
 (0)