diff --git a/lastfm/api.py b/lastfm/api.py index 5975535..ddff48d 100644 --- a/lastfm/api.py +++ b/lastfm/api.py @@ -7,6 +7,7 @@ __license__ = "GNU Lesser General Public License" __package__ = "lastfm" from threading import Lock +from lastfm.util import Wormhole, logging from lastfm.decorators import cached_property, async_callback _lock = Lock() @@ -23,6 +24,12 @@ class Api(object): """The minimum interval between successive HTTP request, in seconds""" SEARCH_XMLNS = "http://a9.com/-/spec/opensearch/1.1/" + + DEBUG_LEVELS = { + 'LOW': 1, + 'MEDIUM': 2, + 'HIGH': 3 + } def __init__(self, api_key, @@ -31,7 +38,8 @@ class Api(object): input_encoding=None, request_headers=None, no_cache = False, - debug = False): + debug = None, + logfile = None): """ Create an Api object to access the last.fm webservice API. Use this object as a starting point for accessing all the webservice methods. @@ -64,8 +72,19 @@ class Api(object): self._initialize_user_agent() self._input_encoding = input_encoding self._no_cache = no_cache - self._debug = debug + self._logfile = logfile self._last_fetch_time = datetime.now() + + if debug is not None: + if debug in Api.DEBUG_LEVELS: + self._debug = Api.DEBUG_LEVELS[debug] + else: + raise InvalidParametersError("debug parameter must be one of the keys in Api.DEBUG_LEVELS dict") + else: + self._debug = None + if self._debug is not None: + Wormhole.enable() + logging.set_api(self) @property def api_key(self): @@ -588,6 +607,7 @@ class Api(object): """ return Venue.search(self, search_item = venue, limit = limit, country = country) + @Wormhole.entrance('lfm-api-url') def _build_url(self, url, path_elements=None, extra_params=None): # Break url into consituent parts (scheme, netloc, path, params, query, fragment) = urlparse.urlparse(url) @@ -656,11 +676,10 @@ class Api(object): self._last_fetch_time = datetime.now() return url_data + @Wormhole.entrance('lfm-api-raw-data') def _fetch_url(self, url, parameters = None, no_cache = False): # Add key/value parameters to the query string of the url url = self._build_url(url, extra_params=parameters) - if self._debug: - print url # Get a url opener that can handle basic auth opener = self._get_opener(url) @@ -690,6 +709,7 @@ class Api(object): # Always return the latest version return url_data + @Wormhole.entrance('lfm-api-processed-data') def _fetch_data(self, params, sign = False, @@ -710,17 +730,17 @@ class Api(object): xml = self._fetch_url(Api.API_ROOT_URL, params, no_cache = self._no_cache or no_cache) return self._check_xml(xml) + @Wormhole.entrance('lfm-api-raw-data') def _post_url(self, url, parameters): url = self._build_url(url) data = self._encode_parameters(parameters) - if self._debug: - print data opener = self._get_opener(url) url_data = self._read_url_data(opener, url, data) return url_data + @Wormhole.entrance('lfm-api-processed-data') def _post_data(self, params): params['api_key'] = self.api_key diff --git a/lastfm/chart.py b/lastfm/chart.py index fb558f8..d6247d0 100644 --- a/lastfm/chart.py +++ b/lastfm/chart.py @@ -8,6 +8,7 @@ __package__ = "lastfm" from functools import reduce from lastfm.base import LastfmBase from lastfm.mixin import cacheable +from lastfm.util import logging from operator import xor @cacheable @@ -404,8 +405,8 @@ class RollingChart(Chart): try: period_wacl.append( getattr(subject, "get_weekly_%s_chart" % chart_type)(wc.start, wc.end)) - except LastfmError: - pass + except LastfmError as ex: + logging.log_silenced_exceptions(ex) stats_dict = period_wacl[0].__dict__["_%ss" % chart_type][0].stats.__dict__ count_attribute = [k for k in stats_dict.keys() if stats_dict[k] is not None and k not in ['_rank', '_subject']][0] diff --git a/lastfm/mixin/_chartable.py b/lastfm/mixin/_chartable.py index 4c48068..4813682 100644 --- a/lastfm/mixin/_chartable.py +++ b/lastfm/mixin/_chartable.py @@ -5,7 +5,7 @@ __version__ = "0.2" __license__ = "GNU Lesser General Public License" __package__ = "lastfm.mixin" -from lastfm.util import lazylist +from lastfm.util import lazylist, logging from lastfm.decorators import cached_property def chartable(chart_types): @@ -83,8 +83,8 @@ def chartable(chart_types): for wc in wcl: try: yield self.get_weekly_album_chart(wc.start, wc.end) - except LastfmError: - pass + except LastfmError as ex: + logging.log_silenced_exceptions(ex) return gen() def get_monthly_album_chart(self, start = None, end = None): @@ -104,8 +104,8 @@ def chartable(chart_types): for mc in mcl: try: yield self.get_monthly_album_chart(mc.start, mc.end) - except LastfmError: - pass + except LastfmError as ex: + logging.log_silenced_exceptions(ex) return gen() def get_quaterly_album_chart(self, start = None, end = None): @@ -179,8 +179,8 @@ def chartable(chart_types): for wc in wcl: try: yield self.get_weekly_artist_chart(wc.start, wc.end) - except LastfmError: - pass + except LastfmError as ex: + logging.log_silenced_exceptions(ex) return gen() def get_monthly_artist_chart(self, start = None, end = None): @@ -200,8 +200,8 @@ def chartable(chart_types): for mc in mcl: try: yield self.get_monthly_artist_chart(mc.start, mc.end) - except LastfmError: - pass + except LastfmError as ex: + logging.log_silenced_exceptions(ex) return gen() def get_quaterly_artist_chart(self, start = None, end = None): @@ -275,8 +275,8 @@ def chartable(chart_types): for wc in wcl: try: yield self.get_weekly_track_chart(wc.start, wc.end) - except LastfmError: - pass + except LastfmError as ex: + logging.log_silenced_exceptions(ex) return gen() def get_monthly_track_chart(self, start = None, end = None): @@ -296,8 +296,8 @@ def chartable(chart_types): for mc in mcl: try: yield self.get_monthly_track_chart(mc.start, mc.end) - except LastfmError: - pass + except LastfmError as ex: + logging.log_silenced_exceptions(ex) return gen() def get_quaterly_track_chart(self, start = None, end = None): @@ -373,8 +373,8 @@ def chartable(chart_types): for wc in wcl: try: yield self.get_weekly_tag_chart(wc.start, wc.end) - except LastfmError: - pass + except LastfmError as ex: + logging.log_silenced_exceptions(ex) return gen() def get_monthly_tag_chart(self, start = None, end = None): @@ -394,8 +394,8 @@ def chartable(chart_types): for mc in mcl: try: yield self.get_monthly_tag_chart(mc.start, mc.end) - except LastfmError: - pass + except LastfmError as ex: + logging.log_silenced_exceptions(ex) return gen() def get_quaterly_tag_chart(self, start = None, end = None): diff --git a/lastfm/util/logging.py b/lastfm/util/logging.py new file mode 100644 index 0000000..3656948 --- /dev/null +++ b/lastfm/util/logging.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python + +__author__ = "Abhinav Sarkar " +__version__ = "0.2" +__license__ = "GNU Lesser General Public License" +__package__ = "lastfm.util" + +from contextlib import contextmanager, nested +from threading import Lock +from lastfm.util import Wormhole +from datetime import datetime +import sys + +api = None +lock = Lock() + +def set_api(api_): + global api + api = api_ + +@contextmanager +def logfile(): + if api._logfile is None: + try: + yield sys.stdout + finally: + pass + else: + try: + log = None + try: + log = open(api._logfile, 'at') + yield log + except IOError: + sys.stderr.write("could not open log file, logging to stdout\n") + yield sys.stdout + finally: + if log is not None: + log.close() + +@Wormhole.exit('lfm-api-url') +def log_url(url, *args, **kwargs): + if api._debug >= api.DEBUG_LEVELS['LOW']: + with nested(logfile(), lock) as (log, l): + log.write("{0}: URL fetched: {1}\n".format(datetime.now(), url)) + +@Wormhole.exit('lfm-obcache-register') +def log_object_registration((inst, already_registered), *args, **kwargs): + if api._debug >= api.DEBUG_LEVELS['MEDIUM']: + with nested(logfile(), lock) as (log, l): + if already_registered: + log.write("{0}: already registered: {1}\n".format(datetime.now(), repr(inst))) + else: + log.write("{0}: not already registered: {1}\n".format(datetime.now(), inst.__class__)) + +@Wormhole.exit('lfm-api-raw-data') +def log_raw_data(raw_data, *args, **kwargs): + if api._debug >= api.DEBUG_LEVELS['HIGH']: + with nested(logfile(), lock) as (log, l): + log.write("{0}: RAW DATA\n {1}\n".format(datetime.now(), raw_data)) + +def log_silenced_exceptions(ex): + if api._debug >= api.DEBUG_LEVELS['LOW']: + with nested(logfile(), lock) as (log, l): + log.write("{0}: Silenced Exception: {1}\n".format(datetime.now(), ex)) \ No newline at end of file diff --git a/lastfm/util/objectcache.py b/lastfm/util/objectcache.py index 8d6d264..355dd5d 100644 --- a/lastfm/util/objectcache.py +++ b/lastfm/util/objectcache.py @@ -4,6 +4,8 @@ __author__ = "Abhinav Sarkar " __version__ = "0.2" __license__ = "GNU Lesser General Public License" __package__ = "lastfm.util" + +from lastfm.util import Wormhole _registry = {} @@ -24,6 +26,7 @@ class ObjectCache(object): ] @staticmethod + @Wormhole.entrance('lfm-obcache-register') def register(ob, key): cls_name = ob.__class__.__name__ if not cls_name in _registry: diff --git a/lastfm/util/wormhole.py b/lastfm/util/wormhole.py new file mode 100644 index 0000000..b707267 --- /dev/null +++ b/lastfm/util/wormhole.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python + +__author__ = "Abhinav Sarkar " +__version__ = "0.2" +__license__ = "GNU Lesser General Public License" +__package__ = "lastfm.util" + +from collections import defaultdict +from decorator import decorator +from threading import Lock + +_lock = Lock() + +class Wormhole(object): + _entrances = defaultdict(set) + _exits = defaultdict(set) + _enabled = False + + @staticmethod + def disable(): + with _lock: + Wormhole._enabled = False + + @staticmethod + def enable(): + with _lock: + Wormhole._enabled = True + + @staticmethod + def add_entrance(topic, entrance): + wrapped = Wormhole.entrance(topic)(entrance) + wrapped._orginal = entrance + return wrapped + + @staticmethod + def add_exit(topic, exit): + Wormhole._exits[topic].add(exit) + + @staticmethod + def remove_entrance(topic, entrance): + entrance = entrance._orginal + if topic in Wormhole._entrances: + if entrance in Wormhole._entrances[topic]: + Wormhole._entrances[topic].remove(entrance) + return entrance + + @staticmethod + def remove_exit(topic, exit): + if topic in Wormhole._exits: + if exit in Wormhole._exits[topic]: + Wormhole._exits[topic].remove(exit) + + @classmethod + def entrance(cls, topic): + @decorator + def wrapper(func, *args, **kwargs): + Wormhole._entrances[topic].add(func) + retval = func(*args, **kwargs) + if topic in Wormhole._exits: + if Wormhole._enabled: + if func in Wormhole._entrances[topic]: + cls._jump(topic, retval, *args, **kwargs) + return retval + return wrapper + + @staticmethod + def exit(topic): + def wrapper(func): + Wormhole._exits[topic].add(func) + return func + return wrapper + + @staticmethod + def _jump(topic, retval, *args, **kwargs): + exceptions = [] + for f in Wormhole._exits[topic]: + try: + f(retval, *args, **kwargs) + except Exception as e: + exceptions.append(e) + for e in exceptions: + raise e + +class ThreadedWormhole(Wormhole): + @staticmethod + def _jump(topic, retval, *args, **kwargs): + import threading + for f in Wormhole._exits[topic]: + threading.Thread(target = lambda: f(retval, *args, **kwargs)).start() \ No newline at end of file