diff --git a/googleapiclient/__init__.py b/googleapiclient/__init__.py deleted file mode 100644 index fe31691..0000000 --- a/googleapiclient/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright (C) 2012 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -__version__ = "1.2" diff --git a/googleapiclient/channel.py b/googleapiclient/channel.py deleted file mode 100644 index 265273e..0000000 --- a/googleapiclient/channel.py +++ /dev/null @@ -1,285 +0,0 @@ -"""Channel notifications support. - -Classes and functions to support channel subscriptions and notifications -on those channels. - -Notes: - - This code is based on experimental APIs and is subject to change. - - Notification does not do deduplication of notification ids, that's up to - the receiver. - - Storing the Channel between calls is up to the caller. - - -Example setting up a channel: - - # Create a new channel that gets notifications via webhook. - channel = new_webhook_channel("https://example.com/my_web_hook") - - # Store the channel, keyed by 'channel.id'. Store it before calling the - # watch method because notifications may start arriving before the watch - # method returns. - ... - - resp = service.objects().watchAll( - bucket="some_bucket_id", body=channel.body()).execute() - channel.update(resp) - - # Store the channel, keyed by 'channel.id'. Store it after being updated - # since the resource_id value will now be correct, and that's needed to - # stop a subscription. - ... - - -An example Webhook implementation using webapp2. Note that webapp2 puts -headers in a case insensitive dictionary, as headers aren't guaranteed to -always be upper case. - - id = self.request.headers[X_GOOG_CHANNEL_ID] - - # Retrieve the channel by id. - channel = ... - - # Parse notification from the headers, including validating the id. - n = notification_from_headers(channel, self.request.headers) - - # Do app specific stuff with the notification here. - if n.resource_state == 'sync': - # Code to handle sync state. - elif n.resource_state == 'exists': - # Code to handle the exists state. - elif n.resource_state == 'not_exists': - # Code to handle the not exists state. - - -Example of unsubscribing. - - service.channels().stop(channel.body()) -""" - -import datetime -import uuid - -from googleapiclient import errors -from oauth2client import util - - -# The unix time epoch starts at midnight 1970. -EPOCH = datetime.datetime.utcfromtimestamp(0) - -# Map the names of the parameters in the JSON channel description to -# the parameter names we use in the Channel class. -CHANNEL_PARAMS = { - 'address': 'address', - 'id': 'id', - 'expiration': 'expiration', - 'params': 'params', - 'resourceId': 'resource_id', - 'resourceUri': 'resource_uri', - 'type': 'type', - 'token': 'token', - } - -X_GOOG_CHANNEL_ID = 'X-GOOG-CHANNEL-ID' -X_GOOG_MESSAGE_NUMBER = 'X-GOOG-MESSAGE-NUMBER' -X_GOOG_RESOURCE_STATE = 'X-GOOG-RESOURCE-STATE' -X_GOOG_RESOURCE_URI = 'X-GOOG-RESOURCE-URI' -X_GOOG_RESOURCE_ID = 'X-GOOG-RESOURCE-ID' - - -def _upper_header_keys(headers): - new_headers = {} - for k, v in headers.iteritems(): - new_headers[k.upper()] = v - return new_headers - - -class Notification(object): - """A Notification from a Channel. - - Notifications are not usually constructed directly, but are returned - from functions like notification_from_headers(). - - Attributes: - message_number: int, The unique id number of this notification. - state: str, The state of the resource being monitored. - uri: str, The address of the resource being monitored. - resource_id: str, The unique identifier of the version of the resource at - this event. - """ - @util.positional(5) - def __init__(self, message_number, state, resource_uri, resource_id): - """Notification constructor. - - Args: - message_number: int, The unique id number of this notification. - state: str, The state of the resource being monitored. Can be one - of "exists", "not_exists", or "sync". - resource_uri: str, The address of the resource being monitored. - resource_id: str, The identifier of the watched resource. - """ - self.message_number = message_number - self.state = state - self.resource_uri = resource_uri - self.resource_id = resource_id - - -class Channel(object): - """A Channel for notifications. - - Usually not constructed directly, instead it is returned from helper - functions like new_webhook_channel(). - - Attributes: - type: str, The type of delivery mechanism used by this channel. For - example, 'web_hook'. - id: str, A UUID for the channel. - token: str, An arbitrary string associated with the channel that - is delivered to the target address with each event delivered - over this channel. - address: str, The address of the receiving entity where events are - delivered. Specific to the channel type. - expiration: int, The time, in milliseconds from the epoch, when this - channel will expire. - params: dict, A dictionary of string to string, with additional parameters - controlling delivery channel behavior. - resource_id: str, An opaque id that identifies the resource that is - being watched. Stable across different API versions. - resource_uri: str, The canonicalized ID of the watched resource. - """ - - @util.positional(5) - def __init__(self, type, id, token, address, expiration=None, - params=None, resource_id="", resource_uri=""): - """Create a new Channel. - - In user code, this Channel constructor will not typically be called - manually since there are functions for creating channels for each specific - type with a more customized set of arguments to pass. - - Args: - type: str, The type of delivery mechanism used by this channel. For - example, 'web_hook'. - id: str, A UUID for the channel. - token: str, An arbitrary string associated with the channel that - is delivered to the target address with each event delivered - over this channel. - address: str, The address of the receiving entity where events are - delivered. Specific to the channel type. - expiration: int, The time, in milliseconds from the epoch, when this - channel will expire. - params: dict, A dictionary of string to string, with additional parameters - controlling delivery channel behavior. - resource_id: str, An opaque id that identifies the resource that is - being watched. Stable across different API versions. - resource_uri: str, The canonicalized ID of the watched resource. - """ - self.type = type - self.id = id - self.token = token - self.address = address - self.expiration = expiration - self.params = params - self.resource_id = resource_id - self.resource_uri = resource_uri - - def body(self): - """Build a body from the Channel. - - Constructs a dictionary that's appropriate for passing into watch() - methods as the value of body argument. - - Returns: - A dictionary representation of the channel. - """ - result = { - 'id': self.id, - 'token': self.token, - 'type': self.type, - 'address': self.address - } - if self.params: - result['params'] = self.params - if self.resource_id: - result['resourceId'] = self.resource_id - if self.resource_uri: - result['resourceUri'] = self.resource_uri - if self.expiration: - result['expiration'] = self.expiration - - return result - - def update(self, resp): - """Update a channel with information from the response of watch(). - - When a request is sent to watch() a resource, the response returned - from the watch() request is a dictionary with updated channel information, - such as the resource_id, which is needed when stopping a subscription. - - Args: - resp: dict, The response from a watch() method. - """ - for json_name, param_name in CHANNEL_PARAMS.iteritems(): - value = resp.get(json_name) - if value is not None: - setattr(self, param_name, value) - - -def notification_from_headers(channel, headers): - """Parse a notification from the webhook request headers, validate - the notification, and return a Notification object. - - Args: - channel: Channel, The channel that the notification is associated with. - headers: dict, A dictionary like object that contains the request headers - from the webhook HTTP request. - - Returns: - A Notification object. - - Raises: - errors.InvalidNotificationError if the notification is invalid. - ValueError if the X-GOOG-MESSAGE-NUMBER can't be converted to an int. - """ - headers = _upper_header_keys(headers) - channel_id = headers[X_GOOG_CHANNEL_ID] - if channel.id != channel_id: - raise errors.InvalidNotificationError( - 'Channel id mismatch: %s != %s' % (channel.id, channel_id)) - else: - message_number = int(headers[X_GOOG_MESSAGE_NUMBER]) - state = headers[X_GOOG_RESOURCE_STATE] - resource_uri = headers[X_GOOG_RESOURCE_URI] - resource_id = headers[X_GOOG_RESOURCE_ID] - return Notification(message_number, state, resource_uri, resource_id) - - -@util.positional(2) -def new_webhook_channel(url, token=None, expiration=None, params=None): - """Create a new webhook Channel. - - Args: - url: str, URL to post notifications to. - token: str, An arbitrary string associated with the channel that - is delivered to the target address with each notification delivered - over this channel. - expiration: datetime.datetime, A time in the future when the channel - should expire. Can also be None if the subscription should use the - default expiration. Note that different services may have different - limits on how long a subscription lasts. Check the response from the - watch() method to see the value the service has set for an expiration - time. - params: dict, Extra parameters to pass on channel creation. Currently - not used for webhook channels. - """ - expiration_ms = 0 - if expiration: - delta = expiration - EPOCH - expiration_ms = delta.microseconds/1000 + ( - delta.seconds + delta.days*24*3600)*1000 - if expiration_ms < 0: - expiration_ms = 0 - - return Channel('web_hook', str(uuid.uuid4()), - token, url, expiration=expiration_ms, - params=params) - diff --git a/googleapiclient/discovery.py b/googleapiclient/discovery.py deleted file mode 100644 index 1ed9921..0000000 --- a/googleapiclient/discovery.py +++ /dev/null @@ -1,959 +0,0 @@ -# Copyright (C) 2010 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Client for discovery based APIs. - -A client library for Google's discovery based APIs. -""" - -__author__ = 'jcgregorio@google.com (Joe Gregorio)' -__all__ = [ - 'build', - 'build_from_document', - 'fix_method_name', - 'key2param', - ] - - -# Standard library imports -import copy -from email.mime.multipart import MIMEMultipart -from email.mime.nonmultipart import MIMENonMultipart -import keyword -import logging -import mimetypes -import os -import re -import urllib -import urlparse - -try: - from urlparse import parse_qsl -except ImportError: - from cgi import parse_qsl - -# Third-party imports -import httplib2 -import mimeparse -import uritemplate - -# Local imports -from googleapiclient.errors import HttpError -from googleapiclient.errors import InvalidJsonError -from googleapiclient.errors import MediaUploadSizeError -from googleapiclient.errors import UnacceptableMimeTypeError -from googleapiclient.errors import UnknownApiNameOrVersion -from googleapiclient.errors import UnknownFileType -from googleapiclient.http import HttpRequest -from googleapiclient.http import MediaFileUpload -from googleapiclient.http import MediaUpload -from googleapiclient.model import JsonModel -from googleapiclient.model import MediaModel -from googleapiclient.model import RawModel -from googleapiclient.schema import Schemas -from oauth2client.anyjson import simplejson -from oauth2client.util import _add_query_parameter -from oauth2client.util import positional - - -# The client library requires a version of httplib2 that supports RETRIES. -httplib2.RETRIES = 1 - -logger = logging.getLogger(__name__) - -URITEMPLATE = re.compile('{[^}]*}') -VARNAME = re.compile('[a-zA-Z0-9_-]+') -DISCOVERY_URI = ('https://www.googleapis.com/discovery/v1/apis/' - '{api}/{apiVersion}/rest') -DEFAULT_METHOD_DOC = 'A description of how to use this function' -HTTP_PAYLOAD_METHODS = frozenset(['PUT', 'POST', 'PATCH']) -_MEDIA_SIZE_BIT_SHIFTS = {'KB': 10, 'MB': 20, 'GB': 30, 'TB': 40} -BODY_PARAMETER_DEFAULT_VALUE = { - 'description': 'The request body.', - 'type': 'object', - 'required': True, -} -MEDIA_BODY_PARAMETER_DEFAULT_VALUE = { - 'description': ('The filename of the media request body, or an instance ' - 'of a MediaUpload object.'), - 'type': 'string', - 'required': False, -} - -# Parameters accepted by the stack, but not visible via discovery. -# TODO(dhermes): Remove 'userip' in 'v2'. -STACK_QUERY_PARAMETERS = frozenset(['trace', 'pp', 'userip', 'strict']) -STACK_QUERY_PARAMETER_DEFAULT_VALUE = {'type': 'string', 'location': 'query'} - -# Library-specific reserved words beyond Python keywords. -RESERVED_WORDS = frozenset(['body']) - - -def fix_method_name(name): - """Fix method names to avoid reserved word conflicts. - - Args: - name: string, method name. - - Returns: - The name with a '_' prefixed if the name is a reserved word. - """ - if keyword.iskeyword(name) or name in RESERVED_WORDS: - return name + '_' - else: - return name - - -def key2param(key): - """Converts key names into parameter names. - - For example, converting "max-results" -> "max_results" - - Args: - key: string, the method key name. - - Returns: - A safe method name based on the key name. - """ - result = [] - key = list(key) - if not key[0].isalpha(): - result.append('x') - for c in key: - if c.isalnum(): - result.append(c) - else: - result.append('_') - - return ''.join(result) - - -@positional(2) -def build(serviceName, - version, - http=None, - discoveryServiceUrl=DISCOVERY_URI, - developerKey=None, - model=None, - requestBuilder=HttpRequest): - """Construct a Resource for interacting with an API. - - Construct a Resource object for interacting with an API. The serviceName and - version are the names from the Discovery service. - - Args: - serviceName: string, name of the service. - version: string, the version of the service. - http: httplib2.Http, An instance of httplib2.Http or something that acts - like it that HTTP requests will be made through. - discoveryServiceUrl: string, a URI Template that points to the location of - the discovery service. It should have two parameters {api} and - {apiVersion} that when filled in produce an absolute URI to the discovery - document for that service. - developerKey: string, key obtained from - https://code.google.com/apis/console. - model: googleapiclient.Model, converts to and from the wire format. - requestBuilder: googleapiclient.http.HttpRequest, encapsulator for an HTTP - request. - - Returns: - A Resource object with methods for interacting with the service. - """ - params = { - 'api': serviceName, - 'apiVersion': version - } - - if http is None: - http = httplib2.Http() - - requested_url = uritemplate.expand(discoveryServiceUrl, params) - - # REMOTE_ADDR is defined by the CGI spec [RFC3875] as the environment - # variable that contains the network address of the client sending the - # request. If it exists then add that to the request for the discovery - # document to avoid exceeding the quota on discovery requests. - if 'REMOTE_ADDR' in os.environ: - requested_url = _add_query_parameter(requested_url, 'userIp', - os.environ['REMOTE_ADDR']) - logger.info('URL being requested: %s' % requested_url) - - resp, content = http.request(requested_url) - - if resp.status == 404: - raise UnknownApiNameOrVersion("name: %s version: %s" % (serviceName, - version)) - if resp.status >= 400: - raise HttpError(resp, content, uri=requested_url) - - try: - service = simplejson.loads(content) - except ValueError, e: - logger.error('Failed to parse as JSON: ' + content) - raise InvalidJsonError() - - return build_from_document(content, base=discoveryServiceUrl, http=http, - developerKey=developerKey, model=model, requestBuilder=requestBuilder) - - -@positional(1) -def build_from_document( - service, - base=None, - future=None, - http=None, - developerKey=None, - model=None, - requestBuilder=HttpRequest): - """Create a Resource for interacting with an API. - - Same as `build()`, but constructs the Resource object from a discovery - document that is it given, as opposed to retrieving one over HTTP. - - Args: - service: string or object, the JSON discovery document describing the API. - The value passed in may either be the JSON string or the deserialized - JSON. - base: string, base URI for all HTTP requests, usually the discovery URI. - This parameter is no longer used as rootUrl and servicePath are included - within the discovery document. (deprecated) - future: string, discovery document with future capabilities (deprecated). - http: httplib2.Http, An instance of httplib2.Http or something that acts - like it that HTTP requests will be made through. - developerKey: string, Key for controlling API usage, generated - from the API Console. - model: Model class instance that serializes and de-serializes requests and - responses. - requestBuilder: Takes an http request and packages it up to be executed. - - Returns: - A Resource object with methods for interacting with the service. - """ - - # future is no longer used. - future = {} - - if isinstance(service, basestring): - service = simplejson.loads(service) - base = urlparse.urljoin(service['rootUrl'], service['servicePath']) - schema = Schemas(service) - - if model is None: - features = service.get('features', []) - model = JsonModel('dataWrapper' in features) - return Resource(http=http, baseUrl=base, model=model, - developerKey=developerKey, requestBuilder=requestBuilder, - resourceDesc=service, rootDesc=service, schema=schema) - - -def _cast(value, schema_type): - """Convert value to a string based on JSON Schema type. - - See http://tools.ietf.org/html/draft-zyp-json-schema-03 for more details on - JSON Schema. - - Args: - value: any, the value to convert - schema_type: string, the type that value should be interpreted as - - Returns: - A string representation of 'value' based on the schema_type. - """ - if schema_type == 'string': - if type(value) == type('') or type(value) == type(u''): - return value - else: - return str(value) - elif schema_type == 'integer': - return str(int(value)) - elif schema_type == 'number': - return str(float(value)) - elif schema_type == 'boolean': - return str(bool(value)).lower() - else: - if type(value) == type('') or type(value) == type(u''): - return value - else: - return str(value) - - -def _media_size_to_long(maxSize): - """Convert a string media size, such as 10GB or 3TB into an integer. - - Args: - maxSize: string, size as a string, such as 2MB or 7GB. - - Returns: - The size as an integer value. - """ - if len(maxSize) < 2: - return 0L - units = maxSize[-2:].upper() - bit_shift = _MEDIA_SIZE_BIT_SHIFTS.get(units) - if bit_shift is not None: - return long(maxSize[:-2]) << bit_shift - else: - return long(maxSize) - - -def _media_path_url_from_info(root_desc, path_url): - """Creates an absolute media path URL. - - Constructed using the API root URI and service path from the discovery - document and the relative path for the API method. - - Args: - root_desc: Dictionary; the entire original deserialized discovery document. - path_url: String; the relative URL for the API method. Relative to the API - root, which is specified in the discovery document. - - Returns: - String; the absolute URI for media upload for the API method. - """ - return '%(root)supload/%(service_path)s%(path)s' % { - 'root': root_desc['rootUrl'], - 'service_path': root_desc['servicePath'], - 'path': path_url, - } - - -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 root_desc.get('parameters', {}).iteritems(): - 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 _fix_up_media_upload(method_desc, root_desc, path_url, parameters): - """Updates parameters of API by adding 'media_body' if supported by method. - - SIDE EFFECTS: If the method supports media upload and has a required body, - sets body to be optional (required=False) instead. Also, if there is a - 'mediaUpload' in the method description, adds 'media_upload' key to - parameters. - - 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. - path_url: String; the relative URL for the API method. Relative to the API - root, which is specified in the discovery document. - parameters: A dictionary describing method parameters for method described - in method_desc. - - Returns: - Triple (accept, max_size, media_path_url) where: - - accept is a list of strings representing what content types are - accepted for media upload. Defaults to empty list if not in the - discovery document. - - max_size is a long representing the max size in bytes allowed for a - media upload. Defaults to 0L if not in the discovery document. - - media_path_url is a String; the absolute URI for media upload for the - API method. Constructed using the API root URI and service path from - the discovery document and the relative path for the API method. If - media upload is not supported, this is None. - """ - media_upload = method_desc.get('mediaUpload', {}) - accept = media_upload.get('accept', []) - max_size = _media_size_to_long(media_upload.get('maxSize', '')) - media_path_url = None - - if media_upload: - media_path_url = _media_path_url_from_info(root_desc, path_url) - parameters['media_body'] = MEDIA_BODY_PARAMETER_DEFAULT_VALUE.copy() - if 'body' in parameters: - parameters['body']['required'] = False - - return accept, max_size, media_path_url - - -def _fix_up_method_description(method_desc, root_desc): - """Updates a method description in a discovery document. - - SIDE EFFECTS: Changes the parameters dictionary in the method description with - extra parameters which are used locally. - - 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. - - Returns: - Tuple (path_url, http_method, method_id, accept, max_size, media_path_url) - where: - - path_url is a String; the relative URL for the API method. Relative to - the API root, which is specified in the discovery document. - - http_method is a String; the HTTP method used to call the API method - described in the method description. - - method_id is a String; the name of the RPC method associated with the - API method, and is in the method description in the 'id' key. - - accept is a list of strings representing what content types are - accepted for media upload. Defaults to empty list if not in the - discovery document. - - max_size is a long representing the max size in bytes allowed for a - media upload. Defaults to 0L if not in the discovery document. - - media_path_url is a String; the absolute URI for media upload for the - API method. Constructed using the API root URI and service path from - the discovery document and the relative path for the API method. If - media upload is not supported, this is None. - """ - path_url = method_desc['path'] - http_method = method_desc['httpMethod'] - method_id = method_desc['id'] - - parameters = _fix_up_parameters(method_desc, root_desc, http_method) - # Order is important. `_fix_up_media_upload` needs `method_desc` to have a - # 'parameters' key and needs to know if there is a 'body' parameter because it - # also sets a 'media_body' parameter. - accept, max_size, media_path_url = _fix_up_media_upload( - method_desc, root_desc, path_url, parameters) - - return path_url, http_method, method_id, accept, max_size, media_path_url - - -# TODO(dhermes): Convert this class to ResourceMethod and make it callable -class ResourceMethodParameters(object): - """Represents the parameters associated with a method. - - Attributes: - argmap: Map from method parameter name (string) to query parameter name - (string). - required_params: List of required parameters (represented by parameter - name as string). - repeated_params: List of repeated parameters (represented by parameter - name as string). - pattern_params: Map from method parameter name (string) to regular - expression (as a string). If the pattern is set for a parameter, the - value for that parameter must match the regular expression. - query_params: List of parameters (represented by parameter name as string) - that will be used in the query string. - path_params: Set of parameters (represented by parameter name as string) - that will be used in the base URL path. - param_types: Map from method parameter name (string) to parameter type. Type - can be any valid JSON schema type; valid values are 'any', 'array', - 'boolean', 'integer', 'number', 'object', or 'string'. Reference: - http://tools.ietf.org/html/draft-zyp-json-schema-03#section-5.1 - enum_params: Map from method parameter name (string) to list of strings, - where each list of strings is the list of acceptable enum values. - """ - - def __init__(self, method_desc): - """Constructor for ResourceMethodParameters. - - Sets default values and defers to set_parameters to populate. - - 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. - """ - self.argmap = {} - self.required_params = [] - self.repeated_params = [] - self.pattern_params = {} - self.query_params = [] - # TODO(dhermes): Change path_params to a list if the extra URITEMPLATE - # parsing is gotten rid of. - self.path_params = set() - self.param_types = {} - self.enum_params = {} - - self.set_parameters(method_desc) - - 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 method_desc.get('parameters', {}).iteritems(): - 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 createMethod(methodName, methodDesc, rootDesc, schema): - """Creates a method for attaching to a Resource. - - Args: - methodName: string, name of the method to use. - methodDesc: object, fragment of deserialized discovery document that - describes the method. - rootDesc: object, the entire deserialized discovery document. - schema: object, mapping of schema names to schema descriptions. - """ - methodName = fix_method_name(methodName) - (pathUrl, httpMethod, methodId, accept, - maxSize, mediaPathUrl) = _fix_up_method_description(methodDesc, rootDesc) - - parameters = ResourceMethodParameters(methodDesc) - - def method(self, **kwargs): - # Don't bother with doc string, it will be over-written by createMethod. - - for name in kwargs.iterkeys(): - if name not in parameters.argmap: - raise TypeError('Got an unexpected keyword argument "%s"' % name) - - # Remove args that have a value of None. - keys = 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: - raise TypeError('Missing required parameter "%s"' % name) - - for name, regex in parameters.pattern_params.iteritems(): - if name in kwargs: - if isinstance(kwargs[name], basestring): - 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 parameters.enum_params.iteritems(): - 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], basestring)): - 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 kwargs.iteritems(): - 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) - - 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 = urlparse.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, basestring): - (media_mime_type, encoding) = 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 maxSize > 0 and media_upload.size() > maxSize: - raise MediaUploadSizeError("Media larger than: %s" % maxSize) - - # Use the media path uri for media uploads - expanded_url = uritemplate.expand(mediaPathUrl, params) - url = urlparse.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) - body = msgRoot.as_string() - - 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' % url) - return self._requestBuilder(self._http, - model.response, - url, - method=httpMethod, - body=body, - headers=headers, - methodId=methodId, - resumable=resumable) - - docs = [methodDesc.get('description', DEFAULT_METHOD_DOC), '\n\n'] - if len(parameters.argmap) > 0: - docs.append('Args:\n') - - # Skip undocumented params and params common to all methods. - skip_parameters = rootDesc.get('parameters', {}).keys() - skip_parameters.extend(STACK_QUERY_PARAMETERS) - - all_args = parameters.argmap.keys() - args_ordered = [key2param(s) for s in methodDesc.get('parameterOrder', [])] - - # Move body to the front of the line. - if 'body' in all_args: - args_ordered.append('body') - - for name in all_args: - if name not in args_ordered: - args_ordered.append(name) - - for arg in args_ordered: - if arg in skip_parameters: - continue - - repeated = '' - if arg in parameters.repeated_params: - repeated = ' (repeated)' - required = '' - if arg in parameters.required_params: - required = ' (required)' - paramdesc = methodDesc['parameters'][parameters.argmap[arg]] - paramdoc = paramdesc.get('description', 'A parameter') - if '$ref' in paramdesc: - docs.append( - (' %s: object, %s%s%s\n The object takes the' - ' form of:\n\n%s\n\n') % (arg, paramdoc, required, repeated, - schema.prettyPrintByName(paramdesc['$ref']))) - else: - paramtype = paramdesc.get('type', 'string') - docs.append(' %s: %s, %s%s%s\n' % (arg, paramtype, paramdoc, required, - repeated)) - enum = paramdesc.get('enum', []) - enumDesc = paramdesc.get('enumDescriptions', []) - if enum and enumDesc: - docs.append(' Allowed values\n') - for (name, desc) in zip(enum, enumDesc): - docs.append(' %s - %s\n' % (name, desc)) - if 'response' in methodDesc: - if methodName.endswith('_media'): - docs.append('\nReturns:\n The media object as a string.\n\n ') - else: - docs.append('\nReturns:\n An object of the form:\n\n ') - docs.append(schema.prettyPrintSchema(methodDesc['response'])) - - setattr(method, '__doc__', ''.join(docs)) - return (methodName, method) - - -def createNextMethod(methodName): - """Creates any _next methods for attaching to a Resource. - - The _next methods allow for easy iteration through list() responses. - - Args: - methodName: string, name of the method to use. - """ - methodName = fix_method_name(methodName) - - def methodNext(self, previous_request, previous_response): - """Retrieves the next page of results. - -Args: - previous_request: The request for the previous page. (required) - previous_response: The response from the request for the previous page. (required) - -Returns: - A request object that you can call 'execute()' on to request the next - page. Returns None if there are no more items in the collection. - """ - # Retrieve nextPageToken from previous_response - # Use as pageToken in previous_request to create new request. - - if 'nextPageToken' not in previous_response: - return None - - request = copy.copy(previous_request) - - pageToken = previous_response['nextPageToken'] - parsed = list(urlparse.urlparse(request.uri)) - q = parse_qsl(parsed[4]) - - # Find and remove old 'pageToken' value from URI - newq = [(key, value) for (key, value) in q if key != 'pageToken'] - newq.append(('pageToken', pageToken)) - parsed[4] = urllib.urlencode(newq) - uri = urlparse.urlunparse(parsed) - - request.uri = uri - - logger.info('URL being requested: %s' % uri) - - return request - - return (methodName, methodNext) - - -class Resource(object): - """A class for interacting with a resource.""" - - def __init__(self, http, baseUrl, model, requestBuilder, developerKey, - resourceDesc, rootDesc, schema): - """Build a Resource from the API description. - - Args: - http: httplib2.Http, Object to make http requests with. - baseUrl: string, base URL for the API. All requests are relative to this - URI. - model: googleapiclient.Model, converts to and from the wire format. - requestBuilder: class or callable that instantiates an - googleapiclient.HttpRequest object. - developerKey: string, key obtained from - https://code.google.com/apis/console - resourceDesc: object, section of deserialized discovery document that - describes a resource. Note that the top level discovery document - is considered a resource. - rootDesc: object, the entire deserialized discovery document. - schema: object, mapping of schema names to schema descriptions. - """ - self._dynamic_attrs = [] - - self._http = http - self._baseUrl = baseUrl - self._model = model - self._developerKey = developerKey - self._requestBuilder = requestBuilder - self._resourceDesc = resourceDesc - self._rootDesc = rootDesc - self._schema = schema - - self._set_service_methods() - - def _set_dynamic_attr(self, attr_name, value): - """Sets an instance attribute and tracks it in a list of dynamic attributes. - - Args: - attr_name: string; The name of the attribute to be set - value: The value being set on the object and tracked in the dynamic cache. - """ - self._dynamic_attrs.append(attr_name) - self.__dict__[attr_name] = value - - def __getstate__(self): - """Trim the state down to something that can be pickled. - - Uses the fact that the instance variable _dynamic_attrs holds attrs that - will be wiped and restored on pickle serialization. - """ - state_dict = copy.copy(self.__dict__) - for dynamic_attr in self._dynamic_attrs: - del state_dict[dynamic_attr] - del state_dict['_dynamic_attrs'] - return state_dict - - def __setstate__(self, state): - """Reconstitute the state of the object from being pickled. - - Uses the fact that the instance variable _dynamic_attrs holds attrs that - will be wiped and restored on pickle serialization. - """ - self.__dict__.update(state) - self._dynamic_attrs = [] - self._set_service_methods() - - def _set_service_methods(self): - self._add_basic_methods(self._resourceDesc, self._rootDesc, self._schema) - self._add_nested_resources(self._resourceDesc, self._rootDesc, self._schema) - self._add_next_methods(self._resourceDesc, self._schema) - - def _add_basic_methods(self, resourceDesc, rootDesc, schema): - # Add basic methods to Resource - if 'methods' in resourceDesc: - for methodName, methodDesc in resourceDesc['methods'].iteritems(): - 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 _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 resourceDesc['resources'].iteritems(): - fixedMethodName, method = createResourceMethod(methodName, methodDesc) - self._set_dynamic_attr(fixedMethodName, - method.__get__(self, self.__class__)) - - def _add_next_methods(self, resourceDesc, schema): - # Add _next() methods - # Look for response bodies in schema that contain nextPageToken, and methods - # that take a pageToken parameter. - if 'methods' in resourceDesc: - for methodName, methodDesc in resourceDesc['methods'].iteritems(): - if 'response' in methodDesc: - responseSchema = methodDesc['response'] - if '$ref' in responseSchema: - responseSchema = schema.get(responseSchema['$ref']) - hasNextPageToken = 'nextPageToken' in responseSchema.get('properties', - {}) - hasPageToken = 'pageToken' in methodDesc.get('parameters', {}) - if hasNextPageToken and hasPageToken: - fixedMethodName, method = createNextMethod(methodName + '_next') - self._set_dynamic_attr(fixedMethodName, - method.__get__(self, self.__class__)) diff --git a/googleapiclient/errors.py b/googleapiclient/errors.py deleted file mode 100644 index ef2b161..0000000 --- a/googleapiclient/errors.py +++ /dev/null @@ -1,140 +0,0 @@ -#!/usr/bin/python2.4 -# -# Copyright (C) 2010 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Errors for the library. - -All exceptions defined by the library -should be defined in this file. -""" - -__author__ = 'jcgregorio@google.com (Joe Gregorio)' - - -from oauth2client import util -from oauth2client.anyjson import simplejson - - -class Error(Exception): - """Base error for this module.""" - pass - - -class HttpError(Error): - """HTTP data was invalid or unexpected.""" - - @util.positional(3) - def __init__(self, resp, content, uri=None): - self.resp = resp - self.content = content - self.uri = uri - - def _get_reason(self): - """Calculate the reason for the error from the response content.""" - reason = self.resp.reason - try: - data = simplejson.loads(self.content) - reason = data['error']['message'] - except (ValueError, KeyError): - pass - if reason is None: - reason = '' - return reason - - def __repr__(self): - if self.uri: - return '' % ( - self.resp.status, self.uri, self._get_reason().strip()) - else: - return '' % (self.resp.status, self._get_reason()) - - __str__ = __repr__ - - -class InvalidJsonError(Error): - """The JSON returned could not be parsed.""" - pass - - -class UnknownFileType(Error): - """File type unknown or unexpected.""" - pass - - -class UnknownLinkType(Error): - """Link type unknown or unexpected.""" - pass - - -class UnknownApiNameOrVersion(Error): - """No API with that name and version exists.""" - pass - - -class UnacceptableMimeTypeError(Error): - """That is an unacceptable mimetype for this operation.""" - pass - - -class MediaUploadSizeError(Error): - """Media is larger than the method can accept.""" - pass - - -class ResumableUploadError(HttpError): - """Error occured during resumable upload.""" - pass - - -class InvalidChunkSizeError(Error): - """The given chunksize is not valid.""" - pass - -class InvalidNotificationError(Error): - """The channel Notification is invalid.""" - pass - -class BatchError(HttpError): - """Error occured during batch operations.""" - - @util.positional(2) - def __init__(self, reason, resp=None, content=None): - self.resp = resp - self.content = content - self.reason = reason - - def __repr__(self): - return '' % (self.resp.status, self.reason) - - __str__ = __repr__ - - -class UnexpectedMethodError(Error): - """Exception raised by RequestMockBuilder on unexpected calls.""" - - @util.positional(1) - def __init__(self, methodId=None): - """Constructor for an UnexpectedMethodError.""" - super(UnexpectedMethodError, self).__init__( - 'Received unexpected call %s' % methodId) - - -class UnexpectedBodyError(Error): - """Exception raised by RequestMockBuilder on unexpected bodies.""" - - def __init__(self, expected, provided): - """Constructor for an UnexpectedMethodError.""" - super(UnexpectedBodyError, self).__init__( - 'Expected: [%s] - Provided: [%s]' % (expected, provided)) diff --git a/googleapiclient/http.py b/googleapiclient/http.py deleted file mode 100644 index 2b65348..0000000 --- a/googleapiclient/http.py +++ /dev/null @@ -1,1609 +0,0 @@ -# Copyright (C) 2012 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Classes to encapsulate a single HTTP request. - -The classes implement a command pattern, with every -object supporting an execute() method that does the -actuall HTTP request. -""" - -__author__ = 'jcgregorio@google.com (Joe Gregorio)' - -import StringIO -import base64 -import copy -import gzip -import httplib2 -import logging -import mimeparse -import mimetypes -import os -import random -import sys -import time -import urllib -import urlparse -import uuid - -from email.generator import Generator -from email.mime.multipart import MIMEMultipart -from email.mime.nonmultipart import MIMENonMultipart -from email.parser import FeedParser -from errors import BatchError -from errors import HttpError -from errors import InvalidChunkSizeError -from errors import ResumableUploadError -from errors import UnexpectedBodyError -from errors import UnexpectedMethodError -from model import JsonModel -from oauth2client import util -from oauth2client.anyjson import simplejson - - -DEFAULT_CHUNK_SIZE = 512*1024 - -MAX_URI_LENGTH = 2048 - - -class MediaUploadProgress(object): - """Status of a resumable upload.""" - - def __init__(self, resumable_progress, total_size): - """Constructor. - - Args: - resumable_progress: int, bytes sent so far. - total_size: int, total bytes in complete upload, or None if the total - upload size isn't known ahead of time. - """ - self.resumable_progress = resumable_progress - self.total_size = total_size - - def progress(self): - """Percent of upload completed, as a float. - - Returns: - the percentage complete as a float, returning 0.0 if the total size of - the upload is unknown. - """ - if self.total_size is not None: - return float(self.resumable_progress) / float(self.total_size) - else: - return 0.0 - - -class MediaDownloadProgress(object): - """Status of a resumable download.""" - - def __init__(self, resumable_progress, total_size): - """Constructor. - - Args: - resumable_progress: int, bytes received so far. - total_size: int, total bytes in complete download. - """ - self.resumable_progress = resumable_progress - self.total_size = total_size - - def progress(self): - """Percent of download completed, as a float. - - Returns: - the percentage complete as a float, returning 0.0 if the total size of - the download is unknown. - """ - if self.total_size is not None: - return float(self.resumable_progress) / float(self.total_size) - else: - return 0.0 - - -class MediaUpload(object): - """Describes a media object to upload. - - Base class that defines the interface of MediaUpload subclasses. - - Note that subclasses of MediaUpload may allow you to control the chunksize - when uploading a media object. It is important to keep the size of the chunk - as large as possible to keep the upload efficient. Other factors may influence - the size of the chunk you use, particularly if you are working in an - environment where individual HTTP requests may have a hardcoded time limit, - such as under certain classes of requests under Google App Engine. - - Streams are io.Base compatible objects that support seek(). Some MediaUpload - subclasses support using streams directly to upload data. Support for - streaming may be indicated by a MediaUpload sub-class and if appropriate for a - platform that stream will be used for uploading the media object. The support - for streaming is indicated by has_stream() returning True. The stream() method - should return an io.Base object that supports seek(). On platforms where the - underlying httplib module supports streaming, for example Python 2.6 and - later, the stream will be passed into the http library which will result in - less memory being used and possibly faster uploads. - - If you need to upload media that can't be uploaded using any of the existing - MediaUpload sub-class then you can sub-class MediaUpload for your particular - needs. - """ - - def chunksize(self): - """Chunk size for resumable uploads. - - Returns: - Chunk size in bytes. - """ - raise NotImplementedError() - - def mimetype(self): - """Mime type of the body. - - Returns: - Mime type. - """ - return 'application/octet-stream' - - def size(self): - """Size of upload. - - Returns: - Size of the body, or None of the size is unknown. - """ - return None - - def resumable(self): - """Whether this upload is resumable. - - Returns: - True if resumable upload or False. - """ - return False - - def getbytes(self, begin, end): - """Get bytes from the media. - - Args: - begin: int, offset from beginning of file. - length: int, number of bytes to read, starting at begin. - - Returns: - A string of bytes read. May be shorter than length if EOF was reached - first. - """ - raise NotImplementedError() - - def has_stream(self): - """Does the underlying upload support a streaming interface. - - Streaming means it is an io.IOBase subclass that supports seek, i.e. - seekable() returns True. - - Returns: - True if the call to stream() will return an instance of a seekable io.Base - subclass. - """ - return False - - def stream(self): - """A stream interface to the data being uploaded. - - Returns: - The returned value is an io.IOBase subclass that supports seek, i.e. - seekable() returns True. - """ - raise NotImplementedError() - - @util.positional(1) - def _to_json(self, strip=None): - """Utility function for creating a JSON representation of a MediaUpload. - - Args: - strip: array, An array of names of members to not include in the JSON. - - Returns: - string, a JSON representation of this instance, suitable to pass to - from_json(). - """ - t = type(self) - d = copy.copy(self.__dict__) - if strip is not None: - for member in strip: - del d[member] - d['_class'] = t.__name__ - d['_module'] = t.__module__ - return simplejson.dumps(d) - - def to_json(self): - """Create a JSON representation of an instance of MediaUpload. - - Returns: - string, a JSON representation of this instance, suitable to pass to - from_json(). - """ - return self._to_json() - - @classmethod - def new_from_json(cls, s): - """Utility class method to instantiate a MediaUpload subclass from a JSON - representation produced by to_json(). - - Args: - s: string, JSON from to_json(). - - Returns: - An instance of the subclass of MediaUpload that was serialized with - to_json(). - """ - data = simplejson.loads(s) - # Find and call the right classmethod from_json() to restore the object. - module = data['_module'] - m = __import__(module, fromlist=module.split('.')[:-1]) - kls = getattr(m, data['_class']) - from_json = getattr(kls, 'from_json') - return from_json(s) - - -class MediaIoBaseUpload(MediaUpload): - """A MediaUpload for a io.Base objects. - - Note that the Python file object is compatible with io.Base and can be used - with this class also. - - fh = io.BytesIO('...Some data to upload...') - media = MediaIoBaseUpload(fh, mimetype='image/png', - chunksize=1024*1024, resumable=True) - farm.animals().insert( - id='cow', - name='cow.png', - media_body=media).execute() - - Depending on the platform you are working on, you may pass -1 as the - chunksize, which indicates that the entire file should be uploaded in a single - request. If the underlying platform supports streams, such as Python 2.6 or - later, then this can be very efficient as it avoids multiple connections, and - also avoids loading the entire file into memory before sending it. Note that - Google App Engine has a 5MB limit on request size, so you should never set - your chunksize larger than 5MB, or to -1. - """ - - @util.positional(3) - def __init__(self, fd, mimetype, chunksize=DEFAULT_CHUNK_SIZE, - resumable=False): - """Constructor. - - Args: - fd: io.Base or file object, The source of the bytes to upload. MUST be - opened in blocking mode, do not use streams opened in non-blocking mode. - The given stream must be seekable, that is, it must be able to call - seek() on fd. - mimetype: string, Mime-type of the file. - chunksize: int, File will be uploaded in chunks of this many bytes. Only - used if resumable=True. Pass in a value of -1 if the file is to be - uploaded as a single chunk. Note that Google App Engine has a 5MB limit - on request size, so you should never set your chunksize larger than 5MB, - or to -1. - resumable: bool, True if this is a resumable upload. False means upload - in a single request. - """ - super(MediaIoBaseUpload, self).__init__() - self._fd = fd - self._mimetype = mimetype - if not (chunksize == -1 or chunksize > 0): - raise InvalidChunkSizeError() - self._chunksize = chunksize - self._resumable = resumable - - self._fd.seek(0, os.SEEK_END) - self._size = self._fd.tell() - - def chunksize(self): - """Chunk size for resumable uploads. - - Returns: - Chunk size in bytes. - """ - return self._chunksize - - def mimetype(self): - """Mime type of the body. - - Returns: - Mime type. - """ - return self._mimetype - - def size(self): - """Size of upload. - - Returns: - Size of the body, or None of the size is unknown. - """ - return self._size - - def resumable(self): - """Whether this upload is resumable. - - Returns: - True if resumable upload or False. - """ - return self._resumable - - def getbytes(self, begin, length): - """Get bytes from the media. - - Args: - begin: int, offset from beginning of file. - length: int, number of bytes to read, starting at begin. - - Returns: - A string of bytes read. May be shorted than length if EOF was reached - first. - """ - self._fd.seek(begin) - return self._fd.read(length) - - def has_stream(self): - """Does the underlying upload support a streaming interface. - - Streaming means it is an io.IOBase subclass that supports seek, i.e. - seekable() returns True. - - Returns: - True if the call to stream() will return an instance of a seekable io.Base - subclass. - """ - return True - - def stream(self): - """A stream interface to the data being uploaded. - - Returns: - The returned value is an io.IOBase subclass that supports seek, i.e. - seekable() returns True. - """ - return self._fd - - def to_json(self): - """This upload type is not serializable.""" - raise NotImplementedError('MediaIoBaseUpload is not serializable.') - - -class MediaFileUpload(MediaIoBaseUpload): - """A MediaUpload for a file. - - Construct a MediaFileUpload and pass as the media_body parameter of the - method. For example, if we had a service that allowed uploading images: - - - media = MediaFileUpload('cow.png', mimetype='image/png', - chunksize=1024*1024, resumable=True) - farm.animals().insert( - id='cow', - name='cow.png', - media_body=media).execute() - - Depending on the platform you are working on, you may pass -1 as the - chunksize, which indicates that the entire file should be uploaded in a single - request. If the underlying platform supports streams, such as Python 2.6 or - later, then this can be very efficient as it avoids multiple connections, and - also avoids loading the entire file into memory before sending it. Note that - Google App Engine has a 5MB limit on request size, so you should never set - your chunksize larger than 5MB, or to -1. - """ - - @util.positional(2) - def __init__(self, filename, mimetype=None, chunksize=DEFAULT_CHUNK_SIZE, - resumable=False): - """Constructor. - - Args: - filename: string, Name of the file. - mimetype: string, Mime-type of the file. If None then a mime-type will be - guessed from the file extension. - chunksize: int, File will be uploaded in chunks of this many bytes. Only - used if resumable=True. Pass in a value of -1 if the file is to be - uploaded in a single chunk. Note that Google App Engine has a 5MB limit - on request size, so you should never set your chunksize larger than 5MB, - or to -1. - resumable: bool, True if this is a resumable upload. False means upload - in a single request. - """ - self._filename = filename - fd = open(self._filename, 'rb') - if mimetype is None: - (mimetype, encoding) = mimetypes.guess_type(filename) - super(MediaFileUpload, self).__init__(fd, mimetype, chunksize=chunksize, - resumable=resumable) - - def to_json(self): - """Creating a JSON representation of an instance of MediaFileUpload. - - Returns: - string, a JSON representation of this instance, suitable to pass to - from_json(). - """ - return self._to_json(strip=['_fd']) - - @staticmethod - def from_json(s): - d = simplejson.loads(s) - return MediaFileUpload(d['_filename'], mimetype=d['_mimetype'], - chunksize=d['_chunksize'], resumable=d['_resumable']) - - -class MediaInMemoryUpload(MediaIoBaseUpload): - """MediaUpload for a chunk of bytes. - - DEPRECATED: Use MediaIoBaseUpload with either io.TextIOBase or StringIO for - the stream. - """ - - @util.positional(2) - def __init__(self, body, mimetype='application/octet-stream', - chunksize=DEFAULT_CHUNK_SIZE, resumable=False): - """Create a new MediaInMemoryUpload. - - DEPRECATED: Use MediaIoBaseUpload with either io.TextIOBase or StringIO for - the stream. - - Args: - body: string, Bytes of body content. - mimetype: string, Mime-type of the file or default of - 'application/octet-stream'. - chunksize: int, File will be uploaded in chunks of this many bytes. Only - used if resumable=True. - resumable: bool, True if this is a resumable upload. False means upload - in a single request. - """ - fd = StringIO.StringIO(body) - super(MediaInMemoryUpload, self).__init__(fd, mimetype, chunksize=chunksize, - resumable=resumable) - - -class MediaIoBaseDownload(object): - """"Download media resources. - - Note that the Python file object is compatible with io.Base and can be used - with this class also. - - - Example: - request = farms.animals().get_media(id='cow') - fh = io.FileIO('cow.png', mode='wb') - downloader = MediaIoBaseDownload(fh, request, chunksize=1024*1024) - - done = False - while done is False: - status, done = downloader.next_chunk() - if status: - print "Download %d%%." % int(status.progress() * 100) - print "Download Complete!" - """ - - @util.positional(3) - def __init__(self, fd, request, chunksize=DEFAULT_CHUNK_SIZE): - """Constructor. - - Args: - fd: io.Base or file object, The stream in which to write the downloaded - bytes. - request: googleapiclient.http.HttpRequest, the media request to perform in - chunks. - chunksize: int, File will be downloaded in chunks of this many bytes. - """ - self._fd = fd - self._request = request - self._uri = request.uri - self._chunksize = chunksize - self._progress = 0 - self._total_size = None - self._done = False - - # Stubs for testing. - self._sleep = time.sleep - self._rand = random.random - - @util.positional(1) - def next_chunk(self, num_retries=0): - """Get the next chunk of the download. - - Args: - num_retries: Integer, number of times to retry 500's with randomized - exponential backoff. If all retries fail, the raised HttpError - represents the last request. If zero (default), we attempt the - request only once. - - Returns: - (status, done): (MediaDownloadStatus, boolean) - The value of 'done' will be True when the media has been fully - downloaded. - - Raises: - googleapiclient.errors.HttpError if the response was not a 2xx. - httplib2.HttpLib2Error if a transport error has occured. - """ - headers = { - 'range': 'bytes=%d-%d' % ( - self._progress, self._progress + self._chunksize) - } - http = self._request.http - - for retry_num in xrange(num_retries + 1): - if retry_num > 0: - self._sleep(self._rand() * 2**retry_num) - logging.warning( - 'Retry #%d for media download: GET %s, following status: %d' - % (retry_num, self._uri, resp.status)) - - resp, content = http.request(self._uri, headers=headers) - if resp.status < 500: - break - - if resp.status in [200, 206]: - if 'content-location' in resp and resp['content-location'] != self._uri: - self._uri = resp['content-location'] - self._progress += len(content) - self._fd.write(content) - - if 'content-range' in resp: - content_range = resp['content-range'] - length = content_range.rsplit('/', 1)[1] - self._total_size = int(length) - - if self._progress == self._total_size: - self._done = True - return MediaDownloadProgress(self._progress, self._total_size), self._done - else: - raise HttpError(resp, content, uri=self._uri) - - -class _StreamSlice(object): - """Truncated stream. - - Takes a stream and presents a stream that is a slice of the original stream. - This is used when uploading media in chunks. In later versions of Python a - stream can be passed to httplib in place of the string of data to send. The - problem is that httplib just blindly reads to the end of the stream. This - wrapper presents a virtual stream that only reads to the end of the chunk. - """ - - def __init__(self, stream, begin, chunksize): - """Constructor. - - Args: - stream: (io.Base, file object), the stream to wrap. - begin: int, the seek position the chunk begins at. - chunksize: int, the size of the chunk. - """ - self._stream = stream - self._begin = begin - self._chunksize = chunksize - self._stream.seek(begin) - - def read(self, n=-1): - """Read n bytes. - - Args: - n, int, the number of bytes to read. - - Returns: - A string of length 'n', or less if EOF is reached. - """ - # The data left available to read sits in [cur, end) - cur = self._stream.tell() - end = self._begin + self._chunksize - if n == -1 or cur + n > end: - n = end - cur - return self._stream.read(n) - - -class HttpRequest(object): - """Encapsulates a single HTTP request.""" - - @util.positional(4) - def __init__(self, http, postproc, uri, - method='GET', - body=None, - headers=None, - methodId=None, - resumable=None): - """Constructor for an HttpRequest. - - Args: - http: httplib2.Http, the transport object to use to make a request - postproc: callable, called on the HTTP response and content to transform - it into a data object before returning, or raising an exception - on an error. - uri: string, the absolute URI to send the request to - method: string, the HTTP method to use - body: string, the request body of the HTTP request, - headers: dict, the HTTP request headers - methodId: string, a unique identifier for the API method being called. - resumable: MediaUpload, None if this is not a resumbale request. - """ - self.uri = uri - self.method = method - self.body = body - self.headers = headers or {} - self.methodId = methodId - self.http = http - self.postproc = postproc - self.resumable = resumable - self.response_callbacks = [] - self._in_error_state = False - - # Pull the multipart boundary out of the content-type header. - major, minor, params = mimeparse.parse_mime_type( - headers.get('content-type', 'application/json')) - - # The size of the non-media part of the request. - self.body_size = len(self.body or '') - - # The resumable URI to send chunks to. - self.resumable_uri = None - - # The bytes that have been uploaded. - self.resumable_progress = 0 - - # Stubs for testing. - self._rand = random.random - self._sleep = time.sleep - - @util.positional(1) - def execute(self, http=None, num_retries=0): - """Execute the request. - - Args: - http: httplib2.Http, an http object to be used in place of the - one the HttpRequest request object was constructed with. - num_retries: Integer, number of times to retry 500's with randomized - exponential backoff. If all retries fail, the raised HttpError - represents the last request. If zero (default), we attempt the - request only once. - - Returns: - A deserialized object model of the response body as determined - by the postproc. - - Raises: - googleapiclient.errors.HttpError if the response was not a 2xx. - httplib2.HttpLib2Error if a transport error has occured. - """ - if http is None: - http = self.http - - if self.resumable: - body = None - while body is None: - _, body = self.next_chunk(http=http, num_retries=num_retries) - return body - - # Non-resumable case. - - if 'content-length' not in self.headers: - self.headers['content-length'] = str(self.body_size) - # If the request URI is too long then turn it into a POST request. - if len(self.uri) > MAX_URI_LENGTH and self.method == 'GET': - self.method = 'POST' - self.headers['x-http-method-override'] = 'GET' - self.headers['content-type'] = 'application/x-www-form-urlencoded' - parsed = urlparse.urlparse(self.uri) - self.uri = urlparse.urlunparse( - (parsed.scheme, parsed.netloc, parsed.path, parsed.params, None, - None) - ) - self.body = parsed.query - self.headers['content-length'] = str(len(self.body)) - - # Handle retries for server-side errors. - for retry_num in xrange(num_retries + 1): - if retry_num > 0: - self._sleep(self._rand() * 2**retry_num) - logging.warning('Retry #%d for request: %s %s, following status: %d' - % (retry_num, self.method, self.uri, resp.status)) - - resp, content = http.request(str(self.uri), method=str(self.method), - body=self.body, headers=self.headers) - if resp.status < 500: - break - - for callback in self.response_callbacks: - callback(resp) - if resp.status >= 300: - raise HttpError(resp, content, uri=self.uri) - return self.postproc(resp, content) - - @util.positional(2) - def add_response_callback(self, cb): - """add_response_headers_callback - - Args: - cb: Callback to be called on receiving the response headers, of signature: - - def cb(resp): - # Where resp is an instance of httplib2.Response - """ - self.response_callbacks.append(cb) - - @util.positional(1) - def next_chunk(self, http=None, num_retries=0): - """Execute the next step of a resumable upload. - - Can only be used if the method being executed supports media uploads and - the MediaUpload object passed in was flagged as using resumable upload. - - Example: - - media = MediaFileUpload('cow.png', mimetype='image/png', - chunksize=1000, resumable=True) - request = farm.animals().insert( - id='cow', - name='cow.png', - media_body=media) - - response = None - while response is None: - status, response = request.next_chunk() - if status: - print "Upload %d%% complete." % int(status.progress() * 100) - - - Args: - http: httplib2.Http, an http object to be used in place of the - one the HttpRequest request object was constructed with. - num_retries: Integer, number of times to retry 500's with randomized - exponential backoff. If all retries fail, the raised HttpError - represents the last request. If zero (default), we attempt the - request only once. - - Returns: - (status, body): (ResumableMediaStatus, object) - The body will be None until the resumable media is fully uploaded. - - Raises: - googleapiclient.errors.HttpError if the response was not a 2xx. - httplib2.HttpLib2Error if a transport error has occured. - """ - if http is None: - http = self.http - - if self.resumable.size() is None: - size = '*' - else: - size = str(self.resumable.size()) - - if self.resumable_uri is None: - start_headers = copy.copy(self.headers) - start_headers['X-Upload-Content-Type'] = self.resumable.mimetype() - if size != '*': - start_headers['X-Upload-Content-Length'] = size - start_headers['content-length'] = str(self.body_size) - - for retry_num in xrange(num_retries + 1): - if retry_num > 0: - self._sleep(self._rand() * 2**retry_num) - logging.warning( - 'Retry #%d for resumable URI request: %s %s, following status: %d' - % (retry_num, self.method, self.uri, resp.status)) - - resp, content = http.request(self.uri, method=self.method, - body=self.body, - headers=start_headers) - if resp.status < 500: - break - - if resp.status == 200 and 'location' in resp: - self.resumable_uri = resp['location'] - else: - raise ResumableUploadError(resp, content) - elif self._in_error_state: - # If we are in an error state then query the server for current state of - # the upload by sending an empty PUT and reading the 'range' header in - # the response. - headers = { - 'Content-Range': 'bytes */%s' % size, - 'content-length': '0' - } - resp, content = http.request(self.resumable_uri, 'PUT', - headers=headers) - status, body = self._process_response(resp, content) - if body: - # The upload was complete. - return (status, body) - - # The httplib.request method can take streams for the body parameter, but - # only in Python 2.6 or later. If a stream is available under those - # conditions then use it as the body argument. - if self.resumable.has_stream() and sys.version_info[1] >= 6: - data = self.resumable.stream() - if self.resumable.chunksize() == -1: - data.seek(self.resumable_progress) - chunk_end = self.resumable.size() - self.resumable_progress - 1 - else: - # Doing chunking with a stream, so wrap a slice of the stream. - data = _StreamSlice(data, self.resumable_progress, - self.resumable.chunksize()) - chunk_end = min( - self.resumable_progress + self.resumable.chunksize() - 1, - self.resumable.size() - 1) - else: - data = self.resumable.getbytes( - self.resumable_progress, self.resumable.chunksize()) - - # A short read implies that we are at EOF, so finish the upload. - if len(data) < self.resumable.chunksize(): - size = str(self.resumable_progress + len(data)) - - chunk_end = self.resumable_progress + len(data) - 1 - - headers = { - 'Content-Range': 'bytes %d-%d/%s' % ( - self.resumable_progress, chunk_end, size), - # Must set the content-length header here because httplib can't - # calculate the size when working with _StreamSlice. - 'Content-Length': str(chunk_end - self.resumable_progress + 1) - } - - for retry_num in xrange(num_retries + 1): - if retry_num > 0: - self._sleep(self._rand() * 2**retry_num) - logging.warning( - 'Retry #%d for media upload: %s %s, following status: %d' - % (retry_num, self.method, self.uri, resp.status)) - - try: - resp, content = http.request(self.resumable_uri, method='PUT', - body=data, - headers=headers) - except: - self._in_error_state = True - raise - if resp.status < 500: - break - - return self._process_response(resp, content) - - def _process_response(self, resp, content): - """Process the response from a single chunk upload. - - Args: - resp: httplib2.Response, the response object. - content: string, the content of the response. - - Returns: - (status, body): (ResumableMediaStatus, object) - The body will be None until the resumable media is fully uploaded. - - Raises: - googleapiclient.errors.HttpError if the response was not a 2xx or a 308. - """ - if resp.status in [200, 201]: - self._in_error_state = False - return None, self.postproc(resp, content) - elif resp.status == 308: - self._in_error_state = False - # A "308 Resume Incomplete" indicates we are not done. - self.resumable_progress = int(resp['range'].split('-')[1]) + 1 - if 'location' in resp: - self.resumable_uri = resp['location'] - else: - self._in_error_state = True - raise HttpError(resp, content, uri=self.uri) - - return (MediaUploadProgress(self.resumable_progress, self.resumable.size()), - None) - - def to_json(self): - """Returns a JSON representation of the HttpRequest.""" - d = copy.copy(self.__dict__) - if d['resumable'] is not None: - d['resumable'] = self.resumable.to_json() - del d['http'] - del d['postproc'] - del d['_sleep'] - del d['_rand'] - - return simplejson.dumps(d) - - @staticmethod - def from_json(s, http, postproc): - """Returns an HttpRequest populated with info from a JSON object.""" - d = simplejson.loads(s) - if d['resumable'] is not None: - d['resumable'] = MediaUpload.new_from_json(d['resumable']) - return HttpRequest( - http, - postproc, - uri=d['uri'], - method=d['method'], - body=d['body'], - headers=d['headers'], - methodId=d['methodId'], - resumable=d['resumable']) - - -class BatchHttpRequest(object): - """Batches multiple HttpRequest objects into a single HTTP request. - - Example: - from googleapiclient.http import BatchHttpRequest - - def list_animals(request_id, response, exception): - \"\"\"Do something with the animals list response.\"\"\" - if exception is not None: - # Do something with the exception. - pass - else: - # Do something with the response. - pass - - def list_farmers(request_id, response, exception): - \"\"\"Do something with the farmers list response.\"\"\" - if exception is not None: - # Do something with the exception. - pass - else: - # Do something with the response. - pass - - service = build('farm', 'v2') - - batch = BatchHttpRequest() - - batch.add(service.animals().list(), list_animals) - batch.add(service.farmers().list(), list_farmers) - batch.execute(http=http) - """ - - @util.positional(1) - def __init__(self, callback=None, batch_uri=None): - """Constructor for a BatchHttpRequest. - - 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 googleapiclient.errors.HttpError exception object if an HTTP error - occurred while processing the request, or None if no error occurred. - batch_uri: string, URI to send batch requests to. - """ - if batch_uri is None: - batch_uri = 'https://www.googleapis.com/batch' - self._batch_uri = batch_uri - - # Global callback to be called for each individual response in the batch. - self._callback = callback - - # A map from id to request. - self._requests = {} - - # A map from id to callback. - self._callbacks = {} - - # List of request ids, in the order in which they were added. - self._order = [] - - # The last auto generated id. - self._last_auto_id = 0 - - # Unique ID on which to base the Content-ID headers. - self._base_id = None - - # A map from request id to (httplib2.Response, content) response pairs - self._responses = {} - - # A map of id(Credentials) that have been refreshed. - self._refreshed_credentials = {} - - def _refresh_and_apply_credentials(self, request, http): - """Refresh the credentials and apply to the request. - - Args: - request: HttpRequest, the request. - http: httplib2.Http, the global http object for the batch. - """ - # For the credentials to refresh, but only once per refresh_token - # If there is no http per the request then refresh the http passed in - # via execute() - creds = None - if request.http is not None and hasattr(request.http.request, - 'credentials'): - creds = request.http.request.credentials - elif http is not None and hasattr(http.request, 'credentials'): - creds = http.request.credentials - if creds is not None: - if id(creds) not in self._refreshed_credentials: - creds.refresh(http) - self._refreshed_credentials[id(creds)] = 1 - - # Only apply the credentials if we are using the http object passed in, - # otherwise apply() will get called during _serialize_request(). - if request.http is None or not hasattr(request.http.request, - 'credentials'): - creds.apply(request.headers) - - def _id_to_header(self, id_): - """Convert an id to a Content-ID header value. - - Args: - id_: string, identifier of individual request. - - Returns: - A Content-ID header with the id_ encoded into it. A UUID is prepended to - the value because Content-ID headers are supposed to be universally - unique. - """ - if self._base_id is None: - self._base_id = uuid.uuid4() - - return '<%s+%s>' % (self._base_id, urllib.quote(id_)) - - def _header_to_id(self, header): - """Convert a Content-ID header value to an id. - - Presumes the Content-ID header conforms to the format that _id_to_header() - returns. - - Args: - header: string, Content-ID header value. - - Returns: - The extracted id value. - - Raises: - BatchError if the header is not in the expected format. - """ - if header[0] != '<' or header[-1] != '>': - raise BatchError("Invalid value for Content-ID: %s" % header) - if '+' not in header: - raise BatchError("Invalid value for Content-ID: %s" % header) - base, id_ = header[1:-1].rsplit('+', 1) - - return urllib.unquote(id_) - - def _serialize_request(self, request): - """Convert an HttpRequest object into a string. - - Args: - request: HttpRequest, the request to serialize. - - Returns: - The request as a string in application/http format. - """ - # Construct status line - parsed = urlparse.urlparse(request.uri) - request_line = urlparse.urlunparse( - (None, None, parsed.path, parsed.params, parsed.query, None) - ) - status_line = request.method + ' ' + request_line + ' HTTP/1.1\n' - major, minor = request.headers.get('content-type', 'application/json').split('/') - msg = MIMENonMultipart(major, minor) - headers = request.headers.copy() - - if request.http is not None and hasattr(request.http.request, - 'credentials'): - request.http.request.credentials.apply(headers) - - # MIMENonMultipart adds its own Content-Type header. - if 'content-type' in headers: - del headers['content-type'] - - for key, value in headers.iteritems(): - msg[key] = value - msg['Host'] = parsed.netloc - msg.set_unixfrom(None) - - if request.body is not None: - msg.set_payload(request.body) - msg['content-length'] = str(len(request.body)) - - # Serialize the mime message. - fp = StringIO.StringIO() - # maxheaderlen=0 means don't line wrap headers. - g = Generator(fp, maxheaderlen=0) - g.flatten(msg, unixfrom=False) - body = fp.getvalue() - - # Strip off the \n\n that the MIME lib tacks onto the end of the payload. - if request.body is None: - body = body[:-2] - - return status_line.encode('utf-8') + body - - def _deserialize_response(self, payload): - """Convert string into httplib2 response and content. - - Args: - payload: string, headers and body as a string. - - Returns: - A pair (resp, content), such as would be returned from httplib2.request. - """ - # Strip off the status line - status_line, payload = payload.split('\n', 1) - protocol, status, reason = status_line.split(' ', 2) - - # Parse the rest of the response - parser = FeedParser() - parser.feed(payload) - msg = parser.close() - msg['status'] = status - - # Create httplib2.Response from the parsed headers. - resp = httplib2.Response(msg) - resp.reason = reason - resp.version = int(protocol.split('/', 1)[1].replace('.', '')) - - content = payload.split('\r\n\r\n', 1)[1] - - return resp, content - - def _new_id(self): - """Create a new id. - - Auto incrementing number that avoids conflicts with ids already used. - - Returns: - string, a new unique id. - """ - self._last_auto_id += 1 - while str(self._last_auto_id) in self._requests: - self._last_auto_id += 1 - return str(self._last_auto_id) - - @util.positional(2) - def add(self, request, callback=None, request_id=None): - """Add a new request. - - Every callback added will be paired with a unique id, the request_id. That - unique id will be passed back to the callback when the response comes back - from the server. The default behavior is to have the library generate it's - own unique id. If the caller passes in a request_id then they must ensure - uniqueness for each request_id, and if they are not an exception is - raised. Callers should either supply all request_ids or nevery supply a - request id, to avoid such an error. - - Args: - request: HttpRequest, Request to add to the batch. - callback: callable, A callback to be called for this 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 googleapiclient.errors.HttpError exception object if an HTTP error - occurred while processing the request, or None if no errors occurred. - request_id: string, A unique id for the request. The id will be passed to - the callback with the response. - - Returns: - None - - Raises: - BatchError if a media request is added to a batch. - KeyError is the request_id is not unique. - """ - if request_id is None: - request_id = self._new_id() - if request.resumable is not None: - raise BatchError("Media requests cannot be used in a batch request.") - if request_id in self._requests: - raise KeyError("A request with this ID already exists: %s" % request_id) - self._requests[request_id] = request - self._callbacks[request_id] = callback - self._order.append(request_id) - - def _execute(self, http, order, requests): - """Serialize batch request, send to server, process response. - - Args: - http: httplib2.Http, an http object to be used to make the request with. - order: list, list of request ids in the order they were added to the - batch. - request: list, list of request objects to send. - - Raises: - httplib2.HttpLib2Error if a transport error has occured. - googleapiclient.errors.BatchError if the response is the wrong format. - """ - message = MIMEMultipart('mixed') - # Message should not write out it's own headers. - setattr(message, '_write_headers', lambda self: None) - - # Add all the individual requests. - for request_id in order: - request = requests[request_id] - - msg = MIMENonMultipart('application', 'http') - msg['Content-Transfer-Encoding'] = 'binary' - msg['Content-ID'] = self._id_to_header(request_id) - - body = self._serialize_request(request) - msg.set_payload(body) - message.attach(msg) - - body = message.as_string() - - headers = {} - headers['content-type'] = ('multipart/mixed; ' - 'boundary="%s"') % message.get_boundary() - - resp, content = http.request(self._batch_uri, method='POST', body=body, - headers=headers) - - if resp.status >= 300: - raise HttpError(resp, content, uri=self._batch_uri) - - # Now break out the individual responses and store each one. - boundary, _ = content.split(None, 1) - - # Prepend with a content-type header so FeedParser can handle it. - header = 'content-type: %s\r\n\r\n' % resp['content-type'] - for_parser = header + content - - parser = FeedParser() - parser.feed(for_parser) - mime_response = parser.close() - - if not mime_response.is_multipart(): - raise BatchError("Response not in multipart/mixed format.", resp=resp, - content=content) - - for part in mime_response.get_payload(): - request_id = self._header_to_id(part['Content-ID']) - response, content = self._deserialize_response(part.get_payload()) - self._responses[request_id] = (response, content) - - @util.positional(1) - def execute(self, http=None): - """Execute all the requests as a single batched HTTP request. - - Args: - http: httplib2.Http, an http object to be used in place of the one the - HttpRequest request object was constructed with. If one isn't supplied - then use a http object from the requests in this batch. - - Returns: - None - - Raises: - httplib2.HttpLib2Error if a transport error has occured. - googleapiclient.errors.BatchError if the response is the wrong format. - """ - - # If http is not supplied use the first valid one given in the requests. - if http is None: - for request_id in self._order: - request = self._requests[request_id] - if request is not None: - http = request.http - break - - if http is None: - raise ValueError("Missing a valid http object.") - - self._execute(http, self._order, self._requests) - - # Loop over all the requests and check for 401s. For each 401 request the - # credentials should be refreshed and then sent again in a separate batch. - redo_requests = {} - redo_order = [] - - for request_id in self._order: - resp, content = self._responses[request_id] - if resp['status'] == '401': - redo_order.append(request_id) - request = self._requests[request_id] - self._refresh_and_apply_credentials(request, http) - redo_requests[request_id] = request - - if redo_requests: - self._execute(http, redo_order, redo_requests) - - # Now process all callbacks that are erroring, and raise an exception for - # ones that return a non-2xx response? Or add extra parameter to callback - # that contains an HttpError? - - for request_id in self._order: - resp, content = self._responses[request_id] - - request = self._requests[request_id] - callback = self._callbacks[request_id] - - response = None - exception = None - try: - if resp.status >= 300: - raise HttpError(resp, content, uri=request.uri) - response = request.postproc(resp, content) - except HttpError, e: - exception = e - - if callback is not None: - callback(request_id, response, exception) - if self._callback is not None: - self._callback(request_id, response, exception) - - -class HttpRequestMock(object): - """Mock of HttpRequest. - - Do not construct directly, instead use RequestMockBuilder. - """ - - def __init__(self, resp, content, postproc): - """Constructor for HttpRequestMock - - Args: - resp: httplib2.Response, the response to emulate coming from the request - content: string, the response body - postproc: callable, the post processing function usually supplied by - the model class. See model.JsonModel.response() as an example. - """ - self.resp = resp - self.content = content - self.postproc = postproc - if resp is None: - self.resp = httplib2.Response({'status': 200, 'reason': 'OK'}) - if 'reason' in self.resp: - self.resp.reason = self.resp['reason'] - - def execute(self, http=None): - """Execute the request. - - Same behavior as HttpRequest.execute(), but the response is - mocked and not really from an HTTP request/response. - """ - return self.postproc(self.resp, self.content) - - -class RequestMockBuilder(object): - """A simple mock of HttpRequest - - Pass in a dictionary to the constructor that maps request methodIds to - tuples of (httplib2.Response, content, opt_expected_body) that should be - returned when that method is called. None may also be passed in for the - httplib2.Response, in which case a 200 OK response will be generated. - If an opt_expected_body (str or dict) is provided, it will be compared to - the body and UnexpectedBodyError will be raised on inequality. - - Example: - response = '{"data": {"id": "tag:google.c...' - requestBuilder = RequestMockBuilder( - { - 'plus.activities.get': (None, response), - } - ) - googleapiclient.discovery.build("plus", "v1", requestBuilder=requestBuilder) - - Methods that you do not supply a response for will return a - 200 OK with an empty string as the response content or raise an excpetion - if check_unexpected is set to True. The methodId is taken from the rpcName - in the discovery document. - - For more details see the project wiki. - """ - - def __init__(self, responses, check_unexpected=False): - """Constructor for RequestMockBuilder - - The constructed object should be a callable object - that can replace the class HttpResponse. - - responses - A dictionary that maps methodIds into tuples - of (httplib2.Response, content). The methodId - comes from the 'rpcName' field in the discovery - document. - check_unexpected - A boolean setting whether or not UnexpectedMethodError - should be raised on unsupplied method. - """ - self.responses = responses - self.check_unexpected = check_unexpected - - def __call__(self, http, postproc, uri, method='GET', body=None, - headers=None, methodId=None, resumable=None): - """Implements the callable interface that discovery.build() expects - of requestBuilder, which is to build an object compatible with - HttpRequest.execute(). See that method for the description of the - parameters and the expected response. - """ - if methodId in self.responses: - response = self.responses[methodId] - resp, content = response[:2] - if len(response) > 2: - # Test the body against the supplied expected_body. - expected_body = response[2] - if bool(expected_body) != bool(body): - # Not expecting a body and provided one - # or expecting a body and not provided one. - raise UnexpectedBodyError(expected_body, body) - if isinstance(expected_body, str): - expected_body = simplejson.loads(expected_body) - body = simplejson.loads(body) - if body != expected_body: - raise UnexpectedBodyError(expected_body, body) - return HttpRequestMock(resp, content, postproc) - elif self.check_unexpected: - raise UnexpectedMethodError(methodId=methodId) - else: - model = JsonModel(False) - return HttpRequestMock(None, '{}', model.response) - - -class HttpMock(object): - """Mock of httplib2.Http""" - - def __init__(self, filename=None, headers=None): - """ - Args: - filename: string, absolute filename to read response from - headers: dict, header to return with response - """ - if headers is None: - headers = {'status': '200 OK'} - if filename: - f = file(filename, 'r') - self.data = f.read() - f.close() - else: - self.data = None - self.response_headers = headers - self.headers = None - self.uri = None - self.method = None - self.body = None - self.headers = None - - - def request(self, uri, - method='GET', - body=None, - headers=None, - redirections=1, - connection_type=None): - self.uri = uri - self.method = method - self.body = body - self.headers = headers - return httplib2.Response(self.response_headers), self.data - - -class HttpMockSequence(object): - """Mock of httplib2.Http - - Mocks a sequence of calls to request returning different responses for each - call. Create an instance initialized with the desired response headers - and content and then use as if an httplib2.Http instance. - - http = HttpMockSequence([ - ({'status': '401'}, ''), - ({'status': '200'}, '{"access_token":"1/3w","expires_in":3600}'), - ({'status': '200'}, 'echo_request_headers'), - ]) - resp, content = http.request("http://examples.com") - - There are special values you can pass in for content to trigger - behavours that are helpful in testing. - - 'echo_request_headers' means return the request headers in the response body - 'echo_request_headers_as_json' means return the request headers in - the response body - 'echo_request_body' means return the request body in the response body - 'echo_request_uri' means return the request uri in the response body - """ - - def __init__(self, iterable): - """ - Args: - iterable: iterable, a sequence of pairs of (headers, body) - """ - self._iterable = iterable - self.follow_redirects = True - - def request(self, uri, - method='GET', - body=None, - headers=None, - redirections=1, - connection_type=None): - resp, content = self._iterable.pop(0) - if content == 'echo_request_headers': - content = headers - elif content == 'echo_request_headers_as_json': - content = simplejson.dumps(headers) - elif content == 'echo_request_body': - if hasattr(body, 'read'): - content = body.read() - else: - content = body - elif content == 'echo_request_uri': - content = uri - return httplib2.Response(resp), content - - -def set_user_agent(http, user_agent): - """Set the user-agent on every request. - - Args: - http - An instance of httplib2.Http - or something that acts like it. - user_agent: string, the value for the user-agent header. - - Returns: - A modified instance of http that was passed in. - - Example: - - h = httplib2.Http() - h = set_user_agent(h, "my-app-name/6.0") - - Most of the time the user-agent will be set doing auth, this is for the rare - cases where you are accessing an unauthenticated endpoint. - """ - request_orig = http.request - - # The closure that will replace 'httplib2.Http.request'. - def new_request(uri, method='GET', body=None, headers=None, - redirections=httplib2.DEFAULT_MAX_REDIRECTS, - connection_type=None): - """Modify the request headers to add the user-agent.""" - if headers is None: - headers = {} - if 'user-agent' in headers: - headers['user-agent'] = user_agent + ' ' + headers['user-agent'] - else: - headers['user-agent'] = user_agent - resp, content = request_orig(uri, method, body, headers, - redirections, connection_type) - return resp, content - - http.request = new_request - return http - - -def tunnel_patch(http): - """Tunnel PATCH requests over POST. - Args: - http - An instance of httplib2.Http - or something that acts like it. - - Returns: - A modified instance of http that was passed in. - - Example: - - h = httplib2.Http() - h = tunnel_patch(h, "my-app-name/6.0") - - Useful if you are running on a platform that doesn't support PATCH. - Apply this last if you are using OAuth 1.0, as changing the method - will result in a different signature. - """ - request_orig = http.request - - # The closure that will replace 'httplib2.Http.request'. - def new_request(uri, method='GET', body=None, headers=None, - redirections=httplib2.DEFAULT_MAX_REDIRECTS, - connection_type=None): - """Modify the request headers to add the user-agent.""" - if headers is None: - headers = {} - if method == 'PATCH': - if 'oauth_token' in headers.get('authorization', ''): - logging.warning( - 'OAuth 1.0 request made with Credentials after tunnel_patch.') - headers['x-http-method-override'] = "PATCH" - method = 'POST' - resp, content = request_orig(uri, method, body, headers, - redirections, connection_type) - return resp, content - - http.request = new_request - return http diff --git a/googleapiclient/mimeparse.py b/googleapiclient/mimeparse.py deleted file mode 100644 index cbb9d07..0000000 --- a/googleapiclient/mimeparse.py +++ /dev/null @@ -1,172 +0,0 @@ -# Copyright (C) 2007 Joe Gregorio -# -# Licensed under the MIT License - -"""MIME-Type Parser - -This module provides basic functions for handling mime-types. It can handle -matching mime-types against a list of media-ranges. See section 14.1 of the -HTTP specification [RFC 2616] for a complete explanation. - - http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1 - -Contents: - - parse_mime_type(): Parses a mime-type into its component parts. - - parse_media_range(): Media-ranges are mime-types with wild-cards and a 'q' - quality parameter. - - quality(): Determines the quality ('q') of a mime-type when - compared against a list of media-ranges. - - quality_parsed(): Just like quality() except the second parameter must be - pre-parsed. - - best_match(): Choose the mime-type with the highest quality ('q') - from a list of candidates. -""" - -__version__ = '0.1.3' -__author__ = 'Joe Gregorio' -__email__ = 'joe@bitworking.org' -__license__ = 'MIT License' -__credits__ = '' - - -def parse_mime_type(mime_type): - """Parses a mime-type into its component parts. - - Carves up a mime-type and returns a tuple of the (type, subtype, params) - where 'params' is a dictionary of all the parameters for the media range. - For example, the media range 'application/xhtml;q=0.5' would get parsed - into: - - ('application', 'xhtml', {'q', '0.5'}) - """ - parts = mime_type.split(';') - params = dict([tuple([s.strip() for s in param.split('=', 1)])\ - for param in parts[1:] - ]) - full_type = parts[0].strip() - # Java URLConnection class sends an Accept header that includes a - # single '*'. Turn it into a legal wildcard. - if full_type == '*': - full_type = '*/*' - (type, subtype) = full_type.split('/') - - return (type.strip(), subtype.strip(), params) - - -def parse_media_range(range): - """Parse a media-range into its component parts. - - Carves up a media range and returns a tuple of the (type, subtype, - params) where 'params' is a dictionary of all the parameters for the media - range. For example, the media range 'application/*;q=0.5' would get parsed - into: - - ('application', '*', {'q', '0.5'}) - - In addition this function also guarantees that there is a value for 'q' - in the params dictionary, filling it in with a proper default if - necessary. - """ - (type, subtype, params) = parse_mime_type(range) - if not params.has_key('q') or not params['q'] or \ - not float(params['q']) or float(params['q']) > 1\ - or float(params['q']) < 0: - params['q'] = '1' - - return (type, subtype, params) - - -def fitness_and_quality_parsed(mime_type, parsed_ranges): - """Find the best match for a mime-type amongst parsed media-ranges. - - Find the best match for a given mime-type against a list of media_ranges - that have already been parsed by parse_media_range(). Returns a tuple of - the fitness value and the value of the 'q' quality parameter of the best - match, or (-1, 0) if no match was found. Just as for quality_parsed(), - 'parsed_ranges' must be a list of parsed media ranges. - """ - best_fitness = -1 - best_fit_q = 0 - (target_type, target_subtype, target_params) =\ - parse_media_range(mime_type) - for (type, subtype, params) in parsed_ranges: - type_match = (type == target_type or\ - type == '*' or\ - target_type == '*') - subtype_match = (subtype == target_subtype or\ - subtype == '*' or\ - target_subtype == '*') - if type_match and subtype_match: - param_matches = reduce(lambda x, y: x + y, [1 for (key, value) in \ - target_params.iteritems() if key != 'q' and \ - params.has_key(key) and value == params[key]], 0) - fitness = (type == target_type) and 100 or 0 - fitness += (subtype == target_subtype) and 10 or 0 - fitness += param_matches - if fitness > best_fitness: - best_fitness = fitness - best_fit_q = params['q'] - - return best_fitness, float(best_fit_q) - - -def quality_parsed(mime_type, parsed_ranges): - """Find the best match for a mime-type amongst parsed media-ranges. - - Find the best match for a given mime-type against a list of media_ranges - that have already been parsed by parse_media_range(). Returns the 'q' - quality parameter of the best match, 0 if no match was found. This function - bahaves the same as quality() except that 'parsed_ranges' must be a list of - parsed media ranges. - """ - - return fitness_and_quality_parsed(mime_type, parsed_ranges)[1] - - -def quality(mime_type, ranges): - """Return the quality ('q') of a mime-type against a list of media-ranges. - - Returns the quality 'q' of a mime-type when compared against the - media-ranges in ranges. For example: - - >>> quality('text/html','text/*;q=0.3, text/html;q=0.7, - text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5') - 0.7 - - """ - parsed_ranges = [parse_media_range(r) for r in ranges.split(',')] - - return quality_parsed(mime_type, parsed_ranges) - - -def best_match(supported, header): - """Return mime-type with the highest quality ('q') from list of candidates. - - Takes a list of supported mime-types and finds the best match for all the - media-ranges listed in header. The value of header must be a string that - conforms to the format of the HTTP Accept: header. The value of 'supported' - is a list of mime-types. The list of supported mime-types should be sorted - in order of increasing desirability, in case of a situation where there is - a tie. - - >>> best_match(['application/xbel+xml', 'text/xml'], - 'text/*;q=0.5,*/*; q=0.1') - 'text/xml' - """ - split_header = _filter_blank(header.split(',')) - parsed_header = [parse_media_range(r) for r in split_header] - weighted_matches = [] - pos = 0 - for mime_type in supported: - weighted_matches.append((fitness_and_quality_parsed(mime_type, - parsed_header), pos, mime_type)) - pos += 1 - weighted_matches.sort() - - return weighted_matches[-1][0][1] and weighted_matches[-1][2] or '' - - -def _filter_blank(i): - for s in i: - if s.strip(): - yield s diff --git a/googleapiclient/model.py b/googleapiclient/model.py deleted file mode 100644 index 566a233..0000000 --- a/googleapiclient/model.py +++ /dev/null @@ -1,383 +0,0 @@ -#!/usr/bin/python2.4 -# -# Copyright (C) 2010 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Model objects for requests and responses. - -Each API may support one or more serializations, such -as JSON, Atom, etc. The model classes are responsible -for converting between the wire format and the Python -object representation. -""" - -__author__ = 'jcgregorio@google.com (Joe Gregorio)' - -import logging -import urllib - -from googleapiclient import __version__ -from errors import HttpError -from oauth2client.anyjson import simplejson - - -dump_request_response = False - - -def _abstract(): - raise NotImplementedError('You need to override this function') - - -class Model(object): - """Model base class. - - All Model classes should implement this interface. - The Model serializes and de-serializes between a wire - format such as JSON and a Python object representation. - """ - - def request(self, headers, path_params, query_params, body_value): - """Updates outgoing requests with a serialized body. - - Args: - headers: dict, request headers - path_params: dict, parameters that appear in the request path - query_params: dict, parameters that appear in the query - body_value: object, the request body as a Python object, which must be - serializable. - Returns: - A tuple of (headers, path_params, query, body) - - headers: dict, request headers - path_params: dict, parameters that appear in the request path - query: string, query part of the request URI - body: string, the body serialized in the desired wire format. - """ - _abstract() - - def response(self, resp, content): - """Convert the response wire format into a Python object. - - Args: - resp: httplib2.Response, the HTTP response headers and status - content: string, the body of the HTTP response - - Returns: - The body de-serialized as a Python object. - - Raises: - googleapiclient.errors.HttpError if a non 2xx response is received. - """ - _abstract() - - -class BaseModel(Model): - """Base model class. - - Subclasses should provide implementations for the "serialize" and - "deserialize" methods, as well as values for the following class attributes. - - Attributes: - accept: The value to use for the HTTP Accept header. - content_type: The value to use for the HTTP Content-type header. - no_content_response: The value to return when deserializing a 204 "No - Content" response. - alt_param: The value to supply as the "alt" query parameter for requests. - """ - - accept = None - content_type = None - no_content_response = None - alt_param = None - - def _log_request(self, headers, path_params, query, body): - """Logs debugging information about the request if requested.""" - if dump_request_response: - logging.info('--request-start--') - logging.info('-headers-start-') - for h, v in headers.iteritems(): - logging.info('%s: %s', h, v) - logging.info('-headers-end-') - logging.info('-path-parameters-start-') - for h, v in path_params.iteritems(): - logging.info('%s: %s', h, v) - logging.info('-path-parameters-end-') - logging.info('body: %s', body) - logging.info('query: %s', query) - logging.info('--request-end--') - - def request(self, headers, path_params, query_params, body_value): - """Updates outgoing requests with a serialized body. - - Args: - headers: dict, request headers - path_params: dict, parameters that appear in the request path - query_params: dict, parameters that appear in the query - body_value: object, the request body as a Python object, which must be - serializable by simplejson. - Returns: - A tuple of (headers, path_params, query, body) - - headers: dict, request headers - path_params: dict, parameters that appear in the request path - query: string, query part of the request URI - body: string, the body serialized as JSON - """ - query = self._build_query(query_params) - headers['accept'] = self.accept - headers['accept-encoding'] = 'gzip, deflate' - if 'user-agent' in headers: - headers['user-agent'] += ' ' - else: - headers['user-agent'] = '' - headers['user-agent'] += 'google-api-python-client/%s (gzip)' % __version__ - - if body_value is not None: - headers['content-type'] = self.content_type - body_value = self.serialize(body_value) - self._log_request(headers, path_params, query, body_value) - return (headers, path_params, query, body_value) - - 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 params.iteritems(): - if type(value) == type([]): - for x in value: - x = x.encode('utf-8') - astuples.append((key, x)) - else: - if getattr(value, 'encode', False) and callable(value.encode): - value = value.encode('utf-8') - astuples.append((key, value)) - return '?' + urllib.urlencode(astuples) - - def _log_response(self, resp, content): - """Logs debugging information about the response if requested.""" - if dump_request_response: - logging.info('--response-start--') - for h, v in resp.iteritems(): - logging.info('%s: %s', h, v) - if content: - logging.info(content) - logging.info('--response-end--') - - def response(self, resp, content): - """Convert the response wire format into a Python object. - - Args: - resp: httplib2.Response, the HTTP response headers and status - content: string, the body of the HTTP response - - Returns: - The body de-serialized as a Python object. - - Raises: - googleapiclient.errors.HttpError if a non 2xx response is received. - """ - self._log_response(resp, content) - # Error handling is TBD, for example, do we retry - # for some operation/error combinations? - if resp.status < 300: - if resp.status == 204: - # A 204: No Content response should be treated differently - # to all the other success states - return self.no_content_response - return self.deserialize(content) - else: - logging.debug('Content from bad request was: %s' % content) - raise HttpError(resp, content) - - def serialize(self, body_value): - """Perform the actual Python object serialization. - - Args: - body_value: object, the request body as a Python object. - - Returns: - string, the body in serialized form. - """ - _abstract() - - def deserialize(self, content): - """Perform the actual deserialization from response string to Python - object. - - Args: - content: string, the body of the HTTP response - - Returns: - The body de-serialized as a Python object. - """ - _abstract() - - -class JsonModel(BaseModel): - """Model class for JSON. - - Serializes and de-serializes between JSON and the Python - object representation of HTTP request and response bodies. - """ - accept = 'application/json' - content_type = 'application/json' - alt_param = 'json' - - def __init__(self, data_wrapper=False): - """Construct a JsonModel. - - Args: - data_wrapper: boolean, wrap requests and responses in a data wrapper - """ - self._data_wrapper = data_wrapper - - def serialize(self, body_value): - if (isinstance(body_value, dict) and 'data' not in body_value and - self._data_wrapper): - body_value = {'data': body_value} - return simplejson.dumps(body_value) - - def deserialize(self, content): - content = content.decode('utf-8') - body = simplejson.loads(content) - if self._data_wrapper and isinstance(body, dict) and 'data' in body: - body = body['data'] - return body - - @property - def no_content_response(self): - return {} - - -class RawModel(JsonModel): - """Model class for requests that don't return JSON. - - Serializes and de-serializes between JSON and the Python - object representation of HTTP request, and returns the raw bytes - of the response body. - """ - accept = '*/*' - content_type = 'application/json' - alt_param = None - - def deserialize(self, content): - return content - - @property - def no_content_response(self): - return '' - - -class MediaModel(JsonModel): - """Model class for requests that return Media. - - Serializes and de-serializes between JSON and the Python - object representation of HTTP request, and returns the raw bytes - of the response body. - """ - accept = '*/*' - content_type = 'application/json' - alt_param = 'media' - - def deserialize(self, content): - return content - - @property - def no_content_response(self): - return '' - - -class ProtocolBufferModel(BaseModel): - """Model class for protocol buffers. - - Serializes and de-serializes the binary protocol buffer sent in the HTTP - request and response bodies. - """ - accept = 'application/x-protobuf' - content_type = 'application/x-protobuf' - alt_param = 'proto' - - def __init__(self, protocol_buffer): - """Constructs a ProtocolBufferModel. - - The serialzed protocol buffer returned in an HTTP response will be - de-serialized using the given protocol buffer class. - - Args: - protocol_buffer: The protocol buffer class used to de-serialize a - response from the API. - """ - self._protocol_buffer = protocol_buffer - - def serialize(self, body_value): - return body_value.SerializeToString() - - def deserialize(self, content): - return self._protocol_buffer.FromString(content) - - @property - def no_content_response(self): - return self._protocol_buffer() - - -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 original.iteritems(): - 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 diff --git a/googleapiclient/sample_tools.py b/googleapiclient/sample_tools.py deleted file mode 100644 index 09f9057..0000000 --- a/googleapiclient/sample_tools.py +++ /dev/null @@ -1,93 +0,0 @@ -# Copyright (C) 2013 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Utilities for making samples. - -Consolidates a lot of code commonly repeated in sample applications. -""" - -__author__ = 'jcgregorio@google.com (Joe Gregorio)' -__all__ = ['init'] - - -import argparse -import httplib2 -import os - -from googleapiclient import discovery -from oauth2client import client -from oauth2client import file -from oauth2client import tools - - -def init(argv, name, version, doc, filename, scope=None, parents=[]): - """A common initialization routine for samples. - - Many of the sample applications do the same initialization, which has now - been consolidated into this function. This function uses common idioms found - in almost all the samples, i.e. for an API with name 'apiname', the - credentials are stored in a file named apiname.dat, and the - client_secrets.json file is stored in the same directory as the application - main file. - - Args: - argv: list of string, the command-line parameters of the application. - name: string, name of the API. - version: string, version of the API. - doc: string, description of the application. Usually set to __doc__. - file: string, filename of the application. Usually set to __file__. - parents: list of argparse.ArgumentParser, additional command-line flags. - scope: string, The OAuth scope used. - - Returns: - A tuple of (service, flags), where service is the service object and flags - is the parsed command-line flags. - """ - if scope is None: - scope = 'https://www.googleapis.com/auth/' + name - - # Parser command-line arguments. - parent_parsers = [tools.argparser] - parent_parsers.extend(parents) - parser = argparse.ArgumentParser( - description=doc, - formatter_class=argparse.RawDescriptionHelpFormatter, - parents=parent_parsers) - flags = parser.parse_args(argv[1:]) - - # Name of a file containing the OAuth 2.0 information for this - # application, including client_id and client_secret, which are found - # on the API Access tab on the Google APIs - # Console . - client_secrets = os.path.join(os.path.dirname(filename), - 'client_secrets.json') - - # Set up a Flow object to be used if we need to authenticate. - flow = client.flow_from_clientsecrets(client_secrets, - scope=scope, - message=tools.message_if_missing(client_secrets)) - - # Prepare credentials, and authorize HTTP object with them. - # If the credentials don't exist or are invalid run through the native client - # flow. The Storage object will ensure that if successful the good - # credentials will get written back to a file. - storage = file.Storage(name + '.dat') - credentials = storage.get() - if credentials is None or credentials.invalid: - credentials = tools.run_flow(flow, storage, flags) - http = credentials.authorize(http = httplib2.Http()) - - # Construct a service object via the discovery service. - service = discovery.build(name, version, http=http) - return (service, flags) diff --git a/googleapiclient/schema.py b/googleapiclient/schema.py deleted file mode 100644 index d076a86..0000000 --- a/googleapiclient/schema.py +++ /dev/null @@ -1,312 +0,0 @@ -# Copyright (C) 2010 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Schema processing for discovery based APIs - -Schemas holds an APIs discovery schemas. It can return those schema as -deserialized JSON objects, or pretty print them as prototype objects that -conform to the schema. - -For example, given the schema: - - schema = \"\"\"{ - "Foo": { - "type": "object", - "properties": { - "etag": { - "type": "string", - "description": "ETag of the collection." - }, - "kind": { - "type": "string", - "description": "Type of the collection ('calendar#acl').", - "default": "calendar#acl" - }, - "nextPageToken": { - "type": "string", - "description": "Token used to access the next - page of this result. Omitted if no further results are available." - } - } - } - }\"\"\" - - s = Schemas(schema) - print s.prettyPrintByName('Foo') - - Produces the following output: - - { - "nextPageToken": "A String", # Token used to access the - # next page of this result. Omitted if no further results are available. - "kind": "A String", # Type of the collection ('calendar#acl'). - "etag": "A String", # ETag of the collection. - }, - -The constructor takes a discovery document in which to look up named schema. -""" - -# TODO(jcgregorio) support format, enum, minimum, maximum - -__author__ = 'jcgregorio@google.com (Joe Gregorio)' - -import copy - -from oauth2client import util -from oauth2client.anyjson import simplejson - - -class Schemas(object): - """Schemas for an API.""" - - def __init__(self, discovery): - """Constructor. - - Args: - discovery: object, Deserialized discovery document from which we pull - out the named schema. - """ - self.schemas = discovery.get('schemas', {}) - - # Cache of pretty printed schemas. - self.pretty = {} - - @util.positional(2) - def _prettyPrintByName(self, name, seen=None, dent=0): - """Get pretty printed object prototype from the schema name. - - Args: - name: string, Name of schema in the discovery document. - seen: list of string, Names of schema already seen. Used to handle - recursive definitions. - - Returns: - string, A string that contains a prototype object with - comments that conforms to the given schema. - """ - if seen is None: - seen = [] - - if name in seen: - # Do not fall into an infinite loop over recursive definitions. - return '# Object with schema name: %s' % name - seen.append(name) - - if name not in self.pretty: - self.pretty[name] = _SchemaToStruct(self.schemas[name], - seen, dent=dent).to_str(self._prettyPrintByName) - - seen.pop() - - return self.pretty[name] - - def prettyPrintByName(self, name): - """Get pretty printed object prototype from the schema name. - - Args: - name: string, Name of schema in the discovery document. - - Returns: - string, A string that contains a prototype object with - comments that conforms to the given schema. - """ - # Return with trailing comma and newline removed. - return self._prettyPrintByName(name, seen=[], dent=1)[:-2] - - @util.positional(2) - def _prettyPrintSchema(self, schema, seen=None, dent=0): - """Get pretty printed object prototype of schema. - - Args: - schema: object, Parsed JSON schema. - seen: list of string, Names of schema already seen. Used to handle - recursive definitions. - - Returns: - string, A string that contains a prototype object with - comments that conforms to the given schema. - """ - if seen is None: - seen = [] - - return _SchemaToStruct(schema, seen, dent=dent).to_str(self._prettyPrintByName) - - def prettyPrintSchema(self, schema): - """Get pretty printed object prototype of schema. - - Args: - schema: object, Parsed JSON schema. - - Returns: - string, A string that contains a prototype object with - comments that conforms to the given schema. - """ - # Return with trailing comma and newline removed. - return self._prettyPrintSchema(schema, dent=1)[:-2] - - def get(self, name): - """Get deserialized JSON schema from the schema name. - - Args: - name: string, Schema name. - """ - return self.schemas[name] - - -class _SchemaToStruct(object): - """Convert schema to a prototype object.""" - - @util.positional(3) - def __init__(self, schema, seen, dent=0): - """Constructor. - - Args: - schema: object, Parsed JSON schema. - seen: list, List of names of schema already seen while parsing. Used to - handle recursive definitions. - dent: int, Initial indentation depth. - """ - # The result of this parsing kept as list of strings. - self.value = [] - - # The final value of the parsing. - self.string = None - - # The parsed JSON schema. - self.schema = schema - - # Indentation level. - self.dent = dent - - # Method that when called returns a prototype object for the schema with - # the given name. - self.from_cache = None - - # List of names of schema already seen while parsing. - self.seen = seen - - def emit(self, text): - """Add text as a line to the output. - - Args: - text: string, Text to output. - """ - self.value.extend([" " * self.dent, text, '\n']) - - def emitBegin(self, text): - """Add text to the output, but with no line terminator. - - Args: - text: string, Text to output. - """ - self.value.extend([" " * self.dent, text]) - - def emitEnd(self, text, comment): - """Add text and comment to the output with line terminator. - - Args: - text: string, Text to output. - comment: string, Python comment. - """ - if comment: - divider = '\n' + ' ' * (self.dent + 2) + '# ' - lines = comment.splitlines() - lines = [x.rstrip() for x in lines] - comment = divider.join(lines) - self.value.extend([text, ' # ', comment, '\n']) - else: - self.value.extend([text, '\n']) - - def indent(self): - """Increase indentation level.""" - self.dent += 1 - - def undent(self): - """Decrease indentation level.""" - self.dent -= 1 - - def _to_str_impl(self, schema): - """Prototype object based on the schema, in Python code with comments. - - Args: - schema: object, Parsed JSON schema file. - - Returns: - Prototype object based on the schema, in Python code with comments. - """ - stype = schema.get('type') - if stype == 'object': - self.emitEnd('{', schema.get('description', '')) - self.indent() - if 'properties' in schema: - for pname, pschema in schema.get('properties', {}).iteritems(): - self.emitBegin('"%s": ' % pname) - self._to_str_impl(pschema) - elif 'additionalProperties' in schema: - self.emitBegin('"a_key": ') - self._to_str_impl(schema['additionalProperties']) - self.undent() - self.emit('},') - elif '$ref' in schema: - schemaName = schema['$ref'] - description = schema.get('description', '') - s = self.from_cache(schemaName, seen=self.seen) - parts = s.splitlines() - self.emitEnd(parts[0], description) - for line in parts[1:]: - self.emit(line.rstrip()) - elif stype == 'boolean': - value = schema.get('default', 'True or False') - self.emitEnd('%s,' % str(value), schema.get('description', '')) - elif stype == 'string': - value = schema.get('default', 'A String') - self.emitEnd('"%s",' % str(value), schema.get('description', '')) - elif stype == 'integer': - value = schema.get('default', '42') - self.emitEnd('%s,' % str(value), schema.get('description', '')) - elif stype == 'number': - value = schema.get('default', '3.14') - self.emitEnd('%s,' % str(value), schema.get('description', '')) - elif stype == 'null': - self.emitEnd('None,', schema.get('description', '')) - elif stype == 'any': - self.emitEnd('"",', schema.get('description', '')) - elif stype == 'array': - self.emitEnd('[', schema.get('description')) - self.indent() - self.emitBegin('') - self._to_str_impl(schema['items']) - self.undent() - self.emit('],') - else: - self.emit('Unknown type! %s' % stype) - self.emitEnd('', '') - - self.string = ''.join(self.value) - return self.string - - def to_str(self, from_cache): - """Prototype object based on the schema, in Python code with comments. - - Args: - from_cache: callable(name, seen), Callable that retrieves an object - prototype for a schema with the given name. Seen is a list of schema - names already seen as we recursively descend the schema definition. - - Returns: - Prototype object based on the schema, in Python code with comments. - The lines of the code will all be properly indented. - """ - self.from_cache = from_cache - return self._to_str_impl(self.schema) diff --git a/tests/data/create-private-keys.sh b/tests/data/create-private-keys.sh deleted file mode 100755 index aaec580..0000000 --- a/tests/data/create-private-keys.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -openssl req -new -newkey rsa:2048 -days 3650 -nodes -x509 \ - -keyout privatekey.pem -out publickey.pem \ - -subj "/CN=unit-tests" - -openssl pkcs12 -export -out privatekey.p12 \ - -inkey privatekey.pem -in publickey.pem \ - -name "key" -passout pass:notasecret - -openssl pkcs12 -in privatekey.p12 \ - -nodes -nocerts -passout pass:notasecret \ - -passin pass:notasecret > pem_from_pkcs12.pem \ No newline at end of file diff --git a/tests/data/latitude.json b/tests/data/latitude.json deleted file mode 100644 index 7717f90..0000000 --- a/tests/data/latitude.json +++ /dev/null @@ -1,241 +0,0 @@ -{ - "kind": "discovery#restDescription", - "id": "latitude:v1", - "name": "latitude", - "version": "v1", - "description": "Google Latitude API", - "icons": { - "x16": "http://www.google.com/images/icons/product/search-16.gif", - "x32": "http://www.google.com/images/icons/product/search-32.gif" - }, - "labels": [ - "labs" - ], - "protocol": "rest", - "basePath": "/latitude/v1/", - "rootUrl": "https://www.googleapis.com/", - "servicePath": "latitude/v1/", - "auth": { - "oauth2": { - "scopes": { - "https://www.googleapis.com/auth/latitude": { - "description": "Manage your current location and location history" - } - } - } - }, - "features": [ - "dataWrapper" - ], - "schemas": { - "LatitudeCurrentlocationResourceJson": { - "$ref": "Location" - }, - "Location": { - "id": "Location", - "type": "object", - "properties": { - "accuracy": { - "type": "any" - }, - "activityId": { - "type": "any" - }, - "altitude": { - "type": "any" - }, - "altitudeAccuracy": { - "type": "any" - }, - "heading": { - "type": "any" - }, - "kind": { - "type": "string", - "default": "latitude#location" - }, - "latitude": { - "type": "any" - }, - "longitude": { - "type": "any" - }, - "placeid": { - "type": "any" - }, - "speed": { - "type": "any" - }, - "timestampMs": { - "type": "any" - } - } - }, - "LocationFeed": { - "id": "LocationFeed", - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "Location" - } - }, - "kind": { - "type": "string", - "default": "latitude#locationFeed" - } - } - } - }, - "resources": { - "currentLocation": { - "methods": { - "delete": { - "id": "latitude.currentLocation.delete", - "path": "currentLocation", - "httpMethod": "DELETE", - "description": "Deletes the authenticated user's current location.", - "scopes": [ - "https://www.googleapis.com/auth/latitude" - ] - }, - "get": { - "id": "latitude.currentLocation.get", - "path": "currentLocation", - "httpMethod": "GET", - "description": "Returns the authenticated user's current location.", - "parameters": { - "granularity": { - "type": "string", - "description": "Granularity of the requested location.", - "location": "query" - } - }, - "response": { - "$ref": "LatitudeCurrentlocationResourceJson" - }, - "scopes": [ - "https://www.googleapis.com/auth/latitude" - ] - }, - "insert": { - "id": "latitude.currentLocation.insert", - "path": "currentLocation", - "httpMethod": "POST", - "description": "Updates or creates the user's current location.", - "request": { - "$ref": "LatitudeCurrentlocationResourceJson" - }, - "response": { - "$ref": "LatitudeCurrentlocationResourceJson" - }, - "scopes": [ - "https://www.googleapis.com/auth/latitude" - ] - } - } - }, - "location": { - "methods": { - "delete": { - "id": "latitude.location.delete", - "path": "location/{locationId}", - "httpMethod": "DELETE", - "description": "Deletes a location from the user's location history.", - "parameters": { - "locationId": { - "type": "string", - "description": "Timestamp of the location to delete (ms since epoch).", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "locationId" - ], - "scopes": [ - "https://www.googleapis.com/auth/latitude" - ] - }, - "get": { - "id": "latitude.location.get", - "path": "location/{locationId}", - "httpMethod": "GET", - "description": "Reads a location from the user's location history.", - "parameters": { - "granularity": { - "type": "string", - "description": "Granularity of the location to return.", - "location": "query" - }, - "locationId": { - "type": "string", - "description": "Timestamp of the location to read (ms since epoch).", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "locationId" - ], - "response": { - "$ref": "Location" - }, - "scopes": [ - "https://www.googleapis.com/auth/latitude" - ] - }, - "insert": { - "id": "latitude.location.insert", - "path": "location", - "httpMethod": "POST", - "description": "Inserts or updates a location in the user's location history.", - "request": { - "$ref": "Location" - }, - "response": { - "$ref": "Location" - }, - "scopes": [ - "https://www.googleapis.com/auth/latitude" - ] - }, - "list": { - "id": "latitude.location.list", - "path": "location", - "httpMethod": "GET", - "description": "Lists the user's location history.", - "parameters": { - "granularity": { - "type": "string", - "description": "Granularity of the requested locations.", - "location": "query" - }, - "max-results": { - "type": "string", - "description": "Maximum number of locations to return.", - "location": "query" - }, - "max-time": { - "type": "string", - "description": "Maximum timestamp of locations to return (ms since epoch).", - "location": "query" - }, - "min-time": { - "type": "string", - "description": "Minimum timestamp of locations to return (ms since epoch).", - "location": "query" - } - }, - "response": { - "$ref": "LocationFeed" - }, - "scopes": [ - "https://www.googleapis.com/auth/latitude" - ] - } - } - } - } -} diff --git a/tests/data/malformed.json b/tests/data/malformed.json deleted file mode 100644 index 98232c6..0000000 --- a/tests/data/malformed.json +++ /dev/null @@ -1 +0,0 @@ -{ diff --git a/tests/data/moderator.json b/tests/data/moderator.json deleted file mode 100644 index 55a7483..0000000 --- a/tests/data/moderator.json +++ /dev/null @@ -1,1665 +0,0 @@ -{ - "kind": "discovery#restDescription", - "id": "moderator:v1", - "name": "moderator", - "version": "v1", - "description": "Moderator API", - "icons": { - "x16": "http://www.google.com/images/icons/product/moderator-32.png", - "x32": "http://www.google.com/images/icons/product/search-32.gif" - }, - "documentationLink": "http://code.google.com/apis/moderator/v1/using_rest.html", - "labels": [ - "labs" - ], - "protocol": "rest", - "basePath": "/moderator/v1/", - "auth": { - "oauth2": { - "scopes": { - "https://www.googleapis.com/auth/moderator": { - "description": "Manage your activity in Google Moderator" - } - } - } - }, - "features": [ - "dataWrapper" - ], - "schemas": { - "ModeratorTopicsResourcePartial": { - "id": "ModeratorTopicsResourcePartial", - "type": "object", - "properties": { - "id": { - "type": "object", - "properties": { - "seriesId": { - "type": "integer" - }, - "topicId": { - "type": "integer" - } - } - } - } - }, - "ModeratorVotesResourcePartial": { - "id": "ModeratorVotesResourcePartial", - "type": "object", - "properties": { - "flag": { - "type": "string" - }, - "vote": { - "type": "string" - } - } - }, - "Profile": { - "id": "Profile", - "type": "object", - "properties": { - "attribution": { - "type": "object", - "properties": { - "avatarUrl": { - "type": "string" - }, - "displayName": { - "type": "string" - }, - "geo": { - "type": "object", - "properties": { - "latitude": { - "type": "number" - }, - "location": { - "type": "string" - }, - "longitude": { - "type": "number" - } - } - }, - "location": { - "type": "string" - } - } - }, - "id": { - "type": "object", - "properties": { - "user": { - "type": "string" - } - } - }, - "kind": { - "type": "string", - "default": "moderator#profile" - } - } - }, - "Series": { - "id": "Series", - "type": "object", - "properties": { - "anonymousSubmissionAllowed": { - "type": "boolean" - }, - "counters": { - "type": "object", - "properties": { - "anonymousSubmissions": { - "type": "integer" - }, - "minusVotes": { - "type": "integer" - }, - "noneVotes": { - "type": "integer" - }, - "plusVotes": { - "type": "integer" - }, - "submissions": { - "type": "integer" - }, - "users": { - "type": "integer" - }, - "videoSubmissions": { - "type": "integer" - } - } - }, - "description": { - "type": "string" - }, - "id": { - "type": "object", - "properties": { - "seriesId": { - "type": "integer" - } - } - }, - "kind": { - "type": "string", - "default": "moderator#series" - }, - "name": { - "type": "string" - }, - "numTopics": { - "type": "integer" - }, - "videoSubmissionAllowed": { - "type": "boolean" - } - } - }, - "SeriesList": { - "id": "SeriesList", - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "Series" - } - }, - "kind": { - "type": "string", - "default": "moderator#seriesList" - } - } - }, - "Submission": { - "id": "Submission", - "type": "object", - "properties": { - "attachmentUrl": { - "type": "string" - }, - "attribution": { - "type": "object", - "properties": { - "avatarUrl": { - "type": "string" - }, - "displayName": { - "type": "string" - }, - "location": { - "type": "string" - } - } - }, - "author": { - "type": "string" - }, - "counters": { - "type": "object", - "properties": { - "minusVotes": { - "type": "integer" - }, - "noneVotes": { - "type": "integer" - }, - "plusVotes": { - "type": "integer" - } - } - }, - "created": { - "type": "integer" - }, - "geo": { - "type": "object", - "properties": { - "latitude": { - "type": "number" - }, - "location": { - "type": "string" - }, - "longitude": { - "type": "number" - } - } - }, - "id": { - "type": "object", - "properties": { - "seriesId": { - "type": "integer" - }, - "submissionId": { - "type": "integer" - } - } - }, - "kind": { - "type": "string", - "default": "moderator#submission" - }, - "parentSubmissionId": { - "type": "object", - "properties": { - "seriesId": { - "type": "integer" - }, - "submissionId": { - "type": "integer" - } - } - }, - "text": { - "type": "string" - }, - "topics": { - "type": "array", - "items": { - "$ref": "ModeratorTopicsResourcePartial" - } - }, - "translations": { - "type": "array", - "items": { - "type": "object", - "properties": { - "lang": { - "type": "string" - }, - "text": { - "type": "string" - } - } - } - }, - "vote": { - "$ref": "ModeratorVotesResourcePartial" - } - } - }, - "SubmissionList": { - "id": "SubmissionList", - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "Submission" - } - }, - "kind": { - "type": "string", - "default": "moderator#submissionList" - } - } - }, - "Tag": { - "id": "Tag", - "type": "object", - "properties": { - "id": { - "type": "object", - "properties": { - "seriesId": { - "type": "integer" - }, - "submissionId": { - "type": "integer" - }, - "tagId": { - "type": "string" - } - } - }, - "kind": { - "type": "string", - "default": "moderator#tag" - }, - "text": { - "type": "string" - } - } - }, - "TagList": { - "id": "TagList", - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "Tag" - } - }, - "kind": { - "type": "string", - "default": "moderator#tagList" - } - } - }, - "Topic": { - "id": "Topic", - "type": "object", - "properties": { - "counters": { - "type": "object", - "properties": { - "minusVotes": { - "type": "integer" - }, - "noneVotes": { - "type": "integer" - }, - "plusVotes": { - "type": "integer" - }, - "submissions": { - "type": "integer" - }, - "users": { - "type": "integer" - }, - "videoSubmissions": { - "type": "integer" - } - } - }, - "description": { - "type": "string" - }, - "featuredSubmission": { - "type": "any" - }, - "id": { - "type": "object", - "properties": { - "seriesId": { - "type": "integer" - }, - "topicId": { - "type": "integer" - } - } - }, - "kind": { - "type": "string", - "default": "moderator#topic" - }, - "name": { - "type": "string" - }, - "presenter": { - "type": "string" - }, - "rules": { - "type": "object", - "properties": { - "submissions": { - "type": "object", - "properties": { - "close": { - "type": "integer" - }, - "open": { - "type": "integer" - } - } - }, - "votes": { - "type": "object", - "properties": { - "close": { - "type": "integer" - }, - "open": { - "type": "integer" - } - } - } - } - } - } - }, - "Topic2": { - "id": "Topic2", - "type": "object", - "properties": { - "counters": { - "type": "object", - "properties": { - "minusVotes": { - "type": "integer" - }, - "noneVotes": { - "type": "integer" - }, - "plusVotes": { - "type": "integer" - }, - "submissions": { - "type": "integer" - }, - "users": { - "type": "integer" - }, - "videoSubmissions": { - "type": "integer" - } - } - }, - "description": { - "type": "string" - }, - "featuredSubmission": { - "$ref": "Submission" - }, - "id": { - "type": "object", - "properties": { - "seriesId": { - "type": "integer" - }, - "topicId": { - "type": "integer" - } - } - }, - "kind": { - "type": "string", - "default": "moderator#topic" - }, - "name": { - "type": "string" - }, - "presenter": { - "type": "string" - }, - "rules": { - "type": "object", - "properties": { - "submissions": { - "type": "object", - "properties": { - "close": { - "type": "integer" - }, - "open": { - "type": "integer" - } - } - }, - "votes": { - "type": "object", - "properties": { - "close": { - "type": "integer" - }, - "open": { - "type": "integer" - } - } - } - } - } - } - }, - "TopicList": { - "id": "TopicList", - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "Topic" - } - }, - "kind": { - "type": "string", - "default": "moderator#topicList" - } - } - }, - "Vote": { - "id": "Vote", - "type": "object", - "properties": { - "flag": { - "type": "string" - }, - "id": { - "type": "object", - "properties": { - "seriesId": { - "type": "integer" - }, - "submissionId": { - "type": "integer" - } - } - }, - "kind": { - "type": "string", - "default": "moderator#vote" - }, - "vote": { - "type": "string" - } - } - }, - "VoteList": { - "id": "VoteList", - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "Vote" - } - }, - "kind": { - "type": "string", - "default": "moderator#voteList" - } - } - } - }, - "resources": { - "featured": { - "resources": { - "series": { - "methods": { - "list": { - "id": "moderator.featured.series.list", - "path": "series/featured", - "httpMethod": "GET", - "description": "Lists the featured series.", - "response": { - "$ref": "SeriesList" - }, - "scopes": [ - "https://www.googleapis.com/auth/moderator" - ] - } - } - } - } - }, - "global": { - "resources": { - "series": { - "methods": { - "list": { - "id": "moderator.global.series.list", - "path": "search", - "httpMethod": "GET", - "description": "Searches the public series and returns the search results.", - "parameters": { - "max-results": { - "type": "integer", - "description": "Maximum number of results to return.", - "minimum": "0", - "maximum": "4294967295", - "location": "query" - }, - "q": { - "type": "string", - "description": "Search query.", - "location": "query" - }, - "start-index": { - "type": "integer", - "description": "Index of the first result to be retrieved.", - "minimum": "0", - "maximum": "4294967295", - "location": "query" - } - }, - "response": { - "$ref": "SeriesList" - }, - "scopes": [ - "https://www.googleapis.com/auth/moderator" - ] - } - } - } - } - }, - "my": { - "resources": { - "series": { - "methods": { - "list": { - "id": "moderator.my.series.list", - "path": "series/@me/mine", - "httpMethod": "GET", - "description": "Lists all series created by the authenticated user.", - "response": { - "$ref": "SeriesList" - }, - "scopes": [ - "https://www.googleapis.com/auth/moderator" - ] - } - } - } - } - }, - "myrecent": { - "resources": { - "series": { - "methods": { - "list": { - "id": "moderator.myrecent.series.list", - "path": "series/@me/recent", - "httpMethod": "GET", - "description": "Lists the series the authenticated user has visited.", - "response": { - "$ref": "SeriesList" - }, - "scopes": [ - "https://www.googleapis.com/auth/moderator" - ] - } - } - } - } - }, - "profiles": { - "methods": { - "get": { - "id": "moderator.profiles.get", - "path": "profiles/@me", - "httpMethod": "GET", - "description": "Returns the profile information for the authenticated user.", - "response": { - "$ref": "Profile" - }, - "scopes": [ - "https://www.googleapis.com/auth/moderator" - ] - }, - "update": { - "id": "moderator.profiles.update", - "path": "profiles/@me", - "httpMethod": "PUT", - "description": "Updates the profile information for the authenticated user.", - "request": { - "$ref": "Profile" - }, - "response": { - "$ref": "Profile" - }, - "scopes": [ - "https://www.googleapis.com/auth/moderator" - ] - } - } - }, - "responses": { - "methods": { - "insert": { - "id": "moderator.responses.insert", - "path": "series/{seriesId}/topics/{topicId}/submissions/{parentSubmissionId}/responses", - "httpMethod": "POST", - "description": "Inserts a response for the specified submission in the specified topic within the specified series.", - "parameters": { - "anonymous": { - "type": "boolean", - "description": "Set to true to mark the new submission as anonymous.", - "location": "query" - }, - "parentSubmissionId": { - "type": "integer", - "description": "The decimal ID of the parent Submission within the Series.", - "required": true, - "minimum": "0", - "maximum": "4294967295", - "location": "path" - }, - "seriesId": { - "type": "integer", - "description": "The decimal ID of the Series.", - "required": true, - "minimum": "0", - "maximum": "4294967295", - "location": "path" - }, - "topicId": { - "type": "integer", - "description": "The decimal ID of the Topic within the Series.", - "required": true, - "minimum": "0", - "maximum": "4294967295", - "location": "path" - } - }, - "parameterOrder": [ - "seriesId", - "topicId", - "parentSubmissionId" - ], - "request": { - "$ref": "Submission" - }, - "response": { - "$ref": "Submission" - }, - "scopes": [ - "https://www.googleapis.com/auth/moderator" - ] - }, - "list": { - "id": "moderator.responses.list", - "path": "series/{seriesId}/submissions/{submissionId}/responses", - "httpMethod": "GET", - "description": "Lists or searches the responses for the specified submission within the specified series and returns the search results.", - "parameters": { - "author": { - "type": "string", - "description": "Restricts the results to submissions by a specific author.", - "location": "query" - }, - "hasAttachedVideo": { - "type": "boolean", - "description": "Specifies whether to restrict to submissions that have videos attached.", - "location": "query" - }, - "max-results": { - "type": "integer", - "description": "Maximum number of results to return.", - "minimum": "0", - "maximum": "4294967295", - "location": "query" - }, - "q": { - "type": "string", - "description": "Search query.", - "location": "query" - }, - "seriesId": { - "type": "integer", - "description": "The decimal ID of the Series.", - "required": true, - "minimum": "0", - "maximum": "4294967295", - "location": "path" - }, - "sort": { - "type": "string", - "description": "Sort order.", - "location": "query" - }, - "start-index": { - "type": "integer", - "description": "Index of the first result to be retrieved.", - "minimum": "0", - "maximum": "4294967295", - "location": "query" - }, - "submissionId": { - "type": "integer", - "description": "The decimal ID of the Submission within the Series.", - "required": true, - "minimum": "0", - "maximum": "4294967295", - "location": "path" - } - }, - "parameterOrder": [ - "seriesId", - "submissionId" - ], - "response": { - "$ref": "SubmissionList" - }, - "scopes": [ - "https://www.googleapis.com/auth/moderator" - ] - } - } - }, - "series": { - "methods": { - "get": { - "id": "moderator.series.get", - "path": "series/{seriesId}", - "httpMethod": "GET", - "description": "Returns the specified series.", - "parameters": { - "seriesId": { - "type": "integer", - "description": "The decimal ID of the Series.", - "required": true, - "minimum": "0", - "maximum": "4294967295", - "location": "path" - } - }, - "parameterOrder": [ - "seriesId" - ], - "response": { - "$ref": "Series" - }, - "scopes": [ - "https://www.googleapis.com/auth/moderator" - ] - }, - "insert": { - "id": "moderator.series.insert", - "path": "series", - "httpMethod": "POST", - "description": "Inserts a new series.", - "request": { - "$ref": "Series" - }, - "response": { - "$ref": "Series" - }, - "scopes": [ - "https://www.googleapis.com/auth/moderator" - ] - }, - "list": { - "id": "moderator.series.list", - "path": "series", - "httpMethod": "GET", - "description": "Searches the series and returns the search results.", - "parameters": { - "max-results": { - "type": "integer", - "description": "Maximum number of results to return.", - "minimum": "0", - "maximum": "4294967295", - "location": "query" - }, - "q": { - "type": "string", - "description": "Search query.", - "location": "query" - }, - "start-index": { - "type": "integer", - "description": "Index of the first result to be retrieved.", - "minimum": "0", - "maximum": "4294967295", - "location": "query" - } - }, - "response": { - "$ref": "SeriesList" - }, - "scopes": [ - "https://www.googleapis.com/auth/moderator" - ] - }, - "update": { - "id": "moderator.series.update", - "path": "series/{seriesId}", - "httpMethod": "PUT", - "description": "Updates the specified series.", - "parameters": { - "seriesId": { - "type": "integer", - "description": "The decimal ID of the Series.", - "required": true, - "minimum": "0", - "maximum": "4294967295", - "location": "path" - } - }, - "parameterOrder": [ - "seriesId" - ], - "request": { - "$ref": "Series" - }, - "response": { - "$ref": "Series" - }, - "scopes": [ - "https://www.googleapis.com/auth/moderator" - ] - } - }, - "resources": { - "responses": { - "methods": { - "list": { - "id": "moderator.series.responses.list", - "path": "series/{seriesId}/responses", - "httpMethod": "GET", - "description": "Searches the responses for the specified series and returns the search results.", - "parameters": { - "author": { - "type": "string", - "description": "Restricts the results to submissions by a specific author.", - "location": "query" - }, - "hasAttachedVideo": { - "type": "boolean", - "description": "Specifies whether to restrict to submissions that have videos attached.", - "location": "query" - }, - "max-results": { - "type": "integer", - "description": "Maximum number of results to return.", - "minimum": "0", - "maximum": "4294967295", - "location": "query" - }, - "q": { - "type": "string", - "description": "Search query.", - "location": "query" - }, - "seriesId": { - "type": "integer", - "description": "The decimal ID of the Series.", - "required": true, - "minimum": "0", - "maximum": "4294967295", - "location": "path" - }, - "sort": { - "type": "string", - "description": "Sort order.", - "location": "query" - }, - "start-index": { - "type": "integer", - "description": "Index of the first result to be retrieved.", - "minimum": "0", - "maximum": "4294967295", - "location": "query" - } - }, - "parameterOrder": [ - "seriesId" - ], - "response": { - "$ref": "SeriesList" - }, - "scopes": [ - "https://www.googleapis.com/auth/moderator" - ] - } - } - }, - "submissions": { - "methods": { - "list": { - "id": "moderator.series.submissions.list", - "path": "series/{seriesId}/submissions", - "httpMethod": "GET", - "description": "Searches the submissions for the specified series and returns the search results.", - "parameters": { - "author": { - "type": "string", - "description": "Restricts the results to submissions by a specific author.", - "location": "query" - }, - "hasAttachedVideo": { - "type": "boolean", - "description": "Specifies whether to restrict to submissions that have videos attached.", - "location": "query" - }, - "includeVotes": { - "type": "boolean", - "description": "Specifies whether to include the current user's vote", - "location": "query" - }, - "lang": { - "type": "string", - "description": "The language code for the language the client prefers resuls in.", - "location": "query" - }, - "max-results": { - "type": "integer", - "description": "Maximum number of results to return.", - "minimum": "0", - "maximum": "4294967295", - "location": "query" - }, - "q": { - "type": "string", - "description": "Search query.", - "location": "query" - }, - "seriesId": { - "type": "integer", - "description": "The decimal ID of the Series.", - "required": true, - "minimum": "0", - "maximum": "4294967295", - "location": "path" - }, - "sort": { - "type": "string", - "description": "Sort order.", - "location": "query" - }, - "start-index": { - "type": "integer", - "description": "Index of the first result to be retrieved.", - "minimum": "0", - "maximum": "4294967295", - "location": "query" - } - }, - "parameterOrder": [ - "seriesId" - ], - "response": { - "$ref": "SubmissionList" - }, - "scopes": [ - "https://www.googleapis.com/auth/moderator" - ] - } - } - } - } - }, - "submissions": { - "methods": { - "get": { - "id": "moderator.submissions.get", - "path": "series/{seriesId}/submissions/{submissionId}", - "httpMethod": "GET", - "description": "Returns the specified submission within the specified series.", - "parameters": { - "includeVotes": { - "type": "boolean", - "description": "Specifies whether to include the current user's vote", - "location": "query" - }, - "lang": { - "type": "string", - "description": "The language code for the language the client prefers resuls in.", - "location": "query" - }, - "seriesId": { - "type": "integer", - "description": "The decimal ID of the Series.", - "required": true, - "minimum": "0", - "maximum": "4294967295", - "location": "path" - }, - "submissionId": { - "type": "integer", - "description": "The decimal ID of the Submission within the Series.", - "required": true, - "minimum": "0", - "maximum": "4294967295", - "location": "path" - } - }, - "parameterOrder": [ - "seriesId", - "submissionId" - ], - "response": { - "$ref": "Submission" - }, - "scopes": [ - "https://www.googleapis.com/auth/moderator" - ] - }, - "insert": { - "id": "moderator.submissions.insert", - "path": "series/{seriesId}/topics/{topicId}/submissions", - "httpMethod": "POST", - "description": "Inserts a new submission in the specified topic within the specified series.", - "parameters": { - "anonymous": { - "type": "boolean", - "description": "Set to true to mark the new submission as anonymous.", - "location": "query" - }, - "seriesId": { - "type": "integer", - "description": "The decimal ID of the Series.", - "required": true, - "minimum": "0", - "maximum": "4294967295", - "location": "path" - }, - "topicId": { - "type": "integer", - "description": "The decimal ID of the Topic within the Series.", - "required": true, - "minimum": "0", - "maximum": "4294967295", - "location": "path" - } - }, - "parameterOrder": [ - "seriesId", - "topicId" - ], - "request": { - "$ref": "Submission" - }, - "response": { - "$ref": "Submission" - }, - "scopes": [ - "https://www.googleapis.com/auth/moderator" - ] - } - } - }, - "tags": { - "methods": { - "delete": { - "id": "moderator.tags.delete", - "path": "series/{seriesId}/submissions/{submissionId}/tags/{tagId}", - "httpMethod": "DELETE", - "description": "Deletes the specified tag from the specified submission within the specified series.", - "parameters": { - "seriesId": { - "type": "integer", - "description": "The decimal ID of the Series.", - "required": true, - "minimum": "0", - "maximum": "4294967295", - "location": "path" - }, - "submissionId": { - "type": "integer", - "description": "The decimal ID of the Submission within the Series.", - "required": true, - "minimum": "0", - "maximum": "4294967295", - "location": "path" - }, - "tagId": { - "type": "string", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "seriesId", - "submissionId", - "tagId" - ], - "scopes": [ - "https://www.googleapis.com/auth/moderator" - ] - }, - "insert": { - "id": "moderator.tags.insert", - "path": "series/{seriesId}/submissions/{submissionId}/tags", - "httpMethod": "POST", - "description": "Inserts a new tag for the specified submission within the specified series.", - "parameters": { - "seriesId": { - "type": "integer", - "description": "The decimal ID of the Series.", - "required": true, - "minimum": "0", - "maximum": "4294967295", - "location": "path" - }, - "submissionId": { - "type": "integer", - "description": "The decimal ID of the Submission within the Series.", - "required": true, - "minimum": "0", - "maximum": "4294967295", - "location": "path" - } - }, - "parameterOrder": [ - "seriesId", - "submissionId" - ], - "request": { - "$ref": "Tag" - }, - "response": { - "$ref": "Tag" - }, - "scopes": [ - "https://www.googleapis.com/auth/moderator" - ] - }, - "list": { - "id": "moderator.tags.list", - "path": "series/{seriesId}/submissions/{submissionId}/tags", - "httpMethod": "GET", - "description": "Lists all tags for the specified submission within the specified series.", - "parameters": { - "seriesId": { - "type": "integer", - "description": "The decimal ID of the Series.", - "required": true, - "minimum": "0", - "maximum": "4294967295", - "location": "path" - }, - "submissionId": { - "type": "integer", - "description": "The decimal ID of the Submission within the Series.", - "required": true, - "minimum": "0", - "maximum": "4294967295", - "location": "path" - } - }, - "parameterOrder": [ - "seriesId", - "submissionId" - ], - "response": { - "$ref": "TagList" - }, - "scopes": [ - "https://www.googleapis.com/auth/moderator" - ] - } - } - }, - "topics": { - "methods": { - "get": { - "id": "moderator.topics.get", - "path": "series/{seriesId}/topics/{topicId}", - "httpMethod": "GET", - "description": "Returns the specified topic from the specified series.", - "parameters": { - "seriesId": { - "type": "integer", - "description": "The decimal ID of the Series.", - "required": true, - "minimum": "0", - "maximum": "4294967295", - "location": "path" - }, - "topicId": { - "type": "integer", - "description": "The decimal ID of the Topic within the Series.", - "required": true, - "minimum": "0", - "maximum": "4294967295", - "location": "path" - } - }, - "parameterOrder": [ - "seriesId", - "topicId" - ], - "response": { - "$ref": "Topic2" - }, - "scopes": [ - "https://www.googleapis.com/auth/moderator" - ] - }, - "insert": { - "id": "moderator.topics.insert", - "path": "series/{seriesId}/topics", - "httpMethod": "POST", - "description": "Inserts a new topic into the specified series.", - "parameters": { - "seriesId": { - "type": "integer", - "description": "The decimal ID of the Series.", - "required": true, - "minimum": "0", - "maximum": "4294967295", - "location": "path" - } - }, - "parameterOrder": [ - "seriesId" - ], - "request": { - "$ref": "Topic" - }, - "response": { - "$ref": "Topic" - }, - "scopes": [ - "https://www.googleapis.com/auth/moderator" - ] - }, - "list": { - "id": "moderator.topics.list", - "path": "series/{seriesId}/topics", - "httpMethod": "GET", - "description": "Searches the topics within the specified series and returns the search results.", - "parameters": { - "max-results": { - "type": "integer", - "description": "Maximum number of results to return.", - "minimum": "0", - "maximum": "4294967295", - "location": "query" - }, - "mode": { - "type": "string", - "location": "query" - }, - "q": { - "type": "string", - "description": "Search query.", - "location": "query" - }, - "seriesId": { - "type": "integer", - "description": "The decimal ID of the Series.", - "required": true, - "minimum": "0", - "maximum": "4294967295", - "location": "path" - }, - "start-index": { - "type": "integer", - "description": "Index of the first result to be retrieved.", - "minimum": "0", - "maximum": "4294967295", - "location": "query" - } - }, - "parameterOrder": [ - "seriesId" - ], - "response": { - "$ref": "TopicList" - }, - "scopes": [ - "https://www.googleapis.com/auth/moderator" - ] - }, - "update": { - "id": "moderator.topics.update", - "path": "series/{seriesId}/topics/{topicId}", - "httpMethod": "PUT", - "description": "Updates the specified topic within the specified series.", - "parameters": { - "seriesId": { - "type": "integer", - "description": "The decimal ID of the Series.", - "required": true, - "minimum": "0", - "maximum": "4294967295", - "location": "path" - }, - "topicId": { - "type": "integer", - "description": "The decimal ID of the Topic within the Series.", - "required": true, - "minimum": "0", - "maximum": "4294967295", - "location": "path" - } - }, - "parameterOrder": [ - "seriesId", - "topicId" - ], - "request": { - "$ref": "Topic" - }, - "response": { - "$ref": "Topic" - }, - "scopes": [ - "https://www.googleapis.com/auth/moderator" - ] - } - }, - "resources": { - "submissions": { - "methods": { - "list": { - "id": "moderator.topics.submissions.list", - "path": "series/{seriesId}/topics/{topicId}/submissions", - "httpMethod": "GET", - "description": "Searches the submissions for the specified topic within the specified series and returns the search results.", - "parameters": { - "author": { - "type": "string", - "description": "Restricts the results to submissions by a specific author.", - "location": "query" - }, - "hasAttachedVideo": { - "type": "boolean", - "description": "Specifies whether to restrict to submissions that have videos attached.", - "location": "query" - }, - "includeVotes": { - "type": "boolean", - "description": "Specifies whether to include the current user's vote", - "location": "query" - }, - "max-results": { - "type": "integer", - "description": "Maximum number of results to return.", - "minimum": "0", - "maximum": "4294967295", - "location": "query" - }, - "q": { - "type": "string", - "description": "Search query.", - "location": "query" - }, - "seriesId": { - "type": "integer", - "description": "The decimal ID of the Series.", - "required": true, - "minimum": "0", - "maximum": "4294967295", - "location": "path" - }, - "sort": { - "type": "string", - "description": "Sort order.", - "location": "query" - }, - "start-index": { - "type": "integer", - "description": "Index of the first result to be retrieved.", - "minimum": "0", - "maximum": "4294967295", - "location": "query" - }, - "topicId": { - "type": "integer", - "description": "The decimal ID of the Topic within the Series.", - "required": true, - "minimum": "0", - "maximum": "4294967295", - "location": "path" - } - }, - "parameterOrder": [ - "seriesId", - "topicId" - ], - "response": { - "$ref": "SubmissionList" - }, - "scopes": [ - "https://www.googleapis.com/auth/moderator" - ] - } - } - } - } - }, - "votes": { - "methods": { - "get": { - "id": "moderator.votes.get", - "path": "series/{seriesId}/submissions/{submissionId}/votes/@me", - "httpMethod": "GET", - "description": "Returns the votes by the authenticated user for the specified submission within the specified series.", - "parameters": { - "seriesId": { - "type": "integer", - "description": "The decimal ID of the Series.", - "required": true, - "minimum": "0", - "maximum": "4294967295", - "location": "path" - }, - "submissionId": { - "type": "integer", - "description": "The decimal ID of the Submission within the Series.", - "required": true, - "minimum": "0", - "maximum": "4294967295", - "location": "path" - }, - "userId": { - "type": "string", - "location": "query" - } - }, - "parameterOrder": [ - "seriesId", - "submissionId" - ], - "response": { - "$ref": "Vote" - }, - "scopes": [ - "https://www.googleapis.com/auth/moderator" - ] - }, - "insert": { - "id": "moderator.votes.insert", - "path": "series/{seriesId}/submissions/{submissionId}/votes/@me", - "httpMethod": "POST", - "description": "Inserts a new vote by the authenticated user for the specified submission within the specified series.", - "parameters": { - "seriesId": { - "type": "integer", - "description": "The decimal ID of the Series.", - "required": true, - "minimum": "0", - "maximum": "4294967295", - "location": "path" - }, - "submissionId": { - "type": "integer", - "description": "The decimal ID of the Submission within the Series.", - "required": true, - "minimum": "0", - "maximum": "4294967295", - "location": "path" - } - }, - "parameterOrder": [ - "seriesId", - "submissionId" - ], - "request": { - "$ref": "Vote" - }, - "response": { - "$ref": "Vote" - }, - "scopes": [ - "https://www.googleapis.com/auth/moderator" - ] - }, - "list": { - "id": "moderator.votes.list", - "path": "series/{seriesId}/votes/@me", - "httpMethod": "GET", - "description": "Lists the votes by the authenticated user for the given series.", - "parameters": { - "max-results": { - "type": "integer", - "description": "Maximum number of results to return.", - "minimum": "0", - "maximum": "4294967295", - "location": "query" - }, - "seriesId": { - "type": "integer", - "description": "The decimal ID of the Series.", - "required": true, - "minimum": "0", - "maximum": "4294967295", - "location": "path" - }, - "start-index": { - "type": "integer", - "description": "Index of the first result to be retrieved.", - "minimum": "0", - "maximum": "4294967295", - "location": "query" - } - }, - "parameterOrder": [ - "seriesId" - ], - "response": { - "$ref": "VoteList" - }, - "scopes": [ - "https://www.googleapis.com/auth/moderator" - ] - }, - "update": { - "id": "moderator.votes.update", - "path": "series/{seriesId}/submissions/{submissionId}/votes/@me", - "httpMethod": "PUT", - "description": "Updates the votes by the authenticated user for the specified submission within the specified series.", - "parameters": { - "seriesId": { - "type": "integer", - "description": "The decimal ID of the Series.", - "required": true, - "minimum": "0", - "maximum": "4294967295", - "location": "path" - }, - "submissionId": { - "type": "integer", - "description": "The decimal ID of the Submission within the Series.", - "required": true, - "minimum": "0", - "maximum": "4294967295", - "location": "path" - }, - "userId": { - "type": "string", - "location": "query" - } - }, - "parameterOrder": [ - "seriesId", - "submissionId" - ], - "request": { - "$ref": "Vote" - }, - "response": { - "$ref": "Vote" - }, - "scopes": [ - "https://www.googleapis.com/auth/moderator" - ] - } - } - } - } -} diff --git a/tests/data/plus.json b/tests/data/plus.json deleted file mode 100644 index 36d3ae9..0000000 --- a/tests/data/plus.json +++ /dev/null @@ -1,1220 +0,0 @@ -{ - "kind": "discovery#restDescription", - "id": "plus:v1", - "name": "plus", - "version": "v1", - "title": "Google+ API", - "description": "The Google+ API enables developers to build on top of the Google+ platform.", - "icons": { - "x16": "http://www.google.com/images/icons/product/gplus-16.png", - "x32": "http://www.google.com/images/icons/product/gplus-32.png" - }, - "documentationLink": "http://developers.google.com/+/api/", - "labels": [ - "labs" - ], - "protocol": "rest", - "basePath": "/plus/v1/", - "rootUrl": "https://www.googleapis.com/", - "servicePath": "plus/v1/", - "parameters": { - "alt": { - "type": "string", - "description": "Data format for the response.", - "default": "json", - "enum": [ - "json" - ], - "enumDescriptions": [ - "Responses with Content-Type of application/json" - ], - "location": "query" - }, - "fields": { - "type": "string", - "description": "Selector specifying which fields to include in a partial response.", - "location": "query" - }, - "key": { - "type": "string", - "description": "API key. Your API key identifies your project and provides you with API access, quota, and reports. Required unless you provide an OAuth 2.0 token.", - "location": "query" - }, - "oauth_token": { - "type": "string", - "description": "OAuth 2.0 token for the current user.", - "location": "query" - }, - "prettyPrint": { - "type": "boolean", - "description": "Returns response with indentations and line breaks.", - "default": "true", - "location": "query" - }, - "quotaUser": { - "type": "string", - "description": "Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. Overrides userIp if both are provided.", - "location": "query" - }, - "userIp": { - "type": "string", - "description": "IP address of the site where the request originates. Use this if you want to enforce per-user limits.", - "location": "query" - } - }, - "auth": { - "oauth2": { - "scopes": { - "https://www.googleapis.com/auth/plus.me": { - "description": "Know who you are on Google" - } - } - } - }, - "schemas": { - "Acl": { - "id": "Acl", - "type": "object", - "properties": { - "description": { - "type": "string", - "description": "Description of the access granted, suitable for display." - }, - "items": { - "type": "array", - "description": "The list of access entries.", - "items": { - "$ref": "PlusAclentryResource" - } - }, - "kind": { - "type": "string", - "description": "Identifies this resource as a collection of access controls. Value: \"plus#acl\".", - "default": "plus#acl" - } - } - }, - "Activity": { - "id": "Activity", - "type": "object", - "properties": { - "access": { - "$ref": "Acl", - "description": "Identifies who has access to see this activity." - }, - "actor": { - "type": "object", - "description": "The person who performed this activity.", - "properties": { - "displayName": { - "type": "string", - "description": "The name of the actor, suitable for display." - }, - "id": { - "type": "string", - "description": "The ID of the actor's person resource." - }, - "image": { - "type": "object", - "description": "The image representation of the actor.", - "properties": { - "url": { - "type": "string", - "description": "The URL of the actor's profile photo. To re-size the image and crop it to a square, append the query string ?sz=x, where x is the dimension in pixels of each side." - } - } - }, - "url": { - "type": "string", - "description": "The link to the actor's Google profile." - } - } - }, - "address": { - "type": "string", - "description": "Street address where this activity occurred." - }, - "annotation": { - "type": "string", - "description": "Additional content added by the person who shared this activity, applicable only when resharing an activity." - }, - "crosspostSource": { - "type": "string", - "description": "If this activity is a crosspost from another system, this property specifies the ID of the original activity." - }, - "geocode": { - "type": "string", - "description": "Latitude and longitude where this activity occurred. Format is latitude followed by longitude, space separated." - }, - "id": { - "type": "string", - "description": "The ID of this activity." - }, - "kind": { - "type": "string", - "description": "Identifies this resource as an activity. Value: \"plus#activity\".", - "default": "plus#activity" - }, - "object": { - "type": "object", - "description": "The object of this activity.", - "properties": { - "actor": { - "type": "object", - "description": "If this activity's object is itself another activity (for example, when a person reshares an activity), this property specifies the original activity's actor.", - "properties": { - "displayName": { - "type": "string", - "description": "The original actor's name, suitable for display." - }, - "id": { - "type": "string", - "description": "ID of the original actor." - }, - "image": { - "type": "object", - "description": "The image representation of the original actor.", - "properties": { - "url": { - "type": "string", - "description": "A URL that points to a thumbnail photo of the original actor." - } - } - }, - "url": { - "type": "string", - "description": "A link to the original actor's Google profile." - } - } - }, - "attachments": { - "type": "array", - "description": "The media objects attached to this activity.", - "items": { - "type": "object", - "properties": { - "categories": { - "type": "array", - "description": "Specifies zero or more categories the attachment belongs to.", - "items": { - "type": "object", - "properties": { - "label": { - "type": "string", - "description": "The category label, suitable for display (e.g. \"album cover\")." - }, - "schema": { - "type": "string", - "description": "Domain of schema, e.g. http://google.com." - }, - "term": { - "type": "string", - "description": "The tag, e.g. album." - } - } - } - }, - "content": { - "type": "string", - "description": "If the attachment is an article, this property contains a snippet of text from the article." - }, - "contentsource": { - "type": "object", - "description": "If the attachment is audio, the link to the content.", - "properties": { - "type": { - "type": "string", - "description": "Media type of the link." - }, - "url": { - "type": "string", - "description": "URL of the link." - } - } - }, - "displayName": { - "type": "string", - "description": "The title of the attachment (such as a photo caption or an article title)." - }, - "embed": { - "type": "object", - "description": "If the attachment is a video, the embeddable link.", - "properties": { - "type": { - "type": "string", - "description": "Media type of the link." - }, - "url": { - "type": "string", - "description": "URL of the link." - } - } - }, - "fullImage": { - "type": "object", - "description": "The full image url for photo attachments.", - "properties": { - "height": { - "type": "integer", - "description": "The height, in pixels, of the linked resource.", - "format": "uint32" - }, - "type": { - "type": "string", - "description": "Media type of the link." - }, - "url": { - "type": "string", - "description": "URL of the link." - }, - "width": { - "type": "integer", - "description": "The width, in pixels, of the linked resource.", - "format": "uint32" - } - } - }, - "id": { - "type": "string", - "description": "The ID of the media object's resource." - }, - "image": { - "type": "object", - "description": "The preview image for photos or videos.", - "properties": { - "height": { - "type": "integer", - "description": "The height, in pixels, of the linked resource.", - "format": "uint32" - }, - "type": { - "type": "string", - "description": "Media type of the link." - }, - "url": { - "type": "string", - "description": "URL of the link." - }, - "width": { - "type": "integer", - "description": "The width, in pixels, of the linked resource.", - "format": "uint32" - } - } - }, - "objectType": { - "type": "string", - "description": "The type of media object. Possible values are: \n- \"photo\" - A photo. \n- \"video\" - A video. \n- \"article\" - An article, specified by a link." - }, - "url": { - "type": "string", - "description": "The link to the attachment, should be of type text/html." - } - } - } - }, - "content": { - "type": "string", - "description": "The HTML-formatted content, suitable for display. When creating or updating an activity, this value must be supplied as plain text in the request. If successful, the response will contain the HTML-formatted content. When updating an activity, use originalContent as the starting value, then assign the updated text to this property." - }, - "id": { - "type": "string", - "description": "The ID of the object. When resharing an activity, this is the ID of the activity being reshared." - }, - "objectType": { - "type": "string", - "description": "The type of the object. Possible values are: \n- \"note\" - Textual content. \n- \"activity\" - A Google+ activity." - }, - "originalContent": { - "type": "string", - "description": "The content (text) as provided by the author, stored without any HTML formatting. When updating an activity's content, use the value of originalContent as the starting point from which to make edits." - }, - "plusoners": { - "type": "object", - "description": "People who +1'd this activity.", - "properties": { - "selfLink": { - "type": "string", - "description": "The URL for the collection of people who +1'd this activity." - }, - "totalItems": { - "type": "integer", - "description": "Total number of people who +1'd this activity.", - "format": "uint32" - } - } - }, - "replies": { - "type": "object", - "description": "Comments in reply to this activity.", - "properties": { - "selfLink": { - "type": "string", - "description": "The URL for the collection of comments in reply to this activity." - }, - "totalItems": { - "type": "integer", - "description": "Total number of comments on this activity.", - "format": "uint32" - } - } - }, - "resharers": { - "type": "object", - "description": "People who reshared this activity.", - "properties": { - "selfLink": { - "type": "string", - "description": "The URL for the collection of resharers." - }, - "totalItems": { - "type": "integer", - "description": "Total number of people who reshared this activity.", - "format": "uint32" - } - } - }, - "url": { - "type": "string", - "description": "The URL that points to the linked resource." - } - } - }, - "placeId": { - "type": "string", - "description": "ID of the place where this activity occurred." - }, - "placeName": { - "type": "string", - "description": "Name of the place where this activity occurred." - }, - "placeholder": { - "type": "boolean", - "description": "True if this activity is a placeholder." - }, - "provider": { - "type": "object", - "description": "The service provider that initially published this activity.", - "properties": { - "title": { - "type": "string", - "description": "Name of the service provider." - } - } - }, - "published": { - "type": "string", - "description": "The time at which this activity was initially published. Formatted as an RFC 3339 timestamp.", - "format": "date-time" - }, - "radius": { - "type": "string", - "description": "Radius, in meters, of the region where this activity occurred, centered at the latitude and longitude identified in geocode." - }, - "title": { - "type": "string", - "description": "Title of this activity." - }, - "updated": { - "type": "string", - "description": "The time at which this activity was last updated. Formatted as an RFC 3339 timestamp.", - "format": "date-time" - }, - "url": { - "type": "string", - "description": "The link to this activity." - }, - "verb": { - "type": "string", - "description": "This activity's verb, indicating what action was performed. Possible values are: \n- \"post\" - Publish content to the stream. \n- \"checkin\" - Check in to a location. \n- \"share\" - Reshare an activity." - } - } - }, - "ActivityFeed": { - "id": "ActivityFeed", - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "The ID of this collection of activities." - }, - "items": { - "type": "array", - "description": "The activities in this page of results.", - "items": { - "$ref": "Activity" - } - }, - "kind": { - "type": "string", - "description": "Identifies this resource as a collection of activities. Value: \"plus#activityFeed\".", - "default": "plus#activityFeed" - }, - "nextLink": { - "type": "string", - "description": "Link to the next page of activities." - }, - "nextPageToken": { - "type": "string", - "description": "The continuation token, used to page through large result sets. Provide this value in a subsequent request to return the next page of results." - }, - "selfLink": { - "type": "string", - "description": "Link to this activity resource." - }, - "title": { - "type": "string", - "description": "The title of this collection of activities." - }, - "updated": { - "type": "string", - "description": "The time at which this collection of activities was last updated. Formatted as an RFC 3339 timestamp.", - "format": "date-time" - } - } - }, - "Comment": { - "id": "Comment", - "type": "object", - "properties": { - "actor": { - "type": "object", - "description": "The person who posted this comment.", - "properties": { - "displayName": { - "type": "string", - "description": "The name of this actor, suitable for display." - }, - "id": { - "type": "string", - "description": "The ID of the actor." - }, - "image": { - "type": "object", - "description": "The image representation of this actor.", - "properties": { - "url": { - "type": "string", - "description": "The URL of the actor's profile photo. To re-size the image and crop it to a square, append the query string ?sz=x, where x is the dimension in pixels of each side." - } - } - }, - "url": { - "type": "string", - "description": "A link to the person resource for this actor." - } - } - }, - "id": { - "type": "string", - "description": "The ID of this comment." - }, - "inReplyTo": { - "type": "array", - "description": "The activity this comment replied to.", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "The id of the activity." - }, - "url": { - "type": "string", - "description": "The url of the activity." - } - } - } - }, - "kind": { - "type": "string", - "description": "Identifies this resource as a comment. Value: \"plus#comment\".", - "default": "plus#comment" - }, - "object": { - "type": "object", - "description": "The object of this comment.", - "properties": { - "content": { - "type": "string", - "description": "The content of this comment." - }, - "objectType": { - "type": "string", - "description": "The object type of this comment. Possible values are: \n- \"comment\" - A comment in reply to an activity.", - "default": "comment" - } - } - }, - "published": { - "type": "string", - "description": "The time at which this comment was initially published. Formatted as an RFC 3339 timestamp.", - "format": "date-time" - }, - "selfLink": { - "type": "string", - "description": "Link to this comment resource." - }, - "updated": { - "type": "string", - "description": "The time at which this comment was last updated. Formatted as an RFC 3339 timestamp.", - "format": "date-time" - }, - "verb": { - "type": "string", - "description": "This comment's verb, indicating what action was performed. Possible values are: \n- \"post\" - Publish content to the stream.", - "default": "post" - } - } - }, - "CommentFeed": { - "id": "CommentFeed", - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "The ID of this collection of comments." - }, - "items": { - "type": "array", - "description": "The comments in this page of results.", - "items": { - "$ref": "Comment" - } - }, - "kind": { - "type": "string", - "description": "Identifies this resource as a collection of comments. Value: \"plus#commentFeed\".", - "default": "plus#commentFeed" - }, - "nextLink": { - "type": "string", - "description": "Link to the next page of activities." - }, - "nextPageToken": { - "type": "string", - "description": "The continuation token, used to page through large result sets. Provide this value in a subsequent request to return the next page of results." - }, - "title": { - "type": "string", - "description": "The title of this collection of comments." - }, - "updated": { - "type": "string", - "description": "The time at which this collection of comments was last updated. Formatted as an RFC 3339 timestamp.", - "format": "date-time" - } - } - }, - "PeopleFeed": { - "id": "PeopleFeed", - "type": "object", - "properties": { - "items": { - "type": "array", - "description": "The people in this page of results. Each item will include the id, displayName, image, and url for the person. To retrieve additional profile data, see the people.get method.", - "items": { - "$ref": "Person" - } - }, - "kind": { - "type": "string", - "description": "Identifies this resource as a collection of people. Value: \"plus#peopleFeed\".", - "default": "plus#peopleFeed" - }, - "nextPageToken": { - "type": "string", - "description": "The continuation token, used to page through large result sets. Provide this value in a subsequent request to return the next page of results." - }, - "selfLink": { - "type": "string", - "description": "Link to this resource." - }, - "title": { - "type": "string", - "description": "The title of this collection of people." - } - } - }, - "Person": { - "id": "Person", - "type": "object", - "properties": { - "aboutMe": { - "type": "string", - "description": "A short biography for this person." - }, - "birthday": { - "type": "string", - "description": "The person's date of birth, represented as YYYY-MM-DD." - }, - "currentLocation": { - "type": "string", - "description": "The current location for this person." - }, - "displayName": { - "type": "string", - "description": "The name of this person, suitable for display." - }, - "emails": { - "type": "array", - "description": "A list of email addresses for this person.", - "items": { - "type": "object", - "properties": { - "primary": { - "type": "boolean", - "description": "If \"true\", indicates this email address is the person's primary one." - }, - "type": { - "type": "string", - "description": "The type of address. Possible values are: \n- \"home\" - Home email address. \n- \"work\" - Work email address. \n- \"other\" - Other." - }, - "value": { - "type": "string", - "description": "The email address." - } - } - } - }, - "gender": { - "type": "string", - "description": "The person's gender. Possible values are: \n- \"male\" - Male gender. \n- \"female\" - Female gender. \n- \"other\" - Other." - }, - "hasApp": { - "type": "boolean", - "description": "If \"true\", indicates that the person has installed the app that is making the request and has chosen to expose this install state to the caller. A value of \"false\" indicates that the install state cannot be determined (it is either not installed or the person has chosen to keep this information private)." - }, - "id": { - "type": "string", - "description": "The ID of this person." - }, - "image": { - "type": "object", - "description": "The representation of the person's profile photo.", - "properties": { - "url": { - "type": "string", - "description": "The URL of the person's profile photo. To re-size the image and crop it to a square, append the query string ?sz=x, where x is the dimension in pixels of each side." - } - } - }, - "kind": { - "type": "string", - "description": "Identifies this resource as a person. Value: \"plus#person\".", - "default": "plus#person" - }, - "languagesSpoken": { - "type": "array", - "description": "The languages spoken by this person.", - "items": { - "type": "string" - } - }, - "name": { - "type": "object", - "description": "An object representation of the individual components of a person's name.", - "properties": { - "familyName": { - "type": "string", - "description": "The family name (last name) of this person." - }, - "formatted": { - "type": "string", - "description": "The full name of this person, including middle names, suffixes, etc." - }, - "givenName": { - "type": "string", - "description": "The given name (first name) of this person." - }, - "honorificPrefix": { - "type": "string", - "description": "The honorific prefixes (such as \"Dr.\" or \"Mrs.\") for this person." - }, - "honorificSuffix": { - "type": "string", - "description": "The honorific suffixes (such as \"Jr.\") for this person." - }, - "middleName": { - "type": "string", - "description": "The middle name of this person." - } - } - }, - "nickname": { - "type": "string", - "description": "The nickname of this person." - }, - "organizations": { - "type": "array", - "description": "A list of current or past organizations with which this person is associated.", - "items": { - "type": "object", - "properties": { - "department": { - "type": "string", - "description": "The department within the organization." - }, - "description": { - "type": "string", - "description": "A short description of the person's role in this organization." - }, - "endDate": { - "type": "string", - "description": "The date the person left this organization." - }, - "location": { - "type": "string", - "description": "The location of this organization." - }, - "name": { - "type": "string", - "description": "The name of the organization." - }, - "primary": { - "type": "boolean", - "description": "If \"true\", indicates this organization is the person's primary one (typically interpreted as current one)." - }, - "startDate": { - "type": "string", - "description": "The date the person joined this organization." - }, - "title": { - "type": "string", - "description": "The person's job title or role within the organization." - }, - "type": { - "type": "string", - "description": "The type of organization. Possible values are: \n- \"work\" - Work. \n- \"school\" - School." - } - } - } - }, - "placesLived": { - "type": "array", - "description": "A list of places where this person has lived.", - "items": { - "type": "object", - "properties": { - "primary": { - "type": "boolean", - "description": "If \"true\", this place of residence is this person's primary residence." - }, - "value": { - "type": "string", - "description": "A place where this person has lived. For example: \"Seattle, WA\", \"Near Toronto\"." - } - } - } - }, - "relationshipStatus": { - "type": "string", - "description": "The person's relationship status. Possible values are: \n- \"single\" - Person is single. \n- \"in_a_relationship\" - Person is in a relationship. \n- \"engaged\" - Person is engaged. \n- \"married\" - Person is married. \n- \"its_complicated\" - The relationship is complicated. \n- \"open_relationship\" - Person is in an open relationship. \n- \"widowed\" - Person is widowed. \n- \"in_domestic_partnership\" - Person is in a domestic partnership. \n- \"in_civil_union\" - Person is in a civil union." - }, - "tagline": { - "type": "string", - "description": "The brief description (tagline) of this person." - }, - "url": { - "type": "string", - "description": "The URL of this person's profile." - }, - "urls": { - "type": "array", - "description": "A list of URLs for this person.", - "items": { - "type": "object", - "properties": { - "primary": { - "type": "boolean", - "description": "If \"true\", this URL is the person's primary URL." - }, - "type": { - "type": "string", - "description": "The type of URL. Possible values are: \n- \"home\" - URL for home. \n- \"work\" - URL for work. \n- \"blog\" - URL for blog. \n- \"profile\" - URL for profile. \n- \"other\" - Other." - }, - "value": { - "type": "string", - "description": "The URL value." - } - } - } - } - } - }, - "PlusAclentryResource": { - "id": "PlusAclentryResource", - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "The ID of the entry. For entries of type \"person\" or \"circle\", this is the ID of the resource. For other types, this property is not set." - }, - "type": { - "type": "string", - "description": "The type of entry describing to whom access is granted. Possible values are: \n- \"person\" - Access to an individual. \n- \"circle\" - Access to members of a circle. \n- \"myCircles\" - Access to members of all the person's circles. \n- \"extendedCircles\" - Access to members of everyone in a person's circles, plus all of the people in their circles. \n- \"public\" - Access to anyone on the web." - } - } - } - }, - "resources": { - "activities": { - "methods": { - "get": { - "id": "plus.activities.get", - "path": "activities/{activityId}", - "httpMethod": "GET", - "description": "Get an activity.", - "parameters": { - "activityId": { - "type": "string", - "description": "The ID of the activity to get.", - "required": true, - "location": "path" - }, - "alt": { - "type": "string", - "description": "Specifies an alternative representation type.", - "default": "json", - "enum": [ - "json" - ], - "enumDescriptions": [ - "Use JSON format" - ], - "location": "query" - } - }, - "parameterOrder": [ - "activityId" - ], - "response": { - "$ref": "Activity" - }, - "scopes": [ - "https://www.googleapis.com/auth/plus.me" - ] - }, - "list": { - "id": "plus.activities.list", - "path": "people/{userId}/activities/{collection}", - "httpMethod": "GET", - "description": "List all of the activities in the specified collection for a particular user.", - "parameters": { - "alt": { - "type": "string", - "description": "Specifies an alternative representation type.", - "default": "json", - "enum": [ - "json" - ], - "enumDescriptions": [ - "Use JSON format" - ], - "location": "query" - }, - "collection": { - "type": "string", - "description": "The collection of activities to list.", - "required": true, - "enum": [ - "public" - ], - "enumDescriptions": [ - "All public activities created by the specified user." - ], - "location": "path" - }, - "maxResults": { - "type": "integer", - "description": "The maximum number of activities to include in the response, used for paging. For any response, the actual number returned may be less than the specified maxResults.", - "default": "20", - "format": "uint32", - "minimum": "1", - "maximum": "100", - "location": "query" - }, - "pageToken": { - "type": "string", - "description": "The continuation token, used to page through large result sets. To get the next page of results, set this parameter to the value of \"nextPageToken\" from the previous response.", - "location": "query" - }, - "userId": { - "type": "string", - "description": "The ID of the user to get activities for. The special value \"me\" can be used to indicate the authenticated user.", - "required": true, - "pattern": "me|[0-9]+", - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "collection" - ], - "response": { - "$ref": "ActivityFeed" - }, - "scopes": [ - "https://www.googleapis.com/auth/plus.me" - ] - }, - "search": { - "id": "plus.activities.search", - "path": "activities", - "httpMethod": "GET", - "description": "Search public activities.", - "parameters": { - "language": { - "type": "string", - "description": "Specify the preferred language to search with. See Language Codes for available values.", - "default": "", - "location": "query" - }, - "maxResults": { - "type": "integer", - "description": "The maximum number of activities to include in the response, used for paging. For any response, the actual number returned may be less than the specified maxResults.", - "default": "10", - "format": "uint32", - "minimum": "1", - "maximum": "20", - "location": "query" - }, - "orderBy": { - "type": "string", - "description": "Specifies how to order search results.", - "default": "recent", - "enum": [ - "best", - "recent" - ], - "enumDescriptions": [ - "Sort activities by relevance to the user, most relevant first.", - "Sort activities by published date, most recent first." - ], - "location": "query" - }, - "pageToken": { - "type": "string", - "description": "The continuation token, used to page through large result sets. To get the next page of results, set this parameter to the value of \"nextPageToken\" from the previous response. This token may be of any length.", - "location": "query" - }, - "query": { - "type": "string", - "description": "Full-text search query string.", - "required": true, - "location": "query" - } - }, - "parameterOrder": [ - "query" - ], - "response": { - "$ref": "ActivityFeed" - }, - "scopes": [ - "https://www.googleapis.com/auth/plus.me" - ] - } - } - }, - "comments": { - "methods": { - "get": { - "id": "plus.comments.get", - "path": "comments/{commentId}", - "httpMethod": "GET", - "description": "Get a comment.", - "parameters": { - "commentId": { - "type": "string", - "description": "The ID of the comment to get.", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "commentId" - ], - "response": { - "$ref": "Comment" - }, - "scopes": [ - "https://www.googleapis.com/auth/plus.me" - ] - }, - "list": { - "id": "plus.comments.list", - "path": "activities/{activityId}/comments", - "httpMethod": "GET", - "description": "List all of the comments for an activity.", - "parameters": { - "activityId": { - "type": "string", - "description": "The ID of the activity to get comments for.", - "required": true, - "location": "path" - }, - "alt": { - "type": "string", - "description": "Specifies an alternative representation type.", - "default": "json", - "enum": [ - "json" - ], - "enumDescriptions": [ - "Use JSON format" - ], - "location": "query" - }, - "maxResults": { - "type": "integer", - "description": "The maximum number of comments to include in the response, used for paging. For any response, the actual number returned may be less than the specified maxResults.", - "default": "20", - "format": "uint32", - "minimum": "0", - "maximum": "100", - "location": "query" - }, - "pageToken": { - "type": "string", - "description": "The continuation token, used to page through large result sets. To get the next page of results, set this parameter to the value of \"nextPageToken\" from the previous response.", - "location": "query" - } - }, - "parameterOrder": [ - "activityId" - ], - "response": { - "$ref": "CommentFeed" - }, - "scopes": [ - "https://www.googleapis.com/auth/plus.me" - ] - } - } - }, - "people": { - "methods": { - "get": { - "id": "plus.people.get", - "path": "people/{userId}", - "httpMethod": "GET", - "description": "Get a person's profile.", - "parameters": { - "userId": { - "type": "string", - "description": "The ID of the person to get the profile for. The special value \"me\" can be used to indicate the authenticated user.", - "required": true, - "pattern": "me|[0-9]+", - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "response": { - "$ref": "Person" - }, - "scopes": [ - "https://www.googleapis.com/auth/plus.me" - ] - }, - "listByActivity": { - "id": "plus.people.listByActivity", - "path": "activities/{activityId}/people/{collection}", - "httpMethod": "GET", - "description": "List all of the people in the specified collection for a particular activity.", - "parameters": { - "activityId": { - "type": "string", - "description": "The ID of the activity to get the list of people for.", - "required": true, - "location": "path" - }, - "collection": { - "type": "string", - "description": "The collection of people to list.", - "required": true, - "enum": [ - "plusoners", - "resharers" - ], - "enumDescriptions": [ - "List all people who have +1'd this activity.", - "List all people who have reshared this activity." - ], - "location": "path" - }, - "maxResults": { - "type": "integer", - "description": "The maximum number of people to include in the response, used for paging. For any response, the actual number returned may be less than the specified maxResults.", - "default": "20", - "format": "uint32", - "minimum": "1", - "maximum": "100", - "location": "query" - }, - "pageToken": { - "type": "string", - "description": "The continuation token, used to page through large result sets. To get the next page of results, set this parameter to the value of \"nextPageToken\" from the previous response.", - "location": "query" - } - }, - "parameterOrder": [ - "activityId", - "collection" - ], - "response": { - "$ref": "PeopleFeed" - }, - "scopes": [ - "https://www.googleapis.com/auth/plus.me" - ] - }, - "search": { - "id": "plus.people.search", - "path": "people", - "httpMethod": "GET", - "description": "Search all public profiles.", - "parameters": { - "language": { - "type": "string", - "description": "Specify the preferred language to search with. See Language Codes for available values.", - "default": "", - "location": "query" - }, - "maxResults": { - "type": "integer", - "description": "The maximum number of people to include in the response, used for paging. For any response, the actual number returned may be less than the specified maxResults.", - "default": "10", - "format": "uint32", - "minimum": "1", - "maximum": "20", - "location": "query" - }, - "pageToken": { - "type": "string", - "description": "The continuation token, used to page through large result sets. To get the next page of results, set this parameter to the value of \"nextPageToken\" from the previous response. This token may be of any length.", - "location": "query" - }, - "query": { - "type": "string", - "description": "Full-text search query string.", - "required": true, - "location": "query" - } - }, - "parameterOrder": [ - "query" - ], - "response": { - "$ref": "PeopleFeed" - }, - "scopes": [ - "https://www.googleapis.com/auth/plus.me" - ] - } - } - } - } -} diff --git a/tests/data/small.jpg b/tests/data/small.jpg deleted file mode 100644 index 6aa8bbc..0000000 Binary files a/tests/data/small.jpg and /dev/null differ diff --git a/tests/data/small.png b/tests/data/small.png deleted file mode 100644 index 5446bc2..0000000 Binary files a/tests/data/small.png and /dev/null differ diff --git a/tests/data/smiley.png b/tests/data/smiley.png deleted file mode 100644 index ba772db..0000000 Binary files a/tests/data/smiley.png and /dev/null differ diff --git a/tests/data/tasks.json b/tests/data/tasks.json deleted file mode 100644 index ee7be10..0000000 --- a/tests/data/tasks.json +++ /dev/null @@ -1,663 +0,0 @@ -{ - "kind": "discovery#restDescription", - "id": "tasks:v1", - "name": "tasks", - "version": "v1", - "title": "Tasks API", - "description": "Lets you manage your tasks and task lists.", - "icons": { - "x16": "http://www.google.com/images/icons/product/tasks-16.png", - "x32": "http://www.google.com/images/icons/product/tasks-32.png" - }, - "documentationLink": "http://code.google.com/apis/tasks/v1/using.html", - "labels": [ - "labs" - ], - "protocol": "rest", - "basePath": "/tasks/v1/", - "rootUrl": "https://www.googleapis.com/", - "servicePath": "tasks/v1/", - "parameters": { - "alt": { - "type": "string", - "description": "Data format for the response.", - "default": "json", - "enum": [ - "json" - ], - "enumDescriptions": [ - "Responses with Content-Type of application/json" - ], - "location": "query" - }, - "fields": { - "type": "string", - "description": "Selector specifying which fields to include in a partial response.", - "location": "query" - }, - "key": { - "type": "string", - "description": "API key. Your API key identifies your project and provides you with API access, quota, and reports. Required unless you provide an OAuth 2.0 token.", - "location": "query" - }, - "oauth_token": { - "type": "string", - "description": "OAuth 2.0 token for the current user.", - "location": "query" - }, - "prettyPrint": { - "type": "boolean", - "description": "Returns response with indentations and line breaks.", - "default": "true", - "location": "query" - }, - "userIp": { - "type": "string", - "description": "IP address of the site where the request originates. Use this if you want to enforce per-user limits.", - "location": "query" - } - }, - "auth": { - "oauth2": { - "scopes": { - "https://www.googleapis.com/auth/tasks": { - "description": "Manage your tasks" - }, - "https://www.googleapis.com/auth/tasks.readonly": { - "description": "View your tasks" - } - } - } - }, - "schemas": { - "Task": { - "id": "Task", - "type": "object", - "properties": { - "completed": { - "type": "string", - "description": "Completion date of the task (as a RFC 3339 timestamp). This field is omitted if the task has not been completed.", - "format": "date-time" - }, - "deleted": { - "type": "boolean", - "description": "Flag indicating whether the task has been deleted. The default if False." - }, - "due": { - "type": "string", - "description": "Due date of the task (as a RFC 3339 timestamp). Optional.", - "format": "date-time" - }, - "etag": { - "type": "string", - "description": "ETag of the resource." - }, - "hidden": { - "type": "boolean", - "description": "Flag indicating whether the task is hidden. This is the case if the task had been marked completed when the task list was last cleared. The default is False. This field is read-only." - }, - "id": { - "type": "string", - "description": "Task identifier." - }, - "kind": { - "type": "string", - "description": "Type of the resource. This is always \"tasks#task\".", - "default": "tasks#task" - }, - "notes": { - "type": "string", - "description": "Notes describing the task. Optional." - }, - "parent": { - "type": "string", - "description": "Parent task identifier. This field is omitted if it is a top-level task. This field is read-only. Use the \"move\" method to move the task under a different parent or to the top level." - }, - "position": { - "type": "string", - "description": "String indicating the position of the task among its sibling tasks under the same parent task or at the top level. If this string is greater than another task's corresponding position string according to lexicographical ordering, the task is positioned after the other task under the same parent task (or at the top level). This field is read-only. Use the \"move\" method to move the task to another position." - }, - "selfLink": { - "type": "string", - "description": "URL pointing to this task. Used to retrieve, update, or delete this task." - }, - "status": { - "type": "string", - "description": "Status of the task. This is either \"needsAction\" or \"completed\"." - }, - "title": { - "type": "string", - "description": "Title of the task." - }, - "updated": { - "type": "string", - "description": "Last modification time of the task (as a RFC 3339 timestamp).", - "format": "date-time" - } - } - }, - "TaskList": { - "id": "TaskList", - "type": "object", - "properties": { - "etag": { - "type": "string", - "description": "ETag of the resource." - }, - "id": { - "type": "string", - "description": "Task list identifier." - }, - "kind": { - "type": "string", - "description": "Type of the resource. This is always \"tasks#taskList\".", - "default": "tasks#taskList" - }, - "selfLink": { - "type": "string", - "description": "URL pointing to this task list. Used to retrieve, update, or delete this task list." - }, - "title": { - "type": "string", - "description": "Title of the task list." - } - } - }, - "TaskLists": { - "id": "TaskLists", - "type": "object", - "properties": { - "etag": { - "type": "string", - "description": "ETag of the resource." - }, - "items": { - "type": "array", - "description": "Collection of task lists.", - "items": { - "$ref": "TaskList" - } - }, - "kind": { - "type": "string", - "description": "Type of the resource. This is always \"tasks#taskLists\".", - "default": "tasks#taskLists" - }, - "nextPageToken": { - "type": "string", - "description": "Token that can be used to request the next page of this result." - } - } - }, - "Tasks": { - "id": "Tasks", - "type": "object", - "properties": { - "etag": { - "type": "string", - "description": "ETag of the resource." - }, - "items": { - "type": "array", - "description": "Collection of tasks.", - "items": { - "$ref": "Task" - } - }, - "kind": { - "type": "string", - "description": "Type of the resource. This is always \"tasks#tasks\".", - "default": "tasks#tasks" - }, - "nextPageToken": { - "type": "string", - "description": "Token used to access the next page of this result." - } - } - } - }, - "resources": { - "tasklists": { - "methods": { - "delete": { - "id": "tasks.tasklists.delete", - "path": "users/@me/lists/{tasklist}", - "httpMethod": "DELETE", - "description": "Deletes the authenticated user's specified task list.", - "parameters": { - "tasklist": { - "type": "string", - "description": "Task list identifier.", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "tasklist" - ], - "scopes": [ - "https://www.googleapis.com/auth/tasks" - ] - }, - "get": { - "id": "tasks.tasklists.get", - "path": "users/@me/lists/{tasklist}", - "httpMethod": "GET", - "description": "Returns the authenticated user's specified task list.", - "parameters": { - "tasklist": { - "type": "string", - "description": "Task list identifier.", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "tasklist" - ], - "response": { - "$ref": "TaskList" - }, - "scopes": [ - "https://www.googleapis.com/auth/tasks", - "https://www.googleapis.com/auth/tasks.readonly" - ] - }, - "insert": { - "id": "tasks.tasklists.insert", - "path": "users/@me/lists", - "httpMethod": "POST", - "description": "Creates a new task list and adds it to the authenticated user's task lists.", - "request": { - "$ref": "TaskList" - }, - "response": { - "$ref": "TaskList" - }, - "scopes": [ - "https://www.googleapis.com/auth/tasks" - ] - }, - "list": { - "id": "tasks.tasklists.list", - "path": "users/@me/lists", - "httpMethod": "GET", - "description": "Returns all the authenticated user's task lists.", - "parameters": { - "maxResults": { - "type": "integer", - "description": "Maximum number of task lists returned on one page. Optional. The default is 100.", - "minimum": "-9223372036854775808", - "maximum": "9223372036854775807", - "location": "query" - }, - "pageToken": { - "type": "string", - "description": "Token specifying the result page to return. Optional.", - "location": "query" - } - }, - "response": { - "$ref": "TaskLists" - }, - "scopes": [ - "https://www.googleapis.com/auth/tasks", - "https://www.googleapis.com/auth/tasks.readonly" - ] - }, - "patch": { - "id": "tasks.tasklists.patch", - "path": "users/@me/lists/{tasklist}", - "httpMethod": "PATCH", - "description": "Updates the authenticated user's specified task list. This method supports patch semantics.", - "parameters": { - "tasklist": { - "type": "string", - "description": "Task list identifier.", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "tasklist" - ], - "request": { - "$ref": "TaskList" - }, - "response": { - "$ref": "TaskList" - }, - "scopes": [ - "https://www.googleapis.com/auth/tasks" - ] - }, - "update": { - "id": "tasks.tasklists.update", - "path": "users/@me/lists/{tasklist}", - "httpMethod": "PUT", - "description": "Updates the authenticated user's specified task list.", - "parameters": { - "tasklist": { - "type": "string", - "description": "Task list identifier.", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "tasklist" - ], - "request": { - "$ref": "TaskList" - }, - "response": { - "$ref": "TaskList" - }, - "scopes": [ - "https://www.googleapis.com/auth/tasks" - ] - } - } - }, - "tasks": { - "methods": { - "clear": { - "id": "tasks.tasks.clear", - "path": "lists/{tasklist}/clear", - "httpMethod": "POST", - "description": "Clears all completed tasks from the specified task list. The affected tasks will be marked as 'hidden' and no longer be returned by default when retrieving all tasks for a task list.", - "parameters": { - "tasklist": { - "type": "string", - "description": "Task list identifier.", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "tasklist" - ], - "scopes": [ - "https://www.googleapis.com/auth/tasks" - ] - }, - "delete": { - "id": "tasks.tasks.delete", - "path": "lists/{tasklist}/tasks/{task}", - "httpMethod": "DELETE", - "description": "Deletes the specified task from the task list.", - "parameters": { - "task": { - "type": "string", - "description": "Task identifier.", - "required": true, - "location": "path" - }, - "tasklist": { - "type": "string", - "description": "Task list identifier.", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "tasklist", - "task" - ], - "scopes": [ - "https://www.googleapis.com/auth/tasks" - ] - }, - "get": { - "id": "tasks.tasks.get", - "path": "lists/{tasklist}/tasks/{task}", - "httpMethod": "GET", - "description": "Returns the specified task.", - "parameters": { - "task": { - "type": "string", - "description": "Task identifier.", - "required": true, - "location": "path" - }, - "tasklist": { - "type": "string", - "description": "Task list identifier.", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "tasklist", - "task" - ], - "response": { - "$ref": "Task" - }, - "scopes": [ - "https://www.googleapis.com/auth/tasks", - "https://www.googleapis.com/auth/tasks.readonly" - ] - }, - "insert": { - "id": "tasks.tasks.insert", - "path": "lists/{tasklist}/tasks", - "httpMethod": "POST", - "description": "Creates a new task on the specified task list.", - "parameters": { - "parent": { - "type": "string", - "description": "Parent task identifier. If the task is created at the top level, this parameter is omitted. Optional.", - "location": "query" - }, - "previous": { - "type": "string", - "description": "Previous sibling task identifier. If the task is created at the first position among its siblings, this parameter is omitted. Optional.", - "location": "query" - }, - "tasklist": { - "type": "string", - "description": "Task list identifier.", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "tasklist" - ], - "request": { - "$ref": "Task" - }, - "response": { - "$ref": "Task" - }, - "scopes": [ - "https://www.googleapis.com/auth/tasks" - ] - }, - "list": { - "id": "tasks.tasks.list", - "path": "lists/{tasklist}/tasks", - "httpMethod": "GET", - "description": "Returns all tasks in the specified task list.", - "parameters": { - "completedMax": { - "type": "string", - "description": "Upper bound for a task's completion date (as a RFC 3339 timestamp) to filter by. Optional. The default is not to filter by completion date.", - "location": "query" - }, - "completedMin": { - "type": "string", - "description": "Lower bound for a task's completion date (as a RFC 3339 timestamp) to filter by. Optional. The default is not to filter by completion date.", - "location": "query" - }, - "dueMax": { - "type": "string", - "description": "Upper bound for a task's due date (as a RFC 3339 timestamp) to filter by. Optional. The default is not to filter by due date.", - "location": "query" - }, - "dueMin": { - "type": "string", - "description": "Lower bound for a task's due date (as a RFC 3339 timestamp) to filter by. Optional. The default is not to filter by due date.", - "location": "query" - }, - "maxResults": { - "type": "integer", - "description": "Maximum number of task lists returned on one page. Optional. The default is 100.", - "minimum": "-9223372036854775808", - "maximum": "9223372036854775807", - "location": "query" - }, - "pageToken": { - "type": "string", - "description": "Token specifying the result page to return. Optional.", - "location": "query" - }, - "showCompleted": { - "type": "boolean", - "description": "Flag indicating whether completed tasks are returned in the result. Optional. The default is True.", - "location": "query" - }, - "showDeleted": { - "type": "boolean", - "description": "Flag indicating whether deleted tasks are returned in the result. Optional. The default is False.", - "location": "query" - }, - "showHidden": { - "type": "boolean", - "description": "Flag indicating whether hidden tasks are returned in the result. Optional. The default is False.", - "location": "query" - }, - "tasklist": { - "type": "string", - "description": "Task list identifier.", - "required": true, - "location": "path" - }, - "updatedMin": { - "type": "string", - "description": "Lower bound for a task's last modification time (as a RFC 3339 timestamp) to filter by. Optional. The default is not to filter by last modification time.", - "location": "query" - } - }, - "parameterOrder": [ - "tasklist" - ], - "response": { - "$ref": "Tasks" - }, - "scopes": [ - "https://www.googleapis.com/auth/tasks", - "https://www.googleapis.com/auth/tasks.readonly" - ] - }, - "move": { - "id": "tasks.tasks.move", - "path": "lists/{tasklist}/tasks/{task}/move", - "httpMethod": "POST", - "description": "Moves the specified task to another position in the task list. This can include putting it as a child task under a new parent and/or move it to a different position among its sibling tasks.", - "parameters": { - "parent": { - "type": "string", - "description": "New parent task identifier. If the task is moved to the top level, this parameter is omitted. Optional.", - "location": "query" - }, - "previous": { - "type": "string", - "description": "New previous sibling task identifier. If the task is moved to the first position among its siblings, this parameter is omitted. Optional.", - "location": "query" - }, - "task": { - "type": "string", - "description": "Task identifier.", - "required": true, - "location": "path" - }, - "tasklist": { - "type": "string", - "description": "Task list identifier.", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "tasklist", - "task" - ], - "response": { - "$ref": "Task" - }, - "scopes": [ - "https://www.googleapis.com/auth/tasks" - ] - }, - "patch": { - "id": "tasks.tasks.patch", - "path": "lists/{tasklist}/tasks/{task}", - "httpMethod": "PATCH", - "description": "Updates the specified task. This method supports patch semantics.", - "parameters": { - "task": { - "type": "string", - "description": "Task identifier.", - "required": true, - "location": "path" - }, - "tasklist": { - "type": "string", - "description": "Task list identifier.", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "tasklist", - "task" - ], - "request": { - "$ref": "Task" - }, - "response": { - "$ref": "Task" - }, - "scopes": [ - "https://www.googleapis.com/auth/tasks" - ] - }, - "update": { - "id": "tasks.tasks.update", - "path": "lists/{tasklist}/tasks/{task}", - "httpMethod": "PUT", - "description": "Updates the specified task.", - "parameters": { - "task": { - "type": "string", - "description": "Task identifier.", - "required": true, - "location": "path" - }, - "tasklist": { - "type": "string", - "description": "Task list identifier.", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "tasklist", - "task" - ], - "request": { - "$ref": "Task" - }, - "response": { - "$ref": "Task" - }, - "scopes": [ - "https://www.googleapis.com/auth/tasks" - ] - } - } - } - } -} diff --git a/tests/data/zoo.json b/tests/data/zoo.json deleted file mode 100644 index 38cb686..0000000 --- a/tests/data/zoo.json +++ /dev/null @@ -1,584 +0,0 @@ -{ - "kind": "discovery#describeItem", - "name": "zoo", - "version": "v1", - "description": "Zoo API used for testing", - "basePath": "/zoo/", - "rootUrl": "https://www.googleapis.com/", - "servicePath": "zoo/v1/", - "rpcPath": "/rpc", - "parameters": { - "alt": { - "type": "string", - "description": "Data format for the response.", - "default": "json", - "enum": [ - "json" - ], - "enumDescriptions": [ - "Responses with Content-Type of application/json" - ], - "location": "query" - }, - "fields": { - "type": "string", - "description": "Selector specifying which fields to include in a partial response.", - "location": "query" - }, - "key": { - "type": "string", - "description": "API key. Your API key identifies your project and provides you with API access, quota, and reports. Required unless you provide an OAuth 2.0 token.", - "location": "query" - }, - "oauth_token": { - "type": "string", - "description": "OAuth 2.0 token for the current user.", - "location": "query" - }, - "prettyPrint": { - "type": "boolean", - "description": "Returns response with indentations and line breaks.", - "default": "true", - "location": "query" - }, - "quotaUser": { - "type": "string", - "description": "Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. Overrides userIp if both are provided.", - "location": "query" - }, - "userIp": { - "type": "string", - "description": "IP address of the site where the request originates. Use this if you want to enforce per-user limits.", - "location": "query" - } - }, - "features": [ - "dataWrapper" - ], - "schemas": { - "Animal": { - "id": "Animal", - "type": "object", - "properties": { - "etag": { - "type": "string" - }, - "kind": { - "type": "string", - "default": "zoo#animal" - }, - "name": { - "type": "string" - }, - "photo": { - "type": "object", - "properties": { - "filename": { - "type": "string" - }, - "hash": { - "type": "string" - }, - "hashAlgorithm": { - "type": "string" - }, - "size": { - "type": "integer" - }, - "type": { - "type": "string" - } - } - } - } - }, - "Animal2": { - "id": "Animal2", - "type": "object", - "properties": { - "kind": { - "type": "string", - "default": "zoo#animal" - }, - "name": { - "type": "string" - } - } - }, - "AnimalFeed": { - "id": "AnimalFeed", - "type": "object", - "properties": { - "etag": { - "type": "string" - }, - "items": { - "type": "array", - "items": { - "$ref": "Animal" - } - }, - "kind": { - "type": "string", - "default": "zoo#animalFeed" - } - } - }, - "AnimalMap": { - "id": "AnimalMap", - "type": "object", - "properties": { - "etag": { - "type": "string" - }, - "animals": { - "type": "object", - "description": "Map of animal id to animal data", - "additionalProperties": { - "$ref": "Animal" - } - }, - "kind": { - "type": "string", - "default": "zoo#animalMap" - } - } - }, - "LoadFeed": { - "id": "LoadFeed", - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "type": "object", - "properties": { - "doubleVal": { - "type": "number" - }, - "nullVal": { - "type": "null" - }, - "booleanVal": { - "type": "boolean", - "description": "True or False." - }, - "anyVal": { - "type": "any", - "description": "Anything will do." - }, - "enumVal": { - "type": "string" - }, - "kind": { - "type": "string", - "default": "zoo#loadValue" - }, - "longVal": { - "type": "integer" - }, - "stringVal": { - "type": "string" - } - } - } - }, - "kind": { - "type": "string", - "default": "zoo#loadFeed" - } - } - } - }, - "methods": { - "query": { - "path": "query", - "id": "bigquery.query", - "httpMethod": "GET", - "parameters": { - "q": { - "type": "string", - "location": "query", - "required": false, - "repeated": false - }, - "i": { - "type": "integer", - "location": "query", - "required": false, - "repeated": false, - "minimum": "0", - "maximum": "4294967295", - "default": "20" - }, - "n": { - "type": "number", - "location": "query", - "required": false, - "repeated": false - }, - "b": { - "type": "boolean", - "location": "query", - "required": false, - "repeated": false - }, - "a": { - "type": "any", - "location": "query", - "required": false, - "repeated": false - }, - "o": { - "type": "object", - "location": "query", - "required": false, - "repeated": false - }, - "e": { - "type": "string", - "location": "query", - "required": false, - "repeated": false, - "enum": [ - "foo", - "bar" - ] - }, - "er": { - "type": "string", - "location": "query", - "required": false, - "repeated": true, - "enum": [ - "one", - "two", - "three" - ] - }, - "rr": { - "type": "string", - "location": "query", - "required": false, - "repeated": true, - "pattern": "[a-z]+" - } - } - } - }, - "resources": { - "my": { - "resources": { - "favorites": { - "methods": { - "list": { - "path": "favorites/@me/mine", - "id": "zoo.animals.mine", - "httpMethod": "GET", - "parameters": { - "max-results": { - "location": "query", - "required": false - } - } - } - } - } - } - }, - "global": { - "resources": { - "print": { - "methods": { - "assert": { - "path": "global/print/assert", - "id": "zoo.animals.mine", - "httpMethod": "GET", - "parameters": { - "max-results": { - "location": "query", - "required": false - } - } - } - } - } - } - }, - "animals": { - "methods": { - "crossbreed": { - "path": "animals/crossbreed", - "id": "zoo.animals.crossbreed", - "httpMethod": "POST", - "description": "Cross-breed animals", - "response": { - "$ref": "Animal2" - }, - "mediaUpload": { - "accept": [ - "image/png" - ], - "protocols": { - "simple": { - "multipart": true, - "path": "upload/activities/{userId}/@self" - }, - "resumable": { - "multipart": true, - "path": "upload/activities/{userId}/@self" - } - } - } - }, - "delete": { - "path": "animals/{name}", - "id": "zoo.animals.delete", - "httpMethod": "DELETE", - "description": "Delete animals", - "parameters": { - "name": { - "location": "path", - "required": true, - "description": "Name of the animal to delete", - "type": "string" - } - }, - "parameterOrder": [ - "name" - ] - }, - "get": { - "path": "animals/{name}", - "id": "zoo.animals.get", - "httpMethod": "GET", - "description": "Get animals", - "supportsMediaDownload": true, - "parameters": { - "name": { - "location": "path", - "required": true, - "description": "Name of the animal to load", - "type": "string" - }, - "projection": { - "location": "query", - "type": "string", - "enum": [ - "full" - ], - "enumDescriptions": [ - "Include everything" - ] - } - }, - "parameterOrder": [ - "name" - ], - "response": { - "$ref": "Animal" - } - }, - "getmedia": { - "path": "animals/{name}", - "id": "zoo.animals.get", - "httpMethod": "GET", - "description": "Get animals", - "parameters": { - "name": { - "location": "path", - "required": true, - "description": "Name of the animal to load", - "type": "string" - }, - "projection": { - "location": "query", - "type": "string", - "enum": [ - "full" - ], - "enumDescriptions": [ - "Include everything" - ] - } - }, - "parameterOrder": [ - "name" - ] - }, - "insert": { - "path": "animals", - "id": "zoo.animals.insert", - "httpMethod": "POST", - "description": "Insert animals", - "request": { - "$ref": "Animal" - }, - "response": { - "$ref": "Animal" - }, - "mediaUpload": { - "accept": [ - "image/png" - ], - "maxSize": "1KB", - "protocols": { - "simple": { - "multipart": true, - "path": "upload/activities/{userId}/@self" - }, - "resumable": { - "multipart": true, - "path": "upload/activities/{userId}/@self" - } - } - } - }, - "list": { - "path": "animals", - "id": "zoo.animals.list", - "httpMethod": "GET", - "description": "List animals", - "parameters": { - "max-results": { - "location": "query", - "description": "Maximum number of results to return", - "type": "integer", - "minimum": "0" - }, - "name": { - "location": "query", - "description": "Restrict result to animals with this name", - "type": "string" - }, - "projection": { - "location": "query", - "type": "string", - "enum": [ - "full" - ], - "enumDescriptions": [ - "Include absolutely everything" - ] - }, - "start-token": { - "location": "query", - "description": "Pagination token", - "type": "string" - } - }, - "response": { - "$ref": "AnimalFeed" - } - }, - "patch": { - "path": "animals/{name}", - "id": "zoo.animals.patch", - "httpMethod": "PATCH", - "description": "Update animals", - "parameters": { - "name": { - "location": "path", - "required": true, - "description": "Name of the animal to update", - "type": "string" - } - }, - "parameterOrder": [ - "name" - ], - "request": { - "$ref": "Animal" - }, - "response": { - "$ref": "Animal" - } - }, - "update": { - "path": "animals/{name}", - "id": "zoo.animals.update", - "httpMethod": "PUT", - "description": "Update animals", - "parameters": { - "name": { - "location": "path", - "description": "Name of the animal to update", - "type": "string" - } - }, - "parameterOrder": [ - "name" - ], - "request": { - "$ref": "Animal" - }, - "response": { - "$ref": "Animal" - } - } - } - }, - "load": { - "methods": { - "list": { - "path": "load", - "id": "zoo.load.list", - "httpMethod": "GET", - "response": { - "$ref": "LoadFeed" - } - } - } - }, - "loadNoTemplate": { - "methods": { - "list": { - "path": "loadNoTemplate", - "id": "zoo.loadNoTemplate.list", - "httpMethod": "GET" - } - } - }, - "scopedAnimals": { - "methods": { - "list": { - "path": "scopedanimals", - "id": "zoo.scopedAnimals.list", - "httpMethod": "GET", - "description": "List animals (scoped)", - "parameters": { - "max-results": { - "location": "query", - "description": "Maximum number of results to return", - "type": "integer", - "minimum": "0" - }, - "name": { - "location": "query", - "description": "Restrict result to animals with this name", - "type": "string" - }, - "projection": { - "location": "query", - "type": "string", - "enum": [ - "full" - ], - "enumDescriptions": [ - "Include absolutely everything" - ] - }, - "start-token": { - "location": "query", - "description": "Pagination token", - "type": "string" - } - }, - "response": { - "$ref": "AnimalFeed" - } - } - } - } - } -} diff --git a/tests/http_mock.py b/tests/http_mock.py new file mode 100644 index 0000000..cbf35a9 --- /dev/null +++ b/tests/http_mock.py @@ -0,0 +1,112 @@ +# Copyright (C) 2012 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Copy of googleapiclient.http's mock functionality.""" + +import httplib2 + +from oauth2client.anyjson import simplejson + +# TODO(craigcitro): Find a cleaner way to share this code with googleapiclient. + + +class HttpMock(object): + """Mock of httplib2.Http""" + + def __init__(self, filename=None, headers=None): + """ + Args: + filename: string, absolute filename to read response from + headers: dict, header to return with response + """ + if headers is None: + headers = {'status': '200 OK'} + if filename: + f = file(filename, 'r') + self.data = f.read() + f.close() + else: + self.data = None + self.response_headers = headers + self.headers = None + self.uri = None + self.method = None + self.body = None + self.headers = None + + + def request(self, uri, + method='GET', + body=None, + headers=None, + redirections=1, + connection_type=None): + self.uri = uri + self.method = method + self.body = body + self.headers = headers + return httplib2.Response(self.response_headers), self.data + + +class HttpMockSequence(object): + """Mock of httplib2.Http + + Mocks a sequence of calls to request returning different responses for each + call. Create an instance initialized with the desired response headers + and content and then use as if an httplib2.Http instance. + + http = HttpMockSequence([ + ({'status': '401'}, ''), + ({'status': '200'}, '{"access_token":"1/3w","expires_in":3600}'), + ({'status': '200'}, 'echo_request_headers'), + ]) + resp, content = http.request("http://examples.com") + + There are special values you can pass in for content to trigger + behavours that are helpful in testing. + + 'echo_request_headers' means return the request headers in the response body + 'echo_request_headers_as_json' means return the request headers in + the response body + 'echo_request_body' means return the request body in the response body + 'echo_request_uri' means return the request uri in the response body + """ + + def __init__(self, iterable): + """ + Args: + iterable: iterable, a sequence of pairs of (headers, body) + """ + self._iterable = iterable + self.follow_redirects = True + + def request(self, uri, + method='GET', + body=None, + headers=None, + redirections=1, + connection_type=None): + resp, content = self._iterable.pop(0) + if content == 'echo_request_headers': + content = headers + elif content == 'echo_request_headers_as_json': + content = simplejson.dumps(headers) + elif content == 'echo_request_body': + if hasattr(body, 'read'): + content = body.read() + else: + content = body + elif content == 'echo_request_uri': + content = uri + return httplib2.Response(resp), content diff --git a/tests/test_oauth2client_appengine.py b/tests/test_appengine.py similarity index 99% rename from tests/test_oauth2client_appengine.py rename to tests/test_appengine.py index 2c3ce75..8a3a9ee 100644 --- a/tests/test_oauth2client_appengine.py +++ b/tests/test_appengine.py @@ -40,7 +40,6 @@ import dev_appserver dev_appserver.fix_sys_path() import webapp2 -from googleapiclient.http import HttpMockSequence from google.appengine.api import apiproxy_stub from google.appengine.api import apiproxy_stub_map from google.appengine.api import app_identity @@ -51,6 +50,7 @@ from google.appengine.ext import db from google.appengine.ext import ndb from google.appengine.ext import testbed from google.appengine.runtime import apiproxy_errors +from http_mock import HttpMockSequence from oauth2client import appengine from oauth2client import GOOGLE_TOKEN_URI from oauth2client.anyjson import simplejson diff --git a/tests/test_channel.py b/tests/test_channel.py deleted file mode 100644 index 0e348a5..0000000 --- a/tests/test_channel.py +++ /dev/null @@ -1,124 +0,0 @@ -"""Notification channels tests.""" - -__author__ = 'jcgregorio@google.com (Joe Gregorio)' - -import unittest -import datetime - -from googleapiclient import channel -from googleapiclient import errors - - -class TestChannel(unittest.TestCase): - def test_basic(self): - ch = channel.Channel('web_hook', 'myid', 'mytoken', - 'http://example.org/callback', - expiration=0, - params={'extra': 'info'}, - resource_id='the_resource_id', - resource_uri='http://example.com/resource_1') - - # Converting to a body. - body = ch.body() - self.assertEqual('http://example.org/callback', body['address']) - self.assertEqual('myid', body['id']) - self.assertEqual('missing', body.get('expiration', 'missing')) - self.assertEqual('info', body['params']['extra']) - self.assertEqual('the_resource_id', body['resourceId']) - self.assertEqual('http://example.com/resource_1', body['resourceUri']) - self.assertEqual('web_hook', body['type']) - - # Converting to a body with expiration set. - ch.expiration = 1 - body = ch.body() - self.assertEqual(1, body.get('expiration', 'missing')) - - # Converting to a body after updating with a response body. - ch.update({ - 'resourceId': 'updated_res_id', - 'resourceUri': 'updated_res_uri', - 'some_random_parameter': 2, - }) - - body = ch.body() - self.assertEqual('http://example.org/callback', body['address']) - self.assertEqual('myid', body['id']) - self.assertEqual(1, body.get('expiration', 'missing')) - self.assertEqual('info', body['params']['extra']) - self.assertEqual('updated_res_id', body['resourceId']) - self.assertEqual('updated_res_uri', body['resourceUri']) - self.assertEqual('web_hook', body['type']) - - def test_new_webhook_channel(self): - ch = channel.new_webhook_channel('http://example.com/callback') - self.assertEqual(0, ch.expiration) - self.assertEqual('http://example.com/callback', ch.address) - self.assertEqual(None, ch.params) - - # New channel with an obviously wrong expiration time. - ch = channel.new_webhook_channel( - 'http://example.com/callback', - expiration=datetime.datetime(1965, 1, 1)) - self.assertEqual(0, ch.expiration) - - # New channel with an expiration time. - ch = channel.new_webhook_channel( - 'http://example.com/callback', - expiration=datetime.datetime(1970, 1, 1, second=5)) - self.assertEqual(5000, ch.expiration) - self.assertEqual('http://example.com/callback', ch.address) - self.assertEqual(None, ch.params) - - # New channel with an expiration time and params. - ch = channel.new_webhook_channel( - 'http://example.com/callback', - expiration=datetime.datetime(1970, 1, 1, second=5, microsecond=1000), - params={'some':'stuff'}) - self.assertEqual(5001, ch.expiration) - self.assertEqual('http://example.com/callback', ch.address) - self.assertEqual({'some': 'stuff'}, ch.params) - - -class TestNotification(unittest.TestCase): - def test_basic(self): - n = channel.Notification(12, 'sync', 'http://example.org', - 'http://example.org/v1') - - self.assertEqual(12, n.message_number) - self.assertEqual('sync', n.state) - self.assertEqual('http://example.org', n.resource_uri) - self.assertEqual('http://example.org/v1', n.resource_id) - - def test_notification_from_headers(self): - headers = { - 'X-GoOG-CHANNEL-ID': 'myid', - 'X-Goog-MESSAGE-NUMBER': '1', - 'X-Goog-rESOURCE-STATE': 'sync', - 'X-Goog-reSOURCE-URI': 'http://example.com/', - 'X-Goog-resOURCE-ID': 'http://example.com/resource_1', - } - - ch = channel.Channel('web_hook', 'myid', 'mytoken', - 'http://example.org/callback', - expiration=0, - params={'extra': 'info'}, - resource_id='the_resource_id', - resource_uri='http://example.com/resource_1') - - # Good test case. - n = channel.notification_from_headers(ch, headers) - self.assertEqual('http://example.com/resource_1', n.resource_id) - self.assertEqual('http://example.com/', n.resource_uri) - self.assertEqual('sync', n.state) - self.assertEqual(1, n.message_number) - - # Detect id mismatch. - ch.id = 'different_id' - try: - n = channel.notification_from_headers(ch, headers) - self.fail('Should have raised exception') - except errors.InvalidNotificationError: - pass - - # Set the id back to a correct value. - ch.id = 'myid' diff --git a/tests/test_oauth2client_clientsecrets.py b/tests/test_clientsecrets.py similarity index 100% rename from tests/test_oauth2client_clientsecrets.py rename to tests/test_clientsecrets.py diff --git a/tests/test_discovery.py b/tests/test_discovery.py deleted file mode 100644 index 3d30d8c..0000000 --- a/tests/test_discovery.py +++ /dev/null @@ -1,1176 +0,0 @@ -#!/usr/bin/python2.4 -# -*- coding: utf-8 -*- -# -# Copyright 2010 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -"""Discovery document tests - -Unit tests for objects created from discovery documents. -""" - -__author__ = 'jcgregorio@google.com (Joe Gregorio)' - -import copy -import datetime -import httplib2 -import os -import pickle -import sys -import unittest -import urlparse -import StringIO - - -try: - from urlparse import parse_qs -except ImportError: - from cgi import parse_qs - - -from googleapiclient.discovery import _fix_up_media_upload -from googleapiclient.discovery import _fix_up_method_description -from googleapiclient.discovery import _fix_up_parameters -from googleapiclient.discovery import build -from googleapiclient.discovery import build_from_document -from googleapiclient.discovery import DISCOVERY_URI -from googleapiclient.discovery import key2param -from googleapiclient.discovery import MEDIA_BODY_PARAMETER_DEFAULT_VALUE -from googleapiclient.discovery import ResourceMethodParameters -from googleapiclient.discovery import STACK_QUERY_PARAMETERS -from googleapiclient.discovery import STACK_QUERY_PARAMETER_DEFAULT_VALUE -from googleapiclient.errors import HttpError -from googleapiclient.errors import InvalidJsonError -from googleapiclient.errors import MediaUploadSizeError -from googleapiclient.errors import ResumableUploadError -from googleapiclient.errors import UnacceptableMimeTypeError -from googleapiclient.http import HttpMock -from googleapiclient.http import HttpMockSequence -from googleapiclient.http import MediaFileUpload -from googleapiclient.http import MediaIoBaseUpload -from googleapiclient.http import MediaUpload -from googleapiclient.http import MediaUploadProgress -from googleapiclient.http import tunnel_patch -from oauth2client import GOOGLE_TOKEN_URI -from oauth2client import util -from oauth2client.anyjson import simplejson -from oauth2client.client import OAuth2Credentials - -import uritemplate - - -DATA_DIR = os.path.join(os.path.dirname(__file__), 'data') - -util.positional_parameters_enforcement = util.POSITIONAL_EXCEPTION - - -def assertUrisEqual(testcase, expected, actual): - """Test that URIs are the same, up to reordering of query parameters.""" - expected = urlparse.urlparse(expected) - actual = urlparse.urlparse(actual) - testcase.assertEqual(expected.scheme, actual.scheme) - testcase.assertEqual(expected.netloc, actual.netloc) - testcase.assertEqual(expected.path, actual.path) - testcase.assertEqual(expected.params, actual.params) - testcase.assertEqual(expected.fragment, actual.fragment) - expected_query = parse_qs(expected.query) - actual_query = parse_qs(actual.query) - for name in expected_query.keys(): - testcase.assertEqual(expected_query[name], actual_query[name]) - for name in actual_query.keys(): - testcase.assertEqual(expected_query[name], actual_query[name]) - - -def datafile(filename): - return os.path.join(DATA_DIR, filename) - - -class SetupHttplib2(unittest.TestCase): - - def test_retries(self): - # Merely loading googleapiclient.discovery should set the RETRIES to 1. - self.assertEqual(1, httplib2.RETRIES) - - -class Utilities(unittest.TestCase): - - def setUp(self): - with open(datafile('zoo.json'), 'r') as fh: - self.zoo_root_desc = simplejson.loads(fh.read()) - self.zoo_get_method_desc = self.zoo_root_desc['methods']['query'] - self.zoo_animals_resource = self.zoo_root_desc['resources']['animals'] - self.zoo_insert_method_desc = self.zoo_animals_resource['methods']['insert'] - - def test_key2param(self): - self.assertEqual('max_results', key2param('max-results')) - self.assertEqual('x007_bond', key2param('007-bond')) - - def _base_fix_up_parameters_test(self, method_desc, http_method, root_desc): - self.assertEqual(method_desc['httpMethod'], http_method) - - method_desc_copy = copy.deepcopy(method_desc) - self.assertEqual(method_desc, method_desc_copy) - - parameters = _fix_up_parameters(method_desc_copy, root_desc, http_method) - - self.assertNotEqual(method_desc, method_desc_copy) - - for param_name in STACK_QUERY_PARAMETERS: - self.assertEqual(STACK_QUERY_PARAMETER_DEFAULT_VALUE, - parameters[param_name]) - - for param_name, value in root_desc.get('parameters', {}).iteritems(): - self.assertEqual(value, parameters[param_name]) - - return parameters - - def test_fix_up_parameters_get(self): - parameters = self._base_fix_up_parameters_test(self.zoo_get_method_desc, - 'GET', self.zoo_root_desc) - # Since http_method is 'GET' - self.assertFalse(parameters.has_key('body')) - - def test_fix_up_parameters_insert(self): - parameters = self._base_fix_up_parameters_test(self.zoo_insert_method_desc, - 'POST', self.zoo_root_desc) - body = { - 'description': 'The request body.', - 'type': 'object', - 'required': True, - '$ref': 'Animal', - } - self.assertEqual(parameters['body'], body) - - def test_fix_up_parameters_check_body(self): - dummy_root_desc = {} - no_payload_http_method = 'DELETE' - with_payload_http_method = 'PUT' - - invalid_method_desc = {'response': 'Who cares'} - valid_method_desc = {'request': {'key1': 'value1', 'key2': 'value2'}} - - parameters = _fix_up_parameters(invalid_method_desc, dummy_root_desc, - no_payload_http_method) - self.assertFalse(parameters.has_key('body')) - - parameters = _fix_up_parameters(valid_method_desc, dummy_root_desc, - no_payload_http_method) - self.assertFalse(parameters.has_key('body')) - - parameters = _fix_up_parameters(invalid_method_desc, dummy_root_desc, - with_payload_http_method) - self.assertFalse(parameters.has_key('body')) - - parameters = _fix_up_parameters(valid_method_desc, dummy_root_desc, - with_payload_http_method) - body = { - 'description': 'The request body.', - 'type': 'object', - 'required': True, - 'key1': 'value1', - 'key2': 'value2', - } - self.assertEqual(parameters['body'], body) - - def _base_fix_up_method_description_test( - self, method_desc, initial_parameters, final_parameters, - final_accept, final_max_size, final_media_path_url): - fake_root_desc = {'rootUrl': 'http://root/', - 'servicePath': 'fake/'} - fake_path_url = 'fake-path/' - - accept, max_size, media_path_url = _fix_up_media_upload( - method_desc, fake_root_desc, fake_path_url, initial_parameters) - self.assertEqual(accept, final_accept) - self.assertEqual(max_size, final_max_size) - self.assertEqual(media_path_url, final_media_path_url) - self.assertEqual(initial_parameters, final_parameters) - - def test_fix_up_media_upload_no_initial_invalid(self): - invalid_method_desc = {'response': 'Who cares'} - self._base_fix_up_method_description_test(invalid_method_desc, {}, {}, - [], 0, None) - - def test_fix_up_media_upload_no_initial_valid_minimal(self): - valid_method_desc = {'mediaUpload': {'accept': []}} - final_parameters = {'media_body': MEDIA_BODY_PARAMETER_DEFAULT_VALUE} - self._base_fix_up_method_description_test( - valid_method_desc, {}, final_parameters, [], 0, - 'http://root/upload/fake/fake-path/') - - def test_fix_up_media_upload_no_initial_valid_full(self): - valid_method_desc = {'mediaUpload': {'accept': ['*/*'], 'maxSize': '10GB'}} - final_parameters = {'media_body': MEDIA_BODY_PARAMETER_DEFAULT_VALUE} - ten_gb = 10 * 2**30 - self._base_fix_up_method_description_test( - valid_method_desc, {}, final_parameters, ['*/*'], - ten_gb, 'http://root/upload/fake/fake-path/') - - def test_fix_up_media_upload_with_initial_invalid(self): - invalid_method_desc = {'response': 'Who cares'} - initial_parameters = {'body': {}} - self._base_fix_up_method_description_test( - invalid_method_desc, initial_parameters, - initial_parameters, [], 0, None) - - def test_fix_up_media_upload_with_initial_valid_minimal(self): - valid_method_desc = {'mediaUpload': {'accept': []}} - initial_parameters = {'body': {}} - final_parameters = {'body': {'required': False}, - 'media_body': MEDIA_BODY_PARAMETER_DEFAULT_VALUE} - self._base_fix_up_method_description_test( - valid_method_desc, initial_parameters, final_parameters, [], 0, - 'http://root/upload/fake/fake-path/') - - def test_fix_up_media_upload_with_initial_valid_full(self): - valid_method_desc = {'mediaUpload': {'accept': ['*/*'], 'maxSize': '10GB'}} - initial_parameters = {'body': {}} - final_parameters = {'body': {'required': False}, - 'media_body': MEDIA_BODY_PARAMETER_DEFAULT_VALUE} - ten_gb = 10 * 2**30 - self._base_fix_up_method_description_test( - valid_method_desc, initial_parameters, final_parameters, ['*/*'], - ten_gb, 'http://root/upload/fake/fake-path/') - - def test_fix_up_method_description_get(self): - result = _fix_up_method_description(self.zoo_get_method_desc, - self.zoo_root_desc) - path_url = 'query' - http_method = 'GET' - method_id = 'bigquery.query' - accept = [] - max_size = 0L - media_path_url = None - self.assertEqual(result, (path_url, http_method, method_id, accept, - max_size, media_path_url)) - - def test_fix_up_method_description_insert(self): - result = _fix_up_method_description(self.zoo_insert_method_desc, - self.zoo_root_desc) - path_url = 'animals' - http_method = 'POST' - method_id = 'zoo.animals.insert' - accept = ['image/png'] - max_size = 1024L - media_path_url = 'https://www.googleapis.com/upload/zoo/v1/animals' - self.assertEqual(result, (path_url, http_method, method_id, accept, - max_size, media_path_url)) - - def test_ResourceMethodParameters_zoo_get(self): - parameters = ResourceMethodParameters(self.zoo_get_method_desc) - - param_types = {'a': 'any', - 'b': 'boolean', - 'e': 'string', - 'er': 'string', - 'i': 'integer', - 'n': 'number', - 'o': 'object', - 'q': 'string', - 'rr': 'string'} - keys = param_types.keys() - self.assertEqual(parameters.argmap, dict((key, key) for key in keys)) - self.assertEqual(parameters.required_params, []) - self.assertEqual(sorted(parameters.repeated_params), ['er', 'rr']) - self.assertEqual(parameters.pattern_params, {'rr': '[a-z]+'}) - self.assertEqual(sorted(parameters.query_params), - ['a', 'b', 'e', 'er', 'i', 'n', 'o', 'q', 'rr']) - self.assertEqual(parameters.path_params, set()) - self.assertEqual(parameters.param_types, param_types) - enum_params = {'e': ['foo', 'bar'], - 'er': ['one', 'two', 'three']} - self.assertEqual(parameters.enum_params, enum_params) - - def test_ResourceMethodParameters_zoo_animals_patch(self): - method_desc = self.zoo_animals_resource['methods']['patch'] - parameters = ResourceMethodParameters(method_desc) - - param_types = {'name': 'string'} - keys = param_types.keys() - self.assertEqual(parameters.argmap, dict((key, key) for key in keys)) - self.assertEqual(parameters.required_params, ['name']) - self.assertEqual(parameters.repeated_params, []) - self.assertEqual(parameters.pattern_params, {}) - self.assertEqual(parameters.query_params, []) - self.assertEqual(parameters.path_params, set(['name'])) - self.assertEqual(parameters.param_types, param_types) - self.assertEqual(parameters.enum_params, {}) - - -class DiscoveryErrors(unittest.TestCase): - - def test_tests_should_be_run_with_strict_positional_enforcement(self): - try: - plus = build('plus', 'v1', None) - self.fail("should have raised a TypeError exception over missing http=.") - except TypeError: - pass - - def test_failed_to_parse_discovery_json(self): - self.http = HttpMock(datafile('malformed.json'), {'status': '200'}) - try: - plus = build('plus', 'v1', http=self.http) - self.fail("should have raised an exception over malformed JSON.") - except InvalidJsonError: - pass - - -class DiscoveryFromDocument(unittest.TestCase): - - def test_can_build_from_local_document(self): - discovery = open(datafile('plus.json')).read() - plus = build_from_document(discovery, base="https://www.googleapis.com/") - self.assertTrue(plus is not None) - self.assertTrue(hasattr(plus, 'activities')) - - def test_can_build_from_local_deserialized_document(self): - discovery = open(datafile('plus.json')).read() - discovery = simplejson.loads(discovery) - plus = build_from_document(discovery, base="https://www.googleapis.com/") - self.assertTrue(plus is not None) - self.assertTrue(hasattr(plus, 'activities')) - - def test_building_with_base_remembers_base(self): - discovery = open(datafile('plus.json')).read() - - base = "https://www.example.com/" - plus = build_from_document(discovery, base=base) - self.assertEquals("https://www.googleapis.com/plus/v1/", plus._baseUrl) - - -class DiscoveryFromHttp(unittest.TestCase): - def setUp(self): - self.old_environ = os.environ.copy() - - def tearDown(self): - os.environ = self.old_environ - - def test_userip_is_added_to_discovery_uri(self): - # build() will raise an HttpError on a 400, use this to pick the request uri - # out of the raised exception. - os.environ['REMOTE_ADDR'] = '10.0.0.1' - try: - http = HttpMockSequence([ - ({'status': '400'}, open(datafile('zoo.json'), 'rb').read()), - ]) - zoo = build('zoo', 'v1', http=http, developerKey='foo', - discoveryServiceUrl='http://example.com') - self.fail('Should have raised an exception.') - except HttpError, e: - self.assertEqual(e.uri, 'http://example.com?userIp=10.0.0.1') - - def test_userip_missing_is_not_added_to_discovery_uri(self): - # build() will raise an HttpError on a 400, use this to pick the request uri - # out of the raised exception. - try: - http = HttpMockSequence([ - ({'status': '400'}, open(datafile('zoo.json'), 'rb').read()), - ]) - zoo = build('zoo', 'v1', http=http, developerKey=None, - discoveryServiceUrl='http://example.com') - self.fail('Should have raised an exception.') - except HttpError, e: - self.assertEqual(e.uri, 'http://example.com') - - -class Discovery(unittest.TestCase): - - def test_method_error_checking(self): - self.http = HttpMock(datafile('plus.json'), {'status': '200'}) - plus = build('plus', 'v1', http=self.http) - - # Missing required parameters - try: - plus.activities().list() - self.fail() - except TypeError, e: - self.assertTrue('Missing' in str(e)) - - # Missing required parameters even if supplied as None. - try: - plus.activities().list(collection=None, userId=None) - self.fail() - except TypeError, e: - self.assertTrue('Missing' in str(e)) - - # Parameter doesn't match regex - try: - plus.activities().list(collection='not_a_collection_name', userId='me') - self.fail() - except TypeError, e: - self.assertTrue('not an allowed value' in str(e)) - - # Unexpected parameter - try: - plus.activities().list(flubber=12) - self.fail() - except TypeError, e: - self.assertTrue('unexpected' in str(e)) - - def _check_query_types(self, request): - parsed = urlparse.urlparse(request.uri) - q = parse_qs(parsed[4]) - self.assertEqual(q['q'], ['foo']) - self.assertEqual(q['i'], ['1']) - self.assertEqual(q['n'], ['1.0']) - self.assertEqual(q['b'], ['false']) - self.assertEqual(q['a'], ['[1, 2, 3]']) - self.assertEqual(q['o'], ['{\'a\': 1}']) - self.assertEqual(q['e'], ['bar']) - - def test_type_coercion(self): - http = HttpMock(datafile('zoo.json'), {'status': '200'}) - zoo = build('zoo', 'v1', http=http) - - request = zoo.query( - q="foo", i=1.0, n=1.0, b=0, a=[1,2,3], o={'a':1}, e='bar') - self._check_query_types(request) - request = zoo.query( - q="foo", i=1, n=1, b=False, a=[1,2,3], o={'a':1}, e='bar') - self._check_query_types(request) - - request = zoo.query( - q="foo", i="1", n="1", b="", a=[1,2,3], o={'a':1}, e='bar', er='two') - - request = zoo.query( - q="foo", i="1", n="1", b="", a=[1,2,3], o={'a':1}, e='bar', - er=['one', 'three'], rr=['foo', 'bar']) - self._check_query_types(request) - - # Five is right out. - self.assertRaises(TypeError, zoo.query, er=['one', 'five']) - - def test_optional_stack_query_parameters(self): - http = HttpMock(datafile('zoo.json'), {'status': '200'}) - zoo = build('zoo', 'v1', http=http) - request = zoo.query(trace='html', fields='description') - - parsed = urlparse.urlparse(request.uri) - q = parse_qs(parsed[4]) - self.assertEqual(q['trace'], ['html']) - self.assertEqual(q['fields'], ['description']) - - def test_string_params_value_of_none_get_dropped(self): - http = HttpMock(datafile('zoo.json'), {'status': '200'}) - zoo = build('zoo', 'v1', http=http) - request = zoo.query(trace=None, fields='description') - - parsed = urlparse.urlparse(request.uri) - q = parse_qs(parsed[4]) - self.assertFalse('trace' in q) - - def test_model_added_query_parameters(self): - http = HttpMock(datafile('zoo.json'), {'status': '200'}) - zoo = build('zoo', 'v1', http=http) - request = zoo.animals().get(name='Lion') - - parsed = urlparse.urlparse(request.uri) - q = parse_qs(parsed[4]) - self.assertEqual(q['alt'], ['json']) - self.assertEqual(request.headers['accept'], 'application/json') - - def test_fallback_to_raw_model(self): - http = HttpMock(datafile('zoo.json'), {'status': '200'}) - zoo = build('zoo', 'v1', http=http) - request = zoo.animals().getmedia(name='Lion') - - parsed = urlparse.urlparse(request.uri) - q = parse_qs(parsed[4]) - self.assertTrue('alt' not in q) - self.assertEqual(request.headers['accept'], '*/*') - - def test_patch(self): - http = HttpMock(datafile('zoo.json'), {'status': '200'}) - zoo = build('zoo', 'v1', http=http) - request = zoo.animals().patch(name='lion', body='{"description": "foo"}') - - self.assertEqual(request.method, 'PATCH') - - def test_tunnel_patch(self): - http = HttpMockSequence([ - ({'status': '200'}, open(datafile('zoo.json'), 'rb').read()), - ({'status': '200'}, 'echo_request_headers_as_json'), - ]) - http = tunnel_patch(http) - zoo = build('zoo', 'v1', http=http) - resp = zoo.animals().patch( - name='lion', body='{"description": "foo"}').execute() - - self.assertTrue('x-http-method-override' in resp) - - def test_plus_resources(self): - self.http = HttpMock(datafile('plus.json'), {'status': '200'}) - plus = build('plus', 'v1', http=self.http) - self.assertTrue(getattr(plus, 'activities')) - self.assertTrue(getattr(plus, 'people')) - - def test_full_featured(self): - # Zoo should exercise all discovery facets - # and should also have no future.json file. - self.http = HttpMock(datafile('zoo.json'), {'status': '200'}) - zoo = build('zoo', 'v1', http=self.http) - self.assertTrue(getattr(zoo, 'animals')) - - request = zoo.animals().list(name='bat', projection="full") - parsed = urlparse.urlparse(request.uri) - q = parse_qs(parsed[4]) - self.assertEqual(q['name'], ['bat']) - self.assertEqual(q['projection'], ['full']) - - def test_nested_resources(self): - self.http = HttpMock(datafile('zoo.json'), {'status': '200'}) - zoo = build('zoo', 'v1', http=self.http) - self.assertTrue(getattr(zoo, 'animals')) - request = zoo.my().favorites().list(max_results="5") - parsed = urlparse.urlparse(request.uri) - q = parse_qs(parsed[4]) - self.assertEqual(q['max-results'], ['5']) - - def test_methods_with_reserved_names(self): - self.http = HttpMock(datafile('zoo.json'), {'status': '200'}) - zoo = build('zoo', 'v1', http=self.http) - self.assertTrue(getattr(zoo, 'animals')) - request = zoo.global_().print_().assert_(max_results="5") - parsed = urlparse.urlparse(request.uri) - self.assertEqual(parsed[2], '/zoo/v1/global/print/assert') - - def test_top_level_functions(self): - self.http = HttpMock(datafile('zoo.json'), {'status': '200'}) - zoo = build('zoo', 'v1', http=self.http) - self.assertTrue(getattr(zoo, 'query')) - request = zoo.query(q="foo") - parsed = urlparse.urlparse(request.uri) - q = parse_qs(parsed[4]) - self.assertEqual(q['q'], ['foo']) - - def test_simple_media_uploads(self): - self.http = HttpMock(datafile('zoo.json'), {'status': '200'}) - zoo = build('zoo', 'v1', http=self.http) - doc = getattr(zoo.animals().insert, '__doc__') - self.assertTrue('media_body' in doc) - - def test_simple_media_upload_no_max_size_provided(self): - self.http = HttpMock(datafile('zoo.json'), {'status': '200'}) - zoo = build('zoo', 'v1', http=self.http) - request = zoo.animals().crossbreed(media_body=datafile('small.png')) - self.assertEquals('image/png', request.headers['content-type']) - self.assertEquals('PNG', request.body[1:4]) - - def test_simple_media_raise_correct_exceptions(self): - self.http = HttpMock(datafile('zoo.json'), {'status': '200'}) - zoo = build('zoo', 'v1', http=self.http) - - try: - zoo.animals().insert(media_body=datafile('smiley.png')) - self.fail("should throw exception if media is too large.") - except MediaUploadSizeError: - pass - - try: - zoo.animals().insert(media_body=datafile('small.jpg')) - self.fail("should throw exception if mimetype is unacceptable.") - except UnacceptableMimeTypeError: - pass - - def test_simple_media_good_upload(self): - self.http = HttpMock(datafile('zoo.json'), {'status': '200'}) - zoo = build('zoo', 'v1', http=self.http) - - request = zoo.animals().insert(media_body=datafile('small.png')) - self.assertEquals('image/png', request.headers['content-type']) - self.assertEquals('PNG', request.body[1:4]) - assertUrisEqual(self, - 'https://www.googleapis.com/upload/zoo/v1/animals?uploadType=media&alt=json', - request.uri) - - def test_multipart_media_raise_correct_exceptions(self): - self.http = HttpMock(datafile('zoo.json'), {'status': '200'}) - zoo = build('zoo', 'v1', http=self.http) - - try: - zoo.animals().insert(media_body=datafile('smiley.png'), body={}) - self.fail("should throw exception if media is too large.") - except MediaUploadSizeError: - pass - - try: - zoo.animals().insert(media_body=datafile('small.jpg'), body={}) - self.fail("should throw exception if mimetype is unacceptable.") - except UnacceptableMimeTypeError: - pass - - def test_multipart_media_good_upload(self): - self.http = HttpMock(datafile('zoo.json'), {'status': '200'}) - zoo = build('zoo', 'v1', http=self.http) - - request = zoo.animals().insert(media_body=datafile('small.png'), body={}) - self.assertTrue(request.headers['content-type'].startswith( - 'multipart/related')) - self.assertEquals('--==', request.body[0:4]) - assertUrisEqual(self, - 'https://www.googleapis.com/upload/zoo/v1/animals?uploadType=multipart&alt=json', - request.uri) - - def test_media_capable_method_without_media(self): - self.http = HttpMock(datafile('zoo.json'), {'status': '200'}) - zoo = build('zoo', 'v1', http=self.http) - - request = zoo.animals().insert(body={}) - self.assertTrue(request.headers['content-type'], 'application/json') - - def test_resumable_multipart_media_good_upload(self): - self.http = HttpMock(datafile('zoo.json'), {'status': '200'}) - zoo = build('zoo', 'v1', http=self.http) - - media_upload = MediaFileUpload(datafile('small.png'), resumable=True) - request = zoo.animals().insert(media_body=media_upload, body={}) - self.assertTrue(request.headers['content-type'].startswith( - 'application/json')) - self.assertEquals('{"data": {}}', request.body) - self.assertEquals(media_upload, request.resumable) - - self.assertEquals('image/png', request.resumable.mimetype()) - - self.assertNotEquals(request.body, None) - self.assertEquals(request.resumable_uri, None) - - http = HttpMockSequence([ - ({'status': '200', - 'location': 'http://upload.example.com'}, ''), - ({'status': '308', - 'location': 'http://upload.example.com/2', - 'range': '0-12'}, ''), - ({'status': '308', - 'location': 'http://upload.example.com/3', - 'range': '0-%d' % (media_upload.size() - 2)}, ''), - ({'status': '200'}, '{"foo": "bar"}'), - ]) - - status, body = request.next_chunk(http=http) - self.assertEquals(None, body) - self.assertTrue(isinstance(status, MediaUploadProgress)) - self.assertEquals(13, status.resumable_progress) - - # Two requests should have been made and the resumable_uri should have been - # updated for each one. - self.assertEquals(request.resumable_uri, 'http://upload.example.com/2') - - self.assertEquals(media_upload, request.resumable) - self.assertEquals(13, request.resumable_progress) - - status, body = request.next_chunk(http=http) - self.assertEquals(request.resumable_uri, 'http://upload.example.com/3') - self.assertEquals(media_upload.size()-1, request.resumable_progress) - self.assertEquals('{"data": {}}', request.body) - - # Final call to next_chunk should complete the upload. - status, body = request.next_chunk(http=http) - self.assertEquals(body, {"foo": "bar"}) - self.assertEquals(status, None) - - - def test_resumable_media_good_upload(self): - """Not a multipart upload.""" - self.http = HttpMock(datafile('zoo.json'), {'status': '200'}) - zoo = build('zoo', 'v1', http=self.http) - - media_upload = MediaFileUpload(datafile('small.png'), resumable=True) - request = zoo.animals().insert(media_body=media_upload, body=None) - self.assertEquals(media_upload, request.resumable) - - self.assertEquals('image/png', request.resumable.mimetype()) - - self.assertEquals(request.body, None) - self.assertEquals(request.resumable_uri, None) - - http = HttpMockSequence([ - ({'status': '200', - 'location': 'http://upload.example.com'}, ''), - ({'status': '308', - 'location': 'http://upload.example.com/2', - 'range': '0-12'}, ''), - ({'status': '308', - 'location': 'http://upload.example.com/3', - 'range': '0-%d' % (media_upload.size() - 2)}, ''), - ({'status': '200'}, '{"foo": "bar"}'), - ]) - - status, body = request.next_chunk(http=http) - self.assertEquals(None, body) - self.assertTrue(isinstance(status, MediaUploadProgress)) - self.assertEquals(13, status.resumable_progress) - - # Two requests should have been made and the resumable_uri should have been - # updated for each one. - self.assertEquals(request.resumable_uri, 'http://upload.example.com/2') - - self.assertEquals(media_upload, request.resumable) - self.assertEquals(13, request.resumable_progress) - - status, body = request.next_chunk(http=http) - self.assertEquals(request.resumable_uri, 'http://upload.example.com/3') - self.assertEquals(media_upload.size()-1, request.resumable_progress) - self.assertEquals(request.body, None) - - # Final call to next_chunk should complete the upload. - status, body = request.next_chunk(http=http) - self.assertEquals(body, {"foo": "bar"}) - self.assertEquals(status, None) - - def test_resumable_media_good_upload_from_execute(self): - """Not a multipart upload.""" - self.http = HttpMock(datafile('zoo.json'), {'status': '200'}) - zoo = build('zoo', 'v1', http=self.http) - - media_upload = MediaFileUpload(datafile('small.png'), resumable=True) - request = zoo.animals().insert(media_body=media_upload, body=None) - assertUrisEqual(self, - 'https://www.googleapis.com/upload/zoo/v1/animals?uploadType=resumable&alt=json', - request.uri) - - http = HttpMockSequence([ - ({'status': '200', - 'location': 'http://upload.example.com'}, ''), - ({'status': '308', - 'location': 'http://upload.example.com/2', - 'range': '0-12'}, ''), - ({'status': '308', - 'location': 'http://upload.example.com/3', - 'range': '0-%d' % media_upload.size()}, ''), - ({'status': '200'}, '{"foo": "bar"}'), - ]) - - body = request.execute(http=http) - self.assertEquals(body, {"foo": "bar"}) - - def test_resumable_media_fail_unknown_response_code_first_request(self): - """Not a multipart upload.""" - self.http = HttpMock(datafile('zoo.json'), {'status': '200'}) - zoo = build('zoo', 'v1', http=self.http) - - media_upload = MediaFileUpload(datafile('small.png'), resumable=True) - request = zoo.animals().insert(media_body=media_upload, body=None) - - http = HttpMockSequence([ - ({'status': '400', - 'location': 'http://upload.example.com'}, ''), - ]) - - try: - request.execute(http=http) - self.fail('Should have raised ResumableUploadError.') - except ResumableUploadError, e: - self.assertEqual(400, e.resp.status) - - def test_resumable_media_fail_unknown_response_code_subsequent_request(self): - """Not a multipart upload.""" - self.http = HttpMock(datafile('zoo.json'), {'status': '200'}) - zoo = build('zoo', 'v1', http=self.http) - - media_upload = MediaFileUpload(datafile('small.png'), resumable=True) - request = zoo.animals().insert(media_body=media_upload, body=None) - - http = HttpMockSequence([ - ({'status': '200', - 'location': 'http://upload.example.com'}, ''), - ({'status': '400'}, ''), - ]) - - self.assertRaises(HttpError, request.execute, http=http) - self.assertTrue(request._in_error_state) - - http = HttpMockSequence([ - ({'status': '308', - 'range': '0-5'}, ''), - ({'status': '308', - 'range': '0-6'}, ''), - ]) - - status, body = request.next_chunk(http=http) - self.assertEquals(status.resumable_progress, 7, - 'Should have first checked length and then tried to PUT more.') - self.assertFalse(request._in_error_state) - - # Put it back in an error state. - http = HttpMockSequence([ - ({'status': '400'}, ''), - ]) - self.assertRaises(HttpError, request.execute, http=http) - self.assertTrue(request._in_error_state) - - # Pretend the last request that 400'd actually succeeded. - http = HttpMockSequence([ - ({'status': '200'}, '{"foo": "bar"}'), - ]) - status, body = request.next_chunk(http=http) - self.assertEqual(body, {'foo': 'bar'}) - - def test_media_io_base_stream_unlimited_chunksize_resume(self): - self.http = HttpMock(datafile('zoo.json'), {'status': '200'}) - zoo = build('zoo', 'v1', http=self.http) - - try: - import io - - # Set up a seekable stream and try to upload in single chunk. - fd = io.BytesIO('01234"56789"') - media_upload = MediaIoBaseUpload( - fd=fd, mimetype='text/plain', chunksize=-1, resumable=True) - - request = zoo.animals().insert(media_body=media_upload, body=None) - - # The single chunk fails, restart at the right point. - http = HttpMockSequence([ - ({'status': '200', - 'location': 'http://upload.example.com'}, ''), - ({'status': '308', - 'location': 'http://upload.example.com/2', - 'range': '0-4'}, ''), - ({'status': '200'}, 'echo_request_body'), - ]) - - body = request.execute(http=http) - self.assertEqual('56789', body) - - except ImportError: - pass - - - def test_media_io_base_stream_chunksize_resume(self): - self.http = HttpMock(datafile('zoo.json'), {'status': '200'}) - zoo = build('zoo', 'v1', http=self.http) - - try: - import io - - # Set up a seekable stream and try to upload in chunks. - fd = io.BytesIO('0123456789') - media_upload = MediaIoBaseUpload( - fd=fd, mimetype='text/plain', chunksize=5, resumable=True) - - request = zoo.animals().insert(media_body=media_upload, body=None) - - # The single chunk fails, pull the content sent out of the exception. - http = HttpMockSequence([ - ({'status': '200', - 'location': 'http://upload.example.com'}, ''), - ({'status': '400'}, 'echo_request_body'), - ]) - - try: - body = request.execute(http=http) - except HttpError, e: - self.assertEqual('01234', e.content) - - except ImportError: - pass - - - def test_resumable_media_handle_uploads_of_unknown_size(self): - http = HttpMockSequence([ - ({'status': '200', - 'location': 'http://upload.example.com'}, ''), - ({'status': '200'}, 'echo_request_headers_as_json'), - ]) - - self.http = HttpMock(datafile('zoo.json'), {'status': '200'}) - zoo = build('zoo', 'v1', http=self.http) - - # Create an upload that doesn't know the full size of the media. - class IoBaseUnknownLength(MediaUpload): - def chunksize(self): - return 10 - - def mimetype(self): - return 'image/png' - - def size(self): - return None - - def resumable(self): - return True - - def getbytes(self, begin, length): - return '0123456789' - - upload = IoBaseUnknownLength() - - request = zoo.animals().insert(media_body=upload, body=None) - status, body = request.next_chunk(http=http) - self.assertEqual(body, { - 'Content-Range': 'bytes 0-9/*', - 'Content-Length': '10', - }) - - def test_resumable_media_no_streaming_on_unsupported_platforms(self): - self.http = HttpMock(datafile('zoo.json'), {'status': '200'}) - zoo = build('zoo', 'v1', http=self.http) - - class IoBaseHasStream(MediaUpload): - def chunksize(self): - return 10 - - def mimetype(self): - return 'image/png' - - def size(self): - return None - - def resumable(self): - return True - - def getbytes(self, begin, length): - return '0123456789' - - def has_stream(self): - return True - - def stream(self): - raise NotImplementedError() - - upload = IoBaseHasStream() - - orig_version = sys.version_info - sys.version_info = (2, 5, 5, 'final', 0) - - request = zoo.animals().insert(media_body=upload, body=None) - - http = HttpMockSequence([ - ({'status': '200', - 'location': 'http://upload.example.com'}, ''), - ({'status': '200'}, 'echo_request_headers_as_json'), - ]) - - # This should not raise an exception because stream() shouldn't be called. - status, body = request.next_chunk(http=http) - self.assertEqual(body, { - 'Content-Range': 'bytes 0-9/*', - 'Content-Length': '10' - }) - - sys.version_info = (2, 6, 5, 'final', 0) - - request = zoo.animals().insert(media_body=upload, body=None) - - # This should raise an exception because stream() will be called. - http = HttpMockSequence([ - ({'status': '200', - 'location': 'http://upload.example.com'}, ''), - ({'status': '200'}, 'echo_request_headers_as_json'), - ]) - - self.assertRaises(NotImplementedError, request.next_chunk, http=http) - - sys.version_info = orig_version - - def test_resumable_media_handle_uploads_of_unknown_size_eof(self): - http = HttpMockSequence([ - ({'status': '200', - 'location': 'http://upload.example.com'}, ''), - ({'status': '200'}, 'echo_request_headers_as_json'), - ]) - - self.http = HttpMock(datafile('zoo.json'), {'status': '200'}) - zoo = build('zoo', 'v1', http=self.http) - - fd = StringIO.StringIO('data goes here') - - # Create an upload that doesn't know the full size of the media. - upload = MediaIoBaseUpload( - fd=fd, mimetype='image/png', chunksize=15, resumable=True) - - request = zoo.animals().insert(media_body=upload, body=None) - status, body = request.next_chunk(http=http) - self.assertEqual(body, { - 'Content-Range': 'bytes 0-13/14', - 'Content-Length': '14', - }) - - def test_resumable_media_handle_resume_of_upload_of_unknown_size(self): - http = HttpMockSequence([ - ({'status': '200', - 'location': 'http://upload.example.com'}, ''), - ({'status': '400'}, ''), - ]) - - self.http = HttpMock(datafile('zoo.json'), {'status': '200'}) - zoo = build('zoo', 'v1', http=self.http) - - # Create an upload that doesn't know the full size of the media. - fd = StringIO.StringIO('data goes here') - - upload = MediaIoBaseUpload( - fd=fd, mimetype='image/png', chunksize=500, resumable=True) - - request = zoo.animals().insert(media_body=upload, body=None) - - # Put it in an error state. - self.assertRaises(HttpError, request.next_chunk, http=http) - - http = HttpMockSequence([ - ({'status': '400', - 'range': '0-5'}, 'echo_request_headers_as_json'), - ]) - try: - # Should resume the upload by first querying the status of the upload. - request.next_chunk(http=http) - except HttpError, e: - expected = { - 'Content-Range': 'bytes */14', - 'content-length': '0' - } - self.assertEqual(expected, simplejson.loads(e.content), - 'Should send an empty body when requesting the current upload status.') - - def test_pickle(self): - sorted_resource_keys = ['_baseUrl', - '_developerKey', - '_dynamic_attrs', - '_http', - '_model', - '_requestBuilder', - '_resourceDesc', - '_rootDesc', - '_schema', - 'animals', - 'global_', - 'load', - 'loadNoTemplate', - 'my', - 'query', - 'scopedAnimals'] - - http = HttpMock(datafile('zoo.json'), {'status': '200'}) - zoo = build('zoo', 'v1', http=http) - self.assertEqual(sorted(zoo.__dict__.keys()), sorted_resource_keys) - - pickled_zoo = pickle.dumps(zoo) - new_zoo = pickle.loads(pickled_zoo) - self.assertEqual(sorted(new_zoo.__dict__.keys()), sorted_resource_keys) - self.assertTrue(hasattr(new_zoo, 'animals')) - self.assertTrue(callable(new_zoo.animals)) - self.assertTrue(hasattr(new_zoo, 'global_')) - self.assertTrue(callable(new_zoo.global_)) - self.assertTrue(hasattr(new_zoo, 'load')) - self.assertTrue(callable(new_zoo.load)) - self.assertTrue(hasattr(new_zoo, 'loadNoTemplate')) - self.assertTrue(callable(new_zoo.loadNoTemplate)) - self.assertTrue(hasattr(new_zoo, 'my')) - self.assertTrue(callable(new_zoo.my)) - self.assertTrue(hasattr(new_zoo, 'query')) - self.assertTrue(callable(new_zoo.query)) - self.assertTrue(hasattr(new_zoo, 'scopedAnimals')) - self.assertTrue(callable(new_zoo.scopedAnimals)) - - self.assertEqual(sorted(zoo._dynamic_attrs), sorted(new_zoo._dynamic_attrs)) - self.assertEqual(zoo._baseUrl, new_zoo._baseUrl) - self.assertEqual(zoo._developerKey, new_zoo._developerKey) - self.assertEqual(zoo._requestBuilder, new_zoo._requestBuilder) - self.assertEqual(zoo._resourceDesc, new_zoo._resourceDesc) - self.assertEqual(zoo._rootDesc, new_zoo._rootDesc) - # _http, _model and _schema won't be equal since we will get new - # instances upon un-pickling - - def _dummy_zoo_request(self): - with open(os.path.join(DATA_DIR, 'zoo.json'), 'rU') as fh: - zoo_contents = fh.read() - - zoo_uri = uritemplate.expand(DISCOVERY_URI, - {'api': 'zoo', 'apiVersion': 'v1'}) - if 'REMOTE_ADDR' in os.environ: - zoo_uri = util._add_query_parameter(zoo_uri, 'userIp', - os.environ['REMOTE_ADDR']) - - http = httplib2.Http() - original_request = http.request - def wrapped_request(uri, method='GET', *args, **kwargs): - if uri == zoo_uri: - return httplib2.Response({'status': '200'}), zoo_contents - return original_request(uri, method=method, *args, **kwargs) - http.request = wrapped_request - return http - - def _dummy_token(self): - access_token = 'foo' - client_id = 'some_client_id' - client_secret = 'cOuDdkfjxxnv+' - refresh_token = '1/0/a.df219fjls0' - token_expiry = datetime.datetime.utcnow() - user_agent = 'refresh_checker/1.0' - return OAuth2Credentials( - access_token, client_id, client_secret, - refresh_token, token_expiry, GOOGLE_TOKEN_URI, - user_agent) - - def test_pickle_with_credentials(self): - credentials = self._dummy_token() - http = self._dummy_zoo_request() - http = credentials.authorize(http) - self.assertTrue(hasattr(http.request, 'credentials')) - - zoo = build('zoo', 'v1', http=http) - pickled_zoo = pickle.dumps(zoo) - new_zoo = pickle.loads(pickled_zoo) - self.assertEqual(sorted(zoo.__dict__.keys()), - sorted(new_zoo.__dict__.keys())) - new_http = new_zoo._http - self.assertFalse(hasattr(new_http.request, 'credentials')) - - -class Next(unittest.TestCase): - - def test_next_successful_none_on_no_next_page_token(self): - self.http = HttpMock(datafile('tasks.json'), {'status': '200'}) - tasks = build('tasks', 'v1', http=self.http) - request = tasks.tasklists().list() - self.assertEqual(None, tasks.tasklists().list_next(request, {})) - - def test_next_successful_with_next_page_token(self): - self.http = HttpMock(datafile('tasks.json'), {'status': '200'}) - tasks = build('tasks', 'v1', http=self.http) - request = tasks.tasklists().list() - next_request = tasks.tasklists().list_next( - request, {'nextPageToken': '123abc'}) - parsed = list(urlparse.urlparse(next_request.uri)) - q = parse_qs(parsed[4]) - self.assertEqual(q['pageToken'][0], '123abc') - - def test_next_with_method_with_no_properties(self): - self.http = HttpMock(datafile('latitude.json'), {'status': '200'}) - service = build('latitude', 'v1', http=self.http) - request = service.currentLocation().get() - - -class MediaGet(unittest.TestCase): - - def test_get_media(self): - http = HttpMock(datafile('zoo.json'), {'status': '200'}) - zoo = build('zoo', 'v1', http=http) - request = zoo.animals().get_media(name='Lion') - - parsed = urlparse.urlparse(request.uri) - q = parse_qs(parsed[4]) - self.assertEqual(q['alt'], ['media']) - self.assertEqual(request.headers['accept'], '*/*') - - http = HttpMockSequence([ - ({'status': '200'}, 'standing in for media'), - ]) - response = request.execute(http=http) - self.assertEqual('standing in for media', response) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_oauth2client_django_orm.py b/tests/test_django_orm.py similarity index 100% rename from tests/test_oauth2client_django_orm.py rename to tests/test_django_orm.py diff --git a/tests/test_errors.py b/tests/test_errors.py deleted file mode 100644 index f3d26d3..0000000 --- a/tests/test_errors.py +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/python2.4 -# -# Copyright 2010 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Tests for errors handling -""" - -__author__ = 'afshar@google.com (Ali Afshar)' - - -import unittest -import httplib2 - - -from googleapiclient.errors import HttpError - - -JSON_ERROR_CONTENT = """ -{ - "error": { - "errors": [ - { - "domain": "global", - "reason": "required", - "message": "country is required", - "locationType": "parameter", - "location": "country" - } - ], - "code": 400, - "message": "country is required" - } -} -""" - -def fake_response(data, headers, reason='Ok'): - response = httplib2.Response(headers) - response.reason = reason - return response, data - - -class Error(unittest.TestCase): - """Test handling of error bodies.""" - - def test_json_body(self): - """Test a nicely formed, expected error response.""" - resp, content = fake_response(JSON_ERROR_CONTENT, - {'status':'400', 'content-type': 'application/json'}, - reason='Failed') - error = HttpError(resp, content, uri='http://example.org') - self.assertEqual(str(error), '') - - def test_bad_json_body(self): - """Test handling of bodies with invalid json.""" - resp, content = fake_response('{', - { 'status':'400', 'content-type': 'application/json'}, - reason='Failed') - error = HttpError(resp, content) - self.assertEqual(str(error), '') - - def test_with_uri(self): - """Test handling of passing in the request uri.""" - resp, content = fake_response('{', - {'status':'400', 'content-type': 'application/json'}, - reason='Failure') - error = HttpError(resp, content, uri='http://example.org') - self.assertEqual(str(error), '') - - def test_missing_message_json_body(self): - """Test handling of bodies with missing expected 'message' element.""" - resp, content = fake_response('{}', - {'status':'400', 'content-type': 'application/json'}, - reason='Failed') - error = HttpError(resp, content) - self.assertEqual(str(error), '') - - def test_non_json(self): - """Test handling of non-JSON bodies""" - resp, content = fake_response('}NOT OK', {'status':'400'}) - error = HttpError(resp, content) - self.assertEqual(str(error), '') - - def test_missing_reason(self): - """Test an empty dict with a missing resp.reason.""" - resp, content = fake_response('}NOT OK', {'status': '400'}, reason=None) - error = HttpError(resp, content) - self.assertEqual(str(error), '') diff --git a/tests/test_oauth2client_file.py b/tests/test_file.py similarity index 99% rename from tests/test_oauth2client_file.py rename to tests/test_file.py index 910466a..1de82af 100644 --- a/tests/test_oauth2client_file.py +++ b/tests/test_file.py @@ -31,7 +31,7 @@ import stat import tempfile import unittest -from googleapiclient.http import HttpMockSequence +from http_mock import HttpMockSequence from oauth2client import GOOGLE_TOKEN_URI from oauth2client import file from oauth2client import locked_file diff --git a/tests/test_oauth2client_gce.py b/tests/test_gce.py similarity index 100% rename from tests/test_oauth2client_gce.py rename to tests/test_gce.py diff --git a/tests/test_http.py b/tests/test_http.py deleted file mode 100644 index 0aab456..0000000 --- a/tests/test_http.py +++ /dev/null @@ -1,1004 +0,0 @@ -#!/usr/bin/python2.4 -# -# Copyright 2010 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Http tests - -Unit tests for the googleapiclient.http. -""" - -__author__ = 'jcgregorio@google.com (Joe Gregorio)' - -# Do not remove the httplib2 import -import httplib2 -import logging -import os -import unittest -import urllib -import random -import StringIO -import time - -from googleapiclient.discovery import build -from googleapiclient.errors import BatchError -from googleapiclient.errors import HttpError -from googleapiclient.errors import InvalidChunkSizeError -from googleapiclient.http import BatchHttpRequest -from googleapiclient.http import HttpMock -from googleapiclient.http import HttpMockSequence -from googleapiclient.http import HttpRequest -from googleapiclient.http import MAX_URI_LENGTH -from googleapiclient.http import MediaFileUpload -from googleapiclient.http import MediaInMemoryUpload -from googleapiclient.http import MediaIoBaseDownload -from googleapiclient.http import MediaIoBaseUpload -from googleapiclient.http import MediaUpload -from googleapiclient.http import _StreamSlice -from googleapiclient.http import set_user_agent -from googleapiclient.model import JsonModel -from oauth2client.client import Credentials - - -class MockCredentials(Credentials): - """Mock class for all Credentials objects.""" - def __init__(self, bearer_token): - super(MockCredentials, self).__init__() - self._authorized = 0 - self._refreshed = 0 - self._applied = 0 - self._bearer_token = bearer_token - - def authorize(self, http): - self._authorized += 1 - - request_orig = http.request - - # The closure that will replace 'httplib2.Http.request'. - def new_request(uri, method='GET', body=None, headers=None, - redirections=httplib2.DEFAULT_MAX_REDIRECTS, - connection_type=None): - # Modify the request headers to add the appropriate - # Authorization header. - if headers is None: - headers = {} - self.apply(headers) - - resp, content = request_orig(uri, method, body, headers, - redirections, connection_type) - - return resp, content - - # Replace the request method with our own closure. - http.request = new_request - - # Set credentials as a property of the request method. - setattr(http.request, 'credentials', self) - - return http - - def refresh(self, http): - self._refreshed += 1 - - def apply(self, headers): - self._applied += 1 - headers['authorization'] = self._bearer_token + ' ' + str(self._refreshed) - - -DATA_DIR = os.path.join(os.path.dirname(__file__), 'data') - - -def datafile(filename): - return os.path.join(DATA_DIR, filename) - -class TestUserAgent(unittest.TestCase): - - def test_set_user_agent(self): - http = HttpMockSequence([ - ({'status': '200'}, 'echo_request_headers'), - ]) - - http = set_user_agent(http, "my_app/5.5") - resp, content = http.request("http://example.com") - self.assertEqual('my_app/5.5', content['user-agent']) - - def test_set_user_agent_nested(self): - http = HttpMockSequence([ - ({'status': '200'}, 'echo_request_headers'), - ]) - - http = set_user_agent(http, "my_app/5.5") - http = set_user_agent(http, "my_library/0.1") - resp, content = http.request("http://example.com") - self.assertEqual('my_app/5.5 my_library/0.1', content['user-agent']) - - -class TestMediaUpload(unittest.TestCase): - - def test_media_file_upload_to_from_json(self): - upload = MediaFileUpload( - datafile('small.png'), chunksize=500, resumable=True) - self.assertEqual('image/png', upload.mimetype()) - self.assertEqual(190, upload.size()) - self.assertEqual(True, upload.resumable()) - self.assertEqual(500, upload.chunksize()) - self.assertEqual('PNG', upload.getbytes(1, 3)) - - json = upload.to_json() - new_upload = MediaUpload.new_from_json(json) - - self.assertEqual('image/png', new_upload.mimetype()) - self.assertEqual(190, new_upload.size()) - self.assertEqual(True, new_upload.resumable()) - self.assertEqual(500, new_upload.chunksize()) - self.assertEqual('PNG', new_upload.getbytes(1, 3)) - - def test_media_file_upload_raises_on_invalid_chunksize(self): - self.assertRaises(InvalidChunkSizeError, MediaFileUpload, - datafile('small.png'), mimetype='image/png', chunksize=-2, - resumable=True) - - def test_media_inmemory_upload(self): - media = MediaInMemoryUpload('abcdef', mimetype='text/plain', chunksize=10, - resumable=True) - self.assertEqual('text/plain', media.mimetype()) - self.assertEqual(10, media.chunksize()) - self.assertTrue(media.resumable()) - self.assertEqual('bc', media.getbytes(1, 2)) - self.assertEqual(6, media.size()) - - def test_http_request_to_from_json(self): - - def _postproc(*kwargs): - pass - - http = httplib2.Http() - media_upload = MediaFileUpload( - datafile('small.png'), chunksize=500, resumable=True) - req = HttpRequest( - http, - _postproc, - 'http://example.com', - method='POST', - body='{}', - headers={'content-type': 'multipart/related; boundary="---flubber"'}, - methodId='foo', - resumable=media_upload) - - json = req.to_json() - new_req = HttpRequest.from_json(json, http, _postproc) - - self.assertEqual({'content-type': - 'multipart/related; boundary="---flubber"'}, - new_req.headers) - self.assertEqual('http://example.com', new_req.uri) - self.assertEqual('{}', new_req.body) - self.assertEqual(http, new_req.http) - self.assertEqual(media_upload.to_json(), new_req.resumable.to_json()) - - self.assertEqual(random.random, new_req._rand) - self.assertEqual(time.sleep, new_req._sleep) - - -class TestMediaIoBaseUpload(unittest.TestCase): - - def test_media_io_base_upload_from_file_io(self): - try: - import io - - fd = io.FileIO(datafile('small.png'), 'r') - upload = MediaIoBaseUpload( - fd=fd, mimetype='image/png', chunksize=500, resumable=True) - self.assertEqual('image/png', upload.mimetype()) - self.assertEqual(190, upload.size()) - self.assertEqual(True, upload.resumable()) - self.assertEqual(500, upload.chunksize()) - self.assertEqual('PNG', upload.getbytes(1, 3)) - except ImportError: - pass - - def test_media_io_base_upload_from_file_object(self): - f = open(datafile('small.png'), 'r') - upload = MediaIoBaseUpload( - fd=f, mimetype='image/png', chunksize=500, resumable=True) - self.assertEqual('image/png', upload.mimetype()) - self.assertEqual(190, upload.size()) - self.assertEqual(True, upload.resumable()) - self.assertEqual(500, upload.chunksize()) - self.assertEqual('PNG', upload.getbytes(1, 3)) - f.close() - - def test_media_io_base_upload_serializable(self): - f = open(datafile('small.png'), 'r') - upload = MediaIoBaseUpload(fd=f, mimetype='image/png') - - try: - json = upload.to_json() - self.fail('MediaIoBaseUpload should not be serializable.') - except NotImplementedError: - pass - - def test_media_io_base_upload_from_string_io(self): - f = open(datafile('small.png'), 'r') - fd = StringIO.StringIO(f.read()) - f.close() - - upload = MediaIoBaseUpload( - fd=fd, mimetype='image/png', chunksize=500, resumable=True) - self.assertEqual('image/png', upload.mimetype()) - self.assertEqual(190, upload.size()) - self.assertEqual(True, upload.resumable()) - self.assertEqual(500, upload.chunksize()) - self.assertEqual('PNG', upload.getbytes(1, 3)) - f.close() - - def test_media_io_base_upload_from_bytes(self): - try: - import io - - f = open(datafile('small.png'), 'r') - fd = io.BytesIO(f.read()) - upload = MediaIoBaseUpload( - fd=fd, mimetype='image/png', chunksize=500, resumable=True) - self.assertEqual('image/png', upload.mimetype()) - self.assertEqual(190, upload.size()) - self.assertEqual(True, upload.resumable()) - self.assertEqual(500, upload.chunksize()) - self.assertEqual('PNG', upload.getbytes(1, 3)) - except ImportError: - pass - - def test_media_io_base_upload_raises_on_invalid_chunksize(self): - try: - import io - - f = open(datafile('small.png'), 'r') - fd = io.BytesIO(f.read()) - self.assertRaises(InvalidChunkSizeError, MediaIoBaseUpload, - fd, 'image/png', chunksize=-2, resumable=True) - except ImportError: - pass - - def test_media_io_base_upload_streamable(self): - try: - import io - - fd = io.BytesIO('stuff') - upload = MediaIoBaseUpload( - fd=fd, mimetype='image/png', chunksize=500, resumable=True) - self.assertEqual(True, upload.has_stream()) - self.assertEqual(fd, upload.stream()) - except ImportError: - pass - - def test_media_io_base_next_chunk_retries(self): - try: - import io - except ImportError: - return - - f = open(datafile('small.png'), 'r') - fd = io.BytesIO(f.read()) - upload = MediaIoBaseUpload( - fd=fd, mimetype='image/png', chunksize=500, resumable=True) - - # Simulate 5XXs for both the request that creates the resumable upload and - # the upload itself. - http = HttpMockSequence([ - ({'status': '500'}, ''), - ({'status': '500'}, ''), - ({'status': '503'}, ''), - ({'status': '200', 'location': 'location'}, ''), - ({'status': '500'}, ''), - ({'status': '500'}, ''), - ({'status': '503'}, ''), - ({'status': '200'}, '{}'), - ]) - - model = JsonModel() - uri = u'https://www.googleapis.com/someapi/v1/upload/?foo=bar' - method = u'POST' - request = HttpRequest( - http, - model.response, - uri, - method=method, - headers={}, - resumable=upload) - - sleeptimes = [] - request._sleep = lambda x: sleeptimes.append(x) - request._rand = lambda: 10 - - request.execute(num_retries=3) - self.assertEqual([20, 40, 80, 20, 40, 80], sleeptimes) - - -class TestMediaIoBaseDownload(unittest.TestCase): - - def setUp(self): - http = HttpMock(datafile('zoo.json'), {'status': '200'}) - zoo = build('zoo', 'v1', http=http) - self.request = zoo.animals().get_media(name='Lion') - self.fd = StringIO.StringIO() - - def test_media_io_base_download(self): - self.request.http = HttpMockSequence([ - ({'status': '200', - 'content-range': '0-2/5'}, '123'), - ({'status': '200', - 'content-range': '3-4/5'}, '45'), - ]) - self.assertEqual(True, self.request.http.follow_redirects) - - download = MediaIoBaseDownload( - fd=self.fd, request=self.request, chunksize=3) - - self.assertEqual(self.fd, download._fd) - self.assertEqual(3, download._chunksize) - self.assertEqual(0, download._progress) - self.assertEqual(None, download._total_size) - self.assertEqual(False, download._done) - self.assertEqual(self.request.uri, download._uri) - - status, done = download.next_chunk() - - self.assertEqual(self.fd.getvalue(), '123') - self.assertEqual(False, done) - self.assertEqual(3, download._progress) - self.assertEqual(5, download._total_size) - self.assertEqual(3, status.resumable_progress) - - status, done = download.next_chunk() - - self.assertEqual(self.fd.getvalue(), '12345') - self.assertEqual(True, done) - self.assertEqual(5, download._progress) - self.assertEqual(5, download._total_size) - - def test_media_io_base_download_handle_redirects(self): - self.request.http = HttpMockSequence([ - ({'status': '200', - 'content-location': 'https://secure.example.net/lion'}, ''), - ({'status': '200', - 'content-range': '0-2/5'}, 'abc'), - ]) - - download = MediaIoBaseDownload( - fd=self.fd, request=self.request, chunksize=3) - - status, done = download.next_chunk() - - self.assertEqual('https://secure.example.net/lion', download._uri) - - def test_media_io_base_download_handle_4xx(self): - self.request.http = HttpMockSequence([ - ({'status': '400'}, ''), - ]) - - download = MediaIoBaseDownload( - fd=self.fd, request=self.request, chunksize=3) - - try: - status, done = download.next_chunk() - self.fail('Should raise an exception') - except HttpError: - pass - - # Even after raising an exception we can pick up where we left off. - self.request.http = HttpMockSequence([ - ({'status': '200', - 'content-range': '0-2/5'}, '123'), - ]) - - status, done = download.next_chunk() - - self.assertEqual(self.fd.getvalue(), '123') - - def test_media_io_base_download_retries_5xx(self): - self.request.http = HttpMockSequence([ - ({'status': '500'}, ''), - ({'status': '500'}, ''), - ({'status': '500'}, ''), - ({'status': '200', - 'content-range': '0-2/5'}, '123'), - ({'status': '503'}, ''), - ({'status': '503'}, ''), - ({'status': '503'}, ''), - ({'status': '200', - 'content-range': '3-4/5'}, '45'), - ]) - - download = MediaIoBaseDownload( - fd=self.fd, request=self.request, chunksize=3) - - self.assertEqual(self.fd, download._fd) - self.assertEqual(3, download._chunksize) - self.assertEqual(0, download._progress) - self.assertEqual(None, download._total_size) - self.assertEqual(False, download._done) - self.assertEqual(self.request.uri, download._uri) - - # Set time.sleep and random.random stubs. - sleeptimes = [] - download._sleep = lambda x: sleeptimes.append(x) - download._rand = lambda: 10 - - status, done = download.next_chunk(num_retries=3) - - # Check for exponential backoff using the rand function above. - self.assertEqual([20, 40, 80], sleeptimes) - - self.assertEqual(self.fd.getvalue(), '123') - self.assertEqual(False, done) - self.assertEqual(3, download._progress) - self.assertEqual(5, download._total_size) - self.assertEqual(3, status.resumable_progress) - - # Reset time.sleep stub. - del sleeptimes[0:len(sleeptimes)] - - status, done = download.next_chunk(num_retries=3) - - # Check for exponential backoff using the rand function above. - self.assertEqual([20, 40, 80], sleeptimes) - - self.assertEqual(self.fd.getvalue(), '12345') - self.assertEqual(True, done) - self.assertEqual(5, download._progress) - self.assertEqual(5, download._total_size) - -EXPECTED = """POST /someapi/v1/collection/?foo=bar HTTP/1.1 -Content-Type: application/json -MIME-Version: 1.0 -Host: www.googleapis.com -content-length: 2\r\n\r\n{}""" - - -NO_BODY_EXPECTED = """POST /someapi/v1/collection/?foo=bar HTTP/1.1 -Content-Type: application/json -MIME-Version: 1.0 -Host: www.googleapis.com -content-length: 0\r\n\r\n""" - - -RESPONSE = """HTTP/1.1 200 OK -Content-Type: application/json -Content-Length: 14 -ETag: "etag/pony"\r\n\r\n{"answer": 42}""" - - -BATCH_RESPONSE = """--batch_foobarbaz -Content-Type: application/http -Content-Transfer-Encoding: binary -Content-ID: - -HTTP/1.1 200 OK -Content-Type: application/json -Content-Length: 14 -ETag: "etag/pony"\r\n\r\n{"foo": 42} - ---batch_foobarbaz -Content-Type: application/http -Content-Transfer-Encoding: binary -Content-ID: - -HTTP/1.1 200 OK -Content-Type: application/json -Content-Length: 14 -ETag: "etag/sheep"\r\n\r\n{"baz": "qux"} ---batch_foobarbaz--""" - - -BATCH_ERROR_RESPONSE = """--batch_foobarbaz -Content-Type: application/http -Content-Transfer-Encoding: binary -Content-ID: - -HTTP/1.1 200 OK -Content-Type: application/json -Content-Length: 14 -ETag: "etag/pony"\r\n\r\n{"foo": 42} - ---batch_foobarbaz -Content-Type: application/http -Content-Transfer-Encoding: binary -Content-ID: - -HTTP/1.1 403 Access Not Configured -Content-Type: application/json -Content-Length: 245 -ETag: "etag/sheep"\r\n\r\n{ - "error": { - "errors": [ - { - "domain": "usageLimits", - "reason": "accessNotConfigured", - "message": "Access Not Configured", - "debugInfo": "QuotaState: BLOCKED" - } - ], - "code": 403, - "message": "Access Not Configured" - } -} - ---batch_foobarbaz--""" - - -BATCH_RESPONSE_WITH_401 = """--batch_foobarbaz -Content-Type: application/http -Content-Transfer-Encoding: binary -Content-ID: - -HTTP/1.1 401 Authorization Required -Content-Type: application/json -Content-Length: 14 -ETag: "etag/pony"\r\n\r\n{"error": {"message": - "Authorizaton failed."}} - ---batch_foobarbaz -Content-Type: application/http -Content-Transfer-Encoding: binary -Content-ID: - -HTTP/1.1 200 OK -Content-Type: application/json -Content-Length: 14 -ETag: "etag/sheep"\r\n\r\n{"baz": "qux"} ---batch_foobarbaz--""" - - -BATCH_SINGLE_RESPONSE = """--batch_foobarbaz -Content-Type: application/http -Content-Transfer-Encoding: binary -Content-ID: - -HTTP/1.1 200 OK -Content-Type: application/json -Content-Length: 14 -ETag: "etag/pony"\r\n\r\n{"foo": 42} ---batch_foobarbaz--""" - -class Callbacks(object): - def __init__(self): - self.responses = {} - self.exceptions = {} - - def f(self, request_id, response, exception): - self.responses[request_id] = response - self.exceptions[request_id] = exception - - -class TestHttpRequest(unittest.TestCase): - def test_unicode(self): - http = HttpMock(datafile('zoo.json'), headers={'status': '200'}) - model = JsonModel() - uri = u'https://www.googleapis.com/someapi/v1/collection/?foo=bar' - method = u'POST' - request = HttpRequest( - http, - model.response, - uri, - method=method, - body=u'{}', - headers={'content-type': 'application/json'}) - request.execute() - self.assertEqual(uri, http.uri) - self.assertEqual(str, type(http.uri)) - self.assertEqual(method, http.method) - self.assertEqual(str, type(http.method)) - - def test_retry(self): - num_retries = 5 - resp_seq = [({'status': '500'}, '')] * num_retries - resp_seq.append(({'status': '200'}, '{}')) - - http = HttpMockSequence(resp_seq) - model = JsonModel() - uri = u'https://www.googleapis.com/someapi/v1/collection/?foo=bar' - method = u'POST' - request = HttpRequest( - http, - model.response, - uri, - method=method, - body=u'{}', - headers={'content-type': 'application/json'}) - - sleeptimes = [] - request._sleep = lambda x: sleeptimes.append(x) - request._rand = lambda: 10 - - request.execute(num_retries=num_retries) - - self.assertEqual(num_retries, len(sleeptimes)) - for retry_num in xrange(num_retries): - self.assertEqual(10 * 2**(retry_num + 1), sleeptimes[retry_num]) - - def test_no_retry_fails_fast(self): - http = HttpMockSequence([ - ({'status': '500'}, ''), - ({'status': '200'}, '{}') - ]) - model = JsonModel() - uri = u'https://www.googleapis.com/someapi/v1/collection/?foo=bar' - method = u'POST' - request = HttpRequest( - http, - model.response, - uri, - method=method, - body=u'{}', - headers={'content-type': 'application/json'}) - - request._rand = lambda: 1.0 - request._sleep = lambda _: self.fail('sleep should not have been called.') - - try: - request.execute() - self.fail('Should have raised an exception.') - except HttpError: - pass - - -class TestBatch(unittest.TestCase): - - def setUp(self): - model = JsonModel() - self.request1 = HttpRequest( - None, - model.response, - 'https://www.googleapis.com/someapi/v1/collection/?foo=bar', - method='POST', - body='{}', - headers={'content-type': 'application/json'}) - - self.request2 = HttpRequest( - None, - model.response, - 'https://www.googleapis.com/someapi/v1/collection/?foo=bar', - method='GET', - body='', - headers={'content-type': 'application/json'}) - - - def test_id_to_from_content_id_header(self): - batch = BatchHttpRequest() - self.assertEquals('12', batch._header_to_id(batch._id_to_header('12'))) - - def test_invalid_content_id_header(self): - batch = BatchHttpRequest() - self.assertRaises(BatchError, batch._header_to_id, '[foo+x]') - self.assertRaises(BatchError, batch._header_to_id, 'foo+1') - self.assertRaises(BatchError, batch._header_to_id, '') - - def test_serialize_request(self): - batch = BatchHttpRequest() - request = HttpRequest( - None, - None, - 'https://www.googleapis.com/someapi/v1/collection/?foo=bar', - method='POST', - body='{}', - headers={'content-type': 'application/json'}, - methodId=None, - resumable=None) - s = batch._serialize_request(request).splitlines() - self.assertEqual(EXPECTED.splitlines(), s) - - def test_serialize_request_media_body(self): - batch = BatchHttpRequest() - f = open(datafile('small.png')) - body = f.read() - f.close() - - request = HttpRequest( - None, - None, - 'https://www.googleapis.com/someapi/v1/collection/?foo=bar', - method='POST', - body=body, - headers={'content-type': 'application/json'}, - methodId=None, - resumable=None) - # Just testing it shouldn't raise an exception. - s = batch._serialize_request(request).splitlines() - - def test_serialize_request_no_body(self): - batch = BatchHttpRequest() - request = HttpRequest( - None, - None, - 'https://www.googleapis.com/someapi/v1/collection/?foo=bar', - method='POST', - body='', - headers={'content-type': 'application/json'}, - methodId=None, - resumable=None) - s = batch._serialize_request(request).splitlines() - self.assertEqual(NO_BODY_EXPECTED.splitlines(), s) - - def test_deserialize_response(self): - batch = BatchHttpRequest() - resp, content = batch._deserialize_response(RESPONSE) - - self.assertEqual(200, resp.status) - self.assertEqual('OK', resp.reason) - self.assertEqual(11, resp.version) - self.assertEqual('{"answer": 42}', content) - - def test_new_id(self): - batch = BatchHttpRequest() - - id_ = batch._new_id() - self.assertEqual('1', id_) - - id_ = batch._new_id() - self.assertEqual('2', id_) - - batch.add(self.request1, request_id='3') - - id_ = batch._new_id() - self.assertEqual('4', id_) - - def test_add(self): - batch = BatchHttpRequest() - batch.add(self.request1, request_id='1') - self.assertRaises(KeyError, batch.add, self.request1, request_id='1') - - def test_add_fail_for_resumable(self): - batch = BatchHttpRequest() - - upload = MediaFileUpload( - datafile('small.png'), chunksize=500, resumable=True) - self.request1.resumable = upload - self.assertRaises(BatchError, batch.add, self.request1, request_id='1') - - def test_execute(self): - batch = BatchHttpRequest() - callbacks = Callbacks() - - batch.add(self.request1, callback=callbacks.f) - batch.add(self.request2, callback=callbacks.f) - http = HttpMockSequence([ - ({'status': '200', - 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'}, - BATCH_RESPONSE), - ]) - batch.execute(http=http) - self.assertEqual({'foo': 42}, callbacks.responses['1']) - self.assertEqual(None, callbacks.exceptions['1']) - self.assertEqual({'baz': 'qux'}, callbacks.responses['2']) - self.assertEqual(None, callbacks.exceptions['2']) - - def test_execute_request_body(self): - batch = BatchHttpRequest() - - batch.add(self.request1) - batch.add(self.request2) - http = HttpMockSequence([ - ({'status': '200', - 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'}, - 'echo_request_body'), - ]) - try: - batch.execute(http=http) - self.fail('Should raise exception') - except BatchError, e: - boundary, _ = e.content.split(None, 1) - self.assertEqual('--', boundary[:2]) - parts = e.content.split(boundary) - self.assertEqual(4, len(parts)) - self.assertEqual('', parts[0]) - self.assertEqual('--', parts[3]) - header = parts[1].splitlines()[1] - self.assertEqual('Content-Type: application/http', header) - - def test_execute_refresh_and_retry_on_401(self): - batch = BatchHttpRequest() - callbacks = Callbacks() - cred_1 = MockCredentials('Foo') - cred_2 = MockCredentials('Bar') - - http = HttpMockSequence([ - ({'status': '200', - 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'}, - BATCH_RESPONSE_WITH_401), - ({'status': '200', - 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'}, - BATCH_SINGLE_RESPONSE), - ]) - - creds_http_1 = HttpMockSequence([]) - cred_1.authorize(creds_http_1) - - creds_http_2 = HttpMockSequence([]) - cred_2.authorize(creds_http_2) - - self.request1.http = creds_http_1 - self.request2.http = creds_http_2 - - batch.add(self.request1, callback=callbacks.f) - batch.add(self.request2, callback=callbacks.f) - batch.execute(http=http) - - self.assertEqual({'foo': 42}, callbacks.responses['1']) - self.assertEqual(None, callbacks.exceptions['1']) - self.assertEqual({'baz': 'qux'}, callbacks.responses['2']) - self.assertEqual(None, callbacks.exceptions['2']) - - self.assertEqual(1, cred_1._refreshed) - self.assertEqual(0, cred_2._refreshed) - - self.assertEqual(1, cred_1._authorized) - self.assertEqual(1, cred_2._authorized) - - self.assertEqual(1, cred_2._applied) - self.assertEqual(2, cred_1._applied) - - def test_http_errors_passed_to_callback(self): - batch = BatchHttpRequest() - callbacks = Callbacks() - cred_1 = MockCredentials('Foo') - cred_2 = MockCredentials('Bar') - - http = HttpMockSequence([ - ({'status': '200', - 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'}, - BATCH_RESPONSE_WITH_401), - ({'status': '200', - 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'}, - BATCH_RESPONSE_WITH_401), - ]) - - creds_http_1 = HttpMockSequence([]) - cred_1.authorize(creds_http_1) - - creds_http_2 = HttpMockSequence([]) - cred_2.authorize(creds_http_2) - - self.request1.http = creds_http_1 - self.request2.http = creds_http_2 - - batch.add(self.request1, callback=callbacks.f) - batch.add(self.request2, callback=callbacks.f) - batch.execute(http=http) - - self.assertEqual(None, callbacks.responses['1']) - self.assertEqual(401, callbacks.exceptions['1'].resp.status) - self.assertEqual( - 'Authorization Required', callbacks.exceptions['1'].resp.reason) - self.assertEqual({u'baz': u'qux'}, callbacks.responses['2']) - self.assertEqual(None, callbacks.exceptions['2']) - - def test_execute_global_callback(self): - callbacks = Callbacks() - batch = BatchHttpRequest(callback=callbacks.f) - - batch.add(self.request1) - batch.add(self.request2) - http = HttpMockSequence([ - ({'status': '200', - 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'}, - BATCH_RESPONSE), - ]) - batch.execute(http=http) - self.assertEqual({'foo': 42}, callbacks.responses['1']) - self.assertEqual({'baz': 'qux'}, callbacks.responses['2']) - - def test_execute_batch_http_error(self): - callbacks = Callbacks() - batch = BatchHttpRequest(callback=callbacks.f) - - batch.add(self.request1) - batch.add(self.request2) - http = HttpMockSequence([ - ({'status': '200', - 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'}, - BATCH_ERROR_RESPONSE), - ]) - batch.execute(http=http) - self.assertEqual({'foo': 42}, callbacks.responses['1']) - expected = ('') - self.assertEqual(expected, str(callbacks.exceptions['2'])) - - -class TestRequestUriTooLong(unittest.TestCase): - - def test_turn_get_into_post(self): - - def _postproc(resp, content): - return content - - http = HttpMockSequence([ - ({'status': '200'}, - 'echo_request_body'), - ({'status': '200'}, - 'echo_request_headers'), - ]) - - # Send a long query parameter. - query = { - 'q': 'a' * MAX_URI_LENGTH + '?&' - } - req = HttpRequest( - http, - _postproc, - 'http://example.com?' + urllib.urlencode(query), - method='GET', - body=None, - headers={}, - methodId='foo', - resumable=None) - - # Query parameters should be sent in the body. - response = req.execute() - self.assertEqual('q=' + 'a' * MAX_URI_LENGTH + '%3F%26', response) - - # Extra headers should be set. - response = req.execute() - self.assertEqual('GET', response['x-http-method-override']) - self.assertEqual(str(MAX_URI_LENGTH + 8), response['content-length']) - self.assertEqual( - 'application/x-www-form-urlencoded', response['content-type']) - - -class TestStreamSlice(unittest.TestCase): - """Test _StreamSlice.""" - - def setUp(self): - self.stream = StringIO.StringIO('0123456789') - - def test_read(self): - s = _StreamSlice(self.stream, 0, 4) - self.assertEqual('', s.read(0)) - self.assertEqual('0', s.read(1)) - self.assertEqual('123', s.read()) - - def test_read_too_much(self): - s = _StreamSlice(self.stream, 1, 4) - self.assertEqual('1234', s.read(6)) - - def test_read_all(self): - s = _StreamSlice(self.stream, 2, 1) - self.assertEqual('2', s.read(-1)) - - -class TestResponseCallback(unittest.TestCase): - """Test adding callbacks to responses.""" - - def test_ensure_response_callback(self): - m = JsonModel() - request = HttpRequest( - None, - m.response, - 'https://www.googleapis.com/someapi/v1/collection/?foo=bar', - method='POST', - body='{}', - headers={'content-type': 'application/json'}) - h = HttpMockSequence([ ({'status': 200}, '{}')]) - responses = [] - def _on_response(resp, responses=responses): - responses.append(resp) - request.add_response_callback(_on_response) - request.execute(http=h) - self.assertEqual(1, len(responses)) - - -if __name__ == '__main__': - logging.getLogger().setLevel(logging.ERROR) - unittest.main() diff --git a/tests/test_json_model.py b/tests/test_json_model.py deleted file mode 100644 index 468db28..0000000 --- a/tests/test_json_model.py +++ /dev/null @@ -1,276 +0,0 @@ -#!/usr/bin/python2.4 -# -# Copyright 2010 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""JSON Model tests - -Unit tests for the JSON model. -""" - -__author__ = 'jcgregorio@google.com (Joe Gregorio)' - -import copy -import os -import unittest -import httplib2 -import googleapiclient.model - -from googleapiclient import __version__ -from googleapiclient.errors import HttpError -from googleapiclient.model import JsonModel -from oauth2client.anyjson import simplejson - -# Python 2.5 requires different modules -try: - from urlparse import parse_qs -except ImportError: - from cgi import parse_qs - - -class Model(unittest.TestCase): - def test_json_no_body(self): - model = JsonModel(data_wrapper=False) - - headers = {} - path_params = {} - query_params = {} - body = None - - headers, unused_params, query, body = model.request( - headers, path_params, query_params, body) - - self.assertEqual(headers['accept'], 'application/json') - self.assertTrue('content-type' not in headers) - self.assertNotEqual(query, '') - self.assertEqual(body, None) - - def test_json_body(self): - model = JsonModel(data_wrapper=False) - - headers = {} - path_params = {} - query_params = {} - body = {} - - headers, unused_params, query, body = model.request( - headers, path_params, query_params, body) - - self.assertEqual(headers['accept'], 'application/json') - self.assertEqual(headers['content-type'], 'application/json') - self.assertNotEqual(query, '') - self.assertEqual(body, '{}') - - def test_json_body_data_wrapper(self): - model = JsonModel(data_wrapper=True) - - headers = {} - path_params = {} - query_params = {} - body = {} - - headers, unused_params, query, body = model.request( - headers, path_params, query_params, body) - - self.assertEqual(headers['accept'], 'application/json') - self.assertEqual(headers['content-type'], 'application/json') - self.assertNotEqual(query, '') - self.assertEqual(body, '{"data": {}}') - - def test_json_body_default_data(self): - """Test that a 'data' wrapper doesn't get added if one is already present.""" - model = JsonModel(data_wrapper=True) - - headers = {} - path_params = {} - query_params = {} - body = {'data': 'foo'} - - headers, unused_params, query, body = model.request( - headers, path_params, query_params, body) - - self.assertEqual(headers['accept'], 'application/json') - self.assertEqual(headers['content-type'], 'application/json') - self.assertNotEqual(query, '') - self.assertEqual(body, '{"data": "foo"}') - - def test_json_build_query(self): - model = JsonModel(data_wrapper=False) - - headers = {} - path_params = {} - query_params = {'foo': 1, 'bar': u'\N{COMET}', - 'baz': ['fe', 'fi', 'fo', 'fum'], # Repeated parameters - 'qux': []} - body = {} - - headers, unused_params, query, body = model.request( - headers, path_params, query_params, body) - - self.assertEqual(headers['accept'], 'application/json') - self.assertEqual(headers['content-type'], 'application/json') - - query_dict = parse_qs(query[1:]) - self.assertEqual(query_dict['foo'], ['1']) - self.assertEqual(query_dict['bar'], [u'\N{COMET}'.encode('utf-8')]) - self.assertEqual(query_dict['baz'], ['fe', 'fi', 'fo', 'fum']) - self.assertTrue('qux' not in query_dict) - self.assertEqual(body, '{}') - - def test_user_agent(self): - model = JsonModel(data_wrapper=False) - - headers = {'user-agent': 'my-test-app/1.23.4'} - path_params = {} - query_params = {} - body = {} - - headers, unused_params, unused_query, body = model.request( - headers, path_params, query_params, body) - - self.assertEqual(headers['user-agent'], - 'my-test-app/1.23.4 google-api-python-client/' + __version__ + - ' (gzip)') - - def test_bad_response(self): - model = JsonModel(data_wrapper=False) - resp = httplib2.Response({'status': '401'}) - resp.reason = 'Unauthorized' - content = '{"error": {"message": "not authorized"}}' - - try: - content = model.response(resp, content) - self.fail('Should have thrown an exception') - except HttpError, e: - self.assertTrue('not authorized' in str(e)) - - resp['content-type'] = 'application/json' - - try: - content = model.response(resp, content) - self.fail('Should have thrown an exception') - except HttpError, e: - self.assertTrue('not authorized' in str(e)) - - def test_good_response(self): - model = JsonModel(data_wrapper=True) - resp = httplib2.Response({'status': '200'}) - resp.reason = 'OK' - content = '{"data": "is good"}' - - content = model.response(resp, content) - self.assertEqual(content, 'is good') - - def test_good_response_wo_data(self): - model = JsonModel(data_wrapper=False) - resp = httplib2.Response({'status': '200'}) - resp.reason = 'OK' - content = '{"foo": "is good"}' - - content = model.response(resp, content) - self.assertEqual(content, {'foo': 'is good'}) - - def test_good_response_wo_data_str(self): - model = JsonModel(data_wrapper=False) - resp = httplib2.Response({'status': '200'}) - resp.reason = 'OK' - content = '"data goes here"' - - content = model.response(resp, content) - self.assertEqual(content, 'data goes here') - - def test_no_content_response(self): - model = JsonModel(data_wrapper=False) - resp = httplib2.Response({'status': '204'}) - resp.reason = 'No Content' - content = '' - - content = model.response(resp, content) - self.assertEqual(content, {}) - - def test_logging(self): - class MockLogging(object): - def __init__(self): - self.info_record = [] - self.debug_record = [] - def info(self, message, *args): - self.info_record.append(message % args) - - def debug(self, message, *args): - self.debug_record.append(message % args) - - class MockResponse(dict): - def __init__(self, items): - super(MockResponse, self).__init__() - self.status = items['status'] - for key, value in items.iteritems(): - self[key] = value - old_logging = googleapiclient.model.logging - googleapiclient.model.logging = MockLogging() - googleapiclient.model.dump_request_response = True - model = JsonModel() - request_body = { - 'field1': 'value1', - 'field2': 'value2' - } - body_string = model.request({}, {}, {}, request_body)[-1] - json_body = simplejson.loads(body_string) - self.assertEqual(request_body, json_body) - - response = {'status': 200, - 'response_field_1': 'response_value_1', - 'response_field_2': 'response_value_2'} - response_body = model.response(MockResponse(response), body_string) - self.assertEqual(request_body, response_body) - self.assertEqual(googleapiclient.model.logging.info_record[:2], - ['--request-start--', - '-headers-start-']) - self.assertTrue('response_field_1: response_value_1' in - googleapiclient.model.logging.info_record) - self.assertTrue('response_field_2: response_value_2' in - googleapiclient.model.logging.info_record) - self.assertEqual(simplejson.loads(googleapiclient.model.logging.info_record[-2]), - request_body) - self.assertEqual(googleapiclient.model.logging.info_record[-1], - '--response-end--') - googleapiclient.model.logging = old_logging - - def test_no_data_wrapper_deserialize(self): - model = JsonModel(data_wrapper=False) - resp = httplib2.Response({'status': '200'}) - resp.reason = 'OK' - content = '{"data": "is good"}' - content = model.response(resp, content) - self.assertEqual(content, {'data': 'is good'}) - - def test_data_wrapper_deserialize(self): - model = JsonModel(data_wrapper=True) - resp = httplib2.Response({'status': '200'}) - resp.reason = 'OK' - content = '{"data": "is good"}' - content = model.response(resp, content) - self.assertEqual(content, 'is good') - - def test_data_wrapper_deserialize_nodata(self): - model = JsonModel(data_wrapper=True) - resp = httplib2.Response({'status': '200'}) - resp.reason = 'OK' - content = '{"atad": "is good"}' - content = model.response(resp, content) - self.assertEqual(content, {'atad': 'is good'}) - - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_oauth2client_jwt.py b/tests/test_jwt.py similarity index 99% rename from tests/test_oauth2client_jwt.py rename to tests/test_jwt.py index f8cf00b..8b17fea 100644 --- a/tests/test_oauth2client_jwt.py +++ b/tests/test_jwt.py @@ -35,7 +35,7 @@ try: except ImportError: from cgi import parse_qs -from googleapiclient.http import HttpMockSequence +from http_mock import HttpMockSequence from oauth2client import crypt from oauth2client.anyjson import simplejson from oauth2client.client import Credentials diff --git a/tests/test_oauth2client_keyring.py b/tests/test_keyring.py similarity index 100% rename from tests/test_oauth2client_keyring.py rename to tests/test_keyring.py diff --git a/tests/test_mocks.py b/tests/test_mocks.py deleted file mode 100644 index 6b2360b..0000000 --- a/tests/test_mocks.py +++ /dev/null @@ -1,150 +0,0 @@ -#!/usr/bin/python2.4 -# -# Copyright 2010 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Mock tests - -Unit tests for the Mocks. -""" - -__author__ = 'jcgregorio@google.com (Joe Gregorio)' - -import httplib2 -import os -import unittest - -from googleapiclient.errors import HttpError -from googleapiclient.errors import UnexpectedBodyError -from googleapiclient.errors import UnexpectedMethodError -from googleapiclient.discovery import build -from googleapiclient.http import RequestMockBuilder -from googleapiclient.http import HttpMock - - -DATA_DIR = os.path.join(os.path.dirname(__file__), 'data') - -def datafile(filename): - return os.path.join(DATA_DIR, filename) - - -class Mocks(unittest.TestCase): - def setUp(self): - self.http = HttpMock(datafile('plus.json'), {'status': '200'}) - self.zoo_http = HttpMock(datafile('zoo.json'), {'status': '200'}) - - def test_default_response(self): - requestBuilder = RequestMockBuilder({}) - plus = build('plus', 'v1', http=self.http, requestBuilder=requestBuilder) - activity = plus.activities().get(activityId='tag:blah').execute() - self.assertEqual({}, activity) - - def test_simple_response(self): - requestBuilder = RequestMockBuilder({ - 'plus.activities.get': (None, '{"foo": "bar"}') - }) - plus = build('plus', 'v1', http=self.http, requestBuilder=requestBuilder) - - activity = plus.activities().get(activityId='tag:blah').execute() - self.assertEqual({"foo": "bar"}, activity) - - def test_unexpected_call(self): - requestBuilder = RequestMockBuilder({}, check_unexpected=True) - - plus = build('plus', 'v1', http=self.http, requestBuilder=requestBuilder) - - try: - plus.activities().get(activityId='tag:blah').execute() - self.fail('UnexpectedMethodError should have been raised') - except UnexpectedMethodError: - pass - - def test_simple_unexpected_body(self): - requestBuilder = RequestMockBuilder({ - 'zoo.animals.insert': (None, '{"data": {"foo": "bar"}}', None) - }) - zoo = build('zoo', 'v1', http=self.zoo_http, requestBuilder=requestBuilder) - - try: - zoo.animals().insert(body='{}').execute() - self.fail('UnexpectedBodyError should have been raised') - except UnexpectedBodyError: - pass - - def test_simple_expected_body(self): - requestBuilder = RequestMockBuilder({ - 'zoo.animals.insert': (None, '{"data": {"foo": "bar"}}', '{}') - }) - zoo = build('zoo', 'v1', http=self.zoo_http, requestBuilder=requestBuilder) - - try: - zoo.animals().insert(body='').execute() - self.fail('UnexpectedBodyError should have been raised') - except UnexpectedBodyError: - pass - - def test_simple_wrong_body(self): - requestBuilder = RequestMockBuilder({ - 'zoo.animals.insert': (None, '{"data": {"foo": "bar"}}', - '{"data": {"foo": "bar"}}') - }) - zoo = build('zoo', 'v1', http=self.zoo_http, requestBuilder=requestBuilder) - - try: - zoo.animals().insert( - body='{"data": {"foo": "blah"}}').execute() - self.fail('UnexpectedBodyError should have been raised') - except UnexpectedBodyError: - pass - - def test_simple_matching_str_body(self): - requestBuilder = RequestMockBuilder({ - 'zoo.animals.insert': (None, '{"data": {"foo": "bar"}}', - '{"data": {"foo": "bar"}}') - }) - zoo = build('zoo', 'v1', http=self.zoo_http, requestBuilder=requestBuilder) - - activity = zoo.animals().insert( - body={'data': {'foo': 'bar'}}).execute() - self.assertEqual({'foo': 'bar'}, activity) - - def test_simple_matching_dict_body(self): - requestBuilder = RequestMockBuilder({ - 'zoo.animals.insert': (None, '{"data": {"foo": "bar"}}', - {'data': {'foo': 'bar'}}) - }) - zoo = build('zoo', 'v1', http=self.zoo_http, requestBuilder=requestBuilder) - - activity = zoo.animals().insert( - body={'data': {'foo': 'bar'}}).execute() - self.assertEqual({'foo': 'bar'}, activity) - - def test_errors(self): - errorResponse = httplib2.Response({'status': 500, 'reason': 'Server Error'}) - requestBuilder = RequestMockBuilder({ - 'plus.activities.list': (errorResponse, '{}') - }) - plus = build('plus', 'v1', http=self.http, requestBuilder=requestBuilder) - - try: - activity = plus.activities().list(collection='public', userId='me').execute() - self.fail('An exception should have been thrown') - except HttpError, e: - self.assertEqual('{}', e.content) - self.assertEqual(500, e.resp.status) - self.assertEqual('Server Error', e.resp.reason) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_model.py b/tests/test_model.py deleted file mode 100644 index 776c7fd..0000000 --- a/tests/test_model.py +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/python2.4 -# -# Copyright 2010 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Model tests - -Unit tests for model utility methods. -""" - -__author__ = 'jcgregorio@google.com (Joe Gregorio)' - -import httplib2 -import unittest - -from googleapiclient.model import makepatch - - -TEST_CASES = [ - # (message, original, modified, expected) - ("Remove an item from an object", - {'a': 1, 'b': 2}, {'a': 1}, {'b': None}), - ("Add an item to an object", - {'a': 1}, {'a': 1, 'b': 2}, {'b': 2}), - ("No changes", - {'a': 1, 'b': 2}, {'a': 1, 'b': 2}, {}), - ("Empty objects", - {}, {}, {}), - ("Modify an item in an object", - {'a': 1, 'b': 2}, {'a': 1, 'b': 3}, {'b': 3}), - ("Change an array", - {'a': 1, 'b': [2, 3]}, {'a': 1, 'b': [2]}, {'b': [2]}), - ("Modify a nested item", - {'a': 1, 'b': {'foo':'bar', 'baz': 'qux'}}, - {'a': 1, 'b': {'foo':'bar', 'baz': 'qaax'}}, - {'b': {'baz': 'qaax'}}), - ("Modify a nested array", - {'a': 1, 'b': [{'foo':'bar', 'baz': 'qux'}]}, - {'a': 1, 'b': [{'foo':'bar', 'baz': 'qaax'}]}, - {'b': [{'foo':'bar', 'baz': 'qaax'}]}), - ("Remove item from a nested array", - {'a': 1, 'b': [{'foo':'bar', 'baz': 'qux'}]}, - {'a': 1, 'b': [{'foo':'bar'}]}, - {'b': [{'foo':'bar'}]}), - ("Remove a nested item", - {'a': 1, 'b': {'foo':'bar', 'baz': 'qux'}}, - {'a': 1, 'b': {'foo':'bar'}}, - {'b': {'baz': None}}) -] - - -class TestPatch(unittest.TestCase): - - def test_patch(self): - for (msg, orig, mod, expected_patch) in TEST_CASES: - self.assertEqual(expected_patch, makepatch(orig, mod), msg=msg) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_oauth2client.py b/tests/test_oauth2client.py index 738bfbe..65ad189 100644 --- a/tests/test_oauth2client.py +++ b/tests/test_oauth2client.py @@ -24,13 +24,12 @@ __author__ = 'jcgregorio@google.com (Joe Gregorio)' import base64 import datetime -import httplib2 import os import unittest import urlparse -from googleapiclient.http import HttpMock -from googleapiclient.http import HttpMockSequence +from http_mock import HttpMock +from http_mock import HttpMockSequence from oauth2client import GOOGLE_REVOKE_URI from oauth2client import GOOGLE_TOKEN_URI from oauth2client.anyjson import simplejson @@ -55,12 +54,29 @@ from oauth2client.client import credentials_from_clientsecrets_and_code from oauth2client.client import credentials_from_code from oauth2client.client import flow_from_clientsecrets from oauth2client.clientsecrets import _loadfile -from test_discovery import assertUrisEqual - DATA_DIR = os.path.join(os.path.dirname(__file__), 'data') +# TODO(craigcitro): This is duplicated from +# googleapiclient.test_discovery; consolidate these definitions. +def assertUrisEqual(testcase, expected, actual): + """Test that URIs are the same, up to reordering of query parameters.""" + expected = urlparse.urlparse(expected) + actual = urlparse.urlparse(actual) + testcase.assertEqual(expected.scheme, actual.scheme) + testcase.assertEqual(expected.netloc, actual.netloc) + testcase.assertEqual(expected.path, actual.path) + testcase.assertEqual(expected.params, actual.params) + testcase.assertEqual(expected.fragment, actual.fragment) + expected_query = urlparse.parse_qs(expected.query) + actual_query = urlparse.parse_qs(actual.query) + for name in expected_query.keys(): + testcase.assertEqual(expected_query[name], actual_query[name]) + for name in actual_query.keys(): + testcase.assertEqual(expected_query[name], actual_query[name]) + + def datafile(filename): return os.path.join(DATA_DIR, filename) diff --git a/tests/test_protobuf_model.py b/tests/test_protobuf_model.py deleted file mode 100644 index b4cde55..0000000 --- a/tests/test_protobuf_model.py +++ /dev/null @@ -1,103 +0,0 @@ -#!/usr/bin/python2.4 -# -# Copyright 2010 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Protocol Buffer Model tests - -Unit tests for the Protocol Buffer model. -""" - -__author__ = 'mmcdonald@google.com (Matt McDonald)' - -import unittest -import httplib2 -import googleapiclient.model - -from googleapiclient.errors import HttpError -from googleapiclient.model import ProtocolBufferModel - -# Python 2.5 requires different modules -try: - from urlparse import parse_qs -except ImportError: - from cgi import parse_qs - - -class MockProtocolBuffer(object): - def __init__(self, data=None): - self.data = data - - def __eq__(self, other): - return self.data == other.data - - @classmethod - def FromString(cls, string): - return cls(string) - - def SerializeToString(self): - return self.data - - -class Model(unittest.TestCase): - def setUp(self): - self.model = ProtocolBufferModel(MockProtocolBuffer) - - def test_no_body(self): - headers = {} - path_params = {} - query_params = {} - body = None - - headers, params, query, body = self.model.request( - headers, path_params, query_params, body) - - self.assertEqual(headers['accept'], 'application/x-protobuf') - self.assertTrue('content-type' not in headers) - self.assertNotEqual(query, '') - self.assertEqual(body, None) - - def test_body(self): - headers = {} - path_params = {} - query_params = {} - body = MockProtocolBuffer('data') - - headers, params, query, body = self.model.request( - headers, path_params, query_params, body) - - self.assertEqual(headers['accept'], 'application/x-protobuf') - self.assertEqual(headers['content-type'], 'application/x-protobuf') - self.assertNotEqual(query, '') - self.assertEqual(body, 'data') - - def test_good_response(self): - resp = httplib2.Response({'status': '200'}) - resp.reason = 'OK' - content = 'data' - - content = self.model.response(resp, content) - self.assertEqual(content, MockProtocolBuffer('data')) - - def test_no_content_response(self): - resp = httplib2.Response({'status': '204'}) - resp.reason = 'No Content' - content = '' - - content = self.model.response(resp, content) - self.assertEqual(content, MockProtocolBuffer()) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_schema.py b/tests/test_schema.py deleted file mode 100644 index 48f123d..0000000 --- a/tests/test_schema.py +++ /dev/null @@ -1,158 +0,0 @@ -# Copyright 2011 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Unit tests for googleapiclient.schema.""" - -__author__ = 'jcgregorio@google.com (Joe Gregorio)' - -import os -import unittest -import StringIO - -from googleapiclient.schema import Schemas -from oauth2client.anyjson import simplejson - - -DATA_DIR = os.path.join(os.path.dirname(__file__), 'data') - - -def datafile(filename): - return os.path.join(DATA_DIR, filename) - -LOAD_FEED = """{ - "items": [ - { - "longVal": 42, - "kind": "zoo#loadValue", - "enumVal": "A String", - "anyVal": "", # Anything will do. - "nullVal": None, - "stringVal": "A String", - "doubleVal": 3.14, - "booleanVal": True or False, # True or False. - }, - ], - "kind": "zoo#loadFeed", - }""" - -class SchemasTest(unittest.TestCase): - def setUp(self): - f = file(datafile('zoo.json')) - discovery = f.read() - f.close() - discovery = simplejson.loads(discovery) - self.sc = Schemas(discovery) - - def test_basic_formatting(self): - self.assertEqual(sorted(LOAD_FEED.splitlines()), - sorted(self.sc.prettyPrintByName('LoadFeed').splitlines())) - - def test_empty_edge_case(self): - self.assertTrue('Unknown type' in self.sc.prettyPrintSchema({})) - - def test_simple_object(self): - self.assertEqual({}, eval(self.sc.prettyPrintSchema({'type': 'object'}))) - - def test_string(self): - self.assertEqual(type(""), type(eval(self.sc.prettyPrintSchema({'type': - 'string'})))) - - def test_integer(self): - self.assertEqual(type(20), type(eval(self.sc.prettyPrintSchema({'type': - 'integer'})))) - - def test_number(self): - self.assertEqual(type(1.2), type(eval(self.sc.prettyPrintSchema({'type': - 'number'})))) - - def test_boolean(self): - self.assertEqual(type(True), type(eval(self.sc.prettyPrintSchema({'type': - 'boolean'})))) - - def test_string_default(self): - self.assertEqual('foo', eval(self.sc.prettyPrintSchema({'type': - 'string', 'default': 'foo'}))) - - def test_integer_default(self): - self.assertEqual(20, eval(self.sc.prettyPrintSchema({'type': - 'integer', 'default': 20}))) - - def test_number_default(self): - self.assertEqual(1.2, eval(self.sc.prettyPrintSchema({'type': - 'number', 'default': 1.2}))) - - def test_boolean_default(self): - self.assertEqual(False, eval(self.sc.prettyPrintSchema({'type': - 'boolean', 'default': False}))) - - def test_null(self): - self.assertEqual(None, eval(self.sc.prettyPrintSchema({'type': 'null'}))) - - def test_any(self): - self.assertEqual('', eval(self.sc.prettyPrintSchema({'type': 'any'}))) - - def test_array(self): - self.assertEqual([{}], eval(self.sc.prettyPrintSchema({'type': 'array', - 'items': {'type': 'object'}}))) - - def test_nested_references(self): - feed = { - 'items': [ { - 'photo': { - 'hash': 'A String', - 'hashAlgorithm': 'A String', - 'filename': 'A String', - 'type': 'A String', - 'size': 42 - }, - 'kind': 'zoo#animal', - 'etag': 'A String', - 'name': 'A String' - } - ], - 'kind': 'zoo#animalFeed', - 'etag': 'A String' - } - - self.assertEqual(feed, eval(self.sc.prettyPrintByName('AnimalFeed'))) - - def test_additional_properties(self): - items = { - 'animals': { - 'a_key': { - 'photo': { - 'hash': 'A String', - 'hashAlgorithm': 'A String', - 'filename': 'A String', - 'type': 'A String', - 'size': 42 - }, - 'kind': 'zoo#animal', - 'etag': 'A String', - 'name': 'A String' - } - }, - 'kind': 'zoo#animalMap', - 'etag': 'A String' - } - - self.assertEqual(items, eval(self.sc.prettyPrintByName('AnimalMap'))) - - def test_unknown_name(self): - self.assertRaises(KeyError, - self.sc.prettyPrintByName, 'UknownSchemaThing') - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_oauth2client_util.py b/tests/test_util.py similarity index 100% rename from tests/test_oauth2client_util.py rename to tests/test_util.py diff --git a/tests/test_oauth2client_xsrfutil.py b/tests/test_xsrfutil.py similarity index 100% rename from tests/test_oauth2client_xsrfutil.py rename to tests/test_xsrfutil.py diff --git a/tox.ini b/tox.ini index aa1b36c..60180e0 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27 +envlist = py26, py27 [testenv] deps = keyring @@ -12,7 +12,7 @@ deps = keyring setenv = PYTHONPATH=../google_appengine [testenv:py26] -commands = nosetests --ignore-files=test_oauth2client_appengine\.py +commands = nosetests --ignore-files=test_appengine\.py [testenv:py27] -commands = nosetests +commands = nosetests --ignore-files=test_appengine\.py