1 import urllib
2 import httplib
3 import mimetypes
4
5 from pyperry.adapter.abstract_adapter import AbstractAdapter
6 from pyperry.errors import ConfigurationError, MalformedResponse
7 from pyperry.response import Response
8
9 ERRORS = {
10 'host': "you must configure the 'host' for the RestfulHttpAdapter",
11 'service': "you must configure the 'service' for the RestfulHttpAdapter"
12 }
13
15 """
16 Adapter for communicating with REST web services over HTTP
17
18 B{Required configuration keywords:}
19
20 - B{host:} the host of the remote server, such as C{github.com} or
21 C{192.0.0.1}
22
23 - B{service:} the name of the service corresponding to your model.
24
25 The service will depend on what services the host server has
26 available, but it is typically the lowercase, plural version of your
27 model's class name. So if you have a model named C{Duck}, your
28 service name will typically be C{ducks}.
29
30 B{Optional configuration keywords:}
31
32 - B{format}: expected data format of the response body, such as
33 C{'xml'} or C{'csv'}. Default is C{'json'}
34
35 - B{primary_key}: an alternate primary_key to use when generating URLs.
36 Defaults to C{model.pk_attr()}
37
38 - B{params_wrapper:} used to wrap your model's attributes before encoding
39 them for your request.
40
41 For example, if you have a model C{m = Model({'id': 4, 'name':
42 'foo'})} with C{params_wrapper='mod'}, the data encoded for the HTTP
43 request will include C{mod[id]=4&mod[name]=foo}
44
45 - B{default_params}: a python dict of additional paramters to include
46 alongside the models attributes in any write or delete request.
47
48 These parameters will not be wrapped in the C{params_wrapper} if that
49 option is also present. One thing C{default_params} are useful for
50 is including an api key with every request.
51
52 """
53
54 - def read(self, **kwargs):
55 """
56 Performs an HTTP GET request and uses the relation dict to construct
57 the query string parameters
58
59 """
60 relation = kwargs['relation']
61 url = self.url_for('GET')
62
63 query_string = self.query_string_for(relation)
64 if query_string is not None:
65 url += query_string
66
67
68 http_response, body = self.http_request('GET', url, {}, **kwargs)
69 response = self.response(http_response, body)
70 records = response.parsed()
71 if not isinstance(records, list):
72 raise MalformedResponse('parsed response is not a list')
73 return records
74
75 - def write(self, **kwargs):
76 model = kwargs['model']
77 if model.new_record:
78 method = 'POST'
79 else:
80 method = 'PUT'
81
82 return self.persistence_request(method, **kwargs)
83
86
93
94 - def response(self, http_response, response_body):
95 r = Response()
96 r.status = http_response.status
97 r.success = r.status == 200
98 r.raw = response_body
99 r.raw_format = self.config_value('format', 'json')
100 r.meta = dict(http_response.getheaders())
101 return r
102
103 - def http_request(self, http_method, url, params, **kwargs):
104 encoded_params = urllib.urlencode(params)
105 headers = {}
106
107 mime_type = mimetypes.guess_type('_.' +
108 self.config_value('format', 'json'))[0]
109 if mime_type is not None:
110 headers['accept'] = mime_type
111
112 if http_method != 'GET':
113 headers['content-type'] = 'application/x-www-form-urlencoded'
114
115
116 conn = httplib.HTTPConnection(self.config_value('host'))
117 try:
118 conn.request(http_method, url, encoded_params, headers)
119 http_response = conn.getresponse()
120 response_body = http_response.read()
121
122
123 finally:
124 conn.close()
125
126 return (http_response, response_body)
127
128 - def url_for(self, http_method, model=None):
146
148 """Builds and encodes a parameters dict for the request"""
149 params = {}
150
151 if hasattr(self.config, 'default_params'):
152 params.update(self.config.default_params)
153
154 if hasattr(self.config, 'params_wrapper'):
155 params.update({self.config.params_wrapper: model.attributes})
156 else:
157 params.update(model.attributes)
158
159 return params
160
162 """
163 Encodes the relation and any query modifiers into a query string
164 suitable for use in a URL. Includes the '?' as part of the query string
165 and returns None if there are no parameters.
166
167 """
168 query = relation.query()
169 mods = relation.modifiers_value()
170 if 'query' in mods:
171 query.update(mods['query'])
172
173 params = self.restful_params(query)
174 if len(params) > 0:
175 return '?' + urllib.urlencode(params)
176
178 """
179 Returns the value of the configuration option named by option.
180
181 If the option is not configured, this method will use the default value
182 if given. Otherwise a ConfigurationError will be thrown.
183
184 """
185 if hasattr(self.config, option):
186 value = getattr(self.config, option)
187 elif default is not None:
188 value = default
189 else:
190 raise ConfigurationError, ERRORS[option]
191 return value
192
194 """
195 Recursively flattens nested dicts into a list of (key, value) tuples
196 so they can be encoded as a query string that can be understood by our
197 webservices.
198
199 In particular, our webservices require nested dicts to be transformed
200 to a format where the nesting is indiciated by a key naming syntax
201 where there are no nested dicts. Instead, the nested dicts are
202 'flattened' by using a key naming syntax where the nested keys are
203 enclosed in brackets and preceded by the non-nested key.
204
205 The best way to understand this format is by example:
206
207 Example input::
208
209 {
210 'key': 'value',
211 'foo': {
212 'list': [1, 2, 3],
213 'bar': {
214 'double-nested': 'value'
215 }
216 }
217 }
218
219 Example output::
220
221 [
222 ('key', 'value'),
223 ('foo[list][]', 1), ('foo[list][]', 2), ('foo[list][]', 3),
224 ('foo[bar][double-nested]', 'value')
225 ]
226
227 When calling the urlencode on the result of this method, you will
228 generate a query string similar to the following. The order of the
229 parameters may vary except that successive array elements will also
230 be successive in the query string::
231
232 'key=value&foo[list][]=1&foo[list][]=2&foo[list][]=3&foo[bar][double-nested]=value'
233
234 """
235 restful = self.params_for_dict(params, [], '')
236 return restful
237
239 for key, value in params.iteritems():
240 new_key_prefix = self.key_for_params(key, value, key_prefix)
241
242 if isinstance(value, dict):
243 self.params_for_dict(value, params_list, new_key_prefix)
244 elif isinstance(value, list):
245 self.params_for_list(value, params_list, new_key_prefix)
246 else:
247 params_list.append((new_key_prefix, self.params_value(value)))
248
249 return params_list
250
252 for value in params:
253 if isinstance(value, dict):
254 self.params_for_dict(value, params_list, key_prefix)
255 elif isinstance(value, list):
256 self.params_for_list(value, params_list, key_prefix + '[]')
257 else:
258 params_list.append((key_prefix, self.params_value(value)))
259
261 if len(key_prefix) > 0:
262 new_key_prefix = '%s[%s]' % (key_prefix, key)
263 else:
264 new_key_prefix = key
265
266 if isinstance(value, list):
267 new_key_prefix += '[]'
268
269 return new_key_prefix
270
272 if value is None:
273 value = ''
274 return value
275