ddeutil-workflow 0.0.33__py3-none-any.whl → 0.0.35__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.
@@ -60,7 +60,7 @@ def tag(
60
60
 
61
61
  @wraps(func)
62
62
  def wrapped(*args: P.args, **kwargs: P.kwargs) -> TagFunc:
63
- # NOTE: Able to do anything before calling hook function.
63
+ # NOTE: Able to do anything before calling the call function.
64
64
  return func(*args, **kwargs)
65
65
 
66
66
  return wrapped
@@ -79,9 +79,9 @@ def make_registry(submodule: str) -> dict[str, Registry]:
79
79
  :rtype: dict[str, Registry]
80
80
  """
81
81
  rs: dict[str, Registry] = {}
82
- regis_hooks: list[str] = config.regis_hook
83
- regis_hooks.extend(["ddeutil.vendors"])
84
- for module in regis_hooks:
82
+ regis_calls: list[str] = config.regis_call
83
+ regis_calls.extend(["ddeutil.vendors"])
84
+ for module in regis_calls:
85
85
  # NOTE: try to sequential import task functions
86
86
  try:
87
87
  importer = import_module(f"{module}.{submodule}")
@@ -114,9 +114,9 @@ def make_registry(submodule: str) -> dict[str, Registry]:
114
114
 
115
115
 
116
116
  @dataclass(frozen=True)
117
- class HookSearchData:
118
- """Hook Search dataclass that use for receive regular expression grouping
119
- dict from searching hook string value.
117
+ class CallSearchData:
118
+ """Call Search dataclass that use for receive regular expression grouping
119
+ dict from searching call string value.
120
120
  """
121
121
 
122
122
  path: str
@@ -124,49 +124,49 @@ class HookSearchData:
124
124
  tag: str
125
125
 
126
126
 
127
- def extract_hook(hook: str) -> Callable[[], TagFunc]:
128
- """Extract Hook function from string value to hook partial function that
127
+ def extract_call(call: str) -> Callable[[], TagFunc]:
128
+ """Extract Call function from string value to call partial function that
129
129
  does run it at runtime.
130
130
 
131
- :raise NotImplementedError: When the searching hook's function result does
131
+ :raise NotImplementedError: When the searching call's function result does
132
132
  not exist in the registry.
133
- :raise NotImplementedError: When the searching hook's tag result does not
133
+ :raise NotImplementedError: When the searching call's tag result does not
134
134
  exist in the registry with its function key.
135
135
 
136
- :param hook: A hook value that able to match with Task regex.
136
+ :param call: A call value that able to match with Task regex.
137
137
 
138
- The format of hook value should contain 3 regular expression groups
138
+ The format of call value should contain 3 regular expression groups
139
139
  which match with the below config format:
140
140
 
141
141
  >>> "^(?P<path>[^/@]+)/(?P<func>[^@]+)@(?P<tag>.+)$"
142
142
 
143
143
  Examples:
144
- >>> extract_hook("tasks/el-postgres-to-delta@polars")
144
+ >>> extract_call("tasks/el-postgres-to-delta@polars")
145
145
  ...
146
- >>> extract_hook("tasks/return-type-not-valid@raise")
146
+ >>> extract_call("tasks/return-type-not-valid@raise")
147
147
  ...
148
148
 
149
149
  :rtype: Callable[[], TagFunc]
150
150
  """
151
- if not (found := Re.RE_TASK_FMT.search(hook)):
151
+ if not (found := Re.RE_TASK_FMT.search(call)):
152
152
  raise ValueError(
153
- f"Hook {hook!r} does not match with hook format regex."
153
+ f"Call {call!r} does not match with the call regex format."
154
154
  )
155
155
 
156
- # NOTE: Pass the searching hook string to `path`, `func`, and `tag`.
157
- hook: HookSearchData = HookSearchData(**found.groupdict())
156
+ # NOTE: Pass the searching call string to `path`, `func`, and `tag`.
157
+ call: CallSearchData = CallSearchData(**found.groupdict())
158
158
 
159
159
  # NOTE: Registry object should implement on this package only.
160
- rgt: dict[str, Registry] = make_registry(f"{hook.path}")
161
- if hook.func not in rgt:
160
+ rgt: dict[str, Registry] = make_registry(f"{call.path}")
161
+ if call.func not in rgt:
162
162
  raise NotImplementedError(
163
- f"``REGISTER-MODULES.{hook.path}.registries`` does not "
164
- f"implement registry: {hook.func!r}."
163
+ f"`REGISTER-MODULES.{call.path}.registries` does not "
164
+ f"implement registry: {call.func!r}."
165
165
  )
166
166
 
167
- if hook.tag not in rgt[hook.func]:
167
+ if call.tag not in rgt[call.func]:
168
168
  raise NotImplementedError(
169
- f"tag: {hook.tag!r} does not found on registry func: "
170
- f"``REGISTER-MODULES.{hook.path}.registries.{hook.func}``"
169
+ f"tag: {call.tag!r} does not found on registry func: "
170
+ f"`REGISTER-MODULES.{call.path}.registries.{call.func}`"
171
171
  )
172
- return rgt[hook.func][hook.tag]
172
+ return rgt[call.func][call.tag]
ddeutil/workflow/conf.py CHANGED
@@ -38,6 +38,7 @@ __all__: TupleStr = (
38
38
  "SimLoad",
39
39
  "Loader",
40
40
  "config",
41
+ "glob_files",
41
42
  )
42
43
 
43
44
 
@@ -97,9 +98,9 @@ class Config(BaseConfig): # pragma: no cov
97
98
 
98
99
  # NOTE: Register
99
100
  @property
100
- def regis_hook(self) -> list[str]:
101
- regis_hook_str: str = env("CORE_REGISTRY", ".")
102
- return [r.strip() for r in regis_hook_str.split(",")]
101
+ def regis_call(self) -> list[str]:
102
+ regis_call_str: str = env("CORE_REGISTRY", ".")
103
+ return [r.strip() for r in regis_call_str.split(",")]
103
104
 
104
105
  @property
105
106
  def regis_filter(self) -> list[str]:
@@ -129,16 +130,26 @@ class Config(BaseConfig): # pragma: no cov
129
130
  )
130
131
 
131
132
  @property
132
- def enable_rotate_file(self) -> bool:
133
- return str2bool(env("LOG_ENABLE_ROTATED_FILE", "false"))
133
+ def log_format_file(self) -> str:
134
+ return env(
135
+ "LOG_FORMAT_FILE",
136
+ (
137
+ "{datetime} ({process:5d}, {thread:5d}) {message:120s} "
138
+ "({filename}:{lineno})"
139
+ ),
140
+ )
141
+
142
+ @property
143
+ def enable_write_log(self) -> bool:
144
+ return str2bool(env("LOG_ENABLE_WRITE", "false"))
134
145
 
135
146
  # NOTE: Audit Log
136
147
  @property
137
148
  def audit_path(self) -> Path:
138
- return Path(env("AUDIT_PATH", "./logs"))
149
+ return Path(env("AUDIT_PATH", "./audits"))
139
150
 
140
151
  @property
141
- def enable_write_log(self) -> bool:
152
+ def enable_write_audit(self) -> bool:
142
153
  return str2bool(env("AUDIT_ENABLE_WRITE", "false"))
143
154
 
144
155
  @property
@@ -254,18 +265,22 @@ class SimLoad:
254
265
  conf_path: Path,
255
266
  externals: DictData | None = None,
256
267
  ) -> None:
268
+ self.conf_path: Path = conf_path
269
+ self.externals: DictData = externals or {}
270
+
257
271
  self.data: DictData = {}
258
272
  for file in glob_files(conf_path):
259
273
 
260
- if data := self.filter_suffix(file, name):
274
+ if self.is_ignore(file, conf_path):
275
+ continue
276
+
277
+ if data := self.filter_suffix(file, name=name):
261
278
  self.data = data
262
279
 
263
280
  # VALIDATE: check the data that reading should not empty.
264
281
  if not self.data:
265
282
  raise ValueError(f"Config {name!r} does not found on conf path")
266
283
 
267
- self.conf_path: Path = conf_path
268
- self.externals: DictData = externals or {}
269
284
  self.data.update(self.externals)
270
285
 
271
286
  @classmethod
@@ -283,8 +298,10 @@ class SimLoad:
283
298
 
284
299
  :param obj: An object that want to validate matching before return.
285
300
  :param conf_path: A config object.
286
- :param included:
287
- :param excluded:
301
+ :param included: An excluded list of data key that want to reject this
302
+ data if any key exist.
303
+ :param excluded: An included list of data key that want to filter from
304
+ data.
288
305
 
289
306
  :rtype: Iterator[tuple[str, DictData]]
290
307
  """
@@ -293,6 +310,9 @@ class SimLoad:
293
310
 
294
311
  for key, data in cls.filter_suffix(file).items():
295
312
 
313
+ if cls.is_ignore(file, conf_path):
314
+ continue
315
+
296
316
  if key in exclude:
297
317
  continue
298
318
 
@@ -303,11 +323,26 @@ class SimLoad:
303
323
  else data
304
324
  )
305
325
 
326
+ @classmethod
327
+ def is_ignore(cls, file: Path, conf_path: Path) -> bool:
328
+ ignore_file: Path = conf_path / ".confignore"
329
+ ignore: list[str] = []
330
+ if ignore_file.exists():
331
+ ignore = ignore_file.read_text(encoding="utf-8").splitlines()
332
+
333
+ if any(
334
+ (file.match(f"**/{pattern}/*") or file.match(f"**/{pattern}*"))
335
+ for pattern in ignore
336
+ ):
337
+ return True
338
+ return False
339
+
306
340
  @classmethod
307
341
  def filter_suffix(cls, file: Path, name: str | None = None) -> DictData:
308
342
  if any(file.suffix.endswith(s) for s in (".yml", ".yaml")):
309
343
  values: DictData = YamlFlResolve(file).read()
310
344
  return values.get(name, {}) if name else values
345
+
311
346
  return {}
312
347
 
313
348
  @cached_property
ddeutil/workflow/job.py CHANGED
@@ -5,7 +5,7 @@
5
5
  # ------------------------------------------------------------------------------
6
6
  """Job Model that use for keeping stages and node that running its stages.
7
7
  The job handle the lineage of stages and location of execution of stages that
8
- mean the job model able to define ``runs-on`` key that allow you to run this
8
+ mean the job model able to define `runs-on` key that allow you to run this
9
9
  job.
10
10
 
11
11
  This module include Strategy Model that use on the job strategy field.
@@ -24,10 +24,10 @@ from enum import Enum
24
24
  from functools import lru_cache
25
25
  from textwrap import dedent
26
26
  from threading import Event
27
- from typing import Any, Optional, Union
27
+ from typing import Annotated, Any, Literal, Optional, Union
28
28
 
29
29
  from ddeutil.core import freeze_args
30
- from pydantic import BaseModel, Field
30
+ from pydantic import BaseModel, ConfigDict, Field
31
31
  from pydantic.functional_validators import field_validator, model_validator
32
32
  from typing_extensions import Self
33
33
 
@@ -39,7 +39,7 @@ from .exceptions import (
39
39
  UtilException,
40
40
  )
41
41
  from .result import Result, Status
42
- from .stage import Stage
42
+ from .stages import Stage
43
43
  from .templates import has_template
44
44
  from .utils import (
45
45
  cross_product,
@@ -56,6 +56,11 @@ __all__: TupleStr = (
56
56
  "Strategy",
57
57
  "Job",
58
58
  "TriggerRules",
59
+ "RunsOn",
60
+ "RunsOnLocal",
61
+ "RunsOnSelfHosted",
62
+ "RunsOnDocker",
63
+ "RunsOnK8s",
59
64
  "make",
60
65
  )
61
66
 
@@ -216,13 +221,60 @@ class TriggerRules(str, Enum):
216
221
  none_skipped: str = "none_skipped"
217
222
 
218
223
 
219
- class RunsOn(str, Enum):
224
+ class RunsOnType(str, Enum):
220
225
  """Runs-On enum object."""
221
226
 
222
- local: str = "local"
223
- docker: str = "docker"
224
- self_hosted: str = "self_hosted"
225
- k8s: str = "k8s"
227
+ LOCAL: str = "local"
228
+ DOCKER: str = "docker"
229
+ SELF_HOSTED: str = "self_hosted"
230
+ K8S: str = "k8s"
231
+
232
+
233
+ class BaseRunsOn(BaseModel):
234
+ model_config = ConfigDict(use_enum_values=True)
235
+
236
+ type: Literal[RunsOnType.LOCAL]
237
+ args: DictData = Field(
238
+ default_factory=dict,
239
+ alias="with",
240
+ )
241
+
242
+
243
+ class RunsOnLocal(BaseRunsOn):
244
+ """Runs-on local."""
245
+
246
+ type: Literal[RunsOnType.LOCAL] = Field(default=RunsOnType.LOCAL)
247
+
248
+
249
+ class RunsOnSelfHosted(BaseRunsOn):
250
+ """Runs-on self-hosted."""
251
+
252
+ type: Literal[RunsOnType.SELF_HOSTED] = Field(
253
+ default=RunsOnType.SELF_HOSTED
254
+ )
255
+
256
+
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):
264
+ """Runs-on Kubernetes."""
265
+
266
+ type: Literal[RunsOnType.K8S] = Field(default=RunsOnType.K8S)
267
+
268
+
269
+ RunsOn = Annotated[
270
+ Union[
271
+ RunsOnLocal,
272
+ RunsOnSelfHosted,
273
+ RunsOnDocker,
274
+ RunsOnK8s,
275
+ ],
276
+ Field(discriminator="type"),
277
+ ]
226
278
 
227
279
 
228
280
  class Job(BaseModel):
@@ -263,9 +315,9 @@ class Job(BaseModel):
263
315
  default=None,
264
316
  description="A job description that can be string of markdown content.",
265
317
  )
266
- runs_on: Optional[str] = Field(
267
- default=None,
268
- description="A target executor node for this job use to execution.",
318
+ runs_on: RunsOn = Field(
319
+ default_factory=RunsOnLocal,
320
+ description="A target node for this job to use for execution.",
269
321
  serialization_alias="runs-on",
270
322
  )
271
323
  stages: list[Stage] = Field(
@@ -359,7 +411,7 @@ class Job(BaseModel):
359
411
 
360
412
  def set_outputs(self, output: DictData, to: DictData) -> DictData:
361
413
  """Set an outputs from execution process to the received context. The
362
- result from execution will pass to value of ``strategies`` key.
414
+ result from execution will pass to value of `strategies` key.
363
415
 
364
416
  For example of setting output method, If you receive execute output
365
417
  and want to set on the `to` like;
@@ -400,10 +452,15 @@ class Job(BaseModel):
400
452
  # NOTE: If the job ID did not set, it will use index of jobs key
401
453
  # instead.
402
454
  _id: str = self.id or str(len(to["jobs"]) + 1)
455
+
456
+ errors: DictData = (
457
+ {"errors": output.pop("errors", {})} if "errors" in output else {}
458
+ )
459
+
403
460
  to["jobs"][_id] = (
404
- {"strategies": output}
461
+ {"strategies": output, **errors}
405
462
  if self.strategy.is_set()
406
- else output.get(next(iter(output), "DUMMY"), {})
463
+ else {**output.get(next(iter(output), "DUMMY"), {}), **errors}
407
464
  )
408
465
  return to
409
466
 
@@ -412,7 +469,6 @@ class Job(BaseModel):
412
469
  strategy: DictData,
413
470
  params: DictData,
414
471
  *,
415
- run_id: str | None = None,
416
472
  result: Result | None = None,
417
473
  event: Event | None = None,
418
474
  ) -> Result:
@@ -420,19 +476,18 @@ class Job(BaseModel):
420
476
  workflow execution to strategy matrix.
