123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397 |
- # Python library for Remember The Milk API
- __author__ = 'Sridhar Ratnakumar <http://nearfar.org/>'
- __all__ = (
- 'API',
- 'createRTM',
- 'set_log_level',
- )
- import new
- import warnings
- import urllib
- import logging
- from md5 import md5
- warnings.simplefilter('default', ImportWarning)
- _use_simplejson = False
- try:
- import simplejson
- _use_simplejson = True
- except ImportError:
- pass
- if not _use_simplejson:
- warnings.warn("simplejson module is not available, "
- "falling back to the internal JSON parser. "
- "Please consider installing the simplejson module from "
- "http://pypi.python.org/pypi/simplejson.", ImportWarning,
- stacklevel=2)
- logging.basicConfig()
- LOG = logging.getLogger(__name__)
- LOG.setLevel(logging.INFO)
- SERVICE_URL = 'http://api.rememberthemilk.com/services/rest/'
- AUTH_SERVICE_URL = 'http://www.rememberthemilk.com/services/auth/'
- class RTMError(Exception): pass
- class RTMAPIError(RTMError): pass
- class AuthStateMachine(object):
- class NoData(RTMError): pass
- def __init__(self, states):
- self.states = states
- self.data = {}
- def dataReceived(self, state, datum):
- if state not in self.states:
- raise RTMError, "Invalid state <%s>" % state
- self.data[state] = datum
- def get(self, state):
- if state in self.data:
- return self.data[state]
- else:
- raise AuthStateMachine.NoData, 'No data for <%s>' % state
- class RTM(object):
- def __init__(self, apiKey, secret, token=None):
- self.apiKey = apiKey
- self.secret = secret
- self.authInfo = AuthStateMachine(['frob', 'token'])
- # this enables one to do 'rtm.tasks.getList()', for example
- for prefix, methods in API.items():
- setattr(self, prefix,
- RTMAPICategory(self, prefix, methods))
- if token:
- self.authInfo.dataReceived('token', token)
- def _sign(self, params):
- "Sign the parameters with MD5 hash"
- pairs = ''.join(['%s%s' % (k,v) for k,v in sortedItems(params)])
- return md5(self.secret+pairs).hexdigest()
- def get(self, **params):
- "Get the XML response for the passed `params`."
- params['api_key'] = self.apiKey
- params['format'] = 'json'
- params['api_sig'] = self._sign(params)
- json = openURL(SERVICE_URL, params).read()
- LOG.debug("JSON response: \n%s" % json)
- if _use_simplejson:
- data = dottedDict('ROOT', simplejson.loads(json))
- else:
- data = dottedJSON(json)
- rsp = data.rsp
- if rsp.stat == 'fail':
- raise RTMAPIError, 'API call failed - %s (%s)' % (
- rsp.err.msg, rsp.err.code)
- else:
- return rsp
- def getNewFrob(self):
- rsp = self.get(method='rtm.auth.getFrob')
- self.authInfo.dataReceived('frob', rsp.frob)
- return rsp.frob
- def getAuthURL(self):
- try:
- frob = self.authInfo.get('frob')
- except AuthStateMachine.NoData:
- frob = self.getNewFrob()
- params = {
- 'api_key': self.apiKey,
- 'perms' : 'delete',
- 'frob' : frob
- }
- params['api_sig'] = self._sign(params)
- return AUTH_SERVICE_URL + '?' + urllib.urlencode(params)
- def getToken(self):
- frob = self.authInfo.get('frob')
- rsp = self.get(method='rtm.auth.getToken', frob=frob)
- self.authInfo.dataReceived('token', rsp.auth.token)
- return rsp.auth.token
- class RTMAPICategory:
- "See the `API` structure and `RTM.__init__`"
- def __init__(self, rtm, prefix, methods):
- self.rtm = rtm
- self.prefix = prefix
- self.methods = methods
- def __getattr__(self, attr):
- if attr in self.methods:
- rargs, oargs = self.methods[attr]
- if self.prefix == 'tasksNotes':
- aname = 'rtm.tasks.notes.%s' % attr
- else:
- aname = 'rtm.%s.%s' % (self.prefix, attr)
- return lambda **params: self.callMethod(
- aname, rargs, oargs, **params)
- else:
- raise AttributeError, 'No such attribute: %s' % attr
- def callMethod(self, aname, rargs, oargs, **params):
- # Sanity checks
- for requiredArg in rargs:
- if requiredArg not in params:
- raise TypeError, 'Required parameter (%s) missing' % requiredArg
- for param in params:
- if param not in rargs + oargs:
- warnings.warn('Invalid parameter (%s)' % param)
- return self.rtm.get(method=aname,
- auth_token=self.rtm.authInfo.get('token'),
- **params)
- # Utility functions
- def sortedItems(dictionary):
- "Return a list of (key, value) sorted based on keys"
- keys = dictionary.keys()
- keys.sort()
- for key in keys:
- yield key, dictionary[key]
- def openURL(url, queryArgs=None):
- if queryArgs:
- url = url + '?' + urllib.urlencode(queryArgs)
- LOG.debug("URL> %s", url)
- return urllib.urlopen(url)
- class dottedDict(object):
- "Make dictionary items accessible via the object-dot notation."
- def __init__(self, name, dictionary):
- self._name = name
- if type(dictionary) is dict:
- for key, value in dictionary.items():
- if type(value) is dict:
- value = dottedDict(key, value)
- elif type(value) in (list, tuple) and key != 'tag':
- value = [dottedDict('%s_%d' % (key, i), item)
- for i, item in indexed(value)]
- setattr(self, key, value)
- def __repr__(self):
- children = [c for c in dir(self) if not c.startswith('_')]
- return 'dotted <%s> : %s' % (
- self._name,
- ', '.join(children))
- def safeEval(string):
- return eval(string, {}, {})
- def dottedJSON(json):
- return dottedDict('ROOT', safeEval(json))
- def indexed(seq):
- index = 0
- for item in seq:
- yield index, item
- index += 1
- # API spec
- API = {
- 'auth': {
- 'checkToken':
- [('auth_token'), ()],
- 'getFrob':
- [(), ()],
- 'getToken':
- [('frob'), ()]
- },
- 'contacts': {
- 'add':
- [('timeline', 'contact'), ()],
- 'delete':
- [('timeline', 'contact_id'), ()],
- 'getList':
- [(), ()]
- },
- 'groups': {
- 'add':
- [('timeline', 'group'), ()],
- 'addContact':
- [('timeline', 'group_id', 'contact_id'), ()],
- 'delete':
- [('timeline', 'group_id'), ()],
- 'getList':
- [(), ()],
- 'removeContact':
- [('timeline', 'group_id', 'contact_id'), ()],
- },
- 'lists': {
- 'add':
- [('timeline', 'name'), ('filter'), ()],
- 'archive':
- [('timeline', 'list_id'), ()],
- 'delete':
- [('timeline', 'list_id'), ()],
- 'getList':
- [(), ()],
- 'setDefaultList':
- [('timeline'), ('list_id'), ()],
- 'setName':
- [('timeline', 'list_id', 'name'), ()],
- 'unarchive':
- [('timeline'), ('list_id'), ()],
- },
- 'locations': {
- 'getList':
- [(), ()]
- },
- 'reflection': {
- 'getMethodInfo':
- [('methodName',), ()],
- 'getMethods':
- [(), ()]
- },
- 'settings': {
- 'getList':
- [(), ()]
- },
- 'tasks': {
- 'add':
- [('timeline', 'name',), ('list_id', 'parse',)],
- 'addTags':
- [('timeline', 'list_id', 'taskseries_id', 'task_id', 'tags'),
- ()],
- 'complete':
- [('timeline', 'list_id', 'taskseries_id', 'task_id',), ()],
- 'delete':
- [('timeline', 'list_id', 'taskseries_id', 'task_id'), ()],
- 'getList':
- [(),
- ('list_id', 'filter', 'last_sync')],
- 'movePriority':
- [('timeline', 'list_id', 'taskseries_id', 'task_id', 'direction'),
- ()],
- 'moveTo':
- [('timeline', 'from_list_id', 'to_list_id', 'taskseries_id', 'task_id'),
- ()],
- 'postpone':
- [('timeline', 'list_id', 'taskseries_id', 'task_id'),
- ()],
- 'removeTags':
- [('timeline', 'list_id', 'taskseries_id', 'task_id', 'tags'),
- ()],
- 'setDueDate':
- [('timeline', 'list_id', 'taskseries_id', 'task_id'),
- ('due', 'has_due_time', 'parse')],
- 'setEstimate':
- [('timeline', 'list_id', 'taskseries_id', 'task_id'),
- ('estimate',)],
- 'setLocation':
- [('timeline', 'list_id', 'taskseries_id', 'task_id'),
- ('location_id',)],
- 'setName':
- [('timeline', 'list_id', 'taskseries_id', 'task_id', 'name'),
- ()],
- 'setPriority':
- [('timeline', 'list_id', 'taskseries_id', 'task_id'),
- ('priority',)],
- 'setRecurrence':
- [('timeline', 'list_id', 'taskseries_id', 'task_id'),
- ('repeat',)],
- 'setTags':
- [('timeline', 'list_id', 'taskseries_id', 'task_id'),
- ('tags',)],
- 'setURL':
- [('timeline', 'list_id', 'taskseries_id', 'task_id'),
- ('url',)],
- 'uncomplete':
- [('timeline', 'list_id', 'taskseries_id', 'task_id'),
- ()],
- },
- 'tasksNotes': {
- 'add':
- [('timeline', 'list_id', 'taskseries_id', 'task_id', 'note_title', 'note_text'), ()],
- 'delete':
- [('timeline', 'note_id'), ()],
- 'edit':
- [('timeline', 'note_id', 'note_title', 'note_text'), ()]
- },
- 'test': {
- 'echo':
- [(), ()],
- 'login':
- [(), ()]
- },
- 'time': {
- 'convert':
- [('to_timezone',), ('from_timezone', 'to_timezone', 'time')],
- 'parse':
- [('text',), ('timezone', 'dateformat')]
- },
- 'timelines': {
- 'create':
- [(), ()]
- },
- 'timezones': {
- 'getList':
- [(), ()]
- },
- 'transactions': {
- 'undo':
- [('timeline', 'transaction_id'), ()]
- },
- }
- def createRTM(apiKey, secret, token=None):
- rtm = RTM(apiKey, secret, token)
- if token is None:
- print 'No token found'
- print 'Give me access here:', rtm.getAuthURL()
- raw_input('Press enter once you gave access')
- print 'Note down this token for future use:', rtm.getToken()
- return rtm
- def test(apiKey, secret, token=None):
- rtm = createRTM(apiKey, secret, token)
- rspTasks = rtm.tasks.getList(filter='dueWithin:"1 week of today"')
- print [t.name for t in rspTasks.tasks.list.taskseries]
- print rspTasks.tasks.list.id
- rspLists = rtm.lists.getList()
- # print rspLists.lists.list
- print [(x.name, x.id) for x in rspLists.lists.list]
- def set_log_level(level):
- '''Sets the log level of the logger used by the module.
-
- >>> import rtm
- >>> import logging
- >>> rtm.set_log_level(logging.INFO)
- '''
-
- LOG.setLevel(level)
|