Ryan Malloy 9f3fd459b3 Implement PyPI Account & Publishing Tools
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.
2025-08-16 08:52:03 -06:00

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)