printbuddies.printbuddies

  1from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
  2from os import get_terminal_size
  3from time import sleep
  4from typing import Any, Callable
  5
  6from noiftimer import Timer
  7
  8
  9def clear():
 10    """Erase the current line from the terminal."""
 11    try:
 12        print(" " * (get_terminal_size().columns - 1), flush=True, end="\r")
 13    except OSError:
 14        ...
 15    except Exception as e:
 16        raise e
 17
 18
 19def print_in_place(string: str, animate: bool = False, animate_refresh: float = 0.01):
 20    """Calls to `print_in_place` will overwrite the previous line of text in the terminal with `string`.
 21
 22    #### :params:
 23
 24    `animate`: Will cause `string` to be printed to the terminal one character at a time.
 25
 26    `animate_refresh`: Number of seconds between the addition of characters when `animate` is `True`."""
 27    clear()
 28    string = str(string)
 29    try:
 30        width = get_terminal_size().columns
 31        string = string[: width - 2]
 32        if animate:
 33            for i in range(len(string)):
 34                print(f"{string[:i+1]}", flush=True, end=" \r")
 35                sleep(animate_refresh)
 36        else:
 37            print(string, flush=True, end="\r")
 38    except OSError:
 39        ...
 40    except Exception as e:
 41        raise e
 42
 43
 44def ticker(info: list[str]):
 45    """Prints `info` to terminal with top and bottom padding so that previous text is not visible.
 46
 47    Similar visually to `print_in_place`, but for multiple lines."""
 48    try:
 49        width = get_terminal_size().columns
 50        info = [str(line)[: width - 1] for line in info]
 51        height = get_terminal_size().lines - len(info)
 52        print("\n" * (height * 2), end="")
 53        print(*info, sep="\n", end="")
 54        print("\n" * (int((height) / 2)), end="")
 55    except OSError:
 56        ...
 57    except Exception as e:
 58        raise e
 59
 60
 61class ProgBar:
 62    """Self incrementing, dynamically sized progress bar.
 63
 64    Includes an internal timer that starts when this object is created.
 65
 66    Easily add runtime to progress display:
 67
 68    >>> bar = ProgBar(total=100)
 69    >>> time.sleep(30)
 70    >>> bar.display(prefix=f"Doin stuff ~ {bar.runtime}")
 71    >>> "Doin stuff ~ runtime: 30s [_///////////////////]-1.00%" """
 72
 73    def __init__(
 74        self,
 75        total: float,
 76        update_frequency: int = 1,
 77        fill_ch: str = "_",
 78        unfill_ch: str = "/",
 79        width_ratio: float = 0.5,
 80        new_line_after_completion: bool = True,
 81        clear_after_completion: bool = False,
 82    ):
 83        """
 84        #### :params:
 85
 86        `total`: The number of calls to reach 100% completion.
 87
 88        `update_frequency`: The progress bar will only update once every this number of calls to `display()`.
 89        The larger the value, the less performance impact `ProgBar` has on the loop in which it is called.
 90        e.g.
 91        >>> bar = ProgBar(100, update_frequency=10)
 92        >>> for _ in range(100):
 93        >>>     bar.display()
 94
 95        ^The progress bar in the terminal will only update once every ten calls, going from 0%->100% in 10% increments.
 96        Note: If `total` is not a multiple of `update_frequency`, the display will not show 100% completion when the loop finishes.
 97
 98        `fill_ch`: The character used to represent the completed part of the bar.
 99
100        `unfill_ch`: The character used to represent the incomplete part of the bar.
101
102        `width_ratio`: The width of the progress bar relative to the width of the terminal window.
103
104        `new_line_after_completion`: Make a call to `print()` once `self.counter >= self.total`.
105
106        `clear_after_completion`: Make a call to `printbuddies.clear()` once `self.counter >= self.total`.
107
108        Note: if `new_line_after_completion` and `clear_after_completion` are both `True`, the line will be cleared
109        then a call to `print()` will be made."""
110        self.total = total
111        self.update_frequency = update_frequency
112        self.fill_ch = fill_ch[0]
113        self.unfill_ch = unfill_ch[0]
114        self.width_ratio = width_ratio
115        self.new_line_after_completion = new_line_after_completion
116        self.clear_after_completion = clear_after_completion
117        self.reset()
118        self.with_context = False
119
120    def __enter__(self):
121        self.with_context = True
122        return self
123
124    def __exit__(self, *args, **kwargs):
125        if self.clear_after_completion:
126            clear()
127        else:
128            print()
129
130    def reset(self):
131        self.counter = 1
132        self.percent = ""
133        self.prefix = ""
134        self.suffix = ""
135        self.filled = ""
136        self.unfilled = ""
137        self.timer = Timer(subsecond_resolution=False).start()
138
139    @property
140    def runtime(self) -> str:
141        return f"runtime:{self.timer.elapsed_str}"
142
143    @property
144    def bar(self) -> str:
145        return f"{self.prefix}{' '*bool(self.prefix)}[{self.filled}{self.unfilled}]-{self.percent}% {self.suffix}"
146
147    def get_percent(self) -> str:
148        """Returns the percentage completed to two decimal places as a string without the `%`."""
149        percent = str(round(100.0 * self.counter / self.total, 2))
150        if len(percent.split(".")[1]) == 1:
151            percent = percent + "0"
152        if len(percent.split(".")[0]) == 1:
153            percent = "0" + percent
154        return percent
155
156    def _prepare_bar(self):
157        self.terminal_width = get_terminal_size().columns - 1
158        bar_length = int(self.terminal_width * self.width_ratio)
159        progress = int(bar_length * min(self.counter / self.total, 1.0))
160        self.filled = self.fill_ch * progress
161        self.unfilled = self.unfill_ch * (bar_length - progress)
162        self.percent = self.get_percent()
163
164    def _trim_bar(self):
165        original_width = self.width_ratio
166        while len(self.bar) > self.terminal_width and self.width_ratio > 0:
167            self.width_ratio -= 0.01
168            self._prepare_bar()
169        self.width_ratio = original_width
170
171    def get_bar(self):
172        return f"{self.prefix}{' '*bool(self.prefix)}[{self.filled}{self.unfilled}]-{self.percent}% {self.suffix}"
173
174    def display(
175        self,
176        prefix: str = "",
177        suffix: str = "",
178        counter_override: float | None = None,
179        total_override: float | None = None,
180        return_object: Any | None = None,
181    ) -> Any:
182        """Writes the progress bar to the terminal.
183
184        #### :params:
185
186        `prefix`: String affixed to the front of the progress bar.
187
188        `suffix`: String appended to the end of the progress bar.
189
190        `counter_override`: When an externally incremented completion counter is needed.
191
192        `total_override`: When an externally controlled bar total is needed.
193
194        `return_object`: An object to be returned by display().
195        Allows `display()` to be called within a comprehension:
196
197        e.g.
198
199        >>> bar = ProgBar(10)
200        >>> def square(x: int | float)->int|float:
201        >>>     return x * x
202        >>> myList = [bar.display(return_object=square(i)) for i in range(10)]
203        >>> <progress bar gets displayed>
204        >>> myList
205        >>> [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]"""
206        if not self.timer.started:
207            self.timer.start()
208        if counter_override is not None:
209            self.counter = counter_override
210        if total_override:
211            self.total = total_override
212        # Don't wanna divide by 0 there, pal
213        while self.total <= 0:
214            self.total += 1
215        try:
216            if self.counter % self.update_frequency == 0:
217                self.prefix = prefix
218                self.suffix = suffix
219                self._prepare_bar()
220                self._trim_bar()
221                pad = " " * (self.terminal_width - len(self.bar))
222                width = get_terminal_size().columns
223                print(f"{self.bar}{pad}"[: width - 2], flush=True, end="\r")
224            if self.counter >= self.total:
225                self.timer.stop()
226                if not self.with_context:
227                    if self.clear_after_completion:
228                        clear()
229                    if self.new_line_after_completion:
230                        print()
231            self.counter += 1
232        except OSError:
233            ...
234        except Exception as e:
235            raise e
236        return return_object
237
238
239class PoolBar:
240    def __init__(
241        self,
242        pool_type: str,
243        funcs: list[Callable[[Any], Any]],
244        args: list[tuple[Any, ...]] | None = None,
245    ):
246        """Integrates multi thread/process execution with `ProgBar`.
247
248        #### :params:
249
250        `pool_type`: Should be either `thread` or `process`.
251
252        `funcs`: List of functions to be executed.
253
254        `args`: A list of tuples where each tuple is the args to be used with each function.
255
256        Returns a list of whatever results the functions in `funcs` return.
257
258        >>> def my_func(page: int)->str:
259        >>>     return requests.get(f"https://somesite.com/pages/{page}").text
260        >>> pool = PoolBar("thread", [my_func for _ in range(10)], [(i,) for i in range(10)])
261        >>> pages = pool.execute()"""
262
263        self.pool_type = pool_type
264        self.funcs = funcs
265        self.args = args
266
267    @property
268    def pool_type(self) -> str:
269        return self._pool_type
270
271    @pool_type.setter
272    def pool_type(self, pool_type: str):
273        if pool_type not in ["thread", "process"]:
274            raise ValueError(f"pool_type '{pool_type}' must be 'thread' or 'process'.")
275        self._pool_type = pool_type
276
277    def _get_executor(self) -> ThreadPoolExecutor | ProcessPoolExecutor:
278        match self.pool_type:
279            case "thread":
280                return ThreadPoolExecutor
281            case "process":
282                return ProcessPoolExecutor
283
284    def execute(self, *progbar_args, **progbar_kwargs) -> list[Any]:
285        """Execute the supplied functions with their supplied arguments, if any.
286
287        `*progbar_args` and `**progbar_kwargs` can be any arguments the `ProgBar` constructor takes besides `total`."""
288        num_workers = len(self.funcs)
289        with ProgBar(num_workers, *progbar_args, **progbar_kwargs) as bar:
290            with self._get_executor()() as executor:
291                if self.args:
292                    workers = [
293                        executor.submit(func, *args)
294                        for func, args in zip(self.funcs, self.args)
295                    ]
296                else:
297                    workers = [executor.submit(func) for func in self.funcs]
298                while (
299                    num_complete := len([worker for worker in workers if worker.done()])
300                ) < num_workers:
301                    bar.display(f"{bar.runtime}", counter_override=num_complete)
302                    sleep(0.1)
303            bar.display(f"{bar.runtime}", counter_override=num_complete)
304        return [worker.result() for worker in workers]
305
306
307class Spinner:
308    """Prints one of a sequence of characters in order everytime `display()` is called.
309
310    The `display` function writes the new character to the same line, overwriting the previous character.
311
312    The sequence will be cycled through indefinitely.
313
314    If used as a context manager, the last printed character will be cleared upon exiting.
315    """
316
317    def __init__(
318        self, sequence: list[str] = ["/", "-", "\\"], width_ratio: float = 0.25
319    ):
320        """
321        #### params:
322
323        `sequence`: Override the built in spin sequence.
324
325        `width_ratio`: The fractional amount of the terminal for characters to move across."""
326        self._base_sequence = sequence
327        self.width_ratio = width_ratio
328        self.sequence = self._base_sequence
329
330    def __enter__(self):
331        return self
332
333    def __exit__(self, *args, **kwargs):
334        clear()
335
336    @property
337    def width_ratio(self) -> float:
338        return self._width_ratio
339
340    @width_ratio.setter
341    def width_ratio(self, ratio: float):
342        self._width_ratio = ratio
343        self._update_width()
344
345    def _update_width(self):
346        self._current_terminal_width = get_terminal_size().columns
347        self._width = int((self._current_terminal_width - 1) * self.width_ratio)
348
349    @property
350    def sequence(self) -> list[Any]:
351        return self._sequence
352
353    @sequence.setter
354    def sequence(self, character_list: list[Any]):
355        self._sequence = [
356            ch.rjust(i + j)
357            for i in range(1, self._width, len(character_list))
358            for j, ch in enumerate(character_list)
359        ]
360        self._sequence += self._sequence[::-1]
361
362    def _get_next(self) -> str:
363        """Pop the first element of `self._sequence`, append it to the end, and return the element."""
364        ch = self.sequence.pop(0)
365        self.sequence.append(ch)
366        return ch
367
368    def display(self):
369        """Print the next character in the sequence."""
370        try:
371            if get_terminal_size().columns != self._current_terminal_width:
372                self._update_width()
373                self.sequence = self._base_sequence
374            print_in_place(self._get_next())
375        except OSError:
376            ...
377        except Exception as e:
378            raise e
def clear():
10def clear():
11    """Erase the current line from the terminal."""
12    try:
13        print(" " * (get_terminal_size().columns - 1), flush=True, end="\r")
14    except OSError:
15        ...
16    except Exception as e:
17        raise e

