diff --git a/README.md b/README.md index 34e46d2..a742d3d 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,28 @@ -# ILSpy MCP Server +# mcilspy A Model Context Protocol (MCP) server that provides .NET assembly decompilation capabilities using ILSpy. ## Features +### ILSpy-based Tools (requires ilspycmd) - **Decompile Assemblies**: Convert .NET assemblies back to readable C# source code - **List Types**: Enumerate classes, interfaces, structs, delegates, and enums in assemblies +- **Search Types**: Find types by name pattern with regex support +- **Search Strings**: Find hardcoded strings in assembly code (URLs, credentials, etc.) - **Generate Diagrammer**: Create interactive HTML visualizations of assembly structure -- **Assembly Information**: Get metadata about .NET assemblies +- **Assembly Information**: Get metadata about .NET assemblies (version, target framework, etc.) + +### Direct Metadata Tools (no ilspycmd required) +- **Search Methods**: Find methods by name pattern directly from metadata tables +- **Search Fields**: Find fields and constants in assemblies +- **Search Properties**: Find properties by name pattern +- **List Events**: Enumerate all events defined in an assembly +- **List Resources**: List embedded resources (files, images, etc.) +- **Metadata Summary**: Get comprehensive assembly metadata with statistics + +### Installation & Diagnostics +- **Check Installation**: Verify ilspycmd and dotnet CLI status +- **Install ILSpy**: Automatically install or update ilspycmd ## Prerequisites @@ -16,21 +31,21 @@ A Model Context Protocol (MCP) server that provides .NET assembly decompilation dotnet tool install --global ilspycmd ``` -2. **Python 3.8+**: Required for running the MCP server +2. **Python 3.10+**: Required for running the MCP server ## Installation Install from PyPI: ```bash -pip install ilspy-mcp-server +pip install mcilspy ``` Or for development: ```bash -git clone https://github.com/Borealin/ilspy-mcp-server.git -cd ilspy-mcp-server +git clone https://github.com/Borealin/mcilspy.git +cd mcilspy pip install -e . ``` @@ -45,7 +60,7 @@ Configure your MCP client (e.g., Claude Desktop) to use the server: "mcpServers": { "ilspy": { "command": "python", - "args": ["-m", "ilspy_mcp_server.server"] + "args": ["-m", "mcilspy.server"] } } } @@ -54,16 +69,18 @@ Configure your MCP client (e.g., Claude Desktop) to use the server: ### Available Tools #### 1. `decompile_assembly` -Decompile a .NET assembly to C# source code. +Decompile a .NET assembly to C# source code. This is the primary tool for reverse-engineering .NET binaries. **Parameters:** - `assembly_path` (required): Path to the .NET assembly file - `output_dir` (optional): Output directory for decompiled files -- `type_name` (optional): Specific type to decompile +- `type_name` (optional): Fully qualified type name to decompile (e.g., "MyNamespace.MyClass") - `language_version` (optional): C# language version (default: "Latest") - `create_project` (optional): Create a compilable project structure -- `show_il_code` (optional): Show IL code instead of C# -- `remove_dead_code` (optional): Remove dead code during decompilation +- `show_il_code` (optional): Show IL bytecode instead of C# +- `remove_dead_code` (optional): Remove unreachable code during decompilation +- `remove_dead_stores` (optional): Remove unused variable assignments +- `show_il_sequence_points` (optional): Include debugging sequence points in IL output - `nested_directories` (optional): Use nested directories for namespaces **Example:** @@ -79,11 +96,16 @@ Decompile a .NET assembly to C# source code. ``` #### 2. `list_types` -List types in a .NET assembly. +List types in a .NET assembly. Typically the **first tool to use** when analyzing an unknown assembly. **Parameters:** - `assembly_path` (required): Path to the .NET assembly file -- `entity_types` (optional): Array of entity types to list ("c", "i", "s", "d", "e") +- `entity_types` (optional): Types to include (accepts full names or single letters): + - `"class"` or `"c"` (default) + - `"interface"` or `"i"` + - `"struct"` or `"s"` + - `"delegate"` or `"d"` + - `"enum"` or `"e"` **Example:** ```json @@ -91,13 +113,57 @@ List types in a .NET assembly. "name": "list_types", "arguments": { "assembly_path": "/path/to/MyAssembly.dll", - "entity_types": ["c", "i"] + "entity_types": ["class", "interface"] } } ``` -#### 3. `generate_diagrammer` -Generate an interactive HTML diagrammer. +#### 3. `search_types` +Search for types by name pattern. Essential for finding specific classes in large assemblies. + +**Parameters:** +- `assembly_path` (required): Path to the .NET assembly file +- `pattern` (required): Search pattern to match against type names +- `namespace_filter` (optional): Only search in namespaces containing this string +- `entity_types` (optional): Types to search (default: all types) +- `case_sensitive` (optional): Case-sensitive matching (default: false) +- `use_regex` (optional): Treat pattern as regular expression (default: false) + +**Example:** +```json +{ + "name": "search_types", + "arguments": { + "assembly_path": "/path/to/MyAssembly.dll", + "pattern": "Service", + "namespace_filter": "MyApp.Services" + } +} +``` + +#### 4. `search_strings` +Search for string literals in assembly code. Crucial for reverse engineering - finds hardcoded URLs, credentials, configuration, etc. + +**Parameters:** +- `assembly_path` (required): Path to the .NET assembly file +- `pattern` (required): String pattern to search for +- `case_sensitive` (optional): Case-sensitive matching (default: false) +- `use_regex` (optional): Treat pattern as regular expression (default: false) +- `max_results` (optional): Maximum matches to return (default: 100) + +**Example:** +```json +{ + "name": "search_strings", + "arguments": { + "assembly_path": "/path/to/MyAssembly.dll", + "pattern": "api.example.com" + } +} +``` + +#### 5. `generate_diagrammer` +Generate an interactive HTML diagram showing assembly type relationships. **Parameters:** - `assembly_path` (required): Path to the .NET assembly file @@ -105,12 +171,130 @@ Generate an interactive HTML diagrammer. - `include_pattern` (optional): Regex pattern for types to include - `exclude_pattern` (optional): Regex pattern for types to exclude -#### 4. `get_assembly_info` -Get basic information about an assembly. +#### 6. `get_assembly_info` +Get metadata and version information about an assembly. **Parameters:** - `assembly_path` (required): Path to the .NET assembly file +Returns: Assembly name, version, target framework, signing status, and debug info availability. + +### Direct Metadata Tools + +These tools use direct PE/metadata parsing via [dnfile](https://github.com/malwarefrank/dnfile) and don't require ilspycmd to be installed. + +#### 7. `search_methods` +Search for methods in an assembly by name pattern. Useful for finding entry points, event handlers, and API endpoints. + +**Parameters:** +- `assembly_path` (required): Path to the .NET assembly file +- `pattern` (required): Search pattern to match against method names +- `type_filter` (optional): Only search methods in types containing this string +- `namespace_filter` (optional): Only search in namespaces containing this string +- `public_only` (optional): Only return public methods (default: false) +- `case_sensitive` (optional): Case-sensitive matching (default: false) +- `use_regex` (optional): Treat pattern as regular expression (default: false) + +**Example:** +```json +{ + "name": "search_methods", + "arguments": { + "assembly_path": "/path/to/MyAssembly.dll", + "pattern": "Handle", + "public_only": true + } +} +``` + +#### 8. `search_fields` +Search for fields and constants in an assembly. Useful for finding configuration values and magic numbers. + +**Parameters:** +- `assembly_path` (required): Path to the .NET assembly file +- `pattern` (required): Search pattern to match against field names +- `type_filter` (optional): Only search fields in types containing this string +- `namespace_filter` (optional): Only search in namespaces containing this string +- `public_only` (optional): Only return public fields (default: false) +- `constants_only` (optional): Only return constant (literal) fields (default: false) +- `case_sensitive` (optional): Case-sensitive matching (default: false) +- `use_regex` (optional): Treat pattern as regular expression (default: false) + +#### 9. `search_properties` +Search for properties in an assembly by name pattern. Useful for finding data model fields and configuration properties. + +**Parameters:** +- `assembly_path` (required): Path to the .NET assembly file +- `pattern` (required): Search pattern to match against property names +- `type_filter` (optional): Only search properties in types containing this string +- `namespace_filter` (optional): Only search in namespaces containing this string +- `case_sensitive` (optional): Case-sensitive matching (default: false) +- `use_regex` (optional): Treat pattern as regular expression (default: false) + +#### 10. `list_events` +List all events defined in an assembly. Useful for understanding event-driven architecture and observer patterns. + +**Parameters:** +- `assembly_path` (required): Path to the .NET assembly file +- `type_filter` (optional): Only list events in types containing this string +- `namespace_filter` (optional): Only list events in namespaces containing this string + +#### 11. `list_resources` +List all embedded resources in an assembly. Finds embedded files, images, localization data, etc. + +**Parameters:** +- `assembly_path` (required): Path to the .NET assembly file + +#### 12. `get_metadata_summary` +Get a comprehensive metadata summary with statistics. More accurate than `get_assembly_info` for metadata counts. + +**Parameters:** +- `assembly_path` (required): Path to the .NET assembly file + +Returns: Assembly identity, type/method/field/property/event/resource counts, and referenced assemblies. + +### Installation & Diagnostics Tools + +#### 13. `check_ilspy_installation` +Check if ilspycmd and dotnet CLI are installed and working. Use this to diagnose issues. + +**Parameters:** None + +Returns: Installation status with version information and troubleshooting tips. + +#### 14. `install_ilspy` +Install or update ilspycmd automatically. Detects your platform and package manager for optimal installation experience. + +**Parameters:** +- `update` (optional): If true, update to the latest version even if already installed (default: false) +- `install_dotnet_sdk` (optional): If true, attempt to install .NET SDK when missing (default: false). Supports automatic detection for: + - **Arch Linux**: `pacman` + - **Ubuntu/Debian**: `apt` + - **Fedora/RHEL**: `dnf` + - **openSUSE**: `zypper` + - **macOS**: `homebrew` + - **Windows**: `winget` or `chocolatey` + +**Example - Update ilspycmd:** +```json +{ + "name": "install_ilspy", + "arguments": { + "update": true + } +} +``` + +**Example - Full installation (SDK + ilspycmd):** +```json +{ + "name": "install_ilspy", + "arguments": { + "install_dotnet_sdk": true + } +} +``` + ### Available Prompts #### 1. `analyze_assembly` @@ -135,7 +319,7 @@ Decompile a specific type and provide explanation of its functionality. 1. **Install the package**: ```bash - pip install ilspy-mcp-server + pip install mcilspy ``` 2. **Configure your MCP client** (Claude Desktop example): @@ -144,7 +328,7 @@ Decompile a specific type and provide explanation of its functionality. "mcpServers": { "ilspy": { "command": "python", - "args": ["-m", "ilspy_mcp_server.server"] + "args": ["-m", "mcilspy.server"] } } } @@ -179,7 +363,7 @@ The server provides detailed error messages for common issues: "mcpServers": { "ilspy": { "command": "python", - "args": ["-m", "ilspy_mcp_server.server"], + "args": ["-m", "mcilspy.server"], "env": { "LOGLEVEL": "INFO" } @@ -194,7 +378,7 @@ The server provides detailed error messages for common issues: "mcpServers": { "ilspy": { "command": "python", - "args": ["-m", "ilspy_mcp_server.server"], + "args": ["-m", "mcilspy.server"], "env": { "LOGLEVEL": "DEBUG" } @@ -237,5 +421,6 @@ This project is licensed under the MIT License - see the LICENSE file for detail ## Acknowledgments +- **Original project by [Borealin](https://github.com/Borealin/ilspy-mcp-server)** - Thank you for creating the foundation for this MCP server! - Built on top of the excellent [ILSpy](https://github.com/icsharpcode/ILSpy) decompiler - Uses the [Model Context Protocol](https://modelcontextprotocol.io/) for integration \ No newline at end of file diff --git a/docs/API.md b/docs/API.md index 6ccd4e1..dd88446 100644 --- a/docs/API.md +++ b/docs/API.md @@ -1,16 +1,34 @@ # API Documentation -This document provides detailed API documentation for the ILSpy MCP Server. +This document provides detailed API documentation for mcilspy. ## Overview -The ILSpy MCP Server provides a Model Context Protocol (MCP) interface to the ILSpy .NET decompiler. It exposes four main tools and two prompts for interacting with .NET assemblies. +mcilspy provides a Model Context Protocol (MCP) interface to the ILSpy .NET decompiler. It exposes **14 tools** and two prompts for interacting with .NET assemblies. + +### Tool Categories + +1. **ILSpy-based tools** (require ilspycmd): Decompilation, type listing, diagram generation +2. **Direct metadata tools** (use dnfile): Method/field/property/event search, resource listing +3. **Installation tools**: Check and install ilspycmd automatically + +## Recommended Workflow + +When analyzing an unknown .NET assembly, follow this typical workflow: + +1. **`get_metadata_summary`** - Quick reconnaissance with accurate metadata counts +2. **`list_types`** - Discover what types exist in the assembly +3. **`search_types`** / **`search_methods`** - Find specific types or methods by pattern +4. **`search_strings`** / **`search_fields`** - Find hardcoded strings and constants +5. **`decompile_assembly`** - Deep dive into specific types of interest +6. **`generate_diagrammer`** - Visualize type relationships +7. **`list_resources`** - Check for embedded files ## Tools ### 1. decompile_assembly -Decompiles a .NET assembly to C# source code. +Decompiles a .NET assembly to C# source code. This is the primary tool for reverse-engineering .NET binaries. **Parameters:** @@ -21,8 +39,10 @@ Decompiles a .NET assembly to C# source code. | `type_name` | string | ✗ | null | Fully qualified name of specific type to decompile | | `language_version` | string | ✗ | "Latest" | C# language version to use | | `create_project` | boolean | ✗ | false | Create a compilable project with multiple files | -| `show_il_code` | boolean | ✗ | false | Show IL code instead of C# | -| `remove_dead_code` | boolean | ✗ | false | Remove dead code during decompilation | +| `show_il_code` | boolean | ✗ | false | Show IL bytecode instead of C# | +| `remove_dead_code` | boolean | ✗ | false | Remove unreachable code during decompilation | +| `remove_dead_stores` | boolean | ✗ | false | Remove unused variable assignments | +| `show_il_sequence_points` | boolean | ✗ | false | Include debugging sequence points in IL output | | `nested_directories` | boolean | ✗ | false | Use nested directories for namespaces | **Language Versions:** @@ -48,21 +68,21 @@ Returns decompiled C# source code as text, or information about saved files if ` ### 2. list_types -Lists types (classes, interfaces, structs, etc.) in a .NET assembly. +Lists types (classes, interfaces, structs, etc.) in a .NET assembly. Typically the **first tool to use** when analyzing an unknown assembly. **Parameters:** | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `assembly_path` | string | ✓ | - | Path to the .NET assembly file (.dll or .exe) | -| `entity_types` | array[string] | ✗ | ["c"] | Types of entities to list | +| `entity_types` | array[string] | ✗ | ["class"] | Types of entities to list | -**Entity Types:** -- `c` - Classes -- `i` - Interfaces -- `s` - Structs -- `d` - Delegates -- `e` - Enums +**Entity Types (accepts full names or single letters):** +- `class` or `c` - Classes +- `interface` or `i` - Interfaces +- `struct` or `s` - Structs +- `delegate` or `d` - Delegates +- `enum` or `e` - Enums **Example:** ```json @@ -70,7 +90,7 @@ Lists types (classes, interfaces, structs, etc.) in a .NET assembly. "name": "list_types", "arguments": { "assembly_path": "/path/to/MyAssembly.dll", - "entity_types": ["c", "i", "s"] + "entity_types": ["class", "interface", "struct"] } } ``` @@ -82,9 +102,84 @@ Returns a formatted list of types organized by namespace, including: - Type kind (Class, Interface, etc.) - Namespace -### 3. generate_diagrammer +### 3. search_types -Generates an interactive HTML diagrammer for visualizing assembly structure. +Search for types by name pattern. Essential for finding specific classes in large assemblies. + +**Parameters:** + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| `assembly_path` | string | ✓ | - | Path to the .NET assembly file (.dll or .exe) | +| `pattern` | string | ✓ | - | Search pattern to match against type names | +| `namespace_filter` | string | ✗ | null | Only return types in namespaces containing this string | +| `entity_types` | array[string] | ✗ | all | Types to search (class, interface, struct, delegate, enum) | +| `case_sensitive` | boolean | ✗ | false | Whether pattern matching is case-sensitive | +| `use_regex` | boolean | ✗ | false | Treat pattern as regular expression | + +**Common Search Patterns:** +- `"Service"` - Find all service classes +- `"Controller"` - Find ASP.NET controllers +- `"Handler"` - Find command/event handlers +- `"Exception"` - Find custom exception types +- `"I.*Service"` (with `use_regex=true`) - Find service interfaces + +**Example:** +```json +{ + "name": "search_types", + "arguments": { + "assembly_path": "/path/to/MyAssembly.dll", + "pattern": "Service", + "namespace_filter": "MyApp.Services", + "entity_types": ["class", "interface"] + } +} +``` + +**Response:** +Returns matching types grouped by namespace with full names for use with `decompile_assembly`. + +### 4. search_strings + +Search for string literals in assembly code. Crucial for reverse engineering - finds hardcoded strings. + +**Parameters:** + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| `assembly_path` | string | ✓ | - | Path to the .NET assembly file (.dll or .exe) | +| `pattern` | string | ✓ | - | String pattern to search for in decompiled code | +| `case_sensitive` | boolean | ✗ | false | Whether search is case-sensitive | +| `use_regex` | boolean | ✗ | false | Treat pattern as regular expression | +| `max_results` | integer | ✗ | 100 | Maximum number of matches to return | + +**Use Cases:** +- Find hardcoded URLs and API endpoints +- Locate connection strings +- Discover error messages and logging text +- Find configuration keys and magic values +- Security analysis - find hardcoded credentials +- Locate registry keys and file paths + +**Example:** +```json +{ + "name": "search_strings", + "arguments": { + "assembly_path": "/path/to/MyAssembly.dll", + "pattern": "api.example.com", + "max_results": 50 + } +} +``` + +**Response:** +Returns matching code lines grouped by type, with context about the containing method. + +### 5. generate_diagrammer + +Generates an interactive HTML diagram showing assembly type relationships. **Parameters:** @@ -102,7 +197,8 @@ Generates an interactive HTML diagrammer for visualizing assembly structure. "arguments": { "assembly_path": "/path/to/MyAssembly.dll", "output_dir": "./diagrams", - "include_pattern": "MyNamespace\\..+" + "include_pattern": "MyNamespace\\..+", + "exclude_pattern": ".*Generated.*" } } ``` @@ -110,9 +206,9 @@ Generates an interactive HTML diagrammer for visualizing assembly structure. **Response:** Returns success status and output directory path. The HTML file can be opened in a web browser to view the interactive diagram. -### 4. get_assembly_info +### 6. get_assembly_info -Gets basic information about a .NET assembly. +Gets metadata and version information about a .NET assembly. **Parameters:** @@ -133,13 +229,236 @@ Gets basic information about a .NET assembly. **Response:** Returns assembly metadata including: - Assembly name -- Version +- Version (extracted from AssemblyVersion attribute) - Full name - Location -- Target framework (if available) +- Target framework (e.g., .NET Framework 4.8, .NET 6.0) - Runtime version (if available) - Whether the assembly is signed -- Whether debug information is available +- Whether debug information (PDB) is available + +## Direct Metadata Tools + +These tools use [dnfile](https://github.com/malwarefrank/dnfile) for direct PE/metadata parsing. They do **not require ilspycmd** to be installed. + +### 7. search_methods + +Search for methods in an assembly by name pattern. Uses direct metadata parsing of the MethodDef table. + +**Parameters:** + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| `assembly_path` | string | ✓ | - | Path to the .NET assembly file (.dll or .exe) | +| `pattern` | string | ✓ | - | Search pattern to match against method names | +| `type_filter` | string | ✗ | null | Only search methods in types containing this string | +| `namespace_filter` | string | ✗ | null | Only search in namespaces containing this string | +| `public_only` | boolean | ✗ | false | Only return public methods | +| `case_sensitive` | boolean | ✗ | false | Whether pattern matching is case-sensitive | +| `use_regex` | boolean | ✗ | false | Treat pattern as regular expression | + +**Use Cases:** +- Find entry points and main methods +- Locate event handlers (`OnClick`, `Handle*`) +- Find lifecycle methods (`Initialize`, `Dispose`) +- Discover API endpoints + +**Example:** +```json +{ + "name": "search_methods", + "arguments": { + "assembly_path": "/path/to/MyAssembly.dll", + "pattern": "Handle", + "public_only": true + } +} +``` + +**Response:** +Returns matching methods grouped by declaring type, showing visibility modifiers (public, static, virtual, abstract). + +### 8. search_fields + +Search for fields and constants in an assembly. Uses direct metadata parsing of the Field table. + +**Parameters:** + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| `assembly_path` | string | ✓ | - | Path to the .NET assembly file (.dll or .exe) | +| `pattern` | string | ✓ | - | Search pattern to match against field names | +| `type_filter` | string | ✗ | null | Only search fields in types containing this string | +| `namespace_filter` | string | ✗ | null | Only search in namespaces containing this string | +| `public_only` | boolean | ✗ | false | Only return public fields | +| `constants_only` | boolean | ✗ | false | Only return constant (literal) fields | +| `case_sensitive` | boolean | ✗ | false | Whether pattern matching is case-sensitive | +| `use_regex` | boolean | ✗ | false | Treat pattern as regular expression | + +**Use Cases:** +- Find configuration values and magic numbers +- Locate constant strings (URLs, error messages) +- Discover static fields (singletons, caches) + +**Example:** +```json +{ + "name": "search_fields", + "arguments": { + "assembly_path": "/path/to/MyAssembly.dll", + "pattern": "ConnectionString", + "constants_only": true + } +} +``` + +### 9. search_properties + +Search for properties in an assembly by name pattern. Uses direct metadata parsing of the Property table. + +**Parameters:** + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| `assembly_path` | string | ✓ | - | Path to the .NET assembly file (.dll or .exe) | +| `pattern` | string | ✓ | - | Search pattern to match against property names | +| `type_filter` | string | ✗ | null | Only search properties in types containing this string | +| `namespace_filter` | string | ✗ | null | Only search in namespaces containing this string | +| `case_sensitive` | boolean | ✗ | false | Whether pattern matching is case-sensitive | +| `use_regex` | boolean | ✗ | false | Treat pattern as regular expression | + +**Use Cases:** +- Find configuration properties +- Locate data model fields +- Discover API response/request properties + +### 10. list_events + +List all events defined in an assembly. Uses direct metadata parsing of the Event table. + +**Parameters:** + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| `assembly_path` | string | ✓ | - | Path to the .NET assembly file (.dll or .exe) | +| `type_filter` | string | ✗ | null | Only list events in types containing this string | +| `namespace_filter` | string | ✗ | null | Only list events in namespaces containing this string | + +**Use Cases:** +- Understand event-driven architecture +- Discover observer patterns +- Analyze UI event handlers + +### 11. list_resources + +List all embedded resources in an assembly. Uses direct metadata parsing of the ManifestResource table. + +**Parameters:** + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| `assembly_path` | string | ✓ | - | Path to the .NET assembly file (.dll or .exe) | + +**Use Cases:** +- Find embedded files (images, configs, data) +- Discover localization resources +- Locate embedded assemblies + +### 12. get_metadata_summary + +Get a comprehensive metadata summary with accurate statistics. Uses dnfile for direct metadata counts. + +**Parameters:** + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| `assembly_path` | string | ✓ | - | Path to the .NET assembly file (.dll or .exe) | + +**Response:** +Returns comprehensive assembly information including: +- Assembly identity (name, version, culture, public key token) +- Target framework (if available) +- Statistics table (type/method/field/property/event/resource counts) +- List of referenced assemblies + +**Example:** +```json +{ + "name": "get_metadata_summary", + "arguments": { + "assembly_path": "/path/to/MyAssembly.dll" + } +} +``` + +## Installation & Diagnostics Tools + +These tools help manage ilspycmd installation and diagnose issues. + +### 13. check_ilspy_installation + +Check if ilspycmd and dotnet CLI are installed and working. Use this to diagnose issues with decompilation tools. + +**Parameters:** None + +**Response:** +Returns installation status including: +- dotnet CLI availability and version +- ilspycmd availability, version, and path +- Instructions if anything is missing + +**Example:** +```json +{ + "name": "check_ilspy_installation", + "arguments": {} +} +``` + +### 14. install_ilspy + +Install or update ilspycmd, the ILSpy command-line decompiler. Automatically detects your platform and package manager to provide optimal installation instructions. + +**Parameters:** + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| `update` | boolean | ✗ | false | Update to latest version even if already installed | +| `install_dotnet_sdk` | boolean | ✗ | false | Attempt to install .NET SDK if missing (may require sudo) | + +**Supported Platforms for Auto-Install:** +- **Arch Linux/Manjaro**: `pacman -S dotnet-sdk` +- **Ubuntu/Debian/Mint**: `apt install dotnet-sdk-8.0` +- **Fedora/RHEL/CentOS**: `dnf install dotnet-sdk-8.0` +- **openSUSE**: `zypper install dotnet-sdk-8.0` +- **macOS**: `brew install dotnet-sdk` (Homebrew) +- **Windows**: `winget install Microsoft.DotNet.SDK.8` or `choco install dotnet-sdk` + +**Example - Update ilspycmd:** +```json +{ + "name": "install_ilspy", + "arguments": { + "update": true + } +} +``` + +**Example - Full installation (SDK + ilspycmd):** +```json +{ + "name": "install_ilspy", + "arguments": { + "install_dotnet_sdk": true + } +} +``` + +**Response:** +- Success message with version and path on successful installation +- Platform-specific installation instructions if dotnet CLI is missing +- Option to auto-install with `install_dotnet_sdk=true` +- PATH troubleshooting tips if installation succeeds but tool isn't found ## Prompts @@ -221,14 +540,15 @@ The server provides detailed error messages for common issues: ```python class DecompileRequest(BaseModel): assembly_path: str - output_dir: Optional[str] = None - type_name: Optional[str] = None + output_dir: str | None = None + type_name: str | None = None language_version: LanguageVersion = LanguageVersion.LATEST create_project: bool = False show_il_code: bool = False remove_dead_code: bool = False + remove_dead_stores: bool = False + show_il_sequence_points: bool = False nested_directories: bool = False - # ... additional fields ``` ### TypeInfo @@ -237,7 +557,7 @@ class TypeInfo(BaseModel): name: str full_name: str kind: str - namespace: Optional[str] = None + namespace: str | None = None ``` ### AssemblyInfo @@ -247,12 +567,61 @@ class AssemblyInfo(BaseModel): version: str full_name: str location: str - target_framework: Optional[str] = None - runtime_version: Optional[str] = None + target_framework: str | None = None + runtime_version: str | None = None is_signed: bool = False has_debug_info: bool = False ``` +### MethodInfo (metadata_reader) +```python +@dataclass +class MethodInfo: + name: str + full_name: str + declaring_type: str + namespace: str | None + return_type: str | None = None + is_public: bool = False + is_static: bool = False + is_virtual: bool = False + is_abstract: bool = False + parameters: list[str] = field(default_factory=list) +``` + +### FieldInfo (metadata_reader) +```python +@dataclass +class FieldInfo: + name: str + full_name: str + declaring_type: str + namespace: str | None + field_type: str | None = None + is_public: bool = False + is_static: bool = False + is_literal: bool = False # Constant + default_value: str | None = None +``` + +### AssemblyMetadata (metadata_reader) +```python +@dataclass +class AssemblyMetadata: + name: str + version: str + culture: str | None = None + public_key_token: str | None = None + target_framework: str | None = None + type_count: int = 0 + method_count: int = 0 + field_count: int = 0 + property_count: int = 0 + event_count: int = 0 + resource_count: int = 0 + referenced_assemblies: list[str] = field(default_factory=list) +``` + ## Usage Examples ### Basic Decompilation @@ -271,7 +640,33 @@ result = await session.call_tool( "list_types", { "assembly_path": "MyApp.dll", - "entity_types": ["c", "i"] + "entity_types": ["class", "interface"] + } +) +``` + +### Search for Service Classes +```python +# Find all classes with "Service" in their name +result = await session.call_tool( + "search_types", + { + "assembly_path": "MyApp.dll", + "pattern": "Service", + "entity_types": ["class", "interface"] + } +) +``` + +### Find Hardcoded URLs +```python +# Search for API endpoints +result = await session.call_tool( + "search_strings", + { + "assembly_path": "MyApp.dll", + "pattern": "https://", + "use_regex": False } ) ``` @@ -285,11 +680,55 @@ result = await session.call_tool( "assembly_path": "MyApp.dll", "type_name": "MyApp.Core.Engine", "language_version": "CSharp11_0", - "remove_dead_code": true + "remove_dead_code": True } ) ``` +### Search for Event Handlers (Direct Metadata) +```python +# Find all methods with "Handle" in their name +result = await session.call_tool( + "search_methods", + { + "assembly_path": "MyApp.dll", + "pattern": "Handle", + "public_only": True + } +) +``` + +### Find Constants +```python +# Search for connection string constants +result = await session.call_tool( + "search_fields", + { + "assembly_path": "MyApp.dll", + "pattern": "Connection", + "constants_only": True + } +) +``` + +### Get Assembly Statistics +```python +# Get comprehensive metadata summary +result = await session.call_tool( + "get_metadata_summary", + {"assembly_path": "MyApp.dll"} +) +``` + +### List Embedded Resources +```python +# Find what resources are embedded +result = await session.call_tool( + "list_resources", + {"assembly_path": "MyApp.dll"} +) +``` + ## Configuration ### Environment Variables @@ -305,7 +744,7 @@ For Claude Desktop: { "mcpServers": { "ilspy": { - "command": "ilspy-mcp-server" + "command": "mcilspy" } } } @@ -317,11 +756,11 @@ For development: "mcpServers": { "ilspy": { "command": "python", - "args": ["-m", "ilspy_mcp_server.server"], + "args": ["-m", "mcilspy.server"], "env": { "LOGLEVEL": "DEBUG" } } } } -``` \ No newline at end of file +``` diff --git a/pyproject.toml b/pyproject.toml index 7976b14..17b485c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,15 +1,16 @@ [project] -name = "ilspy-mcp-server" +name = "mcilspy" version = "0.1.1" description = "MCP Server for ILSpy .NET Decompiler" authors = [ {name = "Borealin", email = "me@borealin.cn"} ] dependencies = [ - "mcp>=0.1.0", - "pydantic>=2.0.0", + "mcp>=1.0.0", + "pydantic>=2.7.0", + "dnfile>=0.15.0", # Direct .NET metadata parsing ] -requires-python = ">=3.8" +requires-python = ">=3.10" readme = "README.md" license = {text = "MIT"} keywords = ["mcp", "ilspy", "decompiler", "dotnet", "csharp", "reverse-engineering"] @@ -18,11 +19,10 @@ classifiers = [ "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Software Development :: Debuggers", "Topic :: System :: Software Distribution", @@ -34,18 +34,30 @@ Repository = "https://github.com/Borealin/ilspy-mcp-server.git" Issues = "https://github.com/Borealin/ilspy-mcp-server/issues" [project.scripts] -ilspy-mcp-server = "ilspy_mcp_server.server:main" +mcilspy = "mcilspy.server:main" [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] -packages = ["src/ilspy_mcp_server"] +packages = ["src/mcilspy"] [tool.hatch.build.targets.sdist] include = [ "/src", "/README.md", "/LICENSE", -] \ No newline at end of file +] + +[tool.ruff] +target-version = "py310" +line-length = 100 +src = ["src"] + +[tool.ruff.lint] +select = ["E", "F", "W", "I", "UP", "B", "SIM"] +ignore = ["E501"] # line length handled by formatter + +[tool.ruff.format] +quote-style = "double" \ No newline at end of file diff --git a/src/ilspy_mcp_server/models.py b/src/ilspy_mcp_server/models.py deleted file mode 100644 index 36469ab..0000000 --- a/src/ilspy_mcp_server/models.py +++ /dev/null @@ -1,96 +0,0 @@ -from typing import Optional, List -from pydantic import BaseModel, Field -from enum import Enum - -class LanguageVersion(str, Enum): - """C# Language versions supported by ILSpy.""" - CSHARP1 = "CSharp1" - CSHARP2 = "CSharp2" - CSHARP3 = "CSharp3" - CSHARP4 = "CSharp4" - CSHARP5 = "CSharp5" - CSHARP6 = "CSharp6" - CSHARP7 = "CSharp7" - CSHARP7_1 = "CSharp7_1" - CSHARP7_2 = "CSharp7_2" - CSHARP7_3 = "CSharp7_3" - CSHARP8_0 = "CSharp8_0" - CSHARP9_0 = "CSharp9_0" - CSHARP10_0 = "CSharp10_0" - CSHARP11_0 = "CSharp11_0" - CSHARP12_0 = "CSharp12_0" - PREVIEW = "Preview" - LATEST = "Latest" - -class EntityType(str, Enum): - """Entity types that can be listed.""" - CLASS = "c" - INTERFACE = "i" - STRUCT = "s" - DELEGATE = "d" - ENUM = "e" -class DecompileRequest(BaseModel): - """Request to decompile a .NET assembly.""" - assembly_path: str - output_dir: Optional[str] = None - type_name: Optional[str] = None - language_version: LanguageVersion = LanguageVersion.LATEST - create_project: bool = False - show_il_code: bool = False - reference_paths: List[str] = Field(default_factory=list) - remove_dead_code: bool = False - nested_directories: bool = False - -class ListTypesRequest(BaseModel): - """Request to list types in an assembly.""" - assembly_path: str - entity_types: List[EntityType] = Field(default_factory=lambda: [EntityType.CLASS]) - reference_paths: List[str] = Field(default_factory=list) - -class TypeInfo(BaseModel): - """Information about a type in an assembly.""" - name: str - full_name: str - kind: str - namespace: Optional[str] = None - -class DecompileResponse(BaseModel): - """Response from decompilation operation.""" - success: bool - source_code: Optional[str] = None - output_path: Optional[str] = None - error_message: Optional[str] = None - assembly_name: str - type_name: Optional[str] = None - -class ListTypesResponse(BaseModel): - """Response from list types operation.""" - success: bool - types: List[TypeInfo] = Field(default_factory=list) - total_count: int = 0 - error_message: Optional[str] = None - -class GenerateDiagrammerRequest(BaseModel): - """Request to generate HTML diagrammer.""" - assembly_path: str - output_dir: Optional[str] = None - include_pattern: Optional[str] = None - exclude_pattern: Optional[str] = None - docs_path: Optional[str] = None - strip_namespaces: List[str] = Field(default_factory=list) - report_excluded: bool = False - -class AssemblyInfoRequest(BaseModel): - """Request to get assembly information.""" - assembly_path: str - -class AssemblyInfo(BaseModel): - """Information about an assembly.""" - name: str - version: str - full_name: str - location: str - target_framework: Optional[str] = None - runtime_version: Optional[str] = None - is_signed: bool = False - has_debug_info: bool = False \ No newline at end of file diff --git a/src/ilspy_mcp_server/server.py b/src/ilspy_mcp_server/server.py deleted file mode 100644 index 0d6b3eb..0000000 --- a/src/ilspy_mcp_server/server.py +++ /dev/null @@ -1,268 +0,0 @@ -import logging -import os -from typing import Optional - -from mcp.server.fastmcp import FastMCP, Context -from .ilspy_wrapper import ILSpyWrapper -from .models import LanguageVersion, EntityType - -# Setup logging -log_level = os.getenv('LOGLEVEL', 'INFO').upper() -logging.basicConfig( - level=getattr(logging, log_level, logging.INFO), - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' -) -logger = logging.getLogger(__name__) - -# Create FastMCP server - much simpler than before! -mcp = FastMCP("ilspy-mcp-server") - -# Global ILSpy wrapper -ilspy_wrapper: Optional[ILSpyWrapper] = None - -def get_wrapper() -> ILSpyWrapper: - """Get ILSpy wrapper instance""" - global ilspy_wrapper - if ilspy_wrapper is None: - ilspy_wrapper = ILSpyWrapper() - return ilspy_wrapper - -@mcp.tool() -async def decompile_assembly( - assembly_path: str, - output_dir: str = None, - type_name: str = None, - language_version: str = "Latest", - create_project: bool = False, - show_il_code: bool = False, - remove_dead_code: bool = False, - nested_directories: bool = False, - ctx: Context = None -) -> str: - """Decompile a .NET assembly to C# source code - - Args: - assembly_path: Path to the .NET assembly file (.dll or .exe) - output_dir: Output directory for decompiled files (optional) - type_name: Fully qualified name of specific type to decompile (optional) - language_version: C# language version to use (default: Latest) - create_project: Create a compilable project with multiple files - show_il_code: Show IL code instead of C# - remove_dead_code: Remove dead code during decompilation - nested_directories: Use nested directories for namespaces - """ - if ctx: - await ctx.info(f"Starting decompilation of assembly: {assembly_path}") - - try: - wrapper = get_wrapper() - - # Use simplified request object (no complex pydantic validation needed) - from .models import DecompileRequest - request = DecompileRequest( - assembly_path=assembly_path, - output_dir=output_dir, - type_name=type_name, - language_version=LanguageVersion(language_version), - create_project=create_project, - show_il_code=show_il_code, - remove_dead_code=remove_dead_code, - nested_directories=nested_directories - ) - - response = await wrapper.decompile(request) - - if response.success: - if response.source_code: - content = f"# Decompilation result: {response.assembly_name}" - if response.type_name: - content += f" - {response.type_name}" - content += f"\n\n```csharp\n{response.source_code}\n```" - return content - else: - return f"Decompilation successful! Files saved to: {response.output_path}" - else: - return f"Decompilation failed: {response.error_message}" - - except Exception as e: - logger.error(f"Decompilation error: {e}") - return f"Error: {str(e)}" - -@mcp.tool() -async def list_types( - assembly_path: str, - entity_types: list[str] = None, - ctx: Context = None -) -> str: - """List types (classes, interfaces, structs, etc.) in a .NET assembly - - Args: - assembly_path: Path to the .NET assembly file (.dll or .exe) - entity_types: Types of entities to list (c=class, i=interface, s=struct, d=delegate, e=enum) - """ - if ctx: - await ctx.info(f"Listing types in assembly: {assembly_path}") - - try: - wrapper = get_wrapper() - - # Default to list only classes - if entity_types is None: - entity_types = ["c"] - - # Convert to EntityType enums - entity_type_enums = [] - for et in entity_types: - try: - entity_type_enums.append(EntityType(et)) - except ValueError: - continue - - from .models import ListTypesRequest - request = ListTypesRequest( - assembly_path=assembly_path, - entity_types=entity_type_enums - ) - - response = await wrapper.list_types(request) - - if response.success and response.types: - content = f"# Types in {assembly_path}\n\n" - content += f"Found {response.total_count} types:\n\n" - - # Group by namespace - by_namespace = {} - for type_info in response.types: - ns = type_info.namespace or "(Global)" - if ns not in by_namespace: - by_namespace[ns] = [] - by_namespace[ns].append(type_info) - - for namespace, types in sorted(by_namespace.items()): - content += f"## {namespace}\n\n" - for type_info in sorted(types, key=lambda t: t.name): - content += f"- **{type_info.name}** ({type_info.kind})\n" - content += f" - Full name: `{type_info.full_name}`\n" - content += "\n" - - return content - else: - return response.error_message or "No types found in assembly" - - except Exception as e: - logger.error(f"Error listing types: {e}") - return f"Error: {str(e)}" - -@mcp.tool() -async def generate_diagrammer( - assembly_path: str, - output_dir: str = None, - include_pattern: str = None, - exclude_pattern: str = None, - ctx: Context = None -) -> str: - """Generate an interactive HTML diagrammer for visualizing assembly structure - - Args: - assembly_path: Path to the .NET assembly file (.dll or .exe) - output_dir: Output directory for the diagrammer (optional) - include_pattern: Regex pattern for types to include (optional) - exclude_pattern: Regex pattern for types to exclude (optional) - """ - if ctx: - await ctx.info(f"Generating assembly diagram: {assembly_path}") - - try: - wrapper = get_wrapper() - - from .models import GenerateDiagrammerRequest - request = GenerateDiagrammerRequest( - assembly_path=assembly_path, - output_dir=output_dir, - include_pattern=include_pattern, - exclude_pattern=exclude_pattern - ) - - response = await wrapper.generate_diagrammer(request) - - if response["success"]: - return f"HTML diagram generated successfully!\nOutput directory: {response['output_directory']}\nOpen the HTML file in a web browser to view the interactive diagram." - else: - return f"Failed to generate diagram: {response['error_message']}" - - except Exception as e: - logger.error(f"Error generating diagram: {e}") - return f"Error: {str(e)}" - -@mcp.tool() -async def get_assembly_info( - assembly_path: str, - ctx: Context = None -) -> str: - """Get basic information about a .NET assembly - - Args: - assembly_path: Path to the .NET assembly file (.dll or .exe) - """ - if ctx: - await ctx.info(f"Getting assembly info: {assembly_path}") - - try: - wrapper = get_wrapper() - - from .models import AssemblyInfoRequest - request = AssemblyInfoRequest(assembly_path=assembly_path) - - info = await wrapper.get_assembly_info(request) - - content = f"# Assembly Information\n\n" - content += f"- **Name**: {info.name}\n" - content += f"- **Full Name**: {info.full_name}\n" - content += f"- **Location**: {info.location}\n" - content += f"- **Version**: {info.version}\n" - if info.target_framework: - content += f"- **Target Framework**: {info.target_framework}\n" - if info.runtime_version: - content += f"- **Runtime Version**: {info.runtime_version}\n" - content += f"- **Is Signed**: {info.is_signed}\n" - content += f"- **Has Debug Info**: {info.has_debug_info}\n" - - return content - - except Exception as e: - logger.error(f"Error getting assembly info: {e}") - return f"Error: {str(e)}" - -# FastMCP automatically handles prompts -@mcp.prompt() -def analyze_assembly_prompt(assembly_path: str, focus_area: str = "types") -> str: - """Prompt template for analyzing .NET assemblies""" - return f"""I need to analyze the .NET assembly at "{assembly_path}". - -Please help me understand: -1. The overall structure and organization of the assembly -2. Key types and their relationships -3. Main namespaces and their purposes -4. Any notable patterns or architectural decisions - -Focus area: {focus_area} - -Start by listing the types in the assembly, then provide insights based on what you find.""" - -@mcp.prompt() -def decompile_and_explain_prompt(assembly_path: str, type_name: str) -> str: - """Prompt template for decompiling and explaining specific types""" - return f"""I want to understand the type "{type_name}" from the assembly "{assembly_path}". - -Please: -1. Decompile this specific type -2. Explain what this type does and its purpose -3. Highlight any interesting patterns, design decisions, or potential issues -4. Suggest how this type fits into the overall architecture - -Type to analyze: {type_name} -Assembly: {assembly_path}""" - -if __name__ == "__main__": - # FastMCP automatically handles running - mcp.run() \ No newline at end of file diff --git a/src/ilspy_mcp_server/__init__.py b/src/mcilspy/__init__.py similarity index 78% rename from src/ilspy_mcp_server/__init__.py rename to src/mcilspy/__init__.py index 97118fe..985683b 100644 --- a/src/ilspy_mcp_server/__init__.py +++ b/src/mcilspy/__init__.py @@ -1,3 +1,3 @@ """ILSpy MCP Server - Model Context Protocol server for .NET decompilation.""" -__version__ = "0.1.0" \ No newline at end of file +__version__ = "0.1.1" diff --git a/src/mcilspy/__main__.py b/src/mcilspy/__main__.py new file mode 100644 index 0000000..90f1517 --- /dev/null +++ b/src/mcilspy/__main__.py @@ -0,0 +1,6 @@ +"""Allow running as python -m mcilspy.""" + +from mcilspy.server import main + +if __name__ == "__main__": + main() diff --git a/src/ilspy_mcp_server/ilspy_wrapper.py b/src/mcilspy/ilspy_wrapper.py similarity index 61% rename from src/ilspy_mcp_server/ilspy_wrapper.py rename to src/mcilspy/ilspy_wrapper.py index 931d790..e407176 100644 --- a/src/ilspy_mcp_server/ilspy_wrapper.py +++ b/src/mcilspy/ilspy_wrapper.py @@ -1,20 +1,23 @@ """Wrapper for ICSharpCode.ILSpyCmd command line tool.""" import asyncio -import json +import logging import os +import re import shutil import tempfile from pathlib import Path -from typing import List, Optional, Tuple, Dict, Any -import re -import logging +from typing import Any from .models import ( - DecompileRequest, DecompileResponse, - ListTypesRequest, ListTypesResponse, TypeInfo, - GenerateDiagrammerRequest, AssemblyInfoRequest, AssemblyInfo, - EntityType + AssemblyInfo, + AssemblyInfoRequest, + DecompileRequest, + DecompileResponse, + GenerateDiagrammerRequest, + ListTypesRequest, + ListTypesResponse, + TypeInfo, ) logger = logging.getLogger(__name__) @@ -22,18 +25,20 @@ logger = logging.getLogger(__name__) class ILSpyWrapper: """Wrapper class for ILSpy command line tool.""" - - def __init__(self, ilspycmd_path: Optional[str] = None): + + def __init__(self, ilspycmd_path: str | None = None): """Initialize the wrapper. - + Args: ilspycmd_path: Path to ilspycmd executable. If None, will try to find it in PATH. """ self.ilspycmd_path = ilspycmd_path or self._find_ilspycmd() if not self.ilspycmd_path: - raise RuntimeError("ILSpyCmd not found. Please install it with: dotnet tool install --global ilspycmd") - - def _find_ilspycmd(self) -> Optional[str]: + raise RuntimeError( + "ILSpyCmd not found. Please install it with: dotnet tool install --global ilspycmd" + ) + + def _find_ilspycmd(self) -> str | None: """Find ilspycmd executable in PATH.""" # Try common names for cmd_name in ["ilspycmd", "ilspycmd.exe"]: @@ -41,46 +46,48 @@ class ILSpyWrapper: if path: return path return None - - async def _run_command(self, args: List[str], input_data: Optional[str] = None) -> Tuple[int, str, str]: + + async def _run_command( + self, args: list[str], input_data: str | None = None + ) -> tuple[int, str, str]: """Run ilspycmd with given arguments. - + Args: args: Command line arguments input_data: Optional input data to pass to stdin - + Returns: Tuple of (return_code, stdout, stderr) """ cmd = [self.ilspycmd_path] + args logger.debug(f"Running command: {' '.join(cmd)}") - + try: process = await asyncio.create_subprocess_exec( *cmd, stdin=asyncio.subprocess.PIPE if input_data else None, stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE + stderr=asyncio.subprocess.PIPE, ) - - input_bytes = input_data.encode('utf-8') if input_data else None + + input_bytes = input_data.encode("utf-8") if input_data else None stdout_bytes, stderr_bytes = await process.communicate(input=input_bytes) - - stdout = stdout_bytes.decode('utf-8', errors='replace') if stdout_bytes else "" - stderr = stderr_bytes.decode('utf-8', errors='replace') if stderr_bytes else "" - + + stdout = stdout_bytes.decode("utf-8", errors="replace") if stdout_bytes else "" + stderr = stderr_bytes.decode("utf-8", errors="replace") if stderr_bytes else "" + return process.returncode, stdout, stderr - + except Exception as e: logger.error(f"Error running command: {e}") return -1, "", str(e) - + async def decompile(self, request: DecompileRequest) -> DecompileResponse: """Decompile a .NET assembly. - + Args: request: Decompilation request - + Returns: Decompilation response """ @@ -88,60 +95,67 @@ class ILSpyWrapper: return DecompileResponse( success=False, error_message=f"Assembly file not found: {request.assembly_path}", - assembly_name=os.path.basename(request.assembly_path) + assembly_name=os.path.basename(request.assembly_path), ) - + args = [request.assembly_path] - + # Add language version args.extend(["-lv", request.language_version.value]) - + # Add type filter if specified if request.type_name: args.extend(["-t", request.type_name]) - + # Add output directory if specified temp_dir = None output_dir = request.output_dir if not output_dir: temp_dir = tempfile.mkdtemp() output_dir = temp_dir - + args.extend(["-o", output_dir]) - + # Add project creation flag if request.create_project: args.append("-p") - + # Add IL code flag if request.show_il_code: args.append("-il") - + # Add reference paths for ref_path in request.reference_paths: args.extend(["-r", ref_path]) - - # Add optimization flag + + # Add optimization flags if request.remove_dead_code: args.append("--no-dead-code") - + + if request.remove_dead_stores: + args.append("--no-dead-stores") + + # Add IL sequence points flag + if request.show_il_sequence_points: + args.append("--il-sequence-points") + # Add directory structure flag if request.nested_directories: args.append("--nested-directories") - + # Disable update check for automation args.append("--disable-updatecheck") - + try: return_code, stdout, stderr = await self._run_command(args) - + assembly_name = os.path.splitext(os.path.basename(request.assembly_path))[0] - + if return_code == 0: # If no output directory was specified, return stdout as source code source_code = None output_path = None - + if request.output_dir is None: source_code = stdout else: @@ -149,23 +163,25 @@ class ILSpyWrapper: # Try to read the main generated file if it exists if request.type_name: # Single type decompilation - type_file = os.path.join(output_dir, f"{request.type_name.split('.')[-1]}.cs") + type_file = os.path.join( + output_dir, f"{request.type_name.split('.')[-1]}.cs" + ) if os.path.exists(type_file): - with open(type_file, 'r', encoding='utf-8') as f: + with open(type_file, encoding="utf-8") as f: source_code = f.read() elif not request.create_project: # Single file decompilation cs_file = os.path.join(output_dir, f"{assembly_name}.cs") if os.path.exists(cs_file): - with open(cs_file, 'r', encoding='utf-8') as f: + with open(cs_file, encoding="utf-8") as f: source_code = f.read() - + return DecompileResponse( success=True, source_code=source_code, output_path=output_path, assembly_name=assembly_name, - type_name=request.type_name + type_name=request.type_name, ) else: error_msg = stderr or stdout or "Unknown error occurred" @@ -173,205 +189,260 @@ class ILSpyWrapper: success=False, error_message=error_msg, assembly_name=assembly_name, - type_name=request.type_name + type_name=request.type_name, ) - + except Exception as e: return DecompileResponse( success=False, error_message=str(e), assembly_name=os.path.basename(request.assembly_path), - type_name=request.type_name + type_name=request.type_name, ) finally: # Clean up temporary directory if temp_dir and os.path.exists(temp_dir): shutil.rmtree(temp_dir, ignore_errors=True) - + async def list_types(self, request: ListTypesRequest) -> ListTypesResponse: """List types in a .NET assembly. - + Args: request: List types request - + Returns: List types response """ if not os.path.exists(request.assembly_path): return ListTypesResponse( - success=False, - error_message=f"Assembly file not found: {request.assembly_path}" + success=False, error_message=f"Assembly file not found: {request.assembly_path}" ) - + args = [request.assembly_path] - + # Add entity types to list entity_types_str = "".join([et.value for et in request.entity_types]) args.extend(["-l", entity_types_str]) - + # Add reference paths for ref_path in request.reference_paths: args.extend(["-r", ref_path]) - + # Disable update check args.append("--disable-updatecheck") - + try: return_code, stdout, stderr = await self._run_command(args) - + if return_code == 0: types = self._parse_types_output(stdout) - return ListTypesResponse( - success=True, - types=types, - total_count=len(types) - ) + return ListTypesResponse(success=True, types=types, total_count=len(types)) else: error_msg = stderr or stdout or "Unknown error occurred" - return ListTypesResponse( - success=False, - error_message=error_msg - ) - + return ListTypesResponse(success=False, error_message=error_msg) + except Exception as e: - return ListTypesResponse( - success=False, - error_message=str(e) - ) - - def _parse_types_output(self, output: str) -> List[TypeInfo]: + return ListTypesResponse(success=False, error_message=str(e)) + + def _parse_types_output(self, output: str) -> list[TypeInfo]: """Parse the output from list types command. - + Args: output: Raw output from ilspycmd - + Returns: List of TypeInfo objects """ types = [] - lines = output.strip().split('\n') - + lines = output.strip().split("\n") + for line in lines: line = line.strip() if not line: continue - + # Parse the line format: "TypeKind: FullTypeName" - match = re.match(r'^(\w+):\s*(.+)$', line) + match = re.match(r"^(\w+):\s*(.+)$", line) if match: kind = match.group(1) full_name = match.group(2) - + # Extract namespace and name - parts = full_name.split('.') + parts = full_name.split(".") if len(parts) > 1: - namespace = '.'.join(parts[:-1]) + namespace = ".".join(parts[:-1]) name = parts[-1] else: namespace = None name = full_name - - types.append(TypeInfo( - name=name, - full_name=full_name, - kind=kind, - namespace=namespace - )) - + + types.append( + TypeInfo(name=name, full_name=full_name, kind=kind, namespace=namespace) + ) + return types - - async def generate_diagrammer(self, request: GenerateDiagrammerRequest) -> Dict[str, Any]: + + async def generate_diagrammer(self, request: GenerateDiagrammerRequest) -> dict[str, Any]: """Generate HTML diagrammer for an assembly. - + Args: request: Generate diagrammer request - + Returns: Dictionary with success status and details """ if not os.path.exists(request.assembly_path): return { "success": False, - "error_message": f"Assembly file not found: {request.assembly_path}" + "error_message": f"Assembly file not found: {request.assembly_path}", } - + args = [request.assembly_path, "--generate-diagrammer"] - + # Add output directory output_dir = request.output_dir if not output_dir: # Generate next to assembly assembly_dir = os.path.dirname(request.assembly_path) output_dir = os.path.join(assembly_dir, "diagrammer") - + args.extend(["-o", output_dir]) - + # Add include/exclude patterns if request.include_pattern: args.extend(["--generate-diagrammer-include", request.include_pattern]) if request.exclude_pattern: args.extend(["--generate-diagrammer-exclude", request.exclude_pattern]) - + # Add documentation file if request.docs_path: args.extend(["--generate-diagrammer-docs", request.docs_path]) - + # Add namespace stripping if request.strip_namespaces: args.extend(["--generate-diagrammer-strip-namespaces"] + request.strip_namespaces) - + # Add report excluded flag if request.report_excluded: args.append("--generate-diagrammer-report-excluded") - + # Disable update check args.append("--disable-updatecheck") - + try: return_code, stdout, stderr = await self._run_command(args) - + if return_code == 0: return { "success": True, "output_directory": output_dir, - "message": "HTML diagrammer generated successfully" + "message": "HTML diagrammer generated successfully", } else: error_msg = stderr or stdout or "Unknown error occurred" - return { - "success": False, - "error_message": error_msg - } - + return {"success": False, "error_message": error_msg} + except Exception as e: - return { - "success": False, - "error_message": str(e) - } - + return {"success": False, "error_message": str(e)} + async def get_assembly_info(self, request: AssemblyInfoRequest) -> AssemblyInfo: - """Get basic information about an assembly. - + """Get detailed information about an assembly by decompiling assembly attributes. + Args: request: Assembly info request - + Returns: - Assembly information + Assembly information including version, target framework, etc. """ if not os.path.exists(request.assembly_path): raise FileNotFoundError(f"Assembly file not found: {request.assembly_path}") - - # For now, we'll extract basic info from the file path - # In a more complete implementation, we could use reflection or metadata reading + assembly_path = Path(request.assembly_path) - + + # Use ilspycmd to list types and extract assembly info from output + args = [request.assembly_path, "-l", "c", "--disable-updatecheck"] + return_code, stdout, stderr = await self._run_command(args) + + # Initialize with defaults + name = assembly_path.stem + version = "Unknown" + full_name = assembly_path.name + target_framework = None + runtime_version = None + is_signed = False + + # Try to extract more info by decompiling assembly attributes + # Decompile with minimal output to get assembly-level attributes + temp_dir = tempfile.mkdtemp() + try: + args = [ + request.assembly_path, + "-o", + temp_dir, + "-lv", + "Latest", + "--disable-updatecheck", + ] + return_code, stdout, stderr = await self._run_command(args) + + if return_code == 0: + # Look for AssemblyInfo or assembly attributes in output + # Parse common patterns from decompiled code + output_text = stdout + + # Try to read main decompiled file for assembly attributes + main_file = os.path.join(temp_dir, f"{name}.cs") + if os.path.exists(main_file): + with open(main_file, encoding="utf-8") as f: + output_text = f.read() + + # Extract version from AssemblyVersion attribute + version_match = re.search( + r'\[assembly:\s*AssemblyVersion\s*\(\s*"([^"]+)"\s*\)\s*\]', output_text + ) + if version_match: + version = version_match.group(1) + + # Extract file version as fallback + if version == "Unknown": + file_version_match = re.search( + r'\[assembly:\s*AssemblyFileVersion\s*\(\s*"([^"]+)"\s*\)\s*\]', output_text + ) + if file_version_match: + version = file_version_match.group(1) + + # Extract target framework + framework_match = re.search( + r'\[assembly:\s*TargetFramework\s*\(\s*"([^"]+)"', output_text + ) + if framework_match: + target_framework = framework_match.group(1) + + # Check for signing + is_signed = ( + "[assembly: AssemblyKeyFile" in output_text + or "[assembly: AssemblyDelaySign" in output_text + or "PublicKeyToken=" in output_text + ) + + # Extract full name from assembly title or product + title_match = re.search( + r'\[assembly:\s*AssemblyTitle\s*\(\s*"([^"]+)"\s*\)\s*\]', output_text + ) + if title_match: + full_name = f"{title_match.group(1)}, Version={version}" + + finally: + # Clean up temp directory + shutil.rmtree(temp_dir, ignore_errors=True) + return AssemblyInfo( - name=assembly_path.stem, - version="Unknown", - full_name=assembly_path.name, + name=name, + version=version, + full_name=full_name, location=str(assembly_path.absolute()), - target_framework=None, - runtime_version=None, - is_signed=False, - has_debug_info=os.path.exists(assembly_path.with_suffix('.pdb')) - ) \ No newline at end of file + target_framework=target_framework, + runtime_version=runtime_version, + is_signed=is_signed, + has_debug_info=os.path.exists(assembly_path.with_suffix(".pdb")), + ) diff --git a/src/mcilspy/metadata_reader.py b/src/mcilspy/metadata_reader.py new file mode 100644 index 0000000..f2e7ebe --- /dev/null +++ b/src/mcilspy/metadata_reader.py @@ -0,0 +1,596 @@ +"""Direct .NET metadata reader using dnfile. + +Provides access to all 34+ CLR metadata tables without requiring ilspycmd. +This enables searching for methods, fields, properties, events, and resources +that are not exposed via the ilspycmd CLI. + +Note: dnfile provides flag attributes as boolean properties (e.g., mdPublic, fdStatic) +rather than traditional IntFlag enums, so we use those directly. +""" + +import logging +from dataclasses import dataclass, field +from pathlib import Path + +import dnfile +from dnfile.mdtable import TypeDefRow + +logger = logging.getLogger(__name__) + + +@dataclass +class MethodInfo: + """Information about a method in an assembly.""" + + name: str + full_name: str + declaring_type: str + namespace: str | None + return_type: str | None = None + is_public: bool = False + is_static: bool = False + is_virtual: bool = False + is_abstract: bool = False + parameters: list[str] = field(default_factory=list) + + +@dataclass +class FieldInfo: + """Information about a field in an assembly.""" + + name: str + full_name: str + declaring_type: str + namespace: str | None + field_type: str | None = None + is_public: bool = False + is_static: bool = False + is_literal: bool = False # Constant + default_value: str | None = None + + +@dataclass +class PropertyInfo: + """Information about a property in an assembly.""" + + name: str + full_name: str + declaring_type: str + namespace: str | None + property_type: str | None = None + has_getter: bool = False + has_setter: bool = False + + +@dataclass +class EventInfo: + """Information about an event in an assembly.""" + + name: str + full_name: str + declaring_type: str + namespace: str | None + event_type: str | None = None + + +@dataclass +class ResourceInfo: + """Information about an embedded resource.""" + + name: str + size: int + is_public: bool = True + + +@dataclass +class AssemblyMetadata: + """Complete assembly metadata from dnfile.""" + + name: str + version: str + culture: str | None = None + public_key_token: str | None = None + target_framework: str | None = None + type_count: int = 0 + method_count: int = 0 + field_count: int = 0 + property_count: int = 0 + event_count: int = 0 + resource_count: int = 0 + referenced_assemblies: list[str] = field(default_factory=list) + + +class MetadataReader: + """Read .NET assembly metadata directly using dnfile.""" + + def __init__(self, assembly_path: str): + """Initialize the metadata reader. + + Args: + assembly_path: Path to the .NET assembly file + """ + self.assembly_path = Path(assembly_path) + if not self.assembly_path.exists(): + raise FileNotFoundError(f"Assembly not found: {assembly_path}") + + self._pe: dnfile.dnPE | None = None + self._type_cache: dict[int, TypeDefRow] = {} + + def _ensure_loaded(self) -> dnfile.dnPE: + """Ensure the PE file is loaded.""" + if self._pe is None: + try: + self._pe = dnfile.dnPE(str(self.assembly_path)) + except Exception as e: + raise ValueError(f"Failed to parse assembly: {e}") from e + + # Build type cache for lookups + if self._pe.net and self._pe.net.mdtables and self._pe.net.mdtables.TypeDef: + for i, td in enumerate(self._pe.net.mdtables.TypeDef): + self._type_cache[i + 1] = td # Metadata tokens are 1-indexed + + return self._pe + + def get_assembly_metadata(self) -> AssemblyMetadata: + """Get comprehensive assembly metadata.""" + pe = self._ensure_loaded() + + name = self.assembly_path.stem + version = "0.0.0.0" + culture = None + public_key_token = None + target_framework = None + referenced_assemblies = [] + + if pe.net and pe.net.mdtables: + # Assembly info + if pe.net.mdtables.Assembly: + for asm in pe.net.mdtables.Assembly: + name = str(asm.Name) if asm.Name else name + version = f"{asm.MajorVersion}.{asm.MinorVersion}.{asm.BuildNumber}.{asm.RevisionNumber}" + culture = str(asm.Culture) if asm.Culture else None + if asm.PublicKey: + # Convert to token format - handle various dnfile representations + try: + pk = asm.PublicKey + if hasattr(pk, "value"): + pk_bytes = bytes(pk.value) + elif hasattr(pk, "__bytes__") or isinstance(pk, (bytes, bytearray)): + pk_bytes = bytes(pk) + else: + pk_bytes = b"" + if pk_bytes: + public_key_token = ( + pk_bytes[-8:].hex() if len(pk_bytes) >= 8 else pk_bytes.hex() + ) + except (TypeError, AttributeError): + # Some dnfile versions can't convert HeapItemBinary directly + pass + + # Assembly references + if pe.net.mdtables.AssemblyRef: + for ref in pe.net.mdtables.AssemblyRef: + ref_name = str(ref.Name) if ref.Name else "Unknown" + ref_version = f"{ref.MajorVersion}.{ref.MinorVersion}.{ref.BuildNumber}.{ref.RevisionNumber}" + referenced_assemblies.append(f"{ref_name}, Version={ref_version}") + + # Try to find TargetFramework from custom attributes + if pe.net.mdtables.CustomAttribute: + for ca in pe.net.mdtables.CustomAttribute: + # Look for TargetFrameworkAttribute + try: + if hasattr(ca, "Type") and ca.Type: + type_name = str(ca.Type) if ca.Type else "" + if "TargetFramework" in type_name and hasattr(ca, "Value") and ca.Value: + target_framework = str(ca.Value) + except Exception: + pass + + type_count = ( + len(pe.net.mdtables.TypeDef) + if pe.net and pe.net.mdtables and pe.net.mdtables.TypeDef + else 0 + ) + method_count = ( + len(pe.net.mdtables.MethodDef) + if pe.net and pe.net.mdtables and pe.net.mdtables.MethodDef + else 0 + ) + field_count = ( + len(pe.net.mdtables.Field) + if pe.net and pe.net.mdtables and pe.net.mdtables.Field + else 0 + ) + property_count = ( + len(pe.net.mdtables.Property) + if pe.net and pe.net.mdtables and pe.net.mdtables.Property + else 0 + ) + event_count = ( + len(pe.net.mdtables.Event) + if pe.net and pe.net.mdtables and pe.net.mdtables.Event + else 0 + ) + resource_count = ( + len(pe.net.mdtables.ManifestResource) + if pe.net and pe.net.mdtables and pe.net.mdtables.ManifestResource + else 0 + ) + + return AssemblyMetadata( + name=name, + version=version, + culture=culture, + public_key_token=public_key_token, + target_framework=target_framework, + type_count=type_count, + method_count=method_count, + field_count=field_count, + property_count=property_count, + event_count=event_count, + resource_count=resource_count, + referenced_assemblies=referenced_assemblies, + ) + + def _get_row_index(self, reference) -> int: + """Safely extract row_index from a metadata reference. + + dnfile references can be either objects with .row_index attribute + or raw integers. This helper handles both cases. + """ + if reference is None: + return 0 + if hasattr(reference, "row_index"): + return reference.row_index + if isinstance(reference, int): + return reference + # Some dnfile versions return the index directly as an attribute + if hasattr(reference, "value"): + return reference.value + return 0 + + def list_methods( + self, + type_filter: str | None = None, + namespace_filter: str | None = None, + public_only: bool = False, + ) -> list[MethodInfo]: + """List all methods in the assembly. + + Args: + type_filter: Only return methods from types containing this string + namespace_filter: Only return methods from types in namespaces containing this string + public_only: Only return public methods + """ + pe = self._ensure_loaded() + methods = [] + + if not ( + pe.net and pe.net.mdtables and pe.net.mdtables.TypeDef and pe.net.mdtables.MethodDef + ): + return methods + + # Build method-to-type mapping + # TypeDef.MethodList points to the first method of each type + type_method_ranges: list[tuple[TypeDefRow, int, int]] = [] + type_defs = list(pe.net.mdtables.TypeDef) + method_count = len(pe.net.mdtables.MethodDef) + + for i, td in enumerate(type_defs): + start_idx = self._get_row_index(td.MethodList) + if i + 1 < len(type_defs): + next_method_list = type_defs[i + 1].MethodList + end_idx = self._get_row_index(next_method_list) or (method_count + 1) + else: + end_idx = method_count + 1 + type_method_ranges.append((td, start_idx, end_idx)) + + # Iterate through methods + for md_idx, md in enumerate(pe.net.mdtables.MethodDef, start=1): + # Find declaring type + declaring_type = None + namespace = None + for td, start, end in type_method_ranges: + if start <= md_idx < end: + declaring_type = str(td.TypeName) if td.TypeName else "Unknown" + namespace = str(td.TypeNamespace) if td.TypeNamespace else None + break + + if declaring_type is None: + declaring_type = "Unknown" + + # Apply filters + if type_filter and type_filter.lower() not in declaring_type.lower(): + continue + if namespace_filter and ( + namespace is None or namespace_filter.lower() not in namespace.lower() + ): + continue + + # Parse attributes using dnfile's boolean properties on ClrMethodAttr + flags = md.Flags if hasattr(md, "Flags") else None + is_public = flags.mdPublic if flags and hasattr(flags, "mdPublic") else False + is_static = flags.mdStatic if flags and hasattr(flags, "mdStatic") else False + is_virtual = flags.mdVirtual if flags and hasattr(flags, "mdVirtual") else False + is_abstract = flags.mdAbstract if flags and hasattr(flags, "mdAbstract") else False + + if public_only and not is_public: + continue + + method_name = str(md.Name) if md.Name else "Unknown" + full_name = ( + f"{namespace}.{declaring_type}.{method_name}" + if namespace + else f"{declaring_type}.{method_name}" + ) + + methods.append( + MethodInfo( + name=method_name, + full_name=full_name, + declaring_type=declaring_type, + namespace=namespace, + is_public=is_public, + is_static=is_static, + is_virtual=is_virtual, + is_abstract=is_abstract, + ) + ) + + return methods + + def list_fields( + self, + type_filter: str | None = None, + namespace_filter: str | None = None, + public_only: bool = False, + constants_only: bool = False, + ) -> list[FieldInfo]: + """List all fields in the assembly. + + Args: + type_filter: Only return fields from types containing this string + namespace_filter: Only return fields from types in namespaces containing this string + public_only: Only return public fields + constants_only: Only return constant (literal) fields + """ + pe = self._ensure_loaded() + fields = [] + + if not (pe.net and pe.net.mdtables and pe.net.mdtables.TypeDef and pe.net.mdtables.Field): + return fields + + # Build field-to-type mapping + type_defs = list(pe.net.mdtables.TypeDef) + field_count = len(pe.net.mdtables.Field) + + type_field_ranges: list[tuple[TypeDefRow, int, int]] = [] + for i, td in enumerate(type_defs): + start_idx = self._get_row_index(td.FieldList) + if i + 1 < len(type_defs): + next_field_list = type_defs[i + 1].FieldList + end_idx = self._get_row_index(next_field_list) or (field_count + 1) + else: + end_idx = field_count + 1 + type_field_ranges.append((td, start_idx, end_idx)) + + # Iterate through fields + for f_idx, fld in enumerate(pe.net.mdtables.Field, start=1): + # Find declaring type + declaring_type = None + namespace = None + for td, start, end in type_field_ranges: + if start <= f_idx < end: + declaring_type = str(td.TypeName) if td.TypeName else "Unknown" + namespace = str(td.TypeNamespace) if td.TypeNamespace else None + break + + if declaring_type is None: + declaring_type = "Unknown" + + # Apply filters + if type_filter and type_filter.lower() not in declaring_type.lower(): + continue + if namespace_filter and ( + namespace is None or namespace_filter.lower() not in namespace.lower() + ): + continue + + # Parse attributes using dnfile's boolean properties on ClrFieldAttr + flags = fld.Flags if hasattr(fld, "Flags") else None + is_public = flags.fdPublic if flags and hasattr(flags, "fdPublic") else False + is_static = flags.fdStatic if flags and hasattr(flags, "fdStatic") else False + is_literal = flags.fdLiteral if flags and hasattr(flags, "fdLiteral") else False + + if public_only and not is_public: + continue + if constants_only and not is_literal: + continue + + field_name = str(fld.Name) if fld.Name else "Unknown" + full_name = ( + f"{namespace}.{declaring_type}.{field_name}" + if namespace + else f"{declaring_type}.{field_name}" + ) + + fields.append( + FieldInfo( + name=field_name, + full_name=full_name, + declaring_type=declaring_type, + namespace=namespace, + is_public=is_public, + is_static=is_static, + is_literal=is_literal, + ) + ) + + return fields + + def list_properties( + self, + type_filter: str | None = None, + namespace_filter: str | None = None, + ) -> list[PropertyInfo]: + """List all properties in the assembly.""" + pe = self._ensure_loaded() + properties = [] + + if not (pe.net and pe.net.mdtables and pe.net.mdtables.Property): + return properties + + # PropertyMap links types to properties + property_type_map: dict[int, tuple[str, str | None]] = {} + if pe.net.mdtables.PropertyMap and pe.net.mdtables.TypeDef: + prop_maps = list(pe.net.mdtables.PropertyMap) + type_defs = list(pe.net.mdtables.TypeDef) + for i, pm in enumerate(prop_maps): + if pm.Parent and pm.PropertyList: + parent_idx = self._get_row_index(pm.Parent) + if parent_idx > 0 and parent_idx <= len(type_defs): + td = type_defs[parent_idx - 1] + type_name = str(td.TypeName) if td.TypeName else "Unknown" + ns = str(td.TypeNamespace) if td.TypeNamespace else None + + # Determine property range + start_idx = self._get_row_index(pm.PropertyList) + if i + 1 < len(prop_maps): + end_idx = self._get_row_index(prop_maps[i + 1].PropertyList) + else: + end_idx = len(pe.net.mdtables.Property) + 1 + + for p_idx in range(start_idx, end_idx): + property_type_map[p_idx] = (type_name, ns) + + # Iterate through properties + for p_idx, prop in enumerate(pe.net.mdtables.Property, start=1): + declaring_type, namespace = property_type_map.get(p_idx, ("Unknown", None)) + + # Apply filters + if type_filter and type_filter.lower() not in declaring_type.lower(): + continue + if namespace_filter and ( + namespace is None or namespace_filter.lower() not in namespace.lower() + ): + continue + + prop_name = str(prop.Name) if prop.Name else "Unknown" + full_name = ( + f"{namespace}.{declaring_type}.{prop_name}" + if namespace + else f"{declaring_type}.{prop_name}" + ) + + properties.append( + PropertyInfo( + name=prop_name, + full_name=full_name, + declaring_type=declaring_type, + namespace=namespace, + ) + ) + + return properties + + def list_events( + self, + type_filter: str | None = None, + namespace_filter: str | None = None, + ) -> list[EventInfo]: + """List all events in the assembly.""" + pe = self._ensure_loaded() + events = [] + + if not (pe.net and pe.net.mdtables and pe.net.mdtables.Event): + return events + + # EventMap links types to events + event_type_map: dict[int, tuple[str, str | None]] = {} + if pe.net.mdtables.EventMap and pe.net.mdtables.TypeDef: + event_maps = list(pe.net.mdtables.EventMap) + type_defs = list(pe.net.mdtables.TypeDef) + for i, em in enumerate(event_maps): + if em.Parent and em.EventList: + parent_idx = self._get_row_index(em.Parent) + if parent_idx > 0 and parent_idx <= len(type_defs): + td = type_defs[parent_idx - 1] + type_name = str(td.TypeName) if td.TypeName else "Unknown" + ns = str(td.TypeNamespace) if td.TypeNamespace else None + + start_idx = self._get_row_index(em.EventList) + if i + 1 < len(event_maps): + end_idx = self._get_row_index(event_maps[i + 1].EventList) + else: + end_idx = len(pe.net.mdtables.Event) + 1 + + for e_idx in range(start_idx, end_idx): + event_type_map[e_idx] = (type_name, ns) + + for e_idx, evt in enumerate(pe.net.mdtables.Event, start=1): + declaring_type, namespace = event_type_map.get(e_idx, ("Unknown", None)) + + if type_filter and type_filter.lower() not in declaring_type.lower(): + continue + if namespace_filter and ( + namespace is None or namespace_filter.lower() not in namespace.lower() + ): + continue + + evt_name = str(evt.Name) if evt.Name else "Unknown" + full_name = ( + f"{namespace}.{declaring_type}.{evt_name}" + if namespace + else f"{declaring_type}.{evt_name}" + ) + + events.append( + EventInfo( + name=evt_name, + full_name=full_name, + declaring_type=declaring_type, + namespace=namespace, + ) + ) + + return events + + def list_resources(self) -> list[ResourceInfo]: + """List all embedded resources in the assembly.""" + pe = self._ensure_loaded() + resources = [] + + if not (pe.net and pe.net.mdtables and pe.net.mdtables.ManifestResource): + return resources + + for res in pe.net.mdtables.ManifestResource: + name = str(res.Name) if res.Name else "Unknown" + # dnfile exposes flags as boolean properties (mrPublic, mrPrivate) + is_public = ( + res.Flags.mrPublic + if hasattr(res, "Flags") and hasattr(res.Flags, "mrPublic") + else True + ) + + resources.append( + ResourceInfo( + name=name, + size=0, # Size requires reading the actual resource data + is_public=is_public, + ) + ) + + return resources + + def close(self): + """Close the PE file.""" + if self._pe: + self._pe.close() + self._pe = None + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + return False diff --git a/src/mcilspy/models.py b/src/mcilspy/models.py new file mode 100644 index 0000000..9af3e32 --- /dev/null +++ b/src/mcilspy/models.py @@ -0,0 +1,163 @@ +from enum import Enum + +from pydantic import BaseModel, Field + + +class LanguageVersion(str, Enum): + """C# Language versions supported by ILSpy.""" + + CSHARP1 = "CSharp1" + CSHARP2 = "CSharp2" + CSHARP3 = "CSharp3" + CSHARP4 = "CSharp4" + CSHARP5 = "CSharp5" + CSHARP6 = "CSharp6" + CSHARP7 = "CSharp7" + CSHARP7_1 = "CSharp7_1" + CSHARP7_2 = "CSharp7_2" + CSHARP7_3 = "CSharp7_3" + CSHARP8_0 = "CSharp8_0" + CSHARP9_0 = "CSharp9_0" + CSHARP10_0 = "CSharp10_0" + CSHARP11_0 = "CSharp11_0" + CSHARP12_0 = "CSharp12_0" + PREVIEW = "Preview" + LATEST = "Latest" + + +class EntityType(str, Enum): + """Entity types that can be listed. + + Accepts both full names and single-letter codes: + - "class" or "c" for classes + - "interface" or "i" for interfaces + - "struct" or "s" for structs + - "delegate" or "d" for delegates + - "enum" or "e" for enums + """ + + CLASS = "c" + INTERFACE = "i" + STRUCT = "s" + DELEGATE = "d" + ENUM = "e" + + @classmethod + def from_string(cls, value: str) -> "EntityType": + """Convert a string (full name or single letter) to EntityType. + + Args: + value: Either a single letter (c, i, s, d, e) or full name (class, interface, etc.) + + Returns: + The corresponding EntityType enum value + + Raises: + ValueError: If the value doesn't match any known entity type + """ + value_lower = value.lower().strip() + + # Map full names to single letters + name_map = { + "class": "c", + "interface": "i", + "struct": "s", + "delegate": "d", + "enum": "e", + } + + # Convert full name to letter if needed + if value_lower in name_map: + value_lower = name_map[value_lower] + + # Try to match against enum values + for member in cls: + if member.value == value_lower: + return member + + valid_options = list(name_map.keys()) + list(name_map.values()) + raise ValueError(f"Invalid entity type: '{value}'. Valid options: {valid_options}") + + +class DecompileRequest(BaseModel): + """Request to decompile a .NET assembly.""" + + assembly_path: str + output_dir: str | None = None + type_name: str | None = None + language_version: LanguageVersion = LanguageVersion.LATEST + create_project: bool = False + show_il_code: bool = False + reference_paths: list[str] = Field(default_factory=list) + remove_dead_code: bool = False + remove_dead_stores: bool = False + show_il_sequence_points: bool = False + nested_directories: bool = False + + +class ListTypesRequest(BaseModel): + """Request to list types in an assembly.""" + + assembly_path: str + entity_types: list[EntityType] = Field(default_factory=lambda: [EntityType.CLASS]) + reference_paths: list[str] = Field(default_factory=list) + + +class TypeInfo(BaseModel): + """Information about a type in an assembly.""" + + name: str + full_name: str + kind: str + namespace: str | None = None + + +class DecompileResponse(BaseModel): + """Response from decompilation operation.""" + + success: bool + source_code: str | None = None + output_path: str | None = None + error_message: str | None = None + assembly_name: str + type_name: str | None = None + + +class ListTypesResponse(BaseModel): + """Response from list types operation.""" + + success: bool + types: list[TypeInfo] = Field(default_factory=list) + total_count: int = 0 + error_message: str | None = None + + +class GenerateDiagrammerRequest(BaseModel): + """Request to generate HTML diagrammer.""" + + assembly_path: str + output_dir: str | None = None + include_pattern: str | None = None + exclude_pattern: str | None = None + docs_path: str | None = None + strip_namespaces: list[str] = Field(default_factory=list) + report_excluded: bool = False + + +class AssemblyInfoRequest(BaseModel): + """Request to get assembly information.""" + + assembly_path: str + + +class AssemblyInfo(BaseModel): + """Information about an assembly.""" + + name: str + version: str + full_name: str + location: str + target_framework: str | None = None + runtime_version: str | None = None + is_signed: bool = False + has_debug_info: bool = False diff --git a/src/mcilspy/server.py b/src/mcilspy/server.py new file mode 100644 index 0000000..45017f5 --- /dev/null +++ b/src/mcilspy/server.py @@ -0,0 +1,1475 @@ +import asyncio +import logging +import os +import platform +import shutil + +from mcp.server.fastmcp import Context, FastMCP + +from .ilspy_wrapper import ILSpyWrapper +from .models import EntityType, LanguageVersion + +# Setup logging +log_level = os.getenv("LOGLEVEL", "INFO").upper() +logging.basicConfig( + level=getattr(logging, log_level, logging.INFO), + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", +) +logger = logging.getLogger(__name__) + +# Create FastMCP server +mcp = FastMCP("mcilspy") + +# Global ILSpy wrapper +ilspy_wrapper: ILSpyWrapper | None = None + + +def get_wrapper() -> ILSpyWrapper: + """Get ILSpy wrapper instance""" + global ilspy_wrapper + if ilspy_wrapper is None: + ilspy_wrapper = ILSpyWrapper() + return ilspy_wrapper + + +async def _check_dotnet_tools() -> dict: + """Check status of dotnet CLI and ilspycmd tool.""" + result = { + "dotnet_available": False, + "dotnet_version": None, + "ilspycmd_available": False, + "ilspycmd_version": None, + "ilspycmd_path": None, + } + + # Check if dotnet CLI is available + dotnet_path = shutil.which("dotnet") + if dotnet_path: + result["dotnet_available"] = True + try: + proc = await asyncio.create_subprocess_exec( + "dotnet", + "--version", + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + stdout, _ = await proc.communicate() + if proc.returncode == 0: + result["dotnet_version"] = stdout.decode().strip() + except Exception: + pass + + # Check if ilspycmd is available + ilspy_path = shutil.which("ilspycmd") + if ilspy_path: + result["ilspycmd_available"] = True + result["ilspycmd_path"] = ilspy_path + try: + proc = await asyncio.create_subprocess_exec( + "ilspycmd", + "--version", + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + stdout, _ = await proc.communicate() + if proc.returncode == 0: + result["ilspycmd_version"] = stdout.decode().strip() + except Exception: + pass + + return result + + +def _detect_platform() -> dict: + """Detect the platform and recommend the appropriate .NET SDK install command.""" + system = platform.system().lower() + result = { + "system": system, + "distro": None, + "package_manager": None, + "install_command": None, + "needs_sudo": True, + } + + if system == "linux": + # Try to detect the Linux distribution + try: + with open("/etc/os-release") as f: + os_release = f.read().lower() + if "arch" in os_release or "manjaro" in os_release or "endeavour" in os_release: + result["distro"] = "arch" + result["package_manager"] = "pacman" + result["install_command"] = "sudo pacman -S dotnet-sdk" + elif "ubuntu" in os_release or "debian" in os_release or "mint" in os_release: + result["distro"] = "debian" + result["package_manager"] = "apt" + result["install_command"] = "sudo apt update && sudo apt install -y dotnet-sdk-8.0" + elif "fedora" in os_release or "rhel" in os_release or "centos" in os_release: + result["distro"] = "fedora" + result["package_manager"] = "dnf" + result["install_command"] = "sudo dnf install -y dotnet-sdk-8.0" + elif "opensuse" in os_release or "suse" in os_release: + result["distro"] = "suse" + result["package_manager"] = "zypper" + result["install_command"] = "sudo zypper install -y dotnet-sdk-8.0" + except FileNotFoundError: + pass + + # Fallback: check for common package managers + if result["install_command"] is None: + if shutil.which("pacman"): + result["package_manager"] = "pacman" + result["install_command"] = "sudo pacman -S dotnet-sdk" + elif shutil.which("apt"): + result["package_manager"] = "apt" + result["install_command"] = "sudo apt update && sudo apt install -y dotnet-sdk-8.0" + elif shutil.which("dnf"): + result["package_manager"] = "dnf" + result["install_command"] = "sudo dnf install -y dotnet-sdk-8.0" + + elif system == "darwin": + result["distro"] = "macos" + if shutil.which("brew"): + result["package_manager"] = "homebrew" + result["install_command"] = "brew install dotnet-sdk" + result["needs_sudo"] = False + else: + result["install_command"] = ( + "Download from https://dotnet.microsoft.com/download or install Homebrew first" + ) + + elif system == "windows": + result["distro"] = "windows" + if shutil.which("winget"): + result["package_manager"] = "winget" + result["install_command"] = "winget install Microsoft.DotNet.SDK.8" + result["needs_sudo"] = False + elif shutil.which("choco"): + result["package_manager"] = "chocolatey" + result["install_command"] = "choco install dotnet-sdk -y" + else: + result["install_command"] = "Download from https://dotnet.microsoft.com/download" + result["needs_sudo"] = False + + # Final fallback + if result["install_command"] is None: + result["install_command"] = "Download from https://dotnet.microsoft.com/download" + result["needs_sudo"] = False + + return result + + +async def _try_install_dotnet_sdk(ctx: Context | None = None) -> tuple[bool, str]: + """Attempt to install the .NET SDK using the detected package manager. + + Returns (success, message) tuple. + """ + platform_info = _detect_platform() + cmd = platform_info.get("install_command") + + if not cmd or "Download from" in cmd: + return False, ( + f"Cannot auto-install .NET SDK on {platform_info['system']}.\n\n" + f"Please install manually: {cmd}" + ) + + if platform_info.get("needs_sudo") and os.geteuid() != 0: + # We need sudo but aren't root - try anyway, it might prompt or fail gracefully + pass + + if ctx: + await ctx.info(f"Installing .NET SDK via {platform_info['package_manager']}...") + + try: + # Use shell=True to handle complex commands with && and sudo + proc = await asyncio.create_subprocess_shell( + cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + stdout, stderr = await proc.communicate() + output = stdout.decode() + stderr.decode() + + if proc.returncode == 0: + return True, f"✅ .NET SDK installed successfully via {platform_info['package_manager']}!" + else: + return False, ( + f"❌ Installation failed (exit code {proc.returncode}).\n\n" + f"Command: `{cmd}`\n\n" + f"Output:\n```\n{output[-1000:]}\n```\n\n" + "Try running the command manually with sudo if needed." + ) + except Exception as e: + return False, f"❌ Failed to run install command: {e}\n\nTry manually: `{cmd}`" + + +@mcp.tool() +async def check_ilspy_installation( + ctx: Context | None = None, +) -> str: + """Check if ilspycmd is installed and show installation status. + + Returns information about: + - Whether dotnet CLI is available + - Whether ilspycmd is installed + - Version information for both tools + - Instructions for installing if missing + + Use this tool to diagnose issues with decompilation tools. + """ + if ctx: + await ctx.info("Checking ILSpy installation status...") + + status = await _check_dotnet_tools() + + content = "# ILSpy Installation Status\n\n" + + # dotnet status + if status["dotnet_available"]: + content += f"✅ **dotnet CLI**: Installed (v{status['dotnet_version']})\n" + else: + content += "❌ **dotnet CLI**: Not found\n" + content += " - Install from: https://dotnet.microsoft.com/download\n\n" + return content + + # ilspycmd status + if status["ilspycmd_available"]: + content += "✅ **ilspycmd**: Installed" + if status["ilspycmd_version"]: + content += f" (v{status['ilspycmd_version']})" + content += f"\n - Path: `{status['ilspycmd_path']}`\n" + content += "\n🎉 All ILSpy-based tools are ready to use!" + else: + content += "❌ **ilspycmd**: Not installed\n\n" + content += "To install, use the `install_ilspy` tool or run manually:\n" + content += "```bash\n" + content += "dotnet tool install --global ilspycmd\n" + content += "```\n" + content += ( + "\n**Note**: The direct metadata tools (`search_methods`, `search_fields`, etc.) " + ) + content += "work without ilspycmd." + + return content + + +@mcp.tool() +async def install_ilspy( + update: bool = False, + install_dotnet_sdk: bool = False, + ctx: Context | None = None, +) -> str: + """Install or update ilspycmd, the ILSpy command-line decompiler. + + This tool handles the complete installation process: + - Detects your platform and package manager + - Optionally installs the .NET SDK if missing (`install_dotnet_sdk=True`) + - Installs ilspycmd globally + - Updates to the latest version if `update=True` + + After installation, all ILSpy-based decompilation tools will be available. + + Args: + update: If True, update ilspycmd to the latest version even if already installed + install_dotnet_sdk: If True, attempt to install .NET SDK when missing (may require sudo) + """ + if ctx: + await ctx.info("Checking prerequisites...") + + status = await _check_dotnet_tools() + + if not status["dotnet_available"]: + platform_info = _detect_platform() + + if install_dotnet_sdk: + # Attempt to install the .NET SDK + if ctx: + await ctx.info("dotnet CLI not found. Attempting to install .NET SDK...") + + success, message = await _try_install_dotnet_sdk(ctx) + + if success: + # Re-check status after SDK installation + status = await _check_dotnet_tools() + if not status["dotnet_available"]: + return ( + f"{message}\n\n" + "However, dotnet is still not found in PATH.\n" + "You may need to restart your terminal or add dotnet to PATH." + ) + else: + return message + else: + # Provide platform-specific instructions + cmd = platform_info.get("install_command", "Download from https://dotnet.microsoft.com/download") + + return ( + "❌ **Error**: dotnet CLI is not installed.\n\n" + f"**Detected platform**: {platform_info['system']}" + + (f" ({platform_info['distro']})" if platform_info.get("distro") else "") + + "\n\n" + f"**Recommended installation**:\n```bash\n{cmd}\n```\n\n" + "**Or** call this tool with `install_dotnet_sdk=True` to attempt automatic installation:\n" + "```json\n" + '{"install_dotnet_sdk": true}\n' + "```\n\n" + "**Alternative**: Download manually from https://dotnet.microsoft.com/download" + ) + + # Check if already installed and not updating + if status["ilspycmd_available"] and not update: + version_info = f" (v{status['ilspycmd_version']})" if status["ilspycmd_version"] else "" + return ( + f"✅ **ilspycmd** is already installed{version_info}.\n\n" + f"Path: `{status['ilspycmd_path']}`\n\n" + "To update to the latest version, call this tool with `update=True`." + ) + + # Determine command + if status["ilspycmd_available"] and update: + cmd = ["dotnet", "tool", "update", "--global", "ilspycmd"] + action = "Updating" + else: + cmd = ["dotnet", "tool", "install", "--global", "ilspycmd"] + action = "Installing" + + if ctx: + await ctx.info(f"{action} ilspycmd...") + + try: + proc = await asyncio.create_subprocess_exec( + *cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + stdout, stderr = await proc.communicate() + + output = stdout.decode() + stderr.decode() + + if proc.returncode == 0: + # Verify installation + new_status = await _check_dotnet_tools() + if new_status["ilspycmd_available"]: + version_info = ( + f" v{new_status['ilspycmd_version']}" if new_status["ilspycmd_version"] else "" + ) + return ( + f"✅ **Success!** ilspycmd{version_info} has been {'updated' if update else 'installed'}.\n\n" + f"Path: `{new_status['ilspycmd_path']}`\n\n" + "All ILSpy-based decompilation tools are now available:\n" + "- `decompile_assembly`\n" + "- `list_types`\n" + "- `search_types`\n" + "- `search_strings`\n" + "- `generate_diagrammer`\n" + "- `get_assembly_info`" + ) + else: + return ( + f"⚠️ Installation appeared to succeed but ilspycmd is not found in PATH.\n\n" + f"Output:\n```\n{output}\n```\n\n" + "You may need to:\n" + "1. Add `~/.dotnet/tools` to your PATH\n" + "2. Restart your terminal/shell\n" + '3. For zsh: `export PATH="$PATH:$HOME/.dotnet/tools"`\n' + '4. For bash: `export PATH="$PATH:$HOME/.dotnet/tools"`' + ) + else: + return ( + f"❌ **Installation failed** (exit code {proc.returncode})\n\n" + f"Output:\n```\n{output}\n```\n\n" + "Try running manually:\n" + "```bash\n" + f"{' '.join(cmd)}\n" + "```" + ) + + except Exception as e: + logger.error(f"Error installing ilspycmd: {e}") + return f"❌ **Error**: {str(e)}" + + +@mcp.tool() +async def decompile_assembly( + assembly_path: str, + output_dir: str | None = None, + type_name: str | None = None, + language_version: str = "Latest", + create_project: bool = False, + show_il_code: bool = False, + remove_dead_code: bool = False, + remove_dead_stores: bool = False, + show_il_sequence_points: bool = False, + nested_directories: bool = False, + ctx: Context | None = None, +) -> str: + """Decompile a .NET assembly to readable C# source code. + + This is the primary tool for reverse-engineering .NET binaries. Use it to: + - Decompile entire assemblies to understand their structure + - Extract specific types by fully qualified name + - Generate compilable project structures for analysis + - View IL (Intermediate Language) code for low-level analysis + + WORKFLOW TIP: Start with `list_types` to discover available types, then use + this tool with `type_name` to decompile specific classes of interest. + + Args: + assembly_path: Full path to the .NET assembly file (.dll or .exe) + output_dir: Directory to save decompiled files. If omitted, returns source code directly + type_name: Fully qualified type name (e.g., "MyNamespace.MyClass") to decompile only that type + language_version: C# version for output syntax. Options: CSharp1-CSharp12_0, Preview, Latest (default) + create_project: Generate a full .csproj project structure with multiple files (useful for large assemblies) + show_il_code: Output IL bytecode instead of C# (useful for understanding low-level behavior) + remove_dead_code: Strip unreachable code paths from output + remove_dead_stores: Strip unused variable assignments from output + show_il_sequence_points: Include debugging sequence points in IL output (implies show_il_code) + nested_directories: Organize output files in namespace-based directory hierarchy + """ + if ctx: + await ctx.info(f"Starting decompilation of assembly: {assembly_path}") + + try: + wrapper = get_wrapper() + + # Use simplified request object (no complex pydantic validation needed) + from .models import DecompileRequest + + request = DecompileRequest( + assembly_path=assembly_path, + output_dir=output_dir, + type_name=type_name, + language_version=LanguageVersion(language_version), + create_project=create_project, + show_il_code=show_il_code or show_il_sequence_points, + remove_dead_code=remove_dead_code, + remove_dead_stores=remove_dead_stores, + show_il_sequence_points=show_il_sequence_points, + nested_directories=nested_directories, + ) + + response = await wrapper.decompile(request) + + if response.success: + if response.source_code: + content = f"# Decompilation result: {response.assembly_name}" + if response.type_name: + content += f" - {response.type_name}" + content += f"\n\n```csharp\n{response.source_code}\n```" + return content + else: + return f"Decompilation successful! Files saved to: {response.output_path}" + else: + return f"Decompilation failed: {response.error_message}" + + except Exception as e: + logger.error(f"Decompilation error: {e}") + return f"Error: {str(e)}" + + +@mcp.tool() +async def list_types( + assembly_path: str, + entity_types: list[str] | None = None, + ctx: Context | None = None, +) -> str: + """List all types (classes, interfaces, structs, etc.) in a .NET assembly. + + This is typically the FIRST tool to use when analyzing an unknown assembly. + It reveals the assembly's structure and helps identify types worth decompiling. + + Results are organized by namespace for easy navigation. Use this to discover: + - Main application classes and their namespaces + - Interface contracts and service definitions + - Data structures (structs) and enumerations + - Event delegates and callbacks + + WORKFLOW TIP: After listing types, use `decompile_assembly` with `type_name` + to examine specific classes that look interesting. + + Args: + assembly_path: Full path to the .NET assembly file (.dll or .exe) + entity_types: Types to include in listing. Accepts full names or single letters: + - "class" or "c" (default if omitted) + - "interface" or "i" + - "struct" or "s" + - "delegate" or "d" + - "enum" or "e" + Example: ["class", "interface"] or ["c", "i"] + """ + if ctx: + await ctx.info(f"Listing types in assembly: {assembly_path}") + + try: + wrapper = get_wrapper() + + # Default to list only classes + if entity_types is None: + entity_types = ["class"] + + # Convert to EntityType enums using the flexible from_string method + entity_type_enums = [] + for et in entity_types: + try: + entity_type_enums.append(EntityType.from_string(et)) + except ValueError as e: + logger.warning(f"Skipping invalid entity type: {e}") + continue + + from .models import ListTypesRequest + + request = ListTypesRequest(assembly_path=assembly_path, entity_types=entity_type_enums) + + response = await wrapper.list_types(request) + + if response.success and response.types: + content = f"# Types in {assembly_path}\n\n" + content += f"Found {response.total_count} types:\n\n" + + # Group by namespace + by_namespace = {} + for type_info in response.types: + ns = type_info.namespace or "(Global)" + if ns not in by_namespace: + by_namespace[ns] = [] + by_namespace[ns].append(type_info) + + for namespace, types in sorted(by_namespace.items()): + content += f"## {namespace}\n\n" + for type_info in sorted(types, key=lambda t: t.name): + content += f"- **{type_info.name}** ({type_info.kind})\n" + content += f" - Full name: `{type_info.full_name}`\n" + content += "\n" + + return content + else: + return response.error_message or "No types found in assembly" + + except Exception as e: + logger.error(f"Error listing types: {e}") + return f"Error: {str(e)}" + + +@mcp.tool() +async def generate_diagrammer( + assembly_path: str, + output_dir: str | None = None, + include_pattern: str | None = None, + exclude_pattern: str | None = None, + ctx: Context | None = None, +) -> str: + """Generate an interactive HTML diagram showing assembly type relationships. + + Creates a visual class diagram that can be opened in any web browser. + Useful for understanding complex assemblies with many interdependent types. + + The diagram shows: + - Type inheritance hierarchies + - Interface implementations + - Type relationships and dependencies + - Namespace organization + + Use regex patterns to focus on specific namespaces or exclude generated code. + + Args: + assembly_path: Full path to the .NET assembly file (.dll or .exe) + output_dir: Where to save the HTML file. Defaults to "diagrammer" folder next to assembly + include_pattern: Regex to whitelist types (e.g., "MyApp\\\\.Services\\\\..+" for Services namespace) + exclude_pattern: Regex to blacklist types (e.g., ".*Generated.*" to hide generated code) + """ + if ctx: + await ctx.info(f"Generating assembly diagram: {assembly_path}") + + try: + wrapper = get_wrapper() + + from .models import GenerateDiagrammerRequest + + request = GenerateDiagrammerRequest( + assembly_path=assembly_path, + output_dir=output_dir, + include_pattern=include_pattern, + exclude_pattern=exclude_pattern, + ) + + response = await wrapper.generate_diagrammer(request) + + if response["success"]: + return f"HTML diagram generated successfully!\nOutput directory: {response['output_directory']}\nOpen the HTML file in a web browser to view the interactive diagram." + else: + return f"Failed to generate diagram: {response['error_message']}" + + except Exception as e: + logger.error(f"Error generating diagram: {e}") + return f"Error: {str(e)}" + + +@mcp.tool() +async def get_assembly_info(assembly_path: str, ctx: Context | None = None) -> str: + """Get metadata and version information about a .NET assembly. + + Returns detailed assembly metadata useful for understanding: + - Assembly name and version (for compatibility checks) + - Target framework (.NET Framework, .NET Core, .NET 5+, etc.) + - Whether the assembly is signed (important for trust verification) + - Debug information availability (PDB files present) + + This is a quick reconnaissance tool - use it first to understand what + kind of assembly you're dealing with before deep-diving with other tools. + + Args: + assembly_path: Full path to the .NET assembly file (.dll or .exe) + """ + if ctx: + await ctx.info(f"Getting assembly info: {assembly_path}") + + try: + wrapper = get_wrapper() + + from .models import AssemblyInfoRequest + + request = AssemblyInfoRequest(assembly_path=assembly_path) + + info = await wrapper.get_assembly_info(request) + + content = "# Assembly Information\n\n" + content += f"- **Name**: {info.name}\n" + content += f"- **Full Name**: {info.full_name}\n" + content += f"- **Location**: {info.location}\n" + content += f"- **Version**: {info.version}\n" + if info.target_framework: + content += f"- **Target Framework**: {info.target_framework}\n" + if info.runtime_version: + content += f"- **Runtime Version**: {info.runtime_version}\n" + content += f"- **Is Signed**: {info.is_signed}\n" + content += f"- **Has Debug Info**: {info.has_debug_info}\n" + + return content + + except Exception as e: + logger.error(f"Error getting assembly info: {e}") + return f"Error: {str(e)}" + + +@mcp.tool() +async def search_types( + assembly_path: str, + pattern: str, + namespace_filter: str | None = None, + entity_types: list[str] | None = None, + case_sensitive: bool = False, + use_regex: bool = False, + ctx: Context | None = None, +) -> str: + """Search for types in an assembly by name pattern. + + Essential for finding specific classes, interfaces, or services in large assemblies. + Much faster than listing all types when you know what you're looking for. + + Common search patterns: + - "Service" - Find all service classes + - "Controller" - Find ASP.NET controllers + - "Handler" - Find command/event handlers + - "Exception" - Find custom exception types + - "I*Service" (with use_regex=True) - Find service interfaces + + Args: + assembly_path: Full path to the .NET assembly file (.dll or .exe) + pattern: Search pattern to match against type names. Matches if type name contains this string + namespace_filter: Only return types in namespaces containing this string (e.g., "MyApp.Services") + entity_types: Types to search. Accepts: "class", "interface", "struct", "delegate", "enum" (default: all) + case_sensitive: Whether pattern matching is case-sensitive (default: False) + use_regex: Treat pattern as regular expression (default: False) + """ + if ctx: + await ctx.info(f"Searching for types matching '{pattern}' in: {assembly_path}") + + try: + wrapper = get_wrapper() + + # Default to search all entity types + if entity_types is None: + entity_types = ["class", "interface", "struct", "delegate", "enum"] + + # Convert to EntityType enums + entity_type_enums = [] + for et in entity_types: + try: + entity_type_enums.append(EntityType.from_string(et)) + except ValueError as e: + logger.warning(f"Skipping invalid entity type: {e}") + continue + + from .models import ListTypesRequest + + request = ListTypesRequest(assembly_path=assembly_path, entity_types=entity_type_enums) + response = await wrapper.list_types(request) + + if not response.success: + return response.error_message or "Failed to list types" + + # Compile regex if needed + import re as regex_module + + if use_regex: + try: + flags = 0 if case_sensitive else regex_module.IGNORECASE + search_pattern = regex_module.compile(pattern, flags) + except regex_module.error as e: + return f"Invalid regex pattern: {e}" + else: + search_pattern = None + + # Filter types by pattern and namespace + matching_types = [] + for type_info in response.types: + # Check namespace filter first + if namespace_filter: + if type_info.namespace is None: + continue + ns_match = namespace_filter.lower() in type_info.namespace.lower() + if not ns_match: + continue + + # Check name pattern + name_to_check = type_info.name + if use_regex and search_pattern: + if not search_pattern.search(name_to_check): + continue + elif case_sensitive: + if pattern not in name_to_check: + continue + else: + if pattern.lower() not in name_to_check.lower(): + continue + + matching_types.append(type_info) + + if not matching_types: + return f"No types found matching pattern '{pattern}'" + + # Format results + content = f"# Search Results for '{pattern}'\n\n" + content += f"Found {len(matching_types)} matching types:\n\n" + + # Group by namespace + by_namespace: dict[str, list] = {} + for type_info in matching_types: + ns = type_info.namespace or "(Global)" + if ns not in by_namespace: + by_namespace[ns] = [] + by_namespace[ns].append(type_info) + + for namespace, types in sorted(by_namespace.items()): + content += f"## {namespace}\n\n" + for type_info in sorted(types, key=lambda t: t.name): + content += f"- **{type_info.name}** ({type_info.kind})\n" + content += f" - Full name: `{type_info.full_name}`\n" + content += "\n" + + content += "\n**TIP**: Use `decompile_assembly` with `type_name` set to the full name to examine any of these types." + return content + + except Exception as e: + logger.error(f"Error searching types: {e}") + return f"Error: {str(e)}" + + +@mcp.tool() +async def search_strings( + assembly_path: str, + pattern: str, + case_sensitive: bool = False, + use_regex: bool = False, + max_results: int = 100, + ctx: Context | None = None, +) -> str: + """Search for string literals in assembly code. + + Crucial for reverse engineering - finds hardcoded strings like: + - URLs, API endpoints, and connection strings + - Error messages and logging text + - Configuration keys and magic values + - Hardcoded credentials (security analysis) + - Registry keys and file paths + + Returns the types and methods containing matching strings. + + Args: + assembly_path: Full path to the .NET assembly file (.dll or .exe) + pattern: String pattern to search for in the decompiled code + case_sensitive: Whether search is case-sensitive (default: False) + use_regex: Treat pattern as regular expression (default: False) + max_results: Maximum number of matches to return (default: 100) + """ + if ctx: + await ctx.info(f"Searching for strings matching '{pattern}' in: {assembly_path}") + + try: + wrapper = get_wrapper() + + # Decompile to IL to find string literals (ldstr instructions) + from .models import DecompileRequest + + request = DecompileRequest( + assembly_path=assembly_path, + show_il_code=True, # IL makes string literals explicit + language_version=LanguageVersion.LATEST, + ) + + response = await wrapper.decompile(request) + + if not response.success: + return f"Failed to decompile assembly: {response.error_message}" + + source_code = response.source_code or "" + + # Compile regex if needed + import re as regex_module + + if use_regex: + try: + flags = 0 if case_sensitive else regex_module.IGNORECASE + search_pattern = regex_module.compile(pattern, flags) + except regex_module.error as e: + return f"Invalid regex pattern: {e}" + + # Search for string literals containing the pattern + # In IL, strings appear as: ldstr "string value" + # In C#, they're just regular string literals + matches = [] + current_type = None + current_method = None + + lines = source_code.split("\n") + for i, line in enumerate(lines): + # Track current type/method context + type_match = regex_module.match( + r"^\s*(?:public|private|internal|protected)?\s*(?:class|struct|interface)\s+(\w+)", + line, + ) + if type_match: + current_type = type_match.group(1) + + method_match = regex_module.match( + r"^\s*(?:public|private|internal|protected)?\s*(?:static\s+)?(?:\w+\s+)+(\w+)\s*\(", + line, + ) + if method_match: + current_method = method_match.group(1) + + # Search for pattern in the line + found = False + if use_regex and search_pattern: + found = bool(search_pattern.search(line)) + elif case_sensitive: + found = pattern in line + else: + found = pattern.lower() in line.lower() + + if found and len(matches) < max_results: + matches.append( + { + "line_num": i + 1, + "line": line.strip()[:200], # Truncate long lines + "type": current_type or "Unknown", + "method": current_method, + } + ) + + if not matches: + return f"No strings found matching pattern '{pattern}'" + + # Format results + content = f"# String Search Results for '{pattern}'\n\n" + content += f"Found {len(matches)} matches" + if len(matches) >= max_results: + content += f" (limited to {max_results})" + content += ":\n\n" + + # Group by type + by_type: dict[str, list] = {} + for match in matches: + type_name = match["type"] + if type_name not in by_type: + by_type[type_name] = [] + by_type[type_name].append(match) + + for type_name, type_matches in sorted(by_type.items()): + content += f"## {type_name}\n\n" + for match in type_matches[:20]: # Limit per type + method_info = f" in `{match['method']}()`" if match["method"] else "" + content += f"- Line {match['line_num']}{method_info}:\n" + content += f" ```\n {match['line']}\n ```\n" + if len(type_matches) > 20: + content += f" ... and {len(type_matches) - 20} more matches in this type\n" + content += "\n" + + content += "\n**TIP**: Use `decompile_assembly` with `type_name` to see the full context of interesting matches." + return content + + except Exception as e: + logger.error(f"Error searching strings: {e}") + return f"Error: {str(e)}" + + +# ============================================================================ +# Direct Metadata Tools (using dnfile - no ilspycmd required) +# ============================================================================ + + +@mcp.tool() +async def search_methods( + assembly_path: str, + pattern: str, + type_filter: str | None = None, + namespace_filter: str | None = None, + public_only: bool = False, + case_sensitive: bool = False, + use_regex: bool = False, + ctx: Context | None = None, +) -> str: + """Search for methods in an assembly by name pattern. + + Uses direct metadata parsing (no ilspycmd required) to find methods. + This searches the MethodDef metadata table directly. + + Useful for finding: + - Entry points and main methods + - Event handlers (OnClick, Handle*, etc.) + - Lifecycle methods (Initialize, Dispose, etc.) + - API endpoints and controllers + + Args: + assembly_path: Full path to the .NET assembly file (.dll or .exe) + pattern: Search pattern to match against method names + type_filter: Only search methods in types containing this string + namespace_filter: Only search in namespaces containing this string + public_only: Only return public methods (default: False) + case_sensitive: Whether pattern matching is case-sensitive (default: False) + use_regex: Treat pattern as regular expression (default: False) + """ + if ctx: + await ctx.info(f"Searching for methods matching '{pattern}' in: {assembly_path}") + + try: + import re as regex_module + + from .metadata_reader import MetadataReader + + with MetadataReader(assembly_path) as reader: + methods = reader.list_methods( + type_filter=type_filter, + namespace_filter=namespace_filter, + public_only=public_only, + ) + + if not methods: + return "No methods found in assembly (or assembly has no metadata)" + + # Compile regex if needed + if use_regex: + try: + flags = 0 if case_sensitive else regex_module.IGNORECASE + search_pattern = regex_module.compile(pattern, flags) + except regex_module.error as e: + return f"Invalid regex pattern: {e}" + else: + search_pattern = None + + # Filter by pattern + matching_methods = [] + for method in methods: + if use_regex and search_pattern: + if not search_pattern.search(method.name): + continue + elif case_sensitive: + if pattern not in method.name: + continue + else: + if pattern.lower() not in method.name.lower(): + continue + matching_methods.append(method) + + if not matching_methods: + return f"No methods found matching pattern '{pattern}'" + + # Format results + content = f"# Method Search Results for '{pattern}'\n\n" + content += f"Found {len(matching_methods)} matching methods:\n\n" + + # Group by type + by_type: dict[str, list] = {} + for method in matching_methods: + key = ( + f"{method.namespace}.{method.declaring_type}" + if method.namespace + else method.declaring_type + ) + if key not in by_type: + by_type[key] = [] + by_type[key].append(method) + + for type_name, type_methods in sorted(by_type.items()): + content += f"## {type_name}\n\n" + for method in sorted(type_methods, key=lambda m: m.name): + modifiers = [] + if method.is_public: + modifiers.append("public") + if method.is_static: + modifiers.append("static") + if method.is_virtual: + modifiers.append("virtual") + if method.is_abstract: + modifiers.append("abstract") + mod_str = " ".join(modifiers) + " " if modifiers else "" + content += f"- `{mod_str}{method.name}()`\n" + content += "\n" + + content += ( + "\n**TIP**: Use `decompile_assembly` with `type_name` to see the full implementation." + ) + return content + + except FileNotFoundError as e: + return f"Error: {e}" + except Exception as e: + logger.error(f"Error searching methods: {e}") + return f"Error: {str(e)}" + + +@mcp.tool() +async def search_fields( + assembly_path: str, + pattern: str, + type_filter: str | None = None, + namespace_filter: str | None = None, + public_only: bool = False, + constants_only: bool = False, + case_sensitive: bool = False, + use_regex: bool = False, + ctx: Context | None = None, +) -> str: + """Search for fields in an assembly by name pattern. + + Uses direct metadata parsing to find fields and constants. + Useful for finding: + - Configuration values and magic numbers + - Constant strings (error messages, URLs) + - Static fields (singletons, caches) + + Args: + assembly_path: Full path to the .NET assembly file (.dll or .exe) + pattern: Search pattern to match against field names + type_filter: Only search fields in types containing this string + namespace_filter: Only search in namespaces containing this string + public_only: Only return public fields (default: False) + constants_only: Only return constant (literal) fields (default: False) + case_sensitive: Whether pattern matching is case-sensitive (default: False) + use_regex: Treat pattern as regular expression (default: False) + """ + if ctx: + await ctx.info(f"Searching for fields matching '{pattern}' in: {assembly_path}") + + try: + import re as regex_module + + from .metadata_reader import MetadataReader + + with MetadataReader(assembly_path) as reader: + fields = reader.list_fields( + type_filter=type_filter, + namespace_filter=namespace_filter, + public_only=public_only, + constants_only=constants_only, + ) + + if not fields: + return "No fields found in assembly" + + # Compile regex if needed + if use_regex: + try: + flags = 0 if case_sensitive else regex_module.IGNORECASE + search_pattern = regex_module.compile(pattern, flags) + except regex_module.error as e: + return f"Invalid regex pattern: {e}" + else: + search_pattern = None + + # Filter by pattern + matching_fields = [] + for field in fields: + if use_regex and search_pattern: + if not search_pattern.search(field.name): + continue + elif case_sensitive: + if pattern not in field.name: + continue + else: + if pattern.lower() not in field.name.lower(): + continue + matching_fields.append(field) + + if not matching_fields: + return f"No fields found matching pattern '{pattern}'" + + # Format results + content = f"# Field Search Results for '{pattern}'\n\n" + content += f"Found {len(matching_fields)} matching fields:\n\n" + + # Group by type + by_type: dict[str, list] = {} + for field in matching_fields: + key = ( + f"{field.namespace}.{field.declaring_type}" + if field.namespace + else field.declaring_type + ) + if key not in by_type: + by_type[key] = [] + by_type[key].append(field) + + for type_name, type_fields in sorted(by_type.items()): + content += f"## {type_name}\n\n" + for field in sorted(type_fields, key=lambda f: f.name): + modifiers = [] + if field.is_public: + modifiers.append("public") + if field.is_static: + modifiers.append("static") + if field.is_literal: + modifiers.append("const") + mod_str = " ".join(modifiers) + " " if modifiers else "" + content += f"- `{mod_str}{field.name}`\n" + content += "\n" + + return content + + except FileNotFoundError as e: + return f"Error: {e}" + except Exception as e: + logger.error(f"Error searching fields: {e}") + return f"Error: {str(e)}" + + +@mcp.tool() +async def search_properties( + assembly_path: str, + pattern: str, + type_filter: str | None = None, + namespace_filter: str | None = None, + case_sensitive: bool = False, + use_regex: bool = False, + ctx: Context | None = None, +) -> str: + """Search for properties in an assembly by name pattern. + + Uses direct metadata parsing to find properties. + Useful for finding: + - Configuration properties + - Data model fields + - API response/request properties + + Args: + assembly_path: Full path to the .NET assembly file (.dll or .exe) + pattern: Search pattern to match against property names + type_filter: Only search properties in types containing this string + namespace_filter: Only search in namespaces containing this string + case_sensitive: Whether pattern matching is case-sensitive (default: False) + use_regex: Treat pattern as regular expression (default: False) + """ + if ctx: + await ctx.info(f"Searching for properties matching '{pattern}' in: {assembly_path}") + + try: + import re as regex_module + + from .metadata_reader import MetadataReader + + with MetadataReader(assembly_path) as reader: + properties = reader.list_properties( + type_filter=type_filter, + namespace_filter=namespace_filter, + ) + + if not properties: + return "No properties found in assembly" + + # Compile regex if needed + if use_regex: + try: + flags = 0 if case_sensitive else regex_module.IGNORECASE + search_pattern = regex_module.compile(pattern, flags) + except regex_module.error as e: + return f"Invalid regex pattern: {e}" + else: + search_pattern = None + + # Filter by pattern + matching_props = [] + for prop in properties: + if use_regex and search_pattern: + if not search_pattern.search(prop.name): + continue + elif case_sensitive: + if pattern not in prop.name: + continue + else: + if pattern.lower() not in prop.name.lower(): + continue + matching_props.append(prop) + + if not matching_props: + return f"No properties found matching pattern '{pattern}'" + + # Format results + content = f"# Property Search Results for '{pattern}'\n\n" + content += f"Found {len(matching_props)} matching properties:\n\n" + + # Group by type + by_type: dict[str, list] = {} + for prop in matching_props: + key = ( + f"{prop.namespace}.{prop.declaring_type}" if prop.namespace else prop.declaring_type + ) + if key not in by_type: + by_type[key] = [] + by_type[key].append(prop) + + for type_name, type_props in sorted(by_type.items()): + content += f"## {type_name}\n\n" + for prop in sorted(type_props, key=lambda p: p.name): + content += f"- `{prop.name}`\n" + content += "\n" + + return content + + except FileNotFoundError as e: + return f"Error: {e}" + except Exception as e: + logger.error(f"Error searching properties: {e}") + return f"Error: {str(e)}" + + +@mcp.tool() +async def list_events( + assembly_path: str, + type_filter: str | None = None, + namespace_filter: str | None = None, + ctx: Context | None = None, +) -> str: + """List all events defined in an assembly. + + Uses direct metadata parsing to enumerate events. + Useful for understanding: + - Event-driven architecture + - Observer patterns + - UI event handlers + + Args: + assembly_path: Full path to the .NET assembly file (.dll or .exe) + type_filter: Only list events in types containing this string + namespace_filter: Only list events in namespaces containing this string + """ + if ctx: + await ctx.info(f"Listing events in: {assembly_path}") + + try: + from .metadata_reader import MetadataReader + + with MetadataReader(assembly_path) as reader: + events = reader.list_events( + type_filter=type_filter, + namespace_filter=namespace_filter, + ) + + if not events: + return "No events found in assembly" + + content = "# Events in Assembly\n\n" + content += f"Found {len(events)} events:\n\n" + + # Group by type + by_type: dict[str, list] = {} + for evt in events: + key = f"{evt.namespace}.{evt.declaring_type}" if evt.namespace else evt.declaring_type + if key not in by_type: + by_type[key] = [] + by_type[key].append(evt) + + for type_name, type_events in sorted(by_type.items()): + content += f"## {type_name}\n\n" + for evt in sorted(type_events, key=lambda e: e.name): + content += f"- `event {evt.name}`\n" + content += "\n" + + return content + + except FileNotFoundError as e: + return f"Error: {e}" + except Exception as e: + logger.error(f"Error listing events: {e}") + return f"Error: {str(e)}" + + +@mcp.tool() +async def list_resources( + assembly_path: str, + ctx: Context | None = None, +) -> str: + """List all embedded resources in an assembly. + + Uses direct metadata parsing to enumerate ManifestResource entries. + Useful for finding: + - Embedded files (images, configs, data) + - Localization resources + - Embedded assemblies + + Args: + assembly_path: Full path to the .NET assembly file (.dll or .exe) + """ + if ctx: + await ctx.info(f"Listing resources in: {assembly_path}") + + try: + from .metadata_reader import MetadataReader + + with MetadataReader(assembly_path) as reader: + resources = reader.list_resources() + + if not resources: + return "No embedded resources found in assembly" + + content = "# Embedded Resources\n\n" + content += f"Found {len(resources)} resources:\n\n" + + for res in sorted(resources, key=lambda r: r.name): + visibility = "public" if res.is_public else "private" + content += f"- `{res.name}` ({visibility})\n" + + return content + + except FileNotFoundError as e: + return f"Error: {e}" + except Exception as e: + logger.error(f"Error listing resources: {e}") + return f"Error: {str(e)}" + + +@mcp.tool() +async def get_metadata_summary( + assembly_path: str, + ctx: Context | None = None, +) -> str: + """Get a comprehensive metadata summary of an assembly. + + Uses direct metadata parsing (dnfile) for accurate counts and details. + This is more accurate than get_assembly_info for metadata statistics. + + Returns: + - Assembly name, version, culture + - Type/method/field/property/event counts + - Referenced assemblies + - Target framework (if available) + + Args: + assembly_path: Full path to the .NET assembly file (.dll or .exe) + """ + if ctx: + await ctx.info(f"Getting metadata summary: {assembly_path}") + + try: + from .metadata_reader import MetadataReader + + with MetadataReader(assembly_path) as reader: + meta = reader.get_assembly_metadata() + + content = "# Assembly Metadata Summary\n\n" + content += "## Identity\n\n" + content += f"- **Name**: {meta.name}\n" + content += f"- **Version**: {meta.version}\n" + if meta.culture: + content += f"- **Culture**: {meta.culture}\n" + if meta.public_key_token: + content += f"- **Public Key Token**: {meta.public_key_token}\n" + if meta.target_framework: + content += f"- **Target Framework**: {meta.target_framework}\n" + + content += "\n## Statistics\n\n" + content += "| Entity | Count |\n" + content += "|--------|-------|\n" + content += f"| Types | {meta.type_count} |\n" + content += f"| Methods | {meta.method_count} |\n" + content += f"| Fields | {meta.field_count} |\n" + content += f"| Properties | {meta.property_count} |\n" + content += f"| Events | {meta.event_count} |\n" + content += f"| Resources | {meta.resource_count} |\n" + + if meta.referenced_assemblies: + content += f"\n## Referenced Assemblies ({len(meta.referenced_assemblies)})\n\n" + for ref in sorted(meta.referenced_assemblies): + content += f"- `{ref}`\n" + + return content + + except FileNotFoundError as e: + return f"Error: {e}" + except Exception as e: + logger.error(f"Error getting metadata summary: {e}") + return f"Error: {str(e)}" + + +# FastMCP automatically handles prompts +@mcp.prompt() +def analyze_assembly_prompt(assembly_path: str, focus_area: str = "types") -> str: + """Prompt template for analyzing .NET assemblies""" + return f"""I need to analyze the .NET assembly at "{assembly_path}". + +Please help me understand: +1. The overall structure and organization of the assembly +2. Key types and their relationships +3. Main namespaces and their purposes +4. Any notable patterns or architectural decisions + +Focus area: {focus_area} + +Start by listing the types in the assembly, then provide insights based on what you find.""" + + +@mcp.prompt() +def decompile_and_explain_prompt(assembly_path: str, type_name: str) -> str: + """Prompt template for decompiling and explaining specific types""" + return f"""I want to understand the type "{type_name}" from the assembly "{assembly_path}". + +Please: +1. Decompile this specific type +2. Explain what this type does and its purpose +3. Highlight any interesting patterns, design decisions, or potential issues +4. Suggest how this type fits into the overall architecture + +Type to analyze: {type_name} +Assembly: {assembly_path}""" + + +def main(): + """Entry point for the MCP server.""" + import sys + + try: + from importlib.metadata import version + + package_version = version("mcilspy") + except Exception: + package_version = "0.1.1" + + # Print banner to stderr (stdout is reserved for MCP protocol) + print( + f"🔬 mcilspy v{package_version} - .NET Assembly Decompiler MCP Server", + file=sys.stderr, + ) + mcp.run(transport="stdio") + + +if __name__ == "__main__": + main() diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..598e2ba --- /dev/null +++ b/uv.lock @@ -0,0 +1,771 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "certifi" +version = "2026.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, + { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, + { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, + { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, + { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, + { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, + { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, + { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, + { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "cryptography" +version = "46.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/19/f748958276519adf6a0c1e79e7b8860b4830dda55ccdf29f2719b5fc499c/cryptography-46.0.4.tar.gz", hash = "sha256:bfd019f60f8abc2ed1b9be4ddc21cfef059c841d86d710bb69909a688cbb8f59", size = 749301, upload-time = "2026-01-28T00:24:37.379Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/99/157aae7949a5f30d51fcb1a9851e8ebd5c74bf99b5285d8bb4b8b9ee641e/cryptography-46.0.4-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:281526e865ed4166009e235afadf3a4c4cba6056f99336a99efba65336fd5485", size = 7173686, upload-time = "2026-01-28T00:23:07.515Z" }, + { url = "https://files.pythonhosted.org/packages/87/91/874b8910903159043b5c6a123b7e79c4559ddd1896e38967567942635778/cryptography-46.0.4-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5f14fba5bf6f4390d7ff8f086c566454bff0411f6d8aa7af79c88b6f9267aecc", size = 4275871, upload-time = "2026-01-28T00:23:09.439Z" }, + { url = "https://files.pythonhosted.org/packages/c0/35/690e809be77896111f5b195ede56e4b4ed0435b428c2f2b6d35046fbb5e8/cryptography-46.0.4-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47bcd19517e6389132f76e2d5303ded6cf3f78903da2158a671be8de024f4cd0", size = 4423124, upload-time = "2026-01-28T00:23:11.529Z" }, + { url = "https://files.pythonhosted.org/packages/1a/5b/a26407d4f79d61ca4bebaa9213feafdd8806dc69d3d290ce24996d3cfe43/cryptography-46.0.4-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:01df4f50f314fbe7009f54046e908d1754f19d0c6d3070df1e6268c5a4af09fa", size = 4277090, upload-time = "2026-01-28T00:23:13.123Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d8/4bb7aec442a9049827aa34cee1aa83803e528fa55da9a9d45d01d1bb933e/cryptography-46.0.4-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5aa3e463596b0087b3da0dbe2b2487e9fc261d25da85754e30e3b40637d61f81", size = 4947652, upload-time = "2026-01-28T00:23:14.554Z" }, + { url = "https://files.pythonhosted.org/packages/2b/08/f83e2e0814248b844265802d081f2fac2f1cbe6cd258e72ba14ff006823a/cryptography-46.0.4-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0a9ad24359fee86f131836a9ac3bffc9329e956624a2d379b613f8f8abaf5255", size = 4455157, upload-time = "2026-01-28T00:23:16.443Z" }, + { url = "https://files.pythonhosted.org/packages/0a/05/19d849cf4096448779d2dcc9bb27d097457dac36f7273ffa875a93b5884c/cryptography-46.0.4-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:dc1272e25ef673efe72f2096e92ae39dea1a1a450dd44918b15351f72c5a168e", size = 3981078, upload-time = "2026-01-28T00:23:17.838Z" }, + { url = "https://files.pythonhosted.org/packages/e6/89/f7bac81d66ba7cde867a743ea5b37537b32b5c633c473002b26a226f703f/cryptography-46.0.4-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:de0f5f4ec8711ebc555f54735d4c673fc34b65c44283895f1a08c2b49d2fd99c", size = 4276213, upload-time = "2026-01-28T00:23:19.257Z" }, + { url = "https://files.pythonhosted.org/packages/da/9f/7133e41f24edd827020ad21b068736e792bc68eecf66d93c924ad4719fb3/cryptography-46.0.4-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:eeeb2e33d8dbcccc34d64651f00a98cb41b2dc69cef866771a5717e6734dfa32", size = 4912190, upload-time = "2026-01-28T00:23:21.244Z" }, + { url = "https://files.pythonhosted.org/packages/a6/f7/6d43cbaddf6f65b24816e4af187d211f0bc536a29961f69faedc48501d8e/cryptography-46.0.4-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:3d425eacbc9aceafd2cb429e42f4e5d5633c6f873f5e567077043ef1b9bbf616", size = 4454641, upload-time = "2026-01-28T00:23:22.866Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4f/ebd0473ad656a0ac912a16bd07db0f5d85184924e14fc88feecae2492834/cryptography-46.0.4-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91627ebf691d1ea3976a031b61fb7bac1ccd745afa03602275dda443e11c8de0", size = 4405159, upload-time = "2026-01-28T00:23:25.278Z" }, + { url = "https://files.pythonhosted.org/packages/d1/f7/7923886f32dc47e27adeff8246e976d77258fd2aa3efdd1754e4e323bf49/cryptography-46.0.4-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2d08bc22efd73e8854b0b7caff402d735b354862f1145d7be3b9c0f740fef6a0", size = 4666059, upload-time = "2026-01-28T00:23:26.766Z" }, + { url = "https://files.pythonhosted.org/packages/eb/a7/0fca0fd3591dffc297278a61813d7f661a14243dd60f499a7a5b48acb52a/cryptography-46.0.4-cp311-abi3-win32.whl", hash = "sha256:82a62483daf20b8134f6e92898da70d04d0ef9a75829d732ea1018678185f4f5", size = 3026378, upload-time = "2026-01-28T00:23:28.317Z" }, + { url = "https://files.pythonhosted.org/packages/2d/12/652c84b6f9873f0909374864a57b003686c642ea48c84d6c7e2c515e6da5/cryptography-46.0.4-cp311-abi3-win_amd64.whl", hash = "sha256:6225d3ebe26a55dbc8ead5ad1265c0403552a63336499564675b29eb3184c09b", size = 3478614, upload-time = "2026-01-28T00:23:30.275Z" }, + { url = "https://files.pythonhosted.org/packages/b9/27/542b029f293a5cce59349d799d4d8484b3b1654a7b9a0585c266e974a488/cryptography-46.0.4-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:485e2b65d25ec0d901bca7bcae0f53b00133bf3173916d8e421f6fddde103908", size = 7116417, upload-time = "2026-01-28T00:23:31.958Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f5/559c25b77f40b6bf828eabaf988efb8b0e17b573545edb503368ca0a2a03/cryptography-46.0.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:078e5f06bd2fa5aea5a324f2a09f914b1484f1d0c2a4d6a8a28c74e72f65f2da", size = 4264508, upload-time = "2026-01-28T00:23:34.264Z" }, + { url = "https://files.pythonhosted.org/packages/49/a1/551fa162d33074b660dc35c9bc3616fefa21a0e8c1edd27b92559902e408/cryptography-46.0.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dce1e4f068f03008da7fa51cc7abc6ddc5e5de3e3d1550334eaf8393982a5829", size = 4409080, upload-time = "2026-01-28T00:23:35.793Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6a/4d8d129a755f5d6df1bbee69ea2f35ebfa954fa1847690d1db2e8bca46a5/cryptography-46.0.4-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:2067461c80271f422ee7bdbe79b9b4be54a5162e90345f86a23445a0cf3fd8a2", size = 4270039, upload-time = "2026-01-28T00:23:37.263Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f5/ed3fcddd0a5e39321e595e144615399e47e7c153a1fb8c4862aec3151ff9/cryptography-46.0.4-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:c92010b58a51196a5f41c3795190203ac52edfd5dc3ff99149b4659eba9d2085", size = 4926748, upload-time = "2026-01-28T00:23:38.884Z" }, + { url = "https://files.pythonhosted.org/packages/43/ae/9f03d5f0c0c00e85ecb34f06d3b79599f20630e4db91b8a6e56e8f83d410/cryptography-46.0.4-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:829c2b12bbc5428ab02d6b7f7e9bbfd53e33efd6672d21341f2177470171ad8b", size = 4442307, upload-time = "2026-01-28T00:23:40.56Z" }, + { url = "https://files.pythonhosted.org/packages/8b/22/e0f9f2dae8040695103369cf2283ef9ac8abe4d51f68710bec2afd232609/cryptography-46.0.4-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:62217ba44bf81b30abaeda1488686a04a702a261e26f87db51ff61d9d3510abd", size = 3959253, upload-time = "2026-01-28T00:23:42.827Z" }, + { url = "https://files.pythonhosted.org/packages/01/5b/6a43fcccc51dae4d101ac7d378a8724d1ba3de628a24e11bf2f4f43cba4d/cryptography-46.0.4-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:9c2da296c8d3415b93e6053f5a728649a87a48ce084a9aaf51d6e46c87c7f2d2", size = 4269372, upload-time = "2026-01-28T00:23:44.655Z" }, + { url = "https://files.pythonhosted.org/packages/17/b7/0f6b8c1dd0779df2b526e78978ff00462355e31c0a6f6cff8a3e99889c90/cryptography-46.0.4-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9b34d8ba84454641a6bf4d6762d15847ecbd85c1316c0a7984e6e4e9f748ec2e", size = 4891908, upload-time = "2026-01-28T00:23:46.48Z" }, + { url = "https://files.pythonhosted.org/packages/83/17/259409b8349aa10535358807a472c6a695cf84f106022268d31cea2b6c97/cryptography-46.0.4-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:df4a817fa7138dd0c96c8c8c20f04b8aaa1fac3bbf610913dcad8ea82e1bfd3f", size = 4441254, upload-time = "2026-01-28T00:23:48.403Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fe/e4a1b0c989b00cee5ffa0764401767e2d1cf59f45530963b894129fd5dce/cryptography-46.0.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b1de0ebf7587f28f9190b9cb526e901bf448c9e6a99655d2b07fff60e8212a82", size = 4396520, upload-time = "2026-01-28T00:23:50.26Z" }, + { url = "https://files.pythonhosted.org/packages/b3/81/ba8fd9657d27076eb40d6a2f941b23429a3c3d2f56f5a921d6b936a27bc9/cryptography-46.0.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9b4d17bc7bd7cdd98e3af40b441feaea4c68225e2eb2341026c84511ad246c0c", size = 4651479, upload-time = "2026-01-28T00:23:51.674Z" }, + { url = "https://files.pythonhosted.org/packages/00/03/0de4ed43c71c31e4fe954edd50b9d28d658fef56555eba7641696370a8e2/cryptography-46.0.4-cp314-cp314t-win32.whl", hash = "sha256:c411f16275b0dea722d76544a61d6421e2cc829ad76eec79280dbdc9ddf50061", size = 3001986, upload-time = "2026-01-28T00:23:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/5c/70/81830b59df7682917d7a10f833c4dab2a5574cd664e86d18139f2b421329/cryptography-46.0.4-cp314-cp314t-win_amd64.whl", hash = "sha256:728fedc529efc1439eb6107b677f7f7558adab4553ef8669f0d02d42d7b959a7", size = 3468288, upload-time = "2026-01-28T00:23:55.09Z" }, + { url = "https://files.pythonhosted.org/packages/56/f7/f648fdbb61d0d45902d3f374217451385edc7e7768d1b03ff1d0e5ffc17b/cryptography-46.0.4-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a9556ba711f7c23f77b151d5798f3ac44a13455cc68db7697a1096e6d0563cab", size = 7169583, upload-time = "2026-01-28T00:23:56.558Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cc/8f3224cbb2a928de7298d6ed4790f5ebc48114e02bdc9559196bfb12435d/cryptography-46.0.4-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8bf75b0259e87fa70bddc0b8b4078b76e7fd512fd9afae6c1193bcf440a4dbef", size = 4275419, upload-time = "2026-01-28T00:23:58.364Z" }, + { url = "https://files.pythonhosted.org/packages/17/43/4a18faa7a872d00e4264855134ba82d23546c850a70ff209e04ee200e76f/cryptography-46.0.4-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3c268a3490df22270955966ba236d6bc4a8f9b6e4ffddb78aac535f1a5ea471d", size = 4419058, upload-time = "2026-01-28T00:23:59.867Z" }, + { url = "https://files.pythonhosted.org/packages/ee/64/6651969409821d791ba12346a124f55e1b76f66a819254ae840a965d4b9c/cryptography-46.0.4-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:812815182f6a0c1d49a37893a303b44eaac827d7f0d582cecfc81b6427f22973", size = 4278151, upload-time = "2026-01-28T00:24:01.731Z" }, + { url = "https://files.pythonhosted.org/packages/20/0b/a7fce65ee08c3c02f7a8310cc090a732344066b990ac63a9dfd0a655d321/cryptography-46.0.4-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:a90e43e3ef65e6dcf969dfe3bb40cbf5aef0d523dff95bfa24256be172a845f4", size = 4939441, upload-time = "2026-01-28T00:24:03.175Z" }, + { url = "https://files.pythonhosted.org/packages/db/a7/20c5701e2cd3e1dfd7a19d2290c522a5f435dd30957d431dcb531d0f1413/cryptography-46.0.4-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a05177ff6296644ef2876fce50518dffb5bcdf903c85250974fc8bc85d54c0af", size = 4451617, upload-time = "2026-01-28T00:24:05.403Z" }, + { url = "https://files.pythonhosted.org/packages/00/dc/3e16030ea9aa47b63af6524c354933b4fb0e352257c792c4deeb0edae367/cryptography-46.0.4-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:daa392191f626d50f1b136c9b4cf08af69ca8279d110ea24f5c2700054d2e263", size = 3977774, upload-time = "2026-01-28T00:24:06.851Z" }, + { url = "https://files.pythonhosted.org/packages/42/c8/ad93f14118252717b465880368721c963975ac4b941b7ef88f3c56bf2897/cryptography-46.0.4-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e07ea39c5b048e085f15923511d8121e4a9dc45cee4e3b970ca4f0d338f23095", size = 4277008, upload-time = "2026-01-28T00:24:08.926Z" }, + { url = "https://files.pythonhosted.org/packages/00/cf/89c99698151c00a4631fbfcfcf459d308213ac29e321b0ff44ceeeac82f1/cryptography-46.0.4-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d5a45ddc256f492ce42a4e35879c5e5528c09cd9ad12420828c972951d8e016b", size = 4903339, upload-time = "2026-01-28T00:24:12.009Z" }, + { url = "https://files.pythonhosted.org/packages/03/c3/c90a2cb358de4ac9309b26acf49b2a100957e1ff5cc1e98e6c4996576710/cryptography-46.0.4-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:6bb5157bf6a350e5b28aee23beb2d84ae6f5be390b2f8ee7ea179cda077e1019", size = 4451216, upload-time = "2026-01-28T00:24:13.975Z" }, + { url = "https://files.pythonhosted.org/packages/96/2c/8d7f4171388a10208671e181ca43cdc0e596d8259ebacbbcfbd16de593da/cryptography-46.0.4-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:dd5aba870a2c40f87a3af043e0dee7d9eb02d4aff88a797b48f2b43eff8c3ab4", size = 4404299, upload-time = "2026-01-28T00:24:16.169Z" }, + { url = "https://files.pythonhosted.org/packages/e9/23/cbb2036e450980f65c6e0a173b73a56ff3bccd8998965dea5cc9ddd424a5/cryptography-46.0.4-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:93d8291da8d71024379ab2cb0b5c57915300155ad42e07f76bea6ad838d7e59b", size = 4664837, upload-time = "2026-01-28T00:24:17.629Z" }, + { url = "https://files.pythonhosted.org/packages/0a/21/f7433d18fe6d5845329cbdc597e30caf983229c7a245bcf54afecc555938/cryptography-46.0.4-cp38-abi3-win32.whl", hash = "sha256:0563655cb3c6d05fb2afe693340bc050c30f9f34e15763361cf08e94749401fc", size = 3009779, upload-time = "2026-01-28T00:24:20.198Z" }, + { url = "https://files.pythonhosted.org/packages/3a/6a/bd2e7caa2facffedf172a45c1a02e551e6d7d4828658c9a245516a598d94/cryptography-46.0.4-cp38-abi3-win_amd64.whl", hash = "sha256:fa0900b9ef9c49728887d1576fd8d9e7e3ea872fa9b25ef9b64888adc434e976", size = 3466633, upload-time = "2026-01-28T00:24:21.851Z" }, + { url = "https://files.pythonhosted.org/packages/59/e0/f9c6c53e1f2a1c2507f00f2faba00f01d2f334b35b0fbfe5286715da2184/cryptography-46.0.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:766330cce7416c92b5e90c3bb71b1b79521760cdcfc3a6a1a182d4c9fab23d2b", size = 3476316, upload-time = "2026-01-28T00:24:24.144Z" }, + { url = "https://files.pythonhosted.org/packages/27/7a/f8d2d13227a9a1a9fe9c7442b057efecffa41f1e3c51d8622f26b9edbe8f/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c236a44acfb610e70f6b3e1c3ca20ff24459659231ef2f8c48e879e2d32b73da", size = 4216693, upload-time = "2026-01-28T00:24:25.758Z" }, + { url = "https://files.pythonhosted.org/packages/c5/de/3787054e8f7972658370198753835d9d680f6cd4a39df9f877b57f0dd69c/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8a15fb869670efa8f83cbffbc8753c1abf236883225aed74cd179b720ac9ec80", size = 4382765, upload-time = "2026-01-28T00:24:27.577Z" }, + { url = "https://files.pythonhosted.org/packages/8a/5f/60e0afb019973ba6a0b322e86b3d61edf487a4f5597618a430a2a15f2d22/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:fdc3daab53b212472f1524d070735b2f0c214239df131903bae1d598016fa822", size = 4216066, upload-time = "2026-01-28T00:24:29.056Z" }, + { url = "https://files.pythonhosted.org/packages/81/8e/bf4a0de294f147fee66f879d9bae6f8e8d61515558e3d12785dd90eca0be/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:44cc0675b27cadb71bdbb96099cca1fa051cd11d2ade09e5cd3a2edb929ed947", size = 4382025, upload-time = "2026-01-28T00:24:30.681Z" }, + { url = "https://files.pythonhosted.org/packages/79/f4/9ceb90cfd6a3847069b0b0b353fd3075dc69b49defc70182d8af0c4ca390/cryptography-46.0.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be8c01a7d5a55f9a47d1888162b76c8f49d62b234d88f0ff91a9fbebe32ffbc3", size = 3406043, upload-time = "2026-01-28T00:24:32.236Z" }, +] + +[[package]] +name = "dnfile" +version = "0.18.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pefile" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/01/c3/89dbe3a48dfa303060d59b74b97b8401cc70b9b4d4c24982202861fd82e9/dnfile-0.18.0.tar.gz", hash = "sha256:24eec63d65535b702236001e648bbf2fa139d1a7bea191fdca17c3be337eb4db", size = 51519, upload-time = "2026-01-31T05:09:11.212Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/ad/4f945155a78800cea02cb258bc0b7704c43cf94f50f75c808f0618694153/dnfile-0.18.0-py3-none-any.whl", hash = "sha256:7579fe2d6bb1d854aa154929c309fbf34e70b622053a043a51d42f079fad1772", size = 48044, upload-time = "2026-01-31T05:09:09.703Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "mcilspy" +version = "0.1.1" +source = { editable = "." } +dependencies = [ + { name = "dnfile" }, + { name = "mcp" }, + { name = "pydantic" }, +] + +[package.metadata] +requires-dist = [ + { name = "dnfile", specifier = ">=0.15.0" }, + { name = "mcp", specifier = ">=1.0.0" }, + { name = "pydantic", specifier = ">=2.7.0" }, +] + +[[package]] +name = "mcp" +version = "1.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/6d/62e76bbb8144d6ed86e202b5edd8a4cb631e7c8130f3f4893c3f90262b10/mcp-1.26.0.tar.gz", hash = "sha256:db6e2ef491eecc1a0d93711a76f28dec2e05999f93afd48795da1c1137142c66", size = 608005, upload-time = "2026-01-24T19:40:32.468Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/d9/eaa1f80170d2b7c5ba23f3b59f766f3a0bb41155fbc32a69adfa1adaaef9/mcp-1.26.0-py3-none-any.whl", hash = "sha256:904a21c33c25aa98ddbeb47273033c435e595bbacfdb177f4bd87f6dceebe1ca", size = 233615, upload-time = "2026-01-24T19:40:30.652Z" }, +] + +[[package]] +name = "pefile" +version = "2024.8.26" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/03/4f/2750f7f6f025a1507cd3b7218691671eecfd0bbebebe8b39aa0fe1d360b8/pefile-2024.8.26.tar.gz", hash = "sha256:3ff6c5d8b43e8c37bb6e6dd5085658d658a7a0bdcd20b6a07b1fcfc1c4e9d632", size = 76008, upload-time = "2024-08-26T20:58:38.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/16/12b82f791c7f50ddec566873d5bdd245baa1491bac11d15ffb98aecc8f8b/pefile-2024.8.26-py3-none-any.whl", hash = "sha256:76f8b485dcd3b1bb8166f1128d395fa3d87af26360c2358fb75b80019b957c6f", size = 74766, upload-time = "2024-08-26T21:01:02.632Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, + { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, + { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, + { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, + { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, + { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, + { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, + { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, + { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, + { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, + { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/ac7e0aae12027748076d72a8764ff1c9d82ca75a7a52622e67ed3f765c54/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0", size = 194184, upload-time = "2025-11-10T14:25:47.013Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" }, +] + +[[package]] +name = "pyjwt" +version = "2.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/5a/b46fa56bf322901eee5b0454a34343cdbdae202cd421775a8ee4e42fd519/pyjwt-2.11.0.tar.gz", hash = "sha256:35f95c1f0fbe5d5ba6e43f00271c275f7a1a4db1dab27bf708073b75318ea623", size = 98019, upload-time = "2026-01-30T19:59:55.694Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/01/c26ce75ba460d5cd503da9e13b21a33804d38c2165dec7b716d06b13010c/pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469", size = 28224, upload-time = "2026-01-30T19:59:54.539Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432, upload-time = "2025-07-14T20:13:05.9Z" }, + { url = "https://files.pythonhosted.org/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103, upload-time = "2025-07-14T20:13:07.698Z" }, + { url = "https://files.pythonhosted.org/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557, upload-time = "2025-07-14T20:13:11.11Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, + { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, + { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" }, + { url = "https://files.pythonhosted.org/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", size = 359751, upload-time = "2025-11-30T20:21:34.591Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696, upload-time = "2025-11-30T20:21:36.122Z" }, + { url = "https://files.pythonhosted.org/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136, upload-time = "2025-11-30T20:21:37.728Z" }, + { url = "https://files.pythonhosted.org/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699, upload-time = "2025-11-30T20:21:38.92Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022, upload-time = "2025-11-30T20:21:40.407Z" }, + { url = "https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522, upload-time = "2025-11-30T20:21:42.17Z" }, + { url = "https://files.pythonhosted.org/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579, upload-time = "2025-11-30T20:21:43.769Z" }, + { url = "https://files.pythonhosted.org/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305, upload-time = "2025-11-30T20:21:44.994Z" }, + { url = "https://files.pythonhosted.org/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503, upload-time = "2025-11-30T20:21:46.91Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322, upload-time = "2025-11-30T20:21:48.709Z" }, + { url = "https://files.pythonhosted.org/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792, upload-time = "2025-11-30T20:21:50.024Z" }, + { url = "https://files.pythonhosted.org/packages/e2/da/4e2b19d0f131f35b6146425f846563d0ce036763e38913d917187307a671/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464", size = 221901, upload-time = "2025-11-30T20:21:51.32Z" }, + { url = "https://files.pythonhosted.org/packages/96/cb/156d7a5cf4f78a7cc571465d8aec7a3c447c94f6749c5123f08438bcf7bc/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169", size = 235823, upload-time = "2025-11-30T20:21:52.505Z" }, + { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, + { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, + { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, + { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, + { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, + { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, + { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, + { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, + { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, + { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, +] + +[[package]] +name = "sse-starlette" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/8d/00d280c03ffd39aaee0e86ec81e2d3b9253036a0f93f51d10503adef0e65/sse_starlette-3.2.0.tar.gz", hash = "sha256:8127594edfb51abe44eac9c49e59b0b01f1039d0c7461c6fd91d4e03b70da422", size = 27253, upload-time = "2026-01-17T13:11:05.62Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/7f/832f015020844a8b8f7a9cbc103dd76ba8e3875004c41e08440ea3a2b41a/sse_starlette-3.2.0-py3-none-any.whl", hash = "sha256:5876954bd51920fc2cd51baee47a080eb88a37b5b784e615abb0b283f801cdbf", size = 12763, upload-time = "2026-01-17T13:11:03.775Z" }, +] + +[[package]] +name = "starlette" +version = "0.52.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/68/79977123bb7be889ad680d79a40f339082c1978b5cfcf62c2d8d196873ac/starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933", size = 2653702, upload-time = "2026-01-18T13:34:11.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761, upload-time = "2025-12-21T14:16:22.45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" }, +]