1
# Python library for Remember The Milk API
3
__author__ = 'Sridhar Ratnakumar <http://nearfar.org/>'
14
from hashlib import md5
17
warnings.simplefilter('default', ImportWarning)
19
_use_simplejson = False
22
_use_simplejson = True
25
from django.utils import simplejson
26
_use_simplejson = True
30
if not _use_simplejson:
31
warnings.warn("simplejson module is not available, "
32
"falling back to the internal JSON parser. "
33
"Please consider installing the simplejson module from "
34
"http://pypi.python.org/pypi/simplejson.", ImportWarning,
37
#logging.basicConfig()
38
#LOG = logging.getLogger(__name__)
39
#LOG.setLevel(logging.INFO)
41
SERVICE_URL = 'http://api.rememberthemilk.com/services/rest/'
42
AUTH_SERVICE_URL = 'http://www.rememberthemilk.com/services/auth/'
45
class RTMError(Exception): pass
47
class RTMAPIError(RTMError): pass
49
class AuthStateMachine(object):
51
class NoData(RTMError): pass
53
def __init__(self, states):
57
def dataReceived(self, state, datum):
58
if state not in self.states:
59
error_string = _("Invalid state")+" <%s>"
61
raise RTMError, error_string % state
62
self.data[state] = datum
65
if state in self.data:
66
return self.data[state]
68
raise AuthStateMachine.NoData, 'No data for <%s>' % state
73
def __init__(self, apiKey, secret, token=None):
76
self.authInfo = AuthStateMachine(['frob', 'token'])
78
# this enables one to do 'rtm.tasks.getList()', for example
79
for prefix, methods in API.items():
81
RTMAPICategory(self, prefix, methods))
84
self.authInfo.dataReceived('token', token)
86
def _sign(self, params):
87
"Sign the parameters with MD5 hash"
88
pairs = ''.join(['%s%s' % (k,v) for k,v in sortedItems(params)])
89
return md5(self.secret+pairs).hexdigest()
91
def get(self, **params):
92
"Get the XML response for the passed `params`."
93
params['api_key'] = self.apiKey
94
params['format'] = 'json'
95
params['api_sig'] = self._sign(params)
97
json = openURL(SERVICE_URL, params).read()
99
#LOG.debug("JSON response: \n%s" % json)
101
data = dottedDict('ROOT', simplejson.loads(json))
103
data = dottedJSON(json)
106
if rsp.stat == 'fail':
107
raise RTMAPIError, 'API call failed - %s (%s)' % (
108
rsp.err.msg, rsp.err.code)
112
def getNewFrob(self):
113
rsp = self.get(method='rtm.auth.getFrob')
114
self.authInfo.dataReceived('frob', rsp.frob)
117
def getAuthURL(self):
119
frob = self.authInfo.get('frob')
120
except AuthStateMachine.NoData:
121
frob = self.getNewFrob()
124
'api_key': self.apiKey,
128
params['api_sig'] = self._sign(params)
129
return AUTH_SERVICE_URL + '?' + urllib.urlencode(params)
132
frob = self.authInfo.get('frob')
133
rsp = self.get(method='rtm.auth.getToken', frob=frob)
134
self.authInfo.dataReceived('token', rsp.auth.token)
135
return rsp.auth.token
137
class RTMAPICategory:
138
"See the `API` structure and `RTM.__init__`"
140
def __init__(self, rtm, prefix, methods):
143
self.methods = methods
145
def __getattr__(self, attr):
146
if attr in self.methods:
147
rargs, oargs = self.methods[attr]
148
if self.prefix == 'tasksNotes':
149
aname = 'rtm.tasks.notes.%s' % attr
151
aname = 'rtm.%s.%s' % (self.prefix, attr)
152
return lambda **params: self.callMethod(
153
aname, rargs, oargs, **params)
155
raise AttributeError, 'No such attribute: %s' % attr
157
def callMethod(self, aname, rargs, oargs, **params):
159
for requiredArg in rargs:
160
if requiredArg not in params:
161
raise TypeError, 'Required parameter (%s) missing' % requiredArg
164
if param not in rargs + oargs:
165
warnings.warn('Invalid parameter (%s)' % param)
167
return self.rtm.get(method=aname,
168
auth_token=self.rtm.authInfo.get('token'),
175
def sortedItems(dictionary):
176
"Return a list of (key, value) sorted based on keys"
177
keys = dictionary.keys()
180
yield key, dictionary[key]
182
def openURL(url, queryArgs=None):
184
url = url + '?' + urllib.urlencode(queryArgs)
185
#LOG.debug("URL> %s", url)
186
return urllib.urlopen(url)
188
class dottedDict(object):
189
"""Make dictionary items accessible via the object-dot notation."""
191
def __init__(self, name, dictionary):
194
if type(dictionary) is dict:
195
for key, value in dictionary.items():
196
if type(value) is dict:
197
value = dottedDict(key, value)
198
elif type(value) in (list, tuple) and key != 'tag':
199
value = [dottedDict('%s_%d' % (key, i), item)
200
for i, item in indexed(value)]
201
setattr(self, key, value)
203
raise ValueError, 'not a dict: %s' % dictionary
206
children = [c for c in dir(self) if not c.startswith('_')]
207
return 'dotted <%s> : %s' % (
212
def safeEval(string):
213
return eval(string, {}, {})
215
def dottedJSON(json):
216
return dottedDict('ROOT', safeEval(json))
230
[('auth_token',), ()],
238
[('timeline', 'contact'), ()],
240
[('timeline', 'contact_id'), ()],
246
[('timeline', 'group'), ()],
248
[('timeline', 'group_id', 'contact_id'), ()],
250
[('timeline', 'group_id'), ()],
254
[('timeline', 'group_id', 'contact_id'), ()],
258
[('timeline', 'name',), ('filter',)],
260
[('timeline', 'list_id'), ()],
262
[('timeline', 'list_id'), ()],
266
[('timeline'), ('list_id')],
268
[('timeline', 'list_id', 'name'), ()],
270
[('timeline',), ('list_id',)]
278
[('methodName',), ()],
288
[('timeline', 'name',), ('list_id', 'parse',)],
290
[('timeline', 'list_id', 'taskseries_id', 'task_id', 'tags'),
293
[('timeline', 'list_id', 'taskseries_id', 'task_id',), ()],
295
[('timeline', 'list_id', 'taskseries_id', 'task_id'), ()],
298
('list_id', 'filter', 'last_sync')],
300
[('timeline', 'list_id', 'taskseries_id', 'task_id', 'direction'),
303
[('timeline', 'from_list_id', 'to_list_id', 'taskseries_id', 'task_id'),
306
[('timeline', 'list_id', 'taskseries_id', 'task_id'),
309
[('timeline', 'list_id', 'taskseries_id', 'task_id', 'tags'),
312
[('timeline', 'list_id', 'taskseries_id', 'task_id'),
313
('due', 'has_due_time', 'parse')],
315
[('timeline', 'list_id', 'taskseries_id', 'task_id'),
318
[('timeline', 'list_id', 'taskseries_id', 'task_id'),
321
[('timeline', 'list_id', 'taskseries_id', 'task_id', 'name'),
324
[('timeline', 'list_id', 'taskseries_id', 'task_id'),
327
[('timeline', 'list_id', 'taskseries_id', 'task_id'),
330
[('timeline', 'list_id', 'taskseries_id', 'task_id'),
333
[('timeline', 'list_id', 'taskseries_id', 'task_id'),
336
[('timeline', 'list_id', 'taskseries_id', 'task_id'),
341
[('timeline', 'list_id', 'taskseries_id', 'task_id', 'note_title', 'note_text'), ()],
343
[('timeline', 'note_id'), ()],
345
[('timeline', 'note_id', 'note_title', 'note_text'), ()]
355
[('to_timezone',), ('from_timezone', 'to_timezone', 'time')],
357
[('text',), ('timezone', 'dateformat')]
369
[('timeline', 'transaction_id'), ()]
373
def createRTM(apiKey, secret, token=None):
374
rtm = RTM(apiKey, secret, token)
376
# print 'No token found'
377
# print 'Give me access here:', rtm.getAuthURL()
378
# raw_input('Press enter once you gave access')
379
# print 'Note down this token for future use:', rtm.getToken()
383
def test(apiKey, secret, token=None):
384
rtm = createRTM(apiKey, secret, token)
386
rspTasks = rtm.tasks.getList(filter='dueWithin:"1 week of today"')
387
print [t.name for t in rspTasks.tasks.list.taskseries]
388
print rspTasks.tasks.list.id
390
rspLists = rtm.lists.getList()
391
# print rspLists.lists.list
392
print [(x.name, x.id) for x in rspLists.lists.list]
394
def set_log_level(level):
395
'''Sets the log level of the logger used by the module.
399
>>> rtm.set_log_level(logging.INFO)