OpenOrchestrator 1.0.2__tar.gz → 1.2.0__tar.gz

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 (58) hide show
  1. OpenOrchestrator-1.2.0/OpenOrchestrator/__init__.py +7 -0
  2. {OpenOrchestrator-1.0.2 → OpenOrchestrator-1.2.0}/OpenOrchestrator/__main__.py +2 -0
  3. OpenOrchestrator-1.2.0/OpenOrchestrator/common/connection_frame.py +81 -0
  4. {OpenOrchestrator-1.0.2 → OpenOrchestrator-1.2.0}/OpenOrchestrator/common/crypto_util.py +4 -4
  5. OpenOrchestrator-1.2.0/OpenOrchestrator/common/datetime_util.py +20 -0
  6. {OpenOrchestrator-1.0.2 → OpenOrchestrator-1.2.0}/OpenOrchestrator/database/constants.py +25 -19
  7. {OpenOrchestrator-1.0.2 → OpenOrchestrator-1.2.0}/OpenOrchestrator/database/db_util.py +77 -30
  8. {OpenOrchestrator-1.0.2 → OpenOrchestrator-1.2.0}/OpenOrchestrator/database/logs.py +13 -0
  9. {OpenOrchestrator-1.0.2 → OpenOrchestrator-1.2.0}/OpenOrchestrator/database/queues.py +17 -0
  10. {OpenOrchestrator-1.0.2 → OpenOrchestrator-1.2.0}/OpenOrchestrator/database/triggers.py +25 -56
  11. OpenOrchestrator-1.2.0/OpenOrchestrator/orchestrator/application.py +94 -0
  12. OpenOrchestrator-1.2.0/OpenOrchestrator/orchestrator/datetime_input.py +75 -0
  13. OpenOrchestrator-1.2.0/OpenOrchestrator/orchestrator/popups/constant_popup.py +95 -0
  14. OpenOrchestrator-1.2.0/OpenOrchestrator/orchestrator/popups/credential_popup.py +99 -0
  15. OpenOrchestrator-1.2.0/OpenOrchestrator/orchestrator/popups/generic_popups.py +27 -0
  16. OpenOrchestrator-1.2.0/OpenOrchestrator/orchestrator/popups/trigger_popup.py +216 -0
  17. OpenOrchestrator-1.2.0/OpenOrchestrator/orchestrator/tabs/constants_tab.py +52 -0
  18. OpenOrchestrator-1.2.0/OpenOrchestrator/orchestrator/tabs/logging_tab.py +70 -0
  19. OpenOrchestrator-1.2.0/OpenOrchestrator/orchestrator/tabs/queue_tab.py +116 -0
  20. OpenOrchestrator-1.2.0/OpenOrchestrator/orchestrator/tabs/settings_tab.py +22 -0
  21. OpenOrchestrator-1.2.0/OpenOrchestrator/orchestrator/tabs/trigger_tab.py +87 -0
  22. {OpenOrchestrator-1.0.2 → OpenOrchestrator-1.2.0}/OpenOrchestrator/scheduler/application.py +3 -3
  23. {OpenOrchestrator-1.0.2/OpenOrchestrator/common → OpenOrchestrator-1.2.0/OpenOrchestrator/scheduler}/connection_frame.py +1 -0
  24. OpenOrchestrator-1.2.0/OpenOrchestrator/scheduler/run_tab.py +175 -0
  25. {OpenOrchestrator-1.0.2 → OpenOrchestrator-1.2.0}/OpenOrchestrator/scheduler/runner.py +33 -25
  26. {OpenOrchestrator-1.0.2 → OpenOrchestrator-1.2.0}/OpenOrchestrator/scheduler/settings_tab.py +2 -1
  27. {OpenOrchestrator-1.0.2 → OpenOrchestrator-1.2.0}/OpenOrchestrator.egg-info/PKG-INFO +2 -2
  28. {OpenOrchestrator-1.0.2 → OpenOrchestrator-1.2.0}/OpenOrchestrator.egg-info/SOURCES.txt +13 -9
  29. {OpenOrchestrator-1.0.2 → OpenOrchestrator-1.2.0}/OpenOrchestrator.egg-info/requires.txt +1 -1
  30. {OpenOrchestrator-1.0.2 → OpenOrchestrator-1.2.0}/PKG-INFO +2 -2
  31. {OpenOrchestrator-1.0.2 → OpenOrchestrator-1.2.0}/pyproject.toml +3 -3
  32. OpenOrchestrator-1.2.0/tests/test_db_util.py +298 -0
  33. OpenOrchestrator-1.2.0/tests/test_orchestrator_connection.py +118 -0
  34. OpenOrchestrator-1.0.2/OpenOrchestrator/orchestrator/application.py +0 -41
  35. OpenOrchestrator-1.0.2/OpenOrchestrator/orchestrator/constants_tab.py +0 -169
  36. OpenOrchestrator-1.0.2/OpenOrchestrator/orchestrator/logging_tab.py +0 -221
  37. OpenOrchestrator-1.0.2/OpenOrchestrator/orchestrator/popups/constant_popup.py +0 -77
  38. OpenOrchestrator-1.0.2/OpenOrchestrator/orchestrator/popups/credential_popup.py +0 -89
  39. OpenOrchestrator-1.0.2/OpenOrchestrator/orchestrator/popups/queue_trigger_popup.py +0 -129
  40. OpenOrchestrator-1.0.2/OpenOrchestrator/orchestrator/popups/scheduled_trigger_popup.py +0 -129
  41. OpenOrchestrator-1.0.2/OpenOrchestrator/orchestrator/popups/single_trigger_popup.py +0 -134
  42. OpenOrchestrator-1.0.2/OpenOrchestrator/orchestrator/settings_tab.py +0 -31
  43. OpenOrchestrator-1.0.2/OpenOrchestrator/orchestrator/table_util.py +0 -76
  44. OpenOrchestrator-1.0.2/OpenOrchestrator/orchestrator/trigger_tab.py +0 -231
  45. OpenOrchestrator-1.0.2/OpenOrchestrator/scheduler/__init__.py +0 -0
  46. OpenOrchestrator-1.0.2/OpenOrchestrator/scheduler/run_tab.py +0 -168
  47. {OpenOrchestrator-1.0.2 → OpenOrchestrator-1.2.0}/LICENSE +0 -0
  48. {OpenOrchestrator-1.0.2/OpenOrchestrator → OpenOrchestrator-1.2.0/OpenOrchestrator/common}/__init__.py +0 -0
  49. {OpenOrchestrator-1.0.2/OpenOrchestrator/common → OpenOrchestrator-1.2.0/OpenOrchestrator/database}/__init__.py +0 -0
  50. {OpenOrchestrator-1.0.2/OpenOrchestrator/database → OpenOrchestrator-1.2.0/OpenOrchestrator/orchestrator}/__init__.py +0 -0
  51. {OpenOrchestrator-1.0.2/OpenOrchestrator/orchestrator → OpenOrchestrator-1.2.0/OpenOrchestrator/orchestrator/popups}/__init__.py +0 -0
  52. {OpenOrchestrator-1.0.2/OpenOrchestrator/orchestrator/popups → OpenOrchestrator-1.2.0/OpenOrchestrator/orchestrator_connection}/__init__.py +0 -0
  53. {OpenOrchestrator-1.0.2 → OpenOrchestrator-1.2.0}/OpenOrchestrator/orchestrator_connection/connection.py +0 -0
  54. {OpenOrchestrator-1.0.2/OpenOrchestrator/orchestrator_connection → OpenOrchestrator-1.2.0/OpenOrchestrator/scheduler}/__init__.py +0 -0
  55. {OpenOrchestrator-1.0.2 → OpenOrchestrator-1.2.0}/OpenOrchestrator.egg-info/dependency_links.txt +0 -0
  56. {OpenOrchestrator-1.0.2 → OpenOrchestrator-1.2.0}/OpenOrchestrator.egg-info/top_level.txt +0 -0
  57. {OpenOrchestrator-1.0.2 → OpenOrchestrator-1.2.0}/README.md +0 -0
  58. {OpenOrchestrator-1.0.2 → OpenOrchestrator-1.2.0}/setup.cfg +0 -0
@@ -0,0 +1,7 @@
1
+ """OpenOrchestrator main module.
2
+ https://itk-dev-rpa.github.io/OpenOrchestrator-docs/
3
+ """
4
+
5
+ import importlib.metadata
6
+
7
+ __version__ = importlib.metadata.version("OpenOrchestrator")
@@ -5,9 +5,11 @@ import sys
5
5
  from OpenOrchestrator.scheduler.application import Application as s_app
6
6
  from OpenOrchestrator.orchestrator.application import Application as o_app
7
7
 
8
+
8
9
  def _print_usage():
9
10
  print("Usage: -o to start Orchestrator. -s to start Scheduler")
10
11
 
12
+
11
13
  if len(sys.argv) != 2:
12
14
  _print_usage()
13
15
  elif sys.argv[1] == '-s':
@@ -0,0 +1,81 @@
1
+ """This module contains a single class: ConnectionFrame."""
2
+
3
+ import os
4
+
5
+ from nicegui import ui
6
+
7
+ from OpenOrchestrator.common import crypto_util
8
+ from OpenOrchestrator.database import db_util
9
+
10
+
11
+ # pylint: disable-next=too-few-public-methods
12
+ class ConnectionFrame():
13
+ """A ui module containing input and buttons for connecting to the database."""
14
+ def __init__(self):
15
+ self.conn_input = ui.input("Connection String").classes("w-4/6")
16
+ self.key_input = ui.input("Encryption Key").classes("w-4/6")
17
+ self._define_validation()
18
+ with ui.row().classes("w-full"):
19
+ self.conn_button = ui.button("Connect", on_click=self._connect)
20
+ self.disconn_button = ui.button("Disconnect", on_click=self._disconnect, )
21
+ self.disconn_button.disable()
22
+
23
+ self._initial_connect()
24
+
25
+ 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}
28
+
29
+ def _connect(self) -> None:
30
+ """Validate the connection string and encryption key
31
+ and connect to the database.
32
+ """
33
+ if not self.conn_input.validate() & self.key_input.validate():
34
+ ui.notify("Please fill out all fields.", type='warning')
35
+ return
36
+
37
+ conn_string = self.conn_input.value
38
+ crypto_key = self.key_input.value
39
+
40
+ if db_util.connect(conn_string):
41
+ crypto_util.set_key(crypto_key)
42
+ self._set_state(True)
43
+ ui.notify("Connected!", type='positive')
44
+
45
+ def _disconnect(self) -> None:
46
+ db_util.disconnect()
47
+ crypto_util.set_key(None)
48
+ self._set_state(False)
49
+ ui.notify("Disconnected!", type='positive')
50
+
51
+ def _set_state(self, connected: bool) -> None:
52
+ if connected:
53
+ self.conn_input.disable()
54
+ self.key_input.disable()
55
+ self.conn_button.disable()
56
+ self.disconn_button.enable()
57
+ else:
58
+ self.conn_input.enable()
59
+ self.key_input.enable()
60
+ self.conn_button.enable()
61
+ self.disconn_button.disable()
62
+
63
+ def _initial_connect(self) -> None:
64
+ """Check the environment for a connection string
65
+ and encryption key and connect to the database if both
66
+ are found.
67
+ """
68
+ conn_string = os.environ.get('OpenOrchestratorConnString', None)
69
+ if conn_string:
70
+ self.conn_input.value = conn_string
71
+
72
+ crypto_key = os.environ.get('OpenOrchestratorKey', None)
73
+ if crypto_key:
74
+ self.key_input.value = crypto_key
75
+
76
+ def new_key(self):
77
+ """Creates a new encryption key and inserts it
78
+ into the key entry.
79
+ """
80
+ key = crypto_util.generate_key()
81
+ self.key_input.value = key.decode()
@@ -3,9 +3,9 @@
3
3
  from cryptography.fernet import Fernet
4
4
  from cryptography.exceptions import InvalidSignature
5
5
 
6
- # The encryption key is a module wide variable used in
7
- # static functions. Linting is disabled on this.
8
- _encryption_key: str
6
+
7
+ _encryption_key: str = None
8
+
9
9
 
10
10
  def generate_key() -> bytes:
11
11
  """Generates a new valid AES crypto key.
@@ -20,7 +20,7 @@ def set_key(key: str) -> None:
20
20
  """Set the crypto key for the module.
21
21
  The key will be used in all subsequent calls to this module.
22
22
  """
23
- global _encryption_key # pylint: disable=global-statement
23
+ global _encryption_key # pylint: disable=global-statement
24
24
  _encryption_key = key
25
25
 
26
26
 
@@ -0,0 +1,20 @@
1
+ """A module for performing common tasks regarding datetimes."""
2
+
3
+
4
+ from datetime import datetime
5
+
6
+
7
+ def format_datetime(datetime_: datetime, default: str = 'N/A') -> str:
8
+ """Format a datetime to a string.
9
+
10
+ Args:
11
+ datetime_: The datetime to format.
12
+ default: A default string to return if the datetime is None. Defaults to 'N/A'.
13
+
14
+ Returns:
15
+ A datetime string in the format %d-%m-%Y %H:%M:%S.
16
+ """
17
+ if not datetime_:
18
+ return default
19
+
20
+ return datetime_.strftime("%d-%m-%Y %H:%M:%S")
@@ -5,9 +5,12 @@ from datetime import datetime
5
5
  from sqlalchemy import String, Engine
6
6
  from sqlalchemy.orm import Mapped, DeclarativeBase, mapped_column
7
7
 
8
+ from OpenOrchestrator.common import datetime_util
9
+
8
10
  # All classes in this module are effectively dataclasses without methods.
9
11
  # pylint: disable=too-few-public-methods
10
12
 
13
+
11
14
  class Base(DeclarativeBase):
12
15
  """SqlAlchemy base class for all ORM classes in this module."""
13
16
 
@@ -20,13 +23,13 @@ class Constant(Base):
20
23
  value: Mapped[str] = mapped_column(String(1000))
21
24
  changed_at: Mapped[datetime] = mapped_column(onupdate=datetime.now, default=datetime.now)
22
25
 
23
- def to_tuple(self) -> tuple:
24
- """Convert the constant to a tuple of values.
25
-
26
- Returns:
27
- tuple: A tuple of all the triggers values.
28
- """
29
- return (self.name, self.value, self.changed_at)
26
+ def to_row_dict(self) -> dict[str, str]:
27
+ """Convert constant to a row dictionary for display in a table."""
28
+ return {
29
+ "Constant Name": self.name,
30
+ "Value": self.value,
31
+ "Last Changed": datetime_util.format_datetime(self.changed_at)
32
+ }
30
33
 
31
34
 
32
35
  class Credential(Base):
@@ -38,18 +41,21 @@ class Credential(Base):
38
41
  password: Mapped[str] = mapped_column(String(1000))
39
42
  changed_at: Mapped[datetime] = mapped_column(onupdate=datetime.now, default=datetime.now)
40
43
 
41
- def to_tuple(self) -> tuple:
42
- """Convert the credential to a tuple of values.
43
-
44
- Returns:
45
- tuple: A tuple of all the triggers values.
46
- """
47
- return (
48
- self.name,
49
- self.username,
50
- self.password,
51
- self.changed_at
52
- )
44
+ def to_row_dict(self) -> dict[str, str]:
45
+ """Convert credential to a row dictionary for display in a table."""
46
+ return {
47
+ "Credential Name": self.name,
48
+ "Username": self.username,
49
+ "Password": self.format_password(),
50
+ "Last Changed": datetime_util.format_datetime(self.changed_at)
51
+ }
52
+
53
+ def format_password(self) -> str:
54
+ """Format the password to be shown in a table."""
55
+ length = len(self.password)
56
+ lower_length = int(((length-100)/20)*16)
57
+ upper_length = lower_length + 15
58
+ return f"{length} encrypted bytes. {lower_length}-{upper_length} decrypted bytes."
53
59
 
54
60
 
55
61
  def create_tables(engine: Engine):
@@ -1,9 +1,9 @@
1
1
  """This module handles the connection to the database in OpenOrchestrator."""
2
2
 
3
3
  from datetime import datetime
4
- from tkinter import messagebox
5
- from typing import Callable
4
+ from typing import Callable, TypeVar, ParamSpec
6
5
 
6
+ from croniter import croniter
7
7
  from sqlalchemy import Engine, create_engine, select, insert, desc
8
8
  from sqlalchemy import exc as alc_exc
9
9
  from sqlalchemy import func as alc_func
@@ -16,7 +16,11 @@ from OpenOrchestrator.database.constants import Constant, Credential
16
16
  from OpenOrchestrator.database.triggers import Trigger, SingleTrigger, ScheduledTrigger, QueueTrigger, TriggerStatus
17
17
  from OpenOrchestrator.database.queues import QueueElement, QueueStatus
18
18
 
19
- _connection_engine: Engine
19
+ # Type hint helpers for decorators
20
+ T = TypeVar("T")
21
+ P = ParamSpec("P")
22
+
23
+ _connection_engine: Engine = None
20
24
 
21
25
 
22
26
  def connect(conn_string: str) -> bool:
@@ -35,28 +39,27 @@ def connect(conn_string: str) -> bool:
35
39
  engine.connect()
36
40
  _connection_engine = engine
37
41
  return True
38
- except alc_exc.InterfaceError as exc:
42
+ except (alc_exc.InterfaceError, alc_exc.ArgumentError, alc_exc.OperationalError):
39
43
  _connection_engine = None
40
- messagebox.showerror("Connection failed", str(exc))
41
44
 
42
45
  return False
43
46
 
44
47
 
45
48
  def disconnect() -> None:
46
49
  """Disconnect from the database."""
47
- global _connection_engine #pylint: disable=global-statement
50
+ global _connection_engine # pylint: disable=global-statement
48
51
  _connection_engine.dispose()
49
52
  _connection_engine = None
50
53
 
51
54
 
52
- def catch_db_error(func: Callable) -> Callable:
55
+ def catch_db_error(func: Callable[P, T]) -> Callable[P, T]:
53
56
  """A decorator that catches errors in SQL calls."""
54
- def inner(*args, **kwargs):
55
- try:
56
- result = func(*args, **kwargs)
57
- except alc_exc.ProgrammingError as exc:
58
- messagebox.showerror("Error", f"Query failed:\n{exc}")
59
- return result
57
+ def inner(*args, **kwargs) -> T:
58
+ if _connection_engine is None:
59
+ raise RuntimeError("Not connected to Database")
60
+
61
+ return func(*args, **kwargs)
62
+
60
63
  return inner
61
64
 
62
65
 
@@ -64,12 +67,14 @@ def get_conn_string() -> str:
64
67
  """Get the connection string.
65
68
 
66
69
  Returns:
67
- str: The connection string.
70
+ str: The connection string if any.
68
71
  """
69
72
  try:
70
73
  return str(_connection_engine.url)
71
- except AttributeError as exc:
72
- raise RuntimeError("Unable to get the connection string from the database engine. Has the connection been established?") from exc
74
+ except AttributeError:
75
+ pass
76
+
77
+ return None
73
78
 
74
79
 
75
80
  @catch_db_error
@@ -79,7 +84,6 @@ def initialize_database() -> None:
79
84
  triggers.create_tables(_connection_engine)
80
85
  constants.create_tables(_connection_engine)
81
86
  queues.create_tables(_connection_engine)
82
- messagebox.showinfo("Database initialized", "Database has been initialized!")
83
87
 
84
88
 
85
89
  @catch_db_error
@@ -101,6 +105,21 @@ def get_trigger(trigger_id: str) -> Trigger:
101
105
  return session.scalar(query)
102
106
 
103
107
 
108
+ @catch_db_error
109
+ def get_all_triggers() -> tuple[Trigger]:
110
+ """Get all triggers in the database.
111
+
112
+ Returns:
113
+ A tuple of Trigger objects.
114
+ """
115
+ with Session(_connection_engine) as session:
116
+ query = (
117
+ select(Trigger)
118
+ .options(selectin_polymorphic(Trigger, (ScheduledTrigger, QueueTrigger, SingleTrigger)))
119
+ )
120
+ return tuple(session.scalars(query))
121
+
122
+
104
123
  @catch_db_error
105
124
  def update_trigger(trigger: Trigger):
106
125
  """Updates an existing trigger in the database.
