mirror of
https://github.com/A-Minos/nonebot-plugin-tetris-stats.git
synced 2026-03-05 05:36:54 +08:00
Compare commits
62 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c02fdfc47f | |||
| 93b169fa40 | |||
| 5cb428ed71 | |||
|
|
ec392ee384 | ||
|
|
d037cf6d44 | ||
|
|
6964e9b655 | ||
|
|
7a032bf947 | ||
|
|
9a91e5ef5b | ||
|
|
5b58697fce | ||
|
|
b14cebe832 | ||
|
|
4306195ee5 | ||
|
|
ac9c6e79d9 | ||
|
|
ed035c65c1 | ||
| dc8bc9b306 | |||
| 454dd57007 | |||
| b396a6d450 | |||
| 7f584a46eb | |||
| 27518c0408 | |||
|
|
d2a3801dac | ||
| 563564ac8d | |||
| 87c87ad231 | |||
| 30515d1907 | |||
|
|
bd0a8ea447 | ||
|
|
1db1e6dbba | ||
|
|
9040aa9fba | ||
|
|
3a5f1eb266 | ||
|
|
43e927430a | ||
| e1b0918a52 | |||
| c86b2eb31b | |||
|
|
47b3f3e881 | ||
|
|
7caee587b4 | ||
|
|
28ae564e59 | ||
|
|
90dee8402d | ||
|
|
8b560e55cb | ||
|
|
3080531503 | ||
|
|
fae0088533 | ||
|
|
db9286a369 | ||
|
|
420fb29318 | ||
|
|
433a6edd3b | ||
|
|
fa81231f78 | ||
|
|
c474cf0af2 | ||
|
|
e38eb5cdff | ||
|
|
7bacf89840 | ||
|
|
4622e90995 | ||
|
|
fa8c2b11e6 | ||
|
|
2123b747af | ||
|
|
e65233d09f | ||
|
|
7e81bf6b8b | ||
|
|
c4614aa006 | ||
|
|
79a657b9f5 | ||
|
|
0164f29c1e | ||
|
|
8db56366df | ||
|
|
de0a1e4c73 | ||
|
|
3670ce7221 | ||
|
|
101ed737ab | ||
|
|
1611bf47fa | ||
|
|
e084cdb145 | ||
|
|
27258ab744 | ||
|
|
07324825e6 | ||
|
|
472becdfe0 | ||
|
|
bc87e4b16d | ||
|
|
28e2a46303 |
@@ -0,0 +1,51 @@
|
|||||||
|
"""Rename field
|
||||||
|
|
||||||
|
迁移 ID: 09d4bb60160d
|
||||||
|
父迁移: b9d65badc713
|
||||||
|
创建时间: 2024-04-23 23:42:04.541672
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
revision: str = '09d4bb60160d'
|
||||||
|
down_revision: str | Sequence[str] | None = 'b9d65badc713'
|
||||||
|
branch_labels: str | Sequence[str] | None = None
|
||||||
|
depends_on: str | Sequence[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade(name: str = '') -> None:
|
||||||
|
if name:
|
||||||
|
return
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('nonebot_plugin_tetris_stats_iorank', schema=None) as batch_op:
|
||||||
|
batch_op.alter_column('create_time', new_column_name='update_time', existing_type=sa.DateTime())
|
||||||
|
batch_op.drop_index('ix_nonebot_plugin_tetris_stats_iorank_create_time')
|
||||||
|
op.create_index(
|
||||||
|
batch_op.f('ix_nonebot_plugin_tetris_stats_iorank_update_time'),
|
||||||
|
'nonebot_plugin_tetris_stats_iorank',
|
||||||
|
['update_time'],
|
||||||
|
unique=False,
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade(name: str = '') -> None:
|
||||||
|
if name:
|
||||||
|
return
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('nonebot_plugin_tetris_stats_iorank', schema=None) as batch_op:
|
||||||
|
batch_op.alter_column('update_time', new_column_name='create_time')
|
||||||
|
batch_op.drop_index(batch_op.f('ix_nonebot_plugin_tetris_stats_iorank_update_time'))
|
||||||
|
op.create_index(
|
||||||
|
'ix_nonebot_plugin_tetris_stats_iorank_create_time',
|
||||||
|
'nonebot_plugin_tetris_stats_iorank',
|
||||||
|
['create_time'],
|
||||||
|
unique=False,
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
"""add field
|
||||||
|
|
||||||
|
迁移 ID: 0d50142b780f
|
||||||
|
父迁移: 09d4bb60160d
|
||||||
|
创建时间: 2024-04-24 14:55:08.064098
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
revision: str = '0d50142b780f'
|
||||||
|
down_revision: str | Sequence[str] | None = '09d4bb60160d'
|
||||||
|
branch_labels: str | Sequence[str] | None = None
|
||||||
|
depends_on: str | Sequence[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade(name: str = '') -> None:
|
||||||
|
if name:
|
||||||
|
return
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('nonebot_plugin_tetris_stats_iorank', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('file_hash', sa.String(length=128), nullable=True))
|
||||||
|
batch_op.create_index(
|
||||||
|
batch_op.f('ix_nonebot_plugin_tetris_stats_iorank_file_hash'), ['file_hash'], unique=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade(name: str = '') -> None:
|
||||||
|
if name:
|
||||||
|
return
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('nonebot_plugin_tetris_stats_iorank', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index(batch_op.f('ix_nonebot_plugin_tetris_stats_iorank_file_hash'))
|
||||||
|
batch_op.drop_column('file_hash')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -3,6 +3,7 @@ from datetime import datetime
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from nonebot.adapters import Message
|
from nonebot.adapters import Message
|
||||||
|
from nonebot.compat import type_validate_json
|
||||||
from nonebot_plugin_orm import Model
|
from nonebot_plugin_orm import Model
|
||||||
from pydantic import BaseModel, ValidationError
|
from pydantic import BaseModel, ValidationError
|
||||||
from sqlalchemy import JSON, DateTime, Dialect, PickleType, String, TypeDecorator
|
from sqlalchemy import JSON, DateTime, Dialect, PickleType, String, TypeDecorator
|
||||||
@@ -30,7 +31,7 @@ class PydanticType(TypeDecorator):
|
|||||||
if isinstance(value, str | bytes):
|
if isinstance(value, str | bytes):
|
||||||
for i in self.get_model():
|
for i in self.get_model():
|
||||||
try:
|
try:
|
||||||
return i.parse_raw(value)
|
return type_validate_json(i, value)
|
||||||
except ValidationError: # noqa: PERF203
|
except ValidationError: # noqa: PERF203
|
||||||
...
|
...
|
||||||
raise TypeError
|
raise TypeError
|
||||||
|
|||||||
@@ -151,19 +151,19 @@ async def _(matcher: Matcher, rank: Rank):
|
|||||||
.where(IORank.rank == rank)
|
.where(IORank.rank == rank)
|
||||||
.order_by(
|
.order_by(
|
||||||
func.abs(
|
func.abs(
|
||||||
func.julianday(IORank.create_time)
|
func.julianday(IORank.update_time)
|
||||||
- func.julianday(latest_data.create_time - timedelta(hours=24))
|
- func.julianday(latest_data.update_time - timedelta(hours=24))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.limit(1)
|
.limit(1)
|
||||||
)
|
)
|
||||||
).one()
|
).one()
|
||||||
message = ''
|
message = ''
|
||||||
if (datetime.now(UTC) - latest_data.create_time.replace(tzinfo=UTC)) > timedelta(hours=7):
|
if (datetime.now(UTC) - latest_data.update_time.replace(tzinfo=UTC)) > timedelta(hours=7):
|
||||||
message += 'Warning: 数据超过7小时未更新, 请联系Bot主人查看后台\n'
|
message += 'Warning: 数据超过7小时未更新, 请联系Bot主人查看后台\n'
|
||||||
message += f'{rank.upper()} 段 分数线 {latest_data.tr_line:.2f} TR, {latest_data.player_count} 名玩家\n'
|
message += f'{rank.upper()} 段 分数线 {latest_data.tr_line:.2f} TR, {latest_data.player_count} 名玩家\n'
|
||||||
if compare_data.id != latest_data.id:
|
if compare_data.id != latest_data.id:
|
||||||
message += f'对比 {(latest_data.create_time-compare_data.create_time).total_seconds()/3600:.2f} 小时前趋势: {f"↑{difference:.2f}" if (difference:=latest_data.tr_line-compare_data.tr_line) > 0 else f"↓{-difference:.2f}" if difference < 0 else "→"}'
|
message += f'对比 {(latest_data.update_time-compare_data.update_time).total_seconds()/3600:.2f} 小时前趋势: {f"↑{difference:.2f}" if (difference:=latest_data.tr_line-compare_data.tr_line) > 0 else f"↓{-difference:.2f}" if difference < 0 else "→"}'
|
||||||
else:
|
else:
|
||||||
message += '暂无对比数据'
|
message += '暂无对比数据'
|
||||||
avg = get_metrics(pps=latest_data.avg_pps, apm=latest_data.avg_apm, vs=latest_data.avg_vs)
|
avg = get_metrics(pps=latest_data.avg_pps, apm=latest_data.avg_apm, vs=latest_data.avg_vs)
|
||||||
@@ -188,7 +188,7 @@ async def _(matcher: Matcher, rank: Rank):
|
|||||||
f'APM: {latest_data.high_apm[1]} By: {latest_data.high_apm[0]["name"].upper()}\n'
|
f'APM: {latest_data.high_apm[1]} By: {latest_data.high_apm[0]["name"].upper()}\n'
|
||||||
f'ADPM: {max_vs.adpm} ( {max_vs.vs}vs ) By: {latest_data.high_vs[0]["name"].upper()}\n'
|
f'ADPM: {max_vs.adpm} ( {max_vs.vs}vs ) By: {latest_data.high_vs[0]["name"].upper()}\n'
|
||||||
'\n'
|
'\n'
|
||||||
f'数据更新时间: {latest_data.create_time.replace(tzinfo=UTC).astimezone(ZoneInfo("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")}'
|
f'数据更新时间: {latest_data.update_time.replace(tzinfo=UTC).astimezone(ZoneInfo("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")}'
|
||||||
)
|
)
|
||||||
await matcher.finish(message)
|
await matcher.finish(message)
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
from aiocache import Cache as ACache # type: ignore[import-untyped]
|
from aiocache import Cache as ACache # type: ignore[import-untyped]
|
||||||
|
from nonebot.compat import type_validate_json
|
||||||
from nonebot.log import logger
|
from nonebot.log import logger
|
||||||
from pydantic import parse_raw_as
|
|
||||||
|
|
||||||
from ...utils.request import Request
|
from ...utils.request import Request
|
||||||
from .schemas.base import FailedModel, SuccessModel
|
from .schemas.base import FailedModel, SuccessModel
|
||||||
@@ -18,7 +18,7 @@ class Cache:
|
|||||||
cached_data = await cls.cache.get(url)
|
cached_data = await cls.cache.get(url)
|
||||||
if cached_data is None:
|
if cached_data is None:
|
||||||
response_data = await Request.request(url)
|
response_data = await Request.request(url)
|
||||||
parsed_data: SuccessModel | FailedModel = parse_raw_as(SuccessModel | FailedModel, response_data) # type: ignore[arg-type]
|
parsed_data: SuccessModel | FailedModel = type_validate_json(SuccessModel | FailedModel, response_data) # type: ignore[arg-type]
|
||||||
if isinstance(parsed_data, SuccessModel):
|
if isinstance(parsed_data, SuccessModel):
|
||||||
await cls.cache.add(
|
await cls.cache.add(
|
||||||
url,
|
url,
|
||||||
|
|||||||
@@ -23,9 +23,8 @@ class IORank(MappedAsDataclass, Model):
|
|||||||
high_pps: Mapped[tuple[dict[str, str], float]] = mapped_column(JSON)
|
high_pps: Mapped[tuple[dict[str, str], float]] = mapped_column(JSON)
|
||||||
high_apm: Mapped[tuple[dict[str, str], float]] = mapped_column(JSON)
|
high_apm: Mapped[tuple[dict[str, str], float]] = mapped_column(JSON)
|
||||||
high_vs: Mapped[tuple[dict[str, str], float]] = mapped_column(JSON)
|
high_vs: Mapped[tuple[dict[str, str], float]] = mapped_column(JSON)
|
||||||
create_time: Mapped[datetime] = mapped_column(
|
update_time: Mapped[datetime] = mapped_column(
|
||||||
DateTime,
|
DateTime,
|
||||||
default=lambda: datetime.now(tz=UTC),
|
|
||||||
index=True,
|
index=True,
|
||||||
init=False,
|
|
||||||
)
|
)
|
||||||
|
file_hash: Mapped[str | None] = mapped_column(String(128), index=True)
|
||||||
|
|||||||
@@ -1,16 +1,21 @@
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from hashlib import sha512
|
||||||
from math import floor
|
from math import floor
|
||||||
from re import match
|
from re import match
|
||||||
from statistics import mean
|
from statistics import mean
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
|
||||||
|
from aiofiles import open
|
||||||
from nonebot import get_driver
|
from nonebot import get_driver
|
||||||
|
from nonebot.compat import type_validate_json
|
||||||
|
from nonebot.utils import run_sync
|
||||||
from nonebot_plugin_apscheduler import scheduler # type: ignore[import-untyped]
|
from nonebot_plugin_apscheduler import scheduler # type: ignore[import-untyped]
|
||||||
|
from nonebot_plugin_localstore import get_data_file # type: ignore[import-untyped]
|
||||||
from nonebot_plugin_orm import get_session
|
from nonebot_plugin_orm import get_session
|
||||||
from pydantic import parse_raw_as
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
|
from zstandard import ZstdCompressor
|
||||||
|
|
||||||
from ...db import create_or_update_bind
|
from ...db import create_or_update_bind
|
||||||
from ...utils.exception import MessageFormatError, RequestError, WhatTheFuckError
|
from ...utils.exception import MessageFormatError, RequestError, WhatTheFuckError
|
||||||
@@ -96,7 +101,7 @@ class Processor(ProcessorMeta):
|
|||||||
self.raw_response.user_info = await Cache.get(
|
self.raw_response.user_info = await Cache.get(
|
||||||
splice_url([BASE_URL, 'users/', f'{self.user.ID or self.user.name}'])
|
splice_url([BASE_URL, 'users/', f'{self.user.ID or self.user.name}'])
|
||||||
)
|
)
|
||||||
user_info: UserInfo = parse_raw_as(UserInfo, self.raw_response.user_info) # type: ignore[arg-type]
|
user_info: UserInfo = type_validate_json(UserInfo, self.raw_response.user_info) # type: ignore[arg-type]
|
||||||
if isinstance(user_info, InfoFailed):
|
if isinstance(user_info, InfoFailed):
|
||||||
raise RequestError(f'用户信息请求错误:\n{user_info.error}')
|
raise RequestError(f'用户信息请求错误:\n{user_info.error}')
|
||||||
self.processed_data.user_info = user_info
|
self.processed_data.user_info = user_info
|
||||||
@@ -108,7 +113,7 @@ class Processor(ProcessorMeta):
|
|||||||
self.raw_response.user_records = await Cache.get(
|
self.raw_response.user_records = await Cache.get(
|
||||||
splice_url([BASE_URL, 'users/', f'{self.user.ID or self.user.name}/', 'records'])
|
splice_url([BASE_URL, 'users/', f'{self.user.ID or self.user.name}/', 'records'])
|
||||||
)
|
)
|
||||||
user_records: UserRecords = parse_raw_as(UserRecords, self.raw_response.user_records) # type: ignore[arg-type]
|
user_records: UserRecords = type_validate_json(UserRecords, self.raw_response.user_records) # type: ignore[arg-type]
|
||||||
if isinstance(user_records, RecordsFailed):
|
if isinstance(user_records, RecordsFailed):
|
||||||
raise RequestError(f'用户Solo数据请求错误:\n{user_records.error}')
|
raise RequestError(f'用户Solo数据请求错误:\n{user_records.error}')
|
||||||
self.processed_data.user_records = user_records
|
self.processed_data.user_records = user_records
|
||||||
@@ -158,7 +163,10 @@ class Processor(ProcessorMeta):
|
|||||||
@scheduler.scheduled_job('cron', hour='0,6,12,18', minute=0)
|
@scheduler.scheduled_job('cron', hour='0,6,12,18', minute=0)
|
||||||
@retry(exception_type=RequestError, delay=timedelta(minutes=15))
|
@retry(exception_type=RequestError, delay=timedelta(minutes=15))
|
||||||
async def get_io_rank_data() -> None:
|
async def get_io_rank_data() -> None:
|
||||||
league_all: LeagueAll = parse_raw_as(LeagueAll, await Cache.get(splice_url([BASE_URL, 'users/lists/league/all']))) # type: ignore[arg-type]
|
league_all: LeagueAll = type_validate_json(
|
||||||
|
LeagueAll, # type: ignore[arg-type]
|
||||||
|
(data := await Cache.get(splice_url([BASE_URL, 'users/lists/league/all']))),
|
||||||
|
)
|
||||||
if isinstance(league_all, LeagueAllFailed):
|
if isinstance(league_all, LeagueAllFailed):
|
||||||
raise RequestError(f'排行榜数据请求错误:\n{league_all.error}')
|
raise RequestError(f'排行榜数据请求错误:\n{league_all.error}')
|
||||||
|
|
||||||
@@ -185,6 +193,10 @@ async def get_io_rank_data() -> None:
|
|||||||
user = sort(users, field)
|
user = sort(users, field)
|
||||||
return User(ID=user.id, name=user.username).dict(), field(user)
|
return User(ID=user.id, name=user.username).dict(), field(user)
|
||||||
|
|
||||||
|
data_hash: str | None = await run_sync((await run_sync(sha512)(data)).hexdigest)()
|
||||||
|
async with open(get_data_file('nonebot_plugin_tetris_stats', f'{data_hash}.json.zst'), mode='wb') as file:
|
||||||
|
await file.write(await run_sync(ZstdCompressor(level=12, threads=-1).compress)(data))
|
||||||
|
|
||||||
users = [i for i in league_all.data.users if isinstance(i, LeagueAllUser)]
|
users = [i for i in league_all.data.users if isinstance(i, LeagueAllUser)]
|
||||||
rank_to_users: defaultdict[Rank, list[LeagueAllUser]] = defaultdict(list)
|
rank_to_users: defaultdict[Rank, list[LeagueAllUser]] = defaultdict(list)
|
||||||
for i in users:
|
for i in users:
|
||||||
@@ -208,6 +220,8 @@ async def get_io_rank_data() -> None:
|
|||||||
high_pps=(build_extremes_data(rank_users, pps, _max)),
|
high_pps=(build_extremes_data(rank_users, pps, _max)),
|
||||||
high_apm=(build_extremes_data(rank_users, apm, _max)),
|
high_apm=(build_extremes_data(rank_users, apm, _max)),
|
||||||
high_vs=(build_extremes_data(rank_users, vs, _max)),
|
high_vs=(build_extremes_data(rank_users, vs, _max)),
|
||||||
|
update_time=league_all.cache.cached_at,
|
||||||
|
file_hash=data_hash,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
async with get_session() as session:
|
async with get_session() as session:
|
||||||
@@ -218,6 +232,6 @@ async def get_io_rank_data() -> None:
|
|||||||
@driver.on_startup
|
@driver.on_startup
|
||||||
async def _() -> None:
|
async def _() -> None:
|
||||||
async with get_session() as session:
|
async with get_session() as session:
|
||||||
latest_time = await session.scalar(select(IORank.create_time).order_by(IORank.id.desc()).limit(1))
|
latest_time = await session.scalar(select(IORank.update_time).order_by(IORank.id.desc()).limit(1))
|
||||||
if latest_time is None or datetime.now(tz=UTC) - latest_time.replace(tzinfo=UTC) > timedelta(hours=6):
|
if latest_time is None or datetime.now(tz=UTC) - latest_time.replace(tzinfo=UTC) > timedelta(hours=6):
|
||||||
await get_io_rank_data()
|
await get_io_rank_data()
|
||||||
|
|||||||
@@ -28,20 +28,20 @@ class SuccessModel(BaseSuccessModel):
|
|||||||
league: League
|
league: League
|
||||||
supporter: bool
|
supporter: bool
|
||||||
verified: bool
|
verified: bool
|
||||||
country: str | None
|
country: str | None = None
|
||||||
|
|
||||||
class InvalidUser(BaseModel):
|
class InvalidUser(BaseModel):
|
||||||
class League(BaseModel):
|
class League(BaseModel):
|
||||||
gamesplayed: int
|
gamesplayed: int
|
||||||
gameswon: int
|
gameswon: int
|
||||||
rating: float
|
rating: float
|
||||||
glicko: float | None
|
glicko: float | None = None
|
||||||
rd: float | None
|
rd: float | None = None
|
||||||
rank: Rank
|
rank: Rank
|
||||||
bestrank: Rank
|
bestrank: Rank
|
||||||
apm: float | None
|
apm: float | None = None
|
||||||
pps: float | None
|
pps: float | None = None
|
||||||
vs: float | None
|
vs: float | None = None
|
||||||
decaying: bool
|
decaying: bool
|
||||||
|
|
||||||
id: str = Field(..., alias='_id')
|
id: str = Field(..., alias='_id')
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class SuccessModel(BaseSuccessModel):
|
|||||||
class Badge(BaseModel):
|
class Badge(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
label: str
|
label: str
|
||||||
ts: datetime | None
|
ts: datetime | None = None
|
||||||
|
|
||||||
class NeverPlayedLeague(BaseModel):
|
class NeverPlayedLeague(BaseModel):
|
||||||
gamesplayed: Literal[0]
|
gamesplayed: Literal[0]
|
||||||
@@ -60,8 +60,8 @@ class SuccessModel(BaseSuccessModel):
|
|||||||
bestrank: Rank
|
bestrank: Rank
|
||||||
standing: int
|
standing: int
|
||||||
standing_local: int
|
standing_local: int
|
||||||
next_rank: Rank | None
|
next_rank: Rank | None = None
|
||||||
prev_rank: Rank | None
|
prev_rank: Rank | None = None
|
||||||
next_at: int
|
next_at: int
|
||||||
prev_at: int
|
prev_at: int
|
||||||
percentile: float
|
percentile: float
|
||||||
@@ -70,7 +70,7 @@ class SuccessModel(BaseSuccessModel):
|
|||||||
rd: float
|
rd: float
|
||||||
apm: float
|
apm: float
|
||||||
pps: float
|
pps: float
|
||||||
vs: float | None
|
vs: float | None = None
|
||||||
decaying: bool
|
decaying: bool
|
||||||
|
|
||||||
class Connections(BaseModel):
|
class Connections(BaseModel):
|
||||||
@@ -78,41 +78,41 @@ class SuccessModel(BaseSuccessModel):
|
|||||||
id: str
|
id: str
|
||||||
username: str
|
username: str
|
||||||
|
|
||||||
discord: Discord | None
|
discord: Discord | None = None
|
||||||
|
|
||||||
class Distinguishment(BaseModel):
|
class Distinguishment(BaseModel):
|
||||||
type: str # noqa: A003
|
type: str
|
||||||
|
|
||||||
id: str = Field(..., alias='_id')
|
id: str = Field(..., alias='_id')
|
||||||
username: str
|
username: str
|
||||||
role: Literal['anon', 'user', 'bot', 'halfmod', 'mod', 'admin', 'sysop', 'banned']
|
role: Literal['anon', 'user', 'bot', 'halfmod', 'mod', 'admin', 'sysop', 'banned']
|
||||||
ts: datetime | None
|
ts: datetime | None = None
|
||||||
botmaster: str | None
|
botmaster: str | None = None
|
||||||
badges: list[Badge]
|
badges: list[Badge]
|
||||||
xp: float
|
xp: float
|
||||||
gamesplayed: int
|
gamesplayed: int
|
||||||
gameswon: int
|
gameswon: int
|
||||||
gametime: float
|
gametime: float
|
||||||
country: str | None
|
country: str | None = None
|
||||||
badstanding: bool | None
|
badstanding: bool | None = None
|
||||||
supporter: bool | None # osk说是必有, 但实际上不是 fk osk
|
supporter: bool | None = None # osk说是必有, 但实际上不是 fk osk
|
||||||
supporter_tier: int
|
supporter_tier: int
|
||||||
verified: bool
|
verified: bool
|
||||||
league: NeverPlayedLeague | NeverRatedLeague | RatedLeague
|
league: NeverPlayedLeague | NeverRatedLeague | RatedLeague
|
||||||
avatar_revision: int | None
|
avatar_revision: int | None = None
|
||||||
"""This user's avatar ID. Get their avatar at
|
"""This user's avatar ID. Get their avatar at
|
||||||
|
|
||||||
https://tetr.io/user-content/avatars/{ USERID }.jpg?rv={ AVATAR_REVISION }"""
|
https://tetr.io/user-content/avatars/{ USERID }.jpg?rv={ AVATAR_REVISION }"""
|
||||||
banner_revision: int | None
|
banner_revision: int | None = None
|
||||||
"""This user's banner ID. Get their banner at
|
"""This user's banner ID. Get their banner at
|
||||||
|
|
||||||
https://tetr.io/user-content/banners/{ USERID }.jpg?rv={ BANNER_REVISION }
|
https://tetr.io/user-content/banners/{ USERID }.jpg?rv={ BANNER_REVISION }
|
||||||
|
|
||||||
Ignore this field if the user is not a supporter."""
|
Ignore this field if the user is not a supporter."""
|
||||||
bio: str | None
|
bio: str | None = None
|
||||||
connections: Connections
|
connections: Connections
|
||||||
friend_count: int | None
|
friend_count: int | None = None
|
||||||
distinguishment: Distinguishment | None
|
distinguishment: Distinguishment | None = None
|
||||||
|
|
||||||
user: User
|
user: User
|
||||||
|
|
||||||
|
|||||||
@@ -12,14 +12,14 @@ class EndContext(BaseModel):
|
|||||||
zero: bool
|
zero: bool
|
||||||
locked: bool
|
locked: bool
|
||||||
prev: int
|
prev: int
|
||||||
frameoffset: int | None
|
frameoffset: int | None = None
|
||||||
|
|
||||||
class Clears(BaseModel):
|
class Clears(BaseModel):
|
||||||
singles: int
|
singles: int
|
||||||
doubles: int
|
doubles: int
|
||||||
triples: int
|
triples: int
|
||||||
quads: int
|
quads: int
|
||||||
pentas: int | None
|
pentas: int | None = None
|
||||||
realtspins: int
|
realtspins: int
|
||||||
minitspins: int
|
minitspins: int
|
||||||
minitspinsingles: int
|
minitspinsingles: int
|
||||||
@@ -33,7 +33,7 @@ class EndContext(BaseModel):
|
|||||||
class Garbage(BaseModel):
|
class Garbage(BaseModel):
|
||||||
sent: int
|
sent: int
|
||||||
received: int
|
received: int
|
||||||
attack: int | None
|
attack: int | None = None
|
||||||
cleared: int
|
cleared: int
|
||||||
|
|
||||||
class Finesse(BaseModel):
|
class Finesse(BaseModel):
|
||||||
@@ -46,18 +46,18 @@ class EndContext(BaseModel):
|
|||||||
level_lines: int
|
level_lines: int
|
||||||
level_lines_needed: int
|
level_lines_needed: int
|
||||||
inputs: int
|
inputs: int
|
||||||
holds: int | None
|
holds: int | None = None
|
||||||
time: Time
|
time: Time
|
||||||
score: int
|
score: int
|
||||||
zenlevel: int | None
|
zenlevel: int | None = None
|
||||||
zenprogress: int | None
|
zenprogress: int | None = None
|
||||||
level: int
|
level: int
|
||||||
combo: int
|
combo: int
|
||||||
currentcombopower: int | None # WTF
|
currentcombopower: int | None = None # WTF
|
||||||
topcombo: int
|
topcombo: int
|
||||||
btb: int
|
btb: int
|
||||||
topbtb: int
|
topbtb: int
|
||||||
currentbtbchainpower: int | None # WTF * 2
|
currentbtbchainpower: int | None = None # WTF * 2
|
||||||
tspins: int
|
tspins: int
|
||||||
piecesplaced: int
|
piecesplaced: int
|
||||||
clears: Clears
|
clears: Clears
|
||||||
@@ -79,7 +79,7 @@ class BaseModeRecord(BaseModel):
|
|||||||
replayid: str
|
replayid: str
|
||||||
user: User
|
user: User
|
||||||
ts: datetime
|
ts: datetime
|
||||||
ismulti: bool | None
|
ismulti: bool | None = None
|
||||||
endcontext: EndContext
|
endcontext: EndContext
|
||||||
|
|
||||||
class MultiRecord(BaseModel):
|
class MultiRecord(BaseModel):
|
||||||
@@ -92,21 +92,19 @@ class BaseModeRecord(BaseModel):
|
|||||||
replayid: str
|
replayid: str
|
||||||
user: User
|
user: User
|
||||||
ts: datetime
|
ts: datetime
|
||||||
ismulti: bool | None
|
ismulti: bool | None = None
|
||||||
endcontext: list[EndContext]
|
endcontext: list[EndContext]
|
||||||
|
|
||||||
record: SoloRecord | MultiRecord | None
|
record: SoloRecord | MultiRecord | None = None
|
||||||
rank: int | None
|
rank: int | None = None
|
||||||
|
|
||||||
|
|
||||||
class SuccessModel(BaseSuccessModel):
|
class SuccessModel(BaseSuccessModel):
|
||||||
class Data(BaseModel):
|
class Data(BaseModel):
|
||||||
class Records(BaseModel):
|
class Records(BaseModel):
|
||||||
class Sprint(BaseModeRecord):
|
class Sprint(BaseModeRecord): ...
|
||||||
...
|
|
||||||
|
|
||||||
class Blitz(BaseModeRecord):
|
class Blitz(BaseModeRecord): ...
|
||||||
...
|
|
||||||
|
|
||||||
sprint: Sprint = Field(..., alias='40l')
|
sprint: Sprint = Field(..., alias='40l')
|
||||||
blitz: Blitz
|
blitz: Blitz
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from contextlib import suppress
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
from re import match
|
from re import match
|
||||||
from typing import Literal, NoReturn
|
from typing import Literal
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
@@ -87,10 +87,9 @@ class Processor(ProcessorMeta):
|
|||||||
self.processed_data.user_profile = self.raw_response.user_profile.decode()
|
self.processed_data.user_profile = self.raw_response.user_profile.decode()
|
||||||
return self.processed_data.user_profile
|
return self.processed_data.user_profile
|
||||||
|
|
||||||
async def check_user(self) -> None | NoReturn:
|
async def check_user(self) -> None:
|
||||||
if 'user not found!' in await self.get_user_profile():
|
if 'user not found!' in await self.get_user_profile():
|
||||||
raise RequestError('用户不存在!')
|
raise RequestError('用户不存在!')
|
||||||
return None
|
|
||||||
|
|
||||||
async def get_user_name(self) -> str:
|
async def get_user_name(self) -> str:
|
||||||
"""获取用户名"""
|
"""获取用户名"""
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
|
||||||
GAME_TYPE: Literal['TOS'] = 'TOS'
|
GAME_TYPE: Literal['TOS'] = 'TOS'
|
||||||
BASE_URL = 'https://teatube.cn:8888/'
|
BASE_URL = {
|
||||||
|
'https://teatube.cn:8888/',
|
||||||
|
'http://cafuuchino1.studio26f.org:19970',
|
||||||
|
'http://cafuuchino2.studio26f.org:19970',
|
||||||
|
'http://cafuuchino3.studio26f.org:19970',
|
||||||
|
'http://cafuuchino4.studio26f.org:19970',
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ from re import match
|
|||||||
from typing import Literal
|
from typing import Literal
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
|
from httpx import TimeoutException
|
||||||
|
from nonebot.compat import type_validate_json
|
||||||
from nonebot_plugin_orm import get_session
|
from nonebot_plugin_orm import get_session
|
||||||
from pydantic import parse_raw_as
|
|
||||||
|
|
||||||
from ...db import create_or_update_bind
|
from ...db import create_or_update_bind
|
||||||
from ...utils.exception import MessageFormatError, RequestError
|
from ...utils.exception import MessageFormatError, RequestError
|
||||||
@@ -104,23 +105,31 @@ class Processor(ProcessorMeta):
|
|||||||
"""获取用户信息"""
|
"""获取用户信息"""
|
||||||
if self.processed_data.user_info is None:
|
if self.processed_data.user_info is None:
|
||||||
if self.user.teaid is not None:
|
if self.user.teaid is not None:
|
||||||
url = splice_url(
|
url = [
|
||||||
[
|
splice_url(
|
||||||
BASE_URL,
|
[
|
||||||
'getTeaIdInfo',
|
i,
|
||||||
f'?{urlencode({"teaId":self.user.teaid})}',
|
'getTeaIdInfo',
|
||||||
]
|
f'?{urlencode({"teaId":self.user.teaid})}',
|
||||||
)
|
]
|
||||||
|
)
|
||||||
|
for i in BASE_URL
|
||||||
|
]
|
||||||
else:
|
else:
|
||||||
url = splice_url(
|
url = [
|
||||||
[
|
splice_url(
|
||||||
BASE_URL,
|
[
|
||||||
'getUsernameInfo',
|
i,
|
||||||
f'?{urlencode({"username":self.user.name})}',
|
'getUsernameInfo',
|
||||||
]
|
f'?{urlencode({"username":self.user.name})}',
|
||||||
)
|
]
|
||||||
self.raw_response.user_info = await Request.request(url)
|
)
|
||||||
user_info: UserInfo = parse_raw_as(UserInfo, self.raw_response.user_info) # type: ignore[arg-type]
|
for i in BASE_URL
|
||||||
|
]
|
||||||
|
self.raw_response.user_info = await Request.failover_request(
|
||||||
|
url, failover_code=[502], failover_exc=(TimeoutException,)
|
||||||
|
)
|
||||||
|
user_info: UserInfo = type_validate_json(UserInfo, self.raw_response.user_info) # type: ignore[arg-type]
|
||||||
if not isinstance(user_info, InfoSuccess):
|
if not isinstance(user_info, InfoSuccess):
|
||||||
raise RequestError(f'用户信息请求错误:\n{user_info.error}')
|
raise RequestError(f'用户信息请求错误:\n{user_info.error}')
|
||||||
self.processed_data.user_info = user_info
|
self.processed_data.user_info = user_info
|
||||||
@@ -132,16 +141,23 @@ class Processor(ProcessorMeta):
|
|||||||
other_parameter = {}
|
other_parameter = {}
|
||||||
params = urlencode(dict(sorted(other_parameter.items())))
|
params = urlencode(dict(sorted(other_parameter.items())))
|
||||||
if self.processed_data.user_profile.get(params) is None:
|
if self.processed_data.user_profile.get(params) is None:
|
||||||
self.raw_response.user_profile[params] = await Request.request(
|
self.raw_response.user_profile[params] = await Request.failover_request(
|
||||||
splice_url(
|
[
|
||||||
[
|
splice_url(
|
||||||
BASE_URL,
|
[
|
||||||
'getProfile',
|
i,
|
||||||
f'?{urlencode({"id":self.user.teaid or self.user.name,**other_parameter})}',
|
'getProfile',
|
||||||
]
|
f'?{urlencode({"id":self.user.teaid or self.user.name,**other_parameter})}',
|
||||||
)
|
]
|
||||||
|
)
|
||||||
|
for i in BASE_URL
|
||||||
|
],
|
||||||
|
failover_code=[502],
|
||||||
|
failover_exc=(TimeoutException,),
|
||||||
|
)
|
||||||
|
self.processed_data.user_profile[params] = type_validate_json(
|
||||||
|
UserProfile, self.raw_response.user_profile[params]
|
||||||
)
|
)
|
||||||
self.processed_data.user_profile[params] = UserProfile.parse_raw(self.raw_response.user_profile[params])
|
|
||||||
return self.processed_data.user_profile[params]
|
return self.processed_data.user_profile[params]
|
||||||
|
|
||||||
async def get_game_data(self) -> GameData | None:
|
async def get_game_data(self) -> GameData | None:
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ class NeedCatchError(TetrisStatsError):
|
|||||||
class RequestError(NeedCatchError):
|
class RequestError(NeedCatchError):
|
||||||
"""请求错误"""
|
"""请求错误"""
|
||||||
|
|
||||||
|
def __init__(self, message: str = '', *, status_code: int | None = None):
|
||||||
|
super().__init__(message)
|
||||||
|
self.status_code = status_code
|
||||||
|
|
||||||
|
|
||||||
class MessageFormatError(NeedCatchError):
|
class MessageFormatError(NeedCatchError):
|
||||||
"""用户发送的消息格式不正确"""
|
"""用户发送的消息格式不正确"""
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ class TetrisMetricsProWithLPMADPM(TetrisMetricsBasicWithLPM, TetrisMetricsBaseWi
|
|||||||
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def get_metrics( # noqa: PLR0913
|
def get_metrics(
|
||||||
*,
|
*,
|
||||||
pps: Number,
|
pps: Number,
|
||||||
lpm: None = None,
|
lpm: None = None,
|
||||||
@@ -157,7 +157,7 @@ def get_metrics( # noqa: PLR0913
|
|||||||
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def get_metrics( # noqa: PLR0913
|
def get_metrics(
|
||||||
*,
|
*,
|
||||||
pps: None = None,
|
pps: None = None,
|
||||||
lpm: Number,
|
lpm: Number,
|
||||||
@@ -170,7 +170,7 @@ def get_metrics( # noqa: PLR0913
|
|||||||
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def get_metrics( # noqa: PLR0913
|
def get_metrics(
|
||||||
*,
|
*,
|
||||||
pps: None = None,
|
pps: None = None,
|
||||||
lpm: None = None,
|
lpm: None = None,
|
||||||
@@ -183,7 +183,7 @@ def get_metrics( # noqa: PLR0913
|
|||||||
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def get_metrics( # noqa: PLR0913
|
def get_metrics(
|
||||||
*,
|
*,
|
||||||
pps: None = None,
|
pps: None = None,
|
||||||
lpm: None = None,
|
lpm: None = None,
|
||||||
@@ -196,7 +196,7 @@ def get_metrics( # noqa: PLR0913
|
|||||||
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def get_metrics( # noqa: PLR0913
|
def get_metrics(
|
||||||
*,
|
*,
|
||||||
pps: Number,
|
pps: Number,
|
||||||
lpm: None = None,
|
lpm: None = None,
|
||||||
@@ -209,7 +209,7 @@ def get_metrics( # noqa: PLR0913
|
|||||||
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def get_metrics( # noqa: PLR0913
|
def get_metrics(
|
||||||
*,
|
*,
|
||||||
pps: None = None,
|
pps: None = None,
|
||||||
lpm: Number,
|
lpm: Number,
|
||||||
@@ -222,7 +222,7 @@ def get_metrics( # noqa: PLR0913
|
|||||||
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def get_metrics( # noqa: PLR0913
|
def get_metrics(
|
||||||
*,
|
*,
|
||||||
pps: Number,
|
pps: Number,
|
||||||
lpm: None = None,
|
lpm: None = None,
|
||||||
@@ -235,7 +235,7 @@ def get_metrics( # noqa: PLR0913
|
|||||||
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def get_metrics( # noqa: PLR0913
|
def get_metrics(
|
||||||
*,
|
*,
|
||||||
pps: Number,
|
pps: Number,
|
||||||
lpm: None = None,
|
lpm: None = None,
|
||||||
@@ -248,7 +248,7 @@ def get_metrics( # noqa: PLR0913
|
|||||||
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def get_metrics( # noqa: PLR0913
|
def get_metrics(
|
||||||
*,
|
*,
|
||||||
pps: None = None,
|
pps: None = None,
|
||||||
lpm: Number,
|
lpm: Number,
|
||||||
@@ -261,7 +261,7 @@ def get_metrics( # noqa: PLR0913
|
|||||||
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def get_metrics( # noqa: PLR0913
|
def get_metrics(
|
||||||
*,
|
*,
|
||||||
pps: None = None,
|
pps: None = None,
|
||||||
lpm: Number,
|
lpm: Number,
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
|
from collections.abc import Sequence
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from urllib.parse import urljoin, urlparse
|
from urllib.parse import urljoin, urlparse
|
||||||
|
|
||||||
from aiofiles import open
|
from aiofiles import open
|
||||||
from httpx import AsyncClient, HTTPError
|
from httpx import AsyncClient, HTTPError
|
||||||
from nonebot import get_driver
|
from nonebot import get_driver, get_plugin_config
|
||||||
from nonebot.log import logger
|
from nonebot.log import logger
|
||||||
from playwright.async_api import Response
|
from playwright.async_api import Response
|
||||||
from ujson import JSONDecodeError, dumps, loads
|
from ujson import JSONDecodeError, dumps, loads
|
||||||
@@ -13,7 +14,7 @@ from .browser import BrowserManager
|
|||||||
from .exception import RequestError
|
from .exception import RequestError
|
||||||
|
|
||||||
driver = get_driver()
|
driver = get_driver()
|
||||||
config = Config.parse_obj(driver.config)
|
config = get_plugin_config(Config)
|
||||||
|
|
||||||
|
|
||||||
@driver.on_startup
|
@driver.on_startup
|
||||||
@@ -116,7 +117,8 @@ class Request:
|
|||||||
response = await session.get(url, headers=cls._headers)
|
response = await session.get(url, headers=cls._headers)
|
||||||
if response.status_code != HTTPStatus.OK:
|
if response.status_code != HTTPStatus.OK:
|
||||||
raise RequestError(
|
raise RequestError(
|
||||||
f'请求错误 code: {response.status_code} {HTTPStatus(response.status_code).phrase}\n{response.text}'
|
f'请求错误 code: {response.status_code} {HTTPStatus(response.status_code).phrase}\n{response.text}',
|
||||||
|
status_code=response.status_code,
|
||||||
)
|
)
|
||||||
if is_json:
|
if is_json:
|
||||||
loads(response.content)
|
loads(response.content)
|
||||||
@@ -127,3 +129,33 @@ class Request:
|
|||||||
if urlparse(url).netloc.lower().endswith('tetr.io'):
|
if urlparse(url).netloc.lower().endswith('tetr.io'):
|
||||||
return await cls._anti_cloudflare(url)
|
return await cls._anti_cloudflare(url)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def failover_request(
|
||||||
|
cls,
|
||||||
|
urls: Sequence[str],
|
||||||
|
*,
|
||||||
|
failover_code: Sequence[int],
|
||||||
|
failover_exc: tuple[type[BaseException], ...],
|
||||||
|
is_json: bool = True,
|
||||||
|
) -> bytes:
|
||||||
|
error_list: list[RequestError] = []
|
||||||
|
for i in urls:
|
||||||
|
logger.debug(f'尝试请求 {i}')
|
||||||
|
try:
|
||||||
|
return await cls.request(i, is_json=is_json)
|
||||||
|
except RequestError as e:
|
||||||
|
if e.status_code in failover_code: # 如果状态码在 failover_code 中, 则继续尝试下一个URL
|
||||||
|
error_list.append(e)
|
||||||
|
continue
|
||||||
|
# 如果状态码不在故障转移列表中, 则查找异常栈, 如果异常栈内有 failover_exc 内的异常类型, 则继续尝试下一个URL
|
||||||
|
tb = e.__traceback__
|
||||||
|
while tb is not None:
|
||||||
|
if isinstance(tb.tb_frame.f_locals.get('exc_value'), failover_exc):
|
||||||
|
error_list.append(e)
|
||||||
|
break
|
||||||
|
tb = tb.tb_next
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
continue
|
||||||
|
raise RequestError(f'所有地址皆不可用\n{error_list!r}')
|
||||||
|
|||||||
2166
poetry.lock
generated
2166
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = 'nonebot-plugin-tetris-stats'
|
name = 'nonebot-plugin-tetris-stats'
|
||||||
version = '1.0.0.a12'
|
version = '1.0.0.a17'
|
||||||
description = '一款基于 NoneBot2 的用于查询 Tetris 相关游戏数据的插件'
|
description = '一款基于 NoneBot2 的用于查询 Tetris 相关游戏数据的插件'
|
||||||
authors = ['scdhh <wallfjjd@gmail.com>']
|
authors = ['scdhh <wallfjjd@gmail.com>']
|
||||||
readme = 'README.md'
|
readme = 'README.md'
|
||||||
@@ -10,36 +10,37 @@ license = 'AGPL-3.0'
|
|||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = '^3.10'
|
python = '^3.10'
|
||||||
nonebot2 = '^2.1.3'
|
nonebot2 = '^2.2.0'
|
||||||
lxml = '^5.0.0'
|
lxml = '^5.1.0'
|
||||||
pandas = '>=1.4.3,<3.0.0'
|
pandas = '>=1.4.3,<3.0.0'
|
||||||
playwright = '^1.40.0'
|
playwright = '^1.41.2'
|
||||||
ujson = '^5.9.0'
|
ujson = '^5.9.0'
|
||||||
aiofiles = "^23.2.1"
|
aiofiles = "^23.2.1"
|
||||||
nonebot-plugin-orm = ">=0.1.1,<0.7.0"
|
nonebot-plugin-orm = ">=0.1.1,<0.8.0"
|
||||||
nonebot-plugin-localstore = "^0.5.1"
|
nonebot-plugin-localstore = "^0.6.0"
|
||||||
httpx = "^0.26.0"
|
httpx = "^0.27.0"
|
||||||
nonebot-plugin-alconna = ">=0.30,<0.36"
|
nonebot-plugin-alconna = ">=0.40"
|
||||||
nonebot-plugin-apscheduler = "^0.3.0"
|
nonebot-plugin-apscheduler = "^0.4.0"
|
||||||
aiocache = "^0.12.2"
|
aiocache = "^0.12.2"
|
||||||
|
zstandard = "^0.22.0"
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
mypy = '>=0.991,<1.9'
|
mypy = '>=1.9'
|
||||||
types-ujson = '^5.9.0'
|
types-ujson = '^5.9.0'
|
||||||
pandas-stubs = '>=1.5.2,<3.0.0'
|
pandas-stubs = '>=1.5.2,<3.0.0'
|
||||||
ruff = '>=0.0.239,<0.1.10'
|
ruff = '>=0.3.0'
|
||||||
types-aiofiles = "^23.2.0.0"
|
types-aiofiles = "^23.2.0.20240106"
|
||||||
nonebot2 = { extras = ["fastapi"], version = "^2.1.3" }
|
nonebot2 = { extras = ["fastapi"], version = "^2.2.0" }
|
||||||
types-lxml = "^2023.3.28"
|
types-lxml = "^2024.2.9"
|
||||||
nonebot-plugin-orm = { extras = ["default"], version = ">=0.3,<0.7" }
|
nonebot-plugin-orm = { extras = ["default"], version = ">=0.3,<0.8" }
|
||||||
nonebot-adapter-onebot = "^2.3.1"
|
nonebot-adapter-onebot = "^2.4.1"
|
||||||
nonebot-adapter-satori = "^0.8.1"
|
nonebot-adapter-satori = "^0.11.3"
|
||||||
nonebot-adapter-kaiheila = "^0.3.0"
|
nonebot-adapter-kaiheila = "^0.3.4"
|
||||||
nonebot-adapter-discord = "^0.1.3"
|
nonebot-adapter-discord = "^0.1.3"
|
||||||
|
|
||||||
[tool.poetry.group.debug.dependencies]
|
[tool.poetry.group.debug.dependencies]
|
||||||
objprint = '^0.2.2'
|
objprint = '^0.2.2'
|
||||||
viztracer = "^0.16.1"
|
viztracer = "^0.16.2"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ['poetry-core>=1.0.0']
|
requires = ['poetry-core>=1.0.0']
|
||||||
|
|||||||
Reference in New Issue
Block a user