def _log_request(self, headers, path_params, query, body): """Logs debugging information about the request if requested.""" if dump_request_response: LOGGER.info('--request-start--') LOGGER.info('-headers-start-') for h, v in six.iteritems(headers): LOGGER.info('%s: %s', h, v) LOGGER.info('-headers-end-') LOGGER.info('-path-parameters-start-') for h, v in six.iteritems(path_params): LOGGER.info('%s: %s', h, v) LOGGER.info('-path-parameters-end-') LOGGER.info('body: %s', body) LOGGER.info('query: %s', query) LOGGER.info('--request-end--')
def _add_next_methods(self, resourceDesc, schema): # Add _next() methods if and only if one of the names 'pageToken' or # 'nextPageToken' occurs among the fields of both the method's response # type either the method's request (query parameters) or request body. if 'methods' not in resourceDesc: return for methodName, methodDesc in six.iteritems(resourceDesc['methods']): nextPageTokenName = _findPageTokenName( _methodProperties(methodDesc, schema, 'response')) if not nextPageTokenName: continue isPageTokenParameter = True pageTokenName = _findPageTokenName(methodDesc.get( 'parameters', {})) if not pageTokenName: isPageTokenParameter = False pageTokenName = _findPageTokenName( _methodProperties(methodDesc, schema, 'request')) if not pageTokenName: continue fixedMethodName, method = createNextMethod(methodName + '_next', pageTokenName, nextPageTokenName, isPageTokenParameter) self._set_dynamic_attr(fixedMethodName, method.__get__(self, self.__class__))
def _add_nested_resources(self, resourceDesc, rootDesc, schema): # Add in nested resources if 'resources' in resourceDesc: def createResourceMethod(methodName, methodDesc): """Create a method on the Resource to access a nested Resource. Args: methodName: string, name of the method to use. methodDesc: object, fragment of deserialized discovery document that describes the method. """ methodName = fix_method_name(methodName) def methodResource(self): return Resource(http=self._http, baseUrl=self._baseUrl, model=self._model, developerKey=self._developerKey, requestBuilder=self._requestBuilder, resourceDesc=methodDesc, rootDesc=rootDesc, schema=schema) setattr(methodResource, '__doc__', 'A collection resource.') setattr(methodResource, '__is_resource__', True) return (methodName, methodResource) for methodName, methodDesc in six.iteritems( resourceDesc['resources']): fixedMethodName, method = createResourceMethod( methodName, methodDesc) self._set_dynamic_attr(fixedMethodName, method.__get__(self, self.__class__))
def _log_response(self, resp, content): """Logs debugging information about the response if requested.""" if dump_request_response: LOGGER.info('--response-start--') for h, v in six.iteritems(resp): LOGGER.info('%s: %s', h, v) if content: LOGGER.info(content) LOGGER.info('--response-end--')
def makepatch(original, modified): """Create a patch object. Some methods support PATCH, an efficient way to send updates to a resource. This method allows the easy construction of patch bodies by looking at the differences between a resource before and after it was modified. Args: original: object, the original deserialized resource modified: object, the modified deserialized resource Returns: An object that contains only the changes from original to modified, in a form suitable to pass to a PATCH method. Example usage: item = service.activities().get(postid=postid, userid=userid).execute() original = copy.deepcopy(item) item['object']['content'] = 'This is updated.' service.activities.patch(postid=postid, userid=userid, body=makepatch(original, item)).execute() """ patch = {} for key, original_value in six.iteritems(original): modified_value = modified.get(key, None) if modified_value is None: # Use None to signal that the element is deleted patch[key] = None elif original_value != modified_value: if type(original_value) == type({}): # Recursively descend objects patch[key] = makepatch(original_value, modified_value) else: # In the case of simple types or arrays we just replace patch[key] = modified_value else: # Don't add anything to patch if there's no change pass for key in modified: if key not in original: patch[key] = modified[key] return patch
def _fix_up_parameters(method_desc, root_desc, http_method): """Updates parameters of an API method with values specific to this library. Specifically, adds whatever global parameters are specified by the API to the parameters for the individual method. Also adds parameters which don't appear in the discovery document, but are available to all discovery based APIs (these are listed in STACK_QUERY_PARAMETERS). SIDE EFFECTS: This updates the parameters dictionary object in the method description. Args: method_desc: Dictionary with metadata describing an API method. Value comes from the dictionary of methods stored in the 'methods' key in the deserialized discovery document. root_desc: Dictionary; the entire original deserialized discovery document. http_method: String; the HTTP method used to call the API method described in method_desc. Returns: The updated Dictionary stored in the 'parameters' key of the method description dictionary. """ parameters = method_desc.setdefault('parameters', {}) # Add in the parameters common to all methods. for name, description in six.iteritems(root_desc.get('parameters', {})): parameters[name] = description # Add in undocumented query parameters. for name in STACK_QUERY_PARAMETERS: parameters[name] = STACK_QUERY_PARAMETER_DEFAULT_VALUE.copy() # Add 'body' (our own reserved word) to parameters if the method supports # a request payload. if http_method in HTTP_PAYLOAD_METHODS and 'request' in method_desc: body = BODY_PARAMETER_DEFAULT_VALUE.copy() body.update(method_desc['request']) parameters['body'] = body return parameters
def _add_basic_methods(self, resourceDesc, rootDesc, schema): # If this is the root Resource, add a new_batch_http_request() method. if resourceDesc == rootDesc: batch_uri = '%s%s' % (rootDesc['rootUrl'], rootDesc.get('batchPath', 'batch')) def new_batch_http_request(callback=None): """Create a BatchHttpRequest object based on the discovery document. Args: callback: callable, A callback to be called for each response, of the form callback(id, response, exception). The first parameter is the request id, and the second is the deserialized response object. The third is an apiclient.errors.HttpError exception object if an HTTP error occurred while processing the request, or None if no error occurred. Returns: A BatchHttpRequest object based on the discovery document. """ return BatchHttpRequest(callback=callback, batch_uri=batch_uri) self._set_dynamic_attr('new_batch_http_request', new_batch_http_request) # Add basic methods to Resource if 'methods' in resourceDesc: for methodName, methodDesc in six.iteritems( resourceDesc['methods']): fixedMethodName, method = createMethod(methodName, methodDesc, rootDesc, schema) self._set_dynamic_attr(fixedMethodName, method.__get__(self, self.__class__)) # Add in _media methods. The functionality of the attached method will # change when it sees that the method name ends in _media. if methodDesc.get('supportsMediaDownload', False): fixedMethodName, method = createMethod( methodName + '_media', methodDesc, rootDesc, schema) self._set_dynamic_attr( fixedMethodName, method.__get__(self, self.__class__))
def _build_query(self, params): """Builds a query string. Args: params: dict, the query parameters Returns: The query parameters properly encoded into an HTTP URI query string. """ if self.alt_param is not None: params.update({'alt': self.alt_param}) astuples = [] for key, value in six.iteritems(params): if type(value) == type([]): for x in value: x = x.encode('utf-8') astuples.append((key, x)) else: if isinstance(value, six.text_type) and callable(value.encode): value = value.encode('utf-8') astuples.append((key, value)) return '?' + urlencode(astuples)
def set_parameters(self, method_desc): """Populates maps and lists based on method description. Iterates through each parameter for the method and parses the values from the parameter dictionary. Args: method_desc: Dictionary with metadata describing an API method. Value comes from the dictionary of methods stored in the 'methods' key in the deserialized discovery document. """ for arg, desc in six.iteritems(method_desc.get('parameters', {})): param = key2param(arg) self.argmap[param] = arg if desc.get('pattern'): self.pattern_params[param] = desc['pattern'] if desc.get('enum'): self.enum_params[param] = desc['enum'] if desc.get('required'): self.required_params.append(param) if desc.get('repeated'): self.repeated_params.append(param) if desc.get('location') == 'query': self.query_params.append(param) if desc.get('location') == 'path': self.path_params.add(param) self.param_types[param] = desc.get('type', 'string') # TODO(dhermes): Determine if this is still necessary. Discovery based APIs # should have all path parameters already marked with # 'location: path'. for match in URITEMPLATE.finditer(method_desc['path']): for namematch in VARNAME.finditer(match.group(0)): name = key2param(namematch.group(0)) self.path_params.add(name) if name in self.query_params: self.query_params.remove(name)
def method(self, **kwargs): # Don't bother with doc string, it will be over-written by createMethod. for name in six.iterkeys(kwargs): if name not in parameters.argmap: raise TypeError('Got an unexpected keyword argument "%s"' % name) # Remove args that have a value of None. keys = list(kwargs.keys()) for name in keys: if kwargs[name] is None: del kwargs[name] for name in parameters.required_params: if name not in kwargs: # temporary workaround for non-paging methods incorrectly requiring # page token parameter (cf. drive.changes.watch vs. drive.changes.list) if name not in _PAGE_TOKEN_NAMES or _findPageTokenName( _methodProperties(methodDesc, schema, 'response')): raise TypeError('Missing required parameter "%s"' % name) for name, regex in six.iteritems(parameters.pattern_params): if name in kwargs: if isinstance(kwargs[name], six.string_types): pvalues = [kwargs[name]] else: pvalues = kwargs[name] for pvalue in pvalues: if re.match(regex, pvalue) is None: raise TypeError( 'Parameter "%s" value "%s" does not match the pattern "%s"' % (name, pvalue, regex)) for name, enums in six.iteritems(parameters.enum_params): if name in kwargs: # We need to handle the case of a repeated enum # name differently, since we want to handle both # arg='value' and arg=['value1', 'value2'] if (name in parameters.repeated_params and not isinstance(kwargs[name], six.string_types)): values = kwargs[name] else: values = [kwargs[name]] for value in values: if value not in enums: raise TypeError( 'Parameter "%s" value "%s" is not an allowed value in "%s"' % (name, value, str(enums))) actual_query_params = {} actual_path_params = {} for key, value in six.iteritems(kwargs): to_type = parameters.param_types.get(key, 'string') # For repeated parameters we cast each member of the list. if key in parameters.repeated_params and type(value) == type([]): cast_value = [_cast(x, to_type) for x in value] else: cast_value = _cast(value, to_type) if key in parameters.query_params: actual_query_params[parameters.argmap[key]] = cast_value if key in parameters.path_params: actual_path_params[parameters.argmap[key]] = cast_value body_value = kwargs.get('body', None) media_filename = kwargs.get('media_body', None) media_mime_type = kwargs.get('media_mime_type', None) if self._developerKey: actual_query_params['key'] = self._developerKey model = self._model if methodName.endswith('_media'): model = MediaModel() elif 'response' not in methodDesc: model = RawModel() headers = {} headers, params, query, body = model.request(headers, actual_path_params, actual_query_params, body_value) expanded_url = uritemplate.expand(pathUrl, params) url = _urljoin(self._baseUrl, expanded_url + query) resumable = None multipart_boundary = '' if media_filename: # Ensure we end up with a valid MediaUpload object. if isinstance(media_filename, six.string_types): if media_mime_type is None: logger.warning( 'media_mime_type argument not specified: trying to auto-detect for %s', media_filename) media_mime_type, _ = mimetypes.guess_type(media_filename) if media_mime_type is None: raise UnknownFileType(media_filename) if not mimeparse.best_match([media_mime_type], ','.join(accept)): raise UnacceptableMimeTypeError(media_mime_type) media_upload = MediaFileUpload(media_filename, mimetype=media_mime_type) elif isinstance(media_filename, MediaUpload): media_upload = media_filename else: raise TypeError('media_filename must be str or MediaUpload.') # Check the maxSize if media_upload.size( ) is not None and media_upload.size() > maxSize > 0: raise MediaUploadSizeError("Media larger than: %s" % maxSize) # Use the media path uri for media uploads expanded_url = uritemplate.expand(mediaPathUrl, params) url = _urljoin(self._baseUrl, expanded_url + query) if media_upload.resumable(): url = _add_query_parameter(url, 'uploadType', 'resumable') if media_upload.resumable(): # This is all we need to do for resumable, if the body exists it gets # sent in the first request, otherwise an empty body is sent. resumable = media_upload else: # A non-resumable upload if body is None: # This is a simple media upload headers['content-type'] = media_upload.mimetype() body = media_upload.getbytes(0, media_upload.size()) url = _add_query_parameter(url, 'uploadType', 'media') else: # This is a multipart/related upload. msgRoot = MIMEMultipart('related') # msgRoot should not write out it's own headers setattr(msgRoot, '_write_headers', lambda self: None) # attach the body as one part msg = MIMENonMultipart(*headers['content-type'].split('/')) msg.set_payload(body) msgRoot.attach(msg) # attach the media as the second part msg = MIMENonMultipart(*media_upload.mimetype().split('/')) msg['Content-Transfer-Encoding'] = 'binary' payload = media_upload.getbytes(0, media_upload.size()) msg.set_payload(payload) msgRoot.attach(msg) # encode the body: note that we can't use `as_string`, because # it plays games with `From ` lines. fp = BytesIO() g = _BytesGenerator(fp, mangle_from_=False) g.flatten(msgRoot, unixfrom=False) body = fp.getvalue() multipart_boundary = msgRoot.get_boundary() headers['content-type'] = ( 'multipart/related; ' 'boundary="%s"') % multipart_boundary url = _add_query_parameter(url, 'uploadType', 'multipart') logger.info('URL being requested: %s %s' % (httpMethod, url)) return self._requestBuilder(self._http, model.response, url, method=httpMethod, body=body, headers=headers, methodId=methodId, resumable=resumable)