From 44f2775c69cf8b22dc82d592739349f048e543e2 Mon Sep 17 00:00:00 2001 From: Eric Nieuwland Date: Sat, 24 Jul 2021 22:04:17 +0200 Subject: [PATCH 1/4] REFACTOR generation of 'repos' output --- audit.py | 320 ++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 232 insertions(+), 88 deletions(-) diff --git a/audit.py b/audit.py index 231f2f7..1735a09 100644 --- a/audit.py +++ b/audit.py @@ -47,8 +47,112 @@ def unknown(what: str) -> None: output_option = typer.Option(None, "--output", "-o") -g = Github(config["github.com"]["token"]) -app = typer.Typer() +class DocumentBase: + + def __init__(self, started, output): + self.started = started + self.output = output + + def begin_document(self, title: str): + raise NotImplementedError(f"{self.__class__.__name__}.begin_document()") + + def empty_line(self): + raise NotImplementedError(f"{self.__class__.__name__}.empty_line()") + + def begin_table(self): + raise NotImplementedError(f"{self.__class__.__name__}.begin_table()") + + def table_row(self, *args, **kwargs): + raise NotImplementedError(f"{self.__class__.__name__}.table_row()") + + def end_table(self): + raise NotImplementedError(f"{self.__class__.__name__}.end_table()") + + def end_document(self): + raise NotImplementedError(f"{self.__class__.__name__}.end_document()") + + +class TextDocument(DocumentBase): + + table = None + + def __init__(self, started, output): + super().__init__(started, output) + self.console = Console(file=output) + + def begin_document(self, title: str): + self.console.print(title) + + def empty_line(self): + self.console.print() + + def begin_table(self): + if self.table is not None: + raise RuntimeError("already building a table") + self.table = Table("Name", box=box.SQUARE) + self._table_header() + + def _table_header(self): + raise NotImplementedError(f"{self.__class__.__name__}._table_header()") + + def table_row(self, *args, **kwargs): + self._table_row(*args, **kwargs) + + def _table_row(self, *args, **kwargs): + raise NotImplementedError(f"{self.__class__.__name__}.table_row()") + + def end_table(self): + if self.table is None: + raise RuntimeError("not building a table") + self.console.print() + self.console.print(self.table) + self.console.print() + + def end_document(self): + self.console.print(format_generated_timestamp(self.started)) + + +class HtmlDocument(DocumentBase): + + def __init__(self, started, output): + super().__init__(started, output) + doc, tag, text = Doc().tagtext() + self.doc = doc + self.tag = tag + self.text = text + + def begin_document(self, title: str): + self.doc.asis("\n") + create_html_head(self.doc, title) + self.doc.asis("\n") + with self.tag("h1"): + self.text(title) + + def empty_line(self): + self.doc.stag("br") + + def begin_table(self): + self.doc.asis("\n") + self._table_header() + + def _table_header(self): + raise NotImplementedError(f"{self.__class__.__name__}._table_header()") + + def table_row(self, *args, **kwargs): + with self.tag("tr"): + self._table_row(*args, **kwargs) + + def _table_row(self, *args, **kwargs): + raise NotImplementedError(f"{self.__class__.__name__}.__table_row()") + + def end_table(self): + self.doc.asis("
\n") + + def end_document(self): + create_html_footer(self.doc, self.started) + self.doc.asis("\n") + self.doc.asis("\n") + print(indent(self.doc.getvalue()), file=self.output) # Github API wrappers @@ -194,6 +298,107 @@ def output_text_elements(title: str, table: Table, started: datetime, output: Op console.print(format_generated_timestamp(started)) +### +# APPLICATION COMMANDS + +g = Github(config["github.com"]["token"]) +app = typer.Typer() + + +class RepoTextDocument(TextDocument): + + empty_columns = None + + def __init__(self, started, output, include_archived_repositories, include_forked_repositories): + super().__init__(started, output) + self.include_archived_repositories = include_archived_repositories + self.include_forked_repositories = include_forked_repositories + + def _table_header(self): + if self.table is None: + raise RuntimeError("not building a table") + if self.include_archived_repositories: + self.table.add_column("Archived", justify="center") + if self.include_forked_repositories: + self.table.add_column("Fork", justify="center") + self.table.add_column("Pushed at") + self.table.add_column("Pull request title") + self.table.add_column("Created at") + + def _table_row( + self, + repo_name=None, archived=None, fork=None, pushed_at=None, title=None, created_at=None, + rowspan=0, first=False + ): + if self.table is None: + raise RuntimeError("not building a table") + repo_row = [] + repo_row.append(repo_name) + if self.include_archived_repositories: + repo_row.append(format_bool(archived) if archived is not None else None) + if self.include_forked_repositories: + repo_row.append(format_bool(fork) if fork is not None else None) + repo_row.append(f"{format_friendly_timestamp(pushed_at, self.started)}" if pushed_at is not None else None) + if title and created_at: + repo_row.extend([title, f"{format_friendly_timestamp(created_at, self.started)}"]) + self.table.add_row(*repo_row) + + +class RepoHtmlDocument(HtmlDocument): + + def __init__(self, started, output, include_archived_repositories, include_forked_repositories): + super().__init__(started, output) + self.include_archived_repositories = include_archived_repositories + self.include_forked_repositories = include_forked_repositories + + def _table_header(self): + with self.tag("tr"): + with self.tag("th", rowspan=2): + self.text("Name") + if self.include_archived_repositories: + with self.tag("th", rowspan=2): + self.text("Archived") + if self.include_forked_repositories: + with self.tag("th", rowspan=2): + self.text("Fork") + with self.tag("th", rowspan=2): + self.text("Pushed at") + with self.tag("th", colspan=2, klass="column-group-header"): + self.text("Pull request") + with self.tag("tr"): + with self.tag("th"): + self.text("Title") + with self.tag("th"): + self.text("Created at") + + def _table_row( + self, + repo_name=None, archived=None, fork=None, pushed_at=None, title=None, created_at=None, + rowspan=0, first=False + ): + klass_first_row = {"klass": "first-row"} if first else {} + klass_first_row_centered = {"klass": "first-row centered"} if first else {} + if repo_name is not None: + with self.tag("td", **klass_first_row, rowspan=rowspan): + self.text(repo_name) + if archived is not None and self.include_archived_repositories: + with self.tag("td", **klass_first_row_centered, rowspan=rowspan): + self.doc.asis(format_bool(archived)) + if fork is not None and self.include_forked_repositories: + with self.tag("td", **klass_first_row_centered, rowspan=rowspan): + self.doc.asis(format_bool(fork)) + if pushed_at is not None: + with self.tag("td", **klass_first_row, rowspan=rowspan): + self.text(format_friendly_timestamp(pushed_at, self.started)) + if title and created_at: + with self.tag("td", **klass_first_row): + self.text(title) + with self.tag("td", **klass_first_row): + self.text(format_friendly_timestamp(created_at, self.started)) + else: + self.doc.stag("td", **klass_first_row, colspan=2) + + @app.command() def repos( organization_name: str = organization_name_argument, @@ -226,95 +431,34 @@ def repos( if output_format == OutputFormat.json: output_json(repositories, output) + return - elif output_format == OutputFormat.html: - doc, tag, text = Doc().tagtext() - with tag("html"): - with tag("head"): - create_html_head(doc, title) - with tag("body"): - with tag("h1"): - text(title) - with tag("table"): - with tag("tr"): - with tag("th", rowspan=2): - text("Name") - if include_archived_repositories: - with tag("th", rowspan=2): - text("Archived") - if include_forked_repositories: - with tag("th", rowspan=2): - text("Fork") - with tag("th", rowspan=2): - text("Pushed at") - with tag("th", colspan=2, klass="column-group-header"): - text("Pull request") - with tag("tr"): - with tag("th"): - text("Title") - with tag("th"): - text("Created at") - for repo in sorted(repositories, key=lambda x: x['name'].lower()): - open_pull_requests = repo["open_pull_requests"] - with tag("tr"): - with tag("td", klass="first-row", rowspan=len(open_pull_requests)): - text(repo["name"]) - if include_archived_repositories: - with tag("td", klass="first-row centered", rowspan=len(open_pull_requests)): - doc.asis(format_bool(repo["archived"])) - if include_forked_repositories: - with tag("td", klass="first-row centered", rowspan=len(open_pull_requests)): - doc.asis(format_bool(repo["fork"])) - with tag("td", klass="first-row", rowspan=len(open_pull_requests)): - text(format_friendly_timestamp(repo["pushed_at"], started)) - if open_pull_requests: - first_pr = open_pull_requests[0] - with tag("td", klass="first-row"): - text(first_pr["title"]) - with tag("td", klass="first-row"): - text(format_friendly_timestamp(first_pr["created_at"], started)) - else: - doc.stag("td", klass="first-row", colspan=2) - for pr in open_pull_requests[1:]: - with tag("tr"): - with tag("td"): - text(pr["title"]) - with tag("td"): - text(format_friendly_timestamp(pr["created_at"], started)) - create_html_footer(doc, started) - print(indent(doc.getvalue()), file=output) - - elif output_format == OutputFormat.text: - table = Table("Name", box=box.SQUARE) - empty_columns = [None, None] # Name, Pushed at - if include_archived_repositories: - table.add_column("Archived", justify="center") - empty_columns.append(None) - if include_forked_repositories: - table.add_column("Fork", justify="center") - empty_columns.append(None) - table.add_column("Pushed at") - table.add_column("Pull request title") - table.add_column("Created at") - for repo in sorted(repositories, key=lambda x: x['name'].lower()): - repo_row = [repo["name"]] - if include_archived_repositories: - repo_row.append(format_bool(repo["archived"])) - if include_forked_repositories: - repo_row.append(format_bool(repo["fork"])) - repo_row.append(f'{format_friendly_timestamp(repo["pushed_at"], started)}') - if repo["open_pull_requests"]: - first_pr = repo["open_pull_requests"][0] - repo_row.extend([first_pr["title"], f'{format_friendly_timestamp(first_pr["created_at"], started)}']) - table.add_row(*repo_row) - for pr in repo["open_pull_requests"][1:]: - pr_row = empty_columns + [pr["title"], f'{format_friendly_timestamp(pr["created_at"], started)}'] - table.add_row(*pr_row) - output_text_elements(title, table, started, output) - - else: + klass = { + OutputFormat.html: RepoHtmlDocument, + OutputFormat.text: RepoTextDocument, + }.get(output_format) + if klass is None: OutputFormat.unknown(output_format) + document = klass(started, output, include_archived_repositories, include_forked_repositories) + document.begin_document(title) + document.begin_table() + for repo in sorted(repositories, key=lambda x: x['name'].lower()): + open_pull_requests = repo["open_pull_requests"] + columns = [ + repo["name"], + repo["archived"], + repo["fork"], + repo["pushed_at"], + open_pull_requests[0]["title"] if open_pull_requests else "", + open_pull_requests[0]["created_at"] if open_pull_requests else "", + ] + document.table_row(*columns, rowspan=len(open_pull_requests), first=True) + for pr in open_pull_requests[1:]: + document.table_row(title=pr["title"], created_at=pr["created_at"]) + document.end_table() + document.end_document() + @app.command() def repo_contributions( From f38bcce1db9301043e174fdedffd63d061cef7d6 Mon Sep 17 00:00:00 2001 From: Eric Nieuwland Date: Sun, 25 Jul 2021 17:19:20 +0200 Subject: [PATCH 2/4] REFACTOR generation of output --- audit.py | 502 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 277 insertions(+), 225 deletions(-) diff --git a/audit.py b/audit.py index 1735a09..54297d1 100644 --- a/audit.py +++ b/audit.py @@ -47,14 +47,17 @@ def unknown(what: str) -> None: output_option = typer.Option(None, "--output", "-o") -class DocumentBase: +class ReportBase: def __init__(self, started, output): self.started = started self.output = output - def begin_document(self, title: str): - raise NotImplementedError(f"{self.__class__.__name__}.begin_document()") + def begin_report(self, title: str): + raise NotImplementedError(f"{self.__class__.__name__}.begin_report()") + + def end_report(self): + raise NotImplementedError(f"{self.__class__.__name__}.end_report()") def empty_line(self): raise NotImplementedError(f"{self.__class__.__name__}.empty_line()") @@ -68,11 +71,8 @@ def table_row(self, *args, **kwargs): def end_table(self): raise NotImplementedError(f"{self.__class__.__name__}.end_table()") - def end_document(self): - raise NotImplementedError(f"{self.__class__.__name__}.end_document()") - -class TextDocument(DocumentBase): +class TextReportBase(ReportBase): table = None @@ -80,16 +80,19 @@ def __init__(self, started, output): super().__init__(started, output) self.console = Console(file=output) - def begin_document(self, title: str): + def begin_report(self, title: str): self.console.print(title) + def end_report(self): + self.console.print(format_generated_timestamp(self.started)) + def empty_line(self): self.console.print() def begin_table(self): if self.table is not None: raise RuntimeError("already building a table") - self.table = Table("Name", box=box.SQUARE) + self.table = Table(box=box.SQUARE) self._table_header() def _table_header(self): @@ -108,11 +111,8 @@ def end_table(self): self.console.print(self.table) self.console.print() - def end_document(self): - self.console.print(format_generated_timestamp(self.started)) - -class HtmlDocument(DocumentBase): +class HtmlReportBase(ReportBase): def __init__(self, started, output): super().__init__(started, output) @@ -121,18 +121,72 @@ def __init__(self, started, output): self.tag = tag self.text = text - def begin_document(self, title: str): - self.doc.asis("\n") - create_html_head(self.doc, title) - self.doc.asis("\n") + def begin_report(self, title: str): + self.doc.asis("") + with self.tag("head"): + self.doc.asis('') + with self.tag("title"): + self.text(title) + with self.tag("style"): + self.text("\nbody {font-size: 100%; }") + self.text("\ndiv.footer {font-size: 50%; padding-top: 24px; }") + self.text("\ntable {border-spacing: 0; border-collapse: collapse; }") + self.text("\nth {vertical-align: bottom; }") + self.text("\ntd {vertical-align: top; }") + self.text("\n.centered {text-align: center; }") + self.text("\n.column-group-header {border-bottom: 1px solid black; }") + self.text("\n.first-row {border-top: 1px solid black; }") + self.text("\n.right {text-align: right; }") + self.doc.asis("") with self.tag("h1"): self.text(title) + self._prologue() + self._begin_main() + + def end_report(self): + self._end_main() + self._epilogue() + with self.doc.tag("div", klass="footer"): + self.doc.text(format_generated_timestamp(self.started)) + self.doc.asis("") + self.doc.asis("") + print(indent(self.doc.getvalue()), file=self.output) + + def _prologue(self): + pass + + def _epilogue(self): + pass + + def _command_options(self, options): + with self.tag("h2"): + self.text("Options") + with self.tag("table"): + for name, value in options.items(): + with self.tag("tr"): + with self.tag("td"): + self.text(name) + with self.tag("td"): + self.text(value) + + # main part of the report + + def _begin_main(self): + with self.tag("h2"): + self.text("Report") + + def _end_main(self): + pass + + # report content def empty_line(self): self.doc.stag("br") + # table creation + def begin_table(self): - self.doc.asis("\n") + self.doc.asis("
") self._table_header() def _table_header(self): @@ -146,13 +200,7 @@ def _table_row(self, *args, **kwargs): raise NotImplementedError(f"{self.__class__.__name__}.__table_row()") def end_table(self): - self.doc.asis("
\n") - - def end_document(self): - create_html_footer(self.doc, self.started) - self.doc.asis("\n") - self.doc.asis("\n") - print(indent(self.doc.getvalue()), file=self.output) + self.doc.asis("") # Github API wrappers @@ -218,31 +266,6 @@ def format_member(member: dict) -> str: # HTML utilities -def create_html_head(doc: SimpleDoc, title: str) -> None: - """ - Standard HTML head segment - - sets text encoding for compatibility with Python - - sets document title - - defines styles for content - :param doc: yattag document - :param title: document title - """ - with doc.tag("head"): - doc.asis('') - with doc.tag("title"): - doc.text(title) - with doc.tag("style"): - doc.text("\nbody {font-size: 100%; }") - doc.text("\ndiv.footer {font-size: 50%; padding-top: 24px; }") - doc.text("\ntable {border-spacing: 0; border-collapse: collapse; }") - doc.text("\nth {vertical-align: bottom; }") - doc.text("\ntd {vertical-align: top; }") - doc.text("\n.centered {text-align: center; }") - doc.text("\n.column-group-header {border-bottom: 1px solid black; }") - doc.text("\n.first-row {border-top: 1px solid black; }") - doc.text("\n.right {text-align: right; }") - - def create_html_email_href(email: str) -> str: """ HTML version of an email address @@ -261,16 +284,6 @@ def create_html_url_href(url: str) -> str: return f'{url}' if url else "" -def create_html_footer(doc: SimpleDoc, started: datetime) -> None: - """ - Standard HTML footer for the body segment - :param doc: yattag document - :param started: when report creation started - """ - with doc.tag("div", klass="footer"): - doc.text(format_generated_timestamp(started)) - - # output creation def output_json(json_data: list, output: Optional[typer.FileTextWrite]) -> None: @@ -282,22 +295,6 @@ def output_json(json_data: list, output: Optional[typer.FileTextWrite]) -> None: typer.echo(json.dumps(json_data, indent=" "), file=output) -def output_text_elements(title: str, table: Table, started: datetime, output: Optional[typer.FileTextWrite]) -> None: - """ - Output the text elements - :param title: title above the table - :param table: data to report - :param started: when creating this report started - :param output: output file to write to (default: stdout) - """ - console = Console(file=output) - console.print(title) - console.print() - console.print(table) - console.print() - console.print(format_generated_timestamp(started)) - - ### # APPLICATION COMMANDS @@ -305,9 +302,7 @@ def output_text_elements(title: str, table: Table, started: datetime, output: Op app = typer.Typer() -class RepoTextDocument(TextDocument): - - empty_columns = None +class RepoTextReport(TextReportBase): def __init__(self, started, output, include_archived_repositories, include_forked_repositories): super().__init__(started, output) @@ -317,6 +312,7 @@ def __init__(self, started, output, include_archived_repositories, include_forke def _table_header(self): if self.table is None: raise RuntimeError("not building a table") + self.table.add_column("Name") if self.include_archived_repositories: self.table.add_column("Archived", justify="center") if self.include_forked_repositories: @@ -332,25 +328,32 @@ def _table_row( ): if self.table is None: raise RuntimeError("not building a table") - repo_row = [] - repo_row.append(repo_name) + row = [] + row.append(repo_name) if self.include_archived_repositories: - repo_row.append(format_bool(archived) if archived is not None else None) + row.append(format_bool(archived) if archived is not None else None) if self.include_forked_repositories: - repo_row.append(format_bool(fork) if fork is not None else None) - repo_row.append(f"{format_friendly_timestamp(pushed_at, self.started)}" if pushed_at is not None else None) + row.append(format_bool(fork) if fork is not None else None) + row.append(f"{format_friendly_timestamp(pushed_at, self.started)}" if pushed_at is not None else None) if title and created_at: - repo_row.extend([title, f"{format_friendly_timestamp(created_at, self.started)}"]) - self.table.add_row(*repo_row) + row.extend([title, f"{format_friendly_timestamp(created_at, self.started)}"]) + self.table.add_row(*row) -class RepoHtmlDocument(HtmlDocument): +class RepoHtmlReport(HtmlReportBase): def __init__(self, started, output, include_archived_repositories, include_forked_repositories): super().__init__(started, output) self.include_archived_repositories = include_archived_repositories self.include_forked_repositories = include_forked_repositories + def _epilogue(self): + options = { + "Archived repositories": "included" if self.include_archived_repositories else "not included", + "Forked repositories": "included" if self.include_forked_repositories else "not included", + } + self._command_options(options) + def _table_header(self): with self.tag("tr"): with self.tag("th", rowspan=2): @@ -433,16 +436,17 @@ def repos( output_json(repositories, output) return - klass = { - OutputFormat.html: RepoHtmlDocument, - OutputFormat.text: RepoTextDocument, + report_class = { + OutputFormat.html: RepoHtmlReport, + OutputFormat.text: RepoTextReport, }.get(output_format) - if klass is None: + if report_class is None: OutputFormat.unknown(output_format) + return - document = klass(started, output, include_archived_repositories, include_forked_repositories) - document.begin_document(title) - document.begin_table() + report = report_class(started, output, include_archived_repositories, include_forked_repositories) + report.begin_report(title) + report.begin_table() for repo in sorted(repositories, key=lambda x: x['name'].lower()): open_pull_requests = repo["open_pull_requests"] columns = [ @@ -453,11 +457,91 @@ def repos( open_pull_requests[0]["title"] if open_pull_requests else "", open_pull_requests[0]["created_at"] if open_pull_requests else "", ] - document.table_row(*columns, rowspan=len(open_pull_requests), first=True) + report.table_row(*columns, rowspan=len(open_pull_requests), first=True) for pr in open_pull_requests[1:]: - document.table_row(title=pr["title"], created_at=pr["created_at"]) - document.end_table() - document.end_document() + report.table_row(title=pr["title"], created_at=pr["created_at"]) + report.end_table() + report.end_report() + + +class RepoContributionsTextReport(TextReportBase): + + def __init__(self, started, output, include_archived_repositories, include_forked_repositories): + super().__init__(started, output) + self.include_archived_repositories = include_archived_repositories + self.include_forked_repositories = include_forked_repositories + + def _table_header(self): + if self.table is None: + raise RuntimeError("not building a table") + + self.table.add_column("Name") + self.table.add_column("Contributor") + self.table.add_column("Nr. of contributions", justify="right") + + def _table_row(self, repo_name=None, contributor=None, rowspan=0, first=False): + if self.table is None: + raise RuntimeError("not building a table") + row = [] + row.append(repo_name) + if contributor is not None: + row.append(format_member(contributor)) + row.append(format_int(contributor.get("contributions"))) + else: + row.extend((None, None)) + self.table.add_row(*row) + + +class RepoContributionsHtmlReport(HtmlReportBase): + + def __init__(self, started, output, include_archived_repositories, include_forked_repositories): + super().__init__(started, output) + self.include_archived_repositories = include_archived_repositories + self.include_forked_repositories = include_forked_repositories + + def _epilogue(self): + options = { + "Archived repositories": "included" if self.include_archived_repositories else "not included", + "Forked repositories": "included" if self.include_forked_repositories else "not included", + } + self._command_options(options) + + def _table_header(self): + with self.tag("tr"): + with self.tag("th", rowspan=2): + self.text("Name") + with self.tag("th", colspan=5, klass="column-group-header"): + self.text("Contributor") + with self.tag("tr"): + with self.tag("th"): + self.text("Name") + with self.tag("th"): + self.text("Login") + with self.tag("th"): + self.text("Email") + with self.tag("th"): + self.text("Profile") + with self.tag("th"): + self.text("#contributions") + + def _table_row(self, repo_name=None, contributor=None, rowspan=0, first=False): + klass_first_row = {"klass": "first-row"} if first else {} + klass_first_row_right = {"klass": "first-row right"} if first else {"klass": "right"} + if repo_name is not None: + with self.tag("td", **klass_first_row, rowspan=rowspan): + self.text(repo_name) + if contributor is None: + contributor = {} + with self.tag("td", **klass_first_row): + self.text(contributor.get("name") or "") + with self.tag("td", **klass_first_row): + self.text(contributor.get("login") or "") + with self.tag("td", **klass_first_row): + self.doc.asis(create_html_email_href(contributor.get("email")) or "") + with self.tag("td", **klass_first_row): + self.doc.asis(create_html_url_href(contributor.get("url")) or "") + with self.tag("td", **klass_first_row_right): + self.text(format_int(contributor.get("contributions"))) @app.command() @@ -496,78 +580,88 @@ def repo_contributions( if output_format == OutputFormat.json: output_json(repositories, output) + return - elif output_format == OutputFormat.html: - doc, tag, text = Doc().tagtext() - with tag('html'): - create_html_head(doc, title) - with tag("body"): - with tag("h1"): - text(title) - with tag("table"): - with tag("tr"): - with tag("th", rowspan=2): - text("Name") - with tag("th", colspan=5, klass="column-group-header"): - text("Contributor") - with tag("tr"): - with tag("th"): - text("Name") - with tag("th"): - text("Login") - with tag("th"): - text("Email") - with tag("th"): - text("Profile") - with tag("th"): - text("#contributions") - - def contributor_row_cells(contributor, klass): - td_optional_klass_arg = dict(klass=klass) if klass else {} - name = contributor.get("name") or "" - login = contributor.get("login") or "" - email = contributor.get("email") or "" - url = contributor.get("url") or "" - with tag("td", **td_optional_klass_arg): - text(name) - with tag("td", **td_optional_klass_arg): - text(login) - with tag("td", **td_optional_klass_arg): - doc.asis(create_html_email_href(email)) - with tag("td", **td_optional_klass_arg): - doc.asis(create_html_url_href(url)) - with tag("td", klass=f"{klass} right" if klass else "right"): - text(format_int(contributor.get("contributions"))) - - for repo in repositories: - contributors = repo['contributors'] - if len(contributors) == 0: - contributors = [{}] - with tag("tr"): - with tag("td", klass="first-row", rowspan=len(contributors)): - text(repo["name"]) - contributor_row_cells(contributors[0], "first-row") - for contributor in contributors[1:]: - with tag("tr"): - contributor_row_cells(contributor, None) - create_html_footer(doc, started) - print(indent(doc.getvalue()), file=output) - - elif output_format == OutputFormat.text: - table = Table( - "Name", "Contributor", Column("Nr. of contributions", justify="right"), box=box.SQUARE - ) - for repo in sorted(repositories, key=lambda x: x['name'].lower()): - contributors = sorted(repo['contributors'], key=lambda x: (1_000_000_000 - x['contributions'], x['login'].lower())) - first_contributor = contributors[0] if contributors else dict() - first_row = [repo["name"], format_member(first_contributor), format_int(first_contributor.get("contributions"))] - table.add_row(*first_row) - for contributor in contributors[1:]: - table.add_row(None, format_member(contributor), format_int(contributor["contributions"])) - output_text_elements(title, table, started, output) - - else: + report_class = { + OutputFormat.html: RepoContributionsHtmlReport, + OutputFormat.text: RepoContributionsTextReport, + }.get(output_format) + if report_class is None: OutputFormat.unknown(output_format) + return + + report = report_class(started, output, include_archived_repositories, include_forked_repositories) + report.begin_report(title) + report.begin_table() + for repo in sorted(repositories, key=lambda x: x['name'].lower()): + contributors = list(sorted( + repo['contributors'], + key=lambda x: (1_000_000_000 - x['contributions'], x['login'].lower()) + )) + if len(contributors) == 0: + contributors = [{}] + report.table_row(repo_name=repo["name"], contributor=contributors[0], first=True, rowspan=len(contributors)) + for contributor in contributors[1:]: + report.table_row(contributor=contributor) + report.end_table() + report.end_report() + + +class MembersTextReport(TextReportBase): + + def _table_header(self): + if self.table is None: + raise RuntimeError("not building a table") + self.table.add_column("Member") + self.table.add_column("Membership state") + self.table.add_column("Membership role") + + def _table_row(self, member): + if self.table is None: + raise RuntimeError("not building a table") + self.table.add_row(format_member(member), member['membership_state'], member['membership_role']) + + +class MembersHtmlReport(HtmlReportBase): + + def _table_header(self): + + with self.tag("tr"): + with self.tag("th", colspan=4, klass="column-group-header"): + self.text("Member") + self.doc.stag("th") + with self.tag("th", colspan=2, klass="column-group-header"): + self.text("Membership") + with self.tag("tr"): + with self.tag("th"): + self.text("Name") + with self.tag("th"): + self.text("Login") + with self.tag("th"): + self.text("Email") + with self.tag("th"): + self.text("Profile") + self.doc.stag("th") + with self.tag("th"): + self.text("state") + with self.tag("th"): + self.text("role") + + def _table_row(self, member): + email = member.get("email") + with self.tag("td"): + self.text(member.get("name") or "") + with self.tag("td"): + self.text(member.get("login") or "") + with self.tag("td"): + self.doc.asis(create_html_email_href(email) if email else "") + with self.tag("td"): + self.doc.asis(create_html_url_href(member.get("url") or "")) + self.doc.stag("td") + with self.tag("td"): + self.text(member['membership_state']) + with self.tag("td"): + self.text(member['membership_role']) @app.command() @@ -594,65 +688,23 @@ def members( if output_format == OutputFormat.json: output_json(member_info, output) + return - elif output_format == OutputFormat.html: - doc, tag, text = Doc().tagtext() - with tag('html'): - create_html_head(doc, title) - with tag("body"): - with tag("h1"): - text(title) - with tag("table"): - with tag("tr"): - with tag("th", colspan=4, klass="column-group-header"): - text("Member") - doc.stag("th") - with tag("th", colspan=2, klass="column-group-header"): - text("Membership") - with tag("tr"): - with tag("th"): - text("Name") - with tag("th"): - text("Login") - with tag("th"): - text("Email") - with tag("th"): - text("Profile") - doc.stag("th") - with tag("th"): - text("state") - with tag("th"): - text("role") - for member in member_info: - name = member.get("name") or "" - login = member.get("login") or "" - email = member.get("email") or "" - url = member.get("url") or "" - with tag("tr"): - with tag("td"): - text(name) - with tag("td"): - text(login) - with tag("td"): - doc.asis(create_html_email_href(email)) - with tag("td"): - doc.asis(create_html_url_href(url)) - doc.stag("td") - with tag("td"): - text(member['membership_state']) - with tag("td"): - text(member['membership_role']) - create_html_footer(doc, started) - print(indent(doc.getvalue()), file=output) - - elif output_format == OutputFormat.text: - table = Table("Member", "Membership state", "Membership role", box=box.SQUARE) - for member in member_info: - table.add_row(format_member(member), member['membership_state'], member['membership_role']) - output_text_elements(title, table, started, output) - - else: + report_class = { + OutputFormat.html: MembersHtmlReport, + OutputFormat.text: MembersTextReport, + }.get(output_format) + if report_class is None: OutputFormat.unknown(output_format) + return + + report = report_class(started, output) + report.begin_report(title) + report.begin_table() + for member in member_info: + report.table_row(member) + report.end_table() + report.end_report() if __name__ == "__main__": From 01622a5194f17cade7ebce7a5d58f774d58bff86 Mon Sep 17 00:00:00 2001 From: Eric Nieuwland Date: Tue, 3 Aug 2021 15:04:32 +0200 Subject: [PATCH 3/4] Review results --- audit.py | 231 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 119 insertions(+), 112 deletions(-) diff --git a/audit.py b/audit.py index 54297d1..dccf52d 100644 --- a/audit.py +++ b/audit.py @@ -1,5 +1,5 @@ """Audit GitHub organizations.""" - +import abc import configparser import json from enum import Enum @@ -20,6 +20,13 @@ from yattag import SimpleDoc, Doc, indent +LARGE_NUMBER = 1_000_000_000_000 + + +def reverse_numeric_sort_order(number: int): + return LARGE_NUMBER - number + + CONFIG_FILE = ".audit.cfg" # supported: # [github.com] @@ -47,38 +54,43 @@ def unknown(what: str) -> None: output_option = typer.Option(None, "--output", "-o") -class ReportBase: +class ReportBase(abc.ABC): - def __init__(self, started, output): + def __init__(self, started: datetime, output): self.started = started self.output = output + @abc.abstractmethod def begin_report(self, title: str): - raise NotImplementedError(f"{self.__class__.__name__}.begin_report()") + pass + @abc.abstractmethod def end_report(self): - raise NotImplementedError(f"{self.__class__.__name__}.end_report()") + pass + @abc.abstractmethod def empty_line(self): - raise NotImplementedError(f"{self.__class__.__name__}.empty_line()") + pass + @abc.abstractmethod def begin_table(self): - raise NotImplementedError(f"{self.__class__.__name__}.begin_table()") + pass + @abc.abstractmethod def table_row(self, *args, **kwargs): - raise NotImplementedError(f"{self.__class__.__name__}.table_row()") + pass + @abc.abstractmethod def end_table(self): - raise NotImplementedError(f"{self.__class__.__name__}.end_table()") + pass class TextReportBase(ReportBase): - table = None - def __init__(self, started, output): super().__init__(started, output) self.console = Console(file=output) + self.table = None def begin_report(self, title: str): self.console.print(title) @@ -95,14 +107,21 @@ def begin_table(self): self.table = Table(box=box.SQUARE) self._table_header() - def _table_header(self): - raise NotImplementedError(f"{self.__class__.__name__}._table_header()") + @abc.abstractmethod + def _table_header(self, headers): + if self.table is None: + raise RuntimeError("not building a table") + for header, kwargs in headers: + self.table.add_column(header, **kwargs) def table_row(self, *args, **kwargs): self._table_row(*args, **kwargs) + @abc.abstractmethod def _table_row(self, *args, **kwargs): - raise NotImplementedError(f"{self.__class__.__name__}.table_row()") + if self.table is None: + raise RuntimeError("not building a table") + self.table.add_row(*args) def end_table(self): if self.table is None: @@ -110,16 +129,14 @@ def end_table(self): self.console.print() self.console.print(self.table) self.console.print() + self.table = None class HtmlReportBase(ReportBase): def __init__(self, started, output): super().__init__(started, output) - doc, tag, text = Doc().tagtext() - self.doc = doc - self.tag = tag - self.text = text + self.doc, self.tag, self.text = Doc().tagtext() def begin_report(self, title: str): self.doc.asis("") @@ -189,15 +206,21 @@ def begin_table(self): self.doc.asis("") self._table_header() - def _table_header(self): - raise NotImplementedError(f"{self.__class__.__name__}._table_header()") + @abc.abstractmethod + def _table_header(self, *header_lines): + for headers in header_lines: + with self.tag("tr"): + for header, kwargs in headers: + with self.tag("th", **kwargs): + self.text(header) def table_row(self, *args, **kwargs): with self.tag("tr"): self._table_row(*args, **kwargs) + @abc.abstractmethod def _table_row(self, *args, **kwargs): - raise NotImplementedError(f"{self.__class__.__name__}.__table_row()") + pass def end_table(self): self.doc.asis("
") @@ -310,34 +333,31 @@ def __init__(self, started, output, include_archived_repositories, include_forke self.include_forked_repositories = include_forked_repositories def _table_header(self): - if self.table is None: - raise RuntimeError("not building a table") - self.table.add_column("Name") - if self.include_archived_repositories: - self.table.add_column("Archived", justify="center") - if self.include_forked_repositories: - self.table.add_column("Fork", justify="center") - self.table.add_column("Pushed at") - self.table.add_column("Pull request title") - self.table.add_column("Created at") + headers = [ + ("Name", {}), + ("Archived", dict(justify="center")) if self.include_archived_repositories else None, + ("Fork", dict(justify="center")) if self.include_forked_repositories else None, + ("Pushed at", {}), + ("Pull request title", {}), + ("Created at", {}), + ] + super()._table_header(headers) def _table_row( self, repo_name=None, archived=None, fork=None, pushed_at=None, title=None, created_at=None, rowspan=0, first=False ): - if self.table is None: - raise RuntimeError("not building a table") row = [] row.append(repo_name) if self.include_archived_repositories: row.append(format_bool(archived) if archived is not None else None) if self.include_forked_repositories: row.append(format_bool(fork) if fork is not None else None) - row.append(f"{format_friendly_timestamp(pushed_at, self.started)}" if pushed_at is not None else None) + row.append(format_friendly_timestamp(pushed_at, self.started) if pushed_at is not None else None) if title and created_at: - row.extend([title, f"{format_friendly_timestamp(created_at, self.started)}"]) - self.table.add_row(*row) + row.extend([title, format_friendly_timestamp(created_at, self.started)]) + super()._table_row(*row) class RepoHtmlReport(HtmlReportBase): @@ -355,24 +375,19 @@ def _epilogue(self): self._command_options(options) def _table_header(self): - with self.tag("tr"): - with self.tag("th", rowspan=2): - self.text("Name") - if self.include_archived_repositories: - with self.tag("th", rowspan=2): - self.text("Archived") - if self.include_forked_repositories: - with self.tag("th", rowspan=2): - self.text("Fork") - with self.tag("th", rowspan=2): - self.text("Pushed at") - with self.tag("th", colspan=2, klass="column-group-header"): - self.text("Pull request") - with self.tag("tr"): - with self.tag("th"): - self.text("Title") - with self.tag("th"): - self.text("Created at") + headers_1 = [ + ("Name", dict(rowspan=2)), + ("Archived", dict(rowspan=2)) if self.include_archived_repositories else None, + ("Fork", dict(rowspan=2)) if self.include_forked_repositories else None, + ("Pushed at", dict(rowspan=2)), + ("Pull request", dict(colspan=2, klass="column-group-header")), + ] + headers_1 = [header for header in headers_1 if header is not None] + headers_2 = [ + ("Title", {}), + ("Created at", {}), + ] + super()._table_header(headers_1, headers_2) def _table_row( self, @@ -447,7 +462,7 @@ def repos( report = report_class(started, output, include_archived_repositories, include_forked_repositories) report.begin_report(title) report.begin_table() - for repo in sorted(repositories, key=lambda x: x['name'].lower()): + for repo in sorted(repositories, key=lambda repo: repo['name'].lower()): open_pull_requests = repo["open_pull_requests"] columns = [ repo["name"], @@ -472,16 +487,14 @@ def __init__(self, started, output, include_archived_repositories, include_forke self.include_forked_repositories = include_forked_repositories def _table_header(self): - if self.table is None: - raise RuntimeError("not building a table") - - self.table.add_column("Name") - self.table.add_column("Contributor") - self.table.add_column("Nr. of contributions", justify="right") + headers = [ + ("Name", {}), + ("Contributor", {}), + ("Nr. of contributions", dict(justify="right")), + ] + super()._table_header(headers) def _table_row(self, repo_name=None, contributor=None, rowspan=0, first=False): - if self.table is None: - raise RuntimeError("not building a table") row = [] row.append(repo_name) if contributor is not None: @@ -489,7 +502,7 @@ def _table_row(self, repo_name=None, contributor=None, rowspan=0, first=False): row.append(format_int(contributor.get("contributions"))) else: row.extend((None, None)) - self.table.add_row(*row) + super()._table_row(*row) class RepoContributionsHtmlReport(HtmlReportBase): @@ -507,22 +520,18 @@ def _epilogue(self): self._command_options(options) def _table_header(self): - with self.tag("tr"): - with self.tag("th", rowspan=2): - self.text("Name") - with self.tag("th", colspan=5, klass="column-group-header"): - self.text("Contributor") - with self.tag("tr"): - with self.tag("th"): - self.text("Name") - with self.tag("th"): - self.text("Login") - with self.tag("th"): - self.text("Email") - with self.tag("th"): - self.text("Profile") - with self.tag("th"): - self.text("#contributions") + headers_1 = [ + ("Name", dict(rowspan=2)), + ("Contributor", dict(colspan=5, klass="column-group-header")), + ] + headers_2 = [ + ("Name", {}), + ("Login", {}), + ("Email", {}), + ("Profile", {}), + ("#contributions", {}), + ] + super()._table_header(headers_1, headers_2) def _table_row(self, repo_name=None, contributor=None, rowspan=0, first=False): klass_first_row = {"klass": "first-row"} if first else {} @@ -568,7 +577,10 @@ def repo_contributions( ) for contributor in sorted( get_contributors(repo), - key=lambda contributor: (1_000_000_000 - contributor.contributions, contributor.login.lower()) + key=lambda contributor: ( + reverse_numeric_sort_order(contributor.contributions), + contributor.login.lower(), + ) ) ] ) @@ -593,10 +605,13 @@ def repo_contributions( report = report_class(started, output, include_archived_repositories, include_forked_repositories) report.begin_report(title) report.begin_table() - for repo in sorted(repositories, key=lambda x: x['name'].lower()): + for repo in sorted(repositories, key=lambda repo: repo['name'].lower()): contributors = list(sorted( repo['contributors'], - key=lambda x: (1_000_000_000 - x['contributions'], x['login'].lower()) + key=lambda contributor: ( + reverse_numeric_sort_order(contributor['contributions']), + contributor['login'].lower() + ) )) if len(contributors) == 0: contributors = [{}] @@ -610,42 +625,34 @@ def repo_contributions( class MembersTextReport(TextReportBase): def _table_header(self): - if self.table is None: - raise RuntimeError("not building a table") - self.table.add_column("Member") - self.table.add_column("Membership state") - self.table.add_column("Membership role") + headers = [ + ("Member", {}), + ("Membership state", {}), + ("Membership role", {}), + ] + super()._table_header(headers) def _table_row(self, member): - if self.table is None: - raise RuntimeError("not building a table") - self.table.add_row(format_member(member), member['membership_state'], member['membership_role']) + super()._table_row(format_member(member), member['membership_state'], member['membership_role']) class MembersHtmlReport(HtmlReportBase): def _table_header(self): - - with self.tag("tr"): - with self.tag("th", colspan=4, klass="column-group-header"): - self.text("Member") - self.doc.stag("th") - with self.tag("th", colspan=2, klass="column-group-header"): - self.text("Membership") - with self.tag("tr"): - with self.tag("th"): - self.text("Name") - with self.tag("th"): - self.text("Login") - with self.tag("th"): - self.text("Email") - with self.tag("th"): - self.text("Profile") - self.doc.stag("th") - with self.tag("th"): - self.text("state") - with self.tag("th"): - self.text("role") + headers_1 = [ + ("Member", dict(colspan=4, klass="column-group-header")), + ("Membership", dict(colspan=2, klass="column-group-header")), + ] + headers_2 = [ + ("Name", {}), + ("Login", {}), + ("Email", {}), + ("Profile", {}), + ("", {}), + ("state", {}), + ("role", {}), + ] + super()._table_header(headers_1, headers_2) def _table_row(self, member): email = member.get("email") From dbe2249d2503aeabbb33764185cdaceabe654d50 Mon Sep 17 00:00:00 2001 From: Eric Nieuwland Date: Tue, 3 Aug 2021 15:14:10 +0200 Subject: [PATCH 4/4] Fix --- audit.py | 1 + 1 file changed, 1 insertion(+) diff --git a/audit.py b/audit.py index dccf52d..093960d 100644 --- a/audit.py +++ b/audit.py @@ -341,6 +341,7 @@ def _table_header(self): ("Pull request title", {}), ("Created at", {}), ] + headers = [header for header in headers if header is not None] super()._table_header(headers) def _table_row(