OpenOrchestrator 1.3.1__py3-none-any.whl → 2.0.0rc2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. OpenOrchestrator/__main__.py +66 -12
  2. OpenOrchestrator/common/connection_frame.py +10 -4
  3. OpenOrchestrator/database/base.py +8 -0
  4. OpenOrchestrator/database/constants.py +3 -15
  5. OpenOrchestrator/database/db_util.py +110 -37
  6. OpenOrchestrator/database/logs.py +3 -15
  7. OpenOrchestrator/database/queues.py +3 -15
  8. OpenOrchestrator/database/schedulers.py +32 -0
  9. OpenOrchestrator/database/triggers.py +7 -16
  10. OpenOrchestrator/orchestrator/application.py +13 -8
  11. OpenOrchestrator/orchestrator/datetime_input.py +2 -2
  12. OpenOrchestrator/orchestrator/popups/constant_popup.py +8 -6
  13. OpenOrchestrator/orchestrator/popups/credential_popup.py +10 -8
  14. OpenOrchestrator/orchestrator/popups/generic_popups.py +15 -2
  15. OpenOrchestrator/orchestrator/popups/trigger_popup.py +25 -14
  16. OpenOrchestrator/orchestrator/tabs/constants_tab.py +5 -2
  17. OpenOrchestrator/orchestrator/tabs/logging_tab.py +3 -0
  18. OpenOrchestrator/orchestrator/tabs/queue_tab.py +4 -1
  19. OpenOrchestrator/orchestrator/tabs/schedulers_tab.py +45 -0
  20. OpenOrchestrator/orchestrator/tabs/settings_tab.py +2 -5
  21. OpenOrchestrator/orchestrator/tabs/trigger_tab.py +9 -5
  22. OpenOrchestrator/orchestrator/test_helper.py +17 -0
  23. OpenOrchestrator/orchestrator_connection/connection.py +21 -4
  24. OpenOrchestrator/scheduler/application.py +3 -2
  25. OpenOrchestrator/scheduler/run_tab.py +16 -6
  26. OpenOrchestrator/scheduler/runner.py +52 -64
  27. OpenOrchestrator/scheduler/settings_tab.py +90 -14
  28. OpenOrchestrator/scheduler/util.py +8 -0
  29. OpenOrchestrator/tests/__init__.py +0 -0
  30. OpenOrchestrator/tests/db_test_util.py +40 -0
  31. OpenOrchestrator/tests/test_db_util.py +372 -0
  32. OpenOrchestrator/tests/test_orchestrator_connection.py +142 -0
  33. OpenOrchestrator/tests/test_trigger_polling.py +143 -0
  34. OpenOrchestrator/tests/ui_tests/__init__.py +0 -0
  35. OpenOrchestrator/tests/ui_tests/test_constants_tab.py +167 -0
  36. OpenOrchestrator/tests/ui_tests/test_logging_tab.py +180 -0
  37. OpenOrchestrator/tests/ui_tests/test_queues_tab.py +126 -0
  38. OpenOrchestrator/tests/ui_tests/test_schedulers_tab.py +47 -0
  39. OpenOrchestrator/tests/ui_tests/test_trigger_tab.py +243 -0
  40. OpenOrchestrator/tests/ui_tests/ui_util.py +151 -0
  41. openorchestrator-2.0.0rc2.dist-info/METADATA +158 -0
  42. openorchestrator-2.0.0rc2.dist-info/RECORD +55 -0
  43. {openorchestrator-1.3.1.dist-info → openorchestrator-2.0.0rc2.dist-info}/WHEEL +1 -1
  44. OpenOrchestrator/scheduler/connection_frame.py +0 -96
  45. openorchestrator-1.3.1.dist-info/METADATA +0 -60
  46. openorchestrator-1.3.1.dist-info/RECORD +0 -39
  47. {openorchestrator-1.3.1.dist-info → openorchestrator-2.0.0rc2.dist-info/licenses}/LICENSE +0 -0
  48. {openorchestrator-1.3.1.dist-info → openorchestrator-2.0.0rc2.dist-info}/top_level.txt +0 -0
