merge, moved tests around
authorFredrik Wendt <fredrik@wendt.se>
Sun, 6 Jun 2010 12:53:29 +0000 (14:53 +0200)
committerFredrik Wendt <fredrik@wendt.se>
Sun, 6 Jun 2010 12:53:29 +0000 (14:53 +0200)
17 files changed:
1  2 
.gitignore
package/.project
package/.pydevproject
package/src/org/maemo/hermes/engine/facebook/service.py
package/src/org/maemo/hermes/engine/friend.py
package/src/org/maemo/hermes/engine/gravatar/service.py
package/src/org/maemo/hermes/engine/linkedin/service.py
package/src/org/maemo/hermes/engine/service.py
package/src/org/maemo/hermes/engine/twitter/provider.py
package/src/org/maemo/hermes/engine/twitter/service.py
package/test/test.py
package/test/test_cases.txt
package/test/test_cases.txt2
package/test/test_facebook.py
package/test/test_gravatar.py
package/test/test_syncjob.py
package/test/test_twitter.py

diff --combined .gitignore
@@@ -5,8 -5,9 +5,14 @@@ hermes_*.tar.g
  package/build-stamp
  package/configure-stamp
  package/po/*.gmo
  package/debian/files
  package/debian/hermes/
  *~
- *.pyc
  *.pyo
++*.pyc
++.project
++.settings
++.pydevproject
++.mica_settings
++.gitignore
+ core
diff --combined package/.project
@@@ -6,11 -6,6 +6,11 @@@
        </projects>
        <buildSpec>
                <buildCommand>
 +                      <name>org.eclipse.wst.common.project.facet.core.builder</name>
 +                      <arguments>
 +                      </arguments>
 +              </buildCommand>
 +              <buildCommand>
                        <name>org.python.pydev.PyDevBuilder</name>
                        <arguments>
                        </arguments>
@@@ -18,6 -13,7 +18,8 @@@
        </buildSpec>
        <natures>
                <nature>org.python.pydev.pythonNature</nature>
 +              <nature>org.eclipse.wst.common.project.facet.core.nature</nature>
+               <nature>org.maemo.mica.python.project.core.pythonNature</nature>
+               <nature>org.maemo.esbox.python.project.pythonNature</nature>
        </natures>
  </projectDescription>
diff --combined package/.pydevproject
@@@ -1,13 -1,10 +1,11 @@@
 -<?xml version="1.0" encoding="UTF-8"?>
 +<?xml version="1.0" encoding="UTF-8" standalone="no"?>
  <?eclipse-pydev version="1.0"?>
  
  <pydev_project>
  <pydev_property name="org.python.pydev.PYTHON_PROJECT_VERSION">python 2.5</pydev_property>
- <pydev_property name="org.python.pydev.PYTHON_PROJECT_INTERPRETER">Default</pydev_property>
 -<pydev_property name="org.python.pydev.PYTHON_PROJECT_INTERPRETER">FREMANTLE_X86 - Python Interpreter</pydev_property>
++<pydev_property name="org.python.pydev.PYTHON_PROJECT_INTERPRETER">python</pydev_property>
  <pydev_pathproperty name="org.python.pydev.PROJECT_SOURCE_PATH">
  <path>/Hermes/src</path>
- </pydev_pathproperty>
- <pydev_pathproperty name="org.python.pydev.PROJECT_EXTERNAL_SOURCE_PATH">
- <path>/home/ceda/pyworkspace/maemo-python-site-packages</path>
++<path>/Hermes/test</path>
  </pydev_pathproperty>
  </pydev_project>
 -import gnome.gconf
  import org.maemo.hermes.engine.service
  
 -from facebook import Facebook,FacebookError
  from org.maemo.hermes.engine.names import canonical
+ from org.maemo.hermes.engine.friend import Friend
  
  class Service(org.maemo.hermes.engine.service.Service):
      """Facebook backend for Hermes.
                  
 -       This requires two gconf paths to contain Facebook application keys:
 -           /apps/maemo/hermes/key_app
 -           /apps/maemo/hermes/key_secret
 -       
         Copyright (c) Andrew Flegg <andrew@bleb.org> 2010.
 +       Copyright (c) Fredrik Wendt <fredrik@wendt.se> 2010.
         Released under the Artistic Licence."""
         
 -       
 +    attrs = ['uid', 'name', 'pic_big', 'birthday_date', 'profile_url', 'first_name', 'last_name', 'website']
-        
++
++
      # -----------------------------------------------------------------------
-     def __init__(self, facebook, autocreate = False, gui_callback = None):
 -    def __init__(self, autocreate = False, gui_callback = None):
++    def __init__(self, facebook, autocreate=False, gui_callback=None):
          """Initialise the Facebook service, finding Facebook API keys in gconf and
             having a gui_callback available."""
          
 -        self._gc  = gnome.gconf.client_get_default()
          self._gui = gui_callback
          self._autocreate = autocreate
 +        self.fb = facebook
          
-         self._friends_by_id = {}
 -        # -- Check the environment is going to work...
 -        #
 -        if (self._gc.get_string('/desktop/gnome/url-handlers/http/command') == 'epiphany %s'):
 -            raise Exception('Browser in gconf invalid (see NB#136012). Installation error.')
 -
 -        key_app    = self._gc.get_string('/apps/maemo/hermes/key_app')
 -        key_secret = self._gc.get_string('/apps/maemo/hermes/key_secret')
 -        if key_app is None or key_secret is None:
 -            raise Exception('No Facebook application keys found. Installation error.')
 -
 -        self.fb = Facebook(key_app, key_secret)
 -        self.fb.desktop = True
 -        
 -        self._friends = None
++        self._friends_by_name = {}
          self._friends_by_url = {}
          self._friends_by_contact = {}
 -        self._should_create = set()
 -        
 -        self.get_friends()
 +        self._contacts_by_friend = {}
 +        self._friends_without_contact = set()
 +        self._known_urls = set()
  
  
      # -----------------------------------------------------------------------
      def get_name(self):
          return "Facebook"
      
-     
      # -----------------------------------------------------------------------
 -    def _do_fb_login(self):
 -        """Perform authentication against Facebook and store the result in gconf
 -             for later use. Uses the 'need_auth' and 'block_for_auth' methods on
 -             the callback class. The former allows a message to warn the user
 -             about what is about to happen to be shown; the second is to wait
 -             for the user to confirm they have logged in."""
 -        self.fb.session_key = None
 -        self.fb.secret = None
 -        self.fb.uid = None
 -        
 -        if self._gui:
 -            self._gui.need_auth()
 -            
 -        self.fb.auth.createToken()
 -        self.fb.login()
 -        
 -        if self._gui:
 -            self._gui.block_for_auth()
 -          
 -        session = self.fb.auth.getSession()
 -        self._gc.set_string('/apps/maemo/hermes/session_key', session['session_key'])
 -        self._gc.set_string('/apps/maemo/hermes/secret_key', session['secret'])
 -        self._gc.set_string('/apps/maemo/hermes/uid', str(session['uid']))
 +    def get_friends(self):
 +        return self._contacts_by_friend.keys()
 +    
 +    
 +    def get_contacts_with_match(self):
 +        return self._friends_by_contact
 +    
 +    def get_unmatched_friends(self):
-         return self._friends_by_id.values()
++        return self._friends_by_name.values()
++
  
 -   
      # -----------------------------------------------------------------------
 -    def get_friends(self):
 -        """Return a list of friends from this service, or 'None' if manual mapping
 -           is not supported."""
 -           
 -        if self._friends:
 -            return self._friends.values()
 -         
 -        if self.fb.session_key is None:
 -            self.fb.session_key = self._gc.get_string('/apps/maemo/hermes/session_key')
 -            self.fb.secret = self._gc.get_string('/apps/maemo/hermes/secret_key')
 -            self.fb.uid = self._gc.get_string('/apps/maemo/hermes/uid')
 +    def pre_process_contact(self, contact):
 +        """Registers URLs of all previous mappings, and makes sure that any Friends with those
 +           URLs don't get match by name."""
 +        for url in contact.get_urls():
 +            self._known_urls.add(url)
 +    
 +    
 +    # -----------------------------------------------------------------------
 +    def process_friends(self):
          
 -        # Check the available session is still valid...
 -        while True:
 -            try:
 -                if self.fb.users.getLoggedInUser() and self.fb.session_key:
 -                    break
 -            except FacebookError:
 -                pass
 -            self._do_fb_login()
 -            
 -            
          def if_defined(data, key, callback):
              if key in data and data[key]:
                  callback(data[key])
          
 -        self._friends = {}
 -        attrs = ['uid', 'name', 'pic_big', 'birthday_date', 'profile_url', 'first_name', 'last_name', 'website']
 -        for data in self.fb.users.getInfo(self.fb.friends.get(), attrs):
 -            key = canonical(data['name'])
 -            friend = Friend(data['name'])
 -            self._friends[key] = friend
 +        friends_data = self._get_friends_data()
 +        for data in friends_data:
 +            key = canonical(data['name']) # FIXME: deal with name collision
 +            friend = self._create_friend(data['name'])
          
              if 'profile_url' not in data:
                  data['profile_url'] = "http://www.facebook.com/profile.php?id=" + str(data['uid'])
          
 -            self._friends_by_url[data['profile_url']] = friend
 -            
 -
              if_defined(data, 'website', friend.add_url)
              if_defined(data, 'profile_url', friend.add_url)
              if_defined(data, 'birthday_date', friend.set_birthday_date)
  
 -            # exception from the rule:
 -#            if_defined(data, 'pic_big', friend.set_photo_url)
 -            friend.set_photo_url(data[attrs[2]])
 +            if_defined(data, 'pic_big', friend.set_photo_url)
              
 -            if friend.has_birthday_date():
 -                self._should_create.add(friend)
 -                
 -        return self._friends.values()
 +            if friend.has_birthday_date(): # FIXME: remove this, either you want to add your contacts or not? 
 +                self._friends_without_contact.add(friend)
 +            url = data['profile_url']
 +            self._friends_by_url[url] = friend
 +            
 +            if url not in self._known_urls:
-                 self._friends_by_id[key] = friend
++                self._friends_by_name[key] = friend
  
      # -----------------------------------------------------------------------
 -    def pre_process_contact(self, contact):
 -        
 -        if not self._friends:
 -            self.get_friends()
 -        
 -        for url in contact.get_urls():
 -            if url in self._friends_by_url:
 -                matched_friend = self._friends_by_url[url]
 -                self._friends_by_contact[contact] = matched_friend
 -                self._should_create.discard(matched_friend)
 -        
 -    
 -    # -----------------------------------------------------------------------
 -    def process_contact(self, contact, friend):
 -           
 -        if not self._friends:
 -            self.get_friends()
 -
 +    def process_contact(self, contact):
          if self._friends_by_contact.has_key(contact):
              return
          
          matched_friend = None
 -        # we might get a hit if the friend has setup a URL with another service 
 +        # we might get a hit if the friend has setup a URL with another service,
 +        # such as putting the id link to Facebook on the Twitter account's profile
          for url in contact.get_urls():
              if url in self._friends_by_url:
                  matched_friend = self._friends_by_url[url]
 -                self._friends_by_contact[contact] = matched_friend
 +                self._register_match(contact, matched_friend)
 +                print contact.get_name(), " -> match by url -> ", matched_friend
                  break
  
          if not matched_friend:
              for id in contact.get_identifiers():
-                 if id in self._friends_by_id:
-                     matched_friend = self._friends_by_id.pop(id)
 -                if id in self._friends:
 -                    matched_friend = self._friends[id]
 -                    self._friends_by_contact[contact] = matched_friend
++                if id in self._friends_by_name:
++                    matched_friend = self._friends_by_name.pop(id)
 +                    self._register_match(contact, matched_friend)
 +                    print contact.get_name(), " -> match by name -> ", matched_friend
                      break
 -                
 -        if matched_friend:
 -            print contact.get_name(), " -> ", matched_friend
 -            self._should_create.discard(matched_friend)
 -        
 -            
 -    
      
 +
      # -----------------------------------------------------------------------
 -    def finalise(self, updated, overwrite = False):
 -        """Once all contacts have been processed, allows for any tidy-up/additional
 -           enrichment. If any contacts are updated at this stage, 'updated' should
 -           be added to."""
 +    def _register_match(self, contact, friend):
 +        self._friends_without_contact.discard(friend)
 +        self._friends_by_contact[contact] = friend
 +        self._contacts_by_friend[friend] = contact
  
 -        if True or self._autocreate: # FIXME - remove True or
 -            for friend in self._should_create:
 -                print "Need to autocreate: %s (%s)" % (friend._attributes, friend._multi_attributes)
 +    # -----------------------------------------------------------------------
 +    def _get_friends_data(self):
 +        """Returns a list of dicts, where each dict represents a friend/contact"""
 +        
 +        return self.fb.users.getInfo(self.fb.friends.get(), Service.attrs)
@@@ -1,10 -1,8 +1,10 @@@
  class Friend():
      
 -    def __init__(self, name):
 +    def __init__(self, name, source=None):
 +        """ source is source service, such as LinkedIn """
          self._attributes = { "fn": name }
          self._multi_attributes = {}
 +        self._source = source 
          
      def __unicode__(self):
          return self.__repr__()
      
      # public accessors -----------------
      
 +    def get_name(self):
 +        return self._attributes['fn']
 +    
++    def get_nickname(self):
++        return self._attributes["nickname"]
++    
      def add_url(self, url):
          self._add('url', url)
          
      
      def _has(self, key):
          return self._attributes.has_key(key) or self._multi_attributes.has_key(key)
 -
 +    
 +    def _contains(self, key, value):
 +        if self._attributes.has_key(key):
 +            return value == self._attributes[key]
 +        if self._multi_attributes.has_key(key):
 +            return value in self._multi_attributes[key]
 +        return False
@@@ -1,6 -1,9 +1,6 @@@
  import urllib, hashlib, xmlrpclib
 -import gnome.gconf
  import org.maemo.hermes.engine.service
  
 -from org.maemo.hermes.engine.names import canonical
 -
  class Service(org.maemo.hermes.engine.service.Service):
      """Gravatar backend for Hermes.
         
      _image_url_base = "http://www.gravatar.com/avatar.php?"
         
      # -----------------------------------------------------------------------
 -    def __init__(self):
 -        """Initialise the Gravatar service"""
 +    def __init__(self, api_email, api_key):
 +        """Initialise the Gravatar service
          
 -        self._gc = gnome.gconf.client_get_default()
 +        api_email is the email address to use when talking to the backend.
 +        api_key is the "secrect" key used when talking to the backend
 +        """
 +        self._api_email = api_email
 +        self._api_key = api_key
 +        if self._api_key is None or self._api_email is None:
 +            raise Exception('No Gravatar application keys found. Installation error.')
 +         
          self._address_to_contact = {}
 -        self._hash_to_address= {}
 +        self._hash_to_address = {}
          self._hash_has_gravatar = {}
          self._empty_cache = True
          
 -        self._api_email = self._gc.get_string('/apps/maemo/hermes/gravatar_api_email')
 -        self._api_key = self._gc.get_string('/apps/maemo/hermes/gravatar_api_key')
 -        self._api_email = 'maemohermes@wendt.se'
 -        self._api_key = 'b14ec179822b' # FIXME
 -        if self._api_key is None or self._api_email is None:
 -            raise Exception('No Gravatar application keys found. Installation error.') 
 +        self._friends_by_contact = {}
 +        self._contacts_by_friend = {}
 +        
          self._api_url = 'https://secure.gravatar.com/xmlrpc?user=' + hashlib.md5(self._api_email).hexdigest()
     
  
      
      
      # -----------------------------------------------------------------------
 -    def get_friends(self):
 -        """Returns 'None' since manual mapping is not supported."""
 -        
 -        return None
 -
 -    
 -    # -----------------------------------------------------------------------
      def pre_process_contact(self, contact):
          """Extracts addresses from the contact."""
          for address in contact.get_emails():
      
      
      # -----------------------------------------------------------------------
 -    def process_contact(self, contact, friend):
 -        """On first call (with a contact missing a photo), go get gravatar existense data from the service."""
 +    def process_contact(self, contact):
 +        """On first call (with a contact missing a photo), go get data from Gravatar's servers."""
          
          if not self._has_photo(contact):
 -            if self._empty_cache:
 -                self._lookup_addresses()
              for address in contact.get_emails():
                  hash = self._get_hash_for_address(address)
 -                if (self._hash_has_gravatar[hash]):
 +                if (self._hash_has_gravatar.has_key(hash) and self._hash_has_gravatar[hash]):
 +                    friend = self._create_friend(contact.get_name())
                      friend.set_photo_url(self._get_url_for_email_hash(hash))
 +                    self._register_match(contact, friend)
 +
 +
 +    # -----------------------------------------------------------------------
 +    def process_friends(self):
 +        self._lookup_addresses()
  
      
      # -----------------------------------------------------------------------
 -    def finalise(self, updated, overwrite=True):
 -        """We could make a new pass over all updated contacts, in the event that another service
 -           added a new e-mail address to a contact (still missing a photo) that has a gravatar setup."""
 +    def get_friends(self):
 +        return self._contacts_by_friend.keys()
  
 -        pass
 -        # It's plausible that info@company.com addresses would be present for several
 -        # contacts, but unlikely that there'd be a gravatar set up for such generic
 -        # addresses - the point of gravatar is to make it more personal ...
 -        
 -        # we only look at contacts that has no photo (unless 
 -#        for contact in self._contacts:
 -#            if not overwrite or not self._has_photo(contact):
 -#                for address in contact.get_emails():
 -#                    self._address_to_contact[address] = contact
 -#        self._lookup_addresses()
 -#        for hash in self._hash_has_gravatar:
 -#            if self._hash_has_gravatar[hash]:
 -#                address = self._hash_to_address[hash]
 -#                contact = self._address_to_contact[address]
 -#                if not self._has_photo(contact):
 -#                    url = self._get_url_for_email_hash(hash)
 -#                    contact.set_photo(url)
 -#                    updated.add(contact)
 +    
 +    def get_contacts_with_match(self):
 +        """Returns a dict with Contact objects as keys and Friend objects as values"""
 +        return self._friends_by_contact
 +    
 +
 +    def get_unmatched_friends(self):
 +        """Will always return None - Gravatar only reacts on e-mail address input."""
 +        return None
 +    
 +    
 +
 +
 +    # -----------------------------------------------------------------------
 +    def _register_match(self, contact, friend):
 +        self._friends_by_contact[contact] = friend
 +        self._contacts_by_friend[friend] = contact
          
      # -----------------------------------------------------------------------
      # FIXME
      
          
      # -----------------------------------------------------------------------
      def _lookup_addresses(self):
          """Constructs hashes for address_to_contact, makes call to the Gravatar.com service and updates
          self._hash_has_gravatar"""
          
 -        args = { "apikey" : self._api_key}
 -        args["hashes"] = list(self._construct_hashes(self._address_to_contact.keys()))
 -        service = xmlrpclib.ServerProxy(self._api_url)
 -        self._hash_has_gravatar = service.grav.exists(args)
 +        addresses = self._address_to_contact.keys()
 +        if len(addresses) == 0:
 +            self._set_hash_information({})
 +        else:
 +            args = { "apikey" : self._api_key}
 +            hashes = self._construct_hashes(addresses)
 +            args["hashes"] = list(hashes)
 +            url = self._api_url
 +            self._set_hash_information(self._get_hash_info_from_server(args, url))
 +            
 +
 +    # -----------------------------------------------------------------------
 +    def _get_hash_info_from_server(self, args, url):
 +        """Makes the actual XML-RPC call - override this for testing"""
 +        
 +        service = xmlrpclib.ServerProxy(url)
 +        return service.grav.exists(args)
 +
 +
 +    # -----------------------------------------------------------------------
 +    def _set_hash_information(self, hash_info):
 +        self._hash_has_gravatar = hash_info
          self._empty_cache = False
 -    
 +        
  
      # -----------------------------------------------------------------------
      def _get_url_for_email_hash(self, hash, default="404", size="128"):
  
      # -----------------------------------------------------------------------
      def _get_hash_for_address(self, address):
 -        return hashlib.md5(address.lower()).hexdigest()
 +        return hashlib.md5(address.lower()).hexdigest()
@@@ -64,7 -64,7 +64,7 @@@ class Service(org.maemo.hermes.engine.s
          self.consumer = oauth.OAuthConsumer(api_key, secret_key)
          self.sig_method = oauth.OAuthSignatureMethod_HMAC_SHA1()
      
--        self._friends = None
++        self._friends_by_name = None
          self._friends_by_url = {}
          self._friends_by_contact = {}
          
          """Return a list of friends from this service, or 'None' if manual mapping
             is not supported."""
             
--        if self._friends:
--            return self._friends.values()
++        if self._friends_by_name:
++            return self._friends_by_name.values()
  
          # FIXME: Check the available OAuth session is still valid...
  
          # get data
--        self._friends = {}
++        self._friends_by_name = {}
          
          xml = self._make_api_request(self.LI_CONN_API_URL)
          dom = parseString(xml)
          
          self._parse_dom(dom)
  
--        print "get_friends found", len(self._friends)
--        return self._friends.values()
++        print "get_friends found", len(self._friends_by_name)
++        return self._friends_by_name.values()
  
      
      
      # -----------------------------------------------------------------------
      def finalise(self, updated):
          if self._autocreate or True:
--            for f in self._friends.values():
++            for f in self._friends_by_name.values():
                  if f not in self._friends_by_contact.values():
                      # FIXME: create friends as contact
                      print "Could/should create contact here for %s" % f
      # -----------------------------------------------------------------------
      def _match_contact_by_identifiers(self, contact, friend):
          for id in contact.get_identifiers():
--            if id in self._friends:
--                matched_friend = self._friends[id]
++            if id in self._friends_by_name:
++                matched_friend = self._friends_by_name[id]
                  if matched_friend in self._friends_by_contact.values():
                      print "Avoiding assigning same friend to two contacts: %s " % matched_friend
                  else:
                  if photo_url: friend.set_photo_url(photo_url)
  
                  key = canonical(name)
--                self._friends[key] = friend
++                self._friends_by_name[key] = friend
                  self._friends_by_url[public_url] = friend
  #            except:
  #                continue   
@@@ -1,5 -1,3 +1,5 @@@
 +from friend import Friend
 +
  class Service:
      """The notional `Service' for a provider. This is responsible for communicating
         with the backend service and enhancing contacts.
      def pre_process_contact(self, contact):
          """If the contact have an URL (or similar) that proves an existing matching
             to a friend on this service, then this should be remembered to avoid
 -           name collission/mapping the same friend to other contacts with the same 
 +           name collision/mapping the same friend to other contacts with the same 
             name."""
           
          return None
  
      
 +    def process_friends(self):
 +        """Called once to signal that it's time to get all friends' data."""
 +        
 +        pass
 +    
 +    
      # -----------------------------------------------------------------------
 -    def process_contact(self, contact, friend):
 +    def process_contact(self, contact):
          """Called for each contact in the address book. Any friends linked to
             from the contact should have their matching updated. The back end should 
             enrich the friend passed with any meta-data it can."""
  
  
      # -----------------------------------------------------------------------
-     def finalise(self, updated, overwrite=False):
+     def finalise(self, updated, overwrite = False):
          """Once all contacts have been processed, allows for any tidy-up/additional
             enrichment. If any contacts are updated at this stage, 'updated' should
             be added to."""
             
          pass
 +
 +
 +    # -----------------------------------------------------------------------
 +    def _create_friend(self, name):
 +        """Used to create a Friend object for a contact"""
 +        
 +        return Friend(name, self.get_name())
@@@ -1,6 -1,6 +1,7 @@@
  import gnome.gconf
  import org.maemo.hermes.engine.provider
  import org.maemo.hermes.engine.twitter.service
++import twitter
  
  class Provider(org.maemo.hermes.engine.provider.Provider):
      """Twitter provider for Hermes. 
@@@ -49,5 -49,5 +50,7 @@@
      
          username = self._gconf.get_string("/apps/maemo/hermes/twitter_user") or ''
          password = self._gconf.get_string("/apps/maemo/hermes/twitter_pwd") or ''
--           
++        
++        api = twitter.Api(username=username, password=password)
++
          return org.maemo.hermes.engine.twitter.service.Service(username, password, gui_callback)
@@@ -1,28 -1,28 +1,23 @@@
--import org.maemo.hermes.engine.service
--
--import twitter
  from org.maemo.hermes.engine.names import canonical
--from org.maemo.hermes.engine.friend import Friend
++import org.maemo.hermes.engine.service
  
  class Service(org.maemo.hermes.engine.service.Service):
      """Twitter backend for Hermes.
         
         Copyright (c) Andrew Flegg <andrew@bleb.org> 2010.
++       Copyright (c) Fredrik Wendt <fredrik@wendt.se> 2010.
         Released under the Artistic Licence."""
         
         
      # -----------------------------------------------------------------------
--    def __init__(self, username, password, gui_callback = None):
--        """Initialise the Twitter service, using the given credentials and
--           having a gui_callback available."""
++    def __init__(self, twitterApi):
++        self._twitter = twitterApi
          
--        self._gui = gui_callback
--        self._username = username
--        self._password = password
--        
--        self._friends = None
++        self._friends_by_name = {}
          self._friends_by_url = {}
          self._friends_by_contact = {}
++        self._friends = []
++        self._known_urls = set()
  
     
      # -----------------------------------------------------------------------
      
      
      # -----------------------------------------------------------------------
 -    def get_friends(self):
 -        """Return a list of friends from this service, or 'None' if manual mapping
 -           is not supported."""
 -           
 -        if self._friends:
 -            return self._friends.values()
 -        
 -        self._friends = {}
 -        
 -        api = twitter.Api(username=self._username, password=self._password)
 -        for tweeter in api.GetFriends():
 -            key    = canonical(tweeter.name)
 -            url    = 'http://twitter.com/%s' % (tweeter.screen_name)
 -            friend = Friend(tweeter.name)
++    def pre_process_contact(self, contact):
++        """Registers URLs of all previous mappings, and makes sure that any Friends with those
++           URLs don't get match by name."""
++        for url in contact.get_urls():
++            self._known_urls.add(url)
++
++
++    # -----------------------------------------------------------------------
 +    def process_friends(self):
-         """Return a list of friends from this service, or 'None' if manual mapping
-            is not supported."""
-            
-         if self._friends:
-             return self._friends.values()
-         
-         self._friends = {}
-         
-         api = twitter.Api(username=self._username, password=self._password)
-         for tweeter in api.GetFriends():
-             key    = canonical(tweeter.name)
-             url    = 'http://twitter.com/%s' % (tweeter.screen_name)
-             friend = Friend(tweeter.name)
++        for tweeter in self._get_tweeters():
++            key = canonical(tweeter.name)
++            url = 'http://twitter.com/%s' % (tweeter.screen_name)
++            friend = self._create_friend(tweeter.name)
              friend.set_nickname(tweeter.screen_name)
              friend.add_url(url)
              friend.add_url(tweeter.url)
              if '/default_profile' not in tweeter.profile_image_url:
                  friend.set_photo_url(tweeter.profile_image_url)
            
--            self._friends[key] = friend
++            self._friends.append(friend)
              self._friends_by_url[url] = friend
--                
--        return self._friends.values()
++            if url not in self._known_urls:
++                self._friends_by_name[key] = friend
  
      
      # -----------------------------------------------------------------------
--    def pre_process_contact(self, contact):
--        if not self._friends:
--            self.get_friends()
--            
--        for url in contact.get_urls():
--            if url in self._friends_by_url:
--                matched_friend = self._friends_by_url[url]
--                self._friends_by_contact[contact] = matched_friend
--
--
--    # -----------------------------------------------------------------------
--    def process_contact(self, contact, friend):
--        """Called for each contact in the address book. Any friends linked to
--           from the contact should have their matching updated. The backend should 
--           enrich the contact with any meta-data it can; and return 'True' if any
--           information was updated. If 'friend' is provided, the information should
--           be retrieved from friend. This will be one of the entries from 
--           'get_friends' when manual mapping is being done."""
--           
--        if not self._friends:
--            self.get_friends()
--        
++    def process_contact(self, contact):
          if self._friends_by_contact.has_key(contact):
--            friend.update(self._friends_by_contact[contact])
              return
          
--        for id in contact.get_identifiers():
--            if id in self._friends:
--                matched_friend = self._friends[id]
--                print contact.get_name(), " -> ", matched_friend
--                if matched_friend in self._friends_by_contact.values():
--                    print "COLLISSION avoided for %s" % friend
--                else:
--                    print "Match found %s" % friend
--                    self._friends_by_contact[contact] = matched_friend
--                    friend.update(matched_friend)
--                
++        if self._match_contact_to_friend_by_urls(contact):
++            return
++        
++        self._match_contact_to_friend_by_identifiers(contact)
      
      
      # -----------------------------------------------------------------------
--    def finalise(self, updated, overwrite = False):
--        """Once all contacts have been processed, allows for any tidy-up/additional
--           enrichment. If any contacts are updated at this stage, 'updated' should
--           be added to."""
--           
++    def finalise(self, updated, overwrite=False):
          pass
++
++
++    # -----------------------------------------------------------------------
++    def get_contacts_with_match(self):
++        return self._friends_by_contact
++    
++
++    # -----------------------------------------------------------------------
++    def get_unmatched_friends(self):
++        return self._friends
++    
++
++    # -----------------------------------------------------------------------
++    def _get_tweeters(self):
++        return self._twitter.GetFriends()
++
++
++    def _match_contact_to_friend_by_urls(self, contact):
++        for url in contact.get_urls():
++            if url in self._friends_by_url:
++                matched_friend = self._friends_by_url[url]
++                self._register_match(contact, matched_friend)
++                print contact.get_name(), " -> url -> ", matched_friend
++                return True
++            
++        return False
++
++
++    def _match_contact_to_friend_by_identifiers(self, contact):
++        for id in contact.get_identifiers():
++            if id in self._friends_by_name:
++                matched_friend = self._friends_by_name[id]
++                print contact.get_name(), " -> name -> ", matched_friend
++                self._register_match(contact, matched_friend)
++                return True
++            
++        return False
++
++
++    def _register_match(self, contact, friend):
++        self._friends_by_contact[contact] = friend
++        self._friends.remove(friend)
++
diff --combined package/test/test.py
index 0000000,0000000..ffa65d0
new file mode 100644 (file)
--- /dev/null
--- /dev/null
@@@ -1,0 -1,0 +1,60 @@@
++#!/usr/bin/python2.5
++
++import evolution
++from org.maemo.hermes.engine.contact import Contact
++from org.maemo.hermes.engine.friend import Friend
++from org.maemo.hermes.gui.console import ConsoleUICallback
++import org.maemo.hermes.engine.facebook.provider
++import org.maemo.hermes.engine.twitter.provider
++import org.maemo.hermes.engine.gravatar.provider
++import org.maemo.hermes.engine.linkedin.provider
++
++
++providers = [
++     org.maemo.hermes.engine.facebook.provider.Provider(),
++     org.maemo.hermes.engine.twitter.provider.Provider(),
++     org.maemo.hermes.engine.gravatar.provider.Provider(),
++     org.maemo.hermes.engine.linkedin.provider.Provider(),
++]
++             
++ui = ConsoleUICallback()
++
++# assemble services, least important (prioritized) first
++print "+++ Initialising Services"
++services = []
++for provider in providers:
++    print "Using %s" % (provider.get_name())
++    services.append(provider.service(ui))
++
++contacts = []
++updated_contacts = set()
++unmatched_contacts = set()
++overwrite = False
++
++print "\n+++ Pre processing"
++addresses = evolution.ebook.open_addressbook('default')
++for econtact in addresses.get_all_contacts():
++    contact = Contact(addresses, econtact)
++    contacts.append(contact)
++    for service in services:
++        print "    Pre processing %s with %s" % (contact, service.get_name())
++        service.pre_process_contact(contact)
++
++print "\n+++ Processing"
++for contact in contacts:
++    friend = Friend(contact.get_name())
++    for service in services:
++        print "    Processing %s with %s" % (friend, service.get_name())
++        service.process_contact(contact, friend)
++    if friend.is_empty():
++        print "No service matched %s\n" % friend
++        unmatched_contacts.add(contact)
++    else:
++        print "    Updating contact data for %s\n" % friend
++        friend.update_contact(contact, overwrite)
++        updated_contacts.add(contact)
++
++print "\n+++ Finalising"
++for service in services:
++    print "    Finalising service %s" % service.get_name()
++    service.finalise(updated_contacts)
index 0000000,0000000..be08f73
new file mode 100644 (file)
--- /dev/null
--- /dev/null
@@@ -1,0 -1,0 +1,59 @@@
++given
++      clean addressbook
++      friend "Adam" on linkedin
++      friend "Adam" on twitter
++when
++      full run
++then
++      addressbook should contain one newly created contact, with data from both services
++
++
++given
++      clean addressbook
++      100 friends on linkedin
++      100 friends on twitter, out of which 50 are the same people (have the same name) as on linkedin
++when
++      full run
++then
++      addressbook should contain 150 contacts (not 200)
++
++
++# this will probably not be too uncommon if a person really has an empty address book and runs this the first
++#     time. We don't want that person to end up with many duplicates
++
++
++given
++      contact "Fredrik Wendt"
++      friend "Fredrik L. Wendt" on facebook
++when
++      full processing the contact
++then
++      the contact should be matched with the friend
++
++      
++given
++      clean addressbook
++      friend "twitter" on twitter
++      friend "linkedin" on linkedin
++      friend "facebook" on facebook
++      friend "plaxo" on plaxo
++when
++      full run
++then
++      addressbook should contain 4 contacts
++      
++# process_contact should not rely on pre_process_contact being called 
++
++
++
++given
++      clean addressbook
++      friend "adam" on twitter, with homepage set to "http://facebook/user/adam"
++      friend "adam" on facebook (with ID url "http://facebook/user/adam")
++when
++      twitter is processed
++      and facebook is processed
++then
++      addressbook should contain one contact with two website URLs (twitter and facebook)
++      
++      
index 0000000,0000000..be08f73
new file mode 100644 (file)
--- /dev/null
--- /dev/null
@@@ -1,0 -1,0 +1,59 @@@
++given
++      clean addressbook
++      friend "Adam" on linkedin
++      friend "Adam" on twitter
++when
++      full run
++then
++      addressbook should contain one newly created contact, with data from both services
++
++
++given
++      clean addressbook
++      100 friends on linkedin
++      100 friends on twitter, out of which 50 are the same people (have the same name) as on linkedin
++when
++      full run
++then
++      addressbook should contain 150 contacts (not 200)
++
++
++# this will probably not be too uncommon if a person really has an empty address book and runs this the first
++#     time. We don't want that person to end up with many duplicates
++
++
++given
++      contact "Fredrik Wendt"
++      friend "Fredrik L. Wendt" on facebook
++when
++      full processing the contact
++then
++      the contact should be matched with the friend
++
++      
++given
++      clean addressbook
++      friend "twitter" on twitter
++      friend "linkedin" on linkedin
++      friend "facebook" on facebook
++      friend "plaxo" on plaxo
++when
++      full run
++then
++      addressbook should contain 4 contacts
++      
++# process_contact should not rely on pre_process_contact being called 
++
++
++
++given
++      clean addressbook
++      friend "adam" on twitter, with homepage set to "http://facebook/user/adam"
++      friend "adam" on facebook (with ID url "http://facebook/user/adam")
++when
++      twitter is processed
++      and facebook is processed
++then
++      addressbook should contain one contact with two website URLs (twitter and facebook)
++      
++      
index 0000000,0000000..9603143
new file mode 100644 (file)
--- /dev/null
--- /dev/null
@@@ -1,0 -1,0 +1,98 @@@
++from org.maemo.hermes.engine.facebook.service import Service
++from org.maemo.hermes.engine.names import canonical
++import unittest
++
++
++class FakeContact():
++    id_counter = 0
++    def __init__(self, name, addr, id=None):
++        self.name = name;
++        self.urls = addr
++        self.id = id or FakeContact.id_counter
++        FakeContact.id_counter = FakeContact.id_counter + 1;
++    def get_urls(self):
++        return self.urls
++    def get_name(self):
++        return self.name
++    def get_identifiers(self):
++        return [canonical(self.name)]
++    
++class TestFacebookService(unittest.TestCase):
++    
++    def setUp(self):
++        self.testee = Service(None)
++        
++    def test_main_flow_one_match_by_url_one_by_name(self):
++        # arrange
++        self.existing_address = 'http://www.facebook.com/profile.php?id=123456'
++        self.existing_contact = FakeContact("Facebook Person", [self.existing_address])
++        existing_fake = {'uid':'123456','name':'Name Doesnt Match but URL Does'}
++        
++        self.other_address = 'http://twitter.com/not/correct/site'
++        self.other_contact = FakeContact("Twitter Person", [self.other_address])
++        other_fake = {'uid':'123','name':self.other_contact.get_name()}
++        
++        self.none_contact = FakeContact("No URLson", [])
++        
++        fake_data = [existing_fake, other_fake]
++        self._fake_server_response(fake_data)
++
++        # act        
++        self._run_service([self.existing_contact, self.other_contact, self.none_contact])
++        
++        # assert
++        friends = self.testee.get_friends()
++        contacts = self.testee.get_contacts_with_match()
++        assert len(friends) == 2
++        assert len(contacts) == 2
++        assert self.other_contact in contacts
++        assert self.existing_contact in contacts
++        assert self.none_contact not in contacts
++        
++        
++    def test_name_collision_avoided_by_previous_matching(self):
++        contact_do_match = FakeContact("Same Name", ["http://www.facebook.com/profile.php?id=123"], 1);
++        contact_no_match = FakeContact("Same Name", [None], 2)
++        
++        data = [{'uid':'123','name':'Same Name'}]
++        self._fake_server_response(data)
++        
++        self._run_service([contact_no_match, contact_do_match])
++        
++        assert len(self.testee.get_unmatched_friends()) == 0
++        matchers = self.testee.get_contacts_with_match().keys()
++        assert len(matchers) == 1
++        assert matchers[0].id == 1
++        
++    def test_name_collision_avoided_only_one_person_matched(self):
++        contact_do_match = FakeContact("Same Name", ["http://twitter.com/same_name"]);
++        contact_no_match = FakeContact("Same Name", [None])
++        
++        data = [{'uid':'123','name':'Same Name'}]
++        self._fake_server_response(data)
++        
++        self._run_service([contact_no_match, contact_do_match])
++        
++        matchers = self.testee.get_contacts_with_match().keys()
++        assert len(matchers) == 1
++        assert len(self.testee.get_unmatched_friends()) == 0
++        
++        
++    def _run_service(self, contacts):
++        for contact in contacts:
++            self.testee.pre_process_contact(contact)
++        self.testee.process_friends()
++        for contact in contacts:
++            self.testee.process_contact(contact)
++        
++    def _fake_server_response(self, data):
++        self.testee._get_friends_data = self._get_friends_data
++        self._server_response = data
++    
++    def _get_friends_data(self):
++        return self._server_response
++
++
++    
++if __name__ == '__main__':
++    unittest.main()
index 0000000,0000000..db9704f
new file mode 100644 (file)
--- /dev/null
--- /dev/null
@@@ -1,0 -1,0 +1,80 @@@
++from org.maemo.hermes.engine.gravatar.service import Service
++from org.maemo.hermes.engine.friend import Friend 
++import unittest
++
++class FakeContact():
++    def __init__(self, addr):
++        self.urls = addr
++    def get_emails(self):
++        return self.urls
++    def get_name(self):
++        return self.urls[0]
++    
++class TestGravatarService(unittest.TestCase):
++    
++    def setUp(self):
++        self._setUp('maemohermes@wendt.se', 'b14ec179822b')
++        
++    def test_main_flow(self):
++        self.testee.pre_process_contact(self.existing_contact)
++        self.testee.pre_process_contact(self.missing_contact)
++        self.testee.process_friends()
++        self.testee.process_contact(self.existing_contact)
++        self.testee.process_contact(self.missing_contact)
++        
++        friends = self.testee.get_friends()
++        contacts = self.testee.get_contacts_with_match()
++        assert len(friends) == 1
++        assert len(contacts) == 1
++        assert self.missing_contact not in contacts.keys()
++        assert self.existing_contact in contacts.keys()
++        assert friends[0].get_name() == self.existing_contact.get_name()
++        
++        
++    def test_that_a_person_with_two_addresses_and_one_gravatar_works(self):
++        self._fake_server_response({self.missing_address: None, self.existing_address: "http://url.to.img/"})
++        
++        self.testee.pre_process_contact(self.multiple_contact)
++        self.testee.process_friends()
++        self.testee.process_contact(self.multiple_contact)
++        
++        friends = self.testee.get_friends()
++        contacts = self.testee.get_contacts_with_match()
++        assert len(friends) == 1
++        assert len(contacts) == 1
++        assert self.multiple_contact in contacts
++        assert self.missing_contact not in contacts
++        assert self.existing_contact not in contacts.keys()
++        assert friends[0].get_name() == self.existing_contact.get_name()
++        
++        
++    def _fake_server_response(self, map):
++        self.testee._get_hash_info_from_server = self._get_hash_info_from_server
++        # in real results the addresses hashed, so we'll add that here, and keep originals for easier debugging/inspection
++        for key in map.keys():
++            hash = self.testee._get_hash_for_address(key)
++            map[hash] = map[key]
++        self._server_response = map
++    
++    def _get_hash_info_from_server(self, args, url):
++        self._server_args = args
++        self._server_url = url
++        return self._server_response
++
++    def _setUp(self, email, key):
++        self.testee = Service(email, key)
++        
++        self.existing_address = 'fredrik@wendt.se'
++        self.existing_contact = FakeContact([self.existing_address])
++        self.existing_friend = Friend("Fredrik Wendt")
++        
++        self.missing_address = 'will_not_ever_exist_i_truly_hope_at_least@wendt.se'
++        self.missing_contact = FakeContact([self.missing_address])
++        self.missing_friend = Friend("Unknown Person")
++        
++        self.multiple_contact = FakeContact([self.existing_address, self.missing_address])
++        self.multiple_friend = Friend("Another Person")
++
++    
++if __name__ == '__main__':
++    unittest.main()
index 0000000,0000000..e677dfa
new file mode 100644 (file)
--- /dev/null
--- /dev/null
@@@ -1,0 -1,0 +1,42 @@@
++import unittest
++from syncjob import Syncjob
++
++class FakeContact():
++    def __init__(self, name, addr):
++        self.name = name
++        self.urls = [addr]
++    def get_emails(self):
++        return self.urls
++    def get_name(self):
++        return self.name
++    
++class FakeService():
++    def __init__(self):
++        self.contacts_preprocessed = []
++        self.contacts_processed = []
++    def get_name(self):
++        return "fake service"
++    def pre_process_contact(self, contact):
++        self.contacts_preprocessed.append(contact)
++    def process_friends(self):
++        pass
++    def process_contact(self, contact):
++        self.contacts_processed.append(contact)
++     
++    
++class TestSyncJob(unittest.TestCase):
++    
++    def setUp(self):
++        self.service = FakeService() 
++        self.services = [self.service]
++        self.contact = FakeContact('Fredrik Wendt', 'fredrik@wendt.se')
++        self.contacts = [self.contact]
++        self.testee = Syncjob(self.services, self.contacts)
++        
++    def test_main_flow(self):
++        self.testee.run()
++        assert self.contact in self.service.contacts_preprocessed
++        assert self.contact in self.service.contacts_processed
++    
++if __name__ == '__main__':
++    unittest.main()
index 0000000,0000000..dd1621a
new file mode 100644 (file)
--- /dev/null
--- /dev/null
@@@ -1,0 -1,0 +1,97 @@@
++from org.maemo.hermes.engine.twitter.service import Service
++from org.maemo.hermes.engine.names import canonical
++import unittest
++
++class FakeContact():
++    def __init__(self, name, addr):
++        self.name = name
++        self.urls = addr
++    def get_urls(self):
++        return self.urls
++    def get_name(self):
++        return self.name
++    def get_identifiers(self):
++        return [canonical(self.name)]
++
++class FakeTweeter():
++    def __init__(self, name, screen_name, props=None):
++        self._props = {}
++        self["name"] = name
++        self["screen_name"] = screen_name
++        if props:
++            for key in props:
++                self[key] = props[key]
++    def __setitem__(self, key, value):
++        self._props[key] = value
++    def __getattr__(self, key):
++        try:
++            return self._props[key]
++        except:
++            return ""
++        
++    
++class TestTwitterService(unittest.TestCase):
++    
++    def setUp(self):
++        self.testee = Service(None)
++    
++    
++    def test_main_flow(self):
++        twitterName = "Twitter Name"
++        self._fake_server([FakeTweeter(twitterName, "wendtse", {"url":"http://wendt.se"})])
++        contact = FakeContact("Fredrik Wendt", ["http://twitter.com/wendtse"])
++        
++        self._exercise_service([contact])
++        
++        assert len(self.testee.get_unmatched_friends()) == 0
++        matchers = self.testee.get_contacts_with_match()
++        assert contact in matchers
++        assert twitterName == matchers[contact].get_name()
++    
++    
++    def test_name_collision_avoided_by_previous_matching(self):
++        twitterName = "Same Name"
++        known_tweeter = FakeTweeter(twitterName, "samename")
++        other_tweeter = FakeTweeter(twitterName, "otherscreenname")
++        self._fake_server([other_tweeter, known_tweeter])
++        contact = FakeContact(twitterName, [])
++        
++        self._exercise_service([contact])
++        
++        assert len(self.testee.get_unmatched_friends()) == 1
++        matchers = self.testee.get_contacts_with_match()
++        assert contact in matchers
++
++
++    def test_name_collision_avoided_only_one_person_matched(self):
++        twitter_name = "Same Name"
++        screen_name = "samename"
++        known_tweeter = FakeTweeter(twitter_name, screen_name)
++        other_tweeter = FakeTweeter(twitter_name, "otherscreenname")
++        self._fake_server([other_tweeter, known_tweeter])
++        contact = FakeContact(twitter_name, ["http://twitter.com/" + screen_name])
++        
++        self._exercise_service([contact])
++        
++        assert len(self.testee.get_unmatched_friends()) == 1
++        matchers = self.testee.get_contacts_with_match()
++        assert contact in matchers
++        assert screen_name == matchers[contact].get_nickname()
++        
++
++    def _exercise_service(self, contacts):
++        for contact in contacts:
++            self.testee.pre_process_contact(contact)
++        self.testee.process_friends()
++        for contact in contacts:
++            self.testee.process_contact(contact)
++
++    def _fake_server(self, data):
++        self._fake_data = data
++        self.testee._get_tweeters = self._get_tweeters
++        
++    def _get_tweeters(self):
++        return self._fake_data
++    
++if __name__ == '__main__':
++    unittest.main()