ddeutil-workflow 0.0.40__py3-none-any.whl → 0.0.42__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 +21 -25
- ddeutil/workflow/api/api.py +8 -8
- ddeutil/workflow/api/routes/logs.py +1 -2
- ddeutil/workflow/api/routes/schedules.py +5 -5
- ddeutil/workflow/api/routes/workflows.py +3 -3
- ddeutil/workflow/conf.py +50 -29
- ddeutil/workflow/cron.py +12 -13
- ddeutil/workflow/job.py +68 -9
- ddeutil/workflow/logs.py +358 -69
- ddeutil/workflow/params.py +1 -0
- ddeutil/workflow/result.py +6 -15
- ddeutil/workflow/{templates.py → reusables.py} +199 -10
- ddeutil/workflow/scheduler.py +27 -29
- ddeutil/workflow/stages.py +423 -64
- ddeutil/workflow/utils.py +10 -0
- ddeutil/workflow/workflow.py +119 -74
- {ddeutil_workflow-0.0.40.dist-info → ddeutil_workflow-0.0.42.dist-info}/METADATA +12 -9
- ddeutil_workflow-0.0.42.dist-info/RECORD +30 -0
- ddeutil/workflow/audit.py +0 -257
- ddeutil/workflow/caller.py +0 -179
- ddeutil/workflow/context.py +0 -61
- ddeutil_workflow-0.0.40.dist-info/RECORD +0 -33
- {ddeutil_workflow-0.0.40.dist-info → ddeutil_workflow-0.0.42.dist-info}/WHEEL +0 -0
- {ddeutil_workflow-0.0.40.dist-info → ddeutil_workflow-0.0.42.dist-info}/licenses/LICENSE +0 -0
- {ddeutil_workflow-0.0.40.dist-info → ddeutil_workflow-0.0.42.dist-info}/top_level.txt +0 -0
ddeutil/workflow/stages.py
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
# Licensed under the MIT License. See LICENSE in the project root for
|
4
4
|
# license information.
|
5
5
|
# ------------------------------------------------------------------------------
|
6
|
+
# [x] Use dynamic config
|
6
7
|
"""Stage Model that use for getting stage data template from the Job Model.
|
7
8
|
The stage handle the minimize task that run in some thread (same thread at
|
8
9
|
its job owner) that mean it is the lowest executor of a workflow that can
|
@@ -41,6 +42,7 @@ from inspect import Parameter
|
|
41
42
|
from pathlib import Path
|
42
43
|
from subprocess import CompletedProcess
|
43
44
|
from textwrap import dedent
|
45
|
+
from threading import Event
|
44
46
|
from typing import Annotated, Optional, Union
|
45
47
|
|
46
48
|
from pydantic import BaseModel, Field
|
@@ -48,11 +50,10 @@ from pydantic.functional_validators import model_validator
|
|
48
50
|
from typing_extensions import Self
|
49
51
|
|
50
52
|
from .__types import DictData, DictStr, TupleStr
|
51
|
-
from .
|
52
|
-
from .conf import config
|
53
|
+
from .conf import dynamic
|
53
54
|
from .exceptions import StageException, to_dict
|
54
55
|
from .result import Result, Status
|
55
|
-
from .
|
56
|
+
from .reusables import TagFunc, extract_call, not_in_template, param2template
|
56
57
|
from .utils import (
|
57
58
|
gen_id,
|
58
59
|
make_exec,
|
@@ -66,6 +67,7 @@ __all__: TupleStr = (
|
|
66
67
|
"TriggerStage",
|
67
68
|
"ForEachStage",
|
68
69
|
"ParallelStage",
|
70
|
+
"RaiseStage",
|
69
71
|
"Stage",
|
70
72
|
)
|
71
73
|
|
@@ -93,6 +95,10 @@ class BaseStage(BaseModel, ABC):
|
|
93
95
|
description="A stage condition statement to allow stage executable.",
|
94
96
|
alias="if",
|
95
97
|
)
|
98
|
+
extras: DictData = Field(
|
99
|
+
default_factory=dict,
|
100
|
+
description="An extra override config values.",
|
101
|
+
)
|
96
102
|
|
97
103
|
@property
|
98
104
|
def iden(self) -> str:
|
@@ -126,7 +132,11 @@ class BaseStage(BaseModel, ABC):
|
|
126
132
|
|
127
133
|
@abstractmethod
|
128
134
|
def execute(
|
129
|
-
self,
|
135
|
+
self,
|
136
|
+
params: DictData,
|
137
|
+
*,
|
138
|
+
result: Result | None = None,
|
139
|
+
event: Event | None = None,
|
130
140
|
) -> Result:
|
131
141
|
"""Execute abstraction method that action something by sub-model class.
|
132
142
|
This is important method that make this class is able to be the stage.
|
@@ -135,6 +145,8 @@ class BaseStage(BaseModel, ABC):
|
|
135
145
|
execution.
|
136
146
|
:param result: (Result) A result object for keeping context and status
|
137
147
|
data.
|
148
|
+
:param event: (Event) An event manager that use to track parent execute
|
149
|
+
was not force stopped.
|
138
150
|
|
139
151
|
:rtype: Result
|
140
152
|
"""
|
@@ -147,8 +159,9 @@ class BaseStage(BaseModel, ABC):
|
|
147
159
|
run_id: str | None = None,
|
148
160
|
parent_run_id: str | None = None,
|
149
161
|
result: Result | None = None,
|
150
|
-
raise_error: bool =
|
162
|
+
raise_error: bool | None = None,
|
151
163
|
to: DictData | None = None,
|
164
|
+
event: Event | None = None,
|
152
165
|
) -> Result:
|
153
166
|
"""Handler stage execution result from the stage `execute` method.
|
154
167
|
|
@@ -183,6 +196,7 @@ class BaseStage(BaseModel, ABC):
|
|
183
196
|
:param raise_error: (bool) A flag that all this method raise error
|
184
197
|
:param to: (DictData) A target object for auto set the return output
|
185
198
|
after execution.
|
199
|
+
:param event: (Event) An event manager that pass to the stage execution.
|
186
200
|
|
187
201
|
:rtype: Result
|
188
202
|
"""
|
@@ -194,14 +208,14 @@ class BaseStage(BaseModel, ABC):
|
|
194
208
|
)
|
195
209
|
|
196
210
|
try:
|
197
|
-
rs: Result = self.execute(params, result=result)
|
211
|
+
rs: Result = self.execute(params, result=result, event=event)
|
198
212
|
if to is not None:
|
199
213
|
return self.set_outputs(rs.context, to=to)
|
200
214
|
return rs
|
201
215
|
except Exception as err:
|
202
216
|
result.trace.error(f"[STAGE]: {err.__class__.__name__}: {err}")
|
203
217
|
|
204
|
-
if raise_error
|
218
|
+
if dynamic("stage_raise_error", f=raise_error, extras=self.extras):
|
205
219
|
if isinstance(err, StageException):
|
206
220
|
raise
|
207
221
|
|
@@ -246,13 +260,17 @@ class BaseStage(BaseModel, ABC):
|
|
246
260
|
if "stages" not in to:
|
247
261
|
to["stages"] = {}
|
248
262
|
|
249
|
-
if self.id is None and not
|
263
|
+
if self.id is None and not dynamic(
|
264
|
+
"stage_default_id", extras=self.extras
|
265
|
+
):
|
250
266
|
return to
|
251
267
|
|
252
268
|
_id: str = (
|
253
|
-
param2template(self.id, params=to)
|
269
|
+
param2template(self.id, params=to, extras=self.extras)
|
254
270
|
if self.id
|
255
|
-
else gen_id(
|
271
|
+
else gen_id(
|
272
|
+
param2template(self.name, params=to, extras=self.extras)
|
273
|
+
)
|
256
274
|
)
|
257
275
|
|
258
276
|
errors: DictData = (
|
@@ -290,7 +308,9 @@ class BaseStage(BaseModel, ABC):
|
|
290
308
|
# should use the `re` module to validate eval-string before
|
291
309
|
# running.
|
292
310
|
rs: bool = eval(
|
293
|
-
param2template(self.condition, params
|
311
|
+
param2template(self.condition, params, extras=self.extras),
|
312
|
+
globals() | params,
|
313
|
+
{},
|
294
314
|
)
|
295
315
|
if not isinstance(rs, bool):
|
296
316
|
raise TypeError("Return type of condition does not be boolean")
|
@@ -299,7 +319,102 @@ class BaseStage(BaseModel, ABC):
|
|
299
319
|
raise StageException(f"{err.__class__.__name__}: {err}") from err
|
300
320
|
|
301
321
|
|
302
|
-
class
|
322
|
+
class BaseAsyncStage(BaseStage):
|
323
|
+
|
324
|
+
@abstractmethod
|
325
|
+
def execute(
|
326
|
+
self,
|
327
|
+
params: DictData,
|
328
|
+
*,
|
329
|
+
result: Result | None = None,
|
330
|
+
event: Event | None = None,
|
331
|
+
) -> Result: ...
|
332
|
+
|
333
|
+
@abstractmethod
|
334
|
+
async def axecute(
|
335
|
+
self,
|
336
|
+
params: DictData,
|
337
|
+
*,
|
338
|
+
result: Result | None = None,
|
339
|
+
event: Event | None = None,
|
340
|
+
) -> Result:
|
341
|
+
"""Async execution method for this Empty stage that only logging out to
|
342
|
+
stdout.
|
343
|
+
|
344
|
+
:param params: (DictData) A context data that want to add output result.
|
345
|
+
But this stage does not pass any output.
|
346
|
+
:param result: (Result) A result object for keeping context and status
|
347
|
+
data.
|
348
|
+
:param event: (Event) An event manager that use to track parent execute
|
349
|
+
was not force stopped.
|
350
|
+
|
351
|
+
:rtype: Result
|
352
|
+
"""
|
353
|
+
raise NotImplementedError(
|
354
|
+
"Async Stage should implement `axecute` method."
|
355
|
+
)
|
356
|
+
|
357
|
+
async def handler_axecute(
|
358
|
+
self,
|
359
|
+
params: DictData,
|
360
|
+
*,
|
361
|
+
run_id: str | None = None,
|
362
|
+
parent_run_id: str | None = None,
|
363
|
+
result: Result | None = None,
|
364
|
+
raise_error: bool | None = None,
|
365
|
+
to: DictData | None = None,
|
366
|
+
event: Event | None = None,
|
367
|
+
) -> Result:
|
368
|
+
"""Async Handler stage execution result from the stage `execute` method.
|
369
|
+
|
370
|
+
:param params: (DictData) A parameterize value data that use in this
|
371
|
+
stage execution.
|
372
|
+
:param run_id: (str) A running stage ID for this execution.
|
373
|
+
:param parent_run_id: (str) A parent workflow running ID for this
|
374
|
+
execution.
|
375
|
+
:param result: (Result) A result object for keeping context and status
|
376
|
+
data before execution.
|
377
|
+
:param raise_error: (bool) A flag that all this method raise error
|
378
|
+
:param to: (DictData) A target object for auto set the return output
|
379
|
+
after execution.
|
380
|
+
:param event: (Event) An event manager that pass to the stage execution.
|
381
|
+
|
382
|
+
:rtype: Result
|
383
|
+
"""
|
384
|
+
result: Result = Result.construct_with_rs_or_id(
|
385
|
+
result,
|
386
|
+
run_id=run_id,
|
387
|
+
parent_run_id=parent_run_id,
|
388
|
+
id_logic=self.iden,
|
389
|
+
)
|
390
|
+
|
391
|
+
try:
|
392
|
+
rs: Result = await self.axecute(params, result=result, event=event)
|
393
|
+
if to is not None:
|
394
|
+
return self.set_outputs(rs.context, to=to)
|
395
|
+
return rs
|
396
|
+
except Exception as err:
|
397
|
+
await result.trace.aerror(
|
398
|
+
f"[STAGE]: {err.__class__.__name__}: {err}"
|
399
|
+
)
|
400
|
+
|
401
|
+
if dynamic("stage_raise_error", f=raise_error, extras=self.extras):
|
402
|
+
if isinstance(err, StageException):
|
403
|
+
raise
|
404
|
+
|
405
|
+
raise StageException(
|
406
|
+
f"{self.__class__.__name__}: \n\t"
|
407
|
+
f"{err.__class__.__name__}: {err}"
|
408
|
+
) from None
|
409
|
+
|
410
|
+
errors: DictData = {"errors": to_dict(err)}
|
411
|
+
if to is not None:
|
412
|
+
return self.set_outputs(errors, to=to)
|
413
|
+
|
414
|
+
return result.catch(status=Status.FAILED, context=errors)
|
415
|
+
|
416
|
+
|
417
|
+
class EmptyStage(BaseAsyncStage):
|
303
418
|
"""Empty stage that do nothing (context equal empty stage) and logging the
|
304
419
|
name of stage only to stdout.
|
305
420
|
|
@@ -322,7 +437,11 @@ class EmptyStage(BaseStage):
|
|
322
437
|
)
|
323
438
|
|
324
439
|
def execute(
|
325
|
-
self,
|
440
|
+
self,
|
441
|
+
params: DictData,
|
442
|
+
*,
|
443
|
+
result: Result | None = None,
|
444
|
+
event: Event | None = None,
|
326
445
|
) -> Result:
|
327
446
|
"""Execution method for the Empty stage that do only logging out to
|
328
447
|
stdout. This method does not use the `handler_result` decorator because
|
@@ -335,6 +454,8 @@ class EmptyStage(BaseStage):
|
|
335
454
|
But this stage does not pass any output.
|
336
455
|
:param result: (Result) A result object for keeping context and status
|
337
456
|
data.
|
457
|
+
:param event: (Event) An event manager that use to track parent execute
|
458
|
+
was not force stopped.
|
338
459
|
|
339
460
|
:rtype: Result
|
340
461
|
"""
|
@@ -344,7 +465,7 @@ class EmptyStage(BaseStage):
|
|
344
465
|
|
345
466
|
result.trace.info(
|
346
467
|
f"[STAGE]: Empty-Execute: {self.name!r}: "
|
347
|
-
f"( {param2template(self.echo, params=
|
468
|
+
f"( {param2template(self.echo, params, extras=self.extras) or '...'} )"
|
348
469
|
)
|
349
470
|
if self.sleep > 0:
|
350
471
|
if self.sleep > 5:
|
@@ -353,21 +474,42 @@ class EmptyStage(BaseStage):
|
|
353
474
|
|
354
475
|
return result.catch(status=Status.SUCCESS)
|
355
476
|
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
477
|
+
async def axecute(
|
478
|
+
self,
|
479
|
+
params: DictData,
|
480
|
+
*,
|
481
|
+
result: Result | None = None,
|
482
|
+
event: Event | None = None,
|
483
|
+
) -> Result:
|
484
|
+
"""Async execution method for this Empty stage that only logging out to
|
485
|
+
stdout.
|
486
|
+
|
487
|
+
:param params: (DictData) A context data that want to add output result.
|
488
|
+
But this stage does not pass any output.
|
489
|
+
:param result: (Result) A result object for keeping context and status
|
490
|
+
data.
|
491
|
+
:param event: (Event) An event manager that use to track parent execute
|
492
|
+
was not force stopped.
|
493
|
+
|
494
|
+
:rtype: Result
|
495
|
+
"""
|
360
496
|
if result is None: # pragma: no cov
|
361
497
|
result: Result = Result(
|
362
498
|
run_id=gen_id(self.name + (self.id or ""), unique=True)
|
363
499
|
)
|
364
500
|
|
365
|
-
result.trace.
|
501
|
+
await result.trace.ainfo(
|
366
502
|
f"[STAGE]: Empty-Execute: {self.name!r}: "
|
367
|
-
f"( {param2template(self.echo, params=
|
503
|
+
f"( {param2template(self.echo, params, extras=self.extras) or '...'} )"
|
368
504
|
)
|
369
505
|
|
370
|
-
|
506
|
+
if self.sleep > 0:
|
507
|
+
if self.sleep > 5:
|
508
|
+
await result.trace.ainfo(
|
509
|
+
f"[STAGE]: ... sleep ({self.sleep} seconds)"
|
510
|
+
)
|
511
|
+
await asyncio.sleep(self.sleep)
|
512
|
+
|
371
513
|
return result.catch(status=Status.SUCCESS)
|
372
514
|
|
373
515
|
|
@@ -438,7 +580,11 @@ class BashStage(BaseStage):
|
|
438
580
|
Path(f"./{f_name}").unlink()
|
439
581
|
|
440
582
|
def execute(
|
441
|
-
self,
|
583
|
+
self,
|
584
|
+
params: DictData,
|
585
|
+
*,
|
586
|
+
result: Result | None = None,
|
587
|
+
event: Event | None = None,
|
442
588
|
) -> Result:
|
443
589
|
"""Execute the Bash statement with the Python build-in ``subprocess``
|
444
590
|
package.
|
@@ -446,6 +592,8 @@ class BashStage(BaseStage):
|
|
446
592
|
:param params: A parameter data that want to use in this execution.
|
447
593
|
:param result: (Result) A result object for keeping context and status
|
448
594
|
data.
|
595
|
+
:param event: (Event) An event manager that use to track parent execute
|
596
|
+
was not force stopped.
|
449
597
|
|
450
598
|
:rtype: Result
|
451
599
|
"""
|
@@ -454,12 +602,14 @@ class BashStage(BaseStage):
|
|
454
602
|
run_id=gen_id(self.name + (self.id or ""), unique=True)
|
455
603
|
)
|
456
604
|
|
457
|
-
bash: str = param2template(
|
605
|
+
bash: str = param2template(
|
606
|
+
dedent(self.bash), params, extras=self.extras
|
607
|
+
)
|
458
608
|
|
459
609
|
result.trace.info(f"[STAGE]: Shell-Execute: {self.name}")
|
460
610
|
with self.create_sh_file(
|
461
611
|
bash=bash,
|
462
|
-
env=param2template(self.env, params),
|
612
|
+
env=param2template(self.env, params, extras=self.extras),
|
463
613
|
run_id=result.run_id,
|
464
614
|
) as sh:
|
465
615
|
result.trace.debug(f"... Start create `{sh[1]}` file.")
|
@@ -555,7 +705,11 @@ class PyStage(BaseStage):
|
|
555
705
|
return to
|
556
706
|
|
557
707
|
def execute(
|
558
|
-
self,
|
708
|
+
self,
|
709
|
+
params: DictData,
|
710
|
+
*,
|
711
|
+
result: Result | None = None,
|
712
|
+
event: Event | None = None,
|
559
713
|
) -> Result:
|
560
714
|
"""Execute the Python statement that pass all globals and input params
|
561
715
|
to globals argument on ``exec`` build-in function.
|
@@ -563,6 +717,8 @@ class PyStage(BaseStage):
|
|
563
717
|
:param params: A parameter that want to pass before run any statement.
|
564
718
|
:param result: (Result) A result object for keeping context and status
|
565
719
|
data.
|
720
|
+
:param event: (Event) An event manager that use to track parent execute
|
721
|
+
was not force stopped.
|
566
722
|
|
567
723
|
:rtype: Result
|
568
724
|
"""
|
@@ -575,7 +731,7 @@ class PyStage(BaseStage):
|
|
575
731
|
gb: DictData = (
|
576
732
|
globals()
|
577
733
|
| params
|
578
|
-
| param2template(self.vars, params)
|
734
|
+
| param2template(self.vars, params, extras=self.extras)
|
579
735
|
| {"result": result}
|
580
736
|
)
|
581
737
|
|
@@ -588,7 +744,9 @@ class PyStage(BaseStage):
|
|
588
744
|
|
589
745
|
# WARNING: The exec build-in function is very dangerous. So, it
|
590
746
|
# should use the re module to validate exec-string before running.
|
591
|
-
exec(
|
747
|
+
exec(
|
748
|
+
param2template(dedent(self.run), params, extras=self.extras), gb, lc
|
749
|
+
)
|
592
750
|
|
593
751
|
return result.catch(
|
594
752
|
status=Status.SUCCESS, context={"locals": lc, "globals": gb}
|
@@ -629,7 +787,11 @@ class CallStage(BaseStage):
|
|
629
787
|
)
|
630
788
|
|
631
789
|
def execute(
|
632
|
-
self,
|
790
|
+
self,
|
791
|
+
params: DictData,
|
792
|
+
*,
|
793
|
+
result: Result | None = None,
|
794
|
+
event: Event | None = None,
|
633
795
|
) -> Result:
|
634
796
|
"""Execute the Call function that already in the call registry.
|
635
797
|
|
@@ -638,11 +800,12 @@ class CallStage(BaseStage):
|
|
638
800
|
:raise TypeError: When the return type of call function does not be
|
639
801
|
dict type.
|
640
802
|
|
641
|
-
:param params: A parameter that want to pass before run any
|
642
|
-
|
803
|
+
:param params: (DictData) A parameter that want to pass before run any
|
804
|
+
statement.
|
643
805
|
:param result: (Result) A result object for keeping context and status
|
644
806
|
data.
|
645
|
-
:
|
807
|
+
:param event: (Event) An event manager that use to track parent execute
|
808
|
+
was not force stopped.
|
646
809
|
|
647
810
|
:rtype: Result
|
648
811
|
"""
|
@@ -651,11 +814,16 @@ class CallStage(BaseStage):
|
|
651
814
|
run_id=gen_id(self.name + (self.id or ""), unique=True)
|
652
815
|
)
|
653
816
|
|
654
|
-
t_func: TagFunc = extract_call(
|
817
|
+
t_func: TagFunc = extract_call(
|
818
|
+
param2template(self.uses, params, extras=self.extras),
|
819
|
+
registries=self.extras.get("regis_call"),
|
820
|
+
)()
|
655
821
|
|
656
822
|
# VALIDATE: check input task caller parameters that exists before
|
657
823
|
# calling.
|
658
|
-
args: DictData = {"result": result} | param2template(
|
824
|
+
args: DictData = {"result": result} | param2template(
|
825
|
+
self.args, params, extras=self.extras
|
826
|
+
)
|
659
827
|
ips = inspect.signature(t_func)
|
660
828
|
necessary_params: list[str] = [
|
661
829
|
k
|
@@ -689,10 +857,12 @@ class CallStage(BaseStage):
|
|
689
857
|
if inspect.iscoroutinefunction(t_func): # pragma: no cov
|
690
858
|
loop = asyncio.get_event_loop()
|
691
859
|
rs: DictData = loop.run_until_complete(
|
692
|
-
t_func(**param2template(args, params))
|
860
|
+
t_func(**param2template(args, params, extras=self.extras))
|
693
861
|
)
|
694
862
|
else:
|
695
|
-
rs: DictData = t_func(
|
863
|
+
rs: DictData = t_func(
|
864
|
+
**param2template(args, params, extras=self.extras)
|
865
|
+
)
|
696
866
|
|
697
867
|
# VALIDATE:
|
698
868
|
# Check the result type from call function, it should be dict.
|
@@ -728,7 +898,11 @@ class TriggerStage(BaseStage):
|
|
728
898
|
)
|
729
899
|
|
730
900
|
def execute(
|
731
|
-
self,
|
901
|
+
self,
|
902
|
+
params: DictData,
|
903
|
+
*,
|
904
|
+
result: Result | None = None,
|
905
|
+
event: Event | None = None,
|
732
906
|
) -> Result:
|
733
907
|
"""Trigger another workflow execution. It will wait the trigger
|
734
908
|
workflow running complete before catching its result.
|
@@ -736,6 +910,8 @@ class TriggerStage(BaseStage):
|
|
736
910
|
:param params: A parameter data that want to use in this execution.
|
737
911
|
:param result: (Result) A result object for keeping context and status
|
738
912
|
data.
|
913
|
+
:param event: (Event) An event manager that use to track parent execute
|
914
|
+
was not force stopped.
|
739
915
|
|
740
916
|
:rtype: Result
|
741
917
|
"""
|
@@ -748,15 +924,16 @@ class TriggerStage(BaseStage):
|
|
748
924
|
)
|
749
925
|
|
750
926
|
# NOTE: Loading workflow object from trigger name.
|
751
|
-
_trigger: str = param2template(self.trigger, params=
|
927
|
+
_trigger: str = param2template(self.trigger, params, extras=self.extras)
|
752
928
|
|
753
929
|
# NOTE: Set running workflow ID from running stage ID to external
|
754
930
|
# params on Loader object.
|
755
|
-
workflow: Workflow = Workflow.
|
931
|
+
workflow: Workflow = Workflow.from_conf(_trigger, extras=self.extras)
|
756
932
|
result.trace.info(f"[STAGE]: Trigger-Execute: {_trigger!r}")
|
757
933
|
return workflow.execute(
|
758
|
-
params=param2template(self.params, params),
|
934
|
+
params=param2template(self.params, params, extras=self.extras),
|
759
935
|
result=result,
|
936
|
+
event=event,
|
760
937
|
)
|
761
938
|
|
762
939
|
|
@@ -838,7 +1015,11 @@ class ParallelStage(BaseStage): # pragma: no cov
|
|
838
1015
|
return context
|
839
1016
|
|
840
1017
|
def execute(
|
841
|
-
self,
|
1018
|
+
self,
|
1019
|
+
params: DictData,
|
1020
|
+
*,
|
1021
|
+
result: Result | None = None,
|
1022
|
+
event: Event | None = None,
|
842
1023
|
) -> Result:
|
843
1024
|
"""Execute the stages that parallel each branch via multi-threading mode
|
844
1025
|
or async mode by changing `async_mode` flag.
|
@@ -846,6 +1027,8 @@ class ParallelStage(BaseStage): # pragma: no cov
|
|
846
1027
|
:param params: A parameter that want to pass before run any statement.
|
847
1028
|
:param result: (Result) A result object for keeping context and status
|
848
1029
|
data.
|
1030
|
+
:param event: (Event) An event manager that use to track parent execute
|
1031
|
+
was not force stopped.
|
849
1032
|
|
850
1033
|
:rtype: Result
|
851
1034
|
"""
|
@@ -854,6 +1037,9 @@ class ParallelStage(BaseStage): # pragma: no cov
|
|
854
1037
|
run_id=gen_id(self.name + (self.id or ""), unique=True)
|
855
1038
|
)
|
856
1039
|
|
1040
|
+
result.trace.info(
|
1041
|
+
f"[STAGE]: Parallel-Execute with {self.max_parallel_core} cores."
|
1042
|
+
)
|
857
1043
|
rs: DictData = {"parallel": {}}
|
858
1044
|
status = Status.SUCCESS
|
859
1045
|
with ThreadPoolExecutor(
|
@@ -904,25 +1090,40 @@ class ForEachStage(BaseStage):
|
|
904
1090
|
... }
|
905
1091
|
"""
|
906
1092
|
|
907
|
-
foreach: Union[list[str], list[int]] = Field(
|
1093
|
+
foreach: Union[list[str], list[int], str] = Field(
|
908
1094
|
description=(
|
909
1095
|
"A items for passing to each stages via ${{ item }} template."
|
910
1096
|
),
|
911
1097
|
)
|
912
1098
|
stages: list[Stage] = Field(
|
1099
|
+
default_factory=list,
|
913
1100
|
description=(
|
914
1101
|
"A list of stage that will run with each item in the foreach field."
|
915
1102
|
),
|
916
1103
|
)
|
1104
|
+
concurrent: int = Field(
|
1105
|
+
default=1,
|
1106
|
+
gt=0,
|
1107
|
+
description=(
|
1108
|
+
"A concurrent value allow to run each item at the same time. It "
|
1109
|
+
"will be sequential mode if this value equal 1."
|
1110
|
+
),
|
1111
|
+
)
|
917
1112
|
|
918
1113
|
def execute(
|
919
|
-
self,
|
1114
|
+
self,
|
1115
|
+
params: DictData,
|
1116
|
+
*,
|
1117
|
+
result: Result | None = None,
|
1118
|
+
event: Event | None = None,
|
920
1119
|
) -> Result:
|
921
1120
|
"""Execute the stages that pass each item form the foreach field.
|
922
1121
|
|
923
1122
|
:param params: A parameter that want to pass before run any statement.
|
924
1123
|
:param result: (Result) A result object for keeping context and status
|
925
1124
|
data.
|
1125
|
+
:param event: (Event) An event manager that use to track parent execute
|
1126
|
+
was not force stopped.
|
926
1127
|
|
927
1128
|
:rtype: Result
|
928
1129
|
"""
|
@@ -931,9 +1132,21 @@ class ForEachStage(BaseStage):
|
|
931
1132
|
run_id=gen_id(self.name + (self.id or ""), unique=True)
|
932
1133
|
)
|
933
1134
|
|
934
|
-
|
1135
|
+
foreach: Union[list[str], list[int]] = (
|
1136
|
+
param2template(self.foreach, params, extras=self.extras)
|
1137
|
+
if isinstance(self.foreach, str)
|
1138
|
+
else self.foreach
|
1139
|
+
)
|
1140
|
+
if not isinstance(foreach, list):
|
1141
|
+
raise StageException(
|
1142
|
+
f"Foreach does not support foreach value: {foreach!r}"
|
1143
|
+
)
|
1144
|
+
|
1145
|
+
result.trace.info(f"[STAGE]: Foreach-Execute: {foreach!r}.")
|
1146
|
+
rs: DictData = {"items": foreach, "foreach": {}}
|
935
1147
|
status = Status.SUCCESS
|
936
|
-
|
1148
|
+
# TODO: Implement concurrent more than 1.
|
1149
|
+
for item in foreach:
|
937
1150
|
result.trace.debug(f"[STAGE]: Execute foreach item: {item!r}")
|
938
1151
|
params["item"] = item
|
939
1152
|
context = {"stages": {}}
|
@@ -970,8 +1183,55 @@ class ForEachStage(BaseStage):
|
|
970
1183
|
|
971
1184
|
|
972
1185
|
# TODO: Not implement this stages yet
|
973
|
-
class
|
974
|
-
"""
|
1186
|
+
class UntilStage(BaseStage): # pragma: no cov
|
1187
|
+
"""Until execution stage.
|
1188
|
+
|
1189
|
+
Data Validate:
|
1190
|
+
>>> stage = {
|
1191
|
+
... "name": "Until stage execution",
|
1192
|
+
... "item": 1,
|
1193
|
+
... "until": "${{ item }} > 3"
|
1194
|
+
... "stages": [
|
1195
|
+
... {
|
1196
|
+
... "name": "Start increase item value.",
|
1197
|
+
... "run": "item = ${{ item }}\\nitem += 1\\n"
|
1198
|
+
... },
|
1199
|
+
... ],
|
1200
|
+
... }
|
1201
|
+
"""
|
1202
|
+
|
1203
|
+
item: Union[str, int, bool] = Field(description="An initial value.")
|
1204
|
+
until: str = Field(description="A until condition.")
|
1205
|
+
stages: list[Stage] = Field(
|
1206
|
+
default_factory=list,
|
1207
|
+
description=(
|
1208
|
+
"A list of stage that will run with each item until condition "
|
1209
|
+
"correct."
|
1210
|
+
),
|
1211
|
+
)
|
1212
|
+
max_until_not_change: int = Field(
|
1213
|
+
default=3,
|
1214
|
+
description="The maximum value of loop if condition not change.",
|
1215
|
+
)
|
1216
|
+
|
1217
|
+
def execute(
|
1218
|
+
self,
|
1219
|
+
params: DictData,
|
1220
|
+
*,
|
1221
|
+
result: Result | None = None,
|
1222
|
+
event: Event | None = None,
|
1223
|
+
) -> Result: ...
|
1224
|
+
|
1225
|
+
|
1226
|
+
# TODO: Not implement this stages yet
|
1227
|
+
class Match(BaseModel):
|
1228
|
+
case: Union[str, int]
|
1229
|
+
stage: Stage
|
1230
|
+
|
1231
|
+
|
1232
|
+
# TODO: Not implement this stages yet
|
1233
|
+
class CaseStage(BaseStage): # pragma: no cov
|
1234
|
+
"""Case execution stage.
|
975
1235
|
|
976
1236
|
Data Validate:
|
977
1237
|
>>> stage = {
|
@@ -1005,47 +1265,150 @@ class IfStage(BaseStage): # pragma: no cov
|
|
1005
1265
|
"""
|
1006
1266
|
|
1007
1267
|
case: str = Field(description="A case condition for routing.")
|
1008
|
-
match: list[
|
1268
|
+
match: list[Match]
|
1009
1269
|
|
1010
1270
|
def execute(
|
1011
|
-
self,
|
1012
|
-
|
1271
|
+
self,
|
1272
|
+
params: DictData,
|
1273
|
+
*,
|
1274
|
+
result: Result | None = None,
|
1275
|
+
event: Event | None = None,
|
1276
|
+
) -> Result:
|
1277
|
+
"""Execute case-match condition that pass to the case field.
|
1278
|
+
|
1279
|
+
:param params: A parameter that want to pass before run any statement.
|
1280
|
+
:param result: (Result) A result object for keeping context and status
|
1281
|
+
data.
|
1282
|
+
:param event: (Event) An event manager that use to track parent execute
|
1283
|
+
was not force stopped.
|
1284
|
+
|
1285
|
+
:rtype: Result
|
1286
|
+
"""
|
1287
|
+
if result is None: # pragma: no cov
|
1288
|
+
result: Result = Result(
|
1289
|
+
run_id=gen_id(self.name + (self.id or ""), unique=True)
|
1290
|
+
)
|
1291
|
+
status = Status.SUCCESS
|
1292
|
+
_case = param2template(self.case, params, extras=self.extras)
|
1293
|
+
_else = None
|
1294
|
+
context = {}
|
1295
|
+
for match in self.match:
|
1296
|
+
if (c := match.case) != "_":
|
1297
|
+
_condition = param2template(c, params, extras=self.extras)
|
1298
|
+
else:
|
1299
|
+
_else = match
|
1300
|
+
continue
|
1301
|
+
|
1302
|
+
if match == _condition:
|
1303
|
+
stage: Stage = match.stage
|
1304
|
+
try:
|
1305
|
+
stage.set_outputs(
|
1306
|
+
stage.handler_execute(
|
1307
|
+
params=params,
|
1308
|
+
run_id=result.run_id,
|
1309
|
+
parent_run_id=result.parent_run_id,
|
1310
|
+
).context,
|
1311
|
+
to=context,
|
1312
|
+
)
|
1313
|
+
except StageException as err: # pragma: no cov
|
1314
|
+
status = Status.FAILED
|
1315
|
+
result.trace.error(
|
1316
|
+
f"[STAGE]: Catch:\n\t{err.__class__.__name__}:"
|
1317
|
+
f"\n\t{err}"
|
1318
|
+
)
|
1319
|
+
context.update(
|
1320
|
+
{
|
1321
|
+
"errors": {
|
1322
|
+
"class": err,
|
1323
|
+
"name": err.__class__.__name__,
|
1324
|
+
"message": f"{err.__class__.__name__}: {err}",
|
1325
|
+
},
|
1326
|
+
},
|
1327
|
+
)
|
1328
|
+
|
1329
|
+
return result.catch(status=status, context=context)
|
1013
1330
|
|
1014
1331
|
|
1015
1332
|
class RaiseStage(BaseStage): # pragma: no cov
|
1333
|
+
"""Raise error stage that raise StageException that use a message field for
|
1334
|
+
making error message before raise.
|
1335
|
+
|
1336
|
+
Data Validate:
|
1337
|
+
>>> stage = {
|
1338
|
+
... "name": "Raise stage",
|
1339
|
+
... "raise": "raise this stage",
|
1340
|
+
... }
|
1341
|
+
|
1342
|
+
"""
|
1343
|
+
|
1016
1344
|
message: str = Field(
|
1017
1345
|
description="An error message that want to raise",
|
1018
1346
|
alias="raise",
|
1019
1347
|
)
|
1020
1348
|
|
1021
1349
|
def execute(
|
1022
|
-
self,
|
1350
|
+
self,
|
1351
|
+
params: DictData,
|
1352
|
+
*,
|
1353
|
+
result: Result | None = None,
|
1354
|
+
event: Event | None = None,
|
1023
1355
|
) -> Result:
|
1356
|
+
"""Raise the stage."""
|
1357
|
+
if result is None: # pragma: no cov
|
1358
|
+
result: Result = Result(
|
1359
|
+
run_id=gen_id(self.name + (self.id or ""), unique=True)
|
1360
|
+
)
|
1361
|
+
result.trace.info(f"[STAGE]: Raise-Execute: {self.message!r}.")
|
1024
1362
|
raise StageException(self.message)
|
1025
1363
|
|
1026
1364
|
|
1027
1365
|
# TODO: Not implement this stages yet
|
1028
1366
|
class HookStage(BaseStage): # pragma: no cov
|
1367
|
+
"""Hook stage execution."""
|
1368
|
+
|
1029
1369
|
hook: str
|
1030
1370
|
args: DictData
|
1031
1371
|
callback: str
|
1032
1372
|
|
1033
1373
|
def execute(
|
1034
|
-
self,
|
1374
|
+
self,
|
1375
|
+
params: DictData,
|
1376
|
+
*,
|
1377
|
+
result: Result | None = None,
|
1378
|
+
event: Event | None = None,
|
1035
1379
|
) -> Result: ...
|
1036
1380
|
|
1037
1381
|
|
1038
1382
|
# TODO: Not implement this stages yet
|
1039
1383
|
class DockerStage(BaseStage): # pragma: no cov
|
1040
|
-
"""Docker container stage execution.
|
1384
|
+
"""Docker container stage execution.
|
1385
|
+
|
1386
|
+
Data Validate:
|
1387
|
+
>>> stage = {
|
1388
|
+
... "name": "Docker stage execution",
|
1389
|
+
... "image": "image-name.pkg.com",
|
1390
|
+
... "env": {
|
1391
|
+
... "ENV": "dev",
|
1392
|
+
... },
|
1393
|
+
... "volume": {
|
1394
|
+
... "secrets": "/secrets",
|
1395
|
+
... },
|
1396
|
+
... }
|
1397
|
+
"""
|
1041
1398
|
|
1042
|
-
image: str
|
1399
|
+
image: str = Field(
|
1400
|
+
description="A Docker image url with tag that want to run.",
|
1401
|
+
)
|
1043
1402
|
env: DictData = Field(default_factory=dict)
|
1044
1403
|
volume: DictData = Field(default_factory=dict)
|
1045
1404
|
auth: DictData = Field(default_factory=dict)
|
1046
1405
|
|
1047
1406
|
def execute(
|
1048
|
-
self,
|
1407
|
+
self,
|
1408
|
+
params: DictData,
|
1409
|
+
*,
|
1410
|
+
result: Result | None = None,
|
1411
|
+
event: Event | None = None,
|
1049
1412
|
) -> Result: ...
|
1050
1413
|
|
1051
1414
|
|
@@ -1059,19 +1422,15 @@ class VirtualPyStage(PyStage): # pragma: no cov
|
|
1059
1422
|
def create_py_file(self, py: str, run_id: str | None): ...
|
1060
1423
|
|
1061
1424
|
def execute(
|
1062
|
-
self,
|
1425
|
+
self,
|
1426
|
+
params: DictData,
|
1427
|
+
*,
|
1428
|
+
result: Result | None = None,
|
1429
|
+
event: Event | None = None,
|
1063
1430
|
) -> Result:
|
1064
1431
|
return super().execute(params, result=result)
|
1065
1432
|
|
1066
1433
|
|
1067
|
-
# TODO: Not implement this stages yet
|
1068
|
-
class SensorStage(BaseStage): # pragma: no cov
|
1069
|
-
|
1070
|
-
def execute(
|
1071
|
-
self, params: DictData, *, result: Result | None = None
|
1072
|
-
) -> Result: ...
|
1073
|
-
|
1074
|
-
|
1075
1434
|
# NOTE:
|
1076
1435
|
# An order of parsing stage model on the Job model with ``stages`` field.
|
1077
1436
|
# From the current build-in stages, they do not have stage that have the same
|
@@ -1079,7 +1438,6 @@ class SensorStage(BaseStage): # pragma: no cov
|
|
1079
1438
|
#
|
1080
1439
|
Stage = Annotated[
|
1081
1440
|
Union[
|
1082
|
-
EmptyStage,
|
1083
1441
|
BashStage,
|
1084
1442
|
CallStage,
|
1085
1443
|
TriggerStage,
|
@@ -1087,6 +1445,7 @@ Stage = Annotated[
|
|
1087
1445
|
ParallelStage,
|
1088
1446
|
PyStage,
|
1089
1447
|
RaiseStage,
|
1448
|
+
EmptyStage,
|
1090
1449
|
],
|
1091
1450
|
Field(union_mode="smart"),
|
1092
1451
|
]
|