From 0bd28bf1be94e7b58113a4d5192844d2d7ce063a Mon Sep 17 00:00:00 2001 From: sbansla Date: Tue, 24 Sep 2024 21:10:38 +0530 Subject: [PATCH 01/12] chore: created http client, serialiser --- requirements.txt | 4 + sendgrid/base/__init__.py | 5 + sendgrid/base/auth_strategy.py | 14 +++ sendgrid/base/client_base.py | 9 ++ sendgrid/client.py | 46 +++++++ sendgrid/converters/__init__.py | 0 sendgrid/converters/serialize.py | 34 +++++ sendgrid/exceptions/__init__.py | 15 +++ sendgrid/http/__init__.py | 5 + sendgrid/http/http_client.py | 210 +++++++++++++++++++++++++++++++ sendgrid/http/request.py | 87 +++++++++++++ sendgrid/http/response.py | 31 +++++ sendgrid/utility/__init__.py | 13 ++ 13 files changed, 473 insertions(+) create mode 100644 sendgrid/base/__init__.py create mode 100644 sendgrid/base/auth_strategy.py create mode 100644 sendgrid/base/client_base.py create mode 100644 sendgrid/client.py create mode 100644 sendgrid/converters/__init__.py create mode 100644 sendgrid/converters/serialize.py create mode 100644 sendgrid/exceptions/__init__.py create mode 100644 sendgrid/http/__init__.py create mode 100644 sendgrid/http/http_client.py create mode 100644 sendgrid/http/request.py create mode 100644 sendgrid/http/response.py create mode 100644 sendgrid/utility/__init__.py diff --git a/requirements.txt b/requirements.txt index 0c34aafd4..c2742ac83 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/sendgrid/base/__init__.py b/sendgrid/base/__init__.py new file mode 100644 index 000000000..5a4589bbb --- /dev/null +++ b/sendgrid/base/__init__.py @@ -0,0 +1,5 @@ +import os +import platform + +# __init__.py + diff --git a/sendgrid/base/auth_strategy.py b/sendgrid/base/auth_strategy.py new file mode 100644 index 000000000..1a443b78d --- /dev/null +++ b/sendgrid/base/auth_strategy.py @@ -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.") \ No newline at end of file diff --git a/sendgrid/base/client_base.py b/sendgrid/base/client_base.py new file mode 100644 index 000000000..75f454495 --- /dev/null +++ b/sendgrid/base/client_base.py @@ -0,0 +1,9 @@ +class ClientBase: + + def __init__(self): + print('Creating ClientBase class') + + def request(self): + print('Making request') + + \ No newline at end of file diff --git a/sendgrid/client.py b/sendgrid/client.py new file mode 100644 index 000000000..b0885f6ae --- /dev/null +++ b/sendgrid/client.py @@ -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 diff --git a/sendgrid/converters/__init__.py b/sendgrid/converters/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/sendgrid/converters/serialize.py b/sendgrid/converters/serialize.py new file mode 100644 index 000000000..7a5a808b2 --- /dev/null +++ b/sendgrid/converters/serialize.py @@ -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 diff --git a/sendgrid/exceptions/__init__.py b/sendgrid/exceptions/__init__.py new file mode 100644 index 000000000..11e921cdd --- /dev/null +++ b/sendgrid/exceptions/__init__.py @@ -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})" diff --git a/sendgrid/http/__init__.py b/sendgrid/http/__init__.py new file mode 100644 index 000000000..504eb219a --- /dev/null +++ b/sendgrid/http/__init__.py @@ -0,0 +1,5 @@ + +from logging import Logger +from typing import Any, Dict, Optional, Tuple +from urllib.parse import urlencode + diff --git a/sendgrid/http/http_client.py b/sendgrid/http/http_client.py new file mode 100644 index 000000000..af2e7167f --- /dev/null +++ b/sendgrid/http/http_client.py @@ -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 diff --git a/sendgrid/http/request.py b/sendgrid/http/request.py new file mode 100644 index 000000000..7279243bf --- /dev/null +++ b/sendgrid/http/request.py @@ -0,0 +1,87 @@ +from enum import Enum +from typing import Any, Dict, Tuple, Union +from urllib.parse import urlencode + + +class Match(Enum): + ANY = "*" + + +class Request(object): + def __init__( + self, + method: Union[str, Match] = Match.ANY, + url: Union[str, Match] = Match.ANY, + auth: Union[Tuple[str, str], Match] = Match.ANY, + params: Union[Dict[str, str], Match] = Match.ANY, + data: Union[Dict[str, str], Match] = Match.ANY, + headers: Union[Dict[str, str], Match] = Match.ANY, + **kwargs: Any + ): + self.method = method + if method and method is not Match.ANY: + self.method = method.upper() + self.url = url + self.auth = auth + self.params = params + self.data = data + self.headers = headers + + @classmethod + def attribute_equal(cls, lhs, rhs) -> bool: + if lhs == Match.ANY or rhs == Match.ANY: + # ANY matches everything + return True + + lhs = lhs or None + rhs = rhs or None + + return lhs == rhs + + def __eq__(self, other) -> bool: + if not isinstance(other, Request): + return False + + return ( + self.attribute_equal(self.method, other.method) + and self.attribute_equal(self.url, other.url) + and self.attribute_equal(self.auth, other.auth) + and self.attribute_equal(self.params, other.params) + and self.attribute_equal(self.data, other.data) + and self.attribute_equal(self.headers, other.headers) + ) + + def __str__(self) -> str: + auth = "" + if self.auth and self.auth != Match.ANY: + auth = "{} ".format(self.auth) + + params = "" + if self.params and self.params != Match.ANY: + params = "?{}".format(urlencode(self.params, doseq=True)) + + data = "" + if self.data and self.data != Match.ANY: + if self.method == "GET": + data = "\n -G" + data += "\n{}".format( + "\n".join(' -d "{}={}"'.format(k, v) for k, v in self.data.items()) + ) + + headers = "" + if self.headers and self.headers != Match.ANY: + headers = "\n{}".format( + "\n".join(' -H "{}: {}"'.format(k, v) for k, v in self.headers.items()) + ) + + return "{auth}{method} {url}{params}{data}{headers}".format( + auth=auth, + method=self.method, + url=self.url, + params=params, + data=data, + headers=headers, + ) + + def __repr__(self) -> str: + return str(self) diff --git a/sendgrid/http/response.py b/sendgrid/http/response.py new file mode 100644 index 000000000..6d86c2d5a --- /dev/null +++ b/sendgrid/http/response.py @@ -0,0 +1,31 @@ +from typing import Any, Optional + + +class HTTPStatus: + SUCCESS = range(200, 300) # Success codes: 200-299 + CLIENT_ERROR = range(400, 500) # Client error codes: 400-499 + SERVER_ERROR = range(500, 600) # Server error codes: 500-599 + + +class Response(object): + def __init__( + self, + status_code: int, + text: str, + headers: Optional[Any] = None, + ): + self.content = text + self.headers = headers + self.cached = False + self.status_code = status_code + self.ok = self.status_code < 400 + + @property + def text(self) -> str: + return self.content + + def is_success(self): + return self.status_code in HTTPStatus.SUCCESS + + def __repr__(self) -> str: + return "HTTP {} {}".format(self.status_code, self.content) \ No newline at end of file diff --git a/sendgrid/utility/__init__.py b/sendgrid/utility/__init__.py new file mode 100644 index 000000000..b423c836a --- /dev/null +++ b/sendgrid/utility/__init__.py @@ -0,0 +1,13 @@ +import json +from typing import Any + +from sendgrid.http.response import Response + + +def parse_response(self, response: Response) -> Any: + + if response.status_code < 200 or response.status_code >= 300: + raise self.exception(response, "Unable to create record") + + return json.loads(response.text) + From 88bd6eb3f3ffbcaa572ec7c69e18db2ebc4901ef Mon Sep 17 00:00:00 2001 From: sbansla Date: Tue, 24 Sep 2024 21:14:11 +0530 Subject: [PATCH 02/12] added docker-compose --- .github/workflows/test-and-deploy.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/test-and-deploy.yml b/.github/workflows/test-and-deploy.yml index 4db532e3d..25408ac0a 100644 --- a/.github/workflows/test-and-deploy.yml +++ b/.github/workflows/test-and-deploy.yml @@ -31,6 +31,11 @@ jobs: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_AUTH_TOKEN }} + - name: Install Docker Compose + run: | + sudo apt-get update + sudo apt-get install -y docker-compose + - name: Build & Test run: make test-docker version=${{ matrix.python-version }} From 550250bea71d151408a2a535606642a56cb47324 Mon Sep 17 00:00:00 2001 From: sbansla Date: Sat, 28 Sep 2024 12:10:36 +0530 Subject: [PATCH 03/12] chore: added __str__ to response --- sendgrid/base/values.py | 13 +++++++++++++ sendgrid/http/response.py | 19 +++++++++++++++++-- 2 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 sendgrid/base/values.py diff --git a/sendgrid/base/values.py b/sendgrid/base/values.py new file mode 100644 index 000000000..16032b112 --- /dev/null +++ b/sendgrid/base/values.py @@ -0,0 +1,13 @@ +from typing import Dict + +unset = object() + + +def of(d: Dict[str, object]) -> Dict[str, object]: + """ + Remove unset values from a dict. + + :param d: A dict to strip. + :return A dict with unset values removed. + """ + return {k: v for k, v in d.items() if v != unset} diff --git a/sendgrid/http/response.py b/sendgrid/http/response.py index 6d86c2d5a..8c947516c 100644 --- a/sendgrid/http/response.py +++ b/sendgrid/http/response.py @@ -27,5 +27,20 @@ def text(self) -> str: def is_success(self): return self.status_code in HTTPStatus.SUCCESS - def __repr__(self) -> str: - return "HTTP {} {}".format(self.status_code, self.content) \ No newline at end of file + def __str__(self) -> str: + return f"Response(status_code={self.status_code}, text={self.text}, headers={self.headers}, ok={self.ok})" + + +class ApiResponse(object): + def __init__( + self, + status_code, + model, + headers + ): + self.status_code = status_code + self.model = model + self.headers = headers + + def __str__(self) -> str: + return f"ApiResponse(status_code={self.status_code}, model={self.model}, headers={self.headers})" From 9614eae6f42e1c5e647bcbba66367fbff46fcff8 Mon Sep 17 00:00:00 2001 From: sbansla Date: Sat, 28 Sep 2024 14:19:14 +0530 Subject: [PATCH 04/12] chore: fixed serialiser for enums, added url builder --- sendgrid/__init__.py | 3 +- sendgrid/base/__init__.py | 4 -- sendgrid/base/auth_strategy.py | 10 ++-- sendgrid/base/client_base.py | 6 +-- sendgrid/base/url_builder.py | 16 +++++++ sendgrid/client.py | 39 ++++++++-------- sendgrid/converters/serialize.py | 23 +++++++--- sendgrid/http/__init__.py | 4 -- sendgrid/http/http_client.py | 78 ++++++++++++++++---------------- sendgrid/http/request.py | 28 ++++++------ sendgrid/http/response.py | 15 ++---- sendgrid/utility/__init__.py | 1 - 12 files changed, 118 insertions(+), 109 deletions(-) create mode 100644 sendgrid/base/url_builder.py diff --git a/sendgrid/__init__.py b/sendgrid/__init__.py index cd994dd2f..37413b584 100644 --- a/sendgrid/__init__.py +++ b/sendgrid/__init__.py @@ -18,7 +18,6 @@ from .helpers.endpoints import * # noqa from .helpers.mail import * # noqa from .helpers.stats import * # noqa -from .helpers.eventwebhook import * # noqa +from .helpers.eventwebhook import * # noqa from .sendgrid import SendGridAPIClient # noqa from .twilio_email import TwilioEmailAPIClient # noqa -from .version import __version__ diff --git a/sendgrid/base/__init__.py b/sendgrid/base/__init__.py index 5a4589bbb..143f486c0 100644 --- a/sendgrid/base/__init__.py +++ b/sendgrid/base/__init__.py @@ -1,5 +1 @@ -import os -import platform - # __init__.py - diff --git a/sendgrid/base/auth_strategy.py b/sendgrid/base/auth_strategy.py index 1a443b78d..bf23ebdbe 100644 --- a/sendgrid/base/auth_strategy.py +++ b/sendgrid/base/auth_strategy.py @@ -1,14 +1,12 @@ - - - # Handle different of authentications, Currently sendgrid authenticate using apikey. +# 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.") \ No newline at end of file +# print(f"Authenticating {api_key} using Token Authentication.") diff --git a/sendgrid/base/client_base.py b/sendgrid/base/client_base.py index 75f454495..387fba9ba 100644 --- a/sendgrid/base/client_base.py +++ b/sendgrid/base/client_base.py @@ -1,9 +1,7 @@ class ClientBase: def __init__(self): - print('Creating ClientBase class') + print("Creating ClientBase class") def request(self): - print('Making request') - - \ No newline at end of file + print("Making request") diff --git a/sendgrid/base/url_builder.py b/sendgrid/base/url_builder.py new file mode 100644 index 000000000..49f682a63 --- /dev/null +++ b/sendgrid/base/url_builder.py @@ -0,0 +1,16 @@ +def build_url(url: str, region: str) -> str: + base_url = "https://api.sendgrid.com" + + if region and isinstance(region, str): + new_url = f"https://api.{region}.sendgrid.com" + else: + new_url = base_url + + # Ensure that there's a '/' before appending the url + if not new_url.endswith('/'): + new_url += '/' + + new_url += url.lstrip('/') + + return new_url + diff --git a/sendgrid/client.py b/sendgrid/client.py index b0885f6ae..a04ec8894 100644 --- a/sendgrid/client.py +++ b/sendgrid/client.py @@ -1,37 +1,33 @@ -import os -import platform -from typing import Dict, List, MutableMapping, Optional, Tuple -from urllib.parse import urlparse, urlunparse -import json +from typing import List, Optional from sendgrid.http.http_client import SendgridHttpClient, HttpClient from sendgrid.http.request import Request - - +from sendgrid.base.url_builder import build_url # 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: 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 @@ -40,7 +36,12 @@ def __init__( self.http_client: SendgridHttpClient = SendgridHttpClient() def send(self, request: Request): + url = build_url(request.url, self.region) response = self.http_client.request( - method='POST', url=request.url, data=request.data, headers=request.headers, api_key=self.api_key + method=request.method, + url=url, + data=request.data, + headers=request.headers, + api_key=self.api_key, ) - return response + return response \ No newline at end of file diff --git a/sendgrid/converters/serialize.py b/sendgrid/converters/serialize.py index 7a5a808b2..57e360693 100644 --- a/sendgrid/converters/serialize.py +++ b/sendgrid/converters/serialize.py @@ -5,14 +5,19 @@ 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 + 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 { + 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 + return obj.value else: return obj @@ -23,12 +28,16 @@ def from_serializable(data, cls=None): 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 + 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 + return { + key: from_serializable(value) for key, value in data.items() + } # Recursively handle dicts else: return data # Return primitive types as is diff --git a/sendgrid/http/__init__.py b/sendgrid/http/__init__.py index 504eb219a..8b1378917 100644 --- a/sendgrid/http/__init__.py +++ b/sendgrid/http/__init__.py @@ -1,5 +1 @@ -from logging import Logger -from typing import Any, Dict, Optional, Tuple -from urllib.parse import urlencode - diff --git a/sendgrid/http/http_client.py b/sendgrid/http/http_client.py index af2e7167f..5e802fcae 100644 --- a/sendgrid/http/http_client.py +++ b/sendgrid/http/http_client.py @@ -30,15 +30,15 @@ def __init__(self, logger: Logger, is_async: bool, timeout: Optional[float] = No """ 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, + 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. @@ -84,15 +84,15 @@ class AsyncHttpClient(HttpClient): """ 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, + 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. @@ -106,13 +106,13 @@ class SendgridHttpClient(HttpClient): """ 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, + 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 @@ -136,20 +136,20 @@ def __init__( 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, + 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 api_key: :param method: The HTTP method to use :param url: The URL to request :param params: Query parameters to append to the URL @@ -167,7 +167,9 @@ def request( raise ValueError(timeout) headers["Authorization"] = f"Bearer {api_key}" - #auth.authenticate() + # Currently supporting 'application/json' content type + headers["Content-Type"] = "application/json" + # auth.authenticate() kwargs = { "method": method.upper(), "url": url, @@ -185,7 +187,7 @@ def request( 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( @@ -196,7 +198,7 @@ def request( prepped_request, allow_redirects=allow_redirects, timeout=timeout, - **settings + **settings, ) print(response) print(response.status_code) diff --git a/sendgrid/http/request.py b/sendgrid/http/request.py index 7279243bf..d157d2ef7 100644 --- a/sendgrid/http/request.py +++ b/sendgrid/http/request.py @@ -9,14 +9,14 @@ class Match(Enum): class Request(object): def __init__( - self, - method: Union[str, Match] = Match.ANY, - url: Union[str, Match] = Match.ANY, - auth: Union[Tuple[str, str], Match] = Match.ANY, - params: Union[Dict[str, str], Match] = Match.ANY, - data: Union[Dict[str, str], Match] = Match.ANY, - headers: Union[Dict[str, str], Match] = Match.ANY, - **kwargs: Any + self, + method: Union[str, Match] = Match.ANY, + url: Union[str, Match] = Match.ANY, + auth: Union[Tuple[str, str], Match] = Match.ANY, + params: Union[Dict[str, str], Match] = Match.ANY, + data: Union[Dict[str, str], Match] = Match.ANY, + headers: Union[Dict[str, str], Match] = Match.ANY, + **kwargs: Any ): self.method = method if method and method is not Match.ANY: @@ -43,12 +43,12 @@ def __eq__(self, other) -> bool: return False return ( - self.attribute_equal(self.method, other.method) - and self.attribute_equal(self.url, other.url) - and self.attribute_equal(self.auth, other.auth) - and self.attribute_equal(self.params, other.params) - and self.attribute_equal(self.data, other.data) - and self.attribute_equal(self.headers, other.headers) + self.attribute_equal(self.method, other.method) + and self.attribute_equal(self.url, other.url) + and self.attribute_equal(self.auth, other.auth) + and self.attribute_equal(self.params, other.params) + and self.attribute_equal(self.data, other.data) + and self.attribute_equal(self.headers, other.headers) ) def __str__(self) -> str: diff --git a/sendgrid/http/response.py b/sendgrid/http/response.py index 8c947516c..5cb61a1d0 100644 --- a/sendgrid/http/response.py +++ b/sendgrid/http/response.py @@ -9,10 +9,10 @@ class HTTPStatus: class Response(object): def __init__( - self, - status_code: int, - text: str, - headers: Optional[Any] = None, + self, + status_code: int, + text: str, + headers: Optional[Any] = None, ): self.content = text self.headers = headers @@ -32,12 +32,7 @@ def __str__(self) -> str: class ApiResponse(object): - def __init__( - self, - status_code, - model, - headers - ): + def __init__(self, status_code, model, headers): self.status_code = status_code self.model = model self.headers = headers diff --git a/sendgrid/utility/__init__.py b/sendgrid/utility/__init__.py index b423c836a..638e153cf 100644 --- a/sendgrid/utility/__init__.py +++ b/sendgrid/utility/__init__.py @@ -10,4 +10,3 @@ def parse_response(self, response: Response) -> Any: raise self.exception(response, "Unable to create record") return json.loads(response.text) - From 5bfc7ac7119c459ce6f72be4f99349c0f0abd572 Mon Sep 17 00:00:00 2001 From: sbansla Date: Sun, 29 Sep 2024 11:54:19 +0530 Subject: [PATCH 05/12] chore: reverted init change, making similar to main branch --- sendgrid/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sendgrid/__init__.py b/sendgrid/__init__.py index 37413b584..cd994dd2f 100644 --- a/sendgrid/__init__.py +++ b/sendgrid/__init__.py @@ -18,6 +18,7 @@ from .helpers.endpoints import * # noqa from .helpers.mail import * # noqa from .helpers.stats import * # noqa -from .helpers.eventwebhook import * # noqa +from .helpers.eventwebhook import * # noqa from .sendgrid import SendGridAPIClient # noqa from .twilio_email import TwilioEmailAPIClient # noqa +from .version import __version__ From fc6d5074d9b62de074495c69a62d696ba3cf5b4d Mon Sep 17 00:00:00 2001 From: sbansla Date: Thu, 3 Oct 2024 10:06:27 +0530 Subject: [PATCH 06/12] chore: ignore openapi-generator hidden files --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 504640243..c7cb684fc 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,4 @@ example.pdf TODO.txt twilio.env prism* +**/.openapi-generator* \ No newline at end of file From 440b22f3151673b988e5551be384aae60eb11081 Mon Sep 17 00:00:00 2001 From: sbansla Date: Thu, 3 Oct 2024 10:37:44 +0530 Subject: [PATCH 07/12] chore: updated dependencies --- requirements.txt | 2 +- setup.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c2742ac83..ee44fb825 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -Flask==1.1.2 +#Flask==1.1.2 requests>=2.31.0 aiohttp>=3.9.4 aiohttp-retry>=2.8.3 diff --git a/setup.py b/setup.py index 41f11e589..cf8d33a6e 100644 --- a/setup.py +++ b/setup.py @@ -9,6 +9,9 @@ def getRequires(): deps = [ + 'requests'>='2.31.0', + 'aiohttp'>='3.9.4', + 'aiohttp-retry'>='2.8.3', 'python_http_client>=3.2.1', 'starkbank-ecdsa>=2.0.1' ] From c8c2d815982dc8565ecbda72fcd882da1f069bd6 Mon Sep 17 00:00:00 2001 From: sbansla Date: Thu, 3 Oct 2024 10:40:37 +0530 Subject: [PATCH 08/12] fixed setup deps --- setup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index cf8d33a6e..44eab073b 100644 --- a/setup.py +++ b/setup.py @@ -9,9 +9,9 @@ def getRequires(): deps = [ - 'requests'>='2.31.0', - 'aiohttp'>='3.9.4', - 'aiohttp-retry'>='2.8.3', + 'requests>=2.31.0', + 'aiohttp>=3.9.4', + 'aiohttp-retry>=2.8.3', 'python_http_client>=3.2.1', 'starkbank-ecdsa>=2.0.1' ] From 8e0e54c488a201b0d068f00893aed38a147c3153 Mon Sep 17 00:00:00 2001 From: sbansla Date: Thu, 3 Oct 2024 10:45:41 +0530 Subject: [PATCH 09/12] removed unused deps --- requirements.txt | 4 ++-- setup.py | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index ee44fb825..a923d3927 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ #Flask==1.1.2 requests>=2.31.0 -aiohttp>=3.9.4 -aiohttp-retry>=2.8.3 +#aiohttp>=3.9.4 +#aiohttp-retry>=2.8.3 PyYAML>=4.2b1 python-http-client>=3.2.1 diff --git a/setup.py b/setup.py index 44eab073b..721a71b6c 100644 --- a/setup.py +++ b/setup.py @@ -10,8 +10,6 @@ def getRequires(): deps = [ 'requests>=2.31.0', - 'aiohttp>=3.9.4', - 'aiohttp-retry>=2.8.3', 'python_http_client>=3.2.1', 'starkbank-ecdsa>=2.0.1' ] From e6c29c4ed014ed58ecb2265b2848ccf0316729af Mon Sep 17 00:00:00 2001 From: sbansla Date: Thu, 3 Oct 2024 11:33:15 +0530 Subject: [PATCH 10/12] chore: removed unsupported python versions --- .github/workflows/test-and-deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-and-deploy.yml b/.github/workflows/test-and-deploy.yml index 25408ac0a..ac662d576 100644 --- a/.github/workflows/test-and-deploy.yml +++ b/.github/workflows/test-and-deploy.yml @@ -17,7 +17,7 @@ jobs: timeout-minutes: 20 strategy: matrix: - python-version: [ '2.7', '3.5', '3.6', '3.7', '3.8', '3.9', '3.10', '3.11' ] + python-version: ['3.8', '3.9', '3.10', '3.11' ] env: DOCKER_LOGIN: ${{ secrets.DOCKER_USERNAME && secrets.DOCKER_AUTH_TOKEN }} steps: From 55d211004f27effacdc9cf98a083cbe7ba2423fb Mon Sep 17 00:00:00 2001 From: sbansla Date: Thu, 3 Oct 2024 11:40:51 +0530 Subject: [PATCH 11/12] chore: changed license year --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 3154774a9..d703157ed 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (C) 2023, Twilio SendGrid, Inc. +Copyright (C) 2024, Twilio SendGrid, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in From a42cf12ec8cbd140a3b3b10753c4613903a3a121 Mon Sep 17 00:00:00 2001 From: sbansla Date: Thu, 3 Oct 2024 12:12:36 +0530 Subject: [PATCH 12/12] chore: corrected success response --- sendgrid/http/response.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sendgrid/http/response.py b/sendgrid/http/response.py index 5cb61a1d0..80e307d39 100644 --- a/sendgrid/http/response.py +++ b/sendgrid/http/response.py @@ -2,7 +2,7 @@ class HTTPStatus: - SUCCESS = range(200, 300) # Success codes: 200-299 + SUCCESS = range(200, 400) # Success codes: 200-399 CLIENT_ERROR = range(400, 500) # Client error codes: 400-499 SERVER_ERROR = range(500, 600) # Server error codes: 500-599