Package pyperry :: Package adapter :: Module http
[frames] | no frames]

Source Code for Module pyperry.adapter.http

  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   
14 -class RestfulHttpAdapter(AbstractAdapter):
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
84 - def delete(self, **kwargs):
85 return self.persistence_request('DELETE', **kwargs)
86
87 - def persistence_request(self, http_method, **kwargs):
88 model = kwargs['model'] 89 url = self.url_for(http_method, model) 90 params = self.restful_params(self.params_for(model)) 91 http_response, body = self.http_request(http_method, url, params) 92 return self.response(http_response, body)
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 # read() must be called before the connection is closed or it will 122 # return an empty string. 123 finally: 124 conn.close() 125 126 return (http_response, response_body)
127
128 - def url_for(self, http_method, model=None):
129 """Constructs the URL for the request""" 130 self.config_value('service') 131 132 service = self.config.service 133 if model is not None: 134 primary_key = self.config_value('primary_key', model.pk_attr()) 135 pk_value = getattr(model, primary_key) 136 format = self.config_value('format', 'json') 137 138 if http_method is 'POST' or model is None: 139 url_tmpl = "/%s.%s" 140 tmpl_args = (service, format) 141 else: 142 url_tmpl = "/%s/%s.%s" 143 tmpl_args = (service, pk_value, format) 144 145 return url_tmpl % tmpl_args
146
147 - def params_for(self, model):
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
161 - def query_string_for(self, relation):
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
177 - def config_value(self, option, default=None):
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
193 - def restful_params(self, params, key_prefix=''):
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
238 - def params_for_dict(self, params, params_list, key_prefix=''):
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
251 - def params_for_list(self, params, params_list, key_prefix=''):
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
260 - def key_for_params(self, key, value, key_prefix=''):
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
271 - def params_value(self, value):
272 if value is None: 273 value = '' 274 return value
275