diff --git a/backend/api/core/Auth.py b/backend/api/core/Auth.py index f1fefd92..5f6b63b1 100644 --- a/backend/api/core/Auth.py +++ b/backend/api/core/Auth.py @@ -2,10 +2,8 @@ import base64 import hmac import json import time -from typing import Optional -from core.character.adventure import get_adv_info from core.basic.character import Character -from fastapi import Header, Request +from fastapi import Header from api.core.exception import ResponseException @@ -67,7 +65,7 @@ def deleteToken(token, redis, env): return token -def uid(access_uid: Optional[str] = Header(None)): +def uid(access_uid: str | None = Header(None)): try: return int(access_uid) except Exception: diff --git a/backend/api/core/Gzip.py b/backend/api/core/Gzip.py index 2704b424..351035b8 100644 --- a/backend/api/core/Gzip.py +++ b/backend/api/core/Gzip.py @@ -1,7 +1,7 @@ import gzip from fastapi import Request, APIRouter, FastAPI from fastapi.responses import Response -from typing import Callable +from collections.abc import Callable from fastapi.middleware.gzip import GZipMiddleware from fastapi.routing import APIRoute diff --git a/backend/api/core/Redis.py b/backend/api/core/Redis.py index 87c449dd..7503971d 100644 --- a/backend/api/core/Redis.py +++ b/backend/api/core/Redis.py @@ -10,7 +10,7 @@ def get_redis_info(redis, key, fun,without_gzip=False): info = json.loads(redis.get(key)) else: info = json.loads(gzip_unzip(redis.get(key))) - except Exception as ex: + except Exception: info = None if info is None: info = fun() diff --git a/backend/api/core/Response.py b/backend/api/core/Response.py index 86200f37..df7a5624 100644 --- a/backend/api/core/Response.py +++ b/backend/api/core/Response.py @@ -1,20 +1,20 @@ import base64 import json from typing import Generic, TypeVar +from pydantic import Field from pydantic.generics import GenericModel import gzip # 导入 gzip 模块 from io import BytesIO # 从 io 模块导入 BytesIO 类 - T = TypeVar('T') - class Return(GenericModel, Generic[T]): - code: int = 200 - message: str = '' - data: T | dict | str = None - - + code: int = Field(..., description='响应状态码') + """响应状态码""" + message: str = Field(..., description='响应消息') + """响应消息""" + data: T = Field(..., description='响应数据') + """响应数据""" # 定义一个函数,将字符串压缩为 gzip 格式,并进行 base64 编码后返回结果 def gzip_str(to_gzip: str) -> str: diff --git a/backend/api/dp.py b/backend/api/dp.py index 015886f3..455dde29 100644 --- a/backend/api/dp.py +++ b/backend/api/dp.py @@ -1,5 +1,5 @@ import time -from typing import Annotated, Optional +from typing import Annotated from redis import Redis from sqlalchemy.orm import Session from fastapi import Depends, Header @@ -16,7 +16,7 @@ SessionDep = Annotated[Session, Depends(get_db)] RedisDep = Annotated[Redis, Depends(get_redis)] -def alterToken(redis:RedisDep,alter_token: Optional[str] = Header(None)): +def alterToken(redis:RedisDep,alter_token: str | None = Header(None)): if alter_token is not None: try: decoded_bytes = base64.b64decode(alter_token) diff --git a/backend/api/routes/openapi.py b/backend/api/routes/openapi.py index 78e99f2b..4761cb17 100644 --- a/backend/api/routes/openapi.py +++ b/backend/api/routes/openapi.py @@ -2,20 +2,19 @@ from fastapi.params import Param from api.core.Gzip import register_gzip_request import json from fastapi import APIRouter, Path -from api.core.Response import response,Return +from api.core.Response import response from api.core.Redis import get_redis_info from api.dp import RedisDep import re -from typing import TYPE_CHECKING, Annotated +from typing import Annotated -from api.types.open import SkillInfo,SkillDetail -if TYPE_CHECKING: - from api.core.Response import Return +from api.types.open import SkillDetailResponse, SkillsListResponse router = APIRouter() register_gzip_request(router) + def replace_placeholders(template): # 匹配 、、、 等 pattern = re.compile(r'<(int|float\d*)>') @@ -29,64 +28,57 @@ def replace_placeholders(template): return pattern.sub(repl, template) -@router.get('/skills/') -async def get_skills_list(redis: RedisDep,skillName: str) -> Return[list[SkillInfo]]: +@router.get('/skills/', operation_id='skillsList') +async def get_skills_list(redis: RedisDep, skillName: Annotated[str, Param(..., description='技能名称,支持模糊查询')])->SkillsListResponse: """ 根据技能名称,获取对应的技能列表信息 """ key = 'openapi:skills' + def get_skills(): # This function should retrieve the skills list from the data source # For now, we return a placeholder list with open('./openapi/skills.json', encoding='utf-8') as f: skills_data = json.load(f) return skills_data + skills_list = get_redis_info(redis, key, get_skills) - data = list( - filter( - lambda x: re.sub(r'\s+', '', skillName) == re.sub(r'\s+', '', x["skillName"]), - skills_list - ) - ) if skillName else skills_list + data = list(filter(lambda x: re.sub(r'\s+', '', skillName) in re.sub(r'\s+', '', x['skillName']), skills_list)) if skillName else skills_list return response(data=data) -@router.get('/{job}/{jobGrow}/{skillId}/') -async def get_cn_skll_info(redis: RedisDep, - job: Annotated[str,Path(..., title='职业')], - jobGrow: Annotated[str,Path(..., title='转职')], - skillId: Annotated[str,Path(..., title='技能')], - level: Annotated[int,Param(..., title="技能等级")] - ) -> Return[SkillDetail]: +@router.get('/{jobId}/{jobGrowId}/{skillId}/', operation_id='skillDetail') +async def get_cn_skill_info(redis: RedisDep, jobId: Annotated[str, Path(..., description='职业')], jobGrowId: Annotated[str, Path(..., description='转职')], skillId: Annotated[str, Path(..., description='技能')], level: Annotated[int, Param(..., description='技能等级')]) -> SkillDetailResponse: """ 获取技能详细信息 """ skill_info = {} - key = f'openapi:{job}:{jobGrow}:{skillId}' + key = f'openapi:{jobId}:{jobGrowId}:{skillId}' try: + def get_skill_info(): # This function should retrieve the skill info based on job and jobGrow # For now, we return a placeholder dictionary - with open(f'./openapi/{job}/{jobGrow}/cn/skillDetail/{skillId}.json', encoding='utf-8') as f: + with open(f'./openapi/{jobId}/{jobGrowId}/cn/skillDetail/{skillId}.json', encoding='utf-8') as f: skill_data = json.load(f) - if "levelInfo" in skill_data and "optionDesc" in skill_data["levelInfo"] and skill_data["levelInfo"]["optionDesc"] is not None: - skill_data["levelInfo"]["optionDesc"] = replace_placeholders(skill_data["levelInfo"]["optionDesc"]) + if 'levelInfo' in skill_data and 'optionDesc' in skill_data['levelInfo'] and skill_data['levelInfo']['optionDesc'] is not None: + skill_data['levelInfo']['optionDesc'] = replace_placeholders(skill_data['levelInfo']['optionDesc']) return skill_data skill_info = get_redis_info(redis, key, get_skill_info) if level is not None: - level = max(0,min(level,skill_info.get("maxLevel",0))) - if "levelInfo" in skill_info and "rows" in skill_info["levelInfo"]: - detail = list(filter(lambda x: x["level"] == level, skill_info["levelInfo"]["rows"])) + level = max(0, min(level, skill_info.get('maxLevel', 0))) + if 'levelInfo' in skill_info and 'rows' in skill_info['levelInfo']: + detail = list(filter(lambda x: x['level'] == level, skill_info['levelInfo']['rows'])) if detail and len(detail) > 0: - skill_info["levelInfo"].update(detail[0]) - skill_info["levelInfo"]["detail"] = skill_info["levelInfo"].get("optionDesc", "") - for key in skill_info["levelInfo"]["optionValue"].keys(): - skill_info["levelInfo"]["detail"] = skill_info["levelInfo"]["detail"].replace(f"{{{key}}}",str(skill_info["levelInfo"]["optionValue"][key])) - del skill_info["levelInfo"]["rows"] - skill_info["attribute"] = {} - skill_info["attribute"].update(skill_info["levelInfo"]) - del skill_info["levelInfo"] + skill_info['levelInfo'].update(detail[0]) + skill_info['levelInfo']['detail'] = skill_info['levelInfo'].get('optionDesc', '') + for key in skill_info['levelInfo']['optionValue'].keys(): + skill_info['levelInfo']['detail'] = skill_info['levelInfo']['detail'].replace(f'{{{key}}}', str(skill_info['levelInfo']['optionValue'][key])) + del skill_info['levelInfo']['rows'] + skill_info['attribute'] = {} + skill_info['attribute'].update(skill_info['levelInfo']) + del skill_info['levelInfo'] except FileNotFoundError: - return response(code=404, message=f"技能信息未找到: {job} {jobGrow} {skillId}", data=None) + return response(code=404, message=f'技能信息未找到: {jobId} {jobGrowId} {skillId}', data=None) return response(data=skill_info) diff --git a/backend/api/types/open.py b/backend/api/types/open.py index b2c76959..f813ba98 100644 --- a/backend/api/types/open.py +++ b/backend/api/types/open.py @@ -1,16 +1,22 @@ from enum import Enum from pydantic import BaseModel, Field - +from typing import Generic, TypeVar +from pydantic.generics import GenericModel +from api.core.Response import Return as Response class SkillInfo(BaseModel): skillId: str = Field(..., description='技能ID') """技能ID""" skillName: str = Field(..., description='技能名称') """技能名称""" - job: str = Field(..., description='职业') - """职业""" - jobGrow: str = Field(..., description='转职') - """转职""" + jobId: str = Field(..., description='职业Id') + """职业Id""" + jobName: str = Field(..., description='职业名称') + """职业名称""" + jobGrowId: str = Field(..., description='转职Id') + """转职Id""" + jobGrowName: str = Field(..., description='转职名称') + """转职名称""" class SkillType(str, Enum): @@ -33,36 +39,38 @@ class IConsumeItem(BaseModel): """物品ID""" itemName: str = Field(..., description='物品名称') """物品名称""" - value: str = Field(..., description='物品类型') + value: int = Field(..., description='物品类型') """物品类型""" + class IPreSkill(BaseModel): - level: str = Field(..., description='前置技能等级') + level: int = Field(..., description='前置技能等级') """前置技能等级""" name: str = Field(..., description='前置技能名称') """前置技能名称""" - skillId: int = Field(..., description='前置技能ID') + skillId: str = Field(..., description='前置技能ID') """前置技能ID""" + class IAttribute(BaseModel): - castingTime: float | None = Field(..., description='施放时间') + castingTime: float | None = Field(None, description='施放时间') """施放时间""" - coolTime: float | None = Field(..., description='冷却时间') + coolTime: float | None = Field(None, description='冷却时间') """冷却时间""" - consumeMp: int | None = Field(..., description='魔法值') + consumeMp: int | None = Field(None, description='魔法值') """魔法值""" - detail: str | None = Field(..., description='技能描述') + detail: str | None = Field(None, description='技能描述') """技能描述""" - level: int | None = Field(..., description='技能等级') + level: int | None = Field(None, description='技能等级') """技能等级""" - optionDesc: str | None = Field(..., description='技能选项描述') + optionDesc: str | None = Field(None, description='技能选项描述') """技能选项描述""" - optionValue: dict[str, float] | None = Field(..., description='技能选项值') + optionValue: dict[str, float | str] | None = Field(None, description='技能选项值') class ILevelInfo(BaseModel): - optionDesc: str | None = Field(..., description='技能选项描述') - rows: list[IAttribute] | None = Field(..., description='技能等级信息') + optionDesc: str | None = Field(None, description='技能选项描述') + rows: list[IAttribute] | None = Field(None, description='技能等级信息') class IEvolution(BaseModel): @@ -70,23 +78,23 @@ class IEvolution(BaseModel): """进化类型""" name: str = Field(..., description='进化名称') """进化名称""" - desc: str | None = Field(..., description='进化简介') + desc: str = Field(..., description='进化简介') """进化简介""" - descDetail: str | None = Field(..., description='进化详细描述') + descDetail: str = Field(..., description='进化详细描述') """进化详细描述""" class IKeyValues(BaseModel): name: str = Field(..., description='名称') """名称""" - value: str | None = Field(..., description='数值') + value: str = Field(..., description='数值') """数值""" class IEnhancement(BaseModel): - type: str = Field(..., description='强化类型') + type: int = Field(None, description='强化类型') """强化类型""" - status: list[IKeyValues] | None = Field(..., description='强化状态') + status: list[IKeyValues] = Field(None, description='强化状态') class SkillDetail(BaseModel): @@ -94,31 +102,34 @@ class SkillDetail(BaseModel): """技能名称""" type: SkillType = Field(..., description='技能类型') """技能类型""" - desc: str | None = Field(..., description='技能描述') + desc: str | None = Field(None, description='技能描述') """技能描述""" - descDetail: str | None = Field(..., description='技能详细描述') + descDetail: str | None = Field(None, description='技能详细描述') """技能详细描述""" - consumeItem: IConsumeItem | None = Field(..., description='消耗物品') + consumeItem: IConsumeItem | None = Field(None, description='消耗物品') """消耗物品""" - descSpecial: str | None = Field(..., description='特殊功能') + descSpecial: list[str] | None = Field(None, description='特殊功能') """特殊功能""" maxLevel: int = Field(..., description='等级上限') """等级上限""" - requiredLevel: int = Field(1, description='学习等级') + requiredLevel: int = Field(..., description='学习等级') """学习等级""" - requiredLevelRange: str | None = Field(..., description='学习间隔') + requiredLevelRange: int = Field(..., description='学习间隔') """学习间隔""" - preRequiredSkill: list[IPreSkill] | None = Field(..., description='前置技能') + preRequiredSkill: list[IPreSkill] | None = Field(None, description='前置技能') """前置技能""" - jobId: str | None = Field(..., description='职业ID') + jobId: str = Field(..., description='职业ID') """职业ID""" - jobName: str | None = Field(..., description='职业名称') + jobName: str = Field(..., description='职业名称') """职业名称""" - levelInfo: list[ILevelInfo] | None = Field(..., description='技能等级信息') + levelInfo: list[ILevelInfo] | None = Field(None, description='技能等级信息') """技能等级信息""" - attribute: IAttribute | None = Field(..., description='技能属性') + attribute: IAttribute | None = Field(None, description='技能属性') """技能属性""" - evolution: list[IEvolution] | None = Field(..., description='技能进化信息') + evolution: list[IEvolution] | None = Field(None, description='技能进化信息') """技能进化信息""" - enhancement: list[IEnhancement] | None = Field(..., description='技能强化信息') + enhancement: list[IEnhancement] | None = Field(None, description='技能强化信息') """技能强化信息""" + +SkillDetailResponse = Response[SkillDetail] +SkillsListResponse = Response[list[SkillInfo]] diff --git a/backend/main.py b/backend/main.py index ca7cd7c8..1a6ac541 100644 --- a/backend/main.py +++ b/backend/main.py @@ -3,15 +3,18 @@ from api.main import api_router from fastapi import FastAPI from multiprocessing import freeze_support from api.core import Exceptions,Middleware,Gzip +from fastapi_mcp import FastApiMCP import uvicorn - -application = FastAPI(docs_url=None, redoc_url=None) +application = FastAPI(docs_url='/doc', redoc_url=None) application.add_exception_handler(Exception, Exceptions.all_exception_handler) Middleware.register_cors(application) Gzip.register_gzip_response(application) application.include_router(api_router) +mcp = FastApiMCP(application,name="dcalc-mcp",description="Dcalc MCP Service",include_operations=["skillsList", "skillDetail"]) +mcp.mount() + if __name__ == '__main__': freeze_support() diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 8d97294c..712e7da2 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -5,6 +5,7 @@ description = "A backend project for dcalc, a calculator application." readme = "README.md" requires-python = ">=3.10" dependencies = [ + "fastapi-mcp>=0.3.4", "fastapi[all]>=0.115.11", "redis>=5.2.1", "requests>=2.32.3", diff --git a/backend/uv.lock b/backend/uv.lock index 31713561..a2f4453a 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -2,10 +2,6 @@ version = 1 revision = 1 requires-python = ">=3.10" -[[tool.uv.index]] -url = "https://pypi.tuna.tsinghua.edu.cn/simple" -default = true - [[package]] name = "annotated-types" version = "0.7.0" @@ -136,6 +132,7 @@ version = "0.1.0" source = { virtual = "." } dependencies = [ { name = "fastapi", extra = ["all"] }, + { name = "fastapi-mcp" }, { name = "redis" }, { name = "requests" }, { name = "sqlalchemy" }, @@ -144,6 +141,7 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "fastapi", extras = ["all"], specifier = ">=0.115.11" }, + { name = "fastapi-mcp", specifier = ">=0.3.4" }, { name = "redis", specifier = ">=5.2.1" }, { name = "requests", specifier = ">=2.32.3" }, { name = "sqlalchemy", specifier = ">=2.0.38" }, @@ -229,6 +227,27 @@ standard = [ { name = "uvicorn", extra = ["standard"] }, ] +[[package]] +name = "fastapi-mcp" +version = "0.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastapi" }, + { name = "httpx" }, + { name = "mcp" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "requests" }, + { name = "rich" }, + { name = "tomli" }, + { name = "typer" }, + { name = "uvicorn" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/93/f0cc88f6ec51ce9c92570d097c3e94a6fe5c98cf9f372504456fc29cb33c/fastapi_mcp-0.3.4.tar.gz", hash = "sha256:2a83dde5b249cfa6276f0ee90958387bb8c9c430f1a6213f559ead757dfe18b2", size = 164950 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/4e/63e2ae715135406b0d3c74c301743f470888740bd4ed0ebd170a8f005ca3/fastapi_mcp-0.3.4-py3-none-any.whl", hash = "sha256:b2bcddd0514201c6434b970263f8f4e9e8e1f494ea93fdc0de251e19337e19cf", size = 22928 }, +] + [[package]] name = "greenlet" version = "3.1.1" @@ -353,6 +372,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, ] +[[package]] +name = "httpx-sse" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 }, +] + [[package]] name = "idna" version = "3.10" @@ -453,6 +481,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, ] +[[package]] +name = "mcp" +version = "1.9.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "python-multipart" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/df/8fefc0c6c7a5c66914763e3ff3893f9a03435628f6625d5e3b0dc45d73db/mcp-1.9.3.tar.gz", hash = "sha256:587ba38448e81885e5d1b84055cfcc0ca56d35cd0c58f50941cab01109405388", size = 333045 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/45/823ad05504bea55cb0feb7470387f151252127ad5c72f8882e8fe6cf5c0e/mcp-1.9.3-py3-none-any.whl", hash = "sha256:69b0136d1ac9927402ed4cf221d4b8ff875e7132b0b06edd446448766f34f9b9", size = 131063 }, +] + [[package]] name = "mdurl" version = "0.1.2" @@ -826,6 +874,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/aa/e4/592120713a314621c692211eba034d09becaf6bc8848fabc1dc2a54d8c16/SQLAlchemy-2.0.38-py3-none-any.whl", hash = "sha256:63178c675d4c80def39f1febd625a6333f44c0ba269edd8a468b156394b27753", size = 1896347 }, ] +[[package]] +name = "sse-starlette" +version = "2.3.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8c/f4/989bc70cb8091eda43a9034ef969b25145291f3601703b82766e5172dfed/sse_starlette-2.3.6.tar.gz", hash = "sha256:0382336f7d4ec30160cf9ca0518962905e1b69b72d6c1c995131e0a703b436e3", size = 18284 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/05/78850ac6e79af5b9508f8841b0f26aa9fd329a1ba00bf65453c2d312bcc8/sse_starlette-2.3.6-py3-none-any.whl", hash = "sha256:d49a8285b182f6e2228e2609c350398b2ca2c36216c2675d875f81e93548f760", size = 10606 }, +] + [[package]] name = "starlette" version = "0.46.1" @@ -838,6 +898,45 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/4b/528ccf7a982216885a1ff4908e886b8fb5f19862d1962f56a3fce2435a70/starlette-0.46.1-py3-none-any.whl", hash = "sha256:77c74ed9d2720138b25875133f3a2dae6d854af2ec37dceb56aef370c1d8a227", size = 71995 }, ] +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, +] + [[package]] name = "typer" version = "0.15.2"