* Made simpleson an optional dependency. If we can't import it,
[pyrtm.git] / rtm.py
blob2e6675d90d133e7fe874aa9319c5ad8287e10c2f
1 # Python library for Remember The Milk API
3 __author__ = 'Sridhar Ratnakumar <http://nearfar.org/>'
4 __all__ = [
5 'DEBUG',
6 'API',
7 'createRTM',
11 import new
12 import warnings
13 import urllib
14 from md5 import md5
15 _use_simplejson = False
16 try:
17 import simplejson
18 _use_simplejson = True
19 except ImportError:
20 pass
23 SERVICE_URL = 'http://api.rememberthemilk.com/services/rest/'
24 AUTH_SERVICE_URL = 'http://www.rememberthemilk.com/services/auth/'
25 DEBUG = False
28 class RTMError(Exception): pass
30 class RTMAPIError(RTMError): pass
32 class AuthStateMachine(object):
34 class NoData(RTMError): pass
36 def __init__(self, states):
37 self.states = states
38 self.data = {}
40 def dataReceived(self, state, datum):
41 if state not in self.states:
42 raise RTMError, "Invalid state <%s>" % state
43 self.data[state] = datum
45 def get(self, state):
46 if state in self.data:
47 return self.data[state]
48 else:
49 raise AuthStateMachine.NoData, 'No data for <%s>' % state
52 class RTM(object):
54 def __init__(self, apiKey, secret, token=None, DEBUG=False):
55 self.apiKey = apiKey
56 self.secret = secret
57 self.authInfo = AuthStateMachine(['frob', 'token'])
58 self.DEBUG = DEBUG
60 # this enables one to do 'rtm.tasks.getList()', for example
61 for prefix, methods in API.items():
62 setattr(self, prefix,
63 RTMAPICategory(self, prefix, methods))
65 if token:
66 self.authInfo.dataReceived('token', token)
68 def _sign(self, params):
69 "Sign the parameters with MD5 hash"
70 pairs = ''.join(['%s%s' % (k,v) for k,v in sortedItems(params)])
71 return md5(self.secret+pairs).hexdigest()
73 def get(self, **params):
74 "Get the XML response for the passed `params`."
75 params['api_key'] = self.apiKey
76 params['format'] = 'json'
77 params['api_sig'] = self._sign(params)
79 json = openURL(SERVICE_URL, params).read()
80 if self.DEBUG:
81 print json
82 if _use_simplejson:
83 data = dottedDict('ROOT', simplejson.loads(json))
84 else:
85 data = dottedJSON(json)
86 rsp = data.rsp
88 if rsp.stat == 'fail':
89 raise RTMAPIError, 'API call failed - %s (%s)' % (
90 rsp.err.msg, rsp.err.code)
91 else:
92 return rsp
94 def getNewFrob(self):
95 rsp = self.get(method='rtm.auth.getFrob')
96 self.authInfo.dataReceived('frob', rsp.frob)
97 return rsp.frob
99 def getAuthURL(self):
100 try:
101 frob = self.authInfo.get('frob')
102 except AuthStateMachine.NoData:
103 frob = self.getNewFrob()
105 params = {
106 'api_key': self.apiKey,
107 'perms' : 'delete',
108 'frob' : frob
110 params['api_sig'] = self._sign(params)
111 return AUTH_SERVICE_URL + '?' + urllib.urlencode(params)
113 def getToken(self):
114 frob = self.authInfo.get('frob')
115 rsp = self.get(method='rtm.auth.getToken', frob=frob)
116 self.authInfo.dataReceived('token', rsp.auth.token)
117 return rsp.auth.token
119 class RTMAPICategory:
120 "See the `API` structure and `RTM.__init__`"
122 def __init__(self, rtm, prefix, methods):
123 self.rtm = rtm
124 self.prefix = prefix
125 self.methods = methods
127 def __getattr__(self, attr):
128 if attr in self.methods:
129 rargs, oargs = self.methods[attr]
130 aname = 'rtm.%s.%s' % (self.prefix, attr)
131 return lambda **params: self.callMethod(
132 aname, rargs, oargs, **params)
133 else:
134 raise AttributeError, 'No such attribute: %s' % attr
136 def callMethod(self, aname, rargs, oargs, **params):
137 # Sanity checks
138 for requiredArg in rargs:
139 if requiredArg not in params:
140 raise TypeError, 'Required parameter (%s) missing' % requiredArg
142 for param in params:
143 if param not in rargs + oargs:
144 warnings.warn('Invalid parameter (%s)' % param)
146 return self.rtm.get(method=aname,
147 auth_token=self.rtm.authInfo.get('token'),
148 **params)
152 # Utility functions
154 def sortedItems(dictionary):
155 "Return a list of (key, value) sorted based on keys"
156 keys = dictionary.keys()
157 keys.sort()
158 for key in keys:
159 yield key, dictionary[key]
161 def openURL(url, queryArgs=None):
162 if queryArgs:
163 url = url + '?' + urllib.urlencode(queryArgs)
164 if DEBUG:
165 print 'URL>', url
166 return urllib.urlopen(url)
168 class dottedDict(object):
169 "Make dictionary items accessible via the object-dot notation."
171 def __init__(self, name, dictionary):
172 self._name = name
174 if type(dictionary) is dict:
175 for key, value in dictionary.items():
176 if type(value) is dict:
177 value = dottedDict(key, value)
178 elif type(value) in (list, tuple):
179 value = [dottedDict('%s_%d' % (key, i), item)
180 for i, item in indexed(value)]
181 setattr(self, key, value)
183 def __repr__(self):
184 children = [c for c in dir(self) if not c.startswith('_')]
185 return 'dotted <%s> : %s' % (
186 self._name,
187 ', '.join(children))
190 def safeEval(string):
191 return eval(string, {}, {})
193 def dottedJSON(json):
194 return dottedDict('ROOT', safeEval(json))
196 def indexed(seq):
197 index = 0
198 for item in seq:
199 yield index, item
200 index += 1
203 # API spec
205 API = {
206 'auth': {
207 'checkToken':
208 [('auth_token'), ()],
209 'getFrob':
210 [(), ()],
211 'getToken':
212 [('frob'), ()]
214 'contacts': {
215 'add':
216 [('timeline', 'contact'), ()],
217 'delete':
218 [('timeline', 'contact_id'), ()],
219 'getList':
220 [(), ()]
222 'groups': {
223 'add':
224 [('timeline', 'group'), ()],
225 'addContact':
226 [('timeline', 'group_id', 'contact_id'), ()],
227 'delete':
228 [('timeline', 'group_id'), ()],
229 'getList':
230 [(), ()],
231 'removeContact':
232 [('timeline', 'group_id', 'contact_id'), ()],
234 'lists': {
235 'add':
236 [('timeline', 'name'), ('filter'), ()],
237 'archive':
238 [('timeline', 'list_id'), ()],
239 'delete':
240 [('timeline', 'list_id'), ()],
241 'getList':
242 [(), ()],
243 'setDefaultList':
244 [('timeline'), ('list_id'), ()],
245 'setName':
246 [('timeline', 'list_id', 'name'), ()],
247 'unarchive':
248 [('timeline'), ('list_id'), ()],
250 'locations': {
251 'getList':
252 [(), ()]
254 'reflection': {
255 'getMethodInfo':
256 [('methodName',), ()],
257 'getMethods':
258 [(), ()]
260 'settings': {
261 'getList':
262 [(), ()]
264 'tasks': {
265 'add':
266 [('timeline', 'name',), ('list_id', 'parse',)],
267 'addTags':
268 [('timeline', 'list_id', 'taskseries_id', 'task_id', 'tags'),
269 ()],
270 'complete':
271 [('timeline', 'list_id', 'taskseries_id', 'task_id',), ()],
272 'delete':
273 [('timeline', 'list_id', 'taskseries_id', 'task_id'), ()],
274 'getList':
275 [(),
276 ('list_id', 'filter', 'last_sync')],
277 'movePriority':
278 [('timeline', 'list_id', 'taskseries_id', 'task_id', 'direction'),
279 ()],
280 'moveTo':
281 [('timeline', 'from_list_id', 'to_list_id', 'taskseries_id', 'task_id'),
282 ()],
283 'postpone':
284 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
285 ()],
286 'removeTags':
287 [('timeline', 'list_id', 'taskseries_id', 'task_id', 'tags'),
288 ()],
289 'setDueDate':
290 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
291 ('due', 'has_due_time', 'parse')],
292 'setEstimate':
293 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
294 ('estimate',)],
295 'setLocation':
296 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
297 ('location_id',)],
298 'setName':
299 [('timeline', 'list_id', 'taskseries_id', 'task_id', 'name'),
300 ()],
301 'setPriority':
302 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
303 ('priority',)],
304 'setRecurrence':
305 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
306 ('repeat',)],
307 'setTags':
308 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
309 ('tags',)],
310 'setURL':
311 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
312 ('url',)],
313 'uncomplete':
314 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
315 ()],
317 'tasksNotes': {
318 'add':
319 [('timeline', 'list_id', 'taskseries_id', 'task_id', 'note_title', 'note_text'), ()],
320 'delete':
321 [('timeline', 'note_id'), ()],
322 'edit':
323 [('timeline', 'note_id', 'note_title', 'note_text'), ()]
325 'test': {
326 'echo':
327 [(), ()],
328 'login':
329 [(), ()]
331 'time': {
332 'convert':
333 [('to_timezone',), ('from_timezone', 'to_timezone', 'time')],
334 'parse':
335 [('text',), ('timezone', 'dateformat')]
337 'timelines': {
338 'create':
339 [(), ()]
341 'timezones': {
342 'getList':
343 [(), ()]
345 'transactions': {
346 'undo':
347 [('timeline', 'transaction_id'), ()]
351 def createRTM(apiKey, secret, token=None, DEBUG=DEBUG):
352 rtm = RTM(apiKey, secret, token, DEBUG)
354 if token is None:
355 print 'No token found'
356 print 'Give me access here:', rtm.getAuthURL()
357 raw_input('Press enter once you gave access')
358 print 'Note down this token for future use:', rtm.getToken()
360 return rtm
362 def test(apiKey, secret, token=None):
363 rtm = createRTM(apiKey, secret, token)
365 rspTasks = rtm.tasks.getList(filter='dueWithin:"1 week of today"')
366 print [t.name for t in rspTasks.tasks.list.taskseries]
367 print rspTasks.tasks.list.id
369 rspLists = rtm.lists.getList()
370 # print rspLists.lists.list
371 print [(x.name, x.id) for x in rspLists.lists.list]