Publish a first version of battery_guard on PyPI

This commit is contained in:
bartbroere committed 2026-10-19 16:29:04 +02:00
commit f7d946b4a5
6 files changed
+251

No files matched your search

+7
View File
@@ -0,0 +1,7 @@
__pycache__/
*.py[cod]
.venv/
dist/
build/
*.egg-info/
.idea/
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Bart Broere
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+47
View File
@@ -0,0 +1,47 @@
# battery-guard
*A Python library to pause work until the battery is charged enough to continue*
Designed for power-hungry laptops connected to slow chargers.
Use it as a decorator on your methods. In the example below, each call to `process` first checks the battery.
If it is below 25%, execution is paused until it is back at or above that same 25%.
```python
from battery_guard import battery_guard
@battery_guard(minimum=25)
def process(item):
... # a power hungry task probably
for item in items:
process(item)
```
The battery is only checked when the decorated function is called, a call that is already running is never paused.
It therefore works best on a function that is called repeatedly, like the body of a loop, rather than on one long-running function.
To pause when the battery dips below 20%, and only continue once it has charged back up to 30%, pass `resume` as well:
```python
@battery_guard(minimum=20, resume=30)
```
When there is no function to decorate, for example in a notebook cell, block directly:
```python
from battery_guard import wait_for_battery
wait_for_battery(minimum=25)
```
## Logging
Messages are emitted through the standard `logging` module, under the `battery_guard` logger.
Warnings (such as failing to read the battery status) are shown by default.
To also see when execution pauses and resumes, enable `INFO` level, or `DEBUG` to see every battery check:
```python
import logging
logging.basicConfig(level=logging.INFO)
```
+110
View File
@@ -0,0 +1,110 @@
import functools
import logging
import time
try:
import psutil
except ImportError:
psutil = None
logger = logging.getLogger(__name__)
def _check_arguments(minimum, resume, wait_interval):
if resume is None:
resume = minimum
if not 0 <= minimum <= 100:
raise ValueError(f"minimum ({minimum}) must be between 0 and 100")
if not minimum <= resume <= 100:
raise ValueError(f"resume ({resume}) must be between minimum ({minimum}) and 100")
if wait_interval <= 0:
raise ValueError(f"wait_interval ({wait_interval}) must be positive")
return resume
def wait_for_battery(minimum=20, resume=None, wait_interval=5):
"""
Return right away if the battery is at least `minimum` percent.
If it is below `minimum`, block until it has charged back up to `resume`
percent (defaults to `minimum`). While blocking, it checks the battery every
`wait_interval` seconds (5 by default).
This is useful where there is no function to decorate, for example in a notebook cell.
See `battery_guard` for the decorator version and advice on choosing `minimum`.
"""
resume = _check_arguments(minimum, resume, wait_interval)
if psutil is None:
logger.warning("Missing psutil library, not sleeping because battery percentage is unknown")
return
waited = False
while True:
try:
battery = psutil.sensors_battery()
if battery is None:
logger.debug("psutil.sensors_battery() reports no battery, resuming")
return
threshold = resume if waited else minimum
if battery.percent >= threshold: # noqa, None comparison can't be reached because of the return above
if waited:
logger.info("Battery at %s%%, resuming", battery.percent)
return
if not waited:
logger.info("Battery at %s%%, waiting until it reaches %s%%",
battery.percent, resume)
waited = True
logger.debug("Battery at %s%%, still waiting", battery.percent)
time.sleep(wait_interval)
except Exception: # noqa: unsupported platform / sensor read failure / who knows -> log it!
logger.warning("Could not read battery status, not sleeping", exc_info=True)
return
def battery_guard(func=None, *, minimum=20, resume=None, wait_interval=5):
"""
Call the function right away if the battery is at least `minimum` percent.
If it is below `minimum`, block until it has charged back up to `resume`
percent (defaults to `minimum`), then call the function.
By default it will check every 5 seconds (`wait_interval`) before it continues.
The battery is only checked when the decorated function is called, a call that
is already running is never paused. It therefore works best on a function that
is called repeatedly, like the body of a loop that processes one item at a time,
rather than on one long-running function. To block without decorating anything,
use `wait_for_battery`.
Note that battery charging slows down when it's near a full charge.
It also may produce more heat and consume more power.
Therefore it's probably better to not set minimum too high.
The recommended range for minimum is between 20 and 80 percent.
There are valid reasons to set minimum higher though, you simply might want
to be able to take it with you and expect it to be charged as much as possible.
Or maybe you want a heavy background task to pause once you leave the office,
and the battery dipping below 95% is the indication that you have left for the train.
Setting it to 100% might also give unexpected results, because some battery management
systems will not start charging until the reported percentage drops below 95% for example.
The percentage psutil.sensors_battery() reports also isn't necessarily the same as the
percentage displayed in your system tray.
This method does not consider whether a charger is plugged in. That is on purpose.
The reason it exists is to deal with chargers that are too slow; a situation
where battery percentage decreases despite having a charger plugged in.
Usable as:
@battery_guard
@battery_guard(minimum=20, resume=25, wait_interval=30)
battery_guard(some_func, minimum=50)(arg1, arg2)
"""
resume = _check_arguments(minimum, resume, wait_interval)
def decorator(f):
@functools.wraps(f)
def wrapper(*args, **kwargs):
wait_for_battery(minimum=minimum, resume=resume, wait_interval=wait_interval)
return f(*args, **kwargs)
return wrapper
return decorator if func is None else decorator(func)
+24
View File
@@ -0,0 +1,24 @@
[project]
name = "battery-guard"
version = "2026.10.19"
description = "Pause work until the battery is charged enough to continue"
readme = "README.md"
license = "MIT"
license-files = ["LICENSE"]
authors = [{ name = "Bart Broere", email = "mail@bartbroere.eu" }]
requires-python = ">=3.11"
dependencies = ["psutil"]
classifiers = [
"Programming Language :: Python :: 3",
"Operating System :: OS Independent",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
only-include = ["battery_guard.py"]
[tool.hatch.build.targets.sdist]
only-include = ["battery_guard.py", "README.md", "LICENSE"]
Generated
+42
View File
@@ -0,0 +1,42 @@
version = 1
revision = 5
requires-python = ">=3.10"
[[package]]
name = "battery-guard"
version = "2026.10.19"
source = { editable = "." }
dependencies = [
{ name = "psutil" },
]
[package.metadata]
requires-dist = [{ name = "psutil" }]
[[package]]
name = "psutil"
version = "7.2.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" },
{ url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" },
{ url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" },
{ url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" },
{ url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" },
{ url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" },
{ url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" },
{ url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" },
{ url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" },
{ url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" },
{ url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" },
{ url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" },
{ url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" },
{ url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" },
{ url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" },
{ url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" },
{ url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" },
{ url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" },
{ url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" },
{ url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" },
]