After story 1
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user