Skip to content

Commit 570db7e

Browse files
Merge pull request #25 from stefanv/template-publish
Publish from templates
2 parents 954d80a + 2683e0b commit 570db7e

15 files changed

Lines changed: 755 additions & 153 deletions

devstats/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
source
2+
build

devstats/__main__.py

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,42 @@
33
import re
44
import sys
55
from glob import glob
6+
import collections
67

78
import click
89
import requests
910

1011
from .query import GithubGrabber
11-
from .publish import publisher
12+
from .publish import template, publish
1213

1314

14-
@click.group()
15+
class OrderedGroup(click.Group):
16+
def __init__(self, name=None, commands=None, **attrs):
17+
super().__init__(name, commands, **attrs)
18+
self.commands = commands or collections.OrderedDict()
19+
20+
def list_commands(self, ctx):
21+
return self.commands
22+
23+
24+
@click.group(cls=OrderedGroup)
1525
def cli():
1626
pass
1727

1828

1929
@cli.command("query")
2030
@click.argument("repo_owner")
2131
@click.argument("repo_name")
22-
def query(repo_owner, repo_name):
23-
"""Download and save issue and pr data for `repo_owner`/`repo_name`."""
32+
@click.option(
33+
"-o",
34+
"--outdir",
35+
default="devstats-data",
36+
help="Output directory",
37+
show_default=True,
38+
)
39+
def query(repo_owner, repo_name, outdir):
40+
"""Download and save issue and pr data for `repo_owner`/`repo_name`"""
41+
os.makedirs(outdir, exist_ok=True)
2442

2543
try:
2644
token = os.environ["GRAPH_API_KEY"]
@@ -59,11 +77,8 @@ def query(repo_owner, repo_name):
5977
)
6078
data.get()
6179
ftype = {"issues": "issues", "pullRequests": "PRs"}
62-
data.dump(f"{repo_name}_{ftype.get(qtype, qtype)}.json")
80+
data.dump(f"{outdir}/{repo_name}_{ftype.get(qtype, qtype)}.json")
6381

6482

65-
@cli.command("publish")
66-
@click.argument("project")
67-
def publish(project):
68-
"""Generate myst report for `repo_owner`/`repo_name`."""
69-
publisher(project)
83+
cli.add_command(template)
84+
cli.add_command(publish)

devstats/publish.py

Lines changed: 84 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,94 @@
11
import os
22
import sys
33
from glob import glob
4-
from pathlib import Path
4+
import shutil
5+
import re
6+
import functools
57

8+
import click
69

7-
def publisher(project):
8-
print(f"Generating {project} report...", end="")
9-
basedir = os.path.dirname(__file__)
10-
with open(os.path.join(basedir, "template.md")) as fh:
11-
template = fh.read()
1210

13-
issues = glob(os.path.join(basedir, "reports/issues/*.md"))
14-
for issue in issues:
15-
with open(issue) as fh:
16-
issue_text = fh.read()
17-
issue_name = Path(issue).stem
18-
template = template.replace(f"{{{{ {issue_name} }}}}", issue_text)
11+
@click.command()
12+
@click.argument("project")
13+
@click.option(
14+
"-o", "--outdir", default="source", help="Output directory", show_default=True
15+
)
16+
def template(project, outdir):
17+
"""Generate myst report templates
1918
20-
prs = glob(os.path.join(basedir, "reports/pull_requests/*.md"))
21-
for pr in prs:
22-
with open(pr) as fh:
23-
pr_text = fh.read()
24-
pr_name = Path(pr).stem
25-
template = template.replace(f"{{{{ {pr_name} }}}}", pr_text)
19+
These templates are copied from `devstats`, and still need to be compiled
20+
to substitute variables.
21+
"""
22+
os.makedirs(outdir, exist_ok=True)
23+
os.makedirs(os.path.join(outdir, project), exist_ok=True)
2624

27-
template = template.replace("{{ project }}", project)
25+
print(f"Populating [{outdir}] with templates for [{project}] report:", flush=True)
2826

29-
os.makedirs("_generated", exist_ok=True)
30-
with open(f"_generated/{project}.md", "w") as fh:
31-
fh.write(template)
27+
report_files = glob(os.path.join(os.path.dirname(__file__), "reports/*.md"))
28+
for f in report_files:
29+
dest = f"{outdir}/{project}/{os.path.basename(f)}"
30+
print(f" - {dest}")
31+
shutil.copyfile(f, dest)
3232

33-
print("OK")
33+
34+
def _include_file(basedir, x):
35+
fn = x.group(1)
36+
with open(os.path.join(basedir, fn)) as f:
37+
return f.read()
38+
39+
40+
@click.command()
41+
@click.argument("project")
42+
@click.option(
43+
"-t",
44+
"--templatedir",
45+
default="source",
46+
help="Template directory",
47+
show_default=True,
48+
)
49+
@click.option(
50+
"-o", "--outdir", default="build", help="Output directory", show_default=True
51+
)
52+
def publish(project, templatedir, outdir):
53+
"""Compile templates (substitute variables) into markdown files ready for sphinx / myst
54+
55+
Include sections like the following are executed:
56+
57+
```
58+
{include} filename.md
59+
```
60+
61+
Thereafter, the following variables are substituted:
62+
63+
- `{{ project }}`: Name of the project
64+
65+
"""
66+
os.makedirs(outdir, exist_ok=True)
67+
os.makedirs(os.path.join(outdir, project), exist_ok=True)
68+
69+
variables = {"project": project}
70+
71+
print(f"Templating [{project}] report from [{templatedir}] to [{outdir}]...")
72+
73+
templatedir = f"{templatedir}/{project}"
74+
template_files = [f"{templatedir}/index.md"]
75+
76+
for f in template_files:
77+
with open(f) as fh:
78+
template = fh.read()
79+
dest_dir = f"{outdir}/{project}"
80+
dest = f"{dest_dir}/{os.path.basename(f)}"
81+
with open(dest, "w") as fh:
82+
print(f" - {dest}")
83+
# Handle myst includes
84+
template = re.sub(
85+
r"```{include}\s*(.*)\s*```",
86+
functools.partial(_include_file, templatedir),
87+
template,
88+
flags=re.MULTILINE,
89+
)
90+
91+
for v in variables:
92+
template = template.replace("{{ " + v + " }}", variables[v])
93+
94+
fh.write(template)

devstats/reports/index.md

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
---
2+
file_format: mystnb
3+
kernelspec:
4+
display_name: Python 3
5+
name: python3
6+
---
7+
8+
# `{{ project }}`
9+
10+
```{include} preamble.md
11+
12+
```
13+
14+
A snapshot of the development on the {{ project }} project.
15+
16+
## Issues
17+
18+
% TODO: query_date should be synced up with the query that generates data,
19+
% rather than specified manually
20+
21+
% TODO improve handling of datetimes (super annoying)
22+
23+
```{code-cell}
24+
query_date = np.datetime64("2020-01-01 00:00:00")
25+
26+
# Load data
27+
with open("devstats-data/{{ project }}_issues.json", "r") as fh:
28+
issues = [item["node"] for item in json.loads(fh.read())]
29+
30+
glue("devstats-data/{{ project }}_query_date", str(query_date.astype("M8[D]")))
31+
```
32+
33+
```{include} new_issues.md
34+
35+
```
36+
37+
```{include} issue_time_to_response.md
38+
39+
```
40+
41+
```{include} issue_first_responders.md
42+
43+
```
44+
45+
## Pull Requests
46+
47+
```{code-cell}
48+
---
49+
tags: [hide-input]
50+
---
51+
with open("devstats-data/{{ project }}_prs.json", "r") as fh:
52+
prs = [item["node"] for item in json.loads(fh.read())]
53+
54+
# Filters
55+
56+
# The following filters are applied to the PRs for the following analysis:
57+
#
58+
# - Only PRs to the default development branch (e.g `main`)[^master_to_main]
59+
# are considered.
60+
# - Only PRs from users with _active_ GitHub accounts are considered. For example,
61+
# if a user opened a Pull Request in 2016, but then deleted their GitHub account
62+
# in 2017, then this PR is excluded from the analysis.
63+
# - PRs opened by dependabot are excluded.
64+
65+
# Only look at PRs to the main development branch - ignore backports,
66+
# gh-pages, etc.
67+
default_branches = {"main", "master"} # Account for default branch update
68+
prs = [pr for pr in prs if pr["baseRefName"] in default_branches]
69+
70+
# Drop data where PR author is unknown (e.g. github account no longer exists)
71+
prs = [pr for pr in prs if pr["author"]] # Failed author query results in None
72+
73+
# Filter out PRs by bots
74+
bot_filter = {
75+
"dependabot-preview",
76+
"github-actions",
77+
"meeseeksmachine",
78+
"pre-commit-ci[bot]"
79+
}
80+
prs = [pr for pr in prs if pr["author"]["login"] not in bot_filter]
81+
```
82+
83+
```{include} prs_merged_over_time.md
84+
85+
```
86+
87+
```{include} prs_lifetime.md
88+
89+
```
90+
91+
```{include} prs_mergeability.md
92+
93+
```
94+
95+
```{include} prs_participants.md
96+
97+
```
98+
99+
```{include} prs_contributor_origin.md
100+
101+
```
102+
103+
```{include} prs_pony_factor.md
104+
105+
```
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#### First responders
2+
3+
```{code-cell}
4+
---
5+
tags: [hide-input]
6+
---
7+
```
8+
9+
```{code-cell}
10+
---
11+
tags: [hide-input]
12+
---
13+
first_commenter_tab = pd.DataFrame(
14+
{
15+
k: v
16+
for k, v in zip(
17+
("Contributor", "# of times commented first"),
18+
np.unique(first_commenters, return_counts=True),
19+
)
20+
}
21+
)
22+
first_commenter_tab.sort_values(
23+
"# of times commented first", ascending=False
24+
).head(10)
25+
```

0 commit comments

Comments
 (0)