orionis 0.619.0__py3-none-any.whl → 0.621.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 Any, Awaitable, Callable, Dict, List, Optional, Union
4
+ from typing import Any, Dict, List, Optional, Union
5
5
  import pytz
6
6
  from apscheduler.events import (
7
7
  EVENT_JOB_ERROR,
@@ -16,7 +16,6 @@ from apscheduler.events import (
16
16
  EVENT_SCHEDULER_STARTED,
17
17
  )
18
18
  from apscheduler.schedulers.asyncio import AsyncIOScheduler as APSAsyncIOScheduler
19
- from apscheduler.triggers.date import DateTrigger
20
19
  from rich.console import Console
21
20
  from rich.panel import Panel
22
21
  from rich.text import Text
@@ -48,96 +47,98 @@ class Schedule(ISchedule):
48
47
  rich_console: Console
49
48
  ) -> None:
50
49
  """
51
- Initialize a new instance of the Scheduler class.
50
+ Initialize a new instance of the Schedule class.
52
51
 
53
52
  This constructor sets up the internal state required for scheduling commands,
54
- including references to the application instance, AsyncIOScheduler, the
53
+ including references to the application instance, the AsyncIOScheduler, the
55
54
  command reactor, and job tracking structures. It also initializes properties
56
- for managing the current scheduling context.
55
+ for managing the current scheduling context, logging, and event listeners.
57
56
 
58
57
  Parameters
59
58
  ----------
60
59
  reactor : IReactor
61
- An instance of a class implementing the IReactor interface, used to
62
- retrieve available commands and execute scheduled jobs.
60
+ An instance implementing the IReactor interface, used to retrieve available
61
+ commands and execute scheduled jobs.
62
+ app : IApplication
63
+ The application container instance, used for configuration, dependency injection,
64
+ and service resolution.
65
+ rich_console : Console
66
+ An instance of Rich's Console for advanced output formatting.
63
67
 
64
68
  Returns
65
69
  -------
66
70
  None
67
- This method does not return any value. It initializes the Scheduler instance.
71
+ This method does not return any value. It initializes the Schedule instance
72
+ and prepares all required internal structures for scheduling and event handling.
68
73
  """
69
74
 
70
- # Store the application instance for configuration access.
75
+ # Store the application instance for configuration and service access.
71
76
  self.__app: IApplication = app
72
77
 
73
78
  # Store the rich console instance for advanced output formatting.
74
79
  self.__rich_console = rich_console
75
80
 
76
- # Initialize AsyncIOScheduler instance with timezone configuration.
77
- self.__scheduler: APSAsyncIOScheduler = APSAsyncIOScheduler(
78
- timezone=pytz.timezone(self.__app.config('app.timezone', 'UTC'))
79
- )
81
+ # Save the timezone configuration from the application settings.
82
+ self.__tz = pytz.timezone(self.__app.config('app.timezone') or 'UTC')
80
83
 
81
- # Clear the APScheduler logger to prevent conflicts with other loggers.
82
- # This is necessary to avoid duplicate log messages or conflicts with other logging configurations.
83
- for name in ["apscheduler", "apscheduler.scheduler", "apscheduler.executors.default"]:
84
- logger = logging.getLogger(name)
85
- logger.handlers.clear()
86
- logger.propagate = False
87
- logger.disabled = True
84
+ # Initialize the AsyncIOScheduler with the configured timezone.
85
+ self.__scheduler = APSAsyncIOScheduler(timezone=self.__tz)
88
86
 
89
- # Initialize the logger from the application instance.
87
+ # Disable APScheduler's internal logging to avoid duplicate/conflicting logs.
88
+ self.__disableLogging()
89
+
90
+ # Initialize the logger using the application's dependency injection.
90
91
  self.__logger: ILogger = self.__app.make(ILogger)
91
92
 
92
- # Store the reactor instance for command management.
93
+ # Store the reactor instance for command management and job execution.
93
94
  self.__reactor: IReactor = reactor
94
95
 
95
96
  # Retrieve and store all available commands from the reactor.
96
97
  self.__available_commands = self.__getCommands()
97
98
 
98
- # Initialize the jobs dictionary to keep track of scheduled jobs.
99
+ # Initialize the dictionary to keep track of scheduled events.
99
100
  self.__events: Dict[str, IEvent] = {}
100
101
 
101
- # Initialize the jobs list to keep track of all scheduled jobs.
102
+ # Initialize the list to keep track of all scheduled job entities.
102
103
  self.__jobs: List[EventEntity] = []
103
104
 
104
- # Initialize the listeners dictionary to manage event listeners.
105
+ # Initialize the dictionary to manage event listeners.
105
106
  self.__listeners: Dict[str, callable] = {}
106
107
 
107
- # Initialize set to track jobs paused by pauseEverythingAt
108
+ # Initialize a set to track jobs paused by pauseEverythingAt.
108
109
  self.__pausedByPauseEverything: set = set()
109
110
 
110
- # Add this line to the existing __init__ method
111
+ # Initialize the asyncio event used to signal scheduler shutdown.
111
112
  self._stopEvent: Optional[asyncio.Event] = None
112
113
 
113
- # Retrieve and initialize the catch instance from the application container.
114
+ # Retrieve and initialize the catch instance for exception handling.
114
115
  self.__catch: ICatch = app.make(ICatch)
115
116
 
116
- def __getCurrentTime(
117
+ def __disableLogging(
117
118
  self
118
- ) -> str:
119
+ ) -> None:
119
120
  """
120
- Get the current date and time formatted as a string.
121
+ Disable APScheduler logging to prevent conflicts and duplicate log messages.
121
122
 
122
- This method retrieves the current date and time in the timezone configured
123
- for the application and formats it as a string in the "YYYY-MM-DD HH:MM:SS" format.
123
+ This method disables logging for the APScheduler library and its key subcomponents.
124
+ It clears all handlers attached to the APScheduler loggers, disables log propagation,
125
+ and turns off the loggers entirely. This is useful in applications that have their
126
+ own logging configuration and want to avoid duplicate or unwanted log output from
127
+ APScheduler.
124
128
 
125
129
  Returns
126
130
  -------
127
- str
128
- A string representing the current date and time in the configured timezone,
129
- formatted as "YYYY-MM-DD HH:MM:SS".
131
+ None
132
+ This method does not return any value. It modifies the logging configuration
133
+ of APScheduler loggers in place.
130
134
  """
131
135
 
132
- # Get the current time in the configured timezone
133
- tz = pytz.timezone(self.__app.config("app.timezone", "UTC"))
134
- now = datetime.now(tz)
135
-
136
- # Log the timezone assignment for debugging purposes
137
- self.__logger.info(f"Timezone assigned to the scheduler: {self.__app.config("app.timezone", "UTC")}")
138
-
139
- # Format the current time as a string
140
- return now.strftime("%Y-%m-%d %H:%M:%S")
136
+ # List of APScheduler logger names to disable
137
+ for name in ["apscheduler", "apscheduler.scheduler", "apscheduler.executors.default"]:
138
+ logger = logging.getLogger(name)
139
+ logger.handlers.clear() # Remove all handlers to prevent output
140
+ logger.propagate = False # Prevent log messages from propagating to ancestor loggers
141
+ logger.disabled = True # Disable the logger entirely
141
142
 
