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:
parent
503ea589f1
commit
8b43927493
@ -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())
|
@ -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")
|
||||||
|
@ -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")
|
||||||
|
@ -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
1010
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -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:
|
||||||
|
@ -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:
|
||||||
|
@ -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):
|
||||||
|
@ -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):
|
||||||
|
@ -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 = {
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -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",
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
|
@ -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", {})
|
||||||
|
101
simple_test.py
101
simple_test.py
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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")
|
||||||
|
@ -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", [])
|
||||||
|
@ -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())
|
@ -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
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
@ -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}")
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
]
|
]
|
||||||
|
@ -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()
|
||||||
|
@ -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()
|
||||||
|
@ -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"]
|
||||||
|
@ -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."""
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user