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