feat: add comprehensive PyPI search functionality with advanced filtering

- Implemented PyPISearchClient with semantic search, filtering, and sorting
- Added 4 new search tools: search_packages, search_by_category, find_alternatives, get_trending_packages
- Created SearchFilter and SearchSort classes for flexible configuration
- Added SearchError exception for search-specific error handling
- Comprehensive test suite with 13 tests covering all search functionality
- Enhanced MCP server with 4 new search endpoints
- Support for filtering by Python version, license, category, downloads, maintenance status
- Multiple sorting options: relevance, popularity, quality, recency, name, downloads
- Semantic search using description similarity scoring
- Category-based package discovery with intelligent keyword matching
- Package alternatives finder using metadata analysis
- Trending packages analysis with download activity tracking
- Robust fallback mechanisms using curated package database
- All tests passing (13/13)

This implements feature #6 from the roadmap: "Advanced PyPI Search with filtering by Python version, license, maintenance status and sorting by popularity, recency, quality score with semantic search capabilities"
This commit is contained in:
Ryan Malloy 2025-08-16 07:45:40 -06:00
parent 7ace364f32
commit e205176ace
6 changed files with 1430 additions and 1 deletions

View file

@ -6,7 +6,7 @@ from typing import Any
import click
from fastmcp import FastMCP
from .core.exceptions import InvalidPackageNameError, NetworkError, PackageNotFoundError
from .core.exceptions import InvalidPackageNameError, NetworkError, PackageNotFoundError, SearchError
from .prompts import (
analyze_daily_trends,
analyze_environment_dependencies,
@ -26,14 +26,18 @@ from .prompts import (
from .tools import (
check_python_compatibility,
download_package_with_dependencies,
find_alternatives,
get_compatible_python_versions,
get_package_download_stats,
get_package_download_trends,
get_top_packages_by_downloads,
get_trending_packages,
query_package_dependencies,
query_package_info,
query_package_versions,
resolve_package_dependencies,
search_by_category,
search_packages,
)
# Configure logging
@ -613,6 +617,195 @@ async def get_top_downloaded_packages(
}
@mcp.tool()
async def search_pypi_packages(
query: str,
limit: int = 20,
python_versions: list[str] | None = None,
licenses: list[str] | None = None,
categories: list[str] | None = None,
min_downloads: int | None = None,
maintenance_status: str | None = None,
has_wheels: bool | None = None,
sort_by: str = "relevance",
sort_desc: bool = True,
semantic_search: bool = False,
) -> dict[str, Any]:
"""Search PyPI packages with advanced filtering and sorting.
This tool provides comprehensive search functionality for PyPI packages with
advanced filtering options, multiple sorting criteria, and semantic search capabilities.
Args:
query: Search query string (required)
limit: Maximum number of results to return (default: 20, max: 100)
python_versions: Filter by Python versions (e.g., ["3.9", "3.10", "3.11"])
licenses: Filter by license types (e.g., ["mit", "apache", "bsd", "gpl"])
categories: Filter by categories (e.g., ["web", "data-science", "testing"])
min_downloads: Minimum monthly downloads threshold
maintenance_status: Filter by maintenance status ("active", "maintained", "stale", "abandoned")
has_wheels: Filter packages that have wheel distributions (true/false)
sort_by: Sort field ("relevance", "popularity", "recency", "quality", "name", "downloads")
sort_desc: Sort in descending order (default: true)
semantic_search: Use semantic search on package descriptions (default: false)
Returns:
Dictionary containing search results with packages, metadata, and filtering info
Raises:
InvalidPackageNameError: If search query is empty or invalid
SearchError: If search operation fails
"""
try:
return await search_packages(
query=query,
limit=limit,
python_versions=python_versions,
licenses=licenses,
categories=categories,
min_downloads=min_downloads,
maintenance_status=maintenance_status,
has_wheels=has_wheels,
sort_by=sort_by,
sort_desc=sort_desc,
semantic_search=semantic_search,
)
except (InvalidPackageNameError, PackageNotFoundError, NetworkError):
raise
except Exception as e:
logger.error(f"Error searching packages for '{query}': {e}")
return {
"error": f"Search failed: {e}",
"error_type": "SearchError",
"query": query,
"limit": limit,
}
@mcp.tool()
async def search_packages_by_category(
category: str,
limit: int = 20,
sort_by: str = "popularity",
python_version: str | None = None,
) -> dict[str, Any]:
"""Search packages by category with popularity sorting.
This tool searches for packages in specific categories, making it easy to discover
relevant packages for particular use cases or domains.
Args:
category: Category to search ("web", "data-science", "database", "testing", "cli",
"security", "networking", "dev-tools", "cloud", "gui")
limit: Maximum number of results to return (default: 20)
sort_by: Sort field (default: "popularity")
python_version: Filter by Python version compatibility (e.g., "3.10")
Returns:
Dictionary containing categorized search results
Raises:
SearchError: If category search fails
"""
try:
return await search_by_category(
category=category,
limit=limit,
sort_by=sort_by,
python_version=python_version,
)
except Exception as e:
logger.error(f"Error searching category '{category}': {e}")
return {
"error": f"Category search failed: {e}",
"error_type": "SearchError",
"category": category,
"limit": limit,
}
@mcp.tool()
async def find_package_alternatives(
package_name: str,
limit: int = 10,
include_similar: bool = True,
) -> dict[str, Any]:
"""Find alternative packages to a given package.
This tool analyzes a package's functionality and finds similar or alternative
packages that could serve the same purpose, useful for evaluating options
or finding replacements.
Args:
package_name: Name of the package to find alternatives for
limit: Maximum number of alternatives to return (default: 10)
include_similar: Include packages with similar functionality (default: true)
Returns:
Dictionary containing alternative packages with analysis and recommendations
Raises:
PackageNotFoundError: If the target package is not found
SearchError: If alternatives search fails
"""
try:
return await find_alternatives(
package_name=package_name,
limit=limit,
include_similar=include_similar,
)
except (InvalidPackageNameError, PackageNotFoundError, NetworkError):
raise
except Exception as e:
logger.error(f"Error finding alternatives for '{package_name}': {e}")
return {
"error": f"Alternatives search failed: {e}",
"error_type": "SearchError",
"package_name": package_name,
"limit": limit,
}
@mcp.tool()
async def get_trending_pypi_packages(
category: str | None = None,
time_period: str = "week",
limit: int = 20,
) -> dict[str, Any]:
"""Get trending packages based on recent download activity.
This tool identifies packages that are gaining popularity or have high
recent download activity, useful for discovering emerging trends in the
Python ecosystem.
Args:
category: Optional category filter ("web", "data-science", "database", etc.)
time_period: Time period for trending analysis ("day", "week", "month")
limit: Maximum number of packages to return (default: 20)
Returns:
Dictionary containing trending packages with analysis and metrics
Raises:
SearchError: If trending analysis fails
"""
try:
return await get_trending_packages(
category=category,
time_period=time_period,
limit=limit,
)
except Exception as e:
logger.error(f"Error getting trending packages (category: {category}): {e}")
return {
"error": f"Trending analysis failed: {e}",
"error_type": "SearchError",
"category": category,
"time_period": time_period,
"limit": limit,
}
# Register prompt templates following standard MCP workflow:
# 1. User calls tool → MCP client sends request
# 2. Tool function executes → Collects necessary data and parameters