ddeutil-workflow 0.0.36__py3-none-any.whl → 0.0.38__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -23,6 +23,7 @@ template searching.
23
23
  """
24
24
  from __future__ import annotations
25
25
 
26
+ import asyncio
26
27
  import contextlib
27
28
  import inspect
28
29
  import subprocess
@@ -31,6 +32,11 @@ import time
31
32
  import uuid
32
33
  from abc import ABC, abstractmethod
33
34
  from collections.abc import Iterator
35
+ from concurrent.futures import (
36
+ Future,
37
+ ThreadPoolExecutor,
38
+ as_completed,
39
+ )
34
40
  from inspect import Parameter
35
41
  from pathlib import Path
36
42
  from subprocess import CompletedProcess
@@ -43,25 +49,23 @@ from typing_extensions import Self
43
49
 
44
50
  from .__types import DictData, DictStr, TupleStr
45
51
  from .caller import TagFunc, extract_call
46
- from .conf import config, get_logger
47
- from .exceptions import StageException
52
+ from .conf import config
53
+ from .exceptions import StageException, to_dict
48
54
  from .result import Result, Status
49
55
  from .templates import not_in_template, param2template
50
56
  from .utils import (
51
- cut_id,
52
57
  gen_id,
53
58
  make_exec,
54
59
  )
55
60
 
56
- logger = get_logger("ddeutil.workflow")
57
-
58
-
59
61
  __all__: TupleStr = (
60
62
  "EmptyStage",
61
63
  "BashStage",
62
64
  "PyStage",
63
65
  "CallStage",
64
66
  "TriggerStage",
67
+ "ForEachStage",
68
+ "ParallelStage",
65
69
  "Stage",
66
70
  )
67
71
 
@@ -127,13 +131,14 @@ class BaseStage(BaseModel, ABC):
127
131
  """Execute abstraction method that action something by sub-model class.
128
132
  This is important method that make this class is able to be the stage.
129
133
 
130
- :param params: A parameter data that want to use in this execution.
134
+ :param params: (DictData) A parameter data that want to use in this
135
+ execution.
131
136
  :param result: (Result) A result object for keeping context and status
132
137
  data.
133
138
 
134
139
  :rtype: Result
135
140
  """
136
- raise NotImplementedError("Stage should implement ``execute`` method.")
141
+ raise NotImplementedError("Stage should implement `execute` method.")
137
142
 
138
143
  def handler_execute(
139
144
  self,
@@ -142,8 +147,10 @@ class BaseStage(BaseModel, ABC):
142
147
  run_id: str | None = None,
143
148
  parent_run_id: str | None = None,
144
149
  result: Result | None = None,
150
+ raise_error: bool = False,
151
+ to: DictData | None = None,
145
152
  ) -> Result:
146
- """Handler execution result from the stage `execute` method.
153
+ """Handler stage execution result from the stage `execute` method.
147
154
 
148
155
  This stage exception handler still use ok-error concept, but it
149
156
  allows you force catching an output result with error message by
@@ -151,26 +158,31 @@ class BaseStage(BaseModel, ABC):
151
158
 
152
159
  Execution --> Ok --> Result
153
160
  |-status: Status.SUCCESS
154
- |-context:
155
- |-outputs: ...
161
+ ╰-context:
162
+ ╰-outputs: ...
156
163
 
157
164
  --> Error --> Result (if env var was set)
158
165
  |-status: Status.FAILED
159
- |-errors:
166
+ ╰-errors:
160
167
  |-class: ...
161
168
  |-name: ...
162
- |-message: ...
169
+ ╰-message: ...
163
170
 
164
171
  --> Error --> Raise StageException(...)
165
172
 
166
173
  On the last step, it will set the running ID on a return result object
167
174
  from current stage ID before release the final result.
168
175
 
169
- :param params: A parameter data that want to use in this execution.
176
+ :param params: (DictData) A parameterize value data that use in this
177
+ stage execution.
170
178
  :param run_id: (str) A running stage ID for this execution.
171
- :param parent_run_id: A parent workflow running ID for this release.
179
+ :param parent_run_id: (str) A parent workflow running ID for this
180
+ execution.
172
181
  :param result: (Result) A result object for keeping context and status
173
- data.
182
+ data before execution.
183
+ :param raise_error: (bool) A flag that all this method raise error
184
+ :param to: (DictData) A target object for auto set the return output
185
+ after execution.
174
186
 
175
187
  :rtype: Result
176
188
  """
