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.
@@ -1 +1 @@
1
- __version__: str = "0.0.23"
1
+ __version__: str = "0.0.24"
@@ -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
@@ -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
- WorkflowTaskData,
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 WorkflowTaskData
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[WorkflowTaskData]
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 workflow_runner
76
+ from .scheduler import schedule_runner
77
77
 
78
78
  # NOTE: Start running workflow scheduler application.
79
- workflow_rs: list[str] = workflow_runner(
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("WORKFLOW_CORE_MAX_ON_PER_WORKFLOW", "5")
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
@@ -4,7 +4,7 @@
4
4
  # license information.
5
5
  # ------------------------------------------------------------------------------
6
6
  """
7
- The main schedule running is ``workflow_runner`` function that trigger the
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, WorkflowTaskData
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
- "workflow_task_release",
75
- "workflow_monitor",
76
- "workflow_control",
77
- "workflow_runner",
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, list[datetime]],
281
+ queue: dict[str, WorkflowQueue],
231
282
  *,
232
283
  externals: DictData | None = None,
233
- ) -> list[WorkflowTaskData]:
234
- """Return the list of WorkflowTaskData object from the specific input
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[WorkflowTaskData]
245
- :return: Return the list of WorkflowTaskData object from the specific
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[WorkflowTaskData] = []
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
- # NOTE: Create default list of release datetime by empty list.
257
- if sch_wf.alias not in queue:
258
- queue[sch_wf.alias]: list[datetime] = []
300
+ for workflow in self.workflows:
259
301
 
260
- # IMPORTANT: Create the default 'on' value if it does not passing
261
- # the on field to the Schedule object.
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
- for on in ons:
265
-
266
- # NOTE: Create CronRunner instance from the start_date param.
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 workflow_task_release(
327
- workflow_tasks: list[WorkflowTaskData],
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 workflow_tasks:
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 running:
342
- :param threads:
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 workflow_tasks:
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
- if (
379
- len(queue[task.alias]) > 0
380
- and task.runner.date != queue[task.alias][0]
381
- ):
382
- logger.debug(
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
- elif len(queue[task.alias]) == 0:
390
- logger.warning(
391
- f"[WORKFLOW]: Queue is empty for : {task.workflow.name!r} : "
392
- f"{task.runner.cron}"
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: Remove this datetime from queue.
397
- queue[task.alias].pop(0)
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.workflow.name}|{str(task.runner.cron)}|"
403
- f"{task.runner.date:%Y%m%d%H%M}"
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"[WORKFLOW]: {'=' * 100}")
438
+ logger.debug(f"[SCHEDULE]: End schedule release {'=' * 80}")
423
439
 
424
440
 
425
- def workflow_monitor(threads: dict[str, Thread]) -> None: # pragma: no cov
426
- """Workflow schedule for monitoring long running thread from the schedule
427
- control.
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
- :rtype: None
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
- snapshot_threads = list(threads.keys())
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 workflow_control(
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
- """Workflow scheduler control.
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
- # NOTE: Design workflow queue caching.
466
- # ---
467
- # {"workflow-name": [<release-datetime>, <release-datetime>, ...]}
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 = (start_date + timedelta(minutes=1)).replace(
493
+ start_date_waiting: datetime = start_date.replace(
473
494
  second=0, microsecond=0
474
- )
495
+ ) + timedelta(minutes=1)
475
496
 
476
- # NOTE: Create pair of workflow and on from schedule model.
477
- workflow_tasks: list[WorkflowTaskData] = []
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=wf_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
- workflow_task_release,
496
- workflow_tasks=workflow_tasks,
497
- stop=(stop or (start_date + config.stop_boundary_delta)),
498
- queue=wf_queue,
499
- threads=thread_releases,
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
- scheduler.every(5).minutes.at(":10").do(
506
- workflow_monitor,
507
- threads=thread_releases,
508
- ).tag("monitor")
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(f"[WORKFLOW]: Start schedule: {schedules}")
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
- logger.warning(
520
- f"[WORKFLOW]: Workflow release thread: {thread_releases}"
521
- )
522
- logger.warning("[WORKFLOW]: Does not have any schedule jobs !!!")
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(queue2str(wf_queue[wf])) for wf in wf_queue]}"
560
+ f"[SCHEDULE]: Queue: {[list(queue[wf].queue) for wf in queue]}"
527
561
  )
528
562
  return schedules
529
563
 
530
564
 
531
- def workflow_runner(
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
- """Workflow application that running multiprocessing schedule with chunk of
537
- workflows that exists in config path.
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
- PIPELINES ==> process 01 ==> schedule --> thread of release
553
- workflow task 01 01
554
- --> thread of release
555
- workflow task 01 02
556
- ==> process 02 ==> schedule --> thread of release
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
- excluded: list[str] = excluded or []
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
- workflow_control,
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
- return results
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
 
@@ -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
- "WorkflowTaskData",
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
- def check_queue(self, value: WorkflowRelease) -> bool:
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 data to the running."""
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 data on the running if it exists."""
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
- run_id: str = run_id or gen_id(self.name, unique=True)
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]: {self.name!r} : Start 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]: {self.name!r} : End 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": self.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
- heappush(queue.complete, release)
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 queue_poking(
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.queue_poking(
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.queue_poking(
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.queue_poking(
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 WorkflowTaskData:
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
- params: DictData = field(default_factory=dict)
1127
+ values: DictData = field(default_factory=dict)
1089
1128
 
1090
1129
  def release(
1091
1130
  self,
1092
- queue: dict[str, list[datetime]],
1093
- log: Log | None = None,
1131
+ release: datetime | WorkflowRelease | None = None,
1094
1132
  run_id: str | None = None,
1095
- *,
1096
- waiting_sec: int = 60,
1097
- sleep_interval: int = 15,
1098
- ) -> Result: # pragma: no cov
1099
- """Release the workflow task data that use the same logic of
1100
- `workflow.release` method but use different the queue object for
1101
- tracking release datetime to run.
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 waiting_sec: A second period value that allow workflow execute.
1108
- :param sleep_interval: A second value that want to waiting until time
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
- run_id: str = run_id or gen_id(self.alias, unique=True)
1115
- rs_release: Result = Result(run_id=run_id)
1116
- runner: CronRunner = self.runner
1117
-
1118
- # NOTE: get next schedule time that generate from now.
1119
- next_time: datetime = runner.date
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
- # NOTE: Add this next running datetime that not in period to queue
1142
- # and remove it to running.
1143
- queue[self.alias].remove(next_time)
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
- time.sleep(0.2)
1146
- return rs_release.catch(status=0, context={})
1173
+ :rtype: WorkflowQueue
1174
+ """
1175
+ if self.runner.date > end_date:
1176
+ return queue
1147
1177
 
1148
- logger.debug(
1149
- f"({cut_id(run_id)}) [CORE]: {self.workflow.name!r} : "
1150
- f"{runner.cron} : Closely to run >> {next_time:%Y-%m-%d %H:%M:%S}"
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
- # NOTE: Release when the time is nearly to schedule time.
1154
- while (duration := get_diff_sec(next_time, tz=config.tz)) > (
1155
- sleep_interval + 5
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
- logger.debug(
1158
- f"({cut_id(run_id)}) [CORE]: {self.workflow.name!r} : "
1159
- f"{runner.cron} : Sleep until: {duration}"
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
- time.sleep(0.5)
1198
+ if self.runner.date > end_date:
1199
+ return queue
1164
1200
 
1165
- # NOTE: Release parameter that use to change if params has
1166
- # templating.
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
- # WARNING: Re-create workflow object that use new running workflow ID.
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
- return rs_release.catch(
1222
- status=0,
1223
- context={
1224
- "params": self.params,
1225
- "release": {"status": "success", "logical_date": next_time},
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: WorkflowTaskData) -> bool:
1213
+ def __eq__(self, other: WorkflowTask) -> bool:
1231
1214
  """Override equal property that will compare only the same type."""
1232
- if isinstance(other, WorkflowTaskData):
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.23
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.2
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
- | `WORKFLOW_CORE_MAX_ON_PER_WORKFLOW` | Core | 5 | | |
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