ddeutil-workflow 0.0.81__tar.gz → 0.0.82__tar.gz
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-0.0.81/src/ddeutil_workflow.egg-info → ddeutil_workflow-0.0.82}/PKG-INFO +1 -1
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/pyproject.toml +0 -1
- ddeutil_workflow-0.0.82/src/ddeutil/workflow/__about__.py +2 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/__init__.py +19 -6
- ddeutil_workflow-0.0.81/src/ddeutil/workflow/cli.py → ddeutil_workflow-0.0.82/src/ddeutil/workflow/__main__.py +3 -4
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/api/routes/job.py +2 -2
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/api/routes/logs.py +8 -61
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/audits.py +46 -17
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/conf.py +45 -25
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/errors.py +9 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/job.py +70 -16
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/result.py +33 -11
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/stages.py +172 -134
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/traces.py +64 -24
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/utils.py +7 -4
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/workflow.py +66 -75
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82/src/ddeutil_workflow.egg-info}/PKG-INFO +1 -1
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil_workflow.egg-info/SOURCES.txt +0 -1
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/tests/test_audits.py +6 -5
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/tests/test_cli.py +1 -1
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/tests/test_conf.py +24 -4
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/tests/test_job.py +7 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/tests/test_job_exec.py +59 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/tests/test_result.py +11 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/tests/test_traces.py +19 -13
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/tests/test_utils.py +2 -4
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/tests/test_workflow_exec.py +175 -2
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/tests/test_workflow_release.py +65 -5
- ddeutil_workflow-0.0.81/src/ddeutil/workflow/__about__.py +0 -1
- ddeutil_workflow-0.0.81/src/ddeutil/workflow/__main__.py +0 -4
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/LICENSE +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/README.md +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/setup.cfg +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/__cron.py +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/__types.py +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/api/__init__.py +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/api/log_conf.py +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/api/routes/__init__.py +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/api/routes/workflows.py +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/event.py +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/params.py +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/plugins/__init__.py +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/plugins/providers/__init__.py +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/plugins/providers/aws.py +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/plugins/providers/az.py +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/plugins/providers/container.py +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/plugins/providers/gcs.py +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/reusables.py +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil_workflow.egg-info/dependency_links.txt +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil_workflow.egg-info/entry_points.txt +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil_workflow.egg-info/requires.txt +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil_workflow.egg-info/top_level.txt +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/tests/test__cron.py +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/tests/test__regex.py +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/tests/test_errors.py +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/tests/test_event.py +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/tests/test_job_exec_strategy.py +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/tests/test_params.py +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/tests/test_reusables_call_tag.py +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/tests/test_reusables_func_model.py +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/tests/test_reusables_template.py +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/tests/test_reusables_template_filter.py +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/tests/test_strategy.py +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/tests/test_workflow.py +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/tests/test_workflow_exec_job.py +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/tests/test_workflow_rerun.py +0 -0
@@ -91,7 +91,6 @@ omit = [
|
|
91
91
|
"src/ddeutil/workflow/__cron.py",
|
92
92
|
"src/ddeutil/workflow/__main__.py",
|
93
93
|
"src/ddeutil/workflow/__types.py",
|
94
|
-
"src/ddeutil/workflow/cli.py",
|
95
94
|
"src/ddeutil/workflow/api/__init__.py",
|
96
95
|
"src/ddeutil/workflow/api/log_conf.py",
|
97
96
|
"src/ddeutil/workflow/api/routes/__init__.py",
|
@@ -50,11 +50,25 @@ Note:
|
|
50
50
|
from .__cron import CronRunner
|
51
51
|
from .__types import DictData, DictStr, Matrix, Re, TupleStr
|
52
52
|
from .audits import (
|
53
|
+
EVENT,
|
54
|
+
FORCE,
|
55
|
+
NORMAL,
|
56
|
+
RERUN,
|
53
57
|
Audit,
|
54
58
|
FileAudit,
|
55
59
|
get_audit,
|
56
60
|
)
|
57
|
-
from .conf import
|
61
|
+
from .conf import (
|
62
|
+
PREFIX,
|
63
|
+
CallerSecret,
|
64
|
+
Config,
|
65
|
+
YamlParser,
|
66
|
+
api_config,
|
67
|
+
config,
|
68
|
+
dynamic,
|
69
|
+
env,
|
70
|
+
pass_env,
|
71
|
+
)
|
58
72
|
from .errors import (
|
59
73
|
BaseError,
|
60
74
|
JobCancelError,
|
@@ -63,6 +77,9 @@ from .errors import (
|
|
63
77
|
ResultError,
|
64
78
|
StageCancelError,
|
65
79
|
StageError,
|
80
|
+
StageNestedCancelError,
|
81
|
+
StageNestedError,
|
82
|
+
StageNestedSkipError,
|
66
83
|
StageSkipError,
|
67
84
|
UtilError,
|
68
85
|
WorkflowCancelError,
|
@@ -132,15 +149,11 @@ from .stages import (
|
|
132
149
|
VirtualPyStage,
|
133
150
|
)
|
134
151
|
from .traces import (
|
135
|
-
|
152
|
+
Trace,
|
136
153
|
get_trace,
|
137
154
|
)
|
138
155
|
from .utils import *
|
139
156
|
from .workflow import (
|
140
|
-
EVENT,
|
141
|
-
FORCE,
|
142
|
-
NORMAL,
|
143
|
-
RERUN,
|
144
157
|
ReleaseType,
|
145
158
|
Workflow,
|
146
159
|
)
|
@@ -238,13 +238,12 @@ def workflow_execute(
|
|
238
238
|
typer.echo(f"... with params: {params_dict}")
|
239
239
|
|
240
240
|
|
241
|
-
WORKFLOW_TYPE = Literal["Workflow"]
|
242
|
-
|
243
|
-
|
244
241
|
class WorkflowSchema(Workflow):
|
245
242
|
"""Override workflow model fields for generate JSON schema file."""
|
246
243
|
|
247
|
-
type:
|
244
|
+
type: Literal["Workflow"] = Field(
|
245
|
+
description="A type of workflow template that should be `Workflow`."
|
246
|
+
)
|
248
247
|
name: Optional[str] = Field(default=None, description="A workflow name.")
|
249
248
|
params: dict[str, Union[Param, str]] = Field(
|
250
249
|
default_factory=dict,
|
@@ -15,7 +15,7 @@ from fastapi.responses import UJSONResponse
|
|
15
15
|
from ...__types import DictData
|
16
16
|
from ...errors import JobError
|
17
17
|
from ...job import Job
|
18
|
-
from ...traces import
|
18
|
+
from ...traces import Trace, get_trace
|
19
19
|
from ...utils import gen_id
|
20
20
|
|
21
21
|
logger = logging.getLogger("uvicorn.error")
|
@@ -41,7 +41,7 @@ async def job_execute(
|
|
41
41
|
if extras:
|
42
42
|
job.extras = extras
|
43
43
|
|
44
|
-
trace:
|
44
|
+
trace: Trace = get_trace(
|
45
45
|
run_id, parent_run_id=parent_run_id, extras=job.extras
|
46
46
|
)
|
47
47
|
|
@@ -3,7 +3,7 @@
|
|
3
3
|
# Licensed under the MIT License. See LICENSE in the project root for
|
4
4
|
# license information.
|
5
5
|
# ------------------------------------------------------------------------------
|
6
|
-
"""This route include audit
|
6
|
+
"""This route include audit log path."""
|
7
7
|
from __future__ import annotations
|
8
8
|
|
9
9
|
from fastapi import APIRouter, Path, Query
|
@@ -11,7 +11,6 @@ from fastapi import status as st
|
|
11
11
|
from fastapi.responses import UJSONResponse
|
12
12
|
|
13
13
|
from ...audits import get_audit
|
14
|
-
from ...result import Result
|
15
14
|
|
16
15
|
router = APIRouter(
|
17
16
|
prefix="/logs",
|
@@ -20,63 +19,6 @@ router = APIRouter(
|
|
20
19
|
)
|
21
20
|
|
22
21
|
|
23
|
-
@router.get(
|
24
|
-
path="/traces/",
|
25
|
-
response_class=UJSONResponse,
|
26
|
-
status_code=st.HTTP_200_OK,
|
27
|
-
summary="Read all trace logs.",
|
28
|
-
tags=["trace"],
|
29
|
-
)
|
30
|
-
async def get_traces(
|
31
|
-
offset: int = Query(default=0, gt=0),
|
32
|
-
limit: int = Query(default=100, gt=0),
|
33
|
-
):
|
34
|
-
"""Return all trace logs from the current trace log path that config with
|
35
|
-
`WORKFLOW_LOG_PATH` environment variable name.
|
36
|
-
"""
|
37
|
-
result = Result()
|
38
|
-
return {
|
39
|
-
"message": (
|
40
|
-
f"Getting trace logs with offset: {offset} and limit: {limit}"
|
41
|
-
),
|
42
|
-
"traces": [
|
43
|
-
trace.model_dump(
|
44
|
-
by_alias=True,
|
45
|
-
exclude_none=True,
|
46
|
-
exclude_unset=True,
|
47
|
-
)
|
48
|
-
for trace in result.trace.find_traces()
|
49
|
-
],
|
50
|
-
}
|
51
|
-
|
52
|
-
|
53
|
-
@router.get(
|
54
|
-
path="/traces/{run_id}",
|
55
|
-
response_class=UJSONResponse,
|
56
|
-
status_code=st.HTTP_200_OK,
|
57
|
-
summary="Read trace log with specific running ID.",
|
58
|
-
tags=["trace"],
|
59
|
-
)
|
60
|
-
async def get_trace_with_id(run_id: str):
|
61
|
-
"""Return trace log with specific running ID from the current trace log path
|
62
|
-
that config with `WORKFLOW_LOG_PATH` environment variable name.
|
63
|
-
|
64
|
-
- **run_id**: A running ID that want to search a trace log from the log
|
65
|
-
path.
|
66
|
-
"""
|
67
|
-
result = Result()
|
68
|
-
return {
|
69
|
-
"message": f"Getting trace log with specific running ID: {run_id}",
|
70
|
-
"trace": (
|
71
|
-
result.trace.find_trace_with_id(run_id).model_dump(
|
72
|
-
by_alias=True,
|
73
|
-
exclude_none=True,
|
74
|
-
exclude_unset=True,
|
75
|
-
)
|
76
|
-
),
|
77
|
-
}
|
78
|
-
|
79
|
-
|
80
22
|
@router.get(
|
81
23
|
path="/audits/",
|
82
24
|
response_class=UJSONResponse,
|
@@ -84,12 +26,17 @@ async def get_trace_with_id(run_id: str):
|
|
84
26
|
summary="Read all audit logs.",
|
85
27
|
tags=["audit"],
|
86
28
|
)
|
87
|
-
async def get_audits(
|
29
|
+
async def get_audits(
|
30
|
+
offset: int = Query(default=0, gt=0),
|
31
|
+
limit: int = Query(default=100, gt=0),
|
32
|
+
):
|
88
33
|
"""Return all audit logs from the current audit log path that config with
|
89
34
|
`WORKFLOW_AUDIT_URL` environment variable name.
|
90
35
|
"""
|
91
36
|
return {
|
92
|
-
"message":
|
37
|
+
"message": (
|
38
|
+
f"Getting audit logs with offset: {offset} and limit: {limit}",
|
39
|
+
),
|
93
40
|
"audits": list(get_audit().find_audits(name="demo")),
|
94
41
|
}
|
95
42
|
|
@@ -49,33 +49,65 @@ import zlib
|
|
49
49
|
from abc import ABC, abstractmethod
|
50
50
|
from collections.abc import Iterator
|
51
51
|
from datetime import datetime, timedelta
|
52
|
+
from enum import Enum
|
52
53
|
from pathlib import Path
|
53
54
|
from typing import Annotated, Any, ClassVar, Literal, Optional, Union
|
54
55
|
from urllib.parse import ParseResult, urlparse
|
55
56
|
|
56
|
-
from pydantic import BaseModel, Field, TypeAdapter
|
57
|
+
from pydantic import BaseModel, ConfigDict, Field, TypeAdapter
|
57
58
|
from pydantic.functional_validators import field_validator, model_validator
|
58
59
|
from typing_extensions import Self
|
59
60
|
|
60
61
|
from .__types import DictData
|
61
62
|
from .conf import dynamic
|
62
|
-
from .traces import
|
63
|
+
from .traces import Trace, get_trace, set_logging
|
63
64
|
|
64
65
|
logger = logging.getLogger("ddeutil.workflow")
|
65
66
|
|
66
67
|
|
68
|
+
class ReleaseType(str, Enum):
|
69
|
+
"""Release type enumeration for workflow execution modes.
|
70
|
+
|
71
|
+
This enum defines the different types of workflow releases that can be
|
72
|
+
triggered, each with specific behavior and use cases.
|
73
|
+
|
74
|
+
Attributes:
|
75
|
+
NORMAL: Standard workflow release execution
|
76
|
+
RERUN: Re-execution of previously failed workflow
|
77
|
+
EVENT: Event-triggered workflow execution
|
78
|
+
FORCE: Forced execution bypassing normal conditions
|
79
|
+
"""
|
80
|
+
|
81
|
+
NORMAL = "normal"
|
82
|
+
RERUN = "rerun"
|
83
|
+
EVENT = "event"
|
84
|
+
FORCE = "force"
|
85
|
+
|
86
|
+
|
87
|
+
NORMAL = ReleaseType.NORMAL
|
88
|
+
RERUN = ReleaseType.RERUN
|
89
|
+
EVENT = ReleaseType.EVENT
|
90
|
+
FORCE = ReleaseType.FORCE
|
91
|
+
|
92
|
+
|
67
93
|
class AuditData(BaseModel):
|
94
|
+
"""Audit Data model."""
|
95
|
+
|
96
|
+
model_config = ConfigDict(use_enum_values=True)
|
97
|
+
|
68
98
|
name: str = Field(description="A workflow name.")
|
69
99
|
release: datetime = Field(description="A release datetime.")
|
70
|
-
type:
|
100
|
+
type: ReleaseType = Field(
|
101
|
+
default=NORMAL, description="A running type before logging."
|
102
|
+
)
|
71
103
|
context: DictData = Field(
|
72
104
|
default_factory=dict,
|
73
105
|
description="A context that receive from a workflow execution result.",
|
74
106
|
)
|
107
|
+
run_id: str = Field(description="A running ID")
|
75
108
|
parent_run_id: Optional[str] = Field(
|
76
109
|
default=None, description="A parent running ID."
|
77
110
|
)
|
78
|
-
run_id: str = Field(description="A running ID")
|
79
111
|
runs_metadata: DictData = Field(
|
80
112
|
default_factory=dict,
|
81
113
|
description="A runs metadata that will use to tracking this audit log.",
|
@@ -122,7 +154,7 @@ class BaseAudit(BaseModel, ABC):
|
|
122
154
|
@abstractmethod
|
123
155
|
def is_pointed(
|
124
156
|
self,
|
125
|
-
data:
|
157
|
+
data: Any,
|
126
158
|
*,
|
127
159
|
extras: Optional[DictData] = None,
|
128
160
|
) -> bool:
|
@@ -328,21 +360,21 @@ class FileAudit(BaseAudit):
|
|
328
360
|
return AuditData.model_validate(obj=json.load(f))
|
329
361
|
|
330
362
|
def is_pointed(
|
331
|
-
self,
|
363
|
+
self,
|
364
|
+
data: Any,
|
365
|
+
*,
|
366
|
+
extras: Optional[DictData] = None,
|
332
367
|
) -> bool:
|
333
368
|
"""Check if the release log already exists at the destination log path.
|
334
369
|
|
335
370
|
Args:
|
336
|
-
data:
|
371
|
+
data (str):
|
337
372
|
extras: Optional extra parameters to override core config.
|
338
373
|
|
339
374
|
Returns:
|
340
375
|
bool: True if the release log exists, False otherwise.
|
341
376
|
"""
|
342
|
-
|
343
|
-
if not dynamic("enable_write_audit", extras=extras):
|
344
|
-
return False
|
345
|
-
return self.pointer(data).exists()
|
377
|
+
return self.pointer(AuditData.model_validate(data)).exists()
|
346
378
|
|
347
379
|
def pointer(self, data: AuditData) -> Path:
|
348
380
|
"""Return release directory path generated from model data.
|
@@ -365,7 +397,7 @@ class FileAudit(BaseAudit):
|
|
365
397
|
Self: The audit instance after saving.
|
366
398
|
"""
|
367
399
|
audit = AuditData.model_validate(data)
|
368
|
-
trace:
|
400
|
+
trace: Trace = get_trace(
|
369
401
|
audit.run_id,
|
370
402
|
parent_run_id=audit.parent_run_id,
|
371
403
|
extras=self.extras,
|
@@ -655,7 +687,7 @@ class SQLiteAudit(BaseAudit): # pragma: no cov
|
|
655
687
|
ValueError: If SQLite database is not properly configured.
|
656
688
|
"""
|
657
689
|
audit = AuditData.model_validate(data)
|
658
|
-
trace:
|
690
|
+
trace: Trace = get_trace(
|
659
691
|
audit.run_id,
|
660
692
|
parent_run_id=audit.parent_run_id,
|
661
693
|
extras=self.extras,
|
@@ -748,10 +780,7 @@ Audit = Annotated[
|
|
748
780
|
]
|
749
781
|
|
750
782
|
|
751
|
-
def get_audit(
|
752
|
-
*,
|
753
|
-
extras: Optional[DictData] = None,
|
754
|
-
) -> Audit: # pragma: no cov
|
783
|
+
def get_audit(extras: Optional[DictData] = None) -> Audit: # pragma: no cov
|
755
784
|
"""Get an audit model dynamically based on the config audit path value.
|
756
785
|
|
757
786
|
Args:
|
@@ -176,17 +176,6 @@ class YamlParser:
|
|
176
176
|
"""Base Load object that use to search config data by given some identity
|
177
177
|
value like name of `Workflow` or `Crontab` templates.
|
178
178
|
|
179
|
-
:param name: (str) A name of key of config data that read with YAML
|
180
|
-
Environment object.
|
181
|
-
:param path: (Path) A config path object.
|
182
|
-
:param externals: (DictData) An external config data that want to add to
|
183
|
-
loaded config data.
|
184
|
-
:param extras: (DictDdata) An extra parameters that use to override core
|
185
|
-
config values.
|
186
|
-
|
187
|
-
:raise ValueError: If the data does not find on the config path with the
|
188
|
-
name parameter.
|
189
|
-
|
190
179
|
Noted:
|
191
180
|
The config data should have `type` key for modeling validation that
|
192
181
|
make this loader know what is config should to do pass to.
|
@@ -209,6 +198,23 @@ class YamlParser:
|
|
209
198
|
extras: Optional[DictData] = None,
|
210
199
|
obj: Optional[Union[object, str]] = None,
|
211
200
|
) -> None:
|
201
|
+
"""Main constructure function.
|
202
|
+
|
203
|
+
Args:
|
204
|
+
name (str): A name of key of config data that read with YAML
|
205
|
+
Environment object.
|
206
|
+
path (Path): A config path object.
|
207
|
+
externals (DictData): An external config data that want to add to
|
208
|
+
loaded config data.
|
209
|
+
extras (DictDdata): An extra parameters that use to override core
|
210
|
+
config values.
|
211
|
+
obj (object | str): An object that want to validate from the `type`
|
212
|
+
key before keeping the config data.
|
213
|
+
|
214
|
+
Raises:
|
215
|
+
ValueError: If the data does not find on the config path with the
|
216
|
+
name parameter.
|
217
|
+
"""
|
212
218
|
self.path: Path = Path(dynamic("conf_path", f=path, extras=extras))
|
213
219
|
self.externals: DictData = externals or {}
|
214
220
|
self.extras: DictData = extras or {}
|
@@ -242,17 +248,19 @@ class YamlParser:
|
|
242
248
|
"""Find data with specific key and return the latest modify date data if
|
243
249
|
this key exists multiple files.
|
244
250
|
|
245
|
-
:
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
251
|
+
Args:
|
252
|
+
name (str): A name of data that want to find.
|
253
|
+
path (Path): A config path object.
|
254
|
+
paths (list[Path]): A list of config path object.
|
255
|
+
obj (object | str): An object that want to validate matching
|
256
|
+
before return.
|
257
|
+
extras (DictData): An extra parameter that use to override core
|
258
|
+
config values.
|
259
|
+
ignore_filename (str): An ignore filename. Default is
|
260
|
+
``.confignore`` filename.
|
254
261
|
|
255
|
-
:
|
262
|
+
Returns:
|
263
|
+
DictData: A config data that was found on the searching paths.
|
256
264
|
"""
|
257
265
|
path: Path = dynamic("conf_path", f=path, extras=extras)
|
258
266
|
if not paths:
|
@@ -317,7 +325,9 @@ class YamlParser:
|
|
317
325
|
``.confignore`` filename.
|
318
326
|
tags (list[str]): A list of tag that want to filter.
|
319
327
|
|
320
|
-
:
|
328
|
+
Returns:
|
329
|
+
Iterator[tuple[str, DictData]]: An iterator of config data that was
|
330
|
+
found on the searching paths.
|
321
331
|
"""
|
322
332
|
excluded: list[str] = excluded or []
|
323
333
|
tags: list[str] = tags or []
|
@@ -353,8 +363,11 @@ class YamlParser:
|
|
353
363
|
):
|
354
364
|
continue
|
355
365
|
|
356
|
-
if (
|
357
|
-
|
366
|
+
if (
|
367
|
+
# isinstance(data, dict) and
|
368
|
+
(t := data.get("type"))
|
369
|
+
and t == obj_type
|
370
|
+
):
|
358
371
|
# NOTE: Start adding file metadata.
|
359
372
|
file_stat: os.stat_result = file.lstat()
|
360
373
|
data["created_at"] = file_stat.st_ctime
|
@@ -397,6 +410,13 @@ class YamlParser:
|
|
397
410
|
def filter_yaml(cls, file: Path, name: Optional[str] = None) -> DictData:
|
398
411
|
"""Read a YAML file context from an input file path and specific name.
|
399
412
|
|
413
|
+
Notes:
|
414
|
+
The data that will return from reading context will map with config
|
415
|
+
name if an input searching name does not pass to this function.
|
416
|
+
|
417
|
+
input: {"name": "foo", "type": "Some"}
|
418
|
+
output: {"foo": {"name": "foo", "type": "Some"}}
|
419
|
+
|
400
420
|
Args:
|
401
421
|
file (Path): A file path that want to extract YAML context.
|
402
422
|
name (str): A key name that search on a YAML context.
|
@@ -413,7 +433,7 @@ class YamlParser:
|
|
413
433
|
return (
|
414
434
|
values[name] | {"name": name} if name in values else {}
|
415
435
|
)
|
416
|
-
return values
|
436
|
+
return {values["name"]: values} if "name" in values else values
|
417
437
|
return {}
|
418
438
|
|
419
439
|
@cached_property
|
@@ -166,6 +166,15 @@ class StageCancelError(StageError): ...
|
|
166
166
|
class StageSkipError(StageError): ...
|
167
167
|
|
168
168
|
|
169
|
+
class StageNestedError(StageError): ...
|
170
|
+
|
171
|
+
|
172
|
+
class StageNestedCancelError(StageNestedError): ...
|
173
|
+
|
174
|
+
|
175
|
+
class StageNestedSkipError(StageNestedError): ...
|
176
|
+
|
177
|
+
|
169
178
|
class JobError(BaseError): ...
|
170
179
|
|
171
180
|
|
@@ -48,7 +48,7 @@ from enum import Enum
|
|
48
48
|
from functools import lru_cache
|
49
49
|
from textwrap import dedent
|
50
50
|
from threading import Event
|
51
|
-
from typing import Annotated, Any, Optional, Union
|
51
|
+
from typing import Annotated, Any, Literal, Optional, Union
|
52
52
|
|
53
53
|
from ddeutil.core import freeze_args
|
54
54
|
from pydantic import BaseModel, Discriminator, Field, SecretStr, Tag
|
@@ -72,7 +72,7 @@ from .result import (
|
|
72
72
|
)
|
73
73
|
from .reusables import has_template, param2template
|
74
74
|
from .stages import Stage
|
75
|
-
from .traces import
|
75
|
+
from .traces import Trace, get_trace
|
76
76
|
from .utils import cross_product, filter_func, gen_id
|
77
77
|
|
78
78
|
MatrixFilter = list[dict[str, Union[str, int]]]
|
@@ -187,10 +187,8 @@ class Strategy(BaseModel):
|
|
187
187
|
),
|
188
188
|
alias="fail-fast",
|
189
189
|
)
|
190
|
-
max_parallel: int = Field(
|
190
|
+
max_parallel: Union[int, str] = Field(
|
191
191
|
default=1,
|
192
|
-
gt=0,
|
193
|
-
lt=10,
|
194
192
|
description=(
|
195
193
|
"The maximum number of executor thread pool that want to run "
|
196
194
|
"parallel. This value should gather than 0 and less than 10."
|
@@ -427,9 +425,9 @@ class OnGCPBatch(BaseRunsOn): # pragma: no cov
|
|
427
425
|
args: GCPBatchArgs = Field(alias="with")
|
428
426
|
|
429
427
|
|
430
|
-
def get_discriminator_runs_on(
|
428
|
+
def get_discriminator_runs_on(data: dict[str, Any]) -> RunsOn:
|
431
429
|
"""Get discriminator of the RunsOn models."""
|
432
|
-
t: str =
|
430
|
+
t: str = data.get("type")
|
433
431
|
return RunsOn(t) if t else LOCAL
|
434
432
|
|
435
433
|
|
@@ -538,13 +536,28 @@ class Job(BaseModel):
|
|
538
536
|
description="An extra override config values.",
|
539
537
|
)
|
540
538
|
|
539
|
+
@field_validator(
|
540
|
+
"runs_on",
|
541
|
+
mode="before",
|
542
|
+
json_schema_input_type=Union[RunsOnModel, Literal["local"]],
|
543
|
+
)
|
544
|
+
def __prepare_runs_on(cls, data: Any) -> Any:
|
545
|
+
"""Prepare runs on value that was passed with string type."""
|
546
|
+
if isinstance(data, str):
|
547
|
+
if data != "local":
|
548
|
+
raise ValueError(
|
549
|
+
"runs-on that pass with str type should be `local` only"
|
550
|
+
)
|
551
|
+
return {"type": data}
|
552
|
+
return data
|
553
|
+
|
541
554
|
@field_validator("desc", mode="after")
|
542
|
-
def ___prepare_desc__(cls,
|
555
|
+
def ___prepare_desc__(cls, data: str) -> str:
|
543
556
|
"""Prepare description string that was created on a template.
|
544
557
|
|
545
558
|
:rtype: str
|
546
559
|
"""
|
547
|
-
return dedent(
|
560
|
+
return dedent(data.lstrip("\n"))
|
548
561
|
|
549
562
|
@field_validator("stages", mode="after")
|
550
563
|
def __validate_stage_id__(cls, value: list[Stage]) -> list[Stage]:
|
@@ -879,7 +892,7 @@ class Job(BaseModel):
|
|
879
892
|
ts: float = time.monotonic()
|
880
893
|
parent_run_id: str = run_id
|
881
894
|
run_id: str = gen_id((self.id or "EMPTY"), unique=True)
|
882
|
-
trace:
|
895
|
+
trace: Trace = get_trace(
|
883
896
|
run_id, parent_run_id=parent_run_id, extras=self.extras
|
884
897
|
)
|
885
898
|
trace.info(
|
@@ -1016,7 +1029,7 @@ def local_execute_strategy(
|
|
1016
1029
|
|
1017
1030
|
:rtype: tuple[Status, DictData]
|
1018
1031
|
"""
|
1019
|
-
trace:
|
1032
|
+
trace: Trace = get_trace(
|
1020
1033
|
run_id, parent_run_id=parent_run_id, extras=job.extras
|
1021
1034
|
)
|
1022
1035
|
if strategy:
|
@@ -1152,7 +1165,7 @@ def local_execute(
|
|
1152
1165
|
ts: float = time.monotonic()
|
1153
1166
|
parent_run_id: StrOrNone = run_id
|
1154
1167
|
run_id: str = gen_id((job.id or "EMPTY"), unique=True)
|
1155
|
-
trace:
|
1168
|
+
trace: Trace = get_trace(
|
1156
1169
|
run_id, parent_run_id=parent_run_id, extras=job.extras
|
1157
1170
|
)
|
1158
1171
|
context: DictData = {"status": WAIT}
|
@@ -1174,11 +1187,52 @@ def local_execute(
|
|
1174
1187
|
|
1175
1188
|
event: Event = event or Event()
|
1176
1189
|
ls: str = "Fail-Fast" if job.strategy.fail_fast else "All-Completed"
|
1177
|
-
workers: int = job.strategy.max_parallel
|
1190
|
+
workers: Union[int, str] = job.strategy.max_parallel
|
1191
|
+
if isinstance(workers, str):
|
1192
|
+
try:
|
1193
|
+
workers: int = int(
|
1194
|
+
param2template(workers, params=params, extras=job.extras)
|
1195
|
+
)
|
1196
|
+
except Exception as err:
|
1197
|
+
trace.exception(
|
1198
|
+
"[JOB]: Got the error on call param2template to "
|
1199
|
+
f"max-parallel value: {workers}"
|
1200
|
+
)
|
1201
|
+
return Result(
|
1202
|
+
run_id=run_id,
|
1203
|
+
parent_run_id=parent_run_id,
|
1204
|
+
status=FAILED,
|
1205
|
+
context=catch(
|
1206
|
+
context,
|
1207
|
+
status=FAILED,
|
1208
|
+
updated={"errors": to_dict(err)},
|
1209
|
+
),
|
1210
|
+
info={"execution_time": time.monotonic() - ts},
|
1211
|
+
extras=job.extras,
|
1212
|
+
)
|
1213
|
+
if workers >= 10:
|
1214
|
+
err_msg: str = (
|
1215
|
+
f"The max-parallel value should not more than 10, the current value "
|
1216
|
+
f"was set: {workers}."
|
1217
|
+
)
|
1218
|
+
trace.error(f"[JOB]: {err_msg}")
|
1219
|
+
return Result(
|
1220
|
+
run_id=run_id,
|
1221
|
+
parent_run_id=parent_run_id,
|
1222
|
+
status=FAILED,
|
1223
|
+
context=catch(
|
1224
|
+
context,
|
1225
|
+
status=FAILED,
|
1226
|
+
updated={"errors": JobError(err_msg).to_dict()},
|
1227
|
+
),
|
1228
|
+
info={"execution_time": time.monotonic() - ts},
|
1229
|
+
extras=job.extras,
|
1230
|
+
)
|
1231
|
+
|
1178
1232
|
strategies: list[DictStr] = job.strategy.make()
|
1179
1233
|
len_strategy: int = len(strategies)
|
1180
1234
|
trace.info(
|
1181
|
-
f"[JOB]:
|
1235
|
+
f"[JOB]: Mode {ls}: {job.id!r} with {workers} "
|
1182
1236
|
f"worker{'s' if workers > 1 else ''}."
|
1183
1237
|
)
|
1184
1238
|
|
@@ -1295,7 +1349,7 @@ def self_hosted_execute(
|
|
1295
1349
|
"""
|
1296
1350
|
parent_run_id: StrOrNone = run_id
|
1297
1351
|
run_id: str = gen_id((job.id or "EMPTY"), unique=True)
|
1298
|
-
trace:
|
1352
|
+
trace: Trace = get_trace(
|
1299
1353
|
run_id, parent_run_id=parent_run_id, extras=job.extras
|
1300
1354
|
)
|
1301
1355
|
context: DictData = {"status": WAIT}
|
@@ -1378,7 +1432,7 @@ def docker_execution(
|
|
1378
1432
|
"""
|
1379
1433
|
parent_run_id: StrOrNone = run_id
|
1380
1434
|
run_id: str = gen_id((job.id or "EMPTY"), unique=True)
|
1381
|
-
trace:
|
1435
|
+
trace: Trace = get_trace(
|
1382
1436
|
run_id, parent_run_id=parent_run_id, extras=job.extras
|
1383
1437
|
)
|
1384
1438
|
context: DictData = {"status": WAIT}
|