Skip to content

Commit

Permalink
merge main
Browse files Browse the repository at this point in the history
  • Loading branch information
andycochran committed Dec 18, 2023
2 parents e47b06b + 6b301c3 commit 42c14c5
Show file tree
Hide file tree
Showing 17 changed files with 722 additions and 50 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci-analytics.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ jobs:
run: make lint

- name: Run tests
run: make test
run: make test-audit

- name: Export GitHub data
run: make gh-data-export
Expand Down
19 changes: 15 additions & 4 deletions analytics/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ ISSUE_FILE ?= data/issue-data.json
SPRINT ?= @current
UNIT ?= points
ACTION ?= show-results
MIN_TEST_COVERAGE ?= 80

check-prereqs:
@echo "=> Checking for pre-requisites"
Expand All @@ -32,10 +33,20 @@ lint:
@echo "============================="
@echo "=> All checks succeeded"

test:
@echo "=> Running tests"
unit-test:
@echo "=> Running unit tests"
@echo "============================="
$(POETRY) run pytest
$(POETRY) run pytest --cov=src

e2e-test:
@echo "=> Running end-to-end tests"
@echo "============================="
$(POETRY) run pytest tests/integrations --cov-append=src

test-audit: unit-test
@echo "=> Running test coverage report"
@echo "============================="
$(POETRY) run coverage report --show-missing --fail-under=$(MIN_TEST_COVERAGE)

sprint-data-export:
@echo "=> Exporting project data from the sprint board"
Expand All @@ -61,7 +72,7 @@ sprint-burndown:
poetry run analytics calculate sprint_burndown \
--sprint-file $(SPRINT_FILE) \
--issue-file $(ISSUE_FILE) \
--sprint $(SPRINT) \
--sprint "$(SPRINT)" \
--unit $(UNIT) \
--$(ACTION)

Expand Down
84 changes: 83 additions & 1 deletion analytics/poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions analytics/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ black = "^23.7.0"
mypy = "^1.4.1"
pylint = "^3.0.2"
pytest = "^7.4.0"
pytest-cov = "^4.1.0"
ruff = "^0.0.278"
safety = "^2.3.5"

Expand All @@ -49,6 +50,7 @@ disable = [
"W0511", # fix-me
"R0913", # too-many-arguments
"R0902", # too-many-instance-attributes
"R0903", # too-few-public-methods
]

[tool.ruff]
Expand All @@ -67,3 +69,10 @@ ignore = [
]
line-length = 100
select = ["ALL"]

[tool.pytest.ini_options]
filterwarnings = [
# kaleido is throwing a Deprecation warning in one of its dependencies
# TODO(widal001): 2022-12-12 - Try removing after Kaleido issues a new release
'ignore:.*setDaemon\(\) is deprecated.*:DeprecationWarning',
]
9 changes: 7 additions & 2 deletions analytics/src/analytics/datasets/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,14 @@ def from_dict(cls, data: list[dict]) -> Self:
"""Load the dataset from a list of python dictionaries representing records."""
return cls(df=pd.DataFrame(data))

def to_csv(self, output_file: str) -> None:
def to_csv(
self,
output_file: str,
*, # force include_index to be passed as keyword instead of positional arg
include_index: bool = False,
) -> None:
"""Export the dataset to a csv."""
return self.df.to_csv(output_file)
return self.df.to_csv(output_file, index=include_index)

def to_dict(self) -> list[dict]:
"""Export the dataset to a list of python dictionaries representing records."""
Expand Down
6 changes: 3 additions & 3 deletions analytics/src/analytics/datasets/sprint_board.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,14 @@ def sprints(self) -> pd.DataFrame:
@property
def current_sprint(self) -> str | None:
"""Return the name of the current sprint, if a sprint is currently active."""
return self.get_sprint_name_from_date(pd.Timestamp.today())
return self.get_sprint_name_from_date(pd.Timestamp.today().floor("d"))

def get_sprint_name_from_date(self, date: pd.Timestamp) -> str | None:
"""Get the name of a sprint from a given date, if that date falls in a sprint."""
# fmt: off
date_filter = (
(self.sprints[self.sprint_start_col] <= date) # after sprint start
& (self.sprints[self.sprint_end_col] > date) # before sprint end
(self.sprints[self.sprint_start_col] < date) # after sprint start
& (self.sprints[self.sprint_end_col] >= date) # before sprint end
)
# fmt: on
matching_sprints = self.sprints.loc[date_filter, self.sprint_col]
Expand Down
33 changes: 17 additions & 16 deletions analytics/src/analytics/metrics/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ class Statistic:
class BaseMetric:
"""Base class for all metrics."""

CHART_PNG = "data/chart-static.png"
CHART_HTML = "data/chart-interactive.html"
RESULTS_CSV = "data/results.csv"
CHART_PNG = "chart-static.png"
CHART_HTML = "chart-interactive.html"
RESULTS_CSV = "results.csv"

