From b43636c45a2d47b974f5afb0e3d3aa7b1dc49986 Mon Sep 17 00:00:00 2001 From: Lee Chapman Date: Sun, 8 Feb 2026 23:35:08 -0800 Subject: [PATCH 1/2] fix: use _PooledConnection wrapper instead of reassigning conn.close conn.close is read-only in psycopg2, causing AttributeError and connections never returned to pool. Wrapper delegates to real connection and overrides close() to putconn() instead of closing. Co-authored-by: Cursor --- src/database.py | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/database.py b/src/database.py index 11e8728..6d075aa 100644 --- a/src/database.py +++ b/src/database.py @@ -55,16 +55,26 @@ def _get_pool(self) -> pool.ThreadedConnectionPool: def _get_connection(self): """Get a database connection from the pool (returns to pool on close).""" conn = self._get_pool().getconn() - # Wrap so conn.close() returns to pool instead of closing the underlying connection - def _close(): - try: - conn.rollback() # Reset connection state for reuse - except Exception: - pass - self._get_pool().putconn(conn) + # Use wrapper since conn.close is read-only in psycopg2 + return _PooledConnection(conn, self._get_pool()) + + +class _PooledConnection: + """Wrapper that returns connection to pool on close() instead of closing it.""" + + def __init__(self, conn, pool_instance): + self._conn = conn + self._pool = pool_instance + + def close(self): + try: + self._conn.rollback() + except Exception: + pass + self._pool.putconn(self._conn) - conn.close = _close - return conn + def __getattr__(self, name): + return getattr(self._conn, name) def close_pool(self): """Close the connection pool. Call on application shutdown.""" From 5c3ffb3243a764ee66690778e6bd623029c4d194 Mon Sep 17 00:00:00 2001 From: Lee Chapman Date: Sun, 8 Feb 2026 23:37:20 -0800 Subject: [PATCH 2/2] fix: remove duplicate Database class, add type hint for __getattr__ _PooledConnection had incorrectly absorbed Database methods. Restore correct structure: _PooledConnection only has close/__getattr__, Database has the rest. Co-authored-by: Cursor --- src/database.py | 39 +++++++++++++++++++-------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/src/database.py b/src/database.py index 6d075aa..e088f92 100644 --- a/src/database.py +++ b/src/database.py @@ -3,7 +3,7 @@ import psycopg2 from psycopg2 import pool from datetime import datetime, timezone -from typing import Dict, List, Optional +from typing import Any, Dict, List, Optional logger = logging.getLogger(__name__) @@ -12,6 +12,24 @@ DEFAULT_POOL_MAX_CONN = 50 +class _PooledConnection: + """Wrapper that returns connection to pool on close() instead of closing it.""" + + def __init__(self, conn, pool_instance): + self._conn = conn + self._pool = pool_instance + + def close(self): + try: + self._conn.rollback() + except Exception: + pass + self._pool.putconn(self._conn) + + def __getattr__(self, name: str) -> Any: + return getattr(self._conn, name) + + class Database: """PostgreSQL database manager for Hell Divers 2 API data with connection pooling""" @@ -55,27 +73,8 @@ def _get_pool(self) -> pool.ThreadedConnectionPool: def _get_connection(self): """Get a database connection from the pool (returns to pool on close).""" conn = self._get_pool().getconn() - # Use wrapper since conn.close is read-only in psycopg2 return _PooledConnection(conn, self._get_pool()) - -class _PooledConnection: - """Wrapper that returns connection to pool on close() instead of closing it.""" - - def __init__(self, conn, pool_instance): - self._conn = conn - self._pool = pool_instance - - def close(self): - try: - self._conn.rollback() - except Exception: - pass - self._pool.putconn(self._conn) - - def __getattr__(self, name): - return getattr(self._conn, name) - def close_pool(self): """Close the connection pool. Call on application shutdown.""" if self._pool is not None: