ddeutil-workflow 0.0.73__py3-none-any.whl → 0.0.75__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,6 +3,37 @@
3
3
  # Licensed under the MIT License. See LICENSE in the project root for
4
4
  # license information.
5
5
  # ------------------------------------------------------------------------------
6
+ """Audit and Execution Tracking Module.
7
+
8
+ This module provides comprehensive audit capabilities for workflow execution
9
+ tracking and monitoring. It supports multiple audit backends for capturing
10
+ execution metadata, status information, and detailed logging.
11
+
12
+ The audit system tracks workflow, job, and stage executions with configurable
13
+ storage backends including file-based JSON storage and database persistence.
14
+
15
+ Classes:
16
+ Audit: Pydantic model for audit data validation
17
+ FileAudit: File-based audit storage implementation
18
+
19
+ Functions:
20
+ get_audit_model: Factory function for creating audit instances
21
+
22
+ Example:
23
+
24
+ ```python
25
+ from ddeutil.workflow.audits import get_audit_model
26
+
27
+ # NOTE: Create file-based Audit.
28
+ audit = get_audit_model(run_id="run-123")
29
+ audit.info("Workflow execution started")
30
+ audit.success("Workflow completed successfully")
31
+ ```
32
+
33
+ Note:
34
+ Audit instances are automatically configured based on the workflow
35
+ configuration and provide detailed execution tracking capabilities.
36
+ """
6
37
  from __future__ import annotations
7
38
 
8
39
  import json
@@ -12,15 +43,17 @@ from abc import ABC, abstractmethod
12
43
  from collections.abc import Iterator
13
44
  from datetime import datetime
14
45
  from pathlib import Path
15
- from typing import ClassVar, Optional, TypeVar, Union
46
+ from typing import ClassVar, Optional, Union
47
+ from urllib.parse import ParseResult
16
48
 
17
49
  from pydantic import BaseModel, Field
50
+ from pydantic.functional_serializers import field_serializer
18
51
  from pydantic.functional_validators import model_validator
19
52
  from typing_extensions import Self
20
53
 
21
54
  from .__types import DictData
22
55
  from .conf import dynamic
23
- from .traces import TraceModel, get_trace, set_logging
56
+ from .traces import Trace, get_trace, set_logging
24
57
 
25
58
  logger = logging.getLogger("ddeutil.workflow")
26
59
 
@@ -108,42 +141,6 @@ class BaseAudit(BaseModel, ABC):
108
141
  raise NotImplementedError("Audit should implement `save` method.")
109
142
 
110
143
 
111
- class NullAudit(BaseAudit):
112
-
113
- @classmethod
114
- def is_pointed(
115
- cls,
116
- name: str,
117
- release: datetime,
118
- *,
119
- extras: Optional[DictData] = None,
120
- ) -> bool:
121
- return False
122
-
123
- @classmethod
124
- def find_audits(
125
- cls,
126
- name: str,
127
- *,
128
- extras: Optional[DictData] = None,
129
- ) -> Iterator[Self]:
130
- raise NotImplementedError()
131
-
132
- @classmethod
133
- def find_audit_with_release(
134
- cls,
135
- name: str,
136
- release: Optional[datetime] = None,
137
- *,
138
- extras: Optional[DictData] = None,
139
- ) -> Self:
140
- raise NotImplementedError()
141
-
142
- def save(self, excluded: Optional[list[str]]) -> None:
143
- """Do nothing when do not set audit."""
144
- return
145
-
146
-
147
144
  class FileAudit(BaseAudit):
148
145
  """File Audit Pydantic Model that use to saving log data from result of
149
146
  workflow execution. It inherits from BaseAudit model that implement the
@@ -154,6 +151,13 @@ class FileAudit(BaseAudit):
154
151
  "workflow={name}/release={release:%Y%m%d%H%M%S}"
155
152
  )
156
153
 
154
+ @field_serializer("extras")
155
+ def __serialize_extras(self, value: DictData) -> DictData:
156
+ return {
157
+ k: (v.geturl() if isinstance(v, ParseResult) else v)
158
+ for k, v in value.items()
159
+ }
160
+
157
161
  def do_before(self) -> None:
158
162
  """Create directory of release before saving log file."""
159
163
  self.pointer().mkdir(parents=True, exist_ok=True)
@@ -171,7 +175,7 @@ class FileAudit(BaseAudit):
171
175
  :rtype: Iterator[Self]
172
176
  """
173
177
  pointer: Path = (
174
- dynamic("audit_path", extras=extras) / f"workflow={name}"
178
+ Path(dynamic("audit_url", extras=extras).path) / f"workflow={name}"
175
179
  )
