ddeutil-workflow 0.0.23__py3-none-any.whl → 0.0.24__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 +6 -0
- ddeutil/workflow/__init__.py +8 -7
- ddeutil/workflow/api.py +2 -2
- ddeutil/workflow/cli.py +2 -2
- ddeutil/workflow/conf.py +18 -2
- ddeutil/workflow/scheduler.py +189 -154
- ddeutil/workflow/stage.py +5 -0
- ddeutil/workflow/utils.py +0 -4
- ddeutil/workflow/workflow.py +121 -138
- {ddeutil_workflow-0.0.23.dist-info → ddeutil_workflow-0.0.24.dist-info}/METADATA +4 -3
- ddeutil_workflow-0.0.24.dist-info/RECORD +24 -0
- ddeutil_workflow-0.0.23.dist-info/RECORD +0 -24
- /ddeutil/workflow/{on.py → cron.py} +0 -0
- {ddeutil_workflow-0.0.23.dist-info → ddeutil_workflow-0.0.24.dist-info}/LICENSE +0 -0
- {ddeutil_workflow-0.0.23.dist-info → ddeutil_workflow-0.0.24.dist-info}/WHEEL +0 -0
- {ddeutil_workflow-0.0.23.dist-info → ddeutil_workflow-0.0.24.dist-info}/entry_points.txt +0 -0
- {ddeutil_workflow-0.0.23.dist-info → ddeutil_workflow-0.0.24.dist-info}/top_level.txt +0 -0
ddeutil/workflow/__about__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__: str = "0.0.
|
1
|
+
__version__: str = "0.0.24"
|
ddeutil/workflow/__cron.py
CHANGED
@@ -736,6 +736,12 @@ class CronRunner:
|
|
736
736
|
self.is_year: bool = isinstance(cron, CronJobYear)
|
737
737
|
self.reset_flag: bool = True
|
738
738
|
|
739
|
+
def __repr__(self) -> str:
|
740
|
+
return (
|
741
|
+
f"{self.__class__.__name__}(CronJob('{self.cron}'), "
|
742
|
+
f"{self.date:%Y-%m-%d %H:%M:%S}, tz='{self.tz}')"
|
743
|
+
)
|
744
|
+
|
739
745
|
def reset(self) -> None:
|
740
746
|
"""Resets the iterator to start time."""
|
741
747
|
self.date: datetime = self.__start_date
|
ddeutil/workflow/__init__.py
CHANGED
@@ -3,11 +3,17 @@
|
|
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
7
|
from .conf import (
|
7
8
|
Config,
|
8
9
|
FileLog,
|
9
10
|
Loader,
|
10
11
|
)
|
12
|
+
from .cron import (
|
13
|
+
On,
|
14
|
+
YearOn,
|
15
|
+
interval2crontab,
|
16
|
+
)
|
11
17
|
from .exceptions import (
|
12
18
|
JobException,
|
13
19
|
ParamValueException,
|
@@ -19,11 +25,6 @@ from .job import (
|
|
19
25
|
Job,
|
20
26
|
Strategy,
|
21
27
|
)
|
22
|
-
from .on import (
|
23
|
-
On,
|
24
|
-
YearOn,
|
25
|
-
interval2crontab,
|
26
|
-
)
|
27
28
|
from .params import (
|
28
29
|
ChoiceParam,
|
29
30
|
DatetimeParam,
|
@@ -35,6 +36,7 @@ from .result import Result
|
|
35
36
|
from .scheduler import (
|
36
37
|
Schedule,
|
37
38
|
WorkflowSchedule,
|
39
|
+
schedule_runner,
|
38
40
|
)
|
39
41
|
from .stage import (
|
40
42
|
BashStage,
|
@@ -68,11 +70,10 @@ from .utils import (
|
|
68
70
|
map_post_filter,
|
69
71
|
not_in_template,
|
70
72
|
param2template,
|
71
|
-
queue2str,
|
72
73
|
str2template,
|
73
74
|
tag,
|
74
75
|
)
|
75
76
|
from .workflow import (
|
76
77
|
Workflow,
|
77
|
-
|
78
|
+
WorkflowTask,
|
78
79
|
)
|
ddeutil/workflow/api.py
CHANGED
@@ -23,7 +23,7 @@ from pydantic import BaseModel
|
|
23
23
|
from .__about__ import __version__
|
24
24
|
from .conf import config, get_logger
|
25
25
|
from .repeat import repeat_at, repeat_every
|
26
|
-
from .workflow import
|
26
|
+
from .workflow import WorkflowTask
|
27
27
|
|
28
28
|
load_dotenv()
|
29
29
|
logger = get_logger("ddeutil.workflow")
|
@@ -34,7 +34,7 @@ class State(TypedDict):
|
|
34
34
|
upper_result: dict[str, str]
|
35
35
|
scheduler: list[str]
|
36
36
|
workflow_threads: dict[str, Thread]
|
37
|
-
workflow_tasks: list[
|
37
|
+
workflow_tasks: list[WorkflowTask]
|
38
38
|
workflow_queue: dict[str, list[datetime]]
|
39
39
|
workflow_running: dict[str, list[datetime]]
|
40
40
|
|
ddeutil/workflow/cli.py
CHANGED
@@ -73,10 +73,10 @@ def schedule(
|
|
73
73
|
if stop:
|
74
74
|
stop: datetime = stop.astimezone(tz=config.tz)
|
75
75
|
|
76
|
-
from .scheduler import
|
76
|
+
from .scheduler import schedule_runner
|
77
77
|
|
78
78
|
# NOTE: Start running workflow scheduler application.
|
79
|
-
workflow_rs: list[str] =
|
79
|
+
workflow_rs: list[str] = schedule_runner(
|
80
80
|
stop=stop, excluded=excluded, externals=json.loads(externals)
|
81
81
|
)
|
82
82
|
logger.info(f"Application run success: {workflow_rs}")
|
ddeutil/workflow/conf.py
CHANGED
@@ -23,7 +23,7 @@ from pydantic import BaseModel, Field
|
|
23
23
|
from pydantic.functional_validators import model_validator
|
24
24
|
from typing_extensions import Self
|
25
25
|
|
26
|
-
from .__types import DictData
|
26
|
+
from .__types import DictData, TupleStr
|
27
27
|
|
28
28
|
AnyModel = TypeVar("AnyModel", bound=BaseModel)
|
29
29
|
AnyModelType = type[AnyModel]
|
@@ -32,6 +32,19 @@ load_dotenv()
|
|
32
32
|
|
33
33
|
env = os.getenv
|
34
34
|
|
35
|
+
__all__: TupleStr = (
|
36
|
+
"get_logger",
|
37
|
+
"Config",
|
38
|
+
"SimLoad",
|
39
|
+
"Loader",
|
40
|
+
"get_type",
|
41
|
+
"config",
|
42
|
+
"logger",
|
43
|
+
"FileLog",
|
44
|
+
"SQLiteLog",
|
45
|
+
"Log",
|
46
|
+
)
|
47
|
+
|
35
48
|
|
36
49
|
@lru_cache
|
37
50
|
def get_logger(name: str):
|
@@ -107,7 +120,10 @@ class Config:
|
|
107
120
|
os.getenv("WORKFLOW_CORE_MAX_NUM_POKING", "4")
|
108
121
|
)
|
109
122
|
max_on_per_workflow: int = int(
|
110
|
-
env("
|
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")
|
111
127
|
)
|
112
128
|
|
113
129
|
# NOTE: Schedule App
|
ddeutil/workflow/scheduler.py
CHANGED
@@ -4,7 +4,7 @@
|
|
4
4
|
# license information.
|
5
5
|
# ------------------------------------------------------------------------------
|
6
6
|
"""
|
7
|
-
The main schedule running is ``
|
7
|
+
The main schedule running is ``schedule_runner`` function that trigger the
|
8
8
|
multiprocess of ``workflow_control`` function for listing schedules on the
|
9
9
|
config by ``Loader.finds(Schedule)``.
|
10
10
|
|
@@ -21,7 +21,6 @@ value with the on field.
|
|
21
21
|
from __future__ import annotations
|
22
22
|
|
23
23
|
import copy
|
24
|
-
import inspect
|
25
24
|
import logging
|
26
25
|
import time
|
27
26
|
from concurrent.futures import (
|
@@ -31,6 +30,7 @@ from concurrent.futures import (
|
|
31
30
|
)
|
32
31
|
from datetime import datetime, timedelta
|
33
32
|
from functools import wraps
|
33
|
+
from heapq import heappop
|
34
34
|
from textwrap import dedent
|
35
35
|
from threading import Thread
|
36
36
|
from typing import Callable, Optional
|
@@ -51,30 +51,29 @@ except ImportError: # pragma: no cov
|
|
51
51
|
|
52
52
|
from .__cron import CronRunner
|
53
53
|
from .__types import DictData, TupleStr
|
54
|
-
from .conf import Loader, config, get_logger
|
54
|
+
from .conf import FileLog, Loader, Log, config, get_logger
|
55
|
+
from .cron import On
|
55
56
|
from .exceptions import WorkflowException
|
56
|
-
from .on import On
|
57
57
|
from .utils import (
|
58
58
|
batch,
|
59
59
|
delay,
|
60
|
-
queue2str,
|
61
60
|
)
|
62
|
-
from .workflow import Workflow,
|
61
|
+
from .workflow import Workflow, WorkflowQueue, WorkflowRelease, WorkflowTask
|
63
62
|
|
64
63
|
P = ParamSpec("P")
|
65
64
|
logger = get_logger("ddeutil.workflow")
|
66
65
|
|
67
|
-
# NOTE: Adjust logging level on the schedule package.
|
66
|
+
# NOTE: Adjust logging level on the `schedule` package.
|
68
67
|
logging.getLogger("schedule").setLevel(logging.INFO)
|
69
68
|
|
70
69
|
|
71
70
|
__all__: TupleStr = (
|
72
71
|
"Schedule",
|
73
72
|
"WorkflowSchedule",
|
74
|
-
"
|
75
|
-
"
|
76
|
-
"
|
77
|
-
"
|
73
|
+
"schedule_task",
|
74
|
+
"monitor",
|
75
|
+
"schedule_control",
|
76
|
+
"schedule_runner",
|
78
77
|
)
|
79
78
|
|
80
79
|
|
@@ -166,6 +165,58 @@ class WorkflowSchedule(BaseModel):
|
|
166
165
|
|
167
166
|
return value
|
168
167
|
|
168
|
+
def tasks(
|
169
|
+
self,
|
170
|
+
start_date: datetime,
|
171
|
+
queue: dict[str, WorkflowQueue],
|
172
|
+
*,
|
173
|
+
externals: DictData | None = None,
|
174
|
+
) -> list[WorkflowTask]:
|
175
|
+
"""Return the list of WorkflowTask object from the specific input
|
176
|
+
datetime that mapping with the on field.
|
177
|
+
|
178
|
+
This task creation need queue to tracking release date already
|
179
|
+
mapped or not.
|
180
|
+
|
181
|
+
:param start_date: A start date that get from the workflow schedule.
|
182
|
+
:param queue: A mapping of name and list of datetime for queue.
|
183
|
+
:param externals: An external parameters that pass to the Loader object.
|
184
|
+
|
185
|
+
:rtype: list[WorkflowTask]
|
186
|
+
:return: Return the list of WorkflowTask object from the specific
|
187
|
+
input datetime that mapping with the on field.
|
188
|
+
"""
|
189
|
+
workflow_tasks: list[WorkflowTask] = []
|
190
|
+
extras: DictData = externals or {}
|
191
|
+
|
192
|
+
# NOTE: Loading workflow model from the name of workflow.
|
193
|
+
wf: Workflow = Workflow.from_loader(self.name, externals=extras)
|
194
|
+
wf_queue: WorkflowQueue = queue[self.alias]
|
195
|
+
|
196
|
+
# IMPORTANT: Create the default 'on' value if it does not passing
|
197
|
+
# the on field to the Schedule object.
|
198
|
+
ons: list[On] = self.on or wf.on.copy()
|
199
|
+
|
200
|
+
for on in ons:
|
201
|
+
|
202
|
+
# NOTE: Create CronRunner instance from the start_date param.
|
203
|
+
runner: CronRunner = on.generate(start_date)
|
204
|
+
next_running_date = runner.next
|
205
|
+
|
206
|
+
while wf_queue.check_queue(next_running_date):
|
207
|
+
next_running_date = runner.next
|
208
|
+
|
209
|
+
workflow_tasks.append(
|
210
|
+
WorkflowTask(
|
211
|
+
alias=self.alias,
|
212
|
+
workflow=wf,
|
213
|
+
runner=runner,
|
214
|
+
values=self.values,
|
215
|
+
),
|
216
|
+
)
|
217
|
+
|
218
|
+
return workflow_tasks
|
219
|
+
|
169
220
|
|
170
221
|
class Schedule(BaseModel):
|
171
222
|
"""Schedule Pydantic model that use to run with any scheduler package.
|
@@ -227,57 +278,33 @@ class Schedule(BaseModel):
|
|
227
278
|
def tasks(
|
228
279
|
self,
|
229
280
|
start_date: datetime,
|
230
|
-
queue: dict[str,
|
281
|
+
queue: dict[str, WorkflowQueue],
|
231
282
|
*,
|
232
283
|
externals: DictData | None = None,
|
233
|
-
) -> list[
|
234
|
-
"""Return the list of
|
235
|
-
datetime that mapping with the on field.
|
236
|
-
|
237
|
-
This task creation need queue to tracking release date already
|
238
|
-
mapped or not.
|
284
|
+
) -> list[WorkflowTask]:
|
285
|
+
"""Return the list of WorkflowTask object from the specific input
|
286
|
+
datetime that mapping with the on field from workflow schedule model.
|
239
287
|
|
240
288
|
:param start_date: A start date that get from the workflow schedule.
|
241
289
|
:param queue: A mapping of name and list of datetime for queue.
|
290
|
+
:type queue: dict[str, WorkflowQueue]
|
242
291
|
:param externals: An external parameters that pass to the Loader object.
|
292
|
+
:type externals: DictData | None
|
243
293
|
|
244
|
-
:rtype: list[
|
245
|
-
:return: Return the list of
|
294
|
+
:rtype: list[WorkflowTask]
|
295
|
+
:return: Return the list of WorkflowTask object from the specific
|
246
296
|
input datetime that mapping with the on field.
|
247
297
|
"""
|
248
|
-
workflow_tasks: list[
|
249
|
-
extras: DictData = externals or {}
|
250
|
-
|
251
|
-
for sch_wf in self.workflows:
|
252
|
-
|
253
|
-
# NOTE: Loading workflow model from the name of workflow.
|
254
|
-
wf: Workflow = Workflow.from_loader(sch_wf.name, externals=extras)
|
298
|
+
workflow_tasks: list[WorkflowTask] = []
|
255
299
|
|
256
|
-
|
257
|
-
if sch_wf.alias not in queue:
|
258
|
-
queue[sch_wf.alias]: list[datetime] = []
|
300
|
+
for workflow in self.workflows:
|
259
301
|
|
260
|
-
|
261
|
-
|
262
|
-
ons: list[On] = sch_wf.on or wf.on.copy()
|
302
|
+
if workflow.alias not in queue:
|
303
|
+
queue[workflow.alias] = WorkflowQueue()
|
263
304
|
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
runner: CronRunner = on.generate(start_date)
|
268
|
-
next_running_date = runner.next
|
269
|
-
|
270
|
-
while next_running_date in queue[sch_wf.alias]:
|
271
|
-
next_running_date = runner.next
|
272
|
-
|
273
|
-
workflow_tasks.append(
|
274
|
-
WorkflowTaskData(
|
275
|
-
alias=sch_wf.alias,
|
276
|
-
workflow=wf,
|
277
|
-
runner=runner,
|
278
|
-
params=sch_wf.values,
|
279
|
-
),
|
280
|
-
)
|
305
|
+
workflow_tasks.extend(
|
306
|
+
workflow.tasks(start_date, queue=queue, externals=externals)
|
307
|
+
)
|
281
308
|
|
282
309
|
return workflow_tasks
|
283
310
|
|
@@ -298,14 +325,6 @@ def catch_exceptions(cancel_on_failure: bool = False) -> DecoratorCancelJob:
|
|
298
325
|
|
299
326
|
def decorator(func: ReturnCancelJob) -> ReturnCancelJob: # pragma: no cov
|
300
327
|
try:
|
301
|
-
# NOTE: Check the function that want to handle is method or not.
|
302
|
-
if inspect.ismethod(func):
|
303
|
-
|
304
|
-
@wraps(func)
|
305
|
-
def wrapper(self, *args, **kwargs):
|
306
|
-
return func(self, *args, **kwargs)
|
307
|
-
|
308
|
-
return wrapper
|
309
328
|
|
310
329
|
@wraps(func)
|
311
330
|
def wrapper(*args, **kwargs):
|
@@ -323,36 +342,28 @@ def catch_exceptions(cancel_on_failure: bool = False) -> DecoratorCancelJob:
|
|
323
342
|
|
324
343
|
|
325
344
|
@catch_exceptions(cancel_on_failure=True) # pragma: no cov
|
326
|
-
def
|
327
|
-
|
345
|
+
def schedule_task(
|
346
|
+
tasks: list[WorkflowTask],
|
328
347
|
stop: datetime,
|
329
|
-
queue,
|
330
|
-
running,
|
348
|
+
queue: dict[str, WorkflowQueue],
|
331
349
|
threads: dict[str, Thread],
|
350
|
+
log: type[Log],
|
332
351
|
) -> CancelJob | None:
|
333
352
|
"""Workflow task generator that create release pair of workflow and on to
|
334
353
|
the threading in background.
|
335
354
|
|
336
355
|
This workflow task will start every minute at ':02' second.
|
337
356
|
|
338
|
-
:param
|
357
|
+
:param tasks: A list of WorkflowTask object.
|
339
358
|
:param stop: A stop datetime object that force stop running scheduler.
|
340
|
-
:param queue:
|
341
|
-
:param
|
342
|
-
:param
|
359
|
+
:param queue: A mapping of alias name and WorkflowQueue object.
|
360
|
+
:param threads: A mapping of alias name and Thread object.
|
361
|
+
:param log: A log class that want to making log object.
|
362
|
+
|
343
363
|
:rtype: CancelJob | None
|
344
364
|
"""
|
345
365
|
current_date: datetime = datetime.now(tz=config.tz)
|
346
|
-
|
347
366
|
if current_date > stop.replace(tzinfo=config.tz):
|
348
|
-
logger.info("[WORKFLOW]: Stop this schedule with datetime stopper.")
|
349
|
-
while len(threads) > 0:
|
350
|
-
logger.warning(
|
351
|
-
"[WORKFLOW]: Waiting workflow release thread that still "
|
352
|
-
"running in background."
|
353
|
-
)
|
354
|
-
time.sleep(15)
|
355
|
-
workflow_monitor(threads)
|
356
367
|
return CancelJob
|
357
368
|
|
358
369
|
# IMPORTANT:
|
@@ -367,48 +378,53 @@ def workflow_task_release(
|
|
367
378
|
# '00:02:00' --> '*/2 * * * *' --> running
|
368
379
|
# --> '*/35 * * * *' --> skip
|
369
380
|
#
|
370
|
-
for task in
|
381
|
+
for task in tasks:
|
382
|
+
|
383
|
+
q: WorkflowQueue = queue[task.alias]
|
384
|
+
|
385
|
+
# NOTE: Start adding queue and move the runner date in the WorkflowTask.
|
386
|
+
task.queue(stop, q, log=log)
|
371
387
|
|
372
388
|
# NOTE: Get incoming datetime queue.
|
373
|
-
logger.debug(
|
374
|
-
f"[WORKFLOW]: Current queue: {task.workflow.name!r} : "
|
375
|
-
f"{list(queue2str(queue[task.alias]))}"
|
376
|
-
)
|
389
|
+
logger.debug(f"[WORKFLOW]: Queue: {task.alias!r} : {list(q.queue)}")
|
377
390
|
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
f"[WORKFLOW]: Skip schedule "
|
384
|
-
f"{task.runner.date:%Y-%m-%d %H:%M:%S} "
|
385
|
-
f"for : {task.workflow.name!r} : {task.runner.cron}"
|
391
|
+
# VALIDATE: Check the queue is empty or not.
|
392
|
+
if not q.is_queued:
|
393
|
+
logger.warning(
|
394
|
+
f"[WORKFLOW]: Queue is empty for : {task.alias!r} : "
|
395
|
+
f"{task.runner.cron}"
|
386
396
|
)
|
387
397
|
continue
|
388
398
|
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
399
|
+
# VALIDATE: Check this task is the first release in the queue or not.
|
400
|
+
current_date: datetime = current_date.replace(second=0, microsecond=0)
|
401
|
+
if (first_date := q.first_queue.date) != current_date:
|
402
|
+
logger.debug(
|
403
|
+
f"[WORKFLOW]: Skip schedule "
|
404
|
+
f"{first_date:%Y-%m-%d %H:%M:%S} "
|
405
|
+
f"for : {task.alias!r} : {task.runner.cron}"
|
393
406
|
)
|
394
407
|
continue
|
395
408
|
|
396
|
-
# NOTE:
|
397
|
-
queue
|
409
|
+
# NOTE: Pop the latest release and push it to running.
|
410
|
+
release: WorkflowRelease = heappop(q.queue)
|
411
|
+
q.push_running(release)
|
412
|
+
|
413
|
+
logger.info(
|
414
|
+
f"[WORKFLOW]: Start thread: '{task.alias}|{str(task.runner.cron)}|"
|
415
|
+
f"{release.date:%Y%m%d%H%M}'"
|
416
|
+
)
|
398
417
|
|
399
418
|
# NOTE: Create thread name that able to tracking with observe schedule
|
400
419
|
# job.
|
401
420
|
thread_name: str = (
|
402
|
-
f"{task.
|
403
|
-
f"{
|
421
|
+
f"{task.alias}|{str(task.runner.cron)}|"
|
422
|
+
f"{release.date:%Y%m%d%H%M}"
|
404
423
|
)
|
405
424
|
|
406
425
|
wf_thread: Thread = Thread(
|
407
426
|
target=catch_exceptions(cancel_on_failure=True)(task.release),
|
408
|
-
kwargs={
|
409
|
-
"queue": queue,
|
410
|
-
"running": running,
|
411
|
-
},
|
427
|
+
kwargs={"release": release, "queue": q, "log": log},
|
412
428
|
name=thread_name,
|
413
429
|
daemon=True,
|
414
430
|
)
|
@@ -419,20 +435,21 @@ def workflow_task_release(
|
|
419
435
|
|
420
436
|
delay()
|
421
437
|
|
422
|
-
logger.debug(f"[
|
438
|
+
logger.debug(f"[SCHEDULE]: End schedule release {'=' * 80}")
|
423
439
|
|
424
440
|
|
425
|
-
def
|
426
|
-
"""
|
427
|
-
|
441
|
+
def monitor(threads: dict[str, Thread]) -> None: # pragma: no cov
|
442
|
+
"""Monitoring function that running every five minute for track long running
|
443
|
+
thread instance from the schedule_control function that run every minute.
|
428
444
|
|
429
445
|
:param threads: A mapping of Thread object and its name.
|
430
|
-
:
|
446
|
+
:type threads: dict[str, Thread]
|
431
447
|
"""
|
432
448
|
logger.debug(
|
433
449
|
"[MONITOR]: Start checking long running workflow release task."
|
434
450
|
)
|
435
|
-
|
451
|
+
|
452
|
+
snapshot_threads: list[str] = list(threads.keys())
|
436
453
|
for t_name in snapshot_threads:
|
437
454
|
|
438
455
|
# NOTE: remove the thread that running success.
|
@@ -440,18 +457,23 @@ def workflow_monitor(threads: dict[str, Thread]) -> None: # pragma: no cov
|
|
440
457
|
threads.pop(t_name)
|
441
458
|
|
442
459
|
|
443
|
-
def
|
460
|
+
def schedule_control(
|
444
461
|
schedules: list[str],
|
445
462
|
stop: datetime | None = None,
|
446
463
|
externals: DictData | None = None,
|
464
|
+
*,
|
465
|
+
log: type[Log] | None = None,
|
447
466
|
) -> list[str]: # pragma: no cov
|
448
|
-
"""
|
467
|
+
"""Scheduler control function that running every minute.
|
449
468
|
|
450
469
|
:param schedules: A list of workflow names that want to schedule running.
|
451
470
|
:param stop: An datetime value that use to stop running schedule.
|
452
471
|
:param externals: An external parameters that pass to Loader.
|
472
|
+
:param log:
|
473
|
+
|
453
474
|
:rtype: list[str]
|
454
475
|
"""
|
476
|
+
# NOTE: Lazy import Scheduler object from the schedule package.
|
455
477
|
try:
|
456
478
|
from schedule import Scheduler
|
457
479
|
except ImportError:
|
@@ -459,30 +481,27 @@ def workflow_control(
|
|
459
481
|
"Should install schedule package before use this module."
|
460
482
|
) from None
|
461
483
|
|
484
|
+
log: type[Log] = log or FileLog
|
462
485
|
scheduler: Scheduler = Scheduler()
|
463
486
|
start_date: datetime = datetime.now(tz=config.tz)
|
487
|
+
stop_date: datetime = stop or (start_date + config.stop_boundary_delta)
|
464
488
|
|
465
|
-
#
|
466
|
-
|
467
|
-
|
468
|
-
#
|
469
|
-
wf_queue: dict[str, list[datetime]] = {}
|
470
|
-
thread_releases: dict[str, Thread] = {}
|
489
|
+
# IMPORTANT: Create main mapping of queue and thread object.
|
490
|
+
queue: dict[str, WorkflowQueue] = {}
|
491
|
+
threads: dict[str, Thread] = {}
|
471
492
|
|
472
|
-
start_date_waiting: datetime =
|
493
|
+
start_date_waiting: datetime = start_date.replace(
|
473
494
|
second=0, microsecond=0
|
474
|
-
)
|
495
|
+
) + timedelta(minutes=1)
|
475
496
|
|
476
|
-
# NOTE:
|
477
|
-
|
497
|
+
# NOTE: Start create workflow tasks from list of schedule name.
|
498
|
+
tasks: list[WorkflowTask] = []
|
478
499
|
for name in schedules:
|
479
500
|
schedule: Schedule = Schedule.from_loader(name, externals=externals)
|
480
|
-
|
481
|
-
# NOTE: Create a workflow task data instance from schedule object.
|
482
|
-
workflow_tasks.extend(
|
501
|
+
tasks.extend(
|
483
502
|
schedule.tasks(
|
484
503
|
start_date_waiting,
|
485
|
-
queue=
|
504
|
+
queue=queue,
|
486
505
|
externals=externals,
|
487
506
|
),
|
488
507
|
)
|
@@ -492,23 +511,33 @@ def workflow_control(
|
|
492
511
|
scheduler.every(1)
|
493
512
|
.minutes.at(":02")
|
494
513
|
.do(
|
495
|
-
|
496
|
-
|
497
|
-
stop=
|
498
|
-
queue=
|
499
|
-
threads=
|
514
|
+
schedule_task,
|
515
|
+
tasks=tasks,
|
516
|
+
stop=stop_date,
|
517
|
+
queue=queue,
|
518
|
+
threads=threads,
|
519
|
+
log=log,
|
500
520
|
)
|
501
521
|
.tag("control")
|
502
522
|
)
|
503
523
|
|
504
524
|
# NOTE: Checking zombie task with schedule job will start every 5 minute.
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
525
|
+
(
|
526
|
+
scheduler.every(5)
|
527
|
+
.minutes.at(":10")
|
528
|
+
.do(
|
529
|
+
monitor,
|
530
|
+
threads=threads,
|
531
|
+
)
|
532
|
+
.tag("monitor")
|
533
|
+
)
|
509
534
|
|
510
535
|
# NOTE: Start running schedule
|
511
|
-
logger.info(
|
536
|
+
logger.info(
|
537
|
+
f"[SCHEDULE]: Schedule: {schedules} with stopper: "
|
538
|
+
f"{stop_date:%Y-%m-%d %H:%M:%S}"
|
539
|
+
)
|
540
|
+
|
512
541
|
while True:
|
513
542
|
scheduler.run_pending()
|
514
543
|
time.sleep(1)
|
@@ -516,29 +545,35 @@ def workflow_control(
|
|
516
545
|
# NOTE: Break the scheduler when the control job does not exists.
|
517
546
|
if not scheduler.get_jobs("control"):
|
518
547
|
scheduler.clear("monitor")
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
548
|
+
|
549
|
+
while len(threads) > 0:
|
550
|
+
logger.warning(
|
551
|
+
"[SCHEDULE]: Waiting schedule release thread that still "
|
552
|
+
"running in background."
|
553
|
+
)
|
554
|
+
delay(15)
|
555
|
+
monitor(threads)
|
556
|
+
|
523
557
|
break
|
524
558
|
|
525
559
|
logger.warning(
|
526
|
-
f"Queue: {[list(
|
560
|
+
f"[SCHEDULE]: Queue: {[list(queue[wf].queue) for wf in queue]}"
|
527
561
|
)
|
528
562
|
return schedules
|
529
563
|
|
530
564
|
|
531
|
-
def
|
565
|
+
def schedule_runner(
|
532
566
|
stop: datetime | None = None,
|
533
567
|
externals: DictData | None = None,
|
534
568
|
excluded: list[str] | None = None,
|
535
569
|
) -> list[str]: # pragma: no cov
|
536
|
-
"""
|
537
|
-
|
570
|
+
"""Schedule runner function for start submit the ``schedule_control`` func
|
571
|
+
in multiprocessing pool with chunk of schedule config that exists in config
|
572
|
+
path by ``WORKFLOW_APP_MAX_SCHEDULE_PER_PROCESS``.
|
538
573
|
|
539
574
|
:param stop: A stop datetime object that force stop running scheduler.
|
540
|
-
:param excluded:
|
541
575
|
:param externals:
|
576
|
+
:param excluded: A list of schedule name that want to excluded from finding.
|
542
577
|
|
543
578
|
:rtype: list[str]
|
544
579
|
|
@@ -549,24 +584,21 @@ def workflow_runner(
|
|
549
584
|
|
550
585
|
The current workflow logic that split to process will be below diagram:
|
551
586
|
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
workflow task 02 01
|
558
|
-
--> thread of release
|
559
|
-
workflow task 02 02
|
560
|
-
==> ...
|
587
|
+
MAIN ==> process 01 ==> schedule --> thread of release task 01 01
|
588
|
+
--> thread of release task 01 02
|
589
|
+
==> schedule --> thread of release task 02 01
|
590
|
+
--> thread of release task 02 02
|
591
|
+
==> process 02
|
561
592
|
"""
|
562
|
-
|
593
|
+
results: list[str] = []
|
563
594
|
|
564
595
|
with ProcessPoolExecutor(
|
565
596
|
max_workers=config.max_schedule_process,
|
566
597
|
) as executor:
|
598
|
+
|
567
599
|
futures: list[Future] = [
|
568
600
|
executor.submit(
|
569
|
-
|
601
|
+
schedule_control,
|
570
602
|
schedules=[load[0] for load in loader],
|
571
603
|
stop=stop,
|
572
604
|
externals=(externals or {}),
|
@@ -577,10 +609,13 @@ def workflow_runner(
|
|
577
609
|
)
|
578
610
|
]
|
579
611
|
|
580
|
-
results: list[str] = []
|
581
612
|
for future in as_completed(futures):
|
613
|
+
|
614
|
+
# NOTE: Raise error when it has any error from schedule_control.
|
582
615
|
if err := future.exception():
|
583
616
|
logger.error(str(err))
|
584
617
|
raise WorkflowException(str(err)) from err
|
618
|
+
|
585
619
|
results.extend(future.result(timeout=1))
|
586
|
-
|
620
|
+
|
621
|
+
return results
|
ddeutil/workflow/stage.py
CHANGED
@@ -346,6 +346,11 @@ class EmptyStage(BaseStage):
|
|
346
346
|
f"( {param2template(self.echo, params=params) or '...'} )"
|
347
347
|
)
|
348
348
|
if self.sleep > 0:
|
349
|
+
if self.sleep > 30:
|
350
|
+
logger.info(
|
351
|
+
f"({cut_id(run_id)}) [STAGE]: ... sleep "
|
352
|
+
f"({self.sleep} seconds)"
|
353
|
+
)
|
349
354
|
time.sleep(self.sleep)
|
350
355
|
return Result(status=0, context={}, run_id=run_id)
|
351
356
|
|
ddeutil/workflow/utils.py
CHANGED
@@ -577,10 +577,6 @@ def batch(iterable: Iterator[Any], n: int) -> Iterator[Any]:
|
|
577
577
|
yield chain((first_el,), chunk_it)
|
578
578
|
|
579
579
|
|
580
|
-
def queue2str(queue: list[datetime]) -> Iterator[str]: # pragma: no cov
|
581
|
-
return (f"{q:%Y-%m-%d %H:%M:%S}" for q in queue)
|
582
|
-
|
583
|
-
|
584
580
|
def cut_id(run_id: str, *, num: int = 6):
|
585
581
|
"""Cutting running ID with length.
|
586
582
|
|
ddeutil/workflow/workflow.py
CHANGED
@@ -43,16 +43,15 @@ from typing_extensions import Self
|
|
43
43
|
from .__cron import CronJob, CronRunner
|
44
44
|
from .__types import DictData, TupleStr
|
45
45
|
from .conf import FileLog, Loader, Log, config, get_logger
|
46
|
+
from .cron import On
|
46
47
|
from .exceptions import JobException, WorkflowException
|
47
48
|
from .job import Job
|
48
|
-
from .on import On
|
49
49
|
from .params import Param
|
50
50
|
from .result import Result
|
51
51
|
from .utils import (
|
52
52
|
cut_id,
|
53
53
|
delay,
|
54
54
|
gen_id,
|
55
|
-
get_diff_sec,
|
56
55
|
get_dt_now,
|
57
56
|
has_template,
|
58
57
|
param2template,
|
@@ -64,7 +63,7 @@ __all__: TupleStr = (
|
|
64
63
|
"Workflow",
|
65
64
|
"WorkflowRelease",
|
66
65
|
"WorkflowQueue",
|
67
|
-
"
|
66
|
+
"WorkflowTask",
|
68
67
|
)
|
69
68
|
|
70
69
|
|
@@ -168,7 +167,23 @@ class WorkflowQueue:
|
|
168
167
|
"""
|
169
168
|
return len(self.queue) > 0
|
170
169
|
|
171
|
-
|
170
|
+
@property
|
171
|
+
def first_queue(self) -> WorkflowRelease:
|
172
|
+
"""Check an input WorkflowRelease object is the first value of the
|
173
|
+
waiting queue.
|
174
|
+
|
175
|
+
:rtype: bool
|
176
|
+
"""
|
177
|
+
# NOTE: Old logic to peeking the first release from waiting queue.
|
178
|
+
#
|
179
|
+
# first_value: WorkflowRelease = heappop(self.queue)
|
180
|
+
# heappush(self.queue, first_value)
|
181
|
+
#
|
182
|
+
# return first_value
|
183
|
+
#
|
184
|
+
return self.queue[0]
|
185
|
+
|
186
|
+
def check_queue(self, value: WorkflowRelease | datetime) -> bool:
|
172
187
|
"""Check a WorkflowRelease value already exists in list of tracking
|
173
188
|
queues.
|
174
189
|
|
@@ -177,6 +192,9 @@ class WorkflowQueue:
|
|
177
192
|
|
178
193
|
:rtype: bool
|
179
194
|
"""
|
195
|
+
if isinstance(value, datetime):
|
196
|
+
value = WorkflowRelease.from_dt(value)
|
197
|
+
|
180
198
|
return (
|
181
199
|
(value in self.queue)
|
182
200
|
or (value in self.running)
|
@@ -184,20 +202,37 @@ class WorkflowQueue:
|
|
184
202
|
)
|
185
203
|
|
186
204
|
def push_queue(self, value: WorkflowRelease) -> Self:
|
187
|
-
"""Push data to the queue."""
|
205
|
+
"""Push data to the waiting queue."""
|
188
206
|
heappush(self.queue, value)
|
189
207
|
return self
|
190
208
|
|
191
209
|
def push_running(self, value: WorkflowRelease) -> Self:
|
192
|
-
"""Push
|
210
|
+
"""Push WorkflowRelease to the running queue."""
|
193
211
|
heappush(self.running, value)
|
194
212
|
return self
|
195
213
|
|
196
214
|
def remove_running(self, value: WorkflowRelease) -> Self:
|
197
|
-
"""Remove
|
215
|
+
"""Remove WorkflowRelease in the running queue if it exists."""
|
198
216
|
if value in self.running:
|
199
217
|
self.running.remove(value)
|
200
218
|
|
219
|
+
def push_complete(self, value: WorkflowRelease) -> Self:
|
220
|
+
"""Push WorkflowRelease to the complete queue."""
|
221
|
+
heappush(self.complete, value)
|
222
|
+
|
223
|
+
# NOTE: Remove complete queue on workflow that keep more than the
|
224
|
+
# maximum config.
|
225
|
+
num_complete_delete: int = (
|
226
|
+
len(self.complete) - config.max_queue_complete_hist
|
227
|
+
)
|
228
|
+
|
229
|
+
if num_complete_delete > 0:
|
230
|
+
print(num_complete_delete)
|
231
|
+
for _ in range(num_complete_delete):
|
232
|
+
heappop(self.complete)
|
233
|
+
|
234
|
+
return self
|
235
|
+
|
201
236
|
|
202
237
|
class Workflow(BaseModel):
|
203
238
|
"""Workflow Pydantic model.
|
@@ -448,6 +483,7 @@ class Workflow(BaseModel):
|
|
448
483
|
queue: (
|
449
484
|
WorkflowQueue | list[datetime] | list[WorkflowRelease] | None
|
450
485
|
) = None,
|
486
|
+
override_log_name: str | None = None,
|
451
487
|
) -> Result:
|
452
488
|
"""Release the workflow execution with overriding parameter with the
|
453
489
|
release templating that include logical date (release date), execution
|
@@ -462,11 +498,14 @@ class Workflow(BaseModel):
|
|
462
498
|
:param run_id: A workflow running ID for this release.
|
463
499
|
:param log: A log class that want to save the execution result.
|
464
500
|
:param queue: A WorkflowQueue object.
|
501
|
+
:param override_log_name: An override logging name that use instead
|
502
|
+
the workflow name.
|
465
503
|
|
466
504
|
:rtype: Result
|
467
505
|
"""
|
468
506
|
log: type[Log] = log or FileLog
|
469
|
-
|
507
|
+
name: str = override_log_name or self.name
|
508
|
+
run_id: str = run_id or gen_id(name, unique=True)
|
470
509
|
rs_release: Result = Result(run_id=run_id)
|
471
510
|
|
472
511
|
# VALIDATE: Change queue value to WorkflowQueue object.
|
@@ -478,7 +517,7 @@ class Workflow(BaseModel):
|
|
478
517
|
release: WorkflowRelease = WorkflowRelease.from_dt(release)
|
479
518
|
|
480
519
|
logger.debug(
|
481
|
-
f"({cut_id(run_id)}) [RELEASE]: {
|
520
|
+
f"({cut_id(run_id)}) [RELEASE]: Start release - {name!r} : "
|
482
521
|
f"{release.date:%Y-%m-%d %H:%M:%S}"
|
483
522
|
)
|
484
523
|
|
@@ -499,14 +538,14 @@ class Workflow(BaseModel):
|
|
499
538
|
run_id=run_id,
|
500
539
|
)
|
501
540
|
logger.debug(
|
502
|
-
f"({cut_id(run_id)}) [RELEASE]: {
|
541
|
+
f"({cut_id(run_id)}) [RELEASE]: End release - {name!r} : "
|
503
542
|
f"{release.date:%Y-%m-%d %H:%M:%S}"
|
504
543
|
)
|
505
544
|
|
506
545
|
rs.set_parent_run_id(run_id)
|
507
546
|
rs_log: Log = log.model_validate(
|
508
547
|
{
|
509
|
-
"name":
|
548
|
+
"name": name,
|
510
549
|
"release": release.date,
|
511
550
|
"type": release.type,
|
512
551
|
"context": rs.context,
|
@@ -520,7 +559,7 @@ class Workflow(BaseModel):
|
|
520
559
|
|
521
560
|
# NOTE: Remove this release from running.
|
522
561
|
queue.remove_running(release)
|
523
|
-
|
562
|
+
queue.push_complete(release)
|
524
563
|
|
525
564
|
context: dict[str, Any] = rs.context
|
526
565
|
context.pop("params")
|
@@ -534,7 +573,7 @@ class Workflow(BaseModel):
|
|
534
573
|
},
|
535
574
|
)
|
536
575
|
|
537
|
-
def
|
576
|
+
def queue(
|
538
577
|
self,
|
539
578
|
offset: float,
|
540
579
|
end_date: datetime,
|
@@ -667,7 +706,7 @@ class Workflow(BaseModel):
|
|
667
706
|
wf_queue: WorkflowQueue = WorkflowQueue()
|
668
707
|
|
669
708
|
# NOTE: Make queue to the workflow queue object.
|
670
|
-
self.
|
709
|
+
self.queue(
|
671
710
|
offset,
|
672
711
|
end_date=end_date,
|
673
712
|
queue=wf_queue,
|
@@ -707,7 +746,7 @@ class Workflow(BaseModel):
|
|
707
746
|
|
708
747
|
# WARNING: I already call queue poking again because issue
|
709
748
|
# about the every minute crontab.
|
710
|
-
self.
|
749
|
+
self.queue(
|
711
750
|
offset,
|
712
751
|
end_date,
|
713
752
|
queue=wf_queue,
|
@@ -729,7 +768,7 @@ class Workflow(BaseModel):
|
|
729
768
|
)
|
730
769
|
)
|
731
770
|
|
732
|
-
self.
|
771
|
+
self.queue(
|
733
772
|
offset,
|
734
773
|
end_date,
|
735
774
|
queue=wf_queue,
|
@@ -1074,7 +1113,7 @@ class Workflow(BaseModel):
|
|
1074
1113
|
|
1075
1114
|
|
1076
1115
|
@dataclass(config=ConfigDict(arbitrary_types_allowed=True))
|
1077
|
-
class
|
1116
|
+
class WorkflowTask:
|
1078
1117
|
"""Workflow task Pydantic dataclass object that use to keep mapping data and
|
1079
1118
|
workflow model for passing to the multithreading task.
|
1080
1119
|
|
@@ -1085,151 +1124,95 @@ class WorkflowTaskData:
|
|
1085
1124
|
alias: str
|
1086
1125
|
workflow: Workflow
|
1087
1126
|
runner: CronRunner
|
1088
|
-
|
1127
|
+
values: DictData = field(default_factory=dict)
|
1089
1128
|
|
1090
1129
|
def release(
|
1091
1130
|
self,
|
1092
|
-
|
1093
|
-
log: Log | None = None,
|
1131
|
+
release: datetime | WorkflowRelease | None = None,
|
1094
1132
|
run_id: str | None = None,
|
1095
|
-
|
1096
|
-
|
1097
|
-
|
1098
|
-
|
1099
|
-
|
1100
|
-
|
1101
|
-
|
1102
|
-
|
1103
|
-
:param queue: A mapping of alias name and list of release datetime.
|
1104
|
-
:param log: A log object for saving result logging from workflow
|
1105
|
-
execution process.
|
1133
|
+
log: type[Log] = None,
|
1134
|
+
queue: (
|
1135
|
+
WorkflowQueue | list[datetime] | list[WorkflowRelease] | None
|
1136
|
+
) = None,
|
1137
|
+
) -> Result:
|
1138
|
+
"""Release the workflow task data.
|
1139
|
+
|
1140
|
+
:param release: A release datetime or WorkflowRelease object.
|
1106
1141
|
:param run_id: A workflow running ID for this release.
|
1107
|
-
:param
|
1108
|
-
:param
|
1109
|
-
to execute.
|
1142
|
+
:param log: A log class that want to save the execution result.
|
1143
|
+
:param queue: A WorkflowQueue object.
|
1110
1144
|
|
1111
1145
|
:rtype: Result
|
1112
1146
|
"""
|
1113
1147
|
log: type[Log] = log or FileLog
|
1114
|
-
|
1115
|
-
|
1116
|
-
|
1117
|
-
|
1118
|
-
|
1119
|
-
|
1120
|
-
|
1121
|
-
# NOTE: get next utils it does not running.
|
1122
|
-
while log.is_pointed(self.workflow.name, next_time) or (
|
1123
|
-
next_time in queue[self.alias]
|
1124
|
-
):
|
1125
|
-
next_time: datetime = runner.next
|
1126
|
-
|
1127
|
-
logger.debug(
|
1128
|
-
f"({cut_id(run_id)}) [CORE]: {self.workflow.name!r} : "
|
1129
|
-
f"{runner.cron} : {next_time:%Y-%m-%d %H:%M:%S}"
|
1148
|
+
return self.workflow.release(
|
1149
|
+
release=release or self.runner.next,
|
1150
|
+
params=self.values,
|
1151
|
+
run_id=run_id,
|
1152
|
+
log=log,
|
1153
|
+
queue=queue,
|
1154
|
+
override_log_name=self.alias,
|
1130
1155
|
)
|
1131
|
-
heappush(queue[self.alias], next_time)
|
1132
|
-
start_sec: float = time.monotonic()
|
1133
|
-
|
1134
|
-
if get_diff_sec(next_time, tz=runner.tz) > waiting_sec:
|
1135
|
-
logger.debug(
|
1136
|
-
f"({cut_id(run_id)}) [WORKFLOW]: {self.workflow.name!r} : "
|
1137
|
-
f"{runner.cron} "
|
1138
|
-
f": Does not closely >> {next_time:%Y-%m-%d %H:%M:%S}"
|
1139
|
-
)
|
1140
1156
|
|
1141
|
-
|
1142
|
-
|
1143
|
-
|
1157
|
+
def queue(
|
1158
|
+
self,
|
1159
|
+
end_date: datetime,
|
1160
|
+
queue: WorkflowQueue,
|
1161
|
+
log: type[Log],
|
1162
|
+
*,
|
1163
|
+
force_run: bool = False,
|
1164
|
+
):
|
1165
|
+
"""Generate WorkflowRelease to WorkflowQueue object.
|
1166
|
+
|
1167
|
+
:param end_date: An end datetime object.
|
1168
|
+
:param queue: A workflow queue object.
|
1169
|
+
:param log: A log class that want to making log object.
|
1170
|
+
:param force_run: A flag that allow to release workflow if the log with
|
1171
|
+
that release was pointed.
|
1144
1172
|
|
1145
|
-
|
1146
|
-
|
1173
|
+
:rtype: WorkflowQueue
|
1174
|
+
"""
|
1175
|
+
if self.runner.date > end_date:
|
1176
|
+
return queue
|
1147
1177
|
|
1148
|
-
|
1149
|
-
|
1150
|
-
|
1178
|
+
workflow_release = WorkflowRelease(
|
1179
|
+
date=self.runner.date,
|
1180
|
+
offset=0,
|
1181
|
+
end_date=end_date,
|
1182
|
+
runner=self.runner,
|
1183
|
+
type="task",
|
1151
1184
|
)
|
1152
1185
|
|
1153
|
-
|
1154
|
-
|
1155
|
-
|
1186
|
+
while queue.check_queue(workflow_release) or (
|
1187
|
+
log.is_pointed(name=self.alias, release=workflow_release.date)
|
1188
|
+
and not force_run
|
1156
1189
|
):
|
1157
|
-
|
1158
|
-
|
1159
|
-
|
1190
|
+
workflow_release = WorkflowRelease(
|
1191
|
+
date=self.runner.next,
|
1192
|
+
offset=0,
|
1193
|
+
end_date=end_date,
|
1194
|
+
runner=self.runner,
|
1195
|
+
type="task",
|
1160
1196
|
)
|
1161
|
-
time.sleep(15)
|
1162
1197
|
|
1163
|
-
|
1198
|
+
if self.runner.date > end_date:
|
1199
|
+
return queue
|
1164
1200
|
|
1165
|
-
# NOTE:
|
1166
|
-
|
1167
|
-
release_params: DictData = {
|
1168
|
-
"release": {
|
1169
|
-
"logical_date": next_time,
|
1170
|
-
"execute_date": datetime.now(tz=config.tz),
|
1171
|
-
"run_id": run_id,
|
1172
|
-
"timezone": runner.tz,
|
1173
|
-
},
|
1174
|
-
}
|
1201
|
+
# NOTE: Push the WorkflowRelease object to queue.
|
1202
|
+
queue.push_queue(workflow_release)
|
1175
1203
|
|
1176
|
-
|
1177
|
-
rs: Result = self.workflow.execute(
|
1178
|
-
params=param2template(self.params, release_params),
|
1179
|
-
)
|
1180
|
-
logger.debug(
|
1181
|
-
f"({cut_id(run_id)}) [CORE]: {self.workflow.name!r} : "
|
1182
|
-
f"{runner.cron} : End release - {next_time:%Y-%m-%d %H:%M:%S}"
|
1183
|
-
)
|
1184
|
-
|
1185
|
-
# NOTE: Set parent ID on this result.
|
1186
|
-
rs.set_parent_run_id(run_id)
|
1187
|
-
|
1188
|
-
# NOTE: Save result to log object saving.
|
1189
|
-
rs_log: Log = log.model_validate(
|
1190
|
-
{
|
1191
|
-
"name": self.workflow.name,
|
1192
|
-
"type": "schedule",
|
1193
|
-
"release": next_time,
|
1194
|
-
"context": rs.context,
|
1195
|
-
"parent_run_id": rs.run_id,
|
1196
|
-
"run_id": rs.run_id,
|
1197
|
-
}
|
1198
|
-
)
|
1199
|
-
rs_log.save(excluded=None)
|
1200
|
-
|
1201
|
-
# NOTE: Remove the current release date from the running.
|
1202
|
-
queue[self.alias].remove(next_time)
|
1203
|
-
total_sec: float = time.monotonic() - start_sec
|
1204
|
-
|
1205
|
-
# IMPORTANT:
|
1206
|
-
# Add the next running datetime to workflow task queue.
|
1207
|
-
future_running_time: datetime = runner.next
|
1208
|
-
|
1209
|
-
while (
|
1210
|
-
future_running_time in queue[self.alias]
|
1211
|
-
or (future_running_time - next_time).total_seconds() < total_sec
|
1212
|
-
): # pragma: no cov
|
1213
|
-
future_running_time: datetime = runner.next
|
1214
|
-
|
1215
|
-
# NOTE: Queue next release date.
|
1216
|
-
logger.debug(f"[CORE]: {'-' * 100}")
|
1217
|
-
|
1218
|
-
context: dict[str, Any] = rs.context
|
1219
|
-
context.pop("params")
|
1204
|
+
return queue
|
1220
1205
|
|
1221
|
-
|
1222
|
-
|
1223
|
-
|
1224
|
-
|
1225
|
-
|
1226
|
-
"outputs": context,
|
1227
|
-
},
|
1206
|
+
def __repr__(self) -> str:
|
1207
|
+
return (
|
1208
|
+
f"{self.__class__.__name__}(alias={self.alias!r}, "
|
1209
|
+
f"workflow={self.workflow.name!r}, runner={self.runner!r}, "
|
1210
|
+
f"values={self.values})"
|
1228
1211
|
)
|
1229
1212
|
|
1230
|
-
def __eq__(self, other:
|
1213
|
+
def __eq__(self, other: WorkflowTask) -> bool:
|
1231
1214
|
"""Override equal property that will compare only the same type."""
|
1232
|
-
if isinstance(other,
|
1215
|
+
if isinstance(other, WorkflowTask):
|
1233
1216
|
return (
|
1234
1217
|
self.workflow.name == other.workflow.name
|
1235
1218
|
and self.runner.cron == other.runner.cron
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: ddeutil-workflow
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.24
|
4
4
|
Summary: Lightweight workflow orchestration with less dependencies
|
5
5
|
Author-email: ddeutils <korawich.anu@gmail.com>
|
6
6
|
License: MIT
|
@@ -24,7 +24,7 @@ Description-Content-Type: text/markdown
|
|
24
24
|
License-File: LICENSE
|
25
25
|
Requires-Dist: ddeutil>=0.4.3
|
26
26
|
Requires-Dist: ddeutil-io[toml,yaml]>=0.2.3
|
27
|
-
Requires-Dist: pydantic==2.10.
|
27
|
+
Requires-Dist: pydantic==2.10.4
|
28
28
|
Requires-Dist: python-dotenv==1.0.1
|
29
29
|
Requires-Dist: typer==0.15.1
|
30
30
|
Requires-Dist: schedule<2.0.0,==1.2.2
|
@@ -196,7 +196,8 @@ and do not raise any error to you.
|
|
196
196
|
| `WORKFLOW_CORE_MAX_NUM_POKING` | Core | 4 | . | |
|
197
197
|
| `WORKFLOW_CORE_MAX_JOB_PARALLEL` | Core | 2 | The maximum job number that able to run parallel in workflow executor. | |
|
198
198
|
| `WORKFLOW_CORE_MAX_JOB_EXEC_TIMEOUT` | Core | 600 | | |
|
199
|
-
| `
|
199
|
+
| `WORKFLOW_CORE_MAX_CRON_PER_WORKFLOW` | Core | 5 | | |
|
200
|
+
| `WORKFLOW_CORE_MAX_QUEUE_COMPLETE_HIST` | Core | 16 | | |
|
200
201
|
| `WORKFLOW_CORE_GENERATE_ID_SIMPLE_MODE` | Core | true | A flog that enable generating ID with `md5` algorithm. | |
|
201
202
|
| `WORKFLOW_LOG_DEBUG_MODE` | Log | true | A flag that enable logging with debug level mode. | |
|
202
203
|
| `WORKFLOW_LOG_ENABLE_WRITE` | Log | true | A flag that enable logging object saving log to its destination. | |
|
@@ -0,0 +1,24 @@
|
|
1
|
+
ddeutil/workflow/__about__.py,sha256=LbAkk7O3dezpuJ-KPhsDQuHdrO9T0qmhBd-oDJzBhq4,28
|
2
|
+
ddeutil/workflow/__cron.py,sha256=uA8XcbY_GwA9rJSHaHUaXaJyGDObJN0ZeYlJSinL8y8,26880
|
3
|
+
ddeutil/workflow/__init__.py,sha256=49eGrCuchPVZKMybRouAviNhbulK_F6VwCmLm76hIss,1478
|
4
|
+
ddeutil/workflow/__types.py,sha256=Ia7f38kvL3NibwmRKi0wQ1ud_45Z-SojYGhNJwIqcu8,3713
|
5
|
+
ddeutil/workflow/api.py,sha256=cdRxqwVyGm_Ni_OmflIP35vUkkq8lHpF3xHh_BvVrKs,4692
|
6
|
+
ddeutil/workflow/cli.py,sha256=8C5Xri1_82B-sxQcKMPRjDJcuYJG3FZ2bJehvs_xZ4s,3278
|
7
|
+
ddeutil/workflow/conf.py,sha256=Al-00Uru2fCJaW2C_vt4IFuBDpI8Y5C4oAuLJ0Vdvbk,16110
|
8
|
+
ddeutil/workflow/cron.py,sha256=0SxC3SH-8V1idgAEFOY-gYFEQPjK_zymmc5XqPoX_0I,7504
|
9
|
+
ddeutil/workflow/exceptions.py,sha256=NqnQJP52S59XIYMeXbTDbr4xH2UZ5EA3ejpU5Z4g6cQ,894
|
10
|
+
ddeutil/workflow/job.py,sha256=cvSLMdc1sMl1MeU7so7Oe2SdRYxQwt6hm55mLV1iP-Y,24219
|
11
|
+
ddeutil/workflow/params.py,sha256=uPGkZx18E-iZ8BteqQ2ONgg0frhF3ZmP5cOyfK2j59U,5280
|
12
|
+
ddeutil/workflow/repeat.py,sha256=s0azh-f5JQeow7kpxM8GKlqgAmKL7oU6St3L4Ggx4cY,4925
|
13
|
+
ddeutil/workflow/result.py,sha256=WIC8MsnfLiWNpZomT6jS4YCdYhlbIVVBjtGGe2dkoKk,3404
|
14
|
+
ddeutil/workflow/route.py,sha256=bH5IT90JVjCDe9A0gIefpQQBEfcd-o1uCHE9AvNglvU,6754
|
15
|
+
ddeutil/workflow/scheduler.py,sha256=UI8wK2xBYmM3Bh_hel0TMzuJWyezM83Yn4xoiYqTSSQ,20238
|
16
|
+
ddeutil/workflow/stage.py,sha256=a2sngzs9DkP6GU2pgAD3QvGoijyBQTR_pOhyJUIuWAo,26692
|
17
|
+
ddeutil/workflow/utils.py,sha256=PhNJ54oKnZfq4nVOeP3tDjFN43ArUsMOnpcbSu7bo4I,18450
|
18
|
+
ddeutil/workflow/workflow.py,sha256=JyT65Tql7CueQn2z4ZGhp6r44jgYDMcCOpxhiwI19uM,41403
|
19
|
+
ddeutil_workflow-0.0.24.dist-info/LICENSE,sha256=nGFZ1QEhhhWeMHf9n99_fdt4vQaXS29xWKxt-OcLywk,1085
|
20
|
+
ddeutil_workflow-0.0.24.dist-info/METADATA,sha256=0yh6zKsIu1COnhl-25rOxBGEqLQbrJZzA0IhriO3XwA,14234
|
21
|
+
ddeutil_workflow-0.0.24.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
22
|
+
ddeutil_workflow-0.0.24.dist-info/entry_points.txt,sha256=0BVOgO3LdUdXVZ-CiHHDKxzEk2c8J30jEwHeKn2YCWI,62
|
23
|
+
ddeutil_workflow-0.0.24.dist-info/top_level.txt,sha256=m9M6XeSWDwt_yMsmH6gcOjHZVK5O0-vgtNBuncHjzW4,8
|
24
|
+
ddeutil_workflow-0.0.24.dist-info/RECORD,,
|
@@ -1,24 +0,0 @@
|
|
1
|
-
ddeutil/workflow/__about__.py,sha256=xc6s739CYU9opnZq2_D6Mx3ekymsbYOQKM2zsgw85oc,28
|
2
|
-
ddeutil/workflow/__cron.py,sha256=_2P9nmGOwGdv5bLgf9TpML2HBgqLv_qRgiO1Rulo1PA,26693
|
3
|
-
ddeutil/workflow/__init__.py,sha256=dH3T06kO8aEiRHAiL-d8a3IvOS0Fx80lS3AWz6rGdQk,1443
|
4
|
-
ddeutil/workflow/__types.py,sha256=Ia7f38kvL3NibwmRKi0wQ1ud_45Z-SojYGhNJwIqcu8,3713
|
5
|
-
ddeutil/workflow/api.py,sha256=ceTJfjXIw-3cgw4yx2QCcGLuA3STb0o7ELcVR_tfZFI,4700
|
6
|
-
ddeutil/workflow/cli.py,sha256=baHhvtI8snbHYHeThoX401Cd6SMB2boyyCbCtTrIl3E,3278
|
7
|
-
ddeutil/workflow/conf.py,sha256=GsbuJDQfQoAGiR4keUEoB4lKfZxdkaiZ4N4FfIHc0xY,15814
|
8
|
-
ddeutil/workflow/exceptions.py,sha256=NqnQJP52S59XIYMeXbTDbr4xH2UZ5EA3ejpU5Z4g6cQ,894
|
9
|
-
ddeutil/workflow/job.py,sha256=cvSLMdc1sMl1MeU7so7Oe2SdRYxQwt6hm55mLV1iP-Y,24219
|
10
|
-
ddeutil/workflow/on.py,sha256=0SxC3SH-8V1idgAEFOY-gYFEQPjK_zymmc5XqPoX_0I,7504
|
11
|
-
ddeutil/workflow/params.py,sha256=uPGkZx18E-iZ8BteqQ2ONgg0frhF3ZmP5cOyfK2j59U,5280
|
12
|
-
ddeutil/workflow/repeat.py,sha256=s0azh-f5JQeow7kpxM8GKlqgAmKL7oU6St3L4Ggx4cY,4925
|
13
|
-
ddeutil/workflow/result.py,sha256=WIC8MsnfLiWNpZomT6jS4YCdYhlbIVVBjtGGe2dkoKk,3404
|
14
|
-
ddeutil/workflow/route.py,sha256=bH5IT90JVjCDe9A0gIefpQQBEfcd-o1uCHE9AvNglvU,6754
|
15
|
-
ddeutil/workflow/scheduler.py,sha256=MYHf1bz8nsT8tJYcXgC-UycWbJ56Hx_zXwUAwWICimM,19141
|
16
|
-
ddeutil/workflow/stage.py,sha256=y6gjNzQy7xAM0n-lwqAEoC4x0lopH0K-Y77a_gvq4t8,26505
|
17
|
-
ddeutil/workflow/utils.py,sha256=ZVQh5vArWHNfCFYWYjHvkVD5aH-350ycfcZxDewELHM,18578
|
18
|
-
ddeutil/workflow/workflow.py,sha256=jmJxAEkczuanmj041OLgeBUDz9Y8XfFNVP_B3Xi7QDY,42557
|
19
|
-
ddeutil_workflow-0.0.23.dist-info/LICENSE,sha256=nGFZ1QEhhhWeMHf9n99_fdt4vQaXS29xWKxt-OcLywk,1085
|
20
|
-
ddeutil_workflow-0.0.23.dist-info/METADATA,sha256=4KqKSGCbc8dWHwWSCSgU4m_wsOouFHsiCGtdyo0Mf5U,14017
|
21
|
-
ddeutil_workflow-0.0.23.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
22
|
-
ddeutil_workflow-0.0.23.dist-info/entry_points.txt,sha256=0BVOgO3LdUdXVZ-CiHHDKxzEk2c8J30jEwHeKn2YCWI,62
|
23
|
-
ddeutil_workflow-0.0.23.dist-info/top_level.txt,sha256=m9M6XeSWDwt_yMsmH6gcOjHZVK5O0-vgtNBuncHjzW4,8
|
24
|
-
ddeutil_workflow-0.0.23.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|