Mercurial > hg > Lemuriformes
comparison 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 |
comparison
equal
deleted
inserted
replaced
| 6:244c29f46554 | 7:92ae11e33f85 |
|---|---|
| 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) |