176
180
  if not pointer.exists():
177
181
  raise FileNotFoundError(f"Pointer: {pointer.absolute()}.")
@@ -206,7 +210,7 @@ class FileAudit(BaseAudit):
206
210
  raise NotImplementedError("Find latest log does not implement yet.")
207
211
 
208
212
  pointer: Path = (
209
- dynamic("audit_path", extras=extras)
213
+ Path(dynamic("audit_url", extras=extras).path)
210
214
  / f"workflow={name}/release={release:%Y%m%d%H%M%S}"
211
215
  )
212
216
  if not pointer.exists():
@@ -242,8 +246,8 @@ class FileAudit(BaseAudit):
242
246
  return False
243
247
 
244
248
  # NOTE: create pointer path that use the same logic of pointer method.
245
- pointer: Path = dynamic(
246
- "audit_path", extras=extras
249
+ pointer: Path = Path(
250
+ dynamic("audit_url", extras=extras).path
247
251
  ) / cls.filename_fmt.format(name=name, release=release)
248
252
 
249
253
  return pointer.exists()
@@ -253,8 +257,8 @@ class FileAudit(BaseAudit):
253
257
 
254
258
  :rtype: Path
255
259
  """
256
- return dynamic(
257
- "audit_path", extras=self.extras
260
+ return Path(
261
+ dynamic("audit_url", extras=self.extras).path
258
262
  ) / self.filename_fmt.format(name=self.name, release=self.release)
259
263
 
260
264
  def save(self, excluded: Optional[list[str]] = None) -> Self:
@@ -266,7 +270,7 @@ class FileAudit(BaseAudit):
266
270
 
267
271
  :rtype: Self
268
272
  """
269
- trace: TraceModel = get_trace(
273
+ trace: Trace = get_trace(
270
274
  self.run_id,
271
275
  parent_run_id=self.parent_run_id,
272
276
  extras=self.extras,
@@ -338,7 +342,7 @@ class SQLiteAudit(BaseAudit): # pragma: no cov
338
342
  """Save logging data that receive a context data from a workflow
339
343
  execution result.
340
344
  """
341
- trace: TraceModel = get_trace(
345
+ trace: Trace = get_trace(
342
346
  self.run_id,
343
347
  parent_run_id=self.parent_run_id,
344
348
  extras=self.extras,
@@ -352,23 +356,34 @@ class SQLiteAudit(BaseAudit): # pragma: no cov
352
356
  raise NotImplementedError("SQLiteAudit does not implement yet.")
353
357
 
354
358
 
355
- Audit = TypeVar("Audit", bound=BaseAudit)
356
- AuditModel = Union[
357
- NullAudit,
359
+ Audit = Union[
358
360
  FileAudit,
359
361
  SQLiteAudit,
362
+ BaseAudit,
360
363
  ]
361
364
 
362
365
 
363
- def get_audit(
366
+ def get_audit_model(
364
367
  extras: Optional[DictData] = None,
365
- ) -> type[AuditModel]: # pragma: no cov
366
- """Get an audit class that dynamic base on the config audit path value.
368
+ ) -> type[Audit]: # pragma: no cov
369
+ """Get an audit model that dynamic base on the config audit path value.
367
370
 
368
371
  :param extras: An extra parameter that want to override the core config.
369
372
 
370
373
  :rtype: type[Audit]
371
374
  """
372
- if dynamic("audit_path", extras=extras).is_file():
373
- return SQLiteAudit
374
- return FileAudit
375
+ # NOTE: Allow you to override trace model by the extra parameter.
376
+ map_audit_models: dict[str, type[Trace]] = extras.get(
377
+ "audit_model_mapping", {}
378
+ )
379
+ url: ParseResult
380
+ if (url := dynamic("audit_url", extras=extras)).scheme and (
381
+ url.scheme == "sqlite"
382
+ or (url.scheme == "file" and Path(url.path).is_file())
383
+ ):
384
+ return map_audit_models.get("sqlite", FileAudit)
385
+ elif url.scheme and url.scheme != "file":
386
+ raise NotImplementedError(
387
+ f"Does not implement the audit model support for URL: {url}"
388
+ )
389
+ return map_audit_models.get("file", FileAudit)
ddeutil/workflow/cli.py CHANGED
@@ -16,7 +16,6 @@ from pydantic import Field, TypeAdapter
16
16
  from .__about__ import __version__
17
17
  from .__types import DictData
18
18
  from .errors import JobError
19
- from .event import Crontab
20
19
  from .job import Job
21
20
  from .params import Param
22
21
  from .result import Result
@@ -153,19 +152,6 @@ class WorkflowSchema(Workflow):
153
152
  default_factory=dict,
154
153
  description="A parameters that need to use on this workflow.",
155
154
  )
156
- on: Union[list[Union[Crontab, str]], str] = Field(
157
- default_factory=list,
158
- description="A list of Crontab instance for this workflow schedule.",
159
- )
160
-
161
-
162
- CRONTAB_TYPE = Literal["Crontab"]
163
-
164
-
165
- class CrontabSchema(Crontab):
166
- """Override crontab model fields for generate JSON schema file."""
167
-
168
- type: CRONTAB_TYPE = Field(description="A type of crontab template.")
169
155
 
170
156
 
171
157
  @workflow_app.command(name="json-schema")
@@ -176,7 +162,7 @@ def workflow_json_schema(
176
162
  ] = Path("./json-schema.json"),
177
163
  ) -> None:
178
164
  """Generate JSON schema file from the Workflow model."""
179
- template = dict[str, Union[WorkflowSchema, CrontabSchema]]
165
+ template = dict[str, WorkflowSchema]
180
166
  json_schema = TypeAdapter(template).json_schema(by_alias=True)
181
167
  template_schema: dict[str, str] = {
182
168
  "$schema": "http://json-schema.org/draft-07/schema#",
ddeutil/workflow/conf.py CHANGED
@@ -3,14 +3,49 @@
3
3
  # Licensed under the MIT License. See LICENSE in the project root for
4
4
  # license information.
5
5
  # ------------------------------------------------------------------------------
6
- from __future__ import annotations
7
-
6
+ """Configuration Management for Workflow System.
7
+
8
+ This module provides comprehensive configuration management for the workflow
9
+ system, including YAML parsing, dynamic configuration loading, environment
10
+ variable handling, and configuration validation.
11
+
12
+ The configuration system supports hierarchical configuration files, environment
13
+ variable substitution, and dynamic parameter resolution for flexible workflow
14
+ deployment across different environments.
15
+
16
+ Classes:
17
+ Config: Main configuration class with validation
18
+ YamlParser: YAML configuration file parser and loader
19
+
20
+ Functions:
21
+ dynamic: Get dynamic configuration values with fallbacks
22
+ pass_env: Process environment variable substitution
23
+ api_config: Get API-specific configuration settings
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
+ Note:
39
+ Configuration files support environment variable substitution using
40
+ ${VAR_NAME} syntax and provide extensive validation capabilities.
41
+ """
8
42
  import copy
9
43
  import os
10
44
  from collections.abc import Iterator
11
45
  from functools import cached_property
12
46
  from pathlib import Path
13
47
  from typing import Final, Optional, TypeVar, Union
48
+ from urllib.parse import ParseResult, urlparse
14
49
  from zoneinfo import ZoneInfo
15
50
 
16
51
  from ddeutil.core import str2bool
@@ -28,10 +63,12 @@ PREFIX: Final[str] = "WORKFLOW"
28
63
  def env(var: str, default: Optional[str] = None) -> Optional[str]:
29
64
  """Get environment variable with uppercase and adding prefix string.
30
65
 
31
- :param var: (str) A env variable name.
32
- :param default: (Optional[str]) A default value if an env var does not set.
66
+ Args:
67
+ var: A env variable name.
68
+ default: A default value if an env var does not set.
33
69
 
34
- :rtype: Optional[str]
70
+ Returns:
71
+ Optional[str]: The environment variable value or default.
35
72
  """
36
73
  return os.getenv(f"{PREFIX}_{var.upper().replace(' ', '_')}", default)
37
74
 
@@ -47,25 +84,18 @@ class Config: # pragma: no cov
47
84
  def conf_path(self) -> Path:
48
85
  """Config path that keep all workflow template YAML files.
49
86
 
50
- :rtype: Path
87
+ Returns:
88
+ Path: The configuration path for workflow templates.
51
89
  """
52
90
  return Path(env("CORE_CONF_PATH", "./conf"))
53
91
 
54
- @property
55
- def tz(self) -> ZoneInfo:
56
- """Timezone value that return with the `ZoneInfo` object and use for all
57
- datetime object in this workflow engine.
58
-
59
- :rtype: ZoneInfo
60
- """
61
- return ZoneInfo(env("CORE_TIMEZONE", "UTC"))
62
-
63
92
  @property
64
93
  def generate_id_simple_mode(self) -> bool:
65
94
  """Flag for generate running ID with simple mode. That does not use
66
95
  `md5` function after generate simple mode.
67
96
 
68
- :rtype: bool
97
+ Returns:
98
+ bool: True if simple mode ID generation is enabled.
69
99
  """
70
100
  return str2bool(env("CORE_GENERATE_ID_SIMPLE_MODE", "true"))
71
101
 
@@ -92,8 +122,8 @@ class Config: # pragma: no cov
92
122
  return [r.strip() for r in regis_filter_str.split(",")]
93
123
 
94
124
  @property
95
- def trace_path(self) -> Path:
96
- return Path(env("LOG_TRACE_PATH", "./logs"))
125
+ def trace_url(self) -> ParseResult:
126
+ return urlparse(env("LOG_TRACE_URL", "file:./logs"))
97
127
 
98
128
  @property
99
129
  def debug(self) -> bool:
@@ -103,6 +133,16 @@ class Config: # pragma: no cov
103
133
  """
104
134
  return str2bool(env("LOG_DEBUG_MODE", "true"))
105
135
 
136
+ @property
137
+ def log_tz(self) -> ZoneInfo:
138
+ """Timezone value that return with the `ZoneInfo` object and use for all
139
+ datetime object in this workflow engine.
140
+
141
+ Returns:
142
+ ZoneInfo: The timezone configuration for the workflow engine.
143
+ """
144
+ return ZoneInfo(env("LOG_TIMEZONE", "UTC"))
145
+
106
146
  @property
107
147
  def log_format(self) -> str:
108
148
  return env(
@@ -129,8 +169,8 @@ class Config: # pragma: no cov
129
169
  return str2bool(env("LOG_TRACE_ENABLE_WRITE", "false"))
130
170
 
131
171
  @property
132
- def audit_path(self) -> Path:
133
- return Path(env("LOG_AUDIT_PATH", "./audits"))
172
+ def audit_url(self) -> ParseResult:
173
+ return urlparse(env("LOG_AUDIT_URL", "file:./audits"))
134
174
 
135
175
  @property
136
176
  def enable_write_audit(self) -> bool:
@@ -190,8 +230,8 @@ class YamlParser:
190
230
  name: str,
191
231
  *,
192
232
  path: Optional[Union[str, Path]] = None,
193
- externals: DictData | None = None,
194
- extras: DictData | None = None,
233
+ externals: Optional[DictData] = None,
234
+ extras: Optional[DictData] = None,
195
235
  obj: Optional[Union[object, str]] = None,
196
236
  ) -> None:
197
237
  self.path: Path = Path(dynamic("conf_path", f=path, extras=extras))
@@ -260,10 +300,13 @@ class YamlParser:
260
300
  continue
261
301
 
262
302
  if data := cls.filter_yaml(file, name=name):
303
+ file_stat: os.stat_result = file.lstat()
304
+ data["created_at"] = file_stat.st_ctime
305
+ data["updated_at"] = file_stat.st_mtime
263
306
  if not obj_type:
264
- all_data.append((file.lstat().st_mtime, data))
307
+ all_data.append((file_stat.st_mtime, data))
265
308
  elif (t := data.get("type")) and t == obj_type:
266
- all_data.append((file.lstat().st_mtime, data))
309
+ all_data.append((file_stat.st_mtime, data))
267
310
  else:
268
311
  continue
269
312
 
@@ -435,10 +478,12 @@ def pass_env(value: T) -> T: # pragma: no cov
435
478
 
436
479
 
437
480
  class CallerSecret(SecretStr): # pragma: no cov
438
- """Workflow Secret String model."""
481
+ """Workflow Secret String model that was inherited from the SecretStr model
482
+ and override the `get_secret_value` method only.
483
+ """
439
484
 
440
485
  def get_secret_value(self) -> str:
441
- """Override get_secret_value by adding pass_env before return the
486
+ """Override the `get_secret_value` by adding pass_env before return the
442
487
  real-value.
443
488
 
444
489
  :rtype: str
@@ -3,9 +3,22 @@
3
3
  # Licensed under the MIT License. See LICENSE in the project root for
4
4
  # license information.
5
5
  # ------------------------------------------------------------------------------
6
- """Exception objects for this package do not do anything because I want to
7
- create the lightweight workflow package. So, this module do just an exception
8
- annotate for handle error only.
6
+ """Exception Classes for Workflow Orchestration.
7
+
8
+ This module provides a comprehensive exception hierarchy for the workflow system.
9
+ The exceptions are designed to be lightweight while providing sufficient context
10
+ for error handling and debugging.
11
+
12
+ Classes:
13
+ BaseError: Base exception class with context support
14
+ StageError: Exceptions related to stage execution
15
+ JobError: Exceptions related to job execution
16
+ WorkflowError: Exceptions related to workflow execution
17
+ ParamError: Exceptions related to parameter validation
18
+ ResultError: Exceptions related to result processing
19
+
20
+ Functions:
21
+ to_dict: Convert exception instances to dictionary format
9
22
  """
10
23
  from __future__ import annotations
11
24
 
@@ -15,8 +28,14 @@ from .__types import DictData, StrOrInt
15
28
 
16
29
 
17
30
  class ErrorData(TypedDict):
18
- """Error data type dict for typing necessary keys of return of to_dict func
19
- and method.
31
+ """Error data structure for exception serialization.
32
+
33
+ This TypedDict defines the standard structure for converting exceptions
34
+ to dictionary format for consistent error handling across the system.
35
+
36
+ Attributes:
37
+ name: Exception class name
38
+ message: Exception message content
20
39
  """
21
40
 
22
41
  name: str
@@ -24,11 +43,26 @@ class ErrorData(TypedDict):
24
43
 
25
44
 
26
45
  def to_dict(exception: Exception, **kwargs) -> ErrorData: # pragma: no cov
27
- """Create dict data from exception instance.
28
-
29
- :param exception: An exception object.
30
-
31
- :rtype: ErrorData
46
+ """Create dictionary data from exception instance.
47
+
48
+ Converts an exception object to a standardized dictionary format
49
+ for consistent error handling and serialization.
50
+
51
+ Args:
52
+ exception: Exception object to convert
53
+ **kwargs: Additional key-value pairs to include in result
54
+
55
+ Returns:
56
+ ErrorData: Dictionary containing exception name and message
57
+
58
+ Example:
59
+ ```python
60
+ try:
61
+ raise ValueError("Something went wrong")
62
+ except Exception as e:
63
+ error_data = to_dict(e, context="workflow_execution")
64
+ # Returns: {"name": "ValueError", "message": "Something went wrong", "context": "workflow_execution"}
65
+ ```
32
66
  """
33
67
  return {
34
68
  "name": exception.__class__.__name__,
@@ -38,14 +72,27 @@ def to_dict(exception: Exception, **kwargs) -> ErrorData: # pragma: no cov
38
72
 
39
73
 
40
74
  class BaseError(Exception):
41
- """Base Workflow exception class will implement the ``refs`` argument for
42
- making an error context to the result context.
75
+ """Base exception class for all workflow-related errors.
43
76
 
44
- Attributes:
45
- refs: (:obj:str, optional)
46
- context: (:obj:DictData)
47
- params: (:obj:DictData)
77
+ BaseError provides the foundation for all workflow exceptions, offering
78
+ enhanced context management and error tracking capabilities. It supports
79
+ reference IDs for error correlation and maintains context information
80
+ for debugging purposes.
48
81
 
82
+ Attributes:
83
+ refs: Optional reference identifier for error correlation
84
+ context: Additional context data related to the error
85
+ params: Parameter data that was being processed when error occurred
86
+
87
+ Example:
88
+ ```python
89
+ try:
90
+ # Some workflow operation
91
+ pass
92
+ except BaseError as e:
93
+ error_dict = e.to_dict(with_refs=True)
94
+ print(f"Error in {e.refs}: {error_dict}")
95
+ ```
49
96
  """
50
97
 
51
98
  def __init__(
@@ -76,10 +123,30 @@ class BaseError(Exception):
76
123
  with_refs: bool = False,
77
124
  **kwargs,
78
125
  ) -> Union[ErrorData, dict[str, ErrorData]]:
79
- """Return ErrorData data from the current exception object. If with_refs
80
- flag was set, it will return mapping of refs and itself data.
126
+ """Convert exception to dictionary format.
127
+
128
+ Serializes the exception to a standardized dictionary format.
129
+ Optionally includes reference mapping for error correlation.
130
+
131
+ Args:
132
+ with_refs: Include reference ID mapping in result
133
+ **kwargs: Additional key-value pairs to include
134
+
135
+ Returns:
136
+ ErrorData or dict: Exception data, optionally mapped by reference ID
137
+
138
+ Example:
139
+ ```python
140
+ error = BaseError("Something failed", refs="stage-1")
141
+
142
+ # Simple format
143
+ data = error.to_dict()
144
+ # Returns: {"name": "BaseError", "message": "Something failed"}
81
145
 
82
- :rtype: ErrorData
146
+ # With reference mapping
147
+ ref_data = error.to_dict(with_refs=True)
148
+ # Returns: {"stage-1": {"name": "BaseError", "message": "Something failed"}}
149
+ ```
83
150
  """
84
151
  data: ErrorData = to_dict(self)
85
152
  if with_refs and (self.refs is not None and self.refs != "EMPTY"):