-
Notifications
You must be signed in to change notification settings - Fork 115
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add http client module and custom error parsing logic, expand test suite
- Loading branch information
Showing
23 changed files
with
461 additions
and
68 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,100 @@ | ||
from json import JSONDecodeError | ||
|
||
from requests import Response | ||
from vonage_error import VonageError | ||
|
||
|
||
class JWTGenerationError(VonageError): | ||
"""Indicates an error with generating a JWT.""" | ||
|
||
|
||
class InvalidHttpClientOptionsError(VonageError): | ||
"""The options passed to the HTTP Client were invalid.""" | ||
|
||
|
||
class HttpRequestError(VonageError): | ||
"""Exception indicating an error in the response received from a Vonage SDK request. | ||
Args: | ||
response (requests.Response): The HTTP response object. | ||
content_type (str): The response content type. | ||
Attributes: | ||
response (requests.Response): The HTTP response object. | ||
message (str): The returned error message. | ||
""" | ||
|
||
def __init__(self, response: Response, content_type: str): | ||
self.response = response | ||
self.set_error_message(self.response, content_type) | ||
super().__init__(self.message) | ||
|
||
def set_error_message(self, response: Response, content_type: str): | ||
body = None | ||
if content_type == 'application/json': | ||
try: | ||
body = response.json() | ||
except JSONDecodeError: | ||
pass | ||
else: | ||
body = response.text | ||
|
||
if body: | ||
self.message = f'{response.status_code} response from {response.url}. Error response body: {body}' | ||
else: | ||
self.message = f'{response.status_code} response from {response.url}.' | ||
|
||
|
||
class AuthenticationError(HttpRequestError): | ||
"""Exception indicating authentication failure in a Vonage SDK request. | ||
This error is raised when the HTTP response status code is 401 (Unauthorized). | ||
Args: | ||
response (requests.Response): The HTTP response object. | ||
content_type (str): The response content type. | ||
Attributes (inherited from HttpRequestError parent exception): | ||
response (requests.Response): The HTTP response object. | ||
message (str): The returned error message. | ||
""" | ||
|
||
def __init__(self, response: Response, content_type: str): | ||
super().__init__(response, content_type) | ||
|
||
|
||
class RateLimitedError(HttpRequestError): | ||
"""Exception indicating a rate limit was hit when making too many requests to a Vonage endpoint. | ||
This error is raised when the HTTP response status code is 429 (Too Many Requests). | ||
Args: | ||
response (requests.Response): The HTTP response object. | ||
content_type (str): The response content type. | ||
Attributes (inherited from HttpRequestError parent exception): | ||
response (requests.Response): The HTTP response object. | ||
message (str): The returned error message. | ||
""" | ||
|
||
def __init__(self, response: Response, content_type: str): | ||
super().__init__(response, content_type) | ||
|
||
|
||
class ServerError(HttpRequestError): | ||
"""Exception indicating an error was returned by a Vonage server in response to a Vonage SDK | ||
request. | ||
This error is raised when the HTTP response status code is 500 (Internal Server Error). | ||
Args: | ||
response (requests.Response): The HTTP response object. | ||
content_type (str): The response content type. | ||
Attributes (inherited from HttpRequestError parent exception): | ||
response (requests.Response): The HTTP response object. | ||
message (str): The returned error message. | ||
""" | ||
|
||
def __init__(self, response: Response, content_type: str): | ||
super().__init__(response, content_type) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,32 +1,136 @@ | ||
import asyncio | ||
import logging | ||
from logging import getLogger | ||
from platform import python_version | ||
from typing import Literal, Optional | ||
|
||
import aiohttp | ||
from http_client.auth import Auth | ||
from http_client.errors import ( | ||
AuthenticationError, | ||
HttpRequestError, | ||
InvalidHttpClientOptionsError, | ||
RateLimitedError, | ||
ServerError, | ||
) | ||
from pydantic import BaseModel, Field, ValidationError, validate_call | ||
from requests import Response | ||
from requests.adapters import HTTPAdapter | ||
from requests.sessions import Session | ||
from typing_extensions import Annotated | ||
|
||
logger = logging.getLogger("vonage") | ||
logger = getLogger('vonage-http-client') | ||
|
||
|
||
class HttpClient: | ||
"""An asynchronous HTTP client used to send authenticated requests to Vonage APIs. | ||
The methods for making HTTP calls are built to be asynchronous but can also be called | ||
synchronously, which is the default. This is to make using the Vonage SDK the simplest possible | ||
experience without customization. | ||
"""A synchronous HTTP client used to send authenticated requests to Vonage APIs. | ||
Args: | ||
auth (:class: Auth): An instance of the Auth class containing credentials to use when making HTTP requests. | ||
http_client_options (dict, optional): Customization options for the HTTP Client. | ||
The http_client_options dict can have any of the following fields: | ||
api_host (str, optional): The API host to use for HTTP requests. Defaults to 'api.nexmo.com'. | ||
rest_host (str, optional): The REST host to use for HTTP requests. Defaults to 'rest.nexmo.com'. | ||
timeout (int, optional): The timeout for HTTP requests in seconds. Defaults to None. | ||
pool_connections (int, optional): The number of pool connections. Must be > 0. Default is 10. | ||
pool_maxsize (int, optional): The maximum size of the connection pool. Must be > 0. Default is 10. | ||
max_retries (int, optional): The maximum number of retries for HTTP requests. Must be >= 0. Default is 3. | ||
""" | ||
|
||
def __init__(self, auth: Auth, client_options: dict = None): | ||
def __init__(self, auth: Auth, http_client_options: dict = None): | ||
self._auth = auth | ||
self._client_options = client_options # as yet undefined | ||
try: | ||
if http_client_options is not None: | ||
self._http_client_options = HttpClientOptions.model_validate( | ||
http_client_options | ||
) | ||
else: | ||
self._http_client_options = HttpClientOptions() | ||
except ValidationError as err: | ||
raise InvalidHttpClientOptionsError( | ||
'Invalid options provided to the HTTP Client' | ||
) from err | ||
|
||
self._api_host = self._http_client_options.api_host | ||
self._rest_host = self._http_client_options.rest_host | ||
|
||
self._timeout = self._http_client_options.timeout | ||
self._session = Session() | ||
self._adapter = HTTPAdapter( | ||
pool_connections=self._http_client_options.pool_connections, | ||
pool_maxsize=self._http_client_options.pool_maxsize, | ||
max_retries=self._http_client_options.max_retries, | ||
) | ||
self._session.mount('https://', self._adapter) | ||
|
||
self.session = aiohttp.ClientSession() | ||
self._user_agent = f'vonage-python-sdk python/{python_version()}' | ||
self._headers = {'User-Agent': self._user_agent, 'Accept': 'application/json'} | ||
|
||
@property | ||
def auth(self): | ||
return self._auth | ||
def http_client_options(self): | ||
return self._http_client_options | ||
|
||
@property | ||
def api_host(self): | ||
return self._api_host | ||
|
||
@property | ||
def rest_host(self): | ||
return self._rest_host | ||
|
||
def post(self, host: str, request_path: str = '', params: dict = None): | ||
return self.make_request('POST', host, request_path, params) | ||
|
||
def get(self, host: str, request_path: str = '', params: dict = None): | ||
return self.make_request('GET', host, request_path, params) | ||
|
||
@validate_call | ||
def make_request( | ||
self, | ||
request_type: Literal['GET', 'POST'], | ||
host: str, | ||
request_path: str = '', | ||
params: Optional[dict] = None, | ||
): | ||
url = f'https://{host}{request_path}' | ||
logger.debug( | ||
f'{request_type} request to {url}, with data: {params}; headers: {self._headers}' | ||
) | ||
with self._session.request( | ||
request_type, | ||
url, | ||
json=params, | ||
headers=self._headers, | ||
timeout=self._timeout, | ||
) as response: | ||
return self._parse_response(response) | ||
|
||
def _parse_response(self, response: Response): | ||
logger.debug( | ||
f'Response received from {response.url} with status code: {response.status_code}; headers: {response.headers}' | ||
) | ||
content_type = response.headers['Content-Type'].split(';', 1)[0] | ||
if 200 <= response.status_code < 300: | ||
if response.status_code == 204: | ||
return None | ||
if content_type == 'application/json': | ||
return response.json() | ||
return response.text | ||
if response.status_code >= 400: | ||
logger.warning( | ||
f'Http Response Error! Status code: {response.status_code}; content: {repr(response.text)}; from url: {response.url}' | ||
) | ||
if response.status_code == 401: | ||
raise AuthenticationError(response, content_type) | ||
elif response.status_code == 429: | ||
raise RateLimitedError(response, content_type) | ||
elif response.status_code == 500: | ||
raise ServerError(response, content_type) | ||
raise HttpRequestError(response, content_type) | ||
|
||
|
||
def make_request(self, method): | ||
... | ||
class HttpClientOptions(BaseModel): | ||
api_host: str = 'api.nexmo.com' | ||
rest_host: Optional[str] = 'rest.nexmo.com' | ||
timeout: Optional[Annotated[int, Field(ge=0)]] = None | ||
pool_connections: Optional[Annotated[int, Field(ge=1)]] = 10 | ||
pool_maxsize: Optional[Annotated[int, Field(ge=1)]] = 10 | ||
max_retries: Optional[Annotated[int, Field(ge=0)]] = 3 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
python_tests(dependencies=['http_client:dummy_private_key']) | ||
python_tests(dependencies=['http_client', 'libs']) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
{"Error": "Bad Request"} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
{"Error": "Authentication Failed"} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
{"Error": "Too Many Requests"} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
{"Error": "Internal Server Error"} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"hello": "world" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Hello, World! |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"hello": "world!" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.