@@ -111,6 +130,7 @@ def update_trigger(trigger: Trigger):
111
130
  with Session(_connection_engine) as session:
112
131
  session.add(trigger)
113
132
  session.commit()
133
+ session.refresh(trigger)
114
134
 
115
135
 
116
136
  @catch_db_error
@@ -167,8 +187,8 @@ def delete_trigger(trigger_id: str) -> None:
167
187
 
168
188
  @catch_db_error
169
189
  def get_logs(offset: int, limit: int,
170
- from_date: datetime|None, to_date: datetime|None,
171
- process_name: str|None, log_level: LogLevel|None) -> tuple[Log]:
190
+ from_date: datetime = None, to_date: datetime = None,
191
+ process_name: str = None, log_level: LogLevel = None) -> tuple[Log]:
172
192
  """Get the logs from the database using filters and pagination.
173
193
 
174
194
  Args:
@@ -343,7 +363,7 @@ def get_constant(name: str) -> Constant:
343
363
 
344
364
  Returns:
345
365
  Constant: The constant with the given name.
346
-
366
+
347
367
  Raises:
348
368
  ValueError: If no constant with the given name exists.
349
369
  """
@@ -418,7 +438,7 @@ def get_credential(name: str) -> Credential:
418
438
 
419
439
  Returns:
