- 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"
65 lines
2.0 KiB
Python
65 lines
2.0 KiB
Python
"""Custom exceptions for PyPI Query MCP Server."""
|
|
|
|
|
|
class PyPIError(Exception):
|
|
"""Base exception for PyPI-related errors."""
|
|
|
|
def __init__(self, message: str, status_code: int | None = None):
|
|
super().__init__(message)
|
|
self.message = message
|
|
self.status_code = status_code
|
|
|
|
|
|
class PackageNotFoundError(PyPIError):
|
|
"""Raised when a package is not found on PyPI."""
|
|
|
|
def __init__(self, package_name: str):
|
|
message = f"Package '{package_name}' not found on PyPI"
|
|
super().__init__(message, status_code=404)
|
|
self.package_name = package_name
|
|
|
|
|
|
class NetworkError(PyPIError):
|
|
"""Raised when network-related errors occur."""
|
|
|
|
def __init__(self, message: str, original_error: Exception | None = None):
|
|
super().__init__(message)
|
|
self.original_error = original_error
|
|
|
|
|
|
class RateLimitError(PyPIError):
|
|
"""Raised when API rate limit is exceeded."""
|
|
|
|
def __init__(self, retry_after: int | None = None):
|
|
message = "PyPI API rate limit exceeded"
|
|
if retry_after:
|
|
message += f". Retry after {retry_after} seconds"
|
|
super().__init__(message, status_code=429)
|
|
self.retry_after = retry_after
|
|
|
|
|
|
class InvalidPackageNameError(PyPIError):
|
|
"""Raised when package name is invalid."""
|
|
|
|
def __init__(self, package_name: str):
|
|
message = f"Invalid package name: '{package_name}'"
|
|
super().__init__(message, status_code=400)
|
|
self.package_name = package_name
|
|
|
|
|
|
class PyPIServerError(PyPIError):
|
|
"""Raised when PyPI server returns a server error."""
|
|
|
|
def __init__(self, status_code: int, message: str | None = None):
|
|
if not message:
|
|
message = f"PyPI server error (HTTP {status_code})"
|
|
super().__init__(message, status_code=status_code)
|
|
|
|
|
|
class SearchError(PyPIError):
|
|
"""Raised when search operations fail."""
|
|
|
|
def __init__(self, message: str, query: str | None = None):
|
|
super().__init__(message)
|
|
self.query = query
|