view lemuriformes/client.py @ 7:92ae11e33f85

abstract API client
author Jeff Hammel <k0scist@gmail.com>
date Sun, 10 Dec 2017 11:54:10 -0800
parents
children
line wrap: on
line source

"""
abstract ReSTful HTTP client
"""

# imports
import requests
import sys
import time
from urlparse import urlparse
from .cli import ConfigurationParser


def isurl(string):
    """is `string` a URL?"""

    return bool(urlparse(string).scheme)


def serialize_headers(headers):
    return '\n'.join(["{key}: {value}".format(key=key, value=value)
                      for key, value in sorted(headers.items(),
                                               key=lambda x: x[0])
    ])


def serialize_request(request):
    """serialize a request object to a string"""

    template = u"""{method} {url}
{headers}

{body}
"""
    headers = '\n'.join(['{key}: {value}'.format(key=key, value=value)
                         for key, value in request.headers.items()])
    retval = template.format(method=request.method,
                             url=request.url,
                             headers=headers,
                             body=request.body or '')
    return retval


def serialize_response(response):
    """serialize a response object to a string"""

    template = u"""{url}
{status_code}
{headers}

{text}
"""

    retval = template.format(url=response.url,
                             status_code=response.status_code,
                             headers=serialize_headers(response.headers),
                             text=response.text.strip())
    return retval


class Endpoint(object):
    """abstract base class for a RESTful API client"""

    path = ''
    endpoints = []

    def __init__(self, base_url, session=None, timeout=60.):
        base_url = base_url.rstrip('/')
        self.url = '{}/{}'.format(base_url, self.path)
        self.timeout = timeout
        if session is None:
            session = requests.Session()
        self.session = session
        for endpoint in self.endpoints:
            path = endpoint.path
            setattr(self,
                    path,
                    endpoint(self.url, session=self.session, timeout=timeout))

    def __call__(self, method, url, **kwargs):
        """make an HTTP request"""

        kwargs.setdefault('timeout', self.timeout)
        start = time.time()
        response = self.session.request(method, url, **kwargs)
        end = time.time()
        response.duration = end - start
        try:
            response.raise_for_status()
        except requests.HTTPError as e:
            sys.stderr.write(serialize_response(response) + '\n')
            sys.stderr.write("=>\n")
            sys.stderr.write(serialize_request(response.request) + '\n')
            raise
        return response

    def POST(self, data, **kwargs):
        return self('POST', self.url, data=data, **kwargs)


class ClientParser(ConfigurationParser):
    """abstract argument parser for HTTP client"""

    client_class = Endpoint

    def add_arguments(self):
        self.add_argument('base_url', help="base URL")
        self.add_argument('--timeout', dest='timeout',
                          type=float, default=60.,
                          help='per request timeout in seconds [DEFAULT: %(default)s]')

    def base_url(self):
        return self.options.base_url

    def client(self):
        """return argument specified requests HTTP client"""

        if self.options is None:
            raise Exception("options not yet parsed!")

        return self.client_class(self.base_url(),
                                 timeout=self.options.timeout)