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 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.")