diff --git a/README.md b/README.md index 9e787e7..35f3398 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,8 @@ Options: --help Show this message and exit. Commands: - audit - audit-bulk + audit Audit a consolidated Dropmate log. + audit-bulk Audit a directory of consolidated Dropmate logs. ``` @@ -44,21 +44,23 @@ The following environment variables are provided to help customize pipeline beha ### `dropmate audit` Process a consolidated Dropmate log CSV. #### Input Parameters -| Parameter | Description | Type | Default | -|------------------|------------------------------------|--------------|------------| -| `--log-filepath` | Path to Dropmate log CSV to parse. | `Path\|None` | GUI Prompt | -| `--min-alt-loss` | Threshold altitude delta. | `int` | `200` | -| `--min-firmware` | Threshold firmware version. | `int\|float` | `5` | +| Parameter | Description | Type | Default | +|------------------------|-----------------------------------------------|--------------|------------| +| `--log-filepath` | Path to Dropmate log CSV to parse. | `Path\|None` | GUI Prompt | +| `--min-alt-loss` | Threshold altitude delta. | `int` | `200` | +| `--min-firmware` | Threshold firmware version. | `int\|float` | `5` | +| `--time-delta-minutes` | Dropmate internal clock delta from real-time. | `int` | `60` | ### `dropmate audit-bulk` Batch process a directory of consolidated Dropmate log CSVs. #### Input Parameters -| Parameter | Description | Type | Default | -|------------------|-----------------------------------------------|--------------|------------| -| `--log-dir` | Path to Dropmate log directory to parse. | `Path\|None` | GUI Prompt | -| `--log-pattern` | Dropmate log file glob pattern.1,2 | `str` | `"*.csv"` | -| `--min-alt-loss` | Threshold altitude delta. | `int` | `200` | -| `--min-firmware` | Threshold firmware version. | `int\|float` | `5` | +| Parameter | Description | Type | Default | +|------------------------|-----------------------------------------------|--------------|------------| +| `--log-dir` | Path to Dropmate log directory to parse. | `Path\|None` | GUI Prompt | +| `--log-pattern` | Dropmate log file glob pattern.1,2 | `str` | `"*.csv"` | +| `--min-alt-loss` | Threshold altitude delta. | `int` | `200` | +| `--min-firmware` | Threshold firmware version. | `int\|float` | `5` | +| `--time-delta-minutes` | Dropmate internal clock delta from real-time. | `int` | `60` | 1. Case sensitivity is deferred to the host OS 2. Recursive globbing requires manual specification (e.g. `**/*.csv`) diff --git a/dropmate_py/cli.py b/dropmate_py/cli.py index 1ba9411..9690766 100644 --- a/dropmate_py/cli.py +++ b/dropmate_py/cli.py @@ -7,6 +7,7 @@ MIN_ALT_LOSS = 200 MIN_FIRMWARE = 5 +MIN_TIME_DELTA_MINUTES = 60 load_dotenv() start_dir = os.environ.get("PROMPT_START_DIR", ".") @@ -20,6 +21,7 @@ def audit( log_filepath: Path = typer.Option(exists=True, file_okay=True, dir_okay=False), min_alt_loss: int = typer.Option(default=MIN_ALT_LOSS), min_firmware: float = typer.Option(default=MIN_FIRMWARE), + time_delta_minutes: int = typer.Option(default=MIN_TIME_DELTA_MINUTES), ) -> None: """Audit a consolidated Dropmate log.""" if log_filepath is None: @@ -41,6 +43,7 @@ def audit_bulk( log_pattern: str = typer.Option("*.csv"), min_alt_loss: int = typer.Option(default=MIN_ALT_LOSS), min_firmware: float = typer.Option(default=MIN_FIRMWARE), + time_delta_minutes: int = typer.Option(default=MIN_TIME_DELTA_MINUTES), ) -> None: """Audit a directory of consolidated Dropmate logs.""" if log_dir is None: diff --git a/dropmate_py/parser.py b/dropmate_py/parser.py new file mode 100644 index 0000000..ed8131a --- /dev/null +++ b/dropmate_py/parser.py @@ -0,0 +1,48 @@ +from __future__ import annotations + +import typing as t +from dataclasses import dataclass, fields + + +@dataclass +class ColumnIndices: + """ + Column index in the parsed Dropmate compiled log CSV. + + Attribute names are assumed to correspond to the Dropmate log header name. Columns contained may + vary by Dropmate app version, so if they are not present in the compiled log they will remain at + a value of `-1`, indicating they were not found. + """ + + serial_number: int = -1 + uid: int = -1 + battery: int = -1 + device_health: int = -1 + firmware_version: int = -1 + start_time_utc: int = -1 + end_time_utc: int = -1 + start_barometric_altitude_msl_ft: int = -1 + end_barometric_altitude_msl_ft: int = -1 + dropmate_internal_time: int = -1 + last_scanned_time: int = -1 + + def __iter__(self) -> t.Generator[str, None, None]: + for f in fields(self): + yield f.name + + @classmethod + def from_header(cls, header: str) -> ColumnIndices: + """Attempt to match attribute names to their corresponding data columns.""" + indices = ColumnIndices() + + # Some column names may have stray spaces in them, listify so we can iterate over repeatedly + col_names = [c.strip().lower() for c in header.split(",")] + for query_col in indices: + for idx, col in enumerate(col_names): + if col == query_col: + setattr(indices, query_col, idx) + + return indices + + def __str__(self) -> str: # pragma: no cover + return ", ".join(f"({f}, {getattr(self, f)})" for f in self) diff --git a/poetry.lock b/poetry.lock index 3955133..c2ab368 100644 --- a/poetry.lock +++ b/poetry.lock @@ -621,20 +621,6 @@ tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] -[[package]] -name = "pytest-check" -version = "2.2.0" -description = "A pytest plugin that allows multiple failures per test." -optional = false -python-versions = ">3.7" -files = [ - {file = "pytest_check-2.2.0-py3-none-any.whl", hash = "sha256:9769c9a13889d1f3298ee6b0ff0b84b59c8af4d927f7d52d070513601b9979c2"}, - {file = "pytest_check-2.2.0.tar.gz", hash = "sha256:ec93701d930de5a628957cd54fe918e324da1b811040e64cb70cae2857cf26ff"}, -] - -[package.dependencies] -pytest = "*" - [[package]] name = "pytest-cov" version = "4.1.0" @@ -843,13 +829,13 @@ files = [ [[package]] name = "virtualenv" -version = "20.24.0" +version = "20.24.1" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" files = [ - {file = "virtualenv-20.24.0-py3-none-any.whl", hash = "sha256:18d1b37fc75cc2670625702d76849a91ebd383768b4e91382a8d51be3246049e"}, - {file = "virtualenv-20.24.0.tar.gz", hash = "sha256:e2a7cef9da880d693b933db7654367754f14e20650dc60e8ee7385571f8593a3"}, + {file = "virtualenv-20.24.1-py3-none-any.whl", hash = "sha256:01aacf8decd346cf9a865ae85c0cdc7f64c8caa07ff0d8b1dfc1733d10677442"}, + {file = "virtualenv-20.24.1.tar.gz", hash = "sha256:2ef6a237c31629da6442b0bcaa3999748108c7166318d1f55cc9f8d7294e97bd"}, ] [package.dependencies] @@ -864,4 +850,4 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "4b2ac8471c534dc6148f9ea0696b5013c8f373a46053ef1acdcd747f99e73ea3" +content-hash = "659ae1fe23148cf6256d101e720508c3b572c5771abdd05534825c92b72c6cfc" diff --git a/pyproject.toml b/pyproject.toml index 05ca0f4..47228fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,6 @@ mypy = "^1.0" pep8-naming = "^0.13" pre-commit = "^3.0" pytest = "^7.2" -pytest-check = "^2.1" pytest-cov = "^4.0" pytest-randomly = "^3.12" tox = "^4.4" diff --git a/tests/test_heading_parse.py b/tests/test_heading_parse.py new file mode 100644 index 0000000..da8dcbd --- /dev/null +++ b/tests/test_heading_parse.py @@ -0,0 +1,44 @@ +from dropmate_py.parser import ColumnIndices + +SAMPLE_FULL_HEADER = "serial_number,uid,battery,device_health, firmware_version,log_timestamp,log_altitude,total_flights,flights_over_18kft,recorded_flights,flight_index,start_time_utc,end_time_utc,start_barometric_altitude_msl_ft,end_barometric_altitude_msl_ft,dropmate_internal_time,last_scanned_time" + + +def test_parse_indices_full_header() -> None: + TRUTH_INDICES = ColumnIndices( + serial_number=0, + uid=1, + battery=2, + device_health=3, + firmware_version=4, + start_time_utc=11, + end_time_utc=12, + start_barometric_altitude_msl_ft=13, + end_barometric_altitude_msl_ft=14, + dropmate_internal_time=15, + last_scanned_time=16, + ) + + indices = ColumnIndices.from_header(SAMPLE_FULL_HEADER) + assert indices == TRUTH_INDICES + + +SAMPLE_OLD_HEADER = "serial_number,uid,battery,log_timestamp,log_altitude,total_flights,prior_flights,flights_over_18kft,recorded_flights,flight_index,start_time_utc,end_time_utc,start_barometric_altitude_msl_ft,end_barometric_altitude_msl_ft" + + +def test_parse_indices_old_header() -> None: + TRUTH_INDICES = ColumnIndices( + serial_number=0, + uid=1, + battery=2, + device_health=-1, + firmware_version=-1, + start_time_utc=10, + end_time_utc=11, + start_barometric_altitude_msl_ft=12, + end_barometric_altitude_msl_ft=13, + dropmate_internal_time=-1, + last_scanned_time=-1, + ) + + indices = ColumnIndices.from_header(SAMPLE_OLD_HEADER) + assert indices == TRUTH_INDICES diff --git a/tests/test_hello.py b/tests/test_hello.py deleted file mode 100644 index 36a5248..0000000 --- a/tests/test_hello.py +++ /dev/null @@ -1,2 +0,0 @@ -def test_placeholder() -> None: - ...