From 38f0edcd25fd062bab68faf03542d9b8ec8cf077 Mon Sep 17 00:00:00 2001 From: Azide Date: Sun, 4 Aug 2024 18:40:01 +0800 Subject: [PATCH] =?UTF-8?q?:bug:=20Bilibili=E8=B0=83=E5=BA=A6=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E5=9B=9E=E9=81=BF=E7=AD=96=E7=95=A5=20(#573)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :bug: 将Bilibili的调度速度降低到60s * :sparkles: 增加回避策略 * :sparkles: 降低轮询间隔,增加回避次数,抛出阶段随机刷新 * :recycle: 更清晰的调度逻辑实现 * :bug: 兼容3.10的NamedTuple多继承 * :recycle: 合并重复逻辑 * :recycle: ctx放入fsm * :bug: 测试并调整逻辑 * :bug: 补全类型标注 * :recycle: 添加Condition和State.on_exit/on_enter,以实现自动状态切换 * :white_check_mark: 调整测试 * :bug: 私有化命名方法 * :loud_sound: 调整补充日志 * :bug: 添加测试后清理 * :pencil2: fix typing typo --- nonebot_bison/platform/bilibili/fsm.py | 168 ++++++++++++ nonebot_bison/platform/bilibili/platforms.py | 38 +-- nonebot_bison/platform/bilibili/retry.py | 253 +++++++++++++++++++ nonebot_bison/platform/bilibili/scheduler.py | 12 +- poetry.lock | 230 ++++------------- pyproject.toml | 2 + tests/platforms/test_bilibili.py | 97 ++++++- 7 files changed, 562 insertions(+), 238 deletions(-) create mode 100644 nonebot_bison/platform/bilibili/fsm.py create mode 100644 nonebot_bison/platform/bilibili/retry.py diff --git a/nonebot_bison/platform/bilibili/fsm.py b/nonebot_bison/platform/bilibili/fsm.py new file mode 100644 index 0000000..9f9466d --- /dev/null +++ b/nonebot_bison/platform/bilibili/fsm.py @@ -0,0 +1,168 @@ +import sys +import asyncio +import inspect +from enum import Enum +from dataclasses import dataclass +from collections.abc import Set as AbstractSet +from collections.abc import Callable, Sequence, Awaitable, AsyncGenerator +from typing import TYPE_CHECKING, Any, Generic, TypeVar, Protocol, TypeAlias, TypedDict, NamedTuple, runtime_checkable + +from nonebot import logger + + +class StrEnum(str, Enum): ... + + +TAddon = TypeVar("TAddon", contravariant=True) +TState = TypeVar("TState", contravariant=True) +TEvent = TypeVar("TEvent", contravariant=True) +TFSM = TypeVar("TFSM", bound="FSM", contravariant=True) + + +class StateError(Exception): ... + + +ActionReturn: TypeAlias = Any + + +@runtime_checkable +class SupportStateOnExit(Generic[TAddon], Protocol): + async def on_exit(self, addon: TAddon) -> None: ... + + +@runtime_checkable +class SupportStateOnEnter(Generic[TAddon], Protocol): + async def on_enter(self, addon: TAddon) -> None: ... + + +class Action(Generic[TState, TEvent, TAddon], Protocol): + async def __call__(self, from_: TState, event: TEvent, to: TState, addon: TAddon) -> ActionReturn: ... + + +ConditionFunc = Callable[[TAddon], Awaitable[bool]] + + +@dataclass(frozen=True) +class Condition(Generic[TAddon]): + call: ConditionFunc[TAddon] + not_: bool = False + + def __repr__(self): + if inspect.isfunction(self.call) or inspect.isclass(self.call): + call_str = self.call.__name__ + else: + call_str = repr(self.call) + return f"Condition(call={call_str})" + + async def __call__(self, addon: TAddon) -> bool: + return (await self.call(addon)) ^ self.not_ + + +# FIXME: Python 3.11+ 才支持 NamedTuple和TypedDict使用多继承添加泛型 +# 所以什么时候 drop 3.10(? +if sys.version_info >= (3, 11) or TYPE_CHECKING: + + class Transition(Generic[TState, TEvent, TAddon], NamedTuple): + action: Action[TState, TEvent, TAddon] + to: TState + conditions: AbstractSet[Condition[TAddon]] | None = None + + class StateGraph(Generic[TState, TEvent, TAddon], TypedDict): + transitions: dict[ + TState, + dict[ + TEvent, + Transition[TState, TEvent, TAddon] | Sequence[Transition[TState, TEvent, TAddon]], + ], + ] + initial: TState + +else: + + class Transition(NamedTuple): + action: Action + to: Any + conditions: AbstractSet[Condition] | None = None + + class StateGraph(TypedDict): + transitions: dict[Any, dict[Any, Transition]] + initial: Any + + +class FSM(Generic[TState, TEvent, TAddon]): + def __init__(self, graph: StateGraph[TState, TEvent, TAddon], addon: TAddon): + self.started = False + self.graph = graph + self.current_state = graph["initial"] + self.machine = self._core() + self.addon = addon + + async def _core(self) -> AsyncGenerator[ActionReturn, TEvent]: + self.current_state = self.graph["initial"] + res = None + while True: + event = yield res + + if not self.started: + raise StateError("FSM not started, please call start() first") + + selected_transition = await self.cherry_pick(event) + + logger.trace(f"exit state: {self.current_state}") + if isinstance(self.current_state, SupportStateOnExit): + logger.trace(f"do {self.current_state}.on_exit") + await self.current_state.on_exit(self.addon) + + logger.trace(f"do action: {selected_transition.action}") + res = await selected_transition.action(self.current_state, event, selected_transition.to, self.addon) + + logger.trace(f"enter state: {selected_transition.to}") + self.current_state = selected_transition.to + + if isinstance(self.current_state, SupportStateOnEnter): + logger.trace(f"do {self.current_state}.on_enter") + await self.current_state.on_enter(self.addon) + + async def start(self): + await anext(self.machine) + self.started = True + logger.trace(f"FSM started, initial state: {self.current_state}") + + async def cherry_pick(self, event: TEvent) -> Transition[TState, TEvent, TAddon]: + transitions = self.graph["transitions"][self.current_state].get(event) + if transitions is None: + raise StateError(f"Invalid event {event} in state {self.current_state}") + + if isinstance(transitions, Transition): + return transitions + elif isinstance(transitions, Sequence): + no_conds: list[Transition[TState, TEvent, TAddon]] = [] + for transition in transitions: + if not transition.conditions: + no_conds.append(transition) + continue + + values = await asyncio.gather(*(condition(self.addon) for condition in transition.conditions)) + + if all(values): + logger.trace(f"conditions {transition.conditions} passed") + return transition + else: + if no_conds: + return no_conds.pop() + else: + raise StateError(f"Invalid event {event} in state {self.current_state}") + else: + raise TypeError("Invalid transition type: {transitions}, expected Transition or Sequence[Transition]") + + async def emit(self, event: TEvent): + return await self.machine.asend(event) + + async def reset(self): + await self.machine.aclose() + self.started = False + + del self.machine + self.machine = self._core() + + logger.trace("FSM closed") diff --git a/nonebot_bison/platform/bilibili/platforms.py b/nonebot_bison/platform/bilibili/platforms.py index 3aeafa3..e3cfc0a 100644 --- a/nonebot_bison/platform/bilibili/platforms.py +++ b/nonebot_bison/platform/bilibili/platforms.py @@ -1,16 +1,13 @@ import re import json from copy import deepcopy -from functools import wraps from enum import Enum, unique +from typing import NamedTuple from typing_extensions import Self -from typing import TypeVar, NamedTuple -from collections.abc import Callable, Awaitable from yarl import URL from nonebot import logger from httpx import AsyncClient -from httpx import URL as HttpxURL from pydantic import Field, BaseModel, ValidationError from nonebot.compat import type_validate_json, type_validate_python @@ -19,6 +16,7 @@ from nonebot_bison.compat import model_rebuild from nonebot_bison.utils import text_similarity, decode_unicode_escapes from nonebot_bison.types import Tag, Target, RawPost, ApiError, Category +from .retry import ApiCode352Error, retry_for_352 from .scheduler import BilibiliSite, BililiveSite, BiliBangumiSite from ..platform import NewMessage, StatusChange, CategoryNotSupport, CategoryNotRecognize from .models import ( @@ -38,38 +36,6 @@ from .models import ( LiveRecommendMajor, ) -B = TypeVar("B", bound="Bilibili") -MAX_352_RETRY_COUNT = 3 - - -class ApiCode352Error(Exception): - def __init__(self, url: HttpxURL) -> None: - msg = f"api {url} error" - super().__init__(msg) - - -def retry_for_352(func: Callable[[B, Target], Awaitable[list[DynRawPost]]]): - retried_times = 0 - - @wraps(func) - async def wrapper(bls: B, *args, **kwargs): - nonlocal retried_times - try: - res = await func(bls, *args, **kwargs) - except ApiCode352Error as e: - if retried_times < MAX_352_RETRY_COUNT: - retried_times += 1 - logger.warning(f"获取动态列表失败,尝试刷新cookie: {retried_times}/{MAX_352_RETRY_COUNT}") - await bls.ctx.refresh_client() - return [] # 返回空列表 - else: - raise ApiError(e.args[0]) - else: - retried_times = 0 - return res - - return wrapper - class _ProcessedText(NamedTuple): title: str diff --git a/nonebot_bison/platform/bilibili/retry.py b/nonebot_bison/platform/bilibili/retry.py new file mode 100644 index 0000000..e9c1597 --- /dev/null +++ b/nonebot_bison/platform/bilibili/retry.py @@ -0,0 +1,253 @@ +import random +from enum import Enum +from functools import wraps +from dataclasses import dataclass +from datetime import datetime, timedelta +from collections.abc import Callable, Awaitable +from typing_extensions import override, assert_never +from typing import TYPE_CHECKING, Generic, Literal, TypeVar + +from strenum import StrEnum +from nonebot.log import logger +from httpx import URL as HttpxURL + +from nonebot_bison.types import Target + +from .models import DynRawPost +from .fsm import FSM, Condition, StateGraph, Transition, ActionReturn + +if TYPE_CHECKING: + from .platforms import Bilibili + +# 不用 TypeVar 的话,使用装饰器 Pyright 会报错 +TBilibili = TypeVar("TBilibili", bound="Bilibili") + + +class ApiCode352Error(Exception): + def __init__(self, url: HttpxURL) -> None: + msg = f"api {url} error" + super().__init__(msg) + + +# see https://docs.python.org/zh-cn/3/howto/enum.html#dataclass-support +@dataclass(frozen=True) +class StateMixin: + state: Literal["NORMAL", "REFRESH", "BACKOFF", "RAISE"] + enter_func: Callable[["RetryAddon"], Awaitable[None]] | None = None + exit_func: Callable[["RetryAddon"], Awaitable[None]] | None = None + + async def on_enter(self, addon: "RetryAddon"): + if self.enter_func: + await self.enter_func(addon) + + async def on_exit(self, addon: "RetryAddon"): + if self.exit_func: + await self.exit_func(addon) + + def __str__(self): + return f"" + + +async def on_normal_enter(addon: "RetryAddon"): + addon.reset_all() + + +async def on_refresh_enter(addon: "RetryAddon"): + addon.refresh_count += 1 + await addon.refresh_client() + logger.warning(f"当前刷新次数: {addon.refresh_count}/{addon.max_refresh_count}") + + +async def on_raise_enter(addon: "RetryAddon"): + if random.random() < 0.1236: + await addon.refresh_client() + logger.warning("触发随机刷新") + + +class RetryState(StateMixin, Enum): + NROMAL = "NORMAL", on_normal_enter + REFRESH = "REFRESH", on_refresh_enter + BACKOFF = "BACKOFF" + RAISE = "RAISE", on_raise_enter + + def __str__(self): + return f"" + + +class RetryEvent(StrEnum): + REQUEST_AND_SUCCESS = "request_and_success" + REQUEST_AND_RAISE = "request_and_raise" + IN_BACKOFF_TIME = "in_backoff_time" + + def __str__(self): + return f"" + + +@dataclass +class RetryAddon(Generic[TBilibili]): + bilibili_platform: TBilibili | None = None + refresh_count: int = 0 + backoff_count: int = 0 + backoff_finish_time: datetime | None = None + + @property + def max_refresh_count(cls): + return 3 + + @property + def max_backoff_count(self): + return 3 + + @property + def backoff_timedelta(self): + return timedelta(minutes=5) + + async def refresh_client(self): + if self.bilibili_platform: + await self.bilibili_platform.ctx.refresh_client() + else: + raise RuntimeError("未设置 bilibili_platform") + + def reset_all(self): + self.refresh_count = 0 + self.backoff_count = 0 + self.backoff_finish_time = None + + def record_backoff_finish_time(self): + self.backoff_finish_time = ( + datetime.now() + + self.backoff_timedelta * self.backoff_count**2 + # + timedelta(seconds=random.randint(1, 60)) # jitter + ) + logger.trace(f"set backoff finish time: {self.backoff_finish_time}") + + def is_in_backoff_time(self): + """是否在指数回避时间内""" + # 指数回避 + if not self.backoff_finish_time: + logger.trace("not have backoff_finish_time") + return False + + logger.trace(f"now: {datetime.now()}, backoff_finish_time: {self.backoff_finish_time}") + return datetime.now() < self.backoff_finish_time + + +async def action_log(from_: RetryState, event: RetryEvent, to: RetryState, addon: RetryAddon) -> ActionReturn: + logger.debug(f"{from_} -> {to}, by {event}") + + +async def action_up_to_backoff(from_: RetryState, event: RetryEvent, to: RetryState, addon: RetryAddon) -> ActionReturn: + addon.refresh_count = 0 + addon.backoff_count += 1 + addon.record_backoff_finish_time() + logger.warning( + f"当前已回避次数: {addon.backoff_count}/{addon.max_backoff_count}, 本次回避时间至 {addon.backoff_finish_time}" + ) + + +async def action_back_to_refresh( + from_: RetryState, event: RetryEvent, to: RetryState, addon: RetryAddon +) -> ActionReturn: + addon.backoff_finish_time = None + logger.debug("back to refresh state") + + +async def is_reach_max_refresh(addon: RetryAddon) -> bool: + return addon.refresh_count > addon.max_refresh_count - 1 + + +async def is_reach_max_backoff(addon: RetryAddon) -> bool: + return addon.backoff_count > addon.max_backoff_count - 1 + + +async def is_out_backoff_time(addon: RetryAddon) -> bool: + return not addon.is_in_backoff_time() + + +RETRY_GRAPH: StateGraph[RetryState, RetryEvent, RetryAddon] = { + "transitions": { + RetryState.NROMAL: { + RetryEvent.REQUEST_AND_SUCCESS: Transition(action_log, RetryState.NROMAL), + RetryEvent.REQUEST_AND_RAISE: Transition(action_log, RetryState.REFRESH), + }, + RetryState.REFRESH: { + RetryEvent.REQUEST_AND_SUCCESS: Transition(action_log, RetryState.NROMAL), + RetryEvent.REQUEST_AND_RAISE: [ + Transition(action_log, RetryState.REFRESH), + Transition( + action_up_to_backoff, + RetryState.BACKOFF, + { + Condition(is_reach_max_refresh), + Condition(is_reach_max_backoff, not_=True), + }, + ), + Transition( + action_log, + RetryState.RAISE, + { + Condition(is_reach_max_refresh), + Condition(is_reach_max_backoff), + }, + ), + ], + }, + RetryState.BACKOFF: { + RetryEvent.IN_BACKOFF_TIME: [ + Transition(action_log, RetryState.BACKOFF), + Transition(action_back_to_refresh, RetryState.REFRESH, {Condition(is_out_backoff_time)}), + ], + }, + RetryState.RAISE: { + RetryEvent.REQUEST_AND_SUCCESS: Transition(action_log, RetryState.NROMAL), + RetryEvent.REQUEST_AND_RAISE: Transition(action_log, RetryState.RAISE), + }, + }, + "initial": RetryState.NROMAL, +} + + +class RetryFSM(FSM[RetryState, RetryEvent, RetryAddon[TBilibili]]): + @override + async def start(self, bls: TBilibili): + self.addon.bilibili_platform = bls + await super().start() + + @override + async def reset(self): + self.addon.reset_all() + await super().reset() + + +# FIXME: 拿出来是方便测试了,但全局单例会导致所有被装饰的函数共享状态,有待改进 +_retry_fsm = RetryFSM(RETRY_GRAPH, RetryAddon["Bilibili"]()) + + +def retry_for_352(api_func: Callable[[TBilibili, Target], Awaitable[list[DynRawPost]]]): + # _retry_fsm = RetryFSM(RETRY_GRAPH, RetryAddon[TBilibili]()) + + @wraps(api_func) + async def wrapper(bls: TBilibili, *args, **kwargs) -> list[DynRawPost]: + # nonlocal _retry_fsm + if not _retry_fsm.started: + await _retry_fsm.start(bls) + + match _retry_fsm.current_state: + case RetryState.NROMAL | RetryState.REFRESH | RetryState.RAISE: + try: + res = await api_func(bls, *args, **kwargs) + except ApiCode352Error: + logger.error("API 352 错误") + await _retry_fsm.emit(RetryEvent.REQUEST_AND_RAISE) + return [] + else: + await _retry_fsm.emit(RetryEvent.REQUEST_AND_SUCCESS) + return res + case RetryState.BACKOFF: + logger.warning("回避中,不请求") + await _retry_fsm.emit(RetryEvent.IN_BACKOFF_TIME) + return [] + case _: + assert_never(_retry_fsm.current_state) + + return wrapper diff --git a/nonebot_bison/platform/bilibili/scheduler.py b/nonebot_bison/platform/bilibili/scheduler.py index 935a7f8..2c0f2a3 100644 --- a/nonebot_bison/platform/bilibili/scheduler.py +++ b/nonebot_bison/platform/bilibili/scheduler.py @@ -1,5 +1,6 @@ -from random import randint +import random from typing_extensions import override +from typing import TYPE_CHECKING, TypeVar from httpx import AsyncClient from nonebot import logger, require @@ -8,9 +9,14 @@ from playwright.async_api import Cookie from nonebot_bison.types import Target from nonebot_bison.utils import Site, ClientManager, http_client +if TYPE_CHECKING: + from .platforms import Bilibili + require("nonebot_plugin_htmlrender") from nonebot_plugin_htmlrender import get_browser +B = TypeVar("B", bound="Bilibili") + class BilibiliClientManager(ClientManager): _client: AsyncClient @@ -22,7 +28,7 @@ class BilibiliClientManager(ClientManager): async def _get_cookies(self) -> list[Cookie]: browser = await get_browser() async with await browser.new_page() as page: - await page.goto(f"https://space.bilibili.com/{randint(1, 1000)}/dynamic") + await page.goto(f"https://space.bilibili.com/{random.randint(1, 1000)}/dynamic") await page.wait_for_load_state("load") cookies = await page.context.cookies() @@ -62,7 +68,7 @@ class BilibiliClientManager(ClientManager): class BilibiliSite(Site): name = "bilibili.com" - schedule_setting = {"seconds": 30} + schedule_setting = {"seconds": 50} schedule_type = "interval" client_mgr = BilibiliClientManager require_browser = True diff --git a/poetry.lock b/poetry.lock index 64487f8..9f3bf1e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1119,13 +1119,13 @@ reference = "offical-source" [[package]] name = "exceptiongroup" -version = "1.2.1" +version = "1.2.2" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" files = [ - {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, - {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, ] [package.extras] @@ -1195,13 +1195,13 @@ reference = "offical-source" [[package]] name = "fastapi" -version = "0.111.0" +version = "0.111.1" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = false python-versions = ">=3.8" files = [ - {file = "fastapi-0.111.0-py3-none-any.whl", hash = "sha256:97ecbf994be0bcbdadedf88c3150252bed7b2087075ac99735403b1b76cc8fc0"}, - {file = "fastapi-0.111.0.tar.gz", hash = "sha256:b9db9dd147c91cb8b769f7183535773d8741dd46f9dc6676cd82eab510228cd7"}, + {file = "fastapi-0.111.1-py3-none-any.whl", hash = "sha256:4f51cfa25d72f9fbc3280832e84b32494cf186f50158d364a8765aabf22587bf"}, + {file = "fastapi-0.111.1.tar.gz", hash = "sha256:ddd1ac34cb1f76c2e2d7f8545a4bcb5463bce4834e81abf0b189e0c359ab2413"}, ] [package.dependencies] @@ -1209,12 +1209,10 @@ email_validator = ">=2.0.0" fastapi-cli = ">=0.0.2" httpx = ">=0.23.0" jinja2 = ">=2.11.2" -orjson = ">=3.2.1" pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" python-multipart = ">=0.0.7" starlette = ">=0.37.2,<0.38.0" typing-extensions = ">=4.8.0" -ujson = ">=4.0.1,<4.0.2 || >4.0.2,<4.1.0 || >4.1.0,<4.2.0 || >4.2.0,<4.3.0 || >4.3.0,<5.0.0 || >5.0.0,<5.1.0 || >5.1.0" uvicorn = {version = ">=0.12.0", extras = ["standard"]} [package.extras] @@ -1319,6 +1317,25 @@ type = "legacy" url = "https://pypi.org/simple" reference = "offical-source" +[[package]] +name = "freezegun" +version = "1.5.1" +description = "Let your Python tests travel through time" +optional = false +python-versions = ">=3.7" +files = [ + {file = "freezegun-1.5.1-py3-none-any.whl", hash = "sha256:bf111d7138a8abe55ab48a71755673dbaa4ab87f4cff5634a4442dfec34c15f1"}, + {file = "freezegun-1.5.1.tar.gz", hash = "sha256:b29dedfcda6d5e8e083ce71b2b542753ad48cfec44037b3fc79702e2980a89e9"}, +] + +[package.dependencies] +python-dateutil = ">=2.7" + +[package.source] +type = "legacy" +url = "https://pypi.org/simple" +reference = "offical-source" + [[package]] name = "frozenlist" version = "1.4.1" @@ -2876,71 +2893,6 @@ type = "legacy" url = "https://pypi.org/simple" reference = "offical-source" -[[package]] -name = "orjson" -version = "3.10.6" -description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" -optional = false -python-versions = ">=3.8" -files = [ - {file = "orjson-3.10.6-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:fb0ee33124db6eaa517d00890fc1a55c3bfe1cf78ba4a8899d71a06f2d6ff5c7"}, - {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c1c4b53b24a4c06547ce43e5fee6ec4e0d8fe2d597f4647fc033fd205707365"}, - {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eadc8fd310edb4bdbd333374f2c8fec6794bbbae99b592f448d8214a5e4050c0"}, - {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:61272a5aec2b2661f4fa2b37c907ce9701e821b2c1285d5c3ab0207ebd358d38"}, - {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57985ee7e91d6214c837936dc1608f40f330a6b88bb13f5a57ce5257807da143"}, - {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:633a3b31d9d7c9f02d49c4ab4d0a86065c4a6f6adc297d63d272e043472acab5"}, - {file = "orjson-3.10.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1c680b269d33ec444afe2bdc647c9eb73166fa47a16d9a75ee56a374f4a45f43"}, - {file = "orjson-3.10.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f759503a97a6ace19e55461395ab0d618b5a117e8d0fbb20e70cfd68a47327f2"}, - {file = "orjson-3.10.6-cp310-none-win32.whl", hash = "sha256:95a0cce17f969fb5391762e5719575217bd10ac5a189d1979442ee54456393f3"}, - {file = "orjson-3.10.6-cp310-none-win_amd64.whl", hash = "sha256:df25d9271270ba2133cc88ee83c318372bdc0f2cd6f32e7a450809a111efc45c"}, - {file = "orjson-3.10.6-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b1ec490e10d2a77c345def52599311849fc063ae0e67cf4f84528073152bb2ba"}, - {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55d43d3feb8f19d07e9f01e5b9be4f28801cf7c60d0fa0d279951b18fae1932b"}, - {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac3045267e98fe749408eee1593a142e02357c5c99be0802185ef2170086a863"}, - {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c27bc6a28ae95923350ab382c57113abd38f3928af3c80be6f2ba7eb8d8db0b0"}, - {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d27456491ca79532d11e507cadca37fb8c9324a3976294f68fb1eff2dc6ced5a"}, - {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05ac3d3916023745aa3b3b388e91b9166be1ca02b7c7e41045da6d12985685f0"}, - {file = "orjson-3.10.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1335d4ef59ab85cab66fe73fd7a4e881c298ee7f63ede918b7faa1b27cbe5212"}, - {file = "orjson-3.10.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4bbc6d0af24c1575edc79994c20e1b29e6fb3c6a570371306db0993ecf144dc5"}, - {file = "orjson-3.10.6-cp311-none-win32.whl", hash = "sha256:450e39ab1f7694465060a0550b3f6d328d20297bf2e06aa947b97c21e5241fbd"}, - {file = "orjson-3.10.6-cp311-none-win_amd64.whl", hash = "sha256:227df19441372610b20e05bdb906e1742ec2ad7a66ac8350dcfd29a63014a83b"}, - {file = "orjson-3.10.6-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:ea2977b21f8d5d9b758bb3f344a75e55ca78e3ff85595d248eee813ae23ecdfb"}, - {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b6f3d167d13a16ed263b52dbfedff52c962bfd3d270b46b7518365bcc2121eed"}, - {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f710f346e4c44a4e8bdf23daa974faede58f83334289df80bc9cd12fe82573c7"}, - {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7275664f84e027dcb1ad5200b8b18373e9c669b2a9ec33d410c40f5ccf4b257e"}, - {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0943e4c701196b23c240b3d10ed8ecd674f03089198cf503105b474a4f77f21f"}, - {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:446dee5a491b5bc7d8f825d80d9637e7af43f86a331207b9c9610e2f93fee22a"}, - {file = "orjson-3.10.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:64c81456d2a050d380786413786b057983892db105516639cb5d3ee3c7fd5148"}, - {file = "orjson-3.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:960db0e31c4e52fa0fc3ecbaea5b2d3b58f379e32a95ae6b0ebeaa25b93dfd34"}, - {file = "orjson-3.10.6-cp312-none-win32.whl", hash = "sha256:a6ea7afb5b30b2317e0bee03c8d34c8181bc5a36f2afd4d0952f378972c4efd5"}, - {file = "orjson-3.10.6-cp312-none-win_amd64.whl", hash = "sha256:874ce88264b7e655dde4aeaacdc8fd772a7962faadfb41abe63e2a4861abc3dc"}, - {file = "orjson-3.10.6-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:66680eae4c4e7fc193d91cfc1353ad6d01b4801ae9b5314f17e11ba55e934183"}, - {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:caff75b425db5ef8e8f23af93c80f072f97b4fb3afd4af44482905c9f588da28"}, - {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3722fddb821b6036fd2a3c814f6bd9b57a89dc6337b9924ecd614ebce3271394"}, - {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2c116072a8533f2fec435fde4d134610f806bdac20188c7bd2081f3e9e0133f"}, - {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6eeb13218c8cf34c61912e9df2de2853f1d009de0e46ea09ccdf3d757896af0a"}, - {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:965a916373382674e323c957d560b953d81d7a8603fbeee26f7b8248638bd48b"}, - {file = "orjson-3.10.6-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:03c95484d53ed8e479cade8628c9cea00fd9d67f5554764a1110e0d5aa2de96e"}, - {file = "orjson-3.10.6-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:e060748a04cccf1e0a6f2358dffea9c080b849a4a68c28b1b907f272b5127e9b"}, - {file = "orjson-3.10.6-cp38-none-win32.whl", hash = "sha256:738dbe3ef909c4b019d69afc19caf6b5ed0e2f1c786b5d6215fbb7539246e4c6"}, - {file = "orjson-3.10.6-cp38-none-win_amd64.whl", hash = "sha256:d40f839dddf6a7d77114fe6b8a70218556408c71d4d6e29413bb5f150a692ff7"}, - {file = "orjson-3.10.6-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:697a35a083c4f834807a6232b3e62c8b280f7a44ad0b759fd4dce748951e70db"}, - {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd502f96bf5ea9a61cbc0b2b5900d0dd68aa0da197179042bdd2be67e51a1e4b"}, - {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f215789fb1667cdc874c1b8af6a84dc939fd802bf293a8334fce185c79cd359b"}, - {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2debd8ddce948a8c0938c8c93ade191d2f4ba4649a54302a7da905a81f00b56"}, - {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5410111d7b6681d4b0d65e0f58a13be588d01b473822483f77f513c7f93bd3b2"}, - {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb1f28a137337fdc18384079fa5726810681055b32b92253fa15ae5656e1dddb"}, - {file = "orjson-3.10.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:bf2fbbce5fe7cd1aa177ea3eab2b8e6a6bc6e8592e4279ed3db2d62e57c0e1b2"}, - {file = "orjson-3.10.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:79b9b9e33bd4c517445a62b90ca0cc279b0f1f3970655c3df9e608bc3f91741a"}, - {file = "orjson-3.10.6-cp39-none-win32.whl", hash = "sha256:30b0a09a2014e621b1adf66a4f705f0809358350a757508ee80209b2d8dae219"}, - {file = "orjson-3.10.6-cp39-none-win_amd64.whl", hash = "sha256:49e3bc615652617d463069f91b867a4458114c5b104e13b7ae6872e5f79d0844"}, - {file = "orjson-3.10.6.tar.gz", hash = "sha256:e54b63d0a7c6c54a5f5f726bc93a2078111ef060fec4ecbf34c5db800ca3b3a7"}, -] - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "offical-source" - [[package]] name = "packaging" version = "24.1" @@ -4218,29 +4170,29 @@ reference = "offical-source" [[package]] name = "ruff" -version = "0.5.1" +version = "0.5.2" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.5.1-py3-none-linux_armv6l.whl", hash = "sha256:6ecf968fcf94d942d42b700af18ede94b07521bd188aaf2cd7bc898dd8cb63b6"}, - {file = "ruff-0.5.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:204fb0a472f00f2e6280a7c8c7c066e11e20e23a37557d63045bf27a616ba61c"}, - {file = "ruff-0.5.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d235968460e8758d1e1297e1de59a38d94102f60cafb4d5382033c324404ee9d"}, - {file = "ruff-0.5.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38beace10b8d5f9b6bdc91619310af6d63dd2019f3fb2d17a2da26360d7962fa"}, - {file = "ruff-0.5.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e478d2f09cf06add143cf8c4540ef77b6599191e0c50ed976582f06e588c994"}, - {file = "ruff-0.5.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f0368d765eec8247b8550251c49ebb20554cc4e812f383ff9f5bf0d5d94190b0"}, - {file = "ruff-0.5.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:3a9a9a1b582e37669b0138b7c1d9d60b9edac880b80eb2baba6d0e566bdeca4d"}, - {file = "ruff-0.5.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bdd9f723e16003623423affabcc0a807a66552ee6a29f90eddad87a40c750b78"}, - {file = "ruff-0.5.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:be9fd62c1e99539da05fcdc1e90d20f74aec1b7a1613463ed77870057cd6bd96"}, - {file = "ruff-0.5.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e216fc75a80ea1fbd96af94a6233d90190d5b65cc3d5dfacf2bd48c3e067d3e1"}, - {file = "ruff-0.5.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c4c2112e9883a40967827d5c24803525145e7dab315497fae149764979ac7929"}, - {file = "ruff-0.5.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dfaf11c8a116394da3b65cd4b36de30d8552fa45b8119b9ef5ca6638ab964fa3"}, - {file = "ruff-0.5.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d7ceb9b2fe700ee09a0c6b192c5ef03c56eb82a0514218d8ff700f6ade004108"}, - {file = "ruff-0.5.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:bac6288e82f6296f82ed5285f597713acb2a6ae26618ffc6b429c597b392535c"}, - {file = "ruff-0.5.1-py3-none-win32.whl", hash = "sha256:5c441d9c24ec09e1cb190a04535c5379b36b73c4bc20aa180c54812c27d1cca4"}, - {file = "ruff-0.5.1-py3-none-win_amd64.whl", hash = "sha256:b1789bf2cd3d1b5a7d38397cac1398ddf3ad7f73f4de01b1e913e2abc7dfc51d"}, - {file = "ruff-0.5.1-py3-none-win_arm64.whl", hash = "sha256:2875b7596a740cbbd492f32d24be73e545a4ce0a3daf51e4f4e609962bfd3cd2"}, - {file = "ruff-0.5.1.tar.gz", hash = "sha256:3164488aebd89b1745b47fd00604fb4358d774465f20d1fcd907f9c0fc1b0655"}, + {file = "ruff-0.5.2-py3-none-linux_armv6l.whl", hash = "sha256:7bab8345df60f9368d5f4594bfb8b71157496b44c30ff035d1d01972e764d3be"}, + {file = "ruff-0.5.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:1aa7acad382ada0189dbe76095cf0a36cd0036779607c397ffdea16517f535b1"}, + {file = "ruff-0.5.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:aec618d5a0cdba5592c60c2dee7d9c865180627f1a4a691257dea14ac1aa264d"}, + {file = "ruff-0.5.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0b62adc5ce81780ff04077e88bac0986363e4a3260ad3ef11ae9c14aa0e67ef"}, + {file = "ruff-0.5.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dc42ebf56ede83cb080a50eba35a06e636775649a1ffd03dc986533f878702a3"}, + {file = "ruff-0.5.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c15c6e9f88c67ffa442681365d11df38afb11059fc44238e71a9d9f1fd51de70"}, + {file = "ruff-0.5.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d3de9a5960f72c335ef00763d861fc5005ef0644cb260ba1b5a115a102157251"}, + {file = "ruff-0.5.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fe5a968ae933e8f7627a7b2fc8893336ac2be0eb0aace762d3421f6e8f7b7f83"}, + {file = "ruff-0.5.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a04f54a9018f75615ae52f36ea1c5515e356e5d5e214b22609ddb546baef7132"}, + {file = "ruff-0.5.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ed02fb52e3741f0738db5f93e10ae0fb5c71eb33a4f2ba87c9a2fa97462a649"}, + {file = "ruff-0.5.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3cf8fe659f6362530435d97d738eb413e9f090e7e993f88711b0377fbdc99f60"}, + {file = "ruff-0.5.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:237a37e673e9f3cbfff0d2243e797c4862a44c93d2f52a52021c1a1b0899f846"}, + {file = "ruff-0.5.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2a2949ce7c1cbd8317432ada80fe32156df825b2fd611688814c8557824ef060"}, + {file = "ruff-0.5.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:481af57c8e99da92ad168924fd82220266043c8255942a1cb87958b108ac9335"}, + {file = "ruff-0.5.2-py3-none-win32.whl", hash = "sha256:f1aea290c56d913e363066d83d3fc26848814a1fed3d72144ff9c930e8c7c718"}, + {file = "ruff-0.5.2-py3-none-win_amd64.whl", hash = "sha256:8532660b72b5d94d2a0a7a27ae7b9b40053662d00357bb2a6864dd7e38819084"}, + {file = "ruff-0.5.2-py3-none-win_arm64.whl", hash = "sha256:73439805c5cb68f364d826a5c5c4b6c798ded6b7ebaa4011f01ce6c94e4d5583"}, + {file = "ruff-0.5.2.tar.gz", hash = "sha256:2c0df2d2de685433794a14d8d2e240df619b748fbe3367346baa519d8e6f1ca2"}, ] [package.source] @@ -4685,98 +4637,6 @@ type = "legacy" url = "https://pypi.org/simple" reference = "offical-source" -[[package]] -name = "ujson" -version = "5.10.0" -description = "Ultra fast JSON encoder and decoder for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "ujson-5.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2601aa9ecdbee1118a1c2065323bda35e2c5a2cf0797ef4522d485f9d3ef65bd"}, - {file = "ujson-5.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:348898dd702fc1c4f1051bc3aacbf894caa0927fe2c53e68679c073375f732cf"}, - {file = "ujson-5.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22cffecf73391e8abd65ef5f4e4dd523162a3399d5e84faa6aebbf9583df86d6"}, - {file = "ujson-5.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26b0e2d2366543c1bb4fbd457446f00b0187a2bddf93148ac2da07a53fe51569"}, - {file = "ujson-5.10.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:caf270c6dba1be7a41125cd1e4fc7ba384bf564650beef0df2dd21a00b7f5770"}, - {file = "ujson-5.10.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a245d59f2ffe750446292b0094244df163c3dc96b3ce152a2c837a44e7cda9d1"}, - {file = "ujson-5.10.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:94a87f6e151c5f483d7d54ceef83b45d3a9cca7a9cb453dbdbb3f5a6f64033f5"}, - {file = "ujson-5.10.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:29b443c4c0a113bcbb792c88bea67b675c7ca3ca80c3474784e08bba01c18d51"}, - {file = "ujson-5.10.0-cp310-cp310-win32.whl", hash = "sha256:c18610b9ccd2874950faf474692deee4223a994251bc0a083c114671b64e6518"}, - {file = "ujson-5.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:924f7318c31874d6bb44d9ee1900167ca32aa9b69389b98ecbde34c1698a250f"}, - {file = "ujson-5.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a5b366812c90e69d0f379a53648be10a5db38f9d4ad212b60af00bd4048d0f00"}, - {file = "ujson-5.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:502bf475781e8167f0f9d0e41cd32879d120a524b22358e7f205294224c71126"}, - {file = "ujson-5.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b91b5d0d9d283e085e821651184a647699430705b15bf274c7896f23fe9c9d8"}, - {file = "ujson-5.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:129e39af3a6d85b9c26d5577169c21d53821d8cf68e079060602e861c6e5da1b"}, - {file = "ujson-5.10.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f77b74475c462cb8b88680471193064d3e715c7c6074b1c8c412cb526466efe9"}, - {file = "ujson-5.10.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7ec0ca8c415e81aa4123501fee7f761abf4b7f386aad348501a26940beb1860f"}, - {file = "ujson-5.10.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab13a2a9e0b2865a6c6db9271f4b46af1c7476bfd51af1f64585e919b7c07fd4"}, - {file = "ujson-5.10.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:57aaf98b92d72fc70886b5a0e1a1ca52c2320377360341715dd3933a18e827b1"}, - {file = "ujson-5.10.0-cp311-cp311-win32.whl", hash = "sha256:2987713a490ceb27edff77fb184ed09acdc565db700ee852823c3dc3cffe455f"}, - {file = "ujson-5.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:f00ea7e00447918ee0eff2422c4add4c5752b1b60e88fcb3c067d4a21049a720"}, - {file = "ujson-5.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:98ba15d8cbc481ce55695beee9f063189dce91a4b08bc1d03e7f0152cd4bbdd5"}, - {file = "ujson-5.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9d2edbf1556e4f56e50fab7d8ff993dbad7f54bac68eacdd27a8f55f433578e"}, - {file = "ujson-5.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6627029ae4f52d0e1a2451768c2c37c0c814ffc04f796eb36244cf16b8e57043"}, - {file = "ujson-5.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8ccb77b3e40b151e20519c6ae6d89bfe3f4c14e8e210d910287f778368bb3d1"}, - {file = "ujson-5.10.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3caf9cd64abfeb11a3b661329085c5e167abbe15256b3b68cb5d914ba7396f3"}, - {file = "ujson-5.10.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6e32abdce572e3a8c3d02c886c704a38a1b015a1fb858004e03d20ca7cecbb21"}, - {file = "ujson-5.10.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a65b6af4d903103ee7b6f4f5b85f1bfd0c90ba4eeac6421aae436c9988aa64a2"}, - {file = "ujson-5.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:604a046d966457b6cdcacc5aa2ec5314f0e8c42bae52842c1e6fa02ea4bda42e"}, - {file = "ujson-5.10.0-cp312-cp312-win32.whl", hash = "sha256:6dea1c8b4fc921bf78a8ff00bbd2bfe166345f5536c510671bccececb187c80e"}, - {file = "ujson-5.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:38665e7d8290188b1e0d57d584eb8110951a9591363316dd41cf8686ab1d0abc"}, - {file = "ujson-5.10.0-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:618efd84dc1acbd6bff8eaa736bb6c074bfa8b8a98f55b61c38d4ca2c1f7f287"}, - {file = "ujson-5.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:38d5d36b4aedfe81dfe251f76c0467399d575d1395a1755de391e58985ab1c2e"}, - {file = "ujson-5.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67079b1f9fb29ed9a2914acf4ef6c02844b3153913eb735d4bf287ee1db6e557"}, - {file = "ujson-5.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7d0e0ceeb8fe2468c70ec0c37b439dd554e2aa539a8a56365fd761edb418988"}, - {file = "ujson-5.10.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:59e02cd37bc7c44d587a0ba45347cc815fb7a5fe48de16bf05caa5f7d0d2e816"}, - {file = "ujson-5.10.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2a890b706b64e0065f02577bf6d8ca3b66c11a5e81fb75d757233a38c07a1f20"}, - {file = "ujson-5.10.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:621e34b4632c740ecb491efc7f1fcb4f74b48ddb55e65221995e74e2d00bbff0"}, - {file = "ujson-5.10.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b9500e61fce0cfc86168b248104e954fead61f9be213087153d272e817ec7b4f"}, - {file = "ujson-5.10.0-cp313-cp313-win32.whl", hash = "sha256:4c4fc16f11ac1612f05b6f5781b384716719547e142cfd67b65d035bd85af165"}, - {file = "ujson-5.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:4573fd1695932d4f619928fd09d5d03d917274381649ade4328091ceca175539"}, - {file = "ujson-5.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a984a3131da7f07563057db1c3020b1350a3e27a8ec46ccbfbf21e5928a43050"}, - {file = "ujson-5.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:73814cd1b9db6fc3270e9d8fe3b19f9f89e78ee9d71e8bd6c9a626aeaeaf16bd"}, - {file = "ujson-5.10.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61e1591ed9376e5eddda202ec229eddc56c612b61ac6ad07f96b91460bb6c2fb"}, - {file = "ujson-5.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2c75269f8205b2690db4572a4a36fe47cd1338e4368bc73a7a0e48789e2e35a"}, - {file = "ujson-5.10.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7223f41e5bf1f919cd8d073e35b229295aa8e0f7b5de07ed1c8fddac63a6bc5d"}, - {file = "ujson-5.10.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc2fd6b3067c0782e7002ac3b38cf48608ee6366ff176bbd02cf969c9c20fe"}, - {file = "ujson-5.10.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:232cc85f8ee3c454c115455195a205074a56ff42608fd6b942aa4c378ac14dd7"}, - {file = "ujson-5.10.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:cc6139531f13148055d691e442e4bc6601f6dba1e6d521b1585d4788ab0bfad4"}, - {file = "ujson-5.10.0-cp38-cp38-win32.whl", hash = "sha256:e7ce306a42b6b93ca47ac4a3b96683ca554f6d35dd8adc5acfcd55096c8dfcb8"}, - {file = "ujson-5.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:e82d4bb2138ab05e18f089a83b6564fee28048771eb63cdecf4b9b549de8a2cc"}, - {file = "ujson-5.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dfef2814c6b3291c3c5f10065f745a1307d86019dbd7ea50e83504950136ed5b"}, - {file = "ujson-5.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4734ee0745d5928d0ba3a213647f1c4a74a2a28edc6d27b2d6d5bd9fa4319e27"}, - {file = "ujson-5.10.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d47ebb01bd865fdea43da56254a3930a413f0c5590372a1241514abae8aa7c76"}, - {file = "ujson-5.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dee5e97c2496874acbf1d3e37b521dd1f307349ed955e62d1d2f05382bc36dd5"}, - {file = "ujson-5.10.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7490655a2272a2d0b072ef16b0b58ee462f4973a8f6bbe64917ce5e0a256f9c0"}, - {file = "ujson-5.10.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ba17799fcddaddf5c1f75a4ba3fd6441f6a4f1e9173f8a786b42450851bd74f1"}, - {file = "ujson-5.10.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2aff2985cef314f21d0fecc56027505804bc78802c0121343874741650a4d3d1"}, - {file = "ujson-5.10.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ad88ac75c432674d05b61184178635d44901eb749786c8eb08c102330e6e8996"}, - {file = "ujson-5.10.0-cp39-cp39-win32.whl", hash = "sha256:2544912a71da4ff8c4f7ab5606f947d7299971bdd25a45e008e467ca638d13c9"}, - {file = "ujson-5.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:3ff201d62b1b177a46f113bb43ad300b424b7847f9c5d38b1b4ad8f75d4a282a"}, - {file = "ujson-5.10.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5b6fee72fa77dc172a28f21693f64d93166534c263adb3f96c413ccc85ef6e64"}, - {file = "ujson-5.10.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:61d0af13a9af01d9f26d2331ce49bb5ac1fb9c814964018ac8df605b5422dcb3"}, - {file = "ujson-5.10.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecb24f0bdd899d368b715c9e6664166cf694d1e57be73f17759573a6986dd95a"}, - {file = "ujson-5.10.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fbd8fd427f57a03cff3ad6574b5e299131585d9727c8c366da4624a9069ed746"}, - {file = "ujson-5.10.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:beeaf1c48e32f07d8820c705ff8e645f8afa690cca1544adba4ebfa067efdc88"}, - {file = "ujson-5.10.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:baed37ea46d756aca2955e99525cc02d9181de67f25515c468856c38d52b5f3b"}, - {file = "ujson-5.10.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7663960f08cd5a2bb152f5ee3992e1af7690a64c0e26d31ba7b3ff5b2ee66337"}, - {file = "ujson-5.10.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:d8640fb4072d36b08e95a3a380ba65779d356b2fee8696afeb7794cf0902d0a1"}, - {file = "ujson-5.10.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78778a3aa7aafb11e7ddca4e29f46bc5139131037ad628cc10936764282d6753"}, - {file = "ujson-5.10.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0111b27f2d5c820e7f2dbad7d48e3338c824e7ac4d2a12da3dc6061cc39c8e6"}, - {file = "ujson-5.10.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:c66962ca7565605b355a9ed478292da628b8f18c0f2793021ca4425abf8b01e5"}, - {file = "ujson-5.10.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ba43cc34cce49cf2d4bc76401a754a81202d8aa926d0e2b79f0ee258cb15d3a4"}, - {file = "ujson-5.10.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:ac56eb983edce27e7f51d05bc8dd820586c6e6be1c5216a6809b0c668bb312b8"}, - {file = "ujson-5.10.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f44bd4b23a0e723bf8b10628288c2c7c335161d6840013d4d5de20e48551773b"}, - {file = "ujson-5.10.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c10f4654e5326ec14a46bcdeb2b685d4ada6911050aa8baaf3501e57024b804"}, - {file = "ujson-5.10.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0de4971a89a762398006e844ae394bd46991f7c385d7a6a3b93ba229e6dac17e"}, - {file = "ujson-5.10.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e1402f0564a97d2a52310ae10a64d25bcef94f8dd643fcf5d310219d915484f7"}, - {file = "ujson-5.10.0.tar.gz", hash = "sha256:b3cd8f3c5d8c7738257f1018880444f7b7d9b66232c64649f562d7ba86ad4bc1"}, -] - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "offical-source" - [[package]] name = "urllib3" version = "2.2.2" @@ -5252,4 +5112,4 @@ yaml = [] [metadata] lock-version = "2.0" python-versions = ">=3.10,<4.0.0" -content-hash = "f803840e2fcf6c5731e3bf842cf898125c464ce52ac4fbbb960258c026407110" +content-hash = "d685583bbdceb8277e33e71357fb9c9e781c6388911cd09aa5a0153605ae38ae" diff --git a/pyproject.toml b/pyproject.toml index 2d787d8..66b8eab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,6 +62,7 @@ pytest-cov = ">=5.0.0,<6" pytest-mock = "^3.14.0" pytest-xdist = { extras = ["psutil"], version = "^3.6.1" } respx = ">=0.21.1,<0.22" +freezegun = "^1.5.1" [tool.poetry.group.docker] optional = true @@ -126,6 +127,7 @@ plugin_dirs = ["extra_plugins"] builtin_plugins = ["echo"] [tool.pyright] +typeCheckingMode = "basic" reportShadowedImports = false pythonVersion = "3.10" pythonPlatform = "All" diff --git a/tests/platforms/test_bilibili.py b/tests/platforms/test_bilibili.py index f953131..15a35de 100644 --- a/tests/platforms/test_bilibili.py +++ b/tests/platforms/test_bilibili.py @@ -1,11 +1,15 @@ +import random from time import time from datetime import datetime from typing import TYPE_CHECKING, Any import respx import pytest +from loguru import logger from nonebug.app import App from httpx import URL, Response +from freezegun import freeze_time +from pytest_mock import MockerFixture from nonebot.compat import model_dump, type_validate_python from .utils import get_json @@ -55,12 +59,15 @@ def without_dynamic(app: App): @pytest.mark.asyncio -async def test_retry_for_352(app: App): +async def test_retry_for_352(app: App, mocker: MockerFixture): from nonebot_bison.post import Post + from nonebot_bison.types import Target, RawPost from nonebot_bison.platform.platform import NewMessage - from nonebot_bison.types import Target, RawPost, ApiError + from nonebot_bison.platform.bilibili.platforms import ApiCode352Error from nonebot_bison.utils import ClientManager, ProcessContext, http_client - from nonebot_bison.platform.bilibili.platforms import MAX_352_RETRY_COUNT, ApiCode352Error, retry_for_352 + from nonebot_bison.platform.bilibili.retry import RetryAddon, RetryState, _retry_fsm, retry_for_352 + + mocker.patch.object(random, "random", return_value=0.0) # 稳定触发RAISE阶段的随缘刷新 now = time() raw_post_1 = {"id": 1, "text": "p1", "date": now, "tags": ["tag1"], "category": 1} @@ -118,18 +125,26 @@ async def test_retry_for_352(app: App): refresh_client_call_count = 0 async def get_client(self, target: Target | None): + logger.debug(f"call get_client: {target}, {datetime.now()}") + logger.debug(f"times: {self.get_client_call_count} + 1") self.get_client_call_count += 1 return http_client() async def get_client_for_static(self): + logger.debug(f"call get_client_for_static: {datetime.now()}") + logger.debug(f"times: {self.get_client_for_static_call_count} + 1") self.get_client_for_static_call_count += 1 return http_client() async def get_query_name_client(self): + logger.debug(f"call get_query_name_client: {datetime.now()}") + logger.debug(f"times: {self.get_query_name_client_call_count} + 1") self.get_query_name_client_call_count += 1 return http_client() async def refresh_client(self): + logger.debug(f"call refresh_client: {datetime.now()}") + logger.debug(f"times: {self.refresh_client_call_count} + 1") self.refresh_client_call_count += 1 fakebili = MockPlatform(ProcessContext(MockClientManager())) @@ -141,29 +156,83 @@ async def test_retry_for_352(app: App): assert client_mgr.refresh_client_call_count == 0 # 无异常 - res: list[dict[str, Any]] = await fakebili.get_sub_list(Target("1")) # type: ignore + res: list[dict[str, Any]] = await fakebili.get_sub_list(Target("t1")) # type: ignore assert len(res) == 1 assert res[0]["id"] == 1 assert client_mgr.get_client_call_count == 1 assert client_mgr.refresh_client_call_count == 0 - res = await fakebili.get_sub_list(Target("1")) # type: ignore + res = await fakebili.get_sub_list(Target("t1")) # type: ignore assert len(res) == 2 assert res[0]["id"] == 1 assert res[1]["id"] == 2 assert client_mgr.get_client_call_count == 2 assert client_mgr.refresh_client_call_count == 0 - # 有异常 + addon = RetryAddon() + + # 异常直到最终报错 + test_state_list: list[RetryState] = [RetryState.NROMAL] + [RetryState.REFRESH] * addon.max_refresh_count + for _ in range(addon.max_backoff_count): + test_state_list += [RetryState.BACKOFF] * 2 + test_state_list += [RetryState.REFRESH] * addon.max_refresh_count + test_state_list += [RetryState.RAISE] * 2 + + freeze_start = datetime(2024, 6, 19, 0, 0, 0, 0) + timedelta_length = addon.backoff_timedelta + fakebili.set_raise352(True) - for i in range(MAX_352_RETRY_COUNT): - res1: list[dict[str, Any]] = await fakebili.get_sub_list(Target("1")) # type: ignore - assert len(res1) == 0 - assert client_mgr.get_client_call_count == 3 + i - assert client_mgr.refresh_client_call_count == i + 1 - # 超过最大重试次数,抛出异常 - with pytest.raises(ApiError): - await fakebili.get_sub_list(Target("1")) + + for state in test_state_list: + logger.info(f"\n\nnow state should be {state}") + assert _retry_fsm.current_state == state + + with freeze_time(freeze_start): + res = await fakebili.get_sub_list(Target("t1")) # type: ignore + assert not res + + if state == RetryState.BACKOFF: + freeze_start += timedelta_length * (_retry_fsm.addon.backoff_count + 1) ** 2 + + assert client_mgr.refresh_client_call_count == 4 * 3 + 3 # refresh + raise + assert client_mgr.get_client_call_count == 2 + 4 * 3 + 3 # previous + refresh + raise + + # 重置回正常状态 + fakebili.set_raise352(False) + res = await fakebili.get_sub_list(Target("t1")) # type: ignore + assert res + + # REFRESH阶段中途正常返回 + test_state_list2 = [RetryState.NROMAL, RetryState.REFRESH, RetryState.NROMAL] + for idx, _ in enumerate(test_state_list2): + if idx == len(test_state_list2) - 1: + fakebili.set_raise352(False) + res = await fakebili.get_sub_list(Target("t1")) # type: ignore + assert res + else: + fakebili.set_raise352(True) + res = await fakebili.get_sub_list(Target("t1")) # type: ignore + assert not res + + fakebili.set_raise352(False) + # BACKOFF阶段在回避时间中 + test_state_list3 = [RetryState.NROMAL] + [RetryState.REFRESH] * addon.max_refresh_count + [RetryState.BACKOFF] + for idx, _ in enumerate(test_state_list3): + if idx == len(test_state_list3) - 1: + fakebili.set_raise352(False) + res = await fakebili.get_sub_list(Target("t1")) # type: ignore + assert not res + else: + fakebili.set_raise352(True) + res = await fakebili.get_sub_list(Target("t1")) # type: ignore + assert not res + + # 测试重置 + await _retry_fsm.reset() + + await fakebili.get_sub_list(Target("t1")) # type: ignore + + await _retry_fsm.reset() @pytest.mark.asyncio