11from __future__ import annotations
22
3- import time
43import warnings
5- from typing import TYPE_CHECKING , Any , NamedTuple
4+ from typing import TYPE_CHECKING , Any , Callable , NamedTuple
65
6+ from .._utils import batched , waiter
77from ..core import BoundModelBase , ClientEntityBase , Meta
8- from .domain import Action , ActionFailedException , ActionTimeoutException
8+ from .domain import (
9+ Action ,
10+ ActionFailedException ,
11+ ActionGroupException ,
12+ ActionTimeoutException ,
13+ )
914
1015if TYPE_CHECKING :
1116 from .._client import Client
@@ -16,18 +21,24 @@ class BoundAction(BoundModelBase, Action):
1621
1722 model = Action
1823
19- def wait_until_finished (self , max_retries : int | None = None ) -> None :
24+ def wait_until_finished (
25+ self ,
26+ max_retries : int | None = None ,
27+ * ,
28+ timeout : float | None = None ,
29+ ) -> None :
2030 """Wait until the specific action has status=finished.
2131
2232 :param max_retries: int Specify how many retries will be performed before an ActionTimeoutException will be raised.
2333 :raises: ActionFailedException when action is finished with status==error
24- :raises: ActionTimeoutException when Action is still in status==running after max_retries is reached.
34+ :raises: ActionTimeoutException when Action is still in status==running after max_retries or timeout is reached.
2535 """
2636 if max_retries is None :
2737 # pylint: disable=protected-access
2838 max_retries = self ._client ._client ._poll_max_retries
2939
3040 retries = 0
41+ wait = waiter (timeout )
3142 while True :
3243 self .reload ()
3344 if self .status != Action .STATUS_RUNNING :
@@ -36,8 +47,8 @@ def wait_until_finished(self, max_retries: int | None = None) -> None:
3647 retries += 1
3748 if retries < max_retries :
3849 # pylint: disable=protected-access
39- time . sleep (self ._client ._client ._poll_interval_func (retries ))
40- continue
50+ if not wait (self ._client ._client ._poll_interval_func (retries )):
51+ continue
4152
4253 raise ActionTimeoutException (action = self )
4354
@@ -129,6 +140,115 @@ class ActionsClient(ResourceActionsClient):
129140 def __init__ (self , client : Client ):
130141 super ().__init__ (client , None )
131142
143+ # TODO: Consider making public?
144+ def _get_list_by_ids (self , ids : list [int ]) -> list [BoundAction ]:
145+ """
146+ Get a list of Actions by their IDs.
147+
148+ :param ids: List of Action IDs to get.
149+ :raises ValueError: Raise when Action IDs were not found.
150+ :return: List of Actions.
151+ """
152+ actions : list [BoundAction ] = []
153+
154+ for ids_batch in batched (ids , 25 ):
155+ params : dict [str , Any ] = {
156+ "id" : ids_batch ,
157+ }
158+
159+ response = self ._client .request (
160+ method = "GET" ,
161+ url = "/actions" ,
162+ params = params ,
163+ )
164+
165+ actions .extend (
166+ BoundAction (self ._client .actions , action_data )
167+ for action_data in response ["actions" ]
168+ )
169+
170+ # TODO: Should this be moved to the the wait function?
171+ if len (ids ) != len (actions ):
172+ found_ids = [a .id for a in actions ]
173+ not_found_ids = list (set (ids ) - set (found_ids ))
174+
175+ raise ValueError (
176+ f"actions not found: { ', ' .join (str (o ) for o in not_found_ids )} "
177+ )
178+
179+ return actions
180+
181+ def wait_for_function (
182+ self ,
183+ handle_update : Callable [[BoundAction ], None ],
184+ actions : list [Action | BoundAction ],
185+ * ,
186+ timeout : float | None = None ,
187+ ) -> list [BoundAction ]:
188+ """
189+ Waits until all Actions succeed by polling the API at the interval defined by
190+ the client's poll interval and function. An Action is considered as complete
191+ when its status is either "success" or "error".
192+
193+ The handle_update callback is called every time an Action is updated.
194+
195+ :param handle_update: Function called every time an Action is updated.
196+ :param actions: List of Actions to wait for.
197+ :param timeout: Timeout in seconds.
198+ :raises: ActionFailedException when an Action failed.
199+ :return: List of succeeded Actions.
200+ """
201+ running : list [BoundAction ] = list (actions )
202+ completed : list [BoundAction ] = []
203+
204+ retries = 0
205+ wait = waiter (timeout )
206+ while len (running ) > 0 :
207+ # pylint: disable=protected-access
208+ if wait (self ._client ._poll_interval_func (retries )):
209+ raise ActionGroupException (
210+ [ActionTimeoutException (action = action ) for action in running ]
211+ )
212+
213+ retries += 1
214+
215+ running = self ._get_list_by_ids ([a .id for a in running ])
216+
217+ for update in running :
218+ if update .status != Action .STATUS_RUNNING :
219+ running .remove (update )
220+ completed .append (update )
221+
222+ handle_update (update )
223+
224+ return completed
225+
226+ def wait_for (
227+ self ,
228+ actions : list [Action | BoundAction ],
229+ * ,
230+ timeout : float | None = None ,
231+ ) -> list [BoundAction ]:
232+ """
233+ Waits until all Actions succeed by polling the API at the interval defined by
234+ the client's poll interval and function. An Action is considered as complete
235+ when its status is either "success" or "error".
236+
237+ If a single Action fails, the function will stop waiting and raise ActionFailedException.
238+
239+ :param actions: List of Actions to wait for.
240+ :param timeout: Timeout in seconds.
241+ :raises: ActionFailedException when an Action failed.
242+ :raises: TimeoutError when the Actions did not succeed before timeout.
243+ :return: List of succeeded Actions.
244+ """
245+
246+ def handle_update (update : BoundAction ) -> None :
247+ if update .status == Action .STATUS_ERROR :
248+ raise ActionFailedException (action = update )
249+
250+ return self .wait_for_function (handle_update , actions , timeout = timeout )
251+
132252 def get_list (
133253 self ,
134254 status : list [str ] | None = None ,
0 commit comments