Initial commit: Email alerts application

This commit is contained in:
Iyeoluwa Akinrinola
2025-07-25 11:31:36 +01:00
commit adfb625ae9
6322 changed files with 2882826 additions and 0 deletions
@@ -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")
@@ -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",
)