After story 1

This commit is contained in:
2026-08-03 11:30:49 -04:00
commit 82590c392f
32 changed files with 4375 additions and 0 deletions
+137
View File
@@ -0,0 +1,137 @@
"""
Database connection layer with connection pooling for VideoDetect.
Provides:
- Connection pooling via DBUtils + PyMySQL
- Automatic reconnection on disconnect
- Context manager support
- Prepared statements for all queries
- Transaction support for atomic state transitions
"""
import contextlib
import logging
from typing import Optional
from dbutils.pooled_db import PooledDB
import pymysql
logger = logging.getLogger(__name__)
class DBConnector:
"""Thread-safe database connection pool manager."""
def __init__(
self,
host: str = "mariadb",
port: int = 3306,
database: str = "videodetect",
user: str = "videodetect",
password: str = "videodetect123",
pool_size: int = 20,
pool_min: int = 5,
pool_recycle: int = 3600,
):
self._pool = PooledDB(
creator=pymysql,
maxconnections=pool_size,
mincached=pool_min,
maxcached=pool_size,
maxusage=200,
blocking=True,
max_idle_time=pool_recycle,
connection_timeout=10,
charset="utf8mb4",
cursorclass=pymysql.cursors.DictCursor,
host=host,
port=port,
database=database,
user=user,
password=password,
read_timeout=30,
write_timeout=30,
)
logger.info(
"DB pool initialized: host=%s db=%s pool_size=%d min=%d",
host, database, pool_size, pool_min,
)
def get_connection(self):
"""Get a connection from the pool."""
return self._pool.connection()
@contextlib.contextmanager
def get_cursor(self, transaction: bool = False):
"""Context manager for getting a cursor with optional transaction support."""
conn = self.get_connection()
try:
cursor = conn.cursor()
if transaction:
conn.begin()
yield cursor
if transaction:
conn.commit()
except Exception:
if transaction:
conn.rollback()
raise
finally:
cursor.close()
conn.close()
@contextlib.contextmanager
def transaction(self):
"""Context manager for a full transaction."""
conn = self.get_connection()
try:
conn.begin()
yield conn
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()
def execute(self, query: str, params=None, transaction: bool = False):
"""Execute a query and return affected rows."""
with self.get_cursor(transaction=transaction) as cursor:
cursor.execute(query, params or ())
return cursor.rowcount
def fetchone(self, query: str, params=None):
"""Execute a query and return one row."""
with self.get_cursor() as cursor:
cursor.execute(query, params or ())
return cursor.fetchone()
def fetchall(self, query: str, params=None):
"""Execute a query and return all rows."""
with self.get_cursor() as cursor:
cursor.execute(query, params or ())
return cursor.fetchall()
def initialize_schema(self, schema_path: str = "db/schema.sql"):
"""Initialize the database schema from SQL file."""
with open(schema_path, "r") as f:
sql = f.read()
# Split on semicolons and execute each statement
statements = [s.strip() for s in sql.split(";") if s.strip()]
for stmt in statements:
if stmt.startswith("--"):
continue
try:
self.execute(stmt)
except Exception as e:
logger.debug("Schema statement skipped (may already exist): %s", e)
logger.info("Schema initialized from %s", schema_path)
def health_check(self) -> bool:
"""Check if the database is reachable."""
try:
result = self.fetchone("SELECT 1")
return result is not None
except Exception as e:
logger.error("Health check failed: %s", e)
return False