-
Notifications
You must be signed in to change notification settings - Fork 0
Open
Labels
enhancementNew feature or requestNew feature or request
Description
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
- Better Error Handling: Consumers can catch specific exceptions
- Retry Logic: Rate limit errors include retry-after information
- Debugging: More informative error messages
- 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
Labels
enhancementNew feature or requestNew feature or request