OpenOrchestrator 1.3.1__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.
- OpenOrchestrator/__main__.py +66 -12
- OpenOrchestrator/common/connection_frame.py +10 -4
- OpenOrchestrator/database/base.py +8 -0
- OpenOrchestrator/database/constants.py +3 -15
- OpenOrchestrator/database/db_util.py +110 -37
- OpenOrchestrator/database/logs.py +3 -15
- OpenOrchestrator/database/queues.py +3 -15
- OpenOrchestrator/database/schedulers.py +32 -0
- OpenOrchestrator/database/triggers.py +7 -16
- OpenOrchestrator/orchestrator/application.py +13 -8
- OpenOrchestrator/orchestrator/datetime_input.py +2 -2
- OpenOrchestrator/orchestrator/popups/constant_popup.py +8 -6
- OpenOrchestrator/orchestrator/popups/credential_popup.py +10 -8
- OpenOrchestrator/orchestrator/popups/generic_popups.py +15 -2
- OpenOrchestrator/orchestrator/popups/trigger_popup.py +25 -14
- OpenOrchestrator/orchestrator/tabs/constants_tab.py +5 -2
- OpenOrchestrator/orchestrator/tabs/logging_tab.py +3 -0
- OpenOrchestrator/orchestrator/tabs/queue_tab.py +4 -1
- OpenOrchestrator/orchestrator/tabs/schedulers_tab.py +45 -0
- OpenOrchestrator/orchestrator/tabs/settings_tab.py +2 -5
- OpenOrchestrator/orchestrator/tabs/trigger_tab.py +9 -5
- OpenOrchestrator/orchestrator/test_helper.py +17 -0
- OpenOrchestrator/orchestrator_connection/connection.py +21 -4
- OpenOrchestrator/scheduler/application.py +3 -2
- OpenOrchestrator/scheduler/run_tab.py +16 -6
- OpenOrchestrator/scheduler/runner.py +51 -63
- OpenOrchestrator/scheduler/settings_tab.py +90 -14
- OpenOrchestrator/scheduler/util.py +8 -0
- OpenOrchestrator/tests/__init__.py +0 -0
- OpenOrchestrator/tests/db_test_util.py +40 -0
- OpenOrchestrator/tests/test_db_util.py +372 -0
- OpenOrchestrator/tests/test_orchestrator_connection.py +142 -0
- OpenOrchestrator/tests/test_trigger_polling.py +143 -0
- OpenOrchestrator/tests/ui_tests/__init__.py +0 -0
- OpenOrchestrator/tests/ui_tests/test_constants_tab.py +167 -0
- OpenOrchestrator/tests/ui_tests/test_logging_tab.py +180 -0
- OpenOrchestrator/tests/ui_tests/test_queues_tab.py +126 -0
- OpenOrchestrator/tests/ui_tests/test_schedulers_tab.py +47 -0
- OpenOrchestrator/tests/ui_tests/test_trigger_tab.py +243 -0
- OpenOrchestrator/tests/ui_tests/ui_util.py +151 -0
- openorchestrator-2.0.0rc1.dist-info/METADATA +158 -0
- openorchestrator-2.0.0rc1.dist-info/RECORD +55 -0
- {openorchestrator-1.3.1.dist-info → openorchestrator-2.0.0rc1.dist-info}/WHEEL +1 -1
- OpenOrchestrator/scheduler/connection_frame.py +0 -96
- openorchestrator-1.3.1.dist-info/METADATA +0 -60
- openorchestrator-1.3.1.dist-info/RECORD +0 -39
- {openorchestrator-1.3.1.dist-info → openorchestrator-2.0.0rc1.dist-info/licenses}/LICENSE +0 -0
- {openorchestrator-1.3.1.dist-info → openorchestrator-2.0.0rc1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,243 @@
|
|
1
|
+
"""Tests relating to the trigger tab in Orchestrator."""
|
2
|
+
|
3
|
+
import unittest
|
4
|
+
from datetime import datetime, timedelta
|
5
|
+
import time
|
6
|
+
|
7
|
+
from selenium.webdriver.common.by import By
|
8
|
+
|
9
|
+
from OpenOrchestrator.common import datetime_util
|
10
|
+
from OpenOrchestrator.tests import db_test_util
|
11
|
+
from OpenOrchestrator.database import db_util
|
12
|
+
from OpenOrchestrator.database.triggers import SingleTrigger, ScheduledTrigger, QueueTrigger, TriggerStatus
|
13
|
+
from OpenOrchestrator.tests.ui_tests import ui_util
|
14
|
+
|
15
|
+
|
16
|
+
class TestTriggerTab(unittest.TestCase):
|
17
|
+
"""Test functionality of the trigger tab ui."""
|
18
|
+
def setUp(self) -> None:
|
19
|
+
self.browser = ui_util.open_orchestrator()
|
20
|
+
db_test_util.establish_clean_database()
|
21
|
+
self.browser.find_element(By.CSS_SELECTOR, "[auto-id=trigger_tab]").click()
|
22
|
+
ui_util.refresh_ui(self.browser)
|
23
|
+
|
24
|
+
def tearDown(self) -> None:
|
25
|
+
self.browser.quit()
|
26
|
+
|
27
|
+
@ui_util.screenshot_on_error
|
28
|
+
def test_single_trigger_creation(self):
|
29
|
+
"""Test creation of a single trigger."""
|
30
|
+
self.browser.find_element(By.CSS_SELECTOR, "[auto-id=trigger_tab_single_button]").click()
|
31
|
+
|
32
|
+
# Fill form
|
33
|
+
self.browser.find_element(By.CSS_SELECTOR, "[auto-id=trigger_popup_trigger_input]").send_keys("Trigger name")
|
34
|
+
self.browser.find_element(By.CSS_SELECTOR, "[auto-id=trigger_popup_name_input]").send_keys("Process name")
|
35
|
+
self.browser.find_element(By.CSS_SELECTOR, "[auto-id=trigger_popup_time_input]").send_keys("12-11-2020 12:34")
|
36
|
+
self.browser.find_element(By.CSS_SELECTOR, "[auto-id=trigger_popup_path_input]").send_keys("Process Path")
|
37
|
+
self.browser.find_element(By.CSS_SELECTOR, "[auto-id=trigger_popup_git_check]").click()
|
38
|
+
self.browser.find_element(By.CSS_SELECTOR, "[auto-id=trigger_popup_args_input]").send_keys("Process args")
|
39
|
+
self.browser.find_element(By.CSS_SELECTOR, "[auto-id=trigger_popup_priority_input]").send_keys("5")
|
40
|
+
self.browser.find_element(By.CSS_SELECTOR, "[auto-id=trigger_popup_whitelist_input]").send_keys("Scheduler1\nScheduler2\n")
|
41
|
+
self.browser.find_element(By.CSS_SELECTOR, "[auto-id=trigger_popup_save_button]").click()
|
42
|
+
self.browser.find_element(By.CSS_SELECTOR, "[auto-id=popup_option1_button]").click()
|
43
|
+
|
44
|
+
# Check result
|
45
|
+
time.sleep(5)
|
46
|
+
triggers = db_util.get_all_triggers()
|
47
|
+
self.assertEqual(len(triggers), 1)
|
48
|
+
trigger: SingleTrigger = triggers[0]
|
49
|
+
self.assertIsInstance(trigger, SingleTrigger)
|
50
|
+
|
51
|
+
self.assertEqual(trigger.trigger_name, "Trigger name")
|
52
|
+
self.assertEqual(trigger.process_name, "Process name")
|
53
|
+
self.assertEqual(trigger.next_run, datetime.strptime("12-11-2020 12:34", "%d-%m-%Y %H:%M"))
|
54
|
+
self.assertEqual(trigger.process_path, "Process Path")
|
55
|
+
self.assertEqual(trigger.is_git_repo, True)
|
56
|
+
self.assertEqual(trigger.process_args, "Process args")
|
57
|
+
self.assertEqual(trigger.is_blocking, True)
|
58
|
+
self.assertEqual(trigger.priority, 5)
|
59
|
+
self.assertEqual(trigger.scheduler_whitelist, '["Scheduler1", "Scheduler2"]')
|
60
|
+
|
61
|
+
@ui_util.screenshot_on_error
|
62
|
+
def test_scheduled_trigger_creation(self):
|
63
|
+
"""Test creation of a scheduled trigger."""
|
64
|
+
self.browser.find_element(By.CSS_SELECTOR, "[auto-id=trigger_tab_scheduled_button]").click()
|
65
|
+
|
66
|
+
# Fill form
|
67
|
+
self.browser.find_element(By.CSS_SELECTOR, "[auto-id=trigger_popup_trigger_input]").send_keys("Trigger name")
|
68
|
+
self.browser.find_element(By.CSS_SELECTOR, "[auto-id=trigger_popup_name_input]").send_keys("Process name")
|
69
|
+
self.browser.find_element(By.CSS_SELECTOR, "[auto-id=trigger_popup_cron_input]").send_keys("1 1 * * *")
|
70
|
+
self.browser.find_element(By.CSS_SELECTOR, "[auto-id=trigger_popup_path_input]").send_keys("Process Path")
|
71
|
+
self.browser.find_element(By.CSS_SELECTOR, "[auto-id=trigger_popup_git_check]").click()
|
72
|
+
self.browser.find_element(By.CSS_SELECTOR, "[auto-id=trigger_popup_args_input]").send_keys("Process args")
|
73
|
+
self.browser.find_element(By.CSS_SELECTOR, "[auto-id=trigger_popup_blocking_check]").click()
|
74
|
+
self.browser.find_element(By.CSS_SELECTOR, "[auto-id=trigger_popup_priority_input]").send_keys("5")
|
75
|
+
self.browser.find_element(By.CSS_SELECTOR, "[auto-id=trigger_popup_whitelist_input]").send_keys("Scheduler1\nScheduler2\n")
|
76
|
+
self.browser.find_element(By.CSS_SELECTOR, "[auto-id=trigger_popup_save_button]").click()
|
77
|
+
|
78
|
+
# Check result
|
79
|
+
time.sleep(2)
|
80
|
+
triggers = db_util.get_all_triggers()
|
81
|
+
self.assertEqual(len(triggers), 1)
|
82
|
+
trigger: ScheduledTrigger = triggers[0]
|
83
|
+
self.assertIsInstance(trigger, ScheduledTrigger)
|
84
|
+
|
85
|
+
self.assertEqual(trigger.trigger_name, "Trigger name")
|
86
|
+
self.assertEqual(trigger.process_name, "Process name")
|
87
|
+
self.assertEqual(trigger.cron_expr, "1 1 * * *")
|
88
|
+
self.assertEqual(trigger.process_path, "Process Path")
|
89
|
+
self.assertEqual(trigger.is_git_repo, True)
|
90
|
+
self.assertEqual(trigger.process_args, "Process args")
|
91
|
+
self.assertEqual(trigger.is_blocking, False)
|
92
|
+
self.assertEqual(trigger.priority, 5)
|
93
|
+
self.assertEqual(trigger.scheduler_whitelist, '["Scheduler1", "Scheduler2"]')
|
94
|
+
|
95
|
+
@ui_util.screenshot_on_error
|
96
|
+
def test_queue_trigger_creation(self):
|
97
|
+
"""Test creation of a queue trigger."""
|
98
|
+
self.browser.find_element(By.CSS_SELECTOR, "[auto-id=trigger_tab_queue_button]").click()
|
99
|
+
|
100
|
+
# Fill form
|
101
|
+
self.browser.find_element(By.CSS_SELECTOR, "[auto-id=trigger_popup_trigger_input]").send_keys("Trigger name")
|
102
|
+
self.browser.find_element(By.CSS_SELECTOR, "[auto-id=trigger_popup_name_input]").send_keys("Process name")
|
103
|
+
self.browser.find_element(By.CSS_SELECTOR, "[auto-id=trigger_popup_queue_input]").send_keys("Queue Name")
|
104
|
+
self.browser.find_element(By.CSS_SELECTOR, "[auto-id=trigger_popup_batch_input]").send_keys("1")
|
105
|
+
self.browser.find_element(By.CSS_SELECTOR, "[auto-id=trigger_popup_path_input]").send_keys("Process Path")
|
106
|
+
self.browser.find_element(By.CSS_SELECTOR, "[auto-id=trigger_popup_git_check]").click()
|
107
|
+
self.browser.find_element(By.CSS_SELECTOR, "[auto-id=trigger_popup_args_input]").send_keys("Process args")
|
108
|
+
self.browser.find_element(By.CSS_SELECTOR, "[auto-id=trigger_popup_blocking_check]").click()
|
109
|
+
self.browser.find_element(By.CSS_SELECTOR, "[auto-id=trigger_popup_priority_input]").send_keys("5")
|
110
|
+
self.browser.find_element(By.CSS_SELECTOR, "[auto-id=trigger_popup_whitelist_input]").send_keys("Scheduler1\nScheduler2\n")
|
111
|
+
self.browser.find_element(By.CSS_SELECTOR, "[auto-id=trigger_popup_save_button]").click()
|
112
|
+
|
113
|
+
# Check result
|
114
|
+
time.sleep(1)
|
115
|
+
triggers = db_util.get_all_triggers()
|
116
|
+
self.assertEqual(len(triggers), 1)
|
117
|
+
trigger: QueueTrigger = triggers[0]
|
118
|
+
self.assertIsInstance(trigger, QueueTrigger)
|
119
|
+
|
120
|
+
self.assertEqual(trigger.trigger_name, "Trigger name")
|
121
|
+
self.assertEqual(trigger.process_name, "Process name")
|
122
|
+
self.assertEqual(trigger.queue_name, "Queue Name")
|
123
|
+
self.assertEqual(trigger.min_batch_size, 11)
|
124
|
+
self.assertEqual(trigger.process_path, "Process Path")
|
125
|
+
self.assertEqual(trigger.is_git_repo, True)
|
126
|
+
self.assertEqual(trigger.process_args, "Process args")
|
127
|
+
self.assertEqual(trigger.is_blocking, False)
|
128
|
+
self.assertEqual(trigger.priority, 5)
|
129
|
+
self.assertEqual(trigger.scheduler_whitelist, '["Scheduler1", "Scheduler2"]')
|
130
|
+
|
131
|
+
@ui_util.screenshot_on_error
|
132
|
+
def test_trigger_table(self):
|
133
|
+
"""Test that data is shown correctly in the trigger table."""
|
134
|
+
# Create some triggers
|
135
|
+
yesterday = datetime.today() - timedelta(days=1)
|
136
|
+
tomorrow = datetime.today() + timedelta(days=1)
|
137
|
+
db_util.create_single_trigger("A Single trigger", "Single Process", yesterday, "Single path", "Single args", False, False, 0, [])
|
138
|
+
db_util.create_scheduled_trigger("B Scheduled trigger", "Scheduled Process", "1 2 3 4 5", tomorrow, "Scheduled path", "Scheduled args", False, False, 0, [])
|
139
|
+
db_util.create_queue_trigger("C Queue trigger", "Queue Process", "Queue Name", "Queue path", "Queue args", False, False, 25, 0, [])
|
140
|
+
|
141
|
+
ui_util.refresh_ui(self.browser)
|
142
|
+
|
143
|
+
table_data = ui_util.get_table_data(self.browser, "trigger_tab_trigger_table")
|
144
|
+
|
145
|
+
# Check single trigger
|
146
|
+
self.assertEqual(table_data[0][0], "A Single trigger")
|
147
|
+
self.assertEqual(table_data[0][1], "0")
|
148
|
+
self.assertEqual(table_data[0][2], "Single")
|
149
|
+
self.assertEqual(table_data[0][3], "Idle")
|
150
|
+
self.assertEqual(table_data[0][4], "Single Process")
|
151
|
+
self.assertEqual(table_data[0][5], "Never")
|
152
|
+
self.assertEqual(table_data[0][6], f"{datetime_util.format_datetime(yesterday)}\nOverdue")
|
153
|
+
|
154
|
+
# Check scheduled trigger
|
155
|
+
self.assertEqual(table_data[1][0], "B Scheduled trigger")
|
156
|
+
self.assertEqual(table_data[1][1], "0")
|
157
|
+
self.assertEqual(table_data[1][2], "Scheduled")
|
158
|
+
self.assertEqual(table_data[1][3], "Idle")
|
159
|
+
self.assertEqual(table_data[1][4], "Scheduled Process")
|
160
|
+
self.assertEqual(table_data[1][5], "Never")
|
161
|
+
self.assertEqual(table_data[1][6], f"{datetime_util.format_datetime(tomorrow)}")
|
162
|
+
|
163
|
+
# Check queue trigger
|
164
|
+
self.assertEqual(table_data[2][0], "C Queue trigger")
|
165
|
+
self.assertEqual(table_data[2][1], "0")
|
166
|
+
self.assertEqual(table_data[2][2], "Queue")
|
167
|
+
self.assertEqual(table_data[2][3], "Idle")
|
168
|
+
self.assertEqual(table_data[2][4], "Queue Process")
|
169
|
+
self.assertEqual(table_data[2][5], "Never")
|
170
|
+
self.assertEqual(table_data[2][6], "N/A")
|
171
|
+
|
172
|
+
@ui_util.screenshot_on_error
|
173
|
+
def test_delete_trigger(self):
|
174
|
+
"""Test deleting a trigger."""
|
175
|
+
# Create a trigger
|
176
|
+
db_util.create_queue_trigger("Queue trigger", "Queue Process", "Queue Name", "Queue path", "Queue args", False, False, 25, 0, [])
|
177
|
+
ui_util.refresh_ui(self.browser)
|
178
|
+
|
179
|
+
# Click trigger
|
180
|
+
ui_util.click_table_row(self.browser, "trigger_tab_trigger_table", 0)
|
181
|
+
|
182
|
+
# Delete trigger
|
183
|
+
self.browser.find_element(By.CSS_SELECTOR, "[auto-id=trigger_popup_delete_button]").click()
|
184
|
+
self.browser.find_element(By.CSS_SELECTOR, "[auto-id=popup_option1_button]").click()
|
185
|
+
|
186
|
+
# Check result
|
187
|
+
time.sleep(1)
|
188
|
+
triggers = db_util.get_all_triggers()
|
189
|
+
self.assertEqual(len(triggers), 0)
|
190
|
+
|
191
|
+
@ui_util.screenshot_on_error
|
192
|
+
def test_enable_disable(self):
|
193
|
+
"""Test disabling and enabling a trigger."""
|
194
|
+
# Create a trigger
|
195
|
+
db_util.create_queue_trigger("Queue trigger", "Queue Process", "Queue Name", "Queue path", "Queue args", False, False, 25, 0, [])
|
196
|
+
ui_util.refresh_ui(self.browser)
|
197
|
+
|
198
|
+
# Click trigger
|
199
|
+
ui_util.click_table_row(self.browser, "trigger_tab_trigger_table", 0)
|
200
|
+
|
201
|
+
# Disable trigger
|
202
|
+
self.browser.find_element(By.CSS_SELECTOR, "[auto-id=trigger_popup_disable_button]").click()
|
203
|
+
time.sleep(1)
|
204
|
+
|
205
|
+
trigger = db_util.get_all_triggers()[0]
|
206
|
+
self.assertEqual(trigger.process_status, TriggerStatus.PAUSED)
|
207
|
+
|
208
|
+
# Enable trigger
|
209
|
+
self.browser.find_element(By.CSS_SELECTOR, "[auto-id=trigger_popup_enable_button]").click()
|
210
|
+
time.sleep(1)
|
211
|
+
|
212
|
+
trigger = db_util.get_all_triggers()[0]
|
213
|
+
self.assertEqual(trigger.process_status, TriggerStatus.IDLE)
|
214
|
+
|
215
|
+
# Close trigger
|
216
|
+
self.browser.find_element(By.CSS_SELECTOR, "[auto-id=trigger_popup_cancel_button]").click()
|
217
|
+
|
218
|
+
@ui_util.screenshot_on_error
|
219
|
+
def test_edit_trigger(self):
|
220
|
+
"""Test editing a trigger."""
|
221
|
+
# Create a trigger
|
222
|
+
db_util.create_queue_trigger("Queue trigger", "Queue Process", "Queue Name", "Queue path", "Queue args", False, False, 25, 0, [])
|
223
|
+
ui_util.refresh_ui(self.browser)
|
224
|
+
|
225
|
+
# Click trigger
|
226
|
+
ui_util.click_table_row(self.browser, "trigger_tab_trigger_table", 0)
|
227
|
+
|
228
|
+
# Edit trigger
|
229
|
+
self.browser.find_element(By.CSS_SELECTOR, "[auto-id=trigger_popup_trigger_input]").send_keys(" Edit")
|
230
|
+
self.browser.find_element(By.CSS_SELECTOR, "[auto-id=trigger_popup_save_button]").click()
|
231
|
+
time.sleep(1)
|
232
|
+
|
233
|
+
# Check result
|
234
|
+
trigger = db_util.get_all_triggers()[0]
|
235
|
+
self.assertEqual(trigger.trigger_name, "Queue trigger Edit")
|
236
|
+
|
237
|
+
|
238
|
+
if __name__ == '__main__':
|
239
|
+
# unittest.main(failfast=True)
|
240
|
+
t = TestTriggerTab()
|
241
|
+
t.setUp()
|
242
|
+
t.test_single_trigger_creation()
|
243
|
+
t.tearDown()
|
@@ -0,0 +1,151 @@
|
|
1
|
+
"""This module contains functions to help with ui tests."""
|
2
|
+
|
3
|
+
import subprocess
|
4
|
+
import os
|
5
|
+
import sys
|
6
|
+
import time
|
7
|
+
from typing import Callable
|
8
|
+
from pathlib import Path
|
9
|
+
|
10
|
+
from selenium import webdriver
|
11
|
+
from selenium.webdriver.common.by import By
|
12
|
+
from selenium.webdriver.common.keys import Keys
|
13
|
+
from selenium.common.exceptions import WebDriverException
|
14
|
+
from selenium.webdriver.remote.webelement import WebElement
|
15
|
+
|
16
|
+
from OpenOrchestrator.orchestrator.application import get_free_port
|
17
|
+
|
18
|
+
|
19
|
+
ENCRYPTION_KEY = None
|
20
|
+
|
21
|
+
|
22
|
+
def open_orchestrator() -> webdriver.Chrome:
|
23
|
+
"""Open Orchestrator in a Selenium Chrome browser.
|
24
|
+
|
25
|
+
Raises:
|
26
|
+
RuntimeError: If the Orchestrator app didn't start.
|
27
|
+
|
28
|
+
Returns:
|
29
|
+
The Chrome browser logged in to Orchestrator.
|
30
|
+
"""
|
31
|
+
conn_string = os.environ['CONN_STRING']
|
32
|
+
os.environ['ORCHESTRATOR_TEST'] = "True"
|
33
|
+
|
34
|
+
port = get_free_port()
|
35
|
+
subprocess.Popen([sys.executable, "-m", "OpenOrchestrator", "o", "--port", str(port), "--dont_show"]) # pylint: disable=consider-using-with
|
36
|
+
|
37
|
+
chrome_options = webdriver.ChromeOptions()
|
38
|
+
chrome_options.add_argument("--disable-search-engine-choice-screen")
|
39
|
+
browser = webdriver.Chrome(options=chrome_options)
|
40
|
+
browser.implicitly_wait(2)
|
41
|
+
browser.maximize_window()
|
42
|
+
for _ in range(5):
|
43
|
+
try:
|
44
|
+
browser.get(f"http://localhost:{port}")
|
45
|
+
break
|
46
|
+
except WebDriverException:
|
47
|
+
pass
|
48
|
+
else:
|
49
|
+
raise RuntimeError("Couldn't connect to app after 5 tries.")
|
50
|
+
|
51
|
+
# Wait for the ui to load
|
52
|
+
time.sleep(1)
|
53
|
+
|
54
|
+
conn_input = browser.find_element(By.CSS_SELECTOR, "input[auto-id=connection_frame_conn_input]")
|
55
|
+
conn_input.send_keys(Keys.CONTROL, "a", Keys.DELETE)
|
56
|
+
conn_input.send_keys(conn_string)
|
57
|
+
|
58
|
+
browser.find_element(By.CSS_SELECTOR, "button[auto-id=settings_tab_key_button]").click()
|
59
|
+
browser.find_element(By.CSS_SELECTOR, "button[auto-id=connection_frame_conn_button]").click()
|
60
|
+
|
61
|
+
global ENCRYPTION_KEY # pylint: disable=global-statement
|
62
|
+
ENCRYPTION_KEY = browser.find_element(By.CSS_SELECTOR, "input[auto-id=connection_frame_key_input]").get_attribute("value")
|
63
|
+
|
64
|
+
time.sleep(1)
|
65
|
+
|
66
|
+
return browser
|
67
|
+
|
68
|
+
|
69
|
+
def refresh_ui(browser: webdriver.Chrome):
|
70
|
+
"""Press the refresh button."""
|
71
|
+
browser.find_element(By.CSS_SELECTOR, "[auto-id=refresh_button").click()
|
72
|
+
time.sleep(0.5)
|
73
|
+
|
74
|
+
|
75
|
+
def monkey_patch_send_keys():
|
76
|
+
"""Monkey patch the WebElement.send_keys function with a
|
77
|
+
slower version. This works better with NiceGui elements.
|
78
|
+
"""
|
79
|
+
old_send_keys = WebElement.send_keys
|
80
|
+
|
81
|
+
def send_slow_keys(element: WebElement, *value: str | list):
|
82
|
+
"""Send keys to an element one at a time.
|
83
|
+
If "value" is a list fall back to the default behavior.
|
84
|
+
"""
|
85
|
+
if len(value) == 1 and isinstance(value[0], str):
|
86
|
+
for c in value[0]:
|
87
|
+
old_send_keys(element, c)
|
88
|
+
else:
|
89
|
+
old_send_keys(element, value)
|
90
|
+
|
91
|
+
WebElement.send_keys = send_slow_keys
|
92
|
+
|
93
|
+
|
94
|
+
monkey_patch_send_keys()
|
95
|
+
|
96
|
+
|
97
|
+
def get_table_data(browser: webdriver.Chrome, auto_id: str) -> list[list[str]]:
|
98
|
+
"""Get the data from a table.
|
99
|
+
|
100
|
+
Args:
|
101
|
+
browser: The browser object to perform the action.
|
102
|
+
auto_id: The automation id of the table.
|
103
|
+
|
104
|
+
Returns:
|
105
|
+
A 2D list of strings of all the data in the table.
|
106
|
+
"""
|
107
|
+
table = browser.find_element(By.CSS_SELECTOR, f"[auto-id={auto_id}]")
|
108
|
+
rows = table.find_element(By.TAG_NAME, "tbody").find_elements(By.TAG_NAME, "tr")
|
109
|
+
|
110
|
+
data = []
|
111
|
+
for row in rows:
|
112
|
+
fields = row.find_elements(By.TAG_NAME, "td")
|
113
|
+
data.append([f.text for f in fields])
|
114
|
+
|
115
|
+
return data
|
116
|
+
|
117
|
+
|
118
|
+
def click_table_row(browser: webdriver.Chrome, auto_id: str, index: int):
|
119
|
+
"""Click a row in a table.
|
120
|
+
|
121
|
+
Args:
|
122
|
+
browser: The browser to perform the action.
|
123
|
+
auto_id: The automation id of the table.
|
124
|
+
index: The 0-based index of the row to click.
|
125
|
+
"""
|
126
|
+
table = browser.find_element(By.CSS_SELECTOR, f"[auto-id={auto_id}]")
|
127
|
+
rows = table.find_element(By.TAG_NAME, "tbody").find_elements(By.TAG_NAME, "tr")
|
128
|
+
rows[index].click()
|
129
|
+
time.sleep(0.5)
|
130
|
+
|
131
|
+
|
132
|
+
def screenshot_on_error(test_func: Callable) -> Callable:
|
133
|
+
"""A decorator that can be used on test functions to take a Selenium screenshot on errors.
|
134
|
+
The unittest class must have a instance variable called "browser" which is the Selenium webdriver.
|
135
|
+
|
136
|
+
Args:
|
137
|
+
test_func: The test function to decorate.
|
138
|
+
"""
|
139
|
+
def wrapper(*args, **kwargs):
|
140
|
+
browser: webdriver.Chrome = args[0].browser
|
141
|
+
|
142
|
+
try:
|
143
|
+
test_func(*args, **kwargs)
|
144
|
+
except Exception:
|
145
|
+
folder = Path("error_screenshots")
|
146
|
+
folder.mkdir(exist_ok=True)
|
147
|
+
path = folder / Path(f"{test_func.__name__}.png")
|
148
|
+
browser.save_screenshot(str(path))
|
149
|
+
raise
|
150
|
+
|
151
|
+
return wrapper
|
@@ -0,0 +1,158 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: OpenOrchestrator
|
3
|
+
Version: 2.0.0rc1
|
4
|
+
Summary: A package containing OpenOrchestrator and OpenOrchestrator Scheduler
|
5
|
+
Author-email: ITK Development <itk-rpa@mkb.aarhus.dk>
|
6
|
+
Project-URL: Homepage, https://github.com/itk-dev-rpa/OpenOrchestrator
|
7
|
+
Project-URL: Bug Tracker, https://github.com/itk-dev-rpa/OpenOrchestrator/issues
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
10
|
+
Classifier: Operating System :: Microsoft :: Windows
|
11
|
+
Requires-Python: >=3.11
|
12
|
+
Description-Content-Type: text/markdown
|
13
|
+
License-File: LICENSE
|
14
|
+
Requires-Dist: cryptography==45.0.*
|
15
|
+
Requires-Dist: cronsim==2.6
|
16
|
+
Requires-Dist: SQLAlchemy==2.0.*
|
17
|
+
Requires-Dist: pyodbc==5.2.*
|
18
|
+
Requires-Dist: nicegui==2.22.*
|
19
|
+
Provides-Extra: dev
|
20
|
+
Requires-Dist: pylint; extra == "dev"
|
21
|
+
Requires-Dist: flake8; extra == "dev"
|
22
|
+
Requires-Dist: selenium; extra == "dev"
|
23
|
+
Provides-Extra: alembic
|
24
|
+
Requires-Dist: alembic==1.16.*; extra == "alembic"
|
25
|
+
Dynamic: license-file
|
26
|
+
|
27
|
+
# OpenOrchestrator
|
28
|
+
|
29
|
+
Read the documentation [here](https://itk-dev-rpa.github.io/OpenOrchestrator-docs).
|
30
|
+
|
31
|
+
Package is located at [Pypi](https://pypi.org/project/OpenOrchestrator/).
|
32
|
+
|
33
|
+
## Usage for Orchestrator admins
|
34
|
+
|
35
|
+
OpenOrchestrator provides a commandline interface (cli) to start the different parts
|
36
|
+
of the application.
|
37
|
+
|
38
|
+
For a full explanation run `python -m OpenOrchestrator -h` in the command line.
|
39
|
+
|
40
|
+
The most common commands would be:
|
41
|
+
|
42
|
+
`python -m OpenOrchestrator o` for orchestrator.
|
43
|
+
|
44
|
+
`python -m OpenOrchestrator s` for scheduler.
|
45
|
+
|
46
|
+
## Usage for RPA developers
|
47
|
+
|
48
|
+
Import the connection module to your RPA code and get access to the orchestrator methods;
|
49
|
+
|
50
|
+
- logging status to OpenOrchestrator
|
51
|
+
- getting credentials and constants from OpenOrchestrator
|
52
|
+
- creating, getting and updating job elements in a queue
|
53
|
+
|
54
|
+
Run the code with arguments
|
55
|
+
|
56
|
+
```bash
|
57
|
+
python run.py "<process name>" "<connection string>" "<secret key>" "<arguments>"
|
58
|
+
```
|
59
|
+
|
60
|
+
```python
|
61
|
+
# run.py
|
62
|
+
# connect to OpenOrchestrator and log something
|
63
|
+
from OpenOrchestrator.orchestrator_connection.connection import OrchestratorConnection
|
64
|
+
|
65
|
+
oc = OrchestratorConnection.create_connection_from_args()
|
66
|
+
oc.log_trace("open orchestrator connected.")
|
67
|
+
```
|
68
|
+
|
69
|
+
## Installation
|
70
|
+
|
71
|
+
Requires Python 3.10 or later.
|
72
|
+
|
73
|
+
Install using `pip install OpenOrchestrator`
|
74
|
+
|
75
|
+
### Creating or upgrading a database
|
76
|
+
|
77
|
+
If you need to create a new database or upgrade to a new revision follow these steps:
|
78
|
+
|
79
|
+
1. Download this repository either via Github or Git.
|
80
|
+
2. Open a command line in the project folder.
|
81
|
+
3. Run the following commands:
|
82
|
+
|
83
|
+
```bash
|
84
|
+
python -m venv .venv
|
85
|
+
.venv\scripts\activate
|
86
|
+
pip install .[alembic]
|
87
|
+
|
88
|
+
python -m OpenOrchestrator upgrade "<Your connection string here>"
|
89
|
+
```
|
90
|
+
|
91
|
+
This will automatically bring you to the newest revision of the database.
|
92
|
+
|
93
|
+
## Contributing
|
94
|
+
|
95
|
+
### Setup
|
96
|
+
|
97
|
+
To start developing for OpenOrchestrator pull the current develop branch using GIT.
|
98
|
+
|
99
|
+
In the new folder run the following commands in the command line:
|
100
|
+
|
101
|
+
```bash
|
102
|
+
python -m venv .venv
|
103
|
+
.venv\scripts\activate
|
104
|
+
pip install -e .
|
105
|
+
```
|
106
|
+
|
107
|
+
This will create a new virtual environment and install the project in 'editable' mode.
|
108
|
+
This means that any changes to the code is automatically included in the installation.
|
109
|
+
|
110
|
+
### Automated Tests
|
111
|
+
|
112
|
+
OpenOrchestrator contains automated tests.
|
113
|
+
|
114
|
+
Before running the tests you need to define the following environment variables:
|
115
|
+
|
116
|
+
```bash
|
117
|
+
SET CONN_STRING="<connection string to test db>"
|
118
|
+
```
|
119
|
+
|
120
|
+
Examples of connection strings:
|
121
|
+
|
122
|
+
- mssql+pyodbc://localhost\SQLEXPRESS/OO_Unittest?driver=ODBC+Driver+17+for+SQL+Server
|
123
|
+
- sqlite+pysqlite:///test_db.db
|
124
|
+
|
125
|
+
To run tests execute the following command from the main directory:
|
126
|
+
|
127
|
+
```bash
|
128
|
+
python -m unittest discover
|
129
|
+
```
|
130
|
+
|
131
|
+
### Manual Tests
|
132
|
+
|
133
|
+
Not all functionality is covered by automated tests. Especially the Scheduler app is not covered well.
|
134
|
+
|
135
|
+
Refer to the `manual_tests.txt` file for a list of things that should be tested.
|
136
|
+
|
137
|
+
### Creating new database revisions
|
138
|
+
|
139
|
+
If your update requires a change to the database schemas you need to create a new revision schema in the `alembic` folder.
|
140
|
+
|
141
|
+
This can mostly be done automatically by the following steps:
|
142
|
+
|
143
|
+
1. First make sure your database is on the __previous__ version of the database schema.
|
144
|
+
|
145
|
+
2. Make sure any new ORM classes are imported in `alembic/env.py`.
|
146
|
+
|
147
|
+
3. Then run the following command (replacing the connection string and message):
|
148
|
+
|
149
|
+
```bash
|
150
|
+
alembic -x "connection_string" revision --autogenerate -m "Some useful message"
|
151
|
+
```
|
152
|
+
|
153
|
+
This will create a new file in the `alembic/versions` folder with a random prefix and then your message as the name.
|
154
|
+
|
155
|
+
4. Open the file and make sure the contents make sense. Obvious changes are detected automatically
|
156
|
+
but some changes might not be.
|
157
|
+
|
158
|
+
5. Update the expected revision number in `db_util.py > check_database_revision`.
|
@@ -0,0 +1,55 @@
|
|
1
|
+
OpenOrchestrator/__init__.py,sha256=i4Ir68mBu7rrqhlEE6Qh_uyble599jaEWCEMf8g58tI,179
|
2
|
+
OpenOrchestrator/__main__.py,sha256=BohTEX2clH9DZVAVVW1uVGC_MJlvvyoKcNugXhSahEY,3072
|
3
|
+
OpenOrchestrator/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
4
|
+
OpenOrchestrator/common/connection_frame.py,sha256=sjfx1NUVBnlHW0krh05HzFohMcg2KLEi1QNWQN4M9E0,3373
|
5
|
+
OpenOrchestrator/common/crypto_util.py,sha256=VJ7fgxyjrW-bGQmsGVKXLUk98pcZGXG4faHtwxCWDDk,2440
|
6
|
+
OpenOrchestrator/common/datetime_util.py,sha256=4I70tz6WZGZ3xdxodJhdHJID14Y9myJTShFArOWCSpA,534
|
7
|
+
OpenOrchestrator/database/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
8
|
+
OpenOrchestrator/database/base.py,sha256=6GaBNOagnzW2eoYeB_uhBMBAxqeMac9yTkzHASWgLAE,235
|
9
|
+
OpenOrchestrator/database/constants.py,sha256=wg1kGa0zk9iBHoeiwauHTcndB8w0MVfq6CHemKh1U5k,2091
|
10
|
+
OpenOrchestrator/database/db_util.py,sha256=K1_LGFSlYkslCklLYA5wdN0G7Otq20RZYsB5ccn0TzI,31507
|
11
|
+
OpenOrchestrator/database/logs.py,sha256=EGbHnD4wGzsNDa5N7N_7xuTyGXcbNApapBUsQnwAX5M,1310
|
12
|
+
OpenOrchestrator/database/queues.py,sha256=GLHJz194j2EctYlgN1paZecDPHlJokPCo3Q7qQZM8PQ,1979
|
13
|
+
OpenOrchestrator/database/schedulers.py,sha256=7eims2tMjKO6sl255_u9eXcVG6662ZSp-v_VQMKd0Vk,1372
|
14
|
+
OpenOrchestrator/database/triggers.py,sha256=xNNKSanSoyIEcQgiBF91Bch4YsMOWeMoQ1ZE1PcL3dE,3878
|
15
|
+
OpenOrchestrator/database/truncated_string.py,sha256=v5TvMn3Sof9sG3RzHZx5_wRGBZ5IcJTsiyIO20MjehA,521
|
16
|
+
OpenOrchestrator/orchestrator/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
17
|
+
OpenOrchestrator/orchestrator/application.py,sha256=rs5GdM6lkePjGxnUQVSeSEQWHtf2_MaieE0GaWaV-gE,3726
|
18
|
+
OpenOrchestrator/orchestrator/datetime_input.py,sha256=zLYjlS3t8BQfyzlXkyQSpM1erC2dOBggwCwi9wq8b1Y,3085
|
19
|
+
OpenOrchestrator/orchestrator/test_helper.py,sha256=NDO7pcJg1GXK7R89WveQ3f-95T-FaOnnm_prB8UylmQ,660
|
20
|
+
OpenOrchestrator/orchestrator/popups/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
21
|
+
OpenOrchestrator/orchestrator/popups/constant_popup.py,sha256=n6RNpzQYpaLi8w-6tQTp64teobmX9JKc7seuzUve_QA,3717
|
22
|
+
OpenOrchestrator/orchestrator/popups/credential_popup.py,sha256=ncTas5zKJYxGFc7x9YUkINb2B6w7dNAQB5IHjLn1FbA,4254
|
23
|
+
OpenOrchestrator/orchestrator/popups/generic_popups.py,sha256=fBtl4vYRhKccoz1QViVjt-uZ6VrE2YrGCSleKzI_rm8,1530
|
24
|
+
OpenOrchestrator/orchestrator/popups/trigger_popup.py,sha256=w2rkJBSPb8cEsf5DPHIThxQuc8FCyCHci35Adr0__Gg,11061
|
25
|
+
OpenOrchestrator/orchestrator/tabs/constants_tab.py,sha256=wuMMAxcDXKFXbwCYbFXV0e-ZPBLuknNmQL-YDCDtRik,2650
|
26
|
+
OpenOrchestrator/orchestrator/tabs/logging_tab.py,sha256=p6dnEZfj9mxsDOyREzOq0Pnnx38IuGd57hex-zYEYb0,3701
|
27
|
+
OpenOrchestrator/orchestrator/tabs/queue_tab.py,sha256=HZTCkxA1-nTd9XPToupXAc4pDO-VIVR7IWpEli_Hekw,6013
|
28
|
+
OpenOrchestrator/orchestrator/tabs/schedulers_tab.py,sha256=YVWH0hjOWWvzYjVbFUvS7M2xwzXNrY9VLm8hdG-rsa8,2059
|
29
|
+
OpenOrchestrator/orchestrator/tabs/settings_tab.py,sha256=j6j9NYi2L6br--Dk1LP4UnUll8NtMVEjhWp_i3RnHDM,695
|
30
|
+
OpenOrchestrator/orchestrator/tabs/trigger_tab.py,sha256=eW8aJuJzCAy-93-EQdpfzDcsShPMJEZPSWV9uDHuIso,4405
|
31
|
+
OpenOrchestrator/orchestrator_connection/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
32
|
+
OpenOrchestrator/orchestrator_connection/connection.py,sha256=dDKyJI7PBxcbT0_6swLUvKT0myO3o-eTWrUUeXh4pxg,9675
|
33
|
+
OpenOrchestrator/scheduler/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
34
|
+
OpenOrchestrator/scheduler/application.py,sha256=TM9iXuFGW27ULESCzcGKcprGthWxOfArbCn3Qp6TI5Q,1632
|
35
|
+
OpenOrchestrator/scheduler/run_tab.py,sha256=WkfePFe1bXda2LdiMW8f4Y4AmgaB_BcW3jh4GobZpw8,6117
|
36
|
+
OpenOrchestrator/scheduler/runner.py,sha256=1WINfKZdGO1bktGymRz5Rd-Y-exK4BecH7ENaqkNIgM,8458
|
37
|
+
OpenOrchestrator/scheduler/settings_tab.py,sha256=AUIM60IZKSUIn3yKjhG5qRLlEluj8zV0BzvF7dD52ig,3980
|
38
|
+
OpenOrchestrator/scheduler/util.py,sha256=VEQ9dRgP60sOoL6uvTlvqSm0DRAGBbDIYVwPGn7krP8,185
|
39
|
+
OpenOrchestrator/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
40
|
+
OpenOrchestrator/tests/db_test_util.py,sha256=nr6FHXGvQl-0rkpRtnE3sZLk7MKZ4g5tLMoWyUey0Sc,1498
|
41
|
+
OpenOrchestrator/tests/test_db_util.py,sha256=p3XOELnU5xhPoYhm61z4bQB_H3UyF_vRjXMz5tJz_G8,13532
|
42
|
+
OpenOrchestrator/tests/test_orchestrator_connection.py,sha256=Bd_i03tZmY_dNzEfin7pT552xiivH8o7ayR0GS1xHwU,5786
|
43
|
+
OpenOrchestrator/tests/test_trigger_polling.py,sha256=sIMLLtcmHIrBlP8JvSU3HoPZQnJD5c0nnGH567M4hoE,6593
|
44
|
+
OpenOrchestrator/tests/ui_tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
45
|
+
OpenOrchestrator/tests/ui_tests/test_constants_tab.py,sha256=Xw4JIi61-tItpAy_b3joSWjDIMTv7MN79H-AlYx7pMM,7572
|
46
|
+
OpenOrchestrator/tests/ui_tests/test_logging_tab.py,sha256=rTtnY-DHAbRqGzuoJWkSWD1xU1Q4ZVaLKZ5lEx0pHdI,7067
|
47
|
+
OpenOrchestrator/tests/ui_tests/test_queues_tab.py,sha256=riZph2PapSnf88m8plCEV7UVHLVWrZ-zJz8RYDKoe0U,5491
|
48
|
+
OpenOrchestrator/tests/ui_tests/test_schedulers_tab.py,sha256=5-8Hx4qlzbs3chz9bew_JoeRpKZFmQ8iFVp0Y03MBlo,1763
|
49
|
+
OpenOrchestrator/tests/ui_tests/test_trigger_tab.py,sha256=9VgCQY0plOF8xjK5OCEN-07dMaOT_ObttKrSOdJW8p0,12521
|
50
|
+
OpenOrchestrator/tests/ui_tests/ui_util.py,sha256=1v4hmXiQ0Aunth8IO-iUFLDiROMpDm1iOLZUGmuBa4A,4822
|
51
|
+
openorchestrator-2.0.0rc1.dist-info/licenses/LICENSE,sha256=4-Kjm-gkbiOLCBYMzsVJZEepdsm2vk8QesNOASvi9mg,1068
|
52
|
+
openorchestrator-2.0.0rc1.dist-info/METADATA,sha256=HNaO8jDEq1hoZv2G9XWq5FD0JpEr_3TA9R3uzCUNAPA,4755
|
53
|
+
openorchestrator-2.0.0rc1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
54
|
+
openorchestrator-2.0.0rc1.dist-info/top_level.txt,sha256=2btKMQESHuRC_ICbCjHTHH_-us2G7CyeskeaSTTL07Y,17
|
55
|
+
openorchestrator-2.0.0rc1.dist-info/RECORD,,
|