michel-orgmode / michel /

Full commit
#!/usr/bin/env python
Pushes/pulls flat text files to google tasks

    michel pull                 prints the default tasklist on stdout
    michel push <textfile>      replace the default tasklist with the
                                content of <textfile>.
from __future__ import with_statement
import gflags
import httplib2
from apiclient.discovery import build
from oauth2client.file import Storage
from oauth2client.client import OAuth2WebServerFlow
from import run
from xdg.BaseDirectory import save_config_path, save_data_path
import os.path
import sys
import re

class TasksTree(object):
    Tree for holding tasks

    A TasksTree:
    - is a task (except the root, which just holds the list)
    - has subtasks
    - may have a task_id
    - may have a title

    def __init__(self, title=None, task_id=None, task_notes=None, task_status=None):
        self.title = title
        self.task_id = task_id
        self.subtasks = []
        self.task_notes = task_notes
        # *status* usually takes on the value 'completed' or 'needsAction'
        self.status = task_status

    def get(self, task_id):
        """Returns the task of given id"""
        if self.task_id == task_id:
            return self
            for subtask in self.subtasks:
                if subtask.get(task_id) != None:
                    return subtask.get(task_id)

    def add_subtask(self, title, task_id = None, parent_id = None,
            task_notes=None, task_status=None):
        Adds a subtask to the tree
        - with the specified task_id
        - as a child of parent_id
        if not parent_id:
            self.subtasks.append(TasksTree(title, task_id, task_notes, task_status))
            if not self.get(parent_id):
                raise ValueError, "No element with suitable parent id"
            self.get(parent_id).add_subtask(title, task_id, None, task_notes,

    def last(self, level):
        """Returns the last task added at a given level of the tree"""
        if level == 0:
            return self
            res = None
            for subtask in self.subtasks:
                res = subtask.last(level - 1) or res
            if res:
                return res

    def push(self, service, list_id, parent = None, root=True):
        """Pushes the task tree to the given list"""
        # We do not want to push the root node
        if not root:
            args = {'tasklist': list_id, 'body':{ 'title' : self.title } }
            if parent:
                args['parent'] = parent
            res = service.tasks().insert(**args).execute()
            self.task_id = res['id']
        # the API head inserts, so we insert in reverse.
        for subtask in reversed(self.subtasks):
            subtask.push(service, list_id, parent=self.task_id, root=False)

    def _lines(self, level):
        """Returns the sequence of lines of the string representation"""
        res = []
        for subtask in self.subtasks:
            #indentations = '\t' * level
            # add number of asterisks corresponding to depth of task, followed
            # by "DONE" if the task is marked as completed.
            done_string = ""
            if (subtask.status is not None) and (subtask.status == "completed"):
                done_string = " DONE"
            indentations = '*' * (level+1) + done_string + " "
            res.append(indentations + subtask.title)
            if subtask.task_notes is not None:
                notes = subtask.task_notes
                # add initial space to lines starting w/'*', so that it isn't treated as a task
                if notes.startswith("*"):
                    notes = " " + notes
                notes = notes.replace("\n*", "\n *")
            subtasks_lines = subtask._lines(level + 1)
            res += subtasks_lines
        return res

    def __str__(self):
        """string representation of the tree"""
        return '\n'.join(self._lines(0))

def get_service():
    Handle oauth's shit (copy-pasta from
    Yes I do publish a secret key here, apparently it is normal
    FLAGS = gflags.FLAGS
    FLOW = OAuth2WebServerFlow(
    FLAGS.auth_local_webserver = False
    storage = Storage(os.path.join(save_data_path("michel"), "oauth.dat"))
    credentials = storage.get()
    if credentials is None or credentials.invalid == True:
        credentials = run(FLOW, storage)
    http = httplib2.Http()
    http = credentials.authorize(http)
    return build(serviceName='tasks', version='v1', http=http)

def print_todolist(list_id):
    """Prints the todo list of given id"""
    service = get_service()
    tasks = service.tasks().list(tasklist=list_id).execute()
    tasks_tree = TasksTree()
    tasklist = [t for t in tasks.get('items', [])]
    fail_count = 0
    while tasklist != [] and fail_count < 1000 :
        t = tasklist.pop(0)
            tasks_tree.add_subtask(t['title'].encode('utf-8'), t['id'],
                    t.get('parent'), t.get('notes'), t.get('status'))
        except ValueError:
            fail_count += 1

def erase_todolist(list_id):
    """Erases the todo list of given id"""
    service = get_service()
    tasks = service.tasks().list(tasklist=list_id).execute()
    for task in tasks.get('items', []):

def parse_old(path):
    """Parses a todolist file and returns a tree"""
    tasks_tree = TasksTree()
    with open(path) as f:
        curr_indent_lvl = 0
        for n, line in enumerate(f):
            # trim trailing '\n'
            if line[-1] == '\n':
                line = line[:-1]
            # get the indent level
            indent_lvl = 0
            while line[0] == '\t':
                line = line[1:]
                indent_lvl += 1
            assert indent_lvl <= curr_indent_lvl + 1, ("line %d: "
                    "subtask has no parent task" % n)
            curr_indent_lvl = indent_lvl
            # TODO: Parse and add task_notes if they exist, not just title
            # (line) -- also parse DONE in a headline, and map to status
            # of 'completed'
    return tasks_tree

def parse(path):
    """Parses a todolist file and returns a tree"""
    headline_regex = re.compile("^(\*+ )( *)(DONE )?")
    tasks_tree = TasksTree()
    with open(path) as f:
        last_indent_level = 0
        seen_first_task = False
        for n, line in enumerate(f):
            matches = headline_regex.findall(line)
            line = line.rstrip("\n")
                # assign task_depth; root depth starts at 0
                num_asterisks_and_space = len(matches[0][0])
                indent_level = num_asterisks_and_space - 2
                # strip off asterisks-and-space prefix
                line = line[num_asterisks_and_space:]
                # if we get to this point, then it means that a new task is
                # starting on this line -- we need to add the last-parsed task
                # to the tree (if this isn't the first task encountered)
                if seen_first_task:
                    # add the task to the tree
                    # this is the first task, so skip adding a task to the
                    # tree, and record that we've encountered our first task
                    seen_first_task = True
                if matches[0][2] == 'DONE ':
                    task_status = 'completed'
                    # number of spaces preceeding 'DONE' and after
                    # asterisks+single-space
                    num_extra_spaces = len(matches[0][1])
                    # remove the '[ ...]DONE ' from the line
                    line = line[num_extra_spaces + len('DONE '):]
                    task_status = 'needsAction'
                task_title = line
                task_notes = None
            except IndexError:
                # this is not a task, but a task-notes line
                if task_notes is None:
                    task_notes = line
                    task_notes += "\n" + line
            assert indent_level <= last_indent_level + 1, ("line %d: "
                    "subtask has no parent task" % n)
            last_indent_level = indent_level
        # add the last task to the tree, since the for loop won't be iterated
        # again after the last line of the file (tasks are added at beginning
        # of the for loop)

    return tasks_tree

def push_todolist(path, list_id):
    """Pushes the specified file to the specified todolist"""
    tasks_tree = parse(path)
    tasks_tree.push(get_service(), list_id)

def main():
    if (len(sys.argv)) < 2:
    elif sys.argv[1] == "pull":
    elif sys.argv[1] == "push":
        if not len(sys.argv) == 3:
            print("'push' expects exactly 1 argument")
        path = sys.argv[2]
        if not os.path.exists(path):
            print("The file you want to push does not exist.")
        push_todolist(path, '@default')

if __name__ == "__main__":