Source code for gitconductor.visualise

"""Visualisation options."""

import pathlib

import gitlab
import rich.box
import rich.console
import rich.table
import rich.tree

from gitconductor.gitlab import GitlabGroup, GitlabProject

CODE_TO_ACCESS = {
    0: "No Access",
    10: "Guest",
    20: "Reporter",
    30: "Developer",
    40: "Maintainer",
    50: "Owner",
}
CODE_TO_COLOUR = {
    0: "white",
    10: "red",
    20: "bright_magenta",
    30: "orange1",
    40: "green",
    50: "blue",
}
ACCESS_WARNING_LEVEL = "[yellow]Unavailable[/]"
ACCESS_WARNING_USER = "[yellow]Members hidden[/]"


[docs] def membership_warning(group: GitlabGroup | GitlabProject, error: Exception) -> list[str]: """Build a warning row for hidden membership. Args: group (GitlabGroup | GitlabProject): GitLab group or project. error (Exception): Error raised while listing members. Returns: list[str]: Warning row. """ message = str(error).strip() or "your token cannot list members here" return [ str(group.path), ACCESS_WARNING_USER, ACCESS_WARNING_LEVEL, "", message, group.visibility, ]
[docs] def visible_members(group: GitlabGroup | GitlabProject) -> tuple[list, list[str] | None]: """Safely list visible members. Args: group (GitlabGroup | GitlabProject): GitLab group or project. Returns: tuple[list, list[str] | None]: Members plus a warning row when access is hidden. """ try: return group.members, None except ( gitlab.exceptions.GitlabAuthenticationError, gitlab.exceptions.GitlabGetError, gitlab.exceptions.GitlabListError, ) as exc: return [], membership_warning(group, exc)
[docs] def build_tree(group: GitlabGroup, tree: rich.tree.Tree) -> rich.tree.Tree: """Iteratively build the tree. Args: group (gitlab.GitlabGroup): GitLab group instance. tree (rich.tree.Tree): Initial Tree instance. Returns: rich.tree.Tree: Updated Tree instance. """ for project in group.projects: branch = tree.add(project.name) for grp in group.subgroups: branch = tree.add(grp.name) build_tree(grp, branch) return tree
[docs] def build_table( group: GitlabGroup, rows: None | list[str] = None, depth: int = 0, maxdepth: int | None = None ) -> list[str]: """Iteratively build the table. Args: group (gitlab.GitlabGroup): GitLab group instance. rows (None, list): Previous table rows. depth (int): Depth inside the tree. maxdepth (int): Maximum recursion depth (0=PWD). Returns: list: New row to print to table. """ if rows is None: rows = [] if maxdepth is None or depth <= maxdepth: for project in group.projects: rows.extend(project.rows) for grp in group.subgroups: rows = build_table(grp, rows, depth=depth + 1, maxdepth=maxdepth) return rows
[docs] def build_access( group: GitlabGroup | GitlabProject, rows: None | list[str] = None, depth: int = 0, unique_ids: list[str] | None = None, explicit: bool = False, root: pathlib.Path = pathlib.Path(), maxdepth: int | None = None, colour_only: bool = False, ) -> list[str]: """Iteratively build access lists. Args: group (gitlab.GitlabGroup): GitLab group instance. rows (None, list): Previous table rows. depth (int): Depth inside the tree. unique_ids (list): List of all unique IDs printed explicit (bool): Explicitly show all members of all groups/projects? root (pathlib.Path): Top level directory. maxdepth (int): Maximum recursion depth (0=PWD). colour_only (bool): Only return the colour code for the access level. Returns: list: New row to print to table. """ if rows is None: rows = [] if unique_ids is None: unique_ids = [] members, warning = visible_members(group) if warning: warning[0] = str(group.path.relative_to(root.parent)) rows.append(warning) if members: members = [member for member in members if member.id not in unique_ids or explicit] for i, member in enumerate(members): rows.append( [ str(group.path.relative_to(root.parent)) if (not i or colour_only) else "", member.name, f"[{CODE_TO_COLOUR[member.access_level]}]{CODE_TO_ACCESS[member.access_level]}" if not colour_only else f"[{CODE_TO_COLOUR[member.access_level]}]", member.public_email, member.expires_at, group.visibility, ] ) unique_ids.append(member.id) if isinstance(group, GitlabGroup) and (maxdepth is None or depth < maxdepth): for project in group.projects: rows = build_access( project, rows, depth=depth + 1, unique_ids=unique_ids, explicit=explicit, maxdepth=maxdepth, root=root, colour_only=colour_only, ) for grp in group.subgroups: rows = build_access( grp, rows, depth=depth + 1, unique_ids=unique_ids, explicit=explicit, maxdepth=maxdepth, root=root, colour_only=colour_only, ) return rows
[docs] def tree(group: GitlabGroup) -> None: """Make a tree visualisation. Args: group (gitlab.GitlabGroup): GitLab group instance. Returns: None """ tree = rich.tree.Tree(group.name) tree = build_tree(group, tree) console = rich.console.Console() console.print(tree, crop=True)
[docs] def table(group: GitlabGroup, maxdepth: int | None = None) -> None: """Make a table visualisation. Args: group (gitlab.GitlabGroup): GitLab group instance. maxdepth (int): Maximum recursion depth (0=PWD). Returns: None """ rows = build_table(group, maxdepth=maxdepth) table = rich.table.Table() [table.add_column(c, "bold cyan") for c in ["Name", "Tree", "Branch", "Path", "Remote"]] for row in rows: table.add_row(*row) console = rich.console.Console() console.print(table, crop=True)
[docs] def access(group: GitlabGroup, explicit: bool = False, maxdepth: int | None = None) -> None: """Make an access visualisation. Args: group (gitlab.GitlabGroup): GitLab group instance. explicit (bool): Explicitly show all members of all groups/projects? maxdepth (int): Maximum recursion depth (0=PWD). Returns: None """ rows = build_access(group, explicit=explicit, maxdepth=maxdepth, root=group.path) table = rich.table.Table() [table.add_column(c) for c in ["Group/Project", "User", "Access Level", "Public Email", "Expiry"]] for row in rows: table.add_row(*row[:5]) console = rich.console.Console() console.print(table, crop=True)
[docs] def access_matrix(group: GitlabGroup, maxdepth: int | None = None) -> None: """Make an access matrix visualisation. Args: group (gitlab.GitlabGroup): GitLab group instance. maxdepth (int): Maximum recursion depth (0=PWD). Returns: None """ rows = build_access(group, explicit=True, maxdepth=maxdepth, root=group.path, colour_only=True) warnings = [r for r in rows if r[1] == ACCESS_WARNING_USER] member_rows = [r for r in rows if r[1] != ACCESS_WARNING_USER] entries = {r[0] for r in member_rows if r[0]} users = {r[1] for r in member_rows if r[1]} # Main table table = rich.table.Table(show_lines=True, box=rich.box.SQUARE) table.add_column("Group/Project", style="bold") [table.add_column(c) for c in sorted(users)] for entry in sorted(entries): visibility = next(r for r in member_rows if r[0] == entry)[-1] row = [ entry, ] for user in sorted(users): access = next((r[2] for r in member_rows if r[0] == entry and r[1] == user), None) if access is None: access = "[red]" if visibility == "public" else "[white]" block = f"[on {access[1:-1]}]{' ' * len(user)}" row.append(block) table.add_row(*row) console = rich.console.Console() console.print(table, crop=True) # Legend table table = rich.table.Table() table.add_column("Access Level", style="bold") for code, access in CODE_TO_ACCESS.items(): block = f"[on {CODE_TO_COLOUR[code]}]{' ' * 10}" table.add_row(f"{access:<10} {block}") console.print(table, crop=True) if warnings: table = rich.table.Table() [table.add_column(c) for c in ["Group/Project", "Warning", "Detail"]] for warning in warnings: table.add_row(warning[0], warning[1], warning[4]) console.print(table, crop=True)