ddeutil-workflow 0.0.35__py3-none-any.whl → 0.0.37__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
@@ -32,7 +32,7 @@ from pydantic.functional_validators import field_validator, model_validator
32
32
  from typing_extensions import Self
33
33
 
34
34
  from .__types import DictData, DictStr, Matrix, TupleStr
35
- from .conf import config, get_logger
35
+ from .conf import config
36
36
  from .exceptions import (
37
37
  JobException,
38
38
  StageException,
@@ -48,7 +48,6 @@ from .utils import (
48
48
  gen_id,
49
49
  )
50
50
 
51
- logger = get_logger("ddeutil.workflow")
52
51
  MatrixFilter = list[dict[str, Union[str, int]]]
53
52
 
54
53
 
@@ -59,9 +58,10 @@ __all__: TupleStr = (
59
58
  "RunsOn",
60
59
  "RunsOnLocal",
61
60
  "RunsOnSelfHosted",
62
- "RunsOnDocker",
63
61
  "RunsOnK8s",
64
62
  "make",
63
+ "local_execute_strategy",
64
+ "local_execute",
65
65
  )
66
66
 
67
67
 
@@ -225,12 +225,15 @@ class RunsOnType(str, Enum):
225
225
  """Runs-On enum object."""
226
226
 
227
227
  LOCAL: str = "local"
228
- DOCKER: str = "docker"
229
228
  SELF_HOSTED: str = "self_hosted"
230
229
  K8S: str = "k8s"
231
230
 
232
231
 
233
- class BaseRunsOn(BaseModel):
232
+ class BaseRunsOn(BaseModel): # pragma: no cov
233
+ """Base Runs-On Model for generate runs-on types via inherit this model
234
+ object and override execute method.
235
+ """
236
+
234
237
  model_config = ConfigDict(use_enum_values=True)
235
238
 
236
239
  type: Literal[RunsOnType.LOCAL]
@@ -240,27 +243,26 @@ class BaseRunsOn(BaseModel):
240
243
  )
241
244
 
242
245
 
243
- class RunsOnLocal(BaseRunsOn):
246
+ class RunsOnLocal(BaseRunsOn): # pragma: no cov
244
247
  """Runs-on local."""
245
248
 
246
249
  type: Literal[RunsOnType.LOCAL] = Field(default=RunsOnType.LOCAL)
247
250
 
248
251
 
249
- class RunsOnSelfHosted(BaseRunsOn):
252
+ class SelfHostedArgs(BaseModel):
253
+ host: str
254
+
255
+
256
+ class RunsOnSelfHosted(BaseRunsOn): # pragma: no cov
250
257
  """Runs-on self-hosted."""
251
258
 
252
259
  type: Literal[RunsOnType.SELF_HOSTED] = Field(
253
260
  default=RunsOnType.SELF_HOSTED
254
261
  )
262
+ args: SelfHostedArgs = Field(alias="with")
255
263
 
256
264
 
257
- class RunsOnDocker(BaseRunsOn):
258
- """Runs-on local Docker."""
259
-
260
- type: Literal[RunsOnType.DOCKER] = Field(default=RunsOnType.DOCKER)
261
-
262
-
263
- class RunsOnK8s(BaseRunsOn):
265
+ class RunsOnK8s(BaseRunsOn): # pragma: no cov
264
266
  """Runs-on Kubernetes."""
265
267
 
266
268
  type: Literal[RunsOnType.K8S] = Field(default=RunsOnType.K8S)
@@ -270,7 +272,6 @@ RunsOn = Annotated[
270
272
  Union[
271
273
  RunsOnLocal,
272
274
  RunsOnSelfHosted,
273
- RunsOnDocker,
274
275
  RunsOnK8s,
275
276
  ],
276
277
  Field(discriminator="type"),
@@ -286,7 +287,7 @@ class Job(BaseModel):
286
287
 
287
288
  Data Validate:
288
289
  >>> job = {
289
- ... "runs-on": None,
290
+ ... "runs-on": {"type": "local"},
290
291
  ... "strategy": {
291
292
  ... "max-parallel": 1,
292
293
  ... "matrix": {
@@ -464,271 +465,314 @@ class Job(BaseModel):
464
465
  )
465
466
  return to
466
467
 
467
- def execute_strategy(
468
+ def execute(
468
469
  self,
469
- strategy: DictData,
470
470
  params: DictData,
471
471
  *,
472
+ run_id: str | None = None,
473
+ parent_run_id: str | None = None,
472
474
  result: Result | None = None,
473
- event: Event | None = None,
474
475
  ) -> Result:
475
- """Job Strategy execution with passing dynamic parameters from the
476
- workflow execution to strategy matrix.
477
-
478
- This execution is the minimum level of execution of this job model.
479
- It different with `self.execute` because this method run only one
480
- strategy and return with context of this strategy data.
481
-
482
- The result of this execution will return result with strategy ID
483
- that generated from the `gen_id` function with an input strategy value.
484
-
485
- :raise JobException: If it has any error from `StageException` or
486
- `UtilException`.
476
+ """Job execution with passing dynamic parameters from the workflow
477
+ execution. It will generate matrix values at the first step and run
478
+ multithread on this metrics to the `stages` field of this job.
487
479
 
488
- :param strategy: A strategy metrix value that use on this execution.
489
- This value will pass to the `matrix` key for templating.
490
- :param params: A dynamic parameters that will deepcopy to the context.
480
+ :param params: An input parameters that use on job execution.
481
+ :param run_id: A job running ID for this execution.
482
+ :param parent_run_id: A parent workflow running ID for this release.
491
483
  :param result: (Result) A result object for keeping context and status
492
484
  data.
493
- :param event: An event manager that pass to the PoolThreadExecutor.
494
485
 
495
486
  :rtype: Result
496
487
  """
497
488
  if result is None: # pragma: no cov
498
- result: Result = Result(run_id=gen_id(self.id or "", unique=True))
499
-
500
- strategy_id: str = gen_id(strategy)
489
+ result: Result = Result(
490
+ run_id=(run_id or gen_id(self.id or "", unique=True)),
491
+ parent_run_id=parent_run_id,
492
+ )
493
+ elif parent_run_id: # pragma: no cov
494
+ result.set_parent_run_id(parent_run_id)
501
495
 
502
- # PARAGRAPH:
503
- #
504
- # Create strategy execution context and update a matrix and copied
505
- # of params. So, the context value will have structure like;
506
- #
507
- # {
508
- # "params": { ... }, <== Current input params
509
- # "jobs": { ... }, <== Current input params
510
- # "matrix": { ... } <== Current strategy value
511
- # "stages": { ... } <== Catching stage outputs
512
- # }
513
- #
514
- context: DictData = copy.deepcopy(params)
515
- context.update({"matrix": strategy, "stages": {}})
496
+ if self.runs_on.type == RunsOnType.LOCAL:
497
+ return local_execute(
498
+ job=self,
499
+ params=params,
500
+ result=result,
501
+ )
502
+ raise NotImplementedError(
503
+ f"The job runs-on other type: {self.runs_on.type} does not "
504
+ f"support yet."
505
+ )
516
506
 
517
- # IMPORTANT: The stage execution only run sequentially one-by-one.
518
- for stage in self.stages:
519
507
 
520
- if stage.is_skipped(params=context):
521
- result.trace.info(f"[JOB]: Skip stage: {stage.iden!r}")
522
- continue
508
+ def local_execute_strategy(
509
+ job: Job,
510
+ strategy: DictData,
511
+ params: DictData,
512
+ *,
513
+ result: Result | None = None,
514
+ event: Event | None = None,
515
+ ) -> Result:
516
+ """Local job strategy execution with passing dynamic parameters from the
517
+ workflow execution to strategy matrix.
518
+
519
+ This execution is the minimum level of execution of this job model.
520
+ It different with `self.execute` because this method run only one
521
+ strategy and return with context of this strategy data.
522
+
523
+ The result of this execution will return result with strategy ID
524
+ that generated from the `gen_id` function with an input strategy value.
525
+
526
+ :raise JobException: If it has any error from `StageException` or
527
+ `UtilException`.
528
+
529
+ :param job: (Job) A job model that want to execute.
530
+ :param strategy: A strategy metrix value that use on this execution.
531
+ This value will pass to the `matrix` key for templating.
532
+ :param params: A dynamic parameters that will deepcopy to the context.
533
+ :param result: (Result) A result object for keeping context and status
534
+ data.
535
+ :param event: (Event) An event manager that pass to the PoolThreadExecutor.
536
+
537
+ :rtype: Result
538
+ """
539
+ if result is None: # pragma: no cov
540
+ result: Result = Result(run_id=gen_id(job.id or "", unique=True))
541
+
542
+ strategy_id: str = gen_id(strategy)
543
+
544
+ # PARAGRAPH:
545
+ #
546
+ # Create strategy execution context and update a matrix and copied
547
+ # of params. So, the context value will have structure like;
548
+ #
549
+ # {
550
+ # "params": { ... }, <== Current input params
551
+ # "jobs": { ... }, <== Current input params
552
+ # "matrix": { ... } <== Current strategy value
553
+ # "stages": { ... } <== Catching stage outputs
554
+ # }
555
+ #
556
+ context: DictData = copy.deepcopy(params)
557
+ context.update({"matrix": strategy, "stages": {}})
558
+
559
+ # IMPORTANT: The stage execution only run sequentially one-by-one.
560
+ for stage in job.stages:
561
+
562
+ if stage.is_skipped(params=context):
563
+ result.trace.info(f"[JOB]: Skip stage: {stage.iden!r}")
564
+ continue
523
565
 
524
- result.trace.info(f"[JOB]: Execute stage: {stage.iden!r}")
566
+ result.trace.info(f"[JOB]: Execute stage: {stage.iden!r}")
525
567
 
526
- # NOTE: Logging a matrix that pass on this stage execution.
527
- if strategy:
528
- result.trace.info(f"[JOB]: ... Matrix: {strategy}")
568
+ # NOTE: Logging a matrix that pass on this stage execution.
569
+ if strategy:
570
+ result.trace.info(f"[JOB]: ... Matrix: {strategy}")
529
571
 
530
- # NOTE: Force stop this execution if event was set from main
531
- # execution.
532
- if event and event.is_set():
533
- error_msg: str = (
534
- "Job strategy was canceled from event that had set before "
535
- "strategy execution."
536
- )
537
- return result.catch(
538
- status=1,
539
- context={
540
- strategy_id: {
541
- "matrix": strategy,
542
- # NOTE: If job strategy executor use multithreading,
543
- # it will not filter function object from context.
544
- # ---
545
- # "stages": filter_func(context.pop("stages", {})),
546
- #
547
- "stages": context.pop("stages", {}),
548
- "errors": {
549
- "class": JobException(error_msg),
550
- "name": "JobException",
551
- "message": error_msg,
552
- },
572
+ # NOTE: Force stop this execution if event was set from main
573
+ # execution.
574
+ if event and event.is_set():
575
+ error_msg: str = (
576
+ "Job strategy was canceled from event that had set before "
577
+ "strategy execution."
578
+ )
579
+ return result.catch(
580
+ status=1,
581
+ context={
582
+ strategy_id: {
583
+ "matrix": strategy,
584
+ # NOTE: If job strategy executor use multithreading,
585
+ # it will not filter function object from context.
586
+ # ---
587
+ # "stages": filter_func(context.pop("stages", {})),
588
+ #
589
+ "stages": context.pop("stages", {}),
590
+ "errors": {
591
+ "class": JobException(error_msg),
592
+ "name": "JobException",
593
+ "message": error_msg,
553
594
  },
554
595
  },
555
- )
596
+ },
597
+ )
556
598
 
557
- # PARAGRAPH:
558
- #
559
- # I do not use below syntax because `params` dict be the
560
- # reference memory pointer, and it was changed when I action
561
- # anything like update or re-construct this.
562
- #
563
- # ... params |= stage.execute(params=params)
564
- #
565
- # This step will add the stage result to `stages` key in
566
- # that stage id. It will have structure like;
567
- #
568
- # {
569
- # "params": { ... },
570
- # "jobs": { ... },
571
- # "matrix": { ... },
572
- # "stages": { { "stage-id-1": ... }, ... }
573
- # }
574
- #
575
- # IMPORTANT:
576
- # This execution change all stage running IDs to the current job
577
- # running ID, but it still trac log to the same parent running ID
578
- # (with passing `run_id` and `parent_run_id` to the stage
579
- # execution arguments).
580
- #
581
- try:
582
- stage.set_outputs(
583
- stage.handler_execute(
584
- params=context,
585
- run_id=result.run_id,
586
- parent_run_id=result.parent_run_id,
587
- ).context,
588
- to=context,
589
- )
590
- except (StageException, UtilException) as err:
591
- result.trace.error(f"[JOB]: {err.__class__.__name__}: {err}")
592
- if config.job_raise_error:
593
- raise JobException(
594
- f"Stage execution error: {err.__class__.__name__}: "
595
- f"{err}"
596
- ) from None
597
-
598
- return result.catch(
599
- status=1,
600
- context={
601
- strategy_id: {
602
- "matrix": strategy,
603
- "stages": context.pop("stages", {}),
604
- "errors": {
605
- "class": err,
606
- "name": err.__class__.__name__,
607
- "message": f"{err.__class__.__name__}: {err}",
608
- },
599
+ # PARAGRAPH:
600
+ #
601
+ # I do not use below syntax because `params` dict be the
602
+ # reference memory pointer, and it was changed when I action
603
+ # anything like update or re-construct this.
604
+ #
605
+ # ... params |= stage.execute(params=params)
606
+ #
607
+ # This step will add the stage result to `stages` key in
608
+ # that stage id. It will have structure like;
609
+ #
610
+ # {
611
+ # "params": { ... },
612
+ # "jobs": { ... },
613
+ # "matrix": { ... },
614
+ # "stages": { { "stage-id-1": ... }, ... }
615
+ # }
616
+ #
617
+ # IMPORTANT:
618
+ # This execution change all stage running IDs to the current job
619
+ # running ID, but it still trac log to the same parent running ID
620
+ # (with passing `run_id` and `parent_run_id` to the stage
621
+ # execution arguments).
622
+ #
623
+ try:
624
+ stage.set_outputs(
625
+ stage.handler_execute(
626
+ params=context,
627
+ run_id=result.run_id,
628
+ parent_run_id=result.parent_run_id,
629
+ ).context,
630
+ to=context,
631
+ )
632
+ except (StageException, UtilException) as err:
633
+ result.trace.error(f"[JOB]: {err.__class__.__name__}: {err}")
634
+ if config.job_raise_error:
635
+ raise JobException(
636
+ f"Stage execution error: {err.__class__.__name__}: "
637
+ f"{err}"
638
+ ) from None
639
+
640
+ return result.catch(
641
+ status=1,
642
+ context={
643
+ strategy_id: {
644
+ "matrix": strategy,
645
+ "stages": context.pop("stages", {}),
646
+ "errors": {
647
+ "class": err,
648
+ "name": err.__class__.__name__,
649
+ "message": f"{err.__class__.__name__}: {err}",
609
650
  },
610
651
  },
611
- )
652
+ },
653
+ )
612
654
 
613
- # NOTE: Remove the current stage object for saving memory.
614
- del stage
655
+ # NOTE: Remove the current stage object for saving memory.
656
+ del stage
615
657
 
616
- return result.catch(
617
- status=Status.SUCCESS,
618
- context={
619
- strategy_id: {
620
- "matrix": strategy,
621
- "stages": filter_func(context.pop("stages", {})),
622
- },
658
+ return result.catch(
659
+ status=Status.SUCCESS,
660
+ context={
661
+ strategy_id: {
662
+ "matrix": strategy,
663
+ "stages": filter_func(context.pop("stages", {})),
623
664
  },
624
- )
625
-
626
- def execute(
627
- self,
628
- params: DictData,
629
- *,
630
- run_id: str | None = None,
631
- parent_run_id: str | None = None,
632
- result: Result | None = None,
633
- ) -> Result:
634
- """Job execution with passing dynamic parameters from the workflow
635
- execution. It will generate matrix values at the first step and run
636
- multithread on this metrics to the `stages` field of this job.
665
+ },
666
+ )
637
667
 
638
- :param params: An input parameters that use on job execution.
639
- :param run_id: A job running ID for this execution.
640
- :param parent_run_id: A parent workflow running ID for this release.
641
- :param result: (Result) A result object for keeping context and status
642
- data.
643
668
 
644
- :rtype: Result
645
- """
646
- if result is None: # pragma: no cov
647
- result: Result = Result(
648
- run_id=(run_id or gen_id(self.id or "", unique=True)),
649
- parent_run_id=parent_run_id,
669
+ def local_execute(
670
+ job: Job,
671
+ params: DictData,
672
+ *,
673
+ run_id: str | None = None,
674
+ parent_run_id: str | None = None,
675
+ result: Result | None = None,
676
+ ) -> Result:
677
+ """Local job execution with passing dynamic parameters from the workflow
678
+ execution. It will generate matrix values at the first step and run
679
+ multithread on this metrics to the `stages` field of this job.
680
+
681
+ :param job: A job model that want to execute.
682
+ :param params: An input parameters that use on job execution.
683
+ :param run_id: A job running ID for this execution.
684
+ :param parent_run_id: A parent workflow running ID for this release.
685
+ :param result: (Result) A result object for keeping context and status
686
+ data.
687
+
688
+ :rtype: Result
689
+ """
690
+ if result is None: # pragma: no cov
691
+ result: Result = Result(
692
+ run_id=(run_id or gen_id(job.id or "", unique=True)),
693
+ parent_run_id=parent_run_id,
694
+ )
695
+ elif parent_run_id: # pragma: no cov
696
+ result.set_parent_run_id(parent_run_id)
697
+
698
+ # NOTE: Normal Job execution without parallel strategy matrix. It uses
699
+ # for-loop to control strategy execution sequentially.
700
+ if (not job.strategy.is_set()) or job.strategy.max_parallel == 1:
701
+
702
+ for strategy in job.strategy.make():
703
+ result: Result = local_execute_strategy(
704
+ job=job,
705
+ strategy=strategy,
706
+ params=params,
707
+ result=result,
650
708
  )
651
- elif parent_run_id: # pragma: no cov
652
- result.set_parent_run_id(parent_run_id)
653
709
 
654
- # NOTE: Normal Job execution without parallel strategy matrix. It uses
655
- # for-loop to control strategy execution sequentially.
656
- if (not self.strategy.is_set()) or self.strategy.max_parallel == 1:
710
+ return result.catch(status=Status.SUCCESS)
711
+
712
+ # NOTE: Create event for cancel executor by trigger stop running event.
713
+ event: Event = Event()
714
+
715
+ # IMPORTANT: Start running strategy execution by multithreading because
716
+ # it will run by strategy values without waiting previous execution.
717
+ with ThreadPoolExecutor(
718
+ max_workers=job.strategy.max_parallel,
719
+ thread_name_prefix="job_strategy_exec_",
720
+ ) as executor:
721
+
722
+ futures: list[Future] = [
723
+ executor.submit(
724
+ local_execute_strategy,
725
+ job=job,
726
+ strategy=strategy,
727
+ params=params,
728
+ result=result,
729
+ event=event,
730
+ )
731
+ for strategy in job.strategy.make()
732
+ ]
733
+
734
+ context: DictData = {}
735
+ status: Status = Status.SUCCESS
736
+ fail_fast_flag: bool = job.strategy.fail_fast
737
+
738
+ if not fail_fast_flag:
739
+ done = as_completed(futures, timeout=1800)
740
+ else:
741
+ # NOTE: Get results from a collection of tasks with a timeout
742
+ # that has the first exception.
743
+ done, not_done = wait(
744
+ futures, timeout=1800, return_when=FIRST_EXCEPTION
745
+ )
746
+ nd: str = (
747
+ f", the strategies do not run is {not_done}" if not_done else ""
748
+ )
749
+ result.trace.debug(f"[JOB]: Strategy is set Fail Fast{nd}")
657
750
 
658
- for strategy in self.strategy.make():
659
- result: Result = self.execute_strategy(
660
- strategy=strategy,
661
- params=params,
662
- result=result,
663
- )
751
+ # NOTE: Stop all running tasks with setting the event manager
752
+ # and cancel any scheduled tasks.
753
+ if len(done) != len(futures):
754
+ event.set()
755
+ for future in not_done:
756
+ future.cancel()
664
757
 
665
- return result.catch(status=Status.SUCCESS)
666
-
667
- # NOTE: Create event for cancel executor by trigger stop running event.
668
- event: Event = Event()
669
-
670
- # IMPORTANT: Start running strategy execution by multithreading because
671
- # it will run by strategy values without waiting previous execution.
672
- with ThreadPoolExecutor(
673
- max_workers=self.strategy.max_parallel,
674
- thread_name_prefix="job_strategy_exec_",
675
- ) as executor:
676
-
677
- futures: list[Future] = [
678
- executor.submit(
679
- self.execute_strategy,
680
- strategy=strategy,
681
- params=params,
682
- result=result,
683
- event=event,
684
- )
685
- for strategy in self.strategy.make()
686
- ]
687
-
688
- context: DictData = {}
689
- status: Status = Status.SUCCESS
690
- fail_fast_flag: bool = self.strategy.fail_fast
691
-
692
- if fail_fast_flag:
693
- # NOTE: Get results from a collection of tasks with a timeout
694
- # that has the first exception.
695
- done, not_done = wait(
696
- futures, timeout=1800, return_when=FIRST_EXCEPTION
697
- )
698
- nd: str = (
699
- f", the strategies do not run is {not_done}"
700
- if not_done
701
- else ""
758
+ for future in done:
759
+ try:
760
+ future.result()
761
+ except JobException as err:
762
+ status = Status.FAILED
763
+ ls: str = "Fail-Fast" if fail_fast_flag else "All-Completed"
764
+ result.trace.error(
765
+ f"[JOB]: {ls} Catch:\n\t{err.__class__.__name__}:"
766
+ f"\n\t{err}"
702
767
  )
703
- result.trace.debug(f"[JOB]: Strategy is set Fail Fast{nd}")
704
-
705
- # NOTE: Stop all running tasks with setting the event manager
706
- # and cancel any scheduled tasks.
707
- if len(done) != len(futures):
708
- event.set()
709
- for future in not_done:
710
- future.cancel()
711
- else:
712
- done = as_completed(futures, timeout=1800)
713
-
714
- for future in done:
715
- try:
716
- future.result()
717
- except JobException as err:
718
- status = Status.FAILED
719
- ls: str = "Fail-Fast" if fail_fast_flag else "All-Completed"
720
- result.trace.error(
721
- f"[JOB]: {ls} Catch:\n\t{err.__class__.__name__}:"
722
- f"\n\t{err}"
723
- )
724
- context.update(
725
- {
726
- "errors": {
727
- "class": err,
728
- "name": err.__class__.__name__,
729
- "message": f"{err.__class__.__name__}: {err}",
730
- },
768
+ context.update(
769
+ {
770
+ "errors": {
771
+ "class": err,
772
+ "name": err.__class__.__name__,
773
+ "message": f"{err.__class__.__name__}: {err}",
731
774
  },
732
- )
775
+ },
776
+ )
733
777
 
734
- return result.catch(status=status, context=context)
778
+ return result.catch(status=status, context=context)