changeset 7:92ae11e33f85

abstract API client
author Jeff Hammel <k0scist@gmail.com>
date Sun, 10 Dec 2017 11:54:10 -0800
parents 244c29f46554
children 59c91bfb6c06
files lemuriformes/client.py
diffstat 1 files changed, 121 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /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)