| 7 | 1 """ | 
|  | 2 abstract ReSTful HTTP client | 
|  | 3 """ | 
|  | 4 | 
|  | 5 # imports | 
|  | 6 import requests | 
|  | 7 import sys | 
|  | 8 import time | 
|  | 9 from urlparse import urlparse | 
|  | 10 from .cli import ConfigurationParser | 
|  | 11 | 
|  | 12 | 
|  | 13 def isurl(string): | 
|  | 14     """is `string` a URL?""" | 
|  | 15 | 
|  | 16     return bool(urlparse(string).scheme) | 
|  | 17 | 
|  | 18 | 
|  | 19 def serialize_headers(headers): | 
|  | 20     return '\n'.join(["{key}: {value}".format(key=key, value=value) | 
|  | 21                       for key, value in sorted(headers.items(), | 
|  | 22                                                key=lambda x: x[0]) | 
|  | 23     ]) | 
|  | 24 | 
|  | 25 | 
|  | 26 def serialize_request(request): | 
|  | 27     """serialize a request object to a string""" | 
|  | 28 | 
|  | 29     template = u"""{method} {url} | 
|  | 30 {headers} | 
|  | 31 | 
|  | 32 {body} | 
|  | 33 """ | 
|  | 34     headers = '\n'.join(['{key}: {value}'.format(key=key, value=value) | 
|  | 35                          for key, value in request.headers.items()]) | 
|  | 36     retval = template.format(method=request.method, | 
|  | 37                              url=request.url, | 
|  | 38                              headers=headers, | 
|  | 39                              body=request.body or '') | 
|  | 40     return retval | 
|  | 41 | 
|  | 42 | 
|  | 43 def serialize_response(response): | 
|  | 44     """serialize a response object to a string""" | 
|  | 45 | 
|  | 46     template = u"""{url} | 
|  | 47 {status_code} | 
|  | 48 {headers} | 
|  | 49 | 
|  | 50 {text} | 
|  | 51 """ | 
|  | 52 | 
|  | 53     retval = template.format(url=response.url, | 
|  | 54                              status_code=response.status_code, | 
|  | 55                              headers=serialize_headers(response.headers), | 
|  | 56                              text=response.text.strip()) | 
|  | 57     return retval | 
|  | 58 | 
|  | 59 | 
|  | 60 class Endpoint(object): | 
|  | 61     """abstract base class for a RESTful API client""" | 
|  | 62 | 
|  | 63     path = '' | 
|  | 64     endpoints = [] | 
|  | 65 | 
|  | 66     def __init__(self, base_url, session=None, timeout=60.): | 
|  | 67         base_url = base_url.rstrip('/') | 
|  | 68         self.url = '{}/{}'.format(base_url, self.path) | 
|  | 69         self.timeout = timeout | 
|  | 70         if session is None: | 
|  | 71             session = requests.Session() | 
|  | 72         self.session = session | 
|  | 73         for endpoint in self.endpoints: | 
|  | 74             path = endpoint.path | 
|  | 75             setattr(self, | 
|  | 76                     path, | 
|  | 77                     endpoint(self.url, session=self.session, timeout=timeout)) | 
|  | 78 | 
|  | 79     def __call__(self, method, url, **kwargs): | 
|  | 80         """make an HTTP request""" | 
|  | 81 | 
|  | 82         kwargs.setdefault('timeout', self.timeout) | 
|  | 83         start = time.time() | 
|  | 84         response = self.session.request(method, url, **kwargs) | 
|  | 85         end = time.time() | 
|  | 86         response.duration = end - start | 
|  | 87         try: | 
|  | 88             response.raise_for_status() | 
|  | 89         except requests.HTTPError as e: | 
|  | 90             sys.stderr.write(serialize_response(response) + '\n') | 
|  | 91             sys.stderr.write("=>\n") | 
|  | 92             sys.stderr.write(serialize_request(response.request) + '\n') | 
|  | 93             raise | 
|  | 94         return response | 
|  | 95 | 
|  | 96     def POST(self, data, **kwargs): | 
|  | 97         return self('POST', self.url, data=data, **kwargs) | 
|  | 98 | 
|  | 99 | 
|  | 100 class ClientParser(ConfigurationParser): | 
|  | 101     """abstract argument parser for HTTP client""" | 
|  | 102 | 
|  | 103     client_class = Endpoint | 
|  | 104 | 
|  | 105     def add_arguments(self): | 
|  | 106         self.add_argument('base_url', help="base URL") | 
|  | 107         self.add_argument('--timeout', dest='timeout', | 
|  | 108                           type=float, default=60., | 
|  | 109                           help='per request timeout in seconds [DEFAULT: %(default)s]') | 
|  | 110 | 
|  | 111     def base_url(self): | 
|  | 112         return self.options.base_url | 
|  | 113 | 
|  | 114     def client(self): | 
|  | 115         """return argument specified requests HTTP client""" | 
|  | 116 | 
|  | 117         if self.options is None: | 
|  | 118             raise Exception("options not yet parsed!") | 
|  | 119 | 
|  | 120         return self.client_class(self.base_url(), | 
|  | 121                                  timeout=self.options.timeout) |