├── logo.png ├── .gitignore ├── tsugu_api_core ├── __init__.py ├── client │ ├── __init__.py │ ├── _client.py │ ├── aiohttp.py │ └── httpx.py ├── _settings.py ├── utils.py ├── exception.py ├── _typing.py └── _network.py ├── setup.py ├── LICENSE ├── .github └── workflows │ └── publish-to-pypi.yml ├── tsugu_api ├── _bandoristation.py ├── __init__.py ├── _station.py ├── _user.py └── _tsugu.py ├── tsugu_api_async ├── _bandoristation.py ├── __init__.py ├── _station.py ├── _user.py └── _tsugu.py └── README.md /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WindowsSov8forUs/tsugu-api-python/HEAD/logo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/__pycache__/ 2 | /.vscode/ 3 | test.py 4 | /tsugu_api_python.egg-info/ 5 | /build/ 6 | /dist/ 7 | /.venv/ 8 | requirements.txt -------------------------------------------------------------------------------- /tsugu_api_core/__init__.py: -------------------------------------------------------------------------------- 1 | '''`tsugu_api_core` tsugu-api-python 核心模块''' 2 | 3 | from . import utils as utils 4 | from . import client as client 5 | from . import _typing as _typing 6 | from . import _network as _network 7 | from . import exception as exception 8 | 9 | from .client import register_client as register_client 10 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | with open('README.md', 'r', encoding='utf-8') as readme: 4 | long_description = readme.read() 5 | 6 | setup( 7 | name='tsugu-api-python', 8 | version='1.5.10', 9 | author='WindowsSov8', 10 | author_email='qwertyuiop2333@hotmail.com', 11 | description='Tsugu BanGDream Bot 的功能 API 统合包', 12 | long_description=long_description, 13 | long_description_content_type='text/markdown', 14 | url='https://github.com/WindowsSov8forUs/tsugu-api-python', 15 | include_package_data=False, 16 | packages=find_packages(), 17 | license='MIT', 18 | classifiers=[ 19 | 'Programming Language :: Python :: 3.8', 20 | 'License :: OSI Approved :: MIT License', 21 | 'Operating System :: OS Independent' 22 | ], 23 | python_requires='>=3.8', 24 | install_requires=[ 25 | 'typing_extensions>=4.5.0', 26 | ], 27 | extras_require={ 28 | 'httpx': [ 29 | 'httpx>=0.22.0', 30 | ], 31 | 'aiohttp': [ 32 | 'aiohttp>=3.8.1', 33 | ], 34 | }, 35 | ) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 WindowsSov8 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /tsugu_api_core/client/__init__.py: -------------------------------------------------------------------------------- 1 | '''`tsugu_api_python` HTTP 客户端模块''' 2 | 3 | from typing import Type, Optional 4 | 5 | from tsugu_api_core._settings import settings 6 | 7 | from ._client import Client as Client 8 | from ._client import Request as Request 9 | from ._client import Response as Response 10 | 11 | __HTTPClient__: Optional[Type[Client]] = None 12 | 13 | def register_client( 14 | client: Type[Client] 15 | ) -> None: 16 | '''注册一个 HTTP 客户端类,之后将会使用该客户端发送请求而不是内置客户端。 17 | 18 | 参数: 19 | client (Type[Client]): HTTP 客户端类 20 | ''' 21 | global __HTTPClient__ 22 | __HTTPClient__ = client 23 | return 24 | 25 | def _Client() -> Type[Client]: 26 | '''获取当前使用的 HTTP 客户端 27 | 28 | 返回: 29 | Client: HTTP 客户端 30 | ''' 31 | global __HTTPClient__ 32 | if __HTTPClient__ is not None: 33 | return __HTTPClient__ 34 | 35 | if settings.client.lower() == 'httpx': 36 | from .httpx import Client as HTTPXClient 37 | __HTTPClient__ = HTTPXClient 38 | return __HTTPClient__ 39 | elif settings.client.lower() == 'aiohttp': 40 | from .aiohttp import Client as AIOHTTPClient 41 | __HTTPClient__ = AIOHTTPClient 42 | return __HTTPClient__ 43 | else: 44 | raise ValueError('Unknown client type') -------------------------------------------------------------------------------- /.github/workflows/publish-to-pypi.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 7 | 8 | workflow_dispatch: 9 | 10 | jobs: 11 | deploy: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | 18 | - name: Set up Python 19 | uses: actions/setup-python@v4 20 | with: 21 | python-version: '3.10' 22 | 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install --upgrade pip 26 | pip install build twine 27 | 28 | - name: Get Version 29 | id: version 30 | run: | 31 | echo "VERSION=$(pip --version)" >> $GITHUB_OUTPUT 32 | echo "TAG_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT 33 | echo "TAG_NAME=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT 34 | 35 | - name: Build distribution 36 | run: | 37 | python -m build 38 | 39 | - name: Publish to PyPI and Release 40 | run: | 41 | export GH_TOKEN=$GITHUB_TOKEN 42 | gh release upload --clobber ${{ steps.version.outputs.TAG_NAME }} dist/*.tar.gz dist/*.whl 43 | python -m twine upload dist/* --repository-url https://upload.pypi.org/legacy/ --username __token__ --password ${{ secrets.PYPI_TOKEN }} 44 | env: 45 | GITHUB_TOKEN: ${{ secrets.PAT }} -------------------------------------------------------------------------------- /tsugu_api_core/_settings.py: -------------------------------------------------------------------------------- 1 | '''对 tsugu_api 进行配置''' 2 | 3 | from enum import Enum 4 | 5 | class Settings: 6 | '''tsugu_api 配置类''' 7 | 8 | client: str = 'httpx' 9 | '''使用的 HTTP 客户端''' 10 | 11 | timeout: float = 10 12 | '''请求超时时间''' 13 | 14 | max_retries: int = 3 15 | '''请求重试次数''' 16 | 17 | proxy: str = '' 18 | '''代理地址''' 19 | 20 | backend_url: str = 'http://tsugubot.com:8080' 21 | ''' 22 | 后端地址 23 | 24 | 默认为 Tsugu 官方后端,若有自建后端服务器可进行修改。 25 | ''' 26 | backend_proxy: bool = True 27 | ''' 28 | 是否使用后端代理 29 | 30 | 当设置代理地址后可修改此项以决定是否使用代理。 31 | 32 | 默认为 True,即使用后端代理。若使用代理时后端服务器无法访问,可将此项设置为 False。 33 | ''' 34 | userdata_backend_url: str = 'http://tsugubot.com:8080' 35 | ''' 36 | 用户数据后端地址 37 | 38 | 所有的 `/user` 路由和 `/station` 路由都基于此 39 | 40 | 由于这些路由需要用户数据库的支持,部分 tsugu 后端可能不存在该路由 41 | 42 | 默认为 Tsugu 官方后端,若有自建后端服务器可进行修改。 43 | ''' 44 | userdata_backend_proxy: bool = True 45 | ''' 46 | 是否使用用户数据后端代理 47 | 48 | 当设置代理地址后可修改此项以决定是否使用代理。 49 | 50 | 默认为 True,即使用后端代理。若使用代理时后端服务器无法访问,可将此项设置为 False。 51 | ''' 52 | 53 | use_easy_bg: bool = True 54 | ''' 55 | 是否使用简易背景,使用可在降低背景质量的前提下加快响应速度。 56 | 57 | 默认为 True,即使用简易背景。若不使用简易背景,可将此项设置为 False。 58 | ''' 59 | compress: bool = True 60 | ''' 61 | 是否压缩返回数据,压缩可减少返回数据大小。 62 | 63 | 默认为 True,即压缩返回数据。若不压缩返回数据,可将此项设置为 False。 64 | ''' 65 | 66 | settings = Settings() 67 | 68 | __all__ = [ 69 | 'settings', 70 | ] 71 | -------------------------------------------------------------------------------- /tsugu_api/_bandoristation.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from tsugu_api_core._settings import settings 4 | from tsugu_api_core._typing import StationRoom 5 | from tsugu_api_core._network import BANDORI_STATION_URL, Api 6 | from tsugu_api_core.exception import ( 7 | RoomQueryFailure, 8 | RoomSubmitFailure 9 | ) 10 | 11 | def submit_room_number(number: int, user_id: str, raw_message: str, source: str, token: str) -> None: 12 | '''上传房间号 13 | 14 | 参数: 15 | number (int): 房间号 16 | user_id (str): 用户 ID 17 | raw_message (str): 原始消息,用作房间号注释 18 | source (str): 房间来源,即令牌名称 19 | token (str): 上传用的车站令牌 20 | ''' 21 | 22 | # 构建参数 23 | params = { 24 | 'number': number, 25 | 'user_id': user_id, 26 | 'raw_message': raw_message, 27 | 'source': source, 28 | 'token': token, 29 | 'function': 'submit_room_number', 30 | } 31 | 32 | # 发送请求 33 | response = Api( 34 | BANDORI_STATION_URL, 35 | '', 36 | proxy=settings.backend_proxy 37 | ).get(params).json() 38 | if response['status'] == 'failure': 39 | raise RoomSubmitFailure(response['response']) 40 | 41 | def query_room_number() -> List[StationRoom]: 42 | '''获取房间号 43 | 44 | 返回: 45 | List[StationRoom]: 房间信息列表 46 | ''' 47 | 48 | # 发送请求 49 | response = Api( 50 | BANDORI_STATION_URL, 51 | '', 52 | proxy=settings.backend_proxy 53 | ).get({'function': 'query_room_number'}).json() 54 | if response['status'] == 'failure': 55 | raise RoomQueryFailure(response['response']) 56 | return response['response'] 57 | -------------------------------------------------------------------------------- /tsugu_api_async/_bandoristation.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from tsugu_api_core._settings import settings 4 | from tsugu_api_core._typing import StationRoom 5 | from tsugu_api_core._network import BANDORI_STATION_URL, Api 6 | from tsugu_api_core.exception import ( 7 | RoomQueryFailure, 8 | RoomSubmitFailure 9 | ) 10 | 11 | async def submit_room_number(number: int, user_id: str, raw_message: str, source: str, token: str) -> None: 12 | '''上传房间号 13 | 14 | 参数: 15 | number (int): 房间号 16 | user_id (str): 用户 ID 17 | raw_message (str): 原始消息,用作房间号注释 18 | source (str): 房间来源,即令牌名称 19 | token (str): 上传用的车站令牌 20 | ''' 21 | 22 | # 构建参数 23 | params = { 24 | 'number': number, 25 | 'user_id': user_id, 26 | 'raw_message': raw_message, 27 | 'source': source, 28 | 'token': token, 29 | 'function': 'submit_room_number' 30 | } 31 | 32 | # 发送请求 33 | response = (await Api( 34 | BANDORI_STATION_URL, 35 | '', 36 | proxy=settings.backend_proxy 37 | ).aget(params)).json() 38 | if response['status'] == 'failure': 39 | raise RoomSubmitFailure(response['response']) 40 | 41 | async def query_room_number() -> List[StationRoom]: 42 | '''获取房间号 43 | 44 | 返回: 45 | List[StationRoom]: 房间信息列表 46 | ''' 47 | 48 | # 发送请求 49 | response = (await Api( 50 | BANDORI_STATION_URL, 51 | '', 52 | proxy=settings.backend_proxy 53 | ).aget({'function': 'query_room_number'})).json() 54 | if response['status'] == 'failure': 55 | raise RoomQueryFailure(response['response']) 56 | return response['response'] 57 | -------------------------------------------------------------------------------- /tsugu_api/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | `tsugu_api` 向 Tsugu 后端 API 请求模块 3 | ''' 4 | 5 | from tsugu_api_core.utils import * 6 | 7 | from ._user import get_user_data as get_user_data 8 | from ._user import change_user_data as change_user_data 9 | from ._user import bind_player_request as bind_player_request 10 | from ._user import bind_player_verification as bind_player_verification 11 | 12 | from ._tsugu import ycx as ycx 13 | from ._tsugu import lsycx as lsycx 14 | from ._tsugu import ycx_all as ycx_all 15 | from ._tsugu import room_list as room_list 16 | from ._tsugu import song_meta as song_meta 17 | from ._tsugu import cutoff_all as cutoff_all 18 | from ._tsugu import song_chart as song_chart 19 | from ._tsugu import event_stage as event_stage 20 | from ._tsugu import search_card as search_card 21 | from ._tsugu import search_song as search_song 22 | from ._tsugu import song_random as song_random 23 | from ._tsugu import fuzzy_search as fuzzy_search 24 | from ._tsugu import search_event as search_event 25 | from ._tsugu import search_gacha as search_gacha 26 | from ._tsugu import cutoff_detail as cutoff_detail 27 | from ._tsugu import search_player as search_player 28 | from ._tsugu import gacha_simulate as gacha_simulate 29 | from ._tsugu import search_character as search_character 30 | from ._tsugu import get_card_illustration as get_card_illustration 31 | from ._tsugu import cutoff_list_of_recent_event as cutoff_list_of_recent_event 32 | 33 | from ._station import station_query_all_room as station_query_all_room 34 | from ._station import station_submit_room_number as station_submit_room_number 35 | 36 | from ._bandoristation import query_room_number as query_room_number 37 | from ._bandoristation import submit_room_number as submit_room_number 38 | 39 | from tsugu_api_core._settings import settings as settings 40 | -------------------------------------------------------------------------------- /tsugu_api_async/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | `tsugu_api` 向 Tsugu 后端 API 请求模块 3 | ''' 4 | 5 | from tsugu_api_core.utils import * 6 | 7 | from ._user import get_user_data as get_user_data 8 | from ._user import change_user_data as change_user_data 9 | from ._user import bind_player_request as bind_player_request 10 | from ._user import bind_player_verification as bind_player_verification 11 | 12 | from ._tsugu import ycx as ycx 13 | from ._tsugu import lsycx as lsycx 14 | from ._tsugu import ycx_all as ycx_all 15 | from ._tsugu import room_list as room_list 16 | from ._tsugu import song_meta as song_meta 17 | from ._tsugu import cutoff_all as cutoff_all 18 | from ._tsugu import song_chart as song_chart 19 | from ._tsugu import event_stage as event_stage 20 | from ._tsugu import search_card as search_card 21 | from ._tsugu import search_song as search_song 22 | from ._tsugu import song_random as song_random 23 | from ._tsugu import fuzzy_search as fuzzy_search 24 | from ._tsugu import search_event as search_event 25 | from ._tsugu import search_gacha as search_gacha 26 | from ._tsugu import cutoff_detail as cutoff_detail 27 | from ._tsugu import search_player as search_player 28 | from ._tsugu import gacha_simulate as gacha_simulate 29 | from ._tsugu import search_character as search_character 30 | from ._tsugu import get_card_illustration as get_card_illustration 31 | from ._tsugu import cutoff_list_of_recent_event as cutoff_list_of_recent_event 32 | 33 | from ._station import station_query_all_room as station_query_all_room 34 | from ._station import station_submit_room_number as station_submit_room_number 35 | 36 | from ._bandoristation import query_room_number as query_room_number 37 | from ._bandoristation import submit_room_number as submit_room_number 38 | 39 | from tsugu_api_core._settings import settings as settings 40 | -------------------------------------------------------------------------------- /tsugu_api/_station.py: -------------------------------------------------------------------------------- 1 | from time import time 2 | from typing import Optional 3 | 4 | from tsugu_api_core._network import Api 5 | from tsugu_api_core._settings import settings 6 | from tsugu_api_core._typing import ( 7 | _QueryResponse, 8 | _SubmitResponse 9 | ) 10 | 11 | def station_submit_room_number( 12 | number: int, 13 | raw_message: str, 14 | platform: str, 15 | user_id: str, 16 | user_name: str, 17 | avatar_url: Optional[str] = None, 18 | bandori_station_token: Optional[str] = None 19 | ) -> _SubmitResponse: 20 | '''提交车牌号 21 | 22 | 参数: 23 | number (int): 车牌号 24 | raw_message (str): 原始消息 25 | platform (str): 平台 26 | user_id (str): 用户 ID 27 | user_name (str): 用户名 28 | avatar_url (Optional[str]): 用户头像 URL 29 | bandori_station_token (Optional[str]): Bandori 车站令牌,不填则使用 Tsugu 后端配置 30 | 31 | 返回: 32 | _SubmitResponse: 响应信息 33 | ''' 34 | 35 | # 构建数据 36 | data = { 37 | 'number': number, 38 | 'rawMessage': raw_message, 39 | 'platform': platform, 40 | 'userId': user_id, 41 | 'userName': user_name, 42 | 'time': int(time()) 43 | } 44 | if avatar_url: 45 | data['avatarUrl'] = avatar_url 46 | if bandori_station_token: 47 | data['bandoriStationToken'] = bandori_station_token 48 | 49 | # 发送请求 50 | return Api( 51 | settings.userdata_backend_url, 52 | '/station/submitRoomNumber', 53 | proxy=settings.userdata_backend_proxy 54 | ).post(data).json() 55 | 56 | def station_query_all_room() -> _QueryResponse: 57 | '''查询最近车站车牌 58 | 59 | 返回: 60 | _QueryResponse: 响应信息 61 | ''' 62 | 63 | # 发送请求 64 | return Api( 65 | settings.userdata_backend_url, 66 | '/station/queryAllRoom', 67 | proxy=settings.userdata_backend_proxy 68 | ).get().json() 69 | -------------------------------------------------------------------------------- /tsugu_api_async/_station.py: -------------------------------------------------------------------------------- 1 | from time import time 2 | from typing import Optional 3 | 4 | from tsugu_api_core._network import Api 5 | from tsugu_api_core._settings import settings 6 | from tsugu_api_core._typing import ( 7 | _QueryResponse, 8 | _SubmitResponse 9 | ) 10 | 11 | async def station_submit_room_number( 12 | number: int, 13 | raw_message: str, 14 | platform: str, 15 | user_id: str, 16 | user_name: str, 17 | avatar_url: Optional[str] = None, 18 | bandori_station_token: Optional[str] = None 19 | ) -> _SubmitResponse: 20 | '''提交车牌号 21 | 22 | 参数: 23 | number (int): 车牌号 24 | raw_message (str): 原始消息 25 | platform (str): 平台 26 | user_id (str): 用户 ID 27 | user_name (str): 用户名 28 | avatar_url (Optional[str]): 用户头像 URL 29 | bandori_station_token (Optional[str]): Bandori 车站令牌,不填则使用 Tsugu 后端配置 30 | 31 | 返回: 32 | _SubmitResponse: 响应信息 33 | ''' 34 | 35 | # 构建数据 36 | data = { 37 | 'number': number, 38 | 'rawMessage': raw_message, 39 | 'platform': platform, 40 | 'userId': user_id, 41 | 'userName': user_name, 42 | 'time': int(time()) 43 | } 44 | if avatar_url: 45 | data['avatarUrl'] = avatar_url 46 | if bandori_station_token: 47 | data['bandoriStationToken'] = bandori_station_token 48 | 49 | # 发送请求 50 | return (await Api( 51 | settings.userdata_backend_url, 52 | '/station/submitRoomNumber', 53 | proxy=settings.userdata_backend_proxy 54 | ).apost(data)).json() 55 | 56 | async def station_query_all_room() -> _QueryResponse: 57 | '''查询最近车站车牌 58 | 59 | 返回: 60 | _QueryResponse: 响应信息 61 | ''' 62 | 63 | # 发送请求 64 | return (await Api( 65 | settings.userdata_backend_url, 66 | '/station/queryAllRoom', 67 | proxy=settings.userdata_backend_proxy 68 | ).aget()).json() 69 | -------------------------------------------------------------------------------- /tsugu_api_core/utils.py: -------------------------------------------------------------------------------- 1 | ''' 2 | `tsugu_api_core.utils` Tsugu API 工具模块 3 | ''' 4 | 5 | from tsugu_api_core._typing import ( 6 | _Room, 7 | ServerId, 8 | ServerName, 9 | StationRoom, 10 | ) 11 | 12 | def string_to_server_code(server: str) -> ServerId: 13 | if server == 'jp': 14 | return 0 15 | elif server == 'en': 16 | return 1 17 | elif server == 'tw': 18 | return 2 19 | elif server == 'cn': 20 | return 3 21 | elif server == 'kr': 22 | return 4 23 | elif server == '日服': 24 | return 0 25 | elif server == '国际服': 26 | return 1 27 | elif server == '台服': 28 | return 2 29 | elif server == '国服': 30 | return 3 31 | elif server == '韩服': 32 | return 4 33 | else: 34 | raise ValueError('服务器名称不存在') 35 | 36 | def int_to_server_short(server: int) -> ServerName: 37 | if server == 0: 38 | return 'jp' 39 | elif server == 1: 40 | return 'en' 41 | elif server == 2: 42 | return 'tw' 43 | elif server == 3: 44 | return 'cn' 45 | elif server == 4: 46 | return 'kr' 47 | else: 48 | raise ValueError('服务器代码不存在') 49 | 50 | def int_to_server_full(server: int) -> str: 51 | if server == 0: 52 | return '日服' 53 | elif server == 1: 54 | return '国际服' 55 | elif server == 2: 56 | return '台服' 57 | elif server == 3: 58 | return '国服' 59 | elif server == 4: 60 | return '韩服' 61 | else: 62 | raise ValueError('服务器代码不存在') 63 | 64 | def station_room_to_tsugu(station_room: StationRoom) -> _Room: 65 | room: _Room = { 66 | 'number': station_room['number'], 67 | 'rawMessage': station_room['raw_message'], 68 | 'source': station_room['source_info']['name'], 69 | 'userId': str(station_room['user_info']['user_id']), 70 | 'time': station_room['time'], 71 | 'avatarUrl': station_room['user_info']['avatar'], 72 | 'userName': station_room['user_info']['username'], 73 | } 74 | return room 75 | 76 | __all__ = [ 77 | 'string_to_server_code', 78 | 'int_to_server_short', 79 | 'int_to_server_full', 80 | 'station_room_to_tsugu', 81 | ] 82 | -------------------------------------------------------------------------------- /tsugu_api_core/client/_client.py: -------------------------------------------------------------------------------- 1 | '''HTTP 客户端类型基类''' 2 | 3 | from json import loads 4 | from abc import ABC, abstractmethod 5 | from typing import Any, Dict, Optional 6 | 7 | from typing_extensions import Self 8 | 9 | class Request: 10 | '''HTTP 请求类''' 11 | 12 | def __init__( 13 | self, 14 | method: str, 15 | url: str, 16 | *, 17 | headers: Optional[Dict[str, str]], 18 | params: Optional[Dict[str, Any]], 19 | data: Optional[Dict[str, Any]], 20 | ) -> None: 21 | self.method = method 22 | '''请求方法''' 23 | self.url = url 24 | '''请求 URL''' 25 | self.headers = headers 26 | '''请求头''' 27 | self.params = params 28 | '''请求参数''' 29 | self.data = data 30 | '''请求数据''' 31 | 32 | class Response: 33 | '''HTTP 响应类''' 34 | 35 | def __init__(self, content: bytes, status_code: int, exception: Optional[Exception]=None) -> None: 36 | self.content = content 37 | '''响应内容''' 38 | self.status_code = status_code 39 | '''状态码''' 40 | self.exception = exception 41 | '''异常''' 42 | 43 | def json(self, **kwargs: Any) -> Any: 44 | '''解析 JSON 响应内容''' 45 | return loads(self.content, **kwargs) 46 | 47 | class Client(ABC): 48 | '''HTTP 客户端类型基类''' 49 | 50 | def __init__(self, proxy: Optional[str], timeout: float, max_retries: int) -> None: 51 | self.proxy = proxy 52 | '''代理服务器地址''' 53 | self.timeout = timeout 54 | '''超时时间''' 55 | self.max_retries = max_retries 56 | '''重试次数''' 57 | 58 | @abstractmethod 59 | def __enter__(self) -> Self: 60 | raise NotImplementedError 61 | 62 | @abstractmethod 63 | async def __aenter__(self) -> Self: 64 | raise NotImplementedError 65 | 66 | @abstractmethod 67 | def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None: 68 | raise NotImplementedError 69 | 70 | @abstractmethod 71 | async def __aexit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None: 72 | raise NotImplementedError 73 | 74 | @abstractmethod 75 | def request(self, request: Request) -> Response: 76 | '''发送请求并获取响应''' 77 | raise NotImplementedError 78 | 79 | @abstractmethod 80 | async def arequest(self, request: Request) -> Response: 81 | '''异步发送请求并获取响应''' 82 | raise NotImplementedError -------------------------------------------------------------------------------- /tsugu_api_core/client/aiohttp.py: -------------------------------------------------------------------------------- 1 | '''内置的 AIOHTTP 适配''' 2 | 3 | import asyncio 4 | from json import dumps 5 | from typing import Any, cast 6 | from typing_extensions import override 7 | 8 | from ._client import Client as _Client 9 | from ._client import Request, Response 10 | 11 | try: 12 | import aiohttp 13 | except ModuleNotFoundError as exception: 14 | raise ImportError( 15 | 'module \'aiohttp\' is not installed, please install it by running \'pip install aiohttp\'' 16 | ) from exception 17 | 18 | class Client(_Client): 19 | _client_session: aiohttp.ClientSession 20 | 21 | @override 22 | def __enter__(self) -> 'Client': 23 | raise RuntimeError('AIOHTTP client is not synchronous, please use async context manager') 24 | 25 | @override 26 | async def __aenter__(self) -> 'Client': 27 | self._client_session = aiohttp.ClientSession( 28 | trust_env=False, 29 | timeout=aiohttp.ClientTimeout(total=self.timeout), 30 | proxy=self.proxy, 31 | ) 32 | await self._client_session.__aenter__() 33 | return self 34 | 35 | @override 36 | def __exit__(self, *args: Any) -> None: 37 | pass 38 | 39 | @override 40 | async def __aexit__(self, *args: Any) -> None: 41 | await self._client_session.__aexit__(*args) 42 | 43 | @override 44 | def request(self, request: Request) -> Response: 45 | raise RuntimeError('AIOHTTP client is not synchronous, please use async request method.') 46 | 47 | @override 48 | async def arequest(self, request: Request) -> Response: 49 | retries = 0 50 | 51 | while True: 52 | try: 53 | response = await self._client_session.request( 54 | request.method, 55 | request.url, 56 | params=request.params, 57 | data=cast(dict, dumps(request.data)) if request.data is not None else request.data, 58 | headers=request.headers, 59 | ) 60 | break 61 | except Exception as exception: 62 | if retries >= self.max_retries: 63 | raise exception 64 | retries += 1 65 | await asyncio.sleep(0.5) 66 | 67 | try: 68 | response.raise_for_status() 69 | return Response( 70 | await response.read(), 71 | response.status, 72 | ) 73 | except Exception as exception: 74 | return Response( 75 | await response.read(), 76 | response.status, 77 | exception, 78 | ) -------------------------------------------------------------------------------- /tsugu_api_core/exception.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from tsugu_api_core._typing import _FailedResponse, _BadRequestResponse 3 | 4 | class TsuguException(Exception): 5 | '''tsugu_api_core 异常基类''' 6 | msg: str = '' 7 | '''从 API 返回参数中获取的错误信息''' 8 | def __init__(self, msg: str) -> None: 9 | '''初始化''' 10 | self.msg = msg 11 | 12 | def __str__(self) -> str: 13 | return self.msg 14 | 15 | class HTTPStatusError(TsuguException): 16 | '''HTTP 状态码错误''' 17 | status_code: int 18 | '''HTTP 状态码''' 19 | def __init__(self, status_code: int) -> None: 20 | '''初始化''' 21 | self.status_code = status_code 22 | super().__init__(f"HTTP status code error: {status_code}, please check your network environment or contact the developer.") 23 | return 24 | 25 | class BadRequestError(TsuguException): 26 | '''请求的参数错误''' 27 | api: str 28 | '''请求的 API''' 29 | response: _BadRequestResponse 30 | '''API 返回的响应''' 31 | def __init__(self, api: str, response: _BadRequestResponse) -> None: 32 | '''初始化''' 33 | self.api = api 34 | self.response = response 35 | 36 | error_messages: List[str] = [] 37 | for error in response['error']: 38 | error_messages.append( 39 | f"\tGot a wrong value '{str(error['value'])}' for {error['type']} parameter '{error['path']}' in {error['location']}: {error['msg']}." 40 | ) 41 | 42 | super().__init__(f"API '{api}' got wrong parameters:\n" + '\n'.join(error_messages)) 43 | 44 | return 45 | 46 | class FailedException(TsuguException): 47 | '''API 请求失败''' 48 | api: str 49 | '''请求的 API''' 50 | status_code: int 51 | '''API 返回的状态码''' 52 | response: _FailedResponse 53 | '''API 返回的响应''' 54 | def __init__(self, api: str, status_code: int, response: _FailedResponse) -> None: 55 | '''初始化''' 56 | self.api = api 57 | self.status_code = status_code 58 | self.response = response 59 | 60 | super().__init__(f"API '{api}' failed ({status_code}) : {response['data']}") 61 | 62 | return 63 | 64 | def json(self) -> _FailedResponse: 65 | '''返回 API 返回的响应''' 66 | return self.response 67 | 68 | @property 69 | def data(self) -> str: 70 | return self.response['data'] 71 | 72 | class RoomSubmitFailure(TsuguException): 73 | '''房间号上传失败''' 74 | def __init__(self, response: str) -> None: 75 | '''初始化''' 76 | super().__init__(response) 77 | return 78 | 79 | class RoomQueryFailure(TsuguException): 80 | '''房间号获取失败''' 81 | def __init__(self, response: str) -> None: 82 | '''初始化''' 83 | super().__init__(response) 84 | return 85 | -------------------------------------------------------------------------------- /tsugu_api/_user.py: -------------------------------------------------------------------------------- 1 | 2 | from tsugu_api_core._network import Api 3 | from tsugu_api_core._settings import settings 4 | from tsugu_api_core._typing import ( 5 | ServerId, 6 | _BindingAction, 7 | PartialTsuguUser, 8 | _GetUserDataResponse, 9 | _ChangeUserDataResponse, 10 | _BindPlayerRequestResponse, 11 | _BindPlayerVerificationResponse 12 | ) 13 | 14 | def get_user_data(platform: str, user_id: str) -> _GetUserDataResponse: 15 | '''获取用户数据 16 | 17 | 参数: 18 | platform (str): 平台名称 19 | user_id (str): 用户 ID 20 | 21 | 返回: 22 | _GetUserDataResponse: API 返回响应 23 | ''' 24 | 25 | # 构建数据 26 | data = { 27 | 'platform': platform, 28 | 'userId': user_id 29 | } 30 | 31 | # 发送请求 32 | return Api( 33 | settings.userdata_backend_url, 34 | '/user/getUserData', 35 | proxy=settings.userdata_backend_proxy 36 | ).post(data).json() 37 | 38 | def change_user_data(platform: str, user_id: str, update: PartialTsuguUser) -> _ChangeUserDataResponse: 39 | '''修改用户数据 40 | 41 | 参数: 42 | platform (str): 平台名称 43 | user_id (str): 用户 ID 44 | update (_PartialTsuguUser): 更新数据 45 | 46 | 返回: 47 | _ChangeUserDataResponse: API 返回响应 48 | ''' 49 | 50 | # 构建数据 51 | data = { 52 | 'platform': platform, 53 | 'userId': user_id, 54 | 'update': update 55 | } 56 | 57 | # 发送请求 58 | return Api( 59 | settings.userdata_backend_url, 60 | '/user/changeUserData', 61 | proxy=settings.userdata_backend_proxy 62 | ).post(data).json() 63 | 64 | def bind_player_request( 65 | platform: str, 66 | user_id: str 67 | ) -> _BindPlayerRequestResponse: 68 | '''绑定玩家请求 69 | 70 | 参数: 71 | platform (str): 平台名称 72 | user_id (str): 用户 ID 73 | 74 | 返回: 75 | _BindPlayerRequestResponse: 请求返回数据 76 | ''' 77 | 78 | # 构建数据 79 | data = { 80 | 'platform': platform, 81 | 'userId': user_id 82 | } 83 | 84 | # 发送请求 85 | return Api( 86 | settings.userdata_backend_url, 87 | '/user/bindPlayerRequest', 88 | proxy=settings.userdata_backend_proxy, 89 | ).post(data).json() 90 | 91 | def bind_player_verification( 92 | platform: str, 93 | user_id: str, 94 | server: ServerId, 95 | player_id: int, 96 | binding_action: _BindingAction 97 | ) -> _BindPlayerVerificationResponse: 98 | '''绑定玩家验证 99 | 100 | 参数: 101 | platform (str): 平台名称 102 | user_id (str): 用户 ID 103 | server (_ServerId): 服务器编号 0 - 日服 1 - 国际服 2 - 台服 3 - 国服 4 - 韩服 104 | player_id (int): 玩家 ID 105 | binding_action (_BindingAction): 绑定操作 106 | 107 | 返回: 108 | _BindPlayerVerificationResponse: 验证返回数据 109 | ''' 110 | 111 | # 构建数据 112 | data = { 113 | 'platform': platform, 114 | 'userId': user_id, 115 | 'server': server, 116 | 'playerId': player_id, 117 | 'bindingAction': binding_action 118 | } 119 | 120 | # 发送请求 121 | return Api( 122 | settings.userdata_backend_url, 123 | '/user/bindPlayerVerification', 124 | proxy=settings.userdata_backend_proxy 125 | ).post(data).json() 126 | -------------------------------------------------------------------------------- /tsugu_api_async/_user.py: -------------------------------------------------------------------------------- 1 | 2 | from tsugu_api_core._network import Api 3 | from tsugu_api_core._settings import settings 4 | from tsugu_api_core._typing import ( 5 | ServerId, 6 | _BindingAction, 7 | PartialTsuguUser, 8 | _GetUserDataResponse, 9 | _ChangeUserDataResponse, 10 | _BindPlayerRequestResponse, 11 | _BindPlayerVerificationResponse 12 | ) 13 | 14 | async def get_user_data(platform: str, user_id: str) -> _GetUserDataResponse: 15 | '''获取用户数据 16 | 17 | 参数: 18 | platform (str): 平台名称 19 | user_id (str): 用户 ID 20 | 21 | 返回: 22 | _GetUserDataResponse: API 返回响应 23 | ''' 24 | 25 | # 构建数据 26 | data = { 27 | 'platform': platform, 28 | 'userId': user_id 29 | } 30 | 31 | # 发送请求 32 | return (await Api( 33 | settings.userdata_backend_url, 34 | '/user/getUserData', 35 | proxy=settings.userdata_backend_proxy 36 | ).apost(data)).json() 37 | 38 | async def change_user_data(platform: str, user_id: str, update: PartialTsuguUser) -> _ChangeUserDataResponse: 39 | '''修改用户数据 40 | 41 | 参数: 42 | platform (str): 平台名称 43 | user_id (str): 用户 ID 44 | update (_PartialTsuguUser): 更新数据 45 | 46 | 返回: 47 | _ChangeUserDataResponse: API 返回响应 48 | ''' 49 | 50 | # 构建数据 51 | data = { 52 | 'platform': platform, 53 | 'userId': user_id, 54 | 'update': update 55 | } 56 | 57 | # 发送请求 58 | return (await Api( 59 | settings.userdata_backend_url, 60 | '/user/changeUserData', 61 | proxy=settings.userdata_backend_proxy 62 | ).apost(data)).json() 63 | 64 | async def bind_player_request( 65 | platform: str, 66 | user_id: str 67 | ) -> _BindPlayerRequestResponse: 68 | '''绑定玩家请求 69 | 70 | 参数: 71 | platform (str): 平台名称 72 | user_id (str): 用户 ID 73 | 74 | 返回: 75 | _BindPlayerRequestResponse: 请求返回数据 76 | ''' 77 | 78 | # 构建数据 79 | data = { 80 | 'platform': platform, 81 | 'userId': user_id 82 | } 83 | 84 | # 发送请求 85 | return (await Api( 86 | settings.userdata_backend_url, 87 | '/user/bindPlayerRequest', 88 | proxy=settings.userdata_backend_proxy, 89 | ).apost(data)).json() 90 | 91 | async def bind_player_verification( 92 | platform: str, 93 | user_id: str, 94 | server: ServerId, 95 | player_id: int, 96 | binding_action: _BindingAction 97 | ) -> _BindPlayerVerificationResponse: 98 | '''绑定玩家验证 99 | 100 | 参数: 101 | platform (str): 平台名称 102 | user_id (str): 用户 ID 103 | server (_ServerId): 服务器编号 0 - 日服 1 - 国际服 2 - 台服 3 - 国服 4 - 韩服 104 | player_id (int): 玩家 ID 105 | binding_action (_BindingAction): 绑定操作 106 | 107 | 返回: 108 | _BindPlayerVerificationResponse: 验证返回数据 109 | ''' 110 | 111 | # 构建数据 112 | data = { 113 | 'platform': platform, 114 | 'userId': user_id, 115 | 'server': server, 116 | 'playerId': player_id, 117 | 'bindingAction': binding_action 118 | } 119 | 120 | # 发送请求 121 | return (await Api( 122 | settings.userdata_backend_url, 123 | '/user/bindPlayerVerification', 124 | proxy=settings.userdata_backend_proxy 125 | ).apost(data)).json() 126 | -------------------------------------------------------------------------------- /tsugu_api_core/_typing.py: -------------------------------------------------------------------------------- 1 | ''' 2 | `tsugu_api_core._typing` 3 | 4 | 定义了一些类型别名 5 | ''' 6 | 7 | from typing import ( 8 | Any, 9 | Dict, 10 | List, 11 | Union, 12 | Literal, 13 | TypeAlias, 14 | TypedDict, 15 | TypeGuard, 16 | ) 17 | from typing_extensions import NotRequired 18 | 19 | ServerId: TypeAlias = Literal[0, 1, 2, 3, 4] 20 | ''' 21 | 服务器 ID 22 | 23 | 值: 24 | 0: 日服 25 | 1: 国际服 26 | 2: 台服 27 | 3: 国服 28 | 4: 韩服 29 | ''' 30 | SERVER_ID = (0, 1, 2, 3, 4) 31 | ServerName: TypeAlias = Literal['jp', 'en', 'tw', 'cn', 'kr'] 32 | ''' 33 | 服务器名 34 | 35 | 值: 36 | 'jp': 日服 37 | 'en': 国际服 38 | 'tw': 台服 39 | 'cn': 国服 40 | 'kr': 韩服 41 | ''' 42 | SERVER_NAME = ('jp', 'en', 'tw', 'cn', 'kr') 43 | Server: TypeAlias = Union[ServerId, ServerName] 44 | '''服务器''' 45 | SERVER = (0, 1, 2, 3, 4, 'jp', 'en', 'tw', 'cn', 'kr') 46 | 47 | def is_server(server: Any) -> TypeGuard[Server]: 48 | return server in SERVER 49 | 50 | def is_server_list(server_list: List[Any]) -> TypeGuard[List[Server]]: 51 | for server in server_list: 52 | if not is_server(server): 53 | return False 54 | return True 55 | 56 | class _Data(TypedDict): 57 | '''API 单个响应数据''' 58 | type: Literal['string', 'base64'] 59 | string: str 60 | 61 | _Response: TypeAlias = List[_Data] 62 | 63 | class _Error(TypedDict): 64 | '''错误信息''' 65 | type: str 66 | '''错误类型''' 67 | location: Literal['body', 'cookies', 'headers', 'params', 'query'] 68 | '''参数位置''' 69 | msg: str 70 | '''错误信息''' 71 | path: str 72 | '''参数名称''' 73 | value: Any 74 | '''参数值''' 75 | 76 | class _BadRequestResponse(TypedDict): 77 | '''参数错误响应信息''' 78 | status: Literal['failed'] 79 | data: str 80 | error: List[_Error] 81 | 82 | class _FailedResponse(TypedDict): 83 | '''失败响应信息''' 84 | status: Literal['failed'] 85 | data: str 86 | 87 | _Status: TypeAlias = Literal['success', 'failed'] 88 | '''响应状态''' 89 | 90 | FuzzySearchResult: TypeAlias = Dict[str, List[Union[str, int]]] 91 | 92 | class _FuzzySearchResponse(TypedDict): 93 | '''`/fuzzySearch` 响应结果''' 94 | status: _Status 95 | data: FuzzySearchResult 96 | 97 | _DifficultyId: TypeAlias = Literal[0, 1, 2, 3, 4] 98 | '''难度 ID''' 99 | 100 | class _UserPlayerInList(TypedDict): 101 | playerId: int 102 | server: ServerId 103 | 104 | class _SubmitResponse(TypedDict): 105 | '''`/station/submitRoomNumber` 响应结果''' 106 | status: _Status 107 | data: str 108 | 109 | class _Room(TypedDict): 110 | '''房间数据''' 111 | number: int 112 | rawMessage: str 113 | source: str 114 | userId: str 115 | time: int 116 | player: NotRequired[_UserPlayerInList] 117 | avatarUrl: NotRequired[str] 118 | userName: NotRequired[str] 119 | 120 | class _QueryResponse(TypedDict): 121 | '''`/station/queryAllRoom` 响应结果''' 122 | status: _Status 123 | data: List[_Room] 124 | 125 | class _TsuguUserServer(TypedDict): 126 | '''服务器数据''' 127 | playerId: int 128 | bindingStatus: Literal[0, 1, 2] 129 | verifyCode: NotRequired[int] 130 | 131 | class _TsuguUser(TypedDict): 132 | '''用户数据''' 133 | userId: str 134 | '''用户 ID''' 135 | platform: str 136 | '''平台''' 137 | mainServer: ServerId 138 | '''主服务器''' 139 | displayedServerList: List[ServerId] 140 | '''显示的服务器列表''' 141 | shareRoomNumber: bool 142 | '''是否分享房间号''' 143 | userPlayerIndex: int 144 | '''用户账号索引''' 145 | userPlayerList: List[_UserPlayerInList] 146 | '''用户账号列表''' 147 | 148 | class _GetUserDataResponse(TypedDict): 149 | '''`/user/getUserData` 响应结果''' 150 | status: _Status 151 | data: _TsuguUser 152 | 153 | class PartialTsuguUser(TypedDict): 154 | '''更新数据''' 155 | userId: NotRequired[str] 156 | '''用户 ID''' 157 | platform: NotRequired[str] 158 | '''平台''' 159 | mainServer: NotRequired[ServerId] 160 | '''主服务器''' 161 | displayedServerList: NotRequired[List[ServerId]] 162 | '''显示的服务器列表''' 163 | shareRoomNumber: NotRequired[bool] 164 | '''是否分享房间号''' 165 | userPlayerIndex: NotRequired[int] 166 | '''用户账号索引''' 167 | userPlayerList: NotRequired[List[_UserPlayerInList]] 168 | '''用户账号列表''' 169 | 170 | class _ChangeUserDataResponse(TypedDict): 171 | '''`/user/changeUserData` 响应结果''' 172 | status: _Status 173 | 174 | class _VerifyCode(TypedDict): 175 | '''验证码''' 176 | verifyCode: int 177 | 178 | _BindingAction: TypeAlias = Literal['bind', 'unbind'] 179 | '''绑定操作''' 180 | 181 | class _BindPlayerRequestResponse(TypedDict): 182 | '''`/user/bindPlayerRequest` 绑定响应''' 183 | status: _Status 184 | data: _VerifyCode 185 | 186 | class _BindPlayerVerificationResponse(TypedDict): 187 | '''`/user/bindPlayerVerification` 响应结果''' 188 | status: _Status 189 | data: str 190 | 191 | class _SourceInfo(TypedDict): 192 | '''来源信息''' 193 | name: str 194 | type: str 195 | 196 | class _UserInfo(TypedDict): 197 | '''用户信息''' 198 | avatar: str 199 | bandori_player_brief_info: Any 200 | role: int 201 | type: str 202 | user_id: int 203 | username: str 204 | 205 | class StationRoom(TypedDict): 206 | '''车站房间数据''' 207 | number: int 208 | raw_message: str 209 | source_info: _SourceInfo 210 | time: int 211 | type: str 212 | user_info: _UserInfo 213 | -------------------------------------------------------------------------------- /tsugu_api_core/client/httpx.py: -------------------------------------------------------------------------------- 1 | '''内置的 HTTPX 客户端适配''' 2 | 3 | import time 4 | import asyncio 5 | from json import dumps 6 | from typing import Any, cast 7 | from typing_extensions import override 8 | 9 | from ._client import Client as _Client 10 | from ._client import Request, Response 11 | 12 | try: 13 | import httpx 14 | except ModuleNotFoundError as exception: 15 | raise ImportError( 16 | 'module \'httpx\' is not installed, please install it by running \'pip install httpx\'' 17 | ) from exception 18 | 19 | __HTTPX_ABOVE_0_28_0__ : bool = tuple(httpx.__version__.split('.')) >= ('0', '28', '0') 20 | 21 | class Client(_Client): 22 | '''HTTPX 客户端适配''' 23 | _client: httpx.Client 24 | '''HTTPX 客户端''' 25 | _async_client: httpx.AsyncClient 26 | '''异步 HTTPX 客户端''' 27 | 28 | @override 29 | def __enter__(self) -> 'Client': 30 | if __HTTPX_ABOVE_0_28_0__: 31 | self._client = httpx.Client( 32 | proxy=self.proxy, 33 | timeout=self.timeout, 34 | trust_env=False, 35 | ) 36 | else: 37 | proxies = { 38 | 'http://': self.proxy, 39 | 'https://': self.proxy, 40 | } if self.proxy else None 41 | 42 | self._client = httpx.Client( 43 | proxies=cast(dict, proxies), 44 | timeout=self.timeout, 45 | trust_env=False, 46 | ) 47 | 48 | self._client.__enter__() 49 | return self 50 | 51 | @override 52 | async def __aenter__(self) -> 'Client': 53 | if __HTTPX_ABOVE_0_28_0__: 54 | self._async_client = httpx.AsyncClient( 55 | proxy=self.proxy, 56 | timeout=self.timeout, 57 | trust_env=False, 58 | ) 59 | else: 60 | proxies = { 61 | 'http://': self.proxy, 62 | 'https://': self.proxy, 63 | } if self.proxy else None 64 | 65 | self._async_client = httpx.AsyncClient( 66 | proxies=cast(dict, proxies), 67 | timeout=self.timeout, 68 | trust_env=False, 69 | ) 70 | 71 | await self._async_client.__aenter__() 72 | return self 73 | 74 | @override 75 | def __exit__(self, *args: Any) -> None: 76 | self._client.__exit__(*args) 77 | 78 | @override 79 | async def __aexit__(self, *args: Any) -> None: 80 | await self._async_client.__aexit__(*args) 81 | 82 | @override 83 | def request(self, request: Request) -> Response: 84 | if __HTTPX_ABOVE_0_28_0__: 85 | _request = httpx.Request( 86 | request.method, 87 | request.url, 88 | params=request.params, 89 | content=cast(dict, dumps(request.data)) if request.data is not None else request.data, 90 | headers=request.headers, 91 | ) 92 | else: 93 | _request = httpx.Request( 94 | request.method, 95 | request.url, 96 | params=request.params, 97 | data=cast(dict, dumps(request.data)) if request.data is not None else request.data, 98 | headers=request.headers, 99 | ) 100 | 101 | retries = 0 102 | while True: 103 | try: 104 | _response = self._client.send(_request) 105 | break 106 | except Exception as exception: 107 | if retries >= self.max_retries: 108 | raise exception 109 | retries += 1 110 | time.sleep(0.5) 111 | 112 | # 处理响应 113 | try: 114 | _response.raise_for_status() 115 | return Response( 116 | _response.content, 117 | _response.status_code, 118 | ) 119 | except httpx.HTTPStatusError as exception: 120 | return Response( 121 | exception.response.content, 122 | exception.response.status_code, 123 | exception, 124 | ) 125 | 126 | @override 127 | async def arequest(self, request: Request) -> Response: 128 | if __HTTPX_ABOVE_0_28_0__: 129 | _request = httpx.Request( 130 | request.method, 131 | request.url, 132 | params=request.params, 133 | content=cast(dict, dumps(request.data)) if request.data is not None else request.data, 134 | headers=request.headers, 135 | ) 136 | else: 137 | _request = httpx.Request( 138 | request.method, 139 | request.url, 140 | params=request.params, 141 | data=cast(dict, dumps(request.data)) if request.data is not None else request.data, 142 | headers=request.headers, 143 | ) 144 | 145 | retries = 0 146 | while True: 147 | try: 148 | _response = await self._async_client.send(_request) 149 | break 150 | except Exception as exception: 151 | if retries >= self.max_retries: 152 | raise exception 153 | retries += 1 154 | await asyncio.sleep(0.5) 155 | 156 | # 处理响应 157 | try: 158 | _response.raise_for_status() 159 | return Response( 160 | _response.content, 161 | _response.status_code, 162 | ) 163 | except httpx.HTTPStatusError as exception: 164 | return Response( 165 | exception.response.content, 166 | exception.response.status_code, 167 | exception, 168 | ) -------------------------------------------------------------------------------- /tsugu_api_core/_network.py: -------------------------------------------------------------------------------- 1 | '''`tsugu_api_async._network` 2 | 3 | 向 Tsugu 后端发送请求相关模块''' 4 | from typing import Any, Literal, Optional 5 | 6 | from ._settings import settings 7 | from .client import _Client, Request, Response 8 | from .exception import BadRequestError, FailedException, HTTPStatusError 9 | 10 | BANDORI_STATION_URL = 'https://api.bandoristation.com' 11 | 12 | # 向后端发送 API 请求类 13 | class Api: 14 | '''向后端发送 API 请求类 15 | 16 | 参数: 17 | host (str): 请求的主机地址 18 | api (str): 请求的 API 路径 19 | proxy (bool): 是否使用代理服务器 20 | ''' 21 | host: str 22 | '''请求的主机地址''' 23 | api: str 24 | '''请求的 API 路径''' 25 | proxy: bool 26 | '''是否使用代理服务器''' 27 | # 初始化 28 | def __init__( 29 | self, 30 | host: str, 31 | api: str, 32 | proxy: bool 33 | ) -> None: 34 | '''初始化''' 35 | self.host = host.rstrip('/') 36 | self.api = api 37 | self.proxy = proxy 38 | return 39 | 40 | # 请求发送 41 | async def _arequest( 42 | self, 43 | method: Literal['get', 'post'], 44 | *, 45 | params: Optional[dict[str, Any]]=None, 46 | data: Optional[dict[str, Any]]=None 47 | ) -> Response: 48 | '''异步请求发送 49 | 50 | 参数: 51 | method (Literal['get', 'post']): API 调用方法 52 | params (Optional[dict[str, Any]]): 请求的参数 53 | data (Optional[dict[str, Any]]): 请求的数据 54 | 55 | 返回: 56 | Response: 收到的响应 57 | ''' 58 | # 构建请求头 59 | if method == 'post': 60 | headers = {'Content-Type': 'application/json'} 61 | else: 62 | headers = None 63 | 64 | # 发送请求并获取响应 65 | async with _Client()( 66 | proxy=settings.proxy if self.proxy and settings.proxy else None, 67 | timeout=settings.timeout, 68 | max_retries=settings.max_retries, 69 | ) as client: 70 | # 构建一个请求体 71 | request = Request( 72 | method, 73 | self.host + self.api, 74 | params=params, 75 | data=data, 76 | headers=headers 77 | ) 78 | 79 | response = await client.arequest(request) 80 | 81 | # 处理接收到的响应 82 | if response.status_code == 200: 83 | return response 84 | if response.status_code == 400: 85 | if self.host == BANDORI_STATION_URL: 86 | return response 87 | _response: dict[str, Any] = response.json() 88 | raise BadRequestError(self.api, _response) from response.exception # type: ignore 89 | elif response.status_code in (404, 409, 422, 500): 90 | _response: dict[str, Any] = response.json() 91 | raise FailedException(self.api, response.status_code, _response) from response.exception # type: ignore 92 | else: 93 | raise HTTPStatusError(response.status_code) from response.exception 94 | 95 | async def apost(self, data: Optional[dict[str, Any]]=None) -> Response: 96 | '''异步发送 POST 请求 97 | 98 | 参数: 99 | data (Optional[dict[str, Any]]): 请求的数据 100 | 101 | 返回: 102 | Response: 收到的响应 103 | ''' 104 | return await self._arequest('post', data=data) 105 | 106 | async def aget(self, params: Optional[dict[str, Any]]=None) -> Response: 107 | '''异步发送 GET 请求 108 | 109 | 参数: 110 | params (Optional[dict[str, Any]]): 请求的参数 111 | 112 | 返回: 113 | _ApiResponse: 收到的响应 114 | ''' 115 | return await self._arequest('get', params=params) 116 | 117 | # 请求发送 118 | def _request( 119 | self, 120 | method: Literal['get', 'post'], 121 | *, 122 | params: Optional[dict[str, Any]]=None, 123 | data: Optional[dict[str, Any]]=None 124 | ) -> Response: 125 | '''请求发送 126 | 127 | 参数: 128 | method (Literal['get', 'post']): API 调用方法 129 | params (Optional[dict[str, Any]]): 请求的参数 130 | data (Optional[dict[str, Any]]): 请求的数据 131 | 132 | 返回: 133 | Response: 收到的响应 134 | ''' 135 | # 构建请求头 136 | if method == 'post': 137 | headers = {'Content-Type': 'application/json'} 138 | else: 139 | headers = None 140 | 141 | # 构建一个请求体 142 | request = Request( 143 | method, 144 | self.host + self.api, 145 | params=params, 146 | data=data, 147 | headers=headers 148 | ) 149 | 150 | # 发送请求并获取响应 151 | with _Client()( 152 | proxy=settings.proxy if self.proxy and settings.proxy else None, 153 | timeout=settings.timeout, 154 | max_retries=settings.max_retries, 155 | ) as client: 156 | response = client.request(request) 157 | 158 | # 处理接收到的响应 159 | if response.status_code == 200: 160 | return response 161 | if response.status_code == 400: 162 | if self.host == BANDORI_STATION_URL: 163 | return response 164 | _response: dict[str, Any] = response.json() 165 | raise BadRequestError(self.api, _response) from response.exception # type: ignore 166 | elif response.status_code in (409, 422, 500): 167 | _response: dict[str, Any] = response.json() 168 | raise FailedException(self.api, response.status_code, _response) from response.exception # type: ignore 169 | else: 170 | raise HTTPStatusError(response.status_code) from response.exception 171 | 172 | def post(self, data: Optional[dict[str, Any]]=None) -> Response: 173 | '''发送 POST 请求 174 | 175 | 参数: 176 | data (Optional[dict[str, Any]]): 请求的数据 177 | 178 | 返回: 179 | Response: 收到的响应 180 | ''' 181 | return self._request('post', data=data) 182 | 183 | def get(self, params: Optional[dict[str, Any]]=None) -> Response: 184 | '''发送 GET 请求 185 | 186 | 参数: 187 | params (Optional[dict[str, Any]]): 请求的参数 188 | 189 | 返回: 190 | Response: 收到的响应 191 | ''' 192 | return self._request('get', params=params) 193 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |