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.
- ddeutil/workflow/__about__.py +1 -1
- ddeutil/workflow/__init__.py +4 -1
- ddeutil/workflow/api/routes/job.py +3 -1
- ddeutil/workflow/api/routes/logs.py +12 -4
- ddeutil/workflow/caller.py +6 -2
- ddeutil/workflow/context.py +59 -0
- ddeutil/workflow/exceptions.py +14 -1
- ddeutil/workflow/job.py +100 -121
- ddeutil/workflow/logs.py +6 -1
- ddeutil/workflow/result.py +1 -1
- ddeutil/workflow/stages.py +364 -111
- ddeutil/workflow/utils.py +1 -44
- ddeutil/workflow/workflow.py +137 -72
- {ddeutil_workflow-0.0.37.dist-info → ddeutil_workflow-0.0.38.dist-info}/METADATA +8 -2
- ddeutil_workflow-0.0.38.dist-info/RECORD +33 -0
- {ddeutil_workflow-0.0.37.dist-info → ddeutil_workflow-0.0.38.dist-info}/WHEEL +1 -1
- ddeutil_workflow-0.0.37.dist-info/RECORD +0 -32
- {ddeutil_workflow-0.0.37.dist-info → ddeutil_workflow-0.0.38.dist-info/licenses}/LICENSE +0 -0
- {ddeutil_workflow-0.0.37.dist-info → ddeutil_workflow-0.0.38.dist-info}/top_level.txt +0 -0
ddeutil/workflow/__about__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__: str = "0.0.
|
1
|
+
__version__: str = "0.0.38"
|
ddeutil/workflow/__init__.py
CHANGED
@@ -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=
|
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":
|
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(
|
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.
|
ddeutil/workflow/caller.py
CHANGED
@@ -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
|
-
|
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)
|
ddeutil/workflow/exceptions.py
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
274
|
-
RunsOnSelfHosted,
|
275
|
-
|
264
|
+
Annotated[RunsOnK8s, Tag(RunsOnType.K8S)],
|
265
|
+
Annotated[RunsOnSelfHosted, Tag(RunsOnType.SELF_HOSTED)],
|
266
|
+
Annotated[RunsOnLocal, Tag(RunsOnType.LOCAL)],
|
276
267
|
],
|
277
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
462
|
-
{"strategies": output, **errors}
|
463
|
-
|
464
|
-
|
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
|
-
|
489
|
-
result
|
490
|
-
|
491
|
-
|
492
|
-
)
|
493
|
-
|
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:
|
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"[
|
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=
|
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
|
-
#
|
602
|
-
#
|
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-
|
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=
|
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
|
-
|
682
|
-
|
683
|
-
|
684
|
-
:param
|
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
|
-
|
691
|
-
result
|
692
|
-
|
693
|
-
|
694
|
-
)
|
695
|
-
|
696
|
-
|
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
|
-
|
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
|
-
|
713
|
-
|
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
|
-
"""
|
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."""
|