From 82ffb9c8ee7d0428ba769683688de6c5c09772f8 Mon Sep 17 00:00:00 2001 From: Stefan Hackenberg Date: Wed, 24 Jul 2024 12:43:39 +0200 Subject: [PATCH] Refactor: Use pdm and ruff. Rename "test" to "tests". Introduce semver --- .flake8 | 4 - .github/workflows/lint.yml | 16 +- .github/workflows/test.yml | 7 +- .github/workflows/test_backends.yml | 10 +- README.md | 35 +- bin/autoonoff.py | 8 +- bin/run.py | 2 +- coffeebuddy/__init__.py | 72 ++- coffeebuddy/camera.py | 8 +- coffeebuddy/card.py | 14 +- coffeebuddy/facerecognition.py | 19 +- coffeebuddy/illumination.py | 12 +- coffeebuddy/model.py | 57 ++- coffeebuddy/pir.py | 4 +- coffeebuddy/reminder.py | 26 +- coffeebuddy/route_api.py | 15 +- coffeebuddy/route_chart.py | 9 +- coffeebuddy/route_coffee.py | 8 +- coffeebuddy/route_oneswipe.py | 4 +- coffeebuddy/route_pay.py | 13 +- coffeebuddy/route_tables.py | 29 +- coffeebuddy/route_welcome.py | 4 +- config.py | 5 +- config_rpi.py | 4 +- pdm.lock | 769 ++++++++++++++++++++++++++++ pyproject.toml | 53 +- requirements-camera.txt | 1 - requirements-facerecognition.txt | 1 - requirements-pcsc.txt | 1 - requirements-postgres.txt | 1 - requirements-rc522.txt | 2 - requirements-rpi.txt | 4 - requirements.txt | 11 - setup.cfg | 6 - {test => tests}/__init__.py | 14 +- {test => tests}/test_database.py | 0 {test => tests}/test_routes.py | 12 +- 37 files changed, 1093 insertions(+), 167 deletions(-) delete mode 100644 .flake8 create mode 100644 pdm.lock delete mode 100644 requirements-camera.txt delete mode 100644 requirements-facerecognition.txt delete mode 100644 requirements-pcsc.txt delete mode 100644 requirements-postgres.txt delete mode 100644 requirements-rc522.txt delete mode 100644 requirements-rpi.txt delete mode 100644 requirements.txt delete mode 100644 setup.cfg rename {test => tests}/__init__.py (74%) rename {test => tests}/test_database.py (100%) rename {test => tests}/test_routes.py (95%) diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 7e77b9d..0000000 --- a/.flake8 +++ /dev/null @@ -1,4 +0,0 @@ -[flake8] -extend-ignore = - W503, # line break before binary operator -max-line-length = 120 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 0c6ab60..fe6f8d0 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -14,15 +14,13 @@ jobs: uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - - name: Format with black + - name: Install dependencies run: | - pip install black - black --check . - - name: Lint with flake8 + pip install --user pdm + pdm sync -G dev + - name: Format run: | - pip install flake8 - flake8 --extend-ignore=W503 - - name: Lint with pylint + pdm run ruff format --check . + - name: Lint run: | - pip install pylint - pylint coffeebuddy + pdm run ruff --check . diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2b5a7be..03befc5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,9 +17,8 @@ jobs: - name: Install dependencies run: | sudo apt-get install libpcsclite-dev - python -m pip install --upgrade pip - pip install -r requirements.txt - pip install pytest + pip install --user pdm + pdm sync -G dev - name: Test with pytest run: | - pytest -v test/* + pdm run pytest -v diff --git a/.github/workflows/test_backends.yml b/.github/workflows/test_backends.yml index 473cb2a..fae3287 100644 --- a/.github/workflows/test_backends.yml +++ b/.github/workflows/test_backends.yml @@ -15,8 +15,8 @@ jobs: - name: Install dependencies run: | sudo apt-get install libpcsclite-dev - python -m pip install --upgrade pip - pip install -r requirements.txt + pip install --user pdm + pdm sync - name: Test backend sqlite run: | sed -i 's/^CARD.*/CARD = ""/' config.py @@ -24,7 +24,7 @@ jobs: sed -i 's/^PIR.*/PIR = False/' config.py sed -i 's/^ILLUMINATION.*/ILLUMINATION = False/' config.py sed -i 's/^DB_BACKEND.*/DB_BACKEND = "sqlite"/' config.py - ./bin/run.py & + pdm run ./bin/run.py & pid=$! sleep 1 ps | grep -c $pid || wait $pid @@ -36,7 +36,7 @@ jobs: sed -i 's/^PIR.*/PIR = False/' config.py sed -i 's/^ILLUMINATION.*/ILLUMINATION = False/' config.py sed -i 's/^DB_BACKEND.*/DB_BACKEND = "postgres"/' config.py - ./bin/run.py & + pdm run ./bin/run.py & pid=$! sleep 1 ps | grep -c $pid || wait $pid @@ -50,7 +50,7 @@ jobs: sed -i 's/^DB_BACKEND.*/DB_BACKEND = "sqlite"/' config.py export FLASK_ENV=prefilled export FLASK_DEBUG=true - ./bin/run.py & + pdm run ./bin/run.py & pid=$! sleep 1 ps | grep -c $pid || wait $pid diff --git a/README.md b/README.md index 89ff7f2..170932f 100644 --- a/README.md +++ b/README.md @@ -7,31 +7,43 @@ Do you use a paper based tally sheet to count your team's coffee consumption? Th ## Usage -1. Optional: Create virtual environment - ```bash - python3 -m venv .env - . .env/bin/activate - pip install -r requirements.txt +Coffebuddy uses [pdm](https://pdm-project.org/en/latest/) to manage its python dependencies. + +1. Install dependencies + + ```sh + pdm sync ``` + If `pyscard` fails building you might need to install dependencies. For Debian based distributions this would be + ```sh sudo apt install swig libpcsclite-dev ``` -2. Connect a pcsc smart card reader. I use a [uTrust 4701f](https://support.identiv.com/4701f/). Drivers for Ubuntu can be installed for example by + +2. Connect a pcsc smart card reader. + I use a [uTrust 4701f](https://support.identiv.com/4701f/). + Drivers for Ubuntu can be installed for example by + ```sh sudo apt install pcscd pcsc-tools ``` + 3. Start `production` environment + ```sh ./bin/run.py ``` + or `development` environment + ```sh FLASK_ENV=development ./bin/run.py ``` ## Tests -Run tests with `python -m unittest test/test_app.py` + +Run tests with `pytest -v tests` ## Application @@ -45,13 +57,16 @@ The final application uses a Raspberry Pi attached to a 7" touchscreen. Thus, th At least I had to adjust the following settings: * Fix screen resolution + ```conf hdmi_group=2 hdmi_mode=87 hdmi_cvt 1024 600 60 3 0 0 1 hdmi_drive=2 ``` + * If display has to be rotated by 180° adjust `/etc/X11/xorg.conf.d/40-libinput.conf` + ```conf Section "InputClass" Identifier "libinput touchscreen catchall" @@ -61,10 +76,14 @@ At least I had to adjust the following settings: Option "CalibrationMatrix" "-1 0 1 0 -1 1 0 0 1" EndSection ``` + * Disable translation option in chrome #### Card reader -Coffeebuddy works with PCSC reader and with SPI RFID module "RC522". Latter is supported on Raspi by several python modules. Although [mrfc522](https://github.com/pimylifeup/MFRC522-python) is widely used it leads to a high CPU consumption when polling for card. [pi-rc522](https://github.com/ondryaso/pi-rc522) uses interrupt based SPI communication. +Coffeebuddy works with PCSC reader and with SPI RFID module "RC522". +Latter is supported on Raspi by several python modules. +Although [mrfc522](https://github.com/pimylifeup/MFRC522-python) is widely used it leads to a high CPU consumption when polling for card. +[pi-rc522](https://github.com/ondryaso/pi-rc522) uses interrupt based SPI communication. Both modules can be used and selected in [config.py](./config.py). diff --git a/bin/autoonoff.py b/bin/autoonoff.py index 6692e97..6403976 100644 --- a/bin/autoonoff.py +++ b/bin/autoonoff.py @@ -1,8 +1,8 @@ #!/usr/bin/env python -import RPi.GPIO as GPIO import subprocess import time +from RPi import GPIO SENSOR_PIN = 14 TIME_ON = 20 @@ -11,12 +11,12 @@ def main(): GPIO.setmode(GPIO.BCM) GPIO.setup(SENSOR_PIN, GPIO.IN) - subprocess.run(["xset", "dpms", "force", "off"]) + subprocess.run(["xset", "dpms", "force", "off"], check=False) def callback(_): - subprocess.run(["xset", "dpms", "force", "on"]) + subprocess.run(["xset", "dpms", "force", "on"], check=False) time.sleep(TIME_ON) - subprocess.run(["xset", "dpms", "force", "off"]) + subprocess.run(["xset", "dpms", "force", "off"], check=False) try: GPIO.add_event_detect(SENSOR_PIN, GPIO.RISING, callback=callback) diff --git a/bin/run.py b/bin/run.py index d27f20a..da9eac2 100755 --- a/bin/run.py +++ b/bin/run.py @@ -8,7 +8,7 @@ import coffeebuddy # noqa: E402 try: - import RPi.GPIO as GPIO + from RPi import GPIO GPIO.setmode(GPIO.BCM) except ModuleNotFoundError: diff --git a/coffeebuddy/__init__.py b/coffeebuddy/__init__.py index 58f0ef4..0cbcf4f 100644 --- a/coffeebuddy/__init__.py +++ b/coffeebuddy/__init__.py @@ -1,5 +1,5 @@ -import datetime import dataclasses +import datetime import logging import os import random @@ -13,7 +13,6 @@ from flask_sqlalchemy import SQLAlchemy from sqlalchemy.exc import OperationalError - db = SQLAlchemy() login_manager = flask_login.LoginManager() @@ -36,7 +35,9 @@ def create_app(config=None): app.socketio = SocketIO(app) if os.path.exists(f"config_{socket.gethostname()}.py"): - logging.getLogger(__name__).info(f'Using config file "config_{socket.gethostname()}"') + logging.getLogger(__name__).info( + f'Using config file "config_{socket.gethostname()}"' + ) app.config.from_object(f"config_{socket.gethostname()}") else: logging.getLogger(__name__).info('Using config file "config"') @@ -63,7 +64,10 @@ def init_db(app): flask.current_app.db.init_app(app) if ( - (flask.current_app.config["DB_BACKEND"] == "sqlite" and not os.path.exists("coffee.db")) + ( + flask.current_app.config["DB_BACKEND"] == "sqlite" + and not os.path.exists("coffee.db") + ) or flask.current_app.config.get("ENV") in ("development", "prefilled") or flask.current_app.testing ): @@ -77,7 +81,10 @@ def init_db(app): if flask.current_app.config.get("ENV") == "development": flask.current_app.db.session.add( coffeebuddy.model.User( - tag=bytes.fromhex("01020304"), name="Mustermann", prename="Max", email="Max.Mustermann@example.com" + tag=bytes.fromhex("01020304"), + name="Mustermann", + prename="Max", + email="Max.Mustermann@example.com", ) ) flask.current_app.db.session.commit() @@ -86,8 +93,14 @@ def init_db(app): prefill() if flask.current_app.config["GUEST"]: - if not coffeebuddy.model.User.query.filter(coffeebuddy.model.User.name == "Guest").first(): - flask.current_app.db.session.add(coffeebuddy.model.User(tag=b"\xff\xff\xff\xff", name="Guest", prename="")) + if not coffeebuddy.model.User.query.filter( + coffeebuddy.model.User.name == "Guest" + ).first(): + flask.current_app.db.session.add( + coffeebuddy.model.User( + tag=b"\xff\xff\xff\xff", name="Guest", prename="" + ) + ) flask.current_app.db.session.commit() return flask.current_app.db @@ -129,12 +142,42 @@ def prefill(): import coffeebuddy.model demousers = [ - {"prename": "Donald", "postname": "Duck", "email": "donald.duck@entenhausen.com", "oneswipe": True}, - {"prename": "Dagobert", "postname": "Duck", "email": "dagobert.duck@entenhausen.com", "oneswipe": False}, - {"prename": "Gyro", "postname": " Gearloose", "email": "gyro.gearloose@entenhausen.com", "oneswipe": False}, - {"prename": "Tick ", "postname": "Duck", "email": "tick.duck@entenhausen.com", "oneswipe": False}, - {"prename": "Trick", "postname": "Duck", "email": "trick.duck@entenhausen.com", "oneswipe": False}, - {"prename": "Truck", "postname": "Duck", "email": "truck.duck@entenhausen.com", "oneswipe": False}, + { + "prename": "Donald", + "postname": "Duck", + "email": "donald.duck@entenhausen.com", + "oneswipe": True, + }, + { + "prename": "Dagobert", + "postname": "Duck", + "email": "dagobert.duck@entenhausen.com", + "oneswipe": False, + }, + { + "prename": "Gyro", + "postname": " Gearloose", + "email": "gyro.gearloose@entenhausen.com", + "oneswipe": False, + }, + { + "prename": "Tick ", + "postname": "Duck", + "email": "tick.duck@entenhausen.com", + "oneswipe": False, + }, + { + "prename": "Trick", + "postname": "Duck", + "email": "trick.duck@entenhausen.com", + "oneswipe": False, + }, + { + "prename": "Truck", + "postname": "Duck", + "email": "truck.duck@entenhausen.com", + "oneswipe": False, + }, ] for idx, data in enumerate(demousers): flask.current_app.db.session.add( @@ -151,7 +194,8 @@ def prefill(): coffeebuddy.model.Drink( userid=random.randint(0, len(demousers)), price=flask.current_app.config["PRICE"], - timestamp=datetime.datetime.now() - datetime.timedelta(seconds=random.randint(0, 365 * 24 * 60 * 60)), + timestamp=datetime.datetime.now() + - datetime.timedelta(seconds=random.randint(0, 365 * 24 * 60 * 60)), ) ) flask.current_app.db.session.commit() diff --git a/coffeebuddy/camera.py b/coffeebuddy/camera.py index cb29837..13eb81a 100644 --- a/coffeebuddy/camera.py +++ b/coffeebuddy/camera.py @@ -57,7 +57,9 @@ def run(self): last_motion_detected = datetime.datetime.now() self.events.fire_reset("motion_lost") self.events.fire("motion_detected") - logging.getLogger(__name__).info(f"Motion detected {last_motion_detected}.") + logging.getLogger(__name__).info( + f"Motion detected {last_motion_detected}." + ) else: self.events.fire_once("motion_lost") time.sleep(0.05) @@ -95,7 +97,9 @@ def init(): elif flask.current_app.config["CAMERA_ROTATION"] == 180: flask.current_app.config["CAMERA_ROTATION"] = cv2.ROTATE_180 elif flask.current_app.config["CAMERA_ROTATION"] == 270: - flask.current_app.config["CAMERA_ROTATION"] = cv2.ROTATE_90_COUNTERCLOCKWISE + flask.current_app.config["CAMERA_ROTATION"] = ( + cv2.ROTATE_90_COUNTERCLOCKWISE + ) else: flask.current_app.config["CAMERA_ROTATION"] = None diff --git a/coffeebuddy/card.py b/coffeebuddy/card.py index 26d829f..c9744b2 100644 --- a/coffeebuddy/card.py +++ b/coffeebuddy/card.py @@ -18,10 +18,14 @@ def run(self): while True: try: - request = smartcard.CardRequest.CardRequest(timeout=100, newcardonly=True) + request = smartcard.CardRequest.CardRequest( + timeout=100, newcardonly=True + ) service = request.waitforcard() service.connection.connect() - uuid = bytes(service.connection.transmit(list(self.PCSC_GET_UUID_APDU))[0])[:4] + uuid = bytes( + service.connection.transmit(list(self.PCSC_GET_UUID_APDU))[0] + )[:4] if len(uuid) == 4: self.socketio.emit("card_connected", data={"tag": uuid.hex()}) time.sleep(2) @@ -55,8 +59,8 @@ def __init__(self): self.socketio = flask.current_app.socketio def run(self): - from RPi import GPIO import pirc522 + from RPi import GPIO pirc522.RFID.antenna_gain = 0x07 reader = pirc522.RFID(pin_rst=25, pin_irq=24, pin_mode=GPIO.BCM) @@ -67,7 +71,9 @@ def run(self): if not error: (_, uid) = reader.anticoll() logging.getLogger(__name__).info(f"Card {uid} connected.") - self.socketio.emit("card_connected", data={"tag": bytes(uid[:4]).hex()}) + self.socketio.emit( + "card_connected", data={"tag": bytes(uid[:4]).hex()} + ) break diff --git a/coffeebuddy/facerecognition.py b/coffeebuddy/facerecognition.py index cb81a32..245208f 100644 --- a/coffeebuddy/facerecognition.py +++ b/coffeebuddy/facerecognition.py @@ -4,13 +4,14 @@ import flask - try: import cv2 import face_recognition face_cascade = cv2.CascadeClassifier( - os.path.join(os.path.dirname(cv2.__file__), "data", "haarcascade_frontalface_default.xml") + os.path.join( + os.path.dirname(cv2.__file__), "data", "haarcascade_frontalface_default.xml" + ) ) """OpenCV cascade for frontal face detection. Used for fast face detection.""" @@ -51,7 +52,9 @@ def mark_faces(img, boxes, name=None): for x, y, w, h in boxes: cv2.rectangle(img, (x, y), (x + w, y + h), (255, 0, 0), 2) if name: - cv2.putText(img, name, (x, y), cv2.FONT_HERSHEY_SIMPLEX, 0.75, (0, 255, 0), 2) + cv2.putText( + img, name, (x, y), cv2.FONT_HERSHEY_SIMPLEX, 0.75, (0, 255, 0), 2 + ) def every_nth(nth): @@ -138,7 +141,9 @@ def cv2_click_callback(self, _event, _x, _y, _flags, img): encoded_face = encode_face(img) logging.getLogger(__name__).info(f"Encoded face: {encoded_face}") if encoded_face is not None: - logging.getLogger(__name__).info(f"Save face: {self.tag} {self.name} {self.prename} {encoded_face}") + logging.getLogger(__name__).info( + f"Save face: {self.tag} {self.name} {self.prename} {encoded_face}" + ) add_face_data(self.tag, self.name, self.prename, encoded_face) self.capturing = False @@ -225,7 +230,11 @@ def main(): parser = argparse.ArgumentParser() parser.add_argument("function", choices=["capture", "recognize"]) parser.add_argument( - "--data", nargs="+", default=None, required=False, help='Tuple (TAG, PRENAME, NAME) for function "capture"' + "--data", + nargs="+", + default=None, + required=False, + help='Tuple (TAG, PRENAME, NAME) for function "capture"', ) args = parser.parse_args() diff --git a/coffeebuddy/illumination.py b/coffeebuddy/illumination.py index d5dfb9a..361e499 100644 --- a/coffeebuddy/illumination.py +++ b/coffeebuddy/illumination.py @@ -4,7 +4,6 @@ import flask import pigpio - PIN_GREEN = 16 PIN_BLUE = 20 PIN_RED = 21 @@ -53,14 +52,19 @@ def init(): flask.current_app.events.register("motion_lost", lambda: color_named("lightrose")) flask.current_app.events.register("route_coffee", lambda: color_named("green")) flask.current_app.events.register("route_welcome", lambda: color_named("rose")) - flask.current_app.events.register("facerecognition_face_detected", lambda: color_named("violet")) - flask.current_app.events.register("facerecognition_face_lost", lambda: color_named("rose")) + flask.current_app.events.register( + "facerecognition_face_detected", lambda: color_named("violet") + ) + flask.current_app.events.register( + "facerecognition_face_lost", lambda: color_named("rose") + ) if __name__ == "__main__": - import IPython import argparse + import IPython + parser = argparse.ArgumentParser() parser.add_argument("color", help="Color in RGB (0-1) or (0-255). E.g. 255 255 0") args = parser.parse_args() diff --git a/coffeebuddy/model.py b/coffeebuddy/model.py index 6bb3048..da0c82e 100644 --- a/coffeebuddy/model.py +++ b/coffeebuddy/model.py @@ -23,26 +23,42 @@ def serialize(self): class User(flask.current_app.db.Model, Serializer): id = flask.current_app.db.Column(flask.current_app.db.Integer, primary_key=True) - tag = flask.current_app.db.Column(flask.current_app.db.LargeBinary, nullable=False, unique=True) - tag2 = flask.current_app.db.Column(flask.current_app.db.LargeBinary, unique=True, default=None) + tag = flask.current_app.db.Column( + flask.current_app.db.LargeBinary, nullable=False, unique=True + ) + tag2 = flask.current_app.db.Column( + flask.current_app.db.LargeBinary, unique=True, default=None + ) name = flask.current_app.db.Column(flask.current_app.db.String(50), nullable=False) - prename = flask.current_app.db.Column(flask.current_app.db.String(50), nullable=False) + prename = flask.current_app.db.Column( + flask.current_app.db.String(50), nullable=False + ) email = flask.current_app.db.Column(flask.current_app.db.String(50), nullable=False) - option_oneswipe = flask.current_app.db.Column(flask.current_app.db.Boolean, default=False) + option_oneswipe = flask.current_app.db.Column( + flask.current_app.db.Boolean, default=False + ) enabled = flask.current_app.db.Column(flask.current_app.db.Boolean, default=True) - pays = flask.current_app.db.relationship("Pay", backref="user", cascade="all, delete") - drinks = flask.current_app.db.relationship("Drink", backref="user", cascade="all, delete") + pays = flask.current_app.db.relationship( + "Pay", backref="user", cascade="all, delete" + ) + drinks = flask.current_app.db.relationship( + "Drink", backref="user", cascade="all, delete" + ) @staticmethod def by_tag(tag): # pylint: disable=singleton-comparison - return User.query.filter((User.tag == tag) | ((User.tag2 != None) & (User.tag2 == tag))).first() # noqa: E711 + return User.query.filter( + (User.tag == tag) | ((User.tag2 is not None) & (User.tag2 == tag)) + ).first() # noqa: E711 @property def drinks_today(self): return ( Drink.query.filter(Drink.user == self) - .filter(flask.current_app.db.func.Date(Drink.timestamp) == datetime.date.today()) + .filter( + flask.current_app.db.func.Date(Drink.timestamp) == datetime.date.today() + ) .all() ) @@ -63,7 +79,9 @@ def drinks_per_day(self): return ( flask.current_app.db.session.query( flask.current_app.db.func.Date(Drink.timestamp), - flask.current_app.db.func.count(flask.current_app.db.func.Date(Drink.timestamp)), + flask.current_app.db.func.count( + flask.current_app.db.func.Date(Drink.timestamp) + ), ) .filter(self.id == Drink.userid) .group_by(flask.current_app.db.func.Date(Drink.timestamp)) @@ -81,7 +99,9 @@ def max_drinks_per_day(self): def drink_days(self): return ( tup[0] - for tup in flask.current_app.db.session.query(flask.current_app.db.func.Date(Drink.timestamp)) + for tup in flask.current_app.db.session.query( + flask.current_app.db.func.Date(Drink.timestamp) + ) .filter(self.id == Drink.userid) .distinct() .order_by(Drink.timestamp) @@ -91,7 +111,10 @@ def __str__(self): return f"{self.prename} {self.name} ({self.email})" def __repr__(self): - return f"" + return ( + f"" + ) def serialize(self): serialized = super().serialize() @@ -107,7 +130,9 @@ class Drink(flask.current_app.db.Model): timestamp = flask.current_app.db.Column(flask.current_app.db.DateTime) price = flask.current_app.db.Column(flask.current_app.db.Float, nullable=False) userid = flask.current_app.db.Column( - flask.current_app.db.Integer, flask.current_app.db.ForeignKey("user.id", ondelete="CASCADE"), nullable=False + flask.current_app.db.Integer, + flask.current_app.db.ForeignKey("user.id", ondelete="CASCADE"), + nullable=False, ) host = flask.current_app.db.Column(flask.current_app.db.String(50)) @@ -122,7 +147,9 @@ def __init__(self, *args, **kwargs): def drinks_vs_days(timedelta): return ( flask.current_app.db.session.query( - flask.current_app.db.func.count(flask.current_app.db.func.Date(Drink.timestamp)), + flask.current_app.db.func.count( + flask.current_app.db.func.Date(Drink.timestamp) + ), flask.current_app.db.func.Date(Drink.timestamp), ) .filter(Drink.timestamp > datetime.datetime.now() - timedelta) @@ -134,7 +161,9 @@ def drinks_vs_days(timedelta): class Pay(flask.current_app.db.Model): id = flask.current_app.db.Column(flask.current_app.db.Integer, primary_key=True) - timestamp = flask.current_app.db.Column(flask.current_app.db.DateTime, nullable=False) + timestamp = flask.current_app.db.Column( + flask.current_app.db.DateTime, nullable=False + ) userid = flask.current_app.db.Column( flask.current_app.db.Integer, flask.current_app.db.ForeignKey("user.id", ondelete="CASCADE"), diff --git a/coffeebuddy/pir.py b/coffeebuddy/pir.py index a0b163f..2e2da76 100644 --- a/coffeebuddy/pir.py +++ b/coffeebuddy/pir.py @@ -18,7 +18,9 @@ def run(self): while True: GPIO.wait_for_edge(self.pin, GPIO.BOTH) - self.events.fire("motion_detected" if GPIO.input(self.pin) else "motion_lost") + self.events.fire( + "motion_detected" if GPIO.input(self.pin) else "motion_lost" + ) def init(): diff --git a/coffeebuddy/reminder.py b/coffeebuddy/reminder.py index d7a93e6..04daac2 100644 --- a/coffeebuddy/reminder.py +++ b/coffeebuddy/reminder.py @@ -35,8 +35,8 @@ def reminder_interval_from_dept(dept): def random_debt_message(dept): x = f"{dept:.2f}€" - # pylint: disable=line-too-long return random.choice( + # ruff: noqa: E501 [ f"Looks like you owe the coffee fund {x}. Time to break open that piggy bank!", f"Hey, remember that time you drank all that coffee? Yeah, it's going to cost you {x}.", @@ -87,7 +87,9 @@ def remind(app): country = flask.current_app.config.get("COUNTRY") local_holidays = holidays.country_holidays(**country) - api = webexteamssdk.WebexTeamsAPI(access_token=flask.current_app.config["WEBEX_ACCESS_TOKEN"]) + api = webexteamssdk.WebexTeamsAPI( + access_token=flask.current_app.config["WEBEX_ACCESS_TOKEN"] + ) coffeebuddy_email = api.people.me().emails[0] now = datetime.datetime.now(timezone) @@ -118,13 +120,19 @@ def remind(app): # no message with user, yet last_reminder = remind_never else: - logging.getLogger(__name__).exception(f"Could not get webex messages for email={user.email}") + logging.getLogger(__name__).exception( + f"Could not get webex messages for email={user.email}" + ) continue # get date of last reminder (= message by coffeebuddy) if last_reminder is None: - reminder_messages = filter(lambda msg: msg.personEmail == coffeebuddy_email, messages) - reminder_messages = sorted(reminder_messages, key=lambda msg: msg.created) + reminder_messages = filter( + lambda msg: msg.personEmail == coffeebuddy_email, messages + ) + reminder_messages = sorted( + reminder_messages, key=lambda msg: msg.created + ) try: last_reminder = reminder_messages[-1].created except IndexError: @@ -134,9 +142,13 @@ def remind(app): # if it's time, send reminder if (now - last_reminder) > reminder_interval: message_oneliner = random_debt_message(user.unpayed) - message_md = flask.current_app.config.get("REMINDER_MESSAGE").format(oneliner=message_oneliner) + message_md = flask.current_app.config.get("REMINDER_MESSAGE").format( + oneliner=message_oneliner + ) try: api.messages.create(toPersonEmail=user.email, markdown=message_md) except webexteamssdk.ApiError: - logging.getLogger(__name__).exception(f"Could not send webex message for email={user.email}") + logging.getLogger(__name__).exception( + f"Could not send webex message for email={user.email}" + ) continue diff --git a/coffeebuddy/route_api.py b/coffeebuddy/route_api.py index bdb2407..cfe6070 100644 --- a/coffeebuddy/route_api.py +++ b/coffeebuddy/route_api.py @@ -1,5 +1,4 @@ import flask - import webexteamssdk from coffeebuddy.model import User @@ -33,20 +32,28 @@ def api(endpoint: str): if endpoint == "check_email": if not flask.current_app.config.get("WEBEX_ACCESS_TOKEN"): return "", 404 - api = webexteamssdk.WebexTeamsAPI(access_token=flask.current_app.config["WEBEX_ACCESS_TOKEN"]) + api = webexteamssdk.WebexTeamsAPI( + access_token=flask.current_app.config["WEBEX_ACCESS_TOKEN"] + ) data = flask.request.json people = api.people.list(email=data["email"]) try: people = list(people) if len(people) > 0: - return {"valid": True, "firstname": people[0].firstName, "lastname": people[0].lastName} + return { + "valid": True, + "firstname": people[0].firstName, + "lastname": people[0].lastName, + } return {"valid": False} except webexteamssdk.exceptions.ApiError: return {"valid": False} if endpoint == "send_message": if not flask.current_app.config.get("WEBEX_ACCESS_TOKEN"): return flask.abort(404) - api = webexteamssdk.WebexTeamsAPI(access_token=flask.current_app.config["WEBEX_ACCESS_TOKEN"]) + api = webexteamssdk.WebexTeamsAPI( + access_token=flask.current_app.config["WEBEX_ACCESS_TOKEN"] + ) data = flask.request.json api.messages.create(toPersonEmail=data["email"], markdown=data["text"]) return "" diff --git a/coffeebuddy/route_chart.py b/coffeebuddy/route_chart.py index 511aae2..138f882 100644 --- a/coffeebuddy/route_chart.py +++ b/coffeebuddy/route_chart.py @@ -24,7 +24,9 @@ def init(): def chart(): user = User.by_tag(escapefromhex(flask.request.args["tag"])) if user is None: - return flask.render_template("cardnotfound.html", uuid=flask.request.args["tag"]) + return flask.render_template( + "cardnotfound.html", uuid=flask.request.args["tag"] + ) if flask.request.method == "POST": if "coffee" in flask.request.form: @@ -38,7 +40,10 @@ def chart(): datasets = [ { "x": x, - "y": [f"1970-01-01T{user.nth_drink(date, i).timestamp.time().isoformat()}" for date in x], + "y": [ + f"1970-01-01T{user.nth_drink(date, i).timestamp.time().isoformat()}" + for date in x + ], "fill": "tozeroy", "name": f"{i}. Coffee", "mode": "markers", diff --git a/coffeebuddy/route_coffee.py b/coffeebuddy/route_coffee.py index ab932d1..abb1e43 100644 --- a/coffeebuddy/route_coffee.py +++ b/coffeebuddy/route_coffee.py @@ -9,12 +9,16 @@ def coffee(): flask.current_app.events.fire("route_coffee") user = User.by_tag(escapefromhex(flask.request.args["tag"])) if user is None: - return flask.render_template("cardnotfound.html", uuid=flask.request.args["tag"]) + return flask.render_template( + "cardnotfound.html", uuid=flask.request.args["tag"] + ) if flask.request.method == "GET" and user.option_oneswipe: return flask.render_template("oneswipe.html", user=user) if flask.request.method == "POST": if "coffee" in flask.request.form: - flask.current_app.db.session.add(Drink(user=user, price=flask.current_app.config["PRICE"])) + flask.current_app.db.session.add( + Drink(user=user, price=flask.current_app.config["PRICE"]) + ) flask.current_app.db.session.commit() elif "pay" in flask.request.form: return flask.redirect(f'pay.html?tag={flask.request.args["tag"]}') diff --git a/coffeebuddy/route_oneswipe.py b/coffeebuddy/route_oneswipe.py index f8d80e2..70d49de 100644 --- a/coffeebuddy/route_oneswipe.py +++ b/coffeebuddy/route_oneswipe.py @@ -8,6 +8,8 @@ def init(): def oneswipe(): user = User.by_tag(escapefromhex(flask.request.args["tag"])) if "coffee" in flask.request.form: - flask.current_app.db.session.add(Drink(user=user, price=flask.current_app.config["PRICE"])) + flask.current_app.db.session.add( + Drink(user=user, price=flask.current_app.config["PRICE"]) + ) flask.current_app.db.session.commit() return "" diff --git a/coffeebuddy/route_pay.py b/coffeebuddy/route_pay.py index f7ba613..cb2724b 100644 --- a/coffeebuddy/route_pay.py +++ b/coffeebuddy/route_pay.py @@ -1,14 +1,16 @@ import logging + import flask import webexteamssdk from coffeebuddy.model import Pay, User, escapefromhex - WEBEX_ACCESS_TOKEN = flask.current_app.config.get("WEBEX_ACCESS_TOKEN") if WEBEX_ACCESS_TOKEN: api = webexteamssdk.WebexTeamsAPI(access_token=WEBEX_ACCESS_TOKEN) -payment_notification_emails = flask.current_app.config.get("PAYMENT_NOTIFICATION_EMAILS") +payment_notification_emails = flask.current_app.config.get( + "PAYMENT_NOTIFICATION_EMAILS" +) def handle_post(): @@ -20,11 +22,14 @@ def handle_post(): for payment_notification_email in payment_notification_emails: message_md = ( f"{user} with bill of {user.unpayed + amount:.2f}€ " - "just entered a payment of **{amount:.2f}€**. Their bill is now {user.unpayed:.2f}€." + f"just entered a payment of **{amount:.2f}€**. " + f"Their bill is now {user.unpayed:.2f}€." ) try: # pylint: disable=possibly-used-before-assignment - api.messages.create(toPersonEmail=payment_notification_email, markdown=message_md) + api.messages.create( + toPersonEmail=payment_notification_email, markdown=message_md + ) except webexteamssdk.ApiError: logging.getLogger(__name__).exception( f"Could not send webex message for email={payment_notification_email}" diff --git a/coffeebuddy/route_tables.py b/coffeebuddy/route_tables.py index 2d6e1ba..09f512b 100644 --- a/coffeebuddy/route_tables.py +++ b/coffeebuddy/route_tables.py @@ -5,7 +5,7 @@ import flask_login import webexteamssdk -from coffeebuddy.model import Drink, User, Pay +from coffeebuddy.model import Drink, Pay, User def init(): @@ -65,7 +65,9 @@ def tables(): @flask.current_app.route("/table_data_messages") @flask_login.login_required def table_data_messages(): - api = webexteamssdk.WebexTeamsAPI(access_token=flask.current_app.config["WEBEX_ACCESS_TOKEN"]) + api = webexteamssdk.WebexTeamsAPI( + access_token=flask.current_app.config["WEBEX_ACCESS_TOKEN"] + ) coffeebuddy_email = api.people.me().emails[0] def generate(users): @@ -74,15 +76,20 @@ def generate(users): continue try: for msg in api.messages.list_direct(personEmail=user.email): - yield json.dumps( - { - "timestamp": str(msg.created), - "name": user.name, - "prename": user.prename, - "direction": "out" if msg.personEmail == coffeebuddy_email else "in", - "message": msg.html if msg.html else msg.text, - } - ).encode() + b"\n" + yield ( + json.dumps( + { + "timestamp": str(msg.created), + "name": user.name, + "prename": user.prename, + "direction": "out" + if msg.personEmail == coffeebuddy_email + else "in", + "message": msg.html if msg.html else msg.text, + } + ).encode() + + b"\n" + ) except webexteamssdk.ApiError: pass diff --git a/coffeebuddy/route_welcome.py b/coffeebuddy/route_welcome.py index 5e54d03..9b4f576 100644 --- a/coffeebuddy/route_welcome.py +++ b/coffeebuddy/route_welcome.py @@ -18,5 +18,7 @@ def welcome(): "welcome.html", dataset=data, hostname=socket.gethostname(), - githash=subprocess.check_output(["git", "rev-parse", "--short", "HEAD"]).decode("ascii").strip(), + githash=subprocess.check_output(["git", "rev-parse", "--short", "HEAD"]) + .decode("ascii") + .strip(), ) diff --git a/config.py b/config.py index 2c7bfcb..a4a91db 100644 --- a/config.py +++ b/config.py @@ -1,4 +1,3 @@ -import pytz import socket # Price per cup in € @@ -10,7 +9,9 @@ # Database connection details DB_BACKEND = "sqlite" if DB_BACKEND == "postgres": - SQLALCHEMY_DATABASE_URI = f"postgresql://{socket.gethostname()}@coffeebuddydb:5432/coffeebuddy" + SQLALCHEMY_DATABASE_URI = ( + f"postgresql://{socket.gethostname()}@coffeebuddydb:5432/coffeebuddy" + ) SQLALCHEMY_ENGINE_OPTIONS = {"connect_args": {"sslmode": "verify-full"}} elif DB_BACKEND == "sqlite": SQLALCHEMY_DATABASE_URI = "sqlite:///coffee.db" diff --git a/config_rpi.py b/config_rpi.py index a27d2f4..ceb08bd 100644 --- a/config_rpi.py +++ b/config_rpi.py @@ -9,7 +9,9 @@ # Database connection details DB_BACKEND = "sqlite" if DB_BACKEND == "postgres": - SQLALCHEMY_DATABASE_URI = f"postgresql://{socket.gethostname()}@coffeebuddydb:5432/coffeebuddy" + SQLALCHEMY_DATABASE_URI = ( + f"postgresql://{socket.gethostname()}@coffeebuddydb:5432/coffeebuddy" + ) SQLALCHEMY_ENGINE_OPTIONS = {"connect_args": {"sslmode": "verify-full"}} elif DB_BACKEND == "sqlite": SQLALCHEMY_DATABASE_URI = "sqlite:///coffee.db" diff --git a/pdm.lock b/pdm.lock new file mode 100644 index 0000000..c758cbd --- /dev/null +++ b/pdm.lock @@ -0,0 +1,769 @@ +# This file is @generated by PDM. +# It is not intended for manual editing. + +[metadata] +groups = ["default", "rpi", "dev", "pcsc", "camera"] +strategy = ["cross_platform", "inherit_metadata"] +lock_version = "4.4.1" +content_hash = "sha256:0603acd523b4b082fe4406932dbf5b6fad1d24646a164cb1c238a426609c74aa" + +[[package]] +name = "apscheduler" +version = "3.10.4" +requires_python = ">=3.6" +summary = "In-process task scheduler with Cron-like capabilities" +groups = ["default"] +dependencies = [ + "pytz", + "six>=1.4.0", + "tzlocal!=3.*,>=2.0", +] +files = [ + {file = "APScheduler-3.10.4-py3-none-any.whl", hash = "sha256:fb91e8a768632a4756a585f79ec834e0e27aad5860bac7eaa523d9ccefd87661"}, + {file = "APScheduler-3.10.4.tar.gz", hash = "sha256:e6df071b27d9be898e486bc7940a7be50b4af2e9da7c08f0744a96d4bd4cef4a"}, +] + +[[package]] +name = "bidict" +version = "0.23.1" +requires_python = ">=3.8" +summary = "The bidirectional mapping library for Python." +groups = ["default"] +files = [ + {file = "bidict-0.23.1-py3-none-any.whl", hash = "sha256:5dae8d4d79b552a71cbabc7deb25dfe8ce710b17ff41711e13010ead2abfc3e5"}, + {file = "bidict-0.23.1.tar.gz", hash = "sha256:03069d763bc387bbd20e7d49914e75fc4132a41937fa3405417e1a5a2d006d71"}, +] + +[[package]] +name = "blinker" +version = "1.8.2" +requires_python = ">=3.8" +summary = "Fast, simple object-to-object and broadcast signaling" +groups = ["default"] +files = [ + {file = "blinker-1.8.2-py3-none-any.whl", hash = "sha256:1779309f71bf239144b9399d06ae925637cf6634cf6bd131104184531bf67c01"}, + {file = "blinker-1.8.2.tar.gz", hash = "sha256:8f77b09d3bf7c795e969e9486f39c2c5e9c39d4ee07424be2bc594ece9642d83"}, +] + +[[package]] +name = "certifi" +version = "2024.7.4" +requires_python = ">=3.6" +summary = "Python package for providing Mozilla's CA Bundle." +groups = ["default"] +files = [ + {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, + {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +requires_python = ">=3.7.0" +summary = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +groups = ["default"] +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, +] + +[[package]] +name = "click" +version = "8.1.7" +requires_python = ">=3.7" +summary = "Composable command line interface toolkit" +groups = ["default"] +dependencies = [ + "colorama; platform_system == \"Windows\"", +] +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +summary = "Cross-platform colored terminal text." +groups = ["default", "dev"] +marker = "sys_platform == \"win32\" or platform_system == \"Windows\"" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "config" +version = "0.5.1" +summary = "A hierarchical, easy-to-use, powerful configuration module for Python" +groups = ["default"] +files = [ + {file = "config-0.5.1-py2.py3-none-any.whl", hash = "sha256:79ffa009ff2663cc8ca29e56bcec031c044609d4bafaa4f884132a413101ce84"}, + {file = "config-0.5.1.zip", hash = "sha256:2dd4a03aa383d30711d5a3325a1858de225328d61950a85be5b74c100f63016d"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +requires_python = ">=3.7" +summary = "Backport of PEP 654 (exception groups)" +groups = ["dev"] +marker = "python_version < \"3.11\"" +files = [ + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, +] + +[[package]] +name = "flask" +version = "3.0.3" +requires_python = ">=3.8" +summary = "A simple framework for building complex web applications." +groups = ["default"] +dependencies = [ + "Jinja2>=3.1.2", + "Werkzeug>=3.0.0", + "blinker>=1.6.2", + "click>=8.1.3", + "itsdangerous>=2.1.2", +] +files = [ + {file = "flask-3.0.3-py3-none-any.whl", hash = "sha256:34e815dfaa43340d1d15a5c3a02b8476004037eb4840b34910c6e21679d288f3"}, + {file = "flask-3.0.3.tar.gz", hash = "sha256:ceb27b0af3823ea2737928a4d99d125a06175b8512c445cbd9a9ce200ef76842"}, +] + +[[package]] +name = "flask-login" +version = "0.6.3" +requires_python = ">=3.7" +summary = "User authentication and session management for Flask." +groups = ["default"] +dependencies = [ + "Flask>=1.0.4", + "Werkzeug>=1.0.1", +] +files = [ + {file = "Flask-Login-0.6.3.tar.gz", hash = "sha256:5e23d14a607ef12806c699590b89d0f0e0d67baeec599d75947bf9c147330333"}, + {file = "Flask_Login-0.6.3-py3-none-any.whl", hash = "sha256:849b25b82a436bf830a054e74214074af59097171562ab10bfa999e6b78aae5d"}, +] + +[[package]] +name = "flask-socketio" +version = "5.3.6" +requires_python = ">=3.6" +summary = "Socket.IO integration for Flask applications" +groups = ["default"] +dependencies = [ + "Flask>=0.9", + "python-socketio>=5.0.2", +] +files = [ + {file = "Flask-SocketIO-5.3.6.tar.gz", hash = "sha256:bb8f9f9123ef47632f5ce57a33514b0c0023ec3696b2384457f0fcaa5b70501c"}, + {file = "Flask_SocketIO-5.3.6-py3-none-any.whl", hash = "sha256:9e62d2131842878ae6bfdd7067dfc3be397c1f2b117ab1dc74e6fe74aad7a579"}, +] + +[[package]] +name = "flask-sqlalchemy" +version = "3.1.1" +requires_python = ">=3.8" +summary = "Add SQLAlchemy support to your Flask application." +groups = ["default"] +dependencies = [ + "flask>=2.2.5", + "sqlalchemy>=2.0.16", +] +files = [ + {file = "flask_sqlalchemy-3.1.1-py3-none-any.whl", hash = "sha256:4ba4be7f419dc72f4efd8802d69974803c37259dd42f3913b0dcf75c9447e0a0"}, + {file = "flask_sqlalchemy-3.1.1.tar.gz", hash = "sha256:e4b68bb881802dda1a7d878b2fc84c06d1ee57fb40b874d3dc97dabfa36b8312"}, +] + +[[package]] +name = "future" +version = "1.0.0" +requires_python = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +summary = "Clean single-source support for Python 3 and 2" +groups = ["default"] +files = [ + {file = "future-1.0.0-py3-none-any.whl", hash = "sha256:929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216"}, + {file = "future-1.0.0.tar.gz", hash = "sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05"}, +] + +[[package]] +name = "greenlet" +version = "3.0.3" +requires_python = ">=3.7" +summary = "Lightweight in-process concurrent programming" +groups = ["default"] +marker = "(platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\") and python_version < \"3.13\"" +files = [ + {file = "greenlet-3.0.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dca1e2f3ca00b84a396bc1bce13dd21f680f035314d2379c4160c98153b2059b"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ed7fb269f15dc662787f4119ec300ad0702fa1b19d2135a37c2c4de6fadfd4a"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd4f49ae60e10adbc94b45c0b5e6a179acc1736cf7a90160b404076ee283cf83"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73a411ef564e0e097dbe7e866bb2dda0f027e072b04da387282b02c308807405"}, + {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7f362975f2d179f9e26928c5b517524e89dd48530a0202570d55ad6ca5d8a56f"}, + {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:649dde7de1a5eceb258f9cb00bdf50e978c9db1b996964cd80703614c86495eb"}, + {file = "greenlet-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:68834da854554926fbedd38c76e60c4a2e3198c6fbed520b106a8986445caaf9"}, + {file = "greenlet-3.0.3.tar.gz", hash = "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491"}, +] + +[[package]] +name = "h11" +version = "0.14.0" +requires_python = ">=3.7" +summary = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +groups = ["default"] +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "holidays" +version = "0.53" +requires_python = ">=3.8" +summary = "Generate and work with holidays in Python" +groups = ["default"] +dependencies = [ + "python-dateutil", +] +files = [ + {file = "holidays-0.53-py3-none-any.whl", hash = "sha256:371080faaa1c85fef49a64b16c52c2ec5cc9674360cc9fafad312eba74f3de1a"}, + {file = "holidays-0.53.tar.gz", hash = "sha256:ed8c935d35ad3c3e0866cd49256a51fb3e63d4ba506ca7ebbf07819feb055bfa"}, +] + +[[package]] +name = "idna" +version = "3.7" +requires_python = ">=3.5" +summary = "Internationalized Domain Names in Applications (IDNA)" +groups = ["default"] +files = [ + {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, + {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +requires_python = ">=3.7" +summary = "brain-dead simple config-ini parsing" +groups = ["dev"] +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "itsdangerous" +version = "2.2.0" +requires_python = ">=3.8" +summary = "Safely pass data to untrusted environments and back." +groups = ["default"] +files = [ + {file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"}, + {file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"}, +] + +[[package]] +name = "jinja2" +version = "3.1.4" +requires_python = ">=3.7" +summary = "A very fast and expressive template engine." +groups = ["default"] +dependencies = [ + "MarkupSafe>=2.0", +] +files = [ + {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, + {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, +] + +[[package]] +name = "markupsafe" +version = "2.1.5" +requires_python = ">=3.7" +summary = "Safely add untrusted strings to HTML/XML markup." +groups = ["default"] +files = [ + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, + {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, +] + +[[package]] +name = "mfrc522" +version = "0.0.7" +summary = "A library to integrate the MFRC522 RFID readers with the Raspberry Pi" +groups = ["rpi"] +dependencies = [ + "RPi-GPIO", + "spidev", +] +files = [ + {file = "mfrc522-0.0.7-py3-none-any.whl", hash = "sha256:874fd0385a7e40190063b60af83d615534b1f764c6b776fed1fcc94551c1e62f"}, + {file = "mfrc522-0.0.7.tar.gz", hash = "sha256:74c7020a4fc4870f5d7022542c36143fba771055a2fae2e5929e6a1159d2bf00"}, +] + +[[package]] +name = "numpy" +version = "2.0.1" +requires_python = ">=3.9" +summary = "Fundamental package for array computing in Python" +groups = ["camera"] +marker = "python_version >= \"3.6\" and platform_system == \"Linux\" and platform_machine == \"aarch64\" or python_version >= \"3.7\"" +files = [ + {file = "numpy-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fbb536eac80e27a2793ffd787895242b7f18ef792563d742c2d673bfcb75134"}, + {file = "numpy-2.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:69ff563d43c69b1baba77af455dd0a839df8d25e8590e79c90fcbe1499ebde42"}, + {file = "numpy-2.0.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:1b902ce0e0a5bb7704556a217c4f63a7974f8f43e090aff03fcf262e0b135e02"}, + {file = "numpy-2.0.1-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:f1659887361a7151f89e79b276ed8dff3d75877df906328f14d8bb40bb4f5101"}, + {file = "numpy-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4658c398d65d1b25e1760de3157011a80375da861709abd7cef3bad65d6543f9"}, + {file = "numpy-2.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4127d4303b9ac9f94ca0441138acead39928938660ca58329fe156f84b9f3015"}, + {file = "numpy-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e5eeca8067ad04bc8a2a8731183d51d7cbaac66d86085d5f4766ee6bf19c7f87"}, + {file = "numpy-2.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9adbd9bb520c866e1bfd7e10e1880a1f7749f1f6e5017686a5fbb9b72cf69f82"}, + {file = "numpy-2.0.1-cp310-cp310-win32.whl", hash = "sha256:7b9853803278db3bdcc6cd5beca37815b133e9e77ff3d4733c247414e78eb8d1"}, + {file = "numpy-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:81b0893a39bc5b865b8bf89e9ad7807e16717f19868e9d234bdaf9b1f1393868"}, + {file = "numpy-2.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:61728fba1e464f789b11deb78a57805c70b2ed02343560456190d0501ba37b0f"}, + {file = "numpy-2.0.1-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:12f5d865d60fb9734e60a60f1d5afa6d962d8d4467c120a1c0cda6eb2964437d"}, + {file = "numpy-2.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eacf3291e263d5a67d8c1a581a8ebbcfd6447204ef58828caf69a5e3e8c75990"}, + {file = "numpy-2.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2c3a346ae20cfd80b6cfd3e60dc179963ef2ea58da5ec074fd3d9e7a1e7ba97f"}, + {file = "numpy-2.0.1.tar.gz", hash = "sha256:485b87235796410c3519a699cfe1faab097e509e90ebb05dcd098db2ae87e7b3"}, +] + +[[package]] +name = "opencv-python" +version = "4.10.0.84" +requires_python = ">=3.6" +summary = "Wrapper package for OpenCV python bindings." +groups = ["camera"] +dependencies = [ + "numpy>=1.17.0; python_version >= \"3.7\"", + "numpy>=1.17.3; python_version >= \"3.8\"", + "numpy>=1.19.3; python_version >= \"3.6\" and platform_system == \"Linux\" and platform_machine == \"aarch64\"", + "numpy>=1.19.3; python_version >= \"3.9\"", + "numpy>=1.21.2; python_version >= \"3.10\"", + "numpy>=1.21.4; python_version >= \"3.10\" and platform_system == \"Darwin\"", +] +files = [ + {file = "opencv-python-4.10.0.84.tar.gz", hash = "sha256:72d234e4582e9658ffea8e9cae5b63d488ad06994ef12d81dc303b17472f3526"}, + {file = "opencv_python-4.10.0.84-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:fc182f8f4cda51b45f01c64e4cbedfc2f00aff799debebc305d8d0210c43f251"}, + {file = "opencv_python-4.10.0.84-cp37-abi3-macosx_12_0_x86_64.whl", hash = "sha256:71e575744f1d23f79741450254660442785f45a0797212852ee5199ef12eed98"}, + {file = "opencv_python-4.10.0.84-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09a332b50488e2dda866a6c5573ee192fe3583239fb26ff2f7f9ceb0bc119ea6"}, + {file = "opencv_python-4.10.0.84-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ace140fc6d647fbe1c692bcb2abce768973491222c067c131d80957c595b71f"}, + {file = "opencv_python-4.10.0.84-cp37-abi3-win32.whl", hash = "sha256:2db02bb7e50b703f0a2d50c50ced72e95c574e1e5a0bb35a8a86d0b35c98c236"}, + {file = "opencv_python-4.10.0.84-cp37-abi3-win_amd64.whl", hash = "sha256:32dbbd94c26f611dc5cc6979e6b7aa1f55a64d6b463cc1dcd3c95505a63e48fe"}, +] + +[[package]] +name = "packaging" +version = "24.1" +requires_python = ">=3.8" +summary = "Core utilities for Python packages" +groups = ["dev"] +files = [ + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, +] + +[[package]] +name = "pi-rc522" +version = "2.3.0" +summary = "Raspberry Pi Python library for SPI RFID RC522 module." +groups = ["rpi"] +dependencies = [ + "RPi-GPIO", + "spidev", +] +files = [ + {file = "pi-rc522-2.3.0.tar.gz", hash = "sha256:edf97b061f39cbfd3f306f41e66c140557af5ec88d814275a30856f0884e2af0"}, + {file = "pi_rc522-2.3.0-py3-none-any.whl", hash = "sha256:e7af0c9d31c1e52305691840dd0e9fb15f17b2fac7ebd71c74b791b858059787"}, +] + +[[package]] +name = "pigpio" +version = "1.78" +summary = "Raspberry Pi GPIO module" +groups = ["rpi"] +files = [ + {file = "pigpio-1.78-py2.py3-none-any.whl", hash = "sha256:81e46f640c4e6342881fa9bbe290dbcd4fc179619dc6591e57a9d4a084dc49fa"}, + {file = "pigpio-1.78.tar.gz", hash = "sha256:91efa50e4990649da97408a384782d6ccf58342fc59cdfe21ed7a42911569975"}, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +requires_python = ">=3.8" +summary = "plugin and hook calling mechanisms for python" +groups = ["dev"] +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[[package]] +name = "psycopg2-binary" +version = "2.9.9" +requires_python = ">=3.7" +summary = "psycopg2 - Python-PostgreSQL Database Adapter" +groups = ["default"] +files = [ + {file = "psycopg2-binary-2.9.9.tar.gz", hash = "sha256:7f01846810177d829c7692f1f5ada8096762d9172af1b1a28d4ab5b77c923c1c"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c2470da5418b76232f02a2fcd2229537bb2d5a7096674ce61859c3229f2eb202"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c6af2a6d4b7ee9615cbb162b0738f6e1fd1f5c3eda7e5da17861eacf4c717ea7"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75723c3c0fbbf34350b46a3199eb50638ab22a0228f93fb472ef4d9becc2382b"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83791a65b51ad6ee6cf0845634859d69a038ea9b03d7b26e703f94c7e93dbcf9"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ef4854e82c09e84cc63084a9e4ccd6d9b154f1dbdd283efb92ecd0b5e2b8c84"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed1184ab8f113e8d660ce49a56390ca181f2981066acc27cf637d5c1e10ce46e"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d2997c458c690ec2bc6b0b7ecbafd02b029b7b4283078d3b32a852a7ce3ddd98"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:b58b4710c7f4161b5e9dcbe73bb7c62d65670a87df7bcce9e1faaad43e715245"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0c009475ee389757e6e34611d75f6e4f05f0cf5ebb76c6037508318e1a1e0d7e"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8dbf6d1bc73f1d04ec1734bae3b4fb0ee3cb2a493d35ede9badbeb901fb40f6f"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-win32.whl", hash = "sha256:3f78fd71c4f43a13d342be74ebbc0666fe1f555b8837eb113cb7416856c79682"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-win_amd64.whl", hash = "sha256:876801744b0dee379e4e3c38b76fc89f88834bb15bf92ee07d94acd06ec890a0"}, +] + +[[package]] +name = "pyjwt" +version = "1.7.1" +summary = "JSON Web Token implementation in Python" +groups = ["default"] +files = [ + {file = "PyJWT-1.7.1-py2.py3-none-any.whl", hash = "sha256:5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e"}, + {file = "PyJWT-1.7.1.tar.gz", hash = "sha256:8d59a976fb773f3e6a39c85636357c4f0e242707394cadadd9814f5cbaa20e96"}, +] + +[[package]] +name = "pyscard" +version = "2.0.10" +summary = "Smartcard module for Python." +groups = ["pcsc"] +files = [ + {file = "pyscard-2.0.10-cp310-cp310-win32.whl", hash = "sha256:2ae1ece465ccd060e0a268cad1a213414ce8f7a8346bdb00b8470cf4b7826915"}, + {file = "pyscard-2.0.10-cp310-cp310-win_amd64.whl", hash = "sha256:c7197af995768e522665c3d01099224a268e1791b0dd5b8762364063a07503fa"}, + {file = "pyscard-2.0.10.tar.gz", hash = "sha256:4b9b865df03b29522e80ebae17790a8b3a096a9d885cda19363b44b1a6bf5c1c"}, +] + +[[package]] +name = "pytest" +version = "8.3.1" +requires_python = ">=3.8" +summary = "pytest: simple powerful testing with Python" +groups = ["dev"] +dependencies = [ + "colorama; sys_platform == \"win32\"", + "exceptiongroup>=1.0.0rc8; python_version < \"3.11\"", + "iniconfig", + "packaging", + "pluggy<2,>=1.5", + "tomli>=1; python_version < \"3.11\"", +] +files = [ + {file = "pytest-8.3.1-py3-none-any.whl", hash = "sha256:e9600ccf4f563976e2c99fa02c7624ab938296551f280835ee6516df8bc4ae8c"}, + {file = "pytest-8.3.1.tar.gz", hash = "sha256:7e8e5c5abd6e93cb1cc151f23e57adc31fcf8cfd2a3ff2da63e23f732de35db6"}, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +summary = "Extensions to the standard Python datetime module" +groups = ["default"] +dependencies = [ + "six>=1.5", +] +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[[package]] +name = "python-engineio" +version = "4.9.1" +requires_python = ">=3.6" +summary = "Engine.IO server and client for Python" +groups = ["default"] +dependencies = [ + "simple-websocket>=0.10.0", +] +files = [ + {file = "python_engineio-4.9.1-py3-none-any.whl", hash = "sha256:f995e702b21f6b9ebde4e2000cd2ad0112ba0e5116ec8d22fe3515e76ba9dddd"}, + {file = "python_engineio-4.9.1.tar.gz", hash = "sha256:7631cf5563086076611e494c643b3fa93dd3a854634b5488be0bba0ef9b99709"}, +] + +[[package]] +name = "python-socketio" +version = "5.11.3" +requires_python = ">=3.8" +summary = "Socket.IO server and client for Python" +groups = ["default"] +dependencies = [ + "bidict>=0.21.0", + "python-engineio>=4.8.0", +] +files = [ + {file = "python_socketio-5.11.3-py3-none-any.whl", hash = "sha256:2a923a831ff70664b7c502df093c423eb6aa93c1ce68b8319e840227a26d8b69"}, + {file = "python_socketio-5.11.3.tar.gz", hash = "sha256:194af8cdbb7b0768c2e807ba76c7abc288eb5bb85559b7cddee51a6bc7a65737"}, +] + +[[package]] +name = "pytz" +version = "2024.1" +summary = "World timezone definitions, modern and historical" +groups = ["default"] +files = [ + {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, + {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, +] + +[[package]] +name = "requests" +version = "2.32.3" +requires_python = ">=3.8" +summary = "Python HTTP for Humans." +groups = ["default"] +dependencies = [ + "certifi>=2017.4.17", + "charset-normalizer<4,>=2", + "idna<4,>=2.5", + "urllib3<3,>=1.21.1", +] +files = [ + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, +] + +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +summary = "A utility belt for advanced users of python-requests" +groups = ["default"] +dependencies = [ + "requests<3.0.0,>=2.0.1", +] +files = [ + {file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"}, + {file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"}, +] + +[[package]] +name = "rpi-gpio" +version = "0.7.1" +summary = "A module to control Raspberry Pi GPIO channels" +groups = ["rpi"] +files = [ + {file = "RPi.GPIO-0.7.1-cp310-cp310-linux_armv6l.whl", hash = "sha256:57b6c044ef5375a78c8dda27cdfadf329e76aa6943cd6cffbbbd345a9adf9ca5"}, + {file = "RPi.GPIO-0.7.1.tar.gz", hash = "sha256:cd61c4b03c37b62bba4a5acfea9862749c33c618e0295e7e90aa4713fb373b70"}, +] + +[[package]] +name = "ruff" +version = "0.5.4" +requires_python = ">=3.7" +summary = "An extremely fast Python linter and code formatter, written in Rust." +groups = ["dev"] +files = [ + {file = "ruff-0.5.4-py3-none-linux_armv6l.whl", hash = "sha256:82acef724fc639699b4d3177ed5cc14c2a5aacd92edd578a9e846d5b5ec18ddf"}, + {file = "ruff-0.5.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:da62e87637c8838b325e65beee485f71eb36202ce8e3cdbc24b9fcb8b99a37be"}, + {file = "ruff-0.5.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e98ad088edfe2f3b85a925ee96da652028f093d6b9b56b76fc242d8abb8e2059"}, + {file = "ruff-0.5.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c55efbecc3152d614cfe6c2247a3054cfe358cefbf794f8c79c8575456efe19"}, + {file = "ruff-0.5.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f9b85eaa1f653abd0a70603b8b7008d9e00c9fa1bbd0bf40dad3f0c0bdd06793"}, + {file = "ruff-0.5.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0cf497a47751be8c883059c4613ba2f50dd06ec672692de2811f039432875278"}, + {file = "ruff-0.5.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:09c14ed6a72af9ccc8d2e313d7acf7037f0faff43cde4b507e66f14e812e37f7"}, + {file = "ruff-0.5.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:628f6b8f97b8bad2490240aa84f3e68f390e13fabc9af5c0d3b96b485921cd60"}, + {file = "ruff-0.5.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3520a00c0563d7a7a7c324ad7e2cde2355733dafa9592c671fb2e9e3cd8194c1"}, + {file = "ruff-0.5.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93789f14ca2244fb91ed481456f6d0bb8af1f75a330e133b67d08f06ad85b516"}, + {file = "ruff-0.5.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:029454e2824eafa25b9df46882f7f7844d36fd8ce51c1b7f6d97e2615a57bbcc"}, + {file = "ruff-0.5.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:9492320eed573a13a0bc09a2957f17aa733fff9ce5bf00e66e6d4a88ec33813f"}, + {file = "ruff-0.5.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a6e1f62a92c645e2919b65c02e79d1f61e78a58eddaebca6c23659e7c7cb4ac7"}, + {file = "ruff-0.5.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:768fa9208df2bec4b2ce61dbc7c2ddd6b1be9fb48f1f8d3b78b3332c7d71c1ff"}, + {file = "ruff-0.5.4-py3-none-win32.whl", hash = "sha256:e1e7393e9c56128e870b233c82ceb42164966f25b30f68acbb24ed69ce9c3a4e"}, + {file = "ruff-0.5.4-py3-none-win_amd64.whl", hash = "sha256:58b54459221fd3f661a7329f177f091eb35cf7a603f01d9eb3eb11cc348d38c4"}, + {file = "ruff-0.5.4-py3-none-win_arm64.whl", hash = "sha256:bd53da65f1085fb5b307c38fd3c0829e76acf7b2a912d8d79cadcdb4875c1eb7"}, + {file = "ruff-0.5.4.tar.gz", hash = "sha256:2795726d5f71c4f4e70653273d1c23a8182f07dd8e48c12de5d867bfb7557eed"}, +] + +[[package]] +name = "simple-websocket" +version = "1.0.0" +requires_python = ">=3.6" +summary = "Simple WebSocket server and client for Python" +groups = ["default"] +dependencies = [ + "wsproto", +] +files = [ + {file = "simple-websocket-1.0.0.tar.gz", hash = "sha256:17d2c72f4a2bd85174a97e3e4c88b01c40c3f81b7b648b0cc3ce1305968928c8"}, + {file = "simple_websocket-1.0.0-py3-none-any.whl", hash = "sha256:1d5bf585e415eaa2083e2bcf02a3ecf91f9712e7b3e6b9fa0b461ad04e0837bc"}, +] + +[[package]] +name = "six" +version = "1.16.0" +requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +summary = "Python 2 and 3 compatibility utilities" +groups = ["default"] +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "spidev" +version = "3.6" +summary = "Python bindings for Linux SPI access through spidev" +groups = ["rpi"] +files = [ + {file = "spidev-3.6.tar.gz", hash = "sha256:14dbc37594a4aaef85403ab617985d3c3ef464d62bc9b769ef552db53701115b"}, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.31" +requires_python = ">=3.7" +summary = "Database Abstraction Library" +groups = ["default"] +dependencies = [ + "greenlet!=0.4.17; (platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\") and python_version < \"3.13\"", + "typing-extensions>=4.6.0", +] +files = [ + {file = "SQLAlchemy-2.0.31-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f2a213c1b699d3f5768a7272de720387ae0122f1becf0901ed6eaa1abd1baf6c"}, + {file = "SQLAlchemy-2.0.31-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9fea3d0884e82d1e33226935dac990b967bef21315cbcc894605db3441347443"}, + {file = "SQLAlchemy-2.0.31-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3ad7f221d8a69d32d197e5968d798217a4feebe30144986af71ada8c548e9fa"}, + {file = "SQLAlchemy-2.0.31-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f2bee229715b6366f86a95d497c347c22ddffa2c7c96143b59a2aa5cc9eebbc"}, + {file = "SQLAlchemy-2.0.31-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cd5b94d4819c0c89280b7c6109c7b788a576084bf0a480ae17c227b0bc41e109"}, + {file = "SQLAlchemy-2.0.31-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:750900a471d39a7eeba57580b11983030517a1f512c2cb287d5ad0fcf3aebd58"}, + {file = "SQLAlchemy-2.0.31-cp310-cp310-win32.whl", hash = "sha256:7bd112be780928c7f493c1a192cd8c5fc2a2a7b52b790bc5a84203fb4381c6be"}, + {file = "SQLAlchemy-2.0.31-cp310-cp310-win_amd64.whl", hash = "sha256:5a48ac4d359f058474fadc2115f78a5cdac9988d4f99eae44917f36aa1476327"}, + {file = "SQLAlchemy-2.0.31-py3-none-any.whl", hash = "sha256:69f3e3c08867a8e4856e92d7afb618b95cdee18e0bc1647b77599722c9a28911"}, + {file = "SQLAlchemy-2.0.31.tar.gz", hash = "sha256:b607489dd4a54de56984a0c7656247504bd5523d9d0ba799aef59d4add009484"}, +] + +[[package]] +name = "tomli" +version = "2.0.1" +requires_python = ">=3.7" +summary = "A lil' TOML parser" +groups = ["dev"] +marker = "python_version < \"3.11\"" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +requires_python = ">=3.8" +summary = "Backported and Experimental Type Hints for Python 3.8+" +groups = ["default"] +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] + +[[package]] +name = "tzdata" +version = "2024.1" +requires_python = ">=2" +summary = "Provider of IANA time zone data" +groups = ["default"] +marker = "platform_system == \"Windows\"" +files = [ + {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, + {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, +] + +[[package]] +name = "tzlocal" +version = "5.2" +requires_python = ">=3.8" +summary = "tzinfo object for the local timezone" +groups = ["default"] +dependencies = [ + "tzdata; platform_system == \"Windows\"", +] +files = [ + {file = "tzlocal-5.2-py3-none-any.whl", hash = "sha256:49816ef2fe65ea8ac19d19aa7a1ae0551c834303d5014c6d5a62e4cbda8047b8"}, + {file = "tzlocal-5.2.tar.gz", hash = "sha256:8d399205578f1a9342816409cc1e46a93ebd5755e39ea2d85334bea911bf0e6e"}, +] + +[[package]] +name = "urllib3" +version = "2.2.2" +requires_python = ">=3.8" +summary = "HTTP library with thread-safe connection pooling, file post, and more." +groups = ["default"] +files = [ + {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, + {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, +] + +[[package]] +name = "webexteamssdk" +version = "1.7" +summary = "Community-developed Python SDK for the Webex Teams APIs" +groups = ["default"] +dependencies = [ + "PyJWT==1.7.1", + "future", + "requests-toolbelt", + "requests>=2.4.2", +] +files = [ + {file = "webexteamssdk-1.7.tar.gz", hash = "sha256:4cc8e7d000d5cf3e25913966bfb2af6d023b1dcc51b75e1cea19e89ebc3f1d00"}, +] + +[[package]] +name = "werkzeug" +version = "3.0.3" +requires_python = ">=3.8" +summary = "The comprehensive WSGI web application library." +groups = ["default"] +dependencies = [ + "MarkupSafe>=2.1.1", +] +files = [ + {file = "werkzeug-3.0.3-py3-none-any.whl", hash = "sha256:fc9645dc43e03e4d630d23143a04a7f947a9a3b5727cd535fdfe155a17cc48c8"}, + {file = "werkzeug-3.0.3.tar.gz", hash = "sha256:097e5bfda9f0aba8da6b8545146def481d06aa7d3266e7448e2cccf67dd8bd18"}, +] + +[[package]] +name = "wsproto" +version = "1.2.0" +requires_python = ">=3.7.0" +summary = "WebSockets state-machine based protocol implementation" +groups = ["default"] +dependencies = [ + "h11<1,>=0.9.0", +] +files = [ + {file = "wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736"}, + {file = "wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065"}, +] diff --git a/pyproject.toml b/pyproject.toml index 62e01af..d90b72a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,22 +1,33 @@ -[tool.black] -line-length = 120 +[project] +name = "coffeebuddy" +version = "1.0.0" +description = "WebApp for Raspi to track your teams' coffee consumption digitally." +authors = [{ name = "Stefan Hackenberg", email = "mail@stefan-hackenberg.de" }] +dependencies = [ + "config>=0.5.1", + "flask>=3.0.3", + "sqlalchemy>=2.0.31", + "flask-sqlalchemy>=3.1.1", + "flask-socketio>=5.3.6", + "flask-login>=0.6.3", + "psycopg2-binary>=2.9.9", + "webexteamssdk>=1.7", + "apscheduler>=3.10.4", + "holidays>=0.53", + "pytz>=2024.1", +] +requires-python = "==3.10.*" +readme = "README.md" +license = { text = "MIT" } -[tool.pylint.BASIC] -good-names = ["x","y","w","h","n","r","g","b","k"] -[tool.pylint.'MESSAGES CONTROL'] -max-line-length = 120 -disable = """ - import-outside-toplevel, - missing-class-docstring, - missing-function-docstring, - missing-module-docstring, - logging-fstring-interpolation, - import-error, - fixme, - too-many-arguments, - too-many-branches, - too-few-public-methods, - too-many-return-statements, - duplicate-code, - cyclic-import, -""" +[project.optional-dependencies] +rpi = ["mfrc522~=0.0.7", "pi-rc522~=2.3.0", "pigpio~=1.78"] +pcsc = ["pyscard>=2.0.10"] +camera = ["opencv-python>=4.10.0.84"] +dev = ["ruff>=0.5.4", "pytest>=8.3.1"] + +[tool.pdm] +distribution = false + +[tool.ruff.lint] +select = ["E", "F", "I", "PLC", "PLE", "W"] diff --git a/requirements-camera.txt b/requirements-camera.txt deleted file mode 100644 index 0dd006b..0000000 --- a/requirements-camera.txt +++ /dev/null @@ -1 +0,0 @@ -opencv-python diff --git a/requirements-facerecognition.txt b/requirements-facerecognition.txt deleted file mode 100644 index c797f54..0000000 --- a/requirements-facerecognition.txt +++ /dev/null @@ -1 +0,0 @@ -face-recognition diff --git a/requirements-pcsc.txt b/requirements-pcsc.txt deleted file mode 100644 index 5635940..0000000 --- a/requirements-pcsc.txt +++ /dev/null @@ -1 +0,0 @@ -pyscard diff --git a/requirements-postgres.txt b/requirements-postgres.txt deleted file mode 100644 index 37ec460..0000000 --- a/requirements-postgres.txt +++ /dev/null @@ -1 +0,0 @@ -psycopg2-binary diff --git a/requirements-rc522.txt b/requirements-rc522.txt deleted file mode 100644 index 2596579..0000000 --- a/requirements-rc522.txt +++ /dev/null @@ -1,2 +0,0 @@ -mfrc522 -pi-rc522 diff --git a/requirements-rpi.txt b/requirements-rpi.txt deleted file mode 100644 index b7ff752..0000000 --- a/requirements-rpi.txt +++ /dev/null @@ -1,4 +0,0 @@ --r requirements.txt -mfrc522 -pi-rc522 -pigpio diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 9558ce9..0000000 --- a/requirements.txt +++ /dev/null @@ -1,11 +0,0 @@ -config -flask -sqlalchemy -flask-sqlalchemy -flask-socketio -flask-login -psycopg2-binary -webexteamssdk -apscheduler -holidays -pytz diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 1efeb78..0000000 --- a/setup.cfg +++ /dev/null @@ -1,6 +0,0 @@ -[flake8] -# E125 continuation line with same indent as next logical line -# E501 line too long -# W504 line break after binary operator -ignore = E125,E501,W504 -exclude = .git,.env,.idea,config.py \ No newline at end of file diff --git a/test/__init__.py b/tests/__init__.py similarity index 74% rename from test/__init__.py rename to tests/__init__.py index 6a1a929..dabdb35 100644 --- a/test/__init__.py +++ b/tests/__init__.py @@ -28,9 +28,19 @@ def truncate_all(self): def add_default_user(self): from coffeebuddy.model import User - user1 = User(tag=b"\x01\x02\x03\x04", name="Mustermann", prename="Max", email="Max.Mustermann@example.com") + user1 = User( + tag=b"\x01\x02\x03\x04", + name="Mustermann", + prename="Max", + email="Max.Mustermann@example.com", + ) self.db.session.add(user1) - user2 = User(tag=b"\x05\x06\x07\x08", name="Doe", prename="Jane", email="Jane.Doe@example.com") + user2 = User( + tag=b"\x05\x06\x07\x08", + name="Doe", + prename="Jane", + email="Jane.Doe@example.com", + ) self.db.session.add(user2) self.db.session.commit() return user1, user2 diff --git a/test/test_database.py b/tests/test_database.py similarity index 100% rename from test/test_database.py rename to tests/test_database.py diff --git a/test/test_routes.py b/tests/test_routes.py similarity index 95% rename from test/test_routes.py rename to tests/test_routes.py index 4953120..238c3e5 100644 --- a/test/test_routes.py +++ b/tests/test_routes.py @@ -156,7 +156,9 @@ def test_logout(self): class TestRouteOneSwipe(TestCoffeebuddy): def test(self): user, _ = self.add_default_user() - response = self.client.post(f"/oneswipe.html?tag={user.tag.hex()}", data=dict(coffee=True)) + response = self.client.post( + f"/oneswipe.html?tag={user.tag.hex()}", data=dict(coffee=True) + ) self.assertEqual(response.status_code, 200) self.assertGreater(len(user.drinks), 0) @@ -170,7 +172,9 @@ def test(self): class TestRoutePay(TestCoffeebuddy): def test(self): user, _ = self.add_default_user() - response = self.client.post(f"/pay.html?tag={user.tag.hex()}", data=dict(amount=1)) + response = self.client.post( + f"/pay.html?tag={user.tag.hex()}", data=dict(amount=1) + ) self.assertEqual(response.status_code, 302) self.assertGreater(len(user.pays), 0) @@ -180,7 +184,9 @@ def test_get_users(self): user1, user2 = self.add_default_user() response = self.client.post("api/get_users") self.assertEqual(response.status_code, 200) - self.assertEqual(json.loads(response.data), [user1.serialize(), user2.serialize()]) + self.assertEqual( + json.loads(response.data), [user1.serialize(), user2.serialize()] + ) def test_set_user(self): user1, _ = self.add_default_user()