Last active
November 20, 2019 08:03
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#! /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