Files
cibuildwheel/bin/projects.py
T

252 lines
6.8 KiB
Python
Raw Normal View History

2020-12-04 12:08:23 -05:00
#!/usr/bin/env python3
2020-12-04 16:21:09 -05:00
"""
Convert a yaml project list into a nice table.
Suggested usage:
./bin/projects.py docs/data/projects.yml --online --auth $GITHUB_API_TOKEN
2020-12-04 16:21:09 -05:00
git diff
"""
2021-04-25 03:45:27 -04:00
from __future__ import annotations
2020-12-04 16:21:09 -05:00
import builtins
import functools
2021-04-25 03:45:27 -04:00
import textwrap
2021-01-06 13:50:58 -05:00
import urllib.request
import xml.dom.minidom
2020-12-04 12:58:27 -05:00
from datetime import datetime
2020-12-04 16:21:09 -05:00
from io import StringIO
2021-01-06 13:50:58 -05:00
from pathlib import Path
2021-04-25 03:45:27 -04:00
from typing import Any, TextIO
2020-12-04 12:08:23 -05:00
import click
import yaml
from github import Github, GithubException
2020-12-04 12:08:23 -05:00
2020-12-04 16:21:09 -05:00
ICONS = (
"appveyor",
"github",
2020-12-04 16:44:04 -05:00
"azurepipelines",
2020-12-04 16:21:09 -05:00
"circleci",
"gitlab",
"travisci",
"cirrusci",
2020-12-04 16:21:09 -05:00
"windows",
"apple",
"linux",
)
2020-12-04 12:08:23 -05:00
class Project:
NAME: int = 0
2021-04-25 03:45:27 -04:00
def __init__(self, config: dict[str, Any], github: Github | None = None):
try:
self.name: str = config["name"]
self.gh: str = config["gh"]
except KeyError:
print("Invalid config, needs at least gh and name!", config)
raise
2020-12-04 12:08:23 -05:00
self.stars_repo: str = config.get("stars", self.gh)
self.notes: str = config.get("notes", "")
2021-04-25 03:45:27 -04:00
self.ci: list[str] = config.get("ci", [])
self.os: list[str] = config.get("os", [])
2020-12-04 12:08:23 -05:00
2020-12-04 16:21:09 -05:00
self.online = github is not None
if github is not None:
try:
repo = github.get_repo(self.stars_repo)
except GithubException:
print(f"Broken: {self.stars_repo}")
raise
2021-06-12 14:02:06 -04:00
self.num_stars: int = repo.stargazers_count
2020-12-04 16:21:09 -05:00
self.pushed_at = repo.pushed_at
if not self.notes:
notes = repo.description
if repo.description:
self.notes = notes
2020-12-04 12:58:27 -05:00
else:
self.num_stars = 0
self.pushed_at = datetime.utcnow()
2020-12-04 12:08:23 -05:00
name_len = len(self.name) + 4
self.__class__.NAME = max(self.__class__.NAME, name_len)
2021-04-25 03:45:27 -04:00
def __lt__(self, other: Project) -> bool:
2020-12-04 16:21:09 -05:00
if self.online:
2020-12-04 12:58:27 -05:00
return self.num_stars < other.num_stars
else:
return self.name < other.name
2020-12-04 12:08:23 -05:00
@classmethod
2020-12-04 16:21:09 -05:00
def header(cls) -> str:
2021-04-30 17:56:34 -04:00
return textwrap.dedent(
f"""\
2021-04-25 03:45:27 -04:00
| {'Name':{cls.NAME}} | CI | OS | Notes |
2021-04-30 17:56:34 -04:00
|{'':-^{cls.NAME+2 }}|----|----|:------|"""
)
2020-12-04 12:08:23 -05:00
@property
def namelink(self) -> str:
return f"[{self.name}][]"
@property
def starslink(self) -> str:
2020-12-04 12:58:27 -05:00
return f"![{self.name} stars][]"
2020-12-04 12:08:23 -05:00
@property
def url(self) -> str:
return f"https://github.com/{self.gh}"
2020-12-04 12:58:27 -05:00
@property
def ci_icons(self) -> str:
return " ".join(f"![{icon} icon][]" for icon in self.ci)
@property
def os_icons(self) -> str:
return " ".join(f"![{icon} icon][]" for icon in self.os)
2020-12-04 12:08:23 -05:00
def table_row(self) -> str:
2021-05-03 11:45:43 -04:00
notes = self.notes.replace("\n", " ")
2020-12-12 12:18:11 +00:00
return f"| {self.namelink: <{self.NAME}} | {self.ci_icons} | {self.os_icons} | {notes} |"
2020-12-04 12:08:23 -05:00
def links(self) -> str:
2020-12-07 09:31:08 -05:00
return f"[{self.name}]: {self.url}"
2020-12-04 12:08:23 -05:00
def info(self) -> str:
2020-12-04 12:58:27 -05:00
days = (datetime.utcnow() - self.pushed_at).days
return f"<!-- {self.name}: {self.num_stars}, last pushed {days} days ago -->"
2020-12-04 12:08:23 -05:00
def fetch_icon(icon_name: str) -> None:
2021-05-03 11:45:43 -04:00
url = f"https://cdn.jsdelivr.net/npm/simple-icons@v4/icons/{icon_name}.svg"
with urllib.request.urlopen(url) as f:
original_svg_data = f.read()
document = xml.dom.minidom.parseString(original_svg_data)
svgElement = document.documentElement
2021-05-03 11:45:43 -04:00
assert svgElement.nodeName == "svg"
svgElement.setAttribute("width", "16px")
svgElement.setAttribute("fill", "#606060")
icon_path = path_for_icon(icon_name)
icon_path.parent.mkdir(parents=True, exist_ok=True)
2021-05-03 11:45:43 -04:00
with open(path_for_icon(icon_name), "w") as f:
f.write(svgElement.toxml())
def path_for_icon(icon_name: str, relative_to: Path | None = None) -> Path:
2021-08-23 18:48:06 +01:00
from_path = Path.cwd() if relative_to is None else relative_to.parent
absolute_path = PROJECT_ROOT / "docs" / "data" / "readme_icons" / f"{icon_name}.svg"
return absolute_path.resolve().relative_to(from_path.resolve())
def get_projects(
2021-04-30 17:56:34 -04:00
config: list[dict[str, Any]],
*,
online: bool = True,
auth: str | None = None,
) -> list[Project]:
if online:
for icon in ICONS:
fetch_icon(icon)
2020-12-04 16:21:09 -05:00
github = Github(auth) if online else None
return sorted((Project(item, github) for item in config), reverse=online)
2021-08-23 18:48:06 +01:00
def render_projects(projects: list[Project], *, dest_path: Path, include_info: bool = True):
io = StringIO()
print = functools.partial(builtins.print, file=io)
2020-12-04 12:08:23 -05:00
print(Project.header())
2020-12-04 12:08:23 -05:00
for project in projects:
print(project.table_row())
print()
for project in projects:
print(project.links())
2020-12-04 12:58:27 -05:00
print()
2020-12-04 16:21:09 -05:00
for icon in ICONS:
print(f"[{icon} icon]: {path_for_icon(icon, relative_to=dest_path).as_posix()}")
2020-12-04 12:58:27 -05:00
if include_info:
print()
for project in projects:
print(project.info())
2020-12-04 16:21:09 -05:00
return io.getvalue()
2020-12-04 12:08:23 -05:00
def insert_projects_table(
file: Path,
*,
projects: list[Project],
input_filename: str,
include_info: bool = True,
):
text = file.read_text()
projects_table = render_projects(projects, include_info=include_info, dest_path=file)
start_str = "<!-- START bin/projects.py -->\n"
start = text.find(start_str)
assert start != -1
end = text.find("<!-- END bin/projects.py -->\n")
assert end != -1
generated_note = f"<!-- this section is generated by bin/projects.py. Don't edit it directly, instead, edit {input_filename} -->"
new_text = (
f"{text[:start + len(start_str)]}\n{generated_note}\n\n{projects_table}\n{text[end:]}"
)
file.write_text(new_text)
PROJECT_ROOT = Path(__file__).parent / ".."
README_FILE = PROJECT_ROOT / "README.md"
DOCS_PAGE = PROJECT_ROOT / "docs" / "working-examples.md"
@click.command(help="Try ./bin/projects.py docs/data/projects.yml")
2020-12-04 12:08:23 -05:00
@click.argument("input", type=click.File("r"))
2020-12-04 16:21:09 -05:00
@click.option("--online/--no-online", default=True, help="Get info from GitHub")
@click.option("--auth", help="GitHub authentication token")
@click.option(
"--dry-run", default=False, help="Print the output, rather than writing it to files in the repo"
)
2020-12-04 16:21:09 -05:00
def projects(
2021-04-30 17:56:34 -04:00
input: TextIO,
online: bool,
auth: str | None,
dry_run: bool,
2020-12-04 16:21:09 -05:00
) -> None:
2020-12-04 12:08:23 -05:00
config = yaml.safe_load(input)
projects = get_projects(config, online=online, auth=auth)
2020-12-04 16:21:09 -05:00
if dry_run:
output = render_projects(projects, dest_path=README_FILE)
2020-12-04 16:21:09 -05:00
print(output)
else:
insert_projects_table(
README_FILE, projects=projects[:10], input_filename=input.name, include_info=False
)
insert_projects_table(
DOCS_PAGE, projects=projects, input_filename=input.name, include_info=False
)
2020-12-04 12:08:23 -05:00
if __name__ == "__main__":
projects()