orionis 0.524.0__py3-none-any.whl → 0.525.0__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.
@@ -14,7 +14,6 @@ from apscheduler.events import (
14
14
  EVENT_JOB_EXECUTED,
15
15
  EVENT_JOB_MISSED,
16
16
  EVENT_JOB_MAX_INSTANCES,
17
- EVENT_JOB_MODIFIED,
18
17
  EVENT_JOB_REMOVED
19
18
  )
20
19
  from apscheduler.schedulers.asyncio import AsyncIOScheduler as APSAsyncIOScheduler
@@ -25,15 +24,8 @@ from orionis.console.contracts.event import IEvent
25
24
  from orionis.console.contracts.reactor import IReactor
26
25
  from orionis.console.contracts.schedule import ISchedule
27
26
  from orionis.console.contracts.schedule_event_listener import IScheduleEventListener
28
- from orionis.console.entities.job_error import JobError
29
- from orionis.console.entities.job_executed import JobExecuted
30
- from orionis.console.entities.job_max_instances import JobMaxInstances
31
- from orionis.console.entities.job_missed import JobMissed
32
- from orionis.console.entities.job_modified import JobModified
33
- from orionis.console.entities.job_pause import JobPause
34
- from orionis.console.entities.job_removed import JobRemoved
35
- from orionis.console.entities.job_resume import JobResume
36
- from orionis.console.entities.job_submitted import JobSubmitted
27
+ from orionis.console.entities.event_job import EventJob
28
+ from orionis.console.entities.scheduler_error import SchedulerError
37
29
  from orionis.console.entities.scheduler_paused import SchedulerPaused
38
30
  from orionis.console.entities.scheduler_resumed import SchedulerResumed
39
31
  from orionis.console.entities.scheduler_shutdown import SchedulerShutdown
@@ -111,10 +103,10 @@ class Scheduler(ISchedule):
111
103
  self.__listeners: Dict[str, callable] = {}
112
104
 
113
105
  # Initialize set to track jobs paused by pauseEverythingAt
114
- self.__paused_by_pause_everything: set = set()
106
+ self.__pausedByPauseEverything: set = set()
115
107
 
116
108
  # Add this line to the existing __init__ method
117
- self._stop_event: Optional[asyncio.Event] = None
109
+ self._stopEvent: Optional[asyncio.Event] = None
118
110
 
119
111
  def __getCurrentTime(
120
112
  self
@@ -297,6 +289,113 @@ class Scheduler(ISchedule):
297
289
  # Return the Event instance for further scheduling configuration
298
290
  return self.__events[signature]
299
291
 
292
+ def __getTaskFromSchedulerById(
293
+ self,
294
+ id: str,
295
+ code: int = None
296
+ ) -> Optional[EventJob]:
297
+ """
298
+ Retrieve a scheduled job from the AsyncIOScheduler by its unique ID.
299
+
300
+ This method fetches a job from the AsyncIOScheduler using its unique identifier (ID).
301
+ It extracts the job's attributes and creates a `Job` entity containing the relevant
302
+ details. If the job does not exist, the method returns `None`.
303
+
304
+ Parameters
305
+ ----------
306
+ id : str
307
+ The unique identifier (ID) of the job to retrieve. This must be a non-empty string.
308
+
309
+ Returns
310
+ -------
311
+ Optional[Job]
312
+ A `Job` entity containing the details of the scheduled job if it exists.
313
+ If the job does not exist, returns `None`.
314
+ """
315
+
316
+ # Extract event data from the internal events list if available
317
+ event_data: dict = {}
318
+ for job in self.events():
319
+ if id == job.get('signature'):
320
+ event_data = job
321
+ break
322
+
323
+ # Retrieve the job data from the scheduler using the provided job ID
324
+ data = self.__scheduler.get_job(id)
325
+
326
+ # If no job is found, return EventJob with default values
327
+ _id = data.id if data and hasattr(data, 'id') else None
328
+ if not _id and code == EVENT_JOB_MISSED:
329
+ _id = event_data.get('signature', None)
330
+ elif not _id:
331
+ return EventJob()
332
+
333
+ # Extract the job name if available
334
+ _name = data.name if data and hasattr(data, 'name') else None
335
+
336
+ # Extract the job function if available
337
+ _func = data.func if data and hasattr(data, 'func') else None
338
+
339
+ # Extract the job arguments if available
340
+ _args = data.args if data and hasattr(data, 'args') else tuple(event_data.get('args', []))
341
+
342
+ # Extract the job trigger if available
343
+ _trigger = data.trigger if data and hasattr(data, 'trigger') else None
344
+
345
+ # Extract the job executor if available
346
+ _executor = data.executor if data and hasattr(data, 'executor') else None
347
+
348
+ # Extract the job jobstore if available
349
+ _jobstore = data.jobstore if data and hasattr(data, 'jobstore') else None
350
+
351
+ # Extract the job misfire_grace_time if available
352
+ _misfire_grace_time = data.misfire_grace_time if data and hasattr(data, 'misfire_grace_time') else None
353
+
354
+ # Extract the job max_instances if available
355
+ _max_instances = data.max_instances if data and hasattr(data, 'max_instances') else 0
356
+
357
+ # Extract the job coalesce if available
358
+ _coalesce = data.coalesce if data and hasattr(data, 'coalesce') else False
359
+
360
+ # Extract the job next_run_time if available
361
+ _next_run_time = data.next_run_time if data and hasattr(data, 'next_run_time') else None
362
+
363
+ # Extract additional event data if available
364
+ _purpose = event_data.get('purpose', None)
365
+
366
+ # Extract additional event data if available
367
+ _start_date = event_data.get('start_date', None)
368
+
369
+ # Extract additional event data if available
370
+ _end_date = event_data.get('end_date', None)
371
+
372
+ # Extract additional event data if available
373
+ _details = event_data.get('details', None)
374
+
375
+
376
+ # Create and return a Job entity based on the retrieved job data
377
+ return EventJob(
378
+ id=_id,
379
+ code=code if code is not None else 0,
380
+ name=_name,
381
+ func=_func,
382
+ args=_args,
383
+ trigger=_trigger,
384
+ executor=_executor,
385
+ jobstore=_jobstore,
386
+ misfire_grace_time=_misfire_grace_time,
387
+ max_instances=_max_instances,
388
+ coalesce=_coalesce,
389
+ next_run_time=_next_run_time,
390
+ exception = None,
391
+ traceback = None,
392
+ retval = None,
393
+ purpose = _purpose,
394
+ start_date = _start_date,
395
+ end_date = _end_date,
396
+ details = _details
397
+ )
398
+
300
399
  def __subscribeListeners(
301
400
  self
302
401
  ) -> None:
@@ -325,12 +424,11 @@ class Scheduler(ISchedule):
325
424
  self.__scheduler.add_listener(self.__executedListener, EVENT_JOB_EXECUTED)
326
425
  self.__scheduler.add_listener(self.__missedListener, EVENT_JOB_MISSED)
327
426
  self.__scheduler.add_listener(self.__maxInstancesListener, EVENT_JOB_MAX_INSTANCES)
328
- self.__scheduler.add_listener(self.__modifiedListener, EVENT_JOB_MODIFIED)
329
427
  self.__scheduler.add_listener(self.__removedListener, EVENT_JOB_REMOVED)
330
428
 
331
429
  def __globalCallableListener(
332
430
  self,
333
- event_data: Optional[Union[SchedulerStarted, SchedulerPaused, SchedulerResumed, SchedulerShutdown, JobError]],
431
+ event_data: Optional[Union[SchedulerStarted, SchedulerPaused, SchedulerResumed, SchedulerShutdown, SchedulerError]],
334
432
  listening_vent: ListeningEvent
335
433
  ) -> None:
336
434
  """
@@ -342,7 +440,7 @@ class Scheduler(ISchedule):
342
440
 
343
441
  Parameters
344
442
  ----------
345
- event_data : Optional[Union[SchedulerStarted, SchedulerPaused, SchedulerResumed, SchedulerShutdown, JobError]]
443
+ event_data : Optional[Union[SchedulerStarted, SchedulerPaused, SchedulerResumed, SchedulerShutdown, ...]]
346
444
  The event data associated with the global scheduler event. This can include details about the event,
347
445
  such as its type and context. If no specific data is available, this parameter can be None.
348
446
  listening_vent : ListeningEvent
@@ -370,46 +468,40 @@ class Scheduler(ISchedule):
370
468
  # Check if a listener is registered for the specified event
371
469
  if scheduler_event in self.__listeners:
372
470
 
373
- # Retrieve the listener for the specified event
471
+ # Get the listener for the specified event
374
472
  listener = self.__listeners[scheduler_event]
375
473
 
376
- # Ensure the listener is callable before invoking it
474
+ # If is Callable
377
475
  if callable(listener):
378
476
 
379
477
  try:
380
478
 
381
- # If the listener is a coroutine, schedule it as an asyncio task
479
+ # If the listener is a coroutine, handle it properly
382
480
  if asyncio.iscoroutinefunction(listener):
383
481
 
384
- # Try to get the running event loop
385
482
  try:
386
483
 
387
- try:
388
-
389
- # Try to get the current running event loop
390
- loop = asyncio.get_running_loop()
391
- loop.create_task(listener(event_data, self))
392
-
393
- except RuntimeError:
484
+ # Try to get the current running event loop
485
+ loop = asyncio.get_running_loop()
394
486
 
395
- # If no event loop is running, create a new one
396
- loop = asyncio.new_event_loop()
397
- asyncio.set_event_loop(loop)
398
- loop.create_task(listener(event_data, self))
487
+ # Schedule the coroutine as a task in the current loop
488
+ loop.create_task(listener(event_data, self))
399
489
 
400
- # If no event loop is running, log a warning instead of creating one
401
490
  except RuntimeError:
402
491
 
403
- # Message indicating that the listener could not be invoked
404
- error_msg = f"Cannot run listener for '{scheduler_event}': no event loop running"
492
+ # No running event loop, create a new one
493
+ try:
405
494
 
406
- # Log the error message
407
- self.__logger.error(error_msg)
495
+ # Create and run the coroutine in a new event loop
496
+ asyncio.run(listener(event_data, self))
408
497
 
409
- # Raise an error to inform the caller that the listener could not be invoked
410
- raise CLIOrionisRuntimeError(error_msg)
498
+ except Exception as e:
499
+
500
+ # Handle exceptions during coroutine execution
501
+ error_msg = f"Failed to run async listener for '{scheduler_event}': {str(e)}"
502
+ self.__logger.error(error_msg)
503
+ raise CLIOrionisRuntimeError(error_msg) from e
411
504
 
412
- # Otherwise, invoke the listener directly as a regular function
413
505
  else:
414
506
 
415
507
  # Call the regular listener function directly
@@ -421,18 +513,14 @@ class Scheduler(ISchedule):
421
513
  if isinstance(e, CLIOrionisRuntimeError):
422
514
  raise e
423
515
 
424
- # Construct the error message
516
+ # Construct and log error message
425
517
  error_msg = f"An error occurred while invoking the listener for event '{scheduler_event}': {str(e)}"
426
-
427
- # Log the error message
428
518
  self.__logger.error(error_msg)
429
-
430
- # Raise a runtime error if listener invocation fails
431
- raise CLIOrionisRuntimeError(error_msg)
519
+ raise CLIOrionisRuntimeError(error_msg) from e
432
520
 
433
521
  def __taskCallableListener(
434
522
  self,
435
- event_data: Optional[Union[JobError, JobExecuted, JobSubmitted, JobMissed, JobMaxInstances]],
523
+ event_data: EventJob,
436
524
  listening_vent: ListeningEvent
437
525
  ) -> None:
438
526
  """
@@ -445,7 +533,7 @@ class Scheduler(ISchedule):
445
533
 
446
534
  Parameters
447
535
  ----------
448
- event_data : Optional[Union[JobError, JobExecuted, JobSubmitted, JobMissed, JobMaxInstances]]
536
+ event_data : EventJob
449
537
  The event data associated with the task/job event. This includes details about the job,
450
538
  such as its ID, exception (if any), and other context. If no specific data is available,
451
539
  this parameter can be None.
@@ -466,23 +554,29 @@ class Scheduler(ISchedule):
466
554
 
467
555
  # Validate that the provided event is an instance of ListeningEvent
468
556
  if not isinstance(listening_vent, ListeningEvent):
469
- raise CLIOrionisValueError("The event must be an instance of ListeningEvent.")
557
+ error_msg = "The event must be an instance of ListeningEvent."
558
+ self.__logger.error(error_msg)
559
+ raise CLIOrionisValueError(error_msg)
470
560
 
471
- # Validate that event_data is not None and has a job_id attribute
472
- if event_data is None or not hasattr(event_data, 'job_id'):
561
+ # Validate that event_data is not None and has a id attribute
562
+ if not isinstance(event_data, EventJob) or not hasattr(event_data, 'id') or not event_data.id:
473
563
  return
474
564
 
475
565
  # Retrieve the global identifier for the event from the ListeningEvent enum
476
566
  scheduler_event = listening_vent.value
477
567
 
478
568
  # Check if a listener is registered for the specific job ID in the event data
479
- if event_data.job_id in self.__listeners:
569
+ if event_data.id in self.__listeners:
480
570
 
481
571
  # Retrieve the listener for the specific job ID
482
- listener = self.__listeners[event_data.job_id]
572
+ listener = self.__listeners[event_data.id]
483
573
 
484
574
  # Check if the listener is an instance of IScheduleEventListener
485
- if isinstance(listener, IScheduleEventListener):
575
+ if issubclass(listener, IScheduleEventListener):
576
+
577
+ # Initialize the listener if it's a class
578
+ if isinstance(listener, type):
579
+ listener = listener()
486
580
 
487
581
  # Check if the listener has a method corresponding to the event type
488
582
  if hasattr(listener, scheduler_event) and callable(getattr(listener, scheduler_event)):
@@ -490,44 +584,37 @@ class Scheduler(ISchedule):
490
584
  # Retrieve the method from the listener
491
585
  listener_method = getattr(listener, scheduler_event)
492
586
 
493
- # Try to invoke the listener method
494
587
  try:
495
588
 
496
- # Invoke the listener method, handling both coroutine and regular functions
589
+ # If the listener_method is a coroutine, handle it properly
497
590
  if asyncio.iscoroutinefunction(listener_method):
498
591
 
499
- # Try to get the running event loop
500
592
  try:
501
593
 
502
- try:
503
-
504
- # Try to get the current running event loop
505
- loop = asyncio.get_running_loop()
506
- loop.create_task(listener_method(event_data, self))
507
-
508
- except RuntimeError:
594
+ # Try to get the current running event loop
595
+ loop = asyncio.get_running_loop()
509
596
 
510
- # If no event loop is running, create a new one
511
- loop = asyncio.new_event_loop()
512
- asyncio.set_event_loop(loop)
513
- loop.create_task(listener_method(event_data, self))
597
+ # Schedule the coroutine as a task in the current loop
598
+ loop.create_task(listener_method(event_data, self))
514
599
 
515
- # If no event loop is running, log a warning
516
600
  except RuntimeError:
517
601
 
518
- # Construct the error message
519
- error_msg = f"Cannot run async listener for '{scheduler_event}' on job '{event_data.job_id}': no event loop running"
602
+ # No running event loop, create a new one
603
+ try:
520
604
 
521
- # Log the error message
522
- self.__logger.error(error_msg)
605
+ # Create and run the coroutine in a new event loop
606
+ asyncio.run(listener_method(event_data, self))
607
+
608
+ except Exception as e:
523
609
 
524
- # Raise an error to inform the caller that the listener could not be invoked
525
- raise CLIOrionisRuntimeError(error_msg)
610
+ # Handle exceptions during coroutine execution
611
+ error_msg = f"Failed to run async listener_method for '{scheduler_event}': {str(e)}"
612
+ self.__logger.error(error_msg)
613
+ raise CLIOrionisRuntimeError(error_msg) from e
526
614
 
527
- # Call the regular listener method directly
528
615
  else:
529
616
 
530
- # Invoke the listener method directly
617
+ # Call the regular listener_method function directly
531
618
  listener_method(event_data, self)
532
619
 
533
620
  except Exception as e:
@@ -536,18 +623,24 @@ class Scheduler(ISchedule):
536
623
  if isinstance(e, CLIOrionisRuntimeError):
537
624
  raise e
538
625
 
539
- # Construct the error message
540
- error_msg = (f"An error occurred while invoking the listener for event '{scheduler_event}' on job '{event_data.job_id}': {str(e)}")
541
-
542
- # Log the error message
626
+ # Construct and log error message
627
+ error_msg = f"An error occurred while invoking the listener_method for event '{scheduler_event}': {str(e)}"
543
628
  self.__logger.error(error_msg)
629
+ raise CLIOrionisRuntimeError(error_msg) from e
630
+
631
+ else:
544
632
 
545
- # Raise a runtime error if listener invocation fails
546
- raise CLIOrionisRuntimeError(error_msg)
633
+ # Log a warning if the listener does not have the required method
634
+ self.__logger.warning(f"The listener for job ID '{event_data.id}' does not have a method for event '{scheduler_event}'.")
635
+
636
+ else:
637
+
638
+ # Log a warning if the listener is not an instance of IScheduleEventListener
639
+ self.__logger.warning(f"The listener for job ID '{event_data.id}' is not an instance of IScheduleEventListener.")
547
640
 
548
641
  def __startedListener(
549
642
  self,
550
- event: SchedulerStarted
643
+ event
551
644
  ) -> None:
552
645
  """
553
646
  Handle the scheduler started event for logging and invoking registered listeners.
@@ -595,14 +688,21 @@ class Scheduler(ISchedule):
595
688
  self.__rich_console.line()
596
689
 
597
690
  # Check if a listener is registered for the scheduler started event
598
- self.__globalCallableListener(event, ListeningEvent.SCHEDULER_STARTED)
691
+ event_data = SchedulerStarted(
692
+ code=event.code if hasattr(event, 'code') else 0,
693
+ time=now,
694
+ tasks=self.events()
695
+ )
696
+
697
+ # If a listener is registered for this event, invoke the listener with the event details
698
+ self.__globalCallableListener(event_data, ListeningEvent.SCHEDULER_STARTED)
599
699
 
600
700
  # Log an informational message indicating that the scheduler has started
601
701
  self.__logger.info(f"Orionis Scheduler started successfully at {now}.")
602
702
 
603
703
  def __shutdownListener(
604
704
  self,
605
- event: SchedulerShutdown
705
+ event
606
706
  ) -> None:
607
707
  """
608
708
  Handle the scheduler shutdown event for logging and invoking registered listeners.
@@ -629,14 +729,19 @@ class Scheduler(ISchedule):
629
729
  now = self.__getCurrentTime()
630
730
 
631
731
  # Check if a listener is registered for the scheduler shutdown event
632
- self.__globalCallableListener(event, ListeningEvent.SCHEDULER_SHUTDOWN)
732
+ event_data = SchedulerShutdown(
733
+ code=event.code if hasattr(event, 'code') else 0,
734
+ time=now,
735
+ tasks=self.events()
736
+ )
737
+ self.__globalCallableListener(event_data, ListeningEvent.SCHEDULER_SHUTDOWN)
633
738
 
634
739
  # Log an informational message indicating that the scheduler has shut down
635
740
  self.__logger.info(f"Orionis Scheduler shut down successfully at {now}.")
636
741
 
637
742
  def __errorListener(
638
743
  self,
639
- event: JobError
744
+ event
640
745
  ) -> None:
641
746
  """
642
747
  Handle job error events for logging and error reporting.
@@ -663,14 +768,31 @@ class Scheduler(ISchedule):
663
768
  self.__logger.error(f"Task {event.job_id} raised an exception: {event.exception}")
664
769
 
665
770
  # If a listener is registered for this job ID, invoke the listener with the event details
666
- self.__taskCallableListener(event, ListeningEvent.JOB_ON_FAILURE)
771
+ event_data = self.__getTaskFromSchedulerById(event.job_id)
772
+ event_data.code = event.code if hasattr(event, 'code') else 0
773
+ event_data.exception = event.exception if hasattr(event, 'exception') else None
774
+ event_data.traceback = event.traceback if hasattr(event, 'traceback') else None
775
+
776
+ # Call the task-specific listener for job errors
777
+ self.__taskCallableListener(
778
+ event_data,
779
+ ListeningEvent.JOB_ON_FAILURE
780
+ )
667
781
 
668
- # Check if a listener is registered for the scheduler started event
669
- self.__globalCallableListener(event, ListeningEvent.SCHEDULER_ERROR)
782
+ # Check if a listener is registered for the scheduler error event
783
+ event_data = SchedulerError(
784
+ code=event.code if hasattr(event, 'code') else 0,
785
+ exception=event.exception if hasattr(event, 'exception') else None,
786
+ traceback=event.traceback if hasattr(event, 'traceback') else None,
787
+ )
788
+ self.__globalCallableListener(
789
+ event_data,
790
+ ListeningEvent.SCHEDULER_ERROR
791
+ )
670
792
 
671
793
  def __submittedListener(
672
794
  self,
673
- event: JobSubmitted
795
+ event
674
796
  ) -> None:
675
797
  """
676
798
  Handle job submission events for logging and error reporting.
@@ -696,12 +818,15 @@ class Scheduler(ISchedule):
696
818
  # Log an informational message indicating that the job has been submitted
697
819
  self.__logger.info(f"Task {event.job_id} submitted to executor.")
698
820
 
821
+ # Create entity for job submitted event
822
+ data_event = self.__getTaskFromSchedulerById(event.job_id, event.code)
823
+
699
824
  # If a listener is registered for this job ID, invoke the listener with the event details
700
- self.__taskCallableListener(event, ListeningEvent.JOB_BEFORE)
825
+ self.__taskCallableListener(data_event, ListeningEvent.JOB_BEFORE)
701
826
 
702
827
  def __executedListener(
703
828
  self,
704
- event: JobExecuted
829
+ event
705
830
  ) -> None:
706
831
  """
707
832
  Handle job execution events for logging and error reporting.
@@ -728,12 +853,15 @@ class Scheduler(ISchedule):
728
853
  # Log an informational message indicating that the job has been executed
729
854
  self.__logger.info(f"Task {event.job_id} executed.")
730
855
 
856
+ # Create entity for job executed event
857
+ data_event = self.__getTaskFromSchedulerById(event.job_id, event.code)
858
+
731
859
  # If a listener is registered for this job ID, invoke the listener with the event details
732
- self.__taskCallableListener(event, ListeningEvent.JOB_AFTER)
860
+ self.__taskCallableListener(data_event, ListeningEvent.JOB_AFTER)
733
861
 
734
862
  def __missedListener(
735
863
  self,
736
- event: JobMissed
864
+ event
737
865
  ) -> None:
738
866
  """
739
867
  Handle job missed events for debugging and error reporting.
@@ -760,12 +888,15 @@ class Scheduler(ISchedule):
760
888
  # Log a warning indicating that the job was missed
761
889
  self.__logger.warning(f"Task {event.job_id} was missed. It was scheduled to run at {event.scheduled_run_time}.")
762
890
 
891
+ # Create entity for job missed event
892
+ data_event = self.__getTaskFromSchedulerById(event.job_id, event.code)
893
+
763
894
  # If a listener is registered for this job ID, invoke the listener with the event details
764
- self.__taskCallableListener(event, ListeningEvent.JOB_ON_MISSED)
895
+ self.__taskCallableListener(data_event, ListeningEvent.JOB_ON_MISSED)
765
896
 
766
897
  def __maxInstancesListener(
767
898
  self,
768
- event: JobMaxInstances
899
+ event
769
900
  ) -> None:
770
901
  """
771
902
  Handle job max instances events for logging and error reporting.
@@ -792,46 +923,15 @@ class Scheduler(ISchedule):
792
923
  # Log an error message indicating that the job exceeded maximum instances
793
924
  self.__logger.error(f"Task {event.job_id} exceeded maximum instances")
794
925
 
795
- # If a listener is registered for this job ID, invoke the listener with the event details
796
- self.__taskCallableListener(event, ListeningEvent.JOB_ON_MAXINSTANCES)
797
-
798
- def __modifiedListener(
799
- self,
800
- event: JobModified
801
- ) -> None:
802
- """
803
- Handle job modified events for logging and error reporting.
804
-
805
- This method is triggered when a job is modified. It logs an informational
806
- message indicating that the job has been modified successfully. If the application
807
- is in debug mode, it also displays a message on the console. Additionally, if a
808
- listener is registered for the modified job, it invokes the listener with the
809
- event details.
810
-
811
- Parameters
812
- ----------
813
- event : JobModified
814
- An instance of the JobModified event containing details about the modified job,
815
- including its ID and other relevant information.
816
-
817
- Returns
818
- -------
819
- None
820
- This method does not return any value. It performs logging, error reporting,
821
- and listener invocation for the job modified event.
822
- """
926
+ # Create entity for job max instances event
927
+ data_event = self.__getTaskFromSchedulerById(event.job_id, event.code)
823
928
 
824
929
  # If a listener is registered for this job ID, invoke the listener with the event details
825
- if event.next_run_time is None:
826
- self.__logger.info(f"Task {event.job_id} has been paused.")
827
- self.__taskCallableListener(event, ListeningEvent.JOB_ON_PAUSED)
828
- else:
829
- self.__logger.info(f"Task {event.job_id} has been resumed.")
830
- self.__taskCallableListener(event, ListeningEvent.JOB_ON_RESUMED)
930
+ self.__taskCallableListener(data_event, ListeningEvent.JOB_ON_MAXINSTANCES)
831
931
 
832
932
  def __removedListener(
833
933
  self,
834
- event: JobRemoved
934
+ event
835
935
  ) -> None:
836
936
  """
837
937
  Handle job removal events for logging and invoking registered listeners.
@@ -857,8 +957,11 @@ class Scheduler(ISchedule):
857
957
  # Log the removal of the job
858
958
  self.__logger.info(f"Task {event.job_id} has been removed.")
859
959
 
960
+ # Create entity for job removed event
961
+ data_event = self.__getTaskFromSchedulerById(event.job_id, event.code)
962
+
860
963
  # If a listener is registered for this job ID, invoke the listener with the event details
861
- self.__taskCallableListener(event, ListeningEvent.JOB_ON_REMOVED)
964
+ self.__taskCallableListener(data_event, ListeningEvent.JOB_ON_REMOVED)
862
965
 
863
966
  def __loadEvents(
864
967
  self
@@ -996,25 +1099,34 @@ class Scheduler(ISchedule):
996
1099
 
997
1100
  Raises
998
1101
  ------
999
- ValueError
1000
- If the provided `func` is not an asynchronous function (coroutine).
1102
+ CLIOrionisRuntimeError
1103
+ If the provided `func` is not an asynchronous function or execution fails.
1001
1104
  """
1002
1105
 
1003
- # Define a synchronous wrapper function
1004
1106
  def sync_wrapper(*args, **kwargs) -> Any:
1107
+
1005
1108
  try:
1109
+
1006
1110
  # Try to get the current event loop
1007
- loop = asyncio.get_event_loop()
1008
- if loop.is_running():
1009
- # If the loop is already running, create a task for the coroutine
1010
- return asyncio.create_task(func(*args, **kwargs))
1011
- else:
1012
- # If the loop is not running, run the coroutine until complete
1013
- return loop.run_until_complete(func(*args, **kwargs))
1014
- except RuntimeError:
1015
- # If no event loop exists, create a new one and run the coroutine
1016
- return asyncio.run(func(*args, **kwargs))
1111
+ try:
1112
+
1113
+ # If we're already in an event loop, create a task
1114
+ loop = asyncio.get_running_loop()
1115
+ task = loop.create_task(func(*args, **kwargs))
1116
+ return task
1017
1117
 
1118
+ except RuntimeError:
1119
+
1120
+ # No running loop, so we can run the coroutine directly
1121
+ return asyncio.run(func(*args, **kwargs))
1122
+
1123
+ except Exception as e:
1124
+
1125
+ # Log the error and re-raise
1126
+ self.__logger.error(f"Error executing async function: {str(e)}")
1127
+ raise CLIOrionisRuntimeError(f"Failed to execute async function: {str(e)}") from e
1128
+
1129
+ # Return the synchronous wrapper function
1018
1130
  return sync_wrapper
1019
1131
 
1020
1132
  def pauseEverythingAt(
@@ -1059,7 +1171,7 @@ class Scheduler(ISchedule):
1059
1171
  if self.isRunning():
1060
1172
 
1061
1173
  # Clear the set of previously paused jobs
1062
- self.__paused_by_pause_everything.clear()
1174
+ self.__pausedByPauseEverything.clear()
1063
1175
 
1064
1176
  # Get all jobs from the scheduler
1065
1177
  all_jobs = self.__scheduler.get_jobs()
@@ -1074,30 +1186,38 @@ class Scheduler(ISchedule):
1074
1186
  # Pause only user jobs, not system jobs
1075
1187
  for job in all_jobs:
1076
1188
 
1077
- # Pause the job only if it is not a system job
1189
+ # Check if the job is not a system job
1078
1190
  if job.id not in system_job_ids:
1079
1191
 
1080
- # Pause the job and add its ID to the set of paused jobs
1081
1192
  try:
1082
1193
 
1083
1194
  # Pause the job in the scheduler
1084
1195
  self.__scheduler.pause_job(job.id)
1085
- self.__paused_by_pause_everything.add(job.id)
1086
-
1087
- # Execute the task callable listener
1088
- self.__globalCallableListener(SchedulerPaused(EVENT_SCHEDULER_PAUSED), ListeningEvent.SCHEDULER_PAUSED)
1196
+ self.__pausedByPauseEverything.add(job.id)
1089
1197
 
1090
1198
  # Get the current time in the configured timezone
1091
1199
  now = self.__getCurrentTime()
1092
1200
 
1093
- # Log an informational message indicating that the scheduler has resumed
1094
- self.__logger.info(f"Orionis Scheduler resumed successfully at {now}.")
1201
+ # Log an informational message indicating that the job has been paused
1202
+ self.__taskCallableListener(
1203
+ self.__getTaskFromSchedulerById(job.id),
1204
+ ListeningEvent.JOB_ON_PAUSED
1205
+ )
1206
+
1207
+ # Log the pause action
1208
+ self.__logger.info(f"Job '{job.id}' paused successfully at {now}.")
1095
1209
 
1096
1210
  except Exception as e:
1097
1211
 
1098
1212
  # Log a warning if pausing a job fails, but continue with others
1099
1213
  self.__logger.warning(f"Failed to pause job '{job.id}': {str(e)}")
1100
1214
 
1215
+ # Execute the global callable listener after all jobs are paused
1216
+ self.__globalCallableListener(SchedulerPaused(
1217
+ code=EVENT_SCHEDULER_PAUSED,
1218
+ time=self.__getCurrentTime()
1219
+ ), ListeningEvent.SCHEDULER_PAUSED)
1220
+
1101
1221
  # Log that all user jobs have been paused
1102
1222
  self.__logger.info("All user jobs have been paused. System jobs remain active.")
1103
1223
 
@@ -1170,25 +1290,24 @@ class Scheduler(ISchedule):
1170
1290
  if self.isRunning():
1171
1291
 
1172
1292
  # Resume only jobs that were paused by pauseEverythingAt
1173
- if self.__paused_by_pause_everything:
1293
+ if self.__pausedByPauseEverything:
1174
1294
 
1175
1295
  # Iterate through the set of paused job IDs and resume each one
1176
- for job_id in list(self.__paused_by_pause_everything):
1296
+ for job_id in list(self.__pausedByPauseEverything):
1177
1297
 
1178
1298
  try:
1179
1299
 
1180
1300
  # Resume the job and log the action
1181
1301
  self.__scheduler.resume_job(job_id)
1182
- self.__logger.info(f"User job '{job_id}' has been resumed.")
1183
1302
 
1184
- # Execute the task callable listener
1185
- self.__globalCallableListener(SchedulerResumed(EVENT_SCHEDULER_RESUMED), ListeningEvent.SCHEDULER_RESUMED)
1303
+ # Invoke the listener for the resumed job
1304
+ self.__taskCallableListener(
1305
+ self.__getTaskFromSchedulerById(job_id),
1306
+ ListeningEvent.JOB_ON_RESUMED
1307
+ )
1186
1308
 
1187
- # Get the current time in the configured timezone
1188
- now = self.__getCurrentTime()
1189
-
1190
- # Log an informational message indicating that the scheduler has been paused
1191
- self.__logger.info(f"Orionis Scheduler paused successfully at {now}.")
1309
+ # Log an informational message indicating that the job has been resumed
1310
+ self.__logger.info(f"User job '{job_id}' has been resumed.")
1192
1311
 
1193
1312
  except Exception as e:
1194
1313
 
@@ -1196,7 +1315,19 @@ class Scheduler(ISchedule):
1196
1315
  self.__logger.warning(f"Failed to resume job '{job_id}': {str(e)}")
1197
1316
 
1198
1317
  # Clear the set after resuming all jobs
1199
- self.__paused_by_pause_everything.clear()
1318
+ self.__pausedByPauseEverything.clear()
1319
+
1320
+ # Execute the global callable listener after all jobs are resumed
1321
+ self.__globalCallableListener(SchedulerResumed(
1322
+ code=EVENT_SCHEDULER_RESUMED,
1323
+ time=self.__getCurrentTime()
1324
+ ), ListeningEvent.SCHEDULER_RESUMED)
1325
+
1326
+ # Get the current time in the configured timezone
1327
+ now = self.__getCurrentTime()
1328
+
1329
+ # Log an informational message indicating that the scheduler has been resumed
1330
+ self.__logger.info(f"Orionis Scheduler resumed successfully at {now}.")
1200
1331
 
1201
1332
  # Log that all previously paused jobs have been resumed
1202
1333
  self.__logger.info("All previously paused user jobs have been resumed.")
@@ -1272,18 +1403,26 @@ class Scheduler(ISchedule):
1272
1403
  if not isinstance(wait, bool):
1273
1404
  raise ValueError("The 'wait' parameter must be a boolean value.")
1274
1405
 
1275
- # Define a function to shut down the scheduler
1276
- def schedule_shutdown():
1277
-
1406
+ # Define an async function to shut down the scheduler
1407
+ async def schedule_shutdown():
1278
1408
  # Only shut down the scheduler if it is currently running
1279
1409
  if self.isRunning():
1410
+ try:
1280
1411
 
1281
- # Execute the shutdown process
1282
- self.__scheduler.shutdown(wait=wait)
1412
+ # Log the shutdown initiation
1413
+ self.__logger.info("Initiating scheduled shutdown...")
1283
1414
 
1284
- # Signal the stop event to break the wait in start()
1285
- if self._stop_event and not self._stop_event.is_set():
1286
- self._stop_event.set()
1415
+ # Call the async shutdown method
1416
+ await self.shutdown(wait=wait)
1417
+
1418
+ except Exception as e:
1419
+
1420
+ # Log any errors that occur during shutdown
1421
+ error_msg = f"Error during scheduled shutdown: {str(e)}"
1422
+ self.__logger.error(error_msg)
1423
+
1424
+ # Force stop if graceful shutdown fails
1425
+ self.forceStop()
1287
1426
 
1288
1427
  try:
1289
1428
 
@@ -1297,7 +1436,7 @@ class Scheduler(ISchedule):
1297
1436
 
1298
1437
  # Add a job to the scheduler to shut it down at the specified datetime
1299
1438
  self.__scheduler.add_job(
1300
- func=schedule_shutdown, # Function to shut down the scheduler
1439
+ func=self.wrapAsyncFunction(schedule_shutdown), # Function to shut down the scheduler
1301
1440
  trigger=DateTrigger(run_date=at), # Trigger type is 'date' for one-time execution
1302
1441
  id="scheduler_shutdown_at", # Unique job ID for shutting down the scheduler
1303
1442
  name="Shutdown Scheduler", # Descriptive name for the job
@@ -1588,7 +1727,7 @@ class Scheduler(ISchedule):
1588
1727
  # Return False if the job could not be removed (e.g., it does not exist)
1589
1728
  return False
1590
1729
 
1591
- def events(self) -> list:
1730
+ def events(self) -> List[Dict]:
1592
1731
  """
1593
1732
  Retrieve all scheduled jobs currently managed by the Scheduler.
1594
1733
 
@@ -1815,16 +1954,24 @@ class Scheduler(ISchedule):
1815
1954
  None
1816
1955
  This method does not return any value. It signals the scheduler to stop.
1817
1956
  """
1818
-
1819
1957
  # Check if the stop event exists and has not already been set
1820
1958
  if self._stop_event and not self._stop_event.is_set():
1821
1959
 
1822
- # Get the current asyncio event loop
1823
- loop = asyncio.get_event_loop()
1960
+ try:
1824
1961
 
1825
- # If the event loop is running, set the stop event in a thread-safe manner
1826
- if loop.is_running():
1962
+ # Try to get the current running event loop
1963
+ loop = asyncio.get_running_loop()
1964
+
1965
+ # If the event loop is running, set the stop event in a thread-safe manner
1827
1966
  loop.call_soon_threadsafe(self._stop_event.set)
1828
- else:
1829
- # Otherwise, set the stop event directly
1967
+
1968
+ except RuntimeError:
1969
+
1970
+ # No running event loop, set the stop event directly
1971
+ self._stop_event.set()
1972
+
1973
+ except Exception as e:
1974
+
1975
+ # Log the error but still try to set the event
1976
+ self.__logger.warning(f"Error setting stop event through event loop: {str(e)}")
1830
1977
  self._stop_event.set()