1 # Python library for Remember The Milk API
3 __author__
= 'Sridhar Ratnakumar <http://nearfar.org/>'
17 warnings
.simplefilter('default', ImportWarning
)
19 _use_simplejson
= False
22 _use_simplejson
= True
26 if not _use_simplejson
:
27 warnings
.warn("simplejson module is not available, "
28 "falling back to the internal JSON parser. "
29 "Please consider installing the simplejson module from "
30 "http://pypi.python.org/pypi/simplejson.", ImportWarning
,
34 LOG
= logging
.getLogger(__name__
)
35 LOG
.setLevel(logging
.INFO
)
37 SERVICE_URL
= 'http://api.rememberthemilk.com/services/rest/'
38 AUTH_SERVICE_URL
= 'http://www.rememberthemilk.com/services/auth/'
41 class RTMError(Exception): pass
43 class RTMAPIError(RTMError
): pass
45 class AuthStateMachine(object):
47 class NoData(RTMError
): pass
49 def __init__(self
, states
):
53 def dataReceived(self
, state
, datum
):
54 if state
not in self
.states
:
55 raise RTMError
, "Invalid state <%s>" % state
56 self
.data
[state
] = datum
59 if state
in self
.data
:
60 return self
.data
[state
]
62 raise AuthStateMachine
.NoData
, 'No data for <%s>' % state
67 def __init__(self
, apiKey
, secret
, token
=None):
70 self
.authInfo
= AuthStateMachine(['frob', 'token'])
72 # this enables one to do 'rtm.tasks.getList()', for example
73 for prefix
, methods
in API
.items():
75 RTMAPICategory(self
, prefix
, methods
))
78 self
.authInfo
.dataReceived('token', token
)
80 def _sign(self
, params
):
81 "Sign the parameters with MD5 hash"
82 pairs
= ''.join(['%s%s' % (k
,v
) for k
,v
in sortedItems(params
)])
83 return md5(self
.secret
+pairs
).hexdigest()
85 def get(self
, **params
):
86 "Get the XML response for the passed `params`."
87 params
['api_key'] = self
.apiKey
88 params
['format'] = 'json'
89 params
['api_sig'] = self
._sign
(params
)
91 json
= openURL(SERVICE_URL
, params
).read()
93 LOG
.debug("JSON response: \n%s" % json
)
96 data
= dottedDict('ROOT', simplejson
.loads(json
))
98 data
= dottedJSON(json
)
101 if rsp
.stat
== 'fail':
102 raise RTMAPIError
, 'API call failed - %s (%s)' % (
103 rsp
.err
.msg
, rsp
.err
.code
)
107 def getNewFrob(self
):
108 rsp
= self
.get(method
='rtm.auth.getFrob')
109 self
.authInfo
.dataReceived('frob', rsp
.frob
)
112 def getAuthURL(self
):
114 frob
= self
.authInfo
.get('frob')
115 except AuthStateMachine
.NoData
:
116 frob
= self
.getNewFrob()
119 'api_key': self
.apiKey
,
123 params
['api_sig'] = self
._sign
(params
)
124 return AUTH_SERVICE_URL
+ '?' + urllib
.urlencode(params
)
127 frob
= self
.authInfo
.get('frob')
128 rsp
= self
.get(method
='rtm.auth.getToken', frob
=frob
)
129 self
.authInfo
.dataReceived('token', rsp
.auth
.token
)
130 return rsp
.auth
.token
132 class RTMAPICategory
:
133 "See the `API` structure and `RTM.__init__`"
135 def __init__(self
, rtm
, prefix
, methods
):
138 self
.methods
= methods
140 def __getattr__(self
, attr
):
141 if attr
in self
.methods
:
142 rargs
, oargs
= self
.methods
[attr
]
143 if self
.prefix
== 'tasksNotes':
144 aname
= 'rtm.tasks.notes.%s' % attr
146 aname
= 'rtm.%s.%s' % (self
.prefix
, attr
)
147 return lambda **params
: self
.callMethod(
148 aname
, rargs
, oargs
, **params
)
150 raise AttributeError, 'No such attribute: %s' % attr
152 def callMethod(self
, aname
, rargs
, oargs
, **params
):
154 for requiredArg
in rargs
:
155 if requiredArg
not in params
:
156 raise TypeError, 'Required parameter (%s) missing' % requiredArg
159 if param
not in rargs
+ oargs
:
160 warnings
.warn('Invalid parameter (%s)' % param
)
162 return self
.rtm
.get(method
=aname
,
163 auth_token
=self
.rtm
.authInfo
.get('token'),
170 def sortedItems(dictionary
):
171 "Return a list of (key, value) sorted based on keys"
172 keys
= dictionary
.keys()
175 yield key
, dictionary
[key
]
177 def openURL(url
, queryArgs
=None):
179 url
= url
+ '?' + urllib
.urlencode(queryArgs
)
180 LOG
.debug("URL> %s", url
)
181 return urllib
.urlopen(url
)
183 class dottedDict(object):
184 "Make dictionary items accessible via the object-dot notation."
186 def __init__(self
, name
, dictionary
):
189 if type(dictionary
) is dict:
190 for key
, value
in dictionary
.items():
191 if type(value
) is dict:
192 value
= dottedDict(key
, value
)
193 elif type(value
) in (list, tuple) and key
!= 'tag':
194 value
= [dottedDict('%s_%d' % (key
, i
), item
)
195 for i
, item
in indexed(value
)]
196 setattr(self
, key
, value
)
199 children
= [c
for c
in dir(self
) if not c
.startswith('_')]
200 return 'dotted <%s> : %s' % (
205 def safeEval(string
):
206 return eval(string
, {}, {})
208 def dottedJSON(json
):
209 return dottedDict('ROOT', safeEval(json
))
223 [('auth_token'), ()],
231 [('timeline', 'contact'), ()],
233 [('timeline', 'contact_id'), ()],
239 [('timeline', 'group'), ()],
241 [('timeline', 'group_id', 'contact_id'), ()],
243 [('timeline', 'group_id'), ()],
247 [('timeline', 'group_id', 'contact_id'), ()],
251 [('timeline', 'name'), ('filter'), ()],
253 [('timeline', 'list_id'), ()],
255 [('timeline', 'list_id'), ()],
259 [('timeline'), ('list_id'), ()],
261 [('timeline', 'list_id', 'name'), ()],
263 [('timeline'), ('list_id'), ()],
271 [('methodName',), ()],
281 [('timeline', 'name',), ('list_id', 'parse',)],
283 [('timeline', 'list_id', 'taskseries_id', 'task_id', 'tags'),
286 [('timeline', 'list_id', 'taskseries_id', 'task_id',), ()],
288 [('timeline', 'list_id', 'taskseries_id', 'task_id'), ()],
291 ('list_id', 'filter', 'last_sync')],
293 [('timeline', 'list_id', 'taskseries_id', 'task_id', 'direction'),
296 [('timeline', 'from_list_id', 'to_list_id', 'taskseries_id', 'task_id'),
299 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
302 [('timeline', 'list_id', 'taskseries_id', 'task_id', 'tags'),
305 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
306 ('due', 'has_due_time', 'parse')],
308 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
311 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
314 [('timeline', 'list_id', 'taskseries_id', 'task_id', 'name'),
317 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
320 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
323 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
326 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
329 [('timeline', 'list_id', 'taskseries_id', 'task_id'),
334 [('timeline', 'list_id', 'taskseries_id', 'task_id', 'note_title', 'note_text'), ()],
336 [('timeline', 'note_id'), ()],
338 [('timeline', 'note_id', 'note_title', 'note_text'), ()]
348 [('to_timezone',), ('from_timezone', 'to_timezone', 'time')],
350 [('text',), ('timezone', 'dateformat')]
362 [('timeline', 'transaction_id'), ()]
366 def createRTM(apiKey
, secret
, token
=None):
367 rtm
= RTM(apiKey
, secret
, token
)
370 print 'No token found'
371 print 'Give me access here:', rtm
.getAuthURL()
372 raw_input('Press enter once you gave access')
373 print 'Note down this token for future use:', rtm
.getToken()
377 def test(apiKey
, secret
, token
=None):
378 rtm
= createRTM(apiKey
, secret
, token
)
380 rspTasks
= rtm
.tasks
.getList(filter='dueWithin:"1 week of today"')
381 print [t
.name
for t
in rspTasks
.tasks
.list.taskseries
]
382 print rspTasks
.tasks
.list.id
384 rspLists
= rtm
.lists
.getList()
385 # print rspLists.lists.list
386 print [(x
.name
, x
.id) for x
in rspLists
.lists
.list]
388 def set_log_level(level
):
389 '''Sets the log level of the logger used by the module.
393 >>> rtm.set_log_level(logging.INFO)