ddeutil-workflow 0.0.77__py3-none-any.whl → 0.0.79__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.
ddeutil/workflow/cli.py CHANGED
@@ -57,7 +57,7 @@ def init() -> None:
57
57
  wf-example:
58
58
  type: Workflow
59
59
  desc: |
60
- An example workflow template.
60
+ An example workflow template that provide the demo of workflow.
61
61
  params:
62
62
  name:
63
63
  type: str
@@ -65,10 +65,18 @@ def init() -> None:
65
65
  jobs:
66
66
  first-job:
67
67
  stages:
68
+
69
+ - name: "Hello Stage"
70
+ echo: "Start say hi to the console"
71
+
68
72
  - name: "Call tasks"
69
73
  uses: tasks/say-hello-func@example
70
74
  with:
71
75
  name: ${{ params.name }}
76
+ second-job:
77
+
78
+ - name: "Hello Env"
79
+ echo: "Start say hi with ${ WORKFLOW_DEMO_HELLO }"
72
80
  """
73
81
  ).lstrip("\n")
74
82
  )
@@ -94,8 +102,22 @@ def init() -> None:
94
102
 
95
103
  init_path = task_path / "__init__.py"
96
104
  init_path.write_text("from .example import hello_world_task\n")
105
+
106
+ dotenv_file = Path(".env")
107
+ mode: str = "a" if dotenv_file.exists() else "w"
108
+ with dotenv_file.open(mode=mode) as f:
109
+ f.write("\n# Workflow env vars\n")
110
+ f.write(
111
+ "WORKFLOW_DEMO_HELLO=foo\n"
112
+ "WORKFLOW_CORE_DEBUG_MODE=true\n"
113
+ "WORKFLOW_LOG_TIMEZONE=Asia/Bangkok\n"
114
+ "WORKFLOW_LOG_TRACE_ENABLE_WRITE=false\n"
115
+ "WORKFLOW_LOG_AUDIT_ENABLE_WRITE=true\n"
116
+ )
117
+
118
+ typer.echo("Starter command:")
97
119
  typer.echo(
98
- "Starter command: `workflow-cli workflows execute --name=wf-example`"
120
+ "> `source .env && workflow-cli workflows execute --name=wf-example`"
99
121
  )
100
122
 
101
123
 
@@ -232,7 +254,7 @@ def workflow_json_schema(
232
254
  json_schema = TypeAdapter(template).json_schema(by_alias=True)
233
255
  template_schema: dict[str, str] = {
234
256
  "$schema": "http://json-schema.org/draft-07/schema#",
235
- "title": "Workflow Configuration Schema",
257
+ "title": "Workflow Configuration JSON Schema",
236
258
  "version": __version__,
237
259
  }
238
260
  with open(output, mode="w", encoding="utf-8") as f:
ddeutil/workflow/conf.py CHANGED
@@ -40,12 +40,12 @@ Note:
40
40
  ${VAR_NAME} syntax and provide extensive validation capabilities.
41
41
  """
42
42
  import copy
43
+ import json
43
44
  import os
44
45
  from collections.abc import Iterator
45
46
  from functools import cached_property
46
47
  from pathlib import Path
47
- from typing import Final, Optional, TypeVar, Union
48
- from urllib.parse import ParseResult, urlparse
48
+ from typing import Any, Final, Optional, TypeVar, Union
49
49
  from zoneinfo import ZoneInfo
50
50
 
51
51
  from ddeutil.core import str2bool
@@ -122,8 +122,8 @@ class Config: # pragma: no cov
122
122
  return [r.strip() for r in regis_filter_str.split(",")]
123
123
 
124
124
  @property
125
- def trace_url(self) -> ParseResult:
126
- return urlparse(env("LOG_TRACE_URL", "file:./logs"))
125
+ def trace_handlers(self) -> list[dict[str, Any]]:
126
+ return json.loads(env("LOG_TRACE_HANDLERS", '[{"type": "console"}]'))
127
127
 
128
128
  @property
129
129
  def debug(self) -> bool:
@@ -155,22 +155,8 @@ class Config: # pragma: no cov
155
155
  )
156
156
 
157
157
  @property
158
- def log_format_file(self) -> str:
159
- return env(
160
- "LOG_FORMAT_FILE",
161
- (
162
- "{datetime} ({process:5d}, {thread:5d}) ({cut_id}) "
163
- "{message:120s} ({filename}:{lineno})"
164
- ),
165
- )
166
-
167
- @property
168
- def enable_write_log(self) -> bool:
169
- return str2bool(env("LOG_TRACE_ENABLE_WRITE", "false"))
170
-
171
- @property
172
- def audit_url(self) -> ParseResult:
173
- return urlparse(env("LOG_AUDIT_URL", "file:./audits"))
158
+ def audit_url(self) -> str:
159
+ return env("LOG_AUDIT_URL", "file:./audits")
174
160
 
175
161
  @property
176
162
  def enable_write_audit(self) -> bool:
@@ -307,8 +293,6 @@ class YamlParser:
307
293
  all_data.append((file_stat.st_mtime, data))
308
294
  elif (t := data.get("type")) and t == obj_type:
309
295
  all_data.append((file_stat.st_mtime, data))
310
- else:
311
- continue
312
296
 
313
297
  return {} if not all_data else max(all_data, key=lambda x: x[0])[1]
314
298
 
@@ -322,7 +306,7 @@ class YamlParser:
322
306
  excluded: Optional[list[str]] = None,
323
307
  extras: Optional[DictData] = None,
324
308
  ignore_filename: Optional[str] = None,
325
- tags: Optional[list[str]] = None,
309
+ tags: Optional[list[Union[str, int]]] = None,
326
310
  ) -> Iterator[tuple[str, DictData]]:
327
311
  """Find all data that match with object type in config path. This class
328
312
  method can use include and exclude list of identity name for filter and
@@ -373,13 +357,15 @@ class YamlParser:
373
357
 
374
358
  if (
375
359
  tags
376
- and (ts := data[key].get("tags"))
377
- and isinstance(ts, list)
378
- and all(t not in tags for t in ts)
379
- ): # pragma: no cov
360
+ and isinstance((ts := data.get("tags", [])), list)
361
+ and any(t not in ts for t in tags)
362
+ ):
380
363
  continue
381
364
 
382
365
  if (t := data.get("type")) and t == obj_type:
366
+ file_stat: os.stat_result = file.lstat()
367
+ data["created_at"] = file_stat.st_ctime
368
+ data["updated_at"] = file_stat.st_mtime
383
369
  marking: tuple[float, DictData] = (
384
370
  file.lstat().st_mtime,
385
371
  data,
@@ -464,7 +450,9 @@ def dynamic(
464
450
  conf: Optional[T] = getattr(config, key, None) if f is None else f
465
451
  if extra is None:
466
452
  return conf
467
- if not isinstance(extra, type(conf)):
453
+ # NOTE: Fix type checking for boolean value and int type like
454
+ # `isinstance(False, int)` which return True.
455
+ if type(extra) is not type(conf):
468
456
  raise TypeError(
469
457
  f"Type of config {key!r} from extras: {extra!r} does not valid "
470
458
  f"as config {type(conf)}."
@@ -56,13 +56,13 @@ def to_dict(exception: Exception, **kwargs) -> ErrorData: # pragma: no cov
56
56
  ErrorData: Dictionary containing exception name and message
57
57
 
58
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
- ```
59
+ >>> try:
60
+ >>> raise ValueError("Something went wrong")
61
+ >>> except Exception as e:
62
+ >>> error_data = to_dict(e, context="workflow_execution")
63
+ >>> # Returns: {
64
+ >>> # "name": "ValueError", "message": "Something went wrong", "context": "workflow_execution"
65
+ >>> # }
66
66
  """
67
67
  return {
68
68
  "name": exception.__class__.__name__,
@@ -85,14 +85,12 @@ class BaseError(Exception):
85
85
  params: Parameter data that was being processed when error occurred
86
86
 
87
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
- ```
88
+ >>> try:
89
+ >>> # NOTE: Some workflow operation
90
+ >>> pass
91
+ >>> except BaseError as e:
92
+ >>> error_dict = e.to_dict(with_refs=True)
93
+ >>> print(f\"Error in {e.refs}: {error_dict}\")
96
94
  """
97
95
 
98
96
  def __init__(
ddeutil/workflow/event.py CHANGED
@@ -19,6 +19,9 @@ Classes:
19
19
  Crontab: Main cron-based event scheduler.
20
20
  CrontabYear: Enhanced cron scheduler with year constraints.
21
21
  ReleaseEvent: Release-based event triggers.
22
+ FileEvent: File system monitoring triggers.
23
+ WebhookEvent: API/webhook-based triggers.
24
+ DatabaseEvent: Database change monitoring triggers.
22
25
  SensorEvent: Sensor-based event monitoring.
23
26
 
24
27
  Example:
@@ -39,7 +42,6 @@ from __future__ import annotations
39
42
  from dataclasses import fields
40
43
  from datetime import datetime
41
44
  from typing import Annotated, Any, Literal, Optional, Union
42
- from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
43
45
 
44
46
  from pydantic import BaseModel, ConfigDict, Field, ValidationInfo
45
47
  from pydantic.functional_serializers import field_serializer
@@ -107,7 +109,7 @@ class BaseCrontab(BaseModel):
107
109
  )
108
110
  tz: TimeZoneName = Field(
109
111
  default="UTC",
110
- description="A timezone string value.",
112
+ description="A timezone string value that will pass to ZoneInfo.",
111
113
  alias="timezone",
112
114
  )
113
115
 
@@ -125,38 +127,25 @@ class BaseCrontab(BaseModel):
125
127
  data["timezone"] = tz
126
128
  return data
127
129
 
128
- @field_validator("tz")
129
- def __validate_tz(cls, value: str) -> str:
130
- """Validate timezone value.
131
-
132
- Args:
133
- value: Timezone string to validate.
134
-
135
- Returns:
136
- Validated timezone string.
137
-
138
- Raises:
139
- ValueError: If timezone is invalid.
140
- """
141
- try:
142
- _ = ZoneInfo(value)
143
- return value
144
- except ZoneInfoNotFoundError as e:
145
- raise ValueError(f"Invalid timezone: {value}") from e
146
-
147
130
 
148
131
  class CrontabValue(BaseCrontab):
149
132
  """Crontab model using interval-based specification.
150
133
 
151
134
  Attributes:
152
- interval: Scheduling interval ('daily', 'weekly', 'monthly').
153
- day: Day specification for weekly/monthly schedules.
135
+ interval: (Interval)
136
+ A scheduling interval string ('daily', 'weekly', 'monthly').
137
+ day: (str, default None)
138
+ Day specification for weekly/monthly schedules.
154
139
  time: Time of day in 'HH:MM' format.
155
140
  """
156
141
 
157
- interval: Interval
142
+ interval: Interval = Field(description="A scheduling interval string.")
158
143
  day: Optional[str] = Field(default=None)
159
- time: str = Field(default="00:00")
144
+ time: str = Field(
145
+ default="00:00",
146
+ pattern=r"\d{2}:\d{2}",
147
+ description="A time of day that pass with format 'HH:MM'.",
148
+ )
160
149
 
161
150
  @property
162
151
  def cronjob(self) -> CronJob:
@@ -182,10 +171,13 @@ class CrontabValue(BaseCrontab):
182
171
  TypeError: If start parameter is neither string nor datetime.
183
172
  """
184
173
  if isinstance(start, str):
185
- start: datetime = datetime.fromisoformat(start)
186
- elif not isinstance(start, datetime):
187
- raise TypeError("start value should be str or datetime type.")
188
- return self.cronjob.schedule(date=start, tz=self.tz)
174
+ return self.cronjob.schedule(
175
+ date=datetime.fromisoformat(start), tz=self.tz
176
+ )
177
+
178
+ if isinstance(start, datetime):
179
+ return self.cronjob.schedule(date=start, tz=self.tz)
180
+ raise TypeError("start value should be str or datetime type.")
189
181
 
190
182
  def next(self, start: Union[str, datetime]) -> CronRunner:
191
183
  """Get next scheduled datetime after given start time.
@@ -222,11 +214,6 @@ class Crontab(BaseCrontab):
222
214
  "A Cronjob object that use for validate and generate datetime."
223
215
  ),
224
216
  )
225
- tz: TimeZoneName = Field(
226
- default="UTC",
227
- description="A timezone string value.",
228
- alias="timezone",
229
- )
230
217
 
231
218
  @model_validator(mode="before")
232
219
  def __prepare_values(cls, data: Any) -> Any:
@@ -328,11 +315,9 @@ class CrontabYear(Crontab):
328
315
  cronjob: CronJobYear instance for year-aware schedule validation and generation.
329
316
  """
330
317
 
331
- cronjob: CronJobYear = (
332
- Field(
333
- description=(
334
- "A Cronjob object that use for validate and generate datetime."
335
- ),
318
+ cronjob: CronJobYear = Field(
319
+ description=(
320
+ "A Cronjob object that use for validate and generate datetime."
336
321
  ),
337
322
  )
338
323
 
@@ -376,13 +361,24 @@ Cron = Annotated[
376
361
  ],
377
362
  Field(
378
363
  union_mode="smart",
379
- description="Event model type supporting year-based, standard, and interval-based cron scheduling.",
364
+ description=(
365
+ "Event model type supporting year-based, standard, and "
366
+ "interval-based cron scheduling."
367
+ ),
380
368
  ),
381
369
  ] # pragma: no cov
382
370
 
383
371
 
384
372
  class Event(BaseModel):
385
- """Event model."""
373
+ """Event model with comprehensive trigger support.
374
+
375
+ Supports multiple types of event triggers including cron scheduling,
376
+ file monitoring, webhooks, database changes, sensor-based triggers,
377
+ polling-based triggers, message queue events, stream processing events,
378
+ batch processing events, data quality events, API rate limiting events,
379
+ data lineage events, ML pipeline events, data catalog events,
380
+ infrastructure events, compliance events, and business events.
381
+ """
386
382
 
387
383
  schedule: list[Cron] = Field(
388
384
  default_factory=list,