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

Side by Side Diff: appengine/chromium_build_logs/third_party/apiclient/discovery.py

Issue 1260293009: make version of ts_mon compatible with appengine (Closed) Base URL: https://chromium.googlesource.com/infra/infra.git@master
Patch Set: clean up code Created 5 years, 4 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 # Copyright (C) 2010 Google Inc.
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 """Client for discovery based APIs
16
17 A client library for Google's discovery based APIs.
18 """
19
20 __author__ = 'jcgregorio@google.com (Joe Gregorio)'
21 __all__ = [
22 'build', 'build_from_document'
23 ]
24
25 import copy
26 import httplib2
27 import logging
28 import os
29 import random
30 import re
31 import uritemplate
32 import urllib
33 import urlparse
34 import mimeparse
35 import mimetypes
36
37 try:
38 from urlparse import parse_qsl
39 except ImportError:
40 from cgi import parse_qsl
41
42 from apiclient.errors import HttpError
43 from apiclient.errors import InvalidJsonError
44 from apiclient.errors import MediaUploadSizeError
45 from apiclient.errors import UnacceptableMimeTypeError
46 from apiclient.errors import UnknownApiNameOrVersion
47 from apiclient.errors import UnknownLinkType
48 from apiclient.http import HttpRequest
49 from apiclient.http import MediaFileUpload
50 from apiclient.http import MediaUpload
51 from apiclient.model import JsonModel
52 from apiclient.model import RawModel
53 from apiclient.schema import Schemas
54 from email.mime.multipart import MIMEMultipart
55 from email.mime.nonmultipart import MIMENonMultipart
56 from oauth2client.anyjson import simplejson
57
58
59 URITEMPLATE = re.compile('{[^}]*}')
60 VARNAME = re.compile('[a-zA-Z0-9_-]+')
61 DISCOVERY_URI = ('https://www.googleapis.com/discovery/v1/apis/'
62 '{api}/{apiVersion}/rest')
63 DEFAULT_METHOD_DOC = 'A description of how to use this function'
64
65 # Query parameters that work, but don't appear in discovery
66 STACK_QUERY_PARAMETERS = ['trace', 'fields', 'pp', 'prettyPrint', 'userIp',
67 'userip', 'strict']
68
69 RESERVED_WORDS = ['and', 'assert', 'break', 'class', 'continue', 'def', 'del',
70 'elif', 'else', 'except', 'exec', 'finally', 'for', 'from',
71 'global', 'if', 'import', 'in', 'is', 'lambda', 'not', 'or',
72 'pass', 'print', 'raise', 'return', 'try', 'while' ]
73
74
75 def _fix_method_name(name):
76 if name in RESERVED_WORDS:
77 return name + '_'
78 else:
79 return name
80
81
82 def _write_headers(self):
83 # Utility no-op method for multipart media handling
84 pass
85
86
87 def _add_query_parameter(url, name, value):
88 """Adds a query parameter to a url
89
90 Args:
91 url: string, url to add the query parameter to.
92 name: string, query parameter name.
93 value: string, query parameter value.
94
95 Returns:
96 Updated query parameter. Does not update the url if value is None.
97 """
98 if value is None:
99 return url
100 else:
101 parsed = list(urlparse.urlparse(url))
102 q = parse_qsl(parsed[4])
103 q.append((name, value))
104 parsed[4] = urllib.urlencode(q)
105 return urlparse.urlunparse(parsed)
106
107
108 def key2param(key):
109 """Converts key names into parameter names.
110
111 For example, converting "max-results" -> "max_results"
112 """
113 result = []
114 key = list(key)
115 if not key[0].isalpha():
116 result.append('x')
117 for c in key:
118 if c.isalnum():
119 result.append(c)
120 else:
121 result.append('_')
122
123 return ''.join(result)
124
125
126 def build(serviceName, version,
127 http=None,
128 discoveryServiceUrl=DISCOVERY_URI,
129 developerKey=None,
130 model=None,
131 requestBuilder=HttpRequest):
132 """Construct a Resource for interacting with an API.
133
134 Construct a Resource object for interacting with
135 an API. The serviceName and version are the
136 names from the Discovery service.
137
138 Args:
139 serviceName: string, name of the service
140 version: string, the version of the service
141 discoveryServiceUrl: string, a URI Template that points to
142 the location of the discovery service. It should have two
143 parameters {api} and {apiVersion} that when filled in
144 produce an absolute URI to the discovery document for
145 that service.
146 developerKey: string, key obtained
147 from https://code.google.com/apis/console
148 model: apiclient.Model, converts to and from the wire format
149 requestBuilder: apiclient.http.HttpRequest, encapsulator for
150 an HTTP request
151
152 Returns:
153 A Resource object with methods for interacting with
154 the service.
155 """
156 params = {
157 'api': serviceName,
158 'apiVersion': version
159 }
160
161 if http is None:
162 http = httplib2.Http()
163
164 requested_url = uritemplate.expand(discoveryServiceUrl, params)
165
166 # REMOTE_ADDR is defined by the CGI spec [RFC3875] as the environment
167 # variable that contains the network address of the client sending the
168 # request. If it exists then add that to the request for the discovery
169 # document to avoid exceeding the quota on discovery requests.
170 if 'REMOTE_ADDR' in os.environ:
171 requested_url = _add_query_parameter(requested_url, 'userIp',
172 os.environ['REMOTE_ADDR'])
173 logging.info('URL being requested: %s' % requested_url)
174
175 resp, content = http.request(requested_url)
176
177 if resp.status == 404:
178 raise UnknownApiNameOrVersion("name: %s version: %s" % (serviceName,
179 version))
180 if resp.status >= 400:
181 raise HttpError(resp, content, requested_url)
182
183 try:
184 service = simplejson.loads(content)
185 except ValueError, e:
186 logging.error('Failed to parse as JSON: ' + content)
187 raise InvalidJsonError()
188
189 filename = os.path.join(os.path.dirname(__file__), 'contrib',
190 serviceName, 'future.json')
191 try:
192 f = file(filename, 'r')
193 future = f.read()
194 f.close()
195 except IOError:
196 future = None
197
198 return build_from_document(content, discoveryServiceUrl, future,
199 http, developerKey, model, requestBuilder)
200
201
202 def build_from_document(
203 service,
204 base,
205 future=None,
206 http=None,
207 developerKey=None,
208 model=None,
209 requestBuilder=HttpRequest):
210 """Create a Resource for interacting with an API.
211
212 Same as `build()`, but constructs the Resource object
213 from a discovery document that is it given, as opposed to
214 retrieving one over HTTP.
215
216 Args:
217 service: string, discovery document
218 base: string, base URI for all HTTP requests, usually the discovery URI
219 future: string, discovery document with future capabilities
220 auth_discovery: dict, information about the authentication the API supports
221 http: httplib2.Http, An instance of httplib2.Http or something that acts
222 like it that HTTP requests will be made through.
223 developerKey: string, Key for controlling API usage, generated
224 from the API Console.
225 model: Model class instance that serializes and
226 de-serializes requests and responses.
227 requestBuilder: Takes an http request and packages it up to be executed.
228
229 Returns:
230 A Resource object with methods for interacting with
231 the service.
232 """
233
234 service = simplejson.loads(service)
235 base = urlparse.urljoin(base, service['basePath'])
236 if future:
237 future = simplejson.loads(future)
238 auth_discovery = future.get('auth', {})
239 else:
240 future = {}
241 auth_discovery = {}
242 schema = Schemas(service)
243
244 if model is None:
245 features = service.get('features', [])
246 model = JsonModel('dataWrapper' in features)
247 resource = createResource(http, base, model, requestBuilder, developerKey,
248 service, future, schema)
249
250 def auth_method():
251 """Discovery information about the authentication the API uses."""
252 return auth_discovery
253
254 setattr(resource, 'auth_discovery', auth_method)
255
256 return resource
257
258
259 def _cast(value, schema_type):
260 """Convert value to a string based on JSON Schema type.
261
262 See http://tools.ietf.org/html/draft-zyp-json-schema-03 for more details on
263 JSON Schema.
264
265 Args:
266 value: any, the value to convert
267 schema_type: string, the type that value should be interpreted as
268
269 Returns:
270 A string representation of 'value' based on the schema_type.
271 """
272 if schema_type == 'string':
273 if type(value) == type('') or type(value) == type(u''):
274 return value
275 else:
276 return str(value)
277 elif schema_type == 'integer':
278 return str(int(value))
279 elif schema_type == 'number':
280 return str(float(value))
281 elif schema_type == 'boolean':
282 return str(bool(value)).lower()
283 else:
284 if type(value) == type('') or type(value) == type(u''):
285 return value
286 else:
287 return str(value)
288
289 MULTIPLIERS = {
290 "KB": 2 ** 10,
291 "MB": 2 ** 20,
292 "GB": 2 ** 30,
293 "TB": 2 ** 40,
294 }
295
296
297 def _media_size_to_long(maxSize):
298 """Convert a string media size, such as 10GB or 3TB into an integer."""
299 if len(maxSize) < 2:
300 return 0
301 units = maxSize[-2:].upper()
302 multiplier = MULTIPLIERS.get(units, 0)
303 if multiplier:
304 return int(maxSize[:-2]) * multiplier
305 else:
306 return int(maxSize)
307
308
309 def createResource(http, baseUrl, model, requestBuilder,
310 developerKey, resourceDesc, futureDesc, schema):
311
312 class Resource(object):
313 """A class for interacting with a resource."""
314
315 def __init__(self):
316 self._http = http
317 self._baseUrl = baseUrl
318 self._model = model
319 self._developerKey = developerKey
320 self._requestBuilder = requestBuilder
321
322 def createMethod(theclass, methodName, methodDesc, futureDesc):
323 methodName = _fix_method_name(methodName)
324 pathUrl = methodDesc['path']
325 httpMethod = methodDesc['httpMethod']
326 methodId = methodDesc['id']
327
328 mediaPathUrl = None
329 accept = []
330 maxSize = 0
331 if 'mediaUpload' in methodDesc:
332 mediaUpload = methodDesc['mediaUpload']
333 mediaPathUrl = mediaUpload['protocols']['simple']['path']
334 mediaResumablePathUrl = mediaUpload['protocols']['resumable']['path']
335 accept = mediaUpload['accept']
336 maxSize = _media_size_to_long(mediaUpload.get('maxSize', ''))
337
338 if 'parameters' not in methodDesc:
339 methodDesc['parameters'] = {}
340 for name in STACK_QUERY_PARAMETERS:
341 methodDesc['parameters'][name] = {
342 'type': 'string',
343 'location': 'query'
344 }
345
346 if httpMethod in ['PUT', 'POST', 'PATCH'] and 'request' in methodDesc:
347 methodDesc['parameters']['body'] = {
348 'description': 'The request body.',
349 'type': 'object',
350 'required': True,
351 }
352 if 'request' in methodDesc:
353 methodDesc['parameters']['body'].update(methodDesc['request'])
354 else:
355 methodDesc['parameters']['body']['type'] = 'object'
356 if 'mediaUpload' in methodDesc:
357 methodDesc['parameters']['media_body'] = {
358 'description': 'The filename of the media request body.',
359 'type': 'string',
360 'required': False,
361 }
362 if 'body' in methodDesc['parameters']:
363 methodDesc['parameters']['body']['required'] = False
364
365 argmap = {} # Map from method parameter name to query parameter name
366 required_params = [] # Required parameters
367 repeated_params = [] # Repeated parameters
368 pattern_params = {} # Parameters that must match a regex
369 query_params = [] # Parameters that will be used in the query string
370 path_params = {} # Parameters that will be used in the base URL
371 param_type = {} # The type of the parameter
372 enum_params = {} # Allowable enumeration values for each parameter
373
374
375 if 'parameters' in methodDesc:
376 for arg, desc in methodDesc['parameters'].iteritems():
377 param = key2param(arg)
378 argmap[param] = arg
379
380 if desc.get('pattern', ''):
381 pattern_params[param] = desc['pattern']
382 if desc.get('enum', ''):
383 enum_params[param] = desc['enum']
384 if desc.get('required', False):
385 required_params.append(param)
386 if desc.get('repeated', False):
387 repeated_params.append(param)
388 if desc.get('location') == 'query':
389 query_params.append(param)
390 if desc.get('location') == 'path':
391 path_params[param] = param
392 param_type[param] = desc.get('type', 'string')
393
394 for match in URITEMPLATE.finditer(pathUrl):
395 for namematch in VARNAME.finditer(match.group(0)):
396 name = key2param(namematch.group(0))
397 path_params[name] = name
398 if name in query_params:
399 query_params.remove(name)
400
401 def method(self, **kwargs):
402 for name in kwargs.iterkeys():
403 if name not in argmap:
404 raise TypeError('Got an unexpected keyword argument "%s"' % name)
405
406 for name in required_params:
407 if name not in kwargs:
408 raise TypeError('Missing required parameter "%s"' % name)
409
410 for name, regex in pattern_params.iteritems():
411 if name in kwargs:
412 if isinstance(kwargs[name], basestring):
413 pvalues = [kwargs[name]]
414 else:
415 pvalues = kwargs[name]
416 for pvalue in pvalues:
417 if re.match(regex, pvalue) is None:
418 raise TypeError(
419 'Parameter "%s" value "%s" does not match the pattern "%s"' %
420 (name, pvalue, regex))
421
422 for name, enums in enum_params.iteritems():
423 if name in kwargs:
424 if kwargs[name] not in enums:
425 raise TypeError(
426 'Parameter "%s" value "%s" is not an allowed value in "%s"' %
427 (name, kwargs[name], str(enums)))
428
429 actual_query_params = {}
430 actual_path_params = {}
431 for key, value in kwargs.iteritems():
432 to_type = param_type.get(key, 'string')
433 # For repeated parameters we cast each member of the list.
434 if key in repeated_params and type(value) == type([]):
435 cast_value = [_cast(x, to_type) for x in value]
436 else:
437 cast_value = _cast(value, to_type)
438 if key in query_params:
439 actual_query_params[argmap[key]] = cast_value
440 if key in path_params:
441 actual_path_params[argmap[key]] = cast_value
442 body_value = kwargs.get('body', None)
443 media_filename = kwargs.get('media_body', None)
444
445 if self._developerKey:
446 actual_query_params['key'] = self._developerKey
447
448 model = self._model
449 # If there is no schema for the response then presume a binary blob.
450 if 'response' not in methodDesc:
451 model = RawModel()
452
453 headers = {}
454 headers, params, query, body = model.request(headers,
455 actual_path_params, actual_query_params, body_value)
456
457 expanded_url = uritemplate.expand(pathUrl, params)
458 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
459
460 resumable = None
461 multipart_boundary = ''
462
463 if media_filename:
464 # Ensure we end up with a valid MediaUpload object.
465 if isinstance(media_filename, basestring):
466 (media_mime_type, encoding) = mimetypes.guess_type(media_filename)
467 if media_mime_type is None:
468 raise UnknownFileType(media_filename)
469 if not mimeparse.best_match([media_mime_type], ','.join(accept)):
470 raise UnacceptableMimeTypeError(media_mime_type)
471 media_upload = MediaFileUpload(media_filename, media_mime_type)
472 elif isinstance(media_filename, MediaUpload):
473 media_upload = media_filename
474 else:
475 raise TypeError('media_filename must be str or MediaUpload.')
476
477 # Check the maxSize
478 if maxSize > 0 and media_upload.size() > maxSize:
479 raise MediaUploadSizeError("Media larger than: %s" % maxSize)
480
481 # Use the media path uri for media uploads
482 if media_upload.resumable():
483 expanded_url = uritemplate.expand(mediaResumablePathUrl, params)
484 else:
485 expanded_url = uritemplate.expand(mediaPathUrl, params)
486 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
487
488 if media_upload.resumable():
489 # This is all we need to do for resumable, if the body exists it gets
490 # sent in the first request, otherwise an empty body is sent.
491 resumable = media_upload
492 else:
493 # A non-resumable upload
494 if body is None:
495 # This is a simple media upload
496 headers['content-type'] = media_upload.mimetype()
497 body = media_upload.getbytes(0, media_upload.size())
498 else:
499 # This is a multipart/related upload.
500 msgRoot = MIMEMultipart('related')
501 # msgRoot should not write out it's own headers
502 setattr(msgRoot, '_write_headers', lambda self: None)
503
504 # attach the body as one part
505 msg = MIMENonMultipart(*headers['content-type'].split('/'))
506 msg.set_payload(body)
507 msgRoot.attach(msg)
508
509 # attach the media as the second part
510 msg = MIMENonMultipart(*media_upload.mimetype().split('/'))
511 msg['Content-Transfer-Encoding'] = 'binary'
512
513 payload = media_upload.getbytes(0, media_upload.size())
514 msg.set_payload(payload)
515 msgRoot.attach(msg)
516 body = msgRoot.as_string()
517
518 multipart_boundary = msgRoot.get_boundary()
519 headers['content-type'] = ('multipart/related; '
520 'boundary="%s"') % multipart_boundary
521
522 logging.info('URL being requested: %s' % url)
523 return self._requestBuilder(self._http,
524 model.response,
525 url,
526 method=httpMethod,
527 body=body,
528 headers=headers,
529 methodId=methodId,
530 resumable=resumable)
531
532 docs = [methodDesc.get('description', DEFAULT_METHOD_DOC), '\n\n']
533 if len(argmap) > 0:
534 docs.append('Args:\n')
535 for arg in argmap.iterkeys():
536 if arg in STACK_QUERY_PARAMETERS:
537 continue
538 repeated = ''
539 if arg in repeated_params:
540 repeated = ' (repeated)'
541 required = ''
542 if arg in required_params:
543 required = ' (required)'
544 paramdesc = methodDesc['parameters'][argmap[arg]]
545 paramdoc = paramdesc.get('description', 'A parameter')
546 if '$ref' in paramdesc:
547 docs.append(
548 (' %s: object, %s%s%s\n The object takes the'
549 ' form of:\n\n%s\n\n') % (arg, paramdoc, required, repeated,
550 schema.prettyPrintByName(paramdesc['$ref'])))
551 else:
552 paramtype = paramdesc.get('type', 'string')
553 docs.append(' %s: %s, %s%s%s\n' % (arg, paramtype, paramdoc, required,
554 repeated))
555 enum = paramdesc.get('enum', [])
556 enumDesc = paramdesc.get('enumDescriptions', [])
557 if enum and enumDesc:
558 docs.append(' Allowed values\n')
559 for (name, desc) in zip(enum, enumDesc):
560 docs.append(' %s - %s\n' % (name, desc))
561 if 'response' in methodDesc:
562 docs.append('\nReturns:\n An object of the form\n\n ')
563 docs.append(schema.prettyPrintSchema(methodDesc['response']))
564
565 setattr(method, '__doc__', ''.join(docs))
566 setattr(theclass, methodName, method)
567
568 def createNextMethodFromFuture(theclass, methodName, methodDesc, futureDesc):
569 """ This is a legacy method, as only Buzz and Moderator use the future.json
570 functionality for generating _next methods. It will be kept around as long
571 as those API versions are around, but no new APIs should depend upon it.
572 """
573 methodName = _fix_method_name(methodName)
574 methodId = methodDesc['id'] + '.next'
575
576 def methodNext(self, previous):
577 """Retrieve the next page of results.
578
579 Takes a single argument, 'body', which is the results
580 from the last call, and returns the next set of items
581 in the collection.
582
583 Returns:
584 None if there are no more items in the collection.
585 """
586 if futureDesc['type'] != 'uri':
587 raise UnknownLinkType(futureDesc['type'])
588
589 try:
590 p = previous
591 for key in futureDesc['location']:
592 p = p[key]
593 url = p
594 except (KeyError, TypeError):
595 return None
596
597 url = _add_query_parameter(url, 'key', self._developerKey)
598
599 headers = {}
600 headers, params, query, body = self._model.request(headers, {}, {}, None)
601
602 logging.info('URL being requested: %s' % url)
603 resp, content = self._http.request(url, method='GET', headers=headers)
604
605 return self._requestBuilder(self._http,
606 self._model.response,
607 url,
608 method='GET',
609 headers=headers,
610 methodId=methodId)
611
612 setattr(theclass, methodName, methodNext)
613
614 def createNextMethod(theclass, methodName, methodDesc, futureDesc):
615 methodName = _fix_method_name(methodName)
616 methodId = methodDesc['id'] + '.next'
617
618 def methodNext(self, previous_request, previous_response):
619 """Retrieves the next page of results.
620
621 Args:
622 previous_request: The request for the previous page.
623 previous_response: The response from the request for the previous page.
624
625 Returns:
626 A request object that you can call 'execute()' on to request the next
627 page. Returns None if there are no more items in the collection.
628 """
629 # Retrieve nextPageToken from previous_response
630 # Use as pageToken in previous_request to create new request.
631
632 if 'nextPageToken' not in previous_response:
633 return None
634
635 request = copy.copy(previous_request)
636
637 pageToken = previous_response['nextPageToken']
638 parsed = list(urlparse.urlparse(request.uri))
639 q = parse_qsl(parsed[4])
640
641 # Find and remove old 'pageToken' value from URI
642 newq = [(key, value) for (key, value) in q if key != 'pageToken']
643 newq.append(('pageToken', pageToken))
644 parsed[4] = urllib.urlencode(newq)
645 uri = urlparse.urlunparse(parsed)
646
647 request.uri = uri
648
649 logging.info('URL being requested: %s' % uri)
650
651 return request
652
653 setattr(theclass, methodName, methodNext)
654
655 # Add basic methods to Resource
656 if 'methods' in resourceDesc:
657 for methodName, methodDesc in resourceDesc['methods'].iteritems():
658 if futureDesc:
659 future = futureDesc['methods'].get(methodName, {})
660 else:
661 future = None
662 createMethod(Resource, methodName, methodDesc, future)
663
664 # Add in nested resources
665 if 'resources' in resourceDesc:
666
667 def createResourceMethod(theclass, methodName, methodDesc, futureDesc):
668 methodName = _fix_method_name(methodName)
669
670 def methodResource(self):
671 return createResource(self._http, self._baseUrl, self._model,
672 self._requestBuilder, self._developerKey,
673 methodDesc, futureDesc, schema)
674
675 setattr(methodResource, '__doc__', 'A collection resource.')
676 setattr(methodResource, '__is_resource__', True)
677 setattr(theclass, methodName, methodResource)
678
679 for methodName, methodDesc in resourceDesc['resources'].iteritems():
680 if futureDesc and 'resources' in futureDesc:
681 future = futureDesc['resources'].get(methodName, {})
682 else:
683 future = {}
684 createResourceMethod(Resource, methodName, methodDesc, future)
685
686 # Add <m>_next() methods to Resource
687 if futureDesc and 'methods' in futureDesc:
688 for methodName, methodDesc in futureDesc['methods'].iteritems():
689 if 'next' in methodDesc and methodName in resourceDesc['methods']:
690 createNextMethodFromFuture(Resource, methodName + '_next',
691 resourceDesc['methods'][methodName],
692 methodDesc['next'])
693 # Add _next() methods
694 # Look for response bodies in schema that contain nextPageToken, and methods
695 # that take a pageToken parameter.
696 if 'methods' in resourceDesc:
697 for methodName, methodDesc in resourceDesc['methods'].iteritems():
698 if 'response' in methodDesc:
699 responseSchema = methodDesc['response']
700 if '$ref' in responseSchema:
701 responseSchema = schema.get(responseSchema['$ref'])
702 hasNextPageToken = 'nextPageToken' in responseSchema.get('properties',
703 {})
704 hasPageToken = 'pageToken' in methodDesc.get('parameters', {})
705 if hasNextPageToken and hasPageToken:
706 createNextMethod(Resource, methodName + '_next',
707 resourceDesc['methods'][methodName],
708 methodName)
709
710 return Resource()
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698