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/__init__.py
ADDED
File without changes
|
p3lib/ate.py
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
from time import time
|
2
|
+
|
3
|
+
# Responsible for providing functionality useful when implementing ATE solutions
|
4
|
+
# including Engineering hardware test and MFG test.
|
5
|
+
|
6
|
+
class TestCase(object):
|
7
|
+
"""@brief Responsible for holding the details of a single test case."""
|
8
|
+
|
9
|
+
def __init__(self, testCaseNumber, testCaseDescription, testMethod):
|
10
|
+
"""@brief Constructor for a single test case
|
11
|
+
@param testCaseNumber The number of the test case. This must be an integer value.
|
12
|
+
@param slTestCaseDescription The test case description as a single line of text.
|
13
|
+
@param testMethod The method to call to perform the test case."""
|
14
|
+
if not isinstance(testCaseNumber, int):
|
15
|
+
raise Exception(f"{testCaseNumber} (test case number) must be an integer value.")
|
16
|
+
|
17
|
+
elems = testCaseDescription.split("\n")
|
18
|
+
if len(elems) > 1:
|
19
|
+
raise Exception(f"The test case description must be a single line of text. slTestCaseDescription = {testCaseDescription}")
|
20
|
+
|
21
|
+
if testMethod is None:
|
22
|
+
raise Exception(f"No test method defined for test {testCaseNumber}")
|
23
|
+
|
24
|
+
self._testCaseNumber = testCaseNumber
|
25
|
+
self._testCaseDescription = testCaseDescription
|
26
|
+
self._testMethod=testMethod
|
27
|
+
|
28
|
+
self._preConditionMethod = None
|
29
|
+
self._postConditionMethod = None
|
30
|
+
|
31
|
+
def getNumber(self):
|
32
|
+
"""@brief Get method for test case number."""
|
33
|
+
return self._testCaseNumber
|
34
|
+
|
35
|
+
def getDescription(self):
|
36
|
+
"""@brief Get method for test case description."""
|
37
|
+
return self._testCaseDescription
|
38
|
+
|
39
|
+
def getMethod(self):
|
40
|
+
"""@brief Get method for test case method."""
|
41
|
+
return self._testMethod
|
42
|
+
|
43
|
+
def setPreConditionMethod(self, preConditionMethod):
|
44
|
+
"""@brief Set the pre condition method to be called before the test method.
|
45
|
+
@param preConditionMethod The method to be called."""
|
46
|
+
self._preConditionMethod = preConditionMethod
|
47
|
+
|
48
|
+
def getPreConditionMethod(self):
|
49
|
+
"""@brief Get the pre condition method to be called before the test method."""
|
50
|
+
return self._preConditionMethod
|
51
|
+
|
52
|
+
def setPostConditionMethod(self, postConditionMethod):
|
53
|
+
"""@brief Set the post condition method to be called after the test method.
|
54
|
+
@param postConditionMethod The method to be called."""
|
55
|
+
self._postConditionMethod = postConditionMethod
|
56
|
+
|
57
|
+
def getPostConditionMethod(self):
|
58
|
+
"""@brief Get the post condition method to be called before the test method."""
|
59
|
+
return self._postConditionMethod
|
60
|
+
|
61
|
+
def showBanner(self, uio):
|
62
|
+
"""@brief Show the test case banner message to the user."""
|
63
|
+
table = []
|
64
|
+
table.append(("Test Case", "Description"))
|
65
|
+
table.append((f"{self._testCaseNumber}", f"{self._testCaseDescription}"))
|
66
|
+
uio.showTable(table)
|
67
|
+
|
68
|
+
class TestCaseBase(object):
|
69
|
+
"""@brief a base class that provides a simple interface to execute a sequence of test cases."""
|
70
|
+
|
71
|
+
def __init__(self, uio):
|
72
|
+
"""@brief Constructor.
|
73
|
+
@param uio A UIO instance for getting input from and sending info to the user."""
|
74
|
+
self._uio = uio
|
75
|
+
self._testCaseList = []
|
76
|
+
|
77
|
+
def addTestCase(self, testCaseNumber, testCaseDescription, testMethod):
|
78
|
+
"""@brief Add a test case to the list of available test cases."""
|
79
|
+
# Check we don't have a duplicaste test case number
|
80
|
+
for testCase in self._testCaseList:
|
81
|
+
if testCase.getNumber() == testCaseNumber:
|
82
|
+
raise Exception("Test case number {testCaseNumber} is already used.")
|
83
|
+
# We don't check for duplicate test case descriptions or methods because
|
84
|
+
# the caller may wish to perform a test case multiple times during testing.
|
85
|
+
testCase = TestCase(testCaseNumber, testCaseDescription, testMethod)
|
86
|
+
self._testCaseList.append(testCase)
|
87
|
+
|
88
|
+
def executeTestCases(self):
|
89
|
+
"""@brief Call all test cases in the test sequence."""
|
90
|
+
startTime = time()
|
91
|
+
try:
|
92
|
+
for testCase in self._testCaseList:
|
93
|
+
testCase.showBanner(self._uio)
|
94
|
+
preMethod = testCase.getPreConditionMethod()
|
95
|
+
if preMethod:
|
96
|
+
preMethod()
|
97
|
+
|
98
|
+
testCaseMethod = testCase.getMethod()
|
99
|
+
testCaseMethod()
|
100
|
+
|
101
|
+
postMethod = testCase.getPostConditionMethod()
|
102
|
+
if postMethod:
|
103
|
+
postMethod()
|
104
|
+
finally:
|
105
|
+
# Report the test time even in the event of a test failure.
|
106
|
+
testSecs = time()-startTime
|
107
|
+
self._uio.info(f"Took {testSecs:.1f} seconds to test.")
|
108
|
+
|
p3lib/bokeh_auth.py
ADDED
@@ -0,0 +1,363 @@
|
|
1
|
+
'''
|
2
|
+
|
3
|
+
This file is a modified version of the Bokeh authorisation example code.
|
4
|
+
Many thanks to bokeh for this.
|
5
|
+
|
6
|
+
This contains a mix of pep8 and camel case method names ...
|
7
|
+
|
8
|
+
'''
|
9
|
+
import os
|
10
|
+
import json
|
11
|
+
import tempfile
|
12
|
+
import tornado
|
13
|
+
from tornado.web import RequestHandler
|
14
|
+
from argon2 import PasswordHasher
|
15
|
+
from argon2.exceptions import VerificationError
|
16
|
+
from datetime import datetime
|
17
|
+
|
18
|
+
# could also define get_login_url function (but must give up LoginHandler)
|
19
|
+
login_url = "/login"
|
20
|
+
|
21
|
+
CRED_FILE_KEY = "CRED_FILE"
|
22
|
+
LOGIN_HTML_FILE_KEY = "LOGIN_HTML_FILE"
|
23
|
+
ACCESS_LOG_FILE = "ACCESS_LOG_FILE"
|
24
|
+
|
25
|
+
# could define get_user_async instead
|
26
|
+
def get_user(request_handler):
|
27
|
+
# Record the get request
|
28
|
+
LoginHandler.RecordGet(request_handler.request.remote_ip)
|
29
|
+
user = request_handler.get_cookie("user")
|
30
|
+
# Record the user making the request
|
31
|
+
LoginHandler.SaveInfoAccessLogMessage(f"USER={user}")
|
32
|
+
return user
|
33
|
+
|
34
|
+
def GetAuthAttrFile():
|
35
|
+
"""@brief Get the file that is used to pass parameters (credentials file and login.html file) to the tornado server.
|
36
|
+
There must be a better way of passing the credentials file to the tornado login handler than this..."""
|
37
|
+
jsonFile = os.path.join( tempfile.gettempdir(), f"bokeh_auth_attr_{os.getpid()}.json")
|
38
|
+
return jsonFile
|
39
|
+
|
40
|
+
def SetBokehAuthAttrs(credentialsJsonFile, loginHTMLFile, accessLogFile=None):
|
41
|
+
"""@brief Set the attributes used to login to the bokeh server. By default
|
42
|
+
no login is required to the bokeh server.
|
43
|
+
@param credentialsJsonFile The file that stores the username and hashed passwords for the server.
|
44
|
+
@param loginHTMLFile The HTML file for the page presented to the user when logging into the bokeh server.
|
45
|
+
@param accessLogFile The log file to record access to. If left as None then no logging occurs."""
|
46
|
+
jsonFile = GetAuthAttrFile()
|
47
|
+
with open(jsonFile, 'w') as fd:
|
48
|
+
cfgDict={CRED_FILE_KEY: credentialsJsonFile}
|
49
|
+
cfgDict[LOGIN_HTML_FILE_KEY]=loginHTMLFile
|
50
|
+
cfgDict[ACCESS_LOG_FILE]=accessLogFile
|
51
|
+
json.dump(cfgDict, fd, ensure_ascii=False, indent=4)
|
52
|
+
|
53
|
+
def _getCredDict():
|
54
|
+
"""@brief Get the dictionary containing the attributes passed into the tornado server auth
|
55
|
+
login process.
|
56
|
+
@return A dict containing the attributes."""
|
57
|
+
jsonFile = GetAuthAttrFile()
|
58
|
+
with open(jsonFile, 'r') as fd:
|
59
|
+
contents = fd.read()
|
60
|
+
return json.loads(contents)
|
61
|
+
|
62
|
+
def GetCredentialsFile():
|
63
|
+
"""@return The file containing the usernames and hashed passwords to login to the server."""
|
64
|
+
credDict = _getCredDict()
|
65
|
+
return credDict[CRED_FILE_KEY]
|
66
|
+
|
67
|
+
def GetLoginHTMLFile():
|
68
|
+
"""@return The html file for the login page."""
|
69
|
+
credDict = _getCredDict()
|
70
|
+
return credDict[LOGIN_HTML_FILE_KEY]
|
71
|
+
|
72
|
+
def GetAccessLogFile():
|
73
|
+
"""@return Get the access log file.."""
|
74
|
+
credDict = _getCredDict()
|
75
|
+
return credDict[ACCESS_LOG_FILE]
|
76
|
+
|
77
|
+
# optional login page for login_url
|
78
|
+
class LoginHandler(RequestHandler):
|
79
|
+
|
80
|
+
@staticmethod
|
81
|
+
def RecordGet(remoteIP):
|
82
|
+
"""@brief Record an HHTP get on the login page.
|
83
|
+
@param remoteIP The IP address of the client."""
|
84
|
+
msg = f"HTTP GET from {remoteIP}"
|
85
|
+
LoginHandler.SaveInfoAccessLogMessage(msg)
|
86
|
+
try:
|
87
|
+
# We import here so that the p3lib module will import even if ip2geotools
|
88
|
+
# is not available as pip install ip2geotools adds in ~ 70 python modules !!!
|
89
|
+
from ip2geotools.databases.noncommercial import DbIpCity
|
90
|
+
response = DbIpCity.get(remoteIP, api_key='free')
|
91
|
+
LoginHandler.SaveInfoAccessLogMessage(f"HTTP GET country = {response.country}")
|
92
|
+
LoginHandler.SaveInfoAccessLogMessage(f"HTTP GET region = {response.region}")
|
93
|
+
LoginHandler.SaveInfoAccessLogMessage(f"HTTP GET city = {response.city}")
|
94
|
+
LoginHandler.SaveInfoAccessLogMessage(f"HTTP GET latitude = {response.latitude}")
|
95
|
+
LoginHandler.SaveInfoAccessLogMessage(f"HTTP GET longitude = {response.longitude}")
|
96
|
+
except:
|
97
|
+
pass
|
98
|
+
|
99
|
+
def _recordLoginAttempt(self, username, password):
|
100
|
+
"""@brief Record an attempt to login to the server.
|
101
|
+
@param username The username entered.
|
102
|
+
@param password The password entered."""
|
103
|
+
pw = "*"*len(password)
|
104
|
+
LoginHandler.SaveInfoAccessLogMessage(f"Login attempt from {self.request.remote_ip}: username = {username}, password={pw}")
|
105
|
+
|
106
|
+
def _recordLoginSuccess(self, username, password):
|
107
|
+
"""@brief Record a successful login to the server.
|
108
|
+
@param username The username entered.
|
109
|
+
@param password The password entered."""
|
110
|
+
pw = "*"*len(password)
|
111
|
+
LoginHandler.SaveInfoAccessLogMessage(f"Login success from {self.request.remote_ip}: username = {username}, password={pw}")
|
112
|
+
|
113
|
+
@staticmethod
|
114
|
+
def SaveInfoAccessLogMessage(msg):
|
115
|
+
"""@brief Save an info level access log message.
|
116
|
+
@param msg The message to save to the access log."""
|
117
|
+
LoginHandler.SaveAccessLogMessage("INFO: "+str(msg))
|
118
|
+
|
119
|
+
@staticmethod
|
120
|
+
def SaveAccessLogMessage(msg):
|
121
|
+
"""@brief Save an access log message.
|
122
|
+
@param msg The message to save to the access log."""
|
123
|
+
now = datetime.now()
|
124
|
+
accessLogFile = GetAccessLogFile()
|
125
|
+
if accessLogFile and len(accessLogFile) > 0:
|
126
|
+
try:
|
127
|
+
if not os.path.isfile(accessLogFile):
|
128
|
+
with open(accessLogFile, 'w'):
|
129
|
+
pass
|
130
|
+
|
131
|
+
with open(accessLogFile, 'a') as fd:
|
132
|
+
line = now.isoformat() + ": " + str(msg)+"\n"
|
133
|
+
fd.write(line)
|
134
|
+
|
135
|
+
except:
|
136
|
+
pass
|
137
|
+
|
138
|
+
def get(self):
|
139
|
+
try:
|
140
|
+
errormessage = self.get_argument("error")
|
141
|
+
except Exception:
|
142
|
+
errormessage = ""
|
143
|
+
loginHTMLFile = GetLoginHTMLFile()
|
144
|
+
self.render(loginHTMLFile, errormessage=errormessage)
|
145
|
+
|
146
|
+
def check_permission(self, username, password):
|
147
|
+
"""@brief Check if we the username and password are valid
|
148
|
+
@return True if the username and password are valid."""
|
149
|
+
self._recordLoginAttempt(username, password)
|
150
|
+
valid = False
|
151
|
+
credentialsJsonFile = GetCredentialsFile()
|
152
|
+
LoginHandler.SaveInfoAccessLogMessage(f"credentialsJsonFile = {credentialsJsonFile}")
|
153
|
+
fileExists = os.path.isfile(credentialsJsonFile)
|
154
|
+
LoginHandler.SaveInfoAccessLogMessage(f"fileExists = {fileExists}")
|
155
|
+
ch = CredentialsHasher(credentialsJsonFile)
|
156
|
+
verified = ch.verify(username, password)
|
157
|
+
LoginHandler.SaveInfoAccessLogMessage(f"verified = {verified}")
|
158
|
+
if verified:
|
159
|
+
valid = True
|
160
|
+
self._recordLoginSuccess(username, password)
|
161
|
+
LoginHandler.SaveInfoAccessLogMessage(f"check_permission(): valid = {valid}")
|
162
|
+
return valid
|
163
|
+
|
164
|
+
def post(self):
|
165
|
+
username = self.get_argument("username", "")
|
166
|
+
password = self.get_argument("password", "")
|
167
|
+
auth = self.check_permission(username, password)
|
168
|
+
if auth:
|
169
|
+
self.set_current_user(username)
|
170
|
+
self.redirect("/")
|
171
|
+
else:
|
172
|
+
error_msg = "?error=" + tornado.escape.url_escape("Login incorrect")
|
173
|
+
self.redirect(login_url + error_msg)
|
174
|
+
|
175
|
+
def set_current_user(self, user):
|
176
|
+
if user:
|
177
|
+
self.set_cookie("user", tornado.escape.json_encode(user))
|
178
|
+
LoginHandler.SaveInfoAccessLogMessage(f"Set user cookie: user={user}")
|
179
|
+
else:
|
180
|
+
self.clear_cookie("user")
|
181
|
+
LoginHandler.SaveInfoAccessLogMessage("Cleared user cookie")
|
182
|
+
|
183
|
+
# optional logout_url, available as curdoc().session_context.logout_url
|
184
|
+
logout_url = "/logout"
|
185
|
+
|
186
|
+
# optional logout handler for logout_url
|
187
|
+
class LogoutHandler(RequestHandler):
|
188
|
+
|
189
|
+
def get(self):
|
190
|
+
self.clear_cookie("user")
|
191
|
+
self.redirect("/")
|
192
|
+
|
193
|
+
class CredentialsHasherExeption(Exception):
|
194
|
+
pass
|
195
|
+
|
196
|
+
|
197
|
+
class CredentialsHasher(object):
|
198
|
+
"""@brief Responsible for storing hashed credentials to a local file.
|
199
|
+
There are issues storing hashed credentials and so this is not
|
200
|
+
recommended for high security systems but is aimed at providing
|
201
|
+
a simple credentials storage solution for Bokeh servers."""
|
202
|
+
|
203
|
+
def __init__(self, credentialsJsonFile):
|
204
|
+
"""@brief Construct an object that can be used to generate a credentials has file and check
|
205
|
+
credentials entered by a user.
|
206
|
+
@param credentialsJsonFile A file that contains the hashed (via argon2) login credentials."""
|
207
|
+
self._credentialsJsonFile = credentialsJsonFile
|
208
|
+
self._passwordHasher = PasswordHasher()
|
209
|
+
self._credDict = self._getCredDict()
|
210
|
+
|
211
|
+
def _getCredDict(self):
|
212
|
+
"""@brief Get a dictionary containing the current credentials.
|
213
|
+
@return A dict containing the credentials.
|
214
|
+
value = username
|
215
|
+
key = hashed password."""
|
216
|
+
credDict = {}
|
217
|
+
# If the hash file exists
|
218
|
+
if os.path.isfile(self._credentialsJsonFile):
|
219
|
+
# Add the hash a a line in the file
|
220
|
+
with open(self._credentialsJsonFile, 'r') as fd:
|
221
|
+
contents = fd.read()
|
222
|
+
credDict = json.loads(contents)
|
223
|
+
return credDict
|
224
|
+
|
225
|
+
def isUsernameAvailable(self, username):
|
226
|
+
"""@brief Determine if the username is not already used.
|
227
|
+
@param username The login username.
|
228
|
+
@return True if the username is not already used."""
|
229
|
+
usernameAvailable = True
|
230
|
+
if username in self._credDict:
|
231
|
+
usernameAvailable = False
|
232
|
+
return usernameAvailable
|
233
|
+
|
234
|
+
def _saveCredentials(self):
|
235
|
+
"""@brief Save the cr3edentials to the file."""
|
236
|
+
with open(self._credentialsJsonFile, 'w', encoding='utf-8') as f:
|
237
|
+
json.dump(self._credDict, f, ensure_ascii=False, indent=4)
|
238
|
+
|
239
|
+
def add(self, username, password):
|
240
|
+
"""@brief Add credential to the stored hashes.
|
241
|
+
@param username The login username.
|
242
|
+
@param password The login password."""
|
243
|
+
if self.isUsernameAvailable(username):
|
244
|
+
hash = self._passwordHasher.hash(password)
|
245
|
+
self._credDict[username] = hash
|
246
|
+
self._saveCredentials()
|
247
|
+
|
248
|
+
else:
|
249
|
+
raise CredentialsHasherExeption(f"{username} username is already in use.")
|
250
|
+
|
251
|
+
def remove(self, username):
|
252
|
+
"""@brief Remove a user from the stored hashes.
|
253
|
+
If the username is not present then this method will return without an error.
|
254
|
+
@param username The login username.
|
255
|
+
@return True if the username/password was removed"""
|
256
|
+
removed = False
|
257
|
+
if username in self._credDict:
|
258
|
+
del self._credDict[username]
|
259
|
+
self._saveCredentials()
|
260
|
+
removed = True
|
261
|
+
return removed
|
262
|
+
|
263
|
+
def verify(self, username, password):
|
264
|
+
"""@brief Check the credentials are valid and stored in the hash file.
|
265
|
+
@param username The login username.
|
266
|
+
@param password The login password.
|
267
|
+
@return True if the username and password are authorised."""
|
268
|
+
validCredential = False
|
269
|
+
if username in self._credDict:
|
270
|
+
storedHash = self._credDict[username]
|
271
|
+
try:
|
272
|
+
self._passwordHasher.verify(storedHash, password)
|
273
|
+
validCredential = True
|
274
|
+
|
275
|
+
except VerificationError:
|
276
|
+
pass
|
277
|
+
|
278
|
+
return validCredential
|
279
|
+
|
280
|
+
def getCredentialCount(self):
|
281
|
+
"""@brief Get the number of credentials that are stored.
|
282
|
+
@return The number of credentials stored."""
|
283
|
+
return len(self._credDict.keys())
|
284
|
+
|
285
|
+
def getUsernameList(self):
|
286
|
+
"""@brief Get a list of usernames.
|
287
|
+
@return A list of usernames."""
|
288
|
+
return list(self._credDict.keys())
|
289
|
+
|
290
|
+
class CredentialsManager(object):
|
291
|
+
"""@brief Responsible for allowing the user to add and remove credentials to a a local file."""
|
292
|
+
|
293
|
+
def __init__(self, uio, credentialsJsonFile):
|
294
|
+
"""@brief Constructor.
|
295
|
+
@param uio A UIO instance that allows user input output.
|
296
|
+
@param credentialsJsonFile A file that contains the hashed (via argon2) login credentials."""
|
297
|
+
self._uio = uio
|
298
|
+
self._credentialsJsonFile = credentialsJsonFile
|
299
|
+
self.credentialsHasher = CredentialsHasher(self._credentialsJsonFile)
|
300
|
+
|
301
|
+
def _add(self):
|
302
|
+
"""@brief Add a username/password to the list of credentials."""
|
303
|
+
self._uio.info('Add a username/password')
|
304
|
+
username = self._uio.getInput('Enter the username: ')
|
305
|
+
if self.credentialsHasher.isUsernameAvailable(username):
|
306
|
+
password = self._uio.getInput('Enter the password: ')
|
307
|
+
self.credentialsHasher.add(username, password)
|
308
|
+
else:
|
309
|
+
self._uio.error(f"{username} is already in use.")
|
310
|
+
|
311
|
+
def _delete(self):
|
312
|
+
"""@brief Delete a username/password from the list of credentials."""
|
313
|
+
self._uio.info('Delete a username/password')
|
314
|
+
username = self._uio.getInput('Enter the username: ')
|
315
|
+
if not self.credentialsHasher.isUsernameAvailable(username):
|
316
|
+
if self.credentialsHasher.remove(username):
|
317
|
+
self._uio.info(f"Removed {username}")
|
318
|
+
else:
|
319
|
+
self._uio.error(f"Failed to remove {username}.")
|
320
|
+
|
321
|
+
else:
|
322
|
+
self._uio.error(f"{username} not found.")
|
323
|
+
|
324
|
+
def _check(self):
|
325
|
+
"""@brief Check a username/password from the list of credentials."""
|
326
|
+
self._uio.info('Check a username/password')
|
327
|
+
username = self._uio.getInput('Enter the username: ')
|
328
|
+
password = self._uio.getInput('Enter the password: ')
|
329
|
+
if self.credentialsHasher.verify(username, password):
|
330
|
+
self._uio.info("The username and password match.")
|
331
|
+
else:
|
332
|
+
self._uio.error("The username and password do not match.")
|
333
|
+
|
334
|
+
def _showUsernames(self):
|
335
|
+
"""@brief Show the user a list of the usernames stored."""
|
336
|
+
table = [["USERNAME"]]
|
337
|
+
for username in self.credentialsHasher.getUsernameList():
|
338
|
+
table.append([username])
|
339
|
+
self._uio.showTable(table)
|
340
|
+
|
341
|
+
def manage(self):
|
342
|
+
"""@brief Allow the user to add and remove user credentials from a local file."""
|
343
|
+
while True:
|
344
|
+
self._uio.info("")
|
345
|
+
self._showUsernames()
|
346
|
+
self._uio.info(f"{self.credentialsHasher.getCredentialCount()} credentials stored in {self._credentialsJsonFile}")
|
347
|
+
self._uio.info("")
|
348
|
+
self._uio.info("A - Add a username/password.")
|
349
|
+
self._uio.info("D - Delete a username/password.")
|
350
|
+
self._uio.info("C - Check a username/password is stored.")
|
351
|
+
self._uio.info("Q - Quit.")
|
352
|
+
response = self._uio.getInput('Enter one of the above options: ')
|
353
|
+
response = response.upper()
|
354
|
+
if response == 'A':
|
355
|
+
self._add()
|
356
|
+
elif response == 'D':
|
357
|
+
self._delete()
|
358
|
+
elif response == 'C':
|
359
|
+
self._check()
|
360
|
+
elif response == 'Q':
|
361
|
+
return
|
362
|
+
else:
|
363
|
+
self._uio.error(f"{response} is an invalid response.")
|