From 002ffa0e3ec0c3ec9d732b1783fad76c81e6d857 Mon Sep 17 00:00:00 2001 From: "sheyang@chromium.org" Date: Fri, 17 Apr 2015 16:24:32 +0000 Subject: [PATCH] Remove apiclient package BUG= Review URL: https://codereview.chromium.org/1091163002 git-svn-id: svn://svn.chromium.org/chrome/trunk/tools/depot_tools@294877 0039d316-1c4b-4281-b951-d872f2087c98 --- .../google_api_python_client/.gitignore | 8 - .../google_api_python_client/.gitmodules | 0 .../google_api_python_client/.hgignore | 24 - .../google_api_python_client/CHANGELOG | 167 -- third_party/google_api_python_client/LICENSE | 22 - .../google_api_python_client/MANIFEST.in | 6 - third_party/google_api_python_client/Makefile | 47 - .../google_api_python_client/README.chromium | 6 - .../google_api_python_client/README.md | 32 - .../google_api_python_client/__init__.py | 0 .../apiclient/__init__.py | 40 - .../google_api_python_client/describe.py | 390 ---- .../expandsymlinks.py | 58 - .../googleapiclient/__init__.py | 15 - .../googleapiclient/channel.py | 285 --- .../googleapiclient/discovery.py | 995 ---------- .../googleapiclient/errors.py | 140 -- .../googleapiclient/http.py | 1614 ----------------- .../googleapiclient/mimeparse.py | 172 -- .../googleapiclient/model.py | 383 ---- .../googleapiclient/sample_tools.py | 102 -- .../googleapiclient/schema.py | 311 ---- .../google_api_python_client/samples-index.py | 246 --- third_party/google_api_python_client/setup.py | 94 - .../google_api_python_client/sitecustomize.py | 12 - .../static/Credentials.png | Bin 116881 -> 0 bytes third_party/google_api_python_client/tox.ini | 18 - third_party/uritemplate/.gitignore | 5 - third_party/uritemplate/.gitmodules | 3 - third_party/uritemplate/.travis.yml | 11 - third_party/uritemplate/MAINTAINERS.rst | 14 - third_party/uritemplate/MANIFEST.in | 1 - third_party/uritemplate/README.chromium | 6 - third_party/uritemplate/README.rst | 71 - third_party/uritemplate/__init__.py | 0 third_party/uritemplate/setup.py | 37 - .../uritemplate/uritemplate/__init__.py | 265 --- 37 files changed, 5600 deletions(-) delete mode 100644 third_party/google_api_python_client/.gitignore delete mode 100644 third_party/google_api_python_client/.gitmodules delete mode 100644 third_party/google_api_python_client/.hgignore delete mode 100644 third_party/google_api_python_client/CHANGELOG delete mode 100644 third_party/google_api_python_client/LICENSE delete mode 100644 third_party/google_api_python_client/MANIFEST.in delete mode 100644 third_party/google_api_python_client/Makefile delete mode 100644 third_party/google_api_python_client/README.chromium delete mode 100644 third_party/google_api_python_client/README.md delete mode 100644 third_party/google_api_python_client/__init__.py delete mode 100644 third_party/google_api_python_client/apiclient/__init__.py delete mode 100755 third_party/google_api_python_client/describe.py delete mode 100644 third_party/google_api_python_client/expandsymlinks.py delete mode 100644 third_party/google_api_python_client/googleapiclient/__init__.py delete mode 100644 third_party/google_api_python_client/googleapiclient/channel.py delete mode 100644 third_party/google_api_python_client/googleapiclient/discovery.py delete mode 100644 third_party/google_api_python_client/googleapiclient/errors.py delete mode 100644 third_party/google_api_python_client/googleapiclient/http.py delete mode 100644 third_party/google_api_python_client/googleapiclient/mimeparse.py delete mode 100644 third_party/google_api_python_client/googleapiclient/model.py delete mode 100644 third_party/google_api_python_client/googleapiclient/sample_tools.py delete mode 100644 third_party/google_api_python_client/googleapiclient/schema.py delete mode 100644 third_party/google_api_python_client/samples-index.py delete mode 100644 third_party/google_api_python_client/setup.py delete mode 100644 third_party/google_api_python_client/sitecustomize.py delete mode 100644 third_party/google_api_python_client/static/Credentials.png delete mode 100644 third_party/google_api_python_client/tox.ini delete mode 100644 third_party/uritemplate/.gitignore delete mode 100644 third_party/uritemplate/.gitmodules delete mode 100644 third_party/uritemplate/.travis.yml delete mode 100644 third_party/uritemplate/MAINTAINERS.rst delete mode 100644 third_party/uritemplate/MANIFEST.in delete mode 100644 third_party/uritemplate/README.chromium delete mode 100644 third_party/uritemplate/README.rst delete mode 100644 third_party/uritemplate/__init__.py delete mode 100755 third_party/uritemplate/setup.py delete mode 100755 third_party/uritemplate/uritemplate/__init__.py diff --git a/third_party/google_api_python_client/.gitignore b/third_party/google_api_python_client/.gitignore deleted file mode 100644 index ddb969d34..000000000 --- a/third_party/google_api_python_client/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Build artifacts -*.py[cod] -google_api_python_client.egg-info/ -build/ -dist/ - -# Test files -.tox/ diff --git a/third_party/google_api_python_client/.gitmodules b/third_party/google_api_python_client/.gitmodules deleted file mode 100644 index e69de29bb..000000000 diff --git a/third_party/google_api_python_client/.hgignore b/third_party/google_api_python_client/.hgignore deleted file mode 100644 index dc9cf7826..000000000 --- a/third_party/google_api_python_client/.hgignore +++ /dev/null @@ -1,24 +0,0 @@ -syntax: glob - -*.pyc -*.pyc-2.4 -*.dat -.*.swp -*/.git/* -*/.cache/* -.gitignore -.tox -samples/buzz/*.dat -samples/moderator/*.dat -htmlcov/* -.coverage -database.sqlite3 -build/* -googlecode_upload.py -google_api_python_client.egg-info/* -dist/* -snapshot/* -MANIFEST -.project -.pydevproject -.settings/* diff --git a/third_party/google_api_python_client/CHANGELOG b/third_party/google_api_python_client/CHANGELOG deleted file mode 100644 index af6d6698e..000000000 --- a/third_party/google_api_python_client/CHANGELOG +++ /dev/null @@ -1,167 +0,0 @@ -v1.3.1 - Version 1.3.1 - - Quick release for a fix around aliasing in v1.3. - -v1.3 - Version 1.3 - - Add support for the Google Application Default Credentials. - Require python 2.6 as a minimum version. - Update several API samples. - Finish splitting out oauth2client repo and update tests. - Various doc cleanup and bugfixes. - - Two important notes: - * We've added `googleapiclient` as the primary suggested import - name, and kept `apiclient` as an alias, in order to have a more - appropriate import name. At some point, we will remove `apiclient` - as an alias. - * Due to an issue around in-place upgrades for Python packages, - it's not possible to do an upgrade from version 1.2 to 1.3. Instead, - setup.py attempts to detect this and prevents it. Simply remove - the previous version and reinstall to fix this. - -v1.2 - Version 1.2 - - The use of the gflags library is now deprecated, and is no longer a - dependency. If you are still using the oauth2client.tools.run() function - then include gflags as a dependency of your application or switch to - oauth2client.tools.run_flow. - Samples have been updated to use the new apiclient.sample_tools, and no - longer use gflags. - Added support for the experimental Object Change Notification, as found in - the Cloud Storage API. - The oauth2client App Engine decorators are now threadsafe. - - - Use the following redirects feature of httplib2 where it returns the - ultimate URL after a series of redirects to avoid multiple hops for every - resumable media upload request. - - Updated AdSense Management API samples to V1.3 - - Add option to automatically retry requests. - - Ability to list registered keys in multistore_file. - - User-agent must contain (gzip). - - The 'method' parameter for httplib2 is not positional. This would cause - spurious warnings in the logging. - - Making OAuth2Decorator more extensible. Fixes Issue 256. - - Update AdExchange Buyer API examples to version v1.2. - - -v1.1 - Version 1.1 - - Add PEM support to SignedJWTAssertionCredentials (used to only support - PKCS12 formatted keys). Note that if you use PEM formatted keys you can use - PyCrypto 2.6 or later instead of OpenSSL. - - Allow deserialized discovery docs to be passed to build_from_document(). - - - Make ResumableUploadError derive from HttpError. - - Many changes to move all the closures in apiclient.discovery into real - - classes and objects. - - Make from_json behavior inheritable. - - Expose the full token response in OAuth2Client and OAuth2Decorator. - - Handle reasons that are None. - - Added support for NDB based storing of oauth2client objects. - - Update grant_type for AssertionCredentials. - - Adding a .revoke() to Credentials. Closes issue 98. - - Modify oauth2client.multistore_file to store and retrieve credentials - using an arbitrary key. - - Don't accept 403 challenges by default for auth challenges. - - Set httplib2.RETRIES to 1. - - Consolidate handling of scopes. - - Upgrade to httplib2 version 0.8. - - Allow setting the response_type in OAuth2WebServerFlow. - - Ensure that dataWrapper feature is checked before using the 'data' value. - - HMAC verification does not use a constant time algorithm. - -v1.0 - Version 1.0 - - - Changes to the code for running tests and building releases. - -v1.0c3 - Version 1.0 Release Candidate 3 - - - In samples and oauth2 decorator, escape untrusted content before displaying it. - - Do not allow credentials files to be symlinks. - - Add XSRF protection to oauth2decorator callback 'state'. - - Handle uploading chunked media by stream. - - Handle passing streams directly to httplib2. - - Add support for Google Compute Engine service accounts. - - Flows no longer need to be saved between uses. - - Change GET to POST if URI is too long. Fixes issue #96. - - Add a keyring based Storage. - - More robust picking up JSON error responses. - - Make batch errors align with normal errors. - - Add a Google Compute sample. - - Token refresh to work with 'old' GData API - - Loading of client_secrets JSON file backed by a cache. - - Switch to new discovery path parameters. - - Add support for additionalProperties when printing schema'd objects. - - Fix media upload parameter names. Reviewed in http://codereview.appspot.com/6374062/ - - oauth2client support for URL-encoded format of exchange token response (e.g. Facebook) - - Build cleaner and easier to read docs for dynamic surfaces. - -v1.0c2 - Version 1.0 Release Candidate 2 - - - Parameter values of None should be treated as missing. Fixes issue #144. - - Distribute the samples separately from the library source. Fixes issue #155. - - Move all remaining samples over to client_secrets.json. Fixes issue #156. - - Make locked_file.py understand win32file primitives for better awesomeness. - -v1.0c1 - Version 1.0 Release Candidate 1 - - - Documentation for the library has switched to epydoc: - http://google-api-python-client.googlecode.com/hg/docs/epy/index.html - - Many improvements for media support: - * Added media download support, including resumable downloads. - * Better handling of streams that report their size as 0. - * Update Media Upload to include io.Base and also fix some bugs. - - OAuth bug fixes and improvements. - * Remove OAuth 1.0 support. - * Added credentials_from_code and credentials_from_clientsecrets_and_code. - * Make oauth2client support Windows-friendly locking. - * Fix bug in StorageByKeyName. - * Fix None handling in Django fields. Reviewed in http://codereview.appspot.com/6298084/. Fixes issue #128. - - Add epydoc generated docs. Reviewed in http://codereview.appspot.com/6305043/ - - Move to PEP386 compliant version numbers. - - New and updated samples - * Ad Exchange Buyer API v1 code samples. - * Automatically generate Samples wiki page from README files. - * Update Google Prediction samples. - * Add a Tasks sample that demonstrates Service accounts. - * new analytics api samples. Reviewed here: http://codereview.appspot.com/5494058/ - - Convert all inline samples to the Farm API for consistency. - -v1.0beta8 - - Updated meda upload support. - - Many fixes for batch requests. - - Better handling for requests that don't require a body. - - Fix issues with Google App Engine Python 2.7 runtime. - - Better support for proxies. - - All Storages now have a .delete() method. - - Important changes which might break your code: - * apiclient.anyjson has moved to oauth2client.anyjson. - * Some calls, for example, taskqueue().lease() used to require a parameter - named body. In this new release only methods that really need to send a body - require a body parameter, and so you may get errors about an unknown - 'body' parameter in your call. The solution is to remove the unneeded - body={} parameter. - -v1.0beta7 - - Support for batch requests. http://code.google.com/p/google-api-python-client/wiki/Batch - - Support for media upload. http://code.google.com/p/google-api-python-client/wiki/MediaUpload - - Better handling for APIs that return something other than JSON. - - Major cleanup and consolidation of the samples. - - Bug fixes and other enhancements: - 72 Defect Appengine OAuth2Decorator: Convert redirect address to string - 22 Defect Better error handling for unknown service name or version - 48 Defect StorageByKeyName().get() has side effects - 50 Defect Need sample client code for Admin Audit API - 28 Defect better comments for app engine sample Nov 9 - 63 Enhancement Let OAuth2Decorator take a list of scope - diff --git a/third_party/google_api_python_client/LICENSE b/third_party/google_api_python_client/LICENSE deleted file mode 100644 index 2987b3b95..000000000 --- a/third_party/google_api_python_client/LICENSE +++ /dev/null @@ -1,22 +0,0 @@ - Copyright 2014 Google Inc. All Rights Reserved. - - 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. - -Dependent Modules -================= - -This code has the following dependencies -above and beyond the Python standard library: - -uritemplates - Apache License 2.0 -httplib2 - MIT License diff --git a/third_party/google_api_python_client/MANIFEST.in b/third_party/google_api_python_client/MANIFEST.in deleted file mode 100644 index cc692b3b7..000000000 --- a/third_party/google_api_python_client/MANIFEST.in +++ /dev/null @@ -1,6 +0,0 @@ -recursive-include apiclient *.json *.py -include CHANGELOG -include LICENSE -include README -include FAQ -include setpath.sh diff --git a/third_party/google_api_python_client/Makefile b/third_party/google_api_python_client/Makefile deleted file mode 100644 index 6366e77dc..000000000 --- a/third_party/google_api_python_client/Makefile +++ /dev/null @@ -1,47 +0,0 @@ -pep8: - find googleapiclient samples -name "*.py" | xargs pep8 --ignore=E111,E202 - -APP_ENGINE_PATH=../google_appengine - -test: - tox - -.PHONY: coverage -coverage: - coverage erase - find tests -name "test_*.py" | xargs --max-args=1 coverage run -a runtests.py - coverage report - coverage html - -.PHONY: docs -docs: - cd docs; ./build - mkdir -p docs/dyn - python describe.py - -.PHONY: wiki -wiki: - python samples-index.py > ../google-api-python-client.wiki/SampleApps.wiki - -.PHONY: prerelease -prerelease: - -rm -rf dist/ - -sudo rm -rf dist/ - -rm -rf snapshot/ - -sudo rm -rf snapshot/ - # ./tools/gae-zip-creator.sh - python expandsymlinks.py - cd snapshot; python setup.py clean - cd snapshot; python setup.py sdist --formats=gztar,zip - cd snapshot; tar czf google-api-python-client-samples-$(shell python setup.py --version).tar.gz samples - cd snapshot; zip -r google-api-python-client-samples-$(shell python setup.py --version).zip samples - - -.PHONY: release -release: prerelease - @echo "This target will upload a new release to PyPi and code.google.com hosting." - @echo "Are you sure you want to proceed? (yes/no)" - @read yn; if [ yes -ne $(yn) ]; then exit 1; fi - @echo "Here we go..." - cd snapshot; python setup.py sdist --formats=gztar,zip register upload - \ No newline at end of file diff --git a/third_party/google_api_python_client/README.chromium b/third_party/google_api_python_client/README.chromium deleted file mode 100644 index eb7a00d0d..000000000 --- a/third_party/google_api_python_client/README.chromium +++ /dev/null @@ -1,6 +0,0 @@ -URL: https://github.com/google/google-api-python-client -Version: v1.3.1 -Revision: 49d45a6c3318b75e551c3022020f46c78655f365 -License: Apache License, Version 2.0 (the "License") - -No local changes diff --git a/third_party/google_api_python_client/README.md b/third_party/google_api_python_client/README.md deleted file mode 100644 index c22221c07..000000000 --- a/third_party/google_api_python_client/README.md +++ /dev/null @@ -1,32 +0,0 @@ -# About -This is the Python client library for Google's discovery based APIs. To get started, please see the [full documentation for this library](http://google.github.io/google-api-python-client). Additionally, [dynamically generated documentation](http://api-python-client-doc.appspot.com/) is available for all of the APIs supported by this library. - - -# Installation -To install, simply use `pip` or `easy_install`: - -```bash -$ pip install --upgrade google-api-python-client -``` -or -```bash -$ easy_install --upgrade google-api-python-client -``` - -See the [Developers Guide](https://developers.google.com/api-client-library/python/start/get_started) for more detailed instructions and additional documentation. - -# Python Version -Python 2.6 or 2.7 is required. Python 3.x is not yet supported. - -# Third Party Libraries and Dependencies -The following libraries will be installed when you install the client library: -* [httplib2](https://github.com/jcgregorio/httplib2) -* [uri-templates](https://github.com/uri-templates/uritemplate-py) - -For development you will also need the following libraries: -* [WebTest](http://pythonpaste.org/webtest/) -* [pycrypto](https://pypi.python.org/pypi/pycrypto) -* [pyopenssl](https://pypi.python.org/pypi/pyOpenSSL) - -# Contributing -Please see the [contributing page](http://google.github.io/google-api-python-client/contributing.html) for more information. In particular, we love pull requests - but please make sure to sign the contributor license agreement. diff --git a/third_party/google_api_python_client/__init__.py b/third_party/google_api_python_client/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/third_party/google_api_python_client/apiclient/__init__.py b/third_party/google_api_python_client/apiclient/__init__.py deleted file mode 100644 index 5efb142e0..000000000 --- a/third_party/google_api_python_client/apiclient/__init__.py +++ /dev/null @@ -1,40 +0,0 @@ -"""Retain apiclient as an alias for googleapiclient.""" - -import googleapiclient - -try: - import oauth2client -except ImportError: - raise RuntimeError( - 'Previous version of google-api-python-client detected; due to a ' - 'packaging issue, we cannot perform an in-place upgrade. To repair, ' - 'remove and reinstall this package, along with oauth2client and ' - 'uritemplate. One can do this with pip via\n' - ' pip install -I google-api-python-client' - ) - -from googleapiclient import channel -from googleapiclient import discovery -from googleapiclient import errors -from googleapiclient import http -from googleapiclient import mimeparse -from googleapiclient import model -from googleapiclient import sample_tools -from googleapiclient import schema - -__version__ = googleapiclient.__version__ - -_SUBMODULES = { - 'channel': channel, - 'discovery': discovery, - 'errors': errors, - 'http': http, - 'mimeparse': mimeparse, - 'model': model, - 'sample_tools': sample_tools, - 'schema': schema, -} - -import sys -for module_name, module in _SUBMODULES.iteritems(): - sys.modules['apiclient.%s' % module_name] = module diff --git a/third_party/google_api_python_client/describe.py b/third_party/google_api_python_client/describe.py deleted file mode 100755 index 5dcac904c..000000000 --- a/third_party/google_api_python_client/describe.py +++ /dev/null @@ -1,390 +0,0 @@ -#!/usr/bin/python -# -# Copyright 2014 Google Inc. All Rights Reserved. -# -# 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. - -"""Create documentation for generate API surfaces. - -Command-line tool that creates documentation for all APIs listed in discovery. -The documentation is generated from a combination of the discovery document and -the generated API surface itself. -""" - -__author__ = 'jcgregorio@google.com (Joe Gregorio)' - -import argparse -import json -import os -import re -import string -import sys - -from googleapiclient.discovery import DISCOVERY_URI -from googleapiclient.discovery import build -from googleapiclient.discovery import build_from_document -import httplib2 -import uritemplate - -CSS = """ -""" - -METHOD_TEMPLATE = """
- $name($params) -
$doc
-
-""" - -COLLECTION_LINK = """

- $name() -

-

Returns the $name Resource.

-""" - -METHOD_LINK = """

- $name($params)

-

$firstline

""" - -BASE = 'docs/dyn' - -DIRECTORY_URI = 'https://www.googleapis.com/discovery/v1/apis?preferred=true' - -parser = argparse.ArgumentParser(description=__doc__) - -parser.add_argument('--discovery_uri_template', default=DISCOVERY_URI, - help='URI Template for discovery.') - -parser.add_argument('--discovery_uri', default='', - help=('URI of discovery document. If supplied then only ' - 'this API will be documented.')) - -parser.add_argument('--directory_uri', default=DIRECTORY_URI, - help=('URI of directory document. Unused if --discovery_uri' - ' is supplied.')) - -parser.add_argument('--dest', default=BASE, - help='Directory name to write documents into.') - - - -def safe_version(version): - """Create a safe version of the verion string. - - Needed so that we can distinguish between versions - and sub-collections in URIs. I.e. we don't want - adsense_v1.1 to refer to the '1' collection in the v1 - version of the adsense api. - - Args: - version: string, The version string. - Returns: - The string with '.' replaced with '_'. - """ - - return version.replace('.', '_') - - -def unsafe_version(version): - """Undoes what safe_version() does. - - See safe_version() for the details. - - - Args: - version: string, The safe version string. - Returns: - The string with '_' replaced with '.'. - """ - - return version.replace('_', '.') - - -def method_params(doc): - """Document the parameters of a method. - - Args: - doc: string, The method's docstring. - - Returns: - The method signature as a string. - """ - doclines = doc.splitlines() - if 'Args:' in doclines: - begin = doclines.index('Args:') - if 'Returns:' in doclines[begin+1:]: - end = doclines.index('Returns:', begin) - args = doclines[begin+1: end] - else: - args = doclines[begin+1:] - - parameters = [] - for line in args: - m = re.search('^\s+([a-zA-Z0-9_]+): (.*)', line) - if m is None: - continue - pname = m.group(1) - desc = m.group(2) - if '(required)' not in desc: - pname = pname + '=None' - parameters.append(pname) - parameters = ', '.join(parameters) - else: - parameters = '' - return parameters - - -def method(name, doc): - """Documents an individual method. - - Args: - name: string, Name of the method. - doc: string, The methods docstring. - """ - - params = method_params(doc) - return string.Template(METHOD_TEMPLATE).substitute( - name=name, params=params, doc=doc) - - -def breadcrumbs(path, root_discovery): - """Create the breadcrumb trail to this page of documentation. - - Args: - path: string, Dot separated name of the resource. - root_discovery: Deserialized discovery document. - - Returns: - HTML with links to each of the parent resources of this resource. - """ - parts = path.split('.') - - crumbs = [] - accumulated = [] - - for i, p in enumerate(parts): - prefix = '.'.join(accumulated) - # The first time through prefix will be [], so we avoid adding in a - # superfluous '.' to prefix. - if prefix: - prefix += '.' - display = p - if i == 0: - display = root_discovery.get('title', display) - crumbs.append('%s' % (prefix + p, display)) - accumulated.append(p) - - return ' . '.join(crumbs) - - -def document_collection(resource, path, root_discovery, discovery, css=CSS): - """Document a single collection in an API. - - Args: - resource: Collection or service being documented. - path: string, Dot separated name of the resource. - root_discovery: Deserialized discovery document. - discovery: Deserialized discovery document, but just the portion that - describes the resource. - css: string, The CSS to include in the generated file. - """ - collections = [] - methods = [] - resource_name = path.split('.')[-2] - html = [ - '', - css, - '

%s

' % breadcrumbs(path[:-1], root_discovery), - '

Instance Methods

' - ] - - # Which methods are for collections. - for name in dir(resource): - if not name.startswith('_') and callable(getattr(resource, name)): - if hasattr(getattr(resource, name), '__is_resource__'): - collections.append(name) - else: - methods.append(name) - - - # TOC - if collections: - for name in collections: - if not name.startswith('_') and callable(getattr(resource, name)): - href = path + name + '.html' - html.append(string.Template(COLLECTION_LINK).substitute( - href=href, name=name)) - - if methods: - for name in methods: - if not name.startswith('_') and callable(getattr(resource, name)): - doc = getattr(resource, name).__doc__ - params = method_params(doc) - firstline = doc.splitlines()[0] - html.append(string.Template(METHOD_LINK).substitute( - name=name, params=params, firstline=firstline)) - - if methods: - html.append('

Method Details

') - for name in methods: - dname = name.rsplit('_')[0] - html.append(method(name, getattr(resource, name).__doc__)) - - html.append('') - - return '\n'.join(html) - - -def document_collection_recursive(resource, path, root_discovery, discovery): - - html = document_collection(resource, path, root_discovery, discovery) - - f = open(os.path.join(FLAGS.dest, path + 'html'), 'w') - f.write(html.encode('utf-8')) - f.close() - - for name in dir(resource): - if (not name.startswith('_') - and callable(getattr(resource, name)) - and hasattr(getattr(resource, name), '__is_resource__')): - dname = name.rsplit('_')[0] - collection = getattr(resource, name)() - document_collection_recursive(collection, path + name + '.', root_discovery, - discovery['resources'].get(dname, {})) - -def document_api(name, version): - """Document the given API. - - Args: - name: string, Name of the API. - version: string, Version of the API. - """ - service = build(name, version) - response, content = http.request( - uritemplate.expand( - FLAGS.discovery_uri_template, { - 'api': name, - 'apiVersion': version}) - ) - discovery = json.loads(content) - - version = safe_version(version) - - document_collection_recursive( - service, '%s_%s.' % (name, version), discovery, discovery) - - -def document_api_from_discovery_document(uri): - """Document the given API. - - Args: - uri: string, URI of discovery document. - """ - http = httplib2.Http() - response, content = http.request(FLAGS.discovery_uri) - discovery = json.loads(content) - - service = build_from_document(discovery) - - name = discovery['version'] - version = safe_version(discovery['version']) - - document_collection_recursive( - service, '%s_%s.' % (name, version), discovery, discovery) - - -if __name__ == '__main__': - FLAGS = parser.parse_args(sys.argv[1:]) - if FLAGS.discovery_uri: - document_api_from_discovery_document(FLAGS.discovery_uri) - else: - http = httplib2.Http() - resp, content = http.request( - FLAGS.directory_uri, - headers={'X-User-IP': '0.0.0.0'}) - if resp.status == 200: - directory = json.loads(content)['items'] - for api in directory: - document_api(api['name'], api['version']) - else: - sys.exit("Failed to load the discovery document.") diff --git a/third_party/google_api_python_client/expandsymlinks.py b/third_party/google_api_python_client/expandsymlinks.py deleted file mode 100644 index 82136221b..000000000 --- a/third_party/google_api_python_client/expandsymlinks.py +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/python2.4 -# -*- coding: utf-8 -*- -# -# Copyright 2014 Google Inc. All Rights Reserved. -# -# 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 files from source to dest expanding symlinks along the way. -""" - -from shutil import copytree - -import argparse -import sys - - -# Ignore these files and directories when copying over files into the snapshot. -IGNORE = set(['.hg', 'httplib2', 'oauth2', 'simplejson', 'static']) - -# In addition to the above files also ignore these files and directories when -# copying over samples into the snapshot. -IGNORE_IN_SAMPLES = set(['googleapiclient', 'oauth2client', 'uritemplate']) - -parser = argparse.ArgumentParser(description=__doc__) - -parser.add_argument('--source', default='.', - help='Directory name to copy from.') - -parser.add_argument('--dest', default='snapshot', - help='Directory name to copy to.') - - -def _ignore(path, names): - retval = set() - if path != '.': - retval = retval.union(IGNORE_IN_SAMPLES.intersection(names)) - retval = retval.union(IGNORE.intersection(names)) - return retval - - -def main(): - copytree(FLAGS.source, FLAGS.dest, symlinks=True, - ignore=_ignore) - - -if __name__ == '__main__': - FLAGS = parser.parse_args(sys.argv[1:]) - main() diff --git a/third_party/google_api_python_client/googleapiclient/__init__.py b/third_party/google_api_python_client/googleapiclient/__init__.py deleted file mode 100644 index 1e1a6cf6d..000000000 --- a/third_party/google_api_python_client/googleapiclient/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright 2014 Google Inc. All Rights Reserved. -# -# 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.3.1" diff --git a/third_party/google_api_python_client/googleapiclient/channel.py b/third_party/google_api_python_client/googleapiclient/channel.py deleted file mode 100644 index 68a3b8919..000000000 --- a/third_party/google_api_python_client/googleapiclient/channel.py +++ /dev/null @@ -1,285 +0,0 @@ -"""Channel notifications support. - -Classes and functions to support channel subscriptions and notifications -on those channels. - -Notes: - - This code is based on experimental APIs and is subject to change. - - Notification does not do deduplication of notification ids, that's up to - the receiver. - - Storing the Channel between calls is up to the caller. - - -Example setting up a channel: - - # Create a new channel that gets notifications via webhook. - channel = new_webhook_channel("https://example.com/my_web_hook") - - # Store the channel, keyed by 'channel.id'. Store it before calling the - # watch method because notifications may start arriving before the watch - # method returns. - ... - - resp = service.objects().watchAll( - bucket="some_bucket_id", body=channel.body()).execute() - channel.update(resp) - - # Store the channel, keyed by 'channel.id'. Store it after being updated - # since the resource_id value will now be correct, and that's needed to - # stop a subscription. - ... - - -An example Webhook implementation using webapp2. Note that webapp2 puts -headers in a case insensitive dictionary, as headers aren't guaranteed to -always be upper case. - - id = self.request.headers[X_GOOG_CHANNEL_ID] - - # Retrieve the channel by id. - channel = ... - - # Parse notification from the headers, including validating the id. - n = notification_from_headers(channel, self.request.headers) - - # Do app specific stuff with the notification here. - if n.resource_state == 'sync': - # Code to handle sync state. - elif n.resource_state == 'exists': - # Code to handle the exists state. - elif n.resource_state == 'not_exists': - # Code to handle the not exists state. - - -Example of unsubscribing. - - service.channels().stop(channel.body()) -""" - -import datetime -import uuid - -from googleapiclient import errors -from ...oauth2client import util - - -# The unix time epoch starts at midnight 1970. -EPOCH = datetime.datetime.utcfromtimestamp(0) - -# Map the names of the parameters in the JSON channel description to -# the parameter names we use in the Channel class. -CHANNEL_PARAMS = { - 'address': 'address', - 'id': 'id', - 'expiration': 'expiration', - 'params': 'params', - 'resourceId': 'resource_id', - 'resourceUri': 'resource_uri', - 'type': 'type', - 'token': 'token', - } - -X_GOOG_CHANNEL_ID = 'X-GOOG-CHANNEL-ID' -X_GOOG_MESSAGE_NUMBER = 'X-GOOG-MESSAGE-NUMBER' -X_GOOG_RESOURCE_STATE = 'X-GOOG-RESOURCE-STATE' -X_GOOG_RESOURCE_URI = 'X-GOOG-RESOURCE-URI' -X_GOOG_RESOURCE_ID = 'X-GOOG-RESOURCE-ID' - - -def _upper_header_keys(headers): - new_headers = {} - for k, v in headers.iteritems(): - new_headers[k.upper()] = v - return new_headers - - -class Notification(object): - """A Notification from a Channel. - - Notifications are not usually constructed directly, but are returned - from functions like notification_from_headers(). - - Attributes: - message_number: int, The unique id number of this notification. - state: str, The state of the resource being monitored. - uri: str, The address of the resource being monitored. - resource_id: str, The unique identifier of the version of the resource at - this event. - """ - @util.positional(5) - def __init__(self, message_number, state, resource_uri, resource_id): - """Notification constructor. - - Args: - message_number: int, The unique id number of this notification. - state: str, The state of the resource being monitored. Can be one - of "exists", "not_exists", or "sync". - resource_uri: str, The address of the resource being monitored. - resource_id: str, The identifier of the watched resource. - """ - self.message_number = message_number - self.state = state - self.resource_uri = resource_uri - self.resource_id = resource_id - - -class Channel(object): - """A Channel for notifications. - - Usually not constructed directly, instead it is returned from helper - functions like new_webhook_channel(). - - Attributes: - type: str, The type of delivery mechanism used by this channel. For - example, 'web_hook'. - id: str, A UUID for the channel. - token: str, An arbitrary string associated with the channel that - is delivered to the target address with each event delivered - over this channel. - address: str, The address of the receiving entity where events are - delivered. Specific to the channel type. - expiration: int, The time, in milliseconds from the epoch, when this - channel will expire. - params: dict, A dictionary of string to string, with additional parameters - controlling delivery channel behavior. - resource_id: str, An opaque id that identifies the resource that is - being watched. Stable across different API versions. - resource_uri: str, The canonicalized ID of the watched resource. - """ - - @util.positional(5) - def __init__(self, type, id, token, address, expiration=None, - params=None, resource_id="", resource_uri=""): - """Create a new Channel. - - In user code, this Channel constructor will not typically be called - manually since there are functions for creating channels for each specific - type with a more customized set of arguments to pass. - - Args: - type: str, The type of delivery mechanism used by this channel. For - example, 'web_hook'. - id: str, A UUID for the channel. - token: str, An arbitrary string associated with the channel that - is delivered to the target address with each event delivered - over this channel. - address: str, The address of the receiving entity where events are - delivered. Specific to the channel type. - expiration: int, The time, in milliseconds from the epoch, when this - channel will expire. - params: dict, A dictionary of string to string, with additional parameters - controlling delivery channel behavior. - resource_id: str, An opaque id that identifies the resource that is - being watched. Stable across different API versions. - resource_uri: str, The canonicalized ID of the watched resource. - """ - self.type = type - self.id = id - self.token = token - self.address = address - self.expiration = expiration - self.params = params - self.resource_id = resource_id - self.resource_uri = resource_uri - - def body(self): - """Build a body from the Channel. - - Constructs a dictionary that's appropriate for passing into watch() - methods as the value of body argument. - - Returns: - A dictionary representation of the channel. - """ - result = { - 'id': self.id, - 'token': self.token, - 'type': self.type, - 'address': self.address - } - if self.params: - result['params'] = self.params - if self.resource_id: - result['resourceId'] = self.resource_id - if self.resource_uri: - result['resourceUri'] = self.resource_uri - if self.expiration: - result['expiration'] = self.expiration - - return result - - def update(self, resp): - """Update a channel with information from the response of watch(). - - When a request is sent to watch() a resource, the response returned - from the watch() request is a dictionary with updated channel information, - such as the resource_id, which is needed when stopping a subscription. - - Args: - resp: dict, The response from a watch() method. - """ - for json_name, param_name in CHANNEL_PARAMS.iteritems(): - value = resp.get(json_name) - if value is not None: - setattr(self, param_name, value) - - -def notification_from_headers(channel, headers): - """Parse a notification from the webhook request headers, validate - the notification, and return a Notification object. - - Args: - channel: Channel, The channel that the notification is associated with. - headers: dict, A dictionary like object that contains the request headers - from the webhook HTTP request. - - Returns: - A Notification object. - - Raises: - errors.InvalidNotificationError if the notification is invalid. - ValueError if the X-GOOG-MESSAGE-NUMBER can't be converted to an int. - """ - headers = _upper_header_keys(headers) - channel_id = headers[X_GOOG_CHANNEL_ID] - if channel.id != channel_id: - raise errors.InvalidNotificationError( - 'Channel id mismatch: %s != %s' % (channel.id, channel_id)) - else: - message_number = int(headers[X_GOOG_MESSAGE_NUMBER]) - state = headers[X_GOOG_RESOURCE_STATE] - resource_uri = headers[X_GOOG_RESOURCE_URI] - resource_id = headers[X_GOOG_RESOURCE_ID] - return Notification(message_number, state, resource_uri, resource_id) - - -@util.positional(2) -def new_webhook_channel(url, token=None, expiration=None, params=None): - """Create a new webhook Channel. - - Args: - url: str, URL to post notifications to. - token: str, An arbitrary string associated with the channel that - is delivered to the target address with each notification delivered - over this channel. - expiration: datetime.datetime, A time in the future when the channel - should expire. Can also be None if the subscription should use the - default expiration. Note that different services may have different - limits on how long a subscription lasts. Check the response from the - watch() method to see the value the service has set for an expiration - time. - params: dict, Extra parameters to pass on channel creation. Currently - not used for webhook channels. - """ - expiration_ms = 0 - if expiration: - delta = expiration - EPOCH - expiration_ms = delta.microseconds/1000 + ( - delta.seconds + delta.days*24*3600)*1000 - if expiration_ms < 0: - expiration_ms = 0 - - return Channel('web_hook', str(uuid.uuid4()), - token, url, expiration=expiration_ms, - params=params) - diff --git a/third_party/google_api_python_client/googleapiclient/discovery.py b/third_party/google_api_python_client/googleapiclient/discovery.py deleted file mode 100644 index 3ddac5712..000000000 --- a/third_party/google_api_python_client/googleapiclient/discovery.py +++ /dev/null @@ -1,995 +0,0 @@ -# Copyright 2014 Google Inc. All Rights Reserved. -# -# 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 StringIO -import copy -from email.generator import Generator -from email.mime.multipart import MIMEMultipart -from email.mime.nonmultipart import MIMENonMultipart -import json -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 -from ... import httplib2 -import mimeparse -from ... 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.client import GoogleCredentials -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, - credentials=None): - """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. - credentials: oauth2client.Credentials, credentials to be used for - authentication. - - 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: GET %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 = json.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, - credentials=credentials) - - -@positional(1) -def build_from_document( - service, - base=None, - future=None, - http=None, - developerKey=None, - model=None, - requestBuilder=HttpRequest, - credentials=None): - """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. - credentials: object, credentials to be used for authentication. - - Returns: - A Resource object with methods for interacting with the service. - """ - - # future is no longer used. - future = {} - - if isinstance(service, basestring): - service = json.loads(service) - base = urlparse.urljoin(service['rootUrl'], service['servicePath']) - schema = Schemas(service) - - if credentials: - # If credentials were passed in, we could have two cases: - # 1. the scopes were specified, in which case the given credentials - # are used for authorizing the http; - # 2. the scopes were not provided (meaning the Application Default - # Credentials are to be used). In this case, the Application Default - # Credentials are built and used instead of the original credentials. - # If there are no scopes found (meaning the given service requires no - # authentication), there is no authorization of the http. - if (isinstance(credentials, GoogleCredentials) and - credentials.create_scoped_required()): - scopes = service.get('auth', {}).get('oauth2', {}).get('scopes', {}) - if scopes: - credentials = credentials.create_scoped(scopes.keys()) - else: - # No need to authorize the http object - # if the service does not require authentication. - credentials = None - - if credentials: - http = credentials.authorize(http) - - 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) - # encode the body: note that we can't use `as_string`, because - # it plays games with `From ` lines. - fp = StringIO.StringIO() - g = Generator(fp, mangle_from_=False) - g.flatten(msgRoot, unixfrom=False) - body = fp.getvalue() - - multipart_boundary = msgRoot.get_boundary() - headers['content-type'] = ('multipart/related; ' - 'boundary="%s"') % multipart_boundary - url = _add_query_parameter(url, 'uploadType', 'multipart') - - logger.info('URL being requested: %s %s' % (httpMethod,url)) - return self._requestBuilder(self._http, - model.response, - url, - method=httpMethod, - body=body, - headers=headers, - methodId=methodId, - resumable=resumable) - - 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 %s' % (methodName,uri)) - - return request - - return (methodName, methodNext) - - -class Resource(object): - """A class for interacting with a resource.""" - - def __init__(self, http, baseUrl, model, requestBuilder, developerKey, - resourceDesc, rootDesc, schema): - """Build a Resource from the API description. - - Args: - http: httplib2.Http, Object to make http requests with. - baseUrl: string, base URL for the API. All requests are relative to this - URI. - model: googleapiclient.Model, converts to and from the wire format. - requestBuilder: class or callable that instantiates an - googleapiclient.HttpRequest object. - developerKey: string, key obtained from - https://code.google.com/apis/console - resourceDesc: object, section of deserialized discovery document that - describes a resource. Note that the top level discovery document - is considered a resource. - rootDesc: object, the entire deserialized discovery document. - schema: object, mapping of schema names to schema descriptions. - """ - self._dynamic_attrs = [] - - self._http = http - self._baseUrl = baseUrl - self._model = model - self._developerKey = developerKey - self._requestBuilder = requestBuilder - self._resourceDesc = resourceDesc - self._rootDesc = rootDesc - self._schema = schema - - self._set_service_methods() - - def _set_dynamic_attr(self, attr_name, value): - """Sets an instance attribute and tracks it in a list of dynamic attributes. - - Args: - attr_name: string; The name of the attribute to be set - value: The value being set on the object and tracked in the dynamic cache. - """ - self._dynamic_attrs.append(attr_name) - self.__dict__[attr_name] = value - - def __getstate__(self): - """Trim the state down to something that can be pickled. - - Uses the fact that the instance variable _dynamic_attrs holds attrs that - will be wiped and restored on pickle serialization. - """ - state_dict = copy.copy(self.__dict__) - for dynamic_attr in self._dynamic_attrs: - del state_dict[dynamic_attr] - del state_dict['_dynamic_attrs'] - return state_dict - - def __setstate__(self, state): - """Reconstitute the state of the object from being pickled. - - Uses the fact that the instance variable _dynamic_attrs holds attrs that - will be wiped and restored on pickle serialization. - """ - self.__dict__.update(state) - self._dynamic_attrs = [] - self._set_service_methods() - - def _set_service_methods(self): - self._add_basic_methods(self._resourceDesc, self._rootDesc, self._schema) - self._add_nested_resources(self._resourceDesc, self._rootDesc, self._schema) - self._add_next_methods(self._resourceDesc, self._schema) - - def _add_basic_methods(self, resourceDesc, rootDesc, schema): - # Add basic methods to Resource - if 'methods' in resourceDesc: - for methodName, methodDesc in resourceDesc['methods'].iteritems(): - fixedMethodName, method = createMethod( - methodName, methodDesc, rootDesc, schema) - self._set_dynamic_attr(fixedMethodName, - method.__get__(self, self.__class__)) - # Add in _media methods. The functionality of the attached method will - # change when it sees that the method name ends in _media. - if methodDesc.get('supportsMediaDownload', False): - fixedMethodName, method = createMethod( - methodName + '_media', methodDesc, rootDesc, schema) - self._set_dynamic_attr(fixedMethodName, - method.__get__(self, self.__class__)) - - def _add_nested_resources(self, resourceDesc, rootDesc, schema): - # Add in nested resources - if 'resources' in resourceDesc: - - def createResourceMethod(methodName, methodDesc): - """Create a method on the Resource to access a nested Resource. - - Args: - methodName: string, name of the method to use. - methodDesc: object, fragment of deserialized discovery document that - describes the method. - """ - methodName = fix_method_name(methodName) - - def methodResource(self): - return Resource(http=self._http, baseUrl=self._baseUrl, - model=self._model, developerKey=self._developerKey, - requestBuilder=self._requestBuilder, - resourceDesc=methodDesc, rootDesc=rootDesc, - schema=schema) - - setattr(methodResource, '__doc__', 'A collection resource.') - setattr(methodResource, '__is_resource__', True) - - return (methodName, methodResource) - - for methodName, methodDesc in resourceDesc['resources'].iteritems(): - fixedMethodName, method = createResourceMethod(methodName, methodDesc) - self._set_dynamic_attr(fixedMethodName, - method.__get__(self, self.__class__)) - - def _add_next_methods(self, resourceDesc, schema): - # Add _next() methods - # Look for response bodies in schema that contain nextPageToken, and methods - # that take a pageToken parameter. - if 'methods' in resourceDesc: - for methodName, methodDesc in resourceDesc['methods'].iteritems(): - if 'response' in methodDesc: - responseSchema = methodDesc['response'] - if '$ref' in responseSchema: - responseSchema = schema.get(responseSchema['$ref']) - hasNextPageToken = 'nextPageToken' in responseSchema.get('properties', - {}) - hasPageToken = 'pageToken' in methodDesc.get('parameters', {}) - if hasNextPageToken and hasPageToken: - fixedMethodName, method = createNextMethod(methodName + '_next') - self._set_dynamic_attr(fixedMethodName, - method.__get__(self, self.__class__)) diff --git a/third_party/google_api_python_client/googleapiclient/errors.py b/third_party/google_api_python_client/googleapiclient/errors.py deleted file mode 100644 index a1999fd50..000000000 --- a/third_party/google_api_python_client/googleapiclient/errors.py +++ /dev/null @@ -1,140 +0,0 @@ -#!/usr/bin/python2.4 -# -# Copyright 2014 Google Inc. All Rights Reserved. -# -# 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)' - -import json - -from ...oauth2client import util - - -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 = json.loads(self.content) - reason = data['error']['message'] - except (ValueError, KeyError): - pass - if reason is None: - reason = '' - return reason - - def __repr__(self): - if self.uri: - return '' % ( - self.resp.status, self.uri, self._get_reason().strip()) - else: - return '' % (self.resp.status, self._get_reason()) - - __str__ = __repr__ - - -class InvalidJsonError(Error): - """The JSON returned could not be parsed.""" - pass - - -class UnknownFileType(Error): - """File type unknown or unexpected.""" - pass - - -class UnknownLinkType(Error): - """Link type unknown or unexpected.""" - pass - - -class UnknownApiNameOrVersion(Error): - """No API with that name and version exists.""" - pass - - -class UnacceptableMimeTypeError(Error): - """That is an unacceptable mimetype for this operation.""" - pass - - -class MediaUploadSizeError(Error): - """Media is larger than the method can accept.""" - pass - - -class ResumableUploadError(HttpError): - """Error occured during resumable upload.""" - pass - - -class InvalidChunkSizeError(Error): - """The given chunksize is not valid.""" - pass - -class InvalidNotificationError(Error): - """The channel Notification is invalid.""" - pass - -class BatchError(HttpError): - """Error occured during batch operations.""" - - @util.positional(2) - def __init__(self, reason, resp=None, content=None): - self.resp = resp - self.content = content - self.reason = reason - - def __repr__(self): - return '' % (self.resp.status, self.reason) - - __str__ = __repr__ - - -class UnexpectedMethodError(Error): - """Exception raised by RequestMockBuilder on unexpected calls.""" - - @util.positional(1) - def __init__(self, methodId=None): - """Constructor for an UnexpectedMethodError.""" - super(UnexpectedMethodError, self).__init__( - 'Received unexpected call %s' % methodId) - - -class UnexpectedBodyError(Error): - """Exception raised by RequestMockBuilder on unexpected bodies.""" - - def __init__(self, expected, provided): - """Constructor for an UnexpectedMethodError.""" - super(UnexpectedBodyError, self).__init__( - 'Expected: [%s] - Provided: [%s]' % (expected, provided)) diff --git a/third_party/google_api_python_client/googleapiclient/http.py b/third_party/google_api_python_client/googleapiclient/http.py deleted file mode 100644 index 863827933..000000000 --- a/third_party/google_api_python_client/googleapiclient/http.py +++ /dev/null @@ -1,1614 +0,0 @@ -# Copyright 2014 Google Inc. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Classes to encapsulate a single HTTP request. - -The classes implement a command pattern, with every -object supporting an execute() method that does the -actuall HTTP request. -""" - -__author__ = 'jcgregorio@google.com (Joe Gregorio)' - -import StringIO -import base64 -import copy -import gzip -import httplib2 -import json -import logging -import mimeparse -import mimetypes -import os -import random -import sys -import time -import urllib -import urlparse -import uuid - -from email.generator import Generator -from email.mime.multipart import MIMEMultipart -from email.mime.nonmultipart import MIMENonMultipart -from email.parser import FeedParser -from errors import BatchError -from errors import HttpError -from errors import InvalidChunkSizeError -from errors import ResumableUploadError -from errors import UnexpectedBodyError -from errors import UnexpectedMethodError -from model import JsonModel -from ...oauth2client import util - - -DEFAULT_CHUNK_SIZE = 512*1024 - -MAX_URI_LENGTH = 2048 - - -class MediaUploadProgress(object): - """Status of a resumable upload.""" - - def __init__(self, resumable_progress, total_size): - """Constructor. - - Args: - resumable_progress: int, bytes sent so far. - total_size: int, total bytes in complete upload, or None if the total - upload size isn't known ahead of time. - """ - self.resumable_progress = resumable_progress - self.total_size = total_size - - def progress(self): - """Percent of upload completed, as a float. - - Returns: - the percentage complete as a float, returning 0.0 if the total size of - the upload is unknown. - """ - if self.total_size is not None: - return float(self.resumable_progress) / float(self.total_size) - else: - return 0.0 - - -class MediaDownloadProgress(object): - """Status of a resumable download.""" - - def __init__(self, resumable_progress, total_size): - """Constructor. - - Args: - resumable_progress: int, bytes received so far. - total_size: int, total bytes in complete download. - """ - self.resumable_progress = resumable_progress - self.total_size = total_size - - def progress(self): - """Percent of download completed, as a float. - - Returns: - the percentage complete as a float, returning 0.0 if the total size of - the download is unknown. - """ - if self.total_size is not None: - return float(self.resumable_progress) / float(self.total_size) - else: - return 0.0 - - -class MediaUpload(object): - """Describes a media object to upload. - - Base class that defines the interface of MediaUpload subclasses. - - Note that subclasses of MediaUpload may allow you to control the chunksize - when uploading a media object. It is important to keep the size of the chunk - as large as possible to keep the upload efficient. Other factors may influence - the size of the chunk you use, particularly if you are working in an - environment where individual HTTP requests may have a hardcoded time limit, - such as under certain classes of requests under Google App Engine. - - Streams are io.Base compatible objects that support seek(). Some MediaUpload - subclasses support using streams directly to upload data. Support for - streaming may be indicated by a MediaUpload sub-class and if appropriate for a - platform that stream will be used for uploading the media object. The support - for streaming is indicated by has_stream() returning True. The stream() method - should return an io.Base object that supports seek(). On platforms where the - underlying httplib module supports streaming, for example Python 2.6 and - later, the stream will be passed into the http library which will result in - less memory being used and possibly faster uploads. - - If you need to upload media that can't be uploaded using any of the existing - MediaUpload sub-class then you can sub-class MediaUpload for your particular - needs. - """ - - def chunksize(self): - """Chunk size for resumable uploads. - - Returns: - Chunk size in bytes. - """ - raise NotImplementedError() - - def mimetype(self): - """Mime type of the body. - - Returns: - Mime type. - """ - return 'application/octet-stream' - - def size(self): - """Size of upload. - - Returns: - Size of the body, or None of the size is unknown. - """ - return None - - def resumable(self): - """Whether this upload is resumable. - - Returns: - True if resumable upload or False. - """ - return False - - def getbytes(self, begin, end): - """Get bytes from the media. - - Args: - begin: int, offset from beginning of file. - length: int, number of bytes to read, starting at begin. - - Returns: - A string of bytes read. May be shorter than length if EOF was reached - first. - """ - raise NotImplementedError() - - def has_stream(self): - """Does the underlying upload support a streaming interface. - - Streaming means it is an io.IOBase subclass that supports seek, i.e. - seekable() returns True. - - Returns: - True if the call to stream() will return an instance of a seekable io.Base - subclass. - """ - return False - - def stream(self): - """A stream interface to the data being uploaded. - - Returns: - The returned value is an io.IOBase subclass that supports seek, i.e. - seekable() returns True. - """ - raise NotImplementedError() - - @util.positional(1) - def _to_json(self, strip=None): - """Utility function for creating a JSON representation of a MediaUpload. - - Args: - strip: array, An array of names of members to not include in the JSON. - - Returns: - string, a JSON representation of this instance, suitable to pass to - from_json(). - """ - t = type(self) - d = copy.copy(self.__dict__) - if strip is not None: - for member in strip: - del d[member] - d['_class'] = t.__name__ - d['_module'] = t.__module__ - return json.dumps(d) - - def to_json(self): - """Create a JSON representation of an instance of MediaUpload. - - Returns: - string, a JSON representation of this instance, suitable to pass to - from_json(). - """ - return self._to_json() - - @classmethod - def new_from_json(cls, s): - """Utility class method to instantiate a MediaUpload subclass from a JSON - representation produced by to_json(). - - Args: - s: string, JSON from to_json(). - - Returns: - An instance of the subclass of MediaUpload that was serialized with - to_json(). - """ - data = json.loads(s) - # Find and call the right classmethod from_json() to restore the object. - module = data['_module'] - m = __import__(module, fromlist=module.split('.')[:-1]) - kls = getattr(m, data['_class']) - from_json = getattr(kls, 'from_json') - return from_json(s) - - -class MediaIoBaseUpload(MediaUpload): - """A MediaUpload for a io.Base objects. - - Note that the Python file object is compatible with io.Base and can be used - with this class also. - - fh = io.BytesIO('...Some data to upload...') - media = MediaIoBaseUpload(fh, mimetype='image/png', - chunksize=1024*1024, resumable=True) - farm.animals().insert( - id='cow', - name='cow.png', - media_body=media).execute() - - Depending on the platform you are working on, you may pass -1 as the - chunksize, which indicates that the entire file should be uploaded in a single - request. If the underlying platform supports streams, such as Python 2.6 or - later, then this can be very efficient as it avoids multiple connections, and - also avoids loading the entire file into memory before sending it. Note that - Google App Engine has a 5MB limit on request size, so you should never set - your chunksize larger than 5MB, or to -1. - """ - - @util.positional(3) - def __init__(self, fd, mimetype, chunksize=DEFAULT_CHUNK_SIZE, - resumable=False): - """Constructor. - - Args: - fd: io.Base or file object, The source of the bytes to upload. MUST be - opened in blocking mode, do not use streams opened in non-blocking mode. - The given stream must be seekable, that is, it must be able to call - seek() on fd. - mimetype: string, Mime-type of the file. - chunksize: int, File will be uploaded in chunks of this many bytes. Only - used if resumable=True. Pass in a value of -1 if the file is to be - uploaded as a single chunk. Note that Google App Engine has a 5MB limit - on request size, so you should never set your chunksize larger than 5MB, - or to -1. - resumable: bool, True if this is a resumable upload. False means upload - in a single request. - """ - super(MediaIoBaseUpload, self).__init__() - self._fd = fd - self._mimetype = mimetype - if not (chunksize == -1 or chunksize > 0): - raise InvalidChunkSizeError() - self._chunksize = chunksize - self._resumable = resumable - - self._fd.seek(0, os.SEEK_END) - self._size = self._fd.tell() - - def chunksize(self): - """Chunk size for resumable uploads. - - Returns: - Chunk size in bytes. - """ - return self._chunksize - - def mimetype(self): - """Mime type of the body. - - Returns: - Mime type. - """ - return self._mimetype - - def size(self): - """Size of upload. - - Returns: - Size of the body, or None of the size is unknown. - """ - return self._size - - def resumable(self): - """Whether this upload is resumable. - - Returns: - True if resumable upload or False. - """ - return self._resumable - - def getbytes(self, begin, length): - """Get bytes from the media. - - Args: - begin: int, offset from beginning of file. - length: int, number of bytes to read, starting at begin. - - Returns: - A string of bytes read. May be shorted than length if EOF was reached - first. - """ - self._fd.seek(begin) - return self._fd.read(length) - - def has_stream(self): - """Does the underlying upload support a streaming interface. - - Streaming means it is an io.IOBase subclass that supports seek, i.e. - seekable() returns True. - - Returns: - True if the call to stream() will return an instance of a seekable io.Base - subclass. - """ - return True - - def stream(self): - """A stream interface to the data being uploaded. - - Returns: - The returned value is an io.IOBase subclass that supports seek, i.e. - seekable() returns True. - """ - return self._fd - - def to_json(self): - """This upload type is not serializable.""" - raise NotImplementedError('MediaIoBaseUpload is not serializable.') - - -class MediaFileUpload(MediaIoBaseUpload): - """A MediaUpload for a file. - - Construct a MediaFileUpload and pass as the media_body parameter of the - method. For example, if we had a service that allowed uploading images: - - - media = MediaFileUpload('cow.png', mimetype='image/png', - chunksize=1024*1024, resumable=True) - farm.animals().insert( - id='cow', - name='cow.png', - media_body=media).execute() - - Depending on the platform you are working on, you may pass -1 as the - chunksize, which indicates that the entire file should be uploaded in a single - request. If the underlying platform supports streams, such as Python 2.6 or - later, then this can be very efficient as it avoids multiple connections, and - also avoids loading the entire file into memory before sending it. Note that - Google App Engine has a 5MB limit on request size, so you should never set - your chunksize larger than 5MB, or to -1. - """ - - @util.positional(2) - def __init__(self, filename, mimetype=None, chunksize=DEFAULT_CHUNK_SIZE, - resumable=False): - """Constructor. - - Args: - filename: string, Name of the file. - mimetype: string, Mime-type of the file. If None then a mime-type will be - guessed from the file extension. - chunksize: int, File will be uploaded in chunks of this many bytes. Only - used if resumable=True. Pass in a value of -1 if the file is to be - uploaded in a single chunk. Note that Google App Engine has a 5MB limit - on request size, so you should never set your chunksize larger than 5MB, - or to -1. - resumable: bool, True if this is a resumable upload. False means upload - in a single request. - """ - self._filename = filename - fd = open(self._filename, 'rb') - if mimetype is None: - (mimetype, encoding) = mimetypes.guess_type(filename) - super(MediaFileUpload, self).__init__(fd, mimetype, chunksize=chunksize, - resumable=resumable) - - def to_json(self): - """Creating a JSON representation of an instance of MediaFileUpload. - - Returns: - string, a JSON representation of this instance, suitable to pass to - from_json(). - """ - return self._to_json(strip=['_fd']) - - @staticmethod - def from_json(s): - d = json.loads(s) - return MediaFileUpload(d['_filename'], mimetype=d['_mimetype'], - chunksize=d['_chunksize'], resumable=d['_resumable']) - - -class MediaInMemoryUpload(MediaIoBaseUpload): - """MediaUpload for a chunk of bytes. - - DEPRECATED: Use MediaIoBaseUpload with either io.TextIOBase or StringIO for - the stream. - """ - - @util.positional(2) - def __init__(self, body, mimetype='application/octet-stream', - chunksize=DEFAULT_CHUNK_SIZE, resumable=False): - """Create a new MediaInMemoryUpload. - - DEPRECATED: Use MediaIoBaseUpload with either io.TextIOBase or StringIO for - the stream. - - Args: - body: string, Bytes of body content. - mimetype: string, Mime-type of the file or default of - 'application/octet-stream'. - chunksize: int, File will be uploaded in chunks of this many bytes. Only - used if resumable=True. - resumable: bool, True if this is a resumable upload. False means upload - in a single request. - """ - fd = StringIO.StringIO(body) - super(MediaInMemoryUpload, self).__init__(fd, mimetype, chunksize=chunksize, - resumable=resumable) - - -class MediaIoBaseDownload(object): - """"Download media resources. - - Note that the Python file object is compatible with io.Base and can be used - with this class also. - - - Example: - request = farms.animals().get_media(id='cow') - fh = io.FileIO('cow.png', mode='wb') - downloader = MediaIoBaseDownload(fh, request, chunksize=1024*1024) - - done = False - while done is False: - status, done = downloader.next_chunk() - if status: - print "Download %d%%." % int(status.progress() * 100) - print "Download Complete!" - """ - - @util.positional(3) - def __init__(self, fd, request, chunksize=DEFAULT_CHUNK_SIZE): - """Constructor. - - Args: - fd: io.Base or file object, The stream in which to write the downloaded - bytes. - request: googleapiclient.http.HttpRequest, the media request to perform in - chunks. - chunksize: int, File will be downloaded in chunks of this many bytes. - """ - self._fd = fd - self._request = request - self._uri = request.uri - self._chunksize = chunksize - self._progress = 0 - self._total_size = None - self._done = False - - # Stubs for testing. - self._sleep = time.sleep - self._rand = random.random - - @util.positional(1) - def next_chunk(self, num_retries=0): - """Get the next chunk of the download. - - Args: - num_retries: Integer, number of times to retry 500's with randomized - exponential backoff. If all retries fail, the raised HttpError - represents the last request. If zero (default), we attempt the - request only once. - - Returns: - (status, done): (MediaDownloadStatus, boolean) - The value of 'done' will be True when the media has been fully - downloaded. - - Raises: - googleapiclient.errors.HttpError if the response was not a 2xx. - httplib2.HttpLib2Error if a transport error has occured. - """ - headers = { - 'range': 'bytes=%d-%d' % ( - self._progress, self._progress + self._chunksize) - } - http = self._request.http - - for retry_num in xrange(num_retries + 1): - if retry_num > 0: - self._sleep(self._rand() * 2**retry_num) - logging.warning( - 'Retry #%d for media download: GET %s, following status: %d' - % (retry_num, self._uri, resp.status)) - - resp, content = http.request(self._uri, headers=headers) - if resp.status < 500: - break - - if resp.status in [200, 206]: - if 'content-location' in resp and resp['content-location'] != self._uri: - self._uri = resp['content-location'] - self._progress += len(content) - self._fd.write(content) - - if 'content-range' in resp: - content_range = resp['content-range'] - length = content_range.rsplit('/', 1)[1] - self._total_size = int(length) - - if self._progress == self._total_size: - self._done = True - return MediaDownloadProgress(self._progress, self._total_size), self._done - else: - raise HttpError(resp, content, uri=self._uri) - - -class _StreamSlice(object): - """Truncated stream. - - Takes a stream and presents a stream that is a slice of the original stream. - This is used when uploading media in chunks. In later versions of Python a - stream can be passed to httplib in place of the string of data to send. The - problem is that httplib just blindly reads to the end of the stream. This - wrapper presents a virtual stream that only reads to the end of the chunk. - """ - - def __init__(self, stream, begin, chunksize): - """Constructor. - - Args: - stream: (io.Base, file object), the stream to wrap. - begin: int, the seek position the chunk begins at. - chunksize: int, the size of the chunk. - """ - self._stream = stream - self._begin = begin - self._chunksize = chunksize - self._stream.seek(begin) - - def read(self, n=-1): - """Read n bytes. - - Args: - n, int, the number of bytes to read. - - Returns: - A string of length 'n', or less if EOF is reached. - """ - # The data left available to read sits in [cur, end) - cur = self._stream.tell() - end = self._begin + self._chunksize - if n == -1 or cur + n > end: - n = end - cur - return self._stream.read(n) - - -class HttpRequest(object): - """Encapsulates a single HTTP request.""" - - @util.positional(4) - def __init__(self, http, postproc, uri, - method='GET', - body=None, - headers=None, - methodId=None, - resumable=None): - """Constructor for an HttpRequest. - - Args: - http: httplib2.Http, the transport object to use to make a request - postproc: callable, called on the HTTP response and content to transform - it into a data object before returning, or raising an exception - on an error. - uri: string, the absolute URI to send the request to - method: string, the HTTP method to use - body: string, the request body of the HTTP request, - headers: dict, the HTTP request headers - methodId: string, a unique identifier for the API method being called. - resumable: MediaUpload, None if this is not a resumbale request. - """ - self.uri = uri - self.method = method - self.body = body - self.headers = headers or {} - self.methodId = methodId - self.http = http - self.postproc = postproc - self.resumable = resumable - self.response_callbacks = [] - self._in_error_state = False - - # Pull the multipart boundary out of the content-type header. - major, minor, params = mimeparse.parse_mime_type( - headers.get('content-type', 'application/json')) - - # The size of the non-media part of the request. - self.body_size = len(self.body or '') - - # The resumable URI to send chunks to. - self.resumable_uri = None - - # The bytes that have been uploaded. - self.resumable_progress = 0 - - # Stubs for testing. - self._rand = random.random - self._sleep = time.sleep - - @util.positional(1) - def execute(self, http=None, num_retries=0): - """Execute the request. - - Args: - http: httplib2.Http, an http object to be used in place of the - one the HttpRequest request object was constructed with. - num_retries: Integer, number of times to retry 500's with randomized - exponential backoff. If all retries fail, the raised HttpError - represents the last request. If zero (default), we attempt the - request only once. - - Returns: - A deserialized object model of the response body as determined - by the postproc. - - Raises: - googleapiclient.errors.HttpError if the response was not a 2xx. - httplib2.HttpLib2Error if a transport error has occured. - """ - if http is None: - http = self.http - - if self.resumable: - body = None - while body is None: - _, body = self.next_chunk(http=http, num_retries=num_retries) - return body - - # Non-resumable case. - - if 'content-length' not in self.headers: - self.headers['content-length'] = str(self.body_size) - # If the request URI is too long then turn it into a POST request. - if len(self.uri) > MAX_URI_LENGTH and self.method == 'GET': - self.method = 'POST' - self.headers['x-http-method-override'] = 'GET' - self.headers['content-type'] = 'application/x-www-form-urlencoded' - parsed = urlparse.urlparse(self.uri) - self.uri = urlparse.urlunparse( - (parsed.scheme, parsed.netloc, parsed.path, parsed.params, None, - None) - ) - self.body = parsed.query - self.headers['content-length'] = str(len(self.body)) - - # Handle retries for server-side errors. - for retry_num in xrange(num_retries + 1): - if retry_num > 0: - self._sleep(self._rand() * 2**retry_num) - logging.warning('Retry #%d for request: %s %s, following status: %d' - % (retry_num, self.method, self.uri, resp.status)) - - resp, content = http.request(str(self.uri), method=str(self.method), - body=self.body, headers=self.headers) - if resp.status < 500: - break - - for callback in self.response_callbacks: - callback(resp) - if resp.status >= 300: - raise HttpError(resp, content, uri=self.uri) - return self.postproc(resp, content) - - @util.positional(2) - def add_response_callback(self, cb): - """add_response_headers_callback - - Args: - cb: Callback to be called on receiving the response headers, of signature: - - def cb(resp): - # Where resp is an instance of httplib2.Response - """ - self.response_callbacks.append(cb) - - @util.positional(1) - def next_chunk(self, http=None, num_retries=0): - """Execute the next step of a resumable upload. - - Can only be used if the method being executed supports media uploads and - the MediaUpload object passed in was flagged as using resumable upload. - - Example: - - media = MediaFileUpload('cow.png', mimetype='image/png', - chunksize=1000, resumable=True) - request = farm.animals().insert( - id='cow', - name='cow.png', - media_body=media) - - response = None - while response is None: - status, response = request.next_chunk() - if status: - print "Upload %d%% complete." % int(status.progress() * 100) - - - Args: - http: httplib2.Http, an http object to be used in place of the - one the HttpRequest request object was constructed with. - num_retries: Integer, number of times to retry 500's with randomized - exponential backoff. If all retries fail, the raised HttpError - represents the last request. If zero (default), we attempt the - request only once. - - Returns: - (status, body): (ResumableMediaStatus, object) - The body will be None until the resumable media is fully uploaded. - - Raises: - googleapiclient.errors.HttpError if the response was not a 2xx. - httplib2.HttpLib2Error if a transport error has occured. - """ - if http is None: - http = self.http - - if self.resumable.size() is None: - size = '*' - else: - size = str(self.resumable.size()) - - if self.resumable_uri is None: - start_headers = copy.copy(self.headers) - start_headers['X-Upload-Content-Type'] = self.resumable.mimetype() - if size != '*': - start_headers['X-Upload-Content-Length'] = size - start_headers['content-length'] = str(self.body_size) - - for retry_num in xrange(num_retries + 1): - if retry_num > 0: - self._sleep(self._rand() * 2**retry_num) - logging.warning( - 'Retry #%d for resumable URI request: %s %s, following status: %d' - % (retry_num, self.method, self.uri, resp.status)) - - resp, content = http.request(self.uri, method=self.method, - body=self.body, - headers=start_headers) - if resp.status < 500: - break - - if resp.status == 200 and 'location' in resp: - self.resumable_uri = resp['location'] - else: - raise ResumableUploadError(resp, content) - elif self._in_error_state: - # If we are in an error state then query the server for current state of - # the upload by sending an empty PUT and reading the 'range' header in - # the response. - headers = { - 'Content-Range': 'bytes */%s' % size, - 'content-length': '0' - } - resp, content = http.request(self.resumable_uri, 'PUT', - headers=headers) - status, body = self._process_response(resp, content) - if body: - # The upload was complete. - return (status, body) - - # The httplib.request method can take streams for the body parameter, but - # only in Python 2.6 or later. If a stream is available under those - # conditions then use it as the body argument. - if self.resumable.has_stream() and sys.version_info[1] >= 6: - data = self.resumable.stream() - if self.resumable.chunksize() == -1: - data.seek(self.resumable_progress) - chunk_end = self.resumable.size() - self.resumable_progress - 1 - else: - # Doing chunking with a stream, so wrap a slice of the stream. - data = _StreamSlice(data, self.resumable_progress, - self.resumable.chunksize()) - chunk_end = min( - self.resumable_progress + self.resumable.chunksize() - 1, - self.resumable.size() - 1) - else: - data = self.resumable.getbytes( - self.resumable_progress, self.resumable.chunksize()) - - # A short read implies that we are at EOF, so finish the upload. - if len(data) < self.resumable.chunksize(): - size = str(self.resumable_progress + len(data)) - - chunk_end = self.resumable_progress + len(data) - 1 - - headers = { - 'Content-Range': 'bytes %d-%d/%s' % ( - self.resumable_progress, chunk_end, size), - # Must set the content-length header here because httplib can't - # calculate the size when working with _StreamSlice. - 'Content-Length': str(chunk_end - self.resumable_progress + 1) - } - - for retry_num in xrange(num_retries + 1): - if retry_num > 0: - self._sleep(self._rand() * 2**retry_num) - logging.warning( - 'Retry #%d for media upload: %s %s, following status: %d' - % (retry_num, self.method, self.uri, resp.status)) - - try: - resp, content = http.request(self.resumable_uri, method='PUT', - body=data, - headers=headers) - except: - self._in_error_state = True - raise - if resp.status < 500: - break - - return self._process_response(resp, content) - - def _process_response(self, resp, content): - """Process the response from a single chunk upload. - - Args: - resp: httplib2.Response, the response object. - content: string, the content of the response. - - Returns: - (status, body): (ResumableMediaStatus, object) - The body will be None until the resumable media is fully uploaded. - - Raises: - googleapiclient.errors.HttpError if the response was not a 2xx or a 308. - """ - if resp.status in [200, 201]: - self._in_error_state = False - return None, self.postproc(resp, content) - elif resp.status == 308: - self._in_error_state = False - # A "308 Resume Incomplete" indicates we are not done. - self.resumable_progress = int(resp['range'].split('-')[1]) + 1 - if 'location' in resp: - self.resumable_uri = resp['location'] - else: - self._in_error_state = True - raise HttpError(resp, content, uri=self.uri) - - return (MediaUploadProgress(self.resumable_progress, self.resumable.size()), - None) - - def to_json(self): - """Returns a JSON representation of the HttpRequest.""" - d = copy.copy(self.__dict__) - if d['resumable'] is not None: - d['resumable'] = self.resumable.to_json() - del d['http'] - del d['postproc'] - del d['_sleep'] - del d['_rand'] - - return json.dumps(d) - - @staticmethod - def from_json(s, http, postproc): - """Returns an HttpRequest populated with info from a JSON object.""" - d = json.loads(s) - if d['resumable'] is not None: - d['resumable'] = MediaUpload.new_from_json(d['resumable']) - return HttpRequest( - http, - postproc, - uri=d['uri'], - method=d['method'], - body=d['body'], - headers=d['headers'], - methodId=d['methodId'], - resumable=d['resumable']) - - -class BatchHttpRequest(object): - """Batches multiple HttpRequest objects into a single HTTP request. - - Example: - from googleapiclient.http import BatchHttpRequest - - def list_animals(request_id, response, exception): - \"\"\"Do something with the animals list response.\"\"\" - if exception is not None: - # Do something with the exception. - pass - else: - # Do something with the response. - pass - - def list_farmers(request_id, response, exception): - \"\"\"Do something with the farmers list response.\"\"\" - if exception is not None: - # Do something with the exception. - pass - else: - # Do something with the response. - pass - - service = build('farm', 'v2') - - batch = BatchHttpRequest() - - batch.add(service.animals().list(), list_animals) - batch.add(service.farmers().list(), list_farmers) - batch.execute(http=http) - """ - - @util.positional(1) - def __init__(self, callback=None, batch_uri=None): - """Constructor for a BatchHttpRequest. - - Args: - callback: callable, A callback to be called for each response, of the - form callback(id, response, exception). The first parameter is the - request id, and the second is the deserialized response object. The - third is an googleapiclient.errors.HttpError exception object if an HTTP error - occurred while processing the request, or None if no error occurred. - batch_uri: string, URI to send batch requests to. - """ - if batch_uri is None: - batch_uri = 'https://www.googleapis.com/batch' - self._batch_uri = batch_uri - - # Global callback to be called for each individual response in the batch. - self._callback = callback - - # A map from id to request. - self._requests = {} - - # A map from id to callback. - self._callbacks = {} - - # List of request ids, in the order in which they were added. - self._order = [] - - # The last auto generated id. - self._last_auto_id = 0 - - # Unique ID on which to base the Content-ID headers. - self._base_id = None - - # A map from request id to (httplib2.Response, content) response pairs - self._responses = {} - - # A map of id(Credentials) that have been refreshed. - self._refreshed_credentials = {} - - def _refresh_and_apply_credentials(self, request, http): - """Refresh the credentials and apply to the request. - - Args: - request: HttpRequest, the request. - http: httplib2.Http, the global http object for the batch. - """ - # For the credentials to refresh, but only once per refresh_token - # If there is no http per the request then refresh the http passed in - # via execute() - creds = None - if request.http is not None and hasattr(request.http.request, - 'credentials'): - creds = request.http.request.credentials - elif http is not None and hasattr(http.request, 'credentials'): - creds = http.request.credentials - if creds is not None: - if id(creds) not in self._refreshed_credentials: - creds.refresh(http) - self._refreshed_credentials[id(creds)] = 1 - - # Only apply the credentials if we are using the http object passed in, - # otherwise apply() will get called during _serialize_request(). - if request.http is None or not hasattr(request.http.request, - 'credentials'): - creds.apply(request.headers) - - def _id_to_header(self, id_): - """Convert an id to a Content-ID header value. - - Args: - id_: string, identifier of individual request. - - Returns: - A Content-ID header with the id_ encoded into it. A UUID is prepended to - the value because Content-ID headers are supposed to be universally - unique. - """ - if self._base_id is None: - self._base_id = uuid.uuid4() - - return '<%s+%s>' % (self._base_id, urllib.quote(id_)) - - def _header_to_id(self, header): - """Convert a Content-ID header value to an id. - - Presumes the Content-ID header conforms to the format that _id_to_header() - returns. - - Args: - header: string, Content-ID header value. - - Returns: - The extracted id value. - - Raises: - BatchError if the header is not in the expected format. - """ - if header[0] != '<' or header[-1] != '>': - raise BatchError("Invalid value for Content-ID: %s" % header) - if '+' not in header: - raise BatchError("Invalid value for Content-ID: %s" % header) - base, id_ = header[1:-1].rsplit('+', 1) - - return urllib.unquote(id_) - - def _serialize_request(self, request): - """Convert an HttpRequest object into a string. - - Args: - request: HttpRequest, the request to serialize. - - Returns: - The request as a string in application/http format. - """ - # Construct status line - parsed = urlparse.urlparse(request.uri) - request_line = urlparse.urlunparse( - (None, None, parsed.path, parsed.params, parsed.query, None) - ) - status_line = request.method + ' ' + request_line + ' HTTP/1.1\n' - major, minor = request.headers.get('content-type', 'application/json').split('/') - msg = MIMENonMultipart(major, minor) - headers = request.headers.copy() - - if request.http is not None and hasattr(request.http.request, - 'credentials'): - request.http.request.credentials.apply(headers) - - # MIMENonMultipart adds its own Content-Type header. - if 'content-type' in headers: - del headers['content-type'] - - for key, value in headers.iteritems(): - msg[key] = value - msg['Host'] = parsed.netloc - msg.set_unixfrom(None) - - if request.body is not None: - msg.set_payload(request.body) - msg['content-length'] = str(len(request.body)) - - # Serialize the mime message. - fp = StringIO.StringIO() - # maxheaderlen=0 means don't line wrap headers. - g = Generator(fp, maxheaderlen=0) - g.flatten(msg, unixfrom=False) - body = fp.getvalue() - - # Strip off the \n\n that the MIME lib tacks onto the end of the payload. - if request.body is None: - body = body[:-2] - - return status_line.encode('utf-8') + body - - def _deserialize_response(self, payload): - """Convert string into httplib2 response and content. - - Args: - payload: string, headers and body as a string. - - Returns: - A pair (resp, content), such as would be returned from httplib2.request. - """ - # Strip off the status line - status_line, payload = payload.split('\n', 1) - protocol, status, reason = status_line.split(' ', 2) - - # Parse the rest of the response - parser = FeedParser() - parser.feed(payload) - msg = parser.close() - msg['status'] = status - - # Create httplib2.Response from the parsed headers. - resp = httplib2.Response(msg) - resp.reason = reason - resp.version = int(protocol.split('/', 1)[1].replace('.', '')) - - content = payload.split('\r\n\r\n', 1)[1] - - return resp, content - - def _new_id(self): - """Create a new id. - - Auto incrementing number that avoids conflicts with ids already used. - - Returns: - string, a new unique id. - """ - self._last_auto_id += 1 - while str(self._last_auto_id) in self._requests: - self._last_auto_id += 1 - return str(self._last_auto_id) - - @util.positional(2) - def add(self, request, callback=None, request_id=None): - """Add a new request. - - Every callback added will be paired with a unique id, the request_id. That - unique id will be passed back to the callback when the response comes back - from the server. The default behavior is to have the library generate it's - own unique id. If the caller passes in a request_id then they must ensure - uniqueness for each request_id, and if they are not an exception is - raised. Callers should either supply all request_ids or nevery supply a - request id, to avoid such an error. - - Args: - request: HttpRequest, Request to add to the batch. - callback: callable, A callback to be called for this response, of the - form callback(id, response, exception). The first parameter is the - request id, and the second is the deserialized response object. The - third is an googleapiclient.errors.HttpError exception object if an HTTP error - occurred while processing the request, or None if no errors occurred. - request_id: string, A unique id for the request. The id will be passed to - the callback with the response. - - Returns: - None - - Raises: - BatchError if a media request is added to a batch. - KeyError is the request_id is not unique. - """ - if request_id is None: - request_id = self._new_id() - if request.resumable is not None: - raise BatchError("Media requests cannot be used in a batch request.") - if request_id in self._requests: - raise KeyError("A request with this ID already exists: %s" % request_id) - self._requests[request_id] = request - self._callbacks[request_id] = callback - self._order.append(request_id) - - def _execute(self, http, order, requests): - """Serialize batch request, send to server, process response. - - Args: - http: httplib2.Http, an http object to be used to make the request with. - order: list, list of request ids in the order they were added to the - batch. - request: list, list of request objects to send. - - Raises: - httplib2.HttpLib2Error if a transport error has occured. - googleapiclient.errors.BatchError if the response is the wrong format. - """ - message = MIMEMultipart('mixed') - # Message should not write out it's own headers. - setattr(message, '_write_headers', lambda self: None) - - # Add all the individual requests. - for request_id in order: - request = requests[request_id] - - msg = MIMENonMultipart('application', 'http') - msg['Content-Transfer-Encoding'] = 'binary' - msg['Content-ID'] = self._id_to_header(request_id) - - body = self._serialize_request(request) - msg.set_payload(body) - message.attach(msg) - - # encode the body: note that we can't use `as_string`, because - # it plays games with `From ` lines. - fp = StringIO.StringIO() - g = Generator(fp, mangle_from_=False) - g.flatten(message, unixfrom=False) - body = fp.getvalue() - - headers = {} - headers['content-type'] = ('multipart/mixed; ' - 'boundary="%s"') % message.get_boundary() - - resp, content = http.request(self._batch_uri, method='POST', body=body, - headers=headers) - - if resp.status >= 300: - raise HttpError(resp, content, uri=self._batch_uri) - - # Now break out the individual responses and store each one. - boundary, _ = content.split(None, 1) - - # Prepend with a content-type header so FeedParser can handle it. - header = 'content-type: %s\r\n\r\n' % resp['content-type'] - for_parser = header + content - - parser = FeedParser() - parser.feed(for_parser) - mime_response = parser.close() - - if not mime_response.is_multipart(): - raise BatchError("Response not in multipart/mixed format.", resp=resp, - content=content) - - for part in mime_response.get_payload(): - request_id = self._header_to_id(part['Content-ID']) - response, content = self._deserialize_response(part.get_payload()) - self._responses[request_id] = (response, content) - - @util.positional(1) - def execute(self, http=None): - """Execute all the requests as a single batched HTTP request. - - Args: - http: httplib2.Http, an http object to be used in place of the one the - HttpRequest request object was constructed with. If one isn't supplied - then use a http object from the requests in this batch. - - Returns: - None - - Raises: - httplib2.HttpLib2Error if a transport error has occured. - googleapiclient.errors.BatchError if the response is the wrong format. - """ - - # If http is not supplied use the first valid one given in the requests. - if http is None: - for request_id in self._order: - request = self._requests[request_id] - if request is not None: - http = request.http - break - - if http is None: - raise ValueError("Missing a valid http object.") - - self._execute(http, self._order, self._requests) - - # Loop over all the requests and check for 401s. For each 401 request the - # credentials should be refreshed and then sent again in a separate batch. - redo_requests = {} - redo_order = [] - - for request_id in self._order: - resp, content = self._responses[request_id] - if resp['status'] == '401': - redo_order.append(request_id) - request = self._requests[request_id] - self._refresh_and_apply_credentials(request, http) - redo_requests[request_id] = request - - if redo_requests: - self._execute(http, redo_order, redo_requests) - - # Now process all callbacks that are erroring, and raise an exception for - # ones that return a non-2xx response? Or add extra parameter to callback - # that contains an HttpError? - - for request_id in self._order: - resp, content = self._responses[request_id] - - request = self._requests[request_id] - callback = self._callbacks[request_id] - - response = None - exception = None - try: - if resp.status >= 300: - raise HttpError(resp, content, uri=request.uri) - response = request.postproc(resp, content) - except HttpError, e: - exception = e - - if callback is not None: - callback(request_id, response, exception) - if self._callback is not None: - self._callback(request_id, response, exception) - - -class HttpRequestMock(object): - """Mock of HttpRequest. - - Do not construct directly, instead use RequestMockBuilder. - """ - - def __init__(self, resp, content, postproc): - """Constructor for HttpRequestMock - - Args: - resp: httplib2.Response, the response to emulate coming from the request - content: string, the response body - postproc: callable, the post processing function usually supplied by - the model class. See model.JsonModel.response() as an example. - """ - self.resp = resp - self.content = content - self.postproc = postproc - if resp is None: - self.resp = httplib2.Response({'status': 200, 'reason': 'OK'}) - if 'reason' in self.resp: - self.resp.reason = self.resp['reason'] - - def execute(self, http=None): - """Execute the request. - - Same behavior as HttpRequest.execute(), but the response is - mocked and not really from an HTTP request/response. - """ - return self.postproc(self.resp, self.content) - - -class RequestMockBuilder(object): - """A simple mock of HttpRequest - - Pass in a dictionary to the constructor that maps request methodIds to - tuples of (httplib2.Response, content, opt_expected_body) that should be - returned when that method is called. None may also be passed in for the - httplib2.Response, in which case a 200 OK response will be generated. - If an opt_expected_body (str or dict) is provided, it will be compared to - the body and UnexpectedBodyError will be raised on inequality. - - Example: - response = '{"data": {"id": "tag:google.c...' - requestBuilder = RequestMockBuilder( - { - 'plus.activities.get': (None, response), - } - ) - googleapiclient.discovery.build("plus", "v1", requestBuilder=requestBuilder) - - Methods that you do not supply a response for will return a - 200 OK with an empty string as the response content or raise an excpetion - if check_unexpected is set to True. The methodId is taken from the rpcName - in the discovery document. - - For more details see the project wiki. - """ - - def __init__(self, responses, check_unexpected=False): - """Constructor for RequestMockBuilder - - The constructed object should be a callable object - that can replace the class HttpResponse. - - responses - A dictionary that maps methodIds into tuples - of (httplib2.Response, content). The methodId - comes from the 'rpcName' field in the discovery - document. - check_unexpected - A boolean setting whether or not UnexpectedMethodError - should be raised on unsupplied method. - """ - self.responses = responses - self.check_unexpected = check_unexpected - - def __call__(self, http, postproc, uri, method='GET', body=None, - headers=None, methodId=None, resumable=None): - """Implements the callable interface that discovery.build() expects - of requestBuilder, which is to build an object compatible with - HttpRequest.execute(). See that method for the description of the - parameters and the expected response. - """ - if methodId in self.responses: - response = self.responses[methodId] - resp, content = response[:2] - if len(response) > 2: - # Test the body against the supplied expected_body. - expected_body = response[2] - if bool(expected_body) != bool(body): - # Not expecting a body and provided one - # or expecting a body and not provided one. - raise UnexpectedBodyError(expected_body, body) - if isinstance(expected_body, str): - expected_body = json.loads(expected_body) - body = json.loads(body) - if body != expected_body: - raise UnexpectedBodyError(expected_body, body) - return HttpRequestMock(resp, content, postproc) - elif self.check_unexpected: - raise UnexpectedMethodError(methodId=methodId) - else: - model = JsonModel(False) - return HttpRequestMock(None, '{}', model.response) - - -class HttpMock(object): - """Mock of httplib2.Http""" - - def __init__(self, filename=None, headers=None): - """ - Args: - filename: string, absolute filename to read response from - headers: dict, header to return with response - """ - if headers is None: - headers = {'status': '200 OK'} - if filename: - f = file(filename, 'r') - self.data = f.read() - f.close() - else: - self.data = None - self.response_headers = headers - self.headers = None - self.uri = None - self.method = None - self.body = None - self.headers = None - - - def request(self, uri, - method='GET', - body=None, - headers=None, - redirections=1, - connection_type=None): - self.uri = uri - self.method = method - self.body = body - self.headers = headers - return httplib2.Response(self.response_headers), self.data - - -class HttpMockSequence(object): - """Mock of httplib2.Http - - Mocks a sequence of calls to request returning different responses for each - call. Create an instance initialized with the desired response headers - and content and then use as if an httplib2.Http instance. - - http = HttpMockSequence([ - ({'status': '401'}, ''), - ({'status': '200'}, '{"access_token":"1/3w","expires_in":3600}'), - ({'status': '200'}, 'echo_request_headers'), - ]) - resp, content = http.request("http://examples.com") - - There are special values you can pass in for content to trigger - behavours that are helpful in testing. - - 'echo_request_headers' means return the request headers in the response body - 'echo_request_headers_as_json' means return the request headers in - the response body - 'echo_request_body' means return the request body in the response body - 'echo_request_uri' means return the request uri in the response body - """ - - def __init__(self, iterable): - """ - Args: - iterable: iterable, a sequence of pairs of (headers, body) - """ - self._iterable = iterable - self.follow_redirects = True - - def request(self, uri, - method='GET', - body=None, - headers=None, - redirections=1, - connection_type=None): - resp, content = self._iterable.pop(0) - if content == 'echo_request_headers': - content = headers - elif content == 'echo_request_headers_as_json': - content = json.dumps(headers) - elif content == 'echo_request_body': - if hasattr(body, 'read'): - content = body.read() - else: - content = body - elif content == 'echo_request_uri': - content = uri - return httplib2.Response(resp), content - - -def set_user_agent(http, user_agent): - """Set the user-agent on every request. - - Args: - http - An instance of httplib2.Http - or something that acts like it. - user_agent: string, the value for the user-agent header. - - Returns: - A modified instance of http that was passed in. - - Example: - - h = httplib2.Http() - h = set_user_agent(h, "my-app-name/6.0") - - Most of the time the user-agent will be set doing auth, this is for the rare - cases where you are accessing an unauthenticated endpoint. - """ - request_orig = http.request - - # The closure that will replace 'httplib2.Http.request'. - def new_request(uri, method='GET', body=None, headers=None, - redirections=httplib2.DEFAULT_MAX_REDIRECTS, - connection_type=None): - """Modify the request headers to add the user-agent.""" - if headers is None: - headers = {} - if 'user-agent' in headers: - headers['user-agent'] = user_agent + ' ' + headers['user-agent'] - else: - headers['user-agent'] = user_agent - resp, content = request_orig(uri, method, body, headers, - redirections, connection_type) - return resp, content - - http.request = new_request - return http - - -def tunnel_patch(http): - """Tunnel PATCH requests over POST. - Args: - http - An instance of httplib2.Http - or something that acts like it. - - Returns: - A modified instance of http that was passed in. - - Example: - - h = httplib2.Http() - h = tunnel_patch(h, "my-app-name/6.0") - - Useful if you are running on a platform that doesn't support PATCH. - Apply this last if you are using OAuth 1.0, as changing the method - will result in a different signature. - """ - request_orig = http.request - - # The closure that will replace 'httplib2.Http.request'. - def new_request(uri, method='GET', body=None, headers=None, - redirections=httplib2.DEFAULT_MAX_REDIRECTS, - connection_type=None): - """Modify the request headers to add the user-agent.""" - if headers is None: - headers = {} - if method == 'PATCH': - if 'oauth_token' in headers.get('authorization', ''): - logging.warning( - 'OAuth 1.0 request made with Credentials after tunnel_patch.') - headers['x-http-method-override'] = "PATCH" - method = 'POST' - resp, content = request_orig(uri, method, body, headers, - redirections, connection_type) - return resp, content - - http.request = new_request - return http diff --git a/third_party/google_api_python_client/googleapiclient/mimeparse.py b/third_party/google_api_python_client/googleapiclient/mimeparse.py deleted file mode 100644 index 8038af18c..000000000 --- a/third_party/google_api_python_client/googleapiclient/mimeparse.py +++ /dev/null @@ -1,172 +0,0 @@ -# Copyright 2014 Joe Gregorio -# -# Licensed under the MIT License - -"""MIME-Type Parser - -This module provides basic functions for handling mime-types. It can handle -matching mime-types against a list of media-ranges. See section 14.1 of the -HTTP specification [RFC 2616] for a complete explanation. - - http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1 - -Contents: - - parse_mime_type(): Parses a mime-type into its component parts. - - parse_media_range(): Media-ranges are mime-types with wild-cards and a 'q' - quality parameter. - - quality(): Determines the quality ('q') of a mime-type when - compared against a list of media-ranges. - - quality_parsed(): Just like quality() except the second parameter must be - pre-parsed. - - best_match(): Choose the mime-type with the highest quality ('q') - from a list of candidates. -""" - -__version__ = '0.1.3' -__author__ = 'Joe Gregorio' -__email__ = 'joe@bitworking.org' -__license__ = 'MIT License' -__credits__ = '' - - -def parse_mime_type(mime_type): - """Parses a mime-type into its component parts. - - Carves up a mime-type and returns a tuple of the (type, subtype, params) - where 'params' is a dictionary of all the parameters for the media range. - For example, the media range 'application/xhtml;q=0.5' would get parsed - into: - - ('application', 'xhtml', {'q', '0.5'}) - """ - parts = mime_type.split(';') - params = dict([tuple([s.strip() for s in param.split('=', 1)])\ - for param in parts[1:] - ]) - full_type = parts[0].strip() - # Java URLConnection class sends an Accept header that includes a - # single '*'. Turn it into a legal wildcard. - if full_type == '*': - full_type = '*/*' - (type, subtype) = full_type.split('/') - - return (type.strip(), subtype.strip(), params) - - -def parse_media_range(range): - """Parse a media-range into its component parts. - - Carves up a media range and returns a tuple of the (type, subtype, - params) where 'params' is a dictionary of all the parameters for the media - range. For example, the media range 'application/*;q=0.5' would get parsed - into: - - ('application', '*', {'q', '0.5'}) - - In addition this function also guarantees that there is a value for 'q' - in the params dictionary, filling it in with a proper default if - necessary. - """ - (type, subtype, params) = parse_mime_type(range) - if not params.has_key('q') or not params['q'] or \ - not float(params['q']) or float(params['q']) > 1\ - or float(params['q']) < 0: - params['q'] = '1' - - return (type, subtype, params) - - -def fitness_and_quality_parsed(mime_type, parsed_ranges): - """Find the best match for a mime-type amongst parsed media-ranges. - - Find the best match for a given mime-type against a list of media_ranges - that have already been parsed by parse_media_range(). Returns a tuple of - the fitness value and the value of the 'q' quality parameter of the best - match, or (-1, 0) if no match was found. Just as for quality_parsed(), - 'parsed_ranges' must be a list of parsed media ranges. - """ - best_fitness = -1 - best_fit_q = 0 - (target_type, target_subtype, target_params) =\ - parse_media_range(mime_type) - for (type, subtype, params) in parsed_ranges: - type_match = (type == target_type or\ - type == '*' or\ - target_type == '*') - subtype_match = (subtype == target_subtype or\ - subtype == '*' or\ - target_subtype == '*') - if type_match and subtype_match: - param_matches = reduce(lambda x, y: x + y, [1 for (key, value) in \ - target_params.iteritems() if key != 'q' and \ - params.has_key(key) and value == params[key]], 0) - fitness = (type == target_type) and 100 or 0 - fitness += (subtype == target_subtype) and 10 or 0 - fitness += param_matches - if fitness > best_fitness: - best_fitness = fitness - best_fit_q = params['q'] - - return best_fitness, float(best_fit_q) - - -def quality_parsed(mime_type, parsed_ranges): - """Find the best match for a mime-type amongst parsed media-ranges. - - Find the best match for a given mime-type against a list of media_ranges - that have already been parsed by parse_media_range(). Returns the 'q' - quality parameter of the best match, 0 if no match was found. This function - bahaves the same as quality() except that 'parsed_ranges' must be a list of - parsed media ranges. - """ - - return fitness_and_quality_parsed(mime_type, parsed_ranges)[1] - - -def quality(mime_type, ranges): - """Return the quality ('q') of a mime-type against a list of media-ranges. - - Returns the quality 'q' of a mime-type when compared against the - media-ranges in ranges. For example: - - >>> quality('text/html','text/*;q=0.3, text/html;q=0.7, - text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5') - 0.7 - - """ - parsed_ranges = [parse_media_range(r) for r in ranges.split(',')] - - return quality_parsed(mime_type, parsed_ranges) - - -def best_match(supported, header): - """Return mime-type with the highest quality ('q') from list of candidates. - - Takes a list of supported mime-types and finds the best match for all the - media-ranges listed in header. The value of header must be a string that - conforms to the format of the HTTP Accept: header. The value of 'supported' - is a list of mime-types. The list of supported mime-types should be sorted - in order of increasing desirability, in case of a situation where there is - a tie. - - >>> best_match(['application/xbel+xml', 'text/xml'], - 'text/*;q=0.5,*/*; q=0.1') - 'text/xml' - """ - split_header = _filter_blank(header.split(',')) - parsed_header = [parse_media_range(r) for r in split_header] - weighted_matches = [] - pos = 0 - for mime_type in supported: - weighted_matches.append((fitness_and_quality_parsed(mime_type, - parsed_header), pos, mime_type)) - pos += 1 - weighted_matches.sort() - - return weighted_matches[-1][0][1] and weighted_matches[-1][2] or '' - - -def _filter_blank(i): - for s in i: - if s.strip(): - yield s diff --git a/third_party/google_api_python_client/googleapiclient/model.py b/third_party/google_api_python_client/googleapiclient/model.py deleted file mode 100644 index 0f0172cab..000000000 --- a/third_party/google_api_python_client/googleapiclient/model.py +++ /dev/null @@ -1,383 +0,0 @@ -#!/usr/bin/python2.4 -# -# Copyright 2014 Google Inc. All Rights Reserved. -# -# 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 json -import logging -import urllib - -from googleapiclient import __version__ -from errors import HttpError - - -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 json. - 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 json.dumps(body_value) - - def deserialize(self, content): - content = content.decode('utf-8') - body = json.loads(content) - if self._data_wrapper and isinstance(body, dict) and 'data' in body: - body = body['data'] - return body - - @property - def no_content_response(self): - return {} - - -class RawModel(JsonModel): - """Model class for requests that don't return JSON. - - Serializes and de-serializes between JSON and the Python - object representation of HTTP request, and returns the raw bytes - of the response body. - """ - accept = '*/*' - content_type = 'application/json' - alt_param = None - - def deserialize(self, content): - return content - - @property - def no_content_response(self): - return '' - - -class MediaModel(JsonModel): - """Model class for requests that return Media. - - Serializes and de-serializes between JSON and the Python - object representation of HTTP request, and returns the raw bytes - of the response body. - """ - accept = '*/*' - content_type = 'application/json' - alt_param = 'media' - - def deserialize(self, content): - return content - - @property - def no_content_response(self): - return '' - - -class ProtocolBufferModel(BaseModel): - """Model class for protocol buffers. - - Serializes and de-serializes the binary protocol buffer sent in the HTTP - request and response bodies. - """ - accept = 'application/x-protobuf' - content_type = 'application/x-protobuf' - alt_param = 'proto' - - def __init__(self, protocol_buffer): - """Constructs a ProtocolBufferModel. - - The serialzed protocol buffer returned in an HTTP response will be - de-serialized using the given protocol buffer class. - - Args: - protocol_buffer: The protocol buffer class used to de-serialize a - response from the API. - """ - self._protocol_buffer = protocol_buffer - - def serialize(self, body_value): - return body_value.SerializeToString() - - def deserialize(self, content): - return self._protocol_buffer.FromString(content) - - @property - def no_content_response(self): - return self._protocol_buffer() - - -def makepatch(original, modified): - """Create a patch object. - - Some methods support PATCH, an efficient way to send updates to a resource. - This method allows the easy construction of patch bodies by looking at the - differences between a resource before and after it was modified. - - Args: - original: object, the original deserialized resource - modified: object, the modified deserialized resource - Returns: - An object that contains only the changes from original to modified, in a - form suitable to pass to a PATCH method. - - Example usage: - item = service.activities().get(postid=postid, userid=userid).execute() - original = copy.deepcopy(item) - item['object']['content'] = 'This is updated.' - service.activities.patch(postid=postid, userid=userid, - body=makepatch(original, item)).execute() - """ - patch = {} - for key, original_value in original.iteritems(): - modified_value = modified.get(key, None) - if modified_value is None: - # Use None to signal that the element is deleted - patch[key] = None - elif original_value != modified_value: - if type(original_value) == type({}): - # Recursively descend objects - patch[key] = makepatch(original_value, modified_value) - else: - # In the case of simple types or arrays we just replace - patch[key] = modified_value - else: - # Don't add anything to patch if there's no change - pass - for key in modified: - if key not in original: - patch[key] = modified[key] - - return patch diff --git a/third_party/google_api_python_client/googleapiclient/sample_tools.py b/third_party/google_api_python_client/googleapiclient/sample_tools.py deleted file mode 100644 index cbd6d6f73..000000000 --- a/third_party/google_api_python_client/googleapiclient/sample_tools.py +++ /dev/null @@ -1,102 +0,0 @@ -# Copyright 2014 Google Inc. All Rights Reserved. -# -# 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=[], discovery_filename=None): - """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. - discovery_filename: string, name of local discovery file (JSON). Use when discovery doc not available via URL. - - Returns: - A tuple of (service, flags), where service is the service object and flags - is the parsed command-line flags. - """ - if scope is None: - scope = 'https://www.googleapis.com/auth/' + name - - # Parser command-line arguments. - parent_parsers = [tools.argparser] - parent_parsers.extend(parents) - parser = argparse.ArgumentParser( - description=doc, - formatter_class=argparse.RawDescriptionHelpFormatter, - parents=parent_parsers) - flags = parser.parse_args(argv[1:]) - - # Name of a file containing the OAuth 2.0 information for this - # application, including client_id and client_secret, which are found - # on the API Access tab on the Google APIs - # Console . - client_secrets = os.path.join(os.path.dirname(filename), - 'client_secrets.json') - - # Set up a Flow object to be used if we need to authenticate. - flow = client.flow_from_clientsecrets(client_secrets, - scope=scope, - message=tools.message_if_missing(client_secrets)) - - # Prepare credentials, and authorize HTTP object with them. - # If the credentials don't exist or are invalid run through the native client - # flow. The Storage object will ensure that if successful the good - # credentials will get written back to a file. - storage = file.Storage(name + '.dat') - credentials = storage.get() - if credentials is None or credentials.invalid: - credentials = tools.run_flow(flow, storage, flags) - http = credentials.authorize(http = httplib2.Http()) - - if discovery_filename is None: - # Construct a service object via the discovery service. - service = discovery.build(name, version, http=http) - else: - # Construct a service object using a local discovery document file. - with open(discovery_filename) as discovery_file: - service = discovery.build_from_document( - discovery_file.read(), - base='https://www.googleapis.com/', - http=http) - return (service, flags) diff --git a/third_party/google_api_python_client/googleapiclient/schema.py b/third_party/google_api_python_client/googleapiclient/schema.py deleted file mode 100644 index af413177f..000000000 --- a/third_party/google_api_python_client/googleapiclient/schema.py +++ /dev/null @@ -1,311 +0,0 @@ -# Copyright 2014 Google Inc. All Rights Reserved. -# -# 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 - - -class Schemas(object): - """Schemas for an API.""" - - def __init__(self, discovery): - """Constructor. - - Args: - discovery: object, Deserialized discovery document from which we pull - out the named schema. - """ - self.schemas = discovery.get('schemas', {}) - - # Cache of pretty printed schemas. - self.pretty = {} - - @util.positional(2) - def _prettyPrintByName(self, name, seen=None, dent=0): - """Get pretty printed object prototype from the schema name. - - Args: - name: string, Name of schema in the discovery document. - seen: list of string, Names of schema already seen. Used to handle - recursive definitions. - - Returns: - string, A string that contains a prototype object with - comments that conforms to the given schema. - """ - if seen is None: - seen = [] - - if name in seen: - # Do not fall into an infinite loop over recursive definitions. - return '# Object with schema name: %s' % name - seen.append(name) - - if name not in self.pretty: - self.pretty[name] = _SchemaToStruct(self.schemas[name], - seen, dent=dent).to_str(self._prettyPrintByName) - - seen.pop() - - return self.pretty[name] - - def prettyPrintByName(self, name): - """Get pretty printed object prototype from the schema name. - - Args: - name: string, Name of schema in the discovery document. - - Returns: - string, A string that contains a prototype object with - comments that conforms to the given schema. - """ - # Return with trailing comma and newline removed. - return self._prettyPrintByName(name, seen=[], dent=1)[:-2] - - @util.positional(2) - def _prettyPrintSchema(self, schema, seen=None, dent=0): - """Get pretty printed object prototype of schema. - - Args: - schema: object, Parsed JSON schema. - seen: list of string, Names of schema already seen. Used to handle - recursive definitions. - - Returns: - string, A string that contains a prototype object with - comments that conforms to the given schema. - """ - if seen is None: - seen = [] - - return _SchemaToStruct(schema, seen, dent=dent).to_str(self._prettyPrintByName) - - def prettyPrintSchema(self, schema): - """Get pretty printed object prototype of schema. - - Args: - schema: object, Parsed JSON schema. - - Returns: - string, A string that contains a prototype object with - comments that conforms to the given schema. - """ - # Return with trailing comma and newline removed. - return self._prettyPrintSchema(schema, dent=1)[:-2] - - def get(self, name): - """Get deserialized JSON schema from the schema name. - - Args: - name: string, Schema name. - """ - return self.schemas[name] - - -class _SchemaToStruct(object): - """Convert schema to a prototype object.""" - - @util.positional(3) - def __init__(self, schema, seen, dent=0): - """Constructor. - - Args: - schema: object, Parsed JSON schema. - seen: list, List of names of schema already seen while parsing. Used to - handle recursive definitions. - dent: int, Initial indentation depth. - """ - # The result of this parsing kept as list of strings. - self.value = [] - - # The final value of the parsing. - self.string = None - - # The parsed JSON schema. - self.schema = schema - - # Indentation level. - self.dent = dent - - # Method that when called returns a prototype object for the schema with - # the given name. - self.from_cache = None - - # List of names of schema already seen while parsing. - self.seen = seen - - def emit(self, text): - """Add text as a line to the output. - - Args: - text: string, Text to output. - """ - self.value.extend([" " * self.dent, text, '\n']) - - def emitBegin(self, text): - """Add text to the output, but with no line terminator. - - Args: - text: string, Text to output. - """ - self.value.extend([" " * self.dent, text]) - - def emitEnd(self, text, comment): - """Add text and comment to the output with line terminator. - - Args: - text: string, Text to output. - comment: string, Python comment. - """ - if comment: - divider = '\n' + ' ' * (self.dent + 2) + '# ' - lines = comment.splitlines() - lines = [x.rstrip() for x in lines] - comment = divider.join(lines) - self.value.extend([text, ' # ', comment, '\n']) - else: - self.value.extend([text, '\n']) - - def indent(self): - """Increase indentation level.""" - self.dent += 1 - - def undent(self): - """Decrease indentation level.""" - self.dent -= 1 - - def _to_str_impl(self, schema): - """Prototype object based on the schema, in Python code with comments. - - Args: - schema: object, Parsed JSON schema file. - - Returns: - Prototype object based on the schema, in Python code with comments. - """ - stype = schema.get('type') - if stype == 'object': - self.emitEnd('{', schema.get('description', '')) - self.indent() - if 'properties' in schema: - for pname, pschema in schema.get('properties', {}).iteritems(): - self.emitBegin('"%s": ' % pname) - self._to_str_impl(pschema) - elif 'additionalProperties' in schema: - self.emitBegin('"a_key": ') - self._to_str_impl(schema['additionalProperties']) - self.undent() - self.emit('},') - elif '$ref' in schema: - schemaName = schema['$ref'] - description = schema.get('description', '') - s = self.from_cache(schemaName, seen=self.seen) - parts = s.splitlines() - self.emitEnd(parts[0], description) - for line in parts[1:]: - self.emit(line.rstrip()) - elif stype == 'boolean': - value = schema.get('default', 'True or False') - self.emitEnd('%s,' % str(value), schema.get('description', '')) - elif stype == 'string': - value = schema.get('default', 'A String') - self.emitEnd('"%s",' % str(value), schema.get('description', '')) - elif stype == 'integer': - value = schema.get('default', '42') - self.emitEnd('%s,' % str(value), schema.get('description', '')) - elif stype == 'number': - value = schema.get('default', '3.14') - self.emitEnd('%s,' % str(value), schema.get('description', '')) - elif stype == 'null': - self.emitEnd('None,', schema.get('description', '')) - elif stype == 'any': - self.emitEnd('"",', schema.get('description', '')) - elif stype == 'array': - self.emitEnd('[', schema.get('description')) - self.indent() - self.emitBegin('') - self._to_str_impl(schema['items']) - self.undent() - self.emit('],') - else: - self.emit('Unknown type! %s' % stype) - self.emitEnd('', '') - - self.string = ''.join(self.value) - return self.string - - def to_str(self, from_cache): - """Prototype object based on the schema, in Python code with comments. - - Args: - from_cache: callable(name, seen), Callable that retrieves an object - prototype for a schema with the given name. Seen is a list of schema - names already seen as we recursively descend the schema definition. - - Returns: - Prototype object based on the schema, in Python code with comments. - The lines of the code will all be properly indented. - """ - self.from_cache = from_cache - return self._to_str_impl(self.schema) diff --git a/third_party/google_api_python_client/samples-index.py b/third_party/google_api_python_client/samples-index.py deleted file mode 100644 index 712f552c9..000000000 --- a/third_party/google_api_python_client/samples-index.py +++ /dev/null @@ -1,246 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# -# Copyright 2014 Google Inc. All Rights Reserved. -# -# 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. - -"""Build wiki page with a list of all samples. - -The information for the wiki page is built from data found in all the README -files in the samples. The format of the README file is: - - - Description is everything up to the first blank line. - - api: plus (Used to look up the long name in discovery). - keywords: appengine (such as appengine, oauth2, cmdline) - - The rest of the file is ignored when it comes to building the index. -""" - -import httplib2 -import itertools -import json -import os -import re - -BASE_HG_URI = ('http://code.google.com/p/google-api-python-client/source/' - 'browse/#hg') - -http = httplib2.Http('.cache') -r, c = http.request('https://www.googleapis.com/discovery/v1/apis') -if r.status != 200: - raise ValueError('Received non-200 response when retrieving Discovery.') - -# Dictionary mapping api names to their discovery description. -DIRECTORY = {} -for item in json.loads(c)['items']: - if item['preferred']: - DIRECTORY[item['name']] = item - -# A list of valid keywords. Should not be taken as complete, add to -# this list as needed. -KEYWORDS = { - 'appengine': 'Google App Engine', - 'oauth2': 'OAuth 2.0', - 'cmdline': 'Command-line', - 'django': 'Django', - 'threading': 'Threading', - 'pagination': 'Pagination', - 'media': 'Media Upload and Download' - } - - -def get_lines(name, lines): - """Return lines that begin with name. - - Lines are expected to look like: - - name: space separated values - - Args: - name: string, parameter name. - lines: iterable of string, lines in the file. - - Returns: - List of values in the lines that match. - """ - retval = [] - matches = itertools.ifilter(lambda x: x.startswith(name + ':'), lines) - for line in matches: - retval.extend(line[len(name)+1:].split()) - return retval - - -def wiki_escape(s): - """Detect WikiSyntax (i.e. InterCaps, a.k.a. CamelCase) and escape it.""" - ret = [] - for word in s.split(): - if re.match(r'[A-Z]+[a-z]+[A-Z]', word): - word = '!%s' % word - ret.append(word) - return ' '.join(ret) - - -def context_from_sample(api, keywords, dirname, desc, uri): - """Return info for expanding a sample into a template. - - Args: - api: string, name of api. - keywords: list of string, list of keywords for the given api. - dirname: string, directory name of the sample. - desc: string, long description of the sample. - uri: string, uri of the sample code if provided in the README. - - Returns: - A dictionary of values useful for template expansion. - """ - if uri is None: - uri = BASE_HG_URI + dirname.replace('/', '%2F') - else: - uri = ''.join(uri) - if api is None: - return None - else: - entry = DIRECTORY[api] - context = { - 'api': api, - 'version': entry['version'], - 'api_name': wiki_escape(entry.get('title', entry.get('description'))), - 'api_desc': wiki_escape(entry['description']), - 'api_icon': entry['icons']['x32'], - 'keywords': keywords, - 'dir': dirname, - 'uri': uri, - 'desc': wiki_escape(desc), - } - return context - - -def keyword_context_from_sample(keywords, dirname, desc, uri): - """Return info for expanding a sample into a template. - - Sample may not be about a specific api. - - Args: - keywords: list of string, list of keywords for the given api. - dirname: string, directory name of the sample. - desc: string, long description of the sample. - uri: string, uri of the sample code if provided in the README. - - Returns: - A dictionary of values useful for template expansion. - """ - if uri is None: - uri = BASE_HG_URI + dirname.replace('/', '%2F') - else: - uri = ''.join(uri) - context = { - 'keywords': keywords, - 'dir': dirname, - 'uri': uri, - 'desc': wiki_escape(desc), - } - return context - - -def scan_readme_files(dirname): - """Scans all subdirs of dirname for README files. - - Args: - dirname: string, name of directory to walk. - - Returns: - (samples, keyword_set): list of information about all samples, the union - of all keywords found. - """ - samples = [] - keyword_set = set() - - for root, dirs, files in os.walk(dirname): - if 'README' in files: - filename = os.path.join(root, 'README') - with open(filename, 'r') as f: - content = f.read() - lines = content.splitlines() - desc = ' '.join(itertools.takewhile(lambda x: x, lines)) - api = get_lines('api', lines) - keywords = get_lines('keywords', lines) - uri = get_lines('uri', lines) - if not uri: - uri = None - - for k in keywords: - if k not in KEYWORDS: - raise ValueError( - '%s is not a valid keyword in file %s' % (k, filename)) - keyword_set.update(keywords) - if not api: - api = [None] - samples.append((api[0], keywords, root[1:], desc, uri)) - - samples.sort() - - return samples, keyword_set - - -def main(): - # Get all the information we need out of the README files in the samples. - samples, keyword_set = scan_readme_files('./samples') - - # Now build a wiki page with all that information. Accumulate all the - # information as string to be concatenated when were done. - page = ['\n= Samples By API =\n'] - - # All the samples, grouped by API. - current_api = None - for api, keywords, dirname, desc, uri in samples: - context = context_from_sample(api, keywords, dirname, desc, uri) - if context is None: - continue - if current_api != api: - page.append(""" -=== %(api_icon)s %(api_name)s === - -%(api_desc)s - -Documentation for the %(api_name)s in [https://google-api-client-libraries.appspot.com/documentation/%(api)s/%(version)s/python/latest/ PyDoc] - -""" % context) - current_api = api - - page.append('|| [%(uri)s %(dir)s] || %(desc)s ||\n' % context) - - # Now group the samples by keywords. - for keyword, keyword_name in KEYWORDS.iteritems(): - if keyword not in keyword_set: - continue - page.append('\n= %s Samples =\n\n' % keyword_name) - page.append('\n') - for _, keywords, dirname, desc, uri in samples: - context = keyword_context_from_sample(keywords, dirname, desc, uri) - if keyword not in keywords: - continue - page.append(""" - - - -""" % context) - page.append('
[%(uri)s %(dir)s] %(desc)s
\n') - - print ''.join(page) - - -if __name__ == '__main__': - main() diff --git a/third_party/google_api_python_client/setup.py b/third_party/google_api_python_client/setup.py deleted file mode 100644 index 40dbc0f15..000000000 --- a/third_party/google_api_python_client/setup.py +++ /dev/null @@ -1,94 +0,0 @@ -# Copyright 2014 Google Inc. All Rights Reserved. -# -# 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. - -"""Setup script for Google API Python client. - -Also installs included versions of third party libraries, if those libraries -are not already installed. -""" -from __future__ import print_function - -import sys - -if sys.version_info < (2, 6): - print('google-api-python-client requires python version >= 2.6.', - file=sys.stderr) - sys.exit(1) - -from setuptools import setup -import pkg_resources - -def _DetectBadness(): - import os - if 'SKIP_GOOGLEAPICLIENT_COMPAT_CHECK' in os.environ: - return - o2c_pkg = None - try: - o2c_pkg = pkg_resources.get_distribution('oauth2client') - except pkg_resources.DistributionNotFound: - pass - oauth2client = None - try: - import oauth2client - except ImportError: - pass - if o2c_pkg is None and oauth2client is not None: - raise RuntimeError( - 'Previous version of google-api-python-client detected; due to a ' - 'packaging issue, we cannot perform an in-place upgrade. Please remove ' - 'the old version and re-install this package.' - ) - -_DetectBadness() - -packages = [ - 'apiclient', - 'googleapiclient', -] - -install_requires = [ - 'httplib2>=0.8', - 'oauth2client>=1.3', - 'uritemplate>=0.6', -] - -if sys.version_info < (2, 7): - install_requires.append('argparse') - -long_desc = """The Google API Client for Python is a client library for -accessing the Plus, Moderator, and many other Google APIs.""" - -import googleapiclient -version = googleapiclient.__version__ - -setup( - name="google-api-python-client", - version=version, - description="Google API Client Library for Python", - long_description=long_desc, - author="Google Inc.", - url="http://github.com/google/google-api-python-client/", - install_requires=install_requires, - packages=packages, - package_data={}, - license="Apache 2.0", - keywords="google api client", - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: Apache Software License', - 'Operating System :: POSIX', - 'Topic :: Internet :: WWW/HTTP', - ], -) diff --git a/third_party/google_api_python_client/sitecustomize.py b/third_party/google_api_python_client/sitecustomize.py deleted file mode 100644 index ef0f06376..000000000 --- a/third_party/google_api_python_client/sitecustomize.py +++ /dev/null @@ -1,12 +0,0 @@ -# Set up the system so that this development -# version of google-api-python-client is run, even if -# an older version is installed on the system. -# -# To make this totally automatic add the following to -# your ~/.bash_profile: -# -# export PYTHONPATH=/path/to/where/you/checked/out/googleapiclient -import sys -import os - -sys.path.insert(0, os.path.dirname(__file__)) diff --git a/third_party/google_api_python_client/static/Credentials.png b/third_party/google_api_python_client/static/Credentials.png deleted file mode 100644 index a5be2c533f83f8b9dd9658fb809aaf9cc30fa1ea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 116881 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 cI~0YUo^5Sk#Trm8(Fl9r9QZ@c?mOI@bz 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 LWo2Aq=o>> from uritemplate import expand - >>> expand("http://www.{domain}/", {"domain": "foo.com"}) - 'http://www.foo.com/' - -It also exposes a method *variables* that returns all variables used in a -uritemplate. For example: - -.. code-block:: python - - >>> from uritemplate import variables - >>> variables('http:www{.domain*}{/top,next}{?q:20}') - >>> set(['domain', 'next', 'q', 'top']) - -This function can be useful to determine what keywords are available to be -expanded. - -.. _RFC6570: http://tools.ietf.org/html/rfc6570 - - -Requirements ------------- - -uritemplate works with Python 2.5+. - -.. note:: You need to install `simplejson`_ module for Python 2.5. - -.. _simplejson: https://pypi.python.org/pypi/simplejson/ - - -Install -------- - -The easiest way to install uritemplate is with pip:: - - $ pip install uritemplate - -See its `Python Package Index entry`_ for more. - -.. _Python Package Index entry: http://pypi.python.org/pypi/uritemplate - - -License -======= - -Copyright 2011-2013 Joe Gregorio - -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. diff --git a/third_party/uritemplate/__init__.py b/third_party/uritemplate/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/third_party/uritemplate/setup.py b/third_party/uritemplate/setup.py deleted file mode 100755 index 9b71aae19..000000000 --- a/third_party/uritemplate/setup.py +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env python - -from distutils.core import setup -import uritemplate - -base_url = "http://github.com/uri-templates/uritemplate-py/" - -setup( - name = 'uritemplate', - version = uritemplate.__version__, - description = 'URI Templates', - author = 'Joe Gregorio', - author_email = 'joe@bitworking.org', - url = base_url, - download_url = \ - '%starball/uritemplate-py-%s' % (base_url, uritemplate.__version__), - packages = ['uritemplate'], - provides = ['uritemplate'], - long_description=open("README.rst").read(), - install_requires = ['simplejson >= 2.5.0'], - classifiers = [ - 'Development Status :: 4 - Beta', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: Apache Software License', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.5', - 'Programming Language :: Python :: 2.6', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', - 'Operating System :: POSIX', - 'Topic :: Internet :: WWW/HTTP', - 'Topic :: Software Development :: Libraries :: Python Modules', - ] -) - diff --git a/third_party/uritemplate/uritemplate/__init__.py b/third_party/uritemplate/uritemplate/__init__.py deleted file mode 100755 index 712405d42..000000000 --- a/third_party/uritemplate/uritemplate/__init__.py +++ /dev/null @@ -1,265 +0,0 @@ -#!/usr/bin/env python - -""" -URI Template (RFC6570) Processor -""" - -__copyright__ = """\ -Copyright 2011-2013 Joe Gregorio - -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. -""" - -import re -try: - from urllib.parse import quote -except ImportError: - from urllib import quote - - - -__version__ = "0.6" - -RESERVED = ":/?#[]@!$&'()*+,;=" -OPERATOR = "+#./;?&|!@" -MODIFIER = ":^" -TEMPLATE = re.compile("{([^\}]+)}") - - -def variables(template): - '''Returns the set of keywords in a uri template''' - vars = set() - for varlist in TEMPLATE.findall(template): - if varlist[0] in OPERATOR: - varlist = varlist[1:] - varspecs = varlist.split(',') - for var in varspecs: - # handle prefix values - var = var.split(':')[0] - # handle composite values - if var.endswith('*'): - var = var[:-1] - vars.add(var) - return vars - - -def _quote(value, safe, prefix=None): - if prefix is not None: - return quote(str(value)[:prefix], safe) - return quote(str(value), safe) - - -def _tostring(varname, value, explode, prefix, operator, safe=""): - if isinstance(value, list): - return ",".join([_quote(x, safe) for x in value]) - if isinstance(value, dict): - keys = sorted(value.keys()) - if explode: - return ",".join([_quote(key, safe) + "=" + \ - _quote(value[key], safe) for key in keys]) - else: - return ",".join([_quote(key, safe) + "," + \ - _quote(value[key], safe) for key in keys]) - elif value is None: - return - else: - return _quote(value, safe, prefix) - - -def _tostring_path(varname, value, explode, prefix, operator, safe=""): - joiner = operator - if isinstance(value, list): - if explode: - out = [_quote(x, safe) for x in value if value is not None] - else: - joiner = "," - out = [_quote(x, safe) for x in value if value is not None] - if out: - return joiner.join(out) - else: - return - elif isinstance(value, dict): - keys = sorted(value.keys()) - if explode: - out = [_quote(key, safe) + "=" + \ - _quote(value[key], safe) for key in keys \ - if value[key] is not None] - else: - joiner = "," - out = [_quote(key, safe) + "," + \ - _quote(value[key], safe) \ - for key in keys if value[key] is not None] - if out: - return joiner.join(out) - else: - return - elif value is None: - return - else: - return _quote(value, safe, prefix) - - -def _tostring_semi(varname, value, explode, prefix, operator, safe=""): - joiner = operator - if operator == "?": - joiner = "&" - if isinstance(value, list): - if explode: - out = [varname + "=" + _quote(x, safe) \ - for x in value if x is not None] - if out: - return joiner.join(out) - else: - return - else: - return varname + "=" + ",".join([_quote(x, safe) \ - for x in value]) - elif isinstance(value, dict): - keys = sorted(value.keys()) - if explode: - return joiner.join([_quote(key, safe) + "=" + \ - _quote(value[key], safe) \ - for key in keys if key is not None]) - else: - return varname + "=" + ",".join([_quote(key, safe) + "," + \ - _quote(value[key], safe) for key in keys \ - if key is not None]) - else: - if value is None: - return - elif value: - return (varname + "=" + _quote(value, safe, prefix)) - else: - return varname - - -def _tostring_query(varname, value, explode, prefix, operator, safe=""): - joiner = operator - if operator in ["?", "&"]: - joiner = "&" - if isinstance(value, list): - if 0 == len(value): - return None - if explode: - return joiner.join([varname + "=" + _quote(x, safe) \ - for x in value]) - else: - return (varname + "=" + ",".join([_quote(x, safe) \ - for x in value])) - elif isinstance(value, dict): - if 0 == len(value): - return None - keys = sorted(value.keys()) - if explode: - return joiner.join([_quote(key, safe) + "=" + \ - _quote(value[key], safe) \ - for key in keys]) - else: - return varname + "=" + \ - ",".join([_quote(key, safe) + "," + \ - _quote(value[key], safe) for key in keys]) - else: - if value is None: - return - elif value: - return (varname + "=" + _quote(value, safe, prefix)) - else: - return (varname + "=") - - -TOSTRING = { - "" : _tostring, - "+": _tostring, - "#": _tostring, - ";": _tostring_semi, - "?": _tostring_query, - "&": _tostring_query, - "/": _tostring_path, - ".": _tostring_path, - } - - -def expand(template, variables): - """ - Expand template as a URI Template using variables. - """ - def _sub(match): - expression = match.group(1) - operator = "" - if expression[0] in OPERATOR: - operator = expression[0] - varlist = expression[1:] - else: - varlist = expression - - safe = "" - if operator in ["+", "#"]: - safe = RESERVED - varspecs = varlist.split(",") - varnames = [] - defaults = {} - for varspec in varspecs: - default = None - explode = False - prefix = None - if "=" in varspec: - varname, default = tuple(varspec.split("=", 1)) - else: - varname = varspec - if varname[-1] == "*": - explode = True - varname = varname[:-1] - elif ":" in varname: - try: - prefix = int(varname[varname.index(":")+1:]) - except ValueError: - raise ValueError("non-integer prefix '{0}'".format( - varname[varname.index(":")+1:])) - varname = varname[:varname.index(":")] - if default: - defaults[varname] = default - varnames.append((varname, explode, prefix)) - - retval = [] - joiner = operator - start = operator - if operator == "+": - start = "" - joiner = "," - if operator == "#": - joiner = "," - if operator == "?": - joiner = "&" - if operator == "&": - start = "&" - if operator == "": - joiner = "," - for varname, explode, prefix in varnames: - if varname in variables: - value = variables[varname] - if not value and value != "" and varname in defaults: - value = defaults[varname] - elif varname in defaults: - value = defaults[varname] - else: - continue - expanded = TOSTRING[operator]( - varname, value, explode, prefix, operator, safe=safe) - if expanded is not None: - retval.append(expanded) - if len(retval) > 0: - return start + joiner.join(retval) - else: - return "" - - return TEMPLATE.sub(_sub, template)