#!/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"" 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 = "\n" start = text.find(start_str) end = text.find("\n") generated_note = f'' 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()