diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml new file mode 100644 index 0000000..354dc8d --- /dev/null +++ b/.gitea/workflows/test.yml @@ -0,0 +1,30 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.13" + + - name: Install dependencies + run: | + pip install ruff + pip install -r requirements.txt + + - name: Lint + run: ruff check . + + - name: Run tests + run: pytest tests/ -v diff --git a/app.py b/app.py index 8222165..99894d1 100644 --- a/app.py +++ b/app.py @@ -35,7 +35,7 @@ def _safe_next(default_url): def _filter_compatible(query, battery_size): """Restrict a Battery query to size-compatible batteries (matching size or unsized).""" if battery_size: - query = query.filter((Battery.size == battery_size) | (Battery.size == None)) + query = query.filter((Battery.size == battery_size) | (Battery.size == None)) # noqa: E711 return query @@ -130,7 +130,7 @@ def create_app(config_object="config"): avail_count = sum(1 for b in batteries if b.status == "available") installed_count = sum(1 for b in batteries if b.status == "installed") retired_count = sum(1 for b in batteries if b.status == "retired") - devices = db.query(Device).filter(Device.parent_id == None).all() + devices = db.query(Device).filter(Device.parent_id == None).all() # noqa: E711 total_devices = len(devices) full_devices = sum( 1 for d in devices @@ -283,16 +283,16 @@ def create_app(config_object="config"): .order_by(BatteryPctLog.recorded_at.desc()) .all()) charge_logs_data = [ - {"id": l.id, "date": l.charged_date, "cycles": l.increment_cycles, "notes": l.notes or ""} - for l in charge_logs + {"id": log.id, "date": log.charged_date, "cycles": log.increment_cycles, "notes": log.notes or ""} + for log in charge_logs ] capacity_tests_data = [ {"id": t.id, "date": t.tested_date, "mah": t.tested_capacity_mah, "notes": t.notes or ""} for t in sorted(capacity_tests, key=lambda t: (t.tested_date, t.id), reverse=True) ] pct_logs_data = [ - {"recorded_at": str(l.recorded_at), "pct": l.percentage, "source": l.source or ""} - for l in pct_logs + {"recorded_at": str(log.recorded_at), "pct": log.percentage, "source": log.source or ""} + for log in pct_logs ] return render_template("battery_detail.html", battery=battery, storage_locations=storage_locations, @@ -699,7 +699,7 @@ def create_app(config_object="config"): @app.route("/device/") def device_list(): - devices = db.query(Device).filter(Device.parent_id == None).order_by(Device.name).all() + devices = db.query(Device).filter(Device.parent_id == None).order_by(Device.name).all() # noqa: E711 device_types = sorted({d.device_type for d in devices if d.device_type}) device_locations = sorted({d.location for d in devices if d.location}) device_battery_sizes = sorted({d.battery_size for d in devices if d.battery_size}) @@ -1237,9 +1237,9 @@ def create_app(config_object="config"): buf = io.StringIO() w = csv.writer(buf) w.writerow(["id", "battery_id", "battery_label", "charged_date", "increment_cycles", "notes"]) - for l in rows: - w.writerow([l.id, l.battery_id, l.battery.label, - l.charged_date, l.increment_cycles, l.notes or ""]) + for row in rows: + w.writerow([row.id, row.battery_id, row.battery.label, + row.charged_date, row.increment_cycles, row.notes or ""]) return buf.getvalue() def _capacity_tests_csv(): @@ -1257,9 +1257,9 @@ def create_app(config_object="config"): buf = io.StringIO() w = csv.writer(buf) w.writerow(["id", "battery_id", "battery_label", "percentage", "recorded_at", "source"]) - for l in rows: - w.writerow([l.id, l.battery_id, l.battery.label, - l.percentage, l.recorded_at, l.source or ""]) + for row in rows: + w.writerow([row.id, row.battery_id, row.battery.label, + row.percentage, row.recorded_at, row.source or ""]) return buf.getvalue() @app.route("/export") @@ -1334,10 +1334,10 @@ def create_app(config_object="config"): for d in devices ], "charge_logs": [ - {"id": l.id, "battery_id": l.battery_id, "battery_label": l.battery.label, - "charged_date": l.charged_date, "increment_cycles": l.increment_cycles, - "notes": l.notes} - for l in charge_logs + {"id": log.id, "battery_id": log.battery_id, "battery_label": log.battery.label, + "charged_date": log.charged_date, "increment_cycles": log.increment_cycles, + "notes": log.notes} + for log in charge_logs ], "capacity_tests": [ {"id": t.id, "battery_id": t.battery_id, "battery_label": t.battery.label, @@ -1346,9 +1346,9 @@ def create_app(config_object="config"): for t in capacity_tests ], "pct_logs": [ - {"id": l.id, "battery_id": l.battery_id, "battery_label": l.battery.label, - "percentage": l.percentage, "recorded_at": l.recorded_at, "source": l.source} - for l in pct_logs + {"id": log.id, "battery_id": log.battery_id, "battery_label": log.battery.label, + "percentage": log.percentage, "recorded_at": log.recorded_at, "source": log.source} + for log in pct_logs ], } return Response(json.dumps(payload, indent=2), mimetype="application/json", diff --git a/ha_poller.py b/ha_poller.py index abed3f9..066a6eb 100644 --- a/ha_poller.py +++ b/ha_poller.py @@ -37,7 +37,7 @@ class HaPoller: self._poll_once() def _poll_once(self): - from models import Battery, BatteryPctLog, Device # local import avoids circular-import risk + from models import BatteryPctLog, Device # local import avoids circular-import risk session = self._Session() try: diff --git a/models.py b/models.py index 462ebb6..5ec1eb3 100644 --- a/models.py +++ b/models.py @@ -1,4 +1,3 @@ -from datetime import datetime from sqlalchemy import Column, Integer, String, Text, ForeignKey, Table, UniqueConstraint from sqlalchemy.orm import declarative_base, relationship diff --git a/sbin/gen_icons.py b/sbin/gen_icons.py index 7ca41e3..0c1e748 100644 --- a/sbin/gen_icons.py +++ b/sbin/gen_icons.py @@ -1,5 +1,7 @@ """Generate solid-color PNG icons for PWA manifest using stdlib only (no Pillow).""" -import zlib, struct, os +import zlib +import struct +import os def make_png(size, rgb=(0x25, 0x63, 0xEB)): """Create a minimal valid RGB PNG of the given size filled with one color.""" diff --git a/sbin/migrate_parent_key.py b/sbin/migrate_parent_key.py index d32c1d8..bb96b96 100644 --- a/sbin/migrate_parent_key.py +++ b/sbin/migrate_parent_key.py @@ -16,7 +16,6 @@ Usage: import os import shutil -import sys from datetime import date from pathlib import Path diff --git a/sbin/setup_mariadb.py b/sbin/setup_mariadb.py index 5b087a6..7c3291a 100755 --- a/sbin/setup_mariadb.py +++ b/sbin/setup_mariadb.py @@ -31,11 +31,11 @@ from pathlib import Path REPO_ROOT = Path(__file__).resolve().parent.parent sys.path.insert(0, str(REPO_ROOT)) -from sqlalchemy import create_engine, text -from sqlalchemy.orm import sessionmaker +from sqlalchemy import create_engine, text # noqa: E402 +from sqlalchemy.orm import sessionmaker # noqa: E402 -import config -from models import Base, Battery, BatteryPctLog, CapacityTest, ChargeLog, Device +import config # noqa: E402 +from models import Base, Battery, BatteryPctLog, CapacityTest, ChargeLog, Device # noqa: E402 # --------------------------------------------------------------------------- @@ -62,7 +62,7 @@ def collect_credentials() -> str: """Return a MariaDB SQLAlchemy URL, prompting for any missing pieces.""" url = os.environ.get("MARIADB_URL", "").strip() if url: - print(f" Using MARIADB_URL from environment.") + print(" Using MARIADB_URL from environment.") return url print("Enter MariaDB connection details (press Enter to accept defaults):\n") diff --git a/tests/test_acceptance.py b/tests/test_acceptance.py index 4a57623..4d08cf4 100644 --- a/tests/test_acceptance.py +++ b/tests/test_acceptance.py @@ -8,7 +8,8 @@ The `seeded_client` fixture pre-populates: BrandX 002 (id=3, retired) """ -import pytest +import io +import json as _json # ------------------------------------------------------------------ # @@ -169,7 +170,7 @@ def test_edit_details_percentage_valid(seeded_client): data = _json.loads(seeded_client.get("/export/all.json").data) bat = next(b for b in data["batteries"] if b["id"] == 1) assert bat["battery_percentage"] == 100 - assert any(l["battery_id"] == 1 and l["source"] == "manual" for l in data["pct_logs"]) + assert any(entry["battery_id"] == 1 and entry["source"] == "manual" for entry in data["pct_logs"]) def test_assign_battery(seeded_client): @@ -699,9 +700,6 @@ def test_bulk_install_filters_by_size(client): # Import # ------------------------------------------------------------------ # -import io -import json as _json - def _make_import_payload(devices=None, batteries=None, charge_logs=None, capacity_tests=None, pct_logs=None): diff --git a/tests/test_coverage.py b/tests/test_coverage.py index c394b86..ae3f8d5 100644 --- a/tests/test_coverage.py +++ b/tests/test_coverage.py @@ -6,7 +6,6 @@ ha_client/ha_poller branches, export/import edge cases, bulk actions. import io import json -import pytest from unittest.mock import patch, MagicMock diff --git a/tests/test_ha_integration.py b/tests/test_ha_integration.py index 7bd321b..91685dc 100644 --- a/tests/test_ha_integration.py +++ b/tests/test_ha_integration.py @@ -484,7 +484,8 @@ def test_poll_skips_update_when_percentage_unchanged(ha_app, ha_client_f): engine = create_engine(ha_app.config["SQLALCHEMY_DATABASE_URI"]) s = sessionmaker(bind=engine)() s.get(Battery, 1).battery_percentage = 50 - s.commit(); s.close() + s.commit() + s.close() from ha_client import HomeAssistantClient from ha_poller import HaPoller