Commit 67e58a51 authored by sheyang@google.com's avatar sheyang@google.com

Added OAuth2 support in depot_tools

BUG=356813
R=nodir@chromium.org, vadimsh@chromium.org

Review URL: https://codereview.chromium.org/963953003

git-svn-id: svn://svn.chromium.org/chrome/trunk/tools/depot_tools@294698 0039d316-1c4b-4281-b951-d872f2087c98
parent 46309bf4
# Copyright (c) 2015 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""OAuth2 related utilities and implementation for git cl commands."""
import copy
import logging
import optparse
import os
from third_party.oauth2client import tools
from third_party.oauth2client.file import Storage
import third_party.oauth2client.client as oa2client
REDIRECT_URI = 'urn:ietf:wg:oauth:2.0:oob'
CLIENT_ID = ('174799409470-8k3b89iov4racu9jrf7if3k4591voig3'
'.apps.googleusercontent.com')
CLIENT_SECRET = 'DddcCK1d6_ADwxqGDEGlsisy'
SCOPE = 'email'
def _fetch_storage(code_review_server):
storage_dir = os.path.expanduser(os.path.join('~', '.git_cl_credentials'))
if not os.path.isdir(storage_dir):
os.makedirs(storage_dir)
storage_path = os.path.join(storage_dir, code_review_server)
storage = Storage(storage_path)
return storage
def _fetch_creds_from_storage(storage):
logging.debug('Fetching OAuth2 credentials from local storage ...')
credentials = storage.get()
if not credentials or credentials.invalid:
return None
if not credentials.access_token or credentials.access_token_expired:
return None
return credentials
def add_oauth2_options(parser):
"""Add OAuth2-related options."""
group = optparse.OptionGroup(parser, "OAuth2 options")
group.add_option(
'--auth-host-name',
default='localhost',
help='Host name to use when running a local web server '
'to handle redirects during OAuth authorization.'
'Default: localhost.'
)
group.add_option(
'--auth-host-port',
type=int,
action='append',
default=[8080, 8090],
help='Port to use when running a local web server to handle '
'redirects during OAuth authorization. '
'Repeat this option to specify a list of values.'
'Default: [8080, 8090].'
)
group.add_option(
'--noauth-local-webserver',
action='store_true',
default=False,
help='Run a local web server to handle redirects '
'during OAuth authorization.'
'Default: False.'
)
group.add_option(
'--no-cache',
action='store_true',
default=False,
help='Get fresh credentials from web server instead of using '
'the crendentials stored on a local storage file.'
'Default: False.'
)
parser.add_option_group(group)
def get_oauth2_creds(options, code_review_server):
"""Get OAuth2 credentials.
Args:
options: Command line options.
code_review_server: Code review server name, e.g., codereview.chromium.org.
"""
storage = _fetch_storage(code_review_server)
creds = None
if not options.no_cache:
creds = _fetch_creds_from_storage(storage)
if creds is None:
logging.debug('Fetching OAuth2 credentials from web server...')
flow = oa2client.OAuth2WebServerFlow(
client_id=CLIENT_ID,
client_secret=CLIENT_SECRET,
scope=SCOPE,
redirect_uri=REDIRECT_URI)
flags = copy.deepcopy(options)
flags.logging_level = 'WARNING'
creds = tools.run_flow(flow, storage, flags)
return creds
# Build artifacts
*.py[cod]
google_api_python_client.egg-info/
build/
dist/
# Test files
.tox/
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/*
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
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
recursive-include apiclient *.json *.py
include CHANGELOG
include LICENSE
include README
include FAQ
include setpath.sh
pep8:
find googleapiclient samples -name "*.py" | xargs pep8 --ignore=E111,E202
APP_ENGINE_PATH=../google_appengine
test:
tox
.PHONY: coverage
coverage:
coverage erase
find tests -name "test_*.py" | xargs --max-args=1 coverage run -a runtests.py
coverage report
coverage html
.PHONY: docs
docs:
cd docs; ./build
mkdir -p docs/dyn
python describe.py
.PHONY: wiki
wiki:
python samples-index.py > ../google-api-python-client.wiki/SampleApps.wiki
.PHONY: prerelease
prerelease:
-rm -rf dist/
-sudo rm -rf dist/
-rm -rf snapshot/
-sudo rm -rf snapshot/
# ./tools/gae-zip-creator.sh
python expandsymlinks.py
cd snapshot; python setup.py clean
cd snapshot; python setup.py sdist --formats=gztar,zip
cd snapshot; tar czf google-api-python-client-samples-$(shell python setup.py --version).tar.gz samples
cd snapshot; zip -r google-api-python-client-samples-$(shell python setup.py --version).zip samples
.PHONY: release
release: prerelease
@echo "This target will upload a new release to PyPi and code.google.com hosting."
@echo "Are you sure you want to proceed? (yes/no)"
@read yn; if [ yes -ne $(yn) ]; then exit 1; fi
@echo "Here we go..."
cd snapshot; python setup.py sdist --formats=gztar,zip register upload
\ No newline at end of file
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
# 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.
"""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
This diff is collapsed.
#!/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()
# 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"
"""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)
#!/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))
This diff is collapsed.
# 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
This diff is collapsed.
# 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)
# 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)
This diff is collapsed.
This diff is collapsed.
# 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__))
[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
*.pyc
build
dist
MANIFEST
[submodule "test/cases"]
path = test/cases
url = git://github.com/uri-templates/uritemplate-test.git
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"
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.
URL: https://github.com/uri-templates/uritemplate-py/
Version: 0.6
Revision: 1e780a49412cdbb273e9421974cb91845c124f3f
License: Apache License, Version 2.0 (the "License")
No local changes
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment