model.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  1. # Copyright 2014 Google Inc. All Rights Reserved.
  2. #
  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. """Model objects for requests and responses.
  15. Each API may support one or more serializations, such
  16. as JSON, Atom, etc. The model classes are responsible
  17. for converting between the wire format and the Python
  18. object representation.
  19. """
  20. from __future__ import absolute_import
  21. __author__ = "jcgregorio@google.com (Joe Gregorio)"
  22. import json
  23. import logging
  24. import platform
  25. import urllib
  26. from googleapiclient import version as googleapiclient_version
  27. from googleapiclient.errors import HttpError
  28. _LIBRARY_VERSION = googleapiclient_version.__version__
  29. _PY_VERSION = platform.python_version()
  30. LOGGER = logging.getLogger(__name__)
  31. dump_request_response = False
  32. def _abstract():
  33. raise NotImplementedError("You need to override this function")
  34. class Model(object):
  35. """Model base class.
  36. All Model classes should implement this interface.
  37. The Model serializes and de-serializes between a wire
  38. format such as JSON and a Python object representation.
  39. """
  40. def request(self, headers, path_params, query_params, body_value):
  41. """Updates outgoing requests with a serialized body.
  42. Args:
  43. headers: dict, request headers
  44. path_params: dict, parameters that appear in the request path
  45. query_params: dict, parameters that appear in the query
  46. body_value: object, the request body as a Python object, which must be
  47. serializable.
  48. Returns:
  49. A tuple of (headers, path_params, query, body)
  50. headers: dict, request headers
  51. path_params: dict, parameters that appear in the request path
  52. query: string, query part of the request URI
  53. body: string, the body serialized in the desired wire format.
  54. """
  55. _abstract()
  56. def response(self, resp, content):
  57. """Convert the response wire format into a Python object.
  58. Args:
  59. resp: httplib2.Response, the HTTP response headers and status
  60. content: string, the body of the HTTP response
  61. Returns:
  62. The body de-serialized as a Python object.
  63. Raises:
  64. googleapiclient.errors.HttpError if a non 2xx response is received.
  65. """
  66. _abstract()
  67. class BaseModel(Model):
  68. """Base model class.
  69. Subclasses should provide implementations for the "serialize" and
  70. "deserialize" methods, as well as values for the following class attributes.
  71. Attributes:
  72. accept: The value to use for the HTTP Accept header.
  73. content_type: The value to use for the HTTP Content-type header.
  74. no_content_response: The value to return when deserializing a 204 "No
  75. Content" response.
  76. alt_param: The value to supply as the "alt" query parameter for requests.
  77. """
  78. accept = None
  79. content_type = None
  80. no_content_response = None
  81. alt_param = None
  82. def _log_request(self, headers, path_params, query, body):
  83. """Logs debugging information about the request if requested."""
  84. if dump_request_response:
  85. LOGGER.info("--request-start--")
  86. LOGGER.info("-headers-start-")
  87. for h, v in headers.items():
  88. LOGGER.info("%s: %s", h, v)
  89. LOGGER.info("-headers-end-")
  90. LOGGER.info("-path-parameters-start-")
  91. for h, v in path_params.items():
  92. LOGGER.info("%s: %s", h, v)
  93. LOGGER.info("-path-parameters-end-")
  94. LOGGER.info("body: %s", body)
  95. LOGGER.info("query: %s", query)
  96. LOGGER.info("--request-end--")
  97. def request(self, headers, path_params, query_params, body_value):
  98. """Updates outgoing requests with a serialized body.
  99. Args:
  100. headers: dict, request headers
  101. path_params: dict, parameters that appear in the request path
  102. query_params: dict, parameters that appear in the query
  103. body_value: object, the request body as a Python object, which must be
  104. serializable by json.
  105. Returns:
  106. A tuple of (headers, path_params, query, body)
  107. headers: dict, request headers
  108. path_params: dict, parameters that appear in the request path
  109. query: string, query part of the request URI
  110. body: string, the body serialized as JSON
  111. """
  112. query = self._build_query(query_params)
  113. headers["accept"] = self.accept
  114. headers["accept-encoding"] = "gzip, deflate"
  115. if "user-agent" in headers:
  116. headers["user-agent"] += " "
  117. else:
  118. headers["user-agent"] = ""
  119. headers["user-agent"] += "(gzip)"
  120. if "x-goog-api-client" in headers:
  121. headers["x-goog-api-client"] += " "
  122. else:
  123. headers["x-goog-api-client"] = ""
  124. headers["x-goog-api-client"] += "gdcl/%s gl-python/%s" % (
  125. _LIBRARY_VERSION,
  126. _PY_VERSION,
  127. )
  128. if body_value is not None:
  129. headers["content-type"] = self.content_type
  130. body_value = self.serialize(body_value)
  131. self._log_request(headers, path_params, query, body_value)
  132. return (headers, path_params, query, body_value)
  133. def _build_query(self, params):
  134. """Builds a query string.
  135. Args:
  136. params: dict, the query parameters
  137. Returns:
  138. The query parameters properly encoded into an HTTP URI query string.
  139. """
  140. if self.alt_param is not None:
  141. params.update({"alt": self.alt_param})
  142. astuples = []
  143. for key, value in params.items():
  144. if type(value) == type([]):
  145. for x in value:
  146. x = x.encode("utf-8")
  147. astuples.append((key, x))
  148. else:
  149. if isinstance(value, str) and callable(value.encode):
  150. value = value.encode("utf-8")
  151. astuples.append((key, value))
  152. return "?" + urllib.parse.urlencode(astuples)
  153. def _log_response(self, resp, content):
  154. """Logs debugging information about the response if requested."""
  155. if dump_request_response:
  156. LOGGER.info("--response-start--")
  157. for h, v in resp.items():
  158. LOGGER.info("%s: %s", h, v)
  159. if content:
  160. LOGGER.info(content)
  161. LOGGER.info("--response-end--")
  162. def response(self, resp, content):
  163. """Convert the response wire format into a Python object.
  164. Args:
  165. resp: httplib2.Response, the HTTP response headers and status
  166. content: string, the body of the HTTP response
  167. Returns:
  168. The body de-serialized as a Python object.
  169. Raises:
  170. googleapiclient.errors.HttpError if a non 2xx response is received.
  171. """
  172. self._log_response(resp, content)
  173. # Error handling is TBD, for example, do we retry
  174. # for some operation/error combinations?
  175. if resp.status < 300:
  176. if resp.status == 204:
  177. # A 204: No Content response should be treated differently
  178. # to all the other success states
  179. return self.no_content_response
  180. return self.deserialize(content)
  181. else:
  182. LOGGER.debug("Content from bad request was: %r" % content)
  183. raise HttpError(resp, content)
  184. def serialize(self, body_value):
  185. """Perform the actual Python object serialization.
  186. Args:
  187. body_value: object, the request body as a Python object.
  188. Returns:
  189. string, the body in serialized form.
  190. """
  191. _abstract()
  192. def deserialize(self, content):
  193. """Perform the actual deserialization from response string to Python
  194. object.
  195. Args:
  196. content: string, the body of the HTTP response
  197. Returns:
  198. The body de-serialized as a Python object.
  199. """
  200. _abstract()
  201. class JsonModel(BaseModel):
  202. """Model class for JSON.
  203. Serializes and de-serializes between JSON and the Python
  204. object representation of HTTP request and response bodies.
  205. """
  206. accept = "application/json"
  207. content_type = "application/json"
  208. alt_param = "json"
  209. def __init__(self, data_wrapper=False):
  210. """Construct a JsonModel.
  211. Args:
  212. data_wrapper: boolean, wrap requests and responses in a data wrapper
  213. """
  214. self._data_wrapper = data_wrapper
  215. def serialize(self, body_value):
  216. if (
  217. isinstance(body_value, dict)
  218. and "data" not in body_value
  219. and self._data_wrapper
  220. ):
  221. body_value = {"data": body_value}
  222. return json.dumps(body_value)
  223. def deserialize(self, content):
  224. try:
  225. content = content.decode("utf-8")
  226. except AttributeError:
  227. pass
  228. try:
  229. body = json.loads(content)
  230. except json.decoder.JSONDecodeError:
  231. body = content
  232. else:
  233. if self._data_wrapper and "data" in body:
  234. body = body["data"]
  235. return body
  236. @property
  237. def no_content_response(self):
  238. return {}
  239. class RawModel(JsonModel):
  240. """Model class for requests that don't return JSON.
  241. Serializes and de-serializes between JSON and the Python
  242. object representation of HTTP request, and returns the raw bytes
  243. of the response body.
  244. """
  245. accept = "*/*"
  246. content_type = "application/json"
  247. alt_param = None
  248. def deserialize(self, content):
  249. return content
  250. @property
  251. def no_content_response(self):
  252. return ""
  253. class MediaModel(JsonModel):
  254. """Model class for requests that return Media.
  255. Serializes and de-serializes between JSON and the Python
  256. object representation of HTTP request, and returns the raw bytes
  257. of the response body.
  258. """
  259. accept = "*/*"
  260. content_type = "application/json"
  261. alt_param = "media"
  262. def deserialize(self, content):
  263. return content
  264. @property
  265. def no_content_response(self):
  266. return ""
  267. class ProtocolBufferModel(BaseModel):
  268. """Model class for protocol buffers.
  269. Serializes and de-serializes the binary protocol buffer sent in the HTTP
  270. request and response bodies.
  271. """
  272. accept = "application/x-protobuf"
  273. content_type = "application/x-protobuf"
  274. alt_param = "proto"
  275. def __init__(self, protocol_buffer):
  276. """Constructs a ProtocolBufferModel.
  277. The serialized protocol buffer returned in an HTTP response will be
  278. de-serialized using the given protocol buffer class.
  279. Args:
  280. protocol_buffer: The protocol buffer class used to de-serialize a
  281. response from the API.
  282. """
  283. self._protocol_buffer = protocol_buffer
  284. def serialize(self, body_value):
  285. return body_value.SerializeToString()
  286. def deserialize(self, content):
  287. return self._protocol_buffer.FromString(content)
  288. @property
  289. def no_content_response(self):
  290. return self._protocol_buffer()
  291. def makepatch(original, modified):
  292. """Create a patch object.
  293. Some methods support PATCH, an efficient way to send updates to a resource.
  294. This method allows the easy construction of patch bodies by looking at the
  295. differences between a resource before and after it was modified.
  296. Args:
  297. original: object, the original deserialized resource
  298. modified: object, the modified deserialized resource
  299. Returns:
  300. An object that contains only the changes from original to modified, in a
  301. form suitable to pass to a PATCH method.
  302. Example usage:
  303. item = service.activities().get(postid=postid, userid=userid).execute()
  304. original = copy.deepcopy(item)
  305. item['object']['content'] = 'This is updated.'
  306. service.activities.patch(postid=postid, userid=userid,
  307. body=makepatch(original, item)).execute()
  308. """
  309. patch = {}
  310. for key, original_value in original.items():
  311. modified_value = modified.get(key, None)
  312. if modified_value is None:
  313. # Use None to signal that the element is deleted
  314. patch[key] = None
  315. elif original_value != modified_value:
  316. if type(original_value) == type({}):
  317. # Recursively descend objects
  318. patch[key] = makepatch(original_value, modified_value)
  319. else:
  320. # In the case of simple types or arrays we just replace
  321. patch[key] = modified_value
  322. else:
  323. # Don't add anything to patch if there's no change
  324. pass
  325. for key in modified:
  326. if key not in original:
  327. patch[key] = modified[key]
  328. return patch