def poll(account_id, provider): """Query a remote contacts provider for updates and persist them to the database. Parameters ---------- account_id: int ID for the account whose contacts should be queried. db_session: sqlalchemy.orm.session.Session Database session provider: Interface to the remote contact data provider. Must have a PROVIDER_NAME attribute and implement the get_contacts() method. """ provider_name = provider.PROVIDER_NAME with session_scope() as db_session: account = db_session.query(Account).get(account_id) # Contact data reflecting any local modifications since the last sync # with the remote provider. local_contacts = db_session.query(Contact).filter_by( source='local', account=account, provider_name=provider_name).all() # Snapshot of contact data from immediately after last sync. cached_contacts = db_session.query(Contact).filter_by( source='remote', account=account, provider_name=provider_name).all() log.info('Query: have {0} contacts, {1} cached.'.format( len(local_contacts), len(cached_contacts))) cached_contact_dict = {contact.uid: contact for contact in cached_contacts} local_contact_dict = {contact.uid: contact for contact in local_contacts} change_counter = Counter() last_sync = or_none(account.last_synced_contacts, datetime.datetime.isoformat) to_commit = [] for remote_contact in provider.get_contacts(last_sync): remote_contact.account = account assert remote_contact.uid is not None, \ 'Got remote contact with null uid' assert isinstance(remote_contact.uid, str) cached_contact = cached_contact_dict.get(remote_contact.uid) local_contact = local_contact_dict.get(remote_contact.uid) # If the remote contact was deleted, purge the corresponding # database entries. if remote_contact.deleted: if cached_contact is not None: db_session.delete(cached_contact) change_counter['deleted'] += 1 if local_contact is not None: db_session.delete(local_contact) continue # Otherwise, update the database. if cached_contact is not None: # The provider gave an update to a contact we already have. if local_contact is not None: try: # Attempt to merge remote updates into local_contact merge(cached_contact, remote_contact, local_contact) # And update cached_contact to reflect both local and # remote updates cached_contact.copy_from(local_contact) except MergeError: log.error('Conflicting local and remote updates to ' 'contact.\nLocal: {0}\ncached: {1}\n ' 'remote: {2}'.format(local_contact, cached_contact, remote_contact)) # TODO(emfree): Come up with a strategy for handling # merge conflicts. For now, just don't update if there # is a conflict. continue else: log.warning('Contact {0} already present as remote but ' 'not local contact'.format(cached_contact)) cached_contact.copy_from(remote_contact) change_counter['updated'] += 1 else: # This is a new contact, create both local and remote DB # entries. local_contact = Contact() local_contact.copy_from(remote_contact) local_contact.source = 'local' to_commit.append(local_contact) to_commit.append(remote_contact) change_counter['added'] += 1 account.last_synced_contacts = datetime.datetime.now() log.info('Added {0} contacts.'.format(change_counter['added'])) log.info('Updated {0} contacts.'.format(change_counter['updated'])) log.info('Deleted {0} contacts.'.format(change_counter['deleted'])) db_session.add_all(to_commit) db_session.commit()
def poll(account_id, provider): """Query a remote contacts provider for updates and persist them to the database. Parameters ---------- account_id: int ID for the account whose contacts should be queried. db_session: sqlalchemy.orm.session.Session Database session provider: Interface to the remote contact data provider. Must have a PROVIDER_NAME attribute and implement the get_contacts() method. """ provider_name = provider.PROVIDER_NAME with session_scope() as db_session: account = db_session.query(Account).get(account_id) change_counter = Counter() last_sync = or_none(account.last_synced_contacts, datetime.datetime.isoformat) to_commit = [] for remote_contact in provider.get_contacts(last_sync): remote_contact.account = account assert remote_contact.uid is not None, \ 'Got remote contact with null uid' assert isinstance(remote_contact.uid, str) matching_contacts = db_session.query(Contact).filter( Contact.account == account, Contact.provider_name == provider_name, Contact.uid == remote_contact.uid) # Snapshot of contact data from immediately after last sync: cached_contact = matching_contacts. \ filter(Contact.source == 'remote').first() # Contact data reflecting any local modifications since the last # sync with the remote provider: local_contact = matching_contacts. \ filter(Contact.source == 'local').first() # If the remote contact was deleted, purge the corresponding # database entries. if remote_contact.deleted: if cached_contact is not None: db_session.delete(cached_contact) change_counter['deleted'] += 1 if local_contact is not None: db_session.delete(local_contact) continue # Otherwise, update the database. if cached_contact is not None: # The provider gave an update to a contact we already have. if local_contact is not None: try: # Attempt to merge remote updates into local_contact merge(cached_contact, remote_contact, local_contact) # And update cached_contact to reflect both local and # remote updates cached_contact.copy_from(local_contact) except MergeError: log.error('Conflicting local and remote updates to ' 'contact.\nLocal: {0}\ncached: {1}\n ' 'remote: {2}'.format(local_contact, cached_contact, remote_contact)) # TODO(emfree): Come up with a strategy for handling # merge conflicts. For now, just don't update if there # is a conflict. continue else: log.warning('Contact {0} already present as remote but ' 'not local contact'.format(cached_contact)) cached_contact.copy_from(remote_contact) change_counter['updated'] += 1 else: # This is a new contact, create both local and remote DB # entries. local_contact = Contact() local_contact.copy_from(remote_contact) local_contact.source = 'local' to_commit.append(local_contact) to_commit.append(remote_contact) change_counter['added'] += 1 account.last_synced_contacts = datetime.datetime.now() log.info('Added {0} contacts.'.format(change_counter['added'])) log.info('Updated {0} contacts.'.format(change_counter['updated'])) log.info('Deleted {0} contacts.'.format(change_counter['deleted'])) db_session.add_all(to_commit) db_session.commit()