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
changes/01/332501/1
sheyang@chromium.org 10 years ago
parent 24daf9e974
commit 002ffa0e3e

@ -1,8 +0,0 @@
# Build artifacts
*.py[cod]
google_api_python_client.egg-info/
build/
dist/
# Test files
.tox/

@ -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/*

@ -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

@ -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

@ -1,6 +0,0 @@
recursive-include apiclient *.json *.py
include CHANGELOG
include LICENSE
include README
include FAQ
include setpath.sh

@ -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

@ -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

@ -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.

@ -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

@ -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 = """<style>
body, h1, h2, h3, div, span, p, pre, a {
margin: 0;
padding: 0;
border: 0;
font-weight: inherit;
font-style: inherit;
font-size: 100%;
font-family: inherit;
vertical-align: baseline;
}
body {
font-size: 13px;
padding: 1em;
}
h1 {
font-size: 26px;
margin-bottom: 1em;
}
h2 {
font-size: 24px;
margin-bottom: 1em;
}
h3 {
font-size: 20px;
margin-bottom: 1em;
margin-top: 1em;
}
pre, code {
line-height: 1.5;
font-family: Monaco, 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Lucida Console', monospace;
}
pre {
margin-top: 0.5em;
}
h1, h2, h3, p {
font-family: Arial, sans serif;
}
h1, h2, h3 {
border-bottom: solid #CCC 1px;
}
.toc_element {
margin-top: 0.5em;
}
.firstline {
margin-left: 2 em;
}
.method {
margin-top: 1em;
border: solid 1px #CCC;
padding: 1em;
background: #EEE;
}
.details {
font-weight: bold;
font-size: 14px;
}
</style>
"""
METHOD_TEMPLATE = """<div class="method">
<code class="details" id="$name">$name($params)</code>
<pre>$doc</pre>
</div>
"""
COLLECTION_LINK = """<p class="toc_element">
<code><a href="$href">$name()</a></code>
</p>
<p class="firstline">Returns the $name Resource.</p>
"""
METHOD_LINK = """<p class="toc_element">
<code><a href="#$name">$name($params)</a></code></p>
<p class="firstline">$firstline</p>"""
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('<a href="%s.html">%s</a>' % (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 = [
'<html><body>',
css,
'<h1>%s</h1>' % breadcrumbs(path[:-1], root_discovery),
'<h2>Instance Methods</h2>'
]
# 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('<h3>Method Details</h3>')
for name in methods:
dname = name.rsplit('_')[0]
html.append(method(name, getattr(resource, name).__doc__))
html.append('</body></html>')
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.")

@ -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()

@ -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"

@ -1,285 +0,0 @@
"""Channel notifications support.
Classes and functions to support channel subscriptions and notifications
on those channels.
Notes:
- This code is based on experimental APIs and is subject to change.
- Notification does not do deduplication of notification ids, that's up to
the receiver.
- Storing the Channel between calls is up to the caller.
Example setting up a channel:
# Create a new channel that gets notifications via webhook.
channel = new_webhook_channel("https://example.com/my_web_hook")
# Store the channel, keyed by 'channel.id'. Store it before calling the
# watch method because notifications may start arriving before the watch
# method returns.
...
resp = service.objects().watchAll(
bucket="some_bucket_id", body=channel.body()).execute()
channel.update(resp)
# Store the channel, keyed by 'channel.id'. Store it after being updated
# since the resource_id value will now be correct, and that's needed to
# stop a subscription.
...
An example Webhook implementation using webapp2. Note that webapp2 puts
headers in a case insensitive dictionary, as headers aren't guaranteed to
always be upper case.
id = self.request.headers[X_GOOG_CHANNEL_ID]
# Retrieve the channel by id.
channel = ...
# Parse notification from the headers, including validating the id.
n = notification_from_headers(channel, self.request.headers)
# Do app specific stuff with the notification here.
if n.resource_state == 'sync':
# Code to handle sync state.
elif n.resource_state == 'exists':
# Code to handle the exists state.
elif n.resource_state == 'not_exists':
# Code to handle the not exists state.
Example of unsubscribing.
service.channels().stop(channel.body())
"""
import datetime
import uuid
from googleapiclient import errors
from ...oauth2client import util
# The unix time epoch starts at midnight 1970.
EPOCH = datetime.datetime.utcfromtimestamp(0)
# Map the names of the parameters in the JSON channel description to
# the parameter names we use in the Channel class.
CHANNEL_PARAMS = {
'address': 'address',
'id': 'id',
'expiration': 'expiration',
'params': 'params',
'resourceId': 'resource_id',
'resourceUri': 'resource_uri',
'type': 'type',
'token': 'token',
}
X_GOOG_CHANNEL_ID = 'X-GOOG-CHANNEL-ID'
X_GOOG_MESSAGE_NUMBER = 'X-GOOG-MESSAGE-NUMBER'
X_GOOG_RESOURCE_STATE = 'X-GOOG-RESOURCE-STATE'
X_GOOG_RESOURCE_URI = 'X-GOOG-RESOURCE-URI'
X_GOOG_RESOURCE_ID = 'X-GOOG-RESOURCE-ID'
def _upper_header_keys(headers):
new_headers = {}
for k, v in headers.iteritems():
new_headers[k.upper()] = v
return new_headers
class Notification(object):
"""A Notification from a Channel.
Notifications are not usually constructed directly, but are returned
from functions like notification_from_headers().
Attributes:
message_number: int, The unique id number of this notification.
state: str, The state of the resource being monitored.
uri: str, The address of the resource being monitored.
resource_id: str, The unique identifier of the version of the resource at
this event.
"""
@util.positional(5)
def __init__(self, message_number, state, resource_uri, resource_id):
"""Notification constructor.
Args:
message_number: int, The unique id number of this notification.
state: str, The state of the resource being monitored. Can be one
of "exists", "not_exists", or "sync".
resource_uri: str, The address of the resource being monitored.
resource_id: str, The identifier of the watched resource.
"""
self.message_number = message_number
self.state = state
self.resource_uri = resource_uri
self.resource_id = resource_id
class Channel(object):
"""A Channel for notifications.
Usually not constructed directly, instead it is returned from helper
functions like new_webhook_channel().
Attributes:
type: str, The type of delivery mechanism used by this channel. For
example, 'web_hook'.
id: str, A UUID for the channel.
token: str, An arbitrary string associated with the channel that
is delivered to the target address with each event delivered
over this channel.
address: str, The address of the receiving entity where events are
delivered. Specific to the channel type.
expiration: int, The time, in milliseconds from the epoch, when this
channel will expire.
params: dict, A dictionary of string to string, with additional parameters
controlling delivery channel behavior.
resource_id: str, An opaque id that identifies the resource that is
being watched. Stable across different API versions.
resource_uri: str, The canonicalized ID of the watched resource.
"""
@util.positional(5)
def __init__(self, type, id, token, address, expiration=None,
params=None, resource_id="", resource_uri=""):
"""Create a new Channel.
In user code, this Channel constructor will not typically be called
manually since there are functions for creating channels for each specific
type with a more customized set of arguments to pass.
Args:
type: str, The type of delivery mechanism used by this channel. For
example, 'web_hook'.
id: str, A UUID for the channel.
token: str, An arbitrary string associated with the channel that
is delivered to the target address with each event delivered
over this channel.
address: str, The address of the receiving entity where events are
delivered. Specific to the channel type.
expiration: int, The time, in milliseconds from the epoch, when this
channel will expire.
params: dict, A dictionary of string to string, with additional parameters
controlling delivery channel behavior.
resource_id: str, An opaque id that identifies the resource that is
being watched. Stable across different API versions.
resource_uri: str, The canonicalized ID of the watched resource.
"""
self.type = type
self.id = id
self.token = token
self.address = address
self.expiration = expiration
self.params = params
self.resource_id = resource_id
self.resource_uri = resource_uri
def body(self):
"""Build a body from the Channel.
Constructs a dictionary that's appropriate for passing into watch()
methods as the value of body argument.
Returns:
A dictionary representation of the channel.
"""
result = {
'id': self.id,
'token': self.token,
'type': self.type,
'address': self.address
}
if self.params:
result['params'] = self.params
if self.resource_id:
result['resourceId'] = self.resource_id
if self.resource_uri:
result['resourceUri'] = self.resource_uri
if self.expiration:
result['expiration'] = self.expiration
return result
def update(self, resp):
"""Update a channel with information from the response of watch().
When a request is sent to watch() a resource, the response returned
from the watch() request is a dictionary with updated channel information,
such as the resource_id, which is needed when stopping a subscription.
Args:
resp: dict, The response from a watch() method.
"""
for json_name, param_name in CHANNEL_PARAMS.iteritems():
value = resp.get(json_name)
if value is not None:
setattr(self, param_name, value)
def notification_from_headers(channel, headers):
"""Parse a notification from the webhook request headers, validate
the notification, and return a Notification object.
Args:
channel: Channel, The channel that the notification is associated with.
headers: dict, A dictionary like object that contains the request headers
from the webhook HTTP request.
Returns:
A Notification object.
Raises:
errors.InvalidNotificationError if the notification is invalid.
ValueError if the X-GOOG-MESSAGE-NUMBER can't be converted to an int.
"""
headers = _upper_header_keys(headers)
channel_id = headers[X_GOOG_CHANNEL_ID]
if channel.id != channel_id:
raise errors.InvalidNotificationError(
'Channel id mismatch: %s != %s' % (channel.id, channel_id))
else:
message_number = int(headers[X_GOOG_MESSAGE_NUMBER])
state = headers[X_GOOG_RESOURCE_STATE]
resource_uri = headers[X_GOOG_RESOURCE_URI]
resource_id = headers[X_GOOG_RESOURCE_ID]
return Notification(message_number, state, resource_uri, resource_id)
@util.positional(2)
def new_webhook_channel(url, token=None, expiration=None, params=None):
"""Create a new webhook Channel.
Args:
url: str, URL to post notifications to.
token: str, An arbitrary string associated with the channel that
is delivered to the target address with each notification delivered
over this channel.
expiration: datetime.datetime, A time in the future when the channel
should expire. Can also be None if the subscription should use the
default expiration. Note that different services may have different
limits on how long a subscription lasts. Check the response from the
watch() method to see the value the service has set for an expiration
time.
params: dict, Extra parameters to pass on channel creation. Currently
not used for webhook channels.
"""
expiration_ms = 0
if expiration:
delta = expiration - EPOCH
expiration_ms = delta.microseconds/1000 + (
delta.seconds + delta.days*24*3600)*1000
if expiration_ms < 0:
expiration_ms = 0
return Channel('web_hook', str(uuid.uuid4()),
token, url, expiration=expiration_ms,
params=params)

@ -1,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__))

@ -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 '<HttpError %s when requesting %s returned "%s">' % (
self.resp.status, self.uri, self._get_reason().strip())
else:
return '<HttpError %s "%s">' % (self.resp.status, self._get_reason())
__str__ = __repr__
class InvalidJsonError(Error):
"""The JSON returned could not be parsed."""
pass
class UnknownFileType(Error):
"""File type unknown or unexpected."""
pass
class UnknownLinkType(Error):
"""Link type unknown or unexpected."""
pass
class UnknownApiNameOrVersion(Error):
"""No API with that name and version exists."""
pass
class UnacceptableMimeTypeError(Error):
"""That is an unacceptable mimetype for this operation."""
pass
class MediaUploadSizeError(Error):
"""Media is larger than the method can accept."""
pass
class ResumableUploadError(HttpError):
"""Error occured during resumable upload."""
pass
class InvalidChunkSizeError(Error):
"""The given chunksize is not valid."""
pass
class InvalidNotificationError(Error):
"""The channel Notification is invalid."""
pass
class BatchError(HttpError):
"""Error occured during batch operations."""
@util.positional(2)
def __init__(self, reason, resp=None, content=None):
self.resp = resp
self.content = content
self.reason = reason
def __repr__(self):
return '<BatchError %s "%s">' % (self.resp.status, self.reason)
__str__ = __repr__
class UnexpectedMethodError(Error):
"""Exception raised by RequestMockBuilder on unexpected calls."""
@util.positional(1)
def __init__(self, methodId=None):
"""Constructor for an UnexpectedMethodError."""
super(UnexpectedMethodError, self).__init__(
'Received unexpected call %s' % methodId)
class UnexpectedBodyError(Error):
"""Exception raised by RequestMockBuilder on unexpected bodies."""
def __init__(self, expected, provided):
"""Constructor for an UnexpectedMethodError."""
super(UnexpectedBodyError, self).__init__(
'Expected: [%s] - Provided: [%s]' % (expected, provided))

File diff suppressed because it is too large Load Diff

@ -1,172 +0,0 @@
# Copyright 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

@ -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

@ -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 <http://code.google.com/apis/console>.
client_secrets = os.path.join(os.path.dirname(filename),
'client_secrets.json')
# Set up a Flow object to be used if we need to authenticate.
flow = client.flow_from_clientsecrets(client_secrets,
scope=scope,
message=tools.message_if_missing(client_secrets))
# Prepare credentials, and authorize HTTP object with them.
# If the credentials don't exist or are invalid run through the native client
# flow. The Storage object will ensure that if successful the good
# credentials will get written back to a file.
storage = file.Storage(name + '.dat')
credentials = storage.get()
if credentials is None or credentials.invalid:
credentials = tools.run_flow(flow, storage, flags)
http = credentials.authorize(http = httplib2.Http())
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)

@ -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)

@ -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 = ['<wiki:toc max_depth="3" />\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('<table border=1 cellspacing=0 cellpadding=8px>\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("""
<tr>
<td>[%(uri)s %(dir)s] </td>
<td> %(desc)s </td>
</tr>""" % context)
page.append('</table>\n')
print ''.join(page)
if __name__ == '__main__':
main()

@ -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',
],
)

@ -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__))

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

@ -1,18 +0,0 @@
[tox]
envlist = py26, py27
[testenv]
deps = keyring
mox
pyopenssl
pycrypto==2.6
django
webtest
nose
setenv = PYTHONPATH=../google_appengine
[testenv:py26]
commands = nosetests --ignore-files=test_oauth2client_appengine\.py
[testenv:py27]
commands = nosetests

@ -1,5 +0,0 @@
*.pyc
build
dist
MANIFEST

@ -1,3 +0,0 @@
[submodule "test/cases"]
path = test/cases
url = git://github.com/uri-templates/uritemplate-test.git

@ -1,11 +0,0 @@
language: python
python:
- "2.5"
- "2.6"
- "2.7"
- "3.3"
- "pypy"
# dependencies
install: pip install simplejson --use-mirrors
# command to run tests
script: "cd test; make"

@ -1,14 +0,0 @@
Instructions for Maintainers
============================
Release
-------
To release a build:
1. Push all changes, verify CI passes (see link in README.rst).
2. Increment __version__ in __init__.py
3. ``git tag -a uri-template-py-[version]``
4. ``git push --tags origin master``
5. ``python setup.py sdist upload``
6. Brew coffee or tea.

@ -1 +0,0 @@
include README.rst

@ -1,6 +0,0 @@
URL: https://github.com/uri-templates/uritemplate-py/
Version: 0.6
Revision: 1e780a49412cdbb273e9421974cb91845c124f3f
License: Apache License, Version 2.0 (the "License")
No local changes

@ -1,71 +0,0 @@
uritemplate
===========
.. image:: https://secure.travis-ci.org/uri-templates/uritemplate-py.png?branch=master
:alt: build status
:target: http://travis-ci.org/uri-templates/uritemplate-py
This is a Python implementation of `RFC6570`_, URI Template, and can
expand templates up to and including Level 4 in that specification.
It exposes a method, *expand*. For example:
.. code-block:: python
>>> 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.

@ -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',
]
)

@ -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)
Loading…
Cancel
Save