ddeutil-workflow 0.0.51__tar.gz → 0.0.52__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.
Files changed (68) hide show
  1. {ddeutil_workflow-0.0.51/src/ddeutil_workflow.egg-info → ddeutil_workflow-0.0.52}/PKG-INFO +1 -1
  2. ddeutil_workflow-0.0.52/src/ddeutil/workflow/__about__.py +1 -0
  3. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/src/ddeutil/workflow/__init__.py +3 -30
  4. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/src/ddeutil/workflow/job.py +19 -11
  5. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/src/ddeutil/workflow/stages.py +15 -12
  6. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/src/ddeutil/workflow/workflow.py +7 -2
  7. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52/src/ddeutil_workflow.egg-info}/PKG-INFO +1 -1
  8. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/tests/test_stage_handler_exec.py +162 -0
  9. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/tests/test_workflow_exec.py +61 -1
  10. ddeutil_workflow-0.0.51/src/ddeutil/workflow/__about__.py +0 -1
  11. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/LICENSE +0 -0
  12. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/README.md +0 -0
  13. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/pyproject.toml +0 -0
  14. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/setup.cfg +0 -0
  15. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/src/ddeutil/workflow/__cron.py +0 -0
  16. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/src/ddeutil/workflow/__main__.py +0 -0
  17. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/src/ddeutil/workflow/__types.py +0 -0
  18. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/src/ddeutil/workflow/api/__init__.py +0 -0
  19. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/src/ddeutil/workflow/api/api.py +0 -0
  20. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/src/ddeutil/workflow/api/log.py +0 -0
  21. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/src/ddeutil/workflow/api/repeat.py +0 -0
  22. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/src/ddeutil/workflow/api/routes/__init__.py +0 -0
  23. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/src/ddeutil/workflow/api/routes/job.py +0 -0
  24. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/src/ddeutil/workflow/api/routes/logs.py +0 -0
  25. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/src/ddeutil/workflow/api/routes/schedules.py +0 -0
  26. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/src/ddeutil/workflow/api/routes/workflows.py +0 -0
  27. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/src/ddeutil/workflow/conf.py +0 -0
  28. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/src/ddeutil/workflow/cron.py +0 -0
  29. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/src/ddeutil/workflow/exceptions.py +0 -0
  30. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/src/ddeutil/workflow/logs.py +0 -0
  31. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/src/ddeutil/workflow/params.py +0 -0
  32. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/src/ddeutil/workflow/result.py +0 -0
  33. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/src/ddeutil/workflow/reusables.py +0 -0
  34. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/src/ddeutil/workflow/scheduler.py +0 -0
  35. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/src/ddeutil/workflow/utils.py +0 -0
  36. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/src/ddeutil_workflow.egg-info/SOURCES.txt +0 -0
  37. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/src/ddeutil_workflow.egg-info/dependency_links.txt +0 -0
  38. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/src/ddeutil_workflow.egg-info/requires.txt +0 -0
  39. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/src/ddeutil_workflow.egg-info/top_level.txt +0 -0
  40. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/tests/test__cron.py +0 -0
  41. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/tests/test__regex.py +0 -0
  42. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/tests/test_conf.py +0 -0
  43. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/tests/test_cron_on.py +0 -0
  44. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/tests/test_job.py +0 -0
  45. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/tests/test_job_exec.py +0 -0
  46. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/tests/test_job_exec_strategy.py +0 -0
  47. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/tests/test_job_strategy.py +0 -0
  48. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/tests/test_logs_audit.py +0 -0
  49. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/tests/test_logs_trace.py +0 -0
  50. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/tests/test_params.py +0 -0
  51. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/tests/test_release.py +0 -0
  52. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/tests/test_release_queue.py +0 -0
  53. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/tests/test_result.py +0 -0
  54. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/tests/test_reusables_call_tag.py +0 -0
  55. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/tests/test_reusables_template.py +0 -0
  56. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/tests/test_reusables_template_filter.py +0 -0
  57. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/tests/test_schedule.py +0 -0
  58. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/tests/test_schedule_pending.py +0 -0
  59. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/tests/test_schedule_tasks.py +0 -0
  60. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/tests/test_schedule_workflow.py +0 -0
  61. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/tests/test_scheduler_control.py +0 -0
  62. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/tests/test_stage.py +0 -0
  63. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/tests/test_utils.py +0 -0
  64. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/tests/test_workflow.py +0 -0
  65. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/tests/test_workflow_exec_job.py +0 -0
  66. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/tests/test_workflow_exec_poke.py +0 -0
  67. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/tests/test_workflow_exec_release.py +0 -0
  68. {ddeutil_workflow-0.0.51 → ddeutil_workflow-0.0.52}/tests/test_workflow_task.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ddeutil-workflow
