orionis 0.478.0__py3-none-any.whl → 0.480.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.
- orionis/console/commands/scheduler_work.py +88 -0
- orionis/console/commands/test.py +16 -9
- orionis/console/contracts/event.py +178 -0
- orionis/console/contracts/schedule.py +124 -1
- orionis/console/core/reactor.py +5 -3
- orionis/console/enums/event.py +55 -0
- orionis/console/kernel.py +1 -1
- orionis/console/tasks/event.py +332 -0
- orionis/console/tasks/exception_report.py +94 -0
- orionis/console/tasks/schedule.py +162 -266
- orionis/container/container.py +33 -22
- orionis/container/facades/facade.py +3 -2
- orionis/foundation/application.py +8 -3
- orionis/foundation/config/roots/paths.py +0 -1
- orionis/foundation/contracts/application.py +2 -2
- orionis/foundation/providers/scheduler_provider.py +51 -0
- orionis/metadata/framework.py +1 -1
- orionis/test/core/unit_test.py +2 -3
- orionis/test/kernel.py +1 -1
- {orionis-0.478.0.dist-info → orionis-0.480.0.dist-info}/METADATA +1 -1
- {orionis-0.478.0.dist-info → orionis-0.480.0.dist-info}/RECORD +26 -21
- tests/testing/test_testing_unit.py +8 -7
- orionis/console/enums/task.py +0 -42
- {orionis-0.478.0.dist-info → orionis-0.480.0.dist-info}/WHEEL +0 -0
- {orionis-0.478.0.dist-info → orionis-0.480.0.dist-info}/licenses/LICENCE +0 -0
- {orionis-0.478.0.dist-info → orionis-0.480.0.dist-info}/top_level.txt +0 -0
- {orionis-0.478.0.dist-info → orionis-0.480.0.dist-info}/zip-safe +0 -0
|
@@ -1,24 +1,26 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import logging
|
|
3
|
-
from
|
|
4
|
-
from typing import List, Optional
|
|
3
|
+
from typing import Dict, List, Optional
|
|
5
4
|
import pytz
|
|
5
|
+
from apscheduler.events import EVENT_JOB_MISSED, EVENT_JOB_ERROR
|
|
6
6
|
from apscheduler.schedulers.asyncio import AsyncIOScheduler as APSAsyncIOScheduler
|
|
7
|
-
from apscheduler.triggers.cron import CronTrigger
|
|
8
|
-
from apscheduler.triggers.date import DateTrigger
|
|
9
|
-
from apscheduler.triggers.interval import IntervalTrigger
|
|
10
|
-
from orionis.app import Orionis
|
|
11
7
|
from orionis.console.contracts.reactor import IReactor
|
|
12
|
-
from orionis.console.
|
|
8
|
+
from orionis.console.contracts.schedule import ISchedule
|
|
13
9
|
from orionis.console.exceptions import CLIOrionisRuntimeError
|
|
14
|
-
from orionis.console.
|
|
10
|
+
from orionis.console.output.contracts.console import IConsole
|
|
11
|
+
from orionis.console.tasks.event import Event
|
|
12
|
+
from orionis.console.tasks.exception_report import ScheduleErrorReporter
|
|
13
|
+
from orionis.foundation.contracts.application import IApplication
|
|
15
14
|
from orionis.services.log.contracts.log_service import ILogger
|
|
16
15
|
|
|
17
|
-
class Scheduler():
|
|
16
|
+
class Scheduler(ISchedule):
|
|
18
17
|
|
|
19
18
|
def __init__(
|
|
20
19
|
self,
|
|
21
|
-
reactor: IReactor
|
|
20
|
+
reactor: IReactor,
|
|
21
|
+
app: IApplication,
|
|
22
|
+
console: IConsole,
|
|
23
|
+
error_reporter: ScheduleErrorReporter
|
|
22
24
|
) -> None:
|
|
23
25
|
"""
|
|
24
26
|
Initialize a new instance of the Scheduler class.
|
|
@@ -41,7 +43,13 @@ class Scheduler():
|
|
|
41
43
|
"""
|
|
42
44
|
|
|
43
45
|
# Store the application instance for configuration access.
|
|
44
|
-
self.__app =
|
|
46
|
+
self.__app = app
|
|
47
|
+
|
|
48
|
+
# Store the console instance for output operations.
|
|
49
|
+
self.__console = console
|
|
50
|
+
|
|
51
|
+
# Store the error reporter instance for handling exceptions.
|
|
52
|
+
self.__error_reporter = error_reporter
|
|
45
53
|
|
|
46
54
|
# Initialize AsyncIOScheduler instance with timezone configuration.
|
|
47
55
|
self.__scheduler: APSAsyncIOScheduler = APSAsyncIOScheduler(
|
|
@@ -50,8 +58,11 @@ class Scheduler():
|
|
|
50
58
|
|
|
51
59
|
# Clear the APScheduler logger to prevent conflicts with other loggers.
|
|
52
60
|
# This is necessary to avoid duplicate log messages or conflicts with other logging configurations.
|
|
53
|
-
|
|
54
|
-
|
|
61
|
+
for name in ["apscheduler", "apscheduler.scheduler", "apscheduler.executors.default"]:
|
|
62
|
+
logger = logging.getLogger(name)
|
|
63
|
+
logger.handlers.clear()
|
|
64
|
+
logger.propagate = False
|
|
65
|
+
logger.disabled = True
|
|
55
66
|
|
|
56
67
|
# Initialize the logger from the application instance.
|
|
57
68
|
self.__logger: ILogger = self.__app.make('x-orionis.services.log.log_service')
|
|
@@ -62,16 +73,57 @@ class Scheduler():
|
|
|
62
73
|
# Retrieve and store all available commands from the reactor.
|
|
63
74
|
self.__available_commands = self.__getCommands()
|
|
64
75
|
|
|
65
|
-
#
|
|
66
|
-
self.
|
|
76
|
+
# Initialize the jobs dictionary to keep track of scheduled jobs.
|
|
77
|
+
self.__events: Dict[str, Event] = {}
|
|
67
78
|
|
|
68
|
-
#
|
|
69
|
-
self.
|
|
70
|
-
|
|
71
|
-
|
|
79
|
+
# Initialize the jobs list to keep track of all scheduled jobs.
|
|
80
|
+
self.__jobs: List[dict] = []
|
|
81
|
+
|
|
82
|
+
# Add a listener to the scheduler to capture job events such as missed jobs or errors.
|
|
83
|
+
self.__scheduler.add_listener(self.__listener, EVENT_JOB_MISSED | EVENT_JOB_ERROR)
|
|
72
84
|
|
|
73
85
|
# Log the initialization of the Scheduler.
|
|
74
|
-
self.__logger.info("
|
|
86
|
+
self.__logger.info("Orionis scheduler initialized.")
|
|
87
|
+
|
|
88
|
+
def __listener(self, event):
|
|
89
|
+
"""
|
|
90
|
+
Handle job events by logging errors and missed jobs.
|
|
91
|
+
|
|
92
|
+
This method acts as a listener for job events emitted by the scheduler. It processes
|
|
93
|
+
two main types of events: job execution errors and missed job executions. When a job
|
|
94
|
+
raises an exception during execution, the method logs the error and delegates reporting
|
|
95
|
+
to the error reporter. If a job is missed (i.e., not executed at its scheduled time),
|
|
96
|
+
the method logs a warning and notifies the error reporter accordingly.
|
|
97
|
+
|
|
98
|
+
Parameters
|
|
99
|
+
----------
|
|
100
|
+
event : apscheduler.events.JobEvent
|
|
101
|
+
The job event object containing information about the job execution, such as
|
|
102
|
+
job_id, exception, traceback, code, and scheduled_run_time.
|
|
103
|
+
|
|
104
|
+
Returns
|
|
105
|
+
-------
|
|
106
|
+
None
|
|
107
|
+
This method does not return any value. It performs logging and error reporting
|
|
108
|
+
based on the event type.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
# If the event contains an exception, log the error and report it
|
|
112
|
+
if event.exception:
|
|
113
|
+
self.__logger.error(f"Job {event.job_id} raised an exception: {event.exception}")
|
|
114
|
+
self.__error_reporter.reportException(
|
|
115
|
+
job_id=event.job_id,
|
|
116
|
+
exception=event.exception,
|
|
117
|
+
traceback=event.traceback
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# If the event indicates a missed job, log a warning and report it
|
|
121
|
+
elif event.code == EVENT_JOB_MISSED:
|
|
122
|
+
self.__logger.warning(f"Job {event.job_id} was missed, scheduled for {event.scheduled_run_time}")
|
|
123
|
+
self.__error_reporter.reportMissed(
|
|
124
|
+
job_id=event.job_id,
|
|
125
|
+
scheduled_time=event.scheduled_run_time
|
|
126
|
+
)
|
|
75
127
|
|
|
76
128
|
def __getCommands(
|
|
77
129
|
self
|
|
@@ -166,56 +218,77 @@ class Scheduler():
|
|
|
166
218
|
# Return the description if the command exists, otherwise return None
|
|
167
219
|
return command_entry['description'] if command_entry else None
|
|
168
220
|
|
|
169
|
-
def
|
|
221
|
+
def __loadEvents(
|
|
170
222
|
self
|
|
171
223
|
) -> None:
|
|
172
224
|
"""
|
|
173
|
-
|
|
225
|
+
Load all scheduled events from the AsyncIOScheduler into the internal jobs dictionary.
|
|
174
226
|
|
|
175
|
-
This method
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
scheduling without retaining any previous settings.
|
|
227
|
+
This method retrieves all jobs currently managed by the AsyncIOScheduler and populates
|
|
228
|
+
the internal jobs dictionary with their details, including signature, arguments, purpose,
|
|
229
|
+
type, trigger, start date, and end date.
|
|
179
230
|
|
|
180
231
|
Returns
|
|
181
232
|
-------
|
|
182
233
|
None
|
|
183
|
-
This method does not return any value. It
|
|
234
|
+
This method does not return any value. It updates the internal jobs dictionary.
|
|
184
235
|
"""
|
|
185
236
|
|
|
186
|
-
|
|
187
|
-
self.
|
|
188
|
-
|
|
237
|
+
# Only load events if the jobs list is empty
|
|
238
|
+
if not self.__jobs:
|
|
239
|
+
|
|
240
|
+
# Iterate through all scheduled jobs in the AsyncIOScheduler
|
|
241
|
+
for signature, event in self.__events.items():
|
|
242
|
+
|
|
243
|
+
# Convert the event to its entity representation
|
|
244
|
+
entity = event.toEntity()
|
|
245
|
+
|
|
246
|
+
# Add the job to the internal jobs list
|
|
247
|
+
self.__jobs.append(entity.toDict())
|
|
248
|
+
|
|
249
|
+
# Create a unique key for the job based on its signature
|
|
250
|
+
self.__scheduler.add_job(
|
|
251
|
+
func= lambda command=signature, args=list(entity.args): self.__reactor.call(
|
|
252
|
+
command,
|
|
253
|
+
args
|
|
254
|
+
),
|
|
255
|
+
trigger=entity.trigger,
|
|
256
|
+
id=signature,
|
|
257
|
+
name=signature,
|
|
258
|
+
replace_existing=True
|
|
259
|
+
)
|
|
189
260
|
|
|
190
261
|
def command(
|
|
191
262
|
self,
|
|
192
263
|
signature: str,
|
|
193
264
|
args: Optional[List[str]] = None
|
|
194
|
-
) -> '
|
|
265
|
+
) -> 'Event':
|
|
195
266
|
"""
|
|
196
|
-
|
|
267
|
+
Prepare an Event instance for a given command signature and its arguments.
|
|
197
268
|
|
|
198
|
-
This method validates the provided command signature and arguments,
|
|
199
|
-
|
|
200
|
-
|
|
269
|
+
This method validates the provided command signature and arguments, ensuring
|
|
270
|
+
that the command exists among the registered commands and that the arguments
|
|
271
|
+
are in the correct format. If validation passes, it creates and returns an
|
|
272
|
+
Event object representing the scheduled command, including its signature,
|
|
273
|
+
arguments, and description.
|
|
201
274
|
|
|
202
275
|
Parameters
|
|
203
276
|
----------
|
|
204
277
|
signature : str
|
|
205
278
|
The unique signature identifying the command to be scheduled. Must be a non-empty string.
|
|
206
279
|
args : Optional[List[str]], optional
|
|
207
|
-
A list of string arguments to be passed to the command.
|
|
280
|
+
A list of string arguments to be passed to the command. Defaults to None.
|
|
208
281
|
|
|
209
282
|
Returns
|
|
210
283
|
-------
|
|
211
|
-
|
|
212
|
-
|
|
284
|
+
Event
|
|
285
|
+
An Event instance containing the command signature, arguments, and its description.
|
|
213
286
|
|
|
214
287
|
Raises
|
|
215
288
|
------
|
|
216
289
|
ValueError
|
|
217
|
-
If the signature is not a non-empty string, if the arguments are not a list
|
|
218
|
-
or if the command
|
|
290
|
+
If the command signature is not a non-empty string, if the arguments are not a list
|
|
291
|
+
of strings or None, or if the command does not exist among the registered commands.
|
|
219
292
|
"""
|
|
220
293
|
|
|
221
294
|
# Validate that the command signature is a non-empty string
|
|
@@ -230,215 +303,15 @@ class Scheduler():
|
|
|
230
303
|
if not self.__isAvailable(signature):
|
|
231
304
|
raise ValueError(f"The command '{signature}' is not available or does not exist.")
|
|
232
305
|
|
|
233
|
-
# Store the command
|
|
234
|
-
self.
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
# Store the provided arguments or default to an empty list
|
|
241
|
-
self.__args = args if args is not None else []
|
|
242
|
-
|
|
243
|
-
# Return self to support method chaining
|
|
244
|
-
return self
|
|
245
|
-
|
|
246
|
-
def purpose(
|
|
247
|
-
self,
|
|
248
|
-
purpose: str
|
|
249
|
-
) -> 'Scheduler':
|
|
250
|
-
"""
|
|
251
|
-
Set the purpose or description for the scheduled command.
|
|
252
|
-
|
|
253
|
-
This method assigns a human-readable purpose or description to the command
|
|
254
|
-
that is being scheduled. The purpose must be a non-empty string. This can
|
|
255
|
-
be useful for documentation, logging, or displaying information about the
|
|
256
|
-
scheduled job.
|
|
257
|
-
|
|
258
|
-
Parameters
|
|
259
|
-
----------
|
|
260
|
-
purpose : str
|
|
261
|
-
The purpose or description to associate with the scheduled command.
|
|
262
|
-
Must be a non-empty string.
|
|
263
|
-
|
|
264
|
-
Returns
|
|
265
|
-
-------
|
|
266
|
-
Scheduler
|
|
267
|
-
Returns the current instance of the Scheduler to allow method chaining.
|
|
268
|
-
|
|
269
|
-
Raises
|
|
270
|
-
------
|
|
271
|
-
ValueError
|
|
272
|
-
If the provided purpose is not a non-empty string.
|
|
273
|
-
"""
|
|
274
|
-
|
|
275
|
-
# Validate that the purpose is a non-empty string
|
|
276
|
-
if not isinstance(purpose, str) or not purpose.strip():
|
|
277
|
-
raise ValueError("The purpose must be a non-empty string.")
|
|
278
|
-
|
|
279
|
-
# Set the internal purpose attribute
|
|
280
|
-
self.__purpose = purpose
|
|
281
|
-
|
|
282
|
-
# Return self to support method chaining
|
|
283
|
-
return self
|
|
284
|
-
|
|
285
|
-
def onceAt(
|
|
286
|
-
self,
|
|
287
|
-
date: datetime
|
|
288
|
-
) -> bool:
|
|
289
|
-
"""
|
|
290
|
-
Schedule a command to run once at a specific date and time.
|
|
291
|
-
|
|
292
|
-
This method schedules the currently registered command to execute exactly once at the
|
|
293
|
-
specified datetime using the AsyncIOScheduler. The job is registered internally and
|
|
294
|
-
added to the scheduler instance.
|
|
295
|
-
|
|
296
|
-
Parameters
|
|
297
|
-
----------
|
|
298
|
-
date : datetime.datetime
|
|
299
|
-
The date and time at which the command should be executed. Must be a valid `datetime` instance.
|
|
300
|
-
|
|
301
|
-
Returns
|
|
302
|
-
-------
|
|
303
|
-
bool
|
|
304
|
-
Returns True if the job was successfully scheduled.
|
|
305
|
-
|
|
306
|
-
Raises
|
|
307
|
-
------
|
|
308
|
-
CLIOrionisRuntimeError
|
|
309
|
-
If the provided date is not a `datetime` instance or if there is an error while scheduling the job.
|
|
310
|
-
"""
|
|
311
|
-
|
|
312
|
-
try:
|
|
313
|
-
|
|
314
|
-
# Ensure the provided date is a valid datetime instance.
|
|
315
|
-
if not isinstance(date, datetime):
|
|
316
|
-
raise CLIOrionisRuntimeError(
|
|
317
|
-
"The date must be an instance of datetime."
|
|
318
|
-
)
|
|
319
|
-
|
|
320
|
-
# Register the job details internally.
|
|
321
|
-
self.__jobs[self.__command] = Task(
|
|
322
|
-
signature=self.__command,
|
|
323
|
-
args=self.__args,
|
|
324
|
-
purpose=self.__purpose,
|
|
325
|
-
trigger='once_at',
|
|
326
|
-
details=f"Date: {date.strftime('%Y-%m-%d %H:%M:%S')}, Timezone: {str(date.tzinfo) if date.tzinfo else 'UTC'}"
|
|
327
|
-
)
|
|
328
|
-
|
|
329
|
-
# Add the job to the scheduler.
|
|
330
|
-
self.__scheduler.add_job(
|
|
331
|
-
func= lambda command=self.__command, args=list(self.__args): self.__reactor.call(
|
|
332
|
-
command,
|
|
333
|
-
args
|
|
334
|
-
),
|
|
335
|
-
trigger=DateTrigger(
|
|
336
|
-
run_date=date
|
|
337
|
-
),
|
|
338
|
-
id=self.__command,
|
|
339
|
-
name=self.__command,
|
|
340
|
-
replace_existing=True
|
|
341
|
-
)
|
|
342
|
-
|
|
343
|
-
# Log the scheduling of the command.
|
|
344
|
-
self.__logger.info(
|
|
345
|
-
f"Scheduled command '{self.__command}' to run once at {date.strftime('%Y-%m-%d %H:%M:%S')}"
|
|
346
|
-
)
|
|
347
|
-
|
|
348
|
-
# Reset the internal state for future scheduling.
|
|
349
|
-
self.__reset()
|
|
350
|
-
|
|
351
|
-
# Return True to indicate successful scheduling.
|
|
352
|
-
return True
|
|
353
|
-
|
|
354
|
-
except Exception as e:
|
|
355
|
-
|
|
356
|
-
# Reraise known CLIOrionisRuntimeError exceptions.
|
|
357
|
-
if isinstance(e, CLIOrionisRuntimeError):
|
|
358
|
-
raise e
|
|
359
|
-
|
|
360
|
-
# Wrap and raise any other exceptions as CLIOrionisRuntimeError.
|
|
361
|
-
raise CLIOrionisRuntimeError(f"Error scheduling the job: {str(e)}")
|
|
362
|
-
|
|
363
|
-
def everySeconds(
|
|
364
|
-
self,
|
|
365
|
-
seconds: int,
|
|
366
|
-
start_date: Optional[datetime] = None,
|
|
367
|
-
end_date: Optional[datetime] = None
|
|
368
|
-
) -> 'Scheduler':
|
|
369
|
-
"""
|
|
370
|
-
Schedule a command to run at regular intervals in seconds.
|
|
371
|
-
|
|
372
|
-
This method sets the interval for the currently registered command to execute
|
|
373
|
-
every specified number of seconds. The job is registered internally and added
|
|
374
|
-
to the scheduler instance.
|
|
375
|
-
|
|
376
|
-
Parameters
|
|
377
|
-
----------
|
|
378
|
-
seconds : int
|
|
379
|
-
The interval in seconds at which the command should be executed. Must be a positive integer.
|
|
380
|
-
|
|
381
|
-
Returns
|
|
382
|
-
-------
|
|
383
|
-
Scheduler
|
|
384
|
-
Returns the current instance of the Scheduler to allow method chaining.
|
|
385
|
-
|
|
386
|
-
Raises
|
|
387
|
-
------
|
|
388
|
-
ValueError
|
|
389
|
-
If the provided seconds is not a positive integer.
|
|
390
|
-
"""
|
|
391
|
-
|
|
392
|
-
try:
|
|
393
|
-
|
|
394
|
-
# Validate that the seconds parameter is a positive integer.
|
|
395
|
-
if not isinstance(seconds, int) or seconds <= 0:
|
|
396
|
-
raise CLIOrionisValueError("The interval must be a positive integer.")
|
|
397
|
-
|
|
398
|
-
# If start_date is provided, ensure it is a datetime instance.
|
|
399
|
-
if start_date is not None and not isinstance(start_date, datetime):
|
|
400
|
-
raise CLIOrionisValueError("Start date must be a datetime instance.")
|
|
401
|
-
|
|
402
|
-
# If end_date is provided, ensure it is a datetime instance.
|
|
403
|
-
if end_date is not None and not isinstance(end_date, datetime):
|
|
404
|
-
raise CLIOrionisValueError("End date must be a datetime instance.")
|
|
405
|
-
|
|
406
|
-
# Store the interval in the jobs dictionary.
|
|
407
|
-
self.__jobs[self.__command] = Task(
|
|
408
|
-
signature=self.__command,
|
|
409
|
-
args=self.__args,
|
|
410
|
-
purpose=self.__purpose,
|
|
411
|
-
trigger='every_seconds',
|
|
412
|
-
details=f"Interval: {seconds} seconds, Start Date: {start_date.strftime('%Y-%m-%d %H:%M:%S') if start_date else 'None'}, End Date: {end_date.strftime('%Y-%m-%d %H:%M:%S') if end_date else 'None'}"
|
|
413
|
-
)
|
|
414
|
-
|
|
415
|
-
# Add the job to the scheduler with an interval trigger.
|
|
416
|
-
self.__scheduler.add_job(
|
|
417
|
-
func=lambda command=self.__command, args=list(self.__args): self.__reactor.call(
|
|
418
|
-
command,
|
|
419
|
-
args
|
|
420
|
-
),
|
|
421
|
-
trigger=IntervalTrigger(
|
|
422
|
-
seconds=seconds,
|
|
423
|
-
start_date=start_date,
|
|
424
|
-
end_date=end_date
|
|
425
|
-
),
|
|
426
|
-
id=self.__command,
|
|
427
|
-
name=self.__command,
|
|
428
|
-
replace_existing=True
|
|
429
|
-
)
|
|
430
|
-
|
|
431
|
-
# Return self to support method chaining.
|
|
432
|
-
return self
|
|
433
|
-
|
|
434
|
-
except Exception as e:
|
|
435
|
-
|
|
436
|
-
# Reraise known CLIOrionisValueError exceptions.
|
|
437
|
-
if isinstance(e, CLIOrionisValueError):
|
|
438
|
-
raise e
|
|
306
|
+
# Store the command and its arguments for scheduling
|
|
307
|
+
self.__events[signature] = Event(
|
|
308
|
+
signature=signature,
|
|
309
|
+
args=args or [],
|
|
310
|
+
purpose=self.__getDescription(signature)
|
|
311
|
+
)
|
|
439
312
|
|
|
440
|
-
|
|
441
|
-
|
|
313
|
+
# Return the Event instance for further scheduling configuration
|
|
314
|
+
return self.__events[signature]
|
|
442
315
|
|
|
443
316
|
async def start(self) -> None:
|
|
444
317
|
"""
|
|
@@ -457,6 +330,9 @@ class Scheduler():
|
|
|
457
330
|
# Start the AsyncIOScheduler to handle asynchronous jobs.
|
|
458
331
|
try:
|
|
459
332
|
|
|
333
|
+
# Load existing events into the scheduler
|
|
334
|
+
self.events()
|
|
335
|
+
|
|
460
336
|
# Ensure we're in an asyncio context
|
|
461
337
|
asyncio.get_running_loop()
|
|
462
338
|
|
|
@@ -467,14 +343,10 @@ class Scheduler():
|
|
|
467
343
|
|
|
468
344
|
# Keep the event loop alive to process scheduled jobs
|
|
469
345
|
try:
|
|
470
|
-
|
|
471
|
-
# Wait for the scheduler to start and keep it running
|
|
472
|
-
while True:
|
|
346
|
+
while self.__scheduler.running and self.__scheduler.get_jobs():
|
|
473
347
|
await asyncio.sleep(1)
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
# Handle graceful shutdown on keyboard interrupt
|
|
348
|
+
except (KeyboardInterrupt, asyncio.CancelledError):
|
|
349
|
+
# Handle graceful shutdown on keyboard interrupt or cancellation
|
|
478
350
|
await self.shutdown()
|
|
479
351
|
|
|
480
352
|
except Exception as e:
|
|
@@ -577,22 +449,46 @@ class Scheduler():
|
|
|
577
449
|
# Job not found or other error
|
|
578
450
|
return False
|
|
579
451
|
|
|
580
|
-
def
|
|
452
|
+
def events(self) -> list:
|
|
581
453
|
"""
|
|
582
454
|
Retrieve all scheduled jobs currently managed by the Scheduler.
|
|
583
455
|
|
|
584
|
-
This method returns a
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
dictionary with details such as the signature, arguments, purpose, type, trigger, start time,
|
|
588
|
-
and end time.
|
|
456
|
+
This method loads and returns a list of dictionaries, each representing a scheduled job
|
|
457
|
+
managed by this Scheduler instance. Each dictionary contains details such as the command
|
|
458
|
+
signature, arguments, purpose, random delay, start and end dates, and additional job details.
|
|
589
459
|
|
|
590
460
|
Returns
|
|
591
461
|
-------
|
|
592
|
-
dict
|
|
593
|
-
A
|
|
594
|
-
|
|
462
|
+
list of dict
|
|
463
|
+
A list where each element is a dictionary containing information about a scheduled job.
|
|
464
|
+
Each dictionary includes the following keys:
|
|
465
|
+
- 'signature': str, the command signature.
|
|
466
|
+
- 'args': list, the arguments passed to the command.
|
|
467
|
+
- 'purpose': str, the description or purpose of the job.
|
|
468
|
+
- 'random_delay': any, the random delay associated with the job (if any).
|
|
469
|
+
- 'start_date': str or None, the formatted start date and time of the job, or None if not set.
|
|
470
|
+
- 'end_date': str or None, the formatted end date and time of the job, or None if not set.
|
|
471
|
+
- 'details': any, additional details about the job.
|
|
595
472
|
"""
|
|
596
473
|
|
|
597
|
-
#
|
|
598
|
-
|
|
474
|
+
# Ensure all events are loaded into the internal jobs list
|
|
475
|
+
self.__loadEvents()
|
|
476
|
+
|
|
477
|
+
# Initialize a list to hold details of each scheduled job
|
|
478
|
+
events: list = []
|
|
479
|
+
|
|
480
|
+
# Iterate over each job in the internal jobs list
|
|
481
|
+
for job in self.__jobs:
|
|
482
|
+
# Append a dictionary with relevant job details to the events list
|
|
483
|
+
events.append({
|
|
484
|
+
'signature': job['signature'],
|
|
485
|
+
'args': job['args'],
|
|
486
|
+
'purpose': job['purpose'],
|
|
487
|
+
'random_delay': job['random_delay'],
|
|
488
|
+
'start_date': job['start_date'].strftime('%Y-%m-%d %H:%M:%S') if job['start_date'] else None,
|
|
489
|
+
'end_date': job['end_date'].strftime('%Y-%m-%d %H:%M:%S') if job['end_date'] else None,
|
|
490
|
+
'details': job['details']
|
|
491
|
+
})
|
|
492
|
+
|
|
493
|
+
# Return the list of scheduled job details
|
|
494
|
+
return events
|
orionis/container/container.py
CHANGED
|
@@ -511,35 +511,46 @@ class Container(IContainer):
|
|
|
511
511
|
registered in the container under both the abstract and the alias.
|
|
512
512
|
"""
|
|
513
513
|
|
|
514
|
-
#
|
|
515
|
-
|
|
514
|
+
# Validate the enforce_decoupling parameter
|
|
515
|
+
if isinstance(enforce_decoupling, bool):
|
|
516
516
|
|
|
517
|
-
|
|
518
|
-
|
|
517
|
+
# Ensure that the abstract is an abstract class
|
|
518
|
+
IsAbstractClass(abstract, f"Instance {Lifetime.SINGLETON}")
|
|
519
519
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
IsNotSubclass(abstract, instance.__class__)
|
|
520
|
+
# Ensure that the instance is a valid instance
|
|
521
|
+
IsInstance(instance)
|
|
523
522
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
523
|
+
# Ensure that instance is NOT a subclass of abstract
|
|
524
|
+
if enforce_decoupling:
|
|
525
|
+
IsNotSubclass(abstract, instance.__class__)
|
|
527
526
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
527
|
+
# Validate that instance is a subclass of abstract
|
|
528
|
+
else:
|
|
529
|
+
IsSubclass(abstract, instance.__class__)
|
|
530
|
+
|
|
531
|
+
# Ensure implementation
|
|
532
|
+
ImplementsAbstractMethods(
|
|
533
|
+
abstract=abstract,
|
|
534
|
+
instance=instance
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
# Ensure that the alias is a valid string if provided
|
|
538
|
+
if alias:
|
|
539
|
+
IsValidAlias(alias)
|
|
540
|
+
else:
|
|
541
|
+
rf_asbtract = ReflectionAbstract(abstract)
|
|
542
|
+
alias = rf_asbtract.getModuleWithClassName()
|
|
543
|
+
|
|
544
|
+
# If the service is already registered, drop it
|
|
545
|
+
self.drop(abstract, alias)
|
|
533
546
|
|
|
534
|
-
# Ensure that the alias is a valid string if provided
|
|
535
|
-
if alias:
|
|
536
|
-
IsValidAlias(alias)
|
|
537
547
|
else:
|
|
538
|
-
rf_asbtract = ReflectionAbstract(abstract)
|
|
539
|
-
alias = rf_asbtract.getModuleWithClassName()
|
|
540
548
|
|
|
541
|
-
|
|
542
|
-
|
|
549
|
+
# Drop the existing alias if it exists
|
|
550
|
+
self.drop(alias=alias)
|
|
551
|
+
|
|
552
|
+
# If enforce_decoupling is not a boolean, set it to False
|
|
553
|
+
enforce_decoupling = False
|
|
543
554
|
|
|
544
555
|
# Register the instance with the abstract type
|
|
545
556
|
self.__bindings[abstract] = Binding(
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from typing import Any
|
|
2
|
+
from orionis.app import Orionis
|
|
2
3
|
from orionis.container.exceptions import OrionisContainerAttributeError, OrionisContainerException
|
|
3
|
-
from orionis.foundation.application import
|
|
4
|
+
from orionis.foundation.application import IApplication
|
|
4
5
|
from typing import TypeVar, Any
|
|
5
6
|
|
|
6
7
|
T = TypeVar('T')
|
|
@@ -34,7 +35,7 @@ class FacadeMeta(type):
|
|
|
34
35
|
class Facade(metaclass=FacadeMeta):
|
|
35
36
|
|
|
36
37
|
# Application instance to resolve services
|
|
37
|
-
_app: IApplication =
|
|
38
|
+
_app: IApplication = Orionis()
|
|
38
39
|
|
|
39
40
|
@classmethod
|
|
40
41
|
def getFacadeAccessor(cls) -> str:
|
|
@@ -181,6 +181,7 @@ class Application(Container, IApplication):
|
|
|
181
181
|
from orionis.foundation.providers.executor_provider import ConsoleExecuteProvider
|
|
182
182
|
from orionis.foundation.providers.reactor_provider import ReactorProvider
|
|
183
183
|
from orionis.foundation.providers.performance_counter_provider import PerformanceCounterProvider
|
|
184
|
+
from orionis.foundation.providers.scheduler_provider import ScheduleProvider
|
|
184
185
|
|
|
185
186
|
# Core framework providers
|
|
186
187
|
core_providers = [
|
|
@@ -193,7 +194,8 @@ class Application(Container, IApplication):
|
|
|
193
194
|
InspirationalProvider,
|
|
194
195
|
ConsoleExecuteProvider,
|
|
195
196
|
ReactorProvider,
|
|
196
|
-
PerformanceCounterProvider
|
|
197
|
+
PerformanceCounterProvider,
|
|
198
|
+
ScheduleProvider
|
|
197
199
|
]
|
|
198
200
|
|
|
199
201
|
# Register each core provider
|
|
@@ -1812,7 +1814,7 @@ class Application(Container, IApplication):
|
|
|
1812
1814
|
self,
|
|
1813
1815
|
key: str = None,
|
|
1814
1816
|
default: Any = None
|
|
1815
|
-
) ->
|
|
1817
|
+
) -> str:
|
|
1816
1818
|
"""
|
|
1817
1819
|
Retrieve application path configuration values using dot notation.
|
|
1818
1820
|
|
|
@@ -1832,7 +1834,7 @@ class Application(Container, IApplication):
|
|
|
1832
1834
|
|
|
1833
1835
|
Returns
|
|
1834
1836
|
-------
|
|
1835
|
-
|
|
1837
|
+
str
|
|
1836
1838
|
The path configuration value corresponding to the given key, the entire
|
|
1837
1839
|
paths dictionary if key is None, or the default value if the key is
|
|
1838
1840
|
not found.
|
|
@@ -1923,6 +1925,9 @@ class Application(Container, IApplication):
|
|
|
1923
1925
|
# Check if already booted
|
|
1924
1926
|
if not self.__booted:
|
|
1925
1927
|
|
|
1928
|
+
# Register the application instance in the container
|
|
1929
|
+
self.instance(IApplication, self, alias="x-orionis.foundation.application", enforce_decoupling='X-ORIONIS')
|
|
1930
|
+
|
|
1926
1931
|
# Load configuration if not already set
|
|
1927
1932
|
self.__loadConfig()
|
|
1928
1933
|
|