Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/googleapiclient/model.py: 39%

160 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-12-08 06:51 +0000

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 

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""" 

22from __future__ import absolute_import 

23 

24__author__ = "jcgregorio@google.com (Joe Gregorio)" 

25 

26import json 

27import logging 

28import platform 

29import urllib 

30 

31from googleapiclient import version as googleapiclient_version 

32from googleapiclient.errors import HttpError 

33 

34_LIBRARY_VERSION = googleapiclient_version.__version__ 

35_PY_VERSION = platform.python_version() 

36 

37LOGGER = logging.getLogger(__name__) 

38 

39dump_request_response = False 

40 

41 

42def _abstract(): 

43 raise NotImplementedError("You need to override this function") 

44 

45 

46class Model(object): 

47 """Model base class. 

48 

49 All Model classes should implement this interface. 

50 The Model serializes and de-serializes between a wire 

51 format such as JSON and a Python object representation. 

52 """ 

53 

54 def request(self, headers, path_params, query_params, body_value): 

55 """Updates outgoing requests with a serialized body. 

56 

57 Args: 

58 headers: dict, request headers 

59 path_params: dict, parameters that appear in the request path 

60 query_params: dict, parameters that appear in the query 

61 body_value: object, the request body as a Python object, which must be 

62 serializable. 

63 Returns: 

64 A tuple of (headers, path_params, query, body) 

65 

66 headers: dict, request headers 

67 path_params: dict, parameters that appear in the request path 

68 query: string, query part of the request URI 

69 body: string, the body serialized in the desired wire format. 

70 """ 

71 _abstract() 

72 

73 def response(self, resp, content): 

74 """Convert the response wire format into a Python object. 

75 

76 Args: 

77 resp: httplib2.Response, the HTTP response headers and status 

78 content: string, the body of the HTTP response 

79 

80 Returns: 

81 The body de-serialized as a Python object. 

82 

83 Raises: 

84 googleapiclient.errors.HttpError if a non 2xx response is received. 

85 """ 

86 _abstract() 

87 

88 

89class BaseModel(Model): 

90 """Base model class. 

91 

92 Subclasses should provide implementations for the "serialize" and 

93 "deserialize" methods, as well as values for the following class attributes. 

94 

95 Attributes: 

96 accept: The value to use for the HTTP Accept header. 

97 content_type: The value to use for the HTTP Content-type header. 

98 no_content_response: The value to return when deserializing a 204 "No 

99 Content" response. 

100 alt_param: The value to supply as the "alt" query parameter for requests. 

101 """ 

102 

103 accept = None 

104 content_type = None 

105 no_content_response = None 

106 alt_param = None 

107 

108 def _log_request(self, headers, path_params, query, body): 

109 """Logs debugging information about the request if requested.""" 

110 if dump_request_response: 

111 LOGGER.info("--request-start--") 

112 LOGGER.info("-headers-start-") 

113 for h, v in headers.items(): 

114 LOGGER.info("%s: %s", h, v) 

115 LOGGER.info("-headers-end-") 

116 LOGGER.info("-path-parameters-start-") 

117 for h, v in path_params.items(): 

118 LOGGER.info("%s: %s", h, v) 

119 LOGGER.info("-path-parameters-end-") 

120 LOGGER.info("body: %s", body) 

121 LOGGER.info("query: %s", query) 

122 LOGGER.info("--request-end--") 

123 

124 def request(self, headers, path_params, query_params, body_value): 

125 """Updates outgoing requests with a serialized body. 

126 

127 Args: 

128 headers: dict, request headers 

129 path_params: dict, parameters that appear in the request path 

130 query_params: dict, parameters that appear in the query 

131 body_value: object, the request body as a Python object, which must be 

132 serializable by json. 

133 Returns: 

134 A tuple of (headers, path_params, query, body) 

135 

136 headers: dict, request headers 

137 path_params: dict, parameters that appear in the request path 

138 query: string, query part of the request URI 

139 body: string, the body serialized as JSON 

140 """ 

141 query = self._build_query(query_params) 

142 headers["accept"] = self.accept 

143 headers["accept-encoding"] = "gzip, deflate" 

144 if "user-agent" in headers: 

145 headers["user-agent"] += " " 

146 else: 

147 headers["user-agent"] = "" 

148 headers["user-agent"] += "(gzip)" 

149 if "x-goog-api-client" in headers: 

150 headers["x-goog-api-client"] += " " 

151 else: 

152 headers["x-goog-api-client"] = "" 

153 headers["x-goog-api-client"] += "gdcl/%s gl-python/%s" % ( 

154 _LIBRARY_VERSION, 

155 _PY_VERSION, 

156 ) 

157 

158 if body_value is not None: 

159 headers["content-type"] = self.content_type 

160 body_value = self.serialize(body_value) 

161 self._log_request(headers, path_params, query, body_value) 

162 return (headers, path_params, query, body_value) 

163 

164 def _build_query(self, params): 

165 """Builds a query string. 

166 

167 Args: 

168 params: dict, the query parameters 

169 

170 Returns: 

171 The query parameters properly encoded into an HTTP URI query string. 

172 """ 

173 if self.alt_param is not None: 

174 params.update({"alt": self.alt_param}) 

175 astuples = [] 

176 for key, value in params.items(): 

177 if type(value) == type([]): 

178 for x in value: 

179 x = x.encode("utf-8") 

180 astuples.append((key, x)) 

181 else: 

182 if isinstance(value, str) and callable(value.encode): 

183 value = value.encode("utf-8") 

184 astuples.append((key, value)) 

185 return "?" + urllib.parse.urlencode(astuples) 

186 

187 def _log_response(self, resp, content): 

188 """Logs debugging information about the response if requested.""" 

189 if dump_request_response: 

190 LOGGER.info("--response-start--") 

191 for h, v in resp.items(): 

192 LOGGER.info("%s: %s", h, v) 

193 if content: 

194 LOGGER.info(content) 

195 LOGGER.info("--response-end--") 

196 

197 def response(self, resp, content): 

198 """Convert the response wire format into a Python object. 

199 

200 Args: 

201 resp: httplib2.Response, the HTTP response headers and status 

202 content: string, the body of the HTTP response 

203 

204 Returns: 

205 The body de-serialized as a Python object. 

206 

207 Raises: 

208 googleapiclient.errors.HttpError if a non 2xx response is received. 

209 """ 

210 self._log_response(resp, content) 

211 # Error handling is TBD, for example, do we retry 

212 # for some operation/error combinations? 

213 if resp.status < 300: 

214 if resp.status == 204: 

215 # A 204: No Content response should be treated differently 

216 # to all the other success states 

217 return self.no_content_response 

218 return self.deserialize(content) 

219 else: 

220 LOGGER.debug("Content from bad request was: %r" % content) 

221 raise HttpError(resp, content) 

222 

223 def serialize(self, body_value): 

224 """Perform the actual Python object serialization. 

225 

226 Args: 

227 body_value: object, the request body as a Python object. 

228 

229 Returns: 

230 string, the body in serialized form. 

231 """ 

232 _abstract() 

233 

234 def deserialize(self, content): 

235 """Perform the actual deserialization from response string to Python 

236 object. 

237 

238 Args: 

239 content: string, the body of the HTTP response 

240 

241 Returns: 

242 The body de-serialized as a Python object. 

243 """ 

244 _abstract() 

245 

246 

247class JsonModel(BaseModel): 

248 """Model class for JSON. 

249 

250 Serializes and de-serializes between JSON and the Python 

251 object representation of HTTP request and response bodies. 

252 """ 

253 

254 accept = "application/json" 

255 content_type = "application/json" 

256 alt_param = "json" 

257 

258 def __init__(self, data_wrapper=False): 

259 """Construct a JsonModel. 

260 

261 Args: 

262 data_wrapper: boolean, wrap requests and responses in a data wrapper 

263 """ 

264 self._data_wrapper = data_wrapper 

265 

266 def serialize(self, body_value): 

267 if ( 

268 isinstance(body_value, dict) 

269 and "data" not in body_value 

270 and self._data_wrapper 

271 ): 

272 body_value = {"data": body_value} 

273 return json.dumps(body_value) 

274 

275 def deserialize(self, content): 

276 try: 

277 content = content.decode("utf-8") 

278 except AttributeError: 

279 pass 

280 try: 

281 body = json.loads(content) 

282 except json.decoder.JSONDecodeError: 

283 body = content 

284 else: 

285 if self._data_wrapper and "data" in body: 

286 body = body["data"] 

287 return body 

288 

289 @property 

290 def no_content_response(self): 

291 return {} 

292 

293 

294class RawModel(JsonModel): 

295 """Model class for requests that don't return JSON. 

296 

297 Serializes and de-serializes between JSON and the Python 

298 object representation of HTTP request, and returns the raw bytes 

299 of the response body. 

300 """ 

301 

302 accept = "*/*" 

303 content_type = "application/json" 

304 alt_param = None 

305 

306 def deserialize(self, content): 

307 return content 

308 

309 @property 

310 def no_content_response(self): 

311 return "" 

312 

313 

314class MediaModel(JsonModel): 

315 """Model class for requests that return Media. 

316 

317 Serializes and de-serializes between JSON and the Python 

318 object representation of HTTP request, and returns the raw bytes 

319 of the response body. 

320 """ 

321 

322 accept = "*/*" 

323 content_type = "application/json" 

324 alt_param = "media" 

325 

326 def deserialize(self, content): 

327 return content 

328 

329 @property 

330 def no_content_response(self): 

331 return "" 

332 

333 

334class ProtocolBufferModel(BaseModel): 

335 """Model class for protocol buffers. 

336 

337 Serializes and de-serializes the binary protocol buffer sent in the HTTP 

338 request and response bodies. 

339 """ 

340 

341 accept = "application/x-protobuf" 

342 content_type = "application/x-protobuf" 

343 alt_param = "proto" 

344 

345 def __init__(self, protocol_buffer): 

346 """Constructs a ProtocolBufferModel. 

347 

348 The serialized protocol buffer returned in an HTTP response will be 

349 de-serialized using the given protocol buffer class. 

350 

351 Args: 

352 protocol_buffer: The protocol buffer class used to de-serialize a 

353 response from the API. 

354 """ 

355 self._protocol_buffer = protocol_buffer 

356 

357 def serialize(self, body_value): 

358 return body_value.SerializeToString() 

359 

360 def deserialize(self, content): 

361 return self._protocol_buffer.FromString(content) 

362 

363 @property 

364 def no_content_response(self): 

365 return self._protocol_buffer() 

366 

367 

368def makepatch(original, modified): 

369 """Create a patch object. 

370 

371 Some methods support PATCH, an efficient way to send updates to a resource. 

372 This method allows the easy construction of patch bodies by looking at the 

373 differences between a resource before and after it was modified. 

374 

375 Args: 

376 original: object, the original deserialized resource 

377 modified: object, the modified deserialized resource 

378 Returns: 

379 An object that contains only the changes from original to modified, in a 

380 form suitable to pass to a PATCH method. 

381 

382 Example usage: 

383 item = service.activities().get(postid=postid, userid=userid).execute() 

384 original = copy.deepcopy(item) 

385 item['object']['content'] = 'This is updated.' 

386 service.activities.patch(postid=postid, userid=userid, 

387 body=makepatch(original, item)).execute() 

388 """ 

389 patch = {} 

390 for key, original_value in original.items(): 

391 modified_value = modified.get(key, None) 

392 if modified_value is None: 

393 # Use None to signal that the element is deleted 

394 patch[key] = None 

395 elif original_value != modified_value: 

396 if type(original_value) == type({}): 

397 # Recursively descend objects 

398 patch[key] = makepatch(original_value, modified_value) 

399 else: 

400 # In the case of simple types or arrays we just replace 

401 patch[key] = modified_value 

402 else: 

403 # Don't add anything to patch if there's no change 

404 pass 

405 for key in modified: 

406 if key not in original: 

407 patch[key] = modified[key] 

408 

409 return patch