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 aname
= 'rtm.%s.%s' % (self
.prefix
, attr
)
134 return lambda **params
: self
.callMethod(
135 aname
, rargs
, oargs
, **params
)
137 raise AttributeError, 'No such attribute: %s' % attr
139 def callMethod(self
, aname
, rargs
, oargs
, **params
):
141 for requiredArg
in rargs
:
142 if requiredArg
not in params
:
143 raise TypeError, 'Required parameter (%s) missing' % requiredArg
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'),
157 def sortedItems(dictionary
):
158 "Return a list of (key, value) sorted based on keys"
159 keys
= dictionary
.keys()
162 yield key
, dictionary
[key
]
164 def openURL(url
, queryArgs
=None):
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
):
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
)
186 children
= [c
for c
in dir(self
) if not c
.startswith('_')]
187 return 'dotted <%s> : %s' % (
192 def safeEval(string
):
193 return eval(string
, {}, {})
195 def dottedJSON(json
):
196 return dottedDict('ROOT', safeEval(json
))
210 [('auth_token'), ()],
218 [('timeline', 'contact'), ()],
220 [('timeline', 'contact_id'), ()],
226 [('timeline', 'group'), ()],
228 [('timeline', 'group_id', 'contact_id'), ()],
230 [('timeline', 'group_id'), ()],
234 [('timeline', 'group_id', 'contact_id'), ()],
238 [('timeline', 'name'), ('filter'), ()],
240 [('timeline', 'list_id'), ()],
242 [('timeline', 'list_id'), ()],
246 [('timeline'), ('list_id'), ()],
248 [('timeline', 'list_id', 'name'), ()],
250 [('timeline'), ('list_id'), ()],
258 [('methodName',), ()],
268 [('timeline', 'name',), ('list_id', 'parse',)],
270 [('timeline', 'list_id', 'taskseries_id', 'task_id', 'tags'),
273 [('timeline', 'list_id', 'taskseries_id', 'task_id',), ()],
275 [('timeline', 'list_id', 'taskseries_id', 'task_id'), ()],
278 ('list_id', 'filter', 'last_sync')],
280 [('timeline', 'list_id', 'taskseries_id', 'task_id', 'direction'),
283 [('timeline', 'from_list_id', 'to_list_id', 'taskseries_id', 'task_id'),
286 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
289 [('timeline', 'list_id', 'taskseries_id', 'task_id', 'tags'),
292 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
293 ('due', 'has_due_time', 'parse')],
295 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
298 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
301 [('timeline', 'list_id', 'taskseries_id', 'task_id', 'name'),
304 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
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'),
321 [('timeline', 'list_id', 'taskseries_id', 'task_id', 'note_title', 'note_text'), ()],
323 [('timeline', 'note_id'), ()],
325 [('timeline', 'note_id', 'note_title', 'note_text'), ()]
335 [('to_timezone',), ('from_timezone', 'to_timezone', 'time')],
337 [('text',), ('timezone', 'dateformat')]
349 [('timeline', 'transaction_id'), ()]
353 def createRTM(apiKey
, secret
, token
=None):
354 rtm
= RTM(apiKey
, secret
, token
)
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()
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.
380 >>> rtm.set_log_level(logging.INFO)