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

Side by Side Diff: gslib/third_party/storage_apitools/base_api.py

Issue 698893003: Update checked in version of gsutil to version 4.6 (Closed) Base URL: http://dart.googlecode.com/svn/third_party/gsutil/
Patch Set: Created 6 years, 1 month 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 | Annotate | Revision Log
Property Changes:
Added: svn:eol-style
+ LF
OLDNEW
(Empty)
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 """Base class for api services."""
15
16 import contextlib
17 import httplib
18 import logging
19 import pprint
20 import types
21 import urllib
22 import urlparse
23
24
25 from gslib.third_party.protorpc import message_types
26 from gslib.third_party.protorpc import messages
27
28 from gslib.third_party.storage_apitools import credentials_lib
29 from gslib.third_party.storage_apitools import encoding
30 from gslib.third_party.storage_apitools import exceptions
31 from gslib.third_party.storage_apitools import http_wrapper
32 from gslib.third_party.storage_apitools import util
33
34 __all__ = [
35 'ApiMethodInfo',
36 'ApiUploadInfo',
37 'BaseApiClient',
38 'BaseApiService',
39 'NormalizeApiEndpoint',
40 ]
41
42 # TODO: Remove this once we quiet the spurious logging in
43 # oauth2client (or drop oauth2client).
44 logging.getLogger('oauth2client.util').setLevel(logging.ERROR)
45
46 _MAX_URL_LENGTH = 2048
47
48
49 class ApiUploadInfo(messages.Message):
50 """Media upload information for a method.
51
52 Fields:
53 accept: (repeated) MIME Media Ranges for acceptable media uploads
54 to this method.
55 max_size: (integer) Maximum size of a media upload, such as 3MB
56 or 1TB (converted to an integer).
57 resumable_path: Path to use for resumable uploads.
58 resumable_multipart: (boolean) Whether or not the resumable endpoint
59 supports multipart uploads.
60 simple_path: Path to use for simple uploads.
61 simple_multipart: (boolean) Whether or not the simple endpoint
62 supports multipart uploads.
63 """
64 accept = messages.StringField(1, repeated=True)
65 max_size = messages.IntegerField(2)
66 resumable_path = messages.StringField(3)
67 resumable_multipart = messages.BooleanField(4)
68 simple_path = messages.StringField(5)
69 simple_multipart = messages.BooleanField(6)
70
71
72 class ApiMethodInfo(messages.Message):
73 """Configuration info for an API method.
74
75 All fields are strings unless noted otherwise.
76
77 Fields:
78 relative_path: Relative path for this method.
79 method_id: ID for this method.
80 http_method: HTTP verb to use for this method.
81 path_params: (repeated) path parameters for this method.
82 query_params: (repeated) query parameters for this method.
83 ordered_params: (repeated) ordered list of parameters for
84 this method.
85 description: description of this method.
86 request_type_name: name of the request type.
87 response_type_name: name of the response type.
88 request_field: if not null, the field to pass as the body
89 of this POST request. may also be the REQUEST_IS_BODY
90 value below to indicate the whole message is the body.
91 upload_config: (ApiUploadInfo) Information about the upload
92 configuration supported by this method.
93 supports_download: (boolean) If True, this method supports
94 downloading the request via the `alt=media` query
95 parameter.
96 """
97
98 relative_path = messages.StringField(1)
99 method_id = messages.StringField(2)
100 http_method = messages.StringField(3)
101 path_params = messages.StringField(4, repeated=True)
102 query_params = messages.StringField(5, repeated=True)
103 ordered_params = messages.StringField(6, repeated=True)
104 description = messages.StringField(7)
105 request_type_name = messages.StringField(8)
106 response_type_name = messages.StringField(9)
107 request_field = messages.StringField(10, default='')
108 upload_config = messages.MessageField(ApiUploadInfo, 11)
109 supports_download = messages.BooleanField(12, default=False)
110 REQUEST_IS_BODY = '<request>'
111
112
113 def _LoadClass(name, messages_module):
114 if name.startswith('message_types.'):
115 _, _, classname = name.partition('.')
116 return getattr(message_types, classname)
117 elif '.' not in name:
118 return getattr(messages_module, name)
119 else:
120 raise exceptions.GeneratedClientError('Unknown class %s' % name)
121
122
123 def _RequireClassAttrs(obj, attrs):
124 for attr in attrs:
125 attr_name = attr.upper()
126 if not hasattr(obj, '%s' % attr_name) or not getattr(obj, attr_name):
127 msg = 'No %s specified for object of class %s.' % (
128 attr_name, type(obj).__name__)
129 raise exceptions.GeneratedClientError(msg)
130
131
132 def NormalizeApiEndpoint(api_endpoint):
133 if not api_endpoint.endswith('/'):
134 api_endpoint += '/'
135 return api_endpoint
136
137
138 class _UrlBuilder(object):
139 """Convenient container for url data."""
140
141 def __init__(self, base_url, relative_path=None, query_params=None):
142 components = urlparse.urlsplit(urlparse.urljoin(
143 base_url, relative_path or ''))
144 if components.fragment:
145 raise exceptions.ConfigurationValueError(
146 'Unexpected url fragment: %s' % components.fragment)
147 self.query_params = urlparse.parse_qs(components.query or '')
148 if query_params is not None:
149 self.query_params.update(query_params)
150 self.__scheme = components.scheme
151 self.__netloc = components.netloc
152 self.relative_path = components.path
153
154 @classmethod
155 def FromUrl(cls, url):
156 urlparts = urlparse.urlsplit(url)
157 query_params = urlparse.parse_qs(urlparts.query)
158 base_url = urlparse.urlunsplit((
159 urlparts.scheme, urlparts.netloc, '', None, None))
160 relative_path = urlparts.path
161 return cls(base_url, relative_path=relative_path, query_params=query_params)
162
163 @property
164 def base_url(self):
165 return urlparse.urlunsplit((self.__scheme, self.__netloc, '', '', ''))
166
167 @base_url.setter
168 def base_url(self, value):
169 components = urlparse.urlsplit(value)
170 if components.path or components.query or components.fragment:
171 raise exceptions.ConfigurationValueError('Invalid base url: %s' % value)
172 self.__scheme = components.scheme
173 self.__netloc = components.netloc
174
175 @property
176 def query(self):
177 # TODO: In the case that some of the query params are
178 # non-ASCII, we may silently fail to encode correctly. We should
179 # figure out who is responsible for owning the object -> str
180 # conversion.
181 return urllib.urlencode(self.query_params, doseq=True)
182
183 @property
184 def url(self):
185 if '{' in self.relative_path or '}' in self.relative_path:
186 raise exceptions.ConfigurationValueError(
187 'Cannot create url with relative path %s' % self.relative_path)
188 return urlparse.urlunsplit((
189 self.__scheme, self.__netloc, self.relative_path, self.query, ''))
190
191
192 class BaseApiClient(object):
193 """Base class for client libraries."""
194 MESSAGES_MODULE = None
195
196 _API_KEY = ''
197 _CLIENT_ID = ''
198 _CLIENT_SECRET = ''
199 _PACKAGE = ''
200 _SCOPES = []
201 _USER_AGENT = ''
202
203 def __init__(self, url, credentials=None, get_credentials=True, http=None,
204 model=None, log_request=False, log_response=False, num_retries=5,
205 credentials_args=None, default_global_params=None):
206 _RequireClassAttrs(self, (
207 '_package', '_scopes', '_client_id', '_client_secret',
208 'messages_module'))
209 if default_global_params is not None:
210 util.Typecheck(default_global_params, self.params_type)
211 self.__default_global_params = default_global_params
212 self.log_request = log_request
213 self.log_response = log_response
214 self.__num_retries = 5
215 # We let the @property machinery below do our validation.
216 self.num_retries = num_retries
217 self._url = url
218 self._credentials = credentials
219 if get_credentials and not credentials:
220 credentials_args = credentials_args or {}
221 self._SetCredentials(**credentials_args)
222 self._http = http or http_wrapper.GetHttp()
223 # Note that "no credentials" is totally possible.
224 if self._credentials is not None:
225 self._http = self._credentials.authorize(self._http)
226 # TODO: Remove this field when we switch to proto2.
227 self.__include_fields = None
228
229 # TODO: Finish deprecating these fields.
230 _ = model
231
232 def _SetCredentials(self, **kwds):
233 """Fetch credentials, and set them for this client.
234
235 Note that we can't simply return credentials, since creating them
236 may involve side-effecting self.
237
238 Args:
239 **kwds: Additional keyword arguments are passed on to GetCredentials.
240
241 Returns:
242 None. Sets self._credentials.
243 """
244 args = {
245 'api_key': self._API_KEY,
246 'client': self,
247 'client_id': self._CLIENT_ID,
248 'client_secret': self._CLIENT_SECRET,
249 'package_name': self._PACKAGE,
250 'scopes': self._SCOPES,
251 'user_agent': self._USER_AGENT,
252 }
253 args.update(kwds)
254 # TODO: It's a bit dangerous to pass this
255 # still-half-initialized self into this method, but we might need
256 # to set attributes on it associated with our credentials.
257 # Consider another way around this (maybe a callback?) and whether
258 # or not it's worth it.
259 self._credentials = credentials_lib.GetCredentials(**args)
260
261 @classmethod
262 def ClientInfo(cls):
263 return {
264 'client_id': cls._CLIENT_ID,
265 'client_secret': cls._CLIENT_SECRET,
266 'scope': ' '.join(sorted(util.NormalizeScopes(cls._SCOPES))),
267 'user_agent': cls._USER_AGENT,
268 }
269
270 @property
271 def base_model_class(self):
272 return None
273
274 @property
275 def http(self):
276 return self._http
277
278 @property
279 def url(self):
280 return self._url
281
282 @classmethod
283 def GetScopes(cls):
284 return cls._SCOPES
285
286 @property
287 def params_type(self):
288 return _LoadClass('StandardQueryParameters', self.MESSAGES_MODULE)
289
290 @property
291 def user_agent(self):
292 return self._USER_AGENT
293
294 @property
295 def _default_global_params(self):
296 if self.__default_global_params is None:
297 self.__default_global_params = self.params_type()
298 return self.__default_global_params
299
300 def AddGlobalParam(self, name, value):
301 params = self._default_global_params
302 setattr(params, name, value)
303
304 @property
305 def global_params(self):
306 return encoding.CopyProtoMessage(self._default_global_params)
307
308 @contextlib.contextmanager
309 def IncludeFields(self, include_fields):
310 self.__include_fields = include_fields
311 yield
312 self.__include_fields = None
313
314 @property
315 def num_retries(self):
316 return self.__num_retries
317
318 @num_retries.setter
319 def num_retries(self, value):
320 util.Typecheck(value, (int, long))
321 if value < 0:
322 raise exceptions.InvalidDataError(
323 'Cannot have negative value for num_retries')
324 self.__num_retries = value
325
326 @contextlib.contextmanager
327 def WithRetries(self, num_retries):
328 old_num_retries = self.num_retries
329 self.num_retries = num_retries
330 yield
331 self.num_retries = old_num_retries
332
333 def ProcessRequest(self, method_config, request):
334 """Hook for pre-processing of requests."""
335 if self.log_request:
336 logging.info(
337 'Calling method %s with %s: %s', method_config.method_id,
338 method_config.request_type_name, request)
339 return request
340
341 def ProcessHttpRequest(self, http_request):
342 """Hook for pre-processing of http requests."""
343 if self.log_request:
344 logging.info('Making http %s to %s',
345 http_request.http_method, http_request.url)
346 logging.info('Headers: %s', pprint.pformat(http_request.headers))
347 if http_request.body:
348 # TODO: Make this safe to print in the case of
349 # non-printable body characters.
350 logging.info('Body:\n%s',
351 http_request.loggable_body or http_request.body)
352 else:
353 logging.info('Body: (none)')
354 return http_request
355
356 def ProcessResponse(self, method_config, response):
357 if self.log_response:
358 logging.info('Response of type %s: %s',
359 method_config.response_type_name, response)
360 return response
361
362 # TODO: Decide where these two functions should live.
363 def SerializeMessage(self, message):
364 return encoding.MessageToJson(message, include_fields=self.__include_fields)
365
366 def DeserializeMessage(self, response_type, data):
367 """Deserialize the given data as method_config.response_type."""
368 try:
369 message = encoding.JsonToMessage(response_type, data)
370 except (exceptions.InvalidDataFromServerError,
371 messages.ValidationError) as e:
372 raise exceptions.InvalidDataFromServerError(
373 'Error decoding response "%s" as type %s: %s' % (
374 data, response_type.__name__, e))
375 return message
376
377 def FinalizeTransferUrl(self, url):
378 """Modify the url for a given transfer, based on auth and version."""
379 url_builder = _UrlBuilder.FromUrl(url)
380 if self.global_params.key:
381 url_builder.query_params['key'] = self.global_params.key
382 return url_builder.url
383
384
385 class BaseApiService(object):
386 """Base class for generated API services."""
387
388 def __init__(self, client):
389 self.__client = client
390 self._method_configs = {}
391 self._upload_configs = {}
392
393 @property
394 def _client(self):
395 return self.__client
396
397 def GetMethodConfig(self, method):
398 return self._method_configs[method]
399
400 def GetUploadConfig(self, method):
401 return self._upload_configs.get(method)
402
403 def GetRequestType(self, method):
404 method_config = self.GetMethodConfig(method)
405 return getattr(self._client.MESSAGES_MODULE,
406 method_config.request_type_name)
407
408 def GetResponseType(self, method):
409 method_config = self.GetMethodConfig(method)
410 return getattr(self._client.MESSAGES_MODULE,
411 method_config.response_type_name)
412
413 def __CombineGlobalParams(self, global_params, default_params):
414 util.Typecheck(global_params, (types.NoneType, self.__client.params_type))
415 result = self.__client.params_type()
416 global_params = global_params or self.__client.params_type()
417 for field in result.all_fields():
418 value = (global_params.get_assigned_value(field.name) or
419 default_params.get_assigned_value(field.name))
420 if value not in (None, [], ()):
421 setattr(result, field.name, value)
422 return result
423
424 def __ConstructQueryParams(self, query_params, request, global_params):
425 """Construct a dictionary of query parameters for this request."""
426 global_params = self.__CombineGlobalParams(
427 global_params, self.__client.global_params)
428 query_info = dict((field.name, getattr(global_params, field.name))
429 for field in self.__client.params_type.all_fields())
430 query_info.update(
431 (param, getattr(request, param, None)) for param in query_params)
432 query_info = dict((k, v) for k, v in query_info.iteritems()
433 if v is not None)
434 for k, v in query_info.iteritems():
435 if isinstance(v, unicode):
436 query_info[k] = v.encode('utf8')
437 elif isinstance(v, str):
438 query_info[k] = v.decode('utf8')
439 return query_info
440
441 def __ConstructRelativePath(self, method_config, request, relative_path=None):
442 """Determine the relative path for request."""
443 path = relative_path or method_config.relative_path
444 path = path.replace('+', '')
445 for param in method_config.path_params:
446 param_template = '{%s}' % param
447 if param_template not in path:
448 raise exceptions.InvalidUserInputError(
449 'Missing path parameter %s' % param)
450 try:
451 # TODO: Do we want to support some sophisticated
452 # mapping here?
453 value = getattr(request, param)
454 except AttributeError:
455 raise exceptions.InvalidUserInputError(
456 'Request missing required parameter %s' % param)
457 if value is None:
458 raise exceptions.InvalidUserInputError(
459 'Request missing required parameter %s' % param)
460 try:
461 if not isinstance(value, basestring):
462 value = str(value)
463 path = path.replace(param_template,
464 urllib.quote(value.encode('utf_8'), ''))
465 except TypeError as e:
466 raise exceptions.InvalidUserInputError(
467 'Error setting required parameter %s to value %s: %s' % (
468 param, value, e))
469 return path
470
471 def __FinalizeRequest(self, http_request, url_builder):
472 """Make any final general adjustments to the request."""
473 if (http_request.http_method == 'GET' and
474 len(http_request.url) > _MAX_URL_LENGTH):
475 http_request.http_method = 'POST'
476 http_request.headers['x-http-method-override'] = 'GET'
477 http_request.headers['content-type'] = 'application/x-www-form-urlencoded'
478 http_request.body = url_builder.query
479 url_builder.query_params = {}
480 http_request.url = url_builder.url
481
482 def __ProcessHttpResponse(self, method_config, http_response):
483 """Process the given http response."""
484 if http_response.status_code not in (httplib.OK, httplib.NO_CONTENT):
485 raise exceptions.HttpError.FromResponse(http_response)
486 if http_response.status_code == httplib.NO_CONTENT:
487 # TODO: Find out why _replace doesn't seem to work here.
488 http_response = http_wrapper.Response(
489 info=http_response.info, content='{}',
490 request_url=http_response.request_url)
491 response_type = _LoadClass(
492 method_config.response_type_name, self.__client.MESSAGES_MODULE)
493 return self.__client.DeserializeMessage(
494 response_type, http_response.content)
495
496 def __SetBaseHeaders(self, http_request, client):
497 """Fill in the basic headers on http_request."""
498 # TODO: Make the default a little better here, and
499 # include the apitools version.
500 user_agent = client.user_agent or 'apitools-client/1.0'
501 http_request.headers['user-agent'] = user_agent
502 http_request.headers['accept'] = 'application/json'
503 http_request.headers['accept-encoding'] = 'gzip, deflate'
504
505 def __SetBody(self, http_request, method_config, request, upload):
506 """Fill in the body on http_request."""
507 if not method_config.request_field:
508 return
509
510 request_type = _LoadClass(
511 method_config.request_type_name, self.__client.MESSAGES_MODULE)
512 if method_config.request_field == REQUEST_IS_BODY:
513 body_value = request
514 body_type = request_type
515 else:
516 body_value = getattr(request, method_config.request_field)
517 body_field = request_type.field_by_name(method_config.request_field)
518 util.Typecheck(body_field, messages.MessageField)
519 body_type = body_field.type
520
521 if upload and not body_value:
522 # We're going to fill in the body later.
523 return
524 util.Typecheck(body_value, body_type)
525 http_request.headers['content-type'] = 'application/json'
526 http_request.body = self.__client.SerializeMessage(body_value)
527
528 def PrepareHttpRequest(self, method_config, request, global_params=None,
529 upload=None, upload_config=None, download=None):
530 """Prepares an HTTP request to be sent."""
531 request_type = _LoadClass(
532 method_config.request_type_name, self.__client.MESSAGES_MODULE)
533 util.Typecheck(request, request_type)
534 request = self.__client.ProcessRequest(method_config, request)
535
536 http_request = http_wrapper.Request(http_method=method_config.http_method)
537 self.__SetBaseHeaders(http_request, self.__client)
538 self.__SetBody(http_request, method_config, request, upload)
539
540 url_builder = _UrlBuilder(
541 self.__client.url, relative_path=method_config.relative_path)
542 url_builder.query_params = self.__ConstructQueryParams(
543 method_config.query_params, request, global_params)
544
545 # It's important that upload and download go before we fill in the
546 # relative path, so that they can replace it.
547 if upload is not None:
548 upload.ConfigureRequest(upload_config, http_request, url_builder)
549 if download is not None:
550 download.ConfigureRequest(http_request, url_builder)
551
552 url_builder.relative_path = self.__ConstructRelativePath(
553 method_config, request, relative_path=url_builder.relative_path)
554 self.__FinalizeRequest(http_request, url_builder)
555
556 return self.__client.ProcessHttpRequest(http_request)
557
558 def _RunMethod(self, method_config, request, global_params=None,
559 upload=None, upload_config=None, download=None):
560 """Call this method with request."""
561 if upload is not None and download is not None:
562 # TODO: This just involves refactoring the logic
563 # below into callbacks that we can pass around; in particular,
564 # the order should be that the upload gets the initial request,
565 # and then passes its reply to a download if one exists, and
566 # then that goes to ProcessResponse and is returned.
567 raise exceptions.NotYetImplementedError(
568 'Cannot yet use both upload and download at once')
569
570 http_request = self.PrepareHttpRequest(
571 method_config, request, global_params, upload, upload_config, download)
572
573 # TODO: Make num_retries customizable on Transfer
574 # objects, and pass in self.__client.num_retries when initializing
575 # an upload or download.
576 if download is not None:
577 download.InitializeDownload(http_request, client=self._client)
578 return
579
580 http_response = None
581 if upload is not None:
582 http_response = upload.InitializeUpload(http_request, client=self._client)
583 if http_response is None:
584 http = self.__client.http
585 if upload and upload.bytes_http:
586 http = upload.bytes_http
587 http_response = http_wrapper.MakeRequest(
588 http, http_request, retries=self.__client.num_retries)
589
590 return self.ProcessHttpResponse(method_config, http_response)
591
592 def ProcessHttpResponse(self, method_config, http_response):
593 """Convert an HTTP response to the expected message type."""
594 return self.__client.ProcessResponse(
595 method_config,
596 self.__ProcessHttpResponse(method_config, http_response))
OLDNEW
« no previous file with comments | « gslib/third_party/storage_apitools/__init__.py ('k') | gslib/third_party/storage_apitools/credentials_lib.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698