ddeutil-workflow 0.0.64__py3-none-any.whl → 0.0.66__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
@@ -12,7 +12,7 @@ for execute on target machine instead of the current local machine.
12
12
  making matrix values before execution parallelism stage execution.
13
13
 
14
14
  The Job model does not implement `handler_execute` same as Stage model
15
- because the job should raise only `JobException` class from the execution
15
+ because the job should raise only `JobError` class from the execution
16
16
  method.
17
17
  """
18
18
  from __future__ import annotations
@@ -40,12 +40,18 @@ from pydantic.functional_validators import field_validator, model_validator
40
40
  from typing_extensions import Self
41
41
 
42
42
  from .__types import DictData, DictStr, Matrix, StrOrNone
43
- from .exceptions import (
44
- JobException,
45
- StageException,
46
- to_dict,
43
+ from .errors import JobCancelError, JobError, to_dict
44
+ from .result import (
45
+ CANCEL,
46
+ FAILED,
47
+ SKIP,
48
+ SUCCESS,
49
+ WAIT,
50
+ Result,
51
+ Status,
52
+ get_status_from_error,
53
+ validate_statuses,
47
54
  )
48
- from .result import CANCEL, FAILED, SKIP, SUCCESS, WAIT, Result, Status
49
55
  from .reusables import has_template, param2template
50
56
  from .stages import Stage
51
57
  from .utils import cross_product, filter_func, gen_id
@@ -238,6 +244,7 @@ class SelfHostedArgs(BaseModel):
238
244
  """Self-Hosted arguments."""
239
245
 
240
246
  host: str = Field(description="A host URL of the target self-hosted.")
247
+ token: SecretStr = Field(description="An API or Access token.")
241
248
 
242
249
 
243
250
  class OnSelfHosted(BaseRunsOn): # pragma: no cov
@@ -250,6 +257,8 @@ class OnSelfHosted(BaseRunsOn): # pragma: no cov
250
257
 
251
258
 
252
259
  class AzBatchArgs(BaseModel):
260
+ """Azure Batch arguments."""
261
+
253
262
  batch_account_name: str
254
263
  batch_account_key: SecretStr
255
264
  batch_account_url: str
@@ -431,11 +440,10 @@ class Job(BaseModel):
431
440
  return stage
432
441
  raise ValueError(f"Stage {stage_id!r} does not exists in this job.")
433
442
 
434
- def check_needs(
435
- self, jobs: dict[str, DictData]
436
- ) -> Status: # pragma: no cov
443
+ def check_needs(self, jobs: dict[str, DictData]) -> Status:
437
444
  """Return trigger status from checking job's need trigger rule logic was
438
- valid. The return status should be SUCCESS, FAILED, WAIT, or SKIP.
445
+ valid. The return status should be `SUCCESS`, `FAILED`, `WAIT`, or
446
+ `SKIP` status.
439
447
 
440
448
  :param jobs: (dict[str, DictData]) A mapping of job ID and its context
441
449
  data that return from execution process.
@@ -450,43 +458,98 @@ class Job(BaseModel):
450
458
  def make_return(result: bool) -> Status:
451
459
  return SUCCESS if result else FAILED
452
460
 
461
+ # NOTE: Filter all job result context only needed in this job.
453
462
  need_exist: dict[str, Any] = {
454
- need: jobs[need] for need in self.needs if need in jobs
463
+ need: jobs[need] or {"status": SUCCESS}
464
+ for need in self.needs
465
+ if need in jobs
455
466
  }
456
- if len(need_exist) != len(self.needs):
467
+
468
+ # NOTE: Return WAIT status if result context not complete, or it has any
469
+ # waiting status.
470
+ if len(need_exist) < len(self.needs) or any(
471
+ need_exist[job].get("status", SUCCESS) == WAIT for job in need_exist
472
+ ):
457
473
  return WAIT
458
- elif all(need_exist[job].get("skipped", False) for job in need_exist):
474
+
475
+ # NOTE: Return SKIP status if all status are SKIP.
476
+ elif all(
477
+ need_exist[job].get("status", SUCCESS) == SKIP for job in need_exist
478
+ ):
459
479
  return SKIP
480
+
481
+ # NOTE: Return CANCEL status if any status is CANCEL.
482
+ elif any(
483
+ need_exist[job].get("status", SUCCESS) == CANCEL
484
+ for job in need_exist
485
+ ):
486
+ return CANCEL
487
+
488
+ # NOTE: Return SUCCESS if all status not be WAIT or all SKIP.
460
489
  elif self.trigger_rule == Rule.ALL_DONE:
461
490
  return SUCCESS
491
+
462
492
  elif self.trigger_rule == Rule.ALL_SUCCESS:
463
493
  rs = all(
464
494
  (
465
495
  "errors" not in need_exist[job]
466
- and not need_exist[job].get("skipped", False)
496
+ and need_exist[job].get("status", SUCCESS) == SUCCESS
467
497
  )
468
498
  for job in need_exist
469
499
  )
470
500
  elif self.trigger_rule == Rule.ALL_FAILED:
471
- rs = all("errors" in need_exist[job] for job in need_exist)
472
- elif self.trigger_rule == Rule.ONE_SUCCESS:
473
- rs = sum(
501
+ rs = all(
474
502
  (
475
- "errors" not in need_exist[job]
476
- and not need_exist[job].get("skipped", False)
503
+ "errors" in need_exist[job]
504
+ or need_exist[job].get("status", SUCCESS) == FAILED
477
505
  )
478
506
  for job in need_exist
479
- ) + 1 == len(self.needs)
507
+ )
508
+
509
+ elif self.trigger_rule == Rule.ONE_SUCCESS:
510
+ rs = (
511
+ sum(
512
+ (
513
+ "errors" not in need_exist[job]
514
+ and need_exist[job].get("status", SUCCESS) == SUCCESS
515
+ )
516
+ for job in need_exist
517
+ )
518
+ == 1
519
+ )
520
+
480
521
  elif self.trigger_rule == Rule.ONE_FAILED:
481
- rs = sum("errors" in need_exist[job] for job in need_exist) == 1
522
+ rs = (
523
+ sum(
524
+ (
525
+ "errors" in need_exist[job]
526
+ or need_exist[job].get("status", SUCCESS) == FAILED
527
+ )
528
+ for job in need_exist
529
+ )
530
+ == 1
531
+ )
532
+
482
533
  elif self.trigger_rule == Rule.NONE_SKIPPED:
483
534
  rs = all(
484
- not need_exist[job].get("skipped", False) for job in need_exist
535
+ need_exist[job].get("status", SUCCESS) != SKIP
536
+ for job in need_exist
485
537
  )
538
+
486
539
  elif self.trigger_rule == Rule.NONE_FAILED:
487
- rs = all("errors" not in need_exist[job] for job in need_exist)
540
+ rs = all(
541
+ (
542
+ "errors" not in need_exist[job]
543
+ and need_exist[job].get("status", SUCCESS) != FAILED
544
+ )
545
+ for job in need_exist
546
+ )
547
+
488
548
  else: # pragma: no cov
489
- return FAILED
549
+ raise NotImplementedError(
550
+ f"Trigger rule {self.trigger_rule} does not implement on this "
551
+ f"`check_needs` method yet."
552
+ )
490
553
  return make_return(rs)
491
554
 
492
555
  def is_skipped(self, params: DictData) -> bool:
@@ -496,9 +559,9 @@ class Job(BaseModel):
496
559
  :param params: (DictData) A parameter value that want to pass to condition
497
560
  template.
498
561
 
499
- :raise JobException: When it has any error raise from the eval
562
+ :raise JobError: When it has any error raise from the eval
500
563
  condition statement.
501
- :raise JobException: When return type of the eval condition statement
564
+ :raise JobError: When return type of the eval condition statement
502
565
  does not return with boolean type.
503
566
 
504
567
  :rtype: bool
@@ -519,7 +582,7 @@ class Job(BaseModel):
519
582
  raise TypeError("Return type of condition does not be boolean")
520
583
  return not rs
521
584
  except Exception as e:
522
- raise JobException(f"{e.__class__.__name__}: {e}") from e
585
+ raise JobError(f"{e.__class__.__name__}: {e}") from e
523
586
 
524
587
  def set_outputs(
525
588
  self,
@@ -561,7 +624,7 @@ class Job(BaseModel):
561
624
  extract from the result context if it exists. If it does not found, it
562
625
  will not set on the received context.
563
626
 
564
- :raise JobException: If the job's ID does not set and the setting
627
+ :raise JobError: If the job's ID does not set and the setting
565
628
  default job ID flag does not set.
566
629
 
567
630
  :param output: (DictData) A result data context that want to extract
@@ -575,34 +638,51 @@ class Job(BaseModel):
575
638
  to["jobs"] = {}
576
639
 
577
640
  if self.id is None and job_id is None:
578
- raise JobException(
641
+ raise JobError(
579
642
  "This job do not set the ID before setting execution output."
580
643
  )
581
644
 
582
645
  _id: str = self.id or job_id
583
- output: DictData = output.copy()
646
+ output: DictData = copy.deepcopy(output)
584
647
  errors: DictData = (
585
- {"errors": output.pop("errors", {})} if "errors" in output else {}
648
+ {"errors": output.pop("errors")} if "errors" in output else {}
586
649
  )
587
- skipping: dict[str, bool] = (
588
- {"skipped": output.pop("skipped", False)}
589
- if "skipped" in output
590
- else {}
650
+ status: dict[str, Status] = (
651
+ {"status": output.pop("status")} if "status" in output else {}
591
652
  )
592
-
593
653
  if self.strategy.is_set():
594
- to["jobs"][_id] = {"strategies": output, **skipping, **errors}
654
+ to["jobs"][_id] = {"strategies": output} | errors | status
595
655
  elif len(k := output.keys()) > 1: # pragma: no cov
596
- raise JobException(
656
+ raise JobError(
597
657
  "Strategy output from execution return more than one ID while "
598
658
  "this job does not set strategy."
599
659
  )
600
660
  else:
601
661
  _output: DictData = {} if len(k) == 0 else output[list(k)[0]]
602
662
  _output.pop("matrix", {})
603
- to["jobs"][_id] = {**_output, **skipping, **errors}
663
+ to["jobs"][_id] = _output | errors | status
604
664
  return to
605
665
 
666
+ def get_outputs(
667
+ self,
668
+ output: DictData,
669
+ *,
670
+ job_id: StrOrNone = None,
671
+ ) -> DictData:
672
+ """Get the outputs from jobs data. It will get this job ID or passing
673
+ custom ID from the job outputs mapping.
674
+
675
+ :param output: (DictData) A job outputs data that want to extract
676
+ :param job_id: (StrOrNone) A job ID if the `id` field does not set.
677
+
678
+ :rtype: DictData
679
+ """
680
+ _id: str = self.id or job_id
681
+ if self.strategy.is_set():
682
+ return output.get("jobs", {}).get(_id, {}).get("strategies", {})
683
+ else:
684
+ return output.get("jobs", {}).get(_id, {})
685
+
606
686
  def execute(
607
687
  self,
608
688
  params: DictData,
@@ -634,10 +714,11 @@ class Job(BaseModel):
634
714
  )
635
715
 
636
716
  result.trace.info(
637
- f"[JOB]: Execute "
717
+ f"[JOB]: Routing for "
638
718
  f"{''.join(self.runs_on.type.value.split('_')).title()}: "
639
719
  f"{self.id!r}"
640
720
  )
721
+
641
722
  if self.runs_on.type == RunsOn.LOCAL:
642
723
  return local_execute(
643
724
  self,
@@ -658,13 +739,13 @@ class Job(BaseModel):
658
739
  )
659
740
 
660
741
  result.trace.error(
661
- f"[JOB]: Execute not support runs-on: {self.runs_on.type.value!r} "
742
+ f"[JOB]: Execution not support runs-on: {self.runs_on.type.value!r} "
662
743
  f"yet."
663
744
  )
664
745
  return result.catch(
665
746
  status=FAILED,
666
747
  context={
667
- "errors": JobException(
748
+ "errors": JobError(
668
749
  f"Execute runs-on type: {self.runs_on.type.value!r} does "
669
750
  f"not support yet."
670
751
  ).to_dict(),
@@ -672,6 +753,19 @@ class Job(BaseModel):
672
753
  )
673
754
 
674
755
 
756
+ def mark_errors(context: DictData, error: JobError) -> None:
757
+ """Make the errors context result with the refs value depends on the nested
758
+ execute func.
759
+
760
+ :param context: (DictData) A context data.
761
+ :param error: (JobError) A stage exception object.
762
+ """
763
+ if "errors" in context:
764
+ context["errors"][error.refs] = error.to_dict()
765
+ else:
766
+ context["errors"] = error.to_dict(with_refs=True)
767
+
768
+
675
769
  def local_execute_strategy(
676
770
  job: Job,
677
771
  strategy: DictData,
@@ -679,7 +773,7 @@ def local_execute_strategy(
679
773
  *,
680
774
  result: Optional[Result] = None,
681
775
  event: Optional[Event] = None,
682
- ) -> Result:
776
+ ) -> tuple[Status, Result]:
683
777
  """Local strategy execution with passing dynamic parameters from the
684
778
  job execution and strategy matrix.
685
779
 
@@ -700,11 +794,11 @@ def local_execute_strategy(
700
794
  :param event: (Event) An Event manager instance that use to cancel this
701
795
  execution if it forces stopped by parent execution.
702
796
 
703
- :raise JobException: If event was set.
704
- :raise JobException: If stage execution raise any error as `StageException`.
705
- :raise JobException: If the result from execution has `FAILED` status.
797
+ :raise JobError: If event was set.
798
+ :raise JobError: If stage execution raise any error as `StageError`.
799
+ :raise JobError: If the result from execution has `FAILED` status.
706
800
 
707
- :rtype: Result
801
+ :rtype: tuple[Status, Result]
708
802
  """
709
803
  result: Result = result or Result(
710
804
  run_id=gen_id(job.id or "EMPTY", unique=True),
@@ -719,81 +813,92 @@ def local_execute_strategy(
719
813
 
720
814
  context: DictData = copy.deepcopy(params)
721
815
  context.update({"matrix": strategy, "stages": {}})
722
- for stage in job.stages:
816
+ total_stage: int = len(job.stages)
817
+ skips: list[bool] = [False] * total_stage
818
+ for i, stage in enumerate(job.stages, start=0):
723
819
 
724
820
  if job.extras:
725
821
  stage.extras = job.extras
726
822
 
727
- if stage.is_skipped(params=context):
728
- result.trace.info(f"[JOB]: Skip Stage: {stage.iden!r}")
729
- stage.set_outputs(output={"skipped": True}, to=context)
730
- continue
731
-
732
823
  if event and event.is_set():
733
- error_msg: str = "Job strategy was canceled because event was set."
824
+ error_msg: str = (
825
+ "Strategy execution was canceled from the event before "
826
+ "start stage execution."
827
+ )
734
828
  result.catch(
735
829
  status=CANCEL,
736
830
  context={
737
831
  strategy_id: {
832
+ "status": CANCEL,
738
833
  "matrix": strategy,
739
834
  "stages": filter_func(context.pop("stages", {})),
740
- "errors": JobException(error_msg).to_dict(),
835
+ "errors": JobCancelError(error_msg).to_dict(),
741
836
  },
742
837
  },
743
838
  )
744
- raise JobException(error_msg, refs=strategy_id)
839
+ raise JobCancelError(error_msg, refs=strategy_id)
840
+
841
+ result.trace.info(f"[JOB]: Execute Stage: {stage.iden!r}")
842
+ rs: Result = stage.handler_execute(
843
+ params=context,
844
+ run_id=result.run_id,
845
+ parent_run_id=result.parent_run_id,
846
+ event=event,
847
+ )
848
+ stage.set_outputs(rs.context, to=context)
745
849
 
746
- try:
747
- result.trace.info(f"[JOB]: Execute Stage: {stage.iden!r}")
748
- rs: Result = stage.handler_execute(
749
- params=context,
750
- run_id=result.run_id,
751
- parent_run_id=result.parent_run_id,
752
- event=event,
850
+ if rs.status == SKIP:
851
+ skips[i] = True
852
+ continue
853
+
854
+ if rs.status == FAILED:
855
+ error_msg: str = (
856
+ f"Strategy execution was break because its nested-stage, "
857
+ f"{stage.iden!r}, failed."
753
858
  )
754
- stage.set_outputs(rs.context, to=context)
755
- except StageException as e:
756
859
  result.catch(
757
860
  status=FAILED,
758
861
  context={
759
862
  strategy_id: {
863
+ "status": FAILED,
760
864
  "matrix": strategy,
761
865
  "stages": filter_func(context.pop("stages", {})),
762
- "errors": e.to_dict(),
866
+ "errors": JobError(error_msg).to_dict(),
763
867
  },
764
868
  },
765
869
  )
766
- raise JobException(
767
- message=f"Handler Error: {e.__class__.__name__}: {e}",
768
- refs=strategy_id,
769
- ) from e
870
+ raise JobError(error_msg, refs=strategy_id)
770
871
 
771
- if rs.status == FAILED:
872
+ elif rs.status == CANCEL:
772
873
  error_msg: str = (
773
- f"Strategy break because stage, {stage.iden!r}, return "
774
- f"`FAILED` status."
874
+ "Strategy execution was canceled from the event after "
875
+ "end stage execution."
775
876
  )
776
877
  result.catch(
777
- status=FAILED,
878
+ status=CANCEL,
778
879
  context={
779
880
  strategy_id: {
881
+ "status": CANCEL,
780
882
  "matrix": strategy,
781
883
  "stages": filter_func(context.pop("stages", {})),
782
- "errors": JobException(error_msg).to_dict(),
884
+ "errors": JobCancelError(error_msg).to_dict(),
783
885
  },
784
886
  },
785
887
  )
786
- raise JobException(error_msg, refs=strategy_id)
888
+ raise JobCancelError(error_msg, refs=strategy_id)
787
889
 
788
- return result.catch(
789
- status=SUCCESS,
890
+ status: Status = SKIP if sum(skips) == total_stage else SUCCESS
891
+ result.catch(
892
+ status=status,
790
893
  context={
791
894
  strategy_id: {
895
+ "status": status,
792
896
  "matrix": strategy,
793
897
  "stages": filter_func(context.pop("stages", {})),
794
898
  },
795
899
  },
796
900
  )
901
+ return status, result
797
902
 
798
903
 
799
904
  def local_execute(
@@ -809,7 +914,7 @@ def local_execute(
809
914
  step and run multithread on this metrics to the `stages` field of this job.
810
915
 
811
916
  Important:
812
- This method does not raise any `JobException` because it allows run
917
+ This method does not raise any `JobError` because it allows run
813
918
  parallel mode. If it raises error from strategy execution, it will catch
814
919
  that error and store it in the `errors` key with list of error.
815
920
 
@@ -835,12 +940,22 @@ def local_execute(
835
940
  extras=job.extras,
836
941
  )
837
942
 
943
+ result.trace.info("[JOB]: Start Local executor.")
944
+
945
+ if job.desc:
946
+ result.trace.debug(f"[JOB]: Description:||{job.desc}||")
947
+
948
+ if job.is_skipped(params=params):
949
+ result.trace.info("[JOB]: Skip because job condition was valid.")
950
+ return result.catch(status=SKIP)
951
+
838
952
  event: Event = event or Event()
839
- fail_fast_flag: bool = job.strategy.fail_fast
840
- ls: str = "Fail-Fast" if fail_fast_flag else "All-Completed"
953
+ ls: str = "Fail-Fast" if job.strategy.fail_fast else "All-Completed"
841
954
  workers: int = job.strategy.max_parallel
955
+ strategies: list[DictStr] = job.strategy.make()
956
+ len_strategy: int = len(strategies)
842
957
  result.trace.info(
843
- f"[JOB]: Execute {ls}: {job.id!r} with {workers} "
958
+ f"[JOB]: ... Mode {ls}: {job.id!r} with {workers} "
844
959
  f"worker{'s' if workers > 1 else ''}."
845
960
  )
846
961
 
@@ -848,17 +963,14 @@ def local_execute(
848
963
  return result.catch(
849
964
  status=CANCEL,
850
965
  context={
851
- "errors": JobException(
852
- "Job was canceled from event that had set before "
966
+ "errors": JobCancelError(
967
+ "Execution was canceled from the event before start "
853
968
  "local job execution."
854
969
  ).to_dict()
855
970
  },
856
971
  )
857
972
 
858
- with ThreadPoolExecutor(
859
- max_workers=workers, thread_name_prefix="job_strategy_exec_"
860
- ) as executor:
861
-
973
+ with ThreadPoolExecutor(workers, "jb_stg") as executor:
862
974
  futures: list[Future] = [
863
975
  executor.submit(
864
976
  local_execute_strategy,
@@ -868,50 +980,57 @@ def local_execute(
868
980
  result=result,
869
981
  event=event,
870
982
  )
871
- for strategy in job.strategy.make()
983
+ for strategy in strategies
872
984
  ]
873
985
 
874
986
  context: DictData = {}
875
- status: Status = SUCCESS
987
+ statuses: list[Status] = [WAIT] * len_strategy
988
+ fail_fast: bool = False
876
989
 
877
- if not fail_fast_flag:
990
+ if not job.strategy.fail_fast:
878
991
  done: Iterator[Future] = as_completed(futures)
879
992
  else:
880
993
  done, not_done = wait(futures, return_when=FIRST_EXCEPTION)
881
994
  if len(list(done)) != len(futures):
882
995
  result.trace.warning(
883
- "[JOB]: Handler Fail-Fast: Got exception and set event."
996
+ "[JOB]: Set the event for stop pending job-execution."
884
997
  )
885
998
  event.set()
886
999
  for future in not_done:
887
1000
  future.cancel()
888
- time.sleep(0.075)
889
1001
 
890
- nd: str = (
891
- (
892
- f", {len(not_done)} strateg"
893
- f"{'ies' if len(not_done) > 1 else 'y'} not run!!!"
1002
+ time.sleep(0.025)
1003
+ nd: str = (
1004
+ (
1005
+ f", {len(not_done)} strateg"
1006
+ f"{'ies' if len(not_done) > 1 else 'y'} not run!!!"
1007
+ )
1008
+ if not_done
1009
+ else ""
894
1010
  )
895
- if not_done
896
- else ""
897
- )
898
- result.trace.debug(f"[JOB]: ... Job was set Fail-Fast{nd}")
899
- done: Iterator[Future] = as_completed(futures)
1011
+ result.trace.debug(f"[JOB]: ... Job was set Fail-Fast{nd}")
1012
+ done: Iterator[Future] = as_completed(futures)
1013
+ fail_fast: bool = True
900
1014
 
901
- for future in done:
1015
+ for i, future in enumerate(done, start=0):
902
1016
  try:
903
- future.result()
904
- except JobException as e:
905
- status = FAILED
1017
+ statuses[i], _ = future.result()
1018
+ except JobError as e:
1019
+ statuses[i] = get_status_from_error(e)
906
1020
  result.trace.error(
907
- f"[JOB]: {ls} Error Handler:||{e.__class__.__name__}:||{e}"
1021
+ f"[JOB]: {ls} Handler:||{e.__class__.__name__}: {e}"
908
1022
  )
909
- if "errors" in context:
910
- context["errors"][e.refs] = e.to_dict()
911
- else:
912
- context["errors"] = e.to_dict(with_refs=True)
1023
+ mark_errors(context, e)
913
1024
  except CancelledError:
914
1025
  pass
1026
+
1027
+ status: Status = validate_statuses(statuses)
1028
+
1029
+ # NOTE: Prepare status because it does not cancel from parent event but
1030
+ # cancel from failed item execution.
1031
+ if fail_fast and status == CANCEL:
1032
+ status = FAILED
1033
+
915
1034
  return result.catch(status=status, context=context)
916
1035
 
917
1036
 
@@ -943,12 +1062,14 @@ def self_hosted_execute(
943
1062
  extras=job.extras,
944
1063
  )
945
1064
 
1065
+ result.trace.info("[JOB]: Start self-hosted executor.")
1066
+
946
1067
  if event and event.is_set():
947
1068
  return result.catch(
948
1069
  status=CANCEL,
949
1070
  context={
950
- "errors": JobException(
951
- "Job was canceled from event that had set before start "
1071
+ "errors": JobCancelError(
1072
+ "Execution was canceled from the event before start "
952
1073
  "self-hosted execution."
953
1074
  ).to_dict()
954
1075
  },
@@ -970,8 +1091,8 @@ def self_hosted_execute(
970
1091
  return result.catch(status=FAILED, context={"errors": to_dict(e)})
971
1092
 
972
1093
  if resp.status_code != 200:
973
- raise JobException(
974
- f"Job execution error from request to self-hosted: "
1094
+ raise JobError(
1095
+ f"Job execution got error response from self-hosted: "
975
1096
  f"{job.runs_on.args.host!r}"
976
1097
  )
977
1098
 
@@ -1018,12 +1139,15 @@ def azure_batch_execute(
1018
1139
  id_logic=(job.id or "EMPTY"),
1019
1140
  extras=job.extras,
1020
1141
  )
1142
+
1143
+ result.trace.info("[JOB]: Start Azure Batch executor.")
1144
+
1021
1145
  if event and event.is_set():
1022
1146
  return result.catch(
1023
1147
  status=CANCEL,
1024
1148
  context={
1025
- "errors": JobException(
1026
- "Job was canceled from event that had set before start "
1149
+ "errors": JobCancelError(
1150
+ "Execution was canceled from the event before start "
1027
1151
  "azure-batch execution."
1028
1152
  ).to_dict()
1029
1153
  },
@@ -1053,13 +1177,16 @@ def docker_execution(
1053
1177
  id_logic=(job.id or "EMPTY"),
1054
1178
  extras=job.extras,
1055
1179
  )
1180
+
1181
+ result.trace.info("[JOB]: Start Docker executor.")
1182
+
1056
1183
  if event and event.is_set():
1057
1184
  return result.catch(
1058
1185
  status=CANCEL,
1059
1186
  context={
1060
- "errors": JobException(
1061
- "Job Docker execution was canceled from event that "
1062
- "had set before start execution."
1187
+ "errors": JobCancelError(
1188
+ "Execution was canceled from the event before start "
1189
+ "start docker execution."
1063
1190
  ).to_dict()
1064
1191
  },
1065
1192
  )
ddeutil/workflow/logs.py CHANGED
@@ -848,7 +848,7 @@ class FileAudit(BaseAudit):
848
848
  "audit_path", extras=self.extras
849
849
  ) / self.filename_fmt.format(name=self.name, release=self.release)
850
850
 
851
- def save(self, excluded: Optional[list[str]]) -> Self:
851
+ def save(self, excluded: Optional[list[str]] = None) -> Self:
852
852
  """Save logging data that receive a context data from a workflow
853
853
  execution result.
854
854