add: PostgreSQL 支持(asyncpg 驱动,from_url/from_config + 集成测试)

This commit is contained in:
2026-08-17 17:26:50 +08:00
parent da7a47ed2f
commit e19575424e
7 changed files with 524 additions and 46 deletions
+79 -7
View File
@@ -1,6 +1,9 @@
# chongming-database
异步 SQLite 数据库封装,基于 `aiosqlite` + `SQLModel`(SQLAlchemy),提供简洁的初始化与会话管理接口,内置多进程并发安全。
异步数据库封装,基于 `SQLModel`(SQLAlchemy),提供简洁的初始化与会话管理接口:
- **SQLite**:`aiosqlite` 驱动,内置多进程并发建表安全
- **PostgreSQL**:`asyncpg` 驱动,支持 URL / 配置字典两种接入方式
---
@@ -20,21 +23,49 @@ 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`) |
| ------------- | ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `__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` | 关闭异步引擎并清除内部状态 |
@@ -42,6 +73,8 @@ await db.dispose() # 关闭连接
`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/<name>/config.toml
[workers.config.database]
type = "postgresql"
url = "postgresql+asyncpg://postgres:chongming@127.0.0.1:5432/chongming"
generate_schemas = true
```
```python
# src/plugins/<name>/__init__.py
@@ -89,7 +144,7 @@ from chongming_worker.plugin import Plugin
class MyPlugin(Plugin):
type = "<name>"
db_name = "<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` — 异步引擎支持
+12 -2
View File
@@ -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"]
+98 -14
View File
@@ -1,35 +1,109 @@
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 = Path(db_path) if db_path is not None else None
self._url = url
if self._db_path is not None:
self._db_path.parent.mkdir(parents=True, exist_ok=True)
async def initialize(self):
"""异步初始化,创建引擎并建立表
@classmethod
def from_url(cls, url: str) -> "Database":
"""按 SQLAlchemy URL 创建实例(如 ``postgresql+asyncpg://user:pwd@host:5432/db``)。"""
return cls(url=url)
注意:DDL(CREATE TABLE)在 SQLite 中会自动提交,无需包裹在事务中。
@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),
- checkfirst=True 会在建表前检查表是否已存在(通过 sqlite_master),
大幅减少多进程并发启动时的 "table already exists" 冲突。
即使 checkfirst 与实际 CREATE 之间存在竞态窗口,后续的 try/except
- 即使 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,
)
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)
await conn.run_sync(
SQLModel.metadata.create_all, checkfirst=True
)
except OperationalError as e:
# 多进程并发启动时,checkfirst 与实际 CREATE 之间仍有极小的竞态窗口:
# 多个进程同时判断"表不存在"并执行 CREATE TABLE,
@@ -39,10 +113,20 @@ class Database:
# 无需手动 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
expire_on_commit=False,
) # type: ignore
def get_session(self) -> AsyncSession:
+39
View File
@@ -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"
+74
View File
@@ -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()
+71
View File
@@ -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()
Generated
+129 -1
View File
@@ -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"