ddeutil-workflow 0.0.33__py3-none-any.whl → 0.0.34__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.
@@ -42,9 +42,9 @@ from pydantic.functional_validators import model_validator
42
42
  from typing_extensions import Self
43
43
 
44
44
  from .__types import DictData, DictStr, TupleStr
45
+ from .call import TagFunc, extract_call
45
46
  from .conf import config, get_logger
46
47
  from .exceptions import StageException
47
- from .hook import TagFunc, extract_hook
48
48
  from .result import Result, Status
49
49
  from .templates import not_in_template, param2template
50
50
  from .utils import (
@@ -60,7 +60,7 @@ __all__: TupleStr = (
60
60
  "EmptyStage",
61
61
  "BashStage",
62
62
  "PyStage",
63
- "HookStage",
63
+ "CallStage",
64
64
  "TriggerStage",
65
65
  "Stage",
66
66
  )
@@ -100,7 +100,7 @@ class BaseStage(BaseModel, ABC):
100
100
  return self.id or self.name
101
101
 
102
102
  @model_validator(mode="after")
103
- def __prepare_running_id__(self) -> Self:
103
+ def __prepare_running_id(self) -> Self:
104
104
  """Prepare stage running ID that use default value of field and this
105
105
  method will validate name and id fields should not contain any template
106
106
  parameter (exclude matrix template).
@@ -140,24 +140,26 @@ class BaseStage(BaseModel, ABC):
140
140
  params: DictData,
141
141
  *,
142
142
  run_id: str | None = None,
143
+ parent_run_id: str | None = None,
143
144
  result: Result | None = None,
144
145
  ) -> Result:
145
- """Handler result from the stage execution.
146
+ """Handler execution result from the stage `execute` method.
146
147
 
147
148
  This stage exception handler still use ok-error concept, but it
148
149
  allows you force catching an output result with error message by
149
150
  specific environment variable,`WORKFLOW_CORE_STAGE_RAISE_ERROR`.
150
151
 
151
152
  Execution --> Ok --> Result
152
- |-status: 0
153
+ |-status: Status.SUCCESS
153
154
  |-context:
154
155
  |-outputs: ...
155
156
 
156
157
  --> Error --> Result (if env var was set)
157
- |-status: 1
158
- |-context:
159
- |-error: ...
160
- |-error_message: ...
158
+ |-status: Status.FAILED
159
+ |-errors:
160
+ |-class: ...
161
+ |-name: ...
162
+ |-message: ...
161
163
 
162
164
  --> Error --> Raise StageException(...)
163
165
 
@@ -166,32 +168,34 @@ class BaseStage(BaseModel, ABC):
166
168
 
167
169
  :param params: A parameter data that want to use in this execution.
168
170
  :param run_id: (str) A running stage ID for this execution.
171
+ :param parent_run_id: A parent workflow running ID for this release.
169
172
  :param result: (Result) A result object for keeping context and status
170
173
  data.
171
174
 
172
175
  :rtype: Result
173
176
  """
174
- if result is None: # pragma: no cov
177
+ if result is None:
175
178
  result: Result = Result(
176
179
  run_id=(
177
180
  run_id or gen_id(self.name + (self.id or ""), unique=True)
178
181
  ),
182
+ parent_run_id=parent_run_id,
179
183
  )
184
+ elif parent_run_id:
185
+ result.set_parent_run_id(parent_run_id)
180
186
 
181
187
  try:
182
- # NOTE: Start calling origin function with a passing args.
183
188
  return self.execute(params, result=result)
184
189
  except Exception as err:
185
- # NOTE: Start catching error from the stage execution.
186
190
  result.trace.error(f"[STAGE]: {err.__class__.__name__}: {err}")
191
+
187
192
  if config.stage_raise_error:
188
193
  # NOTE: If error that raise from stage execution course by
189
194
  # itself, it will return that error with previous
190
195
  # dependency.
191
196
  if isinstance(err, StageException):
192
- raise StageException(
193
- f"{self.__class__.__name__}: \n\t{err}"
194
- ) from err
197
+ raise
198
+
195
199
  raise StageException(
196
200
  f"{self.__class__.__name__}: \n\t"
197
201
  f"{err.__class__.__name__}: {err}"
@@ -202,8 +206,11 @@ class BaseStage(BaseModel, ABC):
202
206
  return result.catch(
203
207
  status=Status.FAILED,
204
208
  context={
205
- "error": err,
206
- "error_message": f"{err.__class__.__name__}: {err}",
209
+ "errors": {
210
+ "class": err,
211
+ "name": err.__class__.__name__,
212
+ "message": f"{err.__class__.__name__}: {err}",
213
+ },
207
214
  },
208
215
  )
209
216
 
@@ -247,8 +254,12 @@ class BaseStage(BaseModel, ABC):
247
254
  else gen_id(param2template(self.name, params=to))
248
255
  )
249
256
 
257
+ errors: DictData = (
258
+ {"errors": output.pop("errors", {})} if "errors" in output else {}
259
+ )
260
+
250
261
  # NOTE: Set the output to that stage generated ID with ``outputs`` key.
251
- to["stages"][_id] = {"outputs": output}
262
+ to["stages"][_id] = {"outputs": output, **errors}
252
263
  return to
253
264
 
254
265
  def is_skipped(self, params: DictData | None = None) -> bool:
@@ -321,6 +332,11 @@ class EmptyStage(BaseStage):
321
332
 
322
333
  :rtype: Result
323
334
  """
335
+ if result is None: # pragma: no cov
336
+ result: Result = Result(
337
+ run_id=gen_id(self.name + (self.id or ""), unique=True)
338
+ )
339
+
324
340
  result.trace.info(
325
341
  f"[STAGE]: Empty-Execute: {self.name!r}: "
326
342
  f"( {param2template(self.echo, params=params) or '...'} )"
@@ -413,6 +429,11 @@ class BashStage(BaseStage):
413
429
 
414
430
  :rtype: Result
415
431
  """
432
+ if result is None: # pragma: no cov
433
+ result: Result = Result(
434
+ run_id=gen_id(self.name + (self.id or ""), unique=True)
435
+ )
436
+
416
437
  bash: str = param2template(dedent(self.bash), params)
417
438
 
418
439
  result.trace.info(f"[STAGE]: Shell-Execute: {self.name}")
@@ -473,12 +494,24 @@ class PyStage(BaseStage):
473
494
  )