Erase the current line from the terminal.

def ticker(info: list[str]):
45def ticker(info: list[str]):
46    """Prints `info` to terminal with top and bottom padding so that previous text is not visible.
47
48    Similar visually to `print_in_place`, but for multiple lines."""
49    try:
50        width = get_terminal_size().columns
51        info = [str(line)[: width - 1] for line in info]
52        height = get_terminal_size().lines - len(info)
53        print("\n" * (height * 2), end="")
54        print(*info, sep="\n", end="")
55        print("\n" * (int((height) / 2)), end="")
56    except OSError:
57        ...
58    except Exception as e:
59        raise e

Prints info to terminal with top and bottom padding so that previous text is not visible.

Similar visually to print_in_place, but for multiple lines.

class ProgBar:
 62class ProgBar:
 63    """Self incrementing, dynamically sized progress bar.
 64
 65    Includes an internal timer that starts when this object is created.
 66
 67    Easily add runtime to progress display:
 68
 69    >>> bar = ProgBar(total=100)
 70    >>> time.sleep(30)
 71    >>> bar.display(prefix=f"Doin stuff ~ {bar.runtime}")
 72    >>> "Doin stuff ~ runtime: 30s [_///////////////////]-1.00%" """
 73
 74    def __init__(
 75        self,
 76        total: float,
 77        update_frequency: int = 1,
 78        fill_ch: str = "_",
 79        unfill_ch: str = "/",
 80        width_ratio: float = 0.5,
 81        new_line_after_completion: bool = True,
 82        clear_after_completion: bool = False,
 83    ):
 84        """
 85        #### :params:
 86
 87        `total`: The number of calls to reach 100% completion.
 88
 89        `update_frequency`: The progress bar will only update once every this number of calls to `display()`.
 90        The larger the value, the less performance impact `ProgBar` has on the loop in which it is called.
 91        e.g.
 92        >>> bar = ProgBar(100, update_frequency=10)
 93        >>> for _ in range(100):
 94        >>>     bar.display()
 95
 96        ^The progress bar in the terminal will only update once every ten calls, going from 0%->100% in 10% increments.
 97        Note: If `total` is not a multiple of `update_frequency`, the display will not show 100% completion when the loop finishes.
 98
 99        `fill_ch`: The character used to represent the completed part of the bar.
100
101        `unfill_ch`: The character used to represent the incomplete part of the bar.
102
103        `width_ratio`: The width of the progress bar relative to the width of the terminal window.
104
105        `new_line_after_completion`: Make a call to `print()` once `self.counter >= self.total`.
106
107        `clear_after_completion`: Make a call to `printbuddies.clear()` once `self.counter >= self.total`.
108
109        Note: if `new_line_after_completion` and `clear_after_completion` are both `True`, the line will be cleared
110        then a call to `print()` will be made."""
111        self.total = total
112        self.update_frequency = update_frequency
113        self.fill_ch = fill_ch[0]
114        self.unfill_ch = unfill_ch[0]
115        self.width_ratio = width_ratio
116        self.new_line_after_completion = new_line_after_completion
117        self.clear_after_completion = clear_after_completion
118        self.reset()
119        self.with_context = False
120
121    def __enter__(self):
122        self.with_context = True
123        return self
124
125    def __exit__(self, *args, **kwargs):
126        if self.clear_after_completion:
127            clear()
128        else:
129            print()
130
131    def reset(self):
132        self.counter = 1
133        self.percent = ""
134        self.prefix = ""
135        self.suffix = ""
136        self.filled = ""
137        self.unfilled = ""
138        self.timer = Timer(subsecond_resolution=False).start()
139
140    @property
141    def runtime(self) -> str:
142        return f"runtime:{self.timer.elapsed_str}"
143
144    @property
145    def bar(self) -> str:
146        return f"{self.prefix}{' '*bool(self.prefix)}[{self.filled}{self.unfilled}]-{self.percent}% {self.suffix}"
147
148    def get_percent(self) -> str:
149        """Returns the percentage completed to two decimal places as a string without the `%`."""
150        percent = str(round(100.0 * self.counter / self.total, 2))
151        if len(percent.split(".")[1]) == 1:
152            percent = percent + "0"
153        if len(percent.split(".")[0]) == 1:
154            percent = "0" + percent
155        return percent
156
157    def _prepare_bar(self):
158        self.terminal_width = get_terminal_size().columns - 1
159        bar_length = int(self.terminal_width * self.width_ratio)
160        progress = int(bar_length * min(self.counter / self.total, 1.0))
161        self.filled = self.fill_ch * progress
162        self.unfilled = self.unfill_ch * (bar_length - progress)
163        self.percent = self.get_percent()
164
165    def _trim_bar(self):
166        original_width = self.width_ratio
167        while len(self.bar) > self.terminal_width and self.width_ratio > 0:
168            self.width_ratio -= 0.01
169            self._prepare_bar()
170        self.width_ratio = original_width
171
172    def get_bar(self):
173        return f"{self.prefix}{' '*bool(self.prefix)}[{self.filled}{self.unfilled}]-{self.percent}% {self.suffix}"
174
175    def display(
176        self,
177        prefix: str = "",
178        suffix: str = "",
179        counter_override: float | None = None,
180        total_override: float | None = None,
181        return_object: Any | None = None,
182    ) -> Any:
183        """Writes the progress bar to the terminal.
184
185        #### :params:
186
187        `prefix`: String affixed to the front of the progress bar.
188
189        `suffix`: String appended to the end of the progress bar.
190
191        `counter_override`: When an externally incremented completion counter is needed.
192
193        `total_override`: When an externally controlled bar total is needed.
194
195        `return_object`: An object to be returned by display().
196        Allows `display()` to be called within a comprehension:
197
198        e.g.
199
200        >>> bar = ProgBar(10)
201        >>> def square(x: int | float)->int|float:
202        >>>     return x * x
203        >>> myList = [bar.display(return_object=square(i)) for i in range(10)]
204        >>> <progress bar gets displayed>
205        >>> myList
206        >>> [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]"""
207        if not self.timer.started:
208            self.timer.start()
209        if counter_override is not None:
210            self.counter = counter_override
211        if total_override:
212            self.total = total_override
213        # Don't wanna divide by 0 there, pal
214        while self.total <= 0:
215            self.total += 1
216        try:
217            if self.counter % self.update_frequency == 0:
218                self.prefix = prefix
219                self.suffix = suffix
220                self._prepare_bar()
221                self._trim_bar()
222                pad = " " * (self.terminal_width - len(self.bar))
223                width = get_terminal_size().columns
224                print(f"{self.bar}{pad}"[: width - 2], flush=True, end="\r")
225            if self.counter >= self.total:
226                self.timer.stop()
227                if not self.with_context:
228                    if self.clear_after_completion:
229                        clear()
230                    if self.new_line_after_completion:
231                        print()
232            self.counter += 1
233        except OSError:
234            ...
235        except Exception as e:
236            raise e
237        return return_object

Self incrementing, dynamically sized progress bar.

Includes an internal timer that starts when this object is created.

Easily add runtime to progress display:

>>> bar = ProgBar(total=100)
>>> time.sleep(30)
>>> bar.display(prefix=f"Doin stuff ~ {bar.runtime}")
>>> "Doin stuff ~ runtime: 30s [_///////////////////]-1.00%"
ProgBar( total: float, update_frequency: int = 1, fill_ch: str = '_', unfill_ch: str = '/', width_ratio: float = 0.5, new_line_after_completion: bool = True, clear_after_completion: bool = False)
 74    def __init__(
 75        self,
 76        total: float,
 77        update_frequency: int = 1,
 78        fill_ch: str = "_",
 79        unfill_ch: str = "/",
 80        width_ratio: float = 0.5,
 81        new_line_after_completion: bool = True,
 82        clear_after_completion: bool = False,
 83    ):
 84        """
 85        #### :params:
 86
 87        `total`: The number of calls to reach 100% completion.
 88
 89        `update_frequency`: The progress bar will only update once every this number of calls to `display()`.
 90        The larger the value, the less performance impact `ProgBar` has on the loop in which it is called.
 91        e.g.
 92        >>> bar = ProgBar(100, update_frequency=10)
 93        >>> for _ in range(100):
 94        >>>     bar.display()
 95
 96        ^The progress bar in the terminal will only update once every ten calls, going from 0%->100% in 10% increments.
 97        Note: If `total` is not a multiple of `update_frequency`, the display will not show 100% completion when the loop finishes.
 98
 99        `fill_ch`: The character used to represent the completed part of the bar.
100
101        `unfill_ch`: The character used to represent the incomplete part of the bar.
102
103        `width_ratio`: The width of the progress bar relative to the width of the terminal window.
104
105        `new_line_after_completion`: Make a call to `print()` once `self.counter >= self.total`.
106
107        `clear_after_completion`: Make a call to `printbuddies.clear()` once `self.counter >= self.total`.
108
109        Note: if `new_line_after_completion` and `clear_after_completion` are both `True`, the line will be cleared
110        then a call to `print()` will be made."""
111        self.total = total
112        self.update_frequency = update_frequency
113        self.fill_ch = fill_ch[0]
114        self.unfill_ch = unfill_ch[0]
115        self.width_ratio = width_ratio
116        self.new_line_after_completion = new_line_after_completion
117        self.clear_after_completion = clear_after_completion
118        self.reset()
119        self.with_context = False

