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

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

170 statements  

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 

30import warnings 

31 

32from googleapiclient import version as googleapiclient_version 

33from googleapiclient.errors import HttpError 

34 

35try: 

36 from google.api_core.version_header import API_VERSION_METADATA_KEY 

37 

38 HAS_API_VERSION = True 

39except ImportError: 

40 HAS_API_VERSION = False 

41 

42_LIBRARY_VERSION = googleapiclient_version.__version__ 

43_PY_VERSION = platform.python_version() 

44 

45LOGGER = logging.getLogger(__name__) 

46 

47dump_request_response = False 

48 

49 

50def _abstract(): 

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

52 

53 

54class Model(object): 

55 """Model base class. 

56 

57 All Model classes should implement this interface. 

58 The Model serializes and de-serializes between a wire 

59 format such as JSON and a Python object representation. 

60 """ 

61 

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

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

64 

65 Args: 

66 headers: dict, request headers 

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

68 query_params: dict, parameters that appear in the query 

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

70 serializable. 

71 Returns: 

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

73 

74 headers: dict, request headers 

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

76 query: string, query part of the request URI 

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

78 """ 

79 _abstract() 

80 

81 def response(self, resp, content): 

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

83 

84 Args: 

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

86 content: string, the body of the HTTP response 

87 

88 Returns: 

89 The body de-serialized as a Python object. 

90 

91 Raises: 

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

93 """ 

94 _abstract() 

95 

96 

97class BaseModel(Model): 

98 """Base model class. 

99 

100 Subclasses should provide implementations for the "serialize" and 

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

102 

103 Attributes: 

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

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

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

107 Content" response. 

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

109 """ 

110 

111 accept = None 

112 content_type = None 

113 no_content_response = None 

114 alt_param = None 

115 

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

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

118 if dump_request_response: 

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

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

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

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

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

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

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

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

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

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

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

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

131 

132 def request(self, headers, path_params, query_params, body_value, api_version=None): 

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

134 

135 Args: 

136 headers: dict, request headers 

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

138 query_params: dict, parameters that appear in the query 

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

140 serializable by json. 

141 api_version: str, The precise API version represented by this request, 

142 which will result in an API Version header being sent along with the 

143 HTTP request. 

144 Returns: 

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

146 

147 headers: dict, request headers 

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

149 query: string, query part of the request URI 

150 body: string, the body serialized as JSON 

151 """ 

152 query = self._build_query(query_params) 

153 headers["accept"] = self.accept 

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

155 if "user-agent" in headers: 

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

157 else: 

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

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

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

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

162 else: 

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

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

165 _LIBRARY_VERSION, 

166 _PY_VERSION, 

167 ) 

168 

169 if api_version and HAS_API_VERSION: 

170 headers[API_VERSION_METADATA_KEY] = api_version 

171 elif api_version: 

172 warnings.warn( 

173 "The `api_version` argument is ignored as a newer version of " 

174 "`google-api-core` is required to use this feature." 

175 "Please upgrade `google-api-core` to 2.19.0 or newer." 

176 ) 

177 

178 if body_value is not None: 

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

180 body_value = self.serialize(body_value) 

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

182 return (headers, path_params, query, body_value) 

183 

184 def _build_query(self, params): 

185 """Builds a query string. 

186 

187 Args: 

188 params: dict, the query parameters 

189 

190 Returns: 

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

192 """ 

193 if self.alt_param is not None: 

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

195 astuples = [] 

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

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

198 for x in value: 

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

200 astuples.append((key, x)) 

201 else: 

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

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

204 astuples.append((key, value)) 

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

206 

207 def _log_response(self, resp, content): 

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

209 if dump_request_response: 

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

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

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

213 if content: 

214 LOGGER.info(content) 

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

216 

217 def response(self, resp, content): 

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

219 

220 Args: 

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

222 content: string, the body of the HTTP response 

223 

224 Returns: 

225 The body de-serialized as a Python object. 

226 

227 Raises: 

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

229 """ 

230 self._log_response(resp, content) 

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

232 # for some operation/error combinations? 

233 if resp.status < 300: 

