ddeutil-workflow 0.0.26.post0__py3-none-any.whl → 0.0.26.post1__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/__about__.py +1 -1
- ddeutil/workflow/__init__.py +5 -2
- ddeutil/workflow/conf.py +144 -97
- ddeutil/workflow/scheduler.py +2 -2
- ddeutil/workflow/workflow.py +4 -4
- {ddeutil_workflow-0.0.26.post0.dist-info → ddeutil_workflow-0.0.26.post1.dist-info}/METADATA +1 -1
- {ddeutil_workflow-0.0.26.post0.dist-info → ddeutil_workflow-0.0.26.post1.dist-info}/RECORD +10 -10
- {ddeutil_workflow-0.0.26.post0.dist-info → ddeutil_workflow-0.0.26.post1.dist-info}/LICENSE +0 -0
- {ddeutil_workflow-0.0.26.post0.dist-info → ddeutil_workflow-0.0.26.post1.dist-info}/WHEEL +0 -0
- {ddeutil_workflow-0.0.26.post0.dist-info → ddeutil_workflow-0.0.26.post1.dist-info}/top_level.txt +0 -0
ddeutil/workflow/__about__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__: str = "0.0.26.
|
1
|
+
__version__: str = "0.0.26.post1"
|
ddeutil/workflow/__init__.py
CHANGED
@@ -3,11 +3,14 @@
|
|
3
3
|
# Licensed under the MIT License. See LICENSE in the project root for
|
4
4
|
# license information.
|
5
5
|
# ------------------------------------------------------------------------------
|
6
|
-
from .__cron import CronRunner
|
6
|
+
from .__cron import CronJob, CronRunner
|
7
7
|
from .conf import (
|
8
8
|
Config,
|
9
|
-
FileLog,
|
10
9
|
Loader,
|
10
|
+
Log,
|
11
|
+
env,
|
12
|
+
get_log,
|
13
|
+
get_logger,
|
11
14
|
)
|
12
15
|
from .cron import (
|
13
16
|
On,
|
ddeutil/workflow/conf.py
CHANGED
@@ -13,27 +13,30 @@ from collections.abc import Iterator
|
|
13
13
|
from datetime import datetime, timedelta
|
14
14
|
from functools import cached_property, lru_cache
|
15
15
|
from pathlib import Path
|
16
|
-
from typing import ClassVar, Optional,
|
16
|
+
from typing import ClassVar, Optional, Union
|
17
17
|
from zoneinfo import ZoneInfo
|
18
18
|
|
19
19
|
from ddeutil.core import str2bool
|
20
20
|
from ddeutil.io import YamlFlResolve
|
21
|
-
from dotenv import load_dotenv
|
22
21
|
from pydantic import BaseModel, Field
|
23
22
|
from pydantic.functional_validators import model_validator
|
24
23
|
from typing_extensions import Self
|
25
24
|
|
26
25
|
from .__types import DictData, TupleStr
|
27
26
|
|
28
|
-
AnyModel = TypeVar("AnyModel", bound=BaseModel)
|
29
|
-
AnyModelType = type[AnyModel]
|
30
27
|
|
31
|
-
|
28
|
+
def env(var: str, default: str | None = None) -> str | None: # pragma: no cov
|
29
|
+
return os.getenv(f"WORKFLOW_{var}", default)
|
30
|
+
|
31
|
+
|
32
|
+
def glob_files(path: Path) -> Iterator[Path]: # pragma: no cov
|
33
|
+
yield from (file for file in path.rglob("*") if file.is_file())
|
32
34
|
|
33
|
-
env = os.getenv
|
34
35
|
|
35
36
|
__all__: TupleStr = (
|
37
|
+
"env",
|
36
38
|
"get_logger",
|
39
|
+
"get_log",
|
37
40
|
"Config",
|
38
41
|
"SimLoad",
|
39
42
|
"Loader",
|
@@ -52,6 +55,14 @@ def get_logger(name: str):
|
|
52
55
|
:param name: A module name that want to log.
|
53
56
|
"""
|
54
57
|
lg = logging.getLogger(name)
|
58
|
+
|
59
|
+
# NOTE: Developers using this package can then disable all logging just for
|
60
|
+
# this package by;
|
61
|
+
#
|
62
|
+
# `logging.getLogger('ddeutil.workflow').propagate = False`
|
63
|
+
#
|
64
|
+
lg.addHandler(logging.NullHandler())
|
65
|
+
|
55
66
|
formatter = logging.Formatter(
|
56
67
|
fmt=(
|
57
68
|
"%(asctime)s.%(msecs)03d (%(name)-10s, %(process)-5d, "
|
@@ -68,115 +79,141 @@ def get_logger(name: str):
|
|
68
79
|
return lg
|
69
80
|
|
70
81
|
|
71
|
-
class Config:
|
82
|
+
class Config: # pragma: no cov
|
72
83
|
"""Config object for keeping application configuration on current session
|
73
84
|
without changing when if the application still running.
|
74
85
|
"""
|
75
86
|
|
76
87
|
# NOTE: Core
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
88
|
+
@property
|
89
|
+
def root_path(self) -> Path:
|
90
|
+
return Path(env("ROOT_PATH", "."))
|
91
|
+
|
92
|
+
@property
|
93
|
+
def conf_path(self) -> Path:
|
94
|
+
"""Config path that use root_path class argument for this construction.
|
95
|
+
|
96
|
+
:rtype: Path
|
97
|
+
"""
|
98
|
+
return self.root_path / env("CORE_PATH_CONF", "conf")
|
99
|
+
|
100
|
+
@property
|
101
|
+
def tz(self) -> ZoneInfo:
|
102
|
+
return ZoneInfo(env("CORE_TIMEZONE", "UTC"))
|
103
|
+
|
104
|
+
@property
|
105
|
+
def gen_id_simple_mode(self) -> bool:
|
106
|
+
return str2bool(env("CORE_GENERATE_ID_SIMPLE_MODE", "true"))
|
82
107
|
|
83
108
|
# NOTE: Register
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
109
|
+
@property
|
110
|
+
def regis_hook(self) -> list[str]:
|
111
|
+
regis_hook_str: str = env(
|
112
|
+
"CORE_REGISTRY", "src,src.ddeutil.workflow,tests,tests.utils"
|
113
|
+
)
|
114
|
+
return [r.strip() for r in regis_hook_str.split(",")]
|
115
|
+
|
116
|
+
@property
|
117
|
+
def regis_filter(self) -> list[str]:
|
118
|
+
regis_filter_str: str = env(
|
119
|
+
"CORE_REGISTRY_FILTER", "ddeutil.workflow.utils"
|
120
|
+
)
|
121
|
+
return [r.strip() for r in regis_filter_str.split(",")]
|
90
122
|
|
91
123
|
# NOTE: Logging
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
124
|
+
@property
|
125
|
+
def log_path(self) -> Path:
|
126
|
+
return Path(env("LOG_PATH", "./logs"))
|
127
|
+
|
128
|
+
@property
|
129
|
+
def debug(self) -> bool:
|
130
|
+
return str2bool(env("LOG_DEBUG_MODE", "true"))
|
131
|
+
|
132
|
+
@property
|
133
|
+
def enable_write_log(self) -> bool:
|
134
|
+
return str2bool(env("LOG_ENABLE_WRITE", "false"))
|
97
135
|
|
98
136
|
# NOTE: Stage
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
stage_default_id: bool = str2bool(
|
103
|
-
env("WORKFLOW_CORE_STAGE_DEFAULT_ID", "false")
|
104
|
-
)
|
137
|
+
@property
|
138
|
+
def stage_raise_error(self) -> bool:
|
139
|
+
return str2bool(env("CORE_STAGE_RAISE_ERROR", "false"))
|
105
140
|
|
106
|
-
|
107
|
-
|
108
|
-
env("
|
109
|
-
)
|
110
|
-
job_default_id: bool = str2bool(
|
111
|
-
env("WORKFLOW_CORE_JOB_DEFAULT_ID", "false")
|
112
|
-
)
|
141
|
+
@property
|
142
|
+
def stage_default_id(self) -> bool:
|
143
|
+
return str2bool(env("CORE_STAGE_DEFAULT_ID", "false"))
|
113
144
|
|
114
|
-
# NOTE:
|
115
|
-
|
116
|
-
|
117
|
-
env("
|
118
|
-
)
|
119
|
-
max_poking_pool_worker: int = int(
|
120
|
-
os.getenv("WORKFLOW_CORE_MAX_NUM_POKING", "4")
|
121
|
-
)
|
122
|
-
max_on_per_workflow: int = int(
|
123
|
-
env("WORKFLOW_CORE_MAX_CRON_PER_WORKFLOW", "5")
|
124
|
-
)
|
125
|
-
max_queue_complete_hist: int = int(
|
126
|
-
os.getenv("WORKFLOW_CORE_MAX_QUEUE_COMPLETE_HIST", "16")
|
127
|
-
)
|
145
|
+
# NOTE: Job
|
146
|
+
@property
|
147
|
+
def job_raise_error(self) -> bool:
|
148
|
+
return str2bool(env("CORE_JOB_RAISE_ERROR", "true"))
|
128
149
|
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
env("WORKFLOW_APP_MAX_SCHEDULE_PER_PROCESS", "100")
|
133
|
-
)
|
134
|
-
stop_boundary_delta_str: str = env(
|
135
|
-
"WORKFLOW_APP_STOP_BOUNDARY_DELTA", '{"minutes": 5, "seconds": 20}'
|
136
|
-
)
|
150
|
+
@property
|
151
|
+
def job_default_id(self) -> bool:
|
152
|
+
return str2bool(env("CORE_JOB_DEFAULT_ID", "false"))
|
137
153
|
|
138
|
-
# NOTE:
|
139
|
-
|
140
|
-
|
141
|
-
env("
|
142
|
-
)
|
143
|
-
enable_route_schedule: bool = str2bool(
|
144
|
-
env("WORKFLOW_API_ENABLE_ROUTE_SCHEDULE", "true")
|
145
|
-
)
|
154
|
+
# NOTE: Workflow
|
155
|
+
@property
|
156
|
+
def max_job_parallel(self) -> int:
|
157
|
+
max_job_parallel = int(env("CORE_MAX_JOB_PARALLEL", "2"))
|
146
158
|
|
147
|
-
def __init__(self) -> None:
|
148
159
|
# VALIDATE: the MAX_JOB_PARALLEL value should not less than 0.
|
149
|
-
if
|
160
|
+
if max_job_parallel < 0:
|
150
161
|
raise ValueError(
|
151
|
-
f"``
|
152
|
-
f"{
|
162
|
+
f"``WORKFLOW_MAX_JOB_PARALLEL`` should more than 0 but got "
|
163
|
+
f"{max_job_parallel}."
|
153
164
|
)
|
165
|
+
return max_job_parallel
|
166
|
+
|
167
|
+
@property
|
168
|
+
def max_job_exec_timeout(self) -> int:
|
169
|
+
return int(env("CORE_MAX_JOB_EXEC_TIMEOUT", "600"))
|
170
|
+
|
171
|
+
@property
|
172
|
+
def max_poking_pool_worker(self) -> int:
|
173
|
+
return int(env("CORE_MAX_NUM_POKING", "4"))
|
174
|
+
|
175
|
+
@property
|
176
|
+
def max_on_per_workflow(self) -> int:
|
177
|
+
return int(env("CORE_MAX_CRON_PER_WORKFLOW", "5"))
|
178
|
+
|
179
|
+
@property
|
180
|
+
def max_queue_complete_hist(self) -> int:
|
181
|
+
return int(env("CORE_MAX_QUEUE_COMPLETE_HIST", "16"))
|
182
|
+
|
183
|
+
# NOTE: Schedule App
|
184
|
+
@property
|
185
|
+
def max_schedule_process(self) -> int:
|
186
|
+
return int(env("APP_MAX_PROCESS", "2"))
|
187
|
+
|
188
|
+
@property
|
189
|
+
def max_schedule_per_process(self) -> int:
|
190
|
+
return int(env("APP_MAX_SCHEDULE_PER_PROCESS", "100"))
|
154
191
|
|
192
|
+
@property
|
193
|
+
def stop_boundary_delta(self) -> timedelta:
|
194
|
+
stop_boundary_delta_str: str = env(
|
195
|
+
"APP_STOP_BOUNDARY_DELTA", '{"minutes": 5, "seconds": 20}'
|
196
|
+
)
|
155
197
|
try:
|
156
|
-
|
157
|
-
**json.loads(self.stop_boundary_delta_str)
|
158
|
-
)
|
198
|
+
return timedelta(**json.loads(stop_boundary_delta_str))
|
159
199
|
except Exception as err:
|
160
200
|
raise ValueError(
|
161
201
|
"Config ``WORKFLOW_APP_STOP_BOUNDARY_DELTA`` can not parsing to"
|
162
|
-
f"timedelta with {
|
202
|
+
f"timedelta with {stop_boundary_delta_str}."
|
163
203
|
) from err
|
164
204
|
|
205
|
+
# NOTE: API
|
165
206
|
@property
|
166
|
-
def
|
167
|
-
"""
|
168
|
-
|
169
|
-
:rtype: Path
|
170
|
-
"""
|
171
|
-
return self.root_path / os.getenv("WORKFLOW_CORE_PATH_CONF", "conf")
|
207
|
+
def prefix_path(self) -> str:
|
208
|
+
return env("API_PREFIX_PATH", "/api/v1")
|
172
209
|
|
173
210
|
@property
|
174
|
-
def
|
175
|
-
return
|
211
|
+
def enable_route_workflow(self) -> bool:
|
212
|
+
return str2bool(env("API_ENABLE_ROUTE_WORKFLOW", "true"))
|
176
213
|
|
177
214
|
@property
|
178
|
-
def
|
179
|
-
return
|
215
|
+
def enable_route_schedule(self) -> bool:
|
216
|
+
return str2bool(env("API_ENABLE_ROUTE_SCHEDULE", "true"))
|
180
217
|
|
181
218
|
|
182
219
|
class SimLoad:
|
@@ -206,14 +243,9 @@ class SimLoad:
|
|
206
243
|
externals: DictData | None = None,
|
207
244
|
) -> None:
|
208
245
|
self.data: DictData = {}
|
209
|
-
for file in conf.conf_path
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
if data := self.filter_suffix(
|
214
|
-
file,
|
215
|
-
name,
|
216
|
-
):
|
246
|
+
for file in glob_files(conf.conf_path):
|
247
|
+
|
248
|
+
if data := self.filter_suffix(file, name):
|
217
249
|
self.data = data
|
218
250
|
|
219
251
|
# VALIDATE: check the data that reading should not empty.
|
@@ -245,10 +277,7 @@ class SimLoad:
|
|
245
277
|
:rtype: Iterator[tuple[str, DictData]]
|
246
278
|
"""
|
247
279
|
exclude: list[str] = excluded or []
|
248
|
-
for file in conf.conf_path
|
249
|
-
|
250
|
-
if not file.is_file():
|
251
|
-
continue
|
280
|
+
for file in glob_files(conf.conf_path):
|
252
281
|
|
253
282
|
for key, data in cls.filter_suffix(file).items():
|
254
283
|
|
@@ -274,7 +303,7 @@ class SimLoad:
|
|
274
303
|
"""Return object of string type which implement on any registry. The
|
275
304
|
object type.
|
276
305
|
|
277
|
-
:rtype:
|
306
|
+
:rtype: str
|
278
307
|
"""
|
279
308
|
if _typ := self.data.get("type"):
|
280
309
|
return _typ
|
@@ -481,6 +510,18 @@ class FileLog(BaseLog):
|
|
481
510
|
|
482
511
|
class SQLiteLog(BaseLog): # pragma: no cov
|
483
512
|
|
513
|
+
table: str = "workflow_log"
|
514
|
+
ddl: str = """
|
515
|
+
workflow str,
|
516
|
+
release int,
|
517
|
+
type str,
|
518
|
+
context json,
|
519
|
+
parent_run_id int,
|
520
|
+
run_id int,
|
521
|
+
update datetime
|
522
|
+
primary key ( run_id )
|
523
|
+
"""
|
524
|
+
|
484
525
|
def save(self, excluded: list[str] | None) -> None:
|
485
526
|
raise NotImplementedError("SQLiteLog does not implement yet.")
|
486
527
|
|
@@ -489,3 +530,9 @@ Log = Union[
|
|
489
530
|
FileLog,
|
490
531
|
SQLiteLog,
|
491
532
|
]
|
533
|
+
|
534
|
+
|
535
|
+
def get_log() -> Log: # pragma: no cov
|
536
|
+
if config.log_path.is_file():
|
537
|
+
return SQLiteLog
|
538
|
+
return FileLog
|
ddeutil/workflow/scheduler.py
CHANGED
@@ -51,7 +51,7 @@ except ImportError: # pragma: no cov
|
|
51
51
|
|
52
52
|
from .__cron import CronRunner
|
53
53
|
from .__types import DictData, TupleStr
|
54
|
-
from .conf import
|
54
|
+
from .conf import Loader, Log, config, get_log, get_logger
|
55
55
|
from .cron import On
|
56
56
|
from .exceptions import WorkflowException
|
57
57
|
from .utils import (
|
@@ -493,7 +493,7 @@ def schedule_control(
|
|
493
493
|
"Should install schedule package before use this module."
|
494
494
|
) from None
|
495
495
|
|
496
|
-
log: type[Log] = log or
|
496
|
+
log: type[Log] = log or get_log()
|
497
497
|
scheduler: Scheduler = Scheduler()
|
498
498
|
start_date: datetime = datetime.now(tz=config.tz)
|
499
499
|
stop_date: datetime = stop or (start_date + config.stop_boundary_delta)
|
ddeutil/workflow/workflow.py
CHANGED
@@ -42,7 +42,7 @@ from typing_extensions import Self
|
|
42
42
|
|
43
43
|
from .__cron import CronJob, CronRunner
|
44
44
|
from .__types import DictData, TupleStr
|
45
|
-
from .conf import
|
45
|
+
from .conf import Loader, Log, config, get_log, get_logger
|
46
46
|
from .cron import On
|
47
47
|
from .exceptions import JobException, WorkflowException
|
48
48
|
from .job import Job
|
@@ -501,7 +501,7 @@ class Workflow(BaseModel):
|
|
501
501
|
|
502
502
|
:rtype: Result
|
503
503
|
"""
|
504
|
-
log: type[Log] = log or
|
504
|
+
log: type[Log] = log or get_log()
|
505
505
|
name: str = override_log_name or self.name
|
506
506
|
run_id: str = run_id or gen_id(name, unique=True)
|
507
507
|
rs_release: Result = Result(run_id=run_id)
|
@@ -670,7 +670,7 @@ class Workflow(BaseModel):
|
|
670
670
|
:rtype: list[Result]
|
671
671
|
:return: A list of all results that return from ``self.release`` method.
|
672
672
|
"""
|
673
|
-
log: type[Log] = log or
|
673
|
+
log: type[Log] = log or get_log()
|
674
674
|
run_id: str = run_id or gen_id(self.name, unique=True)
|
675
675
|
|
676
676
|
# NOTE: If this workflow does not set the on schedule, it will return
|
@@ -1151,7 +1151,7 @@ class WorkflowTask:
|
|
1151
1151
|
|
1152
1152
|
:rtype: Result
|
1153
1153
|
"""
|
1154
|
-
log: type[Log] = log or
|
1154
|
+
log: type[Log] = log or get_log()
|
1155
1155
|
|
1156
1156
|
if release is None:
|
1157
1157
|
if queue.check_queue(self.runner.date):
|
@@ -1,23 +1,23 @@
|
|
1
|
-
ddeutil/workflow/__about__.py,sha256=
|
1
|
+
ddeutil/workflow/__about__.py,sha256=jU_KFZf1uiZIWhuownbhRsjIL3oHGR_URL-jKTEnMKo,34
|
2
2
|
ddeutil/workflow/__cron.py,sha256=uA8XcbY_GwA9rJSHaHUaXaJyGDObJN0ZeYlJSinL8y8,26880
|
3
|
-
ddeutil/workflow/__init__.py,sha256=
|
3
|
+
ddeutil/workflow/__init__.py,sha256=ozadVrqfqFRuukjv_zXUcgLANdiSrC6wrKkyVjdGg3w,1521
|
4
4
|
ddeutil/workflow/__types.py,sha256=Ia7f38kvL3NibwmRKi0wQ1ud_45Z-SojYGhNJwIqcu8,3713
|
5
|
-
ddeutil/workflow/conf.py,sha256=
|
5
|
+
ddeutil/workflow/conf.py,sha256=AU3GKTaxFFGDN-Sg8BGb08xj7vRBCTTjwk0FaORLJIk,16188
|
6
6
|
ddeutil/workflow/cron.py,sha256=75A0hqevvouziKoLALncLJspVAeki9qCH3zniAJaxzY,7513
|
7
7
|
ddeutil/workflow/exceptions.py,sha256=P56K7VD3etGm9y-k_GXrzEyqsTCaz9EJazTIshZDf9g,943
|
8
8
|
ddeutil/workflow/job.py,sha256=cvSLMdc1sMl1MeU7so7Oe2SdRYxQwt6hm55mLV1iP-Y,24219
|
9
9
|
ddeutil/workflow/params.py,sha256=uPGkZx18E-iZ8BteqQ2ONgg0frhF3ZmP5cOyfK2j59U,5280
|
10
10
|
ddeutil/workflow/result.py,sha256=WIC8MsnfLiWNpZomT6jS4YCdYhlbIVVBjtGGe2dkoKk,3404
|
11
|
-
ddeutil/workflow/scheduler.py,sha256=
|
11
|
+
ddeutil/workflow/scheduler.py,sha256=BbY_3Y3QOdNwDfdvnRa7grGC2_a0Hn1KJbZKAscchk8,20454
|
12
12
|
ddeutil/workflow/stage.py,sha256=a2sngzs9DkP6GU2pgAD3QvGoijyBQTR_pOhyJUIuWAo,26692
|
13
13
|
ddeutil/workflow/utils.py,sha256=pucRnCi9aLJDptXhzzReHZd5d-S0o5oZif5tr6H4iy8,18736
|
14
|
-
ddeutil/workflow/workflow.py,sha256=
|
14
|
+
ddeutil/workflow/workflow.py,sha256=s6E-mKzSVQPTSV0biIAu5lFjslo6blKA-WTAjeOfLuw,42183
|
15
15
|
ddeutil/workflow/api/__init__.py,sha256=F53NMBWtb9IKaDWkPU5KvybGGfKAcbehgn6TLBwHuuM,21
|
16
16
|
ddeutil/workflow/api/api.py,sha256=Md1cz3Edc7_uz63s_L_i-R3IE4mkO3aTADrX8GOGU-Y,5644
|
17
17
|
ddeutil/workflow/api/repeat.py,sha256=zyvsrXKk-3-_N8ZRZSki0Mueshugum2jtqctEOp9QSc,4927
|
18
18
|
ddeutil/workflow/api/route.py,sha256=v96jNbgjM1cJ2MpVSRWs2kgRqF8DQElEBdRZrVFEpEw,8578
|
19
|
-
ddeutil_workflow-0.0.26.
|
20
|
-
ddeutil_workflow-0.0.26.
|
21
|
-
ddeutil_workflow-0.0.26.
|
22
|
-
ddeutil_workflow-0.0.26.
|
23
|
-
ddeutil_workflow-0.0.26.
|
19
|
+
ddeutil_workflow-0.0.26.post1.dist-info/LICENSE,sha256=nGFZ1QEhhhWeMHf9n99_fdt4vQaXS29xWKxt-OcLywk,1085
|
20
|
+
ddeutil_workflow-0.0.26.post1.dist-info/METADATA,sha256=B95z9M1Z9DWiKXQr1VoRvtlYcB6eX11RGktlAwn4MvI,14364
|
21
|
+
ddeutil_workflow-0.0.26.post1.dist-info/WHEEL,sha256=A3WOREP4zgxI0fKrHUG8DC8013e3dK3n7a6HDbcEIwE,91
|
22
|
+
ddeutil_workflow-0.0.26.post1.dist-info/top_level.txt,sha256=m9M6XeSWDwt_yMsmH6gcOjHZVK5O0-vgtNBuncHjzW4,8
|
23
|
+
ddeutil_workflow-0.0.26.post1.dist-info/RECORD,,
|
File without changes
|
File without changes
|
{ddeutil_workflow-0.0.26.post0.dist-info → ddeutil_workflow-0.0.26.post1.dist-info}/top_level.txt
RENAMED
File without changes
|