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:
parent
adc785ade4
commit
3d6c57a71b
2000
pylast.html
Normal file
2000
pylast.html
Normal file
File diff suppressed because it is too large
Load diff
125
pylast.py
125
pylast.py
|
@ -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):
|
||||||
|
|
Loading…
Reference in a new issue