orionis 0.513.0__py3-none-any.whl → 0.514.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,18 +1,20 @@
1
1
  import asyncio
2
2
  from datetime import datetime
3
3
  import logging
4
- from typing import Dict, List, Optional
4
+ from typing import Dict, List, Optional, Union
5
5
  import pytz
6
6
  from apscheduler.events import (
7
- EVENT_JOB_ERROR,
8
- EVENT_JOB_EXECUTED,
9
- EVENT_JOB_MISSED,
10
- EVENT_JOB_SUBMITTED,
7
+ EVENT_SCHEDULER_STARTED,
11
8
  EVENT_SCHEDULER_PAUSED,
12
9
  EVENT_SCHEDULER_RESUMED,
13
10
  EVENT_SCHEDULER_SHUTDOWN,
14
- EVENT_SCHEDULER_STARTED,
15
- EVENT_JOB_MAX_INSTANCES
11
+ EVENT_JOB_ERROR,
12
+ EVENT_JOB_SUBMITTED,
13
+ EVENT_JOB_EXECUTED,
14
+ EVENT_JOB_MISSED,
15
+ EVENT_JOB_MAX_INSTANCES,
16
+ EVENT_JOB_MODIFIED,
17
+ EVENT_JOB_REMOVED
16
18
  )
17
19
  from apscheduler.schedulers.asyncio import AsyncIOScheduler as APSAsyncIOScheduler
18
20
  from rich.console import Console
@@ -20,17 +22,22 @@ from rich.panel import Panel
20
22
  from rich.text import Text
21
23
  from orionis.console.contracts.reactor import IReactor
22
24
  from orionis.console.contracts.schedule import ISchedule
25
+ from orionis.console.contracts.schedule_event_listener import IScheduleEventListener
23
26
  from orionis.console.entities.job_error import JobError
24
27
  from orionis.console.entities.job_executed import JobExecuted
25
28
  from orionis.console.entities.job_max_instances import JobMaxInstances
26
29
  from orionis.console.entities.job_missed import JobMissed
30
+ from orionis.console.entities.job_modified import JobModified
31
+ from orionis.console.entities.job_removed import JobRemoved
27
32
  from orionis.console.entities.job_submitted import JobSubmitted
28
33
  from orionis.console.entities.scheduler_paused import SchedulerPaused
29
34
  from orionis.console.entities.scheduler_resumed import SchedulerResumed
30
35
  from orionis.console.entities.scheduler_shutdown import SchedulerShutdown
31
36
  from orionis.console.entities.scheduler_started import SchedulerStarted
32
37
  from orionis.console.enums.listener import ListeningEvent
38
+ from orionis.console.enums.event import Event as EventEntity
33
39
  from orionis.console.exceptions import CLIOrionisRuntimeError
40
+ from orionis.console.exceptions.cli_orionis_value_error import CLIOrionisValueError
34
41
  from orionis.console.output.contracts.console import IConsole
35
42
  from orionis.console.tasks.event import Event
36
43
  from orionis.foundation.contracts.application import IApplication
@@ -100,7 +107,7 @@ class Scheduler(ISchedule):
100
107
  self.__events: Dict[str, Event] = {}
101
108
 
102
109
  # Initialize the jobs list to keep track of all scheduled jobs.
103
- self.__jobs: List[dict] = []
110
+ self.__jobs: List[EventEntity] = []
104
111
 
105
112
  # Initialize the listeners dictionary to manage event listeners.
106
113
  self.__listeners: Dict[str, callable] = {}
@@ -121,10 +128,165 @@ class Scheduler(ISchedule):
121
128
  formatted as "YYYY-MM-DD HH:MM:SS".
122
129
  """
123
130
 
131
+ # Get the current time in the configured timezone
124
132
  tz = pytz.timezone(self.__app.config("app.timezone", "UTC"))
125
133
  now = datetime.now(tz)
134
+
135
+ # Format the current time as a string
126
136
  return now.strftime("%Y-%m-%d %H:%M:%S")
127
137
 
138
+ def __getCommands(
139
+ self
140
+ ) -> dict:
141
+ """
142
+ Retrieve available commands from the reactor and return them as a dictionary.
143
+
144
+ This method queries the reactor for all available jobs/commands, extracting their
145
+ signatures and descriptions. The result is a dictionary where each key is the command
146
+ signature and the value is another dictionary containing the command's signature and
147
+ its description.
148
+
149
+ Returns
150
+ -------
151
+ dict
152
+ A dictionary mapping command signatures to their details. Each value is a dictionary
153
+ with 'signature' and 'description' keys.
154
+ """
155
+
156
+ # Initialize the commands dictionary
157
+ commands = {}
158
+
159
+ # Iterate over all jobs provided by the reactor's info method
160
+ for job in self.__reactor.info():
161
+
162
+ # Store each job's signature and description in the commands dictionary
163
+ commands[job['signature']] = {
164
+ 'signature': job['signature'],
165
+ 'description': job.get('description', '')
166
+ }
167
+
168
+ # Return the commands dictionary
169
+ return commands
170
+
171
+ def __isAvailable(
172
+ self,
173
+ signature: str
174
+ ) -> bool:
175
+ """
176
+ Check if a command with the given signature is available.
177
+
178
+ This method iterates through the available commands and determines
179
+ whether the provided signature matches any registered command.
180
+
181
+ Parameters
182
+ ----------
183
+ signature : str
184
+ The signature of the command to check for availability.
185
+
186
+ Returns
187
+ -------
188
+ bool
189
+ True if the command with the specified signature exists and is available,
190
+ False otherwise.
191
+ """
192
+
193
+ # Iterate through all available command signatures
194
+ for command in self.__available_commands.keys():
195
+
196
+ # Return True if the signature matches an available command
197
+ if command == signature:
198
+ return True
199
+
200
+ # Return False if the signature is not found among available commands
201
+ return False
202
+
203
+ def __getDescription(
204
+ self,
205
+ signature: str
206
+ ) -> Optional[str]:
207
+ """
208
+ Retrieve the description of a command given its signature.
209
+
210
+ This method looks up the available commands dictionary and returns the description
211
+ associated with the provided command signature. If the signature does not exist,
212
+ it returns None.
213
+
214
+ Parameters
215
+ ----------
216
+ signature : str
217
+ The unique signature identifying the command.
218
+
219
+ Returns
220
+ -------
221
+ Optional[str]
222
+ The description of the command if found; otherwise, None.
223
+ """
224
+
225
+ # Attempt to retrieve the command entry from the available commands dictionary
226
+ command_entry = self.__available_commands.get(signature)
227
+
228
+ # Return the description if the command exists, otherwise return None
229
+ return command_entry['description'] if command_entry else None
230
+
231
+ def command(
232
+ self,
233
+ signature: str,
234
+ args: Optional[List[str]] = None
235
+ ) -> 'Event':
236
+ """
237
+ Prepare an Event instance for a given command signature and its arguments.
238
+
239
+ This method validates the provided command signature and arguments, ensuring
240
+ that the command exists among the registered commands and that the arguments
241
+ are in the correct format. If validation passes, it creates and returns an
242
+ Event object representing the scheduled command, including its signature,
243
+ arguments, and description.
244
+
245
+ Parameters
246
+ ----------
247
+ signature : str
248
+ The unique signature identifying the command to be scheduled. Must be a non-empty string.
249
+ args : Optional[List[str]], optional
250
+ A list of string arguments to be passed to the command. Defaults to None.
251
+
252
+ Returns
253
+ -------
254
+ Event
255
+ An Event instance containing the command signature, arguments, and its description.
256
+
257
+ Raises
258
+ ------
259
+ ValueError
260
+ If the command signature is not a non-empty string, if the arguments are not a list
261
+ of strings or None, or if the command does not exist among the registered commands.
262
+ """
263
+
264
+ # Prevent adding new commands while the scheduler is running
265
+ if self.__scheduler.running:
266
+ raise CLIOrionisRuntimeError("Cannot add new commands while the scheduler is running.")
267
+
268
+ # Validate that the command signature is a non-empty string
269
+ if not isinstance(signature, str) or not signature.strip():
270
+ raise CLIOrionisValueError("Command signature must be a non-empty string.")
271
+
272
+ # Ensure that arguments are either a list of strings or None
273
+ if args is not None and not isinstance(args, list):
274
+ raise CLIOrionisValueError("Arguments must be a list of strings or None.")
275
+
276
+ # Check if the command is available in the registered commands
277
+ if not self.__isAvailable(signature):
278
+ raise CLIOrionisValueError(f"The command '{signature}' is not available or does not exist.")
279
+
280
+ # Store the command and its arguments for scheduling
281
+ self.__events[signature] = Event(
282
+ signature=signature,
283
+ args=args or [],
284
+ purpose=self.__getDescription(signature)
285
+ )
286
+
287
+ # Return the Event instance for further scheduling configuration
288
+ return self.__events[signature]
289
+
128
290
  def __suscribeListeners(
129
291
  self
130
292
  ) -> None:
@@ -146,32 +308,131 @@ class Scheduler(ISchedule):
146
308
  This method does not return any value. It configures event listeners on the scheduler.
147
309
  """
148
310
 
149
- # Add a listener for the scheduler started event
150
311
  self.__scheduler.add_listener(self.__startedListener, EVENT_SCHEDULER_STARTED)
151
-
152
- # Add a listener for the scheduler shutdown event
312
+ self.__scheduler.add_listener(self.__pausedListener, EVENT_SCHEDULER_PAUSED)
313
+ self.__scheduler.add_listener(self.__resumedListener, EVENT_SCHEDULER_RESUMED)
153
314
  self.__scheduler.add_listener(self.__shutdownListener, EVENT_SCHEDULER_SHUTDOWN)
315
+ self.__scheduler.add_listener(self.__errorListener, EVENT_JOB_ERROR)
316
+ self.__scheduler.add_listener(self.__submittedListener, EVENT_JOB_SUBMITTED)
317
+ self.__scheduler.add_listener(self.__executedListener, EVENT_JOB_EXECUTED)
318
+ self.__scheduler.add_listener(self.__missedListener, EVENT_JOB_MISSED)
319
+ self.__scheduler.add_listener(self.__maxInstancesListener, EVENT_JOB_MAX_INSTANCES)
320
+ self.__scheduler.add_listener(self.__modifiedListener, EVENT_JOB_MODIFIED)
321
+ self.__scheduler.add_listener(self.__removedListener, EVENT_JOB_REMOVED)
154
322
 
155
- # Add a listener for the scheduler paused event
156
- self.__scheduler.add_listener(self.__pausedListener, EVENT_SCHEDULER_PAUSED)
323
+ def __globalCallableListener(
324
+ self,
325
+ event_data: Optional[Union[SchedulerStarted, SchedulerPaused, SchedulerResumed, SchedulerShutdown, JobError]],
326
+ listening_vent: ListeningEvent
327
+ ) -> None:
328
+ """
329
+ Invoke registered listeners for global scheduler events.
157
330
 
158
- # Add a listener for the scheduler resumed event
159
- self.__scheduler.add_listener(self.__resumedListener, EVENT_SCHEDULER_RESUMED)
331
+ This method handles global scheduler events such as when the scheduler starts, pauses, resumes,
332
+ or shuts down. It checks if a listener is registered for the specified event and invokes it if callable.
333
+ The listener can be either a coroutine or a regular function.
160
334
 
161
- # Add a listener for job submission events
162
- self.__scheduler.add_listener(self.__submittedListener, EVENT_JOB_SUBMITTED)
335
+ Parameters
336
+ ----------
337
+ event_data : Optional[Union[SchedulerStarted, SchedulerPaused, SchedulerResumed, SchedulerShutdown, JobError]]
338
+ The event data associated with the global scheduler event. This can include details about the event,
339
+ such as its type and context. If no specific data is available, this parameter can be None.
340
+ listening_vent : ListeningEvent
341
+ An instance of the ListeningEvent enum representing the global scheduler event to handle.
163
342
 
164
- # Add a listener for job execution events
165
- self.__scheduler.add_listener(self.__executedListener, EVENT_JOB_EXECUTED)
343
+ Returns
344
+ -------
345
+ None
346
+ This method does not return any value. It invokes the registered listener for the specified event,
347
+ if one exists.
166
348
 
167
- # Add a listener for missed job events
168
- self.__scheduler.add_listener(self.__missedListener, EVENT_JOB_MISSED)
349
+ Raises
350
+ ------
351
+ CLIOrionisValueError
352
+ If the provided `listening_vent` is not an instance of ListeningEvent.
353
+ """
169
354
 
170
- # Add a listener for job error events
171
- self.__scheduler.add_listener(self.__errorListener, EVENT_JOB_ERROR)
355
+ # Validate that the provided event is an instance of ListeningEvent
356
+ if not isinstance(listening_vent, ListeningEvent):
357
+ raise CLIOrionisValueError("The event must be an instance of ListeningEvent.")
172
358
 
173
- # Add a listener for job max instances events
174
- self.__scheduler.add_listener(self.__maxInstancesListener, EVENT_JOB_MAX_INSTANCES)
359
+ # Retrieve the global identifier for the event from the ListeningEvent enum
360
+ scheduler_event = listening_vent.value
361
+
362
+ # Check if a listener is registered for the specified event
363
+ if scheduler_event in self.__listeners:
364
+ listener = self.__listeners[scheduler_event]
365
+
366
+ # Ensure the listener is callable before invoking it
367
+ if callable(listener):
368
+ # If the listener is a coroutine, schedule it as an asyncio task
369
+ if asyncio.iscoroutinefunction(listener):
370
+ asyncio.create_task(listener(event_data, self))
371
+ # Otherwise, invoke the listener directly as a regular function
372
+ else:
373
+ listener(event_data, self)
374
+
375
+ def __taskCallableListener(
376
+ self,
377
+ event_data: Optional[Union[JobError, JobExecuted, JobSubmitted, JobMissed, JobMaxInstances]],
378
+ listening_vent: ListeningEvent
379
+ ) -> None:
380
+ """
381
+ Invoke registered listeners for specific task/job events.
382
+
383
+ This method handles task/job-specific events such as job errors, executions, submissions,
384
+ missed jobs, and max instance violations. It checks if a listener is registered for the
385
+ specific job ID associated with the event and invokes the appropriate method on the listener
386
+ if callable. The listener can be either a coroutine or a regular function.
387
+
388
+ Parameters
389
+ ----------
390
+ event_data : Optional[Union[JobError, JobExecuted, JobSubmitted, JobMissed, JobMaxInstances]]
391
+ The event data associated with the task/job event. This includes details about the job,
392
+ such as its ID, exception (if any), and other context. If no specific data is available,
393
+ this parameter can be None.
394
+ listening_vent : ListeningEvent
395
+ An instance of the ListeningEvent enum representing the task/job event to handle.
396
+
397
+ Returns
398
+ -------
399
+ None
400
+ This method does not return any value. It invokes the registered listener for the
401
+ specified job event, if one exists.
402
+
403
+ Raises
404
+ ------
405
+ CLIOrionisValueError
406
+ If the provided `listening_vent` is not an instance of ListeningEvent.
407
+ """
408
+
409
+ # Validate that the provided event is an instance of ListeningEvent
410
+ if not isinstance(listening_vent, ListeningEvent):
411
+ raise CLIOrionisValueError("The event must be an instance of ListeningEvent.")
412
+
413
+ # Retrieve the global identifier for the event from the ListeningEvent enum
414
+ scheduler_event = listening_vent.value
415
+
416
+ # Check if a listener is registered for the specific job ID in the event data
417
+ if event_data.job_id in self.__listeners:
418
+
419
+ # Retrieve the listener for the specific job ID
420
+ listener = self.__listeners[event_data.job_id]
421
+
422
+ # Check if the listener is an instance of IScheduleEventListener
423
+ if isinstance(listener, IScheduleEventListener):
424
+
425
+ # Check if the listener has a method corresponding to the event type
426
+ if hasattr(listener, scheduler_event) and callable(getattr(listener, scheduler_event)):
427
+ listener_method = getattr(listener, scheduler_event)
428
+
429
+ # Invoke the listener method, handling both coroutine and regular functions
430
+ if asyncio.iscoroutinefunction(listener_method):
431
+ # Schedule the coroutine listener method as an asyncio task
432
+ asyncio.create_task(listener_method(event_data, self))
433
+ else:
434
+ # Call the regular listener method directly
435
+ listener_method(event_data, self)
175
436
 
