blob: 7ab80e97c1b4bee2b54f0532e56df1da1535b993 [file] [log] [blame]
Craig Citro751b7fb2014-09-23 11:20:38 -07001# Copyright 2014 Google Inc. All Rights Reserved.
John Asmuth864311d2014-04-24 15:46:08 -04002#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Model objects for requests and responses.
16
17Each API may support one or more serializations, such
18as JSON, Atom, etc. The model classes are responsible
19for converting between the wire format and the Python
20object representation.
21"""
INADA Naoki0bceb332014-08-20 15:27:52 +090022from __future__ import absolute_import
INADA Naokie4ea1a92015-03-04 03:45:42 +090023import six
John Asmuth864311d2014-04-24 15:46:08 -040024
25__author__ = 'jcgregorio@google.com (Joe Gregorio)'
26
Craig Citro6ae34d72014-08-18 23:10:09 -070027import json
John Asmuth864311d2014-04-24 15:46:08 -040028import logging
Bu Sun Kim07f647c2019-08-09 14:55:24 -070029import platform
Pat Ferated5b61bd2015-03-03 16:04:11 -080030
31from six.moves.urllib.parse import urlencode
John Asmuth864311d2014-04-24 15:46:08 -040032
33from googleapiclient import __version__
Pat Ferateb240c172015-03-03 16:23:51 -080034from googleapiclient.errors import HttpError
John Asmuth864311d2014-04-24 15:46:08 -040035
Bu Sun Kim07f647c2019-08-09 14:55:24 -070036_PY_VERSION = platform.python_version()
John Asmuth864311d2014-04-24 15:46:08 -040037
Emmett Butler09699152016-02-08 14:26:00 -080038LOGGER = logging.getLogger(__name__)
39
John Asmuth864311d2014-04-24 15:46:08 -040040dump_request_response = False
41
42
43def _abstract():
44 raise NotImplementedError('You need to override this function')
45
46
47class Model(object):
48 """Model base class.
49
50 All Model classes should implement this interface.
51 The Model serializes and de-serializes between a wire
52 format such as JSON and a Python object representation.
53 """
54
55 def request(self, headers, path_params, query_params, body_value):
56 """Updates outgoing requests with a serialized body.
57
58 Args:
59 headers: dict, request headers
60 path_params: dict, parameters that appear in the request path
61 query_params: dict, parameters that appear in the query
62 body_value: object, the request body as a Python object, which must be
63 serializable.
64 Returns:
65 A tuple of (headers, path_params, query, body)
66
67 headers: dict, request headers
68 path_params: dict, parameters that appear in the request path
69 query: string, query part of the request URI
70 body: string, the body serialized in the desired wire format.
71 """
72 _abstract()
73
74 def response(self, resp, content):
75 """Convert the response wire format into a Python object.
76
77 Args:
78 resp: httplib2.Response, the HTTP response headers and status
79 content: string, the body of the HTTP response
80
81 Returns:
82 The body de-serialized as a Python object.
83
84 Raises:
85 googleapiclient.errors.HttpError if a non 2xx response is received.
86 """
87 _abstract()
88
89
90class BaseModel(Model):
91 """Base model class.
92
93 Subclasses should provide implementations for the "serialize" and
94 "deserialize" methods, as well as values for the following class attributes.
95
96 Attributes:
97 accept: The value to use for the HTTP Accept header.
98 content_type: The value to use for the HTTP Content-type header.
99 no_content_response: The value to return when deserializing a 204 "No
100 Content" response.
101 alt_param: The value to supply as the "alt" query parameter for requests.
102 """
103
104 accept = None
105 content_type = None
106 no_content_response = None
107 alt_param = None
108
109 def _log_request(self, headers, path_params, query, body):
110 """Logs debugging information about the request if requested."""
111 if dump_request_response:
Emmett Butler09699152016-02-08 14:26:00 -0800112 LOGGER.info('--request-start--')
113 LOGGER.info('-headers-start-')
INADA Naokie4ea1a92015-03-04 03:45:42 +0900114 for h, v in six.iteritems(headers):
Emmett Butler09699152016-02-08 14:26:00 -0800115 LOGGER.info('%s: %s', h, v)
116 LOGGER.info('-headers-end-')
117 LOGGER.info('-path-parameters-start-')
INADA Naokie4ea1a92015-03-04 03:45:42 +0900118 for h, v in six.iteritems(path_params):
Emmett Butler09699152016-02-08 14:26:00 -0800119 LOGGER.info('%s: %s', h, v)
120 LOGGER.info('-path-parameters-end-')
121 LOGGER.info('body: %s', body)
122 LOGGER.info('query: %s', query)
123 LOGGER.info('--request-end--')
John Asmuth864311d2014-04-24 15:46:08 -0400124
125 def request(self, headers, path_params, query_params, body_value):
126 """Updates outgoing requests with a serialized body.
127
128 Args:
129 headers: dict, request headers
130 path_params: dict, parameters that appear in the request path
131 query_params: dict, parameters that appear in the query
132 body_value: object, the request body as a Python object, which must be
Craig Citro6ae34d72014-08-18 23:10:09 -0700133 serializable by json.
John Asmuth864311d2014-04-24 15:46:08 -0400134 Returns:
135 A tuple of (headers, path_params, query, body)
136
137 headers: dict, request headers
138 path_params: dict, parameters that appear in the request path
139 query: string, query part of the request URI
140 body: string, the body serialized as JSON
141 """
142 query = self._build_query(query_params)
143 headers['accept'] = self.accept
144 headers['accept-encoding'] = 'gzip, deflate'
145 if 'user-agent' in headers:
146 headers['user-agent'] += ' '
147 else:
148 headers['user-agent'] = ''
Bu Sun Kim07f647c2019-08-09 14:55:24 -0700149 headers['user-agent'] += '(gzip)'
150 if 'x-goog-api-client' in headers:
151 headers['x-goog-api-client'] += ' '
152 else:
153 headers['x-goog-api-client'] = ''
154 headers['x-goog-api-client'] += 'gdcl/%s gl-python/%s' % (__version__, _PY_VERSION)
John Asmuth864311d2014-04-24 15:46:08 -0400155
156 if body_value is not None:
157 headers['content-type'] = self.content_type
158 body_value = self.serialize(body_value)
159 self._log_request(headers, path_params, query, body_value)
160 return (headers, path_params, query, body_value)
161
162 def _build_query(self, params):
163 """Builds a query string.
164
165 Args:
166 params: dict, the query parameters
167
168 Returns:
169 The query parameters properly encoded into an HTTP URI query string.
170 """
171 if self.alt_param is not None:
172 params.update({'alt': self.alt_param})
173 astuples = []
INADA Naokie4ea1a92015-03-04 03:45:42 +0900174 for key, value in six.iteritems(params):
John Asmuth864311d2014-04-24 15:46:08 -0400175 if type(value) == type([]):
176 for x in value:
177 x = x.encode('utf-8')
178 astuples.append((key, x))
179 else:
INADA Naokie4ea1a92015-03-04 03:45:42 +0900180 if isinstance(value, six.text_type) and callable(value.encode):
John Asmuth864311d2014-04-24 15:46:08 -0400181 value = value.encode('utf-8')
182 astuples.append((key, value))
Pat Ferated5b61bd2015-03-03 16:04:11 -0800183 return '?' + urlencode(astuples)
John Asmuth864311d2014-04-24 15:46:08 -0400184
185 def _log_response(self, resp, content):
186 """Logs debugging information about the response if requested."""
187 if dump_request_response:
Emmett Butler09699152016-02-08 14:26:00 -0800188 LOGGER.info('--response-start--')
INADA Naokie4ea1a92015-03-04 03:45:42 +0900189 for h, v in six.iteritems(resp):
Emmett Butler09699152016-02-08 14:26:00 -0800190 LOGGER.info('%s: %s', h, v)
John Asmuth864311d2014-04-24 15:46:08 -0400191 if content:
Emmett Butler09699152016-02-08 14:26:00 -0800192 LOGGER.info(content)
193 LOGGER.info('--response-end--')
John Asmuth864311d2014-04-24 15:46:08 -0400194
195 def response(self, resp, content):
196 """Convert the response wire format into a Python object.
197
198 Args:
199 resp: httplib2.Response, the HTTP response headers and status
200 content: string, the body of the HTTP response
201
202 Returns:
203 The body de-serialized as a Python object.
204
205 Raises:
206 googleapiclient.errors.HttpError if a non 2xx response is received.
207 """
208 self._log_response(resp, content)
209 # Error handling is TBD, for example, do we retry
210 # for some operation/error combinations?
211 if resp.status < 300:
212 if resp.status == 204:
213 # A 204: No Content response should be treated differently
214 # to all the other success states
215 return self.no_content_response
216 return self.deserialize(content)
217 else:
Emmett Butler09699152016-02-08 14:26:00 -0800218 LOGGER.debug('Content from bad request was: %s' % content)
John Asmuth864311d2014-04-24 15:46:08 -0400219 raise HttpError(resp, content)
220
221 def serialize(self, body_value):
222 """Perform the actual Python object serialization.
223
224 Args:
225 body_value: object, the request body as a Python object.
226
227 Returns:
228 string, the body in serialized form.
229 """
230 _abstract()
231
232 def deserialize(self, content):
233 """Perform the actual deserialization from response string to Python
234 object.
235
236 Args:
237 content: string, the body of the HTTP response
238
239 Returns:
240 The body de-serialized as a Python object.
241 """
242 _abstract()
243
244
245class JsonModel(BaseModel):
246 """Model class for JSON.
247
248 Serializes and de-serializes between JSON and the Python
249 object representation of HTTP request and response bodies.
250 """
251 accept = 'application/json'
252 content_type = 'application/json'
253 alt_param = 'json'
254
255 def __init__(self, data_wrapper=False):
256 """Construct a JsonModel.
257
258 Args:
259 data_wrapper: boolean, wrap requests and responses in a data wrapper
260 """
261 self._data_wrapper = data_wrapper
262
263 def serialize(self, body_value):
264 if (isinstance(body_value, dict) and 'data' not in body_value and
265 self._data_wrapper):
266 body_value = {'data': body_value}
Craig Citro6ae34d72014-08-18 23:10:09 -0700267 return json.dumps(body_value)
John Asmuth864311d2014-04-24 15:46:08 -0400268
269 def deserialize(self, content):
Pat Ferate9b0452c2015-03-03 17:59:56 -0800270 try:
271 content = content.decode('utf-8')
272 except AttributeError:
273 pass
Craig Citro6ae34d72014-08-18 23:10:09 -0700274 body = json.loads(content)
John Asmuth864311d2014-04-24 15:46:08 -0400275 if self._data_wrapper and isinstance(body, dict) and 'data' in body:
276 body = body['data']
277 return body
278
279 @property
280 def no_content_response(self):
281 return {}
282
283
284class RawModel(JsonModel):
285 """Model class for requests that don't return JSON.
286
287 Serializes and de-serializes between JSON and the Python
288 object representation of HTTP request, and returns the raw bytes
289 of the response body.
290 """
291 accept = '*/*'
292 content_type = 'application/json'
293 alt_param = None
294
295 def deserialize(self, content):
296 return content
297
298 @property
299 def no_content_response(self):
300 return ''
301
302
303class MediaModel(JsonModel):
304 """Model class for requests that return Media.
305
306 Serializes and de-serializes between JSON and the Python
307 object representation of HTTP request, and returns the raw bytes
308 of the response body.
309 """
310 accept = '*/*'
311 content_type = 'application/json'
312 alt_param = 'media'
313
314 def deserialize(self, content):
315 return content
316
317 @property
318 def no_content_response(self):
319 return ''
320
321
322class ProtocolBufferModel(BaseModel):
323 """Model class for protocol buffers.
324
325 Serializes and de-serializes the binary protocol buffer sent in the HTTP
326 request and response bodies.
327 """
328 accept = 'application/x-protobuf'
329 content_type = 'application/x-protobuf'
330 alt_param = 'proto'
331
332 def __init__(self, protocol_buffer):
333 """Constructs a ProtocolBufferModel.
334
335 The serialzed protocol buffer returned in an HTTP response will be
336 de-serialized using the given protocol buffer class.
337
338 Args:
339 protocol_buffer: The protocol buffer class used to de-serialize a
340 response from the API.
341 """
342 self._protocol_buffer = protocol_buffer
343
344 def serialize(self, body_value):
345 return body_value.SerializeToString()
346
347 def deserialize(self, content):
348 return self._protocol_buffer.FromString(content)
349
350 @property
351 def no_content_response(self):
352 return self._protocol_buffer()
353
354
355def makepatch(original, modified):
356 """Create a patch object.
357
358 Some methods support PATCH, an efficient way to send updates to a resource.
359 This method allows the easy construction of patch bodies by looking at the
360 differences between a resource before and after it was modified.
361
362 Args:
363 original: object, the original deserialized resource
364 modified: object, the modified deserialized resource
365 Returns:
366 An object that contains only the changes from original to modified, in a
367 form suitable to pass to a PATCH method.
368
369 Example usage:
370 item = service.activities().get(postid=postid, userid=userid).execute()
371 original = copy.deepcopy(item)
372 item['object']['content'] = 'This is updated.'
373 service.activities.patch(postid=postid, userid=userid,
374 body=makepatch(original, item)).execute()
375 """
376 patch = {}
INADA Naokie4ea1a92015-03-04 03:45:42 +0900377 for key, original_value in six.iteritems(original):
John Asmuth864311d2014-04-24 15:46:08 -0400378 modified_value = modified.get(key, None)
379 if modified_value is None:
380 # Use None to signal that the element is deleted
381 patch[key] = None
382 elif original_value != modified_value:
383 if type(original_value) == type({}):
384 # Recursively descend objects
385 patch[key] = makepatch(original_value, modified_value)
386 else:
387 # In the case of simple types or arrays we just replace
388 patch[key] = modified_value
389 else:
390 # Don't add anything to patch if there's no change
391 pass
392 for key in modified:
393 if key not in original:
394 patch[key] = modified[key]
395
396 return patch