orionis 0.512.0__py3-none-any.whl → 0.514.0__py3-none-any.whl

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