Files
cibuildwheel/bin/projects.py
T

199 lines
5.5 KiB
Python
Executable File

#!/usr/bin/env python3
"""
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
git diff
"""
import builtins
import functools
from datetime import datetime
from io import StringIO
from typing import Dict, Any, List, Optional, TextIO
import click
import yaml
from github import Github
import urllib.request
import xml.dom.minidom
from pathlib import Path
ICONS = (
"appveyor",
"github",
"azurepipelines",
"circleci",
"gitlab",
"travisci",
"windows",
"apple",
"linux",
)
class Project:
NAME: int = 0
def __init__(self, config: Dict[str, Any], github: Optional[Github] = 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
self.stars_repo: str = config.get("stars", self.gh)
self.notes: str = config.get("notes", "")
self.ci: List[str] = config.get("ci", [])
self.os: List[str] = config.get("os", [])
self.online = github is not None
if github is not None:
repo = github.get_repo(self.stars_repo)
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
else:
self.num_stars = 0
self.pushed_at = datetime.utcnow()
name_len = len(self.name) + 4
self.__class__.NAME = max(self.__class__.NAME, name_len)
def __lt__(self, other: "Project") -> bool:
if self.online:
return self.num_stars < other.num_stars
else:
return self.name < other.name
@classmethod
def header(cls) -> str:
return (
f"| {'Name':{cls.NAME}} | CI | OS | Notes |\n"
f"|{'':-^{cls.NAME+2 }}|----|----|:------|"
)
@property
def namelink(self) -> str:
return f"[{self.name}][]"
@property
def starslink(self) -> str:
return f"![{self.name} stars][]"
@property
def url(self) -> str:
return f"https://github.com/{self.gh}"
@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)
def table_row(self) -> str:
notes = self.notes.replace('\n', ' ')
return f"| {self.namelink: <{self.NAME}} | {self.ci_icons} | {self.os_icons} | {notes} |"
def links(self) -> str:
return f"[{self.name}]: {self.url}"
def info(self) -> str:
days = (datetime.utcnow() - self.pushed_at).days
return f"<!-- {self.name}: {self.num_stars}, last pushed {days} days ago -->"
def fetch_icon(icon_name: str) -> None:
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
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)
with open(path_for_icon(icon_name), 'w') as f:
f.write(svgElement.toxml())
def path_for_icon(icon_name: str) -> Path:
return Path('.') / 'docs' / 'data' / 'readme_icons' / f'{icon_name}.svg'
def str_projects(
config: List[Dict[str, Any]], *, online: bool = True, auth: Optional[str] = None
) -> str:
io = StringIO()
print = functools.partial(builtins.print, file=io)
if online:
for icon in ICONS:
fetch_icon(icon)
github = Github(auth) if online else None
projects = sorted((Project(item, github) for item in config), reverse=online)
print(Project.header())
for project in projects:
print(project.table_row())
print()
for project in projects:
print(project.links())
print()
for icon in ICONS:
print(f"[{icon} icon]: {path_for_icon(icon).as_posix()}")
print()
for project in projects:
print(project.info())
return io.getvalue()
@click.command()
@click.argument("input", type=click.File("r"))
@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(
input: TextIO, online: bool, auth: Optional[str], readme: Optional[TextIO]
) -> None:
config = yaml.safe_load(input)
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"
start = text.find(start_str)
end = text.find("<!-- END bin/projects.py -->\n")
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:]}"
readme.seek(0)
readme.write(new_text)
readme.truncate()
if __name__ == "__main__":
projects()