ddeutil-workflow 0.0.33__py3-none-any.whl → 0.0.35__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 .caller 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,31 @@ 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
175
- result: Result = Result(
176
- run_id=(
177
- run_id or gen_id(self.name + (self.id or ""), unique=True)
178
- ),
179
- )
177
+ result: Result = Result.construct_with_rs_or_id(
178
+ result,
179
+ run_id=run_id,
180
+ parent_run_id=parent_run_id,
181
+ id_logic=(self.name + (self.id or "")),
182
+ )
180
183
 
181
184
  try:
182
- # NOTE: Start calling origin function with a passing args.
183
185
  return self.execute(params, result=result)
184
186
  except Exception as err:
185
- # NOTE: Start catching error from the stage execution.
186
187
  result.trace.error(f"[STAGE]: {err.__class__.__name__}: {err}")
188
+
187
189
  if config.stage_raise_error:
188
190
  # NOTE: If error that raise from stage execution course by
189
191
  # itself, it will return that error with previous
190
192
  # dependency.
191
193
  if isinstance(err, StageException):
192
- raise StageException(
193
- f"{self.__class__.__name__}: \n\t{err}"
194
- ) from err
194
+ raise
195
+
195
196
  raise StageException(
196
197
  f"{self.__class__.__name__}: \n\t"
197
198
  f"{err.__class__.__name__}: {err}"
@@ -202,8 +203,11 @@ class BaseStage(BaseModel, ABC):
202
203
  return result.catch(
203
204
  status=Status.FAILED,
204
205
  context={
205
- "error": err,
206
- "error_message": f"{err.__class__.__name__}: {err}",
206
+ "errors": {
207
+ "class": err,
208
+ "name": err.__class__.__name__,
209
+ "message": f"{err.__class__.__name__}: {err}",
210
+ },
207
211
  },
208
212
  )
209
213
 
@@ -247,8 +251,12 @@ class BaseStage(BaseModel, ABC):
247
251
  else gen_id(param2template(self.name, params=to))
248
252
  )
249
253
 
254
+ errors: DictData = (
255
+ {"errors": output.pop("errors", {})} if "errors" in output else {}
256
+ )
257
+
250
258
  # NOTE: Set the output to that stage generated ID with ``outputs`` key.
251
- to["stages"][_id] = {"outputs": output}
259
+ to["stages"][_id] = {"outputs": output, **errors}
252
260
  return to
253
261
 
254
262
  def is_skipped(self, params: DictData | None = None) -> bool:
@@ -321,6 +329,11 @@ class EmptyStage(BaseStage):
321
329
 
322
330
  :rtype: Result
323
331
  """
332
+ if result is None: # pragma: no cov
333
+ result: Result = Result(
334
+ run_id=gen_id(self.name + (self.id or ""), unique=True)
335
+ )
336
+
324
337
  result.trace.info(
325
338
  f"[STAGE]: Empty-Execute: {self.name!r}: "
326
339
  f"( {param2template(self.echo, params=params) or '...'} )"
@@ -413,6 +426,11 @@ class BashStage(BaseStage):
413
426
 
414
427
  :rtype: Result
415
428
  """
429
+ if result is None: # pragma: no cov
430
+ result: Result = Result(
431
+ run_id=gen_id(self.name + (self.id or ""), unique=True)
432
+ )
433
+
416
434
  bash: str = param2template(dedent(self.bash), params)
417
435
 
418
436
  result.trace.info(f"[STAGE]: Shell-Execute: {self.name}")
@@ -473,12 +491,24 @@ class PyStage(BaseStage):
473
491
  )
474
492
 
475
493
  @staticmethod
476
- def pick_keys_from_locals(values: DictData) -> Iterator[str]:
477
- from inspect import ismodule
494
+ def filter_locals(values: DictData) -> Iterator[str]:
495
+ """Filter a locals input values.
496
+
497
+ :param values: (DictData) A locals values that want to filter.
498
+
499
+ :rtype: Iterator[str]
500
+ """
501
+ from inspect import isclass, ismodule
478
502
 
479
503
  for value in values:
480
- if value == "__annotations__" or ismodule(values[value]):
504
+
505
+ if (
506
+ value == "__annotations__"
507
+ or ismodule(values[value])
508
+ or isclass(values[value])
509
+ ):
481
510
  continue
511
+
482
512
  yield value
483
513
 
484
514
  def set_outputs(self, output: DictData, to: DictData) -> DictData:
@@ -494,7 +524,7 @@ class PyStage(BaseStage):
494
524
  lc: DictData = output.get("locals", {})
495
525
  super().set_outputs(
496
526
  (
497
- {k: lc[k] for k in self.pick_keys_from_locals(lc)}
527
+ {k: lc[k] for k in self.filter_locals(lc)}
498
528
  | {k: output[k] for k in output if k.startswith("error")}
499
529
  ),
500
530
  to=to,
@@ -518,6 +548,11 @@ class PyStage(BaseStage):
518
548
 
519
549
  :rtype: Result
520
550
  """
551
+ if result is None: # pragma: no cov
552
+ result: Result = Result(
553
+ run_id=gen_id(self.name + (self.id or ""), unique=True)
554
+ )
555
+
521
556
  # NOTE: Replace the run statement that has templating value.
522
557
  run: str = param2template(dedent(self.run), params)
523
558
 
@@ -539,8 +574,8 @@ class PyStage(BaseStage):
539
574
  )
540
575
 
541
576
 
542
- class HookStage(BaseStage):
543
- """Hook executor that hook the Python function from registry with tag
577
+ class CallStage(BaseStage):
578
+ """Call executor that call the Python function from registry with tag
544
579
  decorator function in ``utils`` module and run it with input arguments.
545
580
 
546
581
  This stage is different with PyStage because the PyStage is just calling
@@ -558,23 +593,23 @@ class HookStage(BaseStage):
558
593
 
559
594
  uses: str = Field(
560
595
  description=(
561
- "A pointer that want to load function from the hook registry."
596
+ "A pointer that want to load function from the call registry."
562
597
  ),
563
598
  )
564
599
  args: DictData = Field(
565
600
  default_factory=dict,
566
- description="An arguments that want to pass to the hook function.",
601
+ description="An arguments that want to pass to the call function.",
567
602
  alias="with",
568
603
  )
569
604
 
570
605
  def execute(
571
606
  self, params: DictData, *, result: Result | None = None
572
607
  ) -> Result:
573
- """Execute the Hook function that already in the hook registry.
608
+ """Execute the Call function that already in the call registry.
574
609
 
575
- :raise ValueError: When the necessary arguments of hook function do not
610
+ :raise ValueError: When the necessary arguments of call function do not
576
611
  set from the input params argument.
577
- :raise TypeError: When the return type of hook function does not be
612
+ :raise TypeError: When the return type of call function does not be
578
613
  dict type.
579
614
 
580
615
  :param params: A parameter that want to pass before run any statement.
@@ -585,7 +620,12 @@ class HookStage(BaseStage):
585
620
 
586
621
  :rtype: Result
587
622
  """
588
- t_func: TagFunc = extract_hook(param2template(self.uses, params))()
623
+ if result is None: # pragma: no cov
624
+ result: Result = Result(
625
+ run_id=gen_id(self.name + (self.id or ""), unique=True)
626
+ )
627
+
628
+ t_func: TagFunc = extract_call(param2template(self.uses, params))()
589
629
 
590
630
  # VALIDATE: check input task caller parameters that exists before
591
631
  # calling.
@@ -608,11 +648,11 @@ class HookStage(BaseStage):
608
648
  if "result" not in ips.parameters:
609
649
  args.pop("result")
610
650
 
611
- result.trace.info(f"[STAGE]: Hook-Execute: {t_func.name}@{t_func.tag}")
651
+ result.trace.info(f"[STAGE]: Call-Execute: {t_func.name}@{t_func.tag}")
612
652
  rs: DictData = t_func(**param2template(args, params))
613
653
 
614
654
  # VALIDATE:
615
- # Check the result type from hook function, it should be dict.
655
+ # Check the result type from call function, it should be dict.
616
656
  if not isinstance(rs, dict):
617
657
  raise TypeError(
618
658
  f"Return type: '{t_func.name}@{t_func.tag}' does not serialize "
@@ -659,14 +699,19 @@ class TriggerStage(BaseStage):
659
699
  # NOTE: Lazy import this workflow object.
660
700
  from . import Workflow
661
701
 
702
+ if result is None: # pragma: no cov
703
+ result: Result = Result(
704
+ run_id=gen_id(self.name + (self.id or ""), unique=True)
705
+ )
706
+
662
707
  # NOTE: Loading workflow object from trigger name.
663
708
  _trigger: str = param2template(self.trigger, params=params)
664
709
 
665
710
  # NOTE: Set running workflow ID from running stage ID to external
666
711
  # params on Loader object.
667
- wf: Workflow = Workflow.from_loader(name=_trigger)
712
+ workflow: Workflow = Workflow.from_loader(name=_trigger)
668
713
  result.trace.info(f"[STAGE]: Trigger-Execute: {_trigger!r}")
669
- return wf.execute(
714
+ return workflow.execute(
670
715
  params=param2template(self.params, params),
671
716
  result=result,
672
717
  )
@@ -680,19 +725,37 @@ class TriggerStage(BaseStage):
680
725
  Stage = Union[
681
726
  PyStage,
682
727
  BashStage,
683
- HookStage,
728
+ CallStage,
684
729
  TriggerStage,
685
730
  EmptyStage,
686
731
  ]
687
732
 
688
733
 
689
734
  # TODO: Not implement this stages yet
690
- class ParallelStage(BaseModel): # pragma: no cov
735
+ class ParallelStage(BaseStage): # pragma: no cov
691
736
  parallel: list[Stage]
692
737
  max_parallel_core: int = Field(default=2)
693
738
 
739
+ def execute(
740
+ self, params: DictData, *, result: Result | None = None
741
+ ) -> Result: ...
742
+
743
+
744
+ # TODO: Not implement this stages yet
745
+ class ForEachStage(BaseStage): # pragma: no cov
746
+ foreach: list[str]
747
+ stages: list[Stage]
748
+
749
+ def execute(
750
+ self, params: DictData, *, result: Result | None = None
751
+ ) -> Result: ...
752
+
694
753
 
695
754
  # TODO: Not implement this stages yet
696
- class ForEachStage(BaseModel): # pragma: no cov
755
+ class HookStage(BaseStage): # pragma: no cov
697
756
  foreach: list[str]
698
757
  stages: list[Stage]
758
+
759
+ def execute(
760
+ self, params: DictData, *, result: Result | None = None
761
+ ) -> 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