@@ -178,18 +190,18 @@ class BaseStage(BaseModel, ABC):
178
190
  result,
179
191
  run_id=run_id,
180
192
  parent_run_id=parent_run_id,
181
- id_logic=(self.name + (self.id or "")),
193
+ id_logic=self.iden,
182
194
  )
183
195
 
184
196
  try:
185
- return self.execute(params, result=result)
197
+ rs: Result = self.execute(params, result=result)
198
+ if to is not None:
199
+ return self.set_outputs(rs.context, to=to)
200
+ return rs
186
201
  except Exception as err:
187
202
  result.trace.error(f"[STAGE]: {err.__class__.__name__}: {err}")
188
203
 
189
- if config.stage_raise_error:
190
- # NOTE: If error that raise from stage execution course by
191
- # itself, it will return that error with previous
192
- # dependency.
204
+ if raise_error or config.stage_raise_error:
193
205
  if isinstance(err, StageException):
194
206
  raise
195
207
 
@@ -198,22 +210,15 @@ class BaseStage(BaseModel, ABC):
198
210
  f"{err.__class__.__name__}: {err}"
199
211
  ) from None
200
212
 
201
- # NOTE: Catching exception error object to result with
202
- # error_message and error keys.
203
- return result.catch(
204
- status=Status.FAILED,
205
- context={
206
- "errors": {
207
- "class": err,
208
- "name": err.__class__.__name__,
209
- "message": f"{err.__class__.__name__}: {err}",
210
- },
211
- },
212
- )
213
+ errors: DictData = {"errors": to_dict(err)}
214
+ if to is not None:
215
+ return self.set_outputs(errors, to=to)
216
+
217
+ return result.catch(status=Status.FAILED, context=errors)
213
218
 
214
219
  def set_outputs(self, output: DictData, to: DictData) -> DictData:
215
220
  """Set an outputs from execution process to the received context. The
216
- result from execution will pass to value of ``outputs`` key.
221
+ result from execution will pass to value of `outputs` key.
217
222
 
218
223
  For example of setting output method, If you receive execute output
219
224
  and want to set on the `to` like;
@@ -221,7 +226,7 @@ class BaseStage(BaseModel, ABC):
221
226
  ... (i) output: {'foo': bar}
222
227
  ... (ii) to: {}
223
228
 
224
- The result of the `to` variable will be;
229
+ The result of the `to` argument will be;
225
230
 
226
231
  ... (iii) to: {
227
232
  'stages': {
@@ -229,22 +234,18 @@ class BaseStage(BaseModel, ABC):
229
234
  }
230
235
  }
231
236
 
232
- :param output: An output data that want to extract to an output key.
233
- :param to: A context data that want to add output result.
237
+ :param output: (DictData) An output data that want to extract to an
238
+ output key.
239
+ :param to: (DictData) A context data that want to add output result.
240
+
234
241
  :rtype: DictData
235
242
  """
236
- if self.id is None and not config.stage_default_id:
237
- logger.warning(
238
- "Output does not set because this stage does not set ID or "
239
- "default stage ID config flag not be True."
240
- )
241
- return to
242
-
243
- # NOTE: Create stages key to receive an output from the stage execution.
244
243
  if "stages" not in to:
245
244
  to["stages"] = {}
246
245
 
247
- # NOTE: If the stage ID did not set, it will use its name instead.
246
+ if self.id is None and not config.stage_default_id:
247
+ return to
248
+
248
249
  _id: str = (
249
250
  param2template(self.id, params=to)
250
251
  if self.id
@@ -255,7 +256,6 @@ class BaseStage(BaseModel, ABC):
255
256
  {"errors": output.pop("errors", {})} if "errors" in output else {}
256
257
  )
257
258
 
258
- # NOTE: Set the output to that stage generated ID with ``outputs`` key.
259
259
  to["stages"][_id] = {"outputs": output, **errors}
260
260
  return to
261
261
 
