Skip to content

Commit

Permalink
feat: Swap out homebrew progress bars for Rich
Browse files Browse the repository at this point in the history
- make bars hideable,
- polish,
- default the progress bar to visible, require show=False explicitly.
- add more bar styles,
- use some of the bars,
  • Loading branch information
kfsone authored and eyeonus committed May 6, 2024
1 parent bd4738a commit 52bb7f4
Show file tree
Hide file tree
Showing 4 changed files with 279 additions and 227 deletions.
39 changes: 23 additions & 16 deletions tradedangerous/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import typing

from .tradeexcept import TradeException
from tradedangerous.misc.progress import Progress, CountingBar
from . import corrections, utils
from . import prices

Expand Down Expand Up @@ -977,25 +978,31 @@ def buildCache(tdb, tdenv):
tempDB.executescript(sqlScript)

# import standard tables
for (importName, importTable) in tdb.importTables:
try:
processImportFile(tdenv, tempDB, Path(importName), importTable)
except FileNotFoundError:
tdenv.DEBUG0(
"WARNING: processImportFile found no {} file", importName
)
except StopIteration:
tdenv.NOTE(
"{} exists but is empty. "
"Remove it or add the column definition line.",
importName
)

tempDB.commit()
with Progress(max_value=len(tdb.importTables) + 1, width=25, style=CountingBar) as prog:
for (importName, importTable) in tdb.importTables:
with prog.sub_task(description=importName, max_value=None):
prog.increment(value=1, description=importName)
try:
processImportFile(tdenv, tempDB, Path(importName), importTable)
except FileNotFoundError:
tdenv.DEBUG0(
"WARNING: processImportFile found no {} file", importName
)
except StopIteration:
tdenv.NOTE(
"{} exists but is empty. "
"Remove it or add the column definition line.",
importName
)
prog.increment(1)

with prog.sub_task(description="Save DB"):
tempDB.commit()

