From d03bc885f581194d5de56b70fa9cdfac6958934b Mon Sep 17 00:00:00 2001
From: John Richard
Date: Sat, 4 Apr 2026 11:12:46 +0800
Subject: [PATCH 1/5] Refactor: plugin
---
.github/workflows/CI.yml | 5 -
.github/workflows/PR.yml | 14 +-
.gitignore | 3 +-
README.md | 2 +
nonebot_plugin_value/api/api_balance.py | 16 +-
nonebot_plugin_value/api/api_currency.py | 24 +-
nonebot_plugin_value/api/api_transaction.py | 9 +-
nonebot_plugin_value/hook/exception.py | 6 +-
nonebot_plugin_value/hook/hooks_manager.py | 21 +-
nonebot_plugin_value/repositories/account.py | 274 ++++++++----------
nonebot_plugin_value/repositories/currency.py | 166 +++++------
.../repositories/transaction.py | 96 +++---
nonebot_plugin_value/services/balance.py | 180 ++++++------
nonebot_plugin_value/services/currency.py | 71 +++--
nonebot_plugin_value/services/transaction.py | 13 +-
pyproject.toml | 31 +-
run_test.sh | 3 +
tests/balance_transfer.py | 46 +++
tests/batch_run.py | 99 +++++++
tests/{value_tests => }/cache_test.py | 49 ++--
tests/conftest.py | 2 +-
tests/create_currency.py | 69 +++++
tests/default_account.py | 47 +++
tests/depends.py | 60 ++++
tests/test_api_coverage.py | 155 ++++++++++
tests/test_dump_tools.py | 129 +++++++++
tests/test_hooks.py | 145 +++++++++
tests/transaction_histories.py | 47 +++
tests/value_tests/balance_transfer.py | 19 --
tests/value_tests/batch_run.py | 44 ---
tests/value_tests/create_currency.py | 30 --
tests/value_tests/default_account.py | 27 --
tests/value_tests/depends.py | 35 ---
tests/value_tests/transaction_histories.py | 16 -
uv.lock | 138 ++++++++-
35 files changed, 1465 insertions(+), 626 deletions(-)
create mode 100755 run_test.sh
create mode 100644 tests/balance_transfer.py
create mode 100644 tests/batch_run.py
rename tests/{value_tests => }/cache_test.py (59%)
create mode 100644 tests/create_currency.py
create mode 100644 tests/default_account.py
create mode 100644 tests/depends.py
create mode 100644 tests/test_api_coverage.py
create mode 100644 tests/test_dump_tools.py
create mode 100644 tests/test_hooks.py
create mode 100644 tests/transaction_histories.py
delete mode 100644 tests/value_tests/balance_transfer.py
delete mode 100644 tests/value_tests/batch_run.py
delete mode 100644 tests/value_tests/create_currency.py
delete mode 100644 tests/value_tests/default_account.py
delete mode 100644 tests/value_tests/depends.py
delete mode 100644 tests/value_tests/transaction_histories.py
diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml
index 9ff0cdd..00bf432 100644
--- a/.github/workflows/CI.yml
+++ b/.github/workflows/CI.yml
@@ -41,11 +41,6 @@ jobs:
uses: astral-sh/ruff-action@v3
with:
args: check . --exit-non-zero-on-fix
-
- - name: Prepare database
- run: uv run nb orm upgrade
- - name: Test
- run: uv run pytest ./tests/value_tests/*
- name: Build package
run: uv build # 生成构建产物到dist目录
diff --git a/.github/workflows/PR.yml b/.github/workflows/PR.yml
index cc1de55..8ea66e2 100644
--- a/.github/workflows/PR.yml
+++ b/.github/workflows/PR.yml
@@ -45,8 +45,18 @@ jobs:
- name: Prepare database
run: uv run nb orm upgrade
- - name: Test
- run: uv run pytest ./tests/value_tests/*
+ - name: Run Unit Tests with JUnit XML output
+ run: uv run pytest tests/* --cov=nonebot_plugin_value --cov-report=term-missing --cov-report=xml --junitxml=test-results.xml -v
+
+ - name: Publish Test Report
+ uses: dorny/test-reporter@v1
+ if: success() || failure()
+ with:
+ name: Python Unit Tests
+ path: test-results.xml
+ reporter: java-junit
+ fail-on-error: false
+
- name: Build package
run: uv build # 生成构建产物到dist目录
diff --git a/.gitignore b/.gitignore
index 855897f..57858fc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -179,4 +179,5 @@ cython_debug/
/data/
/config/
/logs/
-/.ruff_cache/
\ No newline at end of file
+/.ruff_cache/
+test-results.xml
\ No newline at end of file
diff --git a/README.md b/README.md
index 3a10514..20afd22 100644
--- a/README.md
+++ b/README.md
@@ -24,6 +24,8 @@
+> *为 NoneBot2 设计的娱乐系统基础设施。*
+
## 核心特性
`nonebot_plugin_value` 是一个基于 NoneBot2 的通用经济系统插件,提供以下核心功能:
diff --git a/nonebot_plugin_value/api/api_balance.py b/nonebot_plugin_value/api/api_balance.py
index 155b70a..3549c67 100644
--- a/nonebot_plugin_value/api/api_balance.py
+++ b/nonebot_plugin_value/api/api_balance.py
@@ -33,6 +33,7 @@ async def set_frozen_all(account_id: str, frozen: bool) -> None:
data_id=get_uni_id(account_id, currency.id),
)
await _set_frozen_all(account_id, frozen, session)
+ await session.commit()
async def set_frozen(account_id: str, currency_id: str, frozen: bool) -> None:
@@ -49,6 +50,7 @@ async def set_frozen(account_id: str, currency_id: str, frozen: bool) -> None:
data_id=get_uni_id(account_id, currency_id),
)
await _set_frozen(account_id, frozen, currency_id, session)
+ await session.commit()
async def list_accounts(
@@ -65,14 +67,8 @@ async def list_accounts(
if currency_id is None:
currency_id = DEFAULT_CURRENCY_UUID.hex
async with get_session() as session:
- result = [
- UserAccountData(
- id=account.id,
- uni_id=account.uni_id,
- currency_id=account.currency_id,
- balance=account.balance,
- last_updated=account.last_updated,
- )
+ result: list[UserAccountData] = [
+ UserAccountData.model_validate(account, from_attributes=True)
for account in await _list_accounts(session, currency_id)
]
if not no_cache_refresh:
@@ -137,6 +133,7 @@ async def get_or_create_account(
category=CacheCategoryEnum.ACCOUNT,
data=data,
)
+ await session.commit()
return data
@@ -241,7 +238,8 @@ async def del_balance(
"""
if currency_id is None:
currency_id = DEFAULT_CURRENCY_UUID.hex
- data = await _d_balance(user_id, currency_id, amount, source)
+ async with get_session() as session:
+ data = await _d_balance(user_id, currency_id, amount, session, source)
if not data.success:
raise RuntimeError(data.message)
await CacheManager().expire_cache(
diff --git a/nonebot_plugin_value/api/api_currency.py b/nonebot_plugin_value/api/api_currency.py
index dba6f75..88fcf33 100644
--- a/nonebot_plugin_value/api/api_currency.py
+++ b/nonebot_plugin_value/api/api_currency.py
@@ -26,9 +26,10 @@ async def update_currency(currency_data: CurrencyData) -> CurrencyData:
async with get_session() as session:
currency = await _update_currency(currency_data, session)
data = CurrencyData.model_validate(currency, from_attributes=True)
- await CacheManager().update_cache(
+ await session.commit()
+ await CacheManager().expire_cache(
category=CacheCategoryEnum.CURRENCY,
- data=data,
+ data_id=data.id,
)
return data
@@ -42,7 +43,9 @@ async def remove_currency(currency_id: str) -> None:
await CacheManager().expire_cache(
category=CacheCategoryEnum.CURRENCY, data_id=currency_id
)
- await _remove_currency(currency_id)
+ async with get_session() as session:
+ await _remove_currency(currency_id)
+ await session.commit()
async def list_currencies(no_cache_update: bool = False) -> list[CurrencyData]:
@@ -57,6 +60,7 @@ async def list_currencies(no_cache_update: bool = False) -> list[CurrencyData]:
CurrencyData.model_validate(currency, from_attributes=True)
for currency in currencies
]
+ await session.commit()
if not no_cache_update:
for currency in result:
await CacheManager().update_cache(
@@ -84,7 +88,9 @@ async def get_currency(currency_id: str, no_cache: bool = False) -> CurrencyData
currency = await _g_currency(currency_id, session)
if currency is None:
return None
- return CurrencyData.model_validate(currency, from_attributes=True)
+ data = CurrencyData.model_validate(currency, from_attributes=True)
+ await session.commit()
+ return data
async def get_currency_by_kwargs(**kwargs: object) -> CurrencyData | None:
@@ -100,7 +106,9 @@ async def get_currency_by_kwargs(**kwargs: object) -> CurrencyData | None:
currency = await __currency_by_kwargs(**kwargs, session=session)
if currency is None:
return None
- return CurrencyData.model_validate(currency, from_attributes=True)
+ data = CurrencyData.model_validate(currency, from_attributes=True)
+ await session.commit()
+ return data
async def get_default_currency() -> CurrencyData:
@@ -111,7 +119,9 @@ async def get_default_currency() -> CurrencyData:
"""
async with get_session() as session:
currency = await _default_currency(session)
- return CurrencyData.model_validate(currency, from_attributes=True)
+ data = CurrencyData.model_validate(currency, from_attributes=True)
+ await session.commit()
+ return data
async def create_currency(currency_data: CurrencyData) -> None:
@@ -129,6 +139,7 @@ async def create_currency(currency_data: CurrencyData) -> None:
data=currency_data,
)
await _create_currency(currency_data, session)
+ await session.commit()
async def get_or_create_currency(currency_data: CurrencyData) -> CurrencyData:
@@ -147,4 +158,5 @@ async def get_or_create_currency(currency_data: CurrencyData) -> CurrencyData:
category=CacheCategoryEnum.CURRENCY,
data=data,
)
+ await session.commit()
return data
diff --git a/nonebot_plugin_value/api/api_transaction.py b/nonebot_plugin_value/api/api_transaction.py
index 5591a3d..c46b668 100644
--- a/nonebot_plugin_value/api/api_transaction.py
+++ b/nonebot_plugin_value/api/api_transaction.py
@@ -40,6 +40,7 @@ async def get_transaction_history_by_time_range(
TransactionData.model_validate(transaction, from_attributes=True)
for transaction in data
]
+ await session.commit()
return result_list
@@ -57,7 +58,7 @@ async def get_transaction_history(
list[TransactionData]: 包含交易数据的列表
"""
async with get_session() as session:
- return [
+ d = [
TransactionData.model_validate(transaction, from_attributes=True)
for transaction in await _transaction_history(
account_id,
@@ -65,6 +66,8 @@ async def get_transaction_history(
limit,
)
]
+ await session.commit()
+ return d
async def remove_transaction(transaction_id: str) -> bool:
@@ -77,7 +80,9 @@ async def remove_transaction(transaction_id: str) -> bool:
bool: 是否成功删除
"""
async with get_session() as session:
- return await _remove_transaction(
+ d = await _remove_transaction(
transaction_id,
session,
)
+ await session.commit()
+ return d
diff --git a/nonebot_plugin_value/hook/exception.py b/nonebot_plugin_value/hook/exception.py
index cab9043..a3c1929 100644
--- a/nonebot_plugin_value/hook/exception.py
+++ b/nonebot_plugin_value/hook/exception.py
@@ -1,7 +1,7 @@
from typing import Any
-class BaseException(Exception):
+class BasicException(Exception):
"""
Base exception class for this module.
"""
@@ -11,13 +11,13 @@ def __init__(self, message: str = "", data: Any | None = None):
self.data = data
-class CancelAction(BaseException):
+class CancelAction(BasicException):
"""
Exception raised when the user cancels an action.
"""
-class DataUpdate(Exception):
+class DataUpdate(BasicException):
"""
Exception raised when the data updated
"""
diff --git a/nonebot_plugin_value/hook/hooks_manager.py b/nonebot_plugin_value/hook/hooks_manager.py
index 6016da7..6d7359a 100644
--- a/nonebot_plugin_value/hook/hooks_manager.py
+++ b/nonebot_plugin_value/hook/hooks_manager.py
@@ -1,10 +1,11 @@
# 事件预处理/后处理钩子
+import asyncio
from collections.abc import Awaitable, Callable
from nonebot import logger
from .context import TransactionComplete, TransactionContext
-from .exception import CancelAction, DataUpdate
+from .exception import BasicException
from .hooks_type import HooksType
@@ -36,7 +37,7 @@ def register(
self, hook_name: str, hook_func: Callable[..., Awaitable[None]]
) -> None:
"""注册一个Hook"""
- if hook_name not in HooksType:
+ if all(hook_name != hook.value for hook in HooksType.__members__.values()):
raise ValueError(f"Invalid hook name: {hook_name}")
self.__hooks.setdefault(hook_name, []).append(hook_func)
@@ -49,10 +50,14 @@ async def run_hooks(
async def _run_single_hook(hook: Callable[..., Awaitable[None]]) -> None:
try:
await hook(context)
- except CancelAction | DataUpdate:
+ except BasicException:
raise
- except Exception:
- logger.opt(exception=True).error("钩子执行失败")
-
- for hook in hooks:
- await _run_single_hook(hook)
+ except Exception as e:
+ logger.opt(exception=e).error("钩子执行失败")
+
+ rst: list[BaseException | None] = await asyncio.gather(
+ *[_run_single_hook(hook) for hook in hooks], return_exceptions=True
+ ) # 即使没用gather的先前实现,遇到exception中断也会抛出异常,此处只是优化性能。
+ for i in rst:
+ if i is not None:
+ raise i
diff --git a/nonebot_plugin_value/repositories/account.py b/nonebot_plugin_value/repositories/account.py
index d912412..cf96250 100644
--- a/nonebot_plugin_value/repositories/account.py
+++ b/nonebot_plugin_value/repositories/account.py
@@ -24,44 +24,40 @@ def __init__(self, session: AsyncSession):
async def get_or_create_account(
self, user_id: str, currency_id: str
) -> UserAccount:
- async with self.session as session:
- """获取或创建用户账户"""
- try:
- # 获取货币配置
- stmt = select(CurrencyMeta).where(CurrencyMeta.id == currency_id)
- result = await session.execute(stmt)
- currency = result.scalar_one_or_none()
- if currency is None:
- raise CurrencyNotFound(f"Currency {currency_id} not found")
-
- # 检查账户是否存在
- stmt = (
- select(UserAccount)
- .where(UserAccount.uni_id == get_uni_id(user_id, currency_id))
- .with_for_update()
- )
- result = await session.execute(stmt)
- account = result.scalar_one_or_none()
-
- if account is not None:
- session.add(account)
- return account
-
- session.add(currency)
- account = UserAccount(
- uni_id=get_uni_id(user_id, currency_id),
- id=user_id,
- currency_id=currency_id,
- balance=currency.default_balance,
- last_updated=datetime.now(timezone.utc),
- )
- session.add(account)
- await session.commit()
- await session.refresh(account)
- return account
- except Exception:
- await session.rollback()
- raise
+ """获取或创建用户账户"""
+ session = self.session
+ # 获取货币配置
+ stmt = select(CurrencyMeta).where(CurrencyMeta.id == currency_id)
+ result = await session.execute(stmt)
+ currency = result.scalar_one_or_none()
+ if currency is None:
+ raise CurrencyNotFound(f"Currency {currency_id} not found")
+
+ # 检查账户是否存在
+ stmt = (
+ select(UserAccount)
+ .where(UserAccount.uni_id == get_uni_id(user_id, currency_id))
+ .with_for_update()
+ )
+ result = await session.execute(stmt)
+ account = result.scalar_one_or_none()
+
+ if account is not None:
+ session.add(account)
+ return account
+
+ session.add(currency)
+ account = UserAccount(
+ uni_id=get_uni_id(user_id, currency_id),
+ id=user_id,
+ currency_id=currency_id,
+ balance=currency.default_balance,
+ last_updated=datetime.now(timezone.utc),
+ )
+ session.add(account)
+ await session.commit()
+ await session.refresh(account)
+ return account
async def set_account_frozen(
self,
@@ -70,32 +66,32 @@ async def set_account_frozen(
frozen: bool,
) -> None:
"""设置账户冻结状态"""
- async with self.session as session:
- try:
- account = await self.get_or_create_account(account_id, currency_id)
- session.add(account)
- account.frozen = frozen
- except Exception:
- await session.rollback()
- raise
- else:
- await session.commit()
+ session = self.session
+ try:
+ account = await self.get_or_create_account(account_id, currency_id)
+ session.add(account)
+ account.frozen = frozen
+ except Exception:
+ await session.rollback()
+ raise
+ else:
+ await session.commit()
async def set_frozen_all(self, account_id: str, frozen: bool):
- async with self.session as session:
- try:
- result = await session.execute(
- select(UserAccount).where(UserAccount.id == account_id)
- )
- accounts = result.scalars().all()
- session.add_all(accounts)
- for account in accounts:
- account.frozen = frozen
- except Exception as e:
- await session.rollback()
- raise e
- else:
- await session.commit()
+ """冻结账户的所有货币资产
+
+ Args:
+ account_id (str): 账户ID
+ frozen (bool): 是否冻结
+ """
+ session = self.session
+ result = await session.execute(
+ select(UserAccount).where(UserAccount.id == account_id)
+ )
+ accounts = result.scalars().all()
+ session.add_all(accounts)
+ for account in accounts:
+ account.frozen = frozen
async def is_account_frozen(
self,
@@ -103,8 +99,7 @@ async def is_account_frozen(
currency_id: str,
) -> bool:
"""判断账户是否冻结"""
- async with self.session:
- return (await self.get_or_create_account(account_id, currency_id)).frozen
+ return (await self.get_or_create_account(account_id, currency_id)).frozen
async def get_balance(self, account_id: str, currency_id: str) -> float | None:
"""获取账户余额"""
@@ -115,92 +110,79 @@ async def get_balance(self, account_id: str, currency_id: str) -> float | None:
async def update_balance(
self, account_id: str, amount: float, currency_id: str
) -> tuple[float, float]:
- async with self.session as session:
- """更新余额"""
- try:
- # 获取账户
- account = (
- await session.execute(
- select(UserAccount)
- .where(
- UserAccount.uni_id == get_uni_id(account_id, currency_id)
- )
- .with_for_update()
- )
- ).scalar_one_or_none()
-
- if account is None:
- raise AccountNotFound("Account not found")
- session.add(account)
-
- if account.frozen:
- raise AccountFrozen(
- f"Account {account_id} on currency {currency_id} is frozen"
- )
-
- # 获取货币规则
- currency = await session.get(CurrencyMeta, account.currency_id)
- session.add(currency)
-
- # 负余额检查
- if amount < 0 and not getattr(currency, "allow_negative", False):
- raise TransactionException("Insufficient funds")
-
- # 记录原始余额
- old_balance = account.balance
-
- # 更新余额
- account.balance = amount
- await session.commit()
-
- return old_balance, amount
- except Exception:
- await session.rollback()
- raise
+ """更新余额"""
+ session = self.session
+ # 获取账户
+ account = (
+ await session.execute(
+ select(UserAccount)
+ .where(UserAccount.uni_id == get_uni_id(account_id, currency_id))
+ .with_for_update()
+ )
+ ).scalar_one_or_none()
+
+ if account is None:
+ raise AccountNotFound("Account not found")
+ session.add(account)
+
+ if account.frozen:
+ raise AccountFrozen(
+ f"Account {account_id} on currency {currency_id} is frozen"
+ )
+
+ # 获取货币规则
+ currency = await session.get(CurrencyMeta, account.currency_id)
+ session.add(currency)
+
+ # 负余额检查
+ if amount < 0 and not getattr(currency, "allow_negative", False):
+ raise TransactionException("Insufficient funds")
+
+ # 记录原始余额
+ old_balance = account.balance
+
+ # 更新余额
+ account.balance = amount
+ await session.commit()
+
+ return old_balance, amount
async def list_accounts(
self, currency_id: str | None = None
) -> Sequence[UserAccount]:
"""列出所有账户"""
- async with self.session as session:
- if not currency_id:
- result = await session.execute(select(UserAccount).with_for_update())
- else:
- result = await session.execute(
- select(UserAccount)
- .where(UserAccount.currency_id == currency_id)
- .with_for_update()
- )
- data = result.scalars().all()
- if len(data) > 0:
- session.add_all(data)
- return data
+ session = self.session
+ if not currency_id:
+ result = await session.execute(select(UserAccount).with_for_update())
+ else:
+ result = await session.execute(
+ select(UserAccount)
+ .where(UserAccount.currency_id == currency_id)
+ .with_for_update()
+ )
+ data = result.scalars().all()
+ if len(data) > 0:
+ session.add_all(data)
+ return data
async def remove_account(self, account_id: str, currency_id: str | None = None):
- """删除账户"""
- async with self.session as session:
- try:
- if not currency_id:
- stmt = (
- select(UserAccount)
- .where(UserAccount.id == account_id)
- .with_for_update()
- )
- else:
- stmt = (
- select(UserAccount)
- .where(
- UserAccount.uni_id == get_uni_id(account_id, currency_id)
- )
- .with_for_update()
- )
- accounts = (await session.execute(stmt)).scalars().all()
- if not accounts:
- raise AccountNotFound("Account not found")
- for account in accounts:
- stmt = delete(UserAccount).where(UserAccount.id == account.id)
- await session.execute(stmt)
- except Exception:
- await session.rollback()
- else:
- await session.commit()
+ session = self.session
+
+ if not currency_id:
+ stmt = (
+ select(UserAccount)
+ .where(UserAccount.id == account_id)
+ .with_for_update()
+ )
+ else:
+ stmt = (
+ select(UserAccount)
+ .where(UserAccount.uni_id == get_uni_id(account_id, currency_id))
+ .with_for_update()
+ )
+ accounts = (await session.execute(stmt)).scalars().all()
+ if not accounts:
+ raise AccountNotFound("Account not found")
+ for account in accounts:
+ stmt = delete(UserAccount).where(UserAccount.id == account.id)
+ await session.execute(stmt)
diff --git a/nonebot_plugin_value/repositories/currency.py b/nonebot_plugin_value/repositories/currency.py
index 6e789d3..9090259 100644
--- a/nonebot_plugin_value/repositories/currency.py
+++ b/nonebot_plugin_value/repositories/currency.py
@@ -20,107 +20,109 @@ def __init__(self, session: AsyncSession):
async def get_currency(self, currency_id: str) -> CurrencyMeta | None:
"""获取货币信息"""
- async with self.session as session:
- result = await self.session.execute(
- select(CurrencyMeta).where(CurrencyMeta.id == currency_id)
- )
- if currency_meta := result.scalar_one_or_none():
- session.add(currency_meta)
- return currency_meta
- return None
+ session = self.session
+ result = await self.session.execute(
+ select(CurrencyMeta).where(CurrencyMeta.id == currency_id)
+ )
+ if currency_meta := result.scalar_one_or_none():
+ session.add(currency_meta)
+ return currency_meta
+ return None
async def get_currency_by_kwargs(self, **kwargs: object) -> CurrencyMeta | None:
"""获取货币信息"""
- async with self.session as session:
- result = await session.execute(
- select(CurrencyMeta).where(
- *(
- getattr(CurrencyMeta, key) == value
- for key, value in kwargs.items()
- if hasattr(CurrencyMeta, key)
- )
+ session = self.session
+ result = await session.execute(
+ select(CurrencyMeta).where(
+ *(
+ getattr(CurrencyMeta, key) == value
+ for key, value in kwargs.items()
+ if hasattr(CurrencyMeta, key)
)
)
- if currency_meta := result.scalar_one_or_none():
- session.add(currency_meta)
- return currency_meta
- return None
+ )
+ if currency_meta := result.scalar_one_or_none():
+ session.add(currency_meta)
+ return currency_meta
+ return None
async def get_or_create_currency(
self, currency_data: CurrencyData
) -> tuple[CurrencyMeta, bool]:
"""获取或创建货币"""
- async with self.session as session:
- stmt = await session.execute(
- select(CurrencyMeta).where(
- CurrencyMeta.id == currency_data.id,
- )
+ session = self.session
+ stmt = await session.execute(
+ select(CurrencyMeta).where(
+ CurrencyMeta.id == currency_data.id,
)
- if (currency := stmt.scalars().first()) is not None:
- session.add(currency)
- return currency, True
- result = await self.createcurrency(currency_data)
- return result, False
+ )
+ if (currency := stmt.scalars().first()) is not None:
+ session.add(currency)
+ return currency, True
+ result = await self.createcurrency(currency_data)
+ return result, False
async def createcurrency(self, currency_data: CurrencyData) -> CurrencyMeta:
- async with self.session as session:
- """创建新货币"""
- currency = CurrencyMeta(**currency_data.model_dump())
- session.add(currency)
- await session.commit()
- await session.refresh(currency)
- return currency
+ """创建新货币"""
+ session = self.session
+ currency = CurrencyMeta(**currency_data.model_dump())
+ session.add(currency)
+ await session.commit()
+ await session.refresh(currency)
+ return currency
async def update_currency(self, currency_data: CurrencyData) -> CurrencyMeta:
"""更新货币信息"""
- async with self.session as session:
- try:
- stmt = (
- update(CurrencyMeta)
- .where(CurrencyMeta.id == currency_data.id)
- .values(**dict(currency_data))
- )
- await session.execute(stmt)
- await session.commit()
- stmt = (
- select(CurrencyMeta)
- .where(CurrencyMeta.id == currency_data.id)
- .with_for_update()
- )
- result = await session.execute(stmt)
- currency_meta = result.scalar_one()
- session.add(currency_meta)
- return currency_meta
- except Exception:
- await session.rollback()
- raise
+ session = self.session
+
+ # 明确指定要更新的字段,避免包含不需要的字段(如 id, created_at 等)
+ update_values = {
+ "display_name": currency_data.display_name,
+ "symbol": currency_data.symbol,
+ "default_balance": currency_data.default_balance,
+ "allow_negative": currency_data.allow_negative,
+ }
+
+ stmt = (
+ update(CurrencyMeta)
+ .where(CurrencyMeta.id == currency_data.id)
+ .values(**update_values)
+ )
+ await session.execute(stmt)
+ await session.flush() # 先 flush 确保更新生效
+
+ # 重新查询获取最新数据
+ stmt = (
+ select(CurrencyMeta)
+ .where(CurrencyMeta.id == currency_data.id)
+ .with_for_update()
+ )
+ result = await session.execute(stmt)
+ currency_meta = result.scalar_one()
+ session.add(currency_meta)
+ return currency_meta
async def list_currencies(self) -> Sequence[CurrencyMeta]:
"""列出所有货币"""
- async with self.session as session:
- result = await self.session.execute(select(CurrencyMeta))
- data = result.scalars().all()
- session.add_all(data)
- return data
+ session = self.session
+ result = await self.session.execute(select(CurrencyMeta))
+ data = result.scalars().all()
+ session.add_all(data)
+ return data
async def remove_currency(self, currency_id: str):
"""删除货币(警告!会同时删除所有关联账户!)"""
- async with self.session as session:
- currency = (
- await session.execute(
- select(CurrencyMeta)
- .where(CurrencyMeta.id == currency_id)
- .with_for_update()
- )
- ).scalar()
- if currency is None:
- raise CurrencyNotFound(f"Currency {currency_id} not found")
- try:
- logger.warning(f"Deleting currency {currency_id}")
- stmt = delete(CurrencyMeta).where(CurrencyMeta.id == currency_id)
- await session.execute(stmt)
- except Exception:
- await session.rollback()
- raise
- else:
- await session.commit()
+ session = self.session
+ currency = (
+ await session.execute(
+ select(CurrencyMeta)
+ .where(CurrencyMeta.id == currency_id)
+ .with_for_update()
+ )
+ ).scalar()
+ if currency is None:
+ raise CurrencyNotFound(f"Currency {currency_id} not found")
+
+ logger.warning(f"Deleting currency {currency_id}")
+ stmt = delete(CurrencyMeta).where(CurrencyMeta.id == currency_id)
+ await session.execute(stmt)
diff --git a/nonebot_plugin_value/repositories/transaction.py b/nonebot_plugin_value/repositories/transaction.py
index ad9e525..99601ec 100644
--- a/nonebot_plugin_value/repositories/transaction.py
+++ b/nonebot_plugin_value/repositories/transaction.py
@@ -28,27 +28,27 @@ async def create_transaction(
balance_after: float,
timestamp: datetime | None = None,
) -> Transaction:
- async with self.session as session:
- """创建交易记录"""
- if timestamp is None:
- timestamp = datetime.now(timezone.utc)
- uuid = uuid1().hex
- transaction_data = Transaction(
- id=uuid,
- account_id=account_id,
- currency_id=currency_id,
- amount=amount,
- action=action,
- source=source,
- balance_before=balance_before,
- balance_after=balance_after,
- timestamp=timestamp,
- )
- session.add(transaction_data)
- await session.commit()
- await session.refresh(transaction_data)
- session.add(transaction_data)
- return transaction_data
+ """创建交易记录"""
+ session = self.session
+ if timestamp is None:
+ timestamp = datetime.now(timezone.utc)
+ uuid = uuid1().hex
+ transaction_data = Transaction(
+ id=uuid,
+ account_id=account_id,
+ currency_id=currency_id,
+ amount=amount,
+ action=action,
+ source=source,
+ balance_before=balance_before,
+ balance_after=balance_after,
+ timestamp=timestamp,
+ )
+ session.add(transaction_data)
+ await session.commit()
+ await session.refresh(transaction_data)
+ session.add(transaction_data)
+ return transaction_data
async def get_transaction_history(
self, account_id: str, limit: int = 100
@@ -72,38 +72,32 @@ async def get_transaction_history_by_time_range(
limit: int = 100,
) -> Sequence[Transaction]:
"""获取账户交易历史"""
- async with self.session as session:
- result = await session.execute(
- select(Transaction)
- .where(
- Transaction.account_id == account_id,
- Transaction.timestamp >= start_time,
- Transaction.timestamp <= end_time,
- )
- .order_by(Transaction.timestamp.desc())
- .limit(limit)
+ session = self.session
+ result = await session.execute(
+ select(Transaction)
+ .where(
+ Transaction.account_id == account_id,
+ Transaction.timestamp >= start_time,
+ Transaction.timestamp <= end_time,
)
- data = result.scalars().all()
- session.add_all(data)
+ .order_by(Transaction.timestamp.desc())
+ .limit(limit)
+ )
+ data = result.scalars().all()
+ session.add_all(data)
return data
async def remove_transaction(self, transaction_id: str) -> None:
"""删除交易记录"""
- async with self.session as session:
- try:
- transaction = (
- await session.execute(
- select(Transaction)
- .where(Transaction.id == transaction_id)
- .with_for_update()
- )
- ).scalar()
- if not transaction:
- raise TransactionNotFound("Transaction not found")
- stmt = delete(Transaction).where(Transaction.id == transaction_id)
- await session.execute(stmt)
- except Exception:
- await session.rollback()
- raise
- else:
- await session.commit()
+ session = self.session
+ transaction = (
+ await session.execute(
+ select(Transaction)
+ .where(Transaction.id == transaction_id)
+ .with_for_update()
+ )
+ ).scalar()
+ if not transaction:
+ raise TransactionNotFound("Transaction not found")
+ stmt = delete(Transaction).where(Transaction.id == transaction_id)
+ await session.execute(stmt)
diff --git a/nonebot_plugin_value/services/balance.py b/nonebot_plugin_value/services/balance.py
index 09ff392..9921e06 100644
--- a/nonebot_plugin_value/services/balance.py
+++ b/nonebot_plugin_value/services/balance.py
@@ -1,3 +1,5 @@
+import asyncio
+import contextlib
from datetime import datetime, timezone
from nonebot import logger
@@ -28,13 +30,15 @@ async def set_frozen(
currency_id (str | None, optional): 货币ID. Defaults to None.
session (AsyncSession | None, optional): 异步Session. Defaults to None.
"""
- if session is None:
- session = get_session()
- async with session:
+ arg_session = session
+ session = session or get_session()
+ async with session if arg_session is None else contextlib.nullcontext():
repo = AccountRepository(session)
await repo.set_account_frozen(
account_id, currency_id or DEFAULT_CURRENCY_UUID.hex, frozen
)
+ if not arg_session:
+ await session.commit()
async def set_frozen_all(
@@ -49,10 +53,12 @@ async def set_frozen_all(
frozen (bool): 是否冻结
session (AsyncSession | None, optional): 异步Session. Defaults to None.
"""
- if session is None:
- session = get_session()
- async with session:
+ arg_session = session
+ session = session or get_session()
+ async with session if arg_session is None else contextlib.nullcontext():
await AccountRepository(session).set_frozen_all(account_id, frozen)
+ if not arg_session:
+ await session.commit()
async def del_account(
@@ -67,11 +73,13 @@ async def del_account(
session (AsyncSession | None, optional): 异步会话. Defaults to None.
user_id (str): 用户ID
"""
- if session is None:
- session = get_session()
- async with session:
+ arg_session = session
+ session = session or get_session()
+ async with session if arg_session is None else contextlib.nullcontext():
try:
await AccountRepository(session).remove_account(account_id, currency_id)
+ if not arg_session:
+ await session.commit()
return True
except Exception:
if fail_then_throw:
@@ -93,8 +101,7 @@ async def list_accounts(
"""
if currency_id is None:
currency_id = DEFAULT_CURRENCY_UUID.hex
- async with session:
- return await AccountRepository(session).list_accounts()
+ return await AccountRepository(session).list_accounts()
async def get_or_create_account(
@@ -112,10 +119,7 @@ async def get_or_create_account(
Returns:
UserAccount: 用户数据模型
"""
- async with session:
- return await AccountRepository(session).get_or_create_account(
- user_id, currency_id
- )
+ return await AccountRepository(session).get_or_create_account(user_id, currency_id)
async def batch_del_balance(
@@ -123,7 +127,7 @@ async def batch_del_balance(
currency_id: str,
source: str = "batch_update",
session: AsyncSession | None = None,
- return_all_on_fail: bool = False,
+ return_all_on_fail: bool = True,
) -> list[ActionResult]:
"""批量减少账户余额
@@ -137,16 +141,22 @@ async def batch_del_balance(
Returns:
list[ActionResult]: 操作结果列表
"""
- if session is None:
- session = get_session()
+ arg_session = session
+ session = session or get_session()
result_list: list[ActionResult] = []
- async with session:
- for uid, amount in updates:
- data: ActionResult = await del_balance(
- uid, currency_id, amount, source, session
- )
- result_list.append(data)
+ async def task_inner(uid: str, amount: float):
+ nonlocal result_list
+ data: ActionResult = await del_balance(
+ uid, currency_id, amount, source=source, session=session
+ )
+ result_list.append(data)
+
+ async with session.begin() if arg_session is None else contextlib.nullcontext():
+ await asyncio.gather(
+ *[task_inner(uid, amount) for uid, amount in updates],
+ return_exceptions=True,
+ )
if not all(r.success for r in result_list):
return [] if not return_all_on_fail else result_list
return result_list
@@ -156,8 +166,8 @@ async def del_balance(
user_id: str,
currency_id: str,
amount: float,
+ session: AsyncSession,
source: str = "",
- session: AsyncSession | None = None,
) -> ActionResult:
"""异步减少余额
@@ -173,62 +183,60 @@ async def del_balance(
"""
if amount <= 0:
return ActionResult(success=False, message="金额必须大于0")
- if session is None:
- session = get_session()
- async with session:
- account_repo = AccountRepository(session)
- tx_repo = TransactionRepository(session)
- account = await account_repo.get_or_create_account(user_id, currency_id)
- session.add(account)
- balance_before = account.balance
- account_id = account.id
- try:
- await HooksManager().run_hooks(
- HooksType.PRE.value,
- TransactionContext(
- user_id=user_id,
- currency=currency_id,
- amount=amount,
- action_type=Method.WITHDRAW.value,
- ),
- )
- except DataUpdate as du:
- amount = du.amount
- except CancelAction as e:
- logger.warning(f"取消了交易:{e.message}")
- return TransferResult(
- success=True,
- message=f"取消了交易:{e.message}",
- )
- balance_after = balance_before - amount
- await account_repo.update_balance(
- account_id, # 使用提前获取的account_id
- balance_after,
- currency_id,
+ account_repo = AccountRepository(session)
+ tx_repo = TransactionRepository(session)
+
+ account = await account_repo.get_or_create_account(user_id, currency_id)
+ session.add(account)
+ balance_before = account.balance
+ account_id = account.id
+ try:
+ await HooksManager().run_hooks(
+ HooksType.PRE.value,
+ TransactionContext(
+ user_id=user_id,
+ currency=currency_id,
+ amount=amount,
+ action_type=Method.WITHDRAW.value,
+ ),
)
- await tx_repo.create_transaction(
- account_id, # 使用提前获取的account_id
- currency_id,
- amount,
- Method.TRANSFER_OUT.value,
- source,
- balance_before,
- balance_after,
+ except DataUpdate as du:
+ amount = du.amount
+ except CancelAction as e:
+ logger.warning(f"取消了交易:{e.message}")
+ return TransferResult(
+ success=True,
+ message=f"取消了交易:{e.message}",
)
- try:
- await HooksManager().run_hooks(
- HooksType.POST.value,
- TransactionComplete(
- message="交易完成",
- source_balance=balance_before,
- new_balance=balance_after,
- timestamp=datetime.now().timestamp(),
- user_id=user_id,
- ),
- )
- finally:
- return ActionResult(success=True, message="操作成功")
+ balance_after = balance_before - amount
+ await account_repo.update_balance(
+ account_id, # 使用提前获取的account_id
+ balance_after,
+ currency_id,
+ )
+ await tx_repo.create_transaction(
+ account_id, # 使用提前获取的account_id
+ currency_id,
+ amount,
+ Method.TRANSFER_OUT.value,
+ source,
+ balance_before,
+ balance_after,
+ )
+ try:
+ await HooksManager().run_hooks(
+ HooksType.POST.value,
+ TransactionComplete(
+ message="交易完成",
+ source_balance=balance_before,
+ new_balance=balance_after,
+ timestamp=datetime.now().timestamp(),
+ user_id=user_id,
+ ),
+ )
+ finally:
+ return ActionResult(success=True, message="操作成功")
async def batch_add_balance(
@@ -249,10 +257,10 @@ async def batch_add_balance(
Returns:
list[ActionResult]: 返回的数据(与列表顺序一致,如果任意一个失败则返回空列表)
"""
- if session is None:
- session = get_session()
result_list: list[ActionResult] = []
- async with session:
+ arg_session = session
+ session = session or get_session()
+ async with session if arg_session is None else contextlib.nullcontext():
for uid, amount in updates:
data: ActionResult = await add_balance(
uid, currency_id, amount, source, session
@@ -282,8 +290,8 @@ async def add_balance(
Returns:
ActionResult: 是否成功("success"),消息说明("message")
"""
- session = get_session() if arg_session is None else arg_session
- async with session:
+ session = arg_session or get_session()
+ async with session if arg_session is None else contextlib.nullcontext():
if amount <= 0:
return ActionResult(
success=False,
@@ -327,6 +335,8 @@ async def add_balance(
balance_after,
currency_id,
)
+ if not arg_session:
+ await session.commit()
try:
await HooksManager().run_hooks(
HooksType.POST.value,
@@ -367,13 +377,13 @@ async def transfer_funds(
TransferResult: 如果成功则包含"from_balance"(源账户现在的balance),"to_balance"(目标账户现在的balance)字段
"""
- session = get_session() if arg_session is None else arg_session
if amount <= 0:
return TransferResult(
message="金额必须大于0",
success=False,
)
- async with session:
+ session = arg_session or get_session()
+ async with session if arg_session is None else contextlib.nullcontext():
account_repo = AccountRepository(session)
tx_repo = TransactionRepository(session)
diff --git a/nonebot_plugin_value/services/currency.py b/nonebot_plugin_value/services/currency.py
index d242e6a..acc566d 100644
--- a/nonebot_plugin_value/services/currency.py
+++ b/nonebot_plugin_value/services/currency.py
@@ -1,3 +1,4 @@
+import contextlib
from collections.abc import Sequence
from nonebot_plugin_orm import AsyncSession, get_session
@@ -15,43 +16,50 @@ async def update_currency(
Args:
currency_data (CurrencyData): 货币元信息
- session (AsyncSession): 异步Session. Defaults to None.
+ session (AsyncSession): 异步Session. .
Returns:
CurrencyMeta: 货币元信息模型
"""
- async with session:
- return await CurrencyRepository(session).update_currency(currency_data)
+ return await CurrencyRepository(session).update_currency(currency_data)
-async def remove_currency(currency_id: str) -> None:
+async def remove_currency(
+ currency_id: str, session: AsyncSession | None = None
+) -> None:
"""删除一个货币(警告!会移除关联账户!)
Args:
currency_id (str): 货币ID
- session (AsyncSession ): 异步Session.
+ session (AsyncSession | None, optional): 异步Session. Defaults to None.
"""
- session = get_session()
- async with session:
+ arg_session = session
+ session = session or get_session()
+ async with session if arg_session is None else contextlib.nullcontext():
await CurrencyRepository(session).remove_currency(currency_id)
+ if not arg_session:
+ await session.commit()
-async def list_currencies(session: AsyncSession) -> Sequence[CurrencyMeta]:
+async def list_currencies(
+ session: AsyncSession,
+) -> Sequence[CurrencyMeta]:
"""获取已存在的货币
Args:
- session (AsyncSession): 异步Session
+ session (AsyncSession | None, optional): 异步Session. Defaults to None.
Returns:
Sequence[CurrencyMeta]: 返回货币列表
"""
- async with session:
- data = await CurrencyRepository(session).list_currencies()
- return data
+ return await CurrencyRepository(session).list_currencies()
-async def get_currency(currency_id: str, session: AsyncSession) -> CurrencyMeta | None:
+async def get_currency(
+ currency_id: str,
+ session: AsyncSession,
+) -> CurrencyMeta | None:
"""获取一个货币的元信息
Args:
@@ -61,9 +69,8 @@ async def get_currency(currency_id: str, session: AsyncSession) -> CurrencyMeta
Returns:
CurrencyMeta | None: 货币元数据(不存在为None)
"""
- async with session:
- metadata = await CurrencyRepository(session).get_currency(currency_id)
- return metadata
+
+ return await CurrencyRepository(session).get_currency(currency_id)
async def get_currency_by_kwargs(
@@ -79,11 +86,13 @@ async def get_currency_by_kwargs(
Returns:
CurrencyMeta | None: 货币元数据(不存在为None)
"""
- async with session:
- return await CurrencyRepository(session).get_currency_by_kwargs(**kwargs)
+
+ return await CurrencyRepository(session).get_currency_by_kwargs(**kwargs)
-async def create_currency(currency_data: CurrencyData, session: AsyncSession) -> None:
+async def create_currency(
+ currency_data: CurrencyData, session: AsyncSession | None = None
+) -> None:
"""创建货币
Args:
@@ -93,7 +102,12 @@ async def create_currency(currency_data: CurrencyData, session: AsyncSession) ->
Returns:
CurrencyMeta: 创建的货币元数据
"""
- await CurrencyRepository(session).createcurrency(currency_data)
+ arg_session = session
+ session = session or get_session()
+ async with session if arg_session is None else contextlib.nullcontext():
+ await CurrencyRepository(session).createcurrency(currency_data)
+ if not arg_session:
+ await session.commit()
async def get_or_create_currency(
@@ -110,9 +124,8 @@ async def get_or_create_currency(
tuple[CurrencyMeta, bool] 元数据和是否创建
"""
- async with session:
- repo = CurrencyRepository(session)
- return await repo.get_or_create_currency(currency_data)
+ repo = CurrencyRepository(session)
+ return await repo.get_or_create_currency(currency_data)
async def get_default_currency(session: AsyncSession) -> CurrencyMeta:
@@ -124,9 +137,9 @@ async def get_default_currency(session: AsyncSession) -> CurrencyMeta:
Returns:
CurrencyMeta: 货币元数据
"""
- async with session:
- return (
- await get_or_create_currency(
- CurrencyData(id=DEFAULT_CURRENCY_UUID.hex), session
- )
- )[0]
+
+ return (
+ await get_or_create_currency(
+ CurrencyData(id=DEFAULT_CURRENCY_UUID.hex), session
+ )
+ )[0]
diff --git a/nonebot_plugin_value/services/transaction.py b/nonebot_plugin_value/services/transaction.py
index c191c2b..32cfeee 100644
--- a/nonebot_plugin_value/services/transaction.py
+++ b/nonebot_plugin_value/services/transaction.py
@@ -1,6 +1,7 @@
+import contextlib
from datetime import datetime
-from nonebot_plugin_orm import AsyncSession
+from nonebot_plugin_orm import AsyncSession, get_session
from ..repository import TransactionRepository
@@ -19,7 +20,7 @@ async def get_transaction_history_by_time_range(
start_time (datetime): 起始时间
end_time (datetime): 结束时间
limit (int, optional): 条数限制. Defaults to 100.
- session (AsyncSession): 会话.
+ session (AsyncSession | None, optional): 会话. Defaults to None.
Returns:
Sequence[Transaction]: 记录
@@ -54,7 +55,7 @@ async def get_transaction_history(
async def remove_transaction(
transaction_id: str,
- session: AsyncSession,
+ session: AsyncSession | None = None,
fail_then_throw: bool = False,
) -> bool:
"""删除交易记录
@@ -67,9 +68,13 @@ async def remove_transaction(
Returns:
bool: 是否成功
"""
- async with session:
+ arg_session = session
+ session = session or get_session()
+ async with session if arg_session is None else contextlib.nullcontext():
try:
await TransactionRepository(session).remove_transaction(transaction_id)
+ if not arg_session:
+ await session.commit()
return True
except Exception:
if fail_then_throw:
diff --git a/pyproject.toml b/pyproject.toml
index ae2651d..5046bed 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "nonebot-plugin-value"
-version = "0.1.7"
+version = "1.0.0"
description = "Economy API for NoneBot2"
readme = "README.md"
requires-python = ">=3.10, <4.0.0"
@@ -28,6 +28,7 @@ dev-dependencies = [
"nonebot2[fastapi]>=2.4.2",
"nonebug>=0.4.3",
"pytest-asyncio>=1.1.0",
+ "pytest-cov>=7.0.0",
]
[tool.uv.pip]
@@ -73,3 +74,31 @@ asyncio_default_fixture_loop_scope = "session"
[tool.nonebot]
plugins = ["nonebot_plugin_value"]
+
+# Coverage Configuration
+[tool.coverage.run]
+source = ["nonebot_plugin_value"]
+omit = [
+ "*/migrations/*",
+ "*/tests/*",
+ "*/__pycache__/*",
+]
+
+[tool.coverage.report]
+exclude_lines = [
+ "pragma: no cover",
+ "def __repr__",
+ "raise AssertionError",
+ "raise NotImplementedError",
+ "if __name__ == .__main__.:",
+ "if TYPE_CHECKING:",
+ "@abstractmethod",
+]
+ignore_errors = true
+show_missing = true
+
+[tool.coverage.html]
+directory = "htmlcov"
+
+[tool.coverage.xml]
+output = "coverage.xml"
diff --git a/run_test.sh b/run_test.sh
new file mode 100755
index 0000000..7f80957
--- /dev/null
+++ b/run_test.sh
@@ -0,0 +1,3 @@
+rm data.sqlite3
+nb orm upgrade
+uv run pytest tests/* --cov=nonebot_plugin_value --cov-report=term-missing --cov-report=xml --junitxml=test-results.xml -v
diff --git a/tests/balance_transfer.py b/tests/balance_transfer.py
new file mode 100644
index 0000000..8c25fb6
--- /dev/null
+++ b/tests/balance_transfer.py
@@ -0,0 +1,46 @@
+import pytest
+from nonebug import App # type:ignore
+
+
+@pytest.mark.asyncio
+async def test_transfer(app: App):
+ from nonebot_plugin_value.api.api_balance import (
+ add_balance,
+ get_or_create_account,
+ transfer_funds,
+ )
+ from nonebot_plugin_value.uuid_lib import to_uuid
+
+ u1 = to_uuid("u1")
+ u2 = to_uuid("u2")
+
+ # 创建账户
+ account1 = await get_or_create_account(u1)
+ account2 = await get_or_create_account(u2)
+ assert account1.balance == 0.0, (
+ f"账户 u1 初始余额应为 0.0,实际为 {account1.balance}"
+ )
+ assert account2.balance == 0.0, (
+ f"账户 u2 初始余额应为 0.0,实际为 {account2.balance}"
+ )
+
+ # 添加余额
+ await add_balance(u1, 100)
+ account1_after_add = await get_or_create_account(u1)
+ assert account1_after_add.balance == 100.0, (
+ f"充值后 u1 余额应为 100.0,实际为 {account1_after_add.balance}"
+ )
+
+ # 转账
+ await transfer_funds(u1, u2, 50)
+
+ # 验证转账后的余额
+ account1_after_transfer = await get_or_create_account(u1)
+ account2_after_transfer = await get_or_create_account(u2)
+
+ assert account1_after_transfer.balance == 50.0, (
+ f"转账后 u1 余额应为 50.0,实际为 {account1_after_transfer.balance}"
+ )
+ assert account2_after_transfer.balance == 50.0, (
+ f"转账后 u2 余额应为 50.0,实际为 {account2_after_transfer.balance}"
+ )
diff --git a/tests/batch_run.py b/tests/batch_run.py
new file mode 100644
index 0000000..ad0f650
--- /dev/null
+++ b/tests/batch_run.py
@@ -0,0 +1,99 @@
+import pytest
+from nonebug import App # type:ignore
+
+
+@pytest.mark.asyncio
+async def test_batch_operations(app: App):
+ from nonebot_plugin_value.api.api_balance import (
+ batch_add_balance,
+ batch_del_balance,
+ del_account,
+ get_or_create_account,
+ list_accounts,
+ )
+ from nonebot_plugin_value.api.api_currency import (
+ CurrencyData,
+ get_or_create_currency,
+ list_currencies,
+ )
+ from nonebot_plugin_value.uuid_lib import to_uuid
+
+ ac_uid: list[str] = [to_uuid(f"account{i}") for i in range(10)]
+ cu_id = to_uuid("currency")
+ cu_data = CurrencyData(id=cu_id, display_name="currency", symbol="C")
+ await get_or_create_currency(cu_data)
+ for uid in ac_uid:
+ await get_or_create_account(uid)
+ await get_or_create_account(uid, cu_id)
+
+ # 批量添加余额(默认货币)
+ await batch_add_balance(
+ [(uid, 1) for uid in ac_uid],
+ )
+
+ # 验证默认货币余额是否都成功增加了 1
+ for uid in ac_uid:
+ account = await get_or_create_account(uid)
+ assert account.balance == 1.0, (
+ f"账户 {uid} 的默认货币余额应为 1.0,实际为 {account.balance}"
+ )
+
+ # 批量添加余额(指定货币)
+ await batch_add_balance(
+ [(uid, 1) for uid in ac_uid],
+ currency_id=cu_id,
+ )
+
+ # 验证指定货币余额是否都成功增加了 1
+ for uid in ac_uid:
+ account = await get_or_create_account(uid, cu_id)
+ assert account.balance == 1.0, (
+ f"账户 {uid} 的指定货币余额应为 1.0,实际为 {account.balance}"
+ )
+
+ # 再次批量添加以验证累加
+ await batch_add_balance(
+ [(uid, 2) for uid in ac_uid],
+ )
+
+ # 验证默认货币余额是否都成功累加到 3.0 (1 + 2)
+ for uid in ac_uid:
+ account = await get_or_create_account(uid)
+ assert account.balance == 3.0, (
+ f"账户 {uid} 的默认货币余额应为 3.0,实际为 {account.balance}"
+ )
+
+ # 批量减少余额
+ await batch_del_balance(
+ [(uid, 1) for uid in ac_uid],
+ )
+
+ # 验证默认货币余额是否都成功减少到 2.0 (3 - 1)
+ for uid in ac_uid:
+ account = await get_or_create_account(uid)
+ assert account.balance == 2.0, (
+ f"账户 {uid} 的默认货币余额应为 2.0,实际为 {account.balance}"
+ )
+
+ # 批量减少余额(指定货币)
+ await batch_del_balance(
+ [(uid, 1) for uid in ac_uid],
+ currency_id=cu_id,
+ )
+
+ # 验证指定货币余额是否都成功减少到 0.0 (1 - 1)
+ for uid in ac_uid:
+ account = await get_or_create_account(uid, cu_id)
+ assert account.balance == 0.0, (
+ f"账户 {uid} 的指定货币余额应为 0.0,实际为 {account.balance}"
+ )
+
+ # 测试列表功能
+ await list_accounts()
+
+ # 删除所有账户并验证删除成功
+ delete_results = [await del_account(uid) for uid in ac_uid]
+ assert all(delete_results), "所有账户应该被成功删除"
+
+ # 验证货币列表功能
+ await list_currencies()
diff --git a/tests/value_tests/cache_test.py b/tests/cache_test.py
similarity index 59%
rename from tests/value_tests/cache_test.py
rename to tests/cache_test.py
index bc705ff..371d051 100644
--- a/tests/value_tests/cache_test.py
+++ b/tests/cache_test.py
@@ -14,6 +14,7 @@ async def test_cache(app: App):
TEST_TRANSACTION = TransactionData(id="1", source="test", amount=1, currency_id="1")
cache_manager = CacheManager()
+ # 测试缓存更新
await cache_manager.update_cache(
category=CacheCategoryEnum.CURRENCY,
data=TEST_CURRENCY,
@@ -26,33 +27,45 @@ async def test_cache(app: App):
category=CacheCategoryEnum.TRANSACTION,
data=TEST_TRANSACTION,
)
- assert (
- await (await cache_manager.get_cache(CacheCategoryEnum.CURRENCY)).get(
- data_id="1"
- )
- == TEST_CURRENCY
- )
- assert (
- await (await cache_manager.get_cache(CacheCategoryEnum.ACCOUNT)).get(
- data_id="1"
- )
- ) == TEST_USER
- assert (
- await (await cache_manager.get_cache(CacheCategoryEnum.TRANSACTION)).get(
- data_id="1"
- )
- == TEST_TRANSACTION
+
+ # 验证缓存命中
+ currency_cache = await (
+ await cache_manager.get_cache(CacheCategoryEnum.CURRENCY)
+ ).get(data_id="1")
+ assert currency_cache == TEST_CURRENCY, "货币缓存应该与测试数据一致"
+
+ user_cache = await (await cache_manager.get_cache(CacheCategoryEnum.ACCOUNT)).get(
+ data_id="1"
)
+ assert user_cache == TEST_USER, "用户缓存应该与测试数据一致"
+
+ transaction_cache = await (
+ await cache_manager.get_cache(CacheCategoryEnum.TRANSACTION)
+ ).get(data_id="1")
+ assert transaction_cache == TEST_TRANSACTION, "交易缓存应该与测试数据一致"
+
+ # 测试缓存过期
await cache_manager.expire_cache(category=CacheCategoryEnum.CURRENCY)
assert not await (await cache_manager.get_cache(CacheCategoryEnum.CURRENCY)).get(
data_id="1"
- )
+ ), "货币缓存过期后应该不存在"
+
await cache_manager.expire_cache(category=CacheCategoryEnum.ACCOUNT)
assert not await (await cache_manager.get_cache(CacheCategoryEnum.ACCOUNT)).get(
data_id="1"
- )
+ ), "用户缓存过期后应该不存在"
+
await cache_manager.expire_cache(category=CacheCategoryEnum.TRANSACTION)
assert not await (await cache_manager.get_cache(CacheCategoryEnum.TRANSACTION)).get(
data_id="1"
+ ), "交易缓存过期后应该不存在"
+
+ # 测试清空所有缓存
+ await cache_manager.update_cache(
+ category=CacheCategoryEnum.CURRENCY,
+ data=TEST_CURRENCY,
)
await cache_manager.expire_all_cache()
+ assert not await (await cache_manager.get_cache(CacheCategoryEnum.CURRENCY)).get(
+ data_id="1"
+ ), "清空所有缓存后应该不存在"
diff --git a/tests/conftest.py b/tests/conftest.py
index 8415720..993604a 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -5,7 +5,7 @@
import pytest
from pytest_asyncio import is_async_test
-# 将项目根目录添加到sys.path中
+# 将项目根目录添加到 sys.path 中
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))
diff --git a/tests/create_currency.py b/tests/create_currency.py
new file mode 100644
index 0000000..080beb2
--- /dev/null
+++ b/tests/create_currency.py
@@ -0,0 +1,69 @@
+import pytest
+from nonebug import App # type: ignore
+
+
+@pytest.mark.asyncio
+async def test_new_currency(app: App):
+ from nonebot_plugin_value.api.api_balance import (
+ add_balance,
+ del_account,
+ del_balance,
+ get_or_create_account,
+ )
+ from nonebot_plugin_value.api.api_currency import (
+ CurrencyData,
+ get_currency,
+ get_or_create_currency,
+ remove_currency,
+ )
+ from nonebot_plugin_value.uuid_lib import to_uuid
+
+ c_id = "114514"
+ u_id = to_uuid("114514")
+
+ # 创建货币
+ currency = CurrencyData(id=c_id, display_name="test", symbol="t")
+ created_currency = await get_or_create_currency(currency)
+ assert created_currency.id == c_id, (
+ f"创建的货币 ID 应为 {c_id},实际为 {created_currency.id}"
+ )
+ assert created_currency.display_name == "test", (
+ f"货币名称应为 'test',实际为 {created_currency.display_name}"
+ )
+
+ # 验证货币存在
+ currency_data = await get_currency(c_id)
+ assert currency_data is not None, "货币应该存在"
+ assert currency_data.id == c_id, "获取的货币 ID 应该与创建的一致"
+
+ # 创建账户
+ account = await get_or_create_account(u_id, c_id)
+ assert account.currency_id == c_id, (
+ f"账户货币 ID 应为 {c_id},实际为 {account.currency_id}"
+ )
+ assert account.balance == 0.0, f"账户初始余额应为 0.0,实际为 {account.balance}"
+
+ # 添加余额
+ await add_balance(u_id, 114514, "1919810", c_id)
+ account_after_add = await get_or_create_account(u_id, c_id)
+ assert account_after_add.balance == 114514.0, (
+ f"充值后余额应为 114514.0,实际为 {account_after_add.balance}"
+ )
+
+ # 减少余额
+ await del_balance(u_id, 114514, "1919810", c_id)
+ account_after_del = await get_or_create_account(u_id, c_id)
+ assert account_after_del.balance == 0.0, (
+ f"扣减后余额应为 0.0,实际为 {account_after_del.balance}"
+ )
+
+ # 删除账户
+ delete_result = await del_account(u_id, c_id)
+ assert delete_result, "账户应该被成功删除"
+
+ # 删除货币
+ await remove_currency(c_id)
+
+ # 验证货币不存在
+ currency_after_remove = await get_currency(c_id)
+ assert currency_after_remove is None, "货币删除后应该不存在"
diff --git a/tests/default_account.py b/tests/default_account.py
new file mode 100644
index 0000000..b105d9d
--- /dev/null
+++ b/tests/default_account.py
@@ -0,0 +1,47 @@
+import pytest
+from nonebug import App # type: ignore
+
+
+@pytest.mark.asyncio
+async def test_balance(app: App):
+ from nonebot_plugin_value.api.api_balance import (
+ add_balance,
+ del_account,
+ del_balance,
+ get_or_create_account,
+ )
+ from nonebot_plugin_value.uuid_lib import to_uuid
+
+ account = to_uuid("Test_Example")
+
+ # 创建账户并验证初始状态
+ created_account = await get_or_create_account(account)
+ assert created_account.balance == 0.0, (
+ f"账户初始余额应为 0.0,实际为 {created_account.balance}"
+ )
+
+ # 添加余额
+ await add_balance(
+ account,
+ 100,
+ "add",
+ )
+ account_after_add = await get_or_create_account(account)
+ assert account_after_add.balance == 100.0, (
+ f"充值后余额应为 100.0,实际为 {account_after_add.balance}"
+ )
+
+ # 减少余额
+ await del_balance(
+ account,
+ 100,
+ "del",
+ )
+ account_after_del = await get_or_create_account(account)
+ assert account_after_del.balance == 0.0, (
+ f"扣减后余额应为 0.0,实际为 {account_after_del.balance}"
+ )
+
+ # 删除账户并验证
+ delete_result = await del_account(account)
+ assert delete_result, "账户应该被成功删除"
diff --git a/tests/depends.py b/tests/depends.py
new file mode 100644
index 0000000..0bc1ffa
--- /dev/null
+++ b/tests/depends.py
@@ -0,0 +1,60 @@
+import pytest
+from nonebug import App # type: ignore
+
+
+def make_event():
+ from nonebot.adapters.onebot.v11 import Message, MessageEvent
+ from nonebot.adapters.onebot.v11.event import Sender
+
+ return MessageEvent(
+ time=0,
+ self_id=0,
+ user_id=123456789,
+ post_type="message",
+ message=Message(""),
+ sub_type="friend",
+ message_type="private",
+ message_id=0,
+ raw_message="",
+ font=0,
+ sender=Sender(),
+ original_message=Message(),
+ )
+
+
+@pytest.mark.asyncio
+async def test_depends(app: App):
+ from nonebot_plugin_value.api.api_balance import (
+ del_account,
+ get_or_create_account,
+ )
+ from nonebot_plugin_value.api.depends.factory import DependsSwitch
+ from nonebot_plugin_value.uuid_lib import to_uuid
+
+ uid = to_uuid("123456789")
+
+ # 创建账户并验证初始状态
+ account = await get_or_create_account(uid)
+ assert account.balance == 0.0, f"账户初始余额应为 0.0,实际为 {account.balance}"
+
+ # 使用依赖注入获取执行器
+ executor = await (DependsSwitch().account_executor())(make_event())
+
+ # 添加余额
+ await executor.add_balance(100)
+
+ # 验证余额(通过执行器)
+ balance_via_executor = await executor.get_balance()
+ assert balance_via_executor == 100.0, (
+ f"通过执行器获取的余额应为 100.0,实际为 {balance_via_executor}"
+ )
+
+ # 再次验证余额(通过 API)
+ account_after_add = await get_or_create_account(uid)
+ assert account_after_add.balance == 100.0, (
+ f"通过 API 获取的余额应为 100.0,实际为 {account_after_add.balance}"
+ )
+
+ # 清理账户
+ delete_result = await del_account(uid)
+ assert delete_result, "账户应该被成功删除"
diff --git a/tests/test_api_coverage.py b/tests/test_api_coverage.py
new file mode 100644
index 0000000..58baae9
--- /dev/null
+++ b/tests/test_api_coverage.py
@@ -0,0 +1,155 @@
+import asyncio
+
+import pytest
+from nonebug import App # type:ignore
+
+
+@pytest.mark.asyncio
+async def test_currency_api(app: App):
+ """测试货币 API 的完整功能"""
+ from nonebot_plugin_value.api.api_currency import (
+ create_currency,
+ get_currency,
+ get_currency_by_kwargs,
+ get_default_currency,
+ list_currencies,
+ remove_currency,
+ update_currency,
+ )
+ from nonebot_plugin_value.pyd_models.currency_pyd import CurrencyData
+ from nonebot_plugin_value.uuid_lib import to_uuid
+
+ # 测试获取默认货币
+ default_currency = await get_default_currency()
+ assert default_currency is not None, "应该存在默认货币"
+ assert default_currency.id is not None, "默认货币应该有 ID"
+
+ # 测试创建新货币
+ test_currency_id = to_uuid("test_api_currency")
+ new_currency_data = CurrencyData(
+ id=test_currency_id, display_name="Test API Currency", symbol="TAC"
+ )
+ await create_currency(new_currency_data)
+
+ # 验证创建成功
+ currency = await get_currency(test_currency_id)
+ assert currency is not None, "货币应该被创建"
+ assert currency.display_name == "Test API Currency", "货币名称应该匹配"
+ assert currency.symbol == "TAC", "货币符号应该匹配"
+
+ # 测试通过 kwargs 查询
+ currency_by_kwargs = await get_currency_by_kwargs(display_name="Test API Currency")
+ assert currency_by_kwargs is not None, "应该能通过 kwargs 查询到货币"
+ assert currency_by_kwargs.id == test_currency_id, "查询结果应该匹配"
+
+ # 测试不存在的 kwargs 查询
+ not_found = await get_currency_by_kwargs(display_name="Non Existent Currency")
+ assert not_found is None, "不存在的货币应该返回 None"
+
+ # 测试更新货币
+ updated_currency_data = CurrencyData(
+ id=test_currency_id, display_name="Updated Test Currency", symbol="UTC"
+ )
+ updated_currency = await update_currency(updated_currency_data)
+ assert updated_currency.display_name == "Updated Test Currency", (
+ "货币名称应该被更新"
+ )
+ assert updated_currency.symbol == "UTC", "货币符号应该被更新"
+
+ # 验证更新持久化(强制从数据库读取,不使用缓存)
+ currency_after_update = await get_currency(test_currency_id, no_cache=True)
+ assert currency_after_update is not None, "更新后的货币应该存在"
+ assert currency_after_update.display_name == "Updated Test Currency", (
+ "更新应该持久化"
+ )
+ # 测试获取所有货币
+ currencies = await list_currencies()
+ assert len(currencies) >= 2, "应该至少有两个货币(默认 + 测试)"
+
+ # 测试 no_cache_update 参数
+ currencies_no_cache = await list_currencies(no_cache_update=True)
+ assert len(currencies_no_cache) >= 2, "即使不更新缓存也应该返回数据"
+
+ # 测试缓存命中
+ cached_currency = await get_currency(test_currency_id, no_cache=False)
+ assert cached_currency is not None, "应该能从缓存获取货币"
+
+ # 测试 no_cache 参数(强制不使用缓存)
+ no_cache_currency = await get_currency(test_currency_id, no_cache=True)
+ assert no_cache_currency is not None, "即使不使用缓存也应该能获取货币"
+
+ # 测试删除货币
+ await remove_currency(test_currency_id)
+
+ # 验证删除成功
+ deleted_currency = await get_currency(test_currency_id)
+ assert deleted_currency is None, "货币应该被删除"
+
+ # 测试获取不存在的货币
+ non_existent = await get_currency(to_uuid("non_existent"))
+ assert non_existent is None, "不存在的货币应该返回 None"
+
+
+@pytest.mark.asyncio
+async def test_transaction_api(app: App):
+ """测试交易记录 API"""
+ import time
+
+ from nonebot_plugin_value.api.api_balance import add_balance, get_or_create_account
+ from nonebot_plugin_value.api.api_transaction import (
+ get_transaction_history,
+ get_transaction_history_by_time_range,
+ remove_transaction,
+ )
+ from nonebot_plugin_value.uuid_lib import to_uuid
+
+ test_user_id = to_uuid("test_transaction_api_user")
+
+ # 创建账户
+ await get_or_create_account(test_user_id)
+
+ # 添加多笔余额以创建交易记录
+ for i in range(5):
+ await add_balance(test_user_id, 10.0 * (i + 1), f"test_add_{i}")
+
+ # 等待一小段时间以确保时间戳不同
+ await asyncio.sleep(0.01)
+ timestamp_before_sleep = time.time()
+ await asyncio.sleep(0.01)
+
+ # 获取交易历史(限制数量)
+ transactions_limited = await get_transaction_history(test_user_id, limit=3)
+ assert len(transactions_limited) <= 3, "交易记录数量不应该超过限制"
+
+ # 获取更多交易记录
+ transactions_all = await get_transaction_history(test_user_id, limit=100)
+ assert len(transactions_all) >= 5, "应该至少有 5 条交易记录"
+
+ # 验证交易记录的属性
+ for tx in transactions_all:
+ assert tx.amount > 0, "充值金额应该大于 0"
+ assert tx.account_id == test_user_id, "交易记录应该属于正确的账户"
+ assert tx.source.startswith("test_add_"), "交易来源应该匹配"
+
+ # 测试按时间范围查询
+ transactions_by_time = await get_transaction_history_by_time_range(
+ account_id=test_user_id,
+ start_time=0, # 从 Unix 纪元开始
+ end_time=timestamp_before_sleep + 10, # 到当前时间之后
+ limit=10,
+ )
+ assert len(transactions_by_time) >= 5, "应该能找到至少 5 条交易记录"
+
+ # 测试空的时间范围
+ empty_transactions = await get_transaction_history_by_time_range(
+ account_id=test_user_id,
+ start_time=9999999999, # 未来时间
+ end_time=9999999999,
+ limit=10,
+ )
+ assert len(empty_transactions) == 0, "未来时间范围应该没有交易记录"
+
+ # 测试删除交易记录(可选,如果需要测试的话)
+ if len(transactions_all) > 0:
+ first_tx_id = transactions_all[0].id
+ await remove_transaction(first_tx_id)
diff --git a/tests/test_dump_tools.py b/tests/test_dump_tools.py
new file mode 100644
index 0000000..1e31dc9
--- /dev/null
+++ b/tests/test_dump_tools.py
@@ -0,0 +1,129 @@
+import json
+import tempfile
+from pathlib import Path
+
+import aiofiles
+import pytest
+from nonebug import App # type:ignore
+
+
+@pytest.mark.asyncio
+async def test_dump_and_migrate(app: App):
+ """测试数据导出和导入功能"""
+ from nonebot_plugin_value.api.api_balance import (
+ add_balance,
+ get_or_create_account,
+ )
+ from nonebot_plugin_value.api.api_currency import (
+ CurrencyData,
+ get_or_create_currency,
+ )
+ from nonebot_plugin_value.dump_tools import (
+ dump_data,
+ dump_data_to_json_file,
+ migrate_from_data,
+ migrate_from_json_file,
+ )
+ from nonebot_plugin_value.uuid_lib import to_uuid
+
+ # 创建测试数据
+ test_currency_id = to_uuid("test_currency")
+ test_user_id = to_uuid("test_user")
+
+ # 创建货币
+ currency_data = CurrencyData(
+ id=test_currency_id, display_name="Test Currency", symbol="TC"
+ )
+ await get_or_create_currency(currency_data)
+
+ # 创建账户并添加余额
+ await get_or_create_account(test_user_id, test_currency_id)
+ await add_balance(test_user_id, 100.0, "test_add", test_currency_id)
+
+ # 验证初始状态
+ account_before = await get_or_create_account(test_user_id, test_currency_id)
+ assert account_before.balance == 100.0
+
+ # 测试导出功能
+ exported_data = await dump_data()
+ assert len(exported_data.currencies) >= 1, "应该至少有一个货币"
+ assert len(exported_data.accounts) >= 1, "应该至少有一个账户"
+
+ # 验证导出的数据包含我们的测试数据
+ exported_currency_ids = [c.id for c in exported_data.currencies]
+ assert test_currency_id in exported_currency_ids, "导出的货币应该包含测试货币"
+
+ exported_account_ids = [a.account_data.id for a in exported_data.accounts]
+ assert test_user_id in exported_account_ids, "导出的账户应该包含测试账户"
+
+ # 测试导出到 JSON 文件
+ with tempfile.TemporaryDirectory() as tmpdir:
+ tmp_path = Path(tmpdir)
+ await dump_data_to_json_file(tmp_path)
+
+ # 验证文件存在
+ json_file = tmp_path / "migration.json"
+ assert json_file.exists(), "JSON 文件应该被创建"
+
+ # 验证文件内容(使用异步方式读取)
+ async with aiofiles.open(json_file, encoding="utf-8") as f:
+ file_content = await f.read()
+ file_data = json.loads(file_content)
+ assert "currencies" in file_data, "JSON 应该包含 currencies 字段"
+ assert "accounts" in file_data, "JSON 应该包含 accounts 字段"
+
+ # 测试从数据模型迁移(修改余额)
+ await add_balance(test_user_id, 50.0, "test_add2", test_currency_id)
+ account_after_add = await get_or_create_account(test_user_id, test_currency_id)
+ assert account_after_add.balance == 150.0, "充值后余额应为 150.0"
+
+ # 重新导出当前状态
+ current_data = await dump_data()
+
+ # 修改导出数据中的余额
+ for account in current_data.accounts:
+ if account.account_data.id == test_user_id:
+ account.account_data.balance = 200.0
+ for tx in account.transactions:
+ if tx.amount == 50.0:
+ tx.amount = 100.0 # 修改交易记录
+
+ # 执行迁移
+ await migrate_from_data(current_data)
+
+ # 验证迁移后的余额
+ account_after_migrate = await get_or_create_account(test_user_id, test_currency_id)
+ assert account_after_migrate.balance == 200.0, "迁移后余额应为 200.0"
+
+ # 测试从 JSON 文件迁移
+ with tempfile.TemporaryDirectory() as tmpdir:
+ tmp_path = Path(tmpdir)
+ json_file = tmp_path / "migration.json"
+
+ # 创建新的测试数据
+ test_user2_id = to_uuid("test_user2")
+ await get_or_create_account(test_user2_id, test_currency_id)
+ await add_balance(test_user2_id, 75.0, "test_user2_add", test_currency_id)
+
+ # 导出并修改
+ data_to_export = await dump_data()
+ async with aiofiles.open(json_file, "w", encoding="utf-8") as f:
+ await f.write(data_to_export.model_dump_json(indent=4))
+
+ # 修改余额为 300
+ for account in data_to_export.accounts:
+ if account.account_data.id == test_user2_id:
+ account.account_data.balance = 300.0
+
+ # 写入修改后的数据
+ async with aiofiles.open(json_file, "w", encoding="utf-8") as f:
+ await f.write(data_to_export.model_dump_json(indent=4))
+
+ # 从文件迁移
+ await migrate_from_json_file(json_file)
+
+ # 验证迁移结果
+ account_after_file_migrate = await get_or_create_account(
+ test_user2_id, test_currency_id
+ )
+ assert account_after_file_migrate.balance == 300.0, "从文件迁移后余额应为 300.0"
diff --git a/tests/test_hooks.py b/tests/test_hooks.py
new file mode 100644
index 0000000..1d0d9f2
--- /dev/null
+++ b/tests/test_hooks.py
@@ -0,0 +1,145 @@
+import pytest
+from nonebug import App # type:ignore
+
+
+@pytest.mark.asyncio
+async def test_hooks_manager(app: App):
+ """测试钩子管理器功能"""
+ from nonebot_plugin_value.hook.context import (
+ TransactionComplete,
+ TransactionContext,
+ )
+ from nonebot_plugin_value.hook.exception import CancelAction, DataUpdate
+ from nonebot_plugin_value.hook.hooks_manager import HooksManager
+ from nonebot_plugin_value.hook.hooks_type import HooksType
+
+ # 获取单例实例
+ manager = HooksManager()
+
+ # 测试 Hook 类型枚举
+ assert HooksType.pre() == "vault_pre_transaction"
+ assert HooksType.post() == "vault_post_transaction"
+ assert len(HooksType.methods()) == 2
+ assert "vault_pre_transaction" in HooksType.methods()
+ assert "vault_post_transaction" in HooksType.methods()
+
+ # 清理可能存在的旧钩子
+ setattr(manager, "_HooksManager__hooks", {})
+
+ # 测试注册钩子
+ pre_hook_called = False
+ post_hook_called = False
+
+ async def pre_transaction_hook(context: TransactionContext):
+ nonlocal pre_hook_called
+ pre_hook_called = True
+ assert getattr(context, "user_id") == "test_user"
+ assert getattr(context, "currency") == "test_currency"
+ assert getattr(context, "amount") == 100.0
+ assert getattr(context, "action_type") == "add_balance"
+
+ async def post_transaction_hook(context: TransactionComplete):
+ nonlocal post_hook_called
+ post_hook_called = True
+ assert getattr(context, "message") == "Transaction completed"
+ assert getattr(context, "source_balance") == 0.0
+ assert getattr(context, "new_balance") == 100.0
+ assert getattr(context, "user_id") == "test_user"
+
+ # 使用 register 方法注册
+ manager.register(HooksType.pre(), pre_transaction_hook)
+ manager.register(HooksType.post(), post_transaction_hook)
+
+ # 验证钩子已注册
+ hooks_dict = getattr(manager, "_HooksManager__hooks")
+ assert HooksType.pre() in hooks_dict
+ assert HooksType.post() in hooks_dict
+ assert len(hooks_dict[HooksType.pre()]) == 1
+ assert len(hooks_dict[HooksType.post()]) == 1
+
+ # 测试运行 PRE 钩子
+ pre_context = TransactionContext(
+ user_id="test_user",
+ currency="test_currency",
+ amount=100.0,
+ action_type="add_balance",
+ )
+ await manager.run_hooks(HooksType.pre(), pre_context)
+ assert pre_hook_called, "PRE 钩子应该被调用"
+
+ # 测试运行 POST 钩子
+ post_context = TransactionComplete(
+ message="Transaction completed",
+ source_balance=0.0,
+ new_balance=100.0,
+ user_id="test_user",
+ )
+ await manager.run_hooks(HooksType.post(), post_context)
+ assert post_hook_called, "POST 钩子应该被调用"
+
+ # 测试装饰器方式注册
+ decorator_hook_called = False
+
+ @manager.on_event(HooksType.pre())
+ async def decorator_hook(context: TransactionContext): # type: ignore[reportUnusedFunction]
+ """装饰器方式注册的钩子函数"""
+ nonlocal decorator_hook_called
+ decorator_hook_called = True
+
+ # 验证装饰器注册的钩子存在
+ hooks_dict = getattr(manager, "_HooksManager__hooks")
+ assert len(hooks_dict[HooksType.pre()]) == 2
+
+ # 运行所有 PRE 钩子(包括装饰器注册的)
+ pre_hook_called = False
+ decorator_hook_called = False
+ await manager.run_hooks(HooksType.pre(), pre_context)
+ assert pre_hook_called, "第一个 PRE 钩子应该被调用"
+ assert decorator_hook_called, "装饰器注册的 PRE 钩子应该被调用"
+
+ # 测试无效钩子名称
+ with pytest.raises(ValueError, match="Invalid hook name"):
+ manager.register("invalid_hook_name", pre_transaction_hook)
+
+ # 测试取消操作
+ async def cancel_hook(context: TransactionContext):
+ context.cancel("Test cancel reason")
+
+ setattr(manager, "_HooksManager__hooks", {})
+ manager.register(HooksType.pre(), cancel_hook)
+
+ with pytest.raises(CancelAction) as exc_info:
+ await manager.run_hooks(HooksType.pre(), pre_context)
+
+ assert str(exc_info.value) == "Test cancel reason"
+
+ # 测试数据更新
+ async def update_hook(context: TransactionContext):
+ context.commit_update()
+
+ setattr(manager, "_HooksManager__hooks", {})
+ manager.register(HooksType.pre(), update_hook)
+
+ with pytest.raises(DataUpdate) as exc_info:
+ await manager.run_hooks(HooksType.pre(), pre_context)
+
+ assert getattr(exc_info.value, "amount") == 100.0
+
+ # 测试不存在的钩子(应该直接返回,不执行任何操作)
+ setattr(manager, "_HooksManager__hooks", {})
+ await manager.run_hooks(HooksType.pre(), pre_context) # 不应该抛出异常
+
+ # 测试钩子执行异常处理
+ error_hook_called = False
+
+ async def error_hook(context: TransactionContext):
+ nonlocal error_hook_called
+ error_hook_called = True
+ raise ValueError("Test error in hook")
+
+ setattr(manager, "_HooksManager__hooks", {})
+ manager.register(HooksType.pre(), error_hook)
+
+ # 应该记录错误但不中断执行
+ await manager.run_hooks(HooksType.pre(), pre_context)
+ assert error_hook_called, "错误的钩子也应该被调用"
diff --git a/tests/transaction_histories.py b/tests/transaction_histories.py
new file mode 100644
index 0000000..7f97ee7
--- /dev/null
+++ b/tests/transaction_histories.py
@@ -0,0 +1,47 @@
+import pytest
+from nonebug import App # type: ignore
+
+
+@pytest.mark.asyncio
+async def test_transactions(app: App):
+ from nonebot_plugin_value.api.api_balance import add_balance, get_or_create_account
+ from nonebot_plugin_value.api.api_transaction import get_transaction_history
+ from nonebot_plugin_value.uuid_lib import to_uuid
+
+ account_id = to_uuid("123")
+
+ # 创建账户并验证初始状态
+ account = await get_or_create_account(account_id)
+ assert account.balance == 0.0, f"账户初始余额应为 0.0,实际为 {account.balance}"
+
+ # 多次添加余额
+ for i in range(20):
+ await add_balance(account_id, 1)
+
+ # 验证最终余额
+ account_after_adds = await get_or_create_account(account_id)
+ assert account_after_adds.balance == 20.0, (
+ f"20 次充值后余额应为 20.0,实际为 {account_after_adds.balance}"
+ )
+
+ # 获取交易历史
+ transactions = await get_transaction_history(account.id, 30)
+
+ # 验证交易记录数量和完整性
+ assert len(transactions) >= 20, (
+ f"交易记录数量应该至少 20 条,实际为 {len(transactions)} 条"
+ )
+
+ # 验证每笔交易的金额(应该是 1)
+ for i, transaction in enumerate(transactions[:20]):
+ assert transaction.amount == 1, (
+ f"第 {i + 1} 笔交易金额应为 1,实际为 {transaction.amount}"
+ )
+
+ # 验证交易记录的连续性(可选)
+ if len(transactions) > 1:
+ # 检查交易记录是否按时间倒序排列(最新的在前)
+ for i in range(len(transactions) - 1):
+ assert transactions[i].timestamp >= transactions[i + 1].timestamp, (
+ "交易记录应该按时间倒序排列"
+ )
diff --git a/tests/value_tests/balance_transfer.py b/tests/value_tests/balance_transfer.py
deleted file mode 100644
index d98571b..0000000
--- a/tests/value_tests/balance_transfer.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import pytest
-from nonebug import App # type:ignore
-
-
-@pytest.mark.asyncio
-async def test_transfer(app: App):
- from nonebot_plugin_value.api.api_balance import (
- add_balance,
- get_or_create_account,
- transfer_funds,
- )
- from nonebot_plugin_value.uuid_lib import to_uuid
-
- u1 = to_uuid("u1")
- u2 = to_uuid("u2")
- await get_or_create_account(u1)
- await get_or_create_account(u2)
- await add_balance(u1, 100)
- await transfer_funds(u1, u2, 50)
diff --git a/tests/value_tests/batch_run.py b/tests/value_tests/batch_run.py
deleted file mode 100644
index 10a6918..0000000
--- a/tests/value_tests/batch_run.py
+++ /dev/null
@@ -1,44 +0,0 @@
-import pytest
-from nonebug import App # type:ignore
-
-
-@pytest.mark.asyncio
-async def test_batch_operations(app: App):
- from nonebot_plugin_value.api.api_balance import (
- batch_add_balance,
- batch_del_balance,
- del_account,
- get_or_create_account,
- list_accounts,
- )
- from nonebot_plugin_value.api.api_currency import (
- CurrencyData,
- get_or_create_currency,
- list_currencies,
- )
- from nonebot_plugin_value.uuid_lib import to_uuid
-
- ac_uid: list[str] = [to_uuid(f"account{i}") for i in range(100)]
- cu_id = to_uuid("currency")
- cu_data = CurrencyData(id=cu_id, display_name="currency", symbol="C")
- await get_or_create_currency(cu_data)
- for uid in ac_uid:
- await get_or_create_account(uid)
- await get_or_create_account(uid, cu_id)
- await batch_add_balance(
- [(uid, 1) for uid in ac_uid],
- )
- await batch_add_balance(
- [(uid, 1) for uid in ac_uid],
- currency_id=cu_id,
- )
- await batch_del_balance(
- [(uid, 1) for uid in ac_uid],
- )
- await batch_del_balance(
- [(uid, 1) for uid in ac_uid],
- currency_id=cu_id,
- )
- await list_accounts()
- assert all([await del_account(uid) for uid in ac_uid])
- await list_currencies()
diff --git a/tests/value_tests/create_currency.py b/tests/value_tests/create_currency.py
deleted file mode 100644
index 8adfd9c..0000000
--- a/tests/value_tests/create_currency.py
+++ /dev/null
@@ -1,30 +0,0 @@
-import pytest
-from nonebug import App # type: ignore
-
-
-@pytest.mark.asyncio
-async def test_new_currency(app: App):
- from nonebot_plugin_value.api.api_balance import (
- add_balance,
- del_account,
- del_balance,
- get_or_create_account,
- )
- from nonebot_plugin_value.api.api_currency import (
- CurrencyData,
- get_currency,
- get_or_create_currency,
- remove_currency,
- )
- from nonebot_plugin_value.uuid_lib import to_uuid
-
- c_id = "114514"
- u_id = to_uuid("114514")
- currency = CurrencyData(id=c_id, display_name="test", symbol="t")
- await get_or_create_currency(currency)
- await get_or_create_account(u_id, c_id)
- await add_balance(u_id, 114514, "1919810", c_id)
- await del_balance(u_id, 114514, "1919810", c_id)
- assert await del_account(u_id, c_id)
- await remove_currency(c_id)
- assert not await get_currency(c_id)
diff --git a/tests/value_tests/default_account.py b/tests/value_tests/default_account.py
deleted file mode 100644
index 37ccaba..0000000
--- a/tests/value_tests/default_account.py
+++ /dev/null
@@ -1,27 +0,0 @@
-import pytest
-from nonebug import App # type: ignore
-
-
-@pytest.mark.asyncio
-async def test_balance(app: App):
- from nonebot_plugin_value.api.api_balance import (
- add_balance,
- del_account,
- del_balance,
- get_or_create_account,
- )
- from nonebot_plugin_value.uuid_lib import to_uuid
-
- account = to_uuid("Test_Example")
- await get_or_create_account(account)
- await add_balance(
- account,
- 100,
- "add",
- )
- await del_balance(
- account,
- 100,
- "del",
- )
- assert await del_account(account)
diff --git a/tests/value_tests/depends.py b/tests/value_tests/depends.py
deleted file mode 100644
index 459a951..0000000
--- a/tests/value_tests/depends.py
+++ /dev/null
@@ -1,35 +0,0 @@
-import pytest
-from nonebug import App # type: ignore
-
-
-def make_event():
- from nonebot.adapters.onebot.v11 import Message, MessageEvent
- from nonebot.adapters.onebot.v11.event import Sender
-
- return MessageEvent(
- time=0,
- self_id=0,
- user_id=123456789,
- post_type="message",
- message=Message(""),
- sub_type="friend",
- message_type="private",
- message_id=0,
- raw_message="",
- font=0,
- sender=Sender(),
- original_message=Message(),
- )
-
-
-@pytest.mark.asyncio
-async def test_depends(app: App):
- from nonebot_plugin_value.api.api_balance import del_account
- from nonebot_plugin_value.api.depends.factory import DependsSwitch
- from nonebot_plugin_value.uuid_lib import to_uuid
-
- uid = to_uuid("123456789")
- executor = await (DependsSwitch().account_executor())(make_event())
- await executor.add_balance(100)
- assert await executor.get_balance() == 100.0
- await del_account(uid)
diff --git a/tests/value_tests/transaction_histories.py b/tests/value_tests/transaction_histories.py
deleted file mode 100644
index 5fb8a2f..0000000
--- a/tests/value_tests/transaction_histories.py
+++ /dev/null
@@ -1,16 +0,0 @@
-import pytest
-from nonebug import App # type: ignore
-
-
-@pytest.mark.asyncio
-async def test_transactions(app: App):
- from nonebot_plugin_value.api.api_balance import add_balance, get_or_create_account
- from nonebot_plugin_value.api.api_transaction import get_transaction_history
- from nonebot_plugin_value.uuid_lib import to_uuid
-
- account = await get_or_create_account(to_uuid("123"))
- for _ in range(20):
- await add_balance(to_uuid("123"), 1)
- transactions = await get_transaction_history(account.id, 30)
- assert len(transactions) >= 20
- assert transactions[0].amount == 1
diff --git a/uv.lock b/uv.lock
index 981abaf..9514768 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1,5 +1,5 @@
version = 1
-revision = 1
+revision = 3
requires-python = ">=3.10, <4.0.0"
[[package]]
@@ -258,6 +258,124 @@ wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/b6/d9/0137658a353168ffa9d0fc14b812d3834772040858ddd1cb6eeaf09f7a44/cookiecutter-2.6.0-py3-none-any.whl", hash = "sha256:a54a8e37995e4ed963b3e82831072d1ad4b005af736bb17b99c2cbd9d41b6e2d" },
]
+[[package]]
+name = "coverage"
+version = "7.13.5"
+source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
+sdist = { url = "https://mirrors.aliyun.com/pypi/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179" }
+wheels = [
+ { url = "https://mirrors.aliyun.com/pypi/packages/69/33/e8c48488c29a73fd089f9d71f9653c1be7478f2ad6b5bc870db11a55d23d/coverage-7.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0723d2c96324561b9aa76fb982406e11d93cdb388a7a7da2b16e04719cf7ca5" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/da/bd/b0ebe9f677d7f4b74a3e115eec7ddd4bcf892074963a00d91e8b164a6386/coverage-7.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52f444e86475992506b32d4e5ca55c24fc88d73bcbda0e9745095b28ef4dc0cf" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/48/cc/5cb9502f4e01972f54eedd48218bb203fe81e294be606a2bc93970208013/coverage-7.13.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:704de6328e3d612a8f6c07000a878ff38181ec3263d5a11da1db294fa6a9bdf8" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/7d/d8/3217636d86c7e7b12e126e4f30ef1581047da73140614523af7495ed5f2d/coverage-7.13.5-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a1a6d79a14e1ec1832cabc833898636ad5f3754a678ef8bb4908515208bf84f4" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/2b/30/2002ac6729ba2d4357438e2ed3c447ad8562866c8c63fc16f6dfc33afe56/coverage-7.13.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79060214983769c7ba3f0cee10b54c97609dca4d478fa1aa32b914480fd5738d" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/6c/85/552496626d6b9359eb0e2f86f920037c9cbfba09b24d914c6e1528155f7d/coverage-7.13.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:356e76b46783a98c2a2fe81ec79df4883a1e62895ea952968fb253c114e7f930" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/44/21/40256eabdcbccdb6acf6b381b3016a154399a75fe39d406f790ae84d1f3c/coverage-7.13.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0cef0cdec915d11254a7f549c1170afecce708d30610c6abdded1f74e581666d" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/b1/e8/96e2a6c3f21a0ea77d7830b254a1542d0328acc8d7bdf6a284ba7e529f77/coverage-7.13.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dc022073d063b25a402454e5712ef9e007113e3a676b96c5f29b2bda29352f40" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/da/ba/8477f549e554827da390ec659f3c38e4b6d95470f4daafc2d8ff94eaa9c2/coverage-7.13.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9b74db26dfea4f4e50d48a4602207cd1e78be33182bc9cbf22da94f332f99878" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/55/59/bc22aef0e6aa179d5b1b001e8b3654785e9adf27ef24c93dc4228ebd5d68/coverage-7.13.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ad146744ca4fd09b50c482650e3c1b1f4dfa1d4792e0a04a369c7f23336f0400" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/de/1b/c6a023a160806a5137dca53468fd97530d6acad24a22003b1578a9c2e429/coverage-7.13.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c555b48be1853fe3997c11c4bd521cdd9a9612352de01fa4508f16ec341e6fe0" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/2d/3f/3532c85a55aa2f899fa17c186f831cfa1aa434d88ff792a709636f64130e/coverage-7.13.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7034b5c56a58ae5e85f23949d52c14aca2cfc6848a31764995b7de88f13a1ea0" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/aa/2e/b9d56af4a24ef45dfbcda88e06870cb7d57b2b0bfa3a888d79b4c8debd76/coverage-7.13.5-cp310-cp310-win32.whl", hash = "sha256:eb7fdf1ef130660e7415e0253a01a7d5a88c9c4d158bcf75cbbd922fd65a5b58" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/9f/cc/d938417e7a4d7f0433ad4edee8bb2acdc60dc7ac5af19e2a07a048ecbee3/coverage-7.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:3e1bb5f6c78feeb1be3475789b14a0f0a5b47d505bfc7267126ccbd50289999e" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/4b/37/d24c8f8220ff07b839b2c043ea4903a33b0f455abe673ae3c03bbdb7f212/coverage-7.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66a80c616f80181f4d643b0f9e709d97bcea413ecd9631e1dedc7401c8e6695d" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/35/8b/cd129b0ca4afe886a6ce9d183c44d8301acbd4ef248622e7c49a23145605/coverage-7.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:145ede53ccbafb297c1c9287f788d1bc3efd6c900da23bf6931b09eafc931587" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/55/2f/e0e5b237bffdb5d6c530ce87cc1d413a5b7d7dfd60fb067ad6d254c35c76/coverage-7.13.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0672854dc733c342fa3e957e0605256d2bf5934feeac328da9e0b5449634a642" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/92/be/b1afb692be85b947f3401375851484496134c5554e67e822c35f28bf2fbc/coverage-7.13.5-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ec10e2a42b41c923c2209b846126c6582db5e43a33157e9870ba9fb70dc7854b" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/da/69/2f47bb6fa1b8d1e3e5d0c4be8ccb4313c63d742476a619418f85740d597b/coverage-7.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be3d4bbad9d4b037791794ddeedd7d64a56f5933a2c1373e18e9e568b9141686" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/d5/d0/79db81da58965bd29dabc8f4ad2a2af70611a57cba9d1ec006f072f30a54/coverage-7.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d2afbc5cc54d286bfb54541aa50b64cdb07a718227168c87b9e2fb8f25e1743" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/e5/32/d0d7cc8168f91ddab44c0ce4806b969df5f5fdfdbb568eaca2dbc2a04936/coverage-7.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ad050321264c49c2fa67bb599100456fc51d004b82534f379d16445da40fb75" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/4d/06/a055311d891ddbe231cd69fdd20ea4be6e3603ffebddf8704b8ca8e10a3c/coverage-7.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7300c8a6d13335b29bb76d7651c66af6bd8658517c43499f110ddc6717bfc209" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/d6/f6/d0fd2d21e29a657b5f77a2fe7082e1568158340dceb941954f776dce1b7b/coverage-7.13.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:eb07647a5738b89baab047f14edd18ded523de60f3b30e75c2acc826f79c839a" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/4e/ab/0d7fb2efc2e9a5eb7ddcc6e722f834a69b454b7e6e5888c3a8567ecffb31/coverage-7.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9adb6688e3b53adffefd4a52d72cbd8b02602bfb8f74dcd862337182fd4d1a4e" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/ba/6f/7467b917bbf5408610178f62a49c0ed4377bb16c1657f689cc61470da8ce/coverage-7.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7c8d4bc913dd70b93488d6c496c77f3aff5ea99a07e36a18f865bca55adef8bd" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/75/2c/1172fb689df92135f5bfbbd69fc83017a76d24ea2e2f3a1154007e2fb9f8/coverage-7.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e3c426ffc4cd952f54ee9ffbdd10345709ecc78a3ecfd796a57236bfad0b9b8" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/67/21/9ac389377380a07884e3b48ba7a620fcd9dbfaf1d40565facdc6b36ec9ef/coverage-7.13.5-cp311-cp311-win32.whl", hash = "sha256:259b69bb83ad9894c4b25be2528139eecba9a82646ebdda2d9db1ba28424a6bf" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/af/7f/4cd8a92531253f9d7c1bbecd9fa1b472907fb54446ca768c59b531248dc5/coverage-7.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:258354455f4e86e3e9d0d17571d522e13b4e1e19bf0f8596bcf9476d61e7d8a9" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/12/a6/1d3f6155fb0010ca68eba7fe48ca6c9da7385058b77a95848710ecf189b1/coverage-7.13.5-cp311-cp311-win_arm64.whl", hash = "sha256:bff95879c33ec8da99fc9b6fe345ddb5be6414b41d6d1ad1c8f188d26f36e028" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/a0/c3/a396306ba7db865bf96fc1fb3b7fd29bcbf3d829df642e77b13555163cd6/coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/a6/16/a68a19e5384e93f811dccc51034b1fd0b865841c390e3c931dcc4699e035/coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/29/72/20b917c6793af3a5ceb7fb9c50033f3ec7865f2911a1416b34a7cfa0813b/coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/8c/49/cd14b789536ac6a4778c453c6a2338bc0a2fb60c5a5a41b4008328b9acc1/coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/9d/00/7b0edcfe64e2ed4c0340dac14a52ad0f4c9bd0b8b5e531af7d55b703db7c/coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/93/89/7ffc4ba0f5d0a55c1e84ea7cee39c9fc06af7b170513d83fbf3bbefce280/coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/81/bd/73ddf85f93f7e6fa83e77ccecb6162d9415c79007b4bc124008a4995e4a7/coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/a0/81/278aff4e8dec4926a0bcb9486320752811f543a3ce5b602cc7a29978d073/coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/70/ee/fe1621488e2e0a58d7e94c4800f0d96f79671553488d401a612bebae324b/coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/37/a6/f79fb37aa104b562207cc23cb5711ab6793608e246cae1e93f26b2236ed9/coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/75/f0/ed15262a58ec81ce457ceb717b7f78752a1713556b19081b76e90896e8d4/coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/0f/e9/9129958f20e7e9d4d56d51d42ccf708d15cac355ff4ac6e736e97a9393d2/coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/a4/d7/0ad9b15812d81272db94379fe4c6df8fd17781cc7671fdfa30c76ba5ff7b/coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/29/3d/821a9a5799fac2556bcf0bd37a70d1d11fa9e49784b6d22e92e8b2f85f18/coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/d4/fa/2238c2ad08e35cf4f020ea721f717e09ec3152aea75d191a7faf3ef009a8/coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/74/8c/74fedc9663dcf168b0a059d4ea756ecae4da77a489048f94b5f512a8d0b3/coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/5f/13/93419671cee82b780bab7ea96b67c8ef448f5f295f36bf5031154ec9a790/coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/4e/5e/3ee3b835647be646dcf3c65a7c6c18f87c27326a858f72ab22c12730773d/coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/44/b3/cb5bd1a04cfcc49ede6cd8409d80bee17661167686741e041abc7ee1b9a9/coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/1b/66/c1dceb7b9714473800b075f5c8a84f4588f887a90eb8645282031676e242/coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/b7/62/5502b73b97aa2e53ea22a39cf8649ff44827bef76d90bf638777daa27a9d/coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/7d/37/7792c2d69854397ca77a55c4646e5897c467928b0e27f2d235d83b5d08c6/coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/a3/23/bc866fb6163be52a8a9e5d708ba0d3b1283c12158cefca0a8bbb6e247a43/coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/7d/8b/ef67e1c222ef49860701d346b8bbb70881bef283bd5f6cbba68a39a086c7/coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/46/0d/866d1f74f0acddbb906db212e096dee77a8e2158ca5e6bb44729f9d93298/coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/7a/f5/be742fec31118f02ce42b21c6af187ad6a344fed546b56ca60caacc6a9a0/coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/48/af/fea819c12a095781f6ccd504890aaddaf88b8fab263c4940e82c7b770124/coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/23/d2/17879af479df7fbbd44bd528a31692a48f6b25055d16482fdf5cdb633805/coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/5b/4c/d20e554f988c8f91d6a02c5118f9abbbf73a8768a3048cb4962230d5743f/coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/29/9c/f9f5277b95184f764b24e7231e166dfdb5780a46d408a2ac665969416d61/coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/d5/f6/7f1ab39393eeb50cfe4747ae8ef0e4fc564b989225aa1152e13a180d74f8/coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/a0/d7/62c084fb489ed9c6fbdf57e006752e7c516ea46fd690e5ed8b8617c7d52e/coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/a9/f6/df63d8660e1a0bff6125947afda112a0502736f470d62ca68b288ea762d8/coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/5b/02/353ca81d36779bd108f6d384425f7139ac3c58c750dcfaafe5d0bee6436b/coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/2c/16/2e79106d5749bcaf3aee6d309123548e3276517cd7851faa8da213bc61bf/coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/29/c7/c29e0c59ffa6942030ae6f50b88ae49988e7e8da06de7ecdbf49c6d4feae/coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/40/48/097cdc3db342f34006a308ab41c3a7c11c3f0d84750d340f45d88a782e00/coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/bb/1f/4994af354689e14fd03a75f8ec85a9a68d94e0188bbdab3fc1516b55e512/coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/22/c6/9bb9ef55903e628033560885f5c31aa227e46878118b63ab15dc7ba87797/coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/14/4f/f5df9007e50b15e53e01edea486814783a7f019893733d9e4d6caad75557/coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/e1/98/aa7fccaa97d0f3192bec013c4e6fd6d294a6ed44b640e6bb61f479e00ed5/coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/3d/8b/e5c469f7352651e5f013198e9e21f97510b23de957dd06a84071683b4b60/coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/8e/77/39703f0d1d4b478bfd30191d3c14f53caf596fac00efb3f8f6ee23646439/coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/6a/6c/1f1917b01eb647c2f2adc9962bd66c79eb978951cab61bdc1acab3290c07/coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/22/e5/06b1f88f42a5a99df42ce61208bdec3bddb3d261412874280a19796fc09c/coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/80/28/2a148a51e5907e504fa7b85490277734e6771d8844ebcc48764a15e28155/coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/61/77/50e8d3d85cc0b7ebe09f30f151d670e302c7ff4a1bf6243f71dd8b0981fa/coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/3b/c4/b5fd1d4b7bf8d0e75d997afd3925c59ba629fc8616f1b3aae7605132e256/coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/f8/66/6ea21f910e92d69ef0b1c3346ea5922a51bad4446c9126db2ae96ee24c4c/coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/9e/ea/879c83cb5d61aa2a35fb80e72715e92672daef8191b84911a643f533840c/coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/8a/fb/616d95d3adb88b9803b275580bdeee8bd1b69a886d057652521f83d7322f/coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/1c/93/25e6917c90ec1c9a56b0b26f6cad6408e5f13bb6b35d484a0d75c9cf000d/coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/fc/7b/dc1776b0464145a929deed214aef9fb1493f159b59ff3c7eeeedf91eddd0/coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/ea/fb/99cbbc56a26e07762a2740713f3c8f9f3f3106e3a3dd8cc4474954bccd34/coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/2c/f2/24d84e1dfe70f8ac9fdf30d338239860d0d1d5da0bda528959d0ebc9da28/coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/60/5b/4a168591057b3668c2428bff25dd3ebc21b629d666d90bcdfa0217940e84/coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/f5/21/1fd5c4dbfe4a58b6b99649125635df46decdfd4a784c3cd6d410d303e370/coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/d6/fe/2a924b3055a5e7e4512655a9d4609781b0d62334fa0140c3e742926834e2/coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/d7/0d/c8928f2bd518c45990fe1a2ab8db42e914ef9b726c975facc4282578c3eb/coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/ef/ae/4ae35bbd9a0af9d820362751f0766582833c211224b38665c0f8de3d487f/coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/9c/20/d326174c55af36f74eac6ae781612d9492f060ce8244b570bb9d50d9d609/coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/7a/5e/31484d62cbd0eabd3412e30d74386ece4a0837d4f6c3040a653878bfc019/coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/e9/d8/49a72d6de146eebb0b7e48cc0f4bc2c0dd858e3d4790ab2b39a2872b62bd/coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/06/3b/0351f1bd566e6e4dd39e978efe7958bde1d32f879e85589de147654f57bb/coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/5d/ce/796a2a2f4017f554d7810f5c573449b35b1e46788424a548d4d19201b222/coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/3d/16/d5ae91455541d1a78bc90abf495be600588aff8f6db5c8b0dae739fa39c9/coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/48/11/07f413dba62db21fb3fad5d0de013a50e073cc4e2dc4306e770360f6dfc8/coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/91/15/d792371332eb4663115becf4bad47e047d16234b1aff687b1b18c58d60ae/coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/db/51/37221f59a111dca5e85be7dbf09696323b5b9f13ff65e0641d535ed06ea8/coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/54/83/6acacc889de8987441aa7d5adfbdbf33d288dad28704a67e574f1df9bcbb/coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61" },
+]
+
+[package.optional-dependencies]
+toml = [
+ { name = "tomli", marker = "python_full_version <= '3.11'" },
+]
+
[[package]]
name = "distlib"
version = "0.4.0"
@@ -808,7 +926,7 @@ default = [
[[package]]
name = "nonebot-plugin-value"
-version = "0.1.6.post1"
+version = "1.0.0"
source = { virtual = "." }
dependencies = [
{ name = "aiofiles" },
@@ -830,6 +948,7 @@ dev = [
{ name = "nonebug" },
{ name = "pyright" },
{ name = "pytest-asyncio" },
+ { name = "pytest-cov" },
{ name = "ruff" },
]
@@ -854,6 +973,7 @@ dev = [
{ name = "nonebug", specifier = ">=0.4.3" },
{ name = "pyright", specifier = ">=1.0.0" },
{ name = "pytest-asyncio", specifier = ">=1.1.0" },
+ { name = "pytest-cov", specifier = ">=7.0.0" },
{ name = "ruff", specifier = ">=0.12.0" },
]
@@ -1230,6 +1350,20 @@ wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/c7/9d/bf86eddabf8c6c9cb1ea9a869d6873b46f105a5d292d3a6f7071f5b07935/pytest_asyncio-1.1.0-py3-none-any.whl", hash = "sha256:5fe2d69607b0bd75c656d1211f969cadba035030156745ee09e7d71740e58ecf" },
]
+[[package]]
+name = "pytest-cov"
+version = "7.1.0"
+source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
+dependencies = [
+ { name = "coverage", extra = ["toml"] },
+ { name = "pluggy" },
+ { name = "pytest" },
+]
+sdist = { url = "https://mirrors.aliyun.com/pypi/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2" }
+wheels = [
+ { url = "https://mirrors.aliyun.com/pypi/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678" },
+]
+
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
From f505b6876db7e842f433aa1c4a7bd5d0e7355370 Mon Sep 17 00:00:00 2001
From: John Richard
Date: Sat, 4 Apr 2026 11:23:38 +0800
Subject: [PATCH 2/5] Fix: comments
---
nonebot_plugin_value/api/api_balance.py | 48 +++++----------
nonebot_plugin_value/api/api_currency.py | 3 -
nonebot_plugin_value/services/balance.py | 64 +++++++++++---------
nonebot_plugin_value/services/currency.py | 29 ++++-----
nonebot_plugin_value/services/transaction.py | 12 ++--
5 files changed, 71 insertions(+), 85 deletions(-)
diff --git a/nonebot_plugin_value/api/api_balance.py b/nonebot_plugin_value/api/api_balance.py
index 3549c67..e1a8f5b 100644
--- a/nonebot_plugin_value/api/api_balance.py
+++ b/nonebot_plugin_value/api/api_balance.py
@@ -40,8 +40,8 @@ async def set_frozen(account_id: str, currency_id: str, frozen: bool) -> None:
"""设置账户特定货币冻结状态
Args:
- account_id (str): 用户ID
- currency_id (str): 货币ID
+ account_id (str): 账户 ID
+ currency_id (str): 货币 ID
frozen (bool): 是否冻结
"""
async with get_session() as session:
@@ -141,16 +141,13 @@ async def batch_del_balance(
updates: list[tuple[str, float]],
currency_id: str | None = None,
source: str = "batch_update",
-) -> list[UserAccountData] | None:
+) -> None:
"""批量减少账户余额
Args:
- updates (list[tuple[str, float]]): 元组列表,包含用户id和金额
- currency_id (str | None, optional): 货币ID. Defaults to None.
- source (str, optional): 源说明. Defaults to "batch_update".
-
- Returns:
- None: None
+ updates (list[tuple[str, float]]): 元组列表,包含用户 id 和金额
+ currency_id (str | None, optional): 货币 ID. Defaults to None.
+ source (str, optional): 源说明。Defaults to "batch_update".
"""
if currency_id is None:
currency_id = DEFAULT_CURRENCY_UUID.hex
@@ -165,16 +162,13 @@ async def batch_add_balance(
updates: list[tuple[str, float]],
currency_id: str | None = None,
source: str = "batch_update",
-) -> list[UserAccountData] | None:
+) -> None:
"""批量添加账户余额
Args:
- updates (list[tuple[str, float]]): 元组列表,包含用户id和金额
- currency_id (str | None, optional): 货币ID. Defaults to None.
- source (str, optional): 源说明. Defaults to "batch_update".
-
- Returns:
- None: None
+ updates (list[tuple[str, float]]): 元组列表,包含用户 id 和金额
+ currency_id (str | None, optional): 货币 ID. Defaults to None.
+ source (str, optional): 源说明。Defaults to "batch_update".
"""
if currency_id is None:
currency_id = DEFAULT_CURRENCY_UUID.hex
@@ -194,18 +188,14 @@ async def add_balance(
"""添加用户余额
Args:
- user_id (str): 用户ID
+ user_id (str): 用户 ID
amount (float): 金额
- source (str, optional): 源描述. Defaults to "_transfer".
- currency_id (str | None, optional): 货币ID(不填使用默认). Defaults to None.
+ source (str, optional): 源描述。Defaults to "_transfer".
+ currency_id (str | None, optional): 货币 ID(不填使用默认). Defaults to None.
Raises:
RuntimeError: 如果添加失败则抛出异常
-
- Returns:
- None: None
"""
-
if currency_id is None:
currency_id = DEFAULT_CURRENCY_UUID.hex
data = await _a_balance(user_id, currency_id, amount, source)
@@ -225,16 +215,13 @@ async def del_balance(
"""减少一个账户的余额
Args:
- user_id (str): 用户ID
+ user_id (str): 用户 ID
amount (float): 金额
- source (str, optional): 源说明. Defaults to "_transfer".
- currency_id (str | None, optional): 货币ID(不填则使用默认货币). Defaults to Noen.
+ source (str, optional): 源说明。Defaults to "_transfer".
+ currency_id (str | None, optional): 货币 ID(不填则使用默认货币). Defaults to None.
Raises:
RuntimeError: 如果失败则抛出
-
- Returns:
- UserAccountData: 用户数据
"""
if currency_id is None:
currency_id = DEFAULT_CURRENCY_UUID.hex
@@ -265,9 +252,6 @@ async def transfer_funds(
Raises:
RuntimeError: 失败则抛出
-
- Returns:
- None: None
"""
if currency_id is None:
currency_id = DEFAULT_CURRENCY_UUID.hex
diff --git a/nonebot_plugin_value/api/api_currency.py b/nonebot_plugin_value/api/api_currency.py
index 88fcf33..deaac15 100644
--- a/nonebot_plugin_value/api/api_currency.py
+++ b/nonebot_plugin_value/api/api_currency.py
@@ -129,9 +129,6 @@ async def create_currency(currency_data: CurrencyData) -> None:
Args:
currency_data (CurrencyData): 货币信息
-
- Returns:
- CurrencyData: 货币信息
"""
async with get_session() as session:
await CacheManager().update_cache(
diff --git a/nonebot_plugin_value/services/balance.py b/nonebot_plugin_value/services/balance.py
index 9921e06..f495bdc 100644
--- a/nonebot_plugin_value/services/balance.py
+++ b/nonebot_plugin_value/services/balance.py
@@ -70,8 +70,10 @@ async def del_account(
"""删除账户
Args:
- session (AsyncSession | None, optional): 异步会话. Defaults to None.
- user_id (str): 用户ID
+ account_id (str): 账户 ID
+ session (AsyncSession | None, optional): 异步会话。Defaults to None.
+ fail_then_throw (bool, optional): 失败时是否抛出异常。Defaults to False.
+ currency_id (str | None, optional): 货币 ID. Defaults to None.
"""
arg_session = session
session = session or get_session()
@@ -94,7 +96,8 @@ async def list_accounts(
"""列出所有账户
Args:
- session (AsyncSession): 异步会话. Defaults to None.
+ session (AsyncSession): 异步会话
+ currency_id (str | None, optional): 货币 ID. Defaults to None.
Returns:
Sequence[UserAccount]: 所有账户(指定或所有货币的)
@@ -112,9 +115,9 @@ async def get_or_create_account(
"""获取或创建一个货币的账户
Args:
- user_id (str): 用户ID
- currency_id (str): 货币ID
- session (AsyncSession): 异步会话. Defaults to None.
+ user_id (str): 用户 ID
+ currency_id (str): 货币 ID
+ session (AsyncSession): 异步会话
Returns:
UserAccount: 用户数据模型
@@ -132,11 +135,11 @@ async def batch_del_balance(
"""批量减少账户余额
Args:
- updates (list[tuple[str, float]]): 元组列表,包含用户id和金额
- currency_id (str): 货币ID
- source (str, optional): 源. Defaults to "batch_update".
- session (AsyncSession | None, optional): 异步Session. Defaults to None.
- return_all_on_fail (bool, optional): 批量操作失败时是否仍然返回所有结果. Defaults to False.
+ updates (list[tuple[str, float]]): 元组列表,包含用户 id 和金额
+ currency_id (str): 货币 ID
+ source (str, optional): 源。Defaults to "batch_update".
+ session (AsyncSession | None, optional): 异步 Session. Defaults to None.
+ return_all_on_fail (bool, optional): 批量操作失败时是否仍然返回所有结果。Defaults to True.
Returns:
list[ActionResult]: 操作结果列表
@@ -172,11 +175,11 @@ async def del_balance(
"""异步减少余额
Args:
- user_id (str): 用户ID
- currency_id (str): 货币ID
+ user_id (str): 用户 ID
+ currency_id (str): 货币 ID
amount (float): 金额
- source (str, optional): 来源说明. Defaults to "".
- session (AsyncSession | None, optional): 数据库异步会话. Defaults to None.
+ source (str, optional): 来源说明。Defaults to "".
+ session (AsyncSession): 数据库异步会话
Returns:
ActionResult: 包含是否成功的说明
@@ -249,10 +252,11 @@ async def batch_add_balance(
"""批量添加余额
Args:
- updates (list[tuple[str, float]]): 元组列表 [(用户ID, 金额变化)]
- source (str, optional): 来源. Defaults to "batch_update".
- session (AsyncSession | None, optional): 会话. Defaults to None.
- return_all_on_fail (bool, optional): 返回所有结果即使失败时. Defaults to False.
+ updates (list[tuple[str, float]]): 元组列表 [(用户 ID, 金额变化)]
+ currency_id (str): 货币 ID
+ source (str, optional): 来源。Defaults to "batch_update".
+ session (AsyncSession | None, optional): 会话。Defaults to None.
+ return_all_on_fail (bool, optional): 返回所有结果即使失败时。Defaults to False.
Returns:
list[ActionResult]: 返回的数据(与列表顺序一致,如果任意一个失败则返回空列表)
@@ -281,14 +285,14 @@ async def add_balance(
"""异步增加余额
Args:
- user_id (str): 用户ID
- currency_id (str): 货币ID
+ user_id (str): 用户 ID
+ currency_id (str): 货币 ID
amount (float): 金额
- source (str, optional): 来源说明. Defaults to "".
- session (AsyncSession | None, optional): 数据库异步会话. Defaults to None.
+ source (str, optional): 来源说明。Defaults to "".
+ arg_session (AsyncSession | None, optional): 数据库异步会话。Defaults to None.
Returns:
- ActionResult: 是否成功("success"),消息说明("message")
+ ActionResult: 是否成功 ("success"),消息说明 ("message")
"""
session = arg_session or get_session()
async with session if arg_session is None else contextlib.nullcontext():
@@ -366,15 +370,15 @@ async def transfer_funds(
"""异步转账
Args:
- fromuser_id (str): 源用户ID
- touser_id (str): 目标用户ID
- currency_id (str): 货币ID
+ fromuser_id (str): 源用户 ID
+ touser_id (str): 目标用户 ID
+ currency_id (str): 货币 ID
amount (float): 金额
- source (str, optional): 源说明. Defaults to "transfer".
- session (AsyncSession | None, optional): 数据库异步Session. Defaults to None.
+ source (str, optional): 源说明。Defaults to "transfer".
+ arg_session (AsyncSession | None, optional): 数据库异步 Session. Defaults to None.
Returns:
- TransferResult: 如果成功则包含"from_balance"(源账户现在的balance),"to_balance"(目标账户现在的balance)字段
+ TransferResult: 如果成功则包含"from_balance"(源账户现在的 balance),"to_balance"(目标账户现在的 balance)字段
"""
if amount <= 0:
diff --git a/nonebot_plugin_value/services/currency.py b/nonebot_plugin_value/services/currency.py
index acc566d..8a68a96 100644
--- a/nonebot_plugin_value/services/currency.py
+++ b/nonebot_plugin_value/services/currency.py
@@ -16,7 +16,7 @@ async def update_currency(
Args:
currency_data (CurrencyData): 货币元信息
- session (AsyncSession): 异步Session. .
+ session (AsyncSession): 异步 Session
Returns:
CurrencyMeta: 货币元信息模型
@@ -48,7 +48,7 @@ async def list_currencies(
"""获取已存在的货币
Args:
- session (AsyncSession | None, optional): 异步Session. Defaults to None.
+ session (AsyncSession): 异步 Session
Returns:
Sequence[CurrencyMeta]: 返回货币列表
@@ -63,11 +63,11 @@ async def get_currency(
"""获取一个货币的元信息
Args:
- session (AsyncSession): SQLAlchemy的异步session
- currency_id (str): 货币唯一ID
+ currency_id (str): 货币唯一 ID
+ session (AsyncSession): SQLAlchemy 的异步 session
Returns:
- CurrencyMeta | None: 货币元数据(不存在为None)
+ CurrencyMeta | None: 货币元数据(不存在为 None)
"""
return await CurrencyRepository(session).get_currency(currency_id)
@@ -80,11 +80,11 @@ async def get_currency_by_kwargs(
"""获取一个货币的元信息
Args:
- session (AsyncSession): SQLAlchemy的异步session
- kwargs (object): 货币元信息字段
+ session (AsyncSession): SQLAlchemy 的异步 session
+ **kwargs (object): 查询条件(货币属性字段)
Returns:
- CurrencyMeta | None: 货币元数据(不存在为None)
+ CurrencyMeta | None: 货币元数据(不存在为 None)
"""
return await CurrencyRepository(session).get_currency_by_kwargs(**kwargs)
@@ -92,12 +92,12 @@ async def get_currency_by_kwargs(
async def create_currency(
currency_data: CurrencyData, session: AsyncSession | None = None
-) -> None:
+) -> CurrencyMeta:
"""创建货币
Args:
- session (AsyncSession): SQLAlchemy的异步session
currency_data (CurrencyData): 货币数据
+ session (AsyncSession | None, optional): SQLAlchemy 的异步 session. Defaults to None.
Returns:
CurrencyMeta: 创建的货币元数据
@@ -105,9 +105,10 @@ async def create_currency(
arg_session = session
session = session or get_session()
async with session if arg_session is None else contextlib.nullcontext():
- await CurrencyRepository(session).createcurrency(currency_data)
+ result = await CurrencyRepository(session).createcurrency(currency_data)
if not arg_session:
await session.commit()
+ return result
async def get_or_create_currency(
@@ -117,11 +118,11 @@ async def get_or_create_currency(
"""获取或创建新货币(如果存在就获取)
Args:
- session (AsyncSession): SQLAlchemy的异步session
currency_data (CurrencyData): 货币元信息
+ session (AsyncSession): SQLAlchemy 的异步 session
Returns:
- tuple[CurrencyMeta, bool] 元数据和是否创建
+ tuple[CurrencyMeta, bool]: 元数据和是否创建
"""
repo = CurrencyRepository(session)
@@ -132,7 +133,7 @@ async def get_default_currency(session: AsyncSession) -> CurrencyMeta:
"""获取默认货币
Args:
- session (AsyncSession | None, optional): 异步会话. Defaults to None.
+ session (AsyncSession): 异步会话
Returns:
CurrencyMeta: 货币元数据
diff --git a/nonebot_plugin_value/services/transaction.py b/nonebot_plugin_value/services/transaction.py
index 32cfeee..013a7e9 100644
--- a/nonebot_plugin_value/services/transaction.py
+++ b/nonebot_plugin_value/services/transaction.py
@@ -16,11 +16,11 @@ async def get_transaction_history_by_time_range(
"""通过时间范围获取账户交易历史
Args:
- account_id (str): 用户ID
+ account_id (str): 账户 ID
start_time (datetime): 起始时间
end_time (datetime): 结束时间
- limit (int, optional): 条数限制. Defaults to 100.
- session (AsyncSession | None, optional): 会话. Defaults to None.
+ session (AsyncSession): 会话
+ limit (int, optional): 条数限制。Defaults to 100.
Returns:
Sequence[Transaction]: 记录
@@ -41,9 +41,9 @@ async def get_transaction_history(
"""获取一个用户的交易记录
Args:
- session (AsyncSession | None, optional): 异步数据库会话
- account_id (str): 用户UUID(应自行处理)
- limit (int, optional): 数据条数. Defaults to 100.
+ account_id (str): 账户 UUID(应自行处理)
+ session (AsyncSession): 异步数据库会话
+ limit (int, optional): 数据条数。Defaults to 100.
Returns:
Sequence[Transaction]: 记录列表
From d3ec73d85c987ace113f2c6442791cfac5415db0 Mon Sep 17 00:00:00 2001
From: John Richard
Date: Sat, 4 Apr 2026 11:25:38 +0800
Subject: [PATCH 3/5] Remove: JUnit
---
.github/workflows/PR.yml | 14 ++------------
1 file changed, 2 insertions(+), 12 deletions(-)
diff --git a/.github/workflows/PR.yml b/.github/workflows/PR.yml
index 8ea66e2..8c93f88 100644
--- a/.github/workflows/PR.yml
+++ b/.github/workflows/PR.yml
@@ -45,18 +45,8 @@ jobs:
- name: Prepare database
run: uv run nb orm upgrade
- - name: Run Unit Tests with JUnit XML output
- run: uv run pytest tests/* --cov=nonebot_plugin_value --cov-report=term-missing --cov-report=xml --junitxml=test-results.xml -v
+ - name: Run Unit Tests
+ run: uv run pytest tests/* --cov=nonebot_plugin_value --cov-report=term-missing --cov-report=xml -v
- - name: Publish Test Report
- uses: dorny/test-reporter@v1
- if: success() || failure()
- with:
- name: Python Unit Tests
- path: test-results.xml
- reporter: java-junit
- fail-on-error: false
-
-
- name: Build package
run: uv build # 生成构建产物到dist目录
From a7a027e429cdff258c4e7f61aac7798cbbe7a612 Mon Sep 17 00:00:00 2001
From: John Richard
Date: Sat, 4 Apr 2026 11:43:12 +0800
Subject: [PATCH 4/5] Resolve: problems
---
nonebot_plugin_value/services/balance.py | 35 ++++++++++++++++--------
tests/test_api_coverage.py | 19 +++++++++++--
tests/test_hooks.py | 4 +--
3 files changed, 42 insertions(+), 16 deletions(-)
diff --git a/nonebot_plugin_value/services/balance.py b/nonebot_plugin_value/services/balance.py
index f495bdc..d54f6a8 100644
--- a/nonebot_plugin_value/services/balance.py
+++ b/nonebot_plugin_value/services/balance.py
@@ -46,19 +46,23 @@ async def set_frozen_all(
frozen: bool,
session: AsyncSession | None = None,
):
- """冻结这个账户ID下的所有货币储备
+ """冻结这个账户 ID 下的所有货币储备
Args:
- account_id (str): 账户ID
+ account_id (str): 账户 ID
frozen (bool): 是否冻结
- session (AsyncSession | None, optional): 异步Session. Defaults to None.
+ session (AsyncSession | None, optional): 异步 Session. Defaults to None.
"""
arg_session = session
session = session or get_session()
- async with session if arg_session is None else contextlib.nullcontext():
- await AccountRepository(session).set_frozen_all(account_id, frozen)
- if not arg_session:
- await session.commit()
+
+ async with session.begin() if not arg_session else contextlib.nullcontext():
+ try:
+ await AccountRepository(session).set_frozen_all(account_id, frozen)
+ except Exception:
+ if arg_session:
+ await session.rollback()
+ raise
async def del_account(
@@ -142,24 +146,31 @@ async def batch_del_balance(
return_all_on_fail (bool, optional): 批量操作失败时是否仍然返回所有结果。Defaults to True.
Returns:
- list[ActionResult]: 操作结果列表
+ list[ActionResult]: 操作结果列表(与输入顺序一致)
"""
arg_session = session
session = session or get_session()
- result_list: list[ActionResult] = []
async def task_inner(uid: str, amount: float):
- nonlocal result_list
data: ActionResult = await del_balance(
uid, currency_id, amount, source=source, session=session
)
- result_list.append(data)
+ return data
async with session.begin() if arg_session is None else contextlib.nullcontext():
- await asyncio.gather(
+ results: list[ActionResult | BaseException] = await asyncio.gather(
*[task_inner(uid, amount) for uid, amount in updates],
return_exceptions=True,
)
+ result_list: list[ActionResult] = []
+ for result in results:
+ if isinstance(result, BaseException):
+ result_list.append(
+ ActionResult(success=False, message=f"操作失败:{result!s}")
+ )
+ else:
+ result_list.append(result)
+
if not all(r.success for r in result_list):
return [] if not return_all_on_fail else result_list
return result_list
diff --git a/tests/test_api_coverage.py b/tests/test_api_coverage.py
index 58baae9..d2e65c8 100644
--- a/tests/test_api_coverage.py
+++ b/tests/test_api_coverage.py
@@ -149,7 +149,22 @@ async def test_transaction_api(app: App):
)
assert len(empty_transactions) == 0, "未来时间范围应该没有交易记录"
- # 测试删除交易记录(可选,如果需要测试的话)
+ # 测试删除交易记录
if len(transactions_all) > 0:
first_tx_id = transactions_all[0].id
- await remove_transaction(first_tx_id)
+
+ # 验证删除操作的返回值
+ delete_result = await remove_transaction(first_tx_id)
+ assert delete_result is True, "删除交易记录应该返回 True"
+
+ # 验证删除效果:重新获取交易记录,确认已删除
+ transactions_after_delete = await get_transaction_history(
+ test_user_id, limit=100
+ )
+ deleted_tx_ids = [tx.id for tx in transactions_after_delete]
+ assert first_tx_id not in deleted_tx_ids, "删除后的交易 ID 不应该出现在列表中"
+
+ # 验证数量减少
+ assert len(transactions_after_delete) == len(transactions_all) - 1, (
+ "删除后交易记录数量应该减少 1"
+ )
diff --git a/tests/test_hooks.py b/tests/test_hooks.py
index 1d0d9f2..060ffc9 100644
--- a/tests/test_hooks.py
+++ b/tests/test_hooks.py
@@ -140,6 +140,6 @@ async def error_hook(context: TransactionContext):
setattr(manager, "_HooksManager__hooks", {})
manager.register(HooksType.pre(), error_hook)
- # 应该记录错误但不中断执行
- await manager.run_hooks(HooksType.pre(), pre_context)
+ with pytest.raises(ValueError, match="Test error in hook"):
+ await manager.run_hooks(HooksType.pre(), pre_context)
assert error_hook_called, "错误的钩子也应该被调用"
From 8e7755f35929d1afa1e76e2654ba6b9c3c2ea9f6 Mon Sep 17 00:00:00 2001
From: John Richard
Date: Sat, 4 Apr 2026 11:45:14 +0800
Subject: [PATCH 5/5] Fix: tests
---
tests/test_hooks.py | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/tests/test_hooks.py b/tests/test_hooks.py
index 060ffc9..3da14c2 100644
--- a/tests/test_hooks.py
+++ b/tests/test_hooks.py
@@ -135,11 +135,10 @@ async def update_hook(context: TransactionContext):
async def error_hook(context: TransactionContext):
nonlocal error_hook_called
error_hook_called = True
- raise ValueError("Test error in hook")
+ raise ValueError("Test error in hook") # 这个会被Inner捕捉
setattr(manager, "_HooksManager__hooks", {})
manager.register(HooksType.pre(), error_hook)
- with pytest.raises(ValueError, match="Test error in hook"):
- await manager.run_hooks(HooksType.pre(), pre_context)
+ await manager.run_hooks(HooksType.pre(), pre_context)
assert error_hook_called, "错误的钩子也应该被调用"