p3lib 1.1.108__py2.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/__init__.py +0 -0
- p3lib/ate.py +108 -0
- p3lib/bokeh_auth.py +363 -0
- p3lib/bokeh_gui.py +845 -0
- p3lib/boot_manager.py +420 -0
- p3lib/conduit.py +145 -0
- p3lib/database_if.py +289 -0
- p3lib/file_io.py +154 -0
- p3lib/gnome_desktop_app.py +146 -0
- p3lib/helper.py +420 -0
- p3lib/json_networking.py +239 -0
- p3lib/login.html +98 -0
- p3lib/mqtt_rpc.py +240 -0
- p3lib/netif.py +226 -0
- p3lib/netplotly.py +223 -0
- p3lib/ngt.py +841 -0
- p3lib/pconfig.py +874 -0
- p3lib/ssh.py +935 -0
- p3lib/table_plot.py +675 -0
- p3lib/uio.py +574 -0
- p3lib-1.1.108.dist-info/LICENSE +21 -0
- p3lib-1.1.108.dist-info/METADATA +34 -0
- p3lib-1.1.108.dist-info/RECORD +24 -0
- p3lib-1.1.108.dist-info/WHEEL +4 -0
p3lib/pconfig.py
ADDED
@@ -0,0 +1,874 @@
|
|
1
|
+
#!/bin/sh/env python3
|
2
|
+
|
3
|
+
import os
|
4
|
+
import base64
|
5
|
+
import datetime
|
6
|
+
import sys
|
7
|
+
import json
|
8
|
+
import copy
|
9
|
+
import platform
|
10
|
+
|
11
|
+
from shutil import copyfile
|
12
|
+
|
13
|
+
from os.path import join, expanduser, getmtime, basename, isdir, isfile
|
14
|
+
from .helper import getHomePath
|
15
|
+
|
16
|
+
class ConfigManager(object):
|
17
|
+
"""@brief Responsible for storing and loading configuration.
|
18
|
+
Also responsible for providing methods that allow users to enter
|
19
|
+
configuration."""
|
20
|
+
|
21
|
+
UNSET_VALUE = "UNSET"
|
22
|
+
DECIMAL_INT_NUMBER_TYPE = 0
|
23
|
+
HEXADECIMAL_INT_NUMBER_TYPE = 1
|
24
|
+
FLOAT_NUMBER_TYPE = 2
|
25
|
+
SSH_FOLDER = ".ssh"
|
26
|
+
PRIVATE_SSH_KEY_FILENAME = "id_rsa"
|
27
|
+
|
28
|
+
@staticmethod
|
29
|
+
def GetString(uio, prompt, previousValue, allowEmpty=True):
|
30
|
+
"""@brief Get a string from the the user.
|
31
|
+
@param uio A UIO (User Inpu Output) instance.
|
32
|
+
@param prompt The prompt presented to the user in order to enter
|
33
|
+
the float value.
|
34
|
+
@param previousValue The previous value of the string.
|
35
|
+
@param allowEmpty If True then allow the string to be empty."""
|
36
|
+
_prompt = prompt
|
37
|
+
try:
|
38
|
+
prompt = "%s (%s)" % (prompt, previousValue)
|
39
|
+
except ValueError:
|
40
|
+
prompt = "%s" % (prompt)
|
41
|
+
|
42
|
+
while True:
|
43
|
+
|
44
|
+
response = uio.getInput("%s" % (prompt))
|
45
|
+
|
46
|
+
if len(response) == 0:
|
47
|
+
|
48
|
+
if allowEmpty:
|
49
|
+
|
50
|
+
if len(previousValue) > 0:
|
51
|
+
booleanResponse = uio.getInput("Do you wish to enter the previous value '%s' y/n: " % (previousValue) )
|
52
|
+
booleanResponse=booleanResponse.lower()
|
53
|
+
if booleanResponse == 'y':
|
54
|
+
response = previousValue
|
55
|
+
break
|
56
|
+
|
57
|
+
booleanResponse = uio.getInput("Do you wish to clear '%s' y/n: " % (_prompt) )
|
58
|
+
booleanResponse=booleanResponse.lower()
|
59
|
+
if booleanResponse == 'y':
|
60
|
+
break
|
61
|
+
else:
|
62
|
+
uio.info("A value is required. Please enter a value.")
|
63
|
+
|
64
|
+
else:
|
65
|
+
|
66
|
+
booleanResponse = uio.getInput("Do you wish to enter the previous value of %s y/n: " % (previousValue) )
|
67
|
+
booleanResponse=booleanResponse.lower()
|
68
|
+
if booleanResponse == 'y':
|
69
|
+
response = previousValue
|
70
|
+
break
|
71
|
+
else:
|
72
|
+
break
|
73
|
+
|
74
|
+
return response
|
75
|
+
|
76
|
+
@staticmethod
|
77
|
+
def IsValidDate(date):
|
78
|
+
"""@brief determine if the string is a valid date.
|
79
|
+
@param date in the form DAY/MONTH/YEAR (02:01:2018)"""
|
80
|
+
validDate = False
|
81
|
+
if len(date) >= 8:
|
82
|
+
elems = date.split("/")
|
83
|
+
try:
|
84
|
+
day = int(elems[0])
|
85
|
+
month = int(elems[1])
|
86
|
+
year = int(elems[2])
|
87
|
+
datetime.date(year, month, day)
|
88
|
+
validDate = True
|
89
|
+
except ValueError:
|
90
|
+
pass
|
91
|
+
return validDate
|
92
|
+
|
93
|
+
@staticmethod
|
94
|
+
def GetDate(uio, prompt, previousValue, allowEmpty=True):
|
95
|
+
"""@brief Input a date in the format DAY:MONTH:YEAR"""
|
96
|
+
if not ConfigManager.IsValidDate(previousValue):
|
97
|
+
today = datetime.date.today()
|
98
|
+
previousValue = today.strftime("%d/%m/%Y")
|
99
|
+
while True:
|
100
|
+
newValue = ConfigManager.GetString(uio, prompt, previousValue, allowEmpty=allowEmpty)
|
101
|
+
if ConfigManager.IsValidDate(newValue):
|
102
|
+
return newValue
|
103
|
+
|
104
|
+
@staticmethod
|
105
|
+
def IsValidTime(theTime):
|
106
|
+
"""@brief determine if the string is a valid time.
|
107
|
+
@param theTime in the form HOUR:MINUTE:SECOND (12:56:01)"""
|
108
|
+
validTime = False
|
109
|
+
if len(theTime) >= 5:
|
110
|
+
elems = theTime.split(":")
|
111
|
+
try:
|
112
|
+
hour = int(elems[0])
|
113
|
+
minute = int(elems[1])
|
114
|
+
second = int(elems[2])
|
115
|
+
datetime.time(hour, minute, second)
|
116
|
+
validTime = True
|
117
|
+
except ValueError:
|
118
|
+
pass
|
119
|
+
return validTime
|
120
|
+
|
121
|
+
@staticmethod
|
122
|
+
def GetTime(uio, prompt, previousValue, allowEmpty=True):
|
123
|
+
"""@brief Input a time in the format HOUR:MINUTE:SECOND"""
|
124
|
+
if not ConfigManager.IsValidTime(previousValue):
|
125
|
+
today = datetime.datetime.now()
|
126
|
+
previousValue = today.strftime("%H:%M:%S")
|
127
|
+
while True:
|
128
|
+
newValue = ConfigManager.GetString(uio, prompt, previousValue, allowEmpty=allowEmpty)
|
129
|
+
if ConfigManager.IsValidTime(newValue):
|
130
|
+
return newValue
|
131
|
+
|
132
|
+
@staticmethod
|
133
|
+
def _GetNumber(uio, prompt, previousValue=UNSET_VALUE, minValue=UNSET_VALUE, maxValue=UNSET_VALUE, numberType=FLOAT_NUMBER_TYPE, radix=10):
|
134
|
+
"""@brief Get float repsonse from user.
|
135
|
+
@param uio A UIO (User Inpu Output) instance.
|
136
|
+
@param prompt The prompt presented to the user in order to enter
|
137
|
+
the float value.
|
138
|
+
@param previousValue The previous number value.
|
139
|
+
@param minValue The minimum acceptable value.
|
140
|
+
@param maxValue The maximum acceptable value.
|
141
|
+
@param numberType The type of number."""
|
142
|
+
|
143
|
+
if numberType == ConfigManager.DECIMAL_INT_NUMBER_TYPE:
|
144
|
+
|
145
|
+
radix=10
|
146
|
+
|
147
|
+
elif numberType == ConfigManager.HEXADECIMAL_INT_NUMBER_TYPE:
|
148
|
+
|
149
|
+
radix = 16
|
150
|
+
|
151
|
+
while True:
|
152
|
+
|
153
|
+
response = ConfigManager.GetString(uio, prompt, previousValue, allowEmpty=False)
|
154
|
+
|
155
|
+
try:
|
156
|
+
|
157
|
+
if numberType == ConfigManager.FLOAT_NUMBER_TYPE:
|
158
|
+
|
159
|
+
value = float(response)
|
160
|
+
|
161
|
+
else:
|
162
|
+
|
163
|
+
value = int(str(response), radix)
|
164
|
+
|
165
|
+
if minValue != ConfigManager.UNSET_VALUE and value < minValue:
|
166
|
+
|
167
|
+
if radix == 16:
|
168
|
+
minValueStr = "0x%x" % (minValue)
|
169
|
+
else:
|
170
|
+
minValueStr = "%d" % (minValue)
|
171
|
+
|
172
|
+
uio.info("%s is less than the min value of %s." % (response, minValueStr) )
|
173
|
+
continue
|
174
|
+
|
175
|
+
if maxValue != ConfigManager.UNSET_VALUE and value > maxValue:
|
176
|
+
|
177
|
+
if radix == 16:
|
178
|
+
maxValueStr = "0x%x" % (maxValue)
|
179
|
+
else:
|
180
|
+
maxValueStr = "%d" % (maxValue)
|
181
|
+
|
182
|
+
uio.info("%s is greater than the max value of %s." % (response, maxValueStr) )
|
183
|
+
continue
|
184
|
+
|
185
|
+
return value
|
186
|
+
|
187
|
+
except ValueError:
|
188
|
+
|
189
|
+
if numberType == ConfigManager.FLOAT_NUMBER_TYPE:
|
190
|
+
|
191
|
+
uio.info("%s is not a valid number." % (response) )
|
192
|
+
|
193
|
+
elif numberType == ConfigManager.DECIMAL_INT_NUMBER_TYPE:
|
194
|
+
|
195
|
+
uio.info("%s is not a valid integer value." % (response) )
|
196
|
+
|
197
|
+
elif numberType == ConfigManager.HEXADECIMAL_INT_NUMBER_TYPE:
|
198
|
+
|
199
|
+
uio.info("%s is not a valid hexadecimal value." % (response) )
|
200
|
+
|
201
|
+
@staticmethod
|
202
|
+
def GetDecInt(uio, prompt, previousValue=UNSET_VALUE, minValue=UNSET_VALUE, maxValue=UNSET_VALUE):
|
203
|
+
"""@brief Get a decimal integer number from the user.
|
204
|
+
@param uio A UIO (User Inpu Output) instance.
|
205
|
+
@param prompt The prompt presented to the user in order to enter
|
206
|
+
the float value.
|
207
|
+
@param previousValue The previous number value.
|
208
|
+
@param minValue The minimum acceptable value.
|
209
|
+
@param maxValue The maximum acceptable value."""
|
210
|
+
|
211
|
+
return ConfigManager._GetNumber(uio, prompt, previousValue=previousValue, minValue=minValue, maxValue=maxValue, numberType=ConfigManager.DECIMAL_INT_NUMBER_TYPE)
|
212
|
+
|
213
|
+
@staticmethod
|
214
|
+
def GetHexInt(uio, prompt, previousValue=UNSET_VALUE, minValue=UNSET_VALUE, maxValue=UNSET_VALUE):
|
215
|
+
"""@brief Get a decimal integer number from the user.
|
216
|
+
@param uio A UIO (User Inpu Output) instance.
|
217
|
+
@param prompt The prompt presented to the user in order to enter
|
218
|
+
the float value.
|
219
|
+
@param previousValue The previous number value.
|
220
|
+
@param minValue The minimum acceptable value.
|
221
|
+
@param maxValue The maximum acceptable value."""
|
222
|
+
|
223
|
+
return ConfigManager._GetNumber(uio, prompt, previousValue=previousValue, minValue=minValue, maxValue=maxValue, numberType=ConfigManager.HEXADECIMAL_INT_NUMBER_TYPE)
|
224
|
+
|
225
|
+
@staticmethod
|
226
|
+
def GetFloat(uio, prompt, previousValue=UNSET_VALUE, minValue=UNSET_VALUE, maxValue=UNSET_VALUE):
|
227
|
+
"""@brief Get a float number from the user.
|
228
|
+
@param uio A UIO (User Inpu Output) instance.
|
229
|
+
@param prompt The prompt presented to the user in order to enter
|
230
|
+
the float value.
|
231
|
+
@param previousValue The previous number value.
|
232
|
+
@param minValue The minimum acceptable value.
|
233
|
+
@param maxValue The maximum acceptable value."""
|
234
|
+
|
235
|
+
return ConfigManager._GetNumber(uio, prompt, previousValue, minValue=minValue, maxValue=maxValue, numberType=ConfigManager.FLOAT_NUMBER_TYPE)
|
236
|
+
|
237
|
+
@staticmethod
|
238
|
+
def GetBool(uio, prompt, previousValue=True):
|
239
|
+
"""@brief Input a boolean value.
|
240
|
+
@param uio A UIO (User Input Output) instance.
|
241
|
+
@param prompt The prompt presented to the user in order to enter
|
242
|
+
the float value.
|
243
|
+
@param previousValue The previous True or False value."""
|
244
|
+
_prompt = "%s y/n" % (prompt)
|
245
|
+
|
246
|
+
if previousValue:
|
247
|
+
prevValue='y'
|
248
|
+
else:
|
249
|
+
prevValue='n'
|
250
|
+
|
251
|
+
while True:
|
252
|
+
value = ConfigManager.GetString(uio, _prompt, prevValue)
|
253
|
+
value=value.lower()
|
254
|
+
if value == 'n':
|
255
|
+
return False
|
256
|
+
elif value == 'y':
|
257
|
+
return True
|
258
|
+
|
259
|
+
@staticmethod
|
260
|
+
def GetPrivateKeyFile():
|
261
|
+
"""@brief Get the private key file."""
|
262
|
+
homePath = getHomePath()
|
263
|
+
folder = os.path.join(homePath, ConfigManager.SSH_FOLDER)
|
264
|
+
priKeyFile = os.path.join(folder, ConfigManager.PRIVATE_SSH_KEY_FILENAME)
|
265
|
+
if not os.path.isfile(priKeyFile):
|
266
|
+
raise Exception("%s file not found" % (priKeyFile) )
|
267
|
+
return priKeyFile
|
268
|
+
|
269
|
+
@staticmethod
|
270
|
+
def GetPrivateSSHKeyFileContents():
|
271
|
+
"""@brief Get the private ssh key file.
|
272
|
+
@return The key file contents"""
|
273
|
+
privateRSAKeyFile = ConfigManager.GetPrivateKeyFile()
|
274
|
+
fd = open(privateRSAKeyFile, 'r')
|
275
|
+
fileContents = fd.read()
|
276
|
+
fd.close()
|
277
|
+
return fileContents
|
278
|
+
|
279
|
+
@staticmethod
|
280
|
+
def GetCrypter():
|
281
|
+
"""@brief Get the object responsible for encrypting and decrypting strings."""
|
282
|
+
# Only import the cryptography module if it is used so as to avoid the
|
283
|
+
# necessity of having the cryptography module to use the pconfig module.
|
284
|
+
from cryptography.fernet import Fernet
|
285
|
+
keyString = ConfigManager.GetPrivateSSHKeyFileContents()
|
286
|
+
keyString = keyString[60:92]
|
287
|
+
priKeyBytes = bytes(keyString, 'utf-8')
|
288
|
+
priKeyBytesB64 = base64.b64encode(priKeyBytes)
|
289
|
+
return Fernet(priKeyBytesB64)
|
290
|
+
|
291
|
+
@staticmethod
|
292
|
+
def Encrypt(inputString):
|
293
|
+
"""@brief Encrypt the string.
|
294
|
+
@param inputString The string to encrypt.
|
295
|
+
@return The encrypted string"""
|
296
|
+
crypter = ConfigManager.GetCrypter()
|
297
|
+
token = crypter.encrypt(bytes(inputString, 'utf-8'))
|
298
|
+
return token.decode('utf-8')
|
299
|
+
|
300
|
+
@staticmethod
|
301
|
+
def Decrypt(inputString):
|
302
|
+
"""@brief Decrypt the string.
|
303
|
+
@param inputString The string to decrypt.
|
304
|
+
return The decrypted string"""
|
305
|
+
crypter = ConfigManager.GetCrypter()
|
306
|
+
token = inputString.encode('utf-8')
|
307
|
+
decryptedBytes = crypter.decrypt(token)
|
308
|
+
return decryptedBytes.decode('utf-8')
|
309
|
+
|
310
|
+
@staticmethod
|
311
|
+
def GetConfigFile(cfgFilename, addDotToFilename=True, cfgPath=None):
|
312
|
+
"""@brief Get the config file."""
|
313
|
+
|
314
|
+
if not cfgFilename:
|
315
|
+
raise Exception("No config filename defined.")
|
316
|
+
|
317
|
+
if addDotToFilename:
|
318
|
+
if not cfgFilename.startswith("."):
|
319
|
+
|
320
|
+
cfgFilename=".%s" % (cfgFilename)
|
321
|
+
|
322
|
+
#The the config path has been set then use it
|
323
|
+
if cfgPath:
|
324
|
+
configPath = cfgPath
|
325
|
+
|
326
|
+
else:
|
327
|
+
# If the root user on a Linux system
|
328
|
+
if platform.system() == 'Linux' and os.geteuid() == 0:
|
329
|
+
# This should be the root users config folder.
|
330
|
+
configPath="/root"
|
331
|
+
|
332
|
+
else:
|
333
|
+
configPath=""
|
334
|
+
#If an absolute path is set for the config file then don't try to
|
335
|
+
#put the file in the users home dir
|
336
|
+
if not cfgFilename.startswith("/"):
|
337
|
+
configPath = expanduser("~")
|
338
|
+
configPath = configPath.strip()
|
339
|
+
|
340
|
+
return join( configPath, cfgFilename )
|
341
|
+
|
342
|
+
def __init__(self,
|
343
|
+
uio,
|
344
|
+
cfgFilename,
|
345
|
+
defaultConfig,
|
346
|
+
addDotToFilename=True,
|
347
|
+
encrypt=False,
|
348
|
+
cfgPath=None,
|
349
|
+
stripUnknownKeys=True,
|
350
|
+
addNewKeys=True):
|
351
|
+
"""@brief Constructor
|
352
|
+
@param uio A UIO (User Input Output) instance. May be set to None if no user messages are required.
|
353
|
+
@param cfgFilename The name of the config file. If this is None then the default config filename is used.
|
354
|
+
@param defaultConfig A default config instance containing all the default key-value pairs.
|
355
|
+
@param addDotToFilename If True (default) then a . is added to the start of the filename. This hides the file in normal cases.
|
356
|
+
@param encrypt If True then data will be encrypted in the saved files.
|
357
|
+
The encryption uses part of the the local SSH RSA private key.
|
358
|
+
This is not secure but assuming the private key has not been compromised it's
|
359
|
+
probably the best we can do. Therefore if encrypt is set True then the
|
360
|
+
an ssh key must be present in the ~/.ssh folder named id_rsa.
|
361
|
+
@param cfgPath The config path when the config file will be stored. By default this is unset and the
|
362
|
+
current users home folder is the location of the config file.
|
363
|
+
@param stripUnknownKeys If True then keys in the dict but not in the default config dict are stripped from the config.
|
364
|
+
@param addNewKeys If keys are found in the default config that are not in the config dict, add them."""
|
365
|
+
self._uio = uio
|
366
|
+
self._cfgFilename = cfgFilename
|
367
|
+
self._defaultConfig = defaultConfig
|
368
|
+
self._addDotToFilename = addDotToFilename
|
369
|
+
self._encrypt = encrypt
|
370
|
+
self._cfgPath = cfgPath
|
371
|
+
self._stripUnknownKeys = stripUnknownKeys
|
372
|
+
self._addNewKeys = addNewKeys
|
373
|
+
self._configDict = {}
|
374
|
+
|
375
|
+
# If the user passed None in as the cfg filename then generate the default config file.
|
376
|
+
if self._cfgFilename is None:
|
377
|
+
self._cfgFilename = ConfigManager.GetDefaultConfigFilename()
|
378
|
+
|
379
|
+
self._cfgFile = self._getConfigFile()
|
380
|
+
self._modifiedTime = self._getModifiedTime()
|
381
|
+
|
382
|
+
def _info(self, msg):
|
383
|
+
"""@brief Display an info message if we have a UIO instance.
|
384
|
+
@param msg The message to be displayed."""
|
385
|
+
if self._uio:
|
386
|
+
self._uio.info(msg)
|
387
|
+
|
388
|
+
def _debug(self, msg):
|
389
|
+
"""@brief Display a debug message if we have a UIO instance.
|
390
|
+
@param msg The message to be displayed."""
|
391
|
+
if self._uio:
|
392
|
+
self._uio.debug(msg)
|
393
|
+
|
394
|
+
def _getConfigFile(self):
|
395
|
+
"""@brief Get the config file."""
|
396
|
+
return ConfigManager.GetConfigFile(self._cfgFilename, addDotToFilename=self._addDotToFilename, cfgPath=self._cfgPath)
|
397
|
+
|
398
|
+
def addAttr(self, key, value):
|
399
|
+
"""@brief Add an attribute value to the config.
|
400
|
+
@param key The key to store the value against.
|
401
|
+
@param value The value to be stored."""
|
402
|
+
self._configDict[key]=value
|
403
|
+
|
404
|
+
def getAttrList(self):
|
405
|
+
"""@return A list of attribute names that are stored."""
|
406
|
+
return self._configDict.keys()
|
407
|
+
|
408
|
+
def getAttr(self, key, allowModify=True):
|
409
|
+
"""@brief Get an attribute value.
|
410
|
+
@param key The key for the value we're after.
|
411
|
+
@param allowModify If True and the configuration has been modified
|
412
|
+
since the last read by the caller then the config will be reloaded."""
|
413
|
+
|
414
|
+
#If the config file has been modified then read the config to get the updated state.
|
415
|
+
if allowModify and self.isModified():
|
416
|
+
self.load(showLoadedMsg=False)
|
417
|
+
self.updateModifiedTime()
|
418
|
+
|
419
|
+
return self._configDict[key]
|
420
|
+
|
421
|
+
def _saveDict(self, dictToSave):
|
422
|
+
"""@brief Save dict to a file.
|
423
|
+
@param dictToSave The dictionary to save."""
|
424
|
+
|
425
|
+
try:
|
426
|
+
|
427
|
+
if self._encrypt:
|
428
|
+
stringToSave = json.dumps(dictToSave, sort_keys=True)
|
429
|
+
stringToSave = ConfigManager.Encrypt(stringToSave)
|
430
|
+
fd = open(self._cfgFile, "w")
|
431
|
+
fd.write(stringToSave)
|
432
|
+
fd.close()
|
433
|
+
else:
|
434
|
+
json.dump(dictToSave, open(self._cfgFile, "w"), sort_keys=True)
|
435
|
+
|
436
|
+
self._info("Saved config to %s" % (self._cfgFile) )
|
437
|
+
|
438
|
+
except IOError as i:
|
439
|
+
raise IOError(i.errno, 'Failed to write file \'%s\': %s'
|
440
|
+
% (self._cfgFile, i.strerror), i.filename).with_traceback(i)
|
441
|
+
|
442
|
+
def store(self, copyToRoot=False):
|
443
|
+
"""@brief Store the config to the config file.
|
444
|
+
@param copyToRoot If True copy the config to the root user (Linux only)
|
445
|
+
if not running with root user config path. If True
|
446
|
+
on non Linux system config will only be saved in
|
447
|
+
the users home path. Default = False."""
|
448
|
+
self._saveDict(self._configDict)
|
449
|
+
|
450
|
+
self.updateModifiedTime()
|
451
|
+
|
452
|
+
if copyToRoot and not self._cfgFile.startswith("/root/") and isdir("/root"):
|
453
|
+
fileN = basename(self._cfgFile)
|
454
|
+
rootCfgFile = join("/root", fileN)
|
455
|
+
copyfile(self._cfgFile, rootCfgFile)
|
456
|
+
self._info("Also updated service list in %s" % (rootCfgFile))
|
457
|
+
|
458
|
+
def _getDict(self):
|
459
|
+
"""@brief Load dict from file
|
460
|
+
@return Return the dict loaded from the file."""
|
461
|
+
dictLoaded = {}
|
462
|
+
|
463
|
+
if self._encrypt:
|
464
|
+
fd = open(self._cfgFile, "r")
|
465
|
+
encryptedData = fd.read()
|
466
|
+
fd.close()
|
467
|
+
decryptedString = ConfigManager.Decrypt(encryptedData)
|
468
|
+
dictLoaded = json.loads(decryptedString)
|
469
|
+
else:
|
470
|
+
|
471
|
+
fp = open(self._cfgFile, 'r')
|
472
|
+
dictLoaded = json.load(fp)
|
473
|
+
fp.close()
|
474
|
+
|
475
|
+
return dictLoaded
|
476
|
+
|
477
|
+
def load(self, showLoadedMsg=True):
|
478
|
+
"""@brief Load the config.
|
479
|
+
@param showLoadedMsg If True load messages are displayed."""
|
480
|
+
|
481
|
+
if not isfile(self._cfgFile):
|
482
|
+
|
483
|
+
self._configDict = self._defaultConfig
|
484
|
+
self.store()
|
485
|
+
|
486
|
+
else:
|
487
|
+
#Get the config from the stored config file.
|
488
|
+
loadedConfig = self._getDict()
|
489
|
+
#Get list of all the keys from the config file loaded.
|
490
|
+
loadedConfigKeys = list(loadedConfig.keys())
|
491
|
+
|
492
|
+
#Get the default config
|
493
|
+
defaultConfig = copy.deepcopy( self._defaultConfig )
|
494
|
+
defaultConfigKeys = list(defaultConfig.keys())
|
495
|
+
|
496
|
+
# Config parameters may be added or dropped over time. We use the default config to
|
497
|
+
# check for parameters that should be added/removed.
|
498
|
+
|
499
|
+
if self._addNewKeys:
|
500
|
+
# Add any missing keys to the loaded config from the default config.
|
501
|
+
for defaultKey in defaultConfigKeys:
|
502
|
+
if defaultKey not in loadedConfigKeys:
|
503
|
+
loadedConfig[defaultKey] = self._defaultConfig[defaultKey]
|
504
|
+
self._debug("----------> DEFAULT VALUE ADDED: {} = {}".format(defaultKey, loadedConfig[defaultKey]))
|
505
|
+
|
506
|
+
if self._stripUnknownKeys:
|
507
|
+
# If some keys have been dropped from the config, remove them.
|
508
|
+
for loadedConfigKey in loadedConfigKeys:
|
509
|
+
if loadedConfigKey not in defaultConfigKeys:
|
510
|
+
self._debug("----------> DROPPED FROM CONFIG: {} = {}".format(loadedConfigKey, loadedConfig[loadedConfigKey]))
|
511
|
+
loadedConfig.pop(loadedConfigKey, None)
|
512
|
+
|
513
|
+
self._configDict = loadedConfig
|
514
|
+
self._info("Loaded config from %s" % (self._cfgFile) )
|
515
|
+
|
516
|
+
def inputFloat(self, key, prompt, minValue=UNSET_VALUE, maxValue=UNSET_VALUE):
|
517
|
+
"""@brief Input a float value into the config.
|
518
|
+
@key The key to store this value in the config.
|
519
|
+
@param prompt The prompt presented to the user in order to enter
|
520
|
+
the float value.
|
521
|
+
@param minValue The minimum acceptable value.
|
522
|
+
@param maxValue The maximum acceptable value."""
|
523
|
+
value = ConfigManager.GetFloat(self._uio, prompt, previousValue=self._getValue(key), minValue=minValue, maxValue=maxValue)
|
524
|
+
self.addAttr(key, value)
|
525
|
+
|
526
|
+
def inputDecInt(self, key, prompt, minValue=UNSET_VALUE, maxValue=UNSET_VALUE):
|
527
|
+
"""@brief Input a decimal integer value into the config.
|
528
|
+
@key The key to store this value in the config.
|
529
|
+
@param prompt The prompt presented to the user in order to enter
|
530
|
+
the float value.
|
531
|
+
@param minValue The minimum acceptable value.
|
532
|
+
@param maxValue The maximum acceptable value."""
|
533
|
+
value = ConfigManager.GetDecInt(self._uio, prompt, previousValue=self._getValue(key), minValue=minValue, maxValue=maxValue)
|
534
|
+
self.addAttr(key, value)
|
535
|
+
|
536
|
+
def inputHexInt(self, key, prompt, minValue=UNSET_VALUE, maxValue=UNSET_VALUE):
|
537
|
+
"""@brief Input a hexadecimal integer value into the config.
|
538
|
+
@key The key to store this value in the config.
|
539
|
+
@param prompt The prompt presented to the user in order to enter
|
540
|
+
the float value.
|
541
|
+
@param minValue The minimum acceptable value.
|
542
|
+
@param maxValue The maximum acceptable value."""
|
543
|
+
value = ConfigManager.GetHexInt(self._uio, prompt, previousValue=self._getValue(key), minValue=minValue, maxValue=maxValue)
|
544
|
+
self.addAttr(key, value)
|
545
|
+
|
546
|
+
def _getValue(self, key):
|
547
|
+
"""@brief Get the current value of the key.
|
548
|
+
@param key The key of the value we're after.
|
549
|
+
@return The value of the key or and empty string if key not found."""
|
550
|
+
value=""
|
551
|
+
|
552
|
+
if key in self._configDict:
|
553
|
+
value = self.getAttr(key)
|
554
|
+
|
555
|
+
return value
|
556
|
+
|
557
|
+
def inputStr(self, key, prompt, allowEmpty):
|
558
|
+
"""@brief Input a string value into the config.
|
559
|
+
@param key The key to store this value in the config.
|
560
|
+
@param prompt The prompt presented to the user in order to enter
|
561
|
+
the float value.
|
562
|
+
@param allowEmpty If True then allow the string to be empty."""
|
563
|
+
value = ConfigManager.GetString(self._uio, prompt, previousValue=self._getValue(key), allowEmpty=allowEmpty )
|
564
|
+
self.addAttr(key, value)
|
565
|
+
|
566
|
+
def inputDate(self, key, prompt, allowEmpty):
|
567
|
+
"""@brief Input a date into the config.
|
568
|
+
@param key The key to store this value in the config.
|
569
|
+
@param prompt The prompt presented to the user in order to enter
|
570
|
+
the float value.
|
571
|
+
@param allowEmpty If True then allow the string to be empty."""
|
572
|
+
value = ConfigManager.GetDate(self._uio, prompt, previousValue=self._getValue(key), allowEmpty=allowEmpty )
|
573
|
+
self.addAttr(key, value)
|
574
|
+
|
575
|
+
def inputTime(self, key, prompt, allowEmpty):
|
576
|
+
"""@brief Input a date into the config.
|
577
|
+
@param key The key to store this value in the config.
|
578
|
+
@param prompt The prompt presented to the user in order to enter
|
579
|
+
the float value.
|
580
|
+
@param allowEmpty If True then allow the string to be empty."""
|
581
|
+
value = ConfigManager.GetTime(self._uio, prompt, previousValue=self._getValue(key), allowEmpty=allowEmpty )
|
582
|
+
self.addAttr(key, value)
|
583
|
+
|
584
|
+
def inputBool(self, key, prompt):
|
585
|
+
"""@brief Input a boolean value.
|
586
|
+
@param key The key to store this value in the config.
|
587
|
+
@param prompt The prompt presented to the user in order to enter
|
588
|
+
the boolean (Yes/No) value."""
|
589
|
+
previousValue=self._getValue(key)
|
590
|
+
yes = self.getYesNo(prompt, previousValue=previousValue)
|
591
|
+
if yes:
|
592
|
+
value=True
|
593
|
+
else:
|
594
|
+
value=False
|
595
|
+
self.addAttr(key, value)
|
596
|
+
|
597
|
+
def getYesNo(self, prompt, previousValue=0):
|
598
|
+
"""@brief Input yes no response.
|
599
|
+
@param prompt The prompt presented to the user in order to enter
|
600
|
+
the float value.
|
601
|
+
@param allowEmpty If True then allow the string to be empty.
|
602
|
+
@return True if Yes, False if No."""
|
603
|
+
|
604
|
+
_prompt = "%s y/n" % (prompt)
|
605
|
+
|
606
|
+
if previousValue:
|
607
|
+
prevValue='y'
|
608
|
+
else:
|
609
|
+
prevValue='n'
|
610
|
+
|
611
|
+
while True:
|
612
|
+
value = ConfigManager.GetString(self._uio, _prompt, prevValue)
|
613
|
+
value=value.lower()
|
614
|
+
if value == 'n':
|
615
|
+
return False
|
616
|
+
elif value == 'y':
|
617
|
+
return True
|
618
|
+
|
619
|
+
def _getModifiedTime(self):
|
620
|
+
"""@brief Get the modified time of the config file."""
|
621
|
+
mtime = 0
|
622
|
+
try:
|
623
|
+
|
624
|
+
if isfile(self._cfgFile):
|
625
|
+
mtime = getmtime(self._cfgFile)
|
626
|
+
|
627
|
+
except OSError:
|
628
|
+
pass
|
629
|
+
|
630
|
+
return mtime
|
631
|
+
|
632
|
+
def updateModifiedTime(self):
|
633
|
+
"""@brief Update the modified time held as an attr in this nistance with the current modified time of the file."""
|
634
|
+
self._modifiedTime = self._getModifiedTime()
|
635
|
+
|
636
|
+
def isModified(self):
|
637
|
+
"""@Return True if the config file has been updated."""
|
638
|
+
mTime = self._getModifiedTime()
|
639
|
+
if mTime != self._modifiedTime:
|
640
|
+
return True
|
641
|
+
return False
|
642
|
+
|
643
|
+
def _getConfigAttDetails(self, key, configAttrDetailsDict):
|
644
|
+
"""@brief Get the configAttrDetails details instance from the dict.
|
645
|
+
@param key The in to the value in the configAttrDetailsDict
|
646
|
+
@param configAttrDetailsDict The dict containing attr meta data."""
|
647
|
+
if key in configAttrDetailsDict:
|
648
|
+
return configAttrDetailsDict[key]
|
649
|
+
raise Exception("getConfigAttDetails(): The %s dict has no key=%s" % ( str(configAttrDetailsDict), key) )
|
650
|
+
|
651
|
+
def edit(self, configAttrDetailsDict):
|
652
|
+
"""@brief A high level method to allow user to edit all config attributes.
|
653
|
+
@param configAttrDetailsDict A dict that holds configAttrDetails
|
654
|
+
instances, each of which provide data required for the
|
655
|
+
user to enter the configuration parameter."""
|
656
|
+
|
657
|
+
if len(self._configDict.keys()) == 0:
|
658
|
+
self.load(showLoadedMsg=True)
|
659
|
+
|
660
|
+
keyList = list(self._configDict.keys())
|
661
|
+
keyList.sort()
|
662
|
+
index = 0
|
663
|
+
while index < len(keyList):
|
664
|
+
|
665
|
+
try:
|
666
|
+
|
667
|
+
key = keyList[index]
|
668
|
+
|
669
|
+
configAttrDetails = self._getConfigAttDetails(key, configAttrDetailsDict)
|
670
|
+
|
671
|
+
if key.endswith("_FLOAT"):
|
672
|
+
|
673
|
+
self.inputFloat(key, configAttrDetails.prompt, minValue=configAttrDetails.minValue, maxValue=configAttrDetails.maxValue)
|
674
|
+
|
675
|
+
elif key.endswith("_INT"):
|
676
|
+
|
677
|
+
self.inputDecInt(key, configAttrDetails.prompt, minValue=configAttrDetails.minValue, maxValue=configAttrDetails.maxValue)
|
678
|
+
|
679
|
+
elif key.endswith("_HEXINT"):
|
680
|
+
|
681
|
+
self.inputHexInt(key, configAttrDetails.prompt, minValue=configAttrDetails.minValue, maxValue=configAttrDetails.maxValue)
|
682
|
+
|
683
|
+
elif key.endswith("_STR"):
|
684
|
+
|
685
|
+
self.inputStr(key, configAttrDetails.prompt, configAttrDetails.allowEmpty)
|
686
|
+
|
687
|
+
index = index + 1
|
688
|
+
|
689
|
+
except KeyboardInterrupt:
|
690
|
+
|
691
|
+
if index > 0:
|
692
|
+
index=index-1
|
693
|
+
print('\n')
|
694
|
+
|
695
|
+
else:
|
696
|
+
while True:
|
697
|
+
try:
|
698
|
+
print('\n')
|
699
|
+
if self.getYesNo("Quit ?"):
|
700
|
+
sys.exit(0)
|
701
|
+
break
|
702
|
+
except KeyboardInterrupt:
|
703
|
+
pass
|
704
|
+
|
705
|
+
self.store()
|
706
|
+
|
707
|
+
def getConfigDict(self):
|
708
|
+
"""@return the dict holding the configuration."""
|
709
|
+
return self._configDict
|
710
|
+
|
711
|
+
def setDefaultConfig(self):
|
712
|
+
"""@brief Set the default configuration by removing the existing configuration file and re loading."""
|
713
|
+
configFile = self._getConfigFile()
|
714
|
+
if isfile(configFile):
|
715
|
+
self._info(configFile)
|
716
|
+
deleteFile = self._uio.getBoolInput("Are you sure you wish to delete the above file [y]/[n]")
|
717
|
+
if deleteFile:
|
718
|
+
os.remove(configFile)
|
719
|
+
self._info("{} has been removed.".format(configFile))
|
720
|
+
self._info("The default configuration will be loaded next time..")
|
721
|
+
|
722
|
+
def configure(self, editConfigMethod, prompt="Enter 'E' to edit a parameter, or 'Q' to quit", editCharacters = 'E', quitCharacters= 'Q'):
|
723
|
+
"""@brief A helper method to edit the dictionary config.
|
724
|
+
@param editConfigMethod The method to call to edit configuration.
|
725
|
+
@return None"""
|
726
|
+
running=True
|
727
|
+
while running:
|
728
|
+
idKeyDict=self.show()
|
729
|
+
response = self._uio.getInput(prompt)
|
730
|
+
response=response.upper()
|
731
|
+
if response in editCharacters:
|
732
|
+
id = self._uio.getIntInput("Enter the ID of the parameter to change")
|
733
|
+
if id not in idKeyDict:
|
734
|
+
self._uio.error("Configuration ID {} is invalid.".format(id))
|
735
|
+
else:
|
736
|
+
key=idKeyDict[id]
|
737
|
+
editConfigMethod(key)
|
738
|
+
self.store()
|
739
|
+
|
740
|
+
elif response in quitCharacters:
|
741
|
+
running = False
|
742
|
+
|
743
|
+
def show(self):
|
744
|
+
"""@brief A helper method to show the dictionary config to the user.
|
745
|
+
@return A dictionary mapping the attribute ID's (keys) to dictionary keys (values)."""
|
746
|
+
maxKeyLen=10
|
747
|
+
for key in self._configDict:
|
748
|
+
if len(key) > maxKeyLen:
|
749
|
+
maxKeyLen = len(key)
|
750
|
+
self._info("ID PARAMETER"+" "*(maxKeyLen-8)+" VALUE")
|
751
|
+
id=1
|
752
|
+
idKeyDict = {}
|
753
|
+
for key in self._configDict:
|
754
|
+
idKeyDict[id]=key
|
755
|
+
self._info("{:<3d} {:<{}} {}".format(id, key, maxKeyLen+1, self._configDict[key]))
|
756
|
+
id=id+1
|
757
|
+
return idKeyDict
|
758
|
+
|
759
|
+
class ConfigAttrDetails(object):
|
760
|
+
"""@brief Responsible for holding config attribute meta data."""
|
761
|
+
|
762
|
+
def __init__(self, prompt, minValue=ConfigManager.UNSET_VALUE, maxValue=ConfigManager.UNSET_VALUE, allowEmpty=True):
|
763
|
+
self.prompt = prompt #Always used to as k user to enter attribute value.
|
764
|
+
self.minValue = minValue #Only used for numbers
|
765
|
+
self.maxValue = maxValue #Only used for numbers
|
766
|
+
self.allowEmpty = allowEmpty #Only used for strings
|
767
|
+
|
768
|
+
class DotConfigManager(ConfigManager):
|
769
|
+
"""@brief This extends the previous ConfigManager and stores config under the ~/.config folder
|
770
|
+
rather than in the home folder (~) using a filename prefixed with the . character.
|
771
|
+
The ~/.config folder holds either a single config file of the startup python filename.
|
772
|
+
The ./config folder can contain another python module folder which contains the config
|
773
|
+
file. E.G for this example app the ~/.config/examples/pconfig_example.cfg folder is used."""
|
774
|
+
DEFAULT_CONFIG = None # This must be overridden in a subclass to define the configuration parameters and values.
|
775
|
+
KEY_EDIT_ORDER_LIST = None
|
776
|
+
|
777
|
+
@staticmethod
|
778
|
+
def GetDefaultConfigFilename(filenameOverride=None):
|
779
|
+
"""@brief Get the default name of the config file for this app. This will be the program name
|
780
|
+
(file that started up initially) without the .py extension. A .cfg extension is added
|
781
|
+
and it will be found in the ~/.config folder.
|
782
|
+
@param filenameOverride The name for the config file in the .config folder. If left as None then the program name ise used
|
783
|
+
as the config filename."""
|
784
|
+
dotConfigFolder = '.config'
|
785
|
+
if platform.system() == 'Linux' and os.geteuid() == 0:
|
786
|
+
homePath = "/root"
|
787
|
+
else:
|
788
|
+
homePath = os.path.expanduser("~")
|
789
|
+
configFolder = os.path.join(homePath, dotConfigFolder)
|
790
|
+
|
791
|
+
if not os.path.isdir(homePath):
|
792
|
+
raise Exception(f"{homePath} HOME path does not exist.")
|
793
|
+
|
794
|
+
# Create the ~/.config folder if it does not exist
|
795
|
+
if not os.path.isdir(configFolder):
|
796
|
+
# Create the ~/.config folder
|
797
|
+
os.makedirs(configFolder)
|
798
|
+
|
799
|
+
if filenameOverride:
|
800
|
+
progName = filenameOverride
|
801
|
+
else:
|
802
|
+
progName = sys.argv[0]
|
803
|
+
if progName.endswith('.py'):
|
804
|
+
progName = progName[0:-3]
|
805
|
+
progName = os.path.basename(progName).strip()
|
806
|
+
|
807
|
+
# Note that we assume that addDotToFilename in the ConfigManager constructor is set True
|
808
|
+
# as this will prefix the filename with the . character.
|
809
|
+
if os.path.isdir(configFolder):
|
810
|
+
configFilename = os.path.join(".config", progName + ".cfg")
|
811
|
+
|
812
|
+
else:
|
813
|
+
raise Exception(f"Failed to create the {configFolder} folder.")
|
814
|
+
|
815
|
+
return configFilename
|
816
|
+
|
817
|
+
def __init__(self,
|
818
|
+
defaultConfig,
|
819
|
+
keyEditOrderList=None,
|
820
|
+
uio=None,
|
821
|
+
encrypt=False,
|
822
|
+
stripUnknownKeys=True,
|
823
|
+
addNewKeys=True,
|
824
|
+
filenameOverride=None):
|
825
|
+
"""@brief Constructor
|
826
|
+
@param defaultConfig A default config instance containing all the default key-value pairs.
|
827
|
+
@param keyEditOrderList A list of all the dict keys in the order that the caller wishes them to be displayed top the user.
|
828
|
+
@param uio A UIO (User Input Output) instance. May be set to None if no user messages are required.
|
829
|
+
@param encrypt If True then data will be encrypted in the saved files.
|
830
|
+
The encryption uses part of the the local SSH RSA private key.
|
831
|
+
This is not secure but assuming the private key has not been compromised it's
|
832
|
+
probably the best we can do.
|
833
|
+
!!! Therefore if encrypt is set True then the an ssh key must be present !!!
|
834
|
+
||| in the ~/.ssh folder named id_rsa. !!!
|
835
|
+
@param stripUnknownKeys If True then keys in the dict but not in the default config dict are stripped from the config.
|
836
|
+
@param addNewKeys If keys are found in the default config that are not in the config dict, add them.
|
837
|
+
@param filenameOverride The name for the config file in the .config folder. If left as None then the program name ise used
|
838
|
+
as the config filename."""
|
839
|
+
super().__init__(uio,
|
840
|
+
DotConfigManager.GetDefaultConfigFilename(filenameOverride),
|
841
|
+
defaultConfig,
|
842
|
+
encrypt=encrypt,
|
843
|
+
stripUnknownKeys=stripUnknownKeys,
|
844
|
+
addNewKeys=addNewKeys)
|
845
|
+
self._keyEditOrderList = keyEditOrderList
|
846
|
+
# Ensure the config file is present and loaded into the internal dict.
|
847
|
+
self.load()
|
848
|
+
|
849
|
+
def show(self):
|
850
|
+
"""@brief A helper method to show the dictionary config to the user.
|
851
|
+
@return A dictionary mapping the attribute ID's (keys) to dictionary keys (values)."""
|
852
|
+
maxKeyLen=10
|
853
|
+
# If the caller wants to present the parameters to the user in a partiular order
|
854
|
+
if self._keyEditOrderList:
|
855
|
+
keys = self._keyEditOrderList
|
856
|
+
defaultKeys = list(self._configDict.keys())
|
857
|
+
for defaultKey in defaultKeys:
|
858
|
+
if defaultKey not in keys:
|
859
|
+
raise Exception(f"BUG: DotConfigManager: {defaultKey} key is missing from the keyEditOrderList.")
|
860
|
+
# Present the parameters to the user in any order.
|
861
|
+
else:
|
862
|
+
keys = list(self._configDict.keys())
|
863
|
+
|
864
|
+
for key in keys:
|
865
|
+
if len(key) > maxKeyLen:
|
866
|
+
maxKeyLen = len(key)
|
867
|
+
self._info("ID PARAMETER"+" "*(maxKeyLen-8)+" VALUE")
|
868
|
+
id=1
|
869
|
+
idKeyDict = {}
|
870
|
+
for key in keys:
|
871
|
+
idKeyDict[id]=key
|
872
|
+
self._info("{:<3d} {:<{}} {}".format(id, key, maxKeyLen+1, self._configDict[key]))
|
873
|
+
id=id+1
|
874
|
+
return idKeyDict
|