| import os | |
| import psycopg2 | |
| from psycopg2.extras import RealDictCursor | |
| from contextlib import contextmanager | |
| import logging | |
| logger = logging.getLogger(__name__) | |
| def get_cursor(): | |
| """ | |
| New connection per call — psycopg2 is not thread-safe across Gradio workers. | |
| Commits on clean exit, rolls back on exception. | |
| """ | |
| url = os.environ.get("DATABASE_URL") | |
| if not url: | |
| raise RuntimeError("DATABASE_URL not set — is Docker Compose running?") | |
| conn = psycopg2.connect(url) | |
| try: | |
| with conn.cursor(cursor_factory=RealDictCursor) as cur: | |
| yield cur | |
| conn.commit() | |
| except Exception: | |
| conn.rollback() | |
| raise | |
| finally: | |
| conn.close() | |