Permalink
Switch branches/tags
Nothing to show
Find file
Fetching contributors…
Cannot retrieve contributors at this time
executable file 171 lines (140 sloc) 6.05 KB
#!/usr/bin/python
'''Take an org-mode file as input and print a timeclock file as
output. This can then be read directly by Ledger for fancy time
reporting and querying. Fields :BILLCODE: and :TASKCODE: are parsed to
generate lines compatible with the format expected by ledger
("billcode taskcode").
See also http://ledger-cli.org/2.6/ledger.html#Using-timeclock-to-record-billable-time
'''
__copyright__ = 'MIT'
__license__ = '''
Copyright © 2011-2016 John Wiegley
2016-2017 Antoine Beaupré
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
'''
from __future__ import print_function
import argparse
import locale
locale.setlocale(locale.LC_ALL, '')
import sys
import re
import time
iso_date_fmt = "%Y-%m-%d %H:%M:%S"
def parse_org_time(s):
return time.strptime(s, "%Y-%m-%d %a %H:%M")
def parse_timestamp(s):
return time.strptime(s, iso_date_fmt)
events = []
last_heading = None
clocks = []
parser = argparse.ArgumentParser(description='convert org clocks into timeclock',
epilog=__doc__ +
'''Note that TIME is provided in the following format: %s'''
% iso_date_fmt)
parser.add_argument('orgfile', help='Org file to process')
parser.add_argument('-s', '--start', metavar='TIME', help='process only entries from this date')
parser.add_argument('-e', '--end', metavar='TIME', help='process only entries to this date')
parser.add_argument('-r', '--regex', help='process only entries matching this regex')
parser.add_argument('-o', '--output', help='output file (default: stdout)',
type=argparse.FileType('w'), default=sys.stdout)
args = parser.parse_args()
data = args.orgfile
range_start = parse_timestamp(args.start) if args.start else None
range_end = parse_timestamp(args.end) if args.end else None
regex = args.regex
fd = open(data, "r")
headings = [None] * 9
acct = "<None>"
(billcode, taskcode) = ("<Unknown>", None)
def add_events():
# XXX: those globals should really be cleaned up, maybe through a clock object or named tuple
global acct, clocks, billcode, taskcode, events, todo_keyword, last_heading
if clocks:
for (clock_in, clock_out, billcode, taskcode) in clocks:
if billcode and ":" not in billcode and taskcode:
acct = "%s:%s" % (billcode, taskcode)
events.append((clock_in, clock_out, todo_keyword,
("%s %s" % (acct, last_heading))
if acct else last_heading))
clocks = []
for line in fd:
match = re.search("^(\*+)\s*(.+)", line)
if match:
depth = len(match.group(1))
headings[depth] = match.group(2)
depth = 0
match = re.search("^(\*+)\s+(TODO|DONE)?(\s+\[#[ABC]\])?\s*(.+)", line)
if match:
add_events()
depth = len(match.group(1))
todo_keyword = match.group(2)
last_heading = match.group(4)
match = re.search("(.+?)\s+:\S+:$", last_heading)
if match:
last_heading = match.group(1)
match = re.search("\[\[.*\]\]\s+(.+?)$", last_heading)
if match:
last_heading = match.group(1)
headings[depth] = last_heading
i = 0
prefix = ""
while i < depth:
if prefix:
prefix += ":" + headings[i]
else:
prefix = headings[i]
i += 1
if prefix:
#last_heading = prefix + " " + last_heading
last_heading = prefix + ":" + last_heading
if regex and not (prefix and re.search(regex, prefix)):
last_heading = None
if last_heading:
match = re.search("CLOCK:\s+\[(.+?)\](--\[(.+?)\])?", line)
if match:
clock_in = parse_org_time(match.group(1))
clock_out = match.group(3) # optional
if clock_out:
clock_out = parse_org_time(clock_out)
else:
#clock_out = time.localtime()
clock_out = None
if (not range_start or clock_in >= range_start) and \
(not range_end or clock_in < range_end):
clocks.append((clock_in, clock_out, billcode, taskcode))
elif clock_in < range_start and clock_out > range_start:
clocks.append((range_start, clock_out, billcode, taskcode))
elif clock_in < range_end and clock_out > range_end:
clocks.append((clock_in, range_end, billcode, taskcode))
match = re.search(":BILLCODE:\s+(.+)", line)
if match:
billcode = match.group(1)
taskcode = None
match = re.search(":TASKCODE:\s+(.+)", line)
if match:
taskcode = match.group(1)
fd.close()
add_events()
events.sort(key=lambda x: time.mktime(x[0]))
for event in events:
print("i %s %s" % (time.strftime(iso_date_fmt, event[0]), event[3]),
file=args.output)
if event[1]:
print("%s %s" % ('O' if event[2] == 'DONE' else 'o',
time.strftime(iso_date_fmt, event[1])),
file=args.output)
# org2tc ends here