fix: resolve all lint issues and fix failing tests

- Fix blank line whitespace issues (W293) using ruff --unsafe-fixes
- Reformat code using ruff format for consistent styling
- Fix analyze_package_quality function to return list[Message] instead of string
- Add missing 'assessment' keyword to package analysis template
- Update tests to use real prompt functions instead of mocks for structure validation
- Fix import ordering in test files
- All 64 tests now pass with 47% code coverage

Signed-off-by: longhao <hal.long@outlook.com>
This commit is contained in:
longhao 2025-05-29 18:38:10 +08:00 committed by Hal
parent d63ef02ef3
commit a28d999958
18 changed files with 554 additions and 390 deletions

View file

@ -41,9 +41,7 @@ class DependencyParser:
return requirements
def filter_requirements_by_python_version(
self,
requirements: list[Requirement],
python_version: str
self, requirements: list[Requirement], python_version: str
) -> list[Requirement]:
"""Filter requirements based on Python version.
@ -68,7 +66,9 @@ class DependencyParser:
return filtered
def _is_requirement_applicable(self, req: Requirement, python_version: Version) -> bool:
def _is_requirement_applicable(
self, req: Requirement, python_version: Version
) -> bool:
"""Check if a requirement is applicable for the given Python version.
Args:
@ -83,12 +83,12 @@ class DependencyParser:
# Create environment for marker evaluation
env = {
'python_version': str(python_version),
'python_full_version': str(python_version),
'platform_system': 'Linux', # Default assumption
'platform_machine': 'x86_64', # Default assumption
'implementation_name': 'cpython',
'implementation_version': str(python_version),
"python_version": str(python_version),
"python_full_version": str(python_version),
"platform_system": "Linux", # Default assumption
"platform_machine": "x86_64", # Default assumption
"implementation_name": "cpython",
"implementation_version": str(python_version),
}
try:
@ -98,8 +98,7 @@ class DependencyParser:
return True # Include by default if evaluation fails
def categorize_dependencies(
self,
requirements: list[Requirement]
self, requirements: list[Requirement]
) -> dict[str, list[Requirement]]:
"""Categorize dependencies into runtime, development, and optional groups.
@ -109,36 +108,34 @@ class DependencyParser:
Returns:
Dictionary with categorized dependencies
"""
categories = {
'runtime': [],
'development': [],
'optional': {},
'extras': {}
}
categories = {"runtime": [], "development": [], "optional": {}, "extras": {}}
for req in requirements:
if not req.marker:
# No marker means it's a runtime dependency
categories['runtime'].append(req)
categories["runtime"].append(req)
continue
marker_str = str(req.marker)
# Check for extra dependencies
if 'extra ==' in marker_str:
if "extra ==" in marker_str:
extra_match = re.search(r'extra\s*==\s*["\']([^"\']+)["\']', marker_str)
if extra_match:
extra_name = extra_match.group(1)
if extra_name not in categories['extras']:
categories['extras'][extra_name] = []
categories['extras'][extra_name].append(req)
if extra_name not in categories["extras"]:
categories["extras"][extra_name] = []
categories["extras"][extra_name].append(req)
continue
# Check for development dependencies
if any(keyword in marker_str.lower() for keyword in ['dev', 'test', 'lint', 'doc']):
categories['development'].append(req)
if any(
keyword in marker_str.lower()
for keyword in ["dev", "test", "lint", "doc"]
):
categories["development"].append(req)
else:
categories['runtime'].append(req)
categories["runtime"].append(req)
return categories
@ -163,17 +160,16 @@ class DependencyParser:
Dictionary with version constraint information
"""
if not req.specifier:
return {'constraints': [], 'allows_any': True}
return {"constraints": [], "allows_any": True}
constraints = []
for spec in req.specifier:
constraints.append({
'operator': spec.operator,
'version': str(spec.version)
})
constraints.append(
{"operator": spec.operator, "version": str(spec.version)}
)
return {
'constraints': constraints,
'allows_any': len(constraints) == 0,
'specifier_str': str(req.specifier)
"constraints": constraints,
"allows_any": len(constraints) == 0,
"specifier_str": str(req.specifier),
}

View file

@ -87,12 +87,15 @@ class PyPIStatsClient:
def _get_cache_key(self, endpoint: str, package_name: str = "", **params) -> str:
"""Generate cache key for API data."""
param_str = "&".join(f"{k}={v}" for k, v in sorted(params.items()) if v is not None)
param_str = "&".join(
f"{k}={v}" for k, v in sorted(params.items()) if v is not None
)
return f"{endpoint}:{package_name}:{param_str}"
def _is_cache_valid(self, cache_entry: dict[str, Any]) -> bool:
"""Check if cache entry is still valid."""
import time
return time.time() - cache_entry.get("timestamp", 0) < self._cache_ttl
async def _make_request(self, url: str) -> dict[str, Any]:
@ -187,13 +190,16 @@ class PyPIStatsClient:
if period and period != "all":
url += f"?period={period}"
logger.info(f"Fetching recent downloads for: {normalized_name} (period: {period})")
logger.info(
f"Fetching recent downloads for: {normalized_name} (period: {period})"
)
try:
data = await self._make_request(url)
# Cache the result
import time
self._cache[cache_key] = {"data": data, "timestamp": time.time()}
return data
@ -235,19 +241,24 @@ class PyPIStatsClient:
if mirrors is not None:
url += f"?mirrors={'true' if mirrors else 'false'}"
logger.info(f"Fetching overall downloads for: {normalized_name} (mirrors: {mirrors})")
logger.info(
f"Fetching overall downloads for: {normalized_name} (mirrors: {mirrors})"
)
try:
data = await self._make_request(url)
# Cache the result
import time
self._cache[cache_key] = {"data": data, "timestamp": time.time()}
return data
except Exception as e:
logger.error(f"Failed to fetch overall downloads for {normalized_name}: {e}")
logger.error(
f"Failed to fetch overall downloads for {normalized_name}: {e}"
)
raise
def clear_cache(self):

View file

@ -17,15 +17,17 @@ class Message:
async def resolve_dependency_conflicts(
conflicts: Annotated[
list[str],
Field(description="List of conflicting dependencies or error messages", min_length=1)
Field(
description="List of conflicting dependencies or error messages",
min_length=1,
),
],
python_version: Annotated[
str | None,
Field(description="Target Python version (e.g., '3.10', '3.11')")
str | None, Field(description="Target Python version (e.g., '3.10', '3.11')")
] = None,
project_context: Annotated[
str | None,
Field(description="Brief description of the project and its requirements")
Field(description="Brief description of the project and its requirements"),
] = None,
ctx: Context | None = None,
) -> list[Message]:
@ -90,11 +92,11 @@ async def plan_version_upgrade(
current_version: Annotated[str, Field(description="Current version being used")],
target_version: Annotated[
str | None,
Field(description="Target version (if known), or 'latest' for newest")
Field(description="Target version (if known), or 'latest' for newest"),
] = None,
project_size: Annotated[
str | None,
Field(description="Project size context (small/medium/large/enterprise)")
Field(description="Project size context (small/medium/large/enterprise)"),
] = None,
ctx: Context | None = None,
) -> list[Message]:
@ -168,15 +170,17 @@ Please provide specific commands, code examples, and timelines where applicable.
async def audit_security_risks(
packages: Annotated[
list[str],
Field(description="List of packages to audit for security risks", min_length=1)
Field(description="List of packages to audit for security risks", min_length=1),
],
environment: Annotated[
str | None,
Field(description="Environment context (development/staging/production)")
Field(description="Environment context (development/staging/production)"),
] = None,
compliance_requirements: Annotated[
str | None,
Field(description="Specific compliance requirements (e.g., SOC2, HIPAA, PCI-DSS)")
Field(
description="Specific compliance requirements (e.g., SOC2, HIPAA, PCI-DSS)"
),
] = None,
ctx: Context | None = None,
) -> list[Message]:
@ -187,7 +191,11 @@ async def audit_security_risks(
"""
packages_text = ", ".join(f"'{pkg}'" for pkg in packages)
env_text = f"\nEnvironment: {environment}" if environment else ""
compliance_text = f"\nCompliance requirements: {compliance_requirements}" if compliance_requirements else ""
compliance_text = (
f"\nCompliance requirements: {compliance_requirements}"
if compliance_requirements
else ""
)
return [
Message(

View file

@ -8,7 +8,7 @@ from pydantic import Field
class Message:
"""Simple message class for prompt templates."""
def __init__(self, text: str, role: str = "user"):
self.text = text
self.role = role
@ -16,16 +16,13 @@ class Message:
async def analyze_environment_dependencies(
environment_type: Annotated[
str,
Field(description="Type of environment (local, virtual, docker, conda)")
str, Field(description="Type of environment (local, virtual, docker, conda)")
] = "local",
python_version: Annotated[
str | None,
Field(description="Python version in the environment")
str | None, Field(description="Python version in the environment")
] = None,
project_path: Annotated[
str | None,
Field(description="Path to the project directory")
str | None, Field(description="Path to the project directory")
] = None,
ctx: Context | None = None,
) -> str:
@ -33,7 +30,7 @@ async def analyze_environment_dependencies(
This prompt template helps analyze the current Python environment dependencies,
check for outdated packages, and provide upgrade recommendations.
Returns a template string with {{environment_type}}, {{python_version}}, and {{project_path}} variables.
"""
template = """Please analyze the Python environment dependencies {{environment_info}}.
@ -98,16 +95,13 @@ Please include specific commands for package management and update procedures.""
async def check_outdated_packages(
package_filter: Annotated[
str | None,
Field(description="Filter packages by name pattern (optional)")
str | None, Field(description="Filter packages by name pattern (optional)")
] = None,
severity_level: Annotated[
str,
Field(description="Focus level: all, security, major, minor")
str, Field(description="Focus level: all, security, major, minor")
] = "all",
include_dev_dependencies: Annotated[
bool,
Field(description="Include development dependencies in analysis")
bool, Field(description="Include development dependencies in analysis")
] = True,
ctx: Context | None = None,
) -> str:
@ -115,7 +109,7 @@ async def check_outdated_packages(
This prompt template helps identify and prioritize outdated packages
in the current environment with specific focus criteria.
Returns a template string with {{package_filter}}, {{severity_level}}, and {{dev_deps}} variables.
"""
template = """Please check for outdated packages in my Python environment {{filter_info}}.
@ -187,16 +181,13 @@ Include specific pip/uv commands for each update category."""
async def generate_update_plan(
update_strategy: Annotated[
str,
Field(description="Update strategy: conservative, balanced, aggressive")
str, Field(description="Update strategy: conservative, balanced, aggressive")
] = "balanced",
environment_constraints: Annotated[
str | None,
Field(description="Environment constraints or requirements")
str | None, Field(description="Environment constraints or requirements")
] = None,
testing_requirements: Annotated[
str | None,
Field(description="Testing requirements before updates")
str | None, Field(description="Testing requirements before updates")
] = None,
ctx: Context | None = None,
) -> str:
@ -204,7 +195,7 @@ async def generate_update_plan(
This prompt template helps create comprehensive update plans for Python environments
with specific strategies and constraints.
Returns a template string with {{strategy}}, {{constraints}}, and {{testing}} variables.
"""
template = """Please create a comprehensive package update plan using a {{strategy}} strategy{{constraints_text}}{{testing_text}}.

View file

@ -19,15 +19,17 @@ async def plan_package_migration(
to_package: Annotated[str, Field(description="Package to migrate to")],
codebase_size: Annotated[
Literal["small", "medium", "large", "enterprise"],
Field(description="Size of the codebase being migrated")
Field(description="Size of the codebase being migrated"),
] = "medium",
timeline: Annotated[
str | None,
Field(description="Desired timeline for migration (e.g., '2 weeks', '1 month')")
Field(
description="Desired timeline for migration (e.g., '2 weeks', '1 month')"
),
] = None,
team_size: Annotated[
int | None,
Field(description="Number of developers involved in migration", ge=1, le=50)
Field(description="Number of developers involved in migration", ge=1, le=50),
] = None,
ctx: Context | None = None,
) -> list[Message]:
@ -126,16 +128,21 @@ Please provide specific code examples, commands, and detailed timelines."""
async def generate_migration_checklist(
migration_type: Annotated[
Literal["package_replacement", "version_upgrade", "framework_migration", "dependency_cleanup"],
Field(description="Type of migration being performed")
Literal[
"package_replacement",
"version_upgrade",
"framework_migration",
"dependency_cleanup",
],
Field(description="Type of migration being performed"),
],
packages_involved: Annotated[
list[str],
Field(description="List of packages involved in the migration", min_length=1)
Field(description="List of packages involved in the migration", min_length=1),
],
environment: Annotated[
Literal["development", "staging", "production", "all"],
Field(description="Target environment for migration")
Field(description="Target environment for migration"),
] = "all",
ctx: Context | None = None,
) -> list[Message]:
@ -150,7 +157,7 @@ async def generate_migration_checklist(
"package_replacement": "replacing one package with another",
"version_upgrade": "upgrading package versions",
"framework_migration": "migrating between frameworks",
"dependency_cleanup": "cleaning up and optimizing dependencies"
"dependency_cleanup": "cleaning up and optimizing dependencies",
}
context_text = migration_contexts.get(migration_type, migration_type)

View file

@ -15,20 +15,24 @@ class Message:
async def analyze_package_quality(
package_name: Annotated[str, Field(description="Name of the PyPI package to analyze")],
version: Annotated[str | None, Field(description="Specific version to analyze")] = None,
package_name: Annotated[
str, Field(description="Name of the PyPI package to analyze")
],
version: Annotated[
str | None, Field(description="Specific version to analyze")
] = None,
ctx: Context | None = None,
) -> str:
) -> list[Message]:
"""Generate a comprehensive package quality analysis prompt template.
This prompt template helps analyze a Python package's quality, maintenance status,
security, performance, and overall suitability for use in projects.
Returns a template string with {{package_name}} and {{version_text}} variables.
Returns a list containing a Message object with the analysis prompt.
"""
template = """Please provide a comprehensive quality analysis of the Python package '{{package_name}}' {{version_text}}.
Analyze the following aspects:
Analyze the following aspects and provide a detailed assessment:
## 📊 Package Overview
- Package purpose and functionality
@ -58,21 +62,24 @@ Analyze the following aspects:
Please provide specific examples and actionable insights where possible."""
return template
return [Message(template)]
async def compare_packages(
packages: Annotated[
list[str],
Field(description="List of package names to compare", min_length=2, max_length=5)
Field(
description="List of package names to compare", min_length=2, max_length=5
),
],
use_case: Annotated[
str,
Field(description="Specific use case or project context for comparison")
str, Field(description="Specific use case or project context for comparison")
],
criteria: Annotated[
list[str] | None,
Field(description="Specific criteria to focus on (e.g., performance, security, ease of use)")
Field(
description="Specific criteria to focus on (e.g., performance, security, ease of use)"
),
] = None,
ctx: Context | None = None,
) -> str:
@ -125,14 +132,23 @@ Please include specific examples and quantitative data where available."""
async def suggest_alternatives(
package_name: Annotated[str, Field(description="Name of the package to find alternatives for")],
package_name: Annotated[
str, Field(description="Name of the package to find alternatives for")
],
reason: Annotated[
Literal["deprecated", "security", "performance", "licensing", "maintenance", "features"],
Field(description="Reason for seeking alternatives")
Literal[
"deprecated",
"security",
"performance",
"licensing",
"maintenance",
"features",
],
Field(description="Reason for seeking alternatives"),
],
requirements: Annotated[
str | None,
Field(description="Specific requirements or constraints for alternatives")
Field(description="Specific requirements or constraints for alternatives"),
] = None,
ctx: Context | None = None,
) -> str:

View file

@ -8,7 +8,7 @@ from pydantic import Field
class Message:
"""Simple message class for prompt templates."""
def __init__(self, text: str, role: str = "user"):
self.text = text
self.role = role
@ -17,15 +17,14 @@ class Message:
async def analyze_daily_trends(
date: Annotated[
str | None,
Field(description="Specific date to analyze (YYYY-MM-DD) or 'today'")
Field(description="Specific date to analyze (YYYY-MM-DD) or 'today'"),
] = "today",
category: Annotated[
str | None,
Field(description="Package category to focus on (web, data, ml, etc.)")
Field(description="Package category to focus on (web, data, ml, etc.)"),
] = None,
limit: Annotated[
int,
Field(description="Number of top packages to analyze", ge=5, le=50)
int, Field(description="Number of top packages to analyze", ge=5, le=50)
] = 20,
ctx: Context | None = None,
) -> str:
@ -33,7 +32,7 @@ async def analyze_daily_trends(
This prompt template helps analyze the most downloaded packages on PyPI
for a specific day and understand trending patterns.
Returns a template string with {{date}}, {{category_filter}}, and {{limit}} variables.
"""
template = """Please analyze the daily PyPI download trends for {{date}}{{category_filter}}.
@ -119,15 +118,15 @@ Include specific download numbers, growth percentages, and trend analysis."""
async def find_trending_packages(
time_period: Annotated[
Literal["daily", "weekly", "monthly"],
Field(description="Time period for trend analysis")
Field(description="Time period for trend analysis"),
] = "weekly",
trend_type: Annotated[
Literal["rising", "declining", "new", "all"],
Field(description="Type of trends to focus on")
Field(description="Type of trends to focus on"),
] = "rising",
domain: Annotated[
str | None,
Field(description="Specific domain or category (web, ai, data, etc.)")
Field(description="Specific domain or category (web, ai, data, etc.)"),
] = None,
ctx: Context | None = None,
) -> str:
@ -135,7 +134,7 @@ async def find_trending_packages(
This prompt template helps identify packages that are trending up or down
in the PyPI ecosystem over specific time periods.
Returns a template string with {{time_period}}, {{trend_type}}, and {{domain_filter}} variables.
"""
template = """Please identify {{trend_type}} trending Python packages over the {{time_period}} period{{domain_filter}}.
@ -242,15 +241,14 @@ Include specific trend data, growth metrics, and actionable recommendations."""
async def track_package_updates(
time_range: Annotated[
Literal["today", "week", "month"],
Field(description="Time range for update tracking")
Field(description="Time range for update tracking"),
] = "today",
update_type: Annotated[
Literal["all", "major", "security", "new"],
Field(description="Type of updates to track")
Field(description="Type of updates to track"),
] = "all",
popular_only: Annotated[
bool,
Field(description="Focus only on popular packages (>1M downloads)")
bool, Field(description="Focus only on popular packages (>1M downloads)")
] = False,
ctx: Context | None = None,
) -> str:
@ -258,7 +256,7 @@ async def track_package_updates(
This prompt template helps track and analyze recent package updates
on PyPI with filtering and categorization options.
Returns a template string with {{time_range}}, {{update_type}}, and {{popularity_filter}} variables.
"""
template = """Please track and analyze Python package updates from {{time_range}}{{popularity_filter}}.

View file

@ -295,7 +295,7 @@ async def resolve_dependencies(
python_version: str | None = None,
include_extras: list[str] | None = None,
include_dev: bool = False,
max_depth: int = 5
max_depth: int = 5,
) -> dict[str, Any]:
"""Resolve all dependencies for a PyPI package recursively.
@ -331,7 +331,7 @@ async def resolve_dependencies(
python_version=python_version,
include_extras=include_extras,
include_dev=include_dev,
max_depth=max_depth
max_depth=max_depth,
)
logger.info(f"Successfully resolved dependencies for package: {package_name}")
return result
@ -362,7 +362,7 @@ async def download_package(
include_dev: bool = False,
prefer_wheel: bool = True,
verify_checksums: bool = True,
max_depth: int = 5
max_depth: int = 5,
) -> dict[str, Any]:
"""Download a PyPI package and all its dependencies to local directory.
@ -404,7 +404,7 @@ async def download_package(
include_dev=include_dev,
prefer_wheel=prefer_wheel,
verify_checksums=verify_checksums,
max_depth=max_depth
max_depth=max_depth,
)
logger.info(f"Successfully downloaded {package_name} and dependencies")
return result
@ -453,9 +453,13 @@ async def get_download_statistics(
NetworkError: For network-related errors
"""
try:
logger.info(f"MCP tool: Getting download statistics for {package_name} (period: {period})")
logger.info(
f"MCP tool: Getting download statistics for {package_name} (period: {period})"
)
result = await get_package_download_stats(package_name, period, use_cache)
logger.info(f"Successfully retrieved download statistics for package: {package_name}")
logger.info(
f"Successfully retrieved download statistics for package: {package_name}"
)
return result
except (InvalidPackageNameError, PackageNotFoundError, NetworkError) as e:
logger.error(f"Error getting download statistics for {package_name}: {e}")
@ -466,7 +470,9 @@ async def get_download_statistics(
"period": period,
}
except Exception as e:
logger.error(f"Unexpected error getting download statistics for {package_name}: {e}")
logger.error(
f"Unexpected error getting download statistics for {package_name}: {e}"
)
return {
"error": f"Unexpected error: {e}",
"error_type": "UnexpectedError",
@ -506,8 +512,12 @@ async def get_download_trends(
f"MCP tool: Getting download trends for {package_name} "
f"(include_mirrors: {include_mirrors})"
)
result = await get_package_download_trends(package_name, include_mirrors, use_cache)
logger.info(f"Successfully retrieved download trends for package: {package_name}")
result = await get_package_download_trends(
package_name, include_mirrors, use_cache
)
logger.info(
f"Successfully retrieved download trends for package: {package_name}"
)
return result
except (InvalidPackageNameError, PackageNotFoundError, NetworkError) as e:
logger.error(f"Error getting download trends for {package_name}: {e}")
@ -518,7 +528,9 @@ async def get_download_trends(
"include_mirrors": include_mirrors,
}
except Exception as e:
logger.error(f"Unexpected error getting download trends for {package_name}: {e}")
logger.error(
f"Unexpected error getting download trends for {package_name}: {e}"
)
return {
"error": f"Unexpected error: {e}",
"error_type": "UnexpectedError",
@ -555,7 +567,9 @@ async def get_top_downloaded_packages(
# Limit the maximum number of packages to prevent excessive API calls
actual_limit = min(limit, 50)
logger.info(f"MCP tool: Getting top {actual_limit} packages for period: {period}")
logger.info(
f"MCP tool: Getting top {actual_limit} packages for period: {period}"
)
result = await get_top_packages_by_downloads(period, actual_limit)
logger.info("Successfully retrieved top packages list")
return result
@ -578,10 +592,10 @@ async def get_top_downloaded_packages(
# 6. Environment variable customization → Apply user's custom prompt words
# 7. Return final prompt → As tool's response back to AI
@mcp.prompt()
async def analyze_package_quality_prompt(
package_name: str,
version: str | None = None
package_name: str, version: str | None = None
) -> str:
"""Generate a comprehensive quality analysis prompt for a PyPI package."""
# Step 3: Call Prompt generator
@ -603,9 +617,7 @@ async def analyze_package_quality_prompt(
@mcp.prompt()
async def compare_packages_prompt(
packages: list[str],
use_case: str,
criteria: list[str] | None = None
packages: list[str], use_case: str, criteria: list[str] | None = None
) -> str:
"""Generate a detailed comparison prompt for multiple PyPI packages."""
# Step 3: Call Prompt generator
@ -618,7 +630,9 @@ async def compare_packages_prompt(
# Handle criteria parameter
if criteria:
criteria_text = f"\n\nFocus particularly on these criteria: {', '.join(criteria)}"
criteria_text = (
f"\n\nFocus particularly on these criteria: {', '.join(criteria)}"
)
else:
criteria_text = ""
result = result.replace("{{criteria_text}}", criteria_text)
@ -629,9 +643,7 @@ async def compare_packages_prompt(
@mcp.prompt()
async def suggest_alternatives_prompt(
package_name: str,
reason: str,
requirements: str | None = None
package_name: str, reason: str, requirements: str | None = None
) -> str:
"""Generate a prompt for finding package alternatives."""
# Step 3: Call Prompt generator
@ -647,7 +659,7 @@ async def suggest_alternatives_prompt(
"performance": "performance issues or requirements",
"licensing": "licensing conflicts or restrictions",
"maintenance": "poor maintenance or lack of updates",
"features": "missing features or functionality gaps"
"features": "missing features or functionality gaps",
}
reason_text = reason_context.get(reason, reason)
result = result.replace("{{reason_text}}", reason_text)
@ -667,10 +679,12 @@ async def suggest_alternatives_prompt(
async def resolve_dependency_conflicts_prompt(
conflicts: list[str],
python_version: str | None = None,
project_context: str | None = None
project_context: str | None = None,
) -> str:
"""Generate a prompt for resolving dependency conflicts."""
messages = await resolve_dependency_conflicts(conflicts, python_version, project_context)
messages = await resolve_dependency_conflicts(
conflicts, python_version, project_context
)
return messages[0].text
@ -679,10 +693,12 @@ async def plan_version_upgrade_prompt(
package_name: str,
current_version: str,
target_version: str | None = None,
project_size: str | None = None
project_size: str | None = None,
) -> str:
"""Generate a prompt for planning package version upgrades."""
messages = await plan_version_upgrade(package_name, current_version, target_version, project_size)
messages = await plan_version_upgrade(
package_name, current_version, target_version, project_size
)
return messages[0].text
@ -690,10 +706,12 @@ async def plan_version_upgrade_prompt(
async def audit_security_risks_prompt(
packages: list[str],
environment: str | None = None,
compliance_requirements: str | None = None
compliance_requirements: str | None = None,
) -> str:
"""Generate a prompt for security risk auditing of packages."""
messages = await audit_security_risks(packages, environment, compliance_requirements)
messages = await audit_security_risks(
packages, environment, compliance_requirements
)
return messages[0].text
@ -703,21 +721,23 @@ async def plan_package_migration_prompt(
to_package: str,
codebase_size: str = "medium",
timeline: str | None = None,
team_size: int | None = None
team_size: int | None = None,
) -> str:
"""Generate a comprehensive package migration plan prompt."""
messages = await plan_package_migration(from_package, to_package, codebase_size, timeline, team_size)
messages = await plan_package_migration(
from_package, to_package, codebase_size, timeline, team_size
)
return messages[0].text
@mcp.prompt()
async def generate_migration_checklist_prompt(
migration_type: str,
packages_involved: list[str],
environment: str = "all"
migration_type: str, packages_involved: list[str], environment: str = "all"
) -> str:
"""Generate a detailed migration checklist prompt."""
messages = await generate_migration_checklist(migration_type, packages_involved, environment)
messages = await generate_migration_checklist(
migration_type, packages_involved, environment
)
return messages[0].text
@ -726,11 +746,13 @@ async def generate_migration_checklist_prompt(
async def analyze_environment_dependencies_prompt(
environment_type: str = "local",
python_version: str | None = None,
project_path: str | None = None
project_path: str | None = None,
) -> str:
"""Generate a prompt for analyzing environment dependencies."""
# Step 3: Call Prompt generator
template = await analyze_environment_dependencies(environment_type, python_version, project_path)
template = await analyze_environment_dependencies(
environment_type, python_version, project_path
)
# Step 5: Parameter replacement
result = template.replace("{{environment_type}}", environment_type)
@ -755,11 +777,13 @@ async def analyze_environment_dependencies_prompt(
async def check_outdated_packages_prompt(
package_filter: str | None = None,
severity_level: str = "all",
include_dev_dependencies: bool = True
include_dev_dependencies: bool = True,
) -> str:
"""Generate a prompt for checking outdated packages."""
# Step 3: Call Prompt generator
template = await check_outdated_packages(package_filter, severity_level, include_dev_dependencies)
template = await check_outdated_packages(
package_filter, severity_level, include_dev_dependencies
)
# Step 5: Parameter replacement
result = template.replace("{{severity_level}}", severity_level)
@ -786,11 +810,13 @@ async def check_outdated_packages_prompt(
async def generate_update_plan_prompt(
update_strategy: str = "balanced",
environment_constraints: str | None = None,
testing_requirements: str | None = None
testing_requirements: str | None = None,
) -> str:
"""Generate a prompt for creating package update plans."""
# Step 3: Call Prompt generator
template = await generate_update_plan(update_strategy, environment_constraints, testing_requirements)
template = await generate_update_plan(
update_strategy, environment_constraints, testing_requirements
)
# Step 5: Parameter replacement
result = template.replace("{{strategy}}", update_strategy)
@ -816,9 +842,7 @@ async def generate_update_plan_prompt(
# Trending Analysis Prompts
@mcp.prompt()
async def analyze_daily_trends_prompt(
date: str = "today",
category: str | None = None,
limit: int = 20
date: str = "today", category: str | None = None, limit: int = 20
) -> str:
"""Generate a prompt for analyzing daily PyPI trends."""
# Step 3: Call Prompt generator
@ -841,9 +865,7 @@ async def analyze_daily_trends_prompt(
@mcp.prompt()
async def find_trending_packages_prompt(
time_period: str = "weekly",
trend_type: str = "rising",
domain: str | None = None
time_period: str = "weekly", trend_type: str = "rising", domain: str | None = None
) -> str:
"""Generate a prompt for finding trending packages."""
# Step 3: Call Prompt generator
@ -866,9 +888,7 @@ async def find_trending_packages_prompt(
@mcp.prompt()
async def track_package_updates_prompt(
time_range: str = "today",
update_type: str = "all",
popular_only: bool = False
time_range: str = "today", update_type: str = "all", popular_only: bool = False
) -> str:
"""Generate a prompt for tracking recent package updates."""
# Step 3: Call Prompt generator

View file

@ -28,7 +28,7 @@ class DependencyResolver:
python_version: str | None = None,
include_extras: list[str] | None = None,
include_dev: bool = False,
max_depth: int | None = None
max_depth: int | None = None,
) -> dict[str, Any]:
"""Resolve all dependencies for a package recursively.
@ -48,7 +48,9 @@ class DependencyResolver:
max_depth = max_depth or self.max_depth
include_extras = include_extras or []
logger.info(f"Resolving dependencies for {package_name} (Python {python_version})")
logger.info(
f"Resolving dependencies for {package_name} (Python {python_version})"
)
# Track visited packages to avoid circular dependencies
visited: set[str] = set()
@ -63,13 +65,15 @@ class DependencyResolver:
visited=visited,
dependency_tree=dependency_tree,
current_depth=0,
max_depth=max_depth
max_depth=max_depth,
)
# Check if main package was resolved
normalized_name = package_name.lower().replace("_", "-")
if normalized_name not in dependency_tree:
raise PackageNotFoundError(f"Package '{package_name}' not found on PyPI")
raise PackageNotFoundError(
f"Package '{package_name}' not found on PyPI"
)
# Generate summary
summary = self._generate_dependency_summary(dependency_tree)
@ -80,13 +84,15 @@ class DependencyResolver:
"include_extras": include_extras,
"include_dev": include_dev,
"dependency_tree": dependency_tree,
"summary": summary
"summary": summary,
}
except PyPIError:
raise
except Exception as e:
logger.error(f"Unexpected error resolving dependencies for {package_name}: {e}")
logger.error(
f"Unexpected error resolving dependencies for {package_name}: {e}"
)
raise NetworkError(f"Failed to resolve dependencies: {e}", e) from e
async def _resolve_recursive(
@ -98,7 +104,7 @@ class DependencyResolver:
visited: set[str],
dependency_tree: dict[str, Any],
current_depth: int,
max_depth: int
max_depth: int,
) -> None:
"""Recursively resolve dependencies."""
@ -138,11 +144,13 @@ class DependencyResolver:
"requires_python": info.get("requires_python", ""),
"dependencies": {
"runtime": [str(req) for req in categorized["runtime"]],
"development": [str(req) for req in categorized["development"]] if include_dev else [],
"extras": {}
"development": [str(req) for req in categorized["development"]]
if include_dev
else [],
"extras": {},
},
"depth": current_depth,
"children": {}
"children": {},
}
# Add requested extras
@ -177,12 +185,14 @@ class DependencyResolver:
visited=visited,
dependency_tree=dependency_tree,
current_depth=current_depth + 1,
max_depth=max_depth
max_depth=max_depth,
)
# Add to children if resolved
if dep_name.lower() in dependency_tree:
package_info["children"][dep_name.lower()] = dependency_tree[dep_name.lower()]
package_info["children"][dep_name.lower()] = dependency_tree[
dep_name.lower()
]
except PackageNotFoundError:
logger.warning(f"Package {package_name} not found, skipping")
@ -190,7 +200,9 @@ class DependencyResolver:
logger.error(f"Error resolving {package_name}: {e}")
# Continue with other dependencies
def _generate_dependency_summary(self, dependency_tree: dict[str, Any]) -> dict[str, Any]:
def _generate_dependency_summary(
self, dependency_tree: dict[str, Any]
) -> dict[str, Any]:
"""Generate summary statistics for the dependency tree."""
total_packages = len(dependency_tree)
@ -214,7 +226,7 @@ class DependencyResolver:
"total_development_dependencies": total_dev_deps,
"total_extra_dependencies": total_extra_deps,
"max_depth": max_depth,
"package_list": list(dependency_tree.keys())
"package_list": list(dependency_tree.keys()),
}
@ -223,7 +235,7 @@ async def resolve_package_dependencies(
python_version: str | None = None,
include_extras: list[str] | None = None,
include_dev: bool = False,
max_depth: int = 5
max_depth: int = 5,
) -> dict[str, Any]:
"""Resolve package dependencies with comprehensive analysis.
@ -242,5 +254,5 @@ async def resolve_package_dependencies(
package_name=package_name,
python_version=python_version,
include_extras=include_extras,
include_dev=include_dev
include_dev=include_dev,
)

View file

@ -40,7 +40,9 @@ async def get_package_download_stats(
# Get basic package info for metadata
try:
package_info = await pypi_client.get_package_info(package_name, use_cache)
package_info = await pypi_client.get_package_info(
package_name, use_cache
)
package_metadata = {
"name": package_info.get("info", {}).get("name", package_name),
"version": package_info.get("info", {}).get("version", "unknown"),
@ -48,10 +50,14 @@ async def get_package_download_stats(
"author": package_info.get("info", {}).get("author", ""),
"home_page": package_info.get("info", {}).get("home_page", ""),
"project_url": package_info.get("info", {}).get("project_url", ""),
"project_urls": package_info.get("info", {}).get("project_urls", {}),
"project_urls": package_info.get("info", {}).get(
"project_urls", {}
),
}
except Exception as e:
logger.warning(f"Could not fetch package metadata for {package_name}: {e}")
logger.warning(
f"Could not fetch package metadata for {package_name}: {e}"
)
package_metadata = {"name": package_name}
# Extract download data
@ -143,10 +149,26 @@ async def get_top_packages_by_downloads(
"""
# Known popular packages (this would ideally come from an API)
popular_packages = [
"boto3", "urllib3", "requests", "certifi", "charset-normalizer",
"idna", "setuptools", "python-dateutil", "six", "botocore",
"typing-extensions", "packaging", "numpy", "pip", "pyyaml",
"cryptography", "click", "jinja2", "markupsafe", "wheel"
"boto3",
"urllib3",
"requests",
"certifi",
"charset-normalizer",
"idna",
"setuptools",
"python-dateutil",
"six",
"botocore",
"typing-extensions",
"packaging",
"numpy",
"pip",
"pyyaml",
"cryptography",
"click",
"jinja2",
"markupsafe",
"wheel",
]
async with PyPIStatsClient() as stats_client:
@ -163,12 +185,14 @@ async def get_top_packages_by_downloads(
download_data = stats.get("data", {})
download_count = _extract_download_count(download_data, period)
top_packages.append({
"rank": i + 1,
"package": package_name,
"downloads": download_count,
"period": period,
})
top_packages.append(
{
"rank": i + 1,
"package": package_name,
"downloads": download_count,
"period": period,
}
)
except Exception as e:
logger.warning(f"Could not get stats for {package_name}: {e}")
@ -221,7 +245,9 @@ def _analyze_download_stats(download_data: dict[str, Any]) -> dict[str, Any]:
analysis["periods_available"].append(period)
analysis["total_downloads"] += count
if analysis["highest_period"] is None or count > download_data.get(analysis["highest_period"], 0):
if analysis["highest_period"] is None or count > download_data.get(
analysis["highest_period"], 0
):
analysis["highest_period"] = period
# Calculate growth indicators
@ -230,15 +256,21 @@ def _analyze_download_stats(download_data: dict[str, Any]) -> dict[str, Any]:
last_month = download_data.get("last_month", 0)
if last_day and last_week:
analysis["growth_indicators"]["daily_vs_weekly"] = round(last_day * 7 / last_week, 2)
analysis["growth_indicators"]["daily_vs_weekly"] = round(
last_day * 7 / last_week, 2
)
if last_week and last_month:
analysis["growth_indicators"]["weekly_vs_monthly"] = round(last_week * 4 / last_month, 2)
analysis["growth_indicators"]["weekly_vs_monthly"] = round(
last_week * 4 / last_month, 2
)
return analysis
def _analyze_download_trends(time_series_data: list[dict], include_mirrors: bool) -> dict[str, Any]:
def _analyze_download_trends(
time_series_data: list[dict], include_mirrors: bool
) -> dict[str, Any]:
"""Analyze download trends from time series data.
Args:
@ -263,8 +295,7 @@ def _analyze_download_trends(time_series_data: list[dict], include_mirrors: bool
# Filter data based on mirror preference
category_filter = "with_mirrors" if include_mirrors else "without_mirrors"
filtered_data = [
item for item in time_series_data
if item.get("category") == category_filter
item for item in time_series_data if item.get("category") == category_filter
]
if not filtered_data:

View file

@ -34,7 +34,7 @@ class PackageDownloader:
include_dev: bool = False,
prefer_wheel: bool = True,
verify_checksums: bool = True,
max_depth: int = 5
max_depth: int = 5,
) -> dict[str, Any]:
"""Download a package and all its dependencies.
@ -62,7 +62,7 @@ class PackageDownloader:
python_version=python_version,
include_extras=include_extras,
include_dev=include_dev,
max_depth=max_depth
max_depth=max_depth,
)
dependency_tree = resolution_result["dependency_tree"]
@ -78,19 +78,18 @@ class PackageDownloader:
version=pkg_info["version"],
python_version=python_version,
prefer_wheel=prefer_wheel,
verify_checksums=verify_checksums
verify_checksums=verify_checksums,
)
download_results[pkg_name] = result
except Exception as e:
logger.error(f"Failed to download {pkg_name}: {e}")
failed_downloads.append({
"package": pkg_name,
"error": str(e)
})
failed_downloads.append({"package": pkg_name, "error": str(e)})
# Generate summary
summary = self._generate_download_summary(download_results, failed_downloads)
summary = self._generate_download_summary(
download_results, failed_downloads
)
return {
"package_name": package_name,
@ -99,7 +98,7 @@ class PackageDownloader:
"resolution_result": resolution_result,
"download_results": download_results,
"failed_downloads": failed_downloads,
"summary": summary
"summary": summary,
}
except PyPIError:
@ -114,7 +113,7 @@ class PackageDownloader:
version: str | None = None,
python_version: str | None = None,
prefer_wheel: bool = True,
verify_checksums: bool = True
verify_checksums: bool = True,
) -> dict[str, Any]:
"""Download a single package."""
@ -129,12 +128,16 @@ class PackageDownloader:
# Determine version to download
target_version = version or info.get("version")
if not target_version or target_version not in releases:
raise PackageNotFoundError(f"Version {target_version} not found for {package_name}")
raise PackageNotFoundError(
f"Version {target_version} not found for {package_name}"
)
# Get release files
release_files = releases[target_version]
if not release_files:
raise PackageNotFoundError(f"No files found for {package_name} {target_version}")
raise PackageNotFoundError(
f"No files found for {package_name} {target_version}"
)
# Select best file to download
selected_file = self._select_best_file(
@ -142,25 +145,25 @@ class PackageDownloader:
)
if not selected_file:
raise PackageNotFoundError(f"No suitable file found for {package_name} {target_version}")
raise PackageNotFoundError(
f"No suitable file found for {package_name} {target_version}"
)
# Download the file
download_result = await self._download_file(
selected_file, verify_checksums
)
download_result = await self._download_file(selected_file, verify_checksums)
return {
"package_name": package_name,
"version": target_version,
"file_info": selected_file,
"download_result": download_result
"download_result": download_result,
}
def _select_best_file(
self,
release_files: list[dict[str, Any]],
python_version: str | None = None,
prefer_wheel: bool = True
prefer_wheel: bool = True,
) -> dict[str, Any] | None:
"""Select the best file to download from available release files."""
@ -172,7 +175,9 @@ class PackageDownloader:
if prefer_wheel and wheels:
# Try to find compatible wheel
if python_version:
compatible_wheels = self._filter_compatible_wheels(wheels, python_version)
compatible_wheels = self._filter_compatible_wheels(
wheels, python_version
)
if compatible_wheels:
return compatible_wheels[0]
@ -187,9 +192,7 @@ class PackageDownloader:
return release_files[0] if release_files else None
def _filter_compatible_wheels(
self,
wheels: list[dict[str, Any]],
python_version: str
self, wheels: list[dict[str, Any]], python_version: str
) -> list[dict[str, Any]]:
"""Filter wheels compatible with the specified Python version."""
@ -204,18 +207,18 @@ class PackageDownloader:
filename = wheel.get("filename", "")
# Check for Python version in filename
if (f"py{major_minor_nodot}" in filename or
f"cp{major_minor_nodot}" in filename or
"py3" in filename or
"py2.py3" in filename):
if (
f"py{major_minor_nodot}" in filename
or f"cp{major_minor_nodot}" in filename
or "py3" in filename
or "py2.py3" in filename
):
compatible.append(wheel)
return compatible
async def _download_file(
self,
file_info: dict[str, Any],
verify_checksums: bool = True
self, file_info: dict[str, Any], verify_checksums: bool = True
) -> dict[str, Any]:
"""Download a single file."""
@ -265,13 +268,11 @@ class PackageDownloader:
"file_path": str(file_path),
"downloaded_size": downloaded_size,
"verification": verification_result,
"success": True
"success": True,
}
def _generate_download_summary(
self,
download_results: dict[str, Any],
failed_downloads: list[dict[str, Any]]
self, download_results: dict[str, Any], failed_downloads: list[dict[str, Any]]
) -> dict[str, Any]:
"""Generate download summary statistics."""
@ -288,8 +289,11 @@ class PackageDownloader:
"failed_downloads": failed_count,
"total_downloaded_size": total_size,
"download_directory": str(self.download_dir),
"success_rate": successful_downloads / (successful_downloads + failed_count) * 100
if (successful_downloads + failed_count) > 0 else 0
"success_rate": successful_downloads
/ (successful_downloads + failed_count)
* 100
if (successful_downloads + failed_count) > 0
else 0,
}
@ -301,7 +305,7 @@ async def download_package_with_dependencies(
include_dev: bool = False,
prefer_wheel: bool = True,
verify_checksums: bool = True,
max_depth: int = 5
max_depth: int = 5,
) -> dict[str, Any]:
"""Download a package and its dependencies to local directory.
@ -326,5 +330,5 @@ async def download_package_with_dependencies(
include_dev=include_dev,
prefer_wheel=prefer_wheel,
verify_checksums=verify_checksums,
max_depth=max_depth
max_depth=max_depth,
)