ddeutil-workflow 0.0.52__tar.gz → 0.0.53__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.52/src/ddeutil_workflow.egg-info → ddeutil_workflow-0.0.53}/PKG-INFO +1 -1
  2. ddeutil_workflow-0.0.53/src/ddeutil/workflow/__about__.py +1 -0
  3. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/src/ddeutil/workflow/job.py +1 -0
  4. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/src/ddeutil/workflow/stages.py +181 -94
  5. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53/src/ddeutil_workflow.egg-info}/PKG-INFO +1 -1
  6. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/tests/test_stage.py +2 -2
  7. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/tests/test_stage_handler_exec.py +46 -20
  8. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/tests/test_workflow_exec.py +98 -0
  9. ddeutil_workflow-0.0.52/src/ddeutil/workflow/__about__.py +0 -1
  10. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/LICENSE +0 -0
  11. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/README.md +0 -0
  12. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/pyproject.toml +0 -0
  13. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/setup.cfg +0 -0
  14. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/src/ddeutil/workflow/__cron.py +0 -0
  15. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/src/ddeutil/workflow/__init__.py +0 -0
  16. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/src/ddeutil/workflow/__main__.py +0 -0
  17. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/src/ddeutil/workflow/__types.py +0 -0
  18. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/src/ddeutil/workflow/api/__init__.py +0 -0
  19. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/src/ddeutil/workflow/api/api.py +0 -0
  20. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/src/ddeutil/workflow/api/log.py +0 -0
  21. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/src/ddeutil/workflow/api/repeat.py +0 -0
  22. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/src/ddeutil/workflow/api/routes/__init__.py +0 -0
  23. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/src/ddeutil/workflow/api/routes/job.py +0 -0
  24. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/src/ddeutil/workflow/api/routes/logs.py +0 -0
  25. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/src/ddeutil/workflow/api/routes/schedules.py +0 -0
  26. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/src/ddeutil/workflow/api/routes/workflows.py +0 -0
  27. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/src/ddeutil/workflow/conf.py +0 -0
  28. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/src/ddeutil/workflow/cron.py +0 -0
  29. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/src/ddeutil/workflow/exceptions.py +0 -0
  30. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/src/ddeutil/workflow/logs.py +0 -0
  31. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/src/ddeutil/workflow/params.py +0 -0
  32. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/src/ddeutil/workflow/result.py +0 -0
  33. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/src/ddeutil/workflow/reusables.py +0 -0
  34. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/src/ddeutil/workflow/scheduler.py +0 -0
  35. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/src/ddeutil/workflow/utils.py +0 -0
  36. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/src/ddeutil/workflow/workflow.py +0 -0
  37. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/src/ddeutil_workflow.egg-info/SOURCES.txt +0 -0
  38. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/src/ddeutil_workflow.egg-info/dependency_links.txt +0 -0
  39. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/src/ddeutil_workflow.egg-info/requires.txt +0 -0
  40. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/src/ddeutil_workflow.egg-info/top_level.txt +0 -0
  41. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/tests/test__cron.py +0 -0
  42. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/tests/test__regex.py +0 -0
  43. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/tests/test_conf.py +0 -0
  44. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/tests/test_cron_on.py +0 -0
  45. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/tests/test_job.py +0 -0
  46. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/tests/test_job_exec.py +0 -0
  47. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/tests/test_job_exec_strategy.py +0 -0
  48. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/tests/test_job_strategy.py +0 -0
  49. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/tests/test_logs_audit.py +0 -0
  50. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/tests/test_logs_trace.py +0 -0
  51. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/tests/test_params.py +0 -0
  52. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/tests/test_release.py +0 -0
  53. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/tests/test_release_queue.py +0 -0
  54. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/tests/test_result.py +0 -0
  55. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/tests/test_reusables_call_tag.py +0 -0
  56. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/tests/test_reusables_template.py +0 -0
  57. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/tests/test_reusables_template_filter.py +0 -0
  58. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/tests/test_schedule.py +0 -0
  59. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/tests/test_schedule_pending.py +0 -0
  60. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/tests/test_schedule_tasks.py +0 -0
  61. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/tests/test_schedule_workflow.py +0 -0
  62. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/tests/test_scheduler_control.py +0 -0
  63. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/tests/test_utils.py +0 -0
  64. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/tests/test_workflow.py +0 -0
  65. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/tests/test_workflow_exec_job.py +0 -0
  66. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/tests/test_workflow_exec_poke.py +0 -0
  67. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/tests/test_workflow_exec_release.py +0 -0
  68. {ddeutil_workflow-0.0.52 → ddeutil_workflow-0.0.53}/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.52
