SHARE
TWEET

Untitled

a guest Nov 17th, 2019 73 Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  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()
RAW Paste Data
We use cookies for various purposes including analytics. By continuing to use Pastebin, you agree to our use of cookies as described in the Cookies Policy. OK, I Understand
 
Top