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.
Files changed (66) hide show
  1. {ddeutil_workflow-0.0.80/src/ddeutil_workflow.egg-info → ddeutil_workflow-0.0.82}/PKG-INFO +1 -1
  2. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/pyproject.toml +0 -1
  3. ddeutil_workflow-0.0.82/src/ddeutil/workflow/__about__.py +2 -0
  4. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/__init__.py +19 -6
  5. ddeutil_workflow-0.0.80/src/ddeutil/workflow/cli.py → ddeutil_workflow-0.0.82/src/ddeutil/workflow/__main__.py +42 -33
  6. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/api/routes/job.py +2 -2
  7. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/api/routes/logs.py +8 -61
  8. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/audits.py +46 -17
  9. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/conf.py +64 -44
  10. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/errors.py +12 -2
  11. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/job.py +70 -16
  12. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/result.py +33 -11
  13. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/reusables.py +16 -17
  14. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/stages.py +172 -134
  15. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/traces.py +64 -24
  16. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/utils.py +7 -15
  17. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/workflow.py +73 -84
  18. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82/src/ddeutil_workflow.egg-info}/PKG-INFO +1 -1
  19. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil_workflow.egg-info/SOURCES.txt +0 -1
  20. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/tests/test_audits.py +10 -6
  21. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/tests/test_cli.py +1 -1
  22. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/tests/test_conf.py +81 -29
  23. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/tests/test_job.py +7 -0
  24. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/tests/test_job_exec.py +59 -0
  25. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/tests/test_result.py +11 -0
  26. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/tests/test_traces.py +19 -13
  27. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/tests/test_utils.py +2 -4
  28. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/tests/test_workflow_exec.py +175 -2
  29. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/tests/test_workflow_release.py +65 -5
  30. ddeutil_workflow-0.0.80/src/ddeutil/workflow/__about__.py +0 -1
  31. ddeutil_workflow-0.0.80/src/ddeutil/workflow/__main__.py +0 -4
  32. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/LICENSE +0 -0
  33. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/README.md +0 -0
  34. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/setup.cfg +0 -0
  35. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/__cron.py +0 -0
  36. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/__types.py +0 -0
  37. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/api/__init__.py +0 -0
  38. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/api/log_conf.py +0 -0
  39. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/api/routes/__init__.py +0 -0
  40. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/api/routes/workflows.py +0 -0
  41. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/event.py +0 -0
  42. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/params.py +0 -0
  43. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/plugins/__init__.py +0 -0
  44. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/plugins/providers/__init__.py +0 -0
  45. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/plugins/providers/aws.py +0 -0
  46. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/plugins/providers/az.py +0 -0
  47. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/plugins/providers/container.py +0 -0
  48. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/plugins/providers/gcs.py +0 -0
  49. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil_workflow.egg-info/dependency_links.txt +0 -0
  50. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil_workflow.egg-info/entry_points.txt +0 -0
  51. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil_workflow.egg-info/requires.txt +0 -0
  52. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/src/ddeutil_workflow.egg-info/top_level.txt +0 -0
  53. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/tests/test__cron.py +0 -0
  54. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/tests/test__regex.py +0 -0
  55. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/tests/test_errors.py +0 -0
  56. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/tests/test_event.py +0 -0
  57. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/tests/test_job_exec_strategy.py +0 -0
  58. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/tests/test_params.py +0 -0
  59. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/tests/test_reusables_call_tag.py +0 -0
  60. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/tests/test_reusables_func_model.py +0 -0
  61. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/tests/test_reusables_template.py +0 -0
  62. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/tests/test_reusables_template_filter.py +0 -0
  63. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/tests/test_strategy.py +0 -0
  64. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/tests/test_workflow.py +0 -0
  65. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/tests/test_workflow_exec_job.py +0 -0
  66. {ddeutil_workflow-0.0.80 → ddeutil_workflow-0.0.82}/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.80
3
+ Version: 0.0.82
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.82"
2
+ __python_version__: str = "3.9"
@@ -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
- TraceManager,
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
- 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
- second-job:
77
-
78
- - name: "Hello Env"
79
- echo: "Start say hi with ${ WORKFLOW_DEMO_HELLO }"
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
- rs.trace.info(f"Hello, {name}")
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 env vars\n")
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
- "WORKFLOW_LOG_TRACE_ENABLE_WRITE=false\n"
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
- "> `source .env && workflow-cli workflows execute --name=wf-example`"
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: 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
+ )
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 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,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 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
+ 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: str = Field(description="A running type before logging.")
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: AuditData,
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, data: AuditData, *, extras: Optional[DictData] = None
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: The workflow name.
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
- # 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()
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: TraceManager = get_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: TraceManager = get_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
- :param name: (str) A name of data that want to find.
259
- :param path: (Path) A config path object.
260
- :param paths: (list[Path]) A list of config path object.
261
- :param obj: (object | str) An object that want to validate matching
262
- before return.
263
- :param extras: (DictData) An extra parameter that use to override core
264
- config values.
265
- :param ignore_filename: (str) An ignore filename. Default is
266
- ``.confignore`` filename.
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
- :rtype: DictData
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
- ``.confignore`` filename.
328
- tags: (list[str])
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
- :rtype: Iterator[tuple[str, DictData]]
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 (t := data.get("type")) and t == obj_type:
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
- :param file: (Path) A file path that want to extract YAML context.
409
- :param name: (str) A key name that search on a YAML context.
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
- :rtype: DictData
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
- return values.get(name, {}) if name else values
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
- >>> # Simple format
138
+
139
+ Simple format
139
140
  >>> error.to_dict()
140
141
  >>> # Returns: {"name": "BaseError", "message": "Something failed"}
141
142
 
142
- >>> # With reference mapping
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