# HG changeset patch # User Jeff Hammel # Date 1512935650 28800 # Node ID 92ae11e33f85596f34ce458d2dc62aeb3857cabb # Parent 244c29f46554ab5916f534d0e28c43884125af5b abstract API client diff -r 244c29f46554 -r 92ae11e33f85 lemuriformes/client.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lemuriformes/client.py Sun Dec 10 11:54:10 2017 -0800 @@ -0,0 +1,121 @@ +""" +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)