
- Add ServerSettings class with pydantic-settings for type-safe configuration - Support multiple PyPI mirror sources with priority-based fallback mechanism - Implement RepositoryConfig and RepositoryManager for multi-repository support - Add environment variable support for all configuration options - Include private repository authentication configuration - Add advanced dependency analysis settings (max depth, concurrency, security) - Provide secure credential management with sensitive data masking - Update documentation and configuration examples - Add comprehensive test suite with 23 test cases covering all features - Include demo script showcasing multi-mirror configuration capabilities Configuration features: - Primary, additional, and fallback index URLs - Automatic duplicate URL removal with priority preservation - Runtime configuration reloading - Integration with repository manager for seamless multi-source queries Signed-off-by: longhao <hal.long@outlook.com>
283 lines
10 KiB
Python
283 lines
10 KiB
Python
"""Python version compatibility checking tools for PyPI MCP server."""
|
|
|
|
import logging
|
|
from typing import Any
|
|
|
|
from ..core import InvalidPackageNameError, NetworkError, PyPIClient, PyPIError
|
|
from ..core.version_utils import VersionCompatibility
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
async def check_python_compatibility(
|
|
package_name: str, target_python_version: str, use_cache: bool = True
|
|
) -> dict[str, Any]:
|
|
"""Check if a package is compatible with a specific Python version.
|
|
|
|
Args:
|
|
package_name: Name of the package to check
|
|
target_python_version: Target Python version (e.g., "3.9", "3.10.5")
|
|
use_cache: Whether to use cached package data
|
|
|
|
Returns:
|
|
Dictionary containing compatibility information
|
|
|
|
Raises:
|
|
InvalidPackageNameError: If package name is invalid
|
|
PackageNotFoundError: If package is not found
|
|
NetworkError: For network-related errors
|
|
"""
|
|
if not package_name or not package_name.strip():
|
|
raise InvalidPackageNameError(package_name)
|
|
|
|
if not target_python_version or not target_python_version.strip():
|
|
raise ValueError("Target Python version cannot be empty")
|
|
|
|
logger.info(
|
|
f"Checking Python {target_python_version} compatibility for package: {package_name}"
|
|
)
|
|
|
|
try:
|
|
async with PyPIClient() as client:
|
|
package_data = await client.get_package_info(package_name, use_cache)
|
|
|
|
info = package_data.get("info", {})
|
|
requires_python = info.get("requires_python")
|
|
classifiers = info.get("classifiers", [])
|
|
|
|
# Perform compatibility check
|
|
compat_checker = VersionCompatibility()
|
|
result = compat_checker.check_version_compatibility(
|
|
target_python_version, requires_python, classifiers
|
|
)
|
|
|
|
# Add package information to result
|
|
result.update(
|
|
{
|
|
"package_name": info.get("name", package_name),
|
|
"package_version": info.get("version", ""),
|
|
"requires_python": requires_python,
|
|
"supported_implementations": compat_checker.extract_python_implementations(
|
|
classifiers
|
|
),
|
|
"classifier_versions": sorted(
|
|
compat_checker.extract_python_versions_from_classifiers(
|
|
classifiers
|
|
)
|
|
),
|
|
}
|
|
)
|
|
|
|
return result
|
|
|
|
except PyPIError:
|
|
# Re-raise PyPI-specific errors
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Unexpected error checking compatibility for {package_name}: {e}")
|
|
raise NetworkError(f"Failed to check Python compatibility: {e}", e) from e
|
|
|
|
|
|
async def get_compatible_python_versions(
|
|
package_name: str, python_versions: list[str] | None = None, use_cache: bool = True
|
|
) -> dict[str, Any]:
|
|
"""Get list of Python versions compatible with a package.
|
|
|
|
Args:
|
|
package_name: Name of the package to check
|
|
python_versions: List of Python versions to check (optional)
|
|
use_cache: Whether to use cached package data
|
|
|
|
Returns:
|
|
Dictionary containing compatibility information for multiple versions
|
|
|
|
Raises:
|
|
InvalidPackageNameError: If package name is invalid
|
|
PackageNotFoundError: If package is not found
|
|
NetworkError: For network-related errors
|
|
"""
|
|
if not package_name or not package_name.strip():
|
|
raise InvalidPackageNameError(package_name)
|
|
|
|
logger.info(f"Getting compatible Python versions for package: {package_name}")
|
|
|
|
try:
|
|
async with PyPIClient() as client:
|
|
package_data = await client.get_package_info(package_name, use_cache)
|
|
|
|
info = package_data.get("info", {})
|
|
requires_python = info.get("requires_python")
|
|
classifiers = info.get("classifiers", [])
|
|
|
|
# Get compatibility information
|
|
compat_checker = VersionCompatibility()
|
|
result = compat_checker.get_compatible_versions(
|
|
requires_python, classifiers, python_versions
|
|
)
|
|
|
|
# Add package information to result
|
|
result.update(
|
|
{
|
|
"package_name": info.get("name", package_name),
|
|
"package_version": info.get("version", ""),
|
|
"requires_python": requires_python,
|
|
"supported_implementations": sorted(
|
|
compat_checker.extract_python_implementations(classifiers)
|
|
),
|
|
"classifier_versions": sorted(
|
|
compat_checker.extract_python_versions_from_classifiers(
|
|
classifiers
|
|
)
|
|
),
|
|
}
|
|
)
|
|
|
|
return result
|
|
|
|
except PyPIError:
|
|
# Re-raise PyPI-specific errors
|
|
raise
|
|
except Exception as e:
|
|
logger.error(
|
|
f"Unexpected error getting compatible versions for {package_name}: {e}"
|
|
)
|
|
raise NetworkError(f"Failed to get compatible Python versions: {e}", e) from e
|
|
|
|
|
|
async def suggest_python_version_for_packages(
|
|
package_names: list[str], use_cache: bool = True
|
|
) -> dict[str, Any]:
|
|
"""Suggest optimal Python version for a list of packages.
|
|
|
|
Args:
|
|
package_names: List of package names to analyze
|
|
use_cache: Whether to use cached package data
|
|
|
|
Returns:
|
|
Dictionary containing version suggestions and compatibility matrix
|
|
|
|
Raises:
|
|
ValueError: If package_names is empty
|
|
NetworkError: For network-related errors
|
|
"""
|
|
if not package_names:
|
|
raise ValueError("Package names list cannot be empty")
|
|
|
|
logger.info(
|
|
f"Analyzing Python version compatibility for {len(package_names)} packages"
|
|
)
|
|
|
|
# Default Python versions to analyze
|
|
python_versions = ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
|
|
|
|
compatibility_matrix = {}
|
|
package_details = {}
|
|
errors = {}
|
|
|
|
async with PyPIClient() as client:
|
|
for package_name in package_names:
|
|
try:
|
|
package_data = await client.get_package_info(package_name, use_cache)
|
|
info = package_data.get("info", {})
|
|
|
|
requires_python = info.get("requires_python")
|
|
classifiers = info.get("classifiers", [])
|
|
|
|
compat_checker = VersionCompatibility()
|
|
compat_result = compat_checker.get_compatible_versions(
|
|
requires_python, classifiers, python_versions
|
|
)
|
|
|
|
# Store compatibility for this package
|
|
compatible_versions = [
|
|
v["version"] for v in compat_result["compatible_versions"]
|
|
]
|
|
compatibility_matrix[package_name] = compatible_versions
|
|
|
|
package_details[package_name] = {
|
|
"version": info.get("version", ""),
|
|
"requires_python": requires_python,
|
|
"compatible_versions": compatible_versions,
|
|
"compatibility_rate": compat_result["compatibility_rate"],
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.warning(f"Failed to analyze package {package_name}: {e}")
|
|
errors[package_name] = str(e)
|
|
compatibility_matrix[package_name] = []
|
|
|
|
# Find common compatible versions
|
|
if compatibility_matrix:
|
|
all_versions = set(python_versions)
|
|
common_versions = all_versions.copy()
|
|
|
|
for _package_name, compatible in compatibility_matrix.items():
|
|
if compatible: # Only consider packages with known compatibility
|
|
common_versions &= set(compatible)
|
|
else:
|
|
common_versions = set()
|
|
|
|
# Generate recommendations
|
|
recommendations = []
|
|
if common_versions:
|
|
latest_common = max(
|
|
common_versions, key=lambda x: tuple(map(int, x.split(".")))
|
|
)
|
|
recommendations.append(
|
|
f"✅ Recommended Python version: {latest_common} "
|
|
f"(compatible with all {len([p for p in compatibility_matrix if compatibility_matrix[p]])} packages)"
|
|
)
|
|
|
|
if len(common_versions) > 1:
|
|
all_common = sorted(
|
|
common_versions, key=lambda x: tuple(map(int, x.split(".")))
|
|
)
|
|
recommendations.append(
|
|
f"📋 All compatible versions: {', '.join(all_common)}"
|
|
)
|
|
else:
|
|
recommendations.append(
|
|
"⚠️ No Python version is compatible with all packages. "
|
|
"Consider updating packages or using different versions."
|
|
)
|
|
|
|
# Find the version compatible with most packages
|
|
version_scores = {}
|
|
for version in python_versions:
|
|
score = sum(
|
|
1
|
|
for compatible in compatibility_matrix.values()
|
|
if version in compatible
|
|
)
|
|
version_scores[version] = score
|
|
|
|
if version_scores:
|
|
best_version = max(version_scores, key=version_scores.get)
|
|
best_score = version_scores[best_version]
|
|
total_packages = len(
|
|
[p for p in compatibility_matrix if compatibility_matrix[p]]
|
|
)
|
|
|
|
if best_score > 0:
|
|
recommendations.append(
|
|
f"📊 Best compromise: Python {best_version} "
|
|
f"(compatible with {best_score}/{total_packages} packages)"
|
|
)
|
|
|
|
return {
|
|
"analyzed_packages": len(package_names),
|
|
"successful_analyses": len(package_details),
|
|
"failed_analyses": len(errors),
|
|
"common_compatible_versions": sorted(common_versions),
|
|
"recommended_version": max(
|
|
common_versions, key=lambda x: tuple(map(int, x.split(".")))
|
|
)
|
|
if common_versions
|
|
else None,
|
|
"compatibility_matrix": compatibility_matrix,
|
|
"package_details": package_details,
|
|
"errors": errors,
|
|
"recommendations": recommendations,
|
|
"python_versions_analyzed": python_versions,
|
|
}
|