Source code for timelog2csv

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""timelog2csv - converts a TimeLog calendar to a CSV file

TimeLog was a time tracking application for OS X. It stored it's data
using iCal. OS X Mountain Lion (and Mavericks) dropped some features
that TimeLog needed to talk to iCal. So it's impossible to access the
recorded data because the application won't even start.

But this small tool can help: Export the iCal calendar which you used to
store TimeLog's data. Then use timelog2csv to convert it to a CSV file.

Usage:
    timelog2csv [-h] [--project=<name> ...] <ical> <csv>
    timelog2csv --version

Options:
    -h --help              Show this help
       --version           Show version
       --project=<name>    Filter by project name

"""
from __future__ import unicode_literals

import codecs
import csv
import os
import sys

from docopt import docopt
from icalendar import Calendar

__author__ = 'Markus Zapke-Gründemann'
__email__ = 'markus@keimlink.de'
__version__ = '0.1.2'

try:
    from cStringIO import StringIO
except ImportError:
    from io import StringIO
try:
    from urllib import unquote
    from urlparse import urlparse, parse_qs
except ImportError:
    from urllib.parse import urlparse, parse_qs, unquote

if sys.version[0] == '3':
    raw_input = input

DATETIME_FORMAT = '%Y-%m-%d %H:%M:%S'


[docs]class UnicodeWriter: """A CSV writer which will write rows to CSV file "f". File "f" is encoded in the given encoding. """ def __init__(self, f, dialect=csv.excel, encoding="utf-8", **kwds): # Redirect output to a queue self.queue = StringIO() self.writer = csv.writer(self.queue, dialect=dialect, **kwds) self.stream = f self.encoder = codecs.getincrementalencoder(encoding)()
[docs] def writerow(self, row): """Writes the ``row`` parameter to the writer’s file object.""" self.writer.writerow([s.encode("utf-8") for s in row]) # Fetch UTF-8 output from the queue ... data = self.queue.getvalue() data = data.decode("utf-8") # ... and reencode it into the target encoding data = self.encoder.encode(data) # write to the target stream self.stream.write(data) # empty queue self.queue.truncate(0)
[docs] def writerows(self, rows): """Writes all the ``rows`` parameters to the writer’s file object.""" for row in rows: self.writerow(row)
[docs]def parse_url(url): """Parses ``url`` into six components, returns components and query string. All query string key/value pairs are unquoted. """ comp = urlparse(url, scheme='timelog') query = parse_qs(comp.query) for key, value in list(query.items()): query[key] = unquote(value[0]) # client data must be reencoded if key == 'client': query[key] = query[key].encode('iso-8859-1').decode('utf-8') return comp, query
[docs]def get_row(component): """Returns a row created from a VEVENT ``component``. A row is a list consisting of eight elements: * client * project * category * description * start * end * duration * status """ url_comp, query = parse_url(component['URL']) row = [query['client']] row.extend(component['summary'].split(':')) row.append(component['description']) start = component['dtstart'].dt row.append(start.strftime(DATETIME_FORMAT)) end = component['dtend'].dt row.append(end.strftime(DATETIME_FORMAT)) row.append(str(int((end - start).total_seconds() / 60))) if query['status'] == '(null)': row.append('') else: row.append(query['status']) return row
[docs]def read_data(ical_in, projects=None): """Reads the data from the iCal file, returns a list of rows. The optional argument ``projects`` can be used to filter the rows by project name. """ data = [] cal = Calendar.from_ical(ical_in.read()) for component in cal.walk('VEVENT'): row = get_row(component) if not projects or row[1] in projects: data.append(row) return data
[docs]def check_source(filename): """Checks if the source file exists.""" if not os.path.exists(filename): sys.stderr.write('Error: iCal file "%s" not found.\n' % filename) sys.exit(1)
[docs]def check_target(filename): """Checks if the target file exists. If the target file exists a confirmation to overwrite is required. """ if os.path.exists(filename): sys.stdout.write('CSV file "%s" already exists.\n' % filename) answer = None while answer not in ('n', 'y'): answer = raw_input('Continue and overwrite the existing file? (yN) ') answer = answer.lower().strip() if len(answer) == 0: answer = 'n' if answer == 'n': sys.stdout.write('Aborted.\n') sys.exit()
[docs]def write_csv(csv_out, data, header=None): """Writes ``data`` to a CSV file. The ``header`` argument can be used to add a header row. """ if header: data = [header] + data writer = UnicodeWriter(csv_out, delimiter=b';') writer.writerows(data) sys.stdout.write('Created CSV file "%s" with %d rows.\n' % (csv_out.name, len(data)))
[docs]def main(): """Runs the main program.""" args = docopt(__doc__, version=__version__) check_source(args['<ical>']) check_target(args['<csv>']) data = read_data(open(args['<ical>'], 'rb'), args['--project']) header = ['Client', 'Project', 'Category', 'Description', 'Start', 'End', 'Duration', 'Status'] with open(args['<csv>'], 'wb') as target: write_csv(target, data, header)
if __name__ == '__main__': main()