def __init__(self) -> None:
"""Initialize and calculate the metric from the input dataset."""
Expand Down Expand Up @@ -64,29 +64,29 @@ def plot_results(self) -> Figure:
"""Create a plotly chart that visually represents the results."""
raise NotImplementedError

def export_results(self) -> Path:
def export_results(self, output_dir: Path = Path("data")) -> Path:
"""Export the self.results dataframe to a csv file."""
# make sure the parent directory exists
output_path = Path(self.RESULTS_CSV)
output_path.parent.mkdir(exist_ok=True, parents=True)
output_dir.mkdir(exist_ok=True, parents=True)
output_path = output_dir / self.RESULTS_CSV
# export results dataframe to a csv
self.results.to_csv(output_path)
return output_path

def export_chart_to_html(self) -> Path:
def export_chart_to_html(self, output_dir: Path = Path("data")) -> Path:
"""Export the plotly chart in self.chart to a png file."""
# make sure the parent directory exists
output_path = Path(self.CHART_HTML)
output_path.parent.mkdir(exist_ok=True, parents=True)
output_dir.mkdir(exist_ok=True, parents=True)
output_path = output_dir / self.CHART_HTML
# export chart to a png
self.chart.write_html(output_path)
return output_path

def export_chart_to_png(self) -> Path:
def export_chart_to_png(self, output_dir: Path = Path("data")) -> Path:
"""Export the plotly chart in self.chart to a png file."""
# make sure the parent directory exists
output_path = Path(self.CHART_PNG)
output_path.parent.mkdir(exist_ok=True, parents=True)
output_dir.mkdir(exist_ok=True, parents=True)
output_path = output_dir / self.CHART_PNG
# export chart to a png
self.chart.write_image(output_path, width=900)
return output_path
Expand All @@ -103,11 +103,12 @@ def post_results_to_slack(
self,
slackbot: SlackBot,
channel_id: str,
output_dir: Path = Path("data"),
) -> None:
"""Upload copies of the results and chart to a slack channel.."""
results_csv = self.export_results()
chart_png = self.export_chart_to_png()
chart_html = self.export_chart_to_html()
"""Upload copies of the results and chart to a slack channel."""
results_csv = self.export_results(output_dir)
chart_png = self.export_chart_to_png(output_dir)
chart_html = self.export_chart_to_html(output_dir)
files = [
FileMapping(path=str(results_csv), name=results_csv.name),
FileMapping(path=str(chart_png), name=chart_png.name),
Expand Down
23 changes: 23 additions & 0 deletions analytics/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from pathlib import Path

import pandas as pd
import pytest

# skips the integration tests in tests/integrations/
# to run the integration tests, invoke them directly: pytest tests/integrations/
Expand All @@ -21,6 +22,28 @@
DAY_5 = "2023-11-05"


class MockSlackbot:
"""Create a mock slackbot issue for unit tests."""

def upload_files_to_slack_channel(
self,
channel_id: str,
files: list,
message: str,
) -> None:
"""Stubs the corresponding method on the main SlackBot class."""
assert isinstance(channel_id, str)
print("Fake posting the following files to Slack with this message:")
print(message)
print(files)


@pytest.fixture(name="mock_slackbot")
def mock_slackbot_fixture():
"""Create a mock slackbot instance to stub post_to_slack() method."""
return MockSlackbot()


def write_test_data_to_file(data: dict, output_file: str):
"""Write test JSON data to a file for use in a test."""
parent_dir = Path(output_file).parent
Expand Down
38 changes: 38 additions & 0 deletions analytics/tests/datasets/test_base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""Test the BaseDataset class."""
from pathlib import Path # noqa: I001

import pandas as pd

from analytics.datasets.base import BaseDataset

TEST_DATA = [
{"Col A": 1, "Col b": "One"},
{"Col A": 2, "Col b": "Two"},
{"Col A": 3, "Col b": "Three"},
]


def test_to_and_from_csv(tmp_path: Path):
"""BaseDataset should write to csv with to_csv() and load from a csv with from_csv()."""
# setup - create sample dataframe and instantiate class
test_df = pd.DataFrame(TEST_DATA)
dataset_in = BaseDataset(test_df)
# setup - set output path and check that it doesn't exist
output_csv = tmp_path / "dataset.csv"
assert output_csv.exists() is False
# execution - write to csv and read from csv
dataset_in.to_csv(output_csv)
dataset_out = BaseDataset.from_csv(output_csv)
# validation - check that csv exists and that datasets match
assert output_csv.exists()
assert dataset_in.df.equals(dataset_out.df)


def test_to_and_from_dict():
"""BaseDataset should have same input and output with to_dict() and from_dict()."""
# execution
dict_in = TEST_DATA
dataset = BaseDataset.from_dict(TEST_DATA)
dict_out = dataset.to_dict()
# validation
assert dict_in == dict_out
Loading

0 comments on commit 42c14c5

Please sign in to comment.