aboutsummaryrefslogtreecommitdiff
path: root/.venv/lib/python3.12/site-packages/python_http_client/client.py
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/python_http_client/client.py')
-rw-r--r--.venv/lib/python3.12/site-packages/python_http_client/client.py296
1 files changed, 296 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/python_http_client/client.py b/.venv/lib/python3.12/site-packages/python_http_client/client.py
new file mode 100644
index 00000000..3a562351
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/python_http_client/client.py
@@ -0,0 +1,296 @@
+"""HTTP Client library"""
+import json
+import logging
+from .exceptions import handle_error
+
+try:
+ # Python 3
+ import urllib.request as urllib
+ from urllib.parse import urlencode
+ from urllib.error import HTTPError
+except ImportError:
+ # Python 2
+ import urllib2 as urllib
+ from urllib2 import HTTPError
+ from urllib import urlencode
+
+_logger = logging.getLogger(__name__)
+
+
+class Response(object):
+ """Holds the response from an API call."""
+
+ def __init__(self, response):
+ """
+ :param response: The return value from a open call
+ on a urllib.build_opener()
+ :type response: urllib response object
+ """
+ self._status_code = response.getcode()
+ self._body = response.read()
+ self._headers = response.info()
+
+ @property
+ def status_code(self):
+ """
+ :return: integer, status code of API call
+ """
+ return self._status_code
+
+ @property
+ def body(self):
+ """
+ :return: response from the API
+ """
+ return self._body
+
+ @property
+ def headers(self):
+ """
+ :return: dict of response headers
+ """
+ return self._headers
+
+ @property
+ def to_dict(self):
+ """
+ :return: dict of response from the API
+ """
+ if self.body:
+ return json.loads(self.body.decode('utf-8'))
+ else:
+ return None
+
+
+class Client(object):
+ """Quickly and easily access any REST or REST-like API."""
+
+ # These are the supported HTTP verbs
+ methods = {'delete', 'get', 'patch', 'post', 'put'}
+
+ def __init__(self,
+ host,
+ request_headers=None,
+ version=None,
+ url_path=None,
+ append_slash=False,
+ timeout=None):
+ """
+ :param host: Base URL for the api. (e.g. https://api.sendgrid.com)
+ :type host: string
+ :param request_headers: A dictionary of the headers you want
+ applied on all calls
+ :type request_headers: dictionary
+ :param version: The version number of the API.
+ Subclass _build_versioned_url for custom behavior.
+ Or just pass the version as part of the URL
+ (e.g. client._("/v3"))
+ :type version: integer
+ :param url_path: A list of the url path segments
+ :type url_path: list of strings
+ """
+ self.host = host
+ self.request_headers = request_headers or {}
+ self._version = version
+ # _url_path keeps track of the dynamically built url
+ self._url_path = url_path or []
+ # APPEND SLASH set
+ self.append_slash = append_slash
+ self.timeout = timeout
+
+ def _build_versioned_url(self, url):
+ """Subclass this function for your own needs.
+ Or just pass the version as part of the URL
+ (e.g. client._('/v3'))
+ :param url: URI portion of the full URL being requested
+ :type url: string
+ :return: string
+ """
+ return '{}/v{}{}'.format(self.host, str(self._version), url)
+
+ def _build_url(self, query_params):
+ """Build the final URL to be passed to urllib
+
+ :param query_params: A dictionary of all the query parameters
+ :type query_params: dictionary
+ :return: string
+ """
+ url = ''
+ count = 0
+ while count < len(self._url_path):
+ url += '/{}'.format(self._url_path[count])
+ count += 1
+
+ # add slash
+ if self.append_slash:
+ url += '/'
+
+ if query_params:
+ url_values = urlencode(sorted(query_params.items()), True)
+ url = '{}?{}'.format(url, url_values)
+
+ if self._version:
+ url = self._build_versioned_url(url)
+ else:
+ url = '{}{}'.format(self.host, url)
+ return url
+
+ def _update_headers(self, request_headers):
+ """Update the headers for the request
+
+ :param request_headers: headers to set for the API call
+ :type request_headers: dictionary
+ :return: dictionary
+ """
+ self.request_headers.update(request_headers)
+
+ def _build_client(self, name=None):
+ """Make a new Client object
+
+ :param name: Name of the url segment
+ :type name: string
+ :return: A Client object
+ """
+ url_path = self._url_path + [name] if name else self._url_path
+ return Client(host=self.host,
+ version=self._version,
+ request_headers=self.request_headers,
+ url_path=url_path,
+ append_slash=self.append_slash,
+ timeout=self.timeout)
+
+ def _make_request(self, opener, request, timeout=None):
+ """Make the API call and return the response. This is separated into
+ it's own function, so we can mock it easily for testing.
+
+ :param opener:
+ :type opener:
+ :param request: url payload to request
+ :type request: urllib.Request object
+ :param timeout: timeout value or None
+ :type timeout: float
+ :return: urllib response
+ """
+ timeout = timeout or self.timeout
+ try:
+ return opener.open(request, timeout=timeout)
+ except HTTPError as err:
+ exc = handle_error(err)
+ exc.__cause__ = None
+ _logger.debug('{method} Response: {status} {body}'.format(
+ method=request.get_method(),
+ status=exc.status_code,
+ body=exc.body))
+ raise exc
+
+ def _(self, name):
+ """Add variable values to the url.
+ (e.g. /your/api/{variable_value}/call)
+ Another example: if you have a Python reserved word, such as global,
+ in your url, you must use this method.
+
+ :param name: Name of the url segment
+ :type name: string
+ :return: Client object
+ """
+ return self._build_client(name)
+
+ def __getattr__(self, name):
+ """Dynamically add method calls to the url, then call a method.
+ (e.g. client.name.name.method())
+ You can also add a version number by using .version(<int>)
+
+ :param name: Name of the url segment or method call
+ :type name: string or integer if name == version
+ :return: mixed
+ """
+ if name == 'version':
+ def get_version(*args, **kwargs):
+ """
+ :param args: dict of settings
+ :param kwargs: unused
+ :return: string, version
+ """
+ self._version = args[0]
+ return self._build_client()
+ return get_version
+
+ # We have reached the end of the method chain, make the API call
+ if name in self.methods:
+ method = name.upper()
+
+ def http_request(
+ request_body=None,
+ query_params=None,
+ request_headers=None,
+ timeout=None,
+ **_):
+ """Make the API call
+ :param timeout: HTTP request timeout. Will be propagated to
+ urllib client
+ :type timeout: float
+ :param request_headers: HTTP headers. Will be merged into
+ current client object state
+ :type request_headers: dict
+ :param query_params: HTTP query parameters
+ :type query_params: dict
+ :param request_body: HTTP request body
+ :type request_body: string or json-serializable object
+ :param kwargs:
+ :return: Response object
+ """
+ if request_headers:
+ self._update_headers(request_headers)
+
+ if request_body is None:
+ data = None
+ else:
+ # Don't serialize to a JSON formatted str
+ # if we don't have a JSON Content-Type
+ if 'Content-Type' in self.request_headers and \
+ self.request_headers['Content-Type'] != \
+ 'application/json':
+ data = request_body.encode('utf-8')
+ else:
+ self.request_headers.setdefault(
+ 'Content-Type', 'application/json')
+ data = json.dumps(request_body).encode('utf-8')
+
+ opener = urllib.build_opener()
+ request = urllib.Request(
+ self._build_url(query_params),
+ headers=self.request_headers,
+ data=data,
+ )
+ request.get_method = lambda: method
+
+ _logger.debug('{method} Request: {url}'.format(
+ method=method,
+ url=request.get_full_url()))
+ if request.data:
+ _logger.debug('PAYLOAD: {data}'.format(
+ data=request.data))
+ _logger.debug('HEADERS: {headers}'.format(
+ headers=request.headers))
+
+ response = Response(
+ self._make_request(opener, request, timeout=timeout)
+ )
+
+ _logger.debug('{method} Response: {status} {body}'.format(
+ method=method,
+ status=response.status_code,
+ body=response.body))
+
+ return response
+
+ return http_request
+ else:
+ # Add a segment to the URL
+ return self._(name)
+
+ def __getstate__(self):
+ return self.__dict__
+
+ def __setstate__(self, state):
+ self.__dict__ = state