:params:

total: The number of calls to reach 100% completion.

update_frequency: The progress bar will only update once every this number of calls to display(). The larger the value, the less performance impact ProgBar has on the loop in which it is called. e.g.

>>> bar = ProgBar(100, update_frequency=10)
>>> for _ in range(100):
>>>     bar.display()

^The progress bar in the terminal will only update once every ten calls, going from 0%->100% in 10% increments. Note: If total is not a multiple of update_frequency, the display will not show 100% completion when the loop finishes.

fill_ch: The character used to represent the completed part of the bar.

unfill_ch: The character used to represent the incomplete part of the bar.

width_ratio: The width of the progress bar relative to the width of the terminal window.

new_line_after_completion: Make a call to print() once self.counter >= self.total.

clear_after_completion: Make a call to printbuddies.clear() once self.counter >= self.total.

Note: if new_line_after_completion and clear_after_completion are both True, the line will be cleared then a call to print() will be made.

def reset(self):
131    def reset(self):
132        self.counter = 1
133        self.percent = ""
134        self.prefix = ""
135        self.suffix = ""
136        self.filled = ""
137        self.unfilled = ""
138        self.timer = Timer(subsecond_resolution=False).start()
def get_percent(self) -> str:
148    def get_percent(self) -> str:
149        """Returns the percentage completed to two decimal places as a string without the `%`."""
150        percent = str(round(100.0 * self.counter / self.total, 2))
151        if len(percent.split(".")[1]) == 1:
152            percent = percent + "0"
153        if len(percent.split(".")[0]) == 1:
154            percent = "0" + percent
155        return percent

