orionis 0.511.0__py3-none-any.whl → 0.513.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.
Files changed (47) hide show
  1. orionis/console/base/command.py +1 -1
  2. orionis/console/base/scheduler.py +1 -1
  3. orionis/console/base/scheduler_event_listener.py +136 -0
  4. orionis/console/commands/scheduler_work.py +37 -25
  5. orionis/console/contracts/event.py +3032 -9
  6. orionis/console/contracts/schedule.py +34 -0
  7. orionis/console/contracts/schedule_event_listener.py +320 -0
  8. orionis/console/contracts/scheduler.py +140 -0
  9. orionis/console/core/reactor.py +1 -1
  10. orionis/console/entities/all_jobs_removed.py +23 -0
  11. orionis/console/entities/executor_added.py +23 -0
  12. orionis/console/entities/executor_removed.py +25 -0
  13. orionis/console/entities/job_added.py +24 -0
  14. orionis/console/entities/job_error.py +35 -0
  15. orionis/console/entities/job_event_data.py +40 -0
  16. orionis/console/entities/job_executed.py +31 -0
  17. orionis/console/entities/job_max_instances.py +27 -0
  18. orionis/console/entities/job_missed.py +25 -0
  19. orionis/console/entities/job_modified.py +23 -0
  20. orionis/console/entities/job_pause.py +22 -0
  21. orionis/console/entities/job_removed.py +22 -0
  22. orionis/console/entities/job_resume.py +25 -0
  23. orionis/console/entities/job_store_added.py +24 -0
  24. orionis/console/entities/job_store_removed.py +25 -0
  25. orionis/console/entities/job_submitted.py +24 -0
  26. orionis/console/entities/scheduler_event_data.py +33 -0
  27. orionis/console/entities/scheduler_paused.py +17 -0
  28. orionis/console/entities/scheduler_resumed.py +24 -0
  29. orionis/console/entities/scheduler_shutdown.py +23 -0
  30. orionis/console/entities/scheduler_started.py +21 -0
  31. orionis/console/enums/listener.py +75 -0
  32. orionis/console/tasks/event.py +3703 -21
  33. orionis/console/tasks/schedule.py +701 -52
  34. orionis/foundation/application.py +1 -1
  35. orionis/metadata/framework.py +1 -1
  36. {orionis-0.511.0.dist-info → orionis-0.513.0.dist-info}/METADATA +1 -1
  37. {orionis-0.511.0.dist-info → orionis-0.513.0.dist-info}/RECORD +42 -22
  38. orionis/console/base/contracts/__init__.py +0 -0
  39. orionis/console/base/contracts/scheduler.py +0 -38
  40. orionis/console/contracts/listener.py +0 -132
  41. orionis/console/entities/listeners.py +0 -241
  42. orionis/console/tasks/exception_report.py +0 -94
  43. /orionis/console/{base/contracts → contracts}/command.py +0 -0
  44. {orionis-0.511.0.dist-info → orionis-0.513.0.dist-info}/WHEEL +0 -0
  45. {orionis-0.511.0.dist-info → orionis-0.513.0.dist-info}/licenses/LICENCE +0 -0
  46. {orionis-0.511.0.dist-info → orionis-0.513.0.dist-info}/top_level.txt +0 -0
  47. {orionis-0.511.0.dist-info → orionis-0.513.0.dist-info}/zip-safe +0 -0
@@ -1,15 +1,38 @@
1
1
  import asyncio
2
+ from datetime import datetime
2
3
  import logging
3
4
  from typing import Dict, List, Optional
4
5
  import pytz
