Add comprehensive PyPI publishing and account management functionality: Features: - upload_package_to_pypi: Upload distributions to PyPI/TestPyPI with safety checks - check_pypi_credentials: Validate API tokens and credentials - get_pypi_upload_history: View upload history for packages with statistics - delete_pypi_release: Safe release deletion with dry-run and confirmation - manage_pypi_maintainers: Add/remove/list package maintainers - get_pypi_account_info: View account details, quotas, and limits Implementation: - Created pypi_query_mcp/tools/publishing.py with all 6 functions - Added PyPIPublishingClient for authenticated API operations - Comprehensive error handling with custom exceptions - Full async/await patterns following existing codebase conventions - Safety checks for destructive operations (deletion requires confirmation) - Support for both production PyPI and TestPyPI Integration: - Added publishing-specific exceptions to core/exceptions.py - Updated tools/__init__.py with publishing function imports - Added 6 MCP server endpoints to server.py with proper error handling - Created comprehensive tests in tests/test_publishing.py Production-ready code with proper authentication, validation, and safety measures.
86 lines
2.6 KiB
Python
86 lines
2.6 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
|
|
|
|
|
|
class PyPIAuthenticationError(PyPIError):
|
|
"""Raised when PyPI authentication fails."""
|
|
|
|
def __init__(self, message: str, status_code: int | None = None):
|
|
super().__init__(message, status_code)
|
|
|
|
|
|
class PyPIUploadError(PyPIError):
|
|
"""Raised when PyPI upload operations fail."""
|
|
|
|
def __init__(self, message: str, status_code: int | None = None):
|
|
super().__init__(message, status_code)
|
|
|
|
|
|
class PyPIPermissionError(PyPIError):
|
|
"""Raised when PyPI permission operations fail."""
|
|
|
|
def __init__(self, message: str, status_code: int | None = None):
|
|
super().__init__(message, status_code)
|