44import os
55import sys
66from contextlib import suppress
7- from datetime import timedelta
7+ from datetime import datetime , timedelta , timezone
88from typing import TYPE_CHECKING , Any , Callable , Literal , TypeVar , cast , overload
99
1010from lazy_object_proxy import Proxy
@@ -693,7 +693,7 @@ async def start(
693693 content_type : str | None = None ,
694694 build : str | None = None ,
695695 memory_mbytes : int | None = None ,
696- timeout : timedelta | None = None ,
696+ timeout : timedelta | None | Literal [ 'RemainingTime' ] = None ,
697697 wait_for_finish : int | None = None ,
698698 webhooks : list [Webhook ] | None = None ,
699699 ) -> ActorRun :
@@ -711,7 +711,8 @@ async def start(
711711 memory_mbytes: Memory limit for the run, in megabytes. By default, the run uses a memory limit specified
712712 in the default run configuration for the Actor.
713713 timeout: Optional timeout for the run, in seconds. By default, the run uses timeout specified in
714- the default run configuration for the Actor.
714+ the default run configuration for the Actor. Using `RemainingTime` will set timeout of the other Actor
715+ to the time remaining from this Actor timeout.
715716 wait_for_finish: The maximum number of seconds the server waits for the run to finish. By default,
716717 it is 0, the maximum value is 300.
717718 webhooks: Optional ad-hoc webhooks (https://docs.apify.com/webhooks/ad-hoc-webhooks) associated with
@@ -732,18 +733,39 @@ async def start(
732733 else :
733734 serialized_webhooks = None
734735
736+ if timeout == 'RemainingTime' :
737+ actor_start_timeout = self ._get_remaining_time ()
738+ elif timeout is None :
739+ actor_start_timeout = None
740+ elif isinstance (timeout , timedelta ):
741+ actor_start_timeout = timeout
742+ else :
743+ raise ValueError (f'Invalid timeout { timeout !r} : expected `None`, `"RemainingTime"`, or a `timedelta`.' )
744+
735745 api_result = await client .actor (actor_id ).start (
736746 run_input = run_input ,
737747 content_type = content_type ,
738748 build = build ,
739749 memory_mbytes = memory_mbytes ,
740- timeout_secs = int (timeout .total_seconds ()) if timeout is not None else None ,
750+ timeout_secs = int (actor_start_timeout .total_seconds ()) if actor_start_timeout is not None else None ,
741751 wait_for_finish = wait_for_finish ,
742752 webhooks = serialized_webhooks ,
743753 )
744754
745755 return ActorRun .model_validate (api_result )
746756
757+ def _get_remaining_time (self ) -> timedelta | None :
758+ """Get time remaining from the actor timeout. Returns `None` if not on an Apify platform."""
759+ if self .is_at_home () and self .configuration .timeout_at :
760+ return self .configuration .timeout_at - datetime .now (tz = timezone .utc )
761+
762+ self .log .warning (
763+ 'Returning `None` instead of remaining time. Using `RemainingTime` argument is only possible when the Actor'
764+ ' is running on the Apify platform and when the timeout for the Actor run is set. '
765+ f'{ self .is_at_home ()= } , { self .configuration .timeout_at = } '
766+ )
767+ return None
768+
747769 async def abort (
748770 self ,
749771 run_id : str ,
@@ -787,7 +809,7 @@ async def call(
787809 content_type : str | None = None ,
788810 build : str | None = None ,
789811 memory_mbytes : int | None = None ,
790- timeout : timedelta | None = None ,
812+ timeout : timedelta | None | Literal [ 'RemainingTime' ] = None ,
791813 webhooks : list [Webhook ] | None = None ,
792814 wait : timedelta | None = None ,
793815 ) -> ActorRun | None :
@@ -805,7 +827,8 @@ async def call(
805827 memory_mbytes: Memory limit for the run, in megabytes. By default, the run uses a memory limit specified
806828 in the default run configuration for the Actor.
807829 timeout: Optional timeout for the run, in seconds. By default, the run uses timeout specified in
808- the default run configuration for the Actor.
830+ the default run configuration for the Actor. Using `RemainingTime` will set timeout of the other Actor
831+ to the time remaining from this Actor timeout.
809832 webhooks: Optional webhooks (https://docs.apify.com/webhooks) associated with the Actor run, which can
810833 be used to receive a notification, e.g. when the Actor finished or failed. If you already have
811834 a webhook set up for the Actor, you do not have to add it again here.
@@ -826,12 +849,21 @@ async def call(
826849 else :
827850 serialized_webhooks = None
828851
852+ if timeout == 'RemainingTime' :
853+ actor_call_timeout = self ._get_remaining_time ()
854+ elif timeout is None :
855+ actor_call_timeout = None
856+ elif isinstance (timeout , timedelta ):
857+ actor_call_timeout = timeout
858+ else :
859+ raise ValueError (f'Invalid timeout { timeout !r} : expected `None`, `"RemainingTime"`, or a `timedelta`.' )
860+
829861 api_result = await client .actor (actor_id ).call (
830862 run_input = run_input ,
831863 content_type = content_type ,
832864 build = build ,
833865 memory_mbytes = memory_mbytes ,
834- timeout_secs = int (timeout .total_seconds ()) if timeout is not None else None ,
866+ timeout_secs = int (actor_call_timeout .total_seconds ()) if actor_call_timeout is not None else None ,
835867 webhooks = serialized_webhooks ,
836868 wait_secs = int (wait .total_seconds ()) if wait is not None else None ,
837869 )
0 commit comments