From a1c66100f51b446fa1cfcd4f5e90550532f06b0b Mon Sep 17 00:00:00 2001 From: Zindello Date: Wed, 27 May 2026 11:57:45 +1000 Subject: [PATCH] fix: remove datetime.UTC from mqtt_handler and add 3.10 compat test Replace the try/except UTC shim in mqtt_handler.py with timezone.utc directly, consistent with the api_endpoints.py fix. Add a static AST scan that fails if datetime.UTC is reintroduced anywhere in the codebase, guarding against future regressions on Python 3.10 (LuckFox Pico Ultra). Co-Authored-By: Zindello --- repeater/data_acquisition/mqtt_handler.py | 19 ++++------ tests/test_python310_compat.py | 43 +++++++++++++++++++++++ 2 files changed, 49 insertions(+), 13 deletions(-) create mode 100644 tests/test_python310_compat.py diff --git a/repeater/data_acquisition/mqtt_handler.py b/repeater/data_acquisition/mqtt_handler.py index 8164611..0d94484 100644 --- a/repeater/data_acquisition/mqtt_handler.py +++ b/repeater/data_acquisition/mqtt_handler.py @@ -4,19 +4,12 @@ import json import logging import string import threading -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from typing import Any, Callable, Dict, List, Optional import paho.mqtt.client as mqtt from nacl.signing import SigningKey -# Try to import datetime.UTC (Python 3.11+) otherwise fallback to timezone.utc -try: - from datetime import UTC -except Exception: - from datetime import timezone - UTC = timezone.utc - from repeater import __version__, config from repeater.presets import get_preset @@ -268,7 +261,7 @@ class _BrokerConnection: def _generate_jwt(self) -> str: """Generate MeshCore-style Ed25519 JWT token""" - now = datetime.now(UTC) + now = datetime.now(timezone.utc) header = {"alg": "Ed25519", "typ": "JWT"} @@ -448,7 +441,7 @@ class _BrokerConnection: else: logger.info(f"No credentials set for {self.broker['name']} (JWT auth disabled and no username/password provided)") - self._connect_time = datetime.now(UTC) + self._connect_time = datetime.now(timezone.utc) except Exception as e: logger.error(f"Failed to set JWT credentials for {self.broker['name']}: {e}") @@ -550,7 +543,7 @@ class _BrokerConnection: """Check if connection should be reconnected due to JWT expiry (at 80% of lifetime)""" if not self._connect_time: return False - elapsed = (datetime.now(UTC) - self._connect_time).total_seconds() + elapsed = (datetime.now(timezone.utc) - self._connect_time).total_seconds() expiry_seconds = self.jwt_expiry_minutes * 60 # Stagger refresh by 5% per broker to prevent simultaneous disconnects # Broker 0: 80%, Broker 1: 85%, Broker 2: 90%, etc. @@ -906,7 +899,7 @@ class MeshCoreToMqttPusher: # Packet helpers # ---------------------------------------------------------------- def _process_packet(self, pkt: dict) -> dict: - return {"timestamp": datetime.now(UTC).isoformat(), "origin_id": self.public_key, **pkt} + return {"timestamp": datetime.now(timezone.utc).isoformat(), "origin_id": self.public_key, **pkt} def publish_packet(self, pkt: dict, subtopic="packets", retain=False): return self.publish(subtopic, self._process_packet(pkt), retain) @@ -941,7 +934,7 @@ class MeshCoreToMqttPusher: status = { "status": state, - "timestamp": datetime.now(UTC).isoformat(), + "timestamp": datetime.now(timezone.utc).isoformat(), "origin": origin or self.node_name, "origin_id": self.public_key, "model": "PyMC-Repeater", diff --git a/tests/test_python310_compat.py b/tests/test_python310_compat.py new file mode 100644 index 0000000..6d90e17 --- /dev/null +++ b/tests/test_python310_compat.py @@ -0,0 +1,43 @@ +"""Regression tests guarding against Python 3.10 compatibility breakage. + +pyMC Repeater supports Python 3.10+ (LuckFox Pico Ultra ships with 3.10). +These tests scan the source tree statically so regressions are caught in CI +without needing a 3.10 interpreter in the test environment. +""" + +import ast +import sys +from pathlib import Path + +_REPEATER_ROOT = Path(__file__).parent.parent / "repeater" + + +def _py_files(): + return [p for p in _REPEATER_ROOT.rglob("*.py") if ".pyc" not in str(p)] + + +def test_minimum_python_version(): + """Fail fast if the test environment itself is below the minimum supported version.""" + assert sys.version_info >= (3, 10), ( + f"Python 3.10+ required, running {sys.version_info.major}.{sys.version_info.minor}" + ) + + +def test_no_datetime_utc(): + """`datetime.UTC` was added in 3.11 — `timezone.utc` must be used instead.""" + violations = [] + for path in _py_files(): + try: + tree = ast.parse(path.read_text(encoding="utf-8")) + except SyntaxError: + continue + for node in ast.walk(tree): + if isinstance(node, ast.ImportFrom) and node.module == "datetime": + if any(alias.name == "UTC" for alias in node.names): + rel = path.relative_to(_REPEATER_ROOT.parent) + violations.append(f" {rel}:{node.lineno}") + + assert not violations, ( + "datetime.UTC (Python 3.11+) found in the following files — " + "use timezone.utc instead:\n" + "\n".join(violations) + )