everbar 0.1.0__tar.gz
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-0.1.0/.github/workflows/ci.yml +58 -0
- everbar-0.1.0/.github/workflows/release.yml +35 -0
- everbar-0.1.0/.gitignore +124 -0
- everbar-0.1.0/PKG-INFO +83 -0
- everbar-0.1.0/README.md +46 -0
- everbar-0.1.0/pyproject.toml +125 -0
- everbar-0.1.0/src/everbar/__init__.py +7 -0
- everbar-0.1.0/src/everbar/_backends.py +174 -0
- everbar-0.1.0/src/everbar/_detect.py +80 -0
- everbar-0.1.0/src/everbar/_progress.py +98 -0
- everbar-0.1.0/tests/__init__.py +0 -0
- everbar-0.1.0/tests/test_detect.py +27 -0
- everbar-0.1.0/tests/test_progress.py +61 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
concurrency:
|
|
10
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
11
|
+
cancel-in-progress: true
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
lint:
|
|
15
|
+
name: Lint (ruff)
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
# setup-uv doesn't publish a floating major tag — must pin to a
|
|
21
|
+
# specific version. Bump as new patches/minors land.
|
|
22
|
+
- name: Install uv
|
|
23
|
+
uses: astral-sh/setup-uv@v8.1.0
|
|
24
|
+
with:
|
|
25
|
+
python-version: "3.13"
|
|
26
|
+
enable-cache: true
|
|
27
|
+
|
|
28
|
+
- name: Sync dev environment
|
|
29
|
+
run: uv sync --all-extras --group dev
|
|
30
|
+
|
|
31
|
+
- name: ruff check
|
|
32
|
+
run: uv run ruff check .
|
|
33
|
+
|
|
34
|
+
- name: ruff format --check
|
|
35
|
+
run: uv run ruff format --check .
|
|
36
|
+
|
|
37
|
+
test:
|
|
38
|
+
name: Test (Python ${{ matrix.python-version }} on ${{ matrix.os }})
|
|
39
|
+
runs-on: ${{ matrix.os }}
|
|
40
|
+
strategy:
|
|
41
|
+
fail-fast: false
|
|
42
|
+
matrix:
|
|
43
|
+
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
44
|
+
python-version: ["3.11", "3.12", "3.13", "3.14"]
|
|
45
|
+
steps:
|
|
46
|
+
- uses: actions/checkout@v4
|
|
47
|
+
|
|
48
|
+
- name: Install uv
|
|
49
|
+
uses: astral-sh/setup-uv@v8.1.0
|
|
50
|
+
with:
|
|
51
|
+
python-version: ${{ matrix.python-version }}
|
|
52
|
+
enable-cache: true
|
|
53
|
+
|
|
54
|
+
- name: Sync environment
|
|
55
|
+
run: uv sync --all-extras --group dev
|
|
56
|
+
|
|
57
|
+
- name: Run tests
|
|
58
|
+
run: uv run pytest -v
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: write # required to create the GitHub Release
|
|
10
|
+
id-token: write # required for PyPI trusted publishing (OIDC)
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
release:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
environment: pypi # gates the job behind manual approval
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v5
|
|
18
|
+
|
|
19
|
+
# setup-uv doesn't publish a floating major tag — must pin to a
|
|
20
|
+
# specific version. Bump as new patches/minors land.
|
|
21
|
+
- uses: astral-sh/setup-uv@v8.1.0
|
|
22
|
+
with:
|
|
23
|
+
python-version: "3.13"
|
|
24
|
+
|
|
25
|
+
- name: Build wheel and sdist
|
|
26
|
+
run: uv build
|
|
27
|
+
|
|
28
|
+
- name: Publish to PyPI
|
|
29
|
+
run: uv publish --trusted-publishing automatic
|
|
30
|
+
|
|
31
|
+
- name: Create GitHub Release
|
|
32
|
+
uses: softprops/action-gh-release@v2
|
|
33
|
+
with:
|
|
34
|
+
files: dist/*
|
|
35
|
+
generate_release_notes: true
|
everbar-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
*.manifest
|
|
31
|
+
*.spec
|
|
32
|
+
|
|
33
|
+
# Installer logs
|
|
34
|
+
pip-log.txt
|
|
35
|
+
pip-delete-this-directory.txt
|
|
36
|
+
|
|
37
|
+
# Unit test / coverage reports
|
|
38
|
+
htmlcov/
|
|
39
|
+
.tox/
|
|
40
|
+
.nox/
|
|
41
|
+
.coverage
|
|
42
|
+
.coverage.*
|
|
43
|
+
.cache
|
|
44
|
+
nosetests.xml
|
|
45
|
+
coverage.xml
|
|
46
|
+
*.cover
|
|
47
|
+
*.py,cover
|
|
48
|
+
.hypothesis/
|
|
49
|
+
.pytest_cache/
|
|
50
|
+
cover/
|
|
51
|
+
|
|
52
|
+
# Translations
|
|
53
|
+
*.mo
|
|
54
|
+
*.pot
|
|
55
|
+
|
|
56
|
+
# Django / Flask stuff (kept for completeness)
|
|
57
|
+
*.log
|
|
58
|
+
local_settings.py
|
|
59
|
+
db.sqlite3
|
|
60
|
+
db.sqlite3-journal
|
|
61
|
+
instance/
|
|
62
|
+
.webassets-cache
|
|
63
|
+
|
|
64
|
+
# Sphinx documentation
|
|
65
|
+
docs/_build/
|
|
66
|
+
|
|
67
|
+
# PyBuilder
|
|
68
|
+
.pybuilder/
|
|
69
|
+
target/
|
|
70
|
+
|
|
71
|
+
# Jupyter Notebook
|
|
72
|
+
.ipynb_checkpoints
|
|
73
|
+
|
|
74
|
+
# IPython
|
|
75
|
+
profile_default/
|
|
76
|
+
ipython_config.py
|
|
77
|
+
|
|
78
|
+
# pyenv
|
|
79
|
+
.python-version
|
|
80
|
+
|
|
81
|
+
# pipenv / poetry / pdm / uv lockfile preferences left to project
|
|
82
|
+
Pipfile.lock
|
|
83
|
+
poetry.lock
|
|
84
|
+
pdm.lock
|
|
85
|
+
.pdm.toml
|
|
86
|
+
.pdm-python
|
|
87
|
+
.pdm-build/
|
|
88
|
+
uv.lock
|
|
89
|
+
|
|
90
|
+
# PEP 582
|
|
91
|
+
__pypackages__/
|
|
92
|
+
|
|
93
|
+
# Environments
|
|
94
|
+
.env
|
|
95
|
+
.env.*
|
|
96
|
+
!.env.example
|
|
97
|
+
.venv
|
|
98
|
+
venv/
|
|
99
|
+
env/
|
|
100
|
+
ENV/
|
|
101
|
+
env.bak/
|
|
102
|
+
venv.bak/
|
|
103
|
+
|
|
104
|
+
# mypy / pyright / ruff caches
|
|
105
|
+
.mypy_cache/
|
|
106
|
+
.dmypy.json
|
|
107
|
+
dmypy.json
|
|
108
|
+
.pyre/
|
|
109
|
+
.pytype/
|
|
110
|
+
.ruff_cache/
|
|
111
|
+
|
|
112
|
+
# Cython debug symbols
|
|
113
|
+
cython_debug/
|
|
114
|
+
|
|
115
|
+
# IDEs / editors
|
|
116
|
+
.idea/
|
|
117
|
+
.vscode/
|
|
118
|
+
*.swp
|
|
119
|
+
*.swo
|
|
120
|
+
*~
|
|
121
|
+
.DS_Store
|
|
122
|
+
|
|
123
|
+
# Marimo
|
|
124
|
+
__marimo__/
|
everbar-0.1.0/PKG-INFO
ADDED
|
@@ -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.
|
everbar-0.1.0/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# everbar
|
|
2
|
+
|
|
3
|
+
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.
|
|
4
|
+
|
|
5
|
+
> Status: 0.1.0 — alpha. API may shift.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install everbar # core only; uses text fallback if nothing else is installed
|
|
11
|
+
pip install "everbar[tqdm]" # terminal + Jupyter via tqdm
|
|
12
|
+
pip install "everbar[all]" # everything (tqdm, rich, ipywidgets, marimo)
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Use
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from everbar import Progress
|
|
19
|
+
|
|
20
|
+
for x in Progress(items, desc="Loading"):
|
|
21
|
+
work(x)
|
|
22
|
+
|
|
23
|
+
with Progress(total=100, desc="Steps") as bar:
|
|
24
|
+
for _ in range(100):
|
|
25
|
+
do_step()
|
|
26
|
+
bar.update(1)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Overrides
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
Progress(items, backend="terminal") # per-call
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
EVERBAR_BACKEND=terminal python script.py # env var
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
import everbar
|
|
41
|
+
everbar.set_default_backend("terminal") # module-wide
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## How it picks a backend
|
|
45
|
+
|
|
46
|
+
`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,125 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "everbar"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A progress bar that works everywhere — terminal, Jupyter, VS Code, Colab, Marimo."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
keywords = ["progress", "progressbar", "tqdm", "jupyter", "marimo", "notebook"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Programming Language :: Python :: 3.14",
|
|
24
|
+
"Topic :: Software Development :: Libraries",
|
|
25
|
+
"Topic :: Terminals",
|
|
26
|
+
]
|
|
27
|
+
dependencies = []
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
tqdm = ["tqdm>=4.65"]
|
|
31
|
+
rich = ["rich>=13.0"]
|
|
32
|
+
notebook = ["ipywidgets>=8.0", "tqdm>=4.65"]
|
|
33
|
+
marimo = ["marimo>=0.10"]
|
|
34
|
+
all = ["tqdm>=4.65", "rich>=13.0", "ipywidgets>=8.0", "marimo>=0.10"]
|
|
35
|
+
|
|
36
|
+
[dependency-groups]
|
|
37
|
+
dev = [
|
|
38
|
+
"pytest>=7.0",
|
|
39
|
+
"ruff>=0.6",
|
|
40
|
+
"tqdm>=4.65",
|
|
41
|
+
"rich>=13.0",
|
|
42
|
+
"ipywidgets>=8.0",
|
|
43
|
+
"marimo>=0.10",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
[project.urls]
|
|
47
|
+
Homepage = "https://github.com/mluttikh/everbar"
|
|
48
|
+
Issues = "https://github.com/mluttikh/everbar/issues"
|
|
49
|
+
|
|
50
|
+
[tool.hatch.build.targets.wheel]
|
|
51
|
+
packages = ["src/everbar"]
|
|
52
|
+
|
|
53
|
+
[tool.pytest.ini_options]
|
|
54
|
+
testpaths = ["tests"]
|
|
55
|
+
|
|
56
|
+
[tool.ruff]
|
|
57
|
+
line-length = 80
|
|
58
|
+
target-version = "py311"
|
|
59
|
+
src = ["src", "tests"]
|
|
60
|
+
|
|
61
|
+
[tool.ruff.format]
|
|
62
|
+
quote-style = "double"
|
|
63
|
+
indent-style = "space"
|
|
64
|
+
|
|
65
|
+
[tool.ruff.lint]
|
|
66
|
+
select = [
|
|
67
|
+
"E", # pycodestyle errors
|
|
68
|
+
"W", # pycodestyle warnings
|
|
69
|
+
"F", # pyflakes
|
|
70
|
+
"I", # isort
|
|
71
|
+
"N", # pep8-naming
|
|
72
|
+
"D", # pydocstyle (google style — see [lint.pydocstyle] below)
|
|
73
|
+
"UP", # pyupgrade
|
|
74
|
+
"B", # flake8-bugbear
|
|
75
|
+
"A", # flake8-builtins (don't shadow builtins)
|
|
76
|
+
"ANN", # flake8-annotations (require type hints)
|
|
77
|
+
"ARG", # flake8-unused-arguments
|
|
78
|
+
"C4", # flake8-comprehensions
|
|
79
|
+
"EM", # flake8-errmsg (extract msgs before raise)
|
|
80
|
+
"FBT", # flake8-boolean-trap (bool params should be kw-only)
|
|
81
|
+
"INP", # flake8-no-pep420 (require __init__.py)
|
|
82
|
+
"ISC", # flake8-implicit-str-concat
|
|
83
|
+
"LOG", # flake8-logging
|
|
84
|
+
"PIE", # flake8-pie
|
|
85
|
+
"PT", # flake8-pytest-style
|
|
86
|
+
"RET", # flake8-return
|
|
87
|
+
"SIM", # flake8-simplify
|
|
88
|
+
"SLF", # flake8-self (don't access private from outside)
|
|
89
|
+
"T20", # flake8-print (no stray print())
|
|
90
|
+
"TC", # flake8-type-checking
|
|
91
|
+
"TRY", # tryceratops (exception handling)
|
|
92
|
+
"PERF", # perflint
|
|
93
|
+
"FURB", # refurb (modernization)
|
|
94
|
+
"PL", # pylint subset (E/W/R/C)
|
|
95
|
+
"ERA", # eradicate (commented-out code)
|
|
96
|
+
"RUF", # ruff-specific (includes RUF100: flag unused noqa directives)
|
|
97
|
+
]
|
|
98
|
+
ignore = [
|
|
99
|
+
"TRY003", # raise long-message — fine for small exceptions here
|
|
100
|
+
"EM101", # raw-string in exception is OK for tiny one-offs
|
|
101
|
+
"EM102", # f-string in exception is OK
|
|
102
|
+
"ISC001", # conflicts with the formatter
|
|
103
|
+
"D203", # one-blank-line-before-class (conflicts with D211)
|
|
104
|
+
"D213", # multi-line-summary-second-line (conflicts with D212)
|
|
105
|
+
"ANN401", # `Any` is permitted (used for **kwargs, __exit__, opaque inner)
|
|
106
|
+
"PLC0415", # imports inside functions — intentional for lazy soft-deps
|
|
107
|
+
"PLR0911", # too-many-return-statements — fine for dispatch funcs
|
|
108
|
+
"PLR0912", # too-many-branches — same
|
|
109
|
+
"PLR0913", # too-many-arguments — Progress legitimately has several
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
[tool.ruff.lint.pydocstyle]
|
|
113
|
+
convention = "google"
|
|
114
|
+
|
|
115
|
+
[tool.ruff.lint.per-file-ignores]
|
|
116
|
+
"tests/*" = [
|
|
117
|
+
"E501", # long lines OK
|
|
118
|
+
"D", # tests don't need docstrings
|
|
119
|
+
"S101", # asserts are the point
|
|
120
|
+
"SLF001", # accessing _private members in tests is fine
|
|
121
|
+
"PT011", # broad pytest.raises is fine
|
|
122
|
+
"PLR2004", # magic numbers in assertions are fine
|
|
123
|
+
"ARG", # pytest fixtures may pass unused args
|
|
124
|
+
"ANN", # tests don't need full type annotations
|
|
125
|
+
]
|
|
@@ -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)
|
|
@@ -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"
|
|
@@ -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)
|
|
File without changes
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Smoke tests for detect_environment()."""
|
|
2
|
+
|
|
3
|
+
from everbar import detect_environment
|
|
4
|
+
|
|
5
|
+
VALID = {
|
|
6
|
+
"marimo",
|
|
7
|
+
"colab",
|
|
8
|
+
"kaggle",
|
|
9
|
+
"vscode_notebook",
|
|
10
|
+
"jupyter",
|
|
11
|
+
"jupyter_qt",
|
|
12
|
+
"spyder",
|
|
13
|
+
"databricks",
|
|
14
|
+
"pyodide",
|
|
15
|
+
"ipython_terminal",
|
|
16
|
+
"terminal",
|
|
17
|
+
"non_tty",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_returns_known_environment():
|
|
22
|
+
env = detect_environment()
|
|
23
|
+
assert env in VALID
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_idempotent():
|
|
27
|
+
assert detect_environment() == detect_environment()
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""End-to-end tests for the Progress facade.
|
|
2
|
+
|
|
3
|
+
We force ``backend="non_tty"`` so tests don't depend on tqdm/marimo and don't
|
|
4
|
+
spam the terminal during CI.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import io
|
|
8
|
+
|
|
9
|
+
from everbar import Progress, set_default_backend
|
|
10
|
+
from everbar._backends import FallbackBackend, NullBackend
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_iterator_form_yields_all_items():
|
|
14
|
+
items = [1, 2, 3, 4, 5]
|
|
15
|
+
out = list(Progress(items, backend="non_tty"))
|
|
16
|
+
assert out == items
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_context_manager_updates():
|
|
20
|
+
with Progress(total=10, backend="non_tty") as bar:
|
|
21
|
+
for _ in range(10):
|
|
22
|
+
bar.update(1)
|
|
23
|
+
assert bar._impl._n == 10 # type: ignore[attr-defined]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_disable_uses_null_backend():
|
|
27
|
+
p = Progress([1, 2, 3], disable=True)
|
|
28
|
+
assert isinstance(p._impl, NullBackend)
|
|
29
|
+
assert list(p) == [1, 2, 3]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_explicit_backend_overrides_detection():
|
|
33
|
+
p = Progress([1, 2, 3], backend="non_tty")
|
|
34
|
+
assert isinstance(p._impl, FallbackBackend)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_env_var_overrides(monkeypatch):
|
|
38
|
+
monkeypatch.setenv("EVERBAR_BACKEND", "non_tty")
|
|
39
|
+
p = Progress([1, 2, 3])
|
|
40
|
+
assert isinstance(p._impl, FallbackBackend)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_set_default_backend(monkeypatch):
|
|
44
|
+
monkeypatch.delenv("EVERBAR_BACKEND", raising=False)
|
|
45
|
+
set_default_backend("non_tty")
|
|
46
|
+
try:
|
|
47
|
+
p = Progress([1, 2, 3])
|
|
48
|
+
assert isinstance(p._impl, FallbackBackend)
|
|
49
|
+
finally:
|
|
50
|
+
set_default_backend(None)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_fallback_writes_lines(monkeypatch):
|
|
54
|
+
buf = io.StringIO()
|
|
55
|
+
bar = FallbackBackend(total=3, desc="x", min_interval=0.0, stream=buf)
|
|
56
|
+
with bar:
|
|
57
|
+
for _ in range(3):
|
|
58
|
+
bar.update(1)
|
|
59
|
+
output = buf.getvalue()
|
|
60
|
+
assert "[progress]" in output or "[done]" in output
|
|
61
|
+
assert "x" in output
|