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): def setTodoist(self, todoist):
self._todoist = todoist self._todoist = todoist
def subProjects(self): def subProjects(self):
if self._subProjects == None: if self._subProjects is None:
self._subProjects = [] self._subProjects = []
initialIndent = self.indent initialIndent = self.indent
initialOrder = self._todoist._orderedProjects.index(self) initialOrder = self._todoist._orderedProjects.index(self)
order = initialOrder + 1 order = initialOrder + 1
maxSize = len(self._todoist._orderedProjects) maxSize = len(self._todoist._orderedProjects)
if order < maxSize: if order < maxSize:
current = self._todoist._orderedProjects[order] current = self._todoist._orderedProjects[order]
while ((current.indent > initialIndent) and (order < maxSize)): while (current.indent > initialIndent) and (order < maxSize):
current = self._todoist._orderedProjects[order] current = self._todoist._orderedProjects[order]
if current != None: if current != None:
self._subProjects.append(current) self._subProjects.append(current)
current.parent = self current.parent = self
order = order + 1 order += 1
return self._subProjects
def IsIgnored(self):
return self.name.startswith('Someday') or self.name.startswith('List - ')
def IsSequential(self): return self._subProjects
ignored = self.IsIgnored()
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): def IsIgnored(self):
return self.name.endswith('=') return self.name.startswith('Someday') or self.name.startswith('List - ')
SortChildren = Item.__dict__['SortChildren'] def IsSequential(self):
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 GetItemMods(self, state): def IsParallel(self):
if self.IsSequential(): return self.name.endswith('=')
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 _CreateItemTree(self, items): SortChildren = Item.__dict__['SortChildren']
'''Build a tree of items based on their indentation level.'''
parent_item = self
previous_item = self
for item_dict in items:
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, def GetItemMods(self, state):
parent_item.content if (parent_item != None) else '') if self.IsSequential():
parent_item.children.append(item) for item in self.children:
item.parent = parent_item item.GetItemMods(state)
previous_item = item 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 _CreateItemTree(self, items):
"""Build a tree of items based on their indentation level."""
parent_item = self
previous_item = self
for item_dict in items:
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 is not 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 is not None) else 0))
parent_item = parent_item.parent
logging.debug('adding item "%s" with parent "%s"', item.content,
parent_item.content if (parent_item is not 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 self._projects.values():
project.subProjects()
def _SetLabelData(self, label_data):
# Store label data - we need this to set the next_action label.
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): for project in initial_data['Projects']:
project_timestamps = dict() p = Project(project)
for project_id, project in self._projects.iteritems(): p.setTodoist(self)
project_timestamps[project_id] = project.last_updated self._projects[project['id']] = p
return {'labels_timestamp': self._labels_timestamp,
'project_timestamps': project_timestamps}
def UpdateChangedData(self, changed_data): self._orderedProjects = sorted(self._projects.values(), key=lambda project: project.itemOrder)
if ('DayOrdersTimestamp' in changed_data
and changed_data['DayOrdersTimestamp'] != self._labels_timestamp): for project in self._projects.values():
self._SetLabelData(changed_data) project.subProjects()
# delete missing projects
if 'ActiveProjectIds' in changed_data: def _SetLabelData(self, label_data):
projects_to_delete = set(self._projects.keys()) - set(changed_data['ActiveProjectIds']) # Store label data - we need this to set the next_action label.
for project_id in projects_to_delete: self._labels_timestamp = label_data['DayOrdersTimestamp']
logging.info("Forgetting deleted project %s", self._projects[project_id].name) self._next_action_id = None
del self._projects[project_id] for label in label_data['Labels'].values():
if 'Projects' in changed_data: if label['name'] == NEXT_ACTION_LABEL:
for project in changed_data['Projects']: self._next_action_id = label['id']
logging.info("Refreshing data for project %s", project['name']) logging.info('Found next_action label, id: %s', label['id'])
if project['id'] in self._projects: if self._next_action_id is None:
logging.info("replacing project data, old timestamp: %s new timestamp: %s", logging.warning('Failed to find next_action label, need to create it.')
self._projects[project['id']].last_updated, project['last_updated'])
self._projects[project['id']] = Project(project) def GetSyncState(self):
# We have already reloaded project data sent to us. project_timestamps = dict()
# Now any project timestamps that have changed are due to the changes we for project_id, project in self._projects.iteritems():
# just sent to the server. Let's update our model. project_timestamps[project_id] = project.last_updated
if 'ActiveProjectTimestamps' in changed_data: return {'labels_timestamp': self._labels_timestamp,
for project_id, timestamp in changed_data['ActiveProjectTimestamps'].iteritems(): 'project_timestamps': project_timestamps}
# for some reason the project id is a string and not an int here.
project_id = int(project_id) def UpdateChangedData(self, changed_data):
if project_id in self._projects: if ('DayOrdersTimestamp' in changed_data
project = self._projects[project_id] and changed_data['DayOrdersTimestamp'] != self._labels_timestamp):
if project.last_updated != timestamp: self._SetLabelData(changed_data)
logging.info("Updating timestamp for project %s to %s", # delete missing projects
project.name, timestamp) if 'ActiveProjectIds' in changed_data:
project.last_updated = timestamp projects_to_delete = set(self._projects.keys()) - set(changed_data['ActiveProjectIds'])
for project_id in projects_to_delete:
logging.info("Forgetting deleted project %s", self._projects[project_id].name)
del self._projects[project_id]
if 'Projects' in changed_data:
for project in changed_data['Projects']:
logging.info("Refreshing data for project %s", project['name'])
if project['id'] in self._projects:
logging.info("replacing project data, old timestamp: %s new timestamp: %s",
self._projects[project['id']].last_updated, project['last_updated'])
self._projects[project['id']] = Project(project)
# We have already reloaded project data sent to us.
# Now any project timestamps that have changed are due to the changes we
# just sent to the server. Let's update our model.
if 'ActiveProjectTimestamps' in changed_data:
for project_id, timestamp in changed_data['ActiveProjectTimestamps'].iteritems():
# 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()