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.
@@ -20,7 +20,6 @@ Functions:
20
20
  from __future__ import annotations
21
21
 
22
22
  from dataclasses import field
23
- from datetime import datetime
24
23
  from enum import Enum
25
24
  from typing import Optional, Union
26
25
 
@@ -40,9 +39,9 @@ from . import (
40
39
  WorkflowError,
41
40
  )
42
41
  from .__types import DictData
43
- from .audits import Trace, get_trace
42
+ from .audits import TraceManager, get_trace
44
43
  from .errors import ResultError
45
- from .utils import default_gen_id, get_dt_now
44
+ from .utils import default_gen_id
46
45
 
47
46
 
48
47
  class Status(str, Enum):
@@ -105,8 +104,9 @@ def validate_statuses(statuses: list[Status]) -> Status:
105
104
  """Determine final status from multiple status values.
106
105
 
107
106
  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.
107
+ of individual status values. Follows priority order:
108
+
109
+ CANCEL > FAILED > WAIT > individual status consistency.
110
110
 
111
111
  Args:
112
112
  statuses: List of status values to evaluate
@@ -132,7 +132,7 @@ def validate_statuses(statuses: list[Status]) -> Status:
132
132
  for status in (SUCCESS, SKIP):
133
133
  if all(s == status for s in statuses):
134
134
  return status
135
- return FAILED if FAILED in statuses else SUCCESS
135
+ return SUCCESS
136
136
 
137
137
 
138
138
  def get_status_from_error(
@@ -166,10 +166,6 @@ def get_status_from_error(
166
166
  return FAILED
167
167
 
168
168
 
169
- def default_context() -> DictData:
170
- return {"status": WAIT}
171
-
172
-
173
169
  @dataclass(
174
170
  config=ConfigDict(arbitrary_types_allowed=True, use_enum_values=True),
175
171
  )
@@ -186,14 +182,15 @@ class Result:
186
182
  field that keep dict value change its ID when update new value to it.
187
183
  """
188
184
 
185
+ extras: DictData = field(default_factory=dict, compare=False, repr=False)
189
186
  status: Status = field(default=WAIT)
190
- context: DictData = field(default_factory=default_context)
187
+ context: Optional[DictData] = field(default=None)
191
188
  info: DictData = field(default_factory=dict)
192
- run_id: Optional[str] = field(default_factory=default_gen_id)
189
+ run_id: str = field(default_factory=default_gen_id)
193
190
  parent_run_id: Optional[str] = field(default=None)
194
- ts: datetime = field(default_factory=get_dt_now, compare=False)
195
- trace: Optional[Trace] = field(default=None, compare=False, repr=False)
196
- extras: DictData = field(default_factory=dict, compare=False, repr=False)
191
+ trace: Optional[TraceManager] = field(
192
+ default=None, compare=False, repr=False
193
+ )
197
194
 
198
195
  @model_validator(mode="after")
199
196
  def __prepare_trace(self) -> Self:
@@ -202,25 +199,23 @@ class Result:
202
199
  :rtype: Self
203
200
  """
204
201
  if self.trace is None: # pragma: no cov
205
- self.trace: Trace = get_trace(
202
+ self.trace: TraceManager = get_trace(
206
203
  self.run_id,
207
204
  parent_run_id=self.parent_run_id,
208
205
  extras=self.extras,
209
206
  )
210
- return self
211
-
212
- def set_parent_run_id(self, running_id: str) -> Self:
213
- """Set a parent running ID.
214
207
 
215
- :param running_id: (str) A running ID that want to update on this model.
208
+ return self
216
209
 
217
- :rtype: Self
218
- """
219
- self.parent_run_id: str = running_id
220
- self.trace: Trace = get_trace(
221
- self.run_id, parent_run_id=running_id, extras=self.extras
210
+ @classmethod
211
+ def from_trace(cls, trace: TraceManager):
212
+ """Construct the result model from trace for clean code objective."""
213
+ return cls(
214
+ run_id=trace.run_id,
215
+ parent_run_id=trace.parent_run_id,
216
+ extras=trace.extras,
217
+ trace=trace,
222
218
  )
223
- return self
224
219
 
225
220
  def catch(
226
221
  self,
@@ -237,7 +232,11 @@ class Result:
237
232
 
238
233
  :rtype: Self
239
234
  """
