Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(69)

Side by Side Diff: third_party/google-endpoints/apitools/base/py/batch.py

Issue 2666783008: Add google-endpoints to third_party/. (Closed)
Patch Set: Created 3 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 #!/usr/bin/env python
2 #
3 # Copyright 2015 Google Inc.
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16
17 """Library for handling batch HTTP requests for apitools."""
18
19 import collections
20 import email.generator as generator
21 import email.mime.multipart as mime_multipart
22 import email.mime.nonmultipart as mime_nonmultipart
23 import email.parser as email_parser
24 import itertools
25 import time
26 import uuid
27
28 import six
29 from six.moves import http_client
30 from six.moves import urllib_parse
31
32 from apitools.base.py import exceptions
33 from apitools.base.py import http_wrapper
34
35 __all__ = [
36 'BatchApiRequest',
37 ]
38
39
40 class RequestResponseAndHandler(collections.namedtuple(
41 'RequestResponseAndHandler', ['request', 'response', 'handler'])):
42
43 """Container for data related to completing an HTTP request.
44
45 This contains an HTTP request, its response, and a callback for handling
46 the response from the server.
47
48 Attributes:
49 request: An http_wrapper.Request object representing the HTTP request.
50 response: The http_wrapper.Response object returned from the server.
51 handler: A callback function accepting two arguments, response
52 and exception. Response is an http_wrapper.Response object, and
53 exception is an apiclient.errors.HttpError object if an error
54 occurred, or otherwise None.
55 """
56
57
58 class BatchApiRequest(object):
59
60 class ApiCall(object):
61
62 """Holds request and response information for each request.
63
64 ApiCalls are ultimately exposed to the client once the HTTP
65 batch request has been completed.
66
67 Attributes:
68 http_request: A client-supplied http_wrapper.Request to be
69 submitted to the server.
70 response: A http_wrapper.Response object given by the server as a
71 response to the user request, or None if an error occurred.
72 exception: An apiclient.errors.HttpError object if an error
73 occurred, or None.
74
75 """
76
77 def __init__(self, request, retryable_codes, service, method_config):
78 """Initialize an individual API request.
79
80 Args:
81 request: An http_wrapper.Request object.
82 retryable_codes: A list of integer HTTP codes that can
83 be retried.
84 service: A service inheriting from base_api.BaseApiService.
85 method_config: Method config for the desired API request.
86
87 """
88 self.__retryable_codes = list(
89 set(retryable_codes + [http_client.UNAUTHORIZED]))
90 self.__http_response = None
91 self.__service = service
92 self.__method_config = method_config
93
94 self.http_request = request
95 # TODO(user): Add some validation to these fields.
96 self.__response = None
97 self.__exception = None
98
99 @property
100 def is_error(self):
101 return self.exception is not None
102
103 @property
104 def response(self):
105 return self.__response
106
107 @property
108 def exception(self):
109 return self.__exception
110
111 @property
112 def authorization_failed(self):
113 return (self.__http_response and (
114 self.__http_response.status_code == http_client.UNAUTHORIZED))
115
116 @property
117 def terminal_state(self):
118 if self.__http_response is None:
119 return False
120 response_code = self.__http_response.status_code
121 return response_code not in self.__retryable_codes
122
123 def HandleResponse(self, http_response, exception):
124 """Handles an incoming http response to the request in http_request.
125
126 This is intended to be used as a callback function for
127 BatchHttpRequest.Add.
128
129 Args:
130 http_response: Deserialized http_wrapper.Response object.
131 exception: apiclient.errors.HttpError object if an error
132 occurred.
133
134 """
135 self.__http_response = http_response
136 self.__exception = exception
137 if self.terminal_state and not self.__exception:
138 self.__response = self.__service.ProcessHttpResponse(
139 self.__method_config, self.__http_response)
140
141 def __init__(self, batch_url=None, retryable_codes=None):
142 """Initialize a batch API request object.
143
144 Args:
145 batch_url: Base URL for batch API calls.
146 retryable_codes: A list of integer HTTP codes that can be retried.
147 """
148 self.api_requests = []
149 self.retryable_codes = retryable_codes or []
150 self.batch_url = batch_url or 'https://www.googleapis.com/batch'
151
152 def Add(self, service, method, request, global_params=None):
153 """Add a request to the batch.
154
155 Args:
156 service: A class inheriting base_api.BaseApiService.
157 method: A string indicated desired method from the service. See
158 the example in the class docstring.
159 request: An input message appropriate for the specified
160 service.method.
161 global_params: Optional additional parameters to pass into
162 method.PrepareHttpRequest.
163
164 Returns:
165 None
166
167 """
168 # Retrieve the configs for the desired method and service.
169 method_config = service.GetMethodConfig(method)
170 upload_config = service.GetUploadConfig(method)
171
172 # Prepare the HTTP Request.
173 http_request = service.PrepareHttpRequest(
174 method_config, request, global_params=global_params,
175 upload_config=upload_config)
176
177 # Create the request and add it to our master list.
178 api_request = self.ApiCall(
179 http_request, self.retryable_codes, service, method_config)
180 self.api_requests.append(api_request)
181
182 def Execute(self, http, sleep_between_polls=5, max_retries=5):
183 """Execute all of the requests in the batch.
184
185 Args:
186 http: httplib2.Http object for use in the request.
187 sleep_between_polls: Integer number of seconds to sleep between
188 polls.
189 max_retries: Max retries. Any requests that have not succeeded by
190 this number of retries simply report the last response or
191 exception, whatever it happened to be.
192
193 Returns:
194 List of ApiCalls.
195 """
196 requests = [request for request in self.api_requests
197 if not request.terminal_state]
198
199 for attempt in range(max_retries):
200 if attempt:
201 time.sleep(sleep_between_polls)
202
203 # Create a batch_http_request object and populate it with
204 # incomplete requests.
205 batch_http_request = BatchHttpRequest(batch_url=self.batch_url)
206 for request in requests:
207 batch_http_request.Add(
208 request.http_request, request.HandleResponse)
209 batch_http_request.Execute(http)
210
211 # Collect retryable requests.
212 requests = [request for request in self.api_requests if not
213 request.terminal_state]
214
215 if hasattr(http.request, 'credentials'):
216 if any(request.authorization_failed for request in requests):
217 http.request.credentials.refresh(http)
218
219 if not requests:
220 break
221
222 return self.api_requests
223
224
225 class BatchHttpRequest(object):
226
227 """Batches multiple http_wrapper.Request objects into a single request."""
228
229 def __init__(self, batch_url, callback=None):
230 """Constructor for a BatchHttpRequest.
231
232 Args:
233 batch_url: URL to send batch requests to.
234 callback: A callback to be called for each response, of the
235 form callback(response, exception). The first parameter is
236 the deserialized Response object. The second is an
237 apiclient.errors.HttpError exception object if an HTTP error
238 occurred while processing the request, or None if no error
239 occurred.
240 """
241 # Endpoint to which these requests are sent.
242 self.__batch_url = batch_url
243
244 # Global callback to be called for each individual response in the
245 # batch.
246 self.__callback = callback
247
248 # List of requests, responses and handlers.
249 self.__request_response_handlers = {}
250
251 # The last auto generated id.
252 self.__last_auto_id = itertools.count()
253
254 # Unique ID on which to base the Content-ID headers.
255 self.__base_id = uuid.uuid4()
256
257 def _ConvertIdToHeader(self, request_id):
258 """Convert an id to a Content-ID header value.
259
260 Args:
261 request_id: String identifier for a individual request.
262
263 Returns:
264 A Content-ID header with the id_ encoded into it. A UUID is
265 prepended to the value because Content-ID headers are
266 supposed to be universally unique.
267
268 """
269 return '<%s+%s>' % (self.__base_id, urllib_parse.quote(request_id))
270
271 @staticmethod
272 def _ConvertHeaderToId(header):
273 """Convert a Content-ID header value to an id.
274
275 Presumes the Content-ID header conforms to the format that
276 _ConvertIdToHeader() returns.
277
278 Args:
279 header: A string indicating the Content-ID header value.
280
281 Returns:
282 The extracted id value.
283
284 Raises:
285 BatchError if the header is not in the expected format.
286 """
287 if not (header.startswith('<') or header.endswith('>')):
288 raise exceptions.BatchError(
289 'Invalid value for Content-ID: %s' % header)
290 if '+' not in header:
291 raise exceptions.BatchError(
292 'Invalid value for Content-ID: %s' % header)
293 _, request_id = header[1:-1].rsplit('+', 1)
294
295 return urllib_parse.unquote(request_id)
296
297 def _SerializeRequest(self, request):
298 """Convert a http_wrapper.Request object into a string.
299
300 Args:
301 request: A http_wrapper.Request to serialize.
302
303 Returns:
304 The request as a string in application/http format.
305 """
306 # Construct status line
307 parsed = urllib_parse.urlsplit(request.url)
308 request_line = urllib_parse.urlunsplit(
309 (None, None, parsed.path, parsed.query, None))
310 status_line = u' '.join((
311 request.http_method,
312 request_line.decode('utf-8'),
313 u'HTTP/1.1\n'
314 ))
315 major, minor = request.headers.get(
316 'content-type', 'application/json').split('/')
317 msg = mime_nonmultipart.MIMENonMultipart(major, minor)
318
319 # MIMENonMultipart adds its own Content-Type header.
320 # Keep all of the other headers in `request.headers`.
321 for key, value in request.headers.items():
322 if key == 'content-type':
323 continue
324 msg[key] = value
325
326 msg['Host'] = parsed.netloc
327 msg.set_unixfrom(None)
328
329 if request.body is not None:
330 msg.set_payload(request.body)
331
332 # Serialize the mime message.
333 str_io = six.StringIO()
334 # maxheaderlen=0 means don't line wrap headers.
335 gen = generator.Generator(str_io, maxheaderlen=0)
336 gen.flatten(msg, unixfrom=False)
337 body = str_io.getvalue()
338
339 return status_line + body
340
341 def _DeserializeResponse(self, payload):
342 """Convert string into Response and content.
343
344 Args:
345 payload: Header and body string to be deserialized.
346
347 Returns:
348 A Response object
349 """
350 # Strip off the status line.
351 status_line, payload = payload.split('\n', 1)
352 _, status, _ = status_line.split(' ', 2)
353
354 # Parse the rest of the response.
355 parser = email_parser.Parser()
356 msg = parser.parsestr(payload)
357
358 # Get the headers.
359 info = dict(msg)
360 info['status'] = status
361
362 # Create Response from the parsed headers.
363 content = msg.get_payload()
364
365 return http_wrapper.Response(info, content, self.__batch_url)
366
367 def _NewId(self):
368 """Create a new id.
369
370 Auto incrementing number that avoids conflicts with ids already used.
371
372 Returns:
373 A new unique id string.
374 """
375 return str(next(self.__last_auto_id))
376
377 def Add(self, request, callback=None):
378 """Add a new request.
379
380 Args:
381 request: A http_wrapper.Request to add to the batch.
382 callback: A callback to be called for this response, of the
383 form callback(response, exception). The first parameter is the
384 deserialized response object. The second is an
385 apiclient.errors.HttpError exception object if an HTTP error
386 occurred while processing the request, or None if no errors
387 occurred.
388
389 Returns:
390 None
391 """
392 handler = RequestResponseAndHandler(request, None, callback)
393 self.__request_response_handlers[self._NewId()] = handler
394
395 def _Execute(self, http):
396 """Serialize batch request, send to server, process response.
397
398 Args:
399 http: A httplib2.Http object to be used to make the request with.
400
401 Raises:
402 httplib2.HttpLib2Error if a transport error has occured.
403 apiclient.errors.BatchError if the response is the wrong format.
404 """
405 message = mime_multipart.MIMEMultipart('mixed')
406 # Message should not write out its own headers.
407 setattr(message, '_write_headers', lambda self: None)
408
409 # Add all the individual requests.
410 for key in self.__request_response_handlers:
411 msg = mime_nonmultipart.MIMENonMultipart('application', 'http')
412 msg['Content-Transfer-Encoding'] = 'binary'
413 msg['Content-ID'] = self._ConvertIdToHeader(key)
414
415 body = self._SerializeRequest(
416 self.__request_response_handlers[key].request)
417 msg.set_payload(body)
418 message.attach(msg)
419
420 request = http_wrapper.Request(self.__batch_url, 'POST')
421 request.body = message.as_string()
422 request.headers['content-type'] = (
423 'multipart/mixed; boundary="%s"') % message.get_boundary()
424
425 response = http_wrapper.MakeRequest(http, request)
426
427 if response.status_code >= 300:
428 raise exceptions.HttpError.FromResponse(response)
429
430 # Prepend with a content-type header so Parser can handle it.
431 header = 'content-type: %s\r\n\r\n' % response.info['content-type']
432
433 parser = email_parser.Parser()
434 mime_response = parser.parsestr(header + response.content)
435
436 if not mime_response.is_multipart():
437 raise exceptions.BatchError(
438 'Response not in multipart/mixed format.')
439
440 for part in mime_response.get_payload():
441 request_id = self._ConvertHeaderToId(part['Content-ID'])
442 response = self._DeserializeResponse(part.get_payload())
443
444 # Disable protected access because namedtuple._replace(...)
445 # is not actually meant to be protected.
446 # pylint: disable=protected-access
447 self.__request_response_handlers[request_id] = (
448 self.__request_response_handlers[request_id]._replace(
449 response=response))
450
451 def Execute(self, http):
452 """Execute all the requests as a single batched HTTP request.
453
454 Args:
455 http: A httplib2.Http object to be used with the request.
456
457 Returns:
458 None
459
460 Raises:
461 BatchError if the response is the wrong format.
462 """
463
464 self._Execute(http)
465
466 for key in self.__request_response_handlers:
467 response = self.__request_response_handlers[key].response
468 callback = self.__request_response_handlers[key].handler
469
470 exception = None
471
472 if response.status_code >= 300:
473 exception = exceptions.HttpError.FromResponse(response)
474
475 if callback is not None:
476 callback(response, exception)
477 if self.__callback is not None:
478 self.__callback(response, exception)
OLDNEW
« no previous file with comments | « third_party/google-endpoints/apitools/base/py/base_cli.py ('k') | third_party/google-endpoints/apitools/base/py/batch_test.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698