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,13 +3,43 @@
3
3
  # Licensed under the MIT License. See LICENSE in the project root for
4
4
  # license information.
5
5
  # ------------------------------------------------------------------------------
6
- """This module include all Param Pydantic Models that use for parsing an
7
- incoming parameters that was passed to the Workflow and Schedule objects before
8
- execution or release methods.
9
-
10
- The Param model allow you to handle validation and preparation steps before
11
- passing an input value to target execution method.
6
+ """Parameter Models for Workflow Validation and Processing.
7
+
8
+ This module provides comprehensive parameter models for handling validation and
9
+ preparation of input values passed to workflows and scheduled executions. The
10
+ parameter system ensures type safety and provides default value management.
11
+
12
+ The parameter models support various data types including strings, numbers,
13
+ dates, choices, and complex types like maps and arrays. Each parameter type
14
+ provides validation and transformation capabilities.
15
+
16
+ Classes:
17
+ BaseParam: Abstract base class for all parameter types
18
+ DefaultParam: Base class for parameters with default values
19
+ DateParam: Date parameter with validation
20
+ DatetimeParam: Datetime parameter with validation
21
+ StrParam: String parameter type
22
+ IntParam: Integer parameter type
23
+ FloatParam: Float parameter with precision control
24
+ DecimalParam: Decimal parameter for financial calculations
25
+ ChoiceParam: Parameter with predefined choices
26
+ MapParam: Dictionary/mapping parameter type
27
+ ArrayParam: List/array parameter type
28
+
29
+ Example:
30
+ ```python
31
+ from ddeutil.workflow.params import StrParam, IntParam
32
+
33
+ # Define parameters
34
+ name_param = StrParam(desc="Username", required=True)
35
+ age_param = IntParam(desc="User age", default=18, required=False)
36
+
37
+ # Process values
38
+ name = name_param.receive("John")
39
+ age = age_param.receive(None) # Uses default value
40
+ ```
12
41
  """
42
+
13
43
  from __future__ import annotations
14
44
 
15
45
  from abc import ABC, abstractmethod
@@ -22,7 +52,7 @@ from pydantic import BaseModel, Field
22
52
 
23
53
  from .__types import StrOrInt
24
54
  from .errors import ParamError
25
- from .utils import get_d_now, get_dt_now
55
+ from .utils import UTC, get_d_now, get_dt_now
26
56
 
27
57
  T = TypeVar("T")
28
58
 
@@ -52,7 +82,7 @@ class BaseParam(BaseModel, ABC):
52
82
  )
53
83
 
54
84
 
55
- class DefaultParam(BaseParam):
85
+ class DefaultParam(BaseParam, ABC):
56
86
  """Default Parameter that will check default if it required. This model do
57
87
  not implement the `receive` method.
58
88
  """
@@ -139,16 +169,18 @@ class DatetimeParam(DefaultParam):
139
169
  return self.default
140
170
 
141
171
  if isinstance(value, datetime):
142
- return value
172
+ if value.tzinfo is None:
173
+ return value.replace(tzinfo=UTC)
174
+ return value.astimezone(UTC)
143
175
  elif isinstance(value, date):
144
- return datetime(value.year, value.month, value.day)
176
+ return datetime(value.year, value.month, value.day, tzinfo=UTC)
145
177
  elif not isinstance(value, str):
146
178
  raise ParamError(
147
179
  f"Value that want to convert to datetime does not support for "
148
180
  f"type: {type(value)}"
149
181
  )
150
182
  try:
151
- return datetime.fromisoformat(value)
183
+ return datetime.fromisoformat(value).replace(tzinfo=UTC)
152
184
  except ValueError:
153
185
  raise ParamError(
154
186
  f"Invalid the ISO format string for datetime: {value!r}"
@@ -3,9 +3,20 @@
3
3
  # Licensed under the MIT License. See LICENSE in the project root for
4
4
  # license information.
5
5
  # ------------------------------------------------------------------------------
6
- """A Result module. It is the data context transfer objects that use by all
7
- object in this package. This module provide Status enum object and Result
8
- dataclass.
6
+ """Result and Status Management Module.
7
+
8
+ This module provides the core result and status management functionality for
9
+ workflow execution tracking. It includes the Status enumeration for execution
10
+ states and the Result dataclass for context transfer between workflow components.
11
+
12
+ Classes:
13
+ Status: Enumeration for execution status tracking
14
+ Result: Dataclass for execution context and result management
15
+
16
+ Functions:
17
+ validate_statuses: Determine final status from multiple status values
18
+ get_status_from_error: Convert exception types to appropriate status
19
+ get_dt_tznow: Get current datetime with timezone configuration
9
20
  """
10
21
  from __future__ import annotations
11
22
 
@@ -13,7 +24,6 @@ from dataclasses import field
13
24
  from datetime import datetime
14
25
  from enum import Enum
15
26
  from typing import Optional, Union
16
- from zoneinfo import ZoneInfo
17
27
 
18
28
  from pydantic import ConfigDict
19
29
  from pydantic.dataclasses import dataclass
@@ -31,23 +41,24 @@ from . import (
31
41
  WorkflowError,
32
42
  )
33
43
  from .__types import DictData
34
- from .audits import TraceModel, get_trace
35
- from .conf import dynamic
44
+ from .audits import Trace, get_trace
36
45
  from .errors import ResultError
37
- from .utils import default_gen_id, gen_id, get_dt_now
38
-
39
-
40
- def get_dt_tznow(tz: Optional[ZoneInfo] = None) -> datetime: # pragma: no cov
41
- """Return the current datetime object that passing the config timezone.
42
-
43
- :rtype: datetime
44
- """
45
- return get_dt_now(tz=dynamic("tz", f=tz))
46
+ from .utils import default_gen_id, get_dt_now
46
47
 
47
48
 
48
49
  class Status(str, Enum):
49
- """Status Int Enum object that use for tracking execution status to the
50
- Result dataclass object.
50
+ """Execution status enumeration for workflow components.
51
+
52
+ Status enum provides standardized status values for tracking the execution
53
+ state of workflows, jobs, and stages. Each status includes an emoji
54
+ representation for visual feedback.
55
+
56
+ Attributes:
57
+ SUCCESS: Successful execution completion
58
+ FAILED: Execution failed with errors
59
+ WAIT: Waiting for execution or dependencies
60
+ SKIP: Execution was skipped due to conditions
61
+ CANCEL: Execution was cancelled
51
62
  """
52
63
 
53
64
  SUCCESS = "SUCCESS"
@@ -58,9 +69,10 @@ class Status(str, Enum):
58
69
 
59
70
  @property
60
71
  def emoji(self) -> str: # pragma: no cov
61
- """Return the emoji value of this status.
72
+ """Get emoji representation of the status.
62
73
 
63
- :rtype: str
74
+ Returns:
75
+ str: Unicode emoji character representing the status
64
76
  """
65
77
  return {
66
78
  "SUCCESS": "✅",
@@ -90,12 +102,28 @@ ResultStatuses: list[Status] = [SUCCESS, FAILED, CANCEL, SKIP]
90
102
 
91
103
 
92
104
  def validate_statuses(statuses: list[Status]) -> Status:
93
- """Validate the final status from list of Status object.
105
+ """Determine final status from multiple status values.
94
106
 
95
- :param statuses: (list[Status]) A list of status that want to validate the
96
- final status.
107
+ Applies workflow logic to determine the overall status based on a collection
108
+ of individual status values. Follows priority order: CANCEL > FAILED > WAIT >
109
+ individual status consistency.
97
110
 
98
- :rtype: Status
111
+ Args:
112
+ statuses: List of status values to evaluate
113
+
114
+ Returns:
115
+ Status: Final consolidated status based on workflow logic
116
+
117
+ Example:
118
+ ```python
119
+ # Mixed statuses - FAILED takes priority
120
+ result = validate_statuses([SUCCESS, FAILED, SUCCESS])
121
+ # Returns: FAILED
122
+
123
+ # All same status
124
+ result = validate_statuses([SUCCESS, SUCCESS, SUCCESS])
125
+ # Returns: SUCCESS
126
+ ```
99
127
  """
100
128
  if any(s == CANCEL for s in statuses):
101
129
  return CANCEL
@@ -123,7 +151,11 @@ def get_status_from_error(
123
151
  BaseException,
124
152
  ]
125
153
  ) -> Status:
126
- """Get the Status from the error object."""
154
+ """Get the Status from the error object.
155
+
156
+ Returns:
157
+ Status: The status from the specific exception class.
158
+ """
127
159
  if isinstance(error, (StageSkipError, JobSkipError)):
128
160
  return SKIP
129
161
  elif isinstance(
@@ -155,49 +187,13 @@ class Result:
155
187
 
156
188
  status: Status = field(default=WAIT)
157
189
  context: DictData = field(default_factory=default_context)
190
+ info: DictData = field(default_factory=dict)
158
191
  run_id: Optional[str] = field(default_factory=default_gen_id)
159
192
  parent_run_id: Optional[str] = field(default=None, compare=False)
160
- ts: datetime = field(default_factory=get_dt_tznow, compare=False)
161
-
162
- trace: Optional[TraceModel] = field(default=None, compare=False, repr=False)
193
+ ts: datetime = field(default_factory=get_dt_now, compare=False)
194
+ trace: Optional[Trace] = field(default=None, compare=False, repr=False)
163
195
  extras: DictData = field(default_factory=dict, compare=False, repr=False)
164
196
 
165
- @classmethod
166
- def construct_with_rs_or_id(
167
- cls,
168
- result: Optional[Result] = None,
169
- run_id: Optional[str] = None,
170
- parent_run_id: Optional[str] = None,
171
- id_logic: Optional[str] = None,
172
- *,
173
- extras: DictData | None = None,
174
- ) -> Self:
175
- """Create the Result object or set parent running id if passing Result
176
- object.
177
-
178
- :param result: A Result instance.
179
- :param run_id: A running ID.
180
- :param parent_run_id: A parent running ID.
181
- :param id_logic: A logic function that use to generate a running ID.
182
- :param extras: An extra parameter that want to override the core config.
183
-
184
- :rtype: Self
185
- """
186
- if result is None:
187
- return cls(
188
- run_id=(run_id or gen_id(id_logic or "", unique=True)),
189
- parent_run_id=parent_run_id,
190
- ts=get_dt_now(dynamic("tz", extras=extras)),
191
- extras=(extras or {}),
192
- )
193
- elif parent_run_id:
194
- result.set_parent_run_id(parent_run_id)
195
-
196
- if extras is not None:
197
- result.extras.update(extras)
198
-
199
- return result
200
-
201
197
  @model_validator(mode="after")
202
198
  def __prepare_trace(self) -> Self:
203
199
  """Prepare trace field that want to pass after its initialize step.
@@ -205,7 +201,7 @@ class Result:
205
201
  :rtype: Self
206
202
  """
207
203
  if self.trace is None: # pragma: no cov
208
- self.trace: TraceModel = get_trace(
204
+ self.trace: Trace = get_trace(
209
205
  self.run_id,
210
206
  parent_run_id=self.parent_run_id,
211
207
  extras=self.extras,
@@ -220,7 +216,7 @@ class Result:
220
216
  :rtype: Self
221
217
  """
222
218
  self.parent_run_id: str = running_id
223
- self.trace: TraceModel = get_trace(
219
+ self.trace: Trace = get_trace(
224
220
  self.run_id, parent_run_id=running_id, extras=self.extras
225
221
  )
226
222
  return self
@@ -240,29 +236,59 @@ class Result:
240
236
 
241
237
  :rtype: Self
242
238
  """
239
+ self.__dict__["context"].update(context or {})
243
240
  self.__dict__["status"] = (
244
241
  Status(status) if isinstance(status, int) else status
245
242
  )
246
- self.__dict__["context"].update(context or {})
247
243
  self.__dict__["context"]["status"] = self.status
244
+
245
+ # NOTE: Update other context data.
248
246
  if kwargs:
249
247
  for k in kwargs:
250
248
  if k in self.__dict__["context"]:
251
249
  self.__dict__["context"][k].update(kwargs[k])
252
250
  # NOTE: Exclude the `info` key for update information data.
253
251
  elif k == "info":
254
- self.__dict__["context"][k].update(kwargs[k])
252
+ self.__dict__["info"].update(kwargs["info"])
255
253
  else:
256
254
  raise ResultError(
257
255
  f"The key {k!r} does not exists on context data."
258
256
  )
259
257
  return self
260
258
 
259
+ def make_info(self, data: DictData) -> Self:
260
+ """Making information."""
261
+ self.__dict__["info"].update(data)
262
+ return self
263
+
261
264
  def alive_time(self) -> float: # pragma: no cov
262
265
  """Return total seconds that this object use since it was created.
263
266
 
264
267
  :rtype: float
265
268
  """
266
- return (
267
- get_dt_now(tz=dynamic("tz", extras=self.extras)) - self.ts
268
- ).total_seconds()
269
+ return (get_dt_now() - self.ts).total_seconds()
270
+
271
+
272
+ def catch(
273
+ context: DictData,
274
+ status: Union[int, Status],
275
+ updated: DictData | None = None,
276
+ **kwargs,
277
+ ) -> DictData:
278
+ """Catch updated context to the current context."""
279
+ context.update(updated or {})
280
+ context["status"] = Status(status) if isinstance(status, int) else status
281
+
282
+ if not kwargs:
283
+ return context
284
+
285
+ # NOTE: Update other context data.
286
+ for k in kwargs:
287
+ if k in context:
288
+ context[k].update(kwargs[k])
289
+ # NOTE: Exclude the `info` key for update information data.
290
+ elif k == "info":
291
+ context["info"].update(kwargs["info"])
292
+ else:
293
+ raise ResultError(f"The key {k!r} does not exists on context data.")
294
+ return context
@@ -3,8 +3,49 @@
3
3
  # Licensed under the MIT License. See LICENSE in the project root for
4
4
  # license information.
5
5
  # ------------------------------------------------------------------------------
6
- # [x] Use dynamic config
7
- """Reusables module that keep any template and template filter functions."""
6
+ """Reusable Components and Registry System.
7
+
8
+ This module provides a registry system for reusable workflow components including
9
+ tagged functions, template operations, and utility functions for parameter
10
+ processing and template rendering.
11
+
12
+ The registry system allows developers to create custom callable functions that
13
+ can be invoked from workflows using the CallStage, enabling extensible and
14
+ modular workflow design.
15
+
16
+ Classes:
17
+ TagFunc: Tagged function wrapper for registry storage
18
+
19
+ Functions:
20
+ tag: Decorator for registering callable functions
21
+ param2template: Convert parameters to template format
22
+ has_template: Check if string contains template variables
23
+ not_in_template: Validate template restrictions
24
+ extract_call: Extract callable information from registry
25
+ create_model_from_caller: Generate Pydantic models from function signatures
26
+
27
+ Example:
28
+ ```python
29
+ from ddeutil.workflow.reusables import tag
30
+
31
+ @tag("data-processing", alias="process-csv")
32
+ def process_csv_file(input_path: str, output_path: str) -> dict:
33
+ # Custom processing logic
34
+ return {"status": "completed", "rows_processed": 1000}
35
+
36
+ # Use in workflow YAML:
37
+ # stages:
38
+ # - name: "Process data"
39
+ # uses: "data-processing/process-csv@latest"
40
+ # args:
41
+ # input_path: "/data/input.csv"
42
+ # output_path: "/data/output.csv"
43
+ ```
44
+
45
+ Note:
46
+ The registry system supports versioning and aliasing for better function
47
+ management and backward compatibility.
48
+ """
8
49
  from __future__ import annotations
9
50
 
10
51
  import copy
@@ -277,6 +318,7 @@ def str2template(
277
318
  value: str,
278
319
  params: DictData,
279
320
  *,
321
+ context: Optional[DictData] = None,
280
322
  filters: Optional[dict[str, FilterRegistry]] = None,
281
323
  registers: Optional[list[str]] = None,
282
324
  ) -> Optional[str]:
@@ -290,6 +332,7 @@ def str2template(
290
332
  :param value: (str) A string value that want to map with params.
291
333
  :param params: (DictData) A parameter value that getting with matched
292
334
  regular expression.
335
+ :param context: (DictData)
293
336
  :param filters: (dict[str, FilterRegistry]) A mapping of filter registry.
294
337
  :param registers: (Optional[list[str]]) Override list of register.
295
338
 
@@ -318,7 +361,7 @@ def str2template(
318
361
  # I recommend to avoid logging params context on this case because it
319
362
  # can include secret value.
320
363
  try:
321
- getter: Any = getdot(caller, params)
364
+ getter: Any = getdot(caller, params | (context or {}))
322
365
  except ValueError:
323
366
  raise UtilError(
324
367
  f"Parameters does not get dot with caller: {caller!r}."
@@ -347,6 +390,7 @@ def str2template(
347
390
  def param2template(
348
391
  value: T,
349
392
  params: DictData,
393
+ context: Optional[DictData] = None,
350
394
  filters: Optional[dict[str, FilterRegistry]] = None,
351
395
  *,
352
396
  extras: Optional[DictData] = None,
@@ -357,6 +401,7 @@ def param2template(
357
401
  :param value: (Any) A value that want to map with params.
358
402
  :param params: (DictData) A parameter value that getting with matched
359
403
  regular expression.
404
+ :param context: (DictData)
360
405
  :param filters: (dict[str, FilterRegistry]) A filter mapping for mapping
361
406
  with `map_post_filter` func.
362
407
  :param extras: (Optional[list[str]]) An Override extras.
@@ -372,16 +417,21 @@ def param2template(
372
417
  )
373
418
  if isinstance(value, dict):
374
419
  return {
375
- k: param2template(value[k], params, filters, extras=extras)
420
+ k: param2template(value[k], params, context, filters, extras=extras)
376
421
  for k in value
377
422
  }
378
423
  elif isinstance(value, (list, tuple, set)):
379
424
  return type(value)(
380
- [param2template(i, params, filters, extras=extras) for i in value]
425
+ [
426
+ param2template(i, params, context, filters, extras=extras)
427
+ for i in value
428
+ ]
381
429
  )
382
430
  elif not isinstance(value, str):
383
431
  return value
384
- return str2template(value, params, filters=filters, registers=registers)
432
+ return str2template(
433
+ value, params, context=context, filters=filters, registers=registers
434
+ )
385
435
 
386
436
 
387
437
  @custom_filter("fmt") # pragma: no cov