├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── app.yaml ├── attachmentproxy ├── __init__.py └── handler.py ├── client_secrets.json ├── lib ├── apiclient │ ├── __init__.py │ ├── discovery.py │ ├── errors.py │ ├── ext │ │ └── __init__.py │ ├── http.py │ ├── mimeparse.py │ ├── model.py │ ├── push.py │ └── schema.py ├── gflags.py ├── gflags_validators.py ├── httplib2 │ ├── __init__.py │ ├── cacerts.txt │ ├── iri2uri.py │ └── socks.py ├── oauth2client │ ├── __init__.py │ ├── anyjson.py │ ├── appengine.py │ ├── client.py │ ├── clientsecrets.py │ ├── crypt.py │ ├── django_orm.py │ ├── file.py │ ├── gce.py │ ├── keyring_storage.py │ ├── locked_file.py │ ├── multistore_file.py │ ├── tools.py │ ├── util.py │ └── xsrfutil.py ├── sessions.py └── uritemplate │ └── __init__.py ├── main.py ├── main_handler.py ├── model.py ├── notify ├── __init__.py └── handler.py ├── oauth ├── __init__.py └── handler.py ├── signout ├── __init__.py └── handler.py ├── static ├── bootstrap │ ├── css │ │ ├── bootstrap-responsive.css │ │ ├── bootstrap-responsive.min.css │ │ ├── bootstrap.css │ │ └── bootstrap.min.css │ ├── img │ │ ├── glyphicons-halflings-white.png │ │ └── glyphicons-halflings.png │ └── js │ │ ├── bootstrap.js │ │ └── bootstrap.min.js ├── images │ ├── chipotle-tube-640x360.jpg │ ├── drill.png │ ├── keys.png │ ├── python.png │ └── saturn-eclipse.jpg └── main.css ├── templates └── index.html └── util.py /.gitignore: -------------------------------------------------------------------------------- 1 | client_secrets.json 2 | session.secret 3 | 4 | *.py[cod] 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Packages 10 | *.egg 11 | *.egg-info 12 | dist 13 | build 14 | eggs 15 | parts 16 | bin 17 | var 18 | sdist 19 | develop-eggs 20 | .installed.cfg 21 | lib64 22 | 23 | # Installer logs 24 | pip-log.txt 25 | 26 | # Unit test / coverage reports 27 | .coverage 28 | .tox 29 | nosetests.xml 30 | 31 | # Translations 32 | *.mo 33 | 34 | # Mr Developer 35 | .mr.developer.cfg 36 | .project 37 | .pydevproject 38 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to become a contributor and submit your own code 2 | 3 | ## Contributor License Agreements 4 | 5 | We'd love to accept your sample apps and patches! Before we can take them, we 6 | have to jump a couple of legal hurdles. 7 | 8 | Please fill out either the individual or corporate Contributor License Agreement 9 | (CLA). 10 | 11 | * If you are an individual writing original source code and you're sure you 12 | own the intellectual property, then you'll need to sign an [individual CLA] 13 | (http://code.google.com/legal/individual-cla-v1.0.html). 14 | * If you work for a company that wants to allow you to contribute your work, 15 | then you'll need to sign a [corporate CLA] 16 | (http://code.google.com/legal/corporate-cla-v1.0.html). 17 | 18 | Follow either of the two links above to access the appropriate CLA and 19 | instructions for how to sign and return it. Once we receive it, we'll be able to 20 | accept your pull requests. 21 | 22 | ## Contributing a Patch 23 | 24 | 1. Sign a Contributor License Agreement, if you have not yet done so (see 25 | details above). 26 | 1. Create your change to the repo in question. 27 | * Fork the desired repo, develop and test your code changes. 28 | * Ensure that your code is clear and comprehensible. 29 | * Ensure that your code has an appropriate set of unit tests which all pass. 30 | 1. Submit a pull request. 31 | 1. The repo owner will review your request. If it is approved, the change will 32 | be merged. If it needs additional work, the repo owner will respond with 33 | useful comments. 34 | 35 | ## Contributing a New Sample App 36 | 37 | 1. Sign a Contributor License Agreement, if you have not yet done so (see 38 | details above). 39 | 1. Create your own repo for your app following this naming convention: 40 | * mirror-{app-name}-{language or plaform} 41 | * apps: quickstart, photohunt-server, photohunt-client 42 | * example: mirror-quickstart-android 43 | * For multi-language apps, concatenate the primary languages like this: 44 | mirror-photohunt-server-java-python. 45 | 46 | 1. Create your sample app in this repo. 47 | * Be sure to clone the README.md, CONTRIBUTING.md and LICENSE files from the 48 | googleglass repo. 49 | * Ensure that your code is clear and comprehensible. 50 | * Ensure that your code has an appropriate set of unit tests which all pass. 51 | * Instructional value is the top priority when evaluating new app proposals for 52 | this collection of repos. 53 | 1. Submit a request to fork your repo in googleglass organization. 54 | 1. The repo owner will review your request. If it is approved, the sample will 55 | be merged. If it needs additional work, the repo owner will respond with 56 | useful comments. 57 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 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 | Dependent Modules 16 | ================= 17 | 18 | This code has the following dependencies 19 | above and beyond the Python standard library: 20 | 21 | google-api-python-client - Apache License 2.0 22 | uritemplates - Apache License 2.0 23 | httplib2 - MIT License 24 | sessions.py from Tornado Framework's web.py - Apache License 2.0 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Google Mirror API's Quickstart for Python 2 | ======================== 3 | 4 | The documentation for this quickstart is maintained on developers.google.com. 5 | Please see here for more information: 6 | https://developers.google.com/glass/quickstart/python 7 | -------------------------------------------------------------------------------- /app.yaml: -------------------------------------------------------------------------------- 1 | application: your_appengine_application_id 2 | version: 1 3 | runtime: python27 4 | api_version: 1 5 | threadsafe: true 6 | 7 | handlers: 8 | - url: /static 9 | static_dir: static 10 | secure: always 11 | 12 | - url: /.* 13 | script: main.app 14 | secure: always 15 | 16 | 17 | libraries: 18 | - name: jinja2 19 | version: latest 20 | 21 | 22 | skip_files: 23 | # Default patterns skipped by App Engine, which must be repeated since 24 | # specifying skip_files overrides them otherwise. See 25 | # https://developers.google.com/appengine/docs/python/config/appconfig#Skipping_Files. 26 | - ^(.*/)?app\.yaml 27 | - ^(.*/)?app\.yml 28 | - ^(.*/)?index\.yaml 29 | - ^(.*/)?index\.yml 30 | - ^(.*/)?#.*# 31 | - ^(.*/)?.*~ 32 | - ^(.*/)?.*\.py[co] 33 | - ^(.*/)?.*/RCS/.* 34 | - ^(.*/)?\..* 35 | - ^.*\.pyc$ 36 | -------------------------------------------------------------------------------- /attachmentproxy/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googleglass/mirror-quickstart-python/e34077bae91657170c305702471f5c249eb1b686/attachmentproxy/__init__.py -------------------------------------------------------------------------------- /attachmentproxy/handler.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 | """Request Handler for /main endpoint.""" 16 | 17 | __author__ = 'alainv@google.com (Alain Vongsouvanh)' 18 | 19 | 20 | import logging 21 | import webapp2 22 | 23 | from util import auth_required 24 | 25 | 26 | class AttachmentProxyHandler(webapp2.RequestHandler): 27 | """Request Handler for the main endpoint.""" 28 | 29 | @auth_required 30 | def get(self): 31 | """Return the attachment's content using the current user's credentials.""" 32 | # self.mirror_service is initialized in util.auth_required. 33 | attachment_id = self.request.get('attachment') 34 | item_id = self.request.get('timelineItem') 35 | logging.info('Attachment ID: %s', attachment_id) 36 | if not attachment_id or not item_id: 37 | self.response.set_status(400) 38 | return 39 | else: 40 | # Retrieve the attachment's metadata. 41 | attachment_metadata = self.mirror_service.timeline().attachments().get( 42 | itemId=item_id, attachmentId=attachment_id).execute() 43 | content_type = str(attachment_metadata.get('contentType')) 44 | content_url = attachment_metadata.get('contentUrl') 45 | 46 | # Retrieve the attachment's content. 47 | resp, content = self.mirror_service._http.request(content_url) 48 | if resp.status == 200: 49 | self.response.headers.add_header('Content-type', content_type) 50 | self.response.out.write(content) 51 | else: 52 | logging.info('Unable to retrieve attachment: %s', resp.status) 53 | self.response.set_status(500) 54 | 55 | 56 | ATTACHMENT_PROXY_ROUTES = [ 57 | ('/attachmentproxy', AttachmentProxyHandler) 58 | ] 59 | -------------------------------------------------------------------------------- /client_secrets.json: -------------------------------------------------------------------------------- 1 | { 2 | "web": { 3 | "client_id": "[[YOUR_CLIENT_ID]]", 4 | "client_secret": "[[YOUR_CLIENT_SECRET]]", 5 | "redirect_uris": [ 6 | ], 7 | "auth_uri": "https://accounts.google.com/o/oauth2/auth", 8 | "token_uri": "https://accounts.google.com/o/oauth2/token" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/apiclient/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "1.1" 2 | -------------------------------------------------------------------------------- /lib/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 | 106 | class BatchError(HttpError): 107 | """Error occured during batch operations.""" 108 | 109 | @util.positional(2) 110 | def __init__(self, reason, resp=None, content=None): 111 | self.resp = resp 112 | self.content = content 113 | self.reason = reason 114 | 115 | def __repr__(self): 116 | return '' % (self.resp.status, self.reason) 117 | 118 | __str__ = __repr__ 119 | 120 | 121 | class UnexpectedMethodError(Error): 122 | """Exception raised by RequestMockBuilder on unexpected calls.""" 123 | 124 | @util.positional(1) 125 | def __init__(self, methodId=None): 126 | """Constructor for an UnexpectedMethodError.""" 127 | super(UnexpectedMethodError, self).__init__( 128 | 'Received unexpected call %s' % methodId) 129 | 130 | 131 | class UnexpectedBodyError(Error): 132 | """Exception raised by RequestMockBuilder on unexpected bodies.""" 133 | 134 | def __init__(self, expected, provided): 135 | """Constructor for an UnexpectedMethodError.""" 136 | super(UnexpectedBodyError, self).__init__( 137 | 'Expected: [%s] - Provided: [%s]' % (expected, provided)) 138 | -------------------------------------------------------------------------------- /lib/apiclient/ext/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googleglass/mirror-quickstart-python/e34077bae91657170c305702471f5c249eb1b686/lib/apiclient/ext/__init__.py -------------------------------------------------------------------------------- /lib/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 | -------------------------------------------------------------------------------- /lib/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 gflags 28 | import logging 29 | import urllib 30 | 31 | from errors import HttpError 32 | from oauth2client.anyjson import simplejson 33 | 34 | FLAGS = gflags.FLAGS 35 | 36 | gflags.DEFINE_boolean('dump_request_response', False, 37 | 'Dump all http server requests and responses. ' 38 | ) 39 | 40 | 41 | def _abstract(): 42 | raise NotImplementedError('You need to override this function') 43 | 44 | 45 | class Model(object): 46 | """Model base class. 47 | 48 | All Model classes should implement this interface. 49 | The Model serializes and de-serializes between a wire 50 | format such as JSON and a Python object representation. 51 | """ 52 | 53 | def request(self, headers, path_params, query_params, body_value): 54 | """Updates outgoing requests with a serialized body. 55 | 56 | Args: 57 | headers: dict, request headers 58 | path_params: dict, parameters that appear in the request path 59 | query_params: dict, parameters that appear in the query 60 | body_value: object, the request body as a Python object, which must be 61 | serializable. 62 | Returns: 63 | A tuple of (headers, path_params, query, body) 64 | 65 | headers: dict, request headers 66 | path_params: dict, parameters that appear in the request path 67 | query: string, query part of the request URI 68 | body: string, the body serialized in the desired wire format. 69 | """ 70 | _abstract() 71 | 72 | def response(self, resp, content): 73 | """Convert the response wire format into a Python object. 74 | 75 | Args: 76 | resp: httplib2.Response, the HTTP response headers and status 77 | content: string, the body of the HTTP response 78 | 79 | Returns: 80 | The body de-serialized as a Python object. 81 | 82 | Raises: 83 | apiclient.errors.HttpError if a non 2xx response is received. 84 | """ 85 | _abstract() 86 | 87 | 88 | class BaseModel(Model): 89 | """Base model class. 90 | 91 | Subclasses should provide implementations for the "serialize" and 92 | "deserialize" methods, as well as values for the following class attributes. 93 | 94 | Attributes: 95 | accept: The value to use for the HTTP Accept header. 96 | content_type: The value to use for the HTTP Content-type header. 97 | no_content_response: The value to return when deserializing a 204 "No 98 | Content" response. 99 | alt_param: The value to supply as the "alt" query parameter for requests. 100 | """ 101 | 102 | accept = None 103 | content_type = None 104 | no_content_response = None 105 | alt_param = None 106 | 107 | def _log_request(self, headers, path_params, query, body): 108 | """Logs debugging information about the request if requested.""" 109 | if FLAGS.dump_request_response: 110 | logging.info('--request-start--') 111 | logging.info('-headers-start-') 112 | for h, v in headers.iteritems(): 113 | logging.info('%s: %s', h, v) 114 | logging.info('-headers-end-') 115 | logging.info('-path-parameters-start-') 116 | for h, v in path_params.iteritems(): 117 | logging.info('%s: %s', h, v) 118 | logging.info('-path-parameters-end-') 119 | logging.info('body: %s', body) 120 | logging.info('query: %s', query) 121 | logging.info('--request-end--') 122 | 123 | def request(self, headers, path_params, query_params, body_value): 124 | """Updates outgoing requests with a serialized body. 125 | 126 | Args: 127 | headers: dict, request headers 128 | path_params: dict, parameters that appear in the request path 129 | query_params: dict, parameters that appear in the query 130 | body_value: object, the request body as a Python object, which must be 131 | serializable by simplejson. 132 | Returns: 133 | A tuple of (headers, path_params, query, body) 134 | 135 | headers: dict, request headers 136 | path_params: dict, parameters that appear in the request path 137 | query: string, query part of the request URI 138 | body: string, the body serialized as JSON 139 | """ 140 | query = self._build_query(query_params) 141 | headers['accept'] = self.accept 142 | headers['accept-encoding'] = 'gzip, deflate' 143 | if 'user-agent' in headers: 144 | headers['user-agent'] += ' ' 145 | else: 146 | headers['user-agent'] = '' 147 | headers['user-agent'] += 'google-api-python-client/1.0' 148 | 149 | if body_value is not None: 150 | headers['content-type'] = self.content_type 151 | body_value = self.serialize(body_value) 152 | self._log_request(headers, path_params, query, body_value) 153 | return (headers, path_params, query, body_value) 154 | 155 | def _build_query(self, params): 156 | """Builds a query string. 157 | 158 | Args: 159 | params: dict, the query parameters 160 | 161 | Returns: 162 | The query parameters properly encoded into an HTTP URI query string. 163 | """ 164 | if self.alt_param is not None: 165 | params.update({'alt': self.alt_param}) 166 | astuples = [] 167 | for key, value in params.iteritems(): 168 | if type(value) == type([]): 169 | for x in value: 170 | x = x.encode('utf-8') 171 | astuples.append((key, x)) 172 | else: 173 | if getattr(value, 'encode', False) and callable(value.encode): 174 | value = value.encode('utf-8') 175 | astuples.append((key, value)) 176 | return '?' + urllib.urlencode(astuples) 177 | 178 | def _log_response(self, resp, content): 179 | """Logs debugging information about the response if requested.""" 180 | if FLAGS.dump_request_response: 181 | logging.info('--response-start--') 182 | for h, v in resp.iteritems(): 183 | logging.info('%s: %s', h, v) 184 | if content: 185 | logging.info(content) 186 | logging.info('--response-end--') 187 | 188 | def response(self, resp, content): 189 | """Convert the response wire format into a Python object. 190 | 191 | Args: 192 | resp: httplib2.Response, the HTTP response headers and status 193 | content: string, the body of the HTTP response 194 | 195 | Returns: 196 | The body de-serialized as a Python object. 197 | 198 | Raises: 199 | apiclient.errors.HttpError if a non 2xx response is received. 200 | """ 201 | self._log_response(resp, content) 202 | # Error handling is TBD, for example, do we retry 203 | # for some operation/error combinations? 204 | if resp.status < 300: 205 | if resp.status == 204: 206 | # A 204: No Content response should be treated differently 207 | # to all the other success states 208 | return self.no_content_response 209 | return self.deserialize(content) 210 | else: 211 | logging.debug('Content from bad request was: %s' % content) 212 | raise HttpError(resp, content) 213 | 214 | def serialize(self, body_value): 215 | """Perform the actual Python object serialization. 216 | 217 | Args: 218 | body_value: object, the request body as a Python object. 219 | 220 | Returns: 221 | string, the body in serialized form. 222 | """ 223 | _abstract() 224 | 225 | def deserialize(self, content): 226 | """Perform the actual deserialization from response string to Python 227 | object. 228 | 229 | Args: 230 | content: string, the body of the HTTP response 231 | 232 | Returns: 233 | The body de-serialized as a Python object. 234 | """ 235 | _abstract() 236 | 237 | 238 | class JsonModel(BaseModel): 239 | """Model class for JSON. 240 | 241 | Serializes and de-serializes between JSON and the Python 242 | object representation of HTTP request and response bodies. 243 | """ 244 | accept = 'application/json' 245 | content_type = 'application/json' 246 | alt_param = 'json' 247 | 248 | def __init__(self, data_wrapper=False): 249 | """Construct a JsonModel. 250 | 251 | Args: 252 | data_wrapper: boolean, wrap requests and responses in a data wrapper 253 | """ 254 | self._data_wrapper = data_wrapper 255 | 256 | def serialize(self, body_value): 257 | if (isinstance(body_value, dict) and 'data' not in body_value and 258 | self._data_wrapper): 259 | body_value = {'data': body_value} 260 | return simplejson.dumps(body_value) 261 | 262 | def deserialize(self, content): 263 | body = simplejson.loads(content) 264 | if self._data_wrapper and isinstance(body, dict) and 'data' in body: 265 | body = body['data'] 266 | return body 267 | 268 | @property 269 | def no_content_response(self): 270 | return {} 271 | 272 | 273 | class RawModel(JsonModel): 274 | """Model class for requests that don't return JSON. 275 | 276 | Serializes and de-serializes between JSON and the Python 277 | object representation of HTTP request, and returns the raw bytes 278 | of the response body. 279 | """ 280 | accept = '*/*' 281 | content_type = 'application/json' 282 | alt_param = None 283 | 284 | def deserialize(self, content): 285 | return content 286 | 287 | @property 288 | def no_content_response(self): 289 | return '' 290 | 291 | 292 | class MediaModel(JsonModel): 293 | """Model class for requests that return Media. 294 | 295 | Serializes and de-serializes between JSON and the Python 296 | object representation of HTTP request, and returns the raw bytes 297 | of the response body. 298 | """ 299 | accept = '*/*' 300 | content_type = 'application/json' 301 | alt_param = 'media' 302 | 303 | def deserialize(self, content): 304 | return content 305 | 306 | @property 307 | def no_content_response(self): 308 | return '' 309 | 310 | 311 | class ProtocolBufferModel(BaseModel): 312 | """Model class for protocol buffers. 313 | 314 | Serializes and de-serializes the binary protocol buffer sent in the HTTP 315 | request and response bodies. 316 | """ 317 | accept = 'application/x-protobuf' 318 | content_type = 'application/x-protobuf' 319 | alt_param = 'proto' 320 | 321 | def __init__(self, protocol_buffer): 322 | """Constructs a ProtocolBufferModel. 323 | 324 | The serialzed protocol buffer returned in an HTTP response will be 325 | de-serialized using the given protocol buffer class. 326 | 327 | Args: 328 | protocol_buffer: The protocol buffer class used to de-serialize a 329 | response from the API. 330 | """ 331 | self._protocol_buffer = protocol_buffer 332 | 333 | def serialize(self, body_value): 334 | return body_value.SerializeToString() 335 | 336 | def deserialize(self, content): 337 | return self._protocol_buffer.FromString(content) 338 | 339 | @property 340 | def no_content_response(self): 341 | return self._protocol_buffer() 342 | 343 | 344 | def makepatch(original, modified): 345 | """Create a patch object. 346 | 347 | Some methods support PATCH, an efficient way to send updates to a resource. 348 | This method allows the easy construction of patch bodies by looking at the 349 | differences between a resource before and after it was modified. 350 | 351 | Args: 352 | original: object, the original deserialized resource 353 | modified: object, the modified deserialized resource 354 | Returns: 355 | An object that contains only the changes from original to modified, in a 356 | form suitable to pass to a PATCH method. 357 | 358 | Example usage: 359 | item = service.activities().get(postid=postid, userid=userid).execute() 360 | original = copy.deepcopy(item) 361 | item['object']['content'] = 'This is updated.' 362 | service.activities.patch(postid=postid, userid=userid, 363 | body=makepatch(original, item)).execute() 364 | """ 365 | patch = {} 366 | for key, original_value in original.iteritems(): 367 | modified_value = modified.get(key, None) 368 | if modified_value is None: 369 | # Use None to signal that the element is deleted 370 | patch[key] = None 371 | elif original_value != modified_value: 372 | if type(original_value) == type({}): 373 | # Recursively descend objects 374 | patch[key] = makepatch(original_value, modified_value) 375 | else: 376 | # In the case of simple types or arrays we just replace 377 | patch[key] = modified_value 378 | else: 379 | # Don't add anything to patch if there's no change 380 | pass 381 | for key in modified: 382 | if key not in original: 383 | patch[key] = modified[key] 384 | 385 | return patch 386 | -------------------------------------------------------------------------------- /lib/apiclient/push.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); 2 | # you may not use this file except in compliance with the License. 3 | # You may obtain a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, 9 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | # See the License for the specific language governing permissions and 11 | # limitations under the License. 12 | 13 | """Push notifications support. 14 | 15 | This code is based on experimental APIs and is subject to change. 16 | """ 17 | 18 | __author__ = 'afshar@google.com (Ali Afshar)' 19 | 20 | import binascii 21 | import collections 22 | import os 23 | import urllib 24 | 25 | SUBSCRIBE = 'X-GOOG-SUBSCRIBE' 26 | SUBSCRIPTION_ID = 'X-GOOG-SUBSCRIPTION-ID' 27 | TOPIC_ID = 'X-GOOG-TOPIC-ID' 28 | TOPIC_URI = 'X-GOOG-TOPIC-URI' 29 | CLIENT_TOKEN = 'X-GOOG-CLIENT-TOKEN' 30 | EVENT_TYPE = 'X-GOOG-EVENT-TYPE' 31 | UNSUBSCRIBE = 'X-GOOG-UNSUBSCRIBE' 32 | 33 | 34 | class InvalidSubscriptionRequestError(ValueError): 35 | """The request cannot be subscribed.""" 36 | 37 | 38 | def new_token(): 39 | """Gets a random token for use as a client_token in push notifications. 40 | 41 | Returns: 42 | str, a new random token. 43 | """ 44 | return binascii.hexlify(os.urandom(32)) 45 | 46 | 47 | class Channel(object): 48 | """Base class for channel types.""" 49 | 50 | def __init__(self, channel_type, channel_args): 51 | """Create a new Channel. 52 | 53 | You probably won't need to create this channel manually, since there are 54 | subclassed Channel for each specific type with a more customized set of 55 | arguments to pass. However, you may wish to just create it manually here. 56 | 57 | Args: 58 | channel_type: str, the type of channel. 59 | channel_args: dict, arguments to pass to the channel. 60 | """ 61 | self.channel_type = channel_type 62 | self.channel_args = channel_args 63 | 64 | def as_header_value(self): 65 | """Create the appropriate header for this channel. 66 | 67 | Returns: 68 | str encoded channel description suitable for use as a header. 69 | """ 70 | return '%s?%s' % (self.channel_type, urllib.urlencode(self.channel_args)) 71 | 72 | def write_header(self, headers): 73 | """Write the appropriate subscribe header to a headers dict. 74 | 75 | Args: 76 | headers: dict, headers to add subscribe header to. 77 | """ 78 | headers[SUBSCRIBE] = self.as_header_value() 79 | 80 | 81 | class WebhookChannel(Channel): 82 | """Channel for registering web hook notifications.""" 83 | 84 | def __init__(self, url, app_engine=False): 85 | """Create a new WebhookChannel 86 | 87 | Args: 88 | url: str, URL to post notifications to. 89 | app_engine: bool, default=False, whether the destination for the 90 | notifications is an App Engine application. 91 | """ 92 | super(WebhookChannel, self).__init__( 93 | channel_type='web_hook', 94 | channel_args={ 95 | 'url': url, 96 | 'app_engine': app_engine and 'true' or 'false', 97 | } 98 | ) 99 | 100 | 101 | class Headers(collections.defaultdict): 102 | """Headers for managing subscriptions.""" 103 | 104 | 105 | ALL_HEADERS = set([SUBSCRIBE, SUBSCRIPTION_ID, TOPIC_ID, TOPIC_URI, 106 | CLIENT_TOKEN, EVENT_TYPE, UNSUBSCRIBE]) 107 | 108 | def __init__(self): 109 | """Create a new subscription configuration instance.""" 110 | collections.defaultdict.__init__(self, str) 111 | 112 | def __setitem__(self, key, value): 113 | """Set a header value, ensuring the key is an allowed value. 114 | 115 | Args: 116 | key: str, the header key. 117 | value: str, the header value. 118 | Raises: 119 | ValueError if key is not one of the accepted headers. 120 | """ 121 | normal_key = self._normalize_key(key) 122 | if normal_key not in self.ALL_HEADERS: 123 | raise ValueError('Header name must be one of %s.' % self.ALL_HEADERS) 124 | else: 125 | return collections.defaultdict.__setitem__(self, normal_key, value) 126 | 127 | def __getitem__(self, key): 128 | """Get a header value, normalizing the key case. 129 | 130 | Args: 131 | key: str, the header key. 132 | Returns: 133 | String header value. 134 | Raises: 135 | KeyError if the key is not one of the accepted headers. 136 | """ 137 | normal_key = self._normalize_key(key) 138 | if normal_key not in self.ALL_HEADERS: 139 | raise ValueError('Header name must be one of %s.' % self.ALL_HEADERS) 140 | else: 141 | return collections.defaultdict.__getitem__(self, normal_key) 142 | 143 | def _normalize_key(self, key): 144 | """Normalize a header name for use as a key.""" 145 | return key.upper() 146 | 147 | def items(self): 148 | """Generator for each header.""" 149 | for header in self.ALL_HEADERS: 150 | value = self[header] 151 | if value: 152 | yield header, value 153 | 154 | def write(self, headers): 155 | """Applies the subscription headers. 156 | 157 | Args: 158 | headers: dict of headers to insert values into. 159 | """ 160 | for header, value in self.items(): 161 | headers[header.lower()] = value 162 | 163 | def read(self, headers): 164 | """Read from headers. 165 | 166 | Args: 167 | headers: dict of headers to read from. 168 | """ 169 | for header in self.ALL_HEADERS: 170 | if header.lower() in headers: 171 | self[header] = headers[header.lower()] 172 | 173 | 174 | class Subscription(object): 175 | """Information about a subscription.""" 176 | 177 | def __init__(self): 178 | """Create a new Subscription.""" 179 | self.headers = Headers() 180 | 181 | @classmethod 182 | def for_request(cls, request, channel, client_token=None): 183 | """Creates a subscription and attaches it to a request. 184 | 185 | Args: 186 | request: An http.HttpRequest to modify for making a subscription. 187 | channel: A apiclient.push.Channel describing the subscription to 188 | create. 189 | client_token: (optional) client token to verify the notification. 190 | 191 | Returns: 192 | New subscription object. 193 | """ 194 | subscription = cls.for_channel(channel=channel, client_token=client_token) 195 | subscription.headers.write(request.headers) 196 | if request.method != 'GET': 197 | raise InvalidSubscriptionRequestError( 198 | 'Can only subscribe to requests which are GET.') 199 | request.method = 'POST' 200 | 201 | def _on_response(response, subscription=subscription): 202 | """Called with the response headers. Reads the subscription headers.""" 203 | subscription.headers.read(response) 204 | 205 | request.add_response_callback(_on_response) 206 | return subscription 207 | 208 | @classmethod 209 | def for_channel(cls, channel, client_token=None): 210 | """Alternate constructor to create a subscription from a channel. 211 | 212 | Args: 213 | channel: A apiclient.push.Channel describing the subscription to 214 | create. 215 | client_token: (optional) client token to verify the notification. 216 | 217 | Returns: 218 | New subscription object. 219 | """ 220 | subscription = cls() 221 | channel.write_header(subscription.headers) 222 | if client_token is None: 223 | client_token = new_token() 224 | subscription.headers[SUBSCRIPTION_ID] = new_token() 225 | subscription.headers[CLIENT_TOKEN] = client_token 226 | return subscription 227 | 228 | def verify(self, headers): 229 | """Verifies that a webhook notification has the correct client_token. 230 | 231 | Args: 232 | headers: dict of request headers for a push notification. 233 | 234 | Returns: 235 | Boolean value indicating whether the notification is verified. 236 | """ 237 | new_subscription = Subscription() 238 | new_subscription.headers.read(headers) 239 | return new_subscription.client_token == self.client_token 240 | 241 | @property 242 | def subscribe(self): 243 | """Subscribe header value.""" 244 | return self.headers[SUBSCRIBE] 245 | 246 | @property 247 | def subscription_id(self): 248 | """Subscription ID header value.""" 249 | return self.headers[SUBSCRIPTION_ID] 250 | 251 | @property 252 | def topic_id(self): 253 | """Topic ID header value.""" 254 | return self.headers[TOPIC_ID] 255 | 256 | @property 257 | def topic_uri(self): 258 | """Topic URI header value.""" 259 | return self.headers[TOPIC_URI] 260 | 261 | @property 262 | def client_token(self): 263 | """Client Token header value.""" 264 | return self.headers[CLIENT_TOKEN] 265 | 266 | @property 267 | def event_type(self): 268 | """Event Type header value.""" 269 | return self.headers[EVENT_TYPE] 270 | 271 | @property 272 | def unsubscribe(self): 273 | """Unsuscribe header value.""" 274 | return self.headers[UNSUBSCRIBE] 275 | -------------------------------------------------------------------------------- /lib/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 | -------------------------------------------------------------------------------- /lib/gflags_validators.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (c) 2010, Google Inc. 4 | # All rights reserved. 5 | # 6 | # Redistribution and use in source and binary forms, with or without 7 | # modification, are permitted provided that the following conditions are 8 | # met: 9 | # 10 | # * Redistributions of source code must retain the above copyright 11 | # notice, this list of conditions and the following disclaimer. 12 | # * Redistributions in binary form must reproduce the above 13 | # copyright notice, this list of conditions and the following disclaimer 14 | # in the documentation and/or other materials provided with the 15 | # distribution. 16 | # * Neither the name of Google Inc. nor the names of its 17 | # contributors may be used to endorse or promote products derived from 18 | # this software without specific prior written permission. 19 | # 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | 32 | """Module to enforce different constraints on flags. 33 | 34 | A validator represents an invariant, enforced over a one or more flags. 35 | See 'FLAGS VALIDATORS' in gflags.py's docstring for a usage manual. 36 | """ 37 | 38 | __author__ = 'olexiy@google.com (Olexiy Oryeshko)' 39 | 40 | 41 | class Error(Exception): 42 | """Thrown If validator constraint is not satisfied.""" 43 | 44 | 45 | class Validator(object): 46 | """Base class for flags validators. 47 | 48 | Users should NOT overload these classes, and use gflags.Register... 49 | methods instead. 50 | """ 51 | 52 | # Used to assign each validator an unique insertion_index 53 | validators_count = 0 54 | 55 | def __init__(self, checker, message): 56 | """Constructor to create all validators. 57 | 58 | Args: 59 | checker: function to verify the constraint. 60 | Input of this method varies, see SimpleValidator and 61 | DictionaryValidator for a detailed description. 62 | message: string, error message to be shown to the user 63 | """ 64 | self.checker = checker 65 | self.message = message 66 | Validator.validators_count += 1 67 | # Used to assert validators in the order they were registered (CL/18694236) 68 | self.insertion_index = Validator.validators_count 69 | 70 | def Verify(self, flag_values): 71 | """Verify that constraint is satisfied. 72 | 73 | flags library calls this method to verify Validator's constraint. 74 | Args: 75 | flag_values: gflags.FlagValues, containing all flags 76 | Raises: 77 | Error: if constraint is not satisfied. 78 | """ 79 | param = self._GetInputToCheckerFunction(flag_values) 80 | if not self.checker(param): 81 | raise Error(self.message) 82 | 83 | def GetFlagsNames(self): 84 | """Return the names of the flags checked by this validator. 85 | 86 | Returns: 87 | [string], names of the flags 88 | """ 89 | raise NotImplementedError('This method should be overloaded') 90 | 91 | def PrintFlagsWithValues(self, flag_values): 92 | raise NotImplementedError('This method should be overloaded') 93 | 94 | def _GetInputToCheckerFunction(self, flag_values): 95 | """Given flag values, construct the input to be given to checker. 96 | 97 | Args: 98 | flag_values: gflags.FlagValues, containing all flags. 99 | Returns: 100 | Return type depends on the specific validator. 101 | """ 102 | raise NotImplementedError('This method should be overloaded') 103 | 104 | 105 | class SimpleValidator(Validator): 106 | """Validator behind RegisterValidator() method. 107 | 108 | Validates that a single flag passes its checker function. The checker function 109 | takes the flag value and returns True (if value looks fine) or, if flag value 110 | is not valid, either returns False or raises an Exception.""" 111 | def __init__(self, flag_name, checker, message): 112 | """Constructor. 113 | 114 | Args: 115 | flag_name: string, name of the flag. 116 | checker: function to verify the validator. 117 | input - value of the corresponding flag (string, boolean, etc). 118 | output - Boolean. Must return True if validator constraint is satisfied. 119 | If constraint is not satisfied, it should either return False or 120 | raise Error. 121 | message: string, error message to be shown to the user if validator's 122 | condition is not satisfied 123 | """ 124 | super(SimpleValidator, self).__init__(checker, message) 125 | self.flag_name = flag_name 126 | 127 | def GetFlagsNames(self): 128 | return [self.flag_name] 129 | 130 | def PrintFlagsWithValues(self, flag_values): 131 | return 'flag --%s=%s' % (self.flag_name, flag_values[self.flag_name].value) 132 | 133 | def _GetInputToCheckerFunction(self, flag_values): 134 | """Given flag values, construct the input to be given to checker. 135 | 136 | Args: 137 | flag_values: gflags.FlagValues 138 | Returns: 139 | value of the corresponding flag. 140 | """ 141 | return flag_values[self.flag_name].value 142 | 143 | 144 | class DictionaryValidator(Validator): 145 | """Validator behind RegisterDictionaryValidator method. 146 | 147 | Validates that flag values pass their common checker function. The checker 148 | function takes flag values and returns True (if values look fine) or, 149 | if values are not valid, either returns False or raises an Exception. 150 | """ 151 | def __init__(self, flag_names, checker, message): 152 | """Constructor. 153 | 154 | Args: 155 | flag_names: [string], containing names of the flags used by checker. 156 | checker: function to verify the validator. 157 | input - dictionary, with keys() being flag_names, and value for each 158 | key being the value of the corresponding flag (string, boolean, etc). 159 | output - Boolean. Must return True if validator constraint is satisfied. 160 | If constraint is not satisfied, it should either return False or 161 | raise Error. 162 | message: string, error message to be shown to the user if validator's 163 | condition is not satisfied 164 | """ 165 | super(DictionaryValidator, self).__init__(checker, message) 166 | self.flag_names = flag_names 167 | 168 | def _GetInputToCheckerFunction(self, flag_values): 169 | """Given flag values, construct the input to be given to checker. 170 | 171 | Args: 172 | flag_values: gflags.FlagValues 173 | Returns: 174 | dictionary, with keys() being self.lag_names, and value for each key 175 | being the value of the corresponding flag (string, boolean, etc). 176 | """ 177 | return dict([key, flag_values[key].value] for key in self.flag_names) 178 | 179 | def PrintFlagsWithValues(self, flag_values): 180 | prefix = 'flags ' 181 | flags_with_values = [] 182 | for key in self.flag_names: 183 | flags_with_values.append('%s=%s' % (key, flag_values[key].value)) 184 | return prefix + ', '.join(flags_with_values) 185 | 186 | def GetFlagsNames(self): 187 | return self.flag_names 188 | -------------------------------------------------------------------------------- /lib/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 | -------------------------------------------------------------------------------- /lib/oauth2client/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "1.1" 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 | -------------------------------------------------------------------------------- /lib/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 | -------------------------------------------------------------------------------- /lib/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 | -------------------------------------------------------------------------------- /lib/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 | -------------------------------------------------------------------------------- /lib/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 | -------------------------------------------------------------------------------- /lib/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 | -------------------------------------------------------------------------------- /lib/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 | -------------------------------------------------------------------------------- /lib/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 | -------------------------------------------------------------------------------- /lib/oauth2client/locked_file.py: -------------------------------------------------------------------------------- 1 | # Copyright 2011 Google Inc. All Rights Reserved. 2 | 3 | """Locked file interface that should work on Unix and Windows pythons. 4 | 5 | This module first tries to use fcntl locking to ensure serialized access 6 | to a file, then falls back on a lock file if that is unavialable. 7 | 8 | Usage: 9 | f = LockedFile('filename', 'r+b', 'rb') 10 | f.open_and_lock() 11 | if f.is_locked(): 12 | print 'Acquired filename with r+b mode' 13 | f.file_handle().write('locked data') 14 | else: 15 | print 'Aquired filename with rb mode' 16 | f.unlock_and_close() 17 | """ 18 | 19 | __author__ = 'cache@google.com (David T McWherter)' 20 | 21 | import errno 22 | import logging 23 | import os 24 | import time 25 | 26 | from oauth2client import util 27 | 28 | logger = logging.getLogger(__name__) 29 | 30 | 31 | class CredentialsFileSymbolicLinkError(Exception): 32 | """Credentials files must not be symbolic links.""" 33 | 34 | 35 | class AlreadyLockedException(Exception): 36 | """Trying to lock a file that has already been locked by the LockedFile.""" 37 | pass 38 | 39 | 40 | def validate_file(filename): 41 | if os.path.islink(filename): 42 | raise CredentialsFileSymbolicLinkError( 43 | 'File: %s is a symbolic link.' % filename) 44 | 45 | class _Opener(object): 46 | """Base class for different locking primitives.""" 47 | 48 | def __init__(self, filename, mode, fallback_mode): 49 | """Create an Opener. 50 | 51 | Args: 52 | filename: string, The pathname of the file. 53 | mode: string, The preferred mode to access the file with. 54 | fallback_mode: string, The mode to use if locking fails. 55 | """ 56 | self._locked = False 57 | self._filename = filename 58 | self._mode = mode 59 | self._fallback_mode = fallback_mode 60 | self._fh = None 61 | 62 | def is_locked(self): 63 | """Was the file locked.""" 64 | return self._locked 65 | 66 | def file_handle(self): 67 | """The file handle to the file. Valid only after opened.""" 68 | return self._fh 69 | 70 | def filename(self): 71 | """The filename that is being locked.""" 72 | return self._filename 73 | 74 | def open_and_lock(self, timeout, delay): 75 | """Open the file and lock it. 76 | 77 | Args: 78 | timeout: float, How long to try to lock for. 79 | delay: float, How long to wait between retries. 80 | """ 81 | pass 82 | 83 | def unlock_and_close(self): 84 | """Unlock and close the file.""" 85 | pass 86 | 87 | 88 | class _PosixOpener(_Opener): 89 | """Lock files using Posix advisory lock files.""" 90 | 91 | def open_and_lock(self, timeout, delay): 92 | """Open the file and lock it. 93 | 94 | Tries to create a .lock file next to the file we're trying to open. 95 | 96 | Args: 97 | timeout: float, How long to try to lock for. 98 | delay: float, How long to wait between retries. 99 | 100 | Raises: 101 | AlreadyLockedException: if the lock is already acquired. 102 | IOError: if the open fails. 103 | CredentialsFileSymbolicLinkError if the file is a symbolic link. 104 | """ 105 | if self._locked: 106 | raise AlreadyLockedException('File %s is already locked' % 107 | self._filename) 108 | self._locked = False 109 | 110 | validate_file(self._filename) 111 | try: 112 | self._fh = open(self._filename, self._mode) 113 | except IOError, e: 114 | # If we can't access with _mode, try _fallback_mode and don't lock. 115 | if e.errno == errno.EACCES: 116 | self._fh = open(self._filename, self._fallback_mode) 117 | return 118 | 119 | lock_filename = self._posix_lockfile(self._filename) 120 | start_time = time.time() 121 | while True: 122 | try: 123 | self._lock_fd = os.open(lock_filename, 124 | os.O_CREAT|os.O_EXCL|os.O_RDWR) 125 | self._locked = True 126 | break 127 | 128 | except OSError, e: 129 | if e.errno != errno.EEXIST: 130 | raise 131 | if (time.time() - start_time) >= timeout: 132 | logger.warn('Could not acquire lock %s in %s seconds' % ( 133 | lock_filename, timeout)) 134 | # Close the file and open in fallback_mode. 135 | if self._fh: 136 | self._fh.close() 137 | self._fh = open(self._filename, self._fallback_mode) 138 | return 139 | time.sleep(delay) 140 | 141 | def unlock_and_close(self): 142 | """Unlock a file by removing the .lock file, and close the handle.""" 143 | if self._locked: 144 | lock_filename = self._posix_lockfile(self._filename) 145 | os.close(self._lock_fd) 146 | os.unlink(lock_filename) 147 | self._locked = False 148 | self._lock_fd = None 149 | if self._fh: 150 | self._fh.close() 151 | 152 | def _posix_lockfile(self, filename): 153 | """The name of the lock file to use for posix locking.""" 154 | return '%s.lock' % filename 155 | 156 | 157 | try: 158 | import fcntl 159 | 160 | class _FcntlOpener(_Opener): 161 | """Open, lock, and unlock a file using fcntl.lockf.""" 162 | 163 | def open_and_lock(self, timeout, delay): 164 | """Open the file and lock it. 165 | 166 | Args: 167 | timeout: float, How long to try to lock for. 168 | delay: float, How long to wait between retries 169 | 170 | Raises: 171 | AlreadyLockedException: if the lock is already acquired. 172 | IOError: if the open fails. 173 | CredentialsFileSymbolicLinkError if the file is a symbolic link. 174 | """ 175 | if self._locked: 176 | raise AlreadyLockedException('File %s is already locked' % 177 | self._filename) 178 | start_time = time.time() 179 | 180 | validate_file(self._filename) 181 | try: 182 | self._fh = open(self._filename, self._mode) 183 | except IOError, e: 184 | # If we can't access with _mode, try _fallback_mode and don't lock. 185 | if e.errno == errno.EACCES: 186 | self._fh = open(self._filename, self._fallback_mode) 187 | return 188 | 189 | # We opened in _mode, try to lock the file. 190 | while True: 191 | try: 192 | fcntl.lockf(self._fh.fileno(), fcntl.LOCK_EX) 193 | self._locked = True 194 | return 195 | except IOError, e: 196 | # If not retrying, then just pass on the error. 197 | if timeout == 0: 198 | raise e 199 | if e.errno != errno.EACCES: 200 | raise e 201 | # We could not acquire the lock. Try again. 202 | if (time.time() - start_time) >= timeout: 203 | logger.warn('Could not lock %s in %s seconds' % ( 204 | self._filename, timeout)) 205 | if self._fh: 206 | self._fh.close() 207 | self._fh = open(self._filename, self._fallback_mode) 208 | return 209 | time.sleep(delay) 210 | 211 | def unlock_and_close(self): 212 | """Close and unlock the file using the fcntl.lockf primitive.""" 213 | if self._locked: 214 | fcntl.lockf(self._fh.fileno(), fcntl.LOCK_UN) 215 | self._locked = False 216 | if self._fh: 217 | self._fh.close() 218 | except ImportError: 219 | _FcntlOpener = None 220 | 221 | 222 | try: 223 | import pywintypes 224 | import win32con 225 | import win32file 226 | 227 | class _Win32Opener(_Opener): 228 | """Open, lock, and unlock a file using windows primitives.""" 229 | 230 | # Error #33: 231 | # 'The process cannot access the file because another process' 232 | FILE_IN_USE_ERROR = 33 233 | 234 | # Error #158: 235 | # 'The segment is already unlocked.' 236 | FILE_ALREADY_UNLOCKED_ERROR = 158 237 | 238 | def open_and_lock(self, timeout, delay): 239 | """Open the file and lock it. 240 | 241 | Args: 242 | timeout: float, How long to try to lock for. 243 | delay: float, How long to wait between retries 244 | 245 | Raises: 246 | AlreadyLockedException: if the lock is already acquired. 247 | IOError: if the open fails. 248 | CredentialsFileSymbolicLinkError if the file is a symbolic link. 249 | """ 250 | if self._locked: 251 | raise AlreadyLockedException('File %s is already locked' % 252 | self._filename) 253 | start_time = time.time() 254 | 255 | validate_file(self._filename) 256 | try: 257 | self._fh = open(self._filename, self._mode) 258 | except IOError, e: 259 | # If we can't access with _mode, try _fallback_mode and don't lock. 260 | if e.errno == errno.EACCES: 261 | self._fh = open(self._filename, self._fallback_mode) 262 | return 263 | 264 | # We opened in _mode, try to lock the file. 265 | while True: 266 | try: 267 | hfile = win32file._get_osfhandle(self._fh.fileno()) 268 | win32file.LockFileEx( 269 | hfile, 270 | (win32con.LOCKFILE_FAIL_IMMEDIATELY| 271 | win32con.LOCKFILE_EXCLUSIVE_LOCK), 0, -0x10000, 272 | pywintypes.OVERLAPPED()) 273 | self._locked = True 274 | return 275 | except pywintypes.error, e: 276 | if timeout == 0: 277 | raise e 278 | 279 | # If the error is not that the file is already in use, raise. 280 | if e[0] != _Win32Opener.FILE_IN_USE_ERROR: 281 | raise 282 | 283 | # We could not acquire the lock. Try again. 284 | if (time.time() - start_time) >= timeout: 285 | logger.warn('Could not lock %s in %s seconds' % ( 286 | self._filename, timeout)) 287 | if self._fh: 288 | self._fh.close() 289 | self._fh = open(self._filename, self._fallback_mode) 290 | return 291 | time.sleep(delay) 292 | 293 | def unlock_and_close(self): 294 | """Close and unlock the file using the win32 primitive.""" 295 | if self._locked: 296 | try: 297 | hfile = win32file._get_osfhandle(self._fh.fileno()) 298 | win32file.UnlockFileEx(hfile, 0, -0x10000, pywintypes.OVERLAPPED()) 299 | except pywintypes.error, e: 300 | if e[0] != _Win32Opener.FILE_ALREADY_UNLOCKED_ERROR: 301 | raise 302 | self._locked = False 303 | if self._fh: 304 | self._fh.close() 305 | except ImportError: 306 | _Win32Opener = None 307 | 308 | 309 | class LockedFile(object): 310 | """Represent a file that has exclusive access.""" 311 | 312 | @util.positional(4) 313 | def __init__(self, filename, mode, fallback_mode, use_native_locking=True): 314 | """Construct a LockedFile. 315 | 316 | Args: 317 | filename: string, The path of the file to open. 318 | mode: string, The mode to try to open the file with. 319 | fallback_mode: string, The mode to use if locking fails. 320 | use_native_locking: bool, Whether or not fcntl/win32 locking is used. 321 | """ 322 | opener = None 323 | if not opener and use_native_locking: 324 | if _Win32Opener: 325 | opener = _Win32Opener(filename, mode, fallback_mode) 326 | if _FcntlOpener: 327 | opener = _FcntlOpener(filename, mode, fallback_mode) 328 | 329 | if not opener: 330 | opener = _PosixOpener(filename, mode, fallback_mode) 331 | 332 | self._opener = opener 333 | 334 | def filename(self): 335 | """Return the filename we were constructed with.""" 336 | return self._opener._filename 337 | 338 | def file_handle(self): 339 | """Return the file_handle to the opened file.""" 340 | return self._opener.file_handle() 341 | 342 | def is_locked(self): 343 | """Return whether we successfully locked the file.""" 344 | return self._opener.is_locked() 345 | 346 | def open_and_lock(self, timeout=0, delay=0.05): 347 | """Open the file, trying to lock it. 348 | 349 | Args: 350 | timeout: float, The number of seconds to try to acquire the lock. 351 | delay: float, The number of seconds to wait between retry attempts. 352 | 353 | Raises: 354 | AlreadyLockedException: if the lock is already acquired. 355 | IOError: if the open fails. 356 | """ 357 | self._opener.open_and_lock(timeout, delay) 358 | 359 | def unlock_and_close(self): 360 | """Unlock and close a file.""" 361 | self._opener.unlock_and_close() 362 | -------------------------------------------------------------------------------- /lib/oauth2client/multistore_file.py: -------------------------------------------------------------------------------- 1 | # Copyright 2011 Google Inc. All Rights Reserved. 2 | 3 | """Multi-credential file store with lock support. 4 | 5 | This module implements a JSON credential store where multiple 6 | credentials can be stored in one file. That file supports locking 7 | both in a single process and across processes. 8 | 9 | The credential themselves are keyed off of: 10 | * client_id 11 | * user_agent 12 | * scope 13 | 14 | The format of the stored data is like so: 15 | { 16 | 'file_version': 1, 17 | 'data': [ 18 | { 19 | 'key': { 20 | 'clientId': '', 21 | 'userAgent': '', 22 | 'scope': '' 23 | }, 24 | 'credential': { 25 | # JSON serialized Credentials. 26 | } 27 | } 28 | ] 29 | } 30 | """ 31 | 32 | __author__ = 'jbeda@google.com (Joe Beda)' 33 | 34 | import base64 35 | import errno 36 | import logging 37 | import os 38 | import threading 39 | 40 | from anyjson import simplejson 41 | from oauth2client.client import Storage as BaseStorage 42 | from oauth2client.client import Credentials 43 | from oauth2client import util 44 | from locked_file import LockedFile 45 | 46 | logger = logging.getLogger(__name__) 47 | 48 | # A dict from 'filename'->_MultiStore instances 49 | _multistores = {} 50 | _multistores_lock = threading.Lock() 51 | 52 | 53 | class Error(Exception): 54 | """Base error for this module.""" 55 | pass 56 | 57 | 58 | class NewerCredentialStoreError(Error): 59 | """The credential store is a newer version that supported.""" 60 | pass 61 | 62 | 63 | @util.positional(4) 64 | def get_credential_storage(filename, client_id, user_agent, scope, 65 | warn_on_readonly=True): 66 | """Get a Storage instance for a credential. 67 | 68 | Args: 69 | filename: The JSON file storing a set of credentials 70 | client_id: The client_id for the credential 71 | user_agent: The user agent for the credential 72 | scope: string or iterable of strings, Scope(s) being requested 73 | warn_on_readonly: if True, log a warning if the store is readonly 74 | 75 | Returns: 76 | An object derived from client.Storage for getting/setting the 77 | credential. 78 | """ 79 | # Recreate the legacy key with these specific parameters 80 | key = {'clientId': client_id, 'userAgent': user_agent, 81 | 'scope': util.scopes_to_string(scope)} 82 | return get_credential_storage_custom_key( 83 | filename, key, warn_on_readonly=warn_on_readonly) 84 | 85 | 86 | @util.positional(2) 87 | def get_credential_storage_custom_string_key( 88 | filename, key_string, warn_on_readonly=True): 89 | """Get a Storage instance for a credential using a single string as a key. 90 | 91 | Allows you to provide a string as a custom key that will be used for 92 | credential storage and retrieval. 93 | 94 | Args: 95 | filename: The JSON file storing a set of credentials 96 | key_string: A string to use as the key for storing this credential. 97 | warn_on_readonly: if True, log a warning if the store is readonly 98 | 99 | Returns: 100 | An object derived from client.Storage for getting/setting the 101 | credential. 102 | """ 103 | # Create a key dictionary that can be used 104 | key_dict = {'key': key_string} 105 | return get_credential_storage_custom_key( 106 | filename, key_dict, warn_on_readonly=warn_on_readonly) 107 | 108 | 109 | @util.positional(2) 110 | def get_credential_storage_custom_key( 111 | filename, key_dict, warn_on_readonly=True): 112 | """Get a Storage instance for a credential using a dictionary as a key. 113 | 114 | Allows you to provide a dictionary as a custom key that will be used for 115 | credential storage and retrieval. 116 | 117 | Args: 118 | filename: The JSON file storing a set of credentials 119 | key_dict: A dictionary to use as the key for storing this credential. There 120 | is no ordering of the keys in the dictionary. Logically equivalent 121 | dictionaries will produce equivalent storage keys. 122 | warn_on_readonly: if True, log a warning if the store is readonly 123 | 124 | Returns: 125 | An object derived from client.Storage for getting/setting the 126 | credential. 127 | """ 128 | filename = os.path.expanduser(filename) 129 | _multistores_lock.acquire() 130 | try: 131 | multistore = _multistores.setdefault( 132 | filename, _MultiStore(filename, warn_on_readonly=warn_on_readonly)) 133 | finally: 134 | _multistores_lock.release() 135 | key = util.dict_to_tuple_key(key_dict) 136 | return multistore._get_storage(key) 137 | 138 | 139 | class _MultiStore(object): 140 | """A file backed store for multiple credentials.""" 141 | 142 | @util.positional(2) 143 | def __init__(self, filename, warn_on_readonly=True): 144 | """Initialize the class. 145 | 146 | This will create the file if necessary. 147 | """ 148 | self._file = LockedFile(filename, 'r+b', 'rb') 149 | self._thread_lock = threading.Lock() 150 | self._read_only = False 151 | self._warn_on_readonly = warn_on_readonly 152 | 153 | self._create_file_if_needed() 154 | 155 | # Cache of deserialized store. This is only valid after the 156 | # _MultiStore is locked or _refresh_data_cache is called. This is 157 | # of the form of: 158 | # 159 | # ((key, value), (key, value)...) -> OAuth2Credential 160 | # 161 | # If this is None, then the store hasn't been read yet. 162 | self._data = None 163 | 164 | class _Storage(BaseStorage): 165 | """A Storage object that knows how to read/write a single credential.""" 166 | 167 | def __init__(self, multistore, key): 168 | self._multistore = multistore 169 | self._key = key 170 | 171 | def acquire_lock(self): 172 | """Acquires any lock necessary to access this Storage. 173 | 174 | This lock is not reentrant. 175 | """ 176 | self._multistore._lock() 177 | 178 | def release_lock(self): 179 | """Release the Storage lock. 180 | 181 | Trying to release a lock that isn't held will result in a 182 | RuntimeError. 183 | """ 184 | self._multistore._unlock() 185 | 186 | def locked_get(self): 187 | """Retrieve credential. 188 | 189 | The Storage lock must be held when this is called. 190 | 191 | Returns: 192 | oauth2client.client.Credentials 193 | """ 194 | credential = self._multistore._get_credential(self._key) 195 | if credential: 196 | credential.set_store(self) 197 | return credential 198 | 199 | def locked_put(self, credentials): 200 | """Write a credential. 201 | 202 | The Storage lock must be held when this is called. 203 | 204 | Args: 205 | credentials: Credentials, the credentials to store. 206 | """ 207 | self._multistore._update_credential(self._key, credentials) 208 | 209 | def locked_delete(self): 210 | """Delete a credential. 211 | 212 | The Storage lock must be held when this is called. 213 | 214 | Args: 215 | credentials: Credentials, the credentials to store. 216 | """ 217 | self._multistore._delete_credential(self._key) 218 | 219 | def _create_file_if_needed(self): 220 | """Create an empty file if necessary. 221 | 222 | This method will not initialize the file. Instead it implements a 223 | simple version of "touch" to ensure the file has been created. 224 | """ 225 | if not os.path.exists(self._file.filename()): 226 | old_umask = os.umask(0177) 227 | try: 228 | open(self._file.filename(), 'a+b').close() 229 | finally: 230 | os.umask(old_umask) 231 | 232 | def _lock(self): 233 | """Lock the entire multistore.""" 234 | self._thread_lock.acquire() 235 | self._file.open_and_lock() 236 | if not self._file.is_locked(): 237 | self._read_only = True 238 | if self._warn_on_readonly: 239 | logger.warn('The credentials file (%s) is not writable. Opening in ' 240 | 'read-only mode. Any refreshed credentials will only be ' 241 | 'valid for this run.' % self._file.filename()) 242 | if os.path.getsize(self._file.filename()) == 0: 243 | logger.debug('Initializing empty multistore file') 244 | # The multistore is empty so write out an empty file. 245 | self._data = {} 246 | self._write() 247 | elif not self._read_only or self._data is None: 248 | # Only refresh the data if we are read/write or we haven't 249 | # cached the data yet. If we are readonly, we assume is isn't 250 | # changing out from under us and that we only have to read it 251 | # once. This prevents us from whacking any new access keys that 252 | # we have cached in memory but were unable to write out. 253 | self._refresh_data_cache() 254 | 255 | def _unlock(self): 256 | """Release the lock on the multistore.""" 257 | self._file.unlock_and_close() 258 | self._thread_lock.release() 259 | 260 | def _locked_json_read(self): 261 | """Get the raw content of the multistore file. 262 | 263 | The multistore must be locked when this is called. 264 | 265 | Returns: 266 | The contents of the multistore decoded as JSON. 267 | """ 268 | assert self._thread_lock.locked() 269 | self._file.file_handle().seek(0) 270 | return simplejson.load(self._file.file_handle()) 271 | 272 | def _locked_json_write(self, data): 273 | """Write a JSON serializable data structure to the multistore. 274 | 275 | The multistore must be locked when this is called. 276 | 277 | Args: 278 | data: The data to be serialized and written. 279 | """ 280 | assert self._thread_lock.locked() 281 | if self._read_only: 282 | return 283 | self._file.file_handle().seek(0) 284 | simplejson.dump(data, self._file.file_handle(), sort_keys=True, indent=2) 285 | self._file.file_handle().truncate() 286 | 287 | def _refresh_data_cache(self): 288 | """Refresh the contents of the multistore. 289 | 290 | The multistore must be locked when this is called. 291 | 292 | Raises: 293 | NewerCredentialStoreError: Raised when a newer client has written the 294 | store. 295 | """ 296 | self._data = {} 297 | try: 298 | raw_data = self._locked_json_read() 299 | except Exception: 300 | logger.warn('Credential data store could not be loaded. ' 301 | 'Will ignore and overwrite.') 302 | return 303 | 304 | version = 0 305 | try: 306 | version = raw_data['file_version'] 307 | except Exception: 308 | logger.warn('Missing version for credential data store. It may be ' 309 | 'corrupt or an old version. Overwriting.') 310 | if version > 1: 311 | raise NewerCredentialStoreError( 312 | 'Credential file has file_version of %d. ' 313 | 'Only file_version of 1 is supported.' % version) 314 | 315 | credentials = [] 316 | try: 317 | credentials = raw_data['data'] 318 | except (TypeError, KeyError): 319 | pass 320 | 321 | for cred_entry in credentials: 322 | try: 323 | (key, credential) = self._decode_credential_from_json(cred_entry) 324 | self._data[key] = credential 325 | except: 326 | # If something goes wrong loading a credential, just ignore it 327 | logger.info('Error decoding credential, skipping', exc_info=True) 328 | 329 | def _decode_credential_from_json(self, cred_entry): 330 | """Load a credential from our JSON serialization. 331 | 332 | Args: 333 | cred_entry: A dict entry from the data member of our format 334 | 335 | Returns: 336 | (key, cred) where the key is the key tuple and the cred is the 337 | OAuth2Credential object. 338 | """ 339 | raw_key = cred_entry['key'] 340 | key = util.dict_to_tuple_key(raw_key) 341 | credential = None 342 | credential = Credentials.new_from_json(simplejson.dumps(cred_entry['credential'])) 343 | return (key, credential) 344 | 345 | def _write(self): 346 | """Write the cached data back out. 347 | 348 | The multistore must be locked. 349 | """ 350 | raw_data = {'file_version': 1} 351 | raw_creds = [] 352 | raw_data['data'] = raw_creds 353 | for (cred_key, cred) in self._data.items(): 354 | raw_key = dict(cred_key) 355 | raw_cred = simplejson.loads(cred.to_json()) 356 | raw_creds.append({'key': raw_key, 'credential': raw_cred}) 357 | self._locked_json_write(raw_data) 358 | 359 | def _get_credential(self, key): 360 | """Get a credential from the multistore. 361 | 362 | The multistore must be locked. 363 | 364 | Args: 365 | key: The key used to retrieve the credential 366 | 367 | Returns: 368 | The credential specified or None if not present 369 | """ 370 | return self._data.get(key, None) 371 | 372 | def _update_credential(self, key, cred): 373 | """Update a credential and write the multistore. 374 | 375 | This must be called when the multistore is locked. 376 | 377 | Args: 378 | key: The key used to retrieve the credential 379 | cred: The OAuth2Credential to update/set 380 | """ 381 | self._data[key] = cred 382 | self._write() 383 | 384 | def _delete_credential(self, key): 385 | """Delete a credential and write the multistore. 386 | 387 | This must be called when the multistore is locked. 388 | 389 | Args: 390 | key: The key used to retrieve the credential 391 | """ 392 | try: 393 | del self._data[key] 394 | except KeyError: 395 | pass 396 | self._write() 397 | 398 | def _get_storage(self, key): 399 | """Get a Storage object to get/set a credential. 400 | 401 | This Storage is a 'view' into the multistore. 402 | 403 | Args: 404 | key: The key used to retrieve the credential 405 | 406 | Returns: 407 | A Storage object that can be used to get/set this cred 408 | """ 409 | return self._Storage(self, key) 410 | -------------------------------------------------------------------------------- /lib/oauth2client/tools.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 | """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__ = ['run'] 24 | 25 | 26 | import BaseHTTPServer 27 | import gflags 28 | import socket 29 | import sys 30 | import webbrowser 31 | 32 | from oauth2client.client import FlowExchangeError 33 | from oauth2client.client import OOB_CALLBACK_URN 34 | from oauth2client import util 35 | 36 | try: 37 | from urlparse import parse_qsl 38 | except ImportError: 39 | from cgi import parse_qsl 40 | 41 | 42 | FLAGS = gflags.FLAGS 43 | 44 | gflags.DEFINE_boolean('auth_local_webserver', True, 45 | ('Run a local web server to handle redirects during ' 46 | 'OAuth authorization.')) 47 | 48 | gflags.DEFINE_string('auth_host_name', 'localhost', 49 | ('Host name to use when running a local web server to ' 50 | 'handle redirects during OAuth authorization.')) 51 | 52 | gflags.DEFINE_multi_int('auth_host_port', [8080, 8090], 53 | ('Port to use when running a local web server to ' 54 | 'handle redirects during OAuth authorization.')) 55 | 56 | 57 | class ClientRedirectServer(BaseHTTPServer.HTTPServer): 58 | """A server to handle OAuth 2.0 redirects back to localhost. 59 | 60 | Waits for a single request and parses the query parameters 61 | into query_params and then stops serving. 62 | """ 63 | query_params = {} 64 | 65 | 66 | class ClientRedirectHandler(BaseHTTPServer.BaseHTTPRequestHandler): 67 | """A handler for OAuth 2.0 redirects back to localhost. 68 | 69 | Waits for a single request and parses the query parameters 70 | into the servers query_params and then stops serving. 71 | """ 72 | 73 | def do_GET(s): 74 | """Handle a GET request. 75 | 76 | Parses the query parameters and prints a message 77 | if the flow has completed. Note that we can't detect 78 | if an error occurred. 79 | """ 80 | s.send_response(200) 81 | s.send_header("Content-type", "text/html") 82 | s.end_headers() 83 | query = s.path.split('?', 1)[-1] 84 | query = dict(parse_qsl(query)) 85 | s.server.query_params = query 86 | s.wfile.write("Authentication Status") 87 | s.wfile.write("

