ddeutil-workflow 0.0.83__tar.gz → 0.0.84__tar.gz
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-0.0.83 → ddeutil_workflow-0.0.84}/PKG-INFO +1 -1
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/src/ddeutil/workflow/__about__.py +1 -1
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/src/ddeutil/workflow/stages.py +335 -55
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/src/ddeutil/workflow/utils.py +14 -3
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/src/ddeutil/workflow/workflow.py +45 -36
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/src/ddeutil_workflow.egg-info/PKG-INFO +1 -1
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/tests/test_workflow.py +4 -15
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/tests/test_workflow_release.py +41 -0
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/LICENSE +0 -0
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/README.md +0 -0
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/pyproject.toml +0 -0
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/setup.cfg +0 -0
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/src/ddeutil/workflow/__cron.py +0 -0
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/src/ddeutil/workflow/__init__.py +0 -0
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/src/ddeutil/workflow/__main__.py +0 -0
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/src/ddeutil/workflow/__types.py +0 -0
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/src/ddeutil/workflow/api/__init__.py +0 -0
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/src/ddeutil/workflow/api/log_conf.py +0 -0
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/src/ddeutil/workflow/api/routes/__init__.py +0 -0
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/src/ddeutil/workflow/api/routes/job.py +0 -0
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/src/ddeutil/workflow/api/routes/logs.py +0 -0
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/src/ddeutil/workflow/api/routes/workflows.py +0 -0
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/src/ddeutil/workflow/audits.py +0 -0
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/src/ddeutil/workflow/conf.py +0 -0
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/src/ddeutil/workflow/errors.py +0 -0
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/src/ddeutil/workflow/event.py +0 -0
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/src/ddeutil/workflow/job.py +0 -0
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/src/ddeutil/workflow/params.py +0 -0
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/src/ddeutil/workflow/plugins/__init__.py +0 -0
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/src/ddeutil/workflow/plugins/providers/__init__.py +0 -0
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/src/ddeutil/workflow/plugins/providers/aws.py +0 -0
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/src/ddeutil/workflow/plugins/providers/az.py +0 -0
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/src/ddeutil/workflow/plugins/providers/container.py +0 -0
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/src/ddeutil/workflow/plugins/providers/gcs.py +0 -0
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/src/ddeutil/workflow/result.py +0 -0
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/src/ddeutil/workflow/reusables.py +0 -0
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/src/ddeutil/workflow/traces.py +0 -0
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/src/ddeutil_workflow.egg-info/SOURCES.txt +0 -0
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/src/ddeutil_workflow.egg-info/dependency_links.txt +0 -0
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/src/ddeutil_workflow.egg-info/entry_points.txt +0 -0
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/src/ddeutil_workflow.egg-info/requires.txt +0 -0
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/src/ddeutil_workflow.egg-info/top_level.txt +0 -0
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/tests/test__cron.py +0 -0
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/tests/test__regex.py +0 -0
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/tests/test_audits.py +0 -0
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/tests/test_cli.py +0 -0
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/tests/test_conf.py +0 -0
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/tests/test_errors.py +0 -0
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/tests/test_event.py +0 -0
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/tests/test_job.py +0 -0
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/tests/test_job_exec.py +0 -0
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/tests/test_job_exec_strategy.py +0 -0
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/tests/test_params.py +0 -0
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/tests/test_result.py +0 -0
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/tests/test_reusables_call_tag.py +0 -0
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/tests/test_reusables_func_model.py +0 -0
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/tests/test_reusables_template.py +0 -0
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/tests/test_reusables_template_filter.py +0 -0
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/tests/test_strategy.py +0 -0
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/tests/test_traces.py +0 -0
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/tests/test_utils.py +0 -0
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/tests/test_workflow_exec.py +0 -0
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/tests/test_workflow_exec_job.py +0 -0
- {ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/tests/test_workflow_rerun.py +0 -0
@@ -1,2 +1,2 @@
|
|
1
|
-
__version__: str = "0.0.
|
1
|
+
__version__: str = "0.0.84"
|
2
2
|
__python_version__: str = "3.9"
|
@@ -206,13 +206,13 @@ class BaseStage(BaseModel, ABC):
|
|
206
206
|
return self.id or self.name
|
207
207
|
|
208
208
|
@field_validator("desc", mode="after")
|
209
|
-
def ___prepare_desc__(cls, value: str) -> str:
|
209
|
+
def ___prepare_desc__(cls, value: Optional[str]) -> Optional[str]:
|
210
210
|
"""Prepare description string that was created on a template.
|
211
211
|
|
212
212
|
Returns:
|
213
213
|
str: A dedent and left strip newline of description string.
|
214
214
|
"""
|
215
|
-
return dedent(value.lstrip("\n"))
|
215
|
+
return value if value is None else dedent(value.lstrip("\n"))
|
216
216
|
|
217
217
|
@model_validator(mode="after")
|
218
218
|
def __prepare_running_id(self) -> Self:
|
@@ -552,7 +552,7 @@ class BaseStage(BaseModel, ABC):
|
|
552
552
|
# should use the `re` module to validate eval-string before
|
553
553
|
# running.
|
554
554
|
rs: bool = eval(
|
555
|
-
|
555
|
+
self.pass_template(self.condition, params),
|
556
556
|
globals() | params,
|
557
557
|
{},
|
558
558
|
)
|
@@ -583,7 +583,8 @@ class BaseStage(BaseModel, ABC):
|
|
583
583
|
def is_nested(self) -> bool:
|
584
584
|
"""Return true if this stage is nested stage.
|
585
585
|
|
586
|
-
:
|
586
|
+
Returns:
|
587
|
+
bool: True if this stage is nested stage.
|
587
588
|
"""
|
588
589
|
return False
|
589
590
|
|
@@ -593,14 +594,46 @@ class BaseStage(BaseModel, ABC):
|
|
593
594
|
Returns:
|
594
595
|
DictData: A dict that was dumped from this model with alias mode.
|
595
596
|
"""
|
596
|
-
return self.model_dump(
|
597
|
+
return self.model_dump(
|
598
|
+
by_alias=True,
|
599
|
+
exclude_defaults=True,
|
600
|
+
exclude={"extras", "id", "name", "desc"},
|
601
|
+
)
|
597
602
|
|
598
|
-
def md(self) -> str: # pragma: no cov
|
603
|
+
def md(self, level: int = 1) -> str: # pragma: no cov
|
599
604
|
"""Return generated document that will be the interface of this stage.
|
600
605
|
|
601
|
-
:
|
606
|
+
Args:
|
607
|
+
level (int, default 0): A header level that want to generate
|
608
|
+
markdown content.
|
609
|
+
|
610
|
+
Returns:
|
611
|
+
str
|
602
612
|
"""
|
603
|
-
|
613
|
+
assert level >= 1, "Header level should gather than 0"
|
614
|
+
|
615
|
+
def align_newline(value: Optional[str]) -> str:
|
616
|
+
space: str = " " * 16
|
617
|
+
if value is None:
|
618
|
+
return ""
|
619
|
+
return value.rstrip("\n").replace("\n", f"\n{space}")
|
620
|
+
|
621
|
+
header: str = "#" * level
|
622
|
+
return dedent(
|
623
|
+
f"""
|
624
|
+
{header} Stage: {self.iden}\n
|
625
|
+
{align_newline(self.desc)}\n
|
626
|
+
#{header} Parameters\n
|
627
|
+
| name | type | default | description |
|
628
|
+
| --- | --- | --- | : --- : |\n\n
|
629
|
+
#{header} Details\n
|
630
|
+
```json
|
631
|
+
{self.detail()}
|
632
|
+
```
|
633
|
+
""".lstrip(
|
634
|
+
"\n"
|
635
|
+
)
|
636
|
+
)
|
604
637
|
|
605
638
|
def dryrun(
|
606
639
|
self,
|
@@ -610,26 +643,73 @@ class BaseStage(BaseModel, ABC):
|
|
610
643
|
*,
|
611
644
|
parent_run_id: Optional[str] = None,
|
612
645
|
event: Optional[Event] = None,
|
613
|
-
) -> Optional[Result]:
|
646
|
+
) -> Optional[Result]:
|
614
647
|
"""Pre-process method that will use to run with dry-run mode, and it
|
615
|
-
should be used
|
648
|
+
should be used replace of process method when workflow release set with
|
649
|
+
DRYRUN mode.
|
650
|
+
|
651
|
+
By default, this method will set logic to convert this stage model
|
652
|
+
to am EmptyStage if it is action stage before use process method
|
653
|
+
instead process itself.
|
654
|
+
|
655
|
+
Args:
|
656
|
+
params (DictData): A parameter data that want to use in this
|
657
|
+
execution.
|
658
|
+
run_id (str): A running stage ID.
|
659
|
+
context (DictData): A context data.
|
660
|
+
parent_run_id (str, default None): A parent running ID.
|
661
|
+
event (Event, default None): An event manager that use to track
|
662
|
+
parent process was not force stopped.
|
663
|
+
|
664
|
+
Returns:
|
665
|
+
Result: The execution result with status and context data.
|
616
666
|
"""
|
667
|
+
trace: Trace = get_trace(
|
668
|
+
run_id, parent_run_id=parent_run_id, extras=self.extras
|
669
|
+
)
|
670
|
+
trace.debug("[STAGE]: Start Dryrun ...")
|
671
|
+
if self.action_stage:
|
672
|
+
return self.to_empty().process(
|
673
|
+
params,
|
674
|
+
run_id,
|
675
|
+
context,
|
676
|
+
parent_run_id=parent_run_id,
|
677
|
+
event=event,
|
678
|
+
)
|
679
|
+
return self.process(
|
680
|
+
params, run_id, context, parent_run_id=parent_run_id, event=event
|
681
|
+
)
|
617
682
|
|
618
|
-
def to_empty(
|
683
|
+
def to_empty(
|
684
|
+
self,
|
685
|
+
sleep: int = 0.35,
|
686
|
+
*,
|
687
|
+
message: Optional[str] = None,
|
688
|
+
) -> EmptyStage:
|
619
689
|
"""Convert the current Stage model to the EmptyStage model for dry-run
|
620
690
|
mode if the `action_stage` class attribute has set.
|
621
691
|
|
692
|
+
Args:
|
693
|
+
sleep (int, default 0.35): An adjustment sleep time.
|
694
|
+
message (str, default None): A message that want to override default
|
695
|
+
message on EmptyStage model.
|
696
|
+
|
622
697
|
Returns:
|
623
698
|
EmptyStage: An EmptyStage model that passing itself model data to
|
624
699
|
message.
|
625
700
|
"""
|
701
|
+
if isinstance(self, EmptyStage):
|
702
|
+
return self.model_copy(update={"sleep": sleep})
|
626
703
|
return EmptyStage.model_validate(
|
627
704
|
{
|
628
705
|
"name": self.name,
|
629
706
|
"id": self.id,
|
630
707
|
"desc": self.desc,
|
631
708
|
"if": self.condition,
|
632
|
-
"echo":
|
709
|
+
"echo": (
|
710
|
+
message
|
711
|
+
or f"Convert from {self.__class__.__name__} to EmptyStage"
|
712
|
+
),
|
633
713
|
"sleep": sleep,
|
634
714
|
}
|
635
715
|
)
|
@@ -794,12 +874,14 @@ class BaseAsyncStage(BaseStage, ABC):
|
|
794
874
|
) -> Result:
|
795
875
|
"""Wrapped the axecute method before returning to handler axecute.
|
796
876
|
|
797
|
-
:
|
798
|
-
|
799
|
-
|
800
|
-
|
877
|
+
Args:
|
878
|
+
params: (DictData) A parameter data that want to use in this
|
879
|
+
execution.
|
880
|
+
event: (Event) An event manager that use to track parent execute
|
881
|
+
was not force stopped.
|
801
882
|
|
802
|
-
:
|
883
|
+
Returns:
|
884
|
+
Result: A Result object.
|
803
885
|
"""
|
804
886
|
catch(context, status=WAIT)
|
805
887
|
return await self.async_process(
|
@@ -820,7 +902,10 @@ class BaseRetryStage(BaseAsyncStage, ABC): # pragma: no cov
|
|
820
902
|
default=0,
|
821
903
|
ge=0,
|
822
904
|
lt=20,
|
823
|
-
description=
|
905
|
+
description=(
|
906
|
+
"A retry number if stage process got the error exclude skip and "
|
907
|
+
"cancel exception class."
|
908
|
+
),
|
824
909
|
)
|
825
910
|
|
826
911
|
def _execute(
|
@@ -834,12 +919,14 @@ class BaseRetryStage(BaseAsyncStage, ABC): # pragma: no cov
|
|
834
919
|
"""Wrapped the execute method with retry strategy before returning to
|
835
920
|
handler execute.
|
836
921
|
|
837
|
-
:
|
838
|
-
|
839
|
-
|
840
|
-
|
922
|
+
Args:
|
923
|
+
params: (DictData) A parameter data that want to use in this
|
924
|
+
execution.
|
925
|
+
event: (Event) An event manager that use to track parent execute
|
926
|
+
was not force stopped.
|
841
927
|
|
842
|
-
:
|
928
|
+
Returns:
|
929
|
+
Result: A Result object.
|
843
930
|
"""
|
844
931
|
current_retry: int = 0
|
845
932
|
exception: Exception
|
@@ -847,9 +934,19 @@ class BaseRetryStage(BaseAsyncStage, ABC): # pragma: no cov
|
|
847
934
|
trace: Trace = get_trace(
|
848
935
|
run_id, parent_run_id=parent_run_id, extras=self.extras
|
849
936
|
)
|
850
|
-
|
851
937
|
# NOTE: First execution for not pass to retry step if it passes.
|
852
938
|
try:
|
939
|
+
if (
|
940
|
+
self.extras.get("__sys_release_dryrun_mode", False)
|
941
|
+
and self.action_stage
|
942
|
+
):
|
943
|
+
return self.dryrun(
|
944
|
+
params | {"retry": current_retry},
|
945
|
+
run_id=run_id,
|
946
|
+
context=context,
|
947
|
+
parent_run_id=parent_run_id,
|
948
|
+
event=event,
|
949
|
+
)
|
853
950
|
return self.process(
|
854
951
|
params | {"retry": current_retry},
|
855
952
|
run_id=run_id,
|
@@ -876,6 +973,17 @@ class BaseRetryStage(BaseAsyncStage, ABC): # pragma: no cov
|
|
876
973
|
status=WAIT,
|
877
974
|
updated={"retry": current_retry},
|
878
975
|
)
|
976
|
+
if (
|
977
|
+
self.extras.get("__sys_release_dryrun_mode", False)
|
978
|
+
and self.action_stage
|
979
|
+
):
|
980
|
+
return self.dryrun(
|
981
|
+
params | {"retry": current_retry},
|
982
|
+
run_id=run_id,
|
983
|
+
context=context,
|
984
|
+
parent_run_id=parent_run_id,
|
985
|
+
event=event,
|
986
|
+
)
|
879
987
|
return self.process(
|
880
988
|
params | {"retry": current_retry},
|
881
989
|
run_id=run_id,
|
@@ -884,10 +992,10 @@ class BaseRetryStage(BaseAsyncStage, ABC): # pragma: no cov
|
|
884
992
|
event=event,
|
885
993
|
)
|
886
994
|
except (
|
887
|
-
StageSkipError,
|
888
995
|
StageNestedSkipError,
|
889
|
-
StageCancelError,
|
890
996
|
StageNestedCancelError,
|
997
|
+
StageSkipError,
|
998
|
+
StageCancelError,
|
891
999
|
):
|
892
1000
|
trace.debug("[STAGE]: process raise skip or cancel error.")
|
893
1001
|
raise
|
@@ -916,12 +1024,14 @@ class BaseRetryStage(BaseAsyncStage, ABC): # pragma: no cov
|
|
916
1024
|
"""Wrapped the axecute method with retry strategy before returning to
|
917
1025
|
handler axecute.
|
918
1026
|
|
919
|
-
:
|
920
|
-
|
921
|
-
|
922
|
-
|
1027
|
+
Args:
|
1028
|
+
params: (DictData) A parameter data that want to use in this
|
1029
|
+
execution.
|
1030
|
+
event: (Event) An event manager that use to track parent execute
|
1031
|
+
was not force stopped.
|
923
1032
|
|
924
|
-
:
|
1033
|
+
Returns:
|
1034
|
+
Result: A Result object.
|
925
1035
|
"""
|
926
1036
|
current_retry: int = 0
|
927
1037
|
exception: Exception
|
@@ -932,6 +1042,17 @@ class BaseRetryStage(BaseAsyncStage, ABC): # pragma: no cov
|
|
932
1042
|
|
933
1043
|
# NOTE: First execution for not pass to retry step if it passes.
|
934
1044
|
try:
|
1045
|
+
if (
|
1046
|
+
self.extras.get("__sys_release_dryrun_mode", False)
|
1047
|
+
and self.action_stage
|
1048
|
+
):
|
1049
|
+
return self.dryrun(
|
1050
|
+
params | {"retry": current_retry},
|
1051
|
+
run_id=run_id,
|
1052
|
+
context=context,
|
1053
|
+
parent_run_id=parent_run_id,
|
1054
|
+
event=event,
|
1055
|
+
)
|
935
1056
|
return await self.async_process(
|
936
1057
|
params | {"retry": current_retry},
|
937
1058
|
run_id=run_id,
|
@@ -958,6 +1079,17 @@ class BaseRetryStage(BaseAsyncStage, ABC): # pragma: no cov
|
|
958
1079
|
status=WAIT,
|
959
1080
|
updated={"retry": current_retry},
|
960
1081
|
)
|
1082
|
+
if (
|
1083
|
+
self.extras.get("__sys_release_dryrun_mode", False)
|
1084
|
+
and self.action_stage
|
1085
|
+
):
|
1086
|
+
return self.dryrun(
|
1087
|
+
params | {"retry": current_retry},
|
1088
|
+
run_id=run_id,
|
1089
|
+
context=context,
|
1090
|
+
parent_run_id=parent_run_id,
|
1091
|
+
event=event,
|
1092
|
+
)
|
961
1093
|
return await self.async_process(
|
962
1094
|
params | {"retry": current_retry},
|
963
1095
|
run_id=run_id,
|
@@ -966,10 +1098,10 @@ class BaseRetryStage(BaseAsyncStage, ABC): # pragma: no cov
|
|
966
1098
|
event=event,
|
967
1099
|
)
|
968
1100
|
except (
|
969
|
-
StageSkipError,
|
970
1101
|
StageNestedSkipError,
|
971
|
-
StageCancelError,
|
972
1102
|
StageNestedCancelError,
|
1103
|
+
StageSkipError,
|
1104
|
+
StageCancelError,
|
973
1105
|
):
|
974
1106
|
await trace.adebug(
|
975
1107
|
"[STAGE]: process raise skip or cancel error."
|
@@ -1165,6 +1297,7 @@ class BashStage(BaseRetryStage):
|
|
1165
1297
|
... }
|
1166
1298
|
"""
|
1167
1299
|
|
1300
|
+
action_stage: ClassVar[bool] = True
|
1168
1301
|
bash: str = Field(
|
1169
1302
|
description=(
|
1170
1303
|
"A bash statement that want to execute via Python subprocess."
|
@@ -1409,6 +1542,7 @@ class PyStage(BaseRetryStage):
|
|
1409
1542
|
... }
|
1410
1543
|
"""
|
1411
1544
|
|
1545
|
+
action_stage: ClassVar[bool] = True
|
1412
1546
|
run: str = Field(
|
1413
1547
|
description="A Python string statement that want to run with `exec`.",
|
1414
1548
|
)
|
@@ -1647,6 +1781,7 @@ class CallStage(BaseRetryStage):
|
|
1647
1781
|
... }
|
1648
1782
|
"""
|
1649
1783
|
|
1784
|
+
action_stage: ClassVar[bool] = True
|
1650
1785
|
uses: str = Field(
|
1651
1786
|
description=(
|
1652
1787
|
"A caller function with registry importer syntax that use to load "
|
@@ -1728,7 +1863,7 @@ class CallStage(BaseRetryStage):
|
|
1728
1863
|
extras=self.extras,
|
1729
1864
|
),
|
1730
1865
|
"extras": self.extras,
|
1731
|
-
} |
|
1866
|
+
} | self.pass_template(self.args, params)
|
1732
1867
|
sig = inspect.signature(call_func)
|
1733
1868
|
necessary_params: list[str] = []
|
1734
1869
|
has_keyword: bool = False
|
@@ -1743,6 +1878,7 @@ class CallStage(BaseRetryStage):
|
|
1743
1878
|
elif v.kind == Parameter.VAR_KEYWORD:
|
1744
1879
|
has_keyword = True
|
1745
1880
|
|
1881
|
+
# NOTE: Validate private parameter should exist in the args field.
|
1746
1882
|
if any(
|
1747
1883
|
(k.removeprefix("_") not in args and k not in args)
|
1748
1884
|
for k in necessary_params
|
@@ -1760,11 +1896,12 @@ class CallStage(BaseRetryStage):
|
|
1760
1896
|
f"does not set to args. It already set {list(args.keys())}."
|
1761
1897
|
)
|
1762
1898
|
|
1763
|
-
if
|
1764
|
-
|
1899
|
+
if not has_keyword:
|
1900
|
+
if "result" not in sig.parameters:
|
1901
|
+
args.pop("result")
|
1765
1902
|
|
1766
|
-
|
1767
|
-
|
1903
|
+
if "extras" not in sig.parameters: # pragma: no cov
|
1904
|
+
args.pop("extras")
|
1768
1905
|
|
1769
1906
|
if event and event.is_set():
|
1770
1907
|
raise StageCancelError("Cancel before start call process.")
|
@@ -1847,7 +1984,7 @@ class CallStage(BaseRetryStage):
|
|
1847
1984
|
extras=self.extras,
|
1848
1985
|
),
|
1849
1986
|
"extras": self.extras,
|
1850
|
-
} |
|
1987
|
+
} | self.pass_template(self.args, params)
|
1851
1988
|
sig = inspect.signature(call_func)
|
1852
1989
|
necessary_params: list[str] = []
|
1853
1990
|
has_keyword: bool = False
|
@@ -1878,11 +2015,13 @@ class CallStage(BaseRetryStage):
|
|
1878
2015
|
f"Necessary params, ({', '.join(necessary_params)}, ), "
|
1879
2016
|
f"does not set to args. It already set {list(args.keys())}."
|
1880
2017
|
)
|
1881
|
-
if "result" not in sig.parameters and not has_keyword:
|
1882
|
-
args.pop("result")
|
1883
2018
|
|
1884
|
-
if
|
1885
|
-
|
2019
|
+
if not has_keyword:
|
2020
|
+
if "result" not in sig.parameters:
|
2021
|
+
args.pop("result")
|
2022
|
+
|
2023
|
+
if "extras" not in sig.parameters: # pragma: no cov
|
2024
|
+
args.pop("extras")
|
1886
2025
|
|
1887
2026
|
if event and event.is_set():
|
1888
2027
|
raise StageCancelError("Cancel before start call process.")
|
@@ -1932,11 +2071,14 @@ class CallStage(BaseRetryStage):
|
|
1932
2071
|
"""Validate an input arguments before passing to the caller function.
|
1933
2072
|
|
1934
2073
|
Args:
|
1935
|
-
func
|
1936
|
-
args
|
1937
|
-
run_id: A running
|
2074
|
+
func (TagFunc): A tag function object that want to get typing.
|
2075
|
+
args (DictData): An arguments before passing to this tag func.
|
2076
|
+
run_id (str): A running ID.
|
2077
|
+
parent_run_id (str, default None): A parent running ID.
|
2078
|
+
extras (DictData, default None): An extra parameters.
|
1938
2079
|
|
1939
|
-
:
|
2080
|
+
Returns:
|
2081
|
+
DictData: A prepared args paramter that validate with model args.
|
1940
2082
|
"""
|
1941
2083
|
try:
|
1942
2084
|
override: DictData = dict(
|
@@ -1969,8 +2111,87 @@ class CallStage(BaseRetryStage):
|
|
1969
2111
|
)
|
1970
2112
|
return args
|
1971
2113
|
|
2114
|
+
def dryrun(
|
2115
|
+
self,
|
2116
|
+
params: DictData,
|
2117
|
+
run_id: str,
|
2118
|
+
context: DictData,
|
2119
|
+
*,
|
2120
|
+
parent_run_id: Optional[str] = None,
|
2121
|
+
event: Optional[Event] = None,
|
2122
|
+
) -> Optional[Result]: # pragma: no cov
|
2123
|
+
"""Override the dryrun method for this CallStage.
|
1972
2124
|
|
1973
|
-
|
2125
|
+
Steps:
|
2126
|
+
- Pre-hook caller function that exist.
|
2127
|
+
- Show function parameters
|
2128
|
+
"""
|
2129
|
+
trace: Trace = get_trace(
|
2130
|
+
run_id, parent_run_id=parent_run_id, extras=self.extras
|
2131
|
+
)
|
2132
|
+
call_func: TagFunc = self.get_caller(params=params)()
|
2133
|
+
trace.info(f"[STAGE]: Caller Func: '{call_func.name}@{call_func.tag}'")
|
2134
|
+
|
2135
|
+
args: DictData = {
|
2136
|
+
"result": Result(
|
2137
|
+
run_id=run_id,
|
2138
|
+
parent_run_id=parent_run_id,
|
2139
|
+
status=WAIT,
|
2140
|
+
context=context,
|
2141
|
+
extras=self.extras,
|
2142
|
+
),
|
2143
|
+
"extras": self.extras,
|
2144
|
+
} | self.pass_template(self.args, params)
|
2145
|
+
sig = inspect.signature(call_func)
|
2146
|
+
trace.debug(f"[STAGE]: {sig.parameters}")
|
2147
|
+
necessary_params: list[str] = []
|
2148
|
+
has_keyword: bool = False
|
2149
|
+
for k in sig.parameters:
|
2150
|
+
if (
|
2151
|
+
v := sig.parameters[k]
|
2152
|
+
).default == Parameter.empty and v.kind not in (
|
2153
|
+
Parameter.VAR_KEYWORD,
|
2154
|
+
Parameter.VAR_POSITIONAL,
|
2155
|
+
):
|
2156
|
+
necessary_params.append(k)
|
2157
|
+
elif v.kind == Parameter.VAR_KEYWORD:
|
2158
|
+
has_keyword = True
|
2159
|
+
|
2160
|
+
func_typed: dict[str, Any] = get_type_hints(call_func)
|
2161
|
+
map_type: str = "||".join(
|
2162
|
+
f"\t{p}: {func_typed[p]}"
|
2163
|
+
for p in necessary_params
|
2164
|
+
if p in func_typed
|
2165
|
+
)
|
2166
|
+
map_type_args: str = "||".join(f"\t{a}: {type(a)}" for a in args)
|
2167
|
+
if not has_keyword:
|
2168
|
+
if "result" not in sig.parameters:
|
2169
|
+
args.pop("result")
|
2170
|
+
|
2171
|
+
if "extras" not in sig.parameters:
|
2172
|
+
args.pop("extras")
|
2173
|
+
|
2174
|
+
trace.debug(
|
2175
|
+
f"[STAGE]: Details"
|
2176
|
+
f"||Necessary Params:"
|
2177
|
+
f"||{map_type}"
|
2178
|
+
f"||Return Type: {func_typed['return']}"
|
2179
|
+
f"||Argument Params:"
|
2180
|
+
f"||{map_type_args}"
|
2181
|
+
f"||"
|
2182
|
+
)
|
2183
|
+
if has_keyword:
|
2184
|
+
trace.debug("[STAGE]: This caller function support keyword param.")
|
2185
|
+
return Result(
|
2186
|
+
run_id=run_id,
|
2187
|
+
parent_run_id=parent_run_id,
|
2188
|
+
status=SUCCESS,
|
2189
|
+
context=catch(context=context, status=SUCCESS),
|
2190
|
+
extras=self.extras,
|
2191
|
+
)
|
2192
|
+
|
2193
|
+
|
2194
|
+
class BaseNestedStage(BaseAsyncStage, ABC):
|
1974
2195
|
"""Base Nested Stage model. This model is use for checking the child stage
|
1975
2196
|
is the nested stage or not.
|
1976
2197
|
"""
|
@@ -2035,7 +2256,7 @@ class BaseNestedStage(BaseRetryStage, ABC):
|
|
2035
2256
|
)
|
2036
2257
|
|
2037
2258
|
|
2038
|
-
class TriggerStage(
|
2259
|
+
class TriggerStage(BaseRetryStage):
|
2039
2260
|
"""Trigger workflow executor stage that run an input trigger Workflow
|
2040
2261
|
execute method. This is the stage that allow you to create the reusable
|
2041
2262
|
Workflow template with dynamic parameters.
|
@@ -2093,7 +2314,7 @@ class TriggerStage(BaseNestedStage):
|
|
2093
2314
|
run_id, parent_run_id=parent_run_id, extras=self.extras
|
2094
2315
|
)
|
2095
2316
|
_trigger: str = param2template(self.trigger, params, extras=self.extras)
|
2096
|
-
if _trigger == self.extras.get("
|
2317
|
+
if _trigger == self.extras.get("__sys_exec_break_circle", "NOTSET"):
|
2097
2318
|
raise StageError("Circle execute via trigger itself workflow name.")
|
2098
2319
|
trace.info(f"[NESTED]: Load Workflow Config: {_trigger!r}")
|
2099
2320
|
result: Result = Workflow.from_conf(
|
@@ -2142,6 +2363,33 @@ class TriggerStage(BaseNestedStage):
|
|
2142
2363
|
)
|
2143
2364
|
return result
|
2144
2365
|
|
2366
|
+
async def async_process(
|
2367
|
+
self,
|
2368
|
+
params: DictData,
|
2369
|
+
run_id: str,
|
2370
|
+
context: DictData,
|
2371
|
+
*,
|
2372
|
+
parent_run_id: Optional[str] = None,
|
2373
|
+
event: Optional[Event] = None,
|
2374
|
+
) -> Result: # pragma: no cov
|
2375
|
+
"""Async process for nested-stage do not implement yet.
|
2376
|
+
|
2377
|
+
Args:
|
2378
|
+
params: A parameter data that want to use in this
|
2379
|
+
execution.
|
2380
|
+
run_id: A running stage ID.
|
2381
|
+
context: A context data.
|
2382
|
+
parent_run_id: A parent running ID. (Default is None)
|
2383
|
+
event: An event manager that use to track parent process
|
2384
|
+
was not force stopped.
|
2385
|
+
|
2386
|
+
Returns:
|
2387
|
+
Result: The execution result with status and context data.
|
2388
|
+
"""
|
2389
|
+
raise NotImplementedError(
|
2390
|
+
"The Trigger stage does not implement the `axecute` method yet."
|
2391
|
+
)
|
2392
|
+
|
2145
2393
|
|
2146
2394
|
class ParallelContext(TypedDict):
|
2147
2395
|
branch: str
|
@@ -2776,7 +3024,7 @@ class UntilStage(BaseNestedStage):
|
|
2776
3024
|
),
|
2777
3025
|
)
|
2778
3026
|
until: str = Field(description="A until condition for stop the while loop.")
|
2779
|
-
stages: list[
|
3027
|
+
stages: list[SubStage] = Field(
|
2780
3028
|
default_factory=list,
|
2781
3029
|
description=(
|
2782
3030
|
"A list of stage that will run with each item in until loop."
|
@@ -3414,7 +3662,7 @@ class RaiseStage(BaseAsyncStage):
|
|
3414
3662
|
raise StageError(message)
|
3415
3663
|
|
3416
3664
|
|
3417
|
-
class DockerStage(
|
3665
|
+
class DockerStage(BaseRetryStage): # pragma: no cov
|
3418
3666
|
"""Docker container stage execution that will pull the specific Docker image
|
3419
3667
|
with custom authentication and run this image by passing environment
|
3420
3668
|
variables and mounting local volume to this Docker container.
|
@@ -3437,6 +3685,7 @@ class DockerStage(BaseStage): # pragma: no cov
|
|
3437
3685
|
... }
|
3438
3686
|
"""
|
3439
3687
|
|
3688
|
+
action_stage: ClassVar[bool] = True
|
3440
3689
|
image: str = Field(
|
3441
3690
|
description="A Docker image url with tag that want to run.",
|
3442
3691
|
)
|
@@ -3593,6 +3842,33 @@ class DockerStage(BaseStage): # pragma: no cov
|
|
3593
3842
|
trace.info(f"[STAGE]: Docker: {self.image}:{self.tag}")
|
3594
3843
|
raise NotImplementedError("Docker Stage does not implement yet.")
|
3595
3844
|
|
3845
|
+
async def async_process(
|
3846
|
+
self,
|
3847
|
+
params: DictData,
|
3848
|
+
run_id: str,
|
3849
|
+
context: DictData,
|
3850
|
+
*,
|
3851
|
+
parent_run_id: Optional[str] = None,
|
3852
|
+
event: Optional[Event] = None,
|
3853
|
+
) -> Result: # pragma: no cov
|
3854
|
+
"""Async process for nested-stage do not implement yet.
|
3855
|
+
|
3856
|
+
Args:
|
3857
|
+
params: A parameter data that want to use in this
|
3858
|
+
execution.
|
3859
|
+
run_id: A running stage ID.
|
3860
|
+
context: A context data.
|
3861
|
+
parent_run_id: A parent running ID. (Default is None)
|
3862
|
+
event: An event manager that use to track parent process
|
3863
|
+
was not force stopped.
|
3864
|
+
|
3865
|
+
Returns:
|
3866
|
+
Result: The execution result with status and context data.
|
3867
|
+
"""
|
3868
|
+
raise NotImplementedError(
|
3869
|
+
"The Docker stage does not implement the `axecute` method yet."
|
3870
|
+
)
|
3871
|
+
|
3596
3872
|
|
3597
3873
|
class VirtualPyStage(PyStage): # pragma: no cov
|
3598
3874
|
"""Virtual Python stage executor that run Python statement on the dependent
|
@@ -3629,7 +3905,7 @@ class VirtualPyStage(PyStage): # pragma: no cov
|
|
3629
3905
|
|
3630
3906
|
Args:
|
3631
3907
|
py: A Python string statement.
|
3632
|
-
values: A variable that want to set before running
|
3908
|
+
values: A variable that want to set before running these
|
3633
3909
|
deps: An additional Python dependencies that want install before
|
3634
3910
|
run this python stage.
|
3635
3911
|
run_id: (StrOrNone) A running ID of this stage execution.
|
@@ -3761,7 +4037,7 @@ class VirtualPyStage(PyStage): # pragma: no cov
|
|
3761
4037
|
)
|
3762
4038
|
|
3763
4039
|
|
3764
|
-
|
4040
|
+
SubStage = Annotated[
|
3765
4041
|
Union[
|
3766
4042
|
BashStage,
|
3767
4043
|
CallStage,
|
@@ -3777,7 +4053,10 @@ NestedStage = Annotated[
|
|
3777
4053
|
],
|
3778
4054
|
Field(
|
3779
4055
|
union_mode="smart",
|
3780
|
-
description=
|
4056
|
+
description=(
|
4057
|
+
"A nested-stage allow list that able to use on the NestedStage "
|
4058
|
+
"model."
|
4059
|
+
),
|
3781
4060
|
),
|
3782
4061
|
] # pragma: no cov
|
3783
4062
|
|
@@ -3790,6 +4069,7 @@ ActionStage = Annotated[
|
|
3790
4069
|
PyStage,
|
3791
4070
|
RaiseStage,
|
3792
4071
|
DockerStage,
|
4072
|
+
TriggerStage,
|
3793
4073
|
EmptyStage,
|
3794
4074
|
],
|
3795
4075
|
Field(
|
@@ -405,6 +405,17 @@ def obj_name(obj: Optional[Union[str, object]] = None) -> Optional[str]:
|
|
405
405
|
return obj_type
|
406
406
|
|
407
407
|
|
408
|
-
def
|
409
|
-
"""Remove key that starts with `__sys_` from the extra dict parameter.
|
410
|
-
|
408
|
+
def pop_sys_extras(extras: DictData, scope: str = "exec") -> DictData:
|
409
|
+
"""Remove key that starts with `__sys_` from the extra dict parameter.
|
410
|
+
|
411
|
+
Args:
|
412
|
+
extras:
|
413
|
+
scope (str):
|
414
|
+
|
415
|
+
Returns:
|
416
|
+
DictData:
|
417
|
+
"""
|
418
|
+
keys: list[str] = [k for k in extras if not k.startswith(f"__sys_{scope}")]
|
419
|
+
for k in keys:
|
420
|
+
extras.pop(k)
|
421
|
+
return extras
|
@@ -41,6 +41,7 @@ from pydantic.functional_serializers import field_serializer
|
|
41
41
|
from pydantic.functional_validators import field_validator, model_validator
|
42
42
|
from typing_extensions import Self
|
43
43
|
|
44
|
+
from . import DRYRUN
|
44
45
|
from .__types import DictData
|
45
46
|
from .audits import NORMAL, RERUN, Audit, ReleaseType, get_audit
|
46
47
|
from .conf import YamlParser, dynamic
|
@@ -66,7 +67,7 @@ from .utils import (
|
|
66
67
|
extract_id,
|
67
68
|
gen_id,
|
68
69
|
get_dt_now,
|
69
|
-
|
70
|
+
pop_sys_extras,
|
70
71
|
)
|
71
72
|
|
72
73
|
|
@@ -244,14 +245,12 @@ class Workflow(BaseModel):
|
|
244
245
|
f"{self.name!r}."
|
245
246
|
)
|
246
247
|
|
247
|
-
# NOTE: Force update internal extras for handler circle execution.
|
248
|
-
self.extras.update({"__sys_break_circle_exec": self.name})
|
249
|
-
|
250
248
|
return self
|
251
249
|
|
252
250
|
@field_serializer("extras")
|
253
251
|
def __serialize_extras(self, extras: DictData) -> DictData:
|
254
|
-
|
252
|
+
"""Serialize extra parameter."""
|
253
|
+
return {k: extras[k] for k in extras if not k.startswith("__sys_")}
|
255
254
|
|
256
255
|
def detail(self) -> DictData: # pragma: no cov
|
257
256
|
"""Return the detail of this workflow for generate markdown."""
|
@@ -264,8 +263,10 @@ class Workflow(BaseModel):
|
|
264
263
|
author (str | None, default None): An author name.
|
265
264
|
"""
|
266
265
|
|
267
|
-
def align_newline(value: str) -> str:
|
266
|
+
def align_newline(value: Optional[str]) -> str:
|
268
267
|
space: str = " " * 16
|
268
|
+
if value is None:
|
269
|
+
return ""
|
269
270
|
return value.rstrip("\n").replace("\n", f"\n{space}")
|
270
271
|
|
271
272
|
info: str = (
|
@@ -452,7 +453,17 @@ class Workflow(BaseModel):
|
|
452
453
|
extras=self.extras,
|
453
454
|
)
|
454
455
|
|
455
|
-
if release_type ==
|
456
|
+
if release_type == RERUN:
|
457
|
+
# TODO: It will load previous audit and use this data to run with
|
458
|
+
# the `rerun` method.
|
459
|
+
raise NotImplementedError(
|
460
|
+
"Release does not support for rerun type yet. Please use the "
|
461
|
+
"`rerun` method instead."
|
462
|
+
)
|
463
|
+
elif release_type == DRYRUN:
|
464
|
+
self.extras.update({"__sys_release_dryrun_mode": True})
|
465
|
+
trace.debug("[RELEASE]: Mark dryrun mode to the extra params.")
|
466
|
+
elif release_type == NORMAL and audit.is_pointed(data=audit_data):
|
456
467
|
trace.info("[RELEASE]: Skip this release because it already audit.")
|
457
468
|
return Result(
|
458
469
|
run_id=run_id,
|
@@ -462,14 +473,6 @@ class Workflow(BaseModel):
|
|
462
473
|
extras=self.extras,
|
463
474
|
)
|
464
475
|
|
465
|
-
if release_type == RERUN:
|
466
|
-
# TODO: It will load previous audit and use this data to run with
|
467
|
-
# the `rerun` method.
|
468
|
-
raise NotImplementedError(
|
469
|
-
"Release does not support for rerun type yet. Please use the "
|
470
|
-
"`rerun` method instead."
|
471
|
-
)
|
472
|
-
|
473
476
|
rs: Result = self.execute(
|
474
477
|
params=values,
|
475
478
|
run_id=parent_run_id,
|
@@ -478,27 +481,29 @@ class Workflow(BaseModel):
|
|
478
481
|
catch(context, status=rs.status, updated=rs.context)
|
479
482
|
trace.info(f"[RELEASE]: End {name!r} : {release:%Y-%m-%d %H:%M:%S}")
|
480
483
|
trace.debug(f"[RELEASE]: Writing audit: {name!r}.")
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
484
|
+
if release_type != DRYRUN:
|
485
|
+
(
|
486
|
+
audit.save(
|
487
|
+
data=audit_data
|
488
|
+
| {
|
489
|
+
"context": context,
|
490
|
+
"runs_metadata": (
|
491
|
+
(runs_metadata or {})
|
492
|
+
| rs.info
|
493
|
+
| {
|
494
|
+
"timeout": timeout,
|
495
|
+
"original_name": self.name,
|
496
|
+
"audit_excluded": audit_excluded,
|
497
|
+
}
|
498
|
+
),
|
499
|
+
},
|
500
|
+
excluded=audit_excluded,
|
501
|
+
)
|
497
502
|
)
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
503
|
+
|
504
|
+
# NOTE: Pop system extra parameters.
|
505
|
+
pop_sys_extras(self.extras, scope="release")
|
506
|
+
return Result.from_trace(trace).catch(
|
502
507
|
status=rs.status,
|
503
508
|
context=catch(
|
504
509
|
context,
|
@@ -513,7 +518,6 @@ class Workflow(BaseModel):
|
|
513
518
|
**(context["errors"] if "errors" in context else {}),
|
514
519
|
},
|
515
520
|
),
|
516
|
-
extras=remove_sys_extras(self.extras),
|
517
521
|
)
|
518
522
|
|
519
523
|
def execute_job(
|
@@ -719,6 +723,8 @@ class Workflow(BaseModel):
|
|
719
723
|
extras=self.extras,
|
720
724
|
)
|
721
725
|
|
726
|
+
# NOTE: Force update internal extras for handler circle execution.
|
727
|
+
self.extras.update({"__sys_exec_break_circle": self.name})
|
722
728
|
with ThreadPoolExecutor(max_job_parallel, "wf") as executor:
|
723
729
|
futures: list[Future] = []
|
724
730
|
|
@@ -747,6 +753,7 @@ class Workflow(BaseModel):
|
|
747
753
|
backoff_sleep = 0.01
|
748
754
|
|
749
755
|
if check == FAILED: # pragma: no cov
|
756
|
+
pop_sys_extras(self.extras)
|
750
757
|
return Result(
|
751
758
|
run_id=run_id,
|
752
759
|
parent_run_id=parent_run_id,
|
@@ -841,6 +848,7 @@ class Workflow(BaseModel):
|
|
841
848
|
for i, s in enumerate(sequence_statuses, start=0):
|
842
849
|
statuses[total + 1 + skip_count + i] = s
|
843
850
|
|
851
|
+
pop_sys_extras(self.extras)
|
844
852
|
st: Status = validate_statuses(statuses)
|
845
853
|
return Result(
|
846
854
|
run_id=run_id,
|
@@ -862,6 +870,7 @@ class Workflow(BaseModel):
|
|
862
870
|
|
863
871
|
time.sleep(0.0025)
|
864
872
|
|
873
|
+
pop_sys_extras(self.extras)
|
865
874
|
return Result(
|
866
875
|
run_id=run_id,
|
867
876
|
parent_run_id=parent_run_id,
|
@@ -47,7 +47,6 @@ def test_workflow():
|
|
47
47
|
|
48
48
|
set_job_id = job.model_copy()
|
49
49
|
set_job_id.id = "demo-run"
|
50
|
-
set_job_id.extras.update({"__sys_break_circle_exec": "manual-workflow"})
|
51
50
|
assert workflow.job("demo-run") == set_job_id
|
52
51
|
|
53
52
|
# NOTE: Raise ValueError when get a job with ID that does not exist.
|
@@ -101,21 +100,14 @@ def test_workflow_bypass_extras():
|
|
101
100
|
assert workflow.jobs["first-job"].extras == {}
|
102
101
|
|
103
102
|
# NOTE: Bypass extras to job model.
|
104
|
-
assert workflow.job("first-job").extras == {
|
105
|
-
|
106
|
-
"__sys_break_circle_exec": "manual-workflow",
|
107
|
-
}
|
108
|
-
assert workflow.job("second-job").extras == {
|
109
|
-
"registries": ["foo", "bar"],
|
110
|
-
"__sys_break_circle_exec": "manual-workflow",
|
111
|
-
}
|
103
|
+
assert workflow.job("first-job").extras == {"registries": ["foo", "bar"]}
|
104
|
+
assert workflow.job("second-job").extras == {"registries": ["foo", "bar"]}
|
112
105
|
|
113
106
|
assert workflow.job("first-job").stages[0].extras == {}
|
114
107
|
|
115
108
|
# NOTE: Bypass extras to stage model.
|
116
109
|
assert workflow.job("first-job").stage("echo").extras == {
|
117
|
-
"registries": ["foo", "bar"]
|
118
|
-
"__sys_break_circle_exec": "manual-workflow",
|
110
|
+
"registries": ["foo", "bar"]
|
119
111
|
}
|
120
112
|
|
121
113
|
|
@@ -237,10 +229,7 @@ def test_workflow_from_conf_override(test_path):
|
|
237
229
|
name="tmp-wf-override-conf-trigger", extras={"conf_path": conf_path}
|
238
230
|
)
|
239
231
|
stage = workflow.job(name="trigger-job").stage("trigger-stage")
|
240
|
-
assert stage.extras == {
|
241
|
-
"conf_path": conf_path,
|
242
|
-
"__sys_break_circle_exec": "tmp-wf-override-conf-trigger",
|
243
|
-
}
|
232
|
+
assert stage.extras == {"conf_path": conf_path}
|
244
233
|
|
245
234
|
rs: Result = workflow.execute(params={"name": "bar"})
|
246
235
|
assert rs.status == SUCCESS
|
@@ -4,6 +4,7 @@ from zoneinfo import ZoneInfo
|
|
4
4
|
|
5
5
|
import pytest
|
6
6
|
from ddeutil.workflow import (
|
7
|
+
DRYRUN,
|
7
8
|
FORCE,
|
8
9
|
NORMAL,
|
9
10
|
RERUN,
|
@@ -238,3 +239,43 @@ def test_workflow_release_rerun():
|
|
238
239
|
)
|
239
240
|
with pytest.raises(NotImplementedError):
|
240
241
|
workflow.release(release=datetime.now(), params={}, release_type=RERUN)
|
242
|
+
|
243
|
+
|
244
|
+
def test_workflow_release_dryrun():
|
245
|
+
workflow: Workflow = Workflow.model_validate(
|
246
|
+
obj={
|
247
|
+
"name": "wf-scheduling-common",
|
248
|
+
"jobs": {
|
249
|
+
"first-job": {
|
250
|
+
"stages": [
|
251
|
+
{"name": "First Stage", "run": "print('test')"},
|
252
|
+
{"name": "Second Stage", "id": "second-stage"},
|
253
|
+
]
|
254
|
+
}
|
255
|
+
},
|
256
|
+
"extra": {"enable_write_audit": True},
|
257
|
+
}
|
258
|
+
)
|
259
|
+
rs: Result = workflow.release(
|
260
|
+
release=datetime(2024, 10, 1),
|
261
|
+
params={},
|
262
|
+
release_type=DRYRUN,
|
263
|
+
)
|
264
|
+
assert rs.status == SUCCESS
|
265
|
+
assert rs.context == {
|
266
|
+
"status": "SUCCESS",
|
267
|
+
"params": {},
|
268
|
+
"release": {
|
269
|
+
"type": DRYRUN,
|
270
|
+
"logical_date": datetime(2024, 10, 1, tzinfo=ZoneInfo(key="UTC")),
|
271
|
+
},
|
272
|
+
"jobs": {
|
273
|
+
"first-job": {
|
274
|
+
"status": SUCCESS,
|
275
|
+
"stages": {
|
276
|
+
"7782830343": {"outputs": {}, "status": SUCCESS},
|
277
|
+
"second-stage": {"outputs": {}, "status": SUCCESS},
|
278
|
+
},
|
279
|
+
},
|
280
|
+
},
|
281
|
+
}
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/src/ddeutil/workflow/api/routes/__init__.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
{ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/src/ddeutil/workflow/api/routes/workflows.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/src/ddeutil/workflow/plugins/__init__.py
RENAMED
File without changes
|
File without changes
|
{ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/src/ddeutil/workflow/plugins/providers/aws.py
RENAMED
File without changes
|
{ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/src/ddeutil/workflow/plugins/providers/az.py
RENAMED
File without changes
|
File without changes
|
{ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/src/ddeutil/workflow/plugins/providers/gcs.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/src/ddeutil_workflow.egg-info/SOURCES.txt
RENAMED
File without changes
|
File without changes
|
{ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/src/ddeutil_workflow.egg-info/entry_points.txt
RENAMED
File without changes
|
{ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/src/ddeutil_workflow.egg-info/requires.txt
RENAMED
File without changes
|
{ddeutil_workflow-0.0.83 → ddeutil_workflow-0.0.84}/src/ddeutil_workflow.egg-info/top_level.txt
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|