5
- from apscheduler.events import EVENT_JOB_MISSED, EVENT_JOB_ERROR, EVENT_ALL, EVENT_SCHEDULER_STARTED
6
+ from apscheduler.events import (
7
+ EVENT_JOB_ERROR,
8
+ EVENT_JOB_EXECUTED,
9
+ EVENT_JOB_MISSED,
10
+ EVENT_JOB_SUBMITTED,
11
+ EVENT_SCHEDULER_PAUSED,
12
+ EVENT_SCHEDULER_RESUMED,
13
+ EVENT_SCHEDULER_SHUTDOWN,
14
+ EVENT_SCHEDULER_STARTED,
15
+ EVENT_JOB_MAX_INSTANCES
16
+ )
6
17
  from apscheduler.schedulers.asyncio import AsyncIOScheduler as APSAsyncIOScheduler
18
+ from rich.console import Console
19
+ from rich.panel import Panel
20
+ from rich.text import Text
7
21
  from orionis.console.contracts.reactor import IReactor
8
22
  from orionis.console.contracts.schedule import ISchedule
23
+ from orionis.console.entities.job_error import JobError
24
+ from orionis.console.entities.job_executed import JobExecuted
25
+ from orionis.console.entities.job_max_instances import JobMaxInstances
26
+ from orionis.console.entities.job_missed import JobMissed
27
+ from orionis.console.entities.job_submitted import JobSubmitted
28
+ from orionis.console.entities.scheduler_paused import SchedulerPaused
29
+ from orionis.console.entities.scheduler_resumed import SchedulerResumed
30
+ from orionis.console.entities.scheduler_shutdown import SchedulerShutdown
31
+ from orionis.console.entities.scheduler_started import SchedulerStarted
32
+ from orionis.console.enums.listener import ListeningEvent
9
33
  from orionis.console.exceptions import CLIOrionisRuntimeError
10
34
  from orionis.console.output.contracts.console import IConsole
11
35
  from orionis.console.tasks.event import Event
12
- from orionis.console.tasks.exception_report import ScheduleErrorReporter
13
36
  from orionis.foundation.contracts.application import IApplication
14
37
  from orionis.services.log.contracts.log_service import ILogger
15
38
 
@@ -20,7 +43,7 @@ class Scheduler(ISchedule):
20
43
  reactor: IReactor,
21
44
  app: IApplication,
22
45
  console: IConsole,
23
- error_reporter: ScheduleErrorReporter
46
+ rich_console: Console
24
47
  ) -> None:
25
48
  """
26
49
  Initialize a new instance of the Scheduler class.
@@ -48,8 +71,8 @@ class Scheduler(ISchedule):
48
71
  # Store the console instance for output operations.
49
72
  self.__console = console
50
73
 
51
- # Store the error reporter instance for handling exceptions.
52
- self.__error_reporter = error_reporter
74
+ # Store the rich console instance for advanced output formatting.
75
+ self.__rich_console = rich_console
53
76
 
54
77
  # Initialize AsyncIOScheduler instance with timezone configuration.
55
78
  self.__scheduler: APSAsyncIOScheduler = APSAsyncIOScheduler(
@@ -79,49 +102,519 @@ class Scheduler(ISchedule):
79
102
  # Initialize the jobs list to keep track of all scheduled jobs.
80
103
  self.__jobs: List[dict] = []
81
104
 
82
- # Add a listener to the scheduler to capture job events such as missed jobs or errors.
105
+ # Initialize the listeners dictionary to manage event listeners.
106
+ self.__listeners: Dict[str, callable] = {}
107
+
108
+ def __getCurrentTime(
109
+ self
110
+ ) -> str:
111
+ """
112
+ Get the current date and time formatted as a string.
113
+
114
+ This method retrieves the current date and time in the timezone configured
115
+ for the application and formats it as a string in the "YYYY-MM-DD HH:MM:SS" format.
116
+
117
+ Returns
118
+ -------
119
+ str
120
+ A string representing the current date and time in the configured timezone,
121
+ formatted as "YYYY-MM-DD HH:MM:SS".
122
+ """
123
+
124
+ tz = pytz.timezone(self.__app.config("app.timezone", "UTC"))
125
+ now = datetime.now(tz)
126
+ return now.strftime("%Y-%m-%d %H:%M:%S")
127
+
128
+ def __suscribeListeners(
129
+ self
130
+ ) -> None:
131
+ """
132
+ Subscribe to scheduler events for monitoring and handling.
133
+
134
+ This method sets up event listeners for the AsyncIOScheduler instance to monitor
135
+ various scheduler events such as scheduler start, shutdown, pause, resume, job submission,
136
+ execution, missed jobs, and errors. Each listener is associated with a specific event type
137
+ and is responsible for handling the corresponding event.
138
+
139
+ The listeners log relevant information, invoke registered callbacks, and handle errors
140
+ or missed jobs as needed. This ensures that the scheduler's state and job execution
141
+ are monitored effectively.
142
+
143
+ Returns
144
+ -------
145
+ None
146
+ This method does not return any value. It configures event listeners on the scheduler.
147
+ """
148
+
149
+ # Add a listener for the scheduler started event
150
+ self.__scheduler.add_listener(self.__startedListener, EVENT_SCHEDULER_STARTED)
151
+
152
+ # Add a listener for the scheduler shutdown event
153
+ self.__scheduler.add_listener(self.__shutdownListener, EVENT_SCHEDULER_SHUTDOWN)
154
+
155
+ # Add a listener for the scheduler paused event
156
+ self.__scheduler.add_listener(self.__pausedListener, EVENT_SCHEDULER_PAUSED)
157
+
158
+ # Add a listener for the scheduler resumed event
159
+ self.__scheduler.add_listener(self.__resumedListener, EVENT_SCHEDULER_RESUMED)
160
+
161
+ # Add a listener for job submission events
162
+ self.__scheduler.add_listener(self.__submittedListener, EVENT_JOB_SUBMITTED)
163
+
164
+ # Add a listener for job execution events
165
+ self.__scheduler.add_listener(self.__executedListener, EVENT_JOB_EXECUTED)
166
+
167
+ # Add a listener for missed job events
168
+ self.__scheduler.add_listener(self.__missedListener, EVENT_JOB_MISSED)
169
+
170
+ # Add a listener for job error events
171
+ self.__scheduler.add_listener(self.__errorListener, EVENT_JOB_ERROR)
172
+
173
+ # Add a listener for job max instances events
174
+ self.__scheduler.add_listener(self.__maxInstancesListener, EVENT_JOB_MAX_INSTANCES)
175
+
176
+ def __startedListener(
177
+ self,
178
+ event: SchedulerStarted
179
+ ) -> None:
180
+ """
181
+ Handle the scheduler started event for logging and invoking registered listeners.
182
+
183
+ This method is triggered when the scheduler starts. It logs an informational
184
+ message indicating that the scheduler has started successfully and displays
185
+ a formatted message on the rich console. If a listener is registered for the
186
+ scheduler started event, it invokes the listener with the event details.
187
+
188
+ Parameters
189
+ ----------
190
+ event : SchedulerStarted
191
+ An event object containing details about the scheduler start event.
192
+
193
+ Returns
194
+ -------
195
+ None
196
+ This method does not return any value. It performs logging, displays
197
+ a message on the console, and invokes any registered listener for the
198
+ scheduler started event.
199
+ """
200
+
201
+ # Get the current time in the configured timezone
202
+ now = self.__getCurrentTime()
203
+
204
+ # Log an informational message indicating that the scheduler has started
205
+ self.__logger.info(f"Orionis Scheduler started successfully at {now}.")
206
+
207
+ # Display a start message for the scheduler worker on the rich console
208
+ # Add a blank line for better formatting
209
+ self.__rich_console.line()
210
+ panel_content = Text.assemble(
211
+ (" Orionis Scheduler Worker ", "bold white on green"), # Header text with styling
212
+ ("\n\n", ""), # Add spacing
213
+ ("The scheduled tasks worker has started successfully.\n", "white"), # Main message
214
+ (f"Started at: {now}\n", "dim"), # Display the start time in dim text
215
+ ("To stop the worker, press ", "white"), # Instruction text
216
+ ("Ctrl+C", "bold yellow"), # Highlight the key combination
217
+ (".", "white") # End the instruction
218
+ )
219
+
220
+ # Display the message in a styled panel
221
+ self.__rich_console.print(
222
+ Panel(panel_content, border_style="green", padding=(1, 2))
223
+ )
224
+
225
+ # Add another blank line for better formatting
226
+ self.__rich_console.line()
227
+
228
+ # Retrieve the global identifier for the scheduler started event
229
+ scheduler_started = ListeningEvent.SCHEDULER_STARTED.value
230
+
231
+ # 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)
290
+
291
+ def __pausedListener(
292
+ self,
293
+ event: SchedulerPaused
294
+ ) -> None:
295
+ """
296
+ Handle the scheduler paused event for logging and invoking registered listeners.
297
+
298
+ This method is triggered when the scheduler is paused. It logs an informational
299
+ message indicating that the scheduler has been paused successfully and displays
300
+ a formatted message on the rich console. If a listener is registered for the
301
+ scheduler paused event, it invokes the listener with the event details.
302
+
303
+ Parameters
304
+ ----------
305
+ event : SchedulerPaused
306
+ An event object containing details about the scheduler paused event.
307
+
308
+ Returns
309
+ -------
310
+ None
311
+ This method does not return any value. It performs logging, displays
312
+ a message on the console, and invokes any registered listener for the
313
+ scheduler paused event.
314
+ """
315
+
316
+ # Get the current time in the configured timezone
317
+ now = self.__getCurrentTime()
318
+
319
+ # Create a paused message
320
+ message = f"Orionis Scheduler paused successfully at {now}."
321
+
322
+ # Log an informational message indicating that the scheduler has been paused
323
+ self.__logger.info(message)
324
+
325
+ # Display a paused message for the scheduler worker on console
326
+ if self.__app.config('app.debug', False):
327
+ self.__console.info(message)
328
+
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)
340
+
341
+ def __resumedListener(
342
+ self,
343
+ event: SchedulerResumed
344
+ ) -> None:
345
+ """
346
+ Handle the scheduler resumed event for logging and invoking registered listeners.
347
+
348
+ This method is triggered when the scheduler resumes from a paused state. It logs an informational
349
+ message indicating that the scheduler has resumed successfully and displays a formatted message
350
+ on the rich console. If a listener is registered for the scheduler resumed event, it invokes
351
+ the listener with the event details.
352
+
353
+ Parameters
354
+ ----------
355
+ event : SchedulerResumed
356
+ An event object containing details about the scheduler resumed event.
357
+
358
+ Returns
359
+ -------
360
+ None
361
+ This method does not return any value. It performs logging, displays
362
+ a message on the console, and invokes any registered listener for the
363
+ scheduler resumed event.
364
+ """
365
+
366
+ # Get the current time in the configured timezone
367
+ now = self.__getCurrentTime()
368
+
369
+ # Create a resumed message
370
+ message = f"Orionis Scheduler resumed successfully at {now}."
371
+
372
+ # Log an informational message indicating that the scheduler has resumed
373
+ self.__logger.info(message)
374
+
375
+ # Display a resumed message for the scheduler worker on console
376
+ if self.__app.config('app.debug', False):
377
+ self.__console.info(message)
378
+
379
+ # Retrieve the global identifier for the scheduler resumed event
380
+ scheduler_resumed = GlobalListener.SCHEDULER_RESUMED.value
381
+
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]
385
+
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)
390
+
391
+ def __submittedListener(
392
+ self,
393
+ event: JobSubmitted
394
+ ) -> None:
395
+ """
396
+ Handle job submission events for logging and error reporting.
397
+
398
+ This method is triggered when a job is submitted to its executor. It logs an informational
399
+ message indicating that the job has been submitted successfully. If the application is in
400
+ debug mode, it also displays a message on the console. Additionally, if a listener is
401
+ registered for the submitted job, it invokes the listener with the event details.
402
+
403
+ Parameters
404
+ ----------
405
+ event : JobSubmitted
406
+ An instance of the JobSubmitted containing details about the submitted job,
407
+ including its ID and scheduled run times.
408
+
409
+ Returns
410
+ -------
411
+ None
412
+ This method does not return any value. It performs logging, error reporting,
413
+ and listener invocation for the job submission event.
414
+ """
415
+
416
+ # Create a submission message
417
+ message = f"Task {event.job_id} submitted to executor."
418
+
419
+ # Log an informational message indicating that the job has been submitted
420
+ self.__logger.info(message)
421
+
422
+ # If the application is in debug mode, display a message on the console
423
+ if self.__app.config('app.debug', False):
424
+ self.__console.info(message)
425
+
426
+ # 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)
435
+
436
+ def __executedListener(
437
+ self,
438
+ event: JobExecuted
439
+ ) -> None:
440
+ """
441
+ Handle job execution events for logging and error reporting.
442
+
443
+ This method is triggered when a job is executed by its executor. It logs an informational
444
+ message indicating that the job has been executed successfully. If the application is in
445
+ debug mode, it also displays a message on the console. If the job execution resulted in
446
+ an exception, it logs the error and reports it using the error reporter. Additionally,
447
+ if a listener is registered for the executed job, it invokes the listener with the event details.
448
+
449
+ Parameters
450
+ ----------
451
+ event : JobExecuted
452
+ An instance of the JobExecuted containing details about the executed job,
453
+ including its ID, return value, exception (if any), and traceback.
454
+
455
+ Returns
456
+ -------
457
+ None
458
+ This method does not return any value. It performs logging, error reporting,
459
+ and listener invocation for the job execution event.
460
+ """
461
+
462
+ # Create an execution message
463
+ message = f"Task {event.job_id} executed."
464
+
465
+ # Log an informational message indicating that the job has been executed
466
+ self.__logger.info(message)
467
+
468
+ # If the application is in debug mode, display a message on the console
83
469
  if self.__app.config('app.debug', False):
84
- self.__scheduler.add_listener(self.__listener, EVENT_ALL)
470
+ self.__console.info(message)
471
+
472
+ # 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):
85
478
 
86
- def __listener(self, event):
479
+ # Invoke the registered listener with the event details
480
+ listener(event)
481
+
482
+ def __missedListener(
483
+ self,
484
+ event: JobMissed
485
+ ) -> None:
87
486
  """
88
- Handle job events by logging errors and missed jobs.
487
+ Handle job missed events for debugging and error reporting.
89
488
 
90
- This method acts as a listener for job events emitted by the scheduler. It processes
91
- two main types of events: job execution errors and missed job executions. When a job
92
- raises an exception during execution, the method logs the error and delegates reporting
93
- to the error reporter. If a job is missed (i.e., not executed at its scheduled time),
94
- the method logs a warning and notifies the error reporter accordingly.
489
+ This method is triggered when a scheduled job is missed. It logs a warning
490
+ message indicating the missed job and its scheduled run time. If the application
491
+ is in debug mode, it reports the missed job using the error reporter. Additionally,
492
+ if a listener is registered for the missed job, it invokes the listener with the
493
+ event details.
95
494
 
96
495
  Parameters
97
496
  ----------
98
- event : apscheduler.events.JobEvent
99
- The job event object containing information about the job execution, such as
100
- job_id, exception, traceback, code, and scheduled_run_time.
497
+ event : JobMissed
498
+ An instance of the JobMissed event containing details about the missed job,
499
+ including its ID and scheduled run time.
101
500
 
102
501
  Returns
103
502
  -------
104
503
  None
105
- This method does not return any value. It performs logging and error reporting
106
- based on the event type.
107
- """
108
-
109
- # If the event contains an exception, log the error and report it
110
- if event.exception:
111
- self.__logger.error(f"Job {event.job_id} raised an exception: {event.exception}")
112
- self.__error_reporter.reportException(
113
- job_id=event.job_id,
114
- exception=event.exception,
115
- traceback=event.traceback
116
- )
117
-
118
- # If the event indicates a missed job, log a warning and report it
119
- elif event.code == EVENT_JOB_MISSED:
120
- self.__logger.warning(f"Job {event.job_id} was missed, scheduled for {event.scheduled_run_time}")
121
- self.__error_reporter.reportMissed(
122
- job_id=event.job_id,
123
- scheduled_time=event.scheduled_run_time
124
- )
504
+ This method does not return any value. It performs logging, error reporting,
505
+ and listener invocation for the missed job event.
506
+ """
507
+
508
+ # Create a missed job message
509
+ message = f"Task {event.job_id} was missed. It was scheduled to run at {event.scheduled_run_time}."
510
+
511
+ # Log a warning indicating that the job was missed
512
+ self.__logger.warning(message)
513
+
514
+ # If the application is in debug mode, report the missed job using the error reporter
515
+ if self.__app.config('app.debug', False):
516
+ self.__console.warning(message)
517
+
518
+ # 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)
572
+
573
+ def __maxInstancesListener(
574
+ self,
575
+ event: JobMaxInstances
576
+ ) -> None:
577
+ """
578
+ Handle job max instances events for logging and error reporting.
579
+
580
+ This method is triggered when a job execution exceeds the maximum allowed
581
+ concurrent instances. It logs an error message indicating the job ID and
582
+ the exception raised. If the application is in debug mode, it also reports
583
+ the error using the error reporter. Additionally, if a listener is registered
584
+ for the job that exceeded max instances, it invokes the listener with the event details.
585
+
586
+ Parameters
587
+ ----------
588
+ event : JobMaxInstances
589
+ An instance of the JobMaxInstances event containing details about the job that
590
+ exceeded max instances, including its ID and the exception raised.
591
+
592
+ Returns
593
+ -------
594
+ None
595
+ This method does not return any value. It performs logging, error reporting,
596
+ and listener invocation for the job max instances event.
597
+ """
598
+
599
+ # Create a max instances error message
600
+ message = f"Task {event.job_id} exceeded maximum instances"
601
+
602
+ # Log an error message indicating that the job exceeded maximum instances
603
+ self.__logger.error(message)
604
+
605
+ # If the application is in debug mode, display a message on the console
606
+ if self.__app.config('app.debug', False):
607
+ self.__console.error(message)
608
+
609
+ # 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)
125
618
 
