All Unkept
Posted in: Python, KDE, Linux  —  February 24, 2012 at 05:57 PM

KTimeTracker replacement - TimeCult

by Luke Plant

For a long time I haven't been able to find a decent replacement or alternative to KTimeTracker. But I've now succeeded: TimeCult.

It is currently lacking an installer for Linux, but it can be made to work with Linux easily.

I also found that it uses a very easy-to-deciper XML format, and, with the help of vobject, the KTimeTracker ical files are only slightly harder to figure out, so I created a Python script that will do the conversion.

Use the script like this:

./ics_to_timecult.py ~/.kde/share/apps/ktimetracker/ktimetracker.ics > out.tmt

It's here for reference - dependencies are vobject and elementtree:

#!/usr/bin/env python
# Quick and dirty script to convert ktimetracker.ics files
# into TimeCult files.

import os.path
import sys
import time
import uuid

import elementtree.ElementTree as ET
import vobject


# Structures to store the tree of data. Use native Python data types, and
# convert to what TimeCult expects when we 'render'

class Struct(object):
    def __init__(self, **kwargs):
        self.__dict__.update(kwargs)


class TimeCult(Struct):
    # <timecult>
    # Eventual attributes:
    #  name
    #  projectTree
    #  timeLog

    def toXML(self):
        root = ET.XML('<timecult appVersion="0.12" fileVersion="10" />')
        root.attrib['uuid'] = str(uuid.uuid1())
        root.attrib['name'] = self.name
        pt = ET.Element('projectTree')
        for n in self.projectTree:
            pt.append(n.toXML())
        root.append(pt)
        tl = ET.Element('timeLog')
        for tr in self.timeLog:
            tl.append(tr.toXML())
        root.append(tl)
        return root

class Node(Struct):
    # <project> and <task>
    # Eventually will have these attributes:
    #  vtodo
    #  summary
    #  uid
    #  created
    #  parent
    #  children
    #  type = "project|task"
    #  finished
    def __repr__(self):
        return "<Node: uid=%s summary=%r, parent=%r>" % (
            self.uid,
            self.summary,
            self.parent.summary if self.parent is not None else None)

    def toXML(self):
        e = ET.Element(self.type)
        e.attrib['id'] = self.id
        e.attrib['name'] = self.summary
        e.attrib['created'] = str(int(time.mktime(self.created.timetuple()) * 1000))
        if (self.type == "task"):
            assert len(self.children) == 0
            e.attrib['status'] = 'finished' if self.finished else 'inProgress'
        else:
            for c in self.children:
                e.append(c.toXML())
        return e

class TimeRec(Struct):
    # <timerec>
    # Eventual attributes:
    #  startTime  # datetime
    #  duration   # seconds
    #  task       # Node
    def toXML(self):
        e = ET.Element("timeRec")
        e.attrib['taskId'] = self.taskId
        e.attrib['startTime'] = str(int(time.mktime(self.startTime.timetuple()) * 1000))
        e.attrib['duration'] = str(int(self.duration * 1000))
        e.attrib['notes'] = ""
        return e

def convert(filename):
    f = vobject.readOne(file(filename).read())

    # First parse Todos
    nodes = {}
    for vtodo in f.vtodo_list:
        n = Node(vtodo = vtodo,
                 uid=vtodo.uid.value,
                 summary=vtodo.summary.value,
                 created=vtodo.created.value,
                 finished=vtodo.contents['percent-complete'][0].value == "100"
                 )
        nodes[n.uid] = n

    children = {}
    for uid, node in nodes.items():
        vtodo = node.vtodo
        try:
            related_uid = vtodo.contents['related-to'][0].value
            node.parent = nodes[related_uid]
            children.setdefault(related_uid, []).append(node)
        except KeyError:
            node.parent = None

    for uid, node in nodes.items():
        node.children = children.get(uid, [])
        if len(node.children) == 0:
            node.type = "task"
        else:
            node.type = "project"



    autouid = 0 # For newly created Tasks

    # Now find the time information in the ICS file
    time_log = []
    for vevent in f.vevent_list:
        related_uid = vevent.contents['related-to'][0].value
        task = nodes[related_uid]
        if task.type == "project":
            # TimeCult can only cope with time events being against 'leaf' nodes
            # in the tree. So we need to adjust and create an additional leaf
            project = task
            undefined_l = [t for t in project.children if t.uid.startswith('autouid')]
            if len(undefined_l) > 0:
                task = undefined_l[0]
            else:
                task = Node(uid='autouid-%d' % autouid,
                            summary='other',
                            parent=project,
                            type="task",
                            created=project.created,
                            finished=False,
                            children=[])
                autouid += 1
                nodes[task.uid] = task
                project.children.insert(0, task)

        dtstart = vevent.dtstart.value
        dtend = vevent.dtend.value
        if dtstart.tzinfo is None:
            dtstart = dtstart.replace(tzinfo=dtend.tzinfo)
        if dtend.tzinfo is None:
            dtend = dtend.replace(tzinfo=dtstart.tzinfo)
        time_rec = TimeRec(startTime=dtstart,
                           duration=(dtend - dtstart).total_seconds(),
                           task=task)
        time_log.append(time_rec)

    # Now need to assign IDs
    taskId = 0
    for u, n in nodes.items():
        taskId += 1
        n.id = str(taskId)

    for tr in time_log:
        tr.taskId = tr.task.id

    return TimeCult(
        name=os.path.basename(filename),
        projectTree=[n for u, n in nodes.items() if n.parent is None],
        timeLog=time_log)


if __name__ == '__main__':
    filename = sys.argv[1]
    timecult = convert(filename)
    sys.stdout.write(
        """<?xml version="1.0" encoding="UTF-8"?>""" +
        ET.tostring(timecult.toXML()))

Comments §

blog comments powered by Disqus