ddeutil-workflow 0.0.33__py3-none-any.whl → 0.0.34__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.
@@ -5,16 +5,16 @@
5
5
  # ------------------------------------------------------------------------------
6
6
  """
7
7
  The main schedule running is ``schedule_runner`` function that trigger the
8
- multiprocess of ``workflow_control`` function for listing schedules on the
8
+ multiprocess of ``schedule_control`` function for listing schedules on the
9
9
  config by ``Loader.finds(Schedule)``.
10
10
 
11
- The ``workflow_control`` is the scheduler function that release 2 schedule
11
+ The ``schedule_control`` is the scheduler function that release 2 schedule
12
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
17
+ The ``schedule_task`` will run ``task.release`` method in threading object
18
18
  for multithreading strategy. This ``release`` method will run only one crontab
19
19
  value with the on field.
20
20
  """
@@ -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
 
@@ -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_date=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,13 +500,20 @@ 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"{'=' * 80}"
513
+ )
514
+ return result.catch(
515
+ status=Status.SUCCESS, context={"task_date": current_date}
516
+ )
551
517
 
552
518
 
553
519
  def monitor(threads: ReleaseThreads) -> None: # pragma: no cov
@@ -560,69 +526,44 @@ def monitor(threads: ReleaseThreads) -> None: # pragma: no cov
560
526
  logger.debug("[MONITOR]: Start checking long running schedule task.")
561
527
 
562
528
  snapshot_threads: list[str] = list(threads.keys())
563
- for t_name in snapshot_threads:
529
+ for thread_name in snapshot_threads:
564
530
 
565
- thread_release: ReleaseThread = threads[t_name]
531
+ thread_release: ReleaseThread = threads[thread_name]
566
532
 
567
533
  # NOTE: remove the thread that running success.
568
- if not thread_release["thread"].is_alive():
569
- threads.pop(t_name)
534
+ thread = thread_release["thread"]
535
+ if thread and (not thread_release["thread"].is_alive()):
536
+ thread_release["thread"] = None
570
537
 
571
538
 
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.
539
+ def scheduler_pending(
540
+ tasks: list[WorkflowTask],
541
+ stop_date,
542
+ queue,
543
+ threads,
544
+ result: Result,
545
+ audit: type[Audit],
546
+ ) -> Result: # pragma: no cov
547
+ """
582
548
 
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.
549
+ :param tasks:
550
+ :param stop_date:
551
+ :param queue:
552
+ :param threads:
553
+ :param result:
554
+ :param audit:
588
555
 
589
- :rtype: list[str]
556
+ :rtype: Result
590
557
  """
591
- # NOTE: Lazy import Scheduler object from the schedule package.
592
558
  try:
593
559
  from schedule import Scheduler
594
560
  except ImportError:
595
561
  raise ImportError(
596
- "Should install schedule package before use this module."
562
+ "Should install schedule package before use this method."
597
563
  ) from None
598
564
 
599
- # NOTE: Get default logging.
600
- log: type[Audit] = log or get_audit()
601
565
  scheduler: Scheduler = Scheduler()
602
566
 
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
567
  # NOTE: This schedule job will start every minute at :02 seconds.
627
568
  (
628
569
  scheduler.every(1)
@@ -633,7 +574,8 @@ def schedule_control(
633
574
  stop=stop_date,
634
575
  queue=queue,
635
576
  threads=threads,
636
- log=log,
577
+ audit=audit,
578
+ parent_run_id=result.parent_run_id,
637
579
  )
638
580
  .tag("control")
639
581
  )
@@ -651,9 +593,8 @@ def schedule_control(
651
593
  )
652
594
 
653
595
  # NOTE: Start running schedule
654
- logger.info(
655
- f"[SCHEDULE]: Schedule: {schedules} with stopper: "
656
- f"{stop_date:%Y-%m-%d %H:%M:%S}"
596
+ result.trace.info(
597
+ f"[SCHEDULE]: Schedule with stopper: {stop_date:%Y-%m-%d %H:%M:%S}"
657
598
  )
658
599
 
659
600
  while True:
@@ -664,8 +605,8 @@ def schedule_control(
664
605
  if not scheduler.get_jobs("control"):
665
606
  scheduler.clear("monitor")
666
607
 
667
- while len(threads) > 0:
668
- logger.warning(
608
+ while len([t for t in threads.values() if t["thread"]]) > 0:
609
+ result.trace.warning(
669
610
  "[SCHEDULE]: Waiting schedule release thread that still "
670
611
  "running in background."
671
612
  )
@@ -674,17 +615,87 @@ def schedule_control(
674
615
 
675
616
  break
676
617
 
677
- logger.warning(
618
+ result.trace.warning(
678
619
  f"[SCHEDULE]: Queue: {[list(queue[wf].queue) for wf in queue]}"
679
620
  )
680
- return schedules
621
+ return result.catch(
622
+ status=Status.SUCCESS,
623
+ context={
624
+ "threads": [
625
+ {
626
+ "name": thread,
627
+ "start_date": threads[thread]["start_date"],
628
+ "release_date": threads[thread]["release_date"],
629
+ }
630
+ for thread in threads
631
+ ],
632
+ },
633
+ )
634
+
635
+
636
+ def schedule_control(
637
+ schedules: list[str],
638
+ stop: datetime | None = None,
639
+ externals: DictData | None = None,
640
+ *,
641
+ audit: type[Audit] | None = None,
642
+ parent_run_id: str | None = None,
643
+ ) -> Result: # pragma: no cov
644
+ """Scheduler control function that run the chuck of schedules every minute
645
+ and this function release monitoring thread for tracking undead thread in
646
+ the background.
647
+
648
+ :param schedules: A list of workflow names that want to schedule running.
649
+ :param stop: A datetime value that use to stop running schedule.
650
+ :param externals: An external parameters that pass to Loader.
651
+ :param audit: An audit class that use on the workflow task release for
652
+ writing its release audit context.
653
+ :param parent_run_id: A parent workflow running ID for this release.
654
+
655
+ :rtype: Result
656
+ """
657
+ audit: type[Audit] = audit or get_audit()
658
+ result: Result = Result().set_parent_run_id(parent_run_id)
659
+
660
+ # NOTE: Create the start and stop datetime.
661
+ start_date: datetime = datetime.now(tz=config.tz)
662
+ stop_date: datetime = stop or (start_date + config.stop_boundary_delta)
663
+
664
+ # IMPORTANT: Create main mapping of queue and thread object.
665
+ queue: dict[str, ReleaseQueue] = {}
666
+ threads: ReleaseThreads = {}
667
+
668
+ start_date_waiting: datetime = start_date.replace(
669
+ second=0, microsecond=0
670
+ ) + timedelta(minutes=1)
671
+
672
+ tasks: list[WorkflowTask] = []
673
+ for name in schedules:
674
+ tasks.extend(
675
+ Schedule.from_loader(name, externals=externals).tasks(
676
+ start_date_waiting,
677
+ queue=queue,
678
+ externals=externals,
679
+ ),
680
+ )
681
+
682
+ scheduler_pending(
683
+ tasks=tasks,
684
+ stop_date=stop_date,
685
+ queue=queue,
686
+ threads=threads,
687
+ result=result,
688
+ audit=audit,
689
+ )
690
+
691
+ return result.catch(status=Status.SUCCESS, context={"schedules": schedules})
681
692
 
682
693
 
683
694
  def schedule_runner(
684
695
  stop: datetime | None = None,
685
696
  externals: DictData | None = None,
686
697
  excluded: list[str] | None = None,
687
- ) -> list[str]: # pragma: no cov
698
+ ) -> Result: # pragma: no cov
688
699
  """Schedule runner function it the multiprocess controller function for
689
700
  split the setting schedule to the `schedule_control` function on the
690
701
  process pool. It chunks schedule configs that exists in config
@@ -707,9 +718,10 @@ def schedule_runner(
707
718
  --> thread of release task 02 02
708
719
  ==> process 02 ==> ...
709
720
 
710
- :rtype: list[str]
721
+ :rtype: Result
711
722
  """
712
- results: list[str] = []
723
+ result: Result = Result()
724
+ context: DictData = {"schedules": [], "threads": []}
713
725
 
714
726
  with ProcessPoolExecutor(
715
727
  max_workers=config.max_schedule_process,
@@ -721,6 +733,7 @@ def schedule_runner(
721
733
  schedules=[load[0] for load in loader],
722
734
  stop=stop,
723
735
  externals=(externals or {}),
736
+ parent_run_id=result.parent_run_id,
724
737
  )
725
738
  for loader in batch(
726
739
  Loader.finds(Schedule, excluded=excluded),
@@ -735,6 +748,8 @@ def schedule_runner(
735
748
  logger.error(str(err))
736
749
  raise WorkflowException(str(err)) from err
737
750
 
738
- results.extend(future.result(timeout=1))
751
+ rs: Result = future.result(timeout=1)
752
+ context["schedule"].extend(rs.context.get("schedules", []))
753
+ context["threads"].extend(rs.context.get("threads", []))
739
754
 
740
- return results
755
+ return result.catch(status=0, context=context)