tldm 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
tldm/__init__.py ADDED
@@ -0,0 +1,11 @@
1
+ from importlib.metadata import version
2
+
3
+ from ._monitor import TMonitor as TMonitor
4
+ from .aliases import tenumerate as tenumerate
5
+ from .aliases import tmap as tmap
6
+ from .aliases import tproduct as tproduct
7
+ from .aliases import trange as trange
8
+ from .aliases import tzip as tzip
9
+ from .std import tldm as tldm
10
+
11
+ __version__ = version("tldm")
tldm/_monitor.py ADDED
@@ -0,0 +1,115 @@
1
+ import atexit
2
+ from collections.abc import Callable
3
+ from threading import Event, Thread, current_thread
4
+ from time import time
5
+ from typing import TYPE_CHECKING
6
+ from warnings import warn
7
+
8
+ if TYPE_CHECKING:
9
+ from tldm.std import tldm
10
+
11
+
12
+ class TldmSynchronisationWarning(RuntimeWarning):
13
+ """tldm multi-thread/-process errors which may cause incorrect nesting
14
+ but otherwise no adverse effects"""
15
+
16
+ pass
17
+
18
+
19
+ class TMonitor(Thread):
20
+ """
21
+ Monitoring thread for tldm bars.
22
+ Monitors if tldm bars are taking too much time to display
23
+ and readjusts miniters automatically if necessary.
24
+
25
+ Parameters
26
+ ----------
27
+ tldm_cls : class
28
+ tldm class to use (can be core tldm or a submodule).
29
+ sleep_interval : float
30
+ Time to sleep between monitoring checks.
31
+ """
32
+
33
+ _test: dict[str, Event] = {} # internal vars for unit testing
34
+
35
+ name: str
36
+ daemon: bool
37
+ woken: float
38
+ tldm_cls: type["tldm"]
39
+ sleep_interval: float
40
+ _time: Callable[[], float]
41
+ was_killed: Event
42
+
43
+ def __init__(self, tldm_cls: type["tldm"], sleep_interval: float) -> None:
44
+ Thread.__init__(self)
45
+ self.name = "tldm_monitor"
46
+ self.daemon = True # kill thread when main killed (KeyboardInterrupt)
47
+ self.woken = 0 # last time woken up, to sync with monitor
48
+ self.tldm_cls = tldm_cls
49
+ self.sleep_interval = sleep_interval
50
+ self._time = self._test.get("time", time) # type: ignore[assignment]
51
+ self.was_killed = self._test.get("Event", Event)() # type: ignore[operator]
52
+ atexit.register(self.exit)
53
+ self.start()
54
+
55
+ def exit(self) -> bool:
56
+ self.was_killed.set()
57
+ if self is not current_thread():
58
+ self.join()
59
+ return self.report()
60
+
61
+ def get_instances(self) -> list["tldm"]:
62
+ # returns a copy of started `tldm_cls` instances
63
+ return [
64
+ i
65
+ for i in self.tldm_cls._instances.copy()
66
+ # Avoid race by checking that the instance started
67
+ if hasattr(i, "start_t")
68
+ ]
69
+
70
+ def run(self) -> None:
71
+ cur_t = self._time()
72
+ while True:
73
+ # After processing and before sleeping, notify that we woke
74
+ # Need to be done just before sleeping
75
+ self.woken = cur_t
76
+ # Sleep some time...
77
+ self.was_killed.wait(self.sleep_interval)
78
+ # Quit if killed
79
+ if self.was_killed.is_set():
80
+ return
81
+ # Then monitor!
82
+ # Acquire lock (to access _instances)
83
+ with self.tldm_cls.get_lock():
84
+ cur_t = self._time()
85
+ # Check tldm instances are waiting too long to print
86
+ instances = self.get_instances()
87
+ for instance in instances:
88
+ # Check event in loop to reduce blocking time on exit
89
+ if self.was_killed.is_set():
90
+ return
91
+ # Only if mininterval > 1 (else iterations are just slow)
92
+ # and last refresh exceeded maxinterval
93
+ if (
94
+ instance.miniters > 1
95
+ and (cur_t - instance.last_print_t) >= instance.maxinterval
96
+ ):
97
+ # force bypassing miniters on next iteration
98
+ # (dynamic_miniters adjusts mininterval automatically)
99
+ instance.miniters = 1
100
+ # Refresh now! (works only for manual tldm)
101
+ instance.refresh(nolock=True)
102
+ # Remove accidental long-lived strong reference
103
+ del instance
104
+ if instances != self.get_instances(): # pragma: nocover
105
+ warn(
106
+ "Set changed size during iteration"
107
+ + " (see https://github.com/tldm/tldm/issues/481)",
108
+ TldmSynchronisationWarning,
109
+ stacklevel=2,
110
+ )
111
+ # Remove accidental long-lived strong references
112
+ del instances
113
+
114
+ def report(self) -> bool:
115
+ return not self.was_killed.is_set()
tldm/aliases.py ADDED
@@ -0,0 +1,86 @@
1
+ import itertools
2
+ from collections.abc import Callable, Iterable, Iterator
3
+ from operator import length_hint
4
+ from typing import Any, TypeVar
5
+
6
+ from .std import tldm
7
+
8
+ T = TypeVar("T")
9
+ R = TypeVar("R")
10
+
11
+
12
+ def tenumerate(
13
+ iterable: Iterable[T],
14
+ start: int = 0,
15
+ total: int | float | None = None,
16
+ tldm_class: type[tldm] = tldm,
17
+ **tldm_kwargs: Any,
18
+ ) -> Iterator[tuple[int, T]]:
19
+ """
20
+ Equivalent of builtin `enumerate`.
21
+
22
+ Parameters
23
+ ----------
24
+ tldm_class : [default: tldm.std.tldm].
25
+ """
26
+ return enumerate(tldm_class(iterable, total=total, **tldm_kwargs), start)
27
+
28
+
29
+ def tzip(
30
+ iter1: Iterable[T], *iter2plus: Iterable[Any], **tldm_kwargs: Any
31
+ ) -> Iterator[tuple[T, ...]]:
32
+ """
33
+ Equivalent of builtin `zip`.
34
+
35
+ Parameters
36
+ ----------
37
+ tldm_class : [default: tldm.std.tldm].
38
+ """
39
+ kwargs = tldm_kwargs.copy()
40
+ tldm_class = kwargs.pop("tldm_class", tldm)
41
+ yield from zip(tldm_class(iter1, **kwargs), *iter2plus)
42
+
43
+
44
+ def tmap(function: Callable[..., R], *sequences: Iterable[Any], **tldm_kwargs: Any) -> Iterator[R]:
45
+ """
46
+ Equivalent of builtin `map`.
47
+
48
+ Parameters
49
+ ----------
50
+ tldm_class : [default: tldm.std.tldm].
51
+ """
52
+ for i in tzip(*sequences, **tldm_kwargs):
53
+ yield function(*i)
54
+
55
+
56
+ def tproduct(*iterables: Iterable[T], **tldm_kwargs: Any) -> Iterator[tuple[T, ...]]:
57
+ """
58
+ Equivalent of `itertools.product`.
59
+
60
+ Parameters
61
+ ----------
62
+ tldm_class : [default: tldm.std.tldm].
63
+ """
64
+ kwargs = tldm_kwargs.copy()
65
+ repeat = kwargs.pop("repeat", 1)
66
+ tldm_class = kwargs.pop("tldm_class", tldm)
67
+ try:
68
+ lens = list(map(length_hint, iterables))
69
+ except TypeError:
70
+ total = None
71
+ else:
72
+ total = 1
73
+ for i in lens:
74
+ total *= i
75
+ total = total**repeat
76
+ kwargs.setdefault("total", total)
77
+ with tldm_class(**kwargs) as t:
78
+ it = itertools.product(*iterables, repeat=repeat)
79
+ for val in it:
80
+ yield val
81
+ t.update()
82
+
83
+
84
+ def trange(*args: int, **kwargs: Any) -> tldm:
85
+ """Shortcut for tldm(range(*args), **kwargs)."""
86
+ return tldm(range(*args), **kwargs)
@@ -0,0 +1 @@
1
+ from .pandas import tldm_pandas as tldm_pandas
@@ -0,0 +1,100 @@
1
+ """
2
+ Asynchronous progressbar decorator for iterators.
3
+ """
4
+
5
+ import asyncio
6
+
7
+ from ..std import tldm as std_tldm
8
+
9
+
10
+ class tldm_asyncio(std_tldm):
11
+ """
12
+ Asynchronous-friendly version of tldm.
13
+ """
14
+
15
+ def __init__(self, iterable=None, *args, **kwargs):
16
+ super().__init__(iterable, *args, **kwargs)
17
+ self.iterable_awaitable = False
18
+ if iterable is not None:
19
+ if hasattr(iterable, "__anext__"):
20
+ self.iterable_next = iterable.__anext__
21
+ self.iterable_awaitable = True
22
+ elif hasattr(iterable, "__next__"):
23
+ self.iterable_next = iterable.__next__
24
+
25
+ def __aiter__(self):
26
+ return self
27
+
28
+ # def __del__(self):
29
+ # self.close()
30
+ # if len(tldm_asyncio._instances) == 0:
31
+ # if hasattr(tldm_asyncio, "_lock"):
32
+ # del tldm_asyncio._lock
33
+ # if hasattr(tldm_asyncio, "monitor") and tldm_asyncio.monitor is not None:
34
+ # tldm_asyncio.monitor.exit()
35
+
36
+ async def __anext__(self):
37
+ try:
38
+ if self.iterable_awaitable:
39
+ res = await self.iterable_next()
40
+ else:
41
+ if not hasattr(self, "iterable_iterator"):
42
+ self.iterable_iterator = iter(self.iterable)
43
+ self.iterable_next = self.iterable_iterator.__next__
44
+ res = self.iterable_next()
45
+ self.update()
46
+ return res
47
+ except StopIteration:
48
+ self.close()
49
+ raise StopAsyncIteration from None
50
+ except BaseException:
51
+ self.close()
52
+ raise
53
+
54
+ def send(self, *args, **kwargs):
55
+ return self.iterable.send(*args, **kwargs)
56
+
57
+ @classmethod
58
+ def as_completed(cls, fs, *, loop=None, timeout=None, total=None, **tldm_kwargs):
59
+ """
60
+ Wrapper for `asyncio.as_completed`.
61
+ """
62
+ if total is None:
63
+ total = len(fs)
64
+ yield from cls(
65
+ asyncio.as_completed(fs, timeout=timeout),
66
+ total=total,
67
+ **tldm_kwargs,
68
+ )
69
+
70
+ @classmethod
71
+ async def gather(
72
+ cls,
73
+ *fs,
74
+ loop=None,
75
+ timeout=None,
76
+ total=None,
77
+ return_exceptions=False,
78
+ **tldm_kwargs,
79
+ ):
80
+ """
81
+ Wrapper for `asyncio.gather`.
82
+ """
83
+ if total is None:
84
+ total = len(fs)
85
+
86
+ async def wrap_awaitable(i, f):
87
+ try:
88
+ return i, await f
89
+ except Exception as e:
90
+ if return_exceptions:
91
+ return i, e
92
+ raise
93
+
94
+ async def aiter_as_completed():
95
+ ifs = [wrap_awaitable(i, f) for i, f in enumerate(fs)]
96
+ for r in asyncio.as_completed(ifs, timeout=timeout):
97
+ yield await r
98
+
99
+ res = [f async for f in cls(aiter_as_completed(), total=total, **tldm_kwargs)]
100
+ return [i for _, i in sorted(res)]
@@ -0,0 +1,125 @@
1
+ """
2
+ Thin wrappers around `concurrent.futures`.
3
+ """
4
+
5
+ from contextlib import contextmanager
6
+ from operator import length_hint
7
+ from os import cpu_count
8
+
9
+ from ..std import tldm as std_tldm
10
+ from ..utils import TldmWarning
11
+
12
+
13
+ @contextmanager
14
+ def ensure_lock(tldm_class, lock_name=""):
15
+ """get (create if necessary) and then restore `tldm_class`'s lock"""
16
+ old_lock = getattr(tldm_class, "_lock", None) # don't create a new lock
17
+ lock = old_lock or tldm_class.get_lock() # maybe create a new lock
18
+ lock = getattr(lock, lock_name, lock) # maybe subtype
19
+ tldm_class.set_lock(lock)
20
+ yield lock
21
+ if old_lock is None:
22
+ del tldm_class._lock
23
+ else:
24
+ tldm_class.set_lock(old_lock)
25
+
26
+
27
+ def _executor_map(PoolExecutor, fn, *iterables, **tldm_kwargs):
28
+ """
29
+ Implementation of `thread_map` and `process_map`.
30
+
31
+ Parameters
32
+ ----------
33
+ tldm_class : [default: tldm.auto.tldm].
34
+ max_workers : [default: min(32, cpu_count() + 4)].
35
+ timeout : [default: None].
36
+ chunksize : [default: 1].
37
+ lock_name : [default: "":str].
38
+ """
39
+ kwargs = tldm_kwargs.copy()
40
+ if "total" not in kwargs:
41
+ kwargs["total"] = length_hint(iterables[0])
42
+ tldm_class = kwargs.pop("tldm_class", std_tldm)
43
+ max_workers = kwargs.pop("max_workers", min(32, cpu_count() + 4))
44
+ timeout = kwargs.pop("timeout", None)
45
+ chunksize = kwargs.pop("chunksize", 1)
46
+ lock_name = kwargs.pop("lock_name", "")
47
+ with ensure_lock(tldm_class, lock_name=lock_name) as lk:
48
+ # share lock in case workers are already using `tldm`
49
+ with PoolExecutor(
50
+ max_workers=max_workers, initializer=tldm_class.set_lock, initargs=(lk,)
51
+ ) as ex:
52
+ return list(
53
+ tldm_class(
54
+ ex.map(fn, *iterables, timeout=timeout, chunksize=chunksize),
55
+ **kwargs,
56
+ )
57
+ )
58
+
59
+
60
+ def thread_map(fn, *iterables, **tldm_kwargs):
61
+ """
62
+ Equivalent of `list(map(fn, *iterables))`
63
+ driven by `concurrent.futures.ThreadPoolExecutor`.
64
+
65
+ Parameters
66
+ ----------
67
+ tldm_class : optional
68
+ `tldm` class to use for bars [default: tldm.auto.tldm].
69
+ max_workers : int, optional
70
+ Maximum number of workers to spawn; passed to
71
+ `concurrent.futures.ThreadPoolExecutor.__init__`.
72
+ [default: min(32, cpu_count() + 4)].
73
+ timeout : int or float, optional
74
+ The iterator raises a TimeoutError if __next()__ is called and the
75
+ result isn't available within the timeout specified from the
76
+ original call to thread_map. [default: None].
77
+ """
78
+ from concurrent.futures import ThreadPoolExecutor
79
+
80
+ return _executor_map(ThreadPoolExecutor, fn, *iterables, **tldm_kwargs)
81
+
82
+
83
+ def process_map(fn, *iterables, **tldm_kwargs):
84
+ """
85
+ Equivalent of `list(map(fn, *iterables))`
86
+ driven by `concurrent.futures.ProcessPoolExecutor`.
87
+
88
+ Parameters
89
+ ----------
90
+ tldm_class : optional
91
+ `tldm` class to use for bars [default: tldm.auto.tldm].
92
+ max_workers : int, optional
93
+ Maximum number of workers to spawn; passed to
94
+ `concurrent.futures.ProcessPoolExecutor.__init__`.
95
+ [default: min(32, cpu_count() + 4)].
96
+ timeout : int or float, optional
97
+ The iterator raises a TimeoutError if __next()__ is called and the
98
+ result isn't available within the timeout specified from the
99
+ original call to process_map. [default: None].
100
+ chunksize : int, optional
101
+ Size of chunks sent to worker processes; passed to
102
+ `concurrent.futures.ProcessPoolExecutor.map`. [default: 1].
103
+ lock_name : str, optional
104
+ Member of `tldm_class.get_lock()` to use [default: mp_lock].
105
+ """
106
+ from concurrent.futures import ProcessPoolExecutor
107
+
108
+ if iterables and "chunksize" not in tldm_kwargs:
109
+ # default `chunksize=1` has poor performance for large iterables
110
+ # (most time spent dispatching items to workers).
111
+ longest_iterable_len = max(map(length_hint, iterables))
112
+ if longest_iterable_len > 1000:
113
+ from warnings import warn
114
+
115
+ warn(
116
+ "Iterable length %d > 1000 but `chunksize` is not set."
117
+ " This may seriously degrade multiprocess performance."
118
+ " Set `chunksize=1` or more." % longest_iterable_len,
119
+ TldmWarning,
120
+ stacklevel=2,
121
+ )
122
+ if "lock_name" not in tldm_kwargs:
123
+ tldm_kwargs = tldm_kwargs.copy()
124
+ tldm_kwargs["lock_name"] = "mp_lock"
125
+ return _executor_map(ProcessPoolExecutor, fn, *iterables, **tldm_kwargs)
@@ -0,0 +1,179 @@
1
+ """
2
+ Registration for `tldm` to provide `pandas` progress indicators.
3
+ """
4
+
5
+ import contextlib
6
+ from typing import Any
7
+
8
+ from ..std import tldm as std_tldm
9
+
10
+
11
+ def tldm_pandas(**tldm_kwargs: dict[str, Any]) -> None:
12
+ """
13
+ Registers the current `tldm` class with
14
+ pandas.core.
15
+ ( frame.DataFrame
16
+ | series.Series
17
+ | groupby.(generic.)DataFrameGroupBy
18
+ | groupby.(generic.)SeriesGroupBy
19
+ ).progress_apply
20
+
21
+ A new instance will be created every time `progress_apply` is called,
22
+ and each instance will automatically `close()` upon completion.
23
+
24
+ Parameters
25
+ ----------
26
+ tldm_kwargs : arguments for the tldm instance
27
+
28
+ Examples
29
+ --------
30
+ >>> import pandas as pd
31
+ >>> import numpy as np
32
+ >>> from tldm import tldm, tldm_pandas
33
+ >>> from tldm.gui import tldm as tldm_gui
34
+ >>>
35
+ >>> df = pd.DataFrame(np.random.randint(0, 100, (100000, 6)))
36
+ >>> tldm_pandas(ncols=50) # can use tldm_gui, optional kwargs, etc
37
+ >>> # Now you can use `progress_apply` instead of `apply`
38
+ >>> df.groupby(0).progress_apply(lambda x: x**2)
39
+
40
+ References
41
+ ----------
42
+ <https://stackoverflow.com/questions/18603270/\
43
+ progress-indicator-during-pandas-operations-python>
44
+ """
45
+ from warnings import catch_warnings, simplefilter
46
+
47
+ from pandas.core.frame import DataFrame
48
+ from pandas.core.series import Series
49
+
50
+ try:
51
+ with catch_warnings():
52
+ simplefilter("ignore", category=FutureWarning)
53
+ from pandas import Panel
54
+ except ImportError: # pandas>=1.2.0
55
+ Panel = None
56
+ Rolling, Expanding = None, None
57
+ try: # pandas>=1.0.0
58
+ from pandas.core.window.rolling import _Rolling_and_Expanding
59
+ except ImportError:
60
+ try: # pandas>=0.18.0
61
+ from pandas.core.window import _Rolling_and_Expanding
62
+ except ImportError: # pandas>=1.2.0
63
+ try: # pandas>=1.2.0
64
+ from pandas.core.window.expanding import Expanding
65
+ from pandas.core.window.rolling import Rolling
66
+
67
+ _Rolling_and_Expanding = Rolling, Expanding
68
+ except ImportError: # pragma: no cover
69
+ _Rolling_and_Expanding = None
70
+ try: # pandas>=0.25.0
71
+ from pandas.core.groupby.generic import (
72
+ DataFrameGroupBy,
73
+ SeriesGroupBy, # , NDFrameGroupBy
74
+ )
75
+ except ImportError: # pragma: no cover
76
+ try: # pandas>=0.23.0
77
+ from pandas.core.groupby.groupby import DataFrameGroupBy, SeriesGroupBy
78
+ except ImportError:
79
+ from pandas.core.groupby import DataFrameGroupBy, SeriesGroupBy
80
+ try: # pandas>=0.23.0
81
+ from pandas.core.groupby.groupby import GroupBy
82
+ except ImportError: # pragma: no cover
83
+ from pandas.core.groupby import GroupBy
84
+
85
+ try: # pandas>=0.23.0
86
+ from pandas.core.groupby.groupby import PanelGroupBy
87
+ except ImportError:
88
+ try:
89
+ from pandas.core.groupby import PanelGroupBy
90
+ except ImportError: # pandas>=0.25.0
91
+ PanelGroupBy = None
92
+
93
+ tldm_kwargs = tldm_kwargs.copy()
94
+
95
+ def inner_generator(df_function="apply"):
96
+ def inner(df, func, **kwargs):
97
+ """
98
+ Parameters
99
+ ----------
100
+ df : (DataFrame|Series)[GroupBy]
101
+ Data (may be grouped).
102
+ func : function
103
+ To be applied on the (grouped) data.
104
+ **kwargs : optional
105
+ Transmitted to `df.apply()`.
106
+ """
107
+
108
+ # Precompute total iterations
109
+ total = tldm_kwargs.pop("total", getattr(df, "ngroups", None))
110
+ if total is None: # not grouped
111
+ if df_function == "applymap":
112
+ total = df.size
113
+ elif isinstance(df, Series):
114
+ total = len(df)
115
+ elif _Rolling_and_Expanding is None or not isinstance(df, _Rolling_and_Expanding):
116
+ # DataFrame or Panel
117
+ axis = kwargs.get("axis", 0)
118
+ if axis == "index":
119
+ axis = 0
120
+ elif axis == "columns":
121
+ axis = 1
122
+ # when axis=0, total is shape[axis1]
123
+ total = df.size // df.shape[axis]
124
+
125
+ # Init bar
126
+ t = std_tldm(total=total, **tldm_kwargs)
127
+
128
+ try: # pandas>=1.3.0
129
+ from pandas.core.common import is_builtin_func
130
+ except ImportError:
131
+ is_builtin_func = df._is_builtin_func
132
+ with contextlib.suppress(TypeError):
133
+ func = is_builtin_func(func)
134
+
135
+ # Define bar updating wrapper
136
+ def wrapper(*args, **kwargs):
137
+ # update tbar correctly
138
+ # it seems `pandas apply` calls `func` twice
139
+ # on the first column/row to decide whether it can
140
+ # take a fast or slow code path; so stop when t.total==t.n
141
+ t.update(n=1 if not t.total or t.n < t.total else 0)
142
+ return func(*args, **kwargs)
143
+
144
+ # Apply the provided function (in **kwargs)
145
+ # on the df using our wrapper (which provides bar updating)
146
+ try:
147
+ return getattr(df, df_function)(wrapper, **kwargs)
148
+ finally:
149
+ t.close()
150
+
151
+ return inner
152
+
153
+ # Monkeypatch pandas to provide easy methods
154
+ # Enable custom tldm progress in pandas!
155
+ Series.progress_apply = inner_generator()
156
+ SeriesGroupBy.progress_apply = inner_generator()
157
+ Series.progress_map = inner_generator("map")
158
+ SeriesGroupBy.progress_map = inner_generator("map")
159
+
160
+ DataFrame.progress_apply = inner_generator()
161
+ DataFrameGroupBy.progress_apply = inner_generator()
162
+ DataFrame.progress_applymap = inner_generator("applymap")
163
+ DataFrame.progress_map = inner_generator("map")
164
+ DataFrameGroupBy.progress_map = inner_generator("map")
165
+
166
+ if Panel is not None:
167
+ Panel.progress_apply = inner_generator()
168
+ if PanelGroupBy is not None:
169
+ PanelGroupBy.progress_apply = inner_generator()
170
+
171
+ GroupBy.progress_apply = inner_generator()
172
+ GroupBy.progress_aggregate = inner_generator("aggregate")
173
+ GroupBy.progress_transform = inner_generator("transform")
174
+
175
+ if Rolling is not None and Expanding is not None:
176
+ Rolling.progress_apply = inner_generator()
177
+ Expanding.progress_apply = inner_generator()
178
+ elif _Rolling_and_Expanding is not None:
179
+ _Rolling_and_Expanding.progress_apply = inner_generator()