diff --git a/.gitignore b/.gitignore index ea15acd..5bda33f 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,4 @@ *.sqlite3 *.db static/upload/ -**/matemat.conf +./matemat.conf diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6e1f340..73cbd52 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,39 +1,126 @@ --- -image: s3lph/matemat-ci:20180802-02 +image: s3lph/matemat-ci:20181107-02 stages: - test - build -- staging +- deploy + + + +before_script: +- export MATEMAT_VERSION=$(python -c 'import matemat; print(matemat.__version__)') + + test: stage: test script: - - pip3 install -r requirements.txt - - sudo -u matemat python3 -m coverage run --rcfile=setup.cfg -m unittest discover matemat - - sudo -u matemat python3 -m coverage combine - - sudo -u matemat python3 -m coverage report --rcfile=setup.cfg + - pip3 install -e . + - sudo -u matemat python3.6 -m coverage run --rcfile=setup.cfg -m unittest discover matemat + - sudo -u matemat python3.6 -m coverage combine + - sudo -u matemat python3.6 -m coverage report --rcfile=setup.cfg codestyle: stage: test script: - - pip3 install -r requirements.txt + - pip3 install -e . - sudo -u matemat pycodestyle matemat -build: + + +build_docker: stage: build script: - - docker build -t "registry.gitlab.com/s3lph/matemat:$(git rev-parse HEAD)" . - - 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 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:$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 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: - 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 '" | 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: - stage: staging + stage: deploy script: - eval $(ssh-agent -s) - ssh-add - <<<"$STAGING_SSH_PRIVATE_KEY" @@ -43,3 +130,12 @@ staging: url: https://matemat.kernelpanic.lol/ only: - staging + + + +release: + stage: deploy + script: + - python package/release.py + only: + - tags diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..2f42ce0 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1 @@ +# Matemat Changelog diff --git a/README.md b/README.md index 1d75157..2902bfa 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ This project intends to provide a well-tested and maintainable alternative to ## Dependencies -- Python 3 (>=3.7) +- Python 3 (>=3.6) - Python dependencies: - file-magic - jinja2 diff --git a/doc b/doc index 411880a..0fcf424 160000 --- a/doc +++ b/doc @@ -1 +1 @@ -Subproject commit 411880ae72b3a2204fed4b945bdb3a15d3ece364 +Subproject commit 0fcf4244f90d93cbc7be8e670aeaa30288d24ae3 diff --git a/matemat/__init__.py b/matemat/__init__.py index a3332a5..373d726 100644 --- a/matemat/__init__.py +++ b/matemat/__init__.py @@ -1,2 +1,2 @@ -__version__ = '2.0' +__version__ = '0.1' diff --git a/matemat/__main__.py b/matemat/__main__.py index 8876386..b10f837 100644 --- a/matemat/__main__.py +++ b/matemat/__main__.py @@ -3,14 +3,15 @@ from typing import Any, Dict, Iterable, Union import sys +from matemat.webserver import MatematWebserver from matemat.webserver import parse_config_file -if __name__ == '__main__': - # Those imports are actually needed, as they implicitly register pagelets. - # noinspection PyUnresolvedReferences - from matemat.webserver.pagelets import * - from matemat.webserver import MatematWebserver +# Those imports are actually needed, as they implicitly register pagelets. +# noinspection PyUnresolvedReferences +from matemat.webserver.pagelets import * + +def main(): # Use config file name from command line, if present configfile: Union[str, Iterable[str]] = '/etc/matemat.conf' if len(sys.argv) > 1: @@ -21,3 +22,7 @@ if __name__ == '__main__': # Start the web server MatematWebserver(**config).start() + + +if __name__ == '__main__': + main() diff --git a/matemat/db/facade.py b/matemat/db/facade.py index 41274ad..78c51d0 100644 --- a/matemat/db/facade.py +++ b/matemat/db/facade.py @@ -1,5 +1,4 @@ -from __future__ import annotations from typing import Any, Dict, List, Optional, Tuple, Type import crypt @@ -39,7 +38,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 diff --git a/matemat/db/primitives/Product.py b/matemat/db/primitives/Product.py index 18b5003..ed196d4 100644 --- a/matemat/db/primitives/Product.py +++ b/matemat/db/primitives/Product.py @@ -1,22 +1,31 @@ -from dataclasses import dataclass - - -@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 _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. """ - id: int - name: str - price_member: int - price_non_member: int - stock: int + def __init__(self, _id: int, name: str, price_member: int, price_non_member: int, stock: int) -> None: + self.id: int = _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) -> 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)) diff --git a/matemat/db/primitives/Receipt.py b/matemat/db/primitives/Receipt.py index 2ddbe12..f11320a 100644 --- a/matemat/db/primitives/Receipt.py +++ b/matemat/db/primitives/Receipt.py @@ -1,17 +1,42 @@ from typing import List -from dataclasses import dataclass from datetime import datetime from matemat.db.primitives import User, Transaction -@dataclass class Receipt: + """ + Representation of a receipt for a user and a given timespan. - id: int - transactions: List[Transaction] - user: User - from_date: datetime - to_date: datetime + :param _id: The receipt ID in the database. + :param transactions: The list of transactions on this receipt. + :param user: The user for whom this receipt was issued. + :param from_date: The beginning of the time span this receipt covers. + :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)) diff --git a/matemat/db/primitives/ReceiptPreference.py b/matemat/db/primitives/ReceiptPreference.py index fb15f18..8afaab5 100644 --- a/matemat/db/primitives/ReceiptPreference.py +++ b/matemat/db/primitives/ReceiptPreference.py @@ -11,7 +11,7 @@ class ReceiptPreference(Enum): 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) # The enum's internal value e._value_: int = args[0] diff --git a/matemat/db/primitives/Transaction.py b/matemat/db/primitives/Transaction.py index 964b835..061f65f 100644 --- a/matemat/db/primitives/Transaction.py +++ b/matemat/db/primitives/Transaction.py @@ -1,6 +1,5 @@ from typing import Optional -from dataclasses import dataclass from datetime import datetime @@ -8,14 +7,23 @@ from matemat.db.primitives import User from matemat.util.currency_format import format_chf -@dataclass(frozen=True) class Transaction: + """ + Representation of a generic transaction involving an user and an amount of money. - id: int - user: User - value: int - old_balance: int - date: datetime + :param _id: The transaction ID in the database. + :param user: The user affected by this transaction. + :param value: The monetary value of this transaction. + :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: + self.id: int = _id + self.user: User = user + self.value: int = value + self.old_balance: int = old_balance + self.date: datetime = date @property def receipt_date(self) -> str: @@ -37,30 +45,101 @@ class Transaction: def receipt_message(self) -> Optional[str]: 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): + """ + 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 def receipt_description(self) -> str: 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): + """ + 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 def receipt_description(self) -> str: 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): + """ + 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 - reason: Optional[str] + :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. + :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 def receipt_description(self) -> str: @@ -72,3 +151,13 @@ class Modification(Transaction): return None else: 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)) diff --git a/matemat/db/primitives/User.py b/matemat/db/primitives/User.py index 3bf25cc..ce203d7 100644 --- a/matemat/db/primitives/User.py +++ b/matemat/db/primitives/User.py @@ -1,30 +1,50 @@ from typing import Optional -from dataclasses import dataclass from matemat.db.primitives.ReceiptPreference import ReceiptPreference -@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 _id: The user ID in the database. + :param name: 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. + :param is_admin: Whether the user is an administrator. + :param is_member: Whether the user is a member. :param receipt_pref: The user's preference on how often to receive transaction receipts. """ - id: int - name: str - balance: int - email: Optional[str] = None - is_admin: bool = False - is_member: bool = False - receipt_pref: ReceiptPreference = ReceiptPreference.NONE + def __init__(self, + _id: int, + name: str, + balance: int, + email: Optional[str] = None, + is_admin: bool = False, + 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)) diff --git a/matemat/db/wrapper.py b/matemat/db/wrapper.py index 81baf04..94c6693 100644 --- a/matemat/db/wrapper.py +++ b/matemat/db/wrapper.py @@ -1,5 +1,4 @@ -from __future__ import annotations from typing import Any, Optional from matemat.exceptions import DatabaseConsistencyError @@ -47,7 +46,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 diff --git a/matemat/util/monthdelta.py b/matemat/util/monthdelta.py index 49c1fa0..b93e2ab 100644 --- a/matemat/util/monthdelta.py +++ b/matemat/util/monthdelta.py @@ -20,10 +20,13 @@ def add_months(d: datetime, months: int) -> datetime: days: int = 0 # Iterate the months between the passed date and the target month for i in range(months): - days += calendar.monthlen(*nextmonth) - nextmonth = calendar.nextmonth(*nextmonth) + days += calendar.monthrange(*nextmonth)[1] + 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 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 - 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 diff --git a/matemat/webserver/requestargs.py b/matemat/webserver/requestargs.py index 6ce11ef..373db90 100644 --- a/matemat/webserver/requestargs.py +++ b/matemat/webserver/requestargs.py @@ -1,5 +1,4 @@ -from __future__ import annotations from typing import Dict, Iterator, List, Tuple, Union @@ -25,7 +24,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. @@ -41,7 +40,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. @@ -279,7 +278,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. diff --git a/package/archlinux/PKGBUILD b/package/archlinux/PKGBUILD new file mode 100644 index 0000000..d062d49 --- /dev/null +++ b/package/archlinux/PKGBUILD @@ -0,0 +1,29 @@ + +# Maintainer: s3lph + +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/ +} diff --git a/package/archlinux/matemat.install b/package/archlinux/matemat.install new file mode 100755 index 0000000..dcd0e22 --- /dev/null +++ b/package/archlinux/matemat.install @@ -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 + +} diff --git a/package/archlinux/matemat/etc/matemat.conf b/package/archlinux/matemat/etc/matemat.conf new file mode 100644 index 0000000..1339a49 --- /dev/null +++ b/package/archlinux/matemat/etc/matemat.conf @@ -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] diff --git a/package/archlinux/matemat/usr/lib/matemat/matemat.conf b/package/archlinux/matemat/usr/lib/matemat/matemat.conf new file mode 100644 index 0000000..5c34f56 --- /dev/null +++ b/package/archlinux/matemat/usr/lib/matemat/matemat.conf @@ -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 diff --git a/package/archlinux/matemat/usr/lib/systemd/system/matemat.service b/package/archlinux/matemat/usr/lib/systemd/system/matemat.service new file mode 100644 index 0000000..6656268 --- /dev/null +++ b/package/archlinux/matemat/usr/lib/systemd/system/matemat.service @@ -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 diff --git a/package/debian/matemat/DEBIAN/conffiles b/package/debian/matemat/DEBIAN/conffiles new file mode 100644 index 0000000..6cda54c --- /dev/null +++ b/package/debian/matemat/DEBIAN/conffiles @@ -0,0 +1 @@ +/etc/matemat.conf diff --git a/package/debian/matemat/DEBIAN/control b/package/debian/matemat/DEBIAN/control new file mode 100644 index 0000000..41f5013 --- /dev/null +++ b/package/debian/matemat/DEBIAN/control @@ -0,0 +1,11 @@ +Package: matemat +Version: 0.1 +Maintainer: s3lph +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). diff --git a/package/debian/matemat/DEBIAN/postinst b/package/debian/matemat/DEBIAN/postinst new file mode 100755 index 0000000..f2b9ccc --- /dev/null +++ b/package/debian/matemat/DEBIAN/postinst @@ -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 diff --git a/package/debian/matemat/DEBIAN/postrm b/package/debian/matemat/DEBIAN/postrm new file mode 100755 index 0000000..305068d --- /dev/null +++ b/package/debian/matemat/DEBIAN/postrm @@ -0,0 +1,9 @@ +#!/bin/bash + +set -e + +if [[ "$1" == "remove" ]]; then + + systemctl daemon-reload || true + +fi diff --git a/package/debian/matemat/DEBIAN/prerm b/package/debian/matemat/DEBIAN/prerm new file mode 100755 index 0000000..85ae20e --- /dev/null +++ b/package/debian/matemat/DEBIAN/prerm @@ -0,0 +1,9 @@ +#!/bin/bash + +set -e + +if [[ "$1" == "remove" ]]; then + + userdel matemat + +fi diff --git a/package/debian/matemat/etc/matemat.conf b/package/debian/matemat/etc/matemat.conf new file mode 100644 index 0000000..1339a49 --- /dev/null +++ b/package/debian/matemat/etc/matemat.conf @@ -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] diff --git a/package/debian/matemat/lib/systemd/system/matemat.service b/package/debian/matemat/lib/systemd/system/matemat.service new file mode 100644 index 0000000..3dba957 --- /dev/null +++ b/package/debian/matemat/lib/systemd/system/matemat.service @@ -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 diff --git a/package/debian/matemat/usr/lib/matemat/matemat.conf b/package/debian/matemat/usr/lib/matemat/matemat.conf new file mode 100644 index 0000000..5c34f56 --- /dev/null +++ b/package/debian/matemat/usr/lib/matemat/matemat.conf @@ -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 diff --git a/package/debian/matemat/usr/share/doc/matemat/copyright b/package/debian/matemat/usr/share/doc/matemat/copyright new file mode 100644 index 0000000..a38a9a5 --- /dev/null +++ b/package/debian/matemat/usr/share/doc/matemat/copyright @@ -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. diff --git a/Dockerfile b/package/docker/Dockerfile similarity index 71% rename from Dockerfile rename to package/docker/Dockerfile index 7338327..d85e4f4 100644 --- a/Dockerfile +++ b/package/docker/Dockerfile @@ -4,9 +4,10 @@ FROM python:3.7-alpine ADD . / RUN mkdir -p /var/matemat/db /var/matemat/upload \ && 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 \ - && rm -rf /var/cache/apk /root/.cache/pip + && rm -rf /var/cache/apk /root/.cache/pip \ + && rm -rf /package EXPOSE 80/tcp CMD [ "/run.sh" ] diff --git a/package/release.py b/package/release.py new file mode 100755 index 0000000..8461268 --- /dev/null +++ b/package/release.py @@ -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'' in line: + done = True + break + release_changelog += line + elif f'' 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() diff --git a/requirements.txt b/requirements.txt index 0da0f5a..9c558e3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1 @@ -file-magic -jinja2 -Pillow +. diff --git a/setup.py b/setup.py index 0fc605f..6c2d005 100644 --- a/setup.py +++ b/setup.py @@ -1,12 +1,32 @@ -from setuptools import setup +from setuptools import setup, find_packages + +from matemat import __version__ setup( name='matemat', - version='2.0', - packages=['matemat'], - url='', - license='', + version=__version__, + url='https://gitlab.com/s3lph/matemat', + license='MIT', author='s3lph', 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' + ] + } ) diff --git a/testing/Dockerfile b/testing/Dockerfile index a67d602..ca8831a 100644 --- a/testing/Dockerfile +++ b/testing/Dockerfile @@ -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. -FROM python:3.7-stretch +FROM python:3.6-stretch RUN sed -re 's/stretch/buster/g' -i /etc/apt/sources.list \ && 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/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 \ + && apt-get install -y --no-install-recommends file sudo openssh-client git docker.io build-essential lintian rsync \ + && python3.6 -m pip install coverage wheel pycodestyle mypy \ && rm -rf /var/lib/apt/lists/* WORKDIR /home/matemat diff --git a/touchkey.html b/touchkey.html deleted file mode 100644 index 5d5b427..0000000 --- a/touchkey.html +++ /dev/null @@ -1,147 +0,0 @@ - - - - - - - -

Welcome, {{username}}

- - - - - - - - - - - - - - - - - - - - - - - - - - -