The authentication flow has completed.

") 88 | s.wfile.write("") 89 | 90 | def log_message(self, format, *args): 91 | """Do not log messages to stdout while running as command line program.""" 92 | pass 93 | 94 | 95 | @util.positional(2) 96 | def run(flow, storage, http=None): 97 | """Core code for a command-line application. 98 | 99 | The run() function is called from your application and runs through all the 100 | steps to obtain credentials. It takes a Flow argument and attempts to open an 101 | authorization server page in the user's default web browser. The server asks 102 | the user to grant your application access to the user's data. If the user 103 | grants access, the run() function returns new credentials. The new credentials 104 | are also stored in the Storage argument, which updates the file associated 105 | with the Storage object. 106 | 107 | It presumes it is run from a command-line application and supports the 108 | following flags: 109 | 110 | --auth_host_name: Host name to use when running a local web server 111 | to handle redirects during OAuth authorization. 112 | (default: 'localhost') 113 | 114 | --auth_host_port: Port to use when running a local web server to handle 115 | redirects during OAuth authorization.; 116 | repeat this option to specify a list of values 117 | (default: '[8080, 8090]') 118 | (an integer) 119 | 120 | --[no]auth_local_webserver: Run a local web server to handle redirects 121 | during OAuth authorization. 122 | (default: 'true') 123 | 124 | Since it uses flags make sure to initialize the gflags module before calling 125 | run(). 126 | 127 | Args: 128 | flow: Flow, an OAuth 2.0 Flow to step through. 129 | storage: Storage, a Storage to store the credential in. 130 | http: An instance of httplib2.Http.request 131 | or something that acts like it. 132 | 133 | Returns: 134 | Credentials, the obtained credential. 135 | """ 136 | if FLAGS.auth_local_webserver: 137 | success = False 138 | port_number = 0 139 | for port in FLAGS.auth_host_port: 140 | port_number = port 141 | try: 142 | httpd = ClientRedirectServer((FLAGS.auth_host_name, port), 143 | ClientRedirectHandler) 144 | except socket.error, e: 145 | pass 146 | else: 147 | success = True 148 | break 149 | FLAGS.auth_local_webserver = success 150 | if not success: 151 | print 'Failed to start a local webserver listening on either port 8080' 152 | print 'or port 9090. Please check your firewall settings and locally' 153 | print 'running programs that may be blocking or using those ports.' 154 | print 155 | print 'Falling back to --noauth_local_webserver and continuing with', 156 | print 'authorization.' 157 | print 158 | 159 | if FLAGS.auth_local_webserver: 160 | oauth_callback = 'http://%s:%s/' % (FLAGS.auth_host_name, port_number) 161 | else: 162 | oauth_callback = OOB_CALLBACK_URN 163 | flow.redirect_uri = oauth_callback 164 | authorize_url = flow.step1_get_authorize_url() 165 | 166 | if FLAGS.auth_local_webserver: 167 | webbrowser.open(authorize_url, new=1, autoraise=True) 168 | print 'Your browser has been opened to visit:' 169 | print 170 | print ' ' + authorize_url 171 | print 172 | print 'If your browser is on a different machine then exit and re-run this' 173 | print 'application with the command-line parameter ' 174 | print 175 | print ' --noauth_local_webserver' 176 | print 177 | else: 178 | print 'Go to the following link in your browser:' 179 | print 180 | print ' ' + authorize_url 181 | print 182 | 183 | code = None 184 | if FLAGS.auth_local_webserver: 185 | httpd.handle_request() 186 | if 'error' in httpd.query_params: 187 | sys.exit('Authentication request was rejected.') 188 | if 'code' in httpd.query_params: 189 | code = httpd.query_params['code'] 190 | else: 191 | print 'Failed to find "code" in the query parameters of the redirect.' 192 | sys.exit('Try running with --noauth_local_webserver.') 193 | else: 194 | code = raw_input('Enter verification code: ').strip() 195 | 196 | try: 197 | credential = flow.step2_exchange(code, http=http) 198 | except FlowExchangeError, e: 199 | sys.exit('Authentication has failed: %s' % e) 200 | 201 | storage.put(credential) 202 | credential.set_store(storage) 203 | print 'Authentication successful.' 204 | 205 | return credential 206 | -------------------------------------------------------------------------------- /lib/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 | ] 26 | 27 | import gflags 28 | import inspect 29 | import logging 30 | import types 31 | import urllib 32 | import urlparse 33 | 34 | try: 35 | from urlparse import parse_qsl 36 | except ImportError: 37 | from cgi import parse_qsl 38 | 39 | logger = logging.getLogger(__name__) 40 | 41 | FLAGS = gflags.FLAGS 42 | 43 | gflags.DEFINE_enum('positional_parameters_enforcement', 'WARNING', 44 | ['EXCEPTION', 'WARNING', 'IGNORE'], 45 | 'The action when an oauth2client.util.positional declaration is violated.') 46 | 47 | 48 | def positional(max_positional_args): 49 | """A decorator to declare that only the first N arguments my be positional. 50 | 51 | This decorator makes it easy to support Python 3 style key-word only 52 | parameters. For example, in Python 3 it is possible to write: 53 | 54 | def fn(pos1, *, kwonly1=None, kwonly1=None): 55 | ... 56 | 57 | All named parameters after * must be a keyword: 58 | 59 | fn(10, 'kw1', 'kw2') # Raises exception. 60 | fn(10, kwonly1='kw1') # Ok. 61 | 62 | Example: 63 | To define a function like above, do: 64 | 65 | @positional(1) 66 | def fn(pos1, kwonly1=None, kwonly2=None): 67 | ... 68 | 69 | If no default value is provided to a keyword argument, it becomes a required 70 | keyword argument: 71 | 72 | @positional(0) 73 | def fn(required_kw): 74 | ... 75 | 76 | This must be called with the keyword parameter: 77 | 78 | fn() # Raises exception. 79 | fn(10) # Raises exception. 80 | fn(required_kw=10) # Ok. 81 | 82 | When defining instance or class methods always remember to account for 83 | 'self' and 'cls': 84 | 85 | class MyClass(object): 86 | 87 | @positional(2) 88 | def my_method(self, pos1, kwonly1=None): 89 | ... 90 | 91 | @classmethod 92 | @positional(2) 93 | def my_method(cls, pos1, kwonly1=None): 94 | ... 95 | 96 | The positional decorator behavior is controlled by the 97 | --positional_parameters_enforcement flag. The flag may be set to 'EXCEPTION', 98 | 'WARNING' or 'IGNORE' to raise an exception, log a warning, or do nothing, 99 | respectively, if a declaration is violated. 100 | 101 | Args: 102 | max_positional_arguments: Maximum number of positional arguments. All 103 | parameters after the this index must be keyword only. 104 | 105 | Returns: 106 | A decorator that prevents using arguments after max_positional_args from 107 | being used as positional parameters. 108 | 109 | Raises: 110 | TypeError if a key-word only argument is provided as a positional parameter, 111 | but only if the --positional_parameters_enforcement flag is set to 112 | 'EXCEPTION'. 113 | """ 114 | def positional_decorator(wrapped): 115 | def positional_wrapper(*args, **kwargs): 116 | if len(args) > max_positional_args: 117 | plural_s = '' 118 | if max_positional_args != 1: 119 | plural_s = 's' 120 | message = '%s() takes at most %d positional argument%s (%d given)' % ( 121 | wrapped.__name__, max_positional_args, plural_s, len(args)) 122 | if FLAGS.positional_parameters_enforcement == 'EXCEPTION': 123 | raise TypeError(message) 124 | elif FLAGS.positional_parameters_enforcement == 'WARNING': 125 | logger.warning(message) 126 | else: # IGNORE 127 | pass 128 | return wrapped(*args, **kwargs) 129 | return positional_wrapper 130 | 131 | if isinstance(max_positional_args, (int, long)): 132 | return positional_decorator 133 | else: 134 | args, _, _, defaults = inspect.getargspec(max_positional_args) 135 | return positional(len(args) - len(defaults))(max_positional_args) 136 | 137 | 138 | def scopes_to_string(scopes): 139 | """Converts scope value to a string. 140 | 141 | If scopes is a string then it is simply passed through. If scopes is an 142 | iterable then a string is returned that is all the individual scopes 143 | concatenated with spaces. 144 | 145 | Args: 146 | scopes: string or iterable of strings, the scopes. 147 | 148 | Returns: 149 | The scopes formatted as a single string. 150 | """ 151 | if isinstance(scopes, types.StringTypes): 152 | return scopes 153 | else: 154 | return ' '.join(scopes) 155 | 156 | 157 | def dict_to_tuple_key(dictionary): 158 | """Converts a dictionary to a tuple that can be used as an immutable key. 159 | 160 | The resulting key is always sorted so that logically equivalent dictionaries 161 | always produce an identical tuple for a key. 162 | 163 | Args: 164 | dictionary: the dictionary to use as the key. 165 | 166 | Returns: 167 | A tuple representing the dictionary in it's naturally sorted ordering. 168 | """ 169 | return tuple(sorted(dictionary.items())) 170 | 171 | 172 | def _add_query_parameter(url, name, value): 173 | """Adds a query parameter to a url. 174 | 175 | Replaces the current value if it already exists in the URL. 176 | 177 | Args: 178 | url: string, url to add the query parameter to. 179 | name: string, query parameter name. 180 | value: string, query parameter value. 181 | 182 | Returns: 183 | Updated query parameter. Does not update the url if value is None. 184 | """ 185 | if value is None: 186 | return url 187 | else: 188 | parsed = list(urlparse.urlparse(url)) 189 | q = dict(parse_qsl(parsed[4])) 190 | q[name] = value 191 | parsed[4] = urllib.urlencode(q) 192 | return urlparse.urlunparse(parsed) 193 | -------------------------------------------------------------------------------- /lib/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 | -------------------------------------------------------------------------------- /lib/sessions.py: -------------------------------------------------------------------------------- 1 | import Cookie 2 | import datetime 3 | import time 4 | import email.utils 5 | import calendar 6 | import base64 7 | import hashlib 8 | import hmac 9 | import re 10 | import logging 11 | 12 | # Ripped from the Tornado Framework's web.py 13 | # http://github.com/facebook/tornado/commit/39ac6d169a36a54bb1f6b9bf1fdebb5c9da96e09 14 | # 15 | # Tornado is licensed under the Apache Licence, Version 2.0 16 | # (http://www.apache.org/licenses/LICENSE-2.0.html). 17 | # 18 | # Example: 19 | # from vendor.prayls.lilcookies import LilCookies 20 | # cookieutil = LilCookies(self, application_settings['cookie_secret']) 21 | # cookieutil.set_secure_cookie(name = 'mykey', value = 'myvalue', expires_days= 365*100) 22 | # cookieutil.get_secure_cookie(name = 'mykey') 23 | class LilCookies: 24 | 25 | @staticmethod 26 | def _utf8(s): 27 | if isinstance(s, unicode): 28 | return s.encode("utf-8") 29 | assert isinstance(s, str) 30 | return s 31 | 32 | @staticmethod 33 | def _time_independent_equals(a, b): 34 | if len(a) != len(b): 35 | return False 36 | result = 0 37 | for x, y in zip(a, b): 38 | result |= ord(x) ^ ord(y) 39 | return result == 0 40 | 41 | @staticmethod 42 | def _signature_from_secret(cookie_secret, *parts): 43 | """ Takes a secret salt value to create a signature for values in the `parts` param.""" 44 | hash = hmac.new(cookie_secret, digestmod=hashlib.sha1) 45 | for part in parts: hash.update(part) 46 | return hash.hexdigest() 47 | 48 | @staticmethod 49 | def _signed_cookie_value(cookie_secret, name, value): 50 | """ Returns a signed value for use in a cookie. 51 | 52 | This is helpful to have in its own method if you need to re-use this function for other needs. """ 53 | timestamp = str(int(time.time())) 54 | value = base64.b64encode(value) 55 | signature = LilCookies._signature_from_secret(cookie_secret, name, value, timestamp) 56 | return "|".join([value, timestamp, signature]) 57 | 58 | @staticmethod 59 | def _verified_cookie_value(cookie_secret, name, signed_value): 60 | """Returns the un-encrypted value given the signed value if it validates, or None.""" 61 | value = signed_value 62 | if not value: return None 63 | parts = value.split("|") 64 | if len(parts) != 3: return None 65 | signature = LilCookies._signature_from_secret(cookie_secret, name, parts[0], parts[1]) 66 | if not LilCookies._time_independent_equals(parts[2], signature): 67 | logging.warning("Invalid cookie signature %r", value) 68 | return None 69 | timestamp = int(parts[1]) 70 | if timestamp < time.time() - 31 * 86400: 71 | logging.warning("Expired cookie %r", value) 72 | return None 73 | try: 74 | return base64.b64decode(parts[0]) 75 | except: 76 | return None 77 | 78 | def __init__(self, handler, cookie_secret): 79 | """You must specify the cookie_secret to use any of the secure methods. 80 | It should be a long, random sequence of bytes to be used as the HMAC 81 | secret for the signature. 82 | """ 83 | if len(cookie_secret) < 45: 84 | raise ValueError("LilCookies cookie_secret should at least be 45 characters long, but got `%s`" % cookie_secret) 85 | self.handler = handler 86 | self.request = handler.request 87 | self.response = handler.response 88 | self.cookie_secret = cookie_secret 89 | 90 | def cookies(self): 91 | """A dictionary of Cookie.Morsel objects.""" 92 | if not hasattr(self, "_cookies"): 93 | self._cookies = Cookie.BaseCookie() 94 | if "Cookie" in self.request.headers: 95 | try: 96 | self._cookies.load(self.request.headers["Cookie"]) 97 | except: 98 | self.clear_all_cookies() 99 | return self._cookies 100 | 101 | def get_cookie(self, name, default=None): 102 | """Gets the value of the cookie with the given name, else default.""" 103 | if name in self.cookies(): 104 | return self._cookies[name].value 105 | return default 106 | 107 | def set_cookie(self, name, value, domain=None, expires=None, path="/", 108 | expires_days=None, **kwargs): 109 | """Sets the given cookie name/value with the given options. 110 | 111 | Additional keyword arguments are set on the Cookie.Morsel 112 | directly. 113 | See http://docs.python.org/library/cookie.html#morsel-objects 114 | for available attributes. 115 | """ 116 | name = LilCookies._utf8(name) 117 | value = LilCookies._utf8(value) 118 | if re.search(r"[\x00-\x20]", name + value): 119 | # Don't let us accidentally inject bad stuff 120 | raise ValueError("Invalid cookie %r: %r" % (name, value)) 121 | if not hasattr(self, "_new_cookies"): 122 | self._new_cookies = [] 123 | new_cookie = Cookie.BaseCookie() 124 | self._new_cookies.append(new_cookie) 125 | new_cookie[name] = value 126 | if domain: 127 | new_cookie[name]["domain"] = domain 128 | if expires_days is not None and not expires: 129 | expires = datetime.datetime.utcnow() + datetime.timedelta(days=expires_days) 130 | if expires: 131 | timestamp = calendar.timegm(expires.utctimetuple()) 132 | new_cookie[name]["expires"] = email.utils.formatdate( 133 | timestamp, localtime=False, usegmt=True) 134 | if path: 135 | new_cookie[name]["path"] = path 136 | for k, v in kwargs.iteritems(): 137 | new_cookie[name][k] = v 138 | 139 | # The 2 lines below were not in Tornado. Instead, they output all their cookies to the headers at once before a response flush. 140 | for vals in new_cookie.values(): 141 | self.response.headers.add('Set-Cookie', vals.OutputString(None)) 142 | 143 | def clear_cookie(self, name, path="/", domain=None): 144 | """Deletes the cookie with the given name.""" 145 | expires = datetime.datetime.utcnow() - datetime.timedelta(days=365) 146 | self.set_cookie(name, value="", path=path, expires=expires, 147 | domain=domain) 148 | 149 | def clear_all_cookies(self): 150 | """Deletes all the cookies the user sent with this request.""" 151 | for name in self.cookies().iterkeys(): 152 | self.clear_cookie(name) 153 | 154 | def set_secure_cookie(self, name, value, expires_days=30, **kwargs): 155 | """Signs and timestamps a cookie so it cannot be forged. 156 | 157 | To read a cookie set with this method, use get_secure_cookie(). 158 | """ 159 | value = LilCookies._signed_cookie_value(self.cookie_secret, name, value) 160 | self.set_cookie(name, value, expires_days=expires_days, **kwargs) 161 | 162 | def get_secure_cookie(self, name, value=None): 163 | """Returns the given signed cookie if it validates, or None.""" 164 | if value is None: value = self.get_cookie(name) 165 | return LilCookies._verified_cookie_value(self.cookie_secret, name, value) 166 | 167 | def _cookie_signature(self, *parts): 168 | return LilCookies._signature_from_secret(self.cookie_secret) 169 | -------------------------------------------------------------------------------- /lib/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 | -------------------------------------------------------------------------------- /main.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 | """RequestHandlers for starter project.""" 16 | 17 | __author__ = 'alainv@google.com (Alain Vongsouvanh)' 18 | 19 | 20 | # Add the library location to the path 21 | import sys 22 | sys.path.insert(0, 'lib') 23 | 24 | import webapp2 25 | 26 | from attachmentproxy.handler import ATTACHMENT_PROXY_ROUTES 27 | from main_handler import MAIN_ROUTES 28 | from notify.handler import NOTIFY_ROUTES 29 | from oauth.handler import OAUTH_ROUTES 30 | from signout.handler import SIGNOUT_ROUTES 31 | 32 | 33 | ROUTES = ( 34 | ATTACHMENT_PROXY_ROUTES + MAIN_ROUTES + NOTIFY_ROUTES + OAUTH_ROUTES + 35 | SIGNOUT_ROUTES) 36 | 37 | 38 | app = webapp2.WSGIApplication(ROUTES) 39 | -------------------------------------------------------------------------------- /main_handler.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 | """Request Handler for /main endpoint.""" 16 | 17 | __author__ = 'alainv@google.com (Alain Vongsouvanh)' 18 | 19 | 20 | import io 21 | import jinja2 22 | import logging 23 | import os 24 | import webapp2 25 | 26 | from google.appengine.api import memcache 27 | from google.appengine.api import urlfetch 28 | 29 | import httplib2 30 | from apiclient import errors 31 | from apiclient.http import MediaIoBaseUpload 32 | from apiclient.http import BatchHttpRequest 33 | from oauth2client.appengine import StorageByKeyName 34 | 35 | from model import Credentials 36 | import util 37 | 38 | 39 | jinja_environment = jinja2.Environment( 40 | loader=jinja2.FileSystemLoader(os.path.dirname(__file__))) 41 | 42 | 43 | PAGINATED_HTML = """ 44 |
45 |

