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