ddeutil-workflow 0.0.33__py3-none-any.whl → 0.0.35__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.
@@ -4,18 +4,18 @@
4
4
  # license information.
5
5
  # ------------------------------------------------------------------------------
6
6
  """
7
- The main schedule running is ``schedule_runner`` function that trigger the
8
- multiprocess of ``workflow_control`` function for listing schedules on the
9
- config by ``Loader.finds(Schedule)``.
7
+ The main schedule running is `schedule_runner` function that trigger the
8
+ multiprocess of `schedule_control` function for listing schedules on the
9
+ config by `Loader.finds(Schedule)`.
10
10
 
11
- The ``workflow_control`` is the scheduler function that release 2 schedule
12
- functions; ``workflow_task``, and ``workflow_monitor``.
11
+ The `schedule_control` is the scheduler function that release 2 schedule
12
+ functions; `workflow_task`, and `workflow_monitor`.
13
13
 
14
- ``workflow_control`` --- Every minute at :02 --> ``workflow_task``
15
- --- Every 5 minutes --> ``workflow_monitor``
14
+ `schedule_control` ---( Every minute at :02 )--> `schedule_task`
15
+ ---( Every 5 minutes )--> `monitor`
16
16
 
17
- The ``workflow_task`` will run ``task.release`` method in threading object
18
- for multithreading strategy. This ``release`` method will run only one crontab
17
+ The `schedule_task` will run `task.release` method in threading object
18
+ for multithreading strategy. This `release` method will run only one crontab
19
19
  value with the on field.
20
20
  """
21
21
  from __future__ import annotations
@@ -55,7 +55,7 @@ from .audit import Audit, get_audit
55
55
  from .conf import Loader, config, get_logger
56
56
  from .cron import On
57
57
  from .exceptions import ScheduleException, WorkflowException
58
- from .result import Result
58
+ from .result import Result, Status
59
59
  from .utils import batch, delay
60
60
  from .workflow import Release, ReleaseQueue, Workflow, WorkflowTask
61
61
 
@@ -134,7 +134,7 @@ class ScheduleWorkflow(BaseModel):
134
134
  on: list[str] = [on]
135
135
 
136
136
  if any(not isinstance(n, (dict, str)) for n in on):
137
- raise TypeError("The ``on`` key should be list of str or dict")
137
+ raise TypeError("The `on` key should be list of str or dict")
138
138
 
139
139
  # NOTE: Pass on value to Loader and keep on model object to on
140
140
  # field.
@@ -314,25 +314,19 @@ class Schedule(BaseModel):
314
314
  *,
315
315
  stop: datetime | None = None,
316
316
  externals: DictData | None = None,
317
- log: type[Audit] | None = None,
318
- ) -> None: # pragma: no cov
317
+ audit: type[Audit] | None = None,
318
+ parent_run_id: str | None = None,
319
+ ) -> Result: # pragma: no cov
319
320
  """Pending this schedule tasks with the schedule package.
320
321
 
321
322
  :param stop: A datetime value that use to stop running schedule.
322
323
  :param externals: An external parameters that pass to Loader.
323
- :param log: A log class that use on the workflow task release for
324
- writing its release log context.
324
+ :param audit: An audit class that use on the workflow task release for
325
+ writing its release audit context.
326
+ :param parent_run_id: A parent workflow running ID for this release.
325
327
  """
326
- try:
327
- from schedule import Scheduler
328
- except ImportError:
329
- raise ImportError(
330
- "Should install schedule package before use this method."
331
- ) from None
332
-
333
- # NOTE: Get default logging.
334
- log: type[Audit] = log or get_audit()
335
- scheduler: Scheduler = Scheduler()
328
+ audit: type[Audit] = audit or get_audit()
329
+ result: Result = Result().set_parent_run_id(parent_run_id)
336
330
 
337
331
  # NOTE: Create the start and stop datetime.
338
332
  start_date: datetime = datetime.now(tz=config.tz)
@@ -346,66 +340,23 @@ class Schedule(BaseModel):
346
340
  second=0, microsecond=0
347
341
  ) + timedelta(minutes=1)
348
342
 
349
- # NOTE: This schedule job will start every minute at :02 seconds.
350
- (
351
- scheduler.every(1)
352
- .minutes.at(":02")
353
- .do(
354
- schedule_task,
355
- tasks=self.tasks(
356
- start_date_waiting, queue=queue, externals=externals
357
- ),
358
- stop=stop_date,
359
- queue=queue,
360
- threads=threads,
361
- log=log,
362
- )
363
- .tag("control")
364
- )
365
-
366
- # NOTE: Checking zombie task with schedule job will start every 5 minute at
367
- # :10 seconds.
368
- (
369
- scheduler.every(5)
370
- .minutes.at(":10")
371
- .do(
372
- monitor,
373
- threads=threads,
374
- )
375
- .tag("monitor")
376
- )
377
-
378
- # NOTE: Start running schedule
379
- logger.info(
380
- f"[SCHEDULE]: Schedule with stopper: {stop_date:%Y-%m-%d %H:%M:%S}"
343
+ scheduler_pending(
344
+ tasks=self.tasks(
345
+ start_date_waiting, queue=queue, externals=externals
346
+ ),
347
+ stop=stop_date,
348
+ queue=queue,
349
+ threads=threads,
350
+ result=result,
351
+ audit=audit,
381
352
  )
382
353
 
383
- while True:
384
- scheduler.run_pending()
385
- time.sleep(1)
354
+ return result.catch(status=Status.SUCCESS)
386
355
 
387
- # NOTE: Break the scheduler when the control job does not exist.
388
- if not scheduler.get_jobs("control"):
389
- scheduler.clear("monitor")
390
356
 
391
- while len(threads) > 0:
392
- logger.warning(
393
- "[SCHEDULE]: Waiting schedule release thread that still "
394
- "running in background."
395
- )
396
- delay(10)
397
- monitor(threads)
398
-
399
- break
400
-
401
- logger.warning(
402
- f"[SCHEDULE]: Queue: {[list(queue[wf].queue) for wf in queue]}"
403
- )
404
-
405
-
406
- ResultOrCancelJob = Union[type[CancelJob], Result]
407
- ReturnCancelJob = Callable[P, ResultOrCancelJob]
408
- DecoratorCancelJob = Callable[[ReturnCancelJob], ReturnCancelJob]
357
+ ResultOrCancel = Union[type[CancelJob], Result]
358
+ ReturnResultOrCancel = Callable[P, ResultOrCancel]
359
+ DecoratorCancelJob = Callable[[ReturnResultOrCancel], ReturnResultOrCancel]
409
360
 
410
361
 
411
362
  def catch_exceptions(cancel_on_failure: bool = False) -> DecoratorCancelJob:
@@ -418,10 +369,12 @@ def catch_exceptions(cancel_on_failure: bool = False) -> DecoratorCancelJob:
418
369
  :rtype: DecoratorCancelJob
419
370
  """
420
371
 
421
- def decorator(func: ReturnCancelJob) -> ReturnCancelJob: # pragma: no cov
372
+ def decorator(
373
+ func: ReturnResultOrCancel,
374
+ ) -> ReturnResultOrCancel: # pragma: no cov
422
375
 
423
376
  @wraps(func)
424
- def wrapper(*args: P.args, **kwargs: P.kwargs) -> ResultOrCancelJob:
377
+ def wrapper(*args: P.args, **kwargs: P.kwargs) -> ResultOrCancel:
425
378
  try:
426
379
  return func(*args, **kwargs)
427
380
  except Exception as err:
@@ -438,8 +391,9 @@ def catch_exceptions(cancel_on_failure: bool = False) -> DecoratorCancelJob:
438
391
  class ReleaseThread(TypedDict):
439
392
  """TypeDict for the release thread."""
440
393
 
441
- thread: Thread
394
+ thread: Optional[Thread]
442
395
  start_date: datetime
396
+ release_date: datetime
443
397
 
444
398
 
445
399
  ReleaseThreads = dict[str, ReleaseThread]
@@ -451,8 +405,9 @@ def schedule_task(
451
405
  stop: datetime,
452
406
  queue: dict[str, ReleaseQueue],
453
407
  threads: ReleaseThreads,
454
- log: type[Audit],
455
- ) -> type[CancelJob] | None:
408
+ audit: type[Audit],
409
+ parent_run_id: str | None = None,
410
+ ) -> ResultOrCancel:
456
411
  """Schedule task function that generate thread of workflow task release
457
412
  method in background. This function do the same logic as the workflow poke
458
413
  method, but it runs with map of schedules and the on values.
@@ -464,10 +419,12 @@ def schedule_task(
464
419
  :param stop: A stop datetime object that force stop running scheduler.
465
420
  :param queue: A mapping of alias name and ReleaseQueue object.
466
421
  :param threads: A mapping of alias name and Thread object.
467
- :param log: A log class that want to make log object.
422
+ :param audit: An audit class that want to make audit object.
423
+ :param parent_run_id: A parent workflow running ID for this release.
468
424
 
469
- :rtype: type[CancelJob] | None
425
+ :rtype: ResultOrCancel
470
426
  """
427
+ result: Result = Result().set_parent_run_id(parent_run_id)
471
428
  current_date: datetime = datetime.now(tz=config.tz)
472
429
  if current_date > stop.replace(tzinfo=config.tz):
473
430
  return CancelJob
@@ -490,14 +447,16 @@ def schedule_task(
490
447
  q: ReleaseQueue = queue[task.alias]
491
448
 
492
449
  # NOTE: Start adding queue and move the runner date in the WorkflowTask.
493
- task.queue(stop, q, log=log)
450
+ task.queue(stop, q, audit=audit)
494
451
 
495
452
  # NOTE: Get incoming datetime queue.
496
- logger.debug(f"[WORKFLOW]: Queue: {task.alias!r} : {list(q.queue)}")
453
+ result.trace.debug(
454
+ f"[WORKFLOW]: Queue: {task.alias!r} : {list(q.queue)}"
455
+ )
497
456
 
498
457
  # VALIDATE: Check the queue is empty or not.
499
458
  if not q.is_queued:
500
- logger.warning(
459
+ result.trace.warning(
501
460
  f"[WORKFLOW]: Queue is empty for : {task.alias!r} : "
502
461
  f"{task.runner.cron}"
503
462
  )
@@ -508,7 +467,7 @@ def schedule_task(
508
467
  second=0, microsecond=0
509
468
  )
510
469
  if (first_date := q.first_queue.date) > current_release:
511
- logger.debug(
470
+ result.trace.debug(
512
471
  f"[WORKFLOW]: Skip schedule "
513
472
  f"{first_date:%Y-%m-%d %H:%M:%S} for : {task.alias!r}"
514
473
  )
@@ -523,7 +482,7 @@ def schedule_task(
523
482
  release: Release = heappop(q.queue)
524
483
  heappush(q.running, release)
525
484
 
526
- logger.info(
485
+ result.trace.info(
527
486
  f"[WORKFLOW]: Start thread: '{task.alias}|"
528
487
  f"{release.date:%Y%m%d%H%M}'"
529
488
  )
@@ -533,7 +492,7 @@ def schedule_task(
533
492
  thread_name: str = f"{task.alias}|{release.date:%Y%m%d%H%M}"
534
493
  thread: Thread = Thread(
535
494
  target=catch_exceptions(cancel_on_failure=True)(task.release),
536
- kwargs={"release": release, "queue": q, "log": log},
495
+ kwargs={"release": release, "queue": q, "audit": audit},
537
496
  name=thread_name,
538
497
  daemon=True,
539
498
  )
@@ -541,88 +500,76 @@ def schedule_task(
541
500
  threads[thread_name] = {
542
501
  "thread": thread,
543
502
  "start_date": datetime.now(tz=config.tz),
503
+ "release_date": release.date,
544
504
  }
545
505
 
546
506
  thread.start()
547
507
 
548
508
  delay()
549
509
 
550
- logger.debug(f"[SCHEDULE]: End schedule task {'=' * 80}")
510
+ result.trace.debug(
511
+ f"[SCHEDULE]: End schedule task at {current_date:%Y-%m-%d %H:%M:%S} "
512
+ f"{'=' * 60}"
513
+ )
514
+ return result.catch(
515
+ status=Status.SUCCESS, context={"task_date": current_date}
516
+ )
551
517
 
552
518
 
553
- def monitor(threads: ReleaseThreads) -> None: # pragma: no cov
519
+ def monitor(
520
+ threads: ReleaseThreads,
521
+ parent_run_id: str | None = None,
522
+ ) -> None: # pragma: no cov
554
523
  """Monitoring function that running every five minute for track long-running
555
524
  thread instance from the schedule_control function that run every minute.
556
525
 
557
526
  :param threads: A mapping of Thread object and its name.
527
+ :param parent_run_id: A parent workflow running ID for this release.
528
+
558
529
  :type threads: ReleaseThreads
559
530
  """
560
- logger.debug("[MONITOR]: Start checking long running schedule task.")
531
+ result: Result = Result().set_parent_run_id(parent_run_id)
532
+ result.trace.debug("[MONITOR]: Start checking long running schedule task.")
561
533
 
562
534
  snapshot_threads: list[str] = list(threads.keys())
563
- for t_name in snapshot_threads:
535
+ for thread_name in snapshot_threads:
564
536
 
565
- thread_release: ReleaseThread = threads[t_name]
537
+ thread_release: ReleaseThread = threads[thread_name]
566
538
 
567
539
  # NOTE: remove the thread that running success.
568
- if not thread_release["thread"].is_alive():
569
- threads.pop(t_name)
540
+ thread = thread_release["thread"]
541
+ if thread and (not thread_release["thread"].is_alive()):
542
+ thread_release["thread"] = None
570
543
 
571
544
 
572
- def schedule_control(
573
- schedules: list[str],
574
- stop: datetime | None = None,
575
- externals: DictData | None = None,
576
- *,
577
- log: type[Audit] | None = None,
578
- ) -> list[str]: # pragma: no cov
579
- """Scheduler control function that run the chuck of schedules every minute
580
- and this function release monitoring thread for tracking undead thread in
581
- the background.
545
+ def scheduler_pending(
546
+ tasks: list[WorkflowTask],
547
+ stop: datetime,
548
+ queue: dict[str, ReleaseQueue],
549
+ threads: ReleaseThreads,
550
+ result: Result,
551
+ audit: type[Audit],
552
+ ) -> Result: # pragma: no cov
553
+ """Scheduler pending function.
582
554
 
583
- :param schedules: A list of workflow names that want to schedule running.
584
- :param stop: A datetime value that use to stop running schedule.
585
- :param externals: An external parameters that pass to Loader.
586
- :param log: A log class that use on the workflow task release for writing
587
- its release log context.
555
+ :param tasks: A list of WorkflowTask object.
556
+ :param stop: A stop datetime object that force stop running scheduler.
557
+ :param queue: A mapping of alias name and ReleaseQueue object.
558
+ :param threads: A mapping of alias name and Thread object.
559
+ :param result: A result object.
560
+ :param audit: An audit class that want to make audit object.
588
561
 
589
- :rtype: list[str]
562
+ :rtype: Result
590
563
  """
591
- # NOTE: Lazy import Scheduler object from the schedule package.
592
564
  try:
593
565
  from schedule import Scheduler
594
566
  except ImportError:
595
567
  raise ImportError(
596
- "Should install schedule package before use this module."
568
+ "Should install schedule package before use this method."
597
569
  ) from None
598
570
 
599
- # NOTE: Get default logging.
600
- log: type[Audit] = log or get_audit()
601
571
  scheduler: Scheduler = Scheduler()
602
572
 
603
- # NOTE: Create the start and stop datetime.
604
- start_date: datetime = datetime.now(tz=config.tz)
605
- stop_date: datetime = stop or (start_date + config.stop_boundary_delta)
606
-
607
- # IMPORTANT: Create main mapping of queue and thread object.
608
- queue: dict[str, ReleaseQueue] = {}
609
- threads: ReleaseThreads = {}
610
-
611
- start_date_waiting: datetime = start_date.replace(
612
- second=0, microsecond=0
613
- ) + timedelta(minutes=1)
614
-
615
- tasks: list[WorkflowTask] = []
616
- for name in schedules:
617
- schedule: Schedule = Schedule.from_loader(name, externals=externals)
618
- tasks.extend(
619
- schedule.tasks(
620
- start_date_waiting,
621
- queue=queue,
622
- externals=externals,
623
- ),
624
- )
625
-
626
573
  # NOTE: This schedule job will start every minute at :02 seconds.
627
574
  (
628
575
  scheduler.every(1)
@@ -630,10 +577,11 @@ def schedule_control(
630
577
  .do(
631
578
  schedule_task,
632
579
  tasks=tasks,
633
- stop=stop_date,
580
+ stop=stop,
634
581
  queue=queue,
635
582
  threads=threads,
636
- log=log,
583
+ audit=audit,
584
+ parent_run_id=result.parent_run_id,
637
585
  )
638
586
  .tag("control")
639
587
  )
@@ -651,9 +599,8 @@ def schedule_control(
651
599
  )
652
600
 
653
601
  # NOTE: Start running schedule
654
- logger.info(
655
- f"[SCHEDULE]: Schedule: {schedules} with stopper: "
656
- f"{stop_date:%Y-%m-%d %H:%M:%S}"
602
+ result.trace.info(
603
+ f"[SCHEDULE]: Schedule with stopper: {stop:%Y-%m-%d %H:%M:%S}"
657
604
  )
658
605
 
659
606
  while True:
@@ -664,8 +611,8 @@ def schedule_control(
664
611
  if not scheduler.get_jobs("control"):
665
612
  scheduler.clear("monitor")
666
613
 
667
- while len(threads) > 0:
668
- logger.warning(
614
+ while len([t for t in threads.values() if t["thread"]]) > 0:
615
+ result.trace.warning(
669
616
  "[SCHEDULE]: Waiting schedule release thread that still "
670
617
  "running in background."
671
618
  )
@@ -674,17 +621,87 @@ def schedule_control(
674
621
 
675
622
  break
676
623
 
677
- logger.warning(
624
+ result.trace.warning(
678
625
  f"[SCHEDULE]: Queue: {[list(queue[wf].queue) for wf in queue]}"
679
626
  )
680
- return schedules
627
+ return result.catch(
628
+ status=Status.SUCCESS,
629
+ context={
630
+ "threads": [
631
+ {
632
+ "name": thread,
633
+ "start_date": threads[thread]["start_date"],
634
+ "release_date": threads[thread]["release_date"],
635
+ }
636
+ for thread in threads
637
+ ],
638
+ },
639
+ )
640
+
641
+
642
+ def schedule_control(
643
+ schedules: list[str],
644
+ stop: datetime | None = None,
645
+ externals: DictData | None = None,
646
+ *,
647
+ audit: type[Audit] | None = None,
648
+ parent_run_id: str | None = None,
649
+ ) -> Result: # pragma: no cov
650
+ """Scheduler control function that run the chuck of schedules every minute
651
+ and this function release monitoring thread for tracking undead thread in
652
+ the background.
653
+
654
+ :param schedules: A list of workflow names that want to schedule running.
655
+ :param stop: A datetime value that use to stop running schedule.
656
+ :param externals: An external parameters that pass to Loader.
657
+ :param audit: An audit class that use on the workflow task release for
658
+ writing its release audit context.
659
+ :param parent_run_id: A parent workflow running ID for this release.
660
+
661
+ :rtype: Result
662
+ """
663
+ audit: type[Audit] = audit or get_audit()
664
+ result: Result = Result().set_parent_run_id(parent_run_id)
665
+
666
+ # NOTE: Create the start and stop datetime.
667
+ start_date: datetime = datetime.now(tz=config.tz)
668
+ stop_date: datetime = stop or (start_date + config.stop_boundary_delta)
669
+
670
+ # IMPORTANT: Create main mapping of queue and thread object.
671
+ queue: dict[str, ReleaseQueue] = {}
672
+ threads: ReleaseThreads = {}
673
+
674
+ start_date_waiting: datetime = start_date.replace(
675
+ second=0, microsecond=0
676
+ ) + timedelta(minutes=1)
677
+
678
+ tasks: list[WorkflowTask] = []
679
+ for name in schedules:
680
+ tasks.extend(
681
+ Schedule.from_loader(name, externals=externals).tasks(
682
+ start_date_waiting,
683
+ queue=queue,
684
+ externals=externals,
685
+ ),
686
+ )
687
+
688
+ scheduler_pending(
689
+ tasks=tasks,
690
+ stop=stop_date,
691
+ queue=queue,
692
+ threads=threads,
693
+ result=result,
694
+ audit=audit,
695
+ )
696
+
697
+ return result.catch(status=Status.SUCCESS, context={"schedules": schedules})
681
698
 
682
699
 
683
700
  def schedule_runner(
684
701
  stop: datetime | None = None,
685
702
  externals: DictData | None = None,
686
703
  excluded: list[str] | None = None,
687
- ) -> list[str]: # pragma: no cov
704
+ ) -> Result: # pragma: no cov
688
705
  """Schedule runner function it the multiprocess controller function for
689
706
  split the setting schedule to the `schedule_control` function on the
690
707
  process pool. It chunks schedule configs that exists in config
@@ -696,20 +713,22 @@ def schedule_runner(
696
713
 
697
714
  This function will get all workflows that include on value that was
698
715
  created in config path and chuck it with application config variable
699
- ``WORKFLOW_APP_MAX_SCHEDULE_PER_PROCESS`` env var to multiprocess executor
716
+ `WORKFLOW_APP_MAX_SCHEDULE_PER_PROCESS` env var to multiprocess executor
700
717
  pool.
701
718
 
702
719
  The current workflow logic that split to process will be below diagram:
703
720
 
704
- MAIN ==> process 01 ==> schedule --> thread of release task 01 01
705
- --> thread of release task 01 02
706
- ==> schedule --> thread of release task 02 01
707
- --> thread of release task 02 02
721
+ MAIN ==> process 01 ==> schedule ==> thread 01 --> 01
722
+ ==> thread 01 --> 02
723
+ ==> schedule ==> thread 02 --> 01
724
+ ==> thread 02 --> 02
725
+ ==> ...
708
726
  ==> process 02 ==> ...
709
727
 
710
- :rtype: list[str]
728
+ :rtype: Result
711
729
  """
712
- results: list[str] = []
730
+ result: Result = Result()
731
+ context: DictData = {"schedules": [], "threads": []}
713
732
 
714
733
  with ProcessPoolExecutor(
715
734
  max_workers=config.max_schedule_process,
@@ -721,6 +740,7 @@ def schedule_runner(
721
740
  schedules=[load[0] for load in loader],
722
741
  stop=stop,
723
742
  externals=(externals or {}),
743
+ parent_run_id=result.parent_run_id,
724
744
  )
725
745
  for loader in batch(
726
746
  Loader.finds(Schedule, excluded=excluded),
@@ -735,6 +755,8 @@ def schedule_runner(
735
755
  logger.error(str(err))
736
756
  raise WorkflowException(str(err)) from err
737
757
 
738
- results.extend(future.result(timeout=1))
758
+ rs: Result = future.result(timeout=1)
759
+ context["schedule"].extend(rs.context.get("schedules", []))
760
+ context["threads"].extend(rs.context.get("threads", []))
739
761
 
740
- return results
762
+ return result.catch(status=0, context=context)