Skip to content

Commit 3012512

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

File tree

8 files changed

+475
-13
lines changed

8 files changed

+475
-13
lines changed

googleapiclient/discovery.py

+46-11
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,53 @@ 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 through which HTTP requests will be made.
207+
cache_discovery: Boolean, whether or not to cache the discovery doc.
208+
cache: googleapiclient.discovery_cache.base.Cache, an optional cache
209+
object for the discovery documents.
210+
211+
Returns:
212+
A unicode string representation of the discovery document.
213+
"""
214+
if cache_discovery:
215+
from . import discovery_cache
216+
from .discovery_cache import base
217+
if cache is None:
218+
cache = discovery_cache.autodetect()
219+
if cache:
220+
content = cache.get(url)
221+
if content:
222+
return content
223+
224+
actual_url = url
188225
# REMOTE_ADDR is defined by the CGI spec [RFC3875] as the environment
189226
# variable that contains the network address of the client sending the
190227
# request. If it exists then add that to the request for the discovery
191228
# document to avoid exceeding the quota on discovery requests.
192229
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)
230+
actual_url = _add_query_parameter(url, 'userIp', os.environ['REMOTE_ADDR'])
231+
logger.info('URL being requested: GET %s', actual_url)
196232

197-
resp, content = http.request(requested_url)
233+
resp, content = http.request(actual_url)
198234

199235
if resp.status == 404:
200236
raise UnknownApiNameOrVersion("name: %s version: %s" % (serviceName,
201-
version))
237+
version))
202238
if resp.status >= 400:
203-
raise HttpError(resp, content, uri=requested_url)
239+
raise HttpError(resp, content, uri=actual_url)
204240

205241
try:
206242
content = content.decode('utf-8')
@@ -212,10 +248,9 @@ def build(serviceName,
212248
except ValueError as e:
213249
logger.error('Failed to parse as JSON: ' + content)
214250
raise InvalidJsonError()
215-
216-
return build_from_document(content, base=discoveryServiceUrl, http=http,
217-
developerKey=developerKey, model=model, requestBuilder=requestBuilder,
218-
credentials=credentials)
251+
if cache_discovery and cache:
252+
cache.set(url, content)
253+
return content
219254

220255

221256
@positional(1)
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+
from __future__ import absolute_import
18+
19+
import logging
20+
import datetime
21+
22+
DISCOVERY_DOC_MAX_AGE = 60 * 60 * 24 # 1 day
23+
24+
25+
def autodetect():
26+
"""Detects an appropriate cache module and returns it.
27+
28+
Returns:
29+
googleapiclient.discovery_cache.base.Cache, a cache object which
30+
is auto detected, or None if no cache object is available.
31+
"""
32+
try:
33+
from google.appengine.api import memcache
34+
from . import appengine_memcache
35+
return appengine_memcache.cache
36+
except Exception:
37+
try:
38+
from . import file_cache
39+
return file_cache.cache
40+
except Exception as e:
41+
logging.warning(e, exc_info=True)
42+
return None
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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+
import logging
18+
19+
# This is only an optional dependency because we only import this
20+
# module when google.appengine.api.memcache is available.
21+
from google.appengine.api import memcache
22+
23+
from . import base
24+
from ..discovery_cache import DISCOVERY_DOC_MAX_AGE
25+
26+
NAMESPACE = 'google-api-client'
27+
28+
29+
class Cache(base.Cache):
30+
"""A cache with app engine memcache API."""
31+
32+
def __init__(self, max_age):
33+
"""Constructor.
34+
35+
Args:
36+
max_age: Cache expiration in seconds.
37+
"""
38+
self._max_age = max_age
39+
40+
def get(self, url):
41+
try:
42+
return memcache.get(url, namespace=NAMESPACE)
43+
except Exception as e:
44+
logging.warning(e, exc_info=True)
45+
46+
def set(self, url, content):
47+
try:
48+
memcache.set(url, content, time=int(self._max_age), namespace=NAMESPACE)
49+
except Exception as e:
50+
logging.warning(e, exc_info=True)
51+
52+
cache = Cache(max_age=DISCOVERY_DOC_MAX_AGE)
+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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+
import abc
18+
19+
20+
class Cache(object):
21+
"""A base abstract cache class."""
22+
__metaclass__ = abc.ABCMeta
23+
24+
@abc.abstractmethod
25+
def get(self, url):
26+
"""Gets the content from the memcache with a given key.
27+
28+
Args:
29+
url: string, the key for the cache.
30+
31+
Returns:
32+
object, the value in the cache for the given key, or None if the key is
33+
not in the cache.
34+
"""
35+
raise NotImplementedError()
36+
37+
@abc.abstractmethod
38+
def set(self, url, content):
39+
"""Sets the given key and content in the cache.
40+
41+
Args:
42+
url: string, the key for the cache.
43+
content: string, the discovery document.
44+
"""
45+
raise NotImplementedError()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
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+
The cache is stored in a single file so that multiple processes can
18+
share the same cache. It locks the file whenever accesing to the
19+
file. When the cache content is corrupted, it will be initialized with
20+
an empty cache.
21+
"""
22+
23+
from __future__ import division
24+
25+
import datetime
26+
import json
27+
import logging
28+
import os
29+
import tempfile
30+
import threading
31+
32+
from oauth2client.locked_file import LockedFile
33+
34+
from . import base
35+
from ..discovery_cache import DISCOVERY_DOC_MAX_AGE
36+
37+
logger = logging.getLogger(__name__)
38+
39+
FILENAME = 'google-api-python-client-discovery-doc.cache'
40+
EPOCH = datetime.datetime.utcfromtimestamp(0)
41+
42+
43+
def _to_timestamp(date):
44+
try:
45+
return (date - EPOCH).total_seconds()
46+
except AttributeError:
47+
# The following is the equivalent of total_seconds() in Python2.6.
48+
# See also: https://docs.python.org/2/library/datetime.html
49+
delta = date - EPOCH
50+
return ((delta.microseconds + (delta.seconds + delta.days * 24 * 3600)
51+
* 10**6) / 10**6)
52+
53+
54+
def _read_or_initialize_cache(f):
55+
f.file_handle().seek(0)
56+
try:
57+
cache = json.load(f.file_handle())
58+
except Exception:
59+
# This means it opens the file for the first time, or the cache is
60+
# corrupted, so initializing the file with an empty dict.
61+
cache = {}
62+
f.file_handle().truncate(0)
63+
f.file_handle().seek(0)
64+
json.dump(cache, f.file_handle())
65+
return cache
66+
67+
68+
class Cache(base.Cache):
69+
"""A file based cache for the discovery documents."""
70+
71+
def __init__(self, max_age):
72+
"""Constructor.
73+
74+
Args:
75+
max_age: Cache expiration in seconds.
76+
"""
77+
self._max_age = max_age
78+
self._file = os.path.join(tempfile.gettempdir(), FILENAME)
79+
f = LockedFile(self._file, 'a+', 'r')
80+
try:
81+
f.open_and_lock()
82+
if f.is_locked():
83+
_read_or_initialize_cache(f)
84+
# If we can not obtain the lock, other process or thread must
85+
# have initialized the file.
86+
except Exception as e:
87+
logging.warning(e, exc_info=True)
88+
finally:
89+
f.unlock_and_close()
90+
91+
def get(self, url):
92+
f = LockedFile(self._file, 'r+', 'r')
93+
try:
94+
f.open_and_lock()
95+
if f.is_locked():
96+
cache = _read_or_initialize_cache(f)
97+
if url in cache:
98+
content, t = cache.get(url, (None, 0))
99+
if _to_timestamp(datetime.datetime.now()) < t + self._max_age:
100+
return content
101+
return None
102+
else:
103+
logger.debug('Could not obtain a lock for the cache file.')
104+
return None
105+
except Exception as e:
106+
logger.warning(e, exc_info=True)
107+
finally:
108+
f.unlock_and_close()
109+
110+
def set(self, url, content):
111+
f = LockedFile(self._file, 'r+', 'r')
112+
try:
113+
f.open_and_lock()
114+
if f.is_locked():
115+
cache = _read_or_initialize_cache(f)
116+
cache[url] = (content, _to_timestamp(datetime.datetime.now()))
117+
# Remove stale cache.
118+
for k, (_, timestamp) in list(cache.items()):
119+
if _to_timestamp(datetime.datetime.now()) >= timestamp + self._max_age:
120+
del cache[k]
121+
f.file_handle().truncate(0)
122+
f.file_handle().seek(0)
123+
json.dump(cache, f.file_handle())
124+
else:
125+
logger.debug('Could not obtain a lock for the cache file.')
126+
except Exception as e:
127+
logger.warning(e, exc_info=True)
128+
finally:
129+
f.unlock_and_close()
130+
131+
132+
cache = Cache(max_age=DISCOVERY_DOC_MAX_AGE)

0 commit comments

Comments
 (0)