OpenOrchestrator 1.3.0__py3-none-any.whl → 2.0.0rc1__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 +112 -39
  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 +30 -19
  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 +51 -63
  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.0rc1.dist-info/METADATA +158 -0
  42. openorchestrator-2.0.0rc1.dist-info/RECORD +55 -0
  43. {OpenOrchestrator-1.3.0.dist-info → openorchestrator-2.0.0rc1.dist-info}/WHEEL +1 -1
  44. OpenOrchestrator/scheduler/connection_frame.py +0 -96
  45. OpenOrchestrator-1.3.0.dist-info/METADATA +0 -60
  46. OpenOrchestrator-1.3.0.dist-info/RECORD +0 -39
  47. {OpenOrchestrator-1.3.0.dist-info → openorchestrator-2.0.0rc1.dist-info/licenses}/LICENSE +0 -0
  48. {OpenOrchestrator-1.3.0.dist-info → openorchestrator-2.0.0rc1.dist-info}/top_level.txt +0 -0
@@ -10,29 +10,32 @@ from OpenOrchestrator.orchestrator.tabs.settings_tab import SettingsTab
10
10
  from OpenOrchestrator.orchestrator.tabs.logging_tab import LoggingTab
11
11
  from OpenOrchestrator.orchestrator.tabs.constants_tab import ConstantTab
12
12
  from OpenOrchestrator.orchestrator.tabs.queue_tab import QueueTab
13
+ from OpenOrchestrator.orchestrator.tabs.schedulers_tab import SchedulerTab
13
14
 
14
15
 
15
16
  class Application():
16
17
  """The main application of Orchestrator.
17
18
  It contains a header and the four tabs of the application.
18
19
  """
19
- def __init__(self) -> None:
20
+ def __init__(self, port: int | None = None, show: bool = True) -> None:
20
21
  with ui.header():
21
22
  with ui.tabs() as self.tabs:
22
- ui.tab('Triggers')
23
- ui.tab('Logs')
24
- ui.tab('Constants')
25
- ui.tab('Queues')
26
- ui.tab('Settings')
23
+ ui.tab('Triggers').props("auto-id=trigger_tab")
24
+ ui.tab('Logs').props("auto-id=logs_tab")
25
+ ui.tab('Constants').props("auto-id=constants_tab")
26
+ ui.tab('Schedulers').props("auto-id=schedulers_tab")
27
+ ui.tab('Queues').props("auto-id=queues_tab")
28
+ ui.tab('Settings').props("auto-id=settings_tab")
27
29
 
28
30
  ui.space()
29
31
  ui.button(icon="contrast", on_click=ui.dark_mode().toggle)
30
- ui.button(icon='refresh', on_click=self.update_tab)
32
+ ui.button(icon='refresh', on_click=self.update_tab).props("auto-id=refresh_button")
31
33
 
32
34
  with ui.tab_panels(self.tabs, value='Settings', on_change=self.update_tab).classes('w-full') as self.tab_panels:
33
35
  self.t_tab = TriggerTab('Triggers')
34
36
  self.l_tab = LoggingTab("Logs")
35
37
  self.c_tab = ConstantTab("Constants")
38
+ self.s_tab = SchedulerTab("Schedulers")
36
39
  self.q_tab = QueueTab("Queues")
37
40
  SettingsTab('Settings')
38
41
 
@@ -41,7 +44,7 @@ class Application():
41
44
  app.on_connect(self.update_loop)
42
45
  app.on_disconnect(app.shutdown)
43
46
  app.on_exception(lambda exc: ui.notify(exc, type='negative'))
44
- ui.run(title="Orchestrator", favicon='🤖', native=False, port=get_free_port(), reload=False)
47
+ ui.run(title="Orchestrator", favicon='🤖', native=False, port=port or get_free_port(), reload=False, show=show)
45
48
 
46
49
  def update_tab(self):
47
50
  """Update the date in the currently selected tab."""
@@ -52,6 +55,8 @@ class Application():
52
55
  self.l_tab.update()
53
56
  case 'Constants':
54
57
  self.c_tab.update()
58
+ case 'Schedulers':
59
+ self.s_tab.update()
55
60
  case 'Queues':
56
61
  self.q_tab.update()
57
62
 
@@ -43,7 +43,7 @@ class DatetimeInput(ui.input):
43
43
 
44
44
  def _define_validation(self, allow_empty: bool):
45
45
  if not allow_empty:
