apify-client 3.2.2b1__tar.gz → 3.2.2b2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/CHANGELOG.md +5 -0
  2. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/PKG-INFO +1 -1
  3. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/pyproject.toml +2 -2
  4. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/pyproject.toml.orig +2 -2
  5. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/_models.py +10 -0
  6. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/_resource_clients/actor.py +65 -14
  7. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/_resource_clients/task.py +57 -14
  8. apify_client-3.2.2b2/src/apify_client/_utils/wait_for_resources.py +82 -0
  9. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/CONTRIBUTING.md +0 -0
  10. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/LICENSE +0 -0
  11. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/README.md +0 -0
  12. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/__init__.py +0 -0
  13. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/_apify_client.py +0 -0
  14. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/_client_registry.py +0 -0
  15. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/_consts.py +0 -0
  16. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/_docs.py +0 -0
  17. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/_literals.py +0 -0
  18. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/_logging.py +0 -0
  19. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/_pagination.py +0 -0
  20. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/_resource_clients/__init__.py +0 -0
  21. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/_resource_clients/_resource_client.py +0 -0
  22. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/_resource_clients/actor_collection.py +0 -0
  23. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/_resource_clients/actor_env_var.py +0 -0
  24. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/_resource_clients/actor_env_var_collection.py +0 -0
  25. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/_resource_clients/actor_version.py +0 -0
  26. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/_resource_clients/actor_version_collection.py +0 -0
  27. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/_resource_clients/build.py +0 -0
  28. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/_resource_clients/build_collection.py +0 -0
  29. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/_resource_clients/dataset.py +0 -0
  30. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/_resource_clients/dataset_collection.py +0 -0
  31. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/_resource_clients/key_value_store.py +0 -0
  32. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/_resource_clients/key_value_store_collection.py +0 -0
  33. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/_resource_clients/log.py +0 -0
  34. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/_resource_clients/request_queue.py +0 -0
  35. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/_resource_clients/request_queue_collection.py +0 -0
  36. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/_resource_clients/run.py +0 -0
  37. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/_resource_clients/run_collection.py +0 -0
  38. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/_resource_clients/schedule.py +0 -0
  39. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/_resource_clients/schedule_collection.py +0 -0
  40. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/_resource_clients/store_collection.py +0 -0
  41. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/_resource_clients/task_collection.py +0 -0
  42. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/_resource_clients/user.py +0 -0
  43. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/_resource_clients/webhook.py +0 -0
  44. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/_resource_clients/webhook_collection.py +0 -0
  45. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/_resource_clients/webhook_dispatch.py +0 -0
  46. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/_resource_clients/webhook_dispatch_collection.py +0 -0
  47. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/_statistics.py +0 -0
  48. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/_status_message_watcher.py +0 -0
  49. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/_streamed_log.py +0 -0
  50. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/_typeddicts.py +0 -0
  51. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/_utils/__init__.py +0 -0
  52. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/_utils/crypto.py +0 -0
  53. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/_utils/encoding.py +0 -0
  54. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/_utils/errors.py +0 -0
  55. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/_utils/http.py +0 -0
  56. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/_utils/time.py +0 -0
  57. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/_utils/try_import.py +0 -0
  58. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/errors.py +0 -0
  59. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/http_clients/__init__.py +0 -0
  60. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/http_clients/_base.py +0 -0
  61. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/http_clients/_httpx2.py +0 -0
  62. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/http_clients/_impit.py +0 -0
  63. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/http_clients/_streamed_body.py +0 -0
  64. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/http_compressors/__init__.py +0 -0
  65. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/http_compressors/_base.py +0 -0
  66. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/http_compressors/_brotli.py +0 -0
  67. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/http_compressors/_gzip.py +0 -0
  68. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/http_compressors/_resolve.py +0 -0
  69. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/py.typed +0 -0
  70. {apify_client-3.2.2b1 → apify_client-3.2.2b2}/src/apify_client/types.py +0 -0
@@ -8,6 +8,11 @@ All notable changes to this project will be documented in this file.
8
8
  ### 🚀 Features
