orionis 0.523.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.
@@ -1,7 +1,7 @@
1
1
  import asyncio
2
2
  from datetime import datetime
3
3
  import logging
4
- from typing import Dict, List, Optional, Union
4
+ from typing import Any, Awaitable, Callable, Dict, List, Optional, Union
5
5
  import pytz
6
6
  from apscheduler.triggers.date import DateTrigger
7
7
  from apscheduler.events import (
@@ -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 async 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))
523
607
 
524
- # Raise an error to inform the caller that the listener could not be invoked
525
- raise CLIOrionisRuntimeError(error_msg)
608
+ except Exception as e:
609
+
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
544
630
 
545
- # Raise a runtime error if listener invocation fails
546
- raise CLIOrionisRuntimeError(error_msg)
631
+ else:
632
+
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,82 +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
- def __pausedListener(
604
- self,
605
- event: SchedulerPaused
606
- ) -> None:
607
- """
608
- Handle the scheduler paused event for logging and invoking registered listeners.
609
-
610
- This method is triggered when the scheduler is paused. It logs an informational
611
- message indicating that the scheduler has been paused successfully and displays
612
- a formatted message on the rich console. If a listener is registered for the
613
- scheduler paused event, it invokes the listener with the event details.
614
-
615
- Parameters
616
- ----------
617
- event : SchedulerPaused
618
- An event object containing details about the scheduler paused event.
619
-
620
- Returns
621
- -------
622
- None
623
- This method does not return any value. It performs logging, displays
624
- a message on the console, and invokes any registered listener for the
625
- scheduler paused event.
626
- """
627
-
628
- # Get the current time in the configured timezone
629
- now = self.__getCurrentTime()
630
-
631
- # Check if a listener is registered for the scheduler paused event
632
- self.__globalCallableListener(event, ListeningEvent.SCHEDULER_PAUSED)
633
-
634
- # Log an informational message indicating that the scheduler has been paused
635
- self.__logger.info(f"Orionis Scheduler paused successfully at {now}.")
636
-
637
- def __resumedListener(
638
- self,
639
- event: SchedulerResumed
640
- ) -> None:
641
- """
642
- Handle the scheduler resumed event for logging and invoking registered listeners.
643
-
644
- This method is triggered when the scheduler resumes from a paused state. It logs an informational
645
- message indicating that the scheduler has resumed successfully and displays a formatted message
646
- on the rich console. If a listener is registered for the scheduler resumed event, it invokes
647
- the listener with the event details.
648
-
649
- Parameters
650
- ----------
651
- event : SchedulerResumed
652
- An event object containing details about the scheduler resumed event.
653
-
654
- Returns
655
- -------
656
- None
657
- This method does not return any value. It performs logging, displays
658
- a message on the console, and invokes any registered listener for the
659
- scheduler resumed event.
660
- """
661
-
662
- # Get the current time in the configured timezone
663
- now = self.__getCurrentTime()
664
-
665
- # Check if a listener is registered for the scheduler resumed event
666
- self.__globalCallableListener(event, ListeningEvent.SCHEDULER_RESUMED)
667
-
668
- # Log an informational message indicating that the scheduler has resumed
669
- self.__logger.info(f"Orionis Scheduler resumed successfully at {now}.")
670
-
671
703
  def __shutdownListener(
672
704
  self,
673
- event: SchedulerShutdown
705
+ event
674
706
  ) -> None:
675
707
  """
676
708
  Handle the scheduler shutdown event for logging and invoking registered listeners.
@@ -697,14 +729,19 @@ class Scheduler(ISchedule):
697
729
  now = self.__getCurrentTime()
698
730
 
699
731
  # Check if a listener is registered for the scheduler shutdown event
700
- 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)
701
738
 
702
739
  # Log an informational message indicating that the scheduler has shut down
703
740
  self.__logger.info(f"Orionis Scheduler shut down successfully at {now}.")
704
741
 
705
742
  def __errorListener(
706
743
  self,
707
- event: JobError
744
+ event
708
745
  ) -> None:
709
746
  """
710
747
  Handle job error events for logging and error reporting.
@@ -731,14 +768,31 @@ class Scheduler(ISchedule):
731
768
  self.__logger.error(f"Task {event.job_id} raised an exception: {event.exception}")
732
769
 
733
770
  # If a listener is registered for this job ID, invoke the listener with the event details
734
- 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
+ )
735
781
 
736
- # Check if a listener is registered for the scheduler started event
737
- 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
+ )
738
792
 
739
793
  def __submittedListener(
740
794
  self,
741
- event: JobSubmitted
795
+ event
742
796
  ) -> None:
743
797
  """
744
798
  Handle job submission events for logging and error reporting.
@@ -764,12 +818,15 @@ class Scheduler(ISchedule):
764
818
  # Log an informational message indicating that the job has been submitted
765
819
  self.__logger.info(f"Task {event.job_id} submitted to executor.")
766
820
 
821
+ # Create entity for job submitted event
822
+ data_event = self.__getTaskFromSchedulerById(event.job_id, event.code)
823
+
767
824
  # If a listener is registered for this job ID, invoke the listener with the event details
768
- self.__taskCallableListener(event, ListeningEvent.JOB_BEFORE)
825
+ self.__taskCallableListener(data_event, ListeningEvent.JOB_BEFORE)
769
826
 
770
827
  def __executedListener(
771
828
  self,
772
- event: JobExecuted
829
+ event
773
830
  ) -> None:
774
831
  """
775
832
  Handle job execution events for logging and error reporting.
@@ -796,12 +853,15 @@ class Scheduler(ISchedule):
796
853
  # Log an informational message indicating that the job has been executed
797
854
  self.__logger.info(f"Task {event.job_id} executed.")
798
855
 
856
+ # Create entity for job executed event
857
+ data_event = self.__getTaskFromSchedulerById(event.job_id, event.code)
858
+
799
859
  # If a listener is registered for this job ID, invoke the listener with the event details
800
- self.__taskCallableListener(event, ListeningEvent.JOB_AFTER)
860
+ self.__taskCallableListener(data_event, ListeningEvent.JOB_AFTER)
801
861
 
802
862
  def __missedListener(
803
863
  self,
804
- event: JobMissed
864
+ event
805
865
  ) -> None:
806
866
  """
807
867
  Handle job missed events for debugging and error reporting.
@@ -828,12 +888,15 @@ class Scheduler(ISchedule):
828
888
  # Log a warning indicating that the job was missed
829
889
  self.__logger.warning(f"Task {event.job_id} was missed. It was scheduled to run at {event.scheduled_run_time}.")
830
890
 
891
+ # Create entity for job missed event
892
+ data_event = self.__getTaskFromSchedulerById(event.job_id, event.code)
893
+
831
894
  # If a listener is registered for this job ID, invoke the listener with the event details
832
- self.__taskCallableListener(event, ListeningEvent.JOB_ON_MISSED)
895
+ self.__taskCallableListener(data_event, ListeningEvent.JOB_ON_MISSED)
833
896
 
834
897
  def __maxInstancesListener(
835
898
  self,
836
- event: JobMaxInstances
899
+ event
837
900
  ) -> None:
838
901
  """
839
902
  Handle job max instances events for logging and error reporting.
@@ -860,46 +923,15 @@ class Scheduler(ISchedule):
860
923
  # Log an error message indicating that the job exceeded maximum instances
861
924
  self.__logger.error(f"Task {event.job_id} exceeded maximum instances")
862
925
 
863
- # If a listener is registered for this job ID, invoke the listener with the event details
864
- self.__taskCallableListener(event, ListeningEvent.JOB_ON_MAXINSTANCES)
865
-
866
- def __modifiedListener(
867
- self,
868
- event: JobModified
869
- ) -> None:
870
- """
871
- Handle job modified events for logging and error reporting.
872
-
873
- This method is triggered when a job is modified. It logs an informational
874
- message indicating that the job has been modified successfully. If the application
875
- is in debug mode, it also displays a message on the console. Additionally, if a
876
- listener is registered for the modified job, it invokes the listener with the
877
- event details.
878
-
879
- Parameters
880
- ----------
881
- event : JobModified
882
- An instance of the JobModified event containing details about the modified job,
883
- including its ID and other relevant information.
884
-
885
- Returns
886
- -------
887
- None
888
- This method does not return any value. It performs logging, error reporting,
889
- and listener invocation for the job modified event.
890
- """
926
+ # Create entity for job max instances event
927
+ data_event = self.__getTaskFromSchedulerById(event.job_id, event.code)
891
928
 
892
929
  # If a listener is registered for this job ID, invoke the listener with the event details
893
- if event.next_run_time is None:
894
- self.__logger.info(f"Task {event.job_id} has been paused.")
895
- self.__taskCallableListener(event, ListeningEvent.JOB_ON_PAUSED)
896
- else:
897
- self.__logger.info(f"Task {event.job_id} has been resumed.")
898
- self.__taskCallableListener(event, ListeningEvent.JOB_ON_RESUMED)
930
+ self.__taskCallableListener(data_event, ListeningEvent.JOB_ON_MAXINSTANCES)
899
931
 
900
932
  def __removedListener(
901
933
  self,
902
- event: JobRemoved
934
+ event
903
935
  ) -> None:
904
936
  """
905
937
  Handle job removal events for logging and invoking registered listeners.
@@ -925,8 +957,11 @@ class Scheduler(ISchedule):
925
957
  # Log the removal of the job
926
958
  self.__logger.info(f"Task {event.job_id} has been removed.")
927
959
 
960
+ # Create entity for job removed event
961
+ data_event = self.__getTaskFromSchedulerById(event.job_id, event.code)
962
+
928
963
  # If a listener is registered for this job ID, invoke the listener with the event details
929
- self.__taskCallableListener(event, ListeningEvent.JOB_ON_REMOVED)
964
+ self.__taskCallableListener(data_event, ListeningEvent.JOB_ON_REMOVED)
930
965
 
931
966
  def __loadEvents(
932
967
  self
@@ -1038,6 +1073,62 @@ class Scheduler(ISchedule):
1038
1073
  # Register the listener for the specified event in the internal listeners dictionary
1039
1074
  self.__listeners[event] = listener
1040
1075
 
1076
+ def wrapAsyncFunction(
1077
+ self,
1078
+ func: Callable[..., Awaitable[Any]]
1079
+ ) -> Callable[..., Any]:
1080
+ """
1081
+ Wrap an asynchronous function to be called in a synchronous context.
1082
+
1083
+ This method takes an asynchronous function (a coroutine) and returns a synchronous
1084
+ wrapper function that can be called in a non-async context. The wrapper function
1085
+ ensures that the asynchronous function is executed within the appropriate event loop.
1086
+
1087
+ Parameters
1088
+ ----------
1089
+ func : Callable[..., Awaitable[Any]]
1090
+ The asynchronous function (coroutine) to be wrapped. This function should be
1091
+ defined using the `async def` syntax and return an awaitable object.
1092
+
1093
+ Returns
1094
+ -------
1095
+ Callable[..., Any]
1096
+ A synchronous wrapper function that can be called in a non-async context.
1097
+ When invoked, this wrapper will execute the original asynchronous function
1098
+ within the appropriate event loop.
1099
+
1100
+ Raises
1101
+ ------
1102
+ CLIOrionisRuntimeError
1103
+ If the provided `func` is not an asynchronous function or execution fails.
1104
+ """
1105
+
1106
+ def sync_wrapper(*args, **kwargs) -> Any:
1107
+
1108
+ try:
1109
+
1110
+ # Try to get the current event loop
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
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
1130
+ return sync_wrapper
1131
+
1041
1132
  def pauseEverythingAt(
1042
1133
  self,
1043
1134
  at: datetime
@@ -1073,14 +1164,14 @@ class Scheduler(ISchedule):
1073
1164
  if not isinstance(at, datetime):
1074
1165
  raise ValueError("The 'at' parameter must be a datetime object.")
1075
1166
 
1076
- # Define a function to pause the scheduler
1077
- def schedule_pause():
1167
+ # Define an async function to pause the scheduler
1168
+ async def schedule_pause():
1078
1169
 
1079
1170
  # Only pause jobs if the scheduler is currently running
1080
1171
  if self.isRunning():
1081
1172
 
1082
1173
  # Clear the set of previously paused jobs
1083
- self.__paused_by_pause_everything.clear()
1174
+ self.__pausedByPauseEverything.clear()
1084
1175
 
1085
1176
  # Get all jobs from the scheduler
1086
1177
  all_jobs = self.__scheduler.get_jobs()
@@ -1095,29 +1186,38 @@ class Scheduler(ISchedule):
1095
1186
  # Pause only user jobs, not system jobs
1096
1187
  for job in all_jobs:
1097
1188
 
1098
- # Pause the job only if it is not a system job
1189
+ # Check if the job is not a system job
1099
1190
  if job.id not in system_job_ids:
1100
1191
 
1101
- # Pause the job and add its ID to the set of paused jobs
1102
1192
  try:
1103
1193
 
1104
1194
  # Pause the job in the scheduler
1105
1195
  self.__scheduler.pause_job(job.id)
1106
- self.__paused_by_pause_everything.add(job.id)
1196
+ self.__pausedByPauseEverything.add(job.id)
1197
+
1198
+ # Get the current time in the configured timezone
1199
+ now = self.__getCurrentTime()
1107
1200
 
1108
- # Excute the paused listener
1109
- self.__pausedListener(JobPause(
1110
- code=EVENT_SCHEDULER_PAUSED,
1111
- job_id=job.id if hasattr(job, 'id') else None,
1112
- alias=job.name if hasattr(job, 'name') else None,
1113
- jobstore=job.jobstore if hasattr(job, 'jobstore') else None,
1114
- ))
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}.")
1115
1209
 
1116
1210
  except Exception as e:
1117
1211
 
1118
1212
  # Log a warning if pausing a job fails, but continue with others
1119
1213
  self.__logger.warning(f"Failed to pause job '{job.id}': {str(e)}")
1120
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
+
1121
1221
  # Log that all user jobs have been paused
1122
1222
  self.__logger.info("All user jobs have been paused. System jobs remain active.")
1123
1223
 
@@ -1133,7 +1233,7 @@ class Scheduler(ISchedule):
1133
1233
 
1134
1234
  # Add a job to the scheduler to pause it at the specified datetime
1135
1235
  self.__scheduler.add_job(
1136
- func=schedule_pause, # Function to pause the scheduler
1236
+ func=self.wrapAsyncFunction(schedule_pause), # Function to pause the scheduler
1137
1237
  trigger=DateTrigger(run_date=at), # Trigger type is 'date' for one-time execution
1138
1238
  id="scheduler_pause_at", # Unique job ID for pausing the scheduler
1139
1239
  name="Pause Scheduler", # Descriptive name for the job
@@ -1183,29 +1283,31 @@ class Scheduler(ISchedule):
1183
1283
  if not isinstance(at, datetime):
1184
1284
  raise ValueError("The 'at' parameter must be a datetime object.")
1185
1285
 
1186
- # Define a function to resume the scheduler
1187
- def schedule_resume():
1286
+ # Define an async function to resume the scheduler
1287
+ async def schedule_resume():
1188
1288
 
1189
1289
  # Only resume jobs if the scheduler is currently running
1190
1290
  if self.isRunning():
1191
1291
 
1192
1292
  # Resume only jobs that were paused by pauseEverythingAt
1193
- if self.__paused_by_pause_everything:
1293
+ if self.__pausedByPauseEverything:
1194
1294
 
1195
1295
  # Iterate through the set of paused job IDs and resume each one
1196
- for job_id in list(self.__paused_by_pause_everything):
1296
+ for job_id in list(self.__pausedByPauseEverything):
1197
1297
 
1198
1298
  try:
1199
1299
 
1200
1300
  # Resume the job and log the action
1201
1301
  self.__scheduler.resume_job(job_id)
1202
- self.__logger.info(f"User job '{job_id}' has been resumed.")
1203
1302
 
1204
- # Excute the resumed listener
1205
- self.__resumedListener(JobResume(
1206
- code=EVENT_SCHEDULER_RESUMED,
1207
- job_id=job_id
1208
- ))
1303
+ # Invoke the listener for the resumed job
1304
+ self.__taskCallableListener(
1305
+ self.__getTaskFromSchedulerById(job_id),
1306
+ ListeningEvent.JOB_ON_RESUMED
1307
+ )
1308
+
1309
+ # Log an informational message indicating that the job has been resumed
1310
+ self.__logger.info(f"User job '{job_id}' has been resumed.")
1209
1311
 
1210
1312
  except Exception as e:
1211
1313
 
@@ -1213,7 +1315,19 @@ class Scheduler(ISchedule):
1213
1315
  self.__logger.warning(f"Failed to resume job '{job_id}': {str(e)}")
1214
1316
 
1215
1317
  # Clear the set after resuming all jobs
1216
- 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}.")
1217
1331
 
1218
1332
  # Log that all previously paused jobs have been resumed
1219
1333
  self.__logger.info("All previously paused user jobs have been resumed.")
@@ -1230,7 +1344,7 @@ class Scheduler(ISchedule):
1230
1344
 
1231
1345
  # Add a job to the scheduler to resume it at the specified datetime
1232
1346
  self.__scheduler.add_job(
1233
- func=schedule_resume, # Function to resume the scheduler
1347
+ func=self.wrapAsyncFunction(schedule_resume), # Function to resume the scheduler
1234
1348
  trigger=DateTrigger(run_date=at), # Trigger type is 'date' for one-time execution
1235
1349
  id="scheduler_resume_at", # Unique job ID for resuming the scheduler
1236
1350
  name="Resume Scheduler", # Descriptive name for the job
@@ -1289,18 +1403,26 @@ class Scheduler(ISchedule):
1289
1403
  if not isinstance(wait, bool):
1290
1404
  raise ValueError("The 'wait' parameter must be a boolean value.")
1291
1405
 
1292
- # Define a function to shut down the scheduler
1293
- def schedule_shutdown():
1294
-
1406
+ # Define an async function to shut down the scheduler
1407
+ async def schedule_shutdown():
1295
1408
  # Only shut down the scheduler if it is currently running
1296
1409
  if self.isRunning():
1410
+ try:
1297
1411
 
1298
- # Execute the shutdown process
1299
- self.__scheduler.shutdown(wait=wait)
1412
+ # Log the shutdown initiation
1413
+ self.__logger.info("Initiating scheduled shutdown...")
1414
+
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)
1300
1423
 
1301
- # Signal the stop event to break the wait in start()
1302
- if self._stop_event and not self._stop_event.is_set():
1303
- self._stop_event.set()
1424
+ # Force stop if graceful shutdown fails
1425
+ self.forceStop()
1304
1426
 
1305
1427
  try:
1306
1428
 
@@ -1314,7 +1436,7 @@ class Scheduler(ISchedule):
1314
1436
 
1315
1437
  # Add a job to the scheduler to shut it down at the specified datetime
1316
1438
  self.__scheduler.add_job(
1317
- func=schedule_shutdown, # Function to shut down the scheduler
1439
+ func=self.wrapAsyncFunction(schedule_shutdown), # Function to shut down the scheduler
1318
1440
  trigger=DateTrigger(run_date=at), # Trigger type is 'date' for one-time execution
1319
1441
  id="scheduler_shutdown_at", # Unique job ID for shutting down the scheduler
1320
1442
  name="Shutdown Scheduler", # Descriptive name for the job
@@ -1605,7 +1727,7 @@ class Scheduler(ISchedule):
1605
1727
  # Return False if the job could not be removed (e.g., it does not exist)
1606
1728
  return False
1607
1729
 
1608
- def events(self) -> list:
1730
+ def events(self) -> List[Dict]:
1609
1731
  """
1610
1732
  Retrieve all scheduled jobs currently managed by the Scheduler.
1611
1733
 
@@ -1832,16 +1954,24 @@ class Scheduler(ISchedule):
1832
1954
  None
1833
1955
  This method does not return any value. It signals the scheduler to stop.
1834
1956
  """
1835
-
1836
1957
  # Check if the stop event exists and has not already been set
1837
1958
  if self._stop_event and not self._stop_event.is_set():
1838
1959
 
1839
- # Get the current asyncio event loop
1840
- loop = asyncio.get_event_loop()
1960
+ try:
1961
+
1962
+ # Try to get the current running event loop
1963
+ loop = asyncio.get_running_loop()
1841
1964
 
1842
- # If the event loop is running, set the stop event in a thread-safe manner
1843
- if loop.is_running():
1965
+ # If the event loop is running, set the stop event in a thread-safe manner
1844
1966
  loop.call_soon_threadsafe(self._stop_event.set)
1845
- else:
1846
- # 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)}")
1847
1977
  self._stop_event.set()