2 import cPickle as pickle
10 import xml.etree.cElementTree as ET
13 __author__ = 'Deepak Sarda'
15 __copyright__ = '(c) 2008 Deepak Sarda'
16 __license__ = 'Public Domain'
17 __url__ = 'http://antrix.net/'
21 # config file format is as follows
22 # ; example ~/.yummy.cfg file
24 # user = delicious-user-name
25 # pass = delicious-password
26 # ; source_url is your public shared items feed url from Google Reader
27 # source_url = http://www.google.com/reader/public/atom/user/../broadcast
28 # ; end of config file
29 config_file = os.path.expanduser('~/.yummy.cfg') 31 # If state file doesn't exist, it will be created
32 state_file = os.path.expanduser('~/.yummy.state') 34 # Set to logging.DEBUG for debug messages
35 LOG_LEVEL = logging.INFO
40 """Class to model a del.icio.us Post. It limits attributes
41 to a retricted subset, i.e. those used in a del.icio.us post."""
43 __slots__ = ['description', 'url', 'extended', 'tags']
45 def __contains__(self, key):
52 # urllib.urlencode() just needs this beyond the basic stuff above
54 return [(k, getattr(self, k).encode('utf-8')) 55 for k in self.__slots__ if k in self]
58 """Iterates over a Feedparser feed object and returns Posts.
59 Tailored for greader shared items feed to return title,
60 link and annotation."""
62 for entry in feed.entries:
64 d.description = entry.title
66 d.tags = "linker via:greader" # TODO: this should be configurable
67 for content in entry.content:
68 if content.base.startswith(
69 'http://www.google.com/reader/public/atom/user/'):
70 d.extended = content.value
75 _endpoint = 'https://api.del.icio.us/v1/posts/add?'
77 def __init__(self, statefile, source_url, user, pw):
78 """`statefile` is where data about which items have already been
79 posted to delicious is saved.
80 `source_url` is the Google Reader feed url from which to pick items
81 `user` is the delicious user name
82 `pw` is the delicious password
85 self._store = statefile
87 self._processed = pickle.load(open(statefile))
89 logging.error('Error loading state file: %s' % statefile) 90 self._processed = set()
92 self._source_url = source_url
94 pass_mgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
95 pass_mgr.add_password(None, 'api.del.icio.us', user, pw)
96 handler = urllib2.HTTPBasicAuthHandler(pass_mgr)
97 opener = urllib2.build_opener(handler)
98 opener.addheaders = [('User-Agent', 99 'yummy - greader->delicious poster (%s)' % __version__)]
100 urllib2.install_opener(opener)
103 """Updates delicious with posts sourced from source_url"""
105 logging.debug('fetching source feed') 106 feed = feedparser.parse(self._source_url)
107 logging.debug('fetched feed. it has %s entries' % len(feed.entries)) 109 for post in posts(feed):
110 if post.url in self._processed:
111 logging.debug('Skipping already processed URL: %s' % post.url) 114 params = urllib.urlencode(post)
115 logging.debug('Posting url: %s' % self._endpoint + params) 117 response = urllib2.urlopen(self._endpoint + params)
118 xml = ET.parse(response)
119 except urllib2.HTTPError, exc:
120 logging.error('HTTPError: %d' % (exc.code)) 121 except urllib2.URLError, exc:
122 logging.error('URL error' % str(exc)) 124 result = xml.getroot()
125 logging.debug('response is: %s: %s' % 126 (result.tag, result.get('code'))) 128 if result.get('code') == 'done': 129 self._processed.add(post.url)
131 logging.error('Error posting to delicious.' \ 132 'Response was: %s' % result.get('code')) 134 # delicious folks require us to wait a second between requests
137 # Done processing feed. Save state to data store before returning
138 logging.debug('Done processing all urls in feed') 139 f = open(self._store, 'w')
140 pickle.dump(self._processed, f)
143 if __name__ == '__main__':
144 logging.basicConfig(level=LOG_LEVEL)
146 config = ConfigParser.ConfigParser()
147 if not config.read(config_file):
148 logging.error('Could not read config file: %s' % config_file) 151 username = config.get('yummy', 'user') 152 password = config.get('yummy', 'pass') 153 source_url = config.get('yummy', 'source_url') 155 y = Yummy(state_file, source_url, username, password)