diff options
Diffstat (limited to 'lib/utils')
-rw-r--r-- | lib/utils/GeoClient.py | 292 | ||||
-rw-r--r-- | lib/utils/__init__.py | 0 | ||||
-rw-r--r-- | lib/utils/email_multipart.py | 80 | ||||
-rw-r--r-- | lib/utils/pydelicious.py | 817 |
4 files changed, 1189 insertions, 0 deletions
diff --git a/lib/utils/GeoClient.py b/lib/utils/GeoClient.py new file mode 100644 index 0000000..d1966ca --- /dev/null +++ b/lib/utils/GeoClient.py @@ -0,0 +1,292 @@ +# -*- 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/__init__.py b/lib/utils/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/lib/utils/__init__.py diff --git a/lib/utils/email_multipart.py b/lib/utils/email_multipart.py new file mode 100644 index 0000000..4c2e154 --- /dev/null +++ b/lib/utils/email_multipart.py @@ -0,0 +1,80 @@ +# -*- 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/utils/pydelicious.py b/lib/utils/pydelicious.py new file mode 100644 index 0000000..dd33788 --- /dev/null +++ b/lib/utils/pydelicious.py @@ -0,0 +1,817 @@ +"""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. +""" +import sys +import os +import time +import datetime +import md5, httplib +import urllib, urllib2, time +from StringIO import StringIO + +try: + from elementtree.ElementTree import parse as parse_xml +except ImportError: + from xml.etree.ElementTree import parse as parse_xml + +import feedparser + + +### Static config + +__version__ = '0.5.0' +__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 +DLCS_WAIT_TIME = 4 +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_PATH = 'v1' +DLCS_API = "%s/%s" % (DLCS_API_HOST, DLCS_API_PATH) +DLCS_RSS = 'http://del.icio.us/rss/' + +ISO_8601_DATETIME = '%Y-%m-%dT%H:%M:%SZ' + +USER_AGENT = 'pydelicious.py/%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. +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): + '''Std. pydelicious error''' + pass + +class DeliciousError(Exception): + """Raised when the server responds with a negative answer""" + + +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 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) + +### Utility functions + +def str2uni(s): + # type(in) str or unicode + # type(out) unicode + return ("".join([unichr(ord(i)) for i in s])) + +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])) + +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 + +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): + """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}) + + # Remember last error + e = None + + # Repeat request on time-out errors + tries = retry; + while tries: + try: + return urllib2.urlopen(request) + + except urllib2.HTTPError, e: # protocol errors, + 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 http_auth_request(url, host, user, passwd, user_agent=USER_AGENT): + """Call an HTTP server with authorization credentials using urllib2. + """ + 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) + +def dlcs_api_request(path, params='', user='', passwd='', throttle=True): + """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))) + else: + url = "%s/%s" % (DLCS_API, path) + + if DEBUG: print >>sys.stderr, "dlcs_api_request: %s" % url + + try: + return http_auth_request(url, DLCS_API_HOST, user, passwd, USER_AGENT) + + # 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. + + 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. + """ + + 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 + + # Return {'result':(True, msg)} for /known/ O.K. messages, + # use (False, msg) otherwise + 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)}} + + 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 + + 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? + """ + tag = str2quote(tag) + user = str2quote(user) + if url != '': + # http://del.icio.us/rss/url/efbfb246d886393d48065551434dab54 + url = DLCS_RSS + '''url/%s'''%md5.new(url).hexdigest() + elif user != '' and tag != '': + url = DLCS_RSS + '''%(user)s/%(tag)s'''%dict(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 + rss = http_request(url).read() + rss = feedparser.parse(rss) + # print rss +# for e in rss.entries: print e;print + l = 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 :( + l.append(post(url = url, description = description, tags = tags, dt = dt, extended = extended, user = user)) + return l + + +### Main module class + +class DeliciousAPI: + """Class providing main interace to del.icio.us 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``. + """ + + 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``. + + ``codec`` sets the encoding of the arguments. + + The ``api_request`` and ``xml_parser`` parameters by default point to + functions within this package with standard implementations to + 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. + """ + assert user != "" + self.user = user + self.passwd = passwd + self.codec = codec + + # Implement communication to server and parsing of respons messages: + 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. + + Calls to some paths will return a `result` message, i.e.:: + + <result code="..." /> + + or:: + + <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``. + + 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: + # get answer and parse + fl = self._call_server(path, **params) + 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 + + 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 + return self._call_server(path, **params) + + ### 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_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. + """ + 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 (optional). + Filter by this tag. + """ + return self.request("posts/dates", tag=tag, **kwds) + + def posts_get(self, tag="", dt="", url="", **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 (optional). + Filter by this tag. + &dt (optional). + Filter by this date (CCYY-MM-DDThh:mm:ssZ). + &url (optional). + Filter by this url. + """ + return self.request("posts/get", tag=tag, dt=dt, url=url, **kwds) + + def posts_recent(self, tag="", count="", **kwds): + """Returns a list of the most recent posts, filtered by argument. + :: + + <posts tag="..." user="..."> + <post ...> + + &tag (optional). + Filter by this tag. + &count (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): + """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. + """ + return self.request("posts/all", tag=tag, **kwds) + + def posts_add(self, url, description, extended="", tags="", dt="", + replace="no", shared="yes", **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=no (optional) - make the item private + """ + 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 (space seperated). + """ + 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/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_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,) + + +### 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="", replace="no"): + return 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] + return posts + +def get_all(user, passwd, tag=""): + return apiNew(user, passwd).posts_all(tag=tag) + +def delete(user, passwd, url): + return 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) + +def get_tags(user, passwd): + return apiNew(user=user, passwd=passwd).tags_get() + + +### 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 + + 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): + return getrss(user = user) + +def get_tagposts(tag): + return getrss(tag = tag) + +def get_urlposts(url): + return getrss(url = url) + +def get_popular(tag = ""): + return getrss(tag = tag, popular = 1) + + +### 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_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_network(user): + """http://del.icio.us/feeds/json/network/mpe + 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 + """ + +def json_fans(user): + """http://del.icio.us/feeds/json/fans/mpe + callback=NAME wrap the object definition in a function call NAME(...) + ?raw a pure JSON object is returned, instead of an object named Delicious. + """ + |