p3lib 1.1.73__tar.gz → 1.1.74__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.
Files changed (31) hide show
  1. {p3lib-1.1.73 → p3lib-1.1.74}/PKG-INFO +1 -1
  2. {p3lib-1.1.73 → p3lib-1.1.74}/setup.cfg +1 -1
  3. {p3lib-1.1.73 → p3lib-1.1.74}/src/p3lib/bokeh_gui.py +2 -8
  4. p3lib-1.1.74/src/p3lib/ngt.py +717 -0
  5. {p3lib-1.1.73 → p3lib-1.1.74}/src/p3lib/ssh.py +1 -1
  6. {p3lib-1.1.73 → p3lib-1.1.74}/src/p3lib.egg-info/PKG-INFO +1 -1
  7. {p3lib-1.1.73 → p3lib-1.1.74}/src/p3lib.egg-info/SOURCES.txt +1 -0
  8. {p3lib-1.1.73 → p3lib-1.1.74}/tests/test_json_networking.py +1 -1
  9. {p3lib-1.1.73 → p3lib-1.1.74}/tests/test_netif.py +2 -2
  10. {p3lib-1.1.73 → p3lib-1.1.74}/tests/test_ssh.py +8 -1
  11. {p3lib-1.1.73 → p3lib-1.1.74}/LICENSE +0 -0
  12. {p3lib-1.1.73 → p3lib-1.1.74}/README.md +0 -0
  13. {p3lib-1.1.73 → p3lib-1.1.74}/pyproject.toml +0 -0
  14. {p3lib-1.1.73 → p3lib-1.1.74}/src/p3lib/__init__.py +0 -0
  15. {p3lib-1.1.73 → p3lib-1.1.74}/src/p3lib/ate.py +0 -0
  16. {p3lib-1.1.73 → p3lib-1.1.74}/src/p3lib/bokeh_auth.py +0 -0
  17. {p3lib-1.1.73 → p3lib-1.1.74}/src/p3lib/boot_manager.py +0 -0
  18. {p3lib-1.1.73 → p3lib-1.1.74}/src/p3lib/conduit.py +0 -0
  19. {p3lib-1.1.73 → p3lib-1.1.74}/src/p3lib/database_if.py +0 -0
  20. {p3lib-1.1.73 → p3lib-1.1.74}/src/p3lib/helper.py +0 -0
  21. {p3lib-1.1.73 → p3lib-1.1.74}/src/p3lib/json_networking.py +0 -0
  22. {p3lib-1.1.73 → p3lib-1.1.74}/src/p3lib/mqtt_rpc.py +0 -0
  23. {p3lib-1.1.73 → p3lib-1.1.74}/src/p3lib/netif.py +0 -0
  24. {p3lib-1.1.73 → p3lib-1.1.74}/src/p3lib/netplotly.py +0 -0
  25. {p3lib-1.1.73 → p3lib-1.1.74}/src/p3lib/pconfig.py +0 -0
  26. {p3lib-1.1.73 → p3lib-1.1.74}/src/p3lib/table_plot.py +0 -0
  27. {p3lib-1.1.73 → p3lib-1.1.74}/src/p3lib/uio.py +0 -0
  28. {p3lib-1.1.73 → p3lib-1.1.74}/src/p3lib.egg-info/dependency_links.txt +0 -0
  29. {p3lib-1.1.73 → p3lib-1.1.74}/src/p3lib.egg-info/requires.txt +0 -0
  30. {p3lib-1.1.73 → p3lib-1.1.74}/src/p3lib.egg-info/top_level.txt +0 -0
  31. {p3lib-1.1.73 → p3lib-1.1.74}/tests/test_conduit.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: p3lib
3
- Version: 1.1.73
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,6 +1,6 @@
1
1
  [metadata]
2
2
  name = p3lib
3
- version = 1.1.73
3
+ version = 1.1.74
4
4
  author = Paul Austen
5
5
  author_email = pausten.os@gmail.com
6
6
  description = A group of python modules for networking, plotting data, config storage, automating boot scripts, ssh access and user input output.
@@ -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
 
@@ -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
+
@@ -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.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
@@ -14,6 +14,7 @@ src/p3lib/json_networking.py
14
14
  src/p3lib/mqtt_rpc.py
15
15
  src/p3lib/netif.py
16
16
  src/p3lib/netplotly.py
17
+ src/p3lib/ngt.py
17
18
  src/p3lib/pconfig.py
18
19
  src/p3lib/ssh.py
19
20
  src/p3lib/table_plot.py
@@ -29,7 +29,7 @@ class TestClass:
29
29
  def setup_class(cls):
30
30
  server = JSONServer((TestClass.HOST, TestClass.PORT), ServerSessionHandler)
31
31
  serverThread = threading.Thread(target=server.serve_forever)
32
- serverThread.setDaemon(True)
32
+ serverThread.daemon = True
33
33
  serverThread.start()
34
34
 
35
35
  def test_connect(self):
@@ -4,8 +4,8 @@ from time import sleep
4
4
  from p3lib.netif import NetIF
5
5
 
6
6
  #These must be changed to use an interface on the local machine.
7
- NET_IF = "enp0s31f6"
8
- IP_ADDRESS = "192.168.0.9"
7
+ NET_IF = "enx0c37960803fc"
8
+ IP_ADDRESS = "192.168.0.10"
9
9
  NETMASK = "255.255.255.0"
10
10
 
11
11
  class TestClass:
@@ -3,11 +3,18 @@
3
3
  import unittest
4
4
  from time import sleep
5
5
 
6
+ # Supress the following warning
7
+ # CryptographyDeprecationWarning: TripleDES has been moved to cryptography.hazmat.decrepit.ciphers.algorithms.TripleDES and will be removed from this module in 48.0.0.
8
+ import warnings
9
+ from cryptography.utils import CryptographyDeprecationWarning
10
+ with warnings.catch_warnings(action="ignore", category=CryptographyDeprecationWarning):
11
+ import paramiko
12
+
6
13
  from p3lib.uio import UIO
7
14
  from p3lib.ssh import SSH, SSHTunnelManager
8
15
 
9
16
  #An ssh login on an ssh server must be available for these test to run.
10
- USERNAME="test"
17
+ USERNAME="pja"
11
18
  SERVER="localhost"
12
19
 
13
20
  class SSHTester(unittest.TestCase):
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