OpenOrchestrator 1.0.2__py3-none-any.whl → 1.2.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.
Files changed (40) hide show
  1. OpenOrchestrator/__init__.py +7 -0
  2. OpenOrchestrator/__main__.py +2 -0
  3. OpenOrchestrator/common/connection_frame.py +35 -49
  4. OpenOrchestrator/common/crypto_util.py +4 -4
  5. OpenOrchestrator/common/datetime_util.py +20 -0
  6. OpenOrchestrator/database/constants.py +25 -19
  7. OpenOrchestrator/database/db_util.py +77 -30
  8. OpenOrchestrator/database/logs.py +13 -0
  9. OpenOrchestrator/database/queues.py +17 -0
  10. OpenOrchestrator/database/triggers.py +25 -56
  11. OpenOrchestrator/orchestrator/application.py +87 -34
  12. OpenOrchestrator/orchestrator/datetime_input.py +75 -0
  13. OpenOrchestrator/orchestrator/popups/constant_popup.py +87 -69
  14. OpenOrchestrator/orchestrator/popups/credential_popup.py +92 -82
  15. OpenOrchestrator/orchestrator/popups/generic_popups.py +27 -0
  16. OpenOrchestrator/orchestrator/popups/trigger_popup.py +216 -0
  17. OpenOrchestrator/orchestrator/tabs/constants_tab.py +52 -0
  18. OpenOrchestrator/orchestrator/tabs/logging_tab.py +70 -0
  19. OpenOrchestrator/orchestrator/tabs/queue_tab.py +116 -0
  20. OpenOrchestrator/orchestrator/tabs/settings_tab.py +22 -0
  21. OpenOrchestrator/orchestrator/tabs/trigger_tab.py +87 -0
  22. OpenOrchestrator/scheduler/application.py +3 -3
  23. OpenOrchestrator/scheduler/connection_frame.py +96 -0
  24. OpenOrchestrator/scheduler/run_tab.py +87 -80
  25. OpenOrchestrator/scheduler/runner.py +33 -25
  26. OpenOrchestrator/scheduler/settings_tab.py +2 -1
  27. {OpenOrchestrator-1.0.2.dist-info → OpenOrchestrator-1.2.0.dist-info}/METADATA +2 -2
  28. OpenOrchestrator-1.2.0.dist-info/RECORD +38 -0
  29. OpenOrchestrator/orchestrator/constants_tab.py +0 -169
  30. OpenOrchestrator/orchestrator/logging_tab.py +0 -221
  31. OpenOrchestrator/orchestrator/popups/queue_trigger_popup.py +0 -129
  32. OpenOrchestrator/orchestrator/popups/scheduled_trigger_popup.py +0 -129
  33. OpenOrchestrator/orchestrator/popups/single_trigger_popup.py +0 -134
  34. OpenOrchestrator/orchestrator/settings_tab.py +0 -31
  35. OpenOrchestrator/orchestrator/table_util.py +0 -76
  36. OpenOrchestrator/orchestrator/trigger_tab.py +0 -231
  37. OpenOrchestrator-1.0.2.dist-info/RECORD +0 -36
  38. {OpenOrchestrator-1.0.2.dist-info → OpenOrchestrator-1.2.0.dist-info}/LICENSE +0 -0
  39. {OpenOrchestrator-1.0.2.dist-info → OpenOrchestrator-1.2.0.dist-info}/WHEEL +0 -0
  40. {OpenOrchestrator-1.0.2.dist-info → OpenOrchestrator-1.2.0.dist-info}/top_level.txt +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':
@@ -1,74 +1,64 @@
1
1
  """This module contains a single class: ConnectionFrame."""
2
2
 
3
3
  import os
4
- import tkinter
5
- from tkinter import ttk, messagebox
4
+
5
+ from nicegui import ui
6
6
 
7
7
  from OpenOrchestrator.common import crypto_util
8
8
  from OpenOrchestrator.database import db_util
9
9
 
10
- # pylint: disable-next=too-many-ancestors
11
- class ConnectionFrame(ttk.Frame):
12
- """A ttk.Frame object that contains two ttk.Entry and
13
- two ttk.Button used to enter a connection string and
14
- encryption key and to connect to the database.
15
- """
16
- def __init__(self, parent: tkinter.Widget):
17
- super().__init__(parent)
18
-
19
- frame = ttk.Frame(self)
20
- frame.columnconfigure(1, weight=1)
21
- frame.pack(fill='both')
22
-
23
- ttk.Label(frame, text="Connection string:").grid(row=0, column=0, sticky='w')
24
-
25
- self.conn_entry = ttk.Entry(frame)
26
- self.conn_entry.grid(row=0, column=1, sticky='ew')
27
-
28
- self.conn_button = ttk.Button(frame, text="Connect", command=self._connect)
29
- self.conn_button.grid(row=0, column=2, sticky='e')
30
-
31
- ttk.Label(frame, text="Encryption key:").grid(row=1, column=0, sticky='w')
32
10
 
33
- self.key_entry = ttk.Entry(frame)
34
- self.key_entry.grid(row=1, column=1, sticky='ew')
35
-
36
- self.disconn_button = ttk.Button(frame, text="Disconnect", command=self._disconnect, state='disabled')
37
- self.disconn_button.grid(row=1, column=2, sticky='e')
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()
38
22
 
39
23
  self._initial_connect()
40
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
+
41
29
  def _connect(self) -> None:
42
30
  """Validate the connection string and encryption key
43
31
  and connect to the database.
44
32
  """
45
- conn_string = self.conn_entry.get()
46
- crypto_key = self.key_entry.get()
47
-
48
- if not crypto_util.validate_key(crypto_key):
49
- messagebox.showerror("Invalid encryption key", "The entered encryption key is not a valid AES key.")
33
+ if not self.conn_input.validate() & self.key_input.validate():
34
+ ui.notify("Please fill out all fields.", type='warning')
50
35
  return
51
36
 
37
+ conn_string = self.conn_input.value
38
+ crypto_key = self.key_input.value
39
+
52
40
  if db_util.connect(conn_string):
53
41
  crypto_util.set_key(crypto_key)
54
42
  self._set_state(True)
43
+ ui.notify("Connected!", type='positive')
55
44
 
56
45
  def _disconnect(self) -> None:
57
46
  db_util.disconnect()
58
47
  crypto_util.set_key(None)
59
48
  self._set_state(False)
49
+ ui.notify("Disconnected!", type='positive')
60
50
 
61
51
  def _set_state(self, connected: bool) -> None:
62
52
  if connected:
63
- self.conn_entry.configure(state='disabled')
64
- self.key_entry.configure(state='disabled')
65
- self.conn_button.configure(state='disabled')
66
- self.disconn_button.configure(state='normal')
53
+ self.conn_input.disable()
54
+ self.key_input.disable()
55
+ self.conn_button.disable()
56
+ self.disconn_button.enable()
67
57
  else:
68
- self.conn_entry.configure(state='normal')
69
- self.key_entry.configure(state='normal')
70
- self.conn_button.configure(state='normal')
71
- self.disconn_button.configure(state='disabled')
58
+ self.conn_input.enable()
59
+ self.key_input.enable()
60
+ self.conn_button.enable()
61
+ self.disconn_button.disable()
72
62
 
73
63
  def _initial_connect(self) -> None:
74
64
  """Check the environment for a connection string
@@ -77,19 +67,15 @@ class ConnectionFrame(ttk.Frame):
77
67
  """
78
68
  conn_string = os.environ.get('OpenOrchestratorConnString', None)
79
69
  if conn_string:
80
- self.conn_entry.insert(0, conn_string)
70
+ self.conn_input.value = conn_string
81
71
 
82
72
  crypto_key = os.environ.get('OpenOrchestratorKey', None)
83
73
  if crypto_key:
84
- self.key_entry.insert(0, crypto_key)
85
-
86
- if conn_string and crypto_key:
87
- self._connect()
74
+ self.key_input.value = crypto_key
88
75
 
89
76
  def new_key(self):
90
77
  """Creates a new encryption key and inserts it
91
78
  into the key entry.
92
79
  """
93
80
  key = crypto_util.generate_key()
94
- self.key_entry.delete(0, 'end')
95
- self.key_entry.insert(0, 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.