46
- self.validation = {
46
+ self._validation = { # pylint: disable=protected-access
47
47
  "Please enter a datetime": bool,
48
48
  f"Invalid datetime: {self.PY_FORMAT}": lambda v: self.get_datetime() is not None
49
49
  }
@@ -55,7 +55,7 @@ class DatetimeInput(ui.input):
55
55
 
56
56
  return self.get_datetime() is not None
57
57
 
58
- self.validation = {f"Invalid datetime: {self.PY_FORMAT}": validate}
58
+ self._validation = {f"Invalid datetime: {self.PY_FORMAT}": validate} # pylint: disable=protected-access
59
59
 
60
60
  def get_datetime(self) -> datetime | None:
61
61
  """Get the text from the input as a datetime object, if
@@ -8,12 +8,13 @@ from nicegui import ui
8
8
  from OpenOrchestrator.database import db_util
9
9
  from OpenOrchestrator.database.constants import Constant
10
10
  from OpenOrchestrator.orchestrator.popups.generic_popups import question_popup
11
+ from OpenOrchestrator.orchestrator import test_helper
11
12
 
12
13
  if TYPE_CHECKING:
13
14
  from OpenOrchestrator.orchestrator.tabs.constants_tab import ConstantTab
14
15
 
15
16
 
16
- # pylint: disable-next=too-few-public-methods
17
+ # pylint: disable-next=too-few-public-methods, too-many-instance-attributes
17
18
  class ConstantPopup():
18
19
  """A popup for creating/updating queue triggers."""
19
20
  def __init__(self, constant_tab: ConstantTab, constant: Constant | None = None):
@@ -34,19 +35,20 @@ class ConstantPopup():
34
35
  self.value_input = ui.input("Constant Value").classes("w-full")
35
36
 
36
37
  with ui.row():
37
- ui.button(button_text, on_click=self._create_constant)
38
- ui.button("Cancel", on_click=self.dialog.close)
38
+ self.save_button = ui.button(button_text, on_click=self._create_constant)
39
+ self.cancel_button = ui.button("Cancel", on_click=self.dialog.close)
39
40
 
40
41
  if constant:
41
- ui.button("Delete", color='red', on_click=self._delete_constant)
42
+ self.delete_button = ui.button("Delete", color='red', on_click=self._delete_constant)
42
43
 
43
44
  self._define_validation()
44
45
  self._pre_populate()
46
+ test_helper.set_automation_ids(self, "constant_popup")
45
47
 
46
48
  def _define_validation(self):
47
49
  """Define validation rules for input elements."""
48
- self.name_input.validation = {"Please enter a name": bool}
49
- self.value_input.validation = {"Please enter a value": bool}
50
+ self.name_input._validation = {"Please enter a name": bool} # pylint: disable=protected-access
51
+ self.value_input._validation = {"Please enter a value": bool} # pylint: disable=protected-access
50
52
 
51
53
  def _pre_populate(self):
52
54
  """Pre populate the inputs with an existing constant."""
@@ -8,12 +8,13 @@ from nicegui import ui
8
8
  from OpenOrchestrator.database import db_util
9
9
  from OpenOrchestrator.database.constants import Credential
10
10
  from OpenOrchestrator.orchestrator.popups.generic_popups import question_popup
11
+ from OpenOrchestrator.orchestrator import test_helper
11
12
 
12
13
  if TYPE_CHECKING:
13
14
  from OpenOrchestrator.orchestrator.tabs.constants_tab import ConstantTab
14
15
 
15
16
 
16
- # pylint: disable-next=too-few-public-methods
17
+ # pylint: disable-next=too-few-public-methods, too-many-instance-attributes
17
18
  class CredentialPopup():
18
19
  """A popup for creating/updating queue triggers."""
19
20
  def __init__(self, constant_tab: ConstantTab, credential: Credential | None = None):
@@ -36,20 +37,21 @@ class CredentialPopup():
36
37
  self.password_input = ui.input("Password").classes("w-full")
37
38
 
38
39
  with ui.row():
39
- ui.button(button_text, on_click=self._save_credential)
40
- ui.button("Cancel", on_click=self.dialog.close)
40
+ self.save_button = ui.button(button_text, on_click=self._save_credential)
41
+ self.cancel_button = ui.button("Cancel", on_click=self.dialog.close)
41
42
 
42
43
  if credential:
43
- ui.button("Delete", color='red', on_click=self._delete_credential)
44
+ self.delete_button = ui.button("Delete", color='red', on_click=self._delete_credential)
44
45
 
45
46
  self._define_validation()
46
47
  self._pre_populate()
48
+ test_helper.set_automation_ids(self, "credential_popup")
47
49
 
48
50
  def _define_validation(self):
49
51
  """Define validation functions for ui elements."""
50
- self.name_input.validation = {"Please enter a name": bool}
51
- self.username_input.validation = {"Please enter a username": bool}
52
- self.password_input.validation = {"Please enter a password": bool}
52
+ self.name_input._validation = {"Please enter a name": bool} # pylint: disable=protected-access
53
+ self.username_input._validation = {"Please enter a username": bool} # pylint: disable=protected-access
54
+ self.password_input._validation = {"Please enter a password": bool} # pylint: disable=protected-access
53
55
 
54
56
  def _pre_populate(self):
55
57
  """Pre populate the inputs with an existing credential."""
@@ -76,7 +78,7 @@ class CredentialPopup():
76
78
  else:
77
79
  # Check if credential already exists
78
80
  try:
79
- db_util.get_credential(name)
81
+ db_util.get_credential(name, decrypt_password=False)
80
82
  exists = True
81
83
  except ValueError:
82
84
  exists = False
@@ -21,7 +21,20 @@ async def question_popup(question: str, option1: str, option2: str, color1: str
21
21
  with ui.dialog(value=True).props('persistent') as dialog, ui.card():
22
22
  ui.label(question).classes("text-lg")
23
23
  with ui.row():
24
- ui.button(option1, on_click=lambda e: dialog.submit(True), color=color1)
25
- ui.button(option2, on_click=lambda e: dialog.submit(False), color=color2)
24
+ ui.button(option1, on_click=lambda e: dialog.submit(True), color=color1).props("auto-id=popup_option1_button")
25
+ ui.button(option2, on_click=lambda e: dialog.submit(False), color=color2).props("auto-id=popup_option2_button")
26
26
 
27
27
  return await dialog
28
+
29
+
30
+ async def info_popup(text: str) -> None:
31
+ """Show a generic popup with the given text.
32
+
33
+ Args:
34
+ text: The text to display in the popup.
35
+ """
36
+ with ui.dialog(value=True).props('persistent') as dialog, ui.card():
37
+ ui.label(text).classes("text-lg")
38
+ ui.button("Close", on_click=lambda e: dialog.submit(True)).props("auto-id=popup_button")
39
+
40
+ await dialog
@@ -3,14 +3,16 @@
3
3
  from __future__ import annotations
4
4
  from typing import TYPE_CHECKING
5
5
  from datetime import datetime
6
+ import json
6
7
 
7
8
  from nicegui import ui
8
- from croniter import croniter, CroniterBadCronError # type: ignore
9
+ from cronsim import CronSim, CronSimError
9
10
 
10
11
  from OpenOrchestrator.orchestrator.datetime_input import DatetimeInput
11
12
  from OpenOrchestrator.database import db_util
12
13
  from OpenOrchestrator.database.triggers import Trigger, TriggerStatus, TriggerType, ScheduledTrigger, SingleTrigger, QueueTrigger
13
14
  from OpenOrchestrator.orchestrator.popups import generic_popups
15
+ from OpenOrchestrator.orchestrator import test_helper
14
16
 
15
17
  if TYPE_CHECKING:
16
18
  from OpenOrchestrator.orchestrator.tabs.trigger_tab import TriggerTab
@@ -48,39 +50,42 @@ class TriggerPopup():
48
50
  self.path_input = ui.input("Process Path").classes("w-full")
49
51
  self.git_check = ui.checkbox("Is path a Git Repo?")
50
52
  self.args_input = ui.input("Process Arguments").classes("w-full")
51
- self.blocking_check = ui.checkbox("Is process blocking?")
53
+ self.blocking_check = ui.checkbox(text="Is process blocking?", value=True)
54
+ self.priority_input = ui.number("Priority", value=0, precision=0, format="%.0f")
55
+ self.whitelist_input = ui.input_chips("Scheduler whitelist").classes("w-full")
52
56
 
53
57
  if trigger:
54
58
  with ui.row():
55
- ui.button("Enable", on_click=self._enable_trigger)
56
- ui.button("Disable", on_click=self._disable_trigger)
57
- ui.button("Delete", on_click=self._delete_trigger, color='red')
59
+ self.enable_button = ui.button("Enable", on_click=self._enable_trigger)
60
+ self.disable_button = ui.button("Disable", on_click=self._disable_trigger)
61
+ self.delete_button = ui.button("Delete", on_click=self._delete_trigger, color='red')
58
62
  else:
59
63
  # Dialog should only be persistent when a new trigger is being created
60
64
  self.dialog.props('persistent')
61
65
 
62
66
  with ui.row():
63
- ui.button("Save", on_click=self._create_trigger)
64
- ui.button("Cancel", on_click=self.dialog.close)
67
+ self.save_button = ui.button("Save", on_click=self._create_trigger)
68
+ self.cancel_button = ui.button("Cancel", on_click=self.dialog.close)
65
69
 
66
70
  self._disable_unused()
67
71
  self._define_validation()
68
72
  self._pre_populate()
73
+ test_helper.set_automation_ids(self, "trigger_popup")
69
74
 
70
75
  def _define_validation(self):
71
- self.trigger_input.validation = {"Please enter a trigger name": bool}
72
- self.name_input.validation = {"Please enter a process name": bool}
73
- self.path_input.validation = {"Please enter a process path": bool}
74
- self.queue_input.validation = {"Please enter a queue name": bool}
76
+ self.trigger_input._validation = {"Please enter a trigger name": bool} # pylint: disable=protected-access
77
+ self.name_input._validation = {"Please enter a process name": bool} # pylint: disable=protected-access
78
+ self.path_input._validation = {"Please enter a process path": bool} # pylint: disable=protected-access
79
+ self.queue_input._validation = {"Please enter a queue name": bool} # pylint: disable=protected-access
75
80
 
76
81
  def validate_cron(value: str):
77
82
  try:
78
- croniter(value)
83
+ CronSim(value, datetime.now())
79
84
  return True
80
- except CroniterBadCronError:
85
+ except CronSimError:
81
86
  return False
82
87
 
83
- self.cron_input.validation = {"Invalid cron expression": validate_cron}
88
+ self.cron_input._validation = {"Invalid cron expression": validate_cron} # pylint: disable=protected-access
84
89
 
85
90
  def _pre_populate(self):
86
91
  """Populate the form with values from an existing trigger"""
@@ -93,6 +98,8 @@ class TriggerPopup():
93
98
  self.args_input.value = self.trigger.process_args
94
99
  self.git_check.value = self.trigger.is_git_repo
95
100
  self.blocking_check.value = self.trigger.is_blocking
101
+ self.priority_input.value = self.trigger.priority
102
+ self.whitelist_input.value = json.loads(self.trigger.scheduler_whitelist)
96
103
 
97
104
  if isinstance(self.trigger, ScheduledTrigger):
98
105
  self.cron_input.value = self.trigger.cron_expr
@@ -118,8 +125,8 @@ class TriggerPopup():
118
125
 
119
126
  def _cron_change(self):
120
127
  if self.cron_input.validate():
121
- cron_iter = croniter(self.cron_input.value, datetime.now())
122
- self.time_input.set_datetime(cron_iter.next(datetime))
128
+ cron_iter = CronSim(self.cron_input.value, datetime.now())
129
+ self.time_input.set_datetime(next(cron_iter))
123
130
 
124
131
  async def _validate(self) -> bool:
125
132
  result = True
@@ -163,15 +170,17 @@ class TriggerPopup():
163
170
  args = self.args_input.value
164
171
  is_git = self.git_check.value
165
172
  is_blocking = self.blocking_check.value
173
+ priority = self.priority_input.value
174
+ whitelist = self.whitelist_input.value
166
175
 
167
176
  if self.trigger is None:
168
177
  # Create new trigger in database
169
178
  if self.trigger_type == TriggerType.SINGLE:
170
- db_util.create_single_trigger(trigger_name, process_name, next_run, path, args, is_git, is_blocking)
179
+ db_util.create_single_trigger(trigger_name, process_name, next_run, path, args, is_git, is_blocking, priority, whitelist)
171
180
  elif self.trigger_type == TriggerType.SCHEDULED:
172
- db_util.create_scheduled_trigger(trigger_name, process_name, cron_expr, next_run, path, args, is_git, is_blocking)
181
+ db_util.create_scheduled_trigger(trigger_name, process_name, cron_expr, next_run, path, args, is_git, is_blocking, priority, whitelist)
173
182
  elif self.trigger_type == TriggerType.QUEUE:
174
- db_util.create_queue_trigger(trigger_name, process_name, queue_name, path, args, is_git, is_blocking, min_batch_size)
183
+ db_util.create_queue_trigger(trigger_name, process_name, queue_name, path, args, is_git, is_blocking, min_batch_size, priority, whitelist)
175
184
 
176
185
  ui.notify("Trigger created", type='positive')
177
186
  else:
@@ -182,6 +191,8 @@ class TriggerPopup():
182
191
  self.trigger.process_args = args
183
192
  self.trigger.is_git_repo = is_git
184
193
  self.trigger.is_blocking = is_blocking
194
+ self.trigger.priority = priority
195
+ self.trigger.scheduler_whitelist = json.dumps(whitelist)
185
196
 
186
197
  if isinstance(self.trigger, SingleTrigger):
187
198
  self.trigger.next_run = next_run
@@ -6,6 +6,7 @@ from nicegui import ui
6
6
  from OpenOrchestrator.database import db_util
7
7
  from OpenOrchestrator.orchestrator.popups.constant_popup import ConstantPopup
8
8
  from OpenOrchestrator.orchestrator.popups.credential_popup import CredentialPopup
9
+ from OpenOrchestrator.orchestrator import test_helper
9
10
 
10
11
  CONSTANT_COLUMNS = ("Constant Name", "Value", "Last Changed")
11
12
  CREDENTIAL_COLUMNS = ("Credential Name", "Username", "Password", "Last Changed")
@@ -16,8 +17,8 @@ class ConstantTab():
16
17
  def __init__(self, tab_name: str) -> None:
17
18
  with ui.tab_panel(tab_name):
18
19
  with ui.row():
19
- ui.button("New Constant", icon='add', on_click=lambda e: ConstantPopup(self))
20
- ui.button("New Credential", icon='add', on_click=lambda e: CredentialPopup(self))
20
+ self.constant_button = ui.button("New Constant", icon='add', on_click=lambda e: ConstantPopup(self))
21
+ self.credential_button = ui.button("New Credential", icon='add', on_click=lambda e: CredentialPopup(self))
21
22
 
22
23
  columns = [{'name': label, 'label': label, 'field': label, 'align': 'left', 'sortable': True} for label in CONSTANT_COLUMNS]
23
24
  self.constants_table = ui.table(title="Constants", columns=columns, rows=[], row_key='Constant Name', pagination=10).classes("w-full")
@@ -27,6 +28,8 @@ class ConstantTab():
27
28
  self.credentials_table = ui.table(title="Credentials", columns=columns, rows=[], row_key='Credential Name', pagination=10).classes("w-full")
28
29
  self.credentials_table.on('rowClick', self.row_click_credential)
29
30
 
31
+ test_helper.set_automation_ids(self, "constants_tab")
32
+
30
33
  def row_click_constant(self, event):
31
34
  """Callback for when a row is clicked in the table."""
32
35
  row = event.args[1]
@@ -6,6 +6,7 @@ from nicegui import ui
6
6
  from OpenOrchestrator.database import db_util
7
7
  from OpenOrchestrator.database.logs import LogLevel
8
8
  from OpenOrchestrator.orchestrator.datetime_input import DatetimeInput
9
+ from OpenOrchestrator.orchestrator import test_helper
9
10
 
10
11
 
11
12
  COLUMNS = [
@@ -32,6 +33,8 @@ class LoggingTab():
32
33
  self.logs_table = ui.table(title="Logs", columns=COLUMNS, rows=[], row_key='ID', pagination=50).classes("w-full")
33
34
  self.logs_table.on("rowClick", self._row_click)
34
35
 
36
+ test_helper.set_automation_ids(self, "logs_tab")
37
+
35
38
  def update(self):
36
39
  """Update the logs table and Process input list"""
37
40
  self._update_table()
@@ -6,6 +6,7 @@ from nicegui import ui
6
6
  from OpenOrchestrator.database import db_util
7
7
  from OpenOrchestrator.database.queues import QueueStatus
8
8
  from OpenOrchestrator.orchestrator.datetime_input import DatetimeInput
9
+ from OpenOrchestrator.orchestrator import test_helper
9
10
 
10
11
 
11
12
  QUEUE_COLUMNS = [
@@ -37,6 +38,7 @@ class QueueTab():
37
38
  with ui.tab_panel(tab_name):
38
39
  self.queue_table = ui.table(title="Queues", columns=QUEUE_COLUMNS, rows=[], row_key='Queue Name', pagination={'rowsPerPage': 50, 'sortBy': 'Queue Name'}).classes("w-full")
39
40
  self.queue_table.on("rowClick", self._row_click)
41
+ test_helper.set_automation_ids(self, "queues_tab")
40
42
 
41
43
  def update(self):
42
44
  """Update the queue table with data from the database."""
@@ -85,11 +87,12 @@ class QueuePopup():
85
87
  ui.switch("Dense", on_change=lambda e: self._dense_table(e.value))
86
88
  self._create_column_filter()
87
89
  ui.button(icon='refresh', on_click=self._update)
88
- ui.button(icon="close", on_click=dialog.close)
90
+ self.close_button = ui.button(icon="close", on_click=dialog.close)
89
91
  with ui.scroll_area().classes("h-full"):
90
92
  self.table = ui.table(columns=ELEMENT_COLUMNS, rows=[], row_key='ID', title=queue_name, pagination=100).classes("w-full")
91
93
 
92
94
  self._update()
95
+ test_helper.set_automation_ids(self, "queue_popup")
93
96
 
94
97
  def _dense_table(self, value: bool):
95
98
  """Change if the table is dense or not."""
@@ -0,0 +1,45 @@
1
+ """This module is responsible for the layout and functionality of the Schedulers tab
2
+ in Orchestrator."""
3
+
4
+ from nicegui import ui
5
+
6
+ from OpenOrchestrator.database import db_util
7
+ from OpenOrchestrator.orchestrator import test_helper
8
+
9
+ COLUMNS = [
10
+ {'name': "machine_name", 'label': "Machine Name", 'field': "Machine Name", 'align': 'left', 'sortable': True},
11
+ {'name': "last_connection", 'label': "Last Connection", 'field': "Last Connection", 'align': 'left', 'sortable': True},
12
+ {'name': "latest_trigger", 'label': "Latest Trigger", 'field': "Latest Trigger", 'align': 'left', 'sortable': True},
13
+ {'name': "latest_trigger_time", 'label': "Latest Trigger Time", 'field': "Latest Trigger Time", 'align': 'left', 'sortable': True},
14
+ ]
15
+
16
+
17
+ class SchedulerTab():
18
+ """A class for the scheduler tab."""
19
+ def __init__(self, tab_name: str) -> None:
20
+ with ui.tab_panel(tab_name):
21
+ self.schedulers_table = ui.table(title="Schedulers", columns=COLUMNS, rows=[], row_key='Machine Name', pagination=50).classes("w-full")
22
+ self.add_column_colors()
23
+ test_helper.set_automation_ids(self, "schedulers_tab")
24
+
25
+ def update(self):
26
+ """Updates the tables on the tab."""
27
+ schedulers = db_util.get_schedulers()
28
+ self.schedulers_table.rows = [s.to_row_dict() for s in schedulers]
29
+ self.schedulers_table.update()
30
+
31
+ def add_column_colors(self):
32
+ """Add red coloring to the scheduler if more than a minute has passed since last ping."""
33
+ self.schedulers_table.add_slot(
34
+ "body-cell-last_connection",
35
+ '''
36
+ <q-td key="last_connection" :props="props">
37
+ <q-badge v-if="(new Date() - new Date(+props.value.substr(6,4), +props.value.substr(3,2)-1, +props.value.substr(0,2), +props.value.substr(11,2), +props.value.substr(14,2))) > 60 * 1000" color='red'>
38
+ {{props.value}}
39
+ </q-badge>
40
+ <p v-else>
41
+ {{props.value}}
42
+ </p>
43
+ </q-td>
44
+ '''
45
+ )
@@ -3,8 +3,8 @@ in Orchestrator."""
3
3
 
4
4
  from nicegui import ui
5
5
 
6
- from OpenOrchestrator.database import db_util
7
6
  from OpenOrchestrator.common.connection_frame import ConnectionFrame
7
+ from OpenOrchestrator.orchestrator import test_helper
8
8
 
9
9
 
10
10
  # pylint: disable-next=too-few-public-methods
@@ -15,8 +15,5 @@ class SettingsTab():
15
15
  conn_frame = ConnectionFrame()
16
16
  with ui.row().classes("w-full"):
17
17
  self.key_button = ui.button("Generate Key", on_click=conn_frame.new_key)
18
- self.init_button = ui.button("Initialize Database", on_click=self._init_database)
19
18
 
20
- def _init_database(self):
21
- db_util.initialize_database()
22
- ui.notify("Database initialized!", type='positive')
19
+ test_helper.set_automation_ids(self, "settings_tab")
@@ -6,9 +6,11 @@ from nicegui import ui
6
6
  from OpenOrchestrator.database import db_util
7
7
  from OpenOrchestrator.database.triggers import SingleTrigger, ScheduledTrigger, QueueTrigger, TriggerType
8
8
  from OpenOrchestrator.orchestrator.popups.trigger_popup import TriggerPopup
9
+ from OpenOrchestrator.orchestrator import test_helper
9
10
 
10
11
  COLUMNS = [
11
12
  {'name': "Trigger Name", 'label': "Trigger Name", 'field': "Trigger Name", 'align': 'left', 'sortable': True},
13
+ {'name': "Priority", 'label': "Priority", 'field': "Priority", 'align': 'left', 'sortable': True, ':sort': '(a, b, rowA, rowB) => b-a'},
12
14
  {'name': "Type", 'label': "Type", 'field': "Type", 'align': 'left', 'sortable': True},
13
15
  {'name': "Status", 'label': "Status", 'field': "Status", 'align': 'left', 'sortable': True},
14
16
  {'name': "Process Name", 'label': "Process Name", 'field': "Process Name", 'align': 'left', 'sortable': True},
@@ -24,14 +26,16 @@ class TriggerTab():
24
26
  def __init__(self, tab_name: str) -> None:
25
27
  with ui.tab_panel(tab_name):
26
28
  with ui.row():
27
- ui.button("New Single Trigger", icon="add", on_click=lambda e: TriggerPopup(self, TriggerType.SINGLE))
28
- ui.button("New Scheduled Trigger", icon="add", on_click=lambda e: TriggerPopup(self, TriggerType.SCHEDULED))
29
- ui.button("New Queue Trigger", icon="add", on_click=lambda e: TriggerPopup(self, TriggerType.QUEUE))
29
+ self.single_button = ui.button("New Single Trigger", icon="add", on_click=lambda e: TriggerPopup(self, TriggerType.SINGLE))
30
+ self.scheduled_button = ui.button("New Scheduled Trigger", icon="add", on_click=lambda e: TriggerPopup(self, TriggerType.SCHEDULED))
31
+ self.queue_button = ui.button("New Queue Trigger", icon="add", on_click=lambda e: TriggerPopup(self, TriggerType.QUEUE))
30
32
 
31
- self.trigger_table = ui.table(COLUMNS, [], title="Triggers", pagination={'rowsPerPage': 50, 'sortBy': 'Trigger Name'}, row_key='ID').classes("w-full")
33
+ self.trigger_table = ui.table(columns=COLUMNS, rows=[], title="Triggers", pagination={'rowsPerPage': 50, 'sortBy': 'Trigger Name'}, row_key='ID').classes("w-full")
32
34
  self.trigger_table.on('rowClick', self._row_click)
33
35
  self.add_column_colors()
34
36
 
37
+ test_helper.set_automation_ids(self, "trigger_tab")
38
+
35
39
  def _row_click(self, event):
36
40
  """Callback for when a row is clicked in the table."""
37
41
  row = event.args[1]
@@ -58,7 +62,7 @@ class TriggerTab():
58
62
  "body-cell-Status",
59
63
  '''
60
64
  <q-td key="Status" :props="props">
61
- <q-badge v-if="{Running: 'green', Pausing: 'orange', Failed: 'red'}[props.value]" :color="{Running: 'green', Pausing: 'orange', Failed: 'red'}[props.value]">
65
+ <q-badge v-if="{Running: 'green', Pausing: 'orange', Paused: 'orange', Failed: 'red'}[props.value]" :color="{Running: 'green', Pausing: 'orange', Paused: 'orange', Failed: 'red'}[props.value]">
62
66
  {{props.value}}
63
67
  </q-badge>
64
68
  <p v-else>
@@ -0,0 +1,17 @@
1
+ """This module contains helper functions for automated tests."""
2
+
3
+ from nicegui import ui
4
+
5
+
6
+ def set_automation_ids(container, prefix: str):
7
+ """Set automation ids for automated tests.
8
+ Iterate though all attributes of the container and set 'auto-id'
9
+ on any ui-elements.
10
+
11
+ Args:
12
+ container: The object that contains a number of ui-elements.
13
+ prefix: The prefix to add to the automation ids for all elements.
14
+ """
15
+ for name, obj in container.__dict__.items():
16
+ if isinstance(obj, (ui.button, ui.input, ui.checkbox, ui.number, ui.table, ui.tab, ui.select, ui.input_chips)):
17
+ obj.props(f"auto-id={prefix}_{name}")
@@ -10,6 +10,7 @@ from OpenOrchestrator.database import db_util
10
10
  from OpenOrchestrator.database.queues import QueueElement, QueueStatus
11
11
  from OpenOrchestrator.database.logs import LogLevel
12
12
  from OpenOrchestrator.database.constants import Constant, Credential
13
+ from OpenOrchestrator.database.triggers import TriggerStatus
13
14
 
14
15
 
15
16
  class OrchestratorConnection:
@@ -19,16 +20,18 @@ class OrchestratorConnection:
19
20
  to instead of initializing the object manually.
20
21
  """
21
22
 
22
- def __init__(self, process_name: str, connection_string: str, crypto_key: str, process_arguments: str):
23
+ def __init__(self, process_name: str, connection_string: str, crypto_key: str, process_arguments: str, trigger_id: str):
23
24
  """
24
25
  Args:
25
26
  process_name: A human friendly tag to identify the process.
26
- connection_string: An ODBC connection to the OpenOrchestrator database
27
- crypto_key: Secret key for decrypting database content
27
+ connection_string: An ODBC connection to the OpenOrchestrator database.
28
+ crypto_key: Secret key for decrypting database content.
28
29
  process_arguments (optional): Arguments for the controlling how the process should run.
30
+ trigger_id: ID of trigger used to start this process.
29
31
  """
30
32
  self.process_name = process_name
31
33
  self.process_arguments = process_arguments
34
+ self.trigger_id = trigger_id
32
35
  crypto_util.set_key(crypto_key)
33
36
  db_util.connect(connection_string)
34
37
 
@@ -188,6 +191,19 @@ class OrchestratorConnection:
188
191
  """
189
192
  db_util.delete_queue_element(element_id)
190
193
 
194
+ def is_trigger_pausing(self) -> bool:
195
+ """Check if my trigger is pausing.
196
+
197
+ Returns:
198
+ bool: Whether or not the trigger used to start this process is pausing.
199
+ """
200
+ my_trigger = db_util.get_trigger(self.trigger_id)
201
+ return my_trigger.process_status in (TriggerStatus.PAUSING, TriggerStatus.PAUSED)
202
+
203
+ def pause_my_trigger(self) -> None:
204
+ """Pause the trigger used to start this process."""
205
+ db_util.set_trigger_status(self.trigger_id, TriggerStatus.PAUSING)
206
+
191
207
  @classmethod
192
208
  def create_connection_from_args(cls):
193
209
  """Create a Connection object using the arguments passed to sys.argv.
@@ -196,4 +212,5 @@ class OrchestratorConnection:
196
212
  connection_string = sys.argv[2]
197
213
  crypto_key = sys.argv[3]
198
214
  process_arguments = sys.argv[4]
199
- return OrchestratorConnection(process_name, connection_string, crypto_key, process_arguments)
215
+ trigger_id = sys.argv[5]
216
+ return OrchestratorConnection(process_name, connection_string, crypto_key, process_arguments, trigger_id)
@@ -3,6 +3,7 @@ that when created starts the application."""
3
3
 
4
4
  import tkinter
5
5
  from tkinter import ttk, messagebox
6
+
6
7
  from OpenOrchestrator.scheduler import settings_tab, run_tab
7
8
 
8
9
 
@@ -27,10 +28,10 @@ class Application(tkinter.Tk):
27
28
  notebook.pack(expand=True, fill='both')
28
29
 
29
30
  run_tab_ = run_tab.RunTab(notebook, self)
30
- settings_tab_ = settings_tab.create_tab(notebook)
31
+ self.settings_tab_ = settings_tab.SettingsTab(notebook)
31
32
 
32
33
  notebook.add(run_tab_, text='Run')
33
- notebook.add(settings_tab_, text="Settings")
34
+ notebook.add(self.settings_tab_, text="Settings")
34
35
 
35
36
  notebook.select(1)
36
37
 
@@ -12,7 +12,7 @@ from sqlalchemy import exc as alc_exc
12
12
 
13
13
  from OpenOrchestrator.common import crypto_util
14
14
  from OpenOrchestrator.database import db_util
15
- from OpenOrchestrator.scheduler import runner
15
+ from OpenOrchestrator.scheduler import runner, util
16
16
 
17
17
  if TYPE_CHECKING:
18
18
  from OpenOrchestrator.scheduler.application import Application
@@ -111,13 +111,15 @@ def loop(app: Application) -> None:
111
111
  app: The Scheduler Application object.
112
112
  """
113
113
  try:
114
+ send_ping_to_orchestrator()
115
+
114
116
  check_heartbeats(app)
115
117
 
116
118
  if app.running:
117
119
  check_triggers(app)
118
120
 
119
- except alc_exc.OperationalError:
120
- print("Couldn't connect to database.")
121
+ except (alc_exc.OperationalError, alc_exc.ProgrammingError) as e:
122
+ print(f"Couldn't connect to database. {e}")
121
123
 
122
124
  if len(app.running_jobs) == 0:
123
125
  print("Doing cleanup...")
@@ -169,7 +171,15 @@ def check_triggers(app: Application) -> None:
169
171
  # Check triggers
170
172
  if not blocking:
171
173
  print('Checking triggers...')
172
- job = runner.poll_triggers(app)
174
+ trigger = runner.poll_triggers(app)
175
+
176
+ if trigger:
177
+ job = runner.run_trigger(trigger)
178
+
179
+ if job:
180
+ app.running_jobs.append(job)
181
+
173
182
 
174
- if job is not None:
175
- app.running_jobs.append(job)
183
+ def send_ping_to_orchestrator():
184
+ """Send a ping to the connected Orchestrator with the Scheduler application's name."""
185
+ db_util.send_ping_from_scheduler(util.get_scheduler_name())