* Use standard python logging module to handle debugging messages.
[pyrtm.git] / rtm.py
bloba3885b848b63a9950ebee5eadc6d617e61620b85
1 # Python library for Remember The Milk API
3 __author__ = 'Sridhar Ratnakumar <http://nearfar.org/>'
4 __all__ = (
5 'API',
6 'createRTM',
7 'set_log_level',
11 import new
12 import warnings
13 import urllib
14 import logging
15 from md5 import md5
16 _use_simplejson = False
17 try:
18 import simplejson
19 _use_simplejson = True
20 except ImportError:
21 pass
23 logging.basicConfig()
24 LOG = logging.getLogger(__name__)
25 LOG.setLevel(logging.INFO)
27 SERVICE_URL = 'http://api.rememberthemilk.com/services/rest/'
28 AUTH_SERVICE_URL = 'http://www.rememberthemilk.com/services/auth/'
31 class RTMError(Exception): pass
33 class RTMAPIError(RTMError): pass
35 class AuthStateMachine(object):
37 class NoData(RTMError): pass
39 def __init__(self, states):
40 self.states = states
41 self.data = {}
43 def dataReceived(self, state, datum):
44 if state not in self.states:
45 raise RTMError, "Invalid state <%s>" % state
46 self.data[state] = datum
48 def get(self, state):
49 if state in self.data:
50 return self.data[state]
51 else:
52 raise AuthStateMachine.NoData, 'No data for <%s>' % state
55 class RTM(object):
57 def __init__(self, apiKey, secret, token=None):
58 self.apiKey = apiKey
59 self.secret = secret
60 self.authInfo = AuthStateMachine(['frob', 'token'])
62 # this enables one to do 'rtm.tasks.getList()', for example
63 for prefix, methods in API.items():
64 setattr(self, prefix,
65 RTMAPICategory(self, prefix, methods))
67 if token:
68 self.authInfo.dataReceived('token', token)
70 def _sign(self, params):
71 "Sign the parameters with MD5 hash"
72 pairs = ''.join(['%s%s' % (k,v) for k,v in sortedItems(params)])
73 return md5(self.secret+pairs).hexdigest()
75 def get(self, **params):
76 "Get the XML response for the passed `params`."
77 params['api_key'] = self.apiKey
78 params['format'] = 'json'
79 params['api_sig'] = self._sign(params)
81 json = openURL(SERVICE_URL, params).read()
83 LOG.debug("JSON response: \n%s" % json)
85 if _use_simplejson:
86 data = dottedDict('ROOT', simplejson.loads(json))
87 else:
88 data = dottedJSON(json)
89 rsp = data.rsp
91 if rsp.stat == 'fail':
92 raise RTMAPIError, 'API call failed - %s (%s)' % (
93 rsp.err.msg, rsp.err.code)
94 else:
95 return rsp
97 def getNewFrob(self):
98 rsp = self.get(method='rtm.auth.getFrob')
99 self.authInfo.dataReceived('frob', rsp.frob)
100 return rsp.frob
102 def getAuthURL(self):
103 try:
104 frob = self.authInfo.get('frob')
105 except AuthStateMachine.NoData:
106 frob = self.getNewFrob()
108 params = {
109 'api_key': self.apiKey,
110 'perms' : 'delete',
111 'frob' : frob
113 params['api_sig'] = self._sign(params)
114 return AUTH_SERVICE_URL + '?' + urllib.urlencode(params)
116 def getToken(self):
117 frob = self.authInfo.get('frob')
118 rsp = self.get(method='rtm.auth.getToken', frob=frob)
119 self.authInfo.dataReceived('token', rsp.auth.token)
120 return rsp.auth.token
122 class RTMAPICategory:
123 "See the `API` structure and `RTM.__init__`"
125 def __init__(self, rtm, prefix, methods):
126 self.rtm = rtm
127 self.prefix = prefix
128 self.methods = methods
130 def __getattr__(self, attr):
131 if attr in self.methods:
132 rargs, oargs = self.methods[attr]
133 aname = 'rtm.%s.%s' % (self.prefix, attr)
134 return lambda **params: self.callMethod(
135 aname, rargs, oargs, **params)
136 else:
137 raise AttributeError, 'No such attribute: %s' % attr
139 def callMethod(self, aname, rargs, oargs, **params):
140 # Sanity checks
141 for requiredArg in rargs:
142 if requiredArg not in params:
143 raise TypeError, 'Required parameter (%s) missing' % requiredArg
145 for param in params:
146 if param not in rargs + oargs:
147 warnings.warn('Invalid parameter (%s)' % param)
149 return self.rtm.get(method=aname,
150 auth_token=self.rtm.authInfo.get('token'),
151 **params)
155 # Utility functions
157 def sortedItems(dictionary):
158 "Return a list of (key, value) sorted based on keys"
159 keys = dictionary.keys()
160 keys.sort()
161 for key in keys:
162 yield key, dictionary[key]
164 def openURL(url, queryArgs=None):
165 if queryArgs:
166 url = url + '?' + urllib.urlencode(queryArgs)
167 LOG.debug("URL> %s", url)
168 return urllib.urlopen(url)
170 class dottedDict(object):
171 "Make dictionary items accessible via the object-dot notation."
173 def __init__(self, name, dictionary):
174 self._name = name
176 if type(dictionary) is dict:
177 for key, value in dictionary.items():
178 if type(value) is dict:
179 value = dottedDict(key, value)
180 elif type(value) in (list, tuple):
181 value = [dottedDict('%s_%d' % (key, i), item)
182 for i, item in indexed(value)]
183 setattr(self, key, value)
185 def __repr__(self):
186 children = [c for c in dir(self) if not c.startswith('_')]
187 return 'dotted <%s> : %s' % (
188 self._name,
189 ', '.join(children))
192 def safeEval(string):
193 return eval(string, {}, {})
195 def dottedJSON(json):
196 return dottedDict('ROOT', safeEval(json))
198 def indexed(seq):
199 index = 0
200 for item in seq:
201 yield index, item
202 index += 1
205 # API spec
207 API = {
208 'auth': {
209 'checkToken':
210 [('auth_token'), ()],
211 'getFrob':
212 [(), ()],
213 'getToken':
214 [('frob'), ()]
216 'contacts': {
217 'add':
218 [('timeline', 'contact'), ()],
219 'delete':
220 [('timeline', 'contact_id'), ()],
221 'getList':
222 [(), ()]
224 'groups': {
225 'add':
226 [('timeline', 'group'), ()],
227 'addContact':
228 [('timeline', 'group_id', 'contact_id'), ()],
229 'delete':
230 [('timeline', 'group_id'), ()],
231 'getList':
232 [(), ()],
233 'removeContact':
234 [('timeline', 'group_id', 'contact_id'), ()],
236 'lists': {
237 'add':
238 [('timeline', 'name'), ('filter'), ()],
239 'archive':
240 [('timeline', 'list_id'), ()],
241 'delete':
242 [('timeline', 'list_id'), ()],
243 'getList':
244 [(), ()],
245 'setDefaultList':
246 [('timeline'), ('list_id'), ()],
247 'setName':
248 [('timeline', 'list_id', 'name'), ()],
249 'unarchive':
250 [('timeline'), ('list_id'), ()],
252 'locations': {
253 'getList':
254 [(), ()]
256 'reflection': {
257 'getMethodInfo':
258 [('methodName',), ()],
259 'getMethods':
260 [(), ()]
262 'settings': {
263 'getList':
264 [(), ()]
266 'tasks': {
267 'add':
268 [('timeline', 'name',), ('list_id', 'parse',)],
269 'addTags':
270 [('timeline', 'list_id', 'taskseries_id', 'task_id', 'tags'),
271 ()],
272 'complete':
273 [('timeline', 'list_id', 'taskseries_id', 'task_id',), ()],
274 'delete':
275 [('timeline', 'list_id', 'taskseries_id', 'task_id'), ()],
276 'getList':
277 [(),
278 ('list_id', 'filter', 'last_sync')],
279 'movePriority':
280 [('timeline', 'list_id', 'taskseries_id', 'task_id', 'direction'),
281 ()],
282 'moveTo':
283 [('timeline', 'from_list_id', 'to_list_id', 'taskseries_id', 'task_id'),
284 ()],
285 'postpone':
286 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
287 ()],
288 'removeTags':
289 [('timeline', 'list_id', 'taskseries_id', 'task_id', 'tags'),
290 ()],
291 'setDueDate':
292 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
293 ('due', 'has_due_time', 'parse')],
294 'setEstimate':
295 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
296 ('estimate',)],
297 'setLocation':
298 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
299 ('location_id',)],
300 'setName':
301 [('timeline', 'list_id', 'taskseries_id', 'task_id', 'name'),
302 ()],
303 'setPriority':
304 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
305 ('priority',)],
306 'setRecurrence':
307 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
308 ('repeat',)],
309 'setTags':
310 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
311 ('tags',)],
312 'setURL':
313 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
314 ('url',)],
315 'uncomplete':
316 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
317 ()],
319 'tasksNotes': {
320 'add':
321 [('timeline', 'list_id', 'taskseries_id', 'task_id', 'note_title', 'note_text'), ()],
322 'delete':
323 [('timeline', 'note_id'), ()],
324 'edit':
325 [('timeline', 'note_id', 'note_title', 'note_text'), ()]
327 'test': {
328 'echo':
329 [(), ()],
330 'login':
331 [(), ()]
333 'time': {
334 'convert':
335 [('to_timezone',), ('from_timezone', 'to_timezone', 'time')],
336 'parse':
337 [('text',), ('timezone', 'dateformat')]
339 'timelines': {
340 'create':
341 [(), ()]
343 'timezones': {
344 'getList':
345 [(), ()]
347 'transactions': {
348 'undo':
349 [('timeline', 'transaction_id'), ()]
353 def createRTM(apiKey, secret, token=None):
354 rtm = RTM(apiKey, secret, token)
356 if token is None:
357 print 'No token found'
358 print 'Give me access here:', rtm.getAuthURL()
359 raw_input('Press enter once you gave access')
360 print 'Note down this token for future use:', rtm.getToken()
362 return rtm
364 def test(apiKey, secret, token=None):
365 rtm = createRTM(apiKey, secret, token)
367 rspTasks = rtm.tasks.getList(filter='dueWithin:"1 week of today"')
368 print [t.name for t in rspTasks.tasks.list.taskseries]
369 print rspTasks.tasks.list.id
371 rspLists = rtm.lists.getList()
372 # print rspLists.lists.list
373 print [(x.name, x.id) for x in rspLists.lists.list]
375 def set_log_level(level):
376 '''Sets the log level of the logger used by the module.
378 >>> import rtm
379 >>> import logging
380 >>> rtm.set_log_level(logging.INFO)
383 LOG.setLevel(level)