everbar 0.1.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.
everbar/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ """everbar — a progress bar that works everywhere."""
2
+
3
+ from everbar._detect import detect_environment
4
+ from everbar._progress import Progress, set_default_backend
5
+
6
+ __all__ = ["Progress", "detect_environment", "set_default_backend"]
7
+ __version__ = "0.1.0"
everbar/_backends.py ADDED
@@ -0,0 +1,174 @@
1
+ """Progress backends.
2
+
3
+ Each backend implements the same minimal surface:
4
+
5
+ __enter__ / __exit__ — context-manager use
6
+ __iter__ — iterator-wrapper use
7
+ update(n=1) — manual advance
8
+
9
+ Backends are constructed lazily by ``Progress``. Optional third-party
10
+ dependencies (``tqdm``, ``marimo``) are imported only inside the backend
11
+ that needs them, so ``everbar`` itself has zero required deps.
12
+ """
13
+
14
+ import sys
15
+ import time
16
+ from collections.abc import Iterable, Iterator
17
+ from contextlib import nullcontext
18
+ from typing import Any, Self
19
+
20
+
21
+ def _len_or_none(obj: Any) -> int | None:
22
+ try:
23
+ return len(obj)
24
+ except (TypeError, AttributeError):
25
+ return None
26
+
27
+
28
+ class NullBackend(nullcontext):
29
+ """No-op backend used when ``disable=True``."""
30
+
31
+ def __init__(self, iterable: Iterable[Any] | None = None, **_: Any) -> None:
32
+ super().__init__()
33
+ self._iterable = iterable
34
+
35
+ def __iter__(self) -> Iterator[Any]:
36
+ return iter(self._iterable or ())
37
+
38
+ def update(self, n: int = 1) -> None: # noqa: ARG002 — protocol shape
39
+ return None
40
+
41
+
42
+ class FallbackBackend:
43
+ r"""Log-line backend for non-TTY environments.
44
+
45
+ Emits one line every ``min_interval`` seconds. Suitable for CI logs,
46
+ Kubernetes, CloudWatch — anywhere ``\r`` would just produce spam.
47
+ """
48
+
49
+ def __init__(
50
+ self,
51
+ iterable: Iterable[Any] | None = None,
52
+ total: int | None = None,
53
+ desc: str = "",
54
+ min_interval: float = 2.0,
55
+ stream: Any = None,
56
+ **_: Any,
57
+ ) -> None:
58
+ self._iterable = iterable
59
+ self._total = total if total is not None else _len_or_none(iterable)
60
+ self._desc = desc
61
+ self._min_interval = min_interval
62
+ self._stream = stream if stream is not None else sys.stderr
63
+ self._n = 0
64
+ self._t0 = 0.0
65
+ self._last_log = 0.0
66
+ self._entered = False
67
+
68
+ def __enter__(self) -> Self:
69
+ self._t0 = time.monotonic()
70
+ self._last_log = self._t0
71
+ self._entered = True
72
+ self._log(final=False)
73
+ return self
74
+
75
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
76
+ self._log(final=True)
77
+ self._entered = False
78
+
79
+ def __iter__(self) -> Iterator[Any]:
80
+ with self:
81
+ for item in self._iterable or ():
82
+ yield item
83
+ self.update(1)
84
+
85
+ def update(self, n: int = 1) -> None:
86
+ self._n += n
87
+ now = time.monotonic()
88
+ if now - self._last_log >= self._min_interval:
89
+ self._last_log = now
90
+ self._log(final=False)
91
+
92
+ def _log(self, *, final: bool) -> None:
93
+ elapsed = time.monotonic() - self._t0 if self._t0 else 0.0
94
+ if self._total:
95
+ pct = f"{100 * self._n / self._total:.0f}%"
96
+ total_str = str(self._total)
97
+ else:
98
+ pct = "?"
99
+ total_str = "?"
100
+ marker = "done" if final else "progress"
101
+ desc = f" {self._desc}" if self._desc else ""
102
+ line = (
103
+ f"[{marker}]{desc} {self._n}/{total_str}"
104
+ f" ({pct}) elapsed={elapsed:.1f}s"
105
+ )
106
+ print(line, file=self._stream, flush=True)
107
+
108
+
109
+ class TqdmBackend:
110
+ """Wraps ``tqdm``.
111
+
112
+ Aggregates rather than subclasses so Marimo's function-style monkey-patch
113
+ of ``tqdm_notebook`` (#4016) can't break us.
114
+ """
115
+
116
+ def __init__(
117
+ self,
118
+ iterable: Iterable[Any] | None = None,
119
+ total: int | None = None,
120
+ desc: str = "",
121
+ *,
122
+ notebook: bool = False,
123
+ **kwargs: Any,
124
+ ) -> None:
125
+ if notebook:
126
+ from tqdm.notebook import tqdm as _tqdm
127
+ else:
128
+ from tqdm import tqdm as _tqdm
129
+ self._inner = _tqdm(iterable, total=total, desc=desc, **kwargs)
130
+
131
+ def __enter__(self) -> Self:
132
+ self._inner.__enter__()
133
+ return self
134
+
135
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> Any:
136
+ return self._inner.__exit__(exc_type, exc_val, exc_tb)
137
+
138
+ def __iter__(self) -> Iterator[Any]:
139
+ return iter(self._inner)
140
+
141
+ def update(self, n: int = 1) -> None:
142
+ self._inner.update(n)
143
+
144
+
145
+ class MarimoBackend:
146
+ """Marimo-native bar via ``marimo.status.progress_bar``."""
147
+
148
+ def __init__(
149
+ self,
150
+ iterable: Iterable[Any] | None = None,
151
+ total: int | None = None,
152
+ desc: str = "",
153
+ **_: Any,
154
+ ) -> None:
155
+ import marimo as mo
156
+
157
+ self._inner = mo.status.progress_bar(
158
+ iterable,
159
+ total=total if total is not None else _len_or_none(iterable),
160
+ title=desc or None,
161
+ )
162
+
163
+ def __enter__(self) -> Self:
164
+ self._inner.__enter__()
165
+ return self
166
+
167
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> Any:
168
+ return self._inner.__exit__(exc_type, exc_val, exc_tb)
169
+
170
+ def __iter__(self) -> Iterator[Any]:
171
+ return iter(self._inner)
172
+
173
+ def update(self, n: int = 1) -> None:
174
+ self._inner.update(n)
everbar/_detect.py ADDED
@@ -0,0 +1,80 @@
1
+ """Runtime environment detection.
2
+
3
+ Heuristic — the caller can override via the ``backend`` argument on ``Progress``
4
+ or the ``EVERBAR_BACKEND`` environment variable.
5
+ """
6
+
7
+ import os
8
+ import sys
9
+ from typing import Literal
10
+
11
+ Environment = Literal[
12
+ "marimo",
13
+ "colab",
14
+ "kaggle",
15
+ "vscode_notebook",
16
+ "jupyter",
17
+ "jupyter_qt",
18
+ "spyder",
19
+ "databricks",
20
+ "pyodide",
21
+ "ipython_terminal",
22
+ "terminal",
23
+ "non_tty",
24
+ ]
25
+
26
+
27
+ def detect_environment() -> Environment:
28
+ """Return a string identifying the runtime environment."""
29
+ # 1. Marimo — official API
30
+ try:
31
+ import marimo # type: ignore
32
+
33
+ if marimo.running_in_notebook():
34
+ return "marimo"
35
+ except Exception:
36
+ pass
37
+
38
+ # 2. Pyodide / JupyterLite
39
+ if "pyodide" in sys.modules or sys.platform == "emscripten":
40
+ return "pyodide"
41
+
42
+ # 3. Databricks
43
+ if "DATABRICKS_RUNTIME_VERSION" in os.environ:
44
+ return "databricks"
45
+
46
+ # 4. IPython-based environments
47
+ try:
48
+ from IPython import get_ipython # type: ignore
49
+
50
+ ip = get_ipython()
51
+ except Exception:
52
+ ip = None
53
+
54
+ if ip is not None:
55
+ shell = ip.__class__.__name__
56
+ mods = sys.modules
57
+
58
+ if "google.colab" in mods:
59
+ return "colab"
60
+ if "kaggle_secrets" in mods or "KAGGLE_KERNEL_RUN_TYPE" in os.environ:
61
+ return "kaggle"
62
+ if "VSCODE_PID" in os.environ or (
63
+ hasattr(ip, "user_ns") and "__vsc_ipynb_file__" in ip.user_ns
64
+ ):
65
+ return "vscode_notebook"
66
+ if "spyder_kernels" in mods or "SPY_TESTING" in os.environ:
67
+ return "spyder"
68
+ if shell == "ZMQInteractiveShell":
69
+ return "jupyter"
70
+ if shell == "TerminalInteractiveShell":
71
+ return "ipython_terminal"
72
+
73
+ # 5. Plain Python
74
+ if sys.stdout is not None and hasattr(sys.stdout, "isatty"):
75
+ try:
76
+ if sys.stdout.isatty():
77
+ return "terminal"
78
+ except Exception:
79
+ pass
80
+ return "non_tty"
everbar/_progress.py ADDED
@@ -0,0 +1,98 @@
1
+ """Public ``Progress`` facade. Picks a backend and delegates."""
2
+
3
+ import os
4
+ from collections.abc import Iterable, Iterator
5
+ from typing import Any, Self
6
+
7
+ from everbar._detect import detect_environment
8
+
9
+ _DEFAULT_BACKEND: str | None = None
10
+
11
+ _NOTEBOOK_ENVS = {"jupyter", "colab", "kaggle", "vscode_notebook", "databricks"}
12
+ _TQDM_STD_ENVS = {"terminal", "ipython_terminal", "spyder", "jupyter_qt"}
13
+
14
+
15
+ def set_default_backend(name: str | None) -> None:
16
+ """Pin the backend globally. Pass ``None`` to restore auto-detection.
17
+
18
+ Valid names: ``"marimo"``, ``"jupyter"``, ``"colab"``, ``"kaggle"``,
19
+ ``"vscode_notebook"``, ``"jupyter_qt"``, ``"spyder"``, ``"databricks"``,
20
+ ``"ipython_terminal"``, ``"terminal"``, ``"pyodide"``, ``"non_tty"``.
21
+ """
22
+ global _DEFAULT_BACKEND # noqa: PLW0603 — module-level pin is the API
23
+ _DEFAULT_BACKEND = name
24
+
25
+
26
+ class Progress:
27
+ """A progress bar that adapts to its environment.
28
+
29
+ Iterator form:
30
+
31
+ for x in Progress(items, desc="Loading"):
32
+ work(x)
33
+
34
+ Context-manager form:
35
+
36
+ with Progress(total=100, desc="Steps") as bar:
37
+ for _ in range(100):
38
+ do_step()
39
+ bar.update(1)
40
+ """
41
+
42
+ def __init__(
43
+ self,
44
+ iterable: Iterable[Any] | None = None,
45
+ total: int | None = None,
46
+ desc: str = "",
47
+ backend: str | None = None,
48
+ *,
49
+ disable: bool = False,
50
+ **kwargs: Any,
51
+ ) -> None:
52
+ self._iterable = iterable
53
+ self._total = total
54
+ self._desc = desc
55
+ self._kwargs = kwargs
56
+
57
+ chosen = (
58
+ backend or os.environ.get("EVERBAR_BACKEND") or _DEFAULT_BACKEND
59
+ )
60
+ self._env: str = chosen or detect_environment()
61
+ self._impl = self._make_impl(disable=disable)
62
+
63
+ def _make_impl(self, *, disable: bool) -> Any:
64
+ from everbar import _backends
65
+
66
+ if disable:
67
+ return _backends.NullBackend(iterable=self._iterable)
68
+
69
+ common = {"total": self._total, "desc": self._desc, **self._kwargs}
70
+
71
+ try:
72
+ if self._env == "marimo":
73
+ return _backends.MarimoBackend(self._iterable, **common)
74
+ if self._env in _NOTEBOOK_ENVS:
75
+ return _backends.TqdmBackend(
76
+ self._iterable, notebook=True, **common
77
+ )
78
+ if self._env in _TQDM_STD_ENVS:
79
+ return _backends.TqdmBackend(
80
+ self._iterable, notebook=False, **common
81
+ )
82
+ except ImportError:
83
+ pass
84
+
85
+ return _backends.FallbackBackend(self._iterable, **common)
86
+
87
+ def __iter__(self) -> Iterator[Any]:
88
+ return iter(self._impl)
89
+
90
+ def __enter__(self) -> Self:
91
+ self._impl.__enter__()
92
+ return self
93
+
94
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> Any:
95
+ return self._impl.__exit__(exc_type, exc_val, exc_tb)
96
+
97
+ def update(self, n: int = 1) -> None:
98
+ self._impl.update(n)
@@ -0,0 +1,83 @@
1
+ Metadata-Version: 2.4
2
+ Name: everbar
3
+ Version: 0.1.0
4
+ Summary: A progress bar that works everywhere — terminal, Jupyter, VS Code, Colab, Marimo.
5
+ Project-URL: Homepage, https://github.com/mluttikh/everbar
6
+ Project-URL: Issues, https://github.com/mluttikh/everbar/issues
7
+ License-Expression: MIT
8
+ Keywords: jupyter,marimo,notebook,progress,progressbar,tqdm
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Software Development :: Libraries
20
+ Classifier: Topic :: Terminals
21
+ Requires-Python: >=3.11
22
+ Provides-Extra: all
23
+ Requires-Dist: ipywidgets>=8.0; extra == 'all'
24
+ Requires-Dist: marimo>=0.10; extra == 'all'
25
+ Requires-Dist: rich>=13.0; extra == 'all'
26
+ Requires-Dist: tqdm>=4.65; extra == 'all'
27
+ Provides-Extra: marimo
28
+ Requires-Dist: marimo>=0.10; extra == 'marimo'
29
+ Provides-Extra: notebook
30
+ Requires-Dist: ipywidgets>=8.0; extra == 'notebook'
31
+ Requires-Dist: tqdm>=4.65; extra == 'notebook'
32
+ Provides-Extra: rich
33
+ Requires-Dist: rich>=13.0; extra == 'rich'
34
+ Provides-Extra: tqdm
35
+ Requires-Dist: tqdm>=4.65; extra == 'tqdm'
36
+ Description-Content-Type: text/markdown
37
+
38
+ # everbar
39
+
40
+ A progress bar that works **everywhere** — terminal, Jupyter, JupyterLab, VS Code notebooks, Google Colab, Marimo, Pyodide, and CI logs. One API, the right backend per environment.
41
+
42
+ > Status: 0.1.0 — alpha. API may shift.
43
+
44
+ ## Install
45
+
46
+ ```bash
47
+ pip install everbar # core only; uses text fallback if nothing else is installed
48
+ pip install "everbar[tqdm]" # terminal + Jupyter via tqdm
49
+ pip install "everbar[all]" # everything (tqdm, rich, ipywidgets, marimo)
50
+ ```
51
+
52
+ ## Use
53
+
54
+ ```python
55
+ from everbar import Progress
56
+
57
+ for x in Progress(items, desc="Loading"):
58
+ work(x)
59
+
60
+ with Progress(total=100, desc="Steps") as bar:
61
+ for _ in range(100):
62
+ do_step()
63
+ bar.update(1)
64
+ ```
65
+
66
+ ## Overrides
67
+
68
+ ```python
69
+ Progress(items, backend="terminal") # per-call
70
+ ```
71
+
72
+ ```bash
73
+ EVERBAR_BACKEND=terminal python script.py # env var
74
+ ```
75
+
76
+ ```python
77
+ import everbar
78
+ everbar.set_default_backend("terminal") # module-wide
79
+ ```
80
+
81
+ ## How it picks a backend
82
+
83
+ `everbar.detect_environment()` returns one of: `marimo`, `colab`, `kaggle`, `vscode_notebook`, `jupyter`, `jupyter_qt`, `spyder`, `databricks`, `pyodide`, `ipython_terminal`, `terminal`, `non_tty`. Each maps to a backend, with graceful fallback to a log-line text mode when nothing better is available.
@@ -0,0 +1,7 @@
1
+ everbar/__init__.py,sha256=ohr1mGbK02QG2Jnfwv2vJVEuGuTf9RGQw8fenN4Q_Iw,255
2
+ everbar/_backends.py,sha256=8fOuAfVtd2qSfMEy3G4dktr1AwgDwT-fIi8SNacw_qc,5025
3
+ everbar/_detect.py,sha256=qHqnDmmD6Y7gxwGmFx9zyWSiPi-K2Pmb9cs4WO_kWwM,2041
4
+ everbar/_progress.py,sha256=H5wsugx7lbZhXGQljAETu01rCApZbWkjFr6nMlQRlBQ,2986
5
+ everbar-0.1.0.dist-info/METADATA,sha256=AUQBbVkDSTUgdIF0hX5tyRM9N-yzvAZ5jk8uQHV4QYs,2795
6
+ everbar-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
7
+ everbar-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any