@@ -1,20 +1,74 @@
1
- """This module is used to run Orchestrator or Scheduler from the command line.
2
- Usage: python -m OpenOrchestrator [-o|-s]"""
1
+ """This module is used to run Orchestrator or Scheduler from the command line."""
2
+
3
+ import argparse
4
+ import subprocess
3
5
 
4
- import sys
5
6
  from OpenOrchestrator.scheduler.application import Application as s_app
6
7
  from OpenOrchestrator.orchestrator.application import Application as o_app
7
8
 
8
9
 
9
- def _print_usage():
10
- print("Usage: -o to start Orchestrator. -s to start Scheduler")
10
+ def main():
11
+ """The main entry point of the CLI."""
12
+ parser = argparse.ArgumentParser(
13
+ prog="OpenOrchestrator",
14
+ description="OpenOrchestrator is used to orchestrate, monitor and run Python-based automation scripts in Windows."
15
+ )
16
+
17
+ subparsers = parser.add_subparsers(title="Subcommands", required=True)
18
+
19
+ o_parser = subparsers.add_parser("orchestrator", aliases=["o"], help="Start the Orchestrator application.")
20
+ o_parser.add_argument("-p", "--port", type=int, help="Set the desired port for Orchestrator.")
21
+ o_parser.add_argument("-d", "--dont_show", action="store_false", help="Set if you don't want Orchestrator to open in the browser automatically.")
22
+ o_parser.set_defaults(func=orchestrator_command)
23
+
24
+ s_parser = subparsers.add_parser("scheduler", aliases=["s"], help="Start the Scheduler application.")
25
+ s_parser.set_defaults(func=scheduler_command)
26
+
27
+ u_parser = subparsers.add_parser("upgrade", aliases=["u"], help="Upgrade the database to the newest revision or create a new database from scratch.")
28
+ u_parser.add_argument("connection_string", type=str, help="The connection string to the database")
29
+ u_parser.set_defaults(func=upgrade_command)
30
+
31
+ args = parser.parse_args()
32
+ args.func(args)
33
+
34
+
35
+ def orchestrator_command(args: argparse.Namespace):
36
+ """Start the Orchestrator app.
11
37
 
38
+ Args:
39
+ args: The arguments Namespace object.
40
+ """
41
+ o_app(port=args.port, show=args.dont_show)
12
42
 
13
- if len(sys.argv) != 2:
14
- _print_usage()
15
- elif sys.argv[1] == '-s':
43
+
44
+ def scheduler_command(args: argparse.Namespace): # pylint: disable=unused-argument
45
+ """Start the Scheduler app."""
16
46
  s_app()
17
- elif sys.argv[1] == '-o':
18
- o_app()
19
- else:
20
- _print_usage()
47
+
48
+
49
+ def upgrade_command(args: argparse.Namespace):
50
+ """Upgrade the database to the newest revision using alembic.
51
+
52
+ Args:
53
+ args: The arguments Namespace object.
54
+ """
55
+ confirmation = input("Are you sure you want to upgrade the database to the newest revision? This cannot be undone. (y/n)").strip()
56
+ if confirmation == "y":
57
+ # Check if database is already stamped
58
+ result = subprocess.run(["alembic", "-x", args.connection_string, "current"], check=True, capture_output=True)
59
+ if result.stdout == b'':
60
+ response = input("It seems like you are either creating a new database or trying to upgrade a legacy database. Please choose an option: (new/legacy/cancel)").strip()
61
+ if response == "legacy":
62
+ subprocess.run(["alembic", "-x", args.connection_string, "stamp", "b67b7649b282"], check=True)
63
+ elif response != "new":
64
+ print("Upgrade canceled")
65
+ return
66
+
67
+ subprocess.run(["alembic", "-x", args.connection_string, "upgrade", "head"], check=True)
68
+ print("Database upgraded to the newest revision!")
69
+ else:
70
+ print("Upgrade canceled")
71
+
72
+
73
+ if __name__ == '__main__':
74
+ main()
@@ -6,6 +6,8 @@ from nicegui import ui
6
6
 
