orionis 0.521.0__py3-none-any.whl → 0.522.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.
@@ -18,6 +18,171 @@ class BaseScheduler(IBaseScheduler):
18
18
  # Finalize Global Scheduler at a specific time
19
19
  FINALIZE_AT: datetime = None
20
20
 
21
+ def pauseAt(self, timezone: str = None) -> datetime:
22
+ """
23
+ Retrieves the datetime at which the global scheduler should be paused.
24
+
25
+ This method returns the `PAUSE_AT` attribute as a timezone-aware datetime object.
26
+ If `PAUSE_AT` is not set (i.e., `None`), the method will return `None`.
27
+ If a timezone is provided, it converts the naive datetime to a timezone-aware
28
+ datetime using the specified timezone. If no timezone is provided, the naive
29
+ datetime is returned as is.
30
+
31
+ Parameters
32
+ ----------
33
+ timezone : str, optional
34
+ The name of the timezone to use for converting the naive datetime
35
+ to a timezone-aware datetime. For example, "UTC" or "America/New_York".
36
+ If not provided, the method returns the naive datetime.
37
+
38
+ Returns
39
+ -------
40
+ datetime or None
41
+ - A timezone-aware datetime object representing the pause time of the scheduler
42
+ if `PAUSE_AT` is set and a valid timezone is provided.
43
+ - A naive datetime object representing the pause time if `PAUSE_AT` is set
44
+ but no timezone is provided.
45
+ - `None` if `PAUSE_AT` is not set.
46
+
47
+ Notes
48
+ -----
49
+ - The `PAUSE_AT` attribute is expected to be a naive datetime object.
50
+ - The method uses the `pytz` library to localize the naive datetime to the specified timezone.
51
+ - If the `PAUSE_AT` attribute is `None`, the method will return `None` without performing any conversion.
52
+ """
53
+
54
+ # Retrieve the naive datetime value for the pause time
55
+ dt_naive = self.PAUSE_AT
56
+
57
+ # If no pause time is set, return None
58
+ if dt_naive is None:
59
+ return None
60
+
61
+ # If no timezone is provided, return the naive datetime as is
62
+ if timezone is None:
63
+ return dt_naive
64
+
65
+ # Import the pytz library for timezone handling
66
+ import pytz
67
+
68
+ # Get the specified timezone using pytz
69
+ # This will raise an exception if the timezone string is invalid
70
+ tz = pytz.timezone(timezone)
71
+
72
+ # Convert the naive datetime to a timezone-aware datetime
73
+ # This ensures the datetime is localized to the specified timezone
74
+ return tz.localize(dt_naive)
75
+
76
+ def resumeAt(self, timezone: str = None) -> datetime:
77
+ """
78
+ Retrieves the datetime at which the global scheduler should be resumed.
79
+
80
+ This method returns the `RESUME_AT` attribute as a timezone-aware datetime object.
81
+ If `RESUME_AT` is not set (i.e., `None`), the method will return `None`.
82
+ If a timezone is provided, it converts the naive datetime to a timezone-aware
83
+ datetime using the specified timezone. If no timezone is provided, the naive
84
+ datetime is returned as is.
85
+
86
+ Parameters
87
+ ----------
88
+ timezone : str, optional
89
+ The name of the timezone to use for converting the naive datetime
90
+ to a timezone-aware datetime. For example, "UTC" or "America/New_York".
91
+ If not provided, the method returns the naive datetime.
92
+
93
+ Returns
94
+ -------
95
+ datetime or None
96
+ - A timezone-aware datetime object representing the resume time of the scheduler
97
+ if `RESUME_AT` is set and a valid timezone is provided.
98
+ - A naive datetime object representing the resume time if `RESUME_AT` is set
99
+ but no timezone is provided.
100
+ - `None` if `RESUME_AT` is not set.
101
+
102
+ Notes
103
+ -----
104
+ - The `RESUME_AT` attribute is expected to be a naive datetime object.
105
+ - The method uses the `pytz` library to localize the naive datetime to the specified timezone.
106
+ - If the `RESUME_AT` attribute is `None`, the method will return `None` without performing any conversion.
107
+ """
108
+
109
+ # Retrieve the naive datetime value for the resume time
110
+ dt_naive = self.RESUME_AT
111
+
112
+ # If no resume time is set, return None
113
+ if dt_naive is None:
114
+ return None
115
+
116
+ # If no timezone is provided, return the naive datetime as is
117
+ if timezone is None:
118
+ return dt_naive
119
+
120
+ # Import the pytz library for timezone handling
121
+ import pytz
122
+
123
+ # Get the specified timezone using pytz
124
+ # This will raise an exception if the timezone string is invalid
125
+ tz = pytz.timezone(timezone)
126
+
127
+ # Convert the naive datetime to a timezone-aware datetime
128
+ # This ensures the datetime is localized to the specified timezone
129
+ return tz.localize(dt_naive)
130
+
131
+ def finalizeAt(self, timezone: str = None) -> datetime:
132
+ """
133
+ Retrieves the datetime at which the global scheduler should be finalized.
134
+
135
+ This method returns the `FINALIZE_AT` attribute as a timezone-aware datetime object.
136
+ If `FINALIZE_AT` is not set (i.e., `None`), the method will return `None`.
137
+ If a timezone is provided, it converts the naive datetime to a timezone-aware
138
+ datetime using the specified timezone. If no timezone is provided, the naive
139
+ datetime is returned as is.
140
+
141
+ Parameters
142
+ ----------
143
+ timezone : str, optional
144
+ The name of the timezone to use for converting the naive datetime
145
+ to a timezone-aware datetime. For example, "UTC" or "America/New_York".
146
+ If not provided, the method returns the naive datetime.
147
+
148
+ Returns
149
+ -------
150
+ datetime or None
151
+ - A timezone-aware datetime object representing the finalize time of the scheduler
152
+ if `FINALIZE_AT` is set and a valid timezone is provided.
153
+ - A naive datetime object representing the finalize time if `FINALIZE_AT` is set
154
+ but no timezone is provided.
155
+ - `None` if `FINALIZE_AT` is not set.
156
+
157
+ Notes
158
+ -----
159
+ - The `FINALIZE_AT` attribute is expected to be a naive datetime object.
160
+ - The method uses the `pytz` library to localize the naive datetime to the specified timezone.
161
+ - If the `FINALIZE_AT` attribute is `None`, the method will return `None` without performing any conversion.
162
+ """
163
+
164
+ # Retrieve the naive datetime value for the finalize time
165
+ dt_naive = self.FINALIZE_AT
166
+
167
+ # If no finalize time is set, return None
168
+ if dt_naive is None:
169
+ return None
170
+
171
+ # If no timezone is provided, return the naive datetime as is
172
+ if timezone is None:
173
+ return dt_naive
174
+
175
+ # Import the pytz library for timezone handling
176
+ import pytz
177
+
178
+ # Get the specified timezone using pytz
179
+ # This will raise an exception if the timezone string is invalid
180
+ tz = pytz.timezone(timezone)
181
+
182
+ # Convert the naive datetime to a timezone-aware datetime
183
+ # This ensures the datetime is localized to the specified timezone
184
+ return tz.localize(dt_naive)
185
+
21
186
  async def tasks(self, schedule: ISchedule):
22
187
  """
23
188
  Defines and registers scheduled tasks for the application.
@@ -110,21 +110,21 @@ class ScheduleWorkCommand(BaseCommand):
110
110
  if hasattr(scheduler, "onError") and callable(scheduler.onError):
111
111
  schedule_service.setListener(ListeningEvent.SCHEDULER_ERROR, scheduler.onError)
112
112
 
113
- # Check if the scheduler has specific pause, resume, and finalize times
114
- if hasattr(scheduler, "PAUSE_AT") and scheduler.PAUSE_AT is not None:
115
- if not isinstance(scheduler.PAUSE_AT, datetime):
116
- raise CLIOrionisRuntimeError("PAUSE_AT must be a datetime instance.")
117
- schedule_service.pauseEverythingAt(scheduler.PAUSE_AT)
113
+ if hasattr(scheduler, "FINALIZE_AT") and scheduler.FINALIZE_AT is not None:
114
+ if not isinstance(scheduler.FINALIZE_AT, datetime):
115
+ raise CLIOrionisRuntimeError("FINALIZE_AT must be a datetime instance.")
116
+ schedule_service.shutdownEverythingAt(scheduler.finalizeAt())
118
117
 
119
118
  if hasattr(scheduler, "RESUME_AT") and scheduler.RESUME_AT is not None:
120
119
  if not isinstance(scheduler.RESUME_AT, datetime):
121
120
  raise CLIOrionisRuntimeError("RESUME_AT must be a datetime instance.")
122
- schedule_service.resumeEverythingAt(scheduler.RESUME_AT)
121
+ schedule_service.resumeEverythingAt(scheduler.resumeAt())
123
122
 
124
- if hasattr(scheduler, "FINALIZE_AT") and scheduler.FINALIZE_AT is not None:
125
- if not isinstance(scheduler.FINALIZE_AT, datetime):
126
- raise CLIOrionisRuntimeError("FINALIZE_AT must be a datetime instance.")
127
- schedule_service.shutdownEverythingAt(scheduler.FINALIZE_AT)
123
+ # Check if the scheduler has specific pause, resume, and finalize times
124
+ if hasattr(scheduler, "PAUSE_AT") and scheduler.PAUSE_AT is not None:
125
+ if not isinstance(scheduler.PAUSE_AT, datetime):
126
+ raise CLIOrionisRuntimeError("PAUSE_AT must be a datetime instance.")
127
+ schedule_service.pauseEverythingAt(scheduler.pauseAt())
128
128
 
129
129
  # Start the scheduler worker asynchronously
130
130
  await schedule_service.start()
@@ -137,4 +137,4 @@ class ScheduleWorkCommand(BaseCommand):
137
137
  # Raise any unexpected exceptions as CLIOrionisRuntimeError
138
138
  raise CLIOrionisRuntimeError(
139
139
  f"An unexpected error occurred while starting the scheduler worker: {e}"
140
- )
140
+ )
@@ -50,7 +50,6 @@ class Scheduler(ISchedule):
50
50
  self,
51
51
  reactor: IReactor,
52
52
  app: IApplication,
53
- console: IConsole,
54
53
  rich_console: Console
55
54
  ) -> None:
56
55
  """
@@ -74,10 +73,7 @@ class Scheduler(ISchedule):
74
73
  """
75
74
 
76
75
  # Store the application instance for configuration access.
77
- self.__app = app
78
-
79
- # Store the console instance for output operations.
80
- self.__console = console
76
+ self.__app: IApplication = app
81
77
 
82
78
  # Store the rich console instance for advanced output formatting.
83
79
  self.__rich_console = rich_console
@@ -99,7 +95,7 @@ class Scheduler(ISchedule):
99
95
  self.__logger: ILogger = self.__app.make('x-orionis.services.log.log_service')
100
96
 
101
97
  # Store the reactor instance for command management.
102
- self.__reactor = reactor
98
+ self.__reactor: IReactor = reactor
103
99
 
104
100
  # Retrieve and store all available commands from the reactor.
105
101
  self.__available_commands = self.__getCommands()
@@ -116,7 +112,6 @@ class Scheduler(ISchedule):
116
112
  # Add this line to the existing __init__ method
117
113
  self._stop_event: Optional[asyncio.Event] = None
118
114
 
119
-
120
115
  def __getCurrentTime(
121
116
  self
122
117
  ) -> str:
@@ -267,7 +262,7 @@ class Scheduler(ISchedule):
267
262
  """
268
263
 
269
264
  # Prevent adding new commands while the scheduler is running
270
- if self.__scheduler.running:
265
+ if self.isRunning():
271
266
  raise CLIOrionisRuntimeError("Cannot add new commands while the scheduler is running.")
272
267
 
273
268
  # Validate that the command signature is a non-empty string
@@ -282,8 +277,10 @@ class Scheduler(ISchedule):
282
277
  if not self.__isAvailable(signature):
283
278
  raise CLIOrionisValueError(f"The command '{signature}' is not available or does not exist.")
284
279
 
285
- # Store the command and its arguments for scheduling
280
+ # Import Event here to avoid circular dependency issues
286
281
  from orionis.console.tasks.event import Event
282
+
283
+ # Store the command and its arguments for scheduling
287
284
  self.__events[signature] = Event(
288
285
  signature=signature,
289
286
  args=args or [],
@@ -367,6 +364,8 @@ class Scheduler(ISchedule):
367
364
 
368
365
  # Check if a listener is registered for the specified event
369
366
  if scheduler_event in self.__listeners:
367
+
368
+ # Retrieve the listener for the specified event
370
369
  listener = self.__listeners[scheduler_event]
371
370
 
372
371
  # Ensure the listener is callable before invoking it
@@ -376,21 +375,29 @@ class Scheduler(ISchedule):
376
375
 
377
376
  # If the listener is a coroutine, schedule it as an asyncio task
378
377
  if asyncio.iscoroutinefunction(listener):
378
+
379
+ # Try to get the running event loop
379
380
  try:
380
- # Try to get the running event loop
381
381
  loop = asyncio.get_running_loop()
382
382
  loop.create_task(listener(event_data, self))
383
+
384
+ # If no event loop is running, log a warning instead of creating one
383
385
  except RuntimeError:
384
- # If no event loop is running, create a new one
385
- asyncio.run(listener(event_data, self))
386
+
387
+ # Raise an error to inform the caller that the listener could not be invoked
388
+ raise CLIOrionisRuntimeError(
389
+ f"Cannot run async listener for '{scheduler_event}': no event loop running"
390
+ )
391
+
386
392
  # Otherwise, invoke the listener directly as a regular function
387
393
  else:
388
394
  listener(event_data, self)
389
395
 
390
396
  except Exception as e:
391
397
 
392
- # Log any exceptions that occur during listener invocation
393
- self.__logger.error(f"Error invoking global listener for event '{scheduler_event}': {str(e)}")
398
+ # Re-raise CLIOrionisRuntimeError exceptions
399
+ if isinstance(e, CLIOrionisRuntimeError):
400
+ raise e
394
401
 
395
402
  # Raise a runtime error if listener invocation fails
396
403
  raise CLIOrionisRuntimeError(
@@ -453,19 +460,43 @@ class Scheduler(ISchedule):
453
460
 
454
461
  # Check if the listener has a method corresponding to the event type
455
462
  if hasattr(listener, scheduler_event) and callable(getattr(listener, scheduler_event)):
463
+
464
+ # Retrieve the method from the listener
456
465
  listener_method = getattr(listener, scheduler_event)
457
466
 
467
+ # Try to invoke the listener method
458
468
  try:
469
+
459
470
  # Invoke the listener method, handling both coroutine and regular functions
460
471
  if asyncio.iscoroutinefunction(listener_method):
461
- # Schedule the coroutine listener method as an asyncio task
462
- asyncio.create_task(listener_method(event_data, self))
472
+
473
+ # Try to get the running event loop
474
+ try:
475
+ loop = asyncio.get_running_loop()
476
+ loop.create_task(listener_method(event_data, self))
477
+
478
+ # If no event loop is running, log a warning
479
+ except RuntimeError:
480
+
481
+ # Raise an error to inform the caller that the listener could not be invoked
482
+ raise CLIOrionisRuntimeError(
483
+ f"Cannot run async listener for '{scheduler_event}' on job '{event_data.job_id}': no event loop running"
484
+ )
485
+
486
+ # Call the regular listener method directly
463
487
  else:
464
- # Call the regular listener method directly
465
488
  listener_method(event_data, self)
489
+
466
490
  except Exception as e:
467
- # Log any exceptions that occur during listener invocation
468
- self.__logger.error(f"Error invoking listener method '{scheduler_event}' for job '{event_data.job_id}': {str(e)}")
491
+
492
+ # Re-raise CLIOrionisRuntimeError exceptions
493
+ if isinstance(e, CLIOrionisRuntimeError):
494
+ raise e
495
+
496
+ # Raise a runtime error if listener invocation fails
497
+ raise CLIOrionisRuntimeError(
498
+ f"An error occurred while invoking the listener for event '{scheduler_event}' on job '{event_data.job_id}': {str(e)}"
499
+ )
469
500
 
470
501
  def __startedListener(
471
502
  self,
@@ -903,27 +934,36 @@ class Scheduler(ISchedule):
903
934
  # Iterate through all scheduled jobs in the AsyncIOScheduler
904
935
  for signature, event in self.__events.items():
905
936
 
906
- # Convert the event to its entity representation
907
- entity = event.toEntity()
908
-
909
- # Add the job to the internal jobs list
910
- self.__jobs.append(entity)
937
+ try:
938
+ # Convert the event to its entity representation
939
+ entity = event.toEntity()
940
+
941
+ # Add the job to the internal jobs list
942
+ self.__jobs.append(entity)
943
+
944
+ # Create a unique key for the job based on its signature
945
+ def create_job_func(cmd, args_list):
946
+ return lambda: self.__reactor.call(cmd, args_list)
947
+
948
+ # Add the job to the scheduler with the specified trigger and parameters
949
+ self.__scheduler.add_job(
950
+ func=create_job_func(signature, list(entity.args)),
951
+ trigger=entity.trigger,
952
+ id=signature,
953
+ name=signature,
954
+ replace_existing=True
955
+ )
911
956
 
912
- # Create a unique key for the job based on its signature
913
- def create_job_func(cmd, args_list):
914
- return lambda: self.__reactor.call(cmd, args_list)
957
+ # If a listener is associated with the event, register it
958
+ if entity.listener:
959
+ self.setListener(signature, entity.listener)
915
960
 
916
- self.__scheduler.add_job(
917
- func=create_job_func(signature, list(entity.args)),
918
- trigger=entity.trigger,
919
- id=signature,
920
- name=signature,
921
- replace_existing=True
922
- )
961
+ except Exception as e:
923
962
 
924
- # If a listener is associated with the event, register it
925
- if entity.listener:
926
- self.setListener(signature, entity.listener)
963
+ # Raise a runtime error if loading the scheduled event fails
964
+ raise CLIOrionisRuntimeError(
965
+ f"Failed to load scheduled event '{signature}': {str(e)}"
966
+ ) from e
927
967
 
928
968
  def setListener(
929
969
  self,
@@ -1001,7 +1041,7 @@ class Scheduler(ISchedule):
1001
1041
  Raises
1002
1042
  ------
1003
1043
  ValueError
1004
- If the 'at' parameter is not a valid datetime object.
1044
+ If the 'at' parameter is not a valid datetime object or is not in the future.
1005
1045
  CLIOrionisRuntimeError
1006
1046
  If the scheduler is not running or if an error occurs during job scheduling.
1007
1047
  """
@@ -1010,27 +1050,32 @@ class Scheduler(ISchedule):
1010
1050
  if not isinstance(at, datetime):
1011
1051
  raise ValueError("The 'at' parameter must be a datetime object.")
1012
1052
 
1013
- # Ensure the scheduler is running before scheduling a pause operation
1014
- if not self.__scheduler.running:
1015
- raise CLIOrionisRuntimeError("Cannot schedule pause operation: scheduler is not running.")
1053
+ # Define a function to pause the scheduler
1054
+ def schedule_pause():
1055
+ if self.isRunning():
1056
+ self.__scheduler.pause()
1016
1057
 
1017
1058
  try:
1018
1059
  # Remove any existing pause job to avoid conflicts
1019
1060
  try:
1020
- self.__scheduler.remove_job(ListeningEvent.SCHEDULER_PAUSED.value)
1061
+ self.__scheduler.remove_job("scheduler_pause_at")
1021
1062
  except:
1022
1063
  pass # If the job doesn't exist, it's fine to proceed
1023
1064
 
1024
1065
  # Add a job to the scheduler to pause it at the specified datetime
1025
1066
  self.__scheduler.add_job(
1026
- func=self.__scheduler.pause, # Function to pause the scheduler
1067
+ func=schedule_pause, # Function to pause the scheduler
1027
1068
  trigger=DateTrigger(run_date=at), # Trigger type is 'date' for one-time execution
1028
- id=ListeningEvent.SCHEDULER_PAUSED.value, # Unique job ID for pausing the scheduler
1069
+ id="scheduler_pause_at", # Unique job ID for pausing the scheduler
1029
1070
  name="Pause Scheduler", # Descriptive name for the job
1030
1071
  replace_existing=True # Replace any existing job with the same ID
1031
1072
  )
1032
1073
 
1074
+ # Log the scheduled pause
1075
+ self.__logger.info(f"Scheduler pause scheduled for {at.strftime('%Y-%m-%d %H:%M:%S')}")
1076
+
1033
1077
  except Exception as e:
1078
+
1034
1079
  # Handle exceptions that may occur during job scheduling
1035
1080
  raise CLIOrionisRuntimeError(f"Failed to schedule scheduler pause: {str(e)}") from e
1036
1081
 
@@ -1060,7 +1105,7 @@ class Scheduler(ISchedule):
1060
1105
  Raises
1061
1106
  ------
1062
1107
  ValueError
1063
- If the 'at' parameter is not a valid datetime object.
1108
+ If the 'at' parameter is not a valid datetime object or is not in the future.
1064
1109
  CLIOrionisRuntimeError
1065
1110
  If the scheduler is not running or if an error occurs during job scheduling.
1066
1111
  """
@@ -1069,33 +1114,40 @@ class Scheduler(ISchedule):
1069
1114
  if not isinstance(at, datetime):
1070
1115
  raise ValueError("The 'at' parameter must be a datetime object.")
1071
1116
 
1072
- # Ensure the scheduler is running before scheduling a resume operation
1073
- if not self.__scheduler.running:
1074
- raise CLIOrionisRuntimeError("Cannot schedule resume operation: scheduler is not running.")
1117
+ # Define a function to resume the scheduler
1118
+ def schedule_resume():
1119
+ if self.isRunning():
1120
+ self.__scheduler.resume()
1075
1121
 
1076
1122
  try:
1123
+
1077
1124
  # Remove any existing resume job to avoid conflicts
1078
1125
  try:
1079
- self.__scheduler.remove_job(ListeningEvent.SCHEDULER_RESUMED.value)
1126
+ self.__scheduler.remove_job("scheduler_resume_at")
1080
1127
  except:
1081
1128
  pass # If the job doesn't exist, it's fine to proceed
1082
1129
 
1083
1130
  # Add a job to the scheduler to resume it at the specified datetime
1084
1131
  self.__scheduler.add_job(
1085
- func=self.__scheduler.resume, # Function to resume the scheduler
1132
+ func=schedule_resume, # Function to resume the scheduler
1086
1133
  trigger=DateTrigger(run_date=at), # Trigger type is 'date' for one-time execution
1087
- id=ListeningEvent.SCHEDULER_RESUMED.value, # Unique job ID for resuming the scheduler
1134
+ id="scheduler_resume_at", # Unique job ID for resuming the scheduler
1088
1135
  name="Resume Scheduler", # Descriptive name for the job
1089
1136
  replace_existing=True # Replace any existing job with the same ID
1090
1137
  )
1091
1138
 
1139
+ # Log the scheduled resume
1140
+ self.__logger.info(f"Scheduler resume scheduled for {at.strftime('%Y-%m-%d %H:%M:%S')}")
1141
+
1092
1142
  except Exception as e:
1143
+
1093
1144
  # Handle exceptions that may occur during job scheduling
1094
1145
  raise CLIOrionisRuntimeError(f"Failed to schedule scheduler resume: {str(e)}") from e
1095
1146
 
1096
1147
  def shutdownEverythingAt(
1097
1148
  self,
1098
- at: datetime
1149
+ at: datetime,
1150
+ wait: bool = True
1099
1151
  ) -> None:
1100
1152
  """
1101
1153
  Schedule the scheduler to shut down all operations at a specific datetime.
@@ -1109,6 +1161,9 @@ class Scheduler(ISchedule):
1109
1161
  at : datetime
1110
1162
  The datetime at which the scheduler should be shut down. Must be a valid
1111
1163
  datetime object.
1164
+ wait : bool, optional
1165
+ Whether to wait for currently running jobs to complete before shutdown.
1166
+ Default is True.
1112
1167
 
1113
1168
  Returns
1114
1169
  -------
@@ -1119,7 +1174,8 @@ class Scheduler(ISchedule):
1119
1174
  Raises
1120
1175
  ------
1121
1176
  ValueError
1122
- If the 'at' parameter is not a valid datetime object.
1177
+ If the 'at' parameter is not a valid datetime object or 'wait' is not boolean,
1178
+ or if the scheduled time is not in the future.
1123
1179
  CLIOrionisRuntimeError
1124
1180
  If the scheduler is not running or if an error occurs during job scheduling.
1125
1181
  """
@@ -1128,27 +1184,40 @@ class Scheduler(ISchedule):
1128
1184
  if not isinstance(at, datetime):
1129
1185
  raise ValueError("The 'at' parameter must be a datetime object.")
1130
1186
 
1131
- # Ensure the scheduler is running before scheduling a shutdown operation
1132
- if not self.__scheduler.running:
1133
- raise CLIOrionisRuntimeError("Cannot schedule shutdown operation: scheduler is not running.")
1187
+ # Validate that the 'wait' parameter is a boolean
1188
+ if not isinstance(wait, bool):
1189
+ raise ValueError("The 'wait' parameter must be a boolean value.")
1190
+
1191
+ # Define a function to shut down the scheduler
1192
+ def schedule_shutdown():
1193
+ if self.isRunning():
1194
+ self.__scheduler.shutdown(wait=wait)
1195
+ # Signal the stop event to break the wait in start()
1196
+ if self._stop_event and not self._stop_event.is_set():
1197
+ self._stop_event.set()
1134
1198
 
1135
1199
  try:
1200
+
1136
1201
  # Remove any existing shutdown job to avoid conflicts
1137
1202
  try:
1138
- self.__scheduler.remove_job(ListeningEvent.SCHEDULER_SHUTDOWN.value)
1203
+ self.__scheduler.remove_job("scheduler_shutdown_at")
1139
1204
  except:
1140
1205
  pass # If the job doesn't exist, it's fine to proceed
1141
1206
 
1142
1207
  # Add a job to the scheduler to shut it down at the specified datetime
1143
1208
  self.__scheduler.add_job(
1144
- func=self.__scheduler.shutdown, # Function to shut down the scheduler
1209
+ func=schedule_shutdown, # Function to shut down the scheduler
1145
1210
  trigger=DateTrigger(run_date=at), # Trigger type is 'date' for one-time execution
1146
- id=ListeningEvent.SCHEDULER_SHUTDOWN.value, # Unique job ID for shutting down the scheduler
1211
+ id="scheduler_shutdown_at", # Unique job ID for shutting down the scheduler
1147
1212
  name="Shutdown Scheduler", # Descriptive name for the job
1148
1213
  replace_existing=True # Replace any existing job with the same ID
1149
1214
  )
1150
1215
 
1216
+ # Log the scheduled shutdown
1217
+ self.__logger.info(f"Scheduler shutdown scheduled for {at.strftime('%Y-%m-%d %H:%M:%S')} (wait={wait})")
1218
+
1151
1219
  except Exception as e:
1220
+
1152
1221
  # Handle exceptions that may occur during job scheduling
1153
1222
  raise CLIOrionisRuntimeError(f"Failed to schedule scheduler shutdown: {str(e)}") from e
1154
1223
 
@@ -1173,6 +1242,7 @@ class Scheduler(ISchedule):
1173
1242
  If the scheduler fails to start due to missing an asyncio event loop or other runtime issues.
1174
1243
  """
1175
1244
  try:
1245
+
1176
1246
  # Ensure the method is called within an asyncio event loop
1177
1247
  loop = asyncio.get_running_loop()
1178
1248
 
@@ -1186,7 +1256,7 @@ class Scheduler(ISchedule):
1186
1256
  self.__subscribeListeners()
1187
1257
 
1188
1258
  # Start the scheduler if it is not already running
1189
- if not self.__scheduler.running:
1259
+ if not self.isRunning():
1190
1260
  self.__scheduler.start()
1191
1261
 
1192
1262
  # Log that the scheduler is now active and waiting for events
@@ -1198,27 +1268,32 @@ class Scheduler(ISchedule):
1198
1268
  await self._stop_event.wait()
1199
1269
 
1200
1270
  except (KeyboardInterrupt, asyncio.CancelledError):
1271
+
1201
1272
  # Handle graceful shutdown when an interruption signal is received
1202
1273
  self.__logger.info("Received shutdown signal, stopping scheduler...")
1203
1274
  await self.shutdown(wait=True)
1204
1275
 
1205
1276
  except Exception as e:
1277
+
1206
1278
  # Log and raise any unexpected exceptions during scheduler operation
1207
1279
  self.__logger.error(f"Error during scheduler operation: {str(e)}")
1208
1280
  raise CLIOrionisRuntimeError(f"Scheduler operation failed: {str(e)}") from e
1209
1281
 
1210
1282
  finally:
1283
+
1211
1284
  # Ensure the scheduler is shut down properly, even if an error occurs
1212
1285
  if self.__scheduler.running:
1213
1286
  await self.shutdown(wait=False)
1214
1287
 
1215
1288
  except RuntimeError as e:
1289
+
1216
1290
  # Handle the case where no asyncio event loop is running
1217
1291
  if "no running event loop" in str(e):
1218
1292
  raise CLIOrionisRuntimeError("Scheduler must be started within an asyncio event loop") from e
1219
1293
  raise CLIOrionisRuntimeError(f"Failed to start the scheduler: {str(e)}") from e
1220
1294
 
1221
1295
  except Exception as e:
1296
+
1222
1297
  # Raise a runtime error for any other issues during startup
1223
1298
  raise CLIOrionisRuntimeError(f"Failed to start the scheduler: {str(e)}") from e
1224
1299
 
@@ -1251,10 +1326,11 @@ class Scheduler(ISchedule):
1251
1326
  raise ValueError("The 'wait' parameter must be a boolean value.")
1252
1327
 
1253
1328
  # If the scheduler is not running, there's nothing to shut down
1254
- if not self.__scheduler.running:
1329
+ if not self.isRunning():
1255
1330
  return
1256
1331
 
1257
1332
  try:
1333
+
1258
1334
  # Log the shutdown process
1259
1335
  self.__logger.info(f"Shutting down scheduler (wait={wait})...")
1260
1336
 
@@ -1269,9 +1345,12 @@ class Scheduler(ISchedule):
1269
1345
  if wait:
1270
1346
  await asyncio.sleep(0.1)
1271
1347
 
1348
+ # Log the successful shutdown
1272
1349
  self.__logger.info("Scheduler shutdown completed successfully.")
1273
1350
 
1274
1351
  except Exception as e:
1352
+
1353
+ # Handle exceptions that may occur during shutdown
1275
1354
  raise CLIOrionisRuntimeError(f"Failed to shut down the scheduler: {str(e)}") from e
1276
1355
 
1277
1356
  def pause(self, signature: str) -> bool:
@@ -1403,7 +1482,7 @@ class Scheduler(ISchedule):
1403
1482
 
1404
1483
  # Iterate through the internal jobs list to find and remove the job
1405
1484
  for job in self.__jobs:
1406
- if job['signature'] == signature:
1485
+ if job.signature == signature:
1407
1486
  self.__jobs.remove(job) # Remove the job from the internal list
1408
1487
  break
1409
1488
 
@@ -1469,4 +1548,182 @@ class Scheduler(ISchedule):
1469
1548
  })
1470
1549
 
1471
1550
  # Return the list of scheduled job details
1472
- return events
1551
+ return events
1552
+
1553
+ def cancelScheduledPause(self) -> bool:
1554
+ """
1555
+ Cancel a previously scheduled pause operation.
1556
+
1557
+ This method attempts to remove a job from the scheduler that was set to pause
1558
+ the scheduler at a specific time. If the job exists, it is removed, and a log entry
1559
+ is created to indicate the cancellation. If no such job exists, the method returns False.
1560
+
1561
+ Returns
1562
+ -------
1563
+ bool
1564
+ True if the scheduled pause job was successfully cancelled.
1565
+ False if no pause job was found or an error occurred during the cancellation process.
1566
+ """
1567
+ try:
1568
+ # Attempt to remove the pause job with the specific ID
1569
+ self.__scheduler.remove_job("scheduler_pause_at")
1570
+
1571
+ # Log the successful cancellation of the pause operation
1572
+ self.__logger.info("Scheduled pause operation cancelled.")
1573
+
1574
+ # Return True to indicate the pause job was successfully cancelled
1575
+ return True
1576
+
1577
+ except:
1578
+ # Return False if the pause job does not exist or an error occurred
1579
+ return False
1580
+
1581
+ def cancelScheduledResume(self) -> bool:
1582
+ """
1583
+ Cancel a previously scheduled resume operation.
1584
+
1585
+ This method attempts to remove a job from the scheduler that was set to resume
1586
+ the scheduler at a specific time. If the job exists, it is removed, and a log entry
1587
+ is created to indicate the cancellation. If no such job exists, the method returns False.
1588
+
1589
+ Returns
1590
+ -------
1591
+ bool
1592
+ True if the scheduled resume job was successfully cancelled.
1593
+ False if no resume job was found or an error occurred during the cancellation process.
1594
+ """
1595
+ try:
1596
+ # Attempt to remove the resume job with the specific ID
1597
+ self.__scheduler.remove_job("scheduler_resume_at")
1598
+
1599
+ # Log the successful cancellation of the resume operation
1600
+ self.__logger.info("Scheduled resume operation cancelled.")
1601
+
1602
+ # Return True to indicate the resume job was successfully cancelled
1603
+ return True
1604
+
1605
+ except:
1606
+
1607
+ # Return False if the resume job does not exist or an error occurred
1608
+ return False
1609
+
1610
+ def cancelScheduledShutdown(self) -> bool:
1611
+ """
1612
+ Cancel a previously scheduled shutdown operation.
1613
+
1614
+ This method attempts to remove a job from the scheduler that was set to shut down
1615
+ the scheduler at a specific time. If the job exists, it is removed, and a log entry
1616
+ is created to indicate the cancellation. If no such job exists, the method returns False.
1617
+
1618
+ Returns
1619
+ -------
1620
+ bool
1621
+ True if the scheduled shutdown job was successfully cancelled.
1622
+ False if no shutdown job was found or an error occurred during the cancellation process.
1623
+ """
1624
+ try:
1625
+ # Attempt to remove the shutdown job with the specific ID
1626
+ self.__scheduler.remove_job("scheduler_shutdown_at")
1627
+
1628
+ # Log the successful cancellation of the shutdown operation
1629
+ self.__logger.info("Scheduled shutdown operation cancelled.")
1630
+
1631
+ # Return True to indicate the shutdown job was successfully cancelled
1632
+ return True
1633
+
1634
+ except:
1635
+
1636
+ # Return False if the shutdown job does not exist or an error occurred
1637
+ return False
1638
+
1639
+ def isRunning(self) -> bool:
1640
+ """
1641
+ Determine if the scheduler is currently active and running.
1642
+
1643
+ This method checks the internal state of the AsyncIOScheduler instance to determine
1644
+ whether it is currently running. The scheduler is considered running if it has been
1645
+ started and has not been paused or shut down.
1646
+
1647
+ Returns
1648
+ -------
1649
+ bool
1650
+ True if the scheduler is running, False otherwise.
1651
+ """
1652
+
1653
+ # Return the running state of the scheduler
1654
+ return self.__scheduler.running
1655
+
1656
+ async def waitUntilStopped(self) -> None:
1657
+ """
1658
+ Wait for the scheduler to stop gracefully.
1659
+
1660
+ This method blocks the execution until the scheduler is stopped. It waits for the
1661
+ internal stop event to be set, which signals that the scheduler has been shut down.
1662
+ This is useful for ensuring that the scheduler completes its operations before
1663
+ proceeding with other tasks.
1664
+
1665
+ Returns
1666
+ -------
1667
+ None
1668
+ This method does not return any value. It waits until the scheduler is stopped.
1669
+ """
1670
+
1671
+ # Check if the stop event is initialized
1672
+ if self._stop_event:
1673
+
1674
+ # Wait for the stop event to be set, signaling the scheduler has stopped
1675
+ await self._stop_event.wait()
1676
+
1677
+ def forceStop(self) -> None:
1678
+ """
1679
+ Forcefully stop the scheduler immediately without waiting for jobs to complete.
1680
+
1681
+ This method shuts down the AsyncIOScheduler instance without waiting for currently
1682
+ running jobs to finish. It is intended for emergency situations where an immediate
1683
+ stop is required. The method also signals the internal stop event to ensure that
1684
+ the scheduler's main loop is interrupted and the application can proceed with
1685
+ shutdown procedures.
1686
+
1687
+ Returns
1688
+ -------
1689
+ None
1690
+ This method does not return any value. It forcefully stops the scheduler and
1691
+ signals the stop event.
1692
+ """
1693
+
1694
+ # Check if the scheduler is currently running
1695
+ if self.__scheduler.running:
1696
+ # Shut down the scheduler immediately without waiting for jobs to complete
1697
+ self.__scheduler.shutdown(wait=False)
1698
+
1699
+ # Check if the stop event exists and has not already been set
1700
+ if self._stop_event and not self._stop_event.is_set():
1701
+ # Signal the stop event to interrupt the scheduler's main loop
1702
+ self._stop_event.set()
1703
+
1704
+ def stop(self) -> None:
1705
+ """
1706
+ Stop the scheduler synchronously by setting the stop event.
1707
+
1708
+ This method signals the scheduler to stop by setting the internal stop event.
1709
+ It can be called from non-async contexts to initiate a shutdown. If the asyncio
1710
+ event loop is running, the stop event is set in a thread-safe manner. Otherwise,
1711
+ the stop event is set directly.
1712
+
1713
+ Returns
1714
+ -------
1715
+ None
1716
+ This method does not return any value. It signals the scheduler to stop.
1717
+ """
1718
+
1719
+ # Check if the stop event exists and has not already been set
1720
+ if self._stop_event and not self._stop_event.is_set():
1721
+ # Get the current asyncio event loop
1722
+ loop = asyncio.get_event_loop()
1723
+
1724
+ # If the event loop is running, set the stop event in a thread-safe manner
1725
+ if loop.is_running():
1726
+ loop.call_soon_threadsafe(self._stop_event.set)
1727
+ else:
1728
+ # Otherwise, set the stop event directly
1729
+ self._stop_event.set()
@@ -5,7 +5,7 @@
5
5
  NAME = "orionis"
6
6
 
7
7
  # Current version of the framework
8
- VERSION = "0.521.0"
8
+ VERSION = "0.522.0"
9
9
 
10
10
  # Full name of the author or maintainer of the project
11
11
  AUTHOR = "Raul Mauricio Uñate Castro"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: orionis
3
- Version: 0.521.0
3
+ Version: 0.522.0
4
4
  Summary: Orionis Framework – Elegant, Fast, and Powerful.
5
5
  Home-page: https://github.com/orionis-framework/framework
6
6
  Author: Raul Mauricio Uñate Castro
@@ -8,14 +8,14 @@ orionis/console/args/enums/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJ
8
8
  orionis/console/args/enums/actions.py,sha256=S3T-vWS6DJSGtANrq3od3-90iYAjPvJwaOZ2V02y34c,1222
9
9
  orionis/console/base/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  orionis/console/base/command.py,sha256=OM4xqVgpv_1RZVyVG8BzOHl1sP9FT5mPUwZjMil8IRg,6637
11
- orionis/console/base/scheduler.py,sha256=w86p-4KjfMqMcGlQBsmiBASpzv33M-PWLbgYza7Um9g,8030
11
+ orionis/console/base/scheduler.py,sha256=LFzWUFk07LrcpKFL7sS7exHzTkxFRd1DPDSqDSpRcOk,15157
12
12
  orionis/console/base/scheduler_event_listener.py,sha256=5qWPmf6jmiRwUz6U1ZvpQCG5eovOpeCl0KAb8kKDkfU,3905
13
13
  orionis/console/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
14
  orionis/console/commands/cache.py,sha256=8DsYoRzSBLn0P9qkGVItRbo0R6snWBDBg0_Xa7tmVhs,2322
15
15
  orionis/console/commands/help.py,sha256=zfSw0pYaOnFN-_Ozdn4veBQDYMgSSDY10nPDCi-7tTY,3199
16
16
  orionis/console/commands/publisher.py,sha256=FUg-EUzK7LLXsla10ZUZro8V0Z5S-KjmsaSdRHSSGbA,21381
17
17
  orionis/console/commands/scheduler_list.py,sha256=A2N_mEXEJDHO8DX2TDrL1ROeeRhFSkWD3rCw64Hrf0o,4763
18
- orionis/console/commands/scheduler_work.py,sha256=yHTbnDH1frAmyvPaUgn0a5q34Eym9QYMXdqYZWwodFs,6336
18
+ orionis/console/commands/scheduler_work.py,sha256=FHBQ8Ajs1zacuQFXaG-KLVRy07m9FqwyJRNVmp7cDg0,6337
19
19
  orionis/console/commands/test.py,sha256=-EmQwFwMBuby3OI9HwqMIwuJzd2CGbWbOqmwrR25sOE,2402
20
20
  orionis/console/commands/version.py,sha256=SUuNDJ40f2uq69OQUmPQXJKaa9Bm_iVRDPmBd7zc1Yc,3658
21
21
  orionis/console/commands/workflow.py,sha256=NYOmjTSvm2o6AE4h9LSTZMFSYPQreNmEJtronyOxaYk,2451
@@ -81,7 +81,7 @@ orionis/console/request/cli_request.py,sha256=7-sgYmNUCipuHLVAwWLJiHv0cJCDmsM1Lu
81
81
  orionis/console/tasks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
82
82
  orionis/console/tasks/event.py,sha256=l4J-HEPaj1mxB_PYQMgG9dRHUe01wUag8fKLLnR2N2M,164395
83
83
  orionis/console/tasks/listener.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
84
- orionis/console/tasks/schedule.py,sha256=iHy6ihs1DkpMccmQbxsgjLh0VolIEIBUCOd2SjDk7Mg,62529
84
+ orionis/console/tasks/schedule.py,sha256=Lpm_P0Brw8XHiqA-Me9SMevNlo0CzUf-rrt2PKTFW_I,72389
85
85
  orionis/container/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
86
86
  orionis/container/container.py,sha256=aF_b6lTUpG4YCo9yFJEzsntTdIzgMMXFW5LyWqAJVBQ,87987
87
87
  orionis/container/context/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -239,7 +239,7 @@ orionis/foundation/providers/scheduler_provider.py,sha256=72SoixFog9IOE9Ve9Xcfw6
239
239
  orionis/foundation/providers/testing_provider.py,sha256=SrJRpdvcblx9WvX7x9Y3zc7OQfiTf7la0HAJrm2ESlE,3725
240
240
  orionis/foundation/providers/workers_provider.py,sha256=oa_2NIDH6UxZrtuGkkoo_zEoNIMGgJ46vg5CCgAm7wI,3926
241
241
  orionis/metadata/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
242
- orionis/metadata/framework.py,sha256=-_fa2tqmC78B94qGtYCVfj_9ftofE2F8dwev3UI20JM,4109
242
+ orionis/metadata/framework.py,sha256=LNUd5w9wDJVTDwfg_BIGNlRYyyCu3UpQnvpmOQsMlIQ,4109
243
243
  orionis/metadata/package.py,sha256=k7Yriyp5aUcR-iR8SK2ec_lf0_Cyc-C7JczgXa-I67w,16039
244
244
  orionis/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
245
245
  orionis/services/asynchrony/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -415,7 +415,7 @@ orionis/test/validators/web_report.py,sha256=n9BfzOZz6aEiNTypXcwuWbFRG0OdHNSmCNu
415
415
  orionis/test/validators/workers.py,sha256=rWcdRexINNEmGaO7mnc1MKUxkHKxrTsVuHgbnIfJYgc,1206
416
416
  orionis/test/view/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
417
417
  orionis/test/view/render.py,sha256=f-zNhtKSg9R5Njqujbg2l2amAs2-mRVESneLIkWOZjU,4082
418
- orionis-0.521.0.dist-info/licenses/LICENCE,sha256=JhC-z_9mbpUrCfPjcl3DhDA8trNDMzb57cvRSam1avc,1463
418
+ orionis-0.522.0.dist-info/licenses/LICENCE,sha256=JhC-z_9mbpUrCfPjcl3DhDA8trNDMzb57cvRSam1avc,1463
419
419
  tests/container/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
420
420
  tests/container/context/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
421
421
  tests/container/context/test_manager.py,sha256=wOwXpl9rHNfTTexa9GBKYMwK0_-KSQPbI-AEyGNkmAE,1356
@@ -561,8 +561,8 @@ tests/testing/validators/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZ
561
561
  tests/testing/validators/test_testing_validators.py,sha256=WPo5GxTP6xE-Dw3X1vZoqOMpb6HhokjNSbgDsDRDvy4,16588
562
562
  tests/testing/view/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
563
563
  tests/testing/view/test_render.py,sha256=tnnMBwS0iKUIbogLvu-7Rii50G6Koddp3XT4wgdFEYM,1050
564
- orionis-0.521.0.dist-info/METADATA,sha256=dOEGgxOl4Q3GpYNTAAZEuCrr-Hs3zhoX7oIE-b2d3QA,4801
565
- orionis-0.521.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
566
- orionis-0.521.0.dist-info/top_level.txt,sha256=2bdoHgyGZhOtLAXS6Om8OCTmL24dUMC_L1quMe_ETbk,14
567
- orionis-0.521.0.dist-info/zip-safe,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
568
- orionis-0.521.0.dist-info/RECORD,,
564
+ orionis-0.522.0.dist-info/METADATA,sha256=tcvGNIB23Yo-X3d9ClTaZfP9tChX79uyxXZPstmEyhs,4801
565
+ orionis-0.522.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
566
+ orionis-0.522.0.dist-info/top_level.txt,sha256=2bdoHgyGZhOtLAXS6Om8OCTmL24dUMC_L1quMe_ETbk,14
567
+ orionis-0.522.0.dist-info/zip-safe,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
568
+ orionis-0.522.0.dist-info/RECORD,,