ddeutil-workflow 0.0.14__py3-none-any.whl → 0.0.15__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.14"
1
+ __version__: str = "0.0.15"
@@ -6,14 +6,19 @@
6
6
  from __future__ import annotations
7
7
 
8
8
  import re
9
+ from collections.abc import Iterator
10
+ from dataclasses import dataclass
9
11
  from re import (
10
12
  IGNORECASE,
11
13
  MULTILINE,
12
14
  UNICODE,
13
15
  VERBOSE,
16
+ Match,
14
17
  Pattern,
15
18
  )
16
- from typing import Any, Union
19
+ from typing import Any, Optional, Union
20
+
21
+ from typing_extensions import Self
17
22
 
18
23
  TupleStr = tuple[str, ...]
19
24
  DictData = dict[str, Any]
@@ -23,6 +28,27 @@ MatrixInclude = list[dict[str, Union[str, int]]]
23
28
  MatrixExclude = list[dict[str, Union[str, int]]]
24
29
 
25
30
 
31
+ @dataclass(frozen=True)
32
+ class CallerRe:
33
+ """Caller dataclass that catching result from the matching regex with the
34
+ Re.RE_CALLER value.
35
+ """
36
+
37
+ full: str
38
+ caller: str
39
+ caller_prefix: Optional[str]
40
+ caller_last: str
41
+ post_filters: str
42
+
43
+ @classmethod
44
+ def from_regex(cls, match: Match[str]) -> Self:
45
+ """Class construct from matching result.
46
+
47
+ :rtype: Self
48
+ """
49
+ return cls(full=match.group(0), **match.groupdict())
50
+
51
+
26
52
  class Re:
27
53
  """Regular expression config for this package."""
28
54
 
@@ -31,10 +57,10 @@ class Re:
31
57
  # - Version 1:
32
58
  # \${{\s*(?P<caller>[a-zA-Z0-9_.\s'\"\[\]\(\)\-\{}]+?)\s*(?P<post_filters>(?:\|\s*(?:[a-zA-Z0-9_]{3,}[a-zA-Z0-9_.,-\\%\s'\"[\]()\{}]+)\s*)*)}}
33
59
  # - Version 2 (2024-09-30):
34
- # \${{\s*(?P<caller>(?P<caller_prefix>[a-zA-Z_-]+\.)*(?P<caller_last>[a-zA-Z0-9_\-.'\"(\)[\]{}]+))\s*(?P<post_filters>(?:\|\s*(?:[a-zA-Z0-9_]{3,}[a-zA-Z0-9_.,-\\%\s'\"[\]()\{}]+)\s*)*)}}
60
+ # \${{\s*(?P<caller>(?P<caller_prefix>(?:[a-zA-Z_-]+\.)*)(?P<caller_last>[a-zA-Z0-9_\-.'\"(\)[\]{}]+))\s*(?P<post_filters>(?:\|\s*(?:[a-zA-Z0-9_]{3,}[a-zA-Z0-9_.,-\\%\s'\"[\]()\{}]+)\s*)*)}}
35
61
  #
36
62
  # Examples:
37
- # - ${{ params.asat_dt }}
63
+ # - ${{ params.data_dt }}
38
64
  # - ${{ params.source.table }}
39
65
  #
40
66
  __re_caller: str = r"""
@@ -42,16 +68,17 @@ class Re:
42
68
  {{
43
69
  \s*
44
70
  (?P<caller>
45
- (?P<caller_prefix>[a-zA-Z_-]+\.)*
71
+ (?P<caller_prefix>(?:[a-zA-Z_-]+\.)*)
46
72
  (?P<caller_last>[a-zA-Z0-9_\-.'\"(\)[\]{}]+)
47
73
  )
48
74
  \s*
49
75
  (?P<post_filters>
50
76
  (?:
51
- \|
52
- \s*
53
- (?:[a-zA-Z0-9_]{3,}[a-zA-Z0-9_.,-\\%\s'\"[\]()\{}]*)
54
- \s*
77
+ \|\s*
78
+ (?:
79
+ [a-zA-Z0-9_]{3,}
80
+ [a-zA-Z0-9_.,-\\%\s'\"[\]()\{}]*
81
+ )\s*
55
82
  )*
56
83
  )
57
84
  }}
@@ -62,7 +89,8 @@ class Re:
62
89
 
63
90
  # NOTE:
64
91
  # Regular expression:
65
- # ^(?P<path>[^/@]+)/(?P<func>[^@]+)@(?P<tag>.+)$
92
+ # - Version 1:
93
+ # ^(?P<path>[^/@]+)/(?P<func>[^@]+)@(?P<tag>.+)$
66
94
  #
67
95
  # Examples:
68
96
  # - tasks/function@dummy
@@ -78,3 +106,8 @@ class Re:
78
106
  RE_TASK_FMT: Pattern = re.compile(
79
107
  __re_task_fmt, MULTILINE | IGNORECASE | UNICODE | VERBOSE
80
108
  )
109
+
110
+ @classmethod
111
+ def finditer_caller(cls, value) -> Iterator[CallerRe]:
112
+ for found in cls.RE_CALLER.finditer(value):
113
+ yield CallerRe.from_regex(found)
ddeutil/workflow/conf.py CHANGED
@@ -16,6 +16,10 @@ env = os.getenv
16
16
 
17
17
 
18
18
  class Config:
19
+ """Config object for keeping application configuration on current session
20
+ without changing when if the application still running.
21
+ """
22
+
19
23
  # NOTE: Core
20
24
  tz: ZoneInfo = ZoneInfo(env("WORKFLOW_CORE_TIMEZONE", "UTC"))
21
25
 
ddeutil/workflow/job.py CHANGED
@@ -320,13 +320,15 @@ class Job(BaseModel):
320
320
  and want to set on the `to` like;
321
321
 
322
322
  ... (i) output: {'strategy01': bar, 'strategy02': bar}
323
- ... (ii) to: {}
323
+ ... (ii) to: {'jobs'}
324
324
 
325
325
  The result of the `to` variable will be;
326
326
 
327
327
  ... (iii) to: {
328
- 'strategies': {
329
- 'strategy01': bar, 'strategy02': bar
328
+ 'jobs': {
329
+ 'strategies': {
330
+ 'strategy01': bar, 'strategy02': bar
331
+ }
330
332
  }
331
333
  }
332
334
 
@@ -339,7 +341,7 @@ class Job(BaseModel):
339
341
  "This job do not set the ID before setting output."
340
342
  )
341
343
 
342
- to[self.id] = (
344
+ to["jobs"][self.id] = (
343
345
  {"strategies": output}
344
346
  if self.strategy.is_set()
345
347
  # NOTE:
@@ -513,7 +513,6 @@ class Workflow(BaseModel):
513
513
  f"workflow."
514
514
  )
515
515
 
516
- context: DictData = {}
517
516
  logger.info(f"({self.run_id}) [WORKFLOW]: Start execute: {job_id!r}")
518
517
 
519
518
  # IMPORTANT:
@@ -523,7 +522,7 @@ class Workflow(BaseModel):
523
522
  job: Job = self.jobs[job_id].get_running_id(self.run_id)
524
523
  job.set_outputs(
525
524
  job.execute(params=params).context,
526
- to=context,
525
+ to=params,
527
526
  )
528
527
  except JobException as err:
529
528
  logger.error(
@@ -536,7 +535,7 @@ class Workflow(BaseModel):
536
535
  else:
537
536
  raise NotImplementedError() from None
538
537
 
539
- return Result(status=0, context=context)
538
+ return Result(status=0, context=params)
540
539
 
541
540
  def execute(
542
541
  self,
@@ -587,8 +586,14 @@ class Workflow(BaseModel):
587
586
  for job_id in self.jobs:
588
587
  jq.put(job_id)
589
588
 
590
- # NOTE: Create result context that will pass this context to any
591
- # execution dependency.
589
+ # NOTE: Create data context that will pass to any job executions
590
+ # on this workflow.
591
+ #
592
+ # {
593
+ # 'params': <input-params>,
594
+ # 'jobs': {},
595
+ # }
596
+ #
592
597
  context: DictData = self.parameterize(params)
593
598
  status: int = 0
594
599
  try:
@@ -657,15 +662,23 @@ class Workflow(BaseModel):
657
662
  job: Job = self.jobs[job_id]
658
663
 
659
664
  if any(need not in context["jobs"] for need in job.needs):
665
+ job_queue.task_done()
660
666
  job_queue.put(job_id)
661
667
  time.sleep(0.25)
662
668
  continue
663
669
 
670
+ # NOTE: Start workflow job execution with deep copy context data
671
+ # before release.
672
+ #
673
+ # {
674
+ # 'params': <input-params>,
675
+ # 'jobs': {},
676
+ # }
664
677
  futures.append(
665
678
  executor.submit(
666
679
  self.execute_job,
667
680
  job_id,
668
- params=copy.deepcopy(context),
681
+ params=context,
669
682
  ),
670
683
  )
671
684
 
@@ -677,14 +690,13 @@ class Workflow(BaseModel):
677
690
 
678
691
  for future in as_completed(futures, timeout=1800):
679
692
  if err := future.exception():
680
- logger.error(f"{err}")
693
+ logger.error(f"({self.run_id}) [CORE]: {err}")
681
694
  raise WorkflowException(f"{err}")
682
695
  try:
683
- # NOTE: Update job result to workflow result.
684
- context["jobs"].update(future.result(timeout=60).context)
696
+ future.result(timeout=60)
685
697
  except TimeoutError as err:
686
698
  raise WorkflowException(
687
- "Get result from future was timeout"
699
+ "Timeout when getting result from future"
688
700
  ) from err
689
701
 
690
702
  if not_time_out_flag:
@@ -731,18 +743,21 @@ class Workflow(BaseModel):
731
743
  job_id: str = job_queue.get()
732
744
  job: Job = self.jobs[job_id]
733
745
 
734
- # NOTE:
746
+ # NOTE: Waiting dependency job run successful before release.
735
747
  if any(need not in context["jobs"] for need in job.needs):
748
+ job_queue.task_done()
736
749
  job_queue.put(job_id)
737
- time.sleep(0.25)
750
+ time.sleep(0.05)
738
751
  continue
739
752
 
740
- # NOTE: Start workflow job execution.
741
- job_rs = self.execute_job(
742
- job_id=job_id,
743
- params=copy.deepcopy(context),
744
- )
745
- context["jobs"].update(job_rs.context)
753
+ # NOTE: Start workflow job execution with deep copy context data
754
+ # before release.
755
+ #
756
+ # {
757
+ # 'params': <input-params>,
758
+ # 'jobs': {},
759
+ # }
760
+ self.execute_job(job_id=job_id, params=context)
746
761
 
747
762
  # NOTE: Mark this job queue done.
748
763
  job_queue.task_done()
ddeutil/workflow/stage.py CHANGED
@@ -24,6 +24,7 @@ import contextlib
24
24
  import inspect
25
25
  import subprocess
26
26
  import sys
27
+ import time
27
28
  import uuid
28
29
  from abc import ABC, abstractmethod
29
30
  from collections.abc import Iterator
@@ -60,6 +61,8 @@ from .utils import (
60
61
  )
61
62
 
62
63
  P = ParamSpec("P")
64
+ ReturnResult = Callable[P, Result]
65
+ DecoratorResult = Callable[[ReturnResult], ReturnResult]
63
66
  logger = get_logger("ddeutil.workflow")
64
67
 
65
68
 
@@ -77,7 +80,7 @@ __all__: TupleStr = (
77
80
  )
78
81
 
79
82
 
80
- def handler_result(message: str | None = None) -> Callable[P, Result]:
83
+ def handler_result(message: str | None = None) -> DecoratorResult:
81
84
  """Decorator function for handler result from the stage execution. This
82
85
  function should to use with execution method only.
83
86
 
@@ -103,7 +106,7 @@ def handler_result(message: str | None = None) -> Callable[P, Result]:
103
106
  #
104
107
  message: str = message or ""
105
108
 
106
- def decorator(func: Callable[P, Result]) -> Callable[P, Result]:
109
+ def decorator(func: ReturnResult) -> ReturnResult:
107
110
 
108
111
  @wraps(func)
109
112
  def wrapped(self: Stage, *args, **kwargs):
@@ -293,6 +296,10 @@ class EmptyStage(BaseStage):
293
296
  default=None,
294
297
  description="A string statement that want to logging",
295
298
  )
299
+ sleep: float = Field(
300
+ default=0,
301
+ description="A second value to sleep before finish execution",
302
+ )
296
303
 
297
304
  def execute(self, params: DictData) -> Result:
298
305
  """Execution method for the Empty stage that do only logging out to
@@ -310,6 +317,8 @@ class EmptyStage(BaseStage):
310
317
  f"({self.run_id}) [STAGE]: Empty-Execute: {self.name!r}: "
311
318
  f"( {param2template(self.echo, params=params) or '...'} )"
312
319
  )
320
+ if self.sleep > 0:
321
+ time.sleep(self.sleep)
313
322
  return Result(status=0, context={})
314
323
 
315
324
 
ddeutil/workflow/utils.py CHANGED
@@ -206,6 +206,12 @@ class SimLoad:
206
206
  """Find all data that match with object type in config path. This class
207
207
  method can use include and exclude list of identity name for filter and
208
208
  adds-on.
209
+
210
+ :param obj:
211
+ :param params:
212
+ :param include:
213
+ :param exclude:
214
+ :rtype: Iterator[tuple[str, DictData]]
209
215
  """
210
216
  exclude: list[str] = exclude or []
211
217
  for file in PathSearch(params.engine.paths.conf).files:
@@ -317,10 +323,14 @@ class TagFunc(Protocol):
317
323
  name: str
318
324
  tag: str
319
325
 
320
- def __call__(self, *args, **kwargs): ...
326
+ def __call__(self, *args, **kwargs): ... # pragma: no cove
327
+
328
+
329
+ ReturnTagFunc = Callable[P, TagFunc]
330
+ DecoratorTagFunc = Callable[[Callable[[...], Any]], ReturnTagFunc]
321
331
 
322
332
 
323
- def tag(name: str, alias: str | None = None) -> Callable[P, TagFunc]:
333
+ def tag(name: str, alias: str | None = None) -> DecoratorTagFunc:
324
334
  """Tag decorator function that set function attributes, ``tag`` and ``name``
325
335
  for making registries variable.
326
336
 
@@ -330,7 +340,7 @@ def tag(name: str, alias: str | None = None) -> Callable[P, TagFunc]:
330
340
  :rtype: Callable[P, TagFunc]
