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