PEP8 Cleanup

master
Andrew Williams 2014-11-15 19:45:52 +00:00
parent 7e226f2255
commit 0b023789a7
1 changed files with 342 additions and 335 deletions

View File

@ -1,6 +1,5 @@
#!/usr/bin/env python #!/usr/bin/env python
import copy
import dateutil.parser import dateutil.parser
import dateutil.tz import dateutil.tz
import datetime import datetime
@ -15,386 +14,394 @@ API_TOKEN = os.environ.get('TODOIST_API_KEY', None)
NEXT_ACTION_LABEL = os.environ.get('TODOIST_NEXT_ACTION_LABEL', 'next_action') NEXT_ACTION_LABEL = os.environ.get('TODOIST_NEXT_ACTION_LABEL', 'next_action')
TODOIST_VERSION = '5.3' TODOIST_VERSION = '5.3'
class TraversalState(object): class TraversalState(object):
"""Simple class to contain the state of the item tree traversal.""" """Simple class to contain the state of the item tree traversal."""
def __init__(self, next_action_label_id):
self.remove_labels = []
self.add_labels = []
self.found_next_action = False
self.next_action_label_id = next_action_label_id
def clone(self): def __init__(self, next_action_label_id):
"""Perform a simple clone of this state object. self.remove_labels = []
self.add_labels = []
self.found_next_action = False
self.next_action_label_id = next_action_label_id
For parallel traversals it's necessary to produce copies so that every def clone(self):
traversal to a lower node has the same found_next_action status. """Perform a simple clone of this state object.
"""
t = TraversalState(self.next_action_label_id)
t.found_next_action = self.found_next_action
return t
def merge(self, other): For parallel traversals it's necessary to produce copies so that every
"""Merge clones back together. traversal to a lower node has the same found_next_action status.
"""
t = TraversalState(self.next_action_label_id)
t.found_next_action = self.found_next_action
return t
After parallel traversals, merge the results back into the parent state. def merge(self, other):
""" """Merge clones back together.
if other.found_next_action:
self.found_next_action = True After parallel traversals, merge the results back into the parent state.
self.remove_labels += other.remove_labels """
self.add_labels += other.add_labels if other.found_next_action:
self.found_next_action = True
self.remove_labels += other.remove_labels
self.add_labels += other.add_labels
class Item(object): class Item(object):
def __init__(self, initial_data): def __init__(self, initial_data):
self.parent = None self.parent = None
self.children = [] self.children = []
self.checked = initial_data['checked'] == 1 self.checked = initial_data['checked'] == 1
self.content = initial_data['content'] self.content = initial_data['content']
self.indent = initial_data['indent'] self.indent = initial_data['indent']
self.item_id = initial_data['id'] self.item_id = initial_data['id']
self.labels = initial_data['labels'] self.labels = initial_data['labels']
self.priority = initial_data['priority'] self.priority = initial_data['priority']
if 'due_date_utc' in initial_data and initial_data['due_date_utc'] != None: if 'due_date_utc' in initial_data and initial_data['due_date_utc'] is not None:
p = dateutil.parser.parser() p = dateutil.parser.parser()
self.due_date_utc = p.parse(initial_data['due_date_utc']) self.due_date_utc = p.parse(initial_data['due_date_utc'])
else: else:
# Arbitrary time in the future to always sort last # Arbitrary time in the future to always sort last
self.due_date_utc = datetime.datetime(2100, 1, 1, tzinfo=dateutil.tz.tzutc()) self.due_date_utc = datetime.datetime(2100, 1, 1, tzinfo=dateutil.tz.tzutc())
def GetItemMods(self, state): def GetItemMods(self, state):
if self.IsSequential(): if self.IsSequential():
self._SequentialItemMods(state) self._SequentialItemMods(state)
elif self.IsParallel(): elif self.IsParallel():
self._ParallelItemMods(state) self._ParallelItemMods(state)
if not state.found_next_action and not self.checked: if not state.found_next_action and not self.checked:
state.found_next_action = True state.found_next_action = True
if not state.next_action_label_id in self.labels: if not state.next_action_label_id in self.labels:
state.add_labels.append(self) state.add_labels.append(self)
elif state.next_action_label_id in self.labels: elif state.next_action_label_id in self.labels:
state.remove_labels.append(self) state.remove_labels.append(self)
def SortChildren(self): def SortChildren(self):
sortfunc = lambda item: [item.due_date_utc, (5 - item.priority)] sortfunc = lambda item: [item.due_date_utc, (5 - item.priority)]
self.children = sorted(self.children, key=sortfunc) self.children = sorted(self.children, key=sortfunc)
for item in self.children: for item in self.children:
item.SortChildren() item.SortChildren()
def GetLabelRemovalMods(self, state): def GetLabelRemovalMods(self, state):
if state.next_action_label_id in self.labels: if state.next_action_label_id in self.labels:
state.remove_labels.append(self) state.remove_labels.append(self)
for item in self.children: for item in self.children:
item.GetLabelRemovalMods(state) item.GetLabelRemovalMods(state)
def _SequentialItemMods(self, state): def _SequentialItemMods(self, state):
""" """
Iterate over every child, walking down the tree. Iterate over every child, walking down the tree.
If none of our children are the next action, check if we are. If none of our children are the next action, check if we are.
""" """
for item in self.children: for item in self.children:
item.GetItemMods(state) item.GetItemMods(state)
def _ParallelItemMods(self, state): def _ParallelItemMods(self, state):
""" """
Iterate over every child, walking down the tree. Iterate over every child, walking down the tree.
If none of our children are the next action, check if we are. If none of our children are the next action, check if we are.
Clone the state each time we descend down to a child. Clone the state each time we descend down to a child.
""" """
frozen_state = state.clone() frozen_state = state.clone()
for item in self.children: for item in self.children:
temp_state = frozen_state.clone() temp_state = frozen_state.clone()
item.GetItemMods(temp_state) item.GetItemMods(temp_state)
state.merge(temp_state) state.merge(temp_state)
def IsSequential(self): def IsSequential(self):
return not self.content.endswith('=') return not self.content.endswith('=')
#if self.content.endswith('--') or self.content.endswith('='): # if self.content.endswith('--') or self.content.endswith('='):
# return self.content.endswith('--') # return self.content.endswith('--')
#else: #else:
# return self.parent.IsSequential() # return self.parent.IsSequential()
def IsParallel(self):
return self.content.endswith('=')
# if self.content.endswith('--') or self.content.endswith('='):
# return self.content.endswith('=')
#else:
# return self.parent.IsParallel()
def IsParallel(self):
return self.content.endswith('=')
#if self.content.endswith('--') or self.content.endswith('='):
# return self.content.endswith('=')
#else:
# return self.parent.IsParallel()
class Project(object): class Project(object):
def __init__(self, initial_data): def __init__(self, initial_data):
self._todoist = None self._todoist = None
self.parent = None self.parent = None
self.children = [] self.children = []
self._subProjects = None self._subProjects = None
self.itemOrder = initial_data['item_order'] self.itemOrder = initial_data['item_order']
self.indent = initial_data['indent'] - 1 self.indent = initial_data['indent'] - 1
self.is_archived = initial_data['is_archived'] == 1 self.is_archived = initial_data['is_archived'] == 1
self.is_deleted = initial_data['is_deleted'] == 1 self.is_deleted = initial_data['is_deleted'] == 1
self.last_updated = initial_data['last_updated'] self.last_updated = initial_data['last_updated']
self.name = initial_data['name'] self.name = initial_data['name']
# Project should act like an item, so it should have content. # Project should act like an item, so it should have content.
self.content = initial_data['name'] self.content = initial_data['name']
self.project_id = initial_data['id'] self.project_id = initial_data['id']
self._CreateItemTree(self.getItems()) self._CreateItemTree(self.getItems())
self.SortChildren() self.SortChildren()
def getItems(self): def getItems(self):
req = urllib2.Request('https://todoist.com/API/getUncompletedItems?project_id='+ str(self.project_id) +'&token=' + API_TOKEN) req = urllib2.Request(
response = urllib2.urlopen(req) 'https://todoist.com/API/getUncompletedItems?project_id=' + str(self.project_id) + '&token=' + API_TOKEN)
return json.loads(response.read()) response = urllib2.urlopen(req)
return json.loads(response.read())
def setTodoist(self, todoist):
self._todoist = todoist
def setTodoist(self, todoist): def subProjects(self):
self._todoist = todoist if self._subProjects is None:
self._subProjects = []
initialIndent = self.indent
initialOrder = self._todoist._orderedProjects.index(self)
order = initialOrder + 1
maxSize = len(self._todoist._orderedProjects)
if order < maxSize:
current = self._todoist._orderedProjects[order]
while (current.indent > initialIndent) and (order < maxSize):
current = self._todoist._orderedProjects[order]
if current != None:
self._subProjects.append(current)
current.parent = self
order += 1
def subProjects(self): return self._subProjects
if self._subProjects == None:
self._subProjects = []
initialIndent = self.indent
initialOrder = self._todoist._orderedProjects.index(self)
order = initialOrder + 1
maxSize = len(self._todoist._orderedProjects)
if order < maxSize:
current = self._todoist._orderedProjects[order]
while ((current.indent > initialIndent) and (order < maxSize)):
current = self._todoist._orderedProjects[order]
if current != None:
self._subProjects.append(current)
current.parent = self
order = order + 1
return self._subProjects def IsIgnored(self):
return self.name.startswith('Someday') or self.name.startswith('List - ')
def IsIgnored(self): def IsSequential(self):
return self.name.startswith('Someday') or self.name.startswith('List - ') ignored = self.IsIgnored()
endsWithEqual = self.name.endswith('=')
validParent = self.parent is None or not self.parent.IsIgnored()
seq = (not ignored) and (not endsWithEqual) and validParent
# if self.name .startsWith('Payer Camille'):
# print startsWithKeyword
# print endsWithEqual
# print parentSequential
# print seq
return seq
def IsSequential(self): def IsParallel(self):
ignored = self.IsIgnored() return self.name.endswith('=')
endsWithEqual = self.name.endswith('=')
validParent = self.parent == None or not self.parent.IsIgnored()
seq = ((not ignored) and (not endsWithEqual)) and validParent
# if self.name .startsWith('Payer Camille'):
# print startsWithKeyword
# print endsWithEqual
# print parentSequential
# print seq
return seq
def IsParallel(self): SortChildren = Item.__dict__['SortChildren']
return self.name.endswith('=')
SortChildren = Item.__dict__['SortChildren'] def GetItemMods(self, state):
if self.IsSequential():
for item in self.children:
item.GetItemMods(state)
elif self.IsParallel():
frozen_state = state.clone()
for item in self.children:
temp_state = frozen_state.clone()
item.GetItemMods(temp_state)
state.merge(temp_state)
else: # Remove all next_action labels in this project.
for item in self.children:
item.GetLabelRemovalMods(state)
def GetItemMods(self, state): def _CreateItemTree(self, items):
if self.IsSequential(): """Build a tree of items based on their indentation level."""
for item in self.children: parent_item = self
item.GetItemMods(state) previous_item = self
elif self.IsParallel(): for item_dict in items:
frozen_state = state.clone() item = Item(item_dict)
for item in self.children: if item.indent > previous_item.indent:
temp_state = frozen_state.clone() logging.debug('pushing "%s" on the parent stack beneath "%s"',
item.GetItemMods(temp_state) previous_item.content, parent_item.content)
state.merge(temp_state) parent_item = previous_item
else: # Remove all next_action labels in this project. # walk up the tree until we reach our parent
for item in self.children: while parent_item.parent is not None and item.indent <= parent_item.indent:
item.GetLabelRemovalMods(state) logging.debug('walking up the tree from "%s" to "%s"',
parent_item.content, (parent_item.parent if (parent_item.parent is not None) else 0))
parent_item = parent_item.parent
def _CreateItemTree(self, items): logging.debug('adding item "%s" with parent "%s"', item.content,
'''Build a tree of items based on their indentation level.''' parent_item.content if (parent_item is not None) else '')
parent_item = self parent_item.children.append(item)
previous_item = self item.parent = parent_item
for item_dict in items: previous_item = item
item = Item(item_dict)
if item.indent > previous_item.indent:
logging.debug('pushing "%s" on the parent stack beneath "%s"',
previous_item.content, parent_item.content)
parent_item = previous_item
# walk up the tree until we reach our parent
while (parent_item.parent != None and item.indent <= parent_item.indent):
logging.debug('walking up the tree from "%s" to "%s"',
parent_item.content, (parent_item.parent if (parent_item.parent != None) else 0))
parent_item = parent_item.parent
logging.debug('adding item "%s" with parent "%s"', item.content,
parent_item.content if (parent_item != None) else '')
parent_item.children.append(item)
item.parent = parent_item
previous_item = item
class TodoistData(object): class TodoistData(object):
'''Construct an object based on a full Todoist /Get request's data''' """Construct an object based on a full Todoist /Get request's data"""
def __init__(self, initial_data):
self._SetLabelData(initial_data)
self._projects = dict()
for project in initial_data['Projects']: def __init__(self, initial_data):
p = Project(project) self._SetLabelData(initial_data)
p.setTodoist(self) self._projects = dict()
self._projects[project['id']] = p
self._orderedProjects = sorted(self._projects.values(), key=lambda project: project.itemOrder) for project in initial_data['Projects']:
p = Project(project)
p.setTodoist(self)
self._projects[project['id']] = p
for project in self._projects.values(): self._orderedProjects = sorted(self._projects.values(), key=lambda project: project.itemOrder)
project.subProjects()
def _SetLabelData(self, label_data): for project in self._projects.values():
# Store label data - we need this to set the next_action label. project.subProjects()
self._labels_timestamp = label_data['DayOrdersTimestamp']
self._next_action_id = None
for label in label_data['Labels'].values():
if label['name'] == NEXT_ACTION_LABEL:
self._next_action_id = label['id']
logging.info('Found next_action label, id: %s', label['id'])
if self._next_action_id == None:
logging.warning('Failed to find next_action label, need to create it.')
def GetSyncState(self): def _SetLabelData(self, label_data):
project_timestamps = dict() # Store label data - we need this to set the next_action label.
for project_id, project in self._projects.iteritems(): self._labels_timestamp = label_data['DayOrdersTimestamp']
project_timestamps[project_id] = project.last_updated self._next_action_id = None
return {'labels_timestamp': self._labels_timestamp, for label in label_data['Labels'].values():
'project_timestamps': project_timestamps} if label['name'] == NEXT_ACTION_LABEL:
self._next_action_id = label['id']
logging.info('Found next_action label, id: %s', label['id'])
if self._next_action_id is None:
logging.warning('Failed to find next_action label, need to create it.')
def UpdateChangedData(self, changed_data): def GetSyncState(self):
if ('DayOrdersTimestamp' in changed_data project_timestamps = dict()
and changed_data['DayOrdersTimestamp'] != self._labels_timestamp): for project_id, project in self._projects.iteritems():
self._SetLabelData(changed_data) project_timestamps[project_id] = project.last_updated
# delete missing projects return {'labels_timestamp': self._labels_timestamp,
if 'ActiveProjectIds' in changed_data: 'project_timestamps': project_timestamps}
projects_to_delete = set(self._projects.keys()) - set(changed_data['ActiveProjectIds'])
for project_id in projects_to_delete: def UpdateChangedData(self, changed_data):
logging.info("Forgetting deleted project %s", self._projects[project_id].name) if ('DayOrdersTimestamp' in changed_data
del self._projects[project_id] and changed_data['DayOrdersTimestamp'] != self._labels_timestamp):
if 'Projects' in changed_data: self._SetLabelData(changed_data)
for project in changed_data['Projects']: # delete missing projects
logging.info("Refreshing data for project %s", project['name']) if 'ActiveProjectIds' in changed_data:
if project['id'] in self._projects: projects_to_delete = set(self._projects.keys()) - set(changed_data['ActiveProjectIds'])
logging.info("replacing project data, old timestamp: %s new timestamp: %s", for project_id in projects_to_delete:
self._projects[project['id']].last_updated, project['last_updated']) logging.info("Forgetting deleted project %s", self._projects[project_id].name)
self._projects[project['id']] = Project(project) del self._projects[project_id]
# We have already reloaded project data sent to us. if 'Projects' in changed_data:
# Now any project timestamps that have changed are due to the changes we for project in changed_data['Projects']:
# just sent to the server. Let's update our model. logging.info("Refreshing data for project %s", project['name'])
if 'ActiveProjectTimestamps' in changed_data: if project['id'] in self._projects:
for project_id, timestamp in changed_data['ActiveProjectTimestamps'].iteritems(): logging.info("replacing project data, old timestamp: %s new timestamp: %s",
# for some reason the project id is a string and not an int here. self._projects[project['id']].last_updated, project['last_updated'])
project_id = int(project_id) self._projects[project['id']] = Project(project)
if project_id in self._projects: # We have already reloaded project data sent to us.
project = self._projects[project_id] # Now any project timestamps that have changed are due to the changes we
if project.last_updated != timestamp: # just sent to the server. Let's update our model.
logging.info("Updating timestamp for project %s to %s", if 'ActiveProjectTimestamps' in changed_data:
project.name, timestamp) for project_id, timestamp in changed_data['ActiveProjectTimestamps'].iteritems():
project.last_updated = timestamp # for some reason the project id is a string and not an int here.
project_id = int(project_id)
if project_id in self._projects:
project = self._projects[project_id]
if project.last_updated != timestamp:
logging.info("Updating timestamp for project %s to %s",
project.name, timestamp)
project.last_updated = timestamp
def GetProjectMods(self):
def GetProjectMods(self): mods = []
mods = [] # We need to create the next_action label
# We need to create the next_action label if self._next_action_id is None:
if self._next_action_id == None: self._next_action_id = '$%d' % int(time.time())
self._next_action_id = '$%d' % int(time.time()) mods.append({'type': 'label_register',
mods.append({'type': 'label_register', 'timestamp': int(time.time()),
'timestamp': int(time.time()), 'temp_id': self._next_action_id,
'temp_id': self._next_action_id, 'args': {
'args': { 'name': NEXT_ACTION_LABEL
'name': NEXT_ACTION_LABEL }})
}}) # Exit early so that we can receive the real ID for the label.
# Exit early so that we can receive the real ID for the label. # Otherwise we end up applying the label two different times, once with
# Otherwise we end up applying the label two different times, once with # the temporary ID and once with the real one.
# the temporary ID and once with the real one. # This makes adding the label take an extra round through the sync
# This makes adding the label take an extra round through the sync # process, but that's fine since this only happens on the first ever run.
# process, but that's fine since this only happens on the first ever run. logging.info("Adding next_action label")
logging.info("Adding next_action label") return mods
return mods for project in self._projects.itervalues():
for project in self._projects.itervalues(): state = TraversalState(self._next_action_id)
state = TraversalState(self._next_action_id) project.GetItemMods(state)
project.GetItemMods(state) if len(state.add_labels) > 0 or len(state.remove_labels) > 0:
if len(state.add_labels) > 0 or len(state.remove_labels) > 0: logging.info("For project %s, the following mods:", project.name)
logging.info("For project %s, the following mods:", project.name) for item in state.add_labels:
for item in state.add_labels: # Intentionally add the next_action label to the item.
# Intentionally add the next_action label to the item. # This prevents us from applying the label twice since the sync
# This prevents us from applying the label twice since the sync # interface does not return our changes back to us on GetAndSync.
# interface does not return our changes back to us on GetAndSync. item.labels.append(self._next_action_id)
item.labels.append(self._next_action_id) mods.append({'type': 'item_update',
mods.append({'type': 'item_update', 'timestamp': int(time.time()),
'timestamp': int(time.time()), 'args': {
'args': { 'id': item.item_id,
'id': item.item_id, 'labels': item.labels
'labels': item.labels }})
}}) logging.info("add next_action to: %s", item.content)
logging.info("add next_action to: %s", item.content) for item in state.remove_labels:
for item in state.remove_labels: item.labels.remove(self._next_action_id)
item.labels.remove(self._next_action_id) mods.append({'type': 'item_update',
mods.append({'type': 'item_update', 'timestamp': int(time.time()),
'timestamp': int(time.time()), 'args': {
'args': { 'id': item.item_id,
'id': item.item_id, 'labels': item.labels
'labels': item.labels }})
}}) logging.info("remove next_action from: %s", item.content)
logging.info("remove next_action from: %s", item.content) return mods
return mods
def GetResponse(): def GetResponse():
values = {'api_token': API_TOKEN, 'resource_types': ['labels']} values = {'api_token': API_TOKEN, 'resource_types': ['labels']}
data = urllib.urlencode(values) data = urllib.urlencode(values)
req = urllib2.Request('https://api.todoist.com/TodoistSync/v' + TODOIST_VERSION + '/get', data) req = urllib2.Request('https://api.todoist.com/TodoistSync/v' + TODOIST_VERSION + '/get', data)
return urllib2.urlopen(req) return urllib2.urlopen(req)
def GetLabels(): def GetLabels():
req = urllib2.Request('https://todoist.com/API/getLabels?token=' + API_TOKEN) req = urllib2.Request('https://todoist.com/API/getLabels?token=' + API_TOKEN)
return urllib2.urlopen(req) return urllib2.urlopen(req)
def GetProjects(): def GetProjects():
req = urllib2.Request('https://todoist.com/API/getProjects?token=' + API_TOKEN) req = urllib2.Request('https://todoist.com/API/getProjects?token=' + API_TOKEN)
return urllib2.urlopen(req) return urllib2.urlopen(req)
def DoSync(items_to_sync): def DoSync(items_to_sync):
values = {'api_token': API_TOKEN, values = {'api_token': API_TOKEN,
'items_to_sync': json.dumps(items_to_sync)} 'items_to_sync': json.dumps(items_to_sync)}
logging.info("posting %s", values) logging.info("posting %s", values)
data = urllib.urlencode(values) data = urllib.urlencode(values)
req = urllib2.Request('https://api.todoist.com/TodoistSync/v' + TODOIST_VERSION + '/sync', data) req = urllib2.Request('https://api.todoist.com/TodoistSync/v' + TODOIST_VERSION + '/sync', data)
return urllib2.urlopen(req) return urllib2.urlopen(req)
def DoSyncAndGetUpdated(items_to_sync, sync_state): def DoSyncAndGetUpdated(items_to_sync, sync_state):
values = {'api_token': API_TOKEN, values = {'api_token': API_TOKEN,
'items_to_sync': json.dumps(items_to_sync)} 'items_to_sync': json.dumps(items_to_sync)}
for key, value in sync_state.iteritems(): for key, value in sync_state.iteritems():
values[key] = json.dumps(value) values[key] = json.dumps(value)
logging.debug("posting %s", values) logging.debug("posting %s", values)
data = urllib.urlencode(values) data = urllib.urlencode(values)
req = urllib2.Request('https://api.todoist.com/TodoistSync/v' + TODOIST_VERSION + '/syncAndGetUpdated', data) req = urllib2.Request('https://api.todoist.com/TodoistSync/v' + TODOIST_VERSION + '/syncAndGetUpdated', data)
return urllib2.urlopen(req) return urllib2.urlopen(req)
def main(): def main():
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
response = GetResponse() response = GetResponse()
json_data = json.loads(response.read()) json_data = json.loads(response.read())
while True: while True:
response = GetLabels() response = GetLabels()
json_data['Labels'] = json.loads(response.read()) json_data['Labels'] = json.loads(response.read())
response = GetProjects() response = GetProjects()
json_data['Projects'] = json.loads(response.read()) json_data['Projects'] = json.loads(response.read())
logging.debug("Got initial data: %s", json_data) logging.debug("Got initial data: %s", json_data)
logging.info("*** Retrieving Data") logging.info("*** Retrieving Data")
singleton = TodoistData(json_data) singleton = TodoistData(json_data)
logging.info("*** Data built") logging.info("*** Data built")
mods = singleton.GetProjectMods() mods = singleton.GetProjectMods()
if len(mods) == 0: if len(mods) == 0:
time.sleep(5) time.sleep(5)
else: else:
logging.info("* Modifications necessary - skipping sleep cycle.") logging.info("* Modifications necessary - skipping sleep cycle.")
logging.info("** Beginning sync") logging.info("** Beginning sync")
sync_state = singleton.GetSyncState() sync_state = singleton.GetSyncState()
changed_data = DoSyncAndGetUpdated(mods, sync_state).read() changed_data = DoSyncAndGetUpdated(mods, sync_state).read()
logging.debug("Got sync data %s", changed_data) logging.debug("Got sync data %s", changed_data)
changed_data = json.loads(changed_data) changed_data = json.loads(changed_data)
logging.info("* Updating model after receiving sync data") logging.info("* Updating model after receiving sync data")
singleton.UpdateChangedData(changed_data) singleton.UpdateChangedData(changed_data)
logging.info("* Finished updating model") logging.info("* Finished updating model")
logging.info("** Finished sync") logging.info("** Finished sync")
if __name__ == '__main__': if __name__ == '__main__':
main() main()