7
7
  from OpenOrchestrator.common import crypto_util
8
8
  from OpenOrchestrator.database import db_util
9
+ from OpenOrchestrator.orchestrator import test_helper
10
+ from OpenOrchestrator.orchestrator.popups import generic_popups
9
11
 
10
12
 
11
13
  # pylint: disable-next=too-few-public-methods
@@ -21,12 +23,13 @@ class ConnectionFrame():
21
23
  self.disconn_button.disable()
22
24
 
23
25
  self._initial_connect()
26
+ test_helper.set_automation_ids(self, "connection_frame")
24
27
 
25
28
  def _define_validation(self):
26
- self.conn_input.validation = {"Please enter a connection string": bool}
27
- self.key_input.validation = {"Invalid AES key": crypto_util.validate_key}
29
+ self.conn_input._validation = {"Please enter a connection string": bool} # pylint: disable=protected-access
30
+ self.key_input._validation = {"Invalid AES key": crypto_util.validate_key} # pylint: disable=protected-access
28
31
 
29
- def _connect(self) -> None:
32
+ async def _connect(self) -> None:
30
33
  """Validate the connection string and encryption key
31
34
  and connect to the database.
32
35
  """
@@ -40,7 +43,10 @@ class ConnectionFrame():
40
43
  if db_util.connect(conn_string):
41
44
  crypto_util.set_key(crypto_key)
42
45
  self._set_state(True)
43
- ui.notify("Connected!", type='positive')
46
+ ui.notify("Connected!", type='positive', timeout=1000)
47
+
48
+ if not os.getenv("ORCHESTRATOR_TEST") and not db_util.check_database_revision():
49
+ await generic_popups.info_popup("This version of Orchestrator doesn't match the version of the connected database. Unexpected errors might occur.")
44
50
 
45
51
  def _disconnect(self) -> None:
46
52
  db_util.disconnect()
@@ -0,0 +1,8 @@
1
+ """Contains the Base class for all ORM classes"""
2
+
3
+ from sqlalchemy.orm import DeclarativeBase
4
+
5
+
6
+ # pylint: disable=too-few-public-methods
7
+ class Base(DeclarativeBase):
8
+ """SqlAlchemy base class for all ORM classes in this project."""
@@ -2,19 +2,16 @@
2
2
 
3
3
  from datetime import datetime
4
4
 
5
- from sqlalchemy import String, Engine
6
- from sqlalchemy.orm import Mapped, DeclarativeBase, mapped_column
5
+ from sqlalchemy import String
6
+ from sqlalchemy.orm import Mapped, mapped_column
7
7
 
8
8
  from OpenOrchestrator.common import datetime_util
9
+ from OpenOrchestrator.database.base import Base
9
10
 
10
11
  # All classes in this module are effectively dataclasses without methods.
11
12
  # pylint: disable=too-few-public-methods
12
13
 
13
14
 
14
- class Base(DeclarativeBase):
15
- """SqlAlchemy base class for all ORM classes in this module."""
16
-
17
-
18
15
  class Constant(Base):
19
16
  """A class representing a constant object in the ORM."""
20
17
  __tablename__ = "Constants"
@@ -56,12 +53,3 @@ class Credential(Base):
56
53
  lower_length = int(((length-100)/20)*16)
57
54
  upper_length = lower_length + 15
58
55
  return f"{length} encrypted bytes. {lower_length}-{upper_length} decrypted bytes."
59
-
60
-
61
- def create_tables(engine: Engine):
62
- """Create all SQL tables related to ORM classes in this module.
63
-
64
- Args:
65
- engine: The SqlAlchemy connection engine used to create the tables.
66
- """
67
- Base.metadata.create_all(engine)
@@ -3,19 +3,21 @@
3
3
  from datetime import datetime
4
4
  from typing import TypeVar, ParamSpec
5
5
  from uuid import UUID
6
+ import json
6
7
 
7
8
  from cronsim import CronSim
