| # Copyright 2014 Google Inc. All Rights Reserved. | |
| # | |
| # Licensed under the Apache License, Version 2.0 (the "License"); | |
| # you may not use this file except in compliance with the License. | |
| # You may obtain a copy of the License at | |
| # | |
| # http://www.apache.org/licenses/LICENSE-2.0 | |
| # | |
| # Unless required by applicable law or agreed to in writing, software | |
| # distributed under the License is distributed on an "AS IS" BASIS, | |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| # See the License for the specific language governing permissions and | |
| # limitations under the License. | |
| """Model objects for requests and responses. | |
| Each API may support one or more serializations, such | |
| as JSON, Atom, etc. The model classes are responsible | |
| for converting between the wire format and the Python | |
| object representation. | |
| """ | |
| from __future__ import absolute_import | |
| import six | |
| __author__ = 'jcgregorio@google.com (Joe Gregorio)' | |
| import json | |
| import logging | |
| from six.moves.urllib.parse import urlencode | |
| from googleapiclient import __version__ | |
| from googleapiclient.errors import HttpError | |
| LOGGER = logging.getLogger(__name__) | |
| dump_request_response = False | |
| def _abstract(): | |
| raise NotImplementedError('You need to override this function') | |
| class Model(object): | |
| """Model base class. | |
| All Model classes should implement this interface. | |
| The Model serializes and de-serializes between a wire | |
| format such as JSON and a Python object representation. | |
| """ | |
| def request(self, headers, path_params, query_params, body_value): | |
| """Updates outgoing requests with a serialized body. | |
| Args: | |
| headers: dict, request headers | |
| path_params: dict, parameters that appear in the request path | |
| query_params: dict, parameters that appear in the query | |
| body_value: object, the request body as a Python object, which must be | |
| serializable. | |
| Returns: | |
| A tuple of (headers, path_params, query, body) | |
| headers: dict, request headers | |
| path_params: dict, parameters that appear in the request path | |
| query: string, query part of the request URI | |
| body: string, the body serialized in the desired wire format. | |
| """ | |
| _abstract() | |
| def response(self, resp, content): | |
| """Convert the response wire format into a Python object. | |
| Args: | |
| resp: httplib2.Response, the HTTP response headers and status | |
| content: string, the body of the HTTP response | |
| Returns: | |
| The body de-serialized as a Python object. | |
| Raises: | |
| googleapiclient.errors.HttpError if a non 2xx response is received. | |
| """ | |
| _abstract() | |
| class BaseModel(Model): | |
| """Base model class. | |
| Subclasses should provide implementations for the "serialize" and | |
| "deserialize" methods, as well as values for the following class attributes. | |
| Attributes: | |
| accept: The value to use for the HTTP Accept header. | |
| content_type: The value to use for the HTTP Content-type header. | |
| no_content_response: The value to return when deserializing a 204 "No | |
| Content" response. | |
| alt_param: The value to supply as the "alt" query parameter for requests. | |
| """ | |
| accept = None | |
| content_type = None | |
| no_content_response = None | |
| alt_param = None | |
| def _log_request(self, headers, path_params, query, body): | |
| """Logs debugging information about the request if requested.""" | |
| if dump_request_response: | |
| LOGGER.info('--request-start--') | |
| LOGGER.info('-headers-start-') | |
| for h, v in six.iteritems(headers): | |
| LOGGER.info('%s: %s', h, v) | |
| LOGGER.info('-headers-end-') | |
| LOGGER.info('-path-parameters-start-') | |
| for h, v in six.iteritems(path_params): | |
| LOGGER.info('%s: %s', h, v) | |
| LOGGER.info('-path-parameters-end-') | |
| LOGGER.info('body: %s', body) | |
| LOGGER.info('query: %s', query) | |
| LOGGER.info('--request-end--') | |
| def request(self, headers, path_params, query_params, body_value): | |
| """Updates outgoing requests with a serialized body. | |
| Args: | |
| headers: dict, request headers | |
| path_params: dict, parameters that appear in the request path | |
| query_params: dict, parameters that appear in the query | |
| body_value: object, the request body as a Python object, which must be | |
| serializable by json. | |
| Returns: | |
| A tuple of (headers, path_params, query, body) | |
| headers: dict, request headers | |
| path_params: dict, parameters that appear in the request path | |
| query: string, query part of the request URI | |
| body: string, the body serialized as JSON | |
| """ | |
| query = self._build_query(query_params) | |
| headers['accept'] = self.accept | |
| headers['accept-encoding'] = 'gzip, deflate' | |
| if 'user-agent' in headers: | |
| headers['user-agent'] += ' ' | |
| else: | |
| headers['user-agent'] = '' | |
| headers['user-agent'] += 'google-api-python-client/%s (gzip)' % __version__ | |
| if body_value is not None: | |
| headers['content-type'] = self.content_type | |
| body_value = self.serialize(body_value) | |
| self._log_request(headers, path_params, query, body_value) | |
| return (headers, path_params, query, body_value) | |
| def _build_query(self, params): | |
| """Builds a query string. | |
| Args: | |
| params: dict, the query parameters | |
| Returns: | |
| The query parameters properly encoded into an HTTP URI query string. | |
| """ | |
| if self.alt_param is not None: | |
| params.update({'alt': self.alt_param}) | |
| astuples = [] | |
| for key, value in six.iteritems(params): | |
| if type(value) == type([]): | |
| for x in value: | |
| x = x.encode('utf-8') | |
| astuples.append((key, x)) | |
| else: | |
| if isinstance(value, six.text_type) and callable(value.encode): | |
| value = value.encode('utf-8') | |
| astuples.append((key, value)) | |
| return '?' + urlencode(astuples) | |
| def _log_response(self, resp, content): | |
| """Logs debugging information about the response if requested.""" | |
| if dump_request_response: | |
| LOGGER.info('--response-start--') | |
| for h, v in six.iteritems(resp): | |
| LOGGER.info('%s: %s', h, v) | |
| if content: | |
| LOGGER.info(content) | |
| LOGGER.info('--response-end--') | |
| def response(self, resp, content): | |
| """Convert the response wire format into a Python object. | |
| Args: | |
| resp: httplib2.Response, the HTTP response headers and status | |
| content: string, the body of the HTTP response | |
| Returns: | |
| The body de-serialized as a Python object. | |
| Raises: | |
| googleapiclient.errors.HttpError if a non 2xx response is received. | |
| """ | |
| self._log_response(resp, content) | |
| # Error handling is TBD, for example, do we retry | |
| # for some operation/error combinations? | |
| if resp.status < 300: | |
| if resp.status == 204: | |
| # A 204: No Content response should be treated differently | |
| # to all the other success states | |
| return self.no_content_response | |
| return self.deserialize(content) | |
| else: | |
| LOGGER.debug('Content from bad request was: %s' % content) | |
| raise HttpError(resp, content) | |
| def serialize(self, body_value): | |
| """Perform the actual Python object serialization. | |
| Args: | |
| body_value: object, the request body as a Python object. | |
| Returns: | |
| string, the body in serialized form. | |
| """ | |
| _abstract() | |
| def deserialize(self, content): | |
| """Perform the actual deserialization from response string to Python | |
| object. | |
| Args: | |
| content: string, the body of the HTTP response | |
| Returns: | |
| The body de-serialized as a Python object. | |
| """ | |
| _abstract() | |
| class JsonModel(BaseModel): | |
| """Model class for JSON. | |
| Serializes and de-serializes between JSON and the Python | |
| object representation of HTTP request and response bodies. | |
| """ | |
| accept = 'application/json' | |
| content_type = 'application/json' | |
| alt_param = 'json' | |
| def __init__(self, data_wrapper=False): | |
| """Construct a JsonModel. | |
| Args: | |
| data_wrapper: boolean, wrap requests and responses in a data wrapper | |
| """ | |
| self._data_wrapper = data_wrapper | |
| def serialize(self, body_value): | |
| if (isinstance(body_value, dict) and 'data' not in body_value and | |
| self._data_wrapper): | |
| body_value = {'data': body_value} | |
| return json.dumps(body_value) | |
| def deserialize(self, content): | |
| try: | |
| content = content.decode('utf-8') | |
| except AttributeError: | |
| pass | |
| body = json.loads(content) | |
| if self._data_wrapper and isinstance(body, dict) and 'data' in body: | |
| body = body['data'] | |
| return body | |
| @property | |
| def no_content_response(self): | |
| return {} | |
| class RawModel(JsonModel): | |
| """Model class for requests that don't return JSON. | |
| Serializes and de-serializes between JSON and the Python | |
| object representation of HTTP request, and returns the raw bytes | |
| of the response body. | |
| """ | |
| accept = '*/*' | |
| content_type = 'application/json' | |
| alt_param = None | |
| def deserialize(self, content): | |
| return content | |
| @property | |
| def no_content_response(self): | |
| return '' | |
| class MediaModel(JsonModel): | |
| """Model class for requests that return Media. | |
| Serializes and de-serializes between JSON and the Python | |
| object representation of HTTP request, and returns the raw bytes | |
| of the response body. | |
| """ | |
| accept = '*/*' | |
| content_type = 'application/json' | |
| alt_param = 'media' | |
| def deserialize(self, content): | |
| return content | |
| @property | |
| def no_content_response(self): | |
| return '' | |
| class ProtocolBufferModel(BaseModel): | |
| """Model class for protocol buffers. | |
| Serializes and de-serializes the binary protocol buffer sent in the HTTP | |
| request and response bodies. | |
| """ | |
| accept = 'application/x-protobuf' | |
| content_type = 'application/x-protobuf' | |
| alt_param = 'proto' | |
| def __init__(self, protocol_buffer): | |
| """Constructs a ProtocolBufferModel. | |
| The serialzed protocol buffer returned in an HTTP response will be | |
| de-serialized using the given protocol buffer class. | |
| Args: | |
| protocol_buffer: The protocol buffer class used to de-serialize a | |
| response from the API. | |
| """ | |
| self._protocol_buffer = protocol_buffer | |
| def serialize(self, body_value): | |
| return body_value.SerializeToString() | |
| def deserialize(self, content): | |
| return self._protocol_buffer.FromString(content) | |
| @property | |
| def no_content_response(self): | |
| return self._protocol_buffer() | |
| def makepatch(original, modified): | |
| """Create a patch object. | |
| Some methods support PATCH, an efficient way to send updates to a resource. | |
| This method allows the easy construction of patch bodies by looking at the | |
| differences between a resource before and after it was modified. | |
| Args: | |
| original: object, the original deserialized resource | |
| modified: object, the modified deserialized resource | |
| Returns: | |
| An object that contains only the changes from original to modified, in a | |
| form suitable to pass to a PATCH method. | |
| Example usage: | |
| item = service.activities().get(postid=postid, userid=userid).execute() | |
| original = copy.deepcopy(item) | |
| item['object']['content'] = 'This is updated.' | |
| service.activities.patch(postid=postid, userid=userid, | |
| body=makepatch(original, item)).execute() | |
| """ | |
| patch = {} | |
| for key, original_value in six.iteritems(original): | |
| modified_value = modified.get(key, None) | |
| if modified_value is None: | |
| # Use None to signal that the element is deleted | |
| patch[key] = None | |
| elif original_value != modified_value: | |
| if type(original_value) == type({}): | |
| # Recursively descend objects | |
| patch[key] = makepatch(original_value, modified_value) | |
| else: | |
| # In the case of simple types or arrays we just replace | |
| patch[key] = modified_value | |
| else: | |
| # Don't add anything to patch if there's no change | |
| pass | |
| for key in modified: | |
| if key not in original: | |
| patch[key] = modified[key] | |
| return patch |