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 <josh@zindello.com.au>
This commit is contained in:
Zindello
2026-05-27 11:57:45 +10:00
parent 9fe0142fa5
commit a1c66100f5
2 changed files with 49 additions and 13 deletions
+6 -13
View File
@@ -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",
+43
View File
@@ -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)
)