def test_CreateTask(self): # read api token values apitoken = config.get('todoistinfo', 'apitoken') # read created task name taskname = config.get('todoistinfo', 'taskname') # read created task ID taskid = config.get('todoistinfo', 'taskid') # Complete Task api = TodoistAPI(apitoken) api.sync() created_task = api.items.get_by_id(int(taskid)) created_task.complete() response = api.commit() # Verify if the task has been completed successfully assert response['items'][0]['content'] == taskname assert response['items'][0]['checked'] == 1 assert 1 in [ i['checked'] for i in api.state['items'] if i['id'] == int(taskid) ] # Reopen Task created_task.uncomplete() response = api.commit() # Verify if the task has been reopened successfully assert response['items'][0]['content'] == taskname assert response['items'][0]['checked'] == 0 assert 0 in [ i['checked'] for i in api.state['items'] if i['id'] == int(taskid) ]
def main(event, context): body = { "tasksUpdated": 0 } API_TOKEN = get_token() if not API_TOKEN: logging.warn('Please set the API token in environment variable.') exit() today = datetime.utcnow().replace(tzinfo=None) api = TodoistAPI(token=API_TOKEN, cache="/tmp/todoist") api.sync() tasks = api.state['items'] for task in tasks: if task['due_date_utc'] and is_habit(task['content']): if is_today(task['due_date_utc']): habit = is_habit(task['content']) streak = int(habit.group(1)) + 1 update_streak(task, streak) body["tasksUpdated"] += 1 elif is_due(task['due_date_utc']): update_streak(task, 0) task.update(date_string='ev day starting tod') api.commit() response = { "statusCode": 200, "body": json.dumps(body) } return response
def instantiate(project_name, loglevel, date_str): api = TodoistAPI(get_api_token()) api.sync() logging.basicConfig(level=getattr(logging, loglevel.upper())) project_id = get_project_id(api, project_name) date = None if date_str is None else dateutil.parser.parse(date_str).date() logging.debug(f"date = {date!r}") tasks = api.projects.get_data(project_id)['items'] instantiable_tasks = [ task for task in tasks if is_instantiable(task, date) ] action_count = 0 for task in instantiable_tasks: clone_task(api, task) postpone_task(api, task) action_count += 2 # The Todoist API will reject a commit if there are more than 100 # changes bundled together. Just to give myself some buffer in case # I'm miscounting actions, I commit everytime the action count gets # above 80. if action_count > 80: r = api.commit() logging.debug(f"commit = {r!r}") action_count = 0 r = api.commit() logging.debug(f"commit = {r!r}")
def handle_payload(request): if request.method == 'POST': received_json_data = json.loads( request.body) # convert json data to python dict if 'events' in received_json_data.keys(): events = received_json_data["events"] for event in events: id = event["resource"] type = event["type"] source = None # by defualt make url-source none if type == "task": source = "https://app.asana.com/api/1.0/tasks/" + str(id) if not source == None: data = requests.get(url=source, headers={"Authorisation": "Bearer "}) jsdata = json.loads(data) if jsdata['data']['assignee']['name'] == 'Stebin Ben': task_name = jsdata['data']['name'] api = TodoistAPI( '7441fc19459367f6d69efaa79175461b521e257d') api.sync() project_name = jsdata['data']['memberships'][ 'project']['name'] project1 = api.projects.add(project_name) api.items.add(task_name, project1['id']) api.commit() else: headers = request.headers x_hook = headers['X-Hook-Secret'] return JsonResponse({"X-Hook-Sercret": x_hook})
def populate_shopping_list(locked_recipes_yaml): data = grocery_list.get_grocery_list(locked_recipes_yaml) groups = {} for item in data.values(): group_name = item.pop('group') if group_name not in groups: groups[group_name] = [] groups[group_name].append(item) # Use Sync API instead of REST API. Sync API is faster, since we can batch # add items, while with the REST API, we can only add them one by one. api = TodoistAPI(SECRETS["todoist_api_token"]) sections = api.projects.get_data(SHOPPING_LIST_ID)['sections'] section_name_id_map = {} for section in sections: section_name_id_map[section['name']] = section['id'] # Sections on Todoist must match those given in the config.yaml assert set(groups.keys()).issubset(set(section_name_id_map.keys())) print('Creating shopping list') for section_name, group in groups.items(): section_id = section_name_id_map[section_name] for item in group: api.items.add( content=item['name'], description=f"{item['amount']} {item['unit']}", project_id=SHOPPING_LIST_ID, section_id=section_id, ) api.commit()
def clear_shopping_list(): print('Clearing shopping list') api = TodoistAPI(SECRETS["todoist_api_token"]) items = api.projects.get_data(SHOPPING_LIST_ID)['items'] for item in items: api.items.get_by_id(item['id']).delete() api.commit()
class Todoist(Todolist): def __init__(self, list_name=None): print("Initializing Todoist API...") self.api = TodoistAPI(settings.todoist_key) # TODO: other users self.api.sync() self.Project_ID = settings.todoist_project_key# TODO: add option for other projects self.project_name = list_name '''if list_name is not None: projects = self.api.projects for project in projects.all('name = ' + list_name): if project['name'] == list_name: self.Project_ID = project['id']''' def add_task(self, name, desc=None, labels=None, due=None, assign=None): if labels is not None: for label in labels: name = name + ' @' + label if self.project_name is not None: name = name + ' #' + self.project_name if due is None: task = self.api.items.add(name, self.Project_ID) else: due = due - timedelta(hours=4) task = self.api.items.add(name, self.Project_ID, due_date_utc=due) task_id = task['id'] # PREMIUM! #if desc is not None: # self.api.notes.add(task_id, desc) print("\tAdded task: Name - " + name) self.api.commit()
def main(): API_TOKEN = get_token() today = datetime.utcnow().replace(tzinfo=None) if not API_TOKEN: logging.error('Please set the API token in environment variable.') exit() api = TodoistAPI(API_TOKEN) api.sync() project_id = get_project(api) tasks = api.state['items'] for task in tasks: content = task['content'] if all([ task['due_date_utc'], is_habit(content), not project_id or task['project_id'] == project_id ]): logger.info("Found task id:%s content:%s", task['id'], content[:20]) date_string = task['date_string'] or 'ev day' task_id = task['id'] due_at = parse(task['due_date_utc'], ignoretz=True) days_left = due_at.date() - today.date() if days_left: habit = is_habit(content) streak = int(habit.group(1)) + 1 update_streak(task, streak) api.notes.add(task_id, '[BOT] Streak extended. Yay!') else: update_streak(task, 0) task.update(date_string=date_string + ' starting tod') api.notes.add(task_id, '[BOT] Chain broken :(') api.commit()
class Todoist(): def __init__(self, api_key): self.api = TodoistAPI(api_key) def projToId(self, project): for proj in self.api.state['projects']: if proj['name'].strip() == project.strip(): return proj['id'] def addTaskProj(self, name, project): self.api.sync() item = self.api.items.add(name, self.projToId(project)) self.api.commit() return item def getTasks(self, fil=TaskFillters.defult): self.api.sync() return [i for i in self.api.state['items'] if fil(vars(i)['data'])] def getProj(self, proj): proj = self.projToId(proj) return self.getTasks(TaskFillters.taskInProj(proj)) def getProjDue(self, proj): proj = self.projToId(proj) return self.getTasks(TaskFillters.taskProjDue(proj)) def deleteTask(self, ID): self.api.sync() self.api.items.get_by_id(ID).delete() self.api.commit()
def main(): print("Loading config...") with open('/home/pi/todoist-ordering/config.json', 'r') as f: config = json.load(f) api = TodoistAPI(config['apikey']) api.sync() projects_to_sort = config['projects'] print("Sorting projects") for project in api.state['projects']: name = project['name'] if name in projects_to_sort.keys(): sortkeys = projects_to_sort[name] result = getTasksInProject(project['id'], api.state['items']) # for item in result: # print("Item Content: (" + item['content'] + ")") for sortkey in sortkeys: sortedResult = sortTasks(result, sortkey) print("Sorting project: '" + project['name'] + "' by key: '" + sortkey + "'") thisOrder = 0 for item in sortedResult: api.items.reorder([{ 'id': item['id'], 'child_order': thisOrder }]) thisOrder += 1 # print("After sort...") # for item in result: # print("Item Content: (" + item['content'] + ")") print("Committing...") else: print("No config for project: '" + project['name'] + "'") api.commit() print("DONE!")
def todoist_task_uploader(cal, fin): """ Method to upload/create tasks with Todoist API Args: cal (string): [name of the project or calendar] fin (string): [path to the file] """ school_pid = '' subject_count = 0 task_count = 0 # Connect with Todoist API api = TodoistAPI(os.getenv('API_TOKEN')) logging.info('Connection established!') api.sync() # Search for desire Project (SCHOOL) for project in api.state['projects']: if 'SCHOOL' in project['name']: school_pid = project['id'] # read file logging.info('Reading file...') wb = load_workbook(fin) logging.info('Creating new project...') # Create new Semester Project semester = api.projects.add('Semester ' + cal, parent_id=school_pid, color=randint(30, 49)) for materia in wb.worksheets: # Create Section per Subject(Worksheet) subject = materia.title.split('-') # worksheet in format: Subject-label section = api.sections.add(subject[0], semester['id']) new_label = api.labels.add(subject[1]) label_id = 2155146353 if subject[1] == 'proyecto' else new_label['id'] subject_count += 1 logging.info('Creating section: ' + subject[0]) logging.info('Creating tasks...') for task in materia.values: # Create Tasks Per Section api.items.add(task[0], due = { "string": task[1], "date": task[1], # datetime must be text in excel with format: 2021-08-10 not 2021-8-10 "recurring": False, }, section_id = section['id'], description = task[2], # type of activity: foro or buzon labels = [2155113156, label_id]) # label 'INICIO' and section label task_count += 1 # Commit Changes and Sync logging.info('Committing changes to API...') api.commit() logging.info("Sync'ing changes to API...") api.sync() # Print successful message logging.info('Successfully created {} section(s) with {} task(s).'.format(subject_count, task_count)) logging.info('Work done!')
def add_data(api_key, issues): """ add tasks to Todoist INBOX due today """ api = TodoistAPI(api_key) api.sync() for v in issues: api.items.add(v, due={"string": "today"}) api.commit()
def create_shop_project(key=tovakey): api = TodoistAPI(key) api.sync() payload = request.get_json() shop_name = payload["context"]["facts"]["shop_name"]["grammar_entry"] added_project = api.projects.add(shop_name) api.commit() print("ADDED PROJECT:", added_project) return action_success_response()
def CreateTodoistTask(taskname): # initialize the API object token = 'YOUR_TD_TOKEN' # Todoist API token api = TodoistAPI(token) # Create a new task api.items.add(taskname, None, date_string='today') # default task setup # commit api.commit()
def add_task(task_content: str, date: str, priority: int = 4): """ Add a new task to todoist. :param priority: Priority of the task :param task_content: Task description :param date: Due date for task """ api = TodoistAPI(TODOIST_APIKEY) api.sync() api.items.add(task_content, date_string=date, priority=priority, section_id=INVESTMENT_SECTION_ID) api.commit()
def add_tasks(cache): PROJECT_ID = os.environ.get("TODOIST_TV_PROJECT_ID") api = TodoistAPI(API_KEY) api.sync() for show in cache.get("_shows_updated"): api.items.add(getTaskForShow(cache.get(show)), PROJECT_ID) cache["_shows_updated"] = set() for film in cache.get("_films_updated"): api.items.add(film, PROJECT_ID) cache["_films_updated"] = set() api.commit()
def create_todoist_clockout_task(): now = datetime.datetime.now().astimezone(timezone('Europe/Amsterdam')) soll = datetime.timedelta(hours=REQUIRED_WORKING_HOURS, minutes=WORKING_MINUTES) + now acc = datetime.timedelta(hours=WORKING_HOURS, minutes=WORKING_MINUTES) + now clockin_time = str(now.hour) + ':' + str('%02d' % now.minute) clockout_time = str(acc.hour) + ':' + str('%02d' % acc.minute) soll_time = str(soll.hour) + ':' + str('%02d' % soll.minute) task_content = 'Gehen (Gekommen: ' + clockin_time + ', Soll erreicht: ' + soll_time + ')' logger.info('Create Todoist Clock-Out Task: ' + task_content + ' due at: ' + clockout_time) api = TodoistAPI(token=get_token(), cache="/tmp/todoist") logger.info("Trying to connect to Todoist API with: %s", get_token()) if not api.sync(): logger.warning('Todoist: API Sync failed') exit() api.items.add(task_content, project_id='178923234', date_string=clockout_time, labels=[2147513595], priority=3) if api.commit(): logger.info("Todoist Clock-Out Task has been created")
def test_CreateProject(self): #read api token values apitoken = config.get('todoistinfo', 'apitoken') # read project name projectname = config.get('todoistinfo', 'projectname') ''' Create project by using API commands ''' api = TodoistAPI(apitoken) api.sync() create_project = api.projects.add(projectname) response = api.commit() # Verify if the project has been created successfully assert response['projects'][0]['name'] == projectname assert projectname in [p['name'] for p in api.state['projects']] assert api.projects.get_by_id(create_project['id']) == create_project # update the created project ID to ini file config.set("todoistinfo", "projectid", str(create_project['id'])) config.write(open('../test_info.ini', 'w')) #Login to mobile apps and verify the project has been created successfully Todoist.Login(self) sleep(6) Todoist.Verify_CreatedProject(self, projectname)
def create_todoist_lastmeal_task(): now = datetime.datetime.now().astimezone(timezone('Europe/Amsterdam')) cleared_time = datetime.timedelta(hours=INTERMITTENT_FASTING_HOURS) + now checkin_time = str(now.hour) + ':' + str('%02d' % now.minute) checkout_time = str(cleared_time.hour) + ':' + str( '%02d' % cleared_time.minute) task_content = ' Intermittent Fasting - Cleared to eat (Check in: ' + checkin_time + ')' logger.info('Create Todoist LastMeal Task: ' + task_content + ' due at: ' + checkout_time) api = TodoistAPI(token=get_token(), cache="/tmp/todoist") logger.info("Trying to connect to Todoist API with: %s", get_token()) if not api.sync(): logger.warning('Todoist: API Sync failed') exit() api.items.add(task_content, project_id='1509802153', date_string=checkout_time, labels=[2154004914], priority=3) if api.commit(): logger.info("Todoist LastMeal Task has been created")
def test_CreateTask(self): # read api token values apitoken = config.get('todoistinfo', 'apitoken') # read task name taskname = config.get('todoistinfo', 'taskname') # read created project ID projectid = config.get('todoistinfo', 'projectid') ''' Create task by using API commands ''' api = TodoistAPI(apitoken) api.sync() create_task = api.items.add(taskname, projectid) response = api.commit() # Verify if the task has been created successfully assert create_task[ 'content'] == taskname, "Task created unsuccessfully." assert taskname in [i['content'] for i in api.state['items']] assert api.items.get_by_id(create_task['id']) == create_task # update the created task ID to ini file config.set("todoistinfo", "taskid", str(create_task['id'])) config.write(open('../test_info.ini', 'w'))
def main(): API_TOKEN = get_token() if not API_TOKEN: logging.warn('Please set the API token in environment variable.') exit() api = TodoistAPI(API_TOKEN) api.sync() tasks = api.state['items'] for task in tasks: if task['due_date_utc'] and is_habit(task['content']): if is_today(task['due_date_utc']): habit = is_habit(task['content']) streak = int(habit.group(1)) + 1 update_streak(task, streak) elif is_due(task['due_date_utc']): update_streak(task, 0) task.update(date_string='ev day starting tod') api.commit()
class Todoist(object): def __init__(self): self.api = TodoistAPI(get_token()) self.api.sync() self.api.notes = NotesManager(self.api) habit_label_ids = [ label['id'] for label in self.api.state['labels'] if label['name'] == 'habit' ] assert (len(habit_label_ids) == 1) self.habit_label_id = habit_label_ids[0] self.habits = self.get_habits() self.get_datetime() def get_datetime(self): timezone = self.api.state['user']['tz_info']['timezone'] tz_location = tz.gettz(timezone) now = datetime.now(tz=tz_location) self.weekstart = datetime.weekday(now) == self.api.state['user'][ 'start_day'] % 7 #Checks if yesterday was the week start day self.off_day = datetime.weekday(now) in [ i % 7 for i in self.api.state['user']['days_off'] ] #Checks if yesterday was an off day self.today = now.strftime(TODOIST_DATE_FORMAT) def get_habits(self): habits = [] for item in self.api.state['items']: if self.habit_label_id in item['labels']: habits.append(item) return habits def update_habit(self): for item in self.habits: notes = [ note for note in self.api.state['notes'] if note['item_id'] == item['id'] ] task = Task(self.api, item, notes) if task.is_due(self.today): task.no_change(self.today, self.weekstart, self.off_day) else: task.increase(self.weekstart) self.api.commit()
def color_code(key=tovakey): api = TodoistAPI(key) api.sync() payload = request.get_json() selected_project = payload["context"]["facts"]["selected_project"][ "grammar_entry"] selected_color = ( payload["context"]["facts"]["selected_color"]["value"]).split('_')[1] print("SELECTED_COLOR: ", selected_color) projects = extract_projects(api) project_found = False for project in projects: if project[0].lower() == selected_project.lower(): project_found = True print("project found") target_project = api.projects.get_by_id(project[1]) target_project.update(color=selected_color) api.commit() return action_success_response()
def main(): API_TOKEN = get_token() today = datetime.utcnow().replace(tzinfo=None) if not API_TOKEN: logging.warn('Please set the API token in environment variable.') exit() api = TodoistAPI(API_TOKEN) api.sync() tasks = api.state['items'] for task in tasks: if task['due'] and is_habit(task['content']) and not task['in_history']: if is_today(task['due']['date']): habit = is_habit(task['content']) streak = int(habit.group(1)) + 1 update_streak(task, streak) elif is_due(task['due']['date']): update_streak(task, 0) task.update(due={'string': 'ev day starting {}'.format(today_date())}) api.commit()
def complete_task(key=tovakey): api = TodoistAPI(key) api.sync() payload = request.get_json() selected_project = payload["context"]["facts"]["selected_project"][ "grammar_entry"] selected_task = payload["context"]["facts"]["selected_task"][ "grammar_entry"] projects = extract_projects(api) for project in projects: if project[0].lower() == selected_project.lower(): print("project found") items = api.projects.get_data(project[1]) for value in items['items']: if value['content'] == selected_task: print("task found") task_id = value['id'] item = api.items.get_by_id(task_id) item.complete() api.commit() return action_success_response()
def main(): API_TOKEN = get_token() if not API_TOKEN: logging.warning('Please set the API token in environment variable.') exit() api = TodoistAPI(API_TOKEN) api.sync() tasks = api.state['items'] for task in tasks: habit = is_habit(task['content']) if task['due'] and habit and not task['in_history']: due_date = task['due']['date'] if is_today(due_date): streak = int(habit.group(1)) + 1 update_streak(task, streak) elif is_due(due_date): streak = int(habit.group(1)) - 1 streak = max(0, streak) update_streak(task, streak) task.update(due={'string': 'ev day starting {}'.format(today_date())}) api.commit()
def addNewIssueToTodoist(repoFullName, username, issueTitle): api = TodoistAPI(TODOIST_API_TOKEN) api.sync() todoProject = api.projects.get_by_id(find_project_by_name(repoFullName)) print(todoProject) if (todoProject != None): if username == 'LSWarss': if api.items.get_by_id(find_task_by_name(issueTitle) != None): task = api.items.add(issueTitle, project_id=todoProject['id']) print(task) api.commit() else: pass else: todoProject = api.projects.add(repoFullName) if username == 'LSWarss': if api.items.get_by_id(find_task_by_name(issueTitle) != None): task = api.items.add(issueTitle, project_id=todoProject['id']) api.commit() else: pass
class Todoist(object): def __init__(self): self.api = TodoistAPI(get_token()) self.api.sync() @property def today(self): timezone = self.api.state['user']['tz_info']['timezone'] tz_location = tz.gettz(timezone) now = datetime.now(tz=tz_location) return now.strftime(TODOIST_DATE_FORMAT) def update_streak(self): items = self.api.state['items'] for item in items: task = Task(item) if task.is_habit(): if task.is_due(self.today): task.decrease(self.today) else: task.increase() self.api.commit()
def lambda_handler(event, context): #Begin print(">>>BEGIN<<<") p = os.environ['todoistkey'] api = TodoistAPI(p) api.sync() for project in event['projects']: print(">ID:" + str(project['id'])) print(">Sortby:" + project['sortby']) result = getTasksInProject(project['id'], api.state['items']) print("===Item count===") print(len(result)) print("==sort items==") sortedResult = sortTasks(result, project['sortby']) thisOrder = 0 for item in sortedResult: api.items.reorder([{'id': item['id'], 'child_order': thisOrder}]) thisOrder += 1 api.commit() print(">>>END<<<") return {"ordered": 1}
def handle_payload(request): if request.method == 'POST': received_json_data = json.loads(request.body) if 'events' in received_json_data.keys(): events = received_json_data["events"] for event in events: id = event["resource"] type = event["type"] url = None if type == "task": url = "https://app.asana.com/api/1.0/tasks/" + str(id) if not url == None: data = requests.get( url=url, headers={ "Authorization": "Bearer 0/92f79995f7c1b4b0f751b9a07c8e52c2" }) data1 = json.loads(data) if data1['data']['assignee']['name'] == 'Yukta Anand': task_name = data1['data']['name'] api = TodoistAPI( '27113e1797906a73a11a7358805ac473499c414e') api.sync() project_name = data1['data']['memberships']['project'][ 'name'] project1 = api.projects.add(project_name) api.items.add(task_name, project1['id']) api.commit() else: headers = request.headers x_hook = headers['X-Hook-Secret'] return JsonResponse({"X-Hook-Secret": x_hook})
def main(): """Main process function.""" parser = argparse.ArgumentParser() parser.add_argument('-a', '--api_key', help='Todoist API Key') parser.add_argument('-l', '--label', help='The next action label to use', default='next_action') parser.add_argument('-d', '--delay', help='Specify the delay in seconds between syncs', default=5, type=int) parser.add_argument('--debug', help='Enable debugging', action='store_true') parser.add_argument('--inbox', help='The method the Inbox project should be processed', default='parallel', choices=['parallel', 'serial', 'none']) parser.add_argument('--parallel_suffix', default='.') parser.add_argument('--serial_suffix', default='_') parser.add_argument('--hide_future', help='Hide future dated next actions until the specified number of days', default=7, type=int) parser.add_argument('--hide_scheduled', help='', action='store_true') # TODO: help parser.add_argument('--remove_label', help='Remove next action label from unmarked projects', action='store_true') parser.add_argument('--onetime', help='Update Todoist once and exit', action='store_true') parser.add_argument('--nocache', help='Disables caching data to disk for quicker syncing', action='store_true') args = parser.parse_args() # Set debug if args.debug: log_level = logging.DEBUG else: log_level = logging.INFO logging.basicConfig(level=log_level) # Check we have a API key if not args.api_key: logging.error('No API key set, exiting...') sys.exit(1) # Run the initial sync logging.debug('Connecting to the Todoist API') api_arguments = {'token': args.api_key} if args.nocache: logging.debug('Disabling local caching') api_arguments['cache'] = None api = TodoistAPI(**api_arguments) logging.debug('Syncing the current state from the API') api.sync() # Check the next action label exists labels = api.labels.all(lambda x: x['name'] == args.label) if len(labels) > 0: label_id = labels[0]['id'] logging.debug('Label %s found as label id %d', args.label, label_id) else: logging.error("Label %s doesn't exist, please create it or change TODOIST_NEXT_ACTION_LABEL.", args.label) sys.exit(1) def get_project_type(project_object): """Identifies how a project should be handled.""" name = project_object['name'].strip() if name == 'Inbox' and args.inbox != 'none': return args.inbox elif name[-1] == args.parallel_suffix: return 'parallel' elif name[-1] == args.serial_suffix: return 'serial' def get_item_type(item): """Identifies how a item with sub items should be handled.""" name = item['content'].strip() if name[-1] == args.parallel_suffix: return 'parallel' elif name[-1] == args.serial_suffix: return 'serial' def add_label(item, label): if label not in item['labels']: logging.debug('Updating %s (%d) with label', item['content'], item['id']) labels = item['labels'] labels.append(label) api.items.update(item['id'], labels=labels) def remove_label(item, label): if label in item['labels']: logging.debug('Updating %s (%d) without label', item['content'], item['id']) labels = item['labels'] labels.remove(label) api.items.update(item['id'], labels=labels) # Main loop while True: try: api.sync() except Exception as e: logging.exception('Error trying to sync with Todoist API: %s' % str(e)) else: for project in api.projects.all(lambda x: not x['is_deleted'] and not x['is_archived']): project_type = get_project_type(project) items = api.items.all( lambda x: x['project_id'] == project['id'] and not (x['checked'] or x['is_deleted'] or x['is_archived']) ) if project_type: logging.debug('Project %s being processed as %s', project['name'], project_type) # Get all items for the project, sort by the item_order field. items = sorted( items, key=lambda x: x['item_order'] ) for real_order, item in enumerate(filter(lambda x: x['indent'] == 1, items)): item_type = get_item_type(item) if item.data.get('due_date_utc'): if args.hide_scheduled and not item_type: remove_label(item, label_id) continue # If its too far in the future, remove the next_action tag and skip if args.hide_future > 0: due_date = datetime.strptime(item['due_date_utc'], '%a %d %b %Y %H:%M:%S +0000') future_diff = (due_date - datetime.utcnow()).total_seconds() if future_diff >= (args.hide_future * 86400): remove_label(item, label_id) continue child_items = get_subitems(items, item) def add_indent1_label(): if child_items: remove_label(item, label_id) add_label(child_items[0], label_id) func = remove_label if item_type == 'serial' else add_label for child_item in child_items[1:]: func(child_item, label_id) else: add_label(item, label_id) def remove_indent1_label(): remove_label(item, label_id) for child_item in child_items: remove_label(child_item, label_id) if project_type == 'serial': if real_order == 0: add_indent1_label() else: remove_indent1_label() elif project_type == 'parallel': add_indent1_label() elif args.remove_label: for item in items: remove_label(item, label_id) if len(api.queue): logging.debug('%d changes queued for sync... commiting to Todoist.', len(api.queue)) api.commit() else: logging.debug('No changes queued, skipping sync.') # If onetime is set, exit after first execution. if args.onetime: break logging.debug('Sleeping for %d seconds', args.delay) time.sleep(args.delay)
def main(): parser = argparse.ArgumentParser() parser.add_argument('-a', '--api_key', help='Todoist API Key') parser.add_argument('-l', '--label', help='The next action label to use', default='next_action') parser.add_argument('-d', '--delay', help='Specify the delay in seconds between syncs', default=5, type=int) parser.add_argument('--debug', help='Enable debugging', action='store_true') parser.add_argument('--inbox', help='The method the Inbox project should be processed', default='parallel', choices=['parallel', 'serial']) parser.add_argument('--parallel_suffix', default='.') parser.add_argument('--serial_suffix', default='_') parser.add_argument('--hide_future', help='Hide future dated next actions until the specified number of days', default=7, type=int) parser.add_argument('--onetime', help='Update Todoist once and exit', action='store_true') args = parser.parse_args() # Set debug if args.debug: log_level = logging.DEBUG else: log_level = logging.INFO logging.basicConfig(level=log_level) # Check we have a API key if not args.api_key: logging.error('No API key set, exiting...') sys.exit(1) # Run the initial sync logging.debug('Connecting to the Todoist API') api = TodoistAPI(token=args.api_key) logging.debug('Syncing the current state from the API') api.sync(resource_types=['projects', 'labels', 'items']) # Check the next action label exists labels = api.labels.all(lambda x: x['name'] == args.label) if len(labels) > 0: label_id = labels[0]['id'] logging.debug('Label %s found as label id %d', args.label, label_id) else: logging.error("Label %s doesn't exist, please create it or change TODOIST_NEXT_ACTION_LABEL.", args.label) sys.exit(1) def get_project_type(project_object): """Identifies how a project should be handled""" name = project_object['name'].strip() if project['name'] == 'Inbox': return args.inbox elif name[-1] == args.parallel_suffix: return 'parallel' elif name[-1] == args.serial_suffix: return 'serial' def get_item_type(item): """Identifies how a item with sub items should be handled""" name = item['content'].strip() if name[-1] == args.parallel_suffix: return 'parallel' elif name[-1] == args.serial_suffix: return 'serial' def add_label(item, label): if label not in item['labels']: labels = item['labels'] logging.debug('Updating %s with label', item['content']) labels.append(label) api.items.update(item['id'], labels=labels) def remove_label(item, label): if label in item['labels']: labels = item['labels'] logging.debug('Updating %s without label', item['content']) labels.remove(label) api.items.update(item['id'], labels=labels) # Main loop while True: try: api.sync(resource_types=['projects', 'labels', 'items']) except Exception as e: logging.exception('Error trying to sync with Todoist API: %s' % str(e)) else: for project in api.projects.all(): project_type = get_project_type(project) if project_type: logging.debug('Project %s being processed as %s', project['name'], project_type) items = sorted(api.items.all(lambda x: x['project_id'] == project['id']), key=lambda x: x['item_order']) for item in items: # If its too far in the future, remove the next_action tag and skip if args.hide_future > 0 and 'due_date_utc' in item.data and item['due_date_utc'] is not None: due_date = datetime.strptime(item['due_date_utc'], '%a %d %b %Y %H:%M:%S +0000') future_diff = (due_date - datetime.utcnow()).total_seconds() if future_diff >= (args.hide_future * 86400): remove_label(item, label_id) continue item_type = get_item_type(item) child_items = get_subitems(items, item) if item_type: logging.debug('Identified %s as %s type', item['content'], item_type) if item_type or len(child_items) > 0: # Process serial tagged items if item_type == 'serial': for idx, child_item in enumerate(child_items): if idx == 0: add_label(child_item, label_id) else: remove_label(child_item, label_id) # Process parallel tagged items or untagged parents else: for child_item in child_items: add_label(child_item, label_id) # Remove the label from the parent remove_label(item, label_id) # Process items as per project type on indent 1 if untagged else: if item['indent'] == 1: if project_type == 'serial': if item['item_order'] == 1: add_label(item, label_id) else: remove_label(item, label_id) elif project_type == 'parallel': add_label(item, label_id) logging.debug('%d changes queued for sync... commiting if needed', len(api.queue)) if len(api.queue): api.commit() if args.onetime: break logging.debug('Sleeping for %d seconds', args.delay) time.sleep(args.delay)
class NextAction(object): def __init__(self): self.args = None self.api = None self.next_label_id = None self.waitfor_label_id = None self.active_label_id = None def main(self): self.setup() self.loop() def check_label(self, label): # Check if the label exists labels = self.api.labels.all(lambda x: x['name'] == label) if len(labels) > 0: label_id = labels[0]['id'] logging.debug('Label %s found as label id %d', label, label_id) return label_id else: logging.error( "Label %s doesn't exist.", label) sys.exit(1) def setup(self): self.parse_args() # Run the initial sync logging.debug('Connecting to the Todoist API') self.api = TodoistAPI(token=self.args.api_key) logging.debug('Syncing the current state from the API') self.api.sync() self.next_label_id = self.check_label(self.args.label) self.active_label_id = self.check_label(self.args.active) self.waitfor_label_id = self.check_label(self.args.waitfor) def loop(self): """ Main loop """ while True: try: self.api.sync() except Exception as exc: logging.exception('Error trying to sync with Todoist API: %s', exc) else: self.process(self.api.projects.all()) logging.debug( '%d changes queued for sync... committing if needed', len(self.api.queue)) if len(self.api.queue): self.api.commit() if self.args.onetime: break logging.debug('Sleeping for %d seconds', self.args.delay) time.sleep(self.args.delay) def process(self, projects, parent_indent=0, parent_type=None): """ Process all projects """ current_type = parent_type while projects and parent_indent < projects[0]["indent"]: # dig deeper if projects[0]["indent"] > parent_indent + 1: self.process(projects, parent_indent + 1, current_type) continue project = projects.pop(0) current_type = self.get_project_type(project, parent_type) if not current_type: # project not marked - not touching continue logging.debug('Project %s being processed as %s', project['name'], current_type) def item_filter(x): return x['project_id'] == project['id'] all_items = self.api.items.all(item_filter) items = sorted(all_items, key=lambda x: x['item_order']) item_objs = [] while items: item_objs.append(Item(items)) self.process_items(item_objs, current_type) self.activate(item_objs) def process_items(self, items, parent_type, not_in_first=False): """ Process all tasks in project """ # get the first item for serial first = None if parent_type == "serial": not_checked = [item for item in items if not item.checked] if not_checked and not self.is_waitfor(not_checked[0]): first = not_checked[0] # process items parent_active = False for item in items: current_type = self.get_item_type(item) if current_type: logging.debug('Identified %s as %s type', item.content, current_type) else: current_type = parent_type if item.children: active = self.process_items(item.children, current_type, not_in_first or first and item != first) else: active = False if self.check_future(item): continue active |= self.process_item(item, parent_type, first, not_in_first) item.active = active parent_active |= active return parent_active def process_item(self, item, type, first=None, not_in_first=False): """ Process single item """ # untag if checked if item.checked or not_in_first: return self.remove_label(item, self.next_label_id) # don't tag if parent with unchecked child elif [child for child in item.children if not child.checked]: return self.remove_label(item, self.next_label_id) # tag all parallel but not waitfors elif type == "parallel" and not self.is_waitfor(item): return self.add_label(item, self.next_label_id) # tag the first serial elif type == "serial" and item == first: return self.add_label(item, self.next_label_id) # untag otherwise else: return self.remove_label(item, self.next_label_id) def activate(self, items): """ Mark indent 1 items as active if supposed to be """ for item in items: if item.active: self.add_label(item, self.active_label_id) else: self.remove_label(item, self.active_label_id) def check_future(self, item): """ If its too far in the future, remove the next_action tag and skip """ if self.args.hide_future > 0 and item.due_date_utc: due_date = datetime.strptime(item.due_date_utc, '%a %d %b %Y %H:%M:%S +0000') future_diff = (due_date - datetime.utcnow()).total_seconds() if future_diff >= (self.args.hide_future * 86400): self.remove_label(item, self.next_label_id) return True def get_project_type(self, project_object, parent_type): """ Identifies how a project should be handled """ name = project_object['name'].strip() if name == 'Inbox': return self.args.inbox elif name[-1] == self.args.parallel_suffix: return 'parallel' elif name[-1] == self.args.serial_suffix: return 'serial' elif parent_type: return parent_type def get_item_type(self, item): """ Identifies how a item with sub items should be handled """ name = item.content.strip() if name[-1] == self.args.parallel_suffix: return 'parallel' elif name[-1] == self.args.serial_suffix: return 'serial' def add_label(self, item, label): if label not in item.labels: labels = item.labels logging.debug('Updating %s with label %s', item.content, label) labels.append(label) self.api.items.update(item.id, labels=labels) return True def remove_label(self, item, label): if label in item.labels: labels = item.labels logging.debug('Updating %s without label %s', item.content, label) labels.remove(label) self.api.items.update(item.id, labels=labels) return False def is_waitfor(self, item): return self.waitfor_label_id in item.labels @staticmethod def get_subitems(items, parent_item=None): """ Search a flat item list for child items """ result_items = [] found = False if parent_item: required_indent = parent_item['indent'] + 1 else: required_indent = 1 for item in items: if parent_item: if not found and item['id'] != parent_item['id']: continue else: found = True if item['indent'] == parent_item['indent'] and item['id'] != \ parent_item['id']: return result_items elif item['indent'] == required_indent and found: result_items.append(item) elif item['indent'] == required_indent: result_items.append(item) return result_items def parse_args(self): """ Parse command-line arguments """ parser = argparse.ArgumentParser() parser.add_argument('-a', '--api_key', help='Todoist API Key') parser.add_argument('-l', '--label', help='The next action label to use', default='next_action') parser.add_argument('-c', '--active', help='The active level1 parent label', default='active') parser.add_argument('-w', '--waitfor', help='The waitfor label', default='waitfor') parser.add_argument('-d', '--delay', help='Specify the delay in seconds between syncs', default=5, type=int) parser.add_argument('--debug', help='Enable debugging', action='store_true') parser.add_argument('--inbox', help='The method the Inbox project should ' 'be processed', default='parallel', choices=['parallel', 'serial']) parser.add_argument('--parallel_suffix', default='.') parser.add_argument('--serial_suffix', default='_') parser.add_argument('--hide_future', help='Hide future dated next actions until the ' 'specified number of days', default=7, type=int) parser.add_argument('--onetime', help='Update Todoist once and exit', action='store_true') self.args = parser.parse_args() # Set debug if self.args.debug: log_level = logging.DEBUG else: log_level = logging.INFO logging.basicConfig(level=log_level) # Check we have a API key if not self.args.api_key: logging.error('No API key set, exiting...') sys.exit(1)