@@ -268,10 +268,11 @@ class BaseStage(BaseModel, ABC):
268
268
  :raise StageException: When return type of the eval condition statement
269
269
  does not return with boolean type.
270
270
 
271
- :param params: A parameters that want to pass to condition template.
271
+ :param params: (DictData) A parameters that want to pass to condition
272
+ template.
273
+
272
274
  :rtype: bool
273
275
  """
274
- # NOTE: Return false result if condition does not set.
275
276
  if self.condition is None:
276
277
  return False
277
278
 
@@ -299,6 +300,7 @@ class EmptyStage(BaseStage):
299
300
  >>> stage = {
300
301
  ... "name": "Empty stage execution",
301
302
  ... "echo": "Hello World",
303
+ ... "sleep": 1,
302
304
  ... }
303
305
  """
304
306
 
@@ -308,7 +310,7 @@ class EmptyStage(BaseStage):
308
310
  )
309
311
  sleep: float = Field(
310
312
  default=0,
311
- description="A second value to sleep before finish execution",
313
+ description="A second value to sleep before start execution",
312
314
  ge=0,
313
315
  )
314
316
 
@@ -322,13 +324,32 @@ class EmptyStage(BaseStage):
322
324
  The result context should be empty and do not process anything
323
325
  without calling logging function.
324
326
 
325
- :param params: A context data that want to add output result. But this
326
- stage does not pass any output.
327
+ :param params: (DictData) A context data that want to add output result.
328
+ But this stage does not pass any output.
327
329
  :param result: (Result) A result object for keeping context and status
328
330
  data.
329
331
 
330
332
  :rtype: Result
331
333
  """
334
+ result: Result = result or Result(
335
+ run_id=gen_id(self.name + (self.id or ""), unique=True)
336
+ )
337
+
338
+ result.trace.info(
339
+ f"[STAGE]: Empty-Execute: {self.name!r}: "
340
+ f"( {param2template(self.echo, params=params) or '...'} )"
341
+ )
342
+ if self.sleep > 0:
343
+ if self.sleep > 5:
344
+ result.trace.info(f"[STAGE]: ... sleep ({self.sleep} seconds)")
345
+ time.sleep(self.sleep)
346
+
347
+ return result.catch(status=Status.SUCCESS)
348
+
349
+ # TODO: Draft async execute method for the perf improvement.
350
+ async def aexecute(
351
+ self, params: DictData, *, result: Result | None = None
352
+ ) -> Result: # pragma: no cov
332
353
  if result is None: # pragma: no cov
333
354
  result: Result = Result(
334
355
  run_id=gen_id(self.name + (self.id or ""), unique=True)
@@ -338,11 +359,8 @@ class EmptyStage(BaseStage):
338
359
  f"[STAGE]: Empty-Execute: {self.name!r}: "
339
360
  f"( {param2template(self.echo, params=params) or '...'} )"
340
361
  )
341
- if self.sleep > 0:
342
- if self.sleep > 30:
343
- result.trace.info(f"[STAGE]: ... sleep ({self.sleep} seconds)")
344
- time.sleep(self.sleep)
345
362
 
363
+ await asyncio.sleep(1)
346
364
  return result.catch(status=Status.SUCCESS)
347
365
 
348
366
 
@@ -382,20 +400,18 @@ class BashStage(BaseStage):
382
400
  step will write the `.sh` file before giving this file name to context.
383
401
  After that, it will auto delete this file automatic.
384
402
 
385
- :param bash: A bash statement that want to execute.
386
- :param env: An environment variable that use on this bash statement.
387
- :param run_id: A running stage ID that use for writing sh file instead
388
- generate by UUID4.
403
+ :param bash: (str) A bash statement that want to execute.
404
+ :param env: (DictStr) An environment variable that use on this bash
405
+ statement.
406
+ :param run_id: (str | None) A running stage ID that use for writing sh
407
+ file instead generate by UUID4.
408
+
389
409
  :rtype: Iterator[TupleStr]
390
410
  """
391
411
  run_id: str = run_id or uuid.uuid4()
392
412
  f_name: str = f"{run_id}.sh"
393
413
  f_shebang: str = "bash" if sys.platform.startswith("win") else "sh"
394
414
 
