chore: upgrade all Python packages and fix linting issues

- Update all dependencies to latest versions (fastmcp, httpx, packaging, etc.)
- Downgrade click from yanked 8.2.2 to stable 8.1.7
- Fix code formatting and linting issues with ruff
- Most tests passing (2 test failures in dependency resolver need investigation)
This commit is contained in:
Ryan Malloy 2025-08-15 20:23:14 -06:00
parent 503ea589f1
commit 8b43927493
34 changed files with 2276 additions and 1593 deletions

View File

@ -3,10 +3,10 @@
import asyncio import asyncio
import sys import sys
import os
# Add the package to Python path # Add the package to Python path
sys.path.insert(0, '/tmp/a/improve-top-packages') sys.path.insert(0, "/tmp/a/improve-top-packages")
async def demo_improvements(): async def demo_improvements():
"""Demonstrate the improvements made to get_top_packages_by_downloads.""" """Demonstrate the improvements made to get_top_packages_by_downloads."""
@ -50,24 +50,26 @@ async def demo_improvements():
try: try:
# Test with current API state (likely failing) # Test with current API state (likely failing)
result = await get_top_packages_by_downloads('month', 8) result = await get_top_packages_by_downloads("month", 8)
print(f"✅ SUCCESS! Returned {len(result.get('top_packages', []))} packages") print(f"✅ SUCCESS! Returned {len(result.get('top_packages', []))} packages")
print(f"📊 Data source: {result.get('data_source')}") print(f"📊 Data source: {result.get('data_source')}")
print(f"🔬 Methodology: {result.get('methodology')}") print(f"🔬 Methodology: {result.get('methodology')}")
print(f"\n📦 Top 5 packages:") print("\n📦 Top 5 packages:")
for i, pkg in enumerate(result.get('top_packages', [])[:5]): for i, pkg in enumerate(result.get("top_packages", [])[:5]):
downloads = pkg.get('downloads', 0) downloads = pkg.get("downloads", 0)
stars = pkg.get('github_stars', 'N/A') stars = pkg.get("github_stars", "N/A")
category = pkg.get('category', 'N/A') category = pkg.get("category", "N/A")
estimated = ' (estimated)' if pkg.get('estimated', False) else ' (real stats)' estimated = (
github_enhanced = ' 🌟' if pkg.get('github_enhanced', False) else '' " (estimated)" if pkg.get("estimated", False) else " (real stats)"
)
github_enhanced = " 🌟" if pkg.get("github_enhanced", False) else ""
print(f" {i+1}. {pkg.get('package', 'N/A')}") print(f" {i + 1}. {pkg.get('package', 'N/A')}")
print(f" Downloads: {downloads:,}{estimated}{github_enhanced}") print(f" Downloads: {downloads:,}{estimated}{github_enhanced}")
print(f" Category: {category}") print(f" Category: {category}")
if stars != 'N/A': if stars != "N/A":
print(f" GitHub: {stars:,} stars") print(f" GitHub: {stars:,} stars")
print() print()
@ -76,23 +78,29 @@ async def demo_improvements():
# Test different periods # Test different periods
periods_test = {} periods_test = {}
for period in ['day', 'week', 'month']: for period in ["day", "week", "month"]:
result = await get_top_packages_by_downloads(period, 3) result = await get_top_packages_by_downloads(period, 3)
avg_downloads = sum(p.get('downloads', 0) for p in result.get('top_packages', [])) // max(len(result.get('top_packages', [])), 1) avg_downloads = sum(
p.get("downloads", 0) for p in result.get("top_packages", [])
) // max(len(result.get("top_packages", [])), 1)
periods_test[period] = avg_downloads periods_test[period] = avg_downloads
print(f"{period}: {len(result.get('top_packages', []))} packages, avg downloads: {avg_downloads:,}") print(
f"{period}: {len(result.get('top_packages', []))} packages, avg downloads: {avg_downloads:,}"
)
# Verify period scaling makes sense # Verify period scaling makes sense
if periods_test['day'] < periods_test['week'] < periods_test['month']: if periods_test["day"] < periods_test["week"] < periods_test["month"]:
print("✅ Period scaling works correctly (day < week < month)") print("✅ Period scaling works correctly (day < week < month)")
# Test different limits # Test different limits
for limit in [5, 15, 25]: for limit in [5, 15, 25]:
result = await get_top_packages_by_downloads('month', limit) result = await get_top_packages_by_downloads("month", limit)
packages = result.get('top_packages', []) packages = result.get("top_packages", [])
real_count = len([p for p in packages if not p.get('estimated', False)]) real_count = len([p for p in packages if not p.get("estimated", False)])
github_count = len([p for p in packages if 'github_stars' in p]) github_count = len([p for p in packages if "github_stars" in p])
print(f"✅ Limit {limit}: {len(packages)} packages ({real_count} real, {github_count} GitHub-enhanced)") print(
f"✅ Limit {limit}: {len(packages)} packages ({real_count} real, {github_count} GitHub-enhanced)"
)
print("\n🎯 KEY IMPROVEMENTS ACHIEVED:") print("\n🎯 KEY IMPROVEMENTS ACHIEVED:")
print("✅ 100% reliability - always returns results even when APIs fail") print("✅ 100% reliability - always returns results even when APIs fail")
@ -102,7 +110,7 @@ async def demo_improvements():
print("✅ Transparency - clear methodology and data source reporting") print("✅ Transparency - clear methodology and data source reporting")
print("✅ Scalability - supports different periods and limits") print("✅ Scalability - supports different periods and limits")
print(f"\n🏆 CONCLUSION:") print("\n🏆 CONCLUSION:")
print("The improved get_top_packages_by_downloads tool now provides") print("The improved get_top_packages_by_downloads tool now provides")
print("reliable, informative results even when external APIs fail,") print("reliable, informative results even when external APIs fail,")
print("making it suitable for production use with robust fallbacks.") print("making it suitable for production use with robust fallbacks.")
@ -110,7 +118,9 @@ async def demo_improvements():
except Exception as e: except Exception as e:
print(f"❌ Error during testing: {e}") print(f"❌ Error during testing: {e}")
import traceback import traceback
traceback.print_exc() traceback.print_exc()
if __name__ == '__main__':
if __name__ == "__main__":
asyncio.run(demo_improvements()) asyncio.run(demo_improvements())

View File

@ -10,19 +10,14 @@ This demonstrates how to use the new transitive dependency functionality.
# Basic usage (backward compatible) # Basic usage (backward compatible)
example_1 = { example_1 = {
"tool": "get_package_dependencies", "tool": "get_package_dependencies",
"parameters": { "parameters": {"package_name": "requests"},
"package_name": "requests"
}
} }
# Returns: Direct dependencies only (existing behavior) # Returns: Direct dependencies only (existing behavior)
# Enable transitive dependencies # Enable transitive dependencies
example_2 = { example_2 = {
"tool": "get_package_dependencies", "tool": "get_package_dependencies",
"parameters": { "parameters": {"package_name": "requests", "include_transitive": True},
"package_name": "requests",
"include_transitive": True
}
} }
# Returns: Complete dependency tree with analysis # Returns: Complete dependency tree with analysis
@ -33,8 +28,8 @@ example_3 = {
"package_name": "django", "package_name": "django",
"include_transitive": True, "include_transitive": True,
"max_depth": 3, "max_depth": 3,
"python_version": "3.11" "python_version": "3.11",
} },
} }
# Returns: Filtered dependency tree for Python 3.11, max 3 levels deep # Returns: Filtered dependency tree for Python 3.11, max 3 levels deep
@ -46,20 +41,18 @@ example_response = {
"include_transitive": True, "include_transitive": True,
"max_depth": 5, "max_depth": 5,
"python_version": "3.10", "python_version": "3.10",
# Direct dependencies (same as before) # Direct dependencies (same as before)
"runtime_dependencies": [ "runtime_dependencies": [
"urllib3>=1.21.1,<3", "urllib3>=1.21.1,<3",
"certifi>=2017.4.17", "certifi>=2017.4.17",
"charset-normalizer>=2,<4", "charset-normalizer>=2,<4",
"idna>=2.5,<4" "idna>=2.5,<4",
], ],
"development_dependencies": [], "development_dependencies": [],
"optional_dependencies": { "optional_dependencies": {
"security": ["pyOpenSSL>=0.14", "cryptography>=1.3.4"], "security": ["pyOpenSSL>=0.14", "cryptography>=1.3.4"],
"socks": ["PySocks>=1.5.6,!=1.5.7"] "socks": ["PySocks>=1.5.6,!=1.5.7"],
}, },
# NEW: Transitive dependency information # NEW: Transitive dependency information
"transitive_dependencies": { "transitive_dependencies": {
"dependency_tree": { "dependency_tree": {
@ -71,41 +64,41 @@ example_response = {
"package_name": "urllib3", "package_name": "urllib3",
"version": "2.0.4", "version": "2.0.4",
"depth": 1, "depth": 1,
"children": {} "children": {},
}, },
"certifi": { "certifi": {
"package_name": "certifi", "package_name": "certifi",
"version": "2023.7.22", "version": "2023.7.22",
"depth": 1, "depth": 1,
"children": {} "children": {},
}, },
"charset-normalizer": { "charset-normalizer": {
"package_name": "charset-normalizer", "package_name": "charset-normalizer",
"version": "3.2.0", "version": "3.2.0",
"depth": 1, "depth": 1,
"children": {} "children": {},
}, },
"idna": { "idna": {
"package_name": "idna", "package_name": "idna",
"version": "3.4", "version": "3.4",
"depth": 1, "depth": 1,
"children": {} "children": {},
} },
} },
}, },
"all_packages": { "all_packages": {
"requests": { "requests": {
"name": "requests", "name": "requests",
"version": "2.31.0", "version": "2.31.0",
"depth": 0, "depth": 0,
"dependency_count": {"runtime": 4, "development": 0, "total_extras": 0} "dependency_count": {"runtime": 4, "development": 0, "total_extras": 0},
}, },
"urllib3": { "urllib3": {
"name": "urllib3", "name": "urllib3",
"version": "2.0.4", "version": "2.0.4",
"depth": 1, "depth": 1,
"dependency_count": {"runtime": 0, "development": 0, "total_extras": 0} "dependency_count": {"runtime": 0, "development": 0, "total_extras": 0},
} },
# ... other packages # ... other packages
}, },
"circular_dependencies": [], "circular_dependencies": [],
@ -115,10 +108,9 @@ example_response = {
"average_depth": 0.8, "average_depth": 0.8,
"shallow_deps": 4, "shallow_deps": 4,
"deep_deps": 0, "deep_deps": 0,
"leaf_packages": ["urllib3", "certifi", "charset-normalizer", "idna"] "leaf_packages": ["urllib3", "certifi", "charset-normalizer", "idna"],
} },
}, },
# Enhanced summary statistics # Enhanced summary statistics
"dependency_summary": { "dependency_summary": {
"direct_runtime_count": 4, "direct_runtime_count": 4,
@ -133,27 +125,22 @@ example_response = {
"score": 8.2, "score": 8.2,
"level": "low", "level": "low",
"recommendation": "Simple dependency structure, low maintenance overhead", "recommendation": "Simple dependency structure, low maintenance overhead",
"factors": { "factors": {"total_packages": 5, "max_depth": 1, "total_dependencies": 4},
"total_packages": 5, },
"max_depth": 1,
"total_dependencies": 4
}
}
}, },
# Performance and health analysis # Performance and health analysis
"analysis": { "analysis": {
"resolution_stats": { "resolution_stats": {
"total_packages": 5, "total_packages": 5,
"total_runtime_dependencies": 4, "total_runtime_dependencies": 4,
"max_depth": 1 "max_depth": 1,
}, },
"potential_conflicts": [], "potential_conflicts": [],
"maintenance_concerns": { "maintenance_concerns": {
"total_packages": 5, "total_packages": 5,
"packages_without_version_info": 0, "packages_without_version_info": 0,
"high_dependency_packages": [], "high_dependency_packages": [],
"maintenance_risk_score": {"score": 0.0, "level": "low"} "maintenance_risk_score": {"score": 0.0, "level": "low"},
}, },
"performance_impact": { "performance_impact": {
"estimated_install_time_seconds": 15, "estimated_install_time_seconds": 15,
@ -163,10 +150,10 @@ example_response = {
"metrics": { "metrics": {
"package_count_impact": "low", "package_count_impact": "low",
"depth_impact": "low", "depth_impact": "low",
"resolution_complexity": "simple" "resolution_complexity": "simple",
} },
} },
} },
} }
# Usage examples for different complexity levels # Usage examples for different complexity levels
@ -174,44 +161,44 @@ complexity_examples = {
"simple_package": { "simple_package": {
"package": "six", "package": "six",
"expected_packages": 1, # No dependencies "expected_packages": 1, # No dependencies
"complexity": "low" "complexity": "low",
}, },
"moderate_package": { "moderate_package": {
"package": "requests", "package": "requests",
"expected_packages": 5, # Few dependencies "expected_packages": 5, # Few dependencies
"complexity": "low" "complexity": "low",
}, },
"complex_package": { "complex_package": {
"package": "django", "package": "django",
"expected_packages": 15, # Moderate dependencies "expected_packages": 15, # Moderate dependencies
"complexity": "moderate" "complexity": "moderate",
}, },
"very_complex_package": { "very_complex_package": {
"package": "tensorflow", "package": "tensorflow",
"expected_packages": 50, # Many dependencies "expected_packages": 50, # Many dependencies
"complexity": "high" "complexity": "high",
} },
} }
# Test cases for edge cases # Test cases for edge cases
edge_case_examples = { edge_case_examples = {
"circular_dependencies": { "circular_dependencies": {
"description": "Package with circular dependency references", "description": "Package with circular dependency references",
"expected_behavior": "Detected and reported in circular_dependencies array" "expected_behavior": "Detected and reported in circular_dependencies array",
}, },
"deep_nesting": { "deep_nesting": {
"description": "Package with very deep dependency chains", "description": "Package with very deep dependency chains",
"max_depth": 2, "max_depth": 2,
"expected_behavior": "Truncated at max_depth with depth tracking" "expected_behavior": "Truncated at max_depth with depth tracking",
}, },
"version_conflicts": { "version_conflicts": {
"description": "Dependencies with conflicting version requirements", "description": "Dependencies with conflicting version requirements",
"expected_behavior": "Reported in potential_conflicts array" "expected_behavior": "Reported in potential_conflicts array",
}, },
"missing_packages": { "missing_packages": {
"description": "Dependencies that don't exist on PyPI", "description": "Dependencies that don't exist on PyPI",
"expected_behavior": "Graceful handling with warnings in logs" "expected_behavior": "Graceful handling with warnings in logs",
} },
} }
print("Enhanced get_package_dependencies Tool") print("Enhanced get_package_dependencies Tool")

View File

@ -7,11 +7,9 @@ to resolve optional dependencies for Python packages.
""" """
import asyncio import asyncio
import json
from pathlib import Path
from pypi_query_mcp.tools.dependency_resolver import resolve_package_dependencies
from pypi_query_mcp.core.pypi_client import PyPIClient from pypi_query_mcp.core.pypi_client import PyPIClient
from pypi_query_mcp.tools.dependency_resolver import resolve_package_dependencies
async def show_available_extras(package_name: str): async def show_available_extras(package_name: str):
@ -36,6 +34,7 @@ async def show_available_extras(package_name: str):
if "extra ==" in req: if "extra ==" in req:
# Extract extra name from requirement like: pytest>=6.0.0; extra=='test' # Extract extra name from requirement like: pytest>=6.0.0; extra=='test'
import re import re
match = re.search(r'extra\s*==\s*["\']([^"\']+)["\']', req) match = re.search(r'extra\s*==\s*["\']([^"\']+)["\']', req)
if match: if match:
extras_in_deps.add(match.group(1)) extras_in_deps.add(match.group(1))
@ -54,23 +53,23 @@ async def demo_extras_resolution():
{ {
"package": "requests", "package": "requests",
"extras": ["socks"], "extras": ["socks"],
"description": "HTTP library with SOCKS proxy support" "description": "HTTP library with SOCKS proxy support",
}, },
{ {
"package": "django", "package": "django",
"extras": ["argon2", "bcrypt"], "extras": ["argon2", "bcrypt"],
"description": "Web framework with password hashing extras" "description": "Web framework with password hashing extras",
}, },
{ {
"package": "setuptools", "package": "setuptools",
"extras": ["test"], "extras": ["test"],
"description": "Package development tools with testing extras" "description": "Package development tools with testing extras",
}, },
{ {
"package": "flask", "package": "flask",
"extras": ["async", "dotenv"], "extras": ["async", "dotenv"],
"description": "Web framework with async and dotenv support" "description": "Web framework with async and dotenv support",
} },
] ]
for example in examples: for example in examples:
@ -78,7 +77,7 @@ async def demo_extras_resolution():
extras = example["extras"] extras = example["extras"]
description = example["description"] description = example["description"]
print(f"\n{'='*60}") print(f"\n{'=' * 60}")
print(f"🔍 Example: {package_name}") print(f"🔍 Example: {package_name}")
print(f"📋 Description: {description}") print(f"📋 Description: {description}")
print(f"🎯 Testing extras: {extras}") print(f"🎯 Testing extras: {extras}")
@ -93,7 +92,7 @@ async def demo_extras_resolution():
package_name=package_name, package_name=package_name,
python_version="3.10", python_version="3.10",
include_extras=[], include_extras=[],
max_depth=1 # Limit depth for demo max_depth=1, # Limit depth for demo
) )
# Resolve with extras # Resolve with extras
@ -102,20 +101,24 @@ async def demo_extras_resolution():
package_name=package_name, package_name=package_name,
python_version="3.10", python_version="3.10",
include_extras=extras, include_extras=extras,
max_depth=1 max_depth=1,
) )
# Compare results # Compare results
print(f"\n📈 Results comparison:") print("\n📈 Results comparison:")
print(f" Without extras: {result_no_extras['summary']['total_extra_dependencies']} extra deps") print(
print(f" With extras: {result_with_extras['summary']['total_extra_dependencies']} extra deps") f" Without extras: {result_no_extras['summary']['total_extra_dependencies']} extra deps"
)
print(
f" With extras: {result_with_extras['summary']['total_extra_dependencies']} extra deps"
)
# Show actual extras resolved # Show actual extras resolved
main_pkg = next(iter(result_with_extras['dependency_tree'].values()), {}) main_pkg = next(iter(result_with_extras["dependency_tree"].values()), {})
extras_resolved = main_pkg.get('dependencies', {}).get('extras', {}) extras_resolved = main_pkg.get("dependencies", {}).get("extras", {})
if extras_resolved: if extras_resolved:
print(f" ✅ Extras resolved successfully:") print(" ✅ Extras resolved successfully:")
for extra_name, deps in extras_resolved.items(): for extra_name, deps in extras_resolved.items():
print(f" - {extra_name}: {len(deps)} dependencies") print(f" - {extra_name}: {len(deps)} dependencies")
for dep in deps[:2]: # Show first 2 for dep in deps[:2]: # Show first 2
@ -123,7 +126,9 @@ async def demo_extras_resolution():
if len(deps) > 2: if len(deps) > 2:
print(f" * ... and {len(deps) - 2} more") print(f" * ... and {len(deps) - 2} more")
else: else:
print(f" ⚠️ No extras resolved (may not exist or have no dependencies)") print(
" ⚠️ No extras resolved (may not exist or have no dependencies)"
)
except Exception as e: except Exception as e:
print(f" ❌ Error: {e}") print(f" ❌ Error: {e}")
@ -131,7 +136,7 @@ async def demo_extras_resolution():
async def demo_incorrect_usage(): async def demo_incorrect_usage():
"""Demonstrate common mistakes with extras usage.""" """Demonstrate common mistakes with extras usage."""
print(f"\n{'='*60}") print(f"\n{'=' * 60}")
print("❌ Common Mistakes with Extras") print("❌ Common Mistakes with Extras")
print("='*60") print("='*60")
@ -139,13 +144,13 @@ async def demo_incorrect_usage():
{ {
"package": "requests", "package": "requests",
"extras": ["dev", "test"], # These don't exist for requests "extras": ["dev", "test"], # These don't exist for requests
"error": "Using generic extra names instead of package-specific ones" "error": "Using generic extra names instead of package-specific ones",
}, },
{ {
"package": "setuptools", "package": "setuptools",
"extras": ["testing"], # Should be "test" not "testing" "extras": ["testing"], # Should be "test" not "testing"
"error": "Using similar but incorrect extra names" "error": "Using similar but incorrect extra names",
} },
] ]
for mistake in mistakes: for mistake in mistakes:
@ -162,13 +167,13 @@ async def demo_incorrect_usage():
package_name=package_name, package_name=package_name,
python_version="3.10", python_version="3.10",
include_extras=extras, include_extras=extras,
max_depth=1 max_depth=1,
) )
total_extras = result['summary']['total_extra_dependencies'] total_extras = result["summary"]["total_extra_dependencies"]
print(f" Result: {total_extras} extra dependencies resolved") print(f" Result: {total_extras} extra dependencies resolved")
if total_extras == 0: if total_extras == 0:
print(f" ⚠️ No extras resolved - these extras likely don't exist") print(" ⚠️ No extras resolved - these extras likely don't exist")
except Exception as e: except Exception as e:
print(f" ❌ Error: {e}") print(f" ❌ Error: {e}")
@ -185,14 +190,16 @@ async def main():
await demo_extras_resolution() await demo_extras_resolution()
await demo_incorrect_usage() await demo_incorrect_usage()
print(f"\n{'='*60}") print(f"\n{'=' * 60}")
print("✨ Demo completed!") print("✨ Demo completed!")
print() print()
print("💡 Key takeaways:") print("💡 Key takeaways:")
print(" 1. Always check what extras are available for a package first") print(" 1. Always check what extras are available for a package first")
print(" 2. Use the exact extra names defined by the package") print(" 2. Use the exact extra names defined by the package")
print(" 3. Check package documentation or PyPI page for available extras") print(" 3. Check package documentation or PyPI page for available extras")
print(" 4. Not all packages have extras, and some extras may have no dependencies") print(
" 4. Not all packages have extras, and some extras may have no dependencies"
)
print() print()
print("📚 To find available extras:") print("📚 To find available extras:")
print(" - Check the package's PyPI page") print(" - Check the package's PyPI page")

View File

@ -2,8 +2,9 @@
"""Direct test of fallback mechanisms.""" """Direct test of fallback mechanisms."""
import asyncio import asyncio
import sys
import os import os
import sys
sys.path.insert(0, os.path.abspath(".")) sys.path.insert(0, os.path.abspath("."))
from pypi_query_mcp.core.stats_client import PyPIStatsClient from pypi_query_mcp.core.stats_client import PyPIStatsClient
@ -18,15 +19,17 @@ async def test_fallback():
client._api_health["consecutive_failures"] = 5 # Force fallback mode client._api_health["consecutive_failures"] = 5 # Force fallback mode
# Test recent downloads fallback # Test recent downloads fallback
fallback_recent = client._generate_fallback_recent_downloads("requests", "month") fallback_recent = client._generate_fallback_recent_downloads(
print(f"✅ Fallback recent downloads generated for requests:") "requests", "month"
)
print("✅ Fallback recent downloads generated for requests:")
print(f" Source: {fallback_recent.get('source')}") print(f" Source: {fallback_recent.get('source')}")
print(f" Downloads: {fallback_recent['data']['last_month']:,}") print(f" Downloads: {fallback_recent['data']['last_month']:,}")
print(f" Note: {fallback_recent.get('note')}") print(f" Note: {fallback_recent.get('note')}")
# Test overall downloads fallback # Test overall downloads fallback
fallback_overall = client._generate_fallback_overall_downloads("numpy", False) fallback_overall = client._generate_fallback_overall_downloads("numpy", False)
print(f"\n✅ Fallback time series generated for numpy:") print("\n✅ Fallback time series generated for numpy:")
print(f" Source: {fallback_overall.get('source')}") print(f" Source: {fallback_overall.get('source')}")
print(f" Data points: {len(fallback_overall['data'])}") print(f" Data points: {len(fallback_overall['data'])}")
print(f" Note: {fallback_overall.get('note')}") print(f" Note: {fallback_overall.get('note')}")

1010
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -120,9 +120,26 @@ class DependencyParser:
# Define development-related extra names # Define development-related extra names
dev_extra_names = { dev_extra_names = {
'dev', 'development', 'test', 'testing', 'tests', 'lint', 'linting', "dev",
'doc', 'docs', 'documentation', 'build', 'check', 'cover', 'coverage', "development",
'type', 'typing', 'mypy', 'style', 'format', 'quality' "test",
"testing",
"tests",
"lint",
"linting",
"doc",
"docs",
"documentation",
"build",
"check",
"cover",
"coverage",
"type",
"typing",
"mypy",
"style",
"format",
"quality",
} }
for req in requirements: for req in requirements:

View File

@ -2,7 +2,7 @@
import asyncio import asyncio
import logging import logging
from typing import Any, Dict, Optional from typing import Any
import httpx import httpx
@ -17,7 +17,7 @@ class GitHubAPIClient:
timeout: float = 10.0, timeout: float = 10.0,
max_retries: int = 2, max_retries: int = 2,
retry_delay: float = 1.0, retry_delay: float = 1.0,
github_token: Optional[str] = None, github_token: str | None = None,
): ):
"""Initialize GitHub API client. """Initialize GitHub API client.
@ -33,7 +33,7 @@ class GitHubAPIClient:
self.retry_delay = retry_delay self.retry_delay = retry_delay
# Simple in-memory cache for repository data # Simple in-memory cache for repository data
self._cache: Dict[str, Dict[str, Any]] = {} self._cache: dict[str, dict[str, Any]] = {}
self._cache_ttl = 3600 # 1 hour cache self._cache_ttl = 3600 # 1 hour cache
# HTTP client configuration # HTTP client configuration
@ -67,12 +67,13 @@ class GitHubAPIClient:
"""Generate cache key for repository data.""" """Generate cache key for repository data."""
return f"repo:{repo}" return f"repo:{repo}"
def _is_cache_valid(self, cache_entry: Dict[str, Any]) -> bool: def _is_cache_valid(self, cache_entry: dict[str, Any]) -> bool:
"""Check if cache entry is still valid.""" """Check if cache entry is still valid."""
import time import time
return time.time() - cache_entry.get("timestamp", 0) < self._cache_ttl return time.time() - cache_entry.get("timestamp", 0) < self._cache_ttl
async def _make_request(self, url: str) -> Optional[Dict[str, Any]]: async def _make_request(self, url: str) -> dict[str, Any] | None:
"""Make HTTP request with retry logic and error handling. """Make HTTP request with retry logic and error handling.
Args: Args:
@ -85,7 +86,9 @@ class GitHubAPIClient:
for attempt in range(self.max_retries + 1): for attempt in range(self.max_retries + 1):
try: try:
logger.debug(f"Making GitHub API request to {url} (attempt {attempt + 1})") logger.debug(
f"Making GitHub API request to {url} (attempt {attempt + 1})"
)
response = await self._client.get(url) response = await self._client.get(url)
@ -100,12 +103,16 @@ class GitHubAPIClient:
logger.warning(f"GitHub API rate limit or permission denied: {url}") logger.warning(f"GitHub API rate limit or permission denied: {url}")
return None return None
elif response.status_code >= 500: elif response.status_code >= 500:
logger.warning(f"GitHub API server error {response.status_code}: {url}") logger.warning(
f"GitHub API server error {response.status_code}: {url}"
)
if attempt < self.max_retries: if attempt < self.max_retries:
continue continue
return None return None
else: else:
logger.warning(f"Unexpected GitHub API status {response.status_code}: {url}") logger.warning(
f"Unexpected GitHub API status {response.status_code}: {url}"
)
return None return None
except httpx.TimeoutException: except httpx.TimeoutException:
@ -120,13 +127,17 @@ class GitHubAPIClient:
# Wait before retry (except on last attempt) # Wait before retry (except on last attempt)
if attempt < self.max_retries: if attempt < self.max_retries:
await asyncio.sleep(self.retry_delay * (2 ** attempt)) await asyncio.sleep(self.retry_delay * (2**attempt))
# If we get here, all retries failed # If we get here, all retries failed
logger.error(f"Failed to fetch GitHub data after {self.max_retries + 1} attempts: {last_exception}") logger.error(
f"Failed to fetch GitHub data after {self.max_retries + 1} attempts: {last_exception}"
)
return None return None
async def get_repository_stats(self, repo_path: str, use_cache: bool = True) -> Optional[Dict[str, Any]]: async def get_repository_stats(
self, repo_path: str, use_cache: bool = True
) -> dict[str, Any] | None:
"""Get repository statistics from GitHub API. """Get repository statistics from GitHub API.
Args: Args:
@ -171,14 +182,19 @@ class GitHubAPIClient:
"has_wiki": data.get("has_wiki", False), "has_wiki": data.get("has_wiki", False),
"archived": data.get("archived", False), "archived": data.get("archived", False),
"disabled": data.get("disabled", False), "disabled": data.get("disabled", False),
"license": data.get("license", {}).get("name") if data.get("license") else None, "license": data.get("license", {}).get("name")
if data.get("license")
else None,
} }
# Cache the result # Cache the result
import time import time
self._cache[cache_key] = {"data": stats, "timestamp": time.time()} self._cache[cache_key] = {"data": stats, "timestamp": time.time()}
logger.debug(f"Fetched GitHub stats for {repo_path}: {stats['stars']} stars") logger.debug(
f"Fetched GitHub stats for {repo_path}: {stats['stars']} stars"
)
return stats return stats
else: else:
return None return None
@ -188,11 +204,8 @@ class GitHubAPIClient:
return None return None
async def get_multiple_repo_stats( async def get_multiple_repo_stats(
self, self, repo_paths: list[str], use_cache: bool = True, max_concurrent: int = 5
repo_paths: list[str], ) -> dict[str, dict[str, Any] | None]:
use_cache: bool = True,
max_concurrent: int = 5
) -> Dict[str, Optional[Dict[str, Any]]]:
"""Get statistics for multiple repositories concurrently. """Get statistics for multiple repositories concurrently.
Args: Args:
@ -205,7 +218,7 @@ class GitHubAPIClient:
""" """
semaphore = asyncio.Semaphore(max_concurrent) semaphore = asyncio.Semaphore(max_concurrent)
async def fetch_repo_stats(repo_path: str) -> tuple[str, Optional[Dict[str, Any]]]: async def fetch_repo_stats(repo_path: str) -> tuple[str, dict[str, Any] | None]:
async with semaphore: async with semaphore:
stats = await self.get_repository_stats(repo_path, use_cache) stats = await self.get_repository_stats(repo_path, use_cache)
return repo_path, stats return repo_path, stats
@ -231,7 +244,7 @@ class GitHubAPIClient:
self._cache.clear() self._cache.clear()
logger.debug("GitHub cache cleared") logger.debug("GitHub cache cleared")
async def get_rate_limit(self) -> Optional[Dict[str, Any]]: async def get_rate_limit(self) -> dict[str, Any] | None:
"""Get current GitHub API rate limit status. """Get current GitHub API rate limit status.
Returns: Returns:

View File

@ -191,13 +191,17 @@ class PyPIClient:
if use_cache and cache_key in self._cache: if use_cache and cache_key in self._cache:
cache_entry = self._cache[cache_key] cache_entry = self._cache[cache_key]
if self._is_cache_valid(cache_entry): if self._is_cache_valid(cache_entry):
logger.debug(f"Using cached data for package: {normalized_name} version: {version or 'latest'}") logger.debug(
f"Using cached data for package: {normalized_name} version: {version or 'latest'}"
)
return cache_entry["data"] return cache_entry["data"]
# Build URL - include version if specified # Build URL - include version if specified
if version: if version:
url = f"{self.base_url}/{quote(normalized_name)}/{quote(version)}/json" url = f"{self.base_url}/{quote(normalized_name)}/{quote(version)}/json"
logger.info(f"Fetching package info for: {normalized_name} version {version}") logger.info(
f"Fetching package info for: {normalized_name} version {version}"
)
else: else:
url = f"{self.base_url}/{quote(normalized_name)}/json" url = f"{self.base_url}/{quote(normalized_name)}/json"
logger.info(f"Fetching package info for: {normalized_name} (latest)") logger.info(f"Fetching package info for: {normalized_name} (latest)")
@ -215,13 +219,19 @@ class PyPIClient:
except PackageNotFoundError as e: except PackageNotFoundError as e:
if version: if version:
# More specific error message for version not found # More specific error message for version not found
logger.error(f"Version {version} not found for package {normalized_name}") logger.error(
raise PackageNotFoundError(f"Version {version} not found for package {normalized_name}") f"Version {version} not found for package {normalized_name}"
)
raise PackageNotFoundError(
f"Version {version} not found for package {normalized_name}"
)
else: else:
logger.error(f"Failed to fetch package info for {normalized_name}: {e}") logger.error(f"Failed to fetch package info for {normalized_name}: {e}")
raise raise
except Exception as e: except Exception as e:
logger.error(f"Failed to fetch package info for {normalized_name} version {version or 'latest'}: {e}") logger.error(
f"Failed to fetch package info for {normalized_name} version {version or 'latest'}: {e}"
)
raise raise
async def get_package_versions( async def get_package_versions(
@ -236,7 +246,9 @@ class PyPIClient:
Returns: Returns:
List of version strings List of version strings
""" """
package_info = await self.get_package_info(package_name, version=None, use_cache=use_cache) package_info = await self.get_package_info(
package_name, version=None, use_cache=use_cache
)
releases = package_info.get("releases", {}) releases = package_info.get("releases", {})
return list(releases.keys()) return list(releases.keys())
@ -252,7 +264,9 @@ class PyPIClient:
Returns: Returns:
Latest version string Latest version string
""" """
package_info = await self.get_package_info(package_name, version=None, use_cache=use_cache) package_info = await self.get_package_info(
package_name, version=None, use_cache=use_cache
)
return package_info.get("info", {}).get("version", "") return package_info.get("info", {}).get("version", "")
def clear_cache(self): def clear_cache(self):

View File

@ -5,7 +5,7 @@ import logging
import random import random
import time import time
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Any, Dict, List, Optional from typing import Any
import httpx import httpx
@ -106,7 +106,9 @@ class PyPIStatsClient:
) )
return f"{endpoint}:{package_name}:{param_str}" return f"{endpoint}:{package_name}:{param_str}"
def _is_cache_valid(self, cache_entry: dict[str, Any], fallback: bool = False) -> bool: def _is_cache_valid(
self, cache_entry: dict[str, Any], fallback: bool = False
) -> bool:
"""Check if cache entry is still valid. """Check if cache entry is still valid.
Args: Args:
@ -152,7 +154,9 @@ class PyPIStatsClient:
for attempt in range(self.max_retries + 1): for attempt in range(self.max_retries + 1):
try: try:
logger.debug(f"Making request to {url} (attempt {attempt + 1}/{self.max_retries + 1})") logger.debug(
f"Making request to {url} (attempt {attempt + 1}/{self.max_retries + 1})"
)
response = await self._client.get(url) response = await self._client.get(url)
@ -171,16 +175,25 @@ class PyPIStatsClient:
elif response.status_code == 429: elif response.status_code == 429:
retry_after = response.headers.get("Retry-After") retry_after = response.headers.get("Retry-After")
retry_after_int = int(retry_after) if retry_after else None retry_after_int = int(retry_after) if retry_after else None
self._update_api_failure(f"Rate limit exceeded (retry after {retry_after_int}s)") self._update_api_failure(
f"Rate limit exceeded (retry after {retry_after_int}s)"
)
raise RateLimitError(retry_after_int) raise RateLimitError(retry_after_int)
elif response.status_code >= 500: elif response.status_code >= 500:
error_msg = f"Server error: HTTP {response.status_code}" error_msg = f"Server error: HTTP {response.status_code}"
self._update_api_failure(error_msg) self._update_api_failure(error_msg)
# For 502/503/504 errors, continue retrying # For 502/503/504 errors, continue retrying
if response.status_code in [502, 503, 504] and attempt < self.max_retries: if (
last_exception = PyPIServerError(response.status_code, error_msg) response.status_code in [502, 503, 504]
logger.warning(f"Retryable server error {response.status_code}, attempt {attempt + 1}") and attempt < self.max_retries
):
last_exception = PyPIServerError(
response.status_code, error_msg
)
logger.warning(
f"Retryable server error {response.status_code}, attempt {attempt + 1}"
)
else: else:
raise PyPIServerError(response.status_code, error_msg) raise PyPIServerError(response.status_code, error_msg)
else: else:
@ -205,7 +218,9 @@ class PyPIStatsClient:
# Only retry certain server errors # Only retry certain server errors
if e.status_code in [502, 503, 504] and attempt < self.max_retries: if e.status_code in [502, 503, 504] and attempt < self.max_retries:
last_exception = e last_exception = e
logger.warning(f"Retrying server error {e.status_code}, attempt {attempt + 1}") logger.warning(
f"Retrying server error {e.status_code}, attempt {attempt + 1}"
)
else: else:
raise raise
except Exception as e: except Exception as e:
@ -216,7 +231,7 @@ class PyPIStatsClient:
# Calculate exponential backoff with jitter # Calculate exponential backoff with jitter
if attempt < self.max_retries: if attempt < self.max_retries:
base_delay = self.retry_delay * (2 ** attempt) base_delay = self.retry_delay * (2**attempt)
jitter = random.uniform(0.1, 0.3) * base_delay # Add 10-30% jitter jitter = random.uniform(0.1, 0.3) * base_delay # Add 10-30% jitter
delay = base_delay + jitter delay = base_delay + jitter
logger.debug(f"Waiting {delay:.2f}s before retry...") logger.debug(f"Waiting {delay:.2f}s before retry...")
@ -232,9 +247,13 @@ class PyPIStatsClient:
"""Update API health tracking on failure.""" """Update API health tracking on failure."""
self._api_health["consecutive_failures"] += 1 self._api_health["consecutive_failures"] += 1
self._api_health["last_error"] = error_msg self._api_health["last_error"] = error_msg
logger.debug(f"API failure count: {self._api_health['consecutive_failures']}, error: {error_msg}") logger.debug(
f"API failure count: {self._api_health['consecutive_failures']}, error: {error_msg}"
)
def _generate_fallback_recent_downloads(self, package_name: str, period: str = "month") -> dict[str, Any]: def _generate_fallback_recent_downloads(
self, package_name: str, period: str = "month"
) -> dict[str, Any]:
"""Generate fallback download statistics when API is unavailable. """Generate fallback download statistics when API is unavailable.
This provides estimated download counts based on package popularity patterns This provides estimated download counts based on package popularity patterns
@ -276,16 +295,27 @@ class PyPIStatsClient:
estimates = popular_packages[package_name.lower()] estimates = popular_packages[package_name.lower()]
else: else:
# Generate estimates based on common package patterns # Generate estimates based on common package patterns
if any(keyword in package_name.lower() for keyword in ["test", "dev", "debug"]): if any(
keyword in package_name.lower() for keyword in ["test", "dev", "debug"]
):
# Development/testing packages - lower usage # Development/testing packages - lower usage
base_daily = random.randint(100, 1000) base_daily = random.randint(100, 1000)
elif any(keyword in package_name.lower() for keyword in ["aws", "google", "microsoft", "azure"]): elif any(
keyword in package_name.lower()
for keyword in ["aws", "google", "microsoft", "azure"]
):
# Cloud provider packages - higher usage # Cloud provider packages - higher usage
base_daily = random.randint(10000, 50000) base_daily = random.randint(10000, 50000)
elif any(keyword in package_name.lower() for keyword in ["http", "request", "client", "api"]): elif any(
keyword in package_name.lower()
for keyword in ["http", "request", "client", "api"]
):
# HTTP/API packages - moderate to high usage # HTTP/API packages - moderate to high usage
base_daily = random.randint(5000, 25000) base_daily = random.randint(5000, 25000)
elif any(keyword in package_name.lower() for keyword in ["data", "pandas", "numpy", "scipy"]): elif any(
keyword in package_name.lower()
for keyword in ["data", "pandas", "numpy", "scipy"]
):
# Data science packages - high usage # Data science packages - high usage
base_daily = random.randint(15000, 75000) base_daily = random.randint(15000, 75000)
else: else:
@ -315,7 +345,9 @@ class PyPIStatsClient:
"note": "Estimated data due to API unavailability. Actual values may differ.", "note": "Estimated data due to API unavailability. Actual values may differ.",
} }
def _generate_fallback_overall_downloads(self, package_name: str, mirrors: bool = False) -> dict[str, Any]: def _generate_fallback_overall_downloads(
self, package_name: str, mirrors: bool = False
) -> dict[str, Any]:
"""Generate fallback time series data when API is unavailable.""" """Generate fallback time series data when API is unavailable."""
logger.warning(f"Generating fallback time series data for {package_name}") logger.warning(f"Generating fallback time series data for {package_name}")
@ -341,14 +373,18 @@ class PyPIStatsClient:
# Add random daily variation # Add random daily variation
daily_variation = random.uniform(0.7, 1.3) daily_variation = random.uniform(0.7, 1.3)
daily_downloads = int(base_daily * week_factor * growth_factor * daily_variation) daily_downloads = int(
base_daily * week_factor * growth_factor * daily_variation
)
category = "with_mirrors" if mirrors else "without_mirrors" category = "with_mirrors" if mirrors else "without_mirrors"
time_series.append({ time_series.append(
"category": category, {
"date": current_date.strftime("%Y-%m-%d"), "category": category,
"downloads": daily_downloads, "date": current_date.strftime("%Y-%m-%d"),
}) "downloads": daily_downloads,
}
)
return { return {
"data": time_series, "data": time_series,
@ -385,15 +421,23 @@ class PyPIStatsClient:
if self._is_cache_valid(cache_entry): if self._is_cache_valid(cache_entry):
logger.debug(f"Using cached recent downloads for: {normalized_name}") logger.debug(f"Using cached recent downloads for: {normalized_name}")
return cache_entry["data"] return cache_entry["data"]
elif self._should_use_fallback() and self._is_cache_valid(cache_entry, fallback=True): elif self._should_use_fallback() and self._is_cache_valid(
logger.info(f"Using extended cache (fallback mode) for: {normalized_name}") cache_entry, fallback=True
):
logger.info(
f"Using extended cache (fallback mode) for: {normalized_name}"
)
cache_entry["data"]["note"] = "Extended cache data due to API issues" cache_entry["data"]["note"] = "Extended cache data due to API issues"
return cache_entry["data"] return cache_entry["data"]
# Check if we should use fallback immediately # Check if we should use fallback immediately
if self._should_use_fallback(): if self._should_use_fallback():
logger.warning(f"API health poor, using fallback data for: {normalized_name}") logger.warning(
fallback_data = self._generate_fallback_recent_downloads(normalized_name, period) f"API health poor, using fallback data for: {normalized_name}"
)
fallback_data = self._generate_fallback_recent_downloads(
normalized_name, period
)
# Cache fallback data with extended TTL # Cache fallback data with extended TTL
self._cache[cache_key] = {"data": fallback_data, "timestamp": time.time()} self._cache[cache_key] = {"data": fallback_data, "timestamp": time.time()}
@ -422,24 +466,35 @@ class PyPIStatsClient:
# Try to use stale cache data if available # Try to use stale cache data if available
if use_cache and cache_key in self._cache: if use_cache and cache_key in self._cache:
cache_entry = self._cache[cache_key] cache_entry = self._cache[cache_key]
logger.warning(f"Using stale cache data for {normalized_name} due to API failure") logger.warning(
f"Using stale cache data for {normalized_name} due to API failure"
)
cache_entry["data"]["note"] = f"Stale cache data due to API error: {e}" cache_entry["data"]["note"] = f"Stale cache data due to API error: {e}"
return cache_entry["data"] return cache_entry["data"]
# Last resort: generate fallback data # Last resort: generate fallback data
if self.fallback_enabled: if self.fallback_enabled:
logger.warning(f"Generating fallback data for {normalized_name} due to API failure") logger.warning(
fallback_data = self._generate_fallback_recent_downloads(normalized_name, period) f"Generating fallback data for {normalized_name} due to API failure"
)
fallback_data = self._generate_fallback_recent_downloads(
normalized_name, period
)
# Cache fallback data # Cache fallback data
self._cache[cache_key] = {"data": fallback_data, "timestamp": time.time()} self._cache[cache_key] = {
"data": fallback_data,
"timestamp": time.time(),
}
return fallback_data return fallback_data
# If fallback is disabled, re-raise the original exception # If fallback is disabled, re-raise the original exception
raise raise
except Exception as e: except Exception as e:
logger.error(f"Unexpected error fetching recent downloads for {normalized_name}: {e}") logger.error(
f"Unexpected error fetching recent downloads for {normalized_name}: {e}"
)
raise raise
async def get_overall_downloads( async def get_overall_downloads(
@ -469,15 +524,23 @@ class PyPIStatsClient:
if self._is_cache_valid(cache_entry): if self._is_cache_valid(cache_entry):
logger.debug(f"Using cached overall downloads for: {normalized_name}") logger.debug(f"Using cached overall downloads for: {normalized_name}")
return cache_entry["data"] return cache_entry["data"]
elif self._should_use_fallback() and self._is_cache_valid(cache_entry, fallback=True): elif self._should_use_fallback() and self._is_cache_valid(
logger.info(f"Using extended cache (fallback mode) for: {normalized_name}") cache_entry, fallback=True
):
logger.info(
f"Using extended cache (fallback mode) for: {normalized_name}"
)
cache_entry["data"]["note"] = "Extended cache data due to API issues" cache_entry["data"]["note"] = "Extended cache data due to API issues"
return cache_entry["data"] return cache_entry["data"]
# Check if we should use fallback immediately # Check if we should use fallback immediately
if self._should_use_fallback(): if self._should_use_fallback():
logger.warning(f"API health poor, using fallback data for: {normalized_name}") logger.warning(
fallback_data = self._generate_fallback_overall_downloads(normalized_name, mirrors) f"API health poor, using fallback data for: {normalized_name}"
)
fallback_data = self._generate_fallback_overall_downloads(
normalized_name, mirrors
)
# Cache fallback data with extended TTL # Cache fallback data with extended TTL
self._cache[cache_key] = {"data": fallback_data, "timestamp": time.time()} self._cache[cache_key] = {"data": fallback_data, "timestamp": time.time()}
@ -506,24 +569,35 @@ class PyPIStatsClient:
# Try to use stale cache data if available # Try to use stale cache data if available
if use_cache and cache_key in self._cache: if use_cache and cache_key in self._cache:
cache_entry = self._cache[cache_key] cache_entry = self._cache[cache_key]
logger.warning(f"Using stale cache data for {normalized_name} due to API failure") logger.warning(
f"Using stale cache data for {normalized_name} due to API failure"
)
cache_entry["data"]["note"] = f"Stale cache data due to API error: {e}" cache_entry["data"]["note"] = f"Stale cache data due to API error: {e}"
return cache_entry["data"] return cache_entry["data"]
# Last resort: generate fallback data # Last resort: generate fallback data
if self.fallback_enabled: if self.fallback_enabled:
logger.warning(f"Generating fallback data for {normalized_name} due to API failure") logger.warning(
fallback_data = self._generate_fallback_overall_downloads(normalized_name, mirrors) f"Generating fallback data for {normalized_name} due to API failure"
)
fallback_data = self._generate_fallback_overall_downloads(
normalized_name, mirrors
)
# Cache fallback data # Cache fallback data
self._cache[cache_key] = {"data": fallback_data, "timestamp": time.time()} self._cache[cache_key] = {
"data": fallback_data,
"timestamp": time.time(),
}
return fallback_data return fallback_data
# If fallback is disabled, re-raise the original exception # If fallback is disabled, re-raise the original exception
raise raise
except Exception as e: except Exception as e:
logger.error(f"Unexpected error fetching overall downloads for {normalized_name}: {e}") logger.error(
f"Unexpected error fetching overall downloads for {normalized_name}: {e}"
)
raise raise
def clear_cache(self): def clear_cache(self):

View File

@ -10,10 +10,12 @@ The rankings and download estimates are based on:
Data is organized by categories and includes estimated relative popularity. Data is organized by categories and includes estimated relative popularity.
""" """
from typing import Dict, List, NamedTuple from typing import NamedTuple
class PackageInfo(NamedTuple): class PackageInfo(NamedTuple):
"""Information about a popular package.""" """Information about a popular package."""
name: str name: str
category: str category: str
estimated_monthly_downloads: int estimated_monthly_downloads: int
@ -21,60 +23,226 @@ class PackageInfo(NamedTuple):
description: str description: str
primary_use_case: str primary_use_case: str
# Core packages that are dependencies for many other packages # Core packages that are dependencies for many other packages
INFRASTRUCTURE_PACKAGES = [ INFRASTRUCTURE_PACKAGES = [
PackageInfo("setuptools", "packaging", 800_000_000, 2100, "Package development tools", "packaging"), PackageInfo(
PackageInfo("wheel", "packaging", 700_000_000, 400, "Binary package format", "packaging"), "setuptools",
PackageInfo("pip", "packaging", 600_000_000, 9500, "Package installer", "packaging"), "packaging",
PackageInfo("certifi", "security", 500_000_000, 800, "Certificate bundle", "security"), 800_000_000,
PackageInfo("urllib3", "networking", 450_000_000, 3600, "HTTP client library", "networking"), 2100,
PackageInfo("charset-normalizer", "text", 400_000_000, 400, "Character encoding detection", "text-processing"), "Package development tools",
PackageInfo("idna", "networking", 380_000_000, 200, "Internationalized domain names", "networking"), "packaging",
PackageInfo("six", "compatibility", 350_000_000, 900, "Python 2 and 3 compatibility", "compatibility"), ),
PackageInfo("python-dateutil", "datetime", 320_000_000, 2200, "Date and time utilities", "datetime"), PackageInfo(
PackageInfo("requests", "networking", 300_000_000, 51000, "HTTP library", "networking"), "wheel", "packaging", 700_000_000, 400, "Binary package format", "packaging"
),
PackageInfo(
"pip", "packaging", 600_000_000, 9500, "Package installer", "packaging"
),
PackageInfo(
"certifi", "security", 500_000_000, 800, "Certificate bundle", "security"
),
PackageInfo(
"urllib3", "networking", 450_000_000, 3600, "HTTP client library", "networking"
),
PackageInfo(
"charset-normalizer",
"text",
400_000_000,
400,
"Character encoding detection",
"text-processing",
),
PackageInfo(
"idna",
"networking",
380_000_000,
200,
"Internationalized domain names",
"networking",
),
PackageInfo(
"six",
"compatibility",
350_000_000,
900,
"Python 2 and 3 compatibility",
"compatibility",
),
PackageInfo(
"python-dateutil",
"datetime",
320_000_000,
2200,
"Date and time utilities",
"datetime",
),
PackageInfo(
"requests", "networking", 300_000_000, 51000, "HTTP library", "networking"
),
] ]
# AWS and cloud packages # AWS and cloud packages
CLOUD_PACKAGES = [ CLOUD_PACKAGES = [
PackageInfo("boto3", "cloud", 280_000_000, 8900, "AWS SDK", "cloud"), PackageInfo("boto3", "cloud", 280_000_000, 8900, "AWS SDK", "cloud"),
PackageInfo("botocore", "cloud", 275_000_000, 1400, "AWS SDK core", "cloud"), PackageInfo("botocore", "cloud", 275_000_000, 1400, "AWS SDK core", "cloud"),
PackageInfo("s3transfer", "cloud", 250_000_000, 200, "S3 transfer utilities", "cloud"), PackageInfo(
"s3transfer", "cloud", 250_000_000, 200, "S3 transfer utilities", "cloud"
),
PackageInfo("awscli", "cloud", 80_000_000, 15000, "AWS command line", "cloud"), PackageInfo("awscli", "cloud", 80_000_000, 15000, "AWS command line", "cloud"),
PackageInfo("azure-core", "cloud", 45_000_000, 400, "Azure SDK core", "cloud"), PackageInfo("azure-core", "cloud", 45_000_000, 400, "Azure SDK core", "cloud"),
PackageInfo("google-cloud-storage", "cloud", 35_000_000, 300, "Google Cloud Storage", "cloud"), PackageInfo(
PackageInfo("azure-storage-blob", "cloud", 30_000_000, 200, "Azure Blob Storage", "cloud"), "google-cloud-storage",
"cloud",
35_000_000,
300,
"Google Cloud Storage",
"cloud",
),
PackageInfo(
"azure-storage-blob", "cloud", 30_000_000, 200, "Azure Blob Storage", "cloud"
),
] ]
# Data science and ML packages # Data science and ML packages
DATA_SCIENCE_PACKAGES = [ DATA_SCIENCE_PACKAGES = [
PackageInfo("numpy", "data-science", 200_000_000, 26000, "Numerical computing", "data-science"), PackageInfo(
PackageInfo("pandas", "data-science", 150_000_000, 42000, "Data manipulation", "data-science"), "numpy",
PackageInfo("scikit-learn", "machine-learning", 80_000_000, 58000, "Machine learning", "machine-learning"), "data-science",
PackageInfo("matplotlib", "visualization", 75_000_000, 19000, "Plotting library", "visualization"), 200_000_000,
PackageInfo("scipy", "data-science", 70_000_000, 12000, "Scientific computing", "data-science"), 26000,
PackageInfo("seaborn", "visualization", 45_000_000, 11000, "Statistical visualization", "visualization"), "Numerical computing",
PackageInfo("plotly", "visualization", 40_000_000, 15000, "Interactive plots", "visualization"), "data-science",
PackageInfo("jupyter", "development", 35_000_000, 7000, "Interactive notebooks", "development"), ),
PackageInfo("ipython", "development", 50_000_000, 8000, "Interactive Python", "development"), PackageInfo(
PackageInfo("tensorflow", "machine-learning", 25_000_000, 185000, "Deep learning", "machine-learning"), "pandas",
PackageInfo("torch", "machine-learning", 20_000_000, 81000, "PyTorch deep learning", "machine-learning"), "data-science",
PackageInfo("transformers", "machine-learning", 15_000_000, 130000, "NLP transformers", "machine-learning"), 150_000_000,
42000,
"Data manipulation",
"data-science",
),
PackageInfo(
"scikit-learn",
"machine-learning",
80_000_000,
58000,
"Machine learning",
"machine-learning",
),
PackageInfo(
"matplotlib",
"visualization",
75_000_000,
19000,
"Plotting library",
"visualization",
),
PackageInfo(
"scipy",
"data-science",
70_000_000,
12000,
"Scientific computing",
"data-science",
),
PackageInfo(
"seaborn",
"visualization",
45_000_000,
11000,
"Statistical visualization",
"visualization",
),
PackageInfo(
"plotly",
"visualization",
40_000_000,
15000,
"Interactive plots",
"visualization",
),
PackageInfo(
"jupyter",
"development",
35_000_000,
7000,
"Interactive notebooks",
"development",
),
PackageInfo(
"ipython", "development", 50_000_000, 8000, "Interactive Python", "development"
),
PackageInfo(
"tensorflow",
"machine-learning",
25_000_000,
185000,
"Deep learning",
"machine-learning",
),
PackageInfo(
"torch",
"machine-learning",
20_000_000,
81000,
"PyTorch deep learning",
"machine-learning",
),
PackageInfo(
"transformers",
"machine-learning",
15_000_000,
130000,
"NLP transformers",
"machine-learning",
),
] ]
# Development and testing # Development and testing
DEVELOPMENT_PACKAGES = [ DEVELOPMENT_PACKAGES = [
PackageInfo("typing-extensions", "development", 180_000_000, 3000, "Typing extensions", "development"), PackageInfo(
PackageInfo("packaging", "development", 160_000_000, 600, "Package utilities", "development"), "typing-extensions",
PackageInfo("pytest", "testing", 100_000_000, 11000, "Testing framework", "testing"), "development",
180_000_000,
3000,
"Typing extensions",
"development",
),
PackageInfo(
"packaging", "development", 160_000_000, 600, "Package utilities", "development"
),
PackageInfo(
"pytest", "testing", 100_000_000, 11000, "Testing framework", "testing"
),
PackageInfo("click", "cli", 90_000_000, 15000, "Command line interface", "cli"), PackageInfo("click", "cli", 90_000_000, 15000, "Command line interface", "cli"),
PackageInfo("pyyaml", "serialization", 85_000_000, 2200, "YAML parser", "serialization"), PackageInfo(
PackageInfo("jinja2", "templating", 80_000_000, 10000, "Template engine", "templating"), "pyyaml", "serialization", 85_000_000, 2200, "YAML parser", "serialization"
PackageInfo("markupsafe", "templating", 75_000_000, 600, "Safe markup", "templating"), ),
PackageInfo("attrs", "development", 60_000_000, 5000, "Classes without boilerplate", "development"), PackageInfo(
PackageInfo("black", "development", 40_000_000, 38000, "Code formatter", "development"), "jinja2", "templating", 80_000_000, 10000, "Template engine", "templating"
PackageInfo("flake8", "development", 35_000_000, 3000, "Code linting", "development"), ),
PackageInfo("mypy", "development", 30_000_000, 17000, "Static type checker", "development"), PackageInfo(
"markupsafe", "templating", 75_000_000, 600, "Safe markup", "templating"
),
PackageInfo(
"attrs",
"development",
60_000_000,
5000,
"Classes without boilerplate",
"development",
),
PackageInfo(
"black", "development", 40_000_000, 38000, "Code formatter", "development"
),
PackageInfo(
"flake8", "development", 35_000_000, 3000, "Code linting", "development"
),
PackageInfo(
"mypy", "development", 30_000_000, 17000, "Static type checker", "development"
),
] ]
# Web development # Web development
@ -83,49 +251,87 @@ WEB_PACKAGES = [
PackageInfo("flask", "web", 55_000_000, 66000, "Micro web framework", "web"), PackageInfo("flask", "web", 55_000_000, 66000, "Micro web framework", "web"),
PackageInfo("fastapi", "web", 35_000_000, 74000, "Modern web API framework", "web"), PackageInfo("fastapi", "web", 35_000_000, 74000, "Modern web API framework", "web"),
PackageInfo("sqlalchemy", "database", 50_000_000, 8000, "SQL toolkit", "database"), PackageInfo("sqlalchemy", "database", 50_000_000, 8000, "SQL toolkit", "database"),
PackageInfo("psycopg2", "database", 25_000_000, 3000, "PostgreSQL adapter", "database"), PackageInfo(
"psycopg2", "database", 25_000_000, 3000, "PostgreSQL adapter", "database"
),
PackageInfo("redis", "database", 30_000_000, 12000, "Redis client", "database"), PackageInfo("redis", "database", 30_000_000, 12000, "Redis client", "database"),
PackageInfo("celery", "async", 25_000_000, 23000, "Distributed task queue", "async"), PackageInfo(
"celery", "async", 25_000_000, 23000, "Distributed task queue", "async"
),
PackageInfo("gunicorn", "web", 20_000_000, 9000, "WSGI server", "web"), PackageInfo("gunicorn", "web", 20_000_000, 9000, "WSGI server", "web"),
PackageInfo("uvicorn", "web", 15_000_000, 8000, "ASGI server", "web"), PackageInfo("uvicorn", "web", 15_000_000, 8000, "ASGI server", "web"),
] ]
# Security and cryptography # Security and cryptography
SECURITY_PACKAGES = [ SECURITY_PACKAGES = [
PackageInfo("cryptography", "security", 120_000_000, 6000, "Cryptographic library", "security"), PackageInfo(
PackageInfo("pyopenssl", "security", 60_000_000, 800, "OpenSSL wrapper", "security"), "cryptography",
"security",
120_000_000,
6000,
"Cryptographic library",
"security",
),
PackageInfo(
"pyopenssl", "security", 60_000_000, 800, "OpenSSL wrapper", "security"
),
PackageInfo("pyjwt", "security", 40_000_000, 5000, "JSON Web Tokens", "security"), PackageInfo("pyjwt", "security", 40_000_000, 5000, "JSON Web Tokens", "security"),
PackageInfo("bcrypt", "security", 35_000_000, 1200, "Password hashing", "security"), PackageInfo("bcrypt", "security", 35_000_000, 1200, "Password hashing", "security"),
PackageInfo("pycryptodome", "security", 30_000_000, 2700, "Cryptographic library", "security"), PackageInfo(
"pycryptodome",
"security",
30_000_000,
2700,
"Cryptographic library",
"security",
),
] ]
# Networking and API # Networking and API
NETWORKING_PACKAGES = [ NETWORKING_PACKAGES = [
PackageInfo("httpx", "networking", 25_000_000, 12000, "HTTP client", "networking"), PackageInfo("httpx", "networking", 25_000_000, 12000, "HTTP client", "networking"),
PackageInfo("aiohttp", "networking", 35_000_000, 14000, "Async HTTP", "networking"), PackageInfo("aiohttp", "networking", 35_000_000, 14000, "Async HTTP", "networking"),
PackageInfo("websockets", "networking", 20_000_000, 5000, "WebSocket implementation", "networking"), PackageInfo(
"websockets",
"networking",
20_000_000,
5000,
"WebSocket implementation",
"networking",
),
PackageInfo("paramiko", "networking", 25_000_000, 8000, "SSH client", "networking"), PackageInfo("paramiko", "networking", 25_000_000, 8000, "SSH client", "networking"),
] ]
# Text processing and parsing # Text processing and parsing
TEXT_PACKAGES = [ TEXT_PACKAGES = [
PackageInfo("beautifulsoup4", "parsing", 40_000_000, 13000, "HTML/XML parser", "parsing"), PackageInfo(
"beautifulsoup4", "parsing", 40_000_000, 13000, "HTML/XML parser", "parsing"
),
PackageInfo("lxml", "parsing", 35_000_000, 2600, "XML/HTML parser", "parsing"), PackageInfo("lxml", "parsing", 35_000_000, 2600, "XML/HTML parser", "parsing"),
PackageInfo("regex", "text", 30_000_000, 700, "Regular expressions", "text-processing"), PackageInfo(
PackageInfo("python-docx", "text", 15_000_000, 4000, "Word document processing", "text-processing"), "regex", "text", 30_000_000, 700, "Regular expressions", "text-processing"
),
PackageInfo(
"python-docx",
"text",
15_000_000,
4000,
"Word document processing",
"text-processing",
),
PackageInfo("pillow", "imaging", 60_000_000, 11000, "Image processing", "imaging"), PackageInfo("pillow", "imaging", 60_000_000, 11000, "Image processing", "imaging"),
] ]
# All packages combined for easy access # All packages combined for easy access
ALL_POPULAR_PACKAGES = ( ALL_POPULAR_PACKAGES = (
INFRASTRUCTURE_PACKAGES + INFRASTRUCTURE_PACKAGES
CLOUD_PACKAGES + + CLOUD_PACKAGES
DATA_SCIENCE_PACKAGES + + DATA_SCIENCE_PACKAGES
DEVELOPMENT_PACKAGES + + DEVELOPMENT_PACKAGES
WEB_PACKAGES + + WEB_PACKAGES
SECURITY_PACKAGES + + SECURITY_PACKAGES
NETWORKING_PACKAGES + + NETWORKING_PACKAGES
TEXT_PACKAGES + TEXT_PACKAGES
) )
# Create lookup dictionaries # Create lookup dictionaries
@ -136,11 +342,10 @@ for pkg in ALL_POPULAR_PACKAGES:
PACKAGES_BY_CATEGORY[pkg.category] = [] PACKAGES_BY_CATEGORY[pkg.category] = []
PACKAGES_BY_CATEGORY[pkg.category].append(pkg) PACKAGES_BY_CATEGORY[pkg.category].append(pkg)
def get_popular_packages( def get_popular_packages(
category: str = None, category: str = None, limit: int = 50, min_downloads: int = 0
limit: int = 50, ) -> list[PackageInfo]:
min_downloads: int = 0
) -> List[PackageInfo]:
"""Get popular packages filtered by criteria. """Get popular packages filtered by criteria.
Args: Args:
@ -157,13 +362,18 @@ def get_popular_packages(
packages = [pkg for pkg in packages if pkg.category == category] packages = [pkg for pkg in packages if pkg.category == category]
if min_downloads: if min_downloads:
packages = [pkg for pkg in packages if pkg.estimated_monthly_downloads >= min_downloads] packages = [
pkg for pkg in packages if pkg.estimated_monthly_downloads >= min_downloads
]
# Sort by estimated downloads (descending) # Sort by estimated downloads (descending)
packages = sorted(packages, key=lambda x: x.estimated_monthly_downloads, reverse=True) packages = sorted(
packages, key=lambda x: x.estimated_monthly_downloads, reverse=True
)
return packages[:limit] return packages[:limit]
def estimate_downloads_for_period(monthly_downloads: int, period: str) -> int: def estimate_downloads_for_period(monthly_downloads: int, period: str) -> int:
"""Estimate downloads for different time periods. """Estimate downloads for different time periods.
@ -183,6 +393,7 @@ def estimate_downloads_for_period(monthly_downloads: int, period: str) -> int:
else: else:
return monthly_downloads return monthly_downloads
def get_package_info(package_name: str) -> PackageInfo: def get_package_info(package_name: str) -> PackageInfo:
"""Get information about a specific package. """Get information about a specific package.
@ -192,7 +403,10 @@ def get_package_info(package_name: str) -> PackageInfo:
Returns: Returns:
PackageInfo object or None if not found PackageInfo object or None if not found
""" """
return PACKAGES_BY_NAME.get(package_name.lower().replace("-", "_").replace("_", "-")) return PACKAGES_BY_NAME.get(
package_name.lower().replace("-", "_").replace("_", "-")
)
# GitHub repository URL patterns for fetching real-time data # GitHub repository URL patterns for fetching real-time data
GITHUB_REPO_PATTERNS = { GITHUB_REPO_PATTERNS = {

View File

@ -140,7 +140,7 @@ async def get_package_dependencies(
version: str | None = None, version: str | None = None,
include_transitive: bool = False, include_transitive: bool = False,
max_depth: int = 5, max_depth: int = 5,
python_version: str | None = None python_version: str | None = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Get dependency information for a PyPI package. """Get dependency information for a PyPI package.
@ -175,7 +175,11 @@ async def get_package_dependencies(
logger.info( logger.info(
f"MCP tool: Querying dependencies for {package_name}" f"MCP tool: Querying dependencies for {package_name}"
+ (f" version {version}" if version else " (latest)") + (f" version {version}" if version else " (latest)")
+ (f" with transitive dependencies (max depth: {max_depth})" if include_transitive else " (direct only)") + (
f" with transitive dependencies (max depth: {max_depth})"
if include_transitive
else " (direct only)"
)
) )
result = await query_package_dependencies( result = await query_package_dependencies(
package_name, version, include_transitive, max_depth, python_version package_name, version, include_transitive, max_depth, python_version

View File

@ -3,14 +3,13 @@
import logging import logging
import os import os
from datetime import datetime from datetime import datetime
from typing import Any, Dict, List, Optional from typing import Any
from ..core.github_client import GitHubAPIClient from ..core.github_client import GitHubAPIClient
from ..core.pypi_client import PyPIClient from ..core.pypi_client import PyPIClient
from ..core.stats_client import PyPIStatsClient from ..core.stats_client import PyPIStatsClient
from ..data.popular_packages import ( from ..data.popular_packages import (
GITHUB_REPO_PATTERNS, GITHUB_REPO_PATTERNS,
PACKAGES_BY_NAME,
estimate_downloads_for_period, estimate_downloads_for_period,
get_popular_packages, get_popular_packages,
) )
@ -95,7 +94,9 @@ async def get_package_download_stats(
# Add reliability indicator # Add reliability indicator
if data_source == "fallback_estimates": if data_source == "fallback_estimates":
result["reliability"] = "estimated" result["reliability"] = "estimated"
result["warning"] = "Data is estimated due to API unavailability. Actual download counts may differ significantly." result["warning"] = (
"Data is estimated due to API unavailability. Actual download counts may differ significantly."
)
elif "stale" in warning_note.lower() if warning_note else False: elif "stale" in warning_note.lower() if warning_note else False:
result["reliability"] = "cached" result["reliability"] = "cached"
result["warning"] = "Data may be outdated due to current API issues." result["warning"] = "Data may be outdated due to current API issues."
@ -163,7 +164,9 @@ async def get_package_download_trends(
# Add reliability indicator # Add reliability indicator
if data_source == "fallback_estimates": if data_source == "fallback_estimates":
result["reliability"] = "estimated" result["reliability"] = "estimated"
result["warning"] = "Data is estimated due to API unavailability. Actual download trends may differ significantly." result["warning"] = (
"Data is estimated due to API unavailability. Actual download trends may differ significantly."
)
elif "stale" in warning_note.lower() if warning_note else False: elif "stale" in warning_note.lower() if warning_note else False:
result["reliability"] = "cached" result["reliability"] = "cached"
result["warning"] = "Data may be outdated due to current API issues." result["warning"] = "Data may be outdated due to current API issues."
@ -203,14 +206,10 @@ async def get_top_packages_by_downloads(
curated_packages = get_popular_packages(limit=max(limit * 2, 100)) curated_packages = get_popular_packages(limit=max(limit * 2, 100))
# Try to enhance with real PyPI stats # Try to enhance with real PyPI stats
enhanced_packages = await _enhance_with_real_stats( enhanced_packages = await _enhance_with_real_stats(curated_packages, period, limit)
curated_packages, period, limit
)
# Try to enhance with GitHub metrics # Try to enhance with GitHub metrics
final_packages = await _enhance_with_github_stats( final_packages = await _enhance_with_github_stats(enhanced_packages, limit)
enhanced_packages, limit
)
# Ensure we have the requested number of packages # Ensure we have the requested number of packages
if len(final_packages) < limit: if len(final_packages) < limit:
@ -220,17 +219,19 @@ async def get_top_packages_by_downloads(
for pkg_info in curated_packages: for pkg_info in curated_packages:
if pkg_info.name not in existing_names and additional_needed > 0: if pkg_info.name not in existing_names and additional_needed > 0:
final_packages.append({ final_packages.append(
"package": pkg_info.name, {
"downloads": estimate_downloads_for_period( "package": pkg_info.name,
pkg_info.estimated_monthly_downloads, period "downloads": estimate_downloads_for_period(
), pkg_info.estimated_monthly_downloads, period
"period": period, ),
"data_source": "curated", "period": period,
"category": pkg_info.category, "data_source": "curated",
"description": pkg_info.description, "category": pkg_info.category,
"estimated": True, "description": pkg_info.description,
}) "estimated": True,
}
)
additional_needed -= 1 additional_needed -= 1
# Sort by download count and assign ranks # Sort by download count and assign ranks
@ -386,8 +387,8 @@ def _analyze_download_trends(
async def _enhance_with_real_stats( async def _enhance_with_real_stats(
curated_packages: List, period: str, limit: int curated_packages: list, period: str, limit: int
) -> List[Dict[str, Any]]: ) -> list[dict[str, Any]]:
"""Try to enhance curated packages with real PyPI download statistics. """Try to enhance curated packages with real PyPI download statistics.
Args: Args:
@ -403,7 +404,7 @@ async def _enhance_with_real_stats(
try: try:
async with PyPIStatsClient() as stats_client: async with PyPIStatsClient() as stats_client:
# Try to get real stats for top packages # Try to get real stats for top packages
for pkg_info in curated_packages[:limit * 2]: # Try more than needed for pkg_info in curated_packages[: limit * 2]: # Try more than needed
try: try:
stats = await stats_client.get_recent_downloads( stats = await stats_client.get_recent_downloads(
pkg_info.name, period, use_cache=True pkg_info.name, period, use_cache=True
@ -414,30 +415,36 @@ async def _enhance_with_real_stats(
if real_download_count > 0: if real_download_count > 0:
# Use real stats # Use real stats
enhanced_packages.append({ enhanced_packages.append(
"package": pkg_info.name, {
"downloads": real_download_count, "package": pkg_info.name,
"period": period, "downloads": real_download_count,
"data_source": "pypistats.org", "period": period,
"category": pkg_info.category, "data_source": "pypistats.org",
"description": pkg_info.description, "category": pkg_info.category,
"estimated": False, "description": pkg_info.description,
}) "estimated": False,
logger.debug(f"Got real stats for {pkg_info.name}: {real_download_count}") }
)
logger.debug(
f"Got real stats for {pkg_info.name}: {real_download_count}"
)
else: else:
# Fall back to estimated downloads # Fall back to estimated downloads
estimated_downloads = estimate_downloads_for_period( estimated_downloads = estimate_downloads_for_period(
pkg_info.estimated_monthly_downloads, period pkg_info.estimated_monthly_downloads, period
) )
enhanced_packages.append({ enhanced_packages.append(
"package": pkg_info.name, {
"downloads": estimated_downloads, "package": pkg_info.name,
"period": period, "downloads": estimated_downloads,
"data_source": "estimated", "period": period,
"category": pkg_info.category, "data_source": "estimated",
"description": pkg_info.description, "category": pkg_info.category,
"estimated": True, "description": pkg_info.description,
}) "estimated": True,
}
)
except Exception as e: except Exception as e:
logger.debug(f"Failed to get real stats for {pkg_info.name}: {e}") logger.debug(f"Failed to get real stats for {pkg_info.name}: {e}")
@ -445,15 +452,17 @@ async def _enhance_with_real_stats(
estimated_downloads = estimate_downloads_for_period( estimated_downloads = estimate_downloads_for_period(
pkg_info.estimated_monthly_downloads, period pkg_info.estimated_monthly_downloads, period
) )
enhanced_packages.append({ enhanced_packages.append(
"package": pkg_info.name, {
"downloads": estimated_downloads, "package": pkg_info.name,
"period": period, "downloads": estimated_downloads,
"data_source": "estimated", "period": period,
"category": pkg_info.category, "data_source": "estimated",
"description": pkg_info.description, "category": pkg_info.category,
"estimated": True, "description": pkg_info.description,
}) "estimated": True,
}
)
# Stop if we have enough packages # Stop if we have enough packages
if len(enhanced_packages) >= limit: if len(enhanced_packages) >= limit:
@ -466,22 +475,24 @@ async def _enhance_with_real_stats(
estimated_downloads = estimate_downloads_for_period( estimated_downloads = estimate_downloads_for_period(
pkg_info.estimated_monthly_downloads, period pkg_info.estimated_monthly_downloads, period
) )
enhanced_packages.append({ enhanced_packages.append(
"package": pkg_info.name, {
"downloads": estimated_downloads, "package": pkg_info.name,
"period": period, "downloads": estimated_downloads,
"data_source": "estimated", "period": period,
"category": pkg_info.category, "data_source": "estimated",
"description": pkg_info.description, "category": pkg_info.category,
"estimated": True, "description": pkg_info.description,
}) "estimated": True,
}
)
return enhanced_packages return enhanced_packages
async def _enhance_with_github_stats( async def _enhance_with_github_stats(
packages: List[Dict[str, Any]], limit: int packages: list[dict[str, Any]], limit: int
) -> List[Dict[str, Any]]: ) -> list[dict[str, Any]]:
"""Try to enhance packages with GitHub repository statistics. """Try to enhance packages with GitHub repository statistics.
Args: Args:
@ -507,7 +518,9 @@ async def _enhance_with_github_stats(
if repo_paths: if repo_paths:
# Fetch GitHub stats for all repositories concurrently # Fetch GitHub stats for all repositories concurrently
logger.debug(f"Fetching GitHub stats for {len(repo_paths)} repositories") logger.debug(
f"Fetching GitHub stats for {len(repo_paths)} repositories"
)
repo_stats = await github_client.get_multiple_repo_stats( repo_stats = await github_client.get_multiple_repo_stats(
repo_paths, use_cache=True, max_concurrent=3 repo_paths, use_cache=True, max_concurrent=3
) )
@ -527,10 +540,14 @@ async def _enhance_with_github_stats(
# Adjust download estimates based on GitHub popularity # Adjust download estimates based on GitHub popularity
if pkg.get("estimated", False): if pkg.get("estimated", False):
popularity_boost = _calculate_popularity_boost(stats) popularity_boost = _calculate_popularity_boost(stats)
pkg["downloads"] = int(pkg["downloads"] * popularity_boost) pkg["downloads"] = int(
pkg["downloads"] * popularity_boost
)
pkg["github_enhanced"] = True pkg["github_enhanced"] = True
logger.info(f"Enhanced {len([p for p in packages if 'github_stars' in p])} packages with GitHub data") logger.info(
f"Enhanced {len([p for p in packages if 'github_stars' in p])} packages with GitHub data"
)
except Exception as e: except Exception as e:
logger.debug(f"GitHub enhancement failed: {e}") logger.debug(f"GitHub enhancement failed: {e}")
@ -540,7 +557,7 @@ async def _enhance_with_github_stats(
return packages return packages
def _calculate_popularity_boost(github_stats: Dict[str, Any]) -> float: def _calculate_popularity_boost(github_stats: dict[str, Any]) -> float:
"""Calculate a popularity boost multiplier based on GitHub metrics. """Calculate a popularity boost multiplier based on GitHub metrics.
Args: Args:

View File

@ -68,8 +68,12 @@ def format_package_info(package_data: dict[str, Any]) -> dict[str, Any]:
formatted["total_versions"] = len(releases) formatted["total_versions"] = len(releases)
# Sort versions semantically and get the most recent 10 # Sort versions semantically and get the most recent 10
if releases: if releases:
sorted_versions = sort_versions_semantically(list(releases.keys()), reverse=True) sorted_versions = sort_versions_semantically(
formatted["available_versions"] = sorted_versions[:10] # Most recent 10 versions list(releases.keys()), reverse=True
)
formatted["available_versions"] = sorted_versions[
:10
] # Most recent 10 versions
else: else:
formatted["available_versions"] = [] formatted["available_versions"] = []
@ -164,9 +168,26 @@ def format_dependency_info(package_data: dict[str, Any]) -> dict[str, Any]:
# Define development-related extra names (same as in DependencyParser) # Define development-related extra names (same as in DependencyParser)
dev_extra_names = { dev_extra_names = {
'dev', 'development', 'test', 'testing', 'tests', 'lint', 'linting', "dev",
'doc', 'docs', 'documentation', 'build', 'check', 'cover', 'coverage', "development",
'type', 'typing', 'mypy', 'style', 'format', 'quality' "test",
"testing",
"tests",
"lint",
"linting",
"doc",
"docs",
"documentation",
"build",
"check",
"cover",
"coverage",
"type",
"typing",
"mypy",
"style",
"format",
"quality",
} }
for extra_name, deps in optional_deps.items(): for extra_name, deps in optional_deps.items():
@ -264,7 +285,7 @@ async def query_package_dependencies(
version: str | None = None, version: str | None = None,
include_transitive: bool = False, include_transitive: bool = False,
max_depth: int = 5, max_depth: int = 5,
python_version: str | None = None python_version: str | None = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Query package dependency information from PyPI. """Query package dependency information from PyPI.
@ -293,7 +314,11 @@ async def query_package_dependencies(
logger.info( logger.info(
f"Querying dependencies for package: {package_name}" f"Querying dependencies for package: {package_name}"
+ (f" version {version}" if version else " (latest)") + (f" version {version}" if version else " (latest)")
+ (f" with transitive dependencies (max depth: {max_depth})" if include_transitive else " (direct only)") + (
f" with transitive dependencies (max depth: {max_depth})"
if include_transitive
else " (direct only)"
)
) )
try: try:
@ -306,7 +331,7 @@ async def query_package_dependencies(
python_version=python_version, python_version=python_version,
include_extras=[], include_extras=[],
include_dev=False, include_dev=False,
max_depth=max_depth max_depth=max_depth,
) )
# Format the transitive dependency result to match expected structure # Format the transitive dependency result to match expected structure
@ -315,7 +340,9 @@ async def query_package_dependencies(
# Use direct dependency logic with version support # Use direct dependency logic with version support
async with PyPIClient() as client: async with PyPIClient() as client:
# Pass the version parameter to get_package_info # Pass the version parameter to get_package_info
package_data = await client.get_package_info(package_name, version=version) package_data = await client.get_package_info(
package_name, version=version
)
return format_dependency_info(package_data) return format_dependency_info(package_data)
except PyPIError: except PyPIError:
# Re-raise PyPI-specific errors # Re-raise PyPI-specific errors
@ -353,40 +380,49 @@ def format_transitive_dependency_info(
"include_transitive": True, "include_transitive": True,
"max_depth": summary.get("max_depth", 0), "max_depth": summary.get("max_depth", 0),
"python_version": resolver_result.get("python_version"), "python_version": resolver_result.get("python_version"),
# Direct dependencies (same as before) # Direct dependencies (same as before)
"runtime_dependencies": main_package.get("dependencies", {}).get("runtime", []), "runtime_dependencies": main_package.get("dependencies", {}).get("runtime", []),
"development_dependencies": main_package.get("dependencies", {}).get("development", []), "development_dependencies": main_package.get("dependencies", {}).get(
"development", []
),
"optional_dependencies": main_package.get("dependencies", {}).get("extras", {}), "optional_dependencies": main_package.get("dependencies", {}).get("extras", {}),
# Transitive dependency information # Transitive dependency information
"transitive_dependencies": { "transitive_dependencies": {
"dependency_tree": _build_dependency_tree_structure(dependency_tree, normalized_name), "dependency_tree": _build_dependency_tree_structure(
dependency_tree, normalized_name
),
"all_packages": _extract_all_packages_info(dependency_tree), "all_packages": _extract_all_packages_info(dependency_tree),
"circular_dependencies": _detect_circular_dependencies(dependency_tree), "circular_dependencies": _detect_circular_dependencies(dependency_tree),
"depth_analysis": _analyze_dependency_depths(dependency_tree), "depth_analysis": _analyze_dependency_depths(dependency_tree),
}, },
# Enhanced summary statistics # Enhanced summary statistics
"dependency_summary": { "dependency_summary": {
"direct_runtime_count": len(main_package.get("dependencies", {}).get("runtime", [])), "direct_runtime_count": len(
"direct_dev_count": len(main_package.get("dependencies", {}).get("development", [])), main_package.get("dependencies", {}).get("runtime", [])
"direct_optional_groups": len(main_package.get("dependencies", {}).get("extras", {})), ),
"total_transitive_packages": summary.get("total_packages", 0) - 1, # Exclude main package "direct_dev_count": len(
main_package.get("dependencies", {}).get("development", [])
),
"direct_optional_groups": len(
main_package.get("dependencies", {}).get("extras", {})
),
"total_transitive_packages": summary.get("total_packages", 0)
- 1, # Exclude main package
"total_runtime_dependencies": summary.get("total_runtime_dependencies", 0), "total_runtime_dependencies": summary.get("total_runtime_dependencies", 0),
"total_development_dependencies": summary.get("total_development_dependencies", 0), "total_development_dependencies": summary.get(
"total_development_dependencies", 0
),
"total_extra_dependencies": summary.get("total_extra_dependencies", 0), "total_extra_dependencies": summary.get("total_extra_dependencies", 0),
"max_dependency_depth": summary.get("max_depth", 0), "max_dependency_depth": summary.get("max_depth", 0),
"complexity_score": _calculate_complexity_score(summary), "complexity_score": _calculate_complexity_score(summary),
}, },
# Performance and health metrics # Performance and health metrics
"analysis": { "analysis": {
"resolution_stats": summary, "resolution_stats": summary,
"potential_conflicts": _analyze_potential_conflicts(dependency_tree), "potential_conflicts": _analyze_potential_conflicts(dependency_tree),
"maintenance_concerns": _analyze_maintenance_concerns(dependency_tree), "maintenance_concerns": _analyze_maintenance_concerns(dependency_tree),
"performance_impact": _assess_performance_impact(summary), "performance_impact": _assess_performance_impact(summary),
} },
} }
return result return result
@ -416,7 +452,7 @@ def _build_dependency_tree_structure(
"depth": package_info.get("depth", 0), "depth": package_info.get("depth", 0),
"requires_python": package_info.get("requires_python", ""), "requires_python": package_info.get("requires_python", ""),
"dependencies": package_info.get("dependencies", {}), "dependencies": package_info.get("dependencies", {}),
"children": {} "children": {},
} }
# Recursively build children (with visited tracking to prevent infinite loops) # Recursively build children (with visited tracking to prevent infinite loops)
@ -428,13 +464,15 @@ def _build_dependency_tree_structure(
else: else:
tree_node["children"][child_name] = { tree_node["children"][child_name] = {
"circular_reference": True, "circular_reference": True,
"package_name": child_name "package_name": child_name,
} }
return tree_node return tree_node
def _extract_all_packages_info(dependency_tree: dict[str, Any]) -> dict[str, dict[str, Any]]: def _extract_all_packages_info(
dependency_tree: dict[str, Any],
) -> dict[str, dict[str, Any]]:
"""Extract comprehensive information about all packages in the dependency tree.""" """Extract comprehensive information about all packages in the dependency tree."""
all_packages = {} all_packages = {}
@ -446,20 +484,31 @@ def _extract_all_packages_info(dependency_tree: dict[str, Any]) -> dict[str, dic
"requires_python": package_info.get("requires_python", ""), "requires_python": package_info.get("requires_python", ""),
"direct_dependencies": { "direct_dependencies": {
"runtime": package_info.get("dependencies", {}).get("runtime", []), "runtime": package_info.get("dependencies", {}).get("runtime", []),
"development": package_info.get("dependencies", {}).get("development", []), "development": package_info.get("dependencies", {}).get(
"development", []
),
"extras": package_info.get("dependencies", {}).get("extras", {}), "extras": package_info.get("dependencies", {}).get("extras", {}),
}, },
"dependency_count": { "dependency_count": {
"runtime": len(package_info.get("dependencies", {}).get("runtime", [])), "runtime": len(package_info.get("dependencies", {}).get("runtime", [])),
"development": len(package_info.get("dependencies", {}).get("development", [])), "development": len(
"total_extras": sum(len(deps) for deps in package_info.get("dependencies", {}).get("extras", {}).values()), package_info.get("dependencies", {}).get("development", [])
} ),
"total_extras": sum(
len(deps)
for deps in package_info.get("dependencies", {})
.get("extras", {})
.values()
),
},
} }
return all_packages return all_packages
def _detect_circular_dependencies(dependency_tree: dict[str, Any]) -> list[dict[str, Any]]: def _detect_circular_dependencies(
dependency_tree: dict[str, Any],
) -> list[dict[str, Any]]:
"""Detect circular dependencies in the dependency tree.""" """Detect circular dependencies in the dependency tree."""
circular_deps = [] circular_deps = []
@ -468,11 +517,13 @@ def _detect_circular_dependencies(dependency_tree: dict[str, Any]) -> list[dict[
# Found a circular dependency # Found a circular dependency
cycle_start = path.index(package_name) cycle_start = path.index(package_name)
cycle = path[cycle_start:] + [package_name] cycle = path[cycle_start:] + [package_name]
circular_deps.append({ circular_deps.append(
"cycle": cycle, {
"length": len(cycle) - 1, "cycle": cycle,
"packages_involved": list(set(cycle)) "length": len(cycle) - 1,
}) "packages_involved": list(set(cycle)),
}
)
return return
if package_name in visited or package_name not in dependency_tree: if package_name in visited or package_name not in dependency_tree:
@ -524,12 +575,19 @@ def _analyze_dependency_depths(dependency_tree: dict[str, Any]) -> dict[str, Any
"max_depth": max_depth, "max_depth": max_depth,
"depth_distribution": depth_counts, "depth_distribution": depth_counts,
"packages_by_depth": depth_packages, "packages_by_depth": depth_packages,
"average_depth": sum(d * c for d, c in depth_counts.items()) / sum(depth_counts.values()) if depth_counts else 0, "average_depth": sum(d * c for d, c in depth_counts.items())
/ sum(depth_counts.values())
if depth_counts
else 0,
"depth_analysis": { "depth_analysis": {
"shallow_deps": depth_counts.get(1, 0), # Direct dependencies "shallow_deps": depth_counts.get(1, 0), # Direct dependencies
"deep_deps": sum(count for depth, count in depth_counts.items() if depth > 2), "deep_deps": sum(
"leaf_packages": [pkg for pkg, info in dependency_tree.items() if not info.get("children")] count for depth, count in depth_counts.items() if depth > 2
} ),
"leaf_packages": [
pkg for pkg, info in dependency_tree.items() if not info.get("children")
],
},
} }
@ -558,7 +616,9 @@ def _calculate_complexity_score(summary: dict[str, Any]) -> dict[str, Any]:
recommendation = "High complexity, consider dependency management strategies" recommendation = "High complexity, consider dependency management strategies"
else: else:
complexity_level = "very_high" complexity_level = "very_high"
recommendation = "Very high complexity, significant maintenance overhead expected" recommendation = (
"Very high complexity, significant maintenance overhead expected"
)
return { return {
"score": round(complexity_score, 2), "score": round(complexity_score, 2),
@ -568,11 +628,13 @@ def _calculate_complexity_score(summary: dict[str, Any]) -> dict[str, Any]:
"total_packages": total_packages, "total_packages": total_packages,
"max_depth": max_depth, "max_depth": max_depth,
"total_dependencies": total_deps, "total_dependencies": total_deps,
} },
} }
def _analyze_potential_conflicts(dependency_tree: dict[str, Any]) -> list[dict[str, Any]]: def _analyze_potential_conflicts(
dependency_tree: dict[str, Any],
) -> list[dict[str, Any]]:
"""Analyze potential version conflicts in dependencies.""" """Analyze potential version conflicts in dependencies."""
# This is a simplified analysis - in a real implementation, # This is a simplified analysis - in a real implementation,
# you'd parse version constraints and check for conflicts # you'd parse version constraints and check for conflicts
@ -585,24 +647,30 @@ def _analyze_potential_conflicts(dependency_tree: dict[str, Any]) -> list[dict[s
for dep_str in runtime_deps: for dep_str in runtime_deps:
# Basic parsing of "package>=version" format # Basic parsing of "package>=version" format
if ">=" in dep_str or "==" in dep_str or "<" in dep_str or ">" in dep_str: if ">=" in dep_str or "==" in dep_str or "<" in dep_str or ">" in dep_str:
parts = dep_str.replace(">=", "@").replace("==", "@").replace("<", "@").replace(">", "@") parts = (
dep_str.replace(">=", "@")
.replace("==", "@")
.replace("<", "@")
.replace(">", "@")
)
dep_name = parts.split("@")[0].strip() dep_name = parts.split("@")[0].strip()
if dep_name not in package_versions: if dep_name not in package_versions:
package_versions[dep_name] = [] package_versions[dep_name] = []
package_versions[dep_name].append({ package_versions[dep_name].append(
"constraint": dep_str, {"constraint": dep_str, "required_by": package_name}
"required_by": package_name )
})
# Look for packages with multiple version constraints # Look for packages with multiple version constraints
for dep_name, constraints in package_versions.items(): for dep_name, constraints in package_versions.items():
if len(constraints) > 1: if len(constraints) > 1:
potential_conflicts.append({ potential_conflicts.append(
"package": dep_name, {
"conflicting_constraints": constraints, "package": dep_name,
"severity": "potential" if len(constraints) == 2 else "high" "conflicting_constraints": constraints,
}) "severity": "potential" if len(constraints) == 2 else "high",
}
)
return potential_conflicts return potential_conflicts
@ -611,20 +679,20 @@ def _analyze_maintenance_concerns(dependency_tree: dict[str, Any]) -> dict[str,
"""Analyze maintenance concerns in the dependency tree.""" """Analyze maintenance concerns in the dependency tree."""
total_packages = len(dependency_tree) total_packages = len(dependency_tree)
packages_without_version = sum( packages_without_version = sum(
1 for info in dependency_tree.values() 1
for info in dependency_tree.values()
if info.get("version") in ["unknown", "", None] if info.get("version") in ["unknown", "", None]
) )
packages_without_python_req = sum( packages_without_python_req = sum(
1 for info in dependency_tree.values() 1 for info in dependency_tree.values() if not info.get("requires_python")
if not info.get("requires_python")
) )
# Calculate dependency concentration (packages with many dependencies) # Calculate dependency concentration (packages with many dependencies)
high_dep_packages = [ high_dep_packages = [
{ {
"name": name, "name": name,
"dependency_count": len(info.get("dependencies", {}).get("runtime", [])) "dependency_count": len(info.get("dependencies", {}).get("runtime", [])),
} }
for name, info in dependency_tree.items() for name, info in dependency_tree.items()
if len(info.get("dependencies", {}).get("runtime", [])) > 5 if len(info.get("dependencies", {}).get("runtime", [])) > 5
@ -637,11 +705,18 @@ def _analyze_maintenance_concerns(dependency_tree: dict[str, Any]) -> dict[str,
"high_dependency_packages": high_dep_packages, "high_dependency_packages": high_dep_packages,
"maintenance_risk_score": { "maintenance_risk_score": {
"score": round( "score": round(
(packages_without_version / total_packages * 100) + (packages_without_version / total_packages * 100)
(len(high_dep_packages) / total_packages * 50), 2 + (len(high_dep_packages) / total_packages * 50),
) if total_packages > 0 else 0, 2,
"level": "low" if total_packages < 10 else "moderate" if total_packages < 30 else "high" )
} if total_packages > 0
else 0,
"level": "low"
if total_packages < 10
else "moderate"
if total_packages < 30
else "high",
},
} }
@ -659,9 +734,13 @@ def _assess_performance_impact(summary: dict[str, Any]) -> dict[str, Any]:
# Performance recommendations # Performance recommendations
recommendations = [] recommendations = []
if total_packages > 50: if total_packages > 50:
recommendations.append("Consider using virtual environments to isolate dependencies") recommendations.append(
"Consider using virtual environments to isolate dependencies"
)
if max_depth > 5: if max_depth > 5:
recommendations.append("Deep dependency chains may slow resolution and installation") recommendations.append(
"Deep dependency chains may slow resolution and installation"
)
if total_packages > 100: if total_packages > 100:
recommendations.append("Consider dependency analysis tools for large projects") recommendations.append("Consider dependency analysis tools for large projects")
@ -669,14 +748,16 @@ def _assess_performance_impact(summary: dict[str, Any]) -> dict[str, Any]:
"estimated_install_time_seconds": estimated_install_time, "estimated_install_time_seconds": estimated_install_time,
"estimated_memory_footprint_mb": estimated_memory_mb, "estimated_memory_footprint_mb": estimated_memory_mb,
"performance_level": ( "performance_level": (
"good" if total_packages < 20 "good"
else "moderate" if total_packages < 50 if total_packages < 20
else "moderate"
if total_packages < 50
else "concerning" else "concerning"
), ),
"recommendations": recommendations, "recommendations": recommendations,
"metrics": { "metrics": {
"package_count_impact": "low" if total_packages < 20 else "high", "package_count_impact": "low" if total_packages < 20 else "high",
"depth_impact": "low" if max_depth < 4 else "high", "depth_impact": "low" if max_depth < 4 else "high",
"resolution_complexity": "simple" if total_packages < 10 else "complex" "resolution_complexity": "simple" if total_packages < 10 else "complex",
} },
} }

View File

@ -34,7 +34,7 @@ httpx = "^0.28.0"
packaging = "^24.0" packaging = "^24.0"
pydantic = "^2.0.0" pydantic = "^2.0.0"
pydantic-settings = "^2.0.0" pydantic-settings = "^2.0.0"
click = "^8.1.0" click = "8.1.7"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
pytest = "^8.0.0" pytest = "^8.0.0"

View File

@ -2,8 +2,9 @@
"""Quick test to verify fallback mechanism works.""" """Quick test to verify fallback mechanism works."""
import asyncio import asyncio
import sys
import os import os
import sys
sys.path.insert(0, os.path.abspath(".")) sys.path.insert(0, os.path.abspath("."))
from pypi_query_mcp.tools.download_stats import get_package_download_stats from pypi_query_mcp.tools.download_stats import get_package_download_stats
@ -16,12 +17,12 @@ async def quick_test():
try: try:
stats = await get_package_download_stats("requests", period="month") stats = await get_package_download_stats("requests", period="month")
print(f"✅ Success!") print("✅ Success!")
print(f"Package: {stats.get('package')}") print(f"Package: {stats.get('package')}")
print(f"Data Source: {stats.get('data_source')}") print(f"Data Source: {stats.get('data_source')}")
print(f"Reliability: {stats.get('reliability')}") print(f"Reliability: {stats.get('reliability')}")
if stats.get('warning'): if stats.get("warning"):
print(f"⚠️ Warning: {stats['warning']}") print(f"⚠️ Warning: {stats['warning']}")
downloads = stats.get("downloads", {}) downloads = stats.get("downloads", {})

View File

@ -1,12 +1,13 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Simple test for the transitive dependency formatting functions.""" """Simple test for the transitive dependency formatting functions."""
import sys
import os import os
import sys
# Add the current directory to Python path # Add the current directory to Python path
sys.path.insert(0, os.path.dirname(__file__)) sys.path.insert(0, os.path.dirname(__file__))
def test_formatting_functions(): def test_formatting_functions():
"""Test the formatting functions directly.""" """Test the formatting functions directly."""
print("Testing transitive dependency formatting functions...") print("Testing transitive dependency formatting functions...")
@ -21,9 +22,13 @@ def test_formatting_functions():
"version": "2.31.0", "version": "2.31.0",
"requires_python": ">=3.7", "requires_python": ">=3.7",
"dependencies": { "dependencies": {
"runtime": ["urllib3>=1.21.1", "certifi>=2017.4.17", "charset-normalizer>=2.0"], "runtime": [
"urllib3>=1.21.1",
"certifi>=2017.4.17",
"charset-normalizer>=2.0",
],
"development": [], "development": [],
"extras": {} "extras": {},
}, },
"depth": 0, "depth": 0,
"children": { "children": {
@ -34,10 +39,10 @@ def test_formatting_functions():
"dependencies": { "dependencies": {
"runtime": [], "runtime": [],
"development": [], "development": [],
"extras": {} "extras": {},
}, },
"depth": 1, "depth": 1,
"children": {} "children": {},
}, },
"certifi": { "certifi": {
"name": "certifi", "name": "certifi",
@ -46,37 +51,29 @@ def test_formatting_functions():
"dependencies": { "dependencies": {
"runtime": [], "runtime": [],
"development": [], "development": [],
"extras": {} "extras": {},
}, },
"depth": 1, "depth": 1,
"children": {} "children": {},
} },
} },
}, },
"urllib3": { "urllib3": {
"name": "urllib3", "name": "urllib3",
"version": "2.0.4", "version": "2.0.4",
"requires_python": ">=3.7", "requires_python": ">=3.7",
"dependencies": { "dependencies": {"runtime": [], "development": [], "extras": {}},
"runtime": [],
"development": [],
"extras": {}
},
"depth": 1, "depth": 1,
"children": {} "children": {},
}, },
"certifi": { "certifi": {
"name": "certifi", "name": "certifi",
"version": "2023.7.22", "version": "2023.7.22",
"requires_python": ">=3.6", "requires_python": ">=3.6",
"dependencies": { "dependencies": {"runtime": [], "development": [], "extras": {}},
"runtime": [],
"development": [],
"extras": {}
},
"depth": 1, "depth": 1,
"children": {} "children": {},
} },
}, },
"summary": { "summary": {
"total_packages": 3, "total_packages": 3,
@ -84,19 +81,19 @@ def test_formatting_functions():
"total_development_dependencies": 0, "total_development_dependencies": 0,
"total_extra_dependencies": 0, "total_extra_dependencies": 0,
"max_depth": 1, "max_depth": 1,
"package_list": ["requests", "urllib3", "certifi"] "package_list": ["requests", "urllib3", "certifi"],
} },
} }
# Import the formatting function # Import the formatting function
try: try:
from pypi_query_mcp.tools.package_query import ( from pypi_query_mcp.tools.package_query import (
format_transitive_dependency_info,
_build_dependency_tree_structure,
_extract_all_packages_info,
_detect_circular_dependencies,
_analyze_dependency_depths, _analyze_dependency_depths,
_calculate_complexity_score _build_dependency_tree_structure,
_calculate_complexity_score,
_detect_circular_dependencies,
_extract_all_packages_info,
format_transitive_dependency_info,
) )
# Test format_transitive_dependency_info # Test format_transitive_dependency_info
@ -110,19 +107,25 @@ def test_formatting_functions():
print(f" Max depth: {result.get('max_depth')}") print(f" Max depth: {result.get('max_depth')}")
# Test transitive dependencies section # Test transitive dependencies section
transitive = result.get('transitive_dependencies', {}) transitive = result.get("transitive_dependencies", {})
print(f" All packages count: {len(transitive.get('all_packages', {}))}") print(f" All packages count: {len(transitive.get('all_packages', {}))}")
print(f" Circular dependencies: {len(transitive.get('circular_dependencies', []))}") print(
f" Circular dependencies: {len(transitive.get('circular_dependencies', []))}"
)
# Test dependency summary # Test dependency summary
summary = result.get('dependency_summary', {}) summary = result.get("dependency_summary", {})
print(f" Direct runtime count: {summary.get('direct_runtime_count')}") print(f" Direct runtime count: {summary.get('direct_runtime_count')}")
print(f" Total transitive packages: {summary.get('total_transitive_packages')}") print(
f" Total transitive packages: {summary.get('total_transitive_packages')}"
)
print(f" Complexity level: {summary.get('complexity_score', {}).get('level')}") print(f" Complexity level: {summary.get('complexity_score', {}).get('level')}")
# Test analysis section # Test analysis section
analysis = result.get('analysis', {}) analysis = result.get("analysis", {})
print(f" Performance level: {analysis.get('performance_impact', {}).get('performance_level')}") print(
f" Performance level: {analysis.get('performance_impact', {}).get('performance_level')}"
)
print("✓ All formatting functions working correctly") print("✓ All formatting functions working correctly")
return True return True
@ -133,6 +136,7 @@ def test_formatting_functions():
except Exception as e: except Exception as e:
print(f"✗ Error testing formatting functions: {e}") print(f"✗ Error testing formatting functions: {e}")
import traceback import traceback
traceback.print_exc() traceback.print_exc()
return False return False
@ -146,27 +150,22 @@ def test_helper_functions():
"name": "pkg-a", "name": "pkg-a",
"version": "1.0.0", "version": "1.0.0",
"depth": 0, "depth": 0,
"children": {"pkg-b": {}, "pkg-c": {}} "children": {"pkg-b": {}, "pkg-c": {}},
},
"pkg-b": {
"name": "pkg-b",
"version": "2.0.0",
"depth": 1,
"children": {}
}, },
"pkg-b": {"name": "pkg-b", "version": "2.0.0", "depth": 1, "children": {}},
"pkg-c": { "pkg-c": {
"name": "pkg-c", "name": "pkg-c",
"version": "3.0.0", "version": "3.0.0",
"depth": 1, "depth": 1,
"children": {"pkg-b": {}} # Creates potential circular reference "children": {"pkg-b": {}}, # Creates potential circular reference
} },
} }
try: try:
from pypi_query_mcp.tools.package_query import ( from pypi_query_mcp.tools.package_query import (
_extract_all_packages_info,
_analyze_dependency_depths, _analyze_dependency_depths,
_calculate_complexity_score _calculate_complexity_score,
_extract_all_packages_info,
) )
# Test _extract_all_packages_info # Test _extract_all_packages_info
@ -178,9 +177,15 @@ def test_helper_functions():
print(f"✓ Depth analysis - max depth: {depth_analysis.get('max_depth')}") print(f"✓ Depth analysis - max depth: {depth_analysis.get('max_depth')}")
# Test _calculate_complexity_score # Test _calculate_complexity_score
sample_summary = {"total_packages": 3, "max_depth": 1, "total_runtime_dependencies": 2} sample_summary = {
"total_packages": 3,
"max_depth": 1,
"total_runtime_dependencies": 2,
}
complexity = _calculate_complexity_score(sample_summary) complexity = _calculate_complexity_score(sample_summary)
print(f"✓ Complexity score: {complexity.get('score')} ({complexity.get('level')})") print(
f"✓ Complexity score: {complexity.get('score')} ({complexity.get('level')})"
)
return True return True

View File

@ -3,22 +3,24 @@
import asyncio import asyncio
import logging import logging
import sys
import os import os
import sys
# Add the project root to the Python path # Add the project root to the Python path
sys.path.insert(0, os.path.dirname(__file__)) sys.path.insert(0, os.path.dirname(__file__))
# Set up logging # Set up logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
async def test_pypi_client(): async def test_pypi_client():
"""Test the PyPIClient with version-specific queries.""" """Test the PyPIClient with version-specific queries."""
# Import only the core modules we need # Import only the core modules we need
from pypi_query_mcp.core.pypi_client import PyPIClient
from pypi_query_mcp.core.exceptions import PackageNotFoundError from pypi_query_mcp.core.exceptions import PackageNotFoundError
from pypi_query_mcp.core.pypi_client import PyPIClient
async with PyPIClient() as client: async with PyPIClient() as client:
# Test 1: Django 4.2.0 (specific version) # Test 1: Django 4.2.0 (specific version)
@ -27,7 +29,9 @@ async def test_pypi_client():
data = await client.get_package_info("django", version="4.2.0") data = await client.get_package_info("django", version="4.2.0")
actual_version = data.get("info", {}).get("version", "") actual_version = data.get("info", {}).get("version", "")
if actual_version in ["4.2", "4.2.0"]: # PyPI may normalize version numbers if actual_version in ["4.2", "4.2.0"]: # PyPI may normalize version numbers
logger.info(f"✅ Django 4.2.0 test passed (got version: {actual_version})") logger.info(
f"✅ Django 4.2.0 test passed (got version: {actual_version})"
)
else: else:
logger.error(f"❌ Expected version 4.2.0, got {actual_version}") logger.error(f"❌ Expected version 4.2.0, got {actual_version}")
return False return False
@ -95,7 +99,10 @@ async def test_pypi_client():
async def test_dependency_formatting(): async def test_dependency_formatting():
"""Test the dependency formatting functions.""" """Test the dependency formatting functions."""
from pypi_query_mcp.tools.package_query import format_dependency_info, validate_version_format from pypi_query_mcp.tools.package_query import (
format_dependency_info,
validate_version_format,
)
# Test version validation # Test version validation
logger.info("Testing version validation...") logger.info("Testing version validation...")
@ -116,7 +123,9 @@ async def test_dependency_formatting():
if result == expected: if result == expected:
logger.info(f"✅ Version validation for '{version}': {result}") logger.info(f"✅ Version validation for '{version}': {result}")
else: else:
logger.error(f"❌ Version validation for '{version}': expected {expected}, got {result}") logger.error(
f"❌ Version validation for '{version}': expected {expected}, got {result}"
)
return False return False
# Test dependency formatting with mock data # Test dependency formatting with mock data
@ -130,13 +139,18 @@ async def test_dependency_formatting():
"requests>=2.25.0", "requests>=2.25.0",
"click>=8.0.0", "click>=8.0.0",
"pytest>=6.0.0; extra=='test'", "pytest>=6.0.0; extra=='test'",
"black>=21.0.0; extra=='dev'" "black>=21.0.0; extra=='dev'",
] ],
} }
} }
result = format_dependency_info(mock_data) result = format_dependency_info(mock_data)
expected_fields = ["package_name", "version", "runtime_dependencies", "dependency_summary"] expected_fields = [
"package_name",
"version",
"runtime_dependencies",
"dependency_summary",
]
for field in expected_fields: for field in expected_fields:
if field not in result: if field not in result:
logger.error(f"❌ Missing field '{field}' in dependency formatting result") logger.error(f"❌ Missing field '{field}' in dependency formatting result")
@ -145,7 +159,9 @@ async def test_dependency_formatting():
if len(result["runtime_dependencies"]) >= 2: # Should have requests and click if len(result["runtime_dependencies"]) >= 2: # Should have requests and click
logger.info("✅ Dependency formatting test passed") logger.info("✅ Dependency formatting test passed")
else: else:
logger.error(f"❌ Expected at least 2 runtime dependencies, got {len(result['runtime_dependencies'])}") logger.error(
f"❌ Expected at least 2 runtime dependencies, got {len(result['runtime_dependencies'])}"
)
return False return False
return True return True
@ -171,13 +187,19 @@ async def test_comparison():
# They should be different (unless 4.2.0 happens to be latest, which is unlikely) # They should be different (unless 4.2.0 happens to be latest, which is unlikely)
if specific_version in ["4.2", "4.2.0"] and latest_version != specific_version: if specific_version in ["4.2", "4.2.0"] and latest_version != specific_version:
logger.info("✅ Version-specific query returns different version than latest") logger.info(
"✅ Version-specific query returns different version than latest"
)
return True return True
elif specific_version in ["4.2", "4.2.0"]: elif specific_version in ["4.2", "4.2.0"]:
logger.info("⚠️ Specific version matches latest (this is fine, but less conclusive)") logger.info(
"⚠️ Specific version matches latest (this is fine, but less conclusive)"
)
return True return True
else: else:
logger.error(f"❌ Specific version query failed: expected 4.2.0, got {specific_version}") logger.error(
f"❌ Specific version query failed: expected 4.2.0, got {specific_version}"
)
return False return False

View File

@ -3,22 +3,26 @@
import asyncio import asyncio
import logging import logging
import sys
import os import os
import re import re
import httpx import sys
from urllib.parse import quote from urllib.parse import quote
import httpx
# Add the project root to the Python path # Add the project root to the Python path
sys.path.insert(0, os.path.dirname(__file__)) sys.path.insert(0, os.path.dirname(__file__))
# Set up logging # Set up logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class SimplePackageNotFoundError(Exception): class SimplePackageNotFoundError(Exception):
"""Simple exception for package not found.""" """Simple exception for package not found."""
pass pass
@ -53,7 +57,9 @@ class SimplePyPIClient:
if response.status_code == 404: if response.status_code == 404:
if version: if version:
raise SimplePackageNotFoundError(f"Version {version} not found for package {package_name}") raise SimplePackageNotFoundError(
f"Version {version} not found for package {package_name}"
)
else: else:
raise SimplePackageNotFoundError(f"Package {package_name} not found") raise SimplePackageNotFoundError(f"Package {package_name} not found")
@ -82,7 +88,9 @@ async def test_version_parameter_fix():
actual_version = data.get("info", {}).get("version", "") actual_version = data.get("info", {}).get("version", "")
if actual_version in ["4.2", "4.2.0"]: if actual_version in ["4.2", "4.2.0"]:
logger.info(f"✅ Django 4.2.0 test passed (got version: {actual_version})") logger.info(
f"✅ Django 4.2.0 test passed (got version: {actual_version})"
)
# Check dependencies # Check dependencies
deps = data.get("info", {}).get("requires_dist", []) deps = data.get("info", {}).get("requires_dist", [])
@ -110,7 +118,9 @@ async def test_version_parameter_fix():
if latest_version not in ["4.2", "4.2.0"]: if latest_version not in ["4.2", "4.2.0"]:
logger.info("✅ Confirmed: latest version is different from 4.2.0") logger.info("✅ Confirmed: latest version is different from 4.2.0")
else: else:
logger.info(" Latest version happens to be 4.2.0 (unlikely but possible)") logger.info(
" Latest version happens to be 4.2.0 (unlikely but possible)"
)
except Exception as e: except Exception as e:
logger.error(f"❌ Django latest test failed: {e}") logger.error(f"❌ Django latest test failed: {e}")
@ -171,7 +181,9 @@ async def test_version_parameter_fix():
actual_version = data.get("info", {}).get("version", "") actual_version = data.get("info", {}).get("version", "")
logger.info(f"✅ Django 5.0a1 test passed - got version: {actual_version}") logger.info(f"✅ Django 5.0a1 test passed - got version: {actual_version}")
except SimplePackageNotFoundError: except SimplePackageNotFoundError:
logger.info(" Django 5.0a1 not found (this is expected for some pre-release versions)") logger.info(
" Django 5.0a1 not found (this is expected for some pre-release versions)"
)
except Exception as e: except Exception as e:
logger.error(f"❌ Django 5.0a1 test failed: {e}") logger.error(f"❌ Django 5.0a1 test failed: {e}")
return False return False
@ -202,7 +214,9 @@ def test_version_validation():
if result == expected: if result == expected:
logger.info(f"✅ Version validation for '{version}': {result}") logger.info(f"✅ Version validation for '{version}': {result}")
else: else:
logger.error(f"❌ Version validation for '{version}': expected {expected}, got {result}") logger.error(
f"❌ Version validation for '{version}': expected {expected}, got {result}"
)
all_passed = False all_passed = False
return all_passed return all_passed
@ -222,7 +236,9 @@ async def compare_dependencies():
deps_latest = data_latest.get("info", {}).get("requires_dist", []) deps_latest = data_latest.get("info", {}).get("requires_dist", [])
logger.info(f"Django 4.2.0 dependencies: {len(deps_420) if deps_420 else 0}") logger.info(f"Django 4.2.0 dependencies: {len(deps_420) if deps_420 else 0}")
logger.info(f"Django latest dependencies: {len(deps_latest) if deps_latest else 0}") logger.info(
f"Django latest dependencies: {len(deps_latest) if deps_latest else 0}"
)
# Show some dependencies for comparison # Show some dependencies for comparison
if deps_420: if deps_420:
@ -262,7 +278,9 @@ async def main():
success = False success = False
if success: if success:
logger.info("🎉 All tests passed! The version parameter fix is working correctly.") logger.info(
"🎉 All tests passed! The version parameter fix is working correctly."
)
logger.info("") logger.info("")
logger.info("Summary of what was fixed:") logger.info("Summary of what was fixed:")
logger.info("- PyPIClient now supports version-specific queries") logger.info("- PyPIClient now supports version-specific queries")

View File

@ -4,8 +4,8 @@ Test script for the enhanced PyPI download statistics with fallback mechanisms.
""" """
import asyncio import asyncio
import sys
import os import os
import sys
# Add the package to Python path # Add the package to Python path
sys.path.insert(0, os.path.abspath(".")) sys.path.insert(0, os.path.abspath("."))
@ -38,21 +38,23 @@ async def test_download_stats():
print(f"Data Source: {stats.get('data_source')}") print(f"Data Source: {stats.get('data_source')}")
print(f"Reliability: {stats.get('reliability', 'unknown')}") print(f"Reliability: {stats.get('reliability', 'unknown')}")
if stats.get('warning'): if stats.get("warning"):
print(f"⚠️ Warning: {stats['warning']}") print(f"⚠️ Warning: {stats['warning']}")
downloads = stats.get("downloads", {}) downloads = stats.get("downloads", {})
print(f"Downloads - Day: {downloads.get('last_day', 0):,}, " + print(
f"Week: {downloads.get('last_week', 0):,}, " + f"Downloads - Day: {downloads.get('last_day', 0):,}, "
f"Month: {downloads.get('last_month', 0):,}") + f"Week: {downloads.get('last_week', 0):,}, "
+ f"Month: {downloads.get('last_month', 0):,}"
)
if stats.get('data_quality_note'): if stats.get("data_quality_note"):
print(f"Note: {stats['data_quality_note']}") print(f"Note: {stats['data_quality_note']}")
except Exception as e: except Exception as e:
print(f"❌ Error: {e}") print(f"❌ Error: {e}")
print(f"\n📈 Testing download trends for 'requests':") print("\n📈 Testing download trends for 'requests':")
print("-" * 50) print("-" * 50)
try: try:
@ -62,7 +64,7 @@ async def test_download_stats():
print(f"Data Source: {trends.get('data_source')}") print(f"Data Source: {trends.get('data_source')}")
print(f"Reliability: {trends.get('reliability', 'unknown')}") print(f"Reliability: {trends.get('reliability', 'unknown')}")
if trends.get('warning'): if trends.get("warning"):
print(f"⚠️ Warning: {trends['warning']}") print(f"⚠️ Warning: {trends['warning']}")
trend_analysis = trends.get("trend_analysis", {}) trend_analysis = trends.get("trend_analysis", {})
@ -70,13 +72,13 @@ async def test_download_stats():
print(f"Total Downloads: {trend_analysis.get('total_downloads', 0):,}") print(f"Total Downloads: {trend_analysis.get('total_downloads', 0):,}")
print(f"Trend Direction: {trend_analysis.get('trend_direction', 'unknown')}") print(f"Trend Direction: {trend_analysis.get('trend_direction', 'unknown')}")
if trends.get('data_quality_note'): if trends.get("data_quality_note"):
print(f"Note: {trends['data_quality_note']}") print(f"Note: {trends['data_quality_note']}")
except Exception as e: except Exception as e:
print(f"❌ Error: {e}") print(f"❌ Error: {e}")
print(f"\n🏆 Testing top packages:") print("\n🏆 Testing top packages:")
print("-" * 50) print("-" * 50)
try: try:
@ -84,9 +86,11 @@ async def test_download_stats():
print(f"Data Source: {top_packages.get('data_source')}") print(f"Data Source: {top_packages.get('data_source')}")
print(f"Reliability: {top_packages.get('reliability', 'unknown')}") print(f"Reliability: {top_packages.get('reliability', 'unknown')}")
print(f"Success Rate: {top_packages.get('data_collection_success_rate', 'unknown')}") print(
f"Success Rate: {top_packages.get('data_collection_success_rate', 'unknown')}"
)
if top_packages.get('warning'): if top_packages.get("warning"):
print(f"⚠️ Warning: {top_packages['warning']}") print(f"⚠️ Warning: {top_packages['warning']}")
packages_list = top_packages.get("top_packages", []) packages_list = top_packages.get("top_packages", [])

View File

@ -2,44 +2,56 @@
"""Test script for the improved get_top_packages_by_downloads function.""" """Test script for the improved get_top_packages_by_downloads function."""
import asyncio import asyncio
from pypi_query_mcp.tools.download_stats import get_top_packages_by_downloads from pypi_query_mcp.tools.download_stats import get_top_packages_by_downloads
async def test_improved(): async def test_improved():
try: try:
result = await get_top_packages_by_downloads('month', 10) result = await get_top_packages_by_downloads("month", 10)
print('✅ Success! Result keys:', list(result.keys())) print("✅ Success! Result keys:", list(result.keys()))
print(f'Number of packages returned: {len(result.get("top_packages", []))}') print(f"Number of packages returned: {len(result.get('top_packages', []))}")
print(f'Data source: {result.get("data_source")}') print(f"Data source: {result.get('data_source')}")
print(f'Methodology: {result.get("methodology")}') print(f"Methodology: {result.get('methodology')}")
print('\nTop 5 packages:') print("\nTop 5 packages:")
for i, pkg in enumerate(result.get('top_packages', [])[:5]): for i, pkg in enumerate(result.get("top_packages", [])[:5]):
downloads = pkg.get('downloads', 0) downloads = pkg.get("downloads", 0)
stars = pkg.get('github_stars', 'N/A') stars = pkg.get("github_stars", "N/A")
estimated = '(estimated)' if pkg.get('estimated', False) else '(real)' estimated = "(estimated)" if pkg.get("estimated", False) else "(real)"
github_enhanced = ' 🌟' if pkg.get('github_enhanced', False) else '' github_enhanced = " 🌟" if pkg.get("github_enhanced", False) else ""
print(f'{i+1}. {pkg.get("package", "N/A")} - {downloads:,} downloads {estimated}{github_enhanced}') print(
if stars != 'N/A': f"{i + 1}. {pkg.get('package', 'N/A')} - {downloads:,} downloads {estimated}{github_enhanced}"
print(f' GitHub: {stars:,} stars, {pkg.get("category", "N/A")} category') )
if stars != "N/A":
print(
f" GitHub: {stars:,} stars, {pkg.get('category', 'N/A')} category"
)
# Test different periods # Test different periods
print('\n--- Testing different periods ---') print("\n--- Testing different periods ---")
for period in ['day', 'week', 'month']: for period in ["day", "week", "month"]:
result = await get_top_packages_by_downloads(period, 3) result = await get_top_packages_by_downloads(period, 3)
top_3 = result.get('top_packages', []) top_3 = result.get("top_packages", [])
print(f'{period}: {len(top_3)} packages, avg downloads: {sum(p.get("downloads", 0) for p in top_3) // max(len(top_3), 1):,}') print(
f"{period}: {len(top_3)} packages, avg downloads: {sum(p.get('downloads', 0) for p in top_3) // max(len(top_3), 1):,}"
)
print('\n--- Testing different limits ---') print("\n--- Testing different limits ---")
for limit in [5, 20, 50]: for limit in [5, 20, 50]:
result = await get_top_packages_by_downloads('month', limit) result = await get_top_packages_by_downloads("month", limit)
packages = result.get('top_packages', []) packages = result.get("top_packages", [])
real_count = len([p for p in packages if not p.get('estimated', False)]) real_count = len([p for p in packages if not p.get("estimated", False)])
print(f'Limit {limit}: {len(packages)} packages returned, {real_count} with real stats') print(
f"Limit {limit}: {len(packages)} packages returned, {real_count} with real stats"
)
except Exception as e: except Exception as e:
print(f'❌ Error: {e}') print(f"❌ Error: {e}")
import traceback import traceback
traceback.print_exc() traceback.print_exc()
if __name__ == '__main__':
if __name__ == "__main__":
asyncio.run(test_improved()) asyncio.run(test_improved())

View File

@ -3,14 +3,16 @@
import asyncio import asyncio
import logging import logging
import sys
import os import os
import sys
# Add the project root to the Python path # Add the project root to the Python path
sys.path.insert(0, os.path.dirname(__file__)) sys.path.insert(0, os.path.dirname(__file__))
# Set up logging # Set up logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -18,13 +20,16 @@ async def test_our_implementation():
"""Test our actual implementation directly.""" """Test our actual implementation directly."""
# Import just the core pieces we need # Import just the core pieces we need
from pypi_query_mcp.core.exceptions import (
InvalidPackageNameError,
PackageNotFoundError,
)
from pypi_query_mcp.core.pypi_client import PyPIClient from pypi_query_mcp.core.pypi_client import PyPIClient
from pypi_query_mcp.tools.package_query import ( from pypi_query_mcp.tools.package_query import (
format_dependency_info,
query_package_dependencies, query_package_dependencies,
validate_version_format, validate_version_format,
format_dependency_info
) )
from pypi_query_mcp.core.exceptions import PackageNotFoundError, InvalidPackageNameError
logger.info("Testing our actual implementation...") logger.info("Testing our actual implementation...")
@ -50,7 +55,9 @@ async def test_our_implementation():
# Verify they're different (unless 4.2 is latest, which is unlikely) # Verify they're different (unless 4.2 is latest, which is unlikely)
if latest_version not in ["4.2", "4.2.0"]: if latest_version not in ["4.2", "4.2.0"]:
logger.info("✅ Confirmed version-specific queries work differently than latest") logger.info(
"✅ Confirmed version-specific queries work differently than latest"
)
# Test 3: Dependency formatting # Test 3: Dependency formatting
logger.info("Testing dependency formatting...") logger.info("Testing dependency formatting...")
@ -63,7 +70,9 @@ async def test_our_implementation():
assert "runtime_dependencies" in formatted assert "runtime_dependencies" in formatted
assert "dependency_summary" in formatted assert "dependency_summary" in formatted
assert formatted["version"] in ["4.2", "4.2.0"] assert formatted["version"] in ["4.2", "4.2.0"]
logger.info(f"✅ Dependency formatting works: {len(formatted['runtime_dependencies'])} runtime deps") logger.info(
f"✅ Dependency formatting works: {len(formatted['runtime_dependencies'])} runtime deps"
)
# Test 4: Full query_package_dependencies function # Test 4: Full query_package_dependencies function
logger.info("Testing query_package_dependencies function...") logger.info("Testing query_package_dependencies function...")
@ -72,16 +81,22 @@ async def test_our_implementation():
result = await query_package_dependencies("django", "4.2.0") result = await query_package_dependencies("django", "4.2.0")
assert result["package_name"].lower() == "django" assert result["package_name"].lower() == "django"
assert result["version"] in ["4.2", "4.2.0"] assert result["version"] in ["4.2", "4.2.0"]
logger.info(f"✅ Django 4.2.0 dependencies: {len(result['runtime_dependencies'])} runtime deps") logger.info(
f"✅ Django 4.2.0 dependencies: {len(result['runtime_dependencies'])} runtime deps"
)
# Test with Django latest # Test with Django latest
result_latest = await query_package_dependencies("django", None) result_latest = await query_package_dependencies("django", None)
assert result_latest["package_name"].lower() == "django" assert result_latest["package_name"].lower() == "django"
logger.info(f"✅ Django latest dependencies: {len(result_latest['runtime_dependencies'])} runtime deps") logger.info(
f"✅ Django latest dependencies: {len(result_latest['runtime_dependencies'])} runtime deps"
)
# Verify they might be different # Verify they might be different
if result["version"] != result_latest["version"]: if result["version"] != result_latest["version"]:
logger.info("✅ Confirmed: version-specific query returns different version than latest") logger.info(
"✅ Confirmed: version-specific query returns different version than latest"
)
# Test 5: Error cases # Test 5: Error cases
logger.info("Testing error cases...") logger.info("Testing error cases...")
@ -114,7 +129,9 @@ async def test_our_implementation():
result = await query_package_dependencies(package, version) result = await query_package_dependencies(package, version)
assert result["package_name"].lower() == package.lower() assert result["package_name"].lower() == package.lower()
assert result["version"] == version assert result["version"] == version
logger.info(f"{package} {version}: {len(result['runtime_dependencies'])} runtime deps") logger.info(
f"{package} {version}: {len(result['runtime_dependencies'])} runtime deps"
)
except Exception as e: except Exception as e:
logger.warning(f"⚠️ {package} {version} failed (may not exist): {e}") logger.warning(f"⚠️ {package} {version} failed (may not exist): {e}")
@ -134,6 +151,7 @@ async def main():
except Exception as e: except Exception as e:
logger.error(f"❌ Test failed with exception: {e}") logger.error(f"❌ Test failed with exception: {e}")
import traceback import traceback
traceback.print_exc() traceback.print_exc()
return 1 return 1

View File

@ -3,6 +3,7 @@
import asyncio import asyncio
import logging import logging
from pypi_query_mcp.tools.package_query import query_package_versions from pypi_query_mcp.tools.package_query import query_package_versions
# Configure logging # Configure logging
@ -18,9 +19,9 @@ async def test_real_package_versions():
# Test packages known to have complex version histories # Test packages known to have complex version histories
test_packages = [ test_packages = [
"django", # Known for alpha, beta, rc versions "django", # Known for alpha, beta, rc versions
"numpy", # Long history with various formats "numpy", # Long history with various formats
"requests" # Simple but well-known package "requests", # Simple but well-known package
] ]
for package_name in test_packages: for package_name in test_packages:
@ -38,12 +39,12 @@ async def test_real_package_versions():
string_sorted = sorted(all_versions[:20], reverse=True) string_sorted = sorted(all_versions[:20], reverse=True)
print(f" String-sorted (first 10): {string_sorted[:10]}") print(f" String-sorted (first 10): {string_sorted[:10]}")
print(f" Semantic vs String comparison:") print(" Semantic vs String comparison:")
for i in range(min(5, len(recent_versions))): for i in range(min(5, len(recent_versions))):
semantic = recent_versions[i] if i < len(recent_versions) else "N/A" semantic = recent_versions[i] if i < len(recent_versions) else "N/A"
string_sort = string_sorted[i] if i < len(string_sorted) else "N/A" string_sort = string_sorted[i] if i < len(string_sorted) else "N/A"
match = "" if semantic == string_sort else "" match = "" if semantic == string_sort else ""
print(f" {i+1}: {semantic} vs {string_sort} {match}") print(f" {i + 1}: {semantic} vs {string_sort} {match}")
except Exception as e: except Exception as e:
print(f" Error querying {package_name}: {e}") print(f" Error querying {package_name}: {e}")
@ -89,7 +90,7 @@ async def test_specific_version_ordering():
async def main(): async def main():
"""Main test function.""" """Main test function."""
print("Real Package Version Sorting Test") print("Real Package Version Sorting Test")
print("="*60) print("=" * 60)
# Test with real packages # Test with real packages
await test_real_package_versions() await test_real_package_versions()

View File

@ -3,21 +3,23 @@
import asyncio import asyncio
import logging import logging
import sys
import os import os
import sys
# Add the project root to the Python path # Add the project root to the Python path
sys.path.insert(0, os.path.dirname(__file__)) sys.path.insert(0, os.path.dirname(__file__))
# Set up logging # Set up logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
async def test_pypi_client(): async def test_pypi_client():
"""Test the PyPIClient with version-specific queries.""" """Test the PyPIClient with version-specific queries."""
from pypi_query_mcp.core.pypi_client import PyPIClient
from pypi_query_mcp.core.exceptions import PackageNotFoundError from pypi_query_mcp.core.exceptions import PackageNotFoundError
from pypi_query_mcp.core.pypi_client import PyPIClient
async with PyPIClient() as client: async with PyPIClient() as client:
# Test 1: Django 4.2.0 (specific version) # Test 1: Django 4.2.0 (specific version)
@ -96,8 +98,13 @@ async def test_pypi_client():
async def test_dependency_query(): async def test_dependency_query():
"""Test the query_package_dependencies function.""" """Test the query_package_dependencies function."""
from pypi_query_mcp.tools.package_query import query_package_dependencies, validate_version_format from pypi_query_mcp.core.exceptions import (
from pypi_query_mcp.core.exceptions import InvalidPackageNameError, PackageNotFoundError InvalidPackageNameError,
)
from pypi_query_mcp.tools.package_query import (
query_package_dependencies,
validate_version_format,
)
# Test version validation # Test version validation
logger.info("Testing version validation...") logger.info("Testing version validation...")
@ -118,7 +125,9 @@ async def test_dependency_query():
if result == expected: if result == expected:
logger.info(f"✅ Version validation for '{version}': {result}") logger.info(f"✅ Version validation for '{version}': {result}")
else: else:
logger.error(f"❌ Version validation for '{version}': expected {expected}, got {result}") logger.error(
f"❌ Version validation for '{version}': expected {expected}, got {result}"
)
return False return False
# Test dependency queries # Test dependency queries
@ -127,10 +136,17 @@ async def test_dependency_query():
# Test Django 4.2.0 dependencies # Test Django 4.2.0 dependencies
try: try:
result = await query_package_dependencies("django", "4.2.0") result = await query_package_dependencies("django", "4.2.0")
if result["package_name"].lower() == "django" and result["version"] in ["4.2", "4.2.0"]: if result["package_name"].lower() == "django" and result["version"] in [
logger.info(f"✅ Django 4.2.0 dependencies query passed - {len(result['runtime_dependencies'])} runtime deps") "4.2",
"4.2.0",
]:
logger.info(
f"✅ Django 4.2.0 dependencies query passed - {len(result['runtime_dependencies'])} runtime deps"
)
else: else:
logger.error(f"❌ Django dependencies query failed - got {result['package_name']} v{result['version']}") logger.error(
f"❌ Django dependencies query failed - got {result['package_name']} v{result['version']}"
)
return False return False
except Exception as e: except Exception as e:
logger.error(f"❌ Django dependencies query failed: {e}") logger.error(f"❌ Django dependencies query failed: {e}")

View File

@ -3,6 +3,7 @@
from pypi_query_mcp.core.version_utils import sort_versions_semantically from pypi_query_mcp.core.version_utils import sort_versions_semantically
def test_specific_case(): def test_specific_case():
"""Test the exact case mentioned in the task requirements.""" """Test the exact case mentioned in the task requirements."""
print("=" * 60) print("=" * 60)
@ -24,7 +25,7 @@ def test_specific_case():
print() print()
print("Analysis:") print("Analysis:")
print(f" Problem: '5.2rc1' was appearing before '5.2.5' in string sorting") print(" Problem: '5.2rc1' was appearing before '5.2.5' in string sorting")
print(f" String sorting result: {old_sorted[0]} comes first") print(f" String sorting result: {old_sorted[0]} comes first")
print(f" Semantic sorting result: {new_sorted[0]} comes first") print(f" Semantic sorting result: {new_sorted[0]} comes first")
print() print()
@ -39,8 +40,14 @@ def test_specific_case():
# Test a more comprehensive example # Test a more comprehensive example
comprehensive_test = [ comprehensive_test = [
"5.2.5", "5.2rc1", "5.2.0", "5.2a1", "5.2b1", "5.2.5",
"5.1.0", "5.3.0", "5.2.1" "5.2rc1",
"5.2.0",
"5.2a1",
"5.2b1",
"5.1.0",
"5.3.0",
"5.2.1",
] ]
old_comprehensive = sorted(comprehensive_test, reverse=True) old_comprehensive = sorted(comprehensive_test, reverse=True)

View File

@ -3,7 +3,7 @@
import asyncio import asyncio
import sys import sys
import json
from pypi_query_mcp.tools.package_query import query_package_dependencies from pypi_query_mcp.tools.package_query import query_package_dependencies
@ -12,10 +12,14 @@ async def test_direct_dependencies():
print("Testing direct dependencies for 'requests'...") print("Testing direct dependencies for 'requests'...")
try: try:
result = await query_package_dependencies("requests", include_transitive=False) result = await query_package_dependencies("requests", include_transitive=False)
print(f"✓ Direct dependencies found: {len(result.get('runtime_dependencies', []))}") print(
f"✓ Direct dependencies found: {len(result.get('runtime_dependencies', []))}"
)
print(f" Package: {result.get('package_name')}") print(f" Package: {result.get('package_name')}")
print(f" Version: {result.get('version')}") print(f" Version: {result.get('version')}")
print(f" Runtime deps: {result.get('runtime_dependencies', [])[:3]}...") # Show first 3 print(
f" Runtime deps: {result.get('runtime_dependencies', [])[:3]}..."
) # Show first 3
return True return True
except Exception as e: except Exception as e:
print(f"✗ Error testing direct dependencies: {e}") print(f"✗ Error testing direct dependencies: {e}")
@ -27,44 +31,44 @@ async def test_transitive_dependencies():
print("\nTesting transitive dependencies for 'requests'...") print("\nTesting transitive dependencies for 'requests'...")
try: try:
result = await query_package_dependencies( result = await query_package_dependencies(
"requests", "requests", include_transitive=True, max_depth=3, python_version="3.10"
include_transitive=True,
max_depth=3,
python_version="3.10"
) )
print(f"✓ Transitive analysis completed") print("✓ Transitive analysis completed")
print(f" Include transitive: {result.get('include_transitive')}") print(f" Include transitive: {result.get('include_transitive')}")
print(f" Package: {result.get('package_name')}") print(f" Package: {result.get('package_name')}")
print(f" Version: {result.get('version')}") print(f" Version: {result.get('version')}")
# Check transitive dependency structure # Check transitive dependency structure
transitive = result.get('transitive_dependencies', {}) transitive = result.get("transitive_dependencies", {})
all_packages = transitive.get('all_packages', {}) all_packages = transitive.get("all_packages", {})
print(f" Total packages in tree: {len(all_packages)}") print(f" Total packages in tree: {len(all_packages)}")
# Check summary # Check summary
summary = result.get('dependency_summary', {}) summary = result.get("dependency_summary", {})
print(f" Direct runtime deps: {summary.get('direct_runtime_count', 0)}") print(f" Direct runtime deps: {summary.get('direct_runtime_count', 0)}")
print(f" Total transitive packages: {summary.get('total_transitive_packages', 0)}") print(
f" Total transitive packages: {summary.get('total_transitive_packages', 0)}"
)
print(f" Max depth: {summary.get('max_dependency_depth', 0)}") print(f" Max depth: {summary.get('max_dependency_depth', 0)}")
# Check analysis # Check analysis
analysis = result.get('analysis', {}) analysis = result.get("analysis", {})
performance = analysis.get('performance_impact', {}) performance = analysis.get("performance_impact", {})
print(f" Performance level: {performance.get('performance_level', 'unknown')}") print(f" Performance level: {performance.get('performance_level', 'unknown')}")
complexity = summary.get('complexity_score', {}) complexity = summary.get("complexity_score", {})
print(f" Complexity level: {complexity.get('level', 'unknown')}") print(f" Complexity level: {complexity.get('level', 'unknown')}")
# Check circular dependencies # Check circular dependencies
circular = transitive.get('circular_dependencies', []) circular = transitive.get("circular_dependencies", [])
print(f" Circular dependencies found: {len(circular)}") print(f" Circular dependencies found: {len(circular)}")
return True return True
except Exception as e: except Exception as e:
print(f"✗ Error testing transitive dependencies: {e}") print(f"✗ Error testing transitive dependencies: {e}")
import traceback import traceback
traceback.print_exc() traceback.print_exc()
return False return False
@ -74,17 +78,15 @@ async def test_small_package():
print("\nTesting transitive dependencies for 'six' (smaller package)...") print("\nTesting transitive dependencies for 'six' (smaller package)...")
try: try:
result = await query_package_dependencies( result = await query_package_dependencies(
"six", "six", include_transitive=True, max_depth=2
include_transitive=True,
max_depth=2
) )
transitive = result.get('transitive_dependencies', {}) transitive = result.get("transitive_dependencies", {})
all_packages = transitive.get('all_packages', {}) all_packages = transitive.get("all_packages", {})
print(f"✓ Analysis completed for 'six'") print("✓ Analysis completed for 'six'")
print(f" Total packages: {len(all_packages)}") print(f" Total packages: {len(all_packages)}")
summary = result.get('dependency_summary', {}) summary = result.get("dependency_summary", {})
print(f" Direct runtime deps: {summary.get('direct_runtime_count', 0)}") print(f" Direct runtime deps: {summary.get('direct_runtime_count', 0)}")
return True return True

View File

@ -3,21 +3,24 @@
import asyncio import asyncio
import logging import logging
import sys
import os import os
import sys
# Add the project root to the Python path # Add the project root to the Python path
sys.path.insert(0, os.path.dirname(__file__)) sys.path.insert(0, os.path.dirname(__file__))
from pypi_query_mcp.tools.package_query import query_package_dependencies from pypi_query_mcp.tools.package_query import query_package_dependencies
from pypi_query_mcp.core.exceptions import PackageNotFoundError, InvalidPackageNameError
# Set up logging # Set up logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
async def test_package_version(package_name: str, version: str = None, expect_error: bool = False): async def test_package_version(
package_name: str, version: str = None, expect_error: bool = False
):
"""Test a specific package and version combination.""" """Test a specific package and version combination."""
version_str = f" version {version}" if version else " (latest)" version_str = f" version {version}" if version else " (latest)"
logger.info(f"Testing {package_name}{version_str}") logger.info(f"Testing {package_name}{version_str}")
@ -26,34 +29,53 @@ async def test_package_version(package_name: str, version: str = None, expect_er
result = await query_package_dependencies(package_name, version) result = await query_package_dependencies(package_name, version)
if expect_error: if expect_error:
logger.error(f"Expected error for {package_name}{version_str}, but got result") logger.error(
f"Expected error for {package_name}{version_str}, but got result"
)
return False return False
# Verify the result contains expected fields # Verify the result contains expected fields
required_fields = ["package_name", "version", "runtime_dependencies", "dependency_summary"] required_fields = [
"package_name",
"version",
"runtime_dependencies",
"dependency_summary",
]
for field in required_fields: for field in required_fields:
if field not in result: if field not in result:
logger.error(f"Missing field '{field}' in result for {package_name}{version_str}") logger.error(
f"Missing field '{field}' in result for {package_name}{version_str}"
)
return False return False
# Check if we got the correct version # Check if we got the correct version
actual_version = result.get("version", "") actual_version = result.get("version", "")
if version and actual_version != version: if version and actual_version != version:
logger.error(f"Expected version {version}, got {actual_version} for {package_name}") logger.error(
f"Expected version {version}, got {actual_version} for {package_name}"
)
return False return False
logger.info(f"✅ Success: {package_name}{version_str} - Got version {actual_version}") logger.info(
f"✅ Success: {package_name}{version_str} - Got version {actual_version}"
)
logger.info(f" Runtime dependencies: {len(result['runtime_dependencies'])}") logger.info(f" Runtime dependencies: {len(result['runtime_dependencies'])}")
logger.info(f" Total dependencies: {result['dependency_summary']['runtime_count']}") logger.info(
f" Total dependencies: {result['dependency_summary']['runtime_count']}"
)
return True return True
except Exception as e: except Exception as e:
if expect_error: if expect_error:
logger.info(f"✅ Expected error for {package_name}{version_str}: {type(e).__name__}: {e}") logger.info(
f"✅ Expected error for {package_name}{version_str}: {type(e).__name__}: {e}"
)
return True return True
else: else:
logger.error(f"❌ Unexpected error for {package_name}{version_str}: {type(e).__name__}: {e}") logger.error(
f"❌ Unexpected error for {package_name}{version_str}: {type(e).__name__}: {e}"
)
return False return False
@ -66,16 +88,13 @@ async def main():
("django", "4.2.0", False), ("django", "4.2.0", False),
("fastapi", "0.100.0", False), ("fastapi", "0.100.0", False),
("numpy", "1.20.0", False), ("numpy", "1.20.0", False),
# Test latest versions (no version specified) # Test latest versions (no version specified)
("requests", None, False), ("requests", None, False),
("click", None, False), ("click", None, False),
# Test edge cases - should fail # Test edge cases - should fail
("django", "999.999.999", True), # Non-existent version ("django", "999.999.999", True), # Non-existent version
("nonexistent-package-12345", None, True), # Non-existent package ("nonexistent-package-12345", None, True), # Non-existent package
("django", "invalid.version.format!", True), # Invalid version format ("django", "invalid.version.format!", True), # Invalid version format
# Test pre-release versions # Test pre-release versions
("django", "5.0a1", False), # Pre-release (may or may not exist) ("django", "5.0a1", False), # Pre-release (may or may not exist)
] ]

View File

@ -3,6 +3,7 @@
import asyncio import asyncio
import logging import logging
from pypi_query_mcp.core.version_utils import sort_versions_semantically from pypi_query_mcp.core.version_utils import sort_versions_semantically
from pypi_query_mcp.tools.package_query import query_package_versions from pypi_query_mcp.tools.package_query import query_package_versions
@ -20,41 +21,61 @@ def test_semantic_version_sorting():
# Test case 1: Basic pre-release ordering # Test case 1: Basic pre-release ordering
test1_versions = ["5.2rc1", "5.2.5", "5.2.0", "5.2a1", "5.2b1"] test1_versions = ["5.2rc1", "5.2.5", "5.2.0", "5.2a1", "5.2b1"]
sorted1 = sort_versions_semantically(test1_versions) sorted1 = sort_versions_semantically(test1_versions)
print(f"Test 1 - Pre-release ordering:") print("Test 1 - Pre-release ordering:")
print(f" Input: {test1_versions}") print(f" Input: {test1_versions}")
print(f" Output: {sorted1}") print(f" Output: {sorted1}")
print(f" Expected: ['5.2.5', '5.2.0', '5.2rc1', '5.2b1', '5.2a1']") print(" Expected: ['5.2.5', '5.2.0', '5.2rc1', '5.2b1', '5.2a1']")
print() print()
# Test case 2: Complex Django-like versions # Test case 2: Complex Django-like versions
test2_versions = [ test2_versions = [
"4.2.0", "4.2a1", "4.2b1", "4.2rc1", "4.1.0", "4.1.7", "4.2.0",
"4.0.0", "3.2.18", "4.2.1", "4.2.2" "4.2a1",
"4.2b1",
"4.2rc1",
"4.1.0",
"4.1.7",
"4.0.0",
"3.2.18",
"4.2.1",
"4.2.2",
] ]
sorted2 = sort_versions_semantically(test2_versions) sorted2 = sort_versions_semantically(test2_versions)
print(f"Test 2 - Django-like versions:") print("Test 2 - Django-like versions:")
print(f" Input: {test2_versions}") print(f" Input: {test2_versions}")
print(f" Output: {sorted2}") print(f" Output: {sorted2}")
print() print()
# Test case 3: TensorFlow-like versions with dev builds # Test case 3: TensorFlow-like versions with dev builds
test3_versions = [ test3_versions = [
"2.13.0", "2.13.0rc1", "2.13.0rc0", "2.12.0", "2.12.1", "2.13.0",
"2.14.0dev20230517", "2.13.0rc2" # This might not parse correctly "2.13.0rc1",
"2.13.0rc0",
"2.12.0",
"2.12.1",
"2.14.0dev20230517",
"2.13.0rc2", # This might not parse correctly
] ]
sorted3 = sort_versions_semantically(test3_versions) sorted3 = sort_versions_semantically(test3_versions)
print(f"Test 3 - TensorFlow-like versions:") print("Test 3 - TensorFlow-like versions:")
print(f" Input: {test3_versions}") print(f" Input: {test3_versions}")
print(f" Output: {sorted3}") print(f" Output: {sorted3}")
print() print()
# Test case 4: Edge cases and malformed versions # Test case 4: Edge cases and malformed versions
test4_versions = [ test4_versions = [
"1.0.0", "1.0.0.post1", "1.0.0.dev0", "1.0.0a1", "1.0.0b1", "1.0.0",
"1.0.0rc1", "1.0.1", "invalid-version", "1.0" "1.0.0.post1",
"1.0.0.dev0",
"1.0.0a1",
"1.0.0b1",
"1.0.0rc1",
"1.0.1",
"invalid-version",
"1.0",
] ]
sorted4 = sort_versions_semantically(test4_versions) sorted4 = sort_versions_semantically(test4_versions)
print(f"Test 4 - Edge cases and malformed versions:") print("Test 4 - Edge cases and malformed versions:")
print(f" Input: {test4_versions}") print(f" Input: {test4_versions}")
print(f" Output: {sorted4}") print(f" Output: {sorted4}")
print() print()
@ -64,7 +85,7 @@ def test_semantic_version_sorting():
test5_single = ["1.0.0"] test5_single = ["1.0.0"]
sorted5_empty = sort_versions_semantically(test5_empty) sorted5_empty = sort_versions_semantically(test5_empty)
sorted5_single = sort_versions_semantically(test5_single) sorted5_single = sort_versions_semantically(test5_single)
print(f"Test 5 - Edge cases:") print("Test 5 - Edge cases:")
print(f" Empty list: {sorted5_empty}") print(f" Empty list: {sorted5_empty}")
print(f" Single item: {sorted5_single}") print(f" Single item: {sorted5_single}")
print() print()
@ -78,10 +99,10 @@ async def test_real_package_versions():
# Test packages known to have complex version histories # Test packages known to have complex version histories
test_packages = [ test_packages = [
"django", # Known for alpha, beta, rc versions "django", # Known for alpha, beta, rc versions
"tensorflow", # Complex versioning with dev builds "tensorflow", # Complex versioning with dev builds
"numpy", # Long history with various formats "numpy", # Long history with various formats
"requests" # Simple but well-known package "requests", # Simple but well-known package
] ]
for package_name in test_packages: for package_name in test_packages:
@ -115,7 +136,7 @@ def validate_sorting_correctness():
print("Task requirement validation:") print("Task requirement validation:")
print(f" Input: {task_example}") print(f" Input: {task_example}")
print(f" Output: {sorted_task}") print(f" Output: {sorted_task}")
print(f" Requirement: '5.2rc1' should come after '5.2.5'") print(" Requirement: '5.2rc1' should come after '5.2.5'")
if sorted_task == ["5.2.5", "5.2rc1"]: if sorted_task == ["5.2.5", "5.2rc1"]:
print(" ✅ PASS: Requirement met!") print(" ✅ PASS: Requirement met!")
@ -131,7 +152,7 @@ def validate_sorting_correctness():
print("Pre-release ordering validation:") print("Pre-release ordering validation:")
print(f" Input: {pre_release_test}") print(f" Input: {pre_release_test}")
print(f" Output: {sorted_pre}") print(f" Output: {sorted_pre}")
print(f" Expected order: stable > rc > beta > alpha") print(" Expected order: stable > rc > beta > alpha")
expected_order = ["1.0.0", "1.0.0rc1", "1.0.0b1", "1.0.0a1"] expected_order = ["1.0.0", "1.0.0rc1", "1.0.0b1", "1.0.0a1"]
if sorted_pre == expected_order: if sorted_pre == expected_order:
@ -145,7 +166,7 @@ def validate_sorting_correctness():
async def main(): async def main():
"""Main test function.""" """Main test function."""
print("Semantic Version Sorting Test Suite") print("Semantic Version Sorting Test Suite")
print("="*60) print("=" * 60)
# Run unit tests # Run unit tests
test_semantic_version_sorting() test_semantic_version_sorting()

View File

@ -2,7 +2,8 @@
"""Standalone test script to verify semantic version sorting functionality.""" """Standalone test script to verify semantic version sorting functionality."""
import logging import logging
from packaging.version import Version, InvalidVersion
from packaging.version import InvalidVersion, Version
# Configure logging # Configure logging
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
@ -77,41 +78,61 @@ def test_semantic_version_sorting():
# Test case 1: Basic pre-release ordering # Test case 1: Basic pre-release ordering
test1_versions = ["5.2rc1", "5.2.5", "5.2.0", "5.2a1", "5.2b1"] test1_versions = ["5.2rc1", "5.2.5", "5.2.0", "5.2a1", "5.2b1"]
sorted1 = sort_versions_semantically(test1_versions) sorted1 = sort_versions_semantically(test1_versions)
print(f"Test 1 - Pre-release ordering:") print("Test 1 - Pre-release ordering:")
print(f" Input: {test1_versions}") print(f" Input: {test1_versions}")
print(f" Output: {sorted1}") print(f" Output: {sorted1}")
print(f" Expected: ['5.2.5', '5.2.0', '5.2rc1', '5.2b1', '5.2a1']") print(" Expected: ['5.2.5', '5.2.0', '5.2rc1', '5.2b1', '5.2a1']")
print() print()
# Test case 2: Complex Django-like versions # Test case 2: Complex Django-like versions
test2_versions = [ test2_versions = [
"4.2.0", "4.2a1", "4.2b1", "4.2rc1", "4.1.0", "4.1.7", "4.2.0",
"4.0.0", "3.2.18", "4.2.1", "4.2.2" "4.2a1",
"4.2b1",
"4.2rc1",
"4.1.0",
"4.1.7",
"4.0.0",
"3.2.18",
"4.2.1",
"4.2.2",
] ]
sorted2 = sort_versions_semantically(test2_versions) sorted2 = sort_versions_semantically(test2_versions)
print(f"Test 2 - Django-like versions:") print("Test 2 - Django-like versions:")
print(f" Input: {test2_versions}") print(f" Input: {test2_versions}")
print(f" Output: {sorted2}") print(f" Output: {sorted2}")
print() print()
# Test case 3: TensorFlow-like versions with dev builds # Test case 3: TensorFlow-like versions with dev builds
test3_versions = [ test3_versions = [
"2.13.0", "2.13.0rc1", "2.13.0rc0", "2.12.0", "2.12.1", "2.13.0",
"2.14.0dev20230517", "2.13.0rc2" # This might not parse correctly "2.13.0rc1",
"2.13.0rc0",
"2.12.0",
"2.12.1",
"2.14.0dev20230517",
"2.13.0rc2", # This might not parse correctly
] ]
sorted3 = sort_versions_semantically(test3_versions) sorted3 = sort_versions_semantically(test3_versions)
print(f"Test 3 - TensorFlow-like versions:") print("Test 3 - TensorFlow-like versions:")
print(f" Input: {test3_versions}") print(f" Input: {test3_versions}")
print(f" Output: {sorted3}") print(f" Output: {sorted3}")
print() print()
# Test case 4: Edge cases and malformed versions # Test case 4: Edge cases and malformed versions
test4_versions = [ test4_versions = [
"1.0.0", "1.0.0.post1", "1.0.0.dev0", "1.0.0a1", "1.0.0b1", "1.0.0",
"1.0.0rc1", "1.0.1", "invalid-version", "1.0" "1.0.0.post1",
"1.0.0.dev0",
"1.0.0a1",
"1.0.0b1",
"1.0.0rc1",
"1.0.1",
"invalid-version",
"1.0",
] ]
sorted4 = sort_versions_semantically(test4_versions) sorted4 = sort_versions_semantically(test4_versions)
print(f"Test 4 - Edge cases and malformed versions:") print("Test 4 - Edge cases and malformed versions:")
print(f" Input: {test4_versions}") print(f" Input: {test4_versions}")
print(f" Output: {sorted4}") print(f" Output: {sorted4}")
print() print()
@ -121,7 +142,7 @@ def test_semantic_version_sorting():
test5_single = ["1.0.0"] test5_single = ["1.0.0"]
sorted5_empty = sort_versions_semantically(test5_empty) sorted5_empty = sort_versions_semantically(test5_empty)
sorted5_single = sort_versions_semantically(test5_single) sorted5_single = sort_versions_semantically(test5_single)
print(f"Test 5 - Edge cases:") print("Test 5 - Edge cases:")
print(f" Empty list: {sorted5_empty}") print(f" Empty list: {sorted5_empty}")
print(f" Single item: {sorted5_single}") print(f" Single item: {sorted5_single}")
print() print()
@ -140,7 +161,7 @@ def validate_sorting_correctness():
print("Task requirement validation:") print("Task requirement validation:")
print(f" Input: {task_example}") print(f" Input: {task_example}")
print(f" Output: {sorted_task}") print(f" Output: {sorted_task}")
print(f" Requirement: '5.2rc1' should come after '5.2.5'") print(" Requirement: '5.2rc1' should come after '5.2.5'")
if sorted_task == ["5.2.5", "5.2rc1"]: if sorted_task == ["5.2.5", "5.2rc1"]:
print(" ✅ PASS: Requirement met!") print(" ✅ PASS: Requirement met!")
@ -156,7 +177,7 @@ def validate_sorting_correctness():
print("Pre-release ordering validation:") print("Pre-release ordering validation:")
print(f" Input: {pre_release_test}") print(f" Input: {pre_release_test}")
print(f" Output: {sorted_pre}") print(f" Output: {sorted_pre}")
print(f" Expected order: stable > rc > beta > alpha") print(" Expected order: stable > rc > beta > alpha")
expected_order = ["1.0.0", "1.0.0rc1", "1.0.0b1", "1.0.0a1"] expected_order = ["1.0.0", "1.0.0rc1", "1.0.0b1", "1.0.0a1"]
if sorted_pre == expected_order: if sorted_pre == expected_order:
@ -200,7 +221,7 @@ def test_version_comparison_details():
def main(): def main():
"""Main test function.""" """Main test function."""
print("Semantic Version Sorting Test Suite") print("Semantic Version Sorting Test Suite")
print("="*60) print("=" * 60)
# Run unit tests # Run unit tests
test_semantic_version_sorting() test_semantic_version_sorting()

View File

@ -122,7 +122,13 @@ class TestDependencyResolver:
elif package_name.lower() == "pytest": elif package_name.lower() == "pytest":
return mock_pytest_data return mock_pytest_data
else: else:
return {"info": {"name": package_name, "version": "1.0.0", "requires_dist": []}} return {
"info": {
"name": package_name,
"version": "1.0.0",
"requires_dist": [],
}
}
mock_client.get_package_info.side_effect = mock_get_package_info mock_client.get_package_info.side_effect = mock_get_package_info
@ -200,13 +206,22 @@ class TestDependencyResolver:
elif package_name.lower() == "coverage": elif package_name.lower() == "coverage":
return mock_coverage_data return mock_coverage_data
else: else:
return {"info": {"name": package_name, "version": "1.0.0", "requires_dist": []}} return {
"info": {
"name": package_name,
"version": "1.0.0",
"requires_dist": [],
}
}
mock_client.get_package_info.side_effect = mock_get_package_info mock_client.get_package_info.side_effect = mock_get_package_info
# Test with Python 3.11 - should not include typing-extensions but should include extras # Test with Python 3.11 - should not include typing-extensions but should include extras
result = await resolver.resolve_dependencies( result = await resolver.resolve_dependencies(
"test-package", python_version="3.11", include_extras=["test"], max_depth=2 "test-package",
python_version="3.11",
include_extras=["test"],
max_depth=2,
) )
assert result["include_extras"] == ["test"] assert result["include_extras"] == ["test"]

View File

@ -194,12 +194,16 @@ class TestDownloadStats:
"forks": 5000, "forks": 5000,
"updated_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z",
"language": "Python", "language": "Python",
"topics": ["http", "requests"] "topics": ["http", "requests"],
} }
with ( with (
patch("pypi_query_mcp.tools.download_stats.PyPIStatsClient") as mock_stats_client, patch(
patch("pypi_query_mcp.tools.download_stats.GitHubAPIClient") as mock_github_client "pypi_query_mcp.tools.download_stats.PyPIStatsClient"
) as mock_stats_client,
patch(
"pypi_query_mcp.tools.download_stats.GitHubAPIClient"
) as mock_github_client,
): ):
# Mock PyPI failure # Mock PyPI failure
mock_stats_instance = AsyncMock() mock_stats_instance = AsyncMock()
@ -211,12 +215,17 @@ class TestDownloadStats:
mock_github_instance.get_multiple_repo_stats.return_value = { mock_github_instance.get_multiple_repo_stats.return_value = {
"psf/requests": mock_github_stats "psf/requests": mock_github_stats
} }
mock_github_client.return_value.__aenter__.return_value = mock_github_instance mock_github_client.return_value.__aenter__.return_value = (
mock_github_instance
)
result = await get_top_packages_by_downloads("month", 10) result = await get_top_packages_by_downloads("month", 10)
# Find requests package (should be enhanced with GitHub data) # Find requests package (should be enhanced with GitHub data)
requests_pkg = next((pkg for pkg in result["top_packages"] if pkg["package"] == "requests"), None) requests_pkg = next(
(pkg for pkg in result["top_packages"] if pkg["package"] == "requests"),
None,
)
if requests_pkg: if requests_pkg:
assert "github_stars" in requests_pkg assert "github_stars" in requests_pkg
@ -245,9 +254,13 @@ class TestDownloadStats:
# Check that downloads are scaled appropriately for the period # Check that downloads are scaled appropriately for the period
# Day should have much smaller numbers than month # Day should have much smaller numbers than month
if period == "day": if period == "day":
assert all(pkg["downloads"] < 50_000_000 for pkg in result["top_packages"]) assert all(
pkg["downloads"] < 50_000_000 for pkg in result["top_packages"]
)
elif period == "month": elif period == "month":
assert any(pkg["downloads"] > 100_000_000 for pkg in result["top_packages"]) assert any(
pkg["downloads"] > 100_000_000 for pkg in result["top_packages"]
)
def test_analyze_download_stats(self): def test_analyze_download_stats(self):
"""Test download statistics analysis.""" """Test download statistics analysis."""

View File

@ -1,6 +1,5 @@
"""Tests for semantic version sorting functionality.""" """Tests for semantic version sorting functionality."""
import pytest
from pypi_query_mcp.core.version_utils import sort_versions_semantically from pypi_query_mcp.core.version_utils import sort_versions_semantically