ddeutil-workflow 0.0.80__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.80/src/ddeutil_workflow.egg-info → ddeutil_workflow-0.0.82}/PKG-INFO +1 -1
- {ddeutil_workflow-0.0.80 → 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.80 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/__init__.py +19 -6
- ddeutil_workflow-0.0.80/src/ddeutil/workflow/cli.py → ddeutil_workflow-0.0.82/src/ddeutil/workflow/__main__.py +42 -33
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/api/routes/job.py +2 -2
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/api/routes/logs.py +8 -61
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/audits.py +46 -17
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/conf.py +64 -44
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/errors.py +12 -2
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/job.py +70 -16
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/result.py +33 -11
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/reusables.py +16 -17
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/stages.py +172 -134
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/traces.py +64 -24
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/utils.py +7 -15
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/workflow.py +73 -84
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82/src/ddeutil_workflow.egg-info}/PKG-INFO +1 -1
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil_workflow.egg-info/SOURCES.txt +0 -1
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/tests/test_audits.py +10 -6
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/tests/test_cli.py +1 -1
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/tests/test_conf.py +81 -29
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/tests/test_job.py +7 -0
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/tests/test_job_exec.py +59 -0
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/tests/test_result.py +11 -0
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/tests/test_traces.py +19 -13
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/tests/test_utils.py +2 -4
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/tests/test_workflow_exec.py +175 -2
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/tests/test_workflow_release.py +65 -5
- ddeutil_workflow-0.0.80/src/ddeutil/workflow/__about__.py +0 -1
- ddeutil_workflow-0.0.80/src/ddeutil/workflow/__main__.py +0 -4
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/LICENSE +0 -0
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/README.md +0 -0
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/setup.cfg +0 -0
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/__cron.py +0 -0
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/__types.py +0 -0
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/api/__init__.py +0 -0
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/api/log_conf.py +0 -0
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/api/routes/__init__.py +0 -0
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/api/routes/workflows.py +0 -0
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/event.py +0 -0
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/params.py +0 -0
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/plugins/__init__.py +0 -0
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/plugins/providers/__init__.py +0 -0
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/plugins/providers/aws.py +0 -0
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/plugins/providers/az.py +0 -0
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/plugins/providers/container.py +0 -0
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/plugins/providers/gcs.py +0 -0
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil_workflow.egg-info/dependency_links.txt +0 -0
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil_workflow.egg-info/entry_points.txt +0 -0
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil_workflow.egg-info/requires.txt +0 -0
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil_workflow.egg-info/top_level.txt +0 -0
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/tests/test__cron.py +0 -0
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/tests/test__regex.py +0 -0
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/tests/test_errors.py +0 -0
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/tests/test_event.py +0 -0
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/tests/test_job_exec_strategy.py +0 -0
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/tests/test_params.py +0 -0
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/tests/test_reusables_call_tag.py +0 -0
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/tests/test_reusables_func_model.py +0 -0
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/tests/test_reusables_template.py +0 -0
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/tests/test_reusables_template_filter.py +0 -0
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/tests/test_strategy.py +0 -0
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/tests/test_workflow.py +0 -0
- {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/tests/test_workflow_exec_job.py +0 -0
- {ddeutil_workflow-0.0.80 → 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
|
)
|
@@ -54,29 +54,30 @@ def init() -> None:
|
|
54
54
|
dedent(
|
55
55
|
"""
|
56
56
|
# Example workflow template.
|
57
|
-
wf-example:
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
57
|
+
name: wf-example:
|
58
|
+
type: Workflow
|
59
|
+
desc: |
|
60
|
+
An example workflow template that provide the demo of workflow.
|
61
|
+
params:
|
62
|
+
name:
|
63
|
+
type: str
|
64
|
+
default: "World"
|
65
|
+
jobs:
|
66
|
+
first-job:
|
67
|
+
stages:
|
68
|
+
|
69
|
+
- name: "Hello Stage"
|
70
|
+
echo: "Start say hi to the console"
|
71
|
+
|
72
|
+
- name: "Call tasks"
|
73
|
+
uses: tasks/say-hello-func@example
|
74
|
+
with:
|
75
|
+
name: ${{ params.name }}
|
76
|
+
|
77
|
+
second-job:
|
78
|
+
|
79
|
+
- name: "Hello Env"
|
80
|
+
echo: "Start say hi with ${ WORKFLOW_DEMO_HELLO }"
|
80
81
|
"""
|
81
82
|
).lstrip("\n")
|
82
83
|
)
|
@@ -89,12 +90,20 @@ def init() -> None:
|
|
89
90
|
dummy_tasks_path.write_text(
|
90
91
|
dedent(
|
91
92
|
"""
|
93
|
+
from typing import Any, Optional
|
94
|
+
|
92
95
|
from ddeutil.workflow import Result, tag
|
93
96
|
|
94
97
|
@tag(name="example", alias="say-hello-func")
|
95
|
-
def hello_world_task(name: str, rs: Result) -> dict[str, str]:
|
98
|
+
def hello_world_task(name: str, rs: Result, extras: Optional[dict[str, Any]] = None) -> dict[str, str]:
|
96
99
|
\"\"\"Logging hello task function\"\"\"
|
97
|
-
|
100
|
+
_extras = extras or {}
|
101
|
+
# NOTE: I will use custom newline logging if you pass `||`.
|
102
|
+
rs.trace.info(
|
103
|
+
f"Hello, {name}||"
|
104
|
+
f"> running ID: {rs.run_id}"
|
105
|
+
f"> extras: {_extras}"
|
106
|
+
)
|
98
107
|
return {"name": name}
|
99
108
|
"""
|
100
109
|
).lstrip("\n")
|
@@ -106,18 +115,19 @@ def init() -> None:
|
|
106
115
|
dotenv_file = Path(".env")
|
107
116
|
mode: str = "a" if dotenv_file.exists() else "w"
|
108
117
|
with dotenv_file.open(mode=mode) as f:
|
109
|
-
f.write("\n# Workflow
|
118
|
+
f.write("\n# Workflow Environment Variables\n")
|
110
119
|
f.write(
|
111
120
|
"WORKFLOW_DEMO_HELLO=foo\n"
|
112
121
|
"WORKFLOW_CORE_DEBUG_MODE=true\n"
|
113
122
|
"WORKFLOW_LOG_TIMEZONE=Asia/Bangkok\n"
|
114
|
-
"
|
123
|
+
'WORKFLOW_LOG_TRACE_HANDLERS=\'[{"type": "console"}]\'\n'
|
124
|
+
'WORKFLOW_LOG_AUDIT_CONF=\'{"type": "file", "path": "./audits"}\''
|
115
125
|
"WORKFLOW_LOG_AUDIT_ENABLE_WRITE=true\n"
|
116
126
|
)
|
117
127
|
|
118
128
|
typer.echo("Starter command:")
|
119
129
|
typer.echo(
|
120
|
-
"
|
130
|
+
">>> `source .env && workflow-cli workflows execute --name=wf-example`"
|
121
131
|
)
|
122
132
|
|
123
133
|
|
@@ -163,7 +173,7 @@ def api(
|
|
163
173
|
debug: Annotated[bool, typer.Option(help="A debug mode flag")] = True,
|
164
174
|
workers: Annotated[int, typer.Option(help="A worker number")] = None,
|
165
175
|
reload: Annotated[bool, typer.Option(help="A reload flag")] = False,
|
166
|
-
):
|
176
|
+
) -> None:
|
167
177
|
"""
|
168
178
|
Provision API application from the FastAPI.
|
169
179
|
"""
|
@@ -228,13 +238,12 @@ def workflow_execute(
|
|
228
238
|
typer.echo(f"... with params: {params_dict}")
|
229
239
|
|
230
240
|
|
231
|
-
WORKFLOW_TYPE = Literal["Workflow"]
|
232
|
-
|
233
|
-
|
234
241
|
class WorkflowSchema(Workflow):
|
235
242
|
"""Override workflow model fields for generate JSON schema file."""
|
236
243
|
|
237
|
-
type:
|
244
|
+
type: Literal["Workflow"] = Field(
|
245
|
+
description="A type of workflow template that should be `Workflow`."
|
246
|
+
)
|
238
247
|
name: Optional[str] = Field(default=None, description="A workflow name.")
|
239
248
|
params: dict[str, Union[Param, str]] = Field(
|
240
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:
|
@@ -22,19 +22,6 @@ Functions:
|
|
22
22
|
pass_env: Process environment variable substitution
|
23
23
|
api_config: Get API-specific configuration settings
|
24
24
|
|
25
|
-
Example:
|
26
|
-
```python
|
27
|
-
from ddeutil.workflow.conf import Config, YamlParser
|
28
|
-
|
29
|
-
# Load workflow configuration
|
30
|
-
parser = YamlParser("my-workflow")
|
31
|
-
workflow_config = parser.data
|
32
|
-
|
33
|
-
# Access dynamic configuration
|
34
|
-
from ddeutil.workflow.conf import dynamic
|
35
|
-
log_level = dynamic("log_level", default="INFO")
|
36
|
-
```
|
37
|
-
|
38
25
|
Note:
|
39
26
|
Configuration files support environment variable substitution using
|
40
27
|
${VAR_NAME} syntax and provide extensive validation capabilities.
|
@@ -155,7 +142,7 @@ class Config: # pragma: no cov
|
|
155
142
|
)
|
156
143
|
|
157
144
|
@property
|
158
|
-
def audit_conf(self) -> str:
|
145
|
+
def audit_conf(self) -> dict[str, Any]:
|
159
146
|
return json.loads(
|
160
147
|
env("LOG_AUDIT_URL", '{"type": "file", "path": "./audits"}')
|
161
148
|
)
|
@@ -189,17 +176,6 @@ class YamlParser:
|
|
189
176
|
"""Base Load object that use to search config data by given some identity
|
190
177
|
value like name of `Workflow` or `Crontab` templates.
|
191
178
|
|
192
|
-
:param name: (str) A name of key of config data that read with YAML
|
193
|
-
Environment object.
|
194
|
-
:param path: (Path) A config path object.
|
195
|
-
:param externals: (DictData) An external config data that want to add to
|
196
|
-
loaded config data.
|
197
|
-
:param extras: (DictDdata) An extra parameters that use to override core
|
198
|
-
config values.
|
199
|
-
|
200
|
-
:raise ValueError: If the data does not find on the config path with the
|
201
|
-
name parameter.
|
202
|
-
|
203
179
|
Noted:
|
204
180
|
The config data should have `type` key for modeling validation that
|
205
181
|
make this loader know what is config should to do pass to.
|
@@ -222,6 +198,23 @@ class YamlParser:
|
|
222
198
|
extras: Optional[DictData] = None,
|
223
199
|
obj: Optional[Union[object, str]] = None,
|
224
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
|
+
"""
|
225
218
|
self.path: Path = Path(dynamic("conf_path", f=path, extras=extras))
|
226
219
|
self.externals: DictData = externals or {}
|
227
220
|
self.extras: DictData = extras or {}
|
@@ -255,17 +248,19 @@ class YamlParser:
|
|
255
248
|
"""Find data with specific key and return the latest modify date data if
|
256
249
|
this key exists multiple files.
|
257
250
|
|
258
|
-
:
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
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.
|
267
261
|
|
268
|
-
:
|
262
|
+
Returns:
|
263
|
+
DictData: A config data that was found on the searching paths.
|
269
264
|
"""
|
270
265
|
path: Path = dynamic("conf_path", f=path, extras=extras)
|
271
266
|
if not paths:
|
@@ -288,9 +283,12 @@ class YamlParser:
|
|
288
283
|
continue
|
289
284
|
|
290
285
|
if data := cls.filter_yaml(file, name=name):
|
286
|
+
|
287
|
+
# NOTE: Start adding file metadata.
|
291
288
|
file_stat: os.stat_result = file.lstat()
|
292
289
|
data["created_at"] = file_stat.st_ctime
|
293
290
|
data["updated_at"] = file_stat.st_mtime
|
291
|
+
|
294
292
|
if not obj_type:
|
295
293
|
all_data.append((file_stat.st_mtime, data))
|
296
294
|
elif (t := data.get("type")) and t == obj_type:
|
@@ -324,11 +322,12 @@ class YamlParser:
|
|
324
322
|
extras: (DictData) An extra parameter that use to override core
|
325
323
|
config values.
|
326
324
|
ignore_filename: (str) An ignore filename. Default is
|
327
|
-
|
328
|
-
tags
|
329
|
-
A list of tag that want to filter.
|
325
|
+
``.confignore`` filename.
|
326
|
+
tags (list[str]): A list of tag that want to filter.
|
330
327
|
|
331
|
-
:
|
328
|
+
Returns:
|
329
|
+
Iterator[tuple[str, DictData]]: An iterator of config data that was
|
330
|
+
found on the searching paths.
|
332
331
|
"""
|
333
332
|
excluded: list[str] = excluded or []
|
334
333
|
tags: list[str] = tags or []
|
@@ -364,7 +363,12 @@ class YamlParser:
|
|
364
363
|
):
|
365
364
|
continue
|
366
365
|
|
367
|
-
if (
|
366
|
+
if (
|
367
|
+
# isinstance(data, dict) and
|
368
|
+
(t := data.get("type"))
|
369
|
+
and t == obj_type
|
370
|
+
):
|
371
|
+
# NOTE: Start adding file metadata.
|
368
372
|
file_stat: os.stat_result = file.lstat()
|
369
373
|
data["created_at"] = file_stat.st_ctime
|
370
374
|
data["updated_at"] = file_stat.st_mtime
|
@@ -372,6 +376,7 @@ class YamlParser:
|
|
372
376
|
file.lstat().st_mtime,
|
373
377
|
data,
|
374
378
|
)
|
379
|
+
|
375
380
|
if key in all_data:
|
376
381
|
all_data[key].append(marking)
|
377
382
|
else:
|
@@ -405,15 +410,30 @@ class YamlParser:
|
|
405
410
|
def filter_yaml(cls, file: Path, name: Optional[str] = None) -> DictData:
|
406
411
|
"""Read a YAML file context from an input file path and specific name.
|
407
412
|
|
408
|
-
:
|
409
|
-
|
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"}}
|
410
419
|
|
411
|
-
:
|
420
|
+
Args:
|
421
|
+
file (Path): A file path that want to extract YAML context.
|
422
|
+
name (str): A key name that search on a YAML context.
|
423
|
+
|
424
|
+
Returns:
|
425
|
+
DictData: A data that read from this file if it is YAML format.
|
412
426
|
"""
|
413
427
|
if any(file.suffix.endswith(s) for s in (".yml", ".yaml")):
|
414
428
|
values: DictData = YamlFlResolve(file).read()
|
415
429
|
if values is not None:
|
416
|
-
|
430
|
+
if name:
|
431
|
+
if "name" in values and values.get("name") == name:
|
432
|
+
return values
|
433
|
+
return (
|
434
|
+
values[name] | {"name": name} if name in values else {}
|
435
|
+
)
|
436
|
+
return {values["name"]: values} if "name" in values else values
|
417
437
|
return {}
|
418
438
|
|
419
439
|
@cached_property
|
@@ -135,11 +135,12 @@ class BaseError(Exception):
|
|
135
135
|
|
136
136
|
Example:
|
137
137
|
>>> error = BaseError("Something failed", refs="stage-1")
|
138
|
-
|
138
|
+
|
139
|
+
Simple format
|
139
140
|
>>> error.to_dict()
|
140
141
|
>>> # Returns: {"name": "BaseError", "message": "Something failed"}
|
141
142
|
|
142
|
-
|
143
|
+
With reference mapping
|
143
144
|
>>> error.to_dict(with_refs=True)
|
144
145
|
>>> # Returns: {"stage-1": {"name": "BaseError", "message": "Something failed"}}
|
145
146
|
```
|
@@ -165,6 +166,15 @@ class StageCancelError(StageError): ...
|
|
165
166
|
class StageSkipError(StageError): ...
|
166
167
|
|
167
168
|
|
169
|
+
class StageNestedError(StageError): ...
|
170
|
+
|
171
|
+
|
172
|
+
class StageNestedCancelError(StageNestedError): ...
|
173
|
+
|
174
|
+
|
175
|
+
class StageNestedSkipError(StageNestedError): ...
|
176
|
+
|
177
|
+
|
168
178
|
class JobError(BaseError): ...
|
169
179
|
|
170
180
|
|