Download app/database.py from bg5/zai: direct link, hf CLI and curl.
- Browser
- Download file 5.32 kB
-
https://huggingface.co/spaces/bg5/zai/resolve/main/app/database.py
- Command line
-
hf download hf://spaces/bg5/zai/app/database.py
-
curl -L -o database.py https://huggingface.co/spaces/bg5/zai/resolve/main/app/database.py
5.32 kB
| import os | |
| import ssl as _stdlib_ssl | |
| from typing import AsyncGenerator | |
| from urllib.parse import urlparse, parse_qs, urlencode, urlunparse | |
| from contextlib import asynccontextmanager | |
| from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine | |
| from app.core.config import settings | |
| def _clean_db_url(url: str) -> tuple[str, dict]: | |
| """Strip unsupported parameters from a PostgreSQL URL and return (cleaned_url, connect_args). | |
| - asyncpg does not accept `sslmode` as a query-string parameter (that is a | |
| libpq / psycopg2 convention). We translate it into asyncpg's `ssl` | |
| connect-arg instead. | |
| - asyncpg does not accept `channel_binding`. We strip it. | |
| """ | |
| parsed = urlparse(url) | |
| qs = parse_qs(parsed.query) | |
| sslmode = qs.pop("sslmode", [None])[0] | |
| # Remove channel_binding to prevent TypeError in asyncpg connect() | |
| qs.pop("channel_binding", None) | |
| # Rebuild the URL without the stripped parameters | |
| new_query = urlencode({k: v[0] for k, v in qs.items()}) | |
| cleaned_url = urlunparse(parsed._replace(query=new_query)) | |
| connect_args: dict = {} | |
| if sslmode: | |
| if sslmode == "disable": | |
| connect_args["ssl"] = False | |
| elif sslmode in ("require", "prefer"): | |
| # asyncpg accepts an ssl.SSLContext or the string "require" (>= 0.29) | |
| # Use a permissive SSLContext that skips certificate verification, | |
| # which matches the behavior of libpq's `sslmode=require`. | |
| ctx = _stdlib_ssl.create_default_context() | |
| ctx.check_hostname = False | |
| ctx.verify_mode = _stdlib_ssl.CERT_NONE | |
| connect_args["ssl"] = ctx | |
| elif sslmode in ("verify-ca", "verify-full"): | |
| connect_args["ssl"] = True # uses default SSL context with verification | |
| return cleaned_url, connect_args | |
| def get_db_url(db_url: str = None) -> tuple[str, dict]: | |
| """获取数据库连接 URL 和 connect_args""" | |
| url = db_url or settings.DATABASE_URL | |
| connect_args: dict = {} | |
| if url: | |
| if url.startswith("postgres://"): | |
| url = url.replace("postgres://", "postgresql+asyncpg://", 1) | |
| elif url.startswith("postgresql://"): | |
| url = url.replace("postgresql://", "postgresql+asyncpg://", 1) | |
| # 如果已经是 postgresql+asyncpg 或者是 sqlite+aiosqlite,则直接使用 | |
| elif url.startswith("sqlite"): | |
| if not url.startswith("sqlite+aiosqlite"): | |
| url = url.replace("sqlite://", "sqlite+aiosqlite://", 1) | |
| # Strip unsupported parameters for asyncpg compatibility | |
| if "asyncpg" in url: | |
| url, connect_args = _clean_db_url(url) | |
| return url, connect_args | |
| db_path = settings.DB_PATH | |
| if not os.path.isabs(db_path): | |
| db_path = os.path.abspath(db_path) | |
| return f"sqlite+aiosqlite:///{db_path}", {} | |
| # 1. 声明全局变量为 None,绝不在这里直接实例化 | |
| engine = None | |
| async_session = None | |
| def get_session_maker(): | |
| """懒加载:获取真实的 session_maker,如果不存在则在当前进程初始化""" | |
| global engine, async_session | |
| if engine is None: | |
| _db_url, _connect_args = get_db_url() | |
| engine = create_async_engine( | |
| _db_url, | |
| echo=False, | |
| pool_pre_ping=True, | |
| pool_size=5, | |
| max_overflow=10, | |
| pool_recycle=3600, | |
| pool_timeout=30, | |
| connect_args=_connect_args | |
| ) | |
| async_session = async_sessionmaker(engine, expire_on_commit=False, class_=AsyncSession) | |
| return async_session | |
| async def get_db_session() -> AsyncGenerator[AsyncSession, None]: | |
| """推荐的上下文管理器用法,自动获取 maker 并管理 session""" | |
| maker = get_session_maker() | |
| async with maker() as session: | |
| yield session | |
| async def init_db(): | |
| """初始化数据库表""" | |
| from app.utils.logger import logger, log_exception | |
| # 确保 engine 已被当前 Worker 进程创建 | |
| get_session_maker() | |
| global engine | |
| async with engine.begin() as conn: | |
| try: | |
| from alembic.config import Config | |
| from alembic import command | |
| import os | |
| logger.debug("自动数据库迁移: 准备执行 Alembic...") | |
| base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | |
| alembic_cfg = Config(os.path.join(base_dir, "alembic.ini")) | |
| alembic_cfg.set_main_option("script_location", os.path.join(base_dir, "migrations")) | |
| def run_upgrade(connection): | |
| alembic_cfg.attributes['connection'] = connection | |
| command.upgrade(alembic_cfg, "head") | |
| await conn.run_sync(run_upgrade) | |
| logger.debug("迁移成功") | |
| except Exception as e: | |
| log_exception(logger, "自动迁移过程中出错") | |
| async def close_db(): | |
| """关闭数据库连接""" | |
| global engine, async_session | |
| if engine is not None: | |
| await engine.dispose() | |
| engine = None # 释放后重置为 None | |
| async_session = None | |