331
341
  """
332
342
 
333
- def func_internal(func: Callable[[...], Any]) -> TagFunc:
343
+ def func_internal(func: Callable[[...], Any]) -> ReturnTagFunc:
334
344
  func.tag = name
335
345
  func.name = alias or func.__name__.replace("_", "-")
336
346
 
@@ -398,6 +408,10 @@ class BaseParam(BaseModel, ABC):
398
408
 
399
409
  @field_serializer("type")
400
410
  def __serializer_type(self, value: str) -> str:
411
+ """Serialize the value of the type field.
412
+
413
+ :rtype: str
414
+ """
401
415
  return value
402
416
 
403
417
 
@@ -793,8 +807,8 @@ def not_in_template(value: Any, *, not_in: str = "matrix.") -> bool:
793
807
  elif not isinstance(value, str):
794
808
  return False
795
809
  return any(
796
- (not found.group("caller").strip().startswith(not_in))
797
- for found in Re.RE_CALLER.finditer(value.strip())
810
+ (not found.caller.strip().startswith(not_in))
811
+ for found in Re.finditer_caller(value.strip())
798
812
  )
799
813
 
800
814
 
@@ -835,18 +849,16 @@ def str2template(
835
849
 
836
850
  # NOTE: remove space before and after this string value.
837
851
  value: str = value.strip()
838
- for found in Re.RE_CALLER.finditer(value):
852
+ for found in Re.finditer_caller(value):
839
853
  # NOTE:
840
854
  # Get caller and filter values that setting inside;
841
855
  #
842
856
  # ... ``${{ <caller-value> [ | <filter-value>] ... }}``
843
857
  #
844
- caller: str = found.group("caller")
858
+ caller: str = found.caller
845
859
  pfilter: list[str] = [
846
860
  i.strip()
847
- for i in (
848
- found.group("post_filters").strip().removeprefix("|").split("|")
849
- )
861
+ for i in (found.post_filters.strip().removeprefix("|").split("|"))
850
862
  if i != ""
851
863
  ]
852
864
  if not hasdot(caller, params):
@@ -859,7 +871,7 @@ def str2template(
859
871
  # If type of getter caller is not string type and it does not use to
860
872
  # concat other string value, it will return origin value from the
861
873
  # ``getdot`` function.
862
- if value.replace(found.group(0), "", 1) == "":
874
+ if value.replace(found.full, "", 1) == "":
863
875
  return map_post_filter(getter, pfilter, filters=filters)
864
876
 
865
877
  # NOTE: map post-filter function.
@@ -867,7 +879,7 @@ def str2template(
867
879
  if not isinstance(getter, str):
868
880
  getter: str = str(getter)
869
881
 
870
- value: str = value.replace(found.group(0), getter, 1)
882
+ value: str = value.replace(found.full, getter, 1)
871
883
 
872
884
  return search_env_replace(value)
873
885
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ddeutil-workflow
3
- Version: 0.0.14
3
+ Version: 0.0.15
4
4
  Summary: Lightweight workflow orchestration with less dependencies
5
5
  Author-email: ddeutils <korawich.anu@gmail.com>
6
6
  License: MIT
@@ -0,0 +1,22 @@
1
+ ddeutil/workflow/__about__.py,sha256=w_vBOopUg1crMbDyfdE0LgsxsncnhGYp0D39LSSnSVI,28
2
+ ddeutil/workflow/__init__.py,sha256=-DIy8SGFsD7_wqp-V-K8v8jTxacmqrcyj_SFx1WS6qg,687
3
+ ddeutil/workflow/__types.py,sha256=WWugALcayRiP0IQO-eBWK767_XxK7KGlY7SuVgyaJnk,3196
4
+ ddeutil/workflow/api.py,sha256=cwju_qhY6m0kLtaoa77QLglC9tl7RjjZ4UnJYV3SlQQ,4810
5
+ ddeutil/workflow/cli.py,sha256=Ikcq526WeIl-737-v55T0PwAZ2pNiZFxlN0Y-DjhDbQ,3374
6
+ ddeutil/workflow/conf.py,sha256=D0g7rHXilpGwOD36QwVd9I5kEwqsAUA0Z3tAINS2Pws,1287
7
+ ddeutil/workflow/cron.py,sha256=naWefHc3EnVo41Yf1zQeXOzF27YlTlnfj0XnQ6_HO-U,25514
8
+ ddeutil/workflow/exceptions.py,sha256=Uf1-Tn8rAzj0aiVHSqo4fBqO80W0za7UFZgKv24E-tg,706
9
+ ddeutil/workflow/job.py,sha256=9H_2C0ikD5y6jLVdIBj8de4CdSpS632XOfqYVhM4bHI,21582
10
+ ddeutil/workflow/log.py,sha256=Ev-Szi0KC_MmbFY4g4BWv6tUSmcLKWKZ03ZInmYPmgU,6490
11
+ ddeutil/workflow/on.py,sha256=vsZG19mNoztDSB_ObD_4ZWPKgHYpBDJMWw97ZiTavNE,7237
12
+ ddeutil/workflow/repeat.py,sha256=e3dekPTlMlxCCizfBYsZ8dD8Juy4rtfqDZJU3Iky2oA,5011
13
+ ddeutil/workflow/route.py,sha256=ABEk-WlVo9XGFc7zCPbckX33URCNH7woQFU1keX_8PQ,6970
14
+ ddeutil/workflow/scheduler.py,sha256=12Dd5CVphOVKjUwoiB8dCHt4WpYRPG3dSOt-pR6NNxc,46167
15
+ ddeutil/workflow/stage.py,sha256=Avz1Mbb8WAP6kFn0bnN0p14-EnQ_AzdKr435JRxjkao,23844
16
+ ddeutil/workflow/utils.py,sha256=XUD5hoygAyxi4xo1spTacoDNGKN2TlRob_o8qfCj4Pc,30993
17
+ ddeutil_workflow-0.0.15.dist-info/LICENSE,sha256=nGFZ1QEhhhWeMHf9n99_fdt4vQaXS29xWKxt-OcLywk,1085
18
+ ddeutil_workflow-0.0.15.dist-info/METADATA,sha256=6JvY9y-cT3WnirRva45NS582Iz7ZuXJZpsiCtN57OoA,11653
19
+ ddeutil_workflow-0.0.15.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
20
+ ddeutil_workflow-0.0.15.dist-info/entry_points.txt,sha256=0BVOgO3LdUdXVZ-CiHHDKxzEk2c8J30jEwHeKn2YCWI,62
21
+ ddeutil_workflow-0.0.15.dist-info/top_level.txt,sha256=m9M6XeSWDwt_yMsmH6gcOjHZVK5O0-vgtNBuncHjzW4,8
22
+ ddeutil_workflow-0.0.15.dist-info/RECORD,,
@@ -1,22 +0,0 @@
1
- ddeutil/workflow/__about__.py,sha256=Xas_M3BaGwGfZOJTvMWUlmlW09aGC-Apst-NtkPddY4,28
2
- ddeutil/workflow/__init__.py,sha256=-DIy8SGFsD7_wqp-V-K8v8jTxacmqrcyj_SFx1WS6qg,687
3
- ddeutil/workflow/__types.py,sha256=aBbytylSPIe_cip2KIyqLN2eUloMOJdkayqKWCBrwhk,2353
4
- ddeutil/workflow/api.py,sha256=cwju_qhY6m0kLtaoa77QLglC9tl7RjjZ4UnJYV3SlQQ,4810
5
- ddeutil/workflow/cli.py,sha256=Ikcq526WeIl-737-v55T0PwAZ2pNiZFxlN0Y-DjhDbQ,3374
6
- ddeutil/workflow/conf.py,sha256=j19G7rDxQRGgSRQW3pxIYtK5lB3fZv0eG_CAoqoIhPw,1140
7
- ddeutil/workflow/cron.py,sha256=naWefHc3EnVo41Yf1zQeXOzF27YlTlnfj0XnQ6_HO-U,25514
8
- ddeutil/workflow/exceptions.py,sha256=Uf1-Tn8rAzj0aiVHSqo4fBqO80W0za7UFZgKv24E-tg,706
9
- ddeutil/workflow/job.py,sha256=zEefiEAxyC34NvbNVpKexTVU1E_031446308zGMdcmE,21488
10
- ddeutil/workflow/log.py,sha256=Ev-Szi0KC_MmbFY4g4BWv6tUSmcLKWKZ03ZInmYPmgU,6490
11
- ddeutil/workflow/on.py,sha256=vsZG19mNoztDSB_ObD_4ZWPKgHYpBDJMWw97ZiTavNE,7237
12
- ddeutil/workflow/repeat.py,sha256=e3dekPTlMlxCCizfBYsZ8dD8Juy4rtfqDZJU3Iky2oA,5011
13
- ddeutil/workflow/route.py,sha256=ABEk-WlVo9XGFc7zCPbckX33URCNH7woQFU1keX_8PQ,6970
14
- ddeutil/workflow/scheduler.py,sha256=CcUFichnvPbQzSEk_ikNgFwZimTObGHfXxHChuysAo4,45706
15
- ddeutil/workflow/stage.py,sha256=Tt5QQrO_dN8MO9gPtiziOqVrd64UTJZwbgifWeXBCIA,23574
16
- ddeutil/workflow/utils.py,sha256=epJMTsA4BPQa0gECgcWJ38IENlejpnF3OTBNc0eaqYE,30715
17
- ddeutil_workflow-0.0.14.dist-info/LICENSE,sha256=nGFZ1QEhhhWeMHf9n99_fdt4vQaXS29xWKxt-OcLywk,1085
18
- ddeutil_workflow-0.0.14.dist-info/METADATA,sha256=sf9kiPoGazaRxqymTjaxZyH47yNyGj0RElc-NLmUa4w,11653
19
- ddeutil_workflow-0.0.14.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
20
- ddeutil_workflow-0.0.14.dist-info/entry_points.txt,sha256=0BVOgO3LdUdXVZ-CiHHDKxzEk2c8J30jEwHeKn2YCWI,62
21
- ddeutil_workflow-0.0.14.dist-info/top_level.txt,sha256=m9M6XeSWDwt_yMsmH6gcOjHZVK5O0-vgtNBuncHjzW4,8
22
- ddeutil_workflow-0.0.14.dist-info/RECORD,,