Пример #1
0
class Presentation:
    def __init__(self, content):
        self.slides = [
            slide_content.strip() for slide_content in content.split("---\n")
        ]
        print(self.slides)
        for i in range(1, len(self.slides)):
            self.slides[i] = "# " + self.slides[i]
        self.index = 0
        self.live = Live()

    def start(self):
        self.live.start()
        self.update()

    def stop(self):
        self.live.stop()

    def next(self):
        if self.index >= self.__len__() - 1:
            self.stop()
            return
        self.index = self.index + 1
        self.update()

    def update(self):
        self.live.update(
            Layout(Padding(Markdown(self.slides[self.index]), (2, 2))))

    @property
    def started(self):
        return self.live.is_started

    def __len__(self):
        return len(self.slides)
Пример #2
0
 def live_table(self, refresh_rate: int = 5) -> ContextManager[Live]:
     """Context manager providing a live UI which can be updated dynamically"""
     live_table = Live(refresh_per_second=refresh_rate)
     live_table.start()
     try:
         yield live_table
     finally:
         live_table.stop()
Пример #3
0
class CommonBarManager:
    def __init__(self):
        self._table = Table.grid()
        self._live = Live(self._table, console=console)

        async def close():
            self._live.stop()

        on_close_callbacks.append(close)

        self._live.start()

    def get_bar(self):
        bar = create_bar()
        tree = Tree(bar)
        self._table.add_row(tree)
        return CommonBar(tree, bar)
Пример #4
0
class OverallBarManager:
    def __init__(self):
        self._bar = create_bar()
        self._tree = Tree(self._bar)
        self._live = Live(self._tree, console=console)

        async def close():
            self._live.stop()

        on_close_callbacks.append(close)

        self._live.start()

        self._overall_bar = OverallBar(self._bar)

    def get_bar(self):
        return VirtualBar(self._tree, self._overall_bar)
Пример #5
0
class RichTablePrinter(object):
    def __init__(self, fields={}, key=None):
        """
        Logger based on `rich` tables

        :param key: str or None
            main key to group results by row
        :param fields: dict of (dict or False)
            Field descriptors containing goal ("lower_is_better" or "higher_is_better"), format and display name
            The key is a regex that will be used to match the fields to log
        """

        self.fields = dict(fields)
        self.key = key
        self.key_to_row_idx = {}
        self.name_to_column_idx = {}
        self.table = None
        self.console = None
        self.live = None
        self.best = {}
        if key is not None and key not in self.fields:
            self.fields = {key: {}, **fields}

    def _repr_html_(self) -> str:
        if self.console is None:
            return "Empty table"
        segments = list(self.console.render(self.table, self.console.options))  # type: ignore
        html = _render_segments(segments)
        return html

    def log(self, info):
        if self.table is None:
            is_in_notebook = check_is_in_notebook()
            self.table = Table()
            self.console = Console(width=2 ** 32 - 1 if is_in_notebook else None)

            if is_in_notebook:
                dh = display(None, display_id=True)
                self.refresh = lambda: dh.update(self)
            else:
                self.live = Live(self.table, console=self.console, auto_refresh=False)
                self.live.start()
                self.refresh = lambda: self.live.refresh()
            # self.console = Console()
            # table_centered = Columns((self.table,), align="center", expand=True)
            # self.live = Live(table_centered, console=console)
            # self.live.start()

        for name, value in info.items():
            if name not in self.name_to_column_idx:
                matcher, column_name = get_last_matching_value(self.fields, name, "name", default=name)
                if column_name is False:
                    self.name_to_column_idx[name] = -1
                    continue
                self.table.add_column(re.sub(matcher, column_name, name) if matcher is not None else name, no_wrap=True)
                self.table.columns[-1]._cells = [''] * (len(self.table.columns[0]._cells) if len(self.table.columns) else 0)
                self.name_to_column_idx[name] = (max(self.name_to_column_idx.values()) + 1) if len(self.name_to_column_idx) else 0
        new_name_to_column_idx = {}
        columns = []

        def get_name_index(name):
            try:
                return get_last_matching_index(self.fields, name)
            except ValueError:
                return len(self.name_to_column_idx)

        for name in sorted(self.name_to_column_idx.keys(), key=get_name_index):
            if self.name_to_column_idx[name] >= 0:
                columns.append(self.table.columns[self.name_to_column_idx[name]])
                new_name_to_column_idx[name] = (max(new_name_to_column_idx.values()) + 1) if len(new_name_to_column_idx) else 0
            else:
                new_name_to_column_idx[name] = -1
        self.table.columns = columns
        self.name_to_column_idx = new_name_to_column_idx

        if self.key is not None and self.key in info and info[self.key] in self.key_to_row_idx:
            idx = self.key_to_row_idx[info[self.key]]
        elif self.key is not None and self.key not in info and self.key_to_row_idx:
            idx = list(self.key_to_row_idx.values())[-1]
        else:
            self.table.add_row()
            idx = len(self.table.rows) - 1
            if self.key is not None:
                self.key_to_row_idx[info[self.key]] = idx
        for name, value in info.items():
            if self.name_to_column_idx[name] < 0:
                continue
            formatted_value = get_last_matching_value(self.fields, name, "format", "{}")[1].format(value)
            goal = get_last_matching_value(self.fields, name, "goal", None)[1]
            if goal is not None:
                if name not in self.best:
                    self.best[name] = value
                else:
                    diff = (value - self.best[name]) * (-1 if goal == "lower_is_better" else 1)
                    if diff > 0:
                        self.best[name] = value
                        formatted_value = "[green]" + formatted_value + "[/green]"
                    elif diff <= 0:
                        formatted_value = "[red]" + formatted_value + "[/red]"
            self.table.columns[self.name_to_column_idx[name]]._cells[idx] = formatted_value

        self.refresh()

    def finalize(self):
        if self.live is not None:
            self.live.stop()
