ddeutil-workflow 0.0.41__py3-none-any.whl → 0.0.43__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 +5 -1
- ddeutil/workflow/api/api.py +7 -7
- ddeutil/workflow/api/routes/schedules.py +5 -5
- ddeutil/workflow/api/routes/workflows.py +2 -2
- ddeutil/workflow/conf.py +39 -28
- ddeutil/workflow/cron.py +12 -13
- ddeutil/workflow/exceptions.py +13 -3
- ddeutil/workflow/job.py +40 -42
- ddeutil/workflow/logs.py +33 -6
- ddeutil/workflow/params.py +77 -18
- ddeutil/workflow/result.py +36 -8
- ddeutil/workflow/reusables.py +16 -13
- ddeutil/workflow/scheduler.py +32 -37
- ddeutil/workflow/stages.py +285 -120
- ddeutil/workflow/utils.py +0 -1
- ddeutil/workflow/workflow.py +127 -90
- {ddeutil_workflow-0.0.41.dist-info → ddeutil_workflow-0.0.43.dist-info}/METADATA +29 -32
- ddeutil_workflow-0.0.43.dist-info/RECORD +30 -0
- ddeutil/workflow/context.py +0 -61
- ddeutil_workflow-0.0.41.dist-info/RECORD +0 -31
- {ddeutil_workflow-0.0.41.dist-info → ddeutil_workflow-0.0.43.dist-info}/WHEEL +0 -0
- {ddeutil_workflow-0.0.41.dist-info → ddeutil_workflow-0.0.43.dist-info}/licenses/LICENSE +0 -0
- {ddeutil_workflow-0.0.41.dist-info → ddeutil_workflow-0.0.43.dist-info}/top_level.txt +0 -0
ddeutil/workflow/stages.py
CHANGED
@@ -3,19 +3,19 @@
|
|
3
3
|
# Licensed under the MIT License. See LICENSE in the project root for
|
4
4
|
# license information.
|
5
5
|
# ------------------------------------------------------------------------------
|
6
|
-
# [x] Use config
|
7
|
-
"""Stage
|
8
|
-
The stage handle the minimize task that run in some thread
|
9
|
-
its job owner) that mean it is the lowest executor of a workflow
|
10
|
-
tracking logs.
|
6
|
+
# [x] Use dynamic config
|
7
|
+
"""Stage model. It stores all stage model that use for getting stage data template
|
8
|
+
from the Job Model. The stage handle the minimize task that run in some thread
|
9
|
+
(same thread at its job owner) that mean it is the lowest executor of a workflow
|
10
|
+
that can tracking logs.
|
11
11
|
|
12
12
|
The output of stage execution only return 0 status because I do not want to
|
13
13
|
handle stage error on this stage model. I think stage model should have a lot of
|
14
14
|
use-case, and it does not worry when I want to create a new one.
|
15
15
|
|
16
|
-
Execution --> Ok --> Result with
|
16
|
+
Execution --> Ok --> Result with SUCCESS
|
17
17
|
|
18
|
-
--> Error ┬-> Result with
|
18
|
+
--> Error ┬-> Result with FAILED (if env var was set)
|
19
19
|
╰-> Raise StageException(...)
|
20
20
|
|
21
21
|
On the context I/O that pass to a stage object at execute process. The
|
@@ -50,9 +50,9 @@ from pydantic.functional_validators import model_validator
|
|
50
50
|
from typing_extensions import Self
|
51
51
|
|
52
52
|
from .__types import DictData, DictStr, TupleStr
|
53
|
-
from .conf import
|
53
|
+
from .conf import dynamic
|
54
54
|
from .exceptions import StageException, to_dict
|
55
|
-
from .result import Result, Status
|
55
|
+
from .result import FAILED, SUCCESS, Result, Status
|
56
56
|
from .reusables import TagFunc, extract_call, not_in_template, param2template
|
57
57
|
from .utils import (
|
58
58
|
gen_id,
|
@@ -67,6 +67,7 @@ __all__: TupleStr = (
|
|
67
67
|
"TriggerStage",
|
68
68
|
"ForEachStage",
|
69
69
|
"ParallelStage",
|
70
|
+
"RaiseStage",
|
70
71
|
"Stage",
|
71
72
|
)
|
72
73
|
|
@@ -79,6 +80,10 @@ class BaseStage(BaseModel, ABC):
|
|
79
80
|
This class is the abstraction class for any stage class.
|
80
81
|
"""
|
81
82
|
|
83
|
+
extras: DictData = Field(
|
84
|
+
default_factory=dict,
|
85
|
+
description="An extra override config values.",
|
86
|
+
)
|
82
87
|
id: Optional[str] = Field(
|
83
88
|
default=None,
|
84
89
|
description=(
|
@@ -94,10 +99,6 @@ class BaseStage(BaseModel, ABC):
|
|
94
99
|
description="A stage condition statement to allow stage executable.",
|
95
100
|
alias="if",
|
96
101
|
)
|
97
|
-
extras: DictData = Field(
|
98
|
-
default_factory=dict,
|
99
|
-
description="An extra override values.",
|
100
|
-
)
|
101
102
|
|
102
103
|
@property
|
103
104
|
def iden(self) -> str:
|
@@ -151,15 +152,6 @@ class BaseStage(BaseModel, ABC):
|
|
151
152
|
"""
|
152
153
|
raise NotImplementedError("Stage should implement `execute` method.")
|
153
154
|
|
154
|
-
async def axecute(
|
155
|
-
self,
|
156
|
-
params: DictData,
|
157
|
-
*,
|
158
|
-
result: Result | None = None,
|
159
|
-
event: Event | None,
|
160
|
-
) -> Result: # pragma: no cov
|
161
|
-
...
|
162
|
-
|
163
155
|
def handler_execute(
|
164
156
|
self,
|
165
157
|
params: DictData,
|
@@ -167,7 +159,7 @@ class BaseStage(BaseModel, ABC):
|
|
167
159
|
run_id: str | None = None,
|
168
160
|
parent_run_id: str | None = None,
|
169
161
|
result: Result | None = None,
|
170
|
-
raise_error: bool =
|
162
|
+
raise_error: bool | None = None,
|
171
163
|
to: DictData | None = None,
|
172
164
|
event: Event | None = None,
|
173
165
|
) -> Result:
|
@@ -178,12 +170,12 @@ class BaseStage(BaseModel, ABC):
|
|
178
170
|
specific environment variable,`WORKFLOW_CORE_STAGE_RAISE_ERROR`.
|
179
171
|
|
180
172
|
Execution --> Ok --> Result
|
181
|
-
|-status:
|
173
|
+
|-status: SUCCESS
|
182
174
|
╰-context:
|
183
175
|
╰-outputs: ...
|
184
176
|
|
185
177
|
--> Error --> Result (if env var was set)
|
186
|
-
|-status:
|
178
|
+
|-status: FAILED
|
187
179
|
╰-errors:
|
188
180
|
|-class: ...
|
189
181
|
|-name: ...
|
@@ -217,26 +209,24 @@ class BaseStage(BaseModel, ABC):
|
|
217
209
|
|
218
210
|
try:
|
219
211
|
rs: Result = self.execute(params, result=result, event=event)
|
220
|
-
if to is not None
|
221
|
-
|
222
|
-
|
223
|
-
except Exception as err:
|
224
|
-
result.trace.error(f"[STAGE]: {err.__class__.__name__}: {err}")
|
212
|
+
return self.set_outputs(rs.context, to=to) if to is not None else rs
|
213
|
+
except Exception as e:
|
214
|
+
result.trace.error(f"[STAGE]: {e.__class__.__name__}: {e}")
|
225
215
|
|
226
|
-
if raise_error
|
227
|
-
if isinstance(
|
216
|
+
if dynamic("stage_raise_error", f=raise_error, extras=self.extras):
|
217
|
+
if isinstance(e, StageException):
|
228
218
|
raise
|
229
219
|
|
230
220
|
raise StageException(
|
231
221
|
f"{self.__class__.__name__}: \n\t"
|
232
|
-
f"{
|
233
|
-
) from
|
222
|
+
f"{e.__class__.__name__}: {e}"
|
223
|
+
) from e
|
234
224
|
|
235
|
-
errors: DictData = {"errors": to_dict(
|
225
|
+
errors: DictData = {"errors": to_dict(e)}
|
236
226
|
if to is not None:
|
237
227
|
return self.set_outputs(errors, to=to)
|
238
228
|
|
239
|
-
return result.catch(status=
|
229
|
+
return result.catch(status=FAILED, context=errors)
|
240
230
|
|
241
231
|
def set_outputs(self, output: DictData, to: DictData) -> DictData:
|
242
232
|
"""Set an outputs from execution process to the received context. The
|
@@ -268,13 +258,17 @@ class BaseStage(BaseModel, ABC):
|
|
268
258
|
if "stages" not in to:
|
269
259
|
to["stages"] = {}
|
270
260
|
|
271
|
-
if self.id is None and not
|
261
|
+
if self.id is None and not dynamic(
|
262
|
+
"stage_default_id", extras=self.extras
|
263
|
+
):
|
272
264
|
return to
|
273
265
|
|
274
266
|
_id: str = (
|
275
|
-
param2template(self.id, params=to)
|
267
|
+
param2template(self.id, params=to, extras=self.extras)
|
276
268
|
if self.id
|
277
|
-
else gen_id(
|
269
|
+
else gen_id(
|
270
|
+
param2template(self.name, params=to, extras=self.extras)
|
271
|
+
)
|
278
272
|
)
|
279
273
|
|
280
274
|
errors: DictData = (
|
@@ -312,16 +306,111 @@ class BaseStage(BaseModel, ABC):
|
|
312
306
|
# should use the `re` module to validate eval-string before
|
313
307
|
# running.
|
314
308
|
rs: bool = eval(
|
315
|
-
param2template(self.condition, params
|
309
|
+
param2template(self.condition, params, extras=self.extras),
|
310
|
+
globals() | params,
|
311
|
+
{},
|
316
312
|
)
|
317
313
|
if not isinstance(rs, bool):
|
318
314
|
raise TypeError("Return type of condition does not be boolean")
|
319
315
|
return not rs
|
320
|
-
except Exception as
|
321
|
-
raise StageException(f"{
|
316
|
+
except Exception as e:
|
317
|
+
raise StageException(f"{e.__class__.__name__}: {e}") from e
|
318
|
+
|
319
|
+
|
320
|
+
class BaseAsyncStage(BaseStage):
|
321
|
+
|
322
|
+
@abstractmethod
|
323
|
+
def execute(
|
324
|
+
self,
|
325
|
+
params: DictData,
|
326
|
+
*,
|
327
|
+
result: Result | None = None,
|
328
|
+
event: Event | None = None,
|
329
|
+
) -> Result: ...
|
330
|
+
|
331
|
+
@abstractmethod
|
332
|
+
async def axecute(
|
333
|
+
self,
|
334
|
+
params: DictData,
|
335
|
+
*,
|
336
|
+
result: Result | None = None,
|
337
|
+
event: Event | None = None,
|
338
|
+
) -> Result:
|
339
|
+
"""Async execution method for this Empty stage that only logging out to
|
340
|
+
stdout.
|
341
|
+
|
342
|
+
:param params: (DictData) A context data that want to add output result.
|
343
|
+
But this stage does not pass any output.
|
344
|
+
:param result: (Result) A result object for keeping context and status
|
345
|
+
data.
|
346
|
+
:param event: (Event) An event manager that use to track parent execute
|
347
|
+
was not force stopped.
|
348
|
+
|
349
|
+
:rtype: Result
|
350
|
+
"""
|
351
|
+
raise NotImplementedError(
|
352
|
+
"Async Stage should implement `axecute` method."
|
353
|
+
)
|
354
|
+
|
355
|
+
async def handler_axecute(
|
356
|
+
self,
|
357
|
+
params: DictData,
|
358
|
+
*,
|
359
|
+
run_id: str | None = None,
|
360
|
+
parent_run_id: str | None = None,
|
361
|
+
result: Result | None = None,
|
362
|
+
raise_error: bool | None = None,
|
363
|
+
to: DictData | None = None,
|
364
|
+
event: Event | None = None,
|
365
|
+
) -> Result:
|
366
|
+
"""Async Handler stage execution result from the stage `execute` method.
|
367
|
+
|
368
|
+
:param params: (DictData) A parameterize value data that use in this
|
369
|
+
stage execution.
|
370
|
+
:param run_id: (str) A running stage ID for this execution.
|
371
|
+
:param parent_run_id: (str) A parent workflow running ID for this
|
372
|
+
execution.
|
373
|
+
:param result: (Result) A result object for keeping context and status
|
374
|
+
data before execution.
|
375
|
+
:param raise_error: (bool) A flag that all this method raise error
|
376
|
+
:param to: (DictData) A target object for auto set the return output
|
377
|
+
after execution.
|
378
|
+
:param event: (Event) An event manager that pass to the stage execution.
|
379
|
+
|
380
|
+
:rtype: Result
|
381
|
+
"""
|
382
|
+
result: Result = Result.construct_with_rs_or_id(
|
383
|
+
result,
|
384
|
+
run_id=run_id,
|
385
|
+
parent_run_id=parent_run_id,
|
386
|
+
id_logic=self.iden,
|
387
|
+
)
|
388
|
+
|
389
|
+
try:
|
390
|
+
rs: Result = await self.axecute(params, result=result, event=event)
|
391
|
+
if to is not None:
|
392
|
+
return self.set_outputs(rs.context, to=to)
|
393
|
+
return rs
|
394
|
+
except Exception as e:
|
395
|
+
await result.trace.aerror(f"[STAGE]: {e.__class__.__name__}: {e}")
|
396
|
+
|
397
|
+
if dynamic("stage_raise_error", f=raise_error, extras=self.extras):
|
398
|
+
if isinstance(e, StageException):
|
399
|
+
raise
|
400
|
+
|
401
|
+
raise StageException(
|
402
|
+
f"{self.__class__.__name__}: \n\t"
|
403
|
+
f"{e.__class__.__name__}: {e}"
|
404
|
+
) from None
|
405
|
+
|
406
|
+
errors: DictData = {"errors": to_dict(e)}
|
407
|
+
if to is not None:
|
408
|
+
return self.set_outputs(errors, to=to)
|
322
409
|
|
410
|
+
return result.catch(status=FAILED, context=errors)
|
323
411
|
|
324
|
-
|
412
|
+
|
413
|
+
class EmptyStage(BaseAsyncStage):
|
325
414
|
"""Empty stage that do nothing (context equal empty stage) and logging the
|
326
415
|
name of stage only to stdout.
|
327
416
|
|
@@ -372,23 +461,22 @@ class EmptyStage(BaseStage):
|
|
372
461
|
|
373
462
|
result.trace.info(
|
374
463
|
f"[STAGE]: Empty-Execute: {self.name!r}: "
|
375
|
-
f"( {param2template(self.echo, params=
|
464
|
+
f"( {param2template(self.echo, params, extras=self.extras) or '...'} )"
|
376
465
|
)
|
377
466
|
if self.sleep > 0:
|
378
467
|
if self.sleep > 5:
|
379
468
|
result.trace.info(f"[STAGE]: ... sleep ({self.sleep} seconds)")
|
380
469
|
time.sleep(self.sleep)
|
381
470
|
|
382
|
-
return result.catch(status=
|
471
|
+
return result.catch(status=SUCCESS)
|
383
472
|
|
384
|
-
# TODO: Draft async execute method for the perf improvement.
|
385
473
|
async def axecute(
|
386
474
|
self,
|
387
475
|
params: DictData,
|
388
476
|
*,
|
389
477
|
result: Result | None = None,
|
390
|
-
event: Event | None,
|
391
|
-
) -> Result:
|
478
|
+
event: Event | None = None,
|
479
|
+
) -> Result:
|
392
480
|
"""Async execution method for this Empty stage that only logging out to
|
393
481
|
stdout.
|
394
482
|
|
@@ -406,17 +494,19 @@ class EmptyStage(BaseStage):
|
|
406
494
|
run_id=gen_id(self.name + (self.id or ""), unique=True)
|
407
495
|
)
|
408
496
|
|
409
|
-
result.trace.
|
497
|
+
await result.trace.ainfo(
|
410
498
|
f"[STAGE]: Empty-Execute: {self.name!r}: "
|
411
|
-
f"( {param2template(self.echo, params=
|
499
|
+
f"( {param2template(self.echo, params, extras=self.extras) or '...'} )"
|
412
500
|
)
|
413
501
|
|
414
502
|
if self.sleep > 0:
|
415
503
|
if self.sleep > 5:
|
416
|
-
result.trace.
|
504
|
+
await result.trace.ainfo(
|
505
|
+
f"[STAGE]: ... sleep ({self.sleep} seconds)"
|
506
|
+
)
|
417
507
|
await asyncio.sleep(self.sleep)
|
418
508
|
|
419
|
-
return result.catch(status=
|
509
|
+
return result.catch(status=SUCCESS)
|
420
510
|
|
421
511
|
|
422
512
|
class BashStage(BaseStage):
|
@@ -508,12 +598,14 @@ class BashStage(BaseStage):
|
|
508
598
|
run_id=gen_id(self.name + (self.id or ""), unique=True)
|
509
599
|
)
|
510
600
|
|
511
|
-
bash: str = param2template(
|
601
|
+
bash: str = param2template(
|
602
|
+
dedent(self.bash), params, extras=self.extras
|
603
|
+
)
|
512
604
|
|
513
605
|
result.trace.info(f"[STAGE]: Shell-Execute: {self.name}")
|
514
606
|
with self.create_sh_file(
|
515
607
|
bash=bash,
|
516
|
-
env=param2template(self.env, params),
|
608
|
+
env=param2template(self.env, params, extras=self.extras),
|
517
609
|
run_id=result.run_id,
|
518
610
|
) as sh:
|
519
611
|
result.trace.debug(f"... Start create `{sh[1]}` file.")
|
@@ -523,17 +615,17 @@ class BashStage(BaseStage):
|
|
523
615
|
|
524
616
|
if rs.returncode > 0:
|
525
617
|
# NOTE: Prepare stderr message that returning from subprocess.
|
526
|
-
|
618
|
+
e: str = (
|
527
619
|
rs.stderr.encode("utf-8").decode("utf-16")
|
528
620
|
if "\\x00" in rs.stderr
|
529
621
|
else rs.stderr
|
530
622
|
).removesuffix("\n")
|
531
623
|
raise StageException(
|
532
|
-
f"Subprocess: {
|
624
|
+
f"Subprocess: {e}\nRunning Statement:\n---\n"
|
533
625
|
f"```bash\n{bash}\n```"
|
534
626
|
)
|
535
627
|
return result.catch(
|
536
|
-
status=
|
628
|
+
status=SUCCESS,
|
537
629
|
context={
|
538
630
|
"return_code": rs.returncode,
|
539
631
|
"stdout": None if (out := rs.stdout.strip("\n")) == "" else out,
|
@@ -635,7 +727,7 @@ class PyStage(BaseStage):
|
|
635
727
|
gb: DictData = (
|
636
728
|
globals()
|
637
729
|
| params
|
638
|
-
| param2template(self.vars, params)
|
730
|
+
| param2template(self.vars, params, extras=self.extras)
|
639
731
|
| {"result": result}
|
640
732
|
)
|
641
733
|
|
@@ -648,10 +740,12 @@ class PyStage(BaseStage):
|
|
648
740
|
|
649
741
|
# WARNING: The exec build-in function is very dangerous. So, it
|
650
742
|
# should use the re module to validate exec-string before running.
|
651
|
-
exec(
|
743
|
+
exec(
|
744
|
+
param2template(dedent(self.run), params, extras=self.extras), gb, lc
|
745
|
+
)
|
652
746
|
|
653
747
|
return result.catch(
|
654
|
-
status=
|
748
|
+
status=SUCCESS, context={"locals": lc, "globals": gb}
|
655
749
|
)
|
656
750
|
|
657
751
|
|
@@ -716,11 +810,16 @@ class CallStage(BaseStage):
|
|
716
810
|
run_id=gen_id(self.name + (self.id or ""), unique=True)
|
717
811
|
)
|
718
812
|
|
719
|
-
t_func: TagFunc = extract_call(
|
813
|
+
t_func: TagFunc = extract_call(
|
814
|
+
param2template(self.uses, params, extras=self.extras),
|
815
|
+
registries=self.extras.get("regis_call"),
|
816
|
+
)()
|
720
817
|
|
721
818
|
# VALIDATE: check input task caller parameters that exists before
|
722
819
|
# calling.
|
723
|
-
args: DictData = {"result": result} | param2template(
|
820
|
+
args: DictData = {"result": result} | param2template(
|
821
|
+
self.args, params, extras=self.extras
|
822
|
+
)
|
724
823
|
ips = inspect.signature(t_func)
|
725
824
|
necessary_params: list[str] = [
|
726
825
|
k
|
@@ -754,10 +853,12 @@ class CallStage(BaseStage):
|
|
754
853
|
if inspect.iscoroutinefunction(t_func): # pragma: no cov
|
755
854
|
loop = asyncio.get_event_loop()
|
756
855
|
rs: DictData = loop.run_until_complete(
|
757
|
-
t_func(**param2template(args, params))
|
856
|
+
t_func(**param2template(args, params, extras=self.extras))
|
758
857
|
)
|
759
858
|
else:
|
760
|
-
rs: DictData = t_func(
|
859
|
+
rs: DictData = t_func(
|
860
|
+
**param2template(args, params, extras=self.extras)
|
861
|
+
)
|
761
862
|
|
762
863
|
# VALIDATE:
|
763
864
|
# Check the result type from call function, it should be dict.
|
@@ -766,7 +867,7 @@ class CallStage(BaseStage):
|
|
766
867
|
f"Return type: '{t_func.name}@{t_func.tag}' does not serialize "
|
767
868
|
f"to result model, you change return type to `dict`."
|
768
869
|
)
|
769
|
-
return result.catch(status=
|
870
|
+
return result.catch(status=SUCCESS, context=rs)
|
770
871
|
|
771
872
|
|
772
873
|
class TriggerStage(BaseStage):
|
@@ -819,15 +920,16 @@ class TriggerStage(BaseStage):
|
|
819
920
|
)
|
820
921
|
|
821
922
|
# NOTE: Loading workflow object from trigger name.
|
822
|
-
_trigger: str = param2template(self.trigger, params=
|
923
|
+
_trigger: str = param2template(self.trigger, params, extras=self.extras)
|
823
924
|
|
824
925
|
# NOTE: Set running workflow ID from running stage ID to external
|
825
926
|
# params on Loader object.
|
826
|
-
workflow: Workflow = Workflow.
|
927
|
+
workflow: Workflow = Workflow.from_conf(_trigger, extras=self.extras)
|
827
928
|
result.trace.info(f"[STAGE]: Trigger-Execute: {_trigger!r}")
|
828
929
|
return workflow.execute(
|
829
|
-
params=param2template(self.params, params),
|
930
|
+
params=param2template(self.params, params, extras=self.extras),
|
830
931
|
result=result,
|
932
|
+
event=event,
|
831
933
|
)
|
832
934
|
|
833
935
|
|
@@ -893,19 +995,11 @@ class ParallelStage(BaseStage): # pragma: no cov
|
|
893
995
|
).context,
|
894
996
|
to=context,
|
895
997
|
)
|
896
|
-
except StageException as
|
998
|
+
except StageException as e: # pragma: no cov
|
897
999
|
result.trace.error(
|
898
|
-
f"[STAGE]: Catch:\n\t{
|
899
|
-
)
|
900
|
-
context.update(
|
901
|
-
{
|
902
|
-
"errors": {
|
903
|
-
"class": err,
|
904
|
-
"name": err.__class__.__name__,
|
905
|
-
"message": f"{err.__class__.__name__}: {err}",
|
906
|
-
},
|
907
|
-
},
|
1000
|
+
f"[STAGE]: Catch:\n\t{e.__class__.__name__}:" f"\n\t{e}"
|
908
1001
|
)
|
1002
|
+
context.update({"errors": e.to_dict()})
|
909
1003
|
return context
|
910
1004
|
|
911
1005
|
def execute(
|
@@ -931,8 +1025,11 @@ class ParallelStage(BaseStage): # pragma: no cov
|
|
931
1025
|
run_id=gen_id(self.name + (self.id or ""), unique=True)
|
932
1026
|
)
|
933
1027
|
|
1028
|
+
result.trace.info(
|
1029
|
+
f"[STAGE]: Parallel-Execute with {self.max_parallel_core} cores."
|
1030
|
+
)
|
934
1031
|
rs: DictData = {"parallel": {}}
|
935
|
-
status =
|
1032
|
+
status = SUCCESS
|
936
1033
|
with ThreadPoolExecutor(
|
937
1034
|
max_workers=self.max_parallel_core,
|
938
1035
|
thread_name_prefix="parallel_stage_exec_",
|
@@ -956,7 +1053,7 @@ class ParallelStage(BaseStage): # pragma: no cov
|
|
956
1053
|
rs["parallel"][context.pop("branch")] = context
|
957
1054
|
|
958
1055
|
if "errors" in context:
|
959
|
-
status =
|
1056
|
+
status = FAILED
|
960
1057
|
|
961
1058
|
return result.catch(status=status, context=rs)
|
962
1059
|
|
@@ -981,7 +1078,7 @@ class ForEachStage(BaseStage):
|
|
981
1078
|
... }
|
982
1079
|
"""
|
983
1080
|
|
984
|
-
foreach: Union[list[str], list[int]] = Field(
|
1081
|
+
foreach: Union[list[str], list[int], str] = Field(
|
985
1082
|
description=(
|
986
1083
|
"A items for passing to each stages via ${{ item }} template."
|
987
1084
|
),
|
@@ -1023,10 +1120,21 @@ class ForEachStage(BaseStage):
|
|
1023
1120
|
run_id=gen_id(self.name + (self.id or ""), unique=True)
|
1024
1121
|
)
|
1025
1122
|
|
1026
|
-
|
1027
|
-
|
1123
|
+
foreach: Union[list[str], list[int]] = (
|
1124
|
+
param2template(self.foreach, params, extras=self.extras)
|
1125
|
+
if isinstance(self.foreach, str)
|
1126
|
+
else self.foreach
|
1127
|
+
)
|
1128
|
+
if not isinstance(foreach, list):
|
1129
|
+
raise StageException(
|
1130
|
+
f"Foreach does not support foreach value: {foreach!r}"
|
1131
|
+
)
|
1132
|
+
|
1133
|
+
result.trace.info(f"[STAGE]: Foreach-Execute: {foreach!r}.")
|
1134
|
+
rs: DictData = {"items": foreach, "foreach": {}}
|
1135
|
+
status: Status = SUCCESS
|
1028
1136
|
# TODO: Implement concurrent more than 1.
|
1029
|
-
for item in
|
1137
|
+
for item in foreach:
|
1030
1138
|
result.trace.debug(f"[STAGE]: Execute foreach item: {item!r}")
|
1031
1139
|
params["item"] = item
|
1032
1140
|
context = {"stages": {}}
|
@@ -1041,21 +1149,12 @@ class ForEachStage(BaseStage):
|
|
1041
1149
|
).context,
|
1042
1150
|
to=context,
|
1043
1151
|
)
|
1044
|
-
except StageException as
|
1045
|
-
status =
|
1152
|
+
except StageException as e: # pragma: no cov
|
1153
|
+
status = FAILED
|
1046
1154
|
result.trace.error(
|
1047
|
-
f"[STAGE]: Catch:\n\t{
|
1048
|
-
f"\n\t{err}"
|
1049
|
-
)
|
1050
|
-
context.update(
|
1051
|
-
{
|
1052
|
-
"errors": {
|
1053
|
-
"class": err,
|
1054
|
-
"name": err.__class__.__name__,
|
1055
|
-
"message": f"{err.__class__.__name__}: {err}",
|
1056
|
-
},
|
1057
|
-
},
|
1155
|
+
f"[STAGE]: Catch:\n\t{e.__class__.__name__}:" f"\n\t{e}"
|
1058
1156
|
)
|
1157
|
+
context.update({"errors": e.to_dict()})
|
1059
1158
|
|
1060
1159
|
rs["foreach"][item] = context
|
1061
1160
|
|
@@ -1064,10 +1163,24 @@ class ForEachStage(BaseStage):
|
|
1064
1163
|
|
1065
1164
|
# TODO: Not implement this stages yet
|
1066
1165
|
class UntilStage(BaseStage): # pragma: no cov
|
1067
|
-
"""Until execution stage.
|
1166
|
+
"""Until execution stage.
|
1167
|
+
|
1168
|
+
Data Validate:
|
1169
|
+
>>> stage = {
|
1170
|
+
... "name": "Until stage execution",
|
1171
|
+
... "item": 1,
|
1172
|
+
... "until": "${{ item }} > 3"
|
1173
|
+
... "stages": [
|
1174
|
+
... {
|
1175
|
+
... "name": "Start increase item value.",
|
1176
|
+
... "run": "item = ${{ item }}\\nitem += 1\\n"
|
1177
|
+
... },
|
1178
|
+
... ],
|
1179
|
+
... }
|
1180
|
+
"""
|
1068
1181
|
|
1069
|
-
until: str = Field(description="A until condition.")
|
1070
1182
|
item: Union[str, int, bool] = Field(description="An initial value.")
|
1183
|
+
until: str = Field(description="A until condition.")
|
1071
1184
|
stages: list[Stage] = Field(
|
1072
1185
|
default_factory=list,
|
1073
1186
|
description=(
|
@@ -1090,8 +1203,14 @@ class UntilStage(BaseStage): # pragma: no cov
|
|
1090
1203
|
|
1091
1204
|
|
1092
1205
|
# TODO: Not implement this stages yet
|
1093
|
-
class
|
1094
|
-
|
1206
|
+
class Match(BaseModel):
|
1207
|
+
case: Union[str, int]
|
1208
|
+
stage: Stage
|
1209
|
+
|
1210
|
+
|
1211
|
+
# TODO: Not implement this stages yet
|
1212
|
+
class CaseStage(BaseStage): # pragma: no cov
|
1213
|
+
"""Case execution stage.
|
1095
1214
|
|
1096
1215
|
Data Validate:
|
1097
1216
|
>>> stage = {
|
@@ -1125,7 +1244,7 @@ class IfStage(BaseStage): # pragma: no cov
|
|
1125
1244
|
"""
|
1126
1245
|
|
1127
1246
|
case: str = Field(description="A case condition for routing.")
|
1128
|
-
match: list[
|
1247
|
+
match: list[Match]
|
1129
1248
|
|
1130
1249
|
def execute(
|
1131
1250
|
self,
|
@@ -1133,7 +1252,51 @@ class IfStage(BaseStage): # pragma: no cov
|
|
1133
1252
|
*,
|
1134
1253
|
result: Result | None = None,
|
1135
1254
|
event: Event | None = None,
|
1136
|
-
) -> Result:
|
1255
|
+
) -> Result:
|
1256
|
+
"""Execute case-match condition that pass to the case field.
|
1257
|
+
|
1258
|
+
:param params: A parameter that want to pass before run any statement.
|
1259
|
+
:param result: (Result) A result object for keeping context and status
|
1260
|
+
data.
|
1261
|
+
:param event: (Event) An event manager that use to track parent execute
|
1262
|
+
was not force stopped.
|
1263
|
+
|
1264
|
+
:rtype: Result
|
1265
|
+
"""
|
1266
|
+
if result is None: # pragma: no cov
|
1267
|
+
result: Result = Result(
|
1268
|
+
run_id=gen_id(self.name + (self.id or ""), unique=True)
|
1269
|
+
)
|
1270
|
+
status = SUCCESS
|
1271
|
+
_case = param2template(self.case, params, extras=self.extras)
|
1272
|
+
_else = None
|
1273
|
+
context = {}
|
1274
|
+
for match in self.match:
|
1275
|
+
if (c := match.case) != "_":
|
1276
|
+
_condition = param2template(c, params, extras=self.extras)
|
1277
|
+
else:
|
1278
|
+
_else = match
|
1279
|
+
continue
|
1280
|
+
|
1281
|
+
if match == _condition:
|
1282
|
+
stage: Stage = match.stage
|
1283
|
+
try:
|
1284
|
+
stage.set_outputs(
|
1285
|
+
stage.handler_execute(
|
1286
|
+
params=params,
|
1287
|
+
run_id=result.run_id,
|
1288
|
+
parent_run_id=result.parent_run_id,
|
1289
|
+
).context,
|
1290
|
+
to=context,
|
1291
|
+
)
|
1292
|
+
except StageException as e: # pragma: no cov
|
1293
|
+
status = FAILED
|
1294
|
+
result.trace.error(
|
1295
|
+
f"[STAGE]: Catch:\n\t{e.__class__.__name__}:" f"\n\t{e}"
|
1296
|
+
)
|
1297
|
+
context.update({"errors": e.to_dict()})
|
1298
|
+
|
1299
|
+
return result.catch(status=status, context=context)
|
1137
1300
|
|
1138
1301
|
|
1139
1302
|
class RaiseStage(BaseStage): # pragma: no cov
|
@@ -1165,13 +1328,14 @@ class RaiseStage(BaseStage): # pragma: no cov
|
|
1165
1328
|
result: Result = Result(
|
1166
1329
|
run_id=gen_id(self.name + (self.id or ""), unique=True)
|
1167
1330
|
)
|
1168
|
-
|
1169
|
-
result.trace.error(f"[STAGE]: ... raise ( {self.message} )")
|
1331
|
+
result.trace.info(f"[STAGE]: Raise-Execute: {self.message!r}.")
|
1170
1332
|
raise StageException(self.message)
|
1171
1333
|
|
1172
1334
|
|
1173
1335
|
# TODO: Not implement this stages yet
|
1174
1336
|
class HookStage(BaseStage): # pragma: no cov
|
1337
|
+
"""Hook stage execution."""
|
1338
|
+
|
1175
1339
|
hook: str
|
1176
1340
|
args: DictData
|
1177
1341
|
callback: str
|
@@ -1187,7 +1351,20 @@ class HookStage(BaseStage): # pragma: no cov
|
|
1187
1351
|
|
1188
1352
|
# TODO: Not implement this stages yet
|
1189
1353
|
class DockerStage(BaseStage): # pragma: no cov
|
1190
|
-
"""Docker container stage execution.
|
1354
|
+
"""Docker container stage execution.
|
1355
|
+
|
1356
|
+
Data Validate:
|
1357
|
+
>>> stage = {
|
1358
|
+
... "name": "Docker stage execution",
|
1359
|
+
... "image": "image-name.pkg.com",
|
1360
|
+
... "env": {
|
1361
|
+
... "ENV": "dev",
|
1362
|
+
... },
|
1363
|
+
... "volume": {
|
1364
|
+
... "secrets": "/secrets",
|
1365
|
+
... },
|
1366
|
+
... }
|
1367
|
+
"""
|
1191
1368
|
|
1192
1369
|
image: str = Field(
|
1193
1370
|
description="A Docker image url with tag that want to run.",
|
@@ -1224,18 +1401,6 @@ class VirtualPyStage(PyStage): # pragma: no cov
|
|
1224
1401
|
return super().execute(params, result=result)
|
1225
1402
|
|
1226
1403
|
|
1227
|
-
# TODO: Not implement this stages yet
|
1228
|
-
class SensorStage(BaseStage): # pragma: no cov
|
1229
|
-
|
1230
|
-
def execute(
|
1231
|
-
self,
|
1232
|
-
params: DictData,
|
1233
|
-
*,
|
1234
|
-
result: Result | None = None,
|
1235
|
-
event: Event | None = None,
|
1236
|
-
) -> Result: ...
|
1237
|
-
|
1238
|
-
|
1239
1404
|
# NOTE:
|
1240
1405
|
# An order of parsing stage model on the Job model with ``stages`` field.
|
1241
1406
|
# From the current build-in stages, they do not have stage that have the same
|
@@ -1243,7 +1408,6 @@ class SensorStage(BaseStage): # pragma: no cov
|
|
1243
1408
|
#
|
1244
1409
|
Stage = Annotated[
|
1245
1410
|
Union[
|
1246
|
-
EmptyStage,
|
1247
1411
|
BashStage,
|
1248
1412
|
CallStage,
|
1249
1413
|
TriggerStage,
|
@@ -1251,6 +1415,7 @@ Stage = Annotated[
|
|
1251
1415
|
ParallelStage,
|
1252
1416
|
PyStage,
|
1253
1417
|
RaiseStage,
|
1418
|
+
EmptyStage,
|
1254
1419
|
],
|
1255
1420
|
Field(union_mode="smart"),
|
1256
1421
|
]
|