11from __future__ import annotations
22
3- import time
43import warnings
4+ from collections .abc import Callable
55from typing import TYPE_CHECKING , Any , Literal , NamedTuple
66
7+ from .._utils import batched , waiter
78from ..core import BoundModelBase , Meta , ResourceClientBase
8- from .domain import Action , ActionFailedException , ActionStatus , ActionTimeoutException
9+ from .domain import (
10+ Action ,
11+ ActionFailedException ,
12+ ActionStatus ,
13+ ActionTimeoutException ,
14+ )
915
1016if TYPE_CHECKING :
1117 from .._client import Client
@@ -25,33 +31,44 @@ class BoundAction(BoundModelBase[Action], Action):
2531
2632 model = Action
2733
28- def wait_until_finished (self , max_retries : int | None = None ) -> None :
29- """Wait until the specific action has status=finished.
30-
31- :param max_retries: int Specify how many retries will be performed before an ActionTimeoutException will be raised.
32- :raises: ActionFailedException when action is finished with status==error
33- :raises: ActionTimeoutException when Action is still in status==running after max_retries is reached.
34+ def wait_until_finished (
35+ self ,
36+ max_retries : int | None = None ,
37+ * ,
38+ timeout : float | None = None ,
39+ ) -> None :
3440 """
35- if max_retries is None :
36- # pylint: disable=protected-access
37- max_retries = self . _client . _client . _poll_max_retries
41+ Waits until the Action is finished by polling the API at the interval defined by
42+ the client's poll interval and function. An Action is considered as finished
43+ when its status is either "success" or "error".
3844
39- retries = 0
40- while True :
41- self .reload ()
42- if self .status != Action .STATUS_RUNNING :
43- break
45+ If the Action fails (its status is "error"), the function will stop waiting
46+ and raise ActionFailedException.
4447
45- retries += 1
46- if retries < max_retries :
47- # pylint: disable=protected-access
48- time .sleep (self ._client ._client ._poll_interval_func (retries ))
49- continue
48+ :param timeout:
49+ Duration in seconds before an ActionTimeoutException will be raised when polling actions from the API.
50+ :param max_retries:
51+ Max retries before an ActionTimeoutException will be raised when polling actions from the API.
52+
53+ :raises: ActionTimeoutException when an Action is still running after max_retries or timeout is reached.
54+ :raises: ActionFailedException when an Action failed.
55+ """
56+
57+ def handle_update (update : BoundAction ) -> None :
58+ self .data_model = update .data_model
5059
51- raise ActionTimeoutException (action = self )
60+ if update .status == Action .STATUS_ERROR :
61+ raise ActionFailedException (action = update )
5262
53- if self .status == Action .STATUS_ERROR :
54- raise ActionFailedException (action = self )
63+ try :
64+ self ._client .wait_for_function (
65+ handle_update ,
66+ [self ],
67+ timeout = timeout ,
68+ max_retries = max_retries ,
69+ )
70+ except* ActionTimeoutException as group :
71+ raise group .exceptions [0 ]
5572
5673
5774ActionSort = Literal [
@@ -189,6 +206,148 @@ class ActionsClient(ResourceActionsClient):
189206 def __init__ (self , client : Client ):
190207 super ().__init__ (client , None )
191208
209+ def _get_list_by_ids (self , ids : list [int ]) -> list [BoundAction ]:
210+ """
211+ Get a list of Actions by their IDs.
212+
213+ :param ids: List of Action IDs to get.
214+ :raises ValueError: Raise when Action IDs were not found.
215+ :return: List of Actions.
216+ """
217+ actions : list [BoundAction ] = []
218+
219+ for ids_batch in batched (ids , 25 ):
220+ params : dict [str , Any ] = {
221+ "id" : ids_batch ,
222+ "sort" : ["status" , "id" ],
223+ }
224+
225+ response = self ._client .request (
226+ method = "GET" ,
227+ url = "/actions" ,
228+ params = params ,
229+ )
230+
231+ actions .extend (
232+ BoundAction (self ._parent .actions , o ) for o in response ["actions" ]
233+ )
234+
235+ if len (ids ) != len (actions ):
236+ found_ids = [a .id for a in actions ]
237+ not_found_ids = list (set (ids ) - set (found_ids ))
238+
239+ raise ValueError (
240+ f"actions not found: { ', ' .join (str (o ) for o in not_found_ids )} "
241+ )
242+
243+ return actions
244+
245+ def wait_for_function (
246+ self ,
247+ handle_update : Callable [[BoundAction ], None ],
248+ actions : list [Action | BoundAction ],
249+ * ,
250+ timeout : float | None = None ,
251+ max_retries : int | None = None ,
252+ ) -> list [BoundAction ]:
253+ """
254+ Waits until all Actions are finished by polling the API at the interval defined
255+ by the client's poll interval and function. An Action is considered as finished
256+ when its status is either "success" or "error".
257+
258+ The handle_update callback is called every time an Action is updated.
259+
260+ :param handle_update:
261+ Function called every time an Action is updated.
262+ :param actions:
263+ List of Actions to wait for.
264+ :param timeout:
265+ Duration in seconds before an ActionTimeoutException will be raised when polling actions from the API.
266+ :param max_retries:
267+ Max retries before an ActionTimeoutException will be raised when polling actions from the API.
268+
269+ :raises: ActionTimeoutException when an Action is still running after max_retries or timeout is reached.
270+
271+ :return: List of finished Actions.
272+ """
273+ if timeout is None :
274+ # pylint: disable=protected-access
275+ timeout = self ._client ._poll_timeout
276+ if max_retries is None :
277+ # pylint: disable=protected-access
278+ max_retries = self ._client ._poll_max_retries
279+
280+ running : list [BoundAction ] = actions .copy () # type: ignore[assignment]
281+ completed : list [BoundAction ] = []
282+
283+ retries = 0
284+ wait = waiter (timeout )
285+ while len (running ) > 0 :
286+ if max_retries is not None and retries > max_retries :
287+ raise ExceptionGroup (
288+ "The actions timed out after" ,
289+ [ActionTimeoutException (action ) for action in running ],
290+ )
291+
292+ # pylint: disable=protected-access
293+ if wait (self ._client ._poll_interval_func (retries )):
294+ raise ExceptionGroup (
295+ "The actions timed out" ,
296+ [ActionTimeoutException (action ) for action in running ],
297+ )
298+
299+ retries += 1
300+
301+ running = self ._get_list_by_ids ([a .id for a in running ])
302+
303+ for update in running :
304+ if update .status != Action .STATUS_RUNNING :
305+ running .remove (update )
306+ completed .append (update )
307+
308+ handle_update (update )
309+
310+ return completed
311+
312+ def wait_for (
313+ self ,
314+ actions : list [Action | BoundAction ],
315+ * ,
316+ timeout : float | None = None ,
317+ max_retries : int | None = None ,
318+ ) -> list [BoundAction ]:
319+ """
320+ Waits until all Actions are finished by polling the API at the interval defined
321+ by the client's poll interval and function. An Action is considered as finished
322+ when its status is either "success" or "error".
323+
324+ If a single Action fails (its status is "error"), the function will stop waiting
325+ and raise ActionFailedException.
326+
327+ :param actions:
328+ List of Actions to wait for.
329+ :param timeout:
330+ Duration in seconds before an ActionTimeoutException will be raised when polling actions from the API.
331+ :param max_retries:
332+ Max retries before an ActionTimeoutException will be raised when polling actions from the API.
333+
334+ :raises: ActionTimeoutException when an Action is still running after max_retries or timeout is reached.
335+ :raises: ActionFailedException when an Action failed.
336+
337+ :return: List of succeeded Actions.
338+ """
339+
340+ def handle_update (update : BoundAction ) -> None :
341+ if update .status == Action .STATUS_ERROR :
342+ raise ActionFailedException (action = update )
343+
344+ return self .wait_for_function (
345+ handle_update ,
346+ actions ,
347+ timeout = timeout ,
348+ max_retries = max_retries ,
349+ )
350+
192351 def get_list (
193352 self ,
194353 status : list [ActionStatus ] | None = None ,
0 commit comments