Skip to content

Add Enhanced Error Handling with Specific Exception Types #1

@adamjacobmuller

Description

@adamjacobmuller

Add Enhanced Error Handling with Specific Exception Types

🎯 Objective

Improve error handling in pytryfi by adding specific exception types for different error scenarios. This will help consuming applications (like Home Assistant integrations) handle errors more gracefully and provide better user feedback.

📋 Background

Currently, pytryfi raises generic Exception objects, making it difficult for consumers to differentiate between authentication errors, rate limits, connection issues, and data errors.

🔧 Implementation Plan

1. Create Custom Exception Hierarchy

Update exceptions.py:

"""TryFi API Exceptions."""

class TryFiError(Exception):
    """Base exception for all TryFi errors."""
    pass

class TryFiAuthError(TryFiError):
    """Authentication failed."""
    pass

class TryFiConnectionError(TryFiError):
    """Connection to API failed."""
    pass

class TryFiRateLimitError(TryFiError):
    """API rate limit exceeded."""
    def __init__(self, message, retry_after=None):
        super().__init__(message)
        self.retry_after = retry_after

class TryFiDataError(TryFiError):
    """Invalid data received from API."""
    pass

class TryFiSessionError(TryFiError):
    """Session expired or invalid."""
    pass

class TryFiDeviceError(TryFiError):
    """Device-specific error."""
    def __init__(self, message, device_id=None):
        super().__init__(message)
        self.device_id = device_id

2. Update Login Method

In __init__.py, update the login method:

def login(self, username: str, password: str):
    """Login to the TryFi API."""
    url = API_HOST_URL_BASE + API_LOGIN
    params = {
        'email': username,
        'password': password,
    }
    
    LOGGER.debug("Logging into TryFi")
    try:
        response = self._session.post(url, data=params, timeout=30)
    except requests.exceptions.Timeout:
        raise TryFiConnectionError("Connection to TryFi timed out")
    except requests.exceptions.ConnectionError as e:
        raise TryFiConnectionError(f"Failed to connect to TryFi: {e}")
    
    try:
        response.raise_for_status()
    except requests.exceptions.HTTPError as e:
        if response.status_code == 401:
            raise TryFiAuthError("Invalid username or password")
        elif response.status_code == 429:
            retry_after = response.headers.get('Retry-After')
            raise TryFiRateLimitError("API rate limit exceeded", retry_after)
        else:
            raise TryFiConnectionError(f"HTTP error: {e}")
    
    try:
        json_response = response.json()
    except ValueError:
        raise TryFiDataError("Invalid JSON response from TryFi API")
    
    if 'error' in json_response:
        error_msg = json_response['error'].get('message', 'Unknown error')
        if 'auth' in error_msg.lower() or 'password' in error_msg.lower():
            raise TryFiAuthError(error_msg)
        raise TryFiError(error_msg)
    
    if 'userId' not in json_response or 'sessionId' not in json_response:
        raise TryFiDataError("Missing required fields in login response")
    
    self._userId = json_response['userId']
    self._sessionId = json_response['sessionId']
    self._cookies = response.cookies
    
    LOGGER.debug(f"Successfully logged in. UserId: {self._userId}")
    self.setHeaders()

3. Update Query Methods

In common/query.py, improve error handling:

def execute(session, method, url, params=None, data=None):
    """Execute HTTP request with proper error handling."""
    try:
        if method == 'POST':
            response = session.post(url, json=data, timeout=30)
        elif method == 'GET':
            response = session.get(url, params=params, timeout=30)
        else:
            raise ValueError(f"Unsupported method: {method}")
    except requests.exceptions.Timeout:
        raise TryFiConnectionError(f"Request to {url} timed out")
    except requests.exceptions.ConnectionError as e:
        raise TryFiConnectionError(f"Connection failed: {e}")
    
    # Check for auth errors
    if response.status_code in (401, 403):
        raise TryFiSessionError("Session expired or invalid")
    elif response.status_code == 429:
        retry_after = response.headers.get('Retry-After')
        raise TryFiRateLimitError("API rate limit exceeded", retry_after)
    
    try:
        response.raise_for_status()
    except requests.exceptions.HTTPError as e:
        raise TryFiConnectionError(f"HTTP error: {e}")
    
    return response

4. Update Update Methods

In pet/base/device update methods:

def updatePets(self):
    """Update all pets data."""
    errors = []
    for pet in self._pets:
        try:
            pet.updateAllDetails(self._session)
        except TryFiDeviceError as e:
            LOGGER.warning(f"Failed to update pet {pet.name}: {e}")
            errors.append(e)
        except TryFiConnectionError as e:
            LOGGER.error(f"Connection error updating pets: {e}")
            raise  # Re-raise connection errors
    
    if errors and len(errors) == len(self._pets):
        raise TryFiError("Failed to update any pets")

📝 Benefits

  1. Better Error Handling: Consumers can catch specific exceptions
  2. Retry Logic: Rate limit errors include retry-after information
  3. Debugging: More informative error messages
  4. Resilience: Partial failures don't break everything

🧪 Testing

def test_auth_error():
    """Test authentication error handling."""
    with pytest.raises(TryFiAuthError):
        PyTryFi("invalid@email.com", "wrongpassword")

def test_rate_limit():
    """Test rate limit error includes retry info."""
    try:
        # Trigger rate limit
    except TryFiRateLimitError as e:
        assert e.retry_after is not None

def test_connection_error():
    """Test connection error handling."""
    with patch('requests.post', side_effect=requests.exceptions.Timeout):
        with pytest.raises(TryFiConnectionError):
            PyTryFi("user@email.com", "password")

📋 Checklist

  • Create new exception classes
  • Update login method with specific exceptions
  • Update query methods with error handling
  • Update all API calls to use new exceptions
  • Add timeout parameters to all requests
  • Include retry-after info for rate limits
  • Update tests for new exceptions
  • Update documentation
  • Ensure backward compatibility

🏷️ Labels

enhancement, error-handling, api

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions