3 Python library for Remember The Milk API
5 @note For help, see http://www.rememberthemilk.com/services/api/methods/
15 _use_simplejson = False
18 _use_simplejson = True
23 __author__ = 'Sridhar Ratnakumar <http://nearfar.org/>'
25 SERVICE_URL = 'http://api.rememberthemilk.com/services/rest/'
26 AUTH_SERVICE_URL = 'http://www.rememberthemilk.com/services/auth/'
29 class RTMError(StandardError):
33 class RTMAPIError(RTMError):
37 class RTMParseError(RTMError):
41 class AuthStateMachine(object):
42 """If the state is in those setup for the machine, then return
43 the datum sent. Along the way, it is an automatic call if the
47 class NoData(RTMError):
50 def __init__(self, states):
54 def dataReceived(self, state, datum):
55 if state not in self.states:
56 raise RTMError, "Invalid state <%s>" % state
57 self.data[state] = datum
60 if state in self.data:
61 return self.data[state]
63 raise AuthStateMachine.NoData('No data for <%s>' % state)
68 def __init__(self, userID, apiKey, secret, token=None):
72 self._authInfo = AuthStateMachine(['frob', 'token'])
74 # this enables one to do 'rtm.tasks.getList()', for example
75 for prefix, methods in API.items():
77 RTMAPICategory(self, prefix, methods))
80 self._authInfo.dataReceived('token', token)
82 def _sign(self, params):
83 "Sign the parameters with MD5 hash"
84 pairs = ''.join(['%s%s' % (k, v) for (k, v) in sortedItems(params)])
85 return hashlib.md5(self._secret+pairs).hexdigest()
88 def open_url(url, queryArgs=None):
90 url += '?' + urllib.urlencode(queryArgs)
91 warnings.warn("Performing download of %s" % url, stacklevel=5)
92 return urllib2.urlopen(url)
95 def read_by_length(connection, timeout):
96 # It appears that urllib uses the non-blocking variant of file objects
97 # which means reads might not always be complete, so grabbing as much
98 # of the data as possible with a sleep in between to give it more time
100 contentLengthField = "Content-Length"
101 assert contentLengthField in connection.info(), "Connection didn't provide content length info"
102 specifiedLength = int(connection.info()["Content-Length"])
107 chunk = connection.read()
108 actuallyRead += len(chunk)
110 while 0 < timeout and actuallyRead < specifiedLength:
113 chunk = connection.read()
114 actuallyRead += len(chunk)
117 json = "".join(chunks)
119 if "Content-Length" in connection.info():
120 assert len(json) == int(connection.info()["Content-Length"]), "The packet header promised %s of data but only was able to read %s of data" % (
121 connection.info()["Content-Length"],
128 def read_by_guess(connection, timeout):
129 # It appears that urllib uses the non-blocking variant of file objects
130 # which means reads might not always be complete, so grabbing as much
131 # of the data as possible with a sleep in between to give it more time
136 chunk = connection.read()
138 while chunk and 0 < timeout:
141 chunk = connection.read()
144 json = "".join(chunks)
146 if "Content-Length" in connection.info():
147 assert len(json) == int(connection.info()["Content-Length"]), "The packet header promised %s of data but only was able to read %s of data" % (
148 connection.info()["Content-Length"],
154 def get(self, **params):
155 "Get the XML response for the passed `params`."
156 params['api_key'] = self._apiKey
157 params['format'] = 'json'
158 params['api_sig'] = self._sign(params)
160 connection = self.open_url(SERVICE_URL, params)
161 json = self.read_by_guess(connection, 5)
162 # json = self.read_by_length(connection, 5)
164 data = DottedDict('ROOT', parse_json(json))
167 if rsp.stat == 'fail':
168 raise RTMAPIError, 'API call failed - %s (%s)' % (
169 rsp.err.msg, rsp.err.code)
173 def getNewFrob(self):
174 rsp = self.get(method='rtm.auth.getFrob')
175 self._authInfo.dataReceived('frob', rsp.frob)
178 def getAuthURL(self):
180 frob = self._authInfo.get('frob')
181 except AuthStateMachine.NoData:
182 frob = self.getNewFrob()
185 'api_key': self._apiKey,
189 params['api_sig'] = self._sign(params)
190 return AUTH_SERVICE_URL + '?' + urllib.urlencode(params)
193 frob = self._authInfo.get('frob')
194 rsp = self.get(method='rtm.auth.getToken', frob=frob)
195 self._authInfo.dataReceived('token', rsp.auth.token)
196 return rsp.auth.token
199 class RTMAPICategory(object):
200 "See the `API` structure and `RTM.__init__`"
202 def __init__(self, rtm, prefix, methods):
203 self._rtm = weakref.ref(rtm)
204 self._prefix = prefix
205 self._methods = methods
207 def __getattr__(self, attr):
208 if attr not in self._methods:
209 raise AttributeError, 'No such attribute: %s' % attr
211 rargs, oargs = self._methods[attr]
212 if self._prefix == 'tasksNotes':
213 aname = 'rtm.tasks.notes.%s' % attr
215 aname = 'rtm.%s.%s' % (self._prefix, attr)
216 return lambda **params: self.callMethod(
217 aname, rargs, oargs, **params
220 def callMethod(self, aname, rargs, oargs, **params):
222 for requiredArg in rargs:
223 if requiredArg not in params:
224 raise TypeError, 'Required parameter (%s) missing' % requiredArg
227 if param not in rargs + oargs:
228 warnings.warn('Invalid parameter (%s)' % param)
230 return self._rtm().get(method=aname,
231 auth_token=self._rtm()._authInfo.get('token'),
235 def sortedItems(dictionary):
236 "Return a list of (key, value) sorted based on keys"
237 keys = dictionary.keys()
240 yield key, dictionary[key]
243 class DottedDict(object):
244 "Make dictionary items accessible via the object-dot notation."
246 def __init__(self, name, dictionary):
249 if isinstance(dictionary, dict):
250 for key, value in dictionary.items():
251 if isinstance(value, dict):
252 value = DottedDict(key, value)
253 elif isinstance(value, (list, tuple)):
254 value = [DottedDict('%s_%d' % (key, i), item)
255 for i, item in enumerate(value)]
256 setattr(self, key, value)
259 children = [c for c in dir(self) if not c.startswith('_')]
260 return '<dotted %s: %s>' % (
265 children = [(c, getattr(self, c)) for c in dir(self) if not c.startswith('_')]
266 return '{dotted %s: %s}' % (
269 ('%s: "%s"' % (k, str(v)))
270 for (k, v) in children)
274 def safer_eval(string):
276 return eval(string, {}, {})
277 except SyntaxError, e:
281 newE = RTMParseError("Error parseing json")
287 parse_json = simplejson.loads
289 parse_json = safer_eval
295 [('auth_token'), ()],
303 [('timeline', 'contact'), ()],
305 [('timeline', 'contact_id'), ()],
311 [('timeline', 'group'), ()],
313 [('timeline', 'group_id', 'contact_id'), ()],
315 [('timeline', 'group_id'), ()],
319 [('timeline', 'group_id', 'contact_id'), ()],
323 [('timeline', 'name'), ('filter'), ()],
325 [('timeline', 'list_id'), ()],
327 [('timeline', 'list_id'), ()],
331 [('timeline'), ('list_id'), ()],
333 [('timeline', 'list_id', 'name'), ()],
335 [('timeline'), ('list_id'), ()],
343 [('methodName',), ()],
353 [('timeline', 'name',), ('list_id', 'parse',)],
355 [('timeline', 'list_id', 'taskseries_id', 'task_id', 'tags'),
358 [('timeline', 'list_id', 'taskseries_id', 'task_id',), ()],
360 [('timeline', 'list_id', 'taskseries_id', 'task_id'), ()],
363 ('list_id', 'filter', 'last_sync')],
365 [('timeline', 'list_id', 'taskseries_id', 'task_id', 'direction'),
368 [('timeline', 'from_list_id', 'to_list_id', 'taskseries_id', 'task_id'),
371 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
374 [('timeline', 'list_id', 'taskseries_id', 'task_id', 'tags'),
377 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
378 ('due', 'has_due_time', 'parse')],
380 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
383 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
386 [('timeline', 'list_id', 'taskseries_id', 'task_id', 'name'),
389 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
392 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
395 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
398 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
401 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
406 [('timeline', 'list_id', 'taskseries_id', 'task_id', 'note_title', 'note_text'), ()],
408 [('timeline', 'note_id'), ()],
410 [('timeline', 'note_id', 'note_title', 'note_text'), ()],
420 [('to_timezone',), ('from_timezone', 'to_timezone', 'time')],
422 [('text',), ('timezone', 'dateformat')],
434 [('timeline', 'transaction_id'), ()],