Merge pull request #5 from craigcitro/drop_apiclient
Drop the googleapiclient copy, and cleanup tests.
This commit is contained in:
		| @@ -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" | ||||
| @@ -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) | ||||
|  | ||||
| @@ -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__)) | ||||
| @@ -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 '<HttpError %s when requesting %s returned "%s">' % ( | ||||
|           self.resp.status, self.uri, self._get_reason().strip()) | ||||
|     else: | ||||
|       return '<HttpError %s "%s">' % (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 '<BatchError %s "%s">' % (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)) | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -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 | ||||
| @@ -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 | ||||
| @@ -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 <http://code.google.com/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) | ||||
| @@ -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) | ||||
| @@ -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 | ||||
| @@ -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" | ||||
|      ] | ||||
|     } | ||||
|    } | ||||
|   } | ||||
|  } | ||||
| } | ||||
| @@ -1 +0,0 @@ | ||||
| { | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										1220
									
								
								tests/data/plus.json
									
									
									
									
									
								
							
							
						
						
									
										1220
									
								
								tests/data/plus.json
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 313 B | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 190 B | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 1.3 KiB | 
| @@ -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" | ||||
|      ] | ||||
|     } | ||||
|    } | ||||
|   } | ||||
|  } | ||||
| } | ||||
| @@ -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" | ||||
|      } | ||||
|     } | ||||
|    } | ||||
|   } | ||||
|  } | ||||
| } | ||||
							
								
								
									
										112
									
								
								tests/http_mock.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								tests/http_mock.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| @@ -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 | ||||
| @@ -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' | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -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), '<HttpError 400 when requesting http://example.org returned "country is required">') | ||||
|  | ||||
|   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), '<HttpError 400 "Failed">') | ||||
|  | ||||
|   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), '<HttpError 400 when requesting http://example.org returned "Failure">') | ||||
|  | ||||
|   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), '<HttpError 400 "Failed">') | ||||
|  | ||||
|   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), '<HttpError 400 "Ok">') | ||||
|  | ||||
|   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), '<HttpError 400 "">') | ||||
| @@ -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 | ||||
							
								
								
									
										1004
									
								
								tests/test_http.py
									
									
									
									
									
								
							
							
						
						
									
										1004
									
								
								tests/test_http.py
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -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() | ||||
| @@ -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 | ||||
| @@ -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() | ||||
| @@ -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() | ||||
| @@ -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) | ||||
|  | ||||
|   | ||||
| @@ -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() | ||||
| @@ -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() | ||||
							
								
								
									
										6
									
								
								tox.ini
									
									
									
									
									
								
							
							
						
						
									
										6
									
								
								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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Craig Citro
					Craig Citro