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/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