├── .gitignore ├── static ├── robots.txt ├── favicon.ico └── apple-touch-icon.png ├── oauth2client ├── __init__.py ├── anyjson.py ├── gce.py ├── keyring_storage.py ├── file.py ├── xsrfutil.py ├── django_orm.py ├── clientsecrets.py ├── old_run.py ├── util.py ├── tools.py ├── crypt.py ├── locked_file.py ├── multistore_file.py └── appengine.py ├── cron.yaml ├── index.yaml ├── app.yaml ├── database_tables.py ├── README.md ├── apiclient ├── __init__.py ├── sample_tools.py ├── errors.py ├── mimeparse.py ├── schema.py ├── channel.py └── model.py ├── Acknowledgements.txt ├── license.txt ├── youtube_integration.py ├── httplib2 ├── iri2uri.py └── socks.py ├── uritemplate └── __init__.py └── main.py /.gitignore: -------------------------------------------------------------------------------- 1 | api_key.txt 2 | **.pyc 3 | -------------------------------------------------------------------------------- /static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: /update 3 | Disallow: /update_push -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ncurrault/brady-vs-grey/HEAD/static/favicon.ico -------------------------------------------------------------------------------- /static/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ncurrault/brady-vs-grey/HEAD/static/apple-touch-icon.png -------------------------------------------------------------------------------- /oauth2client/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "1.2" 2 | 3 | GOOGLE_AUTH_URI = 'https://accounts.google.com/o/oauth2/auth' 4 | GOOGLE_REVOKE_URI = 'https://accounts.google.com/o/oauth2/revoke' 5 | GOOGLE_TOKEN_URI = 'https://accounts.google.com/o/oauth2/token' 6 | -------------------------------------------------------------------------------- /cron.yaml: -------------------------------------------------------------------------------- 1 | cron: 2 | - description: 4x daily update to push to the database 3 | url: /update 4 | schedule: every 6 hours from 00:00 to 23:59 5 | - description: hourly database query to update memcache; always at least 5 min after an update 6 | url: /update_push 7 | schedule: every 1 hours from 00:05 to 23:10 -------------------------------------------------------------------------------- /index.yaml: -------------------------------------------------------------------------------- 1 | indexes: 2 | 3 | # AUTOGENERATED 4 | 5 | # This index.yaml is automatically updated whenever the dev_appserver 6 | # detects that a new type of query is run. If you want to manage the 7 | # index.yaml file manually, remove the above marker line (the line 8 | # saying "# AUTOGENERATED"). If you want to manage some indexes 9 | # manually, move them above the marker line. The index.yaml file is 10 | # automatically uploaded to the admin console when you next deploy 11 | # your application using appcfg.py. 12 | -------------------------------------------------------------------------------- /app.yaml: -------------------------------------------------------------------------------- 1 | application: brady-vs-grey 2 | version: 1 3 | runtime: python27 4 | api_version: 1 5 | threadsafe: yes 6 | 7 | handlers: 8 | - url: /favicon\.ico 9 | static_files: static/favicon.ico 10 | upload: static/favicon.ico 11 | 12 | - url: /apple-touch-icon\.png 13 | static_files: static/apple-touch-icon.png 14 | upload: static/apple-touch-icon.png 15 | 16 | - url: /robots\.txt 17 | static_files: static/robots.txt 18 | upload: static/robots.txt 19 | 20 | - url: .* 21 | script: main.app 22 | 23 | libraries: 24 | - name: webapp2 25 | version: "2.5.2" 26 | -------------------------------------------------------------------------------- /database_tables.py: -------------------------------------------------------------------------------- 1 | from google.appengine.ext import db 2 | 3 | class Video(db.Model): 4 | yt_id = db.StringProperty() 5 | title = db.StringProperty() 6 | published = db.DateTimeProperty() 7 | channel = db.StringProperty() 8 | 9 | viewcount = db.IntegerProperty() 10 | # [0,inf) for actual view count 11 | # -1 for live videos 12 | # -2 for not yet calculated 13 | # -3 for misc. errors 14 | 15 | 16 | class BradyVideo(Video): 17 | pass 18 | class GreyVideo(Video): 19 | pass 20 | 21 | class UpdateLog(db.Model): 22 | update_time = db.DateTimeProperty(auto_now=True) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Brady vs. Grey 2 | 3 | ## API Key Hiding (`api_key.txt`) 4 | 5 | To make sure that my own YouTube Data API key was not published, 6 | I separated it into a text file (`api_key.txt`) and used `.gitignore` to ensure that it wasn't published. 7 | 8 | If you wish to do this yourself, you must 9 | 10 | 1. [Get your own API key from Google](http://developers.google.com/youtube/v3/getting-started#intro). 11 | 2. Put this key in a file entitled `api_key.txt`. 12 | 3. Put this file in the same directory as `youtube_integration.py`. 13 | 14 | 15 | ## Enjoy! 16 | 17 | You can view the running version [here](http://brady-vs-grey.appspot.com). 18 | -------------------------------------------------------------------------------- /apiclient/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2012 Google Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | __version__ = "1.2" 16 | -------------------------------------------------------------------------------- /Acknowledgements.txt: -------------------------------------------------------------------------------- 1 | The following software may be included in this product: 2 | Google APIs Client Library for Python; Use of any of this software is governed by the terms of the license below: 3 | 4 | Copyright 2014 Google Inc. All Rights Reserved. 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | 18 | Dependent Modules 19 | ================= 20 | 21 | This code has the following dependencies 22 | above and beyond the Python standard library: 23 | 24 | uritemplates - Apache License 2.0 25 | httplib2 - MIT License 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Nicholas Currault 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /oauth2client/anyjson.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2010 Google Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Utility module to import a JSON module 16 | 17 | Hides all the messy details of exactly where 18 | we get a simplejson module from. 19 | """ 20 | 21 | __author__ = 'jcgregorio@google.com (Joe Gregorio)' 22 | 23 | 24 | try: # pragma: no cover 25 | # Should work for Python2.6 and higher. 26 | import json as simplejson 27 | except ImportError: # pragma: no cover 28 | try: 29 | import simplejson 30 | except ImportError: 31 | # Try to import from django, should work on App Engine 32 | from django.utils import simplejson 33 | -------------------------------------------------------------------------------- /oauth2client/gce.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2012 Google Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Utilities for Google Compute Engine 16 | 17 | Utilities for making it easier to use OAuth 2.0 on Google Compute Engine. 18 | """ 19 | 20 | __author__ = 'jcgregorio@google.com (Joe Gregorio)' 21 | 22 | import httplib2 23 | import logging 24 | import uritemplate 25 | 26 | from oauth2client import util 27 | from oauth2client.anyjson import simplejson 28 | from oauth2client.client import AccessTokenRefreshError 29 | from oauth2client.client import AssertionCredentials 30 | 31 | logger = logging.getLogger(__name__) 32 | 33 | # URI Template for the endpoint that returns access_tokens. 34 | META = ('http://metadata.google.internal/0.1/meta-data/service-accounts/' 35 | 'default/acquire{?scope}') 36 | 37 | 38 | class AppAssertionCredentials(AssertionCredentials): 39 | """Credentials object for Compute Engine Assertion Grants 40 | 41 | This object will allow a Compute Engine instance to identify itself to 42 | Google and other OAuth 2.0 servers that can verify assertions. It can be used 43 | for the purpose of accessing data stored under an account assigned to the 44 | Compute Engine instance itself. 45 | 46 | This credential does not require a flow to instantiate because it represents 47 | a two legged flow, and therefore has all of the required information to 48 | generate and refresh its own access tokens. 49 | """ 50 | 51 | @util.positional(2) 52 | def __init__(self, scope, **kwargs): 53 | """Constructor for AppAssertionCredentials 54 | 55 | Args: 56 | scope: string or iterable of strings, scope(s) of the credentials being 57 | requested. 58 | """ 59 | self.scope = util.scopes_to_string(scope) 60 | 61 | # Assertion type is no longer used, but still in the parent class signature. 62 | super(AppAssertionCredentials, self).__init__(None) 63 | 64 | @classmethod 65 | def from_json(cls, json): 66 | data = simplejson.loads(json) 67 | return AppAssertionCredentials(data['scope']) 68 | 69 | def _refresh(self, http_request): 70 | """Refreshes the access_token. 71 | 72 | Skip all the storage hoops and just refresh using the API. 73 | 74 | Args: 75 | http_request: callable, a callable that matches the method signature of 76 | httplib2.Http.request, used to make the refresh request. 77 | 78 | Raises: 79 | AccessTokenRefreshError: When the refresh fails. 80 | """ 81 | uri = uritemplate.expand(META, {'scope': self.scope}) 82 | response, content = http_request(uri) 83 | if response.status == 200: 84 | try: 85 | d = simplejson.loads(content) 86 | except StandardError, e: 87 | raise AccessTokenRefreshError(str(e)) 88 | self.access_token = d['accessToken'] 89 | else: 90 | raise AccessTokenRefreshError(content) 91 | -------------------------------------------------------------------------------- /oauth2client/keyring_storage.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2012 Google Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """A keyring based Storage. 16 | 17 | A Storage for Credentials that uses the keyring module. 18 | """ 19 | 20 | __author__ = 'jcgregorio@google.com (Joe Gregorio)' 21 | 22 | import keyring 23 | import threading 24 | 25 | from client import Storage as BaseStorage 26 | from client import Credentials 27 | 28 | 29 | class Storage(BaseStorage): 30 | """Store and retrieve a single credential to and from the keyring. 31 | 32 | To use this module you must have the keyring module installed. See 33 | . This is an optional module and is not 34 | installed with oauth2client by default because it does not work on all the 35 | platforms that oauth2client supports, such as Google App Engine. 36 | 37 | The keyring module is a cross-platform 38 | library for access the keyring capabilities of the local system. The user will 39 | be prompted for their keyring password when this module is used, and the 40 | manner in which the user is prompted will vary per platform. 41 | 42 | Usage: 43 | from oauth2client.keyring_storage import Storage 44 | 45 | s = Storage('name_of_application', 'user1') 46 | credentials = s.get() 47 | 48 | """ 49 | 50 | def __init__(self, service_name, user_name): 51 | """Constructor. 52 | 53 | Args: 54 | service_name: string, The name of the service under which the credentials 55 | are stored. 56 | user_name: string, The name of the user to store credentials for. 57 | """ 58 | self._service_name = service_name 59 | self._user_name = user_name 60 | self._lock = threading.Lock() 61 | 62 | def acquire_lock(self): 63 | """Acquires any lock necessary to access this Storage. 64 | 65 | This lock is not reentrant.""" 66 | self._lock.acquire() 67 | 68 | def release_lock(self): 69 | """Release the Storage lock. 70 | 71 | Trying to release a lock that isn't held will result in a 72 | RuntimeError. 73 | """ 74 | self._lock.release() 75 | 76 | def locked_get(self): 77 | """Retrieve Credential from file. 78 | 79 | Returns: 80 | oauth2client.client.Credentials 81 | """ 82 | credentials = None 83 | content = keyring.get_password(self._service_name, self._user_name) 84 | 85 | if content is not None: 86 | try: 87 | credentials = Credentials.new_from_json(content) 88 | credentials.set_store(self) 89 | except ValueError: 90 | pass 91 | 92 | return credentials 93 | 94 | def locked_put(self, credentials): 95 | """Write Credentials to file. 96 | 97 | Args: 98 | credentials: Credentials, the credentials to store. 99 | """ 100 | keyring.set_password(self._service_name, self._user_name, 101 | credentials.to_json()) 102 | 103 | def locked_delete(self): 104 | """Delete Credentials file. 105 | 106 | Args: 107 | credentials: Credentials, the credentials to store. 108 | """ 109 | keyring.set_password(self._service_name, self._user_name, '') 110 | -------------------------------------------------------------------------------- /oauth2client/file.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2010 Google Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Utilities for OAuth. 16 | 17 | Utilities for making it easier to work with OAuth 2.0 18 | credentials. 19 | """ 20 | 21 | __author__ = 'jcgregorio@google.com (Joe Gregorio)' 22 | 23 | import os 24 | import stat 25 | import threading 26 | 27 | from anyjson import simplejson 28 | from client import Storage as BaseStorage 29 | from client import Credentials 30 | 31 | 32 | class CredentialsFileSymbolicLinkError(Exception): 33 | """Credentials files must not be symbolic links.""" 34 | 35 | 36 | class Storage(BaseStorage): 37 | """Store and retrieve a single credential to and from a file.""" 38 | 39 | def __init__(self, filename): 40 | self._filename = filename 41 | self._lock = threading.Lock() 42 | 43 | def _validate_file(self): 44 | if os.path.islink(self._filename): 45 | raise CredentialsFileSymbolicLinkError( 46 | 'File: %s is a symbolic link.' % self._filename) 47 | 48 | def acquire_lock(self): 49 | """Acquires any lock necessary to access this Storage. 50 | 51 | This lock is not reentrant.""" 52 | self._lock.acquire() 53 | 54 | def release_lock(self): 55 | """Release the Storage lock. 56 | 57 | Trying to release a lock that isn't held will result in a 58 | RuntimeError. 59 | """ 60 | self._lock.release() 61 | 62 | def locked_get(self): 63 | """Retrieve Credential from file. 64 | 65 | Returns: 66 | oauth2client.client.Credentials 67 | 68 | Raises: 69 | CredentialsFileSymbolicLinkError if the file is a symbolic link. 70 | """ 71 | credentials = None 72 | self._validate_file() 73 | try: 74 | f = open(self._filename, 'rb') 75 | content = f.read() 76 | f.close() 77 | except IOError: 78 | return credentials 79 | 80 | try: 81 | credentials = Credentials.new_from_json(content) 82 | credentials.set_store(self) 83 | except ValueError: 84 | pass 85 | 86 | return credentials 87 | 88 | def _create_file_if_needed(self): 89 | """Create an empty file if necessary. 90 | 91 | This method will not initialize the file. Instead it implements a 92 | simple version of "touch" to ensure the file has been created. 93 | """ 94 | if not os.path.exists(self._filename): 95 | old_umask = os.umask(0177) 96 | try: 97 | open(self._filename, 'a+b').close() 98 | finally: 99 | os.umask(old_umask) 100 | 101 | def locked_put(self, credentials): 102 | """Write Credentials to file. 103 | 104 | Args: 105 | credentials: Credentials, the credentials to store. 106 | 107 | Raises: 108 | CredentialsFileSymbolicLinkError if the file is a symbolic link. 109 | """ 110 | 111 | self._create_file_if_needed() 112 | self._validate_file() 113 | f = open(self._filename, 'wb') 114 | f.write(credentials.to_json()) 115 | f.close() 116 | 117 | def locked_delete(self): 118 | """Delete Credentials file. 119 | 120 | Args: 121 | credentials: Credentials, the credentials to store. 122 | """ 123 | 124 | os.unlink(self._filename) 125 | -------------------------------------------------------------------------------- /apiclient/sample_tools.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2013 Google Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Utilities for making samples. 16 | 17 | Consolidates a lot of code commonly repeated in sample applications. 18 | """ 19 | 20 | __author__ = 'jcgregorio@google.com (Joe Gregorio)' 21 | __all__ = ['init'] 22 | 23 | 24 | import argparse 25 | import httplib2 26 | import os 27 | 28 | from apiclient import discovery 29 | from oauth2client import client 30 | from oauth2client import file 31 | from oauth2client import tools 32 | 33 | 34 | def init(argv, name, version, doc, filename, scope=None, parents=[]): 35 | """A common initialization routine for samples. 36 | 37 | Many of the sample applications do the same initialization, which has now 38 | been consolidated into this function. This function uses common idioms found 39 | in almost all the samples, i.e. for an API with name 'apiname', the 40 | credentials are stored in a file named apiname.dat, and the 41 | client_secrets.json file is stored in the same directory as the application 42 | main file. 43 | 44 | Args: 45 | argv: list of string, the command-line parameters of the application. 46 | name: string, name of the API. 47 | version: string, version of the API. 48 | doc: string, description of the application. Usually set to __doc__. 49 | file: string, filename of the application. Usually set to __file__. 50 | parents: list of argparse.ArgumentParser, additional command-line flags. 51 | scope: string, The OAuth scope used. 52 | 53 | Returns: 54 | A tuple of (service, flags), where service is the service object and flags 55 | is the parsed command-line flags. 56 | """ 57 | if scope is None: 58 | scope = 'https://www.googleapis.com/auth/' + name 59 | 60 | # Parser command-line arguments. 61 | parent_parsers = [tools.argparser] 62 | parent_parsers.extend(parents) 63 | parser = argparse.ArgumentParser( 64 | description=doc, 65 | formatter_class=argparse.RawDescriptionHelpFormatter, 66 | parents=parent_parsers) 67 | flags = parser.parse_args(argv[1:]) 68 | 69 | # Name of a file containing the OAuth 2.0 information for this 70 | # application, including client_id and client_secret, which are found 71 | # on the API Access tab on the Google APIs 72 | # Console . 73 | client_secrets = os.path.join(os.path.dirname(filename), 74 | 'client_secrets.json') 75 | 76 | # Set up a Flow object to be used if we need to authenticate. 77 | flow = client.flow_from_clientsecrets(client_secrets, 78 | scope=scope, 79 | message=tools.message_if_missing(client_secrets)) 80 | 81 | # Prepare credentials, and authorize HTTP object with them. 82 | # If the credentials don't exist or are invalid run through the native client 83 | # flow. The Storage object will ensure that if successful the good 84 | # credentials will get written back to a file. 85 | storage = file.Storage(name + '.dat') 86 | credentials = storage.get() 87 | if credentials is None or credentials.invalid: 88 | credentials = tools.run_flow(flow, storage, flags) 89 | http = credentials.authorize(http = httplib2.Http()) 90 | 91 | # Construct a service object via the discovery service. 92 | service = discovery.build(name, version, http=http) 93 | return (service, flags) 94 | -------------------------------------------------------------------------------- /youtube_integration.py: -------------------------------------------------------------------------------- 1 | import os 2 | import urllib 3 | 4 | from apiclient.discovery import build 5 | from optparse import OptionParser 6 | 7 | import json 8 | 9 | import datetime 10 | import time 11 | 12 | from database_tables import * 13 | 14 | REGISTRATION_INSTRUCTIONS = """ 15 | You must set up a project and get an API key to run this code. Please see 16 | the instructions for creating a project and a key at https://developers.google.com/youtube/registering_an_application. 19 |