8
- from sqlalchemy import Engine, create_engine, select, insert, desc
9
+ from sqlalchemy import Engine, create_engine, select, insert, desc, text
10
+
9
11
  from sqlalchemy import exc as alc_exc
10
12
  from sqlalchemy import func as alc_func
11
13
  from sqlalchemy.orm import Session, selectin_polymorphic
12
14
 
13
15
  from OpenOrchestrator.common import crypto_util
14
- from OpenOrchestrator.database import logs, triggers, constants, queues
15
16
  from OpenOrchestrator.database.logs import Log, LogLevel
16
17
  from OpenOrchestrator.database.constants import Constant, Credential
17
18
  from OpenOrchestrator.database.triggers import Trigger, SingleTrigger, ScheduledTrigger, QueueTrigger, TriggerStatus
18
19
  from OpenOrchestrator.database.queues import QueueElement, QueueStatus
20
+ from OpenOrchestrator.database.schedulers import Scheduler
19
21
  from OpenOrchestrator.database.truncated_string import truncate_message
20
22
 
21
23
  # Type hint helpers for decorators
@@ -55,6 +57,19 @@ def disconnect() -> None:
55
57
  _connection_engine = None
56
58
 
57
59
 
60
+ def check_database_revision() -> bool:
61
+ """Check if the revision number of the connected database matches the expected revision."""
62
+ try:
63
+ with _get_session() as session:
64
+ version = session.execute(text(
65
+ """SELECT version_num FROM alembic_version"""
66
+ )).scalar()
67
+ except alc_exc.ProgrammingError:
68
+ return False
69
+
70
+ return version == "90d46abd44a3"
71
+
72
+
58
73
  def _get_session() -> Session:
59
74
  """Check if theres a database connection and return a
60
75
  session to it.
@@ -83,17 +98,6 @@ def get_conn_string() -> str:
83
98
  return str(_connection_engine.url)
84
99
 
85
100
 
86
- def initialize_database() -> None:
87
- """Initializes the database with all the needed tables."""
88
- if not _connection_engine:
89
- raise RuntimeError("Not connected to database.")
90
-
91
- logs.create_tables(_connection_engine)
92
- triggers.create_tables(_connection_engine)
93
- constants.create_tables(_connection_engine)
94
- queues.create_tables(_connection_engine)
95
-
96
-
97
101
  def get_trigger(trigger_id: UUID | str) -> Trigger:
98
102
  """Get the trigger with the given id.
99
103
 
@@ -276,8 +280,10 @@ def get_unique_log_process_names() -> tuple[str, ...]:
276
280
  return tuple(result)
277
281
 
278
282
 
283
+ # pylint: disable=too-many-positional-arguments
279
284
  def create_single_trigger(trigger_name: str, process_name: str, next_run: datetime,
280
- process_path: str, process_args: str, is_git_repo: bool, is_blocking: bool) -> None:
285
+ process_path: str, process_args: str, is_git_repo: bool, is_blocking: bool,
286
+ priority: int, scheduler_whitelist: list[str]) -> str:
281
287
  """Create a new single trigger in the database.
282
288
 
283
289
  Args:
@@ -288,6 +294,11 @@ def create_single_trigger(trigger_name: str, process_name: str, next_run: dateti
288
294
  process_args: The argument string of the process.
289
295
  is_git_repo: If the process_path points to a git repo.
290
296
  is_blocking: If the process should be blocking.
297
+ priority: The integer priority of the trigger.
298
+ cheduler_whitelist: A list of names of schedulers the trigger may run on.
299
+
300
+ Returns:
301
+ The id of the trigger that was created.
291
302
  """
292
303
  with _get_session() as session:
293
304
  trigger = SingleTrigger(
@@ -297,15 +308,19 @@ def create_single_trigger(trigger_name: str, process_name: str, next_run: dateti
297
308
  process_args = process_args,
298
309
  is_git_repo = is_git_repo,
299
310
  is_blocking = is_blocking,
300
- next_run = next_run
311
+ next_run = next_run,
312
+ priority=priority,
313
+ scheduler_whitelist=json.dumps(scheduler_whitelist)
301
314
  )
302
315
  session.add(trigger)
303
316
  session.commit()
317
+ return trigger.id
304
318
 
305
319
 
320
+ # pylint: disable=too-many-positional-arguments
306
321
  def create_scheduled_trigger(trigger_name: str, process_name: str, cron_expr: str, next_run: datetime,
307
322
  process_path: str, process_args: str, is_git_repo: bool,
308
- is_blocking: bool) -> None:
323
+ is_blocking: bool, priority: int, scheduler_whitelist: list[str]) -> str:
309
324
  """Create a new scheduled trigger in the database.
310
325
 
311
326
  Args:
@@ -317,6 +332,10 @@ def create_scheduled_trigger(trigger_name: str, process_name: str, cron_expr: st
317
332
  process_args: The argument string of the process.
318
333
  is_git_repo: If the process_path points to a git repo.
319
334
  is_blocking: If the process should be blocking.
335
+ priority: The integer priority of the trigger.
336
+ scheduler_whitelist: A list of names of schedulers the trigger may run on.
337
+ Returns:
338
+ The id of the trigger that was created.
320
339
  """
321
340
  with _get_session() as session:
322
341
  trigger = ScheduledTrigger(
@@ -327,15 +346,19 @@ def create_scheduled_trigger(trigger_name: str, process_name: str, cron_expr: st
327
346
  is_git_repo = is_git_repo,
328
347
  is_blocking = is_blocking,
329
348
  next_run = next_run,
330
- cron_expr = cron_expr
349
+ cron_expr = cron_expr,
350
+ priority=priority,
351
+ scheduler_whitelist=json.dumps(scheduler_whitelist)
331
352
  )
332
353
  session.add(trigger)
333
354
  session.commit()
355
+ return trigger.id
334
356
 
335
357
 
358
+ # pylint: disable=too-many-positional-arguments
336
359
  def create_queue_trigger(trigger_name: str, process_name: str, queue_name: str, process_path: str,
337
360
  process_args: str, is_git_repo: bool, is_blocking: bool,
338
- min_batch_size: int) -> None:
361
+ min_batch_size: int, priority: int, scheduler_whitelist: list[str]) -> str:
339
362
  """Create a new queue trigger in the database.
340
363
 
341
364
  Args:
@@ -347,6 +370,11 @@ def create_queue_trigger(trigger_name: str, process_name: str, queue_name: str,
347
370
  is_git_repo: The is_git value of the process.
348
371
  is_blocking: The is_blocking value of the process.
349
372
  min_batch_size: The minimum number of queue elements before triggering.
373
+ priority: The integer priority of the trigger.
374
+ scheduler_whitelist: A list of names of schedulers the trigger may run on.
375
+
376
+ Returns:
377
+ The id of the trigger that was created.
350
378
  """
351
379
  with _get_session() as session:
352
380
  trigger = QueueTrigger(
@@ -357,10 +385,13 @@ def create_queue_trigger(trigger_name: str, process_name: str, queue_name: str,
357
385
  is_git_repo = is_git_repo,
358
386
  is_blocking = is_blocking,
359
387
  queue_name = queue_name,
360
- min_batch_size = min_batch_size
388
+ min_batch_size = min_batch_size,
389
+ priority=priority,
390
+ scheduler_whitelist=json.dumps(scheduler_whitelist)
361
391
  )
362
392
  session.add(trigger)
363
393
  session.commit()
394
+ return trigger.id
364
395
 
365
396
 
366
397
  def get_constant(name: str) -> Constant:
@@ -559,11 +590,11 @@ def begin_single_trigger(trigger_id: UUID | str) -> bool:
559
590
  return True
560
591
 
561
592
 
562
- def get_next_single_trigger() -> SingleTrigger | None:
563
- """Get the single trigger that should trigger next.
593
+ def get_pending_single_triggers() -> list[SingleTrigger]:
594
+ """Get all single triggers that are ready to run.
564
595
 
565
596
  Returns:
566
- The next single trigger to run if any.
597
+ All single triggers ready to run if any.
567
598
  """
568
599
  with _get_session() as session:
569
600
  query = (
@@ -571,16 +602,15 @@ def get_next_single_trigger() -> SingleTrigger | None:
571
602
  .where(SingleTrigger.process_status == TriggerStatus.IDLE)
572
603
  .where(SingleTrigger.next_run <= datetime.now())
573
604
  .order_by(SingleTrigger.next_run)
574
- .limit(1)
575
605
  )
576
- return session.scalar(query)
606
+ return list(session.scalars(query))
577
607
 
578
608
 
579
- def get_next_scheduled_trigger() -> ScheduledTrigger | None:
580
- """Get the scheduled trigger that should trigger next.
609
+ def get_pending_scheduled_triggers() -> list[ScheduledTrigger]:
610
+ """Get all scheduled triggers that are ready to run.
581
611
 
582
612
  Returns:
583
- The next scheduled trigger to run if any.
613
+ All scheduled triggers ready to run if any.
584
614
  """
585
615
  with _get_session() as session:
586
616
  query = (
@@ -588,9 +618,8 @@ def get_next_scheduled_trigger() -> ScheduledTrigger | None:
588
618
  .where(ScheduledTrigger.process_status == TriggerStatus.IDLE)
589
619
  .where(ScheduledTrigger.next_run <= datetime.now())
590
620
  .order_by(ScheduledTrigger.next_run)
591
- .limit(1)
592
621
  )
593
- return session.scalar(query)
622
+ return list(session.scalars(query))
594
623
 
595
624
 
596
625
  def begin_scheduled_trigger(trigger_id: UUID | str) -> bool:
@@ -625,14 +654,11 @@ def begin_scheduled_trigger(trigger_id: UUID | str) -> bool:
625
654
  return True
626
655
 
627
656
 
628
- def get_next_queue_trigger() -> QueueTrigger | None:
629
- """Get the next queue trigger to run.
630
- This functions loops through the queue triggers and checks
631
- if the number of queue elements with status 'New' is above
632
- the triggers min_batch_size.
657
+ def get_pending_queue_triggers() -> list[QueueTrigger]:
658
+ """Get all queue triggers that are ready to run.
633
659
 
634
660
  Returns:
635
- QueueTrigger | None: The next queue trigger to run if any.
661
+ All queue triggers that are ready to run if any.
636
662
  """
637
663
 
638
664
  with _get_session() as session:
@@ -648,9 +674,8 @@ def get_next_queue_trigger() -> QueueTrigger | None:
648
674
  select(QueueTrigger)
649
675
  .where(QueueTrigger.process_status == TriggerStatus.IDLE)
650
676
  .where(sub_query >= QueueTrigger.min_batch_size)
651
- .limit(1)
652
677
  )
653
- return session.scalar(query)
678
+ return list(session.scalars(query))
654
679
 
655
680
 
656
681
  def begin_queue_trigger(trigger_id: UUID | str) -> bool:
@@ -910,3 +935,51 @@ def delete_queue_element(element_id: UUID | str) -> None:
910
935
  q_element = session.get(QueueElement, element_id)
911
936
  session.delete(q_element)
912
937
  session.commit()
938
+
939
+
940
+ def get_schedulers() -> tuple[Scheduler, ...]:
941
+ """Get Schedulers from the database"""
942
+ with _get_session() as session:
943
+ query = select(Scheduler).order_by(Scheduler.machine_name)
944
+ result = session.scalars(query).all()
945
+ return tuple(result)
946
+
947
+
948
+ def send_ping_from_scheduler(machine_name: str) -> None:
949
+ """Send a ping from a running scheduler, updating the machine in the database.
950
+
951
+ Args:
952
+ machine_name: The machine pinging the Orchestrator
953
+ """
954
+ with _get_session() as session:
955
+ scheduler = session.get(Scheduler, machine_name)
956
+
957
+ if scheduler:
958
+ scheduler.last_update = datetime.now()
959
+ else:
960
+ scheduler = Scheduler(machine_name=machine_name, last_update=datetime.now())
961
+ session.add(scheduler)
962
+
963
+ session.commit()
964
+
965
+
966
+ def start_trigger_from_machine(machine_name: str, trigger_name: str) -> None:
967
+ """Start a trigger from a machine running a scheduler, updating the name and time for triggers in the database.
968
+
969
+ Args:
970
+ machine_name: The machine starting the trigger
971
+ trigger_name: The trigger being started på the machine
972
+ """
973
+ with _get_session() as session:
974
+ scheduler = session.get(Scheduler, machine_name)
975
+ now = datetime.now()
976
+
977
+ if scheduler:
978
+ scheduler.last_update = now
979
+ scheduler.latest_trigger = trigger_name
980
+ scheduler.latest_trigger_time = now
981
+ else:
982
+ scheduler = Scheduler(machine_name=machine_name, last_update=now, latest_trigger=trigger_name, latest_trigger_time=now)
983
+ session.add(scheduler)
984
+
985
+ session.commit()
@@ -4,10 +4,11 @@ from datetime import datetime
4
4
  import enum
5
5
  import uuid
6
6
 
7
- from sqlalchemy import String, Engine
8
- from sqlalchemy.orm import Mapped, DeclarativeBase, mapped_column
7
+ from sqlalchemy import String
8
+ from sqlalchemy.orm import Mapped, mapped_column
9
9
 
10
10
  from OpenOrchestrator.common import datetime_util
11
+ from OpenOrchestrator.database.base import Base
11
12
 
12
13
  # All classes in this module are effectively dataclasses without methods.
13
14
  # pylint: disable=too-few-public-methods
@@ -20,10 +21,6 @@ class LogLevel(enum.Enum):
20
21
  ERROR = "Error"
21
22
 
22
23
 
23
- class Base(DeclarativeBase):
24
- """SqlAlchemy base class for all ORM classes in this module."""
25
-
26
-
27
24
  class Log(Base):
28
25
  """A class representing log objects in the ORM."""
29
26
  __tablename__ = "Logs"
@@ -43,12 +40,3 @@ class Log(Base):
43
40
  "Message": self.log_message,
44
41
  "ID": str(self.id)
45
42
  }
46
-
47
-
48
- def create_tables(engine: Engine):
49
- """Create all SQL tables related to ORM classes in this module.
50
-
51
- Args:
52
- engine: The SqlAlchemy connection engine used to create the tables.
53
- """
54
- Base.metadata.create_all(engine)
@@ -5,10 +5,11 @@ import enum
5
5
  from typing import Optional
6
6
  import uuid
7
7
 
8
- from sqlalchemy import String, Engine
9
- from sqlalchemy.orm import Mapped, DeclarativeBase, mapped_column
8
+ from sqlalchemy import String
9
+ from sqlalchemy.orm import Mapped, mapped_column
10
10
 
11
11
  from OpenOrchestrator.common import datetime_util
12
+ from OpenOrchestrator.database.base import Base
12
13
 
13
14
  # All classes in this module are effectively dataclasses without methods.
14
15
  # pylint: disable=too-few-public-methods
@@ -23,10 +24,6 @@ class QueueStatus(enum.Enum):
23
24
  ABANDONED = 'Abandoned'
24
25
 
25
26
 
26
- class Base(DeclarativeBase):
27
- """SqlAlchemy base class for all ORM classes in this module."""
28
-
29
-
30
27
  class QueueElement(Base):
31
28
  """A class representing a queue element in the ORM."""
32
29
  __tablename__ = "Queues"
@@ -55,12 +52,3 @@ class QueueElement(Base):
55
52
  "Message": self.message,
56
53
  "Created By": self.created_by
57
54
  }
58
-
59
-
60
- def create_tables(engine: Engine):
61
- """Create all SQL tables related to ORM classes in this module.
62
-
63
- Args:
64
- engine: The SqlAlchemy connection engine used to create the tables.
65
- """
66
- Base.metadata.create_all(engine)
@@ -0,0 +1,32 @@
1
+ """Module for the Scheduler ORM class"""
2
+
3
+ from datetime import datetime
4
+ from typing import Optional
5
+
6
+ from sqlalchemy import String
7
+ from sqlalchemy.orm import Mapped, mapped_column
8
+
9
+ from OpenOrchestrator.common import datetime_util
10
+ from OpenOrchestrator.database.base import Base
11
+
12
+ # All classes in this module are effectively dataclasses without methods.
13
+ # pylint: disable=too-few-public-methods
14
+
15
+
16
+ class Scheduler(Base):
17
+ """Class containing the ORM model for schedulers"""
18
+ __tablename__ = "Schedulers"
19
+
20
+ machine_name: Mapped[str] = mapped_column(String(100), primary_key=True)
21
+ last_update: Mapped[datetime] = mapped_column(onupdate=datetime.now, default=datetime.now)
22
+ latest_trigger: Mapped[Optional[str]] = mapped_column(String(100), nullable=True, default=None)
23
+ latest_trigger_time: Mapped[Optional[datetime]] = mapped_column(nullable=True, default=None)
24
+
25
+ def to_row_dict(self) -> dict[str, str]:
26
+ """Convert scheduler to a row dictionary for display in a table."""
27
+ return {
28
+ "Machine Name": self.machine_name,
29
+ "Last Connection": datetime_util.format_datetime(self.last_update),
30
+ "Latest Trigger": self.latest_trigger if self.latest_trigger else "None yet",
31
+ "Latest Trigger Time": datetime_util.format_datetime(self.latest_trigger_time) if self.latest_trigger_time else ""
32
+ }
@@ -5,10 +5,11 @@ import enum
5
5
  from typing import Optional
6
6
  import uuid
7
7
 
8
- from sqlalchemy import String, ForeignKey, Engine
9
- from sqlalchemy.orm import Mapped, DeclarativeBase, mapped_column
8
+ from sqlalchemy import String, ForeignKey
9
+ from sqlalchemy.orm import Mapped, mapped_column
10
10
 
11
11
  from OpenOrchestrator.common import datetime_util
12
+ from OpenOrchestrator.database.base import Base
12
13
 
13
14
  # All classes in this module are effectively dataclasses without methods.
14
15
  # pylint: disable=too-few-public-methods
@@ -31,10 +32,6 @@ class TriggerType(enum.Enum):
31
32
  QUEUE = "Queue"
32
33
 
33
34
 
34
- class Base(DeclarativeBase):
35
- """SqlAlchemy base class for all ORM classes in this module."""
36
-
37
-
38
35
  class Trigger(Base):
39
36
  """A base class for all triggers in the ORM."""
40
37
  __tablename__ = "Triggers"
@@ -48,6 +45,8 @@ class Trigger(Base):
48
45
  process_status: Mapped[TriggerStatus] = mapped_column(default=TriggerStatus.IDLE)
49
46
  is_git_repo: Mapped[bool]
50
47
  is_blocking: Mapped[bool]
48
+ scheduler_whitelist: Mapped[str] = mapped_column(String(250))
49
+ priority: Mapped[int] = mapped_column(default=0)
51
50
  type: Mapped[TriggerType]
52
51
 
53
52
  __mapper_args__ = {
@@ -66,7 +65,8 @@ class Trigger(Base):
66
65
  "Status": self.process_status.value,
67
66
  "Process Name": self.process_name,
68
67
  "Last Run": datetime_util.format_datetime(self.last_run, "Never"),
69
- "ID": str(self.id)
68
+ "ID": str(self.id),
69
+ "Priority": str(self.priority)
70
70
  }
71
71
 
72
72
 
@@ -115,12 +115,3 @@ class QueueTrigger(Trigger):
115
115
  row_dict = super().to_row_dict()
116
116
  row_dict["Next Run"] = "N/A"
117
117
  return row_dict
118
-
119
-
120
- def create_tables(engine: Engine):
121
- """Create all SQL tables related to ORM classes in this module.
122
-
123
- Args:
124
- engine: The SqlAlchemy connection engine used to create the tables.
125
- """
126
- Base.metadata.create_all(engine)