ddeutil-workflow 0.0.72__py3-none-any.whl → 0.0.74__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.
@@ -1 +1 @@
1
- __version__: str = "0.0.72"
1
+ __version__: str = "0.0.74"
@@ -38,8 +38,11 @@ class YearReachLimit(Exception):
38
38
  def str2cron(value: str) -> str: # pragma: no cov
39
39
  """Convert Special String with the @ prefix to Crontab value.
40
40
 
41
- :param value: (str) A string value that want to convert to cron value.
42
- :rtype: str
41
+ Args:
42
+ value: A string value that want to convert to cron value.
43
+
44
+ Returns:
45
+ str: The converted cron expression.
43
46
 
44
47
  Table:
45
48
 
@@ -165,9 +168,10 @@ CRON_UNITS_YEAR: Units = CRON_UNITS + (
165
168
  class CronPart:
166
169
  """Part of Cron object that represent a collection of positive integers.
167
170
 
168
- :param unit: A Unit dataclass object.
169
- :param values: A crontab values that want to validate
170
- :param options: A Options dataclass object.
171
+ Args:
172
+ unit: A Unit dataclass object.
173
+ values: A crontab values that want to validate
174
+ options: A Options dataclass object.
171
175
  """
172
176
 
173
177
  __slots__: tuple[str, ...] = (
@@ -288,7 +292,11 @@ class CronPart:
288
292
  """Parses a string as a range of positive integers. The string should
289
293
  include only `-` and `,` special strings.
290
294
 
291
- :param value: (str) A string value that want to parse
295
+ Args:
296
+ value: A string value that want to parse
297
+
298
+ Returns:
299
+ tuple[int, ...]: Parsed range of integers.
292
300
 
293
301
  TODO: support for `L`, `W`, and `#`
294
302
  ---
@@ -334,8 +342,6 @@ class CronPart:
334
342
  - 15 10 ? * 6L 2002-2005
335
343
  Run at 10:15am UTC on the last Friday of each month during the
336
344
  years 2002 to 2005
