Skip to content

Commit c9905bf

Browse files
author
Takashi Matsuo
committed
Implemented automatic caching for the discovery documents.
1 parent a98add2 commit c9905bf

File tree

7 files changed

+414
-13
lines changed

7 files changed

+414
-13
lines changed

googleapiclient/discovery.py

Lines changed: 53 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,9 @@ def build(serviceName,
149149
developerKey=None,
150150
model=None,
151151
requestBuilder=HttpRequest,
152-
credentials=None):
152+
credentials=None,
153+
cache_discovery=True,
154+
cache=None):
153155
"""Construct a Resource for interacting with an API.
154156
155157
Construct a Resource object for interacting with an API. The serviceName and
@@ -171,6 +173,9 @@ def build(serviceName,
171173
request.
172174
credentials: oauth2client.Credentials, credentials to be used for
173175
authentication.
176+
cache_discovery: Boolean, whether or not to cache the discovery doc.
177+
cache: googleapiclient.discovery_cache.base.CacheBase, an optional
178+
cache object for the discovery documents.
174179
175180
Returns:
176181
A Resource object with methods for interacting with the service.
@@ -185,22 +190,60 @@ def build(serviceName,
185190

186191
requested_url = uritemplate.expand(discoveryServiceUrl, params)
187192

193+
content = _retrieve_discovery_doc(requested_url, http, cache_discovery, cache)
194+
195+
return build_from_document(content, base=discoveryServiceUrl, http=http,
196+
developerKey=developerKey, model=model, requestBuilder=requestBuilder,
197+
credentials=credentials)
198+
199+
200+
def _retrieve_discovery_doc(url, http, cache_discovery, cache=None):
201+
"""Retrieves the discovery_doc from cache or the internet.
202+
203+
Args:
204+
url: string, the URL of the discovery document.
205+
http: httplib2.Http, An instance of httplib2.Http or something that acts
206+
like it that HTTP requests will be made through.
207+
cache_discovery: Boolean, whether or not to cache the discovery doc.
208+
cache: googleapiclient.discovery_cache.base.CacheBase, an optional
209+
cache object for the discovery documents.
210+
211+
Returns:
212+
A unicode string representation of the discovery document.
213+
"""
214+
if http is None:
215+
http = httplib2.Http()
216+
217+
if cache_discovery:
218+
from . import discovery_cache
219+
from .discovery_cache import base
220+
if cache is None:
221+
cache = discovery_cache.autodetect()
222+
if cache:
223+
if isinstance(cache, base.CacheBase):
224+
content = cache.get(url)
225+
if content:
226+
return content
227+
else:
228+
logging.warning('The given cache object is not an instance of '
229+
'googleapiclient.discovery_cache.base.CacheBase.')
230+
231+
actual_url = url
188232
# REMOTE_ADDR is defined by the CGI spec [RFC3875] as the environment
189233
# variable that contains the network address of the client sending the
190234
# request. If it exists then add that to the request for the discovery
191235
# document to avoid exceeding the quota on discovery requests.
192236
if 'REMOTE_ADDR' in os.environ:
193-
requested_url = _add_query_parameter(requested_url, 'userIp',
194-
os.environ['REMOTE_ADDR'])
195-
logger.info('URL being requested: GET %s' % requested_url)
237+
actual_url = _add_query_parameter(url, 'userIp', os.environ['REMOTE_ADDR'])
238+
logger.info('URL being requested: GET %s' % actual_url)
196239

197-
resp, content = http.request(requested_url)
240+
resp, content = http.request(actual_url)
198241

199242
if resp.status == 404:
200243
raise UnknownApiNameOrVersion("name: %s version: %s" % (serviceName,
201-
version))
244+
version))
202245
if resp.status >= 400:
203-
raise HttpError(resp, content, uri=requested_url)
246+
raise HttpError(resp, content, uri=actual_url)
204247

205248
try:
206249
content = content.decode('utf-8')
@@ -212,10 +255,9 @@ def build(serviceName,
212255
except ValueError as e:
213256
logger.error('Failed to parse as JSON: ' + content)
214257
raise InvalidJsonError()
215-
216-
return build_from_document(content, base=discoveryServiceUrl, http=http,
217-
developerKey=developerKey, model=model, requestBuilder=requestBuilder,
218-
credentials=credentials)
258+
if cache_discovery and cache and isinstance(cache, base.CacheBase):
259+
cache.set(url, content)
260+
return content
219261

220262

221263
@positional(1)
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Copyright 2014 Google Inc. All Rights Reserved.
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+
"""Caching utility for the discovery document."""
16+
17+
18+
from __future__ import absolute_import
19+
20+
import logging
21+
import datetime
22+
23+
24+
DISCOVERY_DOC_MAX_AGE = 60 * 60 * 24 # 1 day
25+
26+
27+
def autodetect():
28+
"""Detects an appropriate cache module and returns it.
29+
30+
Returns:
31+
An object with the following methods; get(url), set(url, content).
32+
"""
33+
try:
34+
from google.appengine.api import memcache
35+
from . import appengine_memcache
36+
return appengine_memcache.Cache(max_age = DISCOVERY_DOC_MAX_AGE)
37+
except:
38+
try:
39+
from . import file_cache
40+
return file_cache.Cache(max_age = DISCOVERY_DOC_MAX_AGE)
41+
except Exception as e:
42+
logging.warning(e, exc_info=True)
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Copyright 2014 Google Inc. All Rights Reserved.
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+
"""App Engine memcache based cache for the discovery document."""
16+
17+
18+
import logging
19+
20+
# This is only an optional dependency because we only import this
21+
# module when google.appengine.api.memcache is available.
22+
from google.appengine.api import memcache
23+
24+
from . import base
25+
26+
NAMESPACE = 'google-api-client'
27+
28+
29+
class Cache(base.CacheBase):
30+
"""A cache with app engine memcache API.
31+
32+
Attributes:
33+
max_age: Cache expiration in seconds.
34+
"""
35+
def __init__(self, max_age):
36+
"""Constructor for appengine_memcache.Cache.
37+
38+
Args:
39+
max_age: Cache expiration in seconds.
40+
"""
41+
self.max_age = max_age
42+
43+
def get(self, url):
44+
"""Gets the content from the memcache with a given key.
45+
46+
Args:
47+
url: string, the key for the cache.
48+
"""
49+
try:
50+
return memcache.get(url, namespace=NAMESPACE)
51+
except Exception as e:
52+
logging.warning(e, exc_info=True)
53+
54+
def set(self, url, content):
55+
"""Sets the given key and content in the cache.
56+
57+
Args:
58+
url: string, the key for the cache.
59+
content: string, the discovery document.
60+
"""
61+
try:
62+
memcache.set(url, content, time=int(self.max_age), namespace=NAMESPACE)
63+
except Exception as e:
64+
logging.warning(e, exc_info=True)
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Copyright 2014 Google Inc. All Rights Reserved.
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+
"""An abstract class for caching the discovery document."""
16+
17+
18+
import abc
19+
20+
21+
class CacheBase(object):
22+
"""A base abstract cache class."""
23+
__metaclass__ = abc.ABCMeta
24+
25+
@abc.abstractmethod
26+
def get(self, url):
27+
"""Gets the content from the memcache with a given key.
28+
29+
Args:
30+
url: string, the key for the cache.
31+
"""
32+
return
33+
34+
@abc.abstractmethod
35+
def set(self, url, content):
36+
"""Sets the given key and content in the cache.
37+
38+
Args:
39+
url: string, the key for the cache.
40+
content: string, the discovery document.
41+
"""
42+
return
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# Copyright 2014 Google Inc. All Rights Reserved.
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+
"""File based cache for the discovery document."""
16+
17+
18+
import datetime
19+
import json
20+
import logging
21+
import os
22+
import tempfile
23+
import threading
24+
25+
from . import base
26+
27+
28+
FILENAME = 'discovery-doc.cache'
29+
EPOCH = datetime.datetime.utcfromtimestamp(0)
30+
31+
32+
def to_timestamp(d):
33+
return (d - EPOCH).total_seconds()
34+
35+
36+
class Cache(base.CacheBase):
37+
"""A file based cache for the discovery documents.
38+
39+
Attributes:
40+
max_age: Cache expiration in seconds.
41+
lock: threading.Lock, to make the operations thread safe.
42+
cache_file: string, full path to the cache file.
43+
"""
44+
def __init__(self, max_age):
45+
"""Constructor for appengine_memcache.Cache.
46+
47+
Args:
48+
max_age: Cache expiration in seconds.
49+
"""
50+
self.max_age = max_age
51+
self.lock = threading.Lock()
52+
self.cache_file = os.path.join(tempfile.gettempdir(), FILENAME)
53+
if not os.path.isfile(self.cache_file):
54+
with open(self.cache_file, 'w+') as f:
55+
json.dump({}, f)
56+
57+
def get(self, url):
58+
"""Gets the content from the memcache with a given key.
59+
60+
Args:
61+
url: string, the key for the cache.
62+
"""
63+
try:
64+
with self.lock:
65+
with open(self.cache_file, 'r') as f:
66+
cache = json.load(f)
67+
if url in cache:
68+
content, t = cache.get(url, (None, 0))
69+
if to_timestamp(datetime.datetime.now()) < t + self.max_age:
70+
return content
71+
except Exception as e:
72+
logging.warning(e, exc_info=True)
73+
74+
def set(self, url, content):
75+
"""Sets the given key and content in the cache.
76+
77+
Args:
78+
url: string, the key for the cache.
79+
content: string, the discovery document.
80+
"""
81+
try:
82+
with self.lock:
83+
with open(self.cache_file, 'r+') as f:
84+
cache = json.load(f)
85+
cache[url] = (content, to_timestamp(datetime.datetime.now()))
86+
f.seek(0)
87+
json.dump(cache, f)
88+
except Exception as e:
89+
logging.warning(e, exc_info=True)

0 commit comments

Comments
 (0)