Merge branch 'python-3.7' into 'staging'

Python 3.7

See merge request s3lph/matemat!26
This commit is contained in:
s3lph 2018-08-02 20:27:38 +00:00
commit 0daf37d94a
10 changed files with 66 additions and 178 deletions

View file

@ -1,5 +1,5 @@
---
image: s3lph/matemat-ci:20180720-01
image: s3lph/matemat-ci:20180802-02
stages:
- test
@ -10,8 +10,8 @@ test:
stage: test
script:
- pip3 install -r requirements.txt
- sudo -u matemat python3-coverage run --branch -m unittest discover matemat
- sudo -u matemat python3-coverage report -m --include 'matemat/*' --omit '*/test_*.py'
- sudo -u matemat python3 -m coverage run --branch -m unittest discover matemat
- sudo -u matemat python3 -m coverage report -m --include 'matemat/*' --omit '*/test_*.py'
codestyle:
stage: test

View file

@ -1,5 +1,5 @@
FROM python:3.6-alpine
FROM python:3.7-alpine
RUN mkdir -p /var/matemat/db /var/matemat/upload
RUN apk --update add libmagic

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:
- file-magic
- 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
@ -116,7 +111,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.
@ -194,7 +189,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
''', {
@ -224,7 +219,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

@ -1,75 +1,22 @@
from typing import Any
from dataclasses import dataclass
class Product(object):
@dataclass
class Product:
"""
Representation of a product offered by the Matemat, with a name, prices for users, and the number of items
currently in stock.
:param id: The product ID in the database.
:param name: The product's name.
:param price_member: The price of a unit of this product for users marked as "members".
:param price_non_member: The price of a unit of this product for users NOT marked as "members".
:param stock: The number of items of this product currently in stock.
"""
def __init__(self,
product_id: int,
name: str,
price_member: int,
price_non_member: int,
stock: int) -> None:
"""
Create a new product instance with the given arguments.
:param product_id: The product ID in the database.
:param name: The product's name.
:param price_member: The price of a unit of this product for users marked as "members".
:param price_non_member: The price of a unit of this product for users NOT marked as "members".
:param stock: The number of items of this product currently in stock.
"""
self._product_id: int = product_id
self._name: str = name
self._price_member: int = price_member
self._price_non_member: int = price_non_member
self._stock: int = stock
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 \
and self._stock == other._stock
@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
@property
def stock(self) -> int:
return self._stock
@stock.setter
def stock(self, stock: int) -> None:
self._stock = stock
id: int
name: str
price_member: int
price_non_member: int
stock: int

View file

@ -1,87 +1,27 @@
from typing import Optional, Any
from typing import Optional
from dataclasses import dataclass
class User(object):
@dataclass
class User:
"""
Representation of a user registered with the Matemat, with a name, e-mail address (optional), whether the user is a
member of the organization the Matemat instance is used in, whether the user is an administrator, and the user's
account balance.
:param id: The user ID in the database.
:param username: The user's name.
:param balance: The balance of the user's account.
:param email: The user's e-mail address (optional).
:param admin: Whether the user is an administrator.
:param member: Whether the user is a member.
"""
def __init__(self,
user_id: int,
username: str,
balance: int,
email: Optional[str] = None,
admin: bool = False,
member: bool = True) -> None:
"""
Create a new user instance with the given arguments.
:param user_id: The user ID in the database.
:param username: The user's name.
:param balance: The balance of the user's account.
:param email: The user's e-mail address (optional).
:param admin: Whether the user is an administrator.
:param member: Whether the user is a member.
"""
self._user_id: int = user_id
self._username: str = username
self._email: Optional[str] = email
self._admin: bool = admin
self._member: bool = member
self._balance: int = balance
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
@name.setter
def name(self, value):
self._username = value
@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
@property
def balance(self) -> int:
return self._balance
@balance.setter
def balance(self, balance: int) -> None:
self._balance = balance
id: int
name: str
balance: int
email: Optional[str] = None
is_admin: bool = False
is_member: bool = False

View file

@ -71,7 +71,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')
@ -112,7 +112,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')
@ -136,7 +136,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')
@ -157,7 +157,7 @@ class DatabaseTest(unittest.TestCase):
self.assertFalse(checkuser.is_admin)
self.assertFalse(checkuser.is_member)
self.assertEqual(4200, checkuser.balance)
user._user_id = -1
user.id = -1
with self.assertRaises(DatabaseConsistencyError):
db.change_user(user, agent, is_member='True')
@ -238,7 +238,7 @@ class DatabaseTest(unittest.TestCase):
self.assertEqual(150, checkproduct.price_member)
self.assertEqual(250, checkproduct.price_non_member)
self.assertEqual(42, checkproduct.stock)
product._product_id = -1
product.id = -1
with self.assertRaises(DatabaseConsistencyError):
db.change_product(product)
product2 = db.create_product('Club Mate', 200, 200)
@ -281,7 +281,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)
@ -300,7 +300,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
@ -48,7 +49,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,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.

View file

@ -1,11 +1,15 @@
FROM debian:buster
# There is no buster image yet and stretch doesn't have a docker package. So let's just "upgrade" the image to buster.
FROM python:3.7-stretch
RUN useradd -d /home/matemat -m matemat
RUN mkdir -p /var/matemat/db && chown matemat:matemat -R /var/matemat/db
RUN mkdir -p /var/matemat/upload && chown matemat:matemat -R /var/matemat/upload
RUN apt-get update -qy
RUN apt-get install -y --no-install-recommends file sudo openssh-client git docker.io python3-dev python3-pip python3-coverage python3-setuptools build-essential
RUN pip3 install wheel pycodestyle mypy
RUN sed -re 's/stretch/buster/g' -i /etc/apt/sources.list \
&& useradd -d /home/matemat -m matemat \
&& mkdir -p /var/matemat/db /var/matemat/upload \
&& chown matemat:matemat -R /var/matemat/db \
&& chown matemat:matemat -R /var/matemat/upload \
&& apt-get update -qy \
&& apt-get install -y --no-install-recommends file sudo openssh-client git docker.io build-essential \
&& python3.7 -m pip install coverage wheel pycodestyle mypy \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /home/matemat