3
+ Version: 0.0.53
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.53"
@@ -555,6 +555,7 @@ class Job(BaseModel):
555
555
  )
556
556
 
557
557
  _id: str = self.id or job_id
558
+ output: DictData = copy.deepcopy(output)
558
559
  errors: DictData = (
559
560
  {"errors": output.pop("errors", {})} if "errors" in output else {}
560
561
  )
@@ -42,7 +42,7 @@ from concurrent.futures import (
42
42
  wait,
43
43
  )
44
44
  from datetime import datetime
45
- from inspect import Parameter
45
+ from inspect import Parameter, isclass, isfunction, ismodule
46
46
  from pathlib import Path
47
47
  from subprocess import CompletedProcess
48
48
  from textwrap import dedent
@@ -266,7 +266,7 @@ class BaseStage(BaseModel, ABC):
266
266
  param2template(self.name, params=to, extras=self.extras)
267
267
  )
268
268
  )
269
-
269
+ output: DictData = output.copy()
270
270
  errors: DictData = (
271
271
  {"errors": output.pop("errors", {})} if "errors" in output else {}
272
272
  )
@@ -302,7 +302,7 @@ class BaseStage(BaseModel, ABC):
302
302
  param2template(self.name, params=outputs, extras=self.extras)
303
303
  )
304
304
  )
305
- return outputs.get("stages", {}).get(_id, {})
305
+ return outputs.get("stages", {}).get(_id, {}).get("outputs", {})
306
306
 
307
307
  def is_skipped(self, params: DictData | None = None) -> bool:
308
308
  """Return true if condition of this stage do not correct. This process
@@ -704,12 +704,11 @@ class PyStage(BaseStage):
704
704
 
705
705
  :rtype: Iterator[str]
706
706
  """
707
- from inspect import isclass, ismodule
708
-
709
707
  for value in values:
710
708
 
711
709
  if (
712
710
  value == "__annotations__"
711
+ or (value.startswith("__") and value.endswith("__"))
713
712
  or ismodule(values[value])
714
713
  or isclass(values[value])
715
714
  ):
@@ -727,11 +726,10 @@ class PyStage(BaseStage):
727
726
 
728
727
  :rtype: DictData
729
728
  """
729
+ output: DictData = output.copy()
730
730
  lc: DictData = output.pop("locals", {})
731
731
  gb: DictData = output.pop("globals", {})
732
- super().set_outputs(
733
- {k: lc[k] for k in self.filter_locals(lc)} | output, to=to
734
- )
732
+ super().set_outputs(lc | output, to=to)
735
733
  to.update({k: gb[k] for k in to if k in gb})
736
734
  return to
737
735
 
@@ -762,26 +760,36 @@ class PyStage(BaseStage):
762
760
  lc: DictData = {}
763
761
  gb: DictData = (
764
762
  globals()
765
- | params
766
763
  | param2template(self.vars, params, extras=self.extras)
767
764
  | {"result": result}
768
765
  )
769
766
 
770
- # NOTE: Start exec the run statement.
771
767
  result.trace.info(f"[STAGE]: Py-Execute: {self.name}")
772
- # result.trace.warning(
773
- # "[STAGE]: This stage allow use `eval` function, so, please "
774
- # "check your statement be safe before execute."
775
- # )
776
- #
768
+
777
769
  # WARNING: The exec build-in function is very dangerous. So, it
778
770
  # should use the re module to validate exec-string before running.
779
771
  exec(
780
- param2template(dedent(self.run), params, extras=self.extras), gb, lc
772
+ param2template(dedent(self.run), params, extras=self.extras),
773
+ gb,
774
+ lc,
781
775
  )
782
776
 
783
777
  return result.catch(
784
- status=SUCCESS, context={"locals": lc, "globals": gb}
778
+ status=SUCCESS,
779
+ context={
780
+ "locals": {k: lc[k] for k in self.filter_locals(lc)},
781
+ "globals": {
782
+ k: gb[k]
783
+ for k in gb
784
+ if (
785
+ not k.startswith("__")
786
+ and k != "annotations"
787
+ and not ismodule(gb[k])
788
+ and not isclass(gb[k])
789
+ and not isfunction(gb[k])
790
+ )
791
+ },
792
+ },
785
793
  )
786
794
 
787
795
 
@@ -841,8 +849,8 @@ class CallStage(BaseStage):
841
849
 
842
850
  :raise ValueError: If necessary arguments does not pass from the `args`
843
851
  field.
844
- :raise TypeError: If the result from the caller function does not by
845
- a `dict` type.
852
+ :raise TypeError: If the result from the caller function does not match
853
+ with a `dict` type.
846
854
 
847
855
  :rtype: Result
848
856
  """
@@ -1099,17 +1107,17 @@ class ParallelStage(BaseStage): # pragma: no cov
1099
1107
  :rtype: DictData
1100
1108
  """
1101
1109
  result.trace.debug(f"... Execute branch: {branch!r}")
1102
- _params: DictData = copy.deepcopy(params)
1103
- _params.update({"branch": branch})
1104
- context: DictData = {"branch": branch, "stages": {}}
1110
+ context: DictData = copy.deepcopy(params)
1111
+ context.update({"branch": branch})
1112
+ output: DictData = {"branch": branch, "stages": {}}
1105
1113
  for stage in self.parallel[branch]:
1106
1114
 
1107
1115
  if extras:
1108
1116
  stage.extras = extras
1109
1117
 
1110
- if stage.is_skipped(params=_params):
1118
+ if stage.is_skipped(params=context):
1111
1119
  result.trace.info(f"... Skip stage: {stage.iden!r}")
1112
- stage.set_outputs(output={"skipped": True}, to=context)
1120
+ stage.set_outputs(output={"skipped": True}, to=output)
1113
1121
  continue
1114
1122
 
1115
1123
  if event and event.is_set():
@@ -1122,7 +1130,7 @@ class ParallelStage(BaseStage): # pragma: no cov
1122
1130
  parallel={
1123
1131
  branch: {
1124
1132
  "branch": branch,
1125
- "stages": filter_func(context.pop("stages", {})),
1133
+ "stages": filter_func(output.pop("stages", {})),
1126
1134
  "errors": StageException(error_msg).to_dict(),
1127
1135
  }
1128
1136
  },
@@ -1130,14 +1138,15 @@ class ParallelStage(BaseStage): # pragma: no cov
1130
1138
 
1131
1139
  try:
1132
1140
  rs: Result = stage.handler_execute(
1133
- params=_params,
1141
+ params=context,
1134
1142
  run_id=result.run_id,
1135
1143
  parent_run_id=result.parent_run_id,
1136
1144
  raise_error=True,
1137
1145
  event=event,
1138
1146
  )
1139
- stage.set_outputs(rs.context, to=context)
1140
- except StageException as e: # pragma: no cov
1147
+ stage.set_outputs(rs.context, to=output)
1148
+ stage.set_outputs(stage.get_outputs(output), to=context)
1149
+ except (StageException, UtilException) as e: # pragma: no cov
1141
1150
  result.trace.error(f"[STAGE]: {e.__class__.__name__}: {e}")
1142
1151
  raise StageException(
1143
1152
  f"Sub-Stage execution error: {e.__class__.__name__}: {e}"
@@ -1153,7 +1162,7 @@ class ParallelStage(BaseStage): # pragma: no cov
1153
1162
  parallel={
1154
1163
  branch: {
1155
1164
  "branch": branch,
1156
- "stages": filter_func(context.pop("stages", {})),
1165
+ "stages": filter_func(output.pop("stages", {})),
1157
1166
  "errors": StageException(error_msg).to_dict(),
1158
1167
  },
1159
1168
  },
@@ -1164,7 +1173,7 @@ class ParallelStage(BaseStage): # pragma: no cov
1164
1173
  parallel={
1165
1174
  branch: {
1166
1175
  "branch": branch,
1167
- "stages": filter_func(context.pop("stages", {})),
1176
+ "stages": filter_func(output.pop("stages", {})),
1168
1177
  },
1169
1178
  },
1170
1179
  )
@@ -1294,18 +1303,18 @@ class ForEachStage(BaseStage):
1294
1303
 
1295
1304
  :rtype: Result
1296
1305
  """
1297
- result.trace.debug(f"... Execute item: {item!r}")
1298
- _params: DictData = copy.deepcopy(params)
1299
- _params.update({"item": item})
1300
- context: DictData = {"item": item, "stages": {}}
1306
+ result.trace.debug(f"[STAGE]: Execute item: {item!r}")
1307
+ context: DictData = copy.deepcopy(params)
1308
+ context.update({"item": item})
1309
+ output: DictData = {"item": item, "stages": {}}
1301
1310
  for stage in self.stages:
