ddeutil-workflow 0.0.32__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
@@ -38,12 +38,11 @@ from .exceptions import (
38
38
  StageException,
39
39
  UtilException,
40
40
  )
41
- from .result import Result
41
+ from .result import Result, Status
42
42
  from .stage import Stage
43
43
  from .templates import has_template
44
44
  from .utils import (
45
45
  cross_product,
46
- cut_id,
47
46
  dash2underscore,
48
47
  filter_func,
49
48
  gen_id,
@@ -222,6 +221,8 @@ class RunsOn(str, Enum):
222
221
 
223
222
  local: str = "local"
224
223
  docker: str = "docker"
224
+ self_hosted: str = "self_hosted"
225
+ k8s: str = "k8s"
225
226
 
226
227
 
227
228
  class Job(BaseModel):
@@ -412,6 +413,7 @@ class Job(BaseModel):
412
413
  params: DictData,
413
414
  *,
414
415
  run_id: str | None = None,
416
+ result: Result | None = None,
415
417
  event: Event | None = None,
416
418
  ) -> Result:
417
419
  """Job Strategy execution with passing dynamic parameters from the
@@ -431,13 +433,18 @@ class Job(BaseModel):
431
433
  This value will pass to the `matrix` key for templating.
432
434
  :param params: A dynamic parameters that will deepcopy to the context.
433
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.
434
438
  :param event: An event manager that pass to the PoolThreadExecutor.
435
439
 
436
440
  :rtype: Result
437
441
  """
438
- 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
+
439
447
  strategy_id: str = gen_id(strategy)
440
- rs: Result = Result(run_id=run_id)
441
448
 
442
449
  # PARAGRAPH:
443
450
  #
@@ -458,18 +465,14 @@ class Job(BaseModel):
458
465
  for stage in self.stages:
459
466
 
460
467
  if stage.is_skipped(params=context):
461
- logger.info(
462
- f"({cut_id(run_id)}) [JOB]: Skip stage: {stage.iden!r}"
463
- )
468
+ result.trace.info(f"[JOB]: Skip stage: {stage.iden!r}")
464
469
  continue
465
470
 
466
- logger.info(
467
- f"({cut_id(run_id)}) [JOB]: Execute stage: {stage.iden!r}"
468
- )
471
+ result.trace.info(f"[JOB]: Execute stage: {stage.iden!r}")
469
472
 
470
473
  # NOTE: Logging a matrix that pass on this stage execution.
471
474
  if strategy:
472
- logger.info(f"({cut_id(run_id)}) [JOB]: ... Matrix: {strategy}")
475
+ result.trace.info(f"[JOB]: ... Matrix: {strategy}")
473
476
 
474
477
  # NOTE: Force stop this execution if event was set from main
475
478
  # execution.
@@ -478,7 +481,7 @@ class Job(BaseModel):
478
481
  "Job strategy was canceled from event that had set before "
479
482
  "strategy execution."
480
483
  )
