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.
Files changed (66) hide show
  1. {ddeutil_workflow-0.0.81/src/ddeutil_workflow.egg-info → ddeutil_workflow-0.0.83}/PKG-INFO +1 -1
  2. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/pyproject.toml +0 -1
  3. ddeutil_workflow-0.0.83/src/ddeutil/workflow/__about__.py +2 -0
  4. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/__cron.py +1 -1
  5. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/__init__.py +21 -7
  6. ddeutil_workflow-0.0.81/src/ddeutil/workflow/cli.py → ddeutil_workflow-0.0.83/src/ddeutil/workflow/__main__.py +3 -4
  7. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/__types.py +10 -1
  8. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/api/routes/job.py +2 -2
  9. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/api/routes/logs.py +8 -61
  10. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/audits.py +101 -49
  11. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/conf.py +45 -25
  12. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/errors.py +12 -0
  13. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/event.py +34 -11
  14. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/job.py +75 -31
  15. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/result.py +73 -22
  16. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/stages.py +625 -375
  17. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/traces.py +71 -27
  18. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/utils.py +41 -24
  19. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/workflow.py +97 -124
  20. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83/src/ddeutil_workflow.egg-info}/PKG-INFO +1 -1
  21. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil_workflow.egg-info/SOURCES.txt +0 -1
  22. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/tests/test_audits.py +24 -20
  23. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/tests/test_cli.py +1 -1
  24. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/tests/test_conf.py +24 -4
  25. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/tests/test_job.py +14 -1
  26. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/tests/test_job_exec.py +60 -1
  27. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/tests/test_result.py +12 -1
  28. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/tests/test_traces.py +19 -13
  29. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/tests/test_utils.py +2 -4
  30. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/tests/test_workflow.py +15 -4
  31. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/tests/test_workflow_exec.py +194 -2
  32. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/tests/test_workflow_release.py +82 -22
  33. ddeutil_workflow-0.0.81/src/ddeutil/workflow/__about__.py +0 -1
  34. ddeutil_workflow-0.0.81/src/ddeutil/workflow/__main__.py +0 -4
  35. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/LICENSE +0 -0
  36. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/README.md +0 -0
  37. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/setup.cfg +0 -0
  38. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/api/__init__.py +0 -0
  39. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/api/log_conf.py +0 -0
  40. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/api/routes/__init__.py +0 -0
  41. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/api/routes/workflows.py +0 -0
  42. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/params.py +0 -0
  43. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/plugins/__init__.py +0 -0
  44. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/plugins/providers/__init__.py +0 -0
  45. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/plugins/providers/aws.py +0 -0
  46. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/plugins/providers/az.py +0 -0
  47. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/plugins/providers/container.py +0 -0
  48. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/plugins/providers/gcs.py +0 -0
  49. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/reusables.py +0 -0
  50. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil_workflow.egg-info/dependency_links.txt +0 -0
  51. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil_workflow.egg-info/entry_points.txt +0 -0
  52. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil_workflow.egg-info/requires.txt +0 -0
  53. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/src/ddeutil_workflow.egg-info/top_level.txt +0 -0
  54. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/tests/test__cron.py +0 -0
  55. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/tests/test__regex.py +0 -0
  56. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/tests/test_errors.py +0 -0
  57. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/tests/test_event.py +0 -0
  58. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/tests/test_job_exec_strategy.py +0 -0
  59. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/tests/test_params.py +0 -0
  60. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/tests/test_reusables_call_tag.py +0 -0
  61. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/tests/test_reusables_func_model.py +0 -0
  62. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/tests/test_reusables_template.py +0 -0
  63. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/tests/test_reusables_template_filter.py +0 -0
  64. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/tests/test_strategy.py +0 -0
  65. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/tests/test_workflow_exec_job.py +0 -0
  66. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.83}/tests/test_workflow_rerun.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ddeutil-workflow
3
- Version: 0.0.81
3
+ Version: 0.0.83
4
4
  Summary: Lightweight workflow orchestration with YAML template
5
5
  Author-email: ddeutils <korawich.anu@gmail.com>
6
6
  License: MIT
@@ -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",
@@ -0,0 +1,2 @@
1
+ __version__: str = "0.0.83"
2
+ __python_version__: str = "3.9"
@@ -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
- FileAudit,
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
- TraceManager,
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: WORKFLOW_TYPE = Field(description="A type of workflow template.")
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 TraceManager, get_trace
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: TraceManager = get_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 and trace log paths."""
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": "Getting audit logs",
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 TraceManager, get_trace, set_logging
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: str = Field(description="A running type before logging.")
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: str
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 validate_extras(cls, v: Any) -> DictData:
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("ddeutil.workflow")
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: AuditData,
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 FileAudit(BaseAudit):
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
- filename_fmt: Class variable defining the filename format for audit files.
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
- filename_fmt: ClassVar[str] = (
231
- "workflow={name}/release={release:%Y%m%d%H%M%S}"
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: str = Field(
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, name: str, *, extras: Optional[DictData] = None
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 = Path(self.path) / f"workflow={name}"
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 = Path(self.path) / f"workflow={name}"
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, data: AuditData, *, extras: Optional[DictData] = None
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: The workflow name.
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
- # NOTE: Return False if enable writing log flag does not set.
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 Path(self.path) / self.filename_fmt.format(
354
- name=data.name, release=data.release
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: TraceManager = get_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 SQLiteAudit(BaseAudit): # pragma: no cov
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
- schemas: Class variable defining the database schema.
487
+ ddl: Class variable defining the database schema.
439
488
  """
440
489
 
441
490
  table_name: ClassVar[str] = "audits"
442
- schemas: ClassVar[
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: str
509
+ path: Path = Field(
510
+ default=Path("./audits.db"),
511
+ description="A SQLite filepath.",
512
+ )
461
513
 
462
- def _ensure_table_exists(self) -> None:
514
+ def do_before(self) -> None:
463
515
  """Ensure the audit table exists in the database."""
464
- audit_url = dynamic("audit_url", extras=self.extras)
465
- if audit_url is None or not audit_url.path:
516
+ if self.path.is_dir():
466
517
  raise ValueError(
467
- "SQLite audit_url must specify a database file path"
518
+ "SQLite path must specify a database file path not dir."
468
519
  )
469
520
 
470
- audit_url_parse: ParseResult = urlparse(audit_url)
471
- db_path = Path(audit_url_parse.path)
472
- db_path.parent.mkdir(parents=True, exist_ok=True)
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: TraceManager = get_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
- FileAudit,
745
- SQLiteAudit,
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