Пример #6
0
class RichUI(UI):
    """
    A UI that uses the rich terminal library
    """
    def __init__(self) -> None:
        self._rich_live: Live

    def __enter__(self) -> 'RichUI':
        self._rich_live = Live(Spinner("bouncingBar", ""),
                               refresh_per_second=16)

        self._rich_live.start()
        return self

    def __exit__(self, *args: Any, **kwargs: Any) -> None:
        self._rich_live.stop()

    def update(self, job: SlurmJobStatus) -> None:
        self._rich_live.update(self._make_table(job))

    def error(self, text: str) -> None:
        self._rich_live.console.print(":cross_mark:",
                                      text,
                                      style="bold red",
                                      emoji=True)

    def info(self, text: str) -> None:
        self._rich_live.console.print(":information_source:",
                                      text,
                                      style="bold blue",
                                      emoji=True)

    def success(self, text: str) -> None:
        self._rich_live.console.print(":heavy_check_mark: ",
                                      text,
                                      style="bold green",
                                      emoji=True)

    def launch(self, text: str) -> None:
        self._rich_live.console.print(":rocket: ",
                                      text,
                                      style="bold yellow",
                                      emoji=True)

    def _make_table(self, job: SlurmJobStatus) -> Table:
        table = Table(style="bold", box=box.MINIMAL)
        table.add_column("ID")
        table.add_column("Name")
        table.add_column("State")

        for task in job.tasks:
            last_column: RenderableType = task.state
            color = "grey42"
            if task.state == "RUNNING":
                color = "blue"
                last_column = Spinner("arc", task.state)
            elif task.state == "COMPLETED":
                color = "green"
                last_column = f":heavy_check_mark: {task.state}"
            elif task.state == "FAILED":
                color = "red"
                last_column = f":cross_mark: {task.state}"

            table.add_row(str(task.id), task.name, last_column, style=color)

        return table
