···3344from config import config
55from gh.project import Project
66+from util.dates import parse_to_local, parse_to_utc
77+6879class BurndownChart:
810911 def __init__(self, project: Project):
1010- # Initialize important dates
1111- self.start_date = datetime.strptime(
1212- config['settings']['sprint_start_date'],
1313- '%Y-%m-%d')
1414- self.end_date = datetime.strptime(
1515- config['settings']['sprint_end_date'],
1616- '%Y-%m-%d')
1717- self.project = project
1818-1212+ self.start_date_utc: datetime = parse_to_utc(
1313+ config['settings']['sprint_start_date'])
1414+ self.end_date_utc: datetime = parse_to_utc(
1515+ config['settings']['sprint_end_date'])
1616+1717+ self.project: Project = project
1818+1919 def render(self):
2020- outstanding_points_by_day = self.project.outstanding_points_by_day(
2121- self.start_date,
2222- self.end_date)
2020+ outstanding_points_by_day = self.project.outstanding_points_by_date(
2121+ self.start_date_utc,
2222+ self.end_date_utc)
2323 # Load date dict for priority values with x being range of how many days are in sprint
2424 x = list(range(len(outstanding_points_by_day.keys())))
2525 y = list(outstanding_points_by_day.values())
···2727 # Plot point values for sprint along xaxis=range yaxis=points over time
2828 plt.plot(x, y)
2929 plt.axline((x[0], self.project.total_points),
3030- slope=-(self.project.total_points/(len(y)-1)),
3131- color="green",
3232- linestyle=(0, (5, 5)))
3030+ slope=-(self.project.total_points/(len(y)-1)),
3131+ color="green",
3232+ linestyle=(0, (5, 5)))
33333434 # Set sprint beginning
3535 plt.ylim(ymin=0)
3636 plt.xlim(xmin=x[0], xmax=x[-1])
37373838 # Replace xaxis range for date matching to range value
3939- plt.xticks(x, outstanding_points_by_day.keys())
3939+ date_labels = [str(parse_to_local(date))[:10]
4040+ for date in outstanding_points_by_day.keys()]
4141+ plt.xticks(x, date_labels)
4042 plt.xticks(rotation=90)
41434244 # Set titles and labels
+53-35
src/github_projects_burndown_chart/gh/project.py
···11from datetime import datetime, timedelta
22+from typing import Dict
33+from dateutil.parser import isoparse
2435from config import config
66+from util.dates import TODAY_UTC
475869class Project:
···1720 def total_points(self):
1821 return sum([column.get_total_points() for column in self.columns])
19222020- def points_completed_by_date(self, start_date, end_date):
2121- points_completed_by_date = {
2222- str(date)[:10] : 0
2323- for date in [
2424- start_date + timedelta(days=x)
2525- for x in range(0, (end_date - start_date).days + 1)
2626- ]
2727- }
2828- for column in self.columns:
2929- for card in column.cards:
3030- if card.closedAt:
3131- date_str = str(card.closedAt)[:10]
3232- points_completed_by_date[date_str] += card.points
2323+ def points_completed_by_date(self, start_date: datetime, end_date: datetime) -> Dict[datetime, int]:
2424+ """Computes the number of points completed by date.
2525+ Basically the data behind a burnup chart for the given date range.
2626+2727+ Args:
2828+ start_date (datetime): The start date of the chart in UTC.
2929+ end_date (datetime): The end date of the chart in UTC.
3030+3131+ Returns:
3232+ Dict[datetime, int]: A dictionary of date and points completed.
3333+ """
3434+ points_completed_by_date = {}
3535+3636+ cards = [card for column in self.columns for card in column.cards]
3737+ completed_cards = [card for card in cards if card.closedAt is not None]
3838+ sprint_dates = [start_date + timedelta(days=x)
3939+ # The +1 includes the end_date in the list
4040+ for x in range(0, (end_date - start_date).days + 1)]
4141+ for date in sprint_dates:
4242+ # Get the issues completed before midnight on the given date.
4343+ date_23_59 = date + timedelta(hours=23, minutes=59)
4444+ cards_done_by_date = [card for card in completed_cards
4545+ if card.closedAt <= date_23_59]
4646+ points_completed_by_date[date] = sum([card.points for card
4747+ in cards_done_by_date])
3348 return points_completed_by_date
34493535- def outstanding_points_by_day(self, start_date, end_date):
3636- outstanding_points_by_day = {}
3737- points_completed = 0
3838- points_completed_by_date = self.points_completed_by_date(start_date, end_date)
3939- current_date = datetime.now()
4040- for date in points_completed_by_date:
4141- points_completed += points_completed_by_date[date]
4242- if datetime.strptime(date, '%Y-%m-%d') < current_date:
4343- outstanding_points_by_day[date] = self.total_points - points_completed
4444- else:
4545- outstanding_points_by_day[date] = None
4646- return outstanding_points_by_day
5050+ def outstanding_points_by_date(self, start_date: datetime, end_date: datetime) -> Dict[datetime, int]:
5151+ """Computes the number of points remaining to be completed by date.
5252+ Basically the data behind a burndown chart for the given date range.
5353+5454+ Args:
5555+ start_date (datetime): The start date of the chart in UTC.
5656+ end_date (datetime): The end date of the chart in UTC.
5757+5858+ Returns:
5959+ Dict[datetime, int]: A dictionary of date and points remaining.
6060+ """
6161+ points_completed_by_date = self.points_completed_by_date(
6262+ start_date, end_date)
6363+ today_23_59 = TODAY_UTC + timedelta(hours=23, minutes=59)
6464+ return {
6565+ date: self.total_points - points_completed_by_date[date]
6666+ if date <= today_23_59 else None
6767+ for date in points_completed_by_date
6868+ }
476948704971class Column:
···6991 def __parse_createdAt(self, card_data):
7092 createdAt = None
7193 if card_data.get('createdAt'):
7272- createdAt = datetime.strptime(
7373- card_data['createdAt'][:10],
7474- '%Y-%m-%d')
9494+ createdAt = isoparse(card_data['createdAt'])
7595 return createdAt
76967797 def __parse_closedAt(self, card_data):
7898 closedAt = None
7999 if card_data.get('closedAt'):
8080- closedAt = datetime.strptime(
8181- card_data['closedAt'][:10],
8282- '%Y-%m-%d')
100100+ closedAt = isoparse(card_data['closedAt'])
83101 return closedAt
8410285103 def __parse_points(self, card_data):
···89107 card_points = 1
90108 else:
91109 card_labels = card_data.get('labels', {"nodes": []})['nodes']
9292- for label in card_labels:
9393- if points_label in label['name']:
9494- card_points += int(label['name'][len(points_label):])
9595- return card_points110110+ card_points = sum([int(label['name'][len(points_label):])
111111+ for label in card_labels
112112+ if points_label in label['name']])
113113+ return card_points