421
477
 
422
478
  This execution is the minimum level of execution of this job model.
423
- It different with ``self.execute`` because this method run only one
479
+ It different with `self.execute` because this method run only one
424
480
  strategy and return with context of this strategy data.
425
481
 
426
482
  The result of this execution will return result with strategy ID
427
483
  that generated from the `gen_id` function with an input strategy value.
428
484
 
429
- :raise JobException: If it has any error from ``StageException`` or
430
- ``UtilException``.
485
+ :raise JobException: If it has any error from `StageException` or
486
+ `UtilException`.
431
487
 
432
488
  :param strategy: A strategy metrix value that use on this execution.
433
489
  This value will pass to the `matrix` key for templating.
434
490
  :param params: A dynamic parameters that will deepcopy to the context.
435
- :param run_id: A job running ID for this strategy execution.
436
491
  :param result: (Result) A result object for keeping context and status
437
492
  data.
438
493
  :param event: An event manager that pass to the PoolThreadExecutor.
@@ -440,9 +495,7 @@ class Job(BaseModel):
440
495
  :rtype: Result
441
496
  """
442
497
  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
- )
498
+ result: Result = Result(run_id=gen_id(self.id or "", unique=True))
446
499
 
447
500
  strategy_id: str = gen_id(strategy)
448
501
 
@@ -492,8 +545,11 @@ class Job(BaseModel):
492
545
  # "stages": filter_func(context.pop("stages", {})),
493
546
  #
494
547
  "stages": context.pop("stages", {}),
495
- "error": JobException(error_msg),
496
- "error_message": error_msg,
548
+ "errors": {
549
+ "class": JobException(error_msg),
550
+ "name": "JobException",
551
+ "message": error_msg,
552
+ },
497
553
  },
498
554
  },
499
555
  )
@@ -506,7 +562,7 @@ class Job(BaseModel):
506
562
  #
507
563
  # ... params |= stage.execute(params=params)
508
564
  #
509
- # This step will add the stage result to ``stages`` key in
565
+ # This step will add the stage result to `stages` key in
510
566
  # that stage id. It will have structure like;
511
567
  #
512
568
  # {
@@ -516,10 +572,18 @@ class Job(BaseModel):
516
572
  # "stages": { { "stage-id-1": ... }, ... }
517
573
  # }
518
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
+ #
519
581
  try:
520
582
  stage.set_outputs(
521
583
  stage.handler_execute(
522
- params=context, run_id=result.run_id
584
+ params=context,
585
+ run_id=result.run_id,
586
+ parent_run_id=result.parent_run_id,
523
587
  ).context,
524
588
  to=context,
525
589
  )
@@ -527,17 +591,21 @@ class Job(BaseModel):
527
591
  result.trace.error(f"[JOB]: {err.__class__.__name__}: {err}")
528
592
  if config.job_raise_error:
529
593
  raise JobException(
530
- f"Get stage execution error: {err.__class__.__name__}: "
594
+ f"Stage execution error: {err.__class__.__name__}: "
531
595
  f"{err}"
532
596
  ) from None
597
+
533
598
  return result.catch(
534
599
  status=1,
535
600
  context={
536
601
  strategy_id: {
537
602
  "matrix": strategy,
538
603
  "stages": context.pop("stages", {}),
539
- "error": err,
540
- "error_message": f"{err.__class__.__name__}: {err}",
604
+ "errors": {
605
+ "class": err,
606
+ "name": err.__class__.__name__,
607
+ "message": f"{err.__class__.__name__}: {err}",
608
+ },
541
609
  },
542
610
  },
543
611
  )
@@ -560,25 +628,28 @@ class Job(BaseModel):
560
628
  params: DictData,
561
629
  *,
562
630
  run_id: str | None = None,
631
+ parent_run_id: str | None = None,
563
632
  result: Result | None = None,
564
633
  ) -> Result:
565
634
  """Job execution with passing dynamic parameters from the workflow
566
635
  execution. It will generate matrix values at the first step and run
567
- multithread on this metrics to the ``stages`` field of this job.
636
+ multithread on this metrics to the `stages` field of this job.
568
637
 
569
638
  :param params: An input parameters that use on job execution.
570
639
  :param run_id: A job running ID for this execution.
640
+ :param parent_run_id: A parent workflow running ID for this release.
571
641
  :param result: (Result) A result object for keeping context and status
572
642
  data.
573
643
 
574
644
  :rtype: Result
575
645
  """
576
-
577
- # NOTE: I use this condition because this method allow passing empty
578
- # params and I do not want to create new dict object.
579
646
  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)
647
+ result: Result = Result(
648
+ run_id=(run_id or gen_id(self.id or "", unique=True)),
649
+ parent_run_id=parent_run_id,
650
+ )
651
+ elif parent_run_id: # pragma: no cov
652
+ result.set_parent_run_id(parent_run_id)
582
653
 
583
654
  # NOTE: Normal Job execution without parallel strategy matrix. It uses
584
655
  # for-loop to control strategy execution sequentially.
@@ -614,110 +685,50 @@ class Job(BaseModel):
614
685
  for strategy in self.strategy.make()
615
686
  ]
616
687
 
617
- return (
618
- self.__catch_fail_fast(event, futures=futures, result=result)
619
- if self.strategy.fail_fast
620
- else self.__catch_all_completed(futures=futures, result=result)
621
- )
622
-
623
- @staticmethod
624
- def __catch_fail_fast(
625
- event: Event,
626
- futures: list[Future],
627
- result: Result,
628
- *,
629
- timeout: int = 1800,
630
- ) -> Result:
631
- """Job parallel pool futures catching with fail-fast mode. That will
632
- stop and set event on all not done futures if it receives the first
633
- exception from all running futures.
634
-
635
- :param event: An event manager instance that able to set stopper on the
636
- observing multithreading.
637
- :param futures: A list of futures.
638
- :param result: (Result) A result object for keeping context and status
639
- data.
640
- :param timeout: A timeout to waiting all futures complete.
641
-
642
- :rtype: Result
643
- """
644
- context: DictData = {}
645
- status: Status = Status.SUCCESS
688
+ context: DictData = {}
689
+ status: Status = Status.SUCCESS
690
+ fail_fast_flag: bool = self.strategy.fail_fast
646
691
 
647
- # NOTE: Get results from a collection of tasks with a timeout that has
648
- # the first exception.
649
- done, not_done = wait(
650
- futures, timeout=timeout, return_when=FIRST_EXCEPTION
651
- )
652
- nd: str = (
653
- f", the strategies do not run is {not_done}" if not_done else ""
654
- )
655
- result.trace.debug(f"[JOB]: Strategy is set Fail Fast{nd}")
656
-
657
- # NOTE:
658
- # Stop all running tasks with setting the event manager and cancel
659
- # any scheduled tasks.
660
- #
661
- if len(done) != len(futures):
662
- event.set()
663
- for future in not_done:
664
- future.cancel()
665
-
666
- future: Future
667
- for future in done:
668
-
669
- # NOTE: Handle the first exception from feature
670
- if err := future.exception():
671
- status: Status = Status.FAILED
672
- result.trace.error(
673
- f"[JOB]: Fail-fast catching:\n\t{future.exception()}"
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
674
697
  )
675
- context.update(
676
- {
677
- "error": err,
678
- "error_message": f"{err.__class__.__name__}: {err}",
679
- },
680
- )
681
- continue
682
-
683
- # NOTE: Update the result context to main job context.
684
- future.result()
685
-
686
- return result.catch(status=status, context=context)
687
-
688
- @staticmethod
689
- def __catch_all_completed(
690
- futures: list[Future],
691
- result: Result,
692
- *,
693
- timeout: int = 1800,
694
- ) -> Result:
695
- """Job parallel pool futures catching with all-completed mode.
696
-
697
- :param futures: A list of futures.
698
- :param result: (Result) A result object for keeping context and status
699
- data.
700
- :param timeout: A timeout to waiting all futures complete.
701
-
702
- :rtype: Result
703
- """
704
- context: DictData = {}
705
- status: Status = Status.SUCCESS
706
-
707
- for future in as_completed(futures, timeout=timeout):
708
- try:
709
- future.result()
710
- except JobException as err:
711
- status = Status.FAILED
712
- result.trace.error(
713
- f"[JOB]: All-completed catching:\n\t"
714
- f"{err.__class__.__name__}:\n\t{err}"
715
- )
716
- context.update(
717
- {
718
- "error": err,
719
- "error_message": f"{err.__class__.__name__}: {err}",
720
- },
698
+ nd: str = (
699
+ f", the strategies do not run is {not_done}"
700
+ if not_done
701
+ else ""
721
702
  )
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
+ },
731
+ },
732
+ )
722
733
 
723
734
  return result.catch(status=status, context=context)