OpenOrchestrator 1.1.0__py3-none-any.whl → 1.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
@@ -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=23406, reload=False)
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()
@@ -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=10).classes("w-full")
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):
@@ -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=10, row_key='ID').classes("w-full")
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.create_tab(notebook, self)
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')
@@ -1,124 +1,123 @@
1
1
  """This module is responsible for the layout and functionality of the run tab
2
2
  in Scheduler."""
3
3
 
4
+ from __future__ import annotations
5
+ from typing import TYPE_CHECKING
6
+
4
7
  import tkinter
5
8
  from tkinter import ttk
6
9
  import sys
7
10
 
11
+ from sqlalchemy import exc as alc_exc
12
+
8
13
  from OpenOrchestrator.common import crypto_util
9
14
  from OpenOrchestrator.database import db_util
10
15
  from OpenOrchestrator.scheduler import runner
11
16
 
17
+ if TYPE_CHECKING:
18
+ from OpenOrchestrator.scheduler.application import Application
12
19
 
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
20
 
36
- text_area = tkinter.Text(text_frame, state='disabled', wrap='none')
37
- sys.stdout.write = lambda s: print_text(text_area, s)
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)
38
27
 
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)
28
+ self.app = app
42
29
 
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)
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()
46
34
 
47
- text_area.pack()
35
+ # Text area
36
+ text_frame = tkinter.Frame(self)
37
+ text_frame.pack()
48
38
 
49
- return tab
39
+ self.text_area = tkinter.Text(text_frame, state='disabled', wrap='none')
50
40
 
41
+ # Redirect stdout to the text area instead of console
42
+ sys.stdout.write = self.print_text
51
43
 
52
- def run(app, status_label: ttk.Label) -> None:
53
- """Starts the Scheduler and sets the app's status to 'running'.
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)
54
48
 
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
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)
70
52
 
71
- # Only start loop if it's not already running
72
- if app.tk.call('after', 'info') == '':
73
- app.after(0, loop, app)
53
+ self.text_area.pack()
74
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()
75
61
 
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")
62
+ def pause(self):
63
+ """Stops the Scheduler and sets the app's status to 'paused'."""
64
+ self.button.configure(text="Run")
85
65
  print('Paused... Please wait for all processes to stop before closing the application\n')
86
- app.running = False
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
87
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)
88
84
 
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).
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).
92
88
 
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)
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)
100
95
 
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")
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")
105
100
 
106
- # Scroll to end
107
- text_widget.see('end')
108
- text_widget.configure(state='disabled')
101
+ # Scroll to end
102
+ self.text_area.see('end')
103
+ self.text_area.configure(state='disabled')
109
104
 
110
105
 
111
- def loop(app) -> None:
106
+ def loop(app: Application) -> None:
112
107
  """The main loop function of the Scheduler.
113
108
  Checks heartbeats, check triggers, and schedules the next loop.
114
109
 
115
110
  Args:
116
111
  app: The Scheduler Application object.
117
112
  """
118
- check_heartbeats(app)
113
+ try:
114
+ check_heartbeats(app)
115
+
116
+ if app.running:
117
+ check_triggers(app)
119
118
 
120
- if app.running:
121
- check_triggers(app)
119
+ except alc_exc.OperationalError:
120
+ print("Couldn't connect to database.")
122
121
 
123
122
  if len(app.running_jobs) == 0:
124
123
  print("Doing cleanup...")
@@ -132,7 +131,7 @@ def loop(app) -> None:
132
131
  print("Scheduler is paused and no more processes are running.")
133
132
 
134
133
 
135
- def check_heartbeats(app) -> None:
134
+ def check_heartbeats(app: Application) -> None:
136
135
  """Check if any running jobs are still running, failed or done.
137
136
 
138
137
  Args:
@@ -141,8 +140,6 @@ def check_heartbeats(app) -> None:
141
140
  print('Checking heartbeats...')
142
141
  for job in app.running_jobs:
143
142
  if job.process.poll() is not None:
144
- app.running_jobs.remove(job)
145
-
146
143
  if job.process.returncode == 0:
147
144
  print(f"Process '{job.trigger.process_name}' is done")
148
145
  runner.end_job(job)
@@ -150,11 +147,12 @@ def check_heartbeats(app) -> None:
150
147
  print(f"Process '{job.trigger.process_name}' failed. Check process log for more info.")
151
148
  runner.fail_job(job)
152
149
 
150
+ app.running_jobs.remove(job)
153
151
  else:
154
152
  print(f"Process '{job.trigger.process_name}' is still running")
155
153
 
156
154
 
157
- def check_triggers(app) -> None:
155
+ def check_triggers(app: Application) -> None:
158
156
  """Checks any process is blocking
159
157
  and if not checks if any trigger should be run.
160
158
 
@@ -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
- process = run_process(trigger)
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
- process = run_process(trigger)
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
- process = run_process(trigger)
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
- subprocess.run(['rmdir', '/s', '/q', repo_folder], check=False, shell=True, capture_output=True)
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
- def run_process(trigger: Trigger) -> subprocess.Popen | None:
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
- subprocess.Popen: The Popen instance of the process if successful.
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
- git_folder_path = clone_git_repo(process_path)
234
- process_path = find_main_file(git_folder_path)
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
- return subprocess.Popen(command_args, stderr=subprocess.PIPE, text=True)
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.1.0
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
@@ -6,12 +6,12 @@ OpenOrchestrator/common/crypto_util.py,sha256=Yo3WvIJnWtBJvlbvkDRu-HaReUUdaW747P
6
6
  OpenOrchestrator/common/datetime_util.py,sha256=xNf_LORb67ohR3KfWE_owY15RqAOLJ8DfDPQlMBfsGc,527
7
7
  OpenOrchestrator/database/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
8
  OpenOrchestrator/database/constants.py,sha256=ik6lY6IR8EOyKEg-N70X-0U654i3nFb3u-XWhqbUbJU,2404
9
- OpenOrchestrator/database/db_util.py,sha256=wqIK01ODOnmG5dfz5doPLDTVJTKTMuaWibk87fGr7T4,27599
9
+ OpenOrchestrator/database/db_util.py,sha256=VViN_V7o2zhKJcxsv-jMQmwecvXRavWgLsBKQZYm61o,27568
10
10
  OpenOrchestrator/database/logs.py,sha256=h2BztjmDRhj8TACYN3LK5c9hIqn5xCs9m_LDUdYYHEk,1623
11
11
  OpenOrchestrator/database/queues.py,sha256=BFCbUUbo_OOCPNCzRRxDcgoxos_LI__Ypg7t9e3qdUk,2264
12
12
  OpenOrchestrator/database/triggers.py,sha256=Qq1lsSNrOYBKXnKhaD5s71YVmviaTxiD8j8kMBc76sg,4004
13
13
  OpenOrchestrator/orchestrator/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
- OpenOrchestrator/orchestrator/application.py,sha256=RgAyc-O79AtrgqOgUoKA5dcusBtIVBxjGpf-jCqAkdE,2804
14
+ OpenOrchestrator/orchestrator/application.py,sha256=Sb0rMrlFsvwkXuTLvBLqTUNqFSYjvdrAz3keiQYiUvY,3224
15
15
  OpenOrchestrator/orchestrator/datetime_input.py,sha256=1EEkmKxYOQSIE6e-anbjACgbo7SL4wyuhqerdiBIg5w,2571
16
16
  OpenOrchestrator/orchestrator/popups/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
17
  OpenOrchestrator/orchestrator/popups/constant_popup.py,sha256=B6-xfEYRNNkf4dwbPgjGRuDqfKDL5pa6wUoZJ-A2l4g,3367
@@ -20,19 +20,19 @@ OpenOrchestrator/orchestrator/popups/generic_popups.py,sha256=wV5isJrwFGUBpDhDpu
20
20
  OpenOrchestrator/orchestrator/popups/trigger_popup.py,sha256=Ir8Hx-DHtvCu3qm5BkG0uSRTObM9G4fpk-VOfeajelA,9684
21
21
  OpenOrchestrator/orchestrator/tabs/constants_tab.py,sha256=JcRIyntrzZBh5Ugkb0QrkBrjI-g9xl2VpbeKhdT6bFM,2478
22
22
  OpenOrchestrator/orchestrator/tabs/logging_tab.py,sha256=7HdXAnAMDHGC6Au8FaQ58M8i88oalBBtBAvGvdiRD_0,3527
