Refactored the code for caching requests, and added the support of sqlite3 if the module is present. If not installed, shelve module would be used instead.

This commit is contained in:
Amr Hassan 2009-07-16 03:04:21 +00:00
parent adc785ade4
commit 3d6c57a71b
2 changed files with 2076 additions and 51 deletions

2000
pylast.html Normal file

File diff suppressed because it is too large Load diff

127
pylast.py
View file

@ -39,8 +39,7 @@ SUBMISSION_SERVER = "http://post.audioscrobbler.com:80/"
__proxy = None __proxy = None
__proxy_enabled = False __proxy_enabled = False
__cache_shelf = None __cache_backend = None
__cache_filename = None
__last_call_time = 0 __last_call_time = 0
import hashlib import hashlib
@ -53,6 +52,12 @@ import time
from logging import info, warn, debug from logging import info, warn, debug
import shelve import shelve
import tempfile import tempfile
import sys
try:
import sqlite3
except ImportError:
pass
STATUS_INVALID_SERVICE = 2 STATUS_INVALID_SERVICE = 2
STATUS_INVALID_METHOD = 3 STATUS_INVALID_METHOD = 3
@ -109,6 +114,40 @@ SCROBBLE_MODE_PLAYED = "L"
SCROBBLE_MODE_BANNED = "B" SCROBBLE_MODE_BANNED = "B"
SCROBBLE_MODE_SKIPPED = "S" SCROBBLE_MODE_SKIPPED = "S"
class _ShelfCacheBackend(object):
"""Used as a backend for caching cacheable requests."""
def __init__(self, file_path = None):
self.shelf = shelve.open(file_path)
def get_xml(self, key):
return self.shelf[key]
def set_xml(self, key, xml_string):
self.shelf[key] = xml_string
def has_key(self, key):
return key in self.shelf.keys()
class _SqliteCacheBackend(object):
"""Used as a backend for caching cacheable requests."""
def __init__(self, file_path = None):
self.connection = sqlite3.connect(file_path)
self.connection.execute("CREATE TABLE IF NOT EXISTS cache(key TEXT, xml TEXT)")
self.connection.commit()
def get_xml(self, key):
row = self.connection.execute("SELECT xml FROM cache WHERE key='%s'" %key).fetchone()
return row[0]
def set_xml(self, key, xml_string):
self.connection.execute("INSERT INTO cache VALUES('%s', '%s')" %(key, xml_string))
self.connection.commit()
def has_key(self, key):
row = self.connection.execute("SELECT COUNT(*) FROM cache WHERE key='%s'" %key).fetchone()
return row[0] > 0
class _ThreadedCall(threading.Thread): class _ThreadedCall(threading.Thread):
"""Facilitates calling a function on another thread.""" """Facilitates calling a function on another thread."""
@ -154,7 +193,7 @@ class _Request(object):
self.params["method"] = method_name self.params["method"] = method_name
if is_caching_enabled(): if is_caching_enabled():
self.shelf = get_cache_shelf() self.cache = _get_cache_backend()
if session_key: if session_key:
self.params["sk"] = session_key self.params["sk"] = session_key
@ -202,14 +241,14 @@ class _Request(object):
if not self._is_cached(): if not self._is_cached():
response = self._download_response() response = self._download_response()
self.shelf[self._get_cache_key()] = response self.cache.set_xml(self._get_cache_key(), response)
return self.shelf[self._get_cache_key()] return self.cache.get_xml(self._get_cache_key())
def _is_cached(self): def _is_cached(self):
"""Returns True if the request is already in cache.""" """Returns True if the request is already in cache."""
return self.shelf.has_key(self._get_cache_key()) return self.cache.has_key(self._get_cache_key())
def _download_response(self): def _download_response(self):
"""Returns a response body string from the server.""" """Returns a response body string from the server."""
@ -487,11 +526,11 @@ class ServiceException(Exception):
"""Exception related to the Last.fm web service""" """Exception related to the Last.fm web service"""
def __init__(self, lastfm_status, details): def __init__(self, lastfm_status, details):
self._lastfm_status = lastfm_status self.lastfm_status = lastfm_status
self._details = details self.details = details
def __str__(self): def __repr__(self):
return self._details return self.details
def get_id(self): def get_id(self):
"""Returns the exception ID, from one of the following: """Returns the exception ID, from one of the following:
@ -510,7 +549,7 @@ class ServiceException(Exception):
STATUS_TOKEN_EXPIRED = 15 STATUS_TOKEN_EXPIRED = 15
""" """
return self._lastfm_status return self.lastfm_status
class TopItem (object): class TopItem (object):
"""A top item in a list that has a weight. Returned from functions like get_top_tracks() and get_top_artists().""" """A top item in a list that has a weight. Returned from functions like get_top_tracks() and get_top_artists()."""
@ -604,8 +643,8 @@ class Album(_BaseObject, _Taggable):
""" """
Create an album instance. Create an album instance.
# Parameters: # Parameters:
* artist str|Artist: An artist name or an Artist object. * artist: An artist name or an Artist object.
* title str: The album title. * title: The album title.
""" """
_BaseObject.__init__(self, api_key, api_secret, session_key) _BaseObject.__init__(self, api_key, api_secret, session_key)
@ -2836,43 +2875,43 @@ def async_call(sender, call, callback = None, call_args = None, callback_args =
thread = _ThreadedCall(sender, call, call_args, callback, callback_args) thread = _ThreadedCall(sender, call, call_args, callback, callback_args)
thread.start() thread.start()
def enable_caching(cache_filename = None): def enable_caching(file_path = None):
"""Enables caching request-wide for all cachable calls. Uses a shelve.DbfilenameShelf object. """Enables caching request-wide for all cachable calls.
* cache_filename: A filename for the db. Defaults to a temporary filename in the tmp directory. In choosing the backend used for caching, it will try _SqliteCacheBackend first if
the module sqlite3 is present. If not, it will fallback to _ShelfCacheBackend which uses shelve.Shelf objects.
* file_path: A file path for the backend storage file. If
None set, a temp file would probably be created, according the backend.
""" """
global __cache_shelf global __cache_backend
global __cache_filename
if not cache_filename: if not file_path:
cache_filename = tempfile.mktemp(prefix="pylast_tmp_") file_path = tempfile.mktemp(prefix="pylast_tmp_")
__cache_filename = cache_filename if "sqlite3" in sys.modules.keys():
__cache_shelf = shelve.open(__cache_filename) __cache_backend = _SqliteCacheBackend(file_path)
debug("Caching to Sqlite3 at " + file_path)
else:
__cache_backend = _ShelfCacheBackend(file_path)
debug("Caching to Shelf at " + file_path)
def disable_caching(): def disable_caching():
"""Disables all caching features.""" """Disables all caching features."""
global __cache_shelf global __cache_backend
__cache_shelf = None __cache_backend = None
def is_caching_enabled(): def is_caching_enabled():
"""Returns True if caching is enabled.""" """Returns True if caching is enabled."""
global __cache_shelf global __cache_backend
return not (__cache_shelf == None) return not (__cache_backend == None)
def get_cache_filename(): def _get_cache_backend():
"""Returns filename of the cache db in use."""
global __cache_filename global __cache_backend
return __cache_filename return __cache_backend
def get_cache_shelf():
"""Returns the Shelf object used for caching."""
global __cache_shelf
return __cache_shelf
def _extract(node, name, index = 0): def _extract(node, name, index = 0):
"""Extracts a value from the xml string""" """Extracts a value from the xml string"""
@ -3011,20 +3050,6 @@ def _delay_call():
__last_call_time = now __last_call_time = now
def clear_cache():
"""Clears the cache data and starts fresh."""
global __cache_dir
if not os.path.exists(__cache_dir):
return
for file in os.listdir(__cache_dir):
os.remove(os.path.join(__cache_dir, file))
# ------------------------------------------------------------ # ------------------------------------------------------------
class ScrobblingException(Exception): class ScrobblingException(Exception):