126
619
  def __getCommands(
127
620
  self
@@ -311,25 +804,178 @@ class Scheduler(ISchedule):
311
804
  # Return the Event instance for further scheduling configuration
312
805
  return self.__events[signature]
313
806
 
314
- def addListenerOnSchedulerStarted(
807
+ def _setListener(
315
808
  self,
809
+ event: str,
316
810
  listener: callable
317
811
  ) -> None:
318
812
  """
319
- Add a listener for the scheduler started event.
813
+ Register a listener callback for a specific scheduler event.
320
814
 
321
- This method allows you to register a callback function that will be called
322
- when the scheduler starts. The callback should accept a single argument, which
323
- is the event object containing details about the scheduler start event.
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.
324
819
 
325
820
  Parameters
326
821
  ----------
822
+ event : str
823
+ The name of the event to listen for. This can be a global event name (e.g., 'scheduler_started')
824
+ or a specific job ID.
327
825
  listener : callable
328
- A function that will be called when the scheduler starts.
329
- It should accept one parameter, which is the event object.
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.
828
+
829
+ Returns
830
+ -------
831
+ None
832
+ This method does not return any value. It registers the listener for the specified event.
833
+
834
+ Raises
835
+ ------
836
+ ValueError
837
+ If the event name is not a non-empty string or if the listener is not callable.
838
+ """
839
+
840
+ # Validate that the event name is a non-empty string
841
+ if not isinstance(event, str) or not event.strip():
842
+ raise ValueError("Event name must be a non-empty string.")
843
+
844
+ # Validate that the listener is a callable function
845
+ if not callable(listener):
846
+ raise ValueError("Listener must be a callable function.")
847
+
848
+ # Register the listener for the specified event
849
+ self.__listeners[event] = listener
850
+
851
+ def pauseEverythingAt(
852
+ self,
853
+ at: datetime
854
+ ) -> None:
330
855
  """
331
- # Register the listener for the scheduler started event
332
- self.__scheduler.add_listener(listener, EVENT_SCHEDULER_STARTED)
856
+ Schedule the scheduler to pause all operations at a specific datetime.
857
+
858
+ This method allows you to schedule a job that will pause the AsyncIOScheduler
859
+ at the specified datetime. The job is added to the scheduler with a 'date'
860
+ trigger, ensuring it executes exactly at the given time.
861
+
862
+ Parameters
863
+ ----------
864
+ at : datetime
865
+ The datetime at which the scheduler should be paused. Must be a valid
866
+ datetime object.
867
+
868
+ Returns
869
+ -------
870
+ None
871
+ This method does not return any value. It schedules a job to pause the
872
+ scheduler at the specified datetime.
873
+
874
+ Raises
875
+ ------
876
+ ValueError
877
+ If the 'at' parameter is not a valid datetime object.
878
+ """
879
+
880
+ # Validate that the 'at' parameter is a datetime object
881
+ if not isinstance(at, datetime):
882
+ raise ValueError("The 'at' parameter must be a datetime object.")
883
+
884
+ # Add a job to the scheduler to pause it at the specified datetime
885
+ self.__scheduler.add_job(
886
+ func=self.__scheduler.pause, # Function to pause the scheduler
887
+ trigger='date', # Trigger type is 'date' for one-time execution
888
+ run_date=at, # The datetime at which the job will run
889
+ id=f"pause_scheduler_at_{at.isoformat()}", # Unique job ID based on the datetime
890
+ name=f"Pause Scheduler at {at.isoformat()}", # Descriptive name for the job
891
+ replace_existing=True # Replace any existing job with the same ID
892
+ )
893
+
894
+ def resumeEverythingAt(
895
+ self,
896
+ at: datetime
897
+ ) -> None:
898
+ """
899
+ Schedule the scheduler to resume all operations at a specific datetime.
900
+
901
+ This method allows you to schedule a job that will resume the AsyncIOScheduler
902
+ at the specified datetime. The job is added to the scheduler with a 'date'
903
+ trigger, ensuring it executes exactly at the given time.
904
+
905
+ Parameters
906
+ ----------
907
+ at : datetime
908
+ The datetime at which the scheduler should be resumed. Must be a valid
909
+ datetime object.
910
+
911
+ Returns
912
+ -------
913
+ None
914
+ This method does not return any value. It schedules a job to resume the
915
+ scheduler at the specified datetime.
916
+
917
+ Raises
918
+ ------
919
+ ValueError
920
+ If the 'at' parameter is not a valid datetime object.
921
+ """
922
+
923
+ # Validate that the 'at' parameter is a datetime object
924
+ if not isinstance(at, datetime):
925
+ raise ValueError("The 'at' parameter must be a datetime object.")
926
+
927
+ # Add a job to the scheduler to resume it at the specified datetime
928
+ self.__scheduler.add_job(
929
+ func=self.__scheduler.resume, # Function to resume the scheduler
930
+ trigger='date', # Trigger type is 'date' for one-time execution
931
+ run_date=at, # The datetime at which the job will run
932
+ id=f"resume_scheduler_at_{at.isoformat()}", # Unique job ID based on the datetime
933
+ name=f"Resume Scheduler at {at.isoformat()}", # Descriptive name for the job
934
+ replace_existing=True # Replace any existing job with the same ID
935
+ )
936
+
937
+ def shutdownEverythingAt(
938
+ self,
939
+ at: datetime
940
+ ) -> None:
941
+ """
942
+ Schedule the scheduler to shut down all operations at a specific datetime.
943
+
944
+ This method allows you to schedule a job that will shut down the AsyncIOScheduler
945
+ at the specified datetime. The job is added to the scheduler with a 'date'
946
+ trigger, ensuring it executes exactly at the given time.
947
+
948
+ Parameters
949
+ ----------
950
+ at : datetime
951
+ The datetime at which the scheduler should be shut down. Must be a valid
952
+ datetime object.
953
+
954
+ Returns
955
+ -------
956
+ None
957
+ This method does not return any value. It schedules a job to shut down the
958
+ scheduler at the specified datetime.
959
+
960
+ Raises
961
+ ------
962
+ ValueError
963
+ If the 'at' parameter is not a valid datetime object.
964
+ """
965
+
966
+ # Validate that the 'at' parameter is a datetime object
967
+ if not isinstance(at, datetime):
968
+ raise ValueError("The 'at' parameter must be a datetime object.")
969
+
970
+ # Add a job to the scheduler to shut it down at the specified datetime
971
+ self.__scheduler.add_job(
972
+ func=self.shutdown, # Function to shut down the scheduler
973
+ trigger='date', # Trigger type is 'date' for one-time execution
974
+ run_date=at, # The datetime at which the job will run
975
+ id=f"shutdown_scheduler_at_{at.isoformat()}", # Unique job ID based on the datetime
976
+ name=f"Shutdown Scheduler at {at.isoformat()}", # Descriptive name for the job
977
+ replace_existing=True # Replace any existing job with the same ID
978
+ )
333
979
 
334
980
  async def start(self) -> None:
335
981
  """
@@ -348,22 +994,28 @@ class Scheduler(ISchedule):
348
994
  # Start the AsyncIOScheduler to handle asynchronous jobs.
349
995
  try:
350
996
 
351
- # Load existing events into the scheduler
352
- self.events()
997
+ # Ensure all events are loaded into the internal jobs list
998
+ self.__loadEvents()
999
+
1000
+ # Subscribe to scheduler events for monitoring and handling
1001
+ self.__suscribeListeners()
353
1002
 
354
1003
  # Ensure we're in an asyncio context
355
1004
  asyncio.get_running_loop()
356
1005
 
357
1006
  # Start the scheduler
358
1007
  if not self.__scheduler.running:
359
- self.__logger.info(f"Orionis Scheduler started. {len(self.__jobs)} jobs scheduled.")
360
1008
  self.__scheduler.start()
361
1009
 
362
1010
  # Keep the event loop alive to process scheduled jobs
363
1011
  try:
1012
+
1013
+ # Run indefinitely until interrupted
364
1014
  while self.__scheduler.running and self.__scheduler.get_jobs():
365
1015
  await asyncio.sleep(1)
1016
+
366
1017
  except (KeyboardInterrupt, asyncio.CancelledError):
1018
+
367
1019
  # Handle graceful shutdown on keyboard interrupt or cancellation
368
1020
  await self.shutdown()
369
1021
 
@@ -408,9 +1060,6 @@ class Scheduler(ISchedule):
408
1060
  if wait:
409
1061
  await asyncio.sleep(0.1)
410
1062
 
411
- # Log the shutdown of the scheduler
412
- self.__logger.info("Orionis Scheduler has been shut down.")
413
-
414
1063
  except Exception:
415
1064
 
416
1065
  # AsyncIOScheduler may not be running or may have issues in shutdown