3 Python library for Remember The Milk API
5 @note For help, see http://www.rememberthemilk.com/services/api/methods/
13 _use_simplejson = False
16 _use_simplejson = True
21 __author__ = 'Sridhar Ratnakumar <http://nearfar.org/>'
23 SERVICE_URL = 'http://api.rememberthemilk.com/services/rest/'
24 AUTH_SERVICE_URL = 'http://www.rememberthemilk.com/services/auth/'
27 class RTMError(StandardError):
31 class RTMAPIError(RTMError):
35 class AuthStateMachine(object):
36 """If the state is in those setup for the machine, then return
37 the datum sent. Along the way, it is an automatic call if the
41 class NoData(RTMError):
44 def __init__(self, states):
48 def dataReceived(self, state, datum):
49 if state not in self.states:
50 raise RTMError, "Invalid state <%s>" % state
51 self.data[state] = datum
54 if state in self.data:
55 return self.data[state]
57 raise AuthStateMachine.NoData('No data for <%s>' % state)
62 def __init__(self, userID, apiKey, secret, token=None):
66 self._authInfo = AuthStateMachine(['frob', 'token'])
68 # this enables one to do 'rtm.tasks.getList()', for example
69 for prefix, methods in API.items():
71 RTMAPICategory(self, prefix, methods))
74 self._authInfo.dataReceived('token', token)
76 def _sign(self, params):
77 "Sign the parameters with MD5 hash"
78 pairs = ''.join(['%s%s' % (k, v) for (k, v) in sortedItems(params)])
79 return md5(self._secret+pairs).hexdigest()
81 def get(self, **params):
82 "Get the XML response for the passed `params`."
83 params['api_key'] = self._apiKey
84 params['format'] = 'json'
85 params['api_sig'] = self._sign(params)
87 json = openURL(SERVICE_URL, params).read()
90 data = dottedDict('ROOT', simplejson.loads(json))
92 data = dottedJSON(json)
95 if rsp.stat == 'fail':
96 raise RTMAPIError, 'API call failed - %s (%s)' % (
97 rsp.err.msg, rsp.err.code)
101 def getNewFrob(self):
102 rsp = self.get(method='rtm.auth.getFrob')
103 self._authInfo.dataReceived('frob', rsp.frob)
106 def getAuthURL(self):
108 frob = self._authInfo.get('frob')
109 except AuthStateMachine.NoData:
110 frob = self.getNewFrob()
113 'api_key': self._apiKey,
117 params['api_sig'] = self._sign(params)
118 return AUTH_SERVICE_URL + '?' + urllib.urlencode(params)
121 frob = self._authInfo.get('frob')
122 rsp = self.get(method='rtm.auth.getToken', frob=frob)
123 self._authInfo.dataReceived('token', rsp.auth.token)
124 return rsp.auth.token
127 class RTMAPICategory(object):
128 "See the `API` structure and `RTM.__init__`"
130 def __init__(self, rtm, prefix, methods):
131 self._rtm = weakref.ref(rtm)
132 self._prefix = prefix
133 self._methods = methods
135 def __getattr__(self, attr):
136 if attr not in self._methods:
137 raise AttributeError, 'No such attribute: %s' % attr
139 rargs, oargs = self._methods[attr]
140 if self._prefix == 'tasksNotes':
141 aname = 'rtm.tasks.notes.%s' % attr
143 aname = 'rtm.%s.%s' % (self._prefix, attr)
144 return lambda **params: self.callMethod(
145 aname, rargs, oargs, **params
148 def callMethod(self, aname, rargs, oargs, **params):
150 for requiredArg in rargs:
151 if requiredArg not in params:
152 raise TypeError, 'Required parameter (%s) missing' % requiredArg
155 if param not in rargs + oargs:
156 warnings.warn('Invalid parameter (%s)' % param)
158 return self._rtm().get(method=aname,
159 auth_token=self._rtm()._authInfo.get('token'),
163 def sortedItems(dictionary):
164 "Return a list of (key, value) sorted based on keys"
165 keys = dictionary.keys()
168 yield key, dictionary[key]
171 def openURL(url, queryArgs=None):
173 url = url + '?' + urllib.urlencode(queryArgs)
174 return urllib.urlopen(url)
177 class dottedDict(object):
178 "Make dictionary items accessible via the object-dot notation."
180 def __init__(self, name, dictionary):
183 if isinstance(dictionary, dict):
184 for key, value in dictionary.items():
185 if isinstance(value, dict):
186 value = dottedDict(key, value)
187 elif isinstance(value, (list, tuple)):
188 value = [dottedDict('%s_%d' % (key, i), item)
189 for i, item in enumerate(value)]
190 setattr(self, key, value)
193 children = [c for c in dir(self) if not c.startswith('_')]
194 return '<dotted %s: %s>' % (
199 children = [(c, getattr(self, c)) for c in dir(self) if not c.startswith('_')]
200 return '{dotted %s: %s}' % (
203 ('%s: "%s"' % (k, str(v)))
204 for (k, v) in children)
208 def safeEval(string):
209 return eval(string, {}, {})
212 def dottedJSON(json):
213 return dottedDict('ROOT', safeEval(json))
219 [('auth_token'), ()],
227 [('timeline', 'contact'), ()],
229 [('timeline', 'contact_id'), ()],
235 [('timeline', 'group'), ()],
237 [('timeline', 'group_id', 'contact_id'), ()],
239 [('timeline', 'group_id'), ()],
243 [('timeline', 'group_id', 'contact_id'), ()],
247 [('timeline', 'name'), ('filter'), ()],
249 [('timeline', 'list_id'), ()],
251 [('timeline', 'list_id'), ()],
255 [('timeline'), ('list_id'), ()],
257 [('timeline', 'list_id', 'name'), ()],
259 [('timeline'), ('list_id'), ()],
267 [('methodName',), ()],
277 [('timeline', 'name',), ('list_id', 'parse',)],
279 [('timeline', 'list_id', 'taskseries_id', 'task_id', 'tags'),
282 [('timeline', 'list_id', 'taskseries_id', 'task_id',), ()],
284 [('timeline', 'list_id', 'taskseries_id', 'task_id'), ()],
287 ('list_id', 'filter', 'last_sync')],
289 [('timeline', 'list_id', 'taskseries_id', 'task_id', 'direction'),
292 [('timeline', 'from_list_id', 'to_list_id', 'taskseries_id', 'task_id'),
295 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
298 [('timeline', 'list_id', 'taskseries_id', 'task_id', 'tags'),
301 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
302 ('due', 'has_due_time', 'parse')],
304 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
307 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
310 [('timeline', 'list_id', 'taskseries_id', 'task_id', 'name'),
313 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
316 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
319 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
322 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
325 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
330 [('timeline', 'list_id', 'taskseries_id', 'task_id', 'note_title', 'note_text'), ()],
332 [('timeline', 'note_id'), ()],
334 [('timeline', 'note_id', 'note_title', 'note_text'), ()]
344 [('to_timezone',), ('from_timezone', 'to_timezone', 'time')],
346 [('text',), ('timezone', 'dateformat')]
358 [('timeline', 'transaction_id'), ()]