r6852 - in bigboard/trunk: . bigboard bigboard/stocks bigboard/stocks/files bigboard/stocks/google_calendar



Author: hp
Date: 2007-10-30 15:34:15 -0500 (Tue, 30 Oct 2007)
New Revision: 6852

Added:
   bigboard/trunk/bigboard/accounts_dialog.py
Removed:
   bigboard/trunk/bigboard/stocks/docs_stock.py
Modified:
   bigboard/trunk/bigboard/accounts.py
   bigboard/trunk/bigboard/google.py
   bigboard/trunk/bigboard/google_stock.py
   bigboard/trunk/bigboard/keyring.py
   bigboard/trunk/bigboard/stocks/files/FilesStock.py
   bigboard/trunk/bigboard/stocks/files/filebrowser.py
   bigboard/trunk/bigboard/stocks/google_calendar/CalendarStock.py
   bigboard/trunk/main.py
Log:
Cleanups of google.py and the stocks using it... make it use accounts.py to know what Google accounts exist, and try to rationalize how authentication works.

The previous code assumed there was a separate login step, distinct from making the API calls.
So if the mail checker hadn't existed, we never would have logged in at all. The correct approach 
is to simply try making the API calls and fail appropriately if we don't authenticate when we do.

There is a lot left to do to get all this working reliably.



Modified: bigboard/trunk/bigboard/accounts.py
===================================================================
--- bigboard/trunk/bigboard/accounts.py	2007-10-29 21:42:41 UTC (rev 6851)
+++ bigboard/trunk/bigboard/accounts.py	2007-10-30 20:34:15 UTC (rev 6852)
@@ -22,7 +22,7 @@
 KIND_GOOGLE = AccountKind("google")
 
 def kind_from_string(s):
-    for kind in (KIND_GOOGLE):
+    for kind in [KIND_GOOGLE]:
         if s == kind.get_id():
             return kind
     return None
@@ -34,6 +34,7 @@
 
     def __init__(self, kind, username='', password='', url='', enabled=True, gconf_dir=None):
         super(Account, self).__init__()
+        
         self.__kind = kind
         self.__username = username
         self.__password = password
@@ -41,12 +42,22 @@
         self.__enabled = enabled
         self.__gconf_dir = gconf_dir
 
+        _logger.debug("Created account %s" % (str(self)))
+
     def get_kind(self):
         return self.__kind
 
     def get_username(self):
         return self.__username
 
+    def get_username_as_google_email(self):
+        if self.__username == '':
+            return self.__username
+        elif '@' not in self.__username:
+            return self.__username + '@gmail.com'
+        else:
+            return self.__username
+
     def get_password(self):
         return self.__password
 
@@ -62,15 +73,17 @@
     def _set_gconf_dir(self, gconf_dir):
         self.__gconf_dir = gconf_dir
 
-    def _update_from_origin(self, **kwargs):
+    def _update_from_origin(self, new_props):
         """This is the only way to modify an Account object. It should be invoked only on change notification or refreshed data from the original origin of the account."""
 
         ## check it out!
         changed = False
-        for (key,value) in kwargs.items():
-            old = self.__dict__['__' + key]
+        for (key,value) in new_props.items():
+            if value is None:
+                value = ''
+            old = getattr(self, '_Account__' + key)
             if old != value:
-                self.__dict__['__' + key] = value
+                setattr(self, '_Account__' + key, value)
                 changed = True
 
         if changed:
@@ -123,12 +136,14 @@
 
     def __update_account(self, account):
 
+        _logger.debug("Updating account %s" % (str(account)))
+
         ## note that "kind" is never updated (not allowed to change without
         ## making a new Account object)
 
         was_enabled = account in self.__enabled_accounts
         if was_enabled != account.get_enabled():
-            raise Error("account enabled state messed up")
+            raise Exception("account enabled state messed up")
 
         fields = { }
 
@@ -172,7 +187,8 @@
             if 'enabled' not in fields:
                 fields['enabled'] = True
         else:
-            self.__weblogin_accounts.remove(account)
+            if account in self.__weblogin_accounts:
+                self.__weblogin_accounts.remove(account)
 
         ## after compositing all this information, update our account object
         account._update_from_origin(fields)
@@ -185,13 +201,19 @@
         password = k.get_password(kind=account.get_kind().get_id(),
                                   username=account.get_username(),
                                   url=account.get_url())
-        if 'password' not in fields:
+        if password and 'password' not in fields:
+            _logger.debug("using password from keyring")
             fields['password'] = password
             
         ## fourth, if no password in keyring, use the weblogin one
         if weblogin_password and 'password' not in fields:
+            _logger.debug("using password from weblogin")
             fields['password'] = weblogin_password
 
+        ## if no password found, the password has to be set to empty
+        if 'password' not in fields:
+            fields['password'] = ''
+
         ## update account object again if we might have the password
         if 'password' in fields:
             account._update_from_origin(fields)
@@ -208,8 +230,9 @@
         account = self.__find_weblogin_account_by_kind(kind)
         added = False
         if not account:
-            account = Account(kind)
+            account = Account(kind, enabled=True)
             self.__weblogin_accounts.add(account)
+            self.__enabled_accounts.add(account)
         self.__update_account(account)
 
     def __try_ensure_and_update_account_for_gconf_dir(self, gconf_dir):
@@ -236,22 +259,32 @@
         if account:
             account._set_gconf_dir(gconf_dir)
         else:
-            account = Account(kind, gconf_dir=gconf_dir)
+            account = Account(kind, gconf_dir=gconf_dir, enabled=False)
 
         self.__gconf_accounts.add(account)
         
         self.__update_account(account)
+
+    def __remove_dirname(self, gconf_key):
+        i = gconf_key.rfind('/')
+        return gconf_key[i+1:]
             
     def __reload_from_gconf(self):
         gconf_dirs = self.__gconf.all_dirs('/apps/bigboard/accounts')
 
+        _logger.debug("Reloading %s from gconf" % (str(gconf_dirs)))
+
         new_gconf_infos = {}
         for gconf_dir in gconf_dirs:
-            base_key = '/apps/bigboard/accounts/' + gconf_dir
-
+            base_key = gconf_dir
+            gconf_dir = self.__remove_dirname(gconf_dir)
+            
             gconf_info = {}
             def get_account_prop(gconf, gconf_info, base_key, prop):
-                value = gconf.get_value(base_key + '/' + prop)
+                try:
+                    value = gconf.get_value(base_key + '/' + prop)
+                except ValueError:
+                    value = None
                 if value:
                     gconf_info[prop] = value
             get_account_prop(self.__gconf, gconf_info, base_key, 'kind')
@@ -319,6 +352,9 @@
 
     def save_account_changes(self, account, new_properties):
 
+        _logger.debug("Saving new props for account %s: %s" % (str(account), str(new_properties.keys())))
+        set_password = False
+
         ## special-case handling of password since it goes in the keyring
         if 'password' in new_properties:
             if 'username' in new_properties:
@@ -331,12 +367,15 @@
             else:
                 url = account.get_url()
 
-            k = keyring.get_keyring()            
+            k = keyring.get_keyring()
+            
             k.store_login(kind=account.get_kind().get_id(),
                           username=username,
                           url=url,
                           password=new_properties['password'])
 
+            set_password = True
+
         ## now do everything else by stuffing it in gconf
             
         gconf_dir = account._get_gconf_dir()
@@ -356,6 +395,9 @@
         base_key = '/apps/bigboard/accounts/' + gconf_dir
         
         def set_account_prop(gconf, base_key, prop, value):
+            _logger.debug("prop %s value %s" % (prop, str(value)))
+            if isinstance(value, AccountKind):
+                value = value.get_id()
             gconf.set_value(base_key + '/' + prop, value)
 
         set_account_prop(self.__gconf, base_key, 'kind', account.get_kind())
@@ -369,6 +411,11 @@
         if 'enabled' in new_properties:
             set_account_prop(self.__gconf, base_key, 'enabled', new_properties['enabled'])
 
+        ## keyring doesn't have change notification so we have to do the work for it
+        if set_password:
+            ## this should notice a new password
+            self.__update_account(account)
+
     def create_account(self, kind):
         gconf_dir = self.__find_unused_gconf_dir(kind)
         
@@ -386,3 +433,11 @@
                 accounts.add(a)
         return accounts
     
+__accounts = None
+
+def get_accounts():
+    global __accounts
+    if not __accounts:
+        __accounts = Accounts()
+
+    return __accounts

Added: bigboard/trunk/bigboard/accounts_dialog.py
===================================================================
--- bigboard/trunk/bigboard/accounts_dialog.py	2007-10-29 21:42:41 UTC (rev 6851)
+++ bigboard/trunk/bigboard/accounts_dialog.py	2007-10-30 20:34:15 UTC (rev 6852)
@@ -0,0 +1,132 @@
+import sys, logging
+
+import gobject, gtk
+import bigboard.accounts as accounts
+
+import bigboard.globals as globals
+import libbig.logutil
+from libbig.logutil import log_except
+
+_logger = logging.getLogger("bigboard.AccountsDialog")
+
+class AccountEditor(gtk.VBox):
+    def __init__(self, *args, **kwargs):
+        if 'account' in kwargs:
+            self.__account = kwargs['account']
+            del kwargs['account']
+        else:
+            raise Error("must provide account to AccountEditor")
+        
+        super(AccountEditor, self).__init__(*args, **kwargs)
+
+        self.__username_entry = gtk.Entry()
+        self.__password_entry = gtk.Entry()
+        self.__password_entry.set_visibility(False)
+
+        self.__username_entry.connect('changed',
+                                      self.__on_username_entry_changed)
+        self.__password_entry.connect('changed',
+                                      self.__on_password_entry_changed)
+
+        hbox = gtk.HBox(spacing=10)
+        label = gtk.Label("Email")
+        label.set_alignment(0.0, 0.5)
+        hbox.pack_start(label)
+        hbox.pack_end(self.__username_entry, False)
+        self.pack_start(hbox)
+
+        hbox = gtk.HBox(spacing=10)
+        label = gtk.Label("Password")
+        label.set_alignment(0.0, 0.5)
+        hbox.pack_start(label)    
+        hbox.pack_end(self.__password_entry, False)
+        self.pack_start(hbox)
+
+        self.show_all()
+
+        self.__on_account_changed(self.__account)
+        self.__account.connect('changed', self.__on_account_changed)
+
+        self.__password_entry.set_activates_default(True)
+
+    def __on_account_changed(self, account):
+        self.__username_entry.set_text(account.get_username())
+        self.__password_entry.set_text(account.get_password())
+
+    def __on_username_entry_changed(self, entry):
+        text = entry.get_text()
+        accounts.get_accounts().save_account_changes(self.__account,
+                                                     { 'username' : text })
+
+    def __on_password_entry_changed(self, entry):
+        text = entry.get_text()
+        accounts.get_accounts().save_account_changes(self.__account,
+                                                     { 'password' : text })
+
+class Dialog(gtk.Dialog):
+    def __init__(self, *args, **kwargs):
+        super(Dialog, self).__init__(*args, **kwargs)        
+        
+        self.set_title('Google Accounts')
+
+        self.connect('delete-event', self.__on_delete_event)
+        self.connect('response', self.__on_response)
+
+        self.add_button(gtk.STOCK_CLOSE,
+                        gtk.RESPONSE_OK)
+        self.set_default_response(gtk.RESPONSE_OK)
+
+        self.__editors_by_account = {}
+
+        accts = accounts.get_accounts().get_accounts_with_kind(accounts.KIND_GOOGLE)
+        if len(accts) == 0:
+            accounts.get_accounts().create_account(accounts.KIND_GOOGLE)
+        else:
+            for a in accts:
+                self.__editors_by_account[a] = AccountEditor(account=a)
+                self.vbox.pack_start(self.__editors_by_account[a])
+
+    def __on_delete_event(self, dialog, event):
+        self.hide()
+        return True
+
+    def __on_response(self, dialog, response_id):
+        _logger.debug("response = %d" % response_id)
+        self.hide()
+        
+__dialog = None
+
+def open_dialog():
+    global __dialog
+    if not __dialog:
+        __dialog = Dialog()
+    __dialog.present()
+    
+
+if __name__ == '__main__':
+
+    import gtk, gtk.gdk
+
+    import bigboard.libbig
+    try:
+        import bigboard.bignative as bignative
+    except:
+        import bignative
+
+    import dbus.glib
+
+    import bigboard.google as google
+
+    gtk.gdk.threads_init()
+
+    libbig.logutil.init('DEBUG', ['AsyncHTTP2LibFetcher', 'bigboard.Keyring', 'bigboard.Google', 'bigboard.Accounts', 'bigboard.AccountsDialog'])
+
+    bignative.set_application_name("BigBoard")
+    bignative.set_program_name("bigboard")
+
+    google.init()
+
+    open_dialog()
+
+    gtk.main()
+    

Modified: bigboard/trunk/bigboard/google.py
===================================================================
--- bigboard/trunk/bigboard/google.py	2007-10-29 21:42:41 UTC (rev 6851)
+++ bigboard/trunk/bigboard/google.py	2007-10-30 20:34:15 UTC (rev 6852)
@@ -3,7 +3,6 @@
 
 import hippo, gobject, gtk, dbus, dbus.glib
 
-from ddm import DataModel
 import bigboard.globals as globals
 from bigboard.libbig.singletonmixin import Singleton
 from bigboard.libbig.http import AsyncHTTPFetcher
@@ -15,6 +14,8 @@
 from libbig.struct import AutoStruct, AutoSignallingStruct
 import libbig.polling
 import htmllib
+import bigboard.accounts as accounts
+import gdata.docs as gdocs
 
 _logger = logging.getLogger("bigboard.Google")
 
@@ -66,58 +67,6 @@
 def fmt_date_for_feed_request(date):
     return datetime.datetime.utcfromtimestamp(time.mktime(date.timetuple())).strftime("%Y-%m-%dT%H:%M:%S")
 
-class AbstractDocument(AutoStruct):
-    def __init__(self):
-        AutoStruct.__init__(self, { 'title' : 'Untitled', 'link' : None })
-
-class SpreadsheetDocument(AbstractDocument):
-    def __init__(self):
-        AbstractDocument.__init__(self)
-
-class WordProcessorDocument(AbstractDocument):
-    def __init__(self):
-        AbstractDocument.__init__(self)
-
-class DocumentsParser(xml.sax.ContentHandler):
-    def __init__(self):
-        self.__docs = []
-        self.__inside_title = False
-
-    def startElement(self, name, attrs):
-        #print "<" + name + ">"
-        #print attrs.getNames() # .getValue('foo')
-
-        if name == 'entry':
-            d = SpreadsheetDocument()
-            self.__docs.append(d)
-        elif len(self.__docs) > 0:
-            d = self.__docs[-1]
-            if name == 'title':
-                self.__inside_title = True
-            elif name == 'link':
-                rel = attrs.getValue('rel')
-                href = attrs.getValue('href')
-                type = attrs.getValue('type')
-                #print str((rel, href, type))
-                if rel == 'alternate' and type == 'text/html':
-                    d.update({'link' : href})
-
-    def endElement(self, name):
-        #print "</" + name + ">"
-        
-        if name == 'title':
-            self.__inside_title = False
-
-    def characters(self, content):
-        #print content
-        if len(self.__docs) > 0:
-            d = self.__docs[-1]
-            if self.__inside_title:
-                d.update({'title' : content})
-
-    def get_documents(self):
-        return self.__docs
-
 #class RemoveTagsParser(xml.sax.ContentHandler):
 #    def __init__(self):
 #        self.__content = ''
@@ -265,10 +214,20 @@
             # in my experience sys.exc_info() is some kind of junk here, while "e" is useful
             gobject.idle_add(lambda: errcb(url, response) and False)
 
-class CheckMailTask(libbig.polling.Task):
+class GooglePollAction(object):
     def __init__(self, google):
-        libbig.polling.Task.__init__(self, 1000 * 120, initial_interval=1000*5)
         self.__google = google
+
+    def get_google(self):
+        return self.__google
+
+    def update(self):
+        pass        
+
+class MailPollAction(GooglePollAction):
+    def __init__(self, google):
+        super(MailPollAction, self).__init__(google)
+
         self.__ids_seen = {}
         self.__newest_modified_seen = None
 
@@ -366,195 +325,155 @@
     def __on_fetch_error(self, exc_info):
         pass
 
+    def update(self):
+        self.get_google().fetch_new_mail(self.__on_fetched_mail, self.__on_fetch_error)
+
+class GenericPollAction(GooglePollAction):
+    def __init__(self, google, func):
+        super(GenericPollAction, self).__init__(google)
+        self.__func = func
+
+    def update(self):
+        self.__func(self.get_google())
+
+class CheckGoogleTask(libbig.polling.Task):
+    def __init__(self, google):
+        libbig.polling.Task.__init__(self, 1000 * 120, initial_interval=1000*5)
+        self.__google = google
+        self.__actions = {} ## hash from id to [add count, GooglePollAction object]
+
+    def add_action(self, id, action_constructor):
+        if id not in self.__actions:
+            action = action_constructor()
+            self.__actions[id] = [1, action]
+        else:
+            self.__actions[id][0] = self.__actions[id][0] + 1
+
+    def remove_action(self, id):
+        if id not in self.__actions:
+            raise Exception("removing action id that wasn't added")
+        self.__actions[id][0] = self.__actions[id][0] - 1
+        if self.__actions[id][0] == 0:
+            del self.__actions[id]
+        
     def do_periodic_task(self):
-        self.__google.fetch_new_mail(self.__on_fetched_mail, self.__on_fetch_error)
+        for (id, a) in self.__actions.values():
+            a.update()
 
 class Google(gobject.GObject):
     __gsignals__ = {
-        "auth" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_BOOLEAN,))
+        ## "auth-badness-changed" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_BOOLEAN,))
     }
 
-    def __init__(self, login_label, storage_key, default_domain='gmail.com', type_hint=None):
+    def __init__(self, account):
         super(Google, self).__init__()
         self.__logger = logging.getLogger("bigboard.Google")
-        self.__username = None
-        self.__password = None
+        self.__last_username_tried = ''
+        self.__last_password_tried = ''
+        self.__last_auth_attempt_failed = False
         self.__fetcher = AsyncHTTPFetcherWithAuth()
-        self.__auth_requested = False
-        self.__post_auth_hooks = []
-        self.__login_label = login_label
-        self.__storage_key = storage_key
-        self.__default_domain = default_domain
-        self.__type_hint = type_hint
+        self.__account = account
 
-        self.__model = DataModel(globals.server_name)
-        
-        self.__ddm_identity = None
-        self.__model.add_connected_handler(self.__on_data_model_connected)
-        if self.__model.self_id:
-            self.__on_data_model_connected()
-        else:
-            _logger.debug("datamodel not connected, deferring")       
+        self.__checker = CheckGoogleTask(self)
+        self.add_poll_action('mail', lambda: MailPollAction(self))
 
-        self.__weblogindriver_proxy = dbus.SessionBus().get_object('org.gnome.WebLoginDriver', '/weblogindriver')
-        self.__weblogindriver_proxy.connect_to_signal("SignonChanged",
-                                                       self.__on_signon_changed)
-        self.__recheck_signons()
+        self.__account.connect('changed', lambda account: self.__on_account_changed())
+        self.__on_account_changed()
 
-        # this line allows to enter new Google account information on bigboard restarts    
-        # k.remove_logins(self.__default_domain)
-        self.__mail_checker = None
+    def get_account(self):
+        return self.__account
 
-        self.__username = None
-        self.__password = None
-        self.__on_auth_cancel()
+    def destroy(self):
+        ## FIXME close down all the polling tasks and remove all signal handlers
+        self.__checker.stop()
 
-    def __consider_checking_mail(self):
-        if self.__username and self.__password:
-            if not self.__mail_checker:
-                self.__mail_checker = CheckMailTask(self)
-            self.__mail_checker.start()
-        elif self.__mail_checker:
-            self.__mail_checker.stop()
+    def add_poll_action(self, id, action_constructor):
+        self.__checker.add_action(id, action_constructor)
 
-    def __handle_matched_signon(self, username, password):
-        if self.__type_hint == 'GMail':
-            username = username + "@gmail.com"
-            _logger.debug("using GMail identity %s", username)
-            self.__on_auth_ok(username, password)
-        elif self.__type_hint == 'GAFYD-Mail':
-            if not self.__ddm_identity:
-                _logger.debug("no DDM identity, skipping GAFYD mail")
-                return
-            if not hasattr(self.__ddm_identity, 'googleEnabledEmails'):
-                _logger.debug("No googleEnabledEmails in DDM identity")
-                return
-            for email in self.__ddm_identity.googleEnabledEmails:                             
-                (emailuser, emailhost) = email.split('@', 1) # TODO is this right?
-                if emailuser == username:
-                    _logger.debug("matched username %s (%s) with googleEnabledEmail", emailuser, email)
-                    self.__on_auth_ok(email, password)
-        else:
-            _logger.error("unknown type hint %s...oops!", self.__type_hint)
+    def remove_poll_action(self, id):
+        self.__checker.remove_action(id)
 
-    def __check_signons(self, signons):
-        for signon in signons:
-            if 'hint' not in signon: continue
-            if signon['hint'] == self.__type_hint:
-                _logger.debug("hint %s matched signon %s", self.__type_hint, signon)
-                self.__handle_matched_signon(signon['username'], base64.b64decode(signon['password']))
-                return
-            
-    def __recheck_signons(self):
-        self.__weblogindriver_proxy.GetSignons(reply_handler=self.__on_get_signons_reply,
-                                               error_handler=self.__on_dbus_error)        
-            
-    def __on_data_model_connected(self, *args):
-        _logger.debug("got data model connection")
-        query = self.__model.query_resource(self.__model.self_id, "+;googleEnabledEmails +")
-        query.add_handler(self.__on_google_enabled_emails)
-        query.add_error_handler(self.__on_datamodel_error)        
-        query.execute()
-        
-    def __on_datamodel_error(self, code, str):
-        _logger.error("datamodel error %s: %s", code, str)        
-        
-    def __on_google_enabled_emails(self, myself):
-        self.__ddm_identity = myself     
-        myself.connect(self.__on_ddm_identity_changed)
-        self.__on_ddm_identity_changed(myself)
-        
-    def __on_ddm_identity_changed(self, myself):
-        _logger.debug("ddm identity (%s) changed", myself.resource_id)
-        call_idle_once(self.__recheck_signons)
+    def add_poll_action_func(self, id, func):
+        self.__checker.add_action(id, lambda: GenericPollAction(self, func))
 
-    @log_except(_logger)
-    def __on_get_signons_reply(self, signondata):
-        _logger.debug("got signons reply")
-        for hostname,signons in signondata.iteritems():
-            self.__check_signons(signons)
+    def __consider_checking(self):
+        if self.get_current_auth_credentials_known_bad():
+            _logger.debug("Disabling google polling since auth credentials known bad")
+            self.__checker.stop()
+        else:
+            _logger.debug("Enabling google polling since auth credentials maybe good")
+            self.__checker.start()
 
-    @log_except(_logger)
-    def __on_signon_changed(self, signons):
-        _logger.debug("signons changed: %s", signons)
-        self.__check_signons(signons)
+    def __auth_needs_retry(self):
 
-    @log_except(_logger)
-    def __on_dbus_error(self, err):
-        self.__logger.error("D-BUS error: %s", err)
+        username = self.__account.get_username_as_google_email()
+        password = self.__account.get_password()
 
-    def __on_auth_ok(self, username, password):
-        if '@' not in username:
-            username = username + '@' + self.__default_domain
-        self.__username = username
-        self.__password = password
-        self.__auth_requested = False
+        _logger.debug("auth retry for google username %s" % (username))
 
-        hooks = self.__post_auth_hooks
-        self.__post_auth_hooks = []
-        for h in hooks:
-            h()
-        self.emit("auth", True)
+        self.__last_username_tried = username
+        self.__last_password_tried = password
+        self.__last_auth_attempt_failed = False
 
-        self.__consider_checking_mail()
+        self.__consider_checking()
 
-    def __on_auth_cancel(self):
-        self.__username = None
-        self.__password = None
+    def __on_auth_failed(self, failed_username, failed_password, errcb):
+        _logger.debug("bad auth for google")
 
-        ## FIXME delete our stored password or mark it as failed somehow (in-process)
-        
-        self.emit("auth", False)        
-        self.__consider_checking_mail()
-        self.__auth_requested = False
-        self.__with_login_info(lambda: True)
+        if failed_username == self.__last_username_tried and \
+           failed_password == self.__last_password_tried:
+            self.__last_auth_attempt_failed = True
+            self.__consider_checking()
 
-    def have_auth(self):
-        return (self.__username is not None) and (self.__password is not None)
+        errcb({ 'status' : 401, 'message' : 'Bad or missing username or password' })
 
-    def get_auth(self):
-        return (self.__username, self.__password)
+    def get_current_auth_credentials_known_bad(self):
+        return self.__account.get_username_as_google_email() == '' or \
+               self.__account.get_password() == '' or \
+             self.__last_auth_attempt_failed
 
-    def get_storage_key(self):
-        return self.__storage_key
+    def __on_account_changed(self):
+        auth_changed = False
+        if self.__last_username_tried != self.__account.get_username_as_google_email():
+            auth_changed = True
+        if self.__last_password_tried != self.__account.get_password():
+            auth_changed = True
 
-    def __with_login_info(self, func, reauth=False):
-        """Call func after we get username and password"""
+        _logger.debug("google account changed, auth_changed=%d" % (auth_changed))
 
-        if self.__username and self.__password:
-            # _logger.debug("auth looks valid")   
-            func()
-            return
+        if auth_changed:
+            self.__auth_needs_retry()
 
+    def __call_if_may_have_auth(self, func, errfunc):
+        if self.get_current_auth_credentials_known_bad():
+            errfunc({ 'status' : 401, 'message' : 'Bad or missing username or password' })
         else:
-            _logger.debug("auth request pending; not resending")            
-        self.__post_auth_hooks.append(func)
+            func()
 
-    def __on_bad_auth(self):
-        _logger.debug("got bad auth; invoking reauth")
-        # don't null username, leave it filled in
-        self.__password = None
-        self.__auth_requested = False
-        self.__with_login_info(lambda: True, reauth=True)
-
     ### Calendar
 
     def __have_login_fetch_calendar_list(self, cb, errcb):
 
+        username = self.__account.get_username_as_google_email()
+        password = self.__account.get_password()
+
         # there is a chance that someone might have access to more than 25 calendars, so let's
         # specify 1000 for max-results to make sure we get information about all calendars 
-        uri = 'http://www.google.com/calendar/feeds/' + self.__username + '?max-results=1000'
+        uri = 'http://www.google.com/calendar/feeds/' + username + '?max-results=1000'
 
-        self.__fetcher.fetch(uri, self.__username, self.__password,
+        self.__fetcher.fetch(uri, username, password,
                              lambda url, data: cb(url, data, self),
                              lambda url, resp: errcb(resp),
-                             lambda url: self.__on_bad_auth())
+                             lambda url: self.__on_auth_failed(username, password, errcb))
 
     def fetch_calendar_list(self, cb, errcb):
-        self.__with_login_info(lambda: self.__have_login_fetch_calendar_list(cb, errcb))
+        self.__call_if_may_have_auth(lambda: self.__have_login_fetch_calendar_list(cb, errcb), errcb)
 
     def __have_login_fetch_calendar(self, cb, errcb, calendar_feed_url, event_range_start, event_range_end):
 
+        username = self.__account.get_username_as_google_email()
+        password = self.__account.get_password()        
+
         min_and_max_str = ""
         if event_range_start is not None and event_range_end is not None:
             # just specifying start-min and start-max includes multiple "when" tags in the response for the recurrent events,
@@ -564,57 +483,55 @@
             min_and_max_str =  "?start-min=" + fmt_date_for_feed_request(event_range_start) + "&start-max=" + fmt_date_for_feed_request(event_range_end) + "&singleevents=true" + "&max-results=1000"
 
         if calendar_feed_url is None:
-            uri = 'http://www.google.com/calendar/feeds/' + self.__username + '/private/full' + min_and_max_str
+            uri = 'http://www.google.com/calendar/feeds/' + username + '/private/full' + min_and_max_str
         else:
             uri = calendar_feed_url + min_and_max_str
 
-        self.__fetcher.fetch(uri, self.__username, self.__password,
+        self.__fetcher.fetch(uri, username, password,
                              lambda url, data: cb(url, data, calendar_feed_url, event_range_start, event_range_end, self),
                              lambda url, resp: errcb(resp),
-                             lambda url: self.__on_bad_auth())
+                             lambda url: self.__on_auth_failed(username, password, errcb))
 
     def fetch_calendar(self, cb, errcb, calendar_feed_url = None, event_range_start = None, event_range_end = None):
-        self.__with_login_info(lambda: self.__have_login_fetch_calendar(cb, errcb, calendar_feed_url, event_range_start, event_range_end))
+        self.__call_if_may_have_auth(lambda: self.__have_login_fetch_calendar(cb, errcb, calendar_feed_url, event_range_start, event_range_end), errcb)
 
-    def request_auth(self):
-        self.__with_login_info(lambda: True)
-
     ### Recent Documents
 
     def __on_documents_load(self, url, data, cb, errcb):
         self.__logger.debug("loaded documents from " + url)
-        try:
-            p = DocumentsParser()
-            xml.sax.parseString(data, p)
-            cb(p.get_documents())
-        except xml.sax.SAXException, e:
-            errcb(sys.exc_info())
+        document_list = gdocs.DocumentListFeedFromString(data)   
+        cb(document_list.entry)
 
     def __on_documents_error(self, url, exc_info, errcb):
         self.__logger.debug("error loading documents from " + url)
         errcb(exc_info)
 
     def __have_login_fetch_documents(self, cb, errcb):
+        username = self.__account.get_username_as_google_email()
+        password = self.__account.get_password()        
+        
         uri = 'http://docs.google.com/feeds/documents/private/full'
         # uri = 'http://spreadsheets.google.com/feeds/spreadsheets/private/full'
 
-        self.__fetcher.fetch(uri, self.__username, self.__password,
-                             lambda url, data: cb(url, data, self),
-                             lambda url, resp: errcb(resp),
-                             lambda url: self.__on_bad_auth())
+        self.__fetcher.fetch(uri, username, password,
+                             lambda url, data: self.__on_documents_load(url, data, cb, errcb),
+                             lambda url, resp: self.__on_documents_error(url, resp, errcb),
+                             lambda url: self.__on_auth_failed(username, password, errcb))
 
     def fetch_documents(self, cb, errcb):
-        self.__with_login_info(lambda: self.__have_login_fetch_documents(cb, errcb))
+        self.__call_if_may_have_auth(lambda: self.__have_login_fetch_documents(cb, errcb), errcb)
 
     ### New Mail
 
     def get_mail_base_url(self):
-        if not self.__username:
+        username = self.__account.get_username_as_google_email()
+        
+        if not username:
             return None
             
-        at_sign = self.__username.find('@')
+        at_sign = username.find('@')
 
-        domain = self.__username[at_sign+1:]
+        domain = username[at_sign+1:]
 
         if not domain.endswith('gmail.com'):
             uri = 'http://mail.google.com/a/' + domain
@@ -639,44 +556,63 @@
 
     def __have_login_fetch_new_mail(self, cb, errcb):
 
+        username = self.__account.get_username_as_google_email()
+        password = self.__account.get_password()        
+
         uri = self.get_mail_base_url() + '/feed/atom'
 
-        self.__fetcher.fetch(uri, self.__username, self.__password,
+        self.__fetcher.fetch(uri, username, password,
                              lambda url, data: self.__on_new_mail_load(url, data, cb, errcb),
                              lambda url, exc_info: self.__on_new_mail_error(url, exc_info, errcb),
-                             lambda url: self.__on_bad_auth())
+                             lambda url: self.__on_auth_failed(username, password, errcb))
 
     def fetch_new_mail(self, cb, errcb):
-        self.__with_login_info(lambda: self.__have_login_fetch_new_mail(cb, errcb))
+        self.__call_if_may_have_auth(lambda: self.__have_login_fetch_new_mail(cb, errcb), errcb)
 
-_google_personal_instance = None
-def get_google_at_personal():
-    global _google_personal_instance
-    if _google_personal_instance is None:
-        _google_personal_instance = Google(login_label='Google Personal', storage_key='google', type_hint='GMail')
-    return _google_personal_instance
+__googles_by_account = {}
+__initialized = False
 
-_google_work_instance = None
-def get_google_at_work():
-    global _google_work_instance
-    if _google_work_instance is None:
-        _google_work_instance = Google(login_label='Google Work', storage_key='google-work', type_hint='GAFYD-Mail')
-    return _google_work_instance
-
 def get_googles():
-    return [get_google_at_personal(), get_google_at_work()]
+    global __googles_by_account
+    return __googles_by_account.values()
 
-## this is a hack to allow incrementally porting code to multiple
-## google accounts, it doesn't really make any sense long-term
-def get_google():
-    personal = get_google_at_personal()
-    work = get_google_at_work()
-    if personal.have_auth():
-        return personal
-    elif work.have_auth():
-        return work
-    else:
-        return personal
+def get_google_for_account(account):
+    global __googles_by_account
+    return __googles_by_account[account]
+
+def __refresh_googles(a):
+    global __googles_by_account    
+    gaccounts = a.get_accounts_with_kind(accounts.KIND_GOOGLE)
+    new_googles = {}
+    for g in gaccounts:
+        if g in __googles_by_account:
+            new_googles[g] = __googles_by_account[g]
+        else:
+            new_googles[g] = Google(g)
+
+    for (g, old) in __googles_by_account.items():
+        if g not in new_googles:
+            old.destroy()
+            
+    __googles_by_account = new_googles
+
+def __on_account_added(a, account):
+    __refresh_googles(a)
+
+def __on_account_removed(a, account):
+    __refresh_googles(a)
+
+def init():
+    global __initialized
+    
+    if __initialized:
+        return
+    __initialized = True
+    
+    a = accounts.get_accounts()
+    __refresh_googles(a)
+    a.connect('account-added', __on_account_added)
+    a.connect('account-removed', __on_account_removed)
         
 if __name__ == '__main__':
 

Modified: bigboard/trunk/bigboard/google_stock.py
===================================================================
--- bigboard/trunk/bigboard/google_stock.py	2007-10-29 21:42:41 UTC (rev 6851)
+++ bigboard/trunk/bigboard/google_stock.py	2007-10-30 20:34:15 UTC (rev 6852)
@@ -1,36 +1,72 @@
+import logging
+import hippo
 import bigboard.google as google
-import bigboard.libbig.polling as polling
+import bigboard.accounts as accounts
+import accounts_dialog
 
-polling_periodicity_seconds = 120
+_logger = logging.getLogger("bigboard.Google")
 
-class GoogleStock(polling.Task):
-    def __init__(self, *args, **kwargs):
+## this base class is messed up, because the polling and account data
+## tracking should be in a model object, not in the view (stock).
+## Some stuff in here is view-specific though, like the _create_login_button
+## method.
+
+class GoogleStock(object):
+    def __init__(self, action_id, **kwargs):
+        super(GoogleStock, self).__init__(**kwargs)
+
         # A dictionary of authenticated google accounts, with keys that are used
         # to identify those accounts within the stock.
         self.googles = set()
+        self.__googles_by_account = {} ## map accounts.Account => google.Google
 
-        polling.Task.__init__(self, polling_periodicity_seconds * 1000)
+        self.__action_id = action_id
 
-        gobj_list = google.get_googles()
-        for gobj in gobj_list:
-            gobj.connect("auth", self.on_google_auth)
-            if gobj.have_auth():
-                self.on_google_auth(gobj, True) 
-            else:
-                gobj.request_auth()
+        accts = accounts.get_accounts()
+        for a in accts.get_accounts_with_kind(accounts.KIND_GOOGLE):
+            self.__on_account_added(a)
+        accts.connect('account-added', self.__on_account_added)
+        accts.connect('account-removed', self.__on_account_removed)
 
-    def on_google_auth(self, gobj, have_auth):
-        if have_auth:           
-            self.googles.add(gobj)
-            self.update_google_data(gobj)
-            if not self.is_running():
-                self.start()
-        elif gobj in self.googles:
-          self.stop()
-          self.remove_google_data(gobj)
-          self.googles.remove(gobj)
+        ## FIXME need to unhook everything when stock is removed
 
-    def do_periodic_task(self):
-        self.update_google_data() 
+    def __on_account_added(self, acct):
+        gobj = google.get_google_for_account(acct)
+        gobj.add_poll_action_func(self.__action_id, lambda gobj: self.update_google_data(gobj))
+        self.googles.add(gobj)
+        self.__googles_by_account[acct] = gobj
 
+        ## update_google_data() should be called in the poll action
+    
+    def __on_account_removed(self, acct):
+        ## we keep our own __googles_by_account because google.get_google_for_account()
+        ## will have dropped the Google before this point
+        gobj = self.__googles_by_account[acct]
+        gobj.remove_poll_action(self.__action_id)
+        self.googles.remove(gobj)
+        del self.__googles_by_account[acct]
 
+        ## hook for derived classes
+        self.remove_google_data(gobj)
+
+    def have_one_good_google(self):
+        for g in self.googles:
+            if not g.get_current_auth_credentials_known_bad():
+                return True
+
+        return False
+
+    def __open_login_dialog(self):
+        accounts_dialog.open_dialog()
+
+    def _create_login_button(self):
+        button = hippo.CanvasButton(text="Login to Google")
+        button.connect('activated', lambda button: self.__open_login_dialog())
+        return button
+
+    def update_google_data(self, gobj=None):
+        pass
+
+    def remove_google_data(self, gobj):
+        pass
+    

Modified: bigboard/trunk/bigboard/keyring.py
===================================================================
--- bigboard/trunk/bigboard/keyring.py	2007-10-29 21:42:41 UTC (rev 6851)
+++ bigboard/trunk/bigboard/keyring.py	2007-10-30 20:34:15 UTC (rev 6852)
@@ -89,12 +89,14 @@
 
     def get_password(self, kind, username, url):
       logins = self.get_logins(kind, username, url)
+      _logger.debug("got logins: %s" % (str(logins)))
       if len(logins) > 0:
           return logins.pop().get_password()
       else:
           return None
         
     def remove_logins(self, kind, username, url):
+        _logger.debug("removing login (%s, %s, %s)" % (kind, username, url))
         new_fallbacks = set()
         for ki in self.__fallback_items:
             if ki.get_kind() == kind and \
@@ -120,7 +122,12 @@
                 gnomekeyring.item_delete_sync('session', f.item_id)
   
     def store_login(self, kind, username, url, password):
-        _logger.debug("storing login " + username)
+
+        if not password:
+            self.remove_logins(kind, username, url)
+            return
+
+        _logger.debug("storing login (%s, %s, %s)" % (kind, username, url))
         if not self.is_available():
             found = None
             for ki in self.__fallback_items:

Deleted: bigboard/trunk/bigboard/stocks/docs_stock.py
===================================================================
--- bigboard/trunk/bigboard/stocks/docs_stock.py	2007-10-29 21:42:41 UTC (rev 6851)
+++ bigboard/trunk/bigboard/stocks/docs_stock.py	2007-10-30 20:34:15 UTC (rev 6852)
@@ -1,93 +0,0 @@
-import logging
-
-import gobject
-import hippo
-
-import bigboard, mugshot, google, pango, os
-from big_widgets import CanvasMugshotURLImage, PhotoContentItem
-
-class DocDisplay(PhotoContentItem):
-    def __init__(self, doc):
-        PhotoContentItem.__init__(self, border_right=6)
-        self.__doc = None
-                
-        self.__photo = CanvasMugshotURLImage(scale_width=30, scale_height=30)
-        self.set_photo(self.__photo)
-        self.__box = hippo.CanvasBox(orientation=hippo.ORIENTATION_VERTICAL, spacing=2, 
-                                     border_right=4)
-        self.__title = hippo.CanvasText(xalign=hippo.ALIGNMENT_START, size_mode=hippo.CANVAS_SIZE_ELLIPSIZE_END)
-        self.__description = hippo.CanvasText(xalign=hippo.ALIGNMENT_START, size_mode=hippo.CANVAS_SIZE_ELLIPSIZE_END)
-        attrs = pango.AttrList()
-        attrs.insert(pango.AttrForeground(0x6666, 0x6666, 0x6666, 0, 0xFFFF))
-        self.__description.set_property("attributes", attrs)        
-        self.__box.append(self.__title)
-        self.__box.append(self.__description)        
-        self.set_child(self.__box)
-    
-        self.connect("button-press-event", lambda self, event: self.__on_button_press(event))
-        
-        self.set_doc(doc)
-        
-    def set_doc(self, doc):
-        self.__doc = doc
-        #self.__doc.connect("changed", lambda doc: self.__doc_display_sync())
-        self.__doc_display_sync()
-    
-    def __get_title(self):
-        if self.__doc is None:
-            return "unknown"
-        return self.__doc.get_title()
-    
-    def __str__(self):
-        return '<DocDisplay name="%s">' % (self.__get_title())
-    
-    def __doc_display_sync(self):
-        self.__title.set_property("text", self.__doc.get_title())
-        #self.__photo.set_url(self.__doc.get_icon_url())
-        
-    def __on_button_press(self, event):
-        if event.button != 1:
-            return False
-        
-        logging.debug("activated doc %s", self)
-
-        os.spawnlp(os.P_NOWAIT, 'gnome-open', 'gnome-open', self.__doc.get_link())
-
-class DocsStock(bigboard.AbstractMugshotStock):
-    def __init__(self):
-        super(DocsStock, self).__init__("Documents")
-        self._box = hippo.CanvasBox(orientation=hippo.ORIENTATION_VERTICAL, spacing=3)
-        self._docs = {}
-
-        self.__update_docs()
-
-    def _on_mugshot_initialized(self):
-        super(DocsStock, self)._on_mugshot_initialized()
-
-    def get_content(self, size):
-        return self._box
-            
-    def _set_item_size(self, item, size):
-        if size == bigboard.Stock.SIZE_BULL:
-            item.set_property('xalign', hippo.ALIGNMENT_FILL)
-        else:
-            item.set_property('xalign', hippo.ALIGNMENT_CENTER)
-        item.set_size(size)            
-            
-    def set_size(self, size):
-        super(DocsStock, self).set_size(size)
-        for child in self._box.get_children():
-            self._set_item_size(child, size)        
-
-    def __on_load_docs(self, docs):
-        self._box.remove_all()
-        for doc in docs:
-            display = DocDisplay(doc)
-            self._box.append(display)
-
-    def __on_failed_load(self, exc_info):
-        pass
-            
-    def __update_docs(self):
-        logging.debug("retrieving documents")
-        google.Google().fetch_documents(self.__on_load_docs, self.__on_failed_load)

Modified: bigboard/trunk/bigboard/stocks/files/FilesStock.py
===================================================================
--- bigboard/trunk/bigboard/stocks/files/FilesStock.py	2007-10-29 21:42:41 UTC (rev 6851)
+++ bigboard/trunk/bigboard/stocks/files/FilesStock.py	2007-10-30 20:34:15 UTC (rev 6852)
@@ -9,7 +9,6 @@
 
 from pyonlinedesktop.fsutil import VfsMonitor
 
-import gdata.docs as gdocs
 import bigboard.libbig as libbig
 from bigboard.libbig.logutil import log_except
 from bigboard.libbig.gutil import *
