Building a Robust REST API Client in Python
This challenge focuses on creating a reusable and reliable REST API client in Python. A well-designed client simplifies interacting with external APIs, handling common tasks like request construction, error handling, and response parsing. This is a fundamental skill for any Python developer working with web services.
Problem Description
You are tasked with building a Python class, APIClient, that acts as a client for interacting with REST APIs. The client should handle making GET, POST, PUT, and DELETE requests to specified endpoints. It should also manage authentication (using a provided API key), handle potential errors gracefully, and return parsed JSON responses.
Key Requirements:
- Base URL: The client should be initialized with a base URL for the API.
- Authentication: The client should accept an API key during initialization and include it in the
Authorizationheader for each request. - HTTP Methods: Implement methods for GET, POST, PUT, and DELETE requests. Each method should accept an endpoint (relative to the base URL) and optional data (for POST/PUT).
- Error Handling: The client should handle HTTP errors (status codes 4xx and 5xx) by raising appropriate exceptions with informative error messages.
- JSON Parsing: The client should automatically parse JSON responses into Python dictionaries.
- Headers: The client should set the
Content-Typeheader toapplication/jsonfor POST and PUT requests.
Expected Behavior:
- Successful requests should return a Python dictionary representing the parsed JSON response.
- Failed requests (HTTP errors) should raise an
APIErrorexception. - The client should handle network errors gracefully.
Edge Cases to Consider:
- Invalid JSON responses.
- Missing API key.
- Malformed endpoint URLs.
- Network connectivity issues.
- Empty responses.
Examples
Example 1:
Input:
api_client = APIClient(base_url="https://api.example.com", api_key="YOUR_API_KEY")
response = api_client.get(endpoint="/users/123")
Output:
{'id': 123, 'name': 'John Doe', 'email': 'john.doe@example.com'}
Explanation:
A GET request is made to "https://api.example.com/users/123" with the API key in the Authorization header. The response is assumed to be valid JSON and is parsed into a Python dictionary.
Example 2:
Input:
api_client = APIClient(base_url="https://api.example.com", api_key="YOUR_API_KEY")
response = api_client.post(endpoint="/users", data={'name': 'Jane Doe', 'email': 'jane.doe@example.com'})
Output:
{'id': 456, 'name': 'Jane Doe', 'email': 'jane.doe@example.com'}
Explanation:
A POST request is made to "https://api.example.com/users" with the provided data and the API key. The `Content-Type` header is set to `application/json`. The response is parsed as JSON.
Example 3: (Error Handling)
Input:
api_client = APIClient(base_url="https://api.example.com", api_key="YOUR_API_KEY")
try:
response = api_client.get(endpoint="/nonexistent_resource")
except APIError as e:
print(f"Error: {e}")
Output:
Error: HTTP Error 404: Not Found - Not Found
Explanation:
A GET request to a non-existent resource results in a 404 error. The client catches this error and raises an APIError exception with a descriptive message.
Constraints
- The API client should be implemented as a class.
- Use the
requestslibrary for making HTTP requests. You will need to install it:pip install requests. - The
APIErrorexception should be a custom exception class inheriting fromException. - The base URL should be a valid URL string.
- The API key should be a non-empty string.
- The endpoint should be a string.
- Data for POST/PUT requests should be a Python dictionary.
- The client should handle timeouts gracefully (e.g., a timeout of 5 seconds).
Notes
- Consider using
try...exceptblocks to handle potential errors during request execution and JSON parsing. - Think about how to structure your code for reusability and maintainability.
- The
requestslibrary provides many useful features for making HTTP requests. Refer to its documentation for more information. - Focus on creating a robust and well-documented client that can handle various scenarios.
- Remember to include appropriate docstrings for your class and methods.
- Consider adding logging for debugging purposes.