Merge branch 'staging' into 'master'
Automated Deployment & Release Management See merge request s3lph/matemat!55
This commit is contained in:
commit
7cb1bbe24a
36 changed files with 760 additions and 238 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -10,4 +10,4 @@
|
||||||
*.sqlite3
|
*.sqlite3
|
||||||
*.db
|
*.db
|
||||||
static/upload/
|
static/upload/
|
||||||
**/matemat.conf
|
./matemat.conf
|
||||||
|
|
122
.gitlab-ci.yml
122
.gitlab-ci.yml
|
@ -1,39 +1,126 @@
|
||||||
---
|
---
|
||||||
image: s3lph/matemat-ci:20180802-02
|
image: s3lph/matemat-ci:20181107-02
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- test
|
- test
|
||||||
- build
|
- build
|
||||||
- staging
|
- deploy
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
before_script:
|
||||||
|
- export MATEMAT_VERSION=$(python -c 'import matemat; print(matemat.__version__)')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
test:
|
test:
|
||||||
stage: test
|
stage: test
|
||||||
script:
|
script:
|
||||||
- pip3 install -r requirements.txt
|
- pip3 install -e .
|
||||||
- sudo -u matemat python3 -m coverage run --rcfile=setup.cfg -m unittest discover matemat
|
- sudo -u matemat python3.6 -m coverage run --rcfile=setup.cfg -m unittest discover matemat
|
||||||
- sudo -u matemat python3 -m coverage combine
|
- sudo -u matemat python3.6 -m coverage combine
|
||||||
- sudo -u matemat python3 -m coverage report --rcfile=setup.cfg
|
- sudo -u matemat python3.6 -m coverage report --rcfile=setup.cfg
|
||||||
|
|
||||||
codestyle:
|
codestyle:
|
||||||
stage: test
|
stage: test
|
||||||
script:
|
script:
|
||||||
- pip3 install -r requirements.txt
|
- pip3 install -e .
|
||||||
- sudo -u matemat pycodestyle matemat
|
- sudo -u matemat pycodestyle matemat
|
||||||
|
|
||||||
build:
|
|
||||||
|
|
||||||
|
build_docker:
|
||||||
stage: build
|
stage: build
|
||||||
script:
|
script:
|
||||||
- docker build -t "registry.gitlab.com/s3lph/matemat:$(git rev-parse HEAD)" .
|
- docker build -t "registry.gitlab.com/s3lph/matemat:$CI_COMMIT_SHA" -f package/docker/Dockerfile .
|
||||||
- docker tag "registry.gitlab.com/s3lph/matemat:$CI_COMMIT_SHA" "registry.gitlab.com/s3lph/matemat:latest-$([[ $CI_COMMIT_REF_NAME == "master" ]] && echo stable || echo staging)"
|
- docker tag "registry.gitlab.com/s3lph/matemat:$CI_COMMIT_SHA" "registry.gitlab.com/s3lph/matemat:$CI_COMMIT_REF_NAME"
|
||||||
|
- if [[ -n "$CI_COMMIT_TAG" ]]; then docker tag "registry.gitlab.com/s3lph/matemat:$CI_COMMIT_SHA" "registry.gitlab.com/s3lph/matemat:$CI_COMMIT_TAG"; fi
|
||||||
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_TOKEN registry.gitlab.com
|
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_TOKEN registry.gitlab.com
|
||||||
- docker push "registry.gitlab.com/s3lph/matemat:$CI_COMMIT_SHA"
|
- docker push "registry.gitlab.com/s3lph/matemat:$CI_COMMIT_SHA"
|
||||||
- docker push registry.gitlab.com/s3lph/matemat:latest-$([[ $CI_COMMIT_REF_NAME == "master" ]] && echo stable || echo staging)
|
- docker push "registry.gitlab.com/s3lph/matemat:$CI_COMMIT_REF_NAME"
|
||||||
|
- if [[ -n "$CI_COMMIT_TAG" ]]; then docker push "registry.gitlab.com/s3lph/matemat:$CI_COMMIT_TAG"; fi
|
||||||
only:
|
only:
|
||||||
- staging
|
- staging
|
||||||
- master
|
- tags
|
||||||
|
|
||||||
|
build_wheel:
|
||||||
|
stage: build
|
||||||
|
script:
|
||||||
|
- python3.6 setup.py egg_info bdist_wheel
|
||||||
|
- cd dist
|
||||||
|
- sha256sum *.whl > SHA256SUMS
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- "dist/*.whl"
|
||||||
|
- dist/SHA256SUMS
|
||||||
|
only:
|
||||||
|
- tags
|
||||||
|
|
||||||
|
build_debian:
|
||||||
|
stage: build
|
||||||
|
script:
|
||||||
|
# The Python package name provided by the python3-magic Debian package is "python-magic" rather than "file-magic".
|
||||||
|
- sed -re 's/file-magic/python-magic/' -i setup.py
|
||||||
|
- echo -n > package/debian/matemat/usr/share/doc/matemat/changelog
|
||||||
|
- |
|
||||||
|
for version in "$(cat CHANGELOG.md | grep '<!-- BEGIN CHANGES' | cut -d ' ' -f 4)"; do
|
||||||
|
echo "matemat (${version}-1); urgency=medium\n" >> package/debian/matemat/usr/share/doc/matemat/changelog
|
||||||
|
cat CHANGELOG.md | grep -A 1000 "<"'!'"-- BEGIN CHANGES ${version} -->" | grep -B 1000 "<"'!'"-- END CHANGES ${version} -->" | tail -n +2 | head -n -1 | sed -re 's/^-/ */g' >> package/debian/matemat/usr/share/doc/matemat/changelog
|
||||||
|
echo "\n -- ${PACKAGE_AUTHOR} $(date -R)\n" >> package/debian/matemat/usr/share/doc/matemat/changelog
|
||||||
|
done
|
||||||
|
- gzip -9n package/debian/matemat/usr/share/doc/matemat/changelog
|
||||||
|
- cp -r static/ package/debian/matemat/usr/lib/matemat/static/
|
||||||
|
- cp -r templates/ package/debian/matemat/usr/lib/matemat/templates/
|
||||||
|
- python3.6 setup.py egg_info install --root=package/debian/matemat/ --prefix=/usr --optimize=1
|
||||||
|
- cd package/debian
|
||||||
|
- mkdir -p matemat/usr/lib/python3/dist-packages/
|
||||||
|
- rsync -a matemat/usr/lib/python3.6/site-packages/ matemat/usr/lib/python3/dist-packages/
|
||||||
|
- rm -rf matemat/usr/lib/python3.6/
|
||||||
|
- find matemat/usr/lib/python3/dist-packages -name __pycache__ -exec rm -r {} \; 2>/dev/null || true
|
||||||
|
- find matemat/usr/lib/python3/dist-packages -name '*.pyc' -exec rm {} \;
|
||||||
|
- mv matemat/usr/bin/matemat matemat/usr/lib/matemat/matemat
|
||||||
|
- rm -rf matemat/usr/bin
|
||||||
|
- sed -re 's$#!/usr/local/bin/python3.6$#!/usr/bin/python3$' -i matemat/usr/lib/matemat/matemat
|
||||||
|
- find matemat -type f -exec chmod 0644 {} \;
|
||||||
|
- find matemat -type d -exec chmod 755 {} \;
|
||||||
|
- chmod +x matemat/usr/lib/matemat/matemat matemat/DEBIAN/postinst matemat/DEBIAN/prerm matemat/DEBIAN/postrm
|
||||||
|
- dpkg-deb --build matemat
|
||||||
|
- mv matemat.deb "matemat_${MATEMAT_VERSION}-1_all.deb"
|
||||||
|
- sudo -u nobody lintian "matemat_${MATEMAT_VERSION}-1_all.deb"
|
||||||
|
- sha256sum *.deb > SHA256SUMS
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- "package/debian/*.deb"
|
||||||
|
- package/debian/SHA256SUMS
|
||||||
|
only:
|
||||||
|
- tags
|
||||||
|
|
||||||
|
build_archlinux:
|
||||||
|
stage: build
|
||||||
|
image: archlinux/base:latest # Use an archlinux image instead of the customized debian image.
|
||||||
|
script:
|
||||||
|
- pacman -Sy --noconfirm python python-setuptools python-pip python-wheel python-jinja python-pillow python-magic base-devel
|
||||||
|
- export MATEMAT_VERSION=$(python -c 'import matemat; print(matemat.__version__)')
|
||||||
|
- cp -r static/ package/archlinux/matemat/usr/lib/matemat/static/
|
||||||
|
- cp -r templates/ package/archlinux/matemat/usr/lib/matemat/templates/
|
||||||
|
- python3 setup.py egg_info -d -b +master install --root=package/archlinux/matemat/ --prefix=/usr --optimize=1
|
||||||
|
- cd package/archlinux
|
||||||
|
- mv matemat/usr/bin/matemat matemat/usr/lib/matemat/matemat
|
||||||
|
- rm -rf matemat/usr/bin
|
||||||
|
- sed -re "s/__VERSION__/${MATEMAT_VERSION}/g" -i PKGBUILD
|
||||||
|
- sudo -u nobody makepkg -c
|
||||||
|
- sha256sum *.pkg.tar.xz > SHA256SUMS
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- "package/archlinux/*.pkg.tar.xz"
|
||||||
|
- package/archlinux/SHA256SUMS
|
||||||
|
only:
|
||||||
|
- tags
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
staging:
|
staging:
|
||||||
stage: staging
|
stage: deploy
|
||||||
script:
|
script:
|
||||||
- eval $(ssh-agent -s)
|
- eval $(ssh-agent -s)
|
||||||
- ssh-add - <<<"$STAGING_SSH_PRIVATE_KEY"
|
- ssh-add - <<<"$STAGING_SSH_PRIVATE_KEY"
|
||||||
|
@ -43,3 +130,12 @@ staging:
|
||||||
url: https://matemat.kernelpanic.lol/
|
url: https://matemat.kernelpanic.lol/
|
||||||
only:
|
only:
|
||||||
- staging
|
- staging
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
release:
|
||||||
|
stage: deploy
|
||||||
|
script:
|
||||||
|
- python package/release.py
|
||||||
|
only:
|
||||||
|
- tags
|
||||||
|
|
1
CHANGELOG.md
Normal file
1
CHANGELOG.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
# Matemat Changelog
|
|
@ -16,7 +16,7 @@ This project intends to provide a well-tested and maintainable alternative to
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
- Python 3 (>=3.7)
|
- Python 3 (>=3.6)
|
||||||
- Python dependencies:
|
- Python dependencies:
|
||||||
- file-magic
|
- file-magic
|
||||||
- jinja2
|
- jinja2
|
||||||
|
|
2
doc
2
doc
|
@ -1 +1 @@
|
||||||
Subproject commit 411880ae72b3a2204fed4b945bdb3a15d3ece364
|
Subproject commit 0fcf4244f90d93cbc7be8e670aeaa30288d24ae3
|
|
@ -1,2 +1,2 @@
|
||||||
|
|
||||||
__version__ = '2.0'
|
__version__ = '0.1'
|
||||||
|
|
|
@ -3,14 +3,15 @@ from typing import Any, Dict, Iterable, Union
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from matemat.webserver import MatematWebserver
|
||||||
from matemat.webserver import parse_config_file
|
from matemat.webserver import parse_config_file
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
# Those imports are actually needed, as they implicitly register pagelets.
|
# Those imports are actually needed, as they implicitly register pagelets.
|
||||||
# noinspection PyUnresolvedReferences
|
# noinspection PyUnresolvedReferences
|
||||||
from matemat.webserver.pagelets import *
|
from matemat.webserver.pagelets import *
|
||||||
from matemat.webserver import MatematWebserver
|
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
# Use config file name from command line, if present
|
# Use config file name from command line, if present
|
||||||
configfile: Union[str, Iterable[str]] = '/etc/matemat.conf'
|
configfile: Union[str, Iterable[str]] = '/etc/matemat.conf'
|
||||||
if len(sys.argv) > 1:
|
if len(sys.argv) > 1:
|
||||||
|
@ -21,3 +22,7 @@ if __name__ == '__main__':
|
||||||
|
|
||||||
# Start the web server
|
# Start the web server
|
||||||
MatematWebserver(**config).start()
|
MatematWebserver(**config).start()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
from typing import Any, Dict, List, Optional, Tuple, Type
|
from typing import Any, Dict, List, Optional, Tuple, Type
|
||||||
|
|
||||||
import crypt
|
import crypt
|
||||||
|
@ -39,7 +38,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
|
||||||
|
|
|
@ -1,22 +1,31 @@
|
||||||
|
|
||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Product:
|
class Product:
|
||||||
"""
|
"""
|
||||||
Representation of a product offered by the Matemat, with a name, prices for users, and the number of items
|
Representation of a product offered by the Matemat, with a name, prices for users, and the number of items
|
||||||
currently in stock.
|
currently in stock.
|
||||||
|
|
||||||
:param id: The product ID in the database.
|
:param _id: The product ID in the database.
|
||||||
:param name: The product's name.
|
:param name: The product's name.
|
||||||
:param price_member: The price of a unit of this product for users marked as "members".
|
: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 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.
|
:param stock: The number of items of this product currently in stock.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
id: int
|
def __init__(self, _id: int, name: str, price_member: int, price_non_member: int, stock: int) -> None:
|
||||||
name: str
|
self.id: int = _id
|
||||||
price_member: int
|
self.name: str = name
|
||||||
price_non_member: int
|
self.price_member: int = price_member
|
||||||
stock: int
|
self.price_non_member: int = price_non_member
|
||||||
|
self.stock: int = stock
|
||||||
|
|
||||||
|
def __eq__(self, other) -> bool:
|
||||||
|
if not isinstance(other, Product):
|
||||||
|
return False
|
||||||
|
return self.id == other.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
|
||||||
|
|
||||||
|
def __hash__(self) -> int:
|
||||||
|
return hash((self.id, self.name, self.price_member, self.price_non_member, self.stock))
|
||||||
|
|
|
@ -1,17 +1,42 @@
|
||||||
|
|
||||||
from typing import List
|
from typing import List
|
||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from matemat.db.primitives import User, Transaction
|
from matemat.db.primitives import User, Transaction
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Receipt:
|
class Receipt:
|
||||||
|
"""
|
||||||
|
Representation of a receipt for a user and a given timespan.
|
||||||
|
|
||||||
id: int
|
:param _id: The receipt ID in the database.
|
||||||
transactions: List[Transaction]
|
:param transactions: The list of transactions on this receipt.
|
||||||
user: User
|
:param user: The user for whom this receipt was issued.
|
||||||
from_date: datetime
|
:param from_date: The beginning of the time span this receipt covers.
|
||||||
to_date: datetime
|
:param to_date: The end of the time span this receipt covers.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
_id: int,
|
||||||
|
transactions: List[Transaction],
|
||||||
|
user: User,
|
||||||
|
from_date: datetime,
|
||||||
|
to_date: datetime) -> None:
|
||||||
|
self.id: int = _id
|
||||||
|
self.transactions: List[Transaction] = transactions
|
||||||
|
self.user: User = user
|
||||||
|
self.from_date: datetime = from_date
|
||||||
|
self.to_date: datetime = to_date
|
||||||
|
|
||||||
|
def __eq__(self, other) -> bool:
|
||||||
|
if not isinstance(other, Receipt):
|
||||||
|
return False
|
||||||
|
return self.id == other.id and \
|
||||||
|
self.transactions == other.transactions and \
|
||||||
|
self.user == other.user and \
|
||||||
|
self.from_date == other.from_date and \
|
||||||
|
self.to_date == other.to_date
|
||||||
|
|
||||||
|
def __hash__(self) -> int:
|
||||||
|
return hash((self.id, self.transactions, self.user, self.from_date, self.to_date))
|
||||||
|
|
|
@ -11,7 +11,7 @@ class ReceiptPreference(Enum):
|
||||||
A user's preference for the frequency of receiving receipts.
|
A user's preference for the frequency of receiving receipts.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __new__(cls, *args, **kwargs):
|
def __new__(cls, *args, **kwargs) -> 'ReceiptPreference':
|
||||||
e = object.__new__(cls)
|
e = object.__new__(cls)
|
||||||
# The enum's internal value
|
# The enum's internal value
|
||||||
e._value_: int = args[0]
|
e._value_: int = args[0]
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
@ -8,14 +7,23 @@ from matemat.db.primitives import User
|
||||||
from matemat.util.currency_format import format_chf
|
from matemat.util.currency_format import format_chf
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class Transaction:
|
class Transaction:
|
||||||
|
"""
|
||||||
|
Representation of a generic transaction involving an user and an amount of money.
|
||||||
|
|
||||||
id: int
|
:param _id: The transaction ID in the database.
|
||||||
user: User
|
:param user: The user affected by this transaction.
|
||||||
value: int
|
:param value: The monetary value of this transaction.
|
||||||
old_balance: int
|
:param old_balance: The balance on the user's account before this transaction.
|
||||||
date: datetime
|
:param date: The date of this transaction.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, _id: int, user: User, value: int, old_balance: int, date: datetime) -> None:
|
||||||
|
self.id: int = _id
|
||||||
|
self.user: User = user
|
||||||
|
self.value: int = value
|
||||||
|
self.old_balance: int = old_balance
|
||||||
|
self.date: datetime = date
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def receipt_date(self) -> str:
|
def receipt_date(self) -> str:
|
||||||
|
@ -37,30 +45,101 @@ class Transaction:
|
||||||
def receipt_message(self) -> Optional[str]:
|
def receipt_message(self) -> Optional[str]:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def __eq__(self, other) -> bool:
|
||||||
|
if not isinstance(other, Transaction):
|
||||||
|
return False
|
||||||
|
return self.id == other.id and \
|
||||||
|
self.user == other.user and \
|
||||||
|
self.value == other.value and \
|
||||||
|
self.old_balance == other.old_balance and \
|
||||||
|
self.date == other.date
|
||||||
|
|
||||||
|
def __hash__(self) -> int:
|
||||||
|
return hash((self.id, self.user, self.value, self.old_balance, self.date))
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class Consumption(Transaction):
|
class Consumption(Transaction):
|
||||||
|
"""
|
||||||
|
Representation of a consumption involving an user, a product and an amount of money.
|
||||||
|
|
||||||
product: str
|
:param _id: The transaction ID in the database.
|
||||||
|
:param user: The user affected by this transaction.
|
||||||
|
:param value: The (negative) price of the product that was bought.
|
||||||
|
:param old_balance: The balance on the user's account before this transaction.
|
||||||
|
:param date: The date of this transaction.
|
||||||
|
:param product: The name of the product that was bought.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, _id: int, user: User, value: int, old_balance: int, date: datetime, product: str) -> None:
|
||||||
|
super().__init__(_id, user, value, old_balance, date)
|
||||||
|
self.product: str = product
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def receipt_description(self) -> str:
|
def receipt_description(self) -> str:
|
||||||
return self.product
|
return self.product
|
||||||
|
|
||||||
|
def __eq__(self, other) -> bool:
|
||||||
|
if not isinstance(other, Consumption):
|
||||||
|
return False
|
||||||
|
return super().__eq__(other) and \
|
||||||
|
self.product == other.product
|
||||||
|
|
||||||
|
def __hash__(self) -> int:
|
||||||
|
return hash((super().__hash__(), self.product))
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class Deposit(Transaction):
|
class Deposit(Transaction):
|
||||||
|
"""
|
||||||
|
Representation of a deposit involving an user and an amount of money.
|
||||||
|
|
||||||
|
:param _id: The transaction ID in the database.
|
||||||
|
:param user: The user affected by this transaction.
|
||||||
|
:param value: The amount of money that was deposited on the account.
|
||||||
|
:param old_balance: The balance on the user's account before this transaction.
|
||||||
|
:param date: The date of this transaction.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, _id: int, user: User, value: int, old_balance: int, date: datetime) -> None:
|
||||||
|
super().__init__(_id, user, value, old_balance, date)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def receipt_description(self) -> str:
|
def receipt_description(self) -> str:
|
||||||
return 'Deposit'
|
return 'Deposit'
|
||||||
|
|
||||||
|
def __eq__(self, other) -> bool:
|
||||||
|
if not isinstance(other, Deposit):
|
||||||
|
return False
|
||||||
|
return super().__eq__(other)
|
||||||
|
|
||||||
|
def __hash__(self) -> int:
|
||||||
|
return super().__hash__()
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class Modification(Transaction):
|
class Modification(Transaction):
|
||||||
|
"""
|
||||||
|
Representation of a administrative account balance modification. Involves the affected user, the agent that
|
||||||
|
performed the modification and optionally a message for the reason of the modification.
|
||||||
|
|
||||||
agent: str
|
:param _id: The transaction ID in the database.
|
||||||
reason: Optional[str]
|
:param user: The user affected by this transaction.
|
||||||
|
:param value: The amount of money that was deposited on the account.
|
||||||
|
:param old_balance: The balance on the user's account before this transaction.
|
||||||
|
:param date: The date of this transaction.
|
||||||
|
:param agent: The username of the agent performing the modification.
|
||||||
|
:param reason: The reason for this modification, as provided by the agent.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
_id: int,
|
||||||
|
user: User,
|
||||||
|
value: int,
|
||||||
|
old_balance: int,
|
||||||
|
date: datetime,
|
||||||
|
agent: str,
|
||||||
|
reason: Optional[str]) -> None:
|
||||||
|
super().__init__(_id, user, value, old_balance, date)
|
||||||
|
self.agent: str = agent
|
||||||
|
self.reason: Optional[str] = reason
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def receipt_description(self) -> str:
|
def receipt_description(self) -> str:
|
||||||
|
@ -72,3 +151,13 @@ class Modification(Transaction):
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
return f'Reason: «{self.reason}»'
|
return f'Reason: «{self.reason}»'
|
||||||
|
|
||||||
|
def __eq__(self, other) -> bool:
|
||||||
|
if not isinstance(other, Modification):
|
||||||
|
return False
|
||||||
|
return super().__eq__(other) and \
|
||||||
|
self.agent == other.agent and \
|
||||||
|
self.reason == other.reason
|
||||||
|
|
||||||
|
def __hash__(self) -> int:
|
||||||
|
return hash((super().__hash__(), self.agent, self.reason))
|
||||||
|
|
|
@ -1,30 +1,50 @@
|
||||||
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from matemat.db.primitives.ReceiptPreference import ReceiptPreference
|
from matemat.db.primitives.ReceiptPreference import ReceiptPreference
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class User:
|
class User:
|
||||||
"""
|
"""
|
||||||
Representation of a user registered with the Matemat, with a name, e-mail address (optional), whether the user is a
|
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
|
member of the organization the Matemat instance is used in, whether the user is an administrator, and the user's
|
||||||
account balance.
|
account balance.
|
||||||
|
|
||||||
:param id: The user ID in the database.
|
:param _id: The user ID in the database.
|
||||||
:param username: The user's name.
|
:param name: The user's name.
|
||||||
:param balance: The balance of the user's account.
|
:param balance: The balance of the user's account.
|
||||||
:param email: The user's e-mail address (optional).
|
:param email: The user's e-mail address (optional).
|
||||||
:param admin: Whether the user is an administrator.
|
:param is_admin: Whether the user is an administrator.
|
||||||
:param member: Whether the user is a member.
|
:param is_member: Whether the user is a member.
|
||||||
:param receipt_pref: The user's preference on how often to receive transaction receipts.
|
:param receipt_pref: The user's preference on how often to receive transaction receipts.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
id: int
|
def __init__(self,
|
||||||
name: str
|
_id: int,
|
||||||
balance: int
|
name: str,
|
||||||
email: Optional[str] = None
|
balance: int,
|
||||||
is_admin: bool = False
|
email: Optional[str] = None,
|
||||||
is_member: bool = False
|
is_admin: bool = False,
|
||||||
receipt_pref: ReceiptPreference = ReceiptPreference.NONE
|
is_member: bool = False,
|
||||||
|
receipt_pref: ReceiptPreference = ReceiptPreference.NONE) -> None:
|
||||||
|
self.id: int = _id
|
||||||
|
self.name: str = name
|
||||||
|
self.balance: int = balance
|
||||||
|
self.email: Optional[str] = email
|
||||||
|
self.is_admin: bool = is_admin
|
||||||
|
self.is_member: bool = is_member
|
||||||
|
self.receipt_pref: ReceiptPreference = receipt_pref
|
||||||
|
|
||||||
|
def __eq__(self, other) -> bool:
|
||||||
|
if not isinstance(other, User):
|
||||||
|
return False
|
||||||
|
return self.id == other.id and \
|
||||||
|
self.name == other.name and \
|
||||||
|
self.balance == other.balance and \
|
||||||
|
self.email == other.email and \
|
||||||
|
self.is_admin == other.is_admin and \
|
||||||
|
self.is_member == other.is_member and \
|
||||||
|
self.receipt_pref == other.receipt_pref
|
||||||
|
|
||||||
|
def __hash__(self) -> int:
|
||||||
|
return hash((self.id, self.name, self.balance, self.email, self.is_admin, self.is_member, self.receipt_pref))
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
from matemat.exceptions import DatabaseConsistencyError
|
from matemat.exceptions import DatabaseConsistencyError
|
||||||
|
@ -47,7 +46,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
|
||||||
|
|
||||||
|
|
|
@ -20,10 +20,13 @@ def add_months(d: datetime, months: int) -> datetime:
|
||||||
days: int = 0
|
days: int = 0
|
||||||
# Iterate the months between the passed date and the target month
|
# Iterate the months between the passed date and the target month
|
||||||
for i in range(months):
|
for i in range(months):
|
||||||
days += calendar.monthlen(*nextmonth)
|
days += calendar.monthrange(*nextmonth)[1]
|
||||||
nextmonth = calendar.nextmonth(*nextmonth)
|
if nextmonth[1] == 12:
|
||||||
|
nextmonth = nextmonth[0] + 1, 1
|
||||||
|
else:
|
||||||
|
nextmonth = nextmonth[0], nextmonth[1] + 1
|
||||||
# Set the day of month temporarily to 1, then add the day offset to reach the 1st of the target month
|
# Set the day of month temporarily to 1, then add the day offset to reach the 1st of the target month
|
||||||
newdate: datetime = d.replace(day=1) + timedelta(days=days)
|
newdate: datetime = d.replace(day=1) + timedelta(days=days)
|
||||||
# Re-set the day of month to the intended value, but capped by the max. day in the target month
|
# Re-set the day of month to the intended value, but capped by the max. day in the target month
|
||||||
newdate = newdate.replace(day=min(d.day, calendar.monthlen(newdate.year, newdate.month)))
|
newdate = newdate.replace(day=min(d.day, calendar.monthrange(newdate.year, newdate.month)[1]))
|
||||||
return newdate
|
return newdate
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
from typing import Dict, Iterator, List, Tuple, Union
|
from typing import Dict, Iterator, List, Tuple, Union
|
||||||
|
|
||||||
|
|
||||||
|
@ -25,7 +24,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.
|
||||||
|
|
||||||
|
@ -41,7 +40,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.
|
||||||
|
@ -279,7 +278,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.
|
||||||
|
|
29
package/archlinux/PKGBUILD
Normal file
29
package/archlinux/PKGBUILD
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
|
||||||
|
# Maintainer: s3lph <account-gitlab-ideynizv@kernelpanic.lol>
|
||||||
|
|
||||||
|
pkgname=matemat
|
||||||
|
pkgver=0.1
|
||||||
|
pkgrel=1
|
||||||
|
arch=('any')
|
||||||
|
|
||||||
|
pkgdesc='A soda machine stock-keeping webservice'
|
||||||
|
url='https://gitlab.com/s3lph/matemat'
|
||||||
|
licence=('MIT')
|
||||||
|
|
||||||
|
depends=(
|
||||||
|
'python'
|
||||||
|
'python-jinja'
|
||||||
|
'python-pillow'
|
||||||
|
'python-magic'
|
||||||
|
'file'
|
||||||
|
)
|
||||||
|
|
||||||
|
backup=(
|
||||||
|
'etc/matemat.conf'
|
||||||
|
)
|
||||||
|
|
||||||
|
install=$pkgname.install
|
||||||
|
|
||||||
|
package() {
|
||||||
|
cp -r ../matemat/* ../pkg/matemat/
|
||||||
|
}
|
31
package/archlinux/matemat.install
Executable file
31
package/archlinux/matemat.install
Executable file
|
@ -0,0 +1,31 @@
|
||||||
|
|
||||||
|
post_install() {
|
||||||
|
|
||||||
|
if ! getent group matemat >/dev/null; then
|
||||||
|
groupadd --system matemat
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! getent passwd matemat >/dev/null; then
|
||||||
|
useradd --system --create-home --gid matemat --home-dir /var/lib/matemat --shell /usr/bin/nologin matemat
|
||||||
|
fi
|
||||||
|
|
||||||
|
chown matemat:matemat /var/lib/matemat
|
||||||
|
chmod 0750 /var/lib/matemat
|
||||||
|
ln -sf /var/lib/matemat/upload /usr/lib/matemat/static/upload
|
||||||
|
|
||||||
|
systemctl daemon-reload
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
pre_remove() {
|
||||||
|
|
||||||
|
systemctl stop matemat.service
|
||||||
|
userdel matemat
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
post_remove() {
|
||||||
|
|
||||||
|
systemctl daemon-reload
|
||||||
|
|
||||||
|
}
|
32
package/archlinux/matemat/etc/matemat.conf
Normal file
32
package/archlinux/matemat/etc/matemat.conf
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
[Matemat]
|
||||||
|
|
||||||
|
# The IP address to listen on
|
||||||
|
Address=::
|
||||||
|
# The TCP port to listen on
|
||||||
|
Port=80
|
||||||
|
|
||||||
|
# The log level, one of NONE, DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||||
|
LogLevel=DEBUG
|
||||||
|
|
||||||
|
[Pagelets]
|
||||||
|
|
||||||
|
# Name of the Matemat instance, shown in the web app
|
||||||
|
InstanceName=Matemat
|
||||||
|
|
||||||
|
#
|
||||||
|
# Configure SMTP credentials
|
||||||
|
#
|
||||||
|
# SmtpFrom=matemat@example.com
|
||||||
|
# SmtpSubj=Matemat Receipt
|
||||||
|
# SmtpHost=exmaple.com
|
||||||
|
# SmtpPort=587
|
||||||
|
# SmtpUser=matemat@example.com
|
||||||
|
# SmtpPass=supersecurepassword
|
||||||
|
|
||||||
|
#
|
||||||
|
# Enable to allow users to receive receipts via email
|
||||||
|
#
|
||||||
|
# SmtpSendReceipts=1
|
||||||
|
|
||||||
|
# Add static HTTP headers in this section
|
||||||
|
# [HttpHeaders]
|
11
package/archlinux/matemat/usr/lib/matemat/matemat.conf
Normal file
11
package/archlinux/matemat/usr/lib/matemat/matemat.conf
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
[Matemat]
|
||||||
|
|
||||||
|
StaticPath=/usr/lib/matemat/static
|
||||||
|
TemplatePath=/usr/lib/matemat/templates
|
||||||
|
|
||||||
|
LogTarget=stdout
|
||||||
|
|
||||||
|
[Pagelets]
|
||||||
|
|
||||||
|
UploadDir=/var/lib/matemat/upload
|
||||||
|
DatabaseFile=/var/lib/matemat/matemat.db
|
|
@ -0,0 +1,12 @@
|
||||||
|
[Unit]
|
||||||
|
Description=matemat
|
||||||
|
After=networking.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=/usr/bin/python -m matemat /etc/matemat.conf /usr/lib/matemat/matemat.conf
|
||||||
|
User=matemat
|
||||||
|
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||||
|
NoNewPrivileges=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
1
package/debian/matemat/DEBIAN/conffiles
Normal file
1
package/debian/matemat/DEBIAN/conffiles
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/etc/matemat.conf
|
11
package/debian/matemat/DEBIAN/control
Normal file
11
package/debian/matemat/DEBIAN/control
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
Package: matemat
|
||||||
|
Version: 0.1
|
||||||
|
Maintainer: s3lph <account-gitlab-ideynizv@kernelpanic.lol>
|
||||||
|
Section: web
|
||||||
|
Priority: optional
|
||||||
|
Architecture: all
|
||||||
|
Depends: python3 (>= 3.6), python3-jinja2, python3-magic, python3-pil
|
||||||
|
Description: Soda machine stock-keeping webservice
|
||||||
|
A web service for automated stock-keeping of a soda machine written in Python.
|
||||||
|
It provides a touch-input-friendly user interface (as most input happens
|
||||||
|
through the soda machine's touch screen).
|
21
package/debian/matemat/DEBIAN/postinst
Executable file
21
package/debian/matemat/DEBIAN/postinst
Executable file
|
@ -0,0 +1,21 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [[ "$1" == "configure" ]]; then
|
||||||
|
|
||||||
|
if ! getent group matemat >/dev/null; then
|
||||||
|
groupadd --system matemat
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! getent passwd matemat >/dev/null; then
|
||||||
|
useradd --system --create-home --gid matemat --home-dir /var/lib/matemat --shell /usr/sbin/nologin matemat
|
||||||
|
fi
|
||||||
|
|
||||||
|
chown matemat:matemat /var/lib/matemat
|
||||||
|
chmod 0750 /var/lib/matemat
|
||||||
|
ln -sf /var/lib/matemat/upload /usr/lib/matemat/static/upload
|
||||||
|
|
||||||
|
systemctl daemon-reload || true
|
||||||
|
|
||||||
|
fi
|
9
package/debian/matemat/DEBIAN/postrm
Executable file
9
package/debian/matemat/DEBIAN/postrm
Executable file
|
@ -0,0 +1,9 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [[ "$1" == "remove" ]]; then
|
||||||
|
|
||||||
|
systemctl daemon-reload || true
|
||||||
|
|
||||||
|
fi
|
9
package/debian/matemat/DEBIAN/prerm
Executable file
9
package/debian/matemat/DEBIAN/prerm
Executable file
|
@ -0,0 +1,9 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [[ "$1" == "remove" ]]; then
|
||||||
|
|
||||||
|
userdel matemat
|
||||||
|
|
||||||
|
fi
|
32
package/debian/matemat/etc/matemat.conf
Normal file
32
package/debian/matemat/etc/matemat.conf
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
[Matemat]
|
||||||
|
|
||||||
|
# The IP address to listen on
|
||||||
|
Address=::
|
||||||
|
# The TCP port to listen on
|
||||||
|
Port=80
|
||||||
|
|
||||||
|
# The log level, one of NONE, DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||||
|
LogLevel=DEBUG
|
||||||
|
|
||||||
|
[Pagelets]
|
||||||
|
|
||||||
|
# Name of the Matemat instance, shown in the web app
|
||||||
|
InstanceName=Matemat
|
||||||
|
|
||||||
|
#
|
||||||
|
# Configure SMTP credentials
|
||||||
|
#
|
||||||
|
# SmtpFrom=matemat@example.com
|
||||||
|
# SmtpSubj=Matemat Receipt
|
||||||
|
# SmtpHost=exmaple.com
|
||||||
|
# SmtpPort=587
|
||||||
|
# SmtpUser=matemat@example.com
|
||||||
|
# SmtpPass=supersecurepassword
|
||||||
|
|
||||||
|
#
|
||||||
|
# Enable to allow users to receive receipts via email
|
||||||
|
#
|
||||||
|
# SmtpSendReceipts=1
|
||||||
|
|
||||||
|
# Add static HTTP headers in this section
|
||||||
|
# [HttpHeaders]
|
12
package/debian/matemat/lib/systemd/system/matemat.service
Normal file
12
package/debian/matemat/lib/systemd/system/matemat.service
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
[Unit]
|
||||||
|
Description=matemat
|
||||||
|
After=networking.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=/usr/bin/python3 -m matemat /etc/matemat.conf /usr/lib/matemat/matemat.conf
|
||||||
|
User=matemat
|
||||||
|
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||||
|
NoNewPrivileges=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
11
package/debian/matemat/usr/lib/matemat/matemat.conf
Normal file
11
package/debian/matemat/usr/lib/matemat/matemat.conf
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
[Matemat]
|
||||||
|
|
||||||
|
StaticPath=/usr/lib/matemat/static
|
||||||
|
TemplatePath=/usr/lib/matemat/templates
|
||||||
|
|
||||||
|
LogTarget=stdout
|
||||||
|
|
||||||
|
[Pagelets]
|
||||||
|
|
||||||
|
UploadDir=/var/lib/matemat/upload
|
||||||
|
DatabaseFile=/var/lib/matemat/matemat.db
|
16
package/debian/matemat/usr/share/doc/matemat/copyright
Normal file
16
package/debian/matemat/usr/share/doc/matemat/copyright
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
Copyright 2018 s3lph
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||||
|
associated documentation files (the "Software"), to deal in the Software without restriction,
|
||||||
|
including without limitation the rights to use, copy, modify, merge, publish, distribute,
|
||||||
|
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
|
||||||
|
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT
|
||||||
|
OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@ -4,9 +4,10 @@ FROM python:3.7-alpine
|
||||||
ADD . /
|
ADD . /
|
||||||
RUN mkdir -p /var/matemat/db /var/matemat/upload \
|
RUN mkdir -p /var/matemat/db /var/matemat/upload \
|
||||||
&& apk --update add libmagic zlib jpeg zlib-dev jpeg-dev build-base \
|
&& apk --update add libmagic zlib jpeg zlib-dev jpeg-dev build-base \
|
||||||
&& pip3 install -r /requirements.txt \
|
&& pip3 install -e . \
|
||||||
&& apk del zlib-dev jpeg-dev build-base \
|
&& apk del zlib-dev jpeg-dev build-base \
|
||||||
&& rm -rf /var/cache/apk /root/.cache/pip
|
&& rm -rf /var/cache/apk /root/.cache/pip \
|
||||||
|
&& rm -rf /package
|
||||||
|
|
||||||
EXPOSE 80/tcp
|
EXPOSE 80/tcp
|
||||||
CMD [ "/run.sh" ]
|
CMD [ "/run.sh" ]
|
168
package/release.py
Executable file
168
package/release.py
Executable file
|
@ -0,0 +1,168 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import urllib.request
|
||||||
|
import http.client
|
||||||
|
from urllib.error import HTTPError
|
||||||
|
|
||||||
|
|
||||||
|
def parse_changelog(tag: str) -> Optional[str]:
|
||||||
|
release_changelog: str = ''
|
||||||
|
with open('CHANGELOG.md', 'r') as f:
|
||||||
|
in_target: bool = False
|
||||||
|
done: bool = False
|
||||||
|
for line in f.readlines():
|
||||||
|
if in_target:
|
||||||
|
if f'<!-- END RELEASE {tag} -->' in line:
|
||||||
|
done = True
|
||||||
|
break
|
||||||
|
release_changelog += line
|
||||||
|
elif f'<!-- BEGIN RELEASE {tag} -->' in line:
|
||||||
|
in_target = True
|
||||||
|
continue
|
||||||
|
if not done:
|
||||||
|
return None
|
||||||
|
return release_changelog
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_job_ids(project_id: int, pipeline_id: int, api_token: str) -> Dict[str, str]:
|
||||||
|
url: str = f'https://gitlab.com/api/v4/projects/{project_id}/pipelines/{pipeline_id}/jobs'
|
||||||
|
headers: Dict[str, str] = {
|
||||||
|
'Private-Token': api_token
|
||||||
|
}
|
||||||
|
req = urllib.request.Request(url, headers=headers)
|
||||||
|
try:
|
||||||
|
resp: http.client.HTTPResponse = urllib.request.urlopen(req)
|
||||||
|
except HTTPError as e:
|
||||||
|
print(e.read().decode())
|
||||||
|
sys.exit(1)
|
||||||
|
resp_data: bytes = resp.read()
|
||||||
|
joblist: List[Dict[str, Any]] = json.loads(resp_data.decode())
|
||||||
|
|
||||||
|
jobidmap: Dict[str, str] = {}
|
||||||
|
for job in joblist:
|
||||||
|
name: str = job['name']
|
||||||
|
job_id: str = job['id']
|
||||||
|
jobidmap[name] = job_id
|
||||||
|
return jobidmap
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_single_shafile(url: str) -> str:
|
||||||
|
req = urllib.request.Request(url)
|
||||||
|
try:
|
||||||
|
resp: http.client.HTTPResponse = urllib.request.urlopen(req)
|
||||||
|
except HTTPError as e:
|
||||||
|
print(e.read().decode())
|
||||||
|
sys.exit(1)
|
||||||
|
resp_data: bytes = resp.readline()
|
||||||
|
shafile: str = resp_data.decode()
|
||||||
|
filename: str = shafile.strip().split(' ')[-1].strip()
|
||||||
|
return filename
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_wheel_url(base_url: str, job_ids: Dict[str, str]) -> Optional[Tuple[str, str]]:
|
||||||
|
mybase: str = f'{base_url}/jobs/{job_ids["build_wheel"]}/artifacts/raw'
|
||||||
|
wheel_sha_url: str = f'{mybase}/dist/SHA256SUMS'
|
||||||
|
wheel_filename: str = fetch_single_shafile(wheel_sha_url)
|
||||||
|
wheel_url: str = f'{mybase}/dist/{wheel_filename}'
|
||||||
|
return wheel_url, wheel_sha_url
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_debian_url(base_url: str, job_ids: Dict[str, str]) -> Optional[Tuple[str, str]]:
|
||||||
|
mybase: str = f'{base_url}/jobs/{job_ids["build_debian"]}/artifacts/raw'
|
||||||
|
debian_sha_url: str = f'{mybase}/package/debian/SHA256SUMS'
|
||||||
|
debian_filename: str = fetch_single_shafile(debian_sha_url)
|
||||||
|
debian_url: str = f'{mybase}/package/debian/{debian_filename}'
|
||||||
|
return debian_url, debian_sha_url
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_arch_url(base_url: str, job_ids: Dict[str, str]) -> Optional[Tuple[str, str]]:
|
||||||
|
mybase: str = f'{base_url}/jobs/{job_ids["build_archlinux"]}/artifacts/raw'
|
||||||
|
arch_sha_url: str = f'{mybase}/package/archlinux/SHA256SUMS'
|
||||||
|
arch_filename: str = fetch_single_shafile(arch_sha_url)
|
||||||
|
arch_url: str = f'{mybase}/package/archlinux/{arch_filename}'
|
||||||
|
return arch_url, arch_sha_url
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
api_token: Optional[str] = os.getenv('GITLAB_API_TOKEN')
|
||||||
|
release_tag: Optional[str] = os.getenv('CI_COMMIT_TAG')
|
||||||
|
project_name: Optional[str] = os.getenv('CI_PROJECT_PATH')
|
||||||
|
project_id: Optional[str] = os.getenv('CI_PROJECT_ID')
|
||||||
|
pipeline_id: Optional[str] = os.getenv('CI_PIPELINE_ID')
|
||||||
|
if api_token is None:
|
||||||
|
print('GITLAB_API_TOKEN is not set.', file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if release_tag is None:
|
||||||
|
print('CI_COMMIT_TAG is not set.', file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if project_name is None:
|
||||||
|
print('CI_PROJECT_PATH is not set.', file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if project_id is None:
|
||||||
|
print('CI_PROJECT_ID is not set.', file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if pipeline_id is None:
|
||||||
|
print('CI_PIPELINE_ID is not set.', file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
changelog: Optional[str] = parse_changelog(release_tag)
|
||||||
|
if changelog is None:
|
||||||
|
print('Changelog could not be parsed.', file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
job_ids: Dict[str, str] = fetch_job_ids(project_id, pipeline_id, api_token)
|
||||||
|
|
||||||
|
base_url: str = f'https://gitlab.com/{project_name}/-'
|
||||||
|
|
||||||
|
wheel_url, wheel_sha_url = fetch_wheel_url(base_url, job_ids)
|
||||||
|
debian_url, debian_sha_url = fetch_debian_url(base_url, job_ids)
|
||||||
|
arch_url, arch_sha_url = fetch_arch_url(base_url, job_ids)
|
||||||
|
|
||||||
|
augmented_changelog = f'''{changelog.strip()}
|
||||||
|
|
||||||
|
### Download
|
||||||
|
|
||||||
|
- [Python Wheel]({wheel_url}) ([sha256]({wheel_sha_url}))
|
||||||
|
- [Debian Package]({debian_url}) ([sha256]({debian_sha_url}))
|
||||||
|
- [Arch Linux Package]({arch_url}) ([sha256]({arch_sha_url}))
|
||||||
|
- Docker image: registry.gitlab.com/{project_name}:{release_tag}'''
|
||||||
|
|
||||||
|
post_body: str = json.dumps({'description': augmented_changelog})
|
||||||
|
|
||||||
|
gitlab_release_api_url: str = \
|
||||||
|
f'https://gitlab.com/api/v4/projects/{project_id}/repository/tags/{release_tag}/release'
|
||||||
|
headers: Dict[str, str] = {
|
||||||
|
'Private-Token': api_token,
|
||||||
|
'Content-Type': 'application/json; charset=utf-8'
|
||||||
|
}
|
||||||
|
|
||||||
|
request = urllib.request.Request(
|
||||||
|
gitlab_release_api_url,
|
||||||
|
post_body.encode('utf-8'),
|
||||||
|
headers=headers,
|
||||||
|
method='POST'
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
response: http.client.HTTPResponse = urllib.request.urlopen(request)
|
||||||
|
except HTTPError as e:
|
||||||
|
print(e.read().decode())
|
||||||
|
sys.exit(1)
|
||||||
|
response_bytes: bytes = response.read()
|
||||||
|
response_str: str = response_bytes.decode()
|
||||||
|
response_data: Dict[str, Any] = json.loads(response_str)
|
||||||
|
|
||||||
|
if response_data['tag_name'] != release_tag:
|
||||||
|
print('Something went wrong...', file=sys.stderr)
|
||||||
|
print(response_str, file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(response_data['description'])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
|
@ -1,3 +1 @@
|
||||||
file-magic
|
.
|
||||||
jinja2
|
|
||||||
Pillow
|
|
||||||
|
|
32
setup.py
32
setup.py
|
@ -1,12 +1,32 @@
|
||||||
from setuptools import setup
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
|
from matemat import __version__
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='matemat',
|
name='matemat',
|
||||||
version='2.0',
|
version=__version__,
|
||||||
packages=['matemat'],
|
url='https://gitlab.com/s3lph/matemat',
|
||||||
url='',
|
license='MIT',
|
||||||
license='',
|
|
||||||
author='s3lph',
|
author='s3lph',
|
||||||
author_email='',
|
author_email='',
|
||||||
description='Replacement for the original Ruby matemat software.'
|
description='Soda machine stock-keeping webservice',
|
||||||
|
long_description='''
|
||||||
|
A web service for automated stock-keeping of a soda machine written in Python.
|
||||||
|
It provides a touch-input-friendly user interface (as most input happens through the
|
||||||
|
soda machine's touch screen).
|
||||||
|
''',
|
||||||
|
|
||||||
|
packages=find_packages(exclude=['*.test']),
|
||||||
|
python_requires='>=3.6',
|
||||||
|
install_requires=[
|
||||||
|
'file-magic',
|
||||||
|
'jinja2',
|
||||||
|
'Pillow'
|
||||||
|
],
|
||||||
|
test_loader='unittest:TestLoader',
|
||||||
|
entry_points={
|
||||||
|
'console_scripts': [
|
||||||
|
'matemat = matemat.__main__:main'
|
||||||
|
]
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
|
|
||||||
# There is no buster image yet and stretch doesn't have a docker package. So let's just "upgrade" the image to 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
|
FROM python:3.6-stretch
|
||||||
|
|
||||||
RUN sed -re 's/stretch/buster/g' -i /etc/apt/sources.list \
|
RUN sed -re 's/stretch/buster/g' -i /etc/apt/sources.list \
|
||||||
&& useradd -d /home/matemat -m matemat \
|
&& useradd -d /home/matemat -m matemat \
|
||||||
|
@ -8,8 +8,8 @@ RUN sed -re 's/stretch/buster/g' -i /etc/apt/sources.list \
|
||||||
&& chown matemat:matemat -R /var/matemat/db \
|
&& chown matemat:matemat -R /var/matemat/db \
|
||||||
&& chown matemat:matemat -R /var/matemat/upload \
|
&& chown matemat:matemat -R /var/matemat/upload \
|
||||||
&& apt-get update -qy \
|
&& apt-get update -qy \
|
||||||
&& apt-get install -y --no-install-recommends file sudo openssh-client git docker.io build-essential \
|
&& apt-get install -y --no-install-recommends file sudo openssh-client git docker.io build-essential lintian rsync \
|
||||||
&& python3.7 -m pip install coverage wheel pycodestyle mypy \
|
&& python3.6 -m pip install coverage wheel pycodestyle mypy \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
WORKDIR /home/matemat
|
WORKDIR /home/matemat
|
||||||
|
|
147
touchkey.html
147
touchkey.html
|
@ -1,147 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<style>
|
|
||||||
svg {
|
|
||||||
width: 600px;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<h1>Welcome, {{username}}</h1>
|
|
||||||
|
|
||||||
<svg id="svg" width="400" height="400">
|
|
||||||
<circle class="c" id="0" cx="12.5%" cy="12.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
|
||||||
<circle class="c" id="1" cx="37.5%" cy="12.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
|
||||||
<circle class="c" id="2" cx="62.5%" cy="12.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
|
||||||
<circle class="c" id="3" cx="87.5%" cy="12.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
|
||||||
|
|
||||||
<circle class="c" id="4" cx="12.5%" cy="37.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
|
||||||
<circle class="c" id="5" cx="37.5%" cy="37.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
|
||||||
<circle class="c" id="6" cx="62.5%" cy="37.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
|
||||||
<circle class="c" id="7" cx="87.5%" cy="37.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
|
||||||
|
|
||||||
<circle class="c" id="8" cx="12.5%" cy="62.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
|
||||||
<circle class="c" id="9" cx="37.5%" cy="62.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
|
||||||
<circle class="c" id="a" cx="62.5%" cy="62.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
|
||||||
<circle class="c" id="b" cx="87.5%" cy="62.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
|
||||||
|
|
||||||
<circle class="c" id="c" cx="12.5%" cy="87.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
|
||||||
<circle class="c" id="d" cx="37.5%" cy="87.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
|
||||||
<circle class="c" id="e" cx="62.5%" cy="87.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
|
||||||
<circle class="c" id="f" cx="87.5%" cy="87.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
|
|
||||||
HTMLCollection.prototype.forEach = Array.prototype.forEach;
|
|
||||||
HTMLCollection.prototype.slice = Array.prototype.slice;
|
|
||||||
|
|
||||||
mouseDown = false;
|
|
||||||
currentStroke = null;
|
|
||||||
strokeId = 0;
|
|
||||||
doneMap = {};
|
|
||||||
enteredKey = '';
|
|
||||||
|
|
||||||
svg = document.getElementById('svg');
|
|
||||||
|
|
||||||
drawLine = (fromX, fromY, toX, toY) => {
|
|
||||||
var line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
|
||||||
var id = 'l-' + (strokeId++);
|
|
||||||
var idAttr = document.createAttribute('id');
|
|
||||||
var classAttr = document.createAttribute('class');
|
|
||||||
var x1attr = document.createAttribute('x1');
|
|
||||||
var y1attr = document.createAttribute('y1');
|
|
||||||
var x2attr = document.createAttribute('x2');
|
|
||||||
var y2attr = document.createAttribute('y2');
|
|
||||||
var styleAttr = document.createAttribute('style');
|
|
||||||
idAttr.value = id;
|
|
||||||
classAttr.value = 'l';
|
|
||||||
x1attr.value = fromX;
|
|
||||||
y1attr.value = fromY;
|
|
||||||
x2attr.value = toX;
|
|
||||||
y2attr.value = toY;
|
|
||||||
styleAttr.value = 'stroke: grey; stroke-width: 5%; stroke-linecap: round';
|
|
||||||
line.setAttributeNode(idAttr);
|
|
||||||
line.setAttributeNode(classAttr);
|
|
||||||
line.setAttributeNode(x1attr);
|
|
||||||
line.setAttributeNode(y1attr);
|
|
||||||
line.setAttributeNode(x2attr);
|
|
||||||
line.setAttributeNode(y2attr);
|
|
||||||
line.setAttributeNode(styleAttr);
|
|
||||||
svg.appendChild(line);
|
|
||||||
return id;
|
|
||||||
};
|
|
||||||
|
|
||||||
svg.onmousedown = (ev) => {
|
|
||||||
var svgrect = svg.getBoundingClientRect();
|
|
||||||
var minId = '';
|
|
||||||
var minDist = Infinity;
|
|
||||||
var minx = 0;
|
|
||||||
var miny = 0;
|
|
||||||
doneMap = {};
|
|
||||||
document.getElementsByClassName('c').forEach((circle) => {
|
|
||||||
var x = parseFloat(circle.getAttribute('cx'))/100.0 * svgrect.width;
|
|
||||||
var y = parseFloat(circle.getAttribute('cy'))/100.0 * svgrect.height;
|
|
||||||
var dist = Math.pow(ev.offsetX - x, 2) + Math.pow(ev.offsetY - y, 2);
|
|
||||||
if (dist < minDist) {
|
|
||||||
minDist = dist;
|
|
||||||
minId = circle.id;
|
|
||||||
minx = x;
|
|
||||||
miny = y;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
var minNode = svg.getElementById(minId);
|
|
||||||
currentStroke = drawLine(minx, miny, ev.offsetX, ev.offsetY);
|
|
||||||
doneMap[minId] = 1;
|
|
||||||
enteredKey += minId;
|
|
||||||
};
|
|
||||||
|
|
||||||
svg.onmouseup = (ev) => {
|
|
||||||
currentStroke = null;
|
|
||||||
doneMap = {};
|
|
||||||
console.log(enteredKey);
|
|
||||||
enteredKey = '';
|
|
||||||
svg.getElementsByClassName('l').slice().reverse().forEach((line) => {
|
|
||||||
svg.removeChild(line);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
svg.onmousemove = (ev) => {
|
|
||||||
if (currentStroke != null) {
|
|
||||||
var svgrect = svg.getBoundingClientRect();
|
|
||||||
var minId = '';
|
|
||||||
var minDist = Infinity;
|
|
||||||
var minx = 0;
|
|
||||||
var miny = 0;
|
|
||||||
document.getElementsByClassName('c').forEach((circle) => {
|
|
||||||
var x = parseFloat(circle.getAttribute('cx'))/100.0 * svgrect.width;
|
|
||||||
var y = parseFloat(circle.getAttribute('cy'))/100.0 * svgrect.height;
|
|
||||||
var dist = Math.pow(ev.offsetX - x, 2) + Math.pow(ev.offsetY - y, 2);
|
|
||||||
if (dist < minDist) {
|
|
||||||
minDist = dist;
|
|
||||||
minId = circle.id;
|
|
||||||
minx = x;
|
|
||||||
miny = y;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (minDist < 2000 && !(minId in doneMap)) {
|
|
||||||
var line = svg.getElementById(currentStroke);
|
|
||||||
line.setAttribute('x2', minx);
|
|
||||||
line.setAttribute('y2', miny);
|
|
||||||
currentStroke = drawLine(minx, miny, ev.offsetX, ev.offsetY);
|
|
||||||
doneMap[minId] = 1;
|
|
||||||
enteredKey += minId;
|
|
||||||
}
|
|
||||||
var line = svg.getElementById(currentStroke);
|
|
||||||
line.setAttribute('x2', ev.offsetX);
|
|
||||||
line.setAttribute('y2', ev.offsetY);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
Loading…
Reference in a new issue