3
- Version: 0.0.51
3
+ Version: 0.0.52
4
4
  Summary: Lightweight workflow orchestration
5
5
  Author-email: ddeutils <korawich.anu@gmail.com>
6
6
  License: MIT
@@ -0,0 +1 @@
1
+ __version__: str = "0.0.52"
@@ -60,33 +60,6 @@ from .scheduler import (
60
60
  schedule_runner,
61
61
  schedule_task,
62
62
  )
63
- from .stages import (
64
- BashStage,
65
- CallStage,
66
- EmptyStage,
67
- ForEachStage,
68
- ParallelStage,
69
- PyStage,
70
- Stage,
71
- TriggerStage,
72
- )
73
- from .utils import (
74
- batch,
75
- cross_product,
76
- default_gen_id,
77
- delay,
78
- filter_func,
79
- gen_id,
80
- get_diff_sec,
81
- get_dt_now,
82
- make_exec,
83
- reach_next_minute,
84
- replace_sec,
85
- wait_to_next_minute,
86
- )
87
- from .workflow import (
88
- Release,
89
- ReleaseQueue,
90
- Workflow,
91
- WorkflowTask,
92
- )
63
+ from .stages import *
64
+ from .utils import *
65
+ from .workflow import *
@@ -27,7 +27,7 @@ from threading import Event
27
27
  from typing import Annotated, Any, Literal, Optional, Union
28
28
 
29
29
  from ddeutil.core import freeze_args
30
- from pydantic import BaseModel, ConfigDict, Discriminator, Field, SecretStr, Tag
30
+ from pydantic import BaseModel, Discriminator, Field, SecretStr, Tag
31
31
  from pydantic.functional_validators import field_validator, model_validator
32
32
  from typing_extensions import Self
33
33
 
@@ -178,7 +178,7 @@ class Strategy(BaseModel):
178
178
 
179
179
 
180
180
  class Rule(str, Enum):
181
- """Trigger rules enum object."""
181
+ """Rule enum object for assign trigger option."""
182
182
 
183
183
  ALL_SUCCESS: str = "all_success"
184
184
  ALL_FAILED: str = "all_failed"
@@ -203,8 +203,6 @@ class BaseRunsOn(BaseModel): # pragma: no cov
203
203
  object and override execute method.
