Skip to content

Instantly share code, notes, and snippets.

@x
Last active November 20, 2019 08:03
Show Gist options
  • Save x/c4a4fa7910ed4eb364bf to your computer and use it in GitHub Desktop.
Save x/c4a4fa7910ed4eb364bf to your computer and use it in GitHub Desktop.
Given a CSV export of an asana project, generate a tab-seperated table of burn down numbers that google sheets can easily translate into a burn down chart.
#! /usr/bin/python
"""
usage: burndown [-h] [--start START] [--end END] [--retro] fname
positional arguments:
fname
optional arguments:
-h, --help show this help message and exit
--start START start of sprint (inclusive)
--end END end of sprint (inclusive)
--retro apply points retroactively in days before their create
"""
import sys
import re
from dateutil.parser import parse
from collections import defaultdict
from datetime import timedelta
from argparse import ArgumentParser
from itertools import product
class Task():
def __init__(self, row):
self.id = row[0]
self.name = row[4]
self.assignee = row[5]
self.created_at = self._parse_date(row[1])
self.completed_at = self._parse_date(row[2])
self.tags = self._parse_tags(row[7])
self.points = self._parse_points(self.name)
def _parse_points(self, name):
match = re.search(r'\[([0-9]+)\]', name)
if match:
return int(match.group(1))
else:
return 0
def _parse_tags(self, s):
return set(s.split(',')).difference(set(['']))
def _parse_date(self, s):
if s:
return parse(s)
else:
return None
def was_active(self, dt):
return self.created_at <= dt and \
(not self.completed_at or dt < self.completed_at)
def was_uncompleted(self, dt):
return not self.completed_at or dt < self.completed_at
def parse_tasks(fname):
def _parse(line):
try:
return Task(line.split(','))
except Exception:
pass
with open(fname, 'r') as f:
return filter(None, map(_parse, f.readlines()[1:]))
def count_points(tasks, start, end, apply_points_retroactively=False):
dates = [start + timedelta(days=i)
for i in range(int((end - start).total_seconds() \
// (60 * 60 * 24)) + 1)]
points = defaultdict(int)
for date, task in product(dates, tasks):
if apply_points_retroactively:
if task.was_uncompleted(date):
points[date] += task.points
else:
if task.was_active(date):
points[date] += task.points
return points
def print_points(points):
for date, points in sorted(points.items()):
print "{}\t{}".format(date, points)
def main():
ap = ArgumentParser()
ap.add_argument('--start', dest='start')
ap.add_argument('--end', dest='end')
ap.add_argument('--retro', dest='retro', action='store_true')
ap.add_argument('fname')
args = ap.parse_args()
tasks = parse_tasks(args.fname)
points = count_points(tasks, parse(args.start), parse(args.end), args.retro)
print_points(points)
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment