From d2f1a913149a177f3769f586d7019513a9cf6247 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 15 Jun 2026 23:01:46 +0100 Subject: [PATCH 1/3] Add ruff and fix all issues raised --- core/pyproject.toml | 5 ++ core/src/vf_core/asset_manager.py | 3 +- core/src/vf_core/config_manager.py | 7 +- core/src/vf_core/main.py | 24 +++--- core/src/vf_core/marine_utils.py | 1 + core/src/vf_core/message_bus.py | 5 +- core/src/vf_core/network_manager.py | 12 +-- core/src/vf_core/plugin_manager.py | 6 +- core/src/vf_core/plugin_types.py | 4 +- core/src/vf_core/render_strategies.py | 5 +- core/src/vf_core/screen_manager.py | 4 +- core/src/vf_core/text_utils.py | 1 + core/src/vf_core/vessel_manager.py | 5 +- core/src/vf_core/vessel_repository.py | 3 +- core/src/vf_core/web_admin/api/auth.py | 15 ++-- core/src/vf_core/web_admin/api/config.py | 8 +- core/src/vf_core/web_admin/api/network.py | 71 +++++++++--------- core/src/vf_core/web_admin/api/plugins.py | 15 ++-- core/src/vf_core/web_admin/api/system.py | 8 +- core/src/vf_core/web_admin/auth.py | 19 +++-- core/src/vf_core/web_admin/dependencies.py | 8 +- core/src/vf_core/web_admin/main.py | 13 ++-- .../src/button_controller/__init__.py | 26 ++++--- .../src/ais_decoder_processor/__init__.py | 11 ++- .../src/com_message_source/__init__.py | 17 +++-- .../src/daisy_message_source/__init__.py | 31 +++++--- .../src/mock_message_source/__init__.py | 4 +- .../src/image_renderer/__init__.py | 8 +- .../src/inky_renderer/__init__.py | 14 ++-- .../map_screen/src/map_screen/__init__.py | 3 +- .../map_screen/src/map_screen/bounds.py | 2 +- .../map_screen/src/map_screen/layout.py | 1 - .../table_screen/src/table_screen/__init__.py | 20 +++-- .../src/table_screen/layouts/__init__.py | 6 +- .../src/table_screen/layouts/base.py | 1 - .../table_screen/layouts/landscape_large.py | 2 +- .../layouts/landscape_standard.py | 1 + .../table_screen/layouts/portrait_large.py | 6 +- .../table_screen/layouts/portrait_standard.py | 1 + .../zone_screen/src/zone_screen/__init__.py | 18 +++-- .../src/zone_screen/layouts/__init__.py | 6 +- .../zone_screen/layouts/landscape_compact.py | 4 +- .../zone_screen/layouts/landscape_large.py | 4 +- .../zone_screen/layouts/landscape_standard.py | 2 +- .../src/zone_screen/layouts/portrait_large.py | 13 +++- .../zone_screen/layouts/portrait_standard.py | 5 +- ruff.toml | 34 +++++++++ scripts/network_mode_service.py | 75 +++++++++---------- scripts/render_map.py | 8 +- scripts/render_table.py | 6 +- scripts/render_zone.py | 13 ++-- 51 files changed, 341 insertions(+), 243 deletions(-) create mode 100644 ruff.toml diff --git a/core/pyproject.toml b/core/pyproject.toml index 3f69242..dfacd7b 100644 --- a/core/pyproject.toml +++ b/core/pyproject.toml @@ -16,6 +16,11 @@ dependencies = [ "pillow>=12.0.0" ] +[project.optional-dependencies] +dev = [ + "ruff==0.15.17", +] + [project.scripts] vf = "vf_core.main:main" diff --git a/core/src/vf_core/asset_manager.py b/core/src/vf_core/asset_manager.py index 7f89687..e022bda 100644 --- a/core/src/vf_core/asset_manager.py +++ b/core/src/vf_core/asset_manager.py @@ -1,6 +1,7 @@ +from dataclasses import dataclass from pathlib import Path + from PIL import ImageFont -from dataclasses import dataclass @dataclass diff --git a/core/src/vf_core/config_manager.py b/core/src/vf_core/config_manager.py index 0e65dc4..e6b5828 100644 --- a/core/src/vf_core/config_manager.py +++ b/core/src/vf_core/config_manager.py @@ -1,8 +1,9 @@ +import copy import tomllib -import tomli_w from pathlib import Path from typing import Any -import copy + +import tomli_w class ConfigManager: @@ -67,7 +68,7 @@ def get(self, key: str, default: Any = None) -> Any: Returns: Any: A deep copy of the config value, or the default if not found. """ - + keys = key.split(".") value = self._cfg diff --git a/core/src/vf_core/main.py b/core/src/vf_core/main.py index 8996032..4daeb24 100644 --- a/core/src/vf_core/main.py +++ b/core/src/vf_core/main.py @@ -10,33 +10,33 @@ import argparse import asyncio -from contextlib import suppress -from functools import partial import logging import os import signal import sys +from contextlib import suppress +from functools import partial +from logging.handlers import RotatingFileHandler from pathlib import Path -from logging.handlers import RotatingFileHandler +from .asset_manager import AssetManager +from .config_manager import ConfigManager +from .message_bus import MessageBus +from .network_manager import NetworkManager +from .plugin_manager import PluginManager from .plugin_types import ( + GROUP_CONTROLLERS, GROUP_PROCESSORS, GROUP_RENDERER, GROUP_SOURCES, - GROUP_CONTROLLERS, Plugin, RendererPlugin, ) -from .message_bus import MessageBus -from .plugin_manager import PluginManager -from .config_manager import ConfigManager +from .screen_manager import ScreenManager from .vessel_manager import VesselManager from .vessel_repository import VesselRepository -from .screen_manager import ScreenManager -from .network_manager import NetworkManager -from .asset_manager import AssetManager -from .web_admin.main import start_admin_server from .web_admin import auth +from .web_admin.main import start_admin_server def _default_data_dir() -> Path: @@ -78,7 +78,7 @@ def _log_admin_status( if task.cancelled(): # Manual shutdown - nothing to report return - + try: task.result() logger.warning("Admin server stopped unexpectedly but cleanly") diff --git a/core/src/vf_core/marine_utils.py b/core/src/vf_core/marine_utils.py index 3d5e261..906ebc0 100644 --- a/core/src/vf_core/marine_utils.py +++ b/core/src/vf_core/marine_utils.py @@ -1,5 +1,6 @@ """AIS data and formatting helpers.""" from __future__ import annotations + import math from typing import NamedTuple diff --git a/core/src/vf_core/message_bus.py b/core/src/vf_core/message_bus.py index 9f30e6f..fdb253a 100644 --- a/core/src/vf_core/message_bus.py +++ b/core/src/vf_core/message_bus.py @@ -1,6 +1,7 @@ import asyncio from collections import defaultdict -from typing import Any, AsyncIterator +from collections.abc import AsyncIterator +from typing import Any _SHUTDOWN = object() @@ -61,7 +62,7 @@ async def subscribe(self, topic: str) -> AsyncIterator[Any]: Yields: Any: Messages published to the specified topic in the order received. """ - + q: asyncio.Queue[Any] = asyncio.Queue(maxsize=1000) async with self._lock: self._subs[topic].append(q) diff --git a/core/src/vf_core/network_manager.py b/core/src/vf_core/network_manager.py index 01fa143..d66b4af 100644 --- a/core/src/vf_core/network_manager.py +++ b/core/src/vf_core/network_manager.py @@ -1,12 +1,12 @@ -import subprocess +import asyncio import json -import time import logging +import subprocess +import time +from dataclasses import asdict, dataclass +from datetime import datetime from pathlib import Path from typing import Any -from dataclasses import dataclass, asdict -from datetime import datetime -import asyncio @dataclass @@ -59,7 +59,7 @@ def _load_config(self) -> NetworkConfig: try: if self.CONFIG_FILE.exists(): - with open(self.CONFIG_FILE, "r") as f: + with open(self.CONFIG_FILE) as f: data = json.load(f) return NetworkConfig.from_dict(data) except Exception: diff --git a/core/src/vf_core/plugin_manager.py b/core/src/vf_core/plugin_manager.py index 9d213fd..6a5bbfb 100644 --- a/core/src/vf_core/plugin_manager.py +++ b/core/src/vf_core/plugin_manager.py @@ -1,7 +1,7 @@ -from importlib.metadata import entry_points, EntryPoint -from collections.abc import Iterable, Callable -from typing import Any import logging +from collections.abc import Callable, Iterable +from importlib.metadata import EntryPoint, entry_points +from typing import Any from .plugin_types import GROUP_SCHEMAS, ConfigFieldType diff --git a/core/src/vf_core/plugin_types.py b/core/src/vf_core/plugin_types.py index 47faddd..9715303 100644 --- a/core/src/vf_core/plugin_types.py +++ b/core/src/vf_core/plugin_types.py @@ -1,6 +1,6 @@ -from typing import Protocol, runtime_checkable, Any, TYPE_CHECKING -from enum import Enum from dataclasses import dataclass +from enum import Enum +from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable if TYPE_CHECKING: from PIL import Image diff --git a/core/src/vf_core/render_strategies.py b/core/src/vf_core/render_strategies.py index 73d345a..9ee3c60 100644 --- a/core/src/vf_core/render_strategies.py +++ b/core/src/vf_core/render_strategies.py @@ -1,8 +1,9 @@ import asyncio +import logging import time -from typing import Callable, Awaitable, Any +from collections.abc import Awaitable, Callable from contextlib import suppress -import logging +from typing import Any class PeriodicRenderStrategy: diff --git a/core/src/vf_core/screen_manager.py b/core/src/vf_core/screen_manager.py index a2ef4f5..8918012 100644 --- a/core/src/vf_core/screen_manager.py +++ b/core/src/vf_core/screen_manager.py @@ -1,16 +1,16 @@ import asyncio import logging from contextlib import suppress - from pathlib import Path from vf_core.config_manager import ConfigManager + +from .asset_manager import AssetManager from .error_screen import ErrorScreen from .message_bus import MessageBus from .plugin_manager import PluginManager from .plugin_types import GROUP_SCREENS, RendererPlugin, ScreenPlugin from .vessel_manager import VesselManager -from .asset_manager import AssetManager class ScreenManager: diff --git a/core/src/vf_core/text_utils.py b/core/src/vf_core/text_utils.py index 7a0e849..944eae9 100644 --- a/core/src/vf_core/text_utils.py +++ b/core/src/vf_core/text_utils.py @@ -4,6 +4,7 @@ anchored text drawing and font metric helpers. """ from __future__ import annotations + from PIL import ImageDraw, ImageFont __all__ = ["FONT_FLOOR", "split_two", "TextRenderingMixin"] diff --git a/core/src/vf_core/vessel_manager.py b/core/src/vf_core/vessel_manager.py index 2598d21..369b7ed 100644 --- a/core/src/vf_core/vessel_manager.py +++ b/core/src/vf_core/vessel_manager.py @@ -1,10 +1,11 @@ import asyncio -import time import logging import math +import time from contextlib import suppress -from .message_bus import MessageBus from typing import Any + +from .message_bus import MessageBus from .vessel_repository import VesselRepository diff --git a/core/src/vf_core/vessel_repository.py b/core/src/vf_core/vessel_repository.py index 6573bff..69622b8 100644 --- a/core/src/vf_core/vessel_repository.py +++ b/core/src/vf_core/vessel_repository.py @@ -1,9 +1,10 @@ import json -import aiosqlite import logging from pathlib import Path from typing import Any +import aiosqlite + class VesselRepository: def __init__(self, db_path: Path | str) -> None: diff --git a/core/src/vf_core/web_admin/api/auth.py b/core/src/vf_core/web_admin/api/auth.py index 67a7087..17e20e2 100644 --- a/core/src/vf_core/web_admin/api/auth.py +++ b/core/src/vf_core/web_admin/api/auth.py @@ -1,15 +1,16 @@ -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta + +import jwt from argon2 import PasswordHasher, Type from argon2.exceptions import VerifyMismatchError -from fastapi import APIRouter, HTTPException, Depends, status -import jwt +from fastapi import APIRouter, Depends, HTTPException, status from pydantic import BaseModel from ..auth import ( - get_or_create_secret_key, get_admin_credentials, - set_admin_credentials, + get_or_create_secret_key, is_admin_configured, + set_admin_credentials, ) ph = PasswordHasher(type=Type.ID) @@ -57,13 +58,13 @@ async def login( try: ph.verify(credentials["password_hash"], request.password) except VerifyMismatchError: - raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials") + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials") from None # Generate JWT token = jwt.encode( { "username": request.username, - "exp": datetime.now(timezone.utc) + timedelta(days=7), + "exp": datetime.now(UTC) + timedelta(days=7), }, secret_key, algorithm="HS256", diff --git a/core/src/vf_core/web_admin/api/config.py b/core/src/vf_core/web_admin/api/config.py index 36547eb..25840be 100644 --- a/core/src/vf_core/web_admin/api/config.py +++ b/core/src/vf_core/web_admin/api/config.py @@ -1,8 +1,8 @@ -from fastapi import APIRouter, HTTPException, Depends, status -from pydantic import BaseModel -from typing import Any import logging +from typing import Any +from fastapi import APIRouter, Depends, HTTPException, status +from pydantic import BaseModel from vf_core.config_manager import ConfigManager from vf_core.plugin_manager import PluginManager from vf_core.plugin_types import GROUP_SCHEMAS, ConfigField, ConfigFieldType @@ -107,7 +107,7 @@ async def update_config( cm.save() return {"success": True, "path": update.path, "value": value} except Exception as e: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) from e def _require_dict_keys(value: Any, keys: tuple[str, ...], label: str) -> None: diff --git a/core/src/vf_core/web_admin/api/network.py b/core/src/vf_core/web_admin/api/network.py index 3a70888..5ed112f 100644 --- a/core/src/vf_core/web_admin/api/network.py +++ b/core/src/vf_core/web_admin/api/network.py @@ -1,8 +1,7 @@ -from fastapi import APIRouter, Depends, HTTPException, Request -from pydantic import BaseModel, Field -from typing import Optional, List import logging +from fastapi import APIRouter, Depends, HTTPException, Request +from pydantic import BaseModel, Field from vf_core.web_admin.dependencies import verify_token logger = logging.getLogger(__name__) @@ -14,21 +13,21 @@ class NetworkStatusResponse(BaseModel): actual_mode: str timestamp: str config_file: str - ap_ssid: Optional[str] = None - ap_ip: Optional[str] = None - connected_ssid: Optional[str] = None - ip_address: Optional[str] = None + ap_ssid: str | None = None + ap_ip: str | None = None + connected_ssid: str | None = None + ip_address: str | None = None class NetworkConfigResponse(BaseModel): """Response model for network configuration""" mode: str ap_ssid: str - ap_password: Optional[str] + ap_password: str | None ap_channel: int ap_ip: str - client_ssid: Optional[str] - client_password: Optional[str] + client_ssid: str | None + client_password: str | None auto_fallback: bool fallback_timeout: int @@ -36,24 +35,24 @@ class NetworkConfigResponse(BaseModel): class NetworkInfo(BaseModel): """Model for scanned network information""" ssid: str - quality: Optional[str] - signal: Optional[str] + quality: str | None + signal: str | None encrypted: bool class APModeRequest(BaseModel): """Request model for AP mode configuration""" - ssid: Optional[str] = Field(None, min_length=1, max_length=32) - password: Optional[str] = Field(None, min_length=8, max_length=63) - channel: Optional[int] = Field(None, ge=1, le=11) + ssid: str | None = Field(None, min_length=1, max_length=32) + password: str | None = Field(None, min_length=8, max_length=63) + channel: int | None = Field(None, ge=1, le=11) class ClientModeRequest(BaseModel): """Request model for client mode configuration""" ssid: str = Field(..., min_length=1, max_length=32) - password: Optional[str] = Field(None, max_length=63) - auto_fallback: Optional[bool] = True - fallback_timeout: Optional[int] = Field(60, ge=30, le=300) + password: str | None = Field(None, max_length=63) + auto_fallback: bool | None = True + fallback_timeout: int | None = Field(60, ge=30, le=300) @router.get("/status", response_model=NetworkStatusResponse, dependencies=[Depends(verify_token)]) @@ -66,7 +65,7 @@ async def get_network_status(request: Request): return status except Exception as e: logger.error(f"Error getting network status: {e}") - raise HTTPException(status_code=500, detail=str(e)) + raise HTTPException(status_code=500, detail=str(e)) from e @router.get("/config", response_model=NetworkConfigResponse, dependencies=[Depends(verify_token)]) @@ -79,10 +78,10 @@ async def get_network_config(request: Request): return config except Exception as e: logger.error(f"Error getting network config: {e}") - raise HTTPException(status_code=500, detail=str(e)) + raise HTTPException(status_code=500, detail=str(e)) from e -@router.get("/scan", response_model=List[NetworkInfo], dependencies=[Depends(verify_token)]) +@router.get("/scan", response_model=list[NetworkInfo], dependencies=[Depends(verify_token)]) async def scan_networks(request: Request): """Scan for available networks""" try: @@ -91,19 +90,19 @@ async def scan_networks(request: Request): return networks except Exception as e: logger.error(f"Error scanning networks: {e}") - raise HTTPException(status_code=500, detail=str(e)) + raise HTTPException(status_code=500, detail=str(e)) from e @router.post("/mode/ap", dependencies=[Depends(verify_token)]) async def set_ap_mode(request: Request, config: APModeRequest): """Configure and schedule AP mode - + This will save the configuration and schedule it for the next reboot. The user will be advised to reboot for changes to take effect. """ try: network_manager = request.app.state.network_manager - + # Update AP configuration if provided if config.ssid or config.password or config.channel: success, message = network_manager.update_ap_config( @@ -113,10 +112,10 @@ async def set_ap_mode(request: Request, config: APModeRequest): ) if not success: raise HTTPException(status_code=400, detail=message) - + # Schedule mode change success, message = network_manager.schedule_mode_change('ap') - + if success: return { "success": True, @@ -125,12 +124,12 @@ async def set_ap_mode(request: Request, config: APModeRequest): } else: raise HTTPException(status_code=500, detail=message) - + except HTTPException: raise except Exception as e: logger.error(f"Error setting AP mode: {e}") - raise HTTPException(status_code=500, detail=str(e)) + raise HTTPException(status_code=500, detail=str(e)) from e @router.post("/mode/offline", dependencies=[Depends(verify_token)]) @@ -153,19 +152,19 @@ async def set_offline_mode(request: Request): raise except Exception as e: logger.error(f"Error setting offline mode: {e}") - raise HTTPException(status_code=500, detail=str(e)) + raise HTTPException(status_code=500, detail=str(e)) from e @router.post("/mode/client", dependencies=[Depends(verify_token)]) async def set_client_mode(request: Request, config: ClientModeRequest): """Configure and schedule client mode - + This will save the configuration and schedule it for the next reboot. The user will be advised to reboot for changes to take effect. """ try: network_manager = request.app.state.network_manager - + # Update client configuration success, message = network_manager.update_client_config( ssid=config.ssid, @@ -173,13 +172,13 @@ async def set_client_mode(request: Request, config: ClientModeRequest): auto_fallback=config.auto_fallback, fallback_timeout=config.fallback_timeout ) - + if not success: raise HTTPException(status_code=400, detail=message) - + # Schedule mode change success, message = network_manager.schedule_mode_change('client') - + if success: return { "success": True, @@ -188,9 +187,9 @@ async def set_client_mode(request: Request, config: ClientModeRequest): } else: raise HTTPException(status_code=500, detail=message) - + except HTTPException: raise except Exception as e: logger.error(f"Error setting client mode: {e}") - raise HTTPException(status_code=500, detail=str(e)) + raise HTTPException(status_code=500, detail=str(e)) from e diff --git a/core/src/vf_core/web_admin/api/plugins.py b/core/src/vf_core/web_admin/api/plugins.py index 9c8b8c3..d83aca4 100644 --- a/core/src/vf_core/web_admin/api/plugins.py +++ b/core/src/vf_core/web_admin/api/plugins.py @@ -1,18 +1,17 @@ +import logging from enum import Enum -from fastapi import APIRouter, HTTPException, Depends, status -from pydantic import BaseModel -from vf_core.plugin_manager import PluginManager +from fastapi import APIRouter, Depends, HTTPException, status +from pydantic import BaseModel from vf_core.config_manager import ConfigManager -import logging - +from vf_core.plugin_manager import PluginManager from vf_core.plugin_types import ( + GROUP_CONTROLLERS, GROUP_PROCESSORS, GROUP_RENDERER, GROUP_SCHEMAS, GROUP_SCREENS, GROUP_SOURCES, - GROUP_CONTROLLERS ) from vf_core.web_admin.dependencies import get_config_manager, get_plugin_manager, verify_token @@ -54,7 +53,7 @@ async def get_plugin_schemas(pm: PluginManager = Depends(get_plugin_manager)): schema = schema_func() schemas[name] = schema.to_dict() - except Exception as e: + except Exception: logger.exception(f"Error loading schema for {entry_point.name}") return schemas @@ -177,7 +176,7 @@ async def disable_plugin( category = update.category.value plugin_list = cm.get(f"plugins.{category}") - + if plugin_list is None: return {"success": True} diff --git a/core/src/vf_core/web_admin/api/system.py b/core/src/vf_core/web_admin/api/system.py index d04ce70..e7b3696 100644 --- a/core/src/vf_core/web_admin/api/system.py +++ b/core/src/vf_core/web_admin/api/system.py @@ -1,8 +1,8 @@ -from fastapi import APIRouter, HTTPException, Depends, status -from pydantic import BaseModel -from typing import Any import logging +from typing import Any +from fastapi import APIRouter, Depends, HTTPException, status +from pydantic import BaseModel from vf_core.config_manager import ConfigManager from vf_core.plugin_manager import PluginManager from vf_core.web_admin.dependencies import get_config_manager, get_plugin_manager, verify_token @@ -81,4 +81,4 @@ async def update_config( cm.save() return {"success": True, "key": update.key, "value": update.value} except Exception as e: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) \ No newline at end of file + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) from e diff --git a/core/src/vf_core/web_admin/auth.py b/core/src/vf_core/web_admin/auth.py index b010a54..325827d 100644 --- a/core/src/vf_core/web_admin/auth.py +++ b/core/src/vf_core/web_admin/auth.py @@ -1,8 +1,7 @@ -import secrets -import logging import json +import logging +import secrets from pathlib import Path -from typing import Optional logger = logging.getLogger(__name__) @@ -23,29 +22,29 @@ def _get_auth_data_path() -> Path: def get_or_create_secret_key() -> str: """Get or generate JWT secret key.""" auth_data = _load_auth_data() - + if not auth_data.get("secret_key"): auth_data["secret_key"] = secrets.token_urlsafe(32) _save_auth_data(auth_data) logger.info("Generated new JWT secret key") - + return auth_data["secret_key"] -def get_admin_credentials() -> Optional[dict]: +def get_admin_credentials() -> dict | None: """ Get admin username and password hash. - + Returns: dict with 'username' and 'password_hash', or None if not configured """ auth_data = _load_auth_data() - + if "username" in auth_data and "password_hash" in auth_data: return { "username": auth_data["username"], "password_hash": auth_data["password_hash"] } - + return None def set_admin_credentials(username: str, password_hash: str) -> None: @@ -85,4 +84,4 @@ def _save_auth_data(data: dict) -> None: pass # Windows doesn't support chmod except Exception: logger.exception("Failed to save auth data") - raise \ No newline at end of file + raise diff --git a/core/src/vf_core/web_admin/dependencies.py b/core/src/vf_core/web_admin/dependencies.py index 0ba505e..9cd3d0c 100644 --- a/core/src/vf_core/web_admin/dependencies.py +++ b/core/src/vf_core/web_admin/dependencies.py @@ -1,7 +1,7 @@ +import jwt from fastapi import Depends, HTTPException, Request, status - from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer -import jwt + from vf_core.config_manager import ConfigManager from vf_core.plugin_manager import PluginManager @@ -57,8 +57,8 @@ async def verify_token( except jwt.ExpiredSignatureError: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Token expired" - ) + ) from None except jwt.InvalidTokenError: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token" - ) + ) from None diff --git a/core/src/vf_core/web_admin/main.py b/core/src/vf_core/web_admin/main.py index d962bb2..d3fe467 100644 --- a/core/src/vf_core/web_admin/main.py +++ b/core/src/vf_core/web_admin/main.py @@ -7,19 +7,20 @@ uvicorn web_admin.main:app --host 127.0.0.1 --port 8000 """ -from fastapi import FastAPI -from fastapi.staticfiles import StaticFiles -from fastapi.responses import FileResponse from pathlib import Path + import uvicorn +from fastapi import FastAPI +from fastapi.responses import FileResponse +from fastapi.staticfiles import StaticFiles + from vf_core.config_manager import ConfigManager -from vf_core.plugin_manager import PluginManager from vf_core.network_manager import NetworkManager +from vf_core.plugin_manager import PluginManager from vf_core.web_admin.api import auth, system from vf_core.web_admin.auth import get_or_create_secret_key -from .api import config, plugins, network - +from .api import config, network, plugins app = FastAPI(title="Vessel Frame Admin Panel") diff --git a/plugins/controllers/button_controller/src/button_controller/__init__.py b/plugins/controllers/button_controller/src/button_controller/__init__.py index 543afab..c3b75c5 100644 --- a/plugins/controllers/button_controller/src/button_controller/__init__.py +++ b/plugins/controllers/button_controller/src/button_controller/__init__.py @@ -1,13 +1,21 @@ -from gpiozero import Button import asyncio import logging from typing import Any -from vf_core.plugin_types import Plugin, ConfigSchema, ConfigField, ConfigFieldType, require_plugin_args + +from gpiozero import Button from vf_core.message_bus import MessageBus +from vf_core.plugin_types import ( + ConfigField, + ConfigFieldType, + ConfigSchema, + Plugin, + require_plugin_args, +) + class ButtonController: """Handles physical button presses.""" - + def __init__( self, *, @@ -69,24 +77,24 @@ def _schedule_publish(self, action: str): if self._loop is None: self._logger.error("Event loop not available, button press ignored") return - + asyncio.run_coroutine_threadsafe( self._bus.publish("screen.command", {"action": action}), self._loop ) - + def _on_button_a(self): """Button A: Go to previous screen.""" self._schedule_publish("previous") - + def _on_button_b(self): """Button B: Go to next screen.""" self._schedule_publish("next") - + def _on_button_c(self): """Button C: No action yet.""" self._logger.info("Button C pressed (no action configured)") - + def _on_button_d(self): """Button D: No action yet.""" self._logger.info("Button D pressed (no action configured)") @@ -136,4 +144,4 @@ def get_config_schema() -> ConfigSchema: ) def make_plugin(**kwargs: Any) -> Plugin: - return ButtonController(**kwargs) \ No newline at end of file + return ButtonController(**kwargs) diff --git a/plugins/message_processors/ais_decoder_processor/src/ais_decoder_processor/__init__.py b/plugins/message_processors/ais_decoder_processor/src/ais_decoder_processor/__init__.py index cd31ce8..15421fb 100644 --- a/plugins/message_processors/ais_decoder_processor/src/ais_decoder_processor/__init__.py +++ b/plugins/message_processors/ais_decoder_processor/src/ais_decoder_processor/__init__.py @@ -1,12 +1,15 @@ from __future__ import annotations + import asyncio -from typing import Any +import logging from contextlib import suppress -from vf_core.message_bus import MessageBus -from vf_core.plugin_types import Plugin, require_plugin_args +from typing import Any + from pyais.queue import NMEAQueue from pyais.stream import TagBlockQueue -import logging +from vf_core.message_bus import MessageBus +from vf_core.plugin_types import Plugin, require_plugin_args + from .ais_utils import get_vessel_full_type_name diff --git a/plugins/message_sources/com_message_source/src/com_message_source/__init__.py b/plugins/message_sources/com_message_source/src/com_message_source/__init__.py index f31de21..d319a39 100644 --- a/plugins/message_sources/com_message_source/src/com_message_source/__init__.py +++ b/plugins/message_sources/com_message_source/src/com_message_source/__init__.py @@ -1,12 +1,19 @@ from __future__ import annotations + import asyncio -import serial_asyncio import logging - -from typing import Any from contextlib import suppress +from typing import Any + +import serial_asyncio from vf_core.message_bus import MessageBus -from vf_core.plugin_types import Plugin, ConfigSchema, ConfigField, ConfigFieldType, require_plugin_args +from vf_core.plugin_types import ( + ConfigField, + ConfigFieldType, + ConfigSchema, + Plugin, + require_plugin_args, +) class COMMessageSource: @@ -87,7 +94,7 @@ async def _loop(self) -> None: except asyncio.CancelledError: raise - except asyncio.TimeoutError: + except TimeoutError: self._logger.warning( f"Timed out connecting to {self._port}, " f"retrying in {self.RECONNECT_DELAY}s" diff --git a/plugins/message_sources/daisy_message_source/src/daisy_message_source/__init__.py b/plugins/message_sources/daisy_message_source/src/daisy_message_source/__init__.py index 296c069..7091e2e 100644 --- a/plugins/message_sources/daisy_message_source/src/daisy_message_source/__init__.py +++ b/plugins/message_sources/daisy_message_source/src/daisy_message_source/__init__.py @@ -1,13 +1,20 @@ from __future__ import annotations + import asyncio -from smbus2 import SMBus -from concurrent.futures import ThreadPoolExecutor import logging - -from typing import Any +from concurrent.futures import ThreadPoolExecutor from contextlib import suppress +from typing import Any + +from smbus2 import SMBus from vf_core.message_bus import MessageBus -from vf_core.plugin_types import Plugin, ConfigSchema, ConfigField, ConfigFieldType, require_plugin_args +from vf_core.plugin_types import ( + ConfigField, + ConfigFieldType, + ConfigSchema, + Plugin, + require_plugin_args, +) class DaisyMessageSource: @@ -18,7 +25,7 @@ class DaisyMessageSource: MESSAGE_BUFF_ADDR = 0xFF MAX_BLOCK_SIZE = 32 RECONNECT_DELAY: float = 5.0 - + def __init__( self, *, @@ -48,7 +55,7 @@ def _parse_i2c_address(self, addr: str | int) -> int: """Parse I2C address from hex/decimal string or int.""" if isinstance(addr, int): return addr - + addr = addr.strip() if addr.startswith("0x") or addr.startswith("0X"): return int(addr, 16) # Parse as hex @@ -84,7 +91,7 @@ async def stop(self) -> None: if self._i2c is not None: self._i2c.close() - + self._executor.shutdown(wait=True) def _read_byte(self, addr: int) -> int: @@ -95,7 +102,7 @@ def _read_byte(self, addr: int) -> int: except Exception: self._logger.exception(f"Error reading byte from register 0x{addr:02X}") return 0 - + def _read_available_count(self) -> int: """Get number of bytes available to read.""" try: @@ -105,7 +112,7 @@ def _read_available_count(self) -> int: except Exception: self._logger.exception("Error reading available byte count") return 0 - + def _read_block(self, size: int) -> bytes: """Read a block of specified size from I2C device.""" try: @@ -123,7 +130,7 @@ def _read_block(self, size: int) -> bytes: except Exception: self._logger.exception("Error reading block from I2C") return b'' - + async def _loop(self) -> None: """Continuously read from I2C and publish complete messages.""" loop = asyncio.get_running_loop() @@ -159,7 +166,7 @@ async def _loop(self) -> None: data = await loop.run_in_executor( self._executor, - lambda: self._read_block(available) + lambda available=available: self._read_block(available) ) if not data: diff --git a/plugins/message_sources/mock_message_source/src/mock_message_source/__init__.py b/plugins/message_sources/mock_message_source/src/mock_message_source/__init__.py index 4903679..d5b9b44 100644 --- a/plugins/message_sources/mock_message_source/src/mock_message_source/__init__.py +++ b/plugins/message_sources/mock_message_source/src/mock_message_source/__init__.py @@ -1,8 +1,10 @@ from __future__ import annotations + import asyncio import random -from typing import Any from contextlib import suppress +from typing import Any + from vf_core.message_bus import MessageBus from vf_core.plugin_types import Plugin, require_plugin_args diff --git a/plugins/renderers/image_renderer/src/image_renderer/__init__.py b/plugins/renderers/image_renderer/src/image_renderer/__init__.py index 17336ab..4936f84 100644 --- a/plugins/renderers/image_renderer/src/image_renderer/__init__.py +++ b/plugins/renderers/image_renderer/src/image_renderer/__init__.py @@ -1,13 +1,15 @@ from __future__ import annotations + +from pathlib import Path from typing import Any + +from PIL import Image, ImageDraw from vf_core.plugin_types import ( ConfigField, ConfigFieldType, ConfigSchema, RendererPlugin, ) -from PIL import Image, ImageDraw -from pathlib import Path class ImageRenderer: @@ -80,7 +82,7 @@ def get_config_schema() -> ConfigSchema: Returns: ConfigSchema: Schema describing this plugin's configuration options. """ - + return ConfigSchema( plugin_name="image_renderer", plugin_type="renderer", diff --git a/plugins/renderers/inky_renderer/src/inky_renderer/__init__.py b/plugins/renderers/inky_renderer/src/inky_renderer/__init__.py index 4a064b2..04f847d 100644 --- a/plugins/renderers/inky_renderer/src/inky_renderer/__init__.py +++ b/plugins/renderers/inky_renderer/src/inky_renderer/__init__.py @@ -1,16 +1,18 @@ from __future__ import annotations -from typing import Any + import asyncio import logging from concurrent.futures import ThreadPoolExecutor +from typing import Any + +from inky.auto import auto +from PIL import Image, ImageDraw from vf_core.plugin_types import ( ConfigField, ConfigFieldType, ConfigSchema, RendererPlugin, ) -from PIL import Image, ImageDraw -from inky.auto import auto class InkyRenderer: @@ -58,7 +60,7 @@ def _flush_block(self, image: Image.Image) -> None: async def flush(self) -> None: """Push the current canvas to the Inky display.""" image = self._canvas - + # Inky expects landscape so rotate if we're in portrait if self._orientation == "portrait": image = image.rotate(90, expand=True) @@ -66,7 +68,7 @@ async def flush(self) -> None: # Run in thread as it's a blocking call loop = asyncio.get_running_loop() await loop.run_in_executor(self._executor, self._flush_block, image) - + def clear(self) -> None: """Clear the canvas by filling it with the background colour.""" @@ -104,7 +106,7 @@ def get_config_schema() -> ConfigSchema: Returns: ConfigSchema: Schema describing this plugin's configuration options. """ - + return ConfigSchema( plugin_name="inky_renderer", plugin_type="renderer", diff --git a/plugins/screens/map_screen/src/map_screen/__init__.py b/plugins/screens/map_screen/src/map_screen/__init__.py index a73df8a..09d861e 100644 --- a/plugins/screens/map_screen/src/map_screen/__init__.py +++ b/plugins/screens/map_screen/src/map_screen/__init__.py @@ -9,12 +9,11 @@ from __future__ import annotations import asyncio +import logging from contextlib import suppress from pathlib import Path from typing import Any -import logging - from vf_core.asset_manager import AssetManager from vf_core.message_bus import MessageBus from vf_core.plugin_types import ( diff --git a/plugins/screens/map_screen/src/map_screen/bounds.py b/plugins/screens/map_screen/src/map_screen/bounds.py index 182ea28..267b190 100644 --- a/plugins/screens/map_screen/src/map_screen/bounds.py +++ b/plugins/screens/map_screen/src/map_screen/bounds.py @@ -19,7 +19,7 @@ class Bounds: max_lon: float @classmethod - def parse(cls, bounds: dict | None, logger) -> "Bounds": + def parse(cls, bounds: dict | None, logger) -> Bounds: """Build from a config dict, warning on (and ignoring) incomplete input.""" keys = ("min_lat", "max_lat", "min_lon", "max_lon") if bounds and not all(k in bounds for k in keys): diff --git a/plugins/screens/map_screen/src/map_screen/layout.py b/plugins/screens/map_screen/src/map_screen/layout.py index d8a6619..deb639a 100644 --- a/plugins/screens/map_screen/src/map_screen/layout.py +++ b/plugins/screens/map_screen/src/map_screen/layout.py @@ -17,7 +17,6 @@ from typing import Any from PIL import Image, ImageDraw, ImageFont - from vf_core.text_utils import TextRenderingMixin from .bounds import Bounds diff --git a/plugins/screens/table_screen/src/table_screen/__init__.py b/plugins/screens/table_screen/src/table_screen/__init__.py index bf52d66..313514d 100644 --- a/plugins/screens/table_screen/src/table_screen/__init__.py +++ b/plugins/screens/table_screen/src/table_screen/__init__.py @@ -1,14 +1,22 @@ from __future__ import annotations + import asyncio -from typing import Any -from contextlib import suppress import logging +from contextlib import suppress +from typing import Any -from vf_core.message_bus import MessageBus -from vf_core.plugin_types import ConfigField, ConfigFieldType, ConfigSchema, ScreenPlugin, RendererPlugin, require_plugin_args -from vf_core.vessel_manager import VesselManager from vf_core.asset_manager import AssetManager +from vf_core.message_bus import MessageBus +from vf_core.plugin_types import ( + ConfigField, + ConfigFieldType, + ConfigSchema, + RendererPlugin, + ScreenPlugin, + require_plugin_args, +) from vf_core.render_strategies import PeriodicRenderStrategy +from vf_core.vessel_manager import VesselManager from .layouts import select_layout @@ -94,7 +102,7 @@ async def deactivate(self) -> None: async def _update_loop(self) -> None: """Internal loop that receives update events and requests renders.""" try: - async for msg in self._bus.subscribe(self._in_topic): + async for _ in self._bus.subscribe(self._in_topic): self._render_strategy.request_render() except asyncio.CancelledError: raise diff --git a/plugins/screens/table_screen/src/table_screen/layouts/__init__.py b/plugins/screens/table_screen/src/table_screen/layouts/__init__.py index f8783de..b088722 100644 --- a/plugins/screens/table_screen/src/table_screen/layouts/__init__.py +++ b/plugins/screens/table_screen/src/table_screen/layouts/__init__.py @@ -7,11 +7,11 @@ from __future__ import annotations from .base import TableLayout -from .portrait_standard import PortraitStandard -from .portrait_large import PortraitLarge from .landscape_base import LandscapeTableLayout -from .landscape_standard import LandscapeStandard from .landscape_large import LandscapeLarge +from .landscape_standard import LandscapeStandard +from .portrait_large import PortraitLarge +from .portrait_standard import PortraitStandard __all__ = [ "TableLayout", "LandscapeTableLayout", "select_layout", diff --git a/plugins/screens/table_screen/src/table_screen/layouts/base.py b/plugins/screens/table_screen/src/table_screen/layouts/base.py index e179f9a..20e12d8 100644 --- a/plugins/screens/table_screen/src/table_screen/layouts/base.py +++ b/plugins/screens/table_screen/src/table_screen/layouts/base.py @@ -11,7 +11,6 @@ from typing import Any from PIL import ImageDraw, ImageFont - from vf_core.marine_utils import nav_status_short from vf_core.text_utils import TextRenderingMixin diff --git a/plugins/screens/table_screen/src/table_screen/layouts/landscape_large.py b/plugins/screens/table_screen/src/table_screen/layouts/landscape_large.py index c61ecbd..b2b3c05 100644 --- a/plugins/screens/table_screen/src/table_screen/layouts/landscape_large.py +++ b/plugins/screens/table_screen/src/table_screen/layouts/landscape_large.py @@ -6,8 +6,8 @@ from __future__ import annotations import time -from PIL import ImageDraw +from PIL import ImageDraw from vf_core.marine_utils import compass from .landscape_base import LandscapeTableLayout diff --git a/plugins/screens/table_screen/src/table_screen/layouts/landscape_standard.py b/plugins/screens/table_screen/src/table_screen/layouts/landscape_standard.py index faf26a5..7331d13 100644 --- a/plugins/screens/table_screen/src/table_screen/layouts/landscape_standard.py +++ b/plugins/screens/table_screen/src/table_screen/layouts/landscape_standard.py @@ -5,6 +5,7 @@ from __future__ import annotations import time + from PIL import ImageDraw from .landscape_base import LandscapeTableLayout diff --git a/plugins/screens/table_screen/src/table_screen/layouts/portrait_large.py b/plugins/screens/table_screen/src/table_screen/layouts/portrait_large.py index 03dc7af..403db7c 100644 --- a/plugins/screens/table_screen/src/table_screen/layouts/portrait_large.py +++ b/plugins/screens/table_screen/src/table_screen/layouts/portrait_large.py @@ -6,9 +6,9 @@ from __future__ import annotations import time -from PIL import ImageDraw -from vf_core.marine_utils import mmsi_country, compass +from PIL import ImageDraw +from vf_core.marine_utils import compass, mmsi_country from .base import TableLayout @@ -53,7 +53,7 @@ async def render(self, vessels: list[dict], total: int) -> None: margin = px(44) x0, x1 = margin, W - margin cw = x1 - x0 - thick, thin = px(2), self._line_w + thick = px(2) cpad = px(8) # --- masthead --- diff --git a/plugins/screens/table_screen/src/table_screen/layouts/portrait_standard.py b/plugins/screens/table_screen/src/table_screen/layouts/portrait_standard.py index ed0f8be..3c75c50 100644 --- a/plugins/screens/table_screen/src/table_screen/layouts/portrait_standard.py +++ b/plugins/screens/table_screen/src/table_screen/layouts/portrait_standard.py @@ -6,6 +6,7 @@ from __future__ import annotations import time + from PIL import ImageDraw from .base import TableLayout diff --git a/plugins/screens/zone_screen/src/zone_screen/__init__.py b/plugins/screens/zone_screen/src/zone_screen/__init__.py index 8545a18..1500cc8 100644 --- a/plugins/screens/zone_screen/src/zone_screen/__init__.py +++ b/plugins/screens/zone_screen/src/zone_screen/__init__.py @@ -1,14 +1,22 @@ from __future__ import annotations + import asyncio -from typing import Any -from contextlib import suppress import logging +from contextlib import suppress +from typing import Any -from vf_core.message_bus import MessageBus -from vf_core.plugin_types import ConfigField, ConfigFieldType, ConfigSchema, ScreenPlugin, RendererPlugin, require_plugin_args -from vf_core.vessel_manager import VesselManager from vf_core.asset_manager import AssetManager +from vf_core.message_bus import MessageBus +from vf_core.plugin_types import ( + ConfigField, + ConfigFieldType, + ConfigSchema, + RendererPlugin, + ScreenPlugin, + require_plugin_args, +) from vf_core.render_strategies import PeriodicRenderStrategy +from vf_core.vessel_manager import VesselManager from .layouts import select_layout diff --git a/plugins/screens/zone_screen/src/zone_screen/layouts/__init__.py b/plugins/screens/zone_screen/src/zone_screen/layouts/__init__.py index ebdba25..20b3363 100644 --- a/plugins/screens/zone_screen/src/zone_screen/layouts/__init__.py +++ b/plugins/screens/zone_screen/src/zone_screen/layouts/__init__.py @@ -7,12 +7,12 @@ from __future__ import annotations from .base import ZoneLayout -from .portrait_standard import PortraitStandard -from .portrait_large import PortraitLarge from .landscape_base import LandscapeLayout from .landscape_compact import LandscapeCompact -from .landscape_standard import LandscapeStandard from .landscape_large import LandscapeLarge +from .landscape_standard import LandscapeStandard +from .portrait_large import PortraitLarge +from .portrait_standard import PortraitStandard __all__ = [ "ZoneLayout", "LandscapeLayout", "select_layout", diff --git a/plugins/screens/zone_screen/src/zone_screen/layouts/landscape_compact.py b/plugins/screens/zone_screen/src/zone_screen/layouts/landscape_compact.py index 73acde8..3c5b0a6 100644 --- a/plugins/screens/zone_screen/src/zone_screen/layouts/landscape_compact.py +++ b/plugins/screens/zone_screen/src/zone_screen/layouts/landscape_compact.py @@ -6,9 +6,9 @@ from __future__ import annotations import datetime -from PIL import ImageDraw -from vf_core.text_utils import split_two, FONT_FLOOR +from PIL import ImageDraw +from vf_core.text_utils import FONT_FLOOR, split_two from .landscape_base import LandscapeLayout diff --git a/plugins/screens/zone_screen/src/zone_screen/layouts/landscape_large.py b/plugins/screens/zone_screen/src/zone_screen/layouts/landscape_large.py index afab817..c89b186 100644 --- a/plugins/screens/zone_screen/src/zone_screen/layouts/landscape_large.py +++ b/plugins/screens/zone_screen/src/zone_screen/layouts/landscape_large.py @@ -8,9 +8,9 @@ import datetime import math -from PIL import ImageDraw -from vf_core.marine_utils import compass, fmt_lat, fmt_lon, range_bearing, nav_status_label +from PIL import ImageDraw +from vf_core.marine_utils import compass, fmt_lat, fmt_lon, nav_status_label, range_bearing from vf_core.text_utils import FONT_FLOOR from .landscape_base import LandscapeLayout diff --git a/plugins/screens/zone_screen/src/zone_screen/layouts/landscape_standard.py b/plugins/screens/zone_screen/src/zone_screen/layouts/landscape_standard.py index f20ad09..df96b23 100644 --- a/plugins/screens/zone_screen/src/zone_screen/layouts/landscape_standard.py +++ b/plugins/screens/zone_screen/src/zone_screen/layouts/landscape_standard.py @@ -7,8 +7,8 @@ from __future__ import annotations import datetime -from PIL import ImageDraw +from PIL import ImageDraw from vf_core.marine_utils import compass, fmt_lat, fmt_lon, nav_status_label from vf_core.text_utils import FONT_FLOOR diff --git a/plugins/screens/zone_screen/src/zone_screen/layouts/portrait_large.py b/plugins/screens/zone_screen/src/zone_screen/layouts/portrait_large.py index dcb72d7..fd6df47 100644 --- a/plugins/screens/zone_screen/src/zone_screen/layouts/portrait_large.py +++ b/plugins/screens/zone_screen/src/zone_screen/layouts/portrait_large.py @@ -6,13 +6,18 @@ from __future__ import annotations import datetime -from PIL import ImageDraw +from PIL import ImageDraw from vf_core.marine_utils import ( - mmsi_country, compass, compass_full, nav_status_label, - fmt_lat, fmt_lon, range_bearing, + compass, + compass_full, + fmt_lat, + fmt_lon, + mmsi_country, + nav_status_label, + range_bearing, ) -from vf_core.text_utils import split_two, FONT_FLOOR +from vf_core.text_utils import FONT_FLOOR, split_two from .base import ZoneLayout diff --git a/plugins/screens/zone_screen/src/zone_screen/layouts/portrait_standard.py b/plugins/screens/zone_screen/src/zone_screen/layouts/portrait_standard.py index 2ced559..576e224 100644 --- a/plugins/screens/zone_screen/src/zone_screen/layouts/portrait_standard.py +++ b/plugins/screens/zone_screen/src/zone_screen/layouts/portrait_standard.py @@ -12,9 +12,8 @@ from typing import Any from PIL import ImageDraw, ImageFont - -from vf_core.marine_utils import mmsi_country, compass, nav_status_label, fmt_lat, fmt_lon -from vf_core.text_utils import split_two, FONT_FLOOR +from vf_core.marine_utils import compass, fmt_lat, fmt_lon, mmsi_country, nav_status_label +from vf_core.text_utils import FONT_FLOOR, split_two from .base import ZoneLayout diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..94d0a9f --- /dev/null +++ b/ruff.toml @@ -0,0 +1,34 @@ +# Ruff configuration for the monorepo. +# Applies to core and all plugins. + +target-version = "py311" +line-length = 100 + +extend-exclude = [ + "**/build", + "**/dist", + "**/*.egg-info", + "node_modules", + ".venv", +] + +[lint] +# pycodestyle (E/W), pyflakes (F), isort (I), pyupgrade (UP), flake8-bugbear (B). +select = ["E", "F", "W", "I", "UP", "B"] +ignore = [ + "E501", # line length not enforced (left to a formatter if adopted) + "UP042", # keep "class X(str, Enum)" rather than StrEnum +] + +[lint.flake8-bugbear] +# FastAPI uses these as default argument values +extend-immutable-calls = [ + "fastapi.Depends", + "fastapi.Query", + "fastapi.Path", + "fastapi.Header", + "fastapi.Body", + "fastapi.Form", + "fastapi.File", + "fastapi.Cookie", +] diff --git a/scripts/network_mode_service.py b/scripts/network_mode_service.py index 286e5e9..1166eac 100644 --- a/scripts/network_mode_service.py +++ b/scripts/network_mode_service.py @@ -7,13 +7,12 @@ network according to the user's preferences. """ -import time import json -import subprocess import logging +import subprocess import sys +import time from pathlib import Path -from typing import Dict logging.basicConfig( level=logging.INFO, @@ -33,35 +32,35 @@ INTERFACE = "wlan0" -def load_config() -> Dict: +def load_config() -> dict: """Load network configuration from file""" try: if CONFIG_FILE.exists(): - with open(CONFIG_FILE, 'r') as f: + with open(CONFIG_FILE) as f: return json.load(f) except Exception as e: logger.error(f"Error loading config: {e}") - + # Default to client mode return {"mode": "client"} -def configure_ap_mode(config: Dict) -> bool: +def configure_ap_mode(config: dict) -> bool: """Configure the device as an access point""" logger.info(f"Configuring AP mode: {config.get('ap_ssid', 'vessel-frame')}") - + try: # Stop NetworkManager subprocess.run(['systemctl', 'stop', 'NetworkManager'], check=False) subprocess.run(['systemctl', 'stop', 'wpa_supplicant'], check=False) - + # Configure hostapd ap_ssid = config.get('ap_ssid', 'vessel-frame') ap_password = config.get('ap_password', 'spook_workshop') ap_channel = config.get('ap_channel', 6) - + hostapd_config = f"""interface={INTERFACE} driver=nl80211 ssid={ap_ssid} @@ -77,10 +76,10 @@ def configure_ap_mode(config: Dict) -> bool: wpa_pairwise=TKIP rsn_pairwise=CCMP """ - + with open(HOSTAPD_CONF, 'w') as f: f.write(hostapd_config) - + # Configure dnsmasq ap_ip = config.get('ap_ip', '10.0.0.1') dnsmasq_config = f"""interface={INTERFACE} @@ -89,52 +88,52 @@ def configure_ap_mode(config: Dict) -> bool: address=/vessel-frame.local/{ap_ip} address=/vessel-frame/{ap_ip} """ - + with open(DNSMASQ_CONF, 'w') as f: f.write(dnsmasq_config) - + # Configure static IP subprocess.run(['ip', 'addr', 'flush', 'dev', INTERFACE], check=False) subprocess.run(['ip', 'addr', 'add', f'{ap_ip}/24', 'dev', INTERFACE], check=True) subprocess.run(['ip', 'link', 'set', INTERFACE, 'up'], check=True) - + # Start AP services subprocess.run(['systemctl', 'unmask', 'hostapd'], check=False) subprocess.run(['systemctl', 'restart', 'dnsmasq'], check=True) subprocess.run(['systemctl', 'restart', 'hostapd'], check=True) - + logger.info("AP mode configured successfully") return True - + except Exception as e: logger.error(f"Error configuring AP mode: {e}", exc_info=True) return False -def configure_client_mode(config: Dict) -> bool: +def configure_client_mode(config: dict) -> bool: """Configure the device as a wifi client""" client_ssid = config.get('client_ssid') client_password = config.get('client_password', '') auto_fallback = config.get('auto_fallback', True) - + logger.info(f"Configuring client mode: {client_ssid}") - + if not client_ssid: logger.error("No client SSID configured") return False - + try: # Stop AP services and start NetworkManager subprocess.run(['systemctl', 'stop', 'hostapd'], check=False) subprocess.run(['systemctl', 'stop', 'dnsmasq'], check=False) - + # Remove static IP subprocess.run(['ip', 'addr', 'flush', 'dev', INTERFACE], check=False) - + # Start NetworkManager subprocess.run(['systemctl', 'start', 'NetworkManager'], check=True) - + # Create wpa_supplicant configuration for NetworkManager wpa_config = f"""ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev update_config=1 @@ -146,49 +145,49 @@ def configure_client_mode(config: Dict) -> bool: key_mgmt=WPA-PSK }} """ - + with open(WPA_SUPPLICANT_CONF, 'w') as f: f.write(wpa_config) - + # Set permissions subprocess.run(['chmod', '600', WPA_SUPPLICANT_CONF], check=True) - + # Give NetworkManager time to connect timeout = config.get('fallback_timeout', 60) - + logger.info(f"Waiting up to {timeout}s for NetworkManager to connect...") start_time = time.time() while time.time() - start_time < timeout: time.sleep(1) - + # Check if connected via NetworkManager result = subprocess.run( ['nmcli', '-t', '-f', 'GENERAL.STATE', 'device', 'show', INTERFACE], capture_output=True, text=True ) - + if 'connected' in result.stdout.lower(): logger.info("Connected to network successfully") return True - + logger.warning(f"Failed to connect within {timeout}s") - + # Fall back to AP mode if configured if auto_fallback: logger.info("Fallback enabled, switching to AP mode") return configure_ap_mode(config) - + return False - + except Exception as e: logger.error(f"Error configuring client mode: {e}", exc_info=True) - + # Try fallback if enabled if auto_fallback: logger.info("Client network mode error, switching to AP mode") return configure_ap_mode(config) - + return False def configure_offline_mode() -> bool: @@ -228,7 +227,7 @@ def main(): else: logger.error(f"Unknown mode: {mode}") success = False - + if success: logger.info("Network config applied successfully") return 0 @@ -238,4 +237,4 @@ def main(): if __name__ == "__main__": - sys.exit(main()) \ No newline at end of file + sys.exit(main()) diff --git a/scripts/render_map.py b/scripts/render_map.py index e36b29c..7de7a72 100644 --- a/scripts/render_map.py +++ b/scripts/render_map.py @@ -1,15 +1,15 @@ """Render the MapScreen plugin at all 6 resolutions for verification.""" from __future__ import annotations + import asyncio +import shutil import sys import time -import shutil from pathlib import Path -from PIL import Image, ImageDraw - -import vf_core import map_screen +import vf_core +from PIL import Image, ImageDraw from vf_core.asset_manager import AssetManager REPO_ROOT = Path(__file__).resolve().parents[1] diff --git a/scripts/render_table.py b/scripts/render_table.py index 8c9ea53..d76bc87 100644 --- a/scripts/render_table.py +++ b/scripts/render_table.py @@ -1,14 +1,14 @@ """Render the TableScreen plugin at the three main resolutions.""" from __future__ import annotations + import asyncio import sys import time from pathlib import Path -from PIL import Image, ImageDraw - -import vf_core import table_screen +import vf_core +from PIL import Image, ImageDraw from vf_core.asset_manager import AssetManager OUT = Path(__file__).resolve().parents[1] / "data" / "mockups" diff --git a/scripts/render_zone.py b/scripts/render_zone.py index f317abf..70f6923 100644 --- a/scripts/render_zone.py +++ b/scripts/render_zone.py @@ -14,15 +14,15 @@ python scripts/render_zone.py after # -> data/mockups/after/ """ from __future__ import annotations + import asyncio import re import sys from pathlib import Path -from PIL import Image, ImageDraw - import vf_core import zone_screen +from PIL import Image, ImageDraw from vf_core.asset_manager import AssetManager RES_FILE = Path(__file__).resolve().parent / "resolutions.txt" @@ -143,11 +143,14 @@ def main(): n_ok = n_tight = n_err = 0 for cw, ch, orient, profile, scale, fits, error, devices in rows: if error: - status = "ERROR"; n_err += 1 + status = "ERROR" + n_err += 1 elif not fits: - status = "OVERFLOW"; n_tight += 1 + status = "OVERFLOW" + n_tight += 1 else: - status = "ok"; n_ok += 1 + status = "ok" + n_ok += 1 note = error if error else devices print(f"{cw:>4}x{ch:<5} {orient} {profile:8} {scale:>5} {status:8} {note}") print("-" * 92) From 7a748d481f66f19966086f5d9814ab90072c426f Mon Sep 17 00:00:00 2001 From: James Date: Mon, 15 Jun 2026 23:02:12 +0100 Subject: [PATCH 2/3] Add ci github action to validate ruff checks --- .github/workflows/ci.yml | 42 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..34ba66a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,42 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + lint: + name: Ruff + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + - name: Install ruff + run: pip install ruff==0.15.17 + - name: Lint + run: ruff check . + + build: + name: Build & import (py${{ matrix.python-version }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.11", "3.13"] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install core and pure-Python plugins + run: | + pip install ./core + pip install \ + ./plugins/message_sources/mock_message_source \ + ./plugins/message_processors/ais_decoder_processor \ + ./plugins/renderers/image_renderer + - name: Import smoke test + run: python -c "import vf_core.main, mock_message_source, ais_decoder_processor, image_renderer" From 90ad820d8e9ca4716a480cf06fa20824ff90eaee Mon Sep 17 00:00:00 2001 From: James Date: Mon, 15 Jun 2026 23:07:39 +0100 Subject: [PATCH 3/3] Bump checkout and python setup options to increase node ver Node 20 is deprected on gh actions --- .github/workflows/ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 34ba66a..798baf3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,8 +10,8 @@ jobs: name: Ruff runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v5 + - uses: actions/setup-python@v6 with: python-version: "3.11" - name: Install ruff @@ -27,8 +27,8 @@ jobs: matrix: python-version: ["3.11", "3.13"] steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v5 + - uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Install core and pure-Python plugins