23
- OpenOrchestrator/orchestrator/tabs/queue_tab.py,sha256=Yl_THeka0ykM-IlbsRpktxiGxZwhKGx0T-XC4S50aQI,5169
23
+ OpenOrchestrator/orchestrator/tabs/queue_tab.py,sha256=3JwrQUFr796uDmASDESjal1py18OT-Vq5K_VrnPlHCk,5169
24
24
  OpenOrchestrator/orchestrator/tabs/settings_tab.py,sha256=xBMBdgrTxehmbuAk-H3QfEP_zdILzJ3sIgMCii7qlQg,845
25
- OpenOrchestrator/orchestrator/tabs/trigger_tab.py,sha256=qIEI3BEuKaknuV7EcH1fNTbGULVR4zAtHdADx5W-26c,4047
25
+ OpenOrchestrator/orchestrator/tabs/trigger_tab.py,sha256=gRwLINuf4wQcFieNZL1grlc5qmPOqGRAI1-TS1za_5Q,4047
26
26
  OpenOrchestrator/orchestrator_connection/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
27
  OpenOrchestrator/orchestrator_connection/connection.py,sha256=DU552EigOu2xzM3SjY4766BXrDEnDC2jC4Jjy0-9OrU,8706
28
28
  OpenOrchestrator/scheduler/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
29
- OpenOrchestrator/scheduler/application.py,sha256=W2nkI6q6BCkgzmIMp3P_OHY-uZN5clACCa9rLtZg5-U,1624
29
+ OpenOrchestrator/scheduler/application.py,sha256=vjDzW8x0MwS9giFI6UOlhtFqUPHKbsRnrb6-HY8jWOI,1620
30
30
  OpenOrchestrator/scheduler/connection_frame.py,sha256=I1Qooso_iqkNhvztQvFgjlC89Lvfqx0s5B6olATqa98,3369
31
- OpenOrchestrator/scheduler/run_tab.py,sha256=huwx5pQ5DhPCXgpDtK7b49BwyxuDdfZ9Dwh_P5kfWBY,5456
32
- OpenOrchestrator/scheduler/runner.py,sha256=2oC-WvfFcdyCWnbLCYh-jybA4t6cnayN__mSii7bW3U,7980
31
+ OpenOrchestrator/scheduler/run_tab.py,sha256=QP-hoZEJAI5A-T7kV1lFCftpTm_e_R3T8I2Fg9BHxnA,5779
32
+ OpenOrchestrator/scheduler/runner.py,sha256=0lI-Xpv4Co617ZvTrRm9pCAZP1-aNg0P-Vy1c1oy7cM,8170
33
33
  OpenOrchestrator/scheduler/settings_tab.py,sha256=AQxt5HxPn4sLfj-6GwyAQ8ffJ35D0PHLBVoi8W3tu2A,613
34
- OpenOrchestrator-1.1.0.dist-info/LICENSE,sha256=4-Kjm-gkbiOLCBYMzsVJZEepdsm2vk8QesNOASvi9mg,1068
35
- OpenOrchestrator-1.1.0.dist-info/METADATA,sha256=6COV_ps1Vx9CEOvt-Nj5D7fCGv7lyf943QPTaRRGQbk,1938
36
- OpenOrchestrator-1.1.0.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
37
- OpenOrchestrator-1.1.0.dist-info/top_level.txt,sha256=2btKMQESHuRC_ICbCjHTHH_-us2G7CyeskeaSTTL07Y,17
38
- OpenOrchestrator-1.1.0.dist-info/RECORD,,
34
+ OpenOrchestrator-1.2.0.dist-info/LICENSE,sha256=4-Kjm-gkbiOLCBYMzsVJZEepdsm2vk8QesNOASvi9mg,1068
35
+ OpenOrchestrator-1.2.0.dist-info/METADATA,sha256=Y0hvXWs-UDhtwmJ4X9YTSGeZwqmxykiulkZleWTBwXw,1938
36
+ OpenOrchestrator-1.2.0.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
37
+ OpenOrchestrator-1.2.0.dist-info/top_level.txt,sha256=2btKMQESHuRC_ICbCjHTHH_-us2G7CyeskeaSTTL07Y,17
38
+ OpenOrchestrator-1.2.0.dist-info/RECORD,,