ddeutil-workflow 0.0.81__tar.gz → 0.0.83__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.83}/PKG-INFO +1 -1
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/pyproject.toml +0 -1
- ddeutil_workflow-0.0.83/src/ddeutil/workflow/__about__.py +2 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/__cron.py +1 -1
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/__init__.py +21 -7
- ddeutil_workflow-0.0.81/src/ddeutil/workflow/cli.py → ddeutil_workflow-0.0.83/src/ddeutil/workflow/__main__.py +3 -4
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/__types.py +10 -1
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/api/routes/job.py +2 -2
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/api/routes/logs.py +8 -61
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/audits.py +101 -49
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/conf.py +45 -25
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/errors.py +12 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/event.py +34 -11
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/job.py +75 -31
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/result.py +73 -22
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/stages.py +625 -375
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/traces.py +71 -27
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/utils.py +41 -24
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/workflow.py +97 -124
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83/src/ddeutil_workflow.egg-info}/PKG-INFO +1 -1
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil_workflow.egg-info/SOURCES.txt +0 -1
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/tests/test_audits.py +24 -20
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/tests/test_cli.py +1 -1
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/tests/test_conf.py +24 -4
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/tests/test_job.py +14 -1
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/tests/test_job_exec.py +60 -1
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/tests/test_result.py +12 -1
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/tests/test_traces.py +19 -13
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/tests/test_utils.py +2 -4
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/tests/test_workflow.py +15 -4
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/tests/test_workflow_exec.py +194 -2
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/tests/test_workflow_release.py +82 -22
- 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.83}/LICENSE +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/README.md +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/setup.cfg +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/api/__init__.py +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/api/log_conf.py +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/api/routes/__init__.py +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/api/routes/workflows.py +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/params.py +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/plugins/__init__.py +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/plugins/providers/__init__.py +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/plugins/providers/aws.py +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/plugins/providers/az.py +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/plugins/providers/container.py +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/plugins/providers/gcs.py +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/reusables.py +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil_workflow.egg-info/dependency_links.txt +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil_workflow.egg-info/entry_points.txt +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil_workflow.egg-info/requires.txt +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil_workflow.egg-info/top_level.txt +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/tests/test__cron.py +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/tests/test__regex.py +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/tests/test_errors.py +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/tests/test_event.py +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/tests/test_job_exec_strategy.py +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/tests/test_params.py +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/tests/test_reusables_call_tag.py +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/tests/test_reusables_func_model.py +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/tests/test_reusables_template.py +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/tests/test_reusables_template_filter.py +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/tests/test_strategy.py +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/tests/test_workflow_exec_job.py +0 -0
- {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/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",
|
@@ -715,7 +715,7 @@ class CronJob:
|
|
715
715
|
self,
|
716
716
|
date: Optional[datetime] = None,
|
717
717
|
*,
|
718
|
-
tz: Optional[str] = None,
|
718
|
+
tz: Optional[Union[str, ZoneInfo]] = None,
|
719
719
|
) -> CronRunner:
|
720
720
|
"""Returns CronRunner instance that be datetime runner with this
|
721
721
|
cronjob. It can use `next`, `prev`, or `reset` methods to generate
|
@@ -50,19 +50,37 @@ Note:
|
|
50
50
|
from .__cron import CronRunner
|
51
51
|
from .__types import DictData, DictStr, Matrix, Re, TupleStr
|
52
52
|
from .audits import (
|
53
|
+
DRYRUN,
|
54
|
+
FORCE,
|
55
|
+
NORMAL,
|
56
|
+
RERUN,
|
53
57
|
Audit,
|
54
|
-
|
58
|
+
LocalFileAudit,
|
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,
|
74
|
+
EventError,
|
60
75
|
JobCancelError,
|
61
76
|
JobError,
|
62
77
|
JobSkipError,
|
63
78
|
ResultError,
|
64
79
|
StageCancelError,
|
65
80
|
StageError,
|
81
|
+
StageNestedCancelError,
|
82
|
+
StageNestedError,
|
83
|
+
StageNestedSkipError,
|
66
84
|
StageSkipError,
|
67
85
|
UtilError,
|
68
86
|
WorkflowCancelError,
|
@@ -132,15 +150,11 @@ from .stages import (
|
|
132
150
|
VirtualPyStage,
|
133
151
|
)
|
134
152
|
from .traces import (
|
135
|
-
|
153
|
+
Trace,
|
136
154
|
get_trace,
|
137
155
|
)
|
138
156
|
from .utils import *
|
139
157
|
from .workflow import (
|
140
|
-
EVENT,
|
141
|
-
FORCE,
|
142
|
-
NORMAL,
|
143
|
-
RERUN,
|
144
158
|
ReleaseType,
|
145
159
|
Workflow,
|
146
160
|
)
|
@@ -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,
|
@@ -16,17 +16,26 @@ from re import (
|
|
16
16
|
Match,
|
17
17
|
Pattern,
|
18
18
|
)
|
19
|
-
from typing import Any, Optional, TypedDict, Union
|
19
|
+
from typing import Any, Optional, TypedDict, Union, cast
|
20
20
|
|
21
21
|
from typing_extensions import Self
|
22
22
|
|
23
23
|
StrOrNone = Optional[str]
|
24
24
|
StrOrInt = Union[str, int]
|
25
25
|
TupleStr = tuple[str, ...]
|
26
|
+
ListStr = list[str]
|
27
|
+
ListInt = list[int]
|
26
28
|
DictData = dict[str, Any]
|
29
|
+
DictRange = dict[int, Any]
|
27
30
|
DictStr = dict[str, str]
|
28
31
|
Matrix = dict[str, Union[list[str], list[int]]]
|
29
32
|
|
33
|
+
|
34
|
+
def cast_dict(value: TypedDict[...]) -> DictData:
|
35
|
+
"""Cast any TypedDict object to DictData type."""
|
36
|
+
return cast(DictData, value)
|
37
|
+
|
38
|
+
|
30
39
|
# Pre-compile regex patterns for better performance
|
31
40
|
_RE_CALLER_PATTERN = r"""
|
32
41
|
\$ # start with $
|
@@ -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,72 @@ 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
|
+
DRYRUN: Dry-execution workflow
|
78
|
+
FORCE: Forced execution bypassing normal conditions
|
79
|
+
"""
|
80
|
+
|
81
|
+
NORMAL = "normal"
|
82
|
+
RERUN = "rerun"
|
83
|
+
FORCE = "force"
|
84
|
+
DRYRUN = "dryrun"
|
85
|
+
|
86
|
+
|
87
|
+
NORMAL = ReleaseType.NORMAL
|
88
|
+
RERUN = ReleaseType.RERUN
|
89
|
+
DRYRUN = ReleaseType.DRYRUN
|
90
|
+
FORCE = ReleaseType.FORCE
|
91
|
+
|
92
|
+
|
67
93
|
class AuditData(BaseModel):
|
94
|
+
"""Audit Data model that use to be the core data for any Audit model manage
|
95
|
+
logging at the target pointer system or service like file-system, sqlite
|
96
|
+
database, etc.
|
97
|
+
"""
|
98
|
+
|
99
|
+
model_config = ConfigDict(use_enum_values=True)
|
100
|
+
|
68
101
|
name: str = Field(description="A workflow name.")
|
69
102
|
release: datetime = Field(description="A release datetime.")
|
70
|
-
type:
|
103
|
+
type: ReleaseType = Field(
|
104
|
+
default=NORMAL,
|
105
|
+
description=(
|
106
|
+
"An execution type that should be value in ('normal', 'rerun', "
|
107
|
+
"'force', 'dryrun')."
|
108
|
+
),
|
109
|
+
)
|
71
110
|
context: DictData = Field(
|
72
111
|
default_factory=dict,
|
73
112
|
description="A context that receive from a workflow execution result.",
|
74
113
|
)
|
114
|
+
run_id: str = Field(description="A running ID")
|
75
115
|
parent_run_id: Optional[str] = Field(
|
76
116
|
default=None, description="A parent running ID."
|
77
117
|
)
|
78
|
-
run_id: str = Field(description="A running ID")
|
79
118
|
runs_metadata: DictData = Field(
|
80
119
|
default_factory=dict,
|
81
120
|
description="A runs metadata that will use to tracking this audit log.",
|
@@ -89,18 +128,17 @@ class BaseAudit(BaseModel, ABC):
|
|
89
128
|
for logging subclasses like file, sqlite, etc.
|
90
129
|
"""
|
91
130
|
|
92
|
-
type:
|
131
|
+
type: Literal["base"] = "base"
|
132
|
+
logging_name: str = "ddeutil.workflow"
|
93
133
|
extras: DictData = Field(
|
94
134
|
default_factory=dict,
|
95
135
|
description="An extras parameter that want to override core config",
|
96
136
|
)
|
97
137
|
|
98
138
|
@field_validator("extras", mode="before")
|
99
|
-
def
|
139
|
+
def __prepare_extras(cls, v: Any) -> Any:
|
100
140
|
"""Validate extras field to ensure it's a dictionary."""
|
101
|
-
if v is None
|
102
|
-
return {}
|
103
|
-
return v
|
141
|
+
return {} if v is None else v
|
104
142
|
|
105
143
|
@model_validator(mode="after")
|
106
144
|
def __model_action(self) -> Self:
|
@@ -116,13 +154,13 @@ class BaseAudit(BaseModel, ABC):
|
|
116
154
|
self.do_before()
|
117
155
|
|
118
156
|
# NOTE: Start setting log config in this line with cache.
|
119
|
-
set_logging(
|
157
|
+
set_logging(self.logging_name)
|
120
158
|
return self
|
121
159
|
|
122
160
|
@abstractmethod
|
123
161
|
def is_pointed(
|
124
162
|
self,
|
125
|
-
data:
|
163
|
+
data: Any,
|
126
164
|
*,
|
127
165
|
extras: Optional[DictData] = None,
|
128
166
|
) -> bool:
|
@@ -216,7 +254,7 @@ class BaseAudit(BaseModel, ABC):
|
|
216
254
|
raise NotImplementedError("Audit should implement `save` method.")
|
217
255
|
|
218
256
|
|
219
|
-
class
|
257
|
+
class LocalFileAudit(BaseAudit):
|
220
258
|
"""File Audit Pydantic Model for saving log data from workflow execution.
|
221
259
|
|
222
260
|
This class inherits from BaseAudit and implements file-based storage
|
@@ -224,19 +262,25 @@ class FileAudit(BaseAudit):
|
|
224
262
|
in a structured directory hierarchy.
|
225
263
|
|
226
264
|
Attributes:
|
227
|
-
|
265
|
+
file_fmt: Class variable defining the filename format for audit log.
|
266
|
+
file_release_fmt: Class variable defining the filename format for audit
|
267
|
+
release log.
|
228
268
|
"""
|
229
269
|
|
230
|
-
|
231
|
-
|
232
|
-
)
|
270
|
+
file_fmt: ClassVar[str] = "workflow={name}"
|
271
|
+
file_release_fmt: ClassVar[str] = "release={release:%Y%m%d%H%M%S}"
|
233
272
|
|
234
273
|
type: Literal["file"] = "file"
|
235
|
-
path:
|
236
|
-
default="./audits",
|
274
|
+
path: Path = Field(
|
275
|
+
default=Path("./audits"),
|
237
276
|
description="A file path that use to manage audit logs.",
|
238
277
|
)
|
239
278
|
|
279
|
+
@field_validator("path", mode="before", json_schema_input_type=str)
|
280
|
+
def __prepare_path(cls, data: Any) -> Any:
|
281
|
+
"""Prepare path that passing with string to Path instance."""
|
282
|
+
return Path(data) if isinstance(data, str) else data
|
283
|
+
|
240
284
|
def do_before(self) -> None:
|
241
285
|
"""Create directory of release before saving log file.
|
242
286
|
|
@@ -246,7 +290,10 @@ class FileAudit(BaseAudit):
|
|
246
290
|
Path(self.path).mkdir(parents=True, exist_ok=True)
|
247
291
|
|
248
292
|
def find_audits(
|
249
|
-
self,
|
293
|
+
self,
|
294
|
+
name: str,
|
295
|
+
*,
|
296
|
+
extras: Optional[DictData] = None,
|
250
297
|
) -> Iterator[AuditData]:
|
251
298
|
"""Generate audit data found from logs path for a specific workflow name.
|
252
299
|
|
@@ -260,7 +307,7 @@ class FileAudit(BaseAudit):
|
|
260
307
|
Raises:
|
261
308
|
FileNotFoundError: If the workflow directory does not exist.
|
262
309
|
"""
|
263
|
-
pointer: Path =
|
310
|
+
pointer: Path = self.path / self.file_fmt.format(name=name)
|
264
311
|
if not pointer.exists():
|
265
312
|
raise FileNotFoundError(f"Pointer: {pointer.absolute()}.")
|
266
313
|
|
@@ -293,7 +340,7 @@ class FileAudit(BaseAudit):
|
|
293
340
|
ValueError: If no releases found when release is None.
|
294
341
|
"""
|
295
342
|
if release is None:
|
296
|
-
pointer: Path =
|
343
|
+
pointer: Path = self.path / self.file_fmt.format(name=name)
|
297
344
|
if not pointer.exists():
|
298
345
|
raise FileNotFoundError(f"Pointer: {pointer.absolute()}.")
|
299
346
|
|
@@ -328,21 +375,21 @@ class FileAudit(BaseAudit):
|
|
328
375
|
return AuditData.model_validate(obj=json.load(f))
|
329
376
|
|
330
377
|
def is_pointed(
|
331
|
-
self,
|
378
|
+
self,
|
379
|
+
data: Any,
|
380
|
+
*,
|
381
|
+
extras: Optional[DictData] = None,
|
332
382
|
) -> bool:
|
333
383
|
"""Check if the release log already exists at the destination log path.
|
334
384
|
|
335
385
|
Args:
|
336
|
-
data:
|
386
|
+
data (str):
|
337
387
|
extras: Optional extra parameters to override core config.
|
338
388
|
|
339
389
|
Returns:
|
340
390
|
bool: True if the release log exists, False otherwise.
|
341
391
|
"""
|
342
|
-
|
343
|
-
if not dynamic("enable_write_audit", extras=extras):
|
344
|
-
return False
|
345
|
-
return self.pointer(data).exists()
|
392
|
+
return self.pointer(AuditData.model_validate(data)).exists()
|
346
393
|
|
347
394
|
def pointer(self, data: AuditData) -> Path:
|
348
395
|
"""Return release directory path generated from model data.
|
@@ -350,8 +397,10 @@ class FileAudit(BaseAudit):
|
|
350
397
|
Returns:
|
351
398
|
Path: The directory path for the current workflow and release.
|
352
399
|
"""
|
353
|
-
return
|
354
|
-
|
400
|
+
return (
|
401
|
+
self.path
|
402
|
+
/ self.file_fmt.format(**data.model_dump(by_alias=True))
|
403
|
+
/ self.file_release_fmt.format(**data.model_dump(by_alias=True))
|
355
404
|
)
|
356
405
|
|
357
406
|
def save(self, data: Any, excluded: Optional[list[str]] = None) -> Self:
|
@@ -365,7 +414,7 @@ class FileAudit(BaseAudit):
|
|
365
414
|
Self: The audit instance after saving.
|
366
415
|
"""
|
367
416
|
audit = AuditData.model_validate(data)
|
368
|
-
trace:
|
417
|
+
trace: Trace = get_trace(
|
369
418
|
audit.run_id,
|
370
419
|
parent_run_id=audit.parent_run_id,
|
371
420
|
extras=self.extras,
|
@@ -427,7 +476,7 @@ class FileAudit(BaseAudit):
|
|
427
476
|
return cleaned_count
|
428
477
|
|
429
478
|
|
430
|
-
class
|
479
|
+
class LocalSQLiteAudit(BaseAudit): # pragma: no cov
|
431
480
|
"""SQLite Audit model for database-based audit storage.
|
432
481
|
|
433
482
|
This class inherits from BaseAudit and implements SQLite database storage
|
@@ -435,11 +484,11 @@ class SQLiteAudit(BaseAudit): # pragma: no cov
|
|
435
484
|
|
436
485
|
Attributes:
|
437
486
|
table_name: Class variable defining the database table name.
|
438
|
-
|
487
|
+
ddl: Class variable defining the database schema.
|
439
488
|
"""
|
440
489
|
|
441
490
|
table_name: ClassVar[str] = "audits"
|
442
|
-
|
491
|
+
ddl: ClassVar[
|
443
492
|
str
|
444
493
|
] = """
|
445
494
|
CREATE TABLE IF NOT EXISTS audits (
|
@@ -457,22 +506,21 @@ class SQLiteAudit(BaseAudit): # pragma: no cov
|
|
457
506
|
"""
|
458
507
|
|
459
508
|
type: Literal["sqlite"] = "sqlite"
|
460
|
-
path:
|
509
|
+
path: Path = Field(
|
510
|
+
default=Path("./audits.db"),
|
511
|
+
description="A SQLite filepath.",
|
512
|
+
)
|
461
513
|
|
462
|
-
def
|
514
|
+
def do_before(self) -> None:
|
463
515
|
"""Ensure the audit table exists in the database."""
|
464
|
-
|
465
|
-
if audit_url is None or not audit_url.path:
|
516
|
+
if self.path.is_dir():
|
466
517
|
raise ValueError(
|
467
|
-
"SQLite
|
518
|
+
"SQLite path must specify a database file path not dir."
|
468
519
|
)
|
469
520
|
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
with sqlite3.connect(db_path) as conn:
|
475
|
-
conn.execute(self.schemas)
|
521
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
522
|
+
with sqlite3.connect(self.path) as conn:
|
523
|
+
conn.execute(self.ddl)
|
476
524
|
conn.commit()
|
477
525
|
|
478
526
|
def is_pointed(
|
@@ -655,7 +703,7 @@ class SQLiteAudit(BaseAudit): # pragma: no cov
|
|
655
703
|
ValueError: If SQLite database is not properly configured.
|
656
704
|
"""
|
657
705
|
audit = AuditData.model_validate(data)
|
658
|
-
trace:
|
706
|
+
trace: Trace = get_trace(
|
659
707
|
audit.run_id,
|
660
708
|
parent_run_id=audit.parent_run_id,
|
661
709
|
extras=self.extras,
|
@@ -739,27 +787,31 @@ class SQLiteAudit(BaseAudit): # pragma: no cov
|
|
739
787
|
return cursor.rowcount
|
740
788
|
|
741
789
|
|
790
|
+
class PostgresAudit(BaseAudit, ABC): ... # pragma: no cov
|
791
|
+
|
792
|
+
|
742
793
|
Audit = Annotated[
|
743
794
|
Union[
|
744
|
-
|
745
|
-
|
795
|
+
LocalFileAudit,
|
796
|
+
LocalSQLiteAudit,
|
746
797
|
],
|
747
798
|
Field(discriminator="type"),
|
748
799
|
]
|
749
800
|
|
750
801
|
|
751
802
|
def get_audit(
|
752
|
-
|
803
|
+
audit_conf: Optional[DictData] = None,
|
753
804
|
extras: Optional[DictData] = None,
|
754
805
|
) -> Audit: # pragma: no cov
|
755
806
|
"""Get an audit model dynamically based on the config audit path value.
|
756
807
|
|
757
808
|
Args:
|
809
|
+
audit_conf (DictData):
|
758
810
|
extras: Optional extra parameters to override the core config.
|
759
811
|
|
760
812
|
Returns:
|
761
813
|
Audit: The appropriate audit model class based on configuration.
|
762
814
|
"""
|
763
|
-
audit_conf = dynamic("audit_conf", extras=extras)
|
815
|
+
audit_conf = dynamic("audit_conf", f=audit_conf, extras=extras)
|
764
816
|
model = TypeAdapter(Audit).validate_python(audit_conf | {"extras": extras})
|
765
817
|
return model
|