From c8e421c03c1e22f35060229ea1c696a85d9a3dbc Mon Sep 17 00:00:00 2001 From: Joe Gregorio Date: Wed, 6 Jun 2012 14:03:13 -0400 Subject: [PATCH] Remove last OAuth 1.0 vestiges. Clean up comments. Fixes issue #127. Reviewed in http://codereview.appspot.com/6299050/ --- apiclient/contrib/__init__.py | 0 apiclient/contrib/latitude/__init__.py | 0 apiclient/contrib/latitude/future.json | 81 -------- apiclient/contrib/moderator/__init__.py | 0 apiclient/contrib/moderator/future.json | 107 ---------- apiclient/discovery.py | 253 ++++++++++++------------ tests/data/zoo.json | 45 +++++ 7 files changed, 169 insertions(+), 317 deletions(-) delete mode 100644 apiclient/contrib/__init__.py delete mode 100644 apiclient/contrib/latitude/__init__.py delete mode 100644 apiclient/contrib/latitude/future.json delete mode 100644 apiclient/contrib/moderator/__init__.py delete mode 100644 apiclient/contrib/moderator/future.json diff --git a/apiclient/contrib/__init__.py b/apiclient/contrib/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apiclient/contrib/latitude/__init__.py b/apiclient/contrib/latitude/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apiclient/contrib/latitude/future.json b/apiclient/contrib/latitude/future.json deleted file mode 100644 index 7b2bfa0..0000000 --- a/apiclient/contrib/latitude/future.json +++ /dev/null @@ -1,81 +0,0 @@ -{ - "baseUrl": "https://www.googleapis.com/", - "auth": { - "request": { - "url": "https://www.google.com/accounts/OAuthGetRequestToken", - "parameters": { - "xoauth_displayname": { - "parameterType": "query", - "required": false - }, - "domain": { - "parameterType": "query", - "required": true - }, - "scope": { - "parameterType": "query", - "required": true - } - } - }, - "authorize": { - "url": "https://www.google.com/latitude/apps/OAuthAuthorizeToken", - "parameters": { - "oauth_token": { - "parameterType": "query", - "required": true - }, - "iconUrl": { - "parameterType": "query", - "required": false - }, - "domain": { - "parameterType": "query", - "required": true - }, - "scope": { - "parameterType": "query", - "required": true - }, - "location": { - "parameterType": "query", - "required": false - }, - "granularity": { - "parameterType": "query", - "required": false - } - } - }, - "access": { - "url": "https://www.google.com/accounts/OAuthGetAccessToken", - "parameters": { - "domain": { - "parameterType": "query", - "required": true - }, - "scope": { - "parameterType": "query", - "required": true - } - } - } - }, - "resources": { - "currentLocation": { - "methods": { - "delete": {}, - "get": {}, - "insert": {} - } - }, - "location": { - "methods": { - "delete": {}, - "get": {}, - "insert": {}, - "list": {} - } - } - } -} diff --git a/apiclient/contrib/moderator/__init__.py b/apiclient/contrib/moderator/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apiclient/contrib/moderator/future.json b/apiclient/contrib/moderator/future.json deleted file mode 100644 index 106bc06..0000000 --- a/apiclient/contrib/moderator/future.json +++ /dev/null @@ -1,107 +0,0 @@ -{ - "baseUrl": "https://www.googleapis.com/", - "auth": { - "request": { - "url": "https://www.google.com/accounts/OAuthGetRequestToken", - "parameters": { - "xoauth_displayname": { - "parameterType": "query", - "required": false - }, - "domain": { - "parameterType": "query", - "required": false - }, - "scope": { - "parameterType": "query", - "required": true - } - } - }, - "authorize": { - "url": "https://www.google.com/accounts/OAuthAuthorizeToken", - "parameters": { - "oauth_token": { - "parameterType": "query", - "required": true - }, - "iconUrl": { - "parameterType": "query", - "required": false - }, - "domain": { - "parameterType": "query", - "required": false - }, - "scope": { - "parameterType": "query", - "required": true - } - } - }, - "access": { - "url": "https://www.google.com/accounts/OAuthGetAccessToken", - "parameters": { - "domain": { - "parameterType": "query", - "required": false - }, - "scope": { - "parameterType": "query", - "required": true - } - } - } - }, - "resources": { - "profiles": { - "methods": { - "get": {}, - "update": {} - } - }, - "responses": { - "methods": { - "insert": {}, - "list": {} - } - }, - "series": { - "methods": { - "get": {}, - "insert": {}, - "list": {}, - "update": {} - } - }, - "submissions": { - "methods": { - "get": {}, - "insert": {}, - "list": {} - } - }, - "tags": { - "methods": { - "delete": {}, - "insert": {}, - "list": {} - } - }, - "topics": { - "methods": { - "get": {}, - "insert": {}, - "list": {} - } - }, - "votes": { - "methods": { - "get": {}, - "insert": {}, - "list": {}, - "update": {} - } - } - } -} diff --git a/apiclient/discovery.py b/apiclient/discovery.py index 0c44516..3d46758 100644 --- a/apiclient/discovery.py +++ b/apiclient/discovery.py @@ -63,10 +63,10 @@ DISCOVERY_URI = ('https://www.googleapis.com/discovery/v1/apis/' '{api}/{apiVersion}/rest') DEFAULT_METHOD_DOC = 'A description of how to use this function' -# Query parameters that work, but don't appear in discovery -STACK_QUERY_PARAMETERS = ['trace', 'fields', 'pp', 'prettyPrint', 'userIp', - 'userip', 'strict'] +# Parameters accepted by the stack, but not visible via discovery. +STACK_QUERY_PARAMETERS = ['trace', 'pp', 'userip', 'strict'] +# Python reserved words. RESERVED_WORDS = ['and', 'assert', 'break', 'class', 'continue', 'def', 'del', 'elif', 'else', 'except', 'exec', 'finally', 'for', 'from', 'global', 'if', 'import', 'in', 'is', 'lambda', 'not', 'or', @@ -74,17 +74,20 @@ RESERVED_WORDS = ['and', 'assert', 'break', 'class', 'continue', 'def', 'del', 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 name in RESERVED_WORDS: return name + '_' else: return name -def _write_headers(self): - # Utility no-op method for multipart media handling - pass - - def _add_query_parameter(url, name, value): """Adds a query parameter to a url. @@ -112,6 +115,12 @@ 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) @@ -135,29 +144,26 @@ def build(serviceName, 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. + 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 + 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: apiclient.Model, converts to and from the wire format - requestBuilder: apiclient.http.HttpRequest, encapsulator for - an HTTP request + 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: apiclient.Model, converts to and from the wire format. + requestBuilder: apiclient.http.HttpRequest, encapsulator for an HTTP + request. Returns: - A Resource object with methods for interacting with - the service. + A Resource object with methods for interacting with the service. """ params = { 'api': serviceName, @@ -192,17 +198,8 @@ def build(serviceName, logger.error('Failed to parse as JSON: ' + content) raise InvalidJsonError() - filename = os.path.join(os.path.dirname(__file__), 'contrib', - serviceName, 'future.json') - try: - f = file(filename, 'r') - future = f.read() - f.close() - except IOError: - future = None - - return build_from_document(content, discoveryServiceUrl, future, - http, developerKey, model, requestBuilder) + return build_from_document(content, discoveryServiceUrl, http=http, + developerKey=developerKey, model=model, requestBuilder=requestBuilder) def build_from_document( @@ -215,49 +212,37 @@ def build_from_document( 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. + 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, discovery document - base: string, base URI for all HTTP requests, usually the discovery URI - future: string, discovery document with future capabilities - auth_discovery: dict, information about the authentication the API supports + service: string, discovery document. + base: string, base URI for all HTTP requests, usually the discovery URI. + 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. + 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. + A Resource object with methods for interacting with the service. """ + # future is no longer used. + future = {} + service = simplejson.loads(service) base = urlparse.urljoin(base, service['basePath']) - if future: - future = simplejson.loads(future) - auth_discovery = future.get('auth', {}) - else: - future = {} - auth_discovery = {} schema = Schemas(service) if model is None: features = service.get('features', []) model = JsonModel('dataWrapper' in features) resource = createResource(http, base, model, requestBuilder, developerKey, - service, future, schema) - - def auth_method(): - """Discovery information about the authentication the API uses.""" - return auth_discovery - - setattr(resource, 'auth_discovery', auth_method) + service, service, schema) return resource @@ -292,6 +277,7 @@ def _cast(value, schema_type): else: return str(value) + MULTIPLIERS = { "KB": 2 ** 10, "MB": 2 ** 20, @@ -301,7 +287,14 @@ MULTIPLIERS = { def _media_size_to_long(maxSize): - """Convert a string media size, such as 10GB or 3TB into an integer.""" + """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 0 units = maxSize[-2:].upper() @@ -313,7 +306,28 @@ def _media_size_to_long(maxSize): def createResource(http, baseUrl, model, requestBuilder, - developerKey, resourceDesc, futureDesc, schema): + 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: apiclient.Model, converts to and from the wire format. + requestBuilder: class or callable that instantiates an + apiclient.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. + + Returns: + An instance of Resource with all the methods attached for interacting with + that resource. + """ class Resource(object): """A class for interacting with a resource.""" @@ -325,7 +339,16 @@ def createResource(http, baseUrl, model, requestBuilder, self._developerKey = developerKey self._requestBuilder = requestBuilder - def createMethod(theclass, methodName, methodDesc, futureDesc): + def createMethod(theclass, methodName, methodDesc, rootDesc): + """Creates a method for attaching to a Resource. + + Args: + theclass: type, the class to attach methods to. + 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. + """ methodName = _fix_method_name(methodName) pathUrl = methodDesc['path'] httpMethod = methodDesc['httpMethod'] @@ -345,6 +368,12 @@ def createResource(http, baseUrl, model, requestBuilder, if 'parameters' not in methodDesc: methodDesc['parameters'] = {} + + # Add in the parameters common to all methods. + for name, desc in rootDesc.get('parameters', {}).iteritems(): + methodDesc['parameters'][name] = desc + + # Add in undocumented query parameters. for name in STACK_QUERY_PARAMETERS: methodDesc['parameters'][name] = { 'type': 'string', @@ -407,6 +436,7 @@ def createResource(http, baseUrl, model, requestBuilder, query_params.remove(name) 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 argmap: raise TypeError('Got an unexpected keyword argument "%s"' % name) @@ -550,9 +580,15 @@ def createResource(http, baseUrl, model, requestBuilder, docs = [methodDesc.get('description', DEFAULT_METHOD_DOC), '\n\n'] if len(argmap) > 0: docs.append('Args:\n') + + # Skip undocumented params and params common to all methods. + skip_parameters = rootDesc.get('parameters', {}).keys() + skip_parameters.append(STACK_QUERY_PARAMETERS) + for arg in argmap.iterkeys(): - if arg in STACK_QUERY_PARAMETERS: + if arg in skip_parameters: continue + repeated = '' if arg in repeated_params: repeated = ' (repeated)' @@ -583,56 +619,21 @@ def createResource(http, baseUrl, model, requestBuilder, setattr(method, '__doc__', ''.join(docs)) setattr(theclass, methodName, method) - def createNextMethodFromFuture(theclass, methodName, methodDesc, futureDesc): - """ This is a legacy method, as only Buzz and Moderator use the future.json - functionality for generating _next methods. It will be kept around as long - as those API versions are around, but no new APIs should depend upon it. + def createNextMethod(theclass, methodName, methodDesc, rootDesc): + """Creates any _next methods for attaching to a Resource. + + The _next methods allow for easy iteration through list() responses. + + Args: + theclass: type, the class to attach methods to. + 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. """ methodName = _fix_method_name(methodName) methodId = methodDesc['id'] + '.next' - def methodNext(self, previous): - """Retrieve the next page of results. - - Takes a single argument, 'body', which is the results - from the last call, and returns the next set of items - in the collection. - - Returns: - None if there are no more items in the collection. - """ - if futureDesc['type'] != 'uri': - raise UnknownLinkType(futureDesc['type']) - - try: - p = previous - for key in futureDesc['location']: - p = p[key] - url = p - except (KeyError, TypeError): - return None - - url = _add_query_parameter(url, 'key', self._developerKey) - - headers = {} - headers, params, query, body = self._model.request(headers, {}, {}, None) - - logger.info('URL being requested: %s' % url) - resp, content = self._http.request(url, method='GET', headers=headers) - - return self._requestBuilder(self._http, - self._model.response, - url, - method='GET', - headers=headers, - methodId=methodId) - - setattr(theclass, methodName, methodNext) - - def createNextMethod(theclass, methodName, methodDesc, futureDesc): - methodName = _fix_method_name(methodName) - methodId = methodDesc['id'] + '.next' - def methodNext(self, previous_request, previous_response): """Retrieves the next page of results. @@ -673,41 +674,35 @@ def createResource(http, baseUrl, model, requestBuilder, # Add basic methods to Resource if 'methods' in resourceDesc: for methodName, methodDesc in resourceDesc['methods'].iteritems(): - if futureDesc: - future = futureDesc['methods'].get(methodName, {}) - else: - future = None - createMethod(Resource, methodName, methodDesc, future) + createMethod(Resource, methodName, methodDesc, rootDesc) # Add in nested resources if 'resources' in resourceDesc: - def createResourceMethod(theclass, methodName, methodDesc, futureDesc): + def createResourceMethod(theclass, methodName, methodDesc, rootDesc): + """Create a method on the Resource to access a nested Resource. + + Args: + theclass: type, the class to attach methods to. + 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. + """ methodName = _fix_method_name(methodName) def methodResource(self): return createResource(self._http, self._baseUrl, self._model, self._requestBuilder, self._developerKey, - methodDesc, futureDesc, schema) + methodDesc, rootDesc, schema) setattr(methodResource, '__doc__', 'A collection resource.') setattr(methodResource, '__is_resource__', True) setattr(theclass, methodName, methodResource) for methodName, methodDesc in resourceDesc['resources'].iteritems(): - if futureDesc and 'resources' in futureDesc: - future = futureDesc['resources'].get(methodName, {}) - else: - future = {} - createResourceMethod(Resource, methodName, methodDesc, future) + createResourceMethod(Resource, methodName, methodDesc, rootDesc) - # Add _next() methods to Resource - if futureDesc and 'methods' in futureDesc: - for methodName, methodDesc in futureDesc['methods'].iteritems(): - if 'next' in methodDesc and methodName in resourceDesc['methods']: - createNextMethodFromFuture(Resource, methodName + '_next', - resourceDesc['methods'][methodName], - methodDesc['next']) # Add _next() methods # Look for response bodies in schema that contain nextPageToken, and methods # that take a pageToken parameter. diff --git a/tests/data/zoo.json b/tests/data/zoo.json index 7ef0f24..b4d7cfe 100644 --- a/tests/data/zoo.json +++ b/tests/data/zoo.json @@ -5,6 +5,51 @@ "description": "Zoo API used for Apiary testing", "basePath": "/zoo/", "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" ],