Initial commit: Email alerts application
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
from logging import Logger
|
||||
from typing import Any, Dict, Optional, Tuple
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from requests import Response
|
||||
|
||||
from twilio.base.exceptions import TwilioException
|
||||
from twilio.http.request import Request as TwilioRequest
|
||||
from twilio.http.response import Response as TwilioResponse
|
||||
|
||||
|
||||
class HttpClient(object):
|
||||
def __init__(self, logger: Logger, is_async: bool, timeout: Optional[float] = None):
|
||||
"""
|
||||
Constructor for the abstract HTTP client
|
||||
|
||||
:param logger
|
||||
:param is_async: Whether the client supports async request calls.
|
||||
:param timeout: Timeout for the requests.
|
||||
Timeout should never be zero (0) or less.
|
||||
"""
|
||||
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[TwilioRequest] = None
|
||||
self._test_only_last_response: Optional[TwilioResponse] = 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,
|
||||
) -> TwilioResponse:
|
||||
"""
|
||||
Make an HTTP request.
|
||||
"""
|
||||
raise TwilioException("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,
|
||||
) -> TwilioResponse:
|
||||
"""
|
||||
Make an asynchronous HTTP request.
|
||||
"""
|
||||
raise TwilioException("AsyncHttpClient is an abstract class")
|
||||
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,135 @@
|
||||
import logging
|
||||
from typing import Dict, Optional, Tuple
|
||||
|
||||
from aiohttp import BasicAuth, ClientSession
|
||||
from aiohttp_retry import ExponentialRetry, RetryClient
|
||||
|
||||
from twilio.http import AsyncHttpClient
|
||||
from twilio.http.request import Request as TwilioRequest
|
||||
from twilio.http.response import Response
|
||||
|
||||
_logger = logging.getLogger("twilio.async_http_client")
|
||||
|
||||
|
||||
class AsyncTwilioHttpClient(AsyncHttpClient):
|
||||
"""
|
||||
General purpose asynchronous HTTP Client for interacting with the Twilio API
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
pool_connections: bool = True,
|
||||
trace_configs=None,
|
||||
timeout: Optional[float] = None,
|
||||
logger: logging.Logger = _logger,
|
||||
proxy_url: Optional[str] = None,
|
||||
max_retries: Optional[int] = None,
|
||||
):
|
||||
"""
|
||||
Constructor for the AsyncTwilioHttpClient
|
||||
|
||||
:param pool_connections: Creates a client session for making requests from.
|
||||
:param trace_configs: Configuration used to trace request lifecycle events. See aiohttp library TraceConfig
|
||||
documentation for more info.
|
||||
:param timeout: Timeout for the requests (seconds)
|
||||
:param logger
|
||||
:param proxy_url: Proxy URL
|
||||
:param max_retries: Maximum number of retries each request should attempt
|
||||
"""
|
||||
super().__init__(logger, True, timeout)
|
||||
self.proxy_url = proxy_url
|
||||
self.trace_configs = trace_configs
|
||||
self.session = (
|
||||
ClientSession(trace_configs=self.trace_configs)
|
||||
if pool_connections
|
||||
else None
|
||||
)
|
||||
if max_retries is not None:
|
||||
retry_options = ExponentialRetry(attempts=max_retries)
|
||||
self.session = RetryClient(
|
||||
client_session=self.session, retry_options=retry_options
|
||||
)
|
||||
|
||||
async def request(
|
||||
self,
|
||||
method: str,
|
||||
url: 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 with parameters provided.
|
||||
|
||||
: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 auth: Basic Auth arguments (username, password entries)
|
||||
:param timeout: Socket/Read timeout for the request. Overrides the timeout if set on the client.
|
||||
:param allow_redirects: Whether or not to allow redirects
|
||||
See the requests documentation for explanation of all these parameters
|
||||
|
||||
:return: An http response
|
||||
"""
|
||||
if timeout is not None and timeout <= 0:
|
||||
raise ValueError(timeout)
|
||||
|
||||
basic_auth = None
|
||||
if auth is not None:
|
||||
basic_auth = BasicAuth(login=auth[0], password=auth[1])
|
||||
|
||||
kwargs = {
|
||||
"method": method.upper(),
|
||||
"url": url,
|
||||
"params": params,
|
||||
"data": data,
|
||||
"headers": headers,
|
||||
"auth": basic_auth,
|
||||
"timeout": timeout,
|
||||
"allow_redirects": allow_redirects,
|
||||
}
|
||||
|
||||
self.log_request(kwargs)
|
||||
self._test_only_last_response = None
|
||||
|
||||
temp = False
|
||||
session = None
|
||||
if self.session:
|
||||
session = self.session
|
||||
else:
|
||||
session = ClientSession()
|
||||
temp = True
|
||||
self._test_only_last_request = TwilioRequest(**kwargs)
|
||||
response = await session.request(**kwargs)
|
||||
self.log_response(response.status, response)
|
||||
self._test_only_last_response = Response(
|
||||
response.status, await response.text(), response.headers
|
||||
)
|
||||
if temp:
|
||||
await session.close()
|
||||
return self._test_only_last_response
|
||||
|
||||
async def close(self):
|
||||
"""
|
||||
Closes the HTTP client session
|
||||
"""
|
||||
if self.session:
|
||||
await self.session.close()
|
||||
|
||||
async def __aenter__(self):
|
||||
"""
|
||||
Async context manager setup
|
||||
"""
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *excinfo):
|
||||
"""
|
||||
Async context manager exit
|
||||
"""
|
||||
if self.session:
|
||||
await self.session.close()
|
||||
@@ -0,0 +1,114 @@
|
||||
import logging
|
||||
from typing import Dict, Optional, Tuple
|
||||
|
||||
from requests import Request, Session, hooks
|
||||
from requests.adapters import HTTPAdapter
|
||||
|
||||
from twilio.http import HttpClient
|
||||
from twilio.http.request import Request as TwilioRequest
|
||||
from twilio.http.response import Response
|
||||
|
||||
_logger = logging.getLogger("twilio.http_client")
|
||||
|
||||
|
||||
class TwilioHttpClient(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))
|
||||
|
||||
self.request_hooks = request_hooks or hooks.default_hooks()
|
||||
self.proxy = proxy if proxy else {}
|
||||
|
||||
def request(
|
||||
self,
|
||||
method: str,
|
||||
url: 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 with parameters provided.
|
||||
|
||||
: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 auth: Basic Auth arguments
|
||||
:param timeout: Socket/Read timeout for the request
|
||||
:param allow_redirects: Whether or not 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)
|
||||
|
||||
kwargs = {
|
||||
"method": method.upper(),
|
||||
"url": url,
|
||||
"params": params,
|
||||
"data": data,
|
||||
"headers": headers,
|
||||
"auth": auth,
|
||||
"hooks": self.request_hooks,
|
||||
}
|
||||
|
||||
self.log_request(kwargs)
|
||||
|
||||
self._test_only_last_response = None
|
||||
session = self.session or Session()
|
||||
request = Request(**kwargs)
|
||||
self._test_only_last_request = TwilioRequest(**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
|
||||
)
|
||||
|
||||
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
|
||||
@@ -0,0 +1,91 @@
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, Tuple, Union
|
||||
from urllib.parse import urlencode
|
||||
|
||||
|
||||
class Match(Enum):
|
||||
ANY = "*"
|
||||
|
||||
|
||||
class Request(object):
|
||||
"""
|
||||
An HTTP request.
|
||||
"""
|
||||
|
||||
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)
|
||||
@@ -0,0 +1,22 @@
|
||||
from typing import Any, Optional
|
||||
|
||||
|
||||
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 __repr__(self) -> str:
|
||||
return "HTTP {} {}".format(self.status_code, self.content)
|
||||
@@ -0,0 +1,138 @@
|
||||
from collections import namedtuple
|
||||
|
||||
from requests import Request, Session
|
||||
|
||||
from twilio.base.exceptions import TwilioRestException
|
||||
from urllib.parse import urlparse
|
||||
from twilio.http import HttpClient
|
||||
from twilio.http.response import Response
|
||||
from twilio.jwt.validation import ClientValidationJwt
|
||||
|
||||
|
||||
ValidationPayload = namedtuple(
|
||||
"ValidationPayload",
|
||||
["method", "path", "query_string", "all_headers", "signed_headers", "body"],
|
||||
)
|
||||
|
||||
|
||||
class ValidationClient(HttpClient):
|
||||
__SIGNED_HEADERS = ["authorization", "host"]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
account_sid,
|
||||
api_key_sid,
|
||||
credential_sid,
|
||||
private_key,
|
||||
pool_connections=True,
|
||||
):
|
||||
"""
|
||||
Build a ValidationClient which signs requests with private_key and allows Twilio to
|
||||
validate request has not been tampered with.
|
||||
|
||||
:param str account_sid: A Twilio Account Sid starting with 'AC'
|
||||
:param str api_key_sid: A Twilio API Key Sid starting with 'SK'
|
||||
:param str credential_sid: A Credential Sid starting with 'CR',
|
||||
corresponds to public key Twilio will use to verify the JWT.
|
||||
:param str private_key: The private key used to sign the Client Validation JWT.
|
||||
"""
|
||||
self.account_sid = account_sid
|
||||
self.credential_sid = credential_sid
|
||||
self.api_key_sid = api_key_sid
|
||||
self.private_key = private_key
|
||||
self.session = Session() if pool_connections else None
|
||||
|
||||
def request(
|
||||
self,
|
||||
method,
|
||||
url,
|
||||
params=None,
|
||||
data=None,
|
||||
headers=None,
|
||||
auth=None,
|
||||
timeout=None,
|
||||
allow_redirects=False,
|
||||
):
|
||||
"""
|
||||
Make a signed HTTP Request
|
||||
|
||||
:param str method: The HTTP method to use
|
||||
:param str url: The URL to request
|
||||
:param dict params: Query parameters to append to the URL
|
||||
:param dict data: Parameters to go in the body of the HTTP request
|
||||
:param dict headers: HTTP Headers to send with the request
|
||||
:param tuple auth: Basic Auth arguments
|
||||
:param float timeout: Socket/Read timeout for the request
|
||||
:param boolean allow_redirects: Whether or not to allow redirects
|
||||
See the requests documentation for explanation of all these parameters
|
||||
|
||||
:return: An http response
|
||||
:rtype: A :class:`Response <twilio.rest.http.response.Response>` object
|
||||
"""
|
||||
session = self.session or Session()
|
||||
request = Request(
|
||||
method.upper(), url, params=params, data=data, headers=headers, auth=auth
|
||||
)
|
||||
prepared_request = session.prepare_request(request)
|
||||
|
||||
if (
|
||||
"Host" not in prepared_request.headers
|
||||
and "host" not in prepared_request.headers
|
||||
):
|
||||
prepared_request.headers["Host"] = self._get_host(prepared_request)
|
||||
|
||||
validation_payload = self._build_validation_payload(prepared_request)
|
||||
jwt = ClientValidationJwt(
|
||||
self.account_sid,
|
||||
self.api_key_sid,
|
||||
self.credential_sid,
|
||||
self.private_key,
|
||||
validation_payload,
|
||||
)
|
||||
prepared_request.headers["Twilio-Client-Validation"] = jwt.to_jwt()
|
||||
|
||||
response = session.send(
|
||||
prepared_request,
|
||||
allow_redirects=allow_redirects,
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
return Response(int(response.status_code), response.text)
|
||||
|
||||
def _build_validation_payload(self, request):
|
||||
"""
|
||||
Extract relevant information from request to build a ClientValidationJWT
|
||||
:param PreparedRequest request: request we will extract information from.
|
||||
:return: ValidationPayload
|
||||
"""
|
||||
parsed = urlparse(request.url)
|
||||
path = parsed.path
|
||||
query_string = parsed.query or ""
|
||||
|
||||
return ValidationPayload(
|
||||
method=request.method,
|
||||
path=path,
|
||||
query_string=query_string,
|
||||
all_headers=request.headers,
|
||||
signed_headers=ValidationClient.__SIGNED_HEADERS,
|
||||
body=request.body or "",
|
||||
)
|
||||
|
||||
def _get_host(self, request):
|
||||
"""Pull the Host out of the request"""
|
||||
parsed = urlparse(request.url)
|
||||
return str(parsed.netloc)
|
||||
|
||||
def validate_ssl_certificate(self, client):
|
||||
"""
|
||||
Validate that a request to the new SSL certificate is successful
|
||||
:return: null on success, raise TwilioRestException if the request fails
|
||||
"""
|
||||
response = client.request("GET", "https://tls-test.twilio.com:443")
|
||||
|
||||
if response.status_code < 200 or response.status_code >= 300:
|
||||
raise TwilioRestException(
|
||||
response.status_code,
|
||||
"https://tls-test.twilio.com:443",
|
||||
"Failed to validate SSL certificate",
|
||||
)
|
||||
Reference in New Issue
Block a user