Ported code to Python 3.7. TODO: Port CI pipeline and deployment images.

This commit is contained in:
s3lph 2018-07-16 19:47:54 +02:00
parent f4685114b2
commit bb3d9e62e6
7 changed files with 36 additions and 125 deletions

View file

@ -16,7 +16,7 @@ This project intends to provide a well-tested and maintainable alternative to
## Dependencies
- Python 3 (>=3.6)
- Python 3 (>=3.7)
- Python dependencies:
- jinja2

View file

@ -1,4 +1,5 @@
from __future__ import annotations
from typing import List, Optional, Any, Type
import crypt
@ -9,12 +10,6 @@ from matemat.exceptions import AuthenticationError, DatabaseConsistencyError
from matemat.db import DatabaseWrapper
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):
"""
@ -42,7 +37,7 @@ class MatematDatabase(object):
"""
self.db: DatabaseWrapper = DatabaseWrapper(filename)
def __enter__(self) -> 'MatematDatabase':
def __enter__(self) -> MatematDatabase:
# Pass context manager stuff through to the database wrapper
self.db.__enter__()
return self
@ -100,7 +95,7 @@ class MatematDatabase(object):
:raises ValueError: If a user with the same name already exists.
"""
# Hash the password.
pwhash: str = crypt.crypt(password, crypt.mksalt(_CRYPT_METHOD))
pwhash: str = crypt.crypt(password, crypt.mksalt())
user_id: int = -1
with self.db.transaction() as c:
# 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]):
raise AuthenticationError('Old password does not match.')
# 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('''
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]):
raise AuthenticationError('Password does not match.')
# 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('''
UPDATE users SET touchkey = :tkhash, lastchange = STRFTIME('%s', 'now') WHERE user_id = :user_id
''', {

View file

@ -58,7 +58,7 @@ class DatabaseTest(unittest.TestCase):
u = db.create_user('testuser', 'supersecurepassword', 'testuser@example.com')
# Add a touchkey without using the provided function
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 = db.login('testuser', 'supersecurepassword')
@ -99,7 +99,7 @@ class DatabaseTest(unittest.TestCase):
with self.assertRaises(AuthenticationError):
db.login('testuser', 'mynewpassword')
db.login('testuser', 'adminpasswordreset')
user._user_id = -1
user.id = -1
with self.assertRaises(AuthenticationError):
# Password change for an inexistent user should fail
db.change_password(user, 'adminpasswordreset', 'passwordwithoutuser')
@ -123,7 +123,7 @@ class DatabaseTest(unittest.TestCase):
with self.assertRaises(AuthenticationError):
db.login('testuser', touchkey='4567')
db.login('testuser', touchkey='89ab')
user._user_id = -1
user.id = -1
with self.assertRaises(AuthenticationError):
# Touchkey change for an inexistent user should fail
db.change_touchkey(user, '89ab', '048c')
@ -139,7 +139,7 @@ class DatabaseTest(unittest.TestCase):
self.assertEqual('newaddress@example.com', checkuser.email)
self.assertFalse(checkuser.is_admin)
self.assertFalse(checkuser.is_member)
user._user_id = -1
user.id = -1
with self.assertRaises(DatabaseConsistencyError):
db.change_user(user)
@ -205,7 +205,7 @@ class DatabaseTest(unittest.TestCase):
self.assertEqual('Flora Power Mate', checkproduct.name)
self.assertEqual(150, checkproduct.price_member)
self.assertEqual(250, checkproduct.price_non_member)
product._product_id = -1
product.id = -1
with self.assertRaises(DatabaseConsistencyError):
db.change_product(product)
product2 = db.create_product('Club Mate', 200, 200)
@ -248,7 +248,7 @@ class DatabaseTest(unittest.TestCase):
with self.assertRaises(ValueError):
# Should fail, negative amount
db.deposit(user, -42)
user._user_id = -1
user.id = -1
with self.assertRaises(DatabaseConsistencyError):
# Should fail, user id -1 does not exist
db.deposit(user, 42)
@ -267,7 +267,7 @@ class DatabaseTest(unittest.TestCase):
self.assertEqual(42, c.fetchone()[0])
c.execute('''SELECT stock FROM products WHERE product_id = ?''', [product2.id])
self.assertEqual(0, c.fetchone()[0])
product._product_id = -1
product.id = -1
with self.assertRaises(DatabaseConsistencyError):
# Should fail, product id -1 does not exist
db.restock(product, 42)

View file

@ -1,4 +1,5 @@
from __future__ import annotations
from typing import Any, Optional
import sqlite3
@ -76,7 +77,7 @@ class DatabaseWrapper(object):
self._filename: str = filename
self._sqlite_db: Optional[sqlite3.Connection] = None
def __enter__(self) -> 'DatabaseWrapper':
def __enter__(self) -> DatabaseWrapper:
self.connect()
return self

View file

@ -1,51 +1,10 @@
from typing import Any
from dataclasses import dataclass
class Product(object):
def __init__(self,
product_id: int,
name: str,
price_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
@dataclass
class Product:
id: int
name: str
price_member: int
price_non_member: int

View file

@ -1,58 +1,13 @@
from typing import Optional, Any
from typing import Optional
from dataclasses import dataclass
class User(object):
def __init__(self,
user_id: int,
username: str,
email: Optional[str] = None,
admin: 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
@dataclass
class User:
id: int
name: str
email: Optional[str] = None
is_admin: bool = False
is_member: bool = False

View file

@ -1,4 +1,5 @@
from __future__ import annotations
from typing import Dict, Iterator, List, Tuple, Union
@ -24,7 +25,7 @@ class RequestArguments(object):
"""
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.
@ -40,7 +41,7 @@ class RequestArguments(object):
# Return the argument for the name
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
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 _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
RequestArgument views.