cybertqdm 0.2.1__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.
cybertqdm/.DS_Store ADDED
Binary file
cybertqdm/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ from .cybertqdm import cybertqdm, CyberTqdm
2
+ from importlib.metadata import version
3
+
4
+ __all__ = ["CyberTqdm", "cybertqdm"]
5
+ __version__ = version("cybertqdm")
cybertqdm/cybertqdm.py ADDED
@@ -0,0 +1,120 @@
1
+ import glob
2
+ import time
3
+ import warnings
4
+
5
+ import serial
6
+ from serial.tools import list_ports
7
+ from tqdm import tqdm
8
+
9
+ __all__ = ["CyberTqdm", "cybertqdm"]
10
+
11
+ PICO_VID = (
12
+ 0x2E8A # Raspberry Pi vendor ID; used to detect the device in macOS and Linux
13
+ )
14
+
15
+
16
+ def find_pico_ports():
17
+ """Check serial ports of connected Pico devices, should work on any OS."""
18
+ return sorted(p.device for p in list_ports.comports() if p.vid == PICO_VID)
19
+
20
+
21
+ class CyberTqdm(tqdm):
22
+ """A tqdm wrapper that also prints a progres bar onto a e-ink display.
23
+
24
+ To prevent wearing down the e-ink display too fast, console and e-ink update interval differs.
25
+
26
+ - console: normal tqdm update interval
27
+ - e-ink display: updates at start, then every `epaper_interval` seconds with a final update when the observed task ends.
28
+ - If no Pico 2 is detected or the connection is lost, a warning is output. Console operation remains unaffected.
29
+ """
30
+
31
+ _ser = None # common connection for progress bars
32
+ _unavailable = False # in case of errors do not try to reconnect again and again...
33
+
34
+ def __init__(self, *args, port=None, epaper_interval=30, **kwargs):
35
+ self._epaper_interval = epaper_interval
36
+ self._last_send = 0.0
37
+ self._epaper_done = False
38
+ self._open_port(port)
39
+ super().__init__(*args, **kwargs)
40
+
41
+ # --- connection ---------------------------------------------------------
42
+ @staticmethod
43
+ def _open_port(port):
44
+ cls = CyberTqdm
45
+ if (cls._ser is not None and cls._ser.is_open) or cls._unavailable:
46
+ return
47
+ if port is None:
48
+ # ports = sorted(glob.glob("/dev/cu.usbmodem*")) # works only on macOS
49
+ ports = find_pico_ports()
50
+ if not ports:
51
+ cls._unavailable = True
52
+ warnings.warn(
53
+ "E-ink display unavailable – falling back to console output."
54
+ )
55
+ return
56
+ port = ports[0]
57
+ try:
58
+ cls._ser = serial.Serial(port, 115200, timeout=0, write_timeout=2)
59
+ except (serial.SerialException, OSError) as e:
60
+ cls._unavailable = True
61
+ warnings.warn(
62
+ f"E-ink display unavailable ({e}) – falling back to console output."
63
+ )
64
+
65
+ @staticmethod
66
+ def _drop_port(err):
67
+ cls = CyberTqdm
68
+ warnings.warn(
69
+ f"Lost connection to e-ink display ({err}) – falling back to console output."
70
+ )
71
+ try:
72
+ cls._ser.close()
73
+ except Exception:
74
+ pass
75
+ cls._ser = None
76
+ cls._unavailable = True
77
+
78
+ # --- send data -------------------------------------------------------------
79
+ def _send(self):
80
+ ser = CyberTqdm._ser
81
+ if ser is None or self.disable:
82
+ return
83
+ try:
84
+ if ser.in_waiting: # drop OK/busy messages from the Pico 2
85
+ ser.read(ser.in_waiting)
86
+ d = self.format_dict
87
+ desc = (
88
+ (self.desc or "").replace("|", "/").replace("\n", " ").strip(": ")[:30]
89
+ )
90
+ n = int(d["n"])
91
+ total = int(d["total"] or 0)
92
+ rate = d["rate"] or 0.0
93
+ ser.write(f"{desc}|{n}|{total}|{rate:.2f}\n".encode())
94
+ ser.flush()
95
+ except (serial.SerialException, OSError) as e:
96
+ self._drop_port(e)
97
+ self._last_send = time.time()
98
+
99
+ # --- tqdm hooks ---------------------------------------------------------
100
+ def display(self, msg=None, pos=None):
101
+ if time.time() - self._last_send >= self._epaper_interval:
102
+ self._send()
103
+ return super().display(msg, pos)
104
+
105
+ def close(self):
106
+ if not getattr(self, "_epaper_done", True):
107
+ self._epaper_done = True
108
+ self._send() # always display final state of the progress bar
109
+ super().close()
110
+
111
+
112
+ def cybertqdm(*args, **kwargs):
113
+ """Shortcut for CyberTqdm(range(*args), **kwargs)."""
114
+ return CyberTqdm(range(*args), **kwargs)
115
+
116
+
117
+ if __name__ == "__main__":
118
+ # quick test: 60 steps, each sleeping for 0.5 seconds, e-ink display update every 10 seconds
119
+ for _ in cybertqdm(60, desc="Loosing faith in humanity...", epaper_interval=10):
120
+ time.sleep(0.5)