1302
1311
 
1303
1312
  if self.extras:
1304
1313
  stage.extras = self.extras
1305
1314
 
1306
- if stage.is_skipped(params=_params):
1315
+ if stage.is_skipped(params=context):
1307
1316
  result.trace.info(f"... Skip stage: {stage.iden!r}")
1308
- stage.set_outputs(output={"skipped": True}, to=context)
1317
+ stage.set_outputs(output={"skipped": True}, to=output)
1309
1318
  continue
1310
1319
 
1311
1320
  if event and event.is_set(): # pragma: no cov
@@ -1318,7 +1327,7 @@ class ForEachStage(BaseStage):
1318
1327
  foreach={
1319
1328
  item: {
1320
1329
  "item": item,
1321
- "stages": filter_func(context.pop("stages", {})),
1330
+ "stages": filter_func(output.pop("stages", {})),
1322
1331
  "errors": StageException(error_msg).to_dict(),
1323
1332
  }
1324
1333
  },
@@ -1326,13 +1335,14 @@ class ForEachStage(BaseStage):
1326
1335
 
1327
1336
  try:
1328
1337
  rs: Result = stage.handler_execute(
1329
- params=_params,
1338
+ params=context,
1330
1339
  run_id=result.run_id,
1331
1340
  parent_run_id=result.parent_run_id,
1332
1341
  raise_error=True,
1333
1342
  event=event,
1334
1343
  )
1335
- stage.set_outputs(rs.context, to=context)
1344
+ stage.set_outputs(rs.context, to=output)
1345
+ stage.set_outputs(stage.get_outputs(output), to=context)
1336
1346
  except (StageException, UtilException) as e:
1337
1347
  result.trace.error(f"[STAGE]: {e.__class__.__name__}: {e}")
1338
1348
  raise StageException(
@@ -1349,18 +1359,17 @@ class ForEachStage(BaseStage):
1349
1359
  foreach={
1350
1360
  item: {
1351
1361
  "item": item,
1352
- "stages": filter_func(context.pop("stages", {})),
1362
+ "stages": filter_func(output.pop("stages", {})),
1353
1363
  "errors": StageException(error_msg).to_dict(),
1354
1364
  },
1355
1365
  },
1356
1366
  )
1357
-
1358
1367
  return result.catch(
1359
1368
  status=SUCCESS,
1360
1369
  foreach={
1361
1370
  item: {
1362
1371
  "item": item,
1363
- "stages": filter_func(context.pop("stages", {})),
1372
+ "stages": filter_func(output.pop("stages", {})),
1364
1373
  },
1365
1374
  },
1366
1375
  )
@@ -1515,18 +1524,18 @@ class UntilStage(BaseStage): # pragma: no cov
1515
1524
  :rtype: tuple[Result, T]
1516
1525
  """
1517
1526
  result.trace.debug(f"... Execute until item: {item!r}")
1518
- _params: DictData = copy.deepcopy(params)
1519
- _params.update({"item": item})
1520
- context: DictData = {"loop": loop, "item": item, "stages": {}}
1527
+ context: DictData = copy.deepcopy(params)
1528
+ context.update({"item": item})
1529
+ output: DictData = {"loop": loop, "item": item, "stages": {}}
1521
1530
  next_item: T = None
1522
1531
  for stage in self.stages:
1523
1532
 
1524
1533
  if self.extras:
1525
1534
  stage.extras = self.extras
1526
1535
 
1527
- if stage.is_skipped(params=_params):
1536
+ if stage.is_skipped(params=context):
1528
1537
  result.trace.info(f"... Skip stage: {stage.iden!r}")
1529
- stage.set_outputs(output={"skipped": True}, to=context)
1538
+ stage.set_outputs(output={"skipped": True}, to=output)
1530
1539
  continue
1531
1540
 
1532
1541
  if event and event.is_set():
@@ -1541,9 +1550,7 @@ class UntilStage(BaseStage): # pragma: no cov
1541
1550
  loop: {
1542
1551
  "loop": loop,
1543
1552
  "item": item,
1544
- "stages": filter_func(
1545
- context.pop("stages", {})
1546
- ),
1553
+ "stages": filter_func(output.pop("stages", {})),
1547
1554
  "errors": StageException(error_msg).to_dict(),
1548
1555
  }
1549
1556
  },
@@ -1553,17 +1560,18 @@ class UntilStage(BaseStage): # pragma: no cov
1553
1560
 
1554
1561
  try:
1555
1562
  rs: Result = stage.handler_execute(
1556
- params=_params,
1563
+ params=context,
1557
1564
  run_id=result.run_id,
1558
1565
  parent_run_id=result.parent_run_id,
1559
1566
  raise_error=True,
1560
1567
  event=event,
1561
1568
  )
1562
- stage.set_outputs(rs.context, to=context)
1563
- if "item" in (
1564
- outputs := stage.get_outputs(context).get("outputs", {})
1565
- ):
1566
- next_item = outputs["item"]
1569
+ stage.set_outputs(rs.context, to=output)
1570
+
1571
+ if "item" in (_output := stage.get_outputs(output)):
1572
+ next_item = _output["item"]
1573
+
1574
+ stage.set_outputs(_output, to=context)
1567
1575
  except (StageException, UtilException) as e:
1568
1576
  result.trace.error(f"[STAGE]: {e.__class__.__name__}: {e}")
1569
1577
  raise StageException(
@@ -1577,7 +1585,7 @@ class UntilStage(BaseStage): # pragma: no cov
1577
1585
  loop: {
1578
1586
  "loop": loop,
1579
1587
  "item": item,
1580
- "stages": filter_func(context.pop("stages", {})),
1588
+ "stages": filter_func(output.pop("stages", {})),
1581
1589
  }
1582
1590
  },
1583
1591
  ),
@@ -1672,7 +1680,9 @@ class Match(BaseModel):
1672
1680
  """Match model for the Case Stage."""
1673
1681
 
1674
1682
  case: Union[str, int] = Field(description="A match case.")
1675
- stage: Stage = Field(description="A stage to execution for this case.")
1683
+ stages: list[Stage] = Field(
1684
+ description="A list of stage to execution for this case."
1685
+ )
1676
1686
 
1677
1687
 
1678
1688
  class CaseStage(BaseStage):
@@ -1685,24 +1695,21 @@ class CaseStage(BaseStage):
1685
1695
  ... "match": [
1686
1696
  ... {
1687
1697
  ... "case": "1",
1688
- ... "stage": {
1689
- ... "name": "Stage case 1",
1690
- ... "eche": "Hello case 1",
1691
- ... },
1692
- ... },
1693
- ... {
1694
- ... "case": "2",
1695
- ... "stage": {
1696
- ... "name": "Stage case 2",
1697
- ... "eche": "Hello case 2",
1698
- ... },
1698
+ ... "stages": [
1699
+ ... {
1700
+ ... "name": "Stage case 1",
1701
+ ... "eche": "Hello case 1",
1702
+ ... },
1703
+ ... ],
1699
1704
  ... },
1700
1705
  ... {
1701
1706
  ... "case": "_",
1702
- ... "stage": {
1703
- ... "name": "Stage else",
1704
- ... "eche": "Hello case else",
1705
- ... },
1707
+ ... "stages": [
1708
+ ... {
1709
+ ... "name": "Stage else",
1710
+ ... "eche": "Hello case else",
1711
+ ... },
1712
+ ... ],
1706
1713
  ... },
1707
1714
  ... ],
1708
1715
  ... }
@@ -1722,6 +1729,98 @@ class CaseStage(BaseStage):
1722
1729
  alias="skip-not-match",
1723
1730
  )
1724
1731
 
1732
+ def execute_case(
1733
+ self,
1734
+ case: str,
1735
+ stages: list[Stage],
1736
+ params: DictData,
1737
+ result: Result,
1738
+ *,
1739
+ event: Event | None = None,
1740
+ ) -> Result:
1741
+ """Execute case.
1742
+
1743
+ :param case: (str) A case that want to execution.
1744
+ :param stages: (list[Stage]) A list of stage.
1745
+ :param params: (DictData) A parameter that want to pass to stage
1746
+ execution.
1747
+ :param result: (Result) A result object for keeping context and status
1748
+ data.
1749
+ :param event: (Event) An event manager that use to track parent execute
1750
+ was not force stopped.
1751
+
1752
+ :rtype: Result
1753
+ """
1754
+ context: DictData = copy.deepcopy(params)
1755
+ context.update({"case": case})
1756
+ output: DictData = {"case": case, "stages": {}}
1757
+
1758
+ for stage in stages:
1759
+
1760
+ if self.extras:
1761
+ stage.extras = self.extras
1762
+
1763
+ if stage.is_skipped(params=context):
1764
+ result.trace.info(f"... Skip stage: {stage.iden!r}")
1765
+ stage.set_outputs(output={"skipped": True}, to=output)
1766
+ continue
1767
+
1768
+ if event and event.is_set(): # pragma: no cov
1769
+ error_msg: str = (
1770
+ "Case-Stage was canceled from event that had set before "
1771
+ "stage case execution."
1772
+ )
1773
+ return result.catch(
1774
+ status=CANCEL,
1775
+ context={
1776
+ "case": case,
1777
+ "stages": filter_func(output.pop("stages", {})),
1778
+ "errors": StageException(error_msg).to_dict(),
1779
+ },
1780
+ )
1781
+
1782
+ try:
1783
+ rs: Result = stage.handler_execute(
1784
+ params=context,
1785
+ run_id=result.run_id,
1786
+ parent_run_id=result.parent_run_id,
1787
+ raise_error=True,
1788
+ event=event,
1789
+ )
1790
+ stage.set_outputs(rs.context, to=output)
1791
+ stage.set_outputs(stage.get_outputs(output), to=context)
1792
+ except (StageException, UtilException) as e: # pragma: no cov
1793
+ result.trace.error(f"[STAGE]: {e.__class__.__name__}: {e}")
1794
+ return result.catch(
1795
+ status=FAILED,
1796
+ context={
1797
+ "case": case,
1798
+ "stages": filter_func(output.pop("stages", {})),
1799
+ "errors": e.to_dict(),
1800
+ },
1801
+ )
1802
+
1803
+ if rs.status == FAILED:
1804
+ error_msg: str = (
1805
+ f"Case-Stage was break because it has a sub stage, "
1806
+ f"{stage.iden}, failed without raise error."
1807
+ )
1808
+ return result.catch(
1809
+ status=FAILED,
1810
+ context={
1811
+ "case": case,
1812
+ "stages": filter_func(output.pop("stages", {})),
1813
+ "errors": StageException(error_msg).to_dict(),
1814
+ },
1815
+ )
1816
+ return result.catch(
1817
+ status=SUCCESS,
1818
+ context={
1819
+ "case": case,
1820
+ "stages": filter_func(output.pop("stages", {})),
1821
+ },
1822
+ )
1823
+
1725
1824
  def execute(
1726
1825
  self,
1727
1826
  params: DictData,
@@ -1751,17 +1850,17 @@ class CaseStage(BaseStage):
1751
1850
 
1752
1851
  result.trace.info(f"[STAGE]: Case-Execute: {_case!r}.")
1753
1852
  _else: Optional[Match] = None
1754
- stage: Optional[Stage] = None
1853
+ stages: Optional[list[Stage]] = None
1755
1854
  for match in self.match:
1756
1855
  if (c := match.case) == "_":
1757
1856
  _else: Match = match
1758
1857
  continue
1759
1858
 
1760
1859
  _condition: str = param2template(c, params, extras=self.extras)
1761
- if stage is None and _case == _condition:
1762
- stage: Stage = match.stage
1860
+ if stages is None and _case == _condition:
1861
+ stages: list[Stage] = match.stages
1763
1862
 
1764
- if stage is None:
1863
+ if stages is None:
1765
1864
  if _else is None:
1766
1865
  if not self.skip_not_match:
1767
1866
  raise StageException(
@@ -1779,10 +1878,8 @@ class CaseStage(BaseStage):
1779
1878
  status=CANCEL,
1780
1879
  context={"errors": StageException(error_msg).to_dict()},
1781
1880
  )
1782
- stage: Stage = _else.stage
1783
-
1784
- if self.extras:
1785
- stage.extras = self.extras
1881
+ _case: str = "_"
1882
+ stages: list[Stage] = _else.stages
1786
1883
 
1787
1884
  if event and event.is_set(): # pragma: no cov
1788
1885
  return result.catch(
@@ -1795,19 +1892,9 @@ class CaseStage(BaseStage):
1795
1892
  },
1796
1893
  )
1797
1894
 
1798
- try:
1799
- return result.catch(
1800
- status=SUCCESS,
1801
- context=stage.handler_execute(
1802
- params=params,
1803
- run_id=result.run_id,
1804
- parent_run_id=result.parent_run_id,
1805
- event=event,
1806
- ).context,
1807
- )
1808
- except StageException as e: # pragma: no cov
1809
- result.trace.error(f"[STAGE]: {e.__class__.__name__}:" f"\n\t{e}")
1810
- return result.catch(status=FAILED, context={"errors": e.to_dict()})
1895
+ return self.execute_case(
1896
+ case=_case, stages=stages, params=params, result=result, event=event
1897
+ )
1811
1898
 
1812
1899
 
1813
1900
  class RaiseStage(BaseStage): # pragma: no cov
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ddeutil-workflow
3
- Version: 0.0.52
3
+ Version: 0.0.53
4
4
  Summary: Lightweight workflow orchestration
5
5
  Author-email: ddeutils <korawich.anu@gmail.com>
6
6
  License: MIT
@@ -109,9 +109,9 @@ def test_stage_get_outputs():
109
109
  assert stage.get_outputs(outputs) == {}
110
110
 
111
111
  stage.extras = {"stage_default_id": True}
112
- assert stage.get_outputs(outputs) == {"outputs": {"foo": "baz"}}
112
+ assert stage.get_outputs(outputs) == {"foo": "baz"}
113
113
 
114
114
  stage: Stage = EmptyStage.model_validate(
115
115
  {"id": "first-stage", "name": "Empty Stage", "echo": "hello world"}
116
116
  )
117
- assert stage.get_outputs(outputs) == {"outputs": {"foo": "bar"}}
117
+ assert stage.get_outputs(outputs) == {"foo": "bar"}
@@ -992,36 +992,36 @@ def test_stage_exec_case_match(test_path):
992
992
  case: ${{ params.name }}
993
993
  match:
994
994
  - case: "bar"
995
- stage:
996
- name: Match name with Bar
997
- echo: Hello ${{ params.name }}
995
+ stages:
996
+ - name: Match name with Bar
997
+ echo: Hello ${{ params.name }}
998
998
 
999
999
  - case: "foo"
1000
- stage:
1001
- name: Match name with For
1002
- echo: Hello ${{ params.name }}
1000
+ stages:
1001
+ - name: Match name with For
1002
+ echo: Hello ${{ params.name }}
1003
1003
 
1004
1004
  - case: "_"
1005
- stage:
1006
- name: Else stage
1007
- echo: Not match any case.
1005
+ stages:
1006
+ - name: Else stage
1007
+ echo: Not match any case.
1008
1008
  - name: "Stage raise not has else condition"
1009
1009
  id: raise-else
1010
1010
  case: ${{ params.name }}
1011
1011
  match:
1012
1012
  - case: "bar"
1013
- stage:
1014
- name: Match name with Bar
1015
- echo: Hello ${{ params.name }}
1013
+ stages:
1014
+ - name: Match name with Bar
1015
+ echo: Hello ${{ params.name }}
1016
1016
  - name: "Stage skip not has else condition"
1017
1017
  id: not-else
1018
1018
  case: ${{ params.name }}
1019
1019
  skip-not-match: true
1020
1020
  match:
1021
1021
  - case: "bar"
1022
- stage:
1023
- name: Match name with Bar
1024
- echo: Hello ${{ params.name }}
1022
+ stages:
1023
+ - name: Match name with Bar
1024
+ echo: Hello ${{ params.name }}
1025
1025
  """,
