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) |