3 Python library for Remember The Milk API
5 @note For help, see http://www.rememberthemilk.com/services/api/methods/
7 @bug Under random situations, the response comes back incomplete. Maybe a race condition, but how?
16 _use_simplejson = False
19 _use_simplejson = True
24 __author__ = 'Sridhar Ratnakumar <http://nearfar.org/>'
26 SERVICE_URL = 'http://api.rememberthemilk.com/services/rest/'
27 AUTH_SERVICE_URL = 'http://www.rememberthemilk.com/services/auth/'
30 class RTMError(StandardError):
34 class RTMAPIError(RTMError):
38 class RTMParseError(RTMError):
42 class AuthStateMachine(object):
43 """If the state is in those setup for the machine, then return
44 the datum sent. Along the way, it is an automatic call if the
48 class NoData(RTMError):
51 def __init__(self, states):
55 def dataReceived(self, state, datum):
56 if state not in self.states:
57 raise RTMError, "Invalid state <%s>" % state
58 self.data[state] = datum
61 if state in self.data:
62 return self.data[state]
64 raise AuthStateMachine.NoData('No data for <%s>' % state)
69 def __init__(self, userID, apiKey, secret, token=None):
73 self._authInfo = AuthStateMachine(['frob', 'token'])
75 # this enables one to do 'rtm.tasks.getList()', for example
76 for prefix, methods in API.items():
78 RTMAPICategory(self, prefix, methods))
81 self._authInfo.dataReceived('token', token)
83 def _sign(self, params):
84 "Sign the parameters with MD5 hash"
85 pairs = ''.join(['%s%s' % (k, v) for (k, v) in sortedItems(params)])
86 return md5.md5(self._secret+pairs).hexdigest()
89 def open_url(url, queryArgs=None):
91 url += '?' + urllib.urlencode(queryArgs)
92 warnings.warn("Performing download of %s" % url, stacklevel=5)
93 return urllib2.urlopen(url)
95 def get(self, **params):
96 "Get the XML response for the passed `params`."
97 params['api_key'] = self._apiKey
98 params['format'] = 'json'
99 params['api_sig'] = self._sign(params)
101 connection = self.open_url(SERVICE_URL, params)
102 json = connection.read()
103 data = DottedDict('ROOT', parse_json(json))
106 if rsp.stat == 'fail':
107 raise RTMAPIError, 'API call failed - %s (%s)' % (
108 rsp.err.msg, rsp.err.code)
112 def getNewFrob(self):
113 rsp = self.get(method='rtm.auth.getFrob')
114 self._authInfo.dataReceived('frob', rsp.frob)
117 def getAuthURL(self):
119 frob = self._authInfo.get('frob')
120 except AuthStateMachine.NoData:
121 frob = self.getNewFrob()
124 'api_key': self._apiKey,
128 params['api_sig'] = self._sign(params)
129 return AUTH_SERVICE_URL + '?' + urllib.urlencode(params)
132 frob = self._authInfo.get('frob')
133 rsp = self.get(method='rtm.auth.getToken', frob=frob)
134 self._authInfo.dataReceived('token', rsp.auth.token)
135 return rsp.auth.token
138 class RTMAPICategory(object):
139 "See the `API` structure and `RTM.__init__`"
141 def __init__(self, rtm, prefix, methods):
142 self._rtm = weakref.ref(rtm)
143 self._prefix = prefix
144 self._methods = methods
146 def __getattr__(self, attr):
147 if attr not in self._methods:
148 raise AttributeError, 'No such attribute: %s' % attr
150 rargs, oargs = self._methods[attr]
151 if self._prefix == 'tasksNotes':
152 aname = 'rtm.tasks.notes.%s' % attr
154 aname = 'rtm.%s.%s' % (self._prefix, attr)
155 return lambda **params: self.callMethod(
156 aname, rargs, oargs, **params
159 def callMethod(self, aname, rargs, oargs, **params):
161 for requiredArg in rargs:
162 if requiredArg not in params:
163 raise TypeError, 'Required parameter (%s) missing' % requiredArg
166 if param not in rargs + oargs:
167 warnings.warn('Invalid parameter (%s)' % param)
169 return self._rtm().get(method=aname,
170 auth_token=self._rtm()._authInfo.get('token'),
174 def sortedItems(dictionary):
175 "Return a list of (key, value) sorted based on keys"
176 keys = dictionary.keys()
179 yield key, dictionary[key]
182 class DottedDict(object):
183 "Make dictionary items accessible via the object-dot notation."
185 def __init__(self, name, dictionary):
188 if isinstance(dictionary, dict):
189 for key, value in dictionary.items():
190 if isinstance(value, dict):
191 value = DottedDict(key, value)
192 elif isinstance(value, (list, tuple)):
193 value = [DottedDict('%s_%d' % (key, i), item)
194 for i, item in enumerate(value)]
195 setattr(self, key, value)
198 children = [c for c in dir(self) if not c.startswith('_')]
199 return '<dotted %s: %s>' % (
204 children = [(c, getattr(self, c)) for c in dir(self) if not c.startswith('_')]
205 return '{dotted %s: %s}' % (
208 ('%s: "%s"' % (k, str(v)))
209 for (k, v) in children)
213 def safer_eval(string):
215 return eval(string, {}, {})
216 except SyntaxError, e:
220 newE = RTMParseError("Error parseing json")
226 parse_json = simplejson.loads
228 parse_json = safer_eval
234 [('auth_token'), ()],
242 [('timeline', 'contact'), ()],
244 [('timeline', 'contact_id'), ()],
250 [('timeline', 'group'), ()],
252 [('timeline', 'group_id', 'contact_id'), ()],
254 [('timeline', 'group_id'), ()],
258 [('timeline', 'group_id', 'contact_id'), ()],
262 [('timeline', 'name'), ('filter'), ()],
264 [('timeline', 'list_id'), ()],
266 [('timeline', 'list_id'), ()],
270 [('timeline'), ('list_id'), ()],
272 [('timeline', 'list_id', 'name'), ()],
274 [('timeline'), ('list_id'), ()],
282 [('methodName',), ()],
292 [('timeline', 'name',), ('list_id', 'parse',)],
294 [('timeline', 'list_id', 'taskseries_id', 'task_id', 'tags'),
297 [('timeline', 'list_id', 'taskseries_id', 'task_id',), ()],
299 [('timeline', 'list_id', 'taskseries_id', 'task_id'), ()],
302 ('list_id', 'filter', 'last_sync')],
304 [('timeline', 'list_id', 'taskseries_id', 'task_id', 'direction'),
307 [('timeline', 'from_list_id', 'to_list_id', 'taskseries_id', 'task_id'),
310 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
313 [('timeline', 'list_id', 'taskseries_id', 'task_id', 'tags'),
316 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
317 ('due', 'has_due_time', 'parse')],
319 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
322 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
325 [('timeline', 'list_id', 'taskseries_id', 'task_id', 'name'),
328 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
331 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
334 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
337 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
340 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
345 [('timeline', 'list_id', 'taskseries_id', 'task_id', 'note_title', 'note_text'), ()],
347 [('timeline', 'note_id'), ()],
349 [('timeline', 'note_id', 'note_title', 'note_text'), ()],
359 [('to_timezone',), ('from_timezone', 'to_timezone', 'time')],
361 [('text',), ('timezone', 'dateformat')],
373 [('timeline', 'transaction_id'), ()],