Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ A Model Context Protocol (MCP) server for querying PyPI package information, dep
- 🐍 Python version compatibility checking
- πŸ” **Advanced dependency analysis and recursive resolution**
- πŸ“₯ **Package download with dependency collection**
- πŸ“Š **Download statistics and popularity analysis**
- πŸ† **Top packages ranking and trends**
- 🏒 Private PyPI repository support
- ⚑ Fast async operations with caching
- πŸ› οΈ Easy integration with MCP clients
Expand Down Expand Up @@ -199,6 +201,11 @@ The server provides the following MCP tools:
6. **resolve_dependencies** - Recursively resolve all package dependencies with detailed analysis
7. **download_package** - Download package and all dependencies to local directory

### Download Statistics & Popularity
8. **get_download_statistics** - Get comprehensive download statistics for any package
9. **get_download_trends** - Analyze download trends and time series data (last 180 days)
10. **get_top_downloaded_packages** - Get the most popular packages by download count

## Usage Examples

Once configured in your MCP client (Claude Desktop, Cline, Cursor, Windsurf), you can ask questions like:
Expand All @@ -220,6 +227,13 @@ Once configured in your MCP client (Claude Desktop, Cline, Cursor, Windsurf), yo
- "Download the requests package with all dependencies to ./downloads folder"
- "Collect all packages needed for Django development"

### Download Statistics & Popularity Analysis
- "What are the download statistics for the requests package this month?"
- "Show me the download trends for numpy over the last 180 days"
- "What are the top 10 most downloaded Python packages today?"
- "Compare the popularity of Django vs Flask vs FastAPI"
- "Which web framework has the highest download count this week?"

### Example Conversations

**User**: "Check if Django 4.2 is compatible with Python 3.9"
Expand All @@ -234,6 +248,12 @@ Once configured in your MCP client (Claude Desktop, Cline, Cursor, Windsurf), yo

*[Uses get_package_dependencies tool]*

**User**: "Show me the download statistics for the requests package and tell me which is more popular: requests or urllib3?"

**AI Assistant**: I'll get the download statistics for both packages and compare their popularity.

*[Uses get_download_statistics tool for both packages]*

### Programmatic Usage

```python
Expand All @@ -247,6 +267,18 @@ result = await mcp_client.call_tool("check_package_python_compatibility", {
info = await mcp_client.call_tool("get_package_info", {
"package_name": "requests"
})

# Example: Get download statistics
stats = await mcp_client.call_tool("get_download_statistics", {
"package_name": "numpy",
"period": "month"
})

# Example: Get top downloaded packages
top_packages = await mcp_client.call_tool("get_top_downloaded_packages", {
"period": "week",
"limit": 10
})
```

## Development Status
Expand All @@ -258,9 +290,12 @@ Current implementation status:
- βœ… PyPI API client with caching
- βœ… MCP tools implementation (package info, versions, dependencies)
- βœ… Python version compatibility checking
- βœ… Advanced dependency analysis and recursive resolution
- βœ… Package download with dependency collection
- βœ… **Download statistics and popularity analysis**
- βœ… **Top packages ranking and trends**
- βœ… CI/CD pipeline with multi-platform testing
- ⏳ Private repository support (planned)
- ⏳ Advanced dependency analysis (planned)

## Contributing

Expand Down
226 changes: 226 additions & 0 deletions examples/download_stats_demo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
#!/usr/bin/env python3
"""
Demo script for PyPI package download statistics functionality.

This script demonstrates how to use the new download statistics tools
to analyze PyPI package popularity and trends.
"""

import asyncio
from datetime import datetime

from pypi_query_mcp.tools.download_stats import (
get_package_download_stats,
get_package_download_trends,
get_top_packages_by_downloads,
)


async def demo_package_download_stats():
"""Demonstrate package download statistics retrieval."""
print("=" * 60)
print("PyPI Package Download Statistics Demo")
print("=" * 60)

# Example packages to analyze
packages = ["requests", "numpy", "django", "flask"]

for package_name in packages:
print(f"\nπŸ“Š Download Statistics for '{package_name}':")
print("-" * 50)

try:
# Get download statistics for the last month
stats = await get_package_download_stats(package_name, period="month")

# Display basic info
metadata = stats.get("metadata", {})
downloads = stats.get("downloads", {})
analysis = stats.get("analysis", {})

print(f"Package: {metadata.get('name', package_name)}")
print(f"Version: {metadata.get('version', 'unknown')}")
print(f"Summary: {metadata.get('summary', 'No summary available')[:80]}...")

# Display download counts
print("\nDownload Counts:")
print(f" Last Day: {downloads.get('last_day', 0):,}")
print(f" Last Week: {downloads.get('last_week', 0):,}")
print(f" Last Month: {downloads.get('last_month', 0):,}")

# Display analysis
if analysis:
print("\nAnalysis:")
print(f" Total Downloads: {analysis.get('total_downloads', 0):,}")
print(f" Highest Period: {analysis.get('highest_period', 'N/A')}")

growth = analysis.get('growth_indicators', {})
if growth:
print(" Growth Indicators:")
for indicator, value in growth.items():
print(f" {indicator}: {value}")

# Display repository info if available
project_urls = metadata.get('project_urls', {})
if project_urls:
print("\nRepository Links:")
for name, url in project_urls.items():
if url:
print(f" {name}: {url}")

except Exception as e:
print(f"❌ Error getting stats for {package_name}: {e}")


async def demo_package_download_trends():
"""Demonstrate package download trends analysis."""
print("\n" + "=" * 60)
print("PyPI Package Download Trends Demo")
print("=" * 60)

# Analyze trends for a popular package
package_name = "requests"

print(f"\nπŸ“ˆ Download Trends for '{package_name}':")
print("-" * 50)

try:
# Get download trends (without mirrors for cleaner data)
trends = await get_package_download_trends(package_name, include_mirrors=False)

trend_analysis = trends.get("trend_analysis", {})
time_series = trends.get("time_series", [])

print(f"Package: {package_name}")
print(f"Data Points: {trend_analysis.get('data_points', 0)}")
print(f"Total Downloads: {trend_analysis.get('total_downloads', 0):,}")
print(f"Average Daily: {trend_analysis.get('average_daily', 0):,.0f}")
print(f"Trend Direction: {trend_analysis.get('trend_direction', 'unknown')}")

# Display date range
date_range = trend_analysis.get('date_range', {})
if date_range:
print(f"Date Range: {date_range.get('start')} to {date_range.get('end')}")

# Display peak day
peak_day = trend_analysis.get('peak_day', {})
if peak_day:
print(f"Peak Day: {peak_day.get('date')} ({peak_day.get('downloads', 0):,} downloads)")

# Show recent data points (last 7 days)
if time_series:
print("\nRecent Download Data (last 7 days):")
recent_data = [item for item in time_series if item.get('category') == 'without_mirrors'][-7:]
for item in recent_data:
date = item.get('date', 'unknown')
downloads = item.get('downloads', 0)
print(f" {date}: {downloads:,} downloads")

except Exception as e:
print(f"❌ Error getting trends for {package_name}: {e}")


async def demo_top_packages():
"""Demonstrate top packages by downloads."""
print("\n" + "=" * 60)
print("Top PyPI Packages by Downloads Demo")
print("=" * 60)

periods = ["day", "week", "month"]

for period in periods:
print(f"\nπŸ† Top 10 Packages (last {period}):")
print("-" * 50)

try:
# Get top packages for this period
top_packages = await get_top_packages_by_downloads(period=period, limit=10)

packages_list = top_packages.get("top_packages", [])
total_found = top_packages.get("total_found", 0)

print(f"Found {total_found} packages")
print(f"Data Source: {top_packages.get('data_source', 'unknown')}")

if top_packages.get("note"):
print(f"Note: {top_packages['note']}")

print("\nRankings:")
for package in packages_list:
rank = package.get("rank", "?")
name = package.get("package", "unknown")
downloads = package.get("downloads", 0)
print(f" {rank:2d}. {name:<20} {downloads:>12,} downloads")

except Exception as e:
print(f"❌ Error getting top packages for {period}: {e}")


async def demo_package_comparison():
"""Demonstrate comparing multiple packages."""
print("\n" + "=" * 60)
print("Package Comparison Demo")
print("=" * 60)

# Compare web frameworks
frameworks = ["django", "flask", "fastapi", "tornado"]

print("\nπŸ” Comparing Web Frameworks (last month downloads):")
print("-" * 70)

comparison_data = []

for framework in frameworks:
try:
stats = await get_package_download_stats(framework, period="month")
downloads = stats.get("downloads", {})
last_month = downloads.get("last_month", 0)

comparison_data.append({
"name": framework,
"downloads": last_month,
"metadata": stats.get("metadata", {}),
})

except Exception as e:
print(f"❌ Error getting stats for {framework}: {e}")

# Sort by downloads (descending)
comparison_data.sort(key=lambda x: x["downloads"], reverse=True)

# Display comparison
print(f"{'Rank':<4} {'Framework':<12} {'Downloads':<15} {'Summary'}")
print("-" * 70)

for i, data in enumerate(comparison_data, 1):
name = data["name"]
downloads = data["downloads"]
summary = data["metadata"].get("summary", "No summary")[:30]
print(f"{i:<4} {name:<12} {downloads:<15,} {summary}...")


async def main():
"""Run all demo functions."""
print("πŸš€ Starting PyPI Download Statistics Demo")
print(f"Timestamp: {datetime.now().isoformat()}")

try:
# Run all demos
await demo_package_download_stats()
await demo_package_download_trends()
await demo_top_packages()
await demo_package_comparison()

print("\n" + "=" * 60)
print("βœ… Demo completed successfully!")
print("=" * 60)

except KeyboardInterrupt:
print("\n❌ Demo interrupted by user")
except Exception as e:
print(f"\n❌ Demo failed with error: {e}")


if __name__ == "__main__":
# Run the demo
asyncio.run(main())
Loading