diff --git a/README.md b/README.md index ace8f56..65f3859 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ # chongming-database -异步 SQLite 数据库封装,基于 `aiosqlite` + `SQLModel`(SQLAlchemy),提供简洁的初始化与会话管理接口,内置多进程并发安全。 +异步数据库封装,基于 `SQLModel`(SQLAlchemy),提供简洁的初始化与会话管理接口: + +- **SQLite**:`aiosqlite` 驱动,内置多进程并发建表安全 +- **PostgreSQL**:`asyncpg` 驱动,支持 URL / 配置字典两种接入方式 --- @@ -20,28 +23,58 @@ uv pip install -e utils/database from pathlib import Path from chongming_database import Database +# SQLite(文件路径) db = Database(Path("data/storage/chongming/app.db")) await db.initialize() # 创建引擎 + 自动建表 +# PostgreSQL(URL) +db = Database.from_url("postgresql+asyncpg://user:pwd@127.0.0.1:5432/mydb") +await db.initialize() + async with db.get_session() as session: result = await session.execute(...) await db.dispose() # 关闭连接 ``` +### 构造方式 + +| 方式 | 说明 | +| ------------------------- | ---------------------------------------------------------------------------- | +| `Database(db_path)` | SQLite 文件路径,自动创建父目录(向后兼容旧用法) | +| `Database(url=...)` | SQLAlchemy URL(PostgreSQL),`db_path` 与 `url` 必须且只能提供一个 | +| `Database.from_url(url)` | 同上,classmethod 写法 | +| `Database.from_config(c)` | 按配置字典创建,对齐 config 中 Database TypedDict 的契约(见下) | + +### `from_config` 契约 + +与各处 config 中已有的 `Database` TypedDict 对齐: + +```python +# SQLite +Database.from_config({"type": "sqlite", "database_path": "./data/app.db"}) + +# PostgreSQL(裸 postgresql:// 会自动归一化为 postgresql+asyncpg://) +Database.from_config({"type": "postgresql", "url": "postgresql://user:pwd@127.0.0.1:5432/mydb"}) +``` + +`type="mysql"` 暂不支持,抛 `NotImplementedError`。 + ### 方法说明 -| 方法 | 签名 | 说明 | -| ------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------- | -| `__init__` | `(db_path: Path)` | 传入 SQLite 文件路径,自动创建父目录 | -| `initialize` | `async () -> None` | 创建异步引擎(`sqlite+aiosqlite:///`),`SQLModel.metadata.create_all` 自动建表,构建 `sessionmaker`(`expire_on_commit=False`) | -| `get_session` | `() -> AsyncSession` | 返回新的异步会话;未初始化则抛 `RuntimeError` | -| `dispose` | `async () -> None` | 关闭异步引擎并清除内部状态 | +| 方法 | 签名 | 说明 | +| ------------- | ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | +| `__init__` | `(db_path=None, url=None)` | 二选一必填,否则抛 `ValueError`;`db_path` 自动创建父目录 | +| `initialize` | `(generate_schemas=True, echo=False, **kwargs)` | 创建异步引擎,`SQLModel.metadata.create_all` 自动建表(`generate_schemas=False` 可跳过,交由外部迁移工具),构建 `sessionmaker` | +| `get_session` | `() -> AsyncSession` | 返回新的异步会话;未初始化则抛 `RuntimeError` | +| `dispose` | `async () -> None` | 关闭异步引擎并清除内部状态 | ### 多进程安全 `initialize()` 使用 `checkfirst=True` 检查表是否存在,并捕获 `OperationalError("already exists")` 作为兜底,确保多个进程并发启动时不会因建表冲突而崩溃。 +- SQLite:DDL 自动提交,用 `connect()` 建表;PG:DDL 事务性,用 `begin()` 建表。 + --- ## 使用示例 @@ -78,10 +111,32 @@ finally: await db.dispose() ``` +### PostgreSQL 本地开发(Docker) + +```bash +docker run -d --name chongming-pg \ + -e POSTGRES_PASSWORD=chongming -e POSTGRES_DB=chongming \ + -p 5432:5432 postgres:16-alpine + +# 连接 +# postgresql+asyncpg://postgres:chongming@127.0.0.1:5432/chongming +``` + ### 插件中的典型用法 -推荐通过 `Plugin` 基类自动建库(`db_name` 指定文件名,`data_dir` 取自运行时配置), -`on_init` 中即可拿到 `self.db`: +推荐通过 `Plugin` 基类自动建库: + +- SQLite:`db_name` 指定文件名,`data_dir` 取自运行时配置(默认行为,现有插件零改动) +- PostgreSQL:在插件 `config.toml` 的 `[workers.config.database]` 下配置 `type`/`url`, + 基类 `init_database()` 检测到 `url` 即走 `Database.from_config` 接入 PG + +```toml +# config/workers//config.toml +[workers.config.database] +type = "postgresql" +url = "postgresql+asyncpg://postgres:chongming@127.0.0.1:5432/chongming" +generate_schemas = true +``` ```python # src/plugins//__init__.py @@ -89,7 +144,7 @@ from chongming_worker.plugin import Plugin class MyPlugin(Plugin): type = "" - db_name = ".db" # 基类自动在 data_dir 下建库并建表 + # db_name 用于 SQLite;配置了 [workers.config.database].url 时优先走 PostgreSQL async def on_init(self): db = self.db # 已由基类 init_database() 初始化 @@ -112,8 +167,25 @@ await db.initialize() --- +## 测试 + +```bash +uv sync --dev + +# SQLite 单元测试(无需外部服务) +pytest tests/ + +# PostgreSQL 集成测试(需本地 PG,可通过上面的 docker 命令启动) +CHONGMING_TEST_PG_URL=postgresql+asyncpg://postgres:chongming@127.0.0.1:5432/chongming pytest tests/ +``` + +未设置 `CHONGMING_TEST_PG_URL` 时 PG 测试自动跳过。 + +--- + ## 依赖 - `aiosqlite` — 异步 SQLite 驱动 +- `asyncpg` — 异步 PostgreSQL 驱动 - `sqlmodel` — SQLAlchemy + Pydantic ORM - `sqlalchemy` — 异步引擎支持 diff --git a/pyproject.toml b/pyproject.toml index 713a36e..fc7a8b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,14 +4,24 @@ requires = ["hatchling"] [project] name = "chongming-database" -version = "0.1.0" -description = "Add your description here" +version = "0.2.0" +description = "异步数据库封装:SQLite(aiosqlite)+ PostgreSQL(asyncpg),基于 SQLModel/SQLAlchemy" readme = "README.md" requires-python = ">=3.12" dependencies = [ "aiosqlite>=0.22.1", + "asyncpg>=0.30", "sqlmodel>=0.0.39", ] +[dependency-groups] +dev = [ + "pytest>=8.0", + "pytest-asyncio>=0.24", +] + +[tool.pytest.ini_options] +asyncio_mode = "auto" + [tool.hatch.build.targets.wheel] packages = ["src/chongming_database"] diff --git a/src/chongming_database/__init__.py b/src/chongming_database/__init__.py index ebea8d3..c48a235 100644 --- a/src/chongming_database/__init__.py +++ b/src/chongming_database/__init__.py @@ -1,56 +1,140 @@ from pathlib import Path -from sqlalchemy.ext.asyncio import create_async_engine, AsyncEngine, AsyncSession -from sqlalchemy.orm import sessionmaker from sqlalchemy.exc import OperationalError +from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, create_async_engine +from sqlalchemy.orm import sessionmaker from sqlmodel import SQLModel + class Database: - def __init__(self, db_path: Path): + """异步数据库封装,支持 SQLite 与 PostgreSQL 两种后端。 + + - SQLite: 传入本地文件路径(``Database(Path(...))``,向后兼容旧用法) + - PostgreSQL: 传入 SQLAlchemy URL(``Database.from_url(...)`` / + ``Database(url=...)``),推荐 ``postgresql+asyncpg://`` + """ + + def __init__( + self, + db_path: Path | str | None = None, + url: str | None = None, + ): + if (db_path is None) == (url is None): + raise ValueError("必须且只能提供 db_path(SQLite)或 url(PostgreSQL)之一") + self._engine: AsyncEngine | None = None self._session_maker: sessionmaker | None = None - self._db_path = db_path - self._db_path.parent.mkdir(parents=True, exist_ok=True) + self._db_path = Path(db_path) if db_path is not None else None + self._url = url - async def initialize(self): - """异步初始化,创建引擎并建立表 + if self._db_path is not None: + self._db_path.parent.mkdir(parents=True, exist_ok=True) - 注意:DDL(CREATE TABLE)在 SQLite 中会自动提交,无需包裹在事务中。 - 使用 connect() 而非 begin() 可以避免事务失败状态导致的连锁异常。 + @classmethod + def from_url(cls, url: str) -> "Database": + """按 SQLAlchemy URL 创建实例(如 ``postgresql+asyncpg://user:pwd@host:5432/db``)。""" + return cls(url=url) - checkfirst=True 会在建表前检查表是否已存在(通过 sqlite_master), - 大幅减少多进程并发启动时的 "table already exists" 冲突。 - 即使 checkfirst 与实际 CREATE 之间存在竞态窗口,后续的 try/except - 也会兜底捕获漏网的 OperationalError。 + @classmethod + def from_config(cls, cfg: dict) -> "Database": + """按配置字典创建实例(对齐各处 config 中 Database TypedDict 的契约)。 + + 支持的键:: + + type "sqlite" / "postgresql" / "mysql" + url PostgreSQL 连接 URL(type=postgresql 时必填) + database_path SQLite 文件路径(type=sqlite 时必填) + """ + db_type = cfg.get("type", "sqlite") + if db_type == "sqlite": + db_path = cfg.get("database_path") or cfg.get("url") + if not db_path: + raise ValueError("sqlite 数据库配置缺少 database_path") + return cls(db_path=db_path) + if db_type == "postgresql": + url = cfg.get("url") + if not url: + raise ValueError("postgresql 数据库配置缺少 url") + return cls(url=url) + raise NotImplementedError(f"暂不支持的数据库类型: {db_type}") + + def _build_url(self) -> str: + """生成 SQLAlchemy 引擎 URL。""" + if self._url is not None: + # 允许配置写成裸 postgresql://,统一归一化为 asyncpg 驱动 + if self._url.startswith("postgresql://"): + return self._url.replace("postgresql://", "postgresql+asyncpg://", 1) + return self._url + return f"sqlite+aiosqlite:///{self._db_path}" + + async def initialize( + self, + generate_schemas: bool = True, + echo: bool = False, + **engine_kwargs, + ): + """异步初始化,创建引擎并建立表。 + + SQLite 建表说明(多进程并发安全): + + - DDL(CREATE TABLE)在 SQLite 中会自动提交,无需包裹在事务中, + 使用 connect() 而非 begin() 可以避免事务失败状态导致的连锁异常。 + - checkfirst=True 会在建表前检查表是否已存在(通过 sqlite_master), + 大幅减少多进程并发启动时的 "table already exists" 冲突。 + - 即使 checkfirst 与实际 CREATE 之间存在竞态窗口,后续的 try/except + 也会兜底捕获漏网的 OperationalError。 + + PostgreSQL:DDL 是事务性的,用 begin() 包裹 create_all, + 同样 checkfirst=True + "already exists" 兜底。 + + Args: + generate_schemas: 是否自动建表(False 表示 schema 由外部迁移工具管理) + echo: 是否打印 SQL(调试用) + engine_kwargs: 透传给 create_async_engine(如 pool_size 等) """ self._engine = create_async_engine( - f"sqlite+aiosqlite:///{self._db_path}", - echo=False, + self._build_url(), + echo=echo, future=True, + **engine_kwargs, ) - async with self._engine.connect() as conn: - try: - await conn.run_sync(SQLModel.metadata.create_all, checkfirst=True) - except OperationalError as e: - # 多进程并发启动时,checkfirst 与实际 CREATE 之间仍有极小的竞态窗口: - # 多个进程同时判断"表不存在"并执行 CREATE TABLE, - # 后到者会报 "table ... already exists"。此时表已由其他进程建好,忽略即可。 - # - # 注意:使用 connect() 而非 begin(),DDL 在 SQLite 中自动提交, - # 无需手动 commit/rollback,避免事务失败状态的连锁异常。 - if "already exists" not in str(e).lower(): - raise + if generate_schemas: + if self._engine.dialect.name == "sqlite": + async with self._engine.connect() as conn: + try: + await conn.run_sync( + SQLModel.metadata.create_all, checkfirst=True + ) + except OperationalError as e: + # 多进程并发启动时,checkfirst 与实际 CREATE 之间仍有极小的竞态窗口: + # 多个进程同时判断"表不存在"并执行 CREATE TABLE, + # 后到者会报 "table ... already exists"。此时表已由其他进程建好,忽略即可。 + # + # 注意:使用 connect() 而非 begin(),DDL 在 SQLite 中自动提交, + # 无需手动 commit/rollback,避免事务失败状态的连锁异常。 + if "already exists" not in str(e).lower(): + raise + else: + # PostgreSQL 等支持事务性 DDL 的后端 + async with self._engine.begin() as conn: + try: + await conn.run_sync( + SQLModel.metadata.create_all, checkfirst=True + ) + except OperationalError as e: + if "already exists" not in str(e).lower(): + raise self._session_maker = sessionmaker( - self._engine, # type: ignore - class_=AsyncSession, - expire_on_commit=False - ) # type: ignore - + self._engine, # type: ignore + class_=AsyncSession, + expire_on_commit=False, + ) # type: ignore + def get_session(self) -> AsyncSession: """返回一个异步上下文管理器,用于获取会话""" if self._session_maker is None: raise RuntimeError("Database not initialized") return self._session_maker() - + async def dispose(self): """关闭引擎,释放资源""" if self._engine: diff --git a/tests/test_database.py b/tests/test_database.py new file mode 100644 index 0000000..b334d95 --- /dev/null +++ b/tests/test_database.py @@ -0,0 +1,39 @@ +"""Database 构造与 from_config 的纯 API 校验(无 IO)。""" + +import pytest + +from chongming_database import Database + + +async def test_constructor_requires_exactly_one_arg(): + # 两个都不给 + with pytest.raises(ValueError): + Database() + # 两个都给 + with pytest.raises(ValueError): + Database("x.db", url="postgresql+asyncpg://localhost/db") + + +async def test_from_config_sqlite_requires_path(): + with pytest.raises(ValueError): + Database.from_config({"type": "sqlite"}) + + +async def test_from_config_postgres_requires_url(): + with pytest.raises(ValueError): + Database.from_config({"type": "postgresql"}) + + +async def test_from_config_unsupported_type(): + with pytest.raises(NotImplementedError): + Database.from_config({"type": "mysql", "url": "mysql://localhost/db"}) + + +async def test_postgres_url_normalized_to_asyncpg(): + db = Database.from_url("postgresql://user:pwd@127.0.0.1:5432/mydb") + assert db._build_url() == "postgresql+asyncpg://user:pwd@127.0.0.1:5432/mydb" + + +async def test_explicit_driver_url_untouched(): + db = Database(url="postgresql+asyncpg://user:pwd@127.0.0.1:5432/mydb") + assert db._build_url() == "postgresql+asyncpg://user:pwd@127.0.0.1:5432/mydb" diff --git a/tests/test_postgres.py b/tests/test_postgres.py new file mode 100644 index 0000000..381642d --- /dev/null +++ b/tests/test_postgres.py @@ -0,0 +1,74 @@ +"""PostgreSQL 后端集成测试(需 CHONGMING_TEST_PG_URL 环境变量,否则跳过)。""" + +import os +import uuid + +import pytest +from sqlmodel import Field, SQLModel, select + +from chongming_database import Database + +PG_URL = os.environ.get("CHONGMING_TEST_PG_URL") + +pytestmark = pytest.mark.skipif( + not PG_URL, reason="CHONGMING_TEST_PG_URL 未设置,跳过 PostgreSQL 集成测试" +) + + +def _make_item_table(tablename: str): + """动态生成随机表名的 SQLModel 表,避免污染共享 PG 实例。""" + + class Item(SQLModel, table=True): + __tablename__ = tablename + + id: int | None = Field(default=None, primary_key=True) + name: str = Field(max_length=50) + + return Item + + +async def test_postgres_crud(): + Item = _make_item_table(f"pg_test_item_{uuid.uuid4().hex[:8]}") + db = Database.from_url(PG_URL) + await db.initialize() + try: + async with db.get_session() as session: + session.add(Item(name="pg")) + await session.commit() + async with db.get_session() as session: + rows = (await session.execute(select(Item))).scalars().all() + assert [r.name for r in rows] == ["pg"] + finally: + await db.dispose() + + +async def test_postgres_bare_url_scheme(): + """裸 postgresql:// 应被归一化为 postgresql+asyncpg:// 后正常连接。""" + bare = PG_URL.replace("postgresql+asyncpg://", "postgresql://", 1) + db = Database.from_url(bare) + await db.initialize() + try: + async with db.get_session() as session: + await session.execute(select(1)) + finally: + await db.dispose() + + +async def test_from_config_postgres_end_to_end(): + db = Database.from_config({"type": "postgresql", "url": PG_URL}) + await db.initialize() + try: + async with db.get_session() as session: + await session.execute(select(1)) + finally: + await db.dispose() + + +async def test_postgres_generate_schemas_false(): + db = Database.from_url(PG_URL) + await db.initialize(generate_schemas=False) + try: + async with db.get_session() as session: + await session.execute(select(1)) + finally: + await db.dispose() diff --git a/tests/test_sqlite.py b/tests/test_sqlite.py new file mode 100644 index 0000000..cbcc3a9 --- /dev/null +++ b/tests/test_sqlite.py @@ -0,0 +1,71 @@ +"""SQLite 后端回归测试(tmp_path,无需外部服务)。""" + +from pathlib import Path + +import pytest +from sqlmodel import Field, SQLModel, select + +from chongming_database import Database + + +class SqliteItem(SQLModel, table=True): + __tablename__ = "backend_test_item" + + id: int | None = Field(default=None, primary_key=True) + name: str = Field(max_length=50) + + +async def test_sqlite_crud(tmp_path: Path): + db = Database(tmp_path / "nested" / "test.db") + await db.initialize() + try: + async with db.get_session() as session: + session.add(SqliteItem(name="a")) + await session.commit() + async with db.get_session() as session: + rows = (await session.execute(select(SqliteItem))).scalars().all() + assert [r.name for r in rows] == ["a"] + finally: + await db.dispose() + + +async def test_sqlite_initialize_is_idempotent(tmp_path: Path): + """重复 initialize(模拟多进程各自初始化)不应报错。""" + db = Database(tmp_path / "test.db") + await db.initialize() + try: + await db.initialize() + finally: + await db.dispose() + + +async def test_get_session_before_initialize_raises(tmp_path: Path): + db = Database(tmp_path / "test.db") + try: + with pytest.raises(RuntimeError): + db.get_session() + finally: + await db.dispose() + + +async def test_from_config_sqlite_end_to_end(tmp_path: Path): + db = Database.from_config( + {"type": "sqlite", "database_path": str(tmp_path / "cfg.db")} + ) + await db.initialize() + try: + async with db.get_session() as session: + await session.execute(select(SqliteItem)) + finally: + await db.dispose() + + +async def test_generate_schemas_false_skips_ddl(tmp_path: Path): + """generate_schemas=False 时只建引擎不建表,会话仍可用。""" + db = Database(tmp_path / "noschema.db") + await db.initialize(generate_schemas=False) + try: + async with db.get_session() as session: + await session.execute(select(1)) + finally: + await db.dispose() diff --git a/uv.lock b/uv.lock index 93cc8f7..25dea6a 100644 --- a/uv.lock +++ b/uv.lock @@ -20,21 +20,84 @@ wheels = [ { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" }, ] +[[package]] +name = "asyncpg" +version = "0.31.0" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/fe/cc/d18065ce2380d80b1bcce927c24a2642efd38918e33fd724bc4bca904877/asyncpg-0.31.0.tar.gz", hash = "sha256:c989386c83940bfbd787180f2b1519415e2d3d6277a70d9d0f0145ac73500735", size = 993667, upload-time = "2025-11-24T23:27:00.812Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/2a/a6/59d0a146e61d20e18db7396583242e32e0f120693b67a8de43f1557033e2/asyncpg-0.31.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b44c31e1efc1c15188ef183f287c728e2046abb1d26af4d20858215d50d91fad", size = 662042, upload-time = "2025-11-24T23:25:49.578Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/36/01/ffaa189dcb63a2471720615e60185c3f6327716fdc0fc04334436fbb7c65/asyncpg-0.31.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0c89ccf741c067614c9b5fc7f1fc6f3b61ab05ae4aaa966e6fd6b93097c7d20d", size = 638504, upload-time = "2025-11-24T23:25:51.501Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/9f/62/3f699ba45d8bd24c5d65392190d19656d74ff0185f42e19d0bbd973bb371/asyncpg-0.31.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:12b3b2e39dc5470abd5e98c8d3373e4b1d1234d9fbdedf538798b2c13c64460a", size = 3426241, upload-time = "2025-11-24T23:25:53.278Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/8c/d1/a867c2150f9c6e7af6462637f613ba67f78a314b00db220cd26ff559d532/asyncpg-0.31.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:aad7a33913fb8bcb5454313377cc330fbb19a0cd5faa7272407d8a0c4257b671", size = 3520321, upload-time = "2025-11-24T23:25:54.982Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/7a/1a/cce4c3f246805ecd285a3591222a2611141f1669d002163abef999b60f98/asyncpg-0.31.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3df118d94f46d85b2e434fd62c84cb66d5834d5a890725fe625f498e72e4d5ec", size = 3316685, upload-time = "2025-11-24T23:25:57.43Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/40/ae/0fc961179e78cc579e138fad6eb580448ecae64908f95b8cb8ee2f241f67/asyncpg-0.31.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bd5b6efff3c17c3202d4b37189969acf8927438a238c6257f66be3c426beba20", size = 3471858, upload-time = "2025-11-24T23:25:59.636Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/52/b2/b20e09670be031afa4cbfabd645caece7f85ec62d69c312239de568e058e/asyncpg-0.31.0-cp312-cp312-win32.whl", hash = "sha256:027eaa61361ec735926566f995d959ade4796f6a49d3bde17e5134b9964f9ba8", size = 527852, upload-time = "2025-11-24T23:26:01.084Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b5/f0/f2ed1de154e15b107dc692262395b3c17fc34eafe2a78fc2115931561730/asyncpg-0.31.0-cp312-cp312-win_amd64.whl", hash = "sha256:72d6bdcbc93d608a1158f17932de2321f68b1a967a13e014998db87a72ed3186", size = 597175, upload-time = "2025-11-24T23:26:02.564Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/95/11/97b5c2af72a5d0b9bc3fa30cd4b9ce22284a9a943a150fdc768763caf035/asyncpg-0.31.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c204fab1b91e08b0f47e90a75d1b3c62174dab21f670ad6c5d0f243a228f015b", size = 661111, upload-time = "2025-11-24T23:26:04.467Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/1b/71/157d611c791a5e2d0423f09f027bd499935f0906e0c2a416ce712ba51ef3/asyncpg-0.31.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:54a64f91839ba59008eccf7aad2e93d6e3de688d796f35803235ea1c4898ae1e", size = 636928, upload-time = "2025-11-24T23:26:05.944Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/2e/fc/9e3486fb2bbe69d4a867c0b76d68542650a7ff1574ca40e84c3111bb0c6e/asyncpg-0.31.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0e0822b1038dc7253b337b0f3f676cadc4ac31b126c5d42691c39691962e403", size = 3424067, upload-time = "2025-11-24T23:26:07.957Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/12/c6/8c9d076f73f07f995013c791e018a1cd5f31823c2a3187fc8581706aa00f/asyncpg-0.31.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bef056aa502ee34204c161c72ca1f3c274917596877f825968368b2c33f585f4", size = 3518156, upload-time = "2025-11-24T23:26:09.591Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ae/3b/60683a0baf50fbc546499cfb53132cb6835b92b529a05f6a81471ab60d0c/asyncpg-0.31.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0bfbcc5b7ffcd9b75ab1558f00db2ae07db9c80637ad1b2469c43df79d7a5ae2", size = 3319636, upload-time = "2025-11-24T23:26:11.168Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/50/dc/8487df0f69bd398a61e1792b3cba0e47477f214eff085ba0efa7eac9ce87/asyncpg-0.31.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22bc525ebbdc24d1261ecbf6f504998244d4e3be1721784b5f64664d61fbe602", size = 3472079, upload-time = "2025-11-24T23:26:13.164Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/13/a1/c5bbeeb8531c05c89135cb8b28575ac2fac618bcb60119ee9696c3faf71c/asyncpg-0.31.0-cp313-cp313-win32.whl", hash = "sha256:f890de5e1e4f7e14023619399a471ce4b71f5418cd67a51853b9910fdfa73696", size = 527606, upload-time = "2025-11-24T23:26:14.78Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/91/66/b25ccb84a246b470eb943b0107c07edcae51804912b824054b3413995a10/asyncpg-0.31.0-cp313-cp313-win_amd64.whl", hash = "sha256:dc5f2fa9916f292e5c5c8b2ac2813763bcd7f58e130055b4ad8a0531314201ab", size = 596569, upload-time = "2025-11-24T23:26:16.189Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/3c/36/e9450d62e84a13aea6580c83a47a437f26c7ca6fa0f0fd40b6670793ea30/asyncpg-0.31.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f6b56b91bb0ffc328c4e3ed113136cddd9deefdf5f79ab448598b9772831df44", size = 660867, upload-time = "2025-11-24T23:26:17.631Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/82/4b/1d0a2b33b3102d210439338e1beea616a6122267c0df459ff0265cd5807a/asyncpg-0.31.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:334dec28cf20d7f5bb9e45b39546ddf247f8042a690bff9b9573d00086e69cb5", size = 638349, upload-time = "2025-11-24T23:26:19.689Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/41/aa/e7f7ac9a7974f08eff9183e392b2d62516f90412686532d27e196c0f0eeb/asyncpg-0.31.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98cc158c53f46de7bb677fd20c417e264fc02b36d901cc2a43bd6cb0dc6dbfd2", size = 3410428, upload-time = "2025-11-24T23:26:21.275Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/6f/de/bf1b60de3dede5c2731e6788617a512bc0ebd9693eac297ee74086f101d7/asyncpg-0.31.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9322b563e2661a52e3cdbc93eed3be7748b289f792e0011cb2720d278b366ce2", size = 3471678, upload-time = "2025-11-24T23:26:23.627Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/46/78/fc3ade003e22d8bd53aaf8f75f4be48f0b460fa73738f0391b9c856a9147/asyncpg-0.31.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19857a358fc811d82227449b7ca40afb46e75b33eb8897240c3839dd8b744218", size = 3313505, upload-time = "2025-11-24T23:26:25.235Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/bf/e9/73eb8a6789e927816f4705291be21f2225687bfa97321e40cd23055e903a/asyncpg-0.31.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ba5f8886e850882ff2c2ace5732300e99193823e8107e2c53ef01c1ebfa1e85d", size = 3434744, upload-time = "2025-11-24T23:26:26.944Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/08/4b/f10b880534413c65c5b5862f79b8e81553a8f364e5238832ad4c0af71b7f/asyncpg-0.31.0-cp314-cp314-win32.whl", hash = "sha256:cea3a0b2a14f95834cee29432e4ddc399b95700eb1d51bbc5bfee8f31fa07b2b", size = 532251, upload-time = "2025-11-24T23:26:28.404Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d3/2d/7aa40750b7a19efa5d66e67fc06008ca0f27ba1bd082e457ad82f59aba49/asyncpg-0.31.0-cp314-cp314-win_amd64.whl", hash = "sha256:04d19392716af6b029411a0264d92093b6e5e8285ae97a39957b9a9c14ea72be", size = 604901, upload-time = "2025-11-24T23:26:30.34Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ce/fe/b9dfe349b83b9dee28cc42360d2c86b2cdce4cb551a2c2d27e156bcac84d/asyncpg-0.31.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bdb957706da132e982cc6856bb2f7b740603472b54c3ebc77fe60ea3e57e1bd2", size = 702280, upload-time = "2025-11-24T23:26:32Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/6a/81/e6be6e37e560bd91e6c23ea8a6138a04fd057b08cf63d3c5055c98e81c1d/asyncpg-0.31.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6d11b198111a72f47154fa03b85799f9be63701e068b43f84ac25da0bda9cb31", size = 682931, upload-time = "2025-11-24T23:26:33.572Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a6/45/6009040da85a1648dd5bc75b3b0a062081c483e75a1a29041ae63a0bf0dc/asyncpg-0.31.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18c83b03bc0d1b23e6230f5bf8d4f217dc9bc08644ce0502a9d91dc9e634a9c7", size = 3581608, upload-time = "2025-11-24T23:26:35.638Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/7e/06/2e3d4d7608b0b2b3adbee0d0bd6a2d29ca0fc4d8a78f8277df04e2d1fd7b/asyncpg-0.31.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e009abc333464ff18b8f6fd146addffd9aaf63e79aa3bb40ab7a4c332d0c5e9e", size = 3498738, upload-time = "2025-11-24T23:26:37.275Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/7d/aa/7d75ede780033141c51d83577ea23236ba7d3a23593929b32b49db8ed36e/asyncpg-0.31.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3b1fbcb0e396a5ca435a8826a87e5c2c2cc0c8c68eb6fadf82168056b0e53a8c", size = 3401026, upload-time = "2025-11-24T23:26:39.423Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ba/7a/15e37d45e7f7c94facc1e9148c0e455e8f33c08f0b8a0b1deb2c5171771b/asyncpg-0.31.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8df714dba348efcc162d2adf02d213e5fab1bd9f557e1305633e851a61814a7a", size = 3429426, upload-time = "2025-11-24T23:26:41.032Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/13/d5/71437c5f6ae5f307828710efbe62163974e71237d5d46ebd2869ea052d10/asyncpg-0.31.0-cp314-cp314t-win32.whl", hash = "sha256:1b41f1afb1033f2b44f3234993b15096ddc9cd71b21a42dbd87fc6a57b43d65d", size = 614495, upload-time = "2025-11-24T23:26:42.659Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/3c/d7/8fb3044eaef08a310acfe23dae9a8e2e07d305edc29a53497e52bc76eca7/asyncpg-0.31.0-cp314-cp314t-win_amd64.whl", hash = "sha256:bd4107bb7cdd0e9e65fae66a62afd3a249663b844fa34d479f6d5b3bef9c04c3", size = 706062, upload-time = "2025-11-24T23:26:44.086Z" }, +] + [[package]] name = "chongming-database" -version = "0.1.0" +version = "0.2.0" source = { editable = "." } dependencies = [ { name = "aiosqlite" }, + { name = "asyncpg" }, { name = "sqlmodel" }, ] +[package.dev-dependencies] +dev = [ + { name = "pytest" }, + { name = "pytest-asyncio" }, +] + [package.metadata] requires-dist = [ { name = "aiosqlite", specifier = ">=0.22.1" }, + { name = "asyncpg", specifier = ">=0.30" }, { name = "sqlmodel", specifier = ">=0.0.39" }, ] +[package.metadata.requires-dev] +dev = [ + { name = "pytest", specifier = ">=8.0" }, + { name = "pytest-asyncio", specifier = ">=0.24" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + [[package]] name = "greenlet" version = "3.5.4" @@ -90,6 +153,33 @@ wheels = [ { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/93/e8/65e8707d00fe2a49bf12f609a9b2b39ba6dd23c2810eacad877c4fc94bfe/greenlet-3.5.4-cp315-cp315t-win_arm64.whl", hash = "sha256:08fc36de8442d5c3e95b044550dbea9bf144d31ec0cc58e36fb241cb6ef6a994", size = 250538, upload-time = "2026-07-22T11:40:17.985Z" }, ] +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "packaging" +version = "26.3" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + [[package]] name = "pydantic" version = "2.13.4" @@ -180,6 +270,44 @@ wheels = [ { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, ] +[[package]] +name = "pygments" +version = "2.21.0" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.4.0" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +dependencies = [ + { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42", size = 58514, upload-time = "2026-05-26T09:56:04.083Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930, upload-time = "2026-05-26T09:56:02.576Z" }, +] + [[package]] name = "sqlalchemy" version = "2.0.51"