From e7da56df46bb5391f87c9afd1aa0fe020202faea Mon Sep 17 00:00:00 2001 From: Jason Antman Date: Wed, 12 Jun 2024 20:10:30 -0400 Subject: [PATCH] initial very basic working Flask app and tests --- .pre-commit-config.yaml | 1 + CONTRIBUTING.md | 14 ++++++++++++++ notes.md | 28 ++++++++++++++-------------- noxfile.py | 1 + pyproject.toml | 2 +- src/dm_mac/__init__.py | 15 ++++++++++++++- src/dm_mac/views/__init__.py | 1 + src/dm_mac/views/api.py | 12 ++++++++++++ tests/__init__.py | 2 +- tests/conftest.py | 30 ++++++++++++++++++++++++++++++ tests/views/__init__.py | 1 + tests/views/test_api.py | 15 +++++++++++++++ 12 files changed, 105 insertions(+), 17 deletions(-) create mode 100644 src/dm_mac/views/__init__.py create mode 100644 src/dm_mac/views/api.py create mode 100644 tests/conftest.py create mode 100644 tests/views/__init__.py create mode 100644 tests/views/test_api.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c95d78a..4191efb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,6 +21,7 @@ repos: entry: check-yaml language: system types: [yaml] + exclude: ^esphome-config\.yaml$ - id: darglint name: darglint entry: darglint diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3a043b1..fab95f0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -33,6 +33,14 @@ Finally, install and set up pre-commit: $ nox --session=pre-commit -- install ``` +## Running Locally + +```console +$ flask --app dm_mac run +``` + +The app will now be available at [http://127.0.0.1:5000](http://127.0.0.1:5000) + ## How to test the project Run the full test suite: @@ -54,6 +62,12 @@ For example, invoke the unit test suite like this: $ nox --session=tests ``` +To manually run the pre-commit tests: + +```console +$ nox --session=pre-commit +``` + Unit tests are located in the [tests/](tests/) directory, and are written using the [pytest](https://pytest.readthedocs.io/) testing framework. diff --git a/notes.md b/notes.md index 654b0c3..51ed9a7 100644 --- a/notes.md +++ b/notes.md @@ -35,8 +35,8 @@ In terms of hardware, what I think makes the most sense is: 1. A main box that mounts near the user controls, with the ESP32, RFID reader, display, and LED. This is a standardized 3d-printed box design with the UI and RFID components on the front, a board inside, a connector on the back, and design provision for mounting with screws and a custom bracket on any side. The box halves would be secured with tamper-proof screws and an appropriate warning label over at least one screw. 2. A “power” box that would contain just: 1. 120VAC (or 240VAC for special cases) to 5VDC power supply - 2. control relay* - 3. current sensor* + 2. control relay\* + 3. current sensor\* 3. A 6-conductor cable connecting the two, likely shielded, using locking aircraft-style connectors - The normal case we assume is a 120VAC power tool that’s essentially just a motor, where we want to control power to it and see if it’s running. For special cases, like the laser or other things that use lower-voltage (or at least lower-amperage) control circuits, we’d develop custom power boxes. It’s a simple enough interface - we have 2 wires for a relay control output from the ESP32 (which could be as simple as ground and a direct connection to an ESP32 pin, or could have an at-least-minimal relay in between), 2 wires that provide +5VDC and GND to the ESP32 and associated components in the control box, and 2 optional wires that provide output from a current clamp on the machine power input or some other method of sensing machine state. @@ -48,9 +48,9 @@ Additional thoughts: The server API should be dead-simple: -* On an event (card scan or removal or button press), POST to the server containing the event type, information (card ID) if applicable, machine name, current sensor value if applicable, and a shared secret string. - * Aside from button presses or card changes, we’ll also have a timer event every X seconds that just updates the current sensor value, if applicable. -* Server responds with a simple JSON data structure containing what should be displayed on screen (written to global variable and printed to screen), desired state of the relay, and desired RGB LED state and/or Oops button LED state. +- On an event (card scan or removal or button press), POST to the server containing the event type, information (card ID) if applicable, machine name, current sensor value if applicable, and a shared secret string. + - Aside from button presses or card changes, we’ll also have a timer event every X seconds that just updates the current sensor value, if applicable. +- Server responds with a simple JSON data structure containing what should be displayed on screen (written to global variable and printed to screen), desired state of the relay, and desired RGB LED state and/or Oops button LED state. I would like the server to run on-premise within the building… (1) we want equipment authentication and logging to continue working even if the Internet is down, and (2) if the power is out this doesn’t matter anyway, so that’s not a concern. @@ -60,12 +60,12 @@ Server to machine reply: desired relay state, LCD message, LCD backlight state, Machine state representation, in database: -* machine name -* machine current IP -* relay state -* relay last state change time -* rfid_card_number (nullable) -* rfid card last state change time -* oopsed_since (nullable) -* current_draw (nullable; maybe integer? or maybe machine_is_on bool?) -* last_changed +- machine name +- machine current IP +- relay state +- relay last state change time +- rfid_card_number (nullable) +- rfid card last state change time +- oopsed_since (nullable) +- current_draw (nullable; maybe integer? or maybe machine_is_on bool?) +- last_changed diff --git a/noxfile.py b/noxfile.py index 9b940db..abd6895 100644 --- a/noxfile.py +++ b/noxfile.py @@ -170,6 +170,7 @@ def tests(session: Session) -> None: "-m", "pytest", "--blockage", + "-v", *session.posargs, ) finally: diff --git a/pyproject.toml b/pyproject.toml index b44a950..ac2d12e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,7 +54,7 @@ tests = ["tests", "*/tests"] [tool.coverage.run] branch = true -source = ["dm_mac", "tests"] +source = ["dm_mac"] [tool.coverage.report] show_missing = true diff --git a/src/dm_mac/__init__.py b/src/dm_mac/__init__.py index 7ccace2..3ea07e4 100644 --- a/src/dm_mac/__init__.py +++ b/src/dm_mac/__init__.py @@ -1 +1,14 @@ -"""Machine_Access_Control.""" +"""Decatur Makers Machine Access Control.""" + +from flask import Flask + + +def create_app() -> Flask: + """Factory to create the app.""" + app: Flask = Flask("dm_mac") + + from dm_mac.views.api import api + + app.register_blueprint(api) + + return app diff --git a/src/dm_mac/views/__init__.py b/src/dm_mac/views/__init__.py new file mode 100644 index 0000000..a49950c --- /dev/null +++ b/src/dm_mac/views/__init__.py @@ -0,0 +1 @@ +"""Init for views (empty).""" diff --git a/src/dm_mac/views/api.py b/src/dm_mac/views/api.py new file mode 100644 index 0000000..6d1b961 --- /dev/null +++ b/src/dm_mac/views/api.py @@ -0,0 +1,12 @@ +"""API Views.""" + +from flask import Blueprint + + +api: Blueprint = Blueprint("api", __name__, url_prefix="/api") + + +@api.route("/") +def index(): + """Main API index route - placeholder.""" + return "Hello, World!" diff --git a/tests/__init__.py b/tests/__init__.py index 563bb89..6ebe281 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1 +1 @@ -"""Test suite for the dm_mac package.""" +"""Test suite for the dm_mac package (empty).""" diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..4fea9de --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,30 @@ +"""Conftest for dm_mac - fixtures.""" + +import pytest +from flask import Flask +from flask.testing import FlaskClient + +from dm_mac import create_app + + +@pytest.fixture() +def app() -> Flask: + """Test App fixture - app instance configured for testing.""" + app = create_app() + app.config.update( + { + "TESTING": True, + } + ) + + # other setup can go here + + yield app + + # clean up / reset resources here + + +@pytest.fixture() +def client(app: Flask) -> FlaskClient: + """Test Client for making requests to test app.""" + return app.test_client() diff --git a/tests/views/__init__.py b/tests/views/__init__.py new file mode 100644 index 0000000..2782bfa --- /dev/null +++ b/tests/views/__init__.py @@ -0,0 +1 @@ +"""Test views init (empty).""" diff --git a/tests/views/test_api.py b/tests/views/test_api.py new file mode 100644 index 0000000..d29d3d6 --- /dev/null +++ b/tests/views/test_api.py @@ -0,0 +1,15 @@ +"""Tests for API Views.""" + +from flask.testing import FlaskClient +from werkzeug.test import TestResponse + + +class TestIndex: + """Tests for API Index view.""" + + def test_index_response(self, client: FlaskClient): + """Test for API index response.""" + response: TestResponse = client.get("/api/") + assert response.status_code == 200 + assert response.text == "Hello, World!" + assert response.headers["Content-Type"] == "text/html; charset=utf-8"