20 | Make sure that you have enabled the YouTube Data API (v3) and the Freebase 21 | API for your project.""" 22 | 23 | # Set API_KEY to the "API key" value from the "Access" tab of the 24 | # Google APIs Console http://code.google.com/apis/console#access 25 | # Please ensure that you have enabled the YouTube Data API and Freebase API 26 | # for your project. 27 | with open('api_key.txt', 'r') as key_file: 28 | API_KEY = key_file.read() 29 | 30 | YOUTUBE_API_SERVICE_NAME = "youtube" 31 | YOUTUBE_API_VERSION = "v3" 32 | FREEBASE_SEARCH_URL = "https://www.googleapis.com/freebase/v1/search?%s" 33 | QUERY_TERM = "dog" 34 | 35 | 36 | 37 | def get_view_count(vid_id): 38 | try: 39 | url = "https://www.googleapis.com/youtube/v3/videos?id=%(id)s&key=%(key)s&fields=items(id,snippet(channelId,title,categoryId),statistics)&part=snippet,statistics" % {'key':API_KEY, 'id':vid_id} 40 | p = urllib.urlopen(url) 41 | json_data = p.read() 42 | p.close() 43 | json_data = json.loads(json_data) 44 | 45 | views = int(json_data['items'][0]['statistics']['viewCount']) 46 | return views 47 | except KeyError: 48 | return -1 # For live videos 49 | except: 50 | return -3 # For misc. errors 51 | 52 | 53 | def get_vids(input_channel_name, save_class='Video'): 54 | # Service for calling the YouTube API 55 | youtube = build(YOUTUBE_API_SERVICE_NAME, 56 | YOUTUBE_API_VERSION, 57 | developerKey=API_KEY) 58 | 59 | # Use form inputs to create request params for channel details 60 | channels_response = None 61 | 62 | channels_response = youtube.channels().list( 63 | forUsername=input_channel_name, 64 | part='snippet,contentDetails' 65 | ).execute() 66 | 67 | channel_name = '' 68 | videos = [] 69 | 70 | for channel in channels_response['items']: 71 | uploads_list_id = channel['contentDetails']['relatedPlaylists']['uploads'] 72 | channel_name = channel['snippet']['title'] 73 | 74 | next_page_token = '' 75 | while next_page_token is not None: 76 | playlistitems_response = youtube.playlistItems().list( 77 | playlistId=uploads_list_id, 78 | part='snippet', 79 | maxResults=50, 80 | pageToken=next_page_token 81 | ).execute() 82 | 83 | for playlist_item in playlistitems_response['items']: 84 | videos.append(playlist_item) 85 | 86 | next_page_token = playlistitems_response.get('tokenPagination', {}).get( 87 | 'nextPageToken') 88 | 89 | if save_class == 'BradyVideo' and len(videos) > 20: # One Brady's channels shouldn't have MORE THAN 20 that are necessary 90 | break 91 | elif save_class == 'GreyVideo' and len(videos) > 1: # For Grey's channels, you only need the most recent video 92 | break 93 | elif len(videos) > 100: # This shouldn't actually happen 94 | break 95 | 96 | 97 | 98 | return [ \ 99 | eval(save_class)( 100 | title=e[u'snippet'][u'title'], \ 101 | channel=input_channel_name, \ 102 | published=(datetime.datetime.strptime(e[u'snippet'][u'publishedAt'][:-5],'%Y-%m-%dT%H:%M:%S')), \ 103 | yt_id=(e[u'snippet'][u'resourceId'][u'videoId']), \ 104 | viewcount=-2 \ 105 | ) \ 106 | for e in videos ] -------------------------------------------------------------------------------- /oauth2client/xsrfutil.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2.5 2 | # 3 | # Copyright 2010 the Melange authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | """Helper methods for creating & verifying XSRF tokens.""" 18 | 19 | __authors__ = [ 20 | '"Doug Coker" ', 21 | '"Joe Gregorio" ', 22 | ] 23 | 24 | 25 | import base64 26 | import hmac 27 | import os # for urandom 28 | import time 29 | 30 | from oauth2client import util 31 | 32 | 33 | # Delimiter character 34 | DELIMITER = ':' 35 | 36 | # 1 hour in seconds 37 | DEFAULT_TIMEOUT_SECS = 1*60*60 38 | 39 | @util.positional(2) 40 | def generate_token(key, user_id, action_id="", when=None): 41 | """Generates a URL-safe token for the given user, action, time tuple. 42 | 43 | Args: 44 | key: secret key to use. 45 | user_id: the user ID of the authenticated user. 46 | action_id: a string identifier of the action they requested 47 | authorization for. 48 | when: the time in seconds since the epoch at which the user was 49 | authorized for this action. If not set the current time is used. 50 | 51 | Returns: 52 | A string XSRF protection token. 53 | """ 54 | when = when or int(time.time()) 55 | digester = hmac.new(key) 56 | digester.update(str(user_id)) 57 | digester.update(DELIMITER) 58 | digester.update(action_id) 59 | digester.update(DELIMITER) 60 | digester.update(str(when)) 61 | digest = digester.digest() 62 | 63 | token = base64.urlsafe_b64encode('%s%s%d' % (digest, 64 | DELIMITER, 65 | when)) 66 | return token 67 | 68 | 69 | @util.positional(3) 70 | def validate_token(key, token, user_id, action_id="", current_time=None): 71 | """Validates that the given token authorizes the user for the action. 72 | 73 | Tokens are invalid if the time of issue is too old or if the token 74 | does not match what generateToken outputs (i.e. the token was forged). 75 | 76 | Args: 77 | key: secret key to use. 78 | token: a string of the token generated by generateToken. 79 | user_id: the user ID of the authenticated user. 80 | action_id: a string identifier of the action they requested 81 | authorization for. 82 | 83 | Returns: 84 | A boolean - True if the user is authorized for the action, False 85 | otherwise. 86 | """ 87 | if not token: 88 | return False 89 | try: 90 | decoded = base64.urlsafe_b64decode(str(token)) 91 | token_time = long(decoded.split(DELIMITER)[-1]) 92 | except (TypeError, ValueError): 93 | return False 94 | if current_time is None: 95 | current_time = time.time() 96 | # If the token is too old it's not valid. 97 | if current_time - token_time > DEFAULT_TIMEOUT_SECS: 98 | return False 99 | 100 | # The given token should match the generated one with the same time. 101 | expected_token = generate_token(key, user_id, action_id=action_id, 102 | when=token_time) 103 | if len(token) != len(expected_token): 104 | return False 105 | 106 | # Perform constant time comparison to avoid timing attacks 107 | different = 0 108 | for x, y in zip(token, expected_token): 109 | different |= ord(x) ^ ord(y) 110 | if different: 111 | return False 112 | 113 | return True 114 | -------------------------------------------------------------------------------- /apiclient/errors.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2.4 2 | # 3 | # Copyright (C) 2010 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | """Errors for the library. 18 | 19 | All exceptions defined by the library 20 | should be defined in this file. 21 | """ 22 | 23 | __author__ = 'jcgregorio@google.com (Joe Gregorio)' 24 | 25 | 26 | from oauth2client import util 27 | from oauth2client.anyjson import simplejson 28 | 29 | 30 | class Error(Exception): 31 | """Base error for this module.""" 32 | pass 33 | 34 | 35 | class HttpError(Error): 36 | """HTTP data was invalid or unexpected.""" 37 | 38 | @util.positional(3) 39 | def __init__(self, resp, content, uri=None): 40 | self.resp = resp 41 | self.content = content 42 | self.uri = uri 43 | 44 | def _get_reason(self): 45 | """Calculate the reason for the error from the response content.""" 46 | reason = self.resp.reason 47 | try: 48 | data = simplejson.loads(self.content) 49 | reason = data['error']['message'] 50 | except (ValueError, KeyError): 51 | pass 52 | if reason is None: 53 | reason = '' 54 | return reason 55 | 56 | def __repr__(self): 57 | if self.uri: 58 | return '' % ( 59 | self.resp.status, self.uri, self._get_reason().strip()) 60 | else: 61 | return '' % (self.resp.status, self._get_reason()) 62 | 63 | __str__ = __repr__ 64 | 65 | 66 | class InvalidJsonError(Error): 67 | """The JSON returned could not be parsed.""" 68 | pass 69 | 70 | 71 | class UnknownFileType(Error): 72 | """File type unknown or unexpected.""" 73 | pass 74 | 75 | 76 | class UnknownLinkType(Error): 77 | """Link type unknown or unexpected.""" 78 | pass 79 | 80 | 81 | class UnknownApiNameOrVersion(Error): 82 | """No API with that name and version exists.""" 83 | pass 84 | 85 | 86 | class UnacceptableMimeTypeError(Error): 87 | """That is an unacceptable mimetype for this operation.""" 88 | pass 89 | 90 | 91 | class MediaUploadSizeError(Error): 92 | """Media is larger than the method can accept.""" 93 | pass 94 | 95 | 96 | class ResumableUploadError(HttpError): 97 | """Error occured during resumable upload.""" 98 | pass 99 | 100 | 101 | class InvalidChunkSizeError(Error): 102 | """The given chunksize is not valid.""" 103 | pass 104 | 105 | class InvalidNotificationError(Error): 106 | """The channel Notification is invalid.""" 107 | pass 108 | 109 | class BatchError(HttpError): 110 | """Error occured during batch operations.""" 111 | 112 | @util.positional(2) 113 | def __init__(self, reason, resp=None, content=None): 114 | self.resp = resp 115 | self.content = content 116 | self.reason = reason 117 | 118 | def __repr__(self): 119 | return '' % (self.resp.status, self.reason) 120 | 121 | __str__ = __repr__ 122 | 123 | 124 | class UnexpectedMethodError(Error): 125 | """Exception raised by RequestMockBuilder on unexpected calls.""" 126 | 127 | @util.positional(1) 128 | def __init__(self, methodId=None): 129 | """Constructor for an UnexpectedMethodError.""" 130 | super(UnexpectedMethodError, self).__init__( 131 | 'Received unexpected call %s' % methodId) 132 | 133 | 134 | class UnexpectedBodyError(Error): 135 | """Exception raised by RequestMockBuilder on unexpected bodies.""" 136 | 137 | def __init__(self, expected, provided): 138 | """Constructor for an UnexpectedMethodError.""" 139 | super(UnexpectedBodyError, self).__init__( 140 | 'Expected: [%s] - Provided: [%s]' % (expected, provided)) 141 | -------------------------------------------------------------------------------- /httplib2/iri2uri.py: -------------------------------------------------------------------------------- 1 | """ 2 | iri2uri 3 | 4 | Converts an IRI to a URI. 5 | 6 | """ 7 | __author__ = "Joe Gregorio (joe@bitworking.org)" 8 | __copyright__ = "Copyright 2006, Joe Gregorio" 9 | __contributors__ = [] 10 | __version__ = "1.0.0" 11 | __license__ = "MIT" 12 | __history__ = """ 13 | """ 14 | 15 | import urlparse 16 | 17 | 18 | # Convert an IRI to a URI following the rules in RFC 3987 19 | # 20 | # The characters we need to enocde and escape are defined in the spec: 21 | # 22 | # iprivate = %xE000-F8FF / %xF0000-FFFFD / %x100000-10FFFD 23 | # ucschar = %xA0-D7FF / %xF900-FDCF / %xFDF0-FFEF 24 | # / %x10000-1FFFD / %x20000-2FFFD / %x30000-3FFFD 25 | # / %x40000-4FFFD / %x50000-5FFFD / %x60000-6FFFD 26 | # / %x70000-7FFFD / %x80000-8FFFD / %x90000-9FFFD 27 | # / %xA0000-AFFFD / %xB0000-BFFFD / %xC0000-CFFFD 28 | # / %xD0000-DFFFD / %xE1000-EFFFD 29 | 30 | escape_range = [ 31 | (0xA0, 0xD7FF), 32 | (0xE000, 0xF8FF), 33 | (0xF900, 0xFDCF), 34 | (0xFDF0, 0xFFEF), 35 | (0x10000, 0x1FFFD), 36 | (0x20000, 0x2FFFD), 37 | (0x30000, 0x3FFFD), 38 | (0x40000, 0x4FFFD), 39 | (0x50000, 0x5FFFD), 40 | (0x60000, 0x6FFFD), 41 | (0x70000, 0x7FFFD), 42 | (0x80000, 0x8FFFD), 43 | (0x90000, 0x9FFFD), 44 | (0xA0000, 0xAFFFD), 45 | (0xB0000, 0xBFFFD), 46 | (0xC0000, 0xCFFFD), 47 | (0xD0000, 0xDFFFD), 48 | (0xE1000, 0xEFFFD), 49 | (0xF0000, 0xFFFFD), 50 | (0x100000, 0x10FFFD), 51 | ] 52 | 53 | def encode(c): 54 | retval = c 55 | i = ord(c) 56 | for low, high in escape_range: 57 | if i < low: 58 | break 59 | if i >= low and i <= high: 60 | retval = "".join(["%%%2X" % ord(o) for o in c.encode('utf-8')]) 61 | break 62 | return retval 63 | 64 | 65 | def iri2uri(uri): 66 | """Convert an IRI to a URI. Note that IRIs must be 67 | passed in a unicode strings. That is, do not utf-8 encode 68 | the IRI before passing it into the function.""" 69 | if isinstance(uri ,unicode): 70 | (scheme, authority, path, query, fragment) = urlparse.urlsplit(uri) 71 | authority = authority.encode('idna') 72 | # For each character in 'ucschar' or 'iprivate' 73 | # 1. encode as utf-8 74 | # 2. then %-encode each octet of that utf-8 75 | uri = urlparse.urlunsplit((scheme, authority, path, query, fragment)) 76 | uri = "".join([encode(c) for c in uri]) 77 | return uri 78 | 79 | if __name__ == "__main__": 80 | import unittest 81 | 82 | class Test(unittest.TestCase): 83 | 84 | def test_uris(self): 85 | """Test that URIs are invariant under the transformation.""" 86 | invariant = [ 87 | u"ftp://ftp.is.co.za/rfc/rfc1808.txt", 88 | u"http://www.ietf.org/rfc/rfc2396.txt", 89 | u"ldap://[2001:db8::7]/c=GB?objectClass?one", 90 | u"mailto:John.Doe@example.com", 91 | u"news:comp.infosystems.www.servers.unix", 92 | u"tel:+1-816-555-1212", 93 | u"telnet://192.0.2.16:80/", 94 | u"urn:oasis:names:specification:docbook:dtd:xml:4.1.2" ] 95 | for uri in invariant: 96 | self.assertEqual(uri, iri2uri(uri)) 97 | 98 | def test_iri(self): 99 | """ Test that the right type of escaping is done for each part of the URI.""" 100 | self.assertEqual("http://xn--o3h.com/%E2%98%84", iri2uri(u"http://\N{COMET}.com/\N{COMET}")) 101 | self.assertEqual("http://bitworking.org/?fred=%E2%98%84", iri2uri(u"http://bitworking.org/?fred=\N{COMET}")) 102 | self.assertEqual("http://bitworking.org/#%E2%98%84", iri2uri(u"http://bitworking.org/#\N{COMET}")) 103 | self.assertEqual("#%E2%98%84", iri2uri(u"#\N{COMET}")) 104 | self.assertEqual("/fred?bar=%E2%98%9A#%E2%98%84", iri2uri(u"/fred?bar=\N{BLACK LEFT POINTING INDEX}#\N{COMET}")) 105 | self.assertEqual("/fred?bar=%E2%98%9A#%E2%98%84", iri2uri(iri2uri(u"/fred?bar=\N{BLACK LEFT POINTING INDEX}#\N{COMET}"))) 106 | self.assertNotEqual("/fred?bar=%E2%98%9A#%E2%98%84", iri2uri(u"/fred?bar=\N{BLACK LEFT POINTING INDEX}#\N{COMET}".encode('utf-8'))) 107 | 108 | unittest.main() 109 | 110 | 111 | -------------------------------------------------------------------------------- /oauth2client/django_orm.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2010 Google Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """OAuth 2.0 utilities for Django. 16 | 17 | Utilities for using OAuth 2.0 in conjunction with 18 | the Django datastore. 19 | """ 20 | 21 | __author__ = 'jcgregorio@google.com (Joe Gregorio)' 22 | 23 | import oauth2client 24 | import base64 25 | import pickle 26 | 27 | from django.db import models 28 | from oauth2client.client import Storage as BaseStorage 29 | 30 | class CredentialsField(models.Field): 31 | 32 | __metaclass__ = models.SubfieldBase 33 | 34 | def __init__(self, *args, **kwargs): 35 | if 'null' not in kwargs: 36 | kwargs['null'] = True 37 | super(CredentialsField, self).__init__(*args, **kwargs) 38 | 39 | def get_internal_type(self): 40 | return "TextField" 41 | 42 | def to_python(self, value): 43 | if value is None: 44 | return None 45 | if isinstance(value, oauth2client.client.Credentials): 46 | return value 47 | return pickle.loads(base64.b64decode(value)) 48 | 49 | def get_db_prep_value(self, value, connection, prepared=False): 50 | if value is None: 51 | return None 52 | return base64.b64encode(pickle.dumps(value)) 53 | 54 | 55 | class FlowField(models.Field): 56 | 57 | __metaclass__ = models.SubfieldBase 58 | 59 | def __init__(self, *args, **kwargs): 60 | if 'null' not in kwargs: 61 | kwargs['null'] = True 62 | super(FlowField, self).__init__(*args, **kwargs) 63 | 64 | def get_internal_type(self): 65 | return "TextField" 66 | 67 | def to_python(self, value): 68 | if value is None: 69 | return None 70 | if isinstance(value, oauth2client.client.Flow): 71 | return value 72 | return pickle.loads(base64.b64decode(value)) 73 | 74 | def get_db_prep_value(self, value, connection, prepared=False): 75 | if value is None: 76 | return None 77 | return base64.b64encode(pickle.dumps(value)) 78 | 79 | 80 | class Storage(BaseStorage): 81 | """Store and retrieve a single credential to and from 82 | the datastore. 83 | 84 | This Storage helper presumes the Credentials 85 | have been stored as a CredenialsField 86 | on a db model class. 87 | """ 88 | 89 | def __init__(self, model_class, key_name, key_value, property_name): 90 | """Constructor for Storage. 91 | 92 | Args: 93 | model: db.Model, model class 94 | key_name: string, key name for the entity that has the credentials 95 | key_value: string, key value for the entity that has the credentials 96 | property_name: string, name of the property that is an CredentialsProperty 97 | """ 98 | self.model_class = model_class 99 | self.key_name = key_name 100 | self.key_value = key_value 101 | self.property_name = property_name 102 | 103 | def locked_get(self): 104 | """Retrieve Credential from datastore. 105 | 106 | Returns: 107 | oauth2client.Credentials 108 | """ 109 | credential = None 110 | 111 | query = {self.key_name: self.key_value} 112 | entities = self.model_class.objects.filter(**query) 113 | if len(entities) > 0: 114 | credential = getattr(entities[0], self.property_name) 115 | if credential and hasattr(credential, 'set_store'): 116 | credential.set_store(self) 117 | return credential 118 | 119 | def locked_put(self, credentials): 120 | """Write a Credentials to the datastore. 121 | 122 | Args: 123 | credentials: Credentials, the credentials to store. 124 | """ 125 | args = {self.key_name: self.key_value} 126 | entity = self.model_class(**args) 127 | setattr(entity, self.property_name, credentials) 128 | entity.save() 129 | 130 | def locked_delete(self): 131 | """Delete Credentials from the datastore.""" 132 | 133 | query = {self.key_name: self.key_value} 134 | entities = self.model_class.objects.filter(**query).delete() 135 | -------------------------------------------------------------------------------- /oauth2client/clientsecrets.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2011 Google Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Utilities for reading OAuth 2.0 client secret files. 16 | 17 | A client_secrets.json file contains all the information needed to interact with 18 | an OAuth 2.0 protected service. 19 | """ 20 | 21 | __author__ = 'jcgregorio@google.com (Joe Gregorio)' 22 | 23 | 24 | from anyjson import simplejson 25 | 26 | # Properties that make a client_secrets.json file valid. 27 | TYPE_WEB = 'web' 28 | TYPE_INSTALLED = 'installed' 29 | 30 | VALID_CLIENT = { 31 | TYPE_WEB: { 32 | 'required': [ 33 | 'client_id', 34 | 'client_secret', 35 | 'redirect_uris', 36 | 'auth_uri', 37 | 'token_uri', 38 | ], 39 | 'string': [ 40 | 'client_id', 41 | 'client_secret', 42 | ], 43 | }, 44 | TYPE_INSTALLED: { 45 | 'required': [ 46 | 'client_id', 47 | 'client_secret', 48 | 'redirect_uris', 49 | 'auth_uri', 50 | 'token_uri', 51 | ], 52 | 'string': [ 53 | 'client_id', 54 | 'client_secret', 55 | ], 56 | }, 57 | } 58 | 59 | 60 | class Error(Exception): 61 | """Base error for this module.""" 62 | pass 63 | 64 | 65 | class InvalidClientSecretsError(Error): 66 | """Format of ClientSecrets file is invalid.""" 67 | pass 68 | 69 | 70 | def _validate_clientsecrets(obj): 71 | if obj is None or len(obj) != 1: 72 | raise InvalidClientSecretsError('Invalid file format.') 73 | client_type = obj.keys()[0] 74 | if client_type not in VALID_CLIENT.keys(): 75 | raise InvalidClientSecretsError('Unknown client type: %s.' % client_type) 76 | client_info = obj[client_type] 77 | for prop_name in VALID_CLIENT[client_type]['required']: 78 | if prop_name not in client_info: 79 | raise InvalidClientSecretsError( 80 | 'Missing property "%s" in a client type of "%s".' % (prop_name, 81 | client_type)) 82 | for prop_name in VALID_CLIENT[client_type]['string']: 83 | if client_info[prop_name].startswith('[['): 84 | raise InvalidClientSecretsError( 85 | 'Property "%s" is not configured.' % prop_name) 86 | return client_type, client_info 87 | 88 | 89 | def load(fp): 90 | obj = simplejson.load(fp) 91 | return _validate_clientsecrets(obj) 92 | 93 | 94 | def loads(s): 95 | obj = simplejson.loads(s) 96 | return _validate_clientsecrets(obj) 97 | 98 | 99 | def _loadfile(filename): 100 | try: 101 | fp = file(filename, 'r') 102 | try: 103 | obj = simplejson.load(fp) 104 | finally: 105 | fp.close() 106 | except IOError: 107 | raise InvalidClientSecretsError('File not found: "%s"' % filename) 108 | return _validate_clientsecrets(obj) 109 | 110 | 111 | def loadfile(filename, cache=None): 112 | """Loading of client_secrets JSON file, optionally backed by a cache. 113 | 114 | Typical cache storage would be App Engine memcache service, 115 | but you can pass in any other cache client that implements 116 | these methods: 117 | - get(key, namespace=ns) 118 | - set(key, value, namespace=ns) 119 | 120 | Usage: 121 | # without caching 122 | client_type, client_info = loadfile('secrets.json') 123 | # using App Engine memcache service 124 | from google.appengine.api import memcache 125 | client_type, client_info = loadfile('secrets.json', cache=memcache) 126 | 127 | Args: 128 | filename: string, Path to a client_secrets.json file on a filesystem. 129 | cache: An optional cache service client that implements get() and set() 130 | methods. If not specified, the file is always being loaded from 131 | a filesystem. 132 | 133 | Raises: 134 | InvalidClientSecretsError: In case of a validation error or some 135 | I/O failure. Can happen only on cache miss. 136 | 137 | Returns: 138 | (client_type, client_info) tuple, as _loadfile() normally would. 139 | JSON contents is validated only during first load. Cache hits are not 140 | validated. 141 | """ 142 | _SECRET_NAMESPACE = 'oauth2client:secrets#ns' 143 | 144 | if not cache: 145 | return _loadfile(filename) 146 | 147 | obj = cache.get(filename, namespace=_SECRET_NAMESPACE) 148 | if obj is None: 149 | client_type, client_info = _loadfile(filename) 150 | obj = {client_type: client_info} 151 | cache.set(filename, obj, namespace=_SECRET_NAMESPACE) 152 | 153 | return obj.iteritems().next() 154 | -------------------------------------------------------------------------------- /uritemplate/__init__.py: -------------------------------------------------------------------------------- 1 | # Early, and incomplete implementation of -04. 2 | # 3 | import re 4 | import urllib 5 | 6 | RESERVED = ":/?#[]@!$&'()*+,;=" 7 | OPERATOR = "+./;?|!@" 8 | EXPLODE = "*+" 9 | MODIFIER = ":^" 10 | TEMPLATE = re.compile(r"{(?P[\+\./;\?|!@])?(?P[^}]+)}", re.UNICODE) 11 | VAR = re.compile(r"^(?P[^=\+\*:\^]+)((?P[\+\*])|(?P[:\^]-?[0-9]+))?(=(?P.*))?$", re.UNICODE) 12 | 13 | def _tostring(varname, value, explode, operator, safe=""): 14 | if type(value) == type([]): 15 | if explode == "+": 16 | return ",".join([varname + "." + urllib.quote(x, safe) for x in value]) 17 | else: 18 | return ",".join([urllib.quote(x, safe) for x in value]) 19 | if type(value) == type({}): 20 | keys = value.keys() 21 | keys.sort() 22 | if explode == "+": 23 | return ",".join([varname + "." + urllib.quote(key, safe) + "," + urllib.quote(value[key], safe) for key in keys]) 24 | else: 25 | return ",".join([urllib.quote(key, safe) + "," + urllib.quote(value[key], safe) for key in keys]) 26 | else: 27 | return urllib.quote(value, safe) 28 | 29 | 30 | def _tostring_path(varname, value, explode, operator, safe=""): 31 | joiner = operator 32 | if type(value) == type([]): 33 | if explode == "+": 34 | return joiner.join([varname + "." + urllib.quote(x, safe) for x in value]) 35 | elif explode == "*": 36 | return joiner.join([urllib.quote(x, safe) for x in value]) 37 | else: 38 | return ",".join([urllib.quote(x, safe) for x in value]) 39 | elif type(value) == type({}): 40 | keys = value.keys() 41 | keys.sort() 42 | if explode == "+": 43 | return joiner.join([varname + "." + urllib.quote(key, safe) + joiner + urllib.quote(value[key], safe) for key in keys]) 44 | elif explode == "*": 45 | return joiner.join([urllib.quote(key, safe) + joiner + urllib.quote(value[key], safe) for key in keys]) 46 | else: 47 | return ",".join([urllib.quote(key, safe) + "," + urllib.quote(value[key], safe) for key in keys]) 48 | else: 49 | if value: 50 | return urllib.quote(value, safe) 51 | else: 52 | return "" 53 | 54 | def _tostring_query(varname, value, explode, operator, safe=""): 55 | joiner = operator 56 | varprefix = "" 57 | if operator == "?": 58 | joiner = "&" 59 | varprefix = varname + "=" 60 | if type(value) == type([]): 61 | if 0 == len(value): 62 | return "" 63 | if explode == "+": 64 | return joiner.join([varname + "=" + urllib.quote(x, safe) for x in value]) 65 | elif explode == "*": 66 | return joiner.join([urllib.quote(x, safe) for x in value]) 67 | else: 68 | return varprefix + ",".join([urllib.quote(x, safe) for x in value]) 69 | elif type(value) == type({}): 70 | if 0 == len(value): 71 | return "" 72 | keys = value.keys() 73 | keys.sort() 74 | if explode == "+": 75 | return joiner.join([varname + "." + urllib.quote(key, safe) + "=" + urllib.quote(value[key], safe) for key in keys]) 76 | elif explode == "*": 77 | return joiner.join([urllib.quote(key, safe) + "=" + urllib.quote(value[key], safe) for key in keys]) 78 | else: 79 | return varprefix + ",".join([urllib.quote(key, safe) + "," + urllib.quote(value[key], safe) for key in keys]) 80 | else: 81 | if value: 82 | return varname + "=" + urllib.quote(value, safe) 83 | else: 84 | return varname 85 | 86 | TOSTRING = { 87 | "" : _tostring, 88 | "+": _tostring, 89 | ";": _tostring_query, 90 | "?": _tostring_query, 91 | "/": _tostring_path, 92 | ".": _tostring_path, 93 | } 94 | 95 | 96 | def expand(template, vars): 97 | def _sub(match): 98 | groupdict = match.groupdict() 99 | operator = groupdict.get('operator') 100 | if operator is None: 101 | operator = '' 102 | varlist = groupdict.get('varlist') 103 | 104 | safe = "@" 105 | if operator == '+': 106 | safe = RESERVED 107 | varspecs = varlist.split(",") 108 | varnames = [] 109 | defaults = {} 110 | for varspec in varspecs: 111 | m = VAR.search(varspec) 112 | groupdict = m.groupdict() 113 | varname = groupdict.get('varname') 114 | explode = groupdict.get('explode') 115 | partial = groupdict.get('partial') 116 | default = groupdict.get('default') 117 | if default: 118 | defaults[varname] = default 119 | varnames.append((varname, explode, partial)) 120 | 121 | retval = [] 122 | joiner = operator 123 | prefix = operator 124 | if operator == "+": 125 | prefix = "" 126 | joiner = "," 127 | if operator == "?": 128 | joiner = "&" 129 | if operator == "": 130 | joiner = "," 131 | for varname, explode, partial in varnames: 132 | if varname in vars: 133 | value = vars[varname] 134 | #if not value and (type(value) == type({}) or type(value) == type([])) and varname in defaults: 135 | if not value and value != "" and varname in defaults: 136 | value = defaults[varname] 137 | elif varname in defaults: 138 | value = defaults[varname] 139 | else: 140 | continue 141 | retval.append(TOSTRING[operator](varname, value, explode, operator, safe=safe)) 142 | if "".join(retval): 143 | return prefix + joiner.join(retval) 144 | else: 145 | return "" 146 | 147 | return TEMPLATE.sub(_sub, template) 148 | -------------------------------------------------------------------------------- /oauth2client/old_run.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2013 Google Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """This module holds the old run() function which is deprecated, the 16 | tools.run_flow() function should be used in its place.""" 17 | 18 | 19 | import logging 20 | import socket 21 | import sys 22 | import webbrowser 23 | 24 | import gflags 25 | 26 | from oauth2client import client 27 | from oauth2client import util 28 | from tools import ClientRedirectHandler 29 | from tools import ClientRedirectServer 30 | 31 | 32 | FLAGS = gflags.FLAGS 33 | 34 | gflags.DEFINE_boolean('auth_local_webserver', True, 35 | ('Run a local web server to handle redirects during ' 36 | 'OAuth authorization.')) 37 | 38 | gflags.DEFINE_string('auth_host_name', 'localhost', 39 | ('Host name to use when running a local web server to ' 40 | 'handle redirects during OAuth authorization.')) 41 | 42 | gflags.DEFINE_multi_int('auth_host_port', [8080, 8090], 43 | ('Port to use when running a local web server to ' 44 | 'handle redirects during OAuth authorization.')) 45 | 46 | 47 | @util.positional(2) 48 | def run(flow, storage, http=None): 49 | """Core code for a command-line application. 50 | 51 | The run() function is called from your application and runs through all 52 | the steps to obtain credentials. It takes a Flow argument and attempts to 53 | open an authorization server page in the user's default web browser. The 54 | server asks the user to grant your application access to the user's data. 55 | If the user grants access, the run() function returns new credentials. The 56 | new credentials are also stored in the Storage argument, which updates the 57 | file associated with the Storage object. 58 | 59 | It presumes it is run from a command-line application and supports the 60 | following flags: 61 | 62 | --auth_host_name: Host name to use when running a local web server 63 | to handle redirects during OAuth authorization. 64 | (default: 'localhost') 65 | 66 | --auth_host_port: Port to use when running a local web server to handle 67 | redirects during OAuth authorization.; 68 | repeat this option to specify a list of values 69 | (default: '[8080, 8090]') 70 | (an integer) 71 | 72 | --[no]auth_local_webserver: Run a local web server to handle redirects 73 | during OAuth authorization. 74 | (default: 'true') 75 | 76 | Since it uses flags make sure to initialize the gflags module before 77 | calling run(). 78 | 79 | Args: 80 | flow: Flow, an OAuth 2.0 Flow to step through. 81 | storage: Storage, a Storage to store the credential in. 82 | http: An instance of httplib2.Http.request 83 | or something that acts like it. 84 | 85 | Returns: 86 | Credentials, the obtained credential. 87 | """ 88 | logging.warning('This function, oauth2client.tools.run(), and the use of ' 89 | 'the gflags library are deprecated and will be removed in a future ' 90 | 'version of the library.') 91 | if FLAGS.auth_local_webserver: 92 | success = False 93 | port_number = 0 94 | for port in FLAGS.auth_host_port: 95 | port_number = port 96 | try: 97 | httpd = ClientRedirectServer((FLAGS.auth_host_name, port), 98 | ClientRedirectHandler) 99 | except socket.error, e: 100 | pass 101 | else: 102 | success = True 103 | break 104 | FLAGS.auth_local_webserver = success 105 | if not success: 106 | print 'Failed to start a local webserver listening on either port 8080' 107 | print 'or port 9090. Please check your firewall settings and locally' 108 | print 'running programs that may be blocking or using those ports.' 109 | print 110 | print 'Falling back to --noauth_local_webserver and continuing with', 111 | print 'authorization.' 112 | print 113 | 114 | if FLAGS.auth_local_webserver: 115 | oauth_callback = 'http://%s:%s/' % (FLAGS.auth_host_name, port_number) 116 | else: 117 | oauth_callback = client.OOB_CALLBACK_URN 118 | flow.redirect_uri = oauth_callback 119 | authorize_url = flow.step1_get_authorize_url() 120 | 121 | if FLAGS.auth_local_webserver: 122 | webbrowser.open(authorize_url, new=1, autoraise=True) 123 | print 'Your browser has been opened to visit:' 124 | print 125 | print ' ' + authorize_url 126 | print 127 | print 'If your browser is on a different machine then exit and re-run' 128 | print 'this application with the command-line parameter ' 129 | print 130 | print ' --noauth_local_webserver' 131 | print 132 | else: 133 | print 'Go to the following link in your browser:' 134 | print 135 | print ' ' + authorize_url 136 | print 137 | 138 | code = None 139 | if FLAGS.auth_local_webserver: 140 | httpd.handle_request() 141 | if 'error' in httpd.query_params: 142 | sys.exit('Authentication request was rejected.') 143 | if 'code' in httpd.query_params: 144 | code = httpd.query_params['code'] 145 | else: 146 | print 'Failed to find "code" in the query parameters of the redirect.' 147 | sys.exit('Try running with --noauth_local_webserver.') 148 | else: 149 | code = raw_input('Enter verification code: ').strip() 150 | 151 | try: 152 | credential = flow.step2_exchange(code, http=http) 153 | except client.FlowExchangeError, e: 154 | sys.exit('Authentication has failed: %s' % e) 155 | 156 | storage.put(credential) 157 | credential.set_store(storage) 158 | print 'Authentication successful.' 159 | 160 | return credential 161 | -------------------------------------------------------------------------------- /oauth2client/util.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2010 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | """Common utility library.""" 19 | 20 | __author__ = ['rafek@google.com (Rafe Kaplan)', 21 | 'guido@google.com (Guido van Rossum)', 22 | ] 23 | __all__ = [ 24 | 'positional', 25 | 'POSITIONAL_WARNING', 26 | 'POSITIONAL_EXCEPTION', 27 | 'POSITIONAL_IGNORE', 28 | ] 29 | 30 | import inspect 31 | import logging 32 | import types 33 | import urllib 34 | import urlparse 35 | 36 | try: 37 | from urlparse import parse_qsl 38 | except ImportError: 39 | from cgi import parse_qsl 40 | 41 | logger = logging.getLogger(__name__) 42 | 43 | POSITIONAL_WARNING = 'WARNING' 44 | POSITIONAL_EXCEPTION = 'EXCEPTION' 45 | POSITIONAL_IGNORE = 'IGNORE' 46 | POSITIONAL_SET = frozenset([POSITIONAL_WARNING, POSITIONAL_EXCEPTION, 47 | POSITIONAL_IGNORE]) 48 | 49 | positional_parameters_enforcement = POSITIONAL_WARNING 50 | 51 | def positional(max_positional_args): 52 | """A decorator to declare that only the first N arguments my be positional. 53 | 54 | This decorator makes it easy to support Python 3 style key-word only 55 | parameters. For example, in Python 3 it is possible to write: 56 | 57 | def fn(pos1, *, kwonly1=None, kwonly1=None): 58 | ... 59 | 60 | All named parameters after * must be a keyword: 61 | 62 | fn(10, 'kw1', 'kw2') # Raises exception. 63 | fn(10, kwonly1='kw1') # Ok. 64 | 65 | Example: 66 | To define a function like above, do: 67 | 68 | @positional(1) 69 | def fn(pos1, kwonly1=None, kwonly2=None): 70 | ... 71 | 72 | If no default value is provided to a keyword argument, it becomes a required 73 | keyword argument: 74 | 75 | @positional(0) 76 | def fn(required_kw): 77 | ... 78 | 79 | This must be called with the keyword parameter: 80 | 81 | fn() # Raises exception. 82 | fn(10) # Raises exception. 83 | fn(required_kw=10) # Ok. 84 | 85 | When defining instance or class methods always remember to account for 86 | 'self' and 'cls': 87 | 88 | class MyClass(object): 89 | 90 | @positional(2) 91 | def my_method(self, pos1, kwonly1=None): 92 | ... 93 | 94 | @classmethod 95 | @positional(2) 96 | def my_method(cls, pos1, kwonly1=None): 97 | ... 98 | 99 | The positional decorator behavior is controlled by 100 | util.positional_parameters_enforcement, which may be set to 101 | POSITIONAL_EXCEPTION, POSITIONAL_WARNING or POSITIONAL_IGNORE to raise an 102 | exception, log a warning, or do nothing, respectively, if a declaration is 103 | violated. 104 | 105 | Args: 106 | max_positional_arguments: Maximum number of positional arguments. All 107 | parameters after the this index must be keyword only. 108 | 109 | Returns: 110 | A decorator that prevents using arguments after max_positional_args from 111 | being used as positional parameters. 112 | 113 | Raises: 114 | TypeError if a key-word only argument is provided as a positional 115 | parameter, but only if util.positional_parameters_enforcement is set to 116 | POSITIONAL_EXCEPTION. 117 | """ 118 | def positional_decorator(wrapped): 119 | def positional_wrapper(*args, **kwargs): 120 | if len(args) > max_positional_args: 121 | plural_s = '' 122 | if max_positional_args != 1: 123 | plural_s = 's' 124 | message = '%s() takes at most %d positional argument%s (%d given)' % ( 125 | wrapped.__name__, max_positional_args, plural_s, len(args)) 126 | if positional_parameters_enforcement == POSITIONAL_EXCEPTION: 127 | raise TypeError(message) 128 | elif positional_parameters_enforcement == POSITIONAL_WARNING: 129 | logger.warning(message) 130 | else: # IGNORE 131 | pass 132 | return wrapped(*args, **kwargs) 133 | return positional_wrapper 134 | 135 | if isinstance(max_positional_args, (int, long)): 136 | return positional_decorator 137 | else: 138 | args, _, _, defaults = inspect.getargspec(max_positional_args) 139 | return positional(len(args) - len(defaults))(max_positional_args) 140 | 141 | 142 | def scopes_to_string(scopes): 143 | """Converts scope value to a string. 144 | 145 | If scopes is a string then it is simply passed through. If scopes is an 146 | iterable then a string is returned that is all the individual scopes 147 | concatenated with spaces. 148 | 149 | Args: 150 | scopes: string or iterable of strings, the scopes. 151 | 152 | Returns: 153 | The scopes formatted as a single string. 154 | """ 155 | if isinstance(scopes, types.StringTypes): 156 | return scopes 157 | else: 158 | return ' '.join(scopes) 159 | 160 | 161 | def dict_to_tuple_key(dictionary): 162 | """Converts a dictionary to a tuple that can be used as an immutable key. 163 | 164 | The resulting key is always sorted so that logically equivalent dictionaries 165 | always produce an identical tuple for a key. 166 | 167 | Args: 168 | dictionary: the dictionary to use as the key. 169 | 170 | Returns: 171 | A tuple representing the dictionary in it's naturally sorted ordering. 172 | """ 173 | return tuple(sorted(dictionary.items())) 174 | 175 | 176 | def _add_query_parameter(url, name, value): 177 | """Adds a query parameter to a url. 178 | 179 | Replaces the current value if it already exists in the URL. 180 | 181 | Args: 182 | url: string, url to add the query parameter to. 183 | name: string, query parameter name. 184 | value: string, query parameter value. 185 | 186 | Returns: 187 | Updated query parameter. Does not update the url if value is None. 188 | """ 189 | if value is None: 190 | return url 191 | else: 192 | parsed = list(urlparse.urlparse(url)) 193 | q = dict(parse_qsl(parsed[4])) 194 | q[name] = value 195 | parsed[4] = urllib.urlencode(q) 196 | return urlparse.urlunparse(parsed) 197 | -------------------------------------------------------------------------------- /apiclient/mimeparse.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2007 Joe Gregorio 2 | # 3 | # Licensed under the MIT License 4 | 5 | """MIME-Type Parser 6 | 7 | This module provides basic functions for handling mime-types. It can handle 8 | matching mime-types against a list of media-ranges. See section 14.1 of the 9 | HTTP specification [RFC 2616] for a complete explanation. 10 | 11 | http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1 12 | 13 | Contents: 14 | - parse_mime_type(): Parses a mime-type into its component parts. 15 | - parse_media_range(): Media-ranges are mime-types with wild-cards and a 'q' 16 | quality parameter. 17 | - quality(): Determines the quality ('q') of a mime-type when 18 | compared against a list of media-ranges. 19 | - quality_parsed(): Just like quality() except the second parameter must be 20 | pre-parsed. 21 | - best_match(): Choose the mime-type with the highest quality ('q') 22 | from a list of candidates. 23 | """ 24 | 25 | __version__ = '0.1.3' 26 | __author__ = 'Joe Gregorio' 27 | __email__ = 'joe@bitworking.org' 28 | __license__ = 'MIT License' 29 | __credits__ = '' 30 | 31 | 32 | def parse_mime_type(mime_type): 33 | """Parses a mime-type into its component parts. 34 | 35 | Carves up a mime-type and returns a tuple of the (type, subtype, params) 36 | where 'params' is a dictionary of all the parameters for the media range. 37 | For example, the media range 'application/xhtml;q=0.5' would get parsed 38 | into: 39 | 40 | ('application', 'xhtml', {'q', '0.5'}) 41 | """ 42 | parts = mime_type.split(';') 43 | params = dict([tuple([s.strip() for s in param.split('=', 1)])\ 44 | for param in parts[1:] 45 | ]) 46 | full_type = parts[0].strip() 47 | # Java URLConnection class sends an Accept header that includes a 48 | # single '*'. Turn it into a legal wildcard. 49 | if full_type == '*': 50 | full_type = '*/*' 51 | (type, subtype) = full_type.split('/') 52 | 53 | return (type.strip(), subtype.strip(), params) 54 | 55 | 56 | def parse_media_range(range): 57 | """Parse a media-range into its component parts. 58 | 59 | Carves up a media range and returns a tuple of the (type, subtype, 60 | params) where 'params' is a dictionary of all the parameters for the media 61 | range. For example, the media range 'application/*;q=0.5' would get parsed 62 | into: 63 | 64 | ('application', '*', {'q', '0.5'}) 65 | 66 | In addition this function also guarantees that there is a value for 'q' 67 | in the params dictionary, filling it in with a proper default if 68 | necessary. 69 | """ 70 | (type, subtype, params) = parse_mime_type(range) 71 | if not params.has_key('q') or not params['q'] or \ 72 | not float(params['q']) or float(params['q']) > 1\ 73 | or float(params['q']) < 0: 74 | params['q'] = '1' 75 | 76 | return (type, subtype, params) 77 | 78 | 79 | def fitness_and_quality_parsed(mime_type, parsed_ranges): 80 | """Find the best match for a mime-type amongst parsed media-ranges. 81 | 82 | Find the best match for a given mime-type against a list of media_ranges 83 | that have already been parsed by parse_media_range(). Returns a tuple of 84 | the fitness value and the value of the 'q' quality parameter of the best 85 | match, or (-1, 0) if no match was found. Just as for quality_parsed(), 86 | 'parsed_ranges' must be a list of parsed media ranges. 87 | """ 88 | best_fitness = -1 89 | best_fit_q = 0 90 | (target_type, target_subtype, target_params) =\ 91 | parse_media_range(mime_type) 92 | for (type, subtype, params) in parsed_ranges: 93 | type_match = (type == target_type or\ 94 | type == '*' or\ 95 | target_type == '*') 96 | subtype_match = (subtype == target_subtype or\ 97 | subtype == '*' or\ 98 | target_subtype == '*') 99 | if type_match and subtype_match: 100 | param_matches = reduce(lambda x, y: x + y, [1 for (key, value) in \ 101 | target_params.iteritems() if key != 'q' and \ 102 | params.has_key(key) and value == params[key]], 0) 103 | fitness = (type == target_type) and 100 or 0 104 | fitness += (subtype == target_subtype) and 10 or 0 105 | fitness += param_matches 106 | if fitness > best_fitness: 107 | best_fitness = fitness 108 | best_fit_q = params['q'] 109 | 110 | return best_fitness, float(best_fit_q) 111 | 112 | 113 | def quality_parsed(mime_type, parsed_ranges): 114 | """Find the best match for a mime-type amongst parsed media-ranges. 115 | 116 | Find the best match for a given mime-type against a list of media_ranges 117 | that have already been parsed by parse_media_range(). Returns the 'q' 118 | quality parameter of the best match, 0 if no match was found. This function 119 | bahaves the same as quality() except that 'parsed_ranges' must be a list of 120 | parsed media ranges. 121 | """ 122 | 123 | return fitness_and_quality_parsed(mime_type, parsed_ranges)[1] 124 | 125 | 126 | def quality(mime_type, ranges): 127 | """Return the quality ('q') of a mime-type against a list of media-ranges. 128 | 129 | Returns the quality 'q' of a mime-type when compared against the 130 | media-ranges in ranges. For example: 131 | 132 | >>> quality('text/html','text/*;q=0.3, text/html;q=0.7, 133 | text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5') 134 | 0.7 135 | 136 | """ 137 | parsed_ranges = [parse_media_range(r) for r in ranges.split(',')] 138 | 139 | return quality_parsed(mime_type, parsed_ranges) 140 | 141 | 142 | def best_match(supported, header): 143 | """Return mime-type with the highest quality ('q') from list of candidates. 144 | 145 | Takes a list of supported mime-types and finds the best match for all the 146 | media-ranges listed in header. The value of header must be a string that 147 | conforms to the format of the HTTP Accept: header. The value of 'supported' 148 | is a list of mime-types. The list of supported mime-types should be sorted 149 | in order of increasing desirability, in case of a situation where there is 150 | a tie. 151 | 152 | >>> best_match(['application/xbel+xml', 'text/xml'], 153 | 'text/*;q=0.5,*/*; q=0.1') 154 | 'text/xml' 155 | """ 156 | split_header = _filter_blank(header.split(',')) 157 | parsed_header = [parse_media_range(r) for r in split_header] 158 | weighted_matches = [] 159 | pos = 0 160 | for mime_type in supported: 161 | weighted_matches.append((fitness_and_quality_parsed(mime_type, 162 | parsed_header), pos, mime_type)) 163 | pos += 1 164 | weighted_matches.sort() 165 | 166 | return weighted_matches[-1][0][1] and weighted_matches[-1][2] or '' 167 | 168 | 169 | def _filter_blank(i): 170 | for s in i: 171 | if s.strip(): 172 | yield s 173 | -------------------------------------------------------------------------------- /oauth2client/tools.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2013 Google Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Command-line tools for authenticating via OAuth 2.0 16 | 17 | Do the OAuth 2.0 Web Server dance for a command line application. Stores the 18 | generated credentials in a common file that is used by other example apps in 19 | the same directory. 20 | """ 21 | 22 | __author__ = 'jcgregorio@google.com (Joe Gregorio)' 23 | __all__ = ['argparser', 'run_flow', 'run', 'message_if_missing'] 24 | 25 | 26 | import BaseHTTPServer 27 | import argparse 28 | import httplib2 29 | import logging 30 | import os 31 | import socket 32 | import sys 33 | import webbrowser 34 | 35 | from oauth2client import client 36 | from oauth2client import file 37 | from oauth2client import util 38 | 39 | try: 40 | from urlparse import parse_qsl 41 | except ImportError: 42 | from cgi import parse_qsl 43 | 44 | _CLIENT_SECRETS_MESSAGE = """WARNING: Please configure OAuth 2.0 45 | 46 | To make this sample run you will need to populate the client_secrets.json file 47 | found at: 48 | 49 | %s 50 | 51 | with information from the APIs Console . 52 | 53 | """ 54 | 55 | # run_parser is an ArgumentParser that contains command-line options expected 56 | # by tools.run(). Pass it in as part of the 'parents' argument to your own 57 | # ArgumentParser. 58 | argparser = argparse.ArgumentParser(add_help=False) 59 | argparser.add_argument('--auth_host_name', default='localhost', 60 | help='Hostname when running a local web server.') 61 | argparser.add_argument('--noauth_local_webserver', action='store_true', 62 | default=False, help='Do not run a local web server.') 63 | argparser.add_argument('--auth_host_port', default=[8080, 8090], type=int, 64 | nargs='*', help='Port web server should listen on.') 65 | argparser.add_argument('--logging_level', default='ERROR', 66 | choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 67 | 'CRITICAL'], 68 | help='Set the logging level of detail.') 69 | 70 | 71 | class ClientRedirectServer(BaseHTTPServer.HTTPServer): 72 | """A server to handle OAuth 2.0 redirects back to localhost. 73 | 74 | Waits for a single request and parses the query parameters 75 | into query_params and then stops serving. 76 | """ 77 | query_params = {} 78 | 79 | 80 | class ClientRedirectHandler(BaseHTTPServer.BaseHTTPRequestHandler): 81 | """A handler for OAuth 2.0 redirects back to localhost. 82 | 83 | Waits for a single request and parses the query parameters 84 | into the servers query_params and then stops serving. 85 | """ 86 | 87 | def do_GET(s): 88 | """Handle a GET request. 89 | 90 | Parses the query parameters and prints a message 91 | if the flow has completed. Note that we can't detect 92 | if an error occurred. 93 | """ 94 | s.send_response(200) 95 | s.send_header("Content-type", "text/html") 96 | s.end_headers() 97 | query = s.path.split('?', 1)[-1] 98 | query = dict(parse_qsl(query)) 99 | s.server.query_params = query 100 | s.wfile.write("Authentication Status") 101 | s.wfile.write("

The authentication flow has completed.

") 102 | s.wfile.write("") 103 | 104 | def log_message(self, format, *args): 105 | """Do not log messages to stdout while running as command line program.""" 106 | pass 107 | 108 | 109 | @util.positional(3) 110 | def run_flow(flow, storage, flags, http=None): 111 | """Core code for a command-line application. 112 | 113 | The run() function is called from your application and runs through all the 114 | steps to obtain credentials. It takes a Flow argument and attempts to open an 115 | authorization server page in the user's default web browser. The server asks 116 | the user to grant your application access to the user's data. If the user 117 | grants access, the run() function returns new credentials. The new credentials 118 | are also stored in the Storage argument, which updates the file associated 119 | with the Storage object. 120 | 121 | It presumes it is run from a command-line application and supports the 122 | following flags: 123 | 124 | --auth_host_name: Host name to use when running a local web server 125 | to handle redirects during OAuth authorization. 126 | (default: 'localhost') 127 | 128 | --auth_host_port: Port to use when running a local web server to handle 129 | redirects during OAuth authorization.; 130 | repeat this option to specify a list of values 131 | (default: '[8080, 8090]') 132 | (an integer) 133 | 134 | --[no]auth_local_webserver: Run a local web server to handle redirects 135 | during OAuth authorization. 136 | (default: 'true') 137 | 138 | The tools module defines an ArgumentParser the already contains the flag 139 | definitions that run() requires. You can pass that ArgumentParser to your 140 | ArgumentParser constructor: 141 | 142 | parser = argparse.ArgumentParser(description=__doc__, 143 | formatter_class=argparse.RawDescriptionHelpFormatter, 144 | parents=[tools.run_parser]) 145 | flags = parser.parse_args(argv) 146 | 147 | Args: 148 | flow: Flow, an OAuth 2.0 Flow to step through. 149 | storage: Storage, a Storage to store the credential in. 150 | flags: argparse.ArgumentParser, the command-line flags. 151 | http: An instance of httplib2.Http.request 152 | or something that acts like it. 153 | 154 | Returns: 155 | Credentials, the obtained credential. 156 | """ 157 | logging.getLogger().setLevel(getattr(logging, flags.logging_level)) 158 | if not flags.noauth_local_webserver: 159 | success = False 160 | port_number = 0 161 | for port in flags.auth_host_port: 162 | port_number = port 163 | try: 164 | httpd = ClientRedirectServer((flags.auth_host_name, port), 165 | ClientRedirectHandler) 166 | except socket.error, e: 167 | pass 168 | else: 169 | success = True 170 | break 171 | flags.noauth_local_webserver = not success 172 | if not success: 173 | print 'Failed to start a local webserver listening on either port 8080' 174 | print 'or port 9090. Please check your firewall settings and locally' 175 | print 'running programs that may be blocking or using those ports.' 176 | print 177 | print 'Falling back to --noauth_local_webserver and continuing with', 178 | print 'authorization.' 179 | print 180 | 181 | if not flags.noauth_local_webserver: 182 | oauth_callback = 'http://%s:%s/' % (flags.auth_host_name, port_number) 183 | else: 184 | oauth_callback = client.OOB_CALLBACK_URN 185 | flow.redirect_uri = oauth_callback 186 | authorize_url = flow.step1_get_authorize_url() 187 | 188 | if not flags.noauth_local_webserver: 189 | webbrowser.open(authorize_url, new=1, autoraise=True) 190 | print 'Your browser has been opened to visit:' 191 | print 192 | print ' ' + authorize_url 193 | print 194 | print 'If your browser is on a different machine then exit and re-run this' 195 | print 'application with the command-line parameter ' 196 | print 197 | print ' --noauth_local_webserver' 198 | print 199 | else: 200 | print 'Go to the following link in your browser:' 201 | print 202 | print ' ' + authorize_url 203 | print 204 | 205 | code = None 206 | if not flags.noauth_local_webserver: 207 | httpd.handle_request() 208 | if 'error' in httpd.query_params: 209 | sys.exit('Authentication request was rejected.') 210 | if 'code' in httpd.query_params: 211 | code = httpd.query_params['code'] 212 | else: 213 | print 'Failed to find "code" in the query parameters of the redirect.' 214 | sys.exit('Try running with --noauth_local_webserver.') 215 | else: 216 | code = raw_input('Enter verification code: ').strip() 217 | 218 | try: 219 | credential = flow.step2_exchange(code, http=http) 220 | except client.FlowExchangeError, e: 221 | sys.exit('Authentication has failed: %s' % e) 222 | 223 | storage.put(credential) 224 | credential.set_store(storage) 225 | print 'Authentication successful.' 226 | 227 | return credential 228 | 229 | 230 | def message_if_missing(filename): 231 | """Helpful message to display if the CLIENT_SECRETS file is missing.""" 232 | 233 | return _CLIENT_SECRETS_MESSAGE % filename 234 | 235 | try: 236 | from old_run import run 237 | from old_run import FLAGS 238 | except ImportError: 239 | def run(*args, **kwargs): 240 | raise NotImplementedError( 241 | 'The gflags library must be installed to use tools.run(). ' 242 | 'Please install gflags or preferrably switch to using ' 243 | 'tools.run_flow().') 244 | -------------------------------------------------------------------------------- /apiclient/schema.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2010 Google Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Schema processing for discovery based APIs 16 | 17 | Schemas holds an APIs discovery schemas. It can return those schema as 18 | deserialized JSON objects, or pretty print them as prototype objects that 19 | conform to the schema. 20 | 21 | For example, given the schema: 22 | 23 | schema = \"\"\"{ 24 | "Foo": { 25 | "type": "object", 26 | "properties": { 27 | "etag": { 28 | "type": "string", 29 | "description": "ETag of the collection." 30 | }, 31 | "kind": { 32 | "type": "string", 33 | "description": "Type of the collection ('calendar#acl').", 34 | "default": "calendar#acl" 35 | }, 36 | "nextPageToken": { 37 | "type": "string", 38 | "description": "Token used to access the next 39 | page of this result. Omitted if no further results are available." 40 | } 41 | } 42 | } 43 | }\"\"\" 44 | 45 | s = Schemas(schema) 46 | print s.prettyPrintByName('Foo') 47 | 48 | Produces the following output: 49 | 50 | { 51 | "nextPageToken": "A String", # Token used to access the 52 | # next page of this result. Omitted if no further results are available. 53 | "kind": "A String", # Type of the collection ('calendar#acl'). 54 | "etag": "A String", # ETag of the collection. 55 | }, 56 | 57 | The constructor takes a discovery document in which to look up named schema. 58 | """ 59 | 60 | # TODO(jcgregorio) support format, enum, minimum, maximum 61 | 62 | __author__ = 'jcgregorio@google.com (Joe Gregorio)' 63 | 64 | import copy 65 | 66 | from oauth2client import util 67 | from oauth2client.anyjson import simplejson 68 | 69 | 70 | class Schemas(object): 71 | """Schemas for an API.""" 72 | 73 | def __init__(self, discovery): 74 | """Constructor. 75 | 76 | Args: 77 | discovery: object, Deserialized discovery document from which we pull 78 | out the named schema. 79 | """ 80 | self.schemas = discovery.get('schemas', {}) 81 | 82 | # Cache of pretty printed schemas. 83 | self.pretty = {} 84 | 85 | @util.positional(2) 86 | def _prettyPrintByName(self, name, seen=None, dent=0): 87 | """Get pretty printed object prototype from the schema name. 88 | 89 | Args: 90 | name: string, Name of schema in the discovery document. 91 | seen: list of string, Names of schema already seen. Used to handle 92 | recursive definitions. 93 | 94 | Returns: 95 | string, A string that contains a prototype object with 96 | comments that conforms to the given schema. 97 | """ 98 | if seen is None: 99 | seen = [] 100 | 101 | if name in seen: 102 | # Do not fall into an infinite loop over recursive definitions. 103 | return '# Object with schema name: %s' % name 104 | seen.append(name) 105 | 106 | if name not in self.pretty: 107 | self.pretty[name] = _SchemaToStruct(self.schemas[name], 108 | seen, dent=dent).to_str(self._prettyPrintByName) 109 | 110 | seen.pop() 111 | 112 | return self.pretty[name] 113 | 114 | def prettyPrintByName(self, name): 115 | """Get pretty printed object prototype from the schema name. 116 | 117 | Args: 118 | name: string, Name of schema in the discovery document. 119 | 120 | Returns: 121 | string, A string that contains a prototype object with 122 | comments that conforms to the given schema. 123 | """ 124 | # Return with trailing comma and newline removed. 125 | return self._prettyPrintByName(name, seen=[], dent=1)[:-2] 126 | 127 | @util.positional(2) 128 | def _prettyPrintSchema(self, schema, seen=None, dent=0): 129 | """Get pretty printed object prototype of schema. 130 | 131 | Args: 132 | schema: object, Parsed JSON schema. 133 | seen: list of string, Names of schema already seen. Used to handle 134 | recursive definitions. 135 | 136 | Returns: 137 | string, A string that contains a prototype object with 138 | comments that conforms to the given schema. 139 | """ 140 | if seen is None: 141 | seen = [] 142 | 143 | return _SchemaToStruct(schema, seen, dent=dent).to_str(self._prettyPrintByName) 144 | 145 | def prettyPrintSchema(self, schema): 146 | """Get pretty printed object prototype of schema. 147 | 148 | Args: 149 | schema: object, Parsed JSON schema. 150 | 151 | Returns: 152 | string, A string that contains a prototype object with 153 | comments that conforms to the given schema. 154 | """ 155 | # Return with trailing comma and newline removed. 156 | return self._prettyPrintSchema(schema, dent=1)[:-2] 157 | 158 | def get(self, name): 159 | """Get deserialized JSON schema from the schema name. 160 | 161 | Args: 162 | name: string, Schema name. 163 | """ 164 | return self.schemas[name] 165 | 166 | 167 | class _SchemaToStruct(object): 168 | """Convert schema to a prototype object.""" 169 | 170 | @util.positional(3) 171 | def __init__(self, schema, seen, dent=0): 172 | """Constructor. 173 | 174 | Args: 175 | schema: object, Parsed JSON schema. 176 | seen: list, List of names of schema already seen while parsing. Used to 177 | handle recursive definitions. 178 | dent: int, Initial indentation depth. 179 | """ 180 | # The result of this parsing kept as list of strings. 181 | self.value = [] 182 | 183 | # The final value of the parsing. 184 | self.string = None 185 | 186 | # The parsed JSON schema. 187 | self.schema = schema 188 | 189 | # Indentation level. 190 | self.dent = dent 191 | 192 | # Method that when called returns a prototype object for the schema with 193 | # the given name. 194 | self.from_cache = None 195 | 196 | # List of names of schema already seen while parsing. 197 | self.seen = seen 198 | 199 | def emit(self, text): 200 | """Add text as a line to the output. 201 | 202 | Args: 203 | text: string, Text to output. 204 | """ 205 | self.value.extend([" " * self.dent, text, '\n']) 206 | 207 | def emitBegin(self, text): 208 | """Add text to the output, but with no line terminator. 209 | 210 | Args: 211 | text: string, Text to output. 212 | """ 213 | self.value.extend([" " * self.dent, text]) 214 | 215 | def emitEnd(self, text, comment): 216 | """Add text and comment to the output with line terminator. 217 | 218 | Args: 219 | text: string, Text to output. 220 | comment: string, Python comment. 221 | """ 222 | if comment: 223 | divider = '\n' + ' ' * (self.dent + 2) + '# ' 224 | lines = comment.splitlines() 225 | lines = [x.rstrip() for x in lines] 226 | comment = divider.join(lines) 227 | self.value.extend([text, ' # ', comment, '\n']) 228 | else: 229 | self.value.extend([text, '\n']) 230 | 231 | def indent(self): 232 | """Increase indentation level.""" 233 | self.dent += 1 234 | 235 | def undent(self): 236 | """Decrease indentation level.""" 237 | self.dent -= 1 238 | 239 | def _to_str_impl(self, schema): 240 | """Prototype object based on the schema, in Python code with comments. 241 | 242 | Args: 243 | schema: object, Parsed JSON schema file. 244 | 245 | Returns: 246 | Prototype object based on the schema, in Python code with comments. 247 | """ 248 | stype = schema.get('type') 249 | if stype == 'object': 250 | self.emitEnd('{', schema.get('description', '')) 251 | self.indent() 252 | if 'properties' in schema: 253 | for pname, pschema in schema.get('properties', {}).iteritems(): 254 | self.emitBegin('"%s": ' % pname) 255 | self._to_str_impl(pschema) 256 | elif 'additionalProperties' in schema: 257 | self.emitBegin('"a_key": ') 258 | self._to_str_impl(schema['additionalProperties']) 259 | self.undent() 260 | self.emit('},') 261 | elif '$ref' in schema: 262 | schemaName = schema['$ref'] 263 | description = schema.get('description', '') 264 | s = self.from_cache(schemaName, seen=self.seen) 265 | parts = s.splitlines() 266 | self.emitEnd(parts[0], description) 267 | for line in parts[1:]: 268 | self.emit(line.rstrip()) 269 | elif stype == 'boolean': 270 | value = schema.get('default', 'True or False') 271 | self.emitEnd('%s,' % str(value), schema.get('description', '')) 272 | elif stype == 'string': 273 | value = schema.get('default', 'A String') 274 | self.emitEnd('"%s",' % str(value), schema.get('description', '')) 275 | elif stype == 'integer': 276 | value = schema.get('default', '42') 277 | self.emitEnd('%s,' % str(value), schema.get('description', '')) 278 | elif stype == 'number': 279 | value = schema.get('default', '3.14') 280 | self.emitEnd('%s,' % str(value), schema.get('description', '')) 281 | elif stype == 'null': 282 | self.emitEnd('None,', schema.get('description', '')) 283 | elif stype == 'any': 284 | self.emitEnd('"",', schema.get('description', '')) 285 | elif stype == 'array': 286 | self.emitEnd('[', schema.get('description')) 287 | self.indent() 288 | self.emitBegin('') 289 | self._to_str_impl(schema['items']) 290 | self.undent() 291 | self.emit('],') 292 | else: 293 | self.emit('Unknown type! %s' % stype) 294 | self.emitEnd('', '') 295 | 296 | self.string = ''.join(self.value) 297 | return self.string 298 | 299 | def to_str(self, from_cache): 300 | """Prototype object based on the schema, in Python code with comments. 301 | 302 | Args: 303 | from_cache: callable(name, seen), Callable that retrieves an object 304 | prototype for a schema with the given name. Seen is a list of schema 305 | names already seen as we recursively descend the schema definition. 306 | 307 | Returns: 308 | Prototype object based on the schema, in Python code with comments. 309 | The lines of the code will all be properly indented. 310 | """ 311 | self.from_cache = from_cache 312 | return self._to_str_impl(self.schema) 313 | -------------------------------------------------------------------------------- /apiclient/channel.py: -------------------------------------------------------------------------------- 1 | """Channel notifications support. 2 | 3 | Classes and functions to support channel subscriptions and notifications 4 | on those channels. 5 | 6 | Notes: 7 | - This code is based on experimental APIs and is subject to change. 8 | - Notification does not do deduplication of notification ids, that's up to 9 | the receiver. 10 | - Storing the Channel between calls is up to the caller. 11 | 12 | 13 | Example setting up a channel: 14 | 15 | # Create a new channel that gets notifications via webhook. 16 | channel = new_webhook_channel("https://example.com/my_web_hook") 17 | 18 | # Store the channel, keyed by 'channel.id'. Store it before calling the 19 | # watch method because notifications may start arriving before the watch 20 | # method returns. 21 | ... 22 | 23 | resp = service.objects().watchAll( 24 | bucket="some_bucket_id", body=channel.body()).execute() 25 | channel.update(resp) 26 | 27 | # Store the channel, keyed by 'channel.id'. Store it after being updated 28 | # since the resource_id value will now be correct, and that's needed to 29 | # stop a subscription. 30 | ... 31 | 32 | 33 | An example Webhook implementation using webapp2. Note that webapp2 puts 34 | headers in a case insensitive dictionary, as headers aren't guaranteed to 35 | always be upper case. 36 | 37 | id = self.request.headers[X_GOOG_CHANNEL_ID] 38 | 39 | # Retrieve the channel by id. 40 | channel = ... 41 | 42 | # Parse notification from the headers, including validating the id. 43 | n = notification_from_headers(channel, self.request.headers) 44 | 45 | # Do app specific stuff with the notification here. 46 | if n.resource_state == 'sync': 47 | # Code to handle sync state. 48 | elif n.resource_state == 'exists': 49 | # Code to handle the exists state. 50 | elif n.resource_state == 'not_exists': 51 | # Code to handle the not exists state. 52 | 53 | 54 | Example of unsubscribing. 55 | 56 | service.channels().stop(channel.body()) 57 | """ 58 | 59 | import datetime 60 | import uuid 61 | 62 | from apiclient import errors 63 | from oauth2client import util 64 | 65 | 66 | # The unix time epoch starts at midnight 1970. 67 | EPOCH = datetime.datetime.utcfromtimestamp(0) 68 | 69 | # Map the names of the parameters in the JSON channel description to 70 | # the parameter names we use in the Channel class. 71 | CHANNEL_PARAMS = { 72 | 'address': 'address', 73 | 'id': 'id', 74 | 'expiration': 'expiration', 75 | 'params': 'params', 76 | 'resourceId': 'resource_id', 77 | 'resourceUri': 'resource_uri', 78 | 'type': 'type', 79 | 'token': 'token', 80 | } 81 | 82 | X_GOOG_CHANNEL_ID = 'X-GOOG-CHANNEL-ID' 83 | X_GOOG_MESSAGE_NUMBER = 'X-GOOG-MESSAGE-NUMBER' 84 | X_GOOG_RESOURCE_STATE = 'X-GOOG-RESOURCE-STATE' 85 | X_GOOG_RESOURCE_URI = 'X-GOOG-RESOURCE-URI' 86 | X_GOOG_RESOURCE_ID = 'X-GOOG-RESOURCE-ID' 87 | 88 | 89 | def _upper_header_keys(headers): 90 | new_headers = {} 91 | for k, v in headers.iteritems(): 92 | new_headers[k.upper()] = v 93 | return new_headers 94 | 95 | 96 | class Notification(object): 97 | """A Notification from a Channel. 98 | 99 | Notifications are not usually constructed directly, but are returned 100 | from functions like notification_from_headers(). 101 | 102 | Attributes: 103 | message_number: int, The unique id number of this notification. 104 | state: str, The state of the resource being monitored. 105 | uri: str, The address of the resource being monitored. 106 | resource_id: str, The unique identifier of the version of the resource at 107 | this event. 108 | """ 109 | @util.positional(5) 110 | def __init__(self, message_number, state, resource_uri, resource_id): 111 | """Notification constructor. 112 | 113 | Args: 114 | message_number: int, The unique id number of this notification. 115 | state: str, The state of the resource being monitored. Can be one 116 | of "exists", "not_exists", or "sync". 117 | resource_uri: str, The address of the resource being monitored. 118 | resource_id: str, The identifier of the watched resource. 119 | """ 120 | self.message_number = message_number 121 | self.state = state 122 | self.resource_uri = resource_uri 123 | self.resource_id = resource_id 124 | 125 | 126 | class Channel(object): 127 | """A Channel for notifications. 128 | 129 | Usually not constructed directly, instead it is returned from helper 130 | functions like new_webhook_channel(). 131 | 132 | Attributes: 133 | type: str, The type of delivery mechanism used by this channel. For 134 | example, 'web_hook'. 135 | id: str, A UUID for the channel. 136 | token: str, An arbitrary string associated with the channel that 137 | is delivered to the target address with each event delivered 138 | over this channel. 139 | address: str, The address of the receiving entity where events are 140 | delivered. Specific to the channel type. 141 | expiration: int, The time, in milliseconds from the epoch, when this 142 | channel will expire. 143 | params: dict, A dictionary of string to string, with additional parameters 144 | controlling delivery channel behavior. 145 | resource_id: str, An opaque id that identifies the resource that is 146 | being watched. Stable across different API versions. 147 | resource_uri: str, The canonicalized ID of the watched resource. 148 | """ 149 | 150 | @util.positional(5) 151 | def __init__(self, type, id, token, address, expiration=None, 152 | params=None, resource_id="", resource_uri=""): 153 | """Create a new Channel. 154 | 155 | In user code, this Channel constructor will not typically be called 156 | manually since there are functions for creating channels for each specific 157 | type with a more customized set of arguments to pass. 158 | 159 | Args: 160 | type: str, The type of delivery mechanism used by this channel. For 161 | example, 'web_hook'. 162 | id: str, A UUID for the channel. 163 | token: str, An arbitrary string associated with the channel that 164 | is delivered to the target address with each event delivered 165 | over this channel. 166 | address: str, The address of the receiving entity where events are 167 | delivered. Specific to the channel type. 168 | expiration: int, The time, in milliseconds from the epoch, when this 169 | channel will expire. 170 | params: dict, A dictionary of string to string, with additional parameters 171 | controlling delivery channel behavior. 172 | resource_id: str, An opaque id that identifies the resource that is 173 | being watched. Stable across different API versions. 174 | resource_uri: str, The canonicalized ID of the watched resource. 175 | """ 176 | self.type = type 177 | self.id = id 178 | self.token = token 179 | self.address = address 180 | self.expiration = expiration 181 | self.params = params 182 | self.resource_id = resource_id 183 | self.resource_uri = resource_uri 184 | 185 | def body(self): 186 | """Build a body from the Channel. 187 | 188 | Constructs a dictionary that's appropriate for passing into watch() 189 | methods as the value of body argument. 190 | 191 | Returns: 192 | A dictionary representation of the channel. 193 | """ 194 | result = { 195 | 'id': self.id, 196 | 'token': self.token, 197 | 'type': self.type, 198 | 'address': self.address 199 | } 200 | if self.params: 201 | result['params'] = self.params 202 | if self.resource_id: 203 | result['resourceId'] = self.resource_id 204 | if self.resource_uri: 205 | result['resourceUri'] = self.resource_uri 206 | if self.expiration: 207 | result['expiration'] = self.expiration 208 | 209 | return result 210 | 211 | def update(self, resp): 212 | """Update a channel with information from the response of watch(). 213 | 214 | When a request is sent to watch() a resource, the response returned 215 | from the watch() request is a dictionary with updated channel information, 216 | such as the resource_id, which is needed when stopping a subscription. 217 | 218 | Args: 219 | resp: dict, The response from a watch() method. 220 | """ 221 | for json_name, param_name in CHANNEL_PARAMS.iteritems(): 222 | value = resp.get(json_name) 223 | if value is not None: 224 | setattr(self, param_name, value) 225 | 226 | 227 | def notification_from_headers(channel, headers): 228 | """Parse a notification from the webhook request headers, validate 229 | the notification, and return a Notification object. 230 | 231 | Args: 232 | channel: Channel, The channel that the notification is associated with. 233 | headers: dict, A dictionary like object that contains the request headers 234 | from the webhook HTTP request. 235 | 236 | Returns: 237 | A Notification object. 238 | 239 | Raises: 240 | errors.InvalidNotificationError if the notification is invalid. 241 | ValueError if the X-GOOG-MESSAGE-NUMBER can't be converted to an int. 242 | """ 243 | headers = _upper_header_keys(headers) 244 | channel_id = headers[X_GOOG_CHANNEL_ID] 245 | if channel.id != channel_id: 246 | raise errors.InvalidNotificationError( 247 | 'Channel id mismatch: %s != %s' % (channel.id, channel_id)) 248 | else: 249 | message_number = int(headers[X_GOOG_MESSAGE_NUMBER]) 250 | state = headers[X_GOOG_RESOURCE_STATE] 251 | resource_uri = headers[X_GOOG_RESOURCE_URI] 252 | resource_id = headers[X_GOOG_RESOURCE_ID] 253 | return Notification(message_number, state, resource_uri, resource_id) 254 | 255 | 256 | @util.positional(2) 257 | def new_webhook_channel(url, token=None, expiration=None, params=None): 258 | """Create a new webhook Channel. 259 | 260 | Args: 261 | url: str, URL to post notifications to. 262 | token: str, An arbitrary string associated with the channel that 263 | is delivered to the target address with each notification delivered 264 | over this channel. 265 | expiration: datetime.datetime, A time in the future when the channel 266 | should expire. Can also be None if the subscription should use the 267 | default expiration. Note that different services may have different 268 | limits on how long a subscription lasts. Check the response from the 269 | watch() method to see the value the service has set for an expiration 270 | time. 271 | params: dict, Extra parameters to pass on channel creation. Currently 272 | not used for webhook channels. 273 | """ 274 | expiration_ms = 0 275 | if expiration: 276 | delta = expiration - EPOCH 277 | expiration_ms = delta.microseconds/1000 + ( 278 | delta.seconds + delta.days*24*3600)*1000 279 | if expiration_ms < 0: 280 | expiration_ms = 0 281 | 282 | return Channel('web_hook', str(uuid.uuid4()), 283 | token, url, expiration=expiration_ms, 284 | params=params) 285 | 286 | -------------------------------------------------------------------------------- /oauth2client/crypt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2.4 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (C) 2011 Google Inc. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | import base64 19 | import hashlib 20 | import logging 21 | import time 22 | 23 | from anyjson import simplejson 24 | 25 | 26 | CLOCK_SKEW_SECS = 300 # 5 minutes in seconds 27 | AUTH_TOKEN_LIFETIME_SECS = 300 # 5 minutes in seconds 28 | MAX_TOKEN_LIFETIME_SECS = 86400 # 1 day in seconds 29 | 30 | 31 | logger = logging.getLogger(__name__) 32 | 33 | 34 | class AppIdentityError(Exception): 35 | pass 36 | 37 | 38 | try: 39 | from OpenSSL import crypto 40 | 41 | 42 | class OpenSSLVerifier(object): 43 | """Verifies the signature on a message.""" 44 | 45 | def __init__(self, pubkey): 46 | """Constructor. 47 | 48 | Args: 49 | pubkey, OpenSSL.crypto.PKey, The public key to verify with. 50 | """ 51 | self._pubkey = pubkey 52 | 53 | def verify(self, message, signature): 54 | """Verifies a message against a signature. 55 | 56 | Args: 57 | message: string, The message to verify. 58 | signature: string, The signature on the message. 59 | 60 | Returns: 61 | True if message was signed by the private key associated with the public 62 | key that this object was constructed with. 63 | """ 64 | try: 65 | crypto.verify(self._pubkey, signature, message, 'sha256') 66 | return True 67 | except: 68 | return False 69 | 70 | @staticmethod 71 | def from_string(key_pem, is_x509_cert): 72 | """Construct a Verified instance from a string. 73 | 74 | Args: 75 | key_pem: string, public key in PEM format. 76 | is_x509_cert: bool, True if key_pem is an X509 cert, otherwise it is 77 | expected to be an RSA key in PEM format. 78 | 79 | Returns: 80 | Verifier instance. 81 | 82 | Raises: 83 | OpenSSL.crypto.Error if the key_pem can't be parsed. 84 | """ 85 | if is_x509_cert: 86 | pubkey = crypto.load_certificate(crypto.FILETYPE_PEM, key_pem) 87 | else: 88 | pubkey = crypto.load_privatekey(crypto.FILETYPE_PEM, key_pem) 89 | return OpenSSLVerifier(pubkey) 90 | 91 | 92 | class OpenSSLSigner(object): 93 | """Signs messages with a private key.""" 94 | 95 | def __init__(self, pkey): 96 | """Constructor. 97 | 98 | Args: 99 | pkey, OpenSSL.crypto.PKey (or equiv), The private key to sign with. 100 | """ 101 | self._key = pkey 102 | 103 | def sign(self, message): 104 | """Signs a message. 105 | 106 | Args: 107 | message: string, Message to be signed. 108 | 109 | Returns: 110 | string, The signature of the message for the given key. 111 | """ 112 | return crypto.sign(self._key, message, 'sha256') 113 | 114 | @staticmethod 115 | def from_string(key, password='notasecret'): 116 | """Construct a Signer instance from a string. 117 | 118 | Args: 119 | key: string, private key in PKCS12 or PEM format. 120 | password: string, password for the private key file. 121 | 122 | Returns: 123 | Signer instance. 124 | 125 | Raises: 126 | OpenSSL.crypto.Error if the key can't be parsed. 127 | """ 128 | if key.startswith('-----BEGIN '): 129 | pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, key) 130 | else: 131 | pkey = crypto.load_pkcs12(key, password).get_privatekey() 132 | return OpenSSLSigner(pkey) 133 | 134 | except ImportError: 135 | OpenSSLVerifier = None 136 | OpenSSLSigner = None 137 | 138 | 139 | try: 140 | from Crypto.PublicKey import RSA 141 | from Crypto.Hash import SHA256 142 | from Crypto.Signature import PKCS1_v1_5 143 | 144 | 145 | class PyCryptoVerifier(object): 146 | """Verifies the signature on a message.""" 147 | 148 | def __init__(self, pubkey): 149 | """Constructor. 150 | 151 | Args: 152 | pubkey, OpenSSL.crypto.PKey (or equiv), The public key to verify with. 153 | """ 154 | self._pubkey = pubkey 155 | 156 | def verify(self, message, signature): 157 | """Verifies a message against a signature. 158 | 159 | Args: 160 | message: string, The message to verify. 161 | signature: string, The signature on the message. 162 | 163 | Returns: 164 | True if message was signed by the private key associated with the public 165 | key that this object was constructed with. 166 | """ 167 | try: 168 | return PKCS1_v1_5.new(self._pubkey).verify( 169 | SHA256.new(message), signature) 170 | except: 171 | return False 172 | 173 | @staticmethod 174 | def from_string(key_pem, is_x509_cert): 175 | """Construct a Verified instance from a string. 176 | 177 | Args: 178 | key_pem: string, public key in PEM format. 179 | is_x509_cert: bool, True if key_pem is an X509 cert, otherwise it is 180 | expected to be an RSA key in PEM format. 181 | 182 | Returns: 183 | Verifier instance. 184 | 185 | Raises: 186 | NotImplementedError if is_x509_cert is true. 187 | """ 188 | if is_x509_cert: 189 | raise NotImplementedError( 190 | 'X509 certs are not supported by the PyCrypto library. ' 191 | 'Try using PyOpenSSL if native code is an option.') 192 | else: 193 | pubkey = RSA.importKey(key_pem) 194 | return PyCryptoVerifier(pubkey) 195 | 196 | 197 | class PyCryptoSigner(object): 198 | """Signs messages with a private key.""" 199 | 200 | def __init__(self, pkey): 201 | """Constructor. 202 | 203 | Args: 204 | pkey, OpenSSL.crypto.PKey (or equiv), The private key to sign with. 205 | """ 206 | self._key = pkey 207 | 208 | def sign(self, message): 209 | """Signs a message. 210 | 211 | Args: 212 | message: string, Message to be signed. 213 | 214 | Returns: 215 | string, The signature of the message for the given key. 216 | """ 217 | return PKCS1_v1_5.new(self._key).sign(SHA256.new(message)) 218 | 219 | @staticmethod 220 | def from_string(key, password='notasecret'): 221 | """Construct a Signer instance from a string. 222 | 223 | Args: 224 | key: string, private key in PEM format. 225 | password: string, password for private key file. Unused for PEM files. 226 | 227 | Returns: 228 | Signer instance. 229 | 230 | Raises: 231 | NotImplementedError if they key isn't in PEM format. 232 | """ 233 | if key.startswith('-----BEGIN '): 234 | pkey = RSA.importKey(key) 235 | else: 236 | raise NotImplementedError( 237 | 'PKCS12 format is not supported by the PyCrpto library. ' 238 | 'Try converting to a "PEM" ' 239 | '(openssl pkcs12 -in xxxxx.p12 -nodes -nocerts > privatekey.pem) ' 240 | 'or using PyOpenSSL if native code is an option.') 241 | return PyCryptoSigner(pkey) 242 | 243 | except ImportError: 244 | PyCryptoVerifier = None 245 | PyCryptoSigner = None 246 | 247 | 248 | if OpenSSLSigner: 249 | Signer = OpenSSLSigner 250 | Verifier = OpenSSLVerifier 251 | elif PyCryptoSigner: 252 | Signer = PyCryptoSigner 253 | Verifier = PyCryptoVerifier 254 | else: 255 | raise ImportError('No encryption library found. Please install either ' 256 | 'PyOpenSSL, or PyCrypto 2.6 or later') 257 | 258 | 259 | def _urlsafe_b64encode(raw_bytes): 260 | return base64.urlsafe_b64encode(raw_bytes).rstrip('=') 261 | 262 | 263 | def _urlsafe_b64decode(b64string): 264 | # Guard against unicode strings, which base64 can't handle. 265 | b64string = b64string.encode('ascii') 266 | padded = b64string + '=' * (4 - len(b64string) % 4) 267 | return base64.urlsafe_b64decode(padded) 268 | 269 | 270 | def _json_encode(data): 271 | return simplejson.dumps(data, separators = (',', ':')) 272 | 273 | 274 | def make_signed_jwt(signer, payload): 275 | """Make a signed JWT. 276 | 277 | See http://self-issued.info/docs/draft-jones-json-web-token.html. 278 | 279 | Args: 280 | signer: crypt.Signer, Cryptographic signer. 281 | payload: dict, Dictionary of data to convert to JSON and then sign. 282 | 283 | Returns: 284 | string, The JWT for the payload. 285 | """ 286 | header = {'typ': 'JWT', 'alg': 'RS256'} 287 | 288 | segments = [ 289 | _urlsafe_b64encode(_json_encode(header)), 290 | _urlsafe_b64encode(_json_encode(payload)), 291 | ] 292 | signing_input = '.'.join(segments) 293 | 294 | signature = signer.sign(signing_input) 295 | segments.append(_urlsafe_b64encode(signature)) 296 | 297 | logger.debug(str(segments)) 298 | 299 | return '.'.join(segments) 300 | 301 | 302 | def verify_signed_jwt_with_certs(jwt, certs, audience): 303 | """Verify a JWT against public certs. 304 | 305 | See http://self-issued.info/docs/draft-jones-json-web-token.html. 306 | 307 | Args: 308 | jwt: string, A JWT. 309 | certs: dict, Dictionary where values of public keys in PEM format. 310 | audience: string, The audience, 'aud', that this JWT should contain. If 311 | None then the JWT's 'aud' parameter is not verified. 312 | 313 | Returns: 314 | dict, The deserialized JSON payload in the JWT. 315 | 316 | Raises: 317 | AppIdentityError if any checks are failed. 318 | """ 319 | segments = jwt.split('.') 320 | 321 | if (len(segments) != 3): 322 | raise AppIdentityError( 323 | 'Wrong number of segments in token: %s' % jwt) 324 | signed = '%s.%s' % (segments[0], segments[1]) 325 | 326 | signature = _urlsafe_b64decode(segments[2]) 327 | 328 | # Parse token. 329 | json_body = _urlsafe_b64decode(segments[1]) 330 | try: 331 | parsed = simplejson.loads(json_body) 332 | except: 333 | raise AppIdentityError('Can\'t parse token: %s' % json_body) 334 | 335 | # Check signature. 336 | verified = False 337 | for (keyname, pem) in certs.items(): 338 | verifier = Verifier.from_string(pem, True) 339 | if (verifier.verify(signed, signature)): 340 | verified = True 341 | break 342 | if not verified: 343 | raise AppIdentityError('Invalid token signature: %s' % jwt) 344 | 345 | # Check creation timestamp. 346 | iat = parsed.get('iat') 347 | if iat is None: 348 | raise AppIdentityError('No iat field in token: %s' % json_body) 349 | earliest = iat - CLOCK_SKEW_SECS 350 | 351 | # Check expiration timestamp. 352 | now = long(time.time()) 353 | exp = parsed.get('exp') 354 | if exp is None: 355 | raise AppIdentityError('No exp field in token: %s' % json_body) 356 | if exp >= now + MAX_TOKEN_LIFETIME_SECS: 357 | raise AppIdentityError( 358 | 'exp field too far in future: %s' % json_body) 359 | latest = exp + CLOCK_SKEW_SECS 360 | 361 | if now < earliest: 362 | raise AppIdentityError('Token used too early, %d < %d: %s' % 363 | (now, earliest, json_body)) 364 | if now > latest: 365 | raise AppIdentityError('Token used too late, %d > %d: %s' % 366 | (now, latest, json_body)) 367 | 368 | # Check audience. 369 | if audience is not None: 370 | aud = parsed.get('aud') 371 | if aud is None: 372 | raise AppIdentityError('No aud field in token: %s' % json_body) 373 | if aud != audience: 374 | raise AppIdentityError('Wrong recipient, %s != %s: %s' % 375 | (aud, audience, json_body)) 376 | 377 | return parsed 378 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2007 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | import cgi 18 | import datetime 19 | import time 20 | import webapp2 21 | 22 | import hashlib 23 | import pickle 24 | 25 | from google.appengine.ext import db 26 | from google.appengine.api import memcache 27 | 28 | import youtube_integration 29 | from database_tables import * 30 | 31 | GREY_CHANNELS = [ 'CGPGrey'] 32 | BRADY_CHANNELS = [ 'numberphile', 'Computerphile', 33 | 'sixtysymbols', 'periodicvideos', 'nottinghamscience', 34 | 'DeepSkyVideos', 'bibledex', 'wordsoftheworld', 35 | 'FavScientist', 'psyfile', 'BackstageScience', 36 | 'foodskey', 'BradyStuff'] 37 | 38 | def disp_viewcount(db_views): 39 | if db_views == 301: 40 | return '301' 41 | elif db_views >= 0: 42 | return db_views 43 | elif db_views == -1: 44 | return "<live video>" 45 | elif db_views == -2: 46 | return "<not yet calculated>" 47 | else: 48 | return "<error>" 49 | 50 | def load_front_data(): 51 | bradyVids = memcache.get('bradyVids') 52 | greyVid = memcache.get('greyVid') 53 | lastUpdate = memcache.get('lastUpdate') 54 | 55 | greyViews = memcache.get('greyViews') 56 | bradyTotal = memcache.get('bradyTotal') 57 | bradyAverage = memcache.get('bradyAverage') 58 | 59 | if not (bradyVids and greyVid and lastUpdate and greyViews and bradyTotal and bradyAverage): 60 | bradyVids = db.GqlQuery("SELECT * FROM BradyVideo ORDER BY published DESC") 61 | bradyVids = list(bradyVids) 62 | memcache.set('bradyVids', bradyVids) 63 | 64 | greyVid = list(db.GqlQuery("SELECT * FROM GreyVideo ORDER BY published DESC LIMIT 1"))[0] 65 | memcache.set('greyVid', greyVid) 66 | 67 | lastUpdate = list(db.GqlQuery("SELECT * FROM UpdateLog ORDER BY update_time DESC LIMIT 1"))[0].update_time 68 | memcache.set('lastUpdate', lastUpdate) 69 | 70 | greyViews = disp_viewcount(greyVid.viewcount) 71 | memcache.set('greyViews', greyViews) 72 | 73 | countable_brady_counts = [vid.viewcount for vid in bradyVids if vid.viewcount>=0] 74 | 75 | bradyTotal = sum(countable_brady_counts) 76 | memcache.set('bradyTotal', bradyTotal) 77 | 78 | 79 | if len(countable_brady_counts)==0: 80 | bradyAvg = 'N/A' 81 | else: 82 | bradyAvg = bradyTotal/len(countable_brady_counts) 83 | 84 | memcache.set('bradyAvg', bradyAvg) 85 | 86 | return (bradyVids, greyVid, lastUpdate, greyViews, bradyTotal, bradyAvg) 87 | 88 | def esc(s): 89 | return cgi.escape(s,quote=True) 90 | 91 | 92 | class Handler(webapp2.RequestHandler): 93 | def write(self,content): 94 | self.response.out.write(content) 95 | 96 | def set_cookie(self, name, value, expires=None, path='/', domain=None): 97 | extra_info = '' 98 | if expires: 99 | extra_info += 'expires=%s; '%expires 100 | if path: 101 | extra_info += 'path=%s; '%path 102 | if domain: 103 | extra_info += 'domain=%s; '%domain 104 | if extra_info: 105 | extra_info = '; ' + extra_info[:-2] 106 | main_statement = '%s=%s'%(name,value) 107 | set_cookie_val = main_statement + extra_info 108 | self.response.headers.add_header('Set-Cookie',set_cookie_val) 109 | 110 | def clear_cookie(self, name): 111 | cookie = self.read_cookie(name) 112 | if not cookie: 113 | return 114 | self.set_cookie(name, '', path=None) 115 | 116 | def read_cookie(self, name, alt=None): 117 | return self.request.cookies.get(name,alt) 118 | 119 | page_template = """ 120 | 121 | 122 | 123 | 124 | Brady vs. Grey | Original 125 | 136 | 137 | 146 | 147 | 148 | Q: How many videos has Brady Haran released since C.G.P. Grey last released a video?
149 | A: 150 |
151 | %(number)s 152 |


