ddeutil-workflow 0.0.66__py3-none-any.whl → 0.0.67__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.66"
1
+ __version__: str = "0.0.67"
ddeutil/workflow/cli.py CHANGED
@@ -48,6 +48,7 @@ def api(
48
48
  host: Annotated[str, typer.Option(help="A host url.")] = "0.0.0.0",
49
49
  port: Annotated[int, typer.Option(help="A port url.")] = 80,
50
50
  debug: Annotated[bool, typer.Option(help="A debug mode flag")] = True,
51
+ worker: Annotated[int, typer.Option(help="A worker number")] = None,
51
52
  ):
52
53
  """
53
54
  Provision API application from the FastAPI.
@@ -59,6 +60,7 @@ def api(
59
60
  port=port,
60
61
  log_config=uvicorn.config.LOGGING_CONFIG | LOGGING_CONFIG,
61
62
  log_level=("DEBUG" if debug else "INFO"),
63
+ workers=worker,
62
64
  )
63
65
 
64
66
 
@@ -90,9 +90,6 @@ class ResultError(UtilError): ...
90
90
  class StageError(BaseError): ...
91
91
 
92
92
 
93
- class StageRetryError(StageError): ...
94
-
95
-
96
93
  class StageCancelError(StageError): ...
97
94
 
98
95
 
@@ -11,7 +11,7 @@ from __future__ import annotations
11
11
 
12
12
  from dataclasses import field
13
13
  from datetime import datetime
14
- from enum import IntEnum, auto
14
+ from enum import Enum
15
15
  from typing import Optional, Union
16
16
 
17
17
  from pydantic import ConfigDict
@@ -36,16 +36,16 @@ from .logs import TraceModel, get_dt_tznow, get_trace
36
36
  from .utils import default_gen_id, gen_id, get_dt_now
37
37
 
38
38
 
39
- class Status(IntEnum):
39
+ class Status(str, Enum):
40
40
  """Status Int Enum object that use for tracking execution status to the
41
41
  Result dataclass object.
42
42
  """
43
43
 
44
- SUCCESS = auto()
45
- FAILED = auto()
46
- WAIT = auto()
47
- SKIP = auto()
48
- CANCEL = auto()
44
+ SUCCESS = "SUCCESS"
45
+ FAILED = "FAILED"
46
+ WAIT = "WAIT"
47
+ SKIP = "SKIP"
48
+ CANCEL = "CANCEL"
49
49
 
50
50
  @property
51
51
  def emoji(self) -> str: # pragma: no cov
@@ -67,6 +67,9 @@ class Status(IntEnum):
67
67
  def __str__(self) -> str:
68
68
  return self.name
69
69
 
70
+ def is_result(self) -> bool:
71
+ return self in ResultStatuses
72
+
70
73
 
71
74
  SUCCESS = Status.SUCCESS
72
75
  FAILED = Status.FAILED
@@ -74,6 +77,8 @@ WAIT = Status.WAIT
74
77
  SKIP = Status.SKIP
75
78
  CANCEL = Status.CANCEL
76
79
 
80
+ ResultStatuses: list[Status] = [SUCCESS, FAILED, CANCEL, SKIP]
81
+
77
82
 
78
83
  def validate_statuses(statuses: list[Status]) -> Status:
79
84
  """Validate the final status from list of Status object.
@@ -62,10 +62,9 @@ from pydantic import BaseModel, Field, ValidationError
62
62
  from pydantic.functional_validators import field_validator, model_validator
63
63
  from typing_extensions import Self
64
64
 
65
- from . import StageCancelError, StageRetryError
66
65
  from .__types import DictData, DictStr, StrOrInt, StrOrNone, TupleStr
67
66
  from .conf import dynamic, pass_env
68
- from .errors import StageError, StageSkipError, to_dict
67
+ from .errors import StageCancelError, StageError, StageSkipError, to_dict
69
68
  from .result import (
70
69
  CANCEL,
71
70
  FAILED,
@@ -252,16 +251,20 @@ class BaseStage(BaseModel, ABC):
252
251
  f"[STAGE]: Handler {to_train(self.__class__.__name__)}: "
253
252
  f"{self.name!r}."
254
253
  )
254
+
255
+ # NOTE: Show the description of this stage before execution.
255
256
  if self.desc:
