Adding little cleanups here or there to the raw API wrapper
[doneit] / src / rtmapi.py
1
2 """
3 Python library for Remember The Milk API
4
5 @note For help, see http://www.rememberthemilk.com/services/api/methods/
6 """
7
8 import weakref
9 import warnings
10 import urllib
11 from md5 import md5
12
13 _use_simplejson = False
14 try:
15         import simplejson
16         _use_simplejson = True
17 except ImportError:
18         pass
19
20
21 __author__ = 'Sridhar Ratnakumar <http://nearfar.org/>'
22
23 SERVICE_URL = 'http://api.rememberthemilk.com/services/rest/'
24 AUTH_SERVICE_URL = 'http://www.rememberthemilk.com/services/auth/'
25
26
27 class RTMError(StandardError):
28         pass
29
30
31 class RTMAPIError(RTMError):
32         pass
33
34
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
38         datum is a method.
39         """
40
41         class NoData(RTMError):
42                 pass
43
44         def __init__(self, states):
45                 self.states = states
46                 self.data = {}
47
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
52
53         def get(self, state):
54                 if state in self.data:
55                         return self.data[state]
56                 else:
57                         raise AuthStateMachine.NoData('No data for <%s>' % state)
58
59
60 class RTMapi(object):
61
62         def __init__(self, userID, apiKey, secret, token=None):
63                 self._userID = userID
64                 self._apiKey = apiKey
65                 self._secret = secret
66                 self._authInfo = AuthStateMachine(['frob', 'token'])
67
68                 # this enables one to do 'rtm.tasks.getList()', for example
69                 for prefix, methods in API.items():
70                         setattr(self, prefix,
71                                         RTMAPICategory(self, prefix, methods))
72
73                 if token:
74                         self._authInfo.dataReceived('token', token)
75
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()
80
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)
86
87                 json = open_url(SERVICE_URL, params).read()
88
89                 if _use_simplejson:
90                         data = DottedDict('ROOT', simplejson.loads(json))
91                 else:
92                         data = DottedDict('ROOT', safer_eval(json))
93                 rsp = data.rsp
94
95                 if rsp.stat == 'fail':
96                         raise RTMAPIError, 'API call failed - %s (%s)' % (
97                                 rsp.err.msg, rsp.err.code)
98                 else:
99                         return rsp
100
101         def getNewFrob(self):
102                 rsp = self.get(method='rtm.auth.getFrob')
103                 self._authInfo.dataReceived('frob', rsp.frob)
104                 return rsp.frob
105
106         def getAuthURL(self):
107                 try:
108                         frob = self._authInfo.get('frob')
109                 except AuthStateMachine.NoData:
110                         frob = self.getNewFrob()
111
112                 params = {
113                         'api_key': self._apiKey,
114                         'perms': 'delete',
115                         'frob': frob
116                 }
117                 params['api_sig'] = self._sign(params)
118                 return AUTH_SERVICE_URL + '?' + urllib.urlencode(params)
119
120         def getToken(self):
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
125
126
127 class RTMAPICategory(object):
128         "See the `API` structure and `RTM.__init__`"
129
130         def __init__(self, rtm, prefix, methods):
131                 self._rtm = weakref.ref(rtm)
132                 self._prefix = prefix
133                 self._methods = methods
134
135         def __getattr__(self, attr):
136                 if attr not in self._methods:
137                         raise AttributeError, 'No such attribute: %s' % attr
138
139                 rargs, oargs = self._methods[attr]
140                 if self._prefix == 'tasksNotes':
141                         aname = 'rtm.tasks.notes.%s' % attr
142                 else:
143                         aname = 'rtm.%s.%s' % (self._prefix, attr)
144                 return lambda **params: self.callMethod(
145                         aname, rargs, oargs, **params
146                 )
147
148         def callMethod(self, aname, rargs, oargs, **params):
149                 # Sanity checks
150                 for requiredArg in rargs:
151                         if requiredArg not in params:
152                                 raise TypeError, 'Required parameter (%s) missing' % requiredArg
153
154                 for param in params:
155                         if param not in rargs + oargs:
156                                 warnings.warn('Invalid parameter (%s)' % param)
157
158                 return self._rtm().get(method=aname,
159                                                         auth_token=self._rtm()._authInfo.get('token'),
160                                                         **params)
161
162
163 def sortedItems(dictionary):
164         "Return a list of (key, value) sorted based on keys"
165         keys = dictionary.keys()
166         keys.sort()
167         for key in keys:
168                 yield key, dictionary[key]
169
170
171 def open_url(url, queryArgs=None):
172         if queryArgs:
173                 url = url + '?' + urllib.urlencode(queryArgs)
174         return urllib.urlopen(url)
175
176
177 class DottedDict(object):
178         "Make dictionary items accessible via the object-dot notation."
179
180         def __init__(self, name, dictionary):
181                 self._name = name
182
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)
191
192         def __repr__(self):
193                 children = [c for c in dir(self) if not c.startswith('_')]
194                 return '<dotted %s: %s>' % (
195                         self._name,
196                         ', '.join(children))
197
198         def __str__(self):
199                 children = [(c, getattr(self, c)) for c in dir(self) if not c.startswith('_')]
200                 return '{dotted %s: %s}' % (
201                         self._name,
202                         ', '.join(
203                                 ('%s: "%s"' % (k, str(v)))
204                                 for (k, v) in children)
205                 )
206
207
208 def safer_eval(string):
209         return eval(string, {}, {})
210
211
212 API = {
213         'auth': {
214                 'checkToken':
215                         [('auth_token'), ()],
216                 'getFrob':
217                         [(), ()],
218                 'getToken':
219                         [('frob'), ()]
220         },
221         'contacts': {
222                 'add':
223                         [('timeline', 'contact'), ()],
224                 'delete':
225                         [('timeline', 'contact_id'), ()],
226                 'getList':
227                         [(), ()],
228         },
229         'groups': {
230                 'add':
231                         [('timeline', 'group'), ()],
232                 'addContact':
233                         [('timeline', 'group_id', 'contact_id'), ()],
234                 'delete':
235                         [('timeline', 'group_id'), ()],
236                 'getList':
237                         [(), ()],
238                 'removeContact':
239                         [('timeline', 'group_id', 'contact_id'), ()],
240         },
241         'lists': {
242                 'add':
243                         [('timeline', 'name'), ('filter'), ()],
244                 'archive':
245                         [('timeline', 'list_id'), ()],
246                 'delete':
247                         [('timeline', 'list_id'), ()],
248                 'getList':
249                         [(), ()],
250                 'setDefaultList':
251                         [('timeline'), ('list_id'), ()],
252                 'setName':
253                         [('timeline', 'list_id', 'name'), ()],
254                 'unarchive':
255                         [('timeline'), ('list_id'), ()],
256         },
257         'locations': {
258                 'getList':
259                         [(), ()],
260         },
261         'reflection': {
262                 'getMethodInfo':
263                         [('methodName',), ()],
264                 'getMethods':
265                         [(), ()],
266         },
267         'settings': {
268                 'getList':
269                         [(), ()],
270         },
271         'tasks': {
272                 'add':
273                         [('timeline', 'name',), ('list_id', 'parse',)],
274                 'addTags':
275                         [('timeline', 'list_id', 'taskseries_id', 'task_id', 'tags'),
276                          ()],
277                 'complete':
278                         [('timeline', 'list_id', 'taskseries_id', 'task_id',), ()],
279                 'delete':
280                         [('timeline', 'list_id', 'taskseries_id', 'task_id'), ()],
281                 'getList':
282                         [(),
283                          ('list_id', 'filter', 'last_sync')],
284                 'movePriority':
285                         [('timeline', 'list_id', 'taskseries_id', 'task_id', 'direction'),
286                          ()],
287                 'moveTo':
288                         [('timeline', 'from_list_id', 'to_list_id', 'taskseries_id', 'task_id'),
289                          ()],
290                 'postpone':
291                         [('timeline', 'list_id', 'taskseries_id', 'task_id'),
292                          ()],
293                 'removeTags':
294                         [('timeline', 'list_id', 'taskseries_id', 'task_id', 'tags'),
295                          ()],
296                 'setDueDate':
297                         [('timeline', 'list_id', 'taskseries_id', 'task_id'),
298                          ('due', 'has_due_time', 'parse')],
299                 'setEstimate':
300                         [('timeline', 'list_id', 'taskseries_id', 'task_id'),
301                          ('estimate',)],
302                 'setLocation':
303                         [('timeline', 'list_id', 'taskseries_id', 'task_id'),
304                          ('location_id',)],
305                 'setName':
306                         [('timeline', 'list_id', 'taskseries_id', 'task_id', 'name'),
307                          ()],
308                 'setPriority':
309                         [('timeline', 'list_id', 'taskseries_id', 'task_id'),
310                          ('priority',)],
311                 'setRecurrence':
312                         [('timeline', 'list_id', 'taskseries_id', 'task_id'),
313                          ('repeat',)],
314                 'setTags':
315                         [('timeline', 'list_id', 'taskseries_id', 'task_id'),
316                          ('tags',)],
317                 'setURL':
318                         [('timeline', 'list_id', 'taskseries_id', 'task_id'),
319                          ('url',)],
320                 'uncomplete':
321                         [('timeline', 'list_id', 'taskseries_id', 'task_id'),
322                          ()],
323         },
324         'tasksNotes': {
325                 'add':
326                         [('timeline', 'list_id', 'taskseries_id', 'task_id', 'note_title', 'note_text'), ()],
327                 'delete':
328                         [('timeline', 'note_id'), ()],
329                 'edit':
330                         [('timeline', 'note_id', 'note_title', 'note_text'), ()],
331         },
332         'test': {
333                 'echo':
334                         [(), ()],
335                 'login':
336                         [(), ()],
337         },
338         'time': {
339                 'convert':
340                         [('to_timezone',), ('from_timezone', 'to_timezone', 'time')],
341                 'parse':
342                         [('text',), ('timezone', 'dateformat')],
343         },
344         'timelines': {
345                 'create':
346                         [(), ()],
347         },
348         'timezones': {
349                 'getList':
350                         [(), ()],
351         },
352         'transactions': {
353                 'undo':
354                         [('timeline', 'transaction_id'), ()],
355         },
356 }