thread-order 1.3.1__tar.gz → 1.3.2__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.
- {thread_order-1.3.1/thread_order.egg-info → thread_order-1.3.2}/PKG-INFO +1 -1
- {thread_order-1.3.1 → thread_order-1.3.2}/thread_order/__init__.py +1 -1
- {thread_order-1.3.1 → thread_order-1.3.2}/thread_order/ui/app.py +339 -109
- {thread_order-1.3.1 → thread_order-1.3.2/thread_order.egg-info}/PKG-INFO +1 -1
- {thread_order-1.3.1 → thread_order-1.3.2}/LICENSE +0 -0
- {thread_order-1.3.1 → thread_order-1.3.2}/README.md +0 -0
- {thread_order-1.3.1 → thread_order-1.3.2}/pyproject.toml +0 -0
- {thread_order-1.3.1 → thread_order-1.3.2}/setup.cfg +0 -0
- {thread_order-1.3.1 → thread_order-1.3.2}/tests/test_graph.py +0 -0
- {thread_order-1.3.1 → thread_order-1.3.2}/tests/test_init.py +0 -0
- {thread_order-1.3.1 → thread_order-1.3.2}/tests/test_scheduler.py +0 -0
- {thread_order-1.3.1 → thread_order-1.3.2}/thread_order/cli/__init__.py +0 -0
- {thread_order-1.3.1 → thread_order-1.3.2}/thread_order/cli/app.py +0 -0
- {thread_order-1.3.1 → thread_order-1.3.2}/thread_order/graph.py +0 -0
- {thread_order-1.3.1 → thread_order-1.3.2}/thread_order/graph_summary.py +0 -0
- {thread_order-1.3.1 → thread_order-1.3.2}/thread_order/logger.py +0 -0
- {thread_order-1.3.1 → thread_order-1.3.2}/thread_order/scheduler.py +0 -0
- {thread_order-1.3.1 → thread_order-1.3.2}/thread_order/timer.py +0 -0
- {thread_order-1.3.1 → thread_order-1.3.2}/thread_order/ui/__init__.py +0 -0
- {thread_order-1.3.1 → thread_order-1.3.2}/thread_order.egg-info/SOURCES.txt +0 -0
- {thread_order-1.3.1 → thread_order-1.3.2}/thread_order.egg-info/dependency_links.txt +0 -0
- {thread_order-1.3.1 → thread_order-1.3.2}/thread_order.egg-info/entry_points.txt +0 -0
- {thread_order-1.3.1 → thread_order-1.3.2}/thread_order.egg-info/requires.txt +0 -0
- {thread_order-1.3.1 → thread_order-1.3.2}/thread_order.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: thread-order
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.2
|
|
4
4
|
Summary: A lightweight framework for running functions concurrently across multiple threads while maintaining a defined execution order.
|
|
5
5
|
Author-email: Emilio Reyes <soda480@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import queue
|
|
2
|
+
import random
|
|
3
|
+
import time
|
|
2
4
|
import threading
|
|
3
5
|
import json
|
|
4
6
|
from pathlib import Path
|
|
@@ -16,10 +18,12 @@ from thread_order import (
|
|
|
16
18
|
register_functions
|
|
17
19
|
)
|
|
18
20
|
|
|
19
|
-
CARD_WIDTH =
|
|
21
|
+
CARD_WIDTH = 80
|
|
20
22
|
CARD_HEIGHT = 64
|
|
23
|
+
ICON_WIDTH = 46
|
|
21
24
|
|
|
22
25
|
class Runner(tb.Frame):
|
|
26
|
+
|
|
23
27
|
def __init__(self, master):
|
|
24
28
|
super().__init__(master)
|
|
25
29
|
self.master = master
|
|
@@ -29,35 +33,58 @@ class Runner(tb.Frame):
|
|
|
29
33
|
'FAILED': self._make_swatch('#e74c3c'), # red
|
|
30
34
|
'SKIPPED': self._make_swatch('#f1c40f'), # yellow
|
|
31
35
|
}
|
|
36
|
+
self._source_icons = {
|
|
37
|
+
'user': self._make_swatch('#000000'), # black
|
|
38
|
+
'state': self._make_swatch('#3498DB'), # blue
|
|
39
|
+
'module': self._make_swatch('#E67E22'), # orange
|
|
40
|
+
}
|
|
41
|
+
# thread icons
|
|
42
|
+
self._thread_icon_size = 12
|
|
43
|
+
self._thread_icon_grey = self.colors.light
|
|
44
|
+
self._thread_icon_palette = [
|
|
45
|
+
"#E74C3C", "#9B59B6", "#3498DB", "#1ABC9C", "#2ECC71",
|
|
46
|
+
"#F1C40F", "#E67E22", "#16A085", "#2980B9", "#8E44AD",
|
|
47
|
+
"#C0392B", "#27AE60", "#F39C12",
|
|
48
|
+
"#D35400", "#2C3E50", "#34495E", "#7F8C8D",
|
|
49
|
+
"#00B894", "#0984E3", "#6C5CE7", "#E84393",
|
|
50
|
+
"#00CEC9", "#A29BFE",
|
|
51
|
+
]
|
|
52
|
+
self._thread_icon_cache = {} # color -> PhotoImage (keep refs!)
|
|
53
|
+
self._thread_icon_color = {} # thread_name -> current color
|
|
54
|
+
|
|
32
55
|
self.setupui()
|
|
33
56
|
self._uiqueue = queue.Queue()
|
|
34
57
|
self._poll_uiqueue()
|
|
35
58
|
self._running = False
|
|
59
|
+
self._run_t0 = None
|
|
60
|
+
self._elapsed_job = None
|
|
36
61
|
|
|
37
62
|
def setupui(self):
|
|
38
63
|
self.pack()
|
|
39
64
|
|
|
40
65
|
# menubar
|
|
41
|
-
menubar = tk.Menu(self.master)
|
|
42
|
-
|
|
43
|
-
file_menu.
|
|
44
|
-
file_menu.add_command(label='Open
|
|
45
|
-
file_menu.
|
|
46
|
-
file_menu.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
66
|
+
self.menubar = tk.Menu(self.master)
|
|
67
|
+
|
|
68
|
+
self.file_menu = tk.Menu(self.menubar, tearoff=0)
|
|
69
|
+
self.file_menu.add_command(label='Open Tasks', command=self.open_tasks)
|
|
70
|
+
self.file_menu.add_command(label='Open State', command=self.open_state)
|
|
71
|
+
self.file_menu.add_separator()
|
|
72
|
+
self.file_menu.add_command(label='Exit', command=self.master.quit)
|
|
73
|
+
self.menubar.add_cascade(label='File', menu=self.file_menu)
|
|
74
|
+
|
|
75
|
+
self.options_menu = tk.Menu(self.menubar, tearoff=0)
|
|
50
76
|
self.log_all_var = tk.BooleanVar(value=False)
|
|
51
|
-
options_menu.add_checkbutton(label='Log All', variable=self.log_all_var)
|
|
77
|
+
self.options_menu.add_checkbutton(label='Log All', variable=self.log_all_var)
|
|
52
78
|
self.skip_dependents_var = tk.BooleanVar(value=False)
|
|
53
|
-
options_menu.add_checkbutton(
|
|
54
|
-
|
|
79
|
+
self.options_menu.add_checkbutton(
|
|
80
|
+
label='Skip Dependents', variable=self.skip_dependents_var)
|
|
81
|
+
self.menubar.add_cascade(label='Options', menu=self.options_menu)
|
|
55
82
|
|
|
56
|
-
help_menu = tk.Menu(menubar, tearoff=0)
|
|
57
|
-
help_menu.add_command(label='About', command=self.show_about)
|
|
58
|
-
menubar.add_cascade(label='Help', menu=help_menu)
|
|
83
|
+
self.help_menu = tk.Menu(self.menubar, tearoff=0)
|
|
84
|
+
self.help_menu.add_command(label='About', command=self.show_about)
|
|
85
|
+
self.menubar.add_cascade(label='Help', menu=self.help_menu)
|
|
59
86
|
|
|
60
|
-
self.master.config(menu=menubar)
|
|
87
|
+
self.master.config(menu=self.menubar)
|
|
61
88
|
|
|
62
89
|
top_frame = tb.Frame(self.master, padding=4)
|
|
63
90
|
top_frame.pack(fill=BOTH, expand=False)
|
|
@@ -100,170 +127,216 @@ class Runner(tb.Frame):
|
|
|
100
127
|
tab3 = tb.Frame(self.notebook)
|
|
101
128
|
tab4 = tb.Frame(self.notebook)
|
|
102
129
|
|
|
103
|
-
|
|
104
|
-
|
|
130
|
+
state_frame = tb.Frame(tab1)
|
|
131
|
+
state_frame.pack(fill=BOTH, expand=True, padx=4, pady=4)
|
|
132
|
+
|
|
133
|
+
frame_state_top = tb.Frame(state_frame)
|
|
134
|
+
frame_state_bot = tb.Frame(state_frame)
|
|
105
135
|
frame_state_top.pack(fill=X, expand=False)
|
|
106
136
|
frame_state_bot.pack(fill=BOTH, expand=True)
|
|
107
137
|
|
|
138
|
+
legend_row = tb.Frame(frame_state_top)
|
|
139
|
+
legend_row.pack(side=LEFT)
|
|
140
|
+
self._make_legend_card(legend_row, 'User\nEntry', bootstyle='dark')
|
|
141
|
+
self._make_legend_card(legend_row, 'State\nFile', bootstyle='primary')
|
|
142
|
+
self._make_legend_card(legend_row, 'Module\nFile', bootstyle='warning')
|
|
143
|
+
|
|
144
|
+
entry_frame = tb.Frame(frame_state_top)
|
|
145
|
+
entry_frame.pack(side=LEFT, fill=X, expand=True)
|
|
146
|
+
|
|
108
147
|
self.key_value = tb.StringVar(value='')
|
|
109
|
-
entry_key_value = tb.Entry(
|
|
110
|
-
|
|
148
|
+
self.entry_key_value = tb.Entry(
|
|
149
|
+
entry_frame,
|
|
111
150
|
width=50,
|
|
112
151
|
justify='left',
|
|
113
152
|
textvariable=self.key_value)
|
|
114
|
-
entry_key_value.pack(side=LEFT, padx=4, pady=4, fill=X, expand=True)
|
|
115
|
-
button_key_value = tb.Button(
|
|
116
|
-
|
|
153
|
+
self.entry_key_value.pack(side=LEFT, padx=4, pady=4, fill=X, expand=True)
|
|
154
|
+
self.button_key_value = tb.Button(
|
|
155
|
+
entry_frame,
|
|
117
156
|
text='Add Key Value',
|
|
118
157
|
command=self.add_key_value,
|
|
119
158
|
state='enabled',
|
|
120
159
|
width=12,
|
|
121
160
|
bootstyle='primary')
|
|
122
|
-
entry_key_value.bind(
|
|
161
|
+
self.entry_key_value.bind(
|
|
123
162
|
'<Return>',
|
|
124
|
-
lambda e: button_key_value.invoke())
|
|
125
|
-
button_key_value.pack(side=LEFT, padx=4, pady=4)
|
|
163
|
+
lambda e: self.button_key_value.invoke())
|
|
164
|
+
self.button_key_value.pack(side=LEFT, padx=4, pady=4)
|
|
165
|
+
|
|
166
|
+
state_table_frame = tb.Frame(frame_state_bot)
|
|
167
|
+
state_table_frame.pack(fill=BOTH, expand=True)
|
|
126
168
|
self.table_state = Tableview(
|
|
127
|
-
master=
|
|
128
|
-
coldata=['Key', 'Value'
|
|
169
|
+
master=state_table_frame,
|
|
170
|
+
coldata=['Key', 'Value'],
|
|
129
171
|
rowdata=[],
|
|
130
172
|
paginated=False,
|
|
131
173
|
autofit=False,
|
|
132
174
|
searchable=False,
|
|
133
175
|
bootstyle='primary',
|
|
134
|
-
yscrollbar=
|
|
176
|
+
yscrollbar=False,
|
|
135
177
|
stripecolor=(self.colors.light, None),
|
|
136
178
|
)
|
|
137
|
-
self.table_state.
|
|
138
|
-
self.table_state.view
|
|
179
|
+
self.table_state.load_table_data()
|
|
180
|
+
table_state_view = self.table_state.view
|
|
181
|
+
self.state_vscroll = tb.Scrollbar(
|
|
182
|
+
state_table_frame, orient='vertical', command=table_state_view.yview)
|
|
183
|
+
table_state_view.configure(
|
|
184
|
+
yscrollcommand=self._autohide_scrollbar(self.state_vscroll),
|
|
185
|
+
show='tree headings')
|
|
186
|
+
table_state_cols = self.table_state.get_columns()
|
|
187
|
+
table_state_view.heading('#0', text='')
|
|
188
|
+
table_state_view.column(
|
|
189
|
+
'#0', width=ICON_WIDTH, minwidth=ICON_WIDTH, stretch=False, anchor='center')
|
|
190
|
+
table_state_key_col = table_state_cols[0].cid
|
|
191
|
+
table_state_view.heading(table_state_key_col, anchor='w')
|
|
192
|
+
table_state_view.column(table_state_key_col, stretch=False)
|
|
193
|
+
table_state_value_col = table_state_cols[1].cid
|
|
194
|
+
table_state_view.heading(table_state_value_col, anchor='w')
|
|
195
|
+
table_state_view.column(table_state_value_col, stretch=True)
|
|
196
|
+
self.state_vscroll.pack(side=RIGHT, fill=Y, padx=(4, 0))
|
|
197
|
+
self.table_state.pack(side=LEFT, fill=BOTH, expand=True)
|
|
139
198
|
self.table_state.autofit_columns()
|
|
140
199
|
|
|
141
200
|
# TASKS TAB
|
|
142
201
|
tasks_frame = tb.Frame(tab2)
|
|
143
202
|
tasks_frame.pack(fill=BOTH, expand=True, padx=4, pady=4)
|
|
144
|
-
|
|
145
203
|
total_tasks_frame = tb.Frame(tasks_frame)
|
|
146
204
|
total_tasks_frame.pack(fill=X, expand=False)
|
|
147
205
|
self.tasks_total_var = tb.IntVar(value=0)
|
|
148
206
|
self._make_counter(total_tasks_frame, 'Total', self.tasks_total_var, bootstyle='dark')
|
|
149
|
-
|
|
207
|
+
tasks_table_frame = tb.Frame(tasks_frame)
|
|
208
|
+
tasks_table_frame.pack(fill=BOTH, expand=True)
|
|
150
209
|
self.table_tasks = Tableview(
|
|
151
|
-
master=
|
|
210
|
+
master=tasks_table_frame,
|
|
152
211
|
coldata=['Tasks', 'Dependencies'],
|
|
153
212
|
rowdata=[],
|
|
154
213
|
paginated=False,
|
|
155
214
|
autofit=False,
|
|
156
215
|
searchable=False,
|
|
157
216
|
bootstyle='primary',
|
|
158
|
-
yscrollbar=
|
|
217
|
+
yscrollbar=False,
|
|
159
218
|
stripecolor=(self.colors.light, None),
|
|
160
219
|
disable_right_click=True,
|
|
161
220
|
)
|
|
162
221
|
self.table_tasks.load_table_data()
|
|
163
|
-
self.table_tasks.pack(fill=BOTH, expand=True, padx=4, pady=4)
|
|
164
222
|
table_tasks_view = self.table_tasks.view
|
|
223
|
+
self.tasks_vscroll = tb.Scrollbar(
|
|
224
|
+
tasks_table_frame, orient='vertical', command=table_tasks_view.yview)
|
|
225
|
+
table_tasks_view.configure(
|
|
226
|
+
yscrollcommand=self._autohide_scrollbar(self.tasks_vscroll),
|
|
227
|
+
show='tree headings')
|
|
228
|
+
table_tasks_view.heading('#0', text='#')
|
|
229
|
+
table_tasks_view.column('#0', stretch=False, anchor='w', width=40, minwidth=40)
|
|
165
230
|
table_tasks_cols = self.table_tasks.get_columns()
|
|
166
231
|
table_tasks_task_col = table_tasks_cols[0].cid
|
|
167
232
|
table_tasks_view.heading(table_tasks_task_col, anchor='w')
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
table_tasks_view.
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
width=40,
|
|
175
|
-
minwidth=40,
|
|
176
|
-
stretch=False,
|
|
177
|
-
anchor='w'
|
|
178
|
-
)
|
|
233
|
+
table_tasks_deps_col = table_tasks_cols[1].cid
|
|
234
|
+
table_tasks_view.heading(table_tasks_deps_col, anchor='w')
|
|
235
|
+
table_tasks_view.column(table_tasks_deps_col, stretch=True, anchor='w')
|
|
236
|
+
self.tasks_vscroll.pack(side=RIGHT, fill=Y, padx=(4, 0))
|
|
237
|
+
self.table_tasks.pack(side=LEFT, fill=BOTH, expand=True)
|
|
238
|
+
self.table_tasks.autofit_columns()
|
|
179
239
|
|
|
180
240
|
# THREADS TAB
|
|
181
241
|
threads_frame = tb.Frame(tab3)
|
|
182
242
|
threads_frame.pack(fill=BOTH, expand=True, padx=4, pady=4)
|
|
183
|
-
|
|
184
243
|
thread_viewer_frame = tb.Frame(threads_frame)
|
|
185
244
|
thread_viewer_frame.pack(fill=X, expand=False)
|
|
186
|
-
|
|
187
245
|
self.queued_var = tb.IntVar(value=0)
|
|
188
246
|
self.active_var = tb.IntVar(value=0)
|
|
189
247
|
self.closed_var = tb.IntVar(value=0)
|
|
190
|
-
|
|
191
248
|
self._make_counter(thread_viewer_frame, 'Queued', self.queued_var, bootstyle='dark')
|
|
192
249
|
self._make_counter(thread_viewer_frame, 'Active', self.active_var, bootstyle='primary')
|
|
193
250
|
self._make_counter(thread_viewer_frame, 'Closed', self.closed_var, bootstyle='secondary')
|
|
194
|
-
|
|
251
|
+
threads_table_frame = tb.Frame(threads_frame)
|
|
252
|
+
threads_table_frame.pack(fill=BOTH, expand=True)
|
|
195
253
|
self.table_threads = Tableview(
|
|
196
|
-
master=
|
|
254
|
+
master=threads_table_frame,
|
|
197
255
|
coldata=['Thread', 'Task'],
|
|
198
256
|
rowdata=[(f'thread_{i}', '') for i in range(int(self.workers.get()))],
|
|
199
257
|
paginated=False,
|
|
200
258
|
autofit=False,
|
|
201
259
|
searchable=False,
|
|
202
260
|
bootstyle='primary',
|
|
203
|
-
yscrollbar=
|
|
261
|
+
yscrollbar=False,
|
|
204
262
|
stripecolor=(self.colors.light, None),
|
|
205
263
|
disable_right_click=True,
|
|
206
264
|
)
|
|
207
265
|
self.table_threads.load_table_data()
|
|
208
|
-
self.table_threads.pack(fill=BOTH, expand=True, padx=4, pady=4)
|
|
209
266
|
table_threads_view = self.table_threads.view
|
|
267
|
+
self.threads_vscroll = tb.Scrollbar(
|
|
268
|
+
threads_table_frame, orient='vertical', command=table_threads_view.yview)
|
|
269
|
+
table_threads_view.configure(
|
|
270
|
+
yscrollcommand=self._autohide_scrollbar(self.threads_vscroll),
|
|
271
|
+
show='tree headings',
|
|
272
|
+
selectmode='none')
|
|
210
273
|
table_threads_cols = self.table_threads.get_columns()
|
|
274
|
+
table_threads_view.heading('#0', text='')
|
|
275
|
+
table_threads_view.column(
|
|
276
|
+
'#0', width=ICON_WIDTH, minwidth=ICON_WIDTH, stretch=False, anchor='center')
|
|
211
277
|
table_threads_task_col = table_threads_cols[1].cid
|
|
212
|
-
table_threads_view.heading(table_threads_task_col, anchor=
|
|
278
|
+
table_threads_view.heading(table_threads_task_col, anchor='w')
|
|
213
279
|
table_threads_view.column(
|
|
214
|
-
table_threads_task_col, stretch=True, anchor=
|
|
280
|
+
table_threads_task_col, stretch=True, anchor='w')
|
|
281
|
+
table_threads_view.selection_remove(table_threads_view.selection())
|
|
282
|
+
table_threads_view.focus('')
|
|
283
|
+
self.threads_vscroll.pack(side=RIGHT, fill=Y, padx=(4, 0))
|
|
284
|
+
self.table_threads.pack(side=LEFT, fill=BOTH, expand=True)
|
|
285
|
+
self.table_threads.autofit_columns()
|
|
286
|
+
self._init_thread_icons()
|
|
215
287
|
|
|
216
288
|
# RUN TAB
|
|
217
289
|
run_frame = tb.Frame(tab4)
|
|
218
290
|
run_frame.pack(fill=BOTH, expand=True, padx=4, pady=4)
|
|
219
|
-
|
|
220
291
|
summary_frame = tb.Frame(run_frame)
|
|
221
292
|
summary_frame.pack(fill=X, expand=False)
|
|
222
|
-
|
|
223
293
|
self.total_var = tb.IntVar(value=0)
|
|
224
294
|
self.passed_var = tb.IntVar(value=0)
|
|
225
295
|
self.failed_var = tb.IntVar(value=0)
|
|
226
296
|
self.skipped_var = tb.IntVar(value=0)
|
|
227
|
-
|
|
228
297
|
self._make_counter(summary_frame, 'Total', self.total_var, bootstyle='dark')
|
|
229
298
|
self._make_counter(summary_frame, 'Passed', self.passed_var, bootstyle='success')
|
|
230
299
|
self._make_counter(summary_frame, 'Failed', self.failed_var, bootstyle='danger')
|
|
231
300
|
self._make_counter(summary_frame, 'Skipped', self.skipped_var, bootstyle='warning')
|
|
232
|
-
|
|
301
|
+
run_table_frame = tb.Frame(run_frame)
|
|
302
|
+
run_table_frame.pack(fill=BOTH, expand=True)
|
|
233
303
|
self.table_run = Tableview(
|
|
234
|
-
master=
|
|
304
|
+
master=run_table_frame,
|
|
235
305
|
coldata=['#', 'Task'],
|
|
236
306
|
rowdata=[],
|
|
237
307
|
paginated=False,
|
|
238
308
|
autofit=False,
|
|
239
309
|
searchable=False,
|
|
240
310
|
bootstyle='primary',
|
|
241
|
-
yscrollbar=
|
|
311
|
+
yscrollbar=False,
|
|
242
312
|
stripecolor=(self.colors.light, None),
|
|
243
313
|
disable_right_click=True,
|
|
244
314
|
)
|
|
245
315
|
self.table_run.load_table_data()
|
|
246
|
-
self.table_run.pack(fill=BOTH, expand=True, padx=4, pady=4)
|
|
247
|
-
|
|
248
316
|
table_run_view = self.table_run.view
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
317
|
+
self.run_vscroll = tb.Scrollbar(
|
|
318
|
+
run_table_frame, orient='vertical', command=table_run_view.yview)
|
|
319
|
+
table_run_view.configure(
|
|
320
|
+
yscrollcommand=self._autohide_scrollbar(self.run_vscroll),
|
|
321
|
+
show='tree headings')
|
|
322
|
+
table_run_cols = self.table_run.get_columns()
|
|
254
323
|
table_run_view.heading('#0', text='')
|
|
255
|
-
table_run_view.column(
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
table_run_view.heading(
|
|
259
|
-
table_run_view.column(
|
|
260
|
-
|
|
261
|
-
|
|
324
|
+
table_run_view.column(
|
|
325
|
+
'#0', width=ICON_WIDTH, minwidth=ICON_WIDTH, stretch=False, anchor='center')
|
|
326
|
+
table_run_num_col = table_run_cols[0].cid
|
|
327
|
+
table_run_view.heading(table_run_num_col, anchor='w')
|
|
328
|
+
table_run_view.column(table_run_num_col, stretch=False, anchor='w', width=40, minwidth=40)
|
|
329
|
+
table_run_task_col = table_run_cols[1].cid
|
|
330
|
+
table_run_view.heading(table_run_task_col, anchor='w')
|
|
331
|
+
table_run_view.column(table_run_task_col, stretch=True, anchor='w')
|
|
332
|
+
self.run_vscroll.pack(side=RIGHT, fill=Y, padx=(4, 0))
|
|
333
|
+
self.table_run.pack(side=LEFT, fill=BOTH, expand=True)
|
|
262
334
|
|
|
263
335
|
self.notebook.add(tab1, text='State')
|
|
264
336
|
self.notebook.add(tab2, text='Tasks')
|
|
265
337
|
self.notebook.add(tab3, text='Threads')
|
|
266
338
|
self.notebook.add(tab4, text='Run')
|
|
339
|
+
self.after_idle(self.hide_all_hscrollbars)
|
|
267
340
|
|
|
268
341
|
# STATUS BAR
|
|
269
342
|
self.footer = tb.Frame(self.master, padding=(8, 4))
|
|
@@ -275,7 +348,7 @@ class Runner(tb.Frame):
|
|
|
275
348
|
font=('Segoe UI', 8),
|
|
276
349
|
anchor='w'
|
|
277
350
|
)
|
|
278
|
-
self.duration_label.pack(side=LEFT, fill=
|
|
351
|
+
self.duration_label.pack(side=LEFT, fill=None, expand=False, padx=(0, 2))
|
|
279
352
|
self.running_frame = tb.Frame(self.footer)
|
|
280
353
|
self.progress_var = tb.IntVar(value=0)
|
|
281
354
|
self.progress = tb.Progressbar(
|
|
@@ -283,7 +356,7 @@ class Runner(tb.Frame):
|
|
|
283
356
|
mode='determinate',
|
|
284
357
|
variable=self.progress_var,
|
|
285
358
|
)
|
|
286
|
-
self.progress.pack(side=LEFT, fill=X, expand=True)
|
|
359
|
+
self.progress.pack(side=LEFT, fill=X, expand=True, padx=(2, 0))
|
|
287
360
|
self.percent_var = tb.StringVar(value="")
|
|
288
361
|
self.percent_label = tb.Label(
|
|
289
362
|
self.running_frame,
|
|
@@ -302,7 +375,7 @@ class Runner(tb.Frame):
|
|
|
302
375
|
key = key_value_split[0].strip()
|
|
303
376
|
value = key_value_split[1].strip()
|
|
304
377
|
if value:
|
|
305
|
-
self.
|
|
378
|
+
self._upsert_state_row(key, value, 'user')
|
|
306
379
|
self.table_state.autofit_columns()
|
|
307
380
|
self.key_value.set('')
|
|
308
381
|
|
|
@@ -312,6 +385,7 @@ class Runner(tb.Frame):
|
|
|
312
385
|
self.table_threads.delete_rows()
|
|
313
386
|
self.table_threads.insert_rows(0, rowdata=[(f'thread_{i}', '') for i in range(workers)])
|
|
314
387
|
self.table_threads.autofit_columns()
|
|
388
|
+
self._init_thread_icons()
|
|
315
389
|
|
|
316
390
|
def open_tasks(self):
|
|
317
391
|
try:
|
|
@@ -372,7 +446,6 @@ class Runner(tb.Frame):
|
|
|
372
446
|
)
|
|
373
447
|
if not path:
|
|
374
448
|
return
|
|
375
|
-
|
|
376
449
|
self.notebook.select(0)
|
|
377
450
|
self._load_state_from_json(path)
|
|
378
451
|
self.duration_var.set(f'Loaded state from {Path(path).name}')
|
|
@@ -391,20 +464,39 @@ class Runner(tb.Frame):
|
|
|
391
464
|
|
|
392
465
|
def _on_scheduler_start_ui(self, meta):
|
|
393
466
|
self._set_running_ui(True)
|
|
394
|
-
|
|
467
|
+
|
|
468
|
+
# determinate progress from the start
|
|
469
|
+
self.progress.configure(mode='determinate')
|
|
395
470
|
self.progress_var.set(0)
|
|
396
471
|
self.percent_var.set('0%')
|
|
397
472
|
|
|
473
|
+
# elapsed timer
|
|
474
|
+
self.duration_var.set('00:00:00')
|
|
475
|
+
self._start_elapsed_ticker()
|
|
476
|
+
|
|
398
477
|
def on_task_run_ui(self, task_name, thread_name):
|
|
399
478
|
thread_number = _get_thread_number(thread_name)
|
|
400
|
-
if thread_number is
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
479
|
+
if thread_number is None:
|
|
480
|
+
return
|
|
481
|
+
|
|
482
|
+
self.active_var.set(self.active_var.get() + 1)
|
|
483
|
+
self.queued_var.set(max(0, self.queued_var.get() - 1))
|
|
484
|
+
|
|
485
|
+
children = self.table_threads.view.get_children('')
|
|
486
|
+
if thread_number >= len(children):
|
|
487
|
+
return
|
|
488
|
+
|
|
489
|
+
tv = self.table_threads.view
|
|
490
|
+
iid = children[thread_number]
|
|
491
|
+
|
|
492
|
+
# update values
|
|
493
|
+
tv.item(iid, values=(thread_name, task_name))
|
|
494
|
+
|
|
495
|
+
# update icon color (must change if reassigned)
|
|
496
|
+
prev = self._thread_icon_color.get(thread_name, self._thread_icon_grey)
|
|
497
|
+
new_color = self._pick_new_thread_color(prev)
|
|
498
|
+
tv.item(iid, image=self._get_thread_icon(new_color))
|
|
499
|
+
self._thread_icon_color[thread_name] = new_color
|
|
408
500
|
|
|
409
501
|
def on_task_done_ui(self, task_name, thread_name, status, count, total):
|
|
410
502
|
key = str(status.value).upper()
|
|
@@ -413,10 +505,10 @@ class Runner(tb.Frame):
|
|
|
413
505
|
iid = self.table_run.view.get_children('')[0]
|
|
414
506
|
self.table_run.view.item(iid, image=icon)
|
|
415
507
|
# highlight row that was just inserted
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
508
|
+
tabke_run_view = self.table_run.view
|
|
509
|
+
tabke_run_view.selection_set(iid)
|
|
510
|
+
tabke_run_view.focus(iid)
|
|
511
|
+
tabke_run_view.see(iid)
|
|
420
512
|
|
|
421
513
|
if key == 'PASSED':
|
|
422
514
|
self.passed_var.set(self.passed_var.get() + 1)
|
|
@@ -429,8 +521,14 @@ class Runner(tb.Frame):
|
|
|
429
521
|
if thread_number is not None:
|
|
430
522
|
self.active_var.set(self.active_var.get() - 1)
|
|
431
523
|
self.closed_var.set(self.closed_var.get() + 1)
|
|
432
|
-
|
|
433
|
-
self.table_threads.view
|
|
524
|
+
|
|
525
|
+
table_threads_view = self.table_threads.view
|
|
526
|
+
iid = table_threads_view.get_children('')[thread_number]
|
|
527
|
+
table_threads_view.item(iid, values=(thread_name, ''))
|
|
528
|
+
|
|
529
|
+
# back to grey when idle
|
|
530
|
+
table_threads_view.item(iid, image=self._get_thread_icon(self._thread_icon_grey))
|
|
531
|
+
self._thread_icon_color[thread_name] = self._thread_icon_grey
|
|
434
532
|
else:
|
|
435
533
|
self.closed_var.set(self.closed_var.get() + 1)
|
|
436
534
|
self.queued_var.set(max(0, self.queued_var.get() - 1))
|
|
@@ -441,6 +539,7 @@ class Runner(tb.Frame):
|
|
|
441
539
|
self.percent_var.set(f'{pct}%')
|
|
442
540
|
|
|
443
541
|
def on_scheduler_done_ui(self, summary):
|
|
542
|
+
self._stop_elapsed_ticker()
|
|
444
543
|
self._set_running_ui(False)
|
|
445
544
|
duration = summary['duration']
|
|
446
545
|
self.duration_var.set(f'Completed in {duration:.2f}s')
|
|
@@ -448,11 +547,20 @@ class Runner(tb.Frame):
|
|
|
448
547
|
def _set_running_ui(self, running):
|
|
449
548
|
self.spinbox_workers.configure(state='disabled' if running else 'enabled')
|
|
450
549
|
self.run_button.configure(state='disabled' if running else 'enabled')
|
|
550
|
+
self.button_key_value.configure(state='disabled' if running else 'enabled')
|
|
551
|
+
self.entry_key_value.configure(state='disabled' if running else 'normal')
|
|
552
|
+
self._set_menu_state('disabled' if running else 'normal')
|
|
451
553
|
if running:
|
|
452
554
|
self._show_running_footer()
|
|
453
555
|
else:
|
|
454
556
|
self._hide_running_footer()
|
|
455
557
|
|
|
558
|
+
def _set_menu_state(self, state):
|
|
559
|
+
# state is 'normal' or 'disabled'
|
|
560
|
+
# disable top-level cascades so nothing inside can be clicked
|
|
561
|
+
self.menubar.entryconfig('File', state=state)
|
|
562
|
+
self.menubar.entryconfig('Options', state=state)
|
|
563
|
+
|
|
456
564
|
def run_tasks(self):
|
|
457
565
|
if getattr(self, '_running', False) or not hasattr(self, '_marked_functions'):
|
|
458
566
|
return
|
|
@@ -483,9 +591,10 @@ class Runner(tb.Frame):
|
|
|
483
591
|
self.scheduler.on_scheduler_done(self.on_scheduler_done)
|
|
484
592
|
self.scheduler.start()
|
|
485
593
|
except (Exception, SystemExit) as e:
|
|
594
|
+
self._uiqueue_put(self._set_running_ui, False)
|
|
595
|
+
self._uiqueue_put(self._stop_elapsed_ticker)
|
|
486
596
|
# surface the error on the UI thread
|
|
487
597
|
self._uiqueue_put(messagebox.showerror, 'Run failed', str(e))
|
|
488
|
-
self._uiqueue_put(self._set_running_ui, False)
|
|
489
598
|
finally:
|
|
490
599
|
self._running = False
|
|
491
600
|
print('state' + json.dumps(self.scheduler.sanitized_state, indent=2, default=str))
|
|
@@ -543,6 +652,30 @@ class Runner(tb.Frame):
|
|
|
543
652
|
anchor='w'
|
|
544
653
|
).pack(anchor='w')
|
|
545
654
|
|
|
655
|
+
def _make_legend_card(self, parent, title, bootstyle='secondary'):
|
|
656
|
+
card = tb.Frame(
|
|
657
|
+
parent,
|
|
658
|
+
width=CARD_WIDTH,
|
|
659
|
+
height=CARD_HEIGHT,
|
|
660
|
+
bootstyle=bootstyle
|
|
661
|
+
)
|
|
662
|
+
card.pack_propagate(False)
|
|
663
|
+
card.pack(side=LEFT, padx=4, pady=4)
|
|
664
|
+
|
|
665
|
+
tb.Label(
|
|
666
|
+
card,
|
|
667
|
+
text=title,
|
|
668
|
+
font=('Segoe UI', 10, 'bold'),
|
|
669
|
+
bootstyle='inverse-' + bootstyle,
|
|
670
|
+
anchor='w'
|
|
671
|
+
).pack(anchor='w')
|
|
672
|
+
|
|
673
|
+
tb.Label(
|
|
674
|
+
card,
|
|
675
|
+
bootstyle='inverse-' + bootstyle,
|
|
676
|
+
anchor='w'
|
|
677
|
+
).pack(anchor='w')
|
|
678
|
+
|
|
546
679
|
def _renumber_table(self, table: Tableview, start=1):
|
|
547
680
|
for i, iid in enumerate(table.view.get_children(""), start=start):
|
|
548
681
|
table.view.item(iid, text=str(i))
|
|
@@ -576,7 +709,7 @@ class Runner(tb.Frame):
|
|
|
576
709
|
value_str = json.dumps(value)
|
|
577
710
|
else:
|
|
578
711
|
value_str = str(value)
|
|
579
|
-
self._upsert_state_row(key, value_str,
|
|
712
|
+
self._upsert_state_row(key, value_str, 'state')
|
|
580
713
|
|
|
581
714
|
self.table_state.autofit_columns()
|
|
582
715
|
|
|
@@ -596,13 +729,18 @@ class Runner(tb.Frame):
|
|
|
596
729
|
return state
|
|
597
730
|
|
|
598
731
|
def _upsert_state_row(self, key, value_str, source):
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
732
|
+
table_state_view = self.table_state.view
|
|
733
|
+
icon = self._source_icons[source]
|
|
734
|
+
for iid in table_state_view.get_children(''):
|
|
735
|
+
k = table_state_view.item(iid, 'values')[0]
|
|
602
736
|
if k == key:
|
|
603
|
-
|
|
737
|
+
table_state_view.item(iid, values=(key, value_str), image=icon)
|
|
738
|
+
table_state_view.move(iid, '', 0) # move to top
|
|
739
|
+
table_state_view.see(iid)
|
|
604
740
|
return
|
|
605
|
-
self.table_state.insert_row(index=
|
|
741
|
+
row = self.table_state.insert_row(index=0, values=(key, value_str))
|
|
742
|
+
table_state_view.item(row.iid, image=icon)
|
|
743
|
+
table_state_view.see(row.iid)
|
|
606
744
|
|
|
607
745
|
def show_about(self):
|
|
608
746
|
version = importlib.metadata.version("thread-order")
|
|
@@ -641,12 +779,94 @@ class Runner(tb.Frame):
|
|
|
641
779
|
# tables
|
|
642
780
|
self.table_run.delete_rows()
|
|
643
781
|
|
|
644
|
-
# clear thread assignments
|
|
782
|
+
# clear thread assignments + reset icons
|
|
645
783
|
tv = self.table_threads.view
|
|
646
784
|
for iid in tv.get_children(''):
|
|
647
785
|
thread_name, *_ = tv.item(iid, 'values')
|
|
648
786
|
tv.item(iid, values=(thread_name, ''))
|
|
649
787
|
|
|
788
|
+
self._init_thread_icons()
|
|
789
|
+
|
|
790
|
+
def _get_thread_icon(self, color):
|
|
791
|
+
img = self._thread_icon_cache.get(color)
|
|
792
|
+
if img:
|
|
793
|
+
return img
|
|
794
|
+
size = self._thread_icon_size
|
|
795
|
+
img = tk.PhotoImage(width=size * 2, height=size)
|
|
796
|
+
img.put(color, to=(0, 0, size * 2, size))
|
|
797
|
+
self._thread_icon_cache[color] = img
|
|
798
|
+
return img
|
|
799
|
+
|
|
800
|
+
def _pick_new_thread_color(self, prev):
|
|
801
|
+
choices = [c for c in self._thread_icon_palette if c != prev]
|
|
802
|
+
if not choices:
|
|
803
|
+
return self._thread_icon_palette[0]
|
|
804
|
+
return random.choice(choices)
|
|
805
|
+
|
|
806
|
+
def _init_thread_icons(self):
|
|
807
|
+
tv = self.table_threads.view
|
|
808
|
+
grey = self._get_thread_icon(self._thread_icon_grey)
|
|
809
|
+
self._thread_icon_color.clear()
|
|
810
|
+
|
|
811
|
+
for iid in tv.get_children(''):
|
|
812
|
+
thread_name = tv.item(iid, 'values')[0]
|
|
813
|
+
tv.item(iid, image=grey)
|
|
814
|
+
self._thread_icon_color[thread_name] = self._thread_icon_grey
|
|
815
|
+
|
|
816
|
+
def _start_elapsed_ticker(self):
|
|
817
|
+
# cancel existing ticker if any
|
|
818
|
+
if self._elapsed_job is not None:
|
|
819
|
+
try:
|
|
820
|
+
self.after_cancel(self._elapsed_job)
|
|
821
|
+
except Exception:
|
|
822
|
+
pass
|
|
823
|
+
self._elapsed_job = None
|
|
824
|
+
|
|
825
|
+
self._run_t0 = time.monotonic()
|
|
826
|
+
self._tick_elapsed()
|
|
827
|
+
|
|
828
|
+
def _stop_elapsed_ticker(self):
|
|
829
|
+
if self._elapsed_job is not None:
|
|
830
|
+
try:
|
|
831
|
+
self.after_cancel(self._elapsed_job)
|
|
832
|
+
except Exception:
|
|
833
|
+
pass
|
|
834
|
+
self._elapsed_job = None
|
|
835
|
+
|
|
836
|
+
def _tick_elapsed(self):
|
|
837
|
+
if not self._running or self._run_t0 is None:
|
|
838
|
+
return
|
|
839
|
+
|
|
840
|
+
elapsed = int(time.monotonic() - self._run_t0)
|
|
841
|
+
hours = elapsed // 3600
|
|
842
|
+
mins = (elapsed % 3600) // 60
|
|
843
|
+
secs = elapsed % 60
|
|
844
|
+
self.duration_var.set(f"{hours:02d}:{mins:02d}:{secs:02d}")
|
|
845
|
+
# was 500ms; 1s is enough since you display seconds
|
|
846
|
+
self._elapsed_job = self.after(1000, self._tick_elapsed)
|
|
847
|
+
|
|
848
|
+
def _autohide_scrollbar(self, scrollbar):
|
|
849
|
+
def _wrapped(first, last):
|
|
850
|
+
first, last = float(first), float(last)
|
|
851
|
+
|
|
852
|
+
if first <= 0.0 and last >= 1.0:
|
|
853
|
+
# content fully visible → hide scrollbar
|
|
854
|
+
if scrollbar.winfo_ismapped():
|
|
855
|
+
scrollbar.pack_forget()
|
|
856
|
+
else:
|
|
857
|
+
# overflow → show scrollbar
|
|
858
|
+
if not scrollbar.winfo_ismapped():
|
|
859
|
+
scrollbar.pack(side=RIGHT, fill=Y, padx=(4, 0))
|
|
860
|
+
|
|
861
|
+
scrollbar.set(first, last)
|
|
862
|
+
|
|
863
|
+
return _wrapped
|
|
864
|
+
|
|
865
|
+
def _legend_item(parent, icon, text):
|
|
866
|
+
w = tb.Label(parent, image=icon, text=f" {text}", compound="left")
|
|
867
|
+
w.pack(side=LEFT, padx=(8, 0))
|
|
868
|
+
return w
|
|
869
|
+
|
|
650
870
|
def _maybe_call_setup_state(module, initial_state):
|
|
651
871
|
""" invoke module-level setup_state(initial_state) if defined
|
|
652
872
|
"""
|
|
@@ -655,11 +875,21 @@ def _maybe_call_setup_state(module, initial_state):
|
|
|
655
875
|
setup_state_function(initial_state)
|
|
656
876
|
|
|
657
877
|
def hide_tableview_hscroll(table):
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
878
|
+
# Hide only scrollbars wired to xview (horizontal).
|
|
879
|
+
def walk(w):
|
|
880
|
+
for child in w.winfo_children():
|
|
881
|
+
if child.winfo_class() == "TScrollbar":
|
|
882
|
+
try:
|
|
883
|
+
cmd = str(child.cget("command"))
|
|
884
|
+
except Exception:
|
|
885
|
+
cmd = ""
|
|
886
|
+
if "xview" in cmd:
|
|
887
|
+
child.pack_forget()
|
|
888
|
+
child.grid_forget()
|
|
889
|
+
child.place_forget()
|
|
890
|
+
else:
|
|
891
|
+
walk(child)
|
|
892
|
+
walk(table)
|
|
663
893
|
|
|
664
894
|
def _get_thread_number(thread_name, thread_prefix='thread_'):
|
|
665
895
|
""" Extract the thread index from a thread name.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: thread-order
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.2
|
|
4
4
|
Summary: A lightweight framework for running functions concurrently across multiple threads while maintaining a defined execution order.
|
|
5
5
|
Author-email: Emilio Reyes <soda480@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|