rtm.py 11 KB

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