refactor: address major code review findings
- Use importlib.metadata for dynamic version (single source in pyproject.toml) - Clean up duplicate `import re` statements across modules - Add missing type hints to all public methods - Fix PATH auto-discovery for ilspycmd (~/.dotnet/tools) - Add pytest test suite with 35 tests covering models, metadata reader, wrapper - Bump version to 0.2.0, add CHANGELOG.md
This commit is contained in:
parent
157d671d28
commit
7d784af17c
47
CHANGELOG.md
Normal file
47
CHANGELOG.md
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to mcilspy will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||||
|
and this project adheres to [Calendar Versioning](https://calver.org/) for major releases.
|
||||||
|
|
||||||
|
## [0.2.0] - 2026-02-07
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- **Fixed shell injection vulnerability** in SDK installation - replaced `create_subprocess_shell` with `create_subprocess_exec` using explicit argument lists
|
||||||
|
- **Added subprocess timeout** (5 minutes) to prevent hanging on malicious/corrupted assemblies
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **pytest test suite** with 35 tests covering models, metadata reader, and wrapper
|
||||||
|
- **PATH auto-discovery** for ilspycmd - now checks `~/.dotnet/tools` when not in PATH
|
||||||
|
- **FastMCP lifespan pattern** for proper dependency management
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **Version handling** now uses `importlib.metadata` - single source of truth in pyproject.toml
|
||||||
|
- **Error messages** standardized across all tools using `_format_error()` helper
|
||||||
|
- **Type parsing** improved with proper handling for nested types (`Outer+Nested`)
|
||||||
|
- **Imports cleaned up** - removed duplicate `re` imports, added top-level import
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Global mutable state** replaced with FastMCP lifespan context pattern
|
||||||
|
- **Type hints** added to all public methods
|
||||||
|
- **Regex parsing** now logs unparsed lines instead of silently ignoring
|
||||||
|
|
||||||
|
## [0.1.1] - 2026-02-06
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- dnfile-based metadata tools (search_methods, search_fields, search_properties, list_events, list_resources, get_metadata_summary)
|
||||||
|
- Platform-aware installation tool with SDK detection
|
||||||
|
- Improved README with badges and quick start guide
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- HeapItemBinary conversion in metadata reader
|
||||||
|
- Property and event listing with proper row index handling
|
||||||
|
|
||||||
|
## [0.1.0] - 2026-02-05
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Initial release
|
||||||
|
- FastMCP server wrapping ilspycmd
|
||||||
|
- Tools: decompile_assembly, list_types, generate_diagrammer, get_assembly_info
|
||||||
|
- check_ilspy_installation and install_ilspy helper tools
|
||||||
@ -1,9 +1,9 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "mcilspy"
|
name = "mcilspy"
|
||||||
version = "0.1.1"
|
version = "0.2.0"
|
||||||
description = "MCP Server for ILSpy .NET Decompiler"
|
description = "MCP Server for ILSpy .NET Decompiler"
|
||||||
authors = [
|
authors = [
|
||||||
{name = "Borealin", email = "me@borealin.cn"}
|
{name = "Ryan Malloy", email = "ryan@supported.systems"}
|
||||||
]
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"mcp>=1.0.0",
|
"mcp>=1.0.0",
|
||||||
@ -28,10 +28,17 @@ classifiers = [
|
|||||||
"Topic :: System :: Software Distribution",
|
"Topic :: System :: Software Distribution",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
dev = [
|
||||||
|
"pytest>=8.0.0",
|
||||||
|
"pytest-asyncio>=0.23.0",
|
||||||
|
"ruff>=0.4.0",
|
||||||
|
]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
Homepage = "https://github.com/Borealin/ilspy-mcp-server"
|
Homepage = "https://git.supported.systems/MCP/mcilspy"
|
||||||
Repository = "https://github.com/Borealin/ilspy-mcp-server.git"
|
Repository = "https://git.supported.systems/MCP/mcilspy.git"
|
||||||
Issues = "https://github.com/Borealin/ilspy-mcp-server/issues"
|
Issues = "https://git.supported.systems/MCP/mcilspy/issues"
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
mcilspy = "mcilspy.server:main"
|
mcilspy = "mcilspy.server:main"
|
||||||
@ -50,6 +57,11 @@ include = [
|
|||||||
"/LICENSE",
|
"/LICENSE",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
testpaths = ["tests"]
|
||||||
|
asyncio_mode = "auto"
|
||||||
|
asyncio_default_fixture_loop_scope = "function"
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
target-version = "py310"
|
target-version = "py310"
|
||||||
line-length = 100
|
line-length = 100
|
||||||
@ -60,4 +72,4 @@ select = ["E", "F", "W", "I", "UP", "B", "SIM"]
|
|||||||
ignore = ["E501"] # line length handled by formatter
|
ignore = ["E501"] # line length handled by formatter
|
||||||
|
|
||||||
[tool.ruff.format]
|
[tool.ruff.format]
|
||||||
quote-style = "double"
|
quote-style = "double"
|
||||||
|
|||||||
@ -1,3 +1,8 @@
|
|||||||
"""ILSpy MCP Server - Model Context Protocol server for .NET decompilation."""
|
"""ILSpy MCP Server - Model Context Protocol server for .NET decompilation."""
|
||||||
|
|
||||||
__version__ = "0.1.1"
|
try:
|
||||||
|
from importlib.metadata import version
|
||||||
|
|
||||||
|
__version__ = version("mcilspy")
|
||||||
|
except Exception:
|
||||||
|
__version__ = "0.0.0" # Fallback for editable installs without metadata
|
||||||
|
|||||||
@ -26,7 +26,7 @@ logger = logging.getLogger(__name__)
|
|||||||
class ILSpyWrapper:
|
class ILSpyWrapper:
|
||||||
"""Wrapper class for ILSpy command line tool."""
|
"""Wrapper class for ILSpy command line tool."""
|
||||||
|
|
||||||
def __init__(self, ilspycmd_path: str | None = None):
|
def __init__(self, ilspycmd_path: str | None = None) -> None:
|
||||||
"""Initialize the wrapper.
|
"""Initialize the wrapper.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -39,12 +39,39 @@ class ILSpyWrapper:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _find_ilspycmd(self) -> str | None:
|
def _find_ilspycmd(self) -> str | None:
|
||||||
"""Find ilspycmd executable in PATH."""
|
"""Find ilspycmd executable in PATH or common install locations.
|
||||||
# Try common names
|
|
||||||
|
Checks:
|
||||||
|
1. Standard PATH (via shutil.which)
|
||||||
|
2. ~/.dotnet/tools (default location for 'dotnet tool install --global')
|
||||||
|
3. Platform-specific locations
|
||||||
|
"""
|
||||||
|
# Try standard PATH first
|
||||||
for cmd_name in ["ilspycmd", "ilspycmd.exe"]:
|
for cmd_name in ["ilspycmd", "ilspycmd.exe"]:
|
||||||
path = shutil.which(cmd_name)
|
path = shutil.which(cmd_name)
|
||||||
if path:
|
if path:
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
# Check common dotnet tools locations (not always in PATH)
|
||||||
|
home = os.path.expanduser("~")
|
||||||
|
dotnet_tools_paths = [
|
||||||
|
os.path.join(home, ".dotnet", "tools", "ilspycmd"),
|
||||||
|
os.path.join(home, ".dotnet", "tools", "ilspycmd.exe"),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Windows-specific paths
|
||||||
|
if os.name == "nt":
|
||||||
|
userprofile = os.environ.get("USERPROFILE", "")
|
||||||
|
if userprofile:
|
||||||
|
dotnet_tools_paths.extend([
|
||||||
|
os.path.join(userprofile, ".dotnet", "tools", "ilspycmd.exe"),
|
||||||
|
])
|
||||||
|
|
||||||
|
for tool_path in dotnet_tools_paths:
|
||||||
|
if os.path.isfile(tool_path) and os.access(tool_path, os.X_OK):
|
||||||
|
logger.info(f"Found ilspycmd at {tool_path} (not in PATH)")
|
||||||
|
return tool_path
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def _run_command(
|
async def _run_command(
|
||||||
@ -79,7 +106,7 @@ class ILSpyWrapper:
|
|||||||
timeout=300.0 # 5 minutes
|
timeout=300.0 # 5 minutes
|
||||||
)
|
)
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
logger.warning(f"Command timed out after 5 minutes, killing process")
|
logger.warning("Command timed out after 5 minutes, killing process")
|
||||||
process.kill()
|
process.kill()
|
||||||
await process.wait() # Ensure process is cleaned up
|
await process.wait() # Ensure process is cleaned up
|
||||||
return -1, "", "Command timed out after 5 minutes. The assembly may be corrupted or too complex."
|
return -1, "", "Command timed out after 5 minutes. The assembly may be corrupted or too complex."
|
||||||
|
|||||||
@ -11,6 +11,7 @@ rather than traditional IntFlag enums, so we use those directly.
|
|||||||
import logging
|
import logging
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import dnfile
|
import dnfile
|
||||||
from dnfile.mdtable import TypeDefRow
|
from dnfile.mdtable import TypeDefRow
|
||||||
@ -103,7 +104,7 @@ class AssemblyMetadata:
|
|||||||
class MetadataReader:
|
class MetadataReader:
|
||||||
"""Read .NET assembly metadata directly using dnfile."""
|
"""Read .NET assembly metadata directly using dnfile."""
|
||||||
|
|
||||||
def __init__(self, assembly_path: str):
|
def __init__(self, assembly_path: str) -> None:
|
||||||
"""Initialize the metadata reader.
|
"""Initialize the metadata reader.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -232,7 +233,7 @@ class MetadataReader:
|
|||||||
referenced_assemblies=referenced_assemblies,
|
referenced_assemblies=referenced_assemblies,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _get_row_index(self, reference) -> int:
|
def _get_row_index(self, reference: Any) -> int:
|
||||||
"""Safely extract row_index from a metadata reference.
|
"""Safely extract row_index from a metadata reference.
|
||||||
|
|
||||||
dnfile references can be either objects with .row_index attribute
|
dnfile references can be either objects with .row_index attribute
|
||||||
@ -582,15 +583,15 @@ class MetadataReader:
|
|||||||
|
|
||||||
return resources
|
return resources
|
||||||
|
|
||||||
def close(self):
|
def close(self) -> None:
|
||||||
"""Close the PE file."""
|
"""Close the PE file."""
|
||||||
if self._pe:
|
if self._pe:
|
||||||
self._pe.close()
|
self._pe.close()
|
||||||
self._pe = None
|
self._pe = None
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self) -> "MetadataReader":
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
||||||
self.close()
|
self.close()
|
||||||
return False
|
return False
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import asyncio
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
@ -95,6 +96,33 @@ def _format_error(error: Exception, context: str = "") -> str:
|
|||||||
return f"**Error**: {error_msg}"
|
return f"**Error**: {error_msg}"
|
||||||
|
|
||||||
|
|
||||||
|
def _find_ilspycmd_path() -> str | None:
|
||||||
|
"""Find ilspycmd in PATH or common install locations."""
|
||||||
|
# Check PATH first
|
||||||
|
path = shutil.which("ilspycmd")
|
||||||
|
if path:
|
||||||
|
return path
|
||||||
|
|
||||||
|
# Check common dotnet tools locations (often not in PATH for MCP servers)
|
||||||
|
home = os.path.expanduser("~")
|
||||||
|
candidates = [
|
||||||
|
os.path.join(home, ".dotnet", "tools", "ilspycmd"),
|
||||||
|
os.path.join(home, ".dotnet", "tools", "ilspycmd.exe"),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Windows-specific
|
||||||
|
if os.name == "nt":
|
||||||
|
userprofile = os.environ.get("USERPROFILE", "")
|
||||||
|
if userprofile:
|
||||||
|
candidates.append(os.path.join(userprofile, ".dotnet", "tools", "ilspycmd.exe"))
|
||||||
|
|
||||||
|
for candidate in candidates:
|
||||||
|
if os.path.isfile(candidate) and os.access(candidate, os.X_OK):
|
||||||
|
return candidate
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
async def _check_dotnet_tools() -> dict:
|
async def _check_dotnet_tools() -> dict:
|
||||||
"""Check status of dotnet CLI and ilspycmd tool."""
|
"""Check status of dotnet CLI and ilspycmd tool."""
|
||||||
result = {
|
result = {
|
||||||
@ -122,14 +150,14 @@ async def _check_dotnet_tools() -> dict:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Check if ilspycmd is available
|
# Check if ilspycmd is available (check PATH and common locations)
|
||||||
ilspy_path = shutil.which("ilspycmd")
|
ilspy_path = _find_ilspycmd_path()
|
||||||
if ilspy_path:
|
if ilspy_path:
|
||||||
result["ilspycmd_available"] = True
|
result["ilspycmd_available"] = True
|
||||||
result["ilspycmd_path"] = ilspy_path
|
result["ilspycmd_path"] = ilspy_path
|
||||||
try:
|
try:
|
||||||
proc = await asyncio.create_subprocess_exec(
|
proc = await asyncio.create_subprocess_exec(
|
||||||
"ilspycmd",
|
ilspy_path,
|
||||||
"--version",
|
"--version",
|
||||||
stdout=asyncio.subprocess.PIPE,
|
stdout=asyncio.subprocess.PIPE,
|
||||||
stderr=asyncio.subprocess.PIPE,
|
stderr=asyncio.subprocess.PIPE,
|
||||||
@ -817,13 +845,11 @@ async def search_types(
|
|||||||
return response.error_message or "Failed to list types"
|
return response.error_message or "Failed to list types"
|
||||||
|
|
||||||
# Compile regex if needed
|
# Compile regex if needed
|
||||||
import re as regex_module
|
|
||||||
|
|
||||||
if use_regex:
|
if use_regex:
|
||||||
try:
|
try:
|
||||||
flags = 0 if case_sensitive else regex_module.IGNORECASE
|
flags = 0 if case_sensitive else re.IGNORECASE
|
||||||
search_pattern = regex_module.compile(pattern, flags)
|
search_pattern = re.compile(pattern, flags)
|
||||||
except regex_module.error as e:
|
except re.error as e:
|
||||||
return f"Invalid regex pattern: {e}"
|
return f"Invalid regex pattern: {e}"
|
||||||
else:
|
else:
|
||||||
search_pattern = None
|
search_pattern = None
|
||||||
@ -933,13 +959,11 @@ async def search_strings(
|
|||||||
source_code = response.source_code or ""
|
source_code = response.source_code or ""
|
||||||
|
|
||||||
# Compile regex if needed
|
# Compile regex if needed
|
||||||
import re as regex_module
|
|
||||||
|
|
||||||
if use_regex:
|
if use_regex:
|
||||||
try:
|
try:
|
||||||
flags = 0 if case_sensitive else regex_module.IGNORECASE
|
flags = 0 if case_sensitive else re.IGNORECASE
|
||||||
search_pattern = regex_module.compile(pattern, flags)
|
search_pattern = re.compile(pattern, flags)
|
||||||
except regex_module.error as e:
|
except re.error as e:
|
||||||
return f"Invalid regex pattern: {e}"
|
return f"Invalid regex pattern: {e}"
|
||||||
|
|
||||||
# Search for string literals containing the pattern
|
# Search for string literals containing the pattern
|
||||||
@ -952,14 +976,14 @@ async def search_strings(
|
|||||||
lines = source_code.split("\n")
|
lines = source_code.split("\n")
|
||||||
for i, line in enumerate(lines):
|
for i, line in enumerate(lines):
|
||||||
# Track current type/method context
|
# Track current type/method context
|
||||||
type_match = regex_module.match(
|
type_match = re.match(
|
||||||
r"^\s*(?:public|private|internal|protected)?\s*(?:class|struct|interface)\s+(\w+)",
|
r"^\s*(?:public|private|internal|protected)?\s*(?:class|struct|interface)\s+(\w+)",
|
||||||
line,
|
line,
|
||||||
)
|
)
|
||||||
if type_match:
|
if type_match:
|
||||||
current_type = type_match.group(1)
|
current_type = type_match.group(1)
|
||||||
|
|
||||||
method_match = regex_module.match(
|
method_match = re.match(
|
||||||
r"^\s*(?:public|private|internal|protected)?\s*(?:static\s+)?(?:\w+\s+)+(\w+)\s*\(",
|
r"^\s*(?:public|private|internal|protected)?\s*(?:static\s+)?(?:\w+\s+)+(\w+)\s*\(",
|
||||||
line,
|
line,
|
||||||
)
|
)
|
||||||
@ -1061,8 +1085,6 @@ async def search_methods(
|
|||||||
await ctx.info(f"Searching for methods matching '{pattern}' in: {assembly_path}")
|
await ctx.info(f"Searching for methods matching '{pattern}' in: {assembly_path}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import re as regex_module
|
|
||||||
|
|
||||||
from .metadata_reader import MetadataReader
|
from .metadata_reader import MetadataReader
|
||||||
|
|
||||||
with MetadataReader(assembly_path) as reader:
|
with MetadataReader(assembly_path) as reader:
|
||||||
@ -1078,9 +1100,9 @@ async def search_methods(
|
|||||||
# Compile regex if needed
|
# Compile regex if needed
|
||||||
if use_regex:
|
if use_regex:
|
||||||
try:
|
try:
|
||||||
flags = 0 if case_sensitive else regex_module.IGNORECASE
|
flags = 0 if case_sensitive else re.IGNORECASE
|
||||||
search_pattern = regex_module.compile(pattern, flags)
|
search_pattern = re.compile(pattern, flags)
|
||||||
except regex_module.error as e:
|
except re.error as e:
|
||||||
return f"Invalid regex pattern: {e}"
|
return f"Invalid regex pattern: {e}"
|
||||||
else:
|
else:
|
||||||
search_pattern = None
|
search_pattern = None
|
||||||
@ -1180,8 +1202,6 @@ async def search_fields(
|
|||||||
await ctx.info(f"Searching for fields matching '{pattern}' in: {assembly_path}")
|
await ctx.info(f"Searching for fields matching '{pattern}' in: {assembly_path}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import re as regex_module
|
|
||||||
|
|
||||||
from .metadata_reader import MetadataReader
|
from .metadata_reader import MetadataReader
|
||||||
|
|
||||||
with MetadataReader(assembly_path) as reader:
|
with MetadataReader(assembly_path) as reader:
|
||||||
@ -1198,9 +1218,9 @@ async def search_fields(
|
|||||||
# Compile regex if needed
|
# Compile regex if needed
|
||||||
if use_regex:
|
if use_regex:
|
||||||
try:
|
try:
|
||||||
flags = 0 if case_sensitive else regex_module.IGNORECASE
|
flags = 0 if case_sensitive else re.IGNORECASE
|
||||||
search_pattern = regex_module.compile(pattern, flags)
|
search_pattern = re.compile(pattern, flags)
|
||||||
except regex_module.error as e:
|
except re.error as e:
|
||||||
return f"Invalid regex pattern: {e}"
|
return f"Invalid regex pattern: {e}"
|
||||||
else:
|
else:
|
||||||
search_pattern = None
|
search_pattern = None
|
||||||
@ -1291,8 +1311,6 @@ async def search_properties(
|
|||||||
await ctx.info(f"Searching for properties matching '{pattern}' in: {assembly_path}")
|
await ctx.info(f"Searching for properties matching '{pattern}' in: {assembly_path}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import re as regex_module
|
|
||||||
|
|
||||||
from .metadata_reader import MetadataReader
|
from .metadata_reader import MetadataReader
|
||||||
|
|
||||||
with MetadataReader(assembly_path) as reader:
|
with MetadataReader(assembly_path) as reader:
|
||||||
@ -1307,9 +1325,9 @@ async def search_properties(
|
|||||||
# Compile regex if needed
|
# Compile regex if needed
|
||||||
if use_regex:
|
if use_regex:
|
||||||
try:
|
try:
|
||||||
flags = 0 if case_sensitive else regex_module.IGNORECASE
|
flags = 0 if case_sensitive else re.IGNORECASE
|
||||||
search_pattern = regex_module.compile(pattern, flags)
|
search_pattern = re.compile(pattern, flags)
|
||||||
except regex_module.error as e:
|
except re.error as e:
|
||||||
return f"Invalid regex pattern: {e}"
|
return f"Invalid regex pattern: {e}"
|
||||||
else:
|
else:
|
||||||
search_pattern = None
|
search_pattern = None
|
||||||
@ -1564,16 +1582,11 @@ def main():
|
|||||||
"""Entry point for the MCP server."""
|
"""Entry point for the MCP server."""
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
try:
|
from . import __version__
|
||||||
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 banner to stderr (stdout is reserved for MCP protocol)
|
||||||
print(
|
print(
|
||||||
f"🔬 mcilspy v{package_version} - .NET Assembly Decompiler MCP Server",
|
f"🔬 mcilspy v{__version__} - .NET Assembly Decompiler MCP Server",
|
||||||
file=sys.stderr,
|
file=sys.stderr,
|
||||||
)
|
)
|
||||||
mcp.run(transport="stdio")
|
mcp.run(transport="stdio")
|
||||||
|
|||||||
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
41
tests/conftest.py
Normal file
41
tests/conftest.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
"""Shared pytest fixtures for mcilspy tests."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def sample_assembly_path() -> str:
|
||||||
|
"""Return path to a .NET assembly for testing.
|
||||||
|
|
||||||
|
Uses a known .NET SDK assembly that should exist on systems with dotnet installed.
|
||||||
|
"""
|
||||||
|
# Try to find a .NET SDK assembly
|
||||||
|
dotnet_base = Path("/usr/share/dotnet/sdk")
|
||||||
|
if dotnet_base.exists():
|
||||||
|
# Find any SDK version
|
||||||
|
for sdk_dir in dotnet_base.iterdir():
|
||||||
|
test_dll = sdk_dir / "Sdks" / "Microsoft.NET.Sdk" / "tools" / "net10.0" / "Microsoft.NET.Build.Tasks.dll"
|
||||||
|
if test_dll.exists():
|
||||||
|
return str(test_dll)
|
||||||
|
# Try older paths
|
||||||
|
for net_version in ["net9.0", "net8.0", "net7.0", "net6.0"]:
|
||||||
|
test_dll = sdk_dir / "Sdks" / "Microsoft.NET.Sdk" / "tools" / net_version / "Microsoft.NET.Build.Tasks.dll"
|
||||||
|
if test_dll.exists():
|
||||||
|
return str(test_dll)
|
||||||
|
|
||||||
|
# Fallback: any .dll in dotnet directory
|
||||||
|
for root, dirs, files in os.walk("/usr/share/dotnet"):
|
||||||
|
for f in files:
|
||||||
|
if f.endswith(".dll"):
|
||||||
|
return os.path.join(root, f)
|
||||||
|
|
||||||
|
pytest.skip("No .NET assembly found for testing")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def nonexistent_path() -> str:
|
||||||
|
"""Return a path that doesn't exist."""
|
||||||
|
return "/nonexistent/path/to/assembly.dll"
|
||||||
176
tests/test_ilspy_wrapper.py
Normal file
176
tests/test_ilspy_wrapper.py
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
"""Tests for mcilspy.ilspy_wrapper module."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from mcilspy.ilspy_wrapper import ILSpyWrapper
|
||||||
|
from mcilspy.models import DecompileRequest, ListTypesRequest
|
||||||
|
|
||||||
|
|
||||||
|
class TestILSpyWrapperInit:
|
||||||
|
"""Tests for ILSpyWrapper initialization."""
|
||||||
|
|
||||||
|
def test_init_finds_ilspycmd_or_raises(self):
|
||||||
|
"""Test that init either finds ilspycmd or raises RuntimeError."""
|
||||||
|
try:
|
||||||
|
wrapper = ILSpyWrapper()
|
||||||
|
# If we get here, ilspycmd was found
|
||||||
|
assert wrapper.ilspycmd_path is not None
|
||||||
|
except RuntimeError as e:
|
||||||
|
# ilspycmd not installed - that's okay for testing
|
||||||
|
assert "ILSpyCmd not found" in str(e)
|
||||||
|
|
||||||
|
|
||||||
|
class TestILSpyWrapperTypeParsing:
|
||||||
|
"""Tests for type name parsing utilities."""
|
||||||
|
|
||||||
|
def test_parse_types_output_empty(self):
|
||||||
|
"""Test parsing empty output."""
|
||||||
|
# Create wrapper only if ilspycmd is available
|
||||||
|
try:
|
||||||
|
wrapper = ILSpyWrapper()
|
||||||
|
except RuntimeError:
|
||||||
|
pytest.skip("ilspycmd not installed")
|
||||||
|
|
||||||
|
types = wrapper._parse_types_output("")
|
||||||
|
assert types == []
|
||||||
|
|
||||||
|
def test_parse_types_output_single_type(self):
|
||||||
|
"""Test parsing single type."""
|
||||||
|
try:
|
||||||
|
wrapper = ILSpyWrapper()
|
||||||
|
except RuntimeError:
|
||||||
|
pytest.skip("ilspycmd not installed")
|
||||||
|
|
||||||
|
output = "Class: MyNamespace.MyClass"
|
||||||
|
types = wrapper._parse_types_output(output)
|
||||||
|
|
||||||
|
assert len(types) == 1
|
||||||
|
assert types[0].name == "MyClass"
|
||||||
|
assert types[0].namespace == "MyNamespace"
|
||||||
|
assert types[0].kind == "Class"
|
||||||
|
assert types[0].full_name == "MyNamespace.MyClass"
|
||||||
|
|
||||||
|
def test_parse_types_output_multiple_types(self):
|
||||||
|
"""Test parsing multiple types."""
|
||||||
|
try:
|
||||||
|
wrapper = ILSpyWrapper()
|
||||||
|
except RuntimeError:
|
||||||
|
pytest.skip("ilspycmd not installed")
|
||||||
|
|
||||||
|
output = """Class: NS.ClassA
|
||||||
|
Interface: NS.IService
|
||||||
|
Struct: NS.MyStruct
|
||||||
|
Enum: NS.MyEnum
|
||||||
|
Delegate: NS.MyDelegate"""
|
||||||
|
|
||||||
|
types = wrapper._parse_types_output(output)
|
||||||
|
assert len(types) == 5
|
||||||
|
|
||||||
|
# Check kinds
|
||||||
|
kinds = [t.kind for t in types]
|
||||||
|
assert "Class" in kinds
|
||||||
|
assert "Interface" in kinds
|
||||||
|
assert "Struct" in kinds
|
||||||
|
assert "Enum" in kinds
|
||||||
|
assert "Delegate" in kinds
|
||||||
|
|
||||||
|
def test_parse_types_output_nested_type(self):
|
||||||
|
"""Test parsing nested types."""
|
||||||
|
try:
|
||||||
|
wrapper = ILSpyWrapper()
|
||||||
|
except RuntimeError:
|
||||||
|
pytest.skip("ilspycmd not installed")
|
||||||
|
|
||||||
|
output = "Class: MyNamespace.Outer+Nested"
|
||||||
|
types = wrapper._parse_types_output(output)
|
||||||
|
|
||||||
|
assert len(types) == 1
|
||||||
|
assert types[0].name == "Outer+Nested"
|
||||||
|
assert types[0].namespace == "MyNamespace"
|
||||||
|
|
||||||
|
def test_parse_types_output_no_namespace(self):
|
||||||
|
"""Test parsing type with no namespace."""
|
||||||
|
try:
|
||||||
|
wrapper = ILSpyWrapper()
|
||||||
|
except RuntimeError:
|
||||||
|
pytest.skip("ilspycmd not installed")
|
||||||
|
|
||||||
|
output = "Class: MyClass"
|
||||||
|
types = wrapper._parse_types_output(output)
|
||||||
|
|
||||||
|
assert len(types) == 1
|
||||||
|
assert types[0].name == "MyClass"
|
||||||
|
assert types[0].namespace is None
|
||||||
|
|
||||||
|
def test_parse_types_output_skips_invalid_lines(self):
|
||||||
|
"""Test that invalid lines are skipped."""
|
||||||
|
try:
|
||||||
|
wrapper = ILSpyWrapper()
|
||||||
|
except RuntimeError:
|
||||||
|
pytest.skip("ilspycmd not installed")
|
||||||
|
|
||||||
|
output = """Class: NS.Valid
|
||||||
|
This is not a valid line
|
||||||
|
Another invalid line
|
||||||
|
Interface: NS.AlsoValid"""
|
||||||
|
|
||||||
|
types = wrapper._parse_types_output(output)
|
||||||
|
assert len(types) == 2
|
||||||
|
assert types[0].full_name == "NS.Valid"
|
||||||
|
assert types[1].full_name == "NS.AlsoValid"
|
||||||
|
|
||||||
|
|
||||||
|
class TestILSpyWrapperDecompile:
|
||||||
|
"""Tests for decompilation functionality."""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_decompile_nonexistent_assembly(self):
|
||||||
|
"""Test decompiling nonexistent assembly."""
|
||||||
|
try:
|
||||||
|
wrapper = ILSpyWrapper()
|
||||||
|
except RuntimeError:
|
||||||
|
pytest.skip("ilspycmd not installed")
|
||||||
|
|
||||||
|
request = DecompileRequest(assembly_path="/nonexistent/assembly.dll")
|
||||||
|
response = await wrapper.decompile(request)
|
||||||
|
|
||||||
|
assert response.success is False
|
||||||
|
assert "not found" in response.error_message.lower()
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_list_types_nonexistent_assembly(self):
|
||||||
|
"""Test listing types from nonexistent assembly."""
|
||||||
|
try:
|
||||||
|
wrapper = ILSpyWrapper()
|
||||||
|
except RuntimeError:
|
||||||
|
pytest.skip("ilspycmd not installed")
|
||||||
|
|
||||||
|
request = ListTypesRequest(assembly_path="/nonexistent/assembly.dll")
|
||||||
|
response = await wrapper.list_types(request)
|
||||||
|
|
||||||
|
assert response.success is False
|
||||||
|
assert "not found" in response.error_message.lower()
|
||||||
|
|
||||||
|
|
||||||
|
class TestILSpyWrapperHelpers:
|
||||||
|
"""Tests for ILSpyWrapper helper methods."""
|
||||||
|
|
||||||
|
def test_split_type_name_simple(self):
|
||||||
|
"""Test splitting simple type names."""
|
||||||
|
assert ILSpyWrapper._split_type_name("MyClass") == ("MyClass", None)
|
||||||
|
assert ILSpyWrapper._split_type_name("NS.MyClass") == ("MyClass", "NS")
|
||||||
|
assert ILSpyWrapper._split_type_name("Deep.NS.MyClass") == ("MyClass", "Deep.NS")
|
||||||
|
|
||||||
|
def test_split_type_name_nested(self):
|
||||||
|
"""Test splitting nested type names."""
|
||||||
|
assert ILSpyWrapper._split_type_name("NS.Outer+Nested") == ("Outer+Nested", "NS")
|
||||||
|
assert ILSpyWrapper._split_type_name("Outer+Nested") == ("Outer+Nested", None)
|
||||||
|
assert ILSpyWrapper._split_type_name("NS.A+B+C") == ("A+B+C", "NS")
|
||||||
|
|
||||||
|
def test_split_type_name_deep_namespace(self):
|
||||||
|
"""Test splitting types with deep namespaces."""
|
||||||
|
assert ILSpyWrapper._split_type_name("A.B.C.D.MyClass") == ("MyClass", "A.B.C.D")
|
||||||
|
|
||||||
|
def test_split_type_name_nested_with_deep_namespace(self):
|
||||||
|
"""Test nested types with deep namespaces."""
|
||||||
|
assert ILSpyWrapper._split_type_name("A.B.C.Outer+Inner") == ("Outer+Inner", "A.B.C")
|
||||||
112
tests/test_metadata_reader.py
Normal file
112
tests/test_metadata_reader.py
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
"""Tests for mcilspy.metadata_reader module."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from mcilspy.metadata_reader import MetadataReader
|
||||||
|
|
||||||
|
|
||||||
|
class TestMetadataReader:
|
||||||
|
"""Tests for MetadataReader class."""
|
||||||
|
|
||||||
|
def test_init_with_nonexistent_file(self, nonexistent_path):
|
||||||
|
"""Test that initializing with nonexistent file raises error."""
|
||||||
|
with pytest.raises(FileNotFoundError):
|
||||||
|
MetadataReader(nonexistent_path)
|
||||||
|
|
||||||
|
def test_context_manager(self, sample_assembly_path):
|
||||||
|
"""Test using MetadataReader as context manager."""
|
||||||
|
with MetadataReader(sample_assembly_path) as reader:
|
||||||
|
assert reader is not None
|
||||||
|
# Should be able to read metadata
|
||||||
|
meta = reader.get_assembly_metadata()
|
||||||
|
assert meta.name is not None
|
||||||
|
|
||||||
|
def test_get_assembly_metadata(self, sample_assembly_path):
|
||||||
|
"""Test reading assembly metadata."""
|
||||||
|
with MetadataReader(sample_assembly_path) as reader:
|
||||||
|
meta = reader.get_assembly_metadata()
|
||||||
|
|
||||||
|
# Basic fields should be populated
|
||||||
|
assert meta.name is not None
|
||||||
|
assert len(meta.name) > 0
|
||||||
|
assert meta.version is not None
|
||||||
|
assert meta.type_count >= 0
|
||||||
|
assert meta.method_count >= 0
|
||||||
|
|
||||||
|
def test_list_methods(self, sample_assembly_path):
|
||||||
|
"""Test listing methods from assembly."""
|
||||||
|
with MetadataReader(sample_assembly_path) as reader:
|
||||||
|
methods = reader.list_methods()
|
||||||
|
|
||||||
|
# Should find some methods
|
||||||
|
assert len(methods) > 0
|
||||||
|
|
||||||
|
# Each method should have required fields
|
||||||
|
for method in methods[:5]: # Check first 5
|
||||||
|
assert method.name is not None
|
||||||
|
assert method.declaring_type is not None
|
||||||
|
|
||||||
|
def test_list_methods_with_filter(self, sample_assembly_path):
|
||||||
|
"""Test listing methods with type filter."""
|
||||||
|
with MetadataReader(sample_assembly_path) as reader:
|
||||||
|
# First get unfiltered list
|
||||||
|
all_methods = reader.list_methods()
|
||||||
|
|
||||||
|
if len(all_methods) > 0:
|
||||||
|
# Get a type name from the first method
|
||||||
|
declaring_type = all_methods[0].declaring_type
|
||||||
|
|
||||||
|
# Filter by that type
|
||||||
|
filtered = reader.list_methods(type_filter=declaring_type)
|
||||||
|
|
||||||
|
# Filtered should be subset
|
||||||
|
assert len(filtered) <= len(all_methods)
|
||||||
|
# All filtered methods should contain the type name
|
||||||
|
for method in filtered:
|
||||||
|
assert declaring_type.lower() in method.declaring_type.lower()
|
||||||
|
|
||||||
|
def test_list_fields(self, sample_assembly_path):
|
||||||
|
"""Test listing fields from assembly."""
|
||||||
|
with MetadataReader(sample_assembly_path) as reader:
|
||||||
|
fields = reader.list_fields()
|
||||||
|
# Fields list may be empty for some assemblies
|
||||||
|
assert isinstance(fields, list)
|
||||||
|
|
||||||
|
def test_list_properties(self, sample_assembly_path):
|
||||||
|
"""Test listing properties from assembly."""
|
||||||
|
with MetadataReader(sample_assembly_path) as reader:
|
||||||
|
props = reader.list_properties()
|
||||||
|
assert isinstance(props, list)
|
||||||
|
|
||||||
|
def test_list_events(self, sample_assembly_path):
|
||||||
|
"""Test listing events from assembly."""
|
||||||
|
with MetadataReader(sample_assembly_path) as reader:
|
||||||
|
events = reader.list_events()
|
||||||
|
assert isinstance(events, list)
|
||||||
|
|
||||||
|
def test_list_resources(self, sample_assembly_path):
|
||||||
|
"""Test listing embedded resources from assembly."""
|
||||||
|
with MetadataReader(sample_assembly_path) as reader:
|
||||||
|
resources = reader.list_resources()
|
||||||
|
assert isinstance(resources, list)
|
||||||
|
|
||||||
|
|
||||||
|
class TestMetadataReaderHelpers:
|
||||||
|
"""Tests for MetadataReader helper methods."""
|
||||||
|
|
||||||
|
def test_split_type_name_simple(self):
|
||||||
|
"""Test splitting simple type names."""
|
||||||
|
# Import the wrapper to access the static method
|
||||||
|
from mcilspy.ilspy_wrapper import ILSpyWrapper
|
||||||
|
|
||||||
|
assert ILSpyWrapper._split_type_name("MyClass") == ("MyClass", None)
|
||||||
|
assert ILSpyWrapper._split_type_name("NS.MyClass") == ("MyClass", "NS")
|
||||||
|
assert ILSpyWrapper._split_type_name("Deep.NS.MyClass") == ("MyClass", "Deep.NS")
|
||||||
|
|
||||||
|
def test_split_type_name_nested(self):
|
||||||
|
"""Test splitting nested type names."""
|
||||||
|
from mcilspy.ilspy_wrapper import ILSpyWrapper
|
||||||
|
|
||||||
|
assert ILSpyWrapper._split_type_name("NS.Outer+Nested") == ("Outer+Nested", "NS")
|
||||||
|
assert ILSpyWrapper._split_type_name("Outer+Nested") == ("Outer+Nested", None)
|
||||||
|
assert ILSpyWrapper._split_type_name("NS.A+B+C") == ("A+B+C", "NS")
|
||||||
116
tests/test_models.py
Normal file
116
tests/test_models.py
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
"""Tests for mcilspy.models module."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from mcilspy.models import (
|
||||||
|
EntityType,
|
||||||
|
LanguageVersion,
|
||||||
|
DecompileRequest,
|
||||||
|
ListTypesRequest,
|
||||||
|
TypeInfo,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestEntityType:
|
||||||
|
"""Tests for EntityType enum."""
|
||||||
|
|
||||||
|
def test_from_string_full_names(self):
|
||||||
|
"""Test parsing full entity type names."""
|
||||||
|
assert EntityType.from_string("class") == EntityType.CLASS
|
||||||
|
assert EntityType.from_string("interface") == EntityType.INTERFACE
|
||||||
|
assert EntityType.from_string("struct") == EntityType.STRUCT
|
||||||
|
assert EntityType.from_string("delegate") == EntityType.DELEGATE
|
||||||
|
assert EntityType.from_string("enum") == EntityType.ENUM
|
||||||
|
|
||||||
|
def test_from_string_short_names(self):
|
||||||
|
"""Test parsing short (single letter) entity type names."""
|
||||||
|
assert EntityType.from_string("c") == EntityType.CLASS
|
||||||
|
assert EntityType.from_string("i") == EntityType.INTERFACE
|
||||||
|
assert EntityType.from_string("s") == EntityType.STRUCT
|
||||||
|
assert EntityType.from_string("d") == EntityType.DELEGATE
|
||||||
|
assert EntityType.from_string("e") == EntityType.ENUM
|
||||||
|
|
||||||
|
def test_from_string_case_insensitive(self):
|
||||||
|
"""Test that parsing is case insensitive."""
|
||||||
|
assert EntityType.from_string("CLASS") == EntityType.CLASS
|
||||||
|
assert EntityType.from_string("Class") == EntityType.CLASS
|
||||||
|
assert EntityType.from_string("C") == EntityType.CLASS
|
||||||
|
|
||||||
|
def test_from_string_invalid(self):
|
||||||
|
"""Test that invalid strings raise ValueError."""
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
EntityType.from_string("invalid")
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
EntityType.from_string("x")
|
||||||
|
|
||||||
|
|
||||||
|
class TestLanguageVersion:
|
||||||
|
"""Tests for LanguageVersion enum."""
|
||||||
|
|
||||||
|
def test_common_versions(self):
|
||||||
|
"""Test common C# version values."""
|
||||||
|
assert LanguageVersion.LATEST.value == "Latest"
|
||||||
|
assert LanguageVersion.PREVIEW.value == "Preview"
|
||||||
|
assert LanguageVersion.CSHARP12_0.value == "CSharp12_0"
|
||||||
|
|
||||||
|
def test_version_count(self):
|
||||||
|
"""Ensure we have all expected versions."""
|
||||||
|
# Should have CSharp1 through CSharp12, plus Latest and Preview
|
||||||
|
assert len(LanguageVersion) >= 14
|
||||||
|
|
||||||
|
|
||||||
|
class TestDecompileRequest:
|
||||||
|
"""Tests for DecompileRequest model."""
|
||||||
|
|
||||||
|
def test_minimal_request(self):
|
||||||
|
"""Test creating request with only required fields."""
|
||||||
|
request = DecompileRequest(assembly_path="/path/to/assembly.dll")
|
||||||
|
assert request.assembly_path == "/path/to/assembly.dll"
|
||||||
|
assert request.language_version == LanguageVersion.LATEST
|
||||||
|
assert request.type_name is None
|
||||||
|
assert request.output_dir is None
|
||||||
|
|
||||||
|
def test_full_request(self):
|
||||||
|
"""Test creating request with all fields."""
|
||||||
|
request = DecompileRequest(
|
||||||
|
assembly_path="/path/to/assembly.dll",
|
||||||
|
language_version=LanguageVersion.CSHARP10_0,
|
||||||
|
type_name="MyNamespace.MyClass",
|
||||||
|
output_dir="/output",
|
||||||
|
create_project=True,
|
||||||
|
show_il_code=True,
|
||||||
|
)
|
||||||
|
assert request.language_version == LanguageVersion.CSHARP10_0
|
||||||
|
assert request.type_name == "MyNamespace.MyClass"
|
||||||
|
assert request.create_project is True
|
||||||
|
|
||||||
|
|
||||||
|
class TestListTypesRequest:
|
||||||
|
"""Tests for ListTypesRequest model."""
|
||||||
|
|
||||||
|
def test_default_entity_types(self):
|
||||||
|
"""Test that default entity types include common types."""
|
||||||
|
request = ListTypesRequest(assembly_path="/path/to/assembly.dll")
|
||||||
|
# Should have some default entity types
|
||||||
|
assert len(request.entity_types) > 0
|
||||||
|
assert EntityType.CLASS in request.entity_types
|
||||||
|
|
||||||
|
|
||||||
|
class TestTypeInfo:
|
||||||
|
"""Tests for TypeInfo model."""
|
||||||
|
|
||||||
|
def test_type_info_creation(self):
|
||||||
|
"""Test creating TypeInfo objects."""
|
||||||
|
info = TypeInfo(
|
||||||
|
name="MyClass",
|
||||||
|
full_name="MyNamespace.MyClass",
|
||||||
|
kind="Class",
|
||||||
|
namespace="MyNamespace",
|
||||||
|
)
|
||||||
|
assert info.name == "MyClass"
|
||||||
|
assert info.namespace == "MyNamespace"
|
||||||
|
|
||||||
|
def test_type_info_no_namespace(self):
|
||||||
|
"""Test TypeInfo with no namespace."""
|
||||||
|
info = TypeInfo(name="MyClass", full_name="MyClass", kind="Class")
|
||||||
|
assert info.namespace is None
|
||||||
169
uv.lock
generated
169
uv.lock
generated
@ -34,6 +34,15 @@ 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" },
|
{ 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 = "backports-asyncio-runner"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "certifi"
|
name = "certifi"
|
||||||
version = "2026.1.4"
|
version = "2026.1.4"
|
||||||
@ -285,6 +294,15 @@ 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" },
|
{ 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 = "iniconfig"
|
||||||
|
version = "2.3.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jsonschema"
|
name = "jsonschema"
|
||||||
version = "4.26.0"
|
version = "4.26.0"
|
||||||
@ -314,7 +332,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mcilspy"
|
name = "mcilspy"
|
||||||
version = "0.1.1"
|
version = "0.2.0"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "dnfile" },
|
{ name = "dnfile" },
|
||||||
@ -322,12 +340,23 @@ dependencies = [
|
|||||||
{ name = "pydantic" },
|
{ name = "pydantic" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[package.optional-dependencies]
|
||||||
|
dev = [
|
||||||
|
{ name = "pytest" },
|
||||||
|
{ name = "pytest-asyncio" },
|
||||||
|
{ name = "ruff" },
|
||||||
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "dnfile", specifier = ">=0.15.0" },
|
{ name = "dnfile", specifier = ">=0.15.0" },
|
||||||
{ name = "mcp", specifier = ">=1.0.0" },
|
{ name = "mcp", specifier = ">=1.0.0" },
|
||||||
{ name = "pydantic", specifier = ">=2.7.0" },
|
{ name = "pydantic", specifier = ">=2.7.0" },
|
||||||
|
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" },
|
||||||
|
{ name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.23.0" },
|
||||||
|
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.4.0" },
|
||||||
]
|
]
|
||||||
|
provides-extras = ["dev"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mcp"
|
name = "mcp"
|
||||||
@ -354,6 +383,15 @@ 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" },
|
{ 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 = "packaging"
|
||||||
|
version = "26.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pefile"
|
name = "pefile"
|
||||||
version = "2024.8.26"
|
version = "2024.8.26"
|
||||||
@ -363,6 +401,15 @@ 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" },
|
{ 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 = "pluggy"
|
||||||
|
version = "1.6.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pycparser"
|
name = "pycparser"
|
||||||
version = "3.0"
|
version = "3.0"
|
||||||
@ -519,6 +566,15 @@ 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" },
|
{ 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 = "pygments"
|
||||||
|
version = "2.19.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyjwt"
|
name = "pyjwt"
|
||||||
version = "2.11.0"
|
version = "2.11.0"
|
||||||
@ -533,6 +589,38 @@ crypto = [
|
|||||||
{ name = "cryptography" },
|
{ name = "cryptography" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest"
|
||||||
|
version = "9.0.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
|
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
|
||||||
|
{ name = "iniconfig" },
|
||||||
|
{ name = "packaging" },
|
||||||
|
{ name = "pluggy" },
|
||||||
|
{ name = "pygments" },
|
||||||
|
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-asyncio"
|
||||||
|
version = "1.3.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" },
|
||||||
|
{ name = "pytest" },
|
||||||
|
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-dotenv"
|
name = "python-dotenv"
|
||||||
version = "1.2.1"
|
version = "1.2.1"
|
||||||
@ -709,6 +797,31 @@ wheels = [
|
|||||||
{ 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" },
|
{ 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 = "ruff"
|
||||||
|
version = "0.15.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c8/39/5cee96809fbca590abea6b46c6d1c586b49663d1d2830a751cc8fc42c666/ruff-0.15.0.tar.gz", hash = "sha256:6bdea47cdbea30d40f8f8d7d69c0854ba7c15420ec75a26f463290949d7f7e9a", size = 4524893, upload-time = "2026-02-03T17:53:35.357Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bc/88/3fd1b0aa4b6330d6aaa63a285bc96c9f71970351579152d231ed90914586/ruff-0.15.0-py3-none-linux_armv6l.whl", hash = "sha256:aac4ebaa612a82b23d45964586f24ae9bc23ca101919f5590bdb368d74ad5455", size = 10354332, upload-time = "2026-02-03T17:52:54.892Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/72/f6/62e173fbb7eb75cc29fe2576a1e20f0a46f671a2587b5f604bfb0eaf5f6f/ruff-0.15.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dcd4be7cc75cfbbca24a98d04d0b9b36a270d0833241f776b788d59f4142b14d", size = 10767189, upload-time = "2026-02-03T17:53:19.778Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/99/e4/968ae17b676d1d2ff101d56dc69cf333e3a4c985e1ec23803df84fc7bf9e/ruff-0.15.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d747e3319b2bce179c7c1eaad3d884dc0a199b5f4d5187620530adf9105268ce", size = 10075384, upload-time = "2026-02-03T17:53:29.241Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a2/bf/9843c6044ab9e20af879c751487e61333ca79a2c8c3058b15722386b8cae/ruff-0.15.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:650bd9c56ae03102c51a5e4b554d74d825ff3abe4db22b90fd32d816c2e90621", size = 10481363, upload-time = "2026-02-03T17:52:43.332Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/55/d9/4ada5ccf4cd1f532db1c8d44b6f664f2208d3d93acbeec18f82315e15193/ruff-0.15.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6664b7eac559e3048223a2da77769c2f92b43a6dfd4720cef42654299a599c9", size = 10187736, upload-time = "2026-02-03T17:53:00.522Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/86/e2/f25eaecd446af7bb132af0a1d5b135a62971a41f5366ff41d06d25e77a91/ruff-0.15.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f811f97b0f092b35320d1556f3353bf238763420ade5d9e62ebd2b73f2ff179", size = 10968415, upload-time = "2026-02-03T17:53:15.705Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e7/dc/f06a8558d06333bf79b497d29a50c3a673d9251214e0d7ec78f90b30aa79/ruff-0.15.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:761ec0a66680fab6454236635a39abaf14198818c8cdf691e036f4bc0f406b2d", size = 11809643, upload-time = "2026-02-03T17:53:23.031Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dd/45/0ece8db2c474ad7df13af3a6d50f76e22a09d078af63078f005057ca59eb/ruff-0.15.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:940f11c2604d317e797b289f4f9f3fa5555ffe4fb574b55ed006c3d9b6f0eb78", size = 11234787, upload-time = "2026-02-03T17:52:46.432Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8a/d9/0e3a81467a120fd265658d127db648e4d3acfe3e4f6f5d4ea79fac47e587/ruff-0.15.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcbca3d40558789126da91d7ef9a7c87772ee107033db7191edefa34e2c7f1b4", size = 11112797, upload-time = "2026-02-03T17:52:49.274Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b2/cb/8c0b3b0c692683f8ff31351dfb6241047fa873a4481a76df4335a8bff716/ruff-0.15.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9a121a96db1d75fa3eb39c4539e607f628920dd72ff1f7c5ee4f1b768ac62d6e", size = 11033133, upload-time = "2026-02-03T17:53:33.105Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f8/5e/23b87370cf0f9081a8c89a753e69a4e8778805b8802ccfe175cc410e50b9/ruff-0.15.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5298d518e493061f2eabd4abd067c7e4fb89e2f63291c94332e35631c07c3662", size = 10442646, upload-time = "2026-02-03T17:53:06.278Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e1/9a/3c94de5ce642830167e6d00b5c75aacd73e6347b4c7fc6828699b150a5ee/ruff-0.15.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:afb6e603d6375ff0d6b0cee563fa21ab570fd15e65c852cb24922cef25050cf1", size = 10195750, upload-time = "2026-02-03T17:53:26.084Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/30/15/e396325080d600b436acc970848d69df9c13977942fb62bb8722d729bee8/ruff-0.15.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:77e515f6b15f828b94dc17d2b4ace334c9ddb7d9468c54b2f9ed2b9c1593ef16", size = 10676120, upload-time = "2026-02-03T17:53:09.363Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8d/c9/229a23d52a2983de1ad0fb0ee37d36e0257e6f28bfd6b498ee2c76361874/ruff-0.15.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6f6e80850a01eb13b3e42ee0ebdf6e4497151b48c35051aab51c101266d187a3", size = 11201636, upload-time = "2026-02-03T17:52:57.281Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6f/b0/69adf22f4e24f3677208adb715c578266842e6e6a3cc77483f48dd999ede/ruff-0.15.0-py3-none-win32.whl", hash = "sha256:238a717ef803e501b6d51e0bdd0d2c6e8513fe9eec14002445134d3907cd46c3", size = 10465945, upload-time = "2026-02-03T17:53:12.591Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/51/ad/f813b6e2c97e9b4598be25e94a9147b9af7e60523b0cb5d94d307c15229d/ruff-0.15.0-py3-none-win_amd64.whl", hash = "sha256:dd5e4d3301dc01de614da3cdffc33d4b1b96fb89e45721f1598e5532ccf78b18", size = 11564657, upload-time = "2026-02-03T17:52:51.893Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f6/b0/2d823f6e77ebe560f4e397d078487e8d52c1516b331e3521bc75db4272ca/ruff-0.15.0-py3-none-win_arm64.whl", hash = "sha256:c480d632cc0ca3f0727acac8b7d053542d9e114a462a145d0b00e7cd658c515a", size = 10865753, upload-time = "2026-02-03T17:53:03.014Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sse-starlette"
|
name = "sse-starlette"
|
||||||
version = "3.2.0"
|
version = "3.2.0"
|
||||||
@ -735,6 +848,60 @@ 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" },
|
{ 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 = "tomli"
|
||||||
|
version = "2.4.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typing-extensions"
|
name = "typing-extensions"
|
||||||
version = "4.15.0"
|
version = "4.15.0"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user