diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 1796208..2ea9fc3 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -24,7 +24,7 @@ jobs: - name: elm-format install run: npm install -g elm-format - name: Install dev dependencies - run: poetry install --only=dev + run: poetry install --no-root env: POETRY_VIRTUALENVS_CREATE: false - name: Check linting diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 46cde30..6df530b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,4 +1,9 @@ repos: +- repo: https://github.com/charliermarsh/ruff-pre-commit + # Ruff version. + rev: 'v0.0.257' + hooks: + - id: ruff - repo: https://github.com/psf/black rev: 22.10.0 hooks: diff --git a/README.md b/README.md index 96c2ce5..05f3c38 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,21 @@ sudo systemctl daemon-reload sudo systemctl start aqimon ``` +## Configuration + +| Variable | Default | Description | +|------------------------------|---------------------|-----------------------------------------------------------------------------------------------------------------------------------| +| AQIMON_DB_PATH | ~/.aqimon/db.sqlite | The path to the database file, where read information is stored. It should be an absolute path; user home expansion is supported. | +| AQIMON_POLL_FREQUENCY_SEC | 900 (15 minutes) | Sets how frequently to read from the device, in seconds. | +| AQIMON_RETENTION_MINUTES | 10080 (1 week) | Sets how long data will be kept in the database, in minutes. | +| AQIMON_READER_TYPE | NOVAPM | The reader type to use, either NOVAPM or MOCK. | +| AQIMON_USB_PATH | /dev/ttyUSB0 | The path to the USB device for the sensor. | +| AQIMON_SLEEP_TIME_SEC | 5 | The number of seconds to wait for between each read in a set of reads. | +| AQIMON_SAMPLE_COUNT_PER_READ | 5 | The number of reads to take with each sample. | +| AQIMON_SERVER_PORT | 8000 | The port to run the server on. | +| AQIMON_SERVER_HOST | 0.0.0.0 | The host to run the server on. | + + ## Contributing ### Toolset @@ -100,6 +115,15 @@ To run auto-formatters, run: just format ``` +### Using the Mock Reader + +Aqimon ships with a mock reader class that you can use in the event that you don't have a reader available on your +development computer. The mock reader just returns randomized reads. To use it, you can start the server like: + +```commandline +AQIMON_READER_TYPE=MOCK poetry run aqimon +``` + ### Submitting a PR Master branch is locked, but you can open a PR on the repo. Build checks must pass, and changes approved by a code diff --git a/aqimon/config.py b/aqimon/config.py index 3c85865..09244e3 100644 --- a/aqimon/config.py +++ b/aqimon/config.py @@ -1,15 +1,10 @@ """Config module.""" import os.path -from typing import Optional from dataclasses import dataclass -from serde import serde -from serde.toml import to_toml, from_toml -DEFAULT_CONFIG_PATH = "~/.aqimon/config" DEFAULT_DB_PATH = "~/.aqimon/db.sqlite" -@serde @dataclass(frozen=True) class Config: """Config data for the application.""" @@ -25,6 +20,10 @@ class Config: usb_sleep_time_sec: int sample_count_per_read: int + # Server properties + server_port: int + server_host: str + DEFAULT_CONFIG = Config( database_path=os.path.expanduser(DEFAULT_DB_PATH), @@ -34,33 +33,21 @@ class Config: usb_path="/dev/ttyUSB0", usb_sleep_time_sec=5, sample_count_per_read=5, + server_port=8000, + server_host="0.0.0.0", ) -def _load_config(path: str) -> Config: - """Load config data from a toml file.""" - with open(path, "r") as file: - return from_toml(Config, file.read()) - - -def save_config(config: Config, path: str): - """Save config data to a given path as a toml file.""" - with open(path, "w") as file: - file.write(to_toml(config)) - - -def get_config(passed_config_path: Optional[str]) -> Config: - """Get the config. - - If a toml config file path is passed, it is loaded and used. - - If no toml config is passed, a default config path is used, if the toml file exists. - - If no toml exists in the default location, a sensible default config is loaded. - """ - if passed_config_path and os.path.exists(passed_config_path): - return _load_config(passed_config_path) - elif not passed_config_path and os.path.exists(DEFAULT_CONFIG_PATH): - return _load_config(DEFAULT_CONFIG_PATH) - else: - return DEFAULT_CONFIG +def get_config_from_env() -> Config: + """Get the config from environment variables.""" + return Config( + database_path=os.path.expanduser(os.environ.get("AQIMON_DB_PATH", DEFAULT_CONFIG.database_path)), + poll_frequency_sec=int(os.environ.get("AQIMON_POLL_FREQUENCY_SEC", DEFAULT_CONFIG.poll_frequency_sec)), + retention_minutes=int(os.environ.get("AQIMON_RETENTION_MINUTES", DEFAULT_CONFIG.retention_minutes)), + reader_type=os.environ.get("AQIMON_READER_TYPE", DEFAULT_CONFIG.reader_type), + usb_path=os.environ.get("AQIMON_USB_PATH", DEFAULT_CONFIG.usb_path), + usb_sleep_time_sec=int(os.environ.get("AQIMON_USB_SLEEP_TIME_SEC", DEFAULT_CONFIG.usb_sleep_time_sec)), + sample_count_per_read=int(os.environ.get("AQIMON_SAMPLE_COUNT_PER_READ", DEFAULT_CONFIG.sample_count_per_read)), + server_port=int(os.environ.get("AQIMON_SERVER_PORT", DEFAULT_CONFIG.server_port)), + server_host=os.environ.get("AQIMON_SERVER_HOST", DEFAULT_CONFIG.server_host), + ) diff --git a/aqimon/server.py b/aqimon/server.py index 63ec482..f954696 100644 --- a/aqimon/server.py +++ b/aqimon/server.py @@ -19,13 +19,13 @@ from .read.mock import MockReader from .read.novapm import NovaPmReader from . import aqi_common -from .config import Config, get_config +from .config import Config, get_config_from_env import logging log = logging.getLogger(__name__) app = FastAPI() -config = get_config(None) +config = get_config_from_env() project_root = Path(__file__).parent.resolve() @@ -145,9 +145,11 @@ async def status(): def start(): """Start the server.""" - uvicorn.run(app, host="0.0.0.0", port=8000) + env_config = get_config_from_env() + uvicorn.run(app, host=env_config.server_host, port=env_config.server_port) def debug(): """Start the server in debug mode, with hotswapping code.""" - uvicorn.run("aqimon.server:app", host="0.0.0.0", port=8000, reload=True) + env_config = get_config_from_env() + uvicorn.run("aqimon.server:app", host=env_config.server_host, port=env_config.server_port, reload=True) diff --git a/justfile b/justfile index bebaba8..af9d273 100644 --- a/justfile +++ b/justfile @@ -11,6 +11,7 @@ compile_elm: lint: black --check . ruff check . + mypy . elm-format --validate elm/ # Format code diff --git a/poetry.lock b/poetry.lock index fbf9dfc..3914609 100644 --- a/poetry.lock +++ b/poetry.lock @@ -83,18 +83,6 @@ d = ["aiohttp (>=3.7.4)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] -[[package]] -name = "casefy" -version = "0.1.7" -description = "Utilities for string case conversion." -category = "main" -optional = false -python-versions = ">=3.6" -files = [ - {file = "casefy-0.1.7-py3-none-any.whl", hash = "sha256:ab05ff1c67f2a8e62d9f8986fa9a849416d61ac5413ec57d1f827b4f36589cf6"}, - {file = "casefy-0.1.7.tar.gz", hash = "sha256:6accce985a64b9edb2a610a29ac489d78fac80e52ff8f2d137e294f2f92b8027"}, -] - [[package]] name = "click" version = "8.1.3" @@ -363,11 +351,58 @@ files = [ {file = "MarkupSafe-2.1.2.tar.gz", hash = "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d"}, ] +[[package]] +name = "mypy" +version = "1.1.1" +description = "Optional static typing for Python" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mypy-1.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39c7119335be05630611ee798cc982623b9e8f0cff04a0b48dfc26100e0b97af"}, + {file = "mypy-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:61bf08362e93b6b12fad3eab68c4ea903a077b87c90ac06c11e3d7a09b56b9c1"}, + {file = "mypy-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbb19c9f662e41e474e0cff502b7064a7edc6764f5262b6cd91d698163196799"}, + {file = "mypy-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:315ac73cc1cce4771c27d426b7ea558fb4e2836f89cb0296cbe056894e3a1f78"}, + {file = "mypy-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:5cb14ff9919b7df3538590fc4d4c49a0f84392237cbf5f7a816b4161c061829e"}, + {file = "mypy-1.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:26cdd6a22b9b40b2fd71881a8a4f34b4d7914c679f154f43385ca878a8297389"}, + {file = "mypy-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5b5f81b40d94c785f288948c16e1f2da37203c6006546c5d947aab6f90aefef2"}, + {file = "mypy-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21b437be1c02712a605591e1ed1d858aba681757a1e55fe678a15c2244cd68a5"}, + {file = "mypy-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d809f88734f44a0d44959d795b1e6f64b2bbe0ea4d9cc4776aa588bb4229fc1c"}, + {file = "mypy-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:a380c041db500e1410bb5b16b3c1c35e61e773a5c3517926b81dfdab7582be54"}, + {file = "mypy-1.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b7c7b708fe9a871a96626d61912e3f4ddd365bf7f39128362bc50cbd74a634d5"}, + {file = "mypy-1.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1c10fa12df1232c936830839e2e935d090fc9ee315744ac33b8a32216b93707"}, + {file = "mypy-1.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0a28a76785bf57655a8ea5eb0540a15b0e781c807b5aa798bd463779988fa1d5"}, + {file = "mypy-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:ef6a01e563ec6a4940784c574d33f6ac1943864634517984471642908b30b6f7"}, + {file = "mypy-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d64c28e03ce40d5303450f547e07418c64c241669ab20610f273c9e6290b4b0b"}, + {file = "mypy-1.1.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:64cc3afb3e9e71a79d06e3ed24bb508a6d66f782aff7e56f628bf35ba2e0ba51"}, + {file = "mypy-1.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce61663faf7a8e5ec6f456857bfbcec2901fbdb3ad958b778403f63b9e606a1b"}, + {file = "mypy-1.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2b0c373d071593deefbcdd87ec8db91ea13bd8f1328d44947e88beae21e8d5e9"}, + {file = "mypy-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:2888ce4fe5aae5a673386fa232473014056967f3904f5abfcf6367b5af1f612a"}, + {file = "mypy-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:19ba15f9627a5723e522d007fe708007bae52b93faab00f95d72f03e1afa9598"}, + {file = "mypy-1.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:59bbd71e5c58eed2e992ce6523180e03c221dcd92b52f0e792f291d67b15a71c"}, + {file = "mypy-1.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9401e33814cec6aec8c03a9548e9385e0e228fc1b8b0a37b9ea21038e64cdd8a"}, + {file = "mypy-1.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4b398d8b1f4fba0e3c6463e02f8ad3346f71956b92287af22c9b12c3ec965a9f"}, + {file = "mypy-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:69b35d1dcb5707382810765ed34da9db47e7f95b3528334a3c999b0c90fe523f"}, + {file = "mypy-1.1.1-py3-none-any.whl", hash = "sha256:4e4e8b362cdf99ba00c2b218036002bdcdf1e0de085cdb296a49df03fb31dfc4"}, + {file = "mypy-1.1.1.tar.gz", hash = "sha256:ae9ceae0f5b9059f33dbc62dea087e942c0ccab4b7a003719cb70f9b8abfa32f"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = ">=3.10" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +python2 = ["typed-ast (>=1.4.0,<2)"] +reports = ["lxml"] + [[package]] name = "mypy-extensions" version = "1.0.0" description = "Type system extensions for programs checked with the mypy type checker." -category = "main" +category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -468,33 +503,6 @@ typing-extensions = ">=4.2.0" dotenv = ["python-dotenv (>=0.10.4)"] email = ["email-validator (>=1.0.3)"] -[[package]] -name = "pyserde" -version = "0.10.2" -description = "Yet another serialization library on top of dataclasses" -category = "main" -optional = false -python-versions = ">=3.7.0,<4.0.0" -files = [ - {file = "pyserde-0.10.2-py3-none-any.whl", hash = "sha256:1996c901d1a3b467058b538324dbf93824fa55b692c946287aaa7ebe4fc28283"}, - {file = "pyserde-0.10.2.tar.gz", hash = "sha256:5a5a47a18c1db02dcc0e0c391dd6f12a2a11a72190c032caedf73752ed2fe6d3"}, -] - -[package.dependencies] -casefy = "*" -jinja2 = "*" -tomli = {version = "*", optional = true, markers = "extra == \"toml\" or extra == \"all\""} -tomli-w = {version = "*", optional = true, markers = "extra == \"toml\" or extra == \"all\""} -typing_inspect = ">=0.4.0" - -[package.extras] -all = ["msgpack", "numpy (>1.21.0)", "numpy (>1.21.0)", "numpy (>1.21.0)", "numpy (>1.22.0)", "orjson", "pyyaml", "tomli", "tomli-w"] -msgpack = ["msgpack"] -numpy = ["numpy (>1.21.0)", "numpy (>1.21.0)", "numpy (>1.21.0)", "numpy (>1.22.0)"] -orjson = ["orjson"] -toml = ["tomli", "tomli-w"] -yaml = ["pyyaml"] - [[package]] name = "pyserial" version = "3.5" @@ -647,7 +655,7 @@ full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyam name = "tomli" version = "2.0.1" description = "A lil' TOML parser" -category = "main" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -655,18 +663,6 @@ files = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] -[[package]] -name = "tomli-w" -version = "1.0.0" -description = "A lil' TOML writer" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "tomli_w-1.0.0-py3-none-any.whl", hash = "sha256:9f2a07e8be30a0729e533ec968016807069991ae2fd921a78d42f429ae5f4463"}, - {file = "tomli_w-1.0.0.tar.gz", hash = "sha256:f463434305e0336248cac9c2dc8076b707d8a12d019dd349f5c1e382dd1ae1b9"}, -] - [[package]] name = "typing-extensions" version = "4.5.0" @@ -679,22 +675,6 @@ files = [ {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"}, ] -[[package]] -name = "typing-inspect" -version = "0.8.0" -description = "Runtime inspection utilities for typing module." -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "typing_inspect-0.8.0-py3-none-any.whl", hash = "sha256:5fbf9c1e65d4fa01e701fe12a5bca6c6e08a4ffd5bc60bfac028253a447c5188"}, - {file = "typing_inspect-0.8.0.tar.gz", hash = "sha256:8b1ff0c400943b6145df8119c41c244ca8207f1f10c9c057aeed1560e4806e3d"}, -] - -[package.dependencies] -mypy-extensions = ">=0.3.0" -typing-extensions = ">=3.7.4" - [[package]] name = "uvicorn" version = "0.21.0" @@ -717,4 +697,4 @@ standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "1d359a9e2cd5a9e84445da63d6675f54e49c60494858d90a39441d5046d6f83c" +content-hash = "8352d5071c797798e1db271d217bd6ce8b2197b41308498d474db523f9eb1bda" diff --git a/pyproject.toml b/pyproject.toml index 935c801..6506e8f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,12 +13,12 @@ uvicorn = "^0.21.0" databases = {extras = ["aiosqlite"], version = "^0.7.0"} jinja2 = "^3.1.2" fastapi-utils = "^0.2.1" -pyserde = {extras = ["toml"], version = "^0.10.2"} [tool.poetry.group.dev.dependencies] ruff = "^0.0.256" black = "^23.1.0" +mypy = "^1.1.1" [build-system] requires = ["poetry-core"] @@ -34,4 +34,8 @@ ignore = ["D203", "D213"] line-length = 120 [tool.black] -line-length = 120 \ No newline at end of file +line-length = 120 + +[[tool.mypy.overrides]] +module = "serial.*" +ignore_missing_imports = true \ No newline at end of file