#!/usr/bin/env python3 ## Todoist CLI - command line client for todoist.com online task manager ## ## Copyright (C) 2016 Robin Obůrka ## ## This program is free software: you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published by ## the Free Software Foundation, either version 3 of the License, or ## (at your option) any later version. ## ## This program is distributed in the hope that it will be useful, ## but WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ## GNU General Public License for more details. ## ## You should have received a copy of the GNU General Public License ## along with this program. If not, see . import os import sys import argparse import json import todoist TOKEN_FILE_NAME = ".todoist_token" def arg_parser(): parser = argparse.ArgumentParser() parser.add_argument("--token", metavar="ACCESS_TOKEN_STRING", help="Store or change access token", type=str) parser.add_argument("--get-token", help="Get current token (e.g. for revocation)", action="store_true") parser.add_argument("--dump", metavar="FILE", help="Dump data to FILE (use - for stdout) and exit", type=argparse.FileType("w")) parser.add_argument("-p", "--project", metavar="PROJECT_NAME", help="Project name (lowercase!)", type=str) parser.add_argument("-s", "--severity", help="Severity (or priority) of task", type=int, choices=[1, 2, 3, 4]) parser.add_argument("-a", "--assign", metavar="NAME", help="Assign to collaborator", type=str) parser.add_argument("-d", "--date", metavar="DATE", help="Date string", type=str, default="today") parser.add_argument("-n", "--no-due-date", help="Add task with no due date", action="store_true") parser.add_argument("-i", "--indent", help="Set indentation of task (default is 1)", type=int) parser.add_argument("-l", "--lines", help="Use every line of stdin as one task", action="store_true") parser.add_argument("msg", metavar="MSG", nargs='*', help="Content of new Task - stdin if absent") return parser.parse_args() def token_path(): return os.path.join(os.path.expanduser("~"), TOKEN_FILE_NAME) def get_token(): path = token_path() with open(path, mode="r") as f: for line in f: token = line.strip() ## Get first line only break return token def store_token(token): path = token_path() with open(path, mode="w+") as f: f.write(token) def dump(to, data): to.write(json.dumps(data, ensure_ascii=False, indent=4)) to.write("\n") to.flush() def build_project_list(data): projects = { proj["name"].lower(): proj["id"] for proj in data["Projects"] } return projects def build_collaborators_list(data): collaborators = { col["full_name"].lower(): col["id"] for col in data["Collaborators"] } return collaborators def find_candidate(name, storage): candidates = [] for k, v in storage.items(): if name == k: return v, None elif k.find(name) != -1: candidates.append((k, v)) if len(candidates) == 0: return None, "No candidate found" elif len(candidates) > 1: clist = [ item[0] for item in candidates ] clist.insert(0, "Multiple candidates found. Pick one of them:") return None, "\n".join(clist) else: return candidates[0][1], None def store_task(api, msg, kwargs_dict): if not msg.strip(): print("Warning: Empty message skipped", file=sys.stderr) return status = api.add_item(msg, **kwargs_dict) if "error" in status: print("Store failed:", file=sys.stderr) print(status["error"], file=sys.stderr) sys.exit(1) def main(): args = arg_parser() if args.token: store_token(args.token) print("Token stored") sys.exit(0) token = get_token() if not token: print("Couldn't read token. Is token stored?", file=sys.stderr) sys.exit(1) if args.get_token: print(token) sys.exit(0) api = todoist.TodoistAPI(token) data = api.sync(resource_types=['all']) if "error" in data: print("Failed to connect or authenticate to server:", file=sys.stderr) print(data["error"], file=sys.stderr) sys.exit(1) if args.dump: dump(args.dump, data) sys.exit(0) ## Prepare data structures kwargs = {} projects = build_project_list(data) collaborators = build_collaborators_list(data) ## Set date if not args.no_due_date: kwargs["date_string"] = args.date ## Set project if args.project: proj, err = find_candidate(args.project.lower(), projects) if not proj: print("Set project error:", err, file=sys.stderr) sys.exit(1) kwargs["project_id"] = proj ## Set priority/severity if args.severity: kwargs["priority"] = args.severity ## Set indentation if args.indent: kwargs["indent"] = args.indent ## Assign to person if args.assign: aid, err = find_candidate(args.assign.lower(), collaborators) if not aid: print("Assign to person error:", err, file=sys.stderr) sys.exit(1) kwargs["responsible_uid"] = aid ## It is decided about storing tasks at this point if args.msg: msg = " ".join(args.msg) store_task(api, msg, kwargs) else: ## Read from stdin if args.lines: for line in sys.stdin: if not line.strip(): continue store_task(api, line, kwargs) else: msg = sys.stdin.read().strip() store_task(api, msg, kwargs) ## All operations done - commit and sync them api.commit() if __name__ == "__main__": main()