Did you know...?

46 |

Cats are solar-powered. The time they spend 47 | napping in direct sunlight is necessary to regenerate their internal 48 | batteries. Cats that do not receive sufficient charge may exhibit the 49 | following symptoms: lethargy, irritability, and disdainful glares. Cats 50 | will reactivate on their own automatically after a complete charge 51 | cycle; it is recommended that they be left undisturbed during this 52 | process to maximize your enjoyment of your cat.


53 | For more cat maintenance tips, tap to view the website!

54 |
55 | """ 56 | 57 | 58 | class _BatchCallback(object): 59 | """Class used to track batch request responses.""" 60 | 61 | def __init__(self): 62 | """Initialize a new _BatchCallback object.""" 63 | self.success = 0 64 | self.failure = 0 65 | 66 | def callback(self, request_id, response, exception): 67 | """Method called on each HTTP Response from a batch request. 68 | 69 | For more information, see 70 | https://developers.google.com/api-client-library/python/guide/batch 71 | """ 72 | if exception is None: 73 | self.success += 1 74 | else: 75 | self.failure += 1 76 | logging.error( 77 | 'Failed to insert item for user %s: %s', request_id, exception) 78 | 79 | 80 | class MainHandler(webapp2.RequestHandler): 81 | """Request Handler for the main endpoint.""" 82 | 83 | def _render_template(self, message=None): 84 | """Render the main page template.""" 85 | template_values = {'userId': self.userid} 86 | if message: 87 | template_values['message'] = message 88 | # self.mirror_service is initialized in util.auth_required. 89 | try: 90 | template_values['contact'] = self.mirror_service.contacts().get( 91 | id='python-quick-start').execute() 92 | except errors.HttpError: 93 | logging.info('Unable to find Python Quick Start contact.') 94 | 95 | timeline_items = self.mirror_service.timeline().list(maxResults=3).execute() 96 | template_values['timelineItems'] = timeline_items.get('items', []) 97 | 98 | subscriptions = self.mirror_service.subscriptions().list().execute() 99 | for subscription in subscriptions.get('items', []): 100 | collection = subscription.get('collection') 101 | if collection == 'timeline': 102 | template_values['timelineSubscriptionExists'] = True 103 | elif collection == 'locations': 104 | template_values['locationSubscriptionExists'] = True 105 | 106 | template = jinja_environment.get_template('templates/index.html') 107 | self.response.out.write(template.render(template_values)) 108 | 109 | @util.auth_required 110 | def get(self): 111 | """Render the main page.""" 112 | # Get the flash message and delete it. 113 | message = memcache.get(key=self.userid) 114 | memcache.delete(key=self.userid) 115 | self._render_template(message) 116 | 117 | @util.auth_required 118 | def post(self): 119 | """Execute the request and render the template.""" 120 | operation = self.request.get('operation') 121 | # Dict of operations to easily map keys to methods. 122 | operations = { 123 | 'insertSubscription': self._insert_subscription, 124 | 'deleteSubscription': self._delete_subscription, 125 | 'insertItem': self._insert_item, 126 | 'insertPaginatedItem': self._insert_paginated_item, 127 | 'insertItemWithAction': self._insert_item_with_action, 128 | 'insertItemAllUsers': self._insert_item_all_users, 129 | 'insertContact': self._insert_contact, 130 | 'deleteContact': self._delete_contact, 131 | 'deleteTimelineItem': self._delete_timeline_item 132 | } 133 | if operation in operations: 134 | message = operations[operation]() 135 | else: 136 | message = "I don't know how to " + operation 137 | # Store the flash message for 5 seconds. 138 | memcache.set(key=self.userid, value=message, time=5) 139 | self.redirect('/') 140 | 141 | def _insert_subscription(self): 142 | """Subscribe the app.""" 143 | # self.userid is initialized in util.auth_required. 144 | body = { 145 | 'collection': self.request.get('collection', 'timeline'), 146 | 'userToken': self.userid, 147 | 'callbackUrl': util.get_full_url(self, '/notify') 148 | } 149 | # self.mirror_service is initialized in util.auth_required. 150 | self.mirror_service.subscriptions().insert(body=body).execute() 151 | return 'Application is now subscribed to updates.' 152 | 153 | def _delete_subscription(self): 154 | """Unsubscribe from notifications.""" 155 | collection = self.request.get('subscriptionId') 156 | self.mirror_service.subscriptions().delete(id=collection).execute() 157 | return 'Application has been unsubscribed.' 158 | 159 | def _insert_item(self): 160 | """Insert a timeline item.""" 161 | logging.info('Inserting timeline item') 162 | body = { 163 | 'notification': {'level': 'DEFAULT'} 164 | } 165 | if self.request.get('html') == 'on': 166 | body['html'] = [self.request.get('message')] 167 | else: 168 | body['text'] = self.request.get('message') 169 | 170 | media_link = self.request.get('imageUrl') 171 | if media_link: 172 | if media_link.startswith('/'): 173 | media_link = util.get_full_url(self, media_link) 174 | resp = urlfetch.fetch(media_link, deadline=20) 175 | media = MediaIoBaseUpload( 176 | io.BytesIO(resp.content), mimetype='image/jpeg', resumable=True) 177 | else: 178 | media = None 179 | 180 | # self.mirror_service is initialized in util.auth_required. 181 | self.mirror_service.timeline().insert(body=body, media_body=media).execute() 182 | return 'A timeline item has been inserted.' 183 | 184 | def _insert_paginated_item(self): 185 | """Insert a paginated timeline item.""" 186 | logging.info('Inserting paginated timeline item') 187 | body = { 188 | 'html': PAGINATED_HTML, 189 | 'notification': {'level': 'DEFAULT'}, 190 | 'menuItems': [{ 191 | 'action': 'OPEN_URI', 192 | 'payload': 'https://www.google.com/search?q=cat+maintenance+tips' 193 | }] 194 | } 195 | # self.mirror_service is initialized in util.auth_required. 196 | self.mirror_service.timeline().insert(body=body).execute() 197 | return 'A timeline item has been inserted.' 198 | 199 | def _insert_item_with_action(self): 200 | """Insert a timeline item user can reply to.""" 201 | logging.info('Inserting timeline item') 202 | body = { 203 | 'creator': { 204 | 'displayName': 'Python Starter Project', 205 | 'id': 'PYTHON_STARTER_PROJECT' 206 | }, 207 | 'text': 'Tell me what you had for lunch :)', 208 | 'notification': {'level': 'DEFAULT'}, 209 | 'menuItems': [{'action': 'REPLY'}] 210 | } 211 | # self.mirror_service is initialized in util.auth_required. 212 | self.mirror_service.timeline().insert(body=body).execute() 213 | return 'A timeline item with action has been inserted.' 214 | 215 | def _insert_item_all_users(self): 216 | """Insert a timeline item to all authorized users.""" 217 | logging.info('Inserting timeline item to all users') 218 | users = Credentials.all() 219 | total_users = users.count() 220 | 221 | if total_users > 10: 222 | return 'Total user count is %d. Aborting broadcast to save your quota' % ( 223 | total_users) 224 | body = { 225 | 'text': 'Hello Everyone!', 226 | 'notification': {'level': 'DEFAULT'} 227 | } 228 | 229 | batch_responses = _BatchCallback() 230 | batch = BatchHttpRequest(callback=batch_responses.callback) 231 | for user in users: 232 | creds = StorageByKeyName( 233 | Credentials, user.key().name(), 'credentials').get() 234 | mirror_service = util.create_service('mirror', 'v1', creds) 235 | batch.add( 236 | mirror_service.timeline().insert(body=body), 237 | request_id=user.key().name()) 238 | 239 | batch.execute(httplib2.Http()) 240 | return 'Successfully sent cards to %d users (%d failed).' % ( 241 | batch_responses.success, batch_responses.failure) 242 | 243 | def _insert_contact(self): 244 | """Insert a new Contact.""" 245 | logging.info('Inserting contact') 246 | id = self.request.get('id') 247 | name = self.request.get('name') 248 | image_url = self.request.get('imageUrl') 249 | if not name or not image_url: 250 | return 'Must specify imageUrl and name to insert contact' 251 | else: 252 | if image_url.startswith('/'): 253 | image_url = util.get_full_url(self, image_url) 254 | body = { 255 | 'id': id, 256 | 'displayName': name, 257 | 'imageUrls': [image_url], 258 | 'acceptCommands': [{ 'type': 'TAKE_A_NOTE' }] 259 | } 260 | # self.mirror_service is initialized in util.auth_required. 261 | self.mirror_service.contacts().insert(body=body).execute() 262 | return 'Inserted contact: ' + name 263 | 264 | def _delete_contact(self): 265 | """Delete a Contact.""" 266 | # self.mirror_service is initialized in util.auth_required. 267 | self.mirror_service.contacts().delete( 268 | id=self.request.get('id')).execute() 269 | return 'Contact has been deleted.' 270 | 271 | def _delete_timeline_item(self): 272 | """Delete a Timeline Item.""" 273 | logging.info('Deleting timeline item') 274 | # self.mirror_service is initialized in util.auth_required. 275 | self.mirror_service.timeline().delete(id=self.request.get('itemId')).execute() 276 | return 'A timeline item has been deleted.' 277 | 278 | 279 | 280 | MAIN_ROUTES = [ 281 | ('/', MainHandler) 282 | ] 283 | -------------------------------------------------------------------------------- /model.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 | """Datastore models for Starter Project""" 16 | 17 | __author__ = 'alainv@google.com (Alain Vongsouvanh)' 18 | 19 | 20 | from google.appengine.ext import db 21 | 22 | from oauth2client.appengine import CredentialsProperty 23 | 24 | 25 | class Credentials(db.Model): 26 | """Datastore entity for storing OAuth2.0 credentials. 27 | 28 | The CredentialsProperty is provided by the Google API Python Client, and is 29 | used by the Storage classes to store OAuth 2.0 credentials in the data store. 30 | """ 31 | credentials = CredentialsProperty() 32 | -------------------------------------------------------------------------------- /notify/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googleglass/mirror-quickstart-python/e34077bae91657170c305702471f5c249eb1b686/notify/__init__.py -------------------------------------------------------------------------------- /notify/handler.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 | """Request Handler for /notify endpoint.""" 16 | 17 | __author__ = 'alainv@google.com (Alain Vongsouvanh)' 18 | 19 | 20 | import io 21 | import json 22 | import logging 23 | import webapp2 24 | 25 | from random import choice 26 | from apiclient.http import MediaIoBaseUpload 27 | from oauth2client.appengine import StorageByKeyName 28 | 29 | from model import Credentials 30 | import util 31 | 32 | 33 | CAT_UTTERANCES = [ 34 | "Purr...", 35 | "Hisss... scratch...", 36 | "Meow..." 37 | ] 38 | 39 | 40 | class NotifyHandler(webapp2.RequestHandler): 41 | """Request Handler for notification pings.""" 42 | 43 | def post(self): 44 | """Handles notification pings.""" 45 | logging.info('Got a notification with payload %s', self.request.body) 46 | data = json.loads(self.request.body) 47 | userid = data['userToken'] 48 | # TODO: Check that the userToken is a valid userToken. 49 | self.mirror_service = util.create_service( 50 | 'mirror', 'v1', 51 | StorageByKeyName(Credentials, userid, 'credentials').get()) 52 | if data.get('collection') == 'locations': 53 | self._handle_locations_notification(data) 54 | elif data.get('collection') == 'timeline': 55 | self._handle_timeline_notification(data) 56 | 57 | def _handle_locations_notification(self, data): 58 | """Handle locations notification.""" 59 | location = self.mirror_service.locations().get(id=data['itemId']).execute() 60 | text = 'Python Quick Start says you are at %s by %s.' % \ 61 | (location.get('latitude'), location.get('longitude')) 62 | body = { 63 | 'text': text, 64 | 'location': location, 65 | 'menuItems': [{'action': 'NAVIGATE'}], 66 | 'notification': {'level': 'DEFAULT'} 67 | } 68 | self.mirror_service.timeline().insert(body=body).execute() 69 | 70 | def _handle_timeline_notification(self, data): 71 | """Handle timeline notification.""" 72 | for user_action in data.get('userActions', []): 73 | # Fetch the timeline item. 74 | item = self.mirror_service.timeline().get(id=data['itemId']).execute() 75 | 76 | if user_action.get('type') == 'SHARE': 77 | # Create a dictionary with just the attributes that we want to patch. 78 | body = { 79 | 'text': 'Python Quick Start got your photo! %s' % item.get('text', '') 80 | } 81 | 82 | # Patch the item. Notice that since we retrieved the entire item above 83 | # in order to access the caption, we could have just changed the text 84 | # in place and used the update method, but we wanted to illustrate the 85 | # patch method here. 86 | self.mirror_service.timeline().patch( 87 | id=data['itemId'], body=body).execute() 88 | 89 | # Only handle the first successful action. 90 | break 91 | elif user_action.get('type') == 'LAUNCH': 92 | # Grab the spoken text from the timeline card and update the card with 93 | # an HTML response (deleting the text as well). 94 | note_text = item.get('text', ''); 95 | utterance = choice(CAT_UTTERANCES) 96 | 97 | item['text'] = None 98 | item['html'] = ("
" + 99 | "