204
204
  """
205
205
 
206
- model_config = ConfigDict(use_enum_values=True)
207
-
208
206
  type: RunsOn = Field(description="A runs-on type.")
209
207
  args: DictData = Field(
210
208
  default_factory=dict,
@@ -219,7 +217,9 @@ class BaseRunsOn(BaseModel): # pragma: no cov
219
217
  class OnLocal(BaseRunsOn): # pragma: no cov
220
218
  """Runs-on local."""
221
219
 
222
- type: Literal[RunsOn.LOCAL] = Field(default=RunsOn.LOCAL)
220
+ type: Literal[RunsOn.LOCAL] = Field(
221
+ default=RunsOn.LOCAL, validate_default=True
222
+ )
223
223
 
224
224
 
225
225
  class SelfHostedArgs(BaseModel):
@@ -231,7 +231,9 @@ class SelfHostedArgs(BaseModel):
231
231
  class OnSelfHosted(BaseRunsOn): # pragma: no cov
232
232
  """Runs-on self-hosted."""
233
233
 
234
- type: Literal[RunsOn.SELF_HOSTED] = Field(default=RunsOn.SELF_HOSTED)
234
+ type: Literal[RunsOn.SELF_HOSTED] = Field(
235
+ default=RunsOn.SELF_HOSTED, validate_default=True
236
+ )
235
237
  args: SelfHostedArgs = Field(alias="with")
236
238
 
237
239
 
@@ -245,7 +247,9 @@ class AzBatchArgs(BaseModel):
245
247
 
246
248
  class OnAzBatch(BaseRunsOn): # pragma: no cov
247
249
 
248
- type: Literal[RunsOn.AZ_BATCH] = Field(default=RunsOn.AZ_BATCH)
250
+ type: Literal[RunsOn.AZ_BATCH] = Field(
251
+ default=RunsOn.AZ_BATCH, validate_default=True
252
+ )
249
253
  args: AzBatchArgs = Field(alias="with")
250
254
 
251
255
 
@@ -264,13 +268,16 @@ class DockerArgs(BaseModel):
264
268
  class OnDocker(BaseRunsOn): # pragma: no cov
265
269
  """Runs-on Docker container."""
266
270
 
267
- type: Literal[RunsOn.DOCKER] = Field(default=RunsOn.DOCKER)
271
+ type: Literal[RunsOn.DOCKER] = Field(
272
+ default=RunsOn.DOCKER, validate_default=True
273
+ )
268
274
  args: DockerArgs = Field(alias="with", default_factory=DockerArgs)
269
275
 
270
276
 
271
- def get_discriminator_runs_on(model: dict[str, Any]) -> str:
277
+ def get_discriminator_runs_on(model: dict[str, Any]) -> RunsOn:
272
278
  """Get discriminator of the RunsOn models."""
273
- return model.get("type", "local")
279
+ t = model.get("type")
280
+ return RunsOn(t) if t else RunsOn.LOCAL
274
281
 
275
282
 
276
283
  RunsOnModel = Annotated[
@@ -336,6 +343,7 @@ class Job(BaseModel):
336
343
  )
337
344
  trigger_rule: Rule = Field(
338
345
  default=Rule.ALL_SUCCESS,
346
+ validate_default=True,
339
347
  description=(
340
348
  "A trigger rule of tracking needed jobs if feature will use when "
341
349
  "the `raise_error` did not set from job and stage executions."
@@ -609,7 +617,7 @@ class Job(BaseModel):
609
617
  )
610
618
 
611
619
  result.trace.info(
612
- f"[JOB]: Execute: {self.id!r} on {self.runs_on.type!r}"
620
+ f"[JOB]: Execute: {self.id!r} on {self.runs_on.type.value!r}"
613
621
  )
614
622
  if self.runs_on.type == RunsOn.LOCAL:
615
623
  return local_execute(
@@ -1099,14 +1099,15 @@ class ParallelStage(BaseStage): # pragma: no cov
1099
1099
  :rtype: DictData
1100
1100
  """
1101
1101
  result.trace.debug(f"... Execute branch: {branch!r}")
1102
- context: DictData = copy.deepcopy(params)
1103
- context.update({"branch": branch, "stages": {}})
1102
+ _params: DictData = copy.deepcopy(params)
1103
+ _params.update({"branch": branch})
1104
+ context: DictData = {"branch": branch, "stages": {}}
1104
1105
  for stage in self.parallel[branch]:
1105
1106
 
1106
1107
  if extras:
1107
1108
  stage.extras = extras
1108
1109
 
1109
- if stage.is_skipped(params=context):
1110
+ if stage.is_skipped(params=_params):
1110
1111
  result.trace.info(f"... Skip stage: {stage.iden!r}")
1111
1112
  stage.set_outputs(output={"skipped": True}, to=context)
1112
1113
  continue
@@ -1129,7 +1130,7 @@ class ParallelStage(BaseStage): # pragma: no cov
1129
1130
 
1130
1131
  try:
1131
1132
  rs: Result = stage.handler_execute(
1132
- params=context,
1133
+ params=_params,
1133
1134
  run_id=result.run_id,
1134
1135
  parent_run_id=result.parent_run_id,
1135
1136
  raise_error=True,
@@ -1294,14 +1295,15 @@ class ForEachStage(BaseStage):
1294
1295
  :rtype: Result
1295
1296
  """
1296
1297
  result.trace.debug(f"... Execute item: {item!r}")
1297
- context: DictData = copy.deepcopy(params)
1298
- context.update({"item": item, "stages": {}})
1298
+ _params: DictData = copy.deepcopy(params)
1299
+ _params.update({"item": item})
1300
+ context: DictData = {"item": item, "stages": {}}
1299
1301
  for stage in self.stages:
1300
1302
 
1301
1303
  if self.extras:
1302
1304
  stage.extras = self.extras
1303
1305
 
1304
- if stage.is_skipped(params=context):
1306
+ if stage.is_skipped(params=_params):
1305
1307
  result.trace.info(f"... Skip stage: {stage.iden!r}")
1306
1308
  stage.set_outputs(output={"skipped": True}, to=context)
1307
1309
  continue
@@ -1324,7 +1326,7 @@ class ForEachStage(BaseStage):
1324
1326
 
1325
1327
  try:
1326
1328
  rs: Result = stage.handler_execute(
1327
- params=context,
1329
+ params=_params,
1328
1330
  run_id=result.run_id,
1329
1331
  parent_run_id=result.parent_run_id,
1330
1332
  raise_error=True,
@@ -1513,15 +1515,16 @@ class UntilStage(BaseStage): # pragma: no cov
1513
1515
  :rtype: tuple[Result, T]
1514
1516
  """
1515
1517
  result.trace.debug(f"... Execute until item: {item!r}")
1516
- context: DictData = copy.deepcopy(params)
1517
- context.update({"loop": loop, "item": item, "stages": {}})
1518
+ _params: DictData = copy.deepcopy(params)
1519
+ _params.update({"item": item})
1520
+ context: DictData = {"loop": loop, "item": item, "stages": {}}
1518
1521
  next_item: T = None
1519
1522
  for stage in self.stages:
1520
1523
 
1521
1524
  if self.extras:
1522
1525
  stage.extras = self.extras
1523
1526
 
1524
- if stage.is_skipped(params=context):
1527
+ if stage.is_skipped(params=_params):
1525
1528
  result.trace.info(f"... Skip stage: {stage.iden!r}")
1526
1529
  stage.set_outputs(output={"skipped": True}, to=context)
1527
1530
  continue
@@ -1550,7 +1553,7 @@ class UntilStage(BaseStage): # pragma: no cov
1550
1553
 
1551
1554
  try:
1552
1555
  rs: Result = stage.handler_execute(
1553
- params=context,
1556
+ params=_params,
1554
1557
  run_id=result.run_id,
1555
1558
  parent_run_id=result.parent_run_id,
1556
1559
  raise_error=True,
@@ -371,7 +371,9 @@ class Workflow(BaseModel):
371
371
  def from_conf(
372
372
  cls,
373
373
  name: str,
374
+ *,
374
375
  extras: DictData | None = None,
376
+ loader: type[Loader] = None,
375
377
  ) -> Self:
376
378
  """Create Workflow instance from the Loader object that only receive
377
379
  an input workflow name. The loader object will use this workflow name to
@@ -380,12 +382,13 @@ class Workflow(BaseModel):
380
382
  :param name: A workflow name that want to pass to Loader object.
381
383
  :param extras: An extra parameters that want to pass to Loader
382
384
  object.
385
+ :param loader: A loader class for override default loader object.
383
386
 
384
387
  :raise ValueError: If the type does not match with current object.
385
388
 
386
389
  :rtype: Self
387
390
  """
388
- loader: Loader = Loader(name, externals=(extras or {}))
391
+ loader: Loader = (loader or Loader)(name, externals=(extras or {}))
389
392
 
390
393
  # NOTE: Validate the config type match with current connection model
391
394
  if loader.type != cls.__name__:
@@ -407,6 +410,7 @@ class Workflow(BaseModel):
407
410
  path: Path,
408
411
  *,
409
412
  extras: DictData | None = None,
413
+ loader: type[Loader] = None,
410
414
  ) -> Self:
411
415
  """Create Workflow instance from the specific path. The loader object
412
416
  will use this workflow name and path to searching configuration data of
@@ -416,12 +420,13 @@ class Workflow(BaseModel):
416
420
  :param path: (Path) A config path that want to search.
417
421
  :param extras: (DictData) An extra parameters that want to override core
418
422
  config values.
423
+ :param loader: A loader class for override default loader object.
419
424
 
420
425
  :raise ValueError: If the type does not match with current object.
421
426
 
422
427
  :rtype: Self
423
428
  """
424
- loader: SimLoad = SimLoad(
429
+ loader: SimLoad = (loader or SimLoad)(
425
430
  name, conf_path=path, externals=(extras or {})
426
431
  )
427
432
  # NOTE: Validate the config type match with current connection model
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ddeutil-workflow
3
- Version: 0.0.51
3
+ Version: 0.0.52
4
4
  Summary: Lightweight workflow orchestration
5
5
  Author-email: ddeutils <korawich.anu@gmail.com>
6
6
  License: MIT
@@ -680,6 +680,168 @@ def test_stage_exec_foreach_with_trigger(test_path):
680
680
  }
681
681
 
682
682
 
683
+ def test_stage_exec_multi_foreach_nested_with_trigger(test_path):
684
+ with dump_yaml_context(
685
+ test_path / "conf/demo/01_99_wf_test_wf_foreach_with_trigger.yml",
686
+ data="""
687
+ tmp-wf-foreach-nested-trigger-task:
688
+ type: Workflow
689
+ params:
690
+ item: int
691
+ jobs:
692
+ first-job:
693
+ stages:
694
+ - name: "Echo"
695
+ id: hello
696
+ echo: "Run trigger with item: ${{ params.item }}"
697
+
698
+ tmp-wf-foreach-nested-trigger:
699
+ type: Workflow
700
+ jobs:
701
+ first-job:
702
+ stages:
703
+ - name: "Start run for-each stage"
704
+ id: foreach-stage
705
+ foreach: [1, 2]
706
+ stages:
707
+
708
+ - name: "Start run for-each stage inside foreach"
709
+ id: foreach-nested
710
+ foreach: [3, 4]
711
+ stages:
712
+ - name: "Stage trigger"
713
+ trigger: tmp-wf-foreach-nested-trigger-task
714
+ params:
715
+ item: ${{ item }}
716
+ """,
717
+ ):
718
+ workflow = Workflow.from_conf(
719
+ name="tmp-wf-foreach-nested-trigger",
720
+ extras={"test": "demo"},
721
+ )
722
+ stage: Stage = workflow.job("first-job").stage("foreach-stage")
723
+ rs = stage.set_outputs(stage.handler_execute({}).context, to={})
724
+ assert rs == {
725
+ "stages": {
726
+ "foreach-stage": {
727
+ "outputs": {
728
+ "items": [1, 2],
729
+ "foreach": {
730
+ 1: {
731
+ "item": 1,
732
+ "stages": {
733
+ "foreach-nested": {
734
+ "outputs": {
735
+ "items": [3, 4],
736
+ "foreach": {
737
+ 3: {
738
+ "item": 3,
739
+ "stages": {
740
+ "8713259197": {
741
+ "outputs": {
742
+ "params": {
743
+ "item": 3
744
+ },
745
+ "jobs": {
746
+ "first-job": {
747
+ "stages": {
748
+ "hello": {
749
+ "outputs": {}
750
+ }
751
+ }
752
+ }
753
+ },
754
+ }
755
+ }
756
+ },
757
+ },
758
+ 4: {
759
+ "item": 4,
760
+ "stages": {
761
+ "8713259197": {
762
+ "outputs": {
763
+ "params": {
764
+ "item": 4
765
+ },
766
+ "jobs": {
767
+ "first-job": {
768
+ "stages": {
769
+ "hello": {
770
+ "outputs": {}
771
+ }
772
+ }
773
+ }
774
+ },
775
+ }
776
+ }
777
+ },
778
+ },
779
+ },
780
+ }
781
+ }
782
+ },
783
+ },
784
+ 2: {
785
+ "item": 2,
786
+ "stages": {
787
+ "foreach-nested": {
788
+ "outputs": {
789
+ "items": [3, 4],
790
+ "foreach": {
791
+ 3: {
792
+ "item": 3,
793
+ "stages": {
794
+ "8713259197": {
795
+ "outputs": {
796
+ "params": {
797
+ "item": 3
798
+ },
799
+ "jobs": {
800
+ "first-job": {
801
+ "stages": {
802
+ "hello": {
803
+ "outputs": {}
804
+ }
805
+ }
806
+ }
807
+ },
808
+ }
809
+ }
810
+ },
811
+ },
812
+ 4: {
813
+ "item": 4,
814
+ "stages": {
815
+ "8713259197": {
816
+ "outputs": {
817
+ "params": {
818
+ "item": 4
819
+ },
820
+ "jobs": {
821
+ "first-job": {
822
+ "stages": {
823
+ "hello": {
824
+ "outputs": {}
825
+ }
826
+ }
827
+ }
828
+ },
829
+ }
830
+ }
831
+ },
832
+ },
833
+ },
834
+ }
835
+ }
836
+ },
837
+ },
838
+ },
839
+ }
840
+ }
841
+ }
842
+ }
843
+
844
+
683
845
  def test_stage_exec_parallel(test_path):
684
846
  with dump_yaml_context(
685
847
  test_path / "conf/demo/01_99_wf_test_wf_parallel.yml",
@@ -599,6 +599,10 @@ def test_workflow_exec_foreach(test_path):
599
599
  - name: "Get Items before run foreach"
600
600
  id: get-items
601
601
  uses: tasks/get-items@demo
602
+ - name: "Create variable"
603
+ id: create-variable
604
+ run: |
605
+ foo: str = "bar"
602
606
  - name: "For-each item"
603
607
  id: foreach-stage
604
608
  foreach: ${{ stages.get-items.outputs.items }}
@@ -606,6 +610,7 @@ def test_workflow_exec_foreach(test_path):
606
610
  - name: "Echo stage"
607
611
  echo: |
608
612
  Start run with item ${{ item }}
613
+ Import variable ${{ stages.create-variable.outputs.foo }}
609
614
  - name: "Final Echo"
610
615
  if: ${{ item }} == 4
611
616
  echo: |
@@ -614,7 +619,62 @@ def test_workflow_exec_foreach(test_path):
614
619
  ):
615
620
  workflow = Workflow.from_conf(name="tmp-wf-foreach")
616
621
  rs = workflow.execute(params={})
617
- print(rs)
622
+ assert rs.status == SUCCESS
623
+ assert rs.context == {
624
+ "params": {},
625
+ "jobs": {
626
+ "transform": {
627
+ "stages": {
628
+ "get-items": {"outputs": {"items": [1, 2, 3, 4]}},
629
+ "create-variable": {"outputs": {"foo": "bar"}},
630
+ "foreach-stage": {
631
+ "outputs": {
632
+ "items": [1, 2, 3, 4],
633
+ "foreach": {
634
+ 1: {
635
+ "item": 1,
636
+ "stages": {
637
+ "2709471980": {"outputs": {}},
638
+ "9263488742": {
639
+ "outputs": {},
640
+ "skipped": True,
641
+ },
642
+ },
643
+ },
644
+ 2: {
645
+ "item": 2,
646
+ "stages": {
647
+ "2709471980": {"outputs": {}},
648
+ "9263488742": {
649
+ "outputs": {},
650
+ "skipped": True,
651
+ },
652
+ },
653
+ },
654
+ 3: {
655
+ "item": 3,
656
+ "stages": {
657
+ "2709471980": {"outputs": {}},
658
+ "9263488742": {
659
+ "outputs": {},
660
+ "skipped": True,
661
+ },
662
+ },
663
+ },
664
+ 4: {
665
+ "item": 4,
666
+ "stages": {
667
+ "2709471980": {"outputs": {}},
668
+ "9263488742": {"outputs": {}},
669
+ },
670
+ },
671
+ },
672
+ }
673
+ },
674
+ }
675
+ }
676
+ },
677
+ }
618
678
 
619
679
 
620
680
  @mock.patch.object(Config, "stage_raise_error", False)
@@ -1 +0,0 @@
1
- __version__: str = "0.0.51"