142
143
  def __getCommands(
143
144
  self
@@ -163,24 +164,124 @@ class Schedule(ISchedule):
163
164
  # Iterate over all jobs provided by the reactor's info method
164
165
  for job in self.__reactor.info():
165
166
 
167
+ signature: str = job.get('signature', None)
168
+ description: str = job.get('description', 'No description available.')
169
+
170
+ # Skip invalid or special method signatures
171
+ if not signature or (signature.startswith('__') and signature.endswith('__')):
172
+ continue
173
+
166
174
  # Store each job's signature and description in the commands dictionary
167
- commands[job['signature']] = {
168
- 'signature': job['signature'],
169
- 'description': job.get('description', 'No description available.')
175
+ commands[signature] = {
176
+ 'signature': signature,
177
+ 'description': description
170
178
  }
171
179
 
172
180
  # Return the commands dictionary
173
181
  return commands
174
182
 
183
+ def __getCurrentTime(
184
+ self
185
+ ) -> str:
186
+ """
187
+ Retrieve the current date and time as a formatted string in the configured timezone.
188
+
189
+ This method obtains the current date and time using the timezone specified in the application's
190
+ configuration (defaulting to UTC if not set). The result is formatted as a string in the
191
+ "YYYY-MM-DD HH:MM:SS" format, which is suitable for logging, display, or timestamping events.
192
+
193
+ Returns
194
+ -------
195
+ str
196
+ The current date and time as a string in the format "YYYY-MM-DD HH:MM:SS", localized to the
197
+ scheduler's configured timezone.
198
+ """
199
+
200
+ # Get the current time in the scheduler's configured timezone
201
+ now = datetime.now(self.__tz)
202
+
203
+ # Log the timezone currently assigned to the scheduler for traceability
204
+ self.__logger.info(
205
+ f"Timezone assigned to the scheduler: {self.__app.config('app.timezone') or 'UTC'}"
206
+ )
207
+
208
+ # Return the formatted current time string
209
+ return now.strftime("%Y-%m-%d %H:%M:%S")
210
+
211
+ def __getNow(
212
+ self
213
+ ) -> datetime:
214
+ """
215
+ Retrieve the current date and time as a timezone-aware datetime object.
216
+
217
+ This method returns the current date and time, localized to the timezone configured
218
+ for the scheduler instance. The timezone is determined by the application's configuration
219
+ (typically set during initialization). This is useful for ensuring that all time-related
220
+ operations within the scheduler are consistent and respect the application's timezone settings.
221
+
222
+ Returns
223
+ -------
224
+ datetime
225
+ A timezone-aware `datetime` object representing the current date and time
226
+ in the scheduler's configured timezone.
227
+ """
228
+
229
+ # Return the current datetime localized to the scheduler's timezone
230
+ return datetime.now(self.__tz)
231
+
232
+ def __getAttribute(
233
+ self,
234
+ obj: Any,
235
+ attr: str,
236
+ default: Any = None
237
+ ) -> Any:
238
+ """
239
+ Safely retrieve an attribute from an object, returning a default value if the attribute does not exist.
240
+
241
+ This method attempts to access the specified attribute of the given object. If the attribute
242
+ exists, its value is returned. If the attribute does not exist, the provided default value
243
+ is returned instead. This prevents `AttributeError` exceptions when accessing attributes
244
+ that may not be present on the object.
245
+
246
+ Parameters
247
+ ----------
248
+ obj : Any
249
+ The object from which to retrieve the attribute.
250
+ attr : str
251
+ The name of the attribute to retrieve.
252
+ default : Any, optional
253
+ The value to return if the attribute does not exist. Defaults to None.
254
+
255
+ Returns
256
+ -------
257
+ Any
258
+ The value of the specified attribute if it exists on the object; otherwise, the value
259
+ of `default`.
260
+
261
+ Notes
262
+ -----
263
+ This method is a wrapper around Python's built-in `getattr()` function, providing a safe
264
+ way to access attributes that may or may not exist on an object.
265
+ """
266
+
267
+ # If the object is None, return the default value immediately
268
+ if obj is None:
269
+ return default
270
+
271
+ # Use Python's built-in getattr to safely retrieve the attribute,
272
+ # returning the default value if the attribute is not found.
273
+ return getattr(obj, attr, default)
274
+
175
275
  def __isAvailable(
176
276
  self,
177
277
  signature: str
178
278
  ) -> bool:
179
279
  """
180
- Check if a command with the given signature is available.
280
+ Check if a command with the given signature is available among registered commands.
181
281
 
182
- This method iterates through the available commands and determines
183
- whether the provided signature matches any registered command.
282
+ This method determines whether the provided command signature exists in the internal
283
+ dictionary of available commands. It is used to validate if a command can be scheduled
284
+ or executed by the scheduler.
184
285
 
185
286
  Parameters
186
287
  ----------
@@ -190,46 +291,51 @@ class Schedule(ISchedule):
190
291
  Returns
191
292
  -------
192
293
  bool
193
- True if the command with the specified signature exists and is available,
194
- False otherwise.
195
- """
196
-
197
- # Iterate through all available command signatures
198
- for command in self.__available_commands.keys():
294
+ Returns True if the command with the specified signature exists in the available
295
+ commands dictionary; otherwise, returns False.
199
296
 
200
- # Return True if the signature matches an available command
201
- if command == signature:
202
- return True
297
+ Notes
298
+ -----
299
+ The method performs a simple membership check in the internal commands dictionary.
300
+ It does not validate the format or correctness of the signature itself, only its
301
+ presence among the registered commands.
302
+ """
203
303
 
204
- # Return False if the signature is not found among available commands
205
- return False
304
+ # Check if the signature exists in the available commands dictionary
305
+ return signature in self.__available_commands
206
306
 
207
307
  def __getDescription(
208
308
  self,
209
309
  signature: str
210
310
  ) -> Optional[str]:
211
311
  """
212
- Retrieve the description of a command given its signature.
312
+ Retrieve the description for a command based on its signature.
213
313
 
214
- This method looks up the available commands dictionary and returns the description
215
- associated with the provided command signature. If the signature does not exist,
216
- it returns None.
314
+ This method searches the internal dictionary of available commands for the provided
315
+ command signature and returns the corresponding description if found. If the signature
316
+ does not exist in the available commands, the method returns None.
217
317
 
218
318
  Parameters
219
319
  ----------
220
320
  signature : str
221
- The unique signature identifying the command.
321
+ The unique signature identifying the command whose description is to be retrieved.
222
322
 
223
323
  Returns
224
324
  -------
225
325
  Optional[str]
226
- The description of the command if found; otherwise, None.
326
+ The description string associated with the command signature if it exists;
327
+ otherwise, returns None.
328
+
329
+ Notes
330
+ -----
331
+ This method is useful for displaying human-readable information about commands
332
+ when scheduling or listing tasks.
227
333
  """
228
334
 
229
335
  # Attempt to retrieve the command entry from the available commands dictionary
230
336
  command_entry = self.__available_commands.get(signature)
231
337
 
232
- # Return the description if the command exists, otherwise return None
338
+ # If the command entry exists, return its description; otherwise, return None
233
339
  return command_entry['description'] if command_entry else None
234
340
 
235
341
  def command(
@@ -238,13 +344,12 @@ class Schedule(ISchedule):
238
344
  args: Optional[List[str]] = None
239
345
  ) -> 'IEvent':
240
346
  """
241
- Prepare an Event instance for a given command signature and its arguments.
347
+ Prepare and register an Event instance for a given command signature and its arguments.
242
348
 
243
- This method validates the provided command signature and arguments, ensuring
244
- that the command exists among the registered commands and that the arguments
245
- are in the correct format. If validation passes, it creates and returns an
246
- Event object representing the scheduled command, including its signature,
247
- arguments, and description.
349
+ This method validates the provided command signature and arguments, ensuring that the command
350
+ exists among the registered commands and that the arguments are in the correct format. If validation
351
+ passes, it creates and registers an Event object representing the scheduled command, including its
352
+ signature, arguments, and description. The Event instance is stored internally for later scheduling.
248
353
 
249
354
  Parameters
250
355
  ----------
@@ -255,36 +360,53 @@ class Schedule(ISchedule):
255
360
 
256
361
  Returns
257
362
  -------
258
- Event
259
- An Event instance containing the command signature, arguments, and its description.
363
+ IEvent
364
+ The Event instance containing the command signature, arguments, and its description.
365
+ This instance is also stored internally for further scheduling configuration.
260
366
 
261
367
  Raises
262
368
  ------
263
- ValueError
369
+ CLIOrionisValueError
264
370
  If the command signature is not a non-empty string, if the arguments are not a list
265
371
  of strings or None, or if the command does not exist among the registered commands.
266
372
  """
267
373
 
268
374
  # Prevent adding new commands while the scheduler is running
269
375
  if self.isRunning():
270
- self.__raiseException(CLIOrionisValueError("Cannot add new commands while the scheduler is running."))
376
+ self.__raiseException(
377
+ CLIOrionisValueError(
378
+ "Cannot add new commands while the scheduler is running. Please stop the scheduler before adding new commands."
379
+ )
380
+ )
271
381
 
272
382
  # Validate that the command signature is a non-empty string
273
383
  if not isinstance(signature, str) or not signature.strip():
274
- raise CLIOrionisValueError("Command signature must be a non-empty string.")
384
+ raise CLIOrionisValueError(
385
+ "The command signature must be a non-empty string. Please provide a valid command signature."
386
+ )
275
387
 
276
388
  # Ensure that arguments are either a list of strings or None
277
- if args is not None and not isinstance(args, list):
278
- raise CLIOrionisValueError("Arguments must be a list of strings or None.")
389
+ if args is not None:
390
+ if not isinstance(args, list):
391
+ raise CLIOrionisValueError(
392
+ "Arguments must be provided as a list of strings or None. Please check your arguments."
393
+ )
394
+ for arg in args:
395
+ if not isinstance(arg, str):
396
+ raise CLIOrionisValueError(
397
+ f"Invalid argument '{arg}'. Each argument must be a string."
398
+ )
279
399
 
280
400
  # Check if the command is available in the registered commands
281
401
  if not self.__isAvailable(signature):
282
- raise CLIOrionisValueError(f"The command '{signature}' is not available or does not exist.")
402
+ raise CLIOrionisValueError(
403
+ f"The command '{signature}' is not available or does not exist. Please check the command signature."
404
+ )
283
405
 
284
406
  # Import Event here to avoid circular dependency issues
285
407
  from orionis.console.fluent.event import Event
286
408
 
287
- # Store the command and its arguments for scheduling
409
+ # Store the command and its arguments for scheduling in the internal events dictionary
288
410
  self.__events[signature] = Event(
289
411
  signature=signature,
290
412
  args=args or [],
@@ -319,51 +441,50 @@ class Schedule(ISchedule):
319
441
  """
320
442
 
321
443
  # Extract event data from the internal events list if available
322
- event_data: dict = {}
323
- for job in self.events():
324
- if id == job.get('signature'):
325
- event_data = job
326
- break
444
+ event_data: dict = self.event(id)
327
445
 
328
446
  # Retrieve the job data from the scheduler using the provided job ID
329
447
  data = self.__scheduler.get_job(id)
330
448
 
331
449
  # If no job is found, return EventJob with default values
332
- _id = data.id if data and hasattr(data, 'id') else None
450
+ _id = self.__getAttribute(data, 'id', None)
333
451
  if not _id and code in (EVENT_JOB_MISSED, EVENT_JOB_REMOVED):
334
- _id = event_data.get('signature', None)
452
+ _id = event_data.get('signature')
335
453
  elif not _id:
336
454
  return EventJob()
337
455
 
456
+ # Extract the job code if available
457
+ _code = code if code is not None else 0
458
+
338
459
  # Extract the job name if available
339
- _name = data.name if data and hasattr(data, 'name') else None
460
+ _name = self.__getAttribute(data, 'name', None)
340
461
 
341
462
  # Extract the job function if available
342
- _func = data.func if data and hasattr(data, 'func') else None
463
+ _func = self.__getAttribute(data, 'func', None)
343
464
 
344
465
  # Extract the job arguments if available
345
- _args = data.args if data and hasattr(data, 'args') else tuple(event_data.get('args', []))
466
+ _args = self.__getAttribute(data, 'args', tuple(event_data.get('args', [])))
346
467
 
347
468
  # Extract the job trigger if available
348
- _trigger = data.trigger if data and hasattr(data, 'trigger') else None
469
+ _trigger = self.__getAttribute(data, 'trigger', None)
349
470
 
350
471
  # Extract the job executor if available
351
- _executor = data.executor if data and hasattr(data, 'executor') else None
472
+ _executor = self.__getAttribute(data, 'executor', None)
352
473
 
353
474
  # Extract the job jobstore if available
354
- _jobstore = data.jobstore if data and hasattr(data, 'jobstore') else None
475
+ _jobstore = self.__getAttribute(data, 'jobstore', None)
355
476
 
356
477
  # Extract the job misfire_grace_time if available
357
- _misfire_grace_time = data.misfire_grace_time if data and hasattr(data, 'misfire_grace_time') else None
478
+ _misfire_grace_time = self.__getAttribute(data, 'misfire_grace_time', None)
358
479
 
359
480
  # Extract the job max_instances if available
360
- _max_instances = data.max_instances if data and hasattr(data, 'max_instances') else 0
481
+ _max_instances = self.__getAttribute(data, 'max_instances', 0)
361
482
 
362
483
  # Extract the job coalesce if available
363
- _coalesce = data.coalesce if data and hasattr(data, 'coalesce') else False
484
+ _coalesce = self.__getAttribute(data, 'coalesce', False)
364
485
 
365
486
  # Extract the job next_run_time if available
366
- _next_run_time = data.next_run_time if data and hasattr(data, 'next_run_time') else None
487
+ _next_run_time = self.__getAttribute(data, 'next_run_time', None)
367
488
 
368
489
  # Extract additional event data if available
369
490
  _purpose = event_data.get('purpose', None)
@@ -380,7 +501,7 @@ class Schedule(ISchedule):
380
501
  # Create and return a Job entity based on the retrieved job data
381
502
  return EventJob(
382
503
  id=_id,
383
- code=code if code is not None else 0,
504
+ code=_code,
384
505
  name=_name,
385
506
  func=_func,
386
507
  args=_args,
@@ -404,30 +525,59 @@ class Schedule(ISchedule):
404
525
  self
405
526
  ) -> None:
406
527
  """
407
- Subscribe to scheduler events for monitoring and handling.
528
+ Subscribe internal handlers to APScheduler events for monitoring and control.
408
529
 
409
- This method sets up event listeners for the AsyncIOScheduler instance to monitor
410
- various scheduler events such as scheduler start, shutdown, pause, resume, job submission,
411
- execution, missed jobs, and errors. Each listener is associated with a specific event type
412
- and is responsible for handling the corresponding event.
530
+ This method attaches internal listener methods to the AsyncIOScheduler instance for a variety of
531
+ scheduler and job-related events. These listeners enable the scheduler to respond to lifecycle
532
+ changes (such as start, shutdown, pause, and resume) and job execution events (such as submission,
533
+ execution, errors, missed runs, max instance violations, and removals).
413
534
 
414
- The listeners log relevant information, invoke registered callbacks, and handle errors
415
- or missed jobs as needed. This ensures that the scheduler's state and job execution
416
- are monitored effectively.
535
+ Each listener is responsible for logging, invoking registered callbacks, and handling errors or
536
+ missed jobs as appropriate. This setup ensures that the scheduler's state and job execution
537
+ are monitored and managed effectively.
417
538
 
418
539
  Returns
419
540
  -------
420
541
  None
421
- This method does not return any value. It configures event listeners on the scheduler.
542
+ This method does not return any value. It registers event listeners on the scheduler instance.
543
+
544
+ Notes
545
+ -----
546
+ The listeners are attached to the following APScheduler events:
547
+ - Scheduler started
548
+ - Scheduler shutdown
549
+ - Job error
550
+ - Job submitted
551
+ - Job executed
552
+ - Job missed
553
+ - Job max instances exceeded
554
+ - Job removed
555
+
556
+ These listeners enable the scheduler to handle and react to all critical scheduling events.
422
557
  """
423
558
 
559
+ # Register listener for when the scheduler starts
424
560
  self.__scheduler.add_listener(self.__startedListener, EVENT_SCHEDULER_STARTED)
561
+
562
+ # Register listener for when the scheduler shuts down
425
563
  self.__scheduler.add_listener(self.__shutdownListener, EVENT_SCHEDULER_SHUTDOWN)
564
+
565
+ # Register listener for job execution errors
426
566
  self.__scheduler.add_listener(self.__errorListener, EVENT_JOB_ERROR)
567
+
568
+ # Register listener for when a job is submitted to its executor
427
569
  self.__scheduler.add_listener(self.__submittedListener, EVENT_JOB_SUBMITTED)
570
+
571
+ # Register listener for when a job has finished executing
428
572
  self.__scheduler.add_listener(self.__executedListener, EVENT_JOB_EXECUTED)
573
+
574
+ # Register listener for when a job is missed (not run at its scheduled time)
429
575
  self.__scheduler.add_listener(self.__missedListener, EVENT_JOB_MISSED)
576
+
577
+ # Register listener for when a job exceeds its maximum allowed concurrent instances
430
578
  self.__scheduler.add_listener(self.__maxInstancesListener, EVENT_JOB_MAX_INSTANCES)
579
+
580
+ # Register listener for when a job is removed from the scheduler
431
581
  self.__scheduler.add_listener(self.__removedListener, EVENT_JOB_REMOVED)
432
582
 
433
583
  def __globalCallableListener(
@@ -438,50 +588,56 @@ class Schedule(ISchedule):
438
588
  """
439
589
  Invoke registered listeners for global scheduler events.
440
590
 
441
- This method handles global scheduler events such as when the scheduler starts, pauses, resumes,
442
- or shuts down. It checks if a listener is registered for the specified event and invokes it if callable.
443
- The listener can be either a coroutine or a regular function.
591
+ This method is responsible for handling global scheduler events such as when the scheduler starts, pauses, resumes,
592
+ shuts down, or encounters an error. It checks if a listener is registered for the specified event type and, if so,
593
+ invokes the listener with the provided event data and the current scheduler instance. The listener can be either a
594
+ coroutine or a regular callable.
444
595
 
445
596
  Parameters
446
597
  ----------
447
- event_data : Optional[Union[SchedulerStarted, SchedulerPaused, SchedulerResumed, SchedulerShutdown, ...]]
448
- The event data associated with the global scheduler event. This can include details about the event,
449
- such as its type and context. If no specific data is available, this parameter can be None.
598
+ event_data : Optional[Union[SchedulerStarted, SchedulerPaused, SchedulerResumed, SchedulerShutdown, SchedulerError]]
599
+ The event data associated with the global scheduler event. This may include details about the event,
600
+ such as its type, code, time, or context. If no specific data is available, this parameter can be None.
450
601
  listening_vent : ListeningEvent
451
602
  An instance of the ListeningEvent enum representing the global scheduler event to handle.
452
603
 
453
604
  Returns
454
605
  -------
455
606
  None
456
- This method does not return any value. It invokes the registered listener for the specified event,
457
- if one exists.
607
+ This method does not return any value. It executes the registered listener for the specified event if one exists.
458
608
 
459
609
  Raises
460
610
  ------
461
611
  CLIOrionisValueError
462
612
  If the provided `listening_vent` is not an instance of ListeningEvent.
613
+
614
+ Notes
615
+ -----
616
+ This method is intended for internal use to centralize the invocation of global event listeners.
617
+ It ensures that only valid event types are processed and that exceptions raised by listeners are
618
+ properly handled through the application's error handling mechanism.
463
619
  """
464
620
 
465
- # Validate that the provided event is an instance of ListeningEvent
621
+ # Ensure the provided event is a valid ListeningEvent instance
466
622
  if not isinstance(listening_vent, ListeningEvent):
467
623
  self.__raiseException(CLIOrionisValueError("The event must be an instance of ListeningEvent."))
468
624
 
469
- # Retrieve the global identifier for the event from the ListeningEvent enum
625
+ # Retrieve the string identifier for the event from the ListeningEvent enum
470
626
  scheduler_event = listening_vent.value
471
627
 
472
- # Check if a listener is registered for the specified event
628
+ # Check if a listener is registered for this global event
473
629
  if scheduler_event in self.__listeners:
474
630
 
475
- # Get the listener for the specified event
631
+ # Retrieve the listener callable for the event
476
632
  listener = self.__listeners[scheduler_event]
477
633
 
478
- # If is Callable
634
+ # If the listener is callable, invoke it with event data and the scheduler instance
479
635
  if callable(listener):
480
-
481
- # Invoke the listener, handling both coroutine and regular functions
482
636
  try:
637
+ # Execute the listener, handling both coroutine and regular functions
483
638
  self.__app.invoke(listener, event_data, self)
484
639
  except BaseException as e:
640
+ # Handle any exceptions raised by the listener using the application's error handler
485
641
  self.__raiseException(e)
486
642
 
487
643
  def __taskCallableListener(
@@ -492,10 +648,11 @@ class Schedule(ISchedule):
492
648
  """
493
649
  Invoke registered listeners for specific task/job events.
494
650
 
495
- This method handles task/job-specific events such as job errors, executions, submissions,
496
- missed jobs, and max instance violations. It checks if a listener is registered for the
497
- specific job ID associated with the event and invokes the appropriate method on the listener
498
- if callable. The listener can be either a coroutine or a regular function.
651
+ This method is responsible for handling task/job-specific events such as job errors,
652
+ executions, submissions, missed jobs, and max instance violations. It checks if a
653
+ listener is registered for the specific job ID associated with the event and, if so,
654
+ invokes the appropriate method on the listener. The listener can be either a class
655
+ implementing `IScheduleEventListener` or a callable.
499
656
 
500
657
  Parameters
501
658
  ----------
@@ -504,7 +661,7 @@ class Schedule(ISchedule):
504
661
  such as its ID, exception (if any), and other context. If no specific data is available,
505
662
  this parameter can be None.
506
663
  listening_vent : ListeningEvent
507
- An instance of the ListeningEvent enum representing the task/job event to handle.
664
+ An instance of the `ListeningEvent` enum representing the task/job event to handle.
508
665
 
509
666
  Returns
510
667
  -------
@@ -515,48 +672,56 @@ class Schedule(ISchedule):
515
672
  Raises
516
673
  ------
517
674
  CLIOrionisValueError
518
- If the provided `listening_vent` is not an instance of ListeningEvent.
675
+ If the provided `listening_vent` is not an instance of `ListeningEvent`.
676
+ If the listener for the job ID is not a subclass of `IScheduleEventListener`.
677
+
678
+ Notes
679
+ -----
680
+ This method is intended for internal use to centralize the invocation of task/job event listeners.
681
+ It ensures that only valid event types and listeners are processed, and that exceptions raised
682
+ by listeners are properly handled through the application's error handling mechanism.
519
683
  """
520
684
 
521
- # Validate that the provided event is an instance of ListeningEvent
685
+ # Ensure the provided event is a valid ListeningEvent instance
522
686
  if not isinstance(listening_vent, ListeningEvent):
523
687
  self.__raiseException(CLIOrionisValueError("The event must be an instance of ListeningEvent."))
524
688
 
525
- # Validate that event_data is not None and has a id attribute
689
+ # Validate that event_data is a valid EventJob with a non-empty id
526
690
  if not isinstance(event_data, EventJob) or not hasattr(event_data, 'id') or not event_data.id:
527
691
  return
528
692
 
529
- # Retrieve the global identifier for the event from the ListeningEvent enum
693
+ # Retrieve the string identifier for the event from the ListeningEvent enum
530
694
  scheduler_event = listening_vent.value
531
695
 
532
- # Check if a listener is registered for the specific job ID in the event data
696
+ # Check if a listener is registered for this specific job ID
533
697
  if event_data.id in self.__listeners:
534
698
 
535
699
  # Retrieve the listener for the specific job ID
536
700
  listener = self.__listeners[event_data.id]
537
701
 
538
- # Check if the listener is an instance of IScheduleEventListener
702
+ # If the listener is a subclass of IScheduleEventListener, invoke the appropriate method
539
703
  if issubclass(listener, IScheduleEventListener):
540
-
541
704
  try:
542
-
543
- # Initialize the listener if it's a class
705
+ # If the listener is a class, instantiate it using the application container
544
706
  if isinstance(listener, type):
545
707
  listener = self.__app.make(listener)
546
708
 
547
- # Check if the listener has a method corresponding to the event type
709
+ # Check if the listener has a method corresponding to the event type and is callable
548
710
  if hasattr(listener, scheduler_event) and callable(getattr(listener, scheduler_event)):
711
+ # Call the event method on the listener, passing event data and the scheduler instance
549
712
  self.__app.call(listener, scheduler_event, event_data, self)
550
713
 
551
714
  except BaseException as e:
552
-
553
- # If an error occurs while invoking the listener, raise an exception
715
+ # Handle any exceptions raised by the listener using the application's error handler
554
716
  self.__raiseException(e)
555
717
 
556
718
  else:
557
-
558
719
  # If the listener is not a subclass of IScheduleEventListener, raise an exception
559
- self.__raiseException(CLIOrionisValueError(f"The listener for job ID '{event_data.id}' must be a subclass of IScheduleEventListener."))
720
+ self.__raiseException(
721
+ CLIOrionisValueError(
722
+ f"The listener for job ID '{event_data.id}' must be a subclass of IScheduleEventListener."
723
+ )
724
+ )
560
725
 
561
726
  def __startedListener(
562
727
  self,
@@ -609,16 +774,15 @@ class Schedule(ISchedule):
609
774
 
610
775
  # Check if a listener is registered for the scheduler started event
611
776
  event_data = SchedulerStarted(
612
- code=event.code if hasattr(event, 'code') else 0,
613
- time=now,
614
- tasks=self.events()
777
+ code=self.__getAttribute(event, 'code', 0),
778
+ time=self.__getNow()
615
779
  )
616
780
 
617
781
  # If a listener is registered for this event, invoke the listener with the event details
618
782
  self.__globalCallableListener(event_data, ListeningEvent.SCHEDULER_STARTED)
619
783
 
620
784
  # Log an informational message indicating that the scheduler has started
621
- self.__logger.info(f"Orionis Scheduler started successfully at {now}.")
785
+ self.__logger.info(f"Orionis Scheduler started successfully at: {now}.")
622
786
 
623
787
  def __shutdownListener(
624
788
  self,
@@ -650,10 +814,11 @@ class Schedule(ISchedule):
650
814
 
651
815
  # Check if a listener is registered for the scheduler shutdown event
652
816
  event_data = SchedulerShutdown(
653
- code=event.code if hasattr(event, 'code') else 0,
654
- time=now,
655
- tasks=self.events()
817
+ code=self.__getAttribute(event, 'code', 0),
818
+ time=self.__getNow()
656
819
  )
820
+
821
+ # If a listener is registered for this event, invoke the listener with the event details
657
822
  self.__globalCallableListener(event_data, ListeningEvent.SCHEDULER_SHUTDOWN)
658
823
 
659
824
  # Log an informational message indicating that the scheduler has shut down
@@ -664,78 +829,105 @@ class Schedule(ISchedule):
664
829
  event
665
830
  ) -> None:
666
831
  """
667
- Handle job error events for logging and error reporting.
832
+ Handle job error events for logging, error reporting, and listener invocation.
668
833
 
669
- This method is triggered when a job execution results in an error. It logs an error
670
- message indicating the job ID and the exception raised. If the application is in
671
- debug mode, it also reports the error using the error reporter. Additionally, if a
672
- listener is registered for the errored job, it invokes the listener with the event details.
834
+ This method is triggered when a scheduled job raises an exception during execution.
835
+ It logs an error message with the job ID and exception details, updates the job event
836
+ data with error information, and invokes any registered listeners for both the specific
837
+ job error and the global scheduler error event. The method also delegates exception
838
+ handling to the application's error catching mechanism.
673
839
 
674
840
  Parameters
675
841
  ----------
676
842
  event : JobError
677
- An instance of the JobError event containing details about the errored job,
678
- including its ID and the exception raised.
843
+ An event object containing details about the errored job, including its ID,
844
+ exception, traceback, and event code.
679
845
 
680
846
  Returns
681
847
  -------
682
848
  None
683
849
  This method does not return any value. It performs logging, error reporting,
684
- and listener invocation for the job error event.
850
+ and invokes any registered listeners for the job error and scheduler error events.
851
+
852
+ Notes
853
+ -----
854
+ - The method updates the job event data with the exception and traceback.
855
+ - Both job-specific and global error listeners are invoked if registered.
856
+ - All exceptions are delegated to the application's error handling system.
685
857
  """
858
+
859
+ # Extract job ID, event code, exception, and traceback from the event object
860
+ event_id = self.__getAttribute(event, 'job_id', None)
861
+ event_code = self.__getAttribute(event, 'code', 0)
862
+ event_exception = self.__getAttribute(event, 'exception', None)
863
+ event_traceback = self.__getAttribute(event, 'traceback', None)
864
+
686
865
  # Log an error message indicating that the job raised an exception
687
- self.__logger.error(f"Task '{event.job_id}' raised an exception: {event.exception}")
866
+ self.__logger.error(f"Task '{event_id}' raised an exception: {event_exception}")
688
867
 
689
- # If a listener is registered for this job ID, invoke the listener with the event details
690
- job_event_data = self.__getTaskFromSchedulerById(event.job_id)
691
- job_event_data.code = event.code if hasattr(event, 'code') else 0
692
- job_event_data.exception = event.exception if hasattr(event, 'exception') else None
693
- job_event_data.traceback = event.traceback if hasattr(event, 'traceback') else None
868
+ # Retrieve the job event data and update it with error details
869
+ job_event_data = self.__getTaskFromSchedulerById(event_id)
870
+ job_event_data.code = event_code
871
+ job_event_data.exception = event_exception
872
+ job_event_data.traceback = event_traceback
694
873
 
695
- # Call the task-specific listener for job errors
874
+ # Invoke the task-specific listener for job errors, if registered
696
875
  self.__taskCallableListener(job_event_data, ListeningEvent.JOB_ON_FAILURE)
697
876
 
698
- # Check if a listener is registered for the scheduler error event
877
+ # Prepare the global scheduler error event data
699
878
  event_data = SchedulerError(
700
- code=event.code if hasattr(event, 'code') else 0,
701
- exception=event.exception if hasattr(event, 'exception') else None,
702
- traceback=event.traceback if hasattr(event, 'traceback') else None,
879
+ code=event_code,
880
+ time=self.__getNow(),
881
+ exception=event_exception,
882
+ traceback=event_traceback
703
883
  )
884
+
885
+ # Invoke the global listener for scheduler errors, if registered
704
886
  self.__globalCallableListener(event_data, ListeningEvent.SCHEDULER_ERROR)
705
887
 
706
- # Catch any exceptions that occur during command handling
707
- self.__raiseException(event.exception)
888
+ # Delegate exception handling to the application's error catching mechanism
889
+ self.__raiseException(event_exception)
708
890
 
709
891
  def __submittedListener(
710
892
  self,
711
893
  event
712
894
  ) -> None:
713
895
  """
714
- Handle job submission events for logging and error reporting.
896
+ Handle job submission events for logging and invoking registered listeners.
715
897
 
716
- This method is triggered when a job is submitted to its executor. It logs an informational
717
- message indicating that the job has been submitted successfully. If the application is in
718
- debug mode, it also displays a message on the console. Additionally, if a listener is
719
- registered for the submitted job, it invokes the listener with the event details.
898
+ This internal method is triggered when a job is submitted to its executor by the scheduler.
899
+ It logs an informational message about the job submission, creates an event entity representing
900
+ the job submission, and invokes any registered listeners for the submitted job. This allows
901
+ for custom pre-execution logic or notifications to be handled externally.
720
902
 
721
903
  Parameters
722
904
  ----------
723
905
  event : JobSubmitted
724
- An instance of the JobSubmitted containing details about the submitted job,
725
- including its ID and scheduled run times.
906
+ An event object containing details about the submitted job, such as its ID and
907
+ any associated event code.
726
908
 
727
909
  Returns
728
910
  -------
729
911
  None
730
- This method does not return any value. It performs logging, error reporting,
731
- and listener invocation for the job submission event.
912
+ This method does not return any value. It performs logging and invokes any
913
+ registered listener for the job submission event.
914
+
915
+ Notes
916
+ -----
917
+ This method is intended for internal use to centralize the handling of job submission
918
+ events. It ensures that job submissions are logged and that any custom listeners
919
+ associated with the job are properly notified.
732
920
  """
733
921
 
734
- # Log an informational message indicating that the job has been submitted
735
- self.__logger.info(f"Task '{event.job_id}' submitted to executor.")
922
+ # Extract job ID and code from the event object, using default values if not present
923
+ event_id = self.__getAttribute(event, 'job_id', None)
924
+ event_code = self.__getAttribute(event, 'code', 0)
736
925
 
737
- # Create entity for job submitted event
738
- data_event = self.__getTaskFromSchedulerById(event.job_id, event.code)
926
+ # Log an informational message indicating that the job has been submitted to the executor
927
+ self.__logger.info(f"Task '{event_id}' submitted to executor.")
928
+
929
+ # Create an event entity for the submitted job, including its ID and code
930
+ data_event = self.__getTaskFromSchedulerById(event_id, event_code)
739
931
 
740
932
  # If a listener is registered for this job ID, invoke the listener with the event details
741
933
  self.__taskCallableListener(data_event, ListeningEvent.JOB_BEFORE)
@@ -745,32 +937,41 @@ class Schedule(ISchedule):
745
937
  event
746
938
  ) -> None:
747
939
  """
748
- Handle job execution events for logging and error reporting.
940
+ Handle job execution events for logging, error reporting, and listener invocation.
749
941
 
750
- This method is triggered when a job is executed by its executor. It logs an informational
751
- message indicating that the job has been executed successfully. If the application is in
752
- debug mode, it also displays a message on the console. If the job execution resulted in
753
- an exception, it logs the error and reports it using the error reporter. Additionally,
754
- if a listener is registered for the executed job, it invokes the listener with the event details.
942
+ This method is triggered when a job has finished executing in the scheduler. It logs an informational
943
+ message indicating the successful execution of the job. If the job execution resulted in an exception,
944
+ the error is logged and reported using the application's error handling system. Additionally, if a
945
+ listener is registered for the executed job, this method invokes the listener with the event details.
755
946
 
756
947
  Parameters
757
948
  ----------
758
949
  event : JobExecuted
759
- An instance of the JobExecuted containing details about the executed job,
760
- including its ID, return value, exception (if any), and traceback.
950
+ An event object containing details about the executed job, including its ID, return value,
951
+ exception (if any), and traceback.
761
952
 
762
953
  Returns
763
954
  -------
764
955
  None
765
- This method does not return any value. It performs logging, error reporting,
766
- and listener invocation for the job execution event.
956
+ This method does not return any value. It performs logging, error reporting, and invokes
957
+ any registered listener for the executed job.
958
+
959
+ Notes
960
+ -----
961
+ This method is intended for internal use to centralize the handling of job execution events.
962
+ It ensures that job executions are logged, errors are reported, and any custom listeners
963
+ associated with the job are properly notified.
767
964
  """
768
965
 
966
+ # Extract the job ID and event code from the event object, using default values if not present
967
+ event_id = self.__getAttribute(event, 'job_id', None)
968
+ event_code = self.__getAttribute(event, 'code', 0)
969
+
769
970
  # Log an informational message indicating that the job has been executed
770
- self.__logger.info(f"Task '{event.job_id}' executed.")
971
+ self.__logger.info(f"Task '{event_id}' executed.")
771
972
 
772
- # Create entity for job executed event
773
- data_event = self.__getTaskFromSchedulerById(event.job_id, event.code)
973
+ # Create an event entity for the executed job, including its ID and code
974
+ data_event = self.__getTaskFromSchedulerById(event_id, event_code)
774
975
 
775
976
  # If a listener is registered for this job ID, invoke the listener with the event details
776
977
  self.__taskCallableListener(data_event, ListeningEvent.JOB_AFTER)
@@ -780,32 +981,46 @@ class Schedule(ISchedule):
780
981
  event
781
982
  ) -> None:
782
983
  """
783
- Handle job missed events for debugging and error reporting.
984
+ Handle job missed events for logging, reporting, and invoking registered listeners.
784
985
 
785
- This method is triggered when a scheduled job is missed. It logs a warning
786
- message indicating the missed job and its scheduled run time. If the application
787
- is in debug mode, it reports the missed job using the error reporter. Additionally,
788
- if a listener is registered for the missed job, it invokes the listener with the
789
- event details.
986
+ This method is triggered when a scheduled job is missed (i.e., it was not executed at its scheduled time)
987
+ by the scheduler. It logs a warning message indicating the missed job and its scheduled run time, creates
988
+ an event entity for the missed job, and invokes any registered listeners for the missed job event. This
989
+ ensures that missed executions are tracked and that any custom logic associated with missed jobs is executed.
790
990
 
791
991
  Parameters
792
992
  ----------
793
993
  event : JobMissed
794
- An instance of the JobMissed event containing details about the missed job,
795
- including its ID and scheduled run time.
994
+ An event object containing details about the missed job, including its ID (`job_id`), event code (`code`),
995
+ and the scheduled run time (`scheduled_run_time`).
796
996
 
797
997
  Returns
798
998
  -------
799
999
  None
800
- This method does not return any value. It performs logging, error reporting,
801
- and listener invocation for the missed job event.
1000
+ This method does not return any value. It performs logging, reporting, and invokes any registered
1001
+ listener for the missed job event.
1002
+
1003
+ Notes
1004
+ -----
1005
+ - This method is intended for internal use to centralize the handling of missed job events.
1006
+ - It ensures that missed jobs are logged and that any custom listeners associated with the job are properly notified.
1007
+ - The event entity created provides structured information to listeners for further processing.
802
1008
  """
803
1009
 
804
- # Log a warning indicating that the job was missed
805
- self.__logger.warning(f"Task '{event.job_id}' was missed. It was scheduled to run at {event.scheduled_run_time}.")
1010
+ # Extract the job ID from the event object, or None if not present
1011
+ event_id = self.__getAttribute(event, 'job_id', None)
1012
+ # Extract the event code from the event object, defaulting to 0 if not present
1013
+ event_code = self.__getAttribute(event, 'code', 0)
1014
+ # Extract the scheduled run time from the event object, or 'Unknown' if not present
1015
+ event_scheduled_run_time = self.__getAttribute(event, 'scheduled_run_time', 'Unknown')
1016
+
1017
+ # Log a warning indicating that the job was missed and when it was scheduled to run
1018
+ self.__logger.warning(
1019
+ f"Task '{event_id}' was missed. It was scheduled to run at: {event_scheduled_run_time}."
1020
+ )
806
1021
 
807
- # Create entity for job missed event
808
- data_event = self.__getTaskFromSchedulerById(event.job_id, event.code)
1022
+ # Create an event entity for the missed job, including its ID and code
1023
+ data_event = self.__getTaskFromSchedulerById(event_id, event_code)
809
1024
 
810
1025
  # If a listener is registered for this job ID, invoke the listener with the event details
811
1026
  self.__taskCallableListener(data_event, ListeningEvent.JOB_ON_MISSED)
@@ -815,32 +1030,42 @@ class Schedule(ISchedule):
815
1030
  event
816
1031
  ) -> None:
817
1032
  """
818
- Handle job max instances events for logging and error reporting.
1033
+ Handle job max instances events for logging, error reporting, and listener invocation.
819
1034
 
820
- This method is triggered when a job execution exceeds the maximum allowed
821
- concurrent instances. It logs an error message indicating the job ID and
822
- the exception raised. If the application is in debug mode, it also reports
823
- the error using the error reporter. Additionally, if a listener is registered
824
- for the job that exceeded max instances, it invokes the listener with the event details.
1035
+ This method is triggered when a job execution exceeds the maximum allowed concurrent instances.
1036
+ It logs an error message indicating the job ID that exceeded its instance limit, creates an event
1037
+ entity for the affected job, and invokes any registered listener for this job's max instances event.
1038
+ This allows for custom handling, notification, or recovery logic to be executed in response to
1039
+ the max instances violation.
825
1040
 
826
1041
  Parameters
827
1042
  ----------
828
1043
  event : JobMaxInstances
829
- An instance of the JobMaxInstances event containing details about the job that
830
- exceeded max instances, including its ID and the exception raised.
1044
+ An event object containing details about the job that exceeded the maximum allowed instances,
1045
+ including its job ID and event code.
831
1046
 
832
1047
  Returns
833
1048
  -------
834
1049
  None
835
- This method does not return any value. It performs logging, error reporting,
836
- and listener invocation for the job max instances event.
1050
+ This method does not return any value. It performs logging, error reporting, and invokes
1051
+ any registered listener for the job max instances event.
1052
+
1053
+ Notes
1054
+ -----
1055
+ This method is intended for internal use to centralize the handling of job max instances events.
1056
+ It ensures that such events are logged and that any custom listeners associated with the job
1057
+ are properly notified. No value is returned.
837
1058
  """
838
1059
 
839
- # Log an error message indicating that the job exceeded maximum instances
840
- self.__logger.error(f"Task '{event.job_id}' exceeded maximum instances")
1060
+ # Extract the job ID and event code from the event object, using default values if not present
1061
+ event_id = self.__getAttribute(event, 'job_id', None)
1062
+ event_code = self.__getAttribute(event, 'code', 0)
1063
+
1064
+ # Log an error message indicating that the job exceeded maximum concurrent instances
1065
+ self.__logger.error(f"Task '{event_id}' exceeded maximum instances")
841
1066
 
842
- # Create entity for job max instances event
843
- data_event = self.__getTaskFromSchedulerById(event.job_id, event.code)
1067
+ # Create an event entity for the job that exceeded max instances
1068
+ data_event = self.__getTaskFromSchedulerById(event_id, event_code)
844
1069
 
845
1070
  # If a listener is registered for this job ID, invoke the listener with the event details
846
1071
  self.__taskCallableListener(data_event, ListeningEvent.JOB_ON_MAXINSTANCES)
@@ -853,28 +1078,37 @@ class Schedule(ISchedule):
853
1078
  Handle job removal events for logging and invoking registered listeners.
854
1079
 
855
1080
  This method is triggered when a job is removed from the scheduler. It logs an informational
856
- message indicating that the job has been removed successfully. If the application is in debug
857
- mode, it displays a message on the console. Additionally, if a listener is registered for the
858
- removed job, it invokes the listener with the event details.
1081
+ message indicating that the job has been removed. If the application is in debug mode, it
1082
+ may display a message on the console. Additionally, if a listener is registered for the
1083
+ removed job, this method invokes the listener with the event details.
859
1084
 
860
1085
  Parameters
861
1086
  ----------
862
1087
  event : JobRemoved
863
- An instance of the JobRemoved event containing details about the removed job,
864
- including its ID and other relevant information.
1088
+ An event object containing details about the removed job, such as its ID and event code.
865
1089
 
866
1090
  Returns
867
1091
  -------
868
1092
  None
869
1093
  This method does not return any value. It performs logging and invokes any registered
870
1094
  listener for the job removal event.
1095
+
1096
+ Notes
1097
+ -----
1098
+ - The method retrieves the job ID and event code from the event object.
1099
+ - It logs the removal of the job and creates an event entity for the removed job.
1100
+ - If a listener is registered for the job, it is invoked with the event details.
871
1101
  """
872
1102
 
1103
+ # Retrieve the job ID and event code from the event object
1104
+ event_id = self.__getAttribute(event, 'job_id', None)
1105
+ event_code = self.__getAttribute(event, 'code', 0)
1106
+
873
1107
  # Log the removal of the job
874
- self.__logger.info(f"Task '{event.job_id}' has been removed.")
1108
+ self.__logger.info(f"Task '{event_id}' has been removed.")
875
1109
 
876
- # Create entity for job removed event
877
- data_event = self.__getTaskFromSchedulerById(event.job_id, event.code)
1110
+ # Create an event entity for the removed job
1111
+ data_event = self.__getTaskFromSchedulerById(event_id, event_code)
878
1112
 
879
1113
  # If a listener is registered for this job ID, invoke the listener with the event details
880
1114
  self.__taskCallableListener(data_event, ListeningEvent.JOB_ON_REMOVED)
@@ -883,36 +1117,64 @@ class Schedule(ISchedule):
883
1117
  self
884
1118
  ) -> None:
885
1119
  """
886
- Load all scheduled events from the AsyncIOScheduler into the internal jobs dictionary.
1120
+ Load all scheduled events from the AsyncIOScheduler into the internal jobs list.
1121
+
1122
+ This method synchronizes the internal jobs list (`self.__jobs`) with the events currently
1123
+ registered in the AsyncIOScheduler. For each event in the internal events dictionary,
1124
+ it converts the event to its entity representation, adds it to the jobs list, and schedules
1125
+ it in the AsyncIOScheduler with the appropriate configuration. If a listener is associated
1126
+ with the event, it is also registered. This ensures that all scheduled jobs are properly
1127
+ tracked and managed by both the internal state and the scheduler.
887
1128
 
888
- This method retrieves all jobs currently managed by the AsyncIOScheduler and populates
889
- the internal jobs dictionary with their details, including signature, arguments, purpose,
890
- type, trigger, start date, and end date.
1129
+ Parameters
1130
+ ----------
1131
+ None
891
1132
 
892
1133
  Returns
893
1134
  -------
894
1135
  None
895
- This method does not return any value. It updates the internal jobs dictionary.
1136
+ This method does not return any value. It updates the internal jobs list and
1137
+ registers jobs with the AsyncIOScheduler.
1138
+
1139
+ Raises
1140
+ ------
1141
+ CLIOrionisRuntimeError
1142
+ If an error occurs while loading or scheduling an event, a runtime error is raised
1143
+ with a descriptive message.
1144
+
1145
+ Notes
1146
+ -----
1147
+ - Events are only loaded if the internal jobs list is empty.
1148
+ - Each event must implement a `toEntity` method to be converted to an entity.
1149
+ - Jobs are added to the scheduler with their respective configuration, and listeners
1150
+ are registered if present.
896
1151
  """
897
1152
 
898
- # Only load events if the jobs list is empty
1153
+ # Only load events if the jobs list is empty to avoid duplicate scheduling
899
1154
  if not self.__jobs:
900
1155
 
901
- # Iterate through all scheduled jobs in the AsyncIOScheduler
1156
+ # Iterate through all scheduled events in the internal events dictionary
902
1157
  for signature, event in self.__events.items():
903
1158
 
904
1159
  try:
905
- # Convert the event to its entity representation
906
- entity: EventEntity = event.toEntity()
907
1160
 
908
- # Add the job to the internal jobs list
1161
+ # Ensure the event has a toEntity method for conversion
1162
+ if not hasattr(event, 'toEntity'):
1163
+ continue
1164
+
1165
+ # Convert the event to its entity representation (EventEntity)
1166
+ to_entity = getattr(event, 'toEntity')
1167
+ entity: EventEntity = to_entity()
1168
+
1169
+ # Add the job entity to the internal jobs list
909
1170
  self.__jobs.append(entity)
910
1171
 
911
- # Create a unique key for the job based on its signature
1172
+ # Helper function to create a job function that calls the reactor
912
1173
  def create_job_func(cmd, args_list):
1174
+ # Returns a lambda that will call the command with its arguments
913
1175
  return lambda: self.__reactor.call(cmd, args_list)
914
1176
 
915
- # Add the job to the scheduler with the specified trigger and parameters
1177
+ # Add the job to the AsyncIOScheduler with the specified configuration
916
1178
  self.__scheduler.add_job(
917
1179
  func=create_job_func(signature, list(entity.args)),
918
1180
  trigger=entity.trigger,
@@ -923,22 +1185,21 @@ class Schedule(ISchedule):
923
1185
  misfire_grace_time=entity.misfire_grace_time
924
1186
  )
925
1187
 
926
- # If a listener is associated with the event, register it
1188
+ # If the event entity has an associated listener, register it
927
1189
  if entity.listener:
928
1190
  self.setListener(signature, entity.listener)
929
1191
 
930
- # Log the successful loading of the scheduled event
1192
+ # Log the successful loading of the scheduled event for debugging
931
1193
  self.__logger.debug(f"Scheduled event '{signature}' loaded successfully.")
932
1194
 
933
1195
  except Exception as e:
934
-
935
- # Construct the error message
1196
+ # Construct an error message for failed event loading
936
1197
  error_msg = f"Failed to load scheduled event '{signature}': {str(e)}"
937
1198
 
938
1199
  # Log the error message
939
1200
  self.__logger.error(error_msg)
940
1201
 
941
- # Raise a runtime error if loading the scheduled event fails
1202
+ # Raise a runtime error to signal failure in loading the scheduled event
942
1203
  raise CLIOrionisRuntimeError(error_msg)
943
1204
 
944
1205
  def __raiseException(
@@ -946,42 +1207,43 @@ class Schedule(ISchedule):
946
1207
  exception: BaseException
947
1208
  ) -> None:
948
1209
  """
949
- Handle and propagate exceptions through the application's error handling system.
950
-
951
- This private method serves as a centralized exception handler for the scheduler,
952
- delegating exception processing to the application's error catching mechanism.
953
- It ensures that all exceptions occurring within the scheduler context are
954
- properly handled according to the application's error handling policies.
1210
+ Centralized exception handling for the scheduler, delegating to the application's error catching system.
955
1211
 
956
- The method acts as a bridge between the scheduler's internal operations and
957
- the application's global exception handling system, providing consistent
958
- error handling behavior across the entire application.
1212
+ This private method provides a unified mechanism for handling exceptions that occur within the scheduler's
1213
+ context. It forwards the exception to the application's error catching mechanism, ensuring consistent
1214
+ processing of errors according to the application's global error handling policies. This may include
1215
+ logging, reporting, or re-raising the exception, depending on the application's configuration.
959
1216
 
960
1217
  Parameters
961
1218
  ----------
962
1219
  exception : BaseException
963
- The exception instance that was raised during command execution. This can be
964
- any type of exception that inherits from BaseException, including system
965
- exceptions, custom application exceptions, and runtime errors.
1220
+ The exception instance raised during scheduler or command execution. This can be any subclass of
1221
+ BaseException, including system, runtime, or custom exceptions.
966
1222
 
967
1223
  Returns
968
1224
  -------
969
1225
  None
970
- This method does not return any value. It delegates exception handling
971
- to the application's error catching mechanism and may re-raise the
972
- exception depending on the configured error handling behavior.
1226
+ This method always returns None. It delegates the exception to the application's error
1227
+ handling system for further processing.
973
1228
 
974
1229
  Notes
975
1230
  -----
976
- This method is intended for internal use within the scheduler and should not
977
- be called directly by external code. The error catching mechanism may perform
978
- various actions such as logging, reporting, or re-raising the exception based
979
- on the application's configuration.
1231
+ This method is intended for internal use within the scheduler. The actual handling of the exception
1232
+ (such as logging or propagation) is determined by the application's error catching implementation.
980
1233
  """
981
1234
 
982
- # Delegate exception handling to the application's error catching mechanism
983
- # This ensures consistent error handling across the entire application
984
- self.__catch.exception(self, CLIRequest(command="schedule:work", args={}), exception)
1235
+ # Create a CLIRequest object representing the current scheduler context
1236
+ request = CLIRequest(
1237
+ command="schedule:work",
1238
+ args={}
1239
+ )
1240
+
1241
+ # Delegate the exception to the application's error catching system
1242
+ self.__catch.exception(
1243
+ self,
1244
+ request,
1245
+ exception
1246
+ )
985
1247
 
986
1248
  def setListener(
987
1249
  self,
@@ -989,36 +1251,44 @@ class Schedule(ISchedule):
989
1251
  listener: Union[IScheduleEventListener, callable]
990
1252
  ) -> None:
991
1253
  """
992
- Register a listener callback for a specific scheduler event.
1254
+ Register a listener for a specific scheduler event or job.
993
1255
 
994
- This method registers a listener function or an instance of IScheduleEventListener
995
- to be invoked when the specified scheduler event occurs. The event can be a global
996
- event name (e.g., 'scheduler_started') or a specific job ID. The listener must be
997
- callable and should accept the event object as a parameter.
1256
+ This method allows you to associate a callback or an instance of
1257
+ `IScheduleEventListener` with a scheduler event. The event can be a global
1258
+ scheduler event (such as 'scheduler_started', 'scheduler_paused', etc.) or
1259
+ a specific job ID. When the specified event occurs, the registered listener
1260
+ will be invoked with the event data.
998
1261
 
999
1262
  Parameters
1000
1263
  ----------
1001
- event : str
1002
- The name of the event to listen for. This can be a global event name (e.g., 'scheduler_started')
1003
- or a specific job ID.
1264
+ event : str or ListeningEvent
1265
+ The name of the event to listen for. This can be a string representing
1266
+ a global event name (e.g., 'scheduler_started') or a job ID, or an
1267
+ instance of `ListeningEvent`.
1004
1268
  listener : IScheduleEventListener or callable
1005
- A callable function or an instance of IScheduleEventListener that will be invoked
1006
- when the specified event occurs. The listener should accept one parameter, which
1007
- will be the event object.
1269
+ The listener to be registered. This can be a callable function or an
1270
+ instance of `IScheduleEventListener`. The listener should accept the
1271
+ event object as its parameter.
1008
1272
 
1009
1273
  Returns
1010
1274
  -------
1011
1275
  None
1012
- This method does not return any value. It registers the listener for the specified event.
1276
+ This method does not return any value. It registers the listener for
1277
+ the specified event.
1013
1278
 
1014
1279
  Raises
1015
1280
  ------
1016
1281
  CLIOrionisValueError
1017
- If the event name is not a non-empty string or if the listener is not callable
1018
- or an instance of IScheduleEventListener.
1282
+ If the event name is not a non-empty string, or if the listener is not
1283
+ callable or an instance of `IScheduleEventListener`.
1284
+
1285
+ Notes
1286
+ -----
1287
+ - If the event parameter is a `ListeningEvent`, its value is used as the event name.
1288
+ - The listener will be stored internally and invoked when the corresponding event occurs.
1019
1289
  """
1020
1290
 
1021
- # If the event is an instance of ListeningEvent, extract its value
1291
+ # If the event is an instance of ListeningEvent, extract its string value
1022
1292
  if isinstance(event, ListeningEvent):
1023
1293
  event = event.value
1024
1294
 
@@ -1028,428 +1298,167 @@ class Schedule(ISchedule):
1028
1298
 
1029
1299
  # Validate that the listener is either callable or an instance of IScheduleEventListener
1030
1300
  if not callable(listener) and not isinstance(listener, IScheduleEventListener):
1031
- raise CLIOrionisValueError("Listener must be a callable function or an instance of IScheduleEventListener.")
1301
+ raise CLIOrionisValueError(
1302
+ "Listener must be a callable function or an instance of IScheduleEventListener."
1303
+ )
1032
1304
 
1033
1305
  # Register the listener for the specified event in the internal listeners dictionary
1034
1306
  self.__listeners[event] = listener
1035
1307
 
1036
- def wrapAsyncFunction(
1037
- self,
1038
- func: Callable[..., Awaitable[Any]]
1039
- ) -> Callable[..., Any]:
1040
- """
1041
- Wrap an asynchronous function to be executed in a synchronous context.
1042
-
1043
- This method creates a synchronous wrapper around an asynchronous function (coroutine)
1044
- that enables its execution within non-async contexts. The wrapper leverages the
1045
- Coroutine utility class to handle the complexities of asyncio event loop management
1046
- and provides proper error handling with detailed logging and custom exception
1047
- propagation.
1048
-
1049
- The wrapper is particularly useful when integrating asynchronous functions with
1050
- synchronous APIs or frameworks that do not natively support async operations,
1051
- such as the APScheduler job execution environment.
1052
-
1053
- Parameters
1054
- ----------
1055
- func : Callable[..., Awaitable[Any]]
1056
- The asynchronous function (coroutine) to be wrapped. This function must be
1057
- defined using the `async def` syntax and return an awaitable object. The
1058
- function can accept any number of positional and keyword arguments.
1059
-
1060
- Returns
1061
- -------
1062
- Callable[..., Any]
1063
- A synchronous wrapper function that executes the original asynchronous
1064
- function and returns its result. The wrapper function accepts the same
1065
- arguments as the original async function and forwards them appropriately.
1066
- The return type depends on what the wrapped asynchronous function returns.
1067
-
1068
- Raises
1069
- ------
1070
- CLIOrionisRuntimeError
1071
- If the asynchronous function execution fails or if the provided `func`
1072
- parameter is not a valid asynchronous function. The original exception
1073
- is wrapped to provide additional context for debugging.
1074
-
1075
- Notes
1076
- -----
1077
- This method relies on the Coroutine utility class from the orionis.services.asynchrony
1078
- module to handle the execution of the wrapped asynchronous function. The wrapper
1079
- uses the instance logger for comprehensive error reporting and debugging information.
1080
- """
1081
-
1082
- def sync_wrapper(*args, **kwargs) -> Any:
1083
- """
1084
- Synchronous wrapper function that executes asynchronous functions in a thread-safe manner.
1085
-
1086
- This wrapper provides a uniform interface for executing asynchronous functions within
1087
- synchronous contexts by leveraging the Coroutine utility class. It handles the complexity
1088
- of managing async/await patterns and provides proper error handling with detailed logging
1089
- and custom exception propagation.
1090
-
1091
- The wrapper is particularly useful when integrating asynchronous functions with
1092
- synchronous APIs or frameworks that do not natively support async operations.
1093
-
1094
- Parameters
1095
- ----------
1096
- *args : tuple
1097
- Variable length argument list to pass to the wrapped asynchronous function.
1098
- These arguments are forwarded directly to the original function.
1099
- **kwargs : dict
1100
- Arbitrary keyword arguments to pass to the wrapped asynchronous function.
1101
- These keyword arguments are forwarded directly to the original function.
1102
-
1103
- Returns
1104
- -------
1105
- Any
1106
- The return value from the executed asynchronous function. The type depends
1107
- on what the wrapped function returns and can be any Python object.
1108
-
1109
- Raises
1110
- ------
1111
- CLIOrionisRuntimeError
1112
- When the asynchronous function execution fails, wrapping the original
1113
- exception with additional context and error details for better debugging.
1114
-
1115
- Notes
1116
- -----
1117
- This function relies on the Coroutine utility class to handle the execution
1118
- of the wrapped asynchronous function and uses the instance logger for comprehensive
1119
- error reporting and debugging information.
1120
- """
1121
-
1122
- # Execute the asynchronous function using the container's invoke method
1123
- try:
1124
- self.__app.invoke(func, *args, **kwargs)
1125
-
1126
- # If an error occurs during execution, raise a custom exception
1127
- except Exception as e:
1128
- self.__raiseException(e)
1129
-
1130
- # Return the synchronous wrapper function
1131
- return sync_wrapper
1132
-
1133
- def pauseEverythingAt(
1134
- self,
1135
- at: datetime
1136
- ) -> None:
1137
- """
1138
- Schedule the scheduler to pause all operations at a specific datetime.
1139
-
1140
- This method schedules a job that pauses the AsyncIOScheduler at the specified datetime.
1141
- The job is added to the scheduler with a 'date' trigger, ensuring it executes exactly
1142
- at the given time. If a pause job already exists, it is replaced to avoid conflicts.
1143
-
1144
- Parameters
1145
- ----------
1146
- at : datetime
1147
- The datetime at which the scheduler should be paused. Must be a valid
1148
- datetime object.
1149
-
1150
- Returns
1151
- -------
1152
- None
1153
- This method does not return any value. It schedules a job to pause the
1154
- scheduler at the specified datetime.
1155
-
1156
- Raises
1157
- ------
1158
- ValueError
1159
- If the 'at' parameter is not a valid datetime object or is not in the future.
1160
- CLIOrionisRuntimeError
1161
- If the scheduler is not running or if an error occurs during job scheduling.
1162
- """
1163
-
1164
- # Validate that the 'at' parameter is a datetime object
1165
- if not isinstance(at, datetime):
1166
- CLIOrionisValueError("The 'at' parameter must be a datetime object.")
1167
-
1168
- # Define an async function to pause the scheduler
1169
- async def schedule_pause():
1170
-
1171
- # Only pause jobs if the scheduler is currently running
1172
- if self.isRunning():
1173
-
1174
- # Clear the set of previously paused jobs
1175
- self.__pausedByPauseEverything.clear()
1176
-
1177
- # Get all jobs from the scheduler
1178
- all_jobs = self.__scheduler.get_jobs()
1179
-
1180
- # Filter out system jobs (pause, resume, shutdown tasks)
1181
- system_job_ids = {
1182
- "scheduler_pause_at",
1183
- "scheduler_resume_at",
1184
- "scheduler_shutdown_at"
1185
- }
1186
-
1187
- # Pause only user jobs, not system jobs
1188
- for job in all_jobs:
1189
-
1190
- # Check if the job is not a system job
1191
- if job.id not in system_job_ids:
1192
-
1193
- try:
1194
-
1195
- # Pause the job in the scheduler
1196
- self.__scheduler.pause_job(job.id)
1197
- self.__pausedByPauseEverything.add(job.id)
1198
-
1199
- # Get the current time in the configured timezone
1200
- now = self.__getCurrentTime()
1201
-
1202
- # Log an informational message indicating that the job has been paused
1203
- self.__taskCallableListener(
1204
- self.__getTaskFromSchedulerById(job.id),
1205
- ListeningEvent.JOB_ON_PAUSED
1206
- )
1207
-
1208
- # Log the pause action
1209
- self.__logger.info(f"Job '{job.id}' paused successfully at {now}.")
1210
-
1211
- except Exception as e:
1212
-
1213
- # If an error occurs while pausing the job, raise an exception
1214
- self.__raiseException(e)
1215
-
1216
- # Execute the global callable listener after all jobs are paused
1217
- self.__globalCallableListener(SchedulerPaused(
1218
- code=EVENT_SCHEDULER_PAUSED,
1219
- time=self.__getCurrentTime()
1220
- ), ListeningEvent.SCHEDULER_PAUSED)
1221
-
1222
- # Log that all user jobs have been paused
1223
- self.__logger.info("All user jobs have been paused. System jobs remain active.")
1224
-
1225
- try:
1226
-
1227
- # Remove any existing pause job to avoid conflicts
1228
- try:
1229
- self.__scheduler.remove_job("scheduler_pause_at")
1230
-
1231
- # If the job doesn't exist, it's fine to proceed
1232
- finally:
1233
- pass
1234
-
1235
- # Add a job to the scheduler to pause it at the specified datetime
1236
- self.__scheduler.add_job(
1237
- func=self.wrapAsyncFunction(schedule_pause), # Function to pause the scheduler
1238
- trigger=DateTrigger(run_date=at), # Trigger type is 'date' for one-time execution
1239
- id="scheduler_pause_at", # Unique job ID for pausing the scheduler
1240
- name="Pause Scheduler", # Descriptive name for the job
1241
- replace_existing=True # Replace any existing job with the same ID
1242
- )
1243
-
1244
- # Log the scheduled pause
1245
- self.__logger.info(f"Scheduler pause scheduled for {at.strftime('%Y-%m-%d %H:%M:%S')}")
1246
-
1247
- except Exception as e:
1248
-
1249
- # Handle exceptions that may occur during job scheduling
1250
- raise CLIOrionisRuntimeError(f"Failed to schedule scheduler pause: {str(e)}") from e
1251
-
1252
- def resumeEverythingAt(
1253
- self,
1254
- at: datetime
1308
+ def pause(
1309
+ self
1255
1310
  ) -> None:
1256
1311
  """
1257
- Schedule the scheduler to resume all operations at a specific datetime.
1312
+ Pause all user jobs managed by the scheduler.
1258
1313
 
1259
- This method schedules a job that resumes the AsyncIOScheduler at the specified datetime.
1260
- The job is added to the scheduler with a 'date' trigger, ensuring it executes exactly
1261
- at the given time. If a resume job already exists, it is replaced to avoid conflicts.
1262
-
1263
- Parameters
1264
- ----------
1265
- at : datetime
1266
- The datetime at which the scheduler should be resumed. Must be a valid
1267
- datetime object.
1314
+ This method pauses all currently scheduled user jobs in the AsyncIOScheduler if the scheduler is running.
1315
+ It iterates through all jobs, attempts to pause each one, and tracks which jobs were paused by adding their
1316
+ IDs to an internal set. For each successfully paused job, the method logs the action and invokes any
1317
+ registered listeners for the pause event. After all jobs are paused, a global listener for the scheduler
1318
+ pause event is also invoked.
1268
1319
 
1269
1320
  Returns
1270
1321
  -------
1271
1322
  None
1272
- This method does not return any value. It schedules a job to resume the
1273
- scheduler at the specified datetime.
1323
+ This method does not return any value. It performs the pausing of all user jobs and triggers
1324
+ the appropriate listeners and logging.
1274
1325
 
1275
- Raises
1276
- ------
1277
- ValueError
1278
- If the 'at' parameter is not a valid datetime object or is not in the future.
1279
- CLIOrionisRuntimeError
1280
- If the scheduler is not running or if an error occurs during job scheduling.
1326
+ Notes
1327
+ -----
1328
+ - Only jobs with valid IDs are paused.
1329
+ - System jobs are not explicitly filtered out; all jobs returned by the scheduler are considered user jobs.
1330
+ - If an error occurs while pausing a job, the exception is handled by the application's error handler.
1331
+ - The set of paused jobs is cleared before pausing to ensure only currently paused jobs are tracked.
1281
1332
  """
1282
1333
 
1283
- # Validate that the 'at' parameter is a datetime object
1284
- if not isinstance(at, datetime):
1285
- raise CLIOrionisValueError("The 'at' parameter must be a datetime object.")
1286
-
1287
- # Define an async function to resume the scheduler
1288
- async def schedule_resume():
1289
-
1290
- # Only resume jobs if the scheduler is currently running
1291
- if self.isRunning():
1292
-
1293
- # Resume only jobs that were paused by pauseEverythingAt
1294
- if self.__pausedByPauseEverything:
1295
-
1296
- # Iterate through the set of paused job IDs and resume each one
1297
- for job_id in list(self.__pausedByPauseEverything):
1298
-
1299
- try:
1300
-
1301
- # Resume the job and log the action
1302
- self.__scheduler.resume_job(job_id)
1334
+ # Only pause jobs if the scheduler is currently running
1335
+ if self.isRunning():
1303
1336
 
1304
- # Invoke the listener for the resumed job
1305
- self.__taskCallableListener(
1306
- self.__getTaskFromSchedulerById(job_id),
1307
- ListeningEvent.JOB_ON_RESUMED
1308
- )
1337
+ # Clear the set of previously paused jobs to avoid stale entries
1338
+ self.__pausedByPauseEverything.clear()
1309
1339
 
1310
- # Log an informational message indicating that the job has been resumed
1311
- self.__logger.info(f"User job '{job_id}' has been resumed.")
1340
+ # Retrieve all jobs currently managed by the scheduler
1341
+ all_jobs = self.__scheduler.get_jobs()
1312
1342
 
1313
- except Exception as e:
1343
+ # Iterate through each job and attempt to pause it
1344
+ for job in all_jobs:
1345
+ try:
1346
+ # Get the job ID safely
1347
+ job_id = self.__getAttribute(job, 'id', None)
1314
1348
 
1315
- # If an error occurs while resuming the job, raise an exception
1316
- self.__raiseException(e)
1349
+ # Skip jobs without a valid ID
1350
+ if not job_id:
1351
+ continue
1317
1352
 
1318
- # Clear the set after resuming all jobs
1319
- self.__pausedByPauseEverything.clear()
1353
+ # Pause the job in the scheduler
1354
+ self.__scheduler.pause_job(job_id)
1320
1355
 
1321
- # Execute the global callable listener after all jobs are resumed
1322
- self.__globalCallableListener(SchedulerResumed(
1323
- code=EVENT_SCHEDULER_RESUMED,
1324
- time=self.__getCurrentTime()
1325
- ), ListeningEvent.SCHEDULER_RESUMED)
1356
+ # Track the paused job's ID
1357
+ self.__pausedByPauseEverything.add(job_id)
1326
1358
 
1327
- # Get the current time in the configured timezone
1359
+ # Get the current time in the configured timezone for logging
1328
1360
  now = self.__getCurrentTime()
1329
1361
 
1330
- # Log an informational message indicating that the scheduler has been resumed
1331
- self.__logger.info(f"Orionis Scheduler resumed successfully at {now}.")
1332
-
1333
- # Log that all previously paused jobs have been resumed
1334
- self.__logger.info("All previously paused user jobs have been resumed.")
1362
+ # Retrieve event data for the paused job
1363
+ event_data = self.__getTaskFromSchedulerById(job_id)
1335
1364
 
1336
- try:
1365
+ # Invoke the listener for the paused job event
1366
+ self.__taskCallableListener(
1367
+ event_data,
1368
+ ListeningEvent.JOB_ON_PAUSED
1369
+ )
1337
1370
 
1338
- # Remove any existing resume job to avoid conflicts
1339
- try:
1340
- self.__scheduler.remove_job("scheduler_resume_at")
1371
+ # Log the pause action for this job
1372
+ self.__logger.info(f"Task '{job_id}' paused successfully at {now}.")
1341
1373
 
1342
- # If the job doesn't exist, it's fine to proceed
1343
- finally:
1344
- pass
1345
-
1346
- # Add a job to the scheduler to resume it at the specified datetime
1347
- self.__scheduler.add_job(
1348
- func=self.wrapAsyncFunction(schedule_resume), # Function to resume the scheduler
1349
- trigger=DateTrigger(run_date=at), # Trigger type is 'date' for one-time execution
1350
- id="scheduler_resume_at", # Unique job ID for resuming the scheduler
1351
- name="Resume Scheduler", # Descriptive name for the job
1352
- replace_existing=True # Replace any existing job with the same ID
1353
- )
1374
+ except Exception as e:
1354
1375
 
1355
- # Log the scheduled resume
1356
- self.__logger.info(f"Scheduler resume scheduled for {at.strftime('%Y-%m-%d %H:%M:%S')}")
1376
+ # Handle any errors that occur while pausing a job
1377
+ self.__raiseException(e)
1357
1378
 
1358
- except Exception as e:
1379
+ # After all jobs are paused, invoke the global listener for scheduler pause
1380
+ self.__globalCallableListener(SchedulerPaused(
1381
+ code=EVENT_SCHEDULER_PAUSED,
1382
+ time=self.__getNow()
1383
+ ), ListeningEvent.SCHEDULER_PAUSED)
1359
1384
 
1360
- # Handle exceptions that may occur during job scheduling
1361
- raise CLIOrionisRuntimeError(f"Failed to schedule scheduler resume: {str(e)}") from e
1385
+ # Log that all tasks have been paused
1386
+ self.__logger.info("All tasks have been paused.")
1362
1387
 
1363
- def shutdownEverythingAt(
1364
- self,
1365
- at: datetime,
1366
- wait: bool = True
1388
+ def resume(
1389
+ self
1367
1390
  ) -> None:
1368
1391
  """
1369
- Schedule the scheduler to shut down all operations at a specific datetime.
1392
+ Resume all user jobs that were previously paused by the scheduler.
1370
1393
 
1371
- This method schedules a job that shuts down the AsyncIOScheduler at the specified datetime.
1372
- The job is added to the scheduler with a 'date' trigger, ensuring it executes exactly
1373
- at the given time. If a shutdown job already exists, it is replaced to avoid conflicts.
1374
-
1375
- Parameters
1376
- ----------
1377
- at : datetime
1378
- The datetime at which the scheduler should be shut down. Must be a valid
1379
- datetime object.
1380
- wait : bool, optional
1381
- Whether to wait for currently running jobs to complete before shutdown.
1382
- Default is True.
1394
+ This method resumes only those jobs that were paused using the `pause` method,
1395
+ as tracked by the internal set `__pausedByPauseEverything`. It iterates through
1396
+ each paused job, attempts to resume it, and triggers any registered listeners
1397
+ for the resumed job event. After all jobs are resumed, a global listener for the
1398
+ scheduler resume event is also invoked. The set of paused jobs is cleared after
1399
+ resumption to ensure accurate tracking.
1383
1400
 
1384
1401
  Returns
1385
1402
  -------
1386
1403
  None
1387
- This method does not return any value. It schedules a job to shut down the
1388
- scheduler at the specified datetime.
1404
+ This method does not return any value. It resumes all jobs that were paused
1405
+ by the scheduler, triggers the appropriate listeners, and logs the actions.
1389
1406
 
1390
- Raises
1391
- ------
1392
- CLIOrionisValueError
1393
- If the 'at' parameter is not a valid datetime object or 'wait' is not boolean,
1394
- or if the scheduled time is not in the future.
1395
- CLIOrionisRuntimeError
1396
- If the scheduler is not running or if an error occurs during job scheduling.
1407
+ Notes
1408
+ -----
1409
+ - Only jobs that were paused by the `pause` method are resumed.
1410
+ - If an error occurs while resuming a job, the exception is handled by the application's error handler.
1411
+ - After resuming all jobs, the global scheduler resumed event is triggered.
1397
1412
  """
1398
1413
 
1399
- # Validate that the 'at' parameter is a datetime object
1400
- if not isinstance(at, datetime):
1401
- raise CLIOrionisValueError("The 'at' parameter must be a datetime object.")
1414
+ # Only resume jobs if the scheduler is currently running
1415
+ if self.isRunning():
1402
1416
 
1403
- # Validate that the 'wait' parameter is a boolean
1404
- if not isinstance(wait, bool):
1405
- raise CLIOrionisValueError("The 'wait' parameter must be a boolean value.")
1417
+ # Resume only jobs that were paused by the pause method
1418
+ if self.__pausedByPauseEverything:
1406
1419
 
1407
- # Define an async function to shut down the scheduler
1408
- async def schedule_shutdown():
1409
- # Only shut down the scheduler if it is currently running
1410
- if self.isRunning():
1411
- try:
1420
+ # Iterate through the set of paused job IDs and resume each one
1421
+ for job_id in list(self.__pausedByPauseEverything):
1412
1422
 
1413
- # Log the shutdown initiation
1414
- self.__logger.info("Initiating scheduled shutdown...")
1423
+ try:
1415
1424
 
1416
- # Call the async shutdown method
1417
- await self.shutdown(wait=wait)
1425
+ # Resume the job in the scheduler
1426
+ self.__scheduler.resume_job(job_id)
1418
1427
 
1419
- except Exception as e:
1428
+ # Retrieve event data for the resumed job
1429
+ event_data = self.__getTaskFromSchedulerById(job_id)
1420
1430
 
1421
- # Log any errors that occur during shutdown
1422
- self.__logger.error(f"Error during scheduled shutdown: {str(e)}")
1431
+ # Invoke the listener for the resumed job event
1432
+ self.__taskCallableListener(
1433
+ event_data,
1434
+ ListeningEvent.JOB_ON_RESUMED
1435
+ )
1423
1436
 
1424
- # Force stop if graceful shutdown fails
1425
- self.forceStop()
1437
+ # Log an informational message indicating that the job has been resumed
1438
+ self.__logger.info(f"Task '{job_id}' has been resumed.")
1426
1439
 
1427
- try:
1440
+ except Exception as e:
1428
1441
 
1429
- # Remove any existing shutdown job to avoid conflicts
1430
- try:
1431
- self.__scheduler.remove_job("scheduler_shutdown_at")
1442
+ # Handle any errors that occur while resuming a job
1443
+ self.__raiseException(e)
1432
1444
 
1433
- # If the job doesn't exist, it's fine to proceed
1434
- finally:
1435
- pass
1436
-
1437
- # Add a job to the scheduler to shut it down at the specified datetime
1438
- self.__scheduler.add_job(
1439
- func=self.wrapAsyncFunction(schedule_shutdown), # Function to shut down the scheduler
1440
- trigger=DateTrigger(run_date=at), # Trigger type is 'date' for one-time execution
1441
- id="scheduler_shutdown_at", # Unique job ID for shutting down the scheduler
1442
- name="Shutdown Scheduler", # Descriptive name for the job
1443
- replace_existing=True # Replace any existing job with the same ID
1444
- )
1445
+ # Clear the set after resuming all jobs to avoid stale entries
1446
+ self.__pausedByPauseEverything.clear()
1445
1447
 
1446
- # Log the scheduled shutdown
1447
- self.__logger.info(f"Scheduler shutdown scheduled for {at.strftime('%Y-%m-%d %H:%M:%S')} (wait={wait})")
1448
+ # Execute the global callable listener after all jobs are resumed
1449
+ self.__globalCallableListener(SchedulerResumed(
1450
+ code=EVENT_SCHEDULER_RESUMED,
1451
+ time=self.__getNow()
1452
+ ), ListeningEvent.SCHEDULER_RESUMED)
1448
1453
 
1449
- except Exception as e:
1454
+ # Get the current time in the configured timezone for logging
1455
+ now = self.__getCurrentTime()
1450
1456
 
1451
- # Handle exceptions that may occur during job scheduling
1452
- raise CLIOrionisRuntimeError(f"Failed to schedule scheduler shutdown: {str(e)}") from e
1457
+ # Log an informational message indicating that the scheduler has been resumed
1458
+ self.__logger.info(f"Orionis Scheduler resumed successfully at {now}.")
1459
+
1460
+ # Log that all previously paused jobs have been resumed
1461
+ self.__logger.info("All previously task have been resumed.")
1453
1462
 
1454
1463
  async def start(self) -> None:
1455
1464
  """
@@ -1553,16 +1562,20 @@ class Schedule(ISchedule):
1553
1562
 
1554
1563
  # Validate the wait parameter
1555
1564
  if not isinstance(wait, bool):
1556
- self.__raiseException(CLIOrionisValueError("The 'wait' parameter must be a boolean value."))
1565
+ self.__raiseException(
1566
+ CLIOrionisValueError(
1567
+ "The 'wait' parameter must be a boolean value (True or False) to indicate whether to wait for running jobs to finish before shutting down the scheduler."
1568
+ )
1569
+ )
1557
1570
 
1558
1571
  # If the scheduler is not running, there's nothing to shut down
1559
1572
  if not self.isRunning():
1573
+ self.__logger.info("The scheduler is already stopped. No shutdown action is required.")
1560
1574
  return
1561
1575
 
1562
1576
  try:
1563
-
1564
1577
  # Log the shutdown process
1565
- self.__logger.info(f"Shutting down scheduler (wait={wait})...")
1578
+ self.__logger.info(f"Starting Orionis Scheduler shutdown process (wait={wait})...")
1566
1579
 
1567
1580
  # Shut down the AsyncIOScheduler
1568
1581
  self.__scheduler.shutdown(wait=wait)
@@ -1576,37 +1589,45 @@ class Schedule(ISchedule):
1576
1589
  await asyncio.sleep(0.1)
1577
1590
 
1578
1591
  # Log the successful shutdown
1579
- self.__logger.info("Scheduler shutdown completed successfully.")
1592
+ self.__logger.info("Orionis Scheduler has been shut down successfully.")
1580
1593
 
1581
1594
  except Exception as e:
1582
-
1583
1595
  # Handle exceptions that may occur during shutdown
1584
- self.__raiseException(CLIOrionisRuntimeError(f"Failed to shut down the scheduler: {str(e)}"))
1596
+ self.__raiseException(
1597
+ CLIOrionisRuntimeError(
1598
+ f"Error while attempting to shut down Orionis Scheduler: {str(e)}"
1599
+ )
1600
+ )
1585
1601
 
1586
1602
  def pauseTask(self, signature: str) -> bool:
1587
1603
  """
1588
1604
  Pause a scheduled job in the AsyncIO scheduler.
1589
1605
 
1590
- This method pauses a job in the AsyncIOScheduler identified by its unique signature.
1591
- It validates the provided signature to ensure it is a non-empty string and attempts
1592
- to pause the job. If the operation is successful, it logs the action and returns True.
1593
- If the job cannot be paused (e.g., it does not exist), the method returns False.
1606
+ This method attempts to pause a job managed by the AsyncIOScheduler, identified by its unique signature.
1607
+ It first validates that the provided signature is a non-empty string. If the job exists and is successfully
1608
+ paused, the method logs the action and returns True. If the job does not exist or an error occurs during
1609
+ the pause operation, the method returns False.
1594
1610
 
1595
1611
  Parameters
1596
1612
  ----------
1597
1613
  signature : str
1598
- The unique signature (ID) of the job to pause. This must be a non-empty string.
1614
+ The unique signature (ID) of the job to pause. Must be a non-empty string.
1599
1615
 
1600
1616
  Returns
1601
1617
  -------
1602
1618
  bool
1603
1619
  True if the job was successfully paused.
1604
- False if the job does not exist or an error occurred.
1620
+ False if the job does not exist or an error occurred during the pause operation.
1605
1621
 
1606
1622
  Raises
1607
1623
  ------
1608
1624
  CLIOrionisValueError
1609
1625
  If the `signature` parameter is not a non-empty string.
1626
+
1627
+ Notes
1628
+ -----
1629
+ This method is intended for use with jobs managed by the AsyncIOScheduler. It does not raise
1630
+ an exception if the job does not exist; instead, it returns False to indicate failure.
1610
1631
  """
1611
1632
 
1612
1633
  # Validate that the signature is a non-empty string
@@ -1614,19 +1635,18 @@ class Schedule(ISchedule):
1614
1635
  self.__raiseException(CLIOrionisValueError("Signature must be a non-empty string."))
1615
1636
 
1616
1637
  try:
1617
-
1618
1638
  # Attempt to pause the job with the given signature
1619
1639
  self.__scheduler.pause_job(signature)
1620
1640
 
1621
1641
  # Log the successful pausing of the job
1622
- self.__logger.info(f"Job '{signature}' has been paused.")
1642
+ self.__logger.info(f"Pause '{signature}' has been paused.")
1623
1643
 
1624
1644
  # Return True to indicate the job was successfully paused
1625
1645
  return True
1626
1646
 
1627
1647
  except Exception:
1628
1648
 
1629
- # Return False if the job could not be paused (e.g., it does not exist)
1649
+ # Return False if the job could not be paused (e.g., it does not exist or another error occurred)
1630
1650
  return False
1631
1651
 
1632
1652
  def resumeTask(self, signature: str) -> bool:
@@ -1634,25 +1654,30 @@ class Schedule(ISchedule):
1634
1654
  Resume a paused job in the AsyncIO scheduler.
1635
1655
 
1636
1656
  This method attempts to resume a job that was previously paused in the AsyncIOScheduler.
1637
- It validates the provided job signature, ensures it is a non-empty string, and then
1638
- resumes the job if it exists and is currently paused. If the operation is successful,
1639
- it logs the action and returns True. If the job cannot be resumed (e.g., it does not exist),
1640
- the method returns False.
1657
+ It first validates that the provided job signature is a non-empty string. If the job exists
1658
+ and is currently paused, the method resumes the job, logs the action, and returns True.
1659
+ If the job does not exist or an error occurs during the resume operation, the method returns False.
1641
1660
 
1642
1661
  Parameters
1643
1662
  ----------
1644
1663
  signature : str
1645
- The unique signature (ID) of the job to resume. This must be a non-empty string.
1664
+ The unique signature (ID) of the job to resume. Must be a non-empty string.
1646
1665
 
1647
1666
  Returns
1648
1667
  -------
1649
1668
  bool
1650
- True if the job was successfully resumed, False if the job does not exist or an error occurred.
1669
+ True if the job was successfully resumed.
1670
+ False if the job does not exist or an error occurred during the resume operation.
1651
1671
 
1652
1672
  Raises
1653
1673
  ------
1654
1674
  CLIOrionisValueError
1655
1675
  If the `signature` parameter is not a non-empty string.
1676
+
1677
+ Notes
1678
+ -----
1679
+ This method is intended for use with jobs managed by the AsyncIOScheduler. It does not raise
1680
+ an exception if the job does not exist; instead, it returns False to indicate failure.
1656
1681
  """
1657
1682
 
1658
1683
  # Validate that the signature is a non-empty string
@@ -1660,45 +1685,51 @@ class Schedule(ISchedule):
1660
1685
  self.__raiseException(CLIOrionisValueError("Signature must be a non-empty string."))
1661
1686
 
1662
1687
  try:
1688
+
1663
1689
  # Attempt to resume the job with the given signature
1664
1690
  self.__scheduler.resume_job(signature)
1665
1691
 
1666
1692
  # Log the successful resumption of the job
1667
- self.__logger.info(f"Job '{signature}' has been resumed.")
1693
+ self.__logger.info(f"Task '{signature}' has been resumed.")
1668
1694
 
1669
1695
  # Return True to indicate the job was successfully resumed
1670
1696
  return True
1671
1697
 
1672
1698
  except Exception:
1673
1699
 
1674
- # Return False if the job could not be resumed (e.g., it does not exist)
1700
+ # Return False if the job could not be resumed (e.g., it does not exist or another error occurred)
1675
1701
  return False
1676
1702
 
1677
1703
  def removeTask(self, signature: str) -> bool:
1678
1704
  """
1679
- Remove a scheduled job from the AsyncIO scheduler.
1705
+ Remove a scheduled job from the AsyncIO scheduler by its signature.
1680
1706
 
1681
- This method removes a job from the AsyncIOScheduler using its unique signature (ID).
1682
- It validates the provided signature to ensure it is a non-empty string, attempts to
1683
- remove the job from the scheduler, and updates the internal jobs list accordingly.
1684
- If the operation is successful, it logs the action and returns True. If the job
1685
- cannot be removed (e.g., it does not exist), the method returns False.
1707
+ This method attempts to remove a job from the AsyncIOScheduler using its unique signature (ID).
1708
+ It first validates that the provided signature is a non-empty string. If the job exists,
1709
+ it is removed from both the scheduler and the internal jobs list. The method logs the removal
1710
+ and returns True if successful. If the job does not exist or an error occurs during removal,
1711
+ the method returns False.
1686
1712
 
1687
1713
  Parameters
1688
1714
  ----------
1689
1715
  signature : str
1690
- The unique signature (ID) of the job to remove. This must be a non-empty string.
1716
+ The unique signature (ID) of the job to remove. Must be a non-empty string.
1691
1717
 
1692
1718
  Returns
1693
1719
  -------
1694
1720
  bool
1695
- True if the job was successfully removed from the scheduler.
1696
- False if the job does not exist or an error occurred.
1721
+ True if the job was successfully removed from both the scheduler and the internal jobs list.
1722
+ False if the job does not exist or an error occurred during removal.
1697
1723
 
1698
1724
  Raises
1699
1725
  ------
1700
1726
  CLIOrionisValueError
1701
1727
  If the `signature` parameter is not a non-empty string.
1728
+
1729
+ Notes
1730
+ -----
1731
+ This method ensures that both the scheduler and the internal jobs list remain consistent
1732
+ after a job is removed. No exception is raised if the job does not exist; instead, False is returned.
1702
1733
  """
1703
1734
 
1704
1735
  # Validate that the signature is a non-empty string
@@ -1710,43 +1741,47 @@ class Schedule(ISchedule):
1710
1741
  # Attempt to remove the job from the scheduler using its signature
1711
1742
  self.__scheduler.remove_job(signature)
1712
1743
 
1713
- # Iterate through the internal jobs list to find and remove the job
1744
+ # Remove the job from the internal jobs list if it exists
1714
1745
  for job in self.__jobs:
1715
1746
  if job.signature == signature:
1716
- self.__jobs.remove(job) # Remove the job from the internal list
1747
+ self.__jobs.remove(job)
1717
1748
  break
1718
1749
 
1719
1750
  # Log the successful removal of the job
1720
- self.__logger.info(f"Job '{signature}' has been removed from the scheduler.")
1751
+ self.__logger.info(f"Task '{signature}' has been removed from the scheduler.")
1721
1752
 
1722
1753
  # Return True to indicate the job was successfully removed
1723
1754
  return True
1724
1755
 
1725
1756
  except Exception:
1726
1757
 
1727
- # Return False if the job could not be removed (e.g., it does not exist)
1758
+ # Return False if the job could not be removed (e.g., it does not exist or another error occurred)
1728
1759
  return False
1729
1760
 
1730
1761
  def events(self) -> List[Dict]:
1731
1762
  """
1732
- Retrieve all scheduled jobs currently managed by the Scheduler.
1763
+ Retrieve a list of all scheduled jobs managed by the scheduler.
1733
1764
 
1734
- This method loads and returns a list of dictionaries, each representing a scheduled job
1735
- managed by this Scheduler instance. Each dictionary contains details such as the command
1736
- signature, arguments, purpose, random delay, start and end dates, and additional job details.
1765
+ This method ensures that all scheduled events are loaded into the internal jobs list,
1766
+ then iterates through each job to collect its details in a dictionary format. Each
1767
+ dictionary contains information such as the command signature, arguments, purpose,
1768
+ random delay, start and end dates, and any additional job details.
1737
1769
 
1738
1770
  Returns
1739
1771
  -------
1740
- list of dict
1741
- A list where each element is a dictionary containing information about a scheduled job.
1742
- Each dictionary includes the following keys:
1743
- - 'signature': str, the command signature.
1744
- - 'args': list, the arguments passed to the command.
1745
- - 'purpose': str, the description or purpose of the job.
1746
- - 'random_delay': any, the random delay associated with the job (if any).
1747
- - 'start_date': str or None, the formatted start date and time of the job, or None if not set.
1748
- - 'end_date': str or None, the formatted end date and time of the job, or None if not set.
1749
- - 'details': any, additional details about the job.
1772
+ List[dict]
1773
+ A list of dictionaries, where each dictionary represents a scheduled job and contains:
1774
+ - 'signature' (str): The command signature of the job.
1775
+ - 'args' (list): The arguments passed to the command.
1776
+ - 'purpose' (str): The description or purpose of the job.
1777
+ - 'random_delay' (int): The random delay associated with the job, if any.
1778
+ - 'start_date' (str): The formatted start date and time of the job, or 'Not Applicable' if not set.
1779
+ - 'end_date' (str): The formatted end date and time of the job, or 'Not Applicable' if not set.
1780
+ - 'details' (str): Additional details about the job.
1781
+ Notes
1782
+ -----
1783
+ This method guarantees that the returned list reflects the current state of all jobs
1784
+ managed by this scheduler instance. If no jobs are scheduled, an empty list is returned.
1750
1785
  """
1751
1786
 
1752
1787
  # Ensure all events are loaded into the internal jobs list
@@ -1758,13 +1793,18 @@ class Schedule(ISchedule):
1758
1793
  # Iterate over each job in the internal jobs list
1759
1794
  for job in self.__jobs:
1760
1795
 
1761
- signature = job.signature
1762
- args = job.args
1763
- purpose = job.purpose
1764
- random_delay = job.random_delay if job.random_delay else 0
1765
- start_date = job.start_date.strftime('%Y-%m-%d %H:%M:%S') if job.start_date else 'Not Applicable'
1766
- end_date = job.end_date.strftime('%Y-%m-%d %H:%M:%S') if job.end_date else 'Not Applicable'
1767
- details = job.details if job.details else 'Not Available'
1796
+ # Safely extract job details with default values if attributes are missing
1797
+ signature: str = self.__getAttribute(job, 'signature', '')
1798
+ args: list = self.__getAttribute(job, 'args', [])
1799
+ purpose: str = self.__getAttribute(job, 'purpose', 'No Description')
1800
+ random_delay: int = self.__getAttribute(job, 'random_delay', 0)
1801
+ start_date: datetime = self.__getAttribute(job, 'start_date', None)
1802
+ end_date: datetime = self.__getAttribute(job, 'end_date', None)
1803
+ details: str = self.__getAttribute(job, 'details', 'Not Available')
1804
+
1805
+ # Format the start and end dates as strings, or mark as 'Not Applicable' if not set
1806
+ formatted_start = start_date.strftime('%Y-%m-%d %H:%M:%S') if start_date else 'Not Applicable'
1807
+ formatted_end = end_date.strftime('%Y-%m-%d %H:%M:%S') if end_date else 'Not Applicable'
1768
1808
 
1769
1809
  # Append a dictionary with relevant job details to the events list
1770
1810
  events.append({
@@ -1772,188 +1812,154 @@ class Schedule(ISchedule):
1772
1812
  'args': args,
1773
1813
  'purpose': purpose,
1774
1814
  'random_delay': random_delay,
1775
- 'start_date': start_date,
1776
- 'end_date': end_date,
1815
+ 'start_date': formatted_start,
1816
+ 'end_date': formatted_end,
1777
1817
  'details': details
1778
1818
  })
1779
1819
 
1780
1820
  # Return the list of scheduled job details
1781
1821
  return events
1782
1822
 
1783
- def cancelScheduledPause(self) -> bool:
1784
- """
1785
- Cancel a previously scheduled pause operation.
1786
-
1787
- This method attempts to remove a job from the scheduler that was set to pause
1788
- the scheduler at a specific time. If the job exists, it is removed, and a log entry
1789
- is created to indicate the cancellation. If no such job exists, the method returns False.
1790
-
1791
- Returns
1792
- -------
1793
- bool
1794
- True if the scheduled pause job was successfully cancelled.
1795
- False if no pause job was found or an error occurred during the cancellation process.
1823
+ def event(
1824
+ self,
1825
+ signature: str
1826
+ ) -> Optional[Dict]:
1796
1827
  """
1797
- try:
1828
+ Retrieve the details of a specific scheduled job by its signature.
1798
1829
 
1799
- # Remove any listener associated with the pause event
1800
- listener = ListeningEvent.SCHEDULER_PAUSED.value
1801
- if listener in self.__listeners:
1802
- del self.__listeners[listener]
1803
-
1804
- # Attempt to remove the pause job with the specific ID
1805
- # if it exists
1806
- try:
1807
- self.__scheduler.remove_job("scheduler_pause_at")
1808
- finally:
1809
- pass
1810
-
1811
- # Log the successful cancellation of the pause operation
1812
- self.__logger.info("Scheduled pause operation cancelled.")
1813
-
1814
- # Return True to indicate the pause job was successfully cancelled
1815
- return True
1816
-
1817
- finally:
1818
-
1819
- # Return False if the pause job does not exist or an error occurred
1820
- return False
1821
-
1822
- def cancelScheduledResume(self) -> bool:
1823
- """
1824
- Cancel a previously scheduled resume operation.
1830
+ This method searches the internal jobs list for a job whose signature matches
1831
+ the provided value. If a matching job is found, it returns a dictionary containing
1832
+ the job's details, such as its arguments, purpose, random delay, start and end dates,
1833
+ and additional details. If no job with the given signature exists, the method returns None.
1825
1834
 
1826
- This method attempts to remove a job from the scheduler that was set to resume
1827
- the scheduler at a specific time. If the job exists, it is removed, and a log entry
1828
- is created to indicate the cancellation. If no such job exists, the method returns False.
1835
+ Parameters
1836
+ ----------
1837
+ signature : str
1838
+ The unique signature (ID) of the job to retrieve. Must be a non-empty string.
1829
1839
 
1830
1840
  Returns
1831
1841
  -------
1832
- bool
1833
- True if the scheduled resume job was successfully cancelled.
1834
- False if no resume job was found or an error occurred during the cancellation process.
1835
- """
1836
- try:
1837
-
1838
- # Remove any listener associated with the resume event
1839
- listener = ListeningEvent.SCHEDULER_RESUMED.value
1840
- if listener in self.__listeners:
1841
- del self.__listeners[listener]
1842
-
1843
- # Attempt to remove the resume job with the specific ID
1844
- # if it exists
1845
- try:
1846
- self.__scheduler.remove_job("scheduler_resume_at")
1847
- finally:
1848
- pass
1849
-
1850
- # Log the successful cancellation of the resume operation
1851
- self.__logger.info("Scheduled resume operation cancelled.")
1852
-
1853
- # Return True to indicate the resume job was successfully cancelled
1854
- return True
1855
-
1856
- finally:
1857
-
1858
- # Return False if the resume job does not exist or an error occurred
1859
- return False
1860
-
1861
- def cancelScheduledShutdown(self) -> bool:
1862
- """
1863
- Cancel a previously scheduled shutdown operation.
1864
-
1865
- This method attempts to remove a job from the scheduler that was set to shut down
1866
- the scheduler at a specific time. If the job exists, it is removed, and a log entry
1867
- is created to indicate the cancellation. If no such job exists, the method returns False.
1842
+ dict or None
1843
+ If a job with the specified signature is found, returns a dictionary with the following keys:
1844
+ - 'signature': str, the job's signature.
1845
+ - 'args': list, the arguments passed to the job.
1846
+ - 'purpose': str, the description or purpose of the job.
1847
+ - 'random_delay': int, the random delay associated with the job (if any).
1848
+ - 'start_date': str, the formatted start date and time of the job, or 'Not Applicable' if not set.
1849
+ - 'end_date': str, the formatted end date and time of the job, or 'Not Applicable' if not set.
1850
+ - 'details': str, additional details about the job.
1851
+ If no job with the given signature is found, returns None.
1868
1852
 
1869
- Returns
1870
- -------
1871
- bool
1872
- True if the scheduled shutdown job was successfully cancelled.
1873
- False if no shutdown job was found or an error occurred during the cancellation process.
1853
+ Notes
1854
+ -----
1855
+ This method ensures that all events are loaded before searching for the job.
1856
+ The returned dictionary provides a summary of the job's configuration and metadata.
1874
1857
  """
1875
- try:
1876
-
1877
- # Remove any listener associated with the shutdown event
1878
- listener = ListeningEvent.SCHEDULER_SHUTDOWN.value
1879
- if listener in self.__listeners:
1880
- del self.__listeners[listener]
1881
-
1882
- # Attempt to remove the shutdown job with the specific ID
1883
- # if it exists
1884
- try:
1885
- self.__scheduler.remove_job("scheduler_shutdown_at")
1886
- finally:
1887
- pass
1888
-
1889
- # Log the successful cancellation of the shutdown operation
1890
- self.__logger.info("Scheduled shutdown operation cancelled.")
1858
+ # Ensure all events are loaded into the internal jobs list
1859
+ self.__loadEvents()
1891
1860
 
1892
- # Return True to indicate the shutdown job was successfully cancelled
1893
- return True
1861
+ # Validate the signature parameter
1862
+ if not isinstance(signature, str) or not signature.strip():
1863
+ return None
1894
1864
 
1895
- finally:
1865
+ # Search for the job with the matching signature
1866
+ for job in self.__jobs:
1867
+ # Get the job's signature attribute safely
1868
+ job_signature = self.__getAttribute(job, 'signature', '')
1869
+
1870
+ # If a matching job is found, return its details in a dictionary
1871
+ if job_signature == signature:
1872
+
1873
+ # Extract job details safely with default values
1874
+ args: list = self.__getAttribute(job, 'args', [])
1875
+ purpose: str = self.__getAttribute(job, 'purpose', 'No Description')
1876
+ random_delay: int = self.__getAttribute(job, 'random_delay', 0)
1877
+ start_date: datetime = self.__getAttribute(job, 'start_date', None)
1878
+ end_date: datetime = self.__getAttribute(job, 'end_date', None)
1879
+ details: str = self.__getAttribute(job, 'details', 'Not Available')
1880
+
1881
+ # Return the job details as a dictionary
1882
+ return {
1883
+ 'signature': job_signature,
1884
+ 'args': args,
1885
+ 'purpose': purpose,
1886
+ 'random_delay': random_delay,
1887
+ 'start_date': start_date.strftime('%Y-%m-%d %H:%M:%S') if start_date else 'Not Applicable',
1888
+ 'end_date': end_date.strftime('%Y-%m-%d %H:%M:%S') if end_date else 'Not Applicable',
1889
+ 'details': details
1890
+ }
1896
1891
 
1897
- # Return False if the shutdown job does not exist or an error occurred
1898
- return False
1892
+ # Return None if no job with the given signature is found
1893
+ return None
1899
1894
 
1900
1895
  def isRunning(self) -> bool:
1901
1896
  """
1902
- Determine if the scheduler is currently active and running.
1897
+ Check if the scheduler is currently running.
1903
1898
 
1904
- This method checks the internal state of the AsyncIOScheduler instance to determine
1905
- whether it is currently running. The scheduler is considered running if it has been
1906
- started and has not been paused or shut down.
1899
+ This method inspects the internal state of the AsyncIOScheduler instance to determine
1900
+ whether the scheduler is actively running. The scheduler is considered running if it
1901
+ has been started and has not been paused or shut down.
1907
1902
 
1908
1903
  Returns
1909
1904
  -------
1910
1905
  bool
1911
- True if the scheduler is running, False otherwise.
1906
+ True if the AsyncIOScheduler is currently running; False otherwise.
1912
1907
  """
1913
1908
 
1914
- # Return the running state of the scheduler
1909
+ # Return True if the scheduler is running, otherwise False
1915
1910
  return self.__scheduler.running
1916
1911
 
1917
1912
  def forceStop(self) -> None:
1918
1913
  """
1919
- Forcefully stop the scheduler immediately without waiting for jobs to complete.
1914
+ Forcefully stop the scheduler immediately, bypassing graceful shutdown.
1920
1915
 
1921
- This method shuts down the AsyncIOScheduler instance without waiting for currently
1922
- running jobs to finish. It is intended for emergency situations where an immediate
1923
- stop is required. The method also signals the internal stop event to ensure that
1924
- the scheduler's main loop is interrupted and the application can proceed with
1925
- shutdown procedures.
1916
+ This method immediately shuts down the AsyncIOScheduler instance without waiting for any currently
1917
+ running jobs to finish. It is intended for emergency or critical situations where an abrupt stop
1918
+ is required, such as unrecoverable errors or forced application termination. In addition to shutting
1919
+ down the scheduler, it also signals the internal stop event to interrupt the scheduler's main loop,
1920
+ allowing the application to proceed with its shutdown procedures.
1926
1921
 
1927
1922
  Returns
1928
1923
  -------
1929
1924
  None
1930
- This method does not return any value. It forcefully stops the scheduler and
1931
- signals the stop event.
1925
+ This method does not return any value. It performs a forceful shutdown of the scheduler and
1926
+ signals the stop event to ensure the main loop is interrupted.
1927
+
1928
+ Notes
1929
+ -----
1930
+ - This method should be used with caution, as it does not wait for running jobs to complete.
1931
+ - After calling this method, the scheduler will be stopped and any pending or running jobs may be interrupted.
1932
1932
  """
1933
1933
 
1934
- # Check if the scheduler is currently running
1934
+ # If the scheduler is currently running, shut it down immediately without waiting for jobs to finish
1935
1935
  if self.__scheduler.running:
1936
- # Shut down the scheduler immediately without waiting for jobs to complete
1937
1936
  self.__scheduler.shutdown(wait=False)
1938
1937
 
1939
- # Check if the stop event exists and has not already been set
1938
+ # If the stop event exists and has not already been set, signal it to interrupt the main loop
1940
1939
  if self._stop_event and not self._stop_event.is_set():
1941
- # Signal the stop event to interrupt the scheduler's main loop
1942
1940
  self._stop_event.set()
1943
1941
 
1944
1942
  def stop(self) -> None:
1945
1943
  """
1946
- Stop the scheduler synchronously by setting the stop event.
1944
+ Signal the scheduler to stop synchronously by setting the internal stop event.
1947
1945
 
1948
- This method signals the scheduler to stop by setting the internal stop event.
1949
- It can be called from non-async contexts to initiate a shutdown. If the asyncio
1950
- event loop is running, the stop event is set in a thread-safe manner. Otherwise,
1951
- the stop event is set directly.
1946
+ This method is used to request a graceful shutdown of the scheduler from a synchronous context.
1947
+ It sets the internal asyncio stop event, which will cause the scheduler's main loop to exit.
1948
+ If an asyncio event loop is currently running, the stop event is set in a thread-safe manner
1949
+ using `call_soon_threadsafe`. If no event loop is running, the stop event is set directly.
1950
+ This method is safe to call from both asynchronous and synchronous contexts.
1952
1951
 
1953
1952
  Returns
1954
1953
  -------
1955
1954
  None
1956
- This method does not return any value. It signals the scheduler to stop.
1955
+ This method does not return any value. It only signals the scheduler to stop by setting
1956
+ the internal stop event.
1957
+
1958
+ Notes
1959
+ -----
1960
+ - If the stop event is already set or does not exist, this method does nothing.
1961
+ - Any exceptions encountered while setting the stop event are logged as warnings, but the
1962
+ method will still attempt to set the event directly.
1957
1963
  """
1958
1964
  # Check if the stop event exists and has not already been set
1959
1965
  if self._stop_event and not self._stop_event.is_set():
@@ -1973,6 +1979,6 @@ class Schedule(ISchedule):
1973
1979
 
1974
1980
  except Exception as e:
1975
1981
 
1976
- # Log the error but still try to set the event
1982
+ # Log any unexpected error but still try to set the event directly
1977
1983
  self.__logger.warning(f"Error setting stop event through event loop: {str(e)}")
1978
1984
  self._stop_event.set()