fix: Make psycopg2 optional for Flask/Werkzeug compatibility

- Bump Flask requirement to >=3.0.0 (required for Werkzeug 3.x)
- Make psycopg2 import conditional in routes/adsb.py and utils/adsb_history.py
- ADS-B history features gracefully disabled when PostgreSQL libs unavailable

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Smittix
2026-01-29 22:19:14 +00:00
parent 7b847e0541
commit 872cc806eb
4 changed files with 493 additions and 476 deletions
+1 -1
View File
@@ -26,7 +26,7 @@ classifiers = [
"Topic :: System :: Networking :: Monitoring", "Topic :: System :: Networking :: Monitoring",
] ]
dependencies = [ dependencies = [
"flask>=2.0.0", "flask>=3.0.0",
"skyfield>=1.45", "skyfield>=1.45",
"pyserial>=3.5", "pyserial>=3.5",
"Werkzeug>=3.1.5", "Werkzeug>=3.1.5",
+1 -1
View File
@@ -1,5 +1,5 @@
# Core dependencies # Core dependencies
flask>=2.0.0 flask>=3.0.0
flask-limiter>=2.5.4 flask-limiter>=2.5.4
requests>=2.28.0 requests>=2.28.0
Werkzeug>=3.1.5 Werkzeug>=3.1.5
+19 -10
View File
@@ -15,8 +15,16 @@ from typing import Any, Generator
from flask import Blueprint, jsonify, request, Response, render_template from flask import Blueprint, jsonify, request, Response, render_template
from flask import make_response from flask import make_response
import psycopg2
from psycopg2.extras import RealDictCursor # psycopg2 is optional - only needed for PostgreSQL history persistence
try:
import psycopg2
from psycopg2.extras import RealDictCursor
PSYCOPG2_AVAILABLE = True
except ImportError:
psycopg2 = None # type: ignore
RealDictCursor = None # type: ignore
PSYCOPG2_AVAILABLE = False
import app as app_module import app as app_module
from config import ( from config import (
@@ -192,7 +200,7 @@ def _parse_int_param(value: str | None, default: int, min_value: int | None = No
def _get_active_session() -> dict[str, Any] | None: def _get_active_session() -> dict[str, Any] | None:
if not ADSB_HISTORY_ENABLED: if not ADSB_HISTORY_ENABLED or not PSYCOPG2_AVAILABLE:
return None return None
_ensure_history_schema() _ensure_history_schema()
try: try:
@@ -222,7 +230,7 @@ def _record_session_start(
start_source: str | None, start_source: str | None,
started_by: str | None, started_by: str | None,
) -> dict[str, Any] | None: ) -> dict[str, Any] | None:
if not ADSB_HISTORY_ENABLED: if not ADSB_HISTORY_ENABLED or not PSYCOPG2_AVAILABLE:
return None return None
_ensure_history_schema() _ensure_history_schema()
try: try:
@@ -257,7 +265,7 @@ def _record_session_start(
def _record_session_stop(*, stop_source: str | None, stopped_by: str | None) -> dict[str, Any] | None: def _record_session_stop(*, stop_source: str | None, stopped_by: str | None) -> dict[str, Any] | None:
if not ADSB_HISTORY_ENABLED: if not ADSB_HISTORY_ENABLED or not PSYCOPG2_AVAILABLE:
return None return None
_ensure_history_schema() _ensure_history_schema()
try: try:
@@ -810,7 +818,8 @@ def adsb_dashboard():
@adsb_bp.route('/history') @adsb_bp.route('/history')
def adsb_history(): def adsb_history():
"""ADS-B history reporting dashboard.""" """ADS-B history reporting dashboard."""
resp = make_response(render_template('adsb_history.html', history_enabled=ADSB_HISTORY_ENABLED)) history_available = ADSB_HISTORY_ENABLED and PSYCOPG2_AVAILABLE
resp = make_response(render_template('adsb_history.html', history_enabled=history_available))
resp.headers['Cache-Control'] = 'no-store' resp.headers['Cache-Control'] = 'no-store'
return resp return resp
@@ -818,7 +827,7 @@ def adsb_history():
@adsb_bp.route('/history/summary') @adsb_bp.route('/history/summary')
def adsb_history_summary(): def adsb_history_summary():
"""Summary stats for ADS-B history window.""" """Summary stats for ADS-B history window."""
if not ADSB_HISTORY_ENABLED: if not ADSB_HISTORY_ENABLED or not PSYCOPG2_AVAILABLE:
return jsonify({'error': 'ADS-B history is disabled'}), 503 return jsonify({'error': 'ADS-B history is disabled'}), 503
_ensure_history_schema() _ensure_history_schema()
@@ -848,7 +857,7 @@ def adsb_history_summary():
@adsb_bp.route('/history/aircraft') @adsb_bp.route('/history/aircraft')
def adsb_history_aircraft(): def adsb_history_aircraft():
"""List latest aircraft snapshots for a time window.""" """List latest aircraft snapshots for a time window."""
if not ADSB_HISTORY_ENABLED: if not ADSB_HISTORY_ENABLED or not PSYCOPG2_AVAILABLE:
return jsonify({'error': 'ADS-B history is disabled'}), 503 return jsonify({'error': 'ADS-B history is disabled'}), 503
_ensure_history_schema() _ensure_history_schema()
@@ -898,7 +907,7 @@ def adsb_history_aircraft():
@adsb_bp.route('/history/timeline') @adsb_bp.route('/history/timeline')
def adsb_history_timeline(): def adsb_history_timeline():
"""Timeline snapshots for a specific aircraft.""" """Timeline snapshots for a specific aircraft."""
if not ADSB_HISTORY_ENABLED: if not ADSB_HISTORY_ENABLED or not PSYCOPG2_AVAILABLE:
return jsonify({'error': 'ADS-B history is disabled'}), 503 return jsonify({'error': 'ADS-B history is disabled'}), 503
_ensure_history_schema() _ensure_history_schema()
@@ -933,7 +942,7 @@ def adsb_history_timeline():
@adsb_bp.route('/history/messages') @adsb_bp.route('/history/messages')
def adsb_history_messages(): def adsb_history_messages():
"""Raw message history for a specific aircraft.""" """Raw message history for a specific aircraft."""
if not ADSB_HISTORY_ENABLED: if not ADSB_HISTORY_ENABLED or not PSYCOPG2_AVAILABLE:
return jsonify({'error': 'ADS-B history is disabled'}), 503 return jsonify({'error': 'ADS-B history is disabled'}), 503
_ensure_history_schema() _ensure_history_schema()
+12 -4
View File
@@ -9,8 +9,16 @@ import time
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Iterable from typing import Iterable
import psycopg2 # psycopg2 is optional - only needed for PostgreSQL history persistence
from psycopg2.extras import execute_values, Json try:
import psycopg2
from psycopg2.extras import execute_values, Json
PSYCOPG2_AVAILABLE = True
except ImportError:
psycopg2 = None # type: ignore
execute_values = None # type: ignore
Json = None # type: ignore
PSYCOPG2_AVAILABLE = False
from config import ( from config import (
ADSB_DB_HOST, ADSB_DB_HOST,
@@ -199,7 +207,7 @@ class AdsbHistoryWriter:
"""Background writer for ADS-B history records.""" """Background writer for ADS-B history records."""
def __init__(self) -> None: def __init__(self) -> None:
self.enabled = ADSB_HISTORY_ENABLED self.enabled = ADSB_HISTORY_ENABLED and PSYCOPG2_AVAILABLE
self._queue: queue.Queue[dict] = queue.Queue(maxsize=ADSB_HISTORY_QUEUE_SIZE) self._queue: queue.Queue[dict] = queue.Queue(maxsize=ADSB_HISTORY_QUEUE_SIZE)
self._thread: threading.Thread | None = None self._thread: threading.Thread | None = None
self._stop_event = threading.Event() self._stop_event = threading.Event()
@@ -297,7 +305,7 @@ class AdsbSnapshotWriter:
"""Background writer for ADS-B snapshot records.""" """Background writer for ADS-B snapshot records."""
def __init__(self) -> None: def __init__(self) -> None:
self.enabled = ADSB_HISTORY_ENABLED self.enabled = ADSB_HISTORY_ENABLED and PSYCOPG2_AVAILABLE
self._queue: queue.Queue[dict] = queue.Queue(maxsize=ADSB_HISTORY_QUEUE_SIZE) self._queue: queue.Queue[dict] = queue.Queue(maxsize=ADSB_HISTORY_QUEUE_SIZE)
self._thread: threading.Thread | None = None self._thread: threading.Thread | None = None
self._stop_event = threading.Event() self._stop_event = threading.Event()