braille-progress
์ ์ ๋ฌธ์๋ฅผ ์ด์ฉํ ํ๋ก๊ทธ๋ ์ค ๋ณด๋. ์์ ๋ก์ด ๋ ์ด์์ ์ปค์คํฐ๋ง์ด์ฆ, ํ์คํฌ๋ณ ๋ก๊ทธ ํจ๋, ํ
์คํฌ ์๋ฌ ๋ฆฌํฌํธ, Windows/Linux/macOS ํฐ๋ฏธ๋ ๋ณต๊ตฌ๊น์ง ์ง์.
ํน์ง
- ์ค๋ฐ๊ฟ/๋ฐ๋ฆผ ์๋ ์์ ๋ ๋๋ง(๊ฐ์ํญ ์ปท, ๊ฐ๋ ์ปฌ๋ผ), Windows VT I/O ์ง์, ์ข
๋ฃ ์ ์๋ ๋ณต๊ตฌ(์๊ทธ๋+atexit), ๋ง์ฐ์ค ๋ชจ๋ ํด์ ๋ฐ ์
๋ ฅ ๋ฒํผ ํ๋ฌ์.
- ์ข์ฐ ๋ถํ : ์ผ์ชฝ ๋ฆฌ์คํธ(ํ์คํฌ), ์ค๋ฅธ์ชฝ ํจ๋(๊ธฐ๋ณธ: ๋ก๊ทธ) + ์ธ๋ก ๊ตฌ๋ถ์ .
- ์ธํฐ๋์
: ๋ง์ฐ์ค ํด๋ฆญ, ํค๋ณด๋
w/s ๋๋ โ/โ, Home/End. (Linux๊ณ์ด ํ๊ฒฝ ํ์ )
- ํ(Row) ๋ ์ด์์ ์์ ๊ตฌ์ฑ: ์ด๋ฆ, ๋ฉ์ธ/์๋ธ ๋ฐ, ํผ์ผํธ, ์ํ, ์นด์ดํฐ, ๋ผ๋ฒจ, ๊ฒฝ๊ณผ์๊ฐ, ํ๊ท ์ฒ๋ฆฌ์๋, ETA, ์์ ํ
์คํธ ๋ฑ.
- ํค๋/ํธํฐ ์์ง ๋ ์ด์์.
- ์ค๋ฅธ์ชฝ ํจ๋ ๋ ๋๋ฌ ์ปค์คํ
(๋ฉํธ๋ฆญ, ํ ๋ฑ ์์ UI).
- ํ์คํฌ ๋ฐํ์ ํต๊ณ(EWMA ํ๊ท ์ฒ๋ฆฌ์๋, ETA).
- ์ข
๋ฃ ์ ์คํจ ํ์คํฌ์ ๋ํ ์ปฌ๋ฌ ํธ๋ ์ด์ค๋ฐฑ ๋ฆฌํฌํธ.
- ๋ฉํฐํ๋ก์ธ์ฑ์ฉ ํ ๋ฐ์ธ๋ ๋ฐ ๋ฉ์์ง ์คํค๋ง.
- ANSI/์ ๊ฐํญ(CJK, โฃฟ) ์์ ํ ๊ฐ์ํญ ์ฒ๋ฆฌ.
- ์ธ๋ก ์ ์ฑ
:
fit(๋ด์ฉ๋ง), full(ํฐ๋ฏธ๋ ๋์ด ์ฑ์). ALT ์คํฌ๋ฆฐ ์ต์
.
์ค์น
pip install braille-progress
Python โฅ 3.8. Windows๋ Windows Terminal ๋ฑ VT ์ง์ ์ฝ์ ๊ถ์ฅ.
๋น ๋ฅธ ์์
๋น ๋ฅธ ์์
from braille_progress import Progress, ProgressTheme
p = Progress(
row_policy="fit",
split_ratio=0.58,
show_vsep=True,
)
t1 = p.add("download", total=100)
t2 = p.add("extract", total=30)
for i in range(10):
t1.advance(1, stage="writing", label=f"part {i}")
p.log(t1, f"chunk {i} ok")
p.loop()
p.close()
์ข์ฐ ๋ถํ + ์ ํ + ์ค๋ฅธ์ชฝ ๋ก๊ทธ
from braille_progress import Progress
p = Progress(split_ratio=0.55, show_vsep=True)
a = p.add("prepare", total=10)
b = p.add("train", total=500)
for i in range(10):
a.advance(1, stage="writing"); p.log(a, f"prepare step {i}")
for i in range(50):
b.advance(1, stage="writing"); p.log(b, f"loss={1/(i+1):.4f}")
p.loop()
p.close()
์กฐ์:
- ํค๋ณด๋:
w/โ ์, s/โ ์๋, Home/End, q ๋๋ Ctrl+C ์ข
๋ฃ
- ๋ง์ฐ์ค: ํ์คํฌ ๋ผ์ธ ํด๋ฆญ(SGR ๋ง์ฐ์ค ๋ชจ๋ ์๋ ํ์ฑํ)
stdout/stderr๋ฅผ ๋ก๊ทธ ํจ๋์ ์ถ๋ ฅ
from braille_progress import Progress
p = Progress()
h = p.add("convert", total=3)
with p.hijack_stdio(h):
print("converting...")
try:
1/0
except Exception as e:
h.fail(error=e)
p.close()
์ปค์คํ
ํค๋/ํธํฐ/ํ(Row) ๋ ์ด์์
from braille_progress import (
Progress, ProgressTheme, Layout, Name, Bar, Percent, Status,
MiniBar, Counter, Label, Text, Gap, Elapsed, AvgRate, ETA,
Rule, Now, VLayout, VGap, default_layout
)
theme = ProgressTheme.auto_fit()
row = Layout([
Name(width=22),
Text(" | "),
Bar(cells=18),
Percent(width=5),
Gap(2),
Status(width=16),
Text(" | "),
MiniBar(cells=10),
Counter(),
Gap(2),
Label(width="flex")
], theme=theme)
header = VLayout([ Rule(), Text(" Jobs"), VGap(1) ])
footer = VLayout([ VGap(1), Rule(), Text(" Ready "), Now() ])
p = Progress(layout=row, header=header, footer=footer, split_ratio=0.6, show_vsep=True)
a = p.add("aa.zip", total=100)
b = p.add("bb.zip", total=100)
a.advance(30, stage="writing", label="downloading")
b.advance(70, stage="writing", label="processing")
p.loop()
p.close()
๋ฉ๋ชจ:
Label(width="flex") ๊ฐ๋ณ ํญ์ผ๋ก ๋จ์ ๊ณต๊ฐ์ ์ฐจ์งํ๋ฉฐ, ์ค๋ง์ถค ํ์ ์ ๋ง์ง๋ง์ ์ถ์๋ฉ๋๋ค.
Label์ด ์๋๋ผ๋ ๋ง์ง๋ง ์ธ๊ทธ๋จผํธ๋ฅผ ์ถ์ํด ์ค๋ฐ๊ฟ์ ๋ฐฉ์งํฉ๋๋ค.
- ๊ฐ์ํญ(ANSI ์ ๊ฑฐ, ์ ๊ฐํญ ๋ฐ์) ๊ธฐ์ค์ผ๋ก ํญ์ ๊ณ์ฐํฉ๋๋ค.
์ธ๋ก ํฌ๊ธฐ ์ ์ฑ
p = Progress(row_policy="full", min_body_rows=8, use_alt_screen=True)
p = Progress(row_policy="fit", max_body_rows=12, use_alt_screen=False)
row_policy:
"full": ํค๋+๋ฐ๋+ํธํฐ๊ฐ ํฐ๋ฏธ๋ ๋์ด๋ฅผ ์ฑ์
"fit": ์ฝํ
์ธ ์ ๋ง๊ฒ ๋ฐ๋ ํ ๊ฐ์๋ฅผ ๊ฒฐ์ (min_body_rows/max_body_rows๋ก ์ยทํํ ์ ์ด)
์ค๋ฅธ์ชฝ ํจ๋ ๋ ๋๋ฌ ์ปค์คํ
from braille_progress import Progress, DetailRenderer
class MetricsPanel(DetailRenderer):
def render(self, *, width, height, styler, title, lines):
out = [styler.color(f"[{title}] metrics", fg="bright_magenta").ljust(width)[:width]]
for i in range(1, height):
out.append(f"logs={len(lines)} row={i}".ljust(width)[:width])
return out
p = Progress(right_renderer=MetricsPanel(), split_ratio=0.5)
h = p.add("task", total=10)
for i in range(10): p.log(h, f"event {i}")
p.loop(); p.close()
๋ด์ฅ ๋ ๋๋ฌ:
ConsoleRenderer(๊ธฐ๋ณธ): ์ ๋ชฉ + ๋๋ถ๋ถ ๋ก๊ทธ
StaticRenderer(lines): ๊ณ ์ ๋ฌธ์์ด ๋ฆฌ์คํธ
์๋ฌ ๋ฆฌํฌํธ
์คํจํ ํ์คํฌ๋ Progress.close() ์ ์ปฌ๋ฌ ํธ๋ ์ด์ค๋ฐฑ๊ณผ ํจ๊ป ์์ฝ๋ฉ๋๋ค.
from braille_progress import Progress
p = Progress()
try:
with p.task("upload", total=3) as h:
raise RuntimeError("remote closed")
except Exception:
pass
p.close()
fail(error=..., error_tb=True)์ ์์ธ๋ฅผ ๋๊ธฐ๋ฉด ํธ๋ ์ด์ค๋ฐฑ์ด ์์๊ฒ ํฌ๋งคํ
๋ฉ๋๋ค.
ํ ๋ฐ์ธ๋(๋ฉํฐํ๋ก์ธ์ฑ)
from braille_progress import Progress, QueueBinder, progress_message
p = Progress()
h = p.add("worker-0", total=100)
binder = p.bind_queue(my_queue)
while True:
changed = binder.drain()
if changed: p.render(throttle=False)
if p.all_finished(): break
my_queue.put(progress_message(0, stage="writing", done=5, total=100, label="chunk-5"))
my_queue.put(progress_message(0, final=True))
๋ฉ์์ง ์คํค๋ง๋ ๊ธฐ๋ณธ ํค(i, stage, case_done, case_total, case_label)๋ฅผ ์ฌ์ฉํ๋ฉฐ, QueueBinder ์์ฑ ์ ํค๋ฅผ ์ค๋ฒ๋ผ์ด๋ํ ์ ์์ต๋๋ค.
Progress ๊ฐ์ฒด API
์์ฑ์(ํ๋ผ๋ฏธํฐ)
Progress(
theme: Optional[ProgressTheme]=None,
*,
auto_vt: bool=True,
auto_refresh: bool=True,
refresh_interval: float=0.05,
force_tty: Optional[bool]=None,
force_color: Optional[bool]=None,
ratio_strategy: Optional[RatioStrategy]=None,
layout: Optional[Layout]=None,
header: Optional[Union[VLayout, Layout, Sequence[Row]]]=None,
footer: Optional[Union[VLayout, Layout, Sequence[Row]]]=None,
split_ratio: float=0.55,
show_vsep: bool=True,
right_renderer: Optional[DetailRenderer]=None,
row_policy: str="fit",
min_body_rows: int=0,
max_body_rows: Optional[int]=None,
use_alt_screen: bool=False,
handle_signals: bool=True
)
ํ์/๊ฐฑ์
auto_refresh: ์ํ ๋ณ๊ฒฝ ์ ์๋ ๋ฆฌ๋ ๋.
refresh_interval: ์๋ ๋ฆฌ๋ ๋ ์ต์ ๊ฐ๊ฒฉ(์ด).
theme: ํญ ๊ณ์ฐ/์์ ํ๋ ํธ. ProgressTheme.auto_fit() ๊ถ์ฅ.
force_color: True๋ฉด ANSI ์ ๊ฐ์ , False๋ฉด ๋นํ์ฑ.
force_tty: ๊ฐ์ ๋ก TTY ๋ชจ๋๋ก ๋ ๋(ํ์ดํ ํ๊ฒฝ ํ
์คํธ์ฉ).
ratio_strategy: ์งํ๋ฅ ๊ณ์ฐ ์ ๋ต ์ปค์คํฐ๋ง์ด์ฆ.
๋ ์ด์์(์ข์ธก ๋ฆฌ์คํธ ๋ผ์ธ)
layout: ํ ์ค์ ๊ตฌ์ฑํ๋ ๋น๋ฉ๋ธ๋ก(Layout DSL). ๋ฏธ์ง์ ์ ๊ธฐ๋ณธ ๋ ์ด์์.
header/footer: ์/ํ๋จ์ ์ธ๋ก ๋ ์ด์์ ์ถ๊ฐ. VLayout, Layout, Row ์ํ์ค ์ง์.
์ข์ฐ ๋ถํ /์ฐ์ธก ํจ๋
split_ratio: ์ข์ธก ๋ฆฌ์คํธ ํญ ๋น์จ(0.1~0.9).
show_vsep: ์ข์ฐ ์ฌ์ด์ โ ํ์.
right_renderer: ์ฐ์ธก ํจ๋ ์ฝํ
์ธ ๋ ๋๋ฌ. ๊ธฐ๋ณธ์ ์ฝ์ ๋ก๊ทธ(ConsoleRenderer).
์ธ๋ก ์ ์ฑ
row_policy="fit": ์ ๊ธฐ์กด ์ถ๋ ฅ์ ๊ทธ๋๋ก ๋๊ณ , ์๋์ ํ์ํ ์ค๋ง ํ๋ณดํ์ฌ ๊ทธ ์์์ ๊ฐฑ์ (์คํฌ๋ฆฐ ์ ์ฒด๋ฅผ ์ฑ์ฐ์ง ์์).
row_policy="full": ํ์ฌ ํฐ๋ฏธ๋ ๋์ด์ ๋ง์ถฐ ๋ณธ๋ฌธ ์์ญ์ ์ฑ์.
min_body_rows: ์ต์ ์ค ๋ณด์ฅ.
max_body_rows: fit ๋ชจ๋์์ ์ต๋ ์ค ์ ํ.
์คํฌ๋ฆฐ/์๊ทธ๋
use_alt_screen: True๋ฉด ALT ์คํฌ๋ฆฐ(๋ณ๋ ๋ฒํผ) ์ฌ์ฉ.
handle_signals: SIGINT/SIGTERM/SIGHUP์์ ํฐ๋ฏธ๋/์
๋ ฅ ๋ชจ๋ ์์ ๋ณต๊ตฌ.
๋ฉ์๋
ํ์คํฌ ์์ฑ/๊ฐฑ์
h = p.add(name: str, total: int = 0) -> TaskHandle
- ์ ํ์คํฌ ์ถ๊ฐ. ๋ฐํ๋๋
TaskHandle๋ก ๊ฐฑ์ .
p.update(handle_or_id, *, advance=0, done=None, total=None,
stage=None, label=None, finished=None, failed=None) -> None
- ํ์คํฌ ์ํ ๊ฐฑ์ .
advance๋ done์ ์ฆ๊ฐ์ํด.
h.advance(n: int=1, *, label: Optional[str]=None, stage: Optional[str]=None) -> TaskHandle
- ์งํ ์์น ๊ฐํธ ์ฆ๊ฐ.
p.done(handle_or_id) -> None
h.complete() -> None
- ํ์คํฌ ์๋ฃ ์ฒ๋ฆฌ(ํ์์
stage="done").
p.fail(handle_or_id, *, stage: str="error",
error: Optional[Any]=None, error_tb: bool=True) -> None
h.fail(stage: str="error") -> None
- ์คํจ ์ฒ๋ฆฌ.
error์ ์์ธ ๊ฐ์ฒด๋ฅผ ๋๊ธฐ๋ฉด ์ข
๋ฃ ์ ์์ traceback ํฌํจ ์๋ฌ ๋ฆฌํฌํธ ์ถ๋ ฅ.
p.all_finished() -> bool
- ๋ชจ๋ ํ์คํฌ ์ข
๋ฃ ์ฌ๋ถ.
์ปจํ
์คํธ API
with p.task("name", total=10) as h:
...
- ๋ธ๋ก ๋ด ์์ธ ๋ฐ์ ์ ์๋
fail(), ์ ์ ์ข
๋ฃ ์ ์๋ done().
๋ฐ๋ณต ๋์ฐ๋ฏธ
for item in p.track(iterable, total=None, description=None, label_from=None):
...
- ๋ฐ๋ณต ์ค ์๋ ์งํ/๋ผ๋ฒจ ๊ฐฑ์ . ์์ธ ์ ์๋
fail().
๋ก๊ทธ/์ฐ์ธก ํจ๋
p.log(handle_or_id, msg: str) -> None
- ํด๋น ํ์คํฌ์ ๋ก๊ทธ๋ฅผ ์ฐ์ธก ํจ๋์ ์ถ๊ฐ(์ต๋ ์ต๊ทผ 2000์ค ์ ์ง).
p.set_right_renderer(renderer: DetailRenderer) -> None
- ์ฐ์ธก ํจ๋ ๋ ๋๋ฌ ๊ต์ฒด.
with p.hijack_stdio(handle_or_id):
print("captured")
๋ ๋/๋ฃจํ/์ข
๋ฃ
p.render(throttle: bool=False) -> None
- ์๋ ๋ ๋.
throttle=True๋ฉด refresh_interval์ ์กด์ค.
p.loop() -> None
p.close() -> None
- ํ๋ฉด ์ ๋ฆฌ, ์คํจ ํ์คํฌ ์๋ฌ ๋ฆฌํฌํธ ์ถ๋ ฅ, ๋ง์ฐ์ค/์
๋ ฅ ๋ชจ๋/ALT ์คํฌ๋ฆฐ/VT ๋ชจ๋ ๋ฑ ์์ ๋ณต๊ตฌ.
ํ ๋ฐ์ธ๋(์ ํ)
qb = p.bind_queue(queue, id_key="i", stage_key="stage",
done_key="case_done", total_key="case_total",
label_key="case_label")
changed = qb.drain()
- ์ธ๋ถ ์์ปค ๋ฉ์์ง๋ฅผ UI์ ๋ฐ์.
์ฐ์ธก ํจ๋ ์ปค์คํ
๋ ๋๋ฌ
from braille_progress import DetailRenderer
class MyPanel(DetailRenderer):
def render(self, *, width, height, styler, title, lines):
out = [styler.color(f"[{title}] metrics", fg="bright_magenta").ljust(width)[:width]]
for i in range(1, height):
txt = f"rows={height}, logs={len(lines)}"
out.append(txt.ljust(width)[:width])
return out
width/height ๋ด์์๋ง ์ถ๋ ฅํ๋๋ก ๋ฐ๋์ ํจ๋ฉ/์ ๋จ ์ฒ๋ฆฌ.
์ข์ธก ๋ผ์ธ ๋ ์ด์์ ๊ต์ฒด(์์ฝ)
- DSL ๊ตฌ์ฑ์์ ์:
Name(), Bar(), Percent(), Status(), MiniBar(), Counter(), Label(), Elapsed(), AvgRate(), ETA(), Text(" | "), Gap(w) ๋ฑ.
- ์:
from braille_progress import Layout, Name, Text, Bar, Percent, Status, MiniBar, Counter, Label
layout = Layout([
Name(w=20), Text(" | "),
Bar(cells=18), Percent(), Text(" "),
Status(w=16), Text(" | "),
MiniBar(cells=10), Counter(), Text(" "),
Label(w=32)
])
p = Progress(layout=layout)
๋ชจ๋/ํ๊ฒฝ ๋ณ์
use_alt_screen=True: ๋ฉ์ธ ํฐ๋ฏธ๋๊ณผ ๋ถ๋ฆฌ๋ ๋ฒํผ์ ๋ ๋(์ธ๋ถ ์ถ๋ ฅ ๊ฐ์ญ ์ค์).
NO_COLOR=1: ๊ฐ์ ๋ก ๋ฌด์ฑ์.
BP_FORCE_TTY=1: ๊ฐ์ ๋ก TTY ๋ชจ๋.
BP_FORCE_COLOR=1: ๊ฐ์ ๋ก ์ปฌ๋ฌ ํ์ฑ.
์๋ฌ ๋ฆฌํฌํธ
์ฌ์ฉ ํ
- ๋ฃจํ ๋์ ์ธ๋ถ
print()๊ฐ ํ์ํ๋ฉด ๋ฐ๋์ hijack_stdio()๋ก ๊ฐ์ธ ์ฐ์ธก ๋ก๊ทธ๋ก ๋ณด๋ด๋ผ(๋ ์ด์์ ๊นจ์ง ๋ฐฉ์ง).
row_policy="fit"์ ๊ธฐ์กด ์๋จ ์ถ๋ ฅ ๋ณด์กด. ํ์ ์ค์ด ๋๋ฉด ์๋๋ก๋ง ํ์ฅํ๋ค.
row_policy="full"์ ํฐ๋ฏธ๋ ๋์ด๋ฅผ ์ฑ์ฐ๋ฉฐ ํค๋/ํธํฐ์ ํจ๊ป ์คํ
์ด๋ธํ๊ฒ ๊ฐฑ์ ํ๋ค.
- Windows์์๋ Windows Terminal + ๊ณ ์ ํญ ํฐํธ ์ฌ์ฉ์ ๊ถ์ฅ.
๋ ๋๋ง/ํฐ๋ฏธ๋ ๋์
- ๋ ๋๋ง ์ค ์๋์ค๋ฐ๊ฟ์ ๋๊ณ , ๋ชจ๋ ์ค์
cols-1 ๋ด๋ก ๊ฐ์ํญ ๊ธฐ์ค ์ ๋จ. ๋ง์ง๋ง ์ค์ ๊ฐํ ์์ด ์ถ๋ ฅํด ์คํฌ๋กค์ ๋ง์ต๋๋ค.
loop()์์ VT ์
๋ ฅยท๋ง์ฐ์ค ํ์ฑํ, close()/์ข
๋ฃ ์ ๋ง์ฐ์ค/ํฌ์ปค์ค/๋ธ๋ํท๋ ํ์ด์คํธ ๋ชจ๋ ํด์ (?1006/?1002/?1000/?1015/?1004/?2004), ์ปค์ ๋ณด์ด๊ธฐยทwrap ๋ณต๊ตฌ, ์
๋ ฅ ๋ฒํผ ํ๋ฌ์(Windows FlushConsoleInputBuffer, POSIX tcflush), ALT ์คํฌ๋ฆฐ ์ข
๋ฃ, ์ฝ์ ๋ชจ๋ ์๋ณต.
- ๋ ๋๋ง ์ค
print() ํธ์ถ์ ํ๋ฉด์ ํ๋ญ๋๋ค. p.log(...) ๋๋ p.hijack_stdio(handle) ์ฌ์ฉ์ ๊ถ์ฅ.
ํ๊ฒฝ ๋ณ์
BP_FORCE_TTY=1 : TTY ๋ชจ๋ ๊ฐ์
BP_FORCE_COLOR=1 : ์ปฌ๋ฌ ๊ฐ์
NO_COLOR=1 : ์ปฌ๋ฌ ๋นํ์ฑํ
์์
์๋ฌ ํฌํจ ์ต์ ์์
from braille_progress import Progress
p = Progress()
h = p.add("upload", total=3)
try:
for i in range(3):
if i == 2: raise RuntimeError("remote closed")
h.advance(1, stage="writing")
except Exception as e:
h.fail(error=e)
p.close()
Fit ๋ชจ๋ ๋์๋ณด๋(ALT ์คํฌ๋ฆฐ ์์ด)
from braille_progress import Progress, default_layout, ProgressTheme
p = Progress(
layout=default_layout(ProgressTheme.auto_fit()),
row_policy="fit",
max_body_rows=10,
split_ratio=0.6,
show_vsep=True,
use_alt_screen=False
)
p.loop(); p.close()
๊ณต๊ฐ ์ฌ๋ณผ
from braille_progress import (
Progress, ProgressTheme, TaskHandle, TaskState,
RatioStrategy, DefaultRatio, QueueBinder, progress_message,
Layout, default_layout, RenderContext,
Name, Bar, Percent, Status, MiniBar, Counter, Label, Text, Gap,
Elapsed, AvgRate, ETA, Spacer, Rule, Now, VGap, VLayout,
DetailRenderer, ConsoleRenderer, StaticRenderer
)