Skip to content

Commit

Permalink
chore: created http client, serialiser
Browse files Browse the repository at this point in the history
  • Loading branch information
sbansla committed Sep 24, 2024
1 parent 9515dce commit 0bd28bf
Show file tree
Hide file tree
Showing 13 changed files with 473 additions and 0 deletions.
4 changes: 4 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
Flask==1.1.2
requests>=2.31.0
aiohttp>=3.9.4
aiohttp-retry>=2.8.3

PyYAML>=4.2b1
python-http-client>=3.2.1
six==1.11.0
Expand Down
5 changes: 5 additions & 0 deletions sendgrid/base/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import os
import platform

# __init__.py

14 changes: 14 additions & 0 deletions sendgrid/base/auth_strategy.py
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.")
9 changes: 9 additions & 0 deletions sendgrid/base/client_base.py
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')


46 changes: 46 additions & 0 deletions sendgrid/client.py
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 added sendgrid/converters/__init__.py
Empty file.
34 changes: 34 additions & 0 deletions sendgrid/converters/serialize.py
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
15 changes: 15 additions & 0 deletions sendgrid/exceptions/__init__.py
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})"
5 changes: 5 additions & 0 deletions sendgrid/http/__init__.py
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

210 changes: 210 additions & 0 deletions sendgrid/http/http_client.py
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
Loading

0 comments on commit 0bd28bf

Please sign in to comment.