Advertisement
Guest User

Untitled

a guest
Nov 17th, 2019
99
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 9.47 KB | None | 0 0
  1. #!/usr/bin/env python
  2. import logging
  3. import argparse
  4.  
  5. # noinspection PyPackageRequirements
  6. from todoist.api import TodoistAPI
  7.  
  8. import time
  9. import sys
  10. from datetime import datetime
  11.  
  12. def chunk(iterable, chunk_size):
  13. """Generate sequences of `chunk_size` elements from `iterable`."""
  14. iterable = iter(iterable)
  15. while True:
  16. chunk = []
  17. try:
  18. for _ in range(chunk_size):
  19. chunk.append(next(iterable))
  20. yield chunk
  21. except StopIteration:
  22. if chunk:
  23. yield chunk
  24. break
  25.  
  26. class TodoistConnection(object):
  27. """docstring for TodoistConnection"""
  28. def __init__(self, args, api, logging):
  29. super(TodoistConnection, self).__init__()
  30. self.args = args
  31. self.api = api
  32. self.logging = logging
  33. self.label = None
  34.  
  35. def get_subitems(self, items, parent_item=None):
  36. """Search a flat item list for child items"""
  37. result_items = []
  38. found = False
  39. if parent_item:
  40. required_indent = parent_item['indent'] + 1
  41. else:
  42. required_indent = 1
  43.  
  44. for item in items:
  45. if parent_item:
  46. if not found and item['id'] != parent_item['id']:
  47. continue
  48. else:
  49. found = True
  50. if item['indent'] == parent_item['indent'] and item['id'] != parent_item['id']:
  51. return result_items
  52. elif item['indent'] == required_indent and found:
  53. result_items.append(item)
  54. elif item['indent'] == required_indent:
  55. result_items.append(item)
  56. return result_items
  57.  
  58. def get_project_type(self, project_object):
  59. """Identifies how a project should be handled"""
  60. name = project_object['name'].strip()
  61. if name == 'Inbox':
  62. return self.args.inbox
  63. elif name[-1] == self.args.parallel_suffix:
  64. return 'parallel'
  65. elif name[-1] == self.args.serial_suffix:
  66. return 'serial'
  67.  
  68. def get_item_type(self, item):
  69. """Identifies how a item with sub items should be handled"""
  70. name = item['content'].strip()
  71. if name[-1] == self.args.parallel_suffix:
  72. return 'parallel'
  73. elif name[-1] == self.args.serial_suffix:
  74. return 'serial'
  75.  
  76. def add_label(self, item):
  77. if self.label not in item['labels']:
  78. labels = item['labels']
  79. self.logging.debug('Updating %s with label', item['content'])
  80. labels.append(self.label)
  81. self.api.items.update(item['id'], labels=labels)
  82.  
  83. def remove_label(self, item):
  84. if self.label in item['labels']:
  85. labels = item['labels']
  86. self.logging.debug('Updating %s without label', item['content'])
  87. labels.remove(self.label)
  88. self.api.items.update(item['id'], labels=labels)
  89.  
  90. def insert_serial_item(self, serial_items, item):
  91. if len(serial_items):
  92. if item['item_order'] < serial_items[-1]['item_order']:
  93. serial_items.insert(0, item)
  94. else:
  95. serial_items.append(item)
  96. else:
  97. serial_items.append(item)
  98.  
  99. return serial_items
  100.  
  101. # Main loop
  102.  
  103. def main():
  104.  
  105. parser = argparse.ArgumentParser()
  106. parser.add_argument('-a', '--api_key', help='Todoist API Key')
  107. parser.add_argument('-l', '--label', help='The next action label to use', default='next_action')
  108. parser.add_argument('-d', '--delay', help='Specify the delay in seconds between syncs', default=5, type=int)
  109. parser.add_argument('--debug', help='Enable debugging', action='store_true')
  110. parser.add_argument('--inbox', help='The method the Inbox project should be processed',
  111. default='parallel', choices=['parallel', 'serial'])
  112. parser.add_argument('--parallel_suffix', default='.')
  113. parser.add_argument('--serial_suffix', default='_')
  114. parser.add_argument('--hide_future', help='Hide future dated next actions until the specified number of days',
  115. default=7, type=int)
  116. parser.add_argument('--onetime', help='Update Todoist once and exit', action='store_true')
  117. args = parser.parse_args()
  118.  
  119. # Set debug
  120. if args.debug:
  121. log_level = logging.DEBUG
  122. else:
  123. log_level = logging.INFO
  124. logging.basicConfig(level=log_level)
  125.  
  126. # Check we have a API key
  127. if not args.api_key:
  128. logging.error('No API key set, exiting...')
  129. sys.exit(1)
  130.  
  131. # Run the initial sync
  132. logging.debug('Connecting to the Todoist API')
  133. api = TodoistAPI(token=args.api_key)
  134. conn = TodoistConnection(args, api, logging)
  135.  
  136. conn.logging.debug('Syncing the current state from the API')
  137. conn.api.sync()
  138.  
  139. # Check the next action label exists
  140. labels = conn.api.labels.all(lambda x: x['name'] == args.label)
  141. if len(labels) > 0:
  142. label_id = labels[0]['id']
  143. conn.logging.debug('Label %s found as label id %d', args.label, label_id)
  144. else:
  145. conn.logging.error("Label %s doesn't exist, please create it or change TODOIST_NEXT_ACTION_LABEL.", args.label)
  146. sys.exit(1)
  147.  
  148. conn.label = label_id
  149.  
  150. while True:
  151. try:
  152. conn.api.sync()
  153. except Exception as e:
  154. conn.logging.exception('Error trying to sync with Todoist API: %s' % str(e))
  155. else:
  156. for project in conn.api.projects.all():
  157. project_type = conn.get_project_type(project)
  158. if project_type:
  159. conn.logging.debug('Project %s being processed as %s', project['name'], project_type)
  160.  
  161. items = sorted(conn.api.items.all(lambda x: x['project_id'] == project['id']), key=lambda x: x['item_order'])
  162.  
  163. # for cases when a task is completed and the lowe task
  164. #is not 1
  165. serial_items = []
  166.  
  167. for item in items:
  168.  
  169. # If its too far in the future, remove the next_action tag and skip
  170. if conn.args.hide_future > 0 and 'due_date_utc' in item.data and item['due_date_utc'] is not None:
  171. due_date = datetime.strptime(item['due_date_utc'], '%a %d %b %Y %H:%M:%S +0000')
  172. future_diff = (due_date - datetime.utcnow()).total_seconds()
  173. if future_diff >= (conn.args.hide_future * 86400):
  174. conn.remove_label(item)
  175. continue
  176.  
  177. item_type = conn.get_item_type(item)
  178. child_items = conn.get_subitems(items, item)
  179.  
  180. if item_type:
  181. conn.logging.debug('Identified %s as %s type', item['content'], item_type)
  182.  
  183. if project_type == 'serial':
  184. serial_items = conn.insert_serial_item(serial_items, item)
  185.  
  186. if len(child_items) > 0:
  187. # Process parallel tagged items or untagged parents
  188. for child_item in child_items:
  189. conn.add_label(child_item)
  190. # Remove the label from the parent
  191. conn.remove_label(item)
  192. # Process items as per project type on indent 1 if untagged
  193. else:
  194. if project_type == 'parallel':
  195. conn.add_label(item)
  196.  
  197. if len(serial_items):
  198. # Label to first item may not necessarily be in pos 1
  199. s_item = serial_items.pop(0)
  200. item_type = conn.get_item_type(s_item)
  201. child_items = conn.get_subitems(items, s_item)
  202.  
  203. if len(child_items) > 0:
  204. if item_type == 'serial':
  205. for idx, child_item in enumerate(child_items):
  206. if idx == 0:
  207. conn.add_label(child_item)
  208. else:
  209. conn.remove_label(child_item)
  210. conn.remove_label(s_item)
  211.  
  212. for child_item in child_items:
  213. serial_items.remove(child_item)
  214.  
  215. else:
  216. conn.add_label(s_item)
  217.  
  218. # Remove labels for items following
  219. for s_item in serial_items:
  220. conn.remove_label(s_item)
  221. child_items = conn.get_subitems(items, s_item)
  222. for child_item in child_items:
  223. conn.remove_label(child_item)
  224.  
  225.  
  226. conn.logging.debug('%d changes queued for sync... commiting if needed', len(conn.api.queue))
  227. if len(conn.api.queue):
  228. queue = conn.api.queue
  229. for ch in chunk(queue, 100):
  230. conn.api.queue = ch
  231. conn.api.commit()
  232.  
  233. if conn.args.onetime:
  234. break
  235. conn.logging.debug('Sleeping for %d seconds', conn.args.delay)
  236. time.sleep(conn.args.delay)
  237.  
  238.  
  239. if __name__ == '__main__':
  240. main()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement