├── .gitignore ├── .python-version ├── LICENSE ├── README.md ├── adbdevicemanager.py ├── config.yaml.example ├── pyproject.toml ├── run_tests.py ├── server.py ├── tests ├── __init__.py ├── test_adb_device_manager.py ├── test_config.py └── test_integration.py └── uv.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Python-generated files 2 | __pycache__/ 3 | *.py[oc] 4 | build/ 5 | dist/ 6 | wheels/ 7 | *.egg-info 8 | 9 | # Virtual environments 10 | .venv 11 | 12 | # Project specific files 13 | *.png 14 | window_dump.xml 15 | output.mov 16 | config.yaml 17 | test.py 18 | .coverage 19 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.11 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Android MCP Server 2 | 3 | An MCP (Model Context Protocol) server that provides programmatic control over 4 | Android devices through ADB (Android Debug Bridge). This server exposes 5 | various Android device management capabilities that can be accessed by MCP 6 | clients like [Claude desktop](https://modelcontextprotocol.io/quickstart/user) 7 | and Code editors 8 | (e.g. [Cursor](https://docs.cursor.com/context/model-context-protocol)) 9 | 10 | ## Features 11 | 12 | - 🔧 ADB Command Execution 13 | - 📸 Device Screenshot Capture 14 | - 🎯 UI Layout Analysis 15 | - 📱 Device Package Management 16 | 17 | ## Prerequisites 18 | 19 | - Python 3.x 20 | - ADB (Android Debug Bridge) installed and configured 21 | - Android device or emulator (not tested) 22 | 23 | ## Installation 24 | 25 | 1. Clone the repository: 26 | 27 | ```bash 28 | git clone https://github.com/minhalvp/android-mcp-server.git 29 | cd android-mcp-server 30 | ``` 31 | 32 | 2. Install dependencies: 33 | This project uses [uv](https://github.com/astral-sh/uv) for project 34 | management via various methods of 35 | [installation](https://docs.astral.sh/uv/getting-started/installation/). 36 | 37 | ```bash 38 | uv python install 3.11 39 | uv sync 40 | ``` 41 | 42 | ## Configuration 43 | 44 | The server supports flexible device configuration with multiple usage scenarios. 45 | 46 | ### Device Selection Modes 47 | 48 | **1. Automatic Selection (Recommended for single device)** 49 | 50 | - No configuration file needed 51 | - Automatically connects to the only connected device 52 | - Perfect for development with a single test device 53 | 54 | **2. Manual Device Selection** 55 | 56 | - Use when you have multiple devices connected 57 | - Specify exact device in configuration file 58 | 59 | ### Configuration File (Optional) 60 | 61 | The configuration file (`config.yaml`) is **optional**. If not present, the server will automatically select the device if only one is connected. 62 | 63 | #### For Automatic Selection 64 | 65 | Simply ensure only one device is connected and run the server - no configuration needed! 66 | 67 | #### For Manual Selection 68 | 69 | 1. Create a configuration file: 70 | 71 | ```bash 72 | cp config.yaml.example config.yaml 73 | ``` 74 | 75 | 2. Edit `config.yaml` and specify your device: 76 | 77 | ```yaml 78 | device: 79 | name: "your-device-serial-here" # Device identifier from 'adb devices' 80 | ``` 81 | 82 | **For auto-selection**, you can use any of these methods: 83 | 84 | ```yaml 85 | device: 86 | name: null # Explicit null (recommended) 87 | # name: "" # Empty string 88 | # name: # Or leave empty/comment out 89 | ``` 90 | 91 | ### Finding Your Device Serial 92 | 93 | To find your device identifier, run: 94 | 95 | ```bash 96 | adb devices 97 | ``` 98 | 99 | Example output: 100 | 101 | ``` 102 | List of devices attached 103 | 13b22d7f device 104 | emulator-5554 device 105 | ``` 106 | 107 | Use the first column value (e.g., `13b22d7f` or `emulator-5554`) as the device name. 108 | 109 | ### Usage Scenarios 110 | 111 | | Scenario | Configuration Required | Behavior | 112 | |----------|----------------------|----------| 113 | | Single device connected | None | ✅ Auto-connects to the device | 114 | | Multiple devices, want specific one | `config.yaml` with `device.name` | ✅ Connects to specified device | 115 | | Multiple devices, no config | None | ❌ Shows error with available devices | 116 | | No devices connected | N/A | ❌ Shows "no devices" error | 117 | 118 | **Note**: If you have multiple devices connected and don't specify which one to use, the server will show an error message listing all available devices. 119 | 120 | ## Usage 121 | 122 | An MCP client is needed to use this server. The Claude Desktop app is an example 123 | of an MCP client. To use this server with Claude Desktop: 124 | 125 | 1. Locate your Claude Desktop configuration file: 126 | 127 | - Windows: `%APPDATA%\Claude\claude_desktop_config.json` 128 | - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json` 129 | 130 | 2. Add the Android MCP server configuration to the `mcpServers` section: 131 | 132 | ```json 133 | { 134 | "mcpServers": { 135 | "android": { 136 | "command": "path/to/uv", 137 | "args": ["--directory", "path/to/android-mcp-server", "run", "server.py"] 138 | } 139 | } 140 | } 141 | ``` 142 | 143 | Replace: 144 | 145 | - `path/to/uv` with the actual path to your `uv` executable 146 | - `path/to/android-mcp-server` with the absolute path to where you cloned this 147 | repository 148 | 149 | 150 | 151 | ### Available Tools 152 | 153 | The server exposes the following tools: 154 | 155 | ```python 156 | def get_packages() -> str: 157 | """ 158 | Get all installed packages on the device. 159 | Returns: 160 | str: A list of all installed packages on the device as a string 161 | """ 162 | ``` 163 | 164 | ```python 165 | def execute_adb_command(command: str) -> str: 166 | """ 167 | Executes an ADB command and returns the output. 168 | Args: 169 | command (str): The ADB command to execute 170 | Returns: 171 | str: The output of the ADB command 172 | """ 173 | ``` 174 | 175 | ```python 176 | def get_uilayout() -> str: 177 | """ 178 | Retrieves information about clickable elements in the current UI. 179 | Returns a formatted string containing details about each clickable element, 180 | including their text, content description, bounds, and center coordinates. 181 | 182 | Returns: 183 | str: A formatted list of clickable elements with their properties 184 | """ 185 | ``` 186 | 187 | ```python 188 | def get_screenshot() -> Image: 189 | """ 190 | Takes a screenshot of the device and returns it. 191 | Returns: 192 | Image: the screenshot 193 | """ 194 | ``` 195 | 196 | ```python 197 | def get_package_action_intents(package_name: str) -> list[str]: 198 | """ 199 | Get all non-data actions from Activity Resolver Table for a package 200 | Args: 201 | package_name (str): The name of the package to get actions for 202 | Returns: 203 | list[str]: A list of all non-data actions from the Activity Resolver 204 | Table for the package 205 | """ 206 | ``` 207 | 208 | ## Contributing 209 | 210 | Contributions are welcome! 211 | 212 | ## Acknowledgments 213 | 214 | - Built with 215 | [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) 216 | -------------------------------------------------------------------------------- /adbdevicemanager.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | import sys 4 | 5 | from PIL import Image as PILImage 6 | from ppadb.client import Client as AdbClient 7 | 8 | 9 | class AdbDeviceManager: 10 | def __init__(self, device_name: str | None = None, exit_on_error: bool = True) -> None: 11 | """ 12 | Initialize the ADB Device Manager 13 | 14 | Args: 15 | device_name: Optional name/serial of the device to manage. 16 | If None, attempts to auto-select if only one device is available. 17 | exit_on_error: Whether to exit the program if device initialization fails 18 | """ 19 | if not self.check_adb_installed(): 20 | error_msg = "adb is not installed or not in PATH. Please install adb and ensure it is in your PATH." 21 | if exit_on_error: 22 | print(error_msg, file=sys.stderr) 23 | sys.exit(1) 24 | else: 25 | raise RuntimeError(error_msg) 26 | 27 | available_devices = self.get_available_devices() 28 | if not available_devices: 29 | error_msg = "No devices connected. Please connect a device and try again." 30 | if exit_on_error: 31 | print(error_msg, file=sys.stderr) 32 | sys.exit(1) 33 | else: 34 | raise RuntimeError(error_msg) 35 | 36 | selected_device_name: str | None = None 37 | 38 | if device_name: 39 | if device_name not in available_devices: 40 | error_msg = f"Device {device_name} not found. Available devices: {available_devices}" 41 | if exit_on_error: 42 | print(error_msg, file=sys.stderr) 43 | sys.exit(1) 44 | else: 45 | raise RuntimeError(error_msg) 46 | selected_device_name = device_name 47 | else: # No device_name provided, try auto-selection 48 | if len(available_devices) == 1: 49 | selected_device_name = available_devices[0] 50 | print( 51 | f"No device specified, automatically selected: {selected_device_name}") 52 | elif len(available_devices) > 1: 53 | error_msg = f"Multiple devices connected: {available_devices}. Please specify a device in config.yaml or connect only one device." 54 | if exit_on_error: 55 | print(error_msg, file=sys.stderr) 56 | sys.exit(1) 57 | else: 58 | raise RuntimeError(error_msg) 59 | # If len(available_devices) == 0, it's already caught by the earlier check 60 | 61 | # At this point, selected_device_name should always be set due to the logic above 62 | # Initialize the device 63 | self.device = AdbClient().device(selected_device_name) 64 | 65 | @staticmethod 66 | def check_adb_installed() -> bool: 67 | """Check if ADB is installed on the system.""" 68 | try: 69 | subprocess.run(["adb", "version"], check=True, 70 | stdout=subprocess.PIPE) 71 | return True 72 | except (subprocess.CalledProcessError, FileNotFoundError): 73 | return False 74 | 75 | @staticmethod 76 | def get_available_devices() -> list[str]: 77 | """Get a list of available devices.""" 78 | return [device.serial for device in AdbClient().devices()] 79 | 80 | def get_packages(self) -> str: 81 | command = "pm list packages" 82 | packages = self.device.shell(command).strip().split("\n") 83 | result = [package[8:] for package in packages] 84 | output = "\n".join(result) 85 | return output 86 | 87 | def get_package_action_intents(self, package_name: str) -> list[str]: 88 | command = f"dumpsys package {package_name}" 89 | output = self.device.shell(command) 90 | 91 | resolver_table_start = output.find("Activity Resolver Table:") 92 | if resolver_table_start == -1: 93 | return [] 94 | resolver_section = output[resolver_table_start:] 95 | 96 | non_data_start = resolver_section.find("\n Non-Data Actions:") 97 | if non_data_start == -1: 98 | return [] 99 | 100 | section_end = resolver_section[non_data_start:].find("\n\n") 101 | if section_end == -1: 102 | non_data_section = resolver_section[non_data_start:] 103 | else: 104 | non_data_section = resolver_section[ 105 | non_data_start: non_data_start + section_end 106 | ] 107 | 108 | actions = [] 109 | for line in non_data_section.split("\n"): 110 | line = line.strip() 111 | if line.startswith("android.") or line.startswith("com."): 112 | actions.append(line) 113 | 114 | return actions 115 | 116 | def execute_adb_shell_command(self, command: str) -> str: 117 | """Executes an ADB command and returns the output.""" 118 | if command.startswith("adb shell "): 119 | command = command[10:] 120 | elif command.startswith("adb "): 121 | command = command[4:] 122 | result = self.device.shell(command) 123 | return result 124 | 125 | def take_screenshot(self) -> None: 126 | self.device.shell("screencap -p /sdcard/screenshot.png") 127 | self.device.pull("/sdcard/screenshot.png", "screenshot.png") 128 | self.device.shell("rm /sdcard/screenshot.png") 129 | 130 | # compressing the ss to avoid "maximum call stack exceeded" error on claude desktop 131 | with PILImage.open("screenshot.png") as img: 132 | width, height = img.size 133 | new_width = int(width * 0.3) 134 | new_height = int(height * 0.3) 135 | resized_img = img.resize( 136 | (new_width, new_height), PILImage.Resampling.LANCZOS 137 | ) 138 | 139 | resized_img.save( 140 | "compressed_screenshot.png", "PNG", quality=85, optimize=True 141 | ) 142 | 143 | def get_uilayout(self) -> str: 144 | self.device.shell("uiautomator dump") 145 | self.device.pull("/sdcard/window_dump.xml", "window_dump.xml") 146 | self.device.shell("rm /sdcard/window_dump.xml") 147 | 148 | import re 149 | import xml.etree.ElementTree as ET 150 | 151 | def calculate_center(bounds_str): 152 | matches = re.findall(r"\[(\d+),(\d+)\]", bounds_str) 153 | if len(matches) == 2: 154 | x1, y1 = map(int, matches[0]) 155 | x2, y2 = map(int, matches[1]) 156 | center_x = (x1 + x2) // 2 157 | center_y = (y1 + y2) // 2 158 | return center_x, center_y 159 | return None 160 | 161 | tree = ET.parse("window_dump.xml") 162 | root = tree.getroot() 163 | 164 | clickable_elements = [] 165 | for element in root.findall(".//node[@clickable='true']"): 166 | text = element.get("text", "") 167 | content_desc = element.get("content-desc", "") 168 | bounds = element.get("bounds", "") 169 | 170 | # Only include elements that have either text or content description 171 | if text or content_desc: 172 | center = calculate_center(bounds) 173 | element_info = "Clickable element:" 174 | if text: 175 | element_info += f"\n Text: {text}" 176 | if content_desc: 177 | element_info += f"\n Description: {content_desc}" 178 | element_info += f"\n Bounds: {bounds}" 179 | if center: 180 | element_info += f"\n Center: ({center[0]}, {center[1]})" 181 | clickable_elements.append(element_info) 182 | 183 | if not clickable_elements: 184 | return "No clickable elements found with text or description" 185 | else: 186 | result = "\n\n".join(clickable_elements) 187 | return result 188 | -------------------------------------------------------------------------------- /config.yaml.example: -------------------------------------------------------------------------------- 1 | # Android MCP Server Configuration 2 | # All sections are optional 3 | 4 | # Device configuration (optional) 5 | # If not specified or name is empty, will auto-select device when only one is connected 6 | device: 7 | # For auto-selection when only one device connected, use any of these: 8 | name: null # Explicit null value (recommended) 9 | # name: "" # Empty string 10 | # name: # or leave empty 11 | 12 | # For specific device, uncomment and set: 13 | # name: "your-device-serial-here" 14 | # name: "google-pixel-7-pro:5555" 15 | # name: "emulator-5554" 16 | 17 | # Usage scenarios: 18 | # 1. No config file: Auto-select when only one device connected 19 | # 2. Config file with name: null or name: "": Auto-select when only one device connected 20 | # 3. Config file with device.name: "specific-device": Connect to specific device 21 | # 4. Multiple devices connected without device.name: Will show error and list available devices -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "android-mcp" 7 | version = "0.1.0" 8 | description = "An MCP server for android automation" 9 | readme = "README.md" 10 | requires-python = ">=3.11" 11 | dependencies = [ 12 | "mcp>=1.0.0", 13 | "pure-python-adb>=0.3.0.dev0", 14 | "PyYAML>=6.0", 15 | "Pillow>=10.0.0", 16 | ] 17 | 18 | [project.optional-dependencies] 19 | test = ["pytest>=8.0.0", "pytest-mock>=3.12.0", "pytest-cov>=4.0.0"] 20 | 21 | [tool.setuptools] 22 | py-modules = ["server", "adbdevicemanager"] 23 | 24 | [tool.pytest.ini_options] 25 | testpaths = ["tests"] 26 | python_files = ["test_*.py"] 27 | python_classes = ["Test*"] 28 | python_functions = ["test_*"] 29 | addopts = ["--strict-markers", "--disable-warnings", "-v"] 30 | -------------------------------------------------------------------------------- /run_tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Test runner script for Android MCP Server 4 | 5 | This script installs test dependencies and runs the complete test suite. 6 | """ 7 | 8 | import os 9 | import subprocess 10 | import sys 11 | 12 | 13 | def run_command(command, description): 14 | """Run a command and handle errors""" 15 | print(f"\n{'='*60}") 16 | print(f"Running: {description}") 17 | print(f"Command: {command}") 18 | print(f"{'='*60}") 19 | 20 | try: 21 | result = subprocess.run(command, shell=True, 22 | check=True, capture_output=True, text=True) 23 | if result.stdout: 24 | print(result.stdout) 25 | return True 26 | except subprocess.CalledProcessError as e: 27 | print(f"Error running command: {e}") 28 | if e.stdout: 29 | print(f"STDOUT: {e.stdout}") 30 | if e.stderr: 31 | print(f"STDERR: {e.stderr}") 32 | return False 33 | 34 | 35 | def main(): 36 | """Main test runner function""" 37 | print("Android MCP Server Test Runner") 38 | print("=" * 60) 39 | 40 | # Change to the script directory 41 | script_dir = os.path.dirname(os.path.abspath(__file__)) 42 | os.chdir(script_dir) 43 | print(f"Working directory: {script_dir}") 44 | 45 | # Install test dependencies 46 | if not run_command("pip install -e .[test]", "Installing test dependencies"): 47 | print("Failed to install test dependencies") 48 | return 1 49 | 50 | # Run tests with coverage 51 | if not run_command("pytest tests/ -v --cov=. --cov-report=term-missing", "Running tests with coverage"): 52 | print("Tests failed") 53 | return 1 54 | 55 | print("\n" + "="*60) 56 | print("All tests passed successfully!") 57 | print("="*60) 58 | return 0 59 | 60 | 61 | if __name__ == "__main__": 62 | sys.exit(main()) 63 | -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | import yaml 5 | from mcp.server.fastmcp import FastMCP, Image 6 | 7 | from adbdevicemanager import AdbDeviceManager 8 | 9 | CONFIG_FILE = "config.yaml" 10 | CONFIG_FILE_EXAMPLE = "config.yaml.example" 11 | 12 | # Load config (make config file optional) 13 | config = {} 14 | device_name = None 15 | 16 | if os.path.exists(CONFIG_FILE): 17 | try: 18 | with open(CONFIG_FILE) as f: 19 | config = yaml.safe_load(f.read()) or {} 20 | device_config = config.get("device", {}) 21 | configured_device_name = device_config.get( 22 | "name") if device_config else None 23 | 24 | # Support multiple ways to specify auto-selection: 25 | # 1. name: null (None in Python) 26 | # 2. name: "" (empty string) 27 | # 3. name field completely missing 28 | if configured_device_name and configured_device_name.strip(): 29 | device_name = configured_device_name.strip() 30 | print(f"Loaded config from {CONFIG_FILE}") 31 | print(f"Configured device: {device_name}") 32 | else: 33 | print(f"Loaded config from {CONFIG_FILE}") 34 | print( 35 | "No device specified in config, will auto-select if only one device connected") 36 | except Exception as e: 37 | print(f"Error loading config file {CONFIG_FILE}: {e}", file=sys.stderr) 38 | print( 39 | f"Please check the format of your config file or recreate it from {CONFIG_FILE_EXAMPLE}", file=sys.stderr) 40 | sys.exit(1) 41 | else: 42 | print( 43 | f"Config file {CONFIG_FILE} not found, using auto-selection for device") 44 | 45 | # Initialize MCP and device manager 46 | # AdbDeviceManager will handle auto-selection if device_name is None 47 | mcp = FastMCP("android") 48 | deviceManager = AdbDeviceManager(device_name) 49 | 50 | 51 | @mcp.tool() 52 | def get_packages() -> str: 53 | """ 54 | Get all installed packages on the device 55 | Returns: 56 | str: A list of all installed packages on the device as a string 57 | """ 58 | result = deviceManager.get_packages() 59 | return result 60 | 61 | 62 | @mcp.tool() 63 | def execute_adb_shell_command(command: str) -> str: 64 | """Executes an ADB command and returns the output or an error. 65 | Args: 66 | command (str): The ADB shell command to execute 67 | Returns: 68 | str: The output of the ADB command 69 | """ 70 | result = deviceManager.execute_adb_shell_command(command) 71 | return result 72 | 73 | 74 | @mcp.tool() 75 | def get_uilayout() -> str: 76 | """ 77 | Retrieves information about clickable elements in the current UI. 78 | Returns a formatted string containing details about each clickable element, 79 | including its text, content description, bounds, and center coordinates. 80 | 81 | Returns: 82 | str: A formatted list of clickable elements with their properties 83 | """ 84 | result = deviceManager.get_uilayout() 85 | return result 86 | 87 | 88 | @mcp.tool() 89 | def get_screenshot() -> Image: 90 | """Takes a screenshot of the device and returns it. 91 | Returns: 92 | Image: the screenshot 93 | """ 94 | deviceManager.take_screenshot() 95 | return Image(path="compressed_screenshot.png") 96 | 97 | 98 | @mcp.tool() 99 | def get_package_action_intents(package_name: str) -> list[str]: 100 | """ 101 | Get all non-data actions from Activity Resolver Table for a package 102 | Args: 103 | package_name (str): The name of the package to get actions for 104 | Returns: 105 | list[str]: A list of all non-data actions from the Activity Resolver Table for the package 106 | """ 107 | result = deviceManager.get_package_action_intents(package_name) 108 | return result 109 | 110 | 111 | if __name__ == "__main__": 112 | mcp.run(transport="stdio") 113 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Android MCP Server Tests 3 | 4 | This package contains unit and integration tests for the Android MCP Server. 5 | """ 6 | -------------------------------------------------------------------------------- /tests/test_adb_device_manager.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for AdbDeviceManager 3 | """ 4 | 5 | import os 6 | import sys 7 | from unittest.mock import MagicMock, patch 8 | 9 | import pytest 10 | 11 | from adbdevicemanager import AdbDeviceManager 12 | 13 | # Add the parent directory to the path so we can import our modules 14 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 15 | 16 | 17 | class TestAdbDeviceManager: 18 | """Test AdbDeviceManager functionality""" 19 | 20 | @patch('adbdevicemanager.AdbDeviceManager.check_adb_installed') 21 | @patch('adbdevicemanager.AdbDeviceManager.get_available_devices') 22 | @patch('adbdevicemanager.AdbClient') 23 | def test_single_device_auto_selection(self, mock_adb_client, mock_get_devices, mock_check_adb): 24 | """Test auto-selection when only one device is connected""" 25 | # Setup mocks 26 | mock_check_adb.return_value = True 27 | mock_get_devices.return_value = ["device123"] 28 | mock_device = MagicMock() 29 | mock_adb_client.return_value.device.return_value = mock_device 30 | 31 | # Test with device_name=None (auto-selection) 32 | with patch('builtins.print') as mock_print: 33 | manager = AdbDeviceManager(device_name=None, exit_on_error=False) 34 | 35 | # Verify the correct device was selected 36 | mock_adb_client.return_value.device.assert_called_once_with( 37 | "device123") 38 | assert manager.device == mock_device 39 | 40 | # Verify auto-selection message was printed 41 | mock_print.assert_called_with( 42 | "No device specified, automatically selected: device123") 43 | 44 | @patch('adbdevicemanager.AdbDeviceManager.check_adb_installed') 45 | @patch('adbdevicemanager.AdbDeviceManager.get_available_devices') 46 | def test_multiple_devices_no_selection_error(self, mock_get_devices, mock_check_adb): 47 | """Test error when multiple devices are connected but none specified""" 48 | # Setup mocks 49 | mock_check_adb.return_value = True 50 | mock_get_devices.return_value = ["device123", "device456"] 51 | 52 | # Test with device_name=None and multiple devices 53 | with pytest.raises(RuntimeError) as exc_info: 54 | AdbDeviceManager(device_name=None, exit_on_error=False) 55 | 56 | assert "Multiple devices connected" in str(exc_info.value) 57 | assert "device123" in str(exc_info.value) 58 | assert "device456" in str(exc_info.value) 59 | 60 | @patch('adbdevicemanager.AdbDeviceManager.check_adb_installed') 61 | @patch('adbdevicemanager.AdbDeviceManager.get_available_devices') 62 | @patch('adbdevicemanager.AdbClient') 63 | def test_specific_device_selection(self, mock_adb_client, mock_get_devices, mock_check_adb): 64 | """Test selecting a specific device""" 65 | # Setup mocks 66 | mock_check_adb.return_value = True 67 | mock_get_devices.return_value = ["device123", "device456"] 68 | mock_device = MagicMock() 69 | mock_adb_client.return_value.device.return_value = mock_device 70 | 71 | # Test with specific device name 72 | manager = AdbDeviceManager( 73 | device_name="device456", exit_on_error=False) 74 | 75 | # Verify the correct device was selected 76 | mock_adb_client.return_value.device.assert_called_once_with( 77 | "device456") 78 | assert manager.device == mock_device 79 | 80 | @patch('adbdevicemanager.AdbDeviceManager.check_adb_installed') 81 | @patch('adbdevicemanager.AdbDeviceManager.get_available_devices') 82 | def test_device_not_found_error(self, mock_get_devices, mock_check_adb): 83 | """Test error when specified device is not found""" 84 | # Setup mocks 85 | mock_check_adb.return_value = True 86 | mock_get_devices.return_value = ["device123", "device456"] 87 | 88 | # Test with non-existent device 89 | with pytest.raises(RuntimeError) as exc_info: 90 | AdbDeviceManager(device_name="non-existent-device", 91 | exit_on_error=False) 92 | 93 | assert "Device non-existent-device not found" in str(exc_info.value) 94 | assert "Available devices" in str(exc_info.value) 95 | 96 | @patch('adbdevicemanager.AdbDeviceManager.check_adb_installed') 97 | @patch('adbdevicemanager.AdbDeviceManager.get_available_devices') 98 | def test_no_devices_connected_error(self, mock_get_devices, mock_check_adb): 99 | """Test error when no devices are connected""" 100 | # Setup mocks 101 | mock_check_adb.return_value = True 102 | mock_get_devices.return_value = [] 103 | 104 | # Test with no devices 105 | with pytest.raises(RuntimeError) as exc_info: 106 | AdbDeviceManager(device_name=None, exit_on_error=False) 107 | 108 | assert "No devices connected" in str(exc_info.value) 109 | 110 | @patch('adbdevicemanager.AdbDeviceManager.check_adb_installed') 111 | def test_adb_not_installed_error(self, mock_check_adb): 112 | """Test error when ADB is not installed""" 113 | # Setup mocks 114 | mock_check_adb.return_value = False 115 | 116 | # Test with ADB not installed 117 | with pytest.raises(RuntimeError) as exc_info: 118 | AdbDeviceManager(device_name=None, exit_on_error=False) 119 | 120 | assert "adb is not installed" in str(exc_info.value) 121 | 122 | @patch('subprocess.run') 123 | def test_check_adb_installed_success(self, mock_run): 124 | """Test successful ADB installation check""" 125 | mock_run.return_value = MagicMock() # Successful run 126 | 127 | result = AdbDeviceManager.check_adb_installed() 128 | 129 | assert result is True 130 | mock_run.assert_called_once() 131 | 132 | @patch('subprocess.run') 133 | def test_check_adb_installed_failure(self, mock_run): 134 | """Test failed ADB installation check""" 135 | mock_run.side_effect = FileNotFoundError() # ADB not found 136 | 137 | result = AdbDeviceManager.check_adb_installed() 138 | 139 | assert result is False 140 | 141 | @patch('adbdevicemanager.AdbClient') 142 | def test_get_available_devices(self, mock_adb_client): 143 | """Test getting available devices""" 144 | # Setup mock devices 145 | mock_device1 = MagicMock() 146 | mock_device1.serial = "device123" 147 | mock_device2 = MagicMock() 148 | mock_device2.serial = "device456" 149 | 150 | mock_adb_client.return_value.devices.return_value = [ 151 | mock_device1, mock_device2] 152 | 153 | devices = AdbDeviceManager.get_available_devices() 154 | 155 | assert devices == ["device123", "device456"] 156 | 157 | @patch('adbdevicemanager.AdbDeviceManager.check_adb_installed') 158 | @patch('adbdevicemanager.AdbDeviceManager.get_available_devices') 159 | @patch('adbdevicemanager.AdbClient') 160 | def test_exit_on_error_true(self, mock_adb_client, mock_get_devices, mock_check_adb): 161 | """Test that exit_on_error=True calls sys.exit""" 162 | # Setup mocks to trigger error 163 | mock_check_adb.return_value = True 164 | mock_get_devices.return_value = [] # No devices 165 | 166 | # Test with exit_on_error=True (default) 167 | with patch('sys.exit') as mock_exit: 168 | with patch('builtins.print'): # Suppress error output 169 | AdbDeviceManager(device_name=None, exit_on_error=True) 170 | 171 | mock_exit.assert_called_once_with(1) 172 | -------------------------------------------------------------------------------- /tests/test_config.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for configuration loading logic 3 | """ 4 | 5 | import os 6 | import tempfile 7 | from unittest.mock import mock_open, patch 8 | 9 | import pytest 10 | import yaml 11 | 12 | 13 | class TestConfigLoading: 14 | """Test configuration loading scenarios""" 15 | 16 | def setup_method(self): 17 | """Setup for each test method""" 18 | self.temp_dir = tempfile.mkdtemp() 19 | self.config_file = os.path.join(self.temp_dir, "config.yaml") 20 | 21 | def _load_config_logic(self, config_file_path): 22 | """ 23 | Simulate the config loading logic from server.py 24 | Returns: (device_name, messages) 25 | """ 26 | messages = [] 27 | device_name = None 28 | 29 | if os.path.exists(config_file_path): 30 | try: 31 | with open(config_file_path) as f: 32 | config = yaml.safe_load(f.read()) or {} 33 | device_config = config.get("device", {}) 34 | configured_device_name = device_config.get( 35 | "name") if device_config else None 36 | 37 | if configured_device_name and configured_device_name.strip(): 38 | device_name = configured_device_name.strip() 39 | messages.append(f"Loaded config from {config_file_path}") 40 | messages.append(f"Configured device: {device_name}") 41 | else: 42 | messages.append(f"Loaded config from {config_file_path}") 43 | messages.append( 44 | "No device specified in config, will auto-select if only one device connected") 45 | except Exception as e: 46 | messages.append( 47 | f"Error loading config file {config_file_path}: {e}") 48 | raise 49 | else: 50 | messages.append( 51 | f"Config file {config_file_path} not found, using auto-selection for device") 52 | 53 | return device_name, messages 54 | 55 | def test_no_config_file(self): 56 | """Test behavior when config file doesn't exist""" 57 | non_existent_file = os.path.join(self.temp_dir, "non_existent.yaml") 58 | 59 | device_name, messages = self._load_config_logic(non_existent_file) 60 | 61 | assert device_name is None 62 | assert any("not found" in msg for msg in messages) 63 | assert any("auto-selection" in msg for msg in messages) 64 | 65 | def test_config_with_null_name(self): 66 | """Test config with name: null""" 67 | config_content = """ 68 | device: 69 | name: null 70 | """ 71 | with open(self.config_file, 'w') as f: 72 | f.write(config_content) 73 | 74 | device_name, messages = self._load_config_logic(self.config_file) 75 | 76 | assert device_name is None 77 | assert any("Loaded config" in msg for msg in messages) 78 | assert any("auto-select" in msg for msg in messages) 79 | 80 | def test_config_with_empty_string_name(self): 81 | """Test config with name: ''""" 82 | config_content = """ 83 | device: 84 | name: "" 85 | """ 86 | with open(self.config_file, 'w') as f: 87 | f.write(config_content) 88 | 89 | device_name, messages = self._load_config_logic(self.config_file) 90 | 91 | assert device_name is None 92 | assert any("auto-select" in msg for msg in messages) 93 | 94 | def test_config_with_whitespace_name(self): 95 | """Test config with name containing only whitespace""" 96 | config_content = """ 97 | device: 98 | name: " \t \n " 99 | """ 100 | with open(self.config_file, 'w') as f: 101 | f.write(config_content) 102 | 103 | device_name, messages = self._load_config_logic(self.config_file) 104 | 105 | assert device_name is None 106 | assert any("auto-select" in msg for msg in messages) 107 | 108 | def test_config_without_name_field(self): 109 | """Test config with device section but no name field""" 110 | config_content = """ 111 | device: 112 | # name field is missing 113 | other_setting: value 114 | """ 115 | with open(self.config_file, 'w') as f: 116 | f.write(config_content) 117 | 118 | device_name, messages = self._load_config_logic(self.config_file) 119 | 120 | assert device_name is None 121 | assert any("auto-select" in msg for msg in messages) 122 | 123 | def test_config_without_device_section(self): 124 | """Test config without device section""" 125 | config_content = """ 126 | # No device section 127 | other_config: value 128 | """ 129 | with open(self.config_file, 'w') as f: 130 | f.write(config_content) 131 | 132 | device_name, messages = self._load_config_logic(self.config_file) 133 | 134 | assert device_name is None 135 | assert any("auto-select" in msg for msg in messages) 136 | 137 | def test_config_with_valid_device_name(self): 138 | """Test config with valid device name""" 139 | config_content = """ 140 | device: 141 | name: "test-device-123" 142 | """ 143 | with open(self.config_file, 'w') as f: 144 | f.write(config_content) 145 | 146 | device_name, messages = self._load_config_logic(self.config_file) 147 | 148 | assert device_name == "test-device-123" 149 | assert any( 150 | "Configured device: test-device-123" in msg for msg in messages) 151 | 152 | def test_config_with_device_name_with_whitespace(self): 153 | """Test config with device name that has surrounding whitespace""" 154 | config_content = """ 155 | device: 156 | name: " test-device-123 " 157 | """ 158 | with open(self.config_file, 'w') as f: 159 | f.write(config_content) 160 | 161 | device_name, messages = self._load_config_logic(self.config_file) 162 | 163 | assert device_name == "test-device-123" # Should be trimmed 164 | assert any( 165 | "Configured device: test-device-123" in msg for msg in messages) 166 | 167 | def test_invalid_yaml_config(self): 168 | """Test behavior with invalid YAML""" 169 | config_content = """ 170 | device: 171 | name: "test-device 172 | # Missing closing quote - invalid YAML 173 | """ 174 | with open(self.config_file, 'w') as f: 175 | f.write(config_content) 176 | 177 | with pytest.raises(Exception): 178 | self._load_config_logic(self.config_file) 179 | 180 | def test_empty_config_file(self): 181 | """Test behavior with empty config file""" 182 | with open(self.config_file, 'w') as f: 183 | f.write("") 184 | 185 | device_name, messages = self._load_config_logic(self.config_file) 186 | 187 | assert device_name is None 188 | assert any("auto-select" in msg for msg in messages) 189 | 190 | def teardown_method(self): 191 | """Cleanup after each test method""" 192 | if os.path.exists(self.config_file): 193 | os.remove(self.config_file) 194 | os.rmdir(self.temp_dir) 195 | -------------------------------------------------------------------------------- /tests/test_integration.py: -------------------------------------------------------------------------------- 1 | """ 2 | Integration tests for the complete server initialization flow 3 | """ 4 | 5 | import os 6 | import sys 7 | import tempfile 8 | from unittest.mock import MagicMock, patch 9 | 10 | from adbdevicemanager import AdbDeviceManager 11 | 12 | # Add the parent directory to the path so we can import our modules 13 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 14 | 15 | 16 | class TestServerIntegration: 17 | """Test complete server initialization scenarios""" 18 | 19 | def setup_method(self): 20 | """Setup for each test method""" 21 | self.temp_dir = tempfile.mkdtemp() 22 | self.config_file = os.path.join(self.temp_dir, "config.yaml") 23 | 24 | def _simulate_server_initialization(self, config_file_path): 25 | """ 26 | Simulate the complete server initialization process 27 | Returns: (device_manager, messages) 28 | """ 29 | import yaml 30 | 31 | messages = [] 32 | config = {} 33 | device_name = None 34 | 35 | if os.path.exists(config_file_path): 36 | try: 37 | with open(config_file_path) as f: 38 | config = yaml.safe_load(f.read()) or {} 39 | device_config = config.get("device", {}) 40 | configured_device_name = device_config.get( 41 | "name") if device_config else None 42 | 43 | if configured_device_name and configured_device_name.strip(): 44 | device_name = configured_device_name.strip() 45 | messages.append(f"Loaded config from {config_file_path}") 46 | messages.append(f"Configured device: {device_name}") 47 | else: 48 | messages.append(f"Loaded config from {config_file_path}") 49 | messages.append( 50 | "No device specified in config, will auto-select if only one device connected") 51 | except Exception as e: 52 | messages.append( 53 | f"Error loading config file {config_file_path}: {e}") 54 | raise 55 | else: 56 | messages.append( 57 | f"Config file {config_file_path} not found, using auto-selection for device") 58 | 59 | # Initialize device manager (with mocked dependencies) 60 | device_manager = AdbDeviceManager(device_name, exit_on_error=False) 61 | 62 | return device_manager, messages 63 | 64 | @patch('adbdevicemanager.AdbDeviceManager.check_adb_installed') 65 | @patch('adbdevicemanager.AdbDeviceManager.get_available_devices') 66 | @patch('adbdevicemanager.AdbClient') 67 | def test_no_config_auto_selection_success(self, mock_adb_client, mock_get_devices, mock_check_adb): 68 | """Test successful server start with no config file and single device""" 69 | # Setup mocks 70 | mock_check_adb.return_value = True 71 | mock_get_devices.return_value = ["device123"] 72 | mock_device = MagicMock() 73 | mock_adb_client.return_value.device.return_value = mock_device 74 | 75 | # Use non-existent config file 76 | non_existent_config = os.path.join(self.temp_dir, "non_existent.yaml") 77 | 78 | with patch('builtins.print') as mock_print: 79 | device_manager, messages = self._simulate_server_initialization( 80 | non_existent_config) 81 | 82 | # Verify results 83 | assert device_manager.device == mock_device 84 | assert any("not found" in msg for msg in messages) 85 | assert any("auto-selection" in msg for msg in messages) 86 | mock_print.assert_called_with( 87 | "No device specified, automatically selected: device123") 88 | 89 | @patch('adbdevicemanager.AdbDeviceManager.check_adb_installed') 90 | @patch('adbdevicemanager.AdbDeviceManager.get_available_devices') 91 | @patch('adbdevicemanager.AdbClient') 92 | def test_config_with_null_device_auto_selection(self, mock_adb_client, mock_get_devices, mock_check_adb): 93 | """Test server start with config file containing name: null""" 94 | # Setup mocks 95 | mock_check_adb.return_value = True 96 | mock_get_devices.return_value = ["device456"] 97 | mock_device = MagicMock() 98 | mock_adb_client.return_value.device.return_value = mock_device 99 | 100 | # Create config with null device name 101 | config_content = """ 102 | device: 103 | name: null 104 | """ 105 | with open(self.config_file, 'w') as f: 106 | f.write(config_content) 107 | 108 | with patch('builtins.print') as mock_print: 109 | device_manager, messages = self._simulate_server_initialization( 110 | self.config_file) 111 | 112 | # Verify results 113 | assert device_manager.device == mock_device 114 | assert any("Loaded config" in msg for msg in messages) 115 | assert any("auto-select" in msg for msg in messages) 116 | mock_print.assert_called_with( 117 | "No device specified, automatically selected: device456") 118 | 119 | @patch('adbdevicemanager.AdbDeviceManager.check_adb_installed') 120 | @patch('adbdevicemanager.AdbDeviceManager.get_available_devices') 121 | @patch('adbdevicemanager.AdbClient') 122 | def test_config_with_specific_device(self, mock_adb_client, mock_get_devices, mock_check_adb): 123 | """Test server start with config file specifying a device""" 124 | # Setup mocks 125 | mock_check_adb.return_value = True 126 | mock_get_devices.return_value = ["device123", "device456"] 127 | mock_device = MagicMock() 128 | mock_adb_client.return_value.device.return_value = mock_device 129 | 130 | # Create config with specific device name 131 | config_content = """ 132 | device: 133 | name: "device456" 134 | """ 135 | with open(self.config_file, 'w') as f: 136 | f.write(config_content) 137 | 138 | device_manager, messages = self._simulate_server_initialization( 139 | self.config_file) 140 | 141 | # Verify results 142 | assert device_manager.device == mock_device 143 | mock_adb_client.return_value.device.assert_called_once_with( 144 | "device456") 145 | assert any("Configured device: device456" in msg for msg in messages) 146 | 147 | @patch('adbdevicemanager.AdbDeviceManager.check_adb_installed') 148 | @patch('adbdevicemanager.AdbDeviceManager.get_available_devices') 149 | def test_multiple_devices_no_config_error(self, mock_get_devices, mock_check_adb): 150 | """Test server initialization fails with multiple devices and no config""" 151 | # Setup mocks 152 | mock_check_adb.return_value = True 153 | mock_get_devices.return_value = ["device123", "device456"] 154 | 155 | # Use non-existent config file 156 | non_existent_config = os.path.join(self.temp_dir, "non_existent.yaml") 157 | 158 | try: 159 | device_manager, messages = self._simulate_server_initialization( 160 | non_existent_config) 161 | assert False, "Should have raised an exception" 162 | except RuntimeError as e: 163 | assert "Multiple devices connected" in str(e) 164 | assert "device123" in str(e) 165 | assert "device456" in str(e) 166 | 167 | @patch('adbdevicemanager.AdbDeviceManager.check_adb_installed') 168 | @patch('adbdevicemanager.AdbDeviceManager.get_available_devices') 169 | def test_device_not_found_error(self, mock_get_devices, mock_check_adb): 170 | """Test server initialization fails when specified device is not found""" 171 | # Setup mocks 172 | mock_check_adb.return_value = True 173 | mock_get_devices.return_value = ["device123"] 174 | 175 | # Create config with non-existent device name 176 | config_content = """ 177 | device: 178 | name: "non-existent-device" 179 | """ 180 | with open(self.config_file, 'w') as f: 181 | f.write(config_content) 182 | 183 | try: 184 | device_manager, messages = self._simulate_server_initialization( 185 | self.config_file) 186 | assert False, "Should have raised an exception" 187 | except RuntimeError as e: 188 | assert "Device non-existent-device not found" in str(e) 189 | assert "Available devices" in str(e) 190 | 191 | def teardown_method(self): 192 | """Cleanup after each test method""" 193 | if os.path.exists(self.config_file): 194 | os.remove(self.config_file) 195 | os.rmdir(self.temp_dir) 196 | -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | revision = 1 3 | requires-python = ">=3.11" 4 | 5 | [[package]] 6 | name = "android-mcp" 7 | version = "0.1.0" 8 | source = { virtual = "." } 9 | dependencies = [ 10 | { name = "httpx" }, 11 | { name = "mcp", extra = ["cli"] }, 12 | { name = "pillow" }, 13 | { name = "pure-python-adb" }, 14 | { name = "pyyaml" }, 15 | ] 16 | 17 | [package.optional-dependencies] 18 | test = [ 19 | { name = "pytest" }, 20 | { name = "pytest-cov" }, 21 | { name = "pytest-mock" }, 22 | ] 23 | 24 | [package.metadata] 25 | requires-dist = [ 26 | { name = "httpx", specifier = ">=0.28.1" }, 27 | { name = "mcp", extras = ["cli"], specifier = ">=1.3.0" }, 28 | { name = "pillow", specifier = ">=11.1.0" }, 29 | { name = "pure-python-adb", specifier = ">=0.3.0.dev0" }, 30 | { name = "pytest", marker = "extra == 'test'", specifier = ">=8.0.0" }, 31 | { name = "pytest-cov", marker = "extra == 'test'", specifier = ">=4.0.0" }, 32 | { name = "pytest-mock", marker = "extra == 'test'", specifier = ">=3.12.0" }, 33 | { name = "pyyaml", specifier = ">=6.0.2" }, 34 | ] 35 | provides-extras = ["test"] 36 | 37 | [[package]] 38 | name = "annotated-types" 39 | version = "0.7.0" 40 | source = { registry = "https://pypi.org/simple" } 41 | sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } 42 | wheels = [ 43 | { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, 44 | ] 45 | 46 | [[package]] 47 | name = "anyio" 48 | version = "4.8.0" 49 | source = { registry = "https://pypi.org/simple" } 50 | dependencies = [ 51 | { name = "idna" }, 52 | { name = "sniffio" }, 53 | { name = "typing-extensions", marker = "python_full_version < '3.13'" }, 54 | ] 55 | sdist = { url = "https://files.pythonhosted.org/packages/a3/73/199a98fc2dae33535d6b8e8e6ec01f8c1d76c9adb096c6b7d64823038cde/anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a", size = 181126 } 56 | wheels = [ 57 | { url = "https://files.pythonhosted.org/packages/46/eb/e7f063ad1fec6b3178a3cd82d1a3c4de82cccf283fc42746168188e1cdd5/anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a", size = 96041 }, 58 | ] 59 | 60 | [[package]] 61 | name = "certifi" 62 | version = "2025.1.31" 63 | source = { registry = "https://pypi.org/simple" } 64 | sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } 65 | wheels = [ 66 | { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, 67 | ] 68 | 69 | [[package]] 70 | name = "click" 71 | version = "8.1.8" 72 | source = { registry = "https://pypi.org/simple" } 73 | dependencies = [ 74 | { name = "colorama", marker = "sys_platform == 'win32'" }, 75 | ] 76 | sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } 77 | wheels = [ 78 | { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, 79 | ] 80 | 81 | [[package]] 82 | name = "colorama" 83 | version = "0.4.6" 84 | source = { registry = "https://pypi.org/simple" } 85 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } 86 | wheels = [ 87 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, 88 | ] 89 | 90 | [[package]] 91 | name = "coverage" 92 | version = "7.8.2" 93 | source = { registry = "https://pypi.org/simple" } 94 | sdist = { url = "https://files.pythonhosted.org/packages/ba/07/998afa4a0ecdf9b1981ae05415dad2d4e7716e1b1f00abbd91691ac09ac9/coverage-7.8.2.tar.gz", hash = "sha256:a886d531373a1f6ff9fad2a2ba4a045b68467b779ae729ee0b3b10ac20033b27", size = 812759 } 95 | wheels = [ 96 | { url = "https://files.pythonhosted.org/packages/6a/4d/1ff618ee9f134d0de5cc1661582c21a65e06823f41caf801aadf18811a8e/coverage-7.8.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b99058eef42e6a8dcd135afb068b3d53aff3921ce699e127602efff9956457a9", size = 211692 }, 97 | { url = "https://files.pythonhosted.org/packages/96/fa/c3c1b476de96f2bc7a8ca01a9f1fcb51c01c6b60a9d2c3e66194b2bdb4af/coverage-7.8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5feb7f2c3e6ea94d3b877def0270dff0947b8d8c04cfa34a17be0a4dc1836879", size = 212115 }, 98 | { url = "https://files.pythonhosted.org/packages/f7/c2/5414c5a1b286c0f3881ae5adb49be1854ac5b7e99011501f81c8c1453065/coverage-7.8.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:670a13249b957bb9050fab12d86acef7bf8f6a879b9d1a883799276e0d4c674a", size = 244740 }, 99 | { url = "https://files.pythonhosted.org/packages/cd/46/1ae01912dfb06a642ef3dd9cf38ed4996fda8fe884dab8952da616f81a2b/coverage-7.8.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0bdc8bf760459a4a4187b452213e04d039990211f98644c7292adf1e471162b5", size = 242429 }, 100 | { url = "https://files.pythonhosted.org/packages/06/58/38c676aec594bfe2a87c7683942e5a30224791d8df99bcc8439fde140377/coverage-7.8.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07a989c867986c2a75f158f03fdb413128aad29aca9d4dbce5fc755672d96f11", size = 244218 }, 101 | { url = "https://files.pythonhosted.org/packages/80/0c/95b1023e881ce45006d9abc250f76c6cdab7134a1c182d9713878dfefcb2/coverage-7.8.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2db10dedeb619a771ef0e2949ccba7b75e33905de959c2643a4607bef2f3fb3a", size = 243865 }, 102 | { url = "https://files.pythonhosted.org/packages/57/37/0ae95989285a39e0839c959fe854a3ae46c06610439350d1ab860bf020ac/coverage-7.8.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e6ea7dba4e92926b7b5f0990634b78ea02f208d04af520c73a7c876d5a8d36cb", size = 242038 }, 103 | { url = "https://files.pythonhosted.org/packages/4d/82/40e55f7c0eb5e97cc62cbd9d0746fd24e8caf57be5a408b87529416e0c70/coverage-7.8.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ef2f22795a7aca99fc3c84393a55a53dd18ab8c93fb431004e4d8f0774150f54", size = 242567 }, 104 | { url = "https://files.pythonhosted.org/packages/f9/35/66a51adc273433a253989f0d9cc7aa6bcdb4855382cf0858200afe578861/coverage-7.8.2-cp311-cp311-win32.whl", hash = "sha256:641988828bc18a6368fe72355df5f1703e44411adbe49bba5644b941ce6f2e3a", size = 214194 }, 105 | { url = "https://files.pythonhosted.org/packages/f6/8f/a543121f9f5f150eae092b08428cb4e6b6d2d134152c3357b77659d2a605/coverage-7.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:8ab4a51cb39dc1933ba627e0875046d150e88478dbe22ce145a68393e9652975", size = 215109 }, 106 | { url = "https://files.pythonhosted.org/packages/77/65/6cc84b68d4f35186463cd7ab1da1169e9abb59870c0f6a57ea6aba95f861/coverage-7.8.2-cp311-cp311-win_arm64.whl", hash = "sha256:8966a821e2083c74d88cca5b7dcccc0a3a888a596a04c0b9668a891de3a0cc53", size = 213521 }, 107 | { url = "https://files.pythonhosted.org/packages/8d/2a/1da1ada2e3044fcd4a3254fb3576e160b8fe5b36d705c8a31f793423f763/coverage-7.8.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e2f6fe3654468d061942591aef56686131335b7a8325684eda85dacdf311356c", size = 211876 }, 108 | { url = "https://files.pythonhosted.org/packages/70/e9/3d715ffd5b6b17a8be80cd14a8917a002530a99943cc1939ad5bb2aa74b9/coverage-7.8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:76090fab50610798cc05241bf83b603477c40ee87acd358b66196ab0ca44ffa1", size = 212130 }, 109 | { url = "https://files.pythonhosted.org/packages/a0/02/fdce62bb3c21649abfd91fbdcf041fb99be0d728ff00f3f9d54d97ed683e/coverage-7.8.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bd0a0a5054be160777a7920b731a0570284db5142abaaf81bcbb282b8d99279", size = 246176 }, 110 | { url = "https://files.pythonhosted.org/packages/a7/52/decbbed61e03b6ffe85cd0fea360a5e04a5a98a7423f292aae62423b8557/coverage-7.8.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da23ce9a3d356d0affe9c7036030b5c8f14556bd970c9b224f9c8205505e3b99", size = 243068 }, 111 | { url = "https://files.pythonhosted.org/packages/38/6c/d0e9c0cce18faef79a52778219a3c6ee8e336437da8eddd4ab3dbd8fadff/coverage-7.8.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9392773cffeb8d7e042a7b15b82a414011e9d2b5fdbbd3f7e6a6b17d5e21b20", size = 245328 }, 112 | { url = "https://files.pythonhosted.org/packages/f0/70/f703b553a2f6b6c70568c7e398ed0789d47f953d67fbba36a327714a7bca/coverage-7.8.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:876cbfd0b09ce09d81585d266c07a32657beb3eaec896f39484b631555be0fe2", size = 245099 }, 113 | { url = "https://files.pythonhosted.org/packages/ec/fb/4cbb370dedae78460c3aacbdad9d249e853f3bc4ce5ff0e02b1983d03044/coverage-7.8.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3da9b771c98977a13fbc3830f6caa85cae6c9c83911d24cb2d218e9394259c57", size = 243314 }, 114 | { url = "https://files.pythonhosted.org/packages/39/9f/1afbb2cb9c8699b8bc38afdce00a3b4644904e6a38c7bf9005386c9305ec/coverage-7.8.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a990f6510b3292686713bfef26d0049cd63b9c7bb17e0864f133cbfd2e6167f", size = 244489 }, 115 | { url = "https://files.pythonhosted.org/packages/79/fa/f3e7ec7d220bff14aba7a4786ae47043770cbdceeea1803083059c878837/coverage-7.8.2-cp312-cp312-win32.whl", hash = "sha256:bf8111cddd0f2b54d34e96613e7fbdd59a673f0cf5574b61134ae75b6f5a33b8", size = 214366 }, 116 | { url = "https://files.pythonhosted.org/packages/54/aa/9cbeade19b7e8e853e7ffc261df885d66bf3a782c71cba06c17df271f9e6/coverage-7.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:86a323a275e9e44cdf228af9b71c5030861d4d2610886ab920d9945672a81223", size = 215165 }, 117 | { url = "https://files.pythonhosted.org/packages/c4/73/e2528bf1237d2448f882bbebaec5c3500ef07301816c5c63464b9da4d88a/coverage-7.8.2-cp312-cp312-win_arm64.whl", hash = "sha256:820157de3a589e992689ffcda8639fbabb313b323d26388d02e154164c57b07f", size = 213548 }, 118 | { url = "https://files.pythonhosted.org/packages/1a/93/eb6400a745ad3b265bac36e8077fdffcf0268bdbbb6c02b7220b624c9b31/coverage-7.8.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ea561010914ec1c26ab4188aef8b1567272ef6de096312716f90e5baa79ef8ca", size = 211898 }, 119 | { url = "https://files.pythonhosted.org/packages/1b/7c/bdbf113f92683024406a1cd226a199e4200a2001fc85d6a6e7e299e60253/coverage-7.8.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cb86337a4fcdd0e598ff2caeb513ac604d2f3da6d53df2c8e368e07ee38e277d", size = 212171 }, 120 | { url = "https://files.pythonhosted.org/packages/91/22/594513f9541a6b88eb0dba4d5da7d71596dadef6b17a12dc2c0e859818a9/coverage-7.8.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26a4636ddb666971345541b59899e969f3b301143dd86b0ddbb570bd591f1e85", size = 245564 }, 121 | { url = "https://files.pythonhosted.org/packages/1f/f4/2860fd6abeebd9f2efcfe0fd376226938f22afc80c1943f363cd3c28421f/coverage-7.8.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5040536cf9b13fb033f76bcb5e1e5cb3b57c4807fef37db9e0ed129c6a094257", size = 242719 }, 122 | { url = "https://files.pythonhosted.org/packages/89/60/f5f50f61b6332451520e6cdc2401700c48310c64bc2dd34027a47d6ab4ca/coverage-7.8.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc67994df9bcd7e0150a47ef41278b9e0a0ea187caba72414b71dc590b99a108", size = 244634 }, 123 | { url = "https://files.pythonhosted.org/packages/3b/70/7f4e919039ab7d944276c446b603eea84da29ebcf20984fb1fdf6e602028/coverage-7.8.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e6c86888fd076d9e0fe848af0a2142bf606044dc5ceee0aa9eddb56e26895a0", size = 244824 }, 124 | { url = "https://files.pythonhosted.org/packages/26/45/36297a4c0cea4de2b2c442fe32f60c3991056c59cdc3cdd5346fbb995c97/coverage-7.8.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:684ca9f58119b8e26bef860db33524ae0365601492e86ba0b71d513f525e7050", size = 242872 }, 125 | { url = "https://files.pythonhosted.org/packages/a4/71/e041f1b9420f7b786b1367fa2a375703889ef376e0d48de9f5723fb35f11/coverage-7.8.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8165584ddedb49204c4e18da083913bdf6a982bfb558632a79bdaadcdafd0d48", size = 244179 }, 126 | { url = "https://files.pythonhosted.org/packages/bd/db/3c2bf49bdc9de76acf2491fc03130c4ffc51469ce2f6889d2640eb563d77/coverage-7.8.2-cp313-cp313-win32.whl", hash = "sha256:34759ee2c65362163699cc917bdb2a54114dd06d19bab860725f94ef45a3d9b7", size = 214393 }, 127 | { url = "https://files.pythonhosted.org/packages/c6/dc/947e75d47ebbb4b02d8babb1fad4ad381410d5bc9da7cfca80b7565ef401/coverage-7.8.2-cp313-cp313-win_amd64.whl", hash = "sha256:2f9bc608fbafaee40eb60a9a53dbfb90f53cc66d3d32c2849dc27cf5638a21e3", size = 215194 }, 128 | { url = "https://files.pythonhosted.org/packages/90/31/a980f7df8a37eaf0dc60f932507fda9656b3a03f0abf188474a0ea188d6d/coverage-7.8.2-cp313-cp313-win_arm64.whl", hash = "sha256:9fe449ee461a3b0c7105690419d0b0aba1232f4ff6d120a9e241e58a556733f7", size = 213580 }, 129 | { url = "https://files.pythonhosted.org/packages/8a/6a/25a37dd90f6c95f59355629417ebcb74e1c34e38bb1eddf6ca9b38b0fc53/coverage-7.8.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8369a7c8ef66bded2b6484053749ff220dbf83cba84f3398c84c51a6f748a008", size = 212734 }, 130 | { url = "https://files.pythonhosted.org/packages/36/8b/3a728b3118988725f40950931abb09cd7f43b3c740f4640a59f1db60e372/coverage-7.8.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:159b81df53a5fcbc7d45dae3adad554fdbde9829a994e15227b3f9d816d00b36", size = 212959 }, 131 | { url = "https://files.pythonhosted.org/packages/53/3c/212d94e6add3a3c3f412d664aee452045ca17a066def8b9421673e9482c4/coverage-7.8.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6fcbbd35a96192d042c691c9e0c49ef54bd7ed865846a3c9d624c30bb67ce46", size = 257024 }, 132 | { url = "https://files.pythonhosted.org/packages/a4/40/afc03f0883b1e51bbe804707aae62e29c4e8c8bbc365c75e3e4ddeee9ead/coverage-7.8.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:05364b9cc82f138cc86128dc4e2e1251c2981a2218bfcd556fe6b0fbaa3501be", size = 252867 }, 133 | { url = "https://files.pythonhosted.org/packages/18/a2/3699190e927b9439c6ded4998941a3c1d6fa99e14cb28d8536729537e307/coverage-7.8.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46d532db4e5ff3979ce47d18e2fe8ecad283eeb7367726da0e5ef88e4fe64740", size = 255096 }, 134 | { url = "https://files.pythonhosted.org/packages/b4/06/16e3598b9466456b718eb3e789457d1a5b8bfb22e23b6e8bbc307df5daf0/coverage-7.8.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4000a31c34932e7e4fa0381a3d6deb43dc0c8f458e3e7ea6502e6238e10be625", size = 256276 }, 135 | { url = "https://files.pythonhosted.org/packages/a7/d5/4b5a120d5d0223050a53d2783c049c311eea1709fa9de12d1c358e18b707/coverage-7.8.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:43ff5033d657cd51f83015c3b7a443287250dc14e69910577c3e03bd2e06f27b", size = 254478 }, 136 | { url = "https://files.pythonhosted.org/packages/ba/85/f9ecdb910ecdb282b121bfcaa32fa8ee8cbd7699f83330ee13ff9bbf1a85/coverage-7.8.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94316e13f0981cbbba132c1f9f365cac1d26716aaac130866ca812006f662199", size = 255255 }, 137 | { url = "https://files.pythonhosted.org/packages/50/63/2d624ac7d7ccd4ebbd3c6a9eba9d7fc4491a1226071360d59dd84928ccb2/coverage-7.8.2-cp313-cp313t-win32.whl", hash = "sha256:3f5673888d3676d0a745c3d0e16da338c5eea300cb1f4ada9c872981265e76d8", size = 215109 }, 138 | { url = "https://files.pythonhosted.org/packages/22/5e/7053b71462e970e869111c1853afd642212568a350eba796deefdfbd0770/coverage-7.8.2-cp313-cp313t-win_amd64.whl", hash = "sha256:2c08b05ee8d7861e45dc5a2cc4195c8c66dca5ac613144eb6ebeaff2d502e73d", size = 216268 }, 139 | { url = "https://files.pythonhosted.org/packages/07/69/afa41aa34147655543dbe96994f8a246daf94b361ccf5edfd5df62ce066a/coverage-7.8.2-cp313-cp313t-win_arm64.whl", hash = "sha256:1e1448bb72b387755e1ff3ef1268a06617afd94188164960dba8d0245a46004b", size = 214071 }, 140 | { url = "https://files.pythonhosted.org/packages/69/2f/572b29496d8234e4a7773200dd835a0d32d9e171f2d974f3fe04a9dbc271/coverage-7.8.2-pp39.pp310.pp311-none-any.whl", hash = "sha256:ec455eedf3ba0bbdf8f5a570012617eb305c63cb9f03428d39bf544cb2b94837", size = 203636 }, 141 | { url = "https://files.pythonhosted.org/packages/a0/1a/0b9c32220ad694d66062f571cc5cedfa9997b64a591e8a500bb63de1bd40/coverage-7.8.2-py3-none-any.whl", hash = "sha256:726f32ee3713f7359696331a18daf0c3b3a70bb0ae71141b9d3c52be7c595e32", size = 203623 }, 142 | ] 143 | 144 | [package.optional-dependencies] 145 | toml = [ 146 | { name = "tomli", marker = "python_full_version <= '3.11'" }, 147 | ] 148 | 149 | [[package]] 150 | name = "h11" 151 | version = "0.14.0" 152 | source = { registry = "https://pypi.org/simple" } 153 | sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } 154 | wheels = [ 155 | { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, 156 | ] 157 | 158 | [[package]] 159 | name = "httpcore" 160 | version = "1.0.7" 161 | source = { registry = "https://pypi.org/simple" } 162 | dependencies = [ 163 | { name = "certifi" }, 164 | { name = "h11" }, 165 | ] 166 | sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 } 167 | wheels = [ 168 | { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 }, 169 | ] 170 | 171 | [[package]] 172 | name = "httpx" 173 | version = "0.28.1" 174 | source = { registry = "https://pypi.org/simple" } 175 | dependencies = [ 176 | { name = "anyio" }, 177 | { name = "certifi" }, 178 | { name = "httpcore" }, 179 | { name = "idna" }, 180 | ] 181 | sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } 182 | wheels = [ 183 | { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, 184 | ] 185 | 186 | [[package]] 187 | name = "httpx-sse" 188 | version = "0.4.0" 189 | source = { registry = "https://pypi.org/simple" } 190 | sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 } 191 | wheels = [ 192 | { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 }, 193 | ] 194 | 195 | [[package]] 196 | name = "idna" 197 | version = "3.10" 198 | source = { registry = "https://pypi.org/simple" } 199 | sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } 200 | wheels = [ 201 | { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, 202 | ] 203 | 204 | [[package]] 205 | name = "iniconfig" 206 | version = "2.1.0" 207 | source = { registry = "https://pypi.org/simple" } 208 | sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 } 209 | wheels = [ 210 | { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 }, 211 | ] 212 | 213 | [[package]] 214 | name = "markdown-it-py" 215 | version = "3.0.0" 216 | source = { registry = "https://pypi.org/simple" } 217 | dependencies = [ 218 | { name = "mdurl" }, 219 | ] 220 | sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } 221 | wheels = [ 222 | { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, 223 | ] 224 | 225 | [[package]] 226 | name = "mcp" 227 | version = "1.3.0" 228 | source = { registry = "https://pypi.org/simple" } 229 | dependencies = [ 230 | { name = "anyio" }, 231 | { name = "httpx" }, 232 | { name = "httpx-sse" }, 233 | { name = "pydantic" }, 234 | { name = "pydantic-settings" }, 235 | { name = "sse-starlette" }, 236 | { name = "starlette" }, 237 | { name = "uvicorn" }, 238 | ] 239 | sdist = { url = "https://files.pythonhosted.org/packages/6b/b6/81e5f2490290351fc97bf46c24ff935128cb7d34d68e3987b522f26f7ada/mcp-1.3.0.tar.gz", hash = "sha256:f409ae4482ce9d53e7ac03f3f7808bcab735bdfc0fba937453782efb43882d45", size = 150235 } 240 | wheels = [ 241 | { url = "https://files.pythonhosted.org/packages/d0/d2/a9e87b506b2094f5aa9becc1af5178842701b27217fa43877353da2577e3/mcp-1.3.0-py3-none-any.whl", hash = "sha256:2829d67ce339a249f803f22eba5e90385eafcac45c94b00cab6cef7e8f217211", size = 70672 }, 242 | ] 243 | 244 | [package.optional-dependencies] 245 | cli = [ 246 | { name = "python-dotenv" }, 247 | { name = "typer" }, 248 | ] 249 | 250 | [[package]] 251 | name = "mdurl" 252 | version = "0.1.2" 253 | source = { registry = "https://pypi.org/simple" } 254 | sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } 255 | wheels = [ 256 | { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, 257 | ] 258 | 259 | [[package]] 260 | name = "packaging" 261 | version = "25.0" 262 | source = { registry = "https://pypi.org/simple" } 263 | sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 } 264 | wheels = [ 265 | { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 }, 266 | ] 267 | 268 | [[package]] 269 | name = "pillow" 270 | version = "11.1.0" 271 | source = { registry = "https://pypi.org/simple" } 272 | sdist = { url = "https://files.pythonhosted.org/packages/f3/af/c097e544e7bd278333db77933e535098c259609c4eb3b85381109602fb5b/pillow-11.1.0.tar.gz", hash = "sha256:368da70808b36d73b4b390a8ffac11069f8a5c85f29eff1f1b01bcf3ef5b2a20", size = 46742715 } 273 | wheels = [ 274 | { url = "https://files.pythonhosted.org/packages/dd/d6/2000bfd8d5414fb70cbbe52c8332f2283ff30ed66a9cde42716c8ecbe22c/pillow-11.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e06695e0326d05b06833b40b7ef477e475d0b1ba3a6d27da1bb48c23209bf457", size = 3229968 }, 275 | { url = "https://files.pythonhosted.org/packages/d9/45/3fe487010dd9ce0a06adf9b8ff4f273cc0a44536e234b0fad3532a42c15b/pillow-11.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96f82000e12f23e4f29346e42702b6ed9a2f2fea34a740dd5ffffcc8c539eb35", size = 3101806 }, 276 | { url = "https://files.pythonhosted.org/packages/e3/72/776b3629c47d9d5f1c160113158a7a7ad177688d3a1159cd3b62ded5a33a/pillow-11.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3cd561ded2cf2bbae44d4605837221b987c216cff94f49dfeed63488bb228d2", size = 4322283 }, 277 | { url = "https://files.pythonhosted.org/packages/e4/c2/e25199e7e4e71d64eeb869f5b72c7ddec70e0a87926398785ab944d92375/pillow-11.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f189805c8be5ca5add39e6f899e6ce2ed824e65fb45f3c28cb2841911da19070", size = 4402945 }, 278 | { url = "https://files.pythonhosted.org/packages/c1/ed/51d6136c9d5911f78632b1b86c45241c712c5a80ed7fa7f9120a5dff1eba/pillow-11.1.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:dd0052e9db3474df30433f83a71b9b23bd9e4ef1de13d92df21a52c0303b8ab6", size = 4361228 }, 279 | { url = "https://files.pythonhosted.org/packages/48/a4/fbfe9d5581d7b111b28f1d8c2762dee92e9821bb209af9fa83c940e507a0/pillow-11.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:837060a8599b8f5d402e97197d4924f05a2e0d68756998345c829c33186217b1", size = 4484021 }, 280 | { url = "https://files.pythonhosted.org/packages/39/db/0b3c1a5018117f3c1d4df671fb8e47d08937f27519e8614bbe86153b65a5/pillow-11.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:aa8dd43daa836b9a8128dbe7d923423e5ad86f50a7a14dc688194b7be5c0dea2", size = 4287449 }, 281 | { url = "https://files.pythonhosted.org/packages/d9/58/bc128da7fea8c89fc85e09f773c4901e95b5936000e6f303222490c052f3/pillow-11.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0a2f91f8a8b367e7a57c6e91cd25af510168091fb89ec5146003e424e1558a96", size = 4419972 }, 282 | { url = "https://files.pythonhosted.org/packages/5f/bb/58f34379bde9fe197f51841c5bbe8830c28bbb6d3801f16a83b8f2ad37df/pillow-11.1.0-cp311-cp311-win32.whl", hash = "sha256:c12fc111ef090845de2bb15009372175d76ac99969bdf31e2ce9b42e4b8cd88f", size = 2291201 }, 283 | { url = "https://files.pythonhosted.org/packages/3a/c6/fce9255272bcf0c39e15abd2f8fd8429a954cf344469eaceb9d0d1366913/pillow-11.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fbd43429d0d7ed6533b25fc993861b8fd512c42d04514a0dd6337fb3ccf22761", size = 2625686 }, 284 | { url = "https://files.pythonhosted.org/packages/c8/52/8ba066d569d932365509054859f74f2a9abee273edcef5cd75e4bc3e831e/pillow-11.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:f7955ecf5609dee9442cbface754f2c6e541d9e6eda87fad7f7a989b0bdb9d71", size = 2375194 }, 285 | { url = "https://files.pythonhosted.org/packages/95/20/9ce6ed62c91c073fcaa23d216e68289e19d95fb8188b9fb7a63d36771db8/pillow-11.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2062ffb1d36544d42fcaa277b069c88b01bb7298f4efa06731a7fd6cc290b81a", size = 3226818 }, 286 | { url = "https://files.pythonhosted.org/packages/b9/d8/f6004d98579a2596c098d1e30d10b248798cceff82d2b77aa914875bfea1/pillow-11.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a85b653980faad27e88b141348707ceeef8a1186f75ecc600c395dcac19f385b", size = 3101662 }, 287 | { url = "https://files.pythonhosted.org/packages/08/d9/892e705f90051c7a2574d9f24579c9e100c828700d78a63239676f960b74/pillow-11.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9409c080586d1f683df3f184f20e36fb647f2e0bc3988094d4fd8c9f4eb1b3b3", size = 4329317 }, 288 | { url = "https://files.pythonhosted.org/packages/8c/aa/7f29711f26680eab0bcd3ecdd6d23ed6bce180d82e3f6380fb7ae35fcf3b/pillow-11.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fdadc077553621911f27ce206ffcbec7d3f8d7b50e0da39f10997e8e2bb7f6a", size = 4412999 }, 289 | { url = "https://files.pythonhosted.org/packages/c8/c4/8f0fe3b9e0f7196f6d0bbb151f9fba323d72a41da068610c4c960b16632a/pillow-11.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:93a18841d09bcdd774dcdc308e4537e1f867b3dec059c131fde0327899734aa1", size = 4368819 }, 290 | { url = "https://files.pythonhosted.org/packages/38/0d/84200ed6a871ce386ddc82904bfadc0c6b28b0c0ec78176871a4679e40b3/pillow-11.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9aa9aeddeed452b2f616ff5507459e7bab436916ccb10961c4a382cd3e03f47f", size = 4496081 }, 291 | { url = "https://files.pythonhosted.org/packages/84/9c/9bcd66f714d7e25b64118e3952d52841a4babc6d97b6d28e2261c52045d4/pillow-11.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3cdcdb0b896e981678eee140d882b70092dac83ac1cdf6b3a60e2216a73f2b91", size = 4296513 }, 292 | { url = "https://files.pythonhosted.org/packages/db/61/ada2a226e22da011b45f7104c95ebda1b63dcbb0c378ad0f7c2a710f8fd2/pillow-11.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:36ba10b9cb413e7c7dfa3e189aba252deee0602c86c309799da5a74009ac7a1c", size = 4431298 }, 293 | { url = "https://files.pythonhosted.org/packages/e7/c4/fc6e86750523f367923522014b821c11ebc5ad402e659d8c9d09b3c9d70c/pillow-11.1.0-cp312-cp312-win32.whl", hash = "sha256:cfd5cd998c2e36a862d0e27b2df63237e67273f2fc78f47445b14e73a810e7e6", size = 2291630 }, 294 | { url = "https://files.pythonhosted.org/packages/08/5c/2104299949b9d504baf3f4d35f73dbd14ef31bbd1ddc2c1b66a5b7dfda44/pillow-11.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a697cd8ba0383bba3d2d3ada02b34ed268cb548b369943cd349007730c92bddf", size = 2626369 }, 295 | { url = "https://files.pythonhosted.org/packages/37/f3/9b18362206b244167c958984b57c7f70a0289bfb59a530dd8af5f699b910/pillow-11.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:4dd43a78897793f60766563969442020e90eb7847463eca901e41ba186a7d4a5", size = 2375240 }, 296 | { url = "https://files.pythonhosted.org/packages/b3/31/9ca79cafdce364fd5c980cd3416c20ce1bebd235b470d262f9d24d810184/pillow-11.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ae98e14432d458fc3de11a77ccb3ae65ddce70f730e7c76140653048c71bfcbc", size = 3226640 }, 297 | { url = "https://files.pythonhosted.org/packages/ac/0f/ff07ad45a1f172a497aa393b13a9d81a32e1477ef0e869d030e3c1532521/pillow-11.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cc1331b6d5a6e144aeb5e626f4375f5b7ae9934ba620c0ac6b3e43d5e683a0f0", size = 3101437 }, 298 | { url = "https://files.pythonhosted.org/packages/08/2f/9906fca87a68d29ec4530be1f893149e0cb64a86d1f9f70a7cfcdfe8ae44/pillow-11.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:758e9d4ef15d3560214cddbc97b8ef3ef86ce04d62ddac17ad39ba87e89bd3b1", size = 4326605 }, 299 | { url = "https://files.pythonhosted.org/packages/b0/0f/f3547ee15b145bc5c8b336401b2d4c9d9da67da9dcb572d7c0d4103d2c69/pillow-11.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b523466b1a31d0dcef7c5be1f20b942919b62fd6e9a9be199d035509cbefc0ec", size = 4411173 }, 300 | { url = "https://files.pythonhosted.org/packages/b1/df/bf8176aa5db515c5de584c5e00df9bab0713548fd780c82a86cba2c2fedb/pillow-11.1.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:9044b5e4f7083f209c4e35aa5dd54b1dd5b112b108648f5c902ad586d4f945c5", size = 4369145 }, 301 | { url = "https://files.pythonhosted.org/packages/de/7c/7433122d1cfadc740f577cb55526fdc39129a648ac65ce64db2eb7209277/pillow-11.1.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:3764d53e09cdedd91bee65c2527815d315c6b90d7b8b79759cc48d7bf5d4f114", size = 4496340 }, 302 | { url = "https://files.pythonhosted.org/packages/25/46/dd94b93ca6bd555588835f2504bd90c00d5438fe131cf01cfa0c5131a19d/pillow-11.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:31eba6bbdd27dde97b0174ddf0297d7a9c3a507a8a1480e1e60ef914fe23d352", size = 4296906 }, 303 | { url = "https://files.pythonhosted.org/packages/a8/28/2f9d32014dfc7753e586db9add35b8a41b7a3b46540e965cb6d6bc607bd2/pillow-11.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b5d658fbd9f0d6eea113aea286b21d3cd4d3fd978157cbf2447a6035916506d3", size = 4431759 }, 304 | { url = "https://files.pythonhosted.org/packages/33/48/19c2cbe7403870fbe8b7737d19eb013f46299cdfe4501573367f6396c775/pillow-11.1.0-cp313-cp313-win32.whl", hash = "sha256:f86d3a7a9af5d826744fabf4afd15b9dfef44fe69a98541f666f66fbb8d3fef9", size = 2291657 }, 305 | { url = "https://files.pythonhosted.org/packages/3b/ad/285c556747d34c399f332ba7c1a595ba245796ef3e22eae190f5364bb62b/pillow-11.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:593c5fd6be85da83656b93ffcccc2312d2d149d251e98588b14fbc288fd8909c", size = 2626304 }, 306 | { url = "https://files.pythonhosted.org/packages/e5/7b/ef35a71163bf36db06e9c8729608f78dedf032fc8313d19bd4be5c2588f3/pillow-11.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:11633d58b6ee5733bde153a8dafd25e505ea3d32e261accd388827ee987baf65", size = 2375117 }, 307 | { url = "https://files.pythonhosted.org/packages/79/30/77f54228401e84d6791354888549b45824ab0ffde659bafa67956303a09f/pillow-11.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:70ca5ef3b3b1c4a0812b5c63c57c23b63e53bc38e758b37a951e5bc466449861", size = 3230060 }, 308 | { url = "https://files.pythonhosted.org/packages/ce/b1/56723b74b07dd64c1010fee011951ea9c35a43d8020acd03111f14298225/pillow-11.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8000376f139d4d38d6851eb149b321a52bb8893a88dae8ee7d95840431977081", size = 3106192 }, 309 | { url = "https://files.pythonhosted.org/packages/e1/cd/7bf7180e08f80a4dcc6b4c3a0aa9e0b0ae57168562726a05dc8aa8fa66b0/pillow-11.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ee85f0696a17dd28fbcfceb59f9510aa71934b483d1f5601d1030c3c8304f3c", size = 4446805 }, 310 | { url = "https://files.pythonhosted.org/packages/97/42/87c856ea30c8ed97e8efbe672b58c8304dee0573f8c7cab62ae9e31db6ae/pillow-11.1.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:dd0e081319328928531df7a0e63621caf67652c8464303fd102141b785ef9547", size = 4530623 }, 311 | { url = "https://files.pythonhosted.org/packages/ff/41/026879e90c84a88e33fb00cc6bd915ac2743c67e87a18f80270dfe3c2041/pillow-11.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e63e4e5081de46517099dc30abe418122f54531a6ae2ebc8680bcd7096860eab", size = 4465191 }, 312 | { url = "https://files.pythonhosted.org/packages/e5/fb/a7960e838bc5df57a2ce23183bfd2290d97c33028b96bde332a9057834d3/pillow-11.1.0-cp313-cp313t-win32.whl", hash = "sha256:dda60aa465b861324e65a78c9f5cf0f4bc713e4309f83bc387be158b077963d9", size = 2295494 }, 313 | { url = "https://files.pythonhosted.org/packages/d7/6c/6ec83ee2f6f0fda8d4cf89045c6be4b0373ebfc363ba8538f8c999f63fcd/pillow-11.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ad5db5781c774ab9a9b2c4302bbf0c1014960a0a7be63278d13ae6fdf88126fe", size = 2631595 }, 314 | { url = "https://files.pythonhosted.org/packages/cf/6c/41c21c6c8af92b9fea313aa47c75de49e2f9a467964ee33eb0135d47eb64/pillow-11.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:67cd427c68926108778a9005f2a04adbd5e67c442ed21d95389fe1d595458756", size = 2377651 }, 315 | ] 316 | 317 | [[package]] 318 | name = "pluggy" 319 | version = "1.6.0" 320 | source = { registry = "https://pypi.org/simple" } 321 | sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 } 322 | wheels = [ 323 | { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 }, 324 | ] 325 | 326 | [[package]] 327 | name = "pure-python-adb" 328 | version = "0.3.0.dev0" 329 | source = { registry = "https://pypi.org/simple" } 330 | sdist = { url = "https://files.pythonhosted.org/packages/0a/b7/1c4d6b2cbe499b4180177abcf3ae2bb2d8b36acf695ae7d8e9eb99ba00ea/pure-python-adb-0.3.0.dev0.tar.gz", hash = "sha256:0ecc89d780160cfe03260ba26df2c471a05263b2cad0318363573ee8043fb94d", size = 25680 } 331 | 332 | [[package]] 333 | name = "pydantic" 334 | version = "2.10.6" 335 | source = { registry = "https://pypi.org/simple" } 336 | dependencies = [ 337 | { name = "annotated-types" }, 338 | { name = "pydantic-core" }, 339 | { name = "typing-extensions" }, 340 | ] 341 | sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 } 342 | wheels = [ 343 | { url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 }, 344 | ] 345 | 346 | [[package]] 347 | name = "pydantic-core" 348 | version = "2.27.2" 349 | source = { registry = "https://pypi.org/simple" } 350 | dependencies = [ 351 | { name = "typing-extensions" }, 352 | ] 353 | sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 } 354 | wheels = [ 355 | { url = "https://files.pythonhosted.org/packages/c2/89/f3450af9d09d44eea1f2c369f49e8f181d742f28220f88cc4dfaae91ea6e/pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc", size = 1893421 }, 356 | { url = "https://files.pythonhosted.org/packages/9e/e3/71fe85af2021f3f386da42d291412e5baf6ce7716bd7101ea49c810eda90/pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7", size = 1814998 }, 357 | { url = "https://files.pythonhosted.org/packages/a6/3c/724039e0d848fd69dbf5806894e26479577316c6f0f112bacaf67aa889ac/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15", size = 1826167 }, 358 | { url = "https://files.pythonhosted.org/packages/2b/5b/1b29e8c1fb5f3199a9a57c1452004ff39f494bbe9bdbe9a81e18172e40d3/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306", size = 1865071 }, 359 | { url = "https://files.pythonhosted.org/packages/89/6c/3985203863d76bb7d7266e36970d7e3b6385148c18a68cc8915fd8c84d57/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99", size = 2036244 }, 360 | { url = "https://files.pythonhosted.org/packages/0e/41/f15316858a246b5d723f7d7f599f79e37493b2e84bfc789e58d88c209f8a/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459", size = 2737470 }, 361 | { url = "https://files.pythonhosted.org/packages/a8/7c/b860618c25678bbd6d1d99dbdfdf0510ccb50790099b963ff78a124b754f/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048", size = 1992291 }, 362 | { url = "https://files.pythonhosted.org/packages/bf/73/42c3742a391eccbeab39f15213ecda3104ae8682ba3c0c28069fbcb8c10d/pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d", size = 1994613 }, 363 | { url = "https://files.pythonhosted.org/packages/94/7a/941e89096d1175d56f59340f3a8ebaf20762fef222c298ea96d36a6328c5/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b", size = 2002355 }, 364 | { url = "https://files.pythonhosted.org/packages/6e/95/2359937a73d49e336a5a19848713555605d4d8d6940c3ec6c6c0ca4dcf25/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474", size = 2126661 }, 365 | { url = "https://files.pythonhosted.org/packages/2b/4c/ca02b7bdb6012a1adef21a50625b14f43ed4d11f1fc237f9d7490aa5078c/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6", size = 2153261 }, 366 | { url = "https://files.pythonhosted.org/packages/72/9d/a241db83f973049a1092a079272ffe2e3e82e98561ef6214ab53fe53b1c7/pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c", size = 1812361 }, 367 | { url = "https://files.pythonhosted.org/packages/e8/ef/013f07248041b74abd48a385e2110aa3a9bbfef0fbd97d4e6d07d2f5b89a/pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc", size = 1982484 }, 368 | { url = "https://files.pythonhosted.org/packages/10/1c/16b3a3e3398fd29dca77cea0a1d998d6bde3902fa2706985191e2313cc76/pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4", size = 1867102 }, 369 | { url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127 }, 370 | { url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340 }, 371 | { url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900 }, 372 | { url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177 }, 373 | { url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046 }, 374 | { url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386 }, 375 | { url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060 }, 376 | { url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870 }, 377 | { url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822 }, 378 | { url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364 }, 379 | { url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303 }, 380 | { url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064 }, 381 | { url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046 }, 382 | { url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092 }, 383 | { url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 }, 384 | { url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 }, 385 | { url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 }, 386 | { url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 }, 387 | { url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 }, 388 | { url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 }, 389 | { url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 }, 390 | { url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 }, 391 | { url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 }, 392 | { url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 }, 393 | { url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 }, 394 | { url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 }, 395 | { url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 }, 396 | { url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 }, 397 | ] 398 | 399 | [[package]] 400 | name = "pydantic-settings" 401 | version = "2.8.0" 402 | source = { registry = "https://pypi.org/simple" } 403 | dependencies = [ 404 | { name = "pydantic" }, 405 | { name = "python-dotenv" }, 406 | ] 407 | sdist = { url = "https://files.pythonhosted.org/packages/ca/a2/ad2511ede77bb424f3939e5148a56d968cdc6b1462620d24b2a1f4ab65b4/pydantic_settings-2.8.0.tar.gz", hash = "sha256:88e2ca28f6e68ea102c99c3c401d6c9078e68a5df600e97b43891c34e089500a", size = 83347 } 408 | wheels = [ 409 | { url = "https://files.pythonhosted.org/packages/c1/a9/3b9642025174bbe67e900785fb99c9bfe91ea584b0b7126ff99945c24a0e/pydantic_settings-2.8.0-py3-none-any.whl", hash = "sha256:c782c7dc3fb40e97b238e713c25d26f64314aece2e91abcff592fcac15f71820", size = 30746 }, 410 | ] 411 | 412 | [[package]] 413 | name = "pygments" 414 | version = "2.19.1" 415 | source = { registry = "https://pypi.org/simple" } 416 | sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } 417 | wheels = [ 418 | { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, 419 | ] 420 | 421 | [[package]] 422 | name = "pytest" 423 | version = "8.3.5" 424 | source = { registry = "https://pypi.org/simple" } 425 | dependencies = [ 426 | { name = "colorama", marker = "sys_platform == 'win32'" }, 427 | { name = "iniconfig" }, 428 | { name = "packaging" }, 429 | { name = "pluggy" }, 430 | ] 431 | sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 } 432 | wheels = [ 433 | { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 }, 434 | ] 435 | 436 | [[package]] 437 | name = "pytest-cov" 438 | version = "6.1.1" 439 | source = { registry = "https://pypi.org/simple" } 440 | dependencies = [ 441 | { name = "coverage", extra = ["toml"] }, 442 | { name = "pytest" }, 443 | ] 444 | sdist = { url = "https://files.pythonhosted.org/packages/25/69/5f1e57f6c5a39f81411b550027bf72842c4567ff5fd572bed1edc9e4b5d9/pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a", size = 66857 } 445 | wheels = [ 446 | { url = "https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde", size = 23841 }, 447 | ] 448 | 449 | [[package]] 450 | name = "pytest-mock" 451 | version = "3.14.1" 452 | source = { registry = "https://pypi.org/simple" } 453 | dependencies = [ 454 | { name = "pytest" }, 455 | ] 456 | sdist = { url = "https://files.pythonhosted.org/packages/71/28/67172c96ba684058a4d24ffe144d64783d2a270d0af0d9e792737bddc75c/pytest_mock-3.14.1.tar.gz", hash = "sha256:159e9edac4c451ce77a5cdb9fc5d1100708d2dd4ba3c3df572f14097351af80e", size = 33241 } 457 | wheels = [ 458 | { url = "https://files.pythonhosted.org/packages/b2/05/77b60e520511c53d1c1ca75f1930c7dd8e971d0c4379b7f4b3f9644685ba/pytest_mock-3.14.1-py3-none-any.whl", hash = "sha256:178aefcd11307d874b4cd3100344e7e2d888d9791a6a1d9bfe90fbc1b74fd1d0", size = 9923 }, 459 | ] 460 | 461 | [[package]] 462 | name = "python-dotenv" 463 | version = "1.0.1" 464 | source = { registry = "https://pypi.org/simple" } 465 | sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 } 466 | wheels = [ 467 | { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, 468 | ] 469 | 470 | [[package]] 471 | name = "pyyaml" 472 | version = "6.0.2" 473 | source = { registry = "https://pypi.org/simple" } 474 | sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } 475 | wheels = [ 476 | { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, 477 | { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, 478 | { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, 479 | { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, 480 | { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, 481 | { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, 482 | { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, 483 | { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, 484 | { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, 485 | { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, 486 | { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, 487 | { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, 488 | { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, 489 | { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, 490 | { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, 491 | { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, 492 | { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, 493 | { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, 494 | { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, 495 | { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, 496 | { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, 497 | { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, 498 | { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, 499 | { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, 500 | { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, 501 | { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, 502 | { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, 503 | ] 504 | 505 | [[package]] 506 | name = "rich" 507 | version = "13.9.4" 508 | source = { registry = "https://pypi.org/simple" } 509 | dependencies = [ 510 | { name = "markdown-it-py" }, 511 | { name = "pygments" }, 512 | ] 513 | sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 } 514 | wheels = [ 515 | { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 }, 516 | ] 517 | 518 | [[package]] 519 | name = "shellingham" 520 | version = "1.5.4" 521 | source = { registry = "https://pypi.org/simple" } 522 | sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 } 523 | wheels = [ 524 | { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, 525 | ] 526 | 527 | [[package]] 528 | name = "sniffio" 529 | version = "1.3.1" 530 | source = { registry = "https://pypi.org/simple" } 531 | sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } 532 | wheels = [ 533 | { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, 534 | ] 535 | 536 | [[package]] 537 | name = "sse-starlette" 538 | version = "2.2.1" 539 | source = { registry = "https://pypi.org/simple" } 540 | dependencies = [ 541 | { name = "anyio" }, 542 | { name = "starlette" }, 543 | ] 544 | sdist = { url = "https://files.pythonhosted.org/packages/71/a4/80d2a11af59fe75b48230846989e93979c892d3a20016b42bb44edb9e398/sse_starlette-2.2.1.tar.gz", hash = "sha256:54470d5f19274aeed6b2d473430b08b4b379ea851d953b11d7f1c4a2c118b419", size = 17376 } 545 | wheels = [ 546 | { url = "https://files.pythonhosted.org/packages/d9/e0/5b8bd393f27f4a62461c5cf2479c75a2cc2ffa330976f9f00f5f6e4f50eb/sse_starlette-2.2.1-py3-none-any.whl", hash = "sha256:6410a3d3ba0c89e7675d4c273a301d64649c03a5ef1ca101f10b47f895fd0e99", size = 10120 }, 547 | ] 548 | 549 | [[package]] 550 | name = "starlette" 551 | version = "0.46.0" 552 | source = { registry = "https://pypi.org/simple" } 553 | dependencies = [ 554 | { name = "anyio" }, 555 | ] 556 | sdist = { url = "https://files.pythonhosted.org/packages/44/b6/fb9a32e3c5d59b1e383c357534c63c2d3caa6f25bf3c59dd89d296ecbaec/starlette-0.46.0.tar.gz", hash = "sha256:b359e4567456b28d473d0193f34c0de0ed49710d75ef183a74a5ce0499324f50", size = 2575568 } 557 | wheels = [ 558 | { url = "https://files.pythonhosted.org/packages/41/94/8af675a62e3c91c2dee47cf92e602cfac86e8767b1a1ac3caf1b327c2ab0/starlette-0.46.0-py3-none-any.whl", hash = "sha256:913f0798bd90ba90a9156383bcf1350a17d6259451d0d8ee27fc0cf2db609038", size = 71991 }, 559 | ] 560 | 561 | [[package]] 562 | name = "tomli" 563 | version = "2.2.1" 564 | source = { registry = "https://pypi.org/simple" } 565 | sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } 566 | wheels = [ 567 | { 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 }, 568 | { 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 }, 569 | { 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 }, 570 | { 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 }, 571 | { 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 }, 572 | { 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 }, 573 | { 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 }, 574 | { 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 }, 575 | { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, 576 | { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, 577 | { 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 }, 578 | { 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 }, 579 | { 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 }, 580 | { 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 }, 581 | { 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 }, 582 | { 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 }, 583 | { 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 }, 584 | { 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 }, 585 | { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, 586 | { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, 587 | { 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 }, 588 | { 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 }, 589 | { 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 }, 590 | { 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 }, 591 | { 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 }, 592 | { 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 }, 593 | { 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 }, 594 | { 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 }, 595 | { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 }, 596 | { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 }, 597 | { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, 598 | ] 599 | 600 | [[package]] 601 | name = "typer" 602 | version = "0.15.1" 603 | source = { registry = "https://pypi.org/simple" } 604 | dependencies = [ 605 | { name = "click" }, 606 | { name = "rich" }, 607 | { name = "shellingham" }, 608 | { name = "typing-extensions" }, 609 | ] 610 | sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/dca7b219718afd37a0068f4f2530a727c2b74a8b6e8e0c0080a4c0de4fcd/typer-0.15.1.tar.gz", hash = "sha256:a0588c0a7fa68a1978a069818657778f86abe6ff5ea6abf472f940a08bfe4f0a", size = 99789 } 611 | wheels = [ 612 | { url = "https://files.pythonhosted.org/packages/d0/cc/0a838ba5ca64dc832aa43f727bd586309846b0ffb2ce52422543e6075e8a/typer-0.15.1-py3-none-any.whl", hash = "sha256:7994fb7b8155b64d3402518560648446072864beefd44aa2dc36972a5972e847", size = 44908 }, 613 | ] 614 | 615 | [[package]] 616 | name = "typing-extensions" 617 | version = "4.12.2" 618 | source = { registry = "https://pypi.org/simple" } 619 | sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } 620 | wheels = [ 621 | { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, 622 | ] 623 | 624 | [[package]] 625 | name = "uvicorn" 626 | version = "0.34.0" 627 | source = { registry = "https://pypi.org/simple" } 628 | dependencies = [ 629 | { name = "click" }, 630 | { name = "h11" }, 631 | ] 632 | sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568 } 633 | wheels = [ 634 | { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 }, 635 | ] 636 | --------------------------------------------------------------------------------