395
- logger.debug(
396
- f"({cut_id(run_id)}) [STAGE]: Start create `{f_name}` file."
397
- )
398
-
399
415
  with open(f"./{f_name}", mode="w", newline="\n") as f:
400
416
  # NOTE: write header of `.sh` file
401
417
  f.write(f"#!/bin/{f_shebang}\n\n")
@@ -439,9 +455,11 @@ class BashStage(BaseStage):
439
455
  env=param2template(self.env, params),
440
456
  run_id=result.run_id,
441
457
  ) as sh:
458
+ result.trace.debug(f"... Start create `{sh[1]}` file.")
442
459
  rs: CompletedProcess = subprocess.run(
443
460
  sh, shell=False, capture_output=True, text=True
444
461
  )
462
+
445
463
  if rs.returncode > 0:
446
464
  # NOTE: Prepare stderr message that returning from subprocess.
447
465
  err: str = (
@@ -457,8 +475,8 @@ class BashStage(BaseStage):
457
475
  status=Status.SUCCESS,
458
476
  context={
459
477
  "return_code": rs.returncode,
460
- "stdout": rs.stdout.rstrip("\n") or None,
461
- "stderr": rs.stderr.rstrip("\n") or None,
478
+ "stdout": None if (out := rs.stdout.strip("\n")) == "" else out,
479
+ "stderr": None if (out := rs.stderr.strip("\n")) == "" else out,
462
480
  },
463
481
  )
464
482
 
@@ -515,8 +533,9 @@ class PyStage(BaseStage):
515
533
  """Override set an outputs method for the Python execution process that
516
534
  extract output from all the locals values.
517
535
 
518
- :param output: An output data that want to extract to an output key.
519
- :param to: A context data that want to add output result.
536
+ :param output: (DictData) An output data that want to extract to an
537
+ output key.
538
+ :param to: (DictData) A context data that want to add output result.
520
539
 
521
540
  :rtype: DictData
522
541
  """
@@ -525,7 +544,7 @@ class PyStage(BaseStage):
525
544
  super().set_outputs(
526
545
  (
527
546
  {k: lc[k] for k in self.filter_locals(lc)}
528
- | {k: output[k] for k in output if k.startswith("error")}
547
+ | ({"errors": output["errors"]} if "errors" in output else {})
529
548
  ),
530
549
  to=to,
531
550
  )
@@ -558,13 +577,22 @@ class PyStage(BaseStage):
558
577
 
559
578
  # NOTE: create custom globals value that will pass to exec function.
560
579
  _globals: DictData = (
561
- globals() | params | param2template(self.vars, params)
580
+ globals()
581
+ | params
582
+ | param2template(self.vars, params)
583
+ | {"result": result}
562
584
  )
563
585
  lc: DictData = {}
564
586
 
565
587
  # NOTE: Start exec the run statement.
566
588
  result.trace.info(f"[STAGE]: Py-Execute: {self.name}")
589
+ result.trace.warning(
590
+ "[STAGE]: This stage allow use `eval` function, so, please "
591
+ "check your statement be safe before execute."
592
+ )
567
593
 
594
+ # TODO: Add Python systax wrapper for checking dangerous code before run
595
+ # this statement.
568
596
  # WARNING: The exec build-in function is very dangerous. So, it
569
597
  # should use the re module to validate exec-string before running.
570
598
  exec(run, _globals, lc)
@@ -583,11 +611,16 @@ class CallStage(BaseStage):
583
611
  statement. So, you can create your function complexly that you can for your
584
612
  objective to invoked by this stage object.
585
613
 
614
+ This stage is the usefull stage for run every job by a custom requirement
615
+ that you want by creating the Python function and adding it to the task
616
+ registry by importer syntax like `module.tasks.registry` not path style like
617
+ `module/tasks/registry`.
618
+
586
619
  Data Validate:
587
620
  >>> stage = {
588
621
  ... "name": "Task stage execution",
589
622
  ... "uses": "tasks/function-name@tag-name",
590
- ... "args": {"FOO": "BAR"},
623
+ ... "args": {"arg01": "BAR", "kwarg01": 10},
591
624
  ... }
592
625
  """
593
626
 
@@ -631,15 +664,26 @@ class CallStage(BaseStage):
631
664
  # calling.
632
665
  args: DictData = {"result": result} | param2template(self.args, params)
633
666
  ips = inspect.signature(t_func)
667
+ necessary_params: list[str] = [
668
+ k
669
+ for k in ips.parameters
670
+ if (
671
+ (v := ips.parameters[k]).default == Parameter.empty
672
+ and (
673
+ v.kind != Parameter.VAR_KEYWORD
674
+ or v.kind != Parameter.VAR_POSITIONAL
675
+ )
676
+ )
677
+ ]
634
678
  if any(
635
679
  (k.removeprefix("_") not in args and k not in args)
636
- for k in ips.parameters
637
- if ips.parameters[k].default == Parameter.empty
680
+ for k in necessary_params
638
681
  ):
639
682
  raise ValueError(
640
- f"Necessary params, ({', '.join(ips.parameters.keys())}, ), "
683
+ f"Necessary params, ({', '.join(necessary_params)}, ), "
641
684
  f"does not set to args"
642
685
  )
686
+
643
687
  # NOTE: add '_' prefix if it wants to use.
644
688
  for k in ips.parameters:
645
689
  if k.removeprefix("_") in args:
@@ -649,7 +693,13 @@ class CallStage(BaseStage):
649
693
  args.pop("result")
650
694
 
651
695
  result.trace.info(f"[STAGE]: Call-Execute: {t_func.name}@{t_func.tag}")
652
- rs: DictData = t_func(**param2template(args, params))
696
+ if inspect.iscoroutinefunction(t_func): # pragma: no cov
697
+ loop = asyncio.get_event_loop()
698
+ rs: DictData = loop.run_until_complete(
699
+ t_func(**param2template(args, params))
700
+ )
701
+ else:
702
+ rs: DictData = t_func(**param2template(args, params))
653
703
 
654
704
  # VALIDATE:
655
705
  # Check the result type from call function, it should be dict.
@@ -717,54 +767,133 @@ class TriggerStage(BaseStage):
717
767
  )
718
768
 
719
769
 
720
- # NOTE:
721
- # An order of parsing stage model on the Job model with ``stages`` field.
722
- # From the current build-in stages, they do not have stage that have the same
723
- # fields that because of parsing on the Job's stages key.
724
- #
725
- Stage = Union[
726
- PyStage,
727
- BashStage,
728
- CallStage,
729
- TriggerStage,
730
- EmptyStage,
731
- ]
732
-
733
-
734
- # TODO: Not implement this stages yet
735
770
  class ParallelStage(BaseStage): # pragma: no cov
736
771
  """Parallel execution stage that execute child stages with parallel.
737
772
 
773
+ This stage is not the low-level stage model because it runs muti-stages
774
+ in this stage execution.
775
+
738
776
  Data Validate:
739
777
  >>> stage = {
740
778
  ... "name": "Parallel stage execution.",
741
- ... "parallel": [
742
- ... {
743
- ... "name": "Echo first stage",
744
- ... "echo": "Start run with branch 1",
745
- ... "sleep": 3,
746
- ... },
747
- ... {
748
- ... "name": "Echo second stage",
749
- ... "echo": "Start run with branch 2",
750
- ... "sleep": 1,
751
- ... },
752
- ... ]
779
+ ... "parallel": {
780
+ ... "branch01": [
781
+ ... {
782
+ ... "name": "Echo first stage",
783
+ ... "echo": "Start run with branch 1",
784
+ ... "sleep": 3,
785
+ ... },
786
+ ... ],
787
+ ... "branch02": [
788
+ ... {
789
+ ... "name": "Echo second stage",
790
+ ... "echo": "Start run with branch 2",
791
+ ... "sleep": 1,
792
+ ... },
793
+ ... ],
794
+ ... }
753
795
  ... }
754
796
  """
755
797
 
756
- parallel: list[Stage]
798
+ parallel: dict[str, list[Stage]] = Field()
757
799
  max_parallel_core: int = Field(default=2)
758
800
 
801
+ @staticmethod
802
+ def task(
803
+ branch: str,
804
+ params: DictData,
805
+ result: Result,
806
+ stages: list[Stage],
807
+ ) -> DictData:
808
+ """Task execution method for passing a branch to each thread.
809
+
810
+ :param branch:
811
+ :param params:
812
+ :param result:
813
+ :param stages:
814
+
815
+ :rtype: DictData
816
+ """
817
+ context = {"branch": branch, "stages": {}}
818
+ result.trace.debug(f"[STAGE]: Execute parallel branch: {branch!r}")
819
+ for stage in stages:
820
+ try:
821
+ stage.set_outputs(
822
+ stage.handler_execute(
823
+ params=params,
824
+ run_id=result.run_id,
825
+ parent_run_id=result.parent_run_id,
826
+ ).context,
827
+ to=context,
828
+ )
829
+ except StageException as err: # pragma: no cov
830
+ result.trace.error(
831
+ f"[STAGE]: Catch:\n\t{err.__class__.__name__}:" f"\n\t{err}"
832
+ )
833
+ context.update(
834
+ {
835
+ "errors": {
836
+ "class": err,
837
+ "name": err.__class__.__name__,
838
+ "message": f"{err.__class__.__name__}: {err}",
839
+ },
840
+ },
841
+ )
842
+ return context
843
+
759
844
  def execute(
760
845
  self, params: DictData, *, result: Result | None = None
761
- ) -> Result: ...
846
+ ) -> Result:
847
+ """Execute the stages that parallel each branch via multi-threading mode
848
+ or async mode by changing `async_mode` flag.
762
849
 
850
+ :param params: A parameter that want to pass before run any statement.
851
+ :param result: (Result) A result object for keeping context and status
852
+ data.
763
853
 
764
- # TODO: Not implement this stages yet
765
- class ForEachStage(BaseStage): # pragma: no cov
766
- """For-Each execution stage that execute child stages with an item in list of
767
- item values.
854
+ :rtype: Result
855
+ """
856
+ if result is None: # pragma: no cov
857
+ result: Result = Result(
858
+ run_id=gen_id(self.name + (self.id or ""), unique=True)
859
+ )
860
+
861
+ rs: DictData = {"parallel": {}}
862
+ status = Status.SUCCESS
863
+ with ThreadPoolExecutor(
864
+ max_workers=self.max_parallel_core,
865
+ thread_name_prefix="parallel_stage_exec_",
866
+ ) as executor:
867
+
868
+ futures: list[Future] = []
869
+ for branch in self.parallel:
870
+ futures.append(
871
+ executor.submit(
872
+ self.task,
873
+ branch=branch,
874
+ params=params,
875
+ result=result,
876
+ stages=self.parallel[branch],
877
+ )
878
+ )
879
+
880
+ done = as_completed(futures, timeout=1800)
881
+ for future in done:
882
+ context: DictData = future.result()
883
+ rs["parallel"][context.pop("branch")] = context
884
+
885
+ if "errors" in context:
886
+ status = Status.FAILED
887
+
888
+ return result.catch(status=status, context=rs)
889
+
890
+
891
+ class ForEachStage(BaseStage):
892
+ """For-Each execution stage that execute child stages with an item in list
893
+ of item values.
894
+
895
+ This stage is not the low-level stage model because it runs muti-stages
896
+ in this stage execution.
768
897
 
769
898
  Data Validate:
770
899
  >>> stage = {
@@ -779,8 +908,108 @@ class ForEachStage(BaseStage): # pragma: no cov
779
908
  ... }
780
909
  """
781
910
 
782
- foreach: list[str]
783
- stages: list[Stage]
911
+ foreach: Union[list[str], list[int]] = Field(
912
+ description=(
913
+ "A items for passing to each stages via ${{ item }} template."
914
+ ),
915
+ )
916
+ stages: list[Stage] = Field(
917
+ description=(
918
+ "A list of stage that will run with each item in the foreach field."
919
+ ),
920
+ )
921
+
922
+ def execute(
923
+ self, params: DictData, *, result: Result | None = None
924
+ ) -> Result:
925
+ """Execute the stages that pass each item form the foreach field.
926
+
927
+ :param params: A parameter that want to pass before run any statement.
928
+ :param result: (Result) A result object for keeping context and status
929
+ data.
930
+
931
+ :rtype: Result
932
+ """
933
+ if result is None: # pragma: no cov
934
+ result: Result = Result(
935
+ run_id=gen_id(self.name + (self.id or ""), unique=True)
936
+ )
937
+
938
+ rs: DictData = {"items": self.foreach, "foreach": {}}
939
+ status = Status.SUCCESS
940
+ for item in self.foreach:
941
+ result.trace.debug(f"[STAGE]: Execute foreach item: {item!r}")
942
+ params["item"] = item
943
+ context = {"stages": {}}
944
+
945
+ for stage in self.stages:
946
+ try:
947
+ stage.set_outputs(
948
+ stage.handler_execute(
949
+ params=params,
950
+ run_id=result.run_id,
951
+ parent_run_id=result.parent_run_id,
952
+ ).context,
953
+ to=context,
954
+ )
955
+ except StageException as err: # pragma: no cov
956
+ status = Status.FAILED
957
+ result.trace.error(
958
+ f"[STAGE]: Catch:\n\t{err.__class__.__name__}:"
959
+ f"\n\t{err}"
960
+ )
961
+ context.update(
962
+ {
963
+ "errors": {
964
+ "class": err,
965
+ "name": err.__class__.__name__,
966
+ "message": f"{err.__class__.__name__}: {err}",
967
+ },
968
+ },
969
+ )
970
+
971
+ rs["foreach"][item] = context
972
+
973
+ return result.catch(status=status, context=rs)
974
+
975
+
976
+ # TODO: Not implement this stages yet
977
+ class IfStage(BaseStage): # pragma: no cov
978
+ """If execution stage.
979
+
980
+ Data Validate:
981
+ >>> stage = {
982
+ ... "name": "If stage execution.",
983
+ ... "case": "${{ param.test }}",
984
+ ... "match": [
985
+ ... {
986
+ ... "case": "1",
987
+ ... "stage": {
988
+ ... "name": "Stage case 1",
989
+ ... "eche": "Hello case 1",
990
+ ... },
991
+ ... },
992
+ ... {
993
+ ... "case": "2",
994
+ ... "stage": {
995
+ ... "name": "Stage case 2",
996
+ ... "eche": "Hello case 2",
997
+ ... },
998
+ ... },
999
+ ... {
1000
+ ... "case": "_",
1001
+ ... "stage": {
1002
+ ... "name": "Stage else",
1003
+ ... "eche": "Hello case else",
1004
+ ... },
1005
+ ... },
1006
+ ... ],
1007
+ ... }
1008
+
1009
+ """
1010
+
1011
+ case: str
1012
+ match: list[dict[str, Union[str, Stage]]]
784
1013
 
785
1014
  def execute(
786
1015
  self, params: DictData, *, result: Result | None = None
@@ -800,8 +1029,12 @@ class HookStage(BaseStage): # pragma: no cov
800
1029
 
801
1030
  # TODO: Not implement this stages yet
802
1031
  class DockerStage(BaseStage): # pragma: no cov
1032
+ """Docker container stage execution."""
1033
+
803
1034
  image: str
804
1035
  env: DictData = Field(default_factory=dict)
1036
+ volume: DictData = Field(default_factory=dict)
1037
+ auth: DictData = Field(default_factory=dict)
805
1038
 
806
1039
  def execute(
807
1040
  self, params: DictData, *, result: Result | None = None
@@ -816,3 +1049,27 @@ class VirtualPyStage(PyStage): # pragma: no cov
816
1049
  vars: DictData
817
1050
 
818
1051
  def create_py_file(self, py: str, run_id: str | None): ...
1052
+
1053
+
1054
+ # TODO: Not implement this stages yet
1055
+ class SensorStage(BaseStage): # pragma: no cov
1056
+
1057
+ def execute(
1058
+ self, params: DictData, *, result: Result | None = None
1059
+ ) -> Result: ...
1060
+
1061
+
1062
+ # NOTE:
1063
+ # An order of parsing stage model on the Job model with ``stages`` field.
1064
+ # From the current build-in stages, they do not have stage that have the same
1065
+ # fields that because of parsing on the Job's stages key.
1066
+ #
1067
+ Stage = Union[
1068
+ EmptyStage,
1069
+ BashStage,
1070
+ CallStage,
1071
+ TriggerStage,
1072
+ ForEachStage,
1073
+ ParallelStage,
1074
+ PyStage,
1075
+ ]