9
9
 
10
10
  - Stream request bodies from files, iterables, and responses ([#1060](https://github.com/apify/apify-client-python/pull/1060)) ([9be215b](https://github.com/apify/apify-client-python/commit/9be215b23a13de0abc4a255a18450cc764e7a6c7)) by [@vdusek](https://github.com/vdusek), closes [#972](https://github.com/apify/apify-client-python/issues/972)
11
+ - Retry starting a run on memory and concurrent-runs limits ([#1081](https://github.com/apify/apify-client-python/pull/1081)) ([3dd8ca5](https://github.com/apify/apify-client-python/commit/3dd8ca5fe4d2537cb009cb310ba12b39927a9558)) by [@vdusek](https://github.com/vdusek), closes [#1071](https://github.com/apify/apify-client-python/issues/1071)
12
+
13
+ ### 🐛 Bug Fixes
14
+
15
+ - Add missing `readme` field to `Profile` model ([#1086](https://github.com/apify/apify-client-python/pull/1086)) ([ae17148](https://github.com/apify/apify-client-python/commit/ae1714891c4f70d66ab4964c1783fee0a96776f1)) by [@apify-service-account](https://github.com/apify-service-account)
11
16
 
12
17
 
13
18
  <!-- git-cliff-unreleased-end -->
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: apify_client
3
- Version: 3.2.2b1
3
+ Version: 3.2.2b2
4
4
  Summary: Apify API client for Python
5
5
  Keywords: apify,api,client,automation,crawling,scraping
6
6
  Author: Apify Technologies s.r.o.
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "apify_client"
7
- version = "3.2.2b1"
7
+ version = "3.2.2b2"
8
8
  description = "Apify API client for Python"
9
9
  license = "Apache-2.0"
10
10
  license-files = ["LICENSE"]
@@ -246,7 +246,7 @@ disable_timestamp = true
246
246
  keep_model_order = true
247
247
 
248
248
  [tool.apify.openapi-spec]
249
- version = "v2-2026-08-31T125154Z"
249
+ version = "v2-2026-10-01T153946Z"
250
250
 
251
251
  [tool.uv]
252
252
  exclude-newer = "24 hours"
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "apify_client"
7
- version = "3.2.2b1"
7
+ version = "3.2.2b2"
8
8
  description = "Apify API client for Python"
9
9
  authors = [{ name = "Apify Technologies s.r.o.", email = "support@apify.com" }]
10
10
  license = "Apache-2.0"
@@ -250,7 +250,7 @@ keep_model_order = true
250
250
  # workflow committing nothing unless the models changed. A local regeneration that only moves this line is that
251
251
  # same case - drop it rather than committing it alone.
252
252
  [tool.apify.openapi-spec]
253
- version = "v2-2026-08-31T125154Z"
253
+ version = "v2-2026-10-01T153946Z"
254
254
 
255
255
  [tool.uv]
256
256
  # Minimal defense against supply-chain atatcks.
@@ -595,6 +595,12 @@ class Build(BaseModel):
595
595
  build_number: Annotated[
596
596
  str, Field(examples=['0.1.1'], pattern='^([0-9]|[1-9][0-9])\\.([0-9]|[1-9][0-9])(\\.[1-9][0-9]{0,4})$')
597
597
  ]
598
+ image_digest: Annotated[
599
+ str | None, Field(examples=['1b2f1e8c0d5a4c7f9e3b6a2d8c4e0f7a5b9d3c1e6f8a2b4d0c7e9f1a3b5d7c9e'])
600
+ ] = None
601
+ """
602
+ Digest of the built Docker image manifest, without the `sha256:` prefix. Compare digests of two builds to find out whether their image contents differ. `null` if the digest is not available.
603
+ """
598
604
  act_version: Annotated[ActVersion | None, Field(title='BuildActVersion')] = None
599
605
  """
600
606
  Snapshot of the Actor version that this build was created from.
@@ -2280,6 +2286,10 @@ class Profile(BaseModel):
2280
2286
  alias_generator=to_camel,
2281
2287
  )
2282
2288
  bio: Annotated[str | None, Field(examples=['I started web scraping in 1985 using Altair BASIC.'])] = None
2289
+ readme: Annotated[str | None, Field(examples=['### Hello world 👋🏻\nI build web scrapers.'])] = None
2290
+ """
2291
+ Markdown README shown on the user's public profile page.
2292
+ """
2283
2293
  name: Annotated[str | None, Field(examples=['Jane Doe'])] = None
2284
2294
  picture_url: Annotated[AnyUrl | None, Field(examples=['https://apify.com/img/anonymous_user_picture.png'])] = None
2285
2295
  github_username: Annotated[str | None, Field(examples=['torvalds.'])] = None
@@ -26,6 +26,11 @@ from apify_client._resource_clients._resource_client import ResourceClient, Reso
26
26
  from apify_client._utils.encoding import encode_key_value_store_record_value, encode_webhooks_to_base64
27
27
  from apify_client._utils.http import response_to_dict
28
28
  from apify_client._utils.time import to_seconds
29
+ from apify_client._utils.wait_for_resources import (
30
+ prepare_resendable_body,
31
+ start_waiting_for_resources,
32
+ start_waiting_for_resources_async,
33
+ )
29
34
 
30
35
  if TYPE_CHECKING:
31
36
  from datetime import timedelta
@@ -229,6 +234,7 @@ class ActorClient(ResourceClient):
229
234
  force_permission_level: ActorPermissionLevel | None = None,
230
235
  wait_for_finish: int | None = None,
231
236
  webhooks: WebhooksList | None = None,
237
+ wait_for_resources: bool | timedelta = False,
232
238
  timeout: Timeout = 'medium',
233
239
  ) -> Run:
234
240
  """Start the Actor and immediately return the Run object.
@@ -263,12 +269,21 @@ class ActorClient(ResourceClient):
263
269
  * `event_types`: List of `WebhookEventType` values which trigger the webhook.
264
270
  * `request_url`: URL to which to send the webhook HTTP request.
265
271
  * `payload_template`: Optional template for the request payload.
272
+ wait_for_resources: Retry the start while the account lacks the memory or a concurrent-run slot for the run,
273
+ that is while the API rejects it with an `ApifyApiError` of type `actor-memory-limit-exceeded` or
274
+ `concurrent-runs-limit-exceeded`. Both clear as other runs or builds finish. The start is retried
275
+ every 10 seconds, and any other error is raised right away. `True` retries until the run starts, a
276
+ `timedelta` stops retrying after that long and raises the last error. A run that requests more memory
277
+ than the whole memory limit of the account is rejected with `actor-memory-limit-exceeded` as well and
278
+ never starts, so `True` retries it forever. A streamed `run_input` that cannot be rewound, such as a
279
+ generator, is sent only once, so its start is not retried.
266
280
  timeout: Timeout for the API HTTP request.
267
281
 
268
282
  Returns:
269
283
  The run object.
270
284
  """
271
285
  run_input, content_type = encode_key_value_store_record_value(run_input, content_type=content_type)
286
+ run_input, wait_for_resources = prepare_resendable_body(run_input, wait_for_resources=wait_for_resources)
272
287
 
273
288
  request_params = self._build_params(
274
289
  build=build,
@@ -282,13 +297,16 @@ class ActorClient(ResourceClient):
282
297
  webhooks=encode_webhooks_to_base64(webhooks),
283
298
  )
284
299
 
285
- response = self._http_client.call(
286
- url=self._build_url('runs'),
287
- method='POST',
288
- headers={'content-type': content_type},
289
- data=run_input,
290
- params=request_params,
291
- timeout=timeout,
300
+ response = start_waiting_for_resources(
301
+ lambda: self._http_client.call(
302
+ url=self._build_url('runs'),
303
+ method='POST',
304
+ headers={'content-type': content_type},
305
+ data=run_input,
306
+ params=request_params,
307
+ timeout=timeout,
308
+ ),
309
+ wait_for_resources=wait_for_resources,
292
310
  )
293
311
 
294
312
  result = response_to_dict(response)
@@ -308,6 +326,7 @@ class ActorClient(ResourceClient):
308
326
  webhooks: WebhooksList | None = None,
309
327
  force_permission_level: ActorPermissionLevel | None = None,
310
328
  wait_duration: timedelta | None = None,
329
+ wait_for_resources: bool | timedelta = False,
311
330
  logger: Logger | Literal['default'] | None = 'default',
312
331
  timeout: Timeout = 'no_timeout',
313
332
  ) -> Run | None:
@@ -341,6 +360,14 @@ class ActorClient(ResourceClient):
341
360
  a webhook set up for the Actor, you do not have to add it again here.
342
361
  wait_duration: The maximum time the server waits for the run to finish. If not provided,
343
362
  waits indefinitely.
363
+ wait_for_resources: Retry the start while the account lacks the memory or a concurrent-run slot for the run,
364
+ that is while the API rejects it with an `ApifyApiError` of type `actor-memory-limit-exceeded` or
365
+ `concurrent-runs-limit-exceeded`. Both clear as other runs or builds finish. The start is retried
366
+ every 10 seconds, and any other error is raised right away. `True` retries until the run starts, a
367
+ `timedelta` stops retrying after that long and raises the last error. A run that requests more memory
368
+ than the whole memory limit of the account is rejected with `actor-memory-limit-exceeded` as well and
369
+ never starts, so `True` retries it forever. The time spent retrying doesn't count toward
370
+ `wait_duration`.
344
371
  logger: Logger used to redirect logs from the Actor run. Using "default" literal means that a predefined
345
372
  default logger will be used. Setting `None` will disable any log propagation. Passing custom logger
346
373
  will redirect logs to the provided logger. The logger is also used to capture status and status message
@@ -361,6 +388,7 @@ class ActorClient(ResourceClient):
361
388
  run_timeout=run_timeout,
362
389
  webhooks=webhooks,
363
390
  force_permission_level=force_permission_level,
391
+ wait_for_resources=wait_for_resources,
364
392
  timeout=timeout,
365
393
  )
366
394
  run_client = self._client_registry.run_client(
@@ -740,6 +768,7 @@ class ActorClientAsync(ResourceClientAsync):
740
768
  force_permission_level: ActorPermissionLevel | None = None,
741
769
  wait_for_finish: int | None = None,
742
770
  webhooks: WebhooksList | None = None,
771
+ wait_for_resources: bool | timedelta = False,
743
772
  timeout: Timeout = 'medium',
744
773
  ) -> Run:
745
774
  """Start the Actor and immediately return the Run object.
@@ -774,12 +803,21 @@ class ActorClientAsync(ResourceClientAsync):
774
803
  * `event_types`: List of `WebhookEventType` values which trigger the webhook.
775
804
  * `request_url`: URL to which to send the webhook HTTP request.
776
805
  * `payload_template`: Optional template for the request payload.
806
+ wait_for_resources: Retry the start while the account lacks the memory or a concurrent-run slot for the run,
807
+ that is while the API rejects it with an `ApifyApiError` of type `actor-memory-limit-exceeded` or
808
+ `concurrent-runs-limit-exceeded`. Both clear as other runs or builds finish. The start is retried
809
+ every 10 seconds, and any other error is raised right away. `True` retries until the run starts, a
810
+ `timedelta` stops retrying after that long and raises the last error. A run that requests more memory
811
+ than the whole memory limit of the account is rejected with `actor-memory-limit-exceeded` as well and
812
+ never starts, so `True` retries it forever. A streamed `run_input` that cannot be rewound, such as a
813
+ generator, is sent only once, so its start is not retried.
777
814
  timeout: Timeout for the API HTTP request.
778
815
 
779
816
  Returns:
780
817
  The run object.
781
818
  """
782
819
  run_input, content_type = encode_key_value_store_record_value(run_input, content_type=content_type)
820
+ run_input, wait_for_resources = prepare_resendable_body(run_input, wait_for_resources=wait_for_resources)
783
821
 
784
822
  request_params = self._build_params(
785
823
  build=build,
@@ -793,13 +831,16 @@ class ActorClientAsync(ResourceClientAsync):
793
831
  webhooks=encode_webhooks_to_base64(webhooks),
794
832
  )
795
833
 
796
- response = await self._http_client.call(
797
- url=self._build_url('runs'),
798
- method='POST',
799
- headers={'content-type': content_type},
800
- data=run_input,
801
- params=request_params,
802
- timeout=timeout,
834
+ response = await start_waiting_for_resources_async(
835
+ lambda: self._http_client.call(
836
+ url=self._build_url('runs'),
837
+ method='POST',
838
+ headers={'content-type': content_type},
839
+ data=run_input,
840
+ params=request_params,
841
+ timeout=timeout,
842
+ ),
843
+ wait_for_resources=wait_for_resources,
803
844
  )
804
845
 
805
846
  result = response_to_dict(response)
@@ -819,6 +860,7 @@ class ActorClientAsync(ResourceClientAsync):
819
860
  webhooks: WebhooksList | None = None,
820
861
  force_permission_level: ActorPermissionLevel | None = None,
821
862
  wait_duration: timedelta | None = None,
863
+ wait_for_resources: bool | timedelta = False,
822
864
  logger: Logger | Literal['default'] | None = 'default',
823
865
  timeout: Timeout = 'no_timeout',
824
866
  ) -> Run | None:
@@ -852,6 +894,14 @@ class ActorClientAsync(ResourceClientAsync):
852
894
  a webhook set up for the Actor, you do not have to add it again here.
853
895
  wait_duration: The maximum time the server waits for the run to finish. If not provided,
854
896
  waits indefinitely.
897
+ wait_for_resources: Retry the start while the account lacks the memory or a concurrent-run slot for the run,
898
+ that is while the API rejects it with an `ApifyApiError` of type `actor-memory-limit-exceeded` or
899
+ `concurrent-runs-limit-exceeded`. Both clear as other runs or builds finish. The start is retried
900
+ every 10 seconds, and any other error is raised right away. `True` retries until the run starts, a
901
+ `timedelta` stops retrying after that long and raises the last error. A run that requests more memory
902
+ than the whole memory limit of the account is rejected with `actor-memory-limit-exceeded` as well and
903
+ never starts, so `True` retries it forever. The time spent retrying doesn't count toward
904
+ `wait_duration`.
855
905
  logger: Logger used to redirect logs from the Actor run. Using "default" literal means that a predefined
856
906
  default logger will be used. Setting `None` will disable any log propagation. Passing custom logger
857
907
  will redirect logs to the provided logger. The logger is also used to capture status and status message
@@ -872,6 +922,7 @@ class ActorClientAsync(ResourceClientAsync):
872
922
  run_timeout=run_timeout,
873
923
  webhooks=webhooks,
874
924
  force_permission_level=force_permission_level,
925
+ wait_for_resources=wait_for_resources,
875
926
  timeout=timeout,
876
927
  )
877
928
 
@@ -18,6 +18,7 @@ from apify_client._resource_clients._resource_client import ResourceClient, Reso
18
18
  from apify_client._utils.encoding import encode_webhooks_to_base64
19
19
  from apify_client._utils.http import response_to_dict
20
20
  from apify_client._utils.time import to_seconds
21
+ from apify_client._utils.wait_for_resources import start_waiting_for_resources, start_waiting_for_resources_async
21
22
 
22
23
  if TYPE_CHECKING:
23
24
  from datetime import timedelta
@@ -221,6 +222,7 @@ class TaskClient(ResourceClient):
221
222
  restart_on_error: bool | None = None,
222
223
  wait_for_finish: int | None = None,
223
224
  webhooks: WebhooksList | None = None,
225
+ wait_for_resources: bool | timedelta = False,
224
226
  timeout: Timeout = 'medium',
225
227
  ) -> Run:
226
228
  """Start the task and immediately return the Run object.
@@ -248,6 +250,13 @@ class TaskClient(ResourceClient):
248
250
  * `event_types`: List of `WebhookEventType` values which trigger the webhook.
249
251
  * `request_url`: URL to which to send the webhook HTTP request.
250
252
  * `payload_template`: Optional template for the request payload.
253
+ wait_for_resources: Retry the start while the account lacks the memory or a concurrent-run slot for the run,
254
+ that is while the API rejects it with an `ApifyApiError` of type `actor-memory-limit-exceeded` or
255
+ `concurrent-runs-limit-exceeded`. Both clear as other runs or builds finish. The start is retried
256
+ every 10 seconds, and any other error is raised right away. `True` retries until the run starts, a
257
+ `timedelta` stops retrying after that long and raises the last error. A run that requests more memory
258
+ than the whole memory limit of the account is rejected with `actor-memory-limit-exceeded` as well and
259
+ never starts, so `True` retries it forever.
251
260
  timeout: Timeout for the API HTTP request.
252
261
 
253
262
  Returns:
@@ -266,13 +275,16 @@ class TaskClient(ResourceClient):
266
275
  webhooks=encode_webhooks_to_base64(webhooks),
267
276
  )
268
277
 
269
- response = self._http_client.call(
270
- url=self._build_url('runs'),
271
- method='POST',
272
- headers={'content-type': 'application/json; charset=utf-8'},
273
- json=task_input.model_dump() if task_input is not None else None,
274
- params=request_params,
275
- timeout=timeout,
278
+ response = start_waiting_for_resources(
279
+ lambda: self._http_client.call(
280
+ url=self._build_url('runs'),
281
+ method='POST',
282
+ headers={'content-type': 'application/json; charset=utf-8'},
283
+ json=task_input.model_dump() if task_input is not None else None,
284
+ params=request_params,
285
+ timeout=timeout,
286
+ ),
287
+ wait_for_resources=wait_for_resources,
276
288
  )
277
289
 
278
290
  result = response_to_dict(response)
@@ -289,6 +301,7 @@ class TaskClient(ResourceClient):
289
301
  restart_on_error: bool | None = None,
290
302
  webhooks: WebhooksList | None = None,
291
303
  wait_duration: timedelta | None = None,
304
+ wait_for_resources: bool | timedelta = False,
292
305
  timeout: Timeout = 'no_timeout',
293
306
  ) -> Run | None:
294
307
  """Start a task and wait for it to finish before returning the Run object.
@@ -314,6 +327,14 @@ class TaskClient(ResourceClient):
314
327
  the Actor or task, you do not have to add it again here.
315
328
  wait_duration: The maximum time the server waits for the task run to finish. If not provided,
316
329
  waits indefinitely.
330
+ wait_for_resources: Retry the start while the account lacks the memory or a concurrent-run slot for the run,
331
+ that is while the API rejects it with an `ApifyApiError` of type `actor-memory-limit-exceeded` or
332
+ `concurrent-runs-limit-exceeded`. Both clear as other runs or builds finish. The start is retried
333
+ every 10 seconds, and any other error is raised right away. `True` retries until the run starts, a
334
+ `timedelta` stops retrying after that long and raises the last error. A run that requests more memory
335
+ than the whole memory limit of the account is rejected with `actor-memory-limit-exceeded` as well and
336
+ never starts, so `True` retries it forever. The time spent retrying doesn't count toward
337
+ `wait_duration`.
317
338
  timeout: Timeout for the API HTTP request.
318
339
 
319
340
  Returns:
@@ -327,6 +348,7 @@ class TaskClient(ResourceClient):
327
348
  run_timeout=run_timeout,
328
349
  restart_on_error=restart_on_error,
329
350
  webhooks=webhooks,
351
+ wait_for_resources=wait_for_resources,
330
352
  timeout=timeout,
331
353
  )
332
354
 
@@ -602,6 +624,7 @@ class TaskClientAsync(ResourceClientAsync):
602
624
  restart_on_error: bool | None = None,
603
625
  wait_for_finish: int | None = None,
604
626
  webhooks: WebhooksList | None = None,
627
+ wait_for_resources: bool | timedelta = False,
605
628
  timeout: Timeout = 'medium',
606
629
  ) -> Run:
607
630
  """Start the task and immediately return the Run object.
@@ -629,6 +652,13 @@ class TaskClientAsync(ResourceClientAsync):
629
652
  * `event_types`: List of `WebhookEventType` values which trigger the webhook.
630
653
  * `request_url`: URL to which to send the webhook HTTP request.
631
654
  * `payload_template`: Optional template for the request payload.
655
+ wait_for_resources: Retry the start while the account lacks the memory or a concurrent-run slot for the run,
656
+ that is while the API rejects it with an `ApifyApiError` of type `actor-memory-limit-exceeded` or
657
+ `concurrent-runs-limit-exceeded`. Both clear as other runs or builds finish. The start is retried
658
+ every 10 seconds, and any other error is raised right away. `True` retries until the run starts, a
659
+ `timedelta` stops retrying after that long and raises the last error. A run that requests more memory
660
+ than the whole memory limit of the account is rejected with `actor-memory-limit-exceeded` as well and
661
+ never starts, so `True` retries it forever.
632
662
  timeout: Timeout for the API HTTP request.
633
663
 
634
664
  Returns:
@@ -647,13 +677,16 @@ class TaskClientAsync(ResourceClientAsync):
647
677
  webhooks=encode_webhooks_to_base64(webhooks),
648
678
  )
649
679
 
650
- response = await self._http_client.call(
651
- url=self._build_url('runs'),
652
- method='POST',
653
- headers={'content-type': 'application/json; charset=utf-8'},
654
- json=task_input.model_dump() if task_input is not None else None,
655
- params=request_params,
656
- timeout=timeout,
680
+ response = await start_waiting_for_resources_async(
681
+ lambda: self._http_client.call(
682
+ url=self._build_url('runs'),
683
+ method='POST',
684
+ headers={'content-type': 'application/json; charset=utf-8'},
685
+ json=task_input.model_dump() if task_input is not None else None,
686
+ params=request_params,
687
+ timeout=timeout,
688
+ ),
689
+ wait_for_resources=wait_for_resources,
657
690
  )
658
691
 
659
692
  result = response_to_dict(response)
@@ -670,6 +703,7 @@ class TaskClientAsync(ResourceClientAsync):
670
703
  restart_on_error: bool | None = None,
671
704
  webhooks: WebhooksList | None = None,
672
705
  wait_duration: timedelta | None = None,
706
+ wait_for_resources: bool | timedelta = False,
673
707
  timeout: Timeout = 'no_timeout',
674
708
  ) -> Run | None:
675
709
  """Start a task and wait for it to finish before returning the Run object.
@@ -695,6 +729,14 @@ class TaskClientAsync(ResourceClientAsync):
695
729
  the Actor or task, you do not have to add it again here.
696
730
  wait_duration: The maximum time the server waits for the task run to finish. If not provided,
697
731
  waits indefinitely.
732
+ wait_for_resources: Retry the start while the account lacks the memory or a concurrent-run slot for the run,
733
+ that is while the API rejects it with an `ApifyApiError` of type `actor-memory-limit-exceeded` or
734
+ `concurrent-runs-limit-exceeded`. Both clear as other runs or builds finish. The start is retried
735
+ every 10 seconds, and any other error is raised right away. `True` retries until the run starts, a
736
+ `timedelta` stops retrying after that long and raises the last error. A run that requests more memory
737
+ than the whole memory limit of the account is rejected with `actor-memory-limit-exceeded` as well and
738
+ never starts, so `True` retries it forever. The time spent retrying doesn't count toward
739
+ `wait_duration`.
698
740
  timeout: Timeout for the API HTTP request.
699
741
 
700
742
  Returns:
@@ -708,6 +750,7 @@ class TaskClientAsync(ResourceClientAsync):
708
750
  run_timeout=run_timeout,
709
751
  restart_on_error=restart_on_error,
710
752
  webhooks=webhooks,
753
+ wait_for_resources=wait_for_resources,
711
754
  timeout=timeout,
712
755
  )
713
756
  run_client = self._client_registry.run_client(
@@ -0,0 +1,82 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import time
5
+ from datetime import timedelta
6
+ from typing import TYPE_CHECKING, Any, TypeVar
7
+
8
+ from apify_client._logging import logger
9
+ from apify_client.errors import ApifyApiError
10
+ from apify_client.http_clients._streamed_body import StreamedRequestBody
11
+
12
+ if TYPE_CHECKING:
13
+ from collections.abc import Awaitable, Callable
14
+
15
+ T = TypeVar('T')
16
+
17
+ RESOURCE_LIMIT_ERROR_TYPES = frozenset({'actor-memory-limit-exceeded', 'concurrent-runs-limit-exceeded'})
18
+ """Error types the API rejects a run start with while the account has no free memory or concurrent-run slot for it.
19
+
20
+ Both clear as other runs or builds finish.
21
+ """
22
+
23
+ WAIT_FOR_RESOURCES_COOLDOWN = timedelta(seconds=10)
24
+ """Cooldown between two attempts to start a run that was rejected for lack of resources."""
25
+
26
+
27
+ def prepare_resendable_body(data: Any, *, wait_for_resources: bool | timedelta) -> tuple[Any, bool | timedelta]:
28
+ """Wrap a streamed request body once, so every start attempt sends the same body.
29
+
30
+ A seekable `io.IOBase` source is rewound before each attempt. Any other streamed source is used up by the first
31
+ attempt, so the returned `wait_for_resources` is `False` and a start rejected for lack of resources is not retried.
32
+ """
33
+ if wait_for_resources is False or not StreamedRequestBody.is_streamable(data):
34
+ return data, wait_for_resources
35
+ body = data if isinstance(data, StreamedRequestBody) else StreamedRequestBody(data)
36
+ return body, wait_for_resources if body.rewindable else False
37
+
38
+
39
+ def _next_delay(exc: ApifyApiError, deadline: float | None) -> float:
40
+ """Return the seconds to sleep before the next attempt, or re-raise `exc` if no attempt should follow."""
41
+ if exc.type not in RESOURCE_LIMIT_ERROR_TYPES:
42
+ raise exc
43
+ delay = WAIT_FOR_RESOURCES_COOLDOWN.total_seconds()
44
+ if deadline is not None:
45
+ remaining = deadline - time.monotonic()
46
+ if remaining <= 0:
47
+ raise exc
48
+ delay = min(delay, remaining)
49
+ logger.info('Not enough resources to start the run, retrying in %.3gs: %s', delay, exc.message)
50
+ return delay
51
+
52
+
53
+ def start_waiting_for_resources(start: Callable[[], T], *, wait_for_resources: bool | timedelta) -> T:
54
+ """Make the `start` request, retrying it every `WAIT_FOR_RESOURCES_COOLDOWN` while it fails for lack of resources.
55
+
56
+ `True` retries until the request succeeds, a `timedelta` bounds the retrying, after which the last error is raised.
57
+ Any other error is raised right away.
58
+ """
59
+ if wait_for_resources is False:
60
+ return start()
61
+ deadline = None if wait_for_resources is True else time.monotonic() + wait_for_resources.total_seconds()
62
+ while True:
63
+ try:
64
+ return start()
65
+ except ApifyApiError as exc:
66
+ time.sleep(_next_delay(exc, deadline))
67
+
68
+
69
+ async def start_waiting_for_resources_async(
70
+ start: Callable[[], Awaitable[T]],
71
+ *,
72
+ wait_for_resources: bool | timedelta,
73
+ ) -> T:
74
+ """Async variant of `start_waiting_for_resources`."""
75
+ if wait_for_resources is False:
76
+ return await start()
77
+ deadline = None if wait_for_resources is True else time.monotonic() + wait_for_resources.total_seconds()
78
+ while True:
79
+ try:
80
+ return await start()
81
+ except ApifyApiError as exc:
82
+ await asyncio.sleep(_next_delay(exc, deadline))
File without changes
File without changes