Belle II Software development
viewer.py
1#!/usr/bin/env python3
2
3
10
11import numbers
12import json
13import time
14from io import StringIO
15from html import escape
16
17from dateutil.relativedelta import relativedelta
18
19
21 """
22 A base class for widgets in the hep ipython projects.
23 """
24
25 def create(self):
26 """
27 Override this method!
28 """
29 return None
30
31 def show(self):
32 """
33 Show the widget
34 """
35 from IPython.core.display import display
36
37 a = self.create()
38 display(a)
39
40
42 """The css string for styling the notebook."""
43
44
45 css_string = """
46 #notebook-container {
47 width: 90%;
48 }
49
50 #menubar-container {
51 width: 90%;
52 }
53
54 #header-container {
55 width: 90%;
56 }
57 """
58
59 def create(self):
60 """Create the styling widget."""
61 from IPython.core.display import HTML
62 html = HTML(f"<style>\n{self.css_string}\n</style>")
63 return html
64
65
67 """
68 Viewer Object used to print data to the IPython Notebook.
69 Do not use it on your own.
70 """
71
72 def __init__(self):
73 """
74 Create a new progress bar viewer.
75 """
76 from IPython.core.display import display
77 from ipywidgets import FloatProgress, VBox, Layout, Label
78
79
80 self.last_time = time.time()
81
83
84
85 self.progress_bar = FloatProgress(value=0, min=0, max=1,
86 layout=Layout(width="100%", height="40px"))
87
88 self.progress_label = Label()
89
90 self.progress_box = VBox([self.progress_bar, self.progress_label])
91
93
94 def update(self, text_or_percentage):
95 """
96 Update the widget with a new event number
97 """
98
99 if isinstance(text_or_percentage, numbers.Number):
100 # text_or_percentage is percentage fraction
101 current_percentage = float(text_or_percentage)
102 current_time = time.time()
103
104 remaining_percentage = 1.0 - current_percentage
105
106 time_delta = current_time - self.last_time
107 percentage_delta = current_percentage - self.last_percentage
108
109 if percentage_delta > 0:
110 time_delta_per_percentage = 1.0 * time_delta / percentage_delta
111
112 # creates a human-readable time delta like '3 minutes 34 seconds'
113 attrs = ['years', 'months', 'days', 'hours', 'minutes', 'seconds']
114
115 def human_readable(delta): return [f'{int(getattr(delta, attr))} {getattr(delta, attr) > 1 and attr or attr[:-1]}'
116 for attr in attrs if getattr(delta, attr)]
117
118 times_list = human_readable(relativedelta(seconds=time_delta_per_percentage * remaining_percentage))
119 human_readable_str = " ".join(times_list)
120
121 display_text = f"{int(100 * current_percentage)} % Remaining time: {human_readable_str}"
122
123 self.progress_label.value = display_text
124 self.progress_bar.value = float(current_percentage)
125 else:
126 # text_or_percentage is status string
127 self.progress_label.value = f"Status: {text_or_percentage}"
128 if "finished" in str(text_or_percentage):
129 self.progress_bar.value = 1.0
130 self.progress_bar.bar_style = "success"
131 elif "failed" in str(text_or_percentage):
132 self.progress_bar.bar_style = "danger"
133 # color box red to see failure even when progress value is 0
134 self.progress_box.box_style = "danger"
135
136 def show(self):
137 """
138 Display the widget
139 """
140 from IPython.core.display import display
141
143
144
146 """
147 Viewer object for the store entries.
148 Do not use it on your own.
149 """
150
151 def __init__(self, collections):
152 """
153 Create a new collections viewer with the collections object from the process.
154 Collections must be a StoreContentList with a list of StoreContents.
155 """
156
157 self.collections = collections
158
159
160 self.table_row_html = """<tr><td style="padding: 10px 0;">{content.name}</td>
161 <td style="padding: 10px 0;">{content.number}</td></tr>"""
162
163 def create(self):
164 """
165 Create the widget
166 """
167
168 import ipywidgets as widgets
169
170 if self.collections is None:
171 return widgets.HTML("")
172
173 a = widgets.Tab()
174 children = []
175
176 for i, event in enumerate(self.collections):
177 html = widgets.HTML()
178 html.value = """<table style="border-collapse: separate; border-spacing: 50px 0;">"""
179 for store_content in event.content:
180 html.value += self.table_row_html.format(content=store_content)
181 html.value += "</table>"
182 children.append(html)
183 a.set_title(i, "Event " + str(event.event_number))
184
185 a.children = children
186
187 return a
188
189
191 """
192 A viewer widget for displaying the
193 statistics in a nicer way in ipython
194 """
195
196 def __init__(self, statistics):
197 """ Init the widget with the statistics from the process.
198 The statistics must be an instance of Statistics. """
199
200 self.statistics = statistics
201
202
203 self.table_column_html = """<td style="padding: 10px;">{content}</td>"""
204
205 self.table_column_3_html = """<td colspan="3" style="padding: 10px;">{content}</td>"""
206
207 self.table_cell_html = """<td style="padding: 10px; text-align: left">{content}</td>"""
208
209 self.table_cell_3_html = """<td style=\"text-align: right\">{content[0]}</td><td>{content[1]}</td><td>{content[2]}</td>"""
210
211 def create(self):
212 """
213 Create the widget
214 """
215 import ipywidgets as widgets
216
217 if self.statistics is None:
218 return widgets.HTML("")
219
220 html = widgets.HTML()
221 html.value = """<table style="border-collapse: collapsed; border: 1px solid black;">
222 <thead><tr style="background: #AAA; font-weight: bold">"""
223
224 for column in self.statistics.columns:
225 if column.three_column_format:
226 html.value += self.table_column_3_html.format(content=column.display_name)
227 else:
228 html.value += self.table_column_html.format(content=column.display_name)
229 html.value += "</tr></thead><tbody>"
230
231 for n, module in enumerate(self.statistics.modules):
232 if n % 2 == 1:
233 html.value += """<tr style="background: #EEE;">"""
234 else:
235 html.value += """<tr>"""
236
237 for column in self.statistics.columns:
238 if column.three_column_format:
239 html.value += self.table_cell_3_html.format(content=module[column.name])
240 else:
241 html.value += self.table_cell_html.format(content=module[column.name])
242
243 html.value += "</tr>"
244
245 # SUMMARY html.value += """<tr style="border: 1px solid black; font-weight: bold">"""
246
247 html.value += "</tbody></table>"
248 html.margin = "10px"
249 return html
250
251
253 """
254 A widget to summarize all the infromation from different processes.
255 Must be filled with the widgets of the single processes
256 """
257
258 def __init__(self, children):
259 """
260 Create a process viewer
261 """
262
263
264 self.children = children
265
266 def create(self):
267 """
268 Create the widget
269 """
270 import ipywidgets as widgets
271
272 a = widgets.Tab()
273 for i in range(len(self.children)):
274 a.set_title(i, "Process " + str(i))
275 a.children = [children.create() for children in self.children if children is not None]
276 return a
277
278 def show(self):
279 """
280 Show the widget
281 """
282
283 import ipywidgets as widgets
284 from IPython.core.display import display
285
286 if len(self.children) > 0:
287 a = self.create()
288
289 else:
290 a = widgets.HTML("<strong>Calculation list empty. Nothing to show.</strong>")
291
292 display(a)
293
294
296 """
297 A widget to show the log of a calculation.
298 """
299
300 def __init__(self, log_content):
301 """
302 Initialize the log viewer.
303 """
304
305
306 self.log_content = log_content
307
308
309 self.log_levels = ["DEBUG", "INFO", "RESULT", "WARNING", "ERROR", "FATAL", "DEFAULT"]
310
311
312 self.log_color_codes = {"DEBUG": "gray", "ERROR": "red", "FATAL": "red", "INFO": "black", "RESULT": "green",
313 "WARNING": "orange", "DEFAULT": "black"}
314
315
316 self.log_message = """<pre class="log-line-{type_lower}" title="{info}">[{level}] {message}{var_output}</pre>"""
317
318
319 self.toggle_button_line = """<a onclick="$('.log-line-{type_lower}').hide();
320 $('.log-line-{type_lower}-hide-button').hide();
321 $('.log-line-{type_lower}-show-button').show();"
322 style="cursor: pointer; margin: 0px 10px;"
323 class="log-line-{type_lower}-hide-button">Hide {type_upper}</a>
324 <a onclick="$('.log-line-{type_lower}').show();
325 $('.log-line-{type_lower}-hide-button').show();
326 $('.log-line-{type_lower}-show-button').hide();"
327 style="cursor: pointer; margin: 0px 10px; display: none;"
328 class="log-line-{type_lower}-show-button">Show {type_upper}</a>"""
329
330 def format_logmessage(self, buf, message, indent=4, base=0):
331 """
332 Format the json object of a logmessage as key: value list with recursion and indentation
333 Funnily this is faster than json.dumps() and looks a bit better in the title attribute
334 """
335 for key, val in message.items():
336 if not val:
337 continue
338 if isinstance(val, dict):
339 buf.write(f"{key}:\n")
340 self.format_logmessage(buf, val, indent=indent, base=base + indent)
341 else:
342 buf.write(base * " ")
343 buf.write(f"{key}: {val!r}\n")
344
345 def create(self):
346 """
347 Create the log viewer.
348 """
349 from ipywidgets import HTML, HBox, VBox
350
351 output = StringIO()
352 output.write("<style scoped>\n")
353 for level, color in self.log_color_codes.items():
354 level = level.lower()
355 output.write(f".log-line-{level} {{margin:0; padding:0; line-height:normal; color: {color} !important;}}\n")
356
357 output.write("""</style><div style="max-height: 400px; overflow-y: auto; width: 100%";>""")
358
359 for line in self.log_content.split("\n"):
360 if line.startswith('{"level"'):
361 try:
362 message = json.loads(line)
363 # ok, message is parsed. Prepare some info string which
364 # contains the info about the message in a indented
365 # format but don't completely json or pprint because it
366 # takes to much time
367 buf = StringIO()
368 self.format_logmessage(buf, message)
369 info = escape(buf.getvalue())
370 # add variables if necessary
371 variables = message.get("variables", "")
372 if variables:
373 variables = "\n".join([""] + [f"\t{k} = {v}" for k, v in variables.items()])
374 # and write out
375 level = message["level"].lower()
376 output.write(self.log_message.format(info=info, type_lower=level, var_output=variables, **message))
377 continue
378 except json.JSONDecodeError:
379 # any error: treat as default output, not a log line
380 pass
381
382 output.write('<pre class="log-line-default">')
383 output.write(line)
384 output.write('</pre>')
385
386 output.write("</div>")
387
388 html = HTML()
389 html.value = output.getvalue()
390 html.width = "100%"
391 html.margin = "5px"
392
393 buttons = []
394 for type in self.log_levels:
395 buttons.append(HTML(self.toggle_button_line.format(type_lower=type.lower(), type_upper=type.upper())))
396
397 buttons_view = HBox(buttons)
398 buttons_view.margin = "10px 0px"
399 result_vbox = VBox((buttons_view, html))
400
401 return result_vbox
collections
The collections to show.
Definition: viewer.py:157
def __init__(self, collections)
Definition: viewer.py:151
table_row_html
Template for a table row.
Definition: viewer.py:160
def format_logmessage(self, buf, message, indent=4, base=0)
Definition: viewer.py:330
toggle_button_line
The toggle button.
Definition: viewer.py:319
def __init__(self, log_content)
Definition: viewer.py:300
log_content
The log content to show.
Definition: viewer.py:306
log_message
A templated line in the log.
Definition: viewer.py:316
log_color_codes
The color codes for the log messages.
Definition: viewer.py:312
log_levels
The log levels of the framework.
Definition: viewer.py:309
def __init__(self, children)
Definition: viewer.py:258
children
The children for each process.
Definition: viewer.py:264
progress_label
Label for the progress bar, shows progress in percent or status.
Definition: viewer.py:88
last_percentage
The starting percentage (obviously 0)
Definition: viewer.py:82
def update(self, text_or_percentage)
Definition: viewer.py:94
progress_box
Box widget that will be displayed, contains progress bar and status label.
Definition: viewer.py:90
progress_bar
Widget of the progress bar itself.
Definition: viewer.py:85
last_time
The starting time of the process.
Definition: viewer.py:80
statistics
The statistics we want to show.
Definition: viewer.py:200
table_column_3_html
Template for a table cell spanning 3 columns.
Definition: viewer.py:205
table_cell_html
Template for a table cell with left alignment.
Definition: viewer.py:207
def __init__(self, statistics)
Definition: viewer.py:196
table_column_html
Template for a table cell.
Definition: viewer.py:203
table_cell_3_html
Template for a table cell with 3 columns.
Definition: viewer.py:209