A python library for interacting with HAL+JSON APIs
—
Comprehensive exception hierarchy for handling HTTP errors, parsing failures, and navigation issues. RestNavigator provides detailed error information to help diagnose and handle API interaction problems.
Primary exception raised for HTTP error responses with detailed context information.
class HALNavigatorError(Exception):
def __init__(self, message: str, nav: 'HALNavigator', response):
"""
Exception raised for HTTP error responses (4xx, 5xx).
Parameters:
- message: Human-readable error message
- nav: HALNavigator instance that caused the error
- response: HTTP response object from requests
"""
@property
def nav(self) -> 'HALNavigator':
"""Navigator that caused the error"""
@property
def response(self):
"""HTTP response object"""
@property
def message(self) -> str:
"""Error message"""
@property
def status(self) -> int:
"""HTTP status code"""Exceptions for invalid URLs and protocol issues.
class WileECoyoteException(ValueError):
"""
Raised for bad URL schemes.
Occurs when URL has invalid or missing scheme.
"""
class ZachMorrisException(ValueError):
"""
Raised for URLs with too many schemes.
Occurs when URL has multiple conflicting schemes.
"""Exceptions for accessing uninitialized or unavailable resource data.
class NoResponseError(ValueError):
"""
Raised when accessing unfetched navigator fields.
Occurs when trying to access state/response data
before navigator has been resolved.
"""Exception for non-JSON response handling.
class UnexpectedlyNotJSON(TypeError):
def __init__(self, uri: str, response):
"""
Raised for non-JSON parseable responses.
Parameters:
- uri: Resource URI that returned non-JSON
- response: HTTP response object
"""
@property
def uri(self) -> str:
"""Resource URI"""
@property
def response(self):
"""HTTP response object"""Exception for invalid navigation operations and traversal failures.
class OffTheRailsException(TypeError):
def __init__(self, traversal: list, index: int, intermediates: list, exception: Exception):
"""
Raised for invalid navigation traversals.
Parameters:
- traversal: The attempted traversal path
- index: Where in the traversal the failure occurred
- intermediates: Successfully traversed navigators
- exception: Original exception that caused the failure
"""
@property
def traversal(self) -> list:
"""The attempted traversal path"""
@property
def index(self) -> int:
"""Index where traversal failed"""
@property
def intermediates(self) -> list:
"""Intermediate navigators before failure"""
@property
def exception(self) -> Exception:
"""Original exception"""from restnavigator import Navigator
from restnavigator.exc import HALNavigatorError
api = Navigator.hal('https://api.example.com/')
try:
user = api['users', 'nonexistent']()
except HALNavigatorError as e:
print(f"HTTP Error {e.status}: {e.message}")
print(f"Failed URL: {e.nav.uri}")
print(f"Response: {e.response.text}")# Disable automatic exception raising
result = api['users'].create(invalid_data, raise_exc=False)
if not result: # Check if operation succeeded
status_code, reason = result.status
print(f"Creation failed: {status_code} {reason}")
# Access error details from response
if hasattr(result, 'state') and result.state:
print("Error details:", result.state)try:
user_data = api['users', user_id]()
except HALNavigatorError as e:
if e.status == 404:
print("User not found")
# Handle missing resource
elif e.status == 403:
print("Access denied")
# Handle permission error
elif e.status >= 500:
print("Server error, retrying...")
# Handle server errors
else:
print(f"Unexpected error: {e.status}")
raisefrom restnavigator.exc import OffTheRailsException
try:
# Complex navigation that might fail
data = api['users', 0, 'posts', 'comments', 'invalid']()
except OffTheRailsException as e:
print(f"Navigation failed at step {e.index}")
print(f"Attempted path: {e.traversal}")
print(f"Successfully navigated: {len(e.intermediates)} steps")
print(f"Error: {e.exception}")
# Work with successful intermediate results
if e.intermediates:
last_successful = e.intermediates[-1]
print(f"Last successful resource: {last_successful.uri}")from restnavigator.exc import UnexpectedlyNotJSON
try:
# Request might return non-JSON (e.g., HTML error page)
data = api['csv-export']()
except UnexpectedlyNotJSON as e:
print(f"Expected JSON from {e.uri}")
print(f"Got content type: {e.response.headers.get('content-type')}")
# Handle non-JSON response appropriately
if 'text/csv' in e.response.headers.get('content-type', ''):
csv_data = e.response.text
# Process CSV data
else:
print(f"Unexpected content: {e.response.text[:200]}...")from restnavigator.exc import WileECoyoteException, ZachMorrisException
try:
# This might have URL issues
api = Navigator.hal('invalid-url')
except WileECoyoteException:
print("Invalid URL scheme - make sure URL starts with http:// or https://")
except ZachMorrisException:
print("Multiple conflicting schemes in URL")from restnavigator.exc import NoResponseError
# Create navigator but don't fetch yet
user = api['users', user_id]
try:
# This will fail because we haven't fetched the resource
print(user.state)
except NoResponseError:
print("Resource not yet fetched")
# Fetch the resource first
user_data = user()
print(user.state) # Now this worksdef safe_api_call(navigator_func):
"""Wrapper for safe API calls with comprehensive error handling"""
try:
return navigator_func()
except HALNavigatorError as e:
if e.status in [401, 403]:
# Authentication/authorization error
handle_auth_error(e)
elif e.status == 404:
# Resource not found
return None
elif e.status == 429:
# Rate limiting
handle_rate_limit(e)
elif e.status >= 500:
# Server error
handle_server_error(e)
else:
# Other HTTP errors
log_error(f"HTTP {e.status}: {e.message}")
raise
except OffTheRailsException as e:
# Navigation error
log_error(f"Navigation failed: {e.traversal}")
return None
except UnexpectedlyNotJSON as e:
# Content type error
log_error(f"Non-JSON response from {e.uri}")
return e.response.text
except (WileECoyoteException, ZachMorrisException) as e:
# URL validation error
log_error(f"Invalid URL: {e}")
raise ValueError("Invalid API URL configuration")
except NoResponseError:
# Resource state error
log_error("Attempted to access unfetched resource")
return None
# Usage
user_data = safe_api_call(lambda: api['users', user_id]())
if user_data is None:
print("Failed to retrieve user data")def debug_error(exception):
"""Extract comprehensive error information for debugging"""
if isinstance(exception, HALNavigatorError):
print(f"HTTP Error: {exception.status}")
print(f"URL: {exception.nav.uri}")
print(f"Message: {exception.message}")
print(f"Response Headers: {dict(exception.response.headers)}")
print(f"Response Body: {exception.response.text}")
# Check request details
request = exception.response.request
print(f"Request Method: {request.method}")
print(f"Request Headers: {dict(request.headers)}")
if request.body:
print(f"Request Body: {request.body}")
elif isinstance(exception, OffTheRailsException):
print(f"Navigation Error at step {exception.index}")
print(f"Full path: {exception.traversal}")
print(f"Completed steps: {len(exception.intermediates)}")
for i, nav in enumerate(exception.intermediates):
print(f" Step {i}: {nav.uri}")
print(f"Underlying error: {exception.exception}")
# Usage in exception handler
try:
data = api['complex', 'navigation', 'path']()
except Exception as e:
debug_error(e)
raiseInstall with Tessl CLI
npx tessl i tessl/pypi-restnavigator