176
437
  def __startedListener(
177
438
  self,
@@ -225,68 +486,8 @@ class Scheduler(ISchedule):
225
486
  # Add another blank line for better formatting
226
487
  self.__rich_console.line()
227
488
 
228
- # Retrieve the global identifier for the scheduler started event
229
- scheduler_started = ListeningEvent.SCHEDULER_STARTED.value
230
-
231
489
  # Check if a listener is registered for the scheduler started event
232
- if scheduler_started in self.__listeners:
233
- listener = self.__listeners[scheduler_started]
234
-
235
- # Ensure the listener is callable before invoking it
236
- if callable(listener):
237
-
238
- # Invoke the registered listener with the event details
239
- listener(event)
240
-
241
- def __shutdownListener(
242
- self,
243
- event: SchedulerShutdown
244
- ) -> None:
245
- """
246
- Handle the scheduler shutdown event for logging and invoking registered listeners.
247
-
248
- This method is triggered when the scheduler shuts down. It logs an informational
249
- message indicating that the scheduler has shut down successfully and displays
250
- a formatted message on the rich console. If a listener is registered for the
251
- scheduler shutdown event, it invokes the listener with the event details.
252
-
253
- Parameters
254
- ----------
255
- event : SchedulerShutdown
256
- An event object containing details about the scheduler shutdown event.
257
-
258
- Returns
259
- -------
260
- None
261
- This method does not return any value. It performs logging, displays
262
- a message on the console, and invokes any registered listener for the
263
- scheduler shutdown event.
264
- """
265
-
266
- # Get the current time in the configured timezone
267
- now = self.__getCurrentTime()
268
-
269
- # Create a shutdown message
270
- message = f"Orionis Scheduler shut down successfully at {now}."
271
-
272
- # Log an informational message indicating that the scheduler has shut down
273
- self.__logger.info(message)
274
-
275
- # Display a shutdown message for the scheduler worker on console
276
- if self.__app.config('app.debug', False):
277
- self.__console.info(message)
278
-
279
- # Retrieve the global identifier for the scheduler shutdown event
280
- scheduler_shutdown = GlobalListener.SCHEDULER_SHUTDOWN.value
281
-
282
- # Check if a listener is registered for the scheduler shutdown event
283
- if scheduler_shutdown in self.__listeners:
284
- listener = self.__listeners[scheduler_shutdown]
285
-
286
- # Ensure the listener is callable before invoking it
287
- if callable(listener):
288
- # Invoke the registered listener with the event details
289
- listener(event)
490
+ self.__globalCallableListener(event, ListeningEvent.SCHEDULER_STARTED)
290
491
 
291
492
  def __pausedListener(
292
493
  self,
@@ -326,17 +527,8 @@ class Scheduler(ISchedule):
326
527
  if self.__app.config('app.debug', False):
327
528
  self.__console.info(message)
328
529
 
329
- # Retrieve the global identifier for the scheduler paused event
330
- scheduler_paused = GlobalListener.SCHEDULER_PAUSED.value
331
-
332
- # Check if a listener is registered for the scheduler paused event
333
- if scheduler_paused in self.__listeners:
334
- listener = self.__listeners[scheduler_paused]
335
-
336
- # Ensure the listener is callable before invoking it
337
- if callable(listener):
338
- # Invoke the registered listener with the event details
339
- listener(event)
530
+ # Check if a listener is registered for the scheduler started event
531
+ self.__globalCallableListener(event, ListeningEvent.SCHEDULER_PAUSED)
340
532
 
341
533
  def __resumedListener(
342
534
  self,
@@ -376,17 +568,90 @@ class Scheduler(ISchedule):
376
568
  if self.__app.config('app.debug', False):
377
569
  self.__console.info(message)
378
570
 
379
- # Retrieve the global identifier for the scheduler resumed event
380
- scheduler_resumed = GlobalListener.SCHEDULER_RESUMED.value
571
+ # Check if a listener is registered for the scheduler started event
572
+ self.__globalCallableListener(event, ListeningEvent.SCHEDULER_RESUMED)
573
+
574
+ def __shutdownListener(
575
+ self,
576
+ event: SchedulerShutdown
577
+ ) -> None:
578
+ """
579
+ Handle the scheduler shutdown event for logging and invoking registered listeners.
381
580
 
382
- # Check if a listener is registered for the scheduler resumed event
383
- if scheduler_resumed in self.__listeners:
384
- listener = self.__listeners[scheduler_resumed]
581
+ This method is triggered when the scheduler shuts down. It logs an informational
582
+ message indicating that the scheduler has shut down successfully and displays
583
+ a formatted message on the rich console. If a listener is registered for the
584
+ scheduler shutdown event, it invokes the listener with the event details.
385
585
 
386
- # Ensure the listener is callable before invoking it
387
- if callable(listener):
388
- # Invoke the registered listener with the event details
389
- listener(event)
586
+ Parameters
587
+ ----------
588
+ event : SchedulerShutdown
589
+ An event object containing details about the scheduler shutdown event.
590
+
591
+ Returns
592
+ -------
593
+ None
594
+ This method does not return any value. It performs logging, displays
595
+ a message on the console, and invokes any registered listener for the
596
+ scheduler shutdown event.
597
+ """
598
+
599
+ # Get the current time in the configured timezone
600
+ now = self.__getCurrentTime()
601
+
602
+ # Create a shutdown message
603
+ message = f"Orionis Scheduler shut down successfully at {now}."
604
+
605
+ # Log an informational message indicating that the scheduler has shut down
606
+ self.__logger.info(message)
607
+
608
+ # Display a shutdown message for the scheduler worker on console
609
+ if self.__app.config('app.debug', False):
610
+ self.__console.info(message)
611
+
612
+ # Check if a listener is registered for the scheduler started event
613
+ self.__globalCallableListener(event, ListeningEvent.SCHEDULER_SHUTDOWN)
614
+
615
+ def __errorListener(
616
+ self,
617
+ event: JobError
618
+ ) -> None:
619
+ """
620
+ Handle job error events for logging and error reporting.
621
+
622
+ This method is triggered when a job execution results in an error. It logs an error
623
+ message indicating the job ID and the exception raised. If the application is in
624
+ debug mode, it also reports the error using the error reporter. Additionally, if a
625
+ listener is registered for the errored job, it invokes the listener with the event details.
626
+
627
+ Parameters
628
+ ----------
629
+ event : JobError
630
+ An instance of the JobError event containing details about the errored job,
631
+ including its ID and the exception raised.
632
+
633
+ Returns
634
+ -------
635
+ None
636
+ This method does not return any value. It performs logging, error reporting,
637
+ and listener invocation for the job error event.
638
+ """
639
+
640
+ # Create an error message
641
+ message = f"Task {event.job_id} raised an exception: {event.exception}"
642
+
643
+ # Log an error message indicating that the job raised an exception
644
+ self.__logger.error(message)
645
+
646
+ # If the application is in debug mode, display a message on the console
647
+ if self.__app.config('app.debug', False):
648
+ self.__console.error(message)
649
+
650
+ # If a listener is registered for this job ID, invoke the listener with the event details
651
+ self.__taskCallableListener(event, ListeningEvent.JOB_ON_FAILURE)
652
+
653
+ # Check if a listener is registered for the scheduler started event
654
+ self.__globalCallableListener(event, ListeningEvent.SCHEDULER_ERROR)
390
655
 
391
656
  def __submittedListener(
392
657
  self,
@@ -424,14 +689,7 @@ class Scheduler(ISchedule):
424
689
  self.__console.info(message)
425
690
 
426
691
  # If a listener is registered for this job ID, invoke the listener with the event details
427
- if event.job_id in self.__listeners:
428
- listener = self.__listeners[event.job_id]
429
-
430
- # Ensure the listener is callable before invoking it
431
- if callable(listener):
432
-
433
- # Invoke the registered listener with the event details
434
- listener(event)
692
+ self.__taskCallableListener(event, ListeningEvent.JOB_BEFORE)
435
693
 
436
694
  def __executedListener(
437
695
  self,
@@ -470,14 +728,7 @@ class Scheduler(ISchedule):
470
728
  self.__console.info(message)
471
729
 
472
730
  # If a listener is registered for this job ID, invoke the listener with the event details
473
- if event.job_id in self.__listeners:
474
- listener = self.__listeners[event.job_id]
475
-
476
- # Ensure the listener is callable before invoking it
477
- if callable(listener):
478
-
479
- # Invoke the registered listener with the event details
480
- listener(event)
731
+ self.__taskCallableListener(event, ListeningEvent.JOB_AFTER)
481
732
 
482
733
  def __missedListener(
483
734
  self,
@@ -516,59 +767,7 @@ class Scheduler(ISchedule):
516
767
  self.__console.warning(message)
517
768
 
518
769
  # If a listener is registered for this job ID, invoke the listener with the event details
519
- if event.job_id in self.__listeners:
520
- listener = self.__listeners[event.job_id]
521
-
522
- # Ensure the listener is callable before invoking it
523
- if callable(listener):
524
-
525
- # Invoke the registered listener with the event details
526
- listener(event)
527
-
528
- def __errorListener(
529
- self,
530
- event: JobError
531
- ) -> None:
532
- """
533
- Handle job error events for logging and error reporting.
534
-
535
- This method is triggered when a job execution results in an error. It logs an error
536
- message indicating the job ID and the exception raised. If the application is in
537
- debug mode, it also reports the error using the error reporter. Additionally, if a
538
- listener is registered for the errored job, it invokes the listener with the event details.
539
-
540
- Parameters
541
- ----------
542
- event : JobError
543
- An instance of the JobError event containing details about the errored job,
544
- including its ID and the exception raised.
545
-
546
- Returns
547
- -------
548
- None
549
- This method does not return any value. It performs logging, error reporting,
550
- and listener invocation for the job error event.
551
- """
552
-
553
- # Create an error message
554
- message = f"Task {event.job_id} raised an exception: {event.exception}"
555
-
556
- # Log an error message indicating that the job raised an exception
557
- self.__logger.error(message)
558
-
559
- # If the application is in debug mode, display a message on the console
560
- if self.__app.config('app.debug', False):
561
- self.__console.error(message)
562
-
563
- # If a listener is registered for this job ID, invoke the listener with the event details
564
- if event.job_id in self.__listeners:
565
- listener = self.__listeners[event.job_id]
566
-
567
- # Ensure the listener is callable before invoking it
568
- if callable(listener):
569
-
570
- # Invoke the registered listener with the event details
571
- listener(event)
770
+ self.__taskCallableListener(event, ListeningEvent.JOB_ON_MISSED)
572
771
 
573
772
  def __maxInstancesListener(
574
773
  self,
@@ -607,107 +806,87 @@ class Scheduler(ISchedule):
607
806
  self.__console.error(message)
608
807
 
609
808
  # If a listener is registered for this job ID, invoke the listener with the event details
610
- if event.job_id in self.__listeners:
611
- listener = self.__listeners[event.job_id]
612
-
613
- # Ensure the listener is callable before invoking it
614
- if callable(listener):
615
-
616
- # Invoke the registered listener with the event details
617
- listener(event)
618
-
619
- def __getCommands(
620
- self
621
- ) -> dict:
622
- """
623
- Retrieve available commands from the reactor and return them as a dictionary.
624
-
625
- This method queries the reactor for all available jobs/commands, extracting their
626
- signatures and descriptions. The result is a dictionary where each key is the command
627
- signature and the value is another dictionary containing the command's signature and
628
- its description.
629
-
630
- Returns
631
- -------
632
- dict
633
- A dictionary mapping command signatures to their details. Each value is a dictionary
634
- with 'signature' and 'description' keys.
635
- """
636
-
637
- # Initialize the commands dictionary
638
- commands = {}
809
+ self.__taskCallableListener(event, ListeningEvent.JOB_ON_MAXINSTANCES)
639
810
 
640
- # Iterate over all jobs provided by the reactor's info method
641
- for job in self.__reactor.info():
642
-
643
- # Store each job's signature and description in the commands dictionary
644
- commands[job['signature']] = {
645
- 'signature': job['signature'],
646
- 'description': job.get('description', '')
647
- }
648
-
649
- # Return the commands dictionary
650
- return commands
651
-
652
- def __isAvailable(
811
+ def __modifiedListener(
653
812
  self,
654
- signature: str
655
- ) -> bool:
813
+ event: JobModified
814
+ ) -> None:
656
815
  """
657
- Check if a command with the given signature is available.
816
+ Handle job modified events for logging and error reporting.
658
817
 
659
- This method iterates through the available commands and determines
660
- whether the provided signature matches any registered command.
818
+ This method is triggered when a job is modified. It logs an informational
819
+ message indicating that the job has been modified successfully. If the application
820
+ is in debug mode, it also displays a message on the console. Additionally, if a
821
+ listener is registered for the modified job, it invokes the listener with the
822
+ event details.
661
823
 
662
824
  Parameters
663
825
  ----------
664
- signature : str
665
- The signature of the command to check for availability.
826
+ event : JobModified
827
+ An instance of the JobModified event containing details about the modified job,
828
+ including its ID and other relevant information.
666
829
 
667
830
  Returns
668
831
  -------
669
- bool
670
- True if the command with the specified signature exists and is available,
671
- False otherwise.
832
+ None
833
+ This method does not return any value. It performs logging, error reporting,
834
+ and listener invocation for the job modified event.
672
835
  """
673
836
 
674
- # Iterate through all available command signatures
675
- for command in self.__available_commands.keys():
837
+ # Create a modified message
838
+ message = f"Task {event.job_id} has been modified."
676
839
 
677
- # Return True if the signature matches an available command
678
- if command == signature:
679
- return True
840
+ # Log an informational message indicating that the job has been modified
841
+ self.__logger.info(message)
680
842
 
681
- # Return False if the signature is not found among available commands
682
- return False
843
+ # If the application is in debug mode, display a message on the console
844
+ if self.__app.config('app.debug', False):
845
+ self.__console.info(message)
683
846
 
684
- def __getDescription(
847
+ # If a listener is registered for this job ID, invoke the listener with the event details
848
+ if event.next_run_time is None:
849
+ self.__taskCallableListener(event, ListeningEvent.JOB_ON_PAUSED)
850
+ else:
851
+ self.__taskCallableListener(event, ListeningEvent.JOB_ON_RESUMED)
852
+
853
+ def __removedListener(
685
854
  self,
686
- signature: str
687
- ) -> Optional[str]:
855
+ event: JobRemoved
856
+ ) -> None:
688
857
  """
689
- Retrieve the description of a command given its signature.
858
+ Handle job removal events for logging and invoking registered listeners.
690
859
 
691
- This method looks up the available commands dictionary and returns the description
692
- associated with the provided command signature. If the signature does not exist,
693
- it returns None.
860
+ This method is triggered when a job is removed from the scheduler. It logs an informational
861
+ message indicating that the job has been removed successfully. If the application is in debug
862
+ mode, it displays a message on the console. Additionally, if a listener is registered for the
863
+ removed job, it invokes the listener with the event details.
694
864
 
695
865
  Parameters
696
866
  ----------
697
- signature : str
698
- The unique signature identifying the command.
867
+ event : JobRemoved
868
+ An instance of the JobRemoved event containing details about the removed job,
869
+ including its ID and other relevant information.
699
870
 
700
871
  Returns
701
872
  -------
702
- Optional[str]
703
- The description of the command if found; otherwise, None.
873
+ None
874
+ This method does not return any value. It performs logging and invokes any registered
875
+ listener for the job removal event.
704
876
  """
705
877
 
706
- # Attempt to retrieve the command entry from the available commands dictionary
707
- command_entry = self.__available_commands.get(signature)
878
+ # Create a message indicating that the job has been removed
879
+ message = f"Task {event.job_id} has been removed."
708
880
 
709
- # Return the description if the command exists, otherwise return None
710
- return command_entry['description'] if command_entry else None
881
+ # Log the removal of the job
882
+ self.__logger.info(message)
883
+
884
+ # If the application is in debug mode, display the message on the console
885
+ if self.__app.config('app.debug', False):
886
+ self.__console.info(message)
887
+
888
+ # If a listener is registered for this job ID, invoke the listener with the event details
889
+ self.__taskCallableListener(event, ListeningEvent.JOB_ON_REMOVED)
711
890
 
712
891
  def __loadEvents(
713
892
  self
@@ -735,7 +914,7 @@ class Scheduler(ISchedule):
735
914
  entity = event.toEntity()
736
915
 
737
916
  # Add the job to the internal jobs list
738
- self.__jobs.append(entity.toDict())
917
+ self.__jobs.append(entity)
739
918
 
740
919
  # Create a unique key for the job based on its signature
741
920
  self.__scheduler.add_job(
@@ -749,82 +928,32 @@ class Scheduler(ISchedule):
749
928
  replace_existing=True
750
929
  )
751
930
 
752
- def command(
753
- self,
754
- signature: str,
755
- args: Optional[List[str]] = None
756
- ) -> 'Event':
757
- """
758
- Prepare an Event instance for a given command signature and its arguments.
759
-
760
- This method validates the provided command signature and arguments, ensuring
761
- that the command exists among the registered commands and that the arguments
762
- are in the correct format. If validation passes, it creates and returns an
763
- Event object representing the scheduled command, including its signature,
764
- arguments, and description.
765
-
766
- Parameters
767
- ----------
768
- signature : str
769
- The unique signature identifying the command to be scheduled. Must be a non-empty string.
770
- args : Optional[List[str]], optional
771
- A list of string arguments to be passed to the command. Defaults to None.
772
-
773
- Returns
774
- -------
775
- Event
776
- An Event instance containing the command signature, arguments, and its description.
777
-
778
- Raises
779
- ------
780
- ValueError
781
- If the command signature is not a non-empty string, if the arguments are not a list
782
- of strings or None, or if the command does not exist among the registered commands.
783
- """
784
-
785
- # Validate that the command signature is a non-empty string
786
- if not isinstance(signature, str) or not signature.strip():
787
- raise ValueError("Command signature must be a non-empty string.")
788
-
789
- # Ensure that arguments are either a list of strings or None
790
- if args is not None and not isinstance(args, list):
791
- raise ValueError("Arguments must be a list of strings or None.")
792
-
793
- # Check if the command is available in the registered commands
794
- if not self.__isAvailable(signature):
795
- raise ValueError(f"The command '{signature}' is not available or does not exist.")
796
-
797
- # Store the command and its arguments for scheduling
798
- self.__events[signature] = Event(
799
- signature=signature,
800
- args=args or [],
801
- purpose=self.__getDescription(signature)
802
- )
803
-
804
- # Return the Event instance for further scheduling configuration
805
- return self.__events[signature]
931
+ # If a listener is associated with the event, register it
932
+ if entity.listener:
933
+ self._setListener(signature, entity.listener)
806
934
 
807
- def _setListener(
935
+ def setListener(
808
936
  self,
809
- event: str,
810
- listener: callable
937
+ event: Union[str, ListeningEvent],
938
+ listener: Union[IScheduleEventListener, callable]
811
939
  ) -> None:
812
940
  """
813
941
  Register a listener callback for a specific scheduler event.
814
942
 
815
- This method allows the registration of a callable listener function that will be
816
- invoked when the specified scheduler event occurs. The event can be one of the
817
- predefined global events or a specific job ID. The listener must be a callable
818
- function that accepts a single argument, which will be the event object.
943
+ This method registers a listener function or an instance of IScheduleEventListener
944
+ to be invoked when the specified scheduler event occurs. The event can be a global
945
+ event name (e.g., 'scheduler_started') or a specific job ID. The listener must be
946
+ callable and should accept the event object as a parameter.
819
947
 
820
948
  Parameters
821
949
  ----------
822
950
  event : str
823
951
  The name of the event to listen for. This can be a global event name (e.g., 'scheduler_started')
824
952
  or a specific job ID.
825
- listener : callable
826
- A callable function that will be invoked when the specified event occurs.
827
- The function should accept one parameter, which will be the event object.
953
+ listener : IScheduleEventListener or callable
954
+ A callable function or an instance of IScheduleEventListener that will be invoked
955
+ when the specified event occurs. The listener should accept one parameter, which
956
+ will be the event object.
828
957
 
829
958
  Returns
830
959
  -------
@@ -834,18 +963,23 @@ class Scheduler(ISchedule):
834
963
  Raises
835
964
  ------
836
965
  ValueError
837
- If the event name is not a non-empty string or if the listener is not callable.
966
+ If the event name is not a non-empty string or if the listener is not callable
967
+ or an instance of IScheduleEventListener.
838
968
  """
839
969
 
970
+ # If the event is an instance of ListeningEvent, extract its value
971
+ if isinstance(event, ListeningEvent):
972
+ event = event.value
973
+
840
974
  # Validate that the event name is a non-empty string
841
975
  if not isinstance(event, str) or not event.strip():
842
976
  raise ValueError("Event name must be a non-empty string.")
843
977
 
844
- # Validate that the listener is a callable function
845
- if not callable(listener):
846
- raise ValueError("Listener must be a callable function.")
978
+ # Validate that the listener is either callable or an instance of IScheduleEventListener
979
+ if not callable(listener) and not isinstance(listener, IScheduleEventListener):
980
+ raise ValueError("Listener must be a callable function or an instance of IScheduleEventListener.")
847
981
 
848
- # Register the listener for the specified event
982
+ # Register the listener for the specified event in the internal listeners dictionary
849
983
  self.__listeners[event] = listener
850
984
 
851
985
  def pauseEverythingAt(
@@ -969,7 +1103,7 @@ class Scheduler(ISchedule):
969
1103
 
970
1104
  # Add a job to the scheduler to shut it down at the specified datetime
971
1105
  self.__scheduler.add_job(
972
- func=self.shutdown, # Function to shut down the scheduler
1106
+ func=self.__scheduler.shutdown, # Function to shut down the scheduler
973
1107
  trigger='date', # Trigger type is 'date' for one-time execution
974
1108
  run_date=at, # The datetime at which the job will run
975
1109
  id=f"shutdown_scheduler_at_{at.isoformat()}", # Unique job ID based on the datetime
@@ -1011,13 +1145,14 @@ class Scheduler(ISchedule):
1011
1145
  try:
1012
1146
 
1013
1147
  # Run indefinitely until interrupted
1014
- while self.__scheduler.running and self.__scheduler.get_jobs():
1148
+ while self.__scheduler.running:
1015
1149
  await asyncio.sleep(1)
1016
1150
 
1017
1151
  except (KeyboardInterrupt, asyncio.CancelledError):
1018
-
1019
- # Handle graceful shutdown on keyboard interrupt or cancellation
1020
1152
  await self.shutdown()
1153
+ except Exception as e:
1154
+ raise CLIOrionisRuntimeError(f"Failed to start the scheduler: {str(e)}") from e
1155
+
1021
1156
 
1022
1157
  except Exception as e:
1023
1158
 
@@ -1028,92 +1163,186 @@ class Scheduler(ISchedule):
1028
1163
  """
1029
1164
  Shut down the AsyncIO scheduler instance asynchronously.
1030
1165
 
1031
- This method gracefully stops the AsyncIOScheduler that handles asynchronous job execution.
1032
- Using async ensures proper cleanup in asyncio environments.
1166
+ This method gracefully stops the AsyncIOScheduler that manages asynchronous job execution.
1167
+ It ensures proper cleanup in asyncio environments and allows for an optional wait period
1168
+ to complete currently executing jobs before shutting down.
1033
1169
 
1034
1170
  Parameters
1035
1171
  ----------
1036
1172
  wait : bool, optional
1037
- If True, the method will wait until all currently executing jobs are completed before shutting down the scheduler.
1038
- If False, the scheduler will be shut down immediately without waiting for running jobs to finish. Default is True.
1173
+ If True, the method waits until all currently executing jobs are completed before shutting down the scheduler.
1174
+ If False, the scheduler shuts down immediately without waiting for running jobs to finish. Default is True.
1039
1175
 
1040
1176
  Returns
1041
1177
  -------
1042
1178
  None
1043
- This method does not return any value. It shuts down the AsyncIO scheduler.
1179
+ This method does not return any value. It performs the shutdown operation for the AsyncIO scheduler.
1180
+
1181
+ Raises
1182
+ ------
1183
+ ValueError
1184
+ If the 'wait' parameter is not a boolean value.
1185
+ CLIOrionisRuntimeError
1186
+ If an error occurs during the shutdown process.
1044
1187
  """
1045
1188
 
1046
- # Validate that the wait parameter is a boolean.
1189
+ # Ensure the 'wait' parameter is a boolean value.
1047
1190
  if not isinstance(wait, bool):
1048
1191
  raise ValueError("The 'wait' parameter must be a boolean value.")
1049
1192
 
1193
+ # If the scheduler is not running, there is nothing to shut down.
1194
+ if not self.__scheduler.running:
1195
+ return
1196
+
1197
+ try:
1198
+ # Shut down the AsyncIOScheduler. If 'wait' is True, it waits for currently executing jobs to finish.
1199
+ self.__scheduler.shutdown(wait=wait)
1200
+
1201
+ # If 'wait' is True, allow a small delay to ensure proper cleanup of resources.
1202
+ if wait:
1203
+ await asyncio.sleep(0)
1204
+
1205
+ except Exception as e:
1206
+
1207
+ # Raise a runtime error if the shutdown process fails.
1208
+ raise CLIOrionisRuntimeError(f"Failed to shut down the scheduler: {str(e)}") from e
1209
+
1210
+ def pause(self, signature: str) -> bool:
1211
+ """
1212
+ Pause a scheduled job in the AsyncIO scheduler.
1213
+
1214
+ This method pauses a job in the AsyncIOScheduler identified by its unique signature.
1215
+ It validates the provided signature to ensure it is a non-empty string and attempts
1216
+ to pause the job. If the operation is successful, it logs the action and returns True.
1217
+ If the job cannot be paused (e.g., it does not exist), the method returns False.
1218
+
1219
+ Parameters
1220
+ ----------
1221
+ signature : str
1222
+ The unique signature (ID) of the job to pause. This must be a non-empty string.
1223
+
1224
+ Returns
1225
+ -------
1226
+ bool
1227
+ True if the job was successfully paused.
1228
+ False if the job does not exist or an error occurred.
1229
+
1230
+ Raises
1231
+ ------
1232
+ CLIOrionisValueError
1233
+ If the `signature` parameter is not a non-empty string.
1234
+ """
1235
+
1236
+ # Validate that the signature is a non-empty string
1237
+ if not isinstance(signature, str) or not signature.strip():
1238
+ raise CLIOrionisValueError("Signature must be a non-empty string.")
1239
+
1050
1240
  try:
1051
1241
 
1052
- # Shut down the AsyncIOScheduler, waiting for jobs if specified.
1053
- if self.__scheduler.running:
1242
+ # Attempt to pause the job with the given signature
1243
+ self.__scheduler.pause_job(signature)
1244
+
1245
+ # Log the successful pausing of the job
1246
+ self.__logger.info(f"Job '{signature}' has been paused.")
1247
+ return True
1248
+
1249
+ except Exception:
1250
+
1251
+ # Return False if the job could not be paused (e.g., it does not exist)
1252
+ return False
1253
+
1254
+ def resume(self, signature: str) -> bool:
1255
+ """
1256
+ Resume a paused job in the AsyncIO scheduler.
1257
+
1258
+ This method attempts to resume a job that was previously paused in the AsyncIOScheduler.
1259
+ It validates the provided job signature, ensures it is a non-empty string, and then
1260
+ resumes the job if it exists and is currently paused. If the operation is successful,
1261
+ it logs the action and returns True. If the job cannot be resumed (e.g., it does not exist),
1262
+ the method returns False.
1263
+
1264
+ Parameters
1265
+ ----------
1266
+ signature : str
1267
+ The unique signature (ID) of the job to resume. This must be a non-empty string.
1268
+
1269
+ Returns
1270
+ -------
1271
+ bool
1272
+ True if the job was successfully resumed, False if the job does not exist or an error occurred.
1273
+
1274
+ Raises
1275
+ ------
1276
+ CLIOrionisValueError
1277
+ If the `signature` parameter is not a non-empty string.
1278
+ """
1279
+
1280
+ # Validate that the signature is a non-empty string
1281
+ if not isinstance(signature, str) or not signature.strip():
1282
+ raise CLIOrionisValueError("Signature must be a non-empty string.")
1054
1283
 
1055
- # For AsyncIOScheduler, shutdown can be called normally
1056
- # but we await any pending operations
1057
- self.__scheduler.shutdown(wait=wait)
1284
+ try:
1285
+ # Attempt to resume the job with the given signature
1286
+ self.__scheduler.resume_job(signature)
1058
1287
 
1059
- # Give a small delay to ensure proper cleanup
1060
- if wait:
1061
- await asyncio.sleep(0.1)
1288
+ # Log the successful resumption of the job
1289
+ self.__logger.info(f"Job '{signature}' has been resumed.")
1290
+ return True
1062
1291
 
1063
1292
  except Exception:
1064
1293
 
1065
- # AsyncIOScheduler may not be running or may have issues in shutdown
1066
- pass
1294
+ # Return False if the job could not be resumed (e.g., it does not exist)
1295
+ return False
1067
1296
 
1068
- async def remove(self, signature: str) -> bool:
1297
+ def remove(self, signature: str) -> bool:
1069
1298
  """
1070
- Remove a scheduled job from the AsyncIO scheduler asynchronously.
1299
+ Remove a scheduled job from the AsyncIO scheduler.
1071
1300
 
1072
- This method removes a job with the specified signature from both the internal
1073
- jobs dictionary and the AsyncIOScheduler instance. Using async ensures proper
1074
- cleanup in asyncio environments.
1301
+ This method removes a job from the AsyncIOScheduler using its unique signature (ID).
1302
+ It validates the provided signature to ensure it is a non-empty string, attempts to
1303
+ remove the job from the scheduler, and updates the internal jobs list accordingly.
1304
+ If the operation is successful, it logs the action and returns True. If the job
1305
+ cannot be removed (e.g., it does not exist), the method returns False.
1075
1306
 
1076
1307
  Parameters
1077
1308
  ----------
1078
1309
  signature : str
1079
- The signature of the command/job to remove from the scheduler.
1310
+ The unique signature (ID) of the job to remove. This must be a non-empty string.
1080
1311
 
1081
1312
  Returns
1082
1313
  -------
1083
1314
  bool
1084
- Returns True if the job was successfully removed, False if the job was not found.
1315
+ True if the job was successfully removed from the scheduler.
1316
+ False if the job does not exist or an error occurred.
1085
1317
 
1086
1318
  Raises
1087
1319
  ------
1088
- ValueError
1089
- If the signature is not a non-empty string.
1320
+ CLIOrionisValueError
1321
+ If the `signature` parameter is not a non-empty string.
1090
1322
  """
1091
1323
 
1092
1324
  # Validate that the signature is a non-empty string
1093
1325
  if not isinstance(signature, str) or not signature.strip():
1094
- raise ValueError("Signature must be a non-empty string.")
1326
+ raise CLIOrionisValueError("Signature must be a non-empty string.")
1095
1327
 
1096
1328
  try:
1097
1329
 
1098
- # Remove from the scheduler
1330
+ # Attempt to remove the job from the scheduler using its signature
1099
1331
  self.__scheduler.remove_job(signature)
1100
1332
 
1101
- # Remove from internal jobs dictionary
1102
- if signature in self.__jobs:
1103
- del self.__jobs[signature]
1333
+ # Iterate through the internal jobs list to find and remove the job
1334
+ for job in self.__jobs:
1335
+ if job['signature'] == signature:
1336
+ self.__jobs.remove(job) # Remove the job from the internal list
1337
+ break
1104
1338
 
1105
- # Give a small delay to ensure proper cleanup
1106
- await asyncio.sleep(0.01)
1107
-
1108
- # Log the removal of the job
1339
+ # Log the successful removal of the job
1109
1340
  self.__logger.info(f"Job '{signature}' has been removed from the scheduler.")
1110
-
1111
- # Return True to indicate successful removal
1112
1341
  return True
1113
1342
 
1114
1343
  except Exception:
1115
1344
 
1116
- # Job not found or other error
1345
+ # Return False if the job could not be removed (e.g., it does not exist)
1117
1346
  return False
1118
1347
 
1119
1348
  def events(self) -> list: