OpenOrchestrator 1.1.0__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.
- {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/database/db_util.py +1 -3
- {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/orchestrator/application.py +18 -1
- {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/orchestrator/tabs/queue_tab.py +1 -1
- {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/orchestrator/tabs/trigger_tab.py +1 -1
- {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/scheduler/application.py +1 -1
- OpenOrchestrator-1.2.0/OpenOrchestrator/scheduler/run_tab.py +175 -0
- {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/scheduler/runner.py +28 -18
- {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0/OpenOrchestrator.egg-info}/PKG-INFO +1 -1
- {OpenOrchestrator-1.1.0/OpenOrchestrator.egg-info → OpenOrchestrator-1.2.0}/PKG-INFO +1 -1
- {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/pyproject.toml +1 -1
- OpenOrchestrator-1.1.0/OpenOrchestrator/scheduler/run_tab.py +0 -177
- {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/LICENSE +0 -0
- {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/__init__.py +0 -0
- {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/__main__.py +0 -0
- {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/common/__init__.py +0 -0
- {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/common/connection_frame.py +0 -0
- {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/common/crypto_util.py +0 -0
- {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/common/datetime_util.py +0 -0
- {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/database/__init__.py +0 -0
- {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/database/constants.py +0 -0
- {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/database/logs.py +0 -0
- {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/database/queues.py +0 -0
- {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/database/triggers.py +0 -0
- {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/orchestrator/__init__.py +0 -0
- {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/orchestrator/datetime_input.py +0 -0
- {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/orchestrator/popups/__init__.py +0 -0
- {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/orchestrator/popups/constant_popup.py +0 -0
- {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/orchestrator/popups/credential_popup.py +0 -0
- {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/orchestrator/popups/generic_popups.py +0 -0
- {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/orchestrator/popups/trigger_popup.py +0 -0
- {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/orchestrator/tabs/constants_tab.py +0 -0
- {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/orchestrator/tabs/logging_tab.py +0 -0
- {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/orchestrator/tabs/settings_tab.py +0 -0
- {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/orchestrator_connection/__init__.py +0 -0
- {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/orchestrator_connection/connection.py +0 -0
- {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/scheduler/__init__.py +0 -0
- {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/scheduler/connection_frame.py +0 -0
- {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/scheduler/settings_tab.py +0 -0
- {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator.egg-info/SOURCES.txt +0 -0
- {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator.egg-info/dependency_links.txt +0 -0
- {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator.egg-info/requires.txt +0 -0
- {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator.egg-info/top_level.txt +0 -0
- {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/README.md +0 -0
- {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/setup.cfg +0 -0
- {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/tests/test_db_util.py +0 -0
- {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/tests/test_orchestrator_connection.py +0 -0
@@ -601,9 +601,7 @@ def begin_scheduled_trigger(trigger_id: str) -> bool:
|
|
601
601
|
|
602
602
|
trigger.process_status = TriggerStatus.RUNNING
|
603
603
|
trigger.last_run = datetime.now()
|
604
|
-
|
605
|
-
next_run = croniter(trigger.cron_expr, trigger.next_run).get_next(datetime)
|
606
|
-
trigger.next_run = next_run
|
604
|
+
trigger.next_run = croniter(trigger.cron_expr, datetime.now()).get_next(datetime)
|
607
605
|
|
608
606
|
session.commit()
|
609
607
|
return True
|
{OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/orchestrator/application.py
RENAMED
@@ -1,6 +1,8 @@
|
|
1
1
|
"""This module is the entry point for the Orchestrator app. It contains a single class
|
2
2
|
that when created starts the application."""
|
3
3
|
|
4
|
+
import socket
|
5
|
+
|
4
6
|
from nicegui import ui, app
|
5
7
|
|
6
8
|
from OpenOrchestrator.orchestrator.tabs.trigger_tab import TriggerTab
|
@@ -39,7 +41,7 @@ class Application():
|
|
39
41
|
app.on_connect(self.update_loop)
|
40
42
|
app.on_disconnect(app.shutdown)
|
41
43
|
app.on_exception(lambda exc: ui.notify(exc, type='negative'))
|
42
|
-
ui.run(title="Orchestrator", favicon='🤖', native=False, port=
|
44
|
+
ui.run(title="Orchestrator", favicon='🤖', native=False, port=get_free_port(), reload=False)
|
43
45
|
|
44
46
|
def update_tab(self):
|
45
47
|
"""Update the date in the currently selected tab."""
|
@@ -73,5 +75,20 @@ class Application():
|
|
73
75
|
''')
|
74
76
|
|
75
77
|
|
78
|
+
def get_free_port():
|
79
|
+
"""Get a free port by creating a new socket and bind it
|
80
|
+
on port 0 allowing the os to select the port.
|
81
|
+
https://docs.python.org/3/library/socket.html#socket.create_connection
|
82
|
+
|
83
|
+
Returns:
|
84
|
+
A port number that should be free to use.
|
85
|
+
"""
|
86
|
+
with socket.socket() as sock:
|
87
|
+
sock.bind(("", 0))
|
88
|
+
port = sock.getsockname()[1]
|
89
|
+
|
90
|
+
return port
|
91
|
+
|
92
|
+
|
76
93
|
if __name__ in {'__main__', '__mp_main__'}:
|
77
94
|
Application()
|
{OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/orchestrator/tabs/queue_tab.py
RENAMED
@@ -33,7 +33,7 @@ class QueueTab():
|
|
33
33
|
"""The 'Queues' tab object. It contains tables and buttons for dealing with queues."""
|
34
34
|
def __init__(self, tab_name: str) -> None:
|
35
35
|
with ui.tab_panel(tab_name):
|
36
|
-
self.queue_table = ui.table(title="Queues", columns=QUEUE_COLUMNS, rows=[], row_key='Queue Name', pagination=
|
36
|
+
self.queue_table = ui.table(title="Queues", columns=QUEUE_COLUMNS, rows=[], row_key='Queue Name', pagination=50).classes("w-full")
|
37
37
|
self.queue_table.on("rowClick", self._row_click)
|
38
38
|
|
39
39
|
def update(self):
|
{OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/orchestrator/tabs/trigger_tab.py
RENAMED
@@ -30,7 +30,7 @@ class TriggerTab():
|
|
30
30
|
ui.button("New Scheduled Trigger", icon="add", on_click=lambda e: TriggerPopup(self, TriggerType.SCHEDULED))
|
31
31
|
ui.button("New Queue Trigger", icon="add", on_click=lambda e: TriggerPopup(self, TriggerType.QUEUE))
|
32
32
|
|
33
|
-
self.trigger_table = ui.table(COLUMNS, [], title="Triggers", pagination=
|
33
|
+
self.trigger_table = ui.table(COLUMNS, [], title="Triggers", pagination=50, row_key='ID').classes("w-full")
|
34
34
|
self.trigger_table.on('rowClick', self._row_click)
|
35
35
|
|
36
36
|
self.add_column_colors()
|
@@ -26,7 +26,7 @@ class Application(tkinter.Tk):
|
|
26
26
|
notebook = ttk.Notebook(self)
|
27
27
|
notebook.pack(expand=True, fill='both')
|
28
28
|
|
29
|
-
run_tab_ = run_tab.
|
29
|
+
run_tab_ = run_tab.RunTab(notebook, self)
|
30
30
|
settings_tab_ = settings_tab.create_tab(notebook)
|
31
31
|
|
32
32
|
notebook.add(run_tab_, text='Run')
|
@@ -0,0 +1,175 @@
|
|
1
|
+
"""This module is responsible for the layout and functionality of the run tab
|
2
|
+
in Scheduler."""
|
3
|
+
|
4
|
+
from __future__ import annotations
|
5
|
+
from typing import TYPE_CHECKING
|
6
|
+
|
7
|
+
import tkinter
|
8
|
+
from tkinter import ttk
|
9
|
+
import sys
|
10
|
+
|
11
|
+
from sqlalchemy import exc as alc_exc
|
12
|
+
|
13
|
+
from OpenOrchestrator.common import crypto_util
|
14
|
+
from OpenOrchestrator.database import db_util
|
15
|
+
from OpenOrchestrator.scheduler import runner
|
16
|
+
|
17
|
+
if TYPE_CHECKING:
|
18
|
+
from OpenOrchestrator.scheduler.application import Application
|
19
|
+
|
20
|
+
|
21
|
+
# pylint: disable-next=too-many-ancestors
|
22
|
+
class RunTab(ttk.Frame):
|
23
|
+
"""A ttk.frame object containing the functionality of the run tab in Scheduler."""
|
24
|
+
def __init__(self, parent: ttk.Notebook, app: Application):
|
25
|
+
super().__init__(parent)
|
26
|
+
self.pack(fill='both', expand=True)
|
27
|
+
|
28
|
+
self.app = app
|
29
|
+
|
30
|
+
s = ttk.Style()
|
31
|
+
s.configure('my.TButton', font=('Helvetica Bold', 24))
|
32
|
+
self.button = ttk.Button(self, text="Run", command=self.button_click, style='my.TButton')
|
33
|
+
self.button.pack()
|
34
|
+
|
35
|
+
# Text area
|
36
|
+
text_frame = tkinter.Frame(self)
|
37
|
+
text_frame.pack()
|
38
|
+
|
39
|
+
self.text_area = tkinter.Text(text_frame, state='disabled', wrap='none')
|
40
|
+
|
41
|
+
# Redirect stdout to the text area instead of console
|
42
|
+
sys.stdout.write = self.print_text
|
43
|
+
|
44
|
+
# Add scroll bars to text area
|
45
|
+
text_yscroll = ttk.Scrollbar(text_frame, orient='vertical', command=self.text_area.yview)
|
46
|
+
text_yscroll.pack(side='right', fill='y')
|
47
|
+
self.text_area.configure(yscrollcommand=text_yscroll.set)
|
48
|
+
|
49
|
+
text_xscroll = ttk.Scrollbar(text_frame, orient='horizontal', command=self.text_area.xview)
|
50
|
+
text_xscroll.pack(side='bottom', fill='x')
|
51
|
+
self.text_area.configure(xscrollcommand=text_xscroll.set)
|
52
|
+
|
53
|
+
self.text_area.pack()
|
54
|
+
|
55
|
+
def button_click(self):
|
56
|
+
"""Callback for when the run/pause button is clicked."""
|
57
|
+
if self.app.running:
|
58
|
+
self.pause()
|
59
|
+
else:
|
60
|
+
self.run()
|
61
|
+
|
62
|
+
def pause(self):
|
63
|
+
"""Stops the Scheduler and sets the app's status to 'paused'."""
|
64
|
+
self.button.configure(text="Run")
|
65
|
+
print('Paused... Please wait for all processes to stop before closing the application\n')
|
66
|
+
self.app.running = False
|
67
|
+
|
68
|
+
def run(self):
|
69
|
+
"""Starts the Scheduler and sets the app's status to 'running'."""
|
70
|
+
if db_util.get_conn_string() is None:
|
71
|
+
print("Can't start without a valid connection string. Go to the settings tab to configure the connection string")
|
72
|
+
return
|
73
|
+
if crypto_util.get_key() is None:
|
74
|
+
print("Can't start without a valid encryption key. Go to the settings tab to configure the encryption key")
|
75
|
+
return
|
76
|
+
|
77
|
+
self.button.configure(text="Pause")
|
78
|
+
print('Running...\n')
|
79
|
+
self.app.running = True
|
80
|
+
|
81
|
+
# Only start a new loop if it's not already running
|
82
|
+
if self.app.tk.call('after', 'info') == '':
|
83
|
+
self.app.after(0, loop, self.app)
|
84
|
+
|
85
|
+
def print_text(self, text: str) -> None:
|
86
|
+
"""Appends text to the text area.
|
87
|
+
Is used to replace the functionality of sys.stdout.write (print).
|
88
|
+
|
89
|
+
Args:
|
90
|
+
string: The string to append.
|
91
|
+
"""
|
92
|
+
# Insert text at the end
|
93
|
+
self.text_area.configure(state='normal')
|
94
|
+
self.text_area.insert('end', text)
|
95
|
+
|
96
|
+
# If the number of lines are above 1000 delete 10 lines from the top
|
97
|
+
num_lines = int(self.text_area.index('end').split('.', maxsplit=1)[0])
|
98
|
+
if num_lines > 1000:
|
99
|
+
self.text_area.delete("1.0", "10.0")
|
100
|
+
|
101
|
+
# Scroll to end
|
102
|
+
self.text_area.see('end')
|
103
|
+
self.text_area.configure(state='disabled')
|
104
|
+
|
105
|
+
|
106
|
+
def loop(app: Application) -> None:
|
107
|
+
"""The main loop function of the Scheduler.
|
108
|
+
Checks heartbeats, check triggers, and schedules the next loop.
|
109
|
+
|
110
|
+
Args:
|
111
|
+
app: The Scheduler Application object.
|
112
|
+
"""
|
113
|
+
try:
|
114
|
+
check_heartbeats(app)
|
115
|
+
|
116
|
+
if app.running:
|
117
|
+
check_triggers(app)
|
118
|
+
|
119
|
+
except alc_exc.OperationalError:
|
120
|
+
print("Couldn't connect to database.")
|
121
|
+
|
122
|
+
if len(app.running_jobs) == 0:
|
123
|
+
print("Doing cleanup...")
|
124
|
+
runner.clear_repo_folder()
|
125
|
+
|
126
|
+
# Schedule next loop
|
127
|
+
if app.running or len(app.running_jobs) > 0:
|
128
|
+
print('Waiting 6 seconds...\n')
|
129
|
+
app.after(6_000, loop, app)
|
130
|
+
else:
|
131
|
+
print("Scheduler is paused and no more processes are running.")
|
132
|
+
|
133
|
+
|
134
|
+
def check_heartbeats(app: Application) -> None:
|
135
|
+
"""Check if any running jobs are still running, failed or done.
|
136
|
+
|
137
|
+
Args:
|
138
|
+
app: The Scheduler Application object.
|
139
|
+
"""
|
140
|
+
print('Checking heartbeats...')
|
141
|
+
for job in app.running_jobs:
|
142
|
+
if job.process.poll() is not None:
|
143
|
+
if job.process.returncode == 0:
|
144
|
+
print(f"Process '{job.trigger.process_name}' is done")
|
145
|
+
runner.end_job(job)
|
146
|
+
else:
|
147
|
+
print(f"Process '{job.trigger.process_name}' failed. Check process log for more info.")
|
148
|
+
runner.fail_job(job)
|
149
|
+
|
150
|
+
app.running_jobs.remove(job)
|
151
|
+
else:
|
152
|
+
print(f"Process '{job.trigger.process_name}' is still running")
|
153
|
+
|
154
|
+
|
155
|
+
def check_triggers(app: Application) -> None:
|
156
|
+
"""Checks any process is blocking
|
157
|
+
and if not checks if any trigger should be run.
|
158
|
+
|
159
|
+
Args:
|
160
|
+
app: The Scheduler Application object.
|
161
|
+
"""
|
162
|
+
# Check if process is blocking
|
163
|
+
blocking = False
|
164
|
+
for job in app.running_jobs:
|
165
|
+
if job.trigger.is_blocking:
|
166
|
+
print(f"Process '{job.trigger.process_name}' is blocking\n")
|
167
|
+
blocking = True
|
168
|
+
|
169
|
+
# Check triggers
|
170
|
+
if not blocking:
|
171
|
+
print('Checking triggers...')
|
172
|
+
job = runner.poll_triggers(app)
|
173
|
+
|
174
|
+
if job is not None:
|
175
|
+
app.running_jobs.append(job)
|
@@ -16,6 +16,7 @@ class Job():
|
|
16
16
|
"""An object that holds information about a running job."""
|
17
17
|
process: subprocess.Popen
|
18
18
|
trigger: Trigger
|
19
|
+
process_folder: str
|
19
20
|
|
20
21
|
|
21
22
|
def poll_triggers(app) -> Job | None:
|
@@ -66,10 +67,7 @@ def run_single_trigger(trigger: SingleTrigger) -> Job | None:
|
|
66
67
|
print('Running trigger: ', trigger.trigger_name)
|
67
68
|
|
68
69
|
if db_util.begin_single_trigger(trigger.id):
|
69
|
-
|
70
|
-
|
71
|
-
if process is not None:
|
72
|
-
return Job(process, trigger)
|
70
|
+
return run_process(trigger)
|
73
71
|
|
74
72
|
return None
|
75
73
|
|
@@ -88,10 +86,7 @@ def run_scheduled_trigger(trigger: ScheduledTrigger) -> Job | None:
|
|
88
86
|
print('Running trigger: ', trigger.trigger_name)
|
89
87
|
|
90
88
|
if db_util.begin_scheduled_trigger(trigger.id):
|
91
|
-
|
92
|
-
|
93
|
-
if process is not None:
|
94
|
-
return Job(process, trigger)
|
89
|
+
return run_process(trigger)
|
95
90
|
|
96
91
|
return None
|
97
92
|
|
@@ -109,10 +104,7 @@ def run_queue_trigger(trigger: QueueTrigger) -> Job | None:
|
|
109
104
|
print('Running trigger: ', trigger.trigger_name)
|
110
105
|
|
111
106
|
if db_util.begin_queue_trigger(trigger.id):
|
112
|
-
|
113
|
-
|
114
|
-
if process is not None:
|
115
|
-
return Job(process, trigger)
|
107
|
+
return run_process(trigger)
|
116
108
|
|
117
109
|
return None
|
118
110
|
|
@@ -142,7 +134,7 @@ def clone_git_repo(repo_url: str) -> str:
|
|
142
134
|
def clear_repo_folder() -> None:
|
143
135
|
"""Completely remove the repos folder."""
|
144
136
|
repo_folder = get_repo_folder_path()
|
145
|
-
|
137
|
+
clear_folder(repo_folder)
|
146
138
|
|
147
139
|
|
148
140
|
def get_repo_folder_path() -> str:
|
@@ -156,6 +148,15 @@ def get_repo_folder_path() -> str:
|
|
156
148
|
return repo_path
|
157
149
|
|
158
150
|
|
151
|
+
def clear_folder(folder_path: str) -> None:
|
152
|
+
"""Clear a folder on the system.
|
153
|
+
|
154
|
+
Args:
|
155
|
+
folder_path: The folder to remove.
|
156
|
+
"""
|
157
|
+
subprocess.run(['rmdir', '/s', '/q', folder_path], check=False, shell=True, capture_output=True)
|
158
|
+
|
159
|
+
|
159
160
|
def find_main_file(folder_path: str) -> str:
|
160
161
|
"""Finds the file in the given folder with the name 'main.py'.
|
161
162
|
The search checks subfolders recursively.
|
@@ -193,6 +194,9 @@ def end_job(job: Job) -> None:
|
|
193
194
|
elif isinstance(job.trigger, QueueTrigger):
|
194
195
|
db_util.set_trigger_status(job.trigger.id, TriggerStatus.IDLE)
|
195
196
|
|
197
|
+
if job.process_folder:
|
198
|
+
clear_folder(job.process_folder)
|
199
|
+
|
196
200
|
|
197
201
|
def fail_job(job: Job) -> None:
|
198
202
|
"""Mark a job as failed in the triggers table in the database.
|
@@ -205,8 +209,11 @@ def fail_job(job: Job) -> None:
|
|
205
209
|
error_msg = f"An uncaught error ocurred during the process:\n{error}"
|
206
210
|
db_util.create_log(job.trigger.process_name, LogLevel.ERROR, error_msg)
|
207
211
|
|
212
|
+
if job.process_folder:
|
213
|
+
clear_folder(job.process_folder)
|
208
214
|
|
209
|
-
|
215
|
+
|
216
|
+
def run_process(trigger: Trigger) -> Job | None:
|
210
217
|
"""Runs the process of the given trigger with the necessary inputs:
|
211
218
|
Process name
|
212
219
|
Connection string
|
@@ -224,14 +231,15 @@ def run_process(trigger: Trigger) -> subprocess.Popen | None:
|
|
224
231
|
trigger: The trigger whose process to run.
|
225
232
|
|
226
233
|
Returns:
|
227
|
-
|
234
|
+
Job: A Job object referencing the process if succesful.
|
228
235
|
"""
|
229
236
|
process_path = trigger.process_path
|
237
|
+
folder_path = None
|
230
238
|
|
231
239
|
try:
|
232
240
|
if trigger.is_git_repo:
|
233
|
-
|
234
|
-
process_path = find_main_file(
|
241
|
+
folder_path = clone_git_repo(process_path)
|
242
|
+
process_path = find_main_file(folder_path)
|
235
243
|
|
236
244
|
if not os.path.isfile(process_path):
|
237
245
|
raise ValueError(f"The process path didn't point to a file on the system. Path: '{process_path}'")
|
@@ -244,7 +252,9 @@ def run_process(trigger: Trigger) -> subprocess.Popen | None:
|
|
244
252
|
|
245
253
|
command_args = ['python', process_path, trigger.process_name, conn_string, crypto_key, trigger.process_args]
|
246
254
|
|
247
|
-
|
255
|
+
process = subprocess.Popen(command_args, stderr=subprocess.PIPE, text=True) # pylint: disable=consider-using-with
|
256
|
+
|
257
|
+
return Job(process, trigger, folder_path)
|
248
258
|
|
249
259
|
# We actually want to catch any exception here
|
250
260
|
# pylint: disable=broad-exception-caught
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: OpenOrchestrator
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.2.0
|
4
4
|
Summary: A package containing OpenOrchestrator and OpenOrchestrator Scheduler
|
5
5
|
Author-email: ITK Development <itk-rpa@mkb.aarhus.dk>
|
6
6
|
Project-URL: Homepage, https://github.com/itk-dev-rpa/OpenOrchestrator
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: OpenOrchestrator
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.2.0
|
4
4
|
Summary: A package containing OpenOrchestrator and OpenOrchestrator Scheduler
|
5
5
|
Author-email: ITK Development <itk-rpa@mkb.aarhus.dk>
|
6
6
|
Project-URL: Homepage, https://github.com/itk-dev-rpa/OpenOrchestrator
|
@@ -1,177 +0,0 @@
|
|
1
|
-
"""This module is responsible for the layout and functionality of the run tab
|
2
|
-
in Scheduler."""
|
3
|
-
|
4
|
-
import tkinter
|
5
|
-
from tkinter import ttk
|
6
|
-
import sys
|
7
|
-
|
8
|
-
from OpenOrchestrator.common import crypto_util
|
9
|
-
from OpenOrchestrator.database import db_util
|
10
|
-
from OpenOrchestrator.scheduler import runner
|
11
|
-
|
12
|
-
|
13
|
-
def create_tab(parent: ttk.Notebook, app) -> ttk.Frame:
|
14
|
-
"""Create a new Run tab object.
|
15
|
-
|
16
|
-
Args:
|
17
|
-
parent: The ttk.Notebook object that this tab is a child of.
|
18
|
-
app: The Scheduler application object.
|
19
|
-
|
20
|
-
Returns:
|
21
|
-
ttk.Frame: The created tab object as a ttk.Frame.
|
22
|
-
"""
|
23
|
-
tab = ttk.Frame(parent)
|
24
|
-
tab.pack(fill='both', expand=True)
|
25
|
-
|
26
|
-
status_label = ttk.Label(tab, text="State: Paused")
|
27
|
-
status_label.pack()
|
28
|
-
|
29
|
-
ttk.Button(tab, text="Run", command=lambda: run(app, status_label)).pack()
|
30
|
-
ttk.Button(tab, text="Pause", command=lambda: pause(app, status_label)).pack()
|
31
|
-
|
32
|
-
# Text area
|
33
|
-
text_frame = tkinter.Frame(tab)
|
34
|
-
text_frame.pack()
|
35
|
-
|
36
|
-
text_area = tkinter.Text(text_frame, state='disabled', wrap='none')
|
37
|
-
sys.stdout.write = lambda s: print_text(text_area, s)
|
38
|
-
|
39
|
-
text_yscroll = ttk.Scrollbar(text_frame, orient='vertical', command=text_area.yview)
|
40
|
-
text_yscroll.pack(side='right', fill='y')
|
41
|
-
text_area.configure(yscrollcommand=text_yscroll.set)
|
42
|
-
|
43
|
-
text_xscroll = ttk.Scrollbar(text_frame, orient='horizontal', command=text_area.xview)
|
44
|
-
text_xscroll.pack(side='bottom', fill='x')
|
45
|
-
text_area.configure(xscrollcommand=text_xscroll.set)
|
46
|
-
|
47
|
-
text_area.pack()
|
48
|
-
|
49
|
-
return tab
|
50
|
-
|
51
|
-
|
52
|
-
def run(app, status_label: ttk.Label) -> None:
|
53
|
-
"""Starts the Scheduler and sets the app's status to 'running'.
|
54
|
-
|
55
|
-
Args:
|
56
|
-
app: The Scheduler application object.
|
57
|
-
status_label: The label showing the current status.
|
58
|
-
"""
|
59
|
-
if db_util.get_conn_string() is None:
|
60
|
-
print("Can't start without a valid connection string. Go to the settings tab to configure the connection string")
|
61
|
-
return
|
62
|
-
if crypto_util.get_key() is None:
|
63
|
-
print("Can't start without a valid encryption key. Go to the settings tab to configure the encryption key")
|
64
|
-
return
|
65
|
-
|
66
|
-
if not app.running:
|
67
|
-
status_label.configure(text='State: Running')
|
68
|
-
print('Running...\n')
|
69
|
-
app.running = True
|
70
|
-
|
71
|
-
# Only start loop if it's not already running
|
72
|
-
if app.tk.call('after', 'info') == '':
|
73
|
-
app.after(0, loop, app)
|
74
|
-
|
75
|
-
|
76
|
-
def pause(app, status_label: ttk.Label):
|
77
|
-
"""Stops the Scheduler and sets the app's status to 'paused'.
|
78
|
-
|
79
|
-
Args:
|
80
|
-
app: The Scheduler application object.
|
81
|
-
status_label: The label showing the current status.
|
82
|
-
"""
|
83
|
-
if app.running:
|
84
|
-
status_label.configure(text="State: Paused")
|
85
|
-
print('Paused... Please wait for all processes to stop before closing the application\n')
|
86
|
-
app.running = False
|
87
|
-
|
88
|
-
|
89
|
-
def print_text(text_widget: tkinter.Text, text: str) -> None:
|
90
|
-
"""Appends text to the text area.
|
91
|
-
Is used to replace the functionality of sys.stdout.write (print).
|
92
|
-
|
93
|
-
Args:
|
94
|
-
print_text: The text area object.
|
95
|
-
string: The string to append.
|
96
|
-
"""
|
97
|
-
# Insert text at the end
|
98
|
-
text_widget.configure(state='normal')
|
99
|
-
text_widget.insert('end', text)
|
100
|
-
|
101
|
-
# If the number of lines are above 1000 delete 10 lines from the top
|
102
|
-
num_lines = int(text_widget.index('end').split('.')[0])
|
103
|
-
if num_lines > 1000:
|
104
|
-
text_widget.delete("1.0", "10.0")
|
105
|
-
|
106
|
-
# Scroll to end
|
107
|
-
text_widget.see('end')
|
108
|
-
text_widget.configure(state='disabled')
|
109
|
-
|
110
|
-
|
111
|
-
def loop(app) -> None:
|
112
|
-
"""The main loop function of the Scheduler.
|
113
|
-
Checks heartbeats, check triggers, and schedules the next loop.
|
114
|
-
|
115
|
-
Args:
|
116
|
-
app: The Scheduler Application object.
|
117
|
-
"""
|
118
|
-
check_heartbeats(app)
|
119
|
-
|
120
|
-
if app.running:
|
121
|
-
check_triggers(app)
|
122
|
-
|
123
|
-
if len(app.running_jobs) == 0:
|
124
|
-
print("Doing cleanup...")
|
125
|
-
runner.clear_repo_folder()
|
126
|
-
|
127
|
-
# Schedule next loop
|
128
|
-
if app.running or len(app.running_jobs) > 0:
|
129
|
-
print('Waiting 6 seconds...\n')
|
130
|
-
app.after(6_000, loop, app)
|
131
|
-
else:
|
132
|
-
print("Scheduler is paused and no more processes are running.")
|
133
|
-
|
134
|
-
|
135
|
-
def check_heartbeats(app) -> None:
|
136
|
-
"""Check if any running jobs are still running, failed or done.
|
137
|
-
|
138
|
-
Args:
|
139
|
-
app: The Scheduler Application object.
|
140
|
-
"""
|
141
|
-
print('Checking heartbeats...')
|
142
|
-
for job in app.running_jobs:
|
143
|
-
if job.process.poll() is not None:
|
144
|
-
app.running_jobs.remove(job)
|
145
|
-
|
146
|
-
if job.process.returncode == 0:
|
147
|
-
print(f"Process '{job.trigger.process_name}' is done")
|
148
|
-
runner.end_job(job)
|
149
|
-
else:
|
150
|
-
print(f"Process '{job.trigger.process_name}' failed. Check process log for more info.")
|
151
|
-
runner.fail_job(job)
|
152
|
-
|
153
|
-
else:
|
154
|
-
print(f"Process '{job.trigger.process_name}' is still running")
|
155
|
-
|
156
|
-
|
157
|
-
def check_triggers(app) -> None:
|
158
|
-
"""Checks any process is blocking
|
159
|
-
and if not checks if any trigger should be run.
|
160
|
-
|
161
|
-
Args:
|
162
|
-
app: The Scheduler Application object.
|
163
|
-
"""
|
164
|
-
# Check if process is blocking
|
165
|
-
blocking = False
|
166
|
-
for job in app.running_jobs:
|
167
|
-
if job.trigger.is_blocking:
|
168
|
-
print(f"Process '{job.trigger.process_name}' is blocking\n")
|
169
|
-
blocking = True
|
170
|
-
|
171
|
-
# Check triggers
|
172
|
-
if not blocking:
|
173
|
-
print('Checking triggers...')
|
174
|
-
job = runner.poll_triggers(app)
|
175
|
-
|
176
|
-
if job is not None:
|
177
|
-
app.running_jobs.append(job)
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/common/connection_frame.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/orchestrator/datetime_input.py
RENAMED
File without changes
|
{OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/orchestrator/popups/__init__.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/orchestrator/tabs/logging_tab.py
RENAMED
File without changes
|
{OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/orchestrator/tabs/settings_tab.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/scheduler/connection_frame.py
RENAMED
File without changes
|
{OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/scheduler/settings_tab.py
RENAMED
File without changes
|
File without changes
|
{OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator.egg-info/dependency_links.txt
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|