From edc74e172d95213694821c1bc01b1522eaaf0363 Mon Sep 17 00:00:00 2001 From: Joseph Hale Date: Sat, 22 Jan 2022 12:23:15 -0700 Subject: [PATCH] Add support for custom point calculators Different people will want to calculate the points for their burndown charts in different ways. This commit breaks a lot of cross module dependencies to add support for a custom `PointsCalculator` interface, which follows the Strategy design pattern for calculating the points over time to show on the burndown chart. Four calculators are made available by default (see `README.md`). Additional calculators may be added over time or by PR. Closes #23 by providing the `taiga` calculator. --- README.md | 1 + docs/images/example_burndown_chart.png | Bin 57088 -> 57088 bytes .../chart/burndown.py | 109 +++++++++++------- .../config/__init__.py | 16 +++ .../gh/api_wrapper.py | 43 ++++++- .../gh/project.py | 71 +++--------- .../gh/queries/OrganizationProject.graphql | 8 ++ .../gh/queries/RepositoryProject.graphql | 11 +- src/github_projects_burndown_chart/main.py | 60 +++++++--- .../util/__init__.py | 19 +++ .../util/calculators.py | 52 +++++++++ .../util/dates.py | 12 +- .../util/stats.py | 36 ++++++ 13 files changed, 323 insertions(+), 115 deletions(-) create mode 100644 src/github_projects_burndown_chart/util/calculators.py create mode 100644 src/github_projects_burndown_chart/util/stats.py diff --git a/README.md b/README.md index f8bdd32..0301dcf 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,7 @@ This allows the `.gitignore` to exclude your `config.json` from being accidental | `sprint_end_date` | The last day of the sprint formatted as `YYYY-MM-DD`.

Must be entered here since GitHub Project Boards don't have an assigned start/end date.

Example: `2021-10-21` | | `chart_end_date` | (OPTIONAL) The last day to show on the burndown chart formatted as `YYYY-MM-DD`.

Used to change the end date of the chart without affecting the slope of the ideal burndown line (e.g. to show tasks that were completed after the official end of a sprint).

Example: `2021-10-24` | | `points_label` | (OPTIONAL) The prefix for issue labels containing the point value of the issue. Removing this prefix must leave just an integer. If set to `null`, the burndown chart will count open issues instead of points.

Example: `Points: ` (with the space) | +| `calculators` | (OPTIONAL) A list of the calculator(s) to use to calculate the point burndown lines to show on the burndown chart. (DEFAULT: [`closed`])

_OPTIONS:_ `closed`, `assigned`, `created`, `taiga`

Example: [`taiga`, `closed`, `assigned`] | #### Organization Projects All settings are the same as for the [Repository Projects](#repository-projects), except `repo_owner` and `repo_name` are replaced with `organization_name` as shown below. diff --git a/docs/images/example_burndown_chart.png b/docs/images/example_burndown_chart.png index c4d15764811c3daf2865cceba59f0ca872336726..452a31fac144923f12e31c2e6bd726ce62f1deef 100644 GIT binary patch delta 36452 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 delta 36452 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

