summaryrefslogtreecommitdiff
path: root/lib/utilslib/pydelicious.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/utilslib/pydelicious.py')
-rw-r--r--lib/utilslib/pydelicious.py1045
1 files changed, 1045 insertions, 0 deletions
diff --git a/lib/utilslib/pydelicious.py b/lib/utilslib/pydelicious.py
new file mode 100644
index 0000000..8e45843
--- /dev/null
+++ b/lib/utilslib/pydelicious.py
@@ -0,0 +1,1045 @@
+"""Library to access del.icio.us data via Python.
+
+An introduction to the project is given in the README.
+pydelicious is released under the BSD 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.
+
+TODO:
+ - distribute license, readme docs via setup.py?
+ - automatic release build?
+"""
+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
+
+v = sys.version_info
+if v[0] >= 2 and v[1] >= 5:
+ from hashlib import md5
+else:
+ 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
+
+__version__ = '0.5.3'
+__author__ = 'Frank Timmermann <regenkind_at_gmx_dot_de>'
+ # GP: does not respond to emails
+__contributors__ = [
+ 'Greg Pinero',
+ 'Berend van Berkum <berend+pydelicious@dotmpe.com>']
+__url__ = 'http://code.google.com/p/pydelicious/'
+# Old URL: 'http://deliciouspython.python-hosting.com/'
+__author_email__ = ""
+__docformat__ = "restructuredtext en"
+__description__ = "pydelicious.py allows you to access the web service of " \
+ "del.icio.us via it's API through Python."
+__long_description__ = "The goal is to design an easy to use and fully " \
+ "functional Python interface to del.icio.us."
+
+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://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 = None
+if 'HTTP_PROXY' in os.environ:
+ HTTP_PROXY = os.environ['HTTP_PROXY']
+ if DEBUG:
+ print >>sys.stderr, \
+ "Set HTTP_PROXY to %i from env." % HTTP_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 HTTPErrorHandler(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
+
+ if DEBUG: httplib.HTTPConnection.debuglevel = 1
+
+ password_manager = urllib2.HTTPPasswordMgrWithDefaultRealm()
+ password_manager.add_password(None, host, user, passwd)
+ auth_handler = urllib2.HTTPBasicAuthHandler(password_manager)
+
+ extra_handlers += ( HTTPErrorHandler(), )
+ if HTTP_PROXY:
+ extra_handlers += ( urllib2.ProxyHandler( {'http': HTTP_PROXY} ), )
+
+ return urllib2.build_opener(auth_handler, *extra_handlers)
+
+
+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='', 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):
+ """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])
+
+ elif not isinstance(params[key], unicode):
+ params[key] = params[key].decode(usercodec)
+
+ assert isinstance(params[key], basestring)
+
+ 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
+
+
+def dlcs_rss_request(tag="", popular=0, user="", url=''):
+ """Parse a RSS request.
+
+ This requests old (now undocumented?) URL paths that still seem to work.
+ """
+
+ tag = quote_plus(tag)
+ user = quote_plus(user)
+
+ if url != '':
+ # http://del.icio.us/rss/url/efbfb246d886393d48065551434dab54
+ 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 == '':
+ # http://del.icio.us/rss/delpy
+ url = DLCS_RSS + '%s' % user
+
+ elif popular == 0 and tag == '':
+ url = DLCS_RSS
+
+ elif popular == 0 and tag != '':
+ # http://del.icio.us/rss/tag/apple
+ # http://del.icio.us/rss/tag/web2.0
+ 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, "dlcs_rss_request 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 = e['categories'][0][1]
+ 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
+
+
+delicious_v2_feeds = {
+ #"Bookmarks from the hotlist"
+ '': "%(format)s",
+ #"Recent bookmarks"
+ 'recent': "%(format)s/recent",
+ #"Recent bookmarks by tag"
+ 'tagged': "%(format)s/tag/%(tags)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",
+ #"Bookmarks for a specific user by tag(s)"
+ 'user_tagged': "%(format)s/%(username)s/%(tags)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/%(tags)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, **params):
+
+ """
+ Request and parse a feed. See delicious_v2_feeds for available names and
+ required parameters. Format defaults to json.
+ """
+
+# http://delicious.com/help/feeds
+# TODO: plain or fancy
+
+ format = params.setdefault('format', 'json')
+ if count == 'all':
+# TODO: fetch all
+ print >>sys.stderr, "! Maxcount 100 "
+ count = 100
+
+ if name_or_url in url_map:
+ params['count'] = count
+ 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):
+
+ """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 = passwd
+ self.codec = codec
+
+ # Implement communication to server and parsing of respons messages:
+ assert callable(encode_params)
+ self._encode_params = encode_params
+ 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)
+
+ # 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)
+ 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
+
+
+### Convenience functions on this package
+
+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):
+ 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"
+ 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)
+
+
+### RSS functions
+
+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)
+
+
+### JSON feeds
+# TODO: untested
+
+def json_posts(user, count=15, tag=None, raw=True):
+ """
+ user
+ count=### the number of posts you want to get (default is 15, maximum
+ is 100)
+ raw a raw JSON object is returned, instead of an object named
+ Delicious.posts
+ """
+ url = "http://del.icio.us/feeds/json/" + \
+ dlcs_encode_params({0:user})[0]
+ if tag: url += '/'+dlcs_encode_params({0:tag})[0]
+
+ return dlcs_feed(url, count=count, raw=raw)
+
+
+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)
+