153 | 154 | 155 | 158 | 161 | 164 | 167 | 170 | 171 | %(rows)s 172 |
156 | Creator 157 | 159 | Channel 160 | 162 | Uploaded 163 | 165 | View Count 166 | 168 | Title/Link 169 |
173 |
174 | Q: How do their view counts compare?
A:
175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 189 | 190 | 191 | 192 | 193 | 194 |
Grey%(grey_views)s
Brady: Average%(brady_avg)s
187 | 188 |
Brady: Total%(brady_total)s
195 | 196 |
197 | Last updated: %(refresh_date)s. 198 | Powered by YouTube Data API (v3). 199 |
200 | View the source code on GitHub. 201 |
202 | Notes: 203 |
    204 |
  • 205 | To conserve processing power and network resources, this app should update only four times per day: 00:00, 06:00, 12:00, and 18:00 UTC. 206 |
  • 207 |
  • 208 | A new version of this web app is now available here. 209 |
  • 210 |
  • 211 | Recently, a pull request by Github user Eseb ensured that Grey's 2nd channel is not counted. This is a response to the video "⌘X 22.1: Seventeen". 212 |
  • 213 |
214 | 215 | 216 | """ 217 | def get_row(vid): 218 | return \ 219 | """ 220 | 221 | 222 | %(creator)s 223 | 224 | 225 | %(channel)s 226 | 227 | 228 | %(published)s 229 | 230 | 231 | %(views)s 232 | 233 | 234 | 235 | %(title)s 236 | 237 | 238 | 239 | """ % { 240 | 'title': vid.title, 241 | 'channel': vid.channel, 242 | 'published': vid.published.strftime('%B %d, %Y @ %I:%M %p'), 243 | 'url': 'http://youtu.be/' + vid.yt_id, 244 | 'creator': ("Brady Haran" if vid.channel in BRADY_CHANNELS else "C.G.P. Grey"), 245 | 'views': disp_viewcount(vid.viewcount) 246 | } 247 | 248 | class MainHandler(Handler): 249 | def get(self): 250 | bradyVids, greyVid, lastUpdate, greyViews, bradyTotal, bradyAvg = load_front_data() 251 | formatting_table = { } 252 | formatting_table['number'] = len(bradyVids) 253 | formatting_table['refresh_date'] = lastUpdate.strftime('%Y-%m-%d, %H:%M:%S UTC') 254 | formatting_table['rows'] = '\n'.join([get_row(greyVid)] + [get_row(vid) for vid in bradyVids[::-1]]) 255 | formatting_table['grey_views'] = greyViews 256 | formatting_table['brady_total'] = bradyTotal 257 | formatting_table['brady_avg'] = bradyAvg 258 | formatting_table['brady_visible'] = ('hidden' if greyViews > bradyTotal else '') 259 | formatting_table['brady_hidden'] = ('hidden' if greyViews <= bradyTotal else '') 260 | 261 | self.write(page_template%formatting_table) 262 | 263 | class UpdateHandler(Handler): 264 | def get(self): 265 | if self.request.headers.get('X-Appengine-Cron') != 'true': # This header is only given by a cron job. 266 | self.error(403) # Forbidden 267 | return 268 | 269 | 270 | all_grey_vids = [ ] 271 | for channel in GREY_CHANNELS: 272 | all_grey_vids += youtube_integration.get_vids(channel,'GreyVideo') 273 | 274 | all_brady_vids = [ ] 275 | for channel in BRADY_CHANNELS: 276 | all_brady_vids += youtube_integration.get_vids(channel,'BradyVideo') 277 | 278 | 279 | all_grey_vids.sort(key=lambda vid:vid.published, reverse=True) 280 | latest_grey_vid = all_grey_vids[0] 281 | 282 | all_brady_vids.sort(key=lambda vid:vid.published, reverse=True) 283 | 284 | for e in db.GqlQuery("SELECT * FROM BradyVideo"): 285 | e.delete() 286 | for e in [vid for vid in all_brady_vids if vid.published > latest_grey_vid.published]: 287 | e.put() 288 | 289 | for e in db.GqlQuery("SELECT * FROM GreyVideo"): 290 | e.delete() 291 | latest_grey_vid.viewcount = youtube_integration.get_view_count(latest_grey_vid.yt_id) 292 | latest_grey_vid.put() 293 | 294 | for e in db.GqlQuery("SELECT * FROM UpdateLog"): 295 | e.delete() 296 | UpdateLog().put() 297 | 298 | time.sleep(1) # Wait for all the videos to be added before view calculation 299 | 300 | for brady_vid in list(db.GqlQuery("SELECT * FROM BradyVideo")): 301 | brady_vid.viewcount = youtube_integration.get_view_count(brady_vid.yt_id) 302 | brady_vid.put() 303 | 304 | time.sleep(1) 305 | 306 | # Extra duplicate prevention 307 | already_cleaned = [ ] 308 | for vid in db.GqlQuery("SELECT * FROM BradyVideo"): 309 | if vid.yt_id in already_cleaned: # Prevents videos from being deleted entirely 310 | continue 311 | 312 | for other_vid in db.GqlQuery("SELECT * FROM BradyVideo"): 313 | if (other_vid.yt_id == vid.yt_id) and (vid.key().id() != other_vid.key().id()): 314 | other_vid.delete() 315 | already_cleaned.append(vid.yt_id) 316 | 317 | self.write('Database updated! You need to push this update for it to take effect.') 318 | 319 | class UpdatePushHandler(Handler): 320 | def get(self): 321 | if self.request.headers.get('X-Appengine-Cron') != 'true': # This header is only given by a cron job. 322 | self.error(403) # Forbidden 323 | return 324 | 325 | memcache.flush_all() 326 | load_front_data() 327 | 328 | self.write('Update pushed!') 329 | 330 | app = webapp2.WSGIApplication([ 331 | ('/', MainHandler), 332 | ('/update/?',UpdateHandler), 333 | ('/update_push/?',UpdatePushHandler), 334 | ], debug=True) 335 | 336 | 337 | 338 | 339 | e404 = """ 340 | 341 | 342 | 343 | 344 | 404 — Not Found 345 | 353 | 354 | 355 |
error 404:   not found
356 |
357 | This is not the page you're looking for.
Move along...
Move along...
358 |

359 |

If you wish to report a broken link, email me at nicholas.curr+webapps@gmail.com

360 | 361 | 362 | """ 363 | def handle_404(request, response, exception): 364 | response.write(e404) 365 | response.set_status(exception.status_int) 366 | app.error_handlers[404] = handle_404 367 | 368 | e500 = """ 369 | 370 | 371 | 372 | 373 | 500 — Internal Server Error 374 | 382 | 383 | 384 |
error 500:   internal server error
385 | 386 |

Something's wrong; that's my fault!

387 |

Please report this (email me at nicholas.curr+webapps@gmail.com) so that the error may be fixed. 388 |

389 | 390 | 391 | """ 392 | def handle_500(request, response, exception): 393 | response.clear() 394 | response.write(e500) 395 | app.error_handlers[500] = handle_500 396 | -------------------------------------------------------------------------------- /oauth2client/locked_file.py: -------------------------------------------------------------------------------- 1 | # Copyright 2011 Google Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Locked file interface that should work on Unix and Windows pythons. 16 | 17 | This module first tries to use fcntl locking to ensure serialized access 18 | to a file, then falls back on a lock file if that is unavialable. 19 | 20 | Usage: 21 | f = LockedFile('filename', 'r+b', 'rb') 22 | f.open_and_lock() 23 | if f.is_locked(): 24 | print 'Acquired filename with r+b mode' 25 | f.file_handle().write('locked data') 26 | else: 27 | print 'Aquired filename with rb mode' 28 | f.unlock_and_close() 29 | """ 30 | 31 | __author__ = 'cache@google.com (David T McWherter)' 32 | 33 | import errno 34 | import logging 35 | import os 36 | import time 37 | 38 | from oauth2client import util 39 | 40 | logger = logging.getLogger(__name__) 41 | 42 | 43 | class CredentialsFileSymbolicLinkError(Exception): 44 | """Credentials files must not be symbolic links.""" 45 | 46 | 47 | class AlreadyLockedException(Exception): 48 | """Trying to lock a file that has already been locked by the LockedFile.""" 49 | pass 50 | 51 | 52 | def validate_file(filename): 53 | if os.path.islink(filename): 54 | raise CredentialsFileSymbolicLinkError( 55 | 'File: %s is a symbolic link.' % filename) 56 | 57 | class _Opener(object): 58 | """Base class for different locking primitives.""" 59 | 60 | def __init__(self, filename, mode, fallback_mode): 61 | """Create an Opener. 62 | 63 | Args: 64 | filename: string, The pathname of the file. 65 | mode: string, The preferred mode to access the file with. 66 | fallback_mode: string, The mode to use if locking fails. 67 | """ 68 | self._locked = False 69 | self._filename = filename 70 | self._mode = mode 71 | self._fallback_mode = fallback_mode 72 | self._fh = None 73 | 74 | def is_locked(self): 75 | """Was the file locked.""" 76 | return self._locked 77 | 78 | def file_handle(self): 79 | """The file handle to the file. Valid only after opened.""" 80 | return self._fh 81 | 82 | def filename(self): 83 | """The filename that is being locked.""" 84 | return self._filename 85 | 86 | def open_and_lock(self, timeout, delay): 87 | """Open the file and lock it. 88 | 89 | Args: 90 | timeout: float, How long to try to lock for. 91 | delay: float, How long to wait between retries. 92 | """ 93 | pass 94 | 95 | def unlock_and_close(self): 96 | """Unlock and close the file.""" 97 | pass 98 | 99 | 100 | class _PosixOpener(_Opener): 101 | """Lock files using Posix advisory lock files.""" 102 | 103 | def open_and_lock(self, timeout, delay): 104 | """Open the file and lock it. 105 | 106 | Tries to create a .lock file next to the file we're trying to open. 107 | 108 | Args: 109 | timeout: float, How long to try to lock for. 110 | delay: float, How long to wait between retries. 111 | 112 | Raises: 113 | AlreadyLockedException: if the lock is already acquired. 114 | IOError: if the open fails. 115 | CredentialsFileSymbolicLinkError if the file is a symbolic link. 116 | """ 117 | if self._locked: 118 | raise AlreadyLockedException('File %s is already locked' % 119 | self._filename) 120 | self._locked = False 121 | 122 | validate_file(self._filename) 123 | try: 124 | self._fh = open(self._filename, self._mode) 125 | except IOError, e: 126 | # If we can't access with _mode, try _fallback_mode and don't lock. 127 | if e.errno == errno.EACCES: 128 | self._fh = open(self._filename, self._fallback_mode) 129 | return 130 | 131 | lock_filename = self._posix_lockfile(self._filename) 132 | start_time = time.time() 133 | while True: 134 | try: 135 | self._lock_fd = os.open(lock_filename, 136 | os.O_CREAT|os.O_EXCL|os.O_RDWR) 137 | self._locked = True 138 | break 139 | 140 | except OSError, e: 141 | if e.errno != errno.EEXIST: 142 | raise 143 | if (time.time() - start_time) >= timeout: 144 | logger.warn('Could not acquire lock %s in %s seconds' % ( 145 | lock_filename, timeout)) 146 | # Close the file and open in fallback_mode. 147 | if self._fh: 148 | self._fh.close() 149 | self._fh = open(self._filename, self._fallback_mode) 150 | return 151 | time.sleep(delay) 152 | 153 | def unlock_and_close(self): 154 | """Unlock a file by removing the .lock file, and close the handle.""" 155 | if self._locked: 156 | lock_filename = self._posix_lockfile(self._filename) 157 | os.close(self._lock_fd) 158 | os.unlink(lock_filename) 159 | self._locked = False 160 | self._lock_fd = None 161 | if self._fh: 162 | self._fh.close() 163 | 164 | def _posix_lockfile(self, filename): 165 | """The name of the lock file to use for posix locking.""" 166 | return '%s.lock' % filename 167 | 168 | 169 | try: 170 | import fcntl 171 | 172 | class _FcntlOpener(_Opener): 173 | """Open, lock, and unlock a file using fcntl.lockf.""" 174 | 175 | def open_and_lock(self, timeout, delay): 176 | """Open the file and lock it. 177 | 178 | Args: 179 | timeout: float, How long to try to lock for. 180 | delay: float, How long to wait between retries 181 | 182 | Raises: 183 | AlreadyLockedException: if the lock is already acquired. 184 | IOError: if the open fails. 185 | CredentialsFileSymbolicLinkError if the file is a symbolic link. 186 | """ 187 | if self._locked: 188 | raise AlreadyLockedException('File %s is already locked' % 189 | self._filename) 190 | start_time = time.time() 191 | 192 | validate_file(self._filename) 193 | try: 194 | self._fh = open(self._filename, self._mode) 195 | except IOError, e: 196 | # If we can't access with _mode, try _fallback_mode and don't lock. 197 | if e.errno == errno.EACCES: 198 | self._fh = open(self._filename, self._fallback_mode) 199 | return 200 | 201 | # We opened in _mode, try to lock the file. 202 | while True: 203 | try: 204 | fcntl.lockf(self._fh.fileno(), fcntl.LOCK_EX) 205 | self._locked = True 206 | return 207 | except IOError, e: 208 | # If not retrying, then just pass on the error. 209 | if timeout == 0: 210 | raise e 211 | if e.errno != errno.EACCES: 212 | raise e 213 | # We could not acquire the lock. Try again. 214 | if (time.time() - start_time) >= timeout: 215 | logger.warn('Could not lock %s in %s seconds' % ( 216 | self._filename, timeout)) 217 | if self._fh: 218 | self._fh.close() 219 | self._fh = open(self._filename, self._fallback_mode) 220 | return 221 | time.sleep(delay) 222 | 223 | def unlock_and_close(self): 224 | """Close and unlock the file using the fcntl.lockf primitive.""" 225 | if self._locked: 226 | fcntl.lockf(self._fh.fileno(), fcntl.LOCK_UN) 227 | self._locked = False 228 | if self._fh: 229 | self._fh.close() 230 | except ImportError: 231 | _FcntlOpener = None 232 | 233 | 234 | try: 235 | import pywintypes 236 | import win32con 237 | import win32file 238 | 239 | class _Win32Opener(_Opener): 240 | """Open, lock, and unlock a file using windows primitives.""" 241 | 242 | # Error #33: 243 | # 'The process cannot access the file because another process' 244 | FILE_IN_USE_ERROR = 33 245 | 246 | # Error #158: 247 | # 'The segment is already unlocked.' 248 | FILE_ALREADY_UNLOCKED_ERROR = 158 249 | 250 | def open_and_lock(self, timeout, delay): 251 | """Open the file and lock it. 252 | 253 | Args: 254 | timeout: float, How long to try to lock for. 255 | delay: float, How long to wait between retries 256 | 257 | Raises: 258 | AlreadyLockedException: if the lock is already acquired. 259 | IOError: if the open fails. 260 | CredentialsFileSymbolicLinkError if the file is a symbolic link. 261 | """ 262 | if self._locked: 263 | raise AlreadyLockedException('File %s is already locked' % 264 | self._filename) 265 | start_time = time.time() 266 | 267 | validate_file(self._filename) 268 | try: 269 | self._fh = open(self._filename, self._mode) 270 | except IOError, e: 271 | # If we can't access with _mode, try _fallback_mode and don't lock. 272 | if e.errno == errno.EACCES: 273 | self._fh = open(self._filename, self._fallback_mode) 274 | return 275 | 276 | # We opened in _mode, try to lock the file. 277 | while True: 278 | try: 279 | hfile = win32file._get_osfhandle(self._fh.fileno()) 280 | win32file.LockFileEx( 281 | hfile, 282 | (win32con.LOCKFILE_FAIL_IMMEDIATELY| 283 | win32con.LOCKFILE_EXCLUSIVE_LOCK), 0, -0x10000, 284 | pywintypes.OVERLAPPED()) 285 | self._locked = True 286 | return 287 | except pywintypes.error, e: 288 | if timeout == 0: 289 | raise e 290 | 291 | # If the error is not that the file is already in use, raise. 292 | if e[0] != _Win32Opener.FILE_IN_USE_ERROR: 293 | raise 294 | 295 | # We could not acquire the lock. Try again. 296 | if (time.time() - start_time) >= timeout: 297 | logger.warn('Could not lock %s in %s seconds' % ( 298 | self._filename, timeout)) 299 | if self._fh: 300 | self._fh.close() 301 | self._fh = open(self._filename, self._fallback_mode) 302 | return 303 | time.sleep(delay) 304 | 305 | def unlock_and_close(self): 306 | """Close and unlock the file using the win32 primitive.""" 307 | if self._locked: 308 | try: 309 | hfile = win32file._get_osfhandle(self._fh.fileno()) 310 | win32file.UnlockFileEx(hfile, 0, -0x10000, pywintypes.OVERLAPPED()) 311 | except pywintypes.error, e: 312 | if e[0] != _Win32Opener.FILE_ALREADY_UNLOCKED_ERROR: 313 | raise 314 | self._locked = False 315 | if self._fh: 316 | self._fh.close() 317 | except ImportError: 318 | _Win32Opener = None 319 | 320 | 321 | class LockedFile(object): 322 | """Represent a file that has exclusive access.""" 323 | 324 | @util.positional(4) 325 | def __init__(self, filename, mode, fallback_mode, use_native_locking=True): 326 | """Construct a LockedFile. 327 | 328 | Args: 329 | filename: string, The path of the file to open. 330 | mode: string, The mode to try to open the file with. 331 | fallback_mode: string, The mode to use if locking fails. 332 | use_native_locking: bool, Whether or not fcntl/win32 locking is used. 333 | """ 334 | opener = None 335 | if not opener and use_native_locking: 336 | if _Win32Opener: 337 | opener = _Win32Opener(filename, mode, fallback_mode) 338 | if _FcntlOpener: 339 | opener = _FcntlOpener(filename, mode, fallback_mode) 340 | 341 | if not opener: 342 | opener = _PosixOpener(filename, mode, fallback_mode) 343 | 344 | self._opener = opener 345 | 346 | def filename(self): 347 | """Return the filename we were constructed with.""" 348 | return self._opener._filename 349 | 350 | def file_handle(self): 351 | """Return the file_handle to the opened file.""" 352 | return self._opener.file_handle() 353 | 354 | def is_locked(self): 355 | """Return whether we successfully locked the file.""" 356 | return self._opener.is_locked() 357 | 358 | def open_and_lock(self, timeout=0, delay=0.05): 359 | """Open the file, trying to lock it. 360 | 361 | Args: 362 | timeout: float, The number of seconds to try to acquire the lock. 363 | delay: float, The number of seconds to wait between retry attempts. 364 | 365 | Raises: 366 | AlreadyLockedException: if the lock is already acquired. 367 | IOError: if the open fails. 368 | """ 369 | self._opener.open_and_lock(timeout, delay) 370 | 371 | def unlock_and_close(self): 372 | """Unlock and close a file.""" 373 | self._opener.unlock_and_close() 374 | -------------------------------------------------------------------------------- /apiclient/model.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2.4 2 | # 3 | # Copyright (C) 2010 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | """Model objects for requests and responses. 18 | 19 | Each API may support one or more serializations, such 20 | as JSON, Atom, etc. The model classes are responsible 21 | for converting between the wire format and the Python 22 | object representation. 23 | """ 24 | 25 | __author__ = 'jcgregorio@google.com (Joe Gregorio)' 26 | 27 | import logging 28 | import urllib 29 | 30 | from apiclient import __version__ 31 | from errors import HttpError 32 | from oauth2client.anyjson import simplejson 33 | 34 | 35 | dump_request_response = False 36 | 37 | 38 | def _abstract(): 39 | raise NotImplementedError('You need to override this function') 40 | 41 | 42 | class Model(object): 43 | """Model base class. 44 | 45 | All Model classes should implement this interface. 46 | The Model serializes and de-serializes between a wire 47 | format such as JSON and a Python object representation. 48 | """ 49 | 50 | def request(self, headers, path_params, query_params, body_value): 51 | """Updates outgoing requests with a serialized body. 52 | 53 | Args: 54 | headers: dict, request headers 55 | path_params: dict, parameters that appear in the request path 56 | query_params: dict, parameters that appear in the query 57 | body_value: object, the request body as a Python object, which must be 58 | serializable. 59 | Returns: 60 | A tuple of (headers, path_params, query, body) 61 | 62 | headers: dict, request headers 63 | path_params: dict, parameters that appear in the request path 64 | query: string, query part of the request URI 65 | body: string, the body serialized in the desired wire format. 66 | """ 67 | _abstract() 68 | 69 | def response(self, resp, content): 70 | """Convert the response wire format into a Python object. 71 | 72 | Args: 73 | resp: httplib2.Response, the HTTP response headers and status 74 | content: string, the body of the HTTP response 75 | 76 | Returns: 77 | The body de-serialized as a Python object. 78 | 79 | Raises: 80 | apiclient.errors.HttpError if a non 2xx response is received. 81 | """ 82 | _abstract() 83 | 84 | 85 | class BaseModel(Model): 86 | """Base model class. 87 | 88 | Subclasses should provide implementations for the "serialize" and 89 | "deserialize" methods, as well as values for the following class attributes. 90 | 91 | Attributes: 92 | accept: The value to use for the HTTP Accept header. 93 | content_type: The value to use for the HTTP Content-type header. 94 | no_content_response: The value to return when deserializing a 204 "No 95 | Content" response. 96 | alt_param: The value to supply as the "alt" query parameter for requests. 97 | """ 98 | 99 | accept = None 100 | content_type = None 101 | no_content_response = None 102 | alt_param = None 103 | 104 | def _log_request(self, headers, path_params, query, body): 105 | """Logs debugging information about the request if requested.""" 106 | if dump_request_response: 107 | logging.info('--request-start--') 108 | logging.info('-headers-start-') 109 | for h, v in headers.iteritems(): 110 | logging.info('%s: %s', h, v) 111 | logging.info('-headers-end-') 112 | logging.info('-path-parameters-start-') 113 | for h, v in path_params.iteritems(): 114 | logging.info('%s: %s', h, v) 115 | logging.info('-path-parameters-end-') 116 | logging.info('body: %s', body) 117 | logging.info('query: %s', query) 118 | logging.info('--request-end--') 119 | 120 | def request(self, headers, path_params, query_params, body_value): 121 | """Updates outgoing requests with a serialized body. 122 | 123 | Args: 124 | headers: dict, request headers 125 | path_params: dict, parameters that appear in the request path 126 | query_params: dict, parameters that appear in the query 127 | body_value: object, the request body as a Python object, which must be 128 | serializable by simplejson. 129 | Returns: 130 | A tuple of (headers, path_params, query, body) 131 | 132 | headers: dict, request headers 133 | path_params: dict, parameters that appear in the request path 134 | query: string, query part of the request URI 135 | body: string, the body serialized as JSON 136 | """ 137 | query = self._build_query(query_params) 138 | headers['accept'] = self.accept 139 | headers['accept-encoding'] = 'gzip, deflate' 140 | if 'user-agent' in headers: 141 | headers['user-agent'] += ' ' 142 | else: 143 | headers['user-agent'] = '' 144 | headers['user-agent'] += 'google-api-python-client/%s (gzip)' % __version__ 145 | 146 | if body_value is not None: 147 | headers['content-type'] = self.content_type 148 | body_value = self.serialize(body_value) 149 | self._log_request(headers, path_params, query, body_value) 150 | return (headers, path_params, query, body_value) 151 | 152 | def _build_query(self, params): 153 | """Builds a query string. 154 | 155 | Args: 156 | params: dict, the query parameters 157 | 158 | Returns: 159 | The query parameters properly encoded into an HTTP URI query string. 160 | """ 161 | if self.alt_param is not None: 162 | params.update({'alt': self.alt_param}) 163 | astuples = [] 164 | for key, value in params.iteritems(): 165 | if type(value) == type([]): 166 | for x in value: 167 | x = x.encode('utf-8') 168 | astuples.append((key, x)) 169 | else: 170 | if getattr(value, 'encode', False) and callable(value.encode): 171 | value = value.encode('utf-8') 172 | astuples.append((key, value)) 173 | return '?' + urllib.urlencode(astuples) 174 | 175 | def _log_response(self, resp, content): 176 | """Logs debugging information about the response if requested.""" 177 | if dump_request_response: 178 | logging.info('--response-start--') 179 | for h, v in resp.iteritems(): 180 | logging.info('%s: %s', h, v) 181 | if content: 182 | logging.info(content) 183 | logging.info('--response-end--') 184 | 185 | def response(self, resp, content): 186 | """Convert the response wire format into a Python object. 187 | 188 | Args: 189 | resp: httplib2.Response, the HTTP response headers and status 190 | content: string, the body of the HTTP response 191 | 192 | Returns: 193 | The body de-serialized as a Python object. 194 | 195 | Raises: 196 | apiclient.errors.HttpError if a non 2xx response is received. 197 | """ 198 | self._log_response(resp, content) 199 | # Error handling is TBD, for example, do we retry 200 | # for some operation/error combinations? 201 | if resp.status < 300: 202 | if resp.status == 204: 203 | # A 204: No Content response should be treated differently 204 | # to all the other success states 205 | return self.no_content_response 206 | return self.deserialize(content) 207 | else: 208 | logging.debug('Content from bad request was: %s' % content) 209 | raise HttpError(resp, content) 210 | 211 | def serialize(self, body_value): 212 | """Perform the actual Python object serialization. 213 | 214 | Args: 215 | body_value: object, the request body as a Python object. 216 | 217 | Returns: 218 | string, the body in serialized form. 219 | """ 220 | _abstract() 221 | 222 | def deserialize(self, content): 223 | """Perform the actual deserialization from response string to Python 224 | object. 225 | 226 | Args: 227 | content: string, the body of the HTTP response 228 | 229 | Returns: 230 | The body de-serialized as a Python object. 231 | """ 232 | _abstract() 233 | 234 | 235 | class JsonModel(BaseModel): 236 | """Model class for JSON. 237 | 238 | Serializes and de-serializes between JSON and the Python 239 | object representation of HTTP request and response bodies. 240 | """ 241 | accept = 'application/json' 242 | content_type = 'application/json' 243 | alt_param = 'json' 244 | 245 | def __init__(self, data_wrapper=False): 246 | """Construct a JsonModel. 247 | 248 | Args: 249 | data_wrapper: boolean, wrap requests and responses in a data wrapper 250 | """ 251 | self._data_wrapper = data_wrapper 252 | 253 | def serialize(self, body_value): 254 | if (isinstance(body_value, dict) and 'data' not in body_value and 255 | self._data_wrapper): 256 | body_value = {'data': body_value} 257 | return simplejson.dumps(body_value) 258 | 259 | def deserialize(self, content): 260 | content = content.decode('utf-8') 261 | body = simplejson.loads(content) 262 | if self._data_wrapper and isinstance(body, dict) and 'data' in body: 263 | body = body['data'] 264 | return body 265 | 266 | @property 267 | def no_content_response(self): 268 | return {} 269 | 270 | 271 | class RawModel(JsonModel): 272 | """Model class for requests that don't return JSON. 273 | 274 | Serializes and de-serializes between JSON and the Python 275 | object representation of HTTP request, and returns the raw bytes 276 | of the response body. 277 | """ 278 | accept = '*/*' 279 | content_type = 'application/json' 280 | alt_param = None 281 | 282 | def deserialize(self, content): 283 | return content 284 | 285 | @property 286 | def no_content_response(self): 287 | return '' 288 | 289 | 290 | class MediaModel(JsonModel): 291 | """Model class for requests that return Media. 292 | 293 | Serializes and de-serializes between JSON and the Python 294 | object representation of HTTP request, and returns the raw bytes 295 | of the response body. 296 | """ 297 | accept = '*/*' 298 | content_type = 'application/json' 299 | alt_param = 'media' 300 | 301 | def deserialize(self, content): 302 | return content 303 | 304 | @property 305 | def no_content_response(self): 306 | return '' 307 | 308 | 309 | class ProtocolBufferModel(BaseModel): 310 | """Model class for protocol buffers. 311 | 312 | Serializes and de-serializes the binary protocol buffer sent in the HTTP 313 | request and response bodies. 314 | """ 315 | accept = 'application/x-protobuf' 316 | content_type = 'application/x-protobuf' 317 | alt_param = 'proto' 318 | 319 | def __init__(self, protocol_buffer): 320 | """Constructs a ProtocolBufferModel. 321 | 322 | The serialzed protocol buffer returned in an HTTP response will be 323 | de-serialized using the given protocol buffer class. 324 | 325 | Args: 326 | protocol_buffer: The protocol buffer class used to de-serialize a 327 | response from the API. 328 | """ 329 | self._protocol_buffer = protocol_buffer 330 | 331 | def serialize(self, body_value): 332 | return body_value.SerializeToString() 333 | 334 | def deserialize(self, content): 335 | return self._protocol_buffer.FromString(content) 336 | 337 | @property 338 | def no_content_response(self): 339 | return self._protocol_buffer() 340 | 341 | 342 | def makepatch(original, modified): 343 | """Create a patch object. 344 | 345 | Some methods support PATCH, an efficient way to send updates to a resource. 346 | This method allows the easy construction of patch bodies by looking at the 347 | differences between a resource before and after it was modified. 348 | 349 | Args: 350 | original: object, the original deserialized resource 351 | modified: object, the modified deserialized resource 352 | Returns: 353 | An object that contains only the changes from original to modified, in a 354 | form suitable to pass to a PATCH method. 355 | 356 | Example usage: 357 | item = service.activities().get(postid=postid, userid=userid).execute() 358 | original = copy.deepcopy(item) 359 | item['object']['content'] = 'This is updated.' 360 | service.activities.patch(postid=postid, userid=userid, 361 | body=makepatch(original, item)).execute() 362 | """ 363 | patch = {} 364 | for key, original_value in original.iteritems(): 365 | modified_value = modified.get(key, None) 366 | if modified_value is None: 367 | # Use None to signal that the element is deleted 368 | patch[key] = None 369 | elif original_value != modified_value: 370 | if type(original_value) == type({}): 371 | # Recursively descend objects 372 | patch[key] = makepatch(original_value, modified_value) 373 | else: 374 | # In the case of simple types or arrays we just replace 375 | patch[key] = modified_value 376 | else: 377 | # Don't add anything to patch if there's no change 378 | pass 379 | for key in modified: 380 | if key not in original: 381 | patch[key] = modified[key] 382 | 383 | return patch 384 | -------------------------------------------------------------------------------- /oauth2client/multistore_file.py: -------------------------------------------------------------------------------- 1 | # Copyright 2011 Google Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Multi-credential file store with lock support. 16 | 17 | This module implements a JSON credential store where multiple 18 | credentials can be stored in one file. That file supports locking 19 | both in a single process and across processes. 20 | 21 | The credential themselves are keyed off of: 22 | * client_id 23 | * user_agent 24 | * scope 25 | 26 | The format of the stored data is like so: 27 | { 28 | 'file_version': 1, 29 | 'data': [ 30 | { 31 | 'key': { 32 | 'clientId': '', 33 | 'userAgent': '', 34 | 'scope': '' 35 | }, 36 | 'credential': { 37 | # JSON serialized Credentials. 38 | } 39 | } 40 | ] 41 | } 42 | """ 43 | 44 | __author__ = 'jbeda@google.com (Joe Beda)' 45 | 46 | import base64 47 | import errno 48 | import logging 49 | import os 50 | import threading 51 | 52 | from anyjson import simplejson 53 | from oauth2client.client import Storage as BaseStorage 54 | from oauth2client.client import Credentials 55 | from oauth2client import util 56 | from locked_file import LockedFile 57 | 58 | logger = logging.getLogger(__name__) 59 | 60 | # A dict from 'filename'->_MultiStore instances 61 | _multistores = {} 62 | _multistores_lock = threading.Lock() 63 | 64 | 65 | class Error(Exception): 66 | """Base error for this module.""" 67 | pass 68 | 69 | 70 | class NewerCredentialStoreError(Error): 71 | """The credential store is a newer version that supported.""" 72 | pass 73 | 74 | 75 | @util.positional(4) 76 | def get_credential_storage(filename, client_id, user_agent, scope, 77 | warn_on_readonly=True): 78 | """Get a Storage instance for a credential. 79 | 80 | Args: 81 | filename: The JSON file storing a set of credentials 82 | client_id: The client_id for the credential 83 | user_agent: The user agent for the credential 84 | scope: string or iterable of strings, Scope(s) being requested 85 | warn_on_readonly: if True, log a warning if the store is readonly 86 | 87 | Returns: 88 | An object derived from client.Storage for getting/setting the 89 | credential. 90 | """ 91 | # Recreate the legacy key with these specific parameters 92 | key = {'clientId': client_id, 'userAgent': user_agent, 93 | 'scope': util.scopes_to_string(scope)} 94 | return get_credential_storage_custom_key( 95 | filename, key, warn_on_readonly=warn_on_readonly) 96 | 97 | 98 | @util.positional(2) 99 | def get_credential_storage_custom_string_key( 100 | filename, key_string, warn_on_readonly=True): 101 | """Get a Storage instance for a credential using a single string as a key. 102 | 103 | Allows you to provide a string as a custom key that will be used for 104 | credential storage and retrieval. 105 | 106 | Args: 107 | filename: The JSON file storing a set of credentials 108 | key_string: A string to use as the key for storing this credential. 109 | warn_on_readonly: if True, log a warning if the store is readonly 110 | 111 | Returns: 112 | An object derived from client.Storage for getting/setting the 113 | credential. 114 | """ 115 | # Create a key dictionary that can be used 116 | key_dict = {'key': key_string} 117 | return get_credential_storage_custom_key( 118 | filename, key_dict, warn_on_readonly=warn_on_readonly) 119 | 120 | 121 | @util.positional(2) 122 | def get_credential_storage_custom_key( 123 | filename, key_dict, warn_on_readonly=True): 124 | """Get a Storage instance for a credential using a dictionary as a key. 125 | 126 | Allows you to provide a dictionary as a custom key that will be used for 127 | credential storage and retrieval. 128 | 129 | Args: 130 | filename: The JSON file storing a set of credentials 131 | key_dict: A dictionary to use as the key for storing this credential. There 132 | is no ordering of the keys in the dictionary. Logically equivalent 133 | dictionaries will produce equivalent storage keys. 134 | warn_on_readonly: if True, log a warning if the store is readonly 135 | 136 | Returns: 137 | An object derived from client.Storage for getting/setting the 138 | credential. 139 | """ 140 | multistore = _get_multistore(filename, warn_on_readonly=warn_on_readonly) 141 | key = util.dict_to_tuple_key(key_dict) 142 | return multistore._get_storage(key) 143 | 144 | 145 | @util.positional(1) 146 | def get_all_credential_keys(filename, warn_on_readonly=True): 147 | """Gets all the registered credential keys in the given Multistore. 148 | 149 | Args: 150 | filename: The JSON file storing a set of credentials 151 | warn_on_readonly: if True, log a warning if the store is readonly 152 | 153 | Returns: 154 | A list of the credential keys present in the file. They are returned as 155 | dictionaries that can be passed into get_credential_storage_custom_key to 156 | get the actual credentials. 157 | """ 158 | multistore = _get_multistore(filename, warn_on_readonly=warn_on_readonly) 159 | multistore._lock() 160 | try: 161 | return multistore._get_all_credential_keys() 162 | finally: 163 | multistore._unlock() 164 | 165 | 166 | @util.positional(1) 167 | def _get_multistore(filename, warn_on_readonly=True): 168 | """A helper method to initialize the multistore with proper locking. 169 | 170 | Args: 171 | filename: The JSON file storing a set of credentials 172 | warn_on_readonly: if True, log a warning if the store is readonly 173 | 174 | Returns: 175 | A multistore object 176 | """ 177 | filename = os.path.expanduser(filename) 178 | _multistores_lock.acquire() 179 | try: 180 | multistore = _multistores.setdefault( 181 | filename, _MultiStore(filename, warn_on_readonly=warn_on_readonly)) 182 | finally: 183 | _multistores_lock.release() 184 | return multistore 185 | 186 | 187 | class _MultiStore(object): 188 | """A file backed store for multiple credentials.""" 189 | 190 | @util.positional(2) 191 | def __init__(self, filename, warn_on_readonly=True): 192 | """Initialize the class. 193 | 194 | This will create the file if necessary. 195 | """ 196 | self._file = LockedFile(filename, 'r+b', 'rb') 197 | self._thread_lock = threading.Lock() 198 | self._read_only = False 199 | self._warn_on_readonly = warn_on_readonly 200 | 201 | self._create_file_if_needed() 202 | 203 | # Cache of deserialized store. This is only valid after the 204 | # _MultiStore is locked or _refresh_data_cache is called. This is 205 | # of the form of: 206 | # 207 | # ((key, value), (key, value)...) -> OAuth2Credential 208 | # 209 | # If this is None, then the store hasn't been read yet. 210 | self._data = None 211 | 212 | class _Storage(BaseStorage): 213 | """A Storage object that knows how to read/write a single credential.""" 214 | 215 | def __init__(self, multistore, key): 216 | self._multistore = multistore 217 | self._key = key 218 | 219 | def acquire_lock(self): 220 | """Acquires any lock necessary to access this Storage. 221 | 222 | This lock is not reentrant. 223 | """ 224 | self._multistore._lock() 225 | 226 | def release_lock(self): 227 | """Release the Storage lock. 228 | 229 | Trying to release a lock that isn't held will result in a 230 | RuntimeError. 231 | """ 232 | self._multistore._unlock() 233 | 234 | def locked_get(self): 235 | """Retrieve credential. 236 | 237 | The Storage lock must be held when this is called. 238 | 239 | Returns: 240 | oauth2client.client.Credentials 241 | """ 242 | credential = self._multistore._get_credential(self._key) 243 | if credential: 244 | credential.set_store(self) 245 | return credential 246 | 247 | def locked_put(self, credentials): 248 | """Write a credential. 249 | 250 | The Storage lock must be held when this is called. 251 | 252 | Args: 253 | credentials: Credentials, the credentials to store. 254 | """ 255 | self._multistore._update_credential(self._key, credentials) 256 | 257 | def locked_delete(self): 258 | """Delete a credential. 259 | 260 | The Storage lock must be held when this is called. 261 | 262 | Args: 263 | credentials: Credentials, the credentials to store. 264 | """ 265 | self._multistore._delete_credential(self._key) 266 | 267 | def _create_file_if_needed(self): 268 | """Create an empty file if necessary. 269 | 270 | This method will not initialize the file. Instead it implements a 271 | simple version of "touch" to ensure the file has been created. 272 | """ 273 | if not os.path.exists(self._file.filename()): 274 | old_umask = os.umask(0177) 275 | try: 276 | open(self._file.filename(), 'a+b').close() 277 | finally: 278 | os.umask(old_umask) 279 | 280 | def _lock(self): 281 | """Lock the entire multistore.""" 282 | self._thread_lock.acquire() 283 | self._file.open_and_lock() 284 | if not self._file.is_locked(): 285 | self._read_only = True 286 | if self._warn_on_readonly: 287 | logger.warn('The credentials file (%s) is not writable. Opening in ' 288 | 'read-only mode. Any refreshed credentials will only be ' 289 | 'valid for this run.' % self._file.filename()) 290 | if os.path.getsize(self._file.filename()) == 0: 291 | logger.debug('Initializing empty multistore file') 292 | # The multistore is empty so write out an empty file. 293 | self._data = {} 294 | self._write() 295 | elif not self._read_only or self._data is None: 296 | # Only refresh the data if we are read/write or we haven't 297 | # cached the data yet. If we are readonly, we assume is isn't 298 | # changing out from under us and that we only have to read it 299 | # once. This prevents us from whacking any new access keys that 300 | # we have cached in memory but were unable to write out. 301 | self._refresh_data_cache() 302 | 303 | def _unlock(self): 304 | """Release the lock on the multistore.""" 305 | self._file.unlock_and_close() 306 | self._thread_lock.release() 307 | 308 | def _locked_json_read(self): 309 | """Get the raw content of the multistore file. 310 | 311 | The multistore must be locked when this is called. 312 | 313 | Returns: 314 | The contents of the multistore decoded as JSON. 315 | """ 316 | assert self._thread_lock.locked() 317 | self._file.file_handle().seek(0) 318 | return simplejson.load(self._file.file_handle()) 319 | 320 | def _locked_json_write(self, data): 321 | """Write a JSON serializable data structure to the multistore. 322 | 323 | The multistore must be locked when this is called. 324 | 325 | Args: 326 | data: The data to be serialized and written. 327 | """ 328 | assert self._thread_lock.locked() 329 | if self._read_only: 330 | return 331 | self._file.file_handle().seek(0) 332 | simplejson.dump(data, self._file.file_handle(), sort_keys=True, indent=2) 333 | self._file.file_handle().truncate() 334 | 335 | def _refresh_data_cache(self): 336 | """Refresh the contents of the multistore. 337 | 338 | The multistore must be locked when this is called. 339 | 340 | Raises: 341 | NewerCredentialStoreError: Raised when a newer client has written the 342 | store. 343 | """ 344 | self._data = {} 345 | try: 346 | raw_data = self._locked_json_read() 347 | except Exception: 348 | logger.warn('Credential data store could not be loaded. ' 349 | 'Will ignore and overwrite.') 350 | return 351 | 352 | version = 0 353 | try: 354 | version = raw_data['file_version'] 355 | except Exception: 356 | logger.warn('Missing version for credential data store. It may be ' 357 | 'corrupt or an old version. Overwriting.') 358 | if version > 1: 359 | raise NewerCredentialStoreError( 360 | 'Credential file has file_version of %d. ' 361 | 'Only file_version of 1 is supported.' % version) 362 | 363 | credentials = [] 364 | try: 365 | credentials = raw_data['data'] 366 | except (TypeError, KeyError): 367 | pass 368 | 369 | for cred_entry in credentials: 370 | try: 371 | (key, credential) = self._decode_credential_from_json(cred_entry) 372 | self._data[key] = credential 373 | except: 374 | # If something goes wrong loading a credential, just ignore it 375 | logger.info('Error decoding credential, skipping', exc_info=True) 376 | 377 | def _decode_credential_from_json(self, cred_entry): 378 | """Load a credential from our JSON serialization. 379 | 380 | Args: 381 | cred_entry: A dict entry from the data member of our format 382 | 383 | Returns: 384 | (key, cred) where the key is the key tuple and the cred is the 385 | OAuth2Credential object. 386 | """ 387 | raw_key = cred_entry['key'] 388 | key = util.dict_to_tuple_key(raw_key) 389 | credential = None 390 | credential = Credentials.new_from_json(simplejson.dumps(cred_entry['credential'])) 391 | return (key, credential) 392 | 393 | def _write(self): 394 | """Write the cached data back out. 395 | 396 | The multistore must be locked. 397 | """ 398 | raw_data = {'file_version': 1} 399 | raw_creds = [] 400 | raw_data['data'] = raw_creds 401 | for (cred_key, cred) in self._data.items(): 402 | raw_key = dict(cred_key) 403 | raw_cred = simplejson.loads(cred.to_json()) 404 | raw_creds.append({'key': raw_key, 'credential': raw_cred}) 405 | self._locked_json_write(raw_data) 406 | 407 | def _get_all_credential_keys(self): 408 | """Gets all the registered credential keys in the multistore. 409 | 410 | Returns: 411 | A list of dictionaries corresponding to all the keys currently registered 412 | """ 413 | return [dict(key) for key in self._data.keys()] 414 | 415 | def _get_credential(self, key): 416 | """Get a credential from the multistore. 417 | 418 | The multistore must be locked. 419 | 420 | Args: 421 | key: The key used to retrieve the credential 422 | 423 | Returns: 424 | The credential specified or None if not present 425 | """ 426 | return self._data.get(key, None) 427 | 428 | def _update_credential(self, key, cred): 429 | """Update a credential and write the multistore. 430 | 431 | This must be called when the multistore is locked. 432 | 433 | Args: 434 | key: The key used to retrieve the credential 435 | cred: The OAuth2Credential to update/set 436 | """ 437 | self._data[key] = cred 438 | self._write() 439 | 440 | def _delete_credential(self, key): 441 | """Delete a credential and write the multistore. 442 | 443 | This must be called when the multistore is locked. 444 | 445 | Args: 446 | key: The key used to retrieve the credential 447 | """ 448 | try: 449 | del self._data[key] 450 | except KeyError: 451 | pass 452 | self._write() 453 | 454 | def _get_storage(self, key): 455 | """Get a Storage object to get/set a credential. 456 | 457 | This Storage is a 'view' into the multistore. 458 | 459 | Args: 460 | key: The key used to retrieve the credential 461 | 462 | Returns: 463 | A Storage object that can be used to get/set this cred 464 | """ 465 | return self._Storage(self, key) 466 | -------------------------------------------------------------------------------- /httplib2/socks.py: -------------------------------------------------------------------------------- 1 | """SocksiPy - Python SOCKS module. 2 | Version 1.00 3 | 4 | Copyright 2006 Dan-Haim. All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without modification, 7 | are permitted provided that the following conditions are met: 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 3. Neither the name of Dan Haim nor the names of his contributors may be used 14 | to endorse or promote products derived from this software without specific 15 | prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY DAN HAIM "AS IS" AND ANY EXPRESS OR IMPLIED 18 | WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 19 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO 20 | EVENT SHALL DAN HAIM OR HIS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 21 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 22 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA 23 | OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 24 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT 25 | OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMANGE. 26 | 27 | 28 | This module provides a standard socket-like interface for Python 29 | for tunneling connections through SOCKS proxies. 30 | 31 | """ 32 | 33 | """ 34 | 35 | Minor modifications made by Christopher Gilbert (http://motomastyle.com/) 36 | for use in PyLoris (http://pyloris.sourceforge.net/) 37 | 38 | Minor modifications made by Mario Vilas (http://breakingcode.wordpress.com/) 39 | mainly to merge bug fixes found in Sourceforge 40 | 41 | """ 42 | 43 | import base64 44 | import socket 45 | import struct 46 | import sys 47 | 48 | if getattr(socket, 'socket', None) is None: 49 | raise ImportError('socket.socket missing, proxy support unusable') 50 | 51 | PROXY_TYPE_SOCKS4 = 1 52 | PROXY_TYPE_SOCKS5 = 2 53 | PROXY_TYPE_HTTP = 3 54 | PROXY_TYPE_HTTP_NO_TUNNEL = 4 55 | 56 | _defaultproxy = None 57 | _orgsocket = socket.socket 58 | 59 | class ProxyError(Exception): pass 60 | class GeneralProxyError(ProxyError): pass 61 | class Socks5AuthError(ProxyError): pass 62 | class Socks5Error(ProxyError): pass 63 | class Socks4Error(ProxyError): pass 64 | class HTTPError(ProxyError): pass 65 | 66 | _generalerrors = ("success", 67 | "invalid data", 68 | "not connected", 69 | "not available", 70 | "bad proxy type", 71 | "bad input") 72 | 73 | _socks5errors = ("succeeded", 74 | "general SOCKS server failure", 75 | "connection not allowed by ruleset", 76 | "Network unreachable", 77 | "Host unreachable", 78 | "Connection refused", 79 | "TTL expired", 80 | "Command not supported", 81 | "Address type not supported", 82 | "Unknown error") 83 | 84 | _socks5autherrors = ("succeeded", 85 | "authentication is required", 86 | "all offered authentication methods were rejected", 87 | "unknown username or invalid password", 88 | "unknown error") 89 | 90 | _socks4errors = ("request granted", 91 | "request rejected or failed", 92 | "request rejected because SOCKS server cannot connect to identd on the client", 93 | "request rejected because the client program and identd report different user-ids", 94 | "unknown error") 95 | 96 | def setdefaultproxy(proxytype=None, addr=None, port=None, rdns=True, username=None, password=None): 97 | """setdefaultproxy(proxytype, addr[, port[, rdns[, username[, password]]]]) 98 | Sets a default proxy which all further socksocket objects will use, 99 | unless explicitly changed. 100 | """ 101 | global _defaultproxy 102 | _defaultproxy = (proxytype, addr, port, rdns, username, password) 103 | 104 | def wrapmodule(module): 105 | """wrapmodule(module) 106 | Attempts to replace a module's socket library with a SOCKS socket. Must set 107 | a default proxy using setdefaultproxy(...) first. 108 | This will only work on modules that import socket directly into the namespace; 109 | most of the Python Standard Library falls into this category. 110 | """ 111 | if _defaultproxy != None: 112 | module.socket.socket = socksocket 113 | else: 114 | raise GeneralProxyError((4, "no proxy specified")) 115 | 116 | class socksocket(socket.socket): 117 | """socksocket([family[, type[, proto]]]) -> socket object 118 | Open a SOCKS enabled socket. The parameters are the same as 119 | those of the standard socket init. In order for SOCKS to work, 120 | you must specify family=AF_INET, type=SOCK_STREAM and proto=0. 121 | """ 122 | 123 | def __init__(self, family=socket.AF_INET, type=socket.SOCK_STREAM, proto=0, _sock=None): 124 | _orgsocket.__init__(self, family, type, proto, _sock) 125 | if _defaultproxy != None: 126 | self.__proxy = _defaultproxy 127 | else: 128 | self.__proxy = (None, None, None, None, None, None) 129 | self.__proxysockname = None 130 | self.__proxypeername = None 131 | self.__httptunnel = True 132 | 133 | def __recvall(self, count): 134 | """__recvall(count) -> data 135 | Receive EXACTLY the number of bytes requested from the socket. 136 | Blocks until the required number of bytes have been received. 137 | """ 138 | data = self.recv(count) 139 | while len(data) < count: 140 | d = self.recv(count-len(data)) 141 | if not d: raise GeneralProxyError((0, "connection closed unexpectedly")) 142 | data = data + d 143 | return data 144 | 145 | def sendall(self, content, *args): 146 | """ override socket.socket.sendall method to rewrite the header 147 | for non-tunneling proxies if needed 148 | """ 149 | if not self.__httptunnel: 150 | content = self.__rewriteproxy(content) 151 | return super(socksocket, self).sendall(content, *args) 152 | 153 | def __rewriteproxy(self, header): 154 | """ rewrite HTTP request headers to support non-tunneling proxies 155 | (i.e. those which do not support the CONNECT method). 156 | This only works for HTTP (not HTTPS) since HTTPS requires tunneling. 157 | """ 158 | host, endpt = None, None 159 | hdrs = header.split("\r\n") 160 | for hdr in hdrs: 161 | if hdr.lower().startswith("host:"): 162 | host = hdr 163 | elif hdr.lower().startswith("get") or hdr.lower().startswith("post"): 164 | endpt = hdr 165 | if host and endpt: 166 | hdrs.remove(host) 167 | hdrs.remove(endpt) 168 | host = host.split(" ")[1] 169 | endpt = endpt.split(" ") 170 | if (self.__proxy[4] != None and self.__proxy[5] != None): 171 | hdrs.insert(0, self.__getauthheader()) 172 | hdrs.insert(0, "Host: %s" % host) 173 | hdrs.insert(0, "%s http://%s%s %s" % (endpt[0], host, endpt[1], endpt[2])) 174 | return "\r\n".join(hdrs) 175 | 176 | def __getauthheader(self): 177 | auth = self.__proxy[4] + ":" + self.__proxy[5] 178 | return "Proxy-Authorization: Basic " + base64.b64encode(auth) 179 | 180 | def setproxy(self, proxytype=None, addr=None, port=None, rdns=True, username=None, password=None): 181 | """setproxy(proxytype, addr[, port[, rdns[, username[, password]]]]) 182 | Sets the proxy to be used. 183 | proxytype - The type of the proxy to be used. Three types 184 | are supported: PROXY_TYPE_SOCKS4 (including socks4a), 185 | PROXY_TYPE_SOCKS5 and PROXY_TYPE_HTTP 186 | addr - The address of the server (IP or DNS). 187 | port - The port of the server. Defaults to 1080 for SOCKS 188 | servers and 8080 for HTTP proxy servers. 189 | rdns - Should DNS queries be preformed on the remote side 190 | (rather than the local side). The default is True. 191 | Note: This has no effect with SOCKS4 servers. 192 | username - Username to authenticate with to the server. 193 | The default is no authentication. 194 | password - Password to authenticate with to the server. 195 | Only relevant when username is also provided. 196 | """ 197 | self.__proxy = (proxytype, addr, port, rdns, username, password) 198 | 199 | def __negotiatesocks5(self, destaddr, destport): 200 | """__negotiatesocks5(self,destaddr,destport) 201 | Negotiates a connection through a SOCKS5 server. 202 | """ 203 | # First we'll send the authentication packages we support. 204 | if (self.__proxy[4]!=None) and (self.__proxy[5]!=None): 205 | # The username/password details were supplied to the 206 | # setproxy method so we support the USERNAME/PASSWORD 207 | # authentication (in addition to the standard none). 208 | self.sendall(struct.pack('BBBB', 0x05, 0x02, 0x00, 0x02)) 209 | else: 210 | # No username/password were entered, therefore we 211 | # only support connections with no authentication. 212 | self.sendall(struct.pack('BBB', 0x05, 0x01, 0x00)) 213 | # We'll receive the server's response to determine which 214 | # method was selected 215 | chosenauth = self.__recvall(2) 216 | if chosenauth[0:1] != chr(0x05).encode(): 217 | self.close() 218 | raise GeneralProxyError((1, _generalerrors[1])) 219 | # Check the chosen authentication method 220 | if chosenauth[1:2] == chr(0x00).encode(): 221 | # No authentication is required 222 | pass 223 | elif chosenauth[1:2] == chr(0x02).encode(): 224 | # Okay, we need to perform a basic username/password 225 | # authentication. 226 | self.sendall(chr(0x01).encode() + chr(len(self.__proxy[4])) + self.__proxy[4] + chr(len(self.__proxy[5])) + self.__proxy[5]) 227 | authstat = self.__recvall(2) 228 | if authstat[0:1] != chr(0x01).encode(): 229 | # Bad response 230 | self.close() 231 | raise GeneralProxyError((1, _generalerrors[1])) 232 | if authstat[1:2] != chr(0x00).encode(): 233 | # Authentication failed 234 | self.close() 235 | raise Socks5AuthError((3, _socks5autherrors[3])) 236 | # Authentication succeeded 237 | else: 238 | # Reaching here is always bad 239 | self.close() 240 | if chosenauth[1] == chr(0xFF).encode(): 241 | raise Socks5AuthError((2, _socks5autherrors[2])) 242 | else: 243 | raise GeneralProxyError((1, _generalerrors[1])) 244 | # Now we can request the actual connection 245 | req = struct.pack('BBB', 0x05, 0x01, 0x00) 246 | # If the given destination address is an IP address, we'll 247 | # use the IPv4 address request even if remote resolving was specified. 248 | try: 249 | ipaddr = socket.inet_aton(destaddr) 250 | req = req + chr(0x01).encode() + ipaddr 251 | except socket.error: 252 | # Well it's not an IP number, so it's probably a DNS name. 253 | if self.__proxy[3]: 254 | # Resolve remotely 255 | ipaddr = None 256 | req = req + chr(0x03).encode() + chr(len(destaddr)).encode() + destaddr 257 | else: 258 | # Resolve locally 259 | ipaddr = socket.inet_aton(socket.gethostbyname(destaddr)) 260 | req = req + chr(0x01).encode() + ipaddr 261 | req = req + struct.pack(">H", destport) 262 | self.sendall(req) 263 | # Get the response 264 | resp = self.__recvall(4) 265 | if resp[0:1] != chr(0x05).encode(): 266 | self.close() 267 | raise GeneralProxyError((1, _generalerrors[1])) 268 | elif resp[1:2] != chr(0x00).encode(): 269 | # Connection failed 270 | self.close() 271 | if ord(resp[1:2])<=8: 272 | raise Socks5Error((ord(resp[1:2]), _socks5errors[ord(resp[1:2])])) 273 | else: 274 | raise Socks5Error((9, _socks5errors[9])) 275 | # Get the bound address/port 276 | elif resp[3:4] == chr(0x01).encode(): 277 | boundaddr = self.__recvall(4) 278 | elif resp[3:4] == chr(0x03).encode(): 279 | resp = resp + self.recv(1) 280 | boundaddr = self.__recvall(ord(resp[4:5])) 281 | else: 282 | self.close() 283 | raise GeneralProxyError((1,_generalerrors[1])) 284 | boundport = struct.unpack(">H", self.__recvall(2))[0] 285 | self.__proxysockname = (boundaddr, boundport) 286 | if ipaddr != None: 287 | self.__proxypeername = (socket.inet_ntoa(ipaddr), destport) 288 | else: 289 | self.__proxypeername = (destaddr, destport) 290 | 291 | def getproxysockname(self): 292 | """getsockname() -> address info 293 | Returns the bound IP address and port number at the proxy. 294 | """ 295 | return self.__proxysockname 296 | 297 | def getproxypeername(self): 298 | """getproxypeername() -> address info 299 | Returns the IP and port number of the proxy. 300 | """ 301 | return _orgsocket.getpeername(self) 302 | 303 | def getpeername(self): 304 | """getpeername() -> address info 305 | Returns the IP address and port number of the destination 306 | machine (note: getproxypeername returns the proxy) 307 | """ 308 | return self.__proxypeername 309 | 310 | def __negotiatesocks4(self,destaddr,destport): 311 | """__negotiatesocks4(self,destaddr,destport) 312 | Negotiates a connection through a SOCKS4 server. 313 | """ 314 | # Check if the destination address provided is an IP address 315 | rmtrslv = False 316 | try: 317 | ipaddr = socket.inet_aton(destaddr) 318 | except socket.error: 319 | # It's a DNS name. Check where it should be resolved. 320 | if self.__proxy[3]: 321 | ipaddr = struct.pack("BBBB", 0x00, 0x00, 0x00, 0x01) 322 | rmtrslv = True 323 | else: 324 | ipaddr = socket.inet_aton(socket.gethostbyname(destaddr)) 325 | # Construct the request packet 326 | req = struct.pack(">BBH", 0x04, 0x01, destport) + ipaddr 327 | # The username parameter is considered userid for SOCKS4 328 | if self.__proxy[4] != None: 329 | req = req + self.__proxy[4] 330 | req = req + chr(0x00).encode() 331 | # DNS name if remote resolving is required 332 | # NOTE: This is actually an extension to the SOCKS4 protocol 333 | # called SOCKS4A and may not be supported in all cases. 334 | if rmtrslv: 335 | req = req + destaddr + chr(0x00).encode() 336 | self.sendall(req) 337 | # Get the response from the server 338 | resp = self.__recvall(8) 339 | if resp[0:1] != chr(0x00).encode(): 340 | # Bad data 341 | self.close() 342 | raise GeneralProxyError((1,_generalerrors[1])) 343 | if resp[1:2] != chr(0x5A).encode(): 344 | # Server returned an error 345 | self.close() 346 | if ord(resp[1:2]) in (91, 92, 93): 347 | self.close() 348 | raise Socks4Error((ord(resp[1:2]), _socks4errors[ord(resp[1:2]) - 90])) 349 | else: 350 | raise Socks4Error((94, _socks4errors[4])) 351 | # Get the bound address/port 352 | self.__proxysockname = (socket.inet_ntoa(resp[4:]), struct.unpack(">H", resp[2:4])[0]) 353 | if rmtrslv != None: 354 | self.__proxypeername = (socket.inet_ntoa(ipaddr), destport) 355 | else: 356 | self.__proxypeername = (destaddr, destport) 357 | 358 | def __negotiatehttp(self, destaddr, destport): 359 | """__negotiatehttp(self,destaddr,destport) 360 | Negotiates a connection through an HTTP server. 361 | """ 362 | # If we need to resolve locally, we do this now 363 | if not self.__proxy[3]: 364 | addr = socket.gethostbyname(destaddr) 365 | else: 366 | addr = destaddr 367 | headers = ["CONNECT ", addr, ":", str(destport), " HTTP/1.1\r\n"] 368 | headers += ["Host: ", destaddr, "\r\n"] 369 | if (self.__proxy[4] != None and self.__proxy[5] != None): 370 | headers += [self.__getauthheader(), "\r\n"] 371 | headers.append("\r\n") 372 | self.sendall("".join(headers).encode()) 373 | # We read the response until we get the string "\r\n\r\n" 374 | resp = self.recv(1) 375 | while resp.find("\r\n\r\n".encode()) == -1: 376 | resp = resp + self.recv(1) 377 | # We just need the first line to check if the connection 378 | # was successful 379 | statusline = resp.splitlines()[0].split(" ".encode(), 2) 380 | if statusline[0] not in ("HTTP/1.0".encode(), "HTTP/1.1".encode()): 381 | self.close() 382 | raise GeneralProxyError((1, _generalerrors[1])) 383 | try: 384 | statuscode = int(statusline[1]) 385 | except ValueError: 386 | self.close() 387 | raise GeneralProxyError((1, _generalerrors[1])) 388 | if statuscode != 200: 389 | self.close() 390 | raise HTTPError((statuscode, statusline[2])) 391 | self.__proxysockname = ("0.0.0.0", 0) 392 | self.__proxypeername = (addr, destport) 393 | 394 | def connect(self, destpair): 395 | """connect(self, despair) 396 | Connects to the specified destination through a proxy. 397 | destpar - A tuple of the IP/DNS address and the port number. 398 | (identical to socket's connect). 399 | To select the proxy server use setproxy(). 400 | """ 401 | # Do a minimal input check first 402 | if (not type(destpair) in (list,tuple)) or (len(destpair) < 2) or (not isinstance(destpair[0], basestring)) or (type(destpair[1]) != int): 403 | raise GeneralProxyError((5, _generalerrors[5])) 404 | if self.__proxy[0] == PROXY_TYPE_SOCKS5: 405 | if self.__proxy[2] != None: 406 | portnum = self.__proxy[2] 407 | else: 408 | portnum = 1080 409 | _orgsocket.connect(self, (self.__proxy[1], portnum)) 410 | self.__negotiatesocks5(destpair[0], destpair[1]) 411 | elif self.__proxy[0] == PROXY_TYPE_SOCKS4: 412 | if self.__proxy[2] != None: 413 | portnum = self.__proxy[2] 414 | else: 415 | portnum = 1080 416 | _orgsocket.connect(self,(self.__proxy[1], portnum)) 417 | self.__negotiatesocks4(destpair[0], destpair[1]) 418 | elif self.__proxy[0] == PROXY_TYPE_HTTP: 419 | if self.__proxy[2] != None: 420 | portnum = self.__proxy[2] 421 | else: 422 | portnum = 8080 423 | _orgsocket.connect(self,(self.__proxy[1], portnum)) 424 | self.__negotiatehttp(destpair[0], destpair[1]) 425 | elif self.__proxy[0] == PROXY_TYPE_HTTP_NO_TUNNEL: 426 | if self.__proxy[2] != None: 427 | portnum = self.__proxy[2] 428 | else: 429 | portnum = 8080 430 | _orgsocket.connect(self,(self.__proxy[1],portnum)) 431 | if destpair[1] == 443: 432 | self.__negotiatehttp(destpair[0],destpair[1]) 433 | else: 434 | self.__httptunnel = False 435 | elif self.__proxy[0] == None: 436 | _orgsocket.connect(self, (destpair[0], destpair[1])) 437 | else: 438 | raise GeneralProxyError((4, _generalerrors[4])) 439 | -------------------------------------------------------------------------------- /oauth2client/appengine.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2010 Google Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Utilities for Google App Engine 16 | 17 | Utilities for making it easier to use OAuth 2.0 on Google App Engine. 18 | """ 19 | 20 | __author__ = 'jcgregorio@google.com (Joe Gregorio)' 21 | 22 | import base64 23 | import cgi 24 | import httplib2 25 | import logging 26 | import os 27 | import pickle 28 | import threading 29 | import time 30 | 31 | from google.appengine.api import app_identity 32 | from google.appengine.api import memcache 33 | from google.appengine.api import users 34 | from google.appengine.ext import db 35 | from google.appengine.ext import webapp 36 | from google.appengine.ext.webapp.util import login_required 37 | from google.appengine.ext.webapp.util import run_wsgi_app 38 | from oauth2client import GOOGLE_AUTH_URI 39 | from oauth2client import GOOGLE_REVOKE_URI 40 | from oauth2client import GOOGLE_TOKEN_URI 41 | from oauth2client import clientsecrets 42 | from oauth2client import util 43 | from oauth2client import xsrfutil 44 | from oauth2client.anyjson import simplejson 45 | from oauth2client.client import AccessTokenRefreshError 46 | from oauth2client.client import AssertionCredentials 47 | from oauth2client.client import Credentials 48 | from oauth2client.client import Flow 49 | from oauth2client.client import OAuth2WebServerFlow 50 | from oauth2client.client import Storage 51 | 52 | # TODO(dhermes): Resolve import issue. 53 | # This is a temporary fix for a Google internal issue. 54 | try: 55 | from google.appengine.ext import ndb 56 | except ImportError: 57 | ndb = None 58 | 59 | 60 | logger = logging.getLogger(__name__) 61 | 62 | OAUTH2CLIENT_NAMESPACE = 'oauth2client#ns' 63 | 64 | XSRF_MEMCACHE_ID = 'xsrf_secret_key' 65 | 66 | 67 | def _safe_html(s): 68 | """Escape text to make it safe to display. 69 | 70 | Args: 71 | s: string, The text to escape. 72 | 73 | Returns: 74 | The escaped text as a string. 75 | """ 76 | return cgi.escape(s, quote=1).replace("'", ''') 77 | 78 | 79 | class InvalidClientSecretsError(Exception): 80 | """The client_secrets.json file is malformed or missing required fields.""" 81 | 82 | 83 | class InvalidXsrfTokenError(Exception): 84 | """The XSRF token is invalid or expired.""" 85 | 86 | 87 | class SiteXsrfSecretKey(db.Model): 88 | """Storage for the sites XSRF secret key. 89 | 90 | There will only be one instance stored of this model, the one used for the 91 | site. 92 | """ 93 | secret = db.StringProperty() 94 | 95 | if ndb is not None: 96 | class SiteXsrfSecretKeyNDB(ndb.Model): 97 | """NDB Model for storage for the sites XSRF secret key. 98 | 99 | Since this model uses the same kind as SiteXsrfSecretKey, it can be used 100 | interchangeably. This simply provides an NDB model for interacting with the 101 | same data the DB model interacts with. 102 | 103 | There should only be one instance stored of this model, the one used for the 104 | site. 105 | """ 106 | secret = ndb.StringProperty() 107 | 108 | @classmethod 109 | def _get_kind(cls): 110 | """Return the kind name for this class.""" 111 | return 'SiteXsrfSecretKey' 112 | 113 | 114 | def _generate_new_xsrf_secret_key(): 115 | """Returns a random XSRF secret key. 116 | """ 117 | return os.urandom(16).encode("hex") 118 | 119 | 120 | def xsrf_secret_key(): 121 | """Return the secret key for use for XSRF protection. 122 | 123 | If the Site entity does not have a secret key, this method will also create 124 | one and persist it. 125 | 126 | Returns: 127 | The secret key. 128 | """ 129 | secret = memcache.get(XSRF_MEMCACHE_ID, namespace=OAUTH2CLIENT_NAMESPACE) 130 | if not secret: 131 | # Load the one and only instance of SiteXsrfSecretKey. 132 | model = SiteXsrfSecretKey.get_or_insert(key_name='site') 133 | if not model.secret: 134 | model.secret = _generate_new_xsrf_secret_key() 135 | model.put() 136 | secret = model.secret 137 | memcache.add(XSRF_MEMCACHE_ID, secret, namespace=OAUTH2CLIENT_NAMESPACE) 138 | 139 | return str(secret) 140 | 141 | 142 | class AppAssertionCredentials(AssertionCredentials): 143 | """Credentials object for App Engine Assertion Grants 144 | 145 | This object will allow an App Engine application to identify itself to Google 146 | and other OAuth 2.0 servers that can verify assertions. It can be used for the 147 | purpose of accessing data stored under an account assigned to the App Engine 148 | application itself. 149 | 150 | This credential does not require a flow to instantiate because it represents 151 | a two legged flow, and therefore has all of the required information to 152 | generate and refresh its own access tokens. 153 | """ 154 | 155 | @util.positional(2) 156 | def __init__(self, scope, **kwargs): 157 | """Constructor for AppAssertionCredentials 158 | 159 | Args: 160 | scope: string or iterable of strings, scope(s) of the credentials being 161 | requested. 162 | """ 163 | self.scope = util.scopes_to_string(scope) 164 | 165 | # Assertion type is no longer used, but still in the parent class signature. 166 | super(AppAssertionCredentials, self).__init__(None) 167 | 168 | @classmethod 169 | def from_json(cls, json): 170 | data = simplejson.loads(json) 171 | return AppAssertionCredentials(data['scope']) 172 | 173 | def _refresh(self, http_request): 174 | """Refreshes the access_token. 175 | 176 | Since the underlying App Engine app_identity implementation does its own 177 | caching we can skip all the storage hoops and just to a refresh using the 178 | API. 179 | 180 | Args: 181 | http_request: callable, a callable that matches the method signature of 182 | httplib2.Http.request, used to make the refresh request. 183 | 184 | Raises: 185 | AccessTokenRefreshError: When the refresh fails. 186 | """ 187 | try: 188 | scopes = self.scope.split() 189 | (token, _) = app_identity.get_access_token(scopes) 190 | except app_identity.Error, e: 191 | raise AccessTokenRefreshError(str(e)) 192 | self.access_token = token 193 | 194 | 195 | class FlowProperty(db.Property): 196 | """App Engine datastore Property for Flow. 197 | 198 | Utility property that allows easy storage and retrieval of an 199 | oauth2client.Flow""" 200 | 201 | # Tell what the user type is. 202 | data_type = Flow 203 | 204 | # For writing to datastore. 205 | def get_value_for_datastore(self, model_instance): 206 | flow = super(FlowProperty, 207 | self).get_value_for_datastore(model_instance) 208 | return db.Blob(pickle.dumps(flow)) 209 | 210 | # For reading from datastore. 211 | def make_value_from_datastore(self, value): 212 | if value is None: 213 | return None 214 | return pickle.loads(value) 215 | 216 | def validate(self, value): 217 | if value is not None and not isinstance(value, Flow): 218 | raise db.BadValueError('Property %s must be convertible ' 219 | 'to a FlowThreeLegged instance (%s)' % 220 | (self.name, value)) 221 | return super(FlowProperty, self).validate(value) 222 | 223 | def empty(self, value): 224 | return not value 225 | 226 | 227 | if ndb is not None: 228 | class FlowNDBProperty(ndb.PickleProperty): 229 | """App Engine NDB datastore Property for Flow. 230 | 231 | Serves the same purpose as the DB FlowProperty, but for NDB models. Since 232 | PickleProperty inherits from BlobProperty, the underlying representation of 233 | the data in the datastore will be the same as in the DB case. 234 | 235 | Utility property that allows easy storage and retrieval of an 236 | oauth2client.Flow 237 | """ 238 | 239 | def _validate(self, value): 240 | """Validates a value as a proper Flow object. 241 | 242 | Args: 243 | value: A value to be set on the property. 244 | 245 | Raises: 246 | TypeError if the value is not an instance of Flow. 247 | """ 248 | logger.info('validate: Got type %s', type(value)) 249 | if value is not None and not isinstance(value, Flow): 250 | raise TypeError('Property %s must be convertible to a flow ' 251 | 'instance; received: %s.' % (self._name, value)) 252 | 253 | 254 | class CredentialsProperty(db.Property): 255 | """App Engine datastore Property for Credentials. 256 | 257 | Utility property that allows easy storage and retrieval of 258 | oath2client.Credentials 259 | """ 260 | 261 | # Tell what the user type is. 262 | data_type = Credentials 263 | 264 | # For writing to datastore. 265 | def get_value_for_datastore(self, model_instance): 266 | logger.info("get: Got type " + str(type(model_instance))) 267 | cred = super(CredentialsProperty, 268 | self).get_value_for_datastore(model_instance) 269 | if cred is None: 270 | cred = '' 271 | else: 272 | cred = cred.to_json() 273 | return db.Blob(cred) 274 | 275 | # For reading from datastore. 276 | def make_value_from_datastore(self, value): 277 | logger.info("make: Got type " + str(type(value))) 278 | if value is None: 279 | return None 280 | if len(value) == 0: 281 | return None 282 | try: 283 | credentials = Credentials.new_from_json(value) 284 | except ValueError: 285 | credentials = None 286 | return credentials 287 | 288 | def validate(self, value): 289 | value = super(CredentialsProperty, self).validate(value) 290 | logger.info("validate: Got type " + str(type(value))) 291 | if value is not None and not isinstance(value, Credentials): 292 | raise db.BadValueError('Property %s must be convertible ' 293 | 'to a Credentials instance (%s)' % 294 | (self.name, value)) 295 | #if value is not None and not isinstance(value, Credentials): 296 | # return None 297 | return value 298 | 299 | 300 | if ndb is not None: 301 | # TODO(dhermes): Turn this into a JsonProperty and overhaul the Credentials 302 | # and subclass mechanics to use new_from_dict, to_dict, 303 | # from_dict, etc. 304 | class CredentialsNDBProperty(ndb.BlobProperty): 305 | """App Engine NDB datastore Property for Credentials. 306 | 307 | Serves the same purpose as the DB CredentialsProperty, but for NDB models. 308 | Since CredentialsProperty stores data as a blob and this inherits from 309 | BlobProperty, the data in the datastore will be the same as in the DB case. 310 | 311 | Utility property that allows easy storage and retrieval of Credentials and 312 | subclasses. 313 | """ 314 | def _validate(self, value): 315 | """Validates a value as a proper credentials object. 316 | 317 | Args: 318 | value: A value to be set on the property. 319 | 320 | Raises: 321 | TypeError if the value is not an instance of Credentials. 322 | """ 323 | logger.info('validate: Got type %s', type(value)) 324 | if value is not None and not isinstance(value, Credentials): 325 | raise TypeError('Property %s must be convertible to a credentials ' 326 | 'instance; received: %s.' % (self._name, value)) 327 | 328 | def _to_base_type(self, value): 329 | """Converts our validated value to a JSON serialized string. 330 | 331 | Args: 332 | value: A value to be set in the datastore. 333 | 334 | Returns: 335 | A JSON serialized version of the credential, else '' if value is None. 336 | """ 337 | if value is None: 338 | return '' 339 | else: 340 | return value.to_json() 341 | 342 | def _from_base_type(self, value): 343 | """Converts our stored JSON string back to the desired type. 344 | 345 | Args: 346 | value: A value from the datastore to be converted to the desired type. 347 | 348 | Returns: 349 | A deserialized Credentials (or subclass) object, else None if the 350 | value can't be parsed. 351 | """ 352 | if not value: 353 | return None 354 | try: 355 | # Uses the from_json method of the implied class of value 356 | credentials = Credentials.new_from_json(value) 357 | except ValueError: 358 | credentials = None 359 | return credentials 360 | 361 | 362 | class StorageByKeyName(Storage): 363 | """Store and retrieve a credential to and from the App Engine datastore. 364 | 365 | This Storage helper presumes the Credentials have been stored as a 366 | CredentialsProperty or CredentialsNDBProperty on a datastore model class, and 367 | that entities are stored by key_name. 368 | """ 369 | 370 | @util.positional(4) 371 | def __init__(self, model, key_name, property_name, cache=None, user=None): 372 | """Constructor for Storage. 373 | 374 | Args: 375 | model: db.Model or ndb.Model, model class 376 | key_name: string, key name for the entity that has the credentials 377 | property_name: string, name of the property that is a CredentialsProperty 378 | or CredentialsNDBProperty. 379 | cache: memcache, a write-through cache to put in front of the datastore. 380 | If the model you are using is an NDB model, using a cache will be 381 | redundant since the model uses an instance cache and memcache for you. 382 | user: users.User object, optional. Can be used to grab user ID as a 383 | key_name if no key name is specified. 384 | """ 385 | if key_name is None: 386 | if user is None: 387 | raise ValueError('StorageByKeyName called with no key name or user.') 388 | key_name = user.user_id() 389 | 390 | self._model = model 391 | self._key_name = key_name 392 | self._property_name = property_name 393 | self._cache = cache 394 | 395 | def _is_ndb(self): 396 | """Determine whether the model of the instance is an NDB model. 397 | 398 | Returns: 399 | Boolean indicating whether or not the model is an NDB or DB model. 400 | """ 401 | # issubclass will fail if one of the arguments is not a class, only need 402 | # worry about new-style classes since ndb and db models are new-style 403 | if isinstance(self._model, type): 404 | if ndb is not None and issubclass(self._model, ndb.Model): 405 | return True 406 | elif issubclass(self._model, db.Model): 407 | return False 408 | 409 | raise TypeError('Model class not an NDB or DB model: %s.' % (self._model,)) 410 | 411 | def _get_entity(self): 412 | """Retrieve entity from datastore. 413 | 414 | Uses a different model method for db or ndb models. 415 | 416 | Returns: 417 | Instance of the model corresponding to the current storage object 418 | and stored using the key name of the storage object. 419 | """ 420 | if self._is_ndb(): 421 | return self._model.get_by_id(self._key_name) 422 | else: 423 | return self._model.get_by_key_name(self._key_name) 424 | 425 | def _delete_entity(self): 426 | """Delete entity from datastore. 427 | 428 | Attempts to delete using the key_name stored on the object, whether or not 429 | the given key is in the datastore. 430 | """ 431 | if self._is_ndb(): 432 | ndb.Key(self._model, self._key_name).delete() 433 | else: 434 | entity_key = db.Key.from_path(self._model.kind(), self._key_name) 435 | db.delete(entity_key) 436 | 437 | def locked_get(self): 438 | """Retrieve Credential from datastore. 439 | 440 | Returns: 441 | oauth2client.Credentials 442 | """ 443 | credentials = None 444 | if self._cache: 445 | json = self._cache.get(self._key_name) 446 | if json: 447 | credentials = Credentials.new_from_json(json) 448 | if credentials is None: 449 | entity = self._get_entity() 450 | if entity is not None: 451 | credentials = getattr(entity, self._property_name) 452 | if self._cache: 453 | self._cache.set(self._key_name, credentials.to_json()) 454 | 455 | if credentials and hasattr(credentials, 'set_store'): 456 | credentials.set_store(self) 457 | return credentials 458 | 459 | def locked_put(self, credentials): 460 | """Write a Credentials to the datastore. 461 | 462 | Args: 463 | credentials: Credentials, the credentials to store. 464 | """ 465 | entity = self._model.get_or_insert(self._key_name) 466 | setattr(entity, self._property_name, credentials) 467 | entity.put() 468 | if self._cache: 469 | self._cache.set(self._key_name, credentials.to_json()) 470 | 471 | def locked_delete(self): 472 | """Delete Credential from datastore.""" 473 | 474 | if self._cache: 475 | self._cache.delete(self._key_name) 476 | 477 | self._delete_entity() 478 | 479 | 480 | class CredentialsModel(db.Model): 481 | """Storage for OAuth 2.0 Credentials 482 | 483 | Storage of the model is keyed by the user.user_id(). 484 | """ 485 | credentials = CredentialsProperty() 486 | 487 | 488 | if ndb is not None: 489 | class CredentialsNDBModel(ndb.Model): 490 | """NDB Model for storage of OAuth 2.0 Credentials 491 | 492 | Since this model uses the same kind as CredentialsModel and has a property 493 | which can serialize and deserialize Credentials correctly, it can be used 494 | interchangeably with a CredentialsModel to access, insert and delete the 495 | same entities. This simply provides an NDB model for interacting with the 496 | same data the DB model interacts with. 497 | 498 | Storage of the model is keyed by the user.user_id(). 499 | """ 500 | credentials = CredentialsNDBProperty() 501 | 502 | @classmethod 503 | def _get_kind(cls): 504 | """Return the kind name for this class.""" 505 | return 'CredentialsModel' 506 | 507 | 508 | def _build_state_value(request_handler, user): 509 | """Composes the value for the 'state' parameter. 510 | 511 | Packs the current request URI and an XSRF token into an opaque string that 512 | can be passed to the authentication server via the 'state' parameter. 513 | 514 | Args: 515 | request_handler: webapp.RequestHandler, The request. 516 | user: google.appengine.api.users.User, The current user. 517 | 518 | Returns: 519 | The state value as a string. 520 | """ 521 | uri = request_handler.request.url 522 | token = xsrfutil.generate_token(xsrf_secret_key(), user.user_id(), 523 | action_id=str(uri)) 524 | return uri + ':' + token 525 | 526 | 527 | def _parse_state_value(state, user): 528 | """Parse the value of the 'state' parameter. 529 | 530 | Parses the value and validates the XSRF token in the state parameter. 531 | 532 | Args: 533 | state: string, The value of the state parameter. 534 | user: google.appengine.api.users.User, The current user. 535 | 536 | Raises: 537 | InvalidXsrfTokenError: if the XSRF token is invalid. 538 | 539 | Returns: 540 | The redirect URI. 541 | """ 542 | uri, token = state.rsplit(':', 1) 543 | if not xsrfutil.validate_token(xsrf_secret_key(), token, user.user_id(), 544 | action_id=uri): 545 | raise InvalidXsrfTokenError() 546 | 547 | return uri 548 | 549 | 550 | class OAuth2Decorator(object): 551 | """Utility for making OAuth 2.0 easier. 552 | 553 | Instantiate and then use with oauth_required or oauth_aware 554 | as decorators on webapp.RequestHandler methods. 555 | 556 | Example: 557 | 558 | decorator = OAuth2Decorator( 559 | client_id='837...ent.com', 560 | client_secret='Qh...wwI', 561 | scope='https://www.googleapis.com/auth/plus') 562 | 563 | 564 | class MainHandler(webapp.RequestHandler): 565 | 566 | @decorator.oauth_required 567 | def get(self): 568 | http = decorator.http() 569 | # http is authorized with the user's Credentials and can be used 570 | # in API calls 571 | 572 | """ 573 | 574 | def set_credentials(self, credentials): 575 | self._tls.credentials = credentials 576 | 577 | def get_credentials(self): 578 | """A thread local Credentials object. 579 | 580 | Returns: 581 | A client.Credentials object, or None if credentials hasn't been set in 582 | this thread yet, which may happen when calling has_credentials inside 583 | oauth_aware. 584 | """ 585 | return getattr(self._tls, 'credentials', None) 586 | 587 | credentials = property(get_credentials, set_credentials) 588 | 589 | def set_flow(self, flow): 590 | self._tls.flow = flow 591 | 592 | def get_flow(self): 593 | """A thread local Flow object. 594 | 595 | Returns: 596 | A credentials.Flow object, or None if the flow hasn't been set in this 597 | thread yet, which happens in _create_flow() since Flows are created 598 | lazily. 599 | """ 600 | return getattr(self._tls, 'flow', None) 601 | 602 | flow = property(get_flow, set_flow) 603 | 604 | 605 | @util.positional(4) 606 | def __init__(self, client_id, client_secret, scope, 607 | auth_uri=GOOGLE_AUTH_URI, 608 | token_uri=GOOGLE_TOKEN_URI, 609 | revoke_uri=GOOGLE_REVOKE_URI, 610 | user_agent=None, 611 | message=None, 612 | callback_path='/oauth2callback', 613 | token_response_param=None, 614 | _storage_class=StorageByKeyName, 615 | _credentials_class=CredentialsModel, 616 | _credentials_property_name='credentials', 617 | **kwargs): 618 | 619 | """Constructor for OAuth2Decorator 620 | 621 | Args: 622 | client_id: string, client identifier. 623 | client_secret: string client secret. 624 | scope: string or iterable of strings, scope(s) of the credentials being 625 | requested. 626 | auth_uri: string, URI for authorization endpoint. For convenience 627 | defaults to Google's endpoints but any OAuth 2.0 provider can be used. 628 | token_uri: string, URI for token endpoint. For convenience 629 | defaults to Google's endpoints but any OAuth 2.0 provider can be used. 630 | revoke_uri: string, URI for revoke endpoint. For convenience 631 | defaults to Google's endpoints but any OAuth 2.0 provider can be used. 632 | user_agent: string, User agent of your application, default to None. 633 | message: Message to display if there are problems with the OAuth 2.0 634 | configuration. The message may contain HTML and will be presented on the 635 | web interface for any method that uses the decorator. 636 | callback_path: string, The absolute path to use as the callback URI. Note 637 | that this must match up with the URI given when registering the 638 | application in the APIs Console. 639 | token_response_param: string. If provided, the full JSON response 640 | to the access token request will be encoded and included in this query 641 | parameter in the callback URI. This is useful with providers (e.g. 642 | wordpress.com) that include extra fields that the client may want. 643 | _storage_class: "Protected" keyword argument not typically provided to 644 | this constructor. A storage class to aid in storing a Credentials object 645 | for a user in the datastore. Defaults to StorageByKeyName. 646 | _credentials_class: "Protected" keyword argument not typically provided to 647 | this constructor. A db or ndb Model class to hold credentials. Defaults 648 | to CredentialsModel. 649 | _credentials_property_name: "Protected" keyword argument not typically 650 | provided to this constructor. A string indicating the name of the field 651 | on the _credentials_class where a Credentials object will be stored. 652 | Defaults to 'credentials'. 653 | **kwargs: dict, Keyword arguments are be passed along as kwargs to the 654 | OAuth2WebServerFlow constructor. 655 | """ 656 | self._tls = threading.local() 657 | self.flow = None 658 | self.credentials = None 659 | self._client_id = client_id 660 | self._client_secret = client_secret 661 | self._scope = util.scopes_to_string(scope) 662 | self._auth_uri = auth_uri 663 | self._token_uri = token_uri 664 | self._revoke_uri = revoke_uri 665 | self._user_agent = user_agent 666 | self._kwargs = kwargs 667 | self._message = message 668 | self._in_error = False 669 | self._callback_path = callback_path 670 | self._token_response_param = token_response_param 671 | self._storage_class = _storage_class 672 | self._credentials_class = _credentials_class 673 | self._credentials_property_name = _credentials_property_name 674 | 675 | def _display_error_message(self, request_handler): 676 | request_handler.response.out.write('') 677 | request_handler.response.out.write(_safe_html(self._message)) 678 | request_handler.response.out.write('') 679 | 680 | def oauth_required(self, method): 681 | """Decorator that starts the OAuth 2.0 dance. 682 | 683 | Starts the OAuth dance for the logged in user if they haven't already 684 | granted access for this application. 685 | 686 | Args: 687 | method: callable, to be decorated method of a webapp.RequestHandler 688 | instance. 689 | """ 690 | 691 | def check_oauth(request_handler, *args, **kwargs): 692 | if self._in_error: 693 | self._display_error_message(request_handler) 694 | return 695 | 696 | user = users.get_current_user() 697 | # Don't use @login_decorator as this could be used in a POST request. 698 | if not user: 699 | request_handler.redirect(users.create_login_url( 700 | request_handler.request.uri)) 701 | return 702 | 703 | self._create_flow(request_handler) 704 | 705 | # Store the request URI in 'state' so we can use it later 706 | self.flow.params['state'] = _build_state_value(request_handler, user) 707 | self.credentials = self._storage_class( 708 | self._credentials_class, None, 709 | self._credentials_property_name, user=user).get() 710 | 711 | if not self.has_credentials(): 712 | return request_handler.redirect(self.authorize_url()) 713 | try: 714 | resp = method(request_handler, *args, **kwargs) 715 | except AccessTokenRefreshError: 716 | return request_handler.redirect(self.authorize_url()) 717 | finally: 718 | self.credentials = None 719 | return resp 720 | 721 | return check_oauth 722 | 723 | def _create_flow(self, request_handler): 724 | """Create the Flow object. 725 | 726 | The Flow is calculated lazily since we don't know where this app is 727 | running until it receives a request, at which point redirect_uri can be 728 | calculated and then the Flow object can be constructed. 729 | 730 | Args: 731 | request_handler: webapp.RequestHandler, the request handler. 732 | """ 733 | if self.flow is None: 734 | redirect_uri = request_handler.request.relative_url( 735 | self._callback_path) # Usually /oauth2callback 736 | self.flow = OAuth2WebServerFlow(self._client_id, self._client_secret, 737 | self._scope, redirect_uri=redirect_uri, 738 | user_agent=self._user_agent, 739 | auth_uri=self._auth_uri, 740 | token_uri=self._token_uri, 741 | revoke_uri=self._revoke_uri, 742 | **self._kwargs) 743 | 744 | def oauth_aware(self, method): 745 | """Decorator that sets up for OAuth 2.0 dance, but doesn't do it. 746 | 747 | Does all the setup for the OAuth dance, but doesn't initiate it. 748 | This decorator is useful if you want to create a page that knows 749 | whether or not the user has granted access to this application. 750 | From within a method decorated with @oauth_aware the has_credentials() 751 | and authorize_url() methods can be called. 752 | 753 | Args: 754 | method: callable, to be decorated method of a webapp.RequestHandler 755 | instance. 756 | """ 757 | 758 | def setup_oauth(request_handler, *args, **kwargs): 759 | if self._in_error: 760 | self._display_error_message(request_handler) 761 | return 762 | 763 | user = users.get_current_user() 764 | # Don't use @login_decorator as this could be used in a POST request. 765 | if not user: 766 | request_handler.redirect(users.create_login_url( 767 | request_handler.request.uri)) 768 | return 769 | 770 | self._create_flow(request_handler) 771 | 772 | self.flow.params['state'] = _build_state_value(request_handler, user) 773 | self.credentials = self._storage_class( 774 | self._credentials_class, None, 775 | self._credentials_property_name, user=user).get() 776 | try: 777 | resp = method(request_handler, *args, **kwargs) 778 | finally: 779 | self.credentials = None 780 | return resp 781 | return setup_oauth 782 | 783 | 784 | def has_credentials(self): 785 | """True if for the logged in user there are valid access Credentials. 786 | 787 | Must only be called from with a webapp.RequestHandler subclassed method 788 | that had been decorated with either @oauth_required or @oauth_aware. 789 | """ 790 | return self.credentials is not None and not self.credentials.invalid 791 | 792 | def authorize_url(self): 793 | """Returns the URL to start the OAuth dance. 794 | 795 | Must only be called from with a webapp.RequestHandler subclassed method 796 | that had been decorated with either @oauth_required or @oauth_aware. 797 | """ 798 | url = self.flow.step1_get_authorize_url() 799 | return str(url) 800 | 801 | def http(self): 802 | """Returns an authorized http instance. 803 | 804 | Must only be called from within an @oauth_required decorated method, or 805 | from within an @oauth_aware decorated method where has_credentials() 806 | returns True. 807 | """ 808 | return self.credentials.authorize(httplib2.Http()) 809 | 810 | @property 811 | def callback_path(self): 812 | """The absolute path where the callback will occur. 813 | 814 | Note this is the absolute path, not the absolute URI, that will be 815 | calculated by the decorator at runtime. See callback_handler() for how this 816 | should be used. 817 | 818 | Returns: 819 | The callback path as a string. 820 | """ 821 | return self._callback_path 822 | 823 | 824 | def callback_handler(self): 825 | """RequestHandler for the OAuth 2.0 redirect callback. 826 | 827 | Usage: 828 | app = webapp.WSGIApplication([ 829 | ('/index', MyIndexHandler), 830 | ..., 831 | (decorator.callback_path, decorator.callback_handler()) 832 | ]) 833 | 834 | Returns: 835 | A webapp.RequestHandler that handles the redirect back from the 836 | server during the OAuth 2.0 dance. 837 | """ 838 | decorator = self 839 | 840 | class OAuth2Handler(webapp.RequestHandler): 841 | """Handler for the redirect_uri of the OAuth 2.0 dance.""" 842 | 843 | @login_required 844 | def get(self): 845 | error = self.request.get('error') 846 | if error: 847 | errormsg = self.request.get('error_description', error) 848 | self.response.out.write( 849 | 'The authorization request failed: %s' % _safe_html(errormsg)) 850 | else: 851 | user = users.get_current_user() 852 | decorator._create_flow(self) 853 | credentials = decorator.flow.step2_exchange(self.request.params) 854 | decorator._storage_class( 855 | decorator._credentials_class, None, 856 | decorator._credentials_property_name, user=user).put(credentials) 857 | redirect_uri = _parse_state_value(str(self.request.get('state')), 858 | user) 859 | 860 | if decorator._token_response_param and credentials.token_response: 861 | resp_json = simplejson.dumps(credentials.token_response) 862 | redirect_uri = util._add_query_parameter( 863 | redirect_uri, decorator._token_response_param, resp_json) 864 | 865 | self.redirect(redirect_uri) 866 | 867 | return OAuth2Handler 868 | 869 | def callback_application(self): 870 | """WSGI application for handling the OAuth 2.0 redirect callback. 871 | 872 | If you need finer grained control use `callback_handler` which returns just 873 | the webapp.RequestHandler. 874 | 875 | Returns: 876 | A webapp.WSGIApplication that handles the redirect back from the 877 | server during the OAuth 2.0 dance. 878 | """ 879 | return webapp.WSGIApplication([ 880 | (self.callback_path, self.callback_handler()) 881 | ]) 882 | 883 | 884 | class OAuth2DecoratorFromClientSecrets(OAuth2Decorator): 885 | """An OAuth2Decorator that builds from a clientsecrets file. 886 | 887 | Uses a clientsecrets file as the source for all the information when 888 | constructing an OAuth2Decorator. 889 | 890 | Example: 891 | 892 | decorator = OAuth2DecoratorFromClientSecrets( 893 | os.path.join(os.path.dirname(__file__), 'client_secrets.json') 894 | scope='https://www.googleapis.com/auth/plus') 895 | 896 | 897 | class MainHandler(webapp.RequestHandler): 898 | 899 | @decorator.oauth_required 900 | def get(self): 901 | http = decorator.http() 902 | # http is authorized with the user's Credentials and can be used 903 | # in API calls 904 | """ 905 | 906 | @util.positional(3) 907 | def __init__(self, filename, scope, message=None, cache=None): 908 | """Constructor 909 | 910 | Args: 911 | filename: string, File name of client secrets. 912 | scope: string or iterable of strings, scope(s) of the credentials being 913 | requested. 914 | message: string, A friendly string to display to the user if the 915 | clientsecrets file is missing or invalid. The message may contain HTML 916 | and will be presented on the web interface for any method that uses the 917 | decorator. 918 | cache: An optional cache service client that implements get() and set() 919 | methods. See clientsecrets.loadfile() for details. 920 | """ 921 | client_type, client_info = clientsecrets.loadfile(filename, cache=cache) 922 | if client_type not in [ 923 | clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]: 924 | raise InvalidClientSecretsError( 925 | 'OAuth2Decorator doesn\'t support this OAuth 2.0 flow.') 926 | constructor_kwargs = { 927 | 'auth_uri': client_info['auth_uri'], 928 | 'token_uri': client_info['token_uri'], 929 | 'message': message, 930 | } 931 | revoke_uri = client_info.get('revoke_uri') 932 | if revoke_uri is not None: 933 | constructor_kwargs['revoke_uri'] = revoke_uri 934 | super(OAuth2DecoratorFromClientSecrets, self).__init__( 935 | client_info['client_id'], client_info['client_secret'], 936 | scope, **constructor_kwargs) 937 | if message is not None: 938 | self._message = message 939 | else: 940 | self._message = 'Please configure your application for OAuth 2.0.' 941 | 942 | 943 | @util.positional(2) 944 | def oauth2decorator_from_clientsecrets(filename, scope, 945 | message=None, cache=None): 946 | """Creates an OAuth2Decorator populated from a clientsecrets file. 947 | 948 | Args: 949 | filename: string, File name of client secrets. 950 | scope: string or list of strings, scope(s) of the credentials being 951 | requested. 952 | message: string, A friendly string to display to the user if the 953 | clientsecrets file is missing or invalid. The message may contain HTML and 954 | will be presented on the web interface for any method that uses the 955 | decorator. 956 | cache: An optional cache service client that implements get() and set() 957 | methods. See clientsecrets.loadfile() for details. 958 | 959 | Returns: An OAuth2Decorator 960 | 961 | """ 962 | return OAuth2DecoratorFromClientSecrets(filename, scope, 963 | message=message, cache=cache) 964 | --------------------------------------------------------------------------------