Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ FROM python:3.12-slim

WORKDIR /app

# Install dependencies (no build tools needed for current deps)
# Install system deps for ODBC/SQL Server (unixODBC, msodbcsql18) and Python requirements
COPY requirements.txt .
RUN apt-get update && apt-get install -y curl gnupg unixodbc unixodbc-dev && \
curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add - && \
curl https://packages.microsoft.com/config/debian/12/prod.list > /etc/apt/sources.list.d/mssql-release.list && \
apt-get update && ACCEPT_EULA=Y apt-get install -y msodbcsql18 && \
RUN apt-get update && apt-get install -y --no-install-recommends curl gnupg ca-certificates unixodbc unixodbc-dev && \
mkdir -p /etc/apt/keyrings && \
curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor -o /etc/apt/keyrings/microsoft.gpg && \
chmod 644 /etc/apt/keyrings/microsoft.gpg && \
curl https://packages.microsoft.com/config/debian/12/prod.list | sed 's|^deb |deb [signed-by=/etc/apt/keyrings/microsoft.gpg] |' > /etc/apt/sources.list.d/mssql-release.list && \
apt-get update && ACCEPT_EULA=Y apt-get install -y --no-install-recommends msodbcsql18 && \
rm -rf /var/lib/apt/lists/* && \
pip install --no-cache-dir -r requirements.txt

# Application
Expand Down
49 changes: 41 additions & 8 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

import json
import os
import pyodbc
from collections import Counter
from datetime import datetime, timedelta
from pathlib import Path
Expand All @@ -22,6 +21,10 @@
# _blob_container_client: container client if connected, False if init failed, None if not tried
_blob_container_client: Any = None

# Optional Azure SQL connection cache.
# None = not yet attempted, False = permanently disabled (misconfiguration or import error), object = live connection
_sql_connection: Any = None


def _get_blob_container():
"""Lazy-init Azure Blob container client from env. Returns None if not configured or init failed."""
Expand Down Expand Up @@ -320,25 +323,49 @@ def convert_numpy_types(obj: Any) -> Any:
return obj

def _get_sql_connection():
"""Create Azure SQL connection. Returns None if not configured."""
"""Return a cached Azure SQL connection. Returns None if not configured or permanently disabled."""
global _sql_connection
if _sql_connection is False:
return None # permanently disabled after earlier failure
if _sql_connection is not None:
try:
# Lightweight connectivity check at the ODBC driver level (no query sent to server).
_sql_connection.getinfo(2) # SQL_DATA_SOURCE_NAME
return _sql_connection
except Exception:
_sql_connection = None # stale, attempt to reconnect below

if not SQL_CONNECTION_STRING:
return None

try:
import pyodbc # noqa: PLC0415 – intentionally deferred for optional dependency
except ImportError:
print("pyodbc is not installed; Azure SQL saving disabled.")
_sql_connection = False
return None

try:
return pyodbc.connect(SQL_CONNECTION_STRING, timeout=10)
conn = pyodbc.connect(SQL_CONNECTION_STRING, timeout=10)
_sql_connection = conn
return _sql_connection
except Exception as e:
print(f"Azure SQL connection failed: {e}")
print(f"Azure SQL connection failed ({type(e).__name__}); SQL saving disabled.")
_sql_connection = False
return None


def save_occupancy_data_sql(occupancy_result: dict, timestamp_iso: Optional[str] = None) -> None:
"""Save occupancy estimation to Azure SQL."""
global _sql_connection
if not SAVE_TO_SQL:
return

conn = _get_sql_connection()
if conn is None:
return

cursor = None
try:
sid = occupancy_result.get("sensor_id") or "unknown"
ts = timestamp_iso or datetime.now().isoformat()
Expand Down Expand Up @@ -376,14 +403,20 @@ def save_occupancy_data_sql(occupancy_result: dict, timestamp_iso: Optional[str]
1 if entry["any_fever"] else 0,
)
conn.commit()
cursor.close()
conn.close()
except Exception as e:
print(f"Error saving occupancy data to Azure SQL: {e}")
print(f"Error saving occupancy data to Azure SQL ({type(e).__name__}); will retry on next call.")
try:
conn.close()
conn.rollback()
except Exception:
pass
# Invalidate the cached connection so the next call will reconnect
_sql_connection = None
finally:
if cursor is not None:
try:
cursor.close()
except Exception:
pass


def save_thermal_data(
Expand Down