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