| """Compat layer for DatabaseManager to provide methods expected by legacy app code. |
| |
| This module monkey-patches the DatabaseManager class from database.db_manager |
| to add: |
| - log_provider_status |
| - get_uptime_percentage |
| - get_avg_response_time |
| |
| The implementations are lightweight and defensive: if the underlying engine |
| is not available, they fail gracefully instead of raising errors. |
| """ |
|
|
| from __future__ import annotations |
|
|
| from datetime import datetime, timedelta |
| from typing import Optional |
|
|
| try: |
| from sqlalchemy import text as _sa_text |
| except Exception: |
| _sa_text = None |
|
|
| try: |
| from .db_manager import DatabaseManager |
| except Exception: |
| DatabaseManager = None |
|
|
|
|
| def _get_engine(instance) -> Optional[object]: |
| """Best-effort helper to get an SQLAlchemy engine from the manager.""" |
| return getattr(instance, "engine", None) |
|
|
|
|
| def _ensure_table(conn) -> None: |
| """Create provider_status table if it does not exist yet.""" |
| if _sa_text is None: |
| return |
| conn.execute( |
| _sa_text( |
| """ |
| CREATE TABLE IF NOT EXISTS provider_status ( |
| id INTEGER PRIMARY KEY AUTOINCREMENT, |
| provider_name TEXT NOT NULL, |
| category TEXT NOT NULL, |
| status TEXT NOT NULL, |
| response_time REAL, |
| status_code INTEGER, |
| error_message TEXT, |
| endpoint_tested TEXT, |
| created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP |
| ) |
| """ |
| ) |
| ) |
|
|
|
|
| def _log_provider_status( |
| self, |
| provider_name: str, |
| category: str, |
| status: str, |
| response_time: Optional[float] = None, |
| status_code: Optional[int] = None, |
| endpoint_tested: Optional[str] = None, |
| error_message: Optional[str] = None, |
| ) -> None: |
| """Insert a status row into provider_status. |
| |
| This is a best-effort logger; if no engine is available it silently returns. |
| """ |
| engine = _get_engine(self) |
| if engine is None or _sa_text is None: |
| return |
|
|
| now = datetime.utcnow() |
| try: |
| with engine.begin() as conn: |
| _ensure_table(conn) |
| conn.execute( |
| _sa_text( |
| """ |
| INSERT INTO provider_status ( |
| provider_name, |
| category, |
| status, |
| response_time, |
| status_code, |
| error_message, |
| endpoint_tested, |
| created_at |
| ) |
| VALUES ( |
| :provider_name, |
| :category, |
| :status, |
| :response_time, |
| :status_code, |
| :error_message, |
| :endpoint_tested, |
| :created_at |
| ) |
| """ |
| ), |
| { |
| "provider_name": provider_name, |
| "category": category, |
| "status": status, |
| "response_time": response_time, |
| "status_code": status_code, |
| "error_message": error_message, |
| "endpoint_tested": endpoint_tested, |
| "created_at": now, |
| }, |
| ) |
| except Exception: |
| |
| return |
|
|
|
|
| def _get_uptime_percentage(self, provider_name: str, hours: int = 24) -> float: |
| """Compute uptime percentage for a provider in the last N hours. |
| |
| Uptime is calculated as the ratio of rows with status='online' to total |
| rows in the provider_status table within the given time window. |
| """ |
| engine = _get_engine(self) |
| if engine is None or _sa_text is None: |
| return 0.0 |
|
|
| cutoff = datetime.utcnow() - timedelta(hours=hours) |
| try: |
| with engine.begin() as conn: |
| _ensure_table(conn) |
| result = conn.execute( |
| _sa_text( |
| """ |
| SELECT |
| COUNT(*) AS total, |
| SUM(CASE WHEN status = 'online' THEN 1 ELSE 0 END) AS online |
| FROM provider_status |
| WHERE provider_name = :provider_name |
| AND created_at >= :cutoff |
| """ |
| ), |
| {"provider_name": provider_name, "cutoff": cutoff}, |
| ).first() |
| except Exception: |
| return 0.0 |
|
|
| if not result or result[0] in (None, 0): |
| return 0.0 |
|
|
| total = float(result[0] or 0) |
| online = float(result[1] or 0) |
| return round(100.0 * online / total, 2) |
|
|
|
|
| def _get_avg_response_time(self, provider_name: str, hours: int = 24) -> float: |
| """Average response time (ms) for a provider over the last N hours.""" |
| engine = _get_engine(self) |
| if engine is None or _sa_text is None: |
| return 0.0 |
|
|
| cutoff = datetime.utcnow() - timedelta(hours=hours) |
| try: |
| with engine.begin() as conn: |
| _ensure_table(conn) |
| result = conn.execute( |
| _sa_text( |
| """ |
| SELECT AVG(response_time) AS avg_response |
| FROM provider_status |
| WHERE provider_name = :provider_name |
| AND response_time IS NOT NULL |
| AND created_at >= :cutoff |
| """ |
| ), |
| {"provider_name": provider_name, "cutoff": cutoff}, |
| ).first() |
| except Exception: |
| return 0.0 |
|
|
| if not result or result[0] is None: |
| return 0.0 |
|
|
| return round(float(result[0]), 2) |
|
|
|
|
| |
| if DatabaseManager is not None: |
| if not hasattr(DatabaseManager, "log_provider_status"): |
| DatabaseManager.log_provider_status = _log_provider_status |
| if not hasattr(DatabaseManager, "get_uptime_percentage"): |
| DatabaseManager.get_uptime_percentage = _get_uptime_percentage |
| if not hasattr(DatabaseManager, "get_avg_response_time"): |
| DatabaseManager.get_avg_response_time = _get_avg_response_time |
|
|