@@ -209,7 +208,7 @@
     """Shows recent files."""
     def __init__(self, *args, **kwargs):
         Stock.__init__(self, *args, **kwargs)
-        google_stock.GoogleStock.__init__(self, *args, **kwargs)
+        google_stock.GoogleStock.__init__(self, 'files', **kwargs)
 
         # files in this list are either LocalFile or GoogleFile 
         self.__files = []
@@ -236,10 +235,10 @@
 
     def update_google_data(self, selected_gobj = None):
         if selected_gobj is not None:
-            selected_gobj.fetch_documents(self.__on_documents_load, self.__on_failed_load)
+            selected_gobj.fetch_documents(lambda entries: self.__on_documents_load(entries, selected_gobj), self.__on_failed_load)
         else:            
             for gobj in self.googles:
-                gobj.fetch_documents(self.__on_documents_load, self.__on_failed_load)    
+                gobj.fetch_documents(lambda entries: self.__on_documents_load(entries, gobj), self.__on_failed_load)    
 
     def __remove_files_for_key(self, source_key):
         files_to_keep = []
@@ -256,11 +255,11 @@
         self._panel.action_taken()
         subprocess.Popen(['gnome-open', fobj.get_url()])        
 
-    def __on_documents_load(self, url, data, gobj):
-        document_list = gdocs.DocumentListFeedFromString(data)   
+    def __on_documents_load(self, document_entries, gobj):
         self.__remove_files_for_key(gobj) 
-        for document_entry in document_list.entry:
-            google_file = GoogleFile(gobj, gobj.get_auth()[0], document_entry)
+        for document_entry in document_entries:
+            google_file = GoogleFile(gobj, gobj.get_account().get_username_as_google_email(),
+                                     document_entry)
             google_file.connect('activated', self.__on_file_activated)
             self.__files.append(google_file)
         self.__files.sort(compare_by_date)

Modified: bigboard/trunk/bigboard/stocks/files/filebrowser.py
===================================================================
--- bigboard/trunk/bigboard/stocks/files/filebrowser.py	2007-10-29 21:42:41 UTC (rev 6851)
+++ bigboard/trunk/bigboard/stocks/files/filebrowser.py	2007-10-30 20:34:15 UTC (rev 6852)
@@ -60,10 +60,10 @@
  
         for google_account in self.__stock.googles:
             # don't list invalid accounts we might have picked up from the signons file
-            if not google_account.have_auth():
+            if google_account.get_current_auth_credentials_known_bad():
                 continue  
-            google_docs_link = ActionLink(text=google_account.get_auth()[0] + " Docs", font="14px", padding_bottom=4, xalign=hippo.ALIGNMENT_START, yalign=hippo.ALIGNMENT_START)
-            google_docs_link.connect("activated", webbrowser.open, create_account_url(google_account.get_auth()[0]))
+            google_docs_link = ActionLink(text=google_account.get_account().get_username_as_google_email() + " Docs", font="14px", padding_bottom=4, xalign=hippo.ALIGNMENT_START, yalign=hippo.ALIGNMENT_START)
+            google_docs_link.connect("activated", webbrowser.open, create_account_url(google_account.get_account().get_username_as_google_email()))
             browse_options.append(google_docs_link)
 
         self.__search_box = CanvasHBox(padding_top=4, padding_bottom=4)        

Modified: bigboard/trunk/bigboard/stocks/google_calendar/CalendarStock.py
===================================================================
--- bigboard/trunk/bigboard/stocks/google_calendar/CalendarStock.py	2007-10-29 21:42:41 UTC (rev 6851)
+++ bigboard/trunk/bigboard/stocks/google_calendar/CalendarStock.py	2007-10-30 20:34:15 UTC (rev 6852)
@@ -438,7 +438,7 @@
 
         # these are at the end since they have the side effect of calling on_mugshot_ready it seems?
         AbstractMugshotStock.__init__(self, *args, **kwargs)
-        google_stock.GoogleStock.__init__(self, *args, **kwargs)
+        google_stock.GoogleStock.__init__(self, 'calendar', **kwargs)
 
         bus = dbus.SessionBus()
         o = bus.get_object('org.freedesktop.Notifications', '/org/freedesktop/Notifications')
@@ -548,7 +548,7 @@
                 #  you must first close the existing Firefox process, or restart your system."
                 time.sleep(2)
                 done_with_sleep_state = 2  
-            libbig.show_url(create_account_url(google_account.get_auth()[0]))
+            libbig.show_url(create_account_url(google_account.get_account().get_username_as_google_email()))
             if done_with_sleep_state == 0:
                 done_with_sleep_state = 1
         
@@ -591,7 +591,7 @@
         self.__refresh_events()
 
     def __on_calendar_list_load(self, url, data, gobj):
-        _logger.debug("loaded calendar list %s", data)
+        _logger.debug("loaded calendar list %d chars" % (len(data)))
         google_key = gobj
         if google_key is None:
             _logger.warn("didn't find google_key for %s", gobj)
@@ -738,8 +738,9 @@
     def __refresh_events(self):      
         self.__box.remove_all()
 
-        if not self.is_running():
-            self.__box.append(hippo.CanvasText(xalign=hippo.ALIGNMENT_CENTER, text="Sign into GMail"))
+        if not self.have_one_good_google():
+            button = self._create_login_button()
+            self.__box.append(button)
             return
 
         title = hippo.CanvasText(xalign=hippo.ALIGNMENT_START, size_mode=hippo.CANVAS_SIZE_ELLIPSIZE_END)
@@ -970,8 +971,9 @@
      
     def __on_failed_load(self, response):
         _logger.debug("load failed")
-        pass
-
+        ## this displays the "need to log in" thing
+        self.__refresh_events()
+    
     def __update_calendar_list_and_events(self, selected_gobj = None):
         _logger.debug("retrieving calendar list")
         # we update events in __on_calendar_list_load() 

Modified: bigboard/trunk/main.py
===================================================================
--- bigboard/trunk/main.py	2007-10-29 21:42:41 UTC (rev 6851)
+++ bigboard/trunk/main.py	2007-10-30 20:34:15 UTC (rev 6852)
@@ -819,7 +819,7 @@
     
     panel.show()
 
-    bigboard.google.get_google() # for side effect of creating the Google object
+    bigboard.google.init()
     #bigboard.presence.get_presence() # for side effect of creating Presence object
         
     gtk.gdk.threads_enter()



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