Ported code to Python 3.7. TODO: Port CI pipeline and deployment images.
This commit is contained in:
parent
f4685114b2
commit
bb3d9e62e6
7 changed files with 36 additions and 125 deletions
|
@ -16,7 +16,7 @@ This project intends to provide a well-tested and maintainable alternative to
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
- Python 3 (>=3.6)
|
- Python 3 (>=3.7)
|
||||||
- Python dependencies:
|
- Python dependencies:
|
||||||
- jinja2
|
- jinja2
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
from typing import List, Optional, Any, Type
|
from typing import List, Optional, Any, Type
|
||||||
|
|
||||||
import crypt
|
import crypt
|
||||||
|
@ -9,12 +10,6 @@ from matemat.exceptions import AuthenticationError, DatabaseConsistencyError
|
||||||
from matemat.db import DatabaseWrapper
|
from matemat.db import DatabaseWrapper
|
||||||
from matemat.db.wrapper import Transaction
|
from matemat.db.wrapper import Transaction
|
||||||
|
|
||||||
# TODO: Change to METHOD_BLOWFISH when adopting Python 3.7
|
|
||||||
"""
|
|
||||||
The method to use for password hashing.
|
|
||||||
"""
|
|
||||||
_CRYPT_METHOD = crypt.METHOD_SHA512
|
|
||||||
|
|
||||||
|
|
||||||
class MatematDatabase(object):
|
class MatematDatabase(object):
|
||||||
"""
|
"""
|
||||||
|
@ -42,7 +37,7 @@ class MatematDatabase(object):
|
||||||
"""
|
"""
|
||||||
self.db: DatabaseWrapper = DatabaseWrapper(filename)
|
self.db: DatabaseWrapper = DatabaseWrapper(filename)
|
||||||
|
|
||||||
def __enter__(self) -> 'MatematDatabase':
|
def __enter__(self) -> MatematDatabase:
|
||||||
# Pass context manager stuff through to the database wrapper
|
# Pass context manager stuff through to the database wrapper
|
||||||
self.db.__enter__()
|
self.db.__enter__()
|
||||||
return self
|
return self
|
||||||
|
@ -100,7 +95,7 @@ class MatematDatabase(object):
|
||||||
:raises ValueError: If a user with the same name already exists.
|
:raises ValueError: If a user with the same name already exists.
|
||||||
"""
|
"""
|
||||||
# Hash the password.
|
# Hash the password.
|
||||||
pwhash: str = crypt.crypt(password, crypt.mksalt(_CRYPT_METHOD))
|
pwhash: str = crypt.crypt(password, crypt.mksalt())
|
||||||
user_id: int = -1
|
user_id: int = -1
|
||||||
with self.db.transaction() as c:
|
with self.db.transaction() as c:
|
||||||
# Look up whether a user with the same name already exists.
|
# Look up whether a user with the same name already exists.
|
||||||
|
@ -178,7 +173,7 @@ class MatematDatabase(object):
|
||||||
if verify_password and not compare_digest(crypt.crypt(oldpass, row[0]), row[0]):
|
if verify_password and not compare_digest(crypt.crypt(oldpass, row[0]), row[0]):
|
||||||
raise AuthenticationError('Old password does not match.')
|
raise AuthenticationError('Old password does not match.')
|
||||||
# Hash the new password and write it to the database.
|
# Hash the new password and write it to the database.
|
||||||
pwhash: str = crypt.crypt(newpass, crypt.mksalt(_CRYPT_METHOD))
|
pwhash: str = crypt.crypt(newpass, crypt.mksalt())
|
||||||
c.execute('''
|
c.execute('''
|
||||||
UPDATE users SET password = :pwhash, lastchange = STRFTIME('%s', 'now') WHERE user_id = :user_id
|
UPDATE users SET password = :pwhash, lastchange = STRFTIME('%s', 'now') WHERE user_id = :user_id
|
||||||
''', {
|
''', {
|
||||||
|
@ -208,7 +203,7 @@ class MatematDatabase(object):
|
||||||
if verify_password and not compare_digest(crypt.crypt(password, row[0]), row[0]):
|
if verify_password and not compare_digest(crypt.crypt(password, row[0]), row[0]):
|
||||||
raise AuthenticationError('Password does not match.')
|
raise AuthenticationError('Password does not match.')
|
||||||
# Hash the new touchkey and write it to the database.
|
# Hash the new touchkey and write it to the database.
|
||||||
tkhash: Optional[str] = crypt.crypt(touchkey, crypt.mksalt(_CRYPT_METHOD)) if touchkey is not None else None
|
tkhash: Optional[str] = crypt.crypt(touchkey, crypt.mksalt()) if touchkey is not None else None
|
||||||
c.execute('''
|
c.execute('''
|
||||||
UPDATE users SET touchkey = :tkhash, lastchange = STRFTIME('%s', 'now') WHERE user_id = :user_id
|
UPDATE users SET touchkey = :tkhash, lastchange = STRFTIME('%s', 'now') WHERE user_id = :user_id
|
||||||
''', {
|
''', {
|
||||||
|
|
|
@ -58,7 +58,7 @@ class DatabaseTest(unittest.TestCase):
|
||||||
u = db.create_user('testuser', 'supersecurepassword', 'testuser@example.com')
|
u = db.create_user('testuser', 'supersecurepassword', 'testuser@example.com')
|
||||||
# Add a touchkey without using the provided function
|
# Add a touchkey without using the provided function
|
||||||
c.execute('''UPDATE users SET touchkey = :tkhash WHERE user_id = :user_id''', {
|
c.execute('''UPDATE users SET touchkey = :tkhash WHERE user_id = :user_id''', {
|
||||||
'tkhash': crypt.crypt('0123', crypt.mksalt(crypt.METHOD_SHA512)),
|
'tkhash': crypt.crypt('0123', crypt.mksalt()),
|
||||||
'user_id': u.id
|
'user_id': u.id
|
||||||
})
|
})
|
||||||
user = db.login('testuser', 'supersecurepassword')
|
user = db.login('testuser', 'supersecurepassword')
|
||||||
|
@ -99,7 +99,7 @@ class DatabaseTest(unittest.TestCase):
|
||||||
with self.assertRaises(AuthenticationError):
|
with self.assertRaises(AuthenticationError):
|
||||||
db.login('testuser', 'mynewpassword')
|
db.login('testuser', 'mynewpassword')
|
||||||
db.login('testuser', 'adminpasswordreset')
|
db.login('testuser', 'adminpasswordreset')
|
||||||
user._user_id = -1
|
user.id = -1
|
||||||
with self.assertRaises(AuthenticationError):
|
with self.assertRaises(AuthenticationError):
|
||||||
# Password change for an inexistent user should fail
|
# Password change for an inexistent user should fail
|
||||||
db.change_password(user, 'adminpasswordreset', 'passwordwithoutuser')
|
db.change_password(user, 'adminpasswordreset', 'passwordwithoutuser')
|
||||||
|
@ -123,7 +123,7 @@ class DatabaseTest(unittest.TestCase):
|
||||||
with self.assertRaises(AuthenticationError):
|
with self.assertRaises(AuthenticationError):
|
||||||
db.login('testuser', touchkey='4567')
|
db.login('testuser', touchkey='4567')
|
||||||
db.login('testuser', touchkey='89ab')
|
db.login('testuser', touchkey='89ab')
|
||||||
user._user_id = -1
|
user.id = -1
|
||||||
with self.assertRaises(AuthenticationError):
|
with self.assertRaises(AuthenticationError):
|
||||||
# Touchkey change for an inexistent user should fail
|
# Touchkey change for an inexistent user should fail
|
||||||
db.change_touchkey(user, '89ab', '048c')
|
db.change_touchkey(user, '89ab', '048c')
|
||||||
|
@ -139,7 +139,7 @@ class DatabaseTest(unittest.TestCase):
|
||||||
self.assertEqual('newaddress@example.com', checkuser.email)
|
self.assertEqual('newaddress@example.com', checkuser.email)
|
||||||
self.assertFalse(checkuser.is_admin)
|
self.assertFalse(checkuser.is_admin)
|
||||||
self.assertFalse(checkuser.is_member)
|
self.assertFalse(checkuser.is_member)
|
||||||
user._user_id = -1
|
user.id = -1
|
||||||
with self.assertRaises(DatabaseConsistencyError):
|
with self.assertRaises(DatabaseConsistencyError):
|
||||||
db.change_user(user)
|
db.change_user(user)
|
||||||
|
|
||||||
|
@ -205,7 +205,7 @@ class DatabaseTest(unittest.TestCase):
|
||||||
self.assertEqual('Flora Power Mate', checkproduct.name)
|
self.assertEqual('Flora Power Mate', checkproduct.name)
|
||||||
self.assertEqual(150, checkproduct.price_member)
|
self.assertEqual(150, checkproduct.price_member)
|
||||||
self.assertEqual(250, checkproduct.price_non_member)
|
self.assertEqual(250, checkproduct.price_non_member)
|
||||||
product._product_id = -1
|
product.id = -1
|
||||||
with self.assertRaises(DatabaseConsistencyError):
|
with self.assertRaises(DatabaseConsistencyError):
|
||||||
db.change_product(product)
|
db.change_product(product)
|
||||||
product2 = db.create_product('Club Mate', 200, 200)
|
product2 = db.create_product('Club Mate', 200, 200)
|
||||||
|
@ -248,7 +248,7 @@ class DatabaseTest(unittest.TestCase):
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
# Should fail, negative amount
|
# Should fail, negative amount
|
||||||
db.deposit(user, -42)
|
db.deposit(user, -42)
|
||||||
user._user_id = -1
|
user.id = -1
|
||||||
with self.assertRaises(DatabaseConsistencyError):
|
with self.assertRaises(DatabaseConsistencyError):
|
||||||
# Should fail, user id -1 does not exist
|
# Should fail, user id -1 does not exist
|
||||||
db.deposit(user, 42)
|
db.deposit(user, 42)
|
||||||
|
@ -267,7 +267,7 @@ class DatabaseTest(unittest.TestCase):
|
||||||
self.assertEqual(42, c.fetchone()[0])
|
self.assertEqual(42, c.fetchone()[0])
|
||||||
c.execute('''SELECT stock FROM products WHERE product_id = ?''', [product2.id])
|
c.execute('''SELECT stock FROM products WHERE product_id = ?''', [product2.id])
|
||||||
self.assertEqual(0, c.fetchone()[0])
|
self.assertEqual(0, c.fetchone()[0])
|
||||||
product._product_id = -1
|
product.id = -1
|
||||||
with self.assertRaises(DatabaseConsistencyError):
|
with self.assertRaises(DatabaseConsistencyError):
|
||||||
# Should fail, product id -1 does not exist
|
# Should fail, product id -1 does not exist
|
||||||
db.restock(product, 42)
|
db.restock(product, 42)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
import sqlite3
|
import sqlite3
|
||||||
|
@ -76,7 +77,7 @@ class DatabaseWrapper(object):
|
||||||
self._filename: str = filename
|
self._filename: str = filename
|
||||||
self._sqlite_db: Optional[sqlite3.Connection] = None
|
self._sqlite_db: Optional[sqlite3.Connection] = None
|
||||||
|
|
||||||
def __enter__(self) -> 'DatabaseWrapper':
|
def __enter__(self) -> DatabaseWrapper:
|
||||||
self.connect()
|
self.connect()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
|
@ -1,51 +1,10 @@
|
||||||
|
|
||||||
from typing import Any
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
class Product(object):
|
@dataclass
|
||||||
|
class Product:
|
||||||
def __init__(self,
|
id: int
|
||||||
product_id: int,
|
name: str
|
||||||
name: str,
|
price_member: int
|
||||||
price_member: int,
|
price_non_member: int
|
||||||
price_non_member: int) -> None:
|
|
||||||
self._product_id: int = product_id
|
|
||||||
self._name: str = name
|
|
||||||
self._price_member: int = price_member
|
|
||||||
self._price_non_member: int = price_non_member
|
|
||||||
|
|
||||||
def __eq__(self, other: Any) -> bool:
|
|
||||||
if other is None or not isinstance(other, Product):
|
|
||||||
return False
|
|
||||||
return self._product_id == other._product_id \
|
|
||||||
and self._name == other._name \
|
|
||||||
and self._price_member == other._price_member \
|
|
||||||
and self._price_non_member == other._price_non_member
|
|
||||||
|
|
||||||
@property
|
|
||||||
def id(self) -> int:
|
|
||||||
return self._product_id
|
|
||||||
|
|
||||||
@property
|
|
||||||
def name(self) -> str:
|
|
||||||
return self._name
|
|
||||||
|
|
||||||
@name.setter
|
|
||||||
def name(self, name: str) -> None:
|
|
||||||
self._name = name
|
|
||||||
|
|
||||||
@property
|
|
||||||
def price_member(self) -> int:
|
|
||||||
return self._price_member
|
|
||||||
|
|
||||||
@price_member.setter
|
|
||||||
def price_member(self, price: int) -> None:
|
|
||||||
self._price_member = price
|
|
||||||
|
|
||||||
@property
|
|
||||||
def price_non_member(self) -> int:
|
|
||||||
return self._price_non_member
|
|
||||||
|
|
||||||
@price_non_member.setter
|
|
||||||
def price_non_member(self, price: int) -> None:
|
|
||||||
self._price_non_member = price
|
|
||||||
|
|
|
@ -1,58 +1,13 @@
|
||||||
|
|
||||||
from typing import Optional, Any
|
from typing import Optional
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
class User(object):
|
@dataclass
|
||||||
|
class User:
|
||||||
def __init__(self,
|
id: int
|
||||||
user_id: int,
|
name: str
|
||||||
username: str,
|
email: Optional[str] = None
|
||||||
email: Optional[str] = None,
|
is_admin: bool = False
|
||||||
admin: bool = False,
|
is_member: bool = False
|
||||||
member: bool = True) -> None:
|
|
||||||
self._user_id: int = user_id
|
|
||||||
self._username: str = username
|
|
||||||
self._email: Optional[str] = email
|
|
||||||
self._admin: bool = admin
|
|
||||||
self._member: bool = member
|
|
||||||
|
|
||||||
def __eq__(self, other: Any) -> bool:
|
|
||||||
if other is None or not isinstance(other, User):
|
|
||||||
return False
|
|
||||||
return self._user_id == other._user_id \
|
|
||||||
and self._username == other._username \
|
|
||||||
and self._email == other._email \
|
|
||||||
and self._admin == other._admin \
|
|
||||||
and self._member == other._member
|
|
||||||
|
|
||||||
@property
|
|
||||||
def id(self) -> int:
|
|
||||||
return self._user_id
|
|
||||||
|
|
||||||
@property
|
|
||||||
def name(self) -> str:
|
|
||||||
return self._username
|
|
||||||
|
|
||||||
@property
|
|
||||||
def email(self) -> Optional[str]:
|
|
||||||
return self._email
|
|
||||||
|
|
||||||
@email.setter
|
|
||||||
def email(self, email: str) -> None:
|
|
||||||
self._email = email
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_admin(self) -> bool:
|
|
||||||
return self._admin
|
|
||||||
|
|
||||||
@is_admin.setter
|
|
||||||
def is_admin(self, admin: bool) -> None:
|
|
||||||
self._admin = admin
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_member(self) -> bool:
|
|
||||||
return self._member
|
|
||||||
|
|
||||||
@is_member.setter
|
|
||||||
def is_member(self, member: bool) -> None:
|
|
||||||
self._member = member
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
from typing import Dict, Iterator, List, Tuple, Union
|
from typing import Dict, Iterator, List, Tuple, Union
|
||||||
|
|
||||||
|
|
||||||
|
@ -24,7 +25,7 @@ class RequestArguments(object):
|
||||||
"""
|
"""
|
||||||
self.__container: Dict[str, RequestArgument] = dict()
|
self.__container: Dict[str, RequestArgument] = dict()
|
||||||
|
|
||||||
def __getitem__(self, key: str) -> 'RequestArgument':
|
def __getitem__(self, key: str) -> RequestArgument:
|
||||||
"""
|
"""
|
||||||
Retrieve the argument for the given name, creating it on the fly, if it doesn't exist.
|
Retrieve the argument for the given name, creating it on the fly, if it doesn't exist.
|
||||||
|
|
||||||
|
@ -40,7 +41,7 @@ class RequestArguments(object):
|
||||||
# Return the argument for the name
|
# Return the argument for the name
|
||||||
return self.__container[key]
|
return self.__container[key]
|
||||||
|
|
||||||
def __getattr__(self, key: str) -> 'RequestArgument':
|
def __getattr__(self, key: str) -> RequestArgument:
|
||||||
"""
|
"""
|
||||||
Syntactic sugar for accessing values with a name that can be used in Python attributes. The value will be
|
Syntactic sugar for accessing values with a name that can be used in Python attributes. The value will be
|
||||||
returned as an immutable view.
|
returned as an immutable view.
|
||||||
|
@ -278,7 +279,7 @@ class RequestArgument(object):
|
||||||
# Yield an immutable scalar view for each (ctype, value) element in the array
|
# Yield an immutable scalar view for each (ctype, value) element in the array
|
||||||
yield _View(self.__name, v)
|
yield _View(self.__name, v)
|
||||||
|
|
||||||
def __getitem__(self, index: Union[int, slice]) -> 'RequestArgument':
|
def __getitem__(self, index: Union[int, slice]) -> RequestArgument:
|
||||||
"""
|
"""
|
||||||
Index the argument with either an int or a slice. The returned values are represented as immutable
|
Index the argument with either an int or a slice. The returned values are represented as immutable
|
||||||
RequestArgument views.
|
RequestArgument views.
|
||||||
|
|
Loading…
Reference in a new issue