conduit r1417 - in trunk: . conduit/modules/RTMModule



Author: jstowers
Date: Fri Apr 11 11:42:21 2008
New Revision: 1417
URL: http://svn.gnome.org/viewvc/conduit?rev=1417&view=rev

Log:
2008-04-11  Kevin Kubasik  <kevin kubasik net>

	* conduit/modules/RTMModule/Makefile.am:
	* conduit/modules/RTMModule/RTMModule.py:
	* conduit/modules/RTMModule/config.glade:
	* conduit/modules/RTMModule/rtm.py: Add preliminary support for remember
	the milk.



Added:
   trunk/conduit/modules/RTMModule/
   trunk/conduit/modules/RTMModule/Makefile.am
   trunk/conduit/modules/RTMModule/RTMModule.py
   trunk/conduit/modules/RTMModule/config.glade
   trunk/conduit/modules/RTMModule/rtm.py
Modified:
   trunk/ChangeLog

Added: trunk/conduit/modules/RTMModule/Makefile.am
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/RTMModule/Makefile.am	Fri Apr 11 11:42:21 2008
@@ -0,0 +1,8 @@
+conduit_handlersdir = $(libdir)/conduit/modules/RTMModule
+conduit_handlers_PYTHON = RTMModule.py
+
+conduit_handlers_DATA = config.glade
+EXTRA_DIST = config.glade
+
+clean-local:
+	rm -rf *.pyc *.pyo

Added: trunk/conduit/modules/RTMModule/RTMModule.py
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/RTMModule/RTMModule.py	Fri Apr 11 11:42:21 2008
@@ -0,0 +1,174 @@
+"""
+RTM Sync.
+"""
+import os, sys
+import traceback
+import md5
+import logging
+log = logging.getLogger("modules.RTM")
+import dateutil
+import urllib, urllib2
+import vobject
+import itertools
+import conduit
+import conduit.utils as Utils
+import conduit.Web as Web
+import conduit.datatypes.Event as Event
+import conduit.Exceptions as Exceptions
+from conduit.datatypes import Rid
+import conduit.dataproviders.DataProvider as DataProvider
+
+from gettext import gettext as _
+Utils.dataprovider_add_dir_to_path(__file__)
+import rtm
+MODULES = {
+    "RTMTasksTwoWay" : {"type":"dataprovider" }
+}
+
+class RTMTasksTwoWay(DataProvider.TwoWay):
+
+    DEFAULT_TASK_URI = "default"
+
+    _name_ = _("Remember The Milk Tasks")
+    _description_ = _("Sync your tasks to Remember The Milk")
+    _category_ = conduit.dataproviders.CATEGORY_OFFICE
+    _module_type_ = "twoway"
+    _in_type_ = "event"
+    _out_type_ = "event"
+    _icon_ = "tomboy"
+    
+    API_KEY = 'fe049e2cec86568f3d79c964d4a45f5c'
+    SECRET='b57757de51f7e919'
+    
+    def __init__(self, *args):
+        DataProvider.TwoWay.__init__(self)
+        self.uids = None
+	self.token = None
+        self.tasks = {}
+	self.ical = None
+	self.username = None
+	self.password = None
+	self.rtm = rtm.RTM(apiKey=self.API_KEY,secret=self.SECRET,token=self.get_configuration().get('token'))
+
+
+    def refresh(self):
+        DataProvider.TwoWay.refresh(self)
+        self.tasks = {}
+        tempt = rtm.get_all_tasks(self.rtm)
+        #self.tasks.sort(key='id')
+        self.ical =open(urllib.urlretrieve('http://%s:%s www rememberthemilk com/icalendar/%s/events/'%(
+	    self.get_configuration().get('username'),
+	    self.get_configuration().get('password'),
+	    self.get_configuration().get('username')))[0]).read()
+	
+        for t in tempt:
+            self.tasks[t.id] = t
+            
+    
+    def get_all(self):
+        return list(self.tasks.iterkeys())
+
+    def get (self, LUID):
+	log.warn("LUID: %s"%LUID)
+	task = None
+        if self.tasks.has_key(LUID):
+            task = self.tasks[LUID]
+        else:
+            self.refresh()
+            if self.tasks.has_key(LUID):
+                task = self.tasks[LUID]
+            else :
+                task = None
+        e = Event.Event()
+        e.set_UID(self.get_UID())
+        e.set_mtime( dateutil.parser.parse(task.modified))
+        e.set_open_URI('http://www.rememberthemilk.com/home/%s/#%s'%(self.username,task.id))
+        res = [sum for sum in vobject.readComponents(self.ical) if sum.vevent.summary.value  == task.name]
+        e.set_from_ical_string(res[0].serialize())
+        return e
+            
+
+    def delete(self, LUID):
+        self.rtm.tasks.delete(task_id=LUID)
+        del self.tasks[LUID]
+    
+    def put(self, putData, overwrite, LUID):
+	raise NotImplementedError("Not done")
+    
+    #----------------------------------------------------------------------
+    def get_UID(self):
+	""""""
+	return "RTM#%s"%(self.get_configuration().get("username"))
+
+    def _login(self):
+        
+	    
+
+        if self.token is None:
+            # get the ticket and open login url
+            #self.token = self.rtm.getToken()
+            url = self.rtm.getAuthURL()
+
+            #wait for log in
+            Web.LoginMagic("Log into RememberTheMilk", url, login_function=self._try_login)
+    def _try_login (self):
+        """
+        Try to perform a login, return None if it does not succeed
+        """
+        try:
+	    
+            self.token = self.rtm.getToken()
+            return self.token
+        except:
+            return None
+
+    
+    def configure(self, window):
+        """
+        Configures the RTM Backend
+        """
+        import gtk
+        import gobject
+        def on_login_finish(*args):
+            Utils.dialog_reset_cursor(dlg)
+            
+        def on_response(sender, responseID):
+            if responseID == gtk.RESPONSE_OK:
+		self.username = str(tree.get_widget('user_entry').get_text())
+		self.password = str(tree.get_widget('pass_entry').get_text())
+                
+        def load_button_clicked(button):
+            Utils.dialog_set_busy_cursor(dlg)
+            conduit.GLOBALS.syncManager.run_blocking_dataprovider_function_calls(
+                                            self,
+                                            on_login_finish,
+                                            self._login)
+
+
+        tree = Utils.dataprovider_glade_get_widget(
+                        __file__,
+                        "config.glade",
+                        "RTMConfigDialog")
+
+        #get a whole bunch of widgets
+        load_button = tree.get_widget("load_button")
+        dlg = tree.get_widget("RTMConfigDialog")
+
+        # load button
+        load_button.connect('clicked', load_button_clicked)
+
+        # run the dialog
+        Utils.run_dialog_non_blocking(dlg, on_response, window)
+
+    def is_configured (self, isSource, isTwoWay):
+        return self.token is not None
+
+    def get_configuration(self):
+        return {
+            "token" : self.token,
+	    "username" : self.username,
+	    "password" : self.password
+            }
+	
+  
+

Added: trunk/conduit/modules/RTMModule/config.glade
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/RTMModule/config.glade	Fri Apr 11 11:42:21 2008
@@ -0,0 +1,131 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE glade-interface SYSTEM "glade-2.0.dtd">
+<!--*- mode: xml -*-->
+<glade-interface>
+  <widget class="GtkDialog" id="RTMConfigDialog">
+    <property name="visible">True</property>
+    <property name="title" translatable="yes">Box.net</property>
+    <property name="resizable">False</property>
+    <property name="default_width">250</property>
+    <property name="default_height">350</property>
+    <property name="type_hint">GDK_WINDOW_TYPE_HINT_DIALOG</property>
+    <child internal-child="vbox">
+      <widget class="GtkVBox" id="vbox30">
+        <property name="visible">True</property>
+        <child>
+          <widget class="GtkLabel" id="folderlabel">
+            <property name="visible">True</property>
+            <property name="xalign">0</property>
+            <property name="xpad">2</property>
+            <property name="ypad">2</property>
+            <property name="label" translatable="yes">Remember The Milk:</property>
+          </widget>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">False</property>
+            <property name="position">2</property>
+          </packing>
+        </child>
+        <child>
+          <widget class="GtkButton" id="load_button">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="label" translatable="yes">Click To Login</property>
+            <property name="use_underline">True</property>
+            <property name="response_id">0</property>
+          </widget>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">False</property>
+            <property name="position">3</property>
+          </packing>
+        </child>
+        <child>
+          <widget class="GtkHBox" id="hbox1">
+            <property name="visible">True</property>
+            <child>
+              <widget class="GtkVBox" id="vbox1">
+                <property name="visible">True</property>
+                <child>
+                  <widget class="GtkLabel" id="label1">
+                    <property name="visible">True</property>
+                    <property name="label" translatable="yes">Username:</property>
+                  </widget>
+                </child>
+                <child>
+                  <widget class="GtkLabel" id="label2">
+                    <property name="visible">True</property>
+                    <property name="label" translatable="yes">Password:</property>
+                  </widget>
+                  <packing>
+                    <property name="position">1</property>
+                  </packing>
+                </child>
+              </widget>
+            </child>
+            <child>
+              <widget class="GtkVBox" id="vbox2">
+                <property name="visible">True</property>
+                <child>
+                  <widget class="GtkEntry" id="user_entry">
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                  </widget>
+                </child>
+                <child>
+                  <widget class="GtkEntry" id="pass_entry">
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <property name="visibility">False</property>
+                  </widget>
+                  <packing>
+                    <property name="position">1</property>
+                  </packing>
+                </child>
+              </widget>
+              <packing>
+                <property name="position">1</property>
+              </packing>
+            </child>
+          </widget>
+          <packing>
+            <property name="position">4</property>
+          </packing>
+        </child>
+        <child internal-child="action_area">
+          <widget class="GtkHButtonBox" id="hbuttonbox12">
+            <property name="visible">True</property>
+            <property name="layout_style">GTK_BUTTONBOX_END</property>
+            <child>
+              <widget class="GtkButton" id="button32">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="can_default">True</property>
+                <property name="label">gtk-cancel</property>
+                <property name="use_stock">True</property>
+                <property name="response_id">-6</property>
+              </widget>
+            </child>
+            <child>
+              <widget class="GtkButton" id="button33">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="can_default">True</property>
+                <property name="label">gtk-ok</property>
+                <property name="use_stock">True</property>
+                <property name="response_id">-5</property>
+              </widget>
+              <packing>
+                <property name="position">1</property>
+              </packing>
+            </child>
+          </widget>
+          <packing>
+            <property name="expand">False</property>
+            <property name="pack_type">GTK_PACK_END</property>
+          </packing>
+        </child>
+      </widget>
+    </child>
+  </widget>
+</glade-interface>

Added: trunk/conduit/modules/RTMModule/rtm.py
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/RTMModule/rtm.py	Fri Apr 11 11:42:21 2008
@@ -0,0 +1,426 @@
+# 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
+_use_simplejson = False
+try:
+    import simplejson
+    _use_simplejson = True
+except ImportError:
+    pass
+
+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'])
+        self.userdata = None
+        # 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)
+        self.userdata = rsp.auth.user
+        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]
+            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):
+                    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)
+
+#----------------------------------------------------------------------
+def get_all_tasks(rtm):
+    """"""
+    rspLists = rtm.lists.getList()
+    tasks = list()
+    tasksf = list()
+    for l in rspLists.lists.list:
+        tasks.append(rtm.tasks.getList(list_id=l.id))
+    temp = [a.tasks.list for a in tasks.__iter__()]
+    map(lambda (x): tasksf.extend(x) , [ a.taskseries for a in temp if hasattr(a,'taskseries')])
+    tasks_sum  = set([t for t in tasksf])
+    print len(tasksf)
+    print tasks_sum
+    
+    return list(tasks_sum)
+    #tasks_sum = list()
+    
+    #map(lambda (x): tasks_sum.extend(x), tasksf)
+    
+    #tasks = [t.name for t in rspTasks.tasks.list]
+    
+    
+import unittest
+
+
+########################################################################
+class TestRTM(unittest.TestCase):
+    """"""
+    
+    #----------------------------------------------------------------------
+    def runTest(self):
+        """"""
+        import rtm
+        rtm2 = rtm.createRTM(apiKey='fe049e2cec86568f3d79c964d4a45f5c',secret='b57757de51f7e919',token=None)
+        get_all_tasks(rtm2)
+        
+    
+    
+
+    
+    
\ No newline at end of file



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]