Initial commit: Email alerts application
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,234 @@
|
||||
import os
|
||||
import platform
|
||||
from typing import Dict, List, MutableMapping, Optional, Tuple
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
|
||||
from twilio import __version__
|
||||
from twilio.base.exceptions import TwilioException
|
||||
from twilio.http import HttpClient
|
||||
from twilio.http.http_client import TwilioHttpClient
|
||||
from twilio.http.response import Response
|
||||
|
||||
|
||||
class ClientBase(object):
|
||||
"""A client for accessing the Twilio API."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
username: Optional[str] = None,
|
||||
password: Optional[str] = None,
|
||||
account_sid: Optional[str] = None,
|
||||
region: Optional[str] = None,
|
||||
http_client: Optional[HttpClient] = None,
|
||||
environment: Optional[MutableMapping[str, str]] = None,
|
||||
edge: Optional[str] = None,
|
||||
user_agent_extensions: Optional[List[str]] = None,
|
||||
):
|
||||
"""
|
||||
Initializes the Twilio Client
|
||||
|
||||
:param username: Username to authenticate with
|
||||
:param password: Password to authenticate with
|
||||
:param account_sid: Account SID, defaults to Username
|
||||
:param region: Twilio Region to make requests to, defaults to 'us1' if an edge is provided
|
||||
:param http_client: HttpClient, defaults to TwilioHttpClient
|
||||
:param environment: Environment to look for auth details, defaults to os.environ
|
||||
:param edge: Twilio Edge to make requests to, defaults to None
|
||||
:param user_agent_extensions: Additions to the user agent string
|
||||
"""
|
||||
environment = environment or os.environ
|
||||
|
||||
self.username = username or environment.get("TWILIO_ACCOUNT_SID")
|
||||
""" :type : str """
|
||||
self.password = password or environment.get("TWILIO_AUTH_TOKEN")
|
||||
""" :type : str """
|
||||
self.edge = edge or environment.get("TWILIO_EDGE")
|
||||
""" :type : str """
|
||||
self.region = region or environment.get("TWILIO_REGION")
|
||||
""" :type : str """
|
||||
self.user_agent_extensions = user_agent_extensions or []
|
||||
""" :type : list[str] """
|
||||
|
||||
if not self.username or not self.password:
|
||||
raise TwilioException("Credentials are required to create a TwilioClient")
|
||||
|
||||
self.account_sid = account_sid or self.username
|
||||
""" :type : str """
|
||||
self.auth = (self.username, self.password)
|
||||
""" :type : tuple(str, str) """
|
||||
self.http_client: HttpClient = http_client or TwilioHttpClient()
|
||||
""" :type : HttpClient """
|
||||
|
||||
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:
|
||||
"""
|
||||
Makes a request to the Twilio API using the configured http client
|
||||
Authentication information is automatically added if none is provided
|
||||
|
||||
:param method: HTTP Method
|
||||
:param uri: Fully qualified url
|
||||
:param params: Query string parameters
|
||||
:param data: POST body data
|
||||
:param headers: HTTP Headers
|
||||
:param auth: Authentication
|
||||
:param timeout: Timeout in seconds
|
||||
:param allow_redirects: Should the client follow redirects
|
||||
|
||||
:returns: Response from the Twilio API
|
||||
"""
|
||||
auth = self.get_auth(auth)
|
||||
headers = self.get_headers(method, headers)
|
||||
uri = self.get_hostname(uri)
|
||||
|
||||
return self.http_client.request(
|
||||
method,
|
||||
uri,
|
||||
params=params,
|
||||
data=data,
|
||||
headers=headers,
|
||||
auth=auth,
|
||||
timeout=timeout,
|
||||
allow_redirects=allow_redirects,
|
||||
)
|
||||
|
||||
async def request_async(
|
||||
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:
|
||||
"""
|
||||
Asynchronously makes a request to the Twilio API using the configured http client
|
||||
The configured http client must be an asynchronous http client
|
||||
Authentication information is automatically added if none is provided
|
||||
|
||||
:param method: HTTP Method
|
||||
:param uri: Fully qualified url
|
||||
:param params: Query string parameters
|
||||
:param data: POST body data
|
||||
:param headers: HTTP Headers
|
||||
:param auth: Authentication
|
||||
:param timeout: Timeout in seconds
|
||||
:param allow_redirects: Should the client follow redirects
|
||||
|
||||
:returns: Response from the Twilio API
|
||||
"""
|
||||
if not self.http_client.is_async:
|
||||
raise RuntimeError(
|
||||
"http_client must be asynchronous to support async API requests"
|
||||
)
|
||||
|
||||
auth = self.get_auth(auth)
|
||||
headers = self.get_headers(method, headers)
|
||||
uri = self.get_hostname(uri)
|
||||
|
||||
return await self.http_client.request(
|
||||
method,
|
||||
uri,
|
||||
params=params,
|
||||
data=data,
|
||||
headers=headers,
|
||||
auth=auth,
|
||||
timeout=timeout,
|
||||
allow_redirects=allow_redirects,
|
||||
)
|
||||
|
||||
def get_auth(self, auth: Optional[Tuple[str, str]]) -> Tuple[str, str]:
|
||||
"""
|
||||
Get the request authentication object
|
||||
:param auth: Authentication (username, password)
|
||||
:returns: The authentication object
|
||||
"""
|
||||
return auth or self.auth
|
||||
|
||||
def get_headers(
|
||||
self, method: str, headers: Optional[Dict[str, str]]
|
||||
) -> Dict[str, str]:
|
||||
"""
|
||||
Get the request headers including user-agent, extensions, encoding, content-type, MIME type
|
||||
:param method: HTTP method
|
||||
:param headers: HTTP headers
|
||||
:returns: HTTP headers
|
||||
"""
|
||||
headers = headers or {}
|
||||
|
||||
# Set User-Agent
|
||||
pkg_version = __version__
|
||||
os_name = platform.system()
|
||||
os_arch = platform.machine()
|
||||
python_version = platform.python_version()
|
||||
headers["User-Agent"] = "twilio-python/{} ({} {}) Python/{}".format(
|
||||
pkg_version,
|
||||
os_name,
|
||||
os_arch,
|
||||
python_version,
|
||||
)
|
||||
# Extensions
|
||||
for extension in self.user_agent_extensions:
|
||||
headers["User-Agent"] += " {}".format(extension)
|
||||
headers["X-Twilio-Client"] = "python-{}".format(__version__)
|
||||
|
||||
# Types, encodings, etc.
|
||||
headers["Accept-Charset"] = "utf-8"
|
||||
if method == "POST" and "Content-Type" not in headers:
|
||||
headers["Content-Type"] = "application/x-www-form-urlencoded"
|
||||
if "Accept" not in headers:
|
||||
headers["Accept"] = "application/json"
|
||||
|
||||
return headers
|
||||
|
||||
def get_hostname(self, uri: str) -> str:
|
||||
"""
|
||||
Determines the proper hostname given edge and region preferences
|
||||
via client configuration or uri.
|
||||
|
||||
:param uri: Fully qualified url
|
||||
|
||||
:returns: The final uri used to make the request
|
||||
"""
|
||||
if not self.edge and not self.region:
|
||||
return uri
|
||||
|
||||
parsed_url = urlparse(uri)
|
||||
pieces = parsed_url.netloc.split(".")
|
||||
prefix = pieces[0]
|
||||
suffix = ".".join(pieces[-2:])
|
||||
region = None
|
||||
edge = None
|
||||
if len(pieces) == 4:
|
||||
# product.region.twilio.com
|
||||
region = pieces[1]
|
||||
elif len(pieces) == 5:
|
||||
# product.edge.region.twilio.com
|
||||
edge = pieces[1]
|
||||
region = pieces[2]
|
||||
|
||||
edge = self.edge or edge
|
||||
region = self.region or region or (edge and "us1")
|
||||
|
||||
parsed_url = parsed_url._replace(
|
||||
netloc=".".join([part for part in [prefix, edge, region, suffix] if part])
|
||||
)
|
||||
return str(urlunparse(parsed_url))
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""
|
||||
Provide a friendly representation
|
||||
|
||||
:returns: Machine friendly representation
|
||||
"""
|
||||
return "<Twilio {}>".format(self.account_sid)
|
||||
@@ -0,0 +1,75 @@
|
||||
import datetime
|
||||
from decimal import BasicContext, Decimal
|
||||
from email.utils import parsedate
|
||||
from typing import Optional, Union
|
||||
|
||||
ISO8601_DATE_FORMAT = "%Y-%m-%d"
|
||||
ISO8601_DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
|
||||
|
||||
|
||||
def iso8601_date(s: str) -> Union[datetime.date, str]:
|
||||
"""
|
||||
Parses an ISO 8601 date string and returns a UTC date object or the string
|
||||
if the parsing failed.
|
||||
:param s: ISO 8601-formatted date string (2015-01-25)
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
return (
|
||||
datetime.datetime.strptime(s, ISO8601_DATE_FORMAT)
|
||||
.replace(tzinfo=datetime.timezone.utc)
|
||||
.date()
|
||||
)
|
||||
except (TypeError, ValueError):
|
||||
return s
|
||||
|
||||
|
||||
def iso8601_datetime(
|
||||
s: str,
|
||||
) -> Union[datetime.datetime, str]:
|
||||
"""
|
||||
Parses an ISO 8601 datetime string and returns a UTC datetime object,
|
||||
or the string if parsing failed.
|
||||
:param s: ISO 8601-formatted datetime string (2015-01-25T12:34:56Z)
|
||||
"""
|
||||
try:
|
||||
return datetime.datetime.strptime(s, ISO8601_DATETIME_FORMAT).replace(
|
||||
tzinfo=datetime.timezone.utc
|
||||
)
|
||||
except (TypeError, ValueError):
|
||||
return s
|
||||
|
||||
|
||||
def rfc2822_datetime(s: str) -> Optional[datetime.datetime]:
|
||||
"""
|
||||
Parses an RFC 2822 date string and returns a UTC datetime object,
|
||||
or the string if parsing failed.
|
||||
:param s: RFC 2822-formatted string date
|
||||
:return: datetime or str
|
||||
"""
|
||||
date_tuple = parsedate(s)
|
||||
if date_tuple is None:
|
||||
return None
|
||||
return datetime.datetime(*date_tuple[:6]).replace(tzinfo=datetime.timezone.utc)
|
||||
|
||||
|
||||
def decimal(d: Optional[str]) -> Union[Decimal, str]:
|
||||
"""
|
||||
Parses a decimal string into a Decimal
|
||||
:param d: decimal string
|
||||
"""
|
||||
if not d:
|
||||
return d
|
||||
return Decimal(d, BasicContext)
|
||||
|
||||
|
||||
def integer(i: str) -> Union[int, str]:
|
||||
"""
|
||||
Parses an integer string into an int
|
||||
:param i: integer string
|
||||
:return: int
|
||||
"""
|
||||
try:
|
||||
return int(i)
|
||||
except (TypeError, ValueError):
|
||||
return i
|
||||
@@ -0,0 +1,93 @@
|
||||
from typing import Dict, Optional, Tuple
|
||||
from twilio.http.response import Response
|
||||
from twilio.rest import Client
|
||||
|
||||
|
||||
class Domain(object):
|
||||
"""
|
||||
This represents at Twilio API subdomain.
|
||||
|
||||
Like, `api.twilio.com` or `lookups.twilio.com'.
|
||||
"""
|
||||
|
||||
def __init__(self, twilio: Client, base_url: str):
|
||||
self.twilio = twilio
|
||||
self.base_url = base_url
|
||||
|
||||
def absolute_url(self, uri: str) -> str:
|
||||
"""
|
||||
Converts a relative `uri` to an absolute url.
|
||||
:param string uri: The relative uri to make absolute.
|
||||
:return: An absolute url (based off this domain)
|
||||
"""
|
||||
return "{}/{}".format(self.base_url.strip("/"), uri.strip("/"))
|
||||
|
||||
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:
|
||||
"""
|
||||
Makes an HTTP request to this domain.
|
||||
:param method: The HTTP method.
|
||||
:param uri: The HTTP uri.
|
||||
:param params: Query parameters.
|
||||
:param data: The request body.
|
||||
:param headers: The HTTP headers.
|
||||
:param auth: Basic auth tuple of (username, password)
|
||||
:param timeout: The request timeout.
|
||||
:param allow_redirects: True if the client should follow HTTP
|
||||
redirects.
|
||||
"""
|
||||
url = self.absolute_url(uri)
|
||||
return self.twilio.request(
|
||||
method,
|
||||
url,
|
||||
params=params,
|
||||
data=data,
|
||||
headers=headers,
|
||||
auth=auth,
|
||||
timeout=timeout,
|
||||
allow_redirects=allow_redirects,
|
||||
)
|
||||
|
||||
async def request_async(
|
||||
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:
|
||||
"""
|
||||
Makes an asynchronous HTTP request to this domain.
|
||||
:param method: The HTTP method.
|
||||
:param uri: The HTTP uri.
|
||||
:param params: Query parameters.
|
||||
:param data: The request body.
|
||||
:param headers: The HTTP headers.
|
||||
:param auth: Basic auth tuple of (username, password)
|
||||
:param timeout: The request timeout.
|
||||
:param allow_redirects: True if the client should follow HTTP
|
||||
redirects.
|
||||
"""
|
||||
url = self.absolute_url(uri)
|
||||
return await self.twilio.request_async(
|
||||
method,
|
||||
url,
|
||||
params=params,
|
||||
data=data,
|
||||
headers=headers,
|
||||
auth=auth,
|
||||
timeout=timeout,
|
||||
allow_redirects=allow_redirects,
|
||||
)
|
||||
@@ -0,0 +1,82 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import sys
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class TwilioException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class TwilioRestException(TwilioException):
|
||||
"""A generic 400 or 500 level exception from the Twilio API
|
||||
|
||||
:param int status: the HTTP status that was returned for the exception
|
||||
:param str uri: The URI that caused the exception
|
||||
:param str msg: A human-readable message for the error
|
||||
:param int|None code: A Twilio-specific error code for the error. This is
|
||||
not available for all errors.
|
||||
:param method: The HTTP method used to make the request
|
||||
:param details: Additional error details returned for the exception
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
status: int,
|
||||
uri: str,
|
||||
msg: str = "",
|
||||
code: Optional[int] = None,
|
||||
method: str = "GET",
|
||||
details: Optional[object] = None,
|
||||
):
|
||||
self.uri = uri
|
||||
self.status = status
|
||||
self.msg = msg
|
||||
self.code = code
|
||||
self.method = method
|
||||
self.details = details
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Try to pretty-print the exception, if this is going on screen."""
|
||||
|
||||
def red(words: str) -> str:
|
||||
return "\033[31m\033[49m%s\033[0m" % words
|
||||
|
||||
def white(words: str) -> str:
|
||||
return "\033[37m\033[49m%s\033[0m" % words
|
||||
|
||||
def blue(words: str) -> str:
|
||||
return "\033[34m\033[49m%s\033[0m" % words
|
||||
|
||||
def teal(words: str) -> str:
|
||||
return "\033[36m\033[49m%s\033[0m" % words
|
||||
|
||||
def get_uri(code: int) -> str:
|
||||
return "https://www.twilio.com/docs/errors/{0}".format(code)
|
||||
|
||||
# If it makes sense to print a human readable error message, try to
|
||||
# do it. The one problem is that someone might catch this error and
|
||||
# try to display the message from it to an end user.
|
||||
if hasattr(sys.stderr, "isatty") and sys.stderr.isatty():
|
||||
msg = (
|
||||
"\n{red_error} {request_was}\n\n{http_line}"
|
||||
"\n\n{twilio_returned}\n\n{message}\n".format(
|
||||
red_error=red("HTTP Error"),
|
||||
request_was=white("Your request was:"),
|
||||
http_line=teal("%s %s" % (self.method, self.uri)),
|
||||
twilio_returned=white("Twilio returned the following information:"),
|
||||
message=blue(str(self.msg)),
|
||||
)
|
||||
)
|
||||
if self.code:
|
||||
msg = "".join(
|
||||
[
|
||||
msg,
|
||||
"\n{more_info}\n\n{uri}\n\n".format(
|
||||
more_info=white("More information may be available here:"),
|
||||
uri=blue(get_uri(self.code)),
|
||||
),
|
||||
]
|
||||
)
|
||||
return msg
|
||||
else:
|
||||
return "HTTP {0} error: {1}".format(self.status, self.msg)
|
||||
@@ -0,0 +1,6 @@
|
||||
from twilio.base.version import Version
|
||||
|
||||
|
||||
class InstanceContext(object):
|
||||
def __init__(self, version: Version):
|
||||
self._version = version
|
||||
@@ -0,0 +1,6 @@
|
||||
from twilio.base.version import Version
|
||||
|
||||
|
||||
class InstanceResource(object):
|
||||
def __init__(self, version: Version):
|
||||
self._version = version
|
||||
@@ -0,0 +1,6 @@
|
||||
from twilio.base.version import Version
|
||||
|
||||
|
||||
class ListResource(object):
|
||||
def __init__(self, version: Version):
|
||||
self._version = version
|
||||
@@ -0,0 +1,47 @@
|
||||
import warnings
|
||||
import functools
|
||||
|
||||
|
||||
class ObsoleteException(Exception):
|
||||
"""Base class for warnings about obsolete features."""
|
||||
|
||||
|
||||
def obsolete_client(func):
|
||||
"""This is a decorator which can be used to mark Client classes as
|
||||
obsolete. It will result in an error being emitted when the class is
|
||||
instantiated."""
|
||||
|
||||
@functools.wraps(func)
|
||||
def new_func(*args, **kwargs):
|
||||
raise ObsoleteException(
|
||||
"{} has been removed from this version of the library. "
|
||||
"Please refer to current documentation for guidance.".format(func.__name__)
|
||||
)
|
||||
|
||||
return new_func
|
||||
|
||||
|
||||
def deprecated_method(new_func=None):
|
||||
"""
|
||||
This is a decorator which can be used to mark deprecated methods.
|
||||
It will report in a DeprecationWarning being emitted to stderr when the deprecated method is used.
|
||||
"""
|
||||
|
||||
def deprecated_method_wrapper(func):
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
msg = "Function method .{}() is deprecated".format(func.__name__)
|
||||
msg += (
|
||||
" in favor of .{}()".format(new_func)
|
||||
if isinstance(new_func, str)
|
||||
else ""
|
||||
)
|
||||
warnings.warn(msg, DeprecationWarning)
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
if callable(new_func):
|
||||
return deprecated_method_wrapper(new_func)
|
||||
|
||||
return deprecated_method_wrapper
|
||||
@@ -0,0 +1,171 @@
|
||||
import json
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from twilio.base.exceptions import TwilioException
|
||||
from twilio.http.response import Response
|
||||
|
||||
|
||||
class Page(object):
|
||||
"""
|
||||
Represents a page of records in a collection.
|
||||
|
||||
A `Page` lets you iterate over its records and fetch the next and previous
|
||||
pages in the collection.
|
||||
"""
|
||||
|
||||
META_KEYS = {
|
||||
"end",
|
||||
"first_page_uri",
|
||||
"next_page_uri",
|
||||
"last_page_uri",
|
||||
"page",
|
||||
"page_size",
|
||||
"previous_page_uri",
|
||||
"total",
|
||||
"num_pages",
|
||||
"start",
|
||||
"uri",
|
||||
}
|
||||
|
||||
def __init__(self, version, response: Response, solution={}):
|
||||
payload = self.process_response(response)
|
||||
|
||||
self._version = version
|
||||
self._payload = payload
|
||||
self._solution = solution
|
||||
self._records = iter(self.load_page(payload))
|
||||
|
||||
def __iter__(self):
|
||||
"""
|
||||
A `Page` is a valid iterator.
|
||||
"""
|
||||
return self
|
||||
|
||||
def __next__(self):
|
||||
return self.next()
|
||||
|
||||
def next(self):
|
||||
"""
|
||||
Returns the next record in the `Page`.
|
||||
"""
|
||||
return self.get_instance(next(self._records))
|
||||
|
||||
@classmethod
|
||||
def process_response(cls, response: Response) -> Any:
|
||||
"""
|
||||
Load a JSON response.
|
||||
|
||||
:param response: The HTTP response.
|
||||
:return The JSON-loaded content.
|
||||
"""
|
||||
if response.status_code != 200:
|
||||
raise TwilioException("Unable to fetch page", response)
|
||||
|
||||
return json.loads(response.text)
|
||||
|
||||
def load_page(self, payload: Dict[str, Any]):
|
||||
"""
|
||||
Parses the collection of records out of a list payload.
|
||||
|
||||
:param payload: The JSON-loaded content.
|
||||
:return list: The list of records.
|
||||
"""
|
||||
if "meta" in payload and "key" in payload["meta"]:
|
||||
return payload[payload["meta"]["key"]]
|
||||
else:
|
||||
keys = set(payload.keys())
|
||||
key = keys - self.META_KEYS
|
||||
if len(key) == 1:
|
||||
return payload[key.pop()]
|
||||
|
||||
raise TwilioException("Page Records can not be deserialized")
|
||||
|
||||
@property
|
||||
def previous_page_url(self) -> Optional[str]:
|
||||
"""
|
||||
:return str: Returns a link to the previous_page_url or None if doesn't exist.
|
||||
"""
|
||||
if "meta" in self._payload and "previous_page_url" in self._payload["meta"]:
|
||||
return self._payload["meta"]["previous_page_url"]
|
||||
elif (
|
||||
"previous_page_uri" in self._payload and self._payload["previous_page_uri"]
|
||||
):
|
||||
return self._version.domain.absolute_url(self._payload["previous_page_uri"])
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def next_page_url(self) -> Optional[str]:
|
||||
"""
|
||||
:return str: Returns a link to the next_page_url or None if doesn't exist.
|
||||
"""
|
||||
if "meta" in self._payload and "next_page_url" in self._payload["meta"]:
|
||||
return self._payload["meta"]["next_page_url"]
|
||||
elif "next_page_uri" in self._payload and self._payload["next_page_uri"]:
|
||||
return self._version.domain.absolute_url(self._payload["next_page_uri"])
|
||||
|
||||
return None
|
||||
|
||||
def get_instance(self, payload: Dict[str, Any]) -> Any:
|
||||
"""
|
||||
:param dict payload: A JSON-loaded representation of an instance record.
|
||||
:return: A rich, resource-dependent object.
|
||||
"""
|
||||
raise TwilioException(
|
||||
"Page.get_instance() must be implemented in the derived class"
|
||||
)
|
||||
|
||||
def next_page(self) -> Optional["Page"]:
|
||||
"""
|
||||
Return the `Page` after this one.
|
||||
:return The next page.
|
||||
"""
|
||||
if not self.next_page_url:
|
||||
return None
|
||||
|
||||
response = self._version.domain.twilio.request("GET", self.next_page_url)
|
||||
cls = type(self)
|
||||
return cls(self._version, response, self._solution)
|
||||
|
||||
async def next_page_async(self) -> Optional["Page"]:
|
||||
"""
|
||||
Asynchronously return the `Page` after this one.
|
||||
:return The next page.
|
||||
"""
|
||||
if not self.next_page_url:
|
||||
return None
|
||||
|
||||
response = await self._version.domain.twilio.request_async(
|
||||
"GET", self.next_page_url
|
||||
)
|
||||
cls = type(self)
|
||||
return cls(self._version, response, self._solution)
|
||||
|
||||
def previous_page(self) -> Optional["Page"]:
|
||||
"""
|
||||
Return the `Page` before this one.
|
||||
:return The previous page.
|
||||
"""
|
||||
if not self.previous_page_url:
|
||||
return None
|
||||
|
||||
response = self._version.domain.twilio.request("GET", self.previous_page_url)
|
||||
cls = type(self)
|
||||
return cls(self._version, response, self._solution)
|
||||
|
||||
async def previous_page_async(self) -> Optional["Page"]:
|
||||
"""
|
||||
Asynchronously return the `Page` before this one.
|
||||
:return The previous page.
|
||||
"""
|
||||
if not self.previous_page_url:
|
||||
return None
|
||||
|
||||
response = await self._version.domain.twilio.request_async(
|
||||
"GET", self.previous_page_url
|
||||
)
|
||||
cls = type(self)
|
||||
return cls(self._version, response, self._solution)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "<Page>"
|
||||
@@ -0,0 +1,80 @@
|
||||
import datetime
|
||||
import json
|
||||
|
||||
from twilio.base import values
|
||||
|
||||
|
||||
def iso8601_date(d):
|
||||
"""
|
||||
Return a string representation of a date that the Twilio API understands
|
||||
Format is YYYY-MM-DD. Returns None if d is not a string, datetime, or date
|
||||
"""
|
||||
if d == values.unset:
|
||||
return d
|
||||
elif isinstance(d, datetime.datetime):
|
||||
return str(d.date())
|
||||
elif isinstance(d, datetime.date):
|
||||
return str(d)
|
||||
elif isinstance(d, str):
|
||||
return d
|
||||
|
||||
|
||||
def iso8601_datetime(d):
|
||||
"""
|
||||
Return a string representation of a date that the Twilio API understands
|
||||
Format is YYYY-MM-DD. Returns None if d is not a string, datetime, or date
|
||||
"""
|
||||
if d == values.unset:
|
||||
return d
|
||||
elif isinstance(d, datetime.datetime) or isinstance(d, datetime.date):
|
||||
return d.strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
elif isinstance(d, str):
|
||||
return d
|
||||
|
||||
|
||||
def prefixed_collapsible_map(m, prefix):
|
||||
"""
|
||||
Return a dict of params corresponding to those in m with the added prefix
|
||||
"""
|
||||
if m == values.unset:
|
||||
return {}
|
||||
|
||||
def flatten_dict(d, result=None, prv_keys=None):
|
||||
if result is None:
|
||||
result = {}
|
||||
|
||||
if prv_keys is None:
|
||||
prv_keys = []
|
||||
|
||||
for k, v in d.items():
|
||||
if isinstance(v, dict):
|
||||
flatten_dict(v, result, prv_keys + [k])
|
||||
else:
|
||||
result[".".join(prv_keys + [k])] = v
|
||||
|
||||
return result
|
||||
|
||||
if isinstance(m, dict):
|
||||
flattened = flatten_dict(m)
|
||||
return {"{}.{}".format(prefix, k): v for k, v in flattened.items()}
|
||||
|
||||
return {}
|
||||
|
||||
|
||||
def object(obj):
|
||||
"""
|
||||
Return a jsonified string represenation of obj if obj is jsonifiable else
|
||||
return obj untouched
|
||||
"""
|
||||
if isinstance(obj, dict) or isinstance(obj, list):
|
||||
return json.dumps(obj)
|
||||
return obj
|
||||
|
||||
|
||||
def map(lst, serialize_func):
|
||||
"""
|
||||
Applies serialize_func to every element in lst
|
||||
"""
|
||||
if not isinstance(lst, list):
|
||||
return lst
|
||||
return [serialize_func(e) for e in lst]
|
||||
@@ -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}
|
||||
@@ -0,0 +1,492 @@
|
||||
import json
|
||||
from typing import Any, AsyncIterator, Dict, Iterator, Optional, Tuple
|
||||
|
||||
from twilio.base import values
|
||||
from twilio.base.domain import Domain
|
||||
from twilio.base.exceptions import TwilioRestException
|
||||
from twilio.base.page import Page
|
||||
from twilio.http.response import Response
|
||||
|
||||
|
||||
class Version(object):
|
||||
"""
|
||||
Represents an API version.
|
||||
"""
|
||||
|
||||
def __init__(self, domain: Domain, version: str):
|
||||
self.domain = domain
|
||||
self.version = version
|
||||
|
||||
def absolute_url(self, uri: str) -> str:
|
||||
"""
|
||||
Turns a relative uri into an absolute url.
|
||||
"""
|
||||
return self.domain.absolute_url(self.relative_uri(uri))
|
||||
|
||||
def relative_uri(self, uri: str) -> str:
|
||||
"""
|
||||
Turns a relative uri into a versioned relative uri.
|
||||
"""
|
||||
return "{}/{}".format(self.version.strip("/"), uri.strip("/"))
|
||||
|
||||
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.
|
||||
"""
|
||||
url = self.relative_uri(uri)
|
||||
return self.domain.request(
|
||||
method,
|
||||
url,
|
||||
params=params,
|
||||
data=data,
|
||||
headers=headers,
|
||||
auth=auth,
|
||||
timeout=timeout,
|
||||
allow_redirects=allow_redirects,
|
||||
)
|
||||
|
||||
async def request_async(
|
||||
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
|
||||
"""
|
||||
url = self.relative_uri(uri)
|
||||
return await self.domain.request_async(
|
||||
method,
|
||||
url,
|
||||
params=params,
|
||||
data=data,
|
||||
headers=headers,
|
||||
auth=auth,
|
||||
timeout=timeout,
|
||||
allow_redirects=allow_redirects,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def exception(
|
||||
cls, method: str, uri: str, response: Response, message: str
|
||||
) -> TwilioRestException:
|
||||
"""
|
||||
Wraps an exceptional response in a `TwilioRestException`.
|
||||
"""
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
error_payload = json.loads(response.text)
|
||||
if "message" in error_payload:
|
||||
message = "{}: {}".format(message, error_payload["message"])
|
||||
details = error_payload.get("details")
|
||||
code = error_payload.get("code", response.status_code)
|
||||
return TwilioRestException(
|
||||
response.status_code, uri, message, code, method, details
|
||||
)
|
||||
except Exception:
|
||||
return TwilioRestException(
|
||||
response.status_code, uri, message, response.status_code, method
|
||||
)
|
||||
|
||||
def _parse_fetch(self, method: str, uri: str, response: Response) -> Any:
|
||||
"""
|
||||
Parses fetch response JSON
|
||||
"""
|
||||
# Note that 3XX response codes are allowed for fetches.
|
||||
if response.status_code < 200 or response.status_code >= 400:
|
||||
raise self.exception(method, uri, response, "Unable to fetch record")
|
||||
|
||||
return json.loads(response.text)
|
||||
|
||||
def fetch(
|
||||
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,
|
||||
) -> Any:
|
||||
"""
|
||||
Fetch a resource instance.
|
||||
"""
|
||||
response = self.request(
|
||||
method,
|
||||
uri,
|
||||
params=params,
|
||||
data=data,
|
||||
headers=headers,
|
||||
auth=auth,
|
||||
timeout=timeout,
|
||||
allow_redirects=allow_redirects,
|
||||
)
|
||||
|
||||
return self._parse_fetch(method, uri, response)
|
||||
|
||||
async def fetch_async(
|
||||
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,
|
||||
) -> Any:
|
||||
"""
|
||||
Asynchronously fetch a resource instance.
|
||||
"""
|
||||
response = await self.request_async(
|
||||
method,
|
||||
uri,
|
||||
params=params,
|
||||
data=data,
|
||||
headers=headers,
|
||||
auth=auth,
|
||||
timeout=timeout,
|
||||
allow_redirects=allow_redirects,
|
||||
)
|
||||
|
||||
return self._parse_fetch(method, uri, response)
|
||||
|
||||
def _parse_update(self, method: str, uri: str, response: Response) -> Any:
|
||||
"""
|
||||
Parses update response JSON
|
||||
"""
|
||||
if response.status_code < 200 or response.status_code >= 300:
|
||||
raise self.exception(method, uri, response, "Unable to update record")
|
||||
|
||||
return json.loads(response.text)
|
||||
|
||||
def update(
|
||||
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,
|
||||
) -> Any:
|
||||
"""
|
||||
Update a resource instance.
|
||||
"""
|
||||
response = self.request(
|
||||
method,
|
||||
uri,
|
||||
params=params,
|
||||
data=data,
|
||||
headers=headers,
|
||||
auth=auth,
|
||||
timeout=timeout,
|
||||
allow_redirects=allow_redirects,
|
||||
)
|
||||
|
||||
return self._parse_update(method, uri, response)
|
||||
|
||||
async def update_async(
|
||||
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,
|
||||
) -> Any:
|
||||
"""
|
||||
Asynchronously update a resource instance.
|
||||
"""
|
||||
response = await self.request_async(
|
||||
method,
|
||||
uri,
|
||||
params=params,
|
||||
data=data,
|
||||
headers=headers,
|
||||
auth=auth,
|
||||
timeout=timeout,
|
||||
allow_redirects=allow_redirects,
|
||||
)
|
||||
|
||||
return self._parse_update(method, uri, response)
|
||||
|
||||
def _parse_delete(self, method: str, uri: str, response: Response) -> bool:
|
||||
"""
|
||||
Parses delete response JSON
|
||||
"""
|
||||
if response.status_code < 200 or response.status_code >= 300:
|
||||
raise self.exception(method, uri, response, "Unable to delete record")
|
||||
|
||||
return response.status_code == 204
|
||||
|
||||
def delete(
|
||||
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,
|
||||
) -> bool:
|
||||
"""
|
||||
Delete a resource.
|
||||
"""
|
||||
response = self.request(
|
||||
method,
|
||||
uri,
|
||||
params=params,
|
||||
data=data,
|
||||
headers=headers,
|
||||
auth=auth,
|
||||
timeout=timeout,
|
||||
allow_redirects=allow_redirects,
|
||||
)
|
||||
|
||||
return self._parse_delete(method, uri, response)
|
||||
|
||||
async def delete_async(
|
||||
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,
|
||||
) -> bool:
|
||||
"""
|
||||
Asynchronously delete a resource.
|
||||
"""
|
||||
response = await self.request_async(
|
||||
method,
|
||||
uri,
|
||||
params=params,
|
||||
data=data,
|
||||
headers=headers,
|
||||
auth=auth,
|
||||
timeout=timeout,
|
||||
allow_redirects=allow_redirects,
|
||||
)
|
||||
|
||||
return self._parse_delete(method, uri, response)
|
||||
|
||||
def read_limits(
|
||||
self, limit: Optional[int] = None, page_size: Optional[int] = None
|
||||
) -> Dict[str, object]:
|
||||
"""
|
||||
Takes a limit on the max number of records to read and a max page_size
|
||||
and calculates the max number of pages to read.
|
||||
|
||||
:param limit: Max number of records to read.
|
||||
:param page_size: Max page size.
|
||||
:return A dictionary of paging limits.
|
||||
"""
|
||||
if limit is not None and page_size is None:
|
||||
page_size = limit
|
||||
|
||||
return {
|
||||
"limit": limit or values.unset,
|
||||
"page_size": page_size or values.unset,
|
||||
}
|
||||
|
||||
def page(
|
||||
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:
|
||||
"""
|
||||
Makes an HTTP request.
|
||||
"""
|
||||
return self.request(
|
||||
method,
|
||||
uri,
|
||||
params=params,
|
||||
data=data,
|
||||
headers=headers,
|
||||
auth=auth,
|
||||
timeout=timeout,
|
||||
allow_redirects=allow_redirects,
|
||||
)
|
||||
|
||||
async def page_async(
|
||||
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:
|
||||
"""
|
||||
Makes an asynchronous HTTP request.
|
||||
"""
|
||||
return await self.request_async(
|
||||
method,
|
||||
uri,
|
||||
params=params,
|
||||
data=data,
|
||||
headers=headers,
|
||||
auth=auth,
|
||||
timeout=timeout,
|
||||
allow_redirects=allow_redirects,
|
||||
)
|
||||
|
||||
def stream(
|
||||
self,
|
||||
page: Optional[Page],
|
||||
limit: Optional[int] = None,
|
||||
page_limit: Optional[int] = None,
|
||||
) -> Iterator[Any]:
|
||||
"""
|
||||
Generates records one a time from a page, stopping at prescribed limits.
|
||||
|
||||
:param page: The page to stream.
|
||||
:param limit: The max number of records to read.
|
||||
:param page_limit: The max number of pages to read.
|
||||
"""
|
||||
current_record = 1
|
||||
current_page = 1
|
||||
|
||||
while page is not None:
|
||||
for record in page:
|
||||
yield record
|
||||
current_record += 1
|
||||
if limit and limit is not values.unset and limit < current_record:
|
||||
return
|
||||
|
||||
current_page += 1
|
||||
if (
|
||||
page_limit
|
||||
and page_limit is not values.unset
|
||||
and page_limit < current_page
|
||||
):
|
||||
return
|
||||
|
||||
page = page.next_page()
|
||||
|
||||
async def stream_async(
|
||||
self,
|
||||
page: Optional[Page],
|
||||
limit: Optional[int] = None,
|
||||
page_limit: Optional[int] = None,
|
||||
) -> AsyncIterator[Any]:
|
||||
"""
|
||||
Generates records one a time from a page, stopping at prescribed limits.
|
||||
|
||||
:param page: The page to stream.
|
||||
:param limit: The max number of records to read.
|
||||
:param page_limit: The max number of pages to read.
|
||||
"""
|
||||
current_record = 1
|
||||
current_page = 1
|
||||
|
||||
while page is not None:
|
||||
for record in page:
|
||||
yield record
|
||||
current_record += 1
|
||||
if limit and limit is not values.unset and limit < current_record:
|
||||
return
|
||||
|
||||
current_page += 1
|
||||
if (
|
||||
page_limit
|
||||
and page_limit is not values.unset
|
||||
and page_limit < current_page
|
||||
):
|
||||
return
|
||||
|
||||
page = await page.next_page_async()
|
||||
|
||||
def _parse_create(self, method: str, uri: str, response: Response) -> Any:
|
||||
"""
|
||||
Parse create response JSON
|
||||
"""
|
||||
if response.status_code < 200 or response.status_code >= 300:
|
||||
raise self.exception(method, uri, response, "Unable to create record")
|
||||
|
||||
return json.loads(response.text)
|
||||
|
||||
def create(
|
||||
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,
|
||||
) -> Any:
|
||||
"""
|
||||
Create a resource instance.
|
||||
"""
|
||||
response = self.request(
|
||||
method,
|
||||
uri,
|
||||
params=params,
|
||||
data=data,
|
||||
headers=headers,
|
||||
auth=auth,
|
||||
timeout=timeout,
|
||||
allow_redirects=allow_redirects,
|
||||
)
|
||||
|
||||
return self._parse_create(method, uri, response)
|
||||
|
||||
async def create_async(
|
||||
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,
|
||||
) -> Any:
|
||||
"""
|
||||
Asynchronously create a resource instance.
|
||||
"""
|
||||
response = await self.request_async(
|
||||
method,
|
||||
uri,
|
||||
params=params,
|
||||
data=data,
|
||||
headers=headers,
|
||||
auth=auth,
|
||||
timeout=timeout,
|
||||
allow_redirects=allow_redirects,
|
||||
)
|
||||
|
||||
return self._parse_create(method, uri, response)
|
||||
Reference in New Issue
Block a user