481
- return rs.catch(
484
+ return result.catch(
482
485
  status=1,
483
486
  context={
484
487
  strategy_id: {
@@ -516,20 +519,18 @@ class Job(BaseModel):
516
519
  try:
517
520
  stage.set_outputs(
518
521
  stage.handler_execute(
519
- params=context, run_id=run_id
522
+ params=context, run_id=result.run_id
520
523
  ).context,
521
524
  to=context,
522
525
  )
523
526
  except (StageException, UtilException) as err:
524
- logger.error(
525
- f"({cut_id(run_id)}) [JOB]: {err.__class__.__name__}: {err}"
526
- )
527
+ result.trace.error(f"[JOB]: {err.__class__.__name__}: {err}")
527
528
  if config.job_raise_error:
528
529
  raise JobException(
529
530
  f"Get stage execution error: {err.__class__.__name__}: "
530
531
  f"{err}"
531
532
  ) from None
532
- return rs.catch(
533
+ return result.catch(
533
534
  status=1,
534
535
  context={
535
536
  strategy_id: {
@@ -544,8 +545,8 @@ class Job(BaseModel):
544
545
  # NOTE: Remove the current stage object for saving memory.
545
546
  del stage
546
547
 
547
- return rs.catch(
548
- status=0,
548
+ return result.catch(
549
+ status=Status.SUCCESS,
549
550
  context={
550
551
  strategy_id: {
551
552
  "matrix": strategy,
@@ -554,36 +555,43 @@ class Job(BaseModel):
554
555
  },
555
556
  )
556
557
 
557
- 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:
558
565
  """Job execution with passing dynamic parameters from the workflow
559
566
  execution. It will generate matrix values at the first step and run
560
567
  multithread on this metrics to the ``stages`` field of this job.
561
568
 
562
569
  :param params: An input parameters that use on job execution.
563
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.
564
573
 
565
574
  :rtype: Result
566
575
  """
567
576
 
568
577
  # NOTE: I use this condition because this method allow passing empty
569
578
  # params and I do not want to create new dict object.
570
- run_id: str = run_id or gen_id(self.id or "", unique=True)
571
- 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)
572
582
 
573
583
  # NOTE: Normal Job execution without parallel strategy matrix. It uses
574
584
  # for-loop to control strategy execution sequentially.
575
585
  if (not self.strategy.is_set()) or self.strategy.max_parallel == 1:
586
+
576
587
  for strategy in self.strategy.make():
577
- rs: Result = self.execute_strategy(
588
+ result: Result = self.execute_strategy(
578
589
  strategy=strategy,
579
590
  params=params,
580
- run_id=run_id,
591
+ result=result,
581
592
  )
582
- context.update(rs.context)
583
- return Result(
584
- status=0,
585
- context=context,
586
- )
593
+
594
+ return result.catch(status=Status.SUCCESS)
587
595
 
588
596
  # NOTE: Create event for cancel executor by trigger stop running event.
589
597
  event: Event = Event()
@@ -600,23 +608,23 @@ class Job(BaseModel):
600
608
  self.execute_strategy,
601
609
  strategy=strategy,
602
610
  params=params,
603
- run_id=run_id,
611
+ result=result,
604
612
  event=event,
605
613
  )
606
614
  for strategy in self.strategy.make()
607
615
  ]
608
616
 
609
617
  return (
610
- self.__catch_fail_fast(event, futures=futures, run_id=run_id)
618
+ self.__catch_fail_fast(event, futures=futures, result=result)
611
619
  if self.strategy.fail_fast
612
- else self.__catch_all_completed(futures=futures, run_id=run_id)
620
+ else self.__catch_all_completed(futures=futures, result=result)
613
621
  )
614
622
 
615
623
  @staticmethod
616
624
  def __catch_fail_fast(
617
625
  event: Event,
618
626
  futures: list[Future],
619
- run_id: str,
627
+ result: Result,
620
628
  *,
621
629
  timeout: int = 1800,
622
630
  ) -> Result:
@@ -627,14 +635,14 @@ class Job(BaseModel):
627
635
  :param event: An event manager instance that able to set stopper on the
628
636
  observing multithreading.
629
637
  :param futures: A list of futures.
630
- :param run_id: A job running ID from execution.
638
+ :param result: (Result) A result object for keeping context and status
639
+ data.
631
640
  :param timeout: A timeout to waiting all futures complete.
632
641
 
633
642
  :rtype: Result
634
643
  """
635
- rs_final: Result = Result(run_id=run_id)
636
644
  context: DictData = {}
637
- status: int = 0
645
+ status: Status = Status.SUCCESS
638
646
 
639
647
  # NOTE: Get results from a collection of tasks with a timeout that has
640
648
  # the first exception.
@@ -644,7 +652,7 @@ class Job(BaseModel):
644
652
  nd: str = (
645
653
  f", the strategies do not run is {not_done}" if not_done else ""
646
654
  )
647
- 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}")
648
656
 
649
657
  # NOTE:
650
658
  # Stop all running tasks with setting the event manager and cancel
@@ -660,10 +668,9 @@ class Job(BaseModel):
660
668
 
661
669
  # NOTE: Handle the first exception from feature
662
670
  if err := future.exception():
663
- status: int = 1
664
- logger.error(
665
- f"({cut_id(run_id)}) [JOB]: Fail-fast catching:\n\t"
666
- f"{future.exception()}"
671
+ status: Status = Status.FAILED
672
+ result.trace.error(
673
+ f"[JOB]: Fail-fast catching:\n\t{future.exception()}"
667
674
  )
668
675
  context.update(
669
676
  {
@@ -674,36 +681,36 @@ class Job(BaseModel):
674
681
  continue
675
682
 
676
683
  # NOTE: Update the result context to main job context.
677
- context.update(future.result().context)
684
+ future.result()
678
685
 
679
- return rs_final.catch(status=status, context=context)
686
+ return result.catch(status=status, context=context)
680
687
 
681
688
  @staticmethod
682
689
  def __catch_all_completed(
683
690
  futures: list[Future],
684
- run_id: str,
691
+ result: Result,
685
692
  *,
686
693
  timeout: int = 1800,
687
694
  ) -> Result:
688
695
  """Job parallel pool futures catching with all-completed mode.
689
696
 
690
697
  :param futures: A list of futures.
691
- :param run_id: A job running ID from execution.
698
+ :param result: (Result) A result object for keeping context and status
699
+ data.
692
700
  :param timeout: A timeout to waiting all futures complete.
693
701
 
694
702
  :rtype: Result
695
703
  """
696
- rs_final: Result = Result(run_id=run_id)
697
704
  context: DictData = {}
698
- status: int = 0
705
+ status: Status = Status.SUCCESS
699
706
 
700
707
  for future in as_completed(futures, timeout=timeout):
701
708
  try:
702
- context.update(future.result().context)
709
+ future.result()
703
710
  except JobException as err:
704
- status = 1
705
- logger.error(
706
- 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"
707
714
  f"{err.__class__.__name__}:\n\t{err}"
708
715
  )
709
716
  context.update(
@@ -713,4 +720,4 @@ class Job(BaseModel):
713
720
  },
714
721
  )
715
722
 
716
- return rs_final.catch(status=status, context=context)
723
+ return result.catch(status=status, context=context)
@@ -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()
@@ -51,7 +51,8 @@ except ImportError: # pragma: no cov
51
51
 
52
52
  from .__cron import CronRunner
53
53
  from .__types import DictData, TupleStr
54
- from .conf import Loader, Log, config, get_log, get_logger
54
+ from .audit import Audit, get_audit
55
+ from .conf import Loader, config, get_logger
55
56
  from .cron import On
56
57
  from .exceptions import ScheduleException, WorkflowException
57
58
  from .result import Result
@@ -313,7 +314,7 @@ class Schedule(BaseModel):
313
314
  *,
314
315
  stop: datetime | None = None,
315
316
  externals: DictData | None = None,
316
- log: type[Log] | None = None,
317
+ log: type[Audit] | None = None,
317
318
  ) -> None: # pragma: no cov
318
319
  """Pending this schedule tasks with the schedule package.
319
320
 
@@ -330,7 +331,7 @@ class Schedule(BaseModel):
330
331
  ) from None
331
332
 
332
333
  # NOTE: Get default logging.
333
- log: type[Log] = log or get_log()
334
+ log: type[Audit] = log or get_audit()
334
335
  scheduler: Scheduler = Scheduler()
335
336
 
336
337
  # NOTE: Create the start and stop datetime.
@@ -450,7 +451,7 @@ def schedule_task(
450
451
  stop: datetime,
451
452
  queue: dict[str, ReleaseQueue],
452
453
  threads: ReleaseThreads,
453
- log: type[Log],
454
+ log: type[Audit],
454
455
  ) -> type[CancelJob] | None:
455
456
  """Schedule task function that generate thread of workflow task release
456
457
  method in background. This function do the same logic as the workflow poke
@@ -573,7 +574,7 @@ def schedule_control(
573
574
  stop: datetime | None = None,
574
575
  externals: DictData | None = None,
575
576
  *,
576
- log: type[Log] | None = None,
577
+ log: type[Audit] | None = None,
577
578
  ) -> list[str]: # pragma: no cov
578
579
  """Scheduler control function that run the chuck of schedules every minute
579
580
  and this function release monitoring thread for tracking undead thread in
@@ -596,7 +597,7 @@ def schedule_control(
596
597
  ) from None
597
598
 
598
599
  # NOTE: Get default logging.
599
- log: type[Log] = log or get_log()
600
+ log: type[Audit] = log or get_audit()
600
601
  scheduler: Scheduler = Scheduler()
601
602
 
602
603
  # NOTE: Create the start and stop datetime.