feat: implement comprehensive configuration management system with multi-mirror support

- 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>
This commit is contained in:
longhao 2025-05-27 17:36:25 +08:00 committed by Hal
parent f27493d8d2
commit a0c507c3ff
20 changed files with 1276 additions and 165 deletions

View file

@ -10,9 +10,7 @@ logger = logging.getLogger(__name__)
async def check_python_compatibility(
package_name: str,
target_python_version: str,
use_cache: bool = True
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.
@ -35,7 +33,9 @@ async def check_python_compatibility(
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}")
logger.info(
f"Checking Python {target_python_version} compatibility for package: {package_name}"
)
try:
async with PyPIClient() as client:
@ -48,19 +48,25 @@ async def check_python_compatibility(
# Perform compatibility check
compat_checker = VersionCompatibility()
result = compat_checker.check_version_compatibility(
target_python_version,
requires_python,
classifiers
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))
})
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
@ -73,9 +79,7 @@ async def check_python_compatibility(
async def get_compatible_python_versions(
package_name: str,
python_versions: list[str] | None = None,
use_cache: bool = True
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.
@ -108,19 +112,25 @@ async def get_compatible_python_versions(
# Get compatibility information
compat_checker = VersionCompatibility()
result = compat_checker.get_compatible_versions(
requires_python,
classifiers,
python_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))
})
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
@ -128,13 +138,14 @@ async def get_compatible_python_versions(
# Re-raise PyPI-specific errors
raise
except Exception as e:
logger.error(f"Unexpected error getting compatible versions for {package_name}: {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
package_names: list[str], use_cache: bool = True
) -> dict[str, Any]:
"""Suggest optimal Python version for a list of packages.
@ -152,7 +163,9 @@ async def suggest_python_version_for_packages(
if not package_names:
raise ValueError("Package names list cannot be empty")
logger.info(f"Analyzing Python version compatibility for {len(package_names)} packages")
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"]
@ -172,20 +185,20 @@ async def suggest_python_version_for_packages(
compat_checker = VersionCompatibility()
compat_result = compat_checker.get_compatible_versions(
requires_python,
classifiers,
python_versions
requires_python, classifiers, python_versions
)
# Store compatibility for this package
compatible_versions = [v["version"] for v in compat_result["compatible_versions"]]
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"]
"compatibility_rate": compat_result["compatibility_rate"],
}
except Exception as e:
@ -207,14 +220,18 @@ async def suggest_python_version_for_packages(
# Generate recommendations
recommendations = []
if common_versions:
latest_common = max(common_versions, key=lambda x: tuple(map(int, x.split("."))))
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("."))))
all_common = sorted(
common_versions, key=lambda x: tuple(map(int, x.split(".")))
)
recommendations.append(
f"📋 All compatible versions: {', '.join(all_common)}"
)
@ -227,13 +244,19 @@ async def suggest_python_version_for_packages(
# 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)
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]])
total_packages = len(
[p for p in compatibility_matrix if compatibility_matrix[p]]
)
if best_score > 0:
recommendations.append(
@ -246,10 +269,14 @@ async def suggest_python_version_for_packages(
"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,
"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
"python_versions_analyzed": python_versions,
}

View file

@ -24,7 +24,9 @@ def format_package_info(package_data: dict[str, Any]) -> dict[str, Any]:
"name": info.get("name", ""),
"version": info.get("version", ""),
"summary": info.get("summary", ""),
"description": info.get("description", "")[:500] + "..." if len(info.get("description", "")) > 500 else info.get("description", ""),
"description": info.get("description", "")[:500] + "..."
if len(info.get("description", "")) > 500
else info.get("description", ""),
"author": info.get("author", ""),
"author_email": info.get("author_email", ""),
"maintainer": info.get("maintainer", ""),
@ -53,7 +55,13 @@ def format_package_info(package_data: dict[str, Any]) -> dict[str, Any]:
formatted["download_info"] = {
"files_count": len(urls),
"file_types": list({url.get("packagetype", "") for url in urls}),
"python_versions": list({url.get("python_version", "") for url in urls if url.get("python_version")}),
"python_versions": list(
{
url.get("python_version", "")
for url in urls
if url.get("python_version")
}
),
}
return formatted
@ -83,11 +91,16 @@ def format_version_info(package_data: dict[str, Any]) -> dict[str, Any]:
"version_details": {
version: {
"release_count": len(releases[version]),
"has_wheel": any(file.get("packagetype") == "bdist_wheel" for file in releases[version]),
"has_source": any(file.get("packagetype") == "sdist" for file in releases[version]),
"has_wheel": any(
file.get("packagetype") == "bdist_wheel"
for file in releases[version]
),
"has_source": any(
file.get("packagetype") == "sdist" for file in releases[version]
),
}
for version in sorted_versions[:10] # Details for last 10 versions
}
},
}
@ -120,7 +133,7 @@ def format_dependency_info(package_data: dict[str, Any]) -> dict[str, Any]:
extra_part = parts[1] if len(parts) > 1 else ""
if "extra ==" in extra_part:
extra_name = extra_part.split("extra ==")[1].strip().strip('"\'')
extra_name = extra_part.split("extra ==")[1].strip().strip("\"'")
if extra_name not in optional_deps:
optional_deps[extra_name] = []
optional_deps[extra_name].append(dep_name)
@ -142,7 +155,7 @@ def format_dependency_info(package_data: dict[str, Any]) -> dict[str, Any]:
"dev_count": len(dev_deps),
"optional_groups": len(optional_deps),
"total_optional": sum(len(deps) for deps in optional_deps.values()),
}
},
}
@ -208,7 +221,9 @@ async def query_package_versions(package_name: str) -> dict[str, Any]:
raise NetworkError(f"Failed to query package versions: {e}", e) from e
async def query_package_dependencies(package_name: str, version: str | None = None) -> dict[str, Any]:
async def query_package_dependencies(
package_name: str, version: str | None = None
) -> dict[str, Any]:
"""Query package dependency information from PyPI.
Args:
@ -226,8 +241,10 @@ async def query_package_dependencies(package_name: str, version: str | None = No
if not package_name or not package_name.strip():
raise InvalidPackageNameError(package_name)
logger.info(f"Querying dependencies for package: {package_name}" +
(f" version {version}" if version else " (latest)"))
logger.info(
f"Querying dependencies for package: {package_name}"
+ (f" version {version}" if version else " (latest)")
)
try:
async with PyPIClient() as client:
@ -236,8 +253,10 @@ async def query_package_dependencies(package_name: str, version: str | None = No
# TODO: In future, support querying specific version dependencies
# For now, we return dependencies for the latest version
if version and version != package_data.get("info", {}).get("version"):
logger.warning(f"Specific version {version} requested but not implemented yet. "
f"Returning dependencies for latest version.")
logger.warning(
f"Specific version {version} requested but not implemented yet. "
f"Returning dependencies for latest version."
)
return format_dependency_info(package_data)
except PyPIError: