Files
cibuildwheel/bin/projects.py
T

213 lines
5.7 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 --readme README.md
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",
"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
2020-12-04 16:21:09 -05:00
self.num_stars = repo.stargazers_count
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) -> Path:
2021-05-03 11:45:43 -04:00
return Path(".") / "docs" / "data" / "readme_icons" / f"{icon_name}.svg"
2020-12-04 16:21:09 -05:00
def str_projects(
2021-04-30 17:56:34 -04:00
config: list[dict[str, Any]],
*,
online: bool = True,
auth: str | None = None,
2020-12-04 16:21:09 -05:00
) -> str:
io = StringIO()
print = functools.partial(builtins.print, file=io)
if online:
for icon in ICONS:
fetch_icon(icon)
2020-12-04 16:21:09 -05:00
github = Github(auth) if online else None
projects = sorted((Project(item, github) for item in config), reverse=online)
2020-12-04 12:08:23 -05:00
print(Project.header())
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).as_posix()}")
2020-12-04 12:58:27 -05:00
2020-12-04 16:21:09 -05:00
print()
for project in projects:
print(project.info())
return io.getvalue()
2020-12-04 12:08:23 -05:00
@click.command(help="Try ./bin/projects.py docs/data/projects.yml --readme README.md")
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("--readme", type=click.File("r+"), help="Modify a readme file if given")
def projects(
2021-04-30 17:56:34 -04:00
input: TextIO,
online: bool,
auth: str | None,
readme: TextIO | None,
2020-12-04 16:21:09 -05:00
) -> None:
2020-12-04 12:08:23 -05:00
config = yaml.safe_load(input)
2020-12-04 16:21:09 -05:00
output = str_projects(config, online=online, auth=auth)
if readme is None:
print(output)
else:
text = readme.read()
start_str = "<!-- START bin/projects.py -->\n"
2020-12-04 16:21:09 -05:00
start = text.find(start_str)
end = text.find("<!-- END bin/projects.py -->\n")
2021-05-03 11:45:43 -04:00
generated_note = f"<!-- this section is generated by bin/projects.py. Don't edit it directly, instead, edit {input.name} -->"
new_text = f"{text[:start + len(start_str)]}\n{generated_note}\n\n{output}\n{text[end:]}"
2020-12-04 16:21:09 -05:00
readme.seek(0)
readme.write(new_text)
readme.truncate()
2020-12-04 12:08:23 -05:00
if __name__ == "__main__":
projects()