Пример #7
0
class Log:
    STATUS_WIDTH = 11

    def __init__(self) -> None:
        self.console = Console(highlight=False)

        self._crawl_progress = Progress(
            TextColumn("{task.description}", table_column=Column(ratio=1)),
            BarColumn(),
            TimeRemainingColumn(),
            expand=True,
        )
        self._download_progress = Progress(
            TextColumn("{task.description}", table_column=Column(ratio=1)),
            TransferSpeedColumn(),
            DownloadColumn(),
            BarColumn(),
            TimeRemainingColumn(),
            expand=True,
        )

        self._live = Live(console=self.console, transient=True)
        self._update_live()

        self._showing_progress = False
        self._progress_suspended = False
        self._lock = asyncio.Lock()
        self._lines: List[str] = []

        # Whether different parts of the output are enabled or disabled
        self.output_explain = False
        self.output_status = True
        self.output_report = True

    def _update_live(self) -> None:
        elements = []
        if self._crawl_progress.task_ids:
            elements.append(self._crawl_progress)
        if self._download_progress.task_ids:
            elements.append(self._download_progress)

        group = Group(*elements)
        self._live.update(group)

    @contextmanager
    def show_progress(self) -> Iterator[None]:
        if self._showing_progress:
            raise RuntimeError(
                "Calling 'show_progress' while already showing progress")

        self._showing_progress = True
        try:
            with self._live:
                yield
        finally:
            self._showing_progress = False

    @asynccontextmanager
    async def exclusive_output(self) -> AsyncIterator[None]:
        if not self._showing_progress:
            raise RuntimeError(
                "Calling 'exclusive_output' while not showing progress")

        async with self._lock:
            self._progress_suspended = True
            self._live.stop()
            try:
                yield
            finally:
                self._live.start()
                self._progress_suspended = False
                for line in self._lines:
                    self.print(line)
                self._lines = []

    def unlock(self) -> None:
        """
        Get rid of an exclusive output state.

        This function is meant to let PFERD print log messages after the event
        loop was forcibly stopped and if it will not be started up again. After
        this is called, it is not safe to use any functions except the logging
        functions (print, warn, ...).
        """

        self._progress_suspended = False
        for line in self._lines:
            self.print(line)

    def print(self, text: str) -> None:
        """
        Print a normal message. Allows markup.
        """

        if self._progress_suspended:
            self._lines.append(text)
        else:
            self.console.print(text)

    # TODO Print errors (and warnings?) to stderr

    def warn(self, text: str) -> None:
        """
        Print a warning message. Allows no markup.
        """

        self.print(f"[bold bright_red]Warning[/] {escape(text)}")

    def warn_contd(self, text: str) -> None:
        """
        Print further lines of a warning message. Allows no markup.
        """

        self.print(f"{escape(text)}")

    def error(self, text: str) -> None:
        """
        Print an error message. Allows no markup.
        """

        self.print(f"[bold bright_red]Error[/] [red]{escape(text)}")

    def error_contd(self, text: str) -> None:
        """
        Print further lines of an error message. Allows no markup.
        """

        self.print(f"[red]{escape(text)}")

    def unexpected_exception(self) -> None:
        """
        Call this in an "except" clause to log an unexpected exception.
        """

        t, v, tb = sys.exc_info()
        if t is None or v is None or tb is None:
            # We're not currently handling an exception, so somebody probably
            # called this function where they shouldn't.
            self.error("Something unexpected happened")
            self.error_contd("")
            for line in traceback.format_stack():
                self.error_contd(line[:-1])  # Without the newline
            self.error_contd("")
        else:
            self.error("An unexpected exception occurred")
            self.error_contd("")
            self.error_contd(traceback.format_exc())

        # Our print function doesn't take types other than strings, but the
        # underlying rich.print function does. This call is a special case
        # anyways, and we're calling it internally, so this should be fine.
        self.print(
            Panel.fit("""
Please copy your program output and send it to the PFERD maintainers, either
directly or as a GitHub issue: https://github.com/Garmelon/PFERD/issues/new
        """.strip()))  # type: ignore

    def explain_topic(self, text: str) -> None:
        """
        Print a top-level explain text. Allows no markup.
        """

        if self.output_explain:
            self.print(f"[yellow]{escape(text)}")

    def explain(self, text: str) -> None:
        """
        Print an indented explain text. Allows no markup.
        """

        if self.output_explain:
            self.print(f"  {escape(text)}")

    def status(self,
               style: str,
               action: str,
               text: str,
               suffix: str = "") -> None:
        """
        Print a status update while crawling. Allows markup in the "style"
        argument which will be applied to the "action" string.
        """

        if self.output_status:
            action = escape(f"{action:<{self.STATUS_WIDTH}}")
            self.print(f"{style}{action}[/] {escape(text)} {suffix}")

    def report(self, text: str) -> None:
        """
        Print a report after crawling. Allows markup.
        """

        if self.output_report:
            self.print(text)

    @contextmanager
    def _bar(
        self,
        progress: Progress,
        description: str,
        total: Optional[float],
    ) -> Iterator[ProgressBar]:
        if total is None:
            # Indeterminate progress bar
            taskid = progress.add_task(description, start=False)
        else:
            taskid = progress.add_task(description, total=total)
        self._update_live()

        try:
            yield ProgressBar(progress, taskid)
        finally:
            progress.remove_task(taskid)
            self._update_live()

    def crawl_bar(
        self,
        style: str,
        action: str,
        text: str,
        total: Optional[float] = None,
    ) -> ContextManager[ProgressBar]:
        """
        Allows markup in the "style" argument which will be applied to the
        "action" string.
        """

        action = escape(f"{action:<{self.STATUS_WIDTH}}")
        description = f"{style}{action}[/] {text}"
        return self._bar(self._crawl_progress, description, total)

    def download_bar(
        self,
        style: str,
        action: str,
        text: str,
        total: Optional[float] = None,
    ) -> ContextManager[ProgressBar]:
        """
        Allows markup in the "style" argument which will be applied to the
        "action" string.
        """

        action = escape(f"{action:<{self.STATUS_WIDTH}}")
        description = f"{style}{action}[/] {text}"
        return self._bar(self._download_progress, description, total)