234 if resp.status == 204: 

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

236 # to all the other success states 

237 return self.no_content_response 

238 return self.deserialize(content) 

239 else: 

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

241 raise HttpError(resp, content) 

242 

243 def serialize(self, body_value): 

244 """Perform the actual Python object serialization. 

245 

246 Args: 

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

248 

249 Returns: 

250 string, the body in serialized form. 

251 """ 

252 _abstract() 

253 

254 def deserialize(self, content): 

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

256 object. 

257 

258 Args: 

259 content: string, the body of the HTTP response 

260 

261 Returns: 

262 The body de-serialized as a Python object. 

263 """ 

264 _abstract() 

265 

266 

267class JsonModel(BaseModel): 

268 """Model class for JSON. 

269 

270 Serializes and de-serializes between JSON and the Python 

271 object representation of HTTP request and response bodies. 

272 """ 

273 

274 accept = "application/json" 

275 content_type = "application/json" 

276 alt_param = "json" 

277 

278 def __init__(self, data_wrapper=False): 

279 """Construct a JsonModel. 

280 

281 Args: 

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

283 """ 

284 self._data_wrapper = data_wrapper 

285 

286 def serialize(self, body_value): 

287 if ( 

288 isinstance(body_value, dict) 

289 and "data" not in body_value 

290 and self._data_wrapper 

291 ): 

292 body_value = {"data": body_value} 

293 return json.dumps(body_value) 

294 

295 def deserialize(self, content): 

296 try: 

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

298 except AttributeError: 

299 pass 

300 try: 

301 body = json.loads(content) 

302 except json.decoder.JSONDecodeError: 

303 body = content 

304 else: 

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

306 body = body["data"] 

307 return body 

308 

309 @property 

310 def no_content_response(self): 

311 return {} 

312 

313 

314class RawModel(JsonModel): 

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

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 = None 

325 

326 def deserialize(self, content): 

327 return content 

328 

329 @property 

330 def no_content_response(self): 

331 return "" 

332 

333 

334class MediaModel(JsonModel): 

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

336 

337 Serializes and de-serializes between JSON and the Python 

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

339 of the response body. 

340 """ 

341 

342 accept = "*/*" 

343 content_type = "application/json" 

344 alt_param = "media" 

345 

346 def deserialize(self, content): 

347 return content 

348 

349 @property 

350 def no_content_response(self): 

351 return "" 

352 

353 

354class ProtocolBufferModel(BaseModel): 

355 """Model class for protocol buffers. 

356 

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

358 request and response bodies. 

359 """ 

360 

361 accept = "application/x-protobuf" 

362 content_type = "application/x-protobuf" 

363 alt_param = "proto" 

364 

365 def __init__(self, protocol_buffer): 

366 """Constructs a ProtocolBufferModel. 

367 

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

369 de-serialized using the given protocol buffer class. 

370 

371 Args: 

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

373 response from the API. 

374 """ 

375 self._protocol_buffer = protocol_buffer 

376 

377 def serialize(self, body_value): 

378 return body_value.SerializeToString() 

379 

380 def deserialize(self, content): 

381 return self._protocol_buffer.FromString(content) 

382 

383 @property 

384 def no_content_response(self): 

385 return self._protocol_buffer() 

386 

387 

388def makepatch(original, modified): 

389 """Create a patch object. 

390 

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

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

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

394 

395 Args: 

396 original: object, the original deserialized resource 

397 modified: object, the modified deserialized resource 

398 Returns: 

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

400 form suitable to pass to a PATCH method. 

401 

402 Example usage: 

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

404 original = copy.deepcopy(item) 

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

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

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

408 """ 

409 patch = {} 

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

411 modified_value = modified.get(key, None) 

412 if modified_value is None: 

413 # Use None to signal that the element is deleted 

414 patch[key] = None 

415 elif original_value != modified_value: 

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

417 # Recursively descend objects 

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

419 else: 

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

421 patch[key] = modified_value 

422 else: 

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

424 pass 

425 for key in modified: 

426 if key not in original: 

427 patch[key] = modified[key] 

428 

429 return patch