474
495
 
475
496
  @staticmethod
476
- def pick_keys_from_locals(values: DictData) -> Iterator[str]:
477
- from inspect import ismodule
497
+ def filter_locals(values: DictData) -> Iterator[str]:
498
+ """Filter a locals input values.
499
+
500
+ :param values: (DictData) A locals values that want to filter.
501
+
502
+ :rtype: Iterator[str]
503
+ """
504
+ from inspect import isclass, ismodule
478
505
 
479
506
  for value in values:
480
- if value == "__annotations__" or ismodule(values[value]):
507
+
508
+ if (
509
+ value == "__annotations__"
510
+ or ismodule(values[value])
511
+ or isclass(values[value])
512
+ ):
481
513
  continue
514
+
482
515
  yield value
483
516
 
484
517
  def set_outputs(self, output: DictData, to: DictData) -> DictData:
@@ -494,7 +527,7 @@ class PyStage(BaseStage):
494
527
  lc: DictData = output.get("locals", {})
495
528
  super().set_outputs(
496
529
  (
497
- {k: lc[k] for k in self.pick_keys_from_locals(lc)}
530
+ {k: lc[k] for k in self.filter_locals(lc)}
498
531
  | {k: output[k] for k in output if k.startswith("error")}
499
532
  ),
500
533
  to=to,
@@ -518,6 +551,11 @@ class PyStage(BaseStage):
518
551
 
519
552
  :rtype: Result
520
553
  """
554
+ if result is None: # pragma: no cov
555
+ result: Result = Result(
556
+ run_id=gen_id(self.name + (self.id or ""), unique=True)
557
+ )
558
+
521
559
  # NOTE: Replace the run statement that has templating value.
522
560
  run: str = param2template(dedent(self.run), params)
523
561
 
@@ -539,8 +577,8 @@ class PyStage(BaseStage):
539
577
  )
540
578
 
541
579
 
542
- class HookStage(BaseStage):
543
- """Hook executor that hook the Python function from registry with tag
580
+ class CallStage(BaseStage):
581
+ """Call executor that call the Python function from registry with tag
544
582
  decorator function in ``utils`` module and run it with input arguments.
545
583
 
546
584
  This stage is different with PyStage because the PyStage is just calling
@@ -558,23 +596,23 @@ class HookStage(BaseStage):
558
596
 
559
597
  uses: str = Field(
560
598
  description=(
561
- "A pointer that want to load function from the hook registry."
599
+ "A pointer that want to load function from the call registry."
562
600
  ),
563
601
  )
564
602
  args: DictData = Field(
565
603
  default_factory=dict,
566
- description="An arguments that want to pass to the hook function.",
604
+ description="An arguments that want to pass to the call function.",
567
605
  alias="with",
568
606
  )
569
607
 
570
608
  def execute(
571
609
  self, params: DictData, *, result: Result | None = None
572
610
  ) -> Result:
573
- """Execute the Hook function that already in the hook registry.
611
+ """Execute the Call function that already in the call registry.
574
612
 
575
- :raise ValueError: When the necessary arguments of hook function do not
613
+ :raise ValueError: When the necessary arguments of call function do not
576
614
  set from the input params argument.
577
- :raise TypeError: When the return type of hook function does not be
615
+ :raise TypeError: When the return type of call function does not be
578
616
  dict type.
579
617
 
580
618
  :param params: A parameter that want to pass before run any statement.
@@ -585,7 +623,12 @@ class HookStage(BaseStage):
585
623
 
586
624
  :rtype: Result
587
625
  """
588
- t_func: TagFunc = extract_hook(param2template(self.uses, params))()
626
+ if result is None: # pragma: no cov
627
+ result: Result = Result(
628
+ run_id=gen_id(self.name + (self.id or ""), unique=True)
629
+ )
630
+
631
+ t_func: TagFunc = extract_call(param2template(self.uses, params))()
589
632
 
590
633
  # VALIDATE: check input task caller parameters that exists before
591
634
  # calling.
@@ -608,11 +651,11 @@ class HookStage(BaseStage):
608
651
  if "result" not in ips.parameters:
609
652
  args.pop("result")
610
653
 
611
- result.trace.info(f"[STAGE]: Hook-Execute: {t_func.name}@{t_func.tag}")
654
+ result.trace.info(f"[STAGE]: Call-Execute: {t_func.name}@{t_func.tag}")
612
655
  rs: DictData = t_func(**param2template(args, params))
613
656
 
614
657
  # VALIDATE:
615
- # Check the result type from hook function, it should be dict.
658
+ # Check the result type from call function, it should be dict.
616
659
  if not isinstance(rs, dict):
617
660
  raise TypeError(
618
661
  f"Return type: '{t_func.name}@{t_func.tag}' does not serialize "
@@ -659,14 +702,19 @@ class TriggerStage(BaseStage):
659
702
  # NOTE: Lazy import this workflow object.
660
703
  from . import Workflow
661
704
 
705
+ if result is None: # pragma: no cov
706
+ result: Result = Result(
707
+ run_id=gen_id(self.name + (self.id or ""), unique=True)
708
+ )
709
+
662
710
  # NOTE: Loading workflow object from trigger name.
663
711
  _trigger: str = param2template(self.trigger, params=params)
664
712
 
665
713
  # NOTE: Set running workflow ID from running stage ID to external
666
714
  # params on Loader object.
667
- wf: Workflow = Workflow.from_loader(name=_trigger)
715
+ workflow: Workflow = Workflow.from_loader(name=_trigger)
668
716
  result.trace.info(f"[STAGE]: Trigger-Execute: {_trigger!r}")
669
- return wf.execute(
717
+ return workflow.execute(
670
718
  params=param2template(self.params, params),
671
719
  result=result,
672
720
  )
@@ -680,19 +728,37 @@ class TriggerStage(BaseStage):
680
728
  Stage = Union[
681
729
  PyStage,
682
730
  BashStage,
683
- HookStage,
731
+ CallStage,
684
732
  TriggerStage,
685
733
  EmptyStage,
686
734
  ]
687
735
 
688
736
 
689
737
  # TODO: Not implement this stages yet
690
- class ParallelStage(BaseModel): # pragma: no cov
738
+ class ParallelStage(BaseStage): # pragma: no cov
691
739
  parallel: list[Stage]
692
740
  max_parallel_core: int = Field(default=2)
693
741
 
742
+ def execute(
743
+ self, params: DictData, *, result: Result | None = None
744
+ ) -> Result: ...
745
+
694
746
 
695
747
  # TODO: Not implement this stages yet
696
- class ForEachStage(BaseModel): # pragma: no cov
748
+ class ForEachStage(BaseStage): # pragma: no cov
697
749
  foreach: list[str]
698
750
  stages: list[Stage]
751
+
752
+ def execute(
753
+ self, params: DictData, *, result: Result | None = None
754
+ ) -> Result: ...
755
+
756
+
757
+ # TODO: Not implement this stages yet
758
+ class HookStage(BaseStage): # pragma: no cov
759
+ foreach: list[str]
760
+ stages: list[Stage]
761
+
762
+ def execute(
763
+ self, params: DictData, *, result: Result | None = None
764
+ ) -> Result: ...
ddeutil/workflow/utils.py CHANGED
@@ -8,7 +8,7 @@ from __future__ import annotations
8
8
  import logging
9
9
  import stat
10
10
  import time
11
- from collections.abc import Iterator
11
+ from collections.abc import Iterator, Mapping
12
12
  from datetime import datetime, timedelta
13
13
  from hashlib import md5
14
14
  from inspect import isfunction
@@ -199,7 +199,7 @@ def cross_product(matrix: Matrix) -> Iterator[DictData]:
199
199
  )
200
200
 
201
201
 
202
- def batch(iterable: Iterator[Any], n: int) -> Iterator[Any]:
202
+ def batch(iterable: Iterator[Any] | range, n: int) -> Iterator[Any]:
203
203
  """Batch data into iterators of length n. The last batch may be shorter.
204
204
 
205
205
  Example:
@@ -240,3 +240,21 @@ def cut_id(run_id: str, *, num: int = 6) -> str:
240
240
  :rtype: str
241
241
  """
242
242
  return run_id[-num:]
243
+
244
+
245
+ def deep_update(origin: DictData, u: Mapping) -> DictData:
246
+ """Deep update dict.
247
+
248
+ Example:
249
+ >>> deep_update(
250
+ ... origin={"jobs": {"job01": "foo"}},
251
+ ... u={"jobs": {"job02": "bar"}},
252
+ ... )
253
+ {"jobs": {"job01": "foo", "job02": "bar"}}
254
+ """
255
+ for k, value in u.items():
256
+ if isinstance(value, Mapping) and value:
257
+ origin[k] = deep_update(origin.get(k, {}), value)
258
+ else:
259
+ origin[k] = value
260
+ return origin