ddeutil-workflow 0.0.63__py3-none-any.whl → 0.0.65__py3-none-any.whl
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.
- ddeutil/workflow/__about__.py +1 -1
- ddeutil/workflow/__init__.py +1 -8
- ddeutil/workflow/api/__init__.py +5 -84
- ddeutil/workflow/api/routes/__init__.py +0 -1
- ddeutil/workflow/api/routes/job.py +2 -3
- ddeutil/workflow/api/routes/logs.py +0 -2
- ddeutil/workflow/api/routes/workflows.py +0 -3
- ddeutil/workflow/conf.py +6 -38
- ddeutil/workflow/{exceptions.py → errors.py} +47 -12
- ddeutil/workflow/job.py +249 -118
- ddeutil/workflow/params.py +11 -11
- ddeutil/workflow/result.py +86 -10
- ddeutil/workflow/reusables.py +54 -23
- ddeutil/workflow/stages.py +692 -464
- ddeutil/workflow/utils.py +37 -2
- ddeutil/workflow/workflow.py +163 -664
- {ddeutil_workflow-0.0.63.dist-info → ddeutil_workflow-0.0.65.dist-info}/METADATA +17 -67
- ddeutil_workflow-0.0.65.dist-info/RECORD +28 -0
- {ddeutil_workflow-0.0.63.dist-info → ddeutil_workflow-0.0.65.dist-info}/WHEEL +1 -1
- ddeutil/workflow/api/routes/schedules.py +0 -141
- ddeutil/workflow/api/utils.py +0 -174
- ddeutil/workflow/scheduler.py +0 -813
- ddeutil_workflow-0.0.63.dist-info/RECORD +0 -31
- {ddeutil_workflow-0.0.63.dist-info → ddeutil_workflow-0.0.65.dist-info}/entry_points.txt +0 -0
- {ddeutil_workflow-0.0.63.dist-info → ddeutil_workflow-0.0.65.dist-info}/licenses/LICENSE +0 -0
- {ddeutil_workflow-0.0.63.dist-info → ddeutil_workflow-0.0.65.dist-info}/top_level.txt +0 -0
ddeutil/workflow/stages.py
CHANGED
@@ -3,9 +3,9 @@
|
|
3
3
|
# Licensed under the MIT License. See LICENSE in the project root for
|
4
4
|
# license information.
|
5
5
|
# ------------------------------------------------------------------------------
|
6
|
-
"""Stages module include all stage model that
|
7
|
-
of this workflow engine. The stage handle the minimize task that run
|
8
|
-
thread (same thread at its job owner) that mean it is the lowest executor that
|
6
|
+
"""Stages module include all stage model that implemented to be the minimum execution
|
7
|
+
layer of this workflow core engine. The stage handle the minimize task that run
|
8
|
+
in a thread (same thread at its job owner) that mean it is the lowest executor that
|
9
9
|
you can track logs.
|
10
10
|
|
11
11
|
The output of stage execution only return SUCCESS or CANCEL status because
|
@@ -17,9 +17,11 @@ the stage execution method.
|
|
17
17
|
|
18
18
|
Execution --> Ok ┬--( handler )--> Result with `SUCCESS` or `CANCEL`
|
19
19
|
|
|
20
|
-
|
20
|
+
├--( handler )--> Result with `FAILED` (Set `raise_error` flag)
|
21
|
+
|
|
22
|
+
╰--( handler )---> Result with `SKIP`
|
21
23
|
|
22
|
-
--> Error ---( handler )--> Raise
|
24
|
+
--> Error ---( handler )--> Raise StageError(...)
|
23
25
|
|
24
26
|
On the context I/O that pass to a stage object at execute process. The
|
25
27
|
execute method receives a `params={"params": {...}}` value for passing template
|
@@ -55,21 +57,40 @@ from textwrap import dedent
|
|
55
57
|
from threading import Event
|
56
58
|
from typing import Annotated, Any, Optional, TypeVar, Union, get_type_hints
|
57
59
|
|
58
|
-
from
|
59
|
-
from pydantic
|
60
|
+
from ddeutil.core import str2list
|
61
|
+
from pydantic import BaseModel, Field, ValidationError
|
62
|
+
from pydantic.functional_validators import field_validator, model_validator
|
60
63
|
from typing_extensions import Self
|
61
64
|
|
65
|
+
from . import StageCancelError, StageRetryError
|
62
66
|
from .__types import DictData, DictStr, StrOrInt, StrOrNone, TupleStr
|
63
67
|
from .conf import dynamic, pass_env
|
64
|
-
from .
|
65
|
-
from .result import
|
66
|
-
|
68
|
+
from .errors import StageError, StageSkipError, to_dict
|
69
|
+
from .result import (
|
70
|
+
CANCEL,
|
71
|
+
FAILED,
|
72
|
+
SKIP,
|
73
|
+
SUCCESS,
|
74
|
+
WAIT,
|
75
|
+
Result,
|
76
|
+
Status,
|
77
|
+
get_status_from_error,
|
78
|
+
validate_statuses,
|
79
|
+
)
|
80
|
+
from .reusables import (
|
81
|
+
TagFunc,
|
82
|
+
create_model_from_caller,
|
83
|
+
extract_call,
|
84
|
+
not_in_template,
|
85
|
+
param2template,
|
86
|
+
)
|
67
87
|
from .utils import (
|
68
88
|
delay,
|
69
89
|
dump_all,
|
70
90
|
filter_func,
|
71
91
|
gen_id,
|
72
92
|
make_exec,
|
93
|
+
to_train,
|
73
94
|
)
|
74
95
|
|
75
96
|
T = TypeVar("T")
|
@@ -100,6 +121,12 @@ class BaseStage(BaseModel, ABC):
|
|
100
121
|
name: str = Field(
|
101
122
|
description="A stage name that want to logging when start execution.",
|
102
123
|
)
|
124
|
+
desc: StrOrNone = Field(
|
125
|
+
default=None,
|
126
|
+
description=(
|
127
|
+
"A stage description that use to logging when start execution."
|
128
|
+
),
|
129
|
+
)
|
103
130
|
condition: StrOrNone = Field(
|
104
131
|
default=None,
|
105
132
|
description=(
|
@@ -118,6 +145,14 @@ class BaseStage(BaseModel, ABC):
|
|
118
145
|
"""
|
119
146
|
return self.id or self.name
|
120
147
|
|
148
|
+
@field_validator("desc", mode="after")
|
149
|
+
def ___prepare_desc__(cls, value: str) -> str:
|
150
|
+
"""Prepare description string that was created on a template.
|
151
|
+
|
152
|
+
:rtype: str
|
153
|
+
"""
|
154
|
+
return dedent(value.lstrip("\n"))
|
155
|
+
|
121
156
|
@model_validator(mode="after")
|
122
157
|
def __prepare_running_id(self) -> Self:
|
123
158
|
"""Prepare stage running ID that use default value of field and this
|
@@ -129,14 +164,12 @@ class BaseStage(BaseModel, ABC):
|
|
129
164
|
|
130
165
|
:rtype: Self
|
131
166
|
"""
|
132
|
-
|
133
167
|
# VALIDATE: Validate stage id and name should not dynamic with params
|
134
168
|
# template. (allow only matrix)
|
135
169
|
if not_in_template(self.id) or not_in_template(self.name):
|
136
170
|
raise ValueError(
|
137
171
|
"Stage name and ID should only template with 'matrix.'"
|
138
172
|
)
|
139
|
-
|
140
173
|
return self
|
141
174
|
|
142
175
|
@abstractmethod
|
@@ -169,47 +202,41 @@ class BaseStage(BaseModel, ABC):
|
|
169
202
|
parent_run_id: StrOrNone = None,
|
170
203
|
result: Optional[Result] = None,
|
171
204
|
event: Optional[Event] = None,
|
172
|
-
raise_error: Optional[bool] = None,
|
173
205
|
) -> Union[Result, DictData]:
|
174
206
|
"""Handler stage execution result from the stage `execute` method.
|
175
207
|
|
176
|
-
This
|
177
|
-
|
178
|
-
|
179
|
-
`raise_error` parameter to True.
|
208
|
+
This handler strategy will catch and mapping message to the result
|
209
|
+
context data before returning. All possible status that will return from
|
210
|
+
this method be:
|
180
211
|
|
181
|
-
|
212
|
+
Handler --> Ok --> Result
|
182
213
|
|-status: SUCCESS
|
183
214
|
╰-context:
|
184
215
|
╰-outputs: ...
|
185
216
|
|
186
217
|
--> Ok --> Result
|
187
|
-
|
188
|
-
╰-errors:
|
189
|
-
|-name: ...
|
190
|
-
╰-message: ...
|
218
|
+
╰-status: CANCEL
|
191
219
|
|
192
|
-
--> Ok --> Result
|
220
|
+
--> Ok --> Result
|
221
|
+
╰-status: SKIP
|
222
|
+
|
223
|
+
--> Ok --> Result
|
193
224
|
|-status: FAILED
|
194
225
|
╰-errors:
|
195
226
|
|-name: ...
|
196
227
|
╰-message: ...
|
197
228
|
|
198
|
-
--> Error --> Raise StageException(...)
|
199
|
-
|
200
229
|
On the last step, it will set the running ID on a return result
|
201
230
|
object from the current stage ID before release the final result.
|
202
231
|
|
203
232
|
:param params: (DictData) A parameter data.
|
204
|
-
:param run_id: (str) A running stage ID.
|
205
|
-
:param parent_run_id: (str) A parent running ID.
|
233
|
+
:param run_id: (str) A running stage ID. (Default is None)
|
234
|
+
:param parent_run_id: (str) A parent running ID. (Default is None)
|
206
235
|
:param result: (Result) A result object for keeping context and status
|
207
236
|
data before execution.
|
237
|
+
(Default is None)
|
208
238
|
:param event: (Event) An event manager that pass to the stage execution.
|
209
|
-
|
210
|
-
|
211
|
-
:raise StageException: If the raise_error was set and the execution
|
212
|
-
raise any error.
|
239
|
+
(Default is None)
|
213
240
|
|
214
241
|
:rtype: Result
|
215
242
|
"""
|
@@ -221,21 +248,71 @@ class BaseStage(BaseModel, ABC):
|
|
221
248
|
extras=self.extras,
|
222
249
|
)
|
223
250
|
try:
|
224
|
-
|
251
|
+
result.trace.info(
|
252
|
+
f"[STAGE]: Handler {to_train(self.__class__.__name__)}: "
|
253
|
+
f"{self.name!r}."
|
254
|
+
)
|
255
|
+
if self.desc:
|
256
|
+
result.trace.debug(f"[STAGE]: Description:||{self.desc}||")
|
257
|
+
|
258
|
+
if self.is_skipped(params):
|
259
|
+
raise StageSkipError(
|
260
|
+
f"Skip because condition {self.condition} was valid."
|
261
|
+
)
|
262
|
+
# NOTE: Start call wrapped execution method that will use custom
|
263
|
+
# execution before the real execution from inherit stage model.
|
264
|
+
result_caught: Result = self.__execute(
|
265
|
+
params, result=result, event=event
|
266
|
+
)
|
267
|
+
if result_caught.status == WAIT:
|
268
|
+
raise StageError(
|
269
|
+
"Status from execution should not return waiting status."
|
270
|
+
)
|
271
|
+
return result_caught
|
272
|
+
|
273
|
+
# NOTE: Catch this error in this line because the execution can raise
|
274
|
+
# this exception class at other location.
|
275
|
+
except (
|
276
|
+
StageSkipError,
|
277
|
+
StageCancelError,
|
278
|
+
StageError,
|
279
|
+
) as e: # pragma: no cov
|
280
|
+
result.trace.info(
|
281
|
+
f"[STAGE]: Handler:||{e.__class__.__name__}: {e}||"
|
282
|
+
f"{traceback.format_exc()}"
|
283
|
+
)
|
284
|
+
return result.catch(
|
285
|
+
status=get_status_from_error(e),
|
286
|
+
context=(
|
287
|
+
None
|
288
|
+
if isinstance(e, StageSkipError)
|
289
|
+
else {"errors": e.to_dict()}
|
290
|
+
),
|
291
|
+
)
|
225
292
|
except Exception as e:
|
226
|
-
e_name: str = e.__class__.__name__
|
227
293
|
result.trace.error(
|
228
|
-
f"[STAGE]: Error Handler:||{
|
294
|
+
f"[STAGE]: Error Handler:||{e.__class__.__name__}: {e}||"
|
229
295
|
f"{traceback.format_exc()}"
|
230
296
|
)
|
231
|
-
if dynamic("stage_raise_error", f=raise_error, extras=self.extras):
|
232
|
-
if isinstance(e, StageException):
|
233
|
-
raise
|
234
|
-
raise StageException(
|
235
|
-
f"{self.__class__.__name__}: {e_name}: {e}"
|
236
|
-
) from e
|
237
297
|
return result.catch(status=FAILED, context={"errors": to_dict(e)})
|
238
298
|
|
299
|
+
def __execute(
|
300
|
+
self, params: DictData, result: Result, event: Optional[Event]
|
301
|
+
) -> Result:
|
302
|
+
"""Wrapped the execute method before returning to handler execution.
|
303
|
+
|
304
|
+
:param params: (DictData) A parameter data that want to use in this
|
305
|
+
execution.
|
306
|
+
:param result: (Result) A result object for keeping context and status
|
307
|
+
data.
|
308
|
+
:param event: (Event) An event manager that use to track parent execute
|
309
|
+
was not force stopped.
|
310
|
+
|
311
|
+
:rtype: Result
|
312
|
+
"""
|
313
|
+
result.catch(status=WAIT)
|
314
|
+
return self.execute(params, result=result, event=event)
|
315
|
+
|
239
316
|
def set_outputs(self, output: DictData, to: DictData) -> DictData:
|
240
317
|
"""Set an outputs from execution result context to the received context
|
241
318
|
with a `to` input parameter. The result context from stage execution
|
@@ -283,20 +360,15 @@ class BaseStage(BaseModel, ABC):
|
|
283
360
|
):
|
284
361
|
return to
|
285
362
|
|
286
|
-
|
363
|
+
_id: str = self.gen_id(params=to)
|
364
|
+
output: DictData = copy.deepcopy(output)
|
287
365
|
errors: DictData = (
|
288
|
-
{"errors": output.pop("errors"
|
366
|
+
{"errors": output.pop("errors")} if "errors" in output else {}
|
289
367
|
)
|
290
|
-
|
291
|
-
{"
|
292
|
-
if "skipped" in output
|
293
|
-
else {}
|
368
|
+
status: dict[str, Status] = (
|
369
|
+
{"status": output.pop("status")} if "status" in output else {}
|
294
370
|
)
|
295
|
-
to["stages"][
|
296
|
-
"outputs": copy.deepcopy(output),
|
297
|
-
**skipping,
|
298
|
-
**errors,
|
299
|
-
}
|
371
|
+
to["stages"][_id] = {"outputs": output} | errors | status
|
300
372
|
return to
|
301
373
|
|
302
374
|
def get_outputs(self, output: DictData) -> DictData:
|
@@ -325,14 +397,15 @@ class BaseStage(BaseModel, ABC):
|
|
325
397
|
:param params: (DictData) A parameters that want to pass to condition
|
326
398
|
template.
|
327
399
|
|
328
|
-
:raise
|
400
|
+
:raise StageError: When it has any error raise from the eval
|
329
401
|
condition statement.
|
330
|
-
:raise
|
402
|
+
:raise StageError: When return type of the eval condition statement
|
331
403
|
does not return with boolean type.
|
332
404
|
|
333
405
|
:rtype: bool
|
334
406
|
"""
|
335
|
-
|
407
|
+
# NOTE: Support for condition value is empty string.
|
408
|
+
if not self.condition:
|
336
409
|
return False
|
337
410
|
|
338
411
|
try:
|
@@ -348,13 +421,13 @@ class BaseStage(BaseModel, ABC):
|
|
348
421
|
raise TypeError("Return type of condition does not be boolean")
|
349
422
|
return not rs
|
350
423
|
except Exception as e:
|
351
|
-
raise
|
424
|
+
raise StageError(f"{e.__class__.__name__}: {e}") from e
|
352
425
|
|
353
426
|
def gen_id(self, params: DictData) -> str:
|
354
427
|
"""Generate stage ID that dynamic use stage's name if it ID does not
|
355
428
|
set.
|
356
429
|
|
357
|
-
:param params: A parameter data.
|
430
|
+
:param params: (DictData) A parameter or context data.
|
358
431
|
|
359
432
|
:rtype: str
|
360
433
|
"""
|
@@ -366,8 +439,16 @@ class BaseStage(BaseModel, ABC):
|
|
366
439
|
)
|
367
440
|
)
|
368
441
|
|
442
|
+
@property
|
443
|
+
def is_nested(self) -> bool:
|
444
|
+
"""Return true if this stage is nested stage.
|
445
|
+
|
446
|
+
:rtype: bool
|
447
|
+
"""
|
448
|
+
return False
|
369
449
|
|
370
|
-
|
450
|
+
|
451
|
+
class BaseAsyncStage(BaseStage, ABC):
|
371
452
|
"""Base Async Stage model to make any stage model allow async execution for
|
372
453
|
optimize CPU and Memory on the current node. If you want to implement any
|
373
454
|
custom async stage, you can inherit this class and implement
|
@@ -377,18 +458,6 @@ class BaseAsyncStage(BaseStage):
|
|
377
458
|
model.
|
378
459
|
"""
|
379
460
|
|
380
|
-
@abstractmethod
|
381
|
-
def execute(
|
382
|
-
self,
|
383
|
-
params: DictData,
|
384
|
-
*,
|
385
|
-
result: Optional[Result] = None,
|
386
|
-
event: Optional[Event] = None,
|
387
|
-
) -> Result:
|
388
|
-
raise NotImplementedError(
|
389
|
-
"Async Stage should implement `execute` method."
|
390
|
-
)
|
391
|
-
|
392
461
|
@abstractmethod
|
393
462
|
async def axecute(
|
394
463
|
self,
|
@@ -421,7 +490,6 @@ class BaseAsyncStage(BaseStage):
|
|
421
490
|
parent_run_id: StrOrNone = None,
|
422
491
|
result: Optional[Result] = None,
|
423
492
|
event: Optional[Event] = None,
|
424
|
-
raise_error: Optional[bool] = None,
|
425
493
|
) -> Result:
|
426
494
|
"""Async Handler stage execution result from the stage `execute` method.
|
427
495
|
|
@@ -431,7 +499,6 @@ class BaseAsyncStage(BaseStage):
|
|
431
499
|
:param result: (Result) A Result instance for return context and status.
|
432
500
|
:param event: (Event) An Event manager instance that use to cancel this
|
433
501
|
execution if it forces stopped by parent execution.
|
434
|
-
:param raise_error: (bool) A flag that all this method raise error
|
435
502
|
|
436
503
|
:rtype: Result
|
437
504
|
"""
|
@@ -442,22 +509,142 @@ class BaseAsyncStage(BaseStage):
|
|
442
509
|
id_logic=self.iden,
|
443
510
|
extras=self.extras,
|
444
511
|
)
|
445
|
-
|
446
512
|
try:
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
raise StageException(
|
456
|
-
f"{self.__class__.__name__}: {e_name}: {e}"
|
457
|
-
) from None
|
513
|
+
await result.trace.ainfo(
|
514
|
+
f"[STAGE]: Handler {to_train(self.__class__.__name__)}: "
|
515
|
+
f"{self.name!r}."
|
516
|
+
)
|
517
|
+
if self.desc:
|
518
|
+
await result.trace.adebug(
|
519
|
+
f"[STAGE]: Description:||{self.desc}||"
|
520
|
+
)
|
458
521
|
|
522
|
+
if self.is_skipped(params=params):
|
523
|
+
raise StageSkipError(
|
524
|
+
f"Skip because condition {self.condition} was valid."
|
525
|
+
)
|
526
|
+
|
527
|
+
# NOTE: Start call wrapped execution method that will use custom
|
528
|
+
# execution before the real execution from inherit stage model.
|
529
|
+
result_caught: Result = await self.__axecute(
|
530
|
+
params, result=result, event=event
|
531
|
+
)
|
532
|
+
if result_caught.status == WAIT:
|
533
|
+
raise StageError(
|
534
|
+
"Status from execution should not return waiting status."
|
535
|
+
)
|
536
|
+
return result_caught
|
537
|
+
|
538
|
+
# NOTE: Catch this error in this line because the execution can raise
|
539
|
+
# this exception class at other location.
|
540
|
+
except (
|
541
|
+
StageSkipError,
|
542
|
+
StageCancelError,
|
543
|
+
StageError,
|
544
|
+
) as e: # pragma: no cov
|
545
|
+
await result.trace.ainfo(
|
546
|
+
f"[STAGE]: Skip Handler:||{e.__class__.__name__}: {e}||"
|
547
|
+
f"{traceback.format_exc()}"
|
548
|
+
)
|
549
|
+
return result.catch(
|
550
|
+
status=get_status_from_error(e),
|
551
|
+
context=(
|
552
|
+
{"errors": e.to_dict()}
|
553
|
+
if isinstance(e, StageError)
|
554
|
+
else None
|
555
|
+
),
|
556
|
+
)
|
557
|
+
except Exception as e:
|
558
|
+
await result.trace.aerror(
|
559
|
+
f"[STAGE]: Error Handler:||{e.__class__.__name__}: {e}||"
|
560
|
+
f"{traceback.format_exc()}"
|
561
|
+
)
|
459
562
|
return result.catch(status=FAILED, context={"errors": to_dict(e)})
|
460
563
|
|
564
|
+
async def __axecute(
|
565
|
+
self, params: DictData, result: Result, event: Optional[Event]
|
566
|
+
) -> Result:
|
567
|
+
"""Wrapped the axecute method before returning to handler axecute.
|
568
|
+
|
569
|
+
:param params: (DictData) A parameter data that want to use in this
|
570
|
+
execution.
|
571
|
+
:param result: (Result) A result object for keeping context and status
|
572
|
+
data.
|
573
|
+
:param event: (Event) An event manager that use to track parent execute
|
574
|
+
was not force stopped.
|
575
|
+
|
576
|
+
:rtype: Result
|
577
|
+
"""
|
578
|
+
result.catch(status=WAIT)
|
579
|
+
return await self.axecute(params, result=result, event=event)
|
580
|
+
|
581
|
+
|
582
|
+
class BaseRetryStage(BaseAsyncStage, ABC): # pragma: no cov
|
583
|
+
"""Base Retry Stage model that will execute again when it raises with the
|
584
|
+
`StageRetryError`.
|
585
|
+
"""
|
586
|
+
|
587
|
+
retry: int = Field(
|
588
|
+
default=0,
|
589
|
+
ge=0,
|
590
|
+
lt=20,
|
591
|
+
description="Retry number if stage execution get the error.",
|
592
|
+
)
|
593
|
+
|
594
|
+
def __execute(
|
595
|
+
self,
|
596
|
+
params: DictData,
|
597
|
+
result: Result,
|
598
|
+
event: Optional[Event],
|
599
|
+
) -> Result:
|
600
|
+
"""Wrapped the execute method with retry strategy before returning to
|
601
|
+
handler execute.
|
602
|
+
|
603
|
+
:param params: (DictData) A parameter data that want to use in this
|
604
|
+
execution.
|
605
|
+
:param result: (Result) A result object for keeping context and status
|
606
|
+
data.
|
607
|
+
:param event: (Event) An event manager that use to track parent execute
|
608
|
+
was not force stopped.
|
609
|
+
|
610
|
+
:rtype: Result
|
611
|
+
"""
|
612
|
+
current_retry: int = 0
|
613
|
+
with current_retry < (self.retry + 1):
|
614
|
+
try:
|
615
|
+
result.catch(status=WAIT, context={"retry": current_retry})
|
616
|
+
return self.execute(params, result=result, event=event)
|
617
|
+
except StageRetryError:
|
618
|
+
current_retry += 1
|
619
|
+
raise StageError(f"Reach the maximum of retry number: {self.retry}.")
|
620
|
+
|
621
|
+
async def __axecute(
|
622
|
+
self,
|
623
|
+
params: DictData,
|
624
|
+
result: Result,
|
625
|
+
event: Optional[Event],
|
626
|
+
) -> Result:
|
627
|
+
"""Wrapped the axecute method with retry strategy before returning to
|
628
|
+
handler axecute.
|
629
|
+
|
630
|
+
:param params: (DictData) A parameter data that want to use in this
|
631
|
+
execution.
|
632
|
+
:param result: (Result) A result object for keeping context and status
|
633
|
+
data.
|
634
|
+
:param event: (Event) An event manager that use to track parent execute
|
635
|
+
was not force stopped.
|
636
|
+
|
637
|
+
:rtype: Result
|
638
|
+
"""
|
639
|
+
current_retry: int = 0
|
640
|
+
with current_retry < (self.retry + 1):
|
641
|
+
try:
|
642
|
+
result.catch(status=WAIT, context={"retry": current_retry})
|
643
|
+
return await self.axecute(params, result=result, event=event)
|
644
|
+
except StageRetryError:
|
645
|
+
current_retry += 1
|
646
|
+
raise StageError(f"Reach the maximum of retry number: {self.retry}.")
|
647
|
+
|
461
648
|
|
462
649
|
class EmptyStage(BaseAsyncStage):
|
463
650
|
"""Empty stage executor that do nothing and log the `message` field to
|
@@ -521,12 +708,15 @@ class EmptyStage(BaseAsyncStage):
|
|
521
708
|
else "..."
|
522
709
|
)
|
523
710
|
|
524
|
-
|
525
|
-
|
526
|
-
|
711
|
+
if event and event.is_set():
|
712
|
+
raise StageCancelError(
|
713
|
+
"Execution was canceled from the event before start parallel."
|
714
|
+
)
|
715
|
+
|
716
|
+
result.trace.info(f"[STAGE]: Message: ( {message} )")
|
527
717
|
if self.sleep > 0:
|
528
718
|
if self.sleep > 5:
|
529
|
-
result.trace.info(f"[STAGE]: ...
|
719
|
+
result.trace.info(f"[STAGE]: Sleep ... ({self.sleep} sec)")
|
530
720
|
time.sleep(self.sleep)
|
531
721
|
return result.catch(status=SUCCESS)
|
532
722
|
|
@@ -560,11 +750,16 @@ class EmptyStage(BaseAsyncStage):
|
|
560
750
|
else "..."
|
561
751
|
)
|
562
752
|
|
563
|
-
|
753
|
+
if event and event.is_set():
|
754
|
+
raise StageCancelError(
|
755
|
+
"Execution was canceled from the event before start parallel."
|
756
|
+
)
|
757
|
+
|
758
|
+
result.trace.info(f"[STAGE]: Message: ( {message} )")
|
564
759
|
if self.sleep > 0:
|
565
760
|
if self.sleep > 5:
|
566
761
|
await result.trace.ainfo(
|
567
|
-
f"[STAGE]: ...
|
762
|
+
f"[STAGE]: Sleep ... ({self.sleep} sec)"
|
568
763
|
)
|
569
764
|
await asyncio.sleep(self.sleep)
|
570
765
|
return result.catch(status=SUCCESS)
|
@@ -697,19 +892,15 @@ class BashStage(BaseAsyncStage):
|
|
697
892
|
run_id=gen_id(self.name + (self.id or ""), unique=True),
|
698
893
|
extras=self.extras,
|
699
894
|
)
|
700
|
-
|
701
|
-
result.trace.info(f"[STAGE]: Execute Shell-Stage: {self.name}")
|
702
|
-
|
703
895
|
bash: str = param2template(
|
704
896
|
dedent(self.bash.strip("\n")), params, extras=self.extras
|
705
897
|
)
|
706
|
-
|
707
898
|
with self.create_sh_file(
|
708
899
|
bash=bash,
|
709
900
|
env=param2template(self.env, params, extras=self.extras),
|
710
901
|
run_id=result.run_id,
|
711
902
|
) as sh:
|
712
|
-
result.trace.debug(f"[STAGE]:
|
903
|
+
result.trace.debug(f"[STAGE]: Create `{sh[1]}` file.")
|
713
904
|
rs: CompletedProcess = subprocess.run(
|
714
905
|
sh,
|
715
906
|
shell=False,
|
@@ -720,7 +911,7 @@ class BashStage(BaseAsyncStage):
|
|
720
911
|
)
|
721
912
|
if rs.returncode > 0:
|
722
913
|
e: str = rs.stderr.removesuffix("\n")
|
723
|
-
raise
|
914
|
+
raise StageError(
|
724
915
|
f"Subprocess: {e}\n---( statement )---\n```bash\n{bash}\n```"
|
725
916
|
)
|
726
917
|
return result.catch(
|
@@ -753,17 +944,15 @@ class BashStage(BaseAsyncStage):
|
|
753
944
|
run_id=gen_id(self.name + (self.id or ""), unique=True),
|
754
945
|
extras=self.extras,
|
755
946
|
)
|
756
|
-
await result.trace.ainfo(f"[STAGE]: Execute Shell-Stage: {self.name}")
|
757
947
|
bash: str = param2template(
|
758
948
|
dedent(self.bash.strip("\n")), params, extras=self.extras
|
759
949
|
)
|
760
|
-
|
761
950
|
async with self.async_create_sh_file(
|
762
951
|
bash=bash,
|
763
952
|
env=param2template(self.env, params, extras=self.extras),
|
764
953
|
run_id=result.run_id,
|
765
954
|
) as sh:
|
766
|
-
await result.trace.adebug(f"[STAGE]:
|
955
|
+
await result.trace.adebug(f"[STAGE]: Create `{sh[1]}` file.")
|
767
956
|
rs: CompletedProcess = subprocess.run(
|
768
957
|
sh,
|
769
958
|
shell=False,
|
@@ -775,7 +964,7 @@ class BashStage(BaseAsyncStage):
|
|
775
964
|
|
776
965
|
if rs.returncode > 0:
|
777
966
|
e: str = rs.stderr.removesuffix("\n")
|
778
|
-
raise
|
967
|
+
raise StageError(
|
779
968
|
f"Subprocess: {e}\n---( statement )---\n```bash\n{bash}\n```"
|
780
969
|
)
|
781
970
|
return result.catch(
|
@@ -882,7 +1071,6 @@ class PyStage(BaseAsyncStage):
|
|
882
1071
|
run_id=gen_id(self.name + (self.id or ""), unique=True),
|
883
1072
|
extras=self.extras,
|
884
1073
|
)
|
885
|
-
|
886
1074
|
lc: DictData = {}
|
887
1075
|
gb: DictData = (
|
888
1076
|
globals()
|
@@ -890,8 +1078,6 @@ class PyStage(BaseAsyncStage):
|
|
890
1078
|
| {"result": result}
|
891
1079
|
)
|
892
1080
|
|
893
|
-
result.trace.info(f"[STAGE]: Execute Py-Stage: {self.name}")
|
894
|
-
|
895
1081
|
# WARNING: The exec build-in function is very dangerous. So, it
|
896
1082
|
# should use the re module to validate exec-string before running.
|
897
1083
|
exec(
|
@@ -915,6 +1101,7 @@ class PyStage(BaseAsyncStage):
|
|
915
1101
|
and not ismodule(gb[k])
|
916
1102
|
and not isclass(gb[k])
|
917
1103
|
and not isfunction(gb[k])
|
1104
|
+
and k in params
|
918
1105
|
)
|
919
1106
|
},
|
920
1107
|
},
|
@@ -950,8 +1137,6 @@ class PyStage(BaseAsyncStage):
|
|
950
1137
|
| param2template(self.vars, params, extras=self.extras)
|
951
1138
|
| {"result": result}
|
952
1139
|
)
|
953
|
-
await result.trace.ainfo(f"[STAGE]: Execute Py-Stage: {self.name}")
|
954
|
-
|
955
1140
|
# WARNING: The exec build-in function is very dangerous. So, it
|
956
1141
|
# should use the re module to validate exec-string before running.
|
957
1142
|
exec(
|
@@ -972,6 +1157,7 @@ class PyStage(BaseAsyncStage):
|
|
972
1157
|
and not ismodule(gb[k])
|
973
1158
|
and not isclass(gb[k])
|
974
1159
|
and not isfunction(gb[k])
|
1160
|
+
and k in params
|
975
1161
|
)
|
976
1162
|
},
|
977
1163
|
},
|
@@ -1054,7 +1240,7 @@ class CallStage(BaseAsyncStage):
|
|
1054
1240
|
)()
|
1055
1241
|
|
1056
1242
|
result.trace.info(
|
1057
|
-
f"[STAGE]:
|
1243
|
+
f"[STAGE]: Caller Func: '{call_func.name}@{call_func.tag}'"
|
1058
1244
|
)
|
1059
1245
|
|
1060
1246
|
# VALIDATE: check input task caller parameters that exists before
|
@@ -1088,8 +1274,12 @@ class CallStage(BaseAsyncStage):
|
|
1088
1274
|
if "result" not in sig.parameters and not has_keyword:
|
1089
1275
|
args.pop("result")
|
1090
1276
|
|
1091
|
-
|
1277
|
+
if event and event.is_set():
|
1278
|
+
raise StageCancelError(
|
1279
|
+
"Execution was canceled from the event before start parallel."
|
1280
|
+
)
|
1092
1281
|
|
1282
|
+
args = self.validate_model_args(call_func, args, result)
|
1093
1283
|
if inspect.iscoroutinefunction(call_func):
|
1094
1284
|
loop = asyncio.get_event_loop()
|
1095
1285
|
rs: DictData = loop.run_until_complete(
|
@@ -1143,7 +1333,7 @@ class CallStage(BaseAsyncStage):
|
|
1143
1333
|
)()
|
1144
1334
|
|
1145
1335
|
await result.trace.ainfo(
|
1146
|
-
f"[STAGE]:
|
1336
|
+
f"[STAGE]: Caller Func: '{call_func.name}@{call_func.tag}'"
|
1147
1337
|
)
|
1148
1338
|
|
1149
1339
|
# VALIDATE: check input task caller parameters that exists before
|
@@ -1177,7 +1367,7 @@ class CallStage(BaseAsyncStage):
|
|
1177
1367
|
if "result" not in sig.parameters and not has_keyword:
|
1178
1368
|
args.pop("result")
|
1179
1369
|
|
1180
|
-
args = self.
|
1370
|
+
args: DictData = self.validate_model_args(call_func, args, result)
|
1181
1371
|
if inspect.iscoroutinefunction(call_func):
|
1182
1372
|
rs: DictOrModel = await call_func(
|
1183
1373
|
**param2template(args, params, extras=self.extras)
|
@@ -1200,23 +1390,41 @@ class CallStage(BaseAsyncStage):
|
|
1200
1390
|
return result.catch(status=SUCCESS, context=dump_all(rs, by_alias=True))
|
1201
1391
|
|
1202
1392
|
@staticmethod
|
1203
|
-
def
|
1393
|
+
def validate_model_args(
|
1204
1394
|
func: TagFunc,
|
1205
1395
|
args: DictData,
|
1206
1396
|
result: Result,
|
1207
1397
|
) -> DictData:
|
1208
|
-
"""
|
1209
|
-
caller function.
|
1398
|
+
"""Validate an input arguments before passing to the caller function.
|
1210
1399
|
|
1211
|
-
:param func: A tag function that want to get typing.
|
1212
|
-
:param args: An arguments before passing to this tag
|
1400
|
+
:param func: (TagFunc) A tag function that want to get typing.
|
1401
|
+
:param args: (DictData) An arguments before passing to this tag func.
|
1213
1402
|
:param result: (Result) A result object for keeping context and status
|
1214
1403
|
data.
|
1215
1404
|
|
1216
1405
|
:rtype: DictData
|
1217
1406
|
"""
|
1218
1407
|
try:
|
1408
|
+
model_instance: BaseModel = create_model_from_caller(
|
1409
|
+
func
|
1410
|
+
).model_validate(args)
|
1411
|
+
override: DictData = dict(model_instance)
|
1412
|
+
args.update(override)
|
1219
1413
|
type_hints: dict[str, Any] = get_type_hints(func)
|
1414
|
+
for arg in type_hints:
|
1415
|
+
|
1416
|
+
if arg == "return":
|
1417
|
+
continue
|
1418
|
+
|
1419
|
+
if arg.removeprefix("_") in args:
|
1420
|
+
args[arg] = args.pop(arg.removeprefix("_"))
|
1421
|
+
continue
|
1422
|
+
|
1423
|
+
return args
|
1424
|
+
except ValidationError as e:
|
1425
|
+
raise StageError(
|
1426
|
+
"Validate argument from the caller function raise invalid type."
|
1427
|
+
) from e
|
1220
1428
|
except TypeError as e:
|
1221
1429
|
result.trace.warning(
|
1222
1430
|
f"[STAGE]: Get type hint raise TypeError: {e}, so, it skip "
|
@@ -1224,28 +1432,43 @@ class CallStage(BaseAsyncStage):
|
|
1224
1432
|
)
|
1225
1433
|
return args
|
1226
1434
|
|
1227
|
-
for arg in type_hints:
|
1228
1435
|
|
1229
|
-
|
1230
|
-
|
1436
|
+
class BaseNestedStage(BaseStage, ABC):
|
1437
|
+
"""Base Nested Stage model. This model is use for checking the child stage
|
1438
|
+
is the nested stage or not.
|
1439
|
+
"""
|
1231
1440
|
|
1232
|
-
|
1233
|
-
|
1441
|
+
def set_outputs(self, output: DictData, to: DictData) -> DictData:
|
1442
|
+
"""Override the set outputs method that support for nested-stage."""
|
1443
|
+
return super().set_outputs(output, to=to)
|
1234
1444
|
|
1235
|
-
|
1445
|
+
def get_outputs(self, output: DictData) -> DictData:
|
1446
|
+
"""Override the get outputs method that support for nested-stage"""
|
1447
|
+
return super().get_outputs(output)
|
1236
1448
|
|
1237
|
-
|
1238
|
-
|
1239
|
-
|
1240
|
-
# args[arg] = result
|
1449
|
+
@property
|
1450
|
+
def is_nested(self) -> bool:
|
1451
|
+
"""Check if this stage is a nested stage or not.
|
1241
1452
|
|
1242
|
-
|
1243
|
-
|
1453
|
+
:rtype: bool
|
1454
|
+
"""
|
1455
|
+
return True
|
1244
1456
|
|
1245
|
-
|
1457
|
+
@staticmethod
|
1458
|
+
def mark_errors(context: DictData, error: StageError) -> None:
|
1459
|
+
"""Make the errors context result with the refs value depends on the nested
|
1460
|
+
execute func.
|
1246
1461
|
|
1462
|
+
:param context: (DictData) A context data.
|
1463
|
+
:param error: (StageError) A stage exception object.
|
1464
|
+
"""
|
1465
|
+
if "errors" in context:
|
1466
|
+
context["errors"][error.refs] = error.to_dict()
|
1467
|
+
else:
|
1468
|
+
context["errors"] = error.to_dict(with_refs=True)
|
1247
1469
|
|
1248
|
-
|
1470
|
+
|
1471
|
+
class TriggerStage(BaseNestedStage):
|
1249
1472
|
"""Trigger workflow executor stage that run an input trigger Workflow
|
1250
1473
|
execute method. This is the stage that allow you to create the reusable
|
1251
1474
|
Workflow template with dynamic parameters.
|
@@ -1277,13 +1500,14 @@ class TriggerStage(BaseStage):
|
|
1277
1500
|
event: Optional[Event] = None,
|
1278
1501
|
) -> Result:
|
1279
1502
|
"""Trigger another workflow execution. It will wait the trigger
|
1280
|
-
workflow running complete before catching its result
|
1503
|
+
workflow running complete before catching its result and raise error
|
1504
|
+
when the result status does not be SUCCESS.
|
1281
1505
|
|
1282
1506
|
:param params: (DictData) A parameter data.
|
1283
1507
|
:param result: (Result) A result object for keeping context and status
|
1284
|
-
data.
|
1508
|
+
data. (Default is None)
|
1285
1509
|
:param event: (Event) An event manager that use to track parent execute
|
1286
|
-
was not force stopped.
|
1510
|
+
was not force stopped. (Default is None)
|
1287
1511
|
|
1288
1512
|
:rtype: Result
|
1289
1513
|
"""
|
@@ -1295,57 +1519,28 @@ class TriggerStage(BaseStage):
|
|
1295
1519
|
)
|
1296
1520
|
|
1297
1521
|
_trigger: str = param2template(self.trigger, params, extras=self.extras)
|
1298
|
-
result
|
1299
|
-
rs: Result = Workflow.from_conf(
|
1522
|
+
result: Result = Workflow.from_conf(
|
1300
1523
|
name=pass_env(_trigger),
|
1301
|
-
extras=self.extras
|
1524
|
+
extras=self.extras,
|
1302
1525
|
).execute(
|
1526
|
+
# NOTE: Should not use the `pass_env` function on this params parameter.
|
1303
1527
|
params=param2template(self.params, params, extras=self.extras),
|
1304
1528
|
run_id=None,
|
1305
1529
|
parent_run_id=result.parent_run_id,
|
1306
1530
|
event=event,
|
1307
1531
|
)
|
1308
|
-
if
|
1532
|
+
if result.status == FAILED:
|
1309
1533
|
err_msg: StrOrNone = (
|
1310
1534
|
f" with:\n{msg}"
|
1311
|
-
if (msg :=
|
1535
|
+
if (msg := result.context.get("errors", {}).get("message"))
|
1312
1536
|
else "."
|
1313
1537
|
)
|
1314
|
-
raise
|
1315
|
-
|
1316
|
-
)
|
1317
|
-
|
1318
|
-
|
1319
|
-
|
1320
|
-
class BaseNestedStage(BaseStage):
|
1321
|
-
"""Base Nested Stage model. This model is use for checking the child stage
|
1322
|
-
is the nested stage or not.
|
1323
|
-
"""
|
1324
|
-
|
1325
|
-
@abstractmethod
|
1326
|
-
def execute(
|
1327
|
-
self,
|
1328
|
-
params: DictData,
|
1329
|
-
*,
|
1330
|
-
result: Optional[Result] = None,
|
1331
|
-
event: Optional[Event] = None,
|
1332
|
-
) -> Result:
|
1333
|
-
"""Execute abstraction method that action something by sub-model class.
|
1334
|
-
This is important method that make this class is able to be the nested
|
1335
|
-
stage.
|
1336
|
-
|
1337
|
-
:param params: (DictData) A parameter data that want to use in this
|
1338
|
-
execution.
|
1339
|
-
:param result: (Result) A result object for keeping context and status
|
1340
|
-
data.
|
1341
|
-
:param event: (Event) An event manager that use to track parent execute
|
1342
|
-
was not force stopped.
|
1343
|
-
|
1344
|
-
:rtype: Result
|
1345
|
-
"""
|
1346
|
-
raise NotImplementedError(
|
1347
|
-
"Nested-Stage should implement `execute` method."
|
1348
|
-
)
|
1538
|
+
raise StageError(f"Trigger workflow was failed{err_msg}")
|
1539
|
+
elif result.status == CANCEL:
|
1540
|
+
raise StageCancelError("Trigger workflow was cancel.")
|
1541
|
+
elif result.status == SKIP:
|
1542
|
+
raise StageSkipError("Trigger workflow was skipped.")
|
1543
|
+
return result
|
1349
1544
|
|
1350
1545
|
|
1351
1546
|
class ParallelStage(BaseNestedStage):
|
@@ -1366,10 +1561,14 @@ class ParallelStage(BaseNestedStage):
|
|
1366
1561
|
... "echo": "Start run with branch 1",
|
1367
1562
|
... "sleep": 3,
|
1368
1563
|
... },
|
1564
|
+
... {
|
1565
|
+
... "name": "Echo second stage",
|
1566
|
+
... "echo": "Start run with branch 1",
|
1567
|
+
... },
|
1369
1568
|
... ],
|
1370
1569
|
... "branch02": [
|
1371
1570
|
... {
|
1372
|
-
... "name": "Echo
|
1571
|
+
... "name": "Echo first stage",
|
1373
1572
|
... "echo": "Start run with branch 2",
|
1374
1573
|
... "sleep": 1,
|
1375
1574
|
... },
|
@@ -1399,94 +1598,116 @@ class ParallelStage(BaseNestedStage):
|
|
1399
1598
|
result: Result,
|
1400
1599
|
*,
|
1401
1600
|
event: Optional[Event] = None,
|
1402
|
-
) -> Result:
|
1403
|
-
"""Execute all stage
|
1601
|
+
) -> tuple[Status, Result]:
|
1602
|
+
"""Execute branch that will execute all nested-stage that was set in
|
1603
|
+
this stage with specific branch ID.
|
1404
1604
|
|
1405
1605
|
:param branch: (str) A branch ID.
|
1406
1606
|
:param params: (DictData) A parameter data.
|
1407
1607
|
:param result: (Result) A Result instance for return context and status.
|
1408
1608
|
:param event: (Event) An Event manager instance that use to cancel this
|
1409
1609
|
execution if it forces stopped by parent execution.
|
1610
|
+
(Default is None)
|
1410
1611
|
|
1411
|
-
:
|
1612
|
+
:raise StageCancelError: If event was set.
|
1613
|
+
|
1614
|
+
:rtype: tuple[Status, Result]
|
1412
1615
|
"""
|
1413
1616
|
result.trace.debug(f"[STAGE]: Execute Branch: {branch!r}")
|
1617
|
+
|
1618
|
+
# NOTE: Create nested-context
|
1414
1619
|
context: DictData = copy.deepcopy(params)
|
1415
1620
|
context.update({"branch": branch})
|
1416
|
-
|
1417
|
-
|
1621
|
+
nestet_context: DictData = {"branch": branch, "stages": {}}
|
1622
|
+
|
1623
|
+
total_stage: int = len(self.parallel[branch])
|
1624
|
+
skips: list[bool] = [False] * total_stage
|
1625
|
+
for i, stage in enumerate(self.parallel[branch], start=0):
|
1418
1626
|
|
1419
1627
|
if self.extras:
|
1420
1628
|
stage.extras = self.extras
|
1421
1629
|
|
1422
|
-
if stage.is_skipped(params=context):
|
1423
|
-
result.trace.info(f"[STAGE]: Skip stage: {stage.iden!r}")
|
1424
|
-
stage.set_outputs(output={"skipped": True}, to=output)
|
1425
|
-
continue
|
1426
|
-
|
1427
1630
|
if event and event.is_set():
|
1428
1631
|
error_msg: str = (
|
1429
|
-
"Branch
|
1430
|
-
"
|
1632
|
+
"Branch execution was canceled from the event before "
|
1633
|
+
"start branch execution."
|
1431
1634
|
)
|
1432
1635
|
result.catch(
|
1433
1636
|
status=CANCEL,
|
1434
1637
|
parallel={
|
1435
1638
|
branch: {
|
1639
|
+
"status": CANCEL,
|
1436
1640
|
"branch": branch,
|
1437
|
-
"stages": filter_func(
|
1438
|
-
|
1641
|
+
"stages": filter_func(
|
1642
|
+
nestet_context.pop("stages", {})
|
1643
|
+
),
|
1644
|
+
"errors": StageCancelError(error_msg).to_dict(),
|
1439
1645
|
}
|
1440
1646
|
},
|
1441
1647
|
)
|
1442
|
-
raise
|
1648
|
+
raise StageCancelError(error_msg, refs=branch)
|
1443
1649
|
|
1444
|
-
|
1445
|
-
|
1446
|
-
|
1447
|
-
|
1448
|
-
|
1449
|
-
|
1450
|
-
|
1650
|
+
rs: Result = stage.handler_execute(
|
1651
|
+
params=context,
|
1652
|
+
run_id=result.run_id,
|
1653
|
+
parent_run_id=result.parent_run_id,
|
1654
|
+
event=event,
|
1655
|
+
)
|
1656
|
+
stage.set_outputs(rs.context, to=nestet_context)
|
1657
|
+
stage.set_outputs(stage.get_outputs(nestet_context), to=context)
|
1658
|
+
|
1659
|
+
if rs.status == SKIP:
|
1660
|
+
skips[i] = True
|
1661
|
+
continue
|
1662
|
+
|
1663
|
+
elif rs.status == FAILED: # pragma: no cov
|
1664
|
+
error_msg: str = (
|
1665
|
+
f"Branch execution was break because its nested-stage, "
|
1666
|
+
f"{stage.iden!r}, failed."
|
1451
1667
|
)
|
1452
|
-
stage.set_outputs(rs.context, to=output)
|
1453
|
-
stage.set_outputs(stage.get_outputs(output), to=context)
|
1454
|
-
except StageException as e:
|
1455
1668
|
result.catch(
|
1456
1669
|
status=FAILED,
|
1457
1670
|
parallel={
|
1458
1671
|
branch: {
|
1672
|
+
"status": FAILED,
|
1459
1673
|
"branch": branch,
|
1460
|
-
"stages": filter_func(
|
1461
|
-
|
1674
|
+
"stages": filter_func(
|
1675
|
+
nestet_context.pop("stages", {})
|
1676
|
+
),
|
1677
|
+
"errors": StageError(error_msg).to_dict(),
|
1462
1678
|
},
|
1463
1679
|
},
|
1464
1680
|
)
|
1465
|
-
raise
|
1681
|
+
raise StageError(error_msg, refs=branch)
|
1466
1682
|
|
1467
|
-
|
1683
|
+
elif rs.status == CANCEL:
|
1468
1684
|
error_msg: str = (
|
1469
|
-
|
1470
|
-
|
1685
|
+
"Branch execution was canceled from the event after "
|
1686
|
+
"end branch execution."
|
1471
1687
|
)
|
1472
1688
|
result.catch(
|
1473
|
-
status=
|
1689
|
+
status=CANCEL,
|
1474
1690
|
parallel={
|
1475
1691
|
branch: {
|
1692
|
+
"status": CANCEL,
|
1476
1693
|
"branch": branch,
|
1477
|
-
"stages": filter_func(
|
1478
|
-
|
1479
|
-
|
1694
|
+
"stages": filter_func(
|
1695
|
+
nestet_context.pop("stages", {})
|
1696
|
+
),
|
1697
|
+
"errors": StageCancelError(error_msg).to_dict(),
|
1698
|
+
}
|
1480
1699
|
},
|
1481
1700
|
)
|
1482
|
-
raise
|
1701
|
+
raise StageCancelError(error_msg, refs=branch)
|
1483
1702
|
|
1484
|
-
|
1485
|
-
|
1703
|
+
status: Status = SKIP if sum(skips) == total_stage else SUCCESS
|
1704
|
+
return status, result.catch(
|
1705
|
+
status=status,
|
1486
1706
|
parallel={
|
1487
1707
|
branch: {
|
1708
|
+
"status": status,
|
1488
1709
|
"branch": branch,
|
1489
|
-
"stages": filter_func(
|
1710
|
+
"stages": filter_func(nestet_context.pop("stages", {})),
|
1490
1711
|
},
|
1491
1712
|
},
|
1492
1713
|
)
|
@@ -1504,6 +1725,7 @@ class ParallelStage(BaseNestedStage):
|
|
1504
1725
|
:param result: (Result) A Result instance for return context and status.
|
1505
1726
|
:param event: (Event) An Event manager instance that use to cancel this
|
1506
1727
|
execution if it forces stopped by parent execution.
|
1728
|
+
(Default is None)
|
1507
1729
|
|
1508
1730
|
:rtype: Result
|
1509
1731
|
"""
|
@@ -1512,28 +1734,18 @@ class ParallelStage(BaseNestedStage):
|
|
1512
1734
|
extras=self.extras,
|
1513
1735
|
)
|
1514
1736
|
event: Event = event or Event()
|
1515
|
-
result.trace.info(
|
1516
|
-
|
1737
|
+
result.trace.info(f"[STAGE]: Parallel with {self.max_workers} workers.")
|
1738
|
+
result.catch(
|
1739
|
+
status=WAIT,
|
1740
|
+
context={"workers": self.max_workers, "parallel": {}},
|
1517
1741
|
)
|
1518
|
-
|
1742
|
+
len_parallel: int = len(self.parallel)
|
1519
1743
|
if event and event.is_set():
|
1520
|
-
|
1521
|
-
|
1522
|
-
context={
|
1523
|
-
"errors": StageException(
|
1524
|
-
"Stage was canceled from event that had set "
|
1525
|
-
"before stage parallel execution."
|
1526
|
-
).to_dict()
|
1527
|
-
},
|
1744
|
+
raise StageCancelError(
|
1745
|
+
"Execution was canceled from the event before start parallel."
|
1528
1746
|
)
|
1529
1747
|
|
1530
|
-
with ThreadPoolExecutor(
|
1531
|
-
max_workers=self.max_workers, thread_name_prefix="stage_parallel_"
|
1532
|
-
) as executor:
|
1533
|
-
|
1534
|
-
context: DictData = {}
|
1535
|
-
status: Status = SUCCESS
|
1536
|
-
|
1748
|
+
with ThreadPoolExecutor(self.max_workers, "stp") as executor:
|
1537
1749
|
futures: list[Future] = [
|
1538
1750
|
executor.submit(
|
1539
1751
|
self.execute_branch,
|
@@ -1544,17 +1756,18 @@ class ParallelStage(BaseNestedStage):
|
|
1544
1756
|
)
|
1545
1757
|
for branch in self.parallel
|
1546
1758
|
]
|
1547
|
-
|
1548
|
-
|
1759
|
+
context: DictData = {}
|
1760
|
+
statuses: list[Status] = [WAIT] * len_parallel
|
1761
|
+
for i, future in enumerate(as_completed(futures), start=0):
|
1549
1762
|
try:
|
1550
|
-
future.result()
|
1551
|
-
except
|
1552
|
-
|
1553
|
-
|
1554
|
-
|
1555
|
-
|
1556
|
-
|
1557
|
-
|
1763
|
+
statuses[i], _ = future.result()
|
1764
|
+
except StageError as e:
|
1765
|
+
statuses[i] = get_status_from_error(e)
|
1766
|
+
self.mark_errors(context, e)
|
1767
|
+
return result.catch(
|
1768
|
+
status=validate_statuses(statuses),
|
1769
|
+
context=context,
|
1770
|
+
)
|
1558
1771
|
|
1559
1772
|
|
1560
1773
|
class ForEachStage(BaseNestedStage):
|
@@ -1614,9 +1827,12 @@ class ForEachStage(BaseNestedStage):
|
|
1614
1827
|
result: Result,
|
1615
1828
|
*,
|
1616
1829
|
event: Optional[Event] = None,
|
1617
|
-
) -> Result:
|
1618
|
-
"""Execute all nested
|
1619
|
-
item
|
1830
|
+
) -> tuple[Status, Result]:
|
1831
|
+
"""Execute item that will execute all nested-stage that was set in this
|
1832
|
+
stage with specific foreach item.
|
1833
|
+
|
1834
|
+
This method will create the nested-context from an input context
|
1835
|
+
data and use it instead the context data.
|
1620
1836
|
|
1621
1837
|
:param index: (int) An index value of foreach loop.
|
1622
1838
|
:param item: (str | int) An item that want to execution.
|
@@ -1624,91 +1840,112 @@ class ForEachStage(BaseNestedStage):
|
|
1624
1840
|
:param result: (Result) A Result instance for return context and status.
|
1625
1841
|
:param event: (Event) An Event manager instance that use to cancel this
|
1626
1842
|
execution if it forces stopped by parent execution.
|
1843
|
+
(Default is None)
|
1627
1844
|
|
1628
|
-
|
1629
|
-
|
1630
|
-
:raise StageException: If the result from execution has `FAILED` status.
|
1845
|
+
This method should raise error when it wants to stop the foreach
|
1846
|
+
loop such as cancel event or getting the failed status.
|
1631
1847
|
|
1632
|
-
:
|
1848
|
+
:raise StageCancelError: If event was set.
|
1849
|
+
:raise StageError: If the stage execution raise any Exception error.
|
1850
|
+
:raise StageError: If the result from execution has `FAILED` status.
|
1851
|
+
|
1852
|
+
:rtype: tuple[Status, Result]
|
1633
1853
|
"""
|
1634
1854
|
result.trace.debug(f"[STAGE]: Execute Item: {item!r}")
|
1635
1855
|
key: StrOrInt = index if self.use_index_as_key else item
|
1856
|
+
|
1636
1857
|
context: DictData = copy.deepcopy(params)
|
1637
1858
|
context.update({"item": item, "loop": index})
|
1638
|
-
|
1639
|
-
|
1859
|
+
nestet_context: DictData = {"item": item, "stages": {}}
|
1860
|
+
total_stage: int = len(self.stages)
|
1861
|
+
skips: list[bool] = [False] * total_stage
|
1862
|
+
for i, stage in enumerate(self.stages, start=0):
|
1640
1863
|
|
1641
1864
|
if self.extras:
|
1642
1865
|
stage.extras = self.extras
|
1643
1866
|
|
1644
|
-
if stage.is_skipped(params=context):
|
1645
|
-
result.trace.info(f"[STAGE]: Skip stage: {stage.iden!r}")
|
1646
|
-
stage.set_outputs(output={"skipped": True}, to=output)
|
1647
|
-
continue
|
1648
|
-
|
1649
1867
|
if event and event.is_set():
|
1650
1868
|
error_msg: str = (
|
1651
|
-
"Item
|
1869
|
+
"Item execution was canceled from the event before start "
|
1870
|
+
"item execution."
|
1652
1871
|
)
|
1653
1872
|
result.catch(
|
1654
1873
|
status=CANCEL,
|
1655
1874
|
foreach={
|
1656
1875
|
key: {
|
1876
|
+
"status": CANCEL,
|
1657
1877
|
"item": item,
|
1658
|
-
"stages": filter_func(
|
1659
|
-
|
1878
|
+
"stages": filter_func(
|
1879
|
+
nestet_context.pop("stages", {})
|
1880
|
+
),
|
1881
|
+
"errors": StageCancelError(error_msg).to_dict(),
|
1660
1882
|
}
|
1661
1883
|
},
|
1662
1884
|
)
|
1663
|
-
raise
|
1885
|
+
raise StageCancelError(error_msg, refs=key)
|
1664
1886
|
|
1665
|
-
|
1666
|
-
|
1667
|
-
|
1668
|
-
|
1669
|
-
|
1670
|
-
|
1671
|
-
|
1887
|
+
rs: Result = stage.handler_execute(
|
1888
|
+
params=context,
|
1889
|
+
run_id=result.run_id,
|
1890
|
+
parent_run_id=result.parent_run_id,
|
1891
|
+
event=event,
|
1892
|
+
)
|
1893
|
+
stage.set_outputs(rs.context, to=nestet_context)
|
1894
|
+
stage.set_outputs(stage.get_outputs(nestet_context), to=context)
|
1895
|
+
|
1896
|
+
if rs.status == SKIP:
|
1897
|
+
skips[i] = True
|
1898
|
+
continue
|
1899
|
+
|
1900
|
+
elif rs.status == FAILED: # pragma: no cov
|
1901
|
+
error_msg: str = (
|
1902
|
+
f"Item execution was break because its nested-stage, "
|
1903
|
+
f"{stage.iden!r}, failed."
|
1672
1904
|
)
|
1673
|
-
|
1674
|
-
stage.set_outputs(stage.get_outputs(output), to=context)
|
1675
|
-
except StageException as e:
|
1905
|
+
result.trace.warning(f"[STAGE]: {error_msg}")
|
1676
1906
|
result.catch(
|
1677
1907
|
status=FAILED,
|
1678
1908
|
foreach={
|
1679
1909
|
key: {
|
1910
|
+
"status": FAILED,
|
1680
1911
|
"item": item,
|
1681
|
-
"stages": filter_func(
|
1682
|
-
|
1912
|
+
"stages": filter_func(
|
1913
|
+
nestet_context.pop("stages", {})
|
1914
|
+
),
|
1915
|
+
"errors": StageError(error_msg).to_dict(),
|
1683
1916
|
},
|
1684
1917
|
},
|
1685
1918
|
)
|
1686
|
-
raise
|
1919
|
+
raise StageError(error_msg, refs=key)
|
1687
1920
|
|
1688
|
-
|
1921
|
+
elif rs.status == CANCEL:
|
1689
1922
|
error_msg: str = (
|
1690
|
-
|
1691
|
-
|
1923
|
+
"Item execution was canceled from the event after "
|
1924
|
+
"end item execution."
|
1692
1925
|
)
|
1693
|
-
result.trace.warning(f"[STAGE]: {error_msg}")
|
1694
1926
|
result.catch(
|
1695
|
-
status=
|
1927
|
+
status=CANCEL,
|
1696
1928
|
foreach={
|
1697
1929
|
key: {
|
1930
|
+
"status": CANCEL,
|
1698
1931
|
"item": item,
|
1699
|
-
"stages": filter_func(
|
1700
|
-
|
1701
|
-
|
1932
|
+
"stages": filter_func(
|
1933
|
+
nestet_context.pop("stages", {})
|
1934
|
+
),
|
1935
|
+
"errors": StageCancelError(error_msg).to_dict(),
|
1936
|
+
}
|
1702
1937
|
},
|
1703
1938
|
)
|
1704
|
-
raise
|
1939
|
+
raise StageCancelError(error_msg, refs=key)
|
1705
1940
|
|
1706
|
-
|
1707
|
-
|
1941
|
+
status: Status = SKIP if sum(skips) == total_stage else SUCCESS
|
1942
|
+
return status, result.catch(
|
1943
|
+
status=status,
|
1708
1944
|
foreach={
|
1709
1945
|
key: {
|
1946
|
+
"status": status,
|
1710
1947
|
"item": item,
|
1711
|
-
"stages": filter_func(
|
1948
|
+
"stages": filter_func(nestet_context.pop("stages", {})),
|
1712
1949
|
},
|
1713
1950
|
},
|
1714
1951
|
)
|
@@ -1736,38 +1973,42 @@ class ForEachStage(BaseNestedStage):
|
|
1736
1973
|
extras=self.extras,
|
1737
1974
|
)
|
1738
1975
|
event: Event = event or Event()
|
1739
|
-
foreach: Union[list[str], list[int]] = (
|
1976
|
+
foreach: Union[list[str], list[int]] = pass_env(
|
1740
1977
|
param2template(self.foreach, params, extras=self.extras)
|
1741
|
-
if isinstance(self.foreach, str)
|
1742
|
-
else self.foreach
|
1743
1978
|
)
|
1744
1979
|
|
1980
|
+
# [NOTE]: Force convert str to list.
|
1981
|
+
if isinstance(foreach, str):
|
1982
|
+
try:
|
1983
|
+
foreach: list[Any] = str2list(foreach)
|
1984
|
+
except ValueError as e:
|
1985
|
+
raise TypeError(
|
1986
|
+
f"Does not support string foreach: {foreach!r} that can "
|
1987
|
+
f"not convert to list."
|
1988
|
+
) from e
|
1989
|
+
|
1745
1990
|
# [VALIDATE]: Type of the foreach should be `list` type.
|
1746
|
-
|
1747
|
-
raise TypeError(
|
1991
|
+
elif not isinstance(foreach, list):
|
1992
|
+
raise TypeError(
|
1993
|
+
f"Does not support foreach: {foreach!r} ({type(foreach)})"
|
1994
|
+
)
|
1995
|
+
# [Validate]: Value in the foreach item should not be duplicate when the
|
1996
|
+
# `use_index_as_key` field did not set.
|
1748
1997
|
elif len(set(foreach)) != len(foreach) and not self.use_index_as_key:
|
1749
1998
|
raise ValueError(
|
1750
1999
|
"Foreach item should not duplicate. If this stage must to pass "
|
1751
2000
|
"duplicate item, it should set `use_index_as_key: true`."
|
1752
2001
|
)
|
1753
2002
|
|
1754
|
-
result.trace.info(f"[STAGE]:
|
2003
|
+
result.trace.info(f"[STAGE]: Foreach: {foreach!r}.")
|
1755
2004
|
result.catch(status=WAIT, context={"items": foreach, "foreach": {}})
|
2005
|
+
len_foreach: int = len(foreach)
|
1756
2006
|
if event and event.is_set():
|
1757
|
-
|
1758
|
-
|
1759
|
-
context={
|
1760
|
-
"errors": StageException(
|
1761
|
-
"Stage was canceled from event that had set "
|
1762
|
-
"before stage foreach execution."
|
1763
|
-
).to_dict()
|
1764
|
-
},
|
2007
|
+
raise StageCancelError(
|
2008
|
+
"Execution was canceled from the event before start foreach."
|
1765
2009
|
)
|
1766
2010
|
|
1767
|
-
with ThreadPoolExecutor(
|
1768
|
-
max_workers=self.concurrent, thread_name_prefix="stage_foreach_"
|
1769
|
-
) as executor:
|
1770
|
-
|
2011
|
+
with ThreadPoolExecutor(self.concurrent, "stf") as executor:
|
1771
2012
|
futures: list[Future] = [
|
1772
2013
|
executor.submit(
|
1773
2014
|
self.execute_item,
|
@@ -1779,19 +2020,21 @@ class ForEachStage(BaseNestedStage):
|
|
1779
2020
|
)
|
1780
2021
|
for i, item in enumerate(foreach, start=0)
|
1781
2022
|
]
|
2023
|
+
|
1782
2024
|
context: DictData = {}
|
1783
|
-
|
2025
|
+
statuses: list[Status] = [WAIT] * len_foreach
|
2026
|
+
fail_fast: bool = False
|
1784
2027
|
|
1785
2028
|
done, not_done = wait(futures, return_when=FIRST_EXCEPTION)
|
1786
2029
|
if len(list(done)) != len(futures):
|
1787
2030
|
result.trace.warning(
|
1788
|
-
"[STAGE]: Set event for stop pending for-each stage."
|
2031
|
+
"[STAGE]: Set the event for stop pending for-each stage."
|
1789
2032
|
)
|
1790
2033
|
event.set()
|
1791
2034
|
for future in not_done:
|
1792
2035
|
future.cancel()
|
1793
|
-
time.sleep(0.075)
|
1794
2036
|
|
2037
|
+
time.sleep(0.025)
|
1795
2038
|
nd: str = (
|
1796
2039
|
(
|
1797
2040
|
f", {len(not_done)} item"
|
@@ -1804,18 +2047,24 @@ class ForEachStage(BaseNestedStage):
|
|
1804
2047
|
f"[STAGE]: ... Foreach-Stage set failed event{nd}"
|
1805
2048
|
)
|
1806
2049
|
done: Iterator[Future] = as_completed(futures)
|
2050
|
+
fail_fast = True
|
1807
2051
|
|
1808
|
-
for future in done:
|
2052
|
+
for i, future in enumerate(done, start=0):
|
1809
2053
|
try:
|
1810
|
-
future.result()
|
1811
|
-
except
|
1812
|
-
|
1813
|
-
|
1814
|
-
context["errors"][e.refs] = e.to_dict()
|
1815
|
-
else:
|
1816
|
-
context["errors"] = e.to_dict(with_refs=True)
|
2054
|
+
statuses[i], _ = future.result()
|
2055
|
+
except StageError as e:
|
2056
|
+
statuses[i] = get_status_from_error(e)
|
2057
|
+
self.mark_errors(context, e)
|
1817
2058
|
except CancelledError:
|
1818
2059
|
pass
|
2060
|
+
|
2061
|
+
status: Status = validate_statuses(statuses)
|
2062
|
+
|
2063
|
+
# NOTE: Prepare status because it does not cancel from parent event but
|
2064
|
+
# cancel from failed item execution.
|
2065
|
+
if fail_fast and status == CANCEL:
|
2066
|
+
status = FAILED
|
2067
|
+
|
1819
2068
|
return result.catch(status=status, context=context)
|
1820
2069
|
|
1821
2070
|
|
@@ -1874,8 +2123,9 @@ class UntilStage(BaseNestedStage):
|
|
1874
2123
|
params: DictData,
|
1875
2124
|
result: Result,
|
1876
2125
|
event: Optional[Event] = None,
|
1877
|
-
) -> tuple[Result, T]:
|
1878
|
-
"""Execute all stage
|
2126
|
+
) -> tuple[Status, Result, T]:
|
2127
|
+
"""Execute loop that will execute all nested-stage that was set in this
|
2128
|
+
stage with specific loop and item.
|
1879
2129
|
|
1880
2130
|
:param item: (T) An item that want to execution.
|
1881
2131
|
:param loop: (int) A number of loop.
|
@@ -1884,98 +2134,115 @@ class UntilStage(BaseNestedStage):
|
|
1884
2134
|
:param event: (Event) An Event manager instance that use to cancel this
|
1885
2135
|
execution if it forces stopped by parent execution.
|
1886
2136
|
|
1887
|
-
:rtype: tuple[Result, T]
|
2137
|
+
:rtype: tuple[Status, Result, T]
|
1888
2138
|
:return: Return a pair of Result and changed item.
|
1889
2139
|
"""
|
1890
|
-
result.trace.debug(f"[STAGE]:
|
2140
|
+
result.trace.debug(f"[STAGE]: Execute Loop: {loop} (Item {item!r})")
|
2141
|
+
|
2142
|
+
# NOTE: Create nested-context
|
1891
2143
|
context: DictData = copy.deepcopy(params)
|
1892
|
-
context.update({"item": item})
|
1893
|
-
|
1894
|
-
|
1895
|
-
|
2144
|
+
context.update({"item": item, "loop": loop})
|
2145
|
+
nestet_context: DictData = {"loop": loop, "item": item, "stages": {}}
|
2146
|
+
|
2147
|
+
next_item: Optional[T] = None
|
2148
|
+
total_stage: int = len(self.stages)
|
2149
|
+
skips: list[bool] = [False] * total_stage
|
2150
|
+
for i, stage in enumerate(self.stages, start=0):
|
1896
2151
|
|
1897
2152
|
if self.extras:
|
1898
2153
|
stage.extras = self.extras
|
1899
2154
|
|
1900
|
-
if stage.is_skipped(params=context):
|
1901
|
-
result.trace.info(f"[STAGE]: Skip stage: {stage.iden!r}")
|
1902
|
-
stage.set_outputs(output={"skipped": True}, to=output)
|
1903
|
-
continue
|
1904
|
-
|
1905
2155
|
if event and event.is_set():
|
1906
2156
|
error_msg: str = (
|
1907
|
-
"Loop
|
1908
|
-
"
|
2157
|
+
"Loop execution was canceled from the event before start "
|
2158
|
+
"loop execution."
|
1909
2159
|
)
|
1910
|
-
|
1911
|
-
|
1912
|
-
|
1913
|
-
|
1914
|
-
|
1915
|
-
|
1916
|
-
|
1917
|
-
|
1918
|
-
"
|
1919
|
-
|
1920
|
-
|
1921
|
-
|
1922
|
-
|
2160
|
+
result.catch(
|
2161
|
+
status=CANCEL,
|
2162
|
+
until={
|
2163
|
+
loop: {
|
2164
|
+
"status": CANCEL,
|
2165
|
+
"loop": loop,
|
2166
|
+
"item": item,
|
2167
|
+
"stages": filter_func(
|
2168
|
+
nestet_context.pop("stages", {})
|
2169
|
+
),
|
2170
|
+
"errors": StageCancelError(error_msg).to_dict(),
|
2171
|
+
}
|
2172
|
+
},
|
1923
2173
|
)
|
2174
|
+
raise StageCancelError(error_msg, refs=loop)
|
1924
2175
|
|
1925
|
-
|
1926
|
-
|
1927
|
-
|
1928
|
-
|
1929
|
-
|
1930
|
-
|
1931
|
-
|
1932
|
-
|
1933
|
-
|
2176
|
+
rs: Result = stage.handler_execute(
|
2177
|
+
params=context,
|
2178
|
+
run_id=result.run_id,
|
2179
|
+
parent_run_id=result.parent_run_id,
|
2180
|
+
event=event,
|
2181
|
+
)
|
2182
|
+
stage.set_outputs(rs.context, to=nestet_context)
|
2183
|
+
|
2184
|
+
if "item" in (_output := stage.get_outputs(nestet_context)):
|
2185
|
+
next_item = _output["item"]
|
1934
2186
|
|
1935
|
-
|
1936
|
-
|
2187
|
+
stage.set_outputs(_output, to=context)
|
2188
|
+
|
2189
|
+
if rs.status == SKIP:
|
2190
|
+
skips[i] = True
|
2191
|
+
continue
|
1937
2192
|
|
1938
|
-
|
1939
|
-
|
2193
|
+
elif rs.status == FAILED:
|
2194
|
+
error_msg: str = (
|
2195
|
+
f"Loop execution was break because its nested-stage, "
|
2196
|
+
f"{stage.iden!r}, failed."
|
2197
|
+
)
|
1940
2198
|
result.catch(
|
1941
2199
|
status=FAILED,
|
1942
2200
|
until={
|
1943
2201
|
loop: {
|
2202
|
+
"status": FAILED,
|
1944
2203
|
"loop": loop,
|
1945
2204
|
"item": item,
|
1946
|
-
"stages": filter_func(
|
1947
|
-
|
2205
|
+
"stages": filter_func(
|
2206
|
+
nestet_context.pop("stages", {})
|
2207
|
+
),
|
2208
|
+
"errors": StageError(error_msg).to_dict(),
|
1948
2209
|
}
|
1949
2210
|
},
|
1950
2211
|
)
|
1951
|
-
raise
|
2212
|
+
raise StageError(error_msg, refs=loop)
|
1952
2213
|
|
1953
|
-
|
2214
|
+
elif rs.status == CANCEL:
|
1954
2215
|
error_msg: str = (
|
1955
|
-
|
1956
|
-
|
2216
|
+
"Loop execution was canceled from the event after "
|
2217
|
+
"end loop execution."
|
1957
2218
|
)
|
1958
2219
|
result.catch(
|
1959
|
-
status=
|
2220
|
+
status=CANCEL,
|
1960
2221
|
until={
|
1961
2222
|
loop: {
|
2223
|
+
"status": CANCEL,
|
1962
2224
|
"loop": loop,
|
1963
2225
|
"item": item,
|
1964
|
-
"stages": filter_func(
|
1965
|
-
|
2226
|
+
"stages": filter_func(
|
2227
|
+
nestet_context.pop("stages", {})
|
2228
|
+
),
|
2229
|
+
"errors": StageCancelError(error_msg).to_dict(),
|
1966
2230
|
}
|
1967
2231
|
},
|
1968
2232
|
)
|
1969
|
-
raise
|
2233
|
+
raise StageCancelError(error_msg, refs=loop)
|
1970
2234
|
|
2235
|
+
status: Status = SKIP if sum(skips) == total_stage else SUCCESS
|
1971
2236
|
return (
|
2237
|
+
status,
|
1972
2238
|
result.catch(
|
1973
|
-
status=
|
2239
|
+
status=status,
|
1974
2240
|
until={
|
1975
2241
|
loop: {
|
2242
|
+
"status": status,
|
1976
2243
|
"loop": loop,
|
1977
2244
|
"item": item,
|
1978
|
-
"stages": filter_func(
|
2245
|
+
"stages": filter_func(nestet_context.pop("stages", {})),
|
1979
2246
|
}
|
1980
2247
|
},
|
1981
2248
|
),
|
@@ -1989,12 +2256,14 @@ class UntilStage(BaseNestedStage):
|
|
1989
2256
|
result: Optional[Result] = None,
|
1990
2257
|
event: Optional[Event] = None,
|
1991
2258
|
) -> Result:
|
1992
|
-
"""Execute until loop with checking until condition
|
2259
|
+
"""Execute until loop with checking the until condition before release
|
2260
|
+
the next loop.
|
1993
2261
|
|
1994
2262
|
:param params: (DictData) A parameter data.
|
1995
2263
|
:param result: (Result) A Result instance for return context and status.
|
1996
2264
|
:param event: (Event) An Event manager instance that use to cancel this
|
1997
2265
|
execution if it forces stopped by parent execution.
|
2266
|
+
(Default is None)
|
1998
2267
|
|
1999
2268
|
:rtype: Result
|
2000
2269
|
"""
|
@@ -2002,29 +2271,24 @@ class UntilStage(BaseNestedStage):
|
|
2002
2271
|
run_id=gen_id(self.name + (self.id or ""), unique=True),
|
2003
2272
|
extras=self.extras,
|
2004
2273
|
)
|
2005
|
-
|
2006
|
-
result.trace.info(f"[STAGE]:
|
2007
|
-
item: Union[str, int, bool] =
|
2008
|
-
self.item, params, extras=self.extras
|
2274
|
+
event: Event = event or Event()
|
2275
|
+
result.trace.info(f"[STAGE]: Until: {self.until!r}")
|
2276
|
+
item: Union[str, int, bool] = pass_env(
|
2277
|
+
param2template(self.item, params, extras=self.extras)
|
2009
2278
|
)
|
2010
2279
|
loop: int = 1
|
2011
|
-
|
2280
|
+
until_rs: bool = True
|
2012
2281
|
exceed_loop: bool = False
|
2013
2282
|
result.catch(status=WAIT, context={"until": {}})
|
2014
|
-
|
2283
|
+
statuses: list[Status] = []
|
2284
|
+
while until_rs and not (exceed_loop := (loop > self.max_loop)):
|
2015
2285
|
|
2016
2286
|
if event and event.is_set():
|
2017
|
-
|
2018
|
-
|
2019
|
-
context={
|
2020
|
-
"errors": StageException(
|
2021
|
-
"Stage was canceled from event that had set "
|
2022
|
-
"before stage loop execution."
|
2023
|
-
).to_dict()
|
2024
|
-
},
|
2287
|
+
raise StageCancelError(
|
2288
|
+
"Execution was canceled from the event before start loop."
|
2025
2289
|
)
|
2026
2290
|
|
2027
|
-
result, item = self.execute_loop(
|
2291
|
+
status, result, item = self.execute_loop(
|
2028
2292
|
item=item,
|
2029
2293
|
loop=loop,
|
2030
2294
|
params=params,
|
@@ -2034,34 +2298,39 @@ class UntilStage(BaseNestedStage):
|
|
2034
2298
|
|
2035
2299
|
loop += 1
|
2036
2300
|
if item is None:
|
2301
|
+
item: int = loop
|
2037
2302
|
result.trace.warning(
|
2038
|
-
f"[STAGE]:
|
2039
|
-
f"default."
|
2303
|
+
f"[STAGE]: Return loop not set the item. It uses loop: "
|
2304
|
+
f"{loop} by default."
|
2040
2305
|
)
|
2041
|
-
item: int = loop
|
2042
2306
|
|
2043
2307
|
next_track: bool = eval(
|
2044
|
-
|
2045
|
-
|
2046
|
-
|
2047
|
-
|
2308
|
+
pass_env(
|
2309
|
+
param2template(
|
2310
|
+
self.until,
|
2311
|
+
params | {"item": item, "loop": loop},
|
2312
|
+
extras=self.extras,
|
2313
|
+
),
|
2048
2314
|
),
|
2049
2315
|
globals() | params | {"item": item},
|
2050
2316
|
{},
|
2051
2317
|
)
|
2052
2318
|
if not isinstance(next_track, bool):
|
2053
|
-
raise
|
2319
|
+
raise TypeError(
|
2054
2320
|
"Return type of until condition not be `boolean`, getting"
|
2055
2321
|
f": {next_track!r}"
|
2056
2322
|
)
|
2057
|
-
|
2058
|
-
|
2323
|
+
until_rs: bool = not next_track
|
2324
|
+
statuses.append(status)
|
2325
|
+
delay(0.005)
|
2059
2326
|
|
2060
2327
|
if exceed_loop:
|
2061
|
-
|
2062
|
-
f"
|
2328
|
+
error_msg: str = (
|
2329
|
+
f"Loop was exceed the maximum {self.max_loop} "
|
2330
|
+
f"loop{'s' if self.max_loop > 1 else ''}."
|
2063
2331
|
)
|
2064
|
-
|
2332
|
+
raise StageError(error_msg)
|
2333
|
+
return result.catch(status=validate_statuses(statuses))
|
2065
2334
|
|
2066
2335
|
|
2067
2336
|
class Match(BaseModel):
|
@@ -2145,11 +2414,6 @@ class CaseStage(BaseNestedStage):
|
|
2145
2414
|
if self.extras:
|
2146
2415
|
stage.extras = self.extras
|
2147
2416
|
|
2148
|
-
if stage.is_skipped(params=context):
|
2149
|
-
result.trace.info(f"[STAGE]: ... Skip stage: {stage.iden!r}")
|
2150
|
-
stage.set_outputs(output={"skipped": True}, to=output)
|
2151
|
-
continue
|
2152
|
-
|
2153
2417
|
if event and event.is_set():
|
2154
2418
|
error_msg: str = (
|
2155
2419
|
"Case-Stage was canceled from event that had set before "
|
@@ -2160,29 +2424,18 @@ class CaseStage(BaseNestedStage):
|
|
2160
2424
|
context={
|
2161
2425
|
"case": case,
|
2162
2426
|
"stages": filter_func(output.pop("stages", {})),
|
2163
|
-
"errors":
|
2427
|
+
"errors": StageError(error_msg).to_dict(),
|
2164
2428
|
},
|
2165
2429
|
)
|
2166
2430
|
|
2167
|
-
|
2168
|
-
|
2169
|
-
|
2170
|
-
|
2171
|
-
|
2172
|
-
|
2173
|
-
|
2174
|
-
|
2175
|
-
stage.set_outputs(rs.context, to=output)
|
2176
|
-
stage.set_outputs(stage.get_outputs(output), to=context)
|
2177
|
-
except StageException as e:
|
2178
|
-
return result.catch(
|
2179
|
-
status=FAILED,
|
2180
|
-
context={
|
2181
|
-
"case": case,
|
2182
|
-
"stages": filter_func(output.pop("stages", {})),
|
2183
|
-
"errors": e.to_dict(),
|
2184
|
-
},
|
2185
|
-
)
|
2431
|
+
rs: Result = stage.handler_execute(
|
2432
|
+
params=context,
|
2433
|
+
run_id=result.run_id,
|
2434
|
+
parent_run_id=result.parent_run_id,
|
2435
|
+
event=event,
|
2436
|
+
)
|
2437
|
+
stage.set_outputs(rs.context, to=output)
|
2438
|
+
stage.set_outputs(stage.get_outputs(output), to=context)
|
2186
2439
|
|
2187
2440
|
if rs.status == FAILED:
|
2188
2441
|
error_msg: str = (
|
@@ -2194,7 +2447,7 @@ class CaseStage(BaseNestedStage):
|
|
2194
2447
|
context={
|
2195
2448
|
"case": case,
|
2196
2449
|
"stages": filter_func(output.pop("stages", {})),
|
2197
|
-
"errors":
|
2450
|
+
"errors": StageError(error_msg).to_dict(),
|
2198
2451
|
},
|
2199
2452
|
)
|
2200
2453
|
return result.catch(
|
@@ -2228,7 +2481,7 @@ class CaseStage(BaseNestedStage):
|
|
2228
2481
|
|
2229
2482
|
_case: StrOrNone = param2template(self.case, params, extras=self.extras)
|
2230
2483
|
|
2231
|
-
result.trace.info(f"[STAGE]:
|
2484
|
+
result.trace.info(f"[STAGE]: Case: {_case!r}.")
|
2232
2485
|
_else: Optional[Match] = None
|
2233
2486
|
stages: Optional[list[Stage]] = None
|
2234
2487
|
for match in self.match:
|
@@ -2237,39 +2490,28 @@ class CaseStage(BaseNestedStage):
|
|
2237
2490
|
continue
|
2238
2491
|
|
2239
2492
|
_condition: str = param2template(c, params, extras=self.extras)
|
2240
|
-
if stages is None and _case == _condition:
|
2493
|
+
if stages is None and pass_env(_case) == pass_env(_condition):
|
2241
2494
|
stages: list[Stage] = match.stages
|
2242
2495
|
|
2243
2496
|
if stages is None:
|
2244
2497
|
if _else is None:
|
2245
2498
|
if not self.skip_not_match:
|
2246
|
-
raise
|
2499
|
+
raise StageError(
|
2247
2500
|
"This stage does not set else for support not match "
|
2248
2501
|
"any case."
|
2249
2502
|
)
|
2250
|
-
|
2251
|
-
"
|
2252
|
-
|
2253
|
-
error_msg: str = (
|
2254
|
-
"Case-Stage was canceled because it does not match any "
|
2255
|
-
"case and else condition does not set too."
|
2256
|
-
)
|
2257
|
-
return result.catch(
|
2258
|
-
status=CANCEL,
|
2259
|
-
context={"errors": StageException(error_msg).to_dict()},
|
2503
|
+
raise StageSkipError(
|
2504
|
+
"Execution was skipped because it does not match any "
|
2505
|
+
"case and the else condition does not set too."
|
2260
2506
|
)
|
2507
|
+
|
2261
2508
|
_case: str = "_"
|
2262
2509
|
stages: list[Stage] = _else.stages
|
2263
2510
|
|
2264
2511
|
if event and event.is_set():
|
2265
|
-
|
2266
|
-
|
2267
|
-
|
2268
|
-
"errors": StageException(
|
2269
|
-
"Stage was canceled from event that had set before "
|
2270
|
-
"case-stage execution."
|
2271
|
-
).to_dict()
|
2272
|
-
},
|
2512
|
+
raise StageCancelError(
|
2513
|
+
"Execution was canceled from the event before start "
|
2514
|
+
"case execution."
|
2273
2515
|
)
|
2274
2516
|
|
2275
2517
|
return self.execute_case(
|
@@ -2278,7 +2520,7 @@ class CaseStage(BaseNestedStage):
|
|
2278
2520
|
|
2279
2521
|
|
2280
2522
|
class RaiseStage(BaseAsyncStage):
|
2281
|
-
"""Raise error stage executor that raise `
|
2523
|
+
"""Raise error stage executor that raise `StageError` that use a message
|
2282
2524
|
field for making error message before raise.
|
2283
2525
|
|
2284
2526
|
Data Validate:
|
@@ -2291,7 +2533,7 @@ class RaiseStage(BaseAsyncStage):
|
|
2291
2533
|
|
2292
2534
|
message: str = Field(
|
2293
2535
|
description=(
|
2294
|
-
"An error message that want to raise with `
|
2536
|
+
"An error message that want to raise with `StageError` class"
|
2295
2537
|
),
|
2296
2538
|
alias="raise",
|
2297
2539
|
)
|
@@ -2303,7 +2545,7 @@ class RaiseStage(BaseAsyncStage):
|
|
2303
2545
|
result: Optional[Result] = None,
|
2304
2546
|
event: Optional[Event] = None,
|
2305
2547
|
) -> Result:
|
2306
|
-
"""Raise the
|
2548
|
+
"""Raise the StageError object with the message field execution.
|
2307
2549
|
|
2308
2550
|
:param params: (DictData) A parameter data.
|
2309
2551
|
:param result: (Result) A Result instance for return context and status.
|
@@ -2315,8 +2557,8 @@ class RaiseStage(BaseAsyncStage):
|
|
2315
2557
|
extras=self.extras,
|
2316
2558
|
)
|
2317
2559
|
message: str = param2template(self.message, params, extras=self.extras)
|
2318
|
-
result.trace.info(f"[STAGE]:
|
2319
|
-
raise
|
2560
|
+
result.trace.info(f"[STAGE]: Message: ( {message} )")
|
2561
|
+
raise StageError(message)
|
2320
2562
|
|
2321
2563
|
async def axecute(
|
2322
2564
|
self,
|
@@ -2343,7 +2585,7 @@ class RaiseStage(BaseAsyncStage):
|
|
2343
2585
|
)
|
2344
2586
|
message: str = param2template(self.message, params, extras=self.extras)
|
2345
2587
|
await result.trace.ainfo(f"[STAGE]: Execute Raise-Stage: ( {message} )")
|
2346
|
-
raise
|
2588
|
+
raise StageError(message)
|
2347
2589
|
|
2348
2590
|
|
2349
2591
|
class DockerStage(BaseStage): # pragma: no cov
|
@@ -2437,7 +2679,7 @@ class DockerStage(BaseStage): # pragma: no cov
|
|
2437
2679
|
)
|
2438
2680
|
return result.catch(
|
2439
2681
|
status=CANCEL,
|
2440
|
-
context={"errors":
|
2682
|
+
context={"errors": StageError(error_msg).to_dict()},
|
2441
2683
|
)
|
2442
2684
|
|
2443
2685
|
unique_image_name: str = f"{self.image}_{datetime.now():%Y%m%d%H%M%S%f}"
|
@@ -2507,9 +2749,7 @@ class DockerStage(BaseStage): # pragma: no cov
|
|
2507
2749
|
extras=self.extras,
|
2508
2750
|
)
|
2509
2751
|
|
2510
|
-
result.trace.info(
|
2511
|
-
f"[STAGE]: Execute Docker-Stage: {self.image}:{self.tag}"
|
2512
|
-
)
|
2752
|
+
result.trace.info(f"[STAGE]: Docker: {self.image}:{self.tag}")
|
2513
2753
|
raise NotImplementedError("Docker Stage does not implement yet.")
|
2514
2754
|
|
2515
2755
|
|
@@ -2608,8 +2848,6 @@ class VirtualPyStage(PyStage): # pragma: no cov
|
|
2608
2848
|
run_id=gen_id(self.name + (self.id or ""), unique=True),
|
2609
2849
|
extras=self.extras,
|
2610
2850
|
)
|
2611
|
-
|
2612
|
-
result.trace.info(f"[STAGE]: Execute VirtualPy-Stage: {self.name}")
|
2613
2851
|
run: str = param2template(dedent(self.run), params, extras=self.extras)
|
2614
2852
|
with self.create_py_file(
|
2615
2853
|
py=run,
|
@@ -2617,19 +2855,9 @@ class VirtualPyStage(PyStage): # pragma: no cov
|
|
2617
2855
|
deps=param2template(self.deps, params, extras=self.extras),
|
2618
2856
|
run_id=result.run_id,
|
2619
2857
|
) as py:
|
2620
|
-
result.trace.debug(f"[STAGE]:
|
2621
|
-
try:
|
2622
|
-
import uv
|
2623
|
-
|
2624
|
-
_ = uv
|
2625
|
-
except ImportError:
|
2626
|
-
raise ImportError(
|
2627
|
-
"The VirtualPyStage need you to install `uv` before"
|
2628
|
-
"execution."
|
2629
|
-
) from None
|
2630
|
-
|
2858
|
+
result.trace.debug(f"[STAGE]: Create `{py}` file.")
|
2631
2859
|
rs: CompletedProcess = subprocess.run(
|
2632
|
-
["uv", "run", py, "--no-cache"],
|
2860
|
+
["python", "-m", "uv", "run", py, "--no-cache"],
|
2633
2861
|
# ["uv", "run", "--python", "3.9", py],
|
2634
2862
|
shell=False,
|
2635
2863
|
capture_output=True,
|
@@ -2643,7 +2871,7 @@ class VirtualPyStage(PyStage): # pragma: no cov
|
|
2643
2871
|
if "\\x00" in rs.stderr
|
2644
2872
|
else rs.stderr
|
2645
2873
|
).removesuffix("\n")
|
2646
|
-
raise
|
2874
|
+
raise StageError(
|
2647
2875
|
f"Subprocess: {e}\nRunning Statement:\n---\n"
|
2648
2876
|
f"```python\n{run}\n```"
|
2649
2877
|
)
|