240
- self.__dict__["context"].update(context or {})
235
+ if self.__dict__["context"] is None:
236
+ self.__dict__["context"] = context
237
+ else:
238
+ self.__dict__["context"].update(context or {})
239
+
241
240
  self.__dict__["status"] = (
242
241
  Status(status) if isinstance(status, int) else status
243
242
  )
@@ -262,13 +261,6 @@ class Result:
262
261
  self.__dict__["info"].update(data)
263
262
  return self
264
263
 
265
- def alive_time(self) -> float: # pragma: no cov
266
- """Return total seconds that this object use since it was created.
267
-
268
- :rtype: float
269
- """
270
- return (get_dt_now() - self.ts).total_seconds()
271
-
272
264
 
273
265
  def catch(
274
266
  context: DictData,
@@ -276,7 +268,13 @@ def catch(
276
268
  updated: DictData | None = None,
277
269
  **kwargs,
278
270
  ) -> DictData:
279
- """Catch updated context to the current context."""
271
+ """Catch updated context to the current context.
272
+
273
+ Args:
274
+ context: A context data that want to be the current context.
275
+ status: A status enum object.
276
+ updated: A updated data that will update to the current context.
277
+ """
280
278
  context.update(updated or {})
281
279
  context["status"] = Status(status) if isinstance(status, int) else status
282
280
 
@@ -289,7 +287,7 @@ def catch(
289
287
  context[k].update(kwargs[k])
290
288
  # NOTE: Exclude the `info` key for update information data.
291
289
  elif k == "info":
292
- context["info"].update(kwargs["info"])
290
+ context.update({"info": kwargs["info"]})
293
291
  else:
294
292
  raise ResultError(f"The key {k!r} does not exists on context data.")
295
293
  return context
@@ -119,12 +119,15 @@ FilterRegistry = Union[FilterFunc, Callable[[...], Any]]
119
119
 
120
120
 
121
121
  def custom_filter(name: str) -> Callable[P, FilterFunc]:
122
- """Custom filter decorator function that set function attributes, ``filter``
123
- for making filter registries variable.
122
+ """Custom filter decorator function that sets function attributes.
124
123
 
125
- :param: name: (str) A filter name for make different use-case of a function.
124
+ This decorator sets the `filter` attribute for making filter registries variable.
126
125
 
127
- :rtype: Callable[P, FilterFunc]
126
+ Args:
127
+ name: A filter name for different use-cases of a function.
128
+
129
+ Returns:
130
+ Callable[P, FilterFunc]: Decorated function with filter attributes.
128
131
  """
129
132
 
130
133
  def func_internal(func: Callable[[...], Any]) -> FilterFunc:
@@ -144,11 +147,13 @@ def custom_filter(name: str) -> Callable[P, FilterFunc]:
144
147
  def make_filter_registry(
145
148
  registers: Optional[list[str]] = None,
146
149
  ) -> dict[str, FilterRegistry]:
147
- """Return registries of all functions that able to called with task.
150
+ """Return registries of all functions that can be called with tasks.
148
151
 
149
- :param registers: (Optional[list[str]]) Override list of register.
152
+ Args:
153
+ registers: Optional override list of registers.
150
154
 
151
- :rtype: dict[str, FilterRegistry]
155
+ Returns:
156
+ dict[str, FilterRegistry]: Dictionary mapping filter names to functions.
152
157
  """
153
158
  rs: dict[str, FilterRegistry] = {}
154
159
  for module in dynamic("registry_filter", f=registers):
@@ -180,9 +185,14 @@ def get_args_const(
180
185
  ) -> tuple[str, list[Constant], dict[str, Constant]]:
181
186
  """Get arguments and keyword-arguments from function calling string.
182
187
 
183
- :param expr: (str) An expr string value.
188
+ Args:
189
+ expr: A string expression representing a function call.
190
+
191
+ Returns:
192
+ tuple[str, list[Constant], dict[str, Constant]]: Function name, args, and kwargs.
184
193
 
185
- :rtype: tuple[str, list[Constant], dict[str, Constant]]
194
+ Raises:
195
+ UtilError: If the expression has syntax errors or invalid format.
186
196
  """
187
197
  try:
188
198
  mod: Module = parse(expr)
@@ -223,12 +233,18 @@ def get_args_from_filter(
223
233
  filters: dict[str, FilterRegistry],
224
234
  ) -> tuple[str, FilterRegistry, list[Any], dict[Any, Any]]: # pragma: no cov
225
235
  """Get arguments and keyword-arguments from filter function calling string.
226
- and validate it with the filter functions mapping dict.
227
236
 
228
- :param ft:
229
- :param filters: A mapping of filter registry.
237
+ This function validates the filter function call with the filter functions mapping dict.
238
+
239
+ Args:
240
+ ft: Filter function calling string.
241
+ filters: A mapping of filter registry.
230
242
 
231
- :rtype: tuple[str, FilterRegistry, list[Any], dict[Any, Any]]
243
+ Returns:
244
+ tuple[str, FilterRegistry, list[Any], dict[Any, Any]]: Function name, function, args, and kwargs.
245
+
246
+ Raises:
247
+ UtilError: If the filter function is not supported or has invalid arguments.
232
248
  """
233
249
  func_name, _args, _kwargs = get_args_const(ft)
234
250
  args: list[Any] = [arg.value for arg in _args]
@@ -250,14 +266,18 @@ def map_post_filter(
250
266
  post_filter: list[str],
251
267
  filters: dict[str, FilterRegistry],
252
268
  ) -> T:
253
- """Mapping post-filter to value with sequence list of filter function name
254
- that will get from the filter registry.
269
+ """Map post-filter functions to value with sequence of filter function names.
270
+
271
+ Args:
272
+ value: A value to map with filter functions.
273
+ post_filter: A list of post-filter function names.
274
+ filters: A mapping of filter registry.
255
275
 
256
- :param value: A string value that want to map with filter function.
257
- :param post_filter: A list of post-filter function name.
258
- :param filters: A mapping of filter registry.
276
+ Returns:
277
+ T: The value after applying all post-filter functions.
259
278
 
260
- :rtype: T
279
+ Raises:
280
+ UtilError: If a post-filter function fails or is incompatible with the value.
261
281
  """
262
282
  for ft in post_filter:
263
283
  func_name, f_func, args, kwargs = get_args_from_filter(ft, filters)
@@ -278,13 +298,15 @@ def map_post_filter(
278
298
 
279
299
 
280
300
  def not_in_template(value: Any, *, not_in: str = "matrix.") -> bool:
281
- """Check value should not pass template with not_in value prefix.
301
+ """Check if value should not pass template with not_in value prefix.
282
302
 
283
- :param value: A value that want to find parameter template prefix.
284
- :param not_in: The not-in string that use in the `.startswith` function.
285
- (Default is `matrix.`)
303
+ Args:
304
+ value: A value to check for parameter template prefix.
305
+ not_in: The not-in string to use in the `.startswith` function.
306
+ Default is `matrix.`.
286
307
 
287
- :rtype: bool
308
+ Returns:
309
+ bool: True if value should not pass template, False otherwise.
288
310
  """
289
311
  if isinstance(value, dict):
290
312
  return any(not_in_template(value[k], not_in=not_in) for k in value)
@@ -299,11 +321,13 @@ def not_in_template(value: Any, *, not_in: str = "matrix.") -> bool:
299
321
 
300
322
 
301
323
  def has_template(value: Any) -> bool:
302
- """Check value include templating string.
324
+ """Check if value includes templating string.
303
325
 
304
- :param value: A value that want to find parameter template.
326
+ Args:
327
+ value: A value to check for parameter template.
305
328
 
306
- :rtype: bool
329
+ Returns:
330
+ bool: True if value contains template variables, False otherwise.
307
331
  """
308
332
  if isinstance(value, dict):
309
333
  return any(has_template(value[k]) for k in value)
@@ -322,21 +346,24 @@ def str2template(
322
346
  filters: Optional[dict[str, FilterRegistry]] = None,
323
347
  registers: Optional[list[str]] = None,
324
348
  ) -> Optional[str]:
325
- """(Sub-function) Pass param to template string that can search by
326
- ``RE_CALLER`` regular expression.
349
+ """Pass parameters to template string using RE_CALLER regular expression.
350
+
351
+ This is a sub-function that processes template strings. The getter value
352
+ that maps a template should have typing support aligned with workflow
353
+ parameter types: `str`, `int`, `datetime`, and `list`.
327
354
 
328
- The getter value that map a template should have typing support align
329
- with the workflow parameter types that is `str`, `int`, `datetime`, and
330
- `list`.
355
+ Args:
356
+ value: A string value to map with parameters.
357
+ params: Parameter values to get with matched regular expression.
358
+ context: Optional context data.
359
+ filters: Optional mapping of filter registry.
360
+ registers: Optional override list of registers.
331
361
 
332
- :param value: (str) A string value that want to map with params.
333
- :param params: (DictData) A parameter value that getting with matched
334
- regular expression.
335
- :param context: (DictData)
336
- :param filters: (dict[str, FilterRegistry]) A mapping of filter registry.
337
- :param registers: (Optional[list[str]]) Override list of register.
362
+ Returns:
363
+ Optional[str]: The processed template string or None if value is "None".
338
364
 
339
- :rtype: str
365
+ Raises:
366
+ UtilError: If parameters cannot be retrieved or template processing fails.
340
367
  """
341
368
  filters: dict[str, FilterRegistry] = filters or make_filter_registry(
342
369
  registers=registers
@@ -395,19 +422,17 @@ def param2template(
395
422
  *,
396
423
  extras: Optional[DictData] = None,
397
424
  ) -> Any:
398
- """Pass param to template string that can search by ``RE_CALLER`` regular
399
- expression.
400
-
401
- :param value: (Any) A value that want to map with params.
402
- :param params: (DictData) A parameter value that getting with matched
403
- regular expression.
404
- :param context: (DictData)
405
- :param filters: (dict[str, FilterRegistry]) A filter mapping for mapping
406
- with `map_post_filter` func.
407
- :param extras: (Optional[list[str]]) An Override extras.
408
-
409
- :rtype: Any
410
- :returns: An any getter value from the params input.
425
+ """Pass parameters to template string using RE_CALLER regular expression.
426
+
427
+ Args:
428
+ value: A value to map with parameters.
429
+ params: Parameter values to get with matched regular expression.
430
+ context: Optional context data.
431
+ filters: Optional filter mapping for use with `map_post_filter` function.
432
+ extras: Optional override extras.
433
+
434
+ Returns:
435
+ Any: The processed value with template variables replaced.
411
436
  """
412
437
  registers: Optional[list[str]] = (
413
438
  extras.get("registry_filter") if extras else None
@@ -437,19 +462,21 @@ def param2template(
437
462
 
438
463
  @custom_filter("fmt") # pragma: no cov
439
464
  def datetime_format(value: datetime, fmt: str = "%Y-%m-%d %H:%M:%S") -> str:
440
- """Format datetime object to string with the format.
465
+ """Format datetime object to string with the specified format.
441
466
 
442
467
  Examples:
443
-
444
468
  >>> "${{ start-date | fmt('%Y%m%d') }}"
445
469
  >>> "${{ start-date | fmt }}"
446
470
 
447
- :param value: (datetime) A datetime value that want to format to string
448
- value.
449
- :param fmt: (str) A format string pattern that passing to the `dt.strftime`
450
- method.
471
+ Args:
472
+ value: A datetime value to format to string.
473
+ fmt: A format string pattern to pass to the `dt.strftime` method.
451
474
 
452
- :rtype: str
475
+ Returns:
476
+ str: The formatted datetime string.
477
+
478
+ Raises:
479
+ UtilError: If the input value is not a datetime object.
453
480
  """
454
481
  if isinstance(value, datetime):
455
482
  return value.strftime(fmt)
@@ -463,12 +490,14 @@ def coalesce(value: Optional[T], default: Any) -> T:
463
490
  """Coalesce with default value if the main value is None.
464
491
 
465
492
  Examples:
466
-
467
493
  >>> "${{ value | coalesce('foo') }}"
468
494
 
469
- :param value: A value that want to check nullable.
470
- :param default: A default value that use to returned value if an input
471
- value was null.
495
+ Args:
496
+ value: A value to check for null.
497
+ default: A default value to return if the input value is null.
498
+
499
+ Returns:
500
+ T: The original value if not None, otherwise the default value.
472
501
  """
473
502
  return default if value is None else value
474
503
 
@@ -477,13 +506,22 @@ def coalesce(value: Optional[T], default: Any) -> T:
477
506
  def get_item(
478
507
  value: DictData, key: Union[str, int], default: Optional[Any] = None
479
508
  ) -> Any:
480
- """Get a value with an input specific key.
509
+ """Get a value with a specific key.
481
510
 
482
511
  Examples:
483
-
484
512
  >>> "${{ value | getitem('key') }}"
485
513
  >>> "${{ value | getitem('key', 'default') }}"
486
514
 
515
+ Args:
516
+ value: A dictionary to get the value from.
517
+ key: The key to look up in the dictionary.
518
+ default: Optional default value if key is not found.
519
+
520
+ Returns:
521
+ Any: The value associated with the key, or the default value.
522
+
523
+ Raises:
524
+ UtilError: If the value is not a dictionary.
487
525
  """
488
526
  if not isinstance(value, dict):
489
527
  raise UtilError(
@@ -495,12 +533,20 @@ def get_item(
495
533
 
496
534
  @custom_filter("getindex") # pragma: no cov
497
535
  def get_index(value: list[Any], index: int) -> Any:
498
- """Get a value with an input specific index.
536
+ """Get a value with a specific index.
499
537
 
500
538
  Examples:
501
-
502
539
  >>> "${{ value | getindex(1) }}"
503
540
 
541
+ Args:
542
+ value: A list to get the value from.
543
+ index: The index to access in the list.
544
+
545
+ Returns:
546
+ Any: The value at the specified index.
547
+
548
+ Raises:
549
+ UtilError: If the value is not a list or if the index is out of range.
504
550
  """
505
551
  if not isinstance(value, list):
506
552
  raise UtilError(
@@ -534,16 +580,18 @@ def tag(
534
580
  name: Optional[str] = None,
535
581
  alias: Optional[str] = None,
536
582
  ) -> DecoratorTagFunc: # pragma: no cov
537
- """Tag decorator function that set function attributes, ``tag`` and ``name``
538
- for making registries variable.
583
+ """Tag decorator function that sets function attributes for registry.
539
584
 
540
- :param: name: (str) A tag name for make different use-case of a function.
541
- It will use 'latest' if this tag name does not set.
542
- :param: alias: (str) A alias function name that keeping in registries.
543
- If this value does not supply, it will use original function name
544
- from `__name__` argument.
585
+ This decorator sets the `tag` and `name` attributes for making registries variable.
545
586
 
546
- :rtype: Callable[P, TagFunc]
587
+ Args:
588
+ name: A tag name for different use-cases of a function.
589
+ Uses 'latest' if not set.
590
+ alias: An alias function name to keep in registries.
591
+ Uses original function name from `__name__` if not supplied.
592
+
593
+ Returns:
594
+ DecoratorTagFunc: Decorated function with tag attributes.
547
595
  """
548
596
 
549
597
  def func_internal(func: Callable[[...], Any]) -> ReturnTagFunc:
@@ -574,12 +622,17 @@ def make_registry(
574
622
  *,
575
623
  registries: Optional[list[str]] = None,
576
624
  ) -> dict[str, Registry]:
577
- """Return registries of all functions that able to called with task.
625
+ """Return registries of all functions that can be called with tasks.
626
+
627
+ Args:
628
+ submodule: A module prefix to import registry from.
629
+ registries: Optional list of registries.
578
630
 
579
- :param submodule: (str) A module prefix that want to import registry.
580
- :param registries: (Optional[list[str]]) A list of registry.
631
+ Returns:
632
+ dict[str, Registry]: Dictionary mapping function names to their registries.
581
633
 
582
- :rtype: dict[str, Registry]
634
+ Raises:
635
+ ValueError: If a tag already exists for a function name.
583
636
  """
584
637
  rs: dict[str, Registry] = {}
585
638
  regis_calls: list[str] = copy.deepcopy(
@@ -599,7 +652,7 @@ def make_registry(
599
652
  if not (
600
653
  hasattr(func, "tag")
601
654
  and hasattr(func, "name")
602
- and str(getattr(func, "mark", "NOT SET")) == "tag"
655
+ and str(getattr(func, "mark", "NOTSET")) == "tag"
603
656
  ): # pragma: no cov
604
657
  continue
605
658
 
@@ -617,6 +670,7 @@ def make_registry(
617
670
  f"{module}.{submodule}, you should change this tag name or "
618
671
  f"change it func name."
619
672
  )
673
+
620
674
  rs[func.name][func.tag] = lazy(f"{module}.{submodule}.{fstr}")
621
675
 
622
676
  return rs
@@ -638,13 +692,9 @@ def extract_call(
638
692
  *,
639
693
  registries: Optional[list[str]] = None,
640
694
  ) -> Callable[[], TagFunc]:
641
- """Extract Call function from string value to call partial function that
642
- does run it at runtime.
695
+ """Extract call function from string value to call partial function at runtime.
643
696
 
644
- :param call: (str) A call value that able to match with Task regex.
645
- :param registries: (Optional[list[str]]) A list of registry.
646
-
647
- The format of call value should contain 3 regular expression groups
697
+ The format of call value should contain 3 regular expression groups
648
698
  which match with the below config format:
649
699
 
650
700
  >>> "^(?P<path>[^/@]+)/(?P<func>[^@]+)@(?P<tag>.+)$"
@@ -655,12 +705,16 @@ def extract_call(
655
705
  >>> extract_call("tasks/return-type-not-valid@raise")
656
706
  ...
657
707
 
658
- :raise NotImplementedError: When the searching call's function result does
659
- not exist in the registry.
660
- :raise NotImplementedError: When the searching call's tag result does not
661
- exist in the registry with its function key.
708
+ Args:
709
+ call: A call value that matches the Task regex.
710
+ registries: Optional list of registries.
711
+
712
+ Returns:
713
+ Callable[[], TagFunc]: A callable function that can be executed.
662
714
 
663
- :rtype: Callable[[], TagFunc]
715
+ Raises:
716
+ ValueError: If the call does not match the regex format.
717
+ NotImplementedError: If the function or tag does not exist in the registry.
664
718
  """
665
719
  if not (found := Re.RE_TASK_FMT.search(call)):
666
720
  raise ValueError(
@@ -696,17 +750,20 @@ class BaseCallerArgs(BaseModel): # pragma: no cov
696
750
 
697
751
 
698
752
  def create_model_from_caller(func: Callable) -> BaseModel: # pragma: no cov
699
- """Create model from the caller function. This function will use for
700
- validate the caller function argument typed-hint that valid with the args
701
- field.
753
+ """Create model from the caller function.
754
+
755
+ This function is used to validate the caller function argument type hints
756
+ that are valid with the args field.
702
757
 
703
758
  Reference:
704
759
  - https://github.com/lmmx/pydantic-function-models
705
760
  - https://docs.pydantic.dev/1.10/usage/models/#dynamic-model-creation
706
761
 
707
- :param func: (Callable) A caller function.
762
+ Args:
763
+ func: A caller function to create a model from.
708
764
 
709
- :rtype: BaseModel
765
+ Returns:
766
+ BaseModel: A Pydantic model created from the function signature.
710
767
  """
711
768
  sig: inspect.Signature = inspect.signature(func)
712
769
  type_hints: dict[str, Any] = get_type_hints(func)