" + 100 | "Oh, did you say " + note_text + "? " + utterance + "

" + 101 | "

Python Quick Start

") 102 | item['menuItems'] = [{ 'action': 'DELETE' }]; 103 | 104 | self.mirror_service.timeline().update( 105 | id=item['id'], body=item).execute() 106 | else: 107 | logging.info( 108 | "I don't know what to do with this notification: %s", user_action) 109 | 110 | 111 | NOTIFY_ROUTES = [ 112 | ('/notify', NotifyHandler) 113 | ] 114 | -------------------------------------------------------------------------------- /oauth/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googleglass/mirror-quickstart-python/e34077bae91657170c305702471f5c249eb1b686/oauth/__init__.py -------------------------------------------------------------------------------- /oauth/handler.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 | """OAuth 2.0 handlers.""" 16 | 17 | __author__ = 'alainv@google.com (Alain Vongsouvanh)' 18 | 19 | 20 | import logging 21 | import webapp2 22 | from urlparse import urlparse 23 | 24 | from oauth2client.appengine import StorageByKeyName 25 | from oauth2client.client import flow_from_clientsecrets 26 | from oauth2client.client import FlowExchangeError 27 | 28 | from model import Credentials 29 | import util 30 | 31 | 32 | SCOPES = ('https://www.googleapis.com/auth/glass.timeline ' 33 | 'https://www.googleapis.com/auth/glass.location ' 34 | 'https://www.googleapis.com/auth/userinfo.profile') 35 | 36 | 37 | class OAuthBaseRequestHandler(webapp2.RequestHandler): 38 | """Base request handler for OAuth 2.0 flow.""" 39 | 40 | def create_oauth_flow(self): 41 | """Create OAuth2.0 flow controller.""" 42 | flow = flow_from_clientsecrets('client_secrets.json', scope=SCOPES) 43 | # Dynamically set the redirect_uri based on the request URL. This is 44 | # extremely convenient for debugging to an alternative host without manually 45 | # setting the redirect URI. 46 | pr = urlparse(self.request.url) 47 | flow.redirect_uri = '%s://%s/oauth2callback' % (pr.scheme, pr.netloc) 48 | return flow 49 | 50 | 51 | class OAuthCodeRequestHandler(OAuthBaseRequestHandler): 52 | """Request handler for OAuth 2.0 auth request.""" 53 | 54 | def get(self): 55 | flow = self.create_oauth_flow() 56 | flow.params['approval_prompt'] = 'force' 57 | # Create the redirect URI by performing step 1 of the OAuth 2.0 web server 58 | # flow. 59 | uri = flow.step1_get_authorize_url() 60 | # Perform the redirect. 61 | self.redirect(str(uri)) 62 | 63 | 64 | class OAuthCodeExchangeHandler(OAuthBaseRequestHandler): 65 | """Request handler for OAuth 2.0 code exchange.""" 66 | 67 | def get(self): 68 | """Handle code exchange.""" 69 | code = self.request.get('code') 70 | if not code: 71 | # TODO: Display error. 72 | return None 73 | oauth_flow = self.create_oauth_flow() 74 | 75 | # Perform the exchange of the code. If there is a failure with exchanging 76 | # the code, return None. 77 | try: 78 | creds = oauth_flow.step2_exchange(code) 79 | except FlowExchangeError: 80 | # TODO: Display error. 81 | return None 82 | 83 | users_service = util.create_service('oauth2', 'v2', creds) 84 | # TODO: Check for errors. 85 | user = users_service.userinfo().get().execute() 86 | 87 | userid = user.get('id') 88 | 89 | # Store the credentials in the data store using the userid as the key. 90 | # TODO: Hash the userid the same way the userToken is. 91 | StorageByKeyName(Credentials, userid, 'credentials').put(creds) 92 | logging.info('Successfully stored credentials for user: %s', userid) 93 | util.store_userid(self, userid) 94 | 95 | self._perform_post_auth_tasks(userid, creds) 96 | self.redirect('/') 97 | 98 | def _perform_post_auth_tasks(self, userid, creds): 99 | """Perform commong post authorization tasks. 100 | 101 | Subscribes the service to notifications for the user and add one sharing 102 | contact. 103 | 104 | Args: 105 | userid: ID of the current user. 106 | creds: Credentials for the current user. 107 | """ 108 | mirror_service = util.create_service('mirror', 'v1', creds) 109 | hostname = util.get_full_url(self, '') 110 | 111 | # Only do the post auth tasks when deployed. 112 | if hostname.startswith('https://'): 113 | # Insert a subscription. 114 | subscription_body = { 115 | 'collection': 'timeline', 116 | # TODO: hash the userToken. 117 | 'userToken': userid, 118 | 'callbackUrl': util.get_full_url(self, '/notify') 119 | } 120 | mirror_service.subscriptions().insert(body=subscription_body).execute() 121 | 122 | # Insert a sharing contact. 123 | contact_body = { 124 | 'id': 'python-quick-start', 125 | 'displayName': 'Python Quick Start', 126 | 'imageUrls': [util.get_full_url(self, '/static/images/python.png')], 127 | 'acceptCommands': [{ 'type': 'TAKE_A_NOTE' }] 128 | } 129 | mirror_service.contacts().insert(body=contact_body).execute() 130 | else: 131 | logging.info('Post auth tasks are not supported on staging.') 132 | 133 | # Insert welcome message. 134 | timeline_item_body = { 135 | 'text': 'Welcome to the Python Quick Start', 136 | 'notification': { 137 | 'level': 'DEFAULT' 138 | } 139 | } 140 | mirror_service.timeline().insert(body=timeline_item_body).execute() 141 | 142 | 143 | OAUTH_ROUTES = [ 144 | ('/auth', OAuthCodeRequestHandler), 145 | ('/oauth2callback', OAuthCodeExchangeHandler) 146 | ] 147 | -------------------------------------------------------------------------------- /signout/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googleglass/mirror-quickstart-python/e34077bae91657170c305702471f5c249eb1b686/signout/__init__.py -------------------------------------------------------------------------------- /signout/handler.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 | """Request Handler for /signout endpoint.""" 16 | 17 | __author__ = 'alainv@google.com (Alain Vongsouvanh)' 18 | 19 | 20 | import webapp2 21 | 22 | from google.appengine.api import urlfetch 23 | 24 | from model import Credentials 25 | import util 26 | 27 | 28 | OAUTH2_REVOKE_ENDPOINT = 'https://accounts.google.com/o/oauth2/revoke?token=%s' 29 | 30 | 31 | class SignoutHandler(webapp2.RequestHandler): 32 | """Request Handler for the signout endpoint.""" 33 | 34 | @util.auth_required 35 | def post(self): 36 | """Delete the user's credentials from the datastore.""" 37 | urlfetch.fetch(OAUTH2_REVOKE_ENDPOINT % self.credentials.refresh_token) 38 | util.store_userid(self, '') 39 | credentials_entity = Credentials.get_by_key_name(self.userid) 40 | if credentials_entity: 41 | credentials_entity.delete() 42 | self.redirect('/') 43 | 44 | 45 | SIGNOUT_ROUTES = [ 46 | ('/signout', SignoutHandler) 47 | ] 48 | -------------------------------------------------------------------------------- /static/bootstrap/css/bootstrap-responsive.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Responsive v2.3.1 3 | * 4 | * Copyright 2012 Twitter, Inc 5 | * Licensed under the Apache License v2.0 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Designed and built with all the love in the world @twitter by @mdo and @fat. 9 | */.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;line-height:0;content:""}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}@-ms-viewport{width:device-width}.hidden{display:none;visibility:hidden}.visible-phone{display:none!important}.visible-tablet{display:none!important}.hidden-desktop{display:none!important}.visible-desktop{display:inherit!important}@media(min-width:768px) and (max-width:979px){.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}.visible-tablet{display:inherit!important}.hidden-tablet{display:none!important}}@media(max-width:767px){.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}.visible-phone{display:inherit!important}.hidden-phone{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:inherit!important}.hidden-print{display:none!important}}@media(min-width:1200px){.row{margin-left:-30px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:30px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:1170px}.span12{width:1170px}.span11{width:1070px}.span10{width:970px}.span9{width:870px}.span8{width:770px}.span7{width:670px}.span6{width:570px}.span5{width:470px}.span4{width:370px}.span3{width:270px}.span2{width:170px}.span1{width:70px}.offset12{margin-left:1230px}.offset11{margin-left:1130px}.offset10{margin-left:1030px}.offset9{margin-left:930px}.offset8{margin-left:830px}.offset7{margin-left:730px}.offset6{margin-left:630px}.offset5{margin-left:530px}.offset4{margin-left:430px}.offset3{margin-left:330px}.offset2{margin-left:230px}.offset1{margin-left:130px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.564102564102564%;*margin-left:2.5109110747408616%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.564102564102564%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.45299145299145%;*width:91.39979996362975%}.row-fluid .span10{width:82.90598290598291%;*width:82.8527914166212%}.row-fluid .span9{width:74.35897435897436%;*width:74.30578286961266%}.row-fluid .span8{width:65.81196581196582%;*width:65.75877432260411%}.row-fluid .span7{width:57.26495726495726%;*width:57.21176577559556%}.row-fluid .span6{width:48.717948717948715%;*width:48.664757228587014%}.row-fluid .span5{width:40.17094017094017%;*width:40.11774868157847%}.row-fluid .span4{width:31.623931623931625%;*width:31.570740134569924%}.row-fluid .span3{width:23.076923076923077%;*width:23.023731587561375%}.row-fluid .span2{width:14.52991452991453%;*width:14.476723040552828%}.row-fluid .span1{width:5.982905982905983%;*width:5.929714493544281%}.row-fluid .offset12{margin-left:105.12820512820512%;*margin-left:105.02182214948171%}.row-fluid .offset12:first-child{margin-left:102.56410256410257%;*margin-left:102.45771958537915%}.row-fluid .offset11{margin-left:96.58119658119658%;*margin-left:96.47481360247316%}.row-fluid .offset11:first-child{margin-left:94.01709401709402%;*margin-left:93.91071103837061%}.row-fluid .offset10{margin-left:88.03418803418803%;*margin-left:87.92780505546462%}.row-fluid .offset10:first-child{margin-left:85.47008547008548%;*margin-left:85.36370249136206%}.row-fluid .offset9{margin-left:79.48717948717949%;*margin-left:79.38079650845607%}.row-fluid .offset9:first-child{margin-left:76.92307692307693%;*margin-left:76.81669394435352%}.row-fluid .offset8{margin-left:70.94017094017094%;*margin-left:70.83378796144753%}.row-fluid .offset8:first-child{margin-left:68.37606837606839%;*margin-left:68.26968539734497%}.row-fluid .offset7{margin-left:62.393162393162385%;*margin-left:62.28677941443899%}.row-fluid .offset7:first-child{margin-left:59.82905982905982%;*margin-left:59.72267685033642%}.row-fluid .offset6{margin-left:53.84615384615384%;*margin-left:53.739770867430444%}.row-fluid .offset6:first-child{margin-left:51.28205128205128%;*margin-left:51.175668303327875%}.row-fluid .offset5{margin-left:45.299145299145295%;*margin-left:45.1927623204219%}.row-fluid .offset5:first-child{margin-left:42.73504273504273%;*margin-left:42.62865975631933%}.row-fluid .offset4{margin-left:36.75213675213675%;*margin-left:36.645753773413354%}.row-fluid .offset4:first-child{margin-left:34.18803418803419%;*margin-left:34.081651209310785%}.row-fluid .offset3{margin-left:28.205128205128204%;*margin-left:28.0987452264048%}.row-fluid .offset3:first-child{margin-left:25.641025641025642%;*margin-left:25.53464266230224%}.row-fluid .offset2{margin-left:19.65811965811966%;*margin-left:19.551736679396257%}.row-fluid .offset2:first-child{margin-left:17.094017094017094%;*margin-left:16.98763411529369%}.row-fluid .offset1{margin-left:11.11111111111111%;*margin-left:11.004728132387708%}.row-fluid .offset1:first-child{margin-left:8.547008547008547%;*margin-left:8.440625568285142%}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:30px}input.span12,textarea.span12,.uneditable-input.span12{width:1156px}input.span11,textarea.span11,.uneditable-input.span11{width:1056px}input.span10,textarea.span10,.uneditable-input.span10{width:956px}input.span9,textarea.span9,.uneditable-input.span9{width:856px}input.span8,textarea.span8,.uneditable-input.span8{width:756px}input.span7,textarea.span7,.uneditable-input.span7{width:656px}input.span6,textarea.span6,.uneditable-input.span6{width:556px}input.span5,textarea.span5,.uneditable-input.span5{width:456px}input.span4,textarea.span4,.uneditable-input.span4{width:356px}input.span3,textarea.span3,.uneditable-input.span3{width:256px}input.span2,textarea.span2,.uneditable-input.span2{width:156px}input.span1,textarea.span1,.uneditable-input.span1{width:56px}.thumbnails{margin-left:-30px}.thumbnails>li{margin-left:30px}.row-fluid .thumbnails{margin-left:0}}@media(min-width:768px) and (max-width:979px){.row{margin-left:-20px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:20px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:724px}.span12{width:724px}.span11{width:662px}.span10{width:600px}.span9{width:538px}.span8{width:476px}.span7{width:414px}.span6{width:352px}.span5{width:290px}.span4{width:228px}.span3{width:166px}.span2{width:104px}.span1{width:42px}.offset12{margin-left:764px}.offset11{margin-left:702px}.offset10{margin-left:640px}.offset9{margin-left:578px}.offset8{margin-left:516px}.offset7{margin-left:454px}.offset6{margin-left:392px}.offset5{margin-left:330px}.offset4{margin-left:268px}.offset3{margin-left:206px}.offset2{margin-left:144px}.offset1{margin-left:82px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.7624309392265194%;*margin-left:2.709239449864817%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.7624309392265194%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.43646408839778%;*width:91.38327259903608%}.row-fluid .span10{width:82.87292817679558%;*width:82.81973668743387%}.row-fluid .span9{width:74.30939226519337%;*width:74.25620077583166%}.row-fluid .span8{width:65.74585635359117%;*width:65.69266486422946%}.row-fluid .span7{width:57.18232044198895%;*width:57.12912895262725%}.row-fluid .span6{width:48.61878453038674%;*width:48.56559304102504%}.row-fluid .span5{width:40.05524861878453%;*width:40.00205712942283%}.row-fluid .span4{width:31.491712707182323%;*width:31.43852121782062%}.row-fluid .span3{width:22.92817679558011%;*width:22.87498530621841%}.row-fluid .span2{width:14.3646408839779%;*width:14.311449394616199%}.row-fluid .span1{width:5.801104972375691%;*width:5.747913483013988%}.row-fluid .offset12{margin-left:105.52486187845304%;*margin-left:105.41847889972962%}.row-fluid .offset12:first-child{margin-left:102.76243093922652%;*margin-left:102.6560479605031%}.row-fluid .offset11{margin-left:96.96132596685082%;*margin-left:96.8549429881274%}.row-fluid .offset11:first-child{margin-left:94.1988950276243%;*margin-left:94.09251204890089%}.row-fluid .offset10{margin-left:88.39779005524862%;*margin-left:88.2914070765252%}.row-fluid .offset10:first-child{margin-left:85.6353591160221%;*margin-left:85.52897613729868%}.row-fluid .offset9{margin-left:79.8342541436464%;*margin-left:79.72787116492299%}.row-fluid .offset9:first-child{margin-left:77.07182320441989%;*margin-left:76.96544022569647%}.row-fluid .offset8{margin-left:71.2707182320442%;*margin-left:71.16433525332079%}.row-fluid .offset8:first-child{margin-left:68.50828729281768%;*margin-left:68.40190431409427%}.row-fluid .offset7{margin-left:62.70718232044199%;*margin-left:62.600799341718584%}.row-fluid .offset7:first-child{margin-left:59.94475138121547%;*margin-left:59.838368402492065%}.row-fluid .offset6{margin-left:54.14364640883978%;*margin-left:54.037263430116376%}.row-fluid .offset6:first-child{margin-left:51.38121546961326%;*margin-left:51.27483249088986%}.row-fluid .offset5{margin-left:45.58011049723757%;*margin-left:45.47372751851417%}.row-fluid .offset5:first-child{margin-left:42.81767955801105%;*margin-left:42.71129657928765%}.row-fluid .offset4{margin-left:37.01657458563536%;*margin-left:36.91019160691196%}.row-fluid .offset4:first-child{margin-left:34.25414364640884%;*margin-left:34.14776066768544%}.row-fluid .offset3{margin-left:28.45303867403315%;*margin-left:28.346655695309746%}.row-fluid .offset3:first-child{margin-left:25.69060773480663%;*margin-left:25.584224756083227%}.row-fluid .offset2{margin-left:19.88950276243094%;*margin-left:19.783119783707537%}.row-fluid .offset2:first-child{margin-left:17.12707182320442%;*margin-left:17.02068884448102%}.row-fluid .offset1{margin-left:11.32596685082873%;*margin-left:11.219583872105325%}.row-fluid .offset1:first-child{margin-left:8.56353591160221%;*margin-left:8.457152932878806%}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:20px}input.span12,textarea.span12,.uneditable-input.span12{width:710px}input.span11,textarea.span11,.uneditable-input.span11{width:648px}input.span10,textarea.span10,.uneditable-input.span10{width:586px}input.span9,textarea.span9,.uneditable-input.span9{width:524px}input.span8,textarea.span8,.uneditable-input.span8{width:462px}input.span7,textarea.span7,.uneditable-input.span7{width:400px}input.span6,textarea.span6,.uneditable-input.span6{width:338px}input.span5,textarea.span5,.uneditable-input.span5{width:276px}input.span4,textarea.span4,.uneditable-input.span4{width:214px}input.span3,textarea.span3,.uneditable-input.span3{width:152px}input.span2,textarea.span2,.uneditable-input.span2{width:90px}input.span1,textarea.span1,.uneditable-input.span1{width:28px}}@media(max-width:767px){body{padding-right:20px;padding-left:20px}.navbar-fixed-top,.navbar-fixed-bottom,.navbar-static-top{margin-right:-20px;margin-left:-20px}.container-fluid{padding:0}.dl-horizontal dt{float:none;width:auto;clear:none;text-align:left}.dl-horizontal dd{margin-left:0}.container{width:auto}.row-fluid{width:100%}.row,.thumbnails{margin-left:0}.thumbnails>li{float:none;margin-left:0}[class*="span"],.uneditable-input[class*="span"],.row-fluid [class*="span"]{display:block;float:none;width:100%;margin-left:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.span12,.row-fluid .span12{width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="offset"]:first-child{margin-left:0}.input-large,.input-xlarge,.input-xxlarge,input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.input-prepend input,.input-append input,.input-prepend input[class*="span"],.input-append input[class*="span"]{display:inline-block;width:auto}.controls-row [class*="span"]+[class*="span"]{margin-left:0}.modal{position:fixed;top:20px;right:20px;left:20px;width:auto;margin:0}.modal.fade{top:-100px}.modal.fade.in{top:20px}}@media(max-width:480px){.nav-collapse{-webkit-transform:translate3d(0,0,0)}.page-header h1 small{display:block;line-height:20px}input[type="checkbox"],input[type="radio"]{border:1px solid #ccc}.form-horizontal .control-label{float:none;width:auto;padding-top:0;text-align:left}.form-horizontal .controls{margin-left:0}.form-horizontal .control-list{padding-top:0}.form-horizontal .form-actions{padding-right:10px;padding-left:10px}.media .pull-left,.media .pull-right{display:block;float:none;margin-bottom:10px}.media-object{margin-right:0;margin-left:0}.modal{top:10px;right:10px;left:10px}.modal-header .close{padding:10px;margin:-10px}.carousel-caption{position:static}}@media(max-width:979px){body{padding-top:0}.navbar-fixed-top,.navbar-fixed-bottom{position:static}.navbar-fixed-top{margin-bottom:20px}.navbar-fixed-bottom{margin-top:20px}.navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding:5px}.navbar .container{width:auto;padding:0}.navbar .brand{padding-right:10px;padding-left:10px;margin:0 0 0 -5px}.nav-collapse{clear:both}.nav-collapse .nav{float:none;margin:0 0 10px}.nav-collapse .nav>li{float:none}.nav-collapse .nav>li>a{margin-bottom:2px}.nav-collapse .nav>.divider-vertical{display:none}.nav-collapse .nav .nav-header{color:#777;text-shadow:none}.nav-collapse .nav>li>a,.nav-collapse .dropdown-menu a{padding:9px 15px;font-weight:bold;color:#777;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.nav-collapse .btn{padding:4px 10px 4px;font-weight:normal;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.nav-collapse .dropdown-menu li+li a{margin-bottom:2px}.nav-collapse .nav>li>a:hover,.nav-collapse .nav>li>a:focus,.nav-collapse .dropdown-menu a:hover,.nav-collapse .dropdown-menu a:focus{background-color:#f2f2f2}.navbar-inverse .nav-collapse .nav>li>a,.navbar-inverse .nav-collapse .dropdown-menu a{color:#999}.navbar-inverse .nav-collapse .nav>li>a:hover,.navbar-inverse .nav-collapse .nav>li>a:focus,.navbar-inverse .nav-collapse .dropdown-menu a:hover,.navbar-inverse .nav-collapse .dropdown-menu a:focus{background-color:#111}.nav-collapse.in .btn-group{padding:0;margin-top:5px}.nav-collapse .dropdown-menu{position:static;top:auto;left:auto;display:none;float:none;max-width:none;padding:0;margin:0 15px;background-color:transparent;border:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.nav-collapse .open>.dropdown-menu{display:block}.nav-collapse .dropdown-menu:before,.nav-collapse .dropdown-menu:after{display:none}.nav-collapse .dropdown-menu .divider{display:none}.nav-collapse .nav>li>.dropdown-menu:before,.nav-collapse .nav>li>.dropdown-menu:after{display:none}.nav-collapse .navbar-form,.nav-collapse .navbar-search{float:none;padding:10px 15px;margin:10px 0;border-top:1px solid #f2f2f2;border-bottom:1px solid #f2f2f2;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1)}.navbar-inverse .nav-collapse .navbar-form,.navbar-inverse .nav-collapse .navbar-search{border-top-color:#111;border-bottom-color:#111}.navbar .nav-collapse .nav.pull-right{float:none;margin-left:0}.nav-collapse,.nav-collapse.collapse{height:0;overflow:hidden}.navbar .btn-navbar{display:block}.navbar-static .navbar-inner{padding-right:10px;padding-left:10px}}@media(min-width:980px){.nav-collapse.collapse{height:auto!important;overflow:visible!important}} 10 | -------------------------------------------------------------------------------- /static/bootstrap/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googleglass/mirror-quickstart-python/e34077bae91657170c305702471f5c249eb1b686/static/bootstrap/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /static/bootstrap/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googleglass/mirror-quickstart-python/e34077bae91657170c305702471f5c249eb1b686/static/bootstrap/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /static/images/chipotle-tube-640x360.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googleglass/mirror-quickstart-python/e34077bae91657170c305702471f5c249eb1b686/static/images/chipotle-tube-640x360.jpg -------------------------------------------------------------------------------- /static/images/drill.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googleglass/mirror-quickstart-python/e34077bae91657170c305702471f5c249eb1b686/static/images/drill.png -------------------------------------------------------------------------------- /static/images/keys.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googleglass/mirror-quickstart-python/e34077bae91657170c305702471f5c249eb1b686/static/images/keys.png -------------------------------------------------------------------------------- /static/images/python.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googleglass/mirror-quickstart-python/e34077bae91657170c305702471f5c249eb1b686/static/images/python.png -------------------------------------------------------------------------------- /static/images/saturn-eclipse.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googleglass/mirror-quickstart-python/e34077bae91657170c305702471f5c249eb1b686/static/images/saturn-eclipse.jpg -------------------------------------------------------------------------------- /static/main.css: -------------------------------------------------------------------------------- 1 | /* Add some padding to shift the body down underneath the navbar. */ 2 | body { 3 | padding-top: 60px; 4 | } 5 | 6 | @media (max-width: 980px) { 7 | body { 8 | padding-top: 0; 9 | } 10 | } 11 | 12 | /* Get rid of margin under form controls that are in table cells. */ 13 | table td form, table td input[type='text'], table td button { 14 | margin-bottom: 0; 15 | } 16 | 17 | img.button-icon { 18 | width: 60px; 19 | } 20 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | Glassware Starter Project 21 | 23 | 25 | 26 | 27 | 28 | 41 | 42 |
43 | 44 | {% if message %} 45 |
{{ message }}
46 | {% endif %} 47 | 48 |

Your Recent Timeline

49 |
50 | 51 |
52 | 53 | {% if timelineItems %} 54 | {% for timelineItem in timelineItems %} 55 |
56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 81 | 82 | 83 | 92 | 93 | 94 |
ID{{ timelineItem.id }}
Text{{ timelineItem.text|e }}
HTML{{ timelineItem.html|e }}
Attachments 73 | {% for attachment in timelineItem.attachments %} 74 | {% if attachment.contentType.startswith('image') %} 75 | 76 | {% else %} 77 | Download 78 | {% endif %} 79 | {% endfor %} 80 |
84 |
85 | 86 | 87 | 90 |
91 |
95 |
96 | {% endfor %} 97 | {% else %} 98 |
99 |
100 | You haven't added any items to your timeline yet. Use the controls 101 | below to add something! 102 |
103 |
104 | {% endif %} 105 |
106 |
107 | 108 |
109 |
110 |

Timeline

111 | 112 |

When you first sign in, this Glassware inserts a welcome message. Use 113 | these controls to insert more items into your timeline. Learn more about 114 | the timeline APIs 115 | here.

116 | 117 |
118 | 119 |
120 | 123 |
124 | 125 |
126 | 127 | 128 | 130 | 131 | 132 | 136 |
137 |
138 | 139 | 141 |
142 |
143 | 144 | 147 |
148 |
149 |
150 | 151 | 154 |
155 | 156 |
157 | 158 |
159 |

