ddeutil-workflow 0.0.30__py3-none-any.whl → 0.0.31__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/__cron.py +1 -0
- ddeutil/workflow/__init__.py +5 -1
- ddeutil/workflow/conf.py +4 -0
- ddeutil/workflow/cron.py +77 -21
- ddeutil/workflow/exceptions.py +3 -0
- ddeutil/workflow/params.py +18 -1
- ddeutil/workflow/result.py +1 -0
- ddeutil/workflow/scheduler.py +90 -56
- ddeutil/workflow/stage.py +13 -1
- ddeutil/workflow/utils.py +36 -10
- ddeutil/workflow/workflow.py +118 -87
- {ddeutil_workflow-0.0.30.dist-info → ddeutil_workflow-0.0.31.dist-info}/METADATA +10 -1
- ddeutil_workflow-0.0.31.dist-info/RECORD +25 -0
- ddeutil_workflow-0.0.30.dist-info/RECORD +0 -25
- {ddeutil_workflow-0.0.30.dist-info → ddeutil_workflow-0.0.31.dist-info}/LICENSE +0 -0
- {ddeutil_workflow-0.0.30.dist-info → ddeutil_workflow-0.0.31.dist-info}/WHEEL +0 -0
- {ddeutil_workflow-0.0.30.dist-info → ddeutil_workflow-0.0.31.dist-info}/top_level.txt +0 -0
ddeutil/workflow/__about__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__: str = "0.0.
|
1
|
+
__version__: str = "0.0.31"
|
ddeutil/workflow/__cron.py
CHANGED
ddeutil/workflow/__init__.py
CHANGED
@@ -47,8 +47,10 @@ from .params import (
|
|
47
47
|
from .result import Result
|
48
48
|
from .scheduler import (
|
49
49
|
Schedule,
|
50
|
-
|
50
|
+
ScheduleWorkflow,
|
51
|
+
schedule_control,
|
51
52
|
schedule_runner,
|
53
|
+
schedule_task,
|
52
54
|
)
|
53
55
|
from .stage import (
|
54
56
|
BashStage,
|
@@ -83,6 +85,8 @@ from .utils import (
|
|
83
85
|
make_exec,
|
84
86
|
)
|
85
87
|
from .workflow import (
|
88
|
+
Release,
|
89
|
+
ReleaseQueue,
|
86
90
|
Workflow,
|
87
91
|
WorkflowTask,
|
88
92
|
)
|
ddeutil/workflow/conf.py
CHANGED
@@ -582,6 +582,10 @@ Log = Union[
|
|
582
582
|
|
583
583
|
|
584
584
|
def get_log() -> type[Log]: # pragma: no cov
|
585
|
+
"""Get logging class that dynamic base on the config log path value.
|
586
|
+
|
587
|
+
:rtype: type[Log]
|
588
|
+
"""
|
585
589
|
if config.log_path.is_file():
|
586
590
|
return SQLiteLog
|
587
591
|
return FileLog
|
ddeutil/workflow/cron.py
CHANGED
@@ -5,16 +5,17 @@
|
|
5
5
|
# ------------------------------------------------------------------------------
|
6
6
|
from __future__ import annotations
|
7
7
|
|
8
|
+
from dataclasses import fields
|
8
9
|
from datetime import datetime
|
9
|
-
from typing import Annotated, Literal
|
10
|
+
from typing import Annotated, Literal, Union
|
10
11
|
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
11
12
|
|
12
|
-
from pydantic import BaseModel, ConfigDict, Field
|
13
|
+
from pydantic import BaseModel, ConfigDict, Field, ValidationInfo
|
13
14
|
from pydantic.functional_serializers import field_serializer
|
14
15
|
from pydantic.functional_validators import field_validator, model_validator
|
15
16
|
from typing_extensions import Self
|
16
17
|
|
17
|
-
from .__cron import WEEKDAYS, CronJob, CronJobYear, CronRunner
|
18
|
+
from .__cron import WEEKDAYS, CronJob, CronJobYear, CronRunner, Options
|
18
19
|
from .__types import DictData, DictStr, TupleStr
|
19
20
|
from .conf import Loader
|
20
21
|
|
@@ -47,6 +48,8 @@ def interval2crontab(
|
|
47
48
|
'0 0 1 * *'
|
48
49
|
>>> interval2crontab(interval='monthly', day='tuesday', time='12:00')
|
49
50
|
'12 0 1 * 2'
|
51
|
+
|
52
|
+
:rtype: str
|
50
53
|
"""
|
51
54
|
d: str = "*"
|
52
55
|
if interval == "weekly":
|
@@ -64,12 +67,19 @@ class On(BaseModel):
|
|
64
67
|
"""On Pydantic model (Warped crontab object by model).
|
65
68
|
|
66
69
|
See Also:
|
67
|
-
* ``generate()`` is the main
|
70
|
+
* ``generate()`` is the main use-case of this schedule object.
|
68
71
|
"""
|
69
72
|
|
70
73
|
model_config = ConfigDict(arbitrary_types_allowed=True)
|
71
74
|
|
72
75
|
# NOTE: This is fields of the base schedule.
|
76
|
+
extras: Annotated[
|
77
|
+
DictData,
|
78
|
+
Field(
|
79
|
+
default_factory=dict,
|
80
|
+
description="An extras mapping parameters",
|
81
|
+
),
|
82
|
+
]
|
73
83
|
cronjob: Annotated[
|
74
84
|
CronJob,
|
75
85
|
Field(description="Cron job of this schedule"),
|
@@ -81,13 +91,6 @@ class On(BaseModel):
|
|
81
91
|
alias="timezone",
|
82
92
|
),
|
83
93
|
] = "Etc/UTC"
|
84
|
-
extras: Annotated[
|
85
|
-
DictData,
|
86
|
-
Field(
|
87
|
-
default_factory=dict,
|
88
|
-
description="An extras mapping parameters",
|
89
|
-
),
|
90
|
-
]
|
91
94
|
|
92
95
|
@classmethod
|
93
96
|
def from_value(cls, value: DictStr, externals: DictData) -> Self:
|
@@ -153,6 +156,7 @@ class On(BaseModel):
|
|
153
156
|
|
154
157
|
@model_validator(mode="before")
|
155
158
|
def __prepare_values(cls, values: DictData) -> DictData:
|
159
|
+
"""Extract tz key from value and change name to timezone key."""
|
156
160
|
if tz := values.pop("tz", None):
|
157
161
|
values["timezone"] = tz
|
158
162
|
return values
|
@@ -160,24 +164,55 @@ class On(BaseModel):
|
|
160
164
|
@field_validator("tz")
|
161
165
|
def __validate_tz(cls, value: str) -> str:
|
162
166
|
"""Validate timezone value that able to initialize with ZoneInfo after
|
163
|
-
it passing to this model in before mode.
|
167
|
+
it passing to this model in before mode.
|
168
|
+
|
169
|
+
:rtype: str
|
170
|
+
"""
|
164
171
|
try:
|
165
172
|
_ = ZoneInfo(value)
|
166
173
|
return value
|
167
174
|
except ZoneInfoNotFoundError as err:
|
168
175
|
raise ValueError(f"Invalid timezone: {value}") from err
|
169
176
|
|
170
|
-
@field_validator(
|
171
|
-
|
172
|
-
|
173
|
-
|
177
|
+
@field_validator(
|
178
|
+
"cronjob", mode="before", json_schema_input_type=Union[CronJob, str]
|
179
|
+
)
|
180
|
+
def __prepare_cronjob(
|
181
|
+
cls, value: str | CronJob, info: ValidationInfo
|
182
|
+
) -> CronJob:
|
183
|
+
"""Prepare crontab value that able to receive with string type.
|
184
|
+
This step will get options kwargs from extras and pass to the
|
185
|
+
CronJob object.
|
186
|
+
|
187
|
+
:rtype: CronJob
|
188
|
+
"""
|
189
|
+
extras: DictData = info.data.get("extras", {})
|
190
|
+
return (
|
191
|
+
CronJob(
|
192
|
+
value,
|
193
|
+
option={
|
194
|
+
name: extras[name]
|
195
|
+
for name in (f.name for f in fields(Options))
|
196
|
+
if name in extras
|
197
|
+
},
|
198
|
+
)
|
199
|
+
if isinstance(value, str)
|
200
|
+
else value
|
201
|
+
)
|
174
202
|
|
175
203
|
@field_serializer("cronjob")
|
176
204
|
def __serialize_cronjob(self, value: CronJob) -> str:
|
205
|
+
"""Serialize the cronjob field that store with CronJob object.
|
206
|
+
|
207
|
+
:rtype: str
|
208
|
+
"""
|
177
209
|
return str(value)
|
178
210
|
|
179
211
|
def generate(self, start: str | datetime) -> CronRunner:
|
180
|
-
"""Return Cron runner object.
|
212
|
+
"""Return Cron runner object.
|
213
|
+
|
214
|
+
:rtype: CronRunner
|
215
|
+
"""
|
181
216
|
if isinstance(start, str):
|
182
217
|
start: datetime = datetime.fromisoformat(start)
|
183
218
|
elif not isinstance(start, datetime):
|
@@ -187,6 +222,8 @@ class On(BaseModel):
|
|
187
222
|
def next(self, start: str | datetime) -> CronRunner:
|
188
223
|
"""Return a next datetime from Cron runner object that start with any
|
189
224
|
date that given from input.
|
225
|
+
|
226
|
+
:rtype: CronRunner
|
190
227
|
"""
|
191
228
|
runner: CronRunner = self.generate(start=start)
|
192
229
|
|
@@ -209,7 +246,26 @@ class YearOn(On):
|
|
209
246
|
Field(description="Cron job of this schedule"),
|
210
247
|
]
|
211
248
|
|
212
|
-
@field_validator(
|
213
|
-
|
214
|
-
|
215
|
-
|
249
|
+
@field_validator(
|
250
|
+
"cronjob", mode="before", json_schema_input_type=Union[CronJob, str]
|
251
|
+
)
|
252
|
+
def __prepare_cronjob(
|
253
|
+
cls, value: str | CronJobYear, info: ValidationInfo
|
254
|
+
) -> CronJobYear:
|
255
|
+
"""Prepare crontab value that able to receive with string type.
|
256
|
+
|
257
|
+
:rtype: CronJobYear
|
258
|
+
"""
|
259
|
+
extras: DictData = info.data.get("extras", {})
|
260
|
+
return (
|
261
|
+
CronJobYear(
|
262
|
+
value,
|
263
|
+
option={
|
264
|
+
name: extras[name]
|
265
|
+
for name in (f.name for f in fields(Options))
|
266
|
+
if name in extras
|
267
|
+
},
|
268
|
+
)
|
269
|
+
if isinstance(value, str)
|
270
|
+
else value
|
271
|
+
)
|
ddeutil/workflow/exceptions.py
CHANGED
ddeutil/workflow/params.py
CHANGED
@@ -5,6 +5,7 @@
|
|
5
5
|
# ------------------------------------------------------------------------------
|
6
6
|
from __future__ import annotations
|
7
7
|
|
8
|
+
import decimal
|
8
9
|
import logging
|
9
10
|
from abc import ABC, abstractmethod
|
10
11
|
from datetime import date, datetime
|
@@ -49,7 +50,7 @@ class BaseParam(BaseModel, ABC):
|
|
49
50
|
|
50
51
|
class DefaultParam(BaseParam):
|
51
52
|
"""Default Parameter that will check default if it required. This model do
|
52
|
-
not implement the receive method.
|
53
|
+
not implement the `receive` method.
|
53
54
|
"""
|
54
55
|
|
55
56
|
required: bool = Field(
|
@@ -68,6 +69,15 @@ class DefaultParam(BaseParam):
|
|
68
69
|
)
|
69
70
|
|
70
71
|
|
72
|
+
# TODO: Not implement this parameter yet
|
73
|
+
class DateParam(DefaultParam):
|
74
|
+
"""Date parameter."""
|
75
|
+
|
76
|
+
type: Literal["date"] = "date"
|
77
|
+
|
78
|
+
def receive(self, value: Optional[str | date] = None) -> date: ...
|
79
|
+
|
80
|
+
|
71
81
|
class DatetimeParam(DefaultParam):
|
72
82
|
"""Datetime parameter."""
|
73
83
|
|
@@ -145,6 +155,13 @@ class IntParam(DefaultParam):
|
|
145
155
|
return value
|
146
156
|
|
147
157
|
|
158
|
+
# TODO: Not implement this parameter yet
|
159
|
+
class DecimalParam(DefaultParam):
|
160
|
+
type: Literal["decimal"] = "decimal"
|
161
|
+
|
162
|
+
def receive(self, value: float | None = None) -> decimal.Decimal: ...
|
163
|
+
|
164
|
+
|
148
165
|
class ChoiceParam(BaseParam):
|
149
166
|
"""Choice parameter."""
|
150
167
|
|
ddeutil/workflow/result.py
CHANGED
@@ -94,6 +94,7 @@ class Result:
|
|
94
94
|
# NOTE: Check the context has jobs key.
|
95
95
|
if "jobs" not in self.__dict__["context"]:
|
96
96
|
self.__dict__["context"]["jobs"] = {}
|
97
|
+
|
97
98
|
self.__dict__["context"]["jobs"].update(result.context)
|
98
99
|
|
99
100
|
# NOTE: Update running ID from an incoming result.
|
ddeutil/workflow/scheduler.py
CHANGED
@@ -33,7 +33,7 @@ from functools import wraps
|
|
33
33
|
from heapq import heappop, heappush
|
34
34
|
from textwrap import dedent
|
35
35
|
from threading import Thread
|
36
|
-
from typing import Callable, Optional, TypedDict
|
36
|
+
from typing import Callable, Optional, TypedDict, Union
|
37
37
|
|
38
38
|
from pydantic import BaseModel, Field
|
39
39
|
from pydantic.functional_validators import field_validator, model_validator
|
@@ -41,7 +41,7 @@ from typing_extensions import Self
|
|
41
41
|
|
42
42
|
try:
|
43
43
|
from typing import ParamSpec
|
44
|
-
except ImportError:
|
44
|
+
except ImportError: # pragma: no cov
|
45
45
|
from typing_extensions import ParamSpec
|
46
46
|
|
47
47
|
try:
|
@@ -53,11 +53,9 @@ from .__cron import CronRunner
|
|
53
53
|
from .__types import DictData, TupleStr
|
54
54
|
from .conf import Loader, Log, config, get_log, get_logger
|
55
55
|
from .cron import On
|
56
|
-
from .exceptions import WorkflowException
|
57
|
-
from .
|
58
|
-
|
59
|
-
delay,
|
60
|
-
)
|
56
|
+
from .exceptions import ScheduleException, WorkflowException
|
57
|
+
from .result import Result
|
58
|
+
from .utils import batch, delay
|
61
59
|
from .workflow import Release, ReleaseQueue, Workflow, WorkflowTask
|
62
60
|
|
63
61
|
P = ParamSpec("P")
|
@@ -69,7 +67,7 @@ logging.getLogger("schedule").setLevel(logging.INFO)
|
|
69
67
|
|
70
68
|
__all__: TupleStr = (
|
71
69
|
"Schedule",
|
72
|
-
"
|
70
|
+
"ScheduleWorkflow",
|
73
71
|
"schedule_task",
|
74
72
|
"monitor",
|
75
73
|
"schedule_control",
|
@@ -79,8 +77,8 @@ __all__: TupleStr = (
|
|
79
77
|
)
|
80
78
|
|
81
79
|
|
82
|
-
class
|
83
|
-
"""Workflow
|
80
|
+
class ScheduleWorkflow(BaseModel):
|
81
|
+
"""Schedule Workflow Pydantic model that use to keep workflow model for
|
84
82
|
the Schedule model. it should not use Workflow model directly because on the
|
85
83
|
schedule config it can adjust crontab value that different from the Workflow
|
86
84
|
model.
|
@@ -233,9 +231,9 @@ class Schedule(BaseModel):
|
|
233
231
|
"A schedule description that can be string of markdown content."
|
234
232
|
),
|
235
233
|
)
|
236
|
-
workflows: list[
|
234
|
+
workflows: list[ScheduleWorkflow] = Field(
|
237
235
|
default_factory=list,
|
238
|
-
description="A list of
|
236
|
+
description="A list of ScheduleWorkflow models.",
|
239
237
|
)
|
240
238
|
|
241
239
|
@field_validator("desc", mode="after")
|
@@ -258,7 +256,7 @@ class Schedule(BaseModel):
|
|
258
256
|
an input schedule name. The loader object will use this schedule name to
|
259
257
|
searching configuration data of this schedule model in conf path.
|
260
258
|
|
261
|
-
:param name: A schedule name that want to pass to Loader object.
|
259
|
+
:param name: (str) A schedule name that want to pass to Loader object.
|
262
260
|
:param externals: An external parameters that want to pass to Loader
|
263
261
|
object.
|
264
262
|
|
@@ -277,6 +275,37 @@ class Schedule(BaseModel):
|
|
277
275
|
|
278
276
|
return cls.model_validate(obj=loader_data)
|
279
277
|
|
278
|
+
@classmethod
|
279
|
+
def extract_tasks(
|
280
|
+
cls,
|
281
|
+
schedules: list[str],
|
282
|
+
start_date: datetime,
|
283
|
+
queue: dict[str, ReleaseQueue],
|
284
|
+
externals: DictData | None = None,
|
285
|
+
) -> list[WorkflowTask]:
|
286
|
+
"""Return the list of WorkflowTask object from all schedule object that
|
287
|
+
include in an input schedules argument.
|
288
|
+
|
289
|
+
:param schedules: A list of schedule name that will use `from_loader`
|
290
|
+
method.
|
291
|
+
:param start_date: A start date that get from the workflow schedule.
|
292
|
+
:param queue: A mapping of name and list of datetime for queue.
|
293
|
+
:param externals: An external parameters that pass to the Loader object.
|
294
|
+
|
295
|
+
:rtype: list[WorkflowTask]
|
296
|
+
"""
|
297
|
+
tasks: list[WorkflowTask] = []
|
298
|
+
for name in schedules:
|
299
|
+
schedule: Schedule = Schedule.from_loader(name, externals=externals)
|
300
|
+
tasks.extend(
|
301
|
+
schedule.tasks(
|
302
|
+
start_date,
|
303
|
+
queue=queue,
|
304
|
+
externals=externals,
|
305
|
+
),
|
306
|
+
)
|
307
|
+
return tasks
|
308
|
+
|
280
309
|
def tasks(
|
281
310
|
self,
|
282
311
|
start_date: datetime,
|
@@ -311,7 +340,8 @@ class Schedule(BaseModel):
|
|
311
340
|
return workflow_tasks
|
312
341
|
|
313
342
|
|
314
|
-
|
343
|
+
ResultOrCancelJob = Union[type[CancelJob], Result]
|
344
|
+
ReturnCancelJob = Callable[P, ResultOrCancelJob]
|
315
345
|
DecoratorCancelJob = Callable[[ReturnCancelJob], ReturnCancelJob]
|
316
346
|
|
317
347
|
|
@@ -326,24 +356,25 @@ def catch_exceptions(cancel_on_failure: bool = False) -> DecoratorCancelJob:
|
|
326
356
|
"""
|
327
357
|
|
328
358
|
def decorator(func: ReturnCancelJob) -> ReturnCancelJob: # pragma: no cov
|
329
|
-
try:
|
330
359
|
|
331
|
-
|
332
|
-
|
360
|
+
@wraps(func)
|
361
|
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> ResultOrCancelJob:
|
362
|
+
try:
|
333
363
|
return func(*args, **kwargs)
|
364
|
+
except Exception as err:
|
365
|
+
logger.exception(err)
|
366
|
+
if cancel_on_failure:
|
367
|
+
return CancelJob
|
368
|
+
raise err
|
334
369
|
|
335
|
-
|
336
|
-
|
337
|
-
except Exception as err:
|
338
|
-
logger.exception(err)
|
339
|
-
if cancel_on_failure:
|
340
|
-
return CancelJob
|
341
|
-
raise err
|
370
|
+
return wrapper
|
342
371
|
|
343
372
|
return decorator
|
344
373
|
|
345
374
|
|
346
375
|
class ReleaseThread(TypedDict):
|
376
|
+
"""TypeDict for the release thread."""
|
377
|
+
|
347
378
|
thread: Thread
|
348
379
|
start_date: datetime
|
349
380
|
|
@@ -358,11 +389,13 @@ def schedule_task(
|
|
358
389
|
queue: dict[str, ReleaseQueue],
|
359
390
|
threads: ReleaseThreads,
|
360
391
|
log: type[Log],
|
361
|
-
) -> CancelJob | None:
|
362
|
-
"""
|
363
|
-
|
392
|
+
) -> type[CancelJob] | None:
|
393
|
+
"""Schedule task function that generate thread of workflow task release
|
394
|
+
method in background. This function do the same logic as the workflow poke
|
395
|
+
method, but it runs with map of schedules and the on values.
|
364
396
|
|
365
|
-
This
|
397
|
+
This schedule task start runs every minute at ':02' second and it does
|
398
|
+
not allow you to run with offset time.
|
366
399
|
|
367
400
|
:param tasks: A list of WorkflowTask object.
|
368
401
|
:param stop: A stop datetime object that force stop running scheduler.
|
@@ -370,7 +403,7 @@ def schedule_task(
|
|
370
403
|
:param threads: A mapping of alias name and Thread object.
|
371
404
|
:param log: A log class that want to make log object.
|
372
405
|
|
373
|
-
:rtype: CancelJob | None
|
406
|
+
:rtype: type[CancelJob] | None
|
374
407
|
"""
|
375
408
|
current_date: datetime = datetime.now(tz=config.tz)
|
376
409
|
if current_date > stop.replace(tzinfo=config.tz):
|
@@ -410,12 +443,17 @@ def schedule_task(
|
|
410
443
|
current_release: datetime = current_date.replace(
|
411
444
|
second=0, microsecond=0
|
412
445
|
)
|
413
|
-
if (first_date := q.first_queue.date)
|
446
|
+
if (first_date := q.first_queue.date) > current_release:
|
414
447
|
logger.debug(
|
415
448
|
f"[WORKFLOW]: Skip schedule "
|
416
449
|
f"{first_date:%Y-%m-%d %H:%M:%S} for : {task.alias!r}"
|
417
450
|
)
|
418
451
|
continue
|
452
|
+
elif first_date < current_release: # pragma: no cov
|
453
|
+
raise ScheduleException(
|
454
|
+
"The first release date from queue should not less than current"
|
455
|
+
"release date."
|
456
|
+
)
|
419
457
|
|
420
458
|
# NOTE: Pop the latest release and push it to running.
|
421
459
|
release: Release = heappop(q.queue)
|
@@ -445,7 +483,7 @@ def schedule_task(
|
|
445
483
|
|
446
484
|
delay()
|
447
485
|
|
448
|
-
logger.debug(f"[SCHEDULE]: End schedule
|
486
|
+
logger.debug(f"[SCHEDULE]: End schedule task {'=' * 80}")
|
449
487
|
|
450
488
|
|
451
489
|
def monitor(threads: ReleaseThreads) -> None: # pragma: no cov
|
@@ -455,9 +493,7 @@ def monitor(threads: ReleaseThreads) -> None: # pragma: no cov
|
|
455
493
|
:param threads: A mapping of Thread object and its name.
|
456
494
|
:type threads: ReleaseThreads
|
457
495
|
"""
|
458
|
-
logger.debug(
|
459
|
-
"[MONITOR]: Start checking long running workflow release task."
|
460
|
-
)
|
496
|
+
logger.debug("[MONITOR]: Start checking long running schedule task.")
|
461
497
|
|
462
498
|
snapshot_threads: list[str] = list(threads.keys())
|
463
499
|
for t_name in snapshot_threads:
|
@@ -476,12 +512,15 @@ def schedule_control(
|
|
476
512
|
*,
|
477
513
|
log: type[Log] | None = None,
|
478
514
|
) -> list[str]: # pragma: no cov
|
479
|
-
"""Scheduler control function that
|
515
|
+
"""Scheduler control function that run the chuck of schedules every minute
|
516
|
+
and this function release monitoring thread for tracking undead thread in
|
517
|
+
the background.
|
480
518
|
|
481
519
|
:param schedules: A list of workflow names that want to schedule running.
|
482
520
|
:param stop: A datetime value that use to stop running schedule.
|
483
521
|
:param externals: An external parameters that pass to Loader.
|
484
|
-
:param log:
|
522
|
+
:param log: A log class that use on the workflow task release for writing
|
523
|
+
its release log context.
|
485
524
|
|
486
525
|
:rtype: list[str]
|
487
526
|
"""
|
@@ -493,8 +532,11 @@ def schedule_control(
|
|
493
532
|
"Should install schedule package before use this module."
|
494
533
|
) from None
|
495
534
|
|
535
|
+
# NOTE: Get default logging.
|
496
536
|
log: type[Log] = log or get_log()
|
497
537
|
scheduler: Scheduler = Scheduler()
|
538
|
+
|
539
|
+
# NOTE: Create the start and stop datetime.
|
498
540
|
start_date: datetime = datetime.now(tz=config.tz)
|
499
541
|
stop_date: datetime = stop or (start_date + config.stop_boundary_delta)
|
500
542
|
|
@@ -506,25 +548,15 @@ def schedule_control(
|
|
506
548
|
second=0, microsecond=0
|
507
549
|
) + timedelta(minutes=1)
|
508
550
|
|
509
|
-
# NOTE: Start create workflow tasks from list of schedule name.
|
510
|
-
tasks: list[WorkflowTask] = []
|
511
|
-
for name in schedules:
|
512
|
-
schedule: Schedule = Schedule.from_loader(name, externals=externals)
|
513
|
-
tasks.extend(
|
514
|
-
schedule.tasks(
|
515
|
-
start_date_waiting,
|
516
|
-
queue=queue,
|
517
|
-
externals=externals,
|
518
|
-
),
|
519
|
-
)
|
520
|
-
|
521
551
|
# NOTE: This schedule job will start every minute at :02 seconds.
|
522
552
|
(
|
523
553
|
scheduler.every(1)
|
524
554
|
.minutes.at(":02")
|
525
555
|
.do(
|
526
556
|
schedule_task,
|
527
|
-
tasks=
|
557
|
+
tasks=Schedule.extract_tasks(
|
558
|
+
schedules, start_date_waiting, queue, externals=externals
|
559
|
+
),
|
528
560
|
stop=stop_date,
|
529
561
|
queue=queue,
|
530
562
|
threads=threads,
|
@@ -533,7 +565,8 @@ def schedule_control(
|
|
533
565
|
.tag("control")
|
534
566
|
)
|
535
567
|
|
536
|
-
# NOTE: Checking zombie task with schedule job will start every 5 minute
|
568
|
+
# NOTE: Checking zombie task with schedule job will start every 5 minute at
|
569
|
+
# :10 seconds.
|
537
570
|
(
|
538
571
|
scheduler.every(5)
|
539
572
|
.minutes.at(":10")
|
@@ -579,16 +612,15 @@ def schedule_runner(
|
|
579
612
|
externals: DictData | None = None,
|
580
613
|
excluded: list[str] | None = None,
|
581
614
|
) -> list[str]: # pragma: no cov
|
582
|
-
"""Schedule runner function
|
583
|
-
|
584
|
-
|
615
|
+
"""Schedule runner function it the multiprocess controller function for
|
616
|
+
split the setting schedule to the `schedule_control` function on the
|
617
|
+
process pool. It chunks schedule configs that exists in config
|
618
|
+
path by `WORKFLOW_APP_MAX_SCHEDULE_PER_PROCESS` value.
|
585
619
|
|
586
620
|
:param stop: A stop datetime object that force stop running scheduler.
|
587
621
|
:param externals:
|
588
622
|
:param excluded: A list of schedule name that want to exclude from finding.
|
589
623
|
|
590
|
-
:rtype: list[str]
|
591
|
-
|
592
624
|
This function will get all workflows that include on value that was
|
593
625
|
created in config path and chuck it with application config variable
|
594
626
|
``WORKFLOW_APP_MAX_SCHEDULE_PER_PROCESS`` env var to multiprocess executor
|
@@ -600,7 +632,9 @@ def schedule_runner(
|
|
600
632
|
--> thread of release task 01 02
|
601
633
|
==> schedule --> thread of release task 02 01
|
602
634
|
--> thread of release task 02 02
|
603
|
-
==> process 02
|
635
|
+
==> process 02 ==> ...
|
636
|
+
|
637
|
+
:rtype: list[str]
|
604
638
|
"""
|
605
639
|
results: list[str] = []
|
606
640
|
|
ddeutil/workflow/stage.py
CHANGED
@@ -328,7 +328,7 @@ class BashStage(BaseStage):
|
|
328
328
|
If your current OS is Windows, it will run on the bash in the WSL.
|
329
329
|
|
330
330
|
I get some limitation when I run shell statement with the built-in
|
331
|
-
|
331
|
+
subprocess package. It does not good enough to use multiline statement.
|
332
332
|
Thus, I add writing ``.sh`` file before execution process for fix this
|
333
333
|
issue.
|
334
334
|
|
@@ -665,3 +665,15 @@ Stage = Union[
|
|
665
665
|
TriggerStage,
|
666
666
|
EmptyStage,
|
667
667
|
]
|
668
|
+
|
669
|
+
|
670
|
+
# TODO: Not implement this stages yet
|
671
|
+
class ParallelStage(BaseModel):
|
672
|
+
parallel: list[Stage]
|
673
|
+
max_parallel_core: int = Field(default=2)
|
674
|
+
|
675
|
+
|
676
|
+
# TODO: Not implement this stages yet
|
677
|
+
class ForEachStage(BaseModel):
|
678
|
+
foreach: list[str]
|
679
|
+
stages: list[Stage]
|
ddeutil/workflow/utils.py
CHANGED
@@ -21,10 +21,9 @@ from zoneinfo import ZoneInfo
|
|
21
21
|
from ddeutil.core import hash_str
|
22
22
|
|
23
23
|
from .__types import DictData, Matrix
|
24
|
-
from .conf import config
|
25
24
|
|
26
25
|
T = TypeVar("T")
|
27
|
-
|
26
|
+
UTC = ZoneInfo("UTC")
|
28
27
|
logger = logging.getLogger("ddeutil.workflow")
|
29
28
|
|
30
29
|
|
@@ -37,7 +36,7 @@ def get_dt_now(
|
|
37
36
|
:param offset:
|
38
37
|
:return: The current datetime object that use an input timezone or UTC.
|
39
38
|
"""
|
40
|
-
return datetime.now(tz=(tz or
|
39
|
+
return datetime.now(tz=(tz or UTC)) - timedelta(seconds=offset)
|
41
40
|
|
42
41
|
|
43
42
|
def get_diff_sec(
|
@@ -52,17 +51,42 @@ def get_diff_sec(
|
|
52
51
|
"""
|
53
52
|
return round(
|
54
53
|
(
|
55
|
-
dt
|
56
|
-
- datetime.now(tz=(tz or ZoneInfo("UTC")))
|
57
|
-
- timedelta(seconds=offset)
|
54
|
+
dt - datetime.now(tz=(tz or UTC)) - timedelta(seconds=offset)
|
58
55
|
).total_seconds()
|
59
56
|
)
|
60
57
|
|
61
58
|
|
62
|
-
def
|
59
|
+
def reach_next_minute(
|
60
|
+
dt: datetime, tz: ZoneInfo | None = None, offset: float = 0.0
|
61
|
+
) -> bool:
|
62
|
+
"""Check this datetime object is not in range of minute level on the current
|
63
|
+
datetime.
|
64
|
+
"""
|
65
|
+
diff: float = (
|
66
|
+
dt.replace(second=0, microsecond=0)
|
67
|
+
- (
|
68
|
+
get_dt_now(tz=(tz or UTC), offset=offset).replace(
|
69
|
+
second=0, microsecond=0
|
70
|
+
)
|
71
|
+
)
|
72
|
+
).total_seconds()
|
73
|
+
if diff >= 60:
|
74
|
+
return True
|
75
|
+
elif diff >= 0:
|
76
|
+
return False
|
77
|
+
|
78
|
+
raise ValueError(
|
79
|
+
"Check reach the next minute function should check a datetime that not "
|
80
|
+
"less than the current date"
|
81
|
+
)
|
82
|
+
|
83
|
+
|
84
|
+
def wait_to_next_minute(
|
85
|
+
dt: datetime, second: float = 0
|
86
|
+
) -> None: # pragma: no cov
|
63
87
|
"""Wait with sleep to the next minute with an offset second value."""
|
64
|
-
future =
|
65
|
-
time.sleep((future -
|
88
|
+
future = dt.replace(second=0, microsecond=0) + timedelta(minutes=1)
|
89
|
+
time.sleep((future - dt).total_seconds() + second)
|
66
90
|
|
67
91
|
|
68
92
|
def delay(second: float = 0) -> None: # pragma: no cov
|
@@ -92,6 +116,8 @@ def gen_id(
|
|
92
116
|
|
93
117
|
:rtype: str
|
94
118
|
"""
|
119
|
+
from .conf import config
|
120
|
+
|
95
121
|
if not isinstance(value, str):
|
96
122
|
value: str = str(value)
|
97
123
|
|
@@ -177,7 +203,7 @@ def batch(iterable: Iterator[Any], n: int) -> Iterator[Any]:
|
|
177
203
|
"""Batch data into iterators of length n. The last batch may be shorter.
|
178
204
|
|
179
205
|
Example:
|
180
|
-
>>> for b in batch('ABCDEFG', 3):
|
206
|
+
>>> for b in batch(iter('ABCDEFG'), 3):
|
181
207
|
... print(list(b))
|
182
208
|
['A', 'B', 'C']
|
183
209
|
['D', 'E', 'F']
|
ddeutil/workflow/workflow.py
CHANGED
@@ -29,7 +29,8 @@ from concurrent.futures import (
|
|
29
29
|
)
|
30
30
|
from dataclasses import field
|
31
31
|
from datetime import datetime, timedelta
|
32
|
-
from
|
32
|
+
from enum import Enum
|
33
|
+
from functools import partial, total_ordering
|
33
34
|
from heapq import heappop, heappush
|
34
35
|
from queue import Queue
|
35
36
|
from textwrap import dedent
|
@@ -53,21 +54,33 @@ from .utils import (
|
|
53
54
|
cut_id,
|
54
55
|
gen_id,
|
55
56
|
get_dt_now,
|
56
|
-
|
57
|
+
reach_next_minute,
|
58
|
+
wait_to_next_minute,
|
57
59
|
)
|
58
60
|
|
59
61
|
logger = get_logger("ddeutil.workflow")
|
60
62
|
|
61
63
|
__all__: TupleStr = (
|
62
|
-
"Workflow",
|
63
64
|
"Release",
|
64
65
|
"ReleaseQueue",
|
66
|
+
"ReleaseType",
|
67
|
+
"Workflow",
|
65
68
|
"WorkflowTask",
|
66
69
|
)
|
67
70
|
|
68
71
|
|
72
|
+
class ReleaseType(str, Enum):
|
73
|
+
"""Release Type Enum support the type field on the Release dataclass."""
|
74
|
+
|
75
|
+
DEFAULT: str = "manual"
|
76
|
+
TASK: str = "task"
|
77
|
+
POKE: str = "poking"
|
78
|
+
|
79
|
+
|
69
80
|
@total_ordering
|
70
|
-
@dataclass(
|
81
|
+
@dataclass(
|
82
|
+
config=ConfigDict(arbitrary_types_allowed=True, use_enum_values=True)
|
83
|
+
)
|
71
84
|
class Release:
|
72
85
|
"""Release Pydantic dataclass object that use for represent
|
73
86
|
the release data that use with the `workflow.release` method."""
|
@@ -76,7 +89,7 @@ class Release:
|
|
76
89
|
offset: float
|
77
90
|
end_date: datetime
|
78
91
|
runner: CronRunner
|
79
|
-
type:
|
92
|
+
type: ReleaseType = field(default=ReleaseType.DEFAULT)
|
80
93
|
|
81
94
|
def __repr__(self) -> str:
|
82
95
|
return repr(f"{self.date:%Y-%m-%d %H:%M:%S}")
|
@@ -94,13 +107,16 @@ class Release:
|
|
94
107
|
"""
|
95
108
|
if isinstance(dt, str):
|
96
109
|
dt: datetime = datetime.fromisoformat(dt)
|
110
|
+
elif not isinstance(dt, datetime):
|
111
|
+
raise TypeError(
|
112
|
+
"The `from_dt` need argument type be str or datetime only."
|
113
|
+
)
|
97
114
|
|
98
115
|
return cls(
|
99
116
|
date=dt,
|
100
117
|
offset=0,
|
101
118
|
end_date=dt + timedelta(days=1),
|
102
119
|
runner=CronJob("* * * * *").schedule(dt.replace(tzinfo=config.tz)),
|
103
|
-
type="manual",
|
104
120
|
)
|
105
121
|
|
106
122
|
def __eq__(self, other: Release | datetime) -> bool:
|
@@ -174,13 +190,6 @@ class ReleaseQueue:
|
|
174
190
|
|
175
191
|
:rtype: bool
|
176
192
|
"""
|
177
|
-
# NOTE: Old logic to peeking the first release from waiting queue.
|
178
|
-
#
|
179
|
-
# first_value: Release = heappop(self.queue)
|
180
|
-
# heappush(self.queue, first_value)
|
181
|
-
#
|
182
|
-
# return first_value
|
183
|
-
#
|
184
193
|
return self.queue[0]
|
185
194
|
|
186
195
|
def check_queue(self, value: Release | datetime) -> bool:
|
@@ -477,7 +486,7 @@ class Workflow(BaseModel):
|
|
477
486
|
*,
|
478
487
|
run_id: str | None = None,
|
479
488
|
log: type[Log] = None,
|
480
|
-
queue: ReleaseQueue |
|
489
|
+
queue: ReleaseQueue | None = None,
|
481
490
|
override_log_name: str | None = None,
|
482
491
|
) -> Result:
|
483
492
|
"""Release the workflow execution with overriding parameter with the
|
@@ -487,7 +496,7 @@ class Workflow(BaseModel):
|
|
487
496
|
This method allow workflow use log object to save the execution
|
488
497
|
result to log destination like file log to the local `/logs` directory.
|
489
498
|
|
490
|
-
|
499
|
+
Steps:
|
491
500
|
- Initialize ReleaseQueue and Release if they do not pass.
|
492
501
|
- Create release data for pass to parameter templating function.
|
493
502
|
- Execute this workflow with mapping release data to its parameters.
|
@@ -497,7 +506,7 @@ class Workflow(BaseModel):
|
|
497
506
|
|
498
507
|
:param release: A release datetime or Release object.
|
499
508
|
:param params: A workflow parameter that pass to execute method.
|
500
|
-
:param queue: A
|
509
|
+
:param queue: A ReleaseQueue that use for mark complete.
|
501
510
|
:param run_id: A workflow running ID for this release.
|
502
511
|
:param log: A log class that want to save the execution result.
|
503
512
|
:param queue: A ReleaseQueue object.
|
@@ -510,13 +519,14 @@ class Workflow(BaseModel):
|
|
510
519
|
name: str = override_log_name or self.name
|
511
520
|
run_id: str = run_id or gen_id(name, unique=True)
|
512
521
|
rs_release: Result = Result(run_id=run_id)
|
513
|
-
rs_release_type: str = "release"
|
514
522
|
|
515
|
-
|
516
|
-
|
517
|
-
|
523
|
+
if queue is not None and not isinstance(queue, ReleaseQueue):
|
524
|
+
raise TypeError(
|
525
|
+
"The queue argument should be ReleaseQueue object only."
|
526
|
+
)
|
518
527
|
|
519
528
|
# VALIDATE: Change release value to Release object.
|
529
|
+
rs_release_type: str = "release"
|
520
530
|
if isinstance(release, datetime):
|
521
531
|
rs_release_type: str = "datetime"
|
522
532
|
release: Release = Release.from_dt(release)
|
@@ -548,24 +558,26 @@ class Workflow(BaseModel):
|
|
548
558
|
)
|
549
559
|
|
550
560
|
rs.set_parent_run_id(run_id)
|
551
|
-
rs_log: Log = log.model_validate(
|
552
|
-
{
|
553
|
-
"name": name,
|
554
|
-
"release": release.date,
|
555
|
-
"type": release.type,
|
556
|
-
"context": rs.context,
|
557
|
-
"parent_run_id": rs.parent_run_id,
|
558
|
-
"run_id": rs.run_id,
|
559
|
-
}
|
560
|
-
)
|
561
561
|
|
562
562
|
# NOTE: Saving execution result to destination of the input log object.
|
563
563
|
logger.debug(f"({cut_id(run_id)}) [LOG]: Writing log: {name!r}.")
|
564
|
-
|
564
|
+
(
|
565
|
+
log.model_validate(
|
566
|
+
{
|
567
|
+
"name": name,
|
568
|
+
"release": release.date,
|
569
|
+
"type": release.type,
|
570
|
+
"context": rs.context,
|
571
|
+
"parent_run_id": rs.parent_run_id,
|
572
|
+
"run_id": rs.run_id,
|
573
|
+
}
|
574
|
+
).save(excluded=None)
|
575
|
+
)
|
565
576
|
|
566
577
|
# NOTE: Remove this release from running.
|
567
|
-
queue
|
568
|
-
|
578
|
+
if queue is not None:
|
579
|
+
queue.remove_running(release)
|
580
|
+
queue.mark_complete(release)
|
569
581
|
|
570
582
|
# NOTE: Remove the params key from the result context for deduplicate.
|
571
583
|
context: dict[str, Any] = rs.context
|
@@ -594,8 +606,16 @@ class Workflow(BaseModel):
|
|
594
606
|
*,
|
595
607
|
force_run: bool = False,
|
596
608
|
) -> ReleaseQueue:
|
597
|
-
"""Generate
|
598
|
-
the
|
609
|
+
"""Generate Release from all on values from the on field and store them
|
610
|
+
to the ReleaseQueue object.
|
611
|
+
|
612
|
+
Steps:
|
613
|
+
- For-loop all the on value in the on field.
|
614
|
+
- Create Release object from the current date that not reach the end
|
615
|
+
date.
|
616
|
+
- Check this release do not store on the release queue object.
|
617
|
+
Generate the next date if it exists.
|
618
|
+
- Push this release to the release queue
|
599
619
|
|
600
620
|
:param offset: An offset in second unit for time travel.
|
601
621
|
:param end_date: An end datetime object.
|
@@ -621,7 +641,7 @@ class Workflow(BaseModel):
|
|
621
641
|
offset=offset,
|
622
642
|
end_date=end_date,
|
623
643
|
runner=runner,
|
624
|
-
type=
|
644
|
+
type=ReleaseType.POKE,
|
625
645
|
)
|
626
646
|
|
627
647
|
while queue.check_queue(workflow_release) or (
|
@@ -633,7 +653,7 @@ class Workflow(BaseModel):
|
|
633
653
|
offset=offset,
|
634
654
|
end_date=end_date,
|
635
655
|
runner=runner,
|
636
|
-
type=
|
656
|
+
type=ReleaseType.POKE,
|
637
657
|
)
|
638
658
|
|
639
659
|
if runner.date > end_date:
|
@@ -662,6 +682,9 @@ class Workflow(BaseModel):
|
|
662
682
|
This method will observe its schedule that nearing to run with the
|
663
683
|
``self.release()`` method.
|
664
684
|
|
685
|
+
The limitation of this method is not allow run a date that less
|
686
|
+
than the current date.
|
687
|
+
|
665
688
|
:param start_date: A start datetime object.
|
666
689
|
:param params: A parameters that want to pass to the release method.
|
667
690
|
:param run_id: A workflow running ID for this poke.
|
@@ -678,6 +701,12 @@ class Workflow(BaseModel):
|
|
678
701
|
log: type[Log] = log or get_log()
|
679
702
|
run_id: str = run_id or gen_id(self.name, unique=True)
|
680
703
|
|
704
|
+
# VALIDATE: Check the periods value should gather than 0.
|
705
|
+
if periods <= 0:
|
706
|
+
raise WorkflowException(
|
707
|
+
"The period of poking should be int and grater or equal than 1."
|
708
|
+
)
|
709
|
+
|
681
710
|
# NOTE: If this workflow does not set the on schedule, it will return
|
682
711
|
# empty result.
|
683
712
|
if len(self.on) == 0:
|
@@ -687,23 +716,25 @@ class Workflow(BaseModel):
|
|
687
716
|
)
|
688
717
|
return []
|
689
718
|
|
690
|
-
|
691
|
-
|
692
|
-
|
693
|
-
|
719
|
+
# NOTE: Create the current date that change microsecond to 0
|
720
|
+
current_date: datetime = datetime.now(tz=config.tz).replace(
|
721
|
+
microsecond=0
|
722
|
+
)
|
694
723
|
|
695
724
|
# NOTE: Create start_date and offset variables.
|
696
|
-
current_date: datetime = datetime.now(tz=config.tz)
|
697
|
-
|
698
725
|
if start_date and start_date <= current_date:
|
699
|
-
start_date = start_date.replace(tzinfo=config.tz)
|
726
|
+
start_date = start_date.replace(tzinfo=config.tz).replace(
|
727
|
+
microsecond=0
|
728
|
+
)
|
700
729
|
offset: float = (current_date - start_date).total_seconds()
|
701
730
|
else:
|
731
|
+
# NOTE: Force change start date if it gathers than the current date,
|
732
|
+
# or it does not pass to this method.
|
702
733
|
start_date: datetime = current_date
|
703
734
|
offset: float = 0
|
704
735
|
|
705
|
-
# NOTE:
|
706
|
-
# value.
|
736
|
+
# NOTE: The end date is using to stop generate queue with an input
|
737
|
+
# periods value.
|
707
738
|
end_date: datetime = start_date + timedelta(minutes=periods)
|
708
739
|
|
709
740
|
logger.info(
|
@@ -715,17 +746,17 @@ class Workflow(BaseModel):
|
|
715
746
|
results: list[Result] = []
|
716
747
|
|
717
748
|
# NOTE: Create empty ReleaseQueue object.
|
718
|
-
|
749
|
+
q: ReleaseQueue = ReleaseQueue()
|
719
750
|
|
720
|
-
# NOTE:
|
721
|
-
|
722
|
-
|
723
|
-
end_date=
|
724
|
-
queue=wf_queue,
|
725
|
-
log=log,
|
726
|
-
force_run=force_run,
|
751
|
+
# NOTE: Create reusable partial function and add Release to the release
|
752
|
+
# queue object.
|
753
|
+
partial_queue = partial(
|
754
|
+
self.queue, offset, end_date, log=log, force_run=force_run
|
727
755
|
)
|
728
|
-
|
756
|
+
partial_queue(q)
|
757
|
+
|
758
|
+
# NOTE: Return the empty result if it does not have any Release.
|
759
|
+
if not q.is_queued:
|
729
760
|
logger.info(
|
730
761
|
f"({cut_id(run_id)}) [POKING]: {self.name!r} does not have "
|
731
762
|
f"any queue."
|
@@ -741,34 +772,27 @@ class Workflow(BaseModel):
|
|
741
772
|
|
742
773
|
futures: list[Future] = []
|
743
774
|
|
744
|
-
while
|
775
|
+
while q.is_queued:
|
745
776
|
|
746
|
-
# NOTE: Pop the latest Release object from queue.
|
747
|
-
release: Release = heappop(
|
777
|
+
# NOTE: Pop the latest Release object from the release queue.
|
778
|
+
release: Release = heappop(q.queue)
|
748
779
|
|
749
|
-
if (
|
750
|
-
release.date - get_dt_now(tz=config.tz, offset=offset)
|
751
|
-
).total_seconds() > 60:
|
780
|
+
if reach_next_minute(release.date, tz=config.tz, offset=offset):
|
752
781
|
logger.debug(
|
753
|
-
f"({cut_id(run_id)}) [POKING]:
|
754
|
-
f"release
|
782
|
+
f"({cut_id(run_id)}) [POKING]: The latest release, "
|
783
|
+
f"{release.date:%Y-%m-%d %H:%M:%S}, is not able to run "
|
784
|
+
f"on this minute"
|
755
785
|
)
|
756
|
-
heappush(
|
757
|
-
|
786
|
+
heappush(q.queue, release)
|
787
|
+
wait_to_next_minute(get_dt_now(tz=config.tz, offset=offset))
|
758
788
|
|
759
789
|
# WARNING: I already call queue poking again because issue
|
760
790
|
# about the every minute crontab.
|
761
|
-
|
762
|
-
offset,
|
763
|
-
end_date,
|
764
|
-
queue=wf_queue,
|
765
|
-
log=log,
|
766
|
-
force_run=force_run,
|
767
|
-
)
|
791
|
+
partial_queue(q)
|
768
792
|
continue
|
769
793
|
|
770
794
|
# NOTE: Push the latest Release to the running queue.
|
771
|
-
heappush(
|
795
|
+
heappush(q.running, release)
|
772
796
|
|
773
797
|
futures.append(
|
774
798
|
executor.submit(
|
@@ -776,17 +800,11 @@ class Workflow(BaseModel):
|
|
776
800
|
release=release,
|
777
801
|
params=params,
|
778
802
|
log=log,
|
779
|
-
queue=
|
803
|
+
queue=q,
|
780
804
|
)
|
781
805
|
)
|
782
806
|
|
783
|
-
|
784
|
-
offset,
|
785
|
-
end_date,
|
786
|
-
queue=wf_queue,
|
787
|
-
log=log,
|
788
|
-
force_run=force_run,
|
789
|
-
)
|
807
|
+
partial_queue(q)
|
790
808
|
|
791
809
|
# WARNING: This poking method does not allow to use fail-fast
|
792
810
|
# logic to catching parallel execution result.
|
@@ -1148,20 +1166,31 @@ class WorkflowTask:
|
|
1148
1166
|
release: datetime | Release | None = None,
|
1149
1167
|
run_id: str | None = None,
|
1150
1168
|
log: type[Log] = None,
|
1151
|
-
queue: ReleaseQueue |
|
1169
|
+
queue: ReleaseQueue | None = None,
|
1152
1170
|
) -> Result:
|
1153
1171
|
"""Release the workflow task data.
|
1154
1172
|
|
1155
1173
|
:param release: A release datetime or Release object.
|
1156
1174
|
:param run_id: A workflow running ID for this release.
|
1157
1175
|
:param log: A log class that want to save the execution result.
|
1158
|
-
:param queue: A ReleaseQueue object.
|
1176
|
+
:param queue: A ReleaseQueue object that use to mark complete.
|
1159
1177
|
|
1160
1178
|
:rtype: Result
|
1161
1179
|
"""
|
1162
1180
|
log: type[Log] = log or get_log()
|
1163
1181
|
|
1164
1182
|
if release is None:
|
1183
|
+
|
1184
|
+
if queue is None:
|
1185
|
+
raise ValueError(
|
1186
|
+
"If pass None release value, you should to pass the queue"
|
1187
|
+
"for generate this release."
|
1188
|
+
)
|
1189
|
+
elif not isinstance(queue, ReleaseQueue):
|
1190
|
+
raise TypeError(
|
1191
|
+
"The queue argument should be ReleaseQueue object only."
|
1192
|
+
)
|
1193
|
+
|
1165
1194
|
if queue.check_queue(self.runner.date):
|
1166
1195
|
release = self.runner.next
|
1167
1196
|
|
@@ -1170,6 +1199,7 @@ class WorkflowTask:
|
|
1170
1199
|
else:
|
1171
1200
|
release = self.runner.date
|
1172
1201
|
|
1202
|
+
# NOTE: Call the workflow release method.
|
1173
1203
|
return self.workflow.release(
|
1174
1204
|
release=release,
|
1175
1205
|
params=self.values,
|
@@ -1186,8 +1216,9 @@ class WorkflowTask:
|
|
1186
1216
|
log: type[Log],
|
1187
1217
|
*,
|
1188
1218
|
force_run: bool = False,
|
1189
|
-
):
|
1190
|
-
"""Generate Release to
|
1219
|
+
) -> ReleaseQueue:
|
1220
|
+
"""Generate Release from the runner field and store it to the
|
1221
|
+
ReleaseQueue object.
|
1191
1222
|
|
1192
1223
|
:param end_date: An end datetime object.
|
1193
1224
|
:param queue: A workflow queue object.
|
@@ -1205,7 +1236,7 @@ class WorkflowTask:
|
|
1205
1236
|
offset=0,
|
1206
1237
|
end_date=end_date,
|
1207
1238
|
runner=self.runner,
|
1208
|
-
type=
|
1239
|
+
type=ReleaseType.TASK,
|
1209
1240
|
)
|
1210
1241
|
|
1211
1242
|
while queue.check_queue(workflow_release) or (
|
@@ -1217,7 +1248,7 @@ class WorkflowTask:
|
|
1217
1248
|
offset=0,
|
1218
1249
|
end_date=end_date,
|
1219
1250
|
runner=self.runner,
|
1220
|
-
type=
|
1251
|
+
type=ReleaseType.TASK,
|
1221
1252
|
)
|
1222
1253
|
|
1223
1254
|
if self.runner.date > end_date:
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.2
|
2
2
|
Name: ddeutil-workflow
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.31
|
4
4
|
Summary: Lightweight workflow orchestration
|
5
5
|
Author-email: ddeutils <korawich.anu@gmail.com>
|
6
6
|
License: MIT
|
@@ -192,6 +192,15 @@ The above workflow template is main executor pipeline that you want to do. If yo
|
|
192
192
|
want to schedule this workflow, you want to dynamic its parameters change base on
|
193
193
|
execution time such as `run-date` should change base on that workflow running date.
|
194
194
|
|
195
|
+
```python
|
196
|
+
from ddeutil.workflow import Workflow, Result
|
197
|
+
|
198
|
+
workflow: Workflow = Workflow.from_loader('run-py-local')
|
199
|
+
result: Result = workflow.execute(
|
200
|
+
params={"source-extract": "USD-THB", "asat-dt": "2024-01-01"}
|
201
|
+
)
|
202
|
+
```
|
203
|
+
|
195
204
|
So, this package provide the `Schedule` template for this action.
|
196
205
|
|
197
206
|
```yaml
|
@@ -0,0 +1,25 @@
|
|
1
|
+
ddeutil/workflow/__about__.py,sha256=3Fv8LNxLB3UmoMSQqDZv556u3eR-OYBH8pw9kM6FeE4,28
|
2
|
+
ddeutil/workflow/__cron.py,sha256=3i-wmjTlh0ADCzN9pLKaWHzJkXzC72aIBmVEQSbyCCE,26895
|
3
|
+
ddeutil/workflow/__init__.py,sha256=pRIZIGwC7Xs8Ur7-jHPIAMLriD5If9zOPc-ZmKZS2XQ,1678
|
4
|
+
ddeutil/workflow/__types.py,sha256=CK1jfzyHP9P-MB0ElhpJZ59ZFGJC9MkQuAop5739_9k,4304
|
5
|
+
ddeutil/workflow/conf.py,sha256=6yGbSi69lsccYgnrwTzdjdPhU54hUop2e1GjBNres08,17663
|
6
|
+
ddeutil/workflow/cron.py,sha256=j8EeoHst70toRfnD_frix41vrI-eLYVJkZ9yeJtpfnI,8871
|
7
|
+
ddeutil/workflow/exceptions.py,sha256=5ghT443VLq0IeU87loHNEqqrrrctklP7YfxwJ51ImWU,949
|
8
|
+
ddeutil/workflow/hook.py,sha256=MgZFlTGvaRSBrTouZGlxwYpKQoKDOT26PNhESeL3LY0,5469
|
9
|
+
ddeutil/workflow/job.py,sha256=XcewyALsLYYq94ycF6mkj3Ydr6if683z7t1oBqEVInE,24290
|
10
|
+
ddeutil/workflow/params.py,sha256=AJLiTaF6lG37SvzyniTCug5-TgZTmNCn5fJXb-CCcqM,5707
|
11
|
+
ddeutil/workflow/result.py,sha256=8LItqF-Xe6pAAWkAsY_QFkKBOA0fEBh97I2og3CZsPc,3409
|
12
|
+
ddeutil/workflow/scheduler.py,sha256=2Y_ewAP1iQKgD81i6H6fXuJLCVqLocumEmCG2SomEqg,22214
|
13
|
+
ddeutil/workflow/stage.py,sha256=glAhvgvyQ98n2JLUBU8MUs48FEPkfU1fvh3Wwi9PSCg,24293
|
14
|
+
ddeutil/workflow/templates.py,sha256=bVU_8gnMQmdhhw3W28ZqwmpEaOx10Nx_aauqiLS0lqg,10807
|
15
|
+
ddeutil/workflow/utils.py,sha256=rTDQKaaber7cRqzJjWpCP9OTbarti1UMKdLgH6VRjFM,6709
|
16
|
+
ddeutil/workflow/workflow.py,sha256=ET1otR5VcfnOMoNiW7EMb1_wIaxNw9yWsBXS5kVWG9s,43428
|
17
|
+
ddeutil/workflow/api/__init__.py,sha256=F53NMBWtb9IKaDWkPU5KvybGGfKAcbehgn6TLBwHuuM,21
|
18
|
+
ddeutil/workflow/api/api.py,sha256=Ma9R8yuQAhowG_hox-k53swFsf5IAvheEnSxNQ-8DaQ,4039
|
19
|
+
ddeutil/workflow/api/repeat.py,sha256=zyvsrXKk-3-_N8ZRZSki0Mueshugum2jtqctEOp9QSc,4927
|
20
|
+
ddeutil/workflow/api/route.py,sha256=v96jNbgjM1cJ2MpVSRWs2kgRqF8DQElEBdRZrVFEpEw,8578
|
21
|
+
ddeutil_workflow-0.0.31.dist-info/LICENSE,sha256=nGFZ1QEhhhWeMHf9n99_fdt4vQaXS29xWKxt-OcLywk,1085
|
22
|
+
ddeutil_workflow-0.0.31.dist-info/METADATA,sha256=-cDcCfcV-4_dU_91GSy5sdV-ZkEWbdk7wDmAJB24puU,15090
|
23
|
+
ddeutil_workflow-0.0.31.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
24
|
+
ddeutil_workflow-0.0.31.dist-info/top_level.txt,sha256=m9M6XeSWDwt_yMsmH6gcOjHZVK5O0-vgtNBuncHjzW4,8
|
25
|
+
ddeutil_workflow-0.0.31.dist-info/RECORD,,
|
@@ -1,25 +0,0 @@
|
|
1
|
-
ddeutil/workflow/__about__.py,sha256=zSy9Xk11PPZkgJ5Db1_kZp2yzt5inkEHVJWwFHPWlmk,28
|
2
|
-
ddeutil/workflow/__cron.py,sha256=uA8XcbY_GwA9rJSHaHUaXaJyGDObJN0ZeYlJSinL8y8,26880
|
3
|
-
ddeutil/workflow/__init__.py,sha256=dghn2lFl3Own4Pyq7SFHu-FMymOgLontJ6aCfxea9h4,1606
|
4
|
-
ddeutil/workflow/__types.py,sha256=CK1jfzyHP9P-MB0ElhpJZ59ZFGJC9MkQuAop5739_9k,4304
|
5
|
-
ddeutil/workflow/conf.py,sha256=7lj_Im9jsa95fWUo19Q4-ZAcHa8Pu1HW-vaLgvrjNUM,17559
|
6
|
-
ddeutil/workflow/cron.py,sha256=OLgniUxmrn65gzckk-uTmE2Pk1enJJyjYUKVeBbDQz0,7522
|
7
|
-
ddeutil/workflow/exceptions.py,sha256=XUnpJSuxOyataClP0w_gpYjzn-NIwZK2BHro-J7Yw24,895
|
8
|
-
ddeutil/workflow/hook.py,sha256=MgZFlTGvaRSBrTouZGlxwYpKQoKDOT26PNhESeL3LY0,5469
|
9
|
-
ddeutil/workflow/job.py,sha256=XcewyALsLYYq94ycF6mkj3Ydr6if683z7t1oBqEVInE,24290
|
10
|
-
ddeutil/workflow/params.py,sha256=svCjmFgEhim8yFJVjZhFmKP8JqTDHQ5EPhwJHVuDGno,5289
|
11
|
-
ddeutil/workflow/result.py,sha256=k4pcj5KjbEcEPymsEUXeGY4gyLMfPkMTO6YDrAtfk7Q,3408
|
12
|
-
ddeutil/workflow/scheduler.py,sha256=f3d7c5SVgY5Q1JsHQ6cH513CJmJkh4l8YcKAGYudJRc,20426
|
13
|
-
ddeutil/workflow/stage.py,sha256=wn8CARTvFJY4ZK1SwjzH8sKoMRz_eIeSGUMgnDWNi6g,24031
|
14
|
-
ddeutil/workflow/templates.py,sha256=bVU_8gnMQmdhhw3W28ZqwmpEaOx10Nx_aauqiLS0lqg,10807
|
15
|
-
ddeutil/workflow/utils.py,sha256=8LTqpvRPfrEYxsxhwszk6GKkyjrswxnwF3r_9vE8szw,6059
|
16
|
-
ddeutil/workflow/workflow.py,sha256=ZLbG-K2gSNAsDGiHPjbtJd7rsEFf6jfVGAVB_9jpy84,42103
|
17
|
-
ddeutil/workflow/api/__init__.py,sha256=F53NMBWtb9IKaDWkPU5KvybGGfKAcbehgn6TLBwHuuM,21
|
18
|
-
ddeutil/workflow/api/api.py,sha256=Ma9R8yuQAhowG_hox-k53swFsf5IAvheEnSxNQ-8DaQ,4039
|
19
|
-
ddeutil/workflow/api/repeat.py,sha256=zyvsrXKk-3-_N8ZRZSki0Mueshugum2jtqctEOp9QSc,4927
|
20
|
-
ddeutil/workflow/api/route.py,sha256=v96jNbgjM1cJ2MpVSRWs2kgRqF8DQElEBdRZrVFEpEw,8578
|
21
|
-
ddeutil_workflow-0.0.30.dist-info/LICENSE,sha256=nGFZ1QEhhhWeMHf9n99_fdt4vQaXS29xWKxt-OcLywk,1085
|
22
|
-
ddeutil_workflow-0.0.30.dist-info/METADATA,sha256=zbVHOL41qpFRG83SacZVYK9tS2JRTCM61cpnSXty9LU,14868
|
23
|
-
ddeutil_workflow-0.0.30.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
24
|
-
ddeutil_workflow-0.0.30.dist-info/top_level.txt,sha256=m9M6XeSWDwt_yMsmH6gcOjHZVK5O0-vgtNBuncHjzW4,8
|
25
|
-
ddeutil_workflow-0.0.30.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|