-
Notifications
You must be signed in to change notification settings - Fork 714
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore: created http client, serialiser
- Loading branch information
Showing
13 changed files
with
473 additions
and
0 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import os | ||
import platform | ||
|
||
# __init__.py | ||
|
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,14 @@ | ||
|
||
|
||
# Handle different of authentications, Currently sendgrid authenticate using apikey. | ||
# class AuthStrategy: | ||
# def authenticate(self): | ||
# print('Not yet implemented') | ||
# | ||
# | ||
# class ApiKeyAuthStrategy(AuthStrategy): | ||
# def __init__(self, api_key): | ||
# self.api_key = api_key | ||
# print('init ApiKeyAuthStrategy') | ||
# def authenticate(self, api_key): | ||
# print(f"Authenticating {api_key} using Token Authentication.") |
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,9 @@ | ||
class ClientBase: | ||
|
||
def __init__(self): | ||
print('Creating ClientBase class') | ||
|
||
def request(self): | ||
print('Making 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,46 @@ | ||
import os | ||
import platform | ||
from typing import Dict, List, MutableMapping, Optional, Tuple | ||
from urllib.parse import urlparse, urlunparse | ||
import json | ||
from sendgrid.http.http_client import SendgridHttpClient, HttpClient | ||
from sendgrid.http.request import Request | ||
|
||
|
||
|
||
# class AuthStrategy: | ||
# def authenticate(self): | ||
# pass | ||
# | ||
# | ||
# class ApiKeyAuthStrategy(AuthStrategy): | ||
# def __init__(self, api_key): | ||
# self.api_key = api_key | ||
# | ||
# def authenticate( | ||
# self, | ||
# headers: Optional[Dict[str, str]] = None | ||
# ): | ||
# headers["Authorization"] = f"Bearer {self.api_key}" | ||
# | ||
|
||
class Client: | ||
def __init__( | ||
self, | ||
api_key: str, | ||
region: Optional[str] = None, | ||
edge: Optional[str] = None, | ||
http_client: Optional[HttpClient] = None, | ||
user_agent_extensions: Optional[List[str]] = None | ||
): | ||
self.api_key = api_key | ||
self.region = region | ||
self.edge = edge | ||
self.user_agent_extensions = user_agent_extensions or [] | ||
self.http_client: SendgridHttpClient = SendgridHttpClient() | ||
|
||
def send(self, request: Request): | ||
response = self.http_client.request( | ||
method='POST', url=request.url, data=request.data, headers=request.headers, api_key=self.api_key | ||
) | ||
return response |
Empty file.
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,34 @@ | ||
from enum import Enum | ||
|
||
from enum import Enum | ||
|
||
|
||
def to_serializable(obj): | ||
if isinstance(obj, list): | ||
return [to_serializable(item) for item in obj if item is not None] # Remove None from lists | ||
elif isinstance(obj, dict): | ||
return {key: to_serializable(value) for key, value in obj.items() if | ||
value is not None} # Remove None from dicts | ||
elif hasattr(obj, 'to_dict'): | ||
return obj.to_dict() | ||
elif isinstance(obj, Enum): | ||
return obj.name | ||
else: | ||
return obj | ||
|
||
|
||
def from_serializable(data, cls=None): | ||
""" | ||
Converts a dictionary or list into a class instance or a list of instances. | ||
If `cls` is provided, it will instantiate the class using the dictionary values. | ||
""" | ||
if isinstance(data, list): | ||
return [from_serializable(item, cls) for item in data] # Recursively handle lists | ||
elif isinstance(data, dict): | ||
if cls: | ||
# If a class is provided, instantiate it using the dictionary | ||
return cls(**{key: from_serializable(value) for key, value in data.items()}) | ||
else: | ||
return {key: from_serializable(value) for key, value in data.items()} # Recursively handle dicts | ||
else: | ||
return data # Return primitive types as is |
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,15 @@ | ||
from typing import Any, Dict | ||
|
||
|
||
class SendgridException(Exception): | ||
pass | ||
|
||
|
||
class ApiException(SendgridException): | ||
def __init__(self, status_code: int, error: Any, headers: Dict[str, Any] = None): | ||
self.status_code = status_code | ||
self.error = error | ||
self.headers = headers or {} | ||
|
||
def __str__(self): | ||
return f"ApiException(status_code={self.status_code}, error={self.error}, headers={self.headers})" |
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,5 @@ | ||
|
||
from logging import Logger | ||
from typing import Any, Dict, Optional, Tuple | ||
from urllib.parse import urlencode | ||
|
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,210 @@ | ||
import logging | ||
import os | ||
from logging import Logger | ||
from typing import Any, Dict, Optional, Tuple | ||
from urllib.parse import urlencode | ||
|
||
from requests import Request, Session, hooks | ||
from requests.adapters import HTTPAdapter | ||
|
||
from sendgrid.exceptions import SendgridException | ||
from sendgrid.http.response import Response | ||
|
||
_logger = logging.getLogger("sendgrid.http_client") # TODO: Validate this logger | ||
|
||
|
||
class HttpClient: | ||
def __init__(self, logger: Logger, is_async: bool, timeout: Optional[float] = None): | ||
self.logger = logger | ||
self.is_async = is_async | ||
|
||
if timeout is not None and timeout <= 0: | ||
raise ValueError(timeout) | ||
self.timeout = timeout | ||
|
||
self._test_only_last_request: Optional[Request] = None | ||
self._test_only_last_response: Optional[Response] = None | ||
|
||
""" | ||
An abstract class representing an HTTP client. | ||
""" | ||
|
||
def request( | ||
self, | ||
method: str, | ||
uri: str, | ||
params: Optional[Dict[str, object]] = None, | ||
data: Optional[Dict[str, object]] = None, | ||
headers: Optional[Dict[str, str]] = None, | ||
auth: Optional[Tuple[str, str]] = None, | ||
timeout: Optional[float] = None, | ||
allow_redirects: bool = False, | ||
) -> Response: | ||
""" | ||
Make an HTTP request. | ||
""" | ||
raise SendgridException("HttpClient is an abstract class") | ||
|
||
def log_request(self, kwargs: Dict[str, Any]) -> None: | ||
""" | ||
Logs the HTTP request | ||
""" | ||
self.logger.info("-- BEGIN Twilio API Request --") | ||
|
||
if kwargs["params"]: | ||
self.logger.info( | ||
"{} Request: {}?{}".format( | ||
kwargs["method"], kwargs["url"], urlencode(kwargs["params"]) | ||
) | ||
) | ||
self.logger.info("Query Params: {}".format(kwargs["params"])) | ||
else: | ||
self.logger.info("{} Request: {}".format(kwargs["method"], kwargs["url"])) | ||
|
||
if kwargs["headers"]: | ||
self.logger.info("Headers:") | ||
for key, value in kwargs["headers"].items(): | ||
# Do not log authorization headers | ||
if "authorization" not in key.lower(): | ||
self.logger.info("{} : {}".format(key, value)) | ||
|
||
self.logger.info("-- END Twilio API Request --") | ||
|
||
def log_response(self, status_code: int, response: Response) -> None: | ||
""" | ||
Logs the HTTP response | ||
""" | ||
self.logger.info("Response Status Code: {}".format(status_code)) | ||
self.logger.info("Response Headers: {}".format(response.headers)) | ||
|
||
|
||
class AsyncHttpClient(HttpClient): | ||
""" | ||
An abstract class representing an asynchronous HTTP client. | ||
""" | ||
|
||
async def request( | ||
self, | ||
method: str, | ||
uri: str, | ||
params: Optional[Dict[str, object]] = None, | ||
data: Optional[Dict[str, object]] = None, | ||
headers: Optional[Dict[str, str]] = None, | ||
auth: Optional[Tuple[str, str]] = None, | ||
timeout: Optional[float] = None, | ||
allow_redirects: bool = False, | ||
) -> Response: | ||
""" | ||
Make an asynchronous HTTP request. | ||
""" | ||
raise SendgridException("AsyncHttpClient is an abstract class") | ||
|
||
|
||
class SendgridHttpClient(HttpClient): | ||
""" | ||
General purpose HTTP Client for interacting with the Twilio API | ||
""" | ||
|
||
def __init__( | ||
self, | ||
pool_connections: bool = True, | ||
request_hooks: Optional[Dict[str, object]] = None, | ||
timeout: Optional[float] = None, | ||
logger: logging.Logger = _logger, | ||
proxy: Optional[Dict[str, str]] = None, | ||
max_retries: Optional[int] = None, | ||
): | ||
""" | ||
Constructor for the TwilioHttpClient | ||
:param pool_connections | ||
:param request_hooks | ||
:param timeout: Timeout for the requests. | ||
Timeout should never be zero (0) or less | ||
:param logger | ||
:param proxy: Http proxy for the requests session | ||
:param max_retries: Maximum number of retries each request should attempt | ||
""" | ||
super().__init__(logger, False, timeout) | ||
self.session = Session() if pool_connections else None | ||
if self.session and max_retries is not None: | ||
self.session.mount("https://", HTTPAdapter(max_retries=max_retries)) | ||
if self.session is not None: | ||
self.session.mount( | ||
"https://", HTTPAdapter(pool_maxsize=min(32, os.cpu_count() + 4)) | ||
) | ||
self.request_hooks = request_hooks or hooks.default_hooks() | ||
self.proxy = proxy if proxy else {} | ||
|
||
def request( | ||
self, | ||
method: str, | ||
url: str, | ||
api_key: str = None, | ||
params: Optional[Dict[str, object]] = None, | ||
data: Optional[Dict[str, object]] = None, | ||
headers: Optional[Dict[str, str]] = None, | ||
timeout: Optional[float] = None, | ||
allow_redirects: bool = False, | ||
) -> Response: | ||
""" | ||
Make an HTTP Request with parameters provided. | ||
:param api_key: | ||
:param method: The HTTP method to use | ||
:param url: The URL to request | ||
:param params: Query parameters to append to the URL | ||
:param data: Parameters to go in the body of the HTTP request | ||
:param headers: HTTP Headers to send with the request | ||
:param timeout: Socket/Read timeout for the request | ||
:param allow_redirects: Whether to allow redirects | ||
See the requests documentation for explanation of all these parameters | ||
:return: An HTTP response | ||
""" | ||
if timeout is None: | ||
timeout = self.timeout | ||
elif timeout <= 0: | ||
raise ValueError(timeout) | ||
|
||
headers["Authorization"] = f"Bearer {api_key}" | ||
#auth.authenticate() | ||
kwargs = { | ||
"method": method.upper(), | ||
"url": url, | ||
"params": params, | ||
"headers": headers, | ||
"hooks": self.request_hooks, | ||
} | ||
if headers and headers.get("Content-Type") == "application/json": | ||
kwargs["json"] = data | ||
else: | ||
kwargs["data"] = data | ||
self.log_request(kwargs) | ||
|
||
self._test_only_last_response = None | ||
session = self.session or Session() | ||
request = Request(**kwargs) | ||
self._test_only_last_request = Request(**kwargs) | ||
|
||
prepped_request = session.prepare_request(request) | ||
|
||
settings = session.merge_environment_settings( | ||
prepped_request.url, self.proxy, None, None, None | ||
) | ||
|
||
response = session.send( | ||
prepped_request, | ||
allow_redirects=allow_redirects, | ||
timeout=timeout, | ||
**settings | ||
) | ||
print(response) | ||
print(response.status_code) | ||
print(response.headers) | ||
self.log_response(response.status_code, response) | ||
|
||
self._test_only_last_response = Response( | ||
int(response.status_code), response.text, response.headers | ||
) | ||
|
||
return self._test_only_last_response |
Oops, something went wrong.