├── .python-version ├── qgis-plugin-amap ├── README.md ├── images │ ├── icon.png │ ├── start.svg │ ├── stop.svg │ └── amap.svg ├── __init__.py ├── metadata.txt ├── utils.py ├── server.py ├── plugin.py ├── transform.py └── tile.py ├── README.md ├── pyproject.toml ├── LICENSE ├── .gitignore └── uv.lock /.python-version: -------------------------------------------------------------------------------- 1 | 3.13 2 | -------------------------------------------------------------------------------- /qgis-plugin-amap/README.md: -------------------------------------------------------------------------------- 1 | see: https://garden.liuxs.pro/amap-rectify -------------------------------------------------------------------------------- /qgis-plugin-amap/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liuxspro/amap-rectify/main/qgis-plugin-amap/images/icon.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # amap-rectify 2 | Rectify the map from GCJ-02 to WGS-84 coordinate system 3 | 4 | see: https://garden.liuxs.pro/amap-rectify -------------------------------------------------------------------------------- /qgis-plugin-amap/__init__.py: -------------------------------------------------------------------------------- 1 | from .plugin import AMAP 2 | 3 | 4 | def classFactory(iface): 5 | """QGIS Plugin""" 6 | return AMAP(iface) 7 | -------------------------------------------------------------------------------- /qgis-plugin-amap/images/start.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /qgis-plugin-amap/images/stop.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "amap-rectify" 3 | version = "0.1.0" 4 | description = "Rectify the map from GCJ-02 to WGS-84 coordinate system" 5 | readme = "README.md" 6 | requires-python = ">=3.13" 7 | dependencies = [ 8 | "fastapi>=0.115.13", 9 | "httpx>=0.28.1", 10 | "pillow>=11.2.1", 11 | "requests>=2.32.4", 12 | "uvicorn>=0.34.3", 13 | ] 14 | -------------------------------------------------------------------------------- /qgis-plugin-amap/metadata.txt: -------------------------------------------------------------------------------- 1 | [general] 2 | name=AMAP 3 | qgisMinimumVersion=3.14 4 | description=高德瓦片地图(无偏)加载 5 | about=高德瓦片地图(无偏)加载,启动本地服务后方可添加高德图层.仅供测试:) 6 | version=0.1.0-dev 7 | tags=basemap, xyz, 高德地图 8 | icon=images/icon.png 9 | experimental=False 10 | deprecated=False 11 | plugin_dependencies=pillow,httpx,fastapi,uvicorn 12 | author=liuxspro 13 | email=liuxspro@gmail.com 14 | homepage=https://github.com/liuxspro/amap-rectify 15 | tracker=https://github.com/liuxspro/amap-rectify/issues 16 | repository=https://github.com/liuxspro/amap-rectify 17 | changelog= -------------------------------------------------------------------------------- /qgis-plugin-amap/utils.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from qgis.core import QgsProject, QgsRasterLayer, QgsMessageLog, Qgis 4 | 5 | PluginDir = Path(__file__).parent 6 | CACHE_DIR = PluginDir.joinpath("./cache") 7 | 8 | 9 | def log_message(message): 10 | QgsMessageLog.logMessage(f"{message}", "AMAP", Qgis.Info) 11 | 12 | 13 | def add_raster_layer(uri: str, name: str, provider_type: str = "wms") -> None: 14 | """QGIS 添加栅格图层 15 | 16 | Args: 17 | uri (str): 栅格图层uri 18 | name (str): 栅格图层名称 19 | provider_type(str): 栅格图层类型(wms,arcgismapserver) 20 | Reference: https://qgis.org/pyqgis/3.32/core/QgsRasterLayer.html 21 | """ 22 | raster_layer = QgsRasterLayer(uri, name, provider_type) 23 | if raster_layer.isValid(): 24 | QgsProject.instance().addMapLayer(raster_layer) 25 | else: 26 | print(f"无效的图层 invalid Layer\n{uri}") 27 | -------------------------------------------------------------------------------- /qgis-plugin-amap/images/amap.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 liuxspro 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. 22 | -------------------------------------------------------------------------------- /qgis-plugin-amap/server.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import time 3 | from io import BytesIO 4 | from typing import Optional 5 | 6 | from fastapi import FastAPI, Response 7 | from uvicorn import Server, Config 8 | 9 | from .tile import get_tile_async 10 | from .utils import log_message 11 | 12 | app = FastAPI() 13 | 14 | 15 | @app.get("/tile/amap/{map_id}/{z}/{x}/{y}") 16 | async def tile(map_id, z: int, x: int, y: int): 17 | image = await get_tile_async(x, y, z, map_id) 18 | img_buffer = BytesIO() 19 | image.save(img_buffer, format="PNG") 20 | img_bytes = img_buffer.getvalue() 21 | img_buffer.close() 22 | return Response(content=img_bytes, media_type="image/png") 23 | 24 | 25 | class ServerManager: 26 | def __init__(self, _app: FastAPI, host: str = "0.0.0.0", port: int = 8080): 27 | self.app = _app 28 | self.host = host 29 | self.port = port 30 | self.server: Optional[Server] = None 31 | self.server_thread: Optional[threading.Thread] = None 32 | self._is_running = False 33 | 34 | def is_running(self) -> bool: 35 | """检查服务器是否正在运行""" 36 | return self._is_running and self.server_thread and self.server_thread.is_alive() 37 | 38 | def start(self) -> bool: 39 | # 如果服务器已在运行,先停止 40 | 41 | if self.is_running(): 42 | self.stop() 43 | 44 | try: 45 | # 创建服务器配置 46 | config = Config( 47 | app=self.app, 48 | host=self.host, 49 | port=self.port, 50 | lifespan="on", 51 | # log_level="info", 52 | log_config=None 53 | ) 54 | self.server = Server(config) 55 | 56 | # 创建并启动服务器线程 57 | self.server_thread = threading.Thread(target=self._run_server) 58 | self.server_thread.daemon = True 59 | self.server_thread.start() 60 | 61 | # 等待服务器启动(最多5秒) 62 | start_time = time.time() 63 | while not self._is_running and (time.time() - start_time) < 5: 64 | time.sleep(0.1) 65 | 66 | if self._is_running: 67 | return True 68 | else: 69 | return False 70 | except Exception as e: 71 | log_message(f"Error: {e}") 72 | return False 73 | 74 | def _run_server(self): 75 | """内部方法:运行服务器""" 76 | try: 77 | self._is_running = True 78 | self.server.run() 79 | except Exception as e: 80 | print(f"🚨 Server crashed: {str(e)}") 81 | finally: 82 | self._is_running = False 83 | 84 | def stop(self, timeout: float = 5.0) -> bool | None: 85 | if not self.is_running(): 86 | return False 87 | 88 | try: 89 | # 通知服务器退出 90 | if self.server: 91 | self.server.should_exit = True 92 | 93 | # 等待线程结束 94 | if self.server_thread: 95 | self.server_thread.join(timeout=timeout) 96 | 97 | # 如果线程仍然存活,强制终止 98 | if self.server_thread and self.server_thread.is_alive(): 99 | if self.server: 100 | self.server.force_exit = True 101 | self.server_thread.join(timeout=1.0) 102 | # 最后尝试强制终止 103 | if self.server_thread.is_alive(): 104 | return False 105 | return True 106 | except Exception as e: 107 | print(f"Error: {e}") 108 | return False 109 | finally: 110 | # 清理资源 111 | self.server = None 112 | self.server_thread = None 113 | self._is_running = False 114 | -------------------------------------------------------------------------------- /qgis-plugin-amap/plugin.py: -------------------------------------------------------------------------------- 1 | from urllib.parse import quote 2 | 3 | from qgis.PyQt.QtGui import QIcon 4 | from qgis.PyQt.QtWidgets import QAction 5 | 6 | from .server import ServerManager, app 7 | from .tile import amap_name 8 | from .utils import PluginDir, add_raster_layer, log_message 9 | 10 | 11 | def add_map(mapid): 12 | map_url = f"http://localhost:8080/tile/amap/{mapid}/{{z}}/{{x}}/{{y}}" 13 | map_name = amap_name[mapid] 14 | # URL编码处理 15 | encoded_url = quote(map_url, safe=":/?=") 16 | uri = f"type=xyz&url={encoded_url}&zmax=18&zmin=2" 17 | add_raster_layer(uri, f"高德地图 - {map_name}") 18 | 19 | 20 | class AMAP: 21 | def __init__(self, iface): 22 | self.iface = iface 23 | self.server = ServerManager(app, port=8080) 24 | # 添加动作列表 25 | self.actions = [] 26 | self.start_action = None 27 | self.add_map_cations = [] 28 | self.stop_action = None 29 | # 添加菜单 30 | self.menu = None 31 | 32 | def initGui(self): 33 | """创建菜单项和工具栏图标""" 34 | log_message("初始化AMAP插件...") 35 | 36 | # 创建菜单 37 | self.menu = self.iface.mainWindow().menuBar().addMenu("&AMAP") 38 | 39 | # 启动服务 Action 40 | icon = QIcon(str(PluginDir / "images" / "start.svg")) 41 | self.start_action = QAction(icon, "启动服务器", self.iface.mainWindow()) 42 | self.start_action.triggered.connect(self.start_server) 43 | 44 | self.actions.append(self.start_action) 45 | 46 | # 停止服务 Action 47 | self.stop_action = QAction( 48 | QIcon(str(PluginDir / "images" / "stop.svg")), 49 | "停止服务器", 50 | self.iface.mainWindow(), 51 | ) 52 | self.stop_action.triggered.connect(self.stop_server) 53 | self.actions.append(self.stop_action) 54 | 55 | # 添加 Action 到菜单 56 | self.menu.addAction(self.start_action) 57 | self.menu.addAction(self.stop_action) 58 | self.menu.addSeparator() 59 | 60 | # 添加地图 Action 61 | amap_icon = QIcon(str(PluginDir / "images" / "amap.svg")) 62 | for mapid in amap_name: 63 | action = QAction( 64 | amap_icon, f"高德地图 - {amap_name[mapid]}", self.iface.mainWindow() 65 | ) 66 | action.triggered.connect(lambda checked, mid=mapid: add_map(mid)) 67 | self.add_map_cations.append(action) 68 | 69 | for action in self.add_map_cations: 70 | self.actions.append(action) 71 | self.menu.addAction(action) 72 | 73 | # 添加动作到工具栏 74 | # self.iface.addToolBarIcon(self.start_action) 75 | # self.iface.addToolBarIcon(self.stop_action) 76 | 77 | # 初始状态:启动按钮可用,停止按钮不可用 78 | self.stop_action.setEnabled(False) 79 | for action in self.add_map_cations: 80 | action.setEnabled(False) 81 | 82 | log_message("AMAP插件初始化完成") 83 | 84 | def start_server(self): 85 | if self.server.start(): 86 | log_message("✅ 服务器启动成功!") 87 | log_message( 88 | f"🌐 可以访问以下地址:http://localhost:{self.server.port}/docs", 89 | ) 90 | # 更新UI状态 91 | self.start_action.setEnabled(False) 92 | self.stop_action.setEnabled(True) 93 | for action in self.add_map_cations: 94 | action.setEnabled(True) 95 | else: 96 | log_message("❌ 服务器启动失败") 97 | 98 | def stop_server(self): 99 | if self.server.stop(): 100 | # 更新UI状态 101 | self.start_action.setEnabled(True) 102 | self.stop_action.setEnabled(False) 103 | for action in self.add_map_cations: 104 | action.setEnabled(False) 105 | log_message("✅ 服务器已停止") 106 | 107 | def unload(self): 108 | """从QGIS界面卸载插件""" 109 | log_message("卸载AMAP插件...") 110 | 111 | # 停止服务器 112 | self.stop_server() 113 | 114 | # 移除菜单 115 | if self.menu: 116 | self.iface.mainWindow().menuBar().removeAction(self.menu.menuAction()) 117 | 118 | # 移除工具栏图标 119 | for action in self.actions: 120 | self.iface.removeToolBarIcon(action) 121 | 122 | print("AMAP插件已卸载") 123 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # UV 98 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | #uv.lock 102 | 103 | # poetry 104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 105 | # This is especially recommended for binary packages to ensure reproducibility, and is more 106 | # commonly ignored for libraries. 107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 108 | #poetry.lock 109 | 110 | # pdm 111 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 112 | #pdm.lock 113 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 114 | # in version control. 115 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 116 | .pdm.toml 117 | .pdm-python 118 | .pdm-build/ 119 | 120 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 121 | __pypackages__/ 122 | 123 | # Celery stuff 124 | celerybeat-schedule 125 | celerybeat.pid 126 | 127 | # SageMath parsed files 128 | *.sage.py 129 | 130 | # Environments 131 | .env 132 | .venv 133 | env/ 134 | venv/ 135 | ENV/ 136 | env.bak/ 137 | venv.bak/ 138 | 139 | # Spyder project settings 140 | .spyderproject 141 | .spyproject 142 | 143 | # Rope project settings 144 | .ropeproject 145 | 146 | # mkdocs documentation 147 | /site 148 | 149 | # mypy 150 | .mypy_cache/ 151 | .dmypy.json 152 | dmypy.json 153 | 154 | # Pyre type checker 155 | .pyre/ 156 | 157 | # pytype static type analyzer 158 | .pytype/ 159 | 160 | # Cython debug symbols 161 | cython_debug/ 162 | 163 | # PyCharm 164 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 165 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 166 | # and can be added to the global gitignore or merged into this file. For a more nuclear 167 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 168 | #.idea/ 169 | 170 | # Abstra 171 | # Abstra is an AI-powered process automation framework. 172 | # Ignore directories containing user credentials, local state, and settings. 173 | # Learn more at https://abstra.io/docs 174 | .abstra/ 175 | 176 | # Visual Studio Code 177 | # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore 178 | # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore 179 | # and can be added to the global gitignore or merged into this file. However, if you prefer, 180 | # you could uncomment the following to ignore the enitre vscode folder 181 | # .vscode/ 182 | 183 | # Ruff stuff: 184 | .ruff_cache/ 185 | 186 | # PyPI configuration file 187 | .pypirc 188 | 189 | # Cursor 190 | # Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to 191 | # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data 192 | # refer to https://docs.cursor.com/context/ignore-files 193 | .cursorignore 194 | .cursorindexingignore 195 | cache/ 196 | .idea/ -------------------------------------------------------------------------------- /qgis-plugin-amap/transform.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ########################################################################################## 3 | """ 4 | /*************************************************************************** 5 | OffsetWGS84Core 6 | A QGIS plugin 7 | Class with methods for geometry and attributes processing 8 | ------------------- 9 | begin : 2016-10-11 10 | git sha : $Format:%H$ 11 | copyright : (C) 2017 by sshuair 12 | email : sshuair@gmail.com 13 | ***************************************************************************/ 14 | 15 | /*************************************************************************** 16 | * * 17 | * This program is free software; you can redistribute it and/or modify * 18 | * it under the terms of the GNU General Public License as published by * 19 | * the Free Software Foundation; either version 2 of the License, or * 20 | * (at your option) any later version. * 21 | * * 22 | ***************************************************************************/ 23 | """ 24 | from __future__ import print_function 25 | 26 | import math 27 | 28 | ########################################################################################## 29 | from builtins import zip 30 | from math import atan2, cos, fabs 31 | from math import pi as PI 32 | from math import sin, sqrt 33 | 34 | # from numba import jit 35 | 36 | 37 | # =================================================sshuair============================================================= 38 | # define ellipsoid 39 | a = 6378245.0 40 | f = 1 / 298.3 41 | b = a * (1 - f) 42 | ee = 1 - (b * b) / (a * a) 43 | 44 | 45 | # check if the point in china 46 | def outOfChina(lng, lat): 47 | return not (72.004 <= lng <= 137.8347 and 0.8293 <= lat <= 55.8271) 48 | 49 | 50 | # @jit 51 | def geohey_transformLat(x, y): 52 | ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * sqrt(fabs(x)) 53 | ret = ret + (20.0 * sin(6.0 * x * PI) + 20.0 * sin(2.0 * x * PI)) * 2.0 / 3.0 54 | ret = ret + (20.0 * sin(y * PI) + 40.0 * sin(y / 3.0 * PI)) * 2.0 / 3.0 55 | ret = ret + (160.0 * sin(y / 12.0 * PI) + 320.0 * sin(y * PI / 30.0)) * 2.0 / 3.0 56 | return ret 57 | 58 | 59 | # @jit 60 | def geohey_transformLon(x, y): 61 | ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * sqrt(fabs(x)) 62 | ret = ret + (20.0 * sin(6.0 * x * PI) + 20.0 * sin(2.0 * x * PI)) * 2.0 / 3.0 63 | ret = ret + (20.0 * sin(x * PI) + 40.0 * sin(x / 3.0 * PI)) * 2.0 / 3.0 64 | ret = ret + (150.0 * sin(x / 12.0 * PI) + 300.0 * sin(x * PI / 30.0)) * 2.0 / 3.0 65 | return ret 66 | 67 | 68 | # @jit 69 | def wgs2gcj(wgsLon, wgsLat): 70 | if outOfChina(wgsLon, wgsLat): 71 | return wgsLon, wgsLat 72 | dLat = geohey_transformLat(wgsLon - 105.0, wgsLat - 35.0) 73 | dLon = geohey_transformLon(wgsLon - 105.0, wgsLat - 35.0) 74 | radLat = wgsLat / 180.0 * PI 75 | magic = math.sin(radLat) 76 | magic = 1 - ee * magic * magic 77 | sqrtMagic = sqrt(magic) 78 | dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * PI) 79 | dLon = (dLon * 180.0) / (a / sqrtMagic * cos(radLat) * PI) 80 | gcjLat = wgsLat + dLat 81 | gcjLon = wgsLon + dLon 82 | return (gcjLon, gcjLat) 83 | 84 | 85 | def gcj2wgs(gcjLon, gcjLat): 86 | g0 = (gcjLon, gcjLat) 87 | w0 = g0 88 | g1 = wgs2gcj(w0[0], w0[1]) 89 | # w1 = w0 - (g1 - g0) 90 | w1 = tuple([x[0] - (x[1] - x[2]) for x in zip(w0, g1, g0)]) 91 | # delta = w1 - w0 92 | delta = tuple([x[0] - x[1] for x in zip(w1, w0)]) 93 | while abs(delta[0]) >= 1e-6 or abs(delta[1]) >= 1e-6: 94 | w0 = w1 95 | g1 = wgs2gcj(w0[0], w0[1]) 96 | # w1 = w0 - (g1 - g0) 97 | w1 = tuple([x[0] - (x[1] - x[2]) for x in zip(w0, g1, g0)]) 98 | # delta = w1 - w0 99 | delta = tuple([x[0] - x[1] for x in zip(w1, w0)]) 100 | return w1 101 | 102 | 103 | def gcj2bd(gcjLon, gcjLat): 104 | z = sqrt(gcjLon * gcjLon + gcjLat * gcjLat) + 0.00002 * sin( 105 | gcjLat * PI * 3000.0 / 180.0 106 | ) 107 | theta = atan2(gcjLat, gcjLon) + 0.000003 * cos(gcjLon * PI * 3000.0 / 180.0) 108 | bdLon = z * cos(theta) + 0.0065 109 | bdLat = z * sin(theta) + 0.006 110 | return (bdLon, bdLat) 111 | 112 | 113 | def bd2gcj(bdLon, bdLat): 114 | x = bdLon - 0.0065 115 | y = bdLat - 0.006 116 | z = sqrt(x * x + y * y) - 0.00002 * sin(y * PI * 3000.0 / 180.0) 117 | theta = atan2(y, x) - 0.000003 * cos(x * PI * 3000.0 / 180.0) 118 | gcjLon = z * cos(theta) 119 | gcjLat = z * sin(theta) 120 | return (gcjLon, gcjLat) 121 | 122 | 123 | def wgs2bd(wgsLon, wgsLat): 124 | gcj = wgs2gcj(wgsLon, wgsLat) 125 | return gcj2bd(gcj[0], gcj[1]) 126 | 127 | 128 | def bd2wgs(bdLon, bdLat): 129 | gcj = bd2gcj(bdLon, bdLat) 130 | return gcj2wgs(gcj[0], gcj[1]) 131 | 132 | 133 | if __name__ == "__main__": 134 | # wgs2gcj 135 | # coord = (112, 40) 136 | # trans = WGS2GCJ() 137 | print(wgs2gcj(117.136230, 34.252676)) 138 | print(gcj2wgs(112.00678230985764, 40.00112245823686)) 139 | 140 | # gcj2wgs 141 | -------------------------------------------------------------------------------- /qgis-plugin-amap/tile.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from io import BytesIO 3 | from math import atan, cos, log, pi, sinh, tan 4 | from typing import Optional 5 | 6 | import httpx 7 | import requests 8 | from PIL import Image 9 | 10 | # 来自 Geohey 的转换算法 https://github.com/GeoHey-Team/qgis-geohey-toolbox 11 | from .transform import wgs2gcj 12 | from .utils import CACHE_DIR 13 | 14 | AMAP_HOST = "https://wprd02.is.autonavi.com" 15 | 16 | amap_url = { 17 | "vec": f"{AMAP_HOST}/appmaptile?lang=zh_cn&size=1&scale=1&style=7&x={{x}}&y={{y}}&z={{z}}", 18 | "vec_note": f"{AMAP_HOST}/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={{x}}&y={{y}}&z={{z}}", 19 | } 20 | 21 | amap_name = {"vec": "矢量地图", "vec_note": "矢量注记"} 22 | 23 | # 全局异步HTTP客户端 24 | _async_client: Optional[httpx.AsyncClient] = None 25 | 26 | 27 | def get_async_client() -> httpx.AsyncClient: 28 | """获取或创建异步HTTP客户端""" 29 | global _async_client 30 | if _async_client is None: 31 | _async_client = httpx.AsyncClient(timeout=30.0) 32 | return _async_client 33 | 34 | 35 | async def close_async_client(): 36 | """关闭异步HTTP客户端""" 37 | global _async_client 38 | if _async_client is not None: 39 | await _async_client.aclose() 40 | _async_client = None 41 | 42 | 43 | def xyz_to_lonlat(x: int, y: int, z: int) -> tuple: 44 | """ 45 | 将XYZ瓦片坐标转换为经纬度(左上角点)。 46 | 47 | Args: 48 | x (int): Tile X coordinate. 49 | y (int): Tile Y coordinate. 50 | z (int): Zoom level. 51 | 52 | Returns: 53 | tuple: Longitude and latitude in degrees. 54 | """ 55 | n = 2.0**z 56 | lon_deg = x / n * 360.0 - 180.0 57 | lat_rad = atan(sinh(pi * (1 - 2 * y / n))) 58 | lat_deg = lat_rad * 180.0 / pi 59 | return lon_deg, lat_deg 60 | 61 | 62 | def lonlat_to_xyz(lon: float, lat: float, z: int) -> tuple: 63 | """ 64 | Convert longitude and latitude to XYZ tile coordinates. 65 | 66 | Args: 67 | lon (float): Longitude in degrees. 68 | lat (float): Latitude in degrees. 69 | z (int): Zoom level. 70 | 71 | Returns: 72 | tuple: Tile X and Y coordinates. 73 | """ 74 | n = 2.0**z 75 | x = (lon + 180.0) / 360.0 * n 76 | lat_rad = lat * pi / 180.0 77 | t = log(tan(lat_rad) + 1 / cos(lat_rad)) 78 | y = (1 - t / pi) * n / 2 79 | return int(x), int(y) 80 | 81 | 82 | def xyz_to_bbox(x, y, z): 83 | """ 84 | Convert XYZ tile coordinates to bounding box coordinates. 85 | 86 | Args: 87 | x (int): Tile X coordinate. 88 | y (int): Tile Y coordinate. 89 | z (int): Zoom level. 90 | 91 | Returns: 92 | tuple: Bounding box in the format (min_lon, min_lat, max_lon, max_lat). 93 | """ 94 | left_upper_lon, left_upper_lat = xyz_to_lonlat(x, y, z) 95 | right_lower_lon, right_lower_lat = xyz_to_lonlat(x + 1, y + 1, z) 96 | 97 | return (left_upper_lon, left_upper_lat), (right_lower_lon, right_lower_lat) 98 | 99 | 100 | def wgsbbox_to_gcjbbox(wgs_bbox): 101 | """ 102 | Convert WGS84 bounding box to GCJ02 bounding box. 103 | 104 | Args: 105 | wgs_bbox (tuple): Bounding box in the format (min_lon, min_lat, max_lon, max_lat). 106 | 107 | Returns: 108 | tuple: GCJ02 bounding box in the same format. 109 | """ 110 | left_upper, right_lower = wgs_bbox 111 | gcj_left_upper = wgs2gcj(left_upper[0], left_upper[1]) 112 | gcj_right_lower = wgs2gcj(right_lower[0], right_lower[1]) 113 | return gcj_left_upper, gcj_right_lower 114 | 115 | 116 | async def get_tile_gcj_async(x: int, y: int, z: int, mapid: str) -> Image: 117 | """ 118 | 异步获取指定瓦片的图像,如果缓存中存在则直接返回,否则从服务器下载。 119 | 这里下载的是高德地图的GCJ02坐标系瓦片。 120 | 该函数会检查缓存目录,如果瓦片已经存在,则直接从缓存中读取并返回。 121 | 122 | Args: 123 | x (int): Tile X coordinate. 124 | y (int): Tile Y coordinate. 125 | z (int): Zoom level. 126 | mapid (str): Map Id 127 | 128 | Returns: 129 | Image: Tile image. 130 | """ 131 | url = amap_url[mapid] 132 | url = url.format(x=x, y=y, z=z) 133 | map_name = amap_name[mapid] 134 | 135 | # 如果缓存中存在,直接返回缓存的瓦片 136 | tile_file_path = CACHE_DIR.joinpath(f"./{map_name}/gcj/{z}/{x}/{y}.png") 137 | if tile_file_path.exists(): 138 | return Image.open(tile_file_path) 139 | 140 | # 使用异步HTTP客户端获取瓦片 141 | client = get_async_client() 142 | async with client.stream("GET", url) as response: 143 | if response.status_code != 200: 144 | raise Exception(f"Failed to fetch tile from {url}") 145 | 146 | # 读取响应内容 147 | content = await response.aread() 148 | 149 | # 保存瓦片图像到缓存 150 | file_path = CACHE_DIR.joinpath(f"./{map_name}/gcj/{z}/{x}/{y}.png") 151 | file_path.parent.mkdir(parents=True, exist_ok=True) 152 | file_path.write_bytes(content) 153 | 154 | return Image.open(BytesIO(content)) 155 | 156 | 157 | def get_tile_gcj(x: int, y: int, z: int, mapid: str) -> Image: 158 | """ 159 | 获取指定瓦片的图像,如果缓存中存在则直接返回,否则从服务器下载。 160 | 这里下载的是高德地图的GCJ02坐标系瓦片。 161 | 该函数会检查缓存目录,如果瓦片已经存在,则直接从缓存中读取并返回。 162 | 163 | Args: 164 | x (int): Tile X coordinate. 165 | y (int): Tile Y coordinate. 166 | z (int): Zoom level. 167 | mapid (str): Map Id 168 | 169 | Returns: 170 | Image: Tile image. 171 | """ 172 | url = amap_url[mapid] 173 | url = url.format(x=x, y=y, z=z) 174 | map_name = amap_name[mapid] 175 | # if in cache, return the cached tile 176 | tile_file_path = CACHE_DIR.joinpath(f"./{map_name}/gcj/{z}/{x}/{y}.png") 177 | if tile_file_path.exists(): 178 | return Image.open(tile_file_path) 179 | 180 | r = requests.get(url) 181 | if r.status_code != 200: 182 | raise Exception(f"Failed to fetch tile from {url}") 183 | # save the tile image to cache 184 | file_path = CACHE_DIR.joinpath(f"./{map_name}/gcj/{z}/{x}/{y}.png") 185 | file_path.parent.mkdir(parents=True, exist_ok=True) 186 | file_path.write_bytes(r.content) 187 | return Image.open(BytesIO(r.content)) 188 | 189 | 190 | async def get_tile_async(x: int, y: int, z: int, mapid: str) -> Image: 191 | """ 192 | 异步获取瓦片,支持高精度坐标转换 193 | """ 194 | map_name = amap_name[mapid] 195 | # 小于 9 级时 没有明显的偏移 直接使用 GCJ02 坐标系的瓦片 196 | if z <= 9: 197 | return await get_tile_gcj_async(x, y, z, mapid) 198 | 199 | # 如果缓存中存在,直接返回缓存的瓦片 200 | tile_file_path = CACHE_DIR.joinpath(f"./{map_name}/wgs/{z}/{x}/{y}.png") 201 | if tile_file_path.exists(): 202 | return Image.open(tile_file_path) 203 | 204 | wgs_bbox = xyz_to_bbox(x, y, z) 205 | gcj_bbox = wgsbbox_to_gcjbbox(wgs_bbox) 206 | left_upper, right_lower = gcj_bbox 207 | 208 | # 计算左上角和右下角的瓦片行列号 209 | x_min, y_min = lonlat_to_xyz(left_upper[0], left_upper[1], z) # 左上角 210 | x_max, y_max = lonlat_to_xyz(right_lower[0], right_lower[1], z) # 右下角 211 | 212 | # 创建任务列表,异步获取所有需要的瓦片 213 | tasks = [] 214 | for ax in range(x_min, x_max + 1): 215 | for ay in range(y_min, y_max + 1): 216 | tasks.append(get_tile_gcj_async(ax, ay, z, mapid)) 217 | 218 | # 并发执行所有瓦片下载任务 219 | tiles = await asyncio.gather(*tasks) 220 | 221 | # 拼合瓦片 222 | composite = Image.new( 223 | "RGBA", ((x_max - x_min + 1) * 256, (y_max - y_min + 1) * 256) 224 | ) 225 | 226 | tile_index = 0 227 | for i, ax in enumerate(range(x_min, x_max + 1)): 228 | for j, ay in enumerate(range(y_min, y_max + 1)): 229 | tile = tiles[tile_index] 230 | if tile: 231 | composite.paste(tile, (i * 256, j * 256)) 232 | tile_index += 1 233 | 234 | # 计算拼合后的瓦片范围 235 | megred_bbox = xyz_to_bbox(x_min, y_min, z)[0], xyz_to_bbox(x_max, y_max, z)[1] 236 | 237 | x_range = megred_bbox[1][0] - megred_bbox[0][0] 238 | y_range = megred_bbox[0][1] - megred_bbox[1][1] 239 | 240 | left_percent = (gcj_bbox[0][0] - megred_bbox[0][0]) / x_range 241 | top_percent = (megred_bbox[0][1] - gcj_bbox[0][1]) / y_range 242 | img_width, img_height = composite.size 243 | # 裁剪选区(left, top, right, bottom) 244 | crop_bbox = ( 245 | int(left_percent * img_width), 246 | int(top_percent * img_height), 247 | int(left_percent * img_width) + 256, 248 | int(top_percent * img_height) + 256, 249 | ) 250 | 251 | # 从拼合的瓦片中裁剪出对应的区域 252 | croped_image = composite.crop(crop_bbox) 253 | wgs_tile_path = CACHE_DIR.joinpath(f"./{map_name}/wgs/{z}/{x}/{y}.png") 254 | wgs_tile_path.parent.mkdir(parents=True, exist_ok=True) 255 | croped_image.save(wgs_tile_path) 256 | return croped_image 257 | -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | revision = 1 3 | requires-python = ">=3.13" 4 | 5 | [[package]] 6 | name = "amap-rectify" 7 | version = "0.1.0" 8 | source = { virtual = "." } 9 | dependencies = [ 10 | { name = "fastapi" }, 11 | { name = "httpx" }, 12 | { name = "pillow" }, 13 | { name = "requests" }, 14 | { name = "uvicorn" }, 15 | ] 16 | 17 | [package.metadata] 18 | requires-dist = [ 19 | { name = "fastapi", specifier = ">=0.115.13" }, 20 | { name = "httpx", specifier = ">=0.28.1" }, 21 | { name = "pillow", specifier = ">=11.2.1" }, 22 | { name = "requests", specifier = ">=2.32.4" }, 23 | { name = "uvicorn", specifier = ">=0.34.3" }, 24 | ] 25 | 26 | [[package]] 27 | name = "annotated-types" 28 | version = "0.7.0" 29 | source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } 30 | sdist = { url = "https://mirrors.aliyun.com/pypi/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89" } 31 | wheels = [ 32 | { url = "https://mirrors.aliyun.com/pypi/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53" }, 33 | ] 34 | 35 | [[package]] 36 | name = "anyio" 37 | version = "4.9.0" 38 | source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } 39 | dependencies = [ 40 | { name = "idna" }, 41 | { name = "sniffio" }, 42 | ] 43 | sdist = { url = "https://mirrors.aliyun.com/pypi/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028" } 44 | wheels = [ 45 | { url = "https://mirrors.aliyun.com/pypi/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c" }, 46 | ] 47 | 48 | [[package]] 49 | name = "certifi" 50 | version = "2025.6.15" 51 | source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } 52 | sdist = { url = "https://mirrors.aliyun.com/pypi/packages/73/f7/f14b46d4bcd21092d7d3ccef689615220d8a08fb25e564b65d20738e672e/certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b" } 53 | wheels = [ 54 | { url = "https://mirrors.aliyun.com/pypi/packages/84/ae/320161bd181fc06471eed047ecce67b693fd7515b16d495d8932db763426/certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057" }, 55 | ] 56 | 57 | [[package]] 58 | name = "charset-normalizer" 59 | version = "3.4.2" 60 | source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } 61 | sdist = { url = "https://mirrors.aliyun.com/pypi/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63" } 62 | wheels = [ 63 | { url = "https://mirrors.aliyun.com/pypi/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0" }, 64 | { url = "https://mirrors.aliyun.com/pypi/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf" }, 65 | { url = "https://mirrors.aliyun.com/pypi/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e" }, 66 | { url = "https://mirrors.aliyun.com/pypi/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1" }, 67 | { url = "https://mirrors.aliyun.com/pypi/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c" }, 68 | { url = "https://mirrors.aliyun.com/pypi/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691" }, 69 | { url = "https://mirrors.aliyun.com/pypi/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0" }, 70 | { url = "https://mirrors.aliyun.com/pypi/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b" }, 71 | { url = "https://mirrors.aliyun.com/pypi/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff" }, 72 | { url = "https://mirrors.aliyun.com/pypi/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b" }, 73 | { url = "https://mirrors.aliyun.com/pypi/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148" }, 74 | { url = "https://mirrors.aliyun.com/pypi/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7" }, 75 | { url = "https://mirrors.aliyun.com/pypi/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980" }, 76 | { url = "https://mirrors.aliyun.com/pypi/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0" }, 77 | ] 78 | 79 | [[package]] 80 | name = "click" 81 | version = "8.2.1" 82 | source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } 83 | dependencies = [ 84 | { name = "colorama", marker = "sys_platform == 'win32'" }, 85 | ] 86 | sdist = { url = "https://mirrors.aliyun.com/pypi/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202" } 87 | wheels = [ 88 | { url = "https://mirrors.aliyun.com/pypi/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b" }, 89 | ] 90 | 91 | [[package]] 92 | name = "colorama" 93 | version = "0.4.6" 94 | source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } 95 | sdist = { url = "https://mirrors.aliyun.com/pypi/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44" } 96 | wheels = [ 97 | { url = "https://mirrors.aliyun.com/pypi/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" }, 98 | ] 99 | 100 | [[package]] 101 | name = "fastapi" 102 | version = "0.115.13" 103 | source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } 104 | dependencies = [ 105 | { name = "pydantic" }, 106 | { name = "starlette" }, 107 | { name = "typing-extensions" }, 108 | ] 109 | sdist = { url = "https://mirrors.aliyun.com/pypi/packages/20/64/ec0788201b5554e2a87c49af26b77a4d132f807a0fa9675257ac92c6aa0e/fastapi-0.115.13.tar.gz", hash = "sha256:55d1d25c2e1e0a0a50aceb1c8705cd932def273c102bff0b1c1da88b3c6eb307" } 110 | wheels = [ 111 | { url = "https://mirrors.aliyun.com/pypi/packages/59/4a/e17764385382062b0edbb35a26b7cf76d71e27e456546277a42ba6545c6e/fastapi-0.115.13-py3-none-any.whl", hash = "sha256:0a0cab59afa7bab22f5eb347f8c9864b681558c278395e94035a741fc10cd865" }, 112 | ] 113 | 114 | [[package]] 115 | name = "h11" 116 | version = "0.16.0" 117 | source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } 118 | sdist = { url = "https://mirrors.aliyun.com/pypi/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1" } 119 | wheels = [ 120 | { url = "https://mirrors.aliyun.com/pypi/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86" }, 121 | ] 122 | 123 | [[package]] 124 | name = "httpcore" 125 | version = "1.0.9" 126 | source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } 127 | dependencies = [ 128 | { name = "certifi" }, 129 | { name = "h11" }, 130 | ] 131 | sdist = { url = "https://mirrors.aliyun.com/pypi/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8" } 132 | wheels = [ 133 | { url = "https://mirrors.aliyun.com/pypi/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55" }, 134 | ] 135 | 136 | [[package]] 137 | name = "httpx" 138 | version = "0.28.1" 139 | source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } 140 | dependencies = [ 141 | { name = "anyio" }, 142 | { name = "certifi" }, 143 | { name = "httpcore" }, 144 | { name = "idna" }, 145 | ] 146 | sdist = { url = "https://mirrors.aliyun.com/pypi/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc" } 147 | wheels = [ 148 | { url = "https://mirrors.aliyun.com/pypi/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad" }, 149 | ] 150 | 151 | [[package]] 152 | name = "idna" 153 | version = "3.10" 154 | source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } 155 | sdist = { url = "https://mirrors.aliyun.com/pypi/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9" } 156 | wheels = [ 157 | { url = "https://mirrors.aliyun.com/pypi/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3" }, 158 | ] 159 | 160 | [[package]] 161 | name = "pillow" 162 | version = "11.2.1" 163 | source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } 164 | sdist = { url = "https://mirrors.aliyun.com/pypi/packages/af/cb/bb5c01fcd2a69335b86c22142b2bccfc3464087efb7fd382eee5ffc7fdf7/pillow-11.2.1.tar.gz", hash = "sha256:a64dd61998416367b7ef979b73d3a85853ba9bec4c2925f74e588879a58716b6" } 165 | wheels = [ 166 | { url = "https://mirrors.aliyun.com/pypi/packages/36/9c/447528ee3776e7ab8897fe33697a7ff3f0475bb490c5ac1456a03dc57956/pillow-11.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fdec757fea0b793056419bca3e9932eb2b0ceec90ef4813ea4c1e072c389eb28" }, 167 | { url = "https://mirrors.aliyun.com/pypi/packages/b5/09/29d5cd052f7566a63e5b506fac9c60526e9ecc553825551333e1e18a4858/pillow-11.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0e130705d568e2f43a17bcbe74d90958e8a16263868a12c3e0d9c8162690830" }, 168 | { url = "https://mirrors.aliyun.com/pypi/packages/71/5d/446ee132ad35e7600652133f9c2840b4799bbd8e4adba881284860da0a36/pillow-11.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bdb5e09068332578214cadd9c05e3d64d99e0e87591be22a324bdbc18925be0" }, 169 | { url = "https://mirrors.aliyun.com/pypi/packages/69/5f/cbe509c0ddf91cc3a03bbacf40e5c2339c4912d16458fcb797bb47bcb269/pillow-11.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d189ba1bebfbc0c0e529159631ec72bb9e9bc041f01ec6d3233d6d82eb823bc1" }, 170 | { url = "https://mirrors.aliyun.com/pypi/packages/f9/b3/dd4338d8fb8a5f312021f2977fb8198a1184893f9b00b02b75d565c33b51/pillow-11.2.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:191955c55d8a712fab8934a42bfefbf99dd0b5875078240943f913bb66d46d9f" }, 171 | { url = "https://mirrors.aliyun.com/pypi/packages/13/eb/2552ecebc0b887f539111c2cd241f538b8ff5891b8903dfe672e997529be/pillow-11.2.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:ad275964d52e2243430472fc5d2c2334b4fc3ff9c16cb0a19254e25efa03a155" }, 172 | { url = "https://mirrors.aliyun.com/pypi/packages/72/d1/924ce51bea494cb6e7959522d69d7b1c7e74f6821d84c63c3dc430cbbf3b/pillow-11.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:750f96efe0597382660d8b53e90dd1dd44568a8edb51cb7f9d5d918b80d4de14" }, 173 | { url = "https://mirrors.aliyun.com/pypi/packages/43/ab/8f81312d255d713b99ca37479a4cb4b0f48195e530cdc1611990eb8fd04b/pillow-11.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fe15238d3798788d00716637b3d4e7bb6bde18b26e5d08335a96e88564a36b6b" }, 174 | { url = "https://mirrors.aliyun.com/pypi/packages/94/86/8f2e9d2dc3d308dfd137a07fe1cc478df0a23d42a6c4093b087e738e4827/pillow-11.2.1-cp313-cp313-win32.whl", hash = "sha256:3fe735ced9a607fee4f481423a9c36701a39719252a9bb251679635f99d0f7d2" }, 175 | { url = "https://mirrors.aliyun.com/pypi/packages/6d/ec/1179083b8d6067a613e4d595359b5fdea65d0a3b7ad623fee906e1b3c4d2/pillow-11.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:74ee3d7ecb3f3c05459ba95eed5efa28d6092d751ce9bf20e3e253a4e497e691" }, 176 | { url = "https://mirrors.aliyun.com/pypi/packages/23/f1/2fc1e1e294de897df39fa8622d829b8828ddad938b0eaea256d65b84dd72/pillow-11.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:5119225c622403afb4b44bad4c1ca6c1f98eed79db8d3bc6e4e160fc6339d66c" }, 177 | { url = "https://mirrors.aliyun.com/pypi/packages/c4/3e/c328c48b3f0ead7bab765a84b4977acb29f101d10e4ef57a5e3400447c03/pillow-11.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8ce2e8411c7aaef53e6bb29fe98f28cd4fbd9a1d9be2eeea434331aac0536b22" }, 178 | { url = "https://mirrors.aliyun.com/pypi/packages/18/0e/1c68532d833fc8b9f404d3a642991441d9058eccd5606eab31617f29b6d4/pillow-11.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9ee66787e095127116d91dea2143db65c7bb1e232f617aa5957c0d9d2a3f23a7" }, 179 | { url = "https://mirrors.aliyun.com/pypi/packages/b7/cb/6faf3fb1e7705fd2db74e070f3bf6f88693601b0ed8e81049a8266de4754/pillow-11.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9622e3b6c1d8b551b6e6f21873bdcc55762b4b2126633014cea1803368a9aa16" }, 180 | { url = "https://mirrors.aliyun.com/pypi/packages/07/94/8be03d50b70ca47fb434a358919d6a8d6580f282bbb7af7e4aa40103461d/pillow-11.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63b5dff3a68f371ea06025a1a6966c9a1e1ee452fc8020c2cd0ea41b83e9037b" }, 181 | { url = "https://mirrors.aliyun.com/pypi/packages/fd/a4/bfe78777076dc405e3bd2080bc32da5ab3945b5a25dc5d8acaa9de64a162/pillow-11.2.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:31df6e2d3d8fc99f993fd253e97fae451a8db2e7207acf97859732273e108406" }, 182 | { url = "https://mirrors.aliyun.com/pypi/packages/65/4d/eaf9068dc687c24979e977ce5677e253624bd8b616b286f543f0c1b91662/pillow-11.2.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:062b7a42d672c45a70fa1f8b43d1d38ff76b63421cbbe7f88146b39e8a558d91" }, 183 | { url = "https://mirrors.aliyun.com/pypi/packages/1d/26/0fd443365d9c63bc79feb219f97d935cd4b93af28353cba78d8e77b61719/pillow-11.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4eb92eca2711ef8be42fd3f67533765d9fd043b8c80db204f16c8ea62ee1a751" }, 184 | { url = "https://mirrors.aliyun.com/pypi/packages/49/65/dca4d2506be482c2c6641cacdba5c602bc76d8ceb618fd37de855653a419/pillow-11.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f91ebf30830a48c825590aede79376cb40f110b387c17ee9bd59932c961044f9" }, 185 | { url = "https://mirrors.aliyun.com/pypi/packages/b3/92/1ca0c3f09233bd7decf8f7105a1c4e3162fb9142128c74adad0fb361b7eb/pillow-11.2.1-cp313-cp313t-win32.whl", hash = "sha256:e0b55f27f584ed623221cfe995c912c61606be8513bfa0e07d2c674b4516d9dd" }, 186 | { url = "https://mirrors.aliyun.com/pypi/packages/a5/ac/77525347cb43b83ae905ffe257bbe2cc6fd23acb9796639a1f56aa59d191/pillow-11.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:36d6b82164c39ce5482f649b437382c0fb2395eabc1e2b1702a6deb8ad647d6e" }, 187 | { url = "https://mirrors.aliyun.com/pypi/packages/67/32/32dc030cfa91ca0fc52baebbba2e009bb001122a1daa8b6a79ad830b38d3/pillow-11.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:225c832a13326e34f212d2072982bb1adb210e0cc0b153e688743018c94a2681" }, 188 | ] 189 | 190 | [[package]] 191 | name = "pydantic" 192 | version = "2.11.7" 193 | source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } 194 | dependencies = [ 195 | { name = "annotated-types" }, 196 | { name = "pydantic-core" }, 197 | { name = "typing-extensions" }, 198 | { name = "typing-inspection" }, 199 | ] 200 | sdist = { url = "https://mirrors.aliyun.com/pypi/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db" } 201 | wheels = [ 202 | { url = "https://mirrors.aliyun.com/pypi/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b" }, 203 | ] 204 | 205 | [[package]] 206 | name = "pydantic-core" 207 | version = "2.33.2" 208 | source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } 209 | dependencies = [ 210 | { name = "typing-extensions" }, 211 | ] 212 | sdist = { url = "https://mirrors.aliyun.com/pypi/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc" } 213 | wheels = [ 214 | { url = "https://mirrors.aliyun.com/pypi/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f" }, 215 | { url = "https://mirrors.aliyun.com/pypi/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6" }, 216 | { url = "https://mirrors.aliyun.com/pypi/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef" }, 217 | { url = "https://mirrors.aliyun.com/pypi/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a" }, 218 | { url = "https://mirrors.aliyun.com/pypi/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916" }, 219 | { url = "https://mirrors.aliyun.com/pypi/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a" }, 220 | { url = "https://mirrors.aliyun.com/pypi/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d" }, 221 | { url = "https://mirrors.aliyun.com/pypi/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56" }, 222 | { url = "https://mirrors.aliyun.com/pypi/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5" }, 223 | { url = "https://mirrors.aliyun.com/pypi/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e" }, 224 | { url = "https://mirrors.aliyun.com/pypi/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162" }, 225 | { url = "https://mirrors.aliyun.com/pypi/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849" }, 226 | { url = "https://mirrors.aliyun.com/pypi/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9" }, 227 | { url = "https://mirrors.aliyun.com/pypi/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9" }, 228 | { url = "https://mirrors.aliyun.com/pypi/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac" }, 229 | { url = "https://mirrors.aliyun.com/pypi/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5" }, 230 | { url = "https://mirrors.aliyun.com/pypi/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9" }, 231 | ] 232 | 233 | [[package]] 234 | name = "requests" 235 | version = "2.32.4" 236 | source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } 237 | dependencies = [ 238 | { name = "certifi" }, 239 | { name = "charset-normalizer" }, 240 | { name = "idna" }, 241 | { name = "urllib3" }, 242 | ] 243 | sdist = { url = "https://mirrors.aliyun.com/pypi/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422" } 244 | wheels = [ 245 | { url = "https://mirrors.aliyun.com/pypi/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c" }, 246 | ] 247 | 248 | [[package]] 249 | name = "sniffio" 250 | version = "1.3.1" 251 | source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } 252 | sdist = { url = "https://mirrors.aliyun.com/pypi/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc" } 253 | wheels = [ 254 | { url = "https://mirrors.aliyun.com/pypi/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2" }, 255 | ] 256 | 257 | [[package]] 258 | name = "starlette" 259 | version = "0.46.2" 260 | source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } 261 | dependencies = [ 262 | { name = "anyio" }, 263 | ] 264 | sdist = { url = "https://mirrors.aliyun.com/pypi/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5" } 265 | wheels = [ 266 | { url = "https://mirrors.aliyun.com/pypi/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35" }, 267 | ] 268 | 269 | [[package]] 270 | name = "typing-extensions" 271 | version = "4.14.0" 272 | source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } 273 | sdist = { url = "https://mirrors.aliyun.com/pypi/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4" } 274 | wheels = [ 275 | { url = "https://mirrors.aliyun.com/pypi/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af" }, 276 | ] 277 | 278 | [[package]] 279 | name = "typing-inspection" 280 | version = "0.4.1" 281 | source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } 282 | dependencies = [ 283 | { name = "typing-extensions" }, 284 | ] 285 | sdist = { url = "https://mirrors.aliyun.com/pypi/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28" } 286 | wheels = [ 287 | { url = "https://mirrors.aliyun.com/pypi/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51" }, 288 | ] 289 | 290 | [[package]] 291 | name = "urllib3" 292 | version = "2.5.0" 293 | source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } 294 | sdist = { url = "https://mirrors.aliyun.com/pypi/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760" } 295 | wheels = [ 296 | { url = "https://mirrors.aliyun.com/pypi/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc" }, 297 | ] 298 | 299 | [[package]] 300 | name = "uvicorn" 301 | version = "0.34.3" 302 | source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } 303 | dependencies = [ 304 | { name = "click" }, 305 | { name = "h11" }, 306 | ] 307 | sdist = { url = "https://mirrors.aliyun.com/pypi/packages/de/ad/713be230bcda622eaa35c28f0d328c3675c371238470abdea52417f17a8e/uvicorn-0.34.3.tar.gz", hash = "sha256:35919a9a979d7a59334b6b10e05d77c1d0d574c50e0fc98b8b1a0f165708b55a" } 308 | wheels = [ 309 | { url = "https://mirrors.aliyun.com/pypi/packages/6d/0d/8adfeaa62945f90d19ddc461c55f4a50c258af7662d34b6a3d5d1f8646f6/uvicorn-0.34.3-py3-none-any.whl", hash = "sha256:16246631db62bdfbf069b0645177d6e8a77ba950cfedbfd093acef9444e4d885" }, 310 | ] 311 | --------------------------------------------------------------------------------