printbuddies.printbuddies

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

Erase the current line from the terminal.

def ticker(info: list[str], use_rich: bool = True, truncate: bool = True):
60def ticker(info: list[str], use_rich: bool = True, truncate: bool = True):
61    """Prints `info` to terminal with top and bottom padding so that previous text is not visible.
62
63    Similar visually to `print_in_place`, but for multiple lines.
64
65    #### :params:
66
67    `use_rich`: Use `rich` package to print `string`.
68
69    `truncate`: Truncate strings that are wider than the terminal window."""
70    if use_rich:
71        print = rich.print
72    try:
73        width = get_terminal_size().columns
74        info = [str(line)[: width - 1] if truncate else str(line) for line in info]
75        height = get_terminal_size().lines - len(info)
76        print("\n" * (height * 2), end="")
77        print(*info, sep="\n", end="")
78        print("\n" * (int((height) / 2)), end="")
79    except OSError:
80        ...
81    except Exception as e:
82        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.

:params:

use_rich: Use rich package to print string.

truncate: Truncate strings that are wider than the terminal window.

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

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

def get_bar(self):
195    def get_bar(self):
196        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:
198    def display(
199        self,
200        prefix: str = "",
201        suffix: str = "",
202        counter_override: float | None = None,
203        total_override: float | None = None,
204        return_object: Any | None = None,
205    ) -> Any:
206        """Writes the progress bar to the terminal.
207
208        #### :params:
209
210        `prefix`: String affixed to the front of the progress bar.
211
212        `suffix`: String appended to the end of the progress bar.
213
214        `counter_override`: When an externally incremented completion counter is needed.
215
216        `total_override`: When an externally controlled bar total is needed.
217
218        `return_object`: An object to be returned by display().
219        Allows `display()` to be called within a comprehension:
220
221        e.g.
222
223        >>> bar = ProgBar(10)
224        >>> def square(x: int | float)->int|float:
225        >>>     return x * x
226        >>> myList = [bar.display(return_object=square(i)) for i in range(10)]
227        >>> <progress bar gets displayed>
228        >>> myList
229        >>> [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]"""
230        if not self.timer.started:
231            self.timer.start()
232        if counter_override is not None:
233            self.counter = counter_override
234        if total_override:
235            self.total = total_override
236        # Don't wanna divide by 0 there, pal
237        while self.total <= 0:
238            self.total += 1
239        try:
240            if self.counter % self.update_frequency == 0:
241                self.prefix = prefix
242                self.suffix = suffix
243                self._prepare_bar()
244                self._trim_bar()
245                pad = " " * (self.terminal_width - len(self.bar))
246                width = get_terminal_size().columns
247                print(f"{self.bar}{pad}"[: width - 2], flush=True, end="\r")
248            if self.counter >= self.total:
249                self.timer.stop()
250                if not self.with_context:
251                    if self.clear_after_completion:
252                        clear()
253                    if self.new_line_after_completion:
254                        print()
255            self.counter += 1
256        except OSError:
257            ...
258        except Exception as e:
259            raise e
260        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:
263class PoolBar:
264    def __init__(
265        self,
266        pool_type: str,
267        funcs: list[Callable[[Any], Any]],
268        args: list[tuple[Any, ...]] | None = None,
269    ):
270        """Integrates multi thread/process execution with `ProgBar`.
271
272        #### :params:
273
274        `pool_type`: Should be either `thread` or `process`.
275
276        `funcs`: List of functions to be executed.
277
278        `args`: A list of tuples where each tuple is the args to be used with each function.
279
280        Returns a list of whatever results the functions in `funcs` return.
281
282        >>> def my_func(page: int)->str:
283        >>>     return requests.get(f"https://somesite.com/pages/{page}").text
284        >>> pool = PoolBar("thread", [my_func for _ in range(10)], [(i,) for i in range(10)])
285        >>> pages = pool.execute()"""
286
287        self.pool_type = pool_type
288        self.funcs = funcs
289        self.args = args
290
291    @property
292    def pool_type(self) -> str:
293        return self._pool_type
294
295    @pool_type.setter
296    def pool_type(self, pool_type: str):
297        if pool_type not in ["thread", "process"]:
298            raise ValueError(f"pool_type '{pool_type}' must be 'thread' or 'process'.")
299        self._pool_type = pool_type
300
301    def _get_executor(self) -> ThreadPoolExecutor | ProcessPoolExecutor:
302        match self.pool_type:
303            case "thread":
304                return ThreadPoolExecutor
305            case "process":
306                return ProcessPoolExecutor
307
308    def execute(self, *progbar_args, **progbar_kwargs) -> list[Any]:
309        """Execute the supplied functions with their supplied arguments, if any.
310
311        `*progbar_args` and `**progbar_kwargs` can be any arguments the `ProgBar` constructor takes besides `total`.
312        """
313        num_workers = len(self.funcs)
314        with ProgBar(num_workers, *progbar_args, **progbar_kwargs) as bar:
315            with self._get_executor()() as executor:
316                if self.args:
317                    workers = [
318                        executor.submit(func, *args)
319                        for func, args in zip(self.funcs, self.args)
320                    ]
321                else:
322                    workers = [executor.submit(func) for func in self.funcs]
323                while (
324                    num_complete := len([worker for worker in workers if worker.done()])
325                ) < num_workers:
326                    bar.display(f"{bar.runtime}", counter_override=num_complete)
327                    sleep(0.1)
328            bar.display(f"{bar.runtime}", counter_override=num_complete)
329        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)
264    def __init__(
265        self,
266        pool_type: str,
267        funcs: list[Callable[[Any], Any]],
268        args: list[tuple[Any, ...]] | None = None,
269    ):
270        """Integrates multi thread/process execution with `ProgBar`.
271
272        #### :params:
273
274        `pool_type`: Should be either `thread` or `process`.
275
276        `funcs`: List of functions to be executed.
277
278        `args`: A list of tuples where each tuple is the args to be used with each function.
279
280        Returns a list of whatever results the functions in `funcs` return.
281
282        >>> def my_func(page: int)->str:
283        >>>     return requests.get(f"https://somesite.com/pages/{page}").text
284        >>> pool = PoolBar("thread", [my_func for _ in range(10)], [(i,) for i in range(10)])
285        >>> pages = pool.execute()"""
286
287        self.pool_type = pool_type
288        self.funcs = funcs
289        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]:
308    def execute(self, *progbar_args, **progbar_kwargs) -> list[Any]:
309        """Execute the supplied functions with their supplied arguments, if any.
310
311        `*progbar_args` and `**progbar_kwargs` can be any arguments the `ProgBar` constructor takes besides `total`.
312        """
313        num_workers = len(self.funcs)
314        with ProgBar(num_workers, *progbar_args, **progbar_kwargs) as bar:
315            with self._get_executor()() as executor:
316                if self.args:
317                    workers = [
318                        executor.submit(func, *args)
319                        for func, args in zip(self.funcs, self.args)
320                    ]
321                else:
322                    workers = [executor.submit(func) for func in self.funcs]
323                while (
324                    num_complete := len([worker for worker in workers if worker.done()])
325                ) < num_workers:
326                    bar.display(f"{bar.runtime}", counter_override=num_complete)
327                    sleep(0.1)
328            bar.display(f"{bar.runtime}", counter_override=num_complete)
329        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:
332class Spinner:
333    """Prints one of a sequence of characters in order everytime `display()` is called.
334
335    The `display` function writes the new character to the same line, overwriting the previous character.
336
337    The sequence will be cycled through indefinitely.
338
339    If used as a context manager, the last printed character will be cleared upon exiting.
340    """
341
342    def __init__(
343        self, sequence: list[str] = ["/", "-", "\\"], width_ratio: float = 0.25
344    ):
345        """
346        #### params:
347
348        `sequence`: Override the built in spin sequence.
349
350        `width_ratio`: The fractional amount of the terminal for characters to move across.
351        """
352        self._base_sequence = sequence
353        self.width_ratio = width_ratio
354        self.sequence = self._base_sequence
355
356    def __enter__(self):
357        return self
358
359    def __exit__(self, *args, **kwargs):
360        clear()
361
362    @property
363    def width_ratio(self) -> float:
364        return self._width_ratio
365
366    @width_ratio.setter
367    def width_ratio(self, ratio: float):
368        self._width_ratio = ratio
369        self._update_width()
370
371    def _update_width(self):
372        self._current_terminal_width = get_terminal_size().columns
373        self._width = int((self._current_terminal_width - 1) * self.width_ratio)
374
375    @property
376    def sequence(self) -> list[Any]:
377        return self._sequence
378
379    @sequence.setter
380    def sequence(self, character_list: list[Any]):
381        self._sequence = [
382            ch.rjust(i + j)
383            for i in range(1, self._width, len(character_list))
384            for j, ch in enumerate(character_list)
385        ]
386        self._sequence += self._sequence[::-1]
387
388    def _get_next(self) -> str:
389        """Pop the first element of `self._sequence`, append it to the end, and return the element."""
390        ch = self.sequence.pop(0)
391        self.sequence.append(ch)
392        return ch
393
394    def display(self):
395        """Print the next character in the sequence."""
396        try:
397            if get_terminal_size().columns != self._current_terminal_width:
398                self._update_width()
399                self.sequence = self._base_sequence
400            print_in_place(self._get_next())
401        except OSError:
402            ...
403        except Exception as e:
404            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)
342    def __init__(
343        self, sequence: list[str] = ["/", "-", "\\"], width_ratio: float = 0.25
344    ):
345        """
346        #### params:
347
348        `sequence`: Override the built in spin sequence.
349
350        `width_ratio`: The fractional amount of the terminal for characters to move across.
351        """
352        self._base_sequence = sequence
353        self.width_ratio = width_ratio
354        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):
394    def display(self):
395        """Print the next character in the sequence."""
396        try:
397            if get_terminal_size().columns != self._current_terminal_width:
398                self._update_width()
399                self.sequence = self._base_sequence
400            print_in_place(self._get_next())
401        except OSError:
402            ...
403        except Exception as e:
404            raise e

Print the next character in the sequence.