1 # Python library for Remember The Milk API
3 __author__
= 'Sridhar Ratnakumar <http://nearfar.org/>'
16 _use_simplejson
= False
19 _use_simplejson
= True
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
):
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
49 if state
in self
.data
:
50 return self
.data
[state
]
52 raise AuthStateMachine
.NoData
, 'No data for <%s>' % state
57 def __init__(self
, apiKey
, secret
, token
=None):
60 self
.authInfo
= AuthStateMachine(['frob', 'token'])
62 # this enables one to do 'rtm.tasks.getList()', for example
63 for prefix
, methods
in API
.items():
65 RTMAPICategory(self
, prefix
, methods
))
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
)
86 data
= dottedDict('ROOT', simplejson
.loads(json
))
88 data
= dottedJSON(json
)
91 if rsp
.stat
== 'fail':
92 raise RTMAPIError
, 'API call failed - %s (%s)' % (
93 rsp
.err
.msg
, rsp
.err
.code
)
98 rsp
= self
.get(method
='rtm.auth.getFrob')
99 self
.authInfo
.dataReceived('frob', rsp
.frob
)
102 def getAuthURL(self
):
104 frob
= self
.authInfo
.get('frob')
105 except AuthStateMachine
.NoData
:
106 frob
= self
.getNewFrob()
109 'api_key': self
.apiKey
,
113 params
['api_sig'] = self
._sign
(params
)
114 return AUTH_SERVICE_URL
+ '?' + urllib
.urlencode(params
)
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
):
128 self
.methods
= methods
130 def __getattr__(self
, attr
):
131 if attr
in self
.methods
:
132 rargs
, oargs
= self
.methods
[attr
]
133 if self
.prefix
== 'tasksNotes':
134 aname
= 'rtm.tasks.notes.%s' % attr
136 aname
= 'rtm.%s.%s' % (self
.prefix
, attr
)
137 return lambda **params
: self
.callMethod(
138 aname
, rargs
, oargs
, **params
)
140 raise AttributeError, 'No such attribute: %s' % attr
142 def callMethod(self
, aname
, rargs
, oargs
, **params
):
144 for requiredArg
in rargs
:
145 if requiredArg
not in params
:
146 raise TypeError, 'Required parameter (%s) missing' % requiredArg
149 if param
not in rargs
+ oargs
:
150 warnings
.warn('Invalid parameter (%s)' % param
)
152 return self
.rtm
.get(method
=aname
,
153 auth_token
=self
.rtm
.authInfo
.get('token'),
160 def sortedItems(dictionary
):
161 "Return a list of (key, value) sorted based on keys"
162 keys
= dictionary
.keys()
165 yield key
, dictionary
[key
]
167 def openURL(url
, queryArgs
=None):
169 url
= url
+ '?' + urllib
.urlencode(queryArgs
)
170 LOG
.debug("URL> %s", url
)
171 return urllib
.urlopen(url
)
173 class dottedDict(object):
174 "Make dictionary items accessible via the object-dot notation."
176 def __init__(self
, name
, dictionary
):
179 if type(dictionary
) is dict:
180 for key
, value
in dictionary
.items():
181 if type(value
) is dict:
182 value
= dottedDict(key
, value
)
183 elif type(value
) in (list, tuple) and key
!= 'tag':
184 value
= [dottedDict('%s_%d' % (key
, i
), item
)
185 for i
, item
in indexed(value
)]
186 setattr(self
, key
, value
)
189 children
= [c
for c
in dir(self
) if not c
.startswith('_')]
190 return 'dotted <%s> : %s' % (
195 def safeEval(string
):
196 return eval(string
, {}, {})
198 def dottedJSON(json
):
199 return dottedDict('ROOT', safeEval(json
))
213 [('auth_token'), ()],
221 [('timeline', 'contact'), ()],
223 [('timeline', 'contact_id'), ()],
229 [('timeline', 'group'), ()],
231 [('timeline', 'group_id', 'contact_id'), ()],
233 [('timeline', 'group_id'), ()],
237 [('timeline', 'group_id', 'contact_id'), ()],
241 [('timeline', 'name'), ('filter'), ()],
243 [('timeline', 'list_id'), ()],
245 [('timeline', 'list_id'), ()],
249 [('timeline'), ('list_id'), ()],
251 [('timeline', 'list_id', 'name'), ()],
253 [('timeline'), ('list_id'), ()],
261 [('methodName',), ()],
271 [('timeline', 'name',), ('list_id', 'parse',)],
273 [('timeline', 'list_id', 'taskseries_id', 'task_id', 'tags'),
276 [('timeline', 'list_id', 'taskseries_id', 'task_id',), ()],
278 [('timeline', 'list_id', 'taskseries_id', 'task_id'), ()],
281 ('list_id', 'filter', 'last_sync')],
283 [('timeline', 'list_id', 'taskseries_id', 'task_id', 'direction'),
286 [('timeline', 'from_list_id', 'to_list_id', 'taskseries_id', 'task_id'),
289 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
292 [('timeline', 'list_id', 'taskseries_id', 'task_id', 'tags'),
295 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
296 ('due', 'has_due_time', 'parse')],
298 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
301 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
304 [('timeline', 'list_id', 'taskseries_id', 'task_id', 'name'),
307 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
310 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
313 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
316 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
319 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
324 [('timeline', 'list_id', 'taskseries_id', 'task_id', 'note_title', 'note_text'), ()],
326 [('timeline', 'note_id'), ()],
328 [('timeline', 'note_id', 'note_title', 'note_text'), ()]
338 [('to_timezone',), ('from_timezone', 'to_timezone', 'time')],
340 [('text',), ('timezone', 'dateformat')]
352 [('timeline', 'transaction_id'), ()]
356 def createRTM(apiKey
, secret
, token
=None):
357 rtm
= RTM(apiKey
, secret
, token
)
360 print 'No token found'
361 print 'Give me access here:', rtm
.getAuthURL()
362 raw_input('Press enter once you gave access')
363 print 'Note down this token for future use:', rtm
.getToken()
367 def test(apiKey
, secret
, token
=None):
368 rtm
= createRTM(apiKey
, secret
, token
)
370 rspTasks
= rtm
.tasks
.getList(filter='dueWithin:"1 week of today"')
371 print [t
.name
for t
in rspTasks
.tasks
.list.taskseries
]
372 print rspTasks
.tasks
.list.id
374 rspLists
= rtm
.lists
.getList()
375 # print rspLists.lists.list
376 print [(x
.name
, x
.id) for x
in rspLists
.lists
.list]
378 def set_log_level(level
):
379 '''Sets the log level of the logger used by the module.
383 >>> rtm.set_log_level(logging.INFO)