Just remembered another task
[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         @staticmethod
82         def open_url(url, queryArgs=None):
83                 if queryArgs:
84                         url += '?' + urllib.urlencode(queryArgs)
85                 warnings.warn("Performing download of %s" % url, stacklevel=5)
86                 return urllib.urlopen(url)
87
88         def get(self, **params):
89                 "Get the XML response for the passed `params`."
90                 params['api_key'] = self._apiKey
91                 params['format'] = 'json'
92                 params['api_sig'] = self._sign(params)
93
94                 json = self.open_url(SERVICE_URL, params).read()
95
96                 if _use_simplejson:
97                         data = DottedDict('ROOT', simplejson.loads(json))
98                 else:
99                         data = DottedDict('ROOT', safer_eval(json))
100                 rsp = data.rsp
101
102                 if rsp.stat == 'fail':
103                         raise RTMAPIError, 'API call failed - %s (%s)' % (
104                                 rsp.err.msg, rsp.err.code)
105                 else:
106                         return rsp
107
108         def getNewFrob(self):
109                 rsp = self.get(method='rtm.auth.getFrob')
110                 self._authInfo.dataReceived('frob', rsp.frob)
111                 return rsp.frob
112
113         def getAuthURL(self):
114                 try:
115                         frob = self._authInfo.get('frob')
116                 except AuthStateMachine.NoData:
117                         frob = self.getNewFrob()
118
119                 params = {
120                         'api_key': self._apiKey,
121                         'perms': 'delete',
122                         'frob': frob
123                 }
124                 params['api_sig'] = self._sign(params)
125                 return AUTH_SERVICE_URL + '?' + urllib.urlencode(params)
126
127         def getToken(self):
128                 frob = self._authInfo.get('frob')
129                 rsp = self.get(method='rtm.auth.getToken', frob=frob)
130                 self._authInfo.dataReceived('token', rsp.auth.token)
131                 return rsp.auth.token
132
133
134 class RTMAPICategory(object):
135         "See the `API` structure and `RTM.__init__`"
136
137         def __init__(self, rtm, prefix, methods):
138                 self._rtm = weakref.ref(rtm)
139                 self._prefix = prefix
140                 self._methods = methods
141
142         def __getattr__(self, attr):
143                 if attr not in self._methods:
144                         raise AttributeError, 'No such attribute: %s' % attr
145
146                 rargs, oargs = self._methods[attr]
147                 if self._prefix == 'tasksNotes':
148                         aname = 'rtm.tasks.notes.%s' % attr
149                 else:
150                         aname = 'rtm.%s.%s' % (self._prefix, attr)
151                 return lambda **params: self.callMethod(
152                         aname, rargs, oargs, **params
153                 )
154
155         def callMethod(self, aname, rargs, oargs, **params):
156                 # Sanity checks
157                 for requiredArg in rargs:
158                         if requiredArg not in params:
159                                 raise TypeError, 'Required parameter (%s) missing' % requiredArg
160
161                 for param in params:
162                         if param not in rargs + oargs:
163                                 warnings.warn('Invalid parameter (%s)' % param)
164
165                 return self._rtm().get(method=aname,
166                                                         auth_token=self._rtm()._authInfo.get('token'),
167                                                         **params)
168
169
170 def sortedItems(dictionary):
171         "Return a list of (key, value) sorted based on keys"
172         keys = dictionary.keys()
173         keys.sort()
174         for key in keys:
175                 yield key, dictionary[key]
176
177
178 class DottedDict(object):
179         "Make dictionary items accessible via the object-dot notation."
180
181         def __init__(self, name, dictionary):
182                 self._name = name
183
184                 if isinstance(dictionary, dict):
185                         for key, value in dictionary.items():
186                                 if isinstance(value, dict):
187                                         value = DottedDict(key, value)
188                                 elif isinstance(value, (list, tuple)):
189                                         value = [DottedDict('%s_%d' % (key, i), item)
190                                                          for i, item in enumerate(value)]
191                                 setattr(self, key, value)
192
193         def __repr__(self):
194                 children = [c for c in dir(self) if not c.startswith('_')]
195                 return '<dotted %s: %s>' % (
196                         self._name,
197                         ', '.join(children))
198
199         def __str__(self):
200                 children = [(c, getattr(self, c)) for c in dir(self) if not c.startswith('_')]
201                 return '{dotted %s: %s}' % (
202                         self._name,
203                         ', '.join(
204                                 ('%s: "%s"' % (k, str(v)))
205                                 for (k, v) in children)
206                 )
207
208
209 def safer_eval(string):
210         return eval(string, {}, {})
211
212
213 API = {
214         'auth': {
215                 'checkToken':
216                         [('auth_token'), ()],
217                 'getFrob':
218                         [(), ()],
219                 'getToken':
220                         [('frob'), ()]
221         },
222         'contacts': {
223                 'add':
224                         [('timeline', 'contact'), ()],
225                 'delete':
226                         [('timeline', 'contact_id'), ()],
227                 'getList':
228                         [(), ()],
229         },
230         'groups': {
231                 'add':
232                         [('timeline', 'group'), ()],
233                 'addContact':
234                         [('timeline', 'group_id', 'contact_id'), ()],
235                 'delete':
236                         [('timeline', 'group_id'), ()],
237                 'getList':
238                         [(), ()],
239                 'removeContact':
240                         [('timeline', 'group_id', 'contact_id'), ()],
241         },
242         'lists': {
243                 'add':
244                         [('timeline', 'name'), ('filter'), ()],
245                 'archive':
246                         [('timeline', 'list_id'), ()],
247                 'delete':
248                         [('timeline', 'list_id'), ()],
249                 'getList':
250                         [(), ()],
251                 'setDefaultList':
252                         [('timeline'), ('list_id'), ()],
253                 'setName':
254                         [('timeline', 'list_id', 'name'), ()],
255                 'unarchive':
256                         [('timeline'), ('list_id'), ()],
257         },
258         'locations': {
259                 'getList':
260                         [(), ()],
261         },
262         'reflection': {
263                 'getMethodInfo':
264                         [('methodName',), ()],
265                 'getMethods':
266                         [(), ()],
267         },
268         'settings': {
269                 'getList':
270                         [(), ()],
271         },
272         'tasks': {
273                 'add':
274                         [('timeline', 'name',), ('list_id', 'parse',)],
275                 'addTags':
276                         [('timeline', 'list_id', 'taskseries_id', 'task_id', 'tags'),
277                          ()],
278                 'complete':
279                         [('timeline', 'list_id', 'taskseries_id', 'task_id',), ()],
280                 'delete':
281                         [('timeline', 'list_id', 'taskseries_id', 'task_id'), ()],
282                 'getList':
283                         [(),
284                          ('list_id', 'filter', 'last_sync')],
285                 'movePriority':
286                         [('timeline', 'list_id', 'taskseries_id', 'task_id', 'direction'),
287                          ()],
288                 'moveTo':
289                         [('timeline', 'from_list_id', 'to_list_id', 'taskseries_id', 'task_id'),
290                          ()],
291                 'postpone':
292                         [('timeline', 'list_id', 'taskseries_id', 'task_id'),
293                          ()],
294                 'removeTags':
295                         [('timeline', 'list_id', 'taskseries_id', 'task_id', 'tags'),
296                          ()],
297                 'setDueDate':
298                         [('timeline', 'list_id', 'taskseries_id', 'task_id'),
299                          ('due', 'has_due_time', 'parse')],
300                 'setEstimate':
301                         [('timeline', 'list_id', 'taskseries_id', 'task_id'),
302                          ('estimate',)],
303                 'setLocation':
304                         [('timeline', 'list_id', 'taskseries_id', 'task_id'),
305                          ('location_id',)],
306                 'setName':
307                         [('timeline', 'list_id', 'taskseries_id', 'task_id', 'name'),
308                          ()],
309                 'setPriority':
310                         [('timeline', 'list_id', 'taskseries_id', 'task_id'),
311                          ('priority',)],
312                 'setRecurrence':
313                         [('timeline', 'list_id', 'taskseries_id', 'task_id'),
314                          ('repeat',)],
315                 'setTags':
316                         [('timeline', 'list_id', 'taskseries_id', 'task_id'),
317                          ('tags',)],
318                 'setURL':
319                         [('timeline', 'list_id', 'taskseries_id', 'task_id'),
320                          ('url',)],
321                 'uncomplete':
322                         [('timeline', 'list_id', 'taskseries_id', 'task_id'),
323                          ()],
324         },
325         'tasksNotes': {
326                 'add':
327                         [('timeline', 'list_id', 'taskseries_id', 'task_id', 'note_title', 'note_text'), ()],
328                 'delete':
329                         [('timeline', 'note_id'), ()],
330                 'edit':
331                         [('timeline', 'note_id', 'note_title', 'note_text'), ()],
332         },
333         'test': {
334                 'echo':
335                         [(), ()],
336                 'login':
337                         [(), ()],
338         },
339         'time': {
340                 'convert':
341                         [('to_timezone',), ('from_timezone', 'to_timezone', 'time')],
342                 'parse':
343                         [('text',), ('timezone', 'dateformat')],
344         },
345         'timelines': {
346                 'create':
347                         [(), ()],
348         },
349         'timezones': {
350                 'getList':
351                         [(), ()],
352         },
353         'transactions': {
354                 'undo':
355                         [('timeline', 'transaction_id'), ()],
356         },
357 }