1026
1026
  ):
1027
1027
  workflow = Workflow.from_conf(name="tmp-wf-case-match")
@@ -1029,17 +1029,44 @@ def test_stage_exec_case_match(test_path):
1029
1029
  rs = stage.set_outputs(
1030
1030
  stage.handler_execute({"params": {"name": "bar"}}).context, to={}
1031
1031
  )
1032
- assert rs == {"stages": {"case-stage": {"outputs": {}}}}
1032
+ assert rs == {
1033
+ "stages": {
1034
+ "case-stage": {
1035
+ "outputs": {
1036
+ "case": "bar",
1037
+ "stages": {"3616274431": {"outputs": {}}},
1038
+ },
1039
+ },
1040
+ },
1041
+ }
1033
1042
 
1034
1043
  rs = stage.set_outputs(
1035
1044
  stage.handler_execute({"params": {"name": "foo"}}).context, to={}
1036
1045
  )
1037
- assert rs == {"stages": {"case-stage": {"outputs": {}}}}
1046
+ assert rs == {
1047
+ "stages": {
1048
+ "case-stage": {
1049
+ "outputs": {
1050
+ "case": "foo",
1051
+ "stages": {"4740784512": {"outputs": {}}},
1052
+ }
1053
+ }
1054
+ }
1055
+ }
1038
1056
 
1039
1057
  rs = stage.set_outputs(
1040
1058
  stage.handler_execute({"params": {"name": "test"}}).context, to={}
1041
1059
  )
1042
- assert rs == {"stages": {"case-stage": {"outputs": {}}}}
1060
+ assert rs == {
1061
+ "stages": {
1062
+ "case-stage": {
1063
+ "outputs": {
1064
+ "case": "_",
1065
+ "stages": {"5883888894": {"outputs": {}}},
1066
+ }
1067
+ }
1068
+ }
1069
+ }
1043
1070
 
1044
1071
  # NOTE: Raise because else condition does not set.
1045
1072
  stage: Stage = workflow.job("first-job").stage("raise-else")
@@ -1048,8 +1075,7 @@ def test_stage_exec_case_match(test_path):
1048
1075
 
1049
1076
  stage: Stage = workflow.job("first-job").stage("not-else")
1050
1077
  rs = stage.set_outputs(
1051
- stage.handler_execute({"params": {"name": "test"}}).context,
1052
- to={},
1078
+ stage.handler_execute({"params": {"name": "test"}}).context, to={}
1053
1079
  )
1054
1080
  assert rs == {
1055
1081
  "stages": {
@@ -677,6 +677,104 @@ def test_workflow_exec_foreach(test_path):
677
677
  }
678
678
 
679
679
 
680
+ def test_workflow_exec_foreach_get_inside(test_path):
681
+ with dump_yaml_context(
682
+ test_path / "conf/demo/01_99_wf_test_wf_foreach_get_inside.yml",
683
+ data="""
684
+ tmp-wf-foreach-inside:
685
+ type: Workflow
686
+ jobs:
687
+ transform:
688
+ stages:
689
+ - name: "Get Items before run foreach"
690
+ id: get-items
691
+ uses: tasks/get-items@demo
692
+ - name: "Create variable"
693
+ id: create-variable
694
+ run: |
695
+ foo: str = "bar"
696
+ - name: "For-each item"
697
+ id: foreach-stage
698
+ foreach: ${{ stages.get-items.outputs.items }}
699
+ stages:
700
+ - name: "Echo stage"
701
+ id: prepare-variable
702
+ run: |
703
+ foo: str = 'baz${{ item }}'
704
+ - name: "Final Echo"
705
+ if: ${{ item }} == 4
706
+ echo: |
707
+ This is the final foo, it be: ${{ stages.prepare-variable.outputs.foo }}
708
+ """,
709
+ ):
710
+ workflow = Workflow.from_conf(name="tmp-wf-foreach-inside")
711
+ rs = workflow.execute(params={})
712
+ assert rs.status == SUCCESS
713
+ assert rs.context == {
714
+ "params": {},
715
+ "jobs": {
716
+ "transform": {
717
+ "stages": {
718
+ "get-items": {"outputs": {"items": [1, 2, 3, 4]}},
719
+ "create-variable": {"outputs": {"foo": "bar"}},
720
+ "foreach-stage": {
721
+ "outputs": {
722
+ "items": [1, 2, 3, 4],
723
+ "foreach": {
724
+ 1: {
725
+ "item": 1,
726
+ "stages": {
727
+ "prepare-variable": {
728
+ "outputs": {"foo": "baz1"}
729
+ },
730
+ "9263488742": {
731
+ "outputs": {},
732
+ "skipped": True,
733
+ },
734
+ },
735
+ },
736
+ 2: {
737
+ "item": 2,
738
+ "stages": {
739
+ "prepare-variable": {
740
+ "outputs": {"foo": "baz2"}
741
+ },
742
+ "9263488742": {
743
+ "outputs": {},
744
+ "skipped": True,
745
+ },
746
+ },
747
+ },
748
+ 3: {
749
+ "item": 3,
750
+ "stages": {
751
+ "prepare-variable": {
752
+ "outputs": {"foo": "baz3"}
753
+ },
754
+ "9263488742": {
755
+ "outputs": {},
756
+ "skipped": True,
757
+ },
758
+ },
759
+ },
760
+ 4: {
761
+ "item": 4,
762
+ "stages": {
763
+ "prepare-variable": {
764
+ "outputs": {"foo": "baz4"}
765
+ },
766
+ "9263488742": {"outputs": {}},
767
+ },
768
+ },
769
+ },
770
+ }
771
+ },
772
+ }
773
+ }
774
+ },
775
+ }
776
+
777
+
680
778
  @mock.patch.object(Config, "stage_raise_error", False)
681
779
  def test_workflow_exec_raise_param(test_path):
682
780
  with dump_yaml_context(
@@ -1 +0,0 @@
1
- __version__: str = "0.0.52"