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 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(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 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()
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) ]
class TodoistActionAPI(RequiredConfig): """An encapsulation of the Todoist API. It wraps the lower level methods with methods appropriate for this task.""" required_config = Namespace() required_config.add_option( name="api_key", default='', doc="Todoist API Key", ) required_config.add_option( name="delay", default=5, doc="Specify the delay in seconds between syncs" ) required_config.add_option( name="debug", default=False, doc="Enable debugging" ) required_config.add_option( name="one_time", default=False, doc="Update Todoist once and exit" ) def __init__(self, config, logging): super(TodoistActionAPI, self).__init__() self.config = config self.api = TodoistAPI(token=config.api_key) self.api.sync() self.logging = logging
def get_all_projects(self): api = TodoistAPI(self.apikey) api.sync() print(api.state["projects"])
def main(subtasks_path, task_type, work_type): with console.status('[bold green]Initial Todoist API sync...') as _: api_token = os.getenv('TODOIST_API_TOKEN') api = TodoistAPI(api_token) api.sync() (project_id, _) = ui_select_project(api, work_type) branch_ref = '' repo = '' if task_type != DevTaskType.NON_MERGE: branch_ref = ui_get_jira_or_branch_ref() if task_type != DevTaskType.NON_MERGE: (_, repo, _) = ui_select_repository('Select Repository for Work') root_task_id = ui_create_root_dev_task(api, project_id, branch_ref, task_type, work_type, repo) if subtasks_path: ui_create_subtasks_from_file(api, subtasks_path, root_task_id, project_id, task_type, work_type) else: create_subtasks = questionary.confirm( 'Would you like to create any subtasks').ask() if create_subtasks: ui_create_subtasks(api, root_task_id, project_id, task_type, work_type) if task_type != DevTaskType.NON_MERGE: create_pr_checklist_subtask(api, project_id, root_task_id, branch_ref, task_type, work_type) create_merge_subtasks(api, project_id, root_task_id, branch_ref, repo, 'main', task_type, work_type) if os.getenv('JIRA_BASE_URL'): create_jira_admin_task(api, project_id, root_task_id, branch_ref)
def find_task_by_name(content): api = TodoistAPI(TODOIST_API_TOKEN) api.sync() items = api.state['items'] for item in items: if item['content'] == content: return (item['id'])
def get_data(): api = TodoistAPI(get_api_token()) api.sync() tasks = api.items.all() tasks_with_labels = [t for t in tasks if len(t['labels']) != 0] tasks_without_labels = [t for t in tasks if len(t['labels']) == 0] test_data_size = math.floor(len(tasks_with_labels) * 0.8) test_data = tasks_with_labels[test_data_size:] training_data = tasks_with_labels[:test_data_size] save(test_data, 'test_data.p') save(training_data, 'training_data.p') save(tasks_without_labels, 'unlabeled_data.p') if debug(): print('Num Tasks with no labels %s' % len(tasks_without_labels)) print('Num Tasks with labels %s' % len(tasks_with_labels)) print('Size of Training Data: %s' % len(training_data)) print('Size of Test Data: %s' % len(test_data)) return { 'test_data': test_data, 'training_data': training_data, 'unlabeled_data': tasks_without_labels }
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)
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 APIcall(caller): cur = datetime.now() if caller == 'Cal': return requests.get('https://calendarific.com/api/v2/holidays',\ params={'api_key':keys.CAL_API, 'country':'SG', 'year':str(cur.year)}\ ).json() elif caller == 'News': return requests.get('http://newsapi.org/v2/top-headlines',\ params={'country':'sg', 'apiKey':keys.NEWS_API}\ ).json() elif caller == 'Todo': todolist = [] api = TodoistAPI(keys.TODO_API) api.sync() for i in range(len(api.state['items'])): temp = api.state['items'][i] if temp['checked'] == 0: todolist.append([temp['content'], temp['due']['date']]) return todolist elif caller == 'Weather': return requests.get('https://api.openweathermap.org/data/2.5/onecall',\ params={'lat':'1.370', 'lon':'103.955', 'exclude':'minutely,daily,current',\ 'appid':keys.WEA_API, 'units':'metric'}\ ).json()
def main(): with console.status('[bold green]Initial Todoist API sync...') as _: api_token = os.getenv('TODOIST_API_TOKEN') api = TodoistAPI(api_token) api.sync() dev_tasks = [] with console.status( '[bold green]Getting outstanding development tasks...') as _: dev_tasks = get_outstanding_dev_tasks(api, DevWorkType.WORK) task = select_dev_task(dev_tasks) if len(task.subtasks) > 3: console.print( '[bold red]Error:[/bold red] A task is only eligible for PR ' \ 'if all subtasks (except the PR/merge tasks) are cleared.' ) console.print( '[bold green]Suggestion:[/bold green] Clear all the non-merge subtasks first.' ) return 1 pr_checklist = task.subtasks[0] process_pr_checklist(api, console, pr_checklist.subtasks) url = create_pr() with console.status( "[bold green]Marking development task as 'waiting'...") as _: task.mark_waiting( api, f'PR submitted, now waiting for review. PR is available at: {url}') submit_pr_task = task.subtasks[1] with console.status('[bold green]Completing submit PR task...') as _: submit_pr_task.complete(api)
def main(): api_token = os.getenv("TODOIST_API_TOKEN") with console.status("[bold green]Initial Todoist API sync...") as _: api = TodoistAPI(api_token) api.sync() link = questionary.text("Link?").ask() item_type = questionary.select("Type?", choices=["book", "film"]).ask() if item_type == "book": author = questionary.text("Author?").ask() title = questionary.text("Title?").ask() notes = questionary.text("Notes?").ask() create_item_to_buy(api, f"{author} - '{title}'", ["buy", "books"], link, notes) elif item_type == "film": title = questionary.text("Title?").ask() media_type = questionary.select("Type?", choices=["Blu-ray", "Digital", "DVD"]).ask() if media_type == "Blu-ray": sub_type = questionary.select("Blu-ray type?", choices=["1080p", "4k"]).ask() media_type += f" {sub_type}" notes = questionary.text("Notes?").ask() create_item_to_buy(api, f"'{title}' ({media_type})", ["buy", "films"], link, notes) return 0
class TodoistAccess: def __init__( self, path=os.path.join( os.path.dirname(__file__), "..", "resources", "secrets", "todoist_token.txt")): #'../resources/secrets/todoist_token.txt'): with open(path, 'rb') as f: my_token = f.readline().decode("utf-8") self.api = TodoistAPI(my_token) self.api.sync() def get_all_tasks(self): self.sync() tasks = self.api.items.all() return tasks def sync(self): self.api.sync() def get_content(self, note): return note["content"] def get_all_content(self): tasks = self.get_all_tasks() return [self.get_content(task) for task in tasks] def get_tasks_info(self): tasks = self.get_all_tasks() return [(task["content"], task["due"]) for task in tasks]
def pullSources(keys): canvasAPI = keys['Canvas'] todoistAPI = keys['Todoist'] courseList = [] projectList = {} header = {"Authorization": "Bearer " + canvasAPI} parameter = {'per_page': 9999, 'include': 'submission'} for ID, course in keys['Courses'].items(): try: assignments = requests.get("https://canvas.instructure.com/api/v1/courses/" + ID+"/assignments", headers=header, params=parameter).json() newCourse = Class(course[0], str(ID), str(course[1])) for assignment in assignments: if not assignment['submission']['submitted_at']: newCourse.assignments.append([assignment['name'], assignment['due_at']]) courseList.append(newCourse) except: print(f"Error requesting {course[0]} from Canvas API") api = TodoistAPI(todoistAPI) api.sync() projectList = createProjects(courseList, api) api.sync() createTasks(api, projectList)
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 connect(parentdebuglog): if not args.api_key: logging.error('No API key set, exiting...') sys.exit(1) # Run the initial sync debuglog = parentdebuglog.sublogger('Connecting to the Todoist API') api = TodoistAPI(token=args.api_key) debuglog.log('Syncing the current state from the API') api.sync() # Check the NoDate label exists labels = api.labels.all(lambda x: x['name'] == args.label) if len(labels) > 0: global nodate_label_id nodate_label_id = labels[0]['id'] debuglog.log('Label %s found as label id %d' % (args.label, nodate_label_id)) else: debuglog.error("Label %s doesn't exist, please create it." % args.label) sys.exit(1) global next_label_ids next_label_ids = set(map( lambda x: x['id'], api.labels.all(lambda x: x['name'].startswith(args.next_prefix)) )) debuglog.log('"Next" label ids: %s' % (next_label_ids)) return api
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 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 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() return api
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 initiate_api(): 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() return api
def show_projects(key=tovakey): api = TodoistAPI(key) api.sync() projects = [] for project in api.state['projects']: projects.append(project['name']) projects_to_show = ', '.join([str(elem) for elem in projects]) return query_response(value=projects_to_show, grammar_entry=None)
def main(): TODOIST_API_TOKEN = get_todoist_token() EXISTIO_API_TOKEN = get_existio_token() if not TODOIST_API_TOKEN or not EXISTIO_API_TOKEN: logger.warning('Please set the API token in environment variable.') exit() api = TodoistAPI(TODOIST_API_TOKEN) api.sync() return api
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 totask(s: str, v: bool) -> (bool): if v: print("todoist - ", s) api = TodoistAPI(settings.TODOISTTOKEN) api.sync() inboxid = api.state['projects'][0]['id'] task = api.quick.add('ChangeBattery: ' + s + " " + settings.TODOISTLABEL, note='auto gen task', remminder='tomorrow') True if task["id"] else False
def get_id(name, component): """ Get the id for a specific name in a component""" api = TodoistAPI(get_token()) api.sync() items = api.state[component] for item in items: if item['name'].lower() == name.lower(): return item['id'] # if we reach this point, the specified project does not exist raise RuntimeError('{} does not exist in {}!'.format(name, component))
def find_project_by_name(name): """ Help function to find specific projects ID by given name Args: name ([str]): [Project name] """ api = TodoistAPI(TODOIST_API_TOKEN) api.sync() projects = api.state['projects'] for project in projects: if project['name'] == name: return (project['id'])
def initialize_todoist(): API_TOKEN = get_token() if not API_TOKEN: logging.warn('Please set the API token in environment variable.') exit() api = TodoistAPI(API_TOKEN) if api is None: return 'Request for Streaks with Todoist not authorized, exiting.' else: api.sync() return api
def useSyncLibrary(self): api = TodoistAPI(secrets.API_TOKEN) api.sync() #print(api.state['projects']) print("\n\n\n") items = api.state["items"] print(json.dumps(items)) # get all completed all_comp = api.completed.get_all() for item in items: if item['checked'] > 0: print(item['content'])
def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Todoist platform.""" token = config.get(CONF_TOKEN) # Look up IDs based on (lowercase) names. project_id_lookup = {} label_id_lookup = {} from todoist.api import TodoistAPI api = TodoistAPI(token) api.sync() # Setup devices: # Grab all projects. projects = api.state[PROJECTS] # Grab all labels labels = api.state[LABELS] # Add all Todoist-defined projects. project_devices = [] for project in projects: # Project is an object, not a dict! # Because of that, we convert what we need to a dict. project_data = { CONF_NAME: project[NAME], CONF_ID: project[ID] } project_devices.append( TodoistProjectDevice(hass, project_data, labels, api) ) # Cache the names so we can easily look up name->ID. project_id_lookup[project[NAME].lower()] = project[ID] # Cache all label names for label in labels: label_id_lookup[label[NAME].lower()] = label[ID] # Check config for more projects. extra_projects = config.get(CONF_EXTRA_PROJECTS) for project in extra_projects: # Special filter: By date project_due_date = project.get(CONF_PROJECT_DUE_DATE) # Special filter: By label project_label_filter = project.get(CONF_PROJECT_LABEL_WHITELIST) # Special filter: By name # Names must be converted into IDs. project_name_filter = project.get(CONF_PROJECT_WHITELIST) project_id_filter = [ project_id_lookup[project_name.lower()] for project_name in project_name_filter] # Create the custom project and add it to the devices array. project_devices.append( TodoistProjectDevice( hass, project, labels, api, project_due_date, project_label_filter, project_id_filter ) ) add_devices(project_devices) def handle_new_task(call): """Call when a user creates a new Todoist Task from HASS.""" project_name = call.data[PROJECT_NAME] project_id = project_id_lookup[project_name] # Create the task item = api.items.add(call.data[CONTENT], project_id) if LABELS in call.data: task_labels = call.data[LABELS] label_ids = [ label_id_lookup[label.lower()] for label in task_labels] item.update(labels=label_ids) if PRIORITY in call.data: item.update(priority=call.data[PRIORITY]) if DUE_DATE in call.data: due_date = dt.parse_datetime(call.data[DUE_DATE]) if due_date is None: due = dt.parse_date(call.data[DUE_DATE]) due_date = datetime(due.year, due.month, due.day) # Format it in the manner Todoist expects due_date = dt.as_utc(due_date) date_format = '%Y-%m-%dT%H:%M' due_date = datetime.strftime(due_date, date_format) item.update(due_date_utc=due_date) # Commit changes api.commit() _LOGGER.debug("Created Todoist task: %s", call.data[CONTENT]) hass.services.register(DOMAIN, SERVICE_NEW_TASK, handle_new_task, schema=NEW_TASK_SERVICE_SCHEMA)
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 ServiceTodoist(ServicesMgr): def __init__(self, token=None, **kwargs): super(ServiceTodoist, self).__init__(token, **kwargs) self.AUTH_URL = 'https://todoist.com/oauth/authorize' self.ACC_TOKEN = 'https://todoist.com/oauth/access_token' self.REQ_TOKEN = 'https://todoist.com/oauth/access_token' self.consumer_key = settings.TH_TODOIST['client_id'] self.consumer_secret = settings.TH_TODOIST['client_secret'] self.scope = 'task:add,data:read,data:read_write' self.service = 'ServiceTodoist' self.oauth = 'oauth2' if token: self.token = token self.todoist = TodoistAPI(token) def read_data(self, **kwargs): """ get the data from the service as the pocket service does not have any date in its API linked to the note, add the triggered date to the dict data thus the service will be triggered when data will be found :param kwargs: contain keyword args : trigger_id at least :type kwargs: dict :rtype: list """ trigger_id = kwargs.get('trigger_id') date_triggered = kwargs.get('date_triggered') data = [] project_name = 'Main Project' items = self.todoist.sync() for item in items.get('items'): date_added = arrow.get(item.get('date_added'), 'ddd DD MMM YYYY HH:mm:ss ZZ') if date_added > date_triggered: for project in items.get('projects'): if item.get('project_id') == project.get('id'): project_name = project.get('name') data.append({'title': "From TodoIst Project {0}" ":".format(project_name), 'content': item.get('content')}) cache.set('th_todoist_' + str(trigger_id), data) return data def save_data(self, trigger_id, **data): """ let's save the data :param trigger_id: trigger ID from which to save data :param data: the data to check to be used and save :type trigger_id: int :type data: dict :return: the status of the save statement :rtype: boolean """ kwargs = {} title, content = super(ServiceTodoist, self).save_data(trigger_id, data, **kwargs) if self.token: if title or content or \ (data.get('link') and len(data.get('link'))) > 0: content = title + ' ' + content + ' ' + data.get('link') self.todoist.add_item(content) sentence = str('todoist {} created').format(data.get('link')) logger.debug(sentence) status = True else: status = False else: logger.critical("no token or link provided for " "trigger ID {} ".format(trigger_id)) status = False return status
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)
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)