Пример #8
0
class ProgressBarRich(ProgressBarBase):
    def __init__(self, min_value, max_value, title=None, progress=None, indent=0, parent=None):
        super(ProgressBarRich, self).__init__(min_value, max_value, title=title)
        import rich.progress
        import rich.table
        import rich.tree
        self.console = rich.console.Console(record=True)
        self.parent = parent
        if progress is None:
            self.progress = rich.progress.Progress(
                rich.progress.SpinnerColumn(),
                rich.progress.TextColumn("[progress.description]{task.description}"),
                rich.progress.BarColumn(),
                rich.progress.TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
                # rich.progress.TimeRemainingColumn(),
                TimeElapsedColumn(),
                rich.progress.TextColumn("[red]{task.fields[status]}"),
                console=self.console,
                transient=False,
                expand=False,
            )
        else:
            self.progress = progress
        if parent is None:
            self.node = rich.tree.Tree(self.progress)
            from rich.live import Live
            self.live = Live(self.node, refresh_per_second=5, console=self.console)
        else:
            self.node = parent.add(self.progress)
        # we do 1000 discrete steps
        self.steps = 0
        self.indent = indent

        padding = max(0, 45- (self.indent * 4) - len(self.title))
        self.passes = None
        self.task = self.progress.add_task(f"[red]{self.title}" + (" " * padding), total=1000, start=False, status=self.status or '', passes=self.passes)
        self.started = False
        self.subtasks = []

    def add_child(self, parent, task, title):
        return ProgressBarRich(self.min_value, self.max_value, title, indent=self.indent+1, parent=self.node)

    def __call__(self, value):
        if not self.started:
            self.progress.start_task(self.task)
        if value > self.value:
            steps = int(value * 1000)
            delta = steps - self.steps
            if delta > 0:
                self.progress.update(self.task, advance=delta, refresh=False, passes=self.passes)
            else:
                start_time = self.progress.tasks[0].start_time
                self.progress.reset(self.task, completed=steps, refresh=False, status=self.status or '')
                self.progress.tasks[0].start_time = start_time
            self.steps = steps
        self.value = value

    def update(self, value):
        self(value)

    def finish(self):
        self(self.max_value)
        if self.parent is None:
            self.live.refresh()

    def start(self):
        if self.parent is None and not self.live.is_started:
            self.live.refresh()
            self.live.start()

    def exit(self):
        if self.parent is None:
            self.live.stop()

    def set_status(self, status):
        self.status = status
        self.progress.update(self.task, status=self.status)

    def set_passes(self, passes):
        self.passes = passes