337
-
338
- :rtype: tuple[int, ...]
339
345
  """
340
346
  interval_list: list[list[int]] = []
341
347
  # NOTE: Start replace alternative like JAN to FEB or MON to SUN.
@@ -3,19 +3,108 @@
3
3
  # Licensed under the MIT License. See LICENSE in the project root for
4
4
  # license information.
5
5
  # ------------------------------------------------------------------------------
6
- from .__cron import CronJob, CronRunner
6
+ """DDE Workflow - Lightweight Workflow Orchestration Package.
7
+
8
+ This package provides a comprehensive workflow orchestration system with YAML template
9
+ support. It enables developers to create, manage, and execute complex workflows with
10
+ minimal configuration.
11
+
12
+ Key Features:
13
+ - YAML-based workflow configuration
14
+ - Job and stage execution management
15
+ - Scheduling with cron-like syntax
16
+ - Parallel and sequential execution support
17
+ - Comprehensive error handling and logging
18
+ - Extensible stage types (Bash, Python, Docker, etc.)
19
+ - Matrix strategy for parameterized workflows
20
+ - Audit and tracing capabilities
21
+
22
+ Main Classes:
23
+ Workflow: Core workflow orchestration class
24
+ Job: Execution unit containing stages
25
+ Stage: Individual task execution unit
26
+ CronJob: Scheduled workflow execution
27
+ Audit: Execution tracking and logging
28
+ Result: Execution status and output management
29
+
30
+ Example:
31
+ Basic workflow usage:
32
+
33
+ ```python
34
+ from ddeutil.workflow import Workflow
35
+
36
+ # Load workflow from configuration
37
+ workflow = Workflow.from_conf('my-workflow')
38
+
39
+ # Execute with parameters
40
+ result = workflow.execute({'param1': 'value1'})
41
+
42
+ if result.status == 'SUCCESS':
43
+ print("Workflow completed successfully")
44
+ ```
45
+
46
+ Note:
47
+ This package requires Python 3.9+ and supports both synchronous and
48
+ asynchronous execution patterns.
49
+ """
50
+ from .__cron import CronRunner
7
51
  from .__types import DictData, DictStr, Matrix, Re, TupleStr
8
52
  from .audits import (
9
53
  Audit,
10
- AuditModel,
11
54
  FileAudit,
12
- get_audit,
55
+ get_audit_model,
13
56
  )
14
57
  from .conf import *
15
- from .errors import *
16
- from .event import *
17
- from .job import *
18
- from .params import *
58
+ from .errors import (
59
+ BaseError,
60
+ JobCancelError,
61
+ JobError,
62
+ JobSkipError,
63
+ ResultError,
64
+ StageCancelError,
65
+ StageError,
66
+ StageSkipError,
67
+ UtilError,
68
+ WorkflowCancelError,
69
+ WorkflowError,
70
+ WorkflowTimeoutError,
71
+ to_dict,
72
+ )
73
+ from .event import (
74
+ Cron,
75
+ CronJob,
76
+ CronJobYear,
77
+ Crontab,
78
+ CrontabValue,
79
+ CrontabYear,
80
+ Event,
81
+ Interval,
82
+ )
83
+ from .job import (
84
+ Job,
85
+ OnAzBatch,
86
+ OnDocker,
87
+ OnLocal,
88
+ OnSelfHosted,
89
+ Rule,
90
+ RunsOnModel,
91
+ Strategy,
92
+ docker_execution,
93
+ local_execute,
94
+ local_execute_strategy,
95
+ self_hosted_execute,
96
+ )
97
+ from .params import (
98
+ ArrayParam,
99
+ DateParam,
100
+ DatetimeParam,
101
+ DecimalParam,
102
+ FloatParam,
103
+ IntParam,
104
+ MapParam,
105
+ Param,
106
+ StrParam,
107
+ )
19
108
  from .result import (
20
109
  CANCEL,
21
110
  FAILED,
@@ -26,15 +115,35 @@ from .result import (
26
115
  Status,
27
116
  )
28
117
  from .reusables import *
29
- from .stages import *
118
+ from .stages import (
119
+ BashStage,
120
+ CallStage,
121
+ CaseStage,
122
+ DockerStage,
123
+ EmptyStage,
124
+ ForEachStage,
125
+ ParallelStage,
126
+ PyStage,
127
+ RaiseStage,
128
+ Stage,
129
+ TriggerStage,
130
+ UntilStage,
131
+ VirtualPyStage,
132
+ )
30
133
  from .traces import (
31
134
  ConsoleTrace,
32
135
  FileTrace,
33
136
  Trace,
34
137
  TraceData,
35
138
  TraceMeta,
36
- TraceModel,
37
139
  get_trace,
38
140
  )
39
141
  from .utils import *
40
- from .workflow import *
142
+ from .workflow import (
143
+ EVENT,
144
+ FORCE,
145
+ NORMAL,
146
+ RERUN,
147
+ ReleaseType,
148
+ Workflow,
149
+ )
@@ -27,6 +27,47 @@ DictData = dict[str, Any]
27
27
  DictStr = dict[str, str]
28
28
  Matrix = dict[str, Union[list[str], list[int]]]
29
29
 
30
+ # Pre-compile regex patterns for better performance
31
+ _RE_CALLER_PATTERN = r"""
32
+ \$ # start with $
33
+ {{ # value open with {{
34
+ \s* # whitespace or not
35
+ (?P<caller>
36
+ (?P<caller_prefix>(?:[a-zA-Z_-]+\??\.)*)
37
+ (?P<caller_last>[a-zA-Z0-9_\-.'\"(\)[\]{}]+\??)
38
+ )
39
+ \s* # whitespace or not
40
+ (?P<post_filters>
41
+ (?:
42
+ \|\s*
43
+ (?:
44
+ [a-zA-Z0-9_]{3,}
45
+ [a-zA-Z0-9_.,-\\%\s'\"[\]()\{}]*
46
+ )\s*
47
+ )*
48
+ )
49
+ }} # value close with }}
50
+ """
51
+
52
+ _RE_TASK_FMT_PATTERN = r"""
53
+ ^ # start task format
54
+ (?P<path>[^/@]+)
55
+ / # start get function with /
56
+ (?P<func>[^@]+)
57
+ @ # start tag with @
58
+ (?P<tag>.+)
59
+ $ # end task format
60
+ """
61
+
62
+ # Compile patterns at module level for better performance
63
+ RE_CALLER: Pattern = re.compile(
64
+ _RE_CALLER_PATTERN, MULTILINE | IGNORECASE | UNICODE | VERBOSE
65
+ )
66
+
67
+ RE_TASK_FMT: Pattern = re.compile(
68
+ _RE_TASK_FMT_PATTERN, MULTILINE | IGNORECASE | UNICODE | VERBOSE
69
+ )
70
+
30
71
 
31
72
  class Context(TypedDict):
32
73
  """TypeDict support the Context."""
@@ -51,10 +92,12 @@ class CallerRe:
51
92
  def from_regex(cls, match: Match[str]) -> Self:
52
93
  """Class construct from matching result.
53
94
 
54
- :param match: A match string object for contract this Caller regex data
55
- class.
95
+ Args:
96
+ match: A match string object for contract this Caller regex data
97
+ class.
56
98
 
57
- :rtype: Self
99
+ Returns:
100
+ Self: The constructed CallerRe instance from regex match.
58
101
  """
59
102
  return cls(full=match.group(0), **match.groupdict())
60
103
 
@@ -79,29 +122,7 @@ class Re:
79
122
  # - ${{ params.datetime | fmt('%Y-%m-%d') }}
80
123
  # - ${{ params.source?.schema }}
81
124
  #
82
- __re_caller: str = r"""
83
- \$ # start with $
84
- {{ # value open with {{
85
- \s* # whitespace or not
86
- (?P<caller>
87
- (?P<caller_prefix>(?:[a-zA-Z_-]+\??\.)*)
88
- (?P<caller_last>[a-zA-Z0-9_\-.'\"(\)[\]{}]+\??)
89
- )
90
- \s* # whitespace or not
91
- (?P<post_filters>
92
- (?:
93
- \|\s*
94
- (?:
95
- [a-zA-Z0-9_]{3,}
96
- [a-zA-Z0-9_.,-\\%\s'\"[\]()\{}]*
97
- )\s*
98
- )*
99
- )
100
- }} # value close with }}
101
- """
102
- RE_CALLER: Pattern = re.compile(
103
- __re_caller, MULTILINE | IGNORECASE | UNICODE | VERBOSE
104
- )
125
+ RE_CALLER: Pattern = RE_CALLER
105
126
 
106
127
  # NOTE:
107
128
  # Regular expression:
@@ -111,28 +132,19 @@ class Re:
111
132
  # Examples:
112
133
  # - tasks/function@dummy
113
134
  #
114
- __re_task_fmt: str = r"""
115
- ^ # start task format
116
- (?P<path>[^/@]+)
117
- / # start get function with /
118
- (?P<func>[^@]+)
119
- @ # start tag with @
120
- (?P<tag>.+)
121
- $ # end task format
122
- """
123
- RE_TASK_FMT: Pattern = re.compile(
124
- __re_task_fmt, MULTILINE | IGNORECASE | UNICODE | VERBOSE
125
- )
135
+ RE_TASK_FMT: Pattern = RE_TASK_FMT
126
136
 
127
137
  @classmethod
128
138
  def finditer_caller(cls, value: str) -> Iterator[CallerRe]:
129
139
  """Generate CallerRe object that create from matching object that
130
140
  extract with re.finditer function.
131
141
 
132
- :param value: (str) A string value that want to finditer with the caller
133
- regular expression.
142
+ Args:
143
+ value: A string value that want to finditer with the caller
144
+ regular expression.
134
145
 
135
- :rtype: Iterator[CallerRe]
146
+ Yields:
147
+ CallerRe: CallerRe objects created from regex matches.
136
148
  """
137
149
  for found in cls.RE_CALLER.finditer(value):
138
150
  yield CallerRe.from_regex(found)
@@ -1,3 +1,30 @@
1
+ """FastAPI Web Application for Workflow Management.
2
+
3
+ This module provides a RESTful API interface for workflow orchestration using
4
+ FastAPI. It enables remote workflow management, execution monitoring, and
5
+ provides endpoints for workflow operations.
6
+
7
+ The API supports:
8
+ - Workflow execution and management
9
+ - Job status monitoring
10
+ - Log streaming and access
11
+ - Result retrieval and analysis
12
+
13
+ Example:
14
+ ```python
15
+ from ddeutil.workflow.api import app
16
+
17
+ # Run the API server
18
+ import uvicorn
19
+ uvicorn.run(app, host="0.0.0.0", port=8000)
20
+ ```
21
+
22
+ Routes:
23
+ - /workflows: Workflow management endpoints
24
+ - /jobs: Job execution and monitoring
25
+ - /logs: Log access and streaming
26
+ """
27
+
1
28
  # ------------------------------------------------------------------------------
2
29
  # Copyright (c) 2022 Korawich Anuttra. All rights reserved.
3
30
  # Licensed under the MIT License. See LICENSE in the project root for
@@ -28,7 +55,17 @@ logger = logging.getLogger("uvicorn.error")
28
55
 
29
56
  @contextlib.asynccontextmanager
30
57
  async def lifespan(_: FastAPI) -> AsyncIterator[dict[str, list]]:
31
- """Lifespan function for the FastAPI application."""
58
+ """FastAPI application lifespan management.
59
+
60
+ Manages the startup and shutdown lifecycle of the FastAPI application.
61
+ Currently yields an empty dictionary for future extension.
62
+
63
+ Args:
64
+ _: FastAPI application instance (unused)
65
+
66
+ Yields:
67
+ dict: Empty dictionary for future lifespan data
68
+ """
32
69
  yield {}
33
70
 
34
71
 
@@ -59,7 +96,20 @@ app.add_middleware(
59
96
 
60
97
  @app.get(path="/", response_class=UJSONResponse)
61
98
  async def health() -> UJSONResponse:
62
- """Index view that not return any template without json status."""
99
+ """Health check endpoint for API status monitoring.
100
+
101
+ Provides a simple health check endpoint to verify the API is running
102
+ and responding correctly. Returns a JSON response with health status.
103
+
104
+ Returns:
105
+ UJSONResponse: JSON response confirming healthy API status
106
+
107
+ Example:
108
+ ```bash
109
+ curl http://localhost:8000/
110
+ # Returns: {"message": "Workflow already start up with healthy status."}
111
+ ```
112
+ """
63
113
  logger.info("[API]: Workflow API Application already running ...")
64
114
  return UJSONResponse(
65
115
  content={"message": "Workflow already start up with healthy status."},
@@ -78,7 +128,28 @@ async def validation_exception_handler(
78
128
  request: Request,
79
129
  exc: RequestValidationError,
80
130
  ) -> UJSONResponse:
81
- """Error Handler for model validate does not valid."""
131
+ """Handle request validation errors from Pydantic models.
132
+
133
+ Provides standardized error responses for request validation failures,
134
+ including detailed error information for debugging and client feedback.
135
+
136
+ Args:
137
+ request: The FastAPI request object (unused)
138
+ exc: The validation exception containing error details
139
+
140
+ Returns:
141
+ UJSONResponse: Standardized error response with validation details
142
+
143
+ Example:
144
+ When a request fails validation:
145
+ ```json
146
+ {
147
+ "message": "Body does not parsing with model.",
148
+ "detail": [...],
149
+ "body": {...}
150
+ }
151
+ ```
152
+ """
82
153
  _ = request
83
154
  return UJSONResponse(
84
155
  status_code=st.HTTP_422_UNPROCESSABLE_ENTITY,
@@ -8,72 +8,58 @@ from __future__ import annotations
8
8
  import logging
9
9
  from typing import Any, Optional
10
10
 
11
- from fastapi import APIRouter
11
+ from fastapi import APIRouter, Body
12
12
  from fastapi import status as st
13
13
  from fastapi.responses import UJSONResponse
14
- from pydantic import BaseModel, Field
15
14
 
16
15
  from ...__types import DictData
17
16
  from ...errors import JobError
18
17
  from ...job import Job
19
- from ...result import Result
18
+ from ...traces import Trace, get_trace
19
+ from ...utils import gen_id
20
20
 
21
21
  logger = logging.getLogger("uvicorn.error")
22
22
  router = APIRouter(prefix="/job", tags=["job"])
23
23
 
24
24
 
25
- class ResultCreate(BaseModel):
26
- """Create Result model for receive running IDs to create the Result
27
- dataclass.
28
- """
29
-
30
- run_id: str = Field(description="A running ID.")
31
- parent_run_id: Optional[str] = Field(
32
- default=None, description="A parent running ID."
33
- )
34
-
35
-
36
25
  @router.post(
37
26
  path="/execute/",
38
27
  response_class=UJSONResponse,
39
28
  status_code=st.HTTP_200_OK,
40
29
  )
41
30
  async def job_execute(
42
- result: ResultCreate,
43
31
  job: Job,
44
32
  params: dict[str, Any],
45
- extras: Optional[dict[str, Any]] = None,
33
+ run_id: str = Body(...),
34
+ extras: Optional[dict[str, Any]] = Body(default=None),
46
35
  ) -> UJSONResponse:
47
36
  """Execute job via RestAPI with execute route path."""
48
37
  logger.info("[API]: Start execute job ...")
49
- rs: Result = Result(
50
- run_id=result.run_id,
51
- parent_run_id=result.parent_run_id,
52
- extras=extras or {},
53
- )
38
+ parent_run_id: str = run_id
39
+ run_id = gen_id(job.id, unique=True)
54
40
 
55
41
  if extras:
56
42
  job.extras = extras
57
43
 
44
+ trace: Trace = get_trace(
45
+ run_id, parent_run_id=parent_run_id, extras=job.extras
46
+ )
47
+
58
48
  context: DictData = {}
59
49
  try:
60
50
  job.set_outputs(
61
51
  job.execute(
62
52
  params=params,
63
- run_id=rs.run_id,
64
- parent_run_id=rs.parent_run_id,
53
+ run_id=parent_run_id,
65
54
  ).context,
66
55
  to=context,
67
56
  )
68
57
  except JobError as err:
69
- rs.trace.error(f"[JOB]: {err.__class__.__name__}: {err}")
58
+ trace.error(f"[JOB]: {err.__class__.__name__}: {err}")
70
59
  return UJSONResponse(
71
60
  content={
72
61
  "message": str(err),
73
- "result": {
74
- "run_id": rs.run_id,
75
- "parent_run_id": rs.parent_run_id,
76
- },
62
+ "run_id": parent_run_id,
77
63
  "job": job.model_dump(
78
64
  by_alias=True,
79
65
  exclude_none=False,
@@ -88,7 +74,7 @@ async def job_execute(
88
74
  return UJSONResponse(
89
75
  content={
90
76
  "message": "Execute job via RestAPI successful.",
91
- "result": {"run_id": rs.run_id, "parent_run_id": rs.parent_run_id},
77
+ "run_id": parent_run_id,
92
78
  "job": job.model_dump(
93
79
  by_alias=True,
94
80
  exclude_none=False,
@@ -10,7 +10,7 @@ from fastapi import APIRouter, Path, Query
10
10
  from fastapi import status as st
11
11
  from fastapi.responses import UJSONResponse
12
12
 
13
- from ...audits import get_audit
13
+ from ...audits import get_audit_model
14
14
  from ...result import Result
15
15
 
16
16
  router = APIRouter(
@@ -86,11 +86,11 @@ async def get_trace_with_id(run_id: str):
86
86
  )
87
87
  async def get_audits():
88
88
  """Return all audit logs from the current audit log path that config with
89
- `WORKFLOW_AUDIT_PATH` environment variable name.
89
+ `WORKFLOW_AUDIT_URL` environment variable name.
90
90
  """
91
91
  return {
92
92
  "message": "Getting audit logs",
93
- "audits": list(get_audit().find_audits(name="demo")),
93
+ "audits": list(get_audit_model().find_audits(name="demo")),
94
94
  }
95
95
 
96
96
 
@@ -103,13 +103,13 @@ async def get_audits():
103
103
  )
104
104
  async def get_audit_with_workflow(workflow: str):
105
105
  """Return all audit logs with specific workflow name from the current audit
106
- log path that config with `WORKFLOW_AUDIT_PATH` environment variable name.
106
+ log path that config with `WORKFLOW_AUDIT_URL` environment variable name.
107
107
 
108
108
  - **workflow**: A specific workflow name that want to find audit logs.
109
109
  """
110
110
  return {
111
111
  "message": f"Getting audit logs with workflow name {workflow}",
112
- "audits": list(get_audit().find_audits(name="demo")),
112
+ "audits": list(get_audit_model().find_audits(name="demo")),
113
113
  }
114
114
 
115
115
 
@@ -125,7 +125,7 @@ async def get_audit_with_workflow_release(
125
125
  release: str = Path(...),
126
126
  ):
127
127
  """Return all audit logs with specific workflow name and release date from
128
- the current audit log path that config with `WORKFLOW_AUDIT_PATH`
128
+ the current audit log path that config with `WORKFLOW_AUDIT_URL`
129
129
  environment variable name.
130
130
 
131
131
  - **workflow**: A specific workflow name that want to find audit logs.
@@ -136,7 +136,7 @@ async def get_audit_with_workflow_release(
136
136
  f"Getting audit logs with workflow name {workflow} and release "
137
137
  f"{release}"
138
138
  ),
139
- "audits": list(get_audit().find_audits(name="demo")),
139
+ "audits": list(get_audit_model().find_audits(name="demo")),
140
140
  }
141
141
 
142
142
 
@@ -154,7 +154,7 @@ async def get_audit_with_workflow_release_run_id(
154
154
  workflow: str, release: str, run_id: str
155
155
  ):
156
156
  """Return all audit logs with specific workflow name and release date from
157
- the current audit log path that config with `WORKFLOW_AUDIT_PATH`
157
+ the current audit log path that config with `WORKFLOW_AUDIT_URL`
158
158
  environment variable name.
159
159
 
160
160
  - **workflow**: A specific workflow name that want to find audit logs.
@@ -167,5 +167,5 @@ async def get_audit_with_workflow_release_run_id(
167
167
  f"Getting audit logs with workflow name {workflow}, release "
168
168
  f"{release}, and running ID {run_id}"
169
169
  ),
170
- "audits": list(get_audit().find_audits(name="demo")),
170
+ "audits": list(get_audit_model().find_audits(name="demo")),
171
171
  }
@@ -16,7 +16,7 @@ from fastapi.responses import UJSONResponse
16
16
  from pydantic import BaseModel
17
17
 
18
18
  from ...__types import DictData
19
- from ...audits import AuditModel, get_audit
19
+ from ...audits import Audit, get_audit_model
20
20
  from ...conf import YamlParser
21
21
  from ...result import Result
22
22
  from ...workflow import Workflow
@@ -100,7 +100,7 @@ async def get_workflow_audits(name: str):
100
100
  exclude_none=False,
101
101
  exclude_unset=True,
102
102
  )
103
- for audit in get_audit().find_audits(name=name)
103
+ for audit in get_audit_model().find_audits(name=name)
104
104
  ],
105
105
  }
106
106
  except FileNotFoundError:
@@ -114,7 +114,7 @@ async def get_workflow_audits(name: str):
114
114
  async def get_workflow_release_audit(name: str, release: str):
115
115
  """Get Workflow audit log with an input release value."""
116
116
  try:
117
- audit: AuditModel = get_audit().find_audit_with_release(
117
+ audit: Audit = get_audit_model().find_audit_with_release(
118
118
  name=name,
119
119
  release=datetime.strptime(release, "%Y%m%d%H%M%S"),
120
120
  )