diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/context-processors/__init__.py | 0 | ||||
-rw-r--r-- | lib/context-processors/context_processors.py | 3 | ||||
-rw-r--r-- | lib/pydelicious.py | 1045 | ||||
-rw-r--r-- | lib/utils/APIClients.py (renamed from lib/APIClients.py) | 0 | ||||
-rw-r--r-- | lib/utils/GeoClient.py | 292 | ||||
-rw-r--r-- | lib/utils/email_multipart.py | 80 | ||||
-rwxr-xr-x | lib/utils/markdown2.py (renamed from lib/markdown2.py) | 0 | ||||
-rw-r--r-- | lib/utils/pydelicious.py | 858 | ||||
-rw-r--r-- | lib/utils/strutils.py (renamed from lib/strutils.py) | 0 | ||||
-rw-r--r-- | lib/view_wrapper.py | 10 |
10 files changed, 543 insertions, 1745 deletions
diff --git a/lib/context-processors/__init__.py b/lib/context-processors/__init__.py deleted file mode 100644 index e69de29..0000000 --- a/lib/context-processors/__init__.py +++ /dev/null diff --git a/lib/context-processors/context_processors.py b/lib/context-processors/context_processors.py deleted file mode 100644 index 5691102..0000000 --- a/lib/context-processors/context_processors.py +++ /dev/null @@ -1,3 +0,0 @@ -def media_url(request): - from django.conf import settings - return {'media_url': settings.MEDIA_URL, 'map_key':settings.MAP_API_KEY} diff --git a/lib/pydelicious.py b/lib/pydelicious.py deleted file mode 100644 index 8e45843..0000000 --- a/lib/pydelicious.py +++ /dev/null @@ -1,1045 +0,0 @@ -"""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) - diff --git a/lib/APIClients.py b/lib/utils/APIClients.py index 24ab97b..24ab97b 100644 --- a/lib/APIClients.py +++ b/lib/utils/APIClients.py diff --git a/lib/utils/GeoClient.py b/lib/utils/GeoClient.py deleted file mode 100644 index d1966ca..0000000 --- a/lib/utils/GeoClient.py +++ /dev/null @@ -1,292 +0,0 @@ -# -*- coding: utf-8 -*- - -"""Python wrapper for geoname web APIs - -created 20/03/2006 By Nicolas Laurance - -This module allows you to access geoname's web APIs, -and get the results programmatically. -Described here: - http://www.geonames.org/export/ - -def postalCodeSearch(postalcode, placename='', country=COUNTRY, maxRows='10', http_proxy=None): -def postalCodeLookupJSON(postalcode, placename='', country=COUNTRY, maxRows='10',gcallback='', http_proxy=None): -def findNearbyPostalCodes(postalcode, placename='', country=COUNTRY, radius='5', maxRows='10',lat=None,lng=None, http_proxy=None): -def postalCodeCountryInfo(http_proxy=None): -def search(placename='', country=COUNTRY, maxRows='10', style='SHORT',lang=LANG, fclass=None, http_proxy=None): -def findNearbyPlaceName(lat,lng, http_proxy=None): - -Sample usage: ->>> import geoname ->>> result=geoname.postalCodeSearch('35580','guichen','fr','10') ->>> result.totalResultsCount.PCDATA -u'1' ->>> result.code[0].lat.PCDATA -u'47.9666667' ->>> result.code[0].lng.PCDATA -u'-1.8' - - - -""" - -__author__ = "Nicolas Laurance (nlaurance@zindep.com)" -__version__ = "2.0" -__cvsversion__ = "$Revision: 2.0 $"[11:-2] -__date__ = "$Date: 2003/06/20 22:40:53 $"[7:-2] -__copyright__ = "Copyright (c) 2006 Nicolas Laurance" -__license__ = "Python" - -import gnosis.xml.objectify as objectify - -import os, sys, urllib, re -try: - import timeoutsocket # http://www.timo-tasi.org/python/timeoutsocket.py - timeoutsocket.setDefaultSocketTimeout(120) -except ImportError: - pass - -HTTP_PROXY = None -DEBUG = 0 -COUNTRY = 'FR' -LANG ='fr' - - -# don't touch the rest of these constants -class GeonameError(Exception): pass - -## administrative functions -def version(): - print """PyGeoname %(__version__)s -%(__copyright__)s -released %(__date__)s -""" % globals() - -def setProxy(http_proxy): - """set HTTP proxy""" - global HTTP_PROXY - HTTP_PROXY = http_proxy - -def getProxy(http_proxy = None): - """get HTTP proxy""" - return http_proxy or HTTP_PROXY - -def getProxies(http_proxy = None): - http_proxy = getProxy(http_proxy) - if http_proxy: - proxies = {"http": http_proxy} - else: - proxies = None - return proxies - -def _contentsOf(dirname, filename): - filename = os.path.join(dirname, filename) - if not os.path.exists(filename): return None - fsock = open(filename) - contents = fsock.read() - fsock.close() - return contents - -def _getScriptDir(): - if __name__ == '__main__': - return os.path.abspath(os.path.dirname(sys.argv[0])) - else: - return os.path.abspath(os.path.dirname(sys.modules[__name__].__file__)) - -class Bag: pass - -_intFields = ('totalResultsCount') -_dateFields = () -_listFields = ('code','geoname','country',) -_floatFields = ('lat','lng','distance') - -def unmarshal(element): - #import pdb;pdb.set_trace() - xml_obj = objectify.XML_Objectify(element) - rc = xml_obj.make_instance() - return rc - -def _do(url, http_proxy): - proxies = getProxies(http_proxy) - u = urllib.FancyURLopener(proxies) - usock = u.open(url) - rawdata = usock.read() - if DEBUG: print rawdata - usock.close() - data = unmarshal(rawdata) - return data - -## main functions - -def _buildfindNearbyPostalCodes(postalcode, placename, country, radius, maxRows ): - placename=urllib.quote(placename) - searchUrl = "http://ws.geonames.org/findNearbyPostalCodes?postalcode=%(postalcode)s&placename=%(placename)s&country=%(country)s&radius=%(radius)s&maxRows=%(maxRows)s" % vars() - return searchUrl - - -def _buildpostalCodeLookupJSON(postalcode,placename,country,maxRows,gcallback): - placename=urllib.quote(placename) - searchUrl = "http://ws.geonames.org/postalCodeLookupJSON?postalcode=%(postalcode)s&placename=%(placename)s&country=%(country)s&maxRows=%(maxRows)s&callback=%(gcallback)s" % vars() - return searchUrl - -def _buildfindNearbyPostalCodesLL(lat,lng,radius,maxRows): - searchUrl = "http://ws.geonames.org/findNearbyPostalCodes?lat=%(lat)s&lng=%(lng)s&radius=%(radius)s&maxRows=%(maxRows)s" % vars() - return searchUrl - -def _buildfindCountrySubdivision(lat,lng): - searchUrl = "http://ws.geonames.org/countrySubdivision?lat=%(lat)s&lng=%(lng)s" % vars() - return searchUrl - -def _buildfindNearbyPlaceName(lat,lng): - searchUrl = "http://ws.geonames.org/findNearbyPlaceName?lat=%(lat)s&lng=%(lng)s" % vars() - return searchUrl - -def _buildpostalCodeSearch(postalcode, placename, country, maxRows ): - placename=urllib.quote(placename) - searchUrl = "http://ws.geonames.org/postalCodeSearch?postalcode=%(postalcode)s&placename=%(placename)s&country=%(country)s&maxRows=%(maxRows)s" % vars() - return searchUrl - -def _buildsearch(placename, country, maxRows,style,lang, fclass): - placename=urllib.quote(placename) - if fclass: - urlfclass='' - for fc in fclass: - urlfclass+=urllib.quote("&fclass=%s" % fc) - searchUrl = "http://ws.geonames.org/search?q=%(placename)s&country=%(country)s&maxRows=%(maxRows)s&lang=%(lang)s&style=%(style)s&fclass=%(fclass)s" % vars() - return searchUrl - -def postalCodeSearch(postalcode, placename='', country=COUNTRY, maxRows='10', http_proxy=None): - """ - http://ws.geonames.org/postalCodeSearch?postalcode=35580&maxRows=10&country=fr - Url : ws.geonames.org/postalCodeSearch? - Parameters : postalcode ,placename,maxRows,country - <geonames> - <totalResultsCount>7</totalResultsCount> - - - <code> - <postalcode>35580</postalcode> - <name>St Senoux</name> - <countryCode>FR</countryCode> - <lat>47.9</lat> - <lng>-1.7833333</lng> - </code> - """ - url = _buildpostalCodeSearch(postalcode,placename,country,maxRows) - if DEBUG: print url - return _do(url,http_proxy) - -def postalCodeLookupJSON(postalcode, placename='', country=COUNTRY, maxRows='10',gcallback='', http_proxy=None): - """ - Webservice Type : REST /JSON - Url : ws.geonames.org/postalCodeLookupJSON? - Parameters : postalcode,country ,maxRows (default = 20),callback - Result : returns a list of places for the given postalcode in JSON format - """ - url = _buildpostalCodeLookupJSON(postalcode,placename,country,maxRows,gcallback) -# print url - proxies = getProxies(http_proxy) - u = urllib.FancyURLopener(proxies) - usock = u.open(url) - rawdata = usock.read() - if DEBUG: print rawdata - usock.close() - return eval(rawdata[:-3]) - -def findNearbyPostalCodes(postalcode, placename='', country=COUNTRY, radius='5', maxRows='10',lat=None,lng=None, http_proxy=None): - """ - Find nearby postal codes / reverse geocoding - This service comes in two flavors. You can either pass the lat/long or a postalcode/placename. - - Webservice Type : REST - Url : ws.geonames.org/findNearbyPostalCodes? - Parameters : - lat,lng, radius (in km), maxRows (default = 5),country (default = all countries) - or - postalcode,country, radius (in Km), maxRows (default = 5) - Result : returns a list of postalcodes and places for the lat/lng query as xml document - Example: - http://ws.geonames.org/findNearbyPostalCodes?postalcode=35580&placename=guichen&country=FR&radius=5 - <geonames> - - - <code> - <postalcode>35580</postalcode> - <name>Guichen</name> - <countryCode>FR</countryCode> - <lat>47.9666667</lat> - <lng>-1.8</lng> - <distance>0.0</distance> - </code> - """ - if lat and lng : - url = _buildfindNearbyPostalCodesLL(lat,lng,radius,maxRows) - else: - url = _buildfindNearbyPostalCodes(postalcode,placename,country,radius,maxRows) - if DEBUG: print url -# import pdb;pdb.set_trace() - return _do(url,http_proxy).code - - -def postalCodeCountryInfo(http_proxy=None): - """ - http://ws.geonames.org/postalCodeCountryInfo? - <country> - <countryCode>FR</countryCode> - <countryName>France</countryName> - <numPostalCodes>39163</numPostalCodes> - <minPostalCode>01000</minPostalCode> - <maxPostalCode>98000</maxPostalCode> - </country> - - """ - return _do("http://ws.geonames.org/postalCodeCountryInfo?",http_proxy).country - -def search(placename='', country=COUNTRY, maxRows='10', style='SHORT',lang=LANG, fclass=None, http_proxy=None): - """ - Url : ws.geonames.org/search? - Parameters : q : place name (urlencoded utf8) - maxRows : maximal number of rows returned (default = 100) - country : iso country code, two characters (default = all countries) - fclass : featureclass(es) (default= all feature classes); this parameter may occur more then once, example: fclass=P&fclass=A - style : SHORT,MEDIUM,LONG (default = MEDIUM), verbosity of returned xml document - lang : ISO 2-letter language code. (default = en), countryName will be returned in the specified language. - - http://ws.geonames.org/search?q=guichen&maxRows=10&style=SHORT&lang=fr&country=fr - <geonames> - <totalResultsCount>3</totalResultsCount> - - - <geoname> - <name>Laill�</name> - <lat>47.9833333</lat> - <lng>-1.7166667</lng> - </geoname> - """ - url = _buildsearch(placename, country, maxRows,style,lang, fclass) - if DEBUG: print url - return _do(url,http_proxy) - -def findNearbyPlaceName(lat,lng, http_proxy=None): - """ - Webservice Type : REST - Url : ws.geonames.org/findNearbyPlaceName? - Parameters : lat,lng - Result : returns the closest populated place for the lat/lng query as xml document - Example: - http://ws.geonames.org/findNearbyPlaceName?lat=47.3&lng=9 - """ - url = _buildfindNearbyPlaceName(lat,lng) - if DEBUG: print url - return _do(url,http_proxy) - -def findCountrySubdivision(lat,lng, http_proxy=None): - """ - Webservice Type : REST - Url : ws.geonames.org/findNearbyPlaceName? - Parameters : lat,lng - Result : returns the closest populated place for the lat/lng query as xml document - Example: - http://ws.geonames.org/findNearbyPlaceName?lat=47.3&lng=9 - """ - url = _buildfindCountrySubdivision(lat,lng) - if DEBUG: print url - return _do(url,http_proxy) - diff --git a/lib/utils/email_multipart.py b/lib/utils/email_multipart.py deleted file mode 100644 index 4c2e154..0000000 --- a/lib/utils/email_multipart.py +++ /dev/null @@ -1,80 +0,0 @@ -# -*- coding: utf-8 -*- - -# Sending html emails in Django -# Report any bugs to esat @t sleytr*net -# Evren Esat Ozkan - - -from feedparser import _sanitizeHTML -from stripogram import html2text - -from django.conf import settings -from django.template import loader, Context - -from email.MIMEMultipart import MIMEMultipart -from email.MIMEText import MIMEText -from email.MIMEImage import MIMEImage -from smtplib import SMTP -import email.Charset - - -charset='utf-8' - - -email.Charset.add_charset( charset, email.Charset.SHORTEST, None, None ) - -def htmlmail(sbj,recip,msg,template='',texttemplate='',textmsg='',images=(), recip_name='',sender=settings.DEFAULT_FROM_EMAIL,sender_name='',charset=charset): - ''' - if you want to use Django template system: - use `msg` and optionally `textmsg` as template context (dict) - and define `template` and optionally `texttemplate` variables. - otherwise msg and textmsg variables are used as html and text message sources. - - if you want to use images in html message, define physical paths and ids in tuples. - (image paths are relative to MEDIA_ROOT) - example: - images=(('email_images/logo.gif','img1'),('email_images/footer.gif','img2')) - and use them in html like this: - <img src="cid:img1"> - ... - <img src="cid:img2"> - ''' - html=render(msg,template) - if texttemplate or textmsg: text=render((textmsg or msg),texttemplate) - else: text= html2text(_sanitizeHTML(html,charset)) - - msgRoot = MIMEMultipart('related') - msgRoot['Subject'] = sbj - msgRoot['From'] = named(sender,sender_name) - msgRoot['To'] = named(recip,recip_name) - msgRoot.preamble = 'This is a multi-part message in MIME format.' - - msgAlternative = MIMEMultipart('alternative') - msgRoot.attach(msgAlternative) - - msgAlternative.attach(MIMEText(text, _charset=charset)) - msgAlternative.attach(MIMEText(html, 'html', _charset=charset)) - - for img in images: - fp = open(img[0], 'rb') - msgImage = MIMEImage(fp.read()) - fp.close() - msgImage.add_header('Content-ID', '<'+img[1]+'>') - msgRoot.attach(msgImage) - - smtp = SMTP() - smtp.connect(settings.EMAIL_HOST) - smtp.login(settings.EMAIL_HOST_USER , settings.EMAIL_HOST_PASSWORD) - smtp.sendmail(sender, recip, msgRoot.as_string()) - smtp.quit() - - -def render(context,template): - if template: - t = loader.get_template(template) - return t.render(Context(context)) - return context - -def named(mail,name): - if name: return '%s <%s>' % (name,mail) - return mail
\ No newline at end of file diff --git a/lib/markdown2.py b/lib/utils/markdown2.py index d72f414..d72f414 100755 --- a/lib/markdown2.py +++ b/lib/utils/markdown2.py diff --git a/lib/utils/pydelicious.py b/lib/utils/pydelicious.py index dd33788..8e45843 100644 --- a/lib/utils/pydelicious.py +++ b/lib/utils/pydelicious.py @@ -1,131 +1,126 @@ """Library to access del.icio.us data via Python. -:examples: - - Using the API class directly: - - >>> a = pydelicious.apiNew('user', 'passwd') - >>> # or: - >>> a = DeliciousAPI('user', 'passwd') - >>> a.tags_get() # Same as: - >>> a.request('tags/get', ) - - Or by calling one of the methods on the module: - - - add(user, passwd, url, description, tags = "", extended = "", dt = "", replace="no") - - get(user, passwd, tag="", dt="", count = 0) - - get_all(user, passwd, tag = "") - - delete(user, passwd, url) - - rename_tag(user, passwd, oldtag, newtag) - - get_tags(user, passwd) - - >>> a = apiNew(user, passwd) - >>> a.posts_add(url="http://my.com/", desciption="my.com", extended="the url is my.moc", tags="my com") - True - >>> len(a.posts_all()) - 1 - >>> get_all(user, passwd) - 1 - - This are short functions for getrss calls. - - >>> rss_ - -def get_userposts(user): -def get_tagposts(tag): -def get_urlposts(url): -def get_popular(tag = ""): - - >>> json_posts() - >>> json_tags() - >>> json_network() - >>> json_fans() - -:License: pydelicious is released under the BSD license. See 'license.txt' - for more informations. - -:todo, bvb: - - Rewriting comments to english. More documentation, examples. - - Added JSON-like return values for XML data (del.icio.us also serves some JSON...) - - better error/exception classes and handling, work in progress. - -:todo: - - Source code SHOULD BE ASCII! - - More tests. - - handling different encodings, what, how? - >>> pydelicious.getrss(tag="t[a]g") - url: http://del.icio.us/rss/tag/t[a]g - - Parse datetimes in XML. - - Test RSS functionality? HTML scraping doesn't work yet? - - API functions need required argument checks. - - interesting functionality in other libraries (ruby, java, perl, etc)? - - what is pydelicious used for? - - license, readme docs via setup.py verdelen? - - automatic releas build - -:done: - * Refactored the API class, much cleaner now and functions dlcs_api_request, dlcs_parse_xml are available for who wants them. +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 md5, httplib -import urllib, urllib2, time +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: - from xml.etree.ElementTree import parse as parse_xml + # Python 2.5 and higher + from xml.etree.ElementTree import parse as parse_xml -import feedparser +try: + import feedparser +except ImportError: + print >>sys.stderr, \ + "Feedparser not available, no RSS parsing." + feedparser = None ### Static config -__version__ = '0.5.0' -__author__ = 'Frank Timmermann <regenkind_at_gmx_dot_de>' # GP: does not respond to emails +__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/' -__author_email__ = "" # Old URL: 'http://deliciouspython.python-hosting.com/' - -__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 +__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 -DLCS_REQUEST_TIMEOUT = 444 # Seconds before socket triggers timeout +"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 = 'https://api.del.icio.us' +DLCS_API_HOST = 'api.del.icio.us' DLCS_API_PATH = 'v1' -DLCS_API = "%s/%s" % (DLCS_API_HOST, DLCS_API_PATH) +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.py/%s %s' % (__version__, __url__) +USER_AGENT = 'pydelicious/%s %s' % (__version__, __url__) DEBUG = 0 if 'DLCS_DEBUG' in os.environ: DEBUG = int(os.environ['DLCS_DEBUG']) - - -# 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. + 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 + 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 @@ -163,91 +158,64 @@ class _Waiter: Waiter = _Waiter(DLCS_WAIT_TIME) + class PyDeliciousException(Exception): - '''Std. pydelicious error''' - pass + """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 DefaultErrorHandler(urllib2.HTTPDefaultErrorHandler): - '''xxx, bvb: Where is this used? should it be registered somewhere with urllib2? - - Handles HTTP Error, currently only 503. - ''' - def http_error_503(self, req, fp, code, msg, headers): - raise urllib2.HTTPError(req, code, throttled_message, headers, fp) - +class DeliciousItemExistsError(DeliciousError): + """Raised then adding an already existing post.""" -class post(dict): - """Post object, contains href, description, hash, dt, tags, - extended, user, count(, shared). - xxx, bvb: Not used in DeliciousAPI - """ - def __init__(self, href="", description="", hash="", time="", tag="", extended="", user="", count="", - tags="", url="", dt=""): # tags or tag? - self["href"] = href - if url != "": self["href"] = url - self["description"] = description - self["hash"] = hash - self["dt"] = dt - if time != "": self["dt"] = time - self["tags"] = tags - if tag != "": self["tags"] = tag # tag or tags? # !! tags - self["extended"] = extended - self["user"] = user - self["count"] = count - - def __getattr__(self, name): - try: return self[name] - except: object.__getattribute__(self, name) - - -class posts(list): - def __init__(self, *args): - for i in args: self.append(i) - - def __getattr__(self, attr): - try: return [p[attr] for p in self] - except: object.__getattribute__(self, attr) +class HTTPErrorHandler(urllib2.HTTPDefaultErrorHandler): -### Utility functions + def http_error_401(self, req, fp, code, msg, headers): + raise PyDeliciousUnauthorized, "Check credentials." -def str2uni(s): - # type(in) str or unicode - # type(out) unicode - return ("".join([unichr(ord(i)) for i in s])) + 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 -def str2utf8(s): - # type(in) str or unicode - # type(out) str - return ("".join([unichr(ord(i)).encode("utf-8") for i in s])) -def str2quote(s): - return urllib.quote_plus("".join([unichr(ord(i)).encode("utf-8") for i in s])) +### Utility functions def dict0(d): - # Trims empty dict entries - # {'a':'a', 'b':'', 'c': 'c'} => {'a': 'a', 'c': 'c'} - dd = dict() - for i in d: - if d[i] != "": dd[i] = d[i] - return dd + "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): + +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 @@ -255,9 +223,10 @@ def http_request(url, user_agent=USER_AGENT, retry=4): tries = retry; while tries: try: - return urllib2.urlopen(request) + return opener.open(request) - except urllib2.HTTPError, e: # protocol errors, + except urllib2.HTTPError, e: + # reraise unexpected protocol errors as PyDeliciousException raise PyDeliciousException, "%s" % e except urllib2.URLError, e: @@ -273,50 +242,99 @@ def http_request(url, user_agent=USER_AGENT, retry=4): raise PyDeliciousException, \ "Unable to retrieve data at '%s', %s" % (url, e) -def http_auth_request(url, host, user, passwd, user_agent=USER_AGENT): - """Call an HTTP server with authorization credentials using urllib2. + +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 - # Hook up handler/opener to urllib2 password_manager = urllib2.HTTPPasswordMgrWithDefaultRealm() password_manager.add_password(None, host, user, passwd) auth_handler = urllib2.HTTPBasicAuthHandler(password_manager) - opener = urllib2.build_opener(auth_handler) - urllib2.install_opener(opener) - return http_request(url, user_agent) + 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()" -def dlcs_api_request(path, params='', user='', passwd='', throttle=True): + 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. - todo: back off on 503's (HTTPError, URLError? testing - - Returned XML does not always correspond with given del.icio.us examples - [#]_. - .. [#] http://del.icio.us/help/api/ """ if throttle: Waiter() if params: - # params come as a dict, strip empty entries and urlencode - url = "%s/%s?%s" % (DLCS_API, path, urllib.urlencode(dict0(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 DEBUG: print >>sys.stderr, \ + "dlcs_api_request: %s" % url - try: - return http_auth_request(url, DLCS_API_HOST, user, passwd, USER_AGENT) + 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 - # bvb: Is this ever raised? When? - except DefaultErrorHandler, e: - print >>sys.stderr, "%s" % e def dlcs_parse_xml(data, split_tags=False): """Parse any del.icio.us XML document and return Python data structure. @@ -332,6 +350,7 @@ def dlcs_parse_xml(data, split_tags=False): {'result':(True, "done")} # etcetera. """ + # TODO: split_tags is not implemented if DEBUG>3: print >>sys.stderr, "dlcs_parse_xml: parsing from ", data @@ -367,55 +386,67 @@ def dlcs_parse_xml(data, split_tags=False): else: msg = root.text - # Return {'result':(True, msg)} for /known/ O.K. messages, - # use (False, msg) otherwise + # 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: root.attrib} - return {fmt: {'time':time.strptime(root.attrib['time'], ISO_8601_DATETIME)}} + 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 = ''): - """Handle a request for RSS - - todo: translate from German - rss sollte nun wieder funktionieren, aber diese try, except scheisse ist so nicht schoen +def dlcs_rss_request(tag="", popular=0, user="", url=''): + """Parse a RSS request. - rss wird unterschiedlich zusammengesetzt. ich kann noch keinen einheitlichen zusammenhang - zwischen daten (url, desc, ext, usw) und dem feed erkennen. warum k[o]nnen die das nicht einheitlich machen? + This requests old (now undocumented?) URL paths that still seem to work. """ - tag = str2quote(tag) - user = str2quote(user) + + tag = quote_plus(tag) + user = quote_plus(user) + if url != '': # http://del.icio.us/rss/url/efbfb246d886393d48065551434dab54 - url = DLCS_RSS + '''url/%s'''%md5.new(url).hexdigest() + url = DLCS_RSS + 'url/%s' % md5(url).hexdigest() + elif user != '' and tag != '': - url = DLCS_RSS + '''%(user)s/%(tag)s'''%dict(user=user, tag=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 + 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 + url = DLCS_RSS + "tag/%s" % tag + elif popular == 1 and tag == '': - url = DLCS_RSS + '''popular/''' + url = DLCS_RSS + 'popular/' + elif popular == 1 and tag != '': - url = DLCS_RSS + '''popular/%s'''%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) - # print rss -# for e in rss.entries: print e;print - l = posts() + + 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"] @@ -453,59 +484,147 @@ def dlcs_rss_request(tag = "", popular = 0, user = "", url = ''): # die benennung der variablen ist nicht einheitlich # api senden und # xml bekommen sind zwei verschiedene schuhe :( - l.append(post(url = url, description = description, tags = tags, dt = dt, extended = extended, user = user)) - return l + 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: - """Class providing main interace to del.icio.us API. + + """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 the parameters and documentation. These all call - ``request`` and pass on extra keywords like ``_raw``. + an explicit declaration of parameters and documentation. """ - def __init__(self, user, passwd, codec='iso-8859-1', api_request=dlcs_api_request, xml_parser=dlcs_parse_xml): - """Initialize access to the API with ``user`` and ``passwd``. + 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. + ``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 to + functions within this package with standard implementations which request and parse a resource. See ``dlcs_api_request()`` and - ``dlcs_parse_xml()``. Note that ``api_request`` should return a - file-like instance with an HTTPMessage instance under ``info()``, - see ``urllib2.openurl`` for more info. + ``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 - def _call_server(self, path, **params): - params = dict0(params) - for key in params: - params[key] = params[key].encode(self.codec) - - # see __init__ for _api_request() - return self._api_request(path, params, self.user, self.passwd) - - ### Core functionality def request(self, path, _raw=False, **params): - """Calls a path in the API, parses the answer to a JSON-like structure by - default. Use with ``_raw=True`` or ``call request_raw()`` directly to - get the filehandler and process the response message manually. + """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.:: @@ -515,46 +634,56 @@ class DeliciousAPI: <result>...</result> - These are all parsed to ``{'result':(Boolean, MessageString)}`` and this - method will raise ``DeliciousError`` on negative `result` answers. Using - ``_raw=True`` bypasses all parsing and will never raise ``DeliciousError``. + 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()``.""" - # method _parse_response is bound in `__init__()`, `_call_server` - # uses `_api_request` also set in `__init__()` if _raw: # return answer return self.request_raw(path, **params) else: + params = self._encode_params(params, self.codec) + # get answer and parse - fl = self._call_server(path, **params) + fl = self._api_request(path, params=params, opener=self._opener) rs = self._parse_response(fl) - # Raise an error for negative 'result' answers - if type(rs) == dict and rs == 'result' and not rs['result'][0]: - errmsg = "" - if len(rs['result'])>0: - errmsg = rs['result'][1:] - raise DeliciousError, errmsg + 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 + """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 - return self._call_server(path, **params) + 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. + """Returns a list of tags and the number of times it is used by the + user. :: <tags> @@ -562,14 +691,22 @@ class DeliciousAPI: """ 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 (required) - Tag to rename. - &new (required) - New name. + &old={TAG} + (required) Tag to rename. + &new={TAG} + (required) New tag name. """ return self.request("tags/rename", old=old, new=new, **kwds) @@ -590,12 +727,12 @@ class DeliciousAPI: <dates> <date date="CCYY-MM-DD" count="888"> - &tag (optional). - Filter by this tag. + &tag={TAG} + (optional) Filter by this tag """ return self.request("posts/dates", tag=tag, **kwds) - def posts_get(self, tag="", dt="", url="", **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. :: @@ -603,14 +740,25 @@ class DeliciousAPI: <posts dt="CCYY-MM-DD" tag="..." user="..."> <post ...> - &tag (optional). - Filter by this tag. - &dt (optional). - Filter by this date (CCYY-MM-DDThh:mm:ssZ). - &url (optional). - Filter by this url. + &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, **kwds) + 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. @@ -619,14 +767,15 @@ class DeliciousAPI: <posts tag="..." user="..."> <post ...> - &tag (optional). - Filter by this tag. - &count (optional). - Number of items to retrieve (Default:15, Maximum:100). + &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="", **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. :: @@ -634,13 +783,34 @@ class DeliciousAPI: <posts tag="..." user="..." update="CCYY-MM-DDThh:mm:ssZ"> <post ...> - &tag (optional). - Filter by this tag. + &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. """ - return self.request("posts/all", tag=tag, **kwds) + 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="no", shared="yes", **kwds): + replace=False, shared=True, **kwds): """Add a post to del.icio.us. Returns a `result` message or raises an ``DeliciousError``. See ``self.request()``. @@ -654,10 +824,12 @@ class DeliciousAPI: 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=no (optional) - make the item private + 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, @@ -690,7 +862,7 @@ class DeliciousAPI: &bundle (required) the bundle name. &tags (required) - list of tags (space seperated). + list of tags. """ if type(tags)==list: tags = " ".join(tags) @@ -710,19 +882,22 @@ class DeliciousAPI: # Lookup table for del.icio.us url-path to DeliciousAPI method. paths = { - 'tags/get': tags_get, - '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, + '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 @@ -730,41 +905,55 @@ class DeliciousAPI: """ 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 - """ + "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="", replace="no"): - return apiNew(user, passwd).posts_add(url=url, description=description, extended=extended, tags=tags, dt=dt, replace=replace) +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="", count = 0): - posts = apiNew(user, passwd).posts_get(tag=tag,dt=dt) - if count != 0: posts = posts[0:count] +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_all(user, passwd, tag=""): - return apiNew(user, passwd).posts_all(tag=tag) +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): - return apiNew(user, passwd).posts_delete(url=url) + "Delete the URL from the del.icio.us account." + apiNew(user, passwd).posts_delete(url=url) def rename_tag(user, passwd, oldtag, newtag): - return apiNew(user=user, passwd=passwd).tags_rename(old=oldtag, new=newtag) + "Rename the tag for the del.icio.us account." + apiNew(user=user, passwd=passwd).tags_rename(old=oldtag, new=newtag) -def get_tags(user, passwd): - return apiNew(user=user, passwd=passwd).tags_get() +### RSS functions -### RSS functions bvb: still working...? def getrss(tag="", popular=0, url='', user=""): - """get posts from del.icio.us via parsing RSS (bvb:or HTML) - - todo: not tested + """Get posts from del.icio.us via parsing RSS. tag (opt) sort by tag popular (opt) look for the popular stuff @@ -774,44 +963,83 @@ def getrss(tag="", popular=0, url='', user=""): return dlcs_rss_request(tag=tag, popular=popular, user=user, url=url) def get_userposts(user): - return getrss(user = user) + "parse RSS for user" + return getrss(user=user) def get_tagposts(tag): - return getrss(tag = tag) + "parse RSS for tag" + return getrss(tag=tag) def get_urlposts(url): - return getrss(url = 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) -def get_popular(tag = ""): - return getrss(tag = tag, popular = 1) +### JSON feeds +# TODO: untested -### TODO: implement JSON fetching -def json_posts(user, count=15): - """http://del.icio.us/feeds/json/mpe - http://del.icio.us/feeds/json/mpe/art+history - 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 +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'): - """http://del.icio.us/feeds/json/tags/mpe - 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 + +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): - """http://del.icio.us/feeds/json/network/mpe + +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 + ?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): - """http://del.icio.us/feeds/json/fans/mpe + +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. + ?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) diff --git a/lib/strutils.py b/lib/utils/strutils.py index 368d3d8..368d3d8 100644 --- a/lib/strutils.py +++ b/lib/utils/strutils.py diff --git a/lib/view_wrapper.py b/lib/view_wrapper.py deleted file mode 100644 index 0e1e492..0000000 --- a/lib/view_wrapper.py +++ /dev/null @@ -1,10 +0,0 @@ -from django.shortcuts import render_to_response -from django.template.context import RequestContext - -def luxagraf_render(request, *args, **kwargs): - """ - Replacement for render_to_response that uses RequestContext and sets an - extra template variable, TEMPLATE_NAME. - """ - kwargs['context_instance'] = RequestContext(request) - return render_to_response(*args, **kwargs)
\ No newline at end of file |