Returns the percentage completed to two decimal places as a string without the %.

def get_bar(self):
172    def get_bar(self):
173        return f"{self.prefix}{' '*bool(self.prefix)}[{self.filled}{self.unfilled}]-{self.percent}% {self.suffix}"
def display( self, prefix: str = '', suffix: str = '', counter_override: float | None = None, total_override: float | None = None, return_object: typing.Any | None = None) -> Any:
175    def display(
176        self,
177        prefix: str = "",
178        suffix: str = "",
179        counter_override: float | None = None,
180        total_override: float | None = None,
181        return_object: Any | None = None,
182    ) -> Any:
183        """Writes the progress bar to the terminal.
184
185        #### :params:
186
187        `prefix`: String affixed to the front of the progress bar.
188
189        `suffix`: String appended to the end of the progress bar.
190
191        `counter_override`: When an externally incremented completion counter is needed.
192
193        `total_override`: When an externally controlled bar total is needed.
194
195        `return_object`: An object to be returned by display().
196        Allows `display()` to be called within a comprehension:
197
198        e.g.
199
200        >>> bar = ProgBar(10)
201        >>> def square(x: int | float)->int|float:
202        >>>     return x * x
203        >>> myList = [bar.display(return_object=square(i)) for i in range(10)]
204        >>> <progress bar gets displayed>
205        >>> myList
206        >>> [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]"""
207        if not self.timer.started:
208            self.timer.start()
209        if counter_override is not None:
210            self.counter = counter_override
211        if total_override:
212            self.total = total_override
213        # Don't wanna divide by 0 there, pal
214        while self.total <= 0:
215            self.total += 1
216        try:
217            if self.counter % self.update_frequency == 0:
218                self.prefix = prefix
219                self.suffix = suffix
220                self._prepare_bar()
221                self._trim_bar()
222                pad = " " * (self.terminal_width - len(self.bar))
223                width = get_terminal_size().columns
224                print(f"{self.bar}{pad}"[: width - 2], flush=True, end="\r")
225            if self.counter >= self.total:
226                self.timer.stop()
227                if not self.with_context:
228                    if self.clear_after_completion:
229                        clear()
230                    if self.new_line_after_completion:
231                        print()
232            self.counter += 1
233        except OSError:
234            ...
235        except Exception as e:
236            raise e
237        return return_object

Writes the progress bar to the terminal.

:params:

prefix: String affixed to the front of the progress bar.

suffix: String appended to the end of the progress bar.

counter_override: When an externally incremented completion counter is needed.

total_override: When an externally controlled bar total is needed.

return_object: An object to be returned by display(). Allows display() to be called within a comprehension:

e.g.

>>> bar = ProgBar(10)
>>> def square(x: int | float)->int|float:
>>>     return x * x
>>> myList = [bar.display(return_object=square(i)) for i in range(10)]
>>> <progress bar gets displayed>
>>> myList
>>> [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
class PoolBar:
240class PoolBar:
241    def __init__(
242        self,
243        pool_type: str,
244        funcs: list[Callable[[Any], Any]],
245        args: list[tuple[Any, ...]] | None = None,
246    ):
247        """Integrates multi thread/process execution with `ProgBar`.
248
249        #### :params:
250
251        `pool_type`: Should be either `thread` or `process`.
252
253        `funcs`: List of functions to be executed.
254
255        `args`: A list of tuples where each tuple is the args to be used with each function.
256
257        Returns a list of whatever results the functions in `funcs` return.
258
259        >>> def my_func(page: int)->str:
260        >>>     return requests.get(f"https://somesite.com/pages/{page}").text
261        >>> pool = PoolBar("thread", [my_func for _ in range(10)], [(i,) for i in range(10)])
262        >>> pages = pool.execute()"""
263
264        self.pool_type = pool_type
265        self.funcs = funcs
266        self.args = args
267
268    @property
269    def pool_type(self) -> str:
270        return self._pool_type
271
272    @pool_type.setter
273    def pool_type(self, pool_type: str):
274        if pool_type not in ["thread", "process"]:
275            raise ValueError(f"pool_type '{pool_type}' must be 'thread' or 'process'.")
276        self._pool_type = pool_type
277
278    def _get_executor(self) -> ThreadPoolExecutor | ProcessPoolExecutor:
279        match self.pool_type:
280            case "thread":
281                return ThreadPoolExecutor
282            case "process":
283                return ProcessPoolExecutor
284
285    def execute(self, *progbar_args, **progbar_kwargs) -> list[Any]:
286        """Execute the supplied functions with their supplied arguments, if any.
287
288        `*progbar_args` and `**progbar_kwargs` can be any arguments the `ProgBar` constructor takes besides `total`."""
289        num_workers = len(self.funcs)
290        with ProgBar(num_workers, *progbar_args, **progbar_kwargs) as bar:
291            with self._get_executor()() as executor:
292                if self.args:
293                    workers = [
294                        executor.submit(func, *args)
295                        for func, args in zip(self.funcs, self.args)
296                    ]
297                else:
298                    workers = [executor.submit(func) for func in self.funcs]
299                while (
300                    num_complete := len([worker for worker in workers if worker.done()])
301                ) < num_workers:
302                    bar.display(f"{bar.runtime}", counter_override=num_complete)
303                    sleep(0.1)
304            bar.display(f"{bar.runtime}", counter_override=num_complete)
305        return [worker.result() for worker in workers]
PoolBar( pool_type: str, funcs: list[typing.Callable[[typing.Any], typing.Any]], args: list[tuple[typing.Any, ...]] | None = None)
241    def __init__(
242        self,
243        pool_type: str,
244        funcs: list[Callable[[Any], Any]],
245        args: list[tuple[Any, ...]] | None = None,
246    ):
247        """Integrates multi thread/process execution with `ProgBar`.
248
249        #### :params:
250
251        `pool_type`: Should be either `thread` or `process`.
252
253        `funcs`: List of functions to be executed.
254
255        `args`: A list of tuples where each tuple is the args to be used with each function.
256
257        Returns a list of whatever results the functions in `funcs` return.
258
259        >>> def my_func(page: int)->str:
260        >>>     return requests.get(f"https://somesite.com/pages/{page}").text
261        >>> pool = PoolBar("thread", [my_func for _ in range(10)], [(i,) for i in range(10)])
262        >>> pages = pool.execute()"""
263
264        self.pool_type = pool_type
265        self.funcs = funcs
266        self.args = args

Integrates multi thread/process execution with ProgBar.

:params:

pool_type: Should be either thread or process.

funcs: List of functions to be executed.

args: A list of tuples where each tuple is the args to be used with each function.

Returns a list of whatever results the functions in funcs return.

>>> def my_func(page: int)->str:
>>>     return requests.get(f"https://somesite.com/pages/{page}").text
>>> pool = PoolBar("thread", [my_func for _ in range(10)], [(i,) for i in range(10)])
>>> pages = pool.execute()
def execute(self, *progbar_args, **progbar_kwargs) -> list[typing.Any]:
285    def execute(self, *progbar_args, **progbar_kwargs) -> list[Any]:
286        """Execute the supplied functions with their supplied arguments, if any.
287
288        `*progbar_args` and `**progbar_kwargs` can be any arguments the `ProgBar` constructor takes besides `total`."""
289        num_workers = len(self.funcs)
290        with ProgBar(num_workers, *progbar_args, **progbar_kwargs) as bar:
291            with self._get_executor()() as executor:
292                if self.args:
293                    workers = [
294                        executor.submit(func, *args)
295                        for func, args in zip(self.funcs, self.args)
296                    ]
297                else:
298                    workers = [executor.submit(func) for func in self.funcs]
299                while (
300                    num_complete := len([worker for worker in workers if worker.done()])
301                ) < num_workers:
302                    bar.display(f"{bar.runtime}", counter_override=num_complete)
303                    sleep(0.1)
304            bar.display(f"{bar.runtime}", counter_override=num_complete)
305        return [worker.result() for worker in workers]

Execute the supplied functions with their supplied arguments, if any.

*progbar_args and **progbar_kwargs can be any arguments the ProgBar constructor takes besides total.

class Spinner:
308class Spinner:
309    """Prints one of a sequence of characters in order everytime `display()` is called.
310
311    The `display` function writes the new character to the same line, overwriting the previous character.
312
313    The sequence will be cycled through indefinitely.
314
315    If used as a context manager, the last printed character will be cleared upon exiting.
316    """
317
318    def __init__(
319        self, sequence: list[str] = ["/", "-", "\\"], width_ratio: float = 0.25
320    ):
321        """
322        #### params:
323
324        `sequence`: Override the built in spin sequence.
325
326        `width_ratio`: The fractional amount of the terminal for characters to move across."""
327        self._base_sequence = sequence
328        self.width_ratio = width_ratio
329        self.sequence = self._base_sequence
330
331    def __enter__(self):
332        return self
333
334    def __exit__(self, *args, **kwargs):
335        clear()
336
337    @property
338    def width_ratio(self) -> float:
339        return self._width_ratio
340
341    @width_ratio.setter
342    def width_ratio(self, ratio: float):
343        self._width_ratio = ratio
344        self._update_width()
345
346    def _update_width(self):
347        self._current_terminal_width = get_terminal_size().columns
348        self._width = int((self._current_terminal_width - 1) * self.width_ratio)
349
350    @property
351    def sequence(self) -> list[Any]:
352        return self._sequence
353
354    @sequence.setter
355    def sequence(self, character_list: list[Any]):
356        self._sequence = [
357            ch.rjust(i + j)
358            for i in range(1, self._width, len(character_list))
359            for j, ch in enumerate(character_list)
360        ]
361        self._sequence += self._sequence[::-1]
362
363    def _get_next(self) -> str:
364        """Pop the first element of `self._sequence`, append it to the end, and return the element."""
365        ch = self.sequence.pop(0)
366        self.sequence.append(ch)
367        return ch
368
369    def display(self):
370        """Print the next character in the sequence."""
371        try:
372            if get_terminal_size().columns != self._current_terminal_width:
373                self._update_width()
374                self.sequence = self._base_sequence
375            print_in_place(self._get_next())
376        except OSError:
377            ...
378        except Exception as e:
379            raise e

Prints one of a sequence of characters in order everytime display() is called.

The display function writes the new character to the same line, overwriting the previous character.

The sequence will be cycled through indefinitely.

If used as a context manager, the last printed character will be cleared upon exiting.

Spinner(sequence: list[str] = ['/', '-', '\\'], width_ratio: float = 0.25)
318    def __init__(
319        self, sequence: list[str] = ["/", "-", "\\"], width_ratio: float = 0.25
320    ):
321        """
322        #### params:
323
324        `sequence`: Override the built in spin sequence.
325
326        `width_ratio`: The fractional amount of the terminal for characters to move across."""
327        self._base_sequence = sequence
328        self.width_ratio = width_ratio
329        self.sequence = self._base_sequence

params:

sequence: Override the built in spin sequence.

width_ratio: The fractional amount of the terminal for characters to move across.

def display(self):
369    def display(self):
370        """Print the next character in the sequence."""
371        try:
372            if get_terminal_size().columns != self._current_terminal_width:
373                self._update_width()
374                self.sequence = self._base_sequence
375            print_in_place(self._get_next())
376        except OSError:
377            ...
378        except Exception as e:
379            raise e

Print the next character in the sequence.