256
257
  result.trace.debug(f"[STAGE]: Description:||{self.desc}||")
257
258
 
259
+ # VALIDATE: Checking stage condition before execution.
258
260
  if self.is_skipped(params):
259
261
  raise StageSkipError(
260
262
  f"Skip because condition {self.condition} was valid."
261
263
  )
264
+
262
265
  # NOTE: Start call wrapped execution method that will use custom
263
266
  # execution before the real execution from inherit stage model.
264
- result_caught: Result = self.__execute(
267
+ result_caught: Result = self._execute(
265
268
  params, result=result, event=event
266
269
  )
267
270
  if result_caught.status == WAIT:
@@ -296,7 +299,7 @@ class BaseStage(BaseModel, ABC):
296
299
  )
297
300
  return result.catch(status=FAILED, context={"errors": to_dict(e)})
298
301
 
299
- def __execute(
302
+ def _execute(
300
303
  self, params: DictData, result: Result, event: Optional[Event]
301
304
  ) -> Result:
302
305
  """Wrapped the execute method before returning to handler execution.
@@ -514,11 +517,14 @@ class BaseAsyncStage(BaseStage, ABC):
514
517
  f"[STAGE]: Handler {to_train(self.__class__.__name__)}: "
515
518
  f"{self.name!r}."
516
519
  )
520
+
521
+ # NOTE: Show the description of this stage before execution.
517
522
  if self.desc:
518
523
  await result.trace.adebug(
519
524
  f"[STAGE]: Description:||{self.desc}||"
520
525
  )
521
526
 
527
+ # VALIDATE: Checking stage condition before execution.
522
528
  if self.is_skipped(params=params):
523
529
  raise StageSkipError(
524
530
  f"Skip because condition {self.condition} was valid."
@@ -526,7 +532,7 @@ class BaseAsyncStage(BaseStage, ABC):
526
532
 
527
533
  # NOTE: Start call wrapped execution method that will use custom
528
534
  # execution before the real execution from inherit stage model.
529
- result_caught: Result = await self.__axecute(
535
+ result_caught: Result = await self._axecute(
530
536
  params, result=result, event=event
531
537
  )
532
538
  if result_caught.status == WAIT:
@@ -561,7 +567,7 @@ class BaseAsyncStage(BaseStage, ABC):
561
567
  )
562
568
  return result.catch(status=FAILED, context={"errors": to_dict(e)})
563
569
 
564
- async def __axecute(
570
+ async def _axecute(
565
571
  self, params: DictData, result: Result, event: Optional[Event]
566
572
  ) -> Result:
567
573
  """Wrapped the axecute method before returning to handler axecute.
@@ -591,7 +597,7 @@ class BaseRetryStage(BaseAsyncStage, ABC): # pragma: no cov
591
597
  description="Retry number if stage execution get the error.",
592
598
  )
593
599
 
594
- def __execute(
600
+ def _execute(
595
601
  self,
596
602
  params: DictData,
597
603
  result: Result,
@@ -610,15 +616,50 @@ class BaseRetryStage(BaseAsyncStage, ABC): # pragma: no cov
610
616
  :rtype: Result
611
617
  """
612
618
  current_retry: int = 0
613
- with current_retry < (self.retry + 1):
619
+ exception: Exception
620
+
621
+ # NOTE: First execution for not pass to retry step if it passes.
622
+ try:
623
+ result.catch(status=WAIT)
624
+ return self.execute(
625
+ params | {"retry": current_retry},
626
+ result=result,
627
+ event=event,
628
+ )
629
+ except Exception as e:
630
+ current_retry += 1
631
+ exception = e
632
+
633
+ if self.retry == 0:
634
+ raise exception
635
+
636
+ result.trace.warning(
637
+ f"[STAGE]: Retry count: {current_retry} ... "
638
+ f"( {exception.__class__.__name__} )"
639
+ )
640
+
641
+ while current_retry < (self.retry + 1):
614
642
  try:
615
643
  result.catch(status=WAIT, context={"retry": current_retry})
616
- return self.execute(params, result=result, event=event)
617
- except StageRetryError:
644
+ return self.execute(
645
+ params | {"retry": current_retry},
646
+ result=result,
647
+ event=event,
648
+ )
649
+ except Exception as e:
618
650
  current_retry += 1
619
- raise StageError(f"Reach the maximum of retry number: {self.retry}.")
651
+ result.trace.warning(
652
+ f"[STAGE]: Retry count: {current_retry} ... "
653
+ f"( {e.__class__.__name__} )"
654
+ )
655
+ exception = e
656
+
657
+ result.trace.error(
658
+ f"[STAGE]: Reach the maximum of retry number: {self.retry}."
659
+ )
660
+ raise exception
620
661
 
621
- async def __axecute(
662
+ async def _axecute(
622
663
  self,
623
664
  params: DictData,
624
665
  result: Result,
@@ -637,13 +678,48 @@ class BaseRetryStage(BaseAsyncStage, ABC): # pragma: no cov
637
678
  :rtype: Result
638
679
  """
639
680
  current_retry: int = 0
640
- with current_retry < (self.retry + 1):
681
+ exception: Exception
682
+
683
+ # NOTE: First execution for not pass to retry step if it passes.
684
+ try:
685
+ result.catch(status=WAIT)
686
+ return await self.axecute(
687
+ params | {"retry": current_retry},
688
+ result=result,
689
+ event=event,
690
+ )
691
+ except Exception as e:
692
+ current_retry += 1
693
+ exception = e
694
+
695
+ if self.retry == 0:
696
+ raise exception
697
+
698
+ await result.trace.awarning(
699
+ f"[STAGE]: Retry count: {current_retry} ... "
700
+ f"( {exception.__class__.__name__} )"
701
+ )
702
+
703
+ while current_retry < (self.retry + 1):
641
704
  try:
642
705
  result.catch(status=WAIT, context={"retry": current_retry})
643
- return await self.axecute(params, result=result, event=event)
644
- except StageRetryError:
706
+ return await self.axecute(
707
+ params | {"retry": current_retry},
708
+ result=result,
709
+ event=event,
710
+ )
711
+ except Exception as e:
645
712
  current_retry += 1
646
- raise StageError(f"Reach the maximum of retry number: {self.retry}.")
713
+ await result.trace.awarning(
714
+ f"[STAGE]: Retry count: {current_retry} ... "
715
+ f"( {e.__class__.__name__} )"
716
+ )
717
+ exception = e
718
+
719
+ await result.trace.aerror(
720
+ f"[STAGE]: Reach the maximum of retry number: {self.retry}."
721
+ )
722
+ raise exception
647
723
 
648
724
 
649
725
  class EmptyStage(BaseAsyncStage):
@@ -765,7 +841,7 @@ class EmptyStage(BaseAsyncStage):
765
841
  return result.catch(status=SUCCESS)
766
842
 
767
843
 
768
- class BashStage(BaseAsyncStage):
844
+ class BashStage(BaseRetryStage):
769
845
  """Bash stage executor that execute bash script on the current OS.
770
846
  If your current OS is Windows, it will run on the bash from the current WSL.
771
847
  It will use `bash` for Windows OS and use `sh` for Linux OS.
@@ -911,9 +987,8 @@ class BashStage(BaseAsyncStage):
911
987
  )
912
988
  if rs.returncode > 0:
913
989
  e: str = rs.stderr.removesuffix("\n")
914
- raise StageError(
915
- f"Subprocess: {e}\n---( statement )---\n```bash\n{bash}\n```"
916
- )
990
+ e_bash: str = bash.replace("\n", "\n\t")
991
+ raise StageError(f"Subprocess: {e}\n\t```bash\n\t{e_bash}\n\t```")
917
992
  return result.catch(
918
993
  status=SUCCESS,
919
994
  context={
@@ -964,9 +1039,8 @@ class BashStage(BaseAsyncStage):
964
1039
 
965
1040
  if rs.returncode > 0:
966
1041
  e: str = rs.stderr.removesuffix("\n")
967
- raise StageError(
968
- f"Subprocess: {e}\n---( statement )---\n```bash\n{bash}\n```"
969
- )
1042
+ e_bash: str = bash.replace("\n", "\n\t")
1043
+ raise StageError(f"Subprocess: {e}\n\t```bash\n\t{e_bash}\n\t```")
970
1044
  return result.catch(
971
1045
  status=SUCCESS,
972
1046
  context={
@@ -977,7 +1051,7 @@ class BashStage(BaseAsyncStage):
977
1051
  )
978
1052
 
979
1053
 
980
- class PyStage(BaseAsyncStage):
1054
+ class PyStage(BaseRetryStage):
981
1055
  """Python stage that running the Python statement with the current globals
982
1056
  and passing an input additional variables via `exec` built-in function.
983
1057
 
@@ -1164,7 +1238,7 @@ class PyStage(BaseAsyncStage):
1164
1238
  )
1165
1239
 
1166
1240
 
1167
- class CallStage(BaseAsyncStage):
1241
+ class CallStage(BaseRetryStage):
1168
1242
  """Call stage executor that call the Python function from registry with tag
1169
1243
  decorator function in `reusables` module and run it with input arguments.
1170
1244
 
@@ -1433,7 +1507,7 @@ class CallStage(BaseAsyncStage):
1433
1507
  return args
1434
1508
 
1435
1509
 
1436
- class BaseNestedStage(BaseStage, ABC):
1510
+ class BaseNestedStage(BaseRetryStage, ABC):
1437
1511
  """Base Nested Stage model. This model is use for checking the child stage
1438
1512
  is the nested stage or not.
1439
1513
  """
@@ -1467,6 +1541,17 @@ class BaseNestedStage(BaseStage, ABC):
1467
1541
  else:
1468
1542
  context["errors"] = error.to_dict(with_refs=True)
1469
1543
 
1544
+ async def axecute(
1545
+ self,
1546
+ params: DictData,
1547
+ *,
1548
+ result: Optional[Result] = None,
1549
+ event: Optional[Event] = None,
1550
+ ) -> Result:
1551
+ raise NotImplementedError(
1552
+ "The nested-stage does not implement the `axecute` method yet."
1553
+ )
1554
+
1470
1555
 
1471
1556
  class TriggerStage(BaseNestedStage):
1472
1557
  """Trigger workflow executor stage that run an input trigger Workflow
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ddeutil-workflow
3
- Version: 0.0.66
4
- Summary: Lightweight workflow orchestration
3
+ Version: 0.0.67
4
+ Summary: Lightweight workflow orchestration with YAML template
5
5
  Author-email: ddeutils <korawich.anu@gmail.com>
6
6
  License: MIT
7
7
  Project-URL: Homepage, https://github.com/ddeutils/ddeutil-workflow/
@@ -35,18 +35,9 @@ Requires-Dist: httpx; extra == "all"
35
35
  Requires-Dist: ujson; extra == "all"
36
36
  Requires-Dist: aiofiles; extra == "all"
37
37
  Requires-Dist: aiohttp; extra == "all"
38
- Provides-Extra: api
39
- Requires-Dist: fastapi<1.0.0,>=0.115.0; extra == "api"
40
- Requires-Dist: uvicorn; extra == "api"
41
- Requires-Dist: httpx; extra == "api"
42
- Requires-Dist: ujson; extra == "api"
43
- Provides-Extra: async
44
- Requires-Dist: aiofiles; extra == "async"
45
- Requires-Dist: aiohttp; extra == "async"
38
+ Requires-Dist: requests==2.32.3; extra == "all"
46
39
  Provides-Extra: docker
47
40
  Requires-Dist: docker==7.1.0; extra == "docker"
48
- Provides-Extra: self-hosted
49
- Requires-Dist: requests==2.32.3; extra == "self-hosted"
50
41
  Dynamic: license-file
51
42
 
52
43
  # Workflow Orchestration
@@ -142,10 +133,10 @@ the base deps.
142
133
  If you want to install this package with application add-ons, you should add
143
134
  `app` in installation;
144
135
 
145
- | Use-case | Install Optional | Support |
146
- |----------------|--------------------------|:-------------------:|
147
- | Python | `ddeutil-workflow` | :heavy_check_mark: |
148
- | FastAPI Server | `ddeutil-workflow[api]` | :heavy_check_mark: |
136
+ | Use-case | Install Optional | Support |
137
+ |----------------|-------------------------|:-------:|
138
+ | Python | `ddeutil-workflow` ||
139
+ | FastAPI Server | `ddeutil-workflow[all]` | ✅ |
149
140
 
150
141
  ## 🎯 Usage
151
142
 
@@ -300,40 +291,27 @@ it will use default value and do not raise any error to you.
300
291
  ## :rocket: Deployment
301
292
 
302
293
  This package able to run as an application service for receive manual trigger
303
- from any node via RestAPI or use to be Scheduler background application
304
- like crontab job but via Python API or FastAPI app.
294
+ from any node via RestAPI with the FastAPI package.
305
295
 
306
296
  ### API Server
307
297
 
308
298
  This server use FastAPI package to be the base application.
309
299
 
310
300
  ```shell
311
- (.venv) $ uvicorn ddeutil.workflow.api:app \
312
- --host 127.0.0.1 \
313
- --port 80 \
314
- --no-access-log
301
+ (.venv) $ workflow-cli api --host 127.0.0.1 --port 80
315
302
  ```
316
303
 
317
304
  > [!NOTE]
318
305
  > If this package already deploy, it is able to use multiprocess;
319
- > `uvicorn ddeutil.workflow.api:app --host 127.0.0.1 --port 80 --workers 4`
320
-
321
- ### Local Schedule
322
-
323
- > [!WARNING]
324
- > This CLI does not implement yet.
325
-
326
- ```shell
327
- (.venv) $ ddeutil-workflow schedule
328
- ```
306
+ > `$ workflow-cli api --host 127.0.0.1 --port 80 --workers 4`
329
307
 
330
308
  ### Docker Container
331
309
 
332
310
  Build a Docker container from this package.
333
311
 
334
312
  ```shell
335
- $ docker build -t ddeutil-workflow:latest -f .container/Dockerfile .
336
- $ docker run -i ddeutil-workflow:latest ddeutil-workflow
313
+ $ docker pull ghcr.io/ddeutils/ddeutil-workflow:latest
314
+ $ docker run --rm ghcr.io/ddeutils/ddeutil-workflow:latest ddeutil-worker
337
315
  ```
338
316
 
339
317
  ## :speech_balloon: Contribute
@@ -1,18 +1,18 @@
1
- ddeutil/workflow/__about__.py,sha256=nkpvuhIACvuey4B8paloyuDA6U0uRDZqZ83SUGdlIt4,28
1
+ ddeutil/workflow/__about__.py,sha256=JZ9Er-4hkPGd0SSb_wI8VFJvPCjm8q09g7oG_MshBMo,28
2
2
  ddeutil/workflow/__cron.py,sha256=BOKQcreiex0SAigrK1gnLxpvOeF3aca_rQwyz9Kfve4,28751
3
3
  ddeutil/workflow/__init__.py,sha256=JfFZlPRDgR2J0rb0SRejt1OSrOrD3GGv9Um14z8MMfs,901
4
4
  ddeutil/workflow/__main__.py,sha256=Qd-f8z2Q2vpiEP2x6PBFsJrpACWDVxFKQk820MhFmHo,59
5
5
  ddeutil/workflow/__types.py,sha256=uNfoRbVmNK5O37UUMVnqcmoghD9oMS1q9fXC0APnjSI,4584
6
- ddeutil/workflow/cli.py,sha256=__ydzTs_e9C1tHqhnyp__0iKelZJRUwZn8VsyVYw5yo,1458
6
+ ddeutil/workflow/cli.py,sha256=YtfNfozYRvQyohhYVcZ2_8o_IBXOpmok531eYw0DScM,1555
7
7
  ddeutil/workflow/conf.py,sha256=w1WDWZDCvRVDSz2HnJxeqySzpYWSubJZjTVjXO9imK0,14669
8
- ddeutil/workflow/errors.py,sha256=vh_AbLegPbb61TvfwHmjSz0t2SH09RQuGHCKmoyNjn0,2934
8
+ ddeutil/workflow/errors.py,sha256=4DaKnyUm8RrUyQA5qakgW0ycSQLO7j-owyoh79LWQ5c,2893
9
9
  ddeutil/workflow/event.py,sha256=S2eJAZZx_V5TuQ0l417hFVCtjWXnfNPZBgSCICzxQ48,11041
10
10
  ddeutil/workflow/job.py,sha256=qcbKSOa39256nfJHL0vKJsHrelcRujX5KET2IEGS8dw,38995
11
11
  ddeutil/workflow/logs.py,sha256=4rL8TsRJsYVqyPfLjFW5bSoWtRwUgwmaRONu7nnVxQ8,31374
12
12
  ddeutil/workflow/params.py,sha256=Pco3DyjptC5Jkx53dhLL9xlIQdJvNAZs4FLzMUfXpbQ,12402
13
- ddeutil/workflow/result.py,sha256=CzSjK9EQtSS0a1z8b6oBW4gg8t0uVbZ_Ppx4Ckvcork,7786
13
+ ddeutil/workflow/result.py,sha256=GU84psZFiJ4LRf_HXgz-R98YN4lOUkER0VR7x9DDdOU,7922
14
14
  ddeutil/workflow/reusables.py,sha256=jPrOCbxagqRvRFGXJzIyDa1wKV5AZ4crZyJ10cldQP0,21620
15
- ddeutil/workflow/stages.py,sha256=s6Jc4A6ApXT40JMrnVf7n7yHG_mmjfHJIOU_niMW5hI,102555
15
+ ddeutil/workflow/stages.py,sha256=xsJactN-Qk5Yg7ooXfoq-JVdlduIAdXXJUzCKFJuWGA,105093
16
16
  ddeutil/workflow/utils.py,sha256=slhBbsBNl0yaSk9EOiCK6UL-o7smgHVsLT7svRqAWXU,10436
17
17
  ddeutil/workflow/workflow.py,sha256=AcSGqsH1N4LqWhYIcCPy9CoV_AGlXUrBgjpl-gniv6g,28267
18
18
  ddeutil/workflow/api/__init__.py,sha256=0UIilYwW29RL6HrCRHACSWvnATJVLSJzXiCMny0bHQk,2627
@@ -21,9 +21,9 @@ ddeutil/workflow/api/routes/__init__.py,sha256=jC1pM7q4_eo45IyO3hQbbe6RnL9B8ibRq
21
21
  ddeutil/workflow/api/routes/job.py,sha256=32TkNm7QY9gt6fxIqEPjDqPgc8XqDiMPjUb7disSrCw,2143
22
22
  ddeutil/workflow/api/routes/logs.py,sha256=QJH8IF102897WLfCJ29-1g15wl29M9Yq6omroZfbahs,5305
23
23
  ddeutil/workflow/api/routes/workflows.py,sha256=Gmg3e-K5rfi95pbRtWI_aIr5C089sIde_vefZVvh3U0,4420
24
- ddeutil_workflow-0.0.66.dist-info/licenses/LICENSE,sha256=nGFZ1QEhhhWeMHf9n99_fdt4vQaXS29xWKxt-OcLywk,1085
25
- ddeutil_workflow-0.0.66.dist-info/METADATA,sha256=LhCFhlwk3N5wbnylmi7zi3sAJqwxkRVlrRThlNB4Cj0,16676
26
- ddeutil_workflow-0.0.66.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
27
- ddeutil_workflow-0.0.66.dist-info/entry_points.txt,sha256=qDTpPSauL0ciO6T4iSVt8bJeYrVEkkoEEw_RlGx6Kgk,63
28
- ddeutil_workflow-0.0.66.dist-info/top_level.txt,sha256=m9M6XeSWDwt_yMsmH6gcOjHZVK5O0-vgtNBuncHjzW4,8
29
- ddeutil_workflow-0.0.66.dist-info/RECORD,,
24
+ ddeutil_workflow-0.0.67.dist-info/licenses/LICENSE,sha256=nGFZ1QEhhhWeMHf9n99_fdt4vQaXS29xWKxt-OcLywk,1085
25
+ ddeutil_workflow-0.0.67.dist-info/METADATA,sha256=w9iP1ofTfKIdirH9WSZf5rMOA4MrqMKM5jJk1hFO3oU,16072
26
+ ddeutil_workflow-0.0.67.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
27
+ ddeutil_workflow-0.0.67.dist-info/entry_points.txt,sha256=qDTpPSauL0ciO6T4iSVt8bJeYrVEkkoEEw_RlGx6Kgk,63
28
+ ddeutil_workflow-0.0.67.dist-info/top_level.txt,sha256=m9M6XeSWDwt_yMsmH6gcOjHZVK5O0-vgtNBuncHjzW4,8
29
+ ddeutil_workflow-0.0.67.dist-info/RECORD,,