p3lib 1.1.73__py3-none-any.whl → 1.1.75__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.
p3lib/bokeh_gui.py CHANGED
@@ -34,13 +34,7 @@ from bokeh.models import Tabs
34
34
  from bokeh.models import DataTable, TableColumn
35
35
  from bokeh.models import CustomJS
36
36
  from bokeh import events
37
-
38
- # In bokeh 2.4.8 -> 3.0.3 Panel was removed.
39
- # TabPanel can be used instead.
40
- try:
41
- from bokeh.models import Panel
42
- except ImportError:
43
- from bokeh.models import TabPanel as Panel
37
+ from bokeh.models import TabPanel
44
38
 
45
39
  class UpdateEvent(object):
46
40
  """@brief Responsible for holding the state of an event sent from a non GUI thread
@@ -251,7 +245,7 @@ class TimeSeriesPlotter(TabbedGUI):
251
245
 
252
246
  plotPanel = self._getPlotPanel()
253
247
 
254
- self._tabList.append( Panel(child=plotPanel, title="Plots") )
248
+ self._tabList.append( TabPanel(child=plotPanel, title="Plots") )
255
249
  self._doc.add_root( Tabs(tabs=self._tabList) )
256
250
  self._doc.add_periodic_callback(self._update, 100)
257
251
 
p3lib/ngt.py ADDED
@@ -0,0 +1,792 @@
1
+ # !/usr/bin/env python3
2
+
3
+ """NiceGui Tools
4
+ Responsible for providing helper classes for nicegui interfaces
5
+ aimed at reducing coding required for a GUI.
6
+ """
7
+
8
+ import traceback
9
+ import os
10
+ import platform
11
+
12
+ from time import sleep
13
+ from queue import Queue
14
+ from time import time, strftime, localtime
15
+
16
+ from nicegui import ui
17
+
18
+ class TabbedNiceGui(object):
19
+ """@brief Responsible for starting the providing a tabbed GUI.
20
+ This class is designed to ease the creation of a tabbed GUI interface.
21
+ The contents of each tab can be defined in the subclass.
22
+ The GUI includes a message log area below each tab. Tasks can send messages
23
+ to this log area.
24
+ If a subclass sets the self._logFile attributed then all messages sent to the
25
+ log area are written to a log file with timestamps."""
26
+
27
+ # This can be used in the markdown text for a TAB description to give slightly larger text
28
+ # than normal.
29
+ DESCRIP_STYLE = '<span style="font-size:1.2em;">'
30
+ ENABLE_BUTTONS = "ENABLE_BUTTONS"
31
+ NOTIFY_DIALOG = "NOTIFY_DIALOG"
32
+ UPDATE_SECONDS = "UPDATE_SECONDS"
33
+ INFO_MESSAGE = "INFO: "
34
+ WARN_MESSAGE = "WARN: "
35
+ ERROR_MESSAGE = "ERROR: "
36
+ DEBUG_MESSAGE = "DEBUG: "
37
+ MAX_PROGRESS_VALUE = 100
38
+ DEFAULT_SERVER_PORT = 9812
39
+ GUI_TIMER_SECONDS = 0.1
40
+ PROGRESS_TIMER_SECONDS = 1.0
41
+ UPDATE_SECONDS = "UPDATE_SECONDS"
42
+ DEFAULT_GUI_RESPONSE_TIMEOUT= 30.0
43
+ POETRY_CONFIG_FILE = "pyproject.toml"
44
+ LOCAL_PATH = os.path.dirname(os.path.abspath(__file__))
45
+
46
+ @staticmethod
47
+ def GetDateTimeStamp():
48
+ """@return The log file date/time stamp """
49
+ return strftime("%Y%m%d%H%M%S", localtime()).lower()
50
+
51
+ @staticmethod
52
+ def GetInstallFolder():
53
+ """@return The folder where the apps are installed."""
54
+ installFolder = os.path.dirname(__file__)
55
+ if not os.path.isdir(installFolder):
56
+ raise Exception(f"{installFolder} folder not found.")
57
+ return installFolder
58
+
59
+ @staticmethod
60
+ def GetLogFileName(logFilePrefix):
61
+ """@param logFilePrefix The text in the log file name before the timestamp.
62
+ @return The name of the logfile including datetime stamp."""
63
+ dateTimeStamp = TabbedNiceGui.GetDateTimeStamp()
64
+ logFileName = f"{logFilePrefix}_{dateTimeStamp}.log"
65
+ return logFileName
66
+
67
+ @staticmethod
68
+ def CheckPort(port):
69
+ """@brief Check the server port.
70
+ @param port The server port."""
71
+ if port < 1024:
72
+ raise Exception("The minimum TCP port that you can bind the GUI server to is 1024.")
73
+ if port > 65535:
74
+ raise Exception("The maximum TCP port that you can bind the GUI server to is 65535.")
75
+
76
+ @staticmethod
77
+ def GetProgramVersion():
78
+ """@brief Get the program version from the poetry pyproject.toml file.
79
+ @return The version of the installed program (string value)."""
80
+ poetryConfigFile = os.path.join(TabbedNiceGui.LOCAL_PATH, TabbedNiceGui.POETRY_CONFIG_FILE)
81
+ if not os.path.isfile(poetryConfigFile):
82
+ poetryConfigFile = os.path.join(TabbedNiceGui.LOCAL_PATH, ".." + os.sep + TabbedNiceGui.POETRY_CONFIG_FILE)
83
+ poetryConfigFile2 = poetryConfigFile
84
+ if not os.path.isfile(poetryConfigFile):
85
+ cwd = os.getcwd()
86
+ poetryConfigFile = os.path.join(cwd, TabbedNiceGui.POETRY_CONFIG_FILE)
87
+ if not os.path.isfile(poetryConfigFile):
88
+ raise Exception(f"{poetryConfigFile}, {poetryConfigFile2} and {poetryConfigFile} not found.")
89
+
90
+ programVersion = None
91
+ with open(poetryConfigFile, 'r') as fd:
92
+ lines = fd.readlines()
93
+ for line in lines:
94
+ line=line.strip("\r\n")
95
+ if line.startswith('version'):
96
+ elems = line.split("=")
97
+ if len(elems) == 2:
98
+ programVersion = elems[1].strip('" ')
99
+ break
100
+ if programVersion is None:
101
+ raise Exception(f"Failed to extract program version from '{line}' line of {poetryConfigFile} file.")
102
+ return programVersion
103
+
104
+ def __init__(self, debugEnabled, logPath=None):
105
+ """@brief Constructor
106
+ @param debugEnabled True if debugging is enabled.
107
+ @param logPath The path to store log files. If left as None then no log files are created."""
108
+ self._debugEnabled = debugEnabled
109
+ self._logFile = None # This must be defined in subclass if logging to a file is required.
110
+ self._buttonList = []
111
+ self._logMessageCount = 0
112
+ self._updateProgressOnTimer = False
113
+ self._progressStepValue = 0
114
+ self._progressBarStartMessage = ""
115
+ self._progressBarExpectedMessageList = []
116
+ self._expectedProgressBarMessageIndex = 0
117
+ self._programVersion = TabbedNiceGui.GetProgramVersion()
118
+
119
+ self._logPath = None
120
+ if logPath:
121
+ self._logPath = os.path.join(os.path.expanduser('~'), logPath)
122
+ self._ensureLogPathExists()
123
+
124
+ self._isWindows = platform.system() == "Windows"
125
+ self._installFolder = TabbedNiceGui.GetInstallFolder()
126
+
127
+ # Make the install folder our current dir
128
+ os.chdir(self._installFolder)
129
+
130
+ # This queue is used to send commands from any thread to the GUI thread.
131
+ self._toGUIQueue = Queue()
132
+ # This queue is for the GUI thread to send messages to other threads
133
+ self._fromGUIQueue = Queue()
134
+
135
+ def _ensureLogPathExists(self):
136
+ """@brief Ensure that the log path exists."""
137
+ if not os.path.isdir(self._logPath):
138
+ os.makedirs(self._logPath)
139
+
140
+ def getLogPath(self):
141
+ """@return the Log file path if defined."""
142
+ return self._logPath
143
+
144
+ # Start ------------------------------
145
+ # Methods that allow the GUI to display standard UIO messages
146
+ # This allows the GUI to be used with code that was written
147
+ # to be used on the command line using UIO class instances
148
+ #
149
+ def info(self, msg):
150
+ """@brief Send a info message to be displayed in the GUI.
151
+ This can be called from outside the GUI thread.
152
+ @param msg The message to be displayed."""
153
+ msgDict = {TabbedNiceGui.INFO_MESSAGE: str(msg)}
154
+ self.updateGUI(msgDict)
155
+
156
+ def warn(self, msg):
157
+ """@brief Send a warning message to be displayed in the GUI.
158
+ This can be called from outside the GUI thread.
159
+ @param msg The message to be displayed."""
160
+ msgDict = {TabbedNiceGui.WARN_MESSAGE: str(msg)}
161
+ self.updateGUI(msgDict)
162
+
163
+ def error(self, msg):
164
+ """@brief Send a error message to be displayed in the GUI.
165
+ This can be called from outside the GUI thread.
166
+ @param msg The message to be displayed."""
167
+ msgDict = {TabbedNiceGui.ERROR_MESSAGE: str(msg)}
168
+ self.updateGUI(msgDict)
169
+
170
+ def infoDialog(self, msg):
171
+ """@brief Display an info level dialog.
172
+ @param msg The message dialog."""
173
+ msgDict = {TabbedNiceGui.NOTIFY_DIALOG: str(msg)}
174
+ self.updateGUI(msgDict)
175
+
176
+ def debug(self, msg):
177
+ """@brief Send a debug message to be displayed in the GUI.
178
+ This can be called from outside the GUI thread.
179
+ @param msg The message to be displayed."""
180
+ if self._debugEnabled:
181
+ msgDict = {TabbedNiceGui.DEBUG_MESSAGE: str(msg)}
182
+ self.updateGUI(msgDict)
183
+
184
+ async def getInput(self, prompt):
185
+ """@brief Allow the user to enter some text.
186
+ This can be called from outside the GUI thread.
187
+ @param prompt The user prompt."""
188
+ with ui.dialog() as dialog, ui.card():
189
+ inputObj = ui.input(label=prompt)
190
+ with ui.row():
191
+ ui.button('OK', on_click=lambda: dialog.submit('OK'))
192
+ ui.button('Cancel', on_click=lambda: dialog.submit('Cancel'))
193
+
194
+ result = await dialog
195
+ if result != 'OK':
196
+ returnText = None
197
+ else:
198
+ returnText = inputObj.value
199
+ return returnText
200
+
201
+ def reportException(self, exception):
202
+ """@brief Report an exception.
203
+ If debug is enabled a full stack trace is displayed.
204
+ If not then the exception message is displayed.
205
+ @param exception The exception instance."""
206
+ if self._debugEnabled:
207
+ lines = traceback.format_exc().split("\n")
208
+ for line in lines:
209
+ self.error(line)
210
+ if len(exception.args) > 0:
211
+ self.error(exception.args[0])
212
+
213
+ def _sendEnableAllButtons(self, state):
214
+ """@brief Send a message to the GUI to enable/disable all the GUI buttons.
215
+ This can be called from outside the GUI thread.
216
+ @param state If True enable the buttons, else disable them."""
217
+ msgDict = {TabbedNiceGui.ENABLE_BUTTONS: state}
218
+ self.updateGUI(msgDict)
219
+
220
+ def updateGUI(self, msgDict):
221
+ """@brief Send a message to the GUI so that it updates itself.
222
+ @param msgDict A dict containing details of how to update the GUI."""
223
+ # Record the seconds when we received the message
224
+ msgDict[TabbedNiceGui.UPDATE_SECONDS]=time()
225
+ self._toGUIQueue.put(msgDict)
226
+
227
+ def showTable(self, table, rowSeparatorChar = "-", colSeparatorChar = "|"):
228
+ """@brief Show the contents of a table to the user.
229
+ @param table This must be a list. Each list element must be a table row (list).
230
+ Each element in each row must be a string.
231
+ @param rowSeparatorChar The character used for horizontal lines to separate table rows.
232
+ @param colSeparatorChar The character used to separate table columns."""
233
+ columnWidths = []
234
+ # Check we have a table to display
235
+ if len(table) == 0:
236
+ raise Exception("No table rows to display")
237
+
238
+ # Check all rows have the same number of columns in the table
239
+ colCount = len(table[0])
240
+ for row in table:
241
+ if len(row) != colCount:
242
+ raise Exception(f"{str(row)} column count different from first row ({colCount})")
243
+
244
+ for row in table:
245
+ for col in row:
246
+ if not isinstance(col, str):
247
+ raise Exception(f"Table column is not a string: {col} in {row}")
248
+
249
+ # Get the max width for each column
250
+ for col in range(0,colCount):
251
+ maxWidth=0
252
+ for row in table:
253
+ if len(row[col]) > maxWidth:
254
+ maxWidth = len(row[col])
255
+ columnWidths.append(maxWidth)
256
+
257
+ tableWidth = 1
258
+ for columnWidth in columnWidths:
259
+ tableWidth += columnWidth + 3 # Space each side of the column + a column divider character
260
+
261
+ # Add the top line of the table
262
+ self.info(rowSeparatorChar*tableWidth)
263
+
264
+ # The starting row index
265
+ for rowIndex in range(0, len(table)):
266
+ rowText = colSeparatorChar
267
+ colIndex = 0
268
+ for col in table[rowIndex]:
269
+ colWidth = columnWidths[colIndex]
270
+ rowText = rowText + " " + f"{col:>{colWidth}s}" + " " + colSeparatorChar
271
+ colIndex += 1
272
+ self.info(rowText)
273
+ # Add the row separator line
274
+ self.info(rowSeparatorChar*tableWidth)
275
+
276
+ def logAll(self, enabled):
277
+ pass
278
+
279
+ def setLogFile(self, logFile):
280
+ pass
281
+
282
+ # End ------------------------------
283
+
284
+ def _saveLogMsg(self, msg):
285
+ """@brief Save the message to a log file.
286
+ @param msg The message text to be stored in the log file."""
287
+ # If a log file has been set
288
+ if self._logFile:
289
+ # If the log file does not exist
290
+ if not os.path.isfile(self._logFile):
291
+ with open(self._logFile, 'w') as fd:
292
+ pass
293
+ # Update the log file
294
+ with open(self._logFile, 'a') as fd:
295
+ dateTimeStamp = TabbedNiceGui.GetDateTimeStamp()
296
+ fd.write(dateTimeStamp + ": " + msg + '\n')
297
+
298
+ def _getDisplayMsg(self, msg, prefix):
299
+ """@brief Get the msg to display. If the msg does not already have a msg level we add one.
300
+ @param msg The source msg.
301
+ @param prefix The message prefix (level indcator) to add."""
302
+ if msg.startswith(TabbedNiceGui.INFO_MESSAGE) or \
303
+ msg.startswith(TabbedNiceGui.WARN_MESSAGE) or \
304
+ msg.startswith(TabbedNiceGui.ERROR_MESSAGE) or \
305
+ msg.startswith(TabbedNiceGui.DEBUG_MESSAGE):
306
+ _msg = msg
307
+ else:
308
+ _msg = prefix + msg
309
+ return _msg
310
+
311
+ def _handleMsg(self, msg):
312
+ """@brief Log a message.
313
+ @param msg the message to the log window and the log file."""
314
+ self._log.push(msg)
315
+ self._saveLogMsg(msg)
316
+ self._logMessageCount += 1
317
+ # We've received a log message so update progress bar if required.
318
+ self._updateProgressBar(msg)
319
+
320
+ def _infoGT(self, msg):
321
+ """@brief Update an info level message. This must be called from the GUI thread.
322
+ @param msg The message to display."""
323
+ _msg = self._getDisplayMsg(msg, TabbedNiceGui.INFO_MESSAGE)
324
+ self._handleMsg(_msg)
325
+
326
+ def _warnGT(self, msg):
327
+ """@brief Update an warning level message. This must be called from the GUI thread.
328
+ @param msg The message to display."""
329
+ _msg = self._getDisplayMsg(msg, TabbedNiceGui.WARN_MESSAGE)
330
+ self._handleMsg(_msg)
331
+
332
+ def _errorGT(self, msg):
333
+ """@brief Update an error level message. This must be called from the GUI thread.
334
+ @param msg The message to display."""
335
+ _msg = self._getDisplayMsg(msg, TabbedNiceGui.ERROR_MESSAGE)
336
+ self._handleMsg(_msg)
337
+
338
+ def _debugGT(self, msg):
339
+ """@brief Update an debug level message. This must be called from the GUI thread.
340
+ @param msg The message to display."""
341
+ _msg = self._getDisplayMsg(msg, TabbedNiceGui.DEBUG_MESSAGE)
342
+ self._handleMsg(_msg)
343
+
344
+ def _clearMessages(self):
345
+ """@brief Clear all messages from the log."""
346
+ self._log.clear()
347
+ self._logMessageCount = 0
348
+
349
+ def _getLogMessageCount(self):
350
+ """@return the number of messages written to the log window/file"""
351
+ return self._logMessageCount
352
+
353
+ def _enableAllButtons(self, enabled):
354
+ """@brief Enable/Disable all buttons.
355
+ @param enabled True if button is enabled."""
356
+ if enabled:
357
+ for button in self._buttonList:
358
+ button.enable()
359
+ # No buttons are enabled, any executed task must be complete therefor hide the progress bar.
360
+ self._stopProgress()
361
+
362
+ else:
363
+ for button in self._buttonList:
364
+ button.disable()
365
+ # If the caller has defined the number of log messages for normal completion
366
+ if self._progressStepValue > 0:
367
+ self._progress.set_visibility(True)
368
+
369
+ def guiTimerCallback(self):
370
+ """@called periodically (quickly) to allow updates of the GUI."""
371
+ while not self._toGUIQueue.empty():
372
+ rxMessage = self._toGUIQueue.get()
373
+ if isinstance(rxMessage, dict):
374
+ self._processRXDict(rxMessage)
375
+
376
+ def initGUI(self, tabNameList, tabMethodInitList, reload=True, address="0.0.0.0", port=DEFAULT_SERVER_PORT, pageTitle="NiceGUI"):
377
+ """@brief Init the tabbed GUI.
378
+ @param tabNameList A list of the names of each tab to be created.
379
+ @param tabMethodInitList A list of the methods to be called to init each of the above tabs.
380
+ The two lists must be the same size.
381
+ @param reload If reload is set False then changes to python files will not cause the server to be restarted.
382
+ @param address The address to bind the server to.
383
+ @param The TCP port to bind the server to.
384
+ @param pageTitle The page title that appears in the browser."""
385
+ # A bit of defensive programming.
386
+ if len(tabNameList) != len(tabMethodInitList):
387
+ raise Exception(f"initGUI: BUG: tabNameList ({len(tabNameList)}) and tabMethodInitList ({len(tabMethodInitList)}) are not the same length.")
388
+ tabObjList = []
389
+ with ui.row():
390
+ with ui.tabs().classes('w-full') as tabs:
391
+ for tabName in tabNameList:
392
+ tabObj = ui.tab(tabName)
393
+ tabObjList.append(tabObj)
394
+
395
+ with ui.tab_panels(tabs, value=tabObjList[0]).classes('w-full'):
396
+ for tabObj in tabObjList:
397
+ with ui.tab_panel(tabObj):
398
+ tabIndex = tabObjList.index(tabObj)
399
+ tabMethodInitList[tabIndex]()
400
+
401
+ guiLogLevel = "warning"
402
+ if self._debugEnabled:
403
+ guiLogLevel = "debug"
404
+
405
+ ui.label("Message Log")
406
+ self._progress = ui.slider(min=0,max=TabbedNiceGui.MAX_PROGRESS_VALUE,step=1)
407
+ self._progress.set_visibility(False)
408
+ self._progress.min = 0
409
+ # Don't allow user to adjust progress bar thumb
410
+ self._progress.disable()
411
+ self._log = ui.log(max_lines=2000)
412
+ self._log.set_visibility(True)
413
+
414
+ with ui.row():
415
+ ui.button('Clear Log', on_click=self._clearLog)
416
+ ui.button('Log Message Count', on_click=self._showLogMsgCount)
417
+ ui.button('Quit', on_click=self.close)
418
+
419
+ with ui.row():
420
+ ui.label(f"Software Version: {self._programVersion}")
421
+
422
+ ui.timer(interval=TabbedNiceGui.GUI_TIMER_SECONDS, callback=self.guiTimerCallback)
423
+ ui.timer(interval=TabbedNiceGui.PROGRESS_TIMER_SECONDS, callback=self.progressTimerCallback)
424
+ ui.run(host=address, port=port, title=pageTitle, dark=True, uvicorn_logging_level=guiLogLevel, reload=reload)
425
+
426
+ def progressTimerCallback(self):
427
+ """@brief Time to update the progress bar. We run the timer all the time because there appears to be a
428
+ bug in the ui.timer instance. Calling cancel() does not appear to cancel the timer."""
429
+ if self._updateProgressOnTimer and self._progress.visible:
430
+ # Increment the progress bar
431
+ self._progress.set_value( self._progress.value + self._progressStepValue )
432
+
433
+ def _startProgress(self, durationSeconds=0, startMessage=None, expectedMsgList=[]):
434
+ """@brief Start a timer that will update the progress bar.
435
+ The progress bar can simply update on a timer every second with durationSeconds set to the expected length
436
+ of the task if startMessage is unset and startMessage = None and expectedMessageCount = 0.
437
+
438
+ Alternatively if durationSeconds is set and startMessage (expectedMessageCount = 0) is set to some text that
439
+ we expect to receive in the log before the progress timer (as detailed above) is triggered.
440
+
441
+ Alternatively if expectedMsgList contains a list of strings we expect to receive then the progress bar is
442
+ updated as each message is received. The messages may be the entire line of a log message or parts of a log message line.
443
+ @param startMessage The text of the log message we expect to receive to trigger the progress bar timer start.
444
+ @param expectedMsgList A list of the expected log file messages."""
445
+ self._progressValue = 0
446
+ self._progressBarStartMessage = ""
447
+ self._progressBarExpectedMessageList = []
448
+ self._expectedProgressBarMessageIndex = 0
449
+ self._updateProgressOnTimer = False
450
+ self._progress.set_value( self._progressValue )
451
+ # If the caller wants to update the progress bar on expected messages.
452
+ if len(expectedMsgList):
453
+ #Use the text of log messages to increment the progress bar.
454
+ self._expectedProgressBarMessageIndex = 0
455
+ self._progressBarExpectedMessageList = expectedMsgList
456
+ self._progressStepValue = TabbedNiceGui.MAX_PROGRESS_VALUE/float(len(expectedMsgList))
457
+
458
+ elif durationSeconds > 0:
459
+ # Calc the step size required to give the required duration
460
+ self._progressStepValue = TabbedNiceGui.MAX_PROGRESS_VALUE/float(durationSeconds)
461
+ if startMessage:
462
+ self._progressBarStartMessage = startMessage
463
+ else:
464
+ # Start updating the progress bar now.
465
+ self._updateProgressOnTimer = True
466
+
467
+ else:
468
+ raise Exception("BUG: _startProgressTimer() called. len(expectedMsgList)=0 and durationSeconds<=0.")
469
+
470
+ self._progress.set_visibility(True)
471
+
472
+ def _stopProgress(self):
473
+ """@brief Stop the progress bar being updated and hide it."""
474
+ self._updateProgressOnTimer = False
475
+ self._progress.set_visibility(False)
476
+
477
+ def _updateProgressBar(self, msg):
478
+ """@brief Update the progress bar if required when a log message is received. This is called as each message is added to the log.
479
+ @param msg The log message received."""
480
+ # If we have a list of log messages to update the progress bar.
481
+ if len(self._progressBarExpectedMessageList) > 0:
482
+ if self._expectedProgressBarMessageIndex < len(self._progressBarExpectedMessageList):
483
+ # Get the message we expect to receive next
484
+ expectedMsg = self._progressBarExpectedMessageList[self._expectedProgressBarMessageIndex]
485
+ if msg.find(expectedMsg) != -1:
486
+ self._progressValue = self._progressValue + self._progressStepValue
487
+ self._progress.set_value( self._progressValue )
488
+ self._expectedProgressBarMessageIndex += 1
489
+
490
+ # If we have a message that we expect to receive to start the progress bar timer.
491
+ elif self._progressBarStartMessage and len(self._progressBarStartMessage):
492
+ # If we found the start message in the message received.
493
+ if msg.find(self._progressBarStartMessage) != -1:
494
+ # Start updating the progress bar now on the timer.
495
+ self._updateProgressOnTimer = True
496
+
497
+ def _initTask(self):
498
+ """@brief Should be called before a task is started."""
499
+ self._enableAllButtons(False)
500
+ self._clearMessages()
501
+
502
+ def _clearLog(self):
503
+ """@brief Clear the log text"""
504
+ if self._log:
505
+ self._log.clear()
506
+
507
+ def _showLogMsgCount(self):
508
+ """@brief Show the number of log messages"""
509
+ ui.notify(f"{self._getLogMessageCount()} messages in the log.")
510
+
511
+ def close(self):
512
+ """@brief Close down the app server."""
513
+ ui.notify("Press 'CTRL C' at command line or close the terminal window to quit.")
514
+ # A subclass close() method can call
515
+ # app.shutdown()
516
+ # if reload=False on ui.run()
517
+
518
+ def _appendButtonList(self, button):
519
+ """@brief Add to the button list. These buttons are disabled during the progress of a task.
520
+ @param button The button instance."""
521
+ self._buttonList.append(button)
522
+
523
+ def _processRXDict(self, rxDict):
524
+ """@brief Process the dicts received from the GUI message queue.
525
+ @param rxDict The dict received from the GUI message queue."""
526
+ if TabbedNiceGui.INFO_MESSAGE in rxDict:
527
+ msg = rxDict[TabbedNiceGui.INFO_MESSAGE]
528
+ self._infoGT(msg)
529
+
530
+ elif TabbedNiceGui.WARN_MESSAGE in rxDict:
531
+ msg = rxDict[TabbedNiceGui.WARN_MESSAGE]
532
+ self._warnGT(msg)
533
+
534
+ elif TabbedNiceGui.ERROR_MESSAGE in rxDict:
535
+ msg = rxDict[TabbedNiceGui.ERROR_MESSAGE]
536
+ self._errorGT(msg)
537
+
538
+ elif TabbedNiceGui.DEBUG_MESSAGE in rxDict:
539
+ msg = rxDict[TabbedNiceGui.DEBUG_MESSAGE]
540
+ self._debugGT(msg)
541
+
542
+ elif TabbedNiceGui.ENABLE_BUTTONS in rxDict:
543
+ state = rxDict[TabbedNiceGui.ENABLE_BUTTONS]
544
+ self._enableAllButtons(state)
545
+
546
+ elif TabbedNiceGui.NOTIFY_DIALOG in rxDict:
547
+ message = rxDict[TabbedNiceGui.NOTIFY_DIALOG]
548
+ ui.notify(message, close_button='OK', type="positive", position="center")
549
+
550
+ else:
551
+
552
+ self._handleGUIUpdate(rxDict)
553
+
554
+ def _updateGUI(self, msgDict):
555
+ """@brief Send a message to the GUI so that it updates itself.
556
+ @param msgDict A dict containing details of how to update the GUI."""
557
+ # Record the seconds when we received the message
558
+ msgDict[TabbedNiceGui.UPDATE_SECONDS]=time()
559
+ self._toGUIQueue.put(msgDict)
560
+
561
+ def _updateExeThread(self, msgDict):
562
+ """@brief Send a message from the GUI thread to an external (non GUI thread).
563
+ @param msgDict A dict containing messages to be sent to the external thread."""
564
+ # Record the seconds when we received the message
565
+ msgDict[TabbedNiceGui.UPDATE_SECONDS]=time()
566
+ self._fromGUIQueue.put(msgDict)
567
+
568
+ def _updateGUIAndWaitForResponse(self, msgDict, timeout=DEFAULT_GUI_RESPONSE_TIMEOUT):
569
+ """@brief Send a message to the GUI and wait for a response.
570
+ @param msgDict The message dictionary to be sent to the GUI.
571
+ @param timeout The number of seconds to wait for a response.
572
+ @return The return dict."""
573
+ timeoutT = time()+timeout
574
+ rxDict = None
575
+ self._updateGUI(msgDict)
576
+ while True:
577
+ if not self._fromGUIQueue.empty():
578
+ rxMessage = self._fromGUIQueue.get()
579
+ if isinstance(rxMessage, dict):
580
+ rxDict = rxMessage
581
+ break
582
+
583
+ elif time() >= timeoutT:
584
+ raise Exception(f"{timeout} second GUI response timeout.")
585
+
586
+ else:
587
+ # Don't spin to fast
588
+ sleep(0.1)
589
+
590
+ return rxDict
591
+
592
+ def _handleGUIUpdate(self, rxDict):
593
+ """@brief Process the dicts received from the GUI message queue
594
+ that were not handled by the parent class.
595
+ @param rxDict The dict received from the GUI message queue."""
596
+ raise NotImplementedError("_handleGUIUpdate() is not implemented. Implement this method in a subclass of TabbedNiceGUI")
597
+
598
+
599
+ class YesNoDialog(object):
600
+ """@brief Responsible for displaying a dialog box to the user with a boolean (I.E yes/no, ok/cancel) response."""
601
+ TEXT_INPUT_FIELD_TYPE = 1
602
+ NUMBER_INPUT_FIELD_TYPE = 2
603
+ SWITCH_INPUT_FIELD_TYPE = 3
604
+ DROPDOWN_INPUT_FIELD = 4
605
+ COLOR_INPUT_FIELD = 5
606
+ DATE_INPUT_FIELD = 6
607
+ TIME_INPUT_FIELD = 7
608
+ KNOB_INPUT_FIELD = 8
609
+ VALID_FIELD_TYPE_LIST = (TEXT_INPUT_FIELD_TYPE,
610
+ NUMBER_INPUT_FIELD_TYPE,
611
+ SWITCH_INPUT_FIELD_TYPE,
612
+ DROPDOWN_INPUT_FIELD,
613
+ COLOR_INPUT_FIELD,
614
+ DATE_INPUT_FIELD,
615
+ TIME_INPUT_FIELD,
616
+ KNOB_INPUT_FIELD)
617
+
618
+ FIELD_TYPE_KEY = "FIELD_TYPE_KEY" # The type of field to be displayed.
619
+ VALUE_KEY = "VALUE_KEY" # The value to be displayed in the field when the dialog is displayed.
620
+ MIN_NUMBER_KEY = "MIN_NUMBER_KEY" # If the type is NUMBER_INPUT_FIELD_TYPE, the min value that can be entered.
621
+ MAX_NUMBER_KEY = "MAX_NUMBER_KEY" # If the type is NUMBER_INPUT_FIELD_TYPE, the max value that can be entered.
622
+ WIDGET_KEY = "WIDGET_KEY" # The key to the GUI widget (E.G ui.input, ui.number etc)
623
+ OPTIONS_KEY = "OPTIONS_KEY" # Some input fields require a list of options (E.G DROPDOWN_INPUT_FIELD).
624
+
625
+ def __init__(self,
626
+ prompt,
627
+ successMethod,
628
+ failureMethod=None,
629
+ successButtonText="Yes",
630
+ failureButtonText="No"):
631
+ """@brief Constructor"""
632
+ self._dialog = None
633
+ self._selectedFile = None
634
+ self._successButtonText = None # The dialogs success button text
635
+ self._failureButtonText = None # The dialogs failure button text
636
+ self._prompt = None # The prompt to be displayed in the dialog
637
+ self._successMethod = None # The method to be called when the success button is selected.
638
+ self._failureMethod = None # The method to be called when the failure button is selected.
639
+ self._inputFieldDict = {} # A dict of input field details to be included in the dialog. Can be left as an empty dict if no input fields are required.
640
+ # The key in this dict is the name of the input field that the user sees.
641
+ # The value in this dict is another dict containing details of the input field which may be
642
+
643
+ self.setPrompt(prompt)
644
+ self.setSuccessMethod(successMethod)
645
+ self.setFailureMethod(failureMethod)
646
+ self.setSuccessButtonLabel(successButtonText)
647
+ self.setFailureButtonLabel(failureButtonText)
648
+
649
+
650
+ def addField(self, name, fieldType, value=None, minNumber=None, maxNumber=None, options=None):
651
+ """@brief Add a field to the dialog.
652
+ @param name The name of the field to be added.
653
+ @param fieldType The type of field to be entered.
654
+ @param value The optional initial value for the field when the dialog is displayed.
655
+ @param minNumber The optional min value if the fieldType = NUMBER_INPUT_FIELD_TYPE.
656
+ @param maxNumber The optional max value if the fieldType = NUMBER_INPUT_FIELD_TYPE.
657
+ """
658
+ if name and len(name) > 0:
659
+ if fieldType in YesNoDialog.VALID_FIELD_TYPE_LIST:
660
+ self._inputFieldDict[name] = {YesNoDialog.FIELD_TYPE_KEY: fieldType,
661
+ YesNoDialog.VALUE_KEY: value,
662
+ YesNoDialog.MIN_NUMBER_KEY: minNumber,
663
+ YesNoDialog.MAX_NUMBER_KEY: maxNumber,
664
+ YesNoDialog.OPTIONS_KEY: options}
665
+
666
+ else:
667
+ raise Exception(f"YesNoDialog.addField() {fieldType} is an invalid field type.")
668
+
669
+ else:
670
+ raise Exception("YesNoDialog.addField() name not set.")
671
+
672
+ def _init(self):
673
+ """@brief Init the dialog."""
674
+ with ui.dialog() as self._dialog, ui.card():
675
+ ui.label(self._prompt)
676
+ for fieldName in self._inputFieldDict:
677
+ fieldType = self._inputFieldDict[fieldName][YesNoDialog.FIELD_TYPE_KEY]
678
+ if fieldType == YesNoDialog.TEXT_INPUT_FIELD_TYPE:
679
+ widget = ui.input(label=fieldName).style('width: 200px;')
680
+
681
+ elif fieldType == YesNoDialog.NUMBER_INPUT_FIELD_TYPE:
682
+ value = self._inputFieldDict[fieldName][YesNoDialog.VALUE_KEY]
683
+ min = self._inputFieldDict[fieldName][YesNoDialog.MIN_NUMBER_KEY]
684
+ max = self._inputFieldDict[fieldName][YesNoDialog.MAX_NUMBER_KEY]
685
+ widget = ui.number(label=fieldName,
686
+ value=value,
687
+ min=min,
688
+ max=max).style('width: 200px;')
689
+
690
+ elif fieldType == YesNoDialog.SWITCH_INPUT_FIELD_TYPE:
691
+ widget = ui.switch(fieldName)
692
+
693
+ elif fieldType == YesNoDialog.DROPDOWN_INPUT_FIELD:
694
+ #ui.label(fieldName)
695
+ options = self._inputFieldDict[fieldName][YesNoDialog.OPTIONS_KEY]
696
+ if options:
697
+ widget = ui.select(options)
698
+ widget.tooltip(fieldName)
699
+ else:
700
+ raise Exception("BUG: DROPDOWN_INPUT_FIELD defined without defining the options.")
701
+
702
+ elif fieldType == YesNoDialog.COLOR_INPUT_FIELD:
703
+ widget = ui.color_input(label=fieldName)
704
+
705
+ elif fieldType == YesNoDialog.DATE_INPUT_FIELD:
706
+ widget = ui.date()
707
+ widget.tooltip(fieldName)
708
+
709
+ elif fieldType == YesNoDialog.TIME_INPUT_FIELD:
710
+ widget = ui.time()
711
+ widget.tooltip(fieldName)
712
+
713
+ elif fieldType == YesNoDialog.KNOB_INPUT_FIELD:
714
+ widget = ui.knob(show_value=True)
715
+ widget.tooltip(fieldName)
716
+
717
+ # Save a ref to the widet in the field dict
718
+ self._inputFieldDict[fieldName][YesNoDialog.WIDGET_KEY] = widget
719
+
720
+ # If we have an initial value then set it
721
+ value = self._inputFieldDict[fieldName][YesNoDialog.VALUE_KEY]
722
+ if value:
723
+ widget.value = value
724
+
725
+ with ui.row():
726
+ ui.button(self._successButtonText, on_click=self._internalSuccessMethod)
727
+ ui.button(self._failureButtonText, on_click=self._internalFailureMethod)
728
+
729
+ def setPrompt(self, prompt):
730
+ """@brief Set the user prompt.
731
+ @param prompt The user prompt."""
732
+ self._prompt = prompt
733
+
734
+ def setSuccessMethod(self, successMethod):
735
+ """@brief Set the text of the success button.
736
+ @param successMethod The method called when the user selects the success button."""
737
+ self._successMethod = successMethod
738
+
739
+ def setFailureMethod(self, failureMethod):
740
+ """@brief Set the text of the success button.
741
+ @param failureMethod The method called when the user selects the failure button."""
742
+ self._failureMethod = failureMethod
743
+
744
+ def setSuccessButtonLabel(self, label):
745
+ """@brief Set the text of the success button.
746
+ @param label The success button text."""
747
+ self._successButtonText = label
748
+
749
+ def setFailureButtonLabel(self, label):
750
+ """@brief Set the text of the failure button.
751
+ @param label The failure button text."""
752
+ self._failureButtonText = label
753
+
754
+ def show(self):
755
+ """@brief Allow the user to select yes/no, ok/cancel etc in response to a question."""
756
+ self._init()
757
+ self._dialog.open()
758
+
759
+ def getValue(self, fieldName):
760
+ """@brief Get the value entered by the user.
761
+ @param fieldName The name of the field entered."""
762
+ value = None
763
+ widget = self._inputFieldDict[fieldName][YesNoDialog.WIDGET_KEY]
764
+ if hasattr(widget, 'value'):
765
+ value = widget.value
766
+
767
+ elif isinstance(widget, ui.upload):
768
+ value = self._selectedFile
769
+
770
+ return value
771
+
772
+ def _internalSuccessMethod(self):
773
+ """@brief Called when the user selects the success button."""
774
+ self.close()
775
+ # Save the entered values for all fields
776
+ for fieldName in self._inputFieldDict:
777
+ widget = self._inputFieldDict[fieldName][YesNoDialog.WIDGET_KEY]
778
+ if hasattr(widget, 'value'):
779
+ self._inputFieldDict[fieldName][YesNoDialog.VALUE_KEY] = self._inputFieldDict[fieldName][YesNoDialog.WIDGET_KEY].value
780
+ # If defined call the method
781
+ if self._successMethod:
782
+ self._successMethod()
783
+
784
+ def _internalFailureMethod(self):
785
+ """@brief Called when the user selects the failure button."""
786
+ self.close()
787
+ if self._failureMethod:
788
+ self._failureMethod()
789
+
790
+ def close(self):
791
+ """@brief Close the boolean dialog."""
792
+ self._dialog.close()
p3lib/ssh.py CHANGED
@@ -700,7 +700,7 @@ class SSHTunnelManager(object):
700
700
  forwardingServer = ForwardingServer(('', serverPort), SubHander)
701
701
  self._forwardingServerList.append(forwardingServer)
702
702
  newThread = threading.Thread(target=forwardingServer.serve_forever)
703
- newThread.setDaemon(True)
703
+ newThread.daemon = True
704
704
  newThread.start()
705
705
 
706
706
  def stopFwdSSHTunnel(self, serverPort):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: p3lib
3
- Version: 1.1.73
3
+ Version: 1.1.75
4
4
  Summary: A group of python modules for networking, plotting data, config storage, automating boot scripts, ssh access and user input output.
5
5
  Home-page: https://github.com/pjaos/p3lib
6
6
  Author: Paul Austen
@@ -1,7 +1,7 @@
1
1
  p3lib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  p3lib/ate.py,sha256=_BiqMUYNAlp4O8MkP_PAUe62Bzd8dzV4Ipv62OFS6Ok,4759
3
3
  p3lib/bokeh_auth.py,sha256=XqdCLOalHL3dkyrMPqQoVQBSziVnqlfjB9YAWhZUd_U,14769
4
- p3lib/bokeh_gui.py,sha256=MNapLctSncZ9YyKGzNcUICMwpI5_h7a7bH3QdMd2UxI,40393
4
+ p3lib/bokeh_gui.py,sha256=55sajP_x9O1lE0uP3w3-T5f2oMzk7jSolLqxlEdLeLg,40245
5
5
  p3lib/boot_manager.py,sha256=-kbfYbFpO-ktKv_heUgYdvvIQrntfCQ7pBcPWqS3C0s,12551
6
6
  p3lib/conduit.py,sha256=jPkjdtyCx2I6SFqcEo8y2g7rgnZ-jNY7oCuYIETzT5Q,6046
7
7
  p3lib/database_if.py,sha256=XKu1w3zftGbj4Rh54wrWJnoCtqHkhCzJUPN2S70XIKg,11915
@@ -10,12 +10,13 @@ p3lib/json_networking.py,sha256=6u4s1SmypjTYPnSxHP712OgQ3ZJaxOqIkgHQ1J7Qews,9738
10
10
  p3lib/mqtt_rpc.py,sha256=6LmFA1kR4HSJs9eWbOJORRHNY01L_lHWjvtE2fmY8P8,10511
11
11
  p3lib/netif.py,sha256=3QV5OGdHhELIf4MBj6mx5MNCtVeZ7JXoNEkeu4KzCaE,9796
12
12
  p3lib/netplotly.py,sha256=PMDx-w1jtRVW6Od5u_kuKbBxNpTS_Y88mMF60puMxLM,9363
13
+ p3lib/ngt.py,sha256=FWiRcVrWR0tTfscz6gWDHcJXam-00OezyQNjTaFXPh4,36635
13
14
  p3lib/pconfig.py,sha256=_ri9w3aauHXZp8u2YLYHBVroFR_iCqaTCwj_MRa3rHo,30153
14
- p3lib/ssh.py,sha256=JiO9MY98zf3uqbonygJi_F9X0eAAgHAINCTeBuYXpCY,39511
15
+ p3lib/ssh.py,sha256=klqQ9YuqU8GwPVPHrAJeEs5PI5hgoYiXflq2kIGG3as,39509
15
16
  p3lib/table_plot.py,sha256=dRPZ9rFpwE9Dpyqrvbm5t2spVaPSHrx_B7KvfWVND3g,32166
16
17
  p3lib/uio.py,sha256=hMarPnYXnqVF23HUIeDfzREo7TMdBjrupXMY_ffuCbI,23133
17
- p3lib-1.1.73.dist-info/LICENSE,sha256=igqTy5u0kVWM1n-NUZMvAlinY6lVjAXKoag0okkS8V8,1067
18
- p3lib-1.1.73.dist-info/METADATA,sha256=W0G4eAqz_wdHdICMxWEQQoGsvonKdTIvZ5k8VuP7J6U,918
19
- p3lib-1.1.73.dist-info/WHEEL,sha256=cVxcB9AmuTcXqmwrtPhNK88dr7IR_b6qagTj0UvIEbY,91
20
- p3lib-1.1.73.dist-info/top_level.txt,sha256=SDCpXYh-19yCFp4Z8ZK4B-3J4NvTCJElZ42NPgcR6-U,6
21
- p3lib-1.1.73.dist-info/RECORD,,
18
+ p3lib-1.1.75.dist-info/LICENSE,sha256=igqTy5u0kVWM1n-NUZMvAlinY6lVjAXKoag0okkS8V8,1067
19
+ p3lib-1.1.75.dist-info/METADATA,sha256=aBudwqSxnGiP-3sDRkKLdiv2ZpNErMlfcbthAg3bmb8,918
20
+ p3lib-1.1.75.dist-info/WHEEL,sha256=cVxcB9AmuTcXqmwrtPhNK88dr7IR_b6qagTj0UvIEbY,91
21
+ p3lib-1.1.75.dist-info/top_level.txt,sha256=SDCpXYh-19yCFp4Z8ZK4B-3J4NvTCJElZ42NPgcR6-U,6
22
+ p3lib-1.1.75.dist-info/RECORD,,
File without changes