ddeutil-workflow 0.0.31__py3-none-any.whl → 0.0.33__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.
ddeutil/workflow/job.py CHANGED
@@ -7,6 +7,8 @@
7
7
  The job handle the lineage of stages and location of execution of stages that
8
8
  mean the job model able to define ``runs-on`` key that allow you to run this
9
9
  job.
10
+
11
+ This module include Strategy Model that use on the job strategy field.
10
12
  """
11
13
  from __future__ import annotations
12
14
 
@@ -36,12 +38,11 @@ from .exceptions import (
36
38
  StageException,
37
39
  UtilException,
38
40
  )
39
- from .result import Result
41
+ from .result import Result, Status
40
42
  from .stage import Stage
41
43
  from .templates import has_template
42
44
  from .utils import (
43
45
  cross_product,
44
- cut_id,
45
46
  dash2underscore,
46
47
  filter_func,
47
48
  gen_id,
@@ -220,6 +221,8 @@ class RunsOn(str, Enum):
220
221
 
221
222
  local: str = "local"
222
223
  docker: str = "docker"
224
+ self_hosted: str = "self_hosted"
225
+ k8s: str = "k8s"
223
226
 
224
227
 
225
228
  class Job(BaseModel):
@@ -410,6 +413,7 @@ class Job(BaseModel):
410
413
  params: DictData,
411
414
  *,
412
415
  run_id: str | None = None,
416
+ result: Result | None = None,
413
417
  event: Event | None = None,
414
418
  ) -> Result:
415
419
  """Job Strategy execution with passing dynamic parameters from the
@@ -429,13 +433,18 @@ class Job(BaseModel):
429
433
  This value will pass to the `matrix` key for templating.
430
434
  :param params: A dynamic parameters that will deepcopy to the context.
431
435
  :param run_id: A job running ID for this strategy execution.
436
+ :param result: (Result) A result object for keeping context and status
437
+ data.
432
438
  :param event: An event manager that pass to the PoolThreadExecutor.
433
439
 
434
440
  :rtype: Result
435
441
  """
436
- run_id: str = run_id or gen_id(self.id or "", unique=True)
442
+ if result is None: # pragma: no cov
443
+ result: Result = Result(
444
+ run_id=(run_id or gen_id(self.id or "", unique=True))
445
+ )
446
+
437
447
  strategy_id: str = gen_id(strategy)
438
- rs: Result = Result(run_id=run_id)
439
448
 
440
449
  # PARAGRAPH:
441
450
  #
@@ -456,18 +465,14 @@ class Job(BaseModel):
456
465
  for stage in self.stages:
457
466
 
458
467
  if stage.is_skipped(params=context):
459
- logger.info(
460
- f"({cut_id(run_id)}) [JOB]: Skip stage: {stage.iden!r}"
461
- )
468
+ result.trace.info(f"[JOB]: Skip stage: {stage.iden!r}")
462
469
  continue
463
470
 
464
- logger.info(
465
- f"({cut_id(run_id)}) [JOB]: Execute stage: {stage.iden!r}"
466
- )
471
+ result.trace.info(f"[JOB]: Execute stage: {stage.iden!r}")
467
472
 
468
473
  # NOTE: Logging a matrix that pass on this stage execution.
469
474
  if strategy:
470
- logger.info(f"({cut_id(run_id)}) [JOB]: ... Matrix: {strategy}")
475
+ result.trace.info(f"[JOB]: ... Matrix: {strategy}")
471
476
 
472
477
  # NOTE: Force stop this execution if event was set from main
473
478
  # execution.
@@ -476,7 +481,7 @@ class Job(BaseModel):
476
481
  "Job strategy was canceled from event that had set before "
477
482
  "strategy execution."
478
483
  )
479
- return rs.catch(
484
+ return result.catch(
480
485
  status=1,
481
486
  context={
482
487
  strategy_id: {
@@ -514,20 +519,18 @@ class Job(BaseModel):
514
519
  try:
515
520
  stage.set_outputs(
516
521
  stage.handler_execute(
517
- params=context, run_id=run_id
522
+ params=context, run_id=result.run_id
518
523
  ).context,
519
524
  to=context,
520
525
  )
521
526
  except (StageException, UtilException) as err:
522
- logger.error(
523
- f"({cut_id(run_id)}) [JOB]: {err.__class__.__name__}: {err}"
524
- )
527
+ result.trace.error(f"[JOB]: {err.__class__.__name__}: {err}")
525
528
  if config.job_raise_error:
526
529
  raise JobException(
527
530
  f"Get stage execution error: {err.__class__.__name__}: "
528
531
  f"{err}"
529
532
  ) from None
530
- return rs.catch(
533
+ return result.catch(
531
534
  status=1,
532
535
  context={
533
536
  strategy_id: {
@@ -542,8 +545,8 @@ class Job(BaseModel):
542
545
  # NOTE: Remove the current stage object for saving memory.
543
546
  del stage
544
547
 
545
- return rs.catch(
546
- status=0,
548
+ return result.catch(
549
+ status=Status.SUCCESS,
547
550
  context={
548
551
  strategy_id: {
549
552
  "matrix": strategy,
@@ -552,36 +555,43 @@ class Job(BaseModel):
552
555
  },
553
556
  )
554
557
 
555
- def execute(self, params: DictData, run_id: str | None = None) -> Result:
558
+ def execute(
559
+ self,
560
+ params: DictData,
561
+ *,
562
+ run_id: str | None = None,
563
+ result: Result | None = None,
564
+ ) -> Result:
556
565
  """Job execution with passing dynamic parameters from the workflow
557
566
  execution. It will generate matrix values at the first step and run
558
567
  multithread on this metrics to the ``stages`` field of this job.
559
568
 
560
569
  :param params: An input parameters that use on job execution.
561
570
  :param run_id: A job running ID for this execution.
571
+ :param result: (Result) A result object for keeping context and status
572
+ data.
562
573
 
563
574
  :rtype: Result
564
575
  """
565
576
 
566
577
  # NOTE: I use this condition because this method allow passing empty
567
578
  # params and I do not want to create new dict object.
568
- run_id: str = run_id or gen_id(self.id or "", unique=True)
569
- context: DictData = {}
579
+ if result is None: # pragma: no cov
580
+ run_id: str = run_id or gen_id(self.id or "", unique=True)
581
+ result: Result = Result(run_id=run_id)
570
582
 
571
583
  # NOTE: Normal Job execution without parallel strategy matrix. It uses
572
584
  # for-loop to control strategy execution sequentially.
573
585
  if (not self.strategy.is_set()) or self.strategy.max_parallel == 1:
586
+
574
587
  for strategy in self.strategy.make():
575
- rs: Result = self.execute_strategy(
588
+ result: Result = self.execute_strategy(
576
589
  strategy=strategy,
577
590
  params=params,
578
- run_id=run_id,
591
+ result=result,
579
592
  )
580
- context.update(rs.context)
581
- return Result(
582
- status=0,
583
- context=context,
584
- )
593
+
594
+ return result.catch(status=Status.SUCCESS)
585
595
 
586
596
  # NOTE: Create event for cancel executor by trigger stop running event.
587
597
  event: Event = Event()
@@ -598,23 +608,23 @@ class Job(BaseModel):
598
608
  self.execute_strategy,
599
609
  strategy=strategy,
600
610
  params=params,
601
- run_id=run_id,
611
+ result=result,
602
612
  event=event,
603
613
  )
604
614
  for strategy in self.strategy.make()
605
615
  ]
606
616
 
607
617
  return (
608
- self.__catch_fail_fast(event, futures=futures, run_id=run_id)
618
+ self.__catch_fail_fast(event, futures=futures, result=result)
609
619
  if self.strategy.fail_fast
610
- else self.__catch_all_completed(futures=futures, run_id=run_id)
620
+ else self.__catch_all_completed(futures=futures, result=result)
611
621
  )
612
622
 
613
623
  @staticmethod
614
624
  def __catch_fail_fast(
615
625
  event: Event,
616
626
  futures: list[Future],
617
- run_id: str,
627
+ result: Result,
618
628
  *,
619
629
  timeout: int = 1800,
620
630
  ) -> Result:
@@ -625,14 +635,14 @@ class Job(BaseModel):
625
635
  :param event: An event manager instance that able to set stopper on the
626
636
  observing multithreading.
627
637
  :param futures: A list of futures.
628
- :param run_id: A job running ID from execution.
638
+ :param result: (Result) A result object for keeping context and status
639
+ data.
629
640
  :param timeout: A timeout to waiting all futures complete.
630
641
 
631
642
  :rtype: Result
632
643
  """
633
- rs_final: Result = Result(run_id=run_id)
634
644
  context: DictData = {}
635
- status: int = 0
645
+ status: Status = Status.SUCCESS
636
646
 
637
647
  # NOTE: Get results from a collection of tasks with a timeout that has
638
648
  # the first exception.
@@ -642,7 +652,7 @@ class Job(BaseModel):
642
652
  nd: str = (
643
653
  f", the strategies do not run is {not_done}" if not_done else ""
644
654
  )
645
- logger.debug(f"({cut_id(run_id)}) [JOB]: Strategy is set Fail Fast{nd}")
655
+ result.trace.debug(f"[JOB]: Strategy is set Fail Fast{nd}")
646
656
 
647
657
  # NOTE:
648
658
  # Stop all running tasks with setting the event manager and cancel
@@ -658,10 +668,9 @@ class Job(BaseModel):
658
668
 
659
669
  # NOTE: Handle the first exception from feature
660
670
  if err := future.exception():
661
- status: int = 1
662
- logger.error(
663
- f"({cut_id(run_id)}) [JOB]: Fail-fast catching:\n\t"
664
- f"{future.exception()}"
671
+ status: Status = Status.FAILED
672
+ result.trace.error(
673
+ f"[JOB]: Fail-fast catching:\n\t{future.exception()}"
665
674
  )
666
675
  context.update(
667
676
  {
@@ -672,36 +681,36 @@ class Job(BaseModel):
672
681
  continue
673
682
 
674
683
  # NOTE: Update the result context to main job context.
675
- context.update(future.result().context)
684
+ future.result()
676
685
 
677
- return rs_final.catch(status=status, context=context)
686
+ return result.catch(status=status, context=context)
678
687
 
679
688
  @staticmethod
680
689
  def __catch_all_completed(
681
690
  futures: list[Future],
682
- run_id: str,
691
+ result: Result,
683
692
  *,
684
693
  timeout: int = 1800,
685
694
  ) -> Result:
686
695
  """Job parallel pool futures catching with all-completed mode.
687
696
 
688
697
  :param futures: A list of futures.
689
- :param run_id: A job running ID from execution.
698
+ :param result: (Result) A result object for keeping context and status
699
+ data.
690
700
  :param timeout: A timeout to waiting all futures complete.
691
701
 
692
702
  :rtype: Result
693
703
  """
694
- rs_final: Result = Result(run_id=run_id)
695
704
  context: DictData = {}
696
- status: int = 0
705
+ status: Status = Status.SUCCESS
697
706
 
698
707
  for future in as_completed(futures, timeout=timeout):
699
708
  try:
700
- context.update(future.result().context)
709
+ future.result()
701
710
  except JobException as err:
702
- status = 1
703
- logger.error(
704
- f"({cut_id(run_id)}) [JOB]: All-completed catching:\n\t"
711
+ status = Status.FAILED
712
+ result.trace.error(
713
+ f"[JOB]: All-completed catching:\n\t"
705
714
  f"{err.__class__.__name__}:\n\t{err}"
706
715
  )
707
716
  context.update(
@@ -711,4 +720,4 @@ class Job(BaseModel):
711
720
  },
712
721
  )
713
722
 
714
- return rs_final.catch(status=status, context=context)
723
+ return result.catch(status=status, context=context)
@@ -3,6 +3,9 @@
3
3
  # Licensed under the MIT License. See LICENSE in the project root for
4
4
  # license information.
5
5
  # ------------------------------------------------------------------------------
6
+ """Param Model that use for parsing incoming parameters that pass to the
7
+ Workflow and Schedule objects.
8
+ """
6
9
  from __future__ import annotations
7
10
 
8
11
  import decimal
@@ -70,7 +73,7 @@ class DefaultParam(BaseParam):
70
73
 
71
74
 
72
75
  # TODO: Not implement this parameter yet
73
- class DateParam(DefaultParam):
76
+ class DateParam(DefaultParam): # pragma: no cov
74
77
  """Date parameter."""
75
78
 
76
79
  type: Literal["date"] = "date"
@@ -156,7 +159,7 @@ class IntParam(DefaultParam):
156
159
 
157
160
 
158
161
  # TODO: Not implement this parameter yet
159
- class DecimalParam(DefaultParam):
162
+ class DecimalParam(DefaultParam): # pragma: no cov
160
163
  type: Literal["decimal"] = "decimal"
161
164
 
162
165
  def receive(self, value: float | None = None) -> decimal.Decimal: ...
@@ -6,19 +6,76 @@
6
6
  from __future__ import annotations
7
7
 
8
8
  from dataclasses import field
9
+ from datetime import datetime
10
+ from enum import IntEnum
11
+ from threading import Event
9
12
  from typing import Optional
10
13
 
14
+ from pydantic import ConfigDict
11
15
  from pydantic.dataclasses import dataclass
12
- from pydantic.functional_validators import model_validator
13
16
  from typing_extensions import Self
14
17
 
15
18
  from .__types import DictData, TupleStr
16
- from .utils import gen_id
19
+ from .conf import config, get_logger
20
+ from .utils import cut_id, gen_id, get_dt_now
17
21
 
18
- __all__: TupleStr = ("Result",)
22
+ logger = get_logger("ddeutil.workflow.audit")
19
23
 
24
+ __all__: TupleStr = (
25
+ "Result",
26
+ "Status",
27
+ )
20
28
 
21
- @dataclass
29
+
30
+ def default_gen_id() -> str:
31
+ """Return running ID which use for making default ID for the Result model if
32
+ a run_id field initializes at the first time.
33
+
34
+ :rtype: str
35
+ """
36
+ return gen_id("manual", unique=True)
37
+
38
+
39
+ def get_dt_tznow() -> datetime:
40
+ """Return the current datetime object that passing the config timezone.
41
+
42
+ :rtype: datetime
43
+ """
44
+ return get_dt_now(tz=config.tz)
45
+
46
+
47
+ class Status(IntEnum):
48
+ """Status Int Enum object."""
49
+
50
+ SUCCESS: int = 0
51
+ FAILED: int = 1
52
+ WAIT: int = 2
53
+
54
+
55
+ class TraceLog: # pragma: no cov
56
+ """Trace Log object."""
57
+
58
+ __slots__: TupleStr = ("run_id",)
59
+
60
+ def __init__(self, run_id: str):
61
+ self.run_id: str = run_id
62
+
63
+ def debug(self, message: str):
64
+ logger.debug(f"({cut_id(self.run_id)}) {message}")
65
+
66
+ def info(self, message: str):
67
+ logger.info(f"({cut_id(self.run_id)}) {message}")
68
+
69
+ def warning(self, message: str):
70
+ logger.warning(f"({cut_id(self.run_id)}) {message}")
71
+
72
+ def error(self, message: str):
73
+ logger.error(f"({cut_id(self.run_id)}) {message}")
74
+
75
+
76
+ @dataclass(
77
+ config=ConfigDict(arbitrary_types_allowed=True, use_enum_values=True)
78
+ )
22
79
  class Result:
23
80
  """Result Pydantic Model for passing and receiving data context from any
24
81
  module execution process like stage execution, job execution, or workflow
@@ -28,22 +85,14 @@ class Result:
28
85
  and ``_run_id`` fields to comparing with other result instance.
29
86
  """
30
87
 
31
- status: int = field(default=2)
88
+ status: Status = field(default=Status.WAIT)
32
89
  context: DictData = field(default_factory=dict)
33
- run_id: Optional[str] = field(default=None)
90
+ run_id: Optional[str] = field(default_factory=default_gen_id)
34
91
 
35
92
  # NOTE: Ignore this field to compare another result model with __eq__.
36
93
  parent_run_id: Optional[str] = field(default=None, compare=False)
37
-
38
- @model_validator(mode="after")
39
- def __prepare_run_id(self) -> Self:
40
- """Prepare running ID which use default ID if it initializes at the
41
- first time.
42
-
43
- :rtype: Self
44
- """
45
- self._run_id = gen_id("manual", unique=True)
46
- return self
94
+ event: Event = field(default_factory=Event, compare=False)
95
+ ts: datetime = field(default_factory=get_dt_tznow, compare=False)
47
96
 
48
97
  def set_run_id(self, running_id: str) -> Self:
49
98
  """Set a running ID.
@@ -51,7 +100,7 @@ class Result:
51
100
  :param running_id: A running ID that want to update on this model.
52
101
  :rtype: Self
53
102
  """
54
- self.run_id = running_id
103
+ self.run_id: str = running_id
55
104
  return self
56
105
 
57
106
  def set_parent_run_id(self, running_id: str) -> Self:
@@ -63,10 +112,22 @@ class Result:
63
112
  self.parent_run_id: str = running_id
64
113
  return self
65
114
 
66
- def catch(self, status: int, context: DictData) -> Self:
67
- """Catch the status and context to current data."""
68
- self.__dict__["status"] = status
69
- self.__dict__["context"].update(context)
115
+ def catch(
116
+ self,
117
+ status: int | Status,
118
+ context: DictData | None = None,
119
+ ) -> Self:
120
+ """Catch the status and context to this Result object. This method will
121
+ use between a child execution return a result, and it wants to pass
122
+ status and context to this object.
123
+
124
+ :param status:
125
+ :param context:
126
+ """
127
+ self.__dict__["status"] = (
128
+ Status(status) if isinstance(status, int) else status
129
+ )
130
+ self.__dict__["context"].update(context or {})
70
131
  return self
71
132
 
72
133
  def receive(self, result: Result) -> Self:
@@ -82,22 +143,13 @@ class Result:
82
143
  self.run_id = result.run_id
83
144
  return self
84
145
 
85
- def receive_jobs(self, result: Result) -> Self:
86
- """Receive context from another result object that use on the workflow
87
- execution which create a ``jobs`` keys on the context if it does not
88
- exist.
146
+ @property
147
+ def trace(self) -> TraceLog:
148
+ """Return TraceLog object that passing its running ID.
89
149
 
90
- :rtype: Self
150
+ :rtype: TraceLog
91
151
  """
92
- self.__dict__["status"] = result.status
152
+ return TraceLog(self.run_id)
93
153
 
94
- # NOTE: Check the context has jobs key.
95
- if "jobs" not in self.__dict__["context"]:
96
- self.__dict__["context"]["jobs"] = {}
97
-
98
- self.__dict__["context"]["jobs"].update(result.context)
99
-
100
- # NOTE: Update running ID from an incoming result.
101
- self.parent_run_id: str = result.parent_run_id
102
- self.run_id: str = result.run_id
103
- return self
154
+ def alive_time(self) -> float: # pragma: no cov
155
+ return (get_dt_tznow() - self.ts).total_seconds()