420
440
  Credential: The credential with the given name.
421
-
441
+
422
442
  Raises:
423
443
  ValueError: If no credential with the given name exists.
424
444
  """
@@ -427,6 +447,7 @@ def get_credential(name: str) -> Credential:
427
447
 
428
448
  if credential is None:
429
449
  raise ValueError(f"No credential with name '{name}' was found.")
450
+
430
451
  credential.password = crypto_util.decrypt_string(credential.password)
431
452
  return credential
432
453
 
@@ -501,12 +522,12 @@ def delete_credential(name: str) -> None:
501
522
 
502
523
  @catch_db_error
503
524
  def begin_single_trigger(trigger_id: str) -> bool:
504
- """Set the status of a single trigger to 'running' and
525
+ """Set the status of a single trigger to 'running' and
505
526
  set the last run time to the current time.
506
527
 
507
528
  Args:
508
529
  trigger_id: The id of the trigger to begin.
509
-
530
+
510
531
  Returns:
511
532
  bool: True if the trigger was 'idle' and now 'running'.
512
533
  """
@@ -560,15 +581,15 @@ def get_next_scheduled_trigger() -> ScheduledTrigger | None:
560
581
 
561
582
 
562
583
  @catch_db_error
563
- def begin_scheduled_trigger(trigger_id: str, next_run: datetime) -> bool:
564
- """Set the status of a scheduled trigger to 'running',
584
+ def begin_scheduled_trigger(trigger_id: str) -> bool:
585
+ """Set the status of a scheduled trigger to 'running',
565
586
  set the last run time to the current time,
566
587
  and set the next run time to the given datetime.
567
588
 
568
589
  Args:
569
590
  trigger_id: The id of the trigger to begin.
570
591
  next_run: The next datetime the trigger should run.
571
-
592
+
572
593
  Returns:
573
594
  bool: True if the trigger was 'idle' and now 'running'.
574
595
  """
@@ -580,7 +601,7 @@ def begin_scheduled_trigger(trigger_id: str, next_run: datetime) -> bool:
580
601
 
581
602
  trigger.process_status = TriggerStatus.RUNNING
582
603
  trigger.last_run = datetime.now()
583
- trigger.next_run = next_run
604
+ trigger.next_run = croniter(trigger.cron_expr, datetime.now()).get_next(datetime)
584
605
 
585
606
  session.commit()
586
607
  return True
@@ -617,12 +638,12 @@ def get_next_queue_trigger() -> QueueTrigger | None:
617
638
 
618
639
  @catch_db_error
619
640
  def begin_queue_trigger(trigger_id: str) -> None:
620
- """Set the status of a queue trigger to 'running' and
641
+ """Set the status of a queue trigger to 'running' and
621
642
  set the last run time to the current time.
622
643
 
623
644
  Args:
624
645
  trigger_id: The id of the trigger to begin.
625
-
646
+
626
647
  Returns:
627
648
  bool: True if the trigger was 'idle' and now 'running'.
628
649
  """
@@ -773,7 +794,7 @@ def get_queue_elements(queue_name: str, reference: str = None, status: QueueStat
773
794
  query = (
774
795
  select(QueueElement)
775
796
  .where(QueueElement.queue_name == queue_name)
776
- .order_by(QueueElement.created_date)
797
+ .order_by(desc(QueueElement.created_date))
777
798
  .offset(offset)
778
799
  .limit(limit)
779
800
  )
@@ -787,6 +808,32 @@ def get_queue_elements(queue_name: str, reference: str = None, status: QueueStat
787
808
  return tuple(result)
788
809
 
789
810
 
811
+ @catch_db_error
812
+ def get_queue_count() -> dict[str, dict[QueueStatus, int]]:
813
+ """Count the number of queue elements of each status for every queue.
814
+
815
+ Returns:
816
+ A dict for each queue with the count for each status. E.g. result[queue_name][status] => count.
817
+ """
818
+ with Session(_connection_engine) as session:
819
+ query = (
820
+ select(QueueElement.queue_name, QueueElement.status, alc_func.count()) # pylint: disable=not-callable
821
+ .group_by(QueueElement.queue_name)
822
+ .group_by(QueueElement.status)
823
+ )
824
+ rows = session.execute(query)
825
+ rows = tuple(rows)
826
+
827
+ # Aggregate result into a dict
828
+ result = {}
829
+ for queue_name, status, count in rows:
830
+ if queue_name not in result:
831
+ result[queue_name] = {}
832
+ result[queue_name][status] = count
833
+
834
+ return result
835
+
836
+
790
837
  @catch_db_error
791
838
  def set_queue_element_status(element_id: str, status: QueueStatus, message: str = None) -> None:
792
839
  """Set the status of a queue element.
@@ -7,9 +7,12 @@ import uuid
7
7
  from sqlalchemy import String, Engine
8
8
  from sqlalchemy.orm import Mapped, DeclarativeBase, mapped_column
9
9
 
10
+ from OpenOrchestrator.common import datetime_util
11
+
10
12
  # All classes in this module are effectively dataclasses without methods.
11
13
  # pylint: disable=too-few-public-methods
12
14
 
15
+
13
16
  class LogLevel(enum.Enum):
14
17
  """An enum representing the level of logs."""
15
18
  TRACE = "Trace"
@@ -31,6 +34,16 @@ class Log(Base):
31
34
  process_name: Mapped[str] = mapped_column(String(100))
32
35
  log_message: Mapped[str] = mapped_column(String(8000))
33
36
 
37
+ def to_row_dict(self) -> dict[str, str]:
38
+ """Convert log to a row dictionary for display in a table."""
39
+ return {
40
+ "Log Time": datetime_util.format_datetime(self.log_time),
41
+ "Level": self.log_level.value,
42
+ "Process Name": self.process_name,
43
+ "Message": self.log_message,
44
+ "ID": str(self.id)
45
+ }
46
+
34
47
 
35
48
  def create_tables(engine: Engine):
36
49
  """Create all SQL tables related to ORM classes in this module.
@@ -8,9 +8,12 @@ import uuid
8
8
  from sqlalchemy import String, Engine
9
9
  from sqlalchemy.orm import Mapped, DeclarativeBase, mapped_column
10
10
 
11
+ from OpenOrchestrator.common import datetime_util
12
+
11
13
  # All classes in this module are effectively dataclasses without methods.
12
14
  # pylint: disable=too-few-public-methods
13
15
 
16
+
14
17
  class QueueStatus(enum.Enum):
15
18
  """An enum representing the status of a queue element."""
16
19
  NEW = 'New'
@@ -38,6 +41,20 @@ class QueueElement(Base):
38
41
  message: Mapped[Optional[str]] = mapped_column(String(1000))
39
42
  created_by: Mapped[Optional[str]] = mapped_column(String(100))
40
43
 
44
+ def to_row_dict(self) -> dict:
45
+ """Convert the object to a dict for display in a table."""
46
+ return {
47
+ "ID": self.id,
48
+ "Reference": self.reference,
49
+ "Status": self.status,
50
+ "Data": self.data,
51
+ "Created Date": datetime_util.format_datetime(self.created_date),
52
+ "Start Date": datetime_util.format_datetime(self.start_date),
53
+ "End Date": datetime_util.format_datetime(self.end_date),
54
+ "Message": self.message,
55
+ "Created By": self.created_by
56
+ }
57
+
41
58
 
42
59
  def create_tables(engine: Engine):
43
60
  """Create all SQL tables related to ORM classes in this module.
@@ -8,6 +8,8 @@ import uuid
8
8
  from sqlalchemy import String, ForeignKey, Engine
9
9
  from sqlalchemy.orm import Mapped, DeclarativeBase, mapped_column
10
10
 
11
+ from OpenOrchestrator.common import datetime_util
12
+
11
13
  # All classes in this module are effectively dataclasses without methods.
12
14
  # pylint: disable=too-few-public-methods
13
15
 
@@ -55,6 +57,17 @@ class Trigger(Base):
55
57
  def __repr__(self) -> str:
56
58
  return f"{self.trigger_name}: {self.type.value}"
57
59
 
60
+ def to_row_dict(self) -> dict[str, str]:
61
+ """Convert trigger to a row dictionary for display in a table."""
62
+ return {
63
+ "Trigger Name": self.trigger_name,
64
+ "Type": self.type.value,
65
+ "Status": self.process_status.value,
66
+ "Process Name": self.process_name,
67
+ "Last Run": datetime_util.format_datetime(self.last_run, "Never"),
68
+ "ID": str(self.id)
69
+ }
70
+
58
71
 
59
72
  class SingleTrigger(Trigger):
60
73
  """A class representing single trigger objects in the ORM."""
@@ -65,24 +78,10 @@ class SingleTrigger(Trigger):
65
78
 
66
79
  __mapper_args__ = {"polymorphic_identity": TriggerType.SINGLE}
67
80
 
68
- def to_tuple(self) -> tuple:
69
- """Convert the trigger to a tuple of values.
70
-
71
- Returns:
72
- tuple: A tuple of all the triggers values.
73
- """
74
- return (
75
- self.trigger_name,
76
- self.process_status.value,
77
- self.process_name,
78
- self.last_run,
79
- self.next_run,
80
- self.process_path,
81
- self.process_args,
82
- self.is_git_repo,
83
- self.is_blocking,
84
- self.id
85
- )
81
+ def to_row_dict(self) -> dict[str, str]:
82
+ row_dict = super().to_row_dict()
83
+ row_dict["Next Run"] = datetime_util.format_datetime(self.next_run)
84
+ return row_dict
86
85
 
87
86
 
88
87
  class ScheduledTrigger(Trigger):
@@ -95,25 +94,10 @@ class ScheduledTrigger(Trigger):
95
94
 
96
95
  __mapper_args__ = {"polymorphic_identity": TriggerType.SCHEDULED}
97
96
 
98
- def to_tuple(self) -> tuple:
99
- """Convert the trigger to a tuple of values.
100
-
101
- Returns:
102
- tuple: A tuple of all the triggers values.
103
- """
104
- return (
105
- self.trigger_name,
106
- self.process_status.value,
107
- self.process_name,
108
- self.cron_expr,
109
- self.last_run,
110
- self.next_run,
111
- self.process_path,
112
- self.process_args,
113
- self.is_git_repo,
114
- self.is_blocking,
115
- self.id
116
- )
97
+ def to_row_dict(self) -> dict[str, str]:
98
+ row_dict = super().to_row_dict()
99
+ row_dict["Next Run"] = datetime_util.format_datetime(self.next_run)
100
+ return row_dict
117
101
 
118
102
 
119
103
  class QueueTrigger(Trigger):
@@ -126,25 +110,10 @@ class QueueTrigger(Trigger):
126
110
 
127
111
  __mapper_args__ = {"polymorphic_identity": TriggerType.QUEUE}
128
112
 
129
- def to_tuple(self) -> tuple:
130
- """Convert the trigger to a tuple of values.
131
-
132
- Returns:
133
- tuple: A tuple of all the triggers values.
134
- """
135
- return (
136
- self.trigger_name,
137
- self.process_status.value,
138
- self.process_name,
139
- self.queue_name,
140
- self.min_batch_size,
141
- self.last_run,
142
- self.process_path,
143
- self.process_args,
144
- self.is_git_repo,
145
- self.is_blocking,
146
- self.id
147
- )
113
+ def to_row_dict(self) -> dict[str, str]:
114
+ row_dict = super().to_row_dict()
115
+ row_dict["Next Run"] = "N/A"
116
+ return row_dict
148
117
 
149
118
 
150
119
  def create_tables(engine: Engine):