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..f52916e 100644 --- a/aqimon/config.py +++ b/aqimon/config.py @@ -1,11 +1,9 @@ """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" @@ -25,6 +23,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,6 +36,8 @@ 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", ) @@ -49,18 +53,16 @@ def save_config(config: Config, path: str): 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..6743885 100644 --- a/poetry.lock +++ b/poetry.lock @@ -363,6 +363,53 @@ 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" @@ -717,4 +764,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 = "3b689e094ddb77cb29ffc7b6bc8eca9d8a35836bdf4049996b012b344fc36ca3" diff --git a/pyproject.toml b/pyproject.toml index 935c801..677f6a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ 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 +35,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