(j|pa-zV2z1(RRI51=9n`n%qokm-)dJzU)c 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 diff --git a/src/github_projects_burndown_chart/chart/burndown.py b/src/github_projects_burndown_chart/chart/burndown.py index cb0a778..37988a0 100644 --- a/src/github_projects_burndown_chart/chart/burndown.py +++ b/src/github_projects_burndown_chart/chart/burndown.py @@ -1,58 +1,83 @@ +from dataclasses import dataclass, field from datetime import datetime +from typing import Any, Dict, Iterable import matplotlib.pyplot as plt import os -from config import config -from gh.project import Project -from util.dates import parse_to_local, parse_to_utc +from util.dates import parse_to_local, date_range -class BurndownChart: +@dataclass +class BurndownChartDataSeries: + name: str + data: Iterable[Dict[datetime, int]] + format: Dict[str, Any] + + +def default_ideal_trendline_format() -> Dict[str, Any]: + return dict( + color="grey", + linestyle=(0, (5, 5)) + ) + + +@dataclass +class BurndownChartData: + sprint_name: str + utc_chart_start: datetime + utc_chart_end: datetime + utc_sprint_start: datetime + utc_sprint_end: datetime + total_points: int + series: Iterable[BurndownChartDataSeries] + points_label: str = "Outstanding Points" + ideal_trendline_format: Dict[str, Any] = field( + default_factory=default_ideal_trendline_format) + - def __init__(self, project: Project): - self.start_date_utc: datetime = parse_to_utc( - config['settings']['sprint_start_date']) - self.end_date_utc: datetime = parse_to_utc( - config['settings']['sprint_end_date']) - self.chart_end_date_utc: datetime = parse_to_utc( - config['settings']['chart_end_date']) \ - if config['settings'].get('chart_end_date') else None +class BurndownChart: - self.project: Project = project + def __init__(self, data: BurndownChartData): + self.data: BurndownChartData = data def __prepare_chart(self): - end_date = self.chart_end_date_utc if self.chart_end_date_utc else self.end_date_utc - outstanding_points_by_day = self.project.outstanding_points_by_date( - self.start_date_utc, - end_date) - # Load date dict for priority values with x being range of how many days are in sprint - x = list(range(len(outstanding_points_by_day.keys()))) - y = list(outstanding_points_by_day.values()) - sprint_days = (self.end_date_utc - self.start_date_utc).days - - # Plot point values for sprint along xaxis=range yaxis=points over time - plt.plot(x, y) - plt.axline((x[0], self.project.total_points), - slope=-(self.project.total_points/(sprint_days)), - color="green", - linestyle=(0, (5, 5))) - - # Set sprint beginning - plt.ylim(ymin=0) - plt.xlim(xmin=x[0], xmax=x[-1]) - - # Replace xaxis range for date matching to range value - date_labels = [str(parse_to_local(date))[:10] - for date in outstanding_points_by_day.keys()] - plt.xticks(x, date_labels) - plt.xticks(rotation=90) + # Plot the data + chart_dates = date_range( + self.data.utc_chart_start, self.data.utc_chart_end) + for series in self.data.series: + series_dates = [chart_dates.index(date) + for date in series.data.keys()] + series_points = list(series.data.values()) + plt.plot( + series_dates, + series_points, + label=series.name, + **series.format + ) + plt.legend() - # Set titles and labels - plt.title(f"{self.project.name}: Burndown Chart") - points_label = config['settings']['points_label'] - plt.ylabel(f"Outstanding {'Points' if points_label else 'Issues'}") + # Configure title and labels + plt.title(f"{self.data.sprint_name}: Burndown Chart") + plt.ylabel(self.data.points_label) plt.xlabel("Date") + # Configure axes limits + plt.ylim(ymin=0, ymax=self.data.total_points * 1.1) + plt.xlim(xmin=chart_dates.index(self.data.utc_chart_start), + xmax=chart_dates.index(self.data.utc_chart_end)) + + # Configure x-axis tick marks + date_labels = [str(parse_to_local(date))[:10] for date in chart_dates] + plt.xticks(range(len(chart_dates)), date_labels) + plt.xticks(rotation=90) + + # Plot the ideal trendline + sprint_days = (self.data.utc_sprint_end - + self.data.utc_sprint_start).days + plt.axline((chart_dates.index(self.data.utc_sprint_start), self.data.total_points), + slope=-(self.data.total_points/(sprint_days)), + **self.data.ideal_trendline_format) + def generate_chart(self, path): self.__prepare_chart() if not os.path.exists(path): diff --git a/src/github_projects_burndown_chart/config/__init__.py b/src/github_projects_burndown_chart/config/__init__.py index 0188b86..ad04ea7 100644 --- a/src/github_projects_burndown_chart/config/__init__.py +++ b/src/github_projects_burndown_chart/config/__init__.py @@ -1,7 +1,10 @@ +from datetime import datetime import json import os import logging +from util.dates import parse_to_utc + # Set up logging __logger = logging.getLogger(__name__) __ch = logging.StreamHandler() @@ -39,6 +42,15 @@ def set_project(self, project_type: str, project_name: str): self.project_type = project_type self.project_name = project_name + def utc_sprint_start(self) -> datetime: + return self.__get_date('sprint_start_date') + + def utc_sprint_end(self) -> datetime: + return self.__get_date('sprint_end_date') + + def utc_chart_end(self) -> datetime: + return self.__get_date('chart_end_date') + def __getitem__(self, key: str): if not hasattr(self, 'project_type'): raise AttributeError('No project has been set.') @@ -46,6 +58,10 @@ def __getitem__(self, key: str): raise AttributeError('No project has been set.') return self.raw_config[self.project_type][self.project_name][key] + def __get_date(self, name: str) -> datetime: + date = self['settings'].get(name) + return parse_to_utc(date) if date else None + config = Config(__config) diff --git a/src/github_projects_burndown_chart/gh/api_wrapper.py b/src/github_projects_burndown_chart/gh/api_wrapper.py index 61a53f6..8bd5797 100644 --- a/src/github_projects_burndown_chart/gh/api_wrapper.py +++ b/src/github_projects_burndown_chart/gh/api_wrapper.py @@ -1,6 +1,10 @@ import logging +import os import requests -from requests.api import head +from datetime import date +import hashlib +import json +import tempfile from config import config, secrets from .project import Project @@ -29,13 +33,25 @@ def get_organization_project() -> dict: def gh_api_query(query: str, variables: dict) -> dict: + response = __get_from_cache(query, variables) + if not response: + response = __get_from_api(query, variables) + __cache_response(query, variables, response) + return response + + +def prepare_payload(query, variables): + return {'query': query, 'variables': variables} + + +def __get_from_api(query, variables): headers = {'Authorization': 'bearer %s' % secrets['github_token']} \ if 'github_token' in secrets else {} response = requests.post( 'https://api.github.com/graphql', headers=headers, - json={'query': query, 'variables': variables}).json() + json=prepare_payload(query, variables)).json() # Gracefully report failures due to bad credentials if response.get('message') and response['message'] == 'Bad credentials': @@ -53,3 +69,26 @@ def gh_api_query(query: str, variables: dict) -> dict: __logger.critical(response['errors']) exit(1) return response + + +def __get_from_cache(query, variables): + temp_path = __temp_path(query, variables) + if os.path.exists(temp_path): + with open(temp_path, 'r') as f: + return json.load(f) + return None + + +def __cache_response(query, variables, response): + temp_path = __temp_path(query, variables) + with open(temp_path, 'w') as f: + json.dump(response, f) + + +def __temp_path(query, variables): + temp_dir = tempfile.gettempdir() + payload = prepare_payload(query, variables) + payload.update({'today': str(date.today())}) + filename = f"{hashlib.sha256(json.dumps(payload).encode('utf-8')).hexdigest()}.json" + temp_path = os.path.join(temp_dir, filename) + return temp_path diff --git a/src/github_projects_burndown_chart/gh/project.py b/src/github_projects_burndown_chart/gh/project.py index 5e003b7..7f93ea2 100644 --- a/src/github_projects_burndown_chart/gh/project.py +++ b/src/github_projects_burndown_chart/gh/project.py @@ -1,9 +1,7 @@ -from datetime import datetime, timedelta -from typing import Dict +from datetime import datetime from dateutil.parser import isoparse from config import config -from util.dates import TODAY_UTC class Project: @@ -20,52 +18,9 @@ def __parse_columns(self, project_data): def total_points(self): return sum([column.get_total_points() for column in self.columns]) - def points_completed_by_date(self, start_date: datetime, end_date: datetime) -> Dict[datetime, int]: - """Computes the number of points completed by date. - Basically the data behind a burnup chart for the given date range. - - Args: - start_date (datetime): The start date of the chart in UTC. - end_date (datetime): The end date of the chart in UTC. - - Returns: - Dict[datetime, int]: A dictionary of date and points completed. - """ - points_completed_by_date = {} - - cards = [card for column in self.columns for card in column.cards] - completed_cards = [card for card in cards if card.closedAt is not None] - sprint_dates = [start_date + timedelta(days=x) - # The +1 includes the end_date in the list - for x in range(0, (end_date - start_date).days + 1)] - for date in sprint_dates: - # Get the issues completed before midnight on the given date. - date_23_59 = date + timedelta(hours=23, minutes=59) - cards_done_by_date = [card for card in completed_cards - if card.closedAt <= date_23_59] - points_completed_by_date[date] = sum([card.points for card - in cards_done_by_date]) - return points_completed_by_date - - def outstanding_points_by_date(self, start_date: datetime, end_date: datetime) -> Dict[datetime, int]: - """Computes the number of points remaining to be completed by date. - Basically the data behind a burndown chart for the given date range. - - Args: - start_date (datetime): The start date of the chart in UTC. - end_date (datetime): The end date of the chart in UTC. - - Returns: - Dict[datetime, int]: A dictionary of date and points remaining. - """ - points_completed_by_date = self.points_completed_by_date( - start_date, end_date) - today_23_59 = TODAY_UTC + timedelta(hours=23, minutes=59) - return { - date: self.total_points - points_completed_by_date[date] - if date <= today_23_59 else None - for date in points_completed_by_date - } + @property + def cards(self): + return [card for column in self.columns for card in column.cards] class Column: @@ -84,23 +39,31 @@ def get_total_points(self): class Card: def __init__(self, card_data): card_data = card_data['content'] if card_data['content'] else card_data - self.createdAt = self.__parse_createdAt(card_data) - self.closedAt = self.__parse_closedAt(card_data) + self.created: datetime = self.__parse_createdAt(card_data) + self.assigned: datetime = self.__parse_assignedAt(card_data) + self.closed: datetime = self.__parse_closedAt(card_data) self.points = self.__parse_points(card_data) - def __parse_createdAt(self, card_data): + def __parse_assignedAt(self, card_data) -> datetime: + assignedAt = None + assignedDates = card_data.get('timelineItems', {}).get('nodes', []) + if assignedDates: + assignedAt = isoparse(assignedDates[0]['createdAt']) + return assignedAt + + def __parse_createdAt(self, card_data) -> datetime: createdAt = None if card_data.get('createdAt'): createdAt = isoparse(card_data['createdAt']) return createdAt - def __parse_closedAt(self, card_data): + def __parse_closedAt(self, card_data) -> datetime: closedAt = None if card_data.get('closedAt'): closedAt = isoparse(card_data['closedAt']) return closedAt - def __parse_points(self, card_data): + def __parse_points(self, card_data) -> int: card_points = 0 points_label = config['settings']['points_label'] if not points_label: diff --git a/src/github_projects_burndown_chart/gh/queries/OrganizationProject.graphql b/src/github_projects_burndown_chart/gh/queries/OrganizationProject.graphql index 1bbd376..1752616 100644 --- a/src/github_projects_burndown_chart/gh/queries/OrganizationProject.graphql +++ b/src/github_projects_burndown_chart/gh/queries/OrganizationProject.graphql @@ -13,6 +13,14 @@ query OrganizationProject($organization_name: String!, $project_number: Int!, $c content { ... on Issue { title + timelineItems(first: 20, itemTypes: [ASSIGNED_EVENT]) { + nodes { + __typename + ... on AssignedEvent { + createdAt + } + } + } createdAt closedAt labels(first: $labels_per_issue_count) { diff --git a/src/github_projects_burndown_chart/gh/queries/RepositoryProject.graphql b/src/github_projects_burndown_chart/gh/queries/RepositoryProject.graphql index 93a16b0..0b73bb2 100644 --- a/src/github_projects_burndown_chart/gh/queries/RepositoryProject.graphql +++ b/src/github_projects_burndown_chart/gh/queries/RepositoryProject.graphql @@ -1,4 +1,3 @@ -# Heavily inspired by https://github.com/radekstepan/burnchart/issues/129#issuecomment-394469442 query RepositoryProject($repo_owner: String!, $repo_name: String!, $project_number: Int!, $column_count: Int!, $max_cards_per_column_count: Int!, $labels_per_issue_count: Int!) { repository(owner: $repo_owner, name: $repo_name) { project(number: $project_number) { @@ -15,6 +14,14 @@ query RepositoryProject($repo_owner: String!, $repo_name: String!, $project_numb content { ... on Issue { title + timelineItems(first: 20, itemTypes: [ASSIGNED_EVENT]) { + nodes { + __typename + ... on AssignedEvent { + createdAt + } + } + } createdAt closedAt labels(first: $labels_per_issue_count) { @@ -30,4 +37,4 @@ query RepositoryProject($repo_owner: String!, $repo_name: String!, $project_numb } } } -} +} \ No newline at end of file diff --git a/src/github_projects_burndown_chart/main.py b/src/github_projects_burndown_chart/main.py index 2c27f8d..3b9143c 100644 --- a/src/github_projects_burndown_chart/main.py +++ b/src/github_projects_burndown_chart/main.py @@ -1,33 +1,65 @@ import argparse -from chart.burndown import BurndownChart +from chart.burndown import * from config import config from discord import webhook from gh.api_wrapper import get_organization_project, get_repository_project from gh.project import Project +from util import calculators, colors +from util.stats import * +from util.calculators import * -if __name__ == '__main__': - # Parse the command line arguments + +def parse_cli_args(): parser = argparse.ArgumentParser( description='Generate a burndown chart for a GitHub project.') - parser.add_argument("project_type", - choices=['repository', 'organization'], + parser.add_argument("project_type", choices=['repository', 'organization'], help="The type of project to generate a burndown chart for. Can be either 'organization' or 'repository'.") - parser.add_argument("project_name", help="The name of the project as it appears in the config.json") - parser.add_argument("--discord", action='store_true', help="If present, posts the burndown chart to the configured webhook") - args = parser.parse_args() + parser.add_argument("project_name", + help="The name of the project as it appears in the config.json") + parser.add_argument("--discord", action='store_true', + help="If present, posts the burndown chart to the configured webhook") + return parser.parse_args() - # Point the config to the correct project - config.set_project(args.project_type, args.project_name) - - # Pull the corresponding project from GitHub + +def download_project_data(args): if args.project_type == 'repository': project: Project = get_repository_project() elif args.project_type == 'organization': project: Project = get_organization_project() - + return project + + +def prepare_chart_data(stats: ProjectStats): + color = colors() + data = BurndownChartData( + sprint_name=stats.project.name, + utc_chart_start=config.utc_sprint_start(), + utc_chart_end=config.utc_chart_end() or config.utc_sprint_end(), + utc_sprint_start=config.utc_sprint_start(), + utc_sprint_end=config.utc_sprint_end(), + total_points=stats.total_points, + series=[ + BurndownChartDataSeries( + name=pts_type, + data=stats.remaining_points_by_date( + calculators(stats.project)[pts_type]), + format=dict(color=next(color)) + ) for pts_type in config['settings'].get('calculators', ['closed']) + ], + points_label=f"Outstanding {'Points' if config['settings']['points_label'] else 'Issues'}" + ) + return data + + +if __name__ == '__main__': + args = parse_cli_args() + config.set_project(args.project_type, args.project_name) + project = download_project_data(args) + stats = ProjectStats(project, config.utc_sprint_start(), + config.utc_chart_end() or config.utc_sprint_end()) # Generate the burndown chart - burndown_chart = BurndownChart(project) + burndown_chart = BurndownChart(prepare_chart_data(stats)) if args.discord: chart_path = "./tmp/chart.png" burndown_chart.generate_chart(chart_path) diff --git a/src/github_projects_burndown_chart/util/__init__.py b/src/github_projects_burndown_chart/util/__init__.py index e69de29..019d9f3 100644 --- a/src/github_projects_burndown_chart/util/__init__.py +++ b/src/github_projects_burndown_chart/util/__init__.py @@ -0,0 +1,19 @@ +from gh.project import Project +from util.calculators import * + + +def calculators(project: Project): + return { + 'created': CreatedPointsCalculator(project.cards), + 'assigned': AssignedPointsCalculator(project.cards), + 'closed': ClosedPointsCalculator(project.cards), + 'taiga': TaigaPointsCalculator(project.cards) + } + +def colors(): + count = -1 + colors = ['blue', 'green', 'gold', 'red', 'orange', + 'purple', 'pink', 'brown', 'black', 'grey'] + while True: + count += 1 + yield colors[count % len(colors)] \ No newline at end of file diff --git a/src/github_projects_burndown_chart/util/calculators.py b/src/github_projects_burndown_chart/util/calculators.py new file mode 100644 index 0000000..738743a --- /dev/null +++ b/src/github_projects_burndown_chart/util/calculators.py @@ -0,0 +1,52 @@ +from datetime import datetime +from typing import List + +from gh.project import Card + + +class PointsCalculator: + + def __init__(self, cards: List[Card]): + self.cards: List[Card] = cards + + def points_as_of(self, date: datetime): + raise NotImplementedError() + + +class ClosedPointsCalculator(PointsCalculator): + + def points_as_of(self, date: datetime): + return sum(card.points for card in self.cards + if isinstance(card.closed, datetime) + and card.closed <= date) + + +class AssignedPointsCalculator(PointsCalculator): + + def points_as_of(self, date: datetime): + return sum(card.points for card in self.cards + if isinstance(card.assigned, datetime) + and card.assigned <= date) + + +class CreatedPointsCalculator(PointsCalculator): + + def points_as_of(self, date: datetime): + return sum(card.points for card in self.cards + if isinstance(card.created, datetime) + and card.created <= date) + + +class TaigaPointsCalculator(PointsCalculator): + + def points_as_of(self, date: datetime): + closed_by_date = [card for card in self.cards + if isinstance(getattr(card, 'closed'), datetime) + and getattr(card, 'closed') <= date] + points = sum(card.points for card in closed_by_date) + assigned_by_date = [card for card in self.cards + if isinstance(getattr(card, 'assigned'), datetime) + and getattr(card, 'assigned') <= date + and card not in closed_by_date] + points += sum(card.points / 2 for card in assigned_by_date) + return points diff --git a/src/github_projects_burndown_chart/util/dates.py b/src/github_projects_burndown_chart/util/dates.py index 5f0a68e..d61e7ca 100644 --- a/src/github_projects_burndown_chart/util/dates.py +++ b/src/github_projects_burndown_chart/util/dates.py @@ -1,9 +1,11 @@ from datetime import datetime, timedelta, timezone from dateutil import parser +from typing import List UTC_OFFSET: timedelta = datetime.utcnow() - datetime.now() """Local time + UTC_OFFSET = UTC Time""" + def parse_to_utc(date_string: str) -> datetime: """ Parse a date string to UTC time. @@ -12,10 +14,18 @@ def parse_to_utc(date_string: str) -> datetime: datetime_utc = raw_datetime.replace(tzinfo=timezone.utc) return datetime_utc + def parse_to_local(datetime_utc: datetime) -> datetime: """ Parse a datetime object to local time. """ return datetime_utc.astimezone() -TODAY_UTC: datetime = parse_to_utc(datetime.today().strftime('%Y-%m-%d')) \ No newline at end of file + +def date_range(start_date: datetime, end_date: datetime) -> List[datetime]: + # The +1 includes the end_date in the list + num_days = (end_date - start_date).days + 1 + return [start_date + timedelta(days=x) for x in range(0, num_days)] + + +TODAY_UTC: datetime = parse_to_utc(datetime.today().strftime('%Y-%m-%d')) diff --git a/src/github_projects_burndown_chart/util/stats.py b/src/github_projects_burndown_chart/util/stats.py new file mode 100644 index 0000000..0feb0a8 --- /dev/null +++ b/src/github_projects_burndown_chart/util/stats.py @@ -0,0 +1,36 @@ +from datetime import datetime, timedelta +from typing import Dict, Iterable +from gh.project import * +from util.dates import TODAY_UTC, date_range +from util.calculators import * + + +class ProjectStats(): + + def __init__(self, project: Project, start_date: datetime, end_date: datetime): + self.start_date: datetime = start_date + self.end_date: datetime = end_date + self.project: Project = project + + @property + def total_points(self) -> int: + return self.project.total_points + + def points_by_date(self, calculator: PointsCalculator) -> Dict[datetime, int]: + points = {} + sprint_dates: Iterable[datetime] = date_range( + self.start_date, self.end_date) + for date in sprint_dates: + # Get the issues completed before midnight on the given date. + date_23_59 = date + timedelta(hours=23, minutes=59) + points[date] = calculator.points_as_of(date_23_59) + return points + + def remaining_points_by_date(self, calculator: PointsCalculator) -> Dict[datetime, int]: + points_by_date = self.points_by_date(calculator) + today_23_59 = TODAY_UTC + timedelta(hours=23, minutes=59) + return { + date: self.total_points - points_by_date[date] + if date <= today_23_59 else None + for date in points_by_date + }