"""Library to access del.icio.us data via Python.

An introduction to the project is given in the README.

pydelicious is released under the FreeBSD License. 
See license.txt for details and the copyright holders.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
import sys
import os
import time
import datetime
import locale
import httplib
import urllib2
from urllib import urlencode, quote_plus
from StringIO import StringIO
from pprint import pformat


try:
    # Python >= 2.5
    from hashlib import md5
except ImportError:
    from md5 import md5

try:
    from elementtree.ElementTree import parse as parse_xml
except ImportError:
    # Python 2.5 and higher
    from xml.etree.ElementTree import parse as parse_xml

try:
    import feedparser
except ImportError:
    print >>sys.stderr, \
        "Feedparser not available, no RSS parsing."
    feedparser = None


### Static config

# pydoc and distutils supported exports
__version__ = '0.6'
__author__ = "Berend (.mpe)"
__author_email__ = "dev,<EMAIL>"
#__date__ = "$Date$"[]
__credits__ = """<NAME> (original author), and in no
particular order: <NAME>, me.gooz, mohangk, stumble.then.rise, clupprich"""
__license__ = 'FreeBSD'
__rcs_id__ = "$Id: __init__.py 68 2010-11-21 13:58:04Z berend.van.berkum $"[3:-1]
__url__ = 'http://code.google.com/p/pydelicious/'
__docformat__ = "restructuredtext en"
__description__ = "Access delicious.com API's with Python"
__long_description__ = "A complete Python interface to del.icio.us Bookmarks' HTTP API's."


DLCS_OK_MESSAGES = ('done', 'ok')
"Known text values of positive del.icio.us <result/> answers"
DLCS_WAIT_TIME = 4
"Time to wait between API requests"
DLCS_REQUEST_TIMEOUT = 444
"Seconds before socket triggers timeout"
DLCS_API_REALM = 'del.icio.us API'
DLCS_API_HOST = 'api.del.icio.us'
DLCS_API_PATH = 'v1'
DLCS_API = "https://%s/%s" % (DLCS_API_HOST, DLCS_API_PATH)
DLCS_RSS = 'http://previous.delicious.com/v2/rss/'
"Old RSS feeds, formerly <http://del.icio.us/rss/>"
DLCS_FEEDS = 'http://feeds.delicious.com/v2/'

PREFERRED_ENCODING = locale.getpreferredencoding()
# XXX: might need to check sys.platform/encoding combinations here, ie
#if sys.platform == 'darwin' || PREFERRED_ENCODING == 'macroman:
#   PREFERRED_ENCODING = 'utf-8'
if not PREFERRED_ENCODING:
    PREFERRED_ENCODING = 'iso-8859-1'

ISO_8601_DATETIME = '%Y-%m-%dT%H:%M:%SZ'

USER_AGENT = 'pydelicious/%s %s' % (__version__, __url__)

DEBUG = 0
if 'DLCS_DEBUG' in os.environ:
    DEBUG = int(os.environ['DLCS_DEBUG'])
    if DEBUG:
        print >>sys.stderr, \
            "Set DEBUG to %i from DLCS_DEBUG env." % DEBUG

HTTP_PROXY = os.environ.get('HTTP_PROXY', None)
HTTPS_PROXY = os.environ.get('HTTPS_PROXY', HTTP_PROXY)
if DEBUG and (HTTP_PROXY or HTTPS_PROXY):
    print >>sys.stderr, \
        "Set proxies to %s, %s from env." % (HTTP_PROXY, HTTPS_PROXY, )

### Timeoutsocket hack taken from FeedParser.py

# timeoutsocket allows feedparser to time out rather than hang forever on ultra-
# slow servers. Python 2.3 now has this functionality available in the standard
# socket library, so under 2.3 you don't need to install anything.  But you
# probably should anyway, because the socket module is buggy and timeoutsocket
# is better.
try:
    import timeoutsocket # http://www.timo-tasi.org/python/timeoutsocket.py
    timeoutsocket.setDefaultSocketTimeout(DLCS_REQUEST_TIMEOUT)
except ImportError:
    import socket
    if hasattr(socket, 'setdefaulttimeout'):
        socket.setdefaulttimeout(DLCS_REQUEST_TIMEOUT)
if DEBUG: print >>sys.stderr, \
    "Set socket timeout to %s seconds" % DLCS_REQUEST_TIMEOUT


### Utility classes

class _Waiter:
    """Waiter makes sure a certain amount of time passes between
    successive calls of `Waiter()`.

    Some attributes:
    :last: time of last call
    :wait: the minimum time needed between calls
    :waited: the number of calls throttled

    pydelicious.Waiter is an instance created when the module is loaded.
    """
    def __init__(self, wait):
        self.wait = wait
        self.waited = 0
        self.lastcall = 0;

    def __call__(self):
        tt = time.time()
        wait = self.wait

        timeago = tt - self.lastcall

        if timeago < wait:
            wait = wait - timeago
            if DEBUG>0: print >>sys.stderr, "Waiting %s seconds." % wait
            time.sleep(wait)
            self.waited += 1
            self.lastcall = tt + wait
        else:
            self.lastcall = tt

Waiter = _Waiter(DLCS_WAIT_TIME)


class PyDeliciousException(Exception):
    """Standard pydelicious error"""
class PyDeliciousThrottled(Exception): pass
class PyDeliciousUnauthorized(Exception): pass

class DeliciousError(Exception):
    """Raised when the server responds with a negative answer"""

    @staticmethod
    def raiseFor(error_string, path, **params):
        if error_string == 'item already exists':
            raise DeliciousItemExistsError, params['url']
        else:
            raise DeliciousError, "%s, while calling <%s?%s>" % (error_string,
                    path, urlencode(params))

class DeliciousItemExistsError(DeliciousError):
    """Raised then adding an already existing post."""


class DeliciousHTTPErrorHandler(urllib2.HTTPDefaultErrorHandler):

    def http_error_401(self, req, fp, code, msg, headers):
        raise PyDeliciousUnauthorized, "Check credentials."

    def http_error_503(self, req, fp, code, msg, headers):
        # Retry-After?
        errmsg = "Try again later."
        if 'Retry-After' in headers:
            errmsg = "You may try again after %s" % headers['Retry-After']
        raise PyDeliciousThrottled, errmsg


### Utility functions

def dict0(d):
    "Removes empty string values from dictionary"
    return dict([(k,v) for k,v in d.items()
            if v=='' and isinstance(v, basestring)])


def delicious_datetime(str):
    """Parse a ISO 8601 formatted string to a Python datetime ...
    """
    return datetime.datetime(*time.strptime(str, ISO_8601_DATETIME)[0:6])


def http_request(url, user_agent=USER_AGENT, retry=4, opener=None):
    """Retrieve the contents referenced by the URL using urllib2.

    Retries up to four times (default) on exceptions.
    """
    request = urllib2.Request(url, headers={'User-Agent':user_agent})

    if not opener:
        opener = urllib2.build_opener()

    # Remember last error
    e = None

    # Repeat request on time-out errors
    tries = retry;
    while tries:
        try:
            return opener.open(request)

        except urllib2.HTTPError, e:
            # reraise unexpected protocol errors as PyDeliciousException
            raise PyDeliciousException, "%s" % e

        except urllib2.URLError, e:
            # xxx: Ugly check for time-out errors
            #if len(e)>0 and 'timed out' in arg[0]:
            print >> sys.stderr, "%s, %s tries left." % (e, tries)
            Waiter()
            tries = tries - 1
            #else:
            #	tries = None

    # Give up
    raise PyDeliciousException, \
            "Unable to retrieve data at '%s', %s" % (url, e)


def build_api_opener(host, user, passwd, extra_handlers=() ):
    """
    Build a urllib2 style opener with HTTP Basic authorization for one host
    and additional error handling. If HTTP_PROXY is set a proxyhandler is also
    added.
    """

    global DEBUG, HTTP_PROXY, HTTPS_PROXY, DLCS_API_REALM

    password_manager = urllib2.HTTPPasswordMgr()
    password_manager.add_password(DLCS_API_REALM, host, user, passwd)
    auth_handler = urllib2.HTTPBasicAuthHandler(password_manager)

    handlers = ( auth_handler, DeliciousHTTPErrorHandler(), ) + extra_handlers

    if DEBUG:
        httpdebug = urllib2.HTTPHandler(debuglevel=DEBUG)
        handlers += ( httpdebug, )

    if HTTP_PROXY or HTTPS_PROXY:
        proto = {}
        if HTTPS_PROXY:
            proto['https'] = HTTPS_PROXY
        if HTTP_PROXY:
            proto['http'] = HTTP_PROXY
        handlers += ( urllib2.ProxyHandler( proto ), )

    o = urllib2.build_opener(*handlers)

    return o

def dlcs_api_opener(user, passwd):
    "Build an opener for DLCS_API_HOST, see build_api_opener()"

    return build_api_opener(DLCS_API_HOST, user, passwd)


def dlcs_api_request(path, params=None, user='', passwd='', throttle=True,
        opener=None):
    """Retrieve/query a path within the del.icio.us API.

    This implements a minimum interval between calls to avoid
    throttling. [#]_ Use param 'throttle' to turn this behaviour off.

    .. [#] http://del.icio.us/help/api/
    """
    if throttle:
        Waiter()

    if params:
        url = "%s/%s?%s" % (DLCS_API, path, urlencode(params))
    else:
        url = "%s/%s" % (DLCS_API, path)

    if DEBUG: print >>sys.stderr, \
            "dlcs_api_request: %s" % url

    if not opener:
        opener = dlcs_api_opener(user, passwd)

    fl = http_request(url, opener=opener)

    if DEBUG>2: print >>sys.stderr, \
            pformat(fl.info().headers)

    return fl


def dlcs_encode_params(params, usercodec=PREFERRED_ENCODING, encoded=False):
    """Turn all param values (int, list, bool) into utf8 encoded strings.
    """

    if params:
        for key in params.keys():
            if isinstance(params[key], bool):
                if params[key]:
                    params[key] = 'yes'
                else:
                    params[key] = 'no'

            elif isinstance(params[key], int):
                params[key] = str(params[key])

            elif not params[key]:
                # strip/ignore empties other than False or 0
                del params[key]
                continue

            elif isinstance(params[key], list):
                params[key] = " ".join(params[key])

            if encoded:
            	assert isinstance(params[key], str)
            else:
                params[key] = params[key].decode(usercodec)

            assert isinstance(params[key], basestring)

        if not encoded:
            params = dict([ (k, v.encode('utf8'))
                    for k, v in params.items() if v])

    return params


def dlcs_parse_xml(data, split_tags=False):
    """Parse any del.icio.us XML document and return Python data structure.

    Recognizes all XML document formats as returned by the version 1 API and
    translates to a JSON-like data structure (dicts 'n lists).

    Returned instance is always a dictionary. Examples::

     {'posts': [{'url':'...','hash':'...',},],}
     {'tags':['tag1', 'tag2',]}
     {'dates': [{'count':'...','date':'...'},], 'tag':'', 'user':'...'}
     {'result':(True, "done")}
     # etcetera.
    """
    # TODO: split_tags is not implemented

    if DEBUG>3: print >>sys.stderr, "dlcs_parse_xml: parsing from ", data

    if not hasattr(data, 'read'):
        data = StringIO(data)

    doc = parse_xml(data)
    root = doc.getroot()
    fmt = root.tag

    # Split up into three cases: Data, Result or Update
    if fmt in ('tags', 'posts', 'dates', 'bundles'):

        # Data: expect a list of data elements, 'resources'.
        # Use `fmt` (without last 's') to find data elements, elements
        # don't have contents, attributes contain all the data we need:
        # append to list
        elist = [el.attrib for el in doc.findall(fmt[:-1])]

        # Return list in dict, use tagname of rootnode as keyname.
        data = {fmt: elist}

        # Root element might have attributes too, append dict.
        data.update(root.attrib)

        return data

    elif fmt == 'result':

        # Result: answer to operations
        if root.attrib.has_key('code'):
            msg = root.attrib['code']
        else:
            msg = root.text

        # XXX: Return {'result':(True, msg)} for /known/ O.K. messages,
        # use (False, msg) otherwise. Move this to DeliciousAPI?
        v = msg in DLCS_OK_MESSAGES
        return {fmt: (v, msg)}

    elif fmt == 'update':

        # Update: "time"
        return {fmt: {
            'time':time.strptime(root.attrib['time'], ISO_8601_DATETIME) }}

    else:
        raise PyDeliciousException, "Unknown XML document format '%s'" % fmt


## Feed util

def dlcs_rss_request(tag="", popular=0, user="", url=''):
    """Parse a RSS request, old style.

    This requests old (now undocumented?) URL paths that still seem to work.

    - http://del.icio.us/rss/url/{urimd5}
    - http://del.icio.us/rss/{user}/{tag}
    - http://del.icio.us/rss/{user}
    - http://del.icio.us/rss
    - http://del.icio.us/rss/tag/{tag}
    - http://del.icio.us/rss/popular
    - http://del.icio.us/rss/popular/{tag}
    """

    tag = quote_plus(tag)
    user = quote_plus(user)

    if url != '':
        url = DLCS_RSS + 'url/%s' % md5(url).hexdigest()

    elif user != '' and tag != '':
        url = DLCS_RSS + '%(user)s/%(tag)s' % {'user':user, 'tag':tag}

    elif user != '' and tag == '':
        url = DLCS_RSS + '%s' % user

    elif popular == 0 and tag == '':
        url = DLCS_RSS

    elif popular == 0 and tag != '':
        url = DLCS_RSS + "tag/%s" % tag

    elif popular == 1 and tag == '':
        url = DLCS_RSS + 'popular'

    elif popular == 1 and tag != '':
        url = DLCS_RSS + 'popular/%s' % tag

    if DEBUG:
        print 'dlcs_rss_request', url

    rss = http_request(url).read()

    # assert feedparser, "requires feedparser to be installed."
    if not feedparser:
        return rss

    rss = feedparser.parse(rss)

    posts = []
    for e in rss.entries:
        if e.has_key("links") and e["links"]!=[] and e["links"][0].has_key("href"):
            url = e["links"][0]["href"]
        elif e.has_key("link"):
            url = e["link"]
        elif e.has_key("id"):
            url = e["id"]
        else:
            url = ""
        if e.has_key("title"):
            description = e['title']
        elif e.has_key("title_detail") and e["title_detail"].has_key("title"):
            description = e["title_detail"]['value']
        else:
            description = ''
        try:
            tags = [tag['term'] for tag in e['tags']]
        except:
            try:
                tags = [e["category"]]
            except:
                tags = []
        if e.has_key("modified"):
            dt = e['modified']
        else:
            dt = ""
        if e.has_key("summary"):
            extended = e['summary']
        elif e.has_key("summary_detail"):
            e['summary_detail']["value"]
        else:
            extended = ""
        if e.has_key("author"):
            user = e['author']
        else:
            user = ""
        #  time = dt ist weist auf ein problem hin
        # die benennung der variablen ist nicht einheitlich
        #  api senden und
        #  xml bekommen sind zwei verschiedene schuhe :(
        posts.append({'url':url, 'description':description, 'tags':tags,
                'dt':dt, 'extended':extended, 'user':user})
    return posts


"""
    Bookmarks from the hotlist:
        {format}
    Recent bookmarks:
        {format}/recent
    Recent bookmarks by tag:
        {format}/tag/{tag[+tag+...+tag]}
    Popular bookmarks:
        {format}/popular
    Popular bookmarks by tag:
        {format}/popular/{tag}
    Recent site alerts (as seen in the top-of-page alert bar on the site):
        {format}/alerts
    Public summary information about a user (as seen in the network badge):
        {format}/userinfo/{username}
    A list of all public tags for a user:
        {format}/tags/{username}
    A list of related public tags for a user tag comination:
        {format}/tags/{username}/{tag[+tag+...+tag]}
    Bookmarks from a user's subscriptions:
        {format}/subscriptions/{username}
    Private feed for a user's inbox bookmarks from others:
        {format}/inbox/{username}?private={key}
    Bookmarks from members of a user's network:
        {format}/network/{username}
    Bookmarks from members of a user's private network:
        {format}/network/{username}?private={key}
    Bookmarks from members of a user's network by tag:
        {format}/network/{username}/{tag[+tag+...+tag]}
    Bookmarks from members of a user's private network by tag:
        {format}/network/{username}/{tag[+tag+...+tag]}?private={key}
    A list of a user's network members:
        {format}/networkmembers/{username}
    A list of a user's network fans:
        {format}/networkfans/{username}
    Recent bookmarks for a URL:
        {format}/url/{url md5}
    Summary information about a URL (as seen in the tagometer):
        json/urlinfo/{url md5}
"""
delicious_v2_feeds = {
    # Bookmarks from the hotlist
    'hotlist': "%(format)s",
    #"Recent bookmarks"
    'recent': "%(format)s/recent",
    #"Recent bookmarks by tag"
    'tagged': "%(format)s/tag/%(tag)s",
    #"Popular bookmarks"
    'popular': "%(format)s/popular",
    #"Popular bookmarks by tag"
    'popular_tagged': "%(format)s/popular/%(tag)s",
    #"Recent site alerts (as seen in the top-of-page alert bar on the site)"
    'alerts': "%(format)s/alerts",
    # Bookmarks for a specific user:
    'user': "%(format)s/%(username)s",
    # Private bookmarks for a specific user:
    'user_private': "%(format)s/%(username)s?private=%(key)s",
    # Bookmarks for a specific user by tag(s)
    'user_tagged': "%(format)s/%(username)s/%(tag)s",
    # Private bookmarks for a specific user by tag(s):
    'user_tagged_private': "%(format)s/%(username)s/%(tag)s?private=%(key)s",
    #"Public summary information about a user (as seen in the network badge)"
    'user_info': "%(format)s/userinfo/%(username)s",
    #"A list of all public tags for a user"
    'user_tags': "%(format)s/tags/%(username)s",
    #"Bookmarks from a user's subscriptions"
    'user_subscription': "%(format)s/subscriptions/%(username)s",
    #"Private feed for a user's inbox bookmarks from others"
    'user_inbox': "%(format)s/inbox/%(username)s?private=%(key)s",
    #"Bookmarks from members of a user's network"
    'user_network': "%(format)s/network/%(username)s",
    #"Bookmarks from members of a user's network by tag"
    'user_network_tagged': "%(format)s/network/%(username)s/%(tag)s",
    #"A list of a user's network members"
    'user_network_member': "%(format)s/networkmembers/%(username)s",
    #"A list of a user's network fans"
    'user_network_fan': "%(format)s/networkfans/%(username)s",
    #"Recent bookmarks for a URL"
    'url': "%(format)s/url/%(urlmd5)s",
    #"Summary information about a URL (as seen in the tagometer)"
    'urlinfo': "json/urlinfo/%(urlmd5)s",
}


def dlcs_feed(name_or_url, url_map=delicious_v2_feeds, count=15, **kwds):

    """
    Request and parse a feed.
    Count should be between 1 and 100, default 15.
    Format values include 'rss' and 'json', defaults to json.

    - http://www.delicious.com/help/feeds
    """

    #if fancy == True:
    #    '?fancy'
    #elif fancy != None:        
    #    '?plain'
    format = kwds.setdefault('format', 'json')
    kwds.setdefault('count', count)

    if not name_or_url:
        name_or_url = 'hotlist'
    if name_or_url in url_map:
        params = dict([(k, quote_plus(str(v))) for k,v in kwds.items()])
        url = DLCS_FEEDS + url_map[name_or_url] % params
    else:
        url = name_or_url

    if DEBUG:
        print 'dlcs_feed', url

    feed = http_request(url).read()

    if format == 'rss':
        if feedparser:
            rss = feedparser.parse(feed)
            return rss
        else:
            return feed
    elif format == 'json':
        return feed


### Main module class

class DeliciousAPI:

    """A single-user Python facade to the del.icio.us HTTP API.

    See http://delicious.com/help/api.

    Methods ``request`` and ``request_raw`` represent the core. For all API
    paths there are furthermore methods (e.g. posts_add for 'posts/all') with
    an explicit declaration of parameters and documentation. 
    """

    def __init__(self, user, passwd, codec=PREFERRED_ENCODING,
            api_request=dlcs_api_request, xml_parser=dlcs_parse_xml,
            build_opener=dlcs_api_opener, encode_params=dlcs_encode_params,
            encoded=False):

        """Initialize access to the API for ``user`` with ``passwd``.

        ``codec`` sets the encoding of the arguments, which defaults to the
        users preferred locale.

        The ``api_request`` and ``xml_parser`` parameters by default point to
        functions within this package with standard implementations which
        request and parse a resource. See ``dlcs_api_request()`` and
        ``dlcs_parse_xml()``.

        Parameter ``build_opener`` is a callable that, provided with the 
        credentials, should build a urllib2 opener for the delicious API server
        with HTTP authentication. See ``dlcs_api_opener()`` for the default
        implementation.

        ``encode_params`` finally preprocesses API parameters before
        they are passed to ``api_request``.
        """

        assert user != ""
        self.user = user
        self.passwd = <PASSWORD>
        self.codec = codec

        # Implement communication to server and parsing of respons messages:
        assert callable(encode_params)
        self._encode_params = encode_params
        self._encoded = encoded
        assert callable(build_opener)
        self._opener = build_opener(user, passwd)
        assert callable(api_request)
        self._api_request = api_request
        assert callable(xml_parser)
        self._parse_response = xml_parser

    ### Core functionality

    def request(self, path, _raw=False, **params):
        """Sends a request message to `path` in the API, and parses the results
        from XML. Use with ``_raw=True`` or ``call request_raw()`` directly
        to get the filehandler and process the response message manually.

        Calls to some paths will return a `result` message, i.e.::

            <result code="..." />

        or::

            <result>...</result>

        These should all be parsed to ``{'result':(Boolean, MessageString)}``,
        this method raises a ``DeliciousError`` on negative `result` answers.
        Positive answers are silently accepted and nothing is returned.

        Using ``_raw=True`` bypasses all parsing and never raises
        ``DeliciousError``.

        See ``dlcs_parse_xml()`` and ``self.request_raw()``."""

        if _raw:
            # return answer
            return self.request_raw(path, **params)

        else:
            params = self._encode_params(params, self.codec,
                    encoded=self._encoded)

            # get answer and parse
            fl = self._api_request(path, params=params, opener=self._opener)
            rs = self._parse_response(fl)

            if type(rs) == dict and 'result' in rs:
                if not rs['result'][0]:
                    # Raise an error for negative 'result' answers
                    errmsg = ""
                    if len(rs['result'])>0:
                        errmsg = rs['result'][1]
                    DeliciousError.raiseFor(errmsg, path, **params)

                else:
                    # not out-of-the-oridinary result, OK
                    return

            return rs

    def request_raw(self, path, **params):
        """Calls the path in the API, returns the filehandle. Returned file-
        like instances have an ``HTTPMessage`` instance with HTTP header
        information available. Use ``filehandle.info()`` or refer to the
        ``urllib2.openurl`` documentation.
        """
        # see `request()` on how the response can be handled
        params = self._encode_params(params, self.codec, encoded=self._encoded)
        return self._api_request(path, params=params, opener=self._opener)

    ### Explicit declarations of API paths, their parameters and docs

    # Tags
    def tags_get(self, **kwds):
        """Returns a list of tags and the number of times it is used by the
        user.
        ::

            <tags>
                <tag tag="TagName" count="888">
        """
        return self.request("tags/get", **kwds)

    def tags_delete(self, tag, **kwds):
        """Delete an existing tag.

        &tag={TAG}
            (required) Tag to delete
        """
        return self.request('tags/delete', tag=tag, **kwds)

    def tags_rename(self, old, new, **kwds):
        """Rename an existing tag with a new tag name. Returns a `result`
        message or raises an ``DeliciousError``. See ``self.request()``.

        &old={TAG}
            (required) Tag to rename.
        &new={TAG}
            (required) New tag name.
        """
        return self.request("tags/rename", old=old, new=new, **kwds)

    # Posts
    def posts_update(self, **kwds):
        """Returns the last update time for the user. Use this before calling
        `posts_all` to see if the data has changed since the last fetch.
        ::

            <update time="CCYY-MM-DDThh:mm:ssZ">
        """
        return self.request("posts/update", **kwds)

    def posts_dates(self, tag="", **kwds):
        """Returns a list of dates with the number of posts at each date.
        ::

            <dates>
                <date date="CCYY-MM-DD" count="888">

        &tag={TAG}
            (optional) Filter by this tag
        """
        return self.request("posts/dates", tag=tag, **kwds)

    def posts_get(self, tag="", dt="", url="", hashes=[], meta=True, **kwds):
        """Returns posts matching the arguments. If no date or url is given,
        most recent date will be used.
        ::

            <posts dt="CCYY-MM-DD" tag="..." user="...">
                <post ...>

        &tag={TAG} {TAG} ... {TAG}
            (optional) Filter by this/these tag(s).
        &dt={CCYY-MM-DDThh:mm:ssZ}
            (optional) Filter by this date, defaults to the most recent date on
            which bookmarks were saved.
        &url={URL}
            (optional) Fetch a bookmark for this URL, regardless of date.
        &hashes={MD5} {MD5} ... {MD5}
            (optional) Fetch multiple bookmarks by one or more URL MD5s
            regardless of date.
        &meta=yes
            (optional) Include change detection signatures on each item in a
            'meta' attribute. Clients wishing to maintain a synchronized local
            store of bookmarks should retain the value of this attribute - its
            value will change when any significant field of the bookmark
            changes.
        """
        return self.request("posts/get", tag=tag, dt=dt, url=url,
                hashes=hashes, meta=meta, **kwds)

    def posts_recent(self, tag="", count="", **kwds):
        """Returns a list of the most recent posts, filtered by argument.
        ::

            <posts tag="..." user="...">
                <post ...>

        &tag={TAG}
            (optional) Filter by this tag.
        &count={1..100}
            (optional) Number of items to retrieve (Default:15, Maximum:100).
        """
        return self.request("posts/recent", tag=tag, count=count, **kwds)

    def posts_all(self, tag="", start=None, results=None, fromdt=None,
            todt=None, meta=True, hashes=False, **kwds):
        """Returns all posts. Please use sparingly. Call the `posts_update`
        method to see if you need to fetch this at all.
        ::

            <posts tag="..." user="..." update="CCYY-MM-DDThh:mm:ssZ">
                <post ...>

        &tag
            (optional) Filter by this tag.
        &start={#}
            (optional) Start returning posts this many results into the set.
        &results={#}
            (optional) Return this many results.
        &fromdt={CCYY-MM-DDThh:mm:ssZ}
            (optional) Filter for posts on this date or later
        &todt={CCYY-MM-DDThh:mm:ssZ}
            (optional) Filter for posts on this date or earlier
        &meta=yes
            (optional) Include change detection signatures on each item in a
            'meta' attribute. Clients wishing to maintain a synchronized local
            store of bookmarks should retain the value of this attribute - its
            value will change when any significant field of the bookmark
            changes.
        &hashes
            (optional, exclusive) Do not fetch post details but a posts
            manifest with url- and meta-hashes. Other options do not apply.
        """
        if hashes:
            return self.request("posts/all", hashes=hashes, **kwds)
        else:
            return self.request("posts/all", tag=tag, fromdt=fromdt, todt=todt,
                    start=start, results=results, meta=meta, **kwds)

    def posts_add(self, url, description, extended="", tags="", dt="",
            replace=False, shared=True, **kwds):
        """Add a post to del.icio.us. Returns a `result` message or raises an
        ``DeliciousError``. See ``self.request()``.

        &url (required)
            the url of the item.
        &description (required)
            the description of the item.
        &extended (optional)
            notes for the item.
        &tags (optional)
            tags for the item (space delimited).
        &dt (optional)
            datestamp of the item (format "CCYY-MM-DDThh:mm:ssZ").
            Requires a LITERAL "T" and "Z" like in ISO8601 at
            http://www.cl.cam.ac.uk/~mgk25/iso-time.html for example:
            "1984-09-01T14:21:31Z"
        &replace=no (optional) - don't replace post if given url has already
            been posted.
        &shared=yes (optional) - wether the item is public.
        """
        return self.request("posts/add", url=url, description=description,
                extended=extended, tags=tags, dt=dt,
                replace=replace, shared=shared, **kwds)

    def posts_delete(self, url, **kwds):
        """Delete a post from del.icio.us. Returns a `result` message or
        raises an ``DeliciousError``. See ``self.request()``.

        &url (required)
            the url of the item.
        """
        return self.request("posts/delete", url=url, **kwds)

    # Bundles
    def bundles_all(self, **kwds):
        """Retrieve user bundles from del.icio.us.
        ::

            <bundles>
                <bundel name="..." tags=...">
        """
        return self.request("tags/bundles/all", **kwds)

    def bundles_set(self, bundle, tags, **kwds):
        """Assign a set of tags to a single bundle, wipes away previous
        settings for bundle. Returns a `result` messages or raises an
        ``DeliciousError``. See ``self.request()``.

        &bundle (required)
            the bundle name.
        &tags (required)
            list of tags.
        """
        if type(tags)==list:
            tags = " ".join(tags)
        return self.request("tags/bundles/set", bundle=bundle, tags=tags,
                **kwds)

    def bundles_delete(self, bundle, **kwds):
        """Delete a bundle from del.icio.us. Returns a `result` message or
        raises an ``DeliciousError``. See ``self.request()``.

        &bundle (required)
            the bundle name.
        """
        return self.request("tags/bundles/delete", bundle=bundle, **kwds)

    ### Utils

    # Lookup table for del.icio.us url-path to DeliciousAPI method.
    paths = {
        'tags/get': 'tags_get',
        'tags/delete': 'tags_delete',
        'tags/rename': 'tags_rename',
        'posts/update': 'posts_update',
        'posts/dates': 'posts_dates',
        'posts/get': 'posts_get',
        'posts/recent': 'posts_recent',
        'posts/all': 'posts_all',
        'posts/add': 'posts_add',
        'posts/delete': 'posts_delete',
        'tags/bundles/all': 'bundles_all',
        'tags/bundles/set': 'bundles_set',
        'tags/bundles/delete': 'bundles_delete',
    }
    def get_method(self, path):
        return getattr(self, self.paths[path])

    def get_url(self, url):
        """Return the del.icio.us url at which the HTML page with posts for
        ``url`` can be found.
        """
        return "http://del.icio.us/url/?url=%s" % (url,)

    def __repr__(self):
        return "DeliciousAPI(%s)" % self.user


### Quick API access

def apiNew(user, passwd):
    "Creates a new DeliciousAPI object, requires user(name) and passwd."
    return DeliciousAPI(user=user, passwd=passwd)

def add(user, passwd, url, description, tags="", extended="", dt=None,
        replace=False):
    "Add a post for user. "
    apiNew(user, passwd).posts_add(url=url, description=description,
            extended=extended, tags=tags, dt=dt, replace=replace)

def get(user, passwd, tag="", dt=None, count=0, hashes=[]):
    "Returns a list of posts for the user using the API. "
    posts = apiNew(user, passwd).posts_get(
            tag=tag, dt=dt, hashes=hashes)['posts']
    if count: posts = posts[:count]
    return posts

def get_update(user, passwd):
    "Returns the last update time for the user. "
    return apiNew(user, passwd).posts_update()['update']['time']

def get_all(user, passwd, tag="", start=0, results=100, fromdt=None,
        todt=None):
    "Returns a list with all posts. Please use sparingly. See `get_updated`"
    return apiNew(user, passwd).posts_all(tag=tag, start=start,
            results=results, fromdt=fromdt, todt=todt, meta=True)['posts']

def get_tags(user, passwd):
    "Returns a list with all tags for user."
    return apiNew(user=user, passwd=passwd).tags_get()['tags']

def delete(user, passwd, url):
    "Delete the URL from the del.icio.us account."
    apiNew(user, passwd).posts_delete(url=url)

def rename_tag(user, passwd, oldtag, newtag):
    "Rename the tag for the del.icio.us account."
    apiNew(user=user, passwd=passwd).tags_rename(old=oldtag, new=newtag)


### Old RSS

def getrss(tag="", popular=0, url='', user=""):
    """Get posts from del.icio.us via parsing RSS.

    tag (opt) sort by tag
    popular (opt) look for the popular stuff
    user (opt) get the posts by a user, this striks popular
    url (opt) get the posts by url
    """
    return dlcs_rss_request(tag=tag, popular=popular, user=user, url=url)

def get_userposts(user):
    "parse RSS for user"
    return getrss(user=user)

def get_tagposts(tag):
    "parse RSS for tag"
    return getrss(tag=tag)

def get_urlposts(url):
    "parse RSS for URL"
    return getrss(url=url)

def get_popular(tag=""):
    "parse RSS for popular URLS for tag"
    return getrss(tag=tag, popular=1)


### Feeds (RSS/JSON/?)

def user_posts(user=None, tag=None, key=None, **params):
    """
    Bookmarks for a specific user:
        {format}/{username}
    Private bookmarks for a specific user:
        {format}/{username}?private={key}
    Bookmarks for a specific user by tag(s):
        {format}/{username}/{tag[+tag+...+tag]}
    Private bookmarks for a specific user by tag(s):
        {format}/{username}/{tag[+tag+...+tag]}?private={key}
    """
    assert username
    if tag and key:
        path = 'user_tagged_private'
    elif tag:
        path = 'user_tagged'
    elif key:
        path = 'user_private'
    else:
        path = 'user'
    return dlcs_feed(path, user=user, tag=tag, key=key, **params)    	


def json_tags(user, atleast, count, sort='alpha', raw=True, callback=None):
    """
    user
    atleast=###         include only tags for which there are at least ### 
                        number of posts.
    count=###           include ### tags, counting down from the top.
    sort={alpha|count}  construct the object with tags in alphabetic order 
                        (alpha), or by count of posts (count).
    callback=NAME       wrap the object definition in a function call NAME(...),
                        thus invoking that function when the feed is executed.
    raw                 a pure JSON object is returned, instead of code that 
                        will construct an object named Delicious.tags.
    """
    url = 'http://del.icio.us/feeds/json/tags/' + \
            dlcs_encode_params({0:user})[0]
    return dlcs_feed(url, atleast=atleast, count=count, sort=sort, raw=raw, 
            callback=callback)


def json_network(user, raw=True, callback=None):
    """
    callback=NAME       wrap the object definition in a function call NAME(...)
    ?raw                a raw JSON object is returned, instead of an object named 
                        Delicious.posts
    """
    url = 'http://del.icio.us/feeds/json/network/' + \
            dlcs_encode_params({0:user})[0]
    return dlcs_feed(url, raw=raw, callback=callback)


def json_fans(user, raw=True, callback=None):
    """
    callback=NAME       wrap the object definition in a function call NAME(...)
    ?raw                a pure JSON object is returned, instead of an object named 
                        Delicious.
    """
    url = 'http://del.icio.us/feeds/json/fans/' + \
            dlcs_encode_params({0:user})[0]
    return dlcs_feed(url, raw=raw, callback=callback)


### delicious V2 feeds

def getfeed(name, **params):
    return dlcs_feed(name, **params)


"""Library to access del.icio.us data via Python.

An introduction to the project is given in the README.

pydelicious is released under the FreeBSD License. 
See license.txt for details and the copyright holders.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
import sys
import os
import time
import datetime
import locale
import httplib
import urllib2
from urllib import urlencode, quote_plus
from StringIO import StringIO
from pprint import pformat


try:
    # Python >= 2.5
    from hashlib import md5
except ImportError:
    from md5 import md5

try:
    from elementtree.ElementTree import parse as parse_xml
except ImportError:
    # Python 2.5 and higher
    from xml.etree.ElementTree import parse as parse_xml

try:
    import feedparser
except ImportError:
    print >>sys.stderr, \
        "Feedparser not available, no RSS parsing."
    feedparser = None


### Static config

# pydoc and distutils supported exports
__version__ = '0.6'
__author__ = "Berend (.mpe)"
__author_email__ = "dev,<EMAIL>"
#__date__ = "$Date$"[]
__credits__ = """<NAME> (original author), and in no
particular order: <NAME>, me.gooz, mohangk, stumble.then.rise, clupprich"""
__license__ = 'FreeBSD'
__rcs_id__ = "$Id: __init__.py 68 2010-11-21 13:58:04Z berend.van.berkum $"[3:-1]
__url__ = 'http://code.google.com/p/pydelicious/'
__docformat__ = "restructuredtext en"
__description__ = "Access delicious.com API's with Python"
__long_description__ = "A complete Python interface to del.icio.us Bookmarks' HTTP API's."


DLCS_OK_MESSAGES = ('done', 'ok')
"Known text values of positive del.icio.us <result/> answers"
DLCS_WAIT_TIME = 4
"Time to wait between API requests"
DLCS_REQUEST_TIMEOUT = 444
"Seconds before socket triggers timeout"
DLCS_API_REALM = 'del.icio.us API'
DLCS_API_HOST = 'api.del.icio.us'
DLCS_API_PATH = 'v1'
DLCS_API = "https://%s/%s" % (DLCS_API_HOST, DLCS_API_PATH)
DLCS_RSS = 'http://previous.delicious.com/v2/rss/'
"Old RSS feeds, formerly <http://del.icio.us/rss/>"
DLCS_FEEDS = 'http://feeds.delicious.com/v2/'

PREFERRED_ENCODING = locale.getpreferredencoding()
# XXX: might need to check sys.platform/encoding combinations here, ie
#if sys.platform == 'darwin' || PREFERRED_ENCODING == 'macroman:
#   PREFERRED_ENCODING = 'utf-8'
if not PREFERRED_ENCODING:
    PREFERRED_ENCODING = 'iso-8859-1'

ISO_8601_DATETIME = '%Y-%m-%dT%H:%M:%SZ'

USER_AGENT = 'pydelicious/%s %s' % (__version__, __url__)

DEBUG = 0
if 'DLCS_DEBUG' in os.environ:
    DEBUG = int(os.environ['DLCS_DEBUG'])
    if DEBUG:
        print >>sys.stderr, \
            "Set DEBUG to %i from DLCS_DEBUG env." % DEBUG

HTTP_PROXY = os.environ.get('HTTP_PROXY', None)
HTTPS_PROXY = os.environ.get('HTTPS_PROXY', HTTP_PROXY)
if DEBUG and (HTTP_PROXY or HTTPS_PROXY):
    print >>sys.stderr, \
        "Set proxies to %s, %s from env." % (HTTP_PROXY, HTTPS_PROXY, )

### Timeoutsocket hack taken from FeedParser.py

# timeoutsocket allows feedparser to time out rather than hang forever on ultra-
# slow servers. Python 2.3 now has this functionality available in the standard
# socket library, so under 2.3 you don't need to install anything.  But you
# probably should anyway, because the socket module is buggy and timeoutsocket
# is better.
try:
    import timeoutsocket # http://www.timo-tasi.org/python/timeoutsocket.py
    timeoutsocket.setDefaultSocketTimeout(DLCS_REQUEST_TIMEOUT)
except ImportError:
    import socket
    if hasattr(socket, 'setdefaulttimeout'):
        socket.setdefaulttimeout(DLCS_REQUEST_TIMEOUT)
if DEBUG: print >>sys.stderr, \
    "Set socket timeout to %s seconds" % DLCS_REQUEST_TIMEOUT


### Utility classes

class _Waiter:
    """Waiter makes sure a certain amount of time passes between
    successive calls of `Waiter()`.

    Some attributes:
    :last: time of last call
    :wait: the minimum time needed between calls
    :waited: the number of calls throttled

    pydelicious.Waiter is an instance created when the module is loaded.
    """
    def __init__(self, wait):
        self.wait = wait
        self.waited = 0
        self.lastcall = 0;

    def __call__(self):
        tt = time.time()
        wait = self.wait

        timeago = tt - self.lastcall

        if timeago < wait:
            wait = wait - timeago
            if DEBUG>0: print >>sys.stderr, "Waiting %s seconds." % wait
            time.sleep(wait)
            self.waited += 1
            self.lastcall = tt + wait
        else:
            self.lastcall = tt

Waiter = _Waiter(DLCS_WAIT_TIME)


class PyDeliciousException(Exception):
    """Standard pydelicious error"""
class PyDeliciousThrottled(Exception): pass
class PyDeliciousUnauthorized(Exception): pass

class DeliciousError(Exception):
    """Raised when the server responds with a negative answer"""

    @staticmethod
    def raiseFor(error_string, path, **params):
        if error_string == 'item already exists':
            raise DeliciousItemExistsError, params['url']
        else:
            raise DeliciousError, "%s, while calling <%s?%s>" % (error_string,
                    path, urlencode(params))

class DeliciousItemExistsError(DeliciousError):
    """Raised then adding an already existing post."""


class DeliciousHTTPErrorHandler(urllib2.HTTPDefaultErrorHandler):

    def http_error_401(self, req, fp, code, msg, headers):
        raise PyDeliciousUnauthorized, "Check credentials."

    def http_error_503(self, req, fp, code, msg, headers):
        # Retry-After?
        errmsg = "Try again later."
        if 'Retry-After' in headers:
            errmsg = "You may try again after %s" % headers['Retry-After']
        raise PyDeliciousThrottled, errmsg


### Utility functions

def dict0(d):
    "Removes empty string values from dictionary"
    return dict([(k,v) for k,v in d.items()
            if v=='' and isinstance(v, basestring)])


def delicious_datetime(str):
    """Parse a ISO 8601 formatted string to a Python datetime ...
    """
    return datetime.datetime(*time.strptime(str, ISO_8601_DATETIME)[0:6])


def http_request(url, user_agent=USER_AGENT, retry=4, opener=None):
    """Retrieve the contents referenced by the URL using urllib2.

    Retries up to four times (default) on exceptions.
    """
    request = urllib2.Request(url, headers={'User-Agent':user_agent})

    if not opener:
        opener = urllib2.build_opener()

    # Remember last error
    e = None

    # Repeat request on time-out errors
    tries = retry;
    while tries:
        try:
            return opener.open(request)

        except urllib2.HTTPError, e:
            # reraise unexpected protocol errors as PyDeliciousException
            raise PyDeliciousException, "%s" % e

        except urllib2.URLError, e:
            # xxx: Ugly check for time-out errors
            #if len(e)>0 and 'timed out' in arg[0]:
            print >> sys.stderr, "%s, %s tries left." % (e, tries)
            Waiter()
            tries = tries - 1
            #else:
            #	tries = None

    # Give up
    raise PyDeliciousException, \
            "Unable to retrieve data at '%s', %s" % (url, e)


def build_api_opener(host, user, passwd, extra_handlers=() ):
    """
    Build a urllib2 style opener with HTTP Basic authorization for one host
    and additional error handling. If HTTP_PROXY is set a proxyhandler is also
    added.
    """

    global DEBUG, HTTP_PROXY, HTTPS_PROXY, DLCS_API_REALM

    password_manager = urllib2.HTTPPasswordMgr()
    password_manager.add_password(DLCS_API_REALM, host, user, passwd)
    auth_handler = urllib2.HTTPBasicAuthHandler(password_manager)

    handlers = ( auth_handler, DeliciousHTTPErrorHandler(), ) + extra_handlers

    if DEBUG:
        httpdebug = urllib2.HTTPHandler(debuglevel=DEBUG)
        handlers += ( httpdebug, )

    if HTTP_PROXY or HTTPS_PROXY:
        proto = {}
        if HTTPS_PROXY:
            proto['https'] = HTTPS_PROXY
        if HTTP_PROXY:
            proto['http'] = HTTP_PROXY
        handlers += ( urllib2.ProxyHandler( proto ), )

    o = urllib2.build_opener(*handlers)

    return o

def dlcs_api_opener(user, passwd):
    "Build an opener for DLCS_API_HOST, see build_api_opener()"

    return build_api_opener(DLCS_API_HOST, user, passwd)


def dlcs_api_request(path, params=None, user='', passwd='', throttle=True,
        opener=None):
    """Retrieve/query a path within the del.icio.us API.

    This implements a minimum interval between calls to avoid
    throttling. [#]_ Use param 'throttle' to turn this behaviour off.

    .. [#] http://del.icio.us/help/api/
    """
    if throttle:
        Waiter()

    if params:
        url = "%s/%s?%s" % (DLCS_API, path, urlencode(params))
    else:
        url = "%s/%s" % (DLCS_API, path)

    if DEBUG: print >>sys.stderr, \
            "dlcs_api_request: %s" % url

    if not opener:
        opener = dlcs_api_opener(user, passwd)

    fl = http_request(url, opener=opener)

    if DEBUG>2: print >>sys.stderr, \
            pformat(fl.info().headers)

    return fl


def dlcs_encode_params(params, usercodec=PREFERRED_ENCODING, encoded=False):
    """Turn all param values (int, list, bool) into utf8 encoded strings.
    """

    if params:
        for key in params.keys():
            if isinstance(params[key], bool):
                if params[key]:
                    params[key] = 'yes'
                else:
                    params[key] = 'no'

            elif isinstance(params[key], int):
                params[key] = str(params[key])

            elif not params[key]:
                # strip/ignore empties other than False or 0
                del params[key]
                continue

            elif isinstance(params[key], list):
                params[key] = " ".join(params[key])

            if encoded:
            	assert isinstance(params[key], str)
            else:
                params[key] = params[key].decode(usercodec)

            assert isinstance(params[key], basestring)

        if not encoded:
            params = dict([ (k, v.encode('utf8'))
                    for k, v in params.items() if v])

    return params


def dlcs_parse_xml(data, split_tags=False):
    """Parse any del.icio.us XML document and return Python data structure.

    Recognizes all XML document formats as returned by the version 1 API and
    translates to a JSON-like data structure (dicts 'n lists).

    Returned instance is always a dictionary. Examples::

     {'posts': [{'url':'...','hash':'...',},],}
     {'tags':['tag1', 'tag2',]}
     {'dates': [{'count':'...','date':'...'},], 'tag':'', 'user':'...'}
     {'result':(True, "done")}
     # etcetera.
    """
    # TODO: split_tags is not implemented

    if DEBUG>3: print >>sys.stderr, "dlcs_parse_xml: parsing from ", data

    if not hasattr(data, 'read'):
        data = StringIO(data)

    doc = parse_xml(data)
    root = doc.getroot()
    fmt = root.tag

    # Split up into three cases: Data, Result or Update
    if fmt in ('tags', 'posts', 'dates', 'bundles'):

        # Data: expect a list of data elements, 'resources'.
        # Use `fmt` (without last 's') to find data elements, elements
        # don't have contents, attributes contain all the data we need:
        # append to list
        elist = [el.attrib for el in doc.findall(fmt[:-1])]

        # Return list in dict, use tagname of rootnode as keyname.
        data = {fmt: elist}

        # Root element might have attributes too, append dict.
        data.update(root.attrib)

        return data

    elif fmt == 'result':

        # Result: answer to operations
        if root.attrib.has_key('code'):
            msg = root.attrib['code']
        else:
            msg = root.text

        # XXX: Return {'result':(True, msg)} for /known/ O.K. messages,
        # use (False, msg) otherwise. Move this to DeliciousAPI?
        v = msg in DLCS_OK_MESSAGES
        return {fmt: (v, msg)}

    elif fmt == 'update':

        # Update: "time"
        return {fmt: {
            'time':time.strptime(root.attrib['time'], ISO_8601_DATETIME) }}

    else:
        raise PyDeliciousException, "Unknown XML document format '%s'" % fmt


## Feed util

def dlcs_rss_request(tag="", popular=0, user="", url=''):
    """Parse a RSS request, old style.

    This requests old (now undocumented?) URL paths that still seem to work.

    - http://del.icio.us/rss/url/{urimd5}
    - http://del.icio.us/rss/{user}/{tag}
    - http://del.icio.us/rss/{user}
    - http://del.icio.us/rss
    - http://del.icio.us/rss/tag/{tag}
    - http://del.icio.us/rss/popular
    - http://del.icio.us/rss/popular/{tag}
    """

    tag = quote_plus(tag)
    user = quote_plus(user)

    if url != '':
        url = DLCS_RSS + 'url/%s' % md5(url).hexdigest()

    elif user != '' and tag != '':
        url = DLCS_RSS + '%(user)s/%(tag)s' % {'user':user, 'tag':tag}

    elif user != '' and tag == '':
        url = DLCS_RSS + '%s' % user

    elif popular == 0 and tag == '':
        url = DLCS_RSS

    elif popular == 0 and tag != '':
        url = DLCS_RSS + "tag/%s" % tag

    elif popular == 1 and tag == '':
        url = DLCS_RSS + 'popular'

    elif popular == 1 and tag != '':
        url = DLCS_RSS + 'popular/%s' % tag

    if DEBUG:
        print 'dlcs_rss_request', url

    rss = http_request(url).read()

    # assert feedparser, "requires feedparser to be installed."
    if not feedparser:
        return rss

    rss = feedparser.parse(rss)

    posts = []
    for e in rss.entries:
        if e.has_key("links") and e["links"]!=[] and e["links"][0].has_key("href"):
            url = e["links"][0]["href"]
        elif e.has_key("link"):
            url = e["link"]
        elif e.has_key("id"):
            url = e["id"]
        else:
            url = ""
        if e.has_key("title"):
            description = e['title']
        elif e.has_key("title_detail") and e["title_detail"].has_key("title"):
            description = e["title_detail"]['value']
        else:
            description = ''
        try:
            tags = [tag['term'] for tag in e['tags']]
        except:
            try:
                tags = [e["category"]]
            except:
                tags = []
        if e.has_key("modified"):
            dt = e['modified']
        else:
            dt = ""
        if e.has_key("summary"):
            extended = e['summary']
        elif e.has_key("summary_detail"):
            e['summary_detail']["value"]
        else:
            extended = ""
        if e.has_key("author"):
            user = e['author']
        else:
            user = ""
        #  time = dt ist weist auf ein problem hin
        # die benennung der variablen ist nicht einheitlich
        #  api senden und
        #  xml bekommen sind zwei verschiedene schuhe :(
        posts.append({'url':url, 'description':description, 'tags':tags,
                'dt':dt, 'extended':extended, 'user':user})
    return posts


"""
    Bookmarks from the hotlist:
        {format}
    Recent bookmarks:
        {format}/recent
    Recent bookmarks by tag:
        {format}/tag/{tag[+tag+...+tag]}
    Popular bookmarks:
        {format}/popular
    Popular bookmarks by tag:
        {format}/popular/{tag}
    Recent site alerts (as seen in the top-of-page alert bar on the site):
        {format}/alerts
    Public summary information about a user (as seen in the network badge):
        {format}/userinfo/{username}
    A list of all public tags for a user:
        {format}/tags/{username}
    A list of related public tags for a user tag comination:
        {format}/tags/{username}/{tag[+tag+...+tag]}
    Bookmarks from a user's subscriptions:
        {format}/subscriptions/{username}
    Private feed for a user's inbox bookmarks from others:
        {format}/inbox/{username}?private={key}
    Bookmarks from members of a user's network:
        {format}/network/{username}
    Bookmarks from members of a user's private network:
        {format}/network/{username}?private={key}
    Bookmarks from members of a user's network by tag:
        {format}/network/{username}/{tag[+tag+...+tag]}
    Bookmarks from members of a user's private network by tag:
        {format}/network/{username}/{tag[+tag+...+tag]}?private={key}
    A list of a user's network members:
        {format}/networkmembers/{username}
    A list of a user's network fans:
        {format}/networkfans/{username}
    Recent bookmarks for a URL:
        {format}/url/{url md5}
    Summary information about a URL (as seen in the tagometer):
        json/urlinfo/{url md5}
"""
delicious_v2_feeds = {
    # Bookmarks from the hotlist
    'hotlist': "%(format)s",
    #"Recent bookmarks"
    'recent': "%(format)s/recent",
    #"Recent bookmarks by tag"
    'tagged': "%(format)s/tag/%(tag)s",
    #"Popular bookmarks"
    'popular': "%(format)s/popular",
    #"Popular bookmarks by tag"
    'popular_tagged': "%(format)s/popular/%(tag)s",
    #"Recent site alerts (as seen in the top-of-page alert bar on the site)"
    'alerts': "%(format)s/alerts",
    # Bookmarks for a specific user:
    'user': "%(format)s/%(username)s",
    # Private bookmarks for a specific user:
    'user_private': "%(format)s/%(username)s?private=%(key)s",
    # Bookmarks for a specific user by tag(s)
    'user_tagged': "%(format)s/%(username)s/%(tag)s",
    # Private bookmarks for a specific user by tag(s):
    'user_tagged_private': "%(format)s/%(username)s/%(tag)s?private=%(key)s",
    #"Public summary information about a user (as seen in the network badge)"
    'user_info': "%(format)s/userinfo/%(username)s",
    #"A list of all public tags for a user"
    'user_tags': "%(format)s/tags/%(username)s",
    #"Bookmarks from a user's subscriptions"
    'user_subscription': "%(format)s/subscriptions/%(username)s",
    #"Private feed for a user's inbox bookmarks from others"
    'user_inbox': "%(format)s/inbox/%(username)s?private=%(key)s",
    #"Bookmarks from members of a user's network"
    'user_network': "%(format)s/network/%(username)s",
    #"Bookmarks from members of a user's network by tag"
    'user_network_tagged': "%(format)s/network/%(username)s/%(tag)s",
    #"A list of a user's network members"
    'user_network_member': "%(format)s/networkmembers/%(username)s",
    #"A list of a user's network fans"
    'user_network_fan': "%(format)s/networkfans/%(username)s",
    #"Recent bookmarks for a URL"
    'url': "%(format)s/url/%(urlmd5)s",
    #"Summary information about a URL (as seen in the tagometer)"
    'urlinfo': "json/urlinfo/%(urlmd5)s",
}


def dlcs_feed(name_or_url, url_map=delicious_v2_feeds, count=15, **kwds):

    """
    Request and parse a feed.
    Count should be between 1 and 100, default 15.
    Format values include 'rss' and 'json', defaults to json.

    - http://www.delicious.com/help/feeds
    """

    #if fancy == True:
    #    '?fancy'
    #elif fancy != None:        
    #    '?plain'
    format = kwds.setdefault('format', 'json')
    kwds.setdefault('count', count)

    if not name_or_url:
        name_or_url = 'hotlist'
    if name_or_url in url_map:
        params = dict([(k, quote_plus(str(v))) for k,v in kwds.items()])
        url = DLCS_FEEDS + url_map[name_or_url] % params
    else:
        url = name_or_url

    if DEBUG:
        print 'dlcs_feed', url

    feed = http_request(url).read()

    if format == 'rss':
        if feedparser:
            rss = feedparser.parse(feed)
            return rss
        else:
            return feed
    elif format == 'json':
        return feed


### Main module class

class DeliciousAPI:

    """A single-user Python facade to the del.icio.us HTTP API.

    See http://delicious.com/help/api.

    Methods ``request`` and ``request_raw`` represent the core. For all API
    paths there are furthermore methods (e.g. posts_add for 'posts/all') with
    an explicit declaration of parameters and documentation. 
    """

    def __init__(self, user, passwd, codec=PREFERRED_ENCODING,
            api_request=dlcs_api_request, xml_parser=dlcs_parse_xml,
            build_opener=dlcs_api_opener, encode_params=dlcs_encode_params,
            encoded=False):

        """Initialize access to the API for ``user`` with ``passwd``.

        ``codec`` sets the encoding of the arguments, which defaults to the
        users preferred locale.

        The ``api_request`` and ``xml_parser`` parameters by default point to
        functions within this package with standard implementations which
        request and parse a resource. See ``dlcs_api_request()`` and
        ``dlcs_parse_xml()``.

        Parameter ``build_opener`` is a callable that, provided with the 
        credentials, should build a urllib2 opener for the delicious API server
        with HTTP authentication. See ``dlcs_api_opener()`` for the default
        implementation.

        ``encode_params`` finally preprocesses API parameters before
        they are passed to ``api_request``.
        """

        assert user != ""
        self.user = user
        self.passwd = <PASSWORD>
        self.codec = codec

        # Implement communication to server and parsing of respons messages:
        assert callable(encode_params)
        self._encode_params = encode_params
        self._encoded = encoded
        assert callable(build_opener)
        self._opener = build_opener(user, passwd)
        assert callable(api_request)
        self._api_request = api_request
        assert callable(xml_parser)
        self._parse_response = xml_parser

    ### Core functionality

    def request(self, path, _raw=False, **params):
        """Sends a request message to `path` in the API, and parses the results
        from XML. Use with ``_raw=True`` or ``call request_raw()`` directly
        to get the filehandler and process the response message manually.

        Calls to some paths will return a `result` message, i.e.::

            <result code="..." />

        or::

            <result>...</result>

        These should all be parsed to ``{'result':(Boolean, MessageString)}``,
        this method raises a ``DeliciousError`` on negative `result` answers.
        Positive answers are silently accepted and nothing is returned.

        Using ``_raw=True`` bypasses all parsing and never raises
        ``DeliciousError``.

        See ``dlcs_parse_xml()`` and ``self.request_raw()``."""

        if _raw:
            # return answer
            return self.request_raw(path, **params)

        else:
            params = self._encode_params(params, self.codec,
                    encoded=self._encoded)

            # get answer and parse
            fl = self._api_request(path, params=params, opener=self._opener)
            rs = self._parse_response(fl)

            if type(rs) == dict and 'result' in rs:
                if not rs['result'][0]:
                    # Raise an error for negative 'result' answers
                    errmsg = ""
                    if len(rs['result'])>0:
                        errmsg = rs['result'][1]
                    DeliciousError.raiseFor(errmsg, path, **params)

                else:
                    # not out-of-the-oridinary result, OK
                    return

            return rs

    def request_raw(self, path, **params):
        """Calls the path in the API, returns the filehandle. Returned file-
        like instances have an ``HTTPMessage`` instance with HTTP header
        information available. Use ``filehandle.info()`` or refer to the
        ``urllib2.openurl`` documentation.
        """
        # see `request()` on how the response can be handled
        params = self._encode_params(params, self.codec, encoded=self._encoded)
        return self._api_request(path, params=params, opener=self._opener)

    ### Explicit declarations of API paths, their parameters and docs

    # Tags
    def tags_get(self, **kwds):
        """Returns a list of tags and the number of times it is used by the
        user.
        ::

            <tags>
                <tag tag="TagName" count="888">
        """
        return self.request("tags/get", **kwds)

    def tags_delete(self, tag, **kwds):
        """Delete an existing tag.

        &tag={TAG}
            (required) Tag to delete
        """
        return self.request('tags/delete', tag=tag, **kwds)

    def tags_rename(self, old, new, **kwds):
        """Rename an existing tag with a new tag name. Returns a `result`
        message or raises an ``DeliciousError``. See ``self.request()``.

        &old={TAG}
            (required) Tag to rename.
        &new={TAG}
            (required) New tag name.
        """
        return self.request("tags/rename", old=old, new=new, **kwds)

    # Posts
    def posts_update(self, **kwds):
        """Returns the last update time for the user. Use this before calling
        `posts_all` to see if the data has changed since the last fetch.
        ::

            <update time="CCYY-MM-DDThh:mm:ssZ">
        """
        return self.request("posts/update", **kwds)

    def posts_dates(self, tag="", **kwds):
        """Returns a list of dates with the number of posts at each date.
        ::

            <dates>
                <date date="CCYY-MM-DD" count="888">

        &tag={TAG}
            (optional) Filter by this tag
        """
        return self.request("posts/dates", tag=tag, **kwds)

    def posts_get(self, tag="", dt="", url="", hashes=[], meta=True, **kwds):
        """Returns posts matching the arguments. If no date or url is given,
        most recent date will be used.
        ::

            <posts dt="CCYY-MM-DD" tag="..." user="...">
                <post ...>

        &tag={TAG} {TAG} ... {TAG}
            (optional) Filter by this/these tag(s).
        &dt={CCYY-MM-DDThh:mm:ssZ}
            (optional) Filter by this date, defaults to the most recent date on
            which bookmarks were saved.
        &url={URL}
            (optional) Fetch a bookmark for this URL, regardless of date.
        &hashes={MD5} {MD5} ... {MD5}
            (optional) Fetch multiple bookmarks by one or more URL MD5s
            regardless of date.
        &meta=yes
            (optional) Include change detection signatures on each item in a
            'meta' attribute. Clients wishing to maintain a synchronized local
            store of bookmarks should retain the value of this attribute - its
            value will change when any significant field of the bookmark
            changes.
        """
        return self.request("posts/get", tag=tag, dt=dt, url=url,
                hashes=hashes, meta=meta, **kwds)

    def posts_recent(self, tag="", count="", **kwds):
        """Returns a list of the most recent posts, filtered by argument.
        ::

            <posts tag="..." user="...">
                <post ...>

        &tag={TAG}
            (optional) Filter by this tag.
        &count={1..100}
            (optional) Number of items to retrieve (Default:15, Maximum:100).
        """
        return self.request("posts/recent", tag=tag, count=count, **kwds)

    def posts_all(self, tag="", start=None, results=None, fromdt=None,
            todt=None, meta=True, hashes=False, **kwds):
        """Returns all posts. Please use sparingly. Call the `posts_update`
        method to see if you need to fetch this at all.
        ::

            <posts tag="..." user="..." update="CCYY-MM-DDThh:mm:ssZ">
                <post ...>

        &tag
            (optional) Filter by this tag.
        &start={#}
            (optional) Start returning posts this many results into the set.
        &results={#}
            (optional) Return this many results.
        &fromdt={CCYY-MM-DDThh:mm:ssZ}
            (optional) Filter for posts on this date or later
        &todt={CCYY-MM-DDThh:mm:ssZ}
            (optional) Filter for posts on this date or earlier
        &meta=yes
            (optional) Include change detection signatures on each item in a
            'meta' attribute. Clients wishing to maintain a synchronized local
            store of bookmarks should retain the value of this attribute - its
            value will change when any significant field of the bookmark
            changes.
        &hashes
            (optional, exclusive) Do not fetch post details but a posts
            manifest with url- and meta-hashes. Other options do not apply.
        """
        if hashes:
            return self.request("posts/all", hashes=hashes, **kwds)
        else:
            return self.request("posts/all", tag=tag, fromdt=fromdt, todt=todt,
                    start=start, results=results, meta=meta, **kwds)

    def posts_add(self, url, description, extended="", tags="", dt="",
            replace=False, shared=True, **kwds):
        """Add a post to del.icio.us. Returns a `result` message or raises an
        ``DeliciousError``. See ``self.request()``.

        &url (required)
            the url of the item.
        &description (required)
            the description of the item.
        &extended (optional)
            notes for the item.
        &tags (optional)
            tags for the item (space delimited).
        &dt (optional)
            datestamp of the item (format "CCYY-MM-DDThh:mm:ssZ").
            Requires a LITERAL "T" and "Z" like in ISO8601 at
            http://www.cl.cam.ac.uk/~mgk25/iso-time.html for example:
            "1984-09-01T14:21:31Z"
        &replace=no (optional) - don't replace post if given url has already
            been posted.
        &shared=yes (optional) - wether the item is public.
        """
        return self.request("posts/add", url=url, description=description,
                extended=extended, tags=tags, dt=dt,
                replace=replace, shared=shared, **kwds)

    def posts_delete(self, url, **kwds):
        """Delete a post from del.icio.us. Returns a `result` message or
        raises an ``DeliciousError``. See ``self.request()``.

        &url (required)
            the url of the item.
        """
        return self.request("posts/delete", url=url, **kwds)

    # Bundles
    def bundles_all(self, **kwds):
        """Retrieve user bundles from del.icio.us.
        ::

            <bundles>
                <bundel name="..." tags=...">
        """
        return self.request("tags/bundles/all", **kwds)

    def bundles_set(self, bundle, tags, **kwds):
        """Assign a set of tags to a single bundle, wipes away previous
        settings for bundle. Returns a `result` messages or raises an
        ``DeliciousError``. See ``self.request()``.

        &bundle (required)
            the bundle name.
        &tags (required)
            list of tags.
        """
        if type(tags)==list:
            tags = " ".join(tags)
        return self.request("tags/bundles/set", bundle=bundle, tags=tags,
                **kwds)

    def bundles_delete(self, bundle, **kwds):
        """Delete a bundle from del.icio.us. Returns a `result` message or
        raises an ``DeliciousError``. See ``self.request()``.

        &bundle (required)
            the bundle name.
        """
        return self.request("tags/bundles/delete", bundle=bundle, **kwds)

    ### Utils

    # Lookup table for del.icio.us url-path to DeliciousAPI method.
    paths = {
        'tags/get': 'tags_get',
        'tags/delete': 'tags_delete',
        'tags/rename': 'tags_rename',
        'posts/update': 'posts_update',
        'posts/dates': 'posts_dates',
        'posts/get': 'posts_get',
        'posts/recent': 'posts_recent',
        'posts/all': 'posts_all',
        'posts/add': 'posts_add',
        'posts/delete': 'posts_delete',
        'tags/bundles/all': 'bundles_all',
        'tags/bundles/set': 'bundles_set',
        'tags/bundles/delete': 'bundles_delete',
    }
    def get_method(self, path):
        return getattr(self, self.paths[path])

    def get_url(self, url):
        """Return the del.icio.us url at which the HTML page with posts for
        ``url`` can be found.
        """
        return "http://del.icio.us/url/?url=%s" % (url,)

    def __repr__(self):
        return "DeliciousAPI(%s)" % self.user


### Quick API access

def apiNew(user, passwd):
    "Creates a new DeliciousAPI object, requires user(name) and passwd."
    return DeliciousAPI(user=user, passwd=passwd)

def add(user, passwd, url, description, tags="", extended="", dt=None,
        replace=False):
    "Add a post for user. "
    apiNew(user, passwd).posts_add(url=url, description=description,
            extended=extended, tags=tags, dt=dt, replace=replace)

def get(user, passwd, tag="", dt=None, count=0, hashes=[]):
    "Returns a list of posts for the user using the API. "
    posts = apiNew(user, passwd).posts_get(
            tag=tag, dt=dt, hashes=hashes)['posts']
    if count: posts = posts[:count]
    return posts

def get_update(user, passwd):
    "Returns the last update time for the user. "
    return apiNew(user, passwd).posts_update()['update']['time']

def get_all(user, passwd, tag="", start=0, results=100, fromdt=None,
        todt=None):
    "Returns a list with all posts. Please use sparingly. See `get_updated`"
    return apiNew(user, passwd).posts_all(tag=tag, start=start,
            results=results, fromdt=fromdt, todt=todt, meta=True)['posts']

def get_tags(user, passwd):
    "Returns a list with all tags for user."
    return apiNew(user=user, passwd=passwd).tags_get()['tags']

def delete(user, passwd, url):
    "Delete the URL from the del.icio.us account."
    apiNew(user, passwd).posts_delete(url=url)

def rename_tag(user, passwd, oldtag, newtag):
    "Rename the tag for the del.icio.us account."
    apiNew(user=user, passwd=passwd).tags_rename(old=oldtag, new=newtag)


### Old RSS

def getrss(tag="", popular=0, url='', user=""):
    """Get posts from del.icio.us via parsing RSS.

    tag (opt) sort by tag
    popular (opt) look for the popular stuff
    user (opt) get the posts by a user, this striks popular
    url (opt) get the posts by url
    """
    return dlcs_rss_request(tag=tag, popular=popular, user=user, url=url)

def get_userposts(user):
    "parse RSS for user"
    return getrss(user=user)

def get_tagposts(tag):
    "parse RSS for tag"
    return getrss(tag=tag)

def get_urlposts(url):
    "parse RSS for URL"
    return getrss(url=url)

def get_popular(tag=""):
    "parse RSS for popular URLS for tag"
    return getrss(tag=tag, popular=1)


### Feeds (RSS/JSON/?)

def user_posts(user=None, tag=None, key=None, **params):
    """
    Bookmarks for a specific user:
        {format}/{username}
    Private bookmarks for a specific user:
        {format}/{username}?private={key}
    Bookmarks for a specific user by tag(s):
        {format}/{username}/{tag[+tag+...+tag]}
    Private bookmarks for a specific user by tag(s):
        {format}/{username}/{tag[+tag+...+tag]}?private={key}
    """
    assert username
    if tag and key:
        path = 'user_tagged_private'
    elif tag:
        path = 'user_tagged'
    elif key:
        path = 'user_private'
    else:
        path = 'user'
    return dlcs_feed(path, user=user, tag=tag, key=key, **params)    	


def json_tags(user, atleast, count, sort='alpha', raw=True, callback=None):
    """
    user
    atleast=###         include only tags for which there are at least ### 
                        number of posts.
    count=###           include ### tags, counting down from the top.
    sort={alpha|count}  construct the object with tags in alphabetic order 
                        (alpha), or by count of posts (count).
    callback=NAME       wrap the object definition in a function call NAME(...),
                        thus invoking that function when the feed is executed.
    raw                 a pure JSON object is returned, instead of code that 
                        will construct an object named Delicious.tags.
    """
    url = 'http://del.icio.us/feeds/json/tags/' + \
            dlcs_encode_params({0:user})[0]
    return dlcs_feed(url, atleast=atleast, count=count, sort=sort, raw=raw, 
            callback=callback)


def json_network(user, raw=True, callback=None):
    """
    callback=NAME       wrap the object definition in a function call NAME(...)
    ?raw                a raw JSON object is returned, instead of an object named 
                        Delicious.posts
    """
    url = 'http://del.icio.us/feeds/json/network/' + \
            dlcs_encode_params({0:user})[0]
    return dlcs_feed(url, raw=raw, callback=callback)


def json_fans(user, raw=True, callback=None):
    """
    callback=NAME       wrap the object definition in a function call NAME(...)
    ?raw                a pure JSON object is returned, instead of an object named 
                        Delicious.
    """
    url = 'http://del.icio.us/feeds/json/fans/' + \
            dlcs_encode_params({0:user})[0]
    return dlcs_feed(url, raw=raw, callback=callback)


### delicious V2 feeds

def getfeed(name, **params):
    return dlcs_feed(name, **params)


Library to access del.icio.us data via Python.  An introduction to the project is given in the README.  pydelicious is released under the FreeBSD License.  See license.txt for details and the copyright holders.  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # Python >= 2.5 # Python 2.5 and higher ### Static config # pydoc and distutils supported exports #__date__ = "$Date$"[] <NAME> (original author), and in no particular order: <NAME>, me.gooz, mohangk, stumble.then.rise, clupprich # XXX: might need to check sys.platform/encoding combinations here, ie #if sys.platform == 'darwin' || PREFERRED_ENCODING == 'macroman: #   PREFERRED_ENCODING = 'utf-8' ### Timeoutsocket hack taken from FeedParser.py # timeoutsocket allows feedparser to time out rather than hang forever on ultra- # slow servers. Python 2.3 now has this functionality available in the standard # socket library, so under 2.3 you don't need to install anything.  But you # probably should anyway, because the socket module is buggy and timeoutsocket # is better. # http://www.timo-tasi.org/python/timeoutsocket.py ### Utility classes Waiter makes sure a certain amount of time passes between     successive calls of `Waiter()`.      Some attributes:     :last: time of last call     :wait: the minimum time needed between calls     :waited: the number of calls throttled      pydelicious.Waiter is an instance created when the module is loaded. Standard pydelicious error Raised when the server responds with a negative answer Raised then adding an already existing post. # Retry-After? ### Utility functions Parse a ISO 8601 formatted string to a Python datetime ... Retrieve the contents referenced by the URL using urllib2.      Retries up to four times (default) on exceptions. # Remember last error # Repeat request on time-out errors # reraise unexpected protocol errors as PyDeliciousException # xxx: Ugly check for time-out errors #if len(e)>0 and 'timed out' in arg[0]: #else: #	tries = None # Give up Build a urllib2 style opener with HTTP Basic authorization for one host     and additional error handling. If HTTP_PROXY is set a proxyhandler is also     added. Retrieve/query a path within the del.icio.us API.      This implements a minimum interval between calls to avoid     throttling. [#]_ Use param 'throttle' to turn this behaviour off.      .. [#] http://del.icio.us/help/api/ Turn all param values (int, list, bool) into utf8 encoded strings. # strip/ignore empties other than False or 0 Parse any del.icio.us XML document and return Python data structure.      Recognizes all XML document formats as returned by the version 1 API and     translates to a JSON-like data structure (dicts 'n lists).      Returned instance is always a dictionary. Examples::       {'posts': [{'url':'...','hash':'...',},],}      {'tags':['tag1', 'tag2',]}      {'dates': [{'count':'...','date':'...'},], 'tag':'', 'user':'...'}      {'result':(True, "done")}      # etcetera. # TODO: split_tags is not implemented # Split up into three cases: Data, Result or Update # Data: expect a list of data elements, 'resources'. # Use `fmt` (without last 's') to find data elements, elements # don't have contents, attributes contain all the data we need: # append to list # Return list in dict, use tagname of rootnode as keyname. # Root element might have attributes too, append dict. # Result: answer to operations # XXX: Return {'result':(True, msg)} for /known/ O.K. messages, # use (False, msg) otherwise. Move this to DeliciousAPI? # Update: "time" ## Feed util Parse a RSS request, old style.      This requests old (now undocumented?) URL paths that still seem to work.      - http://del.icio.us/rss/url/{urimd5}     - http://del.icio.us/rss/{user}/{tag}     - http://del.icio.us/rss/{user}     - http://del.icio.us/rss     - http://del.icio.us/rss/tag/{tag}     - http://del.icio.us/rss/popular     - http://del.icio.us/rss/popular/{tag} # assert feedparser, "requires feedparser to be installed." #  time = dt ist weist auf ein problem hin # die benennung der variablen ist nicht einheitlich #  api senden und #  xml bekommen sind zwei verschiedene schuhe :( Bookmarks from the hotlist:         {format}     Recent bookmarks:         {format}/recent     Recent bookmarks by tag:         {format}/tag/{tag[+tag+...+tag]}     Popular bookmarks:         {format}/popular     Popular bookmarks by tag:         {format}/popular/{tag}     Recent site alerts (as seen in the top-of-page alert bar on the site):         {format}/alerts     Public summary information about a user (as seen in the network badge):         {format}/userinfo/{username}     A list of all public tags for a user:         {format}/tags/{username}     A list of related public tags for a user tag comination:         {format}/tags/{username}/{tag[+tag+...+tag]}     Bookmarks from a user's subscriptions:         {format}/subscriptions/{username}     Private feed for a user's inbox bookmarks from others:         {format}/inbox/{username}?private={key}     Bookmarks from members of a user's network:         {format}/network/{username}     Bookmarks from members of a user's private network:         {format}/network/{username}?private={key}     Bookmarks from members of a user's network by tag:         {format}/network/{username}/{tag[+tag+...+tag]}     Bookmarks from members of a user's private network by tag:         {format}/network/{username}/{tag[+tag+...+tag]}?private={key}     A list of a user's network members:         {format}/networkmembers/{username}     A list of a user's network fans:         {format}/networkfans/{username}     Recent bookmarks for a URL:         {format}/url/{url md5}     Summary information about a URL (as seen in the tagometer):         json/urlinfo/{url md5} # Bookmarks from the hotlist #"Recent bookmarks" #"Recent bookmarks by tag" #"Popular bookmarks" #"Popular bookmarks by tag" #"Recent site alerts (as seen in the top-of-page alert bar on the site)" # Bookmarks for a specific user: # Private bookmarks for a specific user: # Bookmarks for a specific user by tag(s) # Private bookmarks for a specific user by tag(s): #"Public summary information about a user (as seen in the network badge)" #"A list of all public tags for a user" #"Bookmarks from a user's subscriptions" #"Private feed for a user's inbox bookmarks from others" #"Bookmarks from members of a user's network" #"Bookmarks from members of a user's network by tag" #"A list of a user's network members" #"A list of a user's network fans" #"Recent bookmarks for a URL" #"Summary information about a URL (as seen in the tagometer)" Request and parse a feed.     Count should be between 1 and 100, default 15.     Format values include 'rss' and 'json', defaults to json.      - http://www.delicious.com/help/feeds #if fancy == True: #    '?fancy' #elif fancy != None: #    '?plain' ### Main module class A single-user Python facade to the del.icio.us HTTP API.      See http://delicious.com/help/api.      Methods ``request`` and ``request_raw`` represent the core. For all API     paths there are furthermore methods (e.g. posts_add for 'posts/all') with     an explicit declaration of parameters and documentation. Initialize access to the API for ``user`` with ``passwd``.          ``codec`` sets the encoding of the arguments, which defaults to the         users preferred locale.          The ``api_request`` and ``xml_parser`` parameters by default point to         functions within this package with standard implementations which         request and parse a resource. See ``dlcs_api_request()`` and         ``dlcs_parse_xml()``.          Parameter ``build_opener`` is a callable that, provided with the          credentials, should build a urllib2 opener for the delicious API server         with HTTP authentication. See ``dlcs_api_opener()`` for the default         implementation.          ``encode_params`` finally preprocesses API parameters before         they are passed to ``api_request``. # Implement communication to server and parsing of respons messages: ### Core functionality Sends a request message to `path` in the API, and parses the results         from XML. Use with ``_raw=True`` or ``call request_raw()`` directly         to get the filehandler and process the response message manually.          Calls to some paths will return a `result` message, i.e.::              <result code="..." />          or::              <result>...</result>          These should all be parsed to ``{'result':(Boolean, MessageString)}``,         this method raises a ``DeliciousError`` on negative `result` answers.         Positive answers are silently accepted and nothing is returned.          Using ``_raw=True`` bypasses all parsing and never raises         ``DeliciousError``.          See ``dlcs_parse_xml()`` and ``self.request_raw()``. # return answer # get answer and parse # Raise an error for negative 'result' answers # not out-of-the-oridinary result, OK Calls the path in the API, returns the filehandle. Returned file-         like instances have an ``HTTPMessage`` instance with HTTP header         information available. Use ``filehandle.info()`` or refer to the         ``urllib2.openurl`` documentation. # see `request()` on how the response can be handled ### Explicit declarations of API paths, their parameters and docs # Tags Returns a list of tags and the number of times it is used by the         user.         ::              <tags>                 <tag tag="TagName" count="888"> Delete an existing tag.          &tag={TAG}             (required) Tag to delete Rename an existing tag with a new tag name. Returns a `result`         message or raises an ``DeliciousError``. See ``self.request()``.          &old={TAG}             (required) Tag to rename.         &new={TAG}             (required) New tag name. # Posts Returns the last update time for the user. Use this before calling         `posts_all` to see if the data has changed since the last fetch.         ::              <update time="CCYY-MM-DDThh:mm:ssZ"> Returns a list of dates with the number of posts at each date.         ::              <dates>                 <date date="CCYY-MM-DD" count="888">          &tag={TAG}             (optional) Filter by this tag Returns posts matching the arguments. If no date or url is given,         most recent date will be used.         ::              <posts dt="CCYY-MM-DD" tag="..." user="...">                 <post ...>          &tag={TAG} {TAG} ... {TAG}             (optional) Filter by this/these tag(s).         &dt={CCYY-MM-DDThh:mm:ssZ}             (optional) Filter by this date, defaults to the most recent date on             which bookmarks were saved.         &url={URL}             (optional) Fetch a bookmark for this URL, regardless of date.         &hashes={MD5} {MD5} ... {MD5}             (optional) Fetch multiple bookmarks by one or more URL MD5s             regardless of date.         &meta=yes             (optional) Include change detection signatures on each item in a             'meta' attribute. Clients wishing to maintain a synchronized local             store of bookmarks should retain the value of this attribute - its             value will change when any significant field of the bookmark             changes. Returns a list of the most recent posts, filtered by argument.         ::              <posts tag="..." user="...">                 <post ...>          &tag={TAG}             (optional) Filter by this tag.         &count={1..100}             (optional) Number of items to retrieve (Default:15, Maximum:100). Returns all posts. Please use sparingly. Call the `posts_update`         method to see if you need to fetch this at all.         ::              <posts tag="..." user="..." update="CCYY-MM-DDThh:mm:ssZ">                 <post ...>          &tag             (optional) Filter by this tag.         &start={#}             (optional) Start returning posts this many results into the set.         &results={#}             (optional) Return this many results.         &fromdt={CCYY-MM-DDThh:mm:ssZ}             (optional) Filter for posts on this date or later         &todt={CCYY-MM-DDThh:mm:ssZ}             (optional) Filter for posts on this date or earlier         &meta=yes             (optional) Include change detection signatures on each item in a             'meta' attribute. Clients wishing to maintain a synchronized local             store of bookmarks should retain the value of this attribute - its             value will change when any significant field of the bookmark             changes.         &hashes             (optional, exclusive) Do not fetch post details but a posts             manifest with url- and meta-hashes. Other options do not apply. Add a post to del.icio.us. Returns a `result` message or raises an         ``DeliciousError``. See ``self.request()``.          &url (required)             the url of the item.         &description (required)             the description of the item.         &extended (optional)             notes for the item.         &tags (optional)             tags for the item (space delimited).         &dt (optional)             datestamp of the item (format "CCYY-MM-DDThh:mm:ssZ").             Requires a LITERAL "T" and "Z" like in ISO8601 at             http://www.cl.cam.ac.uk/~mgk25/iso-time.html for example:             "1984-09-01T14:21:31Z"         &replace=no (optional) - don't replace post if given url has already             been posted.         &shared=yes (optional) - wether the item is public. Delete a post from del.icio.us. Returns a `result` message or         raises an ``DeliciousError``. See ``self.request()``.          &url (required)             the url of the item. # Bundles Retrieve user bundles from del.icio.us.         ::              <bundles>                 <bundel name="..." tags=..."> Assign a set of tags to a single bundle, wipes away previous         settings for bundle. Returns a `result` messages or raises an         ``DeliciousError``. See ``self.request()``.          &bundle (required)             the bundle name.         &tags (required)             list of tags. Delete a bundle from del.icio.us. Returns a `result` message or         raises an ``DeliciousError``. See ``self.request()``.          &bundle (required)             the bundle name. ### Utils # Lookup table for del.icio.us url-path to DeliciousAPI method. Return the del.icio.us url at which the HTML page with posts for         ``url`` can be found. ### Quick API access ### Old RSS Get posts from del.icio.us via parsing RSS.      tag (opt) sort by tag     popular (opt) look for the popular stuff     user (opt) get the posts by a user, this striks popular     url (opt) get the posts by url ### Feeds (RSS/JSON/?) Bookmarks for a specific user:         {format}/{username}     Private bookmarks for a specific user:         {format}/{username}?private={key}     Bookmarks for a specific user by tag(s):         {format}/{username}/{tag[+tag+...+tag]}     Private bookmarks for a specific user by tag(s):         {format}/{username}/{tag[+tag+...+tag]}?private={key} user     atleast=###         include only tags for which there are at least ###                          number of posts.     count=###           include ### tags, counting down from the top.     sort={alpha|count}  construct the object with tags in alphabetic order                          (alpha), or by count of posts (count).     callback=NAME       wrap the object definition in a function call NAME(...),                         thus invoking that function when the feed is executed.     raw                 a pure JSON object is returned, instead of code that                          will construct an object named Delicious.tags. callback=NAME       wrap the object definition in a function call NAME(...)     ?raw                a raw JSON object is returned, instead of an object named                          Delicious.posts callback=NAME       wrap the object definition in a function call NAME(...)     ?raw                a pure JSON object is returned, instead of an object named                          Delicious. ### delicious V2 feeds