Contacts

160 |

By default, this project inserts a single contact that accepts 161 | all content types. Learn more about contacts 162 | here.

163 | 164 | {% if contact %} 165 |
166 | 167 | 168 | 171 |
172 | {% else %} 173 |
174 | 175 | 177 | 178 | 179 | 182 |
183 | {% endif %} 184 | 185 |

Voice Commands

186 |

The "Python Quick Start" contact also accepts the take a 187 | note command. Take a note with the "Python Quick Start" 188 | contact and the cat in the server will record your note and reply 189 | with one of a few cat utterances.

190 |
191 | 192 |
193 |

Subscriptions

194 | 195 |

196 | By default a subscription is inserted for changes to the 197 | timeline collection. Learn more about subscriptions 198 | here. 199 |

200 | 201 |
202 | Note: Subscriptions require SSL. They will not work on localhost. 203 |
204 | 205 | {% if timelineSubscriptionExists %} 206 |
207 | 208 | 209 | 212 |
213 | {% else %} 214 |
215 | 216 | 217 | 220 |
221 | {% endif %} 222 | 223 | {% if locationSubscriptionExists %} 224 |
225 | 226 | 227 | 230 |
231 | {% else %} 232 |
233 | 234 | 235 | 238 |
239 | {% endif %} 240 |
241 |
242 |
243 | 244 | 246 | 247 | 248 | 249 | -------------------------------------------------------------------------------- /util.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 | """Utility functions for the Quickstart.""" 16 | 17 | __author__ = 'alainv@google.com (Alain Vongsouvanh)' 18 | 19 | 20 | from urlparse import urlparse 21 | 22 | import httplib2 23 | from apiclient.discovery import build 24 | from oauth2client.appengine import StorageByKeyName 25 | from oauth2client.client import AccessTokenRefreshError 26 | import sessions 27 | 28 | from model import Credentials 29 | 30 | 31 | # Load the secret that is used for client side sessions 32 | # Create one of these for yourself with, for example: 33 | # python -c "import os; print os.urandom(64)" > session.secret 34 | SESSION_SECRET = open('session.secret').read() 35 | 36 | 37 | def get_full_url(request_handler, path): 38 | """Return the full url from the provided request handler and path.""" 39 | pr = urlparse(request_handler.request.url) 40 | return '%s://%s%s' % (pr.scheme, pr.netloc, path) 41 | 42 | 43 | def load_session_credentials(request_handler): 44 | """Load credentials from the current session.""" 45 | session = sessions.LilCookies(request_handler, SESSION_SECRET) 46 | userid = session.get_secure_cookie(name='userid') 47 | if userid: 48 | return userid, StorageByKeyName(Credentials, userid, 'credentials').get() 49 | else: 50 | return None, None 51 | 52 | 53 | def store_userid(request_handler, userid): 54 | """Store current user's ID in session.""" 55 | session = sessions.LilCookies(request_handler, SESSION_SECRET) 56 | session.set_secure_cookie(name='userid', value=userid) 57 | 58 | 59 | def create_service(service, version, creds=None): 60 | """Create a Google API service. 61 | 62 | Load an API service from a discovery document and authorize it with the 63 | provided credentials. 64 | 65 | Args: 66 | service: Service name (e.g 'mirror', 'oauth2'). 67 | version: Service version (e.g 'v1'). 68 | creds: Credentials used to authorize service. 69 | Returns: 70 | Authorized Google API service. 71 | """ 72 | # Instantiate an Http instance 73 | http = httplib2.Http() 74 | 75 | if creds: 76 | # Authorize the Http instance with the passed credentials 77 | creds.authorize(http) 78 | 79 | return build(service, version, http=http) 80 | 81 | 82 | def auth_required(handler_method): 83 | """A decorator to require that the user has authorized the Glassware.""" 84 | 85 | def check_auth(self, *args): 86 | self.userid, self.credentials = load_session_credentials(self) 87 | self.mirror_service = create_service('mirror', 'v1', self.credentials) 88 | # TODO: Also check that credentials are still valid. 89 | if self.credentials: 90 | try: 91 | self.credentials.refresh(httplib2.Http()) 92 | return handler_method(self, *args) 93 | except AccessTokenRefreshError: 94 | # Access has been revoked. 95 | store_userid(self, '') 96 | credentials_entity = Credentials.get_by_key_name(self.userid) 97 | if credentials_entity: 98 | credentials_entity.delete() 99 | self.redirect('/auth') 100 | return check_auth 101 | --------------------------------------------------------------------------------