""" db.py — Транспортный слой к базе данных (с пулом соединений). Пул psycopg2.pool.ThreadedConnectionPool (min=1, max=3). Все query/execute забирают соединение из пула и возвращают обратно. """ import os from psycopg2 import pool as _pgpool import psycopg2 _pool = None def _get_pool(): """Ленивая инициализация пула.""" global _pool if _pool is None: _pool = _pgpool.ThreadedConnectionPool( minconn=1, maxconn=3, host=os.getenv("DB_HOST"), port=os.getenv("DB_PORT", 5432), dbname=os.getenv("DB_NAME"), user=os.getenv("DB_USER"), password=os.getenv("DB_PASS"), sslmode=os.getenv("DB_SSLMODE", "disable"), ) return _pool def _pg_connect(dbname): """ Сырое подключение к ЛЮБОЙ базе (для /test createdb). НЕ из пула — для создания БД нужна отдельная сессия. """ return psycopg2.connect( host=os.getenv("DB_HOST"), port=os.getenv("DB_PORT", 5432), dbname=dbname, user=os.getenv("DB_USER"), password=os.getenv("DB_PASS"), sslmode=os.getenv("DB_SSLMODE", "disable"), ) def connect(): """ Подключение к ЦЕЛЕВОЙ базе из пула. Возвращает (connection, None) или (None, error). """ try: return _get_pool().getconn(), None except Exception as e: return None, str(e) def put_conn(conn): """Вернуть соединение в пул.""" try: _get_pool().putconn(conn) except Exception: pass def query(sql_text, params=None): """ SELECT → (result, None) или (None, error). result = {"columns": [...], "rows": [[...], ...]} """ conn, err = connect() if err: return None, f"connect: {err}" try: cur = conn.cursor() cur.execute(sql_text, params) rows = cur.fetchall() cols = [desc[0] for desc in cur.description] if cur.description else [] cur.close() return {"columns": cols, "rows": [list(r) for r in rows]}, None except Exception as e: return None, str(e) finally: put_conn(conn) def execute(sql_text, params=None): """ INSERT/UPDATE/DELETE → (rowcount, None) или (None, error). """ conn, err = connect() if err: return None, f"connect: {err}" try: cur = conn.cursor() cur.execute(sql_text, params) conn.commit() rowcount = cur.rowcount cur.close() return rowcount, None except Exception as e: try: conn.rollback() except: pass return None, str(e) finally: put_conn(conn) def query_one(sql_text, params=None): """ SELECT одной строки → (row_dict, None) или (None, error). """ result, err = query(sql_text, params) if err: return None, err rows = result["rows"] if not rows: return None, None return dict(zip(result["columns"], rows[0])), None