ddeutil-workflow 0.0.37__py3-none-any.whl → 0.0.38__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.
@@ -1 +1 @@
1
- __version__: str = "0.0.37"
1
+ __version__: str = "0.0.38"
@@ -39,6 +39,8 @@ from .job import (
39
39
  Job,
40
40
  RunsOn,
41
41
  Strategy,
42
+ local_execute,
43
+ local_execute_strategy,
42
44
  )
43
45
  from .logs import (
44
46
  TraceData,
@@ -69,6 +71,8 @@ from .stages import (
69
71
  BashStage,
70
72
  CallStage,
71
73
  EmptyStage,
74
+ ForEachStage,
75
+ ParallelStage,
72
76
  PyStage,
73
77
  Stage,
74
78
  TriggerStage,
@@ -89,7 +93,6 @@ from .templates import (
89
93
  from .utils import (
90
94
  batch,
91
95
  cross_product,
92
- dash2underscore,
93
96
  delay,
94
97
  filter_func,
95
98
  gen_id,
@@ -45,6 +45,7 @@ async def job_execute(
45
45
  run_id=result.run_id,
46
46
  parent_run_id=result.parent_run_id,
47
47
  )
48
+ context: DictData = {}
48
49
  try:
49
50
  job.set_outputs(
50
51
  job.execute(
@@ -52,7 +53,7 @@ async def job_execute(
52
53
  run_id=rs.run_id,
53
54
  parent_run_id=rs.parent_run_id,
54
55
  ).context,
55
- to=params,
56
+ to=context,
56
57
  )
57
58
  except JobException as err:
58
59
  rs.trace.error(f"[WORKFLOW]: {err.__class__.__name__}: {err}")
@@ -70,4 +71,5 @@ async def job_execute(
70
71
  exclude_defaults=True,
71
72
  ),
72
73
  "params": params,
74
+ "context": context,
73
75
  }
@@ -6,7 +6,7 @@
6
6
  """This route include audit and trace log paths."""
7
7
  from __future__ import annotations
8
8
 
9
- from fastapi import APIRouter
9
+ from fastapi import APIRouter, Path, Query
10
10
  from fastapi import status as st
11
11
  from fastapi.responses import UJSONResponse
12
12
 
@@ -27,12 +27,17 @@ log_route = APIRouter(
27
27
  summary="Read all trace logs.",
28
28
  tags=["trace"],
29
29
  )
30
- async def get_traces():
30
+ async def get_traces(
31
+ offset: int = Query(default=0, gt=0),
32
+ limit: int = Query(default=100, gt=0),
33
+ ):
31
34
  """Return all trace logs from the current trace log path that config with
32
35
  `WORKFLOW_LOG_PATH` environment variable name.
33
36
  """
34
37
  return {
35
- "message": "Getting trace logs",
38
+ "message": (
39
+ f"Getting trace logs with offset: {offset} and limit: {limit}"
40
+ ),
36
41
  "traces": [
37
42
  trace.model_dump(
38
43
  by_alias=True,
@@ -117,7 +122,10 @@ async def get_audit_with_workflow(workflow: str):
117
122
  summary="Read all audit logs with specific workflow name and release date.",
118
123
  tags=["audit"],
119
124
  )
120
- async def get_audit_with_workflow_release(workflow: str, release: str):
125
+ async def get_audit_with_workflow_release(
126
+ workflow: str = Path(...),
127
+ release: str = Path(...),
128
+ ):
121
129
  """Return all audit logs with specific workflow name and release date from
122
130
  the current audit log path that config with `WORKFLOW_AUDIT_PATH`
123
131
  environment variable name.
@@ -26,6 +26,7 @@ T = TypeVar("T")
26
26
  P = ParamSpec("P")
27
27
 
28
28
  logger = logging.getLogger("ddeutil.workflow")
29
+ logging.getLogger("asyncio").setLevel(logging.INFO)
29
30
 
30
31
 
31
32
  class TagFunc(Protocol):
@@ -60,10 +61,13 @@ def tag(
60
61
 
61
62
  @wraps(func)
62
63
  def wrapped(*args: P.args, **kwargs: P.kwargs) -> TagFunc:
63
- # NOTE: Able to do anything before calling the call function.
64
64
  return func(*args, **kwargs)
65
65
 
66
- return wrapped
66
+ @wraps(func)
67
+ async def awrapped(*args: P.args, **kwargs: P.kwargs) -> TagFunc:
68
+ return await func(*args, **kwargs)
69
+
70
+ return awrapped if inspect.iscoroutinefunction(func) else wrapped
67
71
 
68
72
  return func_internal
69
73
 
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional, Union
4
+
5
+ from pydantic import BaseModel, ConfigDict, Field
6
+
7
+ from .__types import DictData
8
+
9
+
10
+ class ErrorContext(BaseModel): # pragma: no cov
11
+ model_config = ConfigDict(arbitrary_types_allowed=True)
12
+
13
+ obj: Exception = Field(alias="class")
14
+ name: str = Field(description="A name of exception class.")
15
+ message: str = Field(description="A exception message.")
16
+
17
+
18
+ class OutputContext(BaseModel): # pragma: no cov
19
+ outputs: DictData = Field(default_factory=dict)
20
+ errors: Optional[ErrorContext] = Field(default=None)
21
+
22
+ def is_exception(self) -> bool:
23
+ return self.errors is not None
24
+
25
+
26
+ class StageContext(BaseModel): # pragma: no cov
27
+ stages: dict[str, OutputContext]
28
+ errors: Optional[ErrorContext] = Field(default=None)
29
+
30
+ def is_exception(self) -> bool:
31
+ return self.errors is not None
32
+
33
+
34
+ class MatrixContext(StageContext): # pragma: no cov
35
+ matrix: DictData = Field(default_factory=dict)
36
+
37
+
38
+ MatrixStageContext = dict[
39
+ str, Union[MatrixContext, StageContext]
40
+ ] # pragma: no cov
41
+
42
+
43
+ class StrategyContext(BaseModel): # pragma: no cov
44
+ strategies: MatrixStageContext
45
+ errors: Optional[ErrorContext] = Field(default=None)
46
+
47
+ def is_exception(self) -> bool:
48
+ return self.errors is not None
49
+
50
+
51
+ StrategyMatrixContext = Union[
52
+ StrategyContext, MatrixStageContext
53
+ ] # pragma: no cov
54
+
55
+
56
+ class JobContext(BaseModel): # pragma: no cov
57
+ params: DictData = Field(description="A parameterize value")
58
+ jobs: dict[str, StrategyMatrixContext]
59
+ errors: Optional[ErrorContext] = Field(default=None)
@@ -9,8 +9,21 @@ annotate for handle error only.
9
9
  """
10
10
  from __future__ import annotations
11
11
 
12
+ from typing import Any
12
13
 
13
- class BaseWorkflowException(Exception): ...
14
+
15
+ def to_dict(exception: Exception) -> dict[str, Any]: # pragma: no cov
16
+ return {
17
+ "class": exception,
18
+ "name": exception.__class__.__name__,
19
+ "message": str(exception),
20
+ }
21
+
22
+
23
+ class BaseWorkflowException(Exception):
24
+
25
+ def to_dict(self) -> dict[str, Any]:
26
+ return to_dict(self)
14
27
 
15
28
 
16
29
  class UtilException(BaseWorkflowException): ...
ddeutil/workflow/job.py CHANGED
@@ -27,7 +27,7 @@ from threading import Event
27
27
  from typing import Annotated, Any, Literal, Optional, Union
28
28
 
29
29
  from ddeutil.core import freeze_args
30
- from pydantic import BaseModel, ConfigDict, Field
30
+ from pydantic import BaseModel, ConfigDict, Discriminator, Field, Tag
31
31
  from pydantic.functional_validators import field_validator, model_validator
32
32
  from typing_extensions import Self
33
33
 
@@ -43,7 +43,6 @@ from .stages import Stage
43
43
  from .templates import has_template
44
44
  from .utils import (
45
45
  cross_product,
46
- dash2underscore,
47
46
  filter_func,
48
47
  gen_id,
49
48
  )
@@ -155,7 +154,7 @@ class Strategy(BaseModel):
155
154
 
156
155
  fail_fast: bool = Field(
157
156
  default=False,
158
- serialization_alias="fail-fast",
157
+ alias="fail-fast",
159
158
  )
160
159
  max_parallel: int = Field(
161
160
  default=1,
@@ -164,7 +163,7 @@ class Strategy(BaseModel):
164
163
  "The maximum number of executor thread pool that want to run "
165
164
  "parallel"
166
165
  ),
167
- serialization_alias="max-parallel",
166
+ alias="max-parallel",
168
167
  )
169
168
  matrix: Matrix = Field(
170
169
  default_factory=dict,
@@ -181,18 +180,6 @@ class Strategy(BaseModel):
181
180
  description="A list of exclude matrix that want to filter-out.",
182
181
  )
183
182
 
184
- @model_validator(mode="before")
185
- def __prepare_keys(cls, values: DictData) -> DictData:
186
- """Rename key that use dash to underscore because Python does not
187
- support this character exist in any variable name.
188
-
189
- :param values: A parsing values to these models
190
- :rtype: DictData
191
- """
192
- dash2underscore("max-parallel", values)
193
- dash2underscore("fail-fast", values)
194
- return values
195
-
196
183
  def is_set(self) -> bool:
197
184
  """Return True if this strategy was set from yaml template.
198
185
 
@@ -268,13 +255,17 @@ class RunsOnK8s(BaseRunsOn): # pragma: no cov
268
255
  type: Literal[RunsOnType.K8S] = Field(default=RunsOnType.K8S)
269
256
 
270
257
 
258
+ def get_discriminator_runs_on(model: dict[str, Any]) -> str:
259
+ return model.get("type", "local")
260
+
261
+
271
262
  RunsOn = Annotated[
272
263
  Union[
273
- RunsOnLocal,
274
- RunsOnSelfHosted,
275
- RunsOnK8s,
264
+ Annotated[RunsOnK8s, Tag(RunsOnType.K8S)],
265
+ Annotated[RunsOnSelfHosted, Tag(RunsOnType.SELF_HOSTED)],
266
+ Annotated[RunsOnLocal, Tag(RunsOnType.LOCAL)],
276
267
  ],
277
- Field(discriminator="type"),
268
+ Discriminator(get_discriminator_runs_on),
278
269
  ]
279
270
 
280
271
 
@@ -319,7 +310,7 @@ class Job(BaseModel):
319
310
  runs_on: RunsOn = Field(
320
311
  default_factory=RunsOnLocal,
321
312
  description="A target node for this job to use for execution.",
322
- serialization_alias="runs-on",
313
+ alias="runs-on",
323
314
  )
324
315
  stages: list[Stage] = Field(
325
316
  default_factory=list,
@@ -328,7 +319,7 @@ class Job(BaseModel):
328
319
  trigger_rule: TriggerRules = Field(
329
320
  default=TriggerRules.all_success,
330
321
  description="A trigger rule of tracking needed jobs.",
331
- serialization_alias="trigger-rule",
322
+ alias="trigger-rule",
332
323
  )
333
324
  needs: list[str] = Field(
334
325
  default_factory=list,
@@ -339,18 +330,6 @@ class Job(BaseModel):
339
330
  description="A strategy matrix that want to generate.",
340
331
  )
341
332
 
342
- @model_validator(mode="before")
343
- def __prepare_keys__(cls, values: DictData) -> DictData:
344
- """Rename key that use dash to underscore because Python does not
345
- support this character exist in any variable name.
346
-
347
- :param values: A passing value that coming for initialize this object.
348
- :rtype: DictData
349
- """
350
- dash2underscore("runs-on", values)
351
- dash2underscore("trigger-rule", values)
352
- return values
353
-
354
333
  @field_validator("desc", mode="after")
355
334
  def ___prepare_desc__(cls, value: str) -> str:
356
335
  """Prepare description string that was created on a template.
@@ -441,15 +420,14 @@ class Job(BaseModel):
441
420
 
442
421
  :rtype: DictData
443
422
  """
423
+ if "jobs" not in to:
424
+ to["jobs"] = {}
425
+
444
426
  if self.id is None and not config.job_default_id:
445
427
  raise JobException(
446
428
  "This job do not set the ID before setting execution output."
447
429
  )
448
430
 
449
- # NOTE: Create jobs key to receive an output from the job execution.
450
- if "jobs" not in to:
451
- to["jobs"] = {}
452
-
453
431
  # NOTE: If the job ID did not set, it will use index of jobs key
454
432
  # instead.
455
433
  _id: str = self.id or str(len(to["jobs"]) + 1)
@@ -458,11 +436,12 @@ class Job(BaseModel):
458
436
  {"errors": output.pop("errors", {})} if "errors" in output else {}
459
437
  )
460
438
 
461
- to["jobs"][_id] = (
462
- {"strategies": output, **errors}
463
- if self.strategy.is_set()
464
- else {**output.get(next(iter(output), "DUMMY"), {}), **errors}
465
- )
439
+ if self.strategy.is_set():
440
+ to["jobs"][_id] = {"strategies": output, **errors}
441
+ else:
442
+ _output = output.get(next(iter(output), "FIRST"), {})
443
+ _output.pop("matrix", {})
444
+ to["jobs"][_id] = {**_output, **errors}
466
445
  return to
467
446
 
468
447
  def execute(
@@ -472,6 +451,7 @@ class Job(BaseModel):
472
451
  run_id: str | None = None,
473
452
  parent_run_id: str | None = None,
474
453
  result: Result | None = None,
454
+ event: Event | None = None,
475
455
  ) -> Result:
476
456
  """Job execution with passing dynamic parameters from the workflow
477
457
  execution. It will generate matrix values at the first step and run
@@ -482,23 +462,35 @@ class Job(BaseModel):
482
462
  :param parent_run_id: A parent workflow running ID for this release.
483
463
  :param result: (Result) A result object for keeping context and status
484
464
  data.
465
+ :param event: (Event) An event manager that pass to the
466
+ PoolThreadExecutor.
485
467
 
486
468
  :rtype: Result
487
469
  """
488
- if result is None: # pragma: no cov
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)
470
+ result: Result = Result.construct_with_rs_or_id(
471
+ result,
472
+ run_id=run_id,
473
+ parent_run_id=parent_run_id,
474
+ id_logic=(self.id or "not-set"),
475
+ )
495
476
 
496
477
  if self.runs_on.type == RunsOnType.LOCAL:
497
478
  return local_execute(
498
479
  job=self,
499
480
  params=params,
500
481
  result=result,
482
+ event=event,
501
483
  )
484
+ elif self.runs_on.type == RunsOnType.SELF_HOSTED: # pragma: no cov
485
+ pass
486
+ elif self.runs_on.type == RunsOnType.K8S: # pragma: no cov
487
+ pass
488
+
489
+ # pragma: no cov
490
+ result.trace.error(
491
+ f"[JOB]: Job executor does not support for runs-on type: "
492
+ f"{self.runs_on.type} yet"
493
+ )
502
494
  raise NotImplementedError(
503
495
  f"The job runs-on other type: {self.runs_on.type} does not "
504
496
  f"support yet."
@@ -512,6 +504,7 @@ def local_execute_strategy(
512
504
  *,
513
505
  result: Result | None = None,
514
506
  event: Event | None = None,
507
+ raise_error: bool = False,
515
508
  ) -> Result:
516
509
  """Local job strategy execution with passing dynamic parameters from the
517
510
  workflow execution to strategy matrix.
@@ -533,11 +526,12 @@ def local_execute_strategy(
533
526
  :param result: (Result) A result object for keeping context and status
534
527
  data.
535
528
  :param event: (Event) An event manager that pass to the PoolThreadExecutor.
529
+ :param raise_error: (bool) A flag that all this method raise error
536
530
 
537
531
  :rtype: Result
538
532
  """
539
- if result is None: # pragma: no cov
540
- result: Result = Result(run_id=gen_id(job.id or "", unique=True))
533
+ if result is None:
534
+ result: Result = Result(run_id=gen_id(job.id or "not-set", unique=True))
541
535
 
542
536
  strategy_id: str = gen_id(strategy)
543
537
 
@@ -556,62 +550,43 @@ def local_execute_strategy(
556
550
  context: DictData = copy.deepcopy(params)
557
551
  context.update({"matrix": strategy, "stages": {}})
558
552
 
553
+ if strategy:
554
+ result.trace.info(f"[JOB]: Execute Strategy ID: {strategy_id}")
555
+ result.trace.info(f"[JOB]: ... Matrix: {strategy_id}")
556
+
559
557
  # IMPORTANT: The stage execution only run sequentially one-by-one.
560
558
  for stage in job.stages:
561
559
 
562
560
  if stage.is_skipped(params=context):
563
- result.trace.info(f"[JOB]: Skip stage: {stage.iden!r}")
561
+ result.trace.info(f"[STAGE]: Skip stage: {stage.iden!r}")
564
562
  continue
565
563
 
566
- result.trace.info(f"[JOB]: Execute stage: {stage.iden!r}")
567
-
568
- # NOTE: Logging a matrix that pass on this stage execution.
569
- if strategy:
570
- result.trace.info(f"[JOB]: ... Matrix: {strategy}")
571
-
572
- # NOTE: Force stop this execution if event was set from main
573
- # execution.
574
564
  if event and event.is_set():
575
565
  error_msg: str = (
576
566
  "Job strategy was canceled from event that had set before "
577
567
  "strategy execution."
578
568
  )
579
569
  return result.catch(
580
- status=1,
570
+ status=Status.FAILED,
581
571
  context={
582
572
  strategy_id: {
583
573
  "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
574
  "stages": context.pop("stages", {}),
590
- "errors": {
591
- "class": JobException(error_msg),
592
- "name": "JobException",
593
- "message": error_msg,
594
- },
575
+ "errors": JobException(error_msg).to_dict(),
595
576
  },
596
577
  },
597
578
  )
598
579
 
599
580
  # PARAGRAPH:
600
581
  #
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;
582
+ # This step will add the stage result to `stages` key in that
583
+ # stage id. It will have structure like;
609
584
  #
610
585
  # {
611
586
  # "params": { ... },
612
587
  # "jobs": { ... },
613
588
  # "matrix": { ... },
614
- # "stages": { { "stage-id-1": ... }, ... }
589
+ # "stages": { { "stage-id-01": { "outputs": { ... } } }, ... }
615
590
  # }
616
591
  #
617
592
  # IMPORTANT:
@@ -631,23 +606,19 @@ def local_execute_strategy(
631
606
  )
632
607
  except (StageException, UtilException) as err:
633
608
  result.trace.error(f"[JOB]: {err.__class__.__name__}: {err}")
634
- if config.job_raise_error:
609
+ if raise_error or config.job_raise_error:
635
610
  raise JobException(
636
611
  f"Stage execution error: {err.__class__.__name__}: "
637
612
  f"{err}"
638
613
  ) from None
639
614
 
640
615
  return result.catch(
641
- status=1,
616
+ status=Status.FAILED,
642
617
  context={
643
618
  strategy_id: {
644
619
  "matrix": strategy,
645
620
  "stages": context.pop("stages", {}),
646
- "errors": {
647
- "class": err,
648
- "name": err.__class__.__name__,
649
- "message": f"{err.__class__.__name__}: {err}",
650
- },
621
+ "errors": err.to_dict(),
651
622
  },
652
623
  },
653
624
  )
@@ -673,44 +644,61 @@ def local_execute(
673
644
  run_id: str | None = None,
674
645
  parent_run_id: str | None = None,
675
646
  result: Result | None = None,
647
+ event: Event | None = None,
648
+ raise_error: bool = False,
676
649
  ) -> Result:
677
650
  """Local job execution with passing dynamic parameters from the workflow
678
651
  execution. It will generate matrix values at the first step and run
679
652
  multithread on this metrics to the `stages` field of this job.
680
653
 
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.
654
+ This method does not raise any JobException if it runs with
655
+ multi-threading strategy.
656
+
657
+ :param job: (Job) A job model that want to execute.
658
+ :param params: (DictData) An input parameters that use on job execution.
659
+ :param run_id: (str) A job running ID for this execution.
660
+ :param parent_run_id: (str) A parent workflow running ID for this release.
685
661
  :param result: (Result) A result object for keeping context and status
686
662
  data.
663
+ :param event: (Event) An event manager that pass to the PoolThreadExecutor.
664
+ :param raise_error: (bool) A flag that all this method raise error to the
665
+ strategy execution.
687
666
 
688
667
  :rtype: Result
689
668
  """
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)
669
+ result: Result = Result.construct_with_rs_or_id(
670
+ result,
671
+ run_id=run_id,
672
+ parent_run_id=parent_run_id,
673
+ id_logic=(job.id or "not-set"),
674
+ )
675
+ event: Event = Event() if event is None else event
697
676
 
698
677
  # NOTE: Normal Job execution without parallel strategy matrix. It uses
699
678
  # for-loop to control strategy execution sequentially.
700
679
  if (not job.strategy.is_set()) or job.strategy.max_parallel == 1:
701
680
 
702
681
  for strategy in job.strategy.make():
703
- result: Result = local_execute_strategy(
682
+
683
+ # TODO: stop and raise error if the event was set.
684
+ local_execute_strategy(
704
685
  job=job,
705
686
  strategy=strategy,
706
687
  params=params,
707
688
  result=result,
689
+ event=event,
690
+ raise_error=raise_error,
708
691
  )
709
692
 
710
693
  return result.catch(status=Status.SUCCESS)
711
694
 
712
- # NOTE: Create event for cancel executor by trigger stop running event.
713
- event: Event = Event()
695
+ fail_fast_flag: bool = job.strategy.fail_fast
696
+ ls: str = "Fail-Fast" if fail_fast_flag else "All-Completed"
697
+
698
+ result.trace.info(
699
+ f"[JOB]: Start multithreading: {job.strategy.max_parallel} threads "
700
+ f"with {ls} mode."
701
+ )
714
702
 
715
703
  # IMPORTANT: Start running strategy execution by multithreading because
716
704
  # it will run by strategy values without waiting previous execution.
@@ -727,52 +715,43 @@ def local_execute(
727
715
  params=params,
728
716
  result=result,
729
717
  event=event,
718
+ raise_error=raise_error,
730
719
  )
731
720
  for strategy in job.strategy.make()
732
721
  ]
733
722
 
734
723
  context: DictData = {}
735
724
  status: Status = Status.SUCCESS
736
- fail_fast_flag: bool = job.strategy.fail_fast
737
725
 
738
726
  if not fail_fast_flag:
739
727
  done = as_completed(futures, timeout=1800)
740
728
  else:
741
- # NOTE: Get results from a collection of tasks with a timeout
742
- # that has the first exception.
743
729
  done, not_done = wait(
744
730
  futures, timeout=1800, return_when=FIRST_EXCEPTION
745
731
  )
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}")
750
732
 
751
- # NOTE: Stop all running tasks with setting the event manager
752
- # and cancel any scheduled tasks.
753
733
  if len(done) != len(futures):
734
+ result.trace.warning(
735
+ "[JOB]: Set the event for stop running stage."
736
+ )
754
737
  event.set()
755
738
  for future in not_done:
756
739
  future.cancel()
757
740
 
741
+ nd: str = (
742
+ f", the strategies do not run is {not_done}" if not_done else ""
743
+ )
744
+ result.trace.debug(f"[JOB]: Strategy is set Fail Fast{nd}")
745
+
758
746
  for future in done:
759
747
  try:
760
748
  future.result()
761
749
  except JobException as err:
762
750
  status = Status.FAILED
763
- ls: str = "Fail-Fast" if fail_fast_flag else "All-Completed"
764
751
  result.trace.error(
765
752
  f"[JOB]: {ls} Catch:\n\t{err.__class__.__name__}:"
766
753
  f"\n\t{err}"
767
754
  )
768
- context.update(
769
- {
770
- "errors": {
771
- "class": err,
772
- "name": err.__class__.__name__,
773
- "message": f"{err.__class__.__name__}: {err}",
774
- },
775
- },
776
- )
755
+ context.update({"errors": err.to_dict()})
777
756
 
778
757
  return result.catch(status=status, context=context)
ddeutil/workflow/logs.py CHANGED
@@ -234,7 +234,7 @@ class FileTraceLog(BaseTraceLog): # pragma: no cov
234
234
  return f"({self.cut_id}) {message}"
235
235
 
236
236
  def writer(self, message: str, is_err: bool = False) -> None:
237
- """ "Write a trace message after making to target file and write metadata
237
+ """Write a trace message after making to target file and write metadata
238
238
  in the same path of standard files.
239
239
 
240
240
  The path of logging data will store by format:
@@ -279,6 +279,11 @@ class FileTraceLog(BaseTraceLog): # pragma: no cov
279
279
  + "\n"
280
280
  )
281
281
 
282
+ async def awriter(
283
+ self, message: str, is_err: bool = False
284
+ ): # pragma: no cov
285
+ """TODO: Use `aiofiles` for make writer method support async."""
286
+
282
287
 
283
288
  class SQLiteTraceLog(BaseTraceLog): # pragma: no cov
284
289
  """Trace Log object that write trace log to the SQLite database file."""