# Parse the prices file
if pricesPath.exists():
processPricesFile(tdenv, tempDB, pricesPath)
with Progress(max_value=None, width=25, prefix="Processing prices file"):
processPricesFile(tdenv, tempDB, pricesPath)
else:
tdenv.NOTE(
"Missing \"{}\" file - no price data.",
Expand Down
130 changes: 93 additions & 37 deletions tradedangerous/misc/progress.py
Original file line number Diff line number Diff line change
@@ -1,76 +1,85 @@
from rich.progress import (
Progress as RichProgress,
TaskID,
ProgressColumn,
BarColumn, DownloadColumn, MofNCompleteColumn, SpinnerColumn,
TaskProgressColumn, TextColumn, TimeElapsedColumn, TimeRemainingColumn,
TransferSpeedColumn
)
from contextlib import contextmanager

from typing import Optional
from typing import Iterable, Optional, Type


class BarStyle:
""" Base class for Progress bar style types. """
def __init__(self, width: int=10, prefix: Optional[str] = None):
self.columns = [SpinnerColumn()]
if prefix is not None:
self.columns += [TextColumn(prefix)]
self.columns += [BarColumn(bar_width=width)]
self.columns += [TaskProgressColumn()]
self.columns += [TimeElapsedColumn()]
def __init__(self, width: int = 10, prefix: Optional[str] = None, *, add_columns: Optional[Iterable[ProgressColumn]]):
self.columns = [SpinnerColumn(), TextColumn(prefix), BarColumn(bar_width=width)]
if add_columns:
self.columns.extend(add_columns)


class CountingBar(BarStyle):
""" Creates a progress bar that is counting M/N items to completion. """
def __init__(self, width: int=10, prefix: Optional[str] = None):
self.columns = [SpinnerColumn()]
if prefix is not None:
self.columns += [TextColumn(prefix)]
self.columns += [BarColumn(bar_width=width)]
self.columns += [MofNCompleteColumn()]
self.columns += [TimeElapsedColumn()]
def __init__(self, width: int = 10, prefix: Optional[str] = None):
my_columns = [MofNCompleteColumn(), TimeElapsedColumn()]
super().__init__(width, prefix, add_columns=my_columns)


class DefaultBar(BarStyle):
""" Creates a simple default progress bar with a percentage and time elapsed. """
pass
def __init__(self, width: int = 10, prefix: Optional[str] = None):
my_columns = [TaskProgressColumn(), TimeElapsedColumn()]
super().__init__(width, prefix, add_columns=my_columns)


class TransferBar(BarStyle):
""" Creates a progress bar representing a data transfer, which shows the amount of
data transferred, speed, and estimated time remaining. """
def __init__(self, width: int=16, prefix: Optional[str] = None):
self.columns = (
SpinnerColumn(),
TextColumn(prefix),
BarColumn(bar_width=width),
DownloadColumn(),
TransferSpeedColumn(),
TimeRemainingColumn(),
)
def __init__(self, width: int = 16, prefix: Optional[str] = None):
my_columns = [DownloadColumn(), TransferSpeedColumn(), TimeRemainingColumn()]
super().__init__(width, prefix, add_columns=my_columns)


class Progress:
"""
Facade around the rich Progress bar system to help transition away from
TD's original basic progress bar implementation.
"""
def __init__(self, max_value: float, width: int, start: float = 0, prefix: str = "", *, style: BarStyle = DefaultBar) -> None:
def __init__(self,
max_value: Optional[float] = None,
width: Optional[int] = None,
start: float = 0,
prefix: Optional[str] = None,
*,
style: Optional[Type[BarStyle]] = None,
show: bool = True,
) -> None:
"""
:param max_value: Last value we can reach (100%).
:param width: How wide to make the bar itself.
:param start: Override initial value to non-zero.
:param prefix: Text to print between the spinner and the bar.
:param style: Bar-style factory to use for styling.
:param show: If False, disables the bar entirely.
"""
self.show = bool(show)
if not show:
return

if style is None:
style = DefaultBar

self.max_value = 0 if max_value is None else max(max_value, start)
self.value = start
self.prefix = prefix
self.width = width

self.prefix = prefix or ""
self.width = width or 25
# The 'Progress' itself is a view for displaying the progress of tasks. So we construct it
# and then create a task for our job.
style_instance = style(width=self.width, prefix=self.prefix)
self.progress = RichProgress(
# What fields to display.
*style(width=self.width, prefix=self.prefix).columns,
*style_instance.columns,
# Hide it once it's finished, update it for us, 4x a second
transient=True, auto_refresh=True, refresh_per_second=4
)
Expand All @@ -84,33 +93,80 @@ def __init__(self, max_value: float, width: int, start: float = 0, prefix: str =
self.progress.start()

def __enter__(self):
""" Context manager.
Example use:
import time
import tradedangerous.progress
# Progress(max_value=100, width=32, style=progress.CountingBar)
with progress.Progress(100, 32, style=progress.CountingBar) as prog:
for i in range(100):
prog.increment(1)
time.sleep(3)
"""
return self

def __exit__(self, *args, **kwargs):
self.clear()

def increment(self, value: float, description: Optional[str] = None, *, postfix: str = "") -> None:
def increment(self, value: Optional[float] = None, description: Optional[str] = None, *, progress: Optional[float] = None) -> None:
"""
Increase the progress of the bar by a given amount.
:param value: How much to increase the progress by.
:param postfix: [deprecated] text added after the bar
:param description: If set, replaces the task description.
:param progress: Instead of increasing by value, set the absolute progress to this.
"""
if not self.show:
return
if description:
self.progress.update(self.task, description=description)
self.value += value # Update our internal count
self.prefix = description
self.progress.update(self.task, description=description, refresh=True)

bump = False
if not value and progress is not None and self.value != progress:
self.value = progress
bump = True
elif value:
self.value += value # Update our internal count
bump = True

if self.value >= self.max_value: # Did we go past the end? Increase the end.
self.max_value += value * 2
self.progress.update(self.task, total=self.max_value)
if self.max_value > 0:
self.progress.update(self.task, completed=self.value)
self.progress.update(self.task, description=self.prefix, total=self.max_value)
bump = True

if bump and self.max_value > 0:
self.progress.update(self.task, description=self.prefix, completed=self.value)

def clear(self) -> None:
""" Remove the current progress bar, if any. """
# These two shouldn't happen separately, but incase someone tinkers, test each
# separately and shut them down.
if not self.show:
return

if self.task:
self.progress.remove_task(self.task)
self.task = None

if self.progress:
self.progress.stop()
self.progress = None

@contextmanager
def sub_task(self, description: str, max_value: Optional[int] = None, width: int = 25):
if not self.show:
yield
return
task = self.progress.add_task(description, total=max_value, start=True, width=width)
try:
yield task
finally:
self.progress.remove_task(task)

def update_task(self, task: TaskID, value: float, description: Optional[str] = None):
if self.show:
self.progress.update(task, value=value, description=description)
Loading

0 comments on commit 52bb7f4

Please sign in to comment.