From 4750dbd7177bf8f9ee66e608a4170283216cada6 Mon Sep 17 00:00:00 2001 From: "Eric W. Tramel" Date: Wed, 20 May 2026 21:55:16 -0400 Subject: [PATCH 01/19] feat: chart generation throughput Replace sticky progress bars with a bounded ANSI/asciichart throughput panel that plots records per second per generation column. Default progress_bar to enabled and add a local demo config plus screenshot for PR review. Signed-off-by: Eric W. Tramel --- .../assets/progress-throughput-panel-demo.png | Bin 0 -> 40810 bytes examples/progress_panel_demo.py | 65 ++++ .../src/data_designer/config/run_config.py | 8 +- .../tests/config/test_run_config.py | 4 + packages/data-designer-engine/pyproject.toml | 1 + .../utils/async_progress_reporter.py | 9 +- .../utils/progress_tracker.py | 12 +- .../utils/sticky_progress_bar.py | 278 ++++++++++++++---- .../utils/test_sticky_progress_bar.py | 145 +++------ uv.lock | 14 + 10 files changed, 366 insertions(+), 170 deletions(-) create mode 100644 docs/assets/progress-throughput-panel-demo.png create mode 100644 examples/progress_panel_demo.py diff --git a/docs/assets/progress-throughput-panel-demo.png b/docs/assets/progress-throughput-panel-demo.png new file mode 100644 index 0000000000000000000000000000000000000000..d9e56fe698eee3d4b1963fe5ef00e1ebefd6b409 GIT binary patch literal 40810 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 Mp00i_>zopr0HE#Jq5uE@ literal 0 HcmV?d00001 diff --git a/examples/progress_panel_demo.py b/examples/progress_panel_demo.py new file mode 100644 index 000000000..e674569b4 --- /dev/null +++ b/examples/progress_panel_demo.py @@ -0,0 +1,65 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import threading +import time + +import data_designer.config as dd + +_WORKERS = threading.Semaphore(6) + + +def _simulate_work(row: dict, *, column: str, base_delay: float) -> str: + topic = str(row["topic"]) + jitter = (sum(ord(ch) for ch in topic + column) % 7) * 0.025 + with _WORKERS: + time.sleep(base_delay + jitter) + return f"{column}:{topic.lower().replace(' ', '-')}" + + +@dd.custom_column_generator(required_columns=["topic"]) +def intent_label(row: dict) -> dict: + row["intent_label"] = _simulate_work(row, column="intent", base_delay=0.12) + return row + + +@dd.custom_column_generator(required_columns=["topic"]) +def risk_signal(row: dict) -> dict: + row["risk_signal"] = _simulate_work(row, column="risk", base_delay=0.16) + return row + + +@dd.custom_column_generator(required_columns=["topic"]) +def routing_bucket(row: dict) -> dict: + row["routing_bucket"] = _simulate_work(row, column="route", base_delay=0.10) + return row + + +def load_config_builder() -> dd.DataDesignerConfigBuilder: + config_builder = dd.DataDesignerConfigBuilder() + + config_builder.add_column( + dd.SamplerColumnConfig( + name="topic", + sampler_type=dd.SamplerType.CATEGORY, + params=dd.CategorySamplerParams( + values=[ + "Account recovery", + "GPU capacity", + "Invoice dispute", + "Model quality", + "Security review", + "Dataset cleanup", + "Latency regression", + "Documentation gap", + ], + ), + ) + ) + config_builder.add_column(dd.CustomColumnConfig(name="intent_label", generator_function=intent_label)) + config_builder.add_column(dd.CustomColumnConfig(name="risk_signal", generator_function=risk_signal)) + config_builder.add_column(dd.CustomColumnConfig(name="routing_bucket", generator_function=routing_bucket)) + + return config_builder diff --git a/packages/data-designer-config/src/data_designer/config/run_config.py b/packages/data-designer-config/src/data_designer/config/run_config.py index ea3393b26..fd096ca38 100644 --- a/packages/data-designer-config/src/data_designer/config/run_config.py +++ b/packages/data-designer-config/src/data_designer/config/run_config.py @@ -142,9 +142,9 @@ class RunConfig(ConfigBase): Default is 0. async_trace: If True, collect per-task tracing data when using the async engine (DATA_DESIGNER_ASYNC_ENGINE=1). Has no effect on the sync path. Default is False. - progress_bar: If True, display sticky ANSI progress bars instead of periodic log lines - during generation. Requires a TTY; falls back to log lines in non-TTY environments. - Default is False. + progress_bar: If True, display a sticky ANSI throughput chart panel instead of periodic + log lines during generation. Requires a TTY; falls back to log lines in non-TTY + environments. Default is True. progress_interval: How often (in seconds) the async progress reporter emits a consolidated log block. Must be > 0. Default is 5.0. jinja_rendering_engine: Template renderer used for engine-side Jinja evaluation. @@ -169,7 +169,7 @@ class RunConfig(ConfigBase): max_conversation_restarts: int = Field(default=5, ge=0) max_conversation_correction_steps: int = Field(default=0, ge=0) async_trace: bool = False - progress_bar: bool = False + progress_bar: bool = True progress_interval: float = Field(default=5.0, gt=0.0) jinja_rendering_engine: JinjaRenderingEngine = Field( default=JinjaRenderingEngine.SECURE, diff --git a/packages/data-designer-config/tests/config/test_run_config.py b/packages/data-designer-config/tests/config/test_run_config.py index 9d216025c..62aae7319 100644 --- a/packages/data-designer-config/tests/config/test_run_config.py +++ b/packages/data-designer-config/tests/config/test_run_config.py @@ -19,6 +19,10 @@ def test_run_config_defaults_to_secure_jinja_renderer() -> None: assert JinjaRenderingEngine(RunConfig().jinja_rendering_engine) == JinjaRenderingEngine.SECURE +def test_run_config_defaults_to_progress_bar_enabled() -> None: + assert RunConfig().progress_bar is True + + def test_run_config_accepts_native_renderer() -> None: run_config = RunConfig(jinja_rendering_engine=JinjaRenderingEngine.NATIVE) assert JinjaRenderingEngine(run_config.jinja_rendering_engine) == JinjaRenderingEngine.NATIVE diff --git a/packages/data-designer-engine/pyproject.toml b/packages/data-designer-engine/pyproject.toml index 87f19785b..83ea684a8 100644 --- a/packages/data-designer-engine/pyproject.toml +++ b/packages/data-designer-engine/pyproject.toml @@ -33,6 +33,7 @@ bump = true [tool.hatch.metadata.hooks.uv-dynamic-versioning] dependencies = [ "anyascii>=0.3.3,<1", + "asciichartpy>=1.5.25,<2", "chardet>=3.0.2,<6", # Pulled in by sqlfluff "cryptography>=46.0.7,<47", # 46.0.7 fixes CVE-2026-39892 pulled in by mcp "data-designer-config=={{ version }}", diff --git a/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/async_progress_reporter.py b/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/async_progress_reporter.py index c394ae613..8a5e81462 100644 --- a/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/async_progress_reporter.py +++ b/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/async_progress_reporter.py @@ -70,8 +70,7 @@ def record_skipped(self, column: str) -> None: def log_final(self) -> None: if self._bar is not None and self._bar.is_active: - for col in self._trackers: - self._bar.remove_bar(col) + self._update_bar() else: self._emit() elapsed = time.perf_counter() - self._start_time @@ -101,10 +100,10 @@ def _maybe_report(self) -> None: def _update_bar(self) -> None: elapsed = time.perf_counter() - self._start_time - updates: dict[str, tuple[int, int, int]] = {} + updates: dict[str, tuple[int, int, int, int]] = {} for col, tracker in self._trackers.items(): - completed, _total, success, failed, _skipped, _pct, _rate, _emoji = tracker.get_snapshot(elapsed) - updates[col] = (completed, success, failed) + completed, _total, success, failed, skipped, _pct, _rate, _emoji = tracker.get_snapshot(elapsed) + updates[col] = (completed, success, failed, skipped) self._bar.update_many(updates) def _emit(self) -> None: diff --git a/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/progress_tracker.py b/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/progress_tracker.py index 73afa2e26..9935d3c78 100644 --- a/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/progress_tracker.py +++ b/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/progress_tracker.py @@ -103,8 +103,15 @@ def get_snapshot(self, elapsed: float | None = None) -> tuple[int, int, int, int def log_final(self) -> None: """Log final progress summary.""" with self.lock: - if self._bar is not None: - self._bar.remove_bar(self._bar_key) + if self._bar is not None and self._bar.is_active: + self._bar.update( + self._bar_key, + completed=self.completed, + success=self.success, + failed=self.failed, + skipped=self.skipped, + ) + return if self.completed > 0: self._log_progress_unlocked() @@ -143,6 +150,7 @@ def _log_progress_unlocked(self) -> None: completed=self.completed, success=self.success, failed=self.failed, + skipped=self.skipped, ) return diff --git a/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/sticky_progress_bar.py b/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/sticky_progress_bar.py index f4df06221..eaa9c72e9 100644 --- a/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/sticky_progress_bar.py +++ b/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/sticky_progress_bar.py @@ -12,17 +12,66 @@ from threading import Lock from typing import TextIO -BAR_FILLED = "ā–ˆ" -BAR_EMPTY = "ā–‘" -_ANSI_RE = re.compile(r"\033\[[0-9;]*m") +import asciichartpy +_ANSI_RE = re.compile(r"\033\[[0-9;?]*[a-zA-Z]") +_RESET = "\033[0m" +_BORDER = "\033[38;5;39m" +_TITLE = "\033[1;38;5;81m" +_MUTED = "\033[2;38;5;245m" +_FAILED = "\033[31m" +_OK = "\033[32m" +_CURVE_COLORS = [ + asciichartpy.lightcyan, + asciichartpy.lightgreen, + asciichartpy.lightmagenta, + asciichartpy.lightyellow, + asciichartpy.lightblue, + asciichartpy.lightred, + asciichartpy.cyan, + asciichartpy.green, +] +_DEFAULT_PANEL_HEIGHT = 16 +_MIN_PANEL_HEIGHT = 9 +_MIN_SAMPLE_INTERVAL_SECONDS = 0.25 -def _compute_stats_width(total: int) -> int: - """Compute the fixed width of the stats portion based on total records.""" - total_w = len(str(total)) - # " 100% | xxx/xxx | 9999.9 rec/s | eta 999s | xxx failed" - sample = f" 100% | {'9' * total_w}/{total} | 9999.9 rec/s | eta 999s | {'9' * total_w} failed" - return len(sample) + +ProgressUpdate = tuple[int, int, int] | tuple[int, int, int, int] + + +def _visible_len(text: str) -> int: + return len(_ANSI_RE.sub("", text)) + + +def _fit_ansi(text: str, width: int) -> str: + visible = _visible_len(text) + if visible > width: + return _ANSI_RE.sub("", text)[:width] + return text + (" " * (width - visible)) + + +def _color(text: str, color: str) -> str: + return f"{color}{text}{_RESET}" + + +def _average(values: list[float]) -> float: + return sum(values) / len(values) if values else 0.0 + + +def _compress_series(series: list[float], max_points: int) -> list[float]: + if max_points <= 0: + return [] + if len(series) <= max_points: + return series or [0.0] + + compressed: list[float] = [] + count = len(series) + for bucket_index in range(max_points): + start = int(bucket_index * count / max_points) + end = int((bucket_index + 1) * count / max_points) + bucket = series[start : max(end, start + 1)] + compressed.append(_average(bucket)) + return compressed @dataclass @@ -32,18 +81,51 @@ class _BarState: completed: int = 0 success: int = 0 failed: int = 0 + skipped: int = 0 start_time: float = field(default_factory=time.perf_counter) - stats_width: int = 0 + last_sample_time: float = field(default_factory=time.perf_counter) + last_completed: int = 0 + latest_rate: float = 0.0 + rates: list[float] = field(default_factory=lambda: [0.0]) - def __post_init__(self) -> None: - self.stats_width = _compute_stats_width(self.total) + def record_update( + self, + *, + completed: int, + success: int, + failed: int, + skipped: int, + now: float, + ) -> None: + bounded_completed = min(max(completed, 0), self.total) if self.total > 0 else max(completed, 0) + elapsed = now - self.last_sample_time + self.completed = bounded_completed + self.success = success + self.failed = failed + self.skipped = skipped + + should_sample = elapsed >= _MIN_SAMPLE_INTERVAL_SECONDS or bounded_completed >= self.total + if should_sample: + delta_completed = max(0, bounded_completed - self.last_completed) + sample_elapsed = max(elapsed, _MIN_SAMPLE_INTERVAL_SECONDS) + rate = delta_completed / sample_elapsed + self.latest_rate = rate + self.rates.append(rate) + self.last_completed = bounded_completed + self.last_sample_time = now + + def average_rate(self, now: float) -> float: + elapsed = max(now - self.start_time, _MIN_SAMPLE_INTERVAL_SECONDS) + return self.completed / elapsed if elapsed > 0 else 0.0 class StickyProgressBar: - """ANSI progress bar that sticks to the bottom of the terminal. + """ANSI throughput chart panel that sticks to the bottom of the terminal. - Log messages (via standard ``logging``) are rendered above the bar - automatically. The bar redraws in-place after each update. + Log messages (via standard ``logging``) are rendered above the panel + automatically. The panel redraws in-place after each update, tracks one + records-per-second curve per active generation column, and keeps a bounded + height so it does not take over the terminal. Usage:: @@ -51,12 +133,11 @@ class StickyProgressBar: bar.add_bar("col_a", "column 'a'", total=100) for i in range(100): bar.update("col_a", completed=i + 1, success=i + 1) - bar.remove_bar("col_a") Falls back to a no-op on non-TTY streams (CI, pipes, notebooks). """ - def __init__(self, stream: TextIO | None = None) -> None: + def __init__(self, stream: TextIO | None = None, *, panel_height: int = _DEFAULT_PANEL_HEIGHT) -> None: self._stream = stream or sys.stderr self._is_tty = hasattr(self._stream, "isatty") and self._stream.isatty() self._bars: dict[str, _BarState] = {} @@ -64,6 +145,8 @@ def __init__(self, stream: TextIO | None = None) -> None: self._drawn_lines = 0 self._active = False self._wrapped_handlers: list[tuple[logging.StreamHandler, object]] = [] + self._panel_height = max(_MIN_PANEL_HEIGHT, panel_height) + self._start_time = time.perf_counter() @property def is_active(self) -> bool: @@ -78,17 +161,17 @@ def drawn_lines(self) -> int: def __enter__(self) -> StickyProgressBar: if self._is_tty: self._active = True + self._start_time = time.perf_counter() self._wrap_handlers() self._write("\033[?25l") # hide cursor return self def __exit__(self, *args: object) -> None: if self._active: - with self._lock: - self._clear_bars() self._write("\033[?25h") # show cursor self._unwrap_handlers() self._active = False + self._drawn_lines = 0 # -- public API -- @@ -105,22 +188,34 @@ def update( completed: int, success: int = 0, failed: int = 0, + skipped: int = 0, ) -> None: with self._lock: if bar := self._bars.get(key): - bar.completed = completed - bar.success = success - bar.failed = failed + bar.record_update( + completed=completed, + success=success, + failed=failed, + skipped=skipped, + now=time.perf_counter(), + ) if self._active: self._redraw() - def update_many(self, updates: dict[str, tuple[int, int, int]]) -> None: + def update_many(self, updates: dict[str, ProgressUpdate]) -> None: with self._lock: - for key, (completed, success, failed) in updates.items(): + now = time.perf_counter() + for key, update in updates.items(): if bar := self._bars.get(key): - bar.completed = completed - bar.success = success - bar.failed = failed + completed, success, failed = update[:3] + skipped = update[3] if len(update) > 3 else bar.skipped + bar.record_update( + completed=completed, + success=success, + failed=failed, + skipped=skipped, + now=now, + ) if self._active: self._redraw() @@ -162,7 +257,7 @@ def _unwrap_handlers(self) -> None: # -- drawing -- def _clear_bars(self) -> None: - """Clear drawn bar lines from the terminal. Caller must hold the lock.""" + """Clear drawn panel lines from the terminal. Caller must hold the lock.""" if self._drawn_lines > 0: for _ in range(self._drawn_lines): self._write("\033[A\033[2K") @@ -170,44 +265,103 @@ def _clear_bars(self) -> None: self._drawn_lines = 0 def _redraw(self) -> None: - """Redraw all bars. Caller must hold the lock.""" + """Redraw the chart panel. Caller must hold the lock.""" self._clear_bars() if not self._bars: return - width = shutil.get_terminal_size().columns - max_label = max(len(b.label) for b in self._bars.values()) - for bar in self._bars.values(): - line = self._format_bar(bar, width, max_label) + lines = self._format_panel() + for line in lines: self._write(line + "\n") - visible = len(_ANSI_RE.sub("", line)) - if width > 0 and visible > width: - self._drawn_lines += (visible + width - 1) // width - else: - self._drawn_lines += 1 - - def _format_bar(self, bar: _BarState, width: int, label_width: int) -> str: - completed = min(bar.completed, bar.total) - pct = (completed / bar.total * 100) if bar.total > 0 else 100.0 - elapsed = time.perf_counter() - bar.start_time - rate = min(bar.completed / elapsed if elapsed > 0 else 0.0, 9999.9) - remaining = max(0, bar.total - completed) - eta = f"{min(remaining / rate, 999):.0f}s" if rate > 0 else "?" - - label = bar.label.ljust(label_width) - total_w = len(str(bar.total)) - count_str = f"{completed:>{total_w}}/{bar.total}" - stats = f" {pct:3.0f}% | {count_str} | {rate:6.1f} rec/s | eta {eta:>4s} | {bar.failed:>{total_w}} failed" - stats = stats.ljust(bar.stats_width) - - bar_width = width - len(label) - bar.stats_width - 4 - if bar_width < 1: - return f" {label} {stats}"[: max(0, width - 1)] - - filled = int(bar_width * pct / 100) - empty = bar_width - filled - - colored_bar = f"\033[32m{BAR_FILLED * filled}\033[90m{BAR_EMPTY * empty}\033[0m" - return f" {label} {colored_bar}{stats}" + self._drawn_lines = len(lines) + + def _format_panel(self) -> list[str]: + terminal_size = shutil.get_terminal_size() + panel_width = max(4, terminal_size.columns - 1) + panel_height = min(self._panel_height, max(_MIN_PANEL_HEIGHT, terminal_size.lines - 1)) + inner_width = panel_width - 2 + + legend_capacity = 4 if panel_height >= 13 else max(1, panel_height - 9) + chart_line_count = max(3, panel_height - 4 - legend_capacity) + chart_height = chart_line_count - 1 + + now = time.perf_counter() + bars = list(self._bars.values()) + chart_lines = self._format_chart_lines(bars, inner_width, chart_height) + legend_lines = self._format_legend_lines(bars, now, legend_capacity) + + lines = [ + self._border("ā•­", "─", "ā•®", panel_width), + self._panel_line(self._format_header(bars, now), inner_width), + ] + lines.extend(self._panel_line(line, inner_width) for line in chart_lines) + lines.append(self._border("ā”œ", "─", "┤", panel_width)) + lines.extend(self._panel_line(line, inner_width) for line in legend_lines) + lines.append(self._border("ā•°", "─", "╯", panel_width)) + return lines + + def _format_header(self, bars: list[_BarState], now: float) -> str: + elapsed = max(now - self._start_time, 0.0) + completed = sum(bar.completed for bar in bars) + total = sum(bar.total for bar in bars) + latest_rate = sum(bar.latest_rate for bar in bars) + failed = sum(bar.failed for bar in bars) + skipped = sum(bar.skipped for bar in bars) + failed_text = _color(f"{failed} failed", _FAILED) if failed else _color("0 failed", _OK) + skipped_text = f" | {skipped} skipped" if skipped else "" + return ( + f"{_TITLE}Throughput{_RESET} " + f"{_MUTED}rec/s | {elapsed:5.1f}s | {completed}/{total} | " + f"now {latest_rate:6.1f}{skipped_text} | {_RESET}{failed_text}" + ) + + def _format_chart_lines(self, bars: list[_BarState], inner_width: int, chart_height: int) -> list[str]: + max_points = max(2, inner_width - 12) + series = [_compress_series(bar.rates, max_points) for bar in bars] + max_rate = max((max(points) for points in series if points), default=0.0) + chart = asciichartpy.plot( + series, + { + "height": chart_height, + "min": 0.0, + "max": max(1.0, max_rate), + "format": "{:6.1f} ", + "colors": [_CURVE_COLORS[i % len(_CURVE_COLORS)] for i in range(len(series))], + }, + ) + lines = chart.splitlines() + while len(lines) < chart_height + 1: + lines.append("") + return lines[: chart_height + 1] + + def _format_legend_lines(self, bars: list[_BarState], now: float, capacity: int) -> list[str]: + lines: list[str] = [] + visible_bars = bars + if len(bars) > capacity: + visible_bars = bars[: max(0, capacity - 1)] + + for index, bar in enumerate(visible_bars): + color = _CURVE_COLORS[index % len(_CURVE_COLORS)] + pct = (bar.completed / bar.total * 100) if bar.total > 0 else 100.0 + failed = f" | {_color(str(bar.failed) + ' failed', _FAILED)}" if bar.failed else "" + skipped = f" | {bar.skipped} skipped" if bar.skipped else "" + lines.append( + f"{_color('ā—', color)} {bar.label}: {bar.completed}/{bar.total} " + f"({pct:3.0f}%) | now {bar.latest_rate:5.1f} rec/s | " + f"avg {bar.average_rate(now):5.1f}{failed}{skipped}" + ) + + if len(bars) > capacity: + lines.append(f"{_MUTED}... {len(bars) - len(visible_bars)} more column(s){_RESET}") + + while len(lines) < capacity: + lines.append("") + return lines[:capacity] + + def _panel_line(self, text: str, inner_width: int) -> str: + return f"{_BORDER}│{_RESET}{_fit_ansi(text, inner_width)}{_BORDER}│{_RESET}" + + def _border(self, left: str, fill: str, right: str, width: int) -> str: + return f"{_BORDER}{left}{fill * (width - 2)}{right}{_RESET}" def _write(self, text: str) -> None: self._stream.write(text) diff --git a/packages/data-designer-engine/tests/engine/dataset_builders/utils/test_sticky_progress_bar.py b/packages/data-designer-engine/tests/engine/dataset_builders/utils/test_sticky_progress_bar.py index d155be394..bc7695169 100644 --- a/packages/data-designer-engine/tests/engine/dataset_builders/utils/test_sticky_progress_bar.py +++ b/packages/data-designer-engine/tests/engine/dataset_builders/utils/test_sticky_progress_bar.py @@ -14,9 +14,7 @@ from data_designer.engine.dataset_builders.utils.async_progress_reporter import AsyncProgressReporter from data_designer.engine.dataset_builders.utils.progress_tracker import ProgressTracker -from data_designer.engine.dataset_builders.utils.sticky_progress_bar import ( - StickyProgressBar, -) +from data_designer.engine.dataset_builders.utils.sticky_progress_bar import StickyProgressBar CURSOR_UP_CLEAR = "\033[A\033[2K" HIDE_CURSOR = "\033[?25l" @@ -36,6 +34,17 @@ def tty_stream() -> FakeTTY: return FakeTTY() +def _clean(text: str) -> str: + return _ALL_ANSI_RE.sub("", text).replace("\r", "") + + +def _last_panel_lines(output: str) -> list[str]: + clean = _clean(output) + panel_start = clean.rfind("ā•­") + assert panel_start >= 0 + return clean[panel_start:].splitlines() + + def test_no_output_when_not_tty() -> None: stream = io.StringIO() with StickyProgressBar(stream=stream) as bar: @@ -52,44 +61,38 @@ def test_hides_and_shows_cursor(tty_stream: FakeTTY) -> None: assert output.endswith(SHOW_CURSOR) -def test_drawn_lines_tracks_add_and_remove(tty_stream: FakeTTY) -> None: +def test_renders_bounded_throughput_panel(tty_stream: FakeTTY) -> None: with StickyProgressBar(stream=tty_stream) as bar: - bar.add_bar("a", "col_a", 10) - bar.add_bar("b", "col_b", 10) - bar.add_bar("c", "col_c", 10) - assert bar.drawn_lines == 3 - - bar.remove_bar("a") - assert bar.drawn_lines == 2 - - bar.add_bar("d", "col_d", 10) - assert bar.drawn_lines == 3 - - bar.update("b", completed=5, success=5) - assert bar.drawn_lines == 3 + bar.add_bar("a", "column 'a'", 100) + bar.add_bar("b", "column 'b'", 100) + bar.update_many({"a": (10, 10, 0), "b": (20, 20, 0)}) - bar.remove_bar("b") - bar.remove_bar("c") - bar.remove_bar("d") - assert bar.drawn_lines == 0 + assert bar.drawn_lines == 16 + panel = "\n".join(_last_panel_lines(tty_stream.getvalue())) + assert "Throughput" in panel + assert "rec/s" in panel + assert "column 'a': 10/100" in panel + assert "column 'b': 20/100" in panel + assert "ā•­" in panel + assert "ā•°" in panel -def test_drawn_lines_stable_across_many_updates(tty_stream: FakeTTY) -> None: +def test_panel_height_stable_across_many_updates(tty_stream: FakeTTY) -> None: with StickyProgressBar(stream=tty_stream) as bar: bar.add_bar("a", "col_a", 100) bar.add_bar("b", "col_b", 100) - bar.add_bar("c", "col_c", 100) + for i in range(50): - bar.update("a", completed=i, success=i) - bar.update("b", completed=i, success=i) - bar.update("c", completed=i, success=i) + bar.update_many({"a": (i, i, 0), "b": (i * 2, i * 2, 0)}) snapshot = tty_stream.getvalue() bar.update("a", completed=50, success=50) - assert tty_stream.getvalue()[len(snapshot) :].count(CURSOR_UP_CLEAR) == 3 + + assert tty_stream.getvalue()[len(snapshot) :].count(CURSOR_UP_CLEAR) == 16 + assert bar.drawn_lines == 16 -def test_log_interleaving_preserves_drawn_lines(tty_stream: FakeTTY) -> None: +def test_log_interleaving_preserves_panel_height(tty_stream: FakeTTY) -> None: root_logger = logging.getLogger() handler = logging.StreamHandler(tty_stream) handler.setFormatter(logging.Formatter("%(message)s")) @@ -99,77 +102,29 @@ def test_log_interleaving_preserves_drawn_lines(tty_stream: FakeTTY) -> None: with StickyProgressBar(stream=tty_stream) as bar: bar.add_bar("x", "col_x", 100) bar.add_bar("y", "col_y", 100) - bar.add_bar("z", "col_z", 100) - for i in range(20): + for i in range(10): bar.update("x", completed=i, success=i) root_logger.info("log at step %d", i) bar.update("y", completed=i, success=i) - bar.update("z", completed=i, success=i) snapshot = tty_stream.getvalue() bar.update("x", completed=20, success=20) - assert tty_stream.getvalue()[len(snapshot) :].count(CURSOR_UP_CLEAR) == 3 + assert tty_stream.getvalue()[len(snapshot) :].count(CURSOR_UP_CLEAR) == 16 finally: root_logger.removeHandler(handler) -def test_wrapping_counts_physical_lines(tty_stream: FakeTTY) -> None: - narrow = os.terminal_size((40, 24)) - with patch.object(shutil, "get_terminal_size", return_value=narrow): - with StickyProgressBar(stream=tty_stream) as bar: - bar.add_bar("a", "col_a", 100) - bar.add_bar("b", "col_b", 100) - - original_format = bar._format_bar - - def oversized_format(b: object, width: int, label_width: int) -> str: - line = original_format(b, width, label_width) - return line + "X" * 20 - - with patch.object(bar, "_format_bar", side_effect=oversized_format): - bar.update("a", completed=5, success=5) - - snapshot = tty_stream.getvalue() - bar.update("b", completed=1, success=1) - assert tty_stream.getvalue()[len(snapshot) :].count(CURSOR_UP_CLEAR) > 2 - - -def test_wrapping_stable_across_updates(tty_stream: FakeTTY) -> None: - narrow = os.terminal_size((40, 24)) - with patch.object(shutil, "get_terminal_size", return_value=narrow): - with StickyProgressBar(stream=tty_stream) as bar: - bar.add_bar("a", "col_a", 100) - bar.add_bar("b", "col_b", 100) - - snapshot = tty_stream.getvalue() - bar.update("a", completed=0, success=0) - initial_clears = tty_stream.getvalue()[len(snapshot) :].count(CURSOR_UP_CLEAR) - - for i in range(1, 21): - bar.update("a", completed=i, success=i) - bar.update("b", completed=i, success=i) - - snapshot = tty_stream.getvalue() - bar.update("a", completed=21, success=21) - assert tty_stream.getvalue()[len(snapshot) :].count(CURSOR_UP_CLEAR) == initial_clears - - -def test_narrow_terminal_graceful_degradation(tty_stream: FakeTTY) -> None: - narrow = os.terminal_size((30, 24)) +def test_narrow_terminal_keeps_panel_within_width(tty_stream: FakeTTY) -> None: + narrow = os.terminal_size((36, 24)) with patch.object(shutil, "get_terminal_size", return_value=narrow): with StickyProgressBar(stream=tty_stream) as bar: bar.add_bar("a", "column 'verification_1'", 300) bar.update("a", completed=50, success=50) - snapshot = tty_stream.getvalue() - bar.update("a", completed=51, success=51) - assert tty_stream.getvalue()[len(snapshot) :].count(CURSOR_UP_CLEAR) == 1 - output = tty_stream.getvalue() - clean = _ALL_ANSI_RE.sub("", output).replace("\r", "") - for line in clean.split("\n"): - assert len(line) <= 29 + for line in _last_panel_lines(output): + assert len(line) <= 35 def test_update_many_single_redraw(tty_stream: FakeTTY) -> None: @@ -182,26 +137,24 @@ def test_update_many_single_redraw(tty_stream: FakeTTY) -> None: after = tty_stream.getvalue() new_output = after[len(before) :] - assert new_output.count(CURSOR_UP_CLEAR) == 2 + assert new_output.count(CURSOR_UP_CLEAR) == 16 - clean = _ALL_ANSI_RE.sub("", after) + clean = _clean(after) assert "10/100" in clean assert "20/100" in clean -def test_update_many_ignores_unknown_keys(tty_stream: FakeTTY) -> None: +def test_update_many_includes_failures_and_skips(tty_stream: FakeTTY) -> None: with StickyProgressBar(stream=tty_stream) as bar: bar.add_bar("a", "col_a", 100) - bar.update_many({"a": (10, 10, 0), "unknown": (5, 5, 0)}) + bar.update_many({"a": (10, 7, 2, 1), "unknown": (5, 5, 0, 0)}) - clean = _ALL_ANSI_RE.sub("", tty_stream.getvalue()) + clean = _clean(tty_stream.getvalue()) assert "10/100" in clean + assert "2 failed" in clean + assert "1 skipped" in clean assert "unknown" not in clean - snapshot = tty_stream.getvalue() - bar.update("a", completed=11, success=11) - assert tty_stream.getvalue()[len(snapshot) :].count(CURSOR_UP_CLEAR) == 1 - def test_reporter_updates_and_logs_keep_drawn_lines_in_sync(tty_stream: FakeTTY) -> None: root_logger = logging.getLogger() @@ -223,19 +176,17 @@ def test_reporter_updates_and_logs_keep_drawn_lines_in_sync(tty_stream: FakeTTY) snapshot = tty_stream.getvalue() reporter.record_success("col_a") - assert tty_stream.getvalue()[len(snapshot) :].count(CURSOR_UP_CLEAR) == 3 + assert tty_stream.getvalue()[len(snapshot) :].count(CURSOR_UP_CLEAR) == 16 for i in range(49): if i % 10 == 0: root_logger.info("Processing batch %d", i) reporter.record_success("col_b") - reporter.record_success("col_c") + reporter.record_skipped("col_c") snapshot = tty_stream.getvalue() - reporter.record_success("col_a") - assert tty_stream.getvalue()[len(snapshot) :].count(CURSOR_UP_CLEAR) == 3 - reporter.log_final() - assert bar.drawn_lines == 0 + assert tty_stream.getvalue()[len(snapshot) :].count(CURSOR_UP_CLEAR) == 16 + assert bar.drawn_lines == 16 finally: root_logger.removeHandler(handler) diff --git a/uv.lock b/uv.lock index 123ac8e8a..583c9c424 100644 --- a/uv.lock +++ b/uv.lock @@ -269,6 +269,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ed/c9/d7977eaacb9df673210491da99e6a247e93df98c715fc43fd136ce1d3d33/arrow-1.4.0-py3-none-any.whl", hash = "sha256:749f0769958ebdc79c173ff0b0670d59051a535fa26e8eba02953dc19eb43205", size = 68797, upload-time = "2025-10-18T17:46:45.663Z" }, ] +[[package]] +name = "asciichartpy" +version = "1.5.25" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/41/3a/b01436be647f881515ec2f253616bf4a40c1d27d02a69e7f038e27fcdf81/asciichartpy-1.5.25.tar.gz", hash = "sha256:63a305302b2aad51da288b58226009b7b0313eba7d8e2452d5a21a13fcf44d74", size = 8201, upload-time = "2020-08-17T02:07:18.292Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/d0/7b958df957e4827837b590944008f0b28078f552b451f7407b4b3d54f574/asciichartpy-1.5.25-py2.py3-none-any.whl", hash = "sha256:33c417a3c8ef7d0a11b98eb9ea6dd9b2c1b17559e539b207a17d26d4302d0258", size = 7228, upload-time = "2020-08-17T02:07:16.386Z" }, +] + [[package]] name = "astroid" version = "3.3.11" @@ -859,6 +871,7 @@ name = "data-designer-engine" source = { editable = "packages/data-designer-engine" } dependencies = [ { name = "anyascii" }, + { name = "asciichartpy" }, { name = "chardet" }, { name = "cryptography" }, { name = "data-designer-config" }, @@ -889,6 +902,7 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "anyascii", specifier = ">=0.3.3,<1" }, + { name = "asciichartpy", specifier = ">=1.5.25,<2" }, { name = "chardet", specifier = ">=3.0.2,<6" }, { name = "cryptography", specifier = ">=46.0.7,<47" }, { name = "data-designer-config", editable = "packages/data-designer-config" }, From 3047c42ebcfcb53a3501b44a7ed9f10874f0e347 Mon Sep 17 00:00:00 2001 From: "Eric W. Tramel" Date: Thu, 21 May 2026 13:05:17 -0400 Subject: [PATCH 02/19] fix: smooth throughput panel updates Throttle active TTY redraws, sample rates over larger windows, smooth and fit chart series, bound rate history, and harden panel tests. Signed-off-by: Eric W. Tramel --- .../assets/progress-throughput-panel-demo.png | Bin 40810 -> 39636 bytes .../utils/async_progress_reporter.py | 13 ++- .../utils/progress_tracker.py | 1 + .../utils/sticky_progress_bar.py | 107 ++++++++++++++---- .../utils/test_sticky_progress_bar.py | 96 ++++++++++++++-- 5 files changed, 177 insertions(+), 40 deletions(-) diff --git a/docs/assets/progress-throughput-panel-demo.png b/docs/assets/progress-throughput-panel-demo.png index d9e56fe698eee3d4b1963fe5ef00e1ebefd6b409..fdbf6375b7c0e0e39270eeffaaf5a0b1899e48a1 100644 GIT binary patch literal 39636 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 Mp00i_>zopr0HE#Jq5uE@ diff --git a/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/async_progress_reporter.py b/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/async_progress_reporter.py index 8a5e81462..162483f9a 100644 --- a/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/async_progress_reporter.py +++ b/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/async_progress_reporter.py @@ -16,6 +16,7 @@ logger = logging.getLogger(__name__) DEFAULT_REPORT_INTERVAL = 5.0 +DEFAULT_TTY_REPORT_INTERVAL = 0.75 class AsyncProgressReporter: @@ -36,6 +37,7 @@ def __init__( self._report_interval = report_interval self._start_time = time.perf_counter() self._last_report_time: float = self._start_time + self._last_bar_report_time: float = self._start_time self._last_reported_total: int = -1 self._bar = progress_bar if self._bar is not None: @@ -70,7 +72,7 @@ def record_skipped(self, column: str) -> None: def log_final(self) -> None: if self._bar is not None and self._bar.is_active: - self._update_bar() + self._update_bar(force=True) else: self._emit() elapsed = time.perf_counter() - self._start_time @@ -89,22 +91,25 @@ def log_final(self) -> None: ) def _maybe_report(self) -> None: + now = time.perf_counter() if self._bar is not None and self._bar.is_active: + if now - self._last_bar_report_time < DEFAULT_TTY_REPORT_INTERVAL: + return + self._last_bar_report_time = now self._update_bar() return - now = time.perf_counter() if now - self._last_report_time < self._report_interval: return self._last_report_time = now self._emit() - def _update_bar(self) -> None: + def _update_bar(self, *, force: bool = False) -> None: elapsed = time.perf_counter() - self._start_time updates: dict[str, tuple[int, int, int, int]] = {} for col, tracker in self._trackers.items(): completed, _total, success, failed, skipped, _pct, _rate, _emoji = tracker.get_snapshot(elapsed) updates[col] = (completed, success, failed, skipped) - self._bar.update_many(updates) + self._bar.update_many(updates, force=force) def _emit(self) -> None: current_total = sum(tracker.get_snapshot(0.0)[0] for tracker in self._trackers.values()) diff --git a/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/progress_tracker.py b/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/progress_tracker.py index 9935d3c78..e9a3f691b 100644 --- a/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/progress_tracker.py +++ b/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/progress_tracker.py @@ -110,6 +110,7 @@ def log_final(self) -> None: success=self.success, failed=self.failed, skipped=self.skipped, + force=True, ) return if self.completed > 0: diff --git a/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/sticky_progress_bar.py b/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/sticky_progress_bar.py index eaa9c72e9..27bffa275 100644 --- a/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/sticky_progress_bar.py +++ b/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/sticky_progress_bar.py @@ -10,11 +10,12 @@ import time from dataclasses import dataclass, field from threading import Lock -from typing import TextIO +from typing import Sequence, TextIO import asciichartpy _ANSI_RE = re.compile(r"\033\[[0-9;?]*[a-zA-Z]") +_CONTROL_RE = re.compile(r"[\x00-\x1f\x7f-\x9f]") _RESET = "\033[0m" _BORDER = "\033[38;5;39m" _TITLE = "\033[1;38;5;81m" @@ -33,10 +34,16 @@ ] _DEFAULT_PANEL_HEIGHT = 16 _MIN_PANEL_HEIGHT = 9 -_MIN_SAMPLE_INTERVAL_SECONDS = 0.25 +_MIN_TERMINAL_WIDTH = 30 +_MIN_REDRAW_INTERVAL_SECONDS = 0.75 +_RATE_SAMPLE_INTERVAL_SECONDS = 2.0 +_RATE_SMOOTHING_WINDOW = 3 +_MAX_RATE_SAMPLES = 7200 +_RATE_FORMAT = "{:6.1f} " +_Y_AXIS_RESERVED = 12 -ProgressUpdate = tuple[int, int, int] | tuple[int, int, int, int] +_ProgressUpdate = tuple[int, int, int, int] def _visible_len(text: str) -> int: @@ -54,15 +61,25 @@ def _color(text: str, color: str) -> str: return f"{color}{text}{_RESET}" -def _average(values: list[float]) -> float: +def _sanitize_label(label: str) -> str: + return _CONTROL_RE.sub("", _ANSI_RE.sub("", label)) + + +def _average(values: Sequence[float]) -> float: return sum(values) / len(values) if values else 0.0 -def _compress_series(series: list[float], max_points: int) -> list[float]: +def _smooth_series(series: Sequence[float], window: int = _RATE_SMOOTHING_WINDOW) -> list[float]: + if window <= 1: + return list(series) + return [_average(series[max(0, i - window + 1) : i + 1]) for i in range(len(series))] + + +def _compress_series(series: Sequence[float], max_points: int) -> list[float]: if max_points <= 0: return [] if len(series) <= max_points: - return series or [0.0] + return list(series) or [0.0] compressed: list[float] = [] count = len(series) @@ -74,6 +91,32 @@ def _compress_series(series: list[float], max_points: int) -> list[float]: return compressed +def _expand_series(series: Sequence[float], point_count: int) -> list[float]: + if point_count <= 0: + return [] + if not series: + return [0.0] * point_count + if len(series) == 1: + return [series[0]] * point_count + + expanded: list[float] = [] + source_last_index = len(series) - 1 + target_last_index = max(1, point_count - 1) + for index in range(point_count): + position = index * source_last_index / target_last_index + left_index = int(position) + right_index = min(left_index + 1, source_last_index) + weight = position - left_index + expanded.append(series[left_index] * (1 - weight) + series[right_index] * weight) + return expanded + + +def _fit_series(series: Sequence[float], point_count: int) -> list[float]: + if len(series) > point_count: + return _compress_series(series, point_count) + return _expand_series(series, point_count) + + @dataclass class _BarState: label: str @@ -104,18 +147,20 @@ def record_update( self.failed = failed self.skipped = skipped - should_sample = elapsed >= _MIN_SAMPLE_INTERVAL_SECONDS or bounded_completed >= self.total + should_sample = elapsed >= _RATE_SAMPLE_INTERVAL_SECONDS or bounded_completed >= self.total if should_sample: delta_completed = max(0, bounded_completed - self.last_completed) - sample_elapsed = max(elapsed, _MIN_SAMPLE_INTERVAL_SECONDS) + sample_elapsed = max(elapsed, 0.001) rate = delta_completed / sample_elapsed - self.latest_rate = rate self.rates.append(rate) + if len(self.rates) > _MAX_RATE_SAMPLES: + del self.rates[: len(self.rates) - _MAX_RATE_SAMPLES] + self.latest_rate = _average(self.rates[-_RATE_SMOOTHING_WINDOW:]) self.last_completed = bounded_completed self.last_sample_time = now def average_rate(self, now: float) -> float: - elapsed = max(now - self.start_time, _MIN_SAMPLE_INTERVAL_SECONDS) + elapsed = max(now - self.start_time, 0.001) return self.completed / elapsed if elapsed > 0 else 0.0 @@ -147,6 +192,7 @@ def __init__(self, stream: TextIO | None = None, *, panel_height: int = _DEFAULT self._wrapped_handlers: list[tuple[logging.StreamHandler, object]] = [] self._panel_height = max(_MIN_PANEL_HEIGHT, panel_height) self._start_time = time.perf_counter() + self._last_redraw_time: float = 0.0 @property def is_active(self) -> bool: @@ -159,9 +205,10 @@ def drawn_lines(self) -> int: # -- context manager -- def __enter__(self) -> StickyProgressBar: - if self._is_tty: + if self._is_tty and shutil.get_terminal_size().columns >= _MIN_TERMINAL_WIDTH: self._active = True self._start_time = time.perf_counter() + self._last_redraw_time = 0.0 self._wrap_handlers() self._write("\033[?25l") # hide cursor return self @@ -177,9 +224,9 @@ def __exit__(self, *args: object) -> None: def add_bar(self, key: str, label: str, total: int) -> None: with self._lock: - self._bars[key] = _BarState(label=label, total=total) + self._bars[key] = _BarState(label=_sanitize_label(label), total=total) if self._active: - self._redraw() + self._redraw(force=True) def update( self, @@ -189,26 +236,27 @@ def update( success: int = 0, failed: int = 0, skipped: int = 0, + force: bool = False, ) -> None: with self._lock: if bar := self._bars.get(key): + now = time.perf_counter() bar.record_update( completed=completed, success=success, failed=failed, skipped=skipped, - now=time.perf_counter(), + now=now, ) if self._active: - self._redraw() + self._redraw_if_due(now, force=force) - def update_many(self, updates: dict[str, ProgressUpdate]) -> None: + def update_many(self, updates: dict[str, _ProgressUpdate], *, force: bool = False) -> None: with self._lock: now = time.perf_counter() for key, update in updates.items(): if bar := self._bars.get(key): - completed, success, failed = update[:3] - skipped = update[3] if len(update) > 3 else bar.skipped + completed, success, failed, skipped = update bar.record_update( completed=completed, success=success, @@ -217,13 +265,13 @@ def update_many(self, updates: dict[str, ProgressUpdate]) -> None: now=now, ) if self._active: - self._redraw() + self._redraw_if_due(now, force=force) def remove_bar(self, key: str) -> None: with self._lock: self._bars.pop(key, None) if self._active: - self._redraw() + self._redraw(force=True) # -- handler wrapping -- @@ -242,7 +290,7 @@ def wrapped_emit(record: logging.LogRecord) -> None: with self._lock: self._clear_bars() orig(record) # type: ignore[operator] - self._redraw() + self._redraw(force=True) return wrapped_emit @@ -264,8 +312,16 @@ def _clear_bars(self) -> None: self._write("\r\033[2K") self._drawn_lines = 0 - def _redraw(self) -> None: + def _redraw_if_due(self, now: float, *, force: bool = False) -> None: + if force or self._drawn_lines == 0 or now - self._last_redraw_time >= _MIN_REDRAW_INTERVAL_SECONDS: + self._redraw(force=True, now=now) + + def _redraw(self, *, force: bool = False, now: float | None = None) -> None: """Redraw the chart panel. Caller must hold the lock.""" + if not force: + current_time = time.perf_counter() if now is None else now + if self._drawn_lines > 0 and current_time - self._last_redraw_time < _MIN_REDRAW_INTERVAL_SECONDS: + return self._clear_bars() if not self._bars: return @@ -273,6 +329,7 @@ def _redraw(self) -> None: for line in lines: self._write(line + "\n") self._drawn_lines = len(lines) + self._last_redraw_time = time.perf_counter() if now is None else now def _format_panel(self) -> list[str]: terminal_size = shutil.get_terminal_size() @@ -315,8 +372,8 @@ def _format_header(self, bars: list[_BarState], now: float) -> str: ) def _format_chart_lines(self, bars: list[_BarState], inner_width: int, chart_height: int) -> list[str]: - max_points = max(2, inner_width - 12) - series = [_compress_series(bar.rates, max_points) for bar in bars] + max_points = max(2, inner_width - _Y_AXIS_RESERVED) + series = [_fit_series(_smooth_series(bar.rates), max_points) for bar in bars] max_rate = max((max(points) for points in series if points), default=0.0) chart = asciichartpy.plot( series, @@ -324,7 +381,7 @@ def _format_chart_lines(self, bars: list[_BarState], inner_width: int, chart_hei "height": chart_height, "min": 0.0, "max": max(1.0, max_rate), - "format": "{:6.1f} ", + "format": _RATE_FORMAT, "colors": [_CURVE_COLORS[i % len(_CURVE_COLORS)] for i in range(len(series))], }, ) diff --git a/packages/data-designer-engine/tests/engine/dataset_builders/utils/test_sticky_progress_bar.py b/packages/data-designer-engine/tests/engine/dataset_builders/utils/test_sticky_progress_bar.py index bc7695169..5070e5f54 100644 --- a/packages/data-designer-engine/tests/engine/dataset_builders/utils/test_sticky_progress_bar.py +++ b/packages/data-designer-engine/tests/engine/dataset_builders/utils/test_sticky_progress_bar.py @@ -8,13 +8,20 @@ import os import re import shutil +from collections.abc import Iterator from unittest.mock import patch import pytest from data_designer.engine.dataset_builders.utils.async_progress_reporter import AsyncProgressReporter from data_designer.engine.dataset_builders.utils.progress_tracker import ProgressTracker -from data_designer.engine.dataset_builders.utils.sticky_progress_bar import StickyProgressBar +from data_designer.engine.dataset_builders.utils.sticky_progress_bar import ( + _MAX_RATE_SAMPLES, + _RATE_SAMPLE_INTERVAL_SECONDS, + StickyProgressBar, + _BarState, + _fit_series, +) CURSOR_UP_CLEAR = "\033[A\033[2K" HIDE_CURSOR = "\033[?25l" @@ -34,6 +41,12 @@ def tty_stream() -> FakeTTY: return FakeTTY() +@pytest.fixture(autouse=True) +def fixed_terminal_size() -> Iterator[None]: + with patch.object(shutil, "get_terminal_size", return_value=os.terminal_size((80, 24))): + yield + + def _clean(text: str) -> str: return _ALL_ANSI_RE.sub("", text).replace("\r", "") @@ -61,11 +74,21 @@ def test_hides_and_shows_cursor(tty_stream: FakeTTY) -> None: assert output.endswith(SHOW_CURSOR) +def test_tiny_terminal_falls_back_to_no_panel(tty_stream: FakeTTY) -> None: + with patch.object(shutil, "get_terminal_size", return_value=os.terminal_size((20, 24))): + with StickyProgressBar(stream=tty_stream) as bar: + assert bar.is_active is False + bar.add_bar("a", "col_a", 10) + bar.update("a", completed=5, success=5, force=True) + + assert tty_stream.getvalue() == "" + + def test_renders_bounded_throughput_panel(tty_stream: FakeTTY) -> None: with StickyProgressBar(stream=tty_stream) as bar: bar.add_bar("a", "column 'a'", 100) bar.add_bar("b", "column 'b'", 100) - bar.update_many({"a": (10, 10, 0), "b": (20, 20, 0)}) + bar.update_many({"a": (10, 10, 0, 0), "b": (20, 20, 0, 0)}, force=True) assert bar.drawn_lines == 16 panel = "\n".join(_last_panel_lines(tty_stream.getvalue())) @@ -77,17 +100,53 @@ def test_renders_bounded_throughput_panel(tty_stream: FakeTTY) -> None: assert "ā•°" in panel -def test_panel_height_stable_across_many_updates(tty_stream: FakeTTY) -> None: +def test_control_sequences_are_removed_from_labels(tty_stream: FakeTTY) -> None: + with StickyProgressBar(stream=tty_stream) as bar: + bar.add_bar("a", "col\x1b[31m_a\nsuffix", 100) + bar.update("a", completed=10, success=10, force=True) + + clean = _clean(tty_stream.getvalue()) + assert "col_asuffix" in clean + + +def test_rate_samples_are_bounded() -> None: + state = _BarState(label="col_a", total=1_000_000, start_time=0.0, last_sample_time=0.0) + + for index in range(_MAX_RATE_SAMPLES + 5): + completed = (index + 1) * 10 + state.record_update( + completed=completed, + success=completed, + failed=0, + skipped=0, + now=(index + 1) * _RATE_SAMPLE_INTERVAL_SECONDS, + ) + + assert len(state.rates) == _MAX_RATE_SAMPLES + + +def test_sparse_rate_samples_span_chart_width() -> None: + fitted = _fit_series([0.0, 10.0, 5.0], 7) + + assert len(fitted) == 7 + assert fitted[0] == 0.0 + assert fitted[3] == pytest.approx(10.0) + assert fitted[-1] == 5.0 + + +def test_frequent_updates_are_redraw_throttled(tty_stream: FakeTTY) -> None: with StickyProgressBar(stream=tty_stream) as bar: bar.add_bar("a", "col_a", 100) bar.add_bar("b", "col_b", 100) + bar.update_many({"a": (1, 1, 0, 0), "b": (2, 2, 0, 0)}, force=True) + snapshot = tty_stream.getvalue() for i in range(50): - bar.update_many({"a": (i, i, 0), "b": (i * 2, i * 2, 0)}) + bar.update_many({"a": (i, i, 0, 0), "b": (i * 2, i * 2, 0, 0)}) - snapshot = tty_stream.getvalue() - bar.update("a", completed=50, success=50) + assert tty_stream.getvalue()[len(snapshot) :].count(CURSOR_UP_CLEAR) == 0 + bar.update("a", completed=50, success=50, force=True) assert tty_stream.getvalue()[len(snapshot) :].count(CURSOR_UP_CLEAR) == 16 assert bar.drawn_lines == 16 @@ -109,7 +168,7 @@ def test_log_interleaving_preserves_panel_height(tty_stream: FakeTTY) -> None: bar.update("y", completed=i, success=i) snapshot = tty_stream.getvalue() - bar.update("x", completed=20, success=20) + bar.update("x", completed=20, success=20, force=True) assert tty_stream.getvalue()[len(snapshot) :].count(CURSOR_UP_CLEAR) == 16 finally: root_logger.removeHandler(handler) @@ -120,7 +179,7 @@ def test_narrow_terminal_keeps_panel_within_width(tty_stream: FakeTTY) -> None: with patch.object(shutil, "get_terminal_size", return_value=narrow): with StickyProgressBar(stream=tty_stream) as bar: bar.add_bar("a", "column 'verification_1'", 300) - bar.update("a", completed=50, success=50) + bar.update("a", completed=50, success=50, force=True) output = tty_stream.getvalue() for line in _last_panel_lines(output): @@ -133,7 +192,7 @@ def test_update_many_single_redraw(tty_stream: FakeTTY) -> None: bar.add_bar("b", "col_b", 100) before = tty_stream.getvalue() - bar.update_many({"a": (10, 10, 0), "b": (20, 20, 0)}) + bar.update_many({"a": (10, 10, 0, 0), "b": (20, 20, 0, 0)}, force=True) after = tty_stream.getvalue() new_output = after[len(before) :] @@ -147,7 +206,7 @@ def test_update_many_single_redraw(tty_stream: FakeTTY) -> None: def test_update_many_includes_failures_and_skips(tty_stream: FakeTTY) -> None: with StickyProgressBar(stream=tty_stream) as bar: bar.add_bar("a", "col_a", 100) - bar.update_many({"a": (10, 7, 2, 1), "unknown": (5, 5, 0, 0)}) + bar.update_many({"a": (10, 7, 2, 1), "unknown": (5, 5, 0, 0)}, force=True) clean = _clean(tty_stream.getvalue()) assert "10/100" in clean @@ -156,6 +215,21 @@ def test_update_many_includes_failures_and_skips(tty_stream: FakeTTY) -> None: assert "unknown" not in clean +def test_remove_bar_redraws_panel(tty_stream: FakeTTY) -> None: + with StickyProgressBar(stream=tty_stream) as bar: + bar.add_bar("a", "col_a", 100) + bar.add_bar("b", "col_b", 100) + + snapshot = tty_stream.getvalue() + bar.remove_bar("a") + + new_output = tty_stream.getvalue()[len(snapshot) :] + assert new_output.count(CURSOR_UP_CLEAR) == 16 + panel = "\n".join(_last_panel_lines(tty_stream.getvalue())) + assert "col_a" not in panel + assert "col_b" in panel + + def test_reporter_updates_and_logs_keep_drawn_lines_in_sync(tty_stream: FakeTTY) -> None: root_logger = logging.getLogger() handler = logging.StreamHandler(tty_stream) @@ -176,7 +250,7 @@ def test_reporter_updates_and_logs_keep_drawn_lines_in_sync(tty_stream: FakeTTY) snapshot = tty_stream.getvalue() reporter.record_success("col_a") - assert tty_stream.getvalue()[len(snapshot) :].count(CURSOR_UP_CLEAR) == 16 + assert tty_stream.getvalue()[len(snapshot) :].count(CURSOR_UP_CLEAR) == 0 for i in range(49): if i % 10 == 0: From 61125a02d0d8a4466eeef85f93619673d7e06c99 Mon Sep 17 00:00:00 2001 From: "Eric W. Tramel" Date: Thu, 21 May 2026 13:37:53 -0400 Subject: [PATCH 03/19] fix: align progress panel metrics Render the progress legend as a stable table with live token-rate columns, attribute model usage to active generation columns across async bridge boundaries, and cancel the async scheduler cleanly on KeyboardInterrupt. Signed-off-by: Eric W. Tramel --- .../assets/progress-throughput-panel-demo.png | Bin 39636 -> 40771 bytes .../column_generators/generators/custom.py | 39 +++- .../src/data_designer/engine/context.py | 15 ++ .../dataset_builders/async_scheduler.py | 98 ++++++---- .../dataset_builders/dataset_builder.py | 22 ++- .../utils/async_progress_reporter.py | 61 ++++-- .../utils/sticky_progress_bar.py | 177 ++++++++++++++++-- .../src/data_designer/engine/models/facade.py | 18 ++ .../engine/models/usage_events.py | 54 ++++++ .../generators/test_custom.py | 58 ++++++ .../dataset_builders/test_dataset_builder.py | 24 +++ .../utils/test_sticky_progress_bar.py | 43 ++++- .../tests/engine/models/test_facade.py | 28 +++ 13 files changed, 562 insertions(+), 75 deletions(-) create mode 100644 packages/data-designer-engine/src/data_designer/engine/models/usage_events.py diff --git a/docs/assets/progress-throughput-panel-demo.png b/docs/assets/progress-throughput-panel-demo.png index fdbf6375b7c0e0e39270eeffaaf5a0b1899e48a1..550d8d3d92ef89aa3b5ba83c851d4aabdd736f9e 100644 GIT binary patch literal 40771 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 GgeCxUt@Stn literal 39636 zcmeFZWmp}}wl&HdA%OrPxD$eV@ZbT02X{|!hu{u@g$H+ccXtRH+}#%L?y`Wd3FPc| z?|b&y-?`8IdHdH|-CfnyRW-*PbJVQ%l@u3*hsA=0f`Wn<7WyO&1@)W?3hJ+wmrsB* zb6#?c#UlUMM0@VL>k-zlD)=c;y589a;^Aa4BGKaGWh8eZoQIzud4oYuAASnG3^jke z4il2oW(3!lpGSc7aK?>oO>5NO@N>)MrY--(Uk@iGY2_HjGRboF6$Bd|3&Mj^I?2m~ zgsW3xn1;#$^NG=DH0ppe5LdKFo%!?MeEMf2KJSyQW-sL$ zI|PEZ-uki55@aJbG@wy#Z24GW2l=Kn{P+}VH45J+5{GUFjIN!KZz`@{E2BF{4<53< zLBM1?pzc4oZ(qZeA3$596u-)Ff_mKI5*Jkdo?}9L?t!_3xaZy_CR=D=*$(&>T$9 zad+1=G!#+nO!I!dXZ``_wyX@DYGU;2w9)Ct>*5N2@vgz@_D6iYv6LaAXz=hbJfd<&+Stx5s9r$xy{Tc z04Z@jN8ogJCXEtD;k7?zTN=KUabHfI#rOo4)FEf_van_ShU;)YN|M$~W+bTdOKSaP zyejpc+?4h0Dpb3RPb88!NI;AX#{I6@`KfF+ON^p<2?2x*Q*jhqhW$PF#I z6OyLE($dnvY`6M$UT?oUJp7#D{HCrU=Ib#xiCC6&j(Zdw7Ta{!l$xBFnwQAf=&+5= z_RiX(TzUUM3BI1#rPg>jcj|mSMU4O}>*b+qzy8p$yGR;&UiKO8`*L8;hqnLtA$9{D zB&jeFoFc7KUcNEnTD(9m<&Ls3-4NwpmnEGh5D+dm%TfQ#UcAQ23R6axT3>HHIY0j$ zAz@lU+RRj*qo}3m8wDxTV7WU+%5q#oRsww6nmAd^H87(%U9UihNcia6u6E$?m}6YF>e(j8Fht%A zX))z;NMyL(%Fo2n{VrfUsHv5^I%U7S-dxzp%umI_JzaxrBD1ninM_Ze?#wl5msIXf z<8zmCADxQAixs@lB8`nv8Qv`YtlLP&1KZitqf%jDyE8a%I}~D7Ys}96V#pExWRpcv zT>KW8Z8I!|nHkOsQc@NAI(?cYeyEg2sH-oOR&RJtb|pQ!!FfH`)je~gCdNBfm{%G| zOA@%;C!EAJ-@ZaHp|P*CKLbW5Ek=#A2~xRg{}t zE>(YE=c=}7BJMtW7s>bfzE5*sKJ}vXXkQ^VX2eRkdrwH{JkZ}nsL|Bh(??B3CggMN zbk{FtV1TKn#`%Ua-vNFV+F-83t528Pwd2~eFADMQQcyW}>Q2S&)vH%ZRPw_UZDzv0 zo}QjLU>WzlxkktB+6^N{CI(t6Dh`*EpXd8s_VzVFc(cyu%%$2TSzm*axZ$Fo<=V4gsSx&$1O?0ddhIOiqemZuKi>df zZzo@@N`FlRUS3E>A4q&3b1AHWJn#%>x&4cw0FkH=+`BysP1ck9u1WSY_b=^E9x{N$ zeTRtzdf3LwLT7_1G9nE5aWTPaVZ=BEJYb&FaZ89BZ~OExSleCIW7ULAKV1 z2ew$Tm!;^PP%zMOD5S#+A~ynMYXGxfN{TRywL z-ps`C6Rg}r7-e2|>6E9h2GTRpB~9+bFdN_9FydE4ByE;8s;F{u@041!5Ot!Eds2Rr z2ssc@J$-q()k^~I;o*zd0Fdvq*nR5K9DXq$}`;PUI?-vf)nI|i_Fz>2rQ!o>na_4-!;A zrM9=OHi>#+BEmOQ12uq8RGBDxDbe1ed_S3-T@iuF^nqZX*Gk7^>&FScSR5wZiCt^U zr6^o1^D^x^w>IiQhiY~<#1wJdZ1z+A^-7Tflff0l0OQWgOI-`r^PL0S(zGP6X7&|K z`aAFWfy!^b1ETl+FKOmu|Jkm__ARn><}7gnjvAIFqhSVYPf0NypqiS$x)X4=}?&~SJuDJjI1r0W|ihN{iq z%2uJ3tT8b+OYpVeVi~dDYo}Ktc!*>e>g%)CIh5-g5OloFtL7H^!tUdL&`|4lv0UYR z{ zCwuI{h)1_`DsQw9C%ll5_{JpOrtVvCOUu}2X%c6MG{D>sezsSnc?RV|m#&`BM{`DD0z+ zMn3N)>_q@O0LkY4T9@xzpE^(V41 zyBzh7o0zCnRiILFSS)CKd#-eP!$UvALj|h$&vit@n{J*GYx)*uP=*eR*63$PgHf6o z5pzkt=FC929CI9bpc2RD;jzR1!T(#*iX%EMlm4WBb1Ud#z&&TECxAy4TSN#>j{Cb$ zV~_J)BF_1xjN`9BGxesG*Hrh@$Srn1P+cC6Mw;Co)4rN)cSUD)ajBL;=`a7&Wx?sX zs*v;jc`7!)Q{XLz>FI3{aAm{gWMpItU#Krxd?0rH%d%#c@Y?o#XRfG(M}{JR#Yma| zzKd`W)_?Bdiqq^BCe)r_zyHy?s;#T*-M;SmuxA(2-n z!uo>^D*Tih^%f1G!}~TIJpD}$QxJTE9Q&G@x=PcXsjQ}u5(885vkFS|aM!aTYAPxe zx(f>r6#pV$jmmtDv97W>0IrW<{S0!*C3yMXX*8iI1j~W(iOO)>csuK|Yg<}cT&v9X zmUgvFzY-TE2LqV7V=!4@6y+bu>T>e&j;A$_k?HeYh|TJf((~t@sRQ!PNh`15?Jt<< znuod~cvC(#+S?z_=A@!$fquO$geNBQ%1!!6QQm_)b!V1z>!C-hD3H}d(&jo}`qi3v z{yt>+=)o@~OWv?LHQ0Lv#7w!eYE-Y$iJ#uxOy8|7rfM%dX+JXmrn4Xk^VB=A)X=2t z%h?KVZwZ{WWs9gCi)n#v6CR`Lo{YldM@FF?o$Dg=K zKN_y5GiT_36K4Hx2o%(Rj5q&RvD^P&>3t6%y^|3;wUDzu7BtnZGAE)RHB^57&@&%B zC8KMG2cB$t>&iseq;pk(j_J)`p=&QZT$Xe+-#qd>Yor1{Kg}hM&+GW2QZ0#}n2ejC zA)|o)a1WHV6*&w$DJaPsZ{MRfkyh`&!8?c6l!O$A#N z!3|l#zQ^m-^dDret`0tJ@0vj0d7fx7t{|KkHcdVFrFn5jF35gxWYCaF+N zcv*U@#AsHVXXzaqn2PYzE0^;?X_TO1vDJ_xHKVnGYmcs#y*+SnZ$2N1NpXLF1>*Fj zYgIhx+SU)Z`7Uc}DtRk~8PjNp+y>+xVlkEn2tfL7xjHr{(iMqi{eyb?N&}6PAGp^} zTzsF)kY+2DThCdpPQ_v-(clqU^RUN_t0&AM;J@;@!I9W{iEC0&lIDe?Vs~vim=;_d z>dC}3c89?vD62Jz%( z+(FV^-NOY1RK0zl$Hru9RQJwD%LhpZ-lfg^I?KG8!YAVMqZqRaMQyEO6_cT%rJ=n2 zDBDs}6_G92$1`E&i<^VTvnzqb%Q%azvcD}WmIsWJ+jKOD??xa+a>rzb#N|fboBqJ> z=xnQ3n;zgDZPGKpB$9*+2uMk{N9KB}YrZZ@L}WMTM%Y);G`WJex9LuRmzy`0@7faKlfE#FQ z*`XDrF3P0+1-^U}n?BRiX!j*ZF9CIi=AU!&XLTHK5`3>Ig# z^pqPJiDh)xJK@SPRv@-Fdfc{0!tH=ngoO`{hFPSI$jB*}jSLa7&Q87xO?IX{1;&S> zHU;(-tj4GMK)X{C)>ZbnJ_EyKa9LfHN!|E4zSsMh&%8eF?)3_Kw!EFK3xlMKv`0tB zaZCoY!@oN2cj@8E&O>IHNRLz%o#0Y?LJ+fY(mxaTg8fOcTE=Xtb<4A;8%k?`xq#8Z@WdLkEVf}pqm7(!^IU5 zcGYO`G)g6^nBK()gKs@7C>-v`g+xV6RY_lc0Hlf5u|hjD%eN8p4x?}Tu1W7siX{r6 zfJ()pX4WUlY;WGRZS^QtDLo?tcY3D0R6i#*wO6||73^B=%tFQzb$+e*QI3BqhtDxh zS8R24b=W0cP=KEs)?V5$EsW1G4xf8CZN8DVR#IMme5z}HG`%L(-AO=qyzZRtwMXj( zck?zHF)_b_-mkP@5^+_=Qti32vf%-6aq3OhMe-)uKYH`2+0Ry1R^)5Fd=$*ysW-gg za5`UH_Kuc_MANk>wQ_MWnlF7GAmDrjSxsQ1@imTna<{LAx zHV}C!T!Z?{RiC3N;WHstkR<;0lxEytt6_Lp7@pqWrBm=;X!%@!Gmz`DHOucC2TEK-%0%^`7B+&W_pHOZHa zZohJ2*t?m|dTwy({^Y0+D~766&ZI~sx*1ZusKmbwI2^a>M37PC=9f`Nj1);b502fr z>0Tv#dOf=?O|<3pIJnrKcQaNSD+AyEQcB_$k}|-pN|G|24{cKY{&i6ELN4}Sok zdTBbxue+appv&M8`mnpJyCR|KvghWf$g`=o8Hs_6nWCn;Ga35#wdaDiw>nQ?EN!hz ztI8Jd=9-)LR`WXda|}>m+l!KdxIDhCPWPSTbLHgaoNX;KS$%10co76+VN5L+u*e6| z*4c!3$6{>(*hEgA6JjQcIhm7}hHJNr#@&Fvu{%;w^L0d{iD^vs^6PB9bJ8gN?zCdf zhbd8EVP`QB1Q*E4+;z0(^Lmas03C!OR|Q+b)neF`2;MTrI2HkBWF$ct%qaK?;w;9x zUuNr-45x#B^itinbU47N)61&|Jtc6IHF;I4%w#e=nOVSex4$OPSKq)Pp4j-)m5iF2 z(@Ijqgj(#D4;591RFECsxe7KwLh899#GK(&CBmZ;QMFkF8|9H#y!U8a+TP z&b8l-AQxeZJQ$JLh3!j7O{>+|ENnh^&qTL`k>=ZaSd2H@ie8(MF`KRx-xiy`c#Ub_ z-ZHzBs*@cci)DKRqr$PLGU*JQ%$&PAK34IdY3%M($!u~wwHiCwxd_T#E1034&s^Us zF#Q^K#iklw!LrYyEG~LJv&>G*O!Fl(3)AjWSximJ_pSAGohLQk`RNd~J?@Q-e^riB z`Iz-qsG>5L#&E!G;8JX~n0%zEsjkBsBqAt@Ys5e{PaZ8kNlDCzQWU@rTJ^@;D|j=6 zl(r&aJ=$l1n5@-mD%-(>8rZpU3ZzO(SKBO7Jk2San=t|5C@Rrd=#VA}9w~&~nbozm z2D{6;fDmq&NU1UsU;6+wy2b`}xVRV@6famC8#?rCnTH z)LmR$!HHgdu_$P0E)W#k>phQ?AD`cxXHTPPR~YI{9qoAjwB6Sl9h1Gex{AiY9@1w- z^Pgy~iQpGCq3%mJ>q4j1khnhN3re~7-}#;;jd+BMqMA-E=*N+n-Q5B7x7ZA>Ha{<( zNs*$U(8|f7F0b|R^0q4FyH8i<h4hYQbW}9clSBek;c5;bOyIfBasx{&5wXu>{&SPOEN<$3B?)V-RUS6FdW5bI~a z-1?Z%#E%JH|0#3G(vP;LnM8kP*2+%SWO+Qv|U9hPNceKU|*Esu@UIaaqjwh zPVr_X>y_Y=St4z3Uq9sH?2>K%i=D|#;HRZg5dLP$R|%sTrv)6E^{L~iyFJc&hq*`-_Vc6G z?VY{PStHMOGt}7Xo~t(~ZH;;R(VH1i(9)j2Tavq(kt7xtJ-j8g6UvY-E48?~K4WAu z5YLbXNL;TVx?%b4Qco*enF59sF?{Q_8pMH7)r!~yo=K=Rte1S!p} z=n&Udg`SLpCyJVvxt)S}thh08RuKjhZFJbcj6ueRW}Q#R=Wb`_6^HJE3wJm)@2-BQ z%bVlG329k!{t*rIxLonNI)bX;)`6dw^cop!#M(yHn|HOvt8EMKr$Wl`fD`E2#Y<-m z!tl8Vd5~tXU)$Cab5W4(zc7GQKM1r827tbXM})o11-_D1##V zIowriJ8nMyl;pIeFpOWHC8&`kKsJt5e$X*!m%InIS9KGr2!i4&qZn=3$;of8=3Ko> zk8|?!M1+O=k~lLaEqJEWGIM!fULqo^>`~xMS5cCY*&UsnU#~7()F>3rotzzEuo{1H zYKhthk7#nW>)tkj&8S1-AvZ^3agZk0biLU|?DScou9n$+U|M)5MPakmSVJJJx2?a9 zsyxW1D=PYXyZtWr>o-od_zve9v8}F6g8{1WmBFJhaj*5owaK*}e$$BgR{|9U!5gFD zs=4bxCkZhbdDE4#sXicHFrqyEl_J4Em1ZaxJVs4-c>YW+MyZ~LnQ^*E6ea+Ryku$z zzykLt8jT0iMmjpgyh6iOEAH-3IK51@N7(@|RHTx(Ow&Dt(ZHx>gQTm4?z@BGT@IkY z5GUQAF|Xp^Eq^l{$Sj0KD3kP~DbUUwx8ltUiqlx>(CubgbcV$tDMGKR*0(kowu_F@ z6ty-a#9D;^z7!&#?3d@_sT)8>FKs&1zE415hhAC>Yk#Qpe6tCAo`JA2&o=+N86&_Q zu2Psu7CkTdNv1C^(*lkxH>{=X!4x)_x96S1Nv@L5xV2w~(1a`=_sDW~YPuB3hGVo5 z5vKL3(lG7*3RGL2k86k*OG%yWtk2jC%K~_~i^L+~!It%C-8nvd9G1S3QKi8g0P9(z z-&x*KjQvv71eQN_znYTJ4q)Gq(b7&-T2szHd$y*_-vfug^h>u)q2Q*DmuY)Tf1A$A z1J4r;n^ogAvn(B8{ATacbT&^+)$uYr1ZXt`k2?C+R)mCw)#|=^Cvj$GW~wz=+d`rj z*`}3K_oXU`gkZ>~kKLPE!)@I!t&0JQ3!>FH#b5(g{KVoI5gbsahkIA{ZoJ{t5+=<2 zz;7CAnWUw$I0jj0Y40X|8QPETrpo{vj)7Q))-{?R>M8hnZ$8DF6y=_A_oj$|K5}X4 z8uHYRpTBNcCrVH%MEY=z#W&&U4GcD6k6Hw%GjD{}2VN4_yWFB^2HkQ0s3#%`!$iHg zo%dL9ei7t+fw4W+UhkxK z$1J<)q8k7U+hkL=629L~V}4VM&|WZ!Cx`8x56ECj!^6Y;VRD?D^LA#c7iQ$7{2Gjiit@jnl2OwH+@c`dX-{9@b zA9C;3Iabh)3Ogtm*8{LR{qldBP=T~AVgP^lu4BFtJbhQW*sLP<`cvQQLRC3PNebkJP#(EEBgtu=Z1wHI+-^Pd_cA~IMguM6vy zl%@dtJ;A7LaI_g4m5mAsk&=-ssVXCnjqK^_Qc_W>_x(X*O+&1+D&X(im2^p`C8SVa=jmc%5Uwyr*D1*#IVEyIuu&}1P zTKvfYrS1lfIjiytAh-sGhW)O7@SsAd%5V_~mSIXeoFT07DUMbBfa%CmR2YPH?9qt3 zuR^9KvSke&XDau#xJneC1L6vsjXv$7(v+2TxgZ?w0U>AFC|rG^@s>q~>blACe$^5K zq026>a-!HI>vPDAm;b9Qw^orZEj{#3GO3DMM!NVbqiKj=x5Nc&Ay1m!ui$jiByFsr zQ5?G{3y}!46MjD$lgdJm{KR*1F}m-&Rp(%whX4k$rC@uK>r-b_Yu_}KORZdk-$<-@ zu{w|1Ji2k2(*@Q587j`$<1A;as@g0990K0)uCY$BH>aV`3EHU2_L!qRj?brOk!ZGY za5625%z4!R3e5&=b4k=m~ES-$2@~T4!$G)UsAQsjvnOmJ39vt3A-P!!u`sQsyZms(9m6jwO3LMC(7gPJfQPLU`Msgmo5dCLV;b~ZpEgpqFO}T#H8Wy(x{#tywp+_Q%TX)rKVQt zP9D!)u&4o~vWhB`ZGE$uv^dhJp1$t>%d1lrk?wuW&Csx>{VCUUtrw1btt3Yl7Thk{ z^>&wRS9i8*k$~k+RGtFroWD4toUDVu+R$>A$+Y6;Hr-fRPaq*Z>ir^%ZUoN_we`(w=RCO^N1@dV1zuFek}bXIoOrh|ZzP;R;Q#V&9f0 zs$l9!?TY!nSH7oDGqMNQ^R8lvl#YBF=A;BUmVvpXkR+Ew--4k!aAE{i|GF)x;ffL?-Q=ZYN$*)TdG}o??6Q8kp&Y zolw`zuZT*P6${=6;UAxVC#pxG=f>YXB24zw4^*xH znDzHiClr!2by=DMOIhSvn!w%S=nyM0>r`EyATjLAlDh>d%U3<2J+`J`i;aa_icjzl zW#H9tMO(0$q}6MkBD7wRl7w|>(osq!594toywNgwxMvV*tb|BaMrI^4VHf5u%^P74 zTy}?;AW)?L-68;ot9mlDPsLW>P+y=aAXjQ;nl zYqz8Wz_P>b(pc(i+@0y-DdTSiZ=O}x#zS^7cMzU~I@)!~QHi78Imd`6jr)X(zM)!) zuIPks^rnV}M6d%C4}w&kdM)gK2zFccp~7Fiv7 zuY(tw=9xmG$|P-4#i#f(3aC+QRr|{pm&SbJSB7keK-p|kEPc_7{HbH7>&rL12s_|1 zqxCcCo|E16U#k|h8uf2GPCMG1ipxrnILd9!A&%SPNqFiS$jI&0Xk@;5zjW(LvVX1U zmqqw7AGf$xhC-9xeGyn(;J8vHKDyg`A}wWxDra4slH9yxn7xiatMb7P%6X5sg`XSi z{@E1epeS)+;jT2xsP%y)5~4N*dqTdxWsD<-3mMxk!l&r&M{0hx8iAAptA;7(FWeTY zg);WI+ajXY4Gq4g0X2LaBY>ezu(ibpSd%FFd%y+qTwj7emw%7)2yJF3HQ&_VdrIb@ zs(s5rbFuygDBuP?j_Z6lj5=5E_!1A8+=#q1>L02M+oj*U3isztIXT;ni;J^dKNI`r z{W}K>NH|uQ6K#p2RV#PeOwC1k3(D81(!Hr=(&6k`wYWaJP3U+u4WH+=X^%hup5go7 zuDF6yvs2Z!kiB%rffQXK3OJx)_iejM%a5-GGDW8%O{ZRje!vRIOFO!1n{TFL7i|L_WsL@NJdMU!yj2;D68*@$i z7%H@~QTk&vDjUo5XcoPWvaCgQwTAV64I)xf+ntTCLle@c_al6NY&qFliHV8D*;JB3 zg08I2Ma0Evl#**ogjx3Se%%Cto^b>90XWdz-;c*&Q=gie>XVo4@Q4Rcrvxx}XVhI; zbrWl1+atr$qSOmpTjDQaYF&fw02tjA5fQNfvP=;I-xa3*qqW{d9PI3M`6qD$ZKL6c zNZ8n-FAe503vAC56=*bwKp5D^k^cX5d%fxwRxGeKUsJ;hbjBWpe<0BdpRG6uF*PNJ zyaWqstC$Lw0F>92v-tp{TUl3;pb-N`ny4slIBYt<<}dl`74cmwePAF@K_nmAtyYvm zAhuYz`9ihuNmZuSZ-B}6-kRLmSk)|5WW<`AY_pm?kaD!W3q!=uDgsAW#mCT=V1eG# z8}7LteIDC3aQg=ykQiEIXXl8)x^ioqTiLFe+E7@fzo9S)wji%T&-pI1qPs3_(L=!} z91&VWrZP#|R2+n1V~5DTGX@1Uc2&_yN7)OH#(eZ9nK^AmI-Pc8a|BDu!rmGC@`En5 zg@>z{t(*r6)2Z;+kfb$u6=x$DiJL^;mr9kI*0bxAX@UrEc!5L`7y>}tB9eb9`{Eu} zkT{>x8tComp7TaQ$9+fUU8y;8p#tk%z5=1A4M>O9h?Dk$*G;o4iWQzh$(~QV`-s~T zv5t1uqetCUxcM>pdj+V;KX7kjXF5M-F?-Qt%9xoT-GG6QxAXdZh~K|2mUEyg37=r) z=PhTQjcL&*_y}a=mk(tCR_!)3{pxKX>jpd-FFU_pJ{(eRg+qW8<4 zo-CFW{KoX*@&yCzi}j&(3K*AdVg3d=*|D6(&SaM=5V+3P+t#Dx#rZoz@Ilmvy6Uyn zHLqvfp^ETtZE*PJ#}Kz@F1vLWK!|+e4C-YB&JrGdKkXY>$#Smsy@9To#wmymeBK7- z$OQFKZo%ly7Jj!a`IZZ7AxBi6r?8Dt1u&PIT+y9ZkN6#52xbM+Sg|!^N$dyq7dqP9 zg(W0VmtcK_=yTu2DP1BAThSyShVpa~!DG9=8gwy{%%1t($!{9bQf^bU502Pra2a;S z4XumVua+_>YwgbwIpE-f-hx!?%}vMrG9bi8^3)O{BC)KFg|)Q;noCiqMhZ!+xRNPy zvJKD=7VCBu49FX``j^A}ys$v3?#_IL)_EZ=Z}%hi$>luwOCV{*R`4N50V=!Vzr~xs z!5<1g>aKLsd&aU`j8k6euMdxc8pYRH9UU7+>aVow;R>o0Ic4!pEu4VZ z3+X$1lxm#}QcQEKD=yz%$)tr5W68+q2zfk85v)9We=sqEB*OUIK!U`xWGl zKs>y|C3~i4uO4cF7jhPDVX7!cz&leg9QC=AY$|E~$-{?(gX0aiHK18SA&ctC8&hTe zOPF*u~7T5YxaRp{c-txQQt3ArPnzq=Qu0ZE}soP>nhW`9~L zA;kTY!8fLt1To>TJJ6@nB*gv`L(A*qZnX%G)~%uZ1R!%JRwmpRh=piH{y4;8=e{4+ zyo>m=y$_tF&_}C?;t_|-4Hy9D(9EX>-TE`xcY{_zRyLm1Xm7ener6+;Ml*dn0=^18?| zVg!XFQ`09}H6-B??`@;`oIgPltSflFGi-g;%WAtnJKbm^dQ@fB7t0jKY1W2I*;!&fcxWY-QIJE^k>JRp}M8qaaQffusClr_&E?|AH2@ z1#mEnR}d>j-W1hHo5hE8yp}2=)aJWG25p{#hJ4Y3>07t*`~#jPTma$*syySKpkuv< z;Qg<4qo{tdSLp{6v~W(;ymO(VT@6C;@$XpJqe4~I8)^M+D$N#v0o|Kxc;VfuRB0up zPGdIlBhaPJba$&&P{Pi7n??kwFgJH=B=5B+@8QmH#m3Q`>pB0f0yxy1iOG82mWZ57 zT2xN1V53#3Z2j`=m=J_B32dtA-*sP=r3m27>5x*Vqr=}Z&VTlL7Z>MlzP9WNkgCj2 zCyiOqPNQ*5Dv^k*!gq@XzE-2x;$$R56R>D>MsOqYQxF#vwICW(y0LkOQV?%vNsmZ< zBu`JBr^wyqIs`R2U`(|s#CvzZ4Du!Mk^tHtE;CacLFX+^M)bN5_~PlV0XHRaYRy+vKLCr^pq5YVG(N}_NI5!4J+UkXvx-} z#xYZgElT`lm;Qle-0wR0I_@|>w#- z_mUzbl}Az@ZJJL3tlR=aP?E$&6VI(;7jwEj4nVR%_;lrM!?$fPc00-=>0;DBEg=4r zq&3d_A5sY=QRK=+om+khyOMv#9+b|GFg{eh>S?o{@IN8 z8>!&YT~H^2exT0XD{c43zugx0a2d$0v7PfydY^RX%- zRXH(}wfq`D!&$g($P!QJ?CmT!&@&q8mp{Q0K)_cu?piBgXJleb0}J@F-)H3jhrS|U zYV$Rg=)h(!OFfCZGD~b%;e)i~1wo#O!r|01705BRTD!)UEM26O^0SG?2aT1+*FKKzs{PPNJ!*U zng1SCjv-R{i1_F6yzENX)(pGv%a0xr!d>q``W8$}LQVj7MBGmVU`fZ-CSBH93=^tnW0NyG}+? z7c`1S`=NjzvDZl)e1@2XG^DPM&~-MPaHCUfh2I7r56y+b8ZB$jDoe^LC=s zG&@J`cYrhsofBUEYLwDhso|>BA%u-zj=!l`-+icftSa)Ld{h=#jtJPkb2z7oZP#?U z#i_F=G!th+9zjDb=q~q+sqtKm=s{=2LCBe~!>q;@4gMZg&R|>Sm?A2sv%nf2&sVO6 ztz9+RIsJPM8U}41Sr$o|(Zl3rKKh5n?4-#JoKd~LsEir_<_Kx_a0CU%l&!}^ zM-S^;J@p`w$r>O6eG+UckmLly7`%i!4%b^of|SGo`=4~$$h_HgE24crVF7Hq+1%aTIe8hx@g|#*&ToCw zhN{^piX?V$arjyZtuW!F{r?cdZV2Plp6L{K=m+SF>fghjjPT`u8yBDY`}%IaxpUKe z^`J?)3{j>?=MV}6Zm!MoiK%fOineqi-|7v-Bia7xj|G6js8X#_EAFiA(V6xC(35Cw zsC5`fXc7_^-sf z0hsK!Eth3eQ&UTUC=We=h4=%8wts9a0TIdS4)1PpC1)feJRI1<33yH7yTgZ@J~~Tt zhnZ`DZ>#n>Ytc{a&gVVEaT6#{l#yj*c!i9OsR+`o<0|Lp9Mx-_LJ&2LdsQBXNYVgk zl$r6Dt@L(J!Si&`oJJH{HUw3yZ~a&mx9NWTI|1gro;7EzYuuqf?g<)=29 zl>q0R$X9Q^yY7hs;m%a*8*SU!Y#Qo}v3V#YOKj-e39KycPH!M!c5e&k42Lkeck$D@l4k9P`*)3V>&}(IBo(`m} z;H2WtysgW~e-a*#v$d#IE~az8{}?#j9A=QzLvsvX@d{l9;q#;S|8sy#do^0Ou>pj8 zeX*Ex(NhY?)X{u>48$B7al58w^v)zv(wlON5Z zMNJ%YA+Q$vro;;fTZ&X0H-@H0a?(T<^@Y%jOmP#`s!XQB6B5L>-u|XLWR9kmzZ(S= z91VoIMaqpRqUVdsYEC>7h|>&>m7zm;Nz$cB>w(qFZd2BtwZwGA#DOO79xO=)+0;EG(R* z9j@$_mm~Ea*!{%n8LIB^$RZKb*kV*7jeb>9vsao($;+2il*KXW-4@PWJ3HDd=zp>u zGa`!!k+QrC)ALcJ`%nv*Bs(hG@XLzPCq~+L3`Oj}^7%~<8d62|jpz>9e|M!D(dc^Q z!H*v&aV&**Wm<8$xw$feQCK-VPiz2njn;~s>F@SDA#Gui?Hx449HI0$JKC~cDnaP{ zDUvS<3=Iwc9|ry3DOS=cT;7Z78y4XQX{kRJF6Q{A* zQ(z036Alt&Rn#3w9ui}p;qH)l2XeYNP}gJ<3MI(GETW5V6m0wRod-inK#e#(J!QAI zN~GWAeueM-%-fSIQV~A%)B3Re+*(f}}=_97~F@+ z(T5V&Z?*VsAIUw`fBX(bXYf^Le#T|?kSjp<#D&Gr_O_%_Cib}PMrT_}SB?^9s&Lq< zHJs=kJKsYF9C`rkppT-_Seci zWqy}<SSI*99OkBI6C;l1)I3>{lnZ%Cdf4tzNj)2=9+659+qyGBk)L> zXl!T&$Bp~v@jkqjelZYjBm#&jREa^kvpf^jr>jwiMb^RgSjE?tosE`3ea3w}uGf3u zzD^<1icmBK55!R6u6$rg;P;X2B-G-$xv~f(*;{Nb*-KOYo$fQ?KAIk5{ocmX>P(d@ zpkqiL_W1)zWB1wyYy+ryXfCF1DrYc6AUgw1k+6>E^B*id8JSAlXr7>h6VV*MgMNx7~13aT^K){ zzK79hv>H0f@CQXHJ~*rf5Y~qsxKvRb-(Ia84@|+&$qTX zZExSPvep0!h-m2_kiP=sHvCVkHEqA6NuXW&h{DxCxJW>>am#tz+6Ayaa`Gg*IMaLrXYHDf05CF34>E8S?;DtblT3Rx`z;ea!WAJULxix$r;Mr$qaVR3#Y;RKm zYW*5_Qt;)_;3(k2G}GJ1-h#BH>4}>jAC6de#Y~6&PXrTlgajQ8Wik;i2{?OWN z_OAqoivn4WNzf&=A0v;+Pn4n4I3n^#Pw5G0_!z zX@AF6t6%aj%2`Bqip4+kz$}T^MMb{8O|NofoqvCV1K78YJyzvAwKrGSTGu@3;TRHP zb|D(Crh05)$>)wQ{C5`dA66S4#{uh;t=0Tz-MmOrV&cAdP6ahJLb}7AtBSmVc!c1; zPa|3~vdIRcUwdlopzueN`G>3E+yApeP}1oCyFl>Ivcvz1X#Kmd6g|H0-xmk|X21XV zT+`p}YfQm+d)*L=6Wvm{_x!o%m~=_U9h?!B;3jF_ug z)3}i$AK8e2#^jnaBbm!tQMPi?jWb3KMYuc%k1Z2*mjd z@3|(36Oi_jne}~yt8Eotvt#FBj@$yi%`nkZCvWh;Cy}ecXqUpU)mv_vE;>3Iw(!Zz zTX<})Qbjr-wFH~l1Rg%98pp0|QU*{_p1y#eay{56VCMlgGlCoTgB`Q_TNI|Nz-~ZA z%qAv=@4ZR^qqg1JKr&xa%o%|7{eJ&BnP0bbi!B*OJk1i~iEI;f5@0_x#P)3kTCf0r zAO>qDQjv)y@K(P4gM%AD{tvJ=B|lyuyeq#m_>_*ms(7|^H;Mh+!^6Eb@ao0M##A1` zjuarUNt#MQhcY@-#HwT0{xh@a4SnA60~hT2^KCO9C8ZH1XNNMy-s|%{IYwyS&id#6bF-XleKChS2Gt4*`!&btU0q$c><)_I zPf%cWdM6J#9Zyq+vj0zWZvmB6y7rChIHG`*3MeT^C@CQwN=i#NDBazu0-_)xDJ|Vy z(kdn04G-Pj>K%ob&$YyziW|*7xsOvu0+khr#{qeee6eu3ufiZy>6T-L)(%RG%ZN zo!xsA$R-CP4|%NrOZN`s6q4yEJZIv)CB4ZgZ1;N;|hiscy+ z6C-ctT*1W3&IVY^>^J1+;3-jPVmjB=0S(b@3ItB*ZtAmy+H>A2Cn+NK$~sF%O%&bM z{`&&&+>WOBfbZB^lk0IDdZut*e_IoNPG5gM@46OQsg|lQiNXZ5C&sL5-&)Z;8^^8=3z|Ij<7H73g@TkT-FfQ5_(pr41JUT!!-oQH5uCgxAmEs1_F2<3BXdmmTOw;pC*j5VyMota2*Q)W}9HU3x%#xEmj>D17?8kZ7{CjN0))R`e(GFL1 zJbL<*92F&5`*pMR3$^lPxv7A!Qw6hE4R72cBBxYr)#BBe`$Aa0IazyX)R4{NNS{ljzf)u5WmAyT+_wN(P0dGF%6+22wN=|2I zAhc!CRMg9BZ5Nfy*3wH#`gaYxN=l8XDLR3X0lBwYxrGFUbeMc_@a|mdcS3lb=}Q;j zw6&P1m7s&np!(aN|H5w?C^PgYUtASEzXrxqYN>bvr}KmG>~bo}SUQfb4>rHyT*N7E zB&PRg^z7X)?$WL%xJ%@)vewulpC(azRq0ohl8DH{Lfnl;1M-xYkMQ#~CaaK+<=5TM zo4`X_hkUXpH-!Op_L&%KG@Ch9VfibQQF6e>cAM-c>pZ)aBlKHGmKdq0SWp@REqdVN{>F-VI zG3vO5(F`yDkIaWiRf2#r+o5=^8h7AkfoZ;Xs|KEIj>>+d)8vPQ$p^5%Wto^18+0YG zIKEC6X>FoP^Zb;cQX;7y;~)`{+uPTd-m`>>Nemby1>&AoolA>lfZN`aj2vdgkIx~9 zWp=?_@O{8=m8&v7R^+lgm6~cfqQaChPC9aK?5?zggzIVNM4rWln%XGf58d5HD|=CW zhMf#bIWdlhu%sW;WdC|or8GS>; z$ncghpBBA^#l>dohDf*8!Lk}-bQTcjaS};OH9inRFNU-yEK)?ITh{nJNl7Unu?h>l zg9pt=T^u)^LHMB6te`6|i&p)~X?kWQZ*usVl+;AJV*-S`?P@o^T9W$0F;ZzCle9Wx zQ^CZvsU95@N_81sa9e_aIzsHKR{X6*?w?vqrWn*jI3D&)owpZPM;PPJYZxOQT+|7JyI10ZS zAn=jl;jFAoRIT2u@bI#-&VW_rV=O>oFGgy*e)NceBMa{2bW&Z(fnA~VX)&C{i&a*IyP#m;yO1EYKQ zoJJ-ZxA(In&&Hp9HD%wV^k1lQu)D`K%t*;u;T2k3T`eH2Y`6A{49-n0 z)xN&YCeSXBc?S>BMxt`vOHWVVMTeuSEwAQ0Enw9`qb@owj`;qu0nh{@9RcpA2lzuY zS%DJYUMG^Vq|0o~EzHfm;&xm59z?3^=5mZ@N@ZfX`ORsXhuzTk>bZ-kNG%f^)B4)7 zVq*E@w^-R40T{O+fDRTWkPOXu%o&1qY^b8R0M;l<*rtZbw)Gq(6qcbhEgsO`K!XnJL^r6Hve zD%q@ipFpM&$K?uCYkF~2F_haipsnUK!;yLLv#z+6cW=qAQq%PmzCX&|>iS%FUQp2C zrAEzD!2p?G%d)<2`WEa#78Y^8GcNa6phGZ>{VodfHGkb(c-_c;1fA@yAX-Z6(=bfq zG&LxX@otiBPKwgf&`8Iz$!lr~g6|z3#S~dRKulS6`NS{Gf_?+}qO^?|$7&a<2Cqn_ zOyStb-2*)15IHG&dMF{-^R?+!S&_5NU(U|X0IKuFTF3I(%YORBb8^{N9MH%6IwZ+MX2K73T5!$Sk;FYtp?;>9E){w+uz>+rtB#N0V&Tbc2( zg|Fgraw1feb=ymHkpWWV*z?X!njd&q2l$;ue_0w%)LnOufp*VRnh1Yy%0tP3%D?fG zpPMa+0cpZb`m3FLbnqEtYdq+mP*X3$CDtkBW3N#s)pca~J8j2$VkF&G<%+52^gP)6 zdexnqYxH1aWPOVa@nY>y_CUcxAO|C+OhkxhvZ^Y~qmha#t<2#dUHXJ2sy*88ImdNj z1i|jk4o#sJS3uMJ&gebgfP0Ovo+#z3b$Z6wRpvap&Oy}4&s}c6Z?~9U3R?X$CJVV2 zTMsQ6gM`r+Je-hu!z(s6PRWC~cWHRutI~NB#r*6F#o_NWv$d7@Avpn0*O&f1OkGUZ{Td=p+KV)NO|~gLH(B%L%dHK~l2EDSmJ%KuKbbn&Uw{Rbu|wN(Z|XyhD*!4 zIywp+*RXN6K}j8ZsBqntK>-{>N)=R6@dta$p-`17Vo@=KIv?xoe?ZJ}8;YibzUJ;`vdLOXoR-@jY4cVb?3P-Ggil@w%I~_0i?6bo!&!eZ zvh8YcRa5g)fJZ7!u$7#IP_T3I#I14Ob?Nmkgre8d*g{rC#p!${9CFP7^?&RlLf>@R zClPS8P)=(ky5FoARm08j3)?kK3yTapyq2K?hLBqqdS;ZZXP92vjFD~hCm}lY_8J#g z>X3NBZ)lpZ@eeC3H%SI6{b+VpR%+~yvqIWZQ`^Q0D{x(7N~QGX2)h_G6%`a|xe9E@ zIz53jNRy0yX+o3Q_R4N&i^qKhg2qIo8eis>C}@pE`-wLWwnhFc?s3A&AH_^I=PK^^ z==ysM(t(!bT~JWqbtc4xhnMOMXz%Qxfvv6B*s#iCcJkWE;RUeIcJ?$hG*MCa!@qSu zzzgSKJ2vFU^xa+9r?s_3hnk~|jqS$4y8Y%{AqdkW=;`yc(J4aYHhd{#;lP*4=Yyb# zVu_)oHpfk{lorMk(XinUm8azE^HR2=ri^dWP5UASzSDx-uDrOEg^g{0d7ax3@$+eR zL?qX%6qK$K_B?H34xCz%0C<}p50<_gXd#skXikXFFW4!i0?s3}E8L@VVb|CFB=hmq56QK@lszf7 zn4qGkU+>Sn^IoI_@<fs_jqV3qTVW9q4)&{iORup#IZ!5t@;~0< zbsr!fT5E4NG3ylIgSQ>vWT%N|S|Tqz{&^;T+rxNW&K*+WtR5yV7ZQ4Tjp`~4>V>1- z-5;dWSJ~O_u{vdV)#t0n`S|R-kLu9=*3i(PAT6CIr5=MCN`=(Ws!lhW>GGeX+$3ru zU0wvqWMblHKp-8lNR`qTMP4q>%q=iucDS#VpQUEtTQ?)Sz?eE?FpMt!^?^CN9rX`s*@hq zqFIf_lY8v#>_vl!7Y7Eo;c#q38ZLt09ldzs6qAz5BWh>ysHjL=Uq5_<*JYy8xkkk{ zk3tt)SYvzZPppKbTzFC2gAOXRlwsE=0R7x1JYxOGsZR?ZrdL zky3Nuj@&pXG(TSrH5FqI>DxqvsT4rC6+8BmoV_UkPJZYYG=u3=x_wpWL^kk$J*Z z-W@T!*{V~c1GMMoC}mGfog0vcH+P{s#jom@x(PA>*WDAvd_H$lo^Q$}p1LM0hxlCy zs^u28JX3U*kGNypFUF(zYR_M|@9mrF2)M=OnxB4Otz&rS-o`+N7$;l{MQ9cpCTM2U6WQs!E4;zKw96~{)$(NDStGDcX^Cy-Gq zTWM1(o79jpHrM?Y4nZOIJk9m>F=bdwNiAomjy=HI?k%k?_xDiJv8MPdmxFd-tjz4T zq?wk*)vK7kwn#)xk8TsadQo?!Jt2CVQsXE}Tx=`^jpZUYjZ4KV7F(1VA94@{>fT6y z?73r;3(B_8^v)M2Cr>F=Qy3(4$W9Dpy@WX5-b8lN9Cjzad<4bTy9$cqJk1)p9n5zF zq75wT&;`MwW!E2t*zs7M!_HeA&eh6{+CY)BEHZ3kC8)FKJ09%ac2D`DQc~{t=;ZO- z{6d~qg=EMj3i*NyH9f^Ga(6kV&QiIsDy(Jvn{dLDr~>iLHDx)_AJ?Ot1`zR=)iu zSN|N#`1M*4WedvKqF37?xOfq{wUm_RlVugd=XoX{v)MQ9nulXxAoo{C(tVXPJ07P( zXSuu39?NFy&=!GLRD_JO{Q}|cUk=hIzyGLzAEZ&DcR!{>>^%N9F7E!auB6Y*-6B7< z$NTkY9Mdw%y>2f1gS~^&FJ1r;PVHO&7^2{}T2mc+WIe9kfV`m_{Ai=q)-_+niUnZv zCQhr&{aYbK!7%HqKiR6};R~yOFjYlpTyaony(#DpP71Xb<3Q=rthVa3oO)%5Ka~A8 z(w|q+;@01a`p**{leN)5_#VSxy76u|GA}KK0@&Y&3>&sRhP+&^e69o+^Af*hy0`Mm z>{w7@?>+||JAjVf!J`s7Q$)tm!ri7YcR&Cs+7sSe0g;8bX#p| ze)dB2-((&bKy7`y`*yX^@u^4n5vUT?h z-0CykaiKmu0|;Y~sZUsgf_W`HDC82neiM|GvE#aBEBl8*ie_Xt7!+Xa6WSh1+?ku( zjR~QjAMYK|to25LI1Cq`(e|Kv&&y9L_tq7i#j!5S!8K=3>7(SiyOX7TCjp+OCMFo9 zT*3X~pq4n;;Mdq+zkmDStow)ifa9*Sk&&;~`bcs_zD8e5eP3fstx6>jhY#-mWLDaK zOq(8|A#ud$yS@&&t!1;R+yJA(M=1I`Wz&c<;85oK@dGtC@8NzXm{k zmP#^9oHFJD;W2w~E?&LWw0??QLx@r&^*$Fj^Tcj5yhb#hHZxmmnQgY1tR&M*bw9=U!s)tk z#4p0@N}wgr6xR4FqXIM|ss4+RCpp3QZj${Q6CKS1zoC4s+5kTyCY$!qf^NTByx*a9 z2_oUn16>}dM_MLpd7Z3~-EV7_mzQxzg6t1;zv<9ZaB>@CmY+;+_pb{r(zzBpkOj6L7ADEfxHcJ+9o2sbLGkhWkJcG zgwf0LtIuZ5A$l{7)g-0b?wF<-Q(BTLv{5I5?uo(L(Zw|sR1!5+RdrQWQqFOkQ-}!5 zeF@;<)D)TQXGZ7^F=AlbYH`XDtj_k>dGG=GRx1(#-v$b1k*8QtdS3KC*A`E zg8118x&N)LG{YCLs)-479PQqy8SSR+U#fULg=4-xefkup0>p3euk=mO0^2=j%2LdH zO`3$1+vJO{x+stqpm2joATJaw+Ex4%=nQVXM|`foVfqAxjEASn+n2wkqvLT|_4qx& z=xn;65|tPbkozI##b4Fy6V+UtoWs&F z%#Ya)FLx_-*ZKBjjLtKCNHxXB-A-QeZ;ZWKjI&nE{Pw{ z2%s=0{Lyd{_B^C>#GOaU1UuJm#5$yUYN1t6Pt^PJKdCbAGf1*raVT@}%pJ3Pye0C| zY8d=X*HAEV%*dIFiYz)j@dub14*#@<$FS$hNJ!9^en(7HvE?CY$!CP2?BA63-^_f7qG3WVJ9UAJeM3J_}XUycC6 zc(71^`kgk?Y4-XB#Czp+##O;ojs$MklZ3dj+Vm8dQ2@1^Iuh{+ap$q;nE<3dVF~`s zpQtW>)guR`IGQ_Uxa*SQo`MFkcPzEOo}LB5`K1|^0Gf}@cI$r4DVD!eUc?poK0MHg zf;R5-;@lPD>y~DNy>1+|w9yXVs_EE;wQ=su z;~UIq?zIq!ZrJL7xIB}gu2SqtSi{K_~^Os1}OJu)&ehEBlX{d>STHmCYOBk2M} z#wtu@@)5NJ1O%{u!OQmHv#wIkiA1$!LzgAkm2DNDmVzFlE2^qM_MyJ+ojwr6(W#~7 zN>yDp6u`3?+1>EP1UEAIfbLAnW5M@I~O~!0q=~lkn-N5+4rGU zx)=c7RTISv@T7aJ_gVFZ3viNK)!Gt5(=mwoY|cjqz^Cj-qF#}WO_CB4(#v0U$jQOg z(%NcbY|N-p(%;*grN=}TqxSFE6ROgnd`slmA;|=~OSS9f^Yp+@;RHZd8EDt(&_i3= zySjp8J}@4K<(j)f^>ZPLWy=u@5atIw1A0_4Qod@&V}%N!x&Nm~raCaQDJ|)))BWwA zjRTr>g>R#yD`htM7?J$gVL!TKo>&*zud)dbHO|}U>LAKl*{Poj{jy*@W4*UM19+`| z3mWtp_V+)1xQUA4S>w3L;9I}FJ|-io94Vd(ciw(`B_cllVl67;yo6p|^^O3Iok-Tw zM+igy_bd|rHmz8gV7@^5%*ul^9#w#=77yqN3Dp&!KK&t~R$A0MI9OqQZsG$P_JC9Q zLLVR!Xcbx@Y=A+t5*t^w)&3cr*OiwH38(X-rRFo%qT(_U6b%cKBnJ`fL zL;2S+mxjhFyfh~fybfk#kjC5PSq5FtLK9U_kEJi>%4Tnm$IKBEz|VOWadhe*cW*qm z+fxT70^B?7EUdR~Rj+N#9GwhEc;0yfC<$r_upFJwwDp@W@!6d14nJ)rT z!jHtwM@crz!{b_MNeRer_Lm20LQQ?82nWF+7C}d)CvS_&PRFXZu-6hd9ve$!=8Hvq zw5|I|hH+Ayl{NfSRY6ww+BIQeVR#W~R|kO>1>{P7ISWb$ZVs+vQf`fv;03b!_sFB| z*_~AMr}wZHGg zyWC;};rd`|?--!xTy?gp$_mSg+SP_kTj0j~&2i;IgNKAfb};^@k#D72isAouuVpE~DJwFk&4UmG0;(Ae0nQaFjFcWMHZ zpBuwYW0#kw^dBjcu(pBR(2rvNpbuC0!^Co1@~5ijg4Mow7uLu+8k%b)Uxyr1w3-^A z`=Gg1D>Axh4q`e!KDM?_z#<(bJzG-dra?CS%#L9E80Zlb8AEtMVb<(`sD4@Iu-5@L(U7pk?{d`bnf7yGpOYdCTtg=qMAZ%*t$xxhv@0YWuqkM?tFF=y5D&kVhgq$*C+!IpmhxxA^mx>`IZQaahXqMEJgzZ@OAlT>jGwBi6k>~?jU zYw}JU8?SG_Ve$MFbNBYyudz1Wo_7_bh&tM`%2*;qhW&LP?Q5NslKfNGLKmGPugsn;#W-PZDQaLo+v!gb$2SOYBxwQ~P63QGg!7A4x1l+iU)u zLs+;ms(2yq+*E(QlOMX8QYKVGM?AK(s&2s)it57JtD8z0KZ3*^IA{9?dl(>P@{mET zU~YgGVJS`=H`BqV`1y3V+3})l)A+|^W9{-oe>(#S>1io)N=9nY2Kql4hHxjCC7}xR z5~eiqAu$KD^U5c9V~kB#!{`k3Cn_BiJl-i4t*H25S5|%kSr5M-E1Ixyc(3rwqcjJ5 z+sO)3h)H-Jlu&&WBS@vh>3;sR#`Kj;J|07V$6S~-x%}$ zs{mk8j%%Z#;c=X<3#<;t*9d@>{hW~CNi(H~(e!|*?ni|UDRyXMQqK}d5dak#1!)0* zW}j|tqF`Un(z*@^wdTZlx!Gwma_+u4aU8JAHLiya{a^Smjve!J%^xdo0|;56VIeKO zjc3z>T{MQyj{zlbTx6u-MX~55qY9!=*JjLW*Se^f-T16mT|VNwe|olI?H(=D8mj-*mR=mrYZ(U7v3Gl`E%1+Q!1- z{PErauj{m*<44kbIJkif;I-xYnl58GMDC8$_|4De!`2Vfk$wnVL8_H!f}SWdArGo;iP^qm`(k~D=;ZUA(W@lsoFQhOd;H9E4bE}@e!kQx%sOYE|2u}P}Qd-DaV3}XTYpGQU zgSHSH&`d4?KGJVL6=qN=qk{r-J+S=M0bj1#iOpGAPN})p0&Q=anE6O=f z6!~(|PENzh(Et5wOT4=;b8lZd@O3*2T5e~PVgL8BKlWU}{(MgDn9-VT<;4&Ys(xbr z+5ix5kTGP+Q{M`Rj!{1*P}v9y%Fz#48WF-{5OWxY-yO z0U*ur(Di!m<;%sfRVja4;HQA|Z1)1AhK`O74K?*pT5gB8Sg0q%VgLp$&GzEJc8DY9 zEY|hC^|7w*?ibIW|ETf}$|l?_6WRY1n72_zT&1Bg;nBGyQC?KU_SBp#|Le<@^{OwD z3o|~u;Kq#1)u2B)uc0aNlk@TB0fE=0GRv|(=1Goa-PQ1WW7;YzU!qTV+1W*(%l3da zWp8*p6_3``Gx$4wD;~YpAV9l7e}E;JSaYIBx7pc#zq6$Ur$)6G)R-IYzTkwDJhpXH z9m$6*kwcOii;3!sr%Z)#5dX;mDfFCD;M;E^h!MoUNXGBj4LCShgU|zaj}cevYeB(L z$>(=^fkOlHb*JY4!T`y5{Rak!&oeMQBt)&qEY(Bz8bN>o_$t8$jZQcW5Iv^!mudlPLS6d7?B1L`~Kc7d7ErdgPYAR(bhPKm| zG8o>>UJvek@O(2h@&_G+C;hK<5cN`S>uN#t$$8_h;b86<5f(=qVQZ&B0>5Kg5Tm7r zox4EdOSAR0<2xLv8dWZ-bLQDcNs}Vyr(J;`J*Drnx%GoxJWCgs@_H-}B0Dm_#%ngp zwwmxJGWOG9^@Ff=@z*JkM%oWDtMCl3zRX&Lih_UK)J zdI4~D7rO&H5HsTu^fqRziy97@tFxg}u`!mvxdMtK&rQS<(`*F2s?kJc0Enq|%)O+g zJ2e;%2Lo194jvz}90C*U~~1`IY^JU0r>X5(Km?Hdex`f2_k3$6yDR=tawM z#=@oqqeCQf6rtUTw}>~T33~yEN_;U62svHMAjB^qEc{L#g+{I2_Umr{BlLx&HmFDX7-wp(3G1jdi+Fm4(q!{^TSdZ*?sdm4}q%neEIspbOzgA3u5oo3#`D){b?e z<2bL&+}E1q*)RM}{LZ&e;QrWL!`8?B>DAuaPITCuald$>#C!}8U3Z8;q--NoFGr^-g285YoW5Hs63t(LJ>R#ws}J)etF7R4mK zg?=hSQ{}Ym2M%&S^9;a#bJYrAa2#-`wdW|_KJV|wf*aJECwvC$dxw)pP`wf1Hj`fu>VR-mRV$N&$dHQzde*(|iyEZTlEWTc zA>mV(9N=o9(SjJEAi;NFu$S8j5l@9e0fhb_WpWzc`s^P;r(4Vkvq>QUj?euQ&Ngl4 zxM2S6be+OG+dRj|wq|={j{q+L=2P$dkV0eMwp67(hGjR%`i_+P7Z@PJi{V*hc*qag zDB{epbgtmY-c=MQoj1J<703$FyaOZ#_%G%SOG8gcw9h#2%Pqxe5BszX%)Dq6Yl{sJ z_jr&K!}UzIO*s#Ibjve(+H2GgZvKT&Qqj^h&7C|dPfHuzO1vhPt2feWvUyRjI2lj& zx(kbVT-lvH!S}tx(m*^f+9J4;GUkT`{=p(05%u^8EmY^UA8VJd`*4YO>uLUjM7q=F z2Z=QIZ=WrV^ifMvfsfiB;AX~$S`V(}W5Yy>w_fir^C4*ObCAs?5RV1MahMMww00Z` z@fx>qk`ugr1{2p&pC&PoxP-sA1yj?TC6oPt=qsQUsl=<$$BF4c02|3B;qxc0G>{u` z01!isxsRnR0~1TyxoRKTGvb$*)Kq;1B*#3nd+{@o8FG0jil-y!qWN|DhuO@>K%FBj zD!L9!bA$!)ND00fuT>tH!?1}%tTG1vNg*X)$-w(BUg?j|Go~s2E@de^#U%2arcYy3`ogK=L$g};e# z`~`LDH0}DB^l5l@6YP7s$S+pYy`^8LsHtV* zRwvReh0#CEfYKM@Swue($$uRBZSulPRAR#>b;DjCXO@9ltPztJ`ymM0=*KQ9Z236= zEzj_L78GAYqZyu<*aNeY0JhJ`u?Fwy8K6tR|Dj((*f&vcJ|@;u|Eu93MA&G6Tgb)!whUwe9cL>Q9qm>mWUFV7F(VJWkWUJ za#tqgv4R-Rul70i%(kv<75@k8$j~q1L@p1=;kB=F*i7#+I`f6D6Wk7pFJJO(c%~$c z1q&p$tAFF5rUvfn`WWezuUAbz&`)2%=mR0{I^;_sf#+j%692EPSg_%S0B%sr#zM;i z%o$97bN*%zHu-PHvs?_3+(ugkMIO&99aq|4EfI6N6`<&1|Ew>Ta5@TwC={Kxwg3>Q z{QKC-N7qmG);C8QO=rIKoV&X&GvAX>>%_i2SAXO1@m)V(L-=moQ-t6Y?uqN@Y@*>f zng%6Dk7csZxRB2HJVjcLAXb*eQo(0d0(!VM3(fI3?qR8TRt9={`?YV|kaCHen+23o zQIXBiZ~0TUSFad-p@{RAxU;Nved2I_qdLUh0Mfk{e`1Db2+7EJ_RQ!5&x&%LpA*N* z0{kbe1e#lr5!4EgBu}I2d}+JS7XMna)EA*tIpO^AKT<}2c1#Esq(Gb$<#>BWK!r7{y|ILqXS|u;#AqzuB4OBGDv1)tY=x8pS@_*yZ04GSR z1NH?QD%n*-2JiQxYuw+fzVmxd-+nG5eJKyp%sEDi2cqKZb1$v3BdYaJ&wUdkf3pO` z#59(l#b}ADFnc3V0v6-7o~MXp-0TaQNQqh$9)|@y{t|M(4c~vVC9xs!Jfvez&|yztPF35%^IIG)!*$gPFOOy$K2FgcMxQ< z31H2Iu$GoI1;l&<>{@`WYV_UpD{s`KlEj>B1AESMOnq$+_H_3*RF*irpHA5vEcbDW z4t&c>Qr6MyKkM$f1GymZzZxQgL_Zq{Nn_SZFF{{tbiB_i zOxC?&WP+k#34jC|OhTqscoYp~^WulLN zgqoU~BKao^#00GJl$@NTjEv!tKGW3$PjK2al#1f+F3uSX`UYv#IqX9)&lx3X+`;fj z$ZXaC^k1dZH^~I%p4Q6!PEFV4J?6fn=$X|qj{mE~l`*g%J-ghULmVgv+IaeTHX?IUM~Fgkt{LLhW?v$N8uoW!z~lt8@= zl3Co%Lt$KNPnxpsj0%Wufdnl?ssw%#xkLpfB%Xn8FT_eJuPJp+UTJNnuAK*+E3TDO z;9(M*Qdd+Rj9FPeDg6#E5pd(Jj#V`Wnyc^Z?jCnJY_ANC4^wcRtam>@*W?yZk z$Z!Dh^=Yj=)I~+>>}?%591rYZp?&|}w!sjXuU#{TEW%#`6I@HuaUL=8d(4ANKzC}6 z?y^V1CFZvI1wI$3x%F{hn)GML8+Q5ZF4{Rb9xP9nIvz5x`O-(McjwEc&)_p2YTFlyM!4q!0`52hD`oOoCdeqXu;q}W4MBbt7|jh`W2kqx0Z&u znRfyTKN?@QeA*0dK{O_#d#cxNU}@Iaj4aDjUq^S{Thkvxc=R>d!;R|zi^V6mVlc`i zFErJ(LcesMh@yZI<&9`s*7J(Wlw-{Ml zS*W!?&AXG{2}cxwQ;kZIpX2j#a>(iFrA*!hdnq5?U(jy=()B3-5<0kLtmRYMAA`?a z6MKx{RVNv*i}@^D$nr(Sfs<@;#YW}UDd$>DSs2(k+GIUO;Xa$OG6?V`S}?(cBchTy-3eZKF&|>I}Yqn z!XXSE`T7L!_2JH#=gr`|xi{GsN}UMQ+v~(2RDgHi!Fvvx;tXVYzQNSI1Tc~d3kyPZ z0hLbEv2gM6wR|}2#KaRAs~4aRRud698ERU3ztgRw~= z-W3<{;m ztZb&EV`6IPxst*k+LDub&s+ZGTrLwMWB%>c)IK8(hWr5Q;SaL6P;~dR=FiH-(%byx z-qcT@72B#S%Q$+dx$;c4lg?7XX}NQ-qf+u}!}oC0FPDMZm)%73`Sj46-BH^=cBnJS zVo--)XR(I@Z8s9ykfD5Sv5Ok86DY{aX1>(6W7@D0=p2}je-)N!mju%%UrAyF8?3>~ACha(- zJ;M}63Z3$$Z^m8x(l>+k-+eQrZs404)4TM|FwTN+Ch5{Qg8{x7<4fPnv>5niKB#MH zA-qlf#B9?++t@;H`Fc}_)>#XPY~?tg2;{U1)54_rFMK_=ti=Awf!G3fLD~~_ zx)Zu{0BQBlt|yz5!i)3sQOLE=7DEU=cJ^k(XZCa7s0z-_Oh5LA%5gyGwzN#r#FZ&p zN8Yi?V>tYse1=o}S7P(D)!;>G5yNi<>L2(U0@2YT~)Q8p6BpUFz^BVsgPM?<9+1Y{3Li$GRO%Nmt zD1)^8pYELjqQ{_tatNssj>211cI=Qdwk;LP_mgNnrk87#Xgxia%c1ZQz@dB0x&63~ z+2h`s3-DAuk#|p(7Iu6W&&%JaxJjI-RjfZ~9lPsqQ|9$6L89@jP6=nypu$z*_3LTC zmWYVrPL2-HQ9TsQdXvF}2d2&k_Tv9vJ8Rlbvhr?IGhN`o4M$Xa=v!EjJ$%?V*tGDy zwq_%a2Qn`aMX8PZriuh1nw)9$?I1G5TAP%&3HhyM&E{v4P^&JWe^LQTE{ruMR&X=#|1Y01a_F}Ps-v8g3d8!EEkr{_TD1F4q{=-s*$w#T< z7#S%?ziT0RLG;tejF{Y~a8H)Y1V><<;nu|n!S1@cTw%zi=_VoU?5zWsZerxrOOCo; zZK{EO>Z{Gz^M>-4Y1t9Jhpm#UUUXYhN`0$ISXoaa)BqiPDa5F+e z3^;iFii?UsEC6)F!TLyeRFw7NK?m$~!Z#Xr7)kCEJY^j1Nj`XVcbNO+5Yr~k+FP;MtMGc_2C|C5=)7^eYdCUy0anF;FK{TF5? z=7r|GX}C?f*9Xf`e&;X6*@T`9wA*N?&O*sO0H>u+eqmQt26GJ!yIzfJmsY%HWosKP zD+_BZizOKu#h!J+2TRy&vmQ}}RzMvFjjleeGP*^c?QYXkXjNfQ#ZRxClZc$w|3J?; zZzD%4+Vn?%njO{(e*CDNcE(woqadWJn(7*)Rc@ihI2cRYa!&}5R8D&OQvJ5pvv+ll zo0yxJdRQB%l_mho!UzGhlSnkim!LAp5qRE5lF#0Obfl@Jl8OOj8IoYb`hT#rgHIVU zHQ-Ic@Ys98$bP8b;19Dquy5fWEq%Kh z$u<@?kLzLQx()w1e{>!YrJN#R^9jyW*B- z+X%5Ve8Z}Uh1_Da!ED>mJTh8_ReiBa^U;uc(N8*9D*|36*ZrMRM=KuNjoWqTtGO{l z+^#Mk`5fLbQXKW{gq!}3HCg9h>pWYzWGR0{qA0mHmf?vUVcPC`45|jM0#7WmIY?t= z$lCzP(pbrBdVUtBztR~m3?w4_6Z1DG>eH8QGelMae$=z;ciE)EMpjl@fxnlqstRXt z6_RCru9W@Y-;g-%EB}{B9HiqyrTu&7edc%d(;N;~(L2@9lDqN$NSE0}mjiqG!3(Y{nJo05m4&;4PC4Xzo^-~?WD z#(f74Wy|sD@(wNsD>)^l$6VRLkDeRo8w8%}Dp+j^QiVpvo|(_@P?GBEcu1S?CuF*} z;X2Ltf~f+Wk5KGeYSeK#_Z}s!2h7(>!C*r%$bEG?pmT$w7gWjHp6H!l4ix#sYZJ1VoyMKX~gQ1S3u_gS z@k;=L)ss@B=_W!TC2RIdAkuJRu$SI#xFk{w?-oTF1?}z+w7!6ln~mfl;!Je6Kkh`x z3fVblez(Ar9q|z&|5b34hVeOGbe<`14*EuLG!=zd>PFs1-fQ&2sP*$Rhpv<3Y7CY$ zLk<>0nC**Pj5QofFRnzs@jP=6nbS2La<6?g?bQ}ZKU(D|)9jkAIH^+XK6Y`=pHAda z55tR|*Kfe^)admwIq?{F&)q{-r#-bjfy5`agXYbF#1jP0X8L?8z*W4|{KDJ1HoFQN6m@K@`B=>3ZL~lO2D~!*+)X%PTI4FZov5 z7eD6_vDc~$wb^^+D#{a(LMBd&BBigi_Jwk^xFx4`pA>agb4dU~y=XJJs8l;L{=^z( zo%TFUx@!BRB~<$KxN(Yo^?*BqPP2CF?P-|>Z@uN}^Q8)WB@L(PB~Hi5D3sK!HU`h# z^}~}|-ny~M*$GEP>&DD(ta%k;>R5q<9%++9=tle)A(^*}@(WvQE6XJt?qXKC!M zc&DA^&Dsf#;`3AJD_e%NJnQ>cNF4^t@Q)(Q(~(|ZljFEe7rUyOQHUr&5W5lUuIzrE zSAEw>e7RIv`{GrH=$-tiyPl=f8(&xs5u4=}Ej#O*kAVE>=#jTT+Yvhss8v? zmZjw;iQ7S}dgRq}uLMm@d1ZN#b6cNPKV42EtFhX7=XXbb;qJvzsny;gE4Fy?y%MXr z@?P_VQpwmJW@{0=*~{z64|m^jBX|%c??XEc`0;x?G_GG-c>T8m zyWqnsh7~$khMBH+F7L`SMmu*MHu#UP+W&byzF@leZG tuple[Any, list]: "Use 'await model.agenerate()' in async custom columns." ) + from data_designer.engine.context import ( + current_generation_column, + current_run_cancel_event, + is_run_cancellation_requested, + ) from data_designer.engine.dataset_builders.utils.async_concurrency import ensure_async_engine_loop # Honor a per-call ``timeout=`` override (passed straight through to the @@ -99,10 +105,41 @@ def generate(self, *args: Any, **kwargs: Any) -> tuple[Any, list]: conversation_restarts = int(kwargs.get("max_conversation_restarts", 0) or 0) bridge_timeout = _compute_bridge_timeout(per_request_timeout, correction_steps, conversation_restarts) + if is_run_cancellation_requested(): + raise asyncio.CancelledError + + column = current_generation_column.get() + cancel_event = current_run_cancel_event.get() + + async def agenerate_with_bridge_context() -> tuple[Any, list]: + column_token = current_generation_column.set(column) + cancel_token = current_run_cancel_event.set(cancel_event) + try: + if is_run_cancellation_requested(): + raise asyncio.CancelledError + return await facade.agenerate(*args, **kwargs) + finally: + # Cross-thread cancellation can close the coroutine from a + # different context after it has started. The task context is + # being discarded either way, so avoid an unraisable reset error. + with contextlib.suppress(ValueError): + current_run_cancel_event.reset(cancel_token) + with contextlib.suppress(ValueError): + current_generation_column.reset(column_token) + loop = ensure_async_engine_loop() - future = asyncio.run_coroutine_threadsafe(facade.agenerate(*args, **kwargs), loop) + coro = agenerate_with_bridge_context() + try: + future = asyncio.run_coroutine_threadsafe(coro, loop) + except RuntimeError as exc: + coro.close() + if is_run_cancellation_requested() or "interpreter shutdown" in str(exc): + raise asyncio.CancelledError from exc + raise try: return future.result(timeout=bridge_timeout) + except concurrent.futures.CancelledError as exc: + raise asyncio.CancelledError from exc except concurrent.futures.TimeoutError as exc: future.cancel() # Demoted to debug: the raised ModelTimeoutError already surfaces diff --git a/packages/data-designer-engine/src/data_designer/engine/context.py b/packages/data-designer-engine/src/data_designer/engine/context.py index 500b6bb51..137a4f65a 100644 --- a/packages/data-designer-engine/src/data_designer/engine/context.py +++ b/packages/data-designer-engine/src/data_designer/engine/context.py @@ -4,11 +4,26 @@ from __future__ import annotations from contextvars import ContextVar +from threading import Event # Set by the async scheduler before executing each task. # Value: (current_rg_index, total_rg_count) or None. current_row_group: ContextVar[tuple[int, int] | None] = ContextVar("current_row_group", default=None) +# Set by the async scheduler before executing each task so model usage can be +# attributed even when scheduler telemetry context is not available. +current_generation_column: ContextVar[str | None] = ContextVar("current_generation_column", default=None) + +# Shared cancellation signal for sync generator work running in thread-pool +# workers. Context variables copy the Event object into worker threads, and the +# scheduler flips the Event on cancellation. +current_run_cancel_event: ContextVar[Event | None] = ContextVar("current_run_cancel_event", default=None) + + +def is_run_cancellation_requested() -> bool: + cancel_event = current_run_cancel_event.get() + return cancel_event.is_set() if cancel_event is not None else False + def format_row_group_tag() -> str: """Return a '(x/X) ' prefix if a row group context is active, else ''.""" diff --git a/packages/data-designer-engine/src/data_designer/engine/dataset_builders/async_scheduler.py b/packages/data-designer-engine/src/data_designer/engine/dataset_builders/async_scheduler.py index 9109eafcc..b377b5461 100644 --- a/packages/data-designer-engine/src/data_designer/engine/dataset_builders/async_scheduler.py +++ b/packages/data-designer-engine/src/data_designer/engine/dataset_builders/async_scheduler.py @@ -12,6 +12,7 @@ from collections import Counter, defaultdict, deque from collections.abc import Coroutine, Mapping from dataclasses import dataclass +from threading import Event from typing import TYPE_CHECKING, Any, Callable import data_designer.lazy_heavy_imports as lazy @@ -25,7 +26,7 @@ RequestAdmissionConfigSnapshot, RowGroupAdmission, ) -from data_designer.engine.context import current_row_group +from data_designer.engine.context import current_generation_column, current_row_group, current_run_cancel_event from data_designer.engine.dataset_builders.errors import DatasetGenerationError from data_designer.engine.dataset_builders.multi_column_configs import MultiColumnConfig from data_designer.engine.dataset_builders.scheduling.completion import CompletionTracker, FrontierDelta @@ -273,6 +274,7 @@ def __init__( # engine drops rows and continues, losing the cause unless we capture it. self._first_non_retryable_error: Exception | None = None self._fatal_worker_error: BaseException | None = None + self._cancel_requested = Event() # Pre-compute row-group sizes for O(1) lookup self._rg_size_map: dict[int, int] = dict(row_groups) @@ -370,6 +372,10 @@ def first_non_retryable_error(self) -> Exception | None: """ return self._first_non_retryable_error + def request_cancel(self) -> None: + """Signal cancellation to scheduler tasks and bridged sync generator work.""" + self._cancel_requested.set() + def _raise_if_fatal_worker_error(self) -> None: if self._fatal_worker_error is None: return @@ -1004,50 +1010,58 @@ async def run(self) -> None: num_rgs = len(self._row_groups) with self._progress_bar or contextlib.nullcontext(): - if self._reporter: - self._reporter.log_start(num_row_groups=num_rgs) - - self._emit_scheduler_event("scheduler_job_started", diagnostics=self._scheduler_job_diagnostics()) - self._emit_scheduler_health_snapshot("start") - - # Launch admission as a background task so it interleaves with dispatch. - admission_task = asyncio.create_task(self._admit_row_groups()) - try: - # Main dispatch loop - await self._main_dispatch_loop(seed_cols, has_pre_batch, all_columns) - finally: - # Always cancel admission + drain in-flight workers, regardless - # of how the dispatch loop exited (normal, early shutdown, - # CancelledError, or processor failure). - if not admission_task.done(): - admission_task.cancel() - with contextlib.suppress(asyncio.CancelledError): - await admission_task - await asyncio.shield(self._cancel_workers()) - # Salvage partially-complete row groups left over from early - # shutdown. Must run AFTER _cancel_workers - in-flight tasks - # could otherwise write into a buffer that's being finalized. - if self._early_shutdown and self._rg_states: - self._finalize_after_shutdown(all_columns) - - # Reached only on the clean-exit path; an exception in the - # dispatch loop or the finally block propagates and skips this. - if self._reporter: - self._reporter.log_final() + if self._reporter: + self._reporter.log_start(num_row_groups=num_rgs) - self._emit_scheduler_health_snapshot("completed") - self._emit_scheduler_event( - "scheduler_job_completed", diagnostics=self._scheduler_health_diagnostics(reason="completed") - ) + self._emit_scheduler_event("scheduler_job_started", diagnostics=self._scheduler_job_diagnostics()) + self._emit_scheduler_health_snapshot("start") + + # Launch admission as a background task so it interleaves with dispatch. + admission_task = asyncio.create_task(self._admit_row_groups()) - if self._rg_states: - incomplete = list(self._rg_states) - logger.error( - f"Scheduler exited with {len(self._rg_states)} unfinished row group(s): {incomplete}. " - "These row groups were not checkpointed." + try: + # Main dispatch loop + try: + await self._main_dispatch_loop(seed_cols, has_pre_batch, all_columns) + except asyncio.CancelledError: + self.request_cancel() + raise + finally: + # Always cancel admission + drain in-flight workers, regardless + # of how the dispatch loop exited (normal, early shutdown, + # CancelledError, or processor failure). + if not admission_task.done(): + admission_task.cancel() + with contextlib.suppress(asyncio.CancelledError): + await admission_task + await asyncio.shield(self._cancel_workers()) + # Salvage partially-complete row groups left over from early + # shutdown. Must run AFTER _cancel_workers - in-flight tasks + # could otherwise write into a buffer that's being finalized. + if self._early_shutdown and self._rg_states: + self._finalize_after_shutdown(all_columns) + + # Reached only on the clean-exit path; an exception in the + # dispatch loop or the finally block propagates and skips this. + if self._reporter: + self._reporter.log_final() + + self._emit_scheduler_health_snapshot("completed") + self._emit_scheduler_event( + "scheduler_job_completed", diagnostics=self._scheduler_health_diagnostics(reason="completed") ) + if self._rg_states: + incomplete = list(self._rg_states) + logger.error( + f"Scheduler exited with {len(self._rg_states)} unfinished row group(s): {incomplete}. " + "These row groups were not checkpointed." + ) + finally: + if self._reporter: + self._reporter.close() + async def _main_dispatch_loop( self, seed_cols: tuple[str, ...], @@ -1446,6 +1460,8 @@ async def _execute_task_inner(self, task: Task, lease: TaskAdmissionLease, task_ """Core task execution logic.""" num_rgs = len(self._row_groups) token = current_row_group.set((task.row_group, num_rgs)) + column_token = current_generation_column.set(task.column) + cancel_token = current_run_cancel_event.set(self._cancel_requested) group = lease.item.group identity_hash = hashlib.sha1("\0".join(group.key.identity).encode()).hexdigest()[:16] correlation_token = runtime_correlation_provider.set( @@ -1463,6 +1479,8 @@ async def _execute_task_inner(self, task: Task, lease: TaskAdmissionLease, task_ await self._execute_task_inner_impl(task, lease, task_execution_id) finally: runtime_correlation_provider.reset(correlation_token) + current_run_cancel_event.reset(cancel_token) + current_generation_column.reset(column_token) current_row_group.reset(token) async def _execute_task_inner_impl(self, task: Task, lease: TaskAdmissionLease, task_execution_id: str) -> None: diff --git a/packages/data-designer-engine/src/data_designer/engine/dataset_builders/dataset_builder.py b/packages/data-designer-engine/src/data_designer/engine/dataset_builders/dataset_builder.py index 8ce6c0cde..5a20fdab5 100644 --- a/packages/data-designer-engine/src/data_designer/engine/dataset_builders/dataset_builder.py +++ b/packages/data-designer-engine/src/data_designer/engine/dataset_builders/dataset_builder.py @@ -3,6 +3,7 @@ from __future__ import annotations +import concurrent.futures import contextlib import functools import json @@ -103,6 +104,23 @@ def _is_async_trace_enabled(settings: RunConfig) -> bool: return settings.async_trace or os.environ.get("DATA_DESIGNER_ASYNC_TRACE", "0") == "1" +def _await_async_scheduler_result(future: concurrent.futures.Future[Any], scheduler: Any) -> None: + try: + future.result() + except KeyboardInterrupt: + request_cancel = getattr(scheduler, "request_cancel", None) + if callable(request_cancel): + request_cancel() + future.cancel() + try: + future.result() + except concurrent.futures.CancelledError: + pass + except Exception: + logger.debug("Async scheduler raised while cancelling after KeyboardInterrupt", exc_info=True) + raise + + class _ConfigCompatibility(StrEnum): COMPATIBLE = "compatible" INCOMPATIBLE = "incompatible" @@ -626,7 +644,7 @@ def _build_async_preview(self, generators: list[ColumnGenerator], num_records: i loop = ensure_async_engine_loop() future = asyncio.run_coroutine_threadsafe(scheduler.run(), loop) try: - future.result() + _await_async_scheduler_result(future, scheduler) finally: self._task_traces = scheduler.traces self._early_shutdown = scheduler.early_shutdown @@ -901,7 +919,7 @@ def on_complete(final_path: Path | str | None) -> None: loop = ensure_async_engine_loop() future = asyncio.run_coroutine_threadsafe(scheduler.run(), loop) try: - future.result() + _await_async_scheduler_result(future, scheduler) finally: self._task_traces = scheduler.traces self._early_shutdown = scheduler.early_shutdown diff --git a/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/async_progress_reporter.py b/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/async_progress_reporter.py index 162483f9a..601a9d4b0 100644 --- a/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/async_progress_reporter.py +++ b/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/async_progress_reporter.py @@ -5,9 +5,11 @@ import logging import time +from collections.abc import Callable from typing import TYPE_CHECKING from data_designer.engine.dataset_builders.utils.progress_tracker import ProgressTracker +from data_designer.engine.models.usage_events import TokenUsageEvent, subscribe_token_usage from data_designer.logging import LOG_INDENT if TYPE_CHECKING: @@ -40,9 +42,11 @@ def __init__( self._last_bar_report_time: float = self._start_time self._last_reported_total: int = -1 self._bar = progress_bar + self._unsubscribe_token_usage: Callable[[], None] | None = None if self._bar is not None: for col, tracker in trackers.items(): self._bar.add_bar(col, f"column '{col}'", tracker.total_records) + self._unsubscribe_token_usage = subscribe_token_usage(self._record_token_usage) def log_start(self, num_row_groups: int) -> None: cols = ", ".join(self._trackers) @@ -71,24 +75,32 @@ def record_skipped(self, column: str) -> None: self._maybe_report() def log_final(self) -> None: - if self._bar is not None and self._bar.is_active: - self._update_bar(force=True) - else: - self._emit() - elapsed = time.perf_counter() - self._start_time - snapshots = [tracker.get_snapshot(elapsed) for tracker in self._trackers.values()] - total_ok = sum(snapshot[2] for snapshot in snapshots) - total_fail = sum(snapshot[3] for snapshot in snapshots) - total_skipped = sum(snapshot[4] for snapshot in snapshots) - skipped_suffix = f", {total_skipped} skipped" if total_skipped else "" - logger.info( - "āœ… Async generation complete [%.1fs]: %d ok, %d failed%s across %d column(s)", - elapsed, - total_ok, - total_fail, - skipped_suffix, - len(self._trackers), - ) + try: + if self._bar is not None and self._bar.is_active: + self._update_bar(force=True) + else: + self._emit() + elapsed = time.perf_counter() - self._start_time + snapshots = [tracker.get_snapshot(elapsed) for tracker in self._trackers.values()] + total_ok = sum(snapshot[2] for snapshot in snapshots) + total_fail = sum(snapshot[3] for snapshot in snapshots) + total_skipped = sum(snapshot[4] for snapshot in snapshots) + skipped_suffix = f", {total_skipped} skipped" if total_skipped else "" + logger.info( + "āœ… Async generation complete [%.1fs]: %d ok, %d failed%s across %d column(s)", + elapsed, + total_ok, + total_fail, + skipped_suffix, + len(self._trackers), + ) + finally: + self.close() + + def close(self) -> None: + if self._unsubscribe_token_usage is not None: + self._unsubscribe_token_usage() + self._unsubscribe_token_usage = None def _maybe_report(self) -> None: now = time.perf_counter() @@ -111,6 +123,19 @@ def _update_bar(self, *, force: bool = False) -> None: updates[col] = (completed, success, failed, skipped) self._bar.update_many(updates, force=force) + def _record_token_usage(self, event: TokenUsageEvent) -> None: + column = event.column + if column is None and event.correlation is not None: + column = event.correlation.task_column + if column is None or column not in self._trackers: + return + if self._bar is not None: + self._bar.record_token_usage( + column, + input_tokens=event.input_tokens, + output_tokens=event.output_tokens, + ) + def _emit(self) -> None: current_total = sum(tracker.get_snapshot(0.0)[0] for tracker in self._trackers.values()) if current_total == self._last_reported_total: diff --git a/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/sticky_progress_bar.py b/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/sticky_progress_bar.py index 27bffa275..aca9430c6 100644 --- a/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/sticky_progress_bar.py +++ b/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/sticky_progress_bar.py @@ -41,6 +41,10 @@ _MAX_RATE_SAMPLES = 7200 _RATE_FORMAT = "{:6.1f} " _Y_AXIS_RESERVED = 12 +_MIN_LEGEND_LABEL_WIDTH = 8 +_RATE_COLUMN_WIDTH = 5 +_INPUT_TOKEN_RATE_WIDTH = 8 +_OUTPUT_TOKEN_RATE_WIDTH = 9 _ProgressUpdate = tuple[int, int, int, int] @@ -65,6 +69,11 @@ def _sanitize_label(label: str) -> str: return _CONTROL_RE.sub("", _ANSI_RE.sub("", label)) +def _fit_plain(text: str, width: int) -> str: + clean = _sanitize_label(text) + return clean[:width].ljust(width) + + def _average(values: Sequence[float]) -> float: return sum(values) / len(values) if values else 0.0 @@ -130,6 +139,8 @@ class _BarState: last_completed: int = 0 latest_rate: float = 0.0 rates: list[float] = field(default_factory=lambda: [0.0]) + input_tokens: int = 0 + output_tokens: int = 0 def record_update( self, @@ -159,10 +170,22 @@ def record_update( self.last_completed = bounded_completed self.last_sample_time = now + def record_token_usage(self, *, input_tokens: int, output_tokens: int) -> None: + self.input_tokens += max(0, input_tokens) + self.output_tokens += max(0, output_tokens) + def average_rate(self, now: float) -> float: elapsed = max(now - self.start_time, 0.001) return self.completed / elapsed if elapsed > 0 else 0.0 + def input_token_rate(self, now: float) -> float: + elapsed = max(now - self.start_time, 0.001) + return self.input_tokens / elapsed if elapsed > 0 else 0.0 + + def output_token_rate(self, now: float) -> float: + elapsed = max(now - self.start_time, 0.001) + return self.output_tokens / elapsed if elapsed > 0 else 0.0 + class StickyProgressBar: """ANSI throughput chart panel that sticks to the bottom of the terminal. @@ -267,6 +290,21 @@ def update_many(self, updates: dict[str, _ProgressUpdate], *, force: bool = Fals if self._active: self._redraw_if_due(now, force=force) + def record_token_usage( + self, + key: str, + *, + input_tokens: int, + output_tokens: int, + force: bool = False, + ) -> None: + with self._lock: + if bar := self._bars.get(key): + now = time.perf_counter() + bar.record_token_usage(input_tokens=input_tokens, output_tokens=output_tokens) + if self._active: + self._redraw_if_due(now, force=force) + def remove_bar(self, key: str) -> None: with self._lock: self._bars.pop(key, None) @@ -337,7 +375,7 @@ def _format_panel(self) -> list[str]: panel_height = min(self._panel_height, max(_MIN_PANEL_HEIGHT, terminal_size.lines - 1)) inner_width = panel_width - 2 - legend_capacity = 4 if panel_height >= 13 else max(1, panel_height - 9) + legend_capacity = 5 if panel_height >= 13 else max(1, panel_height - 9) chart_line_count = max(3, panel_height - 4 - legend_capacity) chart_height = chart_line_count - 1 @@ -392,28 +430,145 @@ def _format_chart_lines(self, bars: list[_BarState], inner_width: int, chart_hei def _format_legend_lines(self, bars: list[_BarState], now: float, capacity: int) -> list[str]: lines: list[str] = [] - visible_bars = bars - if len(bars) > capacity: - visible_bars = bars[: max(0, capacity - 1)] + if capacity <= 0: + return lines + + include_status = any(bar.failed or bar.skipped for bar in bars) + label_width, done_width, rate_width, input_width, output_width, status_width = self._legend_column_widths( + bars, + now, + include_status=include_status, + ) + lines.append( + self._format_legend_table_line( + marker="", + label="column", + done="done", + now_value="now", + avg_value="avg", + input_token_rate="in tok/s", + output_token_rate="out tok/s", + status="status" if include_status else None, + label_width=label_width, + done_width=done_width, + rate_width=rate_width, + input_width=input_width, + output_width=output_width, + status_width=status_width, + ) + ) + + row_capacity = max(0, capacity - 1) + visible_bars = bars[:row_capacity] + if len(bars) > row_capacity and row_capacity > 0: + visible_bars = bars[: max(0, row_capacity - 1)] for index, bar in enumerate(visible_bars): color = _CURVE_COLORS[index % len(_CURVE_COLORS)] - pct = (bar.completed / bar.total * 100) if bar.total > 0 else 100.0 - failed = f" | {_color(str(bar.failed) + ' failed', _FAILED)}" if bar.failed else "" - skipped = f" | {bar.skipped} skipped" if bar.skipped else "" lines.append( - f"{_color('ā—', color)} {bar.label}: {bar.completed}/{bar.total} " - f"({pct:3.0f}%) | now {bar.latest_rate:5.1f} rec/s | " - f"avg {bar.average_rate(now):5.1f}{failed}{skipped}" + self._format_legend_table_line( + marker=_color("ā—", color), + label=bar.label, + done=self._format_done(bar), + now_value=f"{bar.latest_rate:.1f}", + avg_value=f"{bar.average_rate(now):.1f}", + input_token_rate=f"{bar.input_token_rate(now):.1f}", + output_token_rate=f"{bar.output_token_rate(now):.1f}", + status=self._format_status(bar) if include_status else None, + label_width=label_width, + done_width=done_width, + rate_width=rate_width, + input_width=input_width, + output_width=output_width, + status_width=status_width, + ) ) - if len(bars) > capacity: + if len(bars) > row_capacity and row_capacity > 0: lines.append(f"{_MUTED}... {len(bars) - len(visible_bars)} more column(s){_RESET}") while len(lines) < capacity: lines.append("") return lines[:capacity] + def _legend_column_widths( + self, + bars: list[_BarState], + now: float, + *, + include_status: bool, + ) -> tuple[int, int, int, int, int, int]: + terminal_size = shutil.get_terminal_size() + inner_width = max(2, terminal_size.columns - 3) + done_width = max(len("done"), *(len(self._format_done(bar)) for bar in bars)) + rate_width = max( + len("now"), + _RATE_COLUMN_WIDTH, + *(len(f"{value:.1f}") for bar in bars for value in (bar.latest_rate, bar.average_rate(now))), + ) + input_width = max( + len("in tok/s"), + _INPUT_TOKEN_RATE_WIDTH, + *(len(f"{bar.input_token_rate(now):.1f}") for bar in bars), + ) + output_width = max( + len("out tok/s"), + _OUTPUT_TOKEN_RATE_WIDTH, + *(len(f"{bar.output_token_rate(now):.1f}") for bar in bars), + ) + status_width = 0 + if include_status: + status_width = max(len("status"), *(len(self._format_status(bar)) for bar in bars)) + + separator_count = 5 + int(include_status) + fixed_width = ( + 2 + (separator_count * 3) + done_width + (rate_width * 2) + input_width + output_width + status_width + ) + available_label_width = max(_MIN_LEGEND_LABEL_WIDTH, inner_width - fixed_width) + content_label_width = max(len("column"), *(len(_sanitize_label(bar.label)) for bar in bars)) + label_width = min(max(_MIN_LEGEND_LABEL_WIDTH, content_label_width), available_label_width) + return label_width, done_width, rate_width, input_width, output_width, status_width + + def _format_legend_table_line( + self, + *, + marker: str, + label: str, + done: str, + now_value: str, + avg_value: str, + input_token_rate: str, + output_token_rate: str, + status: str | None, + label_width: int, + done_width: int, + rate_width: int, + input_width: int, + output_width: int, + status_width: int, + ) -> str: + marker_text = f"{marker} " if marker else " " + line = ( + f"{marker_text}{_fit_plain(label, label_width)} | {done:>{done_width}} | " + f"{now_value:>{rate_width}} | {avg_value:>{rate_width}} | " + f"{input_token_rate:>{input_width}} | {output_token_rate:>{output_width}}" + ) + if status is not None: + line = f"{line} | {status:>{status_width}}" + return line + + def _format_done(self, bar: _BarState) -> str: + pct = (bar.completed / bar.total * 100) if bar.total > 0 else 100.0 + return f"{bar.completed}/{bar.total} {pct:3.0f}%" + + def _format_status(self, bar: _BarState) -> str: + parts: list[str] = [] + if bar.failed: + parts.append(f"{bar.failed} failed") + if bar.skipped: + parts.append(f"{bar.skipped} skipped") + return ", ".join(parts) if parts else "ok" + def _panel_line(self, text: str, inner_width: int) -> str: return f"{_BORDER}│{_RESET}{_fit_ansi(text, inner_width)}{_BORDER}│{_RESET}" diff --git a/packages/data-designer-engine/src/data_designer/engine/models/facade.py b/packages/data-designer-engine/src/data_designer/engine/models/facade.py index 81a935282..5c2ab99c8 100644 --- a/packages/data-designer-engine/src/data_designer/engine/models/facade.py +++ b/packages/data-designer-engine/src/data_designer/engine/models/facade.py @@ -16,6 +16,7 @@ OPENROUTER_PROVIDER_NAME, ) from data_designer.config.utils.image_helpers import is_image_diffusion_model +from data_designer.engine.context import current_generation_column from data_designer.engine.mcp.errors import MCPConfigurationError from data_designer.engine.model_provider import ModelProviderRegistry from data_designer.engine.models.clients.types import ( @@ -42,7 +43,9 @@ RequestUsageStats, TokenUsageStats, ) +from data_designer.engine.models.usage_events import TokenUsageEvent, emit_token_usage_event from data_designer.engine.models.utils import ChatMessage, prompt_to_messages +from data_designer.engine.observability import runtime_correlation_provider if TYPE_CHECKING: from data_designer.engine.mcp.facade import MCPFacade @@ -863,3 +866,18 @@ def _track_usage(self, usage: Usage | None, *, is_request_successful: bool) -> N token_usage=token_usage, request_usage=RequestUsageStats(successful_requests=1, failed_requests=0), ) + if token_usage is not None: + correlation = runtime_correlation_provider.current() + column = current_generation_column.get() + if column is None and correlation is not None: + column = correlation.task_column + emit_token_usage_event( + TokenUsageEvent( + model_alias=self.model_alias, + model_name=self.model_name, + input_tokens=token_usage.input_tokens, + output_tokens=token_usage.output_tokens, + column=column, + correlation=correlation, + ) + ) diff --git a/packages/data-designer-engine/src/data_designer/engine/models/usage_events.py b/packages/data-designer-engine/src/data_designer/engine/models/usage_events.py new file mode 100644 index 000000000..ea4ce8205 --- /dev/null +++ b/packages/data-designer-engine/src/data_designer/engine/models/usage_events.py @@ -0,0 +1,54 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import itertools +import logging +from collections.abc import Callable +from dataclasses import dataclass +from threading import Lock + +from data_designer.engine.observability import RuntimeCorrelation + +logger = logging.getLogger(__name__) + + +@dataclass(frozen=True) +class TokenUsageEvent: + model_alias: str + model_name: str + input_tokens: int + output_tokens: int + column: str | None = None + correlation: RuntimeCorrelation | None = None + + +TokenUsageCallback = Callable[[TokenUsageEvent], None] + +_callback_lock = Lock() +_callback_ids = itertools.count() +_callbacks: dict[int, TokenUsageCallback] = {} + + +def subscribe_token_usage(callback: TokenUsageCallback) -> Callable[[], None]: + callback_id = next(_callback_ids) + with _callback_lock: + _callbacks[callback_id] = callback + + def unsubscribe() -> None: + with _callback_lock: + _callbacks.pop(callback_id, None) + + return unsubscribe + + +def emit_token_usage_event(event: TokenUsageEvent) -> None: + with _callback_lock: + callbacks = tuple(_callbacks.values()) + + for callback in callbacks: + try: + callback(event) + except Exception: + logger.debug("Token usage event callback failed", exc_info=True) diff --git a/packages/data-designer-engine/tests/engine/column_generators/generators/test_custom.py b/packages/data-designer-engine/tests/engine/column_generators/generators/test_custom.py index af7bc7109..2f3676a52 100644 --- a/packages/data-designer-engine/tests/engine/column_generators/generators/test_custom.py +++ b/packages/data-designer-engine/tests/engine/column_generators/generators/test_custom.py @@ -26,6 +26,7 @@ _compute_bridge_timeout, ) from data_designer.engine.column_generators.utils.errors import CustomColumnGenerationError +from data_designer.engine.context import current_generation_column, current_run_cancel_event from data_designer.engine.models.clients.errors import SyncClientUnavailableError from data_designer.engine.models.errors import RETRYABLE_MODEL_ERRORS, ModelTimeoutError from data_designer.engine.resources.resource_provider import ResourceProvider @@ -570,6 +571,63 @@ async def fake_agenerate(*args: Any, **kwargs: Any) -> tuple: engine_thread.join(timeout=5) +def test_async_bridge_preserves_generation_column_context() -> None: + """Bridged sync custom generators should still attribute model usage to their column.""" + facade = Mock() + facade.generate.side_effect = SyncClientUnavailableError( + "Sync methods are not available on an async-mode HttpModelClient." + ) + facade.request_timeout = 60.0 + + async def fake_agenerate(*args: Any, **kwargs: Any) -> tuple: + assert current_generation_column.get() == "intent_label" + return ("async_result", list(args), kwargs) + + facade.agenerate = fake_agenerate + proxy = _AsyncBridgedModelFacade(facade) + + engine_loop = asyncio.new_event_loop() + engine_thread = threading.Thread(target=engine_loop.run_forever, daemon=True) + engine_thread.start() + column_token = current_generation_column.set("intent_label") + + try: + with patch( + "data_designer.engine.dataset_builders.utils.async_concurrency.ensure_async_engine_loop", + return_value=engine_loop, + ): + result = proxy.generate("hello", parser=str) + finally: + current_generation_column.reset(column_token) + engine_loop.call_soon_threadsafe(engine_loop.stop) + engine_thread.join(timeout=5) + + assert result == ("async_result", ["hello"], {"parser": str}) + + +def test_async_bridge_obeys_run_cancellation_before_scheduling() -> None: + """Cancelled runs should not schedule new async model calls from worker threads.""" + facade = Mock() + facade.generate.side_effect = SyncClientUnavailableError( + "Sync methods are not available on an async-mode HttpModelClient." + ) + facade.request_timeout = 60.0 + facade.agenerate = Mock() + proxy = _AsyncBridgedModelFacade(facade) + + cancel_event = threading.Event() + cancel_event.set() + cancel_token = current_run_cancel_event.set(cancel_event) + + try: + with pytest.raises(asyncio.CancelledError): + proxy.generate("hello", parser=str) + finally: + current_run_cancel_event.reset(cancel_token) + + facade.agenerate.assert_not_called() + + def test_async_bridge_non_client_mode_errors_propagate() -> None: """Only SyncClientUnavailableError triggers bridging; other errors propagate.""" # ValueError - different type entirely diff --git a/packages/data-designer-engine/tests/engine/dataset_builders/test_dataset_builder.py b/packages/data-designer-engine/tests/engine/dataset_builders/test_dataset_builder.py index 0a68a84db..8355b831a 100644 --- a/packages/data-designer-engine/tests/engine/dataset_builders/test_dataset_builder.py +++ b/packages/data-designer-engine/tests/engine/dataset_builders/test_dataset_builder.py @@ -3,6 +3,7 @@ from __future__ import annotations +import concurrent.futures import logging from typing import TYPE_CHECKING from unittest.mock import Mock, patch @@ -720,6 +721,29 @@ def mock_run_coroutine_threadsafe(coro, loop): buffer_manager.free_row_group.assert_not_called() +def test_await_async_scheduler_result_cancels_scheduler_on_keyboard_interrupt() -> None: + class MockFuture: + def __init__(self) -> None: + self.result_calls = 0 + self.cancel = Mock() + + def result(self) -> None: + self.result_calls += 1 + if self.result_calls == 1: + raise KeyboardInterrupt + raise concurrent.futures.CancelledError + + scheduler = Mock() + future = MockFuture() + + with pytest.raises(KeyboardInterrupt): + builder_mod._await_async_scheduler_result(future, scheduler) + + scheduler.request_cancel.assert_called_once_with() + future.cancel.assert_called_once_with() + assert future.result_calls == 2 + + def test_reset_run_state_clears_per_run_signals(stub_resource_provider, stub_test_config_builder) -> None: """``_reset_run_state`` must clear all per-run state so reused builders don't leak.""" builder = DatasetBuilder( diff --git a/packages/data-designer-engine/tests/engine/dataset_builders/utils/test_sticky_progress_bar.py b/packages/data-designer-engine/tests/engine/dataset_builders/utils/test_sticky_progress_bar.py index 5070e5f54..f86c00198 100644 --- a/packages/data-designer-engine/tests/engine/dataset_builders/utils/test_sticky_progress_bar.py +++ b/packages/data-designer-engine/tests/engine/dataset_builders/utils/test_sticky_progress_bar.py @@ -8,6 +8,7 @@ import os import re import shutil +import time from collections.abc import Iterator from unittest.mock import patch @@ -22,6 +23,7 @@ _BarState, _fit_series, ) +from data_designer.engine.models.usage_events import TokenUsageEvent, emit_token_usage_event CURSOR_UP_CLEAR = "\033[A\033[2K" HIDE_CURSOR = "\033[?25l" @@ -58,6 +60,10 @@ def _last_panel_lines(output: str) -> list[str]: return clean[panel_start:].splitlines() +def _pipe_positions(line: str) -> list[int]: + return [index for index, char in enumerate(line) if char == "|"] + + def test_no_output_when_not_tty() -> None: stream = io.StringIO() with StickyProgressBar(stream=stream) as bar: @@ -91,15 +97,35 @@ def test_renders_bounded_throughput_panel(tty_stream: FakeTTY) -> None: bar.update_many({"a": (10, 10, 0, 0), "b": (20, 20, 0, 0)}, force=True) assert bar.drawn_lines == 16 - panel = "\n".join(_last_panel_lines(tty_stream.getvalue())) + panel_lines = _last_panel_lines(tty_stream.getvalue()) + panel = "\n".join(panel_lines) assert "Throughput" in panel assert "rec/s" in panel - assert "column 'a': 10/100" in panel - assert "column 'b': 20/100" in panel + assert "column 'a'" in panel + assert "10/100" in panel + assert "column 'b'" in panel + assert "20/100" in panel + header = next(line for line in panel_lines if "in tok/s" in line) + row = next(line for line in panel_lines if "column 'a'" in line) + assert _pipe_positions(header) == _pipe_positions(row) assert "ā•­" in panel assert "ā•°" in panel +def test_token_usage_rates_render_in_legend_table(tty_stream: FakeTTY) -> None: + with StickyProgressBar(stream=tty_stream) as bar: + bar.add_bar("a", "column 'a'", 100) + bar.update("a", completed=10, success=10, force=True) + bar._bars["a"].start_time = time.perf_counter() - 10.0 # noqa: SLF001 + bar.record_token_usage("a", input_tokens=100, output_tokens=25, force=True) + + panel = "\n".join(_last_panel_lines(tty_stream.getvalue())) + assert "in tok/s" in panel + assert "out tok/s" in panel + assert "10.0" in panel + assert "2.5" in panel + + def test_control_sequences_are_removed_from_labels(tty_stream: FakeTTY) -> None: with StickyProgressBar(stream=tty_stream) as bar: bar.add_bar("a", "col\x1b[31m_a\nsuffix", 100) @@ -247,6 +273,17 @@ def test_reporter_updates_and_logs_keep_drawn_lines_in_sync(tty_stream: FakeTTY) with bar: reporter = AsyncProgressReporter(trackers, report_interval=0.1, progress_bar=bar) reporter.log_start(num_row_groups=1) + emit_token_usage_event( + TokenUsageEvent( + model_alias="test", + model_name="test-model", + input_tokens=120, + output_tokens=30, + column="col_a", + ) + ) + assert bar._bars["col_a"].input_tokens == 120 # noqa: SLF001 + assert bar._bars["col_a"].output_tokens == 30 # noqa: SLF001 snapshot = tty_stream.getvalue() reporter.record_success("col_a") diff --git a/packages/data-designer-engine/tests/engine/models/test_facade.py b/packages/data-designer-engine/tests/engine/models/test_facade.py index 0be33bd02..2e2d6ea60 100644 --- a/packages/data-designer-engine/tests/engine/models/test_facade.py +++ b/packages/data-designer-engine/tests/engine/models/test_facade.py @@ -8,6 +8,7 @@ import pytest +from data_designer.engine.context import current_generation_column from data_designer.engine.mcp.errors import MCPConfigurationError, MCPToolError from data_designer.engine.models.clients.errors import ProviderError, ProviderErrorKind from data_designer.engine.models.clients.types import ( @@ -28,6 +29,7 @@ from data_designer.engine.models.facade import ModelFacade from data_designer.engine.models.parsers.errors import ParserException from data_designer.engine.models.usage import TokenCountSource +from data_designer.engine.models.usage_events import TokenUsageEvent, subscribe_token_usage from data_designer.engine.models.utils import ChatMessage from data_designer.engine.testing import StubMCPFacade, StubMCPRegistry, make_stub_completion_response @@ -344,6 +346,32 @@ def test_completion_tracks_reasoning_tokens_without_changing_output_tokens( assert token_usage.total_tokens == 18 +def test_completion_emits_token_usage_event( + stub_model_facade: ModelFacade, + stub_model_client: MagicMock, +) -> None: + events: list[TokenUsageEvent] = [] + unsubscribe = subscribe_token_usage(events.append) + stub_model_client.completion.return_value = ChatCompletionResponse( + message=AssistantMessage(content="ok"), + usage=Usage(input_tokens=10, output_tokens=8), + ) + token = current_generation_column.set("intent_label") + + try: + stub_model_facade.completion([ChatMessage.as_user("hi")]) + finally: + current_generation_column.reset(token) + unsubscribe() + + assert len(events) == 1 + assert events[0].model_alias == stub_model_facade.model_alias + assert events[0].model_name == stub_model_facade.model_name + assert events[0].input_tokens == 10 + assert events[0].output_tokens == 8 + assert events[0].column == "intent_label" + + def test_consolidate_kwargs(stub_model_configs: list[Any], stub_model_facade: ModelFacade) -> None: # Model config generate kwargs are used as base, and purpose is removed. # When telemetry is enabled (default), X-Title is injected. From 1cf84d53a719c41ab724c46d9b5cd9aa7f4b70ec Mon Sep 17 00:00:00 2001 From: "Eric W. Tramel" Date: Thu, 21 May 2026 13:41:10 -0400 Subject: [PATCH 04/19] fix: show token rates in progress demo Emit synthetic token usage in the credential-free progress panel demo so the live token-rate columns are visible, and accept output-only provider usage as a progress event. Signed-off-by: Eric W. Tramel --- examples/progress_panel_demo.py | 18 +++++++++++++++- .../src/data_designer/engine/models/facade.py | 4 ++-- .../tests/engine/models/test_facade.py | 21 +++++++++++++++++++ 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/examples/progress_panel_demo.py b/examples/progress_panel_demo.py index e674569b4..da5eb2a0c 100644 --- a/examples/progress_panel_demo.py +++ b/examples/progress_panel_demo.py @@ -7,6 +7,8 @@ import time import data_designer.config as dd +from data_designer.engine.context import current_generation_column +from data_designer.engine.models.usage_events import TokenUsageEvent, emit_token_usage_event _WORKERS = threading.Semaphore(6) @@ -14,8 +16,22 @@ def _simulate_work(row: dict, *, column: str, base_delay: float) -> str: topic = str(row["topic"]) jitter = (sum(ord(ch) for ch in topic + column) % 7) * 0.025 + input_tokens = 80 + (sum(ord(ch) for ch in topic) % 45) + output_tokens = 12 + (sum(ord(ch) for ch in column) % 16) with _WORKERS: - time.sleep(base_delay + jitter) + # This example is intentionally credential-free, so emit synthetic + # token usage to exercise the progress panel's live token-rate columns. + for _ in range(4): + time.sleep((base_delay + jitter) / 4) + emit_token_usage_event( + TokenUsageEvent( + model_alias="progress-panel-demo", + model_name="synthetic-token-stream", + input_tokens=input_tokens // 4, + output_tokens=output_tokens // 4, + column=current_generation_column.get() or column, + ) + ) return f"{column}:{topic.lower().replace(' ', '-')}" diff --git a/packages/data-designer-engine/src/data_designer/engine/models/facade.py b/packages/data-designer-engine/src/data_designer/engine/models/facade.py index 5c2ab99c8..3fe4738f4 100644 --- a/packages/data-designer-engine/src/data_designer/engine/models/facade.py +++ b/packages/data-designer-engine/src/data_designer/engine/models/facade.py @@ -854,9 +854,9 @@ def _track_usage(self, usage: Usage | None, *, is_request_successful: bool) -> N return token_usage = None - if usage is not None and usage.input_tokens is not None: + if usage is not None and (usage.input_tokens is not None or usage.output_tokens is not None): token_usage = TokenUsageStats( - input_tokens=usage.input_tokens, + input_tokens=usage.input_tokens or 0, output_tokens=usage.output_tokens or 0, reasoning_tokens=usage.reasoning_tokens, reasoning_token_count_source=usage.reasoning_token_count_source, diff --git a/packages/data-designer-engine/tests/engine/models/test_facade.py b/packages/data-designer-engine/tests/engine/models/test_facade.py index 2e2d6ea60..abda8aeae 100644 --- a/packages/data-designer-engine/tests/engine/models/test_facade.py +++ b/packages/data-designer-engine/tests/engine/models/test_facade.py @@ -372,6 +372,27 @@ def test_completion_emits_token_usage_event( assert events[0].column == "intent_label" +def test_completion_emits_token_usage_event_when_only_output_tokens_are_reported( + stub_model_facade: ModelFacade, + stub_model_client: MagicMock, +) -> None: + events: list[TokenUsageEvent] = [] + unsubscribe = subscribe_token_usage(events.append) + stub_model_client.completion.return_value = ChatCompletionResponse( + message=AssistantMessage(content="ok"), + usage=Usage(output_tokens=8), + ) + + try: + stub_model_facade.completion([ChatMessage.as_user("hi")]) + finally: + unsubscribe() + + assert len(events) == 1 + assert events[0].input_tokens == 0 + assert events[0].output_tokens == 8 + + def test_consolidate_kwargs(stub_model_configs: list[Any], stub_model_facade: ModelFacade) -> None: # Model config generate kwargs are used as base, and purpose is removed. # When telemetry is enabled (default), X-Title is injected. From 33cf915feac544a1c6ca01a00c39cb9f220a2ac5 Mon Sep 17 00:00:00 2001 From: "Eric W. Tramel" Date: Thu, 21 May 2026 13:46:07 -0400 Subject: [PATCH 05/19] fix: render progress legend without ascii separators Replace the pipe-delimited progress legend with a native spaced layout, keep column alignment via computed widths, mute the header row, and refresh the PR screenshot. Signed-off-by: Eric W. Tramel --- .../assets/progress-throughput-panel-demo.png | Bin 40771 -> 34908 bytes .../utils/sticky_progress_bar.py | 22 +++++++++++++----- .../utils/test_sticky_progress_bar.py | 7 ++---- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/docs/assets/progress-throughput-panel-demo.png b/docs/assets/progress-throughput-panel-demo.png index 550d8d3d92ef89aa3b5ba83c851d4aabdd736f9e..60a7756dda081dabcf8ec59a4cf14fee80da16f8 100644 GIT binary patch literal 34908 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 literal 40771 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 GgeCxUt@Stn diff --git a/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/sticky_progress_bar.py b/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/sticky_progress_bar.py index aca9430c6..0cbe50f84 100644 --- a/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/sticky_progress_bar.py +++ b/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/sticky_progress_bar.py @@ -45,6 +45,7 @@ _RATE_COLUMN_WIDTH = 5 _INPUT_TOKEN_RATE_WIDTH = 8 _OUTPUT_TOKEN_RATE_WIDTH = 9 +_LEGEND_COLUMN_GAP = 2 _ProgressUpdate = tuple[int, int, int, int] @@ -522,7 +523,13 @@ def _legend_column_widths( separator_count = 5 + int(include_status) fixed_width = ( - 2 + (separator_count * 3) + done_width + (rate_width * 2) + input_width + output_width + status_width + 2 + + (separator_count * _LEGEND_COLUMN_GAP) + + done_width + + (rate_width * 2) + + input_width + + output_width + + status_width ) available_label_width = max(_MIN_LEGEND_LABEL_WIDTH, inner_width - fixed_width) content_label_width = max(len("column"), *(len(_sanitize_label(bar.label)) for bar in bars)) @@ -548,14 +555,17 @@ def _format_legend_table_line( status_width: int, ) -> str: marker_text = f"{marker} " if marker else " " + gap = " " * _LEGEND_COLUMN_GAP line = ( - f"{marker_text}{_fit_plain(label, label_width)} | {done:>{done_width}} | " - f"{now_value:>{rate_width}} | {avg_value:>{rate_width}} | " - f"{input_token_rate:>{input_width}} | {output_token_rate:>{output_width}}" + f"{marker_text}{_fit_plain(label, label_width)}{gap}{done:>{done_width}}" + f"{gap}{now_value:>{rate_width}}{gap}{avg_value:>{rate_width}}" + f"{gap}{input_token_rate:>{input_width}}{gap}{output_token_rate:>{output_width}}" ) if status is not None: - line = f"{line} | {status:>{status_width}}" - return line + line = f"{line}{gap}{status:>{status_width}}" + if marker: + return line + return f"{_MUTED}{line}{_RESET}" def _format_done(self, bar: _BarState) -> str: pct = (bar.completed / bar.total * 100) if bar.total > 0 else 100.0 diff --git a/packages/data-designer-engine/tests/engine/dataset_builders/utils/test_sticky_progress_bar.py b/packages/data-designer-engine/tests/engine/dataset_builders/utils/test_sticky_progress_bar.py index f86c00198..8528cdf5d 100644 --- a/packages/data-designer-engine/tests/engine/dataset_builders/utils/test_sticky_progress_bar.py +++ b/packages/data-designer-engine/tests/engine/dataset_builders/utils/test_sticky_progress_bar.py @@ -60,10 +60,6 @@ def _last_panel_lines(output: str) -> list[str]: return clean[panel_start:].splitlines() -def _pipe_positions(line: str) -> list[int]: - return [index for index, char in enumerate(line) if char == "|"] - - def test_no_output_when_not_tty() -> None: stream = io.StringIO() with StickyProgressBar(stream=stream) as bar: @@ -107,7 +103,8 @@ def test_renders_bounded_throughput_panel(tty_stream: FakeTTY) -> None: assert "20/100" in panel header = next(line for line in panel_lines if "in tok/s" in line) row = next(line for line in panel_lines if "column 'a'" in line) - assert _pipe_positions(header) == _pipe_positions(row) + assert "|" not in header + assert "|" not in row assert "ā•­" in panel assert "ā•°" in panel From 3679ad380b2b5f60673c22d28cfcdb06e19015dc Mon Sep 17 00:00:00 2001 From: "Eric W. Tramel" Date: Thu, 21 May 2026 13:48:08 -0400 Subject: [PATCH 06/19] fix: avoid repeated column label in progress legend Show raw column names in the async progress panel legend because the table header already provides the column context, and refresh the PR screenshot. Signed-off-by: Eric W. Tramel --- .../assets/progress-throughput-panel-demo.png | Bin 34908 -> 33223 bytes .../utils/async_progress_reporter.py | 2 +- .../utils/test_sticky_progress_bar.py | 4 ++++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/assets/progress-throughput-panel-demo.png b/docs/assets/progress-throughput-panel-demo.png index 60a7756dda081dabcf8ec59a4cf14fee80da16f8..bc49efcec2af1875c5ed942e70599967b032fb6d 100644 GIT binary patch literal 33223 zcmce8Wk8kdwk~UlilTs|fPjH?NOvmTEv-n0NH-`3Qliw9?wF*~jY@-bx6<96cT8M+ zueJBt>zs3c+{=$83z+l!=KH;4jOTgAGhSasd5No+NiL(IpiomJ7jXXfNJLJrGfKj$R(U_z=JUWbah-VfdJv3cAuKZ|@6~guu29iz)hi{J&)|vL)|5sBK03X;Jt=G z-6UF%ZwWr?-+1}j6x6?H4!4=kqyA0Dv!O-(D;)Dy<~P(g3lDVuKfFbk7K4hpaU92E zF{N_7%Sici_?J)@@2Hi$ylKyTYFzPCKlocky(x2R!S^yUs~(e6)&=z= zG{sB*>xGgsc%m}t)bi_P`lihPve5BTbtxO~UeJzHA|CQ`{qg2<%GpU`L;v{!2)DeI z$#z4>I=C~Kv%2Yh!2pg)kM!AZMSjX((qHUZt~ISL$2(qC z$Rk-$_vdiV!JM}-a{ z^4qs&itvUY3L(20*+HIc?c$Ze{Iu-s?C@}P9i5TSPa4PtT$2q5m61K8qm@q@-W3%U z;rdbv2po5%$Z$O!!C?=IiHV7ei;IfVmX~kq*Z0y*@>8~9vrr8QBUg%4msFM3&?vT> zZCf5L=HlYgpuwM?pC@A03wZaADXM;KtV$zS4+{%ROG}HGm>7OuVLR=&u)V+0!aO`Y ztP!ohzB?RG$*U!A?KT`>p7tb3-+9PY`21wzhPL)F%aeLj3k&~{zP`R&w{EdUEPZ>8>5$dS=d{8;<7uOPlr@dhKgWe- zbR1!NEUI1H9@op-;j$*DD%byHY<2zPtA6q2Hr}XY8l|3`dt6jYxuf1_Xa(^lTMJ!Y zy1EC<>-mc<*2P|v<3!_bclYh)5>2s=kYj&3YKUMviWII6P1-t%ao}e$$Tlr=w*tf*xg`J}Guw5f>NF z)+#J}_VZf=n~9$5{9UA~R<~b9j>A=$&Ad+s0aPN#E+Wy=SrZMmv{G-ZY0+*!Q-0_G<1; zdxMt*glNyH6s|6(V4V{$tLD{iZ(jMCf~P?!$NXKA@zCnat^~RW_kR6z$B-GGHx)FY z;bY^)`#ZaFw9+!?MLJkmtcA>~z)2iaDEv7@ztz2_rt#cH(4 zy5qBzLVhLptfOs)<*mf!u2G6MHyv}Sdp!&dOE4gI%@r~m$jIJPOY>q}+{!hYn5{{O z-S}?bn`!gy)g|wnS^Cw2H*dy6NL_wCF;r+lEg!8&Lp~nR4_j+{d$}XwB@$`&`0-;) z%O%*RwKC#&Ra8`jgoKEQh&()wC4$JsBqZVl+={wV>Gbu-;r~a=95R<=1q1}Vyu2XP z?%%IjT3Y(}@ngP8dw7dzjf=DM+}zyJcE3JEJJIaX@v8-klATw!tel*jOiXRv-6{$S z3dD$v%EY|<{OaSK;fjh1t=xp6)+{qQb^b*5Pw%fL$VGA95f9jd_efDTgJ`&N_3Gs0 z zAH7^rl}dWMv(wf+KXV(ysZzbZPIF`HoYN{JuXdBgnG|+vgY%u!S(kHX-5}h7%SJ9;%lb$|hn_*nfA1;}{bXYtyR-T=5 zXR*|v{Im8&o2n`g`r`G~>qX{n^Dq0=zUwwL7R)x2x%OcN-Qf~AZnF!grZ~+ed~Hv{ z-dD7Xvz|ZsrSnbp&LtlZq(y;76**!~<{83hG|BxQ{cw$rm`+|Pap z>*z$6mhMA9!VWo(J3W#hcGS|;3?$=I*U-?2E+{D2+TB%$#9=9FK!4`vyJ`ED0qlbik z_6`o(#ZO05<)R?Yv$7akku(I_Pczfg&AIZ$IW*_5rI@|8RD@&$?|~~+Y}Rf3<@s+P zK75!+E-KpbXRb1}vSO5vjEofA{LV&5NLYPxka-j1(xppboNFZ4uYZh;)T?w=Qw*7y zm`F@a3<&Ug+J4D9-Z@c+m7YFxs4hiMPcJ1UCGcpf=&^*12+qb63L#I+uI@2o`@_)e zVoUKtwz)^N>4XS%8Dr}Ko9;)`$s&Aq2<*5{E2A5`Cti0>m;7ndqO0&41Xx#$Rk|4{ zhj7~|D44JFtv;y#>FlAQvKw~H`;-`Wd!(()yTJ=9dB!!SSSam@)!N<~+xsHjJ_%yK zb7&pWlgd87C7o#PsW&6fU69WiB|0{o9M?C0q#BLb6f$Dv%*Vdr=<;={z|KqZYC!ab zz-!VJ#RS|}uA43O-CEoUqmOHEtUN7_NOMAXoyO_L&3@-S;4e$rr=<_}v~Z!=L7%<1ebUjZsnY$aZ1={y~wv?o2N0V#jD|{}<`|BYy7!SJm_<$>$dq zT%Dbl#wrh2M$1_vWGy)e&=a64GchqiB`kN@OudOAB_-vyvqUW9S%E@=V4%EK`{{9wR`F9^9UXT_9Fq+}CdS5n zH2!0iZo6}x_mA9X+hX#rX>9)6u7R>MkZ+=3X_*DPdF^M-=*Wm6YlO~8iG`^T7Fkwy zwu-Xy&hjwg!-vP#)u|+%$9b*%1@SJI> z2l;!K1Cej1x<$mj^2UeUuT<)ilGY6*2Ujw0ZvId=8Ommi6TQXH8~(mv$GmTFs>|Ep zoy?1DkrUVYbC@#`bRcg7PeEr;`5e?h-d=b9;un-`Ve5m5jc^ns}Lq zg~vDlkTiL))Qjpp#{xn!1qIt= zS_qjW>qY0|78hTiJN|jB8zB?cxyQG6PbQYpx`Y{YmXoQfrqV}!K)MeA3!;+(nj;39l0GrdOn6o{`Hy{tWV z@bE$)zl>M7ut7OuW83UOsu;Mv_VJN0Gecev^@9q}#>S?rt6L`p8%9E+p|-YGz-$YP?SqaWA2DYP0^mynQv0$5Y?WNF~DtE+2ao2raV zrscpOKV8O9t7;`xoZ|yC)mx-cV*8(tl-xqJgoK3T>XhF8ASo*+SC^|*?CG8{SZu8? zEiJ92)Ct?LHtkv6zQGhNmg?g2vVcV&16;x8ooIrjtZL1qKU@Mny@I49&2Z`NNU0s% z=4iQ-*JdkMnnIkQ?NnpA69w1T-j#O&K0ZEGRi2Z@7cAz@>7^;gd!9wkq%2zA;5&3S z`#Q;&tmV4xD5Mb^>UNwm-i19z-^Ree$k5~~?Xe~o`aq(kV>0fL*};UPM@~tH?m|jp zbb7(JJDb9l3?L1Zoa4h?FR1f)xVYqk?#>X{OG}m_A+Qn1xSt6hMMp%qEDf+c zdGZ8+2Wp9j0R&iCS;6D+I+#z}+Omg@79SrE8^CFyM{4j>mfsVFxy3~&F}yT1rdxBJ zfDtMOSPETKRSTn#u5yCQZ@bb(ISZ1LDW^@=A1R9WQ&Cafy?d9ImbMuko17nK7SsFY zh0L9uozBiq$b7b?6#!_Uz&JWOPG|&{5*VER{BTDCN)KS-@Nk#+w^&$gTEZDmPEO!8 z@SOW|^{ZXC&C}##%r!)rnR5YOiIe;8u4ljhhRdjHRf84&?x)_m3^Uhx8cQh5bEv~ZDt%BiaOW%`&v!(7l zfee4mFx!-$o)Ml<6}s4>p)M|7zdPQ)9rGDz0#Gj^38w75bE79ux>IBj zzu{AX@V^o&FB5&c2^!>|&7|m}?2q?Z~c@NN-o0z1fq)aa@ z9nHph6tgG1LBPFlu98A{9PDII{VtRA z_S#$9rEUOS(&t{<+If3>8w*?m6~I7zdZ8Mrf!f6ea_Vjm%tUYX($mw+KoPP`XV@81 z6Ss$53d~zxULGC}q|H3|DlYEt@3)@vk!fvhZFol(6&H8f!d(4Up5L9dc=l}~c-0To7#@CnpETV3(EOnNx^oc4*4#n+Cv9M=TP0g& zwI(8b#}-JbVfi7~3xs+n%j3?gO-Dkt=q?K6_pJn$)RH)tQ{L1YNwgBA#PTEp_T5-L zWqW3MhlAbh_}ilTs9S#`Wm|h-J~jyz$30f_?yAZ1I3cE**OPSNB9v9Q^*Otbbo~#k zGkxE^>+9{!o~R4p;o*Tbfw;W?=E~>1ykO}RV`F1MK|!NCYCw~zyPDi@~qONP-O}xvM0Ycrxx@f z#>EAd4=y&jv@}xgWIgT9!qnT@xxT)>f4Exh@8^e$i+gl*MB>tM2NVS0cX0FS*Fv#~ zS!>6}^ytF09zJ~a=8fM=p0MRFSqB|Y;e|ou0_6C`A5vD`M(ntczl=s#pSUP|6q8&G z9!U%sU))@m5wkHY?8?q+%>4%KU5us8%{T=TMwt-LaWe|` zBb(jfNJ+j^pZ<&EG}LTKr_zE&5_PO^0h(;$PB4`3VvX3OIIc2hi>j)ImX{yewF@{+ zHC|h{?Hd{@18VHFGNReb`uk)aw8>3#ytfAIqp1VM+!u!+-5)ZPu-bp_Qko7 zICr6g#UP{9Q?`u9N;^qx*4V4}j`1*a^~5yii*E~d=X;Jwgw`5n5X`$v;i8O+`hR8QgvZ@*#lTY8pmBDV`EUQdRtmrzJ3*< z3W5I}E3rxXa4`tk2W1Qh$+8>-KMz}p z$MdMfZ?y~m&OVcFSxH$La819OWnh)t&0+MB>Pjy!TwNIWoMda;m!%oxQ&eSZ1Pmwx zg)TI${>aN5B2rRK;f&g`9y^*KvOpF95CHWsoNPgz2?4b(A|j%$p}{{1vAsO3p=Ov_0$O86;L(+^UnM^9#r^2M+* zbKVW~&fF0SiQNH}!_B{4s&yOHNeEO>?q(61B0EP>S=nh@6|_^+pdluw+rQgCu!|{h zucJ^kr>hY8PYq0`8TnFlb9#9hvStB5ds`))a}hjtGaw1dN2gD}RAZFr&Q2E>Oe7KM z9wlPL;lqTTsj_JLnYIwo=g+jM0d*nRb6?f$@e7dZ4y)QtbOh226o*75DRC(%;y^tz z#uF8;`Gtf;h{zHL`ztO=JV+Yas^c1i!f?q+8hpRiszdqF6yeRU*Fdv>KEpa&E4*M2M3^*G`6+LkDf=hE}qvbE}DMW`PIq! z`^HW1Ws+3b;J|!$H#cQuWJuYJWf^f*TfcsNe6+U?zu{nGlaZ4X@Hp724)!At?)%XDf*^m4#wm_a@icv2_ z>j*tu2Sn%U;$k&iWCe8I%*?D%QZf6>7hynQkimjTxt{*`@*;#v&Mz>~sy8F7xY)_V z@~j;bPg{%t4Ec_fi|Njtq`pfTcSD-cMcxJk$VyA&qTga-S^=Q{^yyPxUfx1IYSh)@ zBMJ~RGc)RBcIr{_TGR@$42+CduU;)IEDV!Q0oV=FGz1CUorC7tBZ%iIKyZ4Ol$7jm z&MHK6+tj?dIuR{{U}Ds(;O67g9u+$WZ~B~2`sI)esZV}>J^*psuC}(erY5QEyQt>a zbHZ$+Ld1m&7oh5I@2yW>yLJtDJ?Ko4f*uDKDco@&2au8JHiVUzlstIwAT(+pbg;A~ zLrF~}~QcX6d4J@o3b!^_dGrYv`j8GA%WYAOyzv)Al&nRTaLCF51ad3BYPGfTI; zXh{Rs#QUZsWw5RP$B2ktfpKUMpkKU5PwnC1LBy!V{&a-fW=S9QUgimlk4*a-XpJ_r zOLi5)rwHoc;a%^ZW_X5wWpV7X3_x;m{yK6TbhUvEaN2OK5E*w^m~ z^SI1)+!Ddr0htK82n2Yl%YbrJQe`h;Vs372v2$@L$jMcL)DN|CaZgJXb-TiBzA{8Y zN(gk&v|T66#>W8f{QUOXf#$5q8829vHT$bE7WwAydYB=#_4VS}XKgmL0@H$jZFzPi zPbj-Id?m~}u9%jVMnp&$E9B(~ayvu-XrOdCP#!ON;}8=^ zgodi>=-eP8a$5T#B7zfY_37+J#QQ=gltpmxFN@VhSPc~@*{p|9D?oeb&6_tvLqpOj z%0xs6pxq#orKhJKFr77S;|J?f;7;IAU@v_B^r-_BwDomj4)f5P7_dlmIgkjRo*eB1 zM=i9e$dwP1*v$f>n1dm^03u0hn2fcfj^JHB|N9XqK+Y2SQXu=_P^4ToQ3yP7| zFCV1QZi{-)a4&CECYGr3m_4@S;;}dL>GM>!s;@><477ldFfdTh(J3`F96;70$D4>4(H-z z@``f&{>K|-1^yP+4zRaL)bRXeTDtoB$T=-P-oyY^ftk;1sK5+%B>_5!T30Rla~Kc9 zN=r*`V&M8Fk7THNi56Z4{Bj8kOI`lp;DAZ1AblnCJen!1?#25cBhp4$*HwDuQRecD ze17Aq8Od#Lv+S|85l*pNssAvvVFj&2l>IZg#O7@K?%EnWItNF`lOHd!0oOv+P_teL z2nvFp0rZI+?d^M@hS=F*Vq+7b{{&6;s?IYs;mzb(13C=RiCv9L(n`!r3_9o{15P_` z-pXi<(s&lkuoEs_xuPmRGds(yTc)~Q$7MA%H9JeR_R7}S)%5`I&q8@JWNDGx-Gt}~ z+}b%S-tiId&4tKYS#s}zL*lpR;=Q#)GDHjypKUMne*5+fww}<*-V*>yp!FzwtOFQV zh+v%taj>bW$+ndL)~(0T>42Ts)Yxd+sQfD|WZdSbTM*+evoRUnf6YTz70(j1Fyam`(Cym>-KlbPIkkU}5XygrwlNVxu=O=2k*ciOAE;o`OI*(0-u?!U ztsxLdDuq}bRn?yNM(C*MVm~gihix}K;|_8g#2=6)b(^a%Uc3On3|c1OVh1g)$o%|G zP&q+wuc)j9NIi@ucH!*BgroN!0`nTfKUo9(9@#D^Mq(FVtOkE-_>vu z>5%Ke@Bq%x(xRh= z4+OgMLb*L^w>C(RbAHdK;i%@$R~$7Gv3q1-+FO0N_kE;bL*hPoF8&~I|5_t(5j(e`mucG;bVA5h zR4$AYD=RDW^DaNW{PtQ@De3(EzXva*S)b726Mz<2^)>j$7cfcN`Ui83Ra7r1_cl@a zFfC6EMaARy(!HNO@icof<*U|1(7`}}9aZzT9|DaQ6NCvtu8HzNZ1?tZi!!9M| zwNJl^0X5)Zxuex!?)TQ#^CIshDW?fp*w`}NkSimlcsFijMT37px4=|sR09+3s!0FW zj;tV+<{539)i{PG3#?sDj_?%U`$YX?63o{7Hsgn{#atGcu+W69HrTp~eSAKjiN+WYVJ&u05yBs)0G<@3Zd#GxhZ_N(?r-1;S6I(mZ*-g+?$S?!j+kQL#wd)b8*oP&>a+62O@Mdy2pL}gWw_3HF;v5mf+y9 zSiXMjY{8b6cGncIV^v=RSB`A2<}#Y;hB+By*Z=5BQ%;zFW@mRe6J_1p8u0e*K!I7v z0{!~dR`k8cqOe7hmCfAU-3<+=;3Fj?fpM#!|Tv!#u6(^;6CGc`3$ zPfP1Zs-y)8oj5nXzm>rZc+bMrl-sBo9}^Q38eTwDLDIhD{Rldm?;%YF-pTJHLQkNS zfr`iOl`7uneRGPyP>AtG!t^BiYaY!&Z6?UVk}9rMAPL(;as^5tD=GQu)2HjKhK(Ot zo+}S`M}%zO7o&@4R}-J4Sd-3ZhDPc~Yo@Q{ zvD;C-Q9mvZYX2_R!q}L_{^G^!uzr6$TZ&XdL$z2~`0#kzx)d85n-&B!kq|W%mH2}P z(6>duaKUk*XMSlgKO^hsOluUt0W1LhYil%2OoHxvYgHbHK!Yw{zPzBv4+s&^R$1;v z;rk(j#6`M8!#(37iQQWV0X<23MnswNkN!kc%U*9-B$h4tcG`ixsA&gjI34LZwjnuW zklJX%GJr+9{1TIr&Ye3aCnu+-Pf15-4(dyD)7O_5A$dnfkK8+t9`E}rMM;@{g$&i@ z8Cdz33|0R5axXtYqn(2OA;(5-M%vx?v(sZEr2Df@`kYTof&4Pa@i75P&%TKf5pmkR zYxZwQE8D=v%4!*4*Cp>~#|PVGL;%Y9zJC4MECpRfF@M5dk3zHsM9#|uO^US8*5ZjV zADP(U}l(qU2kB>^!ibUFftWKiPhjiBPonX%48#Rz77&)FEKg>qBWub&fk{K^=n zo`35~l>#?Uj~42$(S-fQf2F#AlD7X|Yk592x6tP_dAQzZEMJ&Y#rfOuiNoE_o)^_1 z*c9Mz42k>y8-@I@uGtV(Q(i;X!Tff?;cBl_Fbhag27_BLhR=zd$M!*jVtN&>fti(Fs;pvjgzCzc8Fbb3kyh3nI&(4E6z+$ zXX{n+3J6qw`m_ed2T*(1@7-J5*f9T-Q)-1LB>Y4zN0LMkwBo3#sWYn{rC=u&fsmmm zbMfrv-yJ)P;Ze)qU^5Q5{#dW%*<aG)|;5|oK+3Sf4# zU+i06E*{sba(4m7=r>6|TH#{1vor`sI#7Y?>gs&3DaK%jgZ2YD)F|c*;LdB;ly(%TIIlet!7<-@Mb{Bf2stw=D5Rqn1@7t!i_2s>3;s-ETDc;z z2sb#V7pd%qtgQ5i6ZB|q<@Y){ zLNOP8-=H9cM~~udS%rkW=9S64ekTuUBoDf+9?fXo)qAVglRrRH@79wqsQV+9qh-?l z^yn*ejb;}Yp=6I(Ot^V?Xz~3Zg+aIf6!JBPmai(E2e>aTUb+70@#7C4 zKbAm04W%ysL(rmOx{8VE_VYDSXb4%HX@AZrcx=)Rq{lpVM**dIZFNe3Mhij0X8gUQ zyL-6I!J-ELkK})_J~K7-KwMlN9^D?EzQ=Z$Xu@?6gP`N)XYtIzf$ZL+FOYYjkZL@9 z=nUG-$jHd2PxruDb%BZRY-#6<5hq*S3P#j`l;LW-u>x*d+S(>YMgVl4KSvY60WJ5g zynHPGU}K}Cdp}Tr3DVRa)Jvbwbm?h*&|zX`U~pgi@xpH&ByD;OQFr%>n;5!fs75)@ zvmeiIVnEuws&l5r3+MaGklnc9x;3{B4RTN>GE3gRek}$FNKY?Tl`aMh-1=U}g*KDo zA~>K`la_o!Bk@IkD7=Z`a(?hC;x(W(hRb%U3#ss6o*SQu@Losj4ZsZO`%8<>StGIv z3P5#5=(#ra^*QcuOoKM>eG|$$r{Uw@*m3`wyf_sJ|B}3V6*b`l0fCs`R|a=)cbz}c z?9`O|-cKKJtzBv|$>^k%y_N(B>7a8v=ws96-7AU$ zap}H=5V8ns`=z^kPOG7gg!|`32EghEwyPFT0zACQ)zxt@&#$dL`uac5&E8>aI(W2q@7Pu<8#?a z-i!SRINe4boXNo8pi!bR{#vZ^^IypxJU}EspLgT}Lp?nOAhbc@V&Xfk!4|?l*JfT< zUr!GGNwArL=ZA<%2jlW(aJ!!F3|rgT*^y@Tq|+mdme$uYUI%!f5(B#eB%o{}un5UW zNnOImF8=gMQdU-{)OH%kUXLyfYP*`2Mk?!+t@)aVWTV7!LL#Emy$2o$EY$BhM&f>n z*6c$64>T!u7v0f=r|v>yL*M7kE`2}XJsgiqUbGx3na+s#CuE{P-AE+k|5lWuRYq=2 zBlOAdJ$eoiWMDAy4ouz4L%ogQON6h2k&-P)Bg>=ZyAl*0_%Hx~kI(+3{t6%um@MI( zIip=@j0F_AnVE~ve>(>`!Vx3^ko=$W4*E|p7*0%q znhq8Y5Dq{pC$%~QVb8-!5%3o3IIs5hoJDZT?B~H&VGFv42o5A1l0W|l>=a-z1R5ok z(nZW>j55Ik2Es@G+SQfU+4sEO#M}^IvYyCJH*RCrHnc^Wf-ywM7(90_<)CH9B_%RGcfOZgAbb+1quc59UDg38`w7H(sBm0-g zSKC)dtrw%F3Q3R#69U))5L&Q-3F#)z*irPPrA3~kfNPhRKW`}-cvp+OR^hg@L&K`~8=gXAWSKwe8lZLIBU01S zFum_tk5#}zt+z6PFV6cWzx&=N`0(QDH`@g~mirs=GGoKzDT`^A`v(p)nJ$sY{u6>A zQ35AylXT#XU;%{KfTxLtjm=C;dksC|AHf_h2XS8jy_L(}5chAuZrNvWwGdc(%#R;n zwuYunqL@E)G+`t_bvG<2t70xdMSfgOiTaP=PJ!S?lM@9tyamsCLR$}LVI2>uIp5wc z1j8mt*{DB2>#&{d0=jzcA{O*t4-O9(7Z$L{`3J_wPt)Q&n_FA03IGwpm)27uJrNve zH-rZVDMM^k4)|P)%xu?!ui)$)So;e z7;YiG@a#g*2?PJF=NqZ2L$`YU zmb(W6l07UF?jNIsrrygb;hP=>6!ra1kI!ycF0A6okECL*J_C`z8Cm;Ni2k3)4*zdN z^RoxYF7@Aa>nI5#5x^v_6BGN!fqnRifkBV%?fBUJgM*#zxS`hH0WTHqHf84E;Mm^Ww6n44?CKpmZ(5rFJKae_{boUE+l>R5JWCMhm% za7+vtwD%$+G##%hBMV@J4rFJb@c?aGTUsnW(TT{IJbgNrt1o0?G7Bz4;B2b0ircHA zVK3D|h3M}uhE@~E)PPe!Zve%zrM;bq{SBSAl9+&OEI-fPyD44dC*TGEZ{r@Md-T?h z3^jUdYip1W*KasaH;4HXG1KAuHSAt1EEscL%$}N>f{YNZYY3wL;NT#z)_iU=V`IYW z*J;S_fdinYzdv{PV({|vvcBiO(YhV@a!O}q7CmF#mkawD7#M2XZz;TugU^x8&tP#d z`(w}~Zh0_&cD4G{6FSL$;}GbAu3M7(X(GvKX|t=Vt1~mkrlucZ8SwB>?C#)LH+^q1 zd&@f9k3g+3T%{Kjjx_rd4P@Jw1Kb^J#Z#E0nI=ckV2HzVbY{ zs_F!UIv9k4QM1lYVs=x-(#{IiBVARS4r$6@XjbxZb3-ThCX85(i~wN$3jF${-RmT& z;%@S-i~+xlMR?w*97>>J`smRkb93+tsfn;%Cnc@koA7Uzg3hw%@c}$*GC}u;IIo~( z0y8$Sg9pL9K|IOB%L|qedeQ@!y#fEVm8B&E`8`OD6&{CZMF%blk1M3 zZHheMnd?pq`qe%MI;68c4Cd~Y3*DPZaD@9nLG{xoQ{tZZyhl7U@?Y1G4j#wReSN=c#gBXRLJ5Szfnh6^ob0kgt) z2XyzrU~gz>2$Z4BaoH8XBFcDjg9rMcMV66RWW1m}Qgd?l!7A;o{RH$59VI42zXmZ{ z3g|ARBqa5Ea4_r%HqRqx z@MuHR)1s#;-GDwL$LAt8xwEq~s04;aMq#=MFt~cX@Ny*B(?)=SfV%^@11JRQ>guq9 zOI;}~5P7vlNf3b)_rB+6+1RXdSeQF+!eScd)a#prbNLgKt)M;{rdG#IXf77fsX}ab`ZHR z9ttKapdYRb=i+++qeG7kjE>;2BgDtoDS7r2Xq?faL>+3Wvwux_uztFRehVD9Y;4`V zy=vW#4+!9v1>Sy08_1)^DhIKgrcH?kAK0L`abr`{AlxB{eLy9Fa)IClvL~2V}aJtC2N8B49kzXZ-3 zMMXvM1%j!6uRyZt&uNl=zIbRrmXvTW`@$}Oh|q=mhmEyecqwp4BKzGR8~H*ZA~Grp z(;F1H-2!w#Lw`&O@JzI7!O(##gE-2>>9yYH3ID1d2oZ#Zh9)0H$HXvVc*{~{Hn5^e zLyy$pf!59%WPIfOYNFpu*tj|}GK(Pm&}uAa2$hbSo<+AvMRHMbF&GgXZEQZYOanra z0);<6AFmph{Y`m!d2w-ZU|`^$@C(V(2!n1@_)ze;ZEbDgV$hm^x$GQSGXsNQ$n`r? zZBtW*M%YdDJ@Kj1;`)ULs;a6fnF~OXVSWYF#O78QvxdFCG+vEzgh#)B zzXmN==q@Dh%%diFBQN z$Y^Ur!{lg{m9I*|-O#9yii`}m;>Dxz&8l0SFoP-7=51ALzl?03SFg}PfdnQC(mXiN zG&MB`hldwIQfjM7@xBSsFUEub{Wk|%;&7J$DNs^gDLc5wU}_z7MKISvL>+=u)*@IC z5f%oqqweU#Ihr4RJxuK&I>FE97~0|B2!{+185=A3=nI++cX}SQ#Lz|3?FSwzfhG)~ zLLrvF6p%AC&p{yWYY0b{5zw8qW+W!6_AF(RpvCXA(=fGy?F}|iaWMs};oGpVFjYFR z3vRBjD~V@Ju*E?w0S^Osi~!a^PE&7VmT^y$sgto{+1lRODFUB-lOasP+d4T}XDy#I zpM=l`BNQbijA%VjP{;)UVZ^uIH|Mm@4qXUfHDOp9!>?4eu_wyo;bu=@H-OFzs|9A= zK$0yOtJ?olHI<=FNlMVlZ7R3%^T^K$5&}0a!#N<}3tgl}U{RX&&iMRk;Sy@@}K$7W| zm8XV=lws0jWMnWTWW=gj#}`|uzO0_U$Cc^lr>vBZ+7~n3S)LWJ^>6SwX3gR?M>Kz( zgY-L=t<81TeYrPX40=-zH{}ng!nk;N;P-;^85I(e6b%O$xSgJOz#h=ha}ldJ0q2%- zRTEl=Ficx43>9V`;!Q+DDD#wvt^(abBObnSmsKEiQ9y4{F$nfx;ZUrH52!a#g z?P_SXR%!Bd)Lq-0ZaCyFGKZGw{ z^vTM~g7Oo#On%8U9-;|81pEQycqFtQVE6}+oY7<#$uSdMiXsgR1pT4?K{rG(L2+tk z1~RdL`<|jHZ$|dTQk~fFaA-dfLTkT>9ukR0H;D`!5kV6s4dz4xLqiw=OF~VMLro00 z4^kHN+F@(t>XxfNHSGr63l|6DH+0`-XJ_NMZA6!pG^=wHJr=Wzn;j)1I4r{VewMFR zWfq8J^`WQ&;4A>IU^oel(f6-80oMy;Yf$m4-FBZE7$mCFK?qgB_k-6wnw!6WAAxy$ z1OfrpL0PJn@84kvLHxLiM=C5^aoxJFn@-xHwfrp4D(@s*Q)}eAAn59rH0@5Y>=Tr` z3}o68TF4OdfD??4-=xaLfaO+Eu>$s zI?xzl6pazi&jS}RIM>0J3ZfKhWEKRqYZOT64t6{|`-9-{@FMuQXT5;L?FU zT^~fTIy;L952tz>in3fl0Xf{7pPZUXR-^%12T92-edA`*OI~0fy z`Oj|CZMQG%;N-&s$!ir73siP<^M(2DG$^EC45sxgH}9o`-lLyP*s)9BXDYR(#KH7Q z;?#F)y85$HU|~#!)vG=|wss);EwXK3z!}&q%vAu33T>Gtz-SO%;u(d3LtN; zp!T=@Yp74)Nv!~ec|9~E9$@WLjKxR%)cB@eh zCV#cpZ{94-&ja_7?BIV=9{?50Hz2@$y^hG|3Nb4v({K<1^pIg_wZH$jZc4CNK)C?| zDX37AJ!RQCxx;k{|>(L)(Uctf=go72}*bm4KiFqnpzTuALKfbProV%KB$%eP0JDz_$7J*kdM?vq|*4#|zc1{fk{g-`#@!=<7 zst~!4_dx61*qCn7cV8C+diL=V0G774(+axTQR>=uIMqXO=LCl6pf1AJ1jO3j+A07E z|G|S-8^f+mi7Rl2fboVv8v&+(0+%rK4D}Ao_|w6aFv1D%0H?qRf`K5C!y@!`mLpq~ z57=>mD29zL*usEMT3!gCJlHjE;5mRsM1Ec#U8)x>75E6A!5lD0cmkm&IyyRr_p^l_ zS{sx<5XNvPzPdw}H^;LFYYG^&LFDkh33z@M91jHS%3uHkkO8~+7zF&cc5VDh-P;c} zH30p)3%0Q1?mtSJ*$uXR?S^!*=>H^lD2ni0 zzI^!tr-8`Qh4q=TMZN0i=m0yz3M_b?9|0&XM%AE>X3y6Z|D$&% zQkM&CZ)Rns3XC1=UDAw3>8gdA1t>uso#hh{U>R^Yyf}BUcFaD5vILNUu<&yjX@mn9 zz`7oatvxh2Sb1`=2p1t_zYlxiyhyqm^>^?KECca?Lgt;C;f#LSy8|Qz0;1eH7UT=) z=V@wbk&uvpJApn-8b~`-X;2Z85)bhO5Uh@(%6Pm_yT81$SW~~AmcJ{4Su5noz8Mm1T6F4%aHW!wr z7kXxJOP9PqSq`!q&C(n3{e)rw{G{q&Arq1!U=DE!33z!rpasYP+hJm(P%XiSS8;lB z2$TM>J=b8Z9Z(*OShpOVAS$qVDF_{dnbm%x))zt^w2I~(3IeWtsb}`~@Z+BD?!_Kt zHxw9t!^Zboxy@g{CcyKpKH88;X;*;O6E7Q^O2wG62C;d=w{MBSR+*U<=)&4QvP|Yy zpA5lR+kk*V7&<&VeyH{kvtG~<1cw;d+&FLE`~fD7LjdA{bf779zV*?QCn2C@goNaS zWd{lYjM^Pn?vBBrOlGI_uQ9b^3a=xj4pq?l{)2&kAW+j(>X)t7+QOn7_y@oXu?G(T z+CeDO^YN7%7(f+x|NcE{Ou$Mzb38JO-JdXVh)uSVqTs4cEIL|2Xh}gsrZw>}kLp3# zeGQ5rk(yTrc91Ah>pML?3P}Uv9*Pi*>(Vg|10sj(!S85h0YlN#(jozgpw=CdNoxe# zESPElhuM}M!5)G|^H-%~bXs+RcQCjG@H*e1?y^mK7l~Es8|cNLP%o)<1q&?n`ecK@ zf&*%cxdg?FgE|8q@=56Mg)QErvbD2Ygu_puTXh)|vsdknR!Ebf0mbT{E!zoony7|M zd6@R}gvBP(cUP*w#JhHFeIYY9me1)afQ~rGVK55MXnFyKvu8G=A3P-1jccT9 zaGC|397;HY+zM~Sp(CdrDTb-EOq}YOS@xnW1RO3K?#RV_5j8Kj<^EHwt6CDU@0!~u zZeE4sDCn3LVfHahn!aizWdtqAp(#-h^5_*(PH^7>hnM6zFfC=FY$k-0o{Ex_q3xO~ z9)}{Q;W=d&0&Ow!hKBy)V@KLI7kd|XcX$i zm#Z2fVaA=Js%~$P4H-{MP~|kGHswNipPIXc>9#_JDs%G*5--k7N&IO>379PDb1S;y zHG7m`&I|lkDmGO`MUK6g;U3>4!dfkD|7lyaCRNUa13DD`K|w_0BR3uQJ>fhxx=Zx{CIOD35S&iQJ!)zb5Jq8;2R_a2 z&W;|KTYxmxy)fy4>AU;d+Hk5`FNE<#_fv2d`o4WjbNF7H0FE&MxB)s2%)+#^>3=p5 z105({SpDfm5fzZV8G|elFy!N#J(oc_P~n0)PYkqp zJ6l^|yP(y>U>ZbK5E&mK8QJkte*5J}lzyo|s+W=lWq`~zapcQ4aBLuN%%VgiY|@=Q zp10td%FK&seN(zW`P(MlN}iud5wtO(!2vohmyTSp#q*2RU`?`MD zc^>C+9>;OX9?XwZ`Ci}7u9}qrbhSEO08X9j_U%UHMhTJt&KVmjg-z1*iXEY+PNHgl zf8aD1apJwQ8G_6$KWtv#^L7QySed)WAD>1Li*sGMp^6FiIDS2aad^B;%c<66&`px=TTt|g_Tb&X6*Z-2T;jzK4Uv)$D z6}MM+XV&Hdrb)2GJ>RMCT)qcJC`m=UdIF zKuLnM8f2+YDnozZ000@r6xLuRkqMQq@3R$L>Gu$1AXr;Fy8zWq7~zz&4VNb$?K3ekvk=_^z|`V2O~)SB zkSMY_N9&|yFo*T+>j76uq~(c=I}%jB&xf;UL7wAOdiD{!w!V_`lpkSof(V-D8TaZSUy5Oov`Kn@B-nxp6+1A;|={2Mh zZQAsf8HVKpq;y8Dzsk5lH<<#1e0`l^VR}U06rHynC7@JS)??C3`&UMf!Kk{>$tmg7 ztI+n>^(%hP$C}ri&>ZRvS-RY`rx_qXw&Ue!R{gF4zTiqUlQ8w?a1>mD{~7B zwUSD|stY@vf2{I(*NdkI7(DD>FlfT?ZE{=c%9ZOi^lZB@?@|}pgbh3{urW1;K^rFK z?K^(rgxBaf77sf8A9Kx26ZY)gs~Rq_HFL#Z9quG#f{$MR%-{U0nPaZEyH3@}uFEpC zC!O28?x#1xUla2?W4iF~Mbx6`fABYrrt&EO7VDf7z5UF8O@r0{#N~)4cDnK+=SAzw zDk(D>f7$I3*e9LkfTYEPjd@*F+q;hj5^6%=&ui^gF!XYgR- ze*?NZMDN;-8!)5o7%w8n6wsas8@qE#^(_Da5G`w{C3$4mb^U5~9cnP^ACHKzBcuo{ z=&dxr$Uo{yW#t=)OP#QM=Z7xOsE^`D_e;6_wHes_@KYz7$f( zg{aBtK(N`xr&P|Hvhqai&*ngKJoGxrPi1too8IGxlHhvoH)OdrpLmGI-BDXD3)|%* zvbFn(<<%+|xm~jT`fsV3%0$XKZfCCD=nvPHTkeGOh01S8+7?b!8aYtVk4CG9$wsU6 zWijwY*B&^3>Y!a&@sd#r1A#zwnv#;iI1S7%{Xvez$ueyvIi{6czTxjg_v*v0i9!B8=JNT2!#zOm(?G$+8!Ha}Ub-*>?tT0mbbd7wp!Wm5JNa4~iUE)tL z8?CjndUKuOhe5WJ7eNn&GuZaZp6*jmI*>7orIYIRq0(bAtFGhruNxbQtFWcO>O}YgCIh}chSYw}2g9O7m%NYi!@at0x$9Vin zqpS8KgRQCK9(21S=eU`vHG&S_GClPXZj(FT*8(LrK8JUJt?v`{5;%GvZ3(7oj{`pX z*PYTEP7pl_73zP>#z^OP7yG|(GIc?J=41?9FB@E(T;p~krtzB|Vz#|&;?fN&ijr~y zAJt{{Pn0SyF08m>v$)O6{~&qVNBv$DSvgWWT3H{E{lrER@6WkA*`AnWUQR+KB6)lJ z7l;o){2Td#SV zn(&6HY~m#3jG9|oeA;D^28IulJ;jAv#ip26_O<4L38PDr&YyQ>*+D~;SL!2SzY+c&zegq%&@a{@ zTP2Rn)fgyH^0_MKMgJzd!zX-zxVXZ^iK;uA*Uv6KBWt46wP@57hfX6Z#BO@a6c`jR5fh)@TM0fue_J2!{zV!{`)I1 zSq2Wh3yX@dlY?+?LyD2ML9%E2`}cucceO+9>_DuJpwB4u=oHa$t*`?FMz3>p+{xAI zXk*izn+tUsICG0AYYhTQ4@reqdNt)tpaIqfx?I0}SsEyPsg$Vec{mZl$bxIn%#1*d zc4k!&!xGttr`F9wYmqe|Aka8tM+?w|5Hz-0+&Uqq&PRO?l9=Z#GD-Y2w~ic1uVEg& zbV)7BU?xqDGi6U<>Dr^;StQ}(u7CkKV2s)Ep`n|YNk1_-sm}sm+)sT=5fd03&w9p` zwA54u)1+^0GpI(~djW#5Mn@8hKGvl$a_Jsvb_2Z?wBaV1W66q6S@lbg4Y#@5k_fs3 z0?ZZ9d3k*YU&jy42^4#AK}PP@>HU#+Ga?_D#Fu-2B^Ep`GAN~R!y;dIji_H!F2)t< zT`05^y`Jmk>3NHc6zQ$iHzAHo7Do2rpalp2c6%@iQI;vryrLJ4B*^h`tL1FxeV zi;R2@2{pLqH0W)}*ocG#r_?oY!fG3z9Fp!ODEfhr^dt0M6j5BaIy(LkpHr)Z)g{CT z8a(j)f`%_#l`O%`TxGuH$sd1YkP?U-syHgN7D2M|_^p(piV8RIh_KbGegs=r3`~U2 zB7{PZJD_}WvCP91m+CNIaAk$-tJxc-4B`8EYB!Au6mavn8|2%2FMSt&q??d4hU)xuP}5 zpVEony=1+ziJhdv!J4+yTR(RMx7K>U?lYIy$GNPGk&25WGri@MwkMxsGIN`NeOO~5 zZ0#v7wZ_rWT+xKFC2-dV7ma~xYHCswJY-AD%h`TTTv$4^YmWO($OWIEtaB6Iz58%) z>`ZCtl?b_jG&8s*%%*_rh7`Hv?8h!jy;(f5L+$|Ha-JcA*@JnFP_g{tZp>-N%HbE+6bu}J$n|;GTuNkGNBuh&@TG++aC!lb<)3HYB(NB zzkjb@%^Xk?Gx*LDyswZ6!g6GC)W<8?e+=tr#7BeSh7~vW z6@|paK#Bb)zJUc|{9w-)fMpj&Sj6I!Lf-2D_2yt8q{QHZpRMzcM2Y$GroD=qwBqh; zEDF4aP+Frs$u~PT#{JM56=e@!eSLkA`7uO{5cuW1C;-(j+f-nNB@Zp$v7B%+KUh8f#n5h)L+~H?9LpH#{(Fff)-)UhsUHq(fUW}Tt|9_O zpq={l6Bd$nb?H#FZ0p_k21NQsMO8ziLt3EvD{GN6ik%=UE2U)nW8M$*wX_a9`^~4_ zN(w3~=-JQtLs=C%AH#=AVoeG#E+#hBUTeC-o^f62*^o+?WY%Zy@}^xRnzYPs|MQhw z7SlTRnD^}QJxs;z^q@k(Wwpw>Xlr++CW&QE8NK* zX|qhX^kGgmT9A`Iffr@fSe;Out0XiIU>*S{qqo$FId%)Y@SaYArWd9vV*3-L zPaD+l~H2K1rsJ(^<3#Q394s$|PR`UO-k?=I4HgmP_ag!8jy?KCP{l z_r6Kx`sM0XW;Xul@vhg6a!}GR%Mh^T$GI(-Ywj2S5dSCXj<+TVo&syr4`O_HbBJFK(<_6sDP3tfOlbAn<}2 zk!`=X7Pm{iEf<;`NHd)IZfWEGCrJUGn{$?pIl6h*>DpZA+BuFhD%k#YP{=uWJ1E!3 z$BGv_NlLJb6hbdcN}L5AA*_RSx<$ORRnd2+W*}u7S|z71(I*Qx4X{Wq98EMJ^|}xe zNhOgf`I^njDss!LpHXEKGFlfkya+@fgUpM0atoV!>u191q|%!AV$i|RDk8HvPt98h zhYBcve(yp;W@ZQT4QnV;iD(cjs`S!|Wb|Ye6ol=kv2m#o_?fzCEerVw<*d+080?PE z{u&P%NjFJhhRwiray+{P>fK(vWQi~>maU< zU7`iD0OckxGvPQ#*3@sL(B7wvV|AgF%}tG_&nLJq58~_ciPO3siWkdO;OgVIZdL$ z69iA9(26T0`n>jDYsLSBft$aiN!`1~<0R4$$$9@gTyA1Ah=oW&6(x6*5T<*$@2QB1%%90kb%S9 zw`j;4(qILcXFcx^Qc2YtED$RS245-|#krO1Xt`JBAH3M%meS@FVyCa-V1!!Co>r9L z`vdZjO`ub{b57MpxIKtG%Zpn*gOiRT{q-E7I#ovIK(LT9q?ge8gHFg8hG3eP+;|Fy z+xz;3qJ*PJn_zYVMS_K-&@1I#CRJj1ME=LTxD?cph=K%NG|v@gO;7Jm>x->arM_~? zM+jYXbQ*t^sGki-gnx=fE;MaOo!a?-MTb7dvHdY$P56(yWul7jj`>5N$UnboDyRnJ zUY4=*V&3yWm!~JW{cI*;G&b?Cgg_4|rM|+C(c5oe>!LP$_HrW3pzt7dbst#Ul}jbb zuwDN*svQ-Z+@BIHK@NnVorNkO_qfmgG3x4W46j%(7fPI@#LBCw?cfUUYH#pOV+N8< z-1dIjiAC^%RWJz%`oEEpc_}z2Zyf61 zRFnoIe})~omUJz}Kugey6q$E3$ojuk>ZrkP@(32(z5C^Ot~IZlvRklFUan43q(ATiCLOUTN zBeUh6JglXrSs%}z0i+S;3t91g3Yp|#;ZB6%m-Ce-OJM5MH^BK!=U0K!z2HQIwETsh zQpqsDFaz##wYUA7^=p66gVw&UK(^^#N8Cz?Anz0t19W7sc;;cLhO+Y}0i|_4=xMvpr z@Ks3dhHn~mCFTHIecE0`=Fa;I;m=ghf6F2%N4rC*yIec;C0T&T;uzIDr+SE{FIz(v zJIJlAxtSNSs56B?#**?=P*A?af=3YV;tBD~XY{K<^e7^k{1|PQ1k9B9^2?CeafYJA zkg(1W(RhbjzHd>S`UKoZ6+nvyDBCd(*^PJuPsPujYZB8*eIj@HSLf%o$4~g#MKaIX zQM2mL+Peulm8KsZeDrsTPrbo<_NOSrG8&3k5QazzH?kJrQZZE5YFfcPpq>oFi53U-H)sQ?ETKsBt&v=(JRKzj4r>R-0@6&oqqfYhk|%044-4)OgC1c#xGY>okQ~eY zl#`?g`2~}wV3IhWg{`d_!Wf{aV3T1>3*gCi^WlYW1s+bRW){ZA%}jIdmdF79r>vBd zBQmScTA~G6=3{%!!vZ2ozv}nWL+0GcoWanZ}ldnuGg1W?fcKv838YeSEsRj@+}gtJGVU zaltjOR?&Xwd!^`xx@I~3gn<5zkgxbA%br};fA|4va%8~f6COUGkL!VDsPnNyew(tz=3-7ImIe} z^k9r)@+*Fhtvw{o?S5BZ&!@b}s)(pwFE5KE+q)bP2`MRA_2b46+6tijdd-OOFjEy&RTaebb;rfZoG+S@utT8J=WO_#t1TbY2=7(4d5tCYEIezG9IfHq1?W zbLztGwCM_Op}8NB*5*9IG&H%av^3TGbZQcU#$dT zk|YnumqAU-c22|AG;?e1HdsKiqevEufV2hFahjt`yNnn$O5N((A?Oj`;o*svnQj;# ztn_3-xkgYL)U*B9^-WFFxFy2V38leyU}(?Fr_@{$dB2}Rv5kWR!4snak#S}b%13zw zsi5p4mrmO3lsmQ1Z?Vi^r~8-Q z90LAPzOk5Bea56?H9}4)p74gMXIL~f1|mehrj2C-wL{_^d|hbXlevC?80A^!nS$+p1@Yv06u z5;^hwy~e4ER?c|Eipl7lu<-#^Fc8%h$N2{XEw05x;l+ZlP0 zw;b|6W!Hk75efGjrM4~m_(UYetz@$b;gpISSbK>?qSRTtKc4&C!dBtj8%Ak+%Lr6d zE{XH z3_YS=!`+1*-^CH&jQO@Vaz+y_y>SoL{(Du^9i`t@P0{aW+W(OSb2afuFy8iGS2dkm ztfWC2DOAXD@6Tix1>vcK%`)M#?&oLNAw}trErPacGaifqH?Ofms_#3$sDCT%15{uxS4acD{g1q(4*_JKtXET zqyGrwYQyBO4ES6$j9v}G8W$W+pWD)@i5)>AXx&UrATIzVk7p0GAPmWOZNkF@YwfLq zf=v%Exu9(Z+@LC=K?sJ%tRK*83L8mYw7{@1adLmQj3Tx-b#>-jz8;<=vle{gP&$Q9 zadNQ@GC~6DMvORxfUMDchT$e+*#KAG$BvQd*Oug=0Jg?ULqu0nzR{KI6E#N$sI@dZ zE{2KMSJ8b}QzVDUp=@G&O}Fe4`H2bgIX~yx7dBK<=J8+QutsJ(r}?+8jOk2FhEi*! z94&3H+Nccp8#&aWSEUBruNvMi$N7~VX^`4U2>E@V)-lQ8zR41AKR>DWnb+2he0Aw< zf6p+nywinCbV}k2EZes1$jA>lvaTX?eT`|$jYiQka2^wZQ%QV4v)8h_w8`U5nW%(# zjafeA)CRRP)GcAU-mxz+mP&&*3LeL?EHUh1MH;wKm1lzH#Wg>?yfL%mgKO$b?dTcw zEWVnLH_f^7;snF{xlB;9nKHF3;1)1V}$^t@~0J2S8v>isIA9=Ewt~Vp-%gDhV=F? zQ$tlkOP~B3--K4e`SQ_~q@u*ctXNn2b8Fnq=<~?SKU~z?u=WeFystLc8LcZH-!ZJM zv2Tod?T58$Kj~Z;kkHcbKq~#4@IG2vS{Mlb;Ayu~Ip0WKEw0SuS~#X3cU1>(hc*(b^js z74yGYAI`CSTQK~uq*T`TlA|Bw!Zff3-GqIF@Omb2pbHOiLZ#yy$hnt1Fu;3&6H1k( zrlzgvy96%~i%HCGMr_)IGX$t?dTCx{pXhZ$-efA-dQ%(jxYs~vHIl6uPAup27(o1p25!fQHe;W zB2SzN-&;mH_h*Y;eN{$&@v+wJ{c4|%a|&iKB&aG!=-&!YnSA?cuG`xX!z*7BMQ?$ z$d9$8Opmba;*v!iGTj}=hj`A_(XnUIK~7FLL65b2yK?@w(F*P{G4`w$eY|m+vycuo z)!Db`qiib>lVB&gm-K-Rur0S16{Dh(5^t_*0qbFnxF+bihD&P-AI+ni?`PQe@E{K ze5tkuVbEdMjl`FY5-#>!Zv&B4`;1?6ca4~);$RjXFhcJ5i6=dm7(JIN-YjIiiOoH4 zAad}jcqS1SYoPrw)NhhpTy+;y)#|jN&O@8~PkuepqQJWI7T)^l(aUkk#?jX|L`nC$ zKKxmqsa;(Ts`Dm$NsmrNo!>C+Rq+o~zulWICpsSssoix*Z{Zh+`Aiwr`ObvT8LBn& x%{tQy^~P8JXRo9g^LGK${~tf1m*`zb*NQ^jpkH#EV});~t@Yjf_<1Y${0C$r*(HIp=ylJeLqcLBvBuKtMnd5$2ahK)5oBfN-hw#vkyV zQ@U#b2ne3{Mfjh}+J0Ucy)KKXaK?RFk?TI@;Ggfxh>rOj4eQb+fjla~r{auyb&9VX z0``6B=;*9#zCQ@SW%{12nEUbr3RYgM=Zza)mRIzVDuPakJ|FK6UPr@L9Qb-<#StAf z$ZoSbaz|&5Vebb)#M`%*F2RlA zp}BhTV-a4}^A~|nfLkf%#a9n*{`JSVC*k$ZKYkDDk}kd?7V-RlbwPg92KpW4@7moZ zJp-uI-FKp%Ke%`%KRQMlkDd9n!!+|p|~rAVAB7neCm`l9cNabT$viL?B9^Wu95o>%_o zBif0KJalYw=l2!No;v(Fp?}XwT4~VSyvn+vb5yq7T+sE4U)7SXNDwIePcuDwpQd+j zo#0fm+P|IRUcFYl-==O;0O{D+SOASXGLL}Bt!1P;_fXhd_Ftt`vyeMfN6hT#6p!gy zvzzCRW6Wlelt&)U(d_M9C;Dc6;V1mqrRC*^2L}XfmK0P}-igfS`E^iZd-qn@tf^Po>l+$w z9UM8?w-AqL=;=^z(DE3ak5KI zl-Kv?kB>%rHRB(B;|V`X=h*X2u{#>t>d#{Bs59^(U%0zBdP?S|ceIy##*iB*4`V^h zVD_+)1~BonqgkDwe_}N7xHhrL0p6)U zTkXCypNNQlohN!1&jDnJ*Q6tet z$H)j@%E`(mlI`s5T)uocTug_Tm-o6M69Yp5)gF?=3Gt}rQjp}UmeuJ+-=(HgGe1|v z`*Zzu#j^>Vg)hXuCVtRuUY*%k+Fzj(+uTN4Q9(mMh{dl@l4Nn-u$;o_(OpW5**o0k zzE%C%-(~8vBK`FbOT8JP$;rt-<7J9ftL%696uwbXQaU?3yZON&b2;wkmY0_|IK=C! ztEe1JHwD5t$H&JX9@<-2Y{7)>tqil!(0po{xxEmZoZOcn?rR+WX- zyQouKL>5UXC9NpD@3**Ukp6aiWw^u*?fUiW8X6ilHhcT~`|0VFyq+E&9`W&$FlDfA zTBbI{#Kna^;?lkT`K7L|E+PU?iqiV*^gDsbcVD8Map_S(*l?9^Cu#FglLWEV3GM5oNzvvOf;(M$fZl0nX@~Z`BFE+@F11=Wpe++LzcN)NvEfmYpSEszIJr1GC2g-e&I<@ zmauwZBC)(t$cevrHtznZ+9PLJ@;YI5bg<0|BU6Lpv}_73^XLcUhjyE${j^vZq8?Yr z7fO*cAJU4PIC1jH44bSjF1;8F&XEy#mnV3*O=q8bHuw^amzuqc z*xKamv=FwJd>kGf9qsS`!qhY;G4X+?D5<*C%*;&9$u7RfH+>t_EE63S!~6wwQg`=d zWO;e{sRmzet3}Kx2_o2~a0hw$zA}qBHx^YTrNQCh3f=bT=H_O&0E7WEGczkID>pw+ zPfyrq7~~R+ZFalMgQTRSLcWA6u34o{N5$*Yuv(}eJ<7_;>h0}KyFC2m%bSKNwJQ5y zHUe!Oov|{DhW`E%`2B^2g%!mFSmjAcGrbuKA~;P4_2?~H7>n#wjU5e@lQ6z_T^QjyoH7owPwfxc@S?4@uUb_XH!C838xWk_d&a`^ ztvfM0=*a%$q|?pdJ70 zGG)Y$sL$-^HStq(u|U5=tCx^-trcHH;D?$~N9gEqrGnD%@n(P8CWq4%LcX)N_ELMO7tcO-NM1aSu6++ zmgC{!v9x4lN=r?BqTltv$;rt;T1Lj=%bQDVY-}kKQTN=mbah#Vzhpp!N=rM~UXZy* zQdCsL#Kc5OdNvk)-)()|Fi1>NGRMg*&D&cP{vLbPQsa%LT*@%2erFYz+i2=9`F$>#MIt8%{S<>Q>nI1@abxrJyFxf1`KQyNx4pIir~ z{;mr%by)SLPSXsvwxM{WF(1LL>*Sv}lr%=yEqMCn7o8j*@M$DXtjomhjWF_~d0j?8 zXtYo@xczuzW2ha`L_?OYV70^~=WYMvaxdjaDU&YEiP_rA?6aK_p7HxG^OOcU-}pqF z_*CACe7Ry(%%`*%NX|`@?zko0GS+**eWuH(l^BvRT=Br)WJCF5Oj4q$wC!4R!J`a= zXWrs}kPWq;6%)AkY^^&LlEpu!kss`6wd=KpRDSL9y9gAs$Z|sF=c-eLd5YBR>=g{U zKPv5ayqdJ6vy$EXAV{bvD@Sv?oE~h=x%okO2n!3#&3%l66MD3FmX-SA+xtAl%<1WA z2>T%+A?aypg98JM!-|U&6SWhs@zgfE7A!?YTVPJ$x>FXF1QO%jl%W?#r^MsHgzJVXF@f4ZUjkq+Kv5Mi$s$IFGjou`nk`bhnnJ%XI50E>%YC|1S64SBE{?=;>Kz`fmzVN~2YUMYN-8SjH7*(*LM6Gm z9esV)^Br+}tD^~r8`351vT}06{x$G6fq^m*OmS=)H1X2c4HuUv4fK$&?)r`;nXwL@ z?KJ|t=*}f^!>F$?RFW0eKPreRcJZw!uE0JJBz$+4+MFFPrG^5Yf?vZyR* zh5CbnWoNXLR!`G5#lN-bbFvWeHU<*m1SfH&Wu!YE{nTmBF&u9vg0Ql4bkx_se!Iqd zT1&S_HmI77P|os9nH@3Q%3fo2W?orB!0!82N@NIAP_0@Iy{G^C_upq)LSwjH)aB&l zl$Gs|c8$}kzW(@;U0ofG`0U}q!9h||62PIJ#E6K9v^0Ga6B98pF)1nZ-VU?1v8rSz zJQQSPYHDgiF2?~1t*nfUjI69j_Bq+vtD_Y*H*emAX}I5&SY6FUM`zX^&8bpsG+1Pa zBga8RL_`*NV0y&*PD)COPNy|2n$v+E7g<|d8`kqfnPiHG4~ra<0D^vfk>V&Tn-TfY zUfg-!Ga#MUx)>nM^t2YCZ)i-+*>n>Io=@y}d2?%ar%CXU|~Owf*>ky*h$VLQVJ}d2x_Rdsc=pb?X6>|M%z5i;nhAMH?1;bhZqa(qe<8{T1ulDEoIA&Pv3c zbM-k(XkgKdWNr>goBYToU(2pR?;ipHn^;m+xP5THc5dhs0u6QdzIZV)J^esW5xah8FDN+p5jFMk@iCcX^zqSJjSPXzOhUZ! zs92}rCZAA{ELMI2+~X#f%=LG~<$WC;5PqmAD9|1>HZ{e~V(_MFLSgWPl(fF9E1+RY zNl8gtR~P@z9Vrc~8TscY9^_(SUuMh`5)zi&3AGyA+Bg9ahlYmc<(WA-ofH6I;c{A> zp6;o>U0zxm6dG!?AzX=4!dq&ENG%|DU9DRvxk>YdEfnnLJCOtTC?YymGR=?>0`sS5NlTG>|-cNU2e9jN6;i;7(nV zlm^!ez)Uq~W1TkN4Gau+7kj<&$_f`sVDpGdO0IP#c)Wf4wxD1u_r*6F^-79b@)3Uj z&`@0fbn%;}EF7pF39+%SXPU7nktt|ss_l0cKi*+DFKW>gzL}}W?HnIiV0$w`)x#Wl zvOLI8mIg6drY^$hz%w9#*ZR`LdgcOO*rQlH0iTZ_0RXhNwXKbmb!RB#M6#IT-MM3R zqGAEija=fhqMDl1_IzjZr8)QsDiIfTn5DI~$wAN#3Di>Gzkdg^L|;bW!&Tav36C#2 zM;(jX(&;T!YLTR2wAg^QjEb4JFB`ZC;^-!x>A6h_y#{R3)U#=mFl4+}lxdL|Ni>@`(= zRu>kQl8}ZE6Xs=UX=x=UqeC?=E`oyZeI*%i6s6t0c&@yP!N$U3!pCKdn3|g_+2%Ag zHAPqGNj=rBI$yZxhWWkspWz`sRD$pYHSpIs`d&i?w9fODiH8>v9&YyJv&Su>#f61C z`1n**R5lymk|5{FB#Wh`r?c5_Q|xFNA;Dm)s;ONyt}#AriNEF<(CGBJIq`io6D6~< zzG%*tA-+U0B{K@A_>=b2;i=?z$lQx^3Q}8+eMt^gd!wu9R>MXLhhC8@Ezdem2ZiMX zAM#0-xnC_C#L6UGv}|)}_+n_=k-7ZX@@z!Ed%s>14g>7!(h;$? z4VWUU35d6)hfYlr;Wzue=BuNBo~rN_7lp943*ALU{Mj=AjRq6-AF;5opd^8XQCnLJ zWsN198eojJHt{FYa`N)l=H}0!^0t=6MM3fTfI&VqFp!#-*12E_F@=whk1S9GgYmBD z+t^rQ1qB5H0s^+EYzQn6Cg$fGr#7xAtjp5sadUG+0S-UO%Hp(~f1X~F=$V1bWDpb? zi7)2?WeU{F@!7d)Xu>9x>Ah{3Nn|Qq@rj9kw}m)8?%?9;X=;Aad<@GSs0r#Y$@(PZ zYv0=1Rvf%8h65-o5kj@ma7e|MW>hSoj80Sm5jHI-MTY zEzQmLd)Y6ZkI=|FR>#e~-l$i=@;!5s43mfzP}3G~k~8CSZpz8ZdHAzOZhd}HY;+=I z*89w%yFk2DX^q`&i3Rnkw?tK<;bW%j%*cYHnO#e!n2KJUET}r+X`M_;WCad}D1zkb z2nh3pYqZ8CpS^`1HMI9F_v-#!3%$y)mD6zJ+SCj-PSAs~PHV!)^X>2bdH1b~lypxG zY^Pezwifr-W=UQg5?1q_R&P~z=5wtrioB}HLm4$4j-Nl}rqMW~45Jpe`|QFg2y>af zng3+)PWj2TqiEZ%>b=lnVmi9c@85N%8~qisRpV%Mf%kw30fcuo>V!tEoKd?O^Jwo1 zlJr`9EHF*z<8G0Amwg#`wFo7;k# zGEE`4+VP;Pp&=zRGc$hDMUZJt7OE!7wMUA%Jhsl3mW4k**ocaXPFRSFi~IcC(ClPo zWu?PK7FmgH)-^@?RM~;_V(E%vWkqxrcX5hDtoH0sh1E<};Vtjgl|eC|)9Sy_y) z{5&wF?_z=`v9(_zJQqA%cy}~?b3E~Q^O4LJyjtc%7n{AUJXECWZnHazGV;gw7Oz8& zmoVQMDdt?+C7aFJ+}4Gyn*5LgbEG`1a>_7WBb!2dA1CH)eMxC|tERWmLQd8oEl`eq z{ooi1{XIZTK>PuEa^$MX&!?xQU0WGGv*xCvq#UZS$w^5`0VJ58zX?@CKtRBi9PGsI zh3xEXn5M($GSd3`3;y?oR8>_09fpbN=**l=VzIoibg~#aGrUQW%5rguV(u(j_GY2L}uIsLxJK-A`kwEBv{=(0%zjD&ynF5N(>8o6FBRiUT2j8X6i7 z!kh)s;0m$BeEiB~#0=A_uxpgPIV~EebMj+jE;0Bx-Xq-6l)0EF-1)EYGURJ0TZa6; zniH;IUyt+~3Y7a!Mlkhr+aFIV^rKe{RP=8C@ftbCxSRVvF+?S}VVQH3(cCOVb z4jkkNjmGy_U)pE7`q``Qp2T7eZDf|6%8BK%eugOr(;+_466!g=DTY+%0>YbYA;YCG zA`YQ(n;4=FwVCAo;)!(OzMTVul#LEQ@27n`9RAtjK8}KA&KSYD=E}7zqWsQf7o0Dv zd7>0%zeotRk~zS$NS5IPO}Rai3as9GAz2*1QKwFn7rN*9A>3NXJHJPr68lF+G0s?( z^zpnkXUUMJW^7)2YCy`yy=zIMS9}C5EGz(+`9-4r@c>Eamx@uCIb5HFXC&19MZ{Ia zSkn(LnmiWVi!4C^8GafC*S;TY?T zKN0Nxyu3U?LBRl;8^5l}qo8L~=(+p9sjhT>2+AueLP0{>+TNz7q(nqPF&1(S2@eZ% zb9dia=oS(c&48+!hGzB4+sm*uUQ%*hJb0s#YI=5d^~-uMU~4SOR0)0iKV8Y{>h2!S zd*xAH&c>t<74(a*R{(1RSq9M%)f5K({L^U8t*)k~28B9QCgPFIS*fY11UHpumX+O}ht8($T2>B!G%8H6l;8$RKT9fNx1vWi!;};T| zo0&o9rKhKd;9RFua0gkiV(YhR!m3Z3m6>V8I%~xAG{a=HTuwuS==SZGdV0bs-H#qU zIypXuM2CTa!5;8?N*ehb^l=&+8x@NTdNY*@cGt!w+w5Rp9_}o)w6u__G5&g#Hzv=# zhD0mgmz9-4e9-KIKWJ?gizmI1!RGtQ6&%uXa@H#CwoFD!(Xp`P6cq=6hrWu0?Rc`& z@9*yqU!oG)#4Ll51*eG`mJeokq*8 zx|78dzMo#Kzz>??sRk%?R8O|=O=YN9eO6w+`mm?U1XoL2yHl@FyCoz%A_6K~=177& zcSLU3og5bZ^%T5K(t&6JM@KSOlB7jZhecIhum5~12`GQ>3WK+|H-x@No#wyR29ItW z9o9{fq8Sd^yq@7?Ms&~xi$?9DDpN!(aLNxLh;fyBq=%hM>qpH2gk_R7zh9r zxw-2=!W`|cEd4Z;x>#jA5|dp!YgP{~k!T$`fKs%x)Nk0Iy~1XuD=Heke}_s!q@4I-;i|=rcHaBnl}q`_>FdYFoDa7b zfIYHg!^1r9z9wqya(JJeBM;nC4qz$ z$!z@cF>cR;$TszStq)#a%|N8`x5BuaHhdyjTwLr)OiNpEyNSWU!Ex{2J*dIhvVQ*h zdjw*f6I+0zu!k%yw-ZGIfk9QsRFsmIrgu8D1niG)vA(k-r=Z~L?F~0ke*U}zzzPuh z4%_n+i;F`|+P~GpL0?ga-Q}i<*TADmsj9Zln8UJj`T6A%fMlRf(JeIT!`=Mm=jVaD zgF@ls$B&|+H0ZonjL#(z_;l0*Vjy*cyq(MdJD6YfxGzfufEWqMdGTe*HrmzI)!v>{ zSjb36CpAL)J5rW;i^8)2%`?{;4h8bJmw6*2BV-bvg>Q6rb={`(fZNDr$lt-kTOF@C zJ3iO~Vg{ZE?)(U#z;yoaF?+!W@H(|#LB znA&~~7uYo60RdgLlS<0Uf&v0ok1;?NXBE5O8;yV6&_28aH@^qzW}?vNnYH5~``W}$ z2Og4dBG%kO=!L3krCcJw1XyE>$iOUX<0CJBHJcpWUUPq=ihXaO-Xw3CxiOgnP9 zoE+}JxVZTh78V*07q7s28pwS?;0yUC)?u;CEVmDhK_v8#X(70_?ow?4q+y=WfV@)K zFc84P=w96*f$ALNoN@g(QAA2Uo;z;H6~ds4i%V@{Ma5CM)e>7YgjHdNkPqp7Xa%l( zwDi^Gat;0-?0v*a<#=R2`rhSmvK`MI)&_YWgVdx0Tn)0v`NunA%e*e_?6}va|pU;2XirYTeDTR z8@$`~P{zTcvsf7_IzDy;;w~!63AQk+#VpXvmO5#_Baix!C=sW9;p>U7A>BqUYkcF? z&L{TpH&M?o5(~oowUs|3lkWfJ$kg$n(6O~@tW_sGBO&A|^`E)I^( z&o6Jgx}?o;urKz{G!4y(pPSnibQz!!RtEC;)K12wFaRF|+4XZ`0v#P)N=BwTT>E!g zhv+ib9y6}@V{hCgCnE!jE6SIs)z#Gy+8`#fTQ5^kQqseO=e@id*=B8JRR!8SIp%NA z!}Q*P^!@wyLFV0>Yxl0c@i&O*?d#Lg(o!wA{DAgV^D$dN;>E)e`$jFseOY%xe;q#(EKV~Wy8T4f;9qp~9%yKyHzlKc- z6@HXFz%@TzF(++aVYNGwT~{x$#-$}o-k6Y`YQDlVwoXZTgM_l&K8aN+fX16Kpm$dq zsAb@PnXM+1!XhJq`9&lHE~~Y*wN8xyz$oSY>VV`dovxh`#g!Tce0yxX^>6%}pIPEVL00UTpD9jnA=G2s^w zz{1Afo$q{A>v~ng<-|%$OABb_>)u-W`ggFfdTYfH5XzCeg^K`fM5vQ7zRw&>2)eBl zNm%%qM`)Ddo>sy;cXBoKZ+Cc>KBH*{5p1cDkYeLuW;`Eeqk&urDr$r`LB9jn zl_UZw1*Jfjp9hgEw^Sss(epb8?H{{J>&Y?YY%YGg+{sdskc}|im9!?Gd}fjM$wP>H zI;qMMxIIU%Ga!iDoZ0_vT)RL=v~vH9jtJVZ5n>r~#bk@#hhg2d_uuHvV*A7~q8qII zU*nnx4rIg!$?1vvwpvfD-+V3%1vLtVfNi@ekQ{1rV>M>*s0b!JQYmij=?SXU%pF{A zY!v2ggi@6~1(Kokv%e8kRNFs3S?r`bH$7O;8(FL%@xB9g4qB2AlBBgNq4S`YiEBNS}<}N z;JRMj+|^Hi^!yL1wKxrqre4O{TU1bXR6=#7NBbKl{CDQGs76%ajzMK+j(|081&Fni zA+MwahNAr4yUS4$?aj?zpFVNftbQ*2bH8umhFX92@o7sIkHkg3#?+)!5K65Cik$iK z0G$X9%CCWojSQ$NwVeAAwSs#8FN`(ra;64W1E3rrxB#*P%DMm&^(4)2TUKGV1EG8(j8p9J`bTr!%&VzxEpkqQSV=WzLx zn08aP?&juaVo4YH2Eoo3$RTmK2{UNKE)AV~oFwE_Yh@`Nt-U4%r>0Byco-Xr5N_b80R{XdbCOdnT`qX$8q%cC2Zv@4B) z5eT&R1SXj9V8Imup-_mZxczNT{&rF8kebt< z2+VaHYK%9luKiOu9|I&OLk1<%QMSmRyj}c@r6%`dcpOD~StIg>qDuwLz1IK?8 z%~W+m@Gf3@nv@1KZ`EAQx0+oOocQ>W;CU3NyK7`*#B4lNr};QlggLTGtLZitRxrdQ zD~Z3xa#9+i6IdF+Isr(kr?;1tot;*r+HrXx4^Ux7W+o=7M@Pr=Ywyqr2u=XJDagz3 zu8uk!Y|g+OaXai9K~$Rii!0jPN`k2x$$i21^5siAJ64ZxKm+^$_ZNt>Akhi5#=$zj zi>eDnZGC;cD$O3SQBV*+hh1Q1w(xg7*O2rTED|sr(-RXLvu%-p&^vLAc3;RsZJif_ z`um;ph-kF|oI#|Lbqn|N%XK=k?rh+ECMik8VY~jB)dH1(jeln<>d%lED2jqW{OZpz zXgwF3KJ0h7Bwj3Y=A6J`ec)i0hTtQZ(7gb1Sv;-zzlwNSVF(EMLVR+jiZ!`i&GF5P zUAr@}@dwR4YG+F97zmU>PRa{kBIh6Eh- zIYxun_P|p=aR>sBI4uSqUkKYlgVO1boGcu%#Z>(B(388KNOj7-&s^A^w-o74zGFYs z#$i?@&N?zyh8_%#Fa&elP-zgDvehcUu~z(gf)|Qos53v5YQB6q*_9-EHSoURM`E|a zg`08+tbG>|j{o~bRHI&yZY(xX*H-OHn~xkGj=KC;RI68}fouCo=HrD*>oF>Kr|@TBK5BC z>gl;luhj@j@(mKO1jp;<8}w4Lv$JD+Ynzyy?(}QGE&@g}B{9)br}mQ8yMQDzk_UTx zDf#$8w|PHWQr{$*TRZs_iwPI!k#CfCNXyIH@FU^FYpZlQbLQoBoewXCdiL-Tkt`!U zo!O*rN)iD@ul2oDok5Eoy&3@&nlDwBFrZ17P?c|vl@RrECR8RnZB*< zeyQ2imB3GHgF{2BLq($7V%W6mu(f}hRv(aoETMnAg$iTq+FAsku|NQ*?8zet|`~YGH!s7!sC>bRFgo}-8ss^wnCnn6Gx&aR} zI!t^puroi;)$8X9{hzQ-}ySG?TH)a4=c(DpJeqVq+ted;TL- ziz-CKBH#=a>82XTmJhY+qdb3~a(n=XvOM6Agu)kyH}G6!XJlYFJtg#Q(4rqS+6Cbm z47Gt#QN}M{c6F?7aIvirgN6=3_tV^E@rA#G75#JWj`UakEJ%`zJB5*Ulj*igE4x2@ z2GGxMGs;5K+uwgqj{~r74yF=Vs}+4nLqq14sai%4)s{UAGE>OfaEU2gSbd*;XoSpc$P}Z?95D-V%{X*Q~eeYF$Ai}$w*1vBN)Jlo}Qk* zzi$gXO5OK=stut!44^Iwm5An0l94HZx z3lotpAWw%=leB92Zk2qX@j0;thPu;(=u*+&eDLO}t0E|DS$J~3iq9w8xe7M8ub4;Y}`u1?;)c-GjX?@54rpbTGU8Fvf}3!4Wc zjb7(HQqrXRLZ2+Pf5+Yq@uLl86_x8(uH0p}rsm>`ZkZV?HA?_Fnu9}CPOhlM{Ni$N zR`Ema9UL%tO~$Lm#Rd8KLBIt^az#ajjnbc&a{65k{ELV{)AC!iC^uD2Pe_naQE8cY zJ#Tp!FW^nc?d%9lJg;YVwn^v=f|}&-2ypiNkK}54lEmzfLWyDNCg0>=Q#C^j|MfU& z_eISf1~z5pD+SN4bTCAHBQ`7`Jk2PH#JAXK4kGlq~OimB;H?>OT-Q(>F7tBhi{^4hAP(Pox?nBjXJcXmVM}zZ>f zAR#b5avFEVDws(irEA>CMK}@nFNi|=`~81i=pS~smYNy?Nh0K4H^1#|%Ly%@5`oxd zB+-K44z;|A5b8?cKW+KZLjCT|%}usw)*DlaK_kq=y}VJFNt|bCC3KX(!YvP3W7k#^ zShM2CHbLIxZM*a2wI;w}M@L7Pq>B$kHYn0-Y&YUz8H9%7c-Q06y>tr=?FU0NVA%@l z-@9$T`o#7o2?#Wl6cnBxKlc1eile}uK~k)E^4Bk!=^Gob#HtXfL zF%LU7|G$v@{*Rg#Kj?sb+xaW70tPC1|1ZRP8MBA&oh3i}kJ;3=empz};P7co{BJp| z4-(TYP^kcmm)Z__A1o>XH0TJz??H_LFnYQ_&d#C=jsPSEEO(3KCN1LA;5AqnNd)x0sk|xu&lku za!XpmFY|<3aYfGdySD-bi$1D9{3b4T-Dg zwJx9BWgeURRx3$h697R5?D~*|04|A&h?D_sZ*2T%Ckx_RNr??8P*B{%H3I_!rzR%> z`|`JrG&dU@?{7e-h5g3lRi+epI#`oX1UCNoq4O9Q37h)7D+dS(aBPM%=mJ&&K@Pfz zEG;ed_0wL1!0H521Y}VN4#3P!y?zoUPl`Z%8#YE9pGQPwB)IXQ-YU>{4Ur^{Kehs^ z5Y!&*Po6;Ql6l$4_VzXqr@)$tv=iRerczGzxM&X2A#xAnbg+hgKcEvnSk8l|6@26V z{^&ceHh>lb2OcP!@A0z{5o4Tom7JYV!CB;n^TE@Tf|m9S9K&fM;7$O&YVl2()lvw& z{s56M=(^EtR+Kws@U)#yl;*enl_JfR*wtYFpah=uY=qspF% zgd}dE1Uw=zoEXRNr9p~+&V_WrV_DaGdMur2%+wa-#<4~gCKZwHA>_Ic5(cSJxuW`3~x6wbp9|hF#$dM zED`)shag*D353Vz^FZzG>MD0UfSguceF}&Nr~`R;jXGPXVqy8XzUKm`u-&E>xVsnM z+(bltCN7TUzzQT6i_riv?q2F3W7OfhuDmJbb0?7xKD#-i!gGOY5O;a0na@P>_GC(r{<1O@-sX^yi%M1WE*xs^F-g=Vz zVV{;mXPvRWz8&y1P;X=KLRtcT1r$1%B@o7G0%B4nBP9g~6LWfb+3~!uVuga06*%9)zCQWao#U|0xnd+&*cY)A z%ggNr1<(61p!|&>e5{sypPRzvc*7%BZWja?V662c%I0e#VamXRZ(4Bh9IOd|u@i!k{m7vwHv`sS&t0yVn8--cL_Pt5dI$mFpn@;~3t($= z6J`SXCR0;Vgh*|-X2k^r1i*)wDuU%h?y7}>l8mi2W;(G55W@rm+i!b zl9HBIavi!vKBIlrqOVtkWk?0Lp}dFH+SJlgP*`ZRbTiSt#ssc;^{P8K8<~}h=6y?T z!hh}MSei88Ib@`zp?A`bBRV)(&f2>8JYHh?<3Zqq2z2e*wVps6%vv33@dPLzL2u7U zO?}M50>Kx`?)St=(4_?-eLe6qbo96q2B@XUxIE|QScVxB7ZXdxM*}*(O=o6e0+i&f z^@(qSTL+G(;Iw>?g3oNV*aIuQ#B>~LJZb!{dKD6-O(|$yy8S@FOdlsUx;wU0&y`i& zV@a;WH1w={(E+6V<--;l=o%4i@ePRobmRc=G^b8!w$*HN} z|0WLOvtl;fzKcoXws_b@5r)~SAR~j(s=xh{0--T=bJHBgY9g6o(ZA99GY=OuAtg$M zIxF=Ua6%K~zx4Twwtq`rK@~)8=!`QN$ejRA(5DN$upluF3=Tr8CG@s| zb6I*nt_><3NNy7Zloj09Wo7%8E#BWF+yyBV+T4 zDIFcMD2ZFh$UQ(U8=%P4Mco)3!<&*lD1@ukU+Z-Q)09=nlLwKJZr`L7cM+erHEV*b}pO*fy{M zqa;`$tuQib(T9thqLip)sE|zhIv`<@-Yd)bwVSh@D?@42BIvcfP8?jrUFrH_I34;* zOf~rUzOApnt}TJK5OJaN%%BGSZ;(nM6am(`vIS)Y;Gb7II?TX0F6IMKCiR$y8IHg?l2b5zTC)u0J+iJgrN_SG{vqW+K~1Z#v04fcT5a;QoJ?Zw|WIYm?>}xZg-XsCYU|>%VqI?<9DZak9d>b&3Jl?Em%%#vwdxds#~M_2Xby)&+|%0n5vu-5I~hQoPiTz0Zzd{%_yuex1hysc z$vuCL7So=Snc3RX;&8P4$e$pq@3LI~2f+EC3%Ikh&^@c5&<|hV^qs$-2{Qn* z4Tf?uGBRKY!IB4~TWKl*c$oj#?CiO~2Z`zoJOmzBy|3-Op00EAN2^!{CA&5kzrt>q`6fK#|7N|I71f31%h%;L!Xn z&uOe!wJE~i<+3OG;l@QR_YbGvgnNnFeX71%GuyuqVIyI8UcJGU$cosN|C?%Z6FU@; z)#KeWDE^84Uj~wid-?c8v0CUL=~o}ERzME}25WC^6qDwcH=owD(OZa{fBd@no835! zgWGs`&jhH!=}I~pSygqi3+G@M4_WRmD=nzHGcHr>Q}A8S8(couXFFQ~S-@IEZ2w35 zcYLuj&%fo^k*Ek5bi}l@rUOVtfk2{8qi4);mt*|AHou)FlXC6K74ULS&(20aF`I;z z1LJ4JRIrqB@bC-_4MkJBp#V98vIY`G$$cVVbU{x9HjP%bbR8H8{s=Jr0Kgrst$izz zq6=VJNCeTC*hFzybEzd4N#)xMO?E76*AwIr$!ccohg83a9a|{C7 zbZC|G!DclxkoW2(hh}07m=Zq`umjpk69LcLO$;*VE#c?qhoZhPR}-Y9QiJ?nxkisq zF)!HEdkMHD{{V-j-BJTm2Pe2XyEQytGq^)O^Y(6mssQXLP_gw`4nk&x2?7K9N2fIq z*=M&7;QWf?=Zv6vp@ESVaJNVTMDk=HjgSd@)GHkXx`fA8S|xZNv& z_`!p^{(c)c)Q8CBIM3f-il6_A|1${xKngSBt*q6YMRV9~LalbX6?0bUd{R!Jo&UKrSAEdG~H5v=0E$1q%RTi}%tS|C@g~_<(6fLPU&!3L4V# zdHJZOmUaawHk{K8kpF6c7J%;?7^MHNLtz=ZD;?n)pLKQvzlKZrn0Ds#H9eFY`4CLf645zHN>#;w_5`i3OAaYrrnMsO^laiIS1U=g6Xcz2&USV(& z$+?pigkcaZtPFm@%fXTmvyzgLm*)bDC*bDm$hbf%^HDqFxQWSjJ6=iO|d756wk40K@nXdH8ss5~OElI?lF+gDmnc z!eXxqj{HeUVF9@o>bzS7Y(c^?pjVkc`AooW4Xy*;#*?GHTX&zV59-F0m6q1k*MFFS zNrw{=&~6hDh@bF+LGs*t*`|YpH-kqm++S*3-9JS0+}vl_a;<(4_sz(xS2S4L72695k=P}0v&g;*BskA>U@ zNChSXQam9cp*QxU+opG*LQkvs@kmwNIuRL~_#x~@!P@)WpDt|GNr zR{YfV&W_9B0yzvWobUk-8hw3Mn7ekaqfDuM*lxf~8T{fx4i67c*T3C@V~WHh9}>Yi zOCaFb!3cvr(AM4_(M1jvtv+v4(934bK@pB{?ga=3!2|?`=b`eqAMzC_s|f%WyGbIe zxv;!^7Z=xdV^ScUC5qxEY_-a&Dge?@_yO^7^_B{>C^&64kwXDU)>R8<_MmlbNQA*I zs{wm52pOJ*2KHBO^8%H_uUbjakcbJ}Kpq}nSlAN|r-+g$fyBSHMfKp`}bVl$y0v3V+0&gNn za%RxR4^$92Ir;F=5Ms*4-rl*&CM?_tK6h^~uWQJ-vGMU;eOLr=hQs(coc1-kLZyqO z9mZ+_Jul~-0KeGs+uK>U3o&5L0H8lP-fD;T&$P@;F%c1^5|bz>N#V^)Dg{{15jOlItuzBlYF~PKz=$FT zoJ3R80tl-psHi43@_?IQRl%+{-aTjnTPPS^rTbL^DEW>cWcT#-nLoM97FDg+84vm~ zoKo~=F`E3DD+CC2P&l_{+ki$9l9K8)tvQ)pSvmQ3i~IQWXbtEoC?EOR0Q|ri0MHo- zqYL+$g%ELmei?-BNcuaBdT5btr{F00PzW}cf8qV>^$E8m2Rj7#yXYAiN5EzVOjSaB zJRHXbhA?Gy^)W`eKTXaduZJUS4f6-(EHSd8U(`8wSbsmC`q?RI-|(0;{BXvjU=bpg ziiubJP5hFQKu^=Lvf4qT5zr>!+-Zws0a6sR@Xqd1zb{%o9LWIq8;DM*GN32o=B#%M z)V{2De*kE?K?2hURwBp4ZT*B8AIP+7W#;bRCK8jAqX9Jloi;EKW|{RV=mmgSSnM#1 z=&g3J4b;`up&+O)^7;fwTDjWM4)`u`75O$eY)sa}>RGU~Pf%$A7B~SB9;!^c1$$!Z zXY@A%{{Dv4V; zF@~ebc7cCDz`cDjAD7s~ME-Jlp=M1T9l@0D$m)@#&V8TT<6@?!Yjm%^=MEY{GO7I; z@AKXhjYbR7x0+hiPg^uI25mJUn81{k)(%e#y)i&(01l9lkbqe>l#vHU4;Xe}!W66Q zt)O$T`wFal&>ew}3(~wZab#^3K-<%%!}1NL<_t@V3JQ{&D#-Kn}`hsBo5s z^YOkJI6M4bLcOO^Y@`Iv4q!cjafJ*A-y*`rrD)5u)CIq36ihI1A7J@KL_&gYs65SD zR}d}P=;@{RM}&bMd`L}w?gg6b)hfuCCsXG9=9!nyzQ%DAfq2U-%`4j(X*_-L1A~dt zBA?l^hcx27&&f|E5`tXMurN( z$~6J9C>H5c$V9%rlCYA7iVR_=$Y&`h#mC>q#-4>EW^~#jz)>l6xm{QlxPIsj0;rm* zVheRFoE-{M?QQ4AtgA3*VEJXlYXc6%_7sfB&|wB;n?$1EKe@%G=jH-eAAp0seGXX4 z=tBpQUyY2zKxlfP;P+9`6am9tQUdj`OSguYS+5pS#yslzz-6Sf@BA9L6I)hOB$fT* z{8XMGvDE8(;FjXXJjTSvh6;5eg9`jx2F?jVJ^NMSTX7~T&{}eH7CH>jSOu&oZX^Hk z01lQ?p_Y76l#AHuq^FBQ#P#{x1Ryv2ky10D)V9FhRldN?OK=$zU;!{SaOhjH$*e~+ zaYE_@%g}aaPI&kryc`rG5Mo5EF!T)IOdU5g^x+YGx&o-_9M^(sA|P)Y;O0nZ$qWi#>B{YQofx&leg`6wl& zvmae7uZ0%;(BYFT9@t&d(g9E!f++?lI3?;3XafWn11ATEn?@J(F}-At%t}n`0apV| zGSktf$>Qq$op+C5oFf0f;?6v*=d|tr*_Wv7BugQo(rBSjv`{K4Z7L#^QkJGrDkf5- zL==T2OGQEyr3^|^sZ=OScF8Crh3I*GGG@EyzVF{W$8#J%f6YNteLtV;y3X@_Z&%F{ z93Kg+gWkP|Pqqqom>$sn!Gi}73n5wQt6uZQqaJoxz3)`(lkA}loVg`9SY0ZVzlR25 zf+O-|lMa^{ZDMU)@TAu^VG~ys0>VXwIt~ zc*%1!Zn++te|u-_hE_pL&eY;il~y^-=&6C_5xqKJ+_t4_yY{Ds#1uvDw+{6-76dub zh4gPAQ6NQq>{QYcfx}BOk0mnCQ*g~v^FC*d+qZZ+oiQ;n`}W;u9HvB$_dZ{Zc{(#Q^I~&-K8?4D z>0UY)QtIfHqB}M&U(n~^=i(dbUhj@Tay*=Tf?~wHZX9?sWZO3Tvo5>lkDEMspZ9FL zXtj4bi^ICmGw|Ln{|M_-q}3w-$s!;6l9T-N`?j7?7 zXvj^T{N-^@u%@OaHJ7sw;EDergE4x(+ec7| zPjVx}#4J7g?H$=hWRa%gv)=s%MXCPq^r-#7%+b%EHQY(DSB&rFwR;a{P~^ljqtyzT z2z7LZq3D5%@^3W0T~_AvHm9F3;-dJ3R-n7$2nA2M?P|G{N$|{)5~em7qInnANUy1T zMm2BB>1RJ-!URUYUn0mYD4RF8@oufd!i5RqOklmf3{b<&#QBM3kOaA7B?Bjd zNC|HVGCp9_Cd}*jCcX-zS3&}efXi%kTWh^&m5dZRic9mArfuyXo17dI9?te3T%+n6 z5|K&X7ipNn)YrQ=38li+H#Ngjcypb$zQ0R+67Py=FO-T)U0m>P`U$Asl{fOXsq#aI z9w_PIiJG*>*v;1Rw;-r{_3WuxCY>aDedrcTt5eRUM>y_qA`Kd}4-YNaoNBtya@C)H z#{8y*=8{oPz&`2A+(y0#=HH|oV41^I5oZN$Ktoj zM!5o-7+O-Pv0I_2r1AcXP99e0M`gLK@j9c_K`%X|YDMa-^@;oa=6r~L`mQ9o?m_RR z&TXc&okme-u4CUB6vd|qfZWgko1nfK2Qzn?*;_@szxBAD^E|rWfzJ{uZ`8(&8nxM5 zqW2=60cUzIMhywUuOKpIWDh4y0QE;9z~$wQZXOoCkBHZ%bG@^Rs;Y*RgHZBk?x+2~ z#$~Tplx+MK%pfvl>wlNB{fa5>DXaZ&TK_y-JMT9SFWqTzLcXB8#W+f3l$zQqI>|wK zPc!PWCi*q6nb>y{?F$Hye&mwX(iy+3k;u<#{q=qW3p)=`n*TFJe#G3qVHavwOhQQd zAJD->z5hdWaD>eRqc(p90x2I|5PJNp&&tkLdHdHw*Ik=88(3Rg^Y07?v{{Hi#H1#e z*x1bQ&0sOEUHkcnVV4%KRVQWj<^)&<8 z2(wMAj{8fTW~2spU-zzXJV*#fn=rzfnP~u6%XLAhLxV+qR13n#3oo;EpT%iBq6Z^+Xm1-mX0S^Lx9F1 zikMzady=P&on3Lx>8H>hYU9Rj2?`pzqqxSgEC28f>|6C<^l&*975t3o7iO_ICrntA zv!xH{7d{Vov52F*O%4lW0U)~kdTcbuqWw6%AJ8blgpIz$P#(}S8VsQBenuq1AUwW* z`P6pDgipOk_6CrjjIq zh^+tGaKe>@^Bs2S+xbo(O>VT6t?d|x=LlwKIcyQ&(~~8r<7}`dfwnx)c+MOGTuY&c z1}V*7|I<&G*gEB9Wn|5mnV6g;hwj3KNspv<8$IWXcy&Xt_?OPof&rB{b=m>FLW5y6K zr}^<|IJuF(+&iCQoYb8@qgVaDumRw3Pd*>=dHvzEOksH@*6d|6|{LI;Q84Jz)(q76z)OQRY1>YwG) z3{)B%3p@TB2xxLN8|yO=#>12%%^z2;+#DPn9$o1AJ;r7BMH8?F#(9U73jYs9({yt1(qN^e+ThTbXEu{Jw-LFfa_Vg4XxpV4xm#a11jjtsy&nT|$Jk6M z@eX(Ex*S?MX7m2d(7$}_4gUTJwP1b`W@7_Dv=qO5K)0(f|I+)2C0T zBSXVVds)^@bZ1SfKubo?T|rC9=7F_#Xze{=bl_mG2i~fGc{B!)X4&1;B;!G8pmzBI z=CfztJ-2cp7zLdI4oW~!YR7qG>8PcdyudGl9H_f zyM|g?7b%_D+1ZYz{56!u+$^6tTUAG0AN_R7u}(&|jd~MrxF_w;CPp%9O{L)g@9Scn zHq4O?S-WG&=LVwgAHLkE+$_00>SSL98{nN)hqP!Dv#L*@KMUTR z!-w7OoRo%tzj0$3@co94-Q8ELSo7hRJ=SU)C3avPXMfJp`r<*V8~%=uboYKuk2>*x zK2pIW$$qDM3S}HhtMZ#T|7Q_!bg{tFBYg(>B?q);nt>qsQkzkSSl-;+ylvaIx~^Sl zI-$xzihzE}SiN=ocH{f^&Bd?Z(N%`Ro6z*RNmd^;KDOi*)DCa($&)87buC{BEhz(o zg#I-SKWkFniY7`E@d6QIr+EA3O?Og6z;g+vP7Z3^uUFOYW)aTtPY>Te03~6cRYp<= zHwOmhva_Ro{oJwjp+PFk2|IMXSd)Q_)hi_iuszo1Of_&nTFB1$Vi50)xCFR}UJ$Qr z!F(k=;wb*d1JAH5FyVdX$P*w#mu{`uV%V!fuO+i>D0M}9lwTCb>hP~%#V~xWp5Wl` z$RLpzf-oaNU3`e99grlmT4{E-j>CXj_?DNQ^xxiUg*+D|ll(1{vjLyRaG0nfC1>qWz@Zgw;*+S zl^^{#un1a6K98f5Q}F4b+j7aDy5U2NzbwoU@2fC0m`fkj>7ba#(mbUMORU~XQyD11 zTFq~C2wrL^E2ImK7Pco4bHrGC;;XFY&AajBEn@-p8&i#a4{Gl=k4yaKhNct%_lk*% z#(mJ%)!n#d%Z!QcB*V3QxVt=j#|}wP^*Guu@EQz;+AGrHU*#7sgBnRsdH1{9rm}0_ z;1M(0YR0rMHgQ5b*WNo1%*9wwjP+Jz65YFh0sF;oX>e!*4kXxG-T;cS&CMEMgTPkI z3cMvL$Zl|AlaX$TV;846EL)~16~Tw?;(bfy@n1kf-=M5NzO`PFE+3=P-Xx_i7dIUT zCMvx#J2!Xy-J?M@X#`Y96V$RheD7YPoaMSc@u?GPcFcTpzGSS&j2T-VDyx3|X@}Xs z^fAHl>FG9a`Wr-}fW_J()?aBpP$y-D>Gk&Q+smU0^75R^;!QV75R*L9Mo$#*6ggb` zcGbPKqh9}b$K8S;-sMP)04}H%@Z)pSJ)CtS-M0t63KOS*;7joPSU)0?=uN>6p6>pP z-e(KZ*tRJ&Q-YfmEQbM%+>9vM-IKh=MdnXtdT-AIpEP(|-Xd=Kpvs!8_NsorNo<&Z z|BPqt`Tsc;h4&5*$H`1njXvhe<;!GJ-2uLlQKfkiZrq?-lDW;KmE~G-8bF_*B%t@b z{0t2Zn-w*+nm)e1FAAd@X%N`$+xq|D0DU2Fu1rH`Qr;`+p*v)Gm#Ow z+S%e0B8lte4~<#hb?Q)|cNqC;h>~!zY&`3GmAnc)U~( zpRGY-#CJ=3#gW7CSSSkZEiF?aY#cxD;&Fd$YT66;sB`~j`hjkP$a*sBJmSruj&~+h`WvxQ&ngy%;#fk}nf)>R9y4sp-^pP)sN@Vwq{ClH42_siNHlGfH2c*VI}2k3NmR=Kr)ubbm+ z>VDz=dr_K$NbLnvtg1=65?V&iY?#%-UqnrBxGR`@{@Ql$KllFru1zJ6G!H$!zSMNw zC{E-VGZ^3O&%0BP@YIrB%3*epOH|$%rX#8-a7>{YCI(0u5XI5{f${z7!+ygznie^2 z+PKl$#wIa7zI}%d26^R&)(nu86x<4II;sM?ypobr^ebS8AvV6Fpo@STm?Gx!ld8Ad zMq8E~XXAC;aLOU4E4ol3o@hYN#3^IanG41pctwzWe)u3JF1}o?A~3Ym$CDl}Z}KIC z=g`Z2Q^1j0B^EfWzf!6Na~YjXVSc_X-!D*c7vu}zwI2)D(Y8azk#2)LUfoit=_Bf& z8jKXh(!!!k`i0cJQ<{9u%j~xtFbyhtVVE~}8*=Jt%Xb`-wCJm7C+8G$xGe19-$09H z`f`m_q9rPorx)jQz081a9it0FvCeKpJ)xzQL90#Y;amUIAVufm76N_e>8&oV$*OMp zeI;u>_{F}br_LSFMf3e(_8w+s6TO5CdQ1y-DE)ZUv6|UeubPcr8T;V1!FjKDj-{8@ zfc+suq!IE$wf*R;0`_?{*e~yAj8~m+hFSp#F~LdxS(lJ$uNwEiDS(t(w`4*U5)`nX zA3?x|_`cshf9TY=Ck`gSQKGgaVgmZb#Oed8Ft@UgCl^D9&|3DrAhWCJ1p~Xr@ZpD? zTi3V{Vkal3zlM~0f!)`;Si5JE7urcjzKYXu*ratFgk_5*q!$4i$@hIlE&StiGgS^Vs4^CPZM_UFQ zzwosF)fv5(pr#%DUEc5fI|R{c?LR~ill}!k4E9#jc`l{yu6wRwteRTHCnYj3FEr9) zXu6cib?VfK;03|Sapz7%m9aNdJM#$g1|~(5ID@lBJ1x%GT|?E67KQ0B8EF-W9Imt@ z8a-$GF4Myt-KFD(9~`nfqXA(m7G7Lk=+e@+Lp$O4QNj;_PK#Xuc8@<`h=xayr0MMW z-{KfUKIHI`buWCP`$Z#1Ds+>{@>7a(8{}-}=1>-?J2JzD(PWYylje5!R&{-5dHc5P zCjoB2!O+4$OTUQaXp=`D=Xe9>lOUo4lpXmfVA_KBpD8v(vdgoD%C-?vQ5s0@Kpm*~ zG%su^J|TAu&1y1^22G(!kP>@=%xs@|t3u?A&(A@p$7;-c|6^}p_t>+ZO1Ass>N1MN zs|9n{-E(z{Atw&>T9lxCypxW7z~>_usuy{`Oj=eM)2VU+W10>i2w~dQv7Iw{T%tnEok)a9IRiY~FDFv^sQ( zG(vMPK0s{aacSmv4Dvq9HknBmeuEyJN%jXz{(vBE-{iHH#F4q} zNFre5PXSDi)Pdyai~xe@c*|louaAln(kHRCA-!nw@uTE2>$ryLAM97zjCgJTXz` zCOwQJb7{AZ8=@AaR2W9cGdlnf?haD-@Ug#^?YPDLXMHWK1rSGEN-;YeAfT755l0KQ zHvGrV3oa4T2NT_;mol^UsYh;iax!u6BTL%s++F$^ed zuALn`@hSjoI{tBc3Z`|Tlp_R$=J@ofVX5{t8lmU(PMl1XJLXK)!WHB^pg=Z`+j&A> zXQJgdK=S0emakNL?Q1aYn#k$+#b875m8C}gE={DpB<5yoP|%&zi$%9?8r?i5&x_uupIdk8VuYv{p)GF&HzWEABBC>o$&4{Zq9 zswLa`KJ%=sklS0>saDjCeyVe9UB9Hkrb)H(CBKOsy9Qn-+GXb6l?iQ)ORo5xvrV3& zpt|Am>N(?M_Ud21P=3n@vlphtn&;2C*=0|7IJ>_EZp&oFD^nevC%~#7>g#V77sL7D zUlUYi=xFr{fWw4rUTXSzzb_i9PTYruQ0qwgB`!Loe0noVwCmVSNlD2%V^vaAR3!)! z2QoSH`?w--Z@ciky+oMN`Ex>n5+3E^Av}M{*U+K;!jBxR=Gs4|2zT*gaLP+B-q|Jv zMvM=aI89pckMGu1jXKjbx#k^vyThO@@mUFFFKAq*Y55&2+d%XZ2RdpR`eIngxY*dY zpj(^=%45bHGq(5vdkuRNDFGnTYKS_2i!}i%drftSpkACF@LM|V)O};HS`khA6OW-FCufAB;>?{=8d+l(IVjss zp1-JVpJp)Z|Bg+5+YO#hzCnQ4C^Up(4WKUODj~AIIdtu!<4eEXKdvl#wN9#9H!e!U zGx5&aJ!~n2IhKZoOVRX@Sw#VLN~s7rF_rDcPhaNhTCivw+&+6#S0i_1jL$eqJ2x57 z-!b44SMNDzGK!wh7gr;-5kKr4JD3Jfm}n7P^bHo|BSbu5GhEes*wCT#;g&#`(H=qK zGYJ!e*Y9?Z@@q$9{P^p1f*GfpBpx_`3bqoINl4egmdlujiLmnIf_A|d zQ_`uc!BDGjtk~*qnN82%uCY@bk6`kp6?FjBhr0XI&Ype7Y)4~WZthM{bLKC}P(s*@u>Fn#*6Ymd5^%M@a)F-=l zYEwcJlas}}bm3AKeA>IYZp{$^wewO|JGkxBFPbUOF^nAp_z5}J z>SfEgXDTZ#ZG2?@;jD(n#xyhq$Xq}nFt|b6a~27~R1Y7%;jWrfuAQZaD=4_p0Shob zC;2uh^?FRNUl4^-053Z;F5XVEWfKiT8;;~})z9nOaF$Am9W@@K(L7JI3m_}F`=|S> zt40hTj>^A%`}VNy!G{OOD`h;1YdB*f&fx}3yKURHmWwT)!Dr3Zt!sgkKD)3FaHb$Z zV~z$V%W8TYj21?Ss>IACqxhh{#|T#dC(6m)GtS{})(=4#!2iM8Jm>zbt2C~t#hgvT zfWTED4F$2)oQ}1%Lm9|wHk7VqUJl*7+1dUNYRA-mRV)YGkqPUKDbhornky@9!~%ii z`zhGG=cFOzkl()=G0BdHd@0~2-v+Tg@m_+MAT6ztN5s+W%S4tq@}!FXpTfF4^Y{LW zRT_GK23|4!6T>P6(avp-xj2YcU zin~`DPWQUuEV zXY&A9_UB;RJysDM?&*pNv{6Gh3Q#ZCP5=po_{HL}_uz6PvB2=CC~pXQB3Fo0l39i> zj!h*bxFEJddC6JI6qj*)ux^R=WX#FT9L=l}%(rC=)#bLPEV3iRu1>xJL`QR zH%Rm(3_?oFuNf3*V2HLl#}f~HwA^6<{>R-zb=7rYwd7`J`%oU}9_YBrM0NwtvBq9s zGyX#ykaNgo#}uUG0;M$Vl&@7pEHWioPp21g+cX8O=mjt3vwi$;)kX9I=R+T{@a7={ zgf|MKM+2rs;b=k7xMWE+8E`H2&kSV-aleusI~lwcgoIx}_$*}=$9;=tt-6^NmRh*e z8S9e&+A-$sG|N+tjQW;Q+D=dfJ@4yo97V zT%3S?GWOs>2hIRGC*xkwOT;W812^+WQu>Far1*?U<}oY2YaPMyhxj!~1H#zbucO67 z_Rha zz{y3uW=YhJX>nbi)19P+nZ3E_f4Qaoo6A~vjNkQ+gz)`PzM(q0VyeRJ4b`9ZPDjtN zvTCH4#iFy0$qwx)0GY(s4?HYo$AY2b;Er?96Ov|RZ20okE6B<$Xf7C`!VgB!YxQs6 zs^giJ%e*0o@BRowmUoZ#&biFq(KC>cl&pF3g!>*+C{wIM!r-~Mlglo+J>&$EH;|1o zYuTGz03-;?{qt~LwmP^EJK5_OIbKiy9!Tu%GK8`L16tZED`9%3_*G*2A@7Wf z3LYos^|h29uTj5R`qK@U2*3>#0f_w?zQnBk0yu-hXMm>9n!9NoMWzhCYglE``)re2 ztlS=x4V~D@>*TJEk(Z}H`A?nEzPnvOK*+nJ(`*4E%WGqgHde`1wgQpjnZ?J;y%EqR zC;0kk=wCWZ17`0+*d?Q&5F+8Q zV8JQGSc$t>O@qxRUXbg8r%{E53MgpHmah2DU%$?zg9Cv;x=Uo9z$odDV2OjdNoY$> zMt1PkC-KY0ik|`|uC>?CbG+Wh?e(VZ9isO?j@o|?p-$n8cc9e8UaF%Wk6vWesYOxu z-kc$cF0$bj16KdsM&zl2qVLXChwoQ)1-)cla8WO2Qx4c2ISyP!fnKWQ$`zfa5%+}{ z%9ILVNsKysRF_(&jIBGe?7i8=HkadHDC=lzV>U;{#Zo-r)N^ysyN>8e;G(Z{Pj_V<+p7PjmUQ1EJV%mBQ0q@F!66VOk5bE#MtS zEQNljXD>Z%flM6(-PZHx)x+}`le&qEH#Rnc1YaJT@4N?Y7NQ20!}7TG+t^?Qt_Z^x zY)=zkVR`AqD4kCC)63sfCbEV_qhvdZNm*N1jHvE?+r90hMpM)2K`Tl#&F79iVRQES zjT@mng6QaVY+HIm4uOLz*E}k+!ZsD|6sK;0n$UYO)iD7UI!>Q9t*3VUYS*recE3ha z7m6jyM+h=H_XAJX^^>VRE{<;beT%($S|}>XgQt3nJl(LyX3fmpFOOeNNUVt6W>z)) z-b|NfmC8UUkHs7zStRPSmo|Np7&y={GdsXLZR9iRjd7<~NK$0ogK6#%xpmn0KHks! zJLqU)@*Q*(U95ksVd!H=vI{_=1Wkt0%;Wv5(mAaLNlE=uTbm`w-!6ZFzzOjlvROt` z(+}*%KaU`c8jEn5WsdpN&4=;6kPW z*+1VAttZ_;KzN6a9Zy>ypY|kua~jxhfK+u;RY1^0QIP|8d(N`6v7Gb%zWr>?A!%1` zRqSD2OxEyCl9LzHs!;~WGAS)C=9b9J6_5NMmOp^Lv`5@1$Sz;LO!($@ z;|!(kR*f!)$z@hJ!@LK+$OrR5vQpM8h*r4$3+sa%Hh!))ZD`)@+T-wGG{}DvG`F#L}GW4Rg8S!rLZQ*QZL83d9ECwTT(NcSX*;U*8>Ki1__Uj&fsXDX03?JRirx7KlE8Ez1TFD(x zWKXTH9X&oLt^4wren;Y`tWFcHS^6aTu%k(n;;U6R6Al@TNTOU;7ke{t?W;oaP^*$%3+DV^d6cvlz6zsDmX=;82=A1OR zsI>G)kQtO8<|d<;__|q|ne}Xt8>QdwUqMTSVL|@Qr&3cv3U<#w4Uhn8v2)4h1Cy~J z!iJix7Jg*0mqett)4#zhYsNsdlnGa0iins9Uq_;=5I0|gRhvKAlWJQd9Qnojj|x&& zZv9J<^=jJRlb6T!iL96M74<7jE>*4DTmS378@c?0ORxSH*qh<;6Z zTV$3Ir}taZ+3^4C4-bTVrp8@cKQn#ai=Dsy$jnWCI$?9lC6re!vrF6^GqdJZV)L*j z`SMi7U&G?u6XwlLcDdFq;Iy{ni@COuhtoXU1Z;7*wL!VOcHy!6xv8F@h{B(+g^5mT zXBH-?<&rbf&~8lU*#R$lk?q0H14trV?JE_JmZ^MvPPpf^+Zg!X*@{{g$7H_iY6 diff --git a/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/async_progress_reporter.py b/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/async_progress_reporter.py index 601a9d4b0..e0f112f5e 100644 --- a/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/async_progress_reporter.py +++ b/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/async_progress_reporter.py @@ -45,7 +45,7 @@ def __init__( self._unsubscribe_token_usage: Callable[[], None] | None = None if self._bar is not None: for col, tracker in trackers.items(): - self._bar.add_bar(col, f"column '{col}'", tracker.total_records) + self._bar.add_bar(col, col, tracker.total_records) self._unsubscribe_token_usage = subscribe_token_usage(self._record_token_usage) def log_start(self, num_row_groups: int) -> None: diff --git a/packages/data-designer-engine/tests/engine/dataset_builders/utils/test_sticky_progress_bar.py b/packages/data-designer-engine/tests/engine/dataset_builders/utils/test_sticky_progress_bar.py index 8528cdf5d..ab7467845 100644 --- a/packages/data-designer-engine/tests/engine/dataset_builders/utils/test_sticky_progress_bar.py +++ b/packages/data-designer-engine/tests/engine/dataset_builders/utils/test_sticky_progress_bar.py @@ -270,6 +270,10 @@ def test_reporter_updates_and_logs_keep_drawn_lines_in_sync(tty_stream: FakeTTY) with bar: reporter = AsyncProgressReporter(trackers, report_interval=0.1, progress_bar=bar) reporter.log_start(num_row_groups=1) + panel = "\n".join(_last_panel_lines(tty_stream.getvalue())) + assert "col_a" in panel + assert "column 'a'" not in panel + emit_token_usage_event( TokenUsageEvent( model_alias="test", From b54d02ac92863612d6dcc8bbbe3dbd9dff8a7625 Mon Sep 17 00:00:00 2001 From: "Eric W. Tramel" Date: Thu, 21 May 2026 13:49:42 -0400 Subject: [PATCH 07/19] fix: include units in progress rate headers Label the progress legend's now and average record-rate columns as rec/s and refresh the screenshot. Signed-off-by: Eric W. Tramel --- .../assets/progress-throughput-panel-demo.png | Bin 33223 -> 33518 bytes .../utils/sticky_progress_bar.py | 9 ++++++--- .../utils/test_sticky_progress_bar.py | 2 ++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/assets/progress-throughput-panel-demo.png b/docs/assets/progress-throughput-panel-demo.png index bc49efcec2af1875c5ed942e70599967b032fb6d..acd1072aae6ded114f6abeff444a994d686e559f 100644 GIT binary patch literal 33518 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%0D{7f4QQeOlPOQ>AVa--*E2A%y}oW`Yg+|+dL(|M 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 z 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 None: panel = "\n".join(panel_lines) assert "Throughput" in panel assert "rec/s" in panel + assert "now rec/s" in panel + assert "avg rec/s" in panel assert "column 'a'" in panel assert "10/100" in panel assert "column 'b'" in panel From a065f06c430dc5f4e17843706ad0f406a9654675 Mon Sep 17 00:00:00 2001 From: "Eric W. Tramel" Date: Thu, 21 May 2026 14:00:49 -0400 Subject: [PATCH 08/19] fix: place progress completion beside row bars Signed-off-by: Eric W. Tramel --- .../assets/progress-throughput-panel-demo.png | Bin 33518 -> 29282 bytes .../utils/sticky_progress_bar.py | 76 ++++++++++++++---- .../utils/test_sticky_progress_bar.py | 4 + 3 files changed, 65 insertions(+), 15 deletions(-) diff --git a/docs/assets/progress-throughput-panel-demo.png b/docs/assets/progress-throughput-panel-demo.png index acd1072aae6ded114f6abeff444a994d686e559f..b64fad90083cd859a1378b495b62988c598cae16 100644 GIT binary patch literal 29282 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%0D{7f4QQeOlPOQ>AVa--*E2A%y}oW`Yg+|+dL(|M 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 list[str]: now = time.perf_counter() bars = list(self._bars.values()) chart_lines = self._format_chart_lines(bars, inner_width, chart_height) - legend_lines = self._format_legend_lines(bars, now, legend_capacity) + legend_lines = self._format_legend_lines(bars, now, legend_capacity, inner_width) lines = [ self._border("ā•­", "─", "ā•®", panel_width), @@ -431,16 +434,25 @@ def _format_chart_lines(self, bars: list[_BarState], inner_width: int, chart_hei lines.append("") return lines[: chart_height + 1] - def _format_legend_lines(self, bars: list[_BarState], now: float, capacity: int) -> list[str]: + def _format_legend_lines( + self, + bars: list[_BarState], + now: float, + capacity: int, + inner_width: int, + ) -> list[str]: lines: list[str] = [] if capacity <= 0: return lines include_status = any(bar.failed or bar.skipped for bar in bars) - label_width, done_width, rate_width, input_width, output_width, status_width = self._legend_column_widths( - bars, - now, - include_status=include_status, + label_width, done_width, rate_width, input_width, output_width, status_width, progress_width = ( + self._legend_column_widths( + bars, + now, + include_status=include_status, + inner_width=inner_width, + ) ) lines.append( self._format_legend_table_line( @@ -458,6 +470,8 @@ def _format_legend_lines(self, bars: list[_BarState], now: float, capacity: int) input_width=input_width, output_width=output_width, status_width=status_width, + progress_bar="", + progress_width=progress_width, ) ) @@ -484,6 +498,8 @@ def _format_legend_lines(self, bars: list[_BarState], now: float, capacity: int) input_width=input_width, output_width=output_width, status_width=status_width, + progress_bar=self._format_progress_bar(bar, progress_width, color), + progress_width=progress_width, ) ) @@ -500,9 +516,8 @@ def _legend_column_widths( now: float, *, include_status: bool, - ) -> tuple[int, int, int, int, int, int]: - terminal_size = shutil.get_terminal_size() - inner_width = max(2, terminal_size.columns - 3) + inner_width: int, + ) -> tuple[int, int, int, int, int, int, int]: done_width = max(len("done"), *(len(self._format_done(bar)) for bar in bars)) rate_width = max( len(_NOW_RATE_HEADER), @@ -524,8 +539,8 @@ def _legend_column_widths( if include_status: status_width = max(len("status"), *(len(self._format_status(bar)) for bar in bars)) - separator_count = 5 + int(include_status) - fixed_width = ( + separator_count = 6 + int(include_status) + fixed_width_without_label_or_progress = ( 2 + (separator_count * _LEGEND_COLUMN_GAP) + done_width @@ -534,10 +549,36 @@ def _legend_column_widths( + output_width + status_width ) - available_label_width = max(_MIN_LEGEND_LABEL_WIDTH, inner_width - fixed_width) content_label_width = max(len("column"), *(len(_sanitize_label(bar.label)) for bar in bars)) - label_width = min(max(_MIN_LEGEND_LABEL_WIDTH, content_label_width), available_label_width) - return label_width, done_width, rate_width, input_width, output_width, status_width + desired_label_width = max(_MIN_LEGEND_LABEL_WIDTH, content_label_width) + available_width = inner_width - fixed_width_without_label_or_progress + + if available_width >= desired_label_width + _MIN_PROGRESS_BAR_WIDTH: + label_width = desired_label_width + progress_width = available_width - label_width + elif available_width >= _MIN_LEGEND_LABEL_WIDTH + _MIN_PROGRESS_BAR_WIDTH: + progress_width = _MIN_PROGRESS_BAR_WIDTH + label_width = available_width - progress_width + else: + label_width = max(_MIN_LEGEND_LABEL_WIDTH, min(desired_label_width, max(0, available_width))) + progress_width = max(0, available_width - label_width) + + return label_width, done_width, rate_width, input_width, output_width, status_width, progress_width + + def _format_progress_bar(self, bar: _BarState, width: int, color: str) -> str: + if width <= 0: + return "" + + if bar.total <= 0: + fraction = 1.0 + else: + fraction = min(max(bar.completed / bar.total, 0.0), 1.0) + + filled_width = min(width, int(round(width * fraction))) + if bar.completed > 0: + filled_width = max(1, filled_width) + empty_width = width - filled_width + return f"{color}{_PROGRESS_BAR_CHAR * filled_width}{_TRACK}{_PROGRESS_BAR_CHAR * empty_width}{_RESET}" def _format_legend_table_line( self, @@ -556,16 +597,21 @@ def _format_legend_table_line( input_width: int, output_width: int, status_width: int, + progress_bar: str, + progress_width: int, ) -> str: marker_text = f"{marker} " if marker else " " gap = " " * _LEGEND_COLUMN_GAP line = ( - f"{marker_text}{_fit_plain(label, label_width)}{gap}{done:>{done_width}}" + f"{marker_text}{_fit_plain(label, label_width)}" f"{gap}{now_value:>{rate_width}}{gap}{avg_value:>{rate_width}}" f"{gap}{input_token_rate:>{input_width}}{gap}{output_token_rate:>{output_width}}" ) if status is not None: line = f"{line}{gap}{status:>{status_width}}" + line = f"{line}{gap}{done:>{done_width}}" + if progress_width > 0: + line = f"{line}{gap}{_fit_ansi(progress_bar, progress_width)}" if marker: return line return f"{_MUTED}{line}{_RESET}" diff --git a/packages/data-designer-engine/tests/engine/dataset_builders/utils/test_sticky_progress_bar.py b/packages/data-designer-engine/tests/engine/dataset_builders/utils/test_sticky_progress_bar.py index dfda4e11d..d70a0ea96 100644 --- a/packages/data-designer-engine/tests/engine/dataset_builders/utils/test_sticky_progress_bar.py +++ b/packages/data-designer-engine/tests/engine/dataset_builders/utils/test_sticky_progress_bar.py @@ -107,6 +107,10 @@ def test_renders_bounded_throughput_panel(tty_stream: FakeTTY) -> None: row = next(line for line in panel_lines if "column 'a'" in line) assert "|" not in header assert "|" not in row + assert header.index("out tok/s") < header.index("done") + assert "━" in row + assert row.rindex("0.0") < row.index("10/100") + assert row.index("10/100") < row.index("━") assert "ā•­" in panel assert "ā•°" in panel From 75dcb5e647d75edeabcd4e3bea4fe8c8ecef9ba5 Mon Sep 17 00:00:00 2001 From: "Eric W. Tramel" Date: Thu, 21 May 2026 14:13:16 -0400 Subject: [PATCH 09/19] fix: split model usage from column progress Signed-off-by: Eric W. Tramel --- .../assets/progress-throughput-panel-demo.png | Bin 29282 -> 35116 bytes examples/progress_panel_demo.py | 20 +- .../utils/async_progress_reporter.py | 10 +- .../utils/sticky_progress_bar.py | 281 ++++++++++++++---- .../utils/test_sticky_progress_bar.py | 45 ++- 5 files changed, 261 insertions(+), 95 deletions(-) diff --git a/docs/assets/progress-throughput-panel-demo.png b/docs/assets/progress-throughput-panel-demo.png index b64fad90083cd859a1378b495b62988c598cae16..ce8173ba93b92ea66e30185913629f86cde88b12 100644 GIT binary patch literal 35116 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

=&@pk6uAjwu_jb3^c%577B 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 str: input_tokens = 80 + (sum(ord(ch) for ch in topic) % 45) output_tokens = 12 + (sum(ord(ch) for ch in column) % 16) with _WORKERS: - # This example is intentionally credential-free, so emit synthetic - # token usage to exercise the progress panel's live token-rate columns. for _ in range(4): time.sleep((base_delay + jitter) / 4) - emit_token_usage_event( - TokenUsageEvent( - model_alias="progress-panel-demo", - model_name="synthetic-token-stream", - input_tokens=input_tokens // 4, - output_tokens=output_tokens // 4, - column=current_generation_column.get() or column, - ) + # This example is intentionally credential-free, so emit one synthetic + # model request per generated cell to exercise the model usage table. + emit_token_usage_event( + TokenUsageEvent( + model_alias="progress-panel-demo", + model_name="synthetic-token-stream", + input_tokens=input_tokens, + output_tokens=output_tokens, + column=current_generation_column.get() or column, ) + ) return f"{column}:{topic.lower().replace(' ', '-')}" diff --git a/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/async_progress_reporter.py b/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/async_progress_reporter.py index e0f112f5e..d58511187 100644 --- a/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/async_progress_reporter.py +++ b/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/async_progress_reporter.py @@ -124,14 +124,10 @@ def _update_bar(self, *, force: bool = False) -> None: self._bar.update_many(updates, force=force) def _record_token_usage(self, event: TokenUsageEvent) -> None: - column = event.column - if column is None and event.correlation is not None: - column = event.correlation.task_column - if column is None or column not in self._trackers: - return if self._bar is not None: - self._bar.record_token_usage( - column, + self._bar.record_model_usage( + model_alias=event.model_alias, + model_name=event.model_name, input_tokens=event.input_tokens, output_tokens=event.output_tokens, ) diff --git a/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/sticky_progress_bar.py b/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/sticky_progress_bar.py index d416180d2..00860141f 100644 --- a/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/sticky_progress_bar.py +++ b/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/sticky_progress_bar.py @@ -33,7 +33,7 @@ asciichartpy.cyan, asciichartpy.green, ] -_DEFAULT_PANEL_HEIGHT = 16 +_DEFAULT_PANEL_HEIGHT = 20 _MIN_PANEL_HEIGHT = 9 _MIN_TERMINAL_WIDTH = 30 _MIN_REDRAW_INTERVAL_SECONDS = 0.75 @@ -43,6 +43,8 @@ _RATE_FORMAT = "{:6.1f} " _Y_AXIS_RESERVED = 12 _MIN_LEGEND_LABEL_WIDTH = 8 +_MIN_MODEL_ALIAS_WIDTH = 10 +_MIN_MODEL_NAME_WIDTH = 10 _RATE_COLUMN_WIDTH = 5 _INPUT_TOKEN_RATE_WIDTH = 8 _OUTPUT_TOKEN_RATE_WIDTH = 9 @@ -145,8 +147,6 @@ class _BarState: last_completed: int = 0 latest_rate: float = 0.0 rates: list[float] = field(default_factory=lambda: [0.0]) - input_tokens: int = 0 - output_tokens: int = 0 def record_update( self, @@ -176,14 +176,30 @@ def record_update( self.last_completed = bounded_completed self.last_sample_time = now - def record_token_usage(self, *, input_tokens: int, output_tokens: int) -> None: - self.input_tokens += max(0, input_tokens) - self.output_tokens += max(0, output_tokens) - def average_rate(self, now: float) -> float: elapsed = max(now - self.start_time, 0.001) return self.completed / elapsed if elapsed > 0 else 0.0 + +@dataclass +class _ModelUsageState: + model_alias: str + model_name: str + start_time: float + request_count: int = 0 + input_tokens: int = 0 + output_tokens: int = 0 + + def record_usage(self, *, model_name: str, input_tokens: int, output_tokens: int) -> None: + self.model_name = model_name + self.request_count += 1 + self.input_tokens += max(0, input_tokens) + self.output_tokens += max(0, output_tokens) + + def rpm(self, now: float) -> float: + elapsed_minutes = max((now - self.start_time) / 60.0, 0.001) + return self.request_count / elapsed_minutes + def input_token_rate(self, now: float) -> float: elapsed = max(now - self.start_time, 0.001) return self.input_tokens / elapsed if elapsed > 0 else 0.0 @@ -215,6 +231,7 @@ def __init__(self, stream: TextIO | None = None, *, panel_height: int = _DEFAULT self._stream = stream or sys.stderr self._is_tty = hasattr(self._stream, "isatty") and self._stream.isatty() self._bars: dict[str, _BarState] = {} + self._model_usage: dict[str, _ModelUsageState] = {} self._lock = Lock() self._drawn_lines = 0 self._active = False @@ -296,20 +313,32 @@ def update_many(self, updates: dict[str, _ProgressUpdate], *, force: bool = Fals if self._active: self._redraw_if_due(now, force=force) - def record_token_usage( + def record_model_usage( self, - key: str, *, + model_alias: str, + model_name: str, input_tokens: int, output_tokens: int, force: bool = False, ) -> None: with self._lock: - if bar := self._bars.get(key): - now = time.perf_counter() - bar.record_token_usage(input_tokens=input_tokens, output_tokens=output_tokens) - if self._active: - self._redraw_if_due(now, force=force) + now = time.perf_counter() + alias = _sanitize_label(model_alias) or "(unknown)" + name = _sanitize_label(model_name) or "(unknown)" + if state := self._model_usage.get(alias): + state.record_usage(model_name=name, input_tokens=input_tokens, output_tokens=output_tokens) + else: + self._model_usage[alias] = _ModelUsageState( + model_alias=alias, + model_name=name, + start_time=self._start_time, + request_count=1, + input_tokens=max(0, input_tokens), + output_tokens=max(0, output_tokens), + ) + if self._active: + self._redraw_if_due(now, force=force) def remove_bar(self, key: str) -> None: with self._lock: @@ -381,14 +410,19 @@ def _format_panel(self) -> list[str]: panel_height = min(self._panel_height, max(_MIN_PANEL_HEIGHT, terminal_size.lines - 1)) inner_width = panel_width - 2 - legend_capacity = 5 if panel_height >= 13 else max(1, panel_height - 9) - chart_line_count = max(3, panel_height - 4 - legend_capacity) + body_capacity = max(1, panel_height - 4) + max_legend_capacity = max(1, body_capacity - 3) + desired_legend_capacity = 8 if self._model_usage and panel_height >= 13 else 5 + legend_capacity = min(max_legend_capacity, desired_legend_capacity) + chart_line_count = max(3, body_capacity - legend_capacity) + legend_capacity = max(1, body_capacity - chart_line_count) chart_height = chart_line_count - 1 now = time.perf_counter() bars = list(self._bars.values()) + model_usage = list(self._model_usage.values()) chart_lines = self._format_chart_lines(bars, inner_width, chart_height) - legend_lines = self._format_legend_lines(bars, now, legend_capacity, inner_width) + legend_lines = self._format_legend_lines(bars, model_usage, now, legend_capacity, inner_width) lines = [ self._border("ā•­", "─", "ā•®", panel_width), @@ -435,6 +469,32 @@ def _format_chart_lines(self, bars: list[_BarState], inner_width: int, chart_hei return lines[: chart_height + 1] def _format_legend_lines( + self, + bars: list[_BarState], + model_usage: list[_ModelUsageState], + now: float, + capacity: int, + inner_width: int, + ) -> list[str]: + if capacity <= 0: + return [] + + if not model_usage or capacity < 6: + lines = self._format_column_table_lines(bars, now, capacity, inner_width) + else: + model_capacity = min(len(model_usage) + 1, 3) + gap_capacity = 1 if capacity - model_capacity >= 3 else 0 + column_capacity = max(1, capacity - model_capacity - gap_capacity) + lines = self._format_column_table_lines(bars, now, column_capacity, inner_width) + if gap_capacity: + lines.append("") + lines.extend(self._format_model_table_lines(model_usage, now, model_capacity, inner_width)) + + while len(lines) < capacity: + lines.append("") + return lines[:capacity] + + def _format_column_table_lines( self, bars: list[_BarState], now: float, @@ -446,13 +506,11 @@ def _format_legend_lines( return lines include_status = any(bar.failed or bar.skipped for bar in bars) - label_width, done_width, rate_width, input_width, output_width, status_width, progress_width = ( - self._legend_column_widths( - bars, - now, - include_status=include_status, - inner_width=inner_width, - ) + label_width, done_width, rate_width, status_width, progress_width = self._column_table_widths( + bars, + now, + include_status=include_status, + inner_width=inner_width, ) lines.append( self._format_legend_table_line( @@ -461,14 +519,10 @@ def _format_legend_lines( done="done", now_value=_NOW_RATE_HEADER, avg_value=_AVG_RATE_HEADER, - input_token_rate="in tok/s", - output_token_rate="out tok/s", status="status" if include_status else None, label_width=label_width, done_width=done_width, rate_width=rate_width, - input_width=input_width, - output_width=output_width, status_width=status_width, progress_bar="", progress_width=progress_width, @@ -489,14 +543,10 @@ def _format_legend_lines( done=self._format_done(bar), now_value=f"{bar.latest_rate:.1f}", avg_value=f"{bar.average_rate(now):.1f}", - input_token_rate=f"{bar.input_token_rate(now):.1f}", - output_token_rate=f"{bar.output_token_rate(now):.1f}", status=self._format_status(bar) if include_status else None, label_width=label_width, done_width=done_width, rate_width=rate_width, - input_width=input_width, - output_width=output_width, status_width=status_width, progress_bar=self._format_progress_bar(bar, progress_width, color), progress_width=progress_width, @@ -506,18 +556,16 @@ def _format_legend_lines( if len(bars) > row_capacity and row_capacity > 0: lines.append(f"{_MUTED}... {len(bars) - len(visible_bars)} more column(s){_RESET}") - while len(lines) < capacity: - lines.append("") return lines[:capacity] - def _legend_column_widths( + def _column_table_widths( self, bars: list[_BarState], now: float, *, include_status: bool, inner_width: int, - ) -> tuple[int, int, int, int, int, int, int]: + ) -> tuple[int, int, int, int, int]: done_width = max(len("done"), *(len(self._format_done(bar)) for bar in bars)) rate_width = max( len(_NOW_RATE_HEADER), @@ -525,29 +573,13 @@ def _legend_column_widths( _RATE_COLUMN_WIDTH, *(len(f"{value:.1f}") for bar in bars for value in (bar.latest_rate, bar.average_rate(now))), ) - input_width = max( - len("in tok/s"), - _INPUT_TOKEN_RATE_WIDTH, - *(len(f"{bar.input_token_rate(now):.1f}") for bar in bars), - ) - output_width = max( - len("out tok/s"), - _OUTPUT_TOKEN_RATE_WIDTH, - *(len(f"{bar.output_token_rate(now):.1f}") for bar in bars), - ) status_width = 0 if include_status: status_width = max(len("status"), *(len(self._format_status(bar)) for bar in bars)) - separator_count = 6 + int(include_status) + separator_count = 4 + int(include_status) fixed_width_without_label_or_progress = ( - 2 - + (separator_count * _LEGEND_COLUMN_GAP) - + done_width - + (rate_width * 2) - + input_width - + output_width - + status_width + 2 + (separator_count * _LEGEND_COLUMN_GAP) + done_width + (rate_width * 2) + status_width ) content_label_width = max(len("column"), *(len(_sanitize_label(bar.label)) for bar in bars)) desired_label_width = max(_MIN_LEGEND_LABEL_WIDTH, content_label_width) @@ -563,7 +595,113 @@ def _legend_column_widths( label_width = max(_MIN_LEGEND_LABEL_WIDTH, min(desired_label_width, max(0, available_width))) progress_width = max(0, available_width - label_width) - return label_width, done_width, rate_width, input_width, output_width, status_width, progress_width + return label_width, done_width, rate_width, status_width, progress_width + + def _format_model_table_lines( + self, + model_usage: list[_ModelUsageState], + now: float, + capacity: int, + inner_width: int, + ) -> list[str]: + lines: list[str] = [] + if capacity <= 0: + return lines + + alias_width, model_width, rpm_width, input_width, output_width = self._model_table_widths( + model_usage, + now, + inner_width, + ) + lines.append( + self._format_model_table_line( + model_alias="model alias", + model_name="model name", + rpm="rpm", + input_token_rate="in tok/s", + output_token_rate="out tok/s", + alias_width=alias_width, + model_width=model_width, + rpm_width=rpm_width, + input_width=input_width, + output_width=output_width, + header=True, + ) + ) + + row_capacity = max(0, capacity - 1) + visible_usage = model_usage[:row_capacity] + if len(model_usage) > row_capacity and row_capacity > 0: + visible_usage = model_usage[: max(0, row_capacity - 1)] + + for state in visible_usage: + lines.append( + self._format_model_table_line( + model_alias=state.model_alias, + model_name=state.model_name, + rpm=f"{state.rpm(now):.1f}", + input_token_rate=f"{state.input_token_rate(now):.1f}", + output_token_rate=f"{state.output_token_rate(now):.1f}", + alias_width=alias_width, + model_width=model_width, + rpm_width=rpm_width, + input_width=input_width, + output_width=output_width, + header=False, + ) + ) + + if len(model_usage) > row_capacity and row_capacity > 0: + lines.append(f"{_MUTED}... {len(model_usage) - len(visible_usage)} more model(s){_RESET}") + + return lines[:capacity] + + def _model_table_widths( + self, + model_usage: list[_ModelUsageState], + now: float, + inner_width: int, + ) -> tuple[int, int, int, int, int]: + rpm_width = max( + len("rpm"), + _RATE_COLUMN_WIDTH, + *(len(f"{state.rpm(now):.1f}") for state in model_usage), + ) + input_width = max( + len("in tok/s"), + _INPUT_TOKEN_RATE_WIDTH, + *(len(f"{state.input_token_rate(now):.1f}") for state in model_usage), + ) + output_width = max( + len("out tok/s"), + _OUTPUT_TOKEN_RATE_WIDTH, + *(len(f"{state.output_token_rate(now):.1f}") for state in model_usage), + ) + + fixed_width_without_text = 2 + (4 * _LEGEND_COLUMN_GAP) + rpm_width + input_width + output_width + available_text_width = inner_width - fixed_width_without_text + desired_alias_width = max( + _MIN_MODEL_ALIAS_WIDTH, + len("model alias"), + *(len(state.model_alias) for state in model_usage), + ) + desired_model_width = max( + _MIN_MODEL_NAME_WIDTH, + len("model name"), + *(len(state.model_name) for state in model_usage), + ) + + if available_text_width >= desired_alias_width + desired_model_width: + alias_width = desired_alias_width + model_width = desired_model_width + elif available_text_width >= _MIN_MODEL_ALIAS_WIDTH + _MIN_MODEL_NAME_WIDTH: + alias_width = min(desired_alias_width, max(_MIN_MODEL_ALIAS_WIDTH, available_text_width // 2)) + model_width = available_text_width - alias_width + else: + alias_width = _MIN_MODEL_ALIAS_WIDTH + model_width = _MIN_MODEL_NAME_WIDTH + + return alias_width, model_width, rpm_width, input_width, output_width def _format_progress_bar(self, bar: _BarState, width: int, color: str) -> str: if width <= 0: @@ -588,14 +726,10 @@ def _format_legend_table_line( done: str, now_value: str, avg_value: str, - input_token_rate: str, - output_token_rate: str, status: str | None, label_width: int, done_width: int, rate_width: int, - input_width: int, - output_width: int, status_width: int, progress_bar: str, progress_width: int, @@ -603,9 +737,7 @@ def _format_legend_table_line( marker_text = f"{marker} " if marker else " " gap = " " * _LEGEND_COLUMN_GAP line = ( - f"{marker_text}{_fit_plain(label, label_width)}" - f"{gap}{now_value:>{rate_width}}{gap}{avg_value:>{rate_width}}" - f"{gap}{input_token_rate:>{input_width}}{gap}{output_token_rate:>{output_width}}" + f"{marker_text}{_fit_plain(label, label_width)}{gap}{now_value:>{rate_width}}{gap}{avg_value:>{rate_width}}" ) if status is not None: line = f"{line}{gap}{status:>{status_width}}" @@ -616,6 +748,31 @@ def _format_legend_table_line( return line return f"{_MUTED}{line}{_RESET}" + def _format_model_table_line( + self, + *, + model_alias: str, + model_name: str, + rpm: str, + input_token_rate: str, + output_token_rate: str, + alias_width: int, + model_width: int, + rpm_width: int, + input_width: int, + output_width: int, + header: bool, + ) -> str: + gap = " " * _LEGEND_COLUMN_GAP + line = ( + f" {_fit_plain(model_alias, alias_width)}{gap}{_fit_plain(model_name, model_width)}" + f"{gap}{rpm:>{rpm_width}}{gap}{input_token_rate:>{input_width}}" + f"{gap}{output_token_rate:>{output_width}}" + ) + if header: + return f"{_MUTED}{line}{_RESET}" + return line + def _format_done(self, bar: _BarState) -> str: pct = (bar.completed / bar.total * 100) if bar.total > 0 else 100.0 return f"{bar.completed}/{bar.total} {pct:3.0f}%" diff --git a/packages/data-designer-engine/tests/engine/dataset_builders/utils/test_sticky_progress_bar.py b/packages/data-designer-engine/tests/engine/dataset_builders/utils/test_sticky_progress_bar.py index d70a0ea96..c032548f1 100644 --- a/packages/data-designer-engine/tests/engine/dataset_builders/utils/test_sticky_progress_bar.py +++ b/packages/data-designer-engine/tests/engine/dataset_builders/utils/test_sticky_progress_bar.py @@ -92,7 +92,7 @@ def test_renders_bounded_throughput_panel(tty_stream: FakeTTY) -> None: bar.add_bar("b", "column 'b'", 100) bar.update_many({"a": (10, 10, 0, 0), "b": (20, 20, 0, 0)}, force=True) - assert bar.drawn_lines == 16 + assert bar.drawn_lines == 20 panel_lines = _last_panel_lines(tty_stream.getvalue()) panel = "\n".join(panel_lines) assert "Throughput" in panel @@ -103,11 +103,13 @@ def test_renders_bounded_throughput_panel(tty_stream: FakeTTY) -> None: assert "10/100" in panel assert "column 'b'" in panel assert "20/100" in panel - header = next(line for line in panel_lines if "in tok/s" in line) + header = next(line for line in panel_lines if "now rec/s" in line) row = next(line for line in panel_lines if "column 'a'" in line) assert "|" not in header assert "|" not in row - assert header.index("out tok/s") < header.index("done") + assert "in tok/s" not in panel + assert "out tok/s" not in panel + assert header.index("avg rec/s") < header.index("done") assert "━" in row assert row.rindex("0.0") < row.index("10/100") assert row.index("10/100") < row.index("━") @@ -115,16 +117,28 @@ def test_renders_bounded_throughput_panel(tty_stream: FakeTTY) -> None: assert "ā•°" in panel -def test_token_usage_rates_render_in_legend_table(tty_stream: FakeTTY) -> None: +def test_model_usage_rates_render_in_separate_table(tty_stream: FakeTTY) -> None: with StickyProgressBar(stream=tty_stream) as bar: bar.add_bar("a", "column 'a'", 100) bar.update("a", completed=10, success=10, force=True) - bar._bars["a"].start_time = time.perf_counter() - 10.0 # noqa: SLF001 - bar.record_token_usage("a", input_tokens=100, output_tokens=25, force=True) + bar._start_time = time.perf_counter() - 10.0 # noqa: SLF001 + bar.record_model_usage( + model_alias="test", + model_name="test-model", + input_tokens=100, + output_tokens=25, + force=True, + ) panel = "\n".join(_last_panel_lines(tty_stream.getvalue())) + assert "model alias" in panel + assert "model name" in panel + assert "test" in panel + assert "test-model" in panel + assert "rpm" in panel assert "in tok/s" in panel assert "out tok/s" in panel + assert "6.0" in panel assert "10.0" in panel assert "2.5" in panel @@ -176,8 +190,8 @@ def test_frequent_updates_are_redraw_throttled(tty_stream: FakeTTY) -> None: assert tty_stream.getvalue()[len(snapshot) :].count(CURSOR_UP_CLEAR) == 0 bar.update("a", completed=50, success=50, force=True) - assert tty_stream.getvalue()[len(snapshot) :].count(CURSOR_UP_CLEAR) == 16 - assert bar.drawn_lines == 16 + assert tty_stream.getvalue()[len(snapshot) :].count(CURSOR_UP_CLEAR) == 20 + assert bar.drawn_lines == 20 def test_log_interleaving_preserves_panel_height(tty_stream: FakeTTY) -> None: @@ -198,7 +212,7 @@ def test_log_interleaving_preserves_panel_height(tty_stream: FakeTTY) -> None: snapshot = tty_stream.getvalue() bar.update("x", completed=20, success=20, force=True) - assert tty_stream.getvalue()[len(snapshot) :].count(CURSOR_UP_CLEAR) == 16 + assert tty_stream.getvalue()[len(snapshot) :].count(CURSOR_UP_CLEAR) == 20 finally: root_logger.removeHandler(handler) @@ -225,7 +239,7 @@ def test_update_many_single_redraw(tty_stream: FakeTTY) -> None: after = tty_stream.getvalue() new_output = after[len(before) :] - assert new_output.count(CURSOR_UP_CLEAR) == 16 + assert new_output.count(CURSOR_UP_CLEAR) == 20 clean = _clean(after) assert "10/100" in clean @@ -253,7 +267,7 @@ def test_remove_bar_redraws_panel(tty_stream: FakeTTY) -> None: bar.remove_bar("a") new_output = tty_stream.getvalue()[len(snapshot) :] - assert new_output.count(CURSOR_UP_CLEAR) == 16 + assert new_output.count(CURSOR_UP_CLEAR) == 20 panel = "\n".join(_last_panel_lines(tty_stream.getvalue())) assert "col_a" not in panel assert "col_b" in panel @@ -286,11 +300,10 @@ def test_reporter_updates_and_logs_keep_drawn_lines_in_sync(tty_stream: FakeTTY) model_name="test-model", input_tokens=120, output_tokens=30, - column="col_a", ) ) - assert bar._bars["col_a"].input_tokens == 120 # noqa: SLF001 - assert bar._bars["col_a"].output_tokens == 30 # noqa: SLF001 + assert bar._model_usage["test"].input_tokens == 120 # noqa: SLF001 + assert bar._model_usage["test"].output_tokens == 30 # noqa: SLF001 snapshot = tty_stream.getvalue() reporter.record_success("col_a") @@ -304,7 +317,7 @@ def test_reporter_updates_and_logs_keep_drawn_lines_in_sync(tty_stream: FakeTTY) snapshot = tty_stream.getvalue() reporter.log_final() - assert tty_stream.getvalue()[len(snapshot) :].count(CURSOR_UP_CLEAR) == 16 - assert bar.drawn_lines == 16 + assert tty_stream.getvalue()[len(snapshot) :].count(CURSOR_UP_CLEAR) == 20 + assert bar.drawn_lines == 20 finally: root_logger.removeHandler(handler) From 461f261db41fab0d2ae63ad1907a37e648704792 Mon Sep 17 00:00:00 2001 From: "Eric W. Tramel" Date: Thu, 21 May 2026 14:20:31 -0400 Subject: [PATCH 10/19] feat: add live multi-model traffic demo Signed-off-by: Eric W. Tramel --- .../assets/progress-throughput-panel-demo.png | Bin 35116 -> 36291 bytes examples/live_traffic_multi_model_demo.py | 104 ++++++++++++++++++ .../utils/sticky_progress_bar.py | 6 +- .../utils/test_sticky_progress_bar.py | 16 +-- 4 files changed, 115 insertions(+), 11 deletions(-) create mode 100644 examples/live_traffic_multi_model_demo.py diff --git a/docs/assets/progress-throughput-panel-demo.png b/docs/assets/progress-throughput-panel-demo.png index ce8173ba93b92ea66e30185913629f86cde88b12..297f99617c9d4e9d6236ac5cb476a99588504282 100644 GIT binary patch literal 36291 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

=&@pk6uAjwu_jb3^c%577B z$Wm;@GNYrLUijnNR-(n{jgE>sZd4chUI;-K%DJm%oN!5PRI!jz+V{Fig-yw)sTiL;v5Usl=3~34ZVf@Uq>LGG^QU6}dR`Tj{($VtM=o6?S zvvA$|?R*sfTL)rcdrVD??jN%l_5Cv*qRssA$6CC26R1%|Y^Lard_^rM%6LH%!4q=| zF(wioqRBpf_>06rA6u2jln0wX0oy(fW%<#`5ieFM-8CtokMh}rE?b6T+X`} zLb##>oy+T#6mGj*_c{&gyvjLn;|$KOs-(}KbK2T;05-k4R%pvFWp;e)%8t=ihr*0` zD0un9Z#vE5l3}{&z@mX@3 zaC1|#y4u1e-FO#TzKZiX93lOK{!9vNgmdp7ciufJcj0Nol`Mu61uC7^eQ<5o;SMjD z@BIpu6rwX?a=XTX>%iPuspg8wz$Oi=@UPrM=yR7Tl%HS>5ITBv5uIp! zcavgf6WM8tYLRe!xoGM_i1^Y=*2*gLtW+}uqi}I@u3f+0-}LLR6*%7Os-4iz95XY)X!LNg+h4!HR>PIj6@5_)N_Rv(UAcCx z<)!2#sPQ}t)`^y^0*;#+IW|s1pDN)8K63al(2l-0cb0aLYyKsZ>K_fnnb?@ZudS`= zQFsk1in>V#MxD8vI8sEIR0qH-4ibMgfG!0-Z9p-T z7aTlvZ(SFLFs<3xe;m1=44fIuQLgO}mclH~Q80D2(e!cWyyjHIdV^vK3u7*w7QDw| zs*)T+U!#V#{ty`EFU?2;&V|JTRev<_6F&)-!m4VpXcS7m{o;rhdR;Ol?b7_D7IR;$ zz}UyZ*VuEQBhkbhId%+QLP28hRJ(I?Ur+kn^-m9`LEVaJeH zjbDHRU(EhCLP{{0MRPB2ohEYF08wKsUP^wxGd?ZBd43)+$IR$bcyc?$;J7ddOR9DM z!k?-p(Klq7)Dp?Zb%jpdcIgNRf&zl}@JAm8L%Uds(x{s@Hxl z=LCXn0b;4=xA5}}%UYR)en3kV2C>YRd5nmYSl}Ye?}h= zQu%uQSjcbDMJh9Uj7M8MK{PisHtNKF^Gf}qxQ;&<7RN65X)otx=v&^tjWH;HmUx9g zCxR9GqQ1o#-L)pU!Ewc3KtVVW*G`Y-R13Rbl-6+-$(%+=d>@YquauLPhT(|+m(dBa zqDHM(T8+HDyvB?6Nl_^N$BcWQ!G_%bai7Fes%)d*PwUlF^g58*4pWMle(56-iSO9y zztUyR(6hy+zfsN8 dd.DataDesignerConfigBuilder: + config_builder = dd.DataDesignerConfigBuilder(model_configs=MODEL_CONFIGS) + + config_builder.add_column( + dd.SamplerColumnConfig( + name="topic", + sampler_type=dd.SamplerType.CATEGORY, + params=dd.CategorySamplerParams( + values=[ + "GPU quota increase", + "Training job timeout", + "Invoice dispute", + "Model quality regression", + "Dataset permission issue", + "Deployment latency spike", + "Account recovery", + "Documentation clarification", + ], + ), + ) + ) + config_builder.add_column( + dd.SamplerColumnConfig( + name="customer_tone", + sampler_type=dd.SamplerType.CATEGORY, + params=dd.CategorySamplerParams(values=["calm", "urgent", "frustrated", "curious"]), + ) + ) + config_builder.add_column( + dd.LLMTextColumnConfig( + name="gpt_oss_20b_intent", + model_alias="gpt-oss-20b", + system_prompt=SYSTEM_PROMPT, + prompt=("Classify the likely support intent for a {{ customer_tone }} customer asking about: {{ topic }}."), + ) + ) + config_builder.add_column( + dd.LLMTextColumnConfig( + name="nemotron_31b_next_action", + model_alias="nemotron-31b", + system_prompt=SYSTEM_PROMPT, + prompt=("Suggest the first support action for a {{ customer_tone }} customer asking about: {{ topic }}."), + ) + ) + config_builder.add_column( + dd.LLMTextColumnConfig( + name="gpt_oss_120b_risk_signal", + model_alias="gpt-oss-120b", + system_prompt=SYSTEM_PROMPT, + prompt=("Name the main operational risk for a {{ customer_tone }} customer asking about: {{ topic }}."), + ) + ) + + return config_builder diff --git a/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/sticky_progress_bar.py b/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/sticky_progress_bar.py index 00860141f..cbfefc1c8 100644 --- a/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/sticky_progress_bar.py +++ b/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/sticky_progress_bar.py @@ -33,7 +33,7 @@ asciichartpy.cyan, asciichartpy.green, ] -_DEFAULT_PANEL_HEIGHT = 20 +_DEFAULT_PANEL_HEIGHT = 22 _MIN_PANEL_HEIGHT = 9 _MIN_TERMINAL_WIDTH = 30 _MIN_REDRAW_INTERVAL_SECONDS = 0.75 @@ -412,7 +412,7 @@ def _format_panel(self) -> list[str]: body_capacity = max(1, panel_height - 4) max_legend_capacity = max(1, body_capacity - 3) - desired_legend_capacity = 8 if self._model_usage and panel_height >= 13 else 5 + desired_legend_capacity = 9 if self._model_usage and panel_height >= 13 else 5 legend_capacity = min(max_legend_capacity, desired_legend_capacity) chart_line_count = max(3, body_capacity - legend_capacity) legend_capacity = max(1, body_capacity - chart_line_count) @@ -482,7 +482,7 @@ def _format_legend_lines( if not model_usage or capacity < 6: lines = self._format_column_table_lines(bars, now, capacity, inner_width) else: - model_capacity = min(len(model_usage) + 1, 3) + model_capacity = min(len(model_usage) + 1, 4) gap_capacity = 1 if capacity - model_capacity >= 3 else 0 column_capacity = max(1, capacity - model_capacity - gap_capacity) lines = self._format_column_table_lines(bars, now, column_capacity, inner_width) diff --git a/packages/data-designer-engine/tests/engine/dataset_builders/utils/test_sticky_progress_bar.py b/packages/data-designer-engine/tests/engine/dataset_builders/utils/test_sticky_progress_bar.py index c032548f1..6cdde2c09 100644 --- a/packages/data-designer-engine/tests/engine/dataset_builders/utils/test_sticky_progress_bar.py +++ b/packages/data-designer-engine/tests/engine/dataset_builders/utils/test_sticky_progress_bar.py @@ -92,7 +92,7 @@ def test_renders_bounded_throughput_panel(tty_stream: FakeTTY) -> None: bar.add_bar("b", "column 'b'", 100) bar.update_many({"a": (10, 10, 0, 0), "b": (20, 20, 0, 0)}, force=True) - assert bar.drawn_lines == 20 + assert bar.drawn_lines == 22 panel_lines = _last_panel_lines(tty_stream.getvalue()) panel = "\n".join(panel_lines) assert "Throughput" in panel @@ -190,8 +190,8 @@ def test_frequent_updates_are_redraw_throttled(tty_stream: FakeTTY) -> None: assert tty_stream.getvalue()[len(snapshot) :].count(CURSOR_UP_CLEAR) == 0 bar.update("a", completed=50, success=50, force=True) - assert tty_stream.getvalue()[len(snapshot) :].count(CURSOR_UP_CLEAR) == 20 - assert bar.drawn_lines == 20 + assert tty_stream.getvalue()[len(snapshot) :].count(CURSOR_UP_CLEAR) == 22 + assert bar.drawn_lines == 22 def test_log_interleaving_preserves_panel_height(tty_stream: FakeTTY) -> None: @@ -212,7 +212,7 @@ def test_log_interleaving_preserves_panel_height(tty_stream: FakeTTY) -> None: snapshot = tty_stream.getvalue() bar.update("x", completed=20, success=20, force=True) - assert tty_stream.getvalue()[len(snapshot) :].count(CURSOR_UP_CLEAR) == 20 + assert tty_stream.getvalue()[len(snapshot) :].count(CURSOR_UP_CLEAR) == 22 finally: root_logger.removeHandler(handler) @@ -239,7 +239,7 @@ def test_update_many_single_redraw(tty_stream: FakeTTY) -> None: after = tty_stream.getvalue() new_output = after[len(before) :] - assert new_output.count(CURSOR_UP_CLEAR) == 20 + assert new_output.count(CURSOR_UP_CLEAR) == 22 clean = _clean(after) assert "10/100" in clean @@ -267,7 +267,7 @@ def test_remove_bar_redraws_panel(tty_stream: FakeTTY) -> None: bar.remove_bar("a") new_output = tty_stream.getvalue()[len(snapshot) :] - assert new_output.count(CURSOR_UP_CLEAR) == 20 + assert new_output.count(CURSOR_UP_CLEAR) == 22 panel = "\n".join(_last_panel_lines(tty_stream.getvalue())) assert "col_a" not in panel assert "col_b" in panel @@ -317,7 +317,7 @@ def test_reporter_updates_and_logs_keep_drawn_lines_in_sync(tty_stream: FakeTTY) snapshot = tty_stream.getvalue() reporter.log_final() - assert tty_stream.getvalue()[len(snapshot) :].count(CURSOR_UP_CLEAR) == 20 - assert bar.drawn_lines == 20 + assert tty_stream.getvalue()[len(snapshot) :].count(CURSOR_UP_CLEAR) == 22 + assert bar.drawn_lines == 22 finally: root_logger.removeHandler(handler) From f6a6d2e1f3ceaa7feaef7f3ca3a77ff501096108 Mon Sep 17 00:00:00 2001 From: "Eric W. Tramel" Date: Thu, 21 May 2026 14:26:16 -0400 Subject: [PATCH 11/19] fix: keep throughput chart height stable Signed-off-by: Eric W. Tramel --- .../utils/sticky_progress_bar.py | 64 +++++-------------- .../utils/test_sticky_progress_bar.py | 34 ++++++++++ 2 files changed, 50 insertions(+), 48 deletions(-) diff --git a/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/sticky_progress_bar.py b/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/sticky_progress_bar.py index cbfefc1c8..8030712ba 100644 --- a/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/sticky_progress_bar.py +++ b/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/sticky_progress_bar.py @@ -42,6 +42,8 @@ _MAX_RATE_SAMPLES = 7200 _RATE_FORMAT = "{:6.1f} " _Y_AXIS_RESERVED = 12 +_CHART_LINE_COUNT = 9 +_MIN_CHART_LINE_COUNT = 3 _MIN_LEGEND_LABEL_WIDTH = 8 _MIN_MODEL_ALIAS_WIDTH = 10 _MIN_MODEL_NAME_WIDTH = 10 @@ -411,18 +413,15 @@ def _format_panel(self) -> list[str]: inner_width = panel_width - 2 body_capacity = max(1, panel_height - 4) - max_legend_capacity = max(1, body_capacity - 3) - desired_legend_capacity = 9 if self._model_usage and panel_height >= 13 else 5 - legend_capacity = min(max_legend_capacity, desired_legend_capacity) - chart_line_count = max(3, body_capacity - legend_capacity) - legend_capacity = max(1, body_capacity - chart_line_count) + chart_line_count = min(_CHART_LINE_COUNT, max(_MIN_CHART_LINE_COUNT, body_capacity - 1)) + minimum_legend_capacity = max(1, body_capacity - chart_line_count) chart_height = chart_line_count - 1 now = time.perf_counter() bars = list(self._bars.values()) model_usage = list(self._model_usage.values()) chart_lines = self._format_chart_lines(bars, inner_width, chart_height) - legend_lines = self._format_legend_lines(bars, model_usage, now, legend_capacity, inner_width) + legend_lines = self._format_legend_lines(bars, model_usage, now, minimum_legend_capacity, inner_width) lines = [ self._border("ā•­", "─", "ā•®", panel_width), @@ -473,37 +472,25 @@ def _format_legend_lines( bars: list[_BarState], model_usage: list[_ModelUsageState], now: float, - capacity: int, + minimum_capacity: int, inner_width: int, ) -> list[str]: - if capacity <= 0: - return [] + lines = self._format_column_table_lines(bars, now, inner_width) + if model_usage: + lines.append("") + lines.extend(self._format_model_table_lines(model_usage, now, inner_width)) - if not model_usage or capacity < 6: - lines = self._format_column_table_lines(bars, now, capacity, inner_width) - else: - model_capacity = min(len(model_usage) + 1, 4) - gap_capacity = 1 if capacity - model_capacity >= 3 else 0 - column_capacity = max(1, capacity - model_capacity - gap_capacity) - lines = self._format_column_table_lines(bars, now, column_capacity, inner_width) - if gap_capacity: - lines.append("") - lines.extend(self._format_model_table_lines(model_usage, now, model_capacity, inner_width)) - - while len(lines) < capacity: + while len(lines) < minimum_capacity: lines.append("") - return lines[:capacity] + return lines def _format_column_table_lines( self, bars: list[_BarState], now: float, - capacity: int, inner_width: int, ) -> list[str]: lines: list[str] = [] - if capacity <= 0: - return lines include_status = any(bar.failed or bar.skipped for bar in bars) label_width, done_width, rate_width, status_width, progress_width = self._column_table_widths( @@ -529,12 +516,7 @@ def _format_column_table_lines( ) ) - row_capacity = max(0, capacity - 1) - visible_bars = bars[:row_capacity] - if len(bars) > row_capacity and row_capacity > 0: - visible_bars = bars[: max(0, row_capacity - 1)] - - for index, bar in enumerate(visible_bars): + for index, bar in enumerate(bars): color = _CURVE_COLORS[index % len(_CURVE_COLORS)] lines.append( self._format_legend_table_line( @@ -553,10 +535,7 @@ def _format_column_table_lines( ) ) - if len(bars) > row_capacity and row_capacity > 0: - lines.append(f"{_MUTED}... {len(bars) - len(visible_bars)} more column(s){_RESET}") - - return lines[:capacity] + return lines def _column_table_widths( self, @@ -601,12 +580,9 @@ def _format_model_table_lines( self, model_usage: list[_ModelUsageState], now: float, - capacity: int, inner_width: int, ) -> list[str]: lines: list[str] = [] - if capacity <= 0: - return lines alias_width, model_width, rpm_width, input_width, output_width = self._model_table_widths( model_usage, @@ -629,12 +605,7 @@ def _format_model_table_lines( ) ) - row_capacity = max(0, capacity - 1) - visible_usage = model_usage[:row_capacity] - if len(model_usage) > row_capacity and row_capacity > 0: - visible_usage = model_usage[: max(0, row_capacity - 1)] - - for state in visible_usage: + for state in model_usage: lines.append( self._format_model_table_line( model_alias=state.model_alias, @@ -651,10 +622,7 @@ def _format_model_table_lines( ) ) - if len(model_usage) > row_capacity and row_capacity > 0: - lines.append(f"{_MUTED}... {len(model_usage) - len(visible_usage)} more model(s){_RESET}") - - return lines[:capacity] + return lines def _model_table_widths( self, diff --git a/packages/data-designer-engine/tests/engine/dataset_builders/utils/test_sticky_progress_bar.py b/packages/data-designer-engine/tests/engine/dataset_builders/utils/test_sticky_progress_bar.py index 6cdde2c09..45aacb226 100644 --- a/packages/data-designer-engine/tests/engine/dataset_builders/utils/test_sticky_progress_bar.py +++ b/packages/data-designer-engine/tests/engine/dataset_builders/utils/test_sticky_progress_bar.py @@ -17,6 +17,7 @@ from data_designer.engine.dataset_builders.utils.async_progress_reporter import AsyncProgressReporter from data_designer.engine.dataset_builders.utils.progress_tracker import ProgressTracker from data_designer.engine.dataset_builders.utils.sticky_progress_bar import ( + _CHART_LINE_COUNT, _MAX_RATE_SAMPLES, _RATE_SAMPLE_INTERVAL_SECONDS, StickyProgressBar, @@ -60,6 +61,11 @@ def _last_panel_lines(output: str) -> list[str]: return clean[panel_start:].splitlines() +def _chart_lines(panel_lines: list[str]) -> list[str]: + separator_index = next(index for index, line in enumerate(panel_lines) if "ā”œ" in line) + return panel_lines[2:separator_index] + + def test_no_output_when_not_tty() -> None: stream = io.StringIO() with StickyProgressBar(stream=stream) as bar: @@ -143,6 +149,34 @@ def test_model_usage_rates_render_in_separate_table(tty_stream: FakeTTY) -> None assert "2.5" in panel +def test_many_columns_and_models_do_not_shrink_chart(tty_stream: FakeTTY) -> None: + with StickyProgressBar(stream=tty_stream) as bar: + for index in range(8): + bar.add_bar(f"col_{index}", f"column_{index}", 100) + bar.update_many( + {f"col_{index}": (index + 1, index + 1, 0, 0) for index in range(8)}, + force=True, + ) + for index in range(8): + bar.record_model_usage( + model_alias=f"model_{index}", + model_name=f"provider/model-{index}", + input_tokens=100 + index, + output_tokens=10 + index, + force=True, + ) + + panel_lines = _last_panel_lines(tty_stream.getvalue()) + panel = "\n".join(panel_lines) + assert len(_chart_lines(panel_lines)) == _CHART_LINE_COUNT + assert len(panel_lines) > 22 + assert "more column(s)" not in panel + assert "more model(s)" not in panel + for index in range(8): + assert f"column_{index}" in panel + assert f"model_{index}" in panel + + def test_control_sequences_are_removed_from_labels(tty_stream: FakeTTY) -> None: with StickyProgressBar(stream=tty_stream) as bar: bar.add_bar("a", "col\x1b[31m_a\nsuffix", 100) From de1c2d7117453781f02dd785ff6d421df3981fce Mon Sep 17 00:00:00 2001 From: "Eric W. Tramel" Date: Thu, 21 May 2026 14:45:27 -0400 Subject: [PATCH 12/19] feat: mark request feedback in throughput chart --- .../utils/async_progress_reporter.py | 13 ++ .../utils/sticky_progress_bar.py | 116 +++++++++++++++++- .../models/request_admission/controller.py | 14 +-- .../src/data_designer/engine/observability.py | 37 +++++- .../utils/test_sticky_progress_bar.py | 52 +++++++- .../request_admission/test_controller.py | 26 +++- 6 files changed, 244 insertions(+), 14 deletions(-) diff --git a/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/async_progress_reporter.py b/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/async_progress_reporter.py index d58511187..048d4d261 100644 --- a/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/async_progress_reporter.py +++ b/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/async_progress_reporter.py @@ -10,6 +10,7 @@ from data_designer.engine.dataset_builders.utils.progress_tracker import ProgressTracker from data_designer.engine.models.usage_events import TokenUsageEvent, subscribe_token_usage +from data_designer.engine.observability import RequestAdmissionEvent, subscribe_request_admission_events from data_designer.logging import LOG_INDENT if TYPE_CHECKING: @@ -19,6 +20,7 @@ DEFAULT_REPORT_INTERVAL = 5.0 DEFAULT_TTY_REPORT_INTERVAL = 0.75 +FEEDBACK_MARKER_EVENTS = frozenset({"request_rate_limited", "request_limit_decreased"}) class AsyncProgressReporter: @@ -43,10 +45,14 @@ def __init__( self._last_reported_total: int = -1 self._bar = progress_bar self._unsubscribe_token_usage: Callable[[], None] | None = None + self._unsubscribe_request_admission_events: Callable[[], None] | None = None if self._bar is not None: for col, tracker in trackers.items(): self._bar.add_bar(col, col, tracker.total_records) self._unsubscribe_token_usage = subscribe_token_usage(self._record_token_usage) + self._unsubscribe_request_admission_events = subscribe_request_admission_events( + self._record_request_admission_event + ) def log_start(self, num_row_groups: int) -> None: cols = ", ".join(self._trackers) @@ -101,6 +107,9 @@ def close(self) -> None: if self._unsubscribe_token_usage is not None: self._unsubscribe_token_usage() self._unsubscribe_token_usage = None + if self._unsubscribe_request_admission_events is not None: + self._unsubscribe_request_admission_events() + self._unsubscribe_request_admission_events = None def _maybe_report(self) -> None: now = time.perf_counter() @@ -132,6 +141,10 @@ def _record_token_usage(self, event: TokenUsageEvent) -> None: output_tokens=event.output_tokens, ) + def _record_request_admission_event(self, event: RequestAdmissionEvent) -> None: + if self._bar is not None and event.event_kind in FEEDBACK_MARKER_EVENTS: + self._bar.record_feedback_signal(event_kind=event.event_kind) + def _emit(self) -> None: current_total = sum(tracker.get_snapshot(0.0)[0] for tracker in self._trackers.values()) if current_total == self._last_reported_total: diff --git a/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/sticky_progress_bar.py b/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/sticky_progress_bar.py index 8030712ba..30a425952 100644 --- a/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/sticky_progress_bar.py +++ b/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/sticky_progress_bar.py @@ -23,6 +23,7 @@ _FAILED = "\033[31m" _OK = "\033[32m" _TRACK = "\033[38;5;238m" +_FEEDBACK_MARKER = "\033[1;38;5;196mā—†\033[0m" _CURVE_COLORS = [ asciichartpy.lightcyan, asciichartpy.lightgreen, @@ -40,6 +41,7 @@ _RATE_SAMPLE_INTERVAL_SECONDS = 2.0 _RATE_SMOOTHING_WINDOW = 3 _MAX_RATE_SAMPLES = 7200 +_MAX_FEEDBACK_MARKERS = 512 _RATE_FORMAT = "{:6.1f} " _Y_AXIS_RESERVED = 12 _CHART_LINE_COUNT = 9 @@ -136,6 +138,46 @@ def _fit_series(series: Sequence[float], point_count: int) -> list[float]: return _expand_series(series, point_count) +def _visible_index_of_any(text: str, chars: str) -> int | None: + visible_index = 0 + index = 0 + while index < len(text): + if match := _ANSI_RE.match(text, index): + index = match.end() + continue + if text[index] in chars: + return visible_index + visible_index += 1 + index += 1 + return None + + +def _replace_visible_char(text: str, visible_index: int, replacement: str) -> str: + output: list[str] = [] + current_visible_index = 0 + index = 0 + replaced = False + while index < len(text): + if match := _ANSI_RE.match(text, index): + output.append(match.group()) + index = match.end() + continue + + if current_visible_index == visible_index: + output.append(replacement) + replaced = True + else: + output.append(text[index]) + + current_visible_index += 1 + index += 1 + + if not replaced and visible_index >= current_visible_index: + output.append(" " * (visible_index - current_visible_index)) + output.append(replacement) + return "".join(output) + + @dataclass class _BarState: label: str @@ -211,6 +253,13 @@ def output_token_rate(self, now: float) -> float: return self.output_tokens / elapsed if elapsed > 0 else 0.0 +@dataclass(frozen=True) +class _FeedbackMarker: + elapsed: float + value: float + event_kind: str + + class StickyProgressBar: """ANSI throughput chart panel that sticks to the bottom of the terminal. @@ -234,6 +283,7 @@ def __init__(self, stream: TextIO | None = None, *, panel_height: int = _DEFAULT self._is_tty = hasattr(self._stream, "isatty") and self._stream.isatty() self._bars: dict[str, _BarState] = {} self._model_usage: dict[str, _ModelUsageState] = {} + self._feedback_markers: list[_FeedbackMarker] = [] self._lock = Lock() self._drawn_lines = 0 self._active = False @@ -342,6 +392,21 @@ def record_model_usage( if self._active: self._redraw_if_due(now, force=force) + def record_feedback_signal(self, *, event_kind: str, force: bool = False) -> None: + with self._lock: + now = time.perf_counter() + self._feedback_markers.append( + _FeedbackMarker( + elapsed=max(now - self._start_time, 0.0), + value=max((bar.latest_rate for bar in self._bars.values()), default=0.0), + event_kind=_sanitize_label(event_kind), + ) + ) + if len(self._feedback_markers) > _MAX_FEEDBACK_MARKERS: + del self._feedback_markers[: len(self._feedback_markers) - _MAX_FEEDBACK_MARKERS] + if self._active: + self._redraw_if_due(now, force=force) + def remove_bar(self, key: str) -> None: with self._lock: self._bars.pop(key, None) @@ -420,7 +485,7 @@ def _format_panel(self) -> list[str]: now = time.perf_counter() bars = list(self._bars.values()) model_usage = list(self._model_usage.values()) - chart_lines = self._format_chart_lines(bars, inner_width, chart_height) + chart_lines = self._format_chart_lines(bars, inner_width, chart_height, now) legend_lines = self._format_legend_lines(bars, model_usage, now, minimum_legend_capacity, inner_width) lines = [ @@ -448,16 +513,23 @@ def _format_header(self, bars: list[_BarState], now: float) -> str: f"now {latest_rate:6.1f}{skipped_text} | {_RESET}{failed_text}" ) - def _format_chart_lines(self, bars: list[_BarState], inner_width: int, chart_height: int) -> list[str]: + def _format_chart_lines( + self, + bars: list[_BarState], + inner_width: int, + chart_height: int, + now: float, + ) -> list[str]: max_points = max(2, inner_width - _Y_AXIS_RESERVED) series = [_fit_series(_smooth_series(bar.rates), max_points) for bar in bars] max_rate = max((max(points) for points in series if points), default=0.0) + chart_max = max(1.0, max_rate) chart = asciichartpy.plot( series, { "height": chart_height, "min": 0.0, - "max": max(1.0, max_rate), + "max": chart_max, "format": _RATE_FORMAT, "colors": [_CURVE_COLORS[i % len(_CURVE_COLORS)] for i in range(len(series))], }, @@ -465,7 +537,43 @@ def _format_chart_lines(self, bars: list[_BarState], inner_width: int, chart_hei lines = chart.splitlines() while len(lines) < chart_height + 1: lines.append("") - return lines[: chart_height + 1] + return self._overlay_feedback_markers( + lines[: chart_height + 1], + current_elapsed=max(now - self._start_time, 0.001), + chart_max=chart_max, + point_count=max_points, + chart_height=chart_height, + ) + + def _overlay_feedback_markers( + self, + lines: list[str], + *, + current_elapsed: float, + chart_max: float, + point_count: int, + chart_height: int, + ) -> list[str]: + if not self._feedback_markers or not lines: + return lines + + marked_lines = list(lines) + plot_column_count = max(1, point_count - 1) + for marker in self._feedback_markers: + marker_elapsed = min(max(marker.elapsed, 0.0), current_elapsed) + x_index = int(round(marker_elapsed / current_elapsed * (plot_column_count - 1))) + y_value = min(max(marker.value, 0.0), chart_max) + row_index = int(round((chart_max - y_value) / chart_max * chart_height)) + row_index = min(max(row_index, 0), len(marked_lines) - 1) + axis_index = _visible_index_of_any(marked_lines[row_index], "┼┤") + if axis_index is None: + continue + marked_lines[row_index] = _replace_visible_char( + marked_lines[row_index], + axis_index + 1 + x_index, + _FEEDBACK_MARKER, + ) + return marked_lines def _format_legend_lines( self, diff --git a/packages/data-designer-engine/src/data_designer/engine/models/request_admission/controller.py b/packages/data-designer-engine/src/data_designer/engine/models/request_admission/controller.py index 38bbd0598..0cd3151af 100644 --- a/packages/data-designer-engine/src/data_designer/engine/models/request_admission/controller.py +++ b/packages/data-designer-engine/src/data_designer/engine/models/request_admission/controller.py @@ -32,6 +32,7 @@ from data_designer.engine.observability import ( RequestAdmissionEvent, RequestAdmissionEventSink, + emit_request_admission_event, runtime_correlation_provider, ) @@ -778,11 +779,10 @@ def _request_event_locked( ) def _emit_events(self, events: list[RequestAdmissionEvent]) -> None: - if self._event_sink is None: - return for event in events: - try: - self._event_sink.emit_request_event(event) - except Exception: - logger.warning("Request admission event sink raised; dropping event.", exc_info=True) - continue + if self._event_sink is not None: + try: + self._event_sink.emit_request_event(event) + except Exception: + logger.warning("Request admission event sink raised; dropping event.", exc_info=True) + emit_request_admission_event(event) diff --git a/packages/data-designer-engine/src/data_designer/engine/observability.py b/packages/data-designer-engine/src/data_designer/engine/observability.py index a7a28c41b..174a387b9 100644 --- a/packages/data-designer-engine/src/data_designer/engine/observability.py +++ b/packages/data-designer-engine/src/data_designer/engine/observability.py @@ -4,12 +4,17 @@ from __future__ import annotations import contextvars +import itertools +import logging import math import time from collections.abc import Mapping from dataclasses import dataclass, field, fields, is_dataclass from enum import Enum -from typing import Literal, Protocol +from threading import Lock +from typing import Callable, Literal, Protocol + +logger = logging.getLogger(__name__) @dataclass(frozen=True) @@ -215,6 +220,36 @@ class RequestAdmissionEventSink(Protocol): def emit_request_event(self, event: RequestAdmissionEvent) -> None: ... +RequestAdmissionEventCallback = Callable[[RequestAdmissionEvent], None] + +_request_event_callback_lock = Lock() +_request_event_callback_ids = itertools.count() +_request_event_callbacks: dict[int, RequestAdmissionEventCallback] = {} + + +def subscribe_request_admission_events(callback: RequestAdmissionEventCallback) -> Callable[[], None]: + callback_id = next(_request_event_callback_ids) + with _request_event_callback_lock: + _request_event_callbacks[callback_id] = callback + + def unsubscribe() -> None: + with _request_event_callback_lock: + _request_event_callbacks.pop(callback_id, None) + + return unsubscribe + + +def emit_request_admission_event(event: RequestAdmissionEvent) -> None: + with _request_event_callback_lock: + callbacks = tuple(_request_event_callbacks.values()) + + for callback in callbacks: + try: + callback(event) + except Exception: + logger.debug("Request admission event callback failed", exc_info=True) + + class InMemoryAdmissionEventSink: """Small sink used by tests, diagnostics, and benchmark smoke runs.""" diff --git a/packages/data-designer-engine/tests/engine/dataset_builders/utils/test_sticky_progress_bar.py b/packages/data-designer-engine/tests/engine/dataset_builders/utils/test_sticky_progress_bar.py index 45aacb226..83542c899 100644 --- a/packages/data-designer-engine/tests/engine/dataset_builders/utils/test_sticky_progress_bar.py +++ b/packages/data-designer-engine/tests/engine/dataset_builders/utils/test_sticky_progress_bar.py @@ -25,6 +25,7 @@ _fit_series, ) from data_designer.engine.models.usage_events import TokenUsageEvent, emit_token_usage_event +from data_designer.engine.observability import RequestAdmissionEvent, emit_request_admission_event CURSOR_UP_CLEAR = "\033[A\033[2K" HIDE_CURSOR = "\033[?25l" @@ -56,7 +57,8 @@ def _clean(text: str) -> str: def _last_panel_lines(output: str) -> list[str]: clean = _clean(output) - panel_start = clean.rfind("ā•­") + panel_start = clean.rfind("\nā•­") + panel_start = panel_start + 1 if panel_start >= 0 else clean.rfind("ā•­") assert panel_start >= 0 return clean[panel_start:].splitlines() @@ -66,6 +68,10 @@ def _chart_lines(panel_lines: list[str]) -> list[str]: return panel_lines[2:separator_index] +def _marker_positions(panel_lines: list[str]) -> list[tuple[int, int]]: + return [(row_index, line.index("ā—†")) for row_index, line in enumerate(_chart_lines(panel_lines)) if "ā—†" in line] + + def test_no_output_when_not_tty() -> None: stream = io.StringIO() with StickyProgressBar(stream=stream) as bar: @@ -177,6 +183,26 @@ def test_many_columns_and_models_do_not_shrink_chart(tty_stream: FakeTTY) -> Non assert f"model_{index}" in panel +def test_feedback_marker_reprojects_as_elapsed_time_grows(tty_stream: FakeTTY) -> None: + with StickyProgressBar(stream=tty_stream) as bar: + bar.add_bar("a", "column_a", 100) + state = bar._bars["a"] # noqa: SLF001 + state.rates = [0.0, 10.0, 20.0] + state.latest_rate = 12.0 + bar._start_time = time.perf_counter() - 10.0 # noqa: SLF001 + + bar.record_feedback_signal(event_kind="request_rate_limited", force=True) + before_positions = _marker_positions(_last_panel_lines(tty_stream.getvalue())) + assert before_positions + + bar._start_time = time.perf_counter() - 100.0 # noqa: SLF001 + bar.update("a", completed=20, success=20, force=True) + after_positions = _marker_positions(_last_panel_lines(tty_stream.getvalue())) + + assert after_positions + assert after_positions[0][1] < before_positions[0][1] + + def test_control_sequences_are_removed_from_labels(tty_stream: FakeTTY) -> None: with StickyProgressBar(stream=tty_stream) as bar: bar.add_bar("a", "col\x1b[31m_a\nsuffix", 100) @@ -355,3 +381,27 @@ def test_reporter_updates_and_logs_keep_drawn_lines_in_sync(tty_stream: FakeTTY) assert bar.drawn_lines == 22 finally: root_logger.removeHandler(handler) + + +def test_reporter_records_feedback_markers_from_request_events(tty_stream: FakeTTY) -> None: + trackers = {"col_a": ProgressTracker(total_records=100, label="column 'a'", quiet=True)} + + with StickyProgressBar(stream=tty_stream) as bar: + reporter = AsyncProgressReporter(trackers, report_interval=0.1, progress_bar=bar) + try: + emit_request_admission_event( + RequestAdmissionEvent.capture("request_rate_limited", sequence=1), + ) + assert len(bar._feedback_markers) == 1 # noqa: SLF001 + + emit_request_admission_event( + RequestAdmissionEvent.capture("request_wait_started", sequence=2), + ) + assert len(bar._feedback_markers) == 1 # noqa: SLF001 + finally: + reporter.close() + + emit_request_admission_event( + RequestAdmissionEvent.capture("request_rate_limited", sequence=3), + ) + assert len(bar._feedback_markers) == 1 # noqa: SLF001 diff --git a/packages/data-designer-engine/tests/engine/models/request_admission/test_controller.py b/packages/data-designer-engine/tests/engine/models/request_admission/test_controller.py index af77f8c40..bd0370704 100644 --- a/packages/data-designer-engine/tests/engine/models/request_admission/test_controller.py +++ b/packages/data-designer-engine/tests/engine/models/request_admission/test_controller.py @@ -25,7 +25,7 @@ RequestGroupSpec, RequestResourceKey, ) -from data_designer.engine.observability import InMemoryAdmissionEventSink +from data_designer.engine.observability import InMemoryAdmissionEventSink, subscribe_request_admission_events def _item(domain: RequestDomain = RequestDomain.CHAT, timeout: float | None = None) -> RequestAdmissionItem: @@ -141,6 +141,30 @@ def test_request_admission_rate_limit_decreases_and_sets_cooldown() -> None: assert snapshot.cooldown_remaining_seconds > 0 +def test_request_admission_broadcasts_global_feedback_events() -> None: + events = [] + unsubscribe = subscribe_request_admission_events(events.append) + try: + controller = _controller( + cap=4, + config=RequestAdmissionConfig( + multiplicative_decrease_factor=0.5, + cooldown_seconds=10, + ), + ) + item = _item() + lease = controller.try_acquire(item) + assert isinstance(lease, RequestAdmissionLease) + + controller.release(lease, RequestReleaseOutcome(kind="rate_limited")) + finally: + unsubscribe() + + event_kinds = {event.event_kind for event in events} + assert "request_rate_limited" in event_kinds + assert "request_limit_decreased" in event_kinds + + def test_request_admission_rate_limit_burst_decreases_once_per_cascade() -> None: controller = _controller( cap=8, From 0525f6ea0a955cf748cf71a127397ffe7d4fc49a Mon Sep 17 00:00:00 2001 From: "Eric W. Tramel" Date: Thu, 21 May 2026 15:03:22 -0400 Subject: [PATCH 13/19] chore: remove example scripts from pr --- examples/live_traffic_multi_model_demo.py | 104 ---------------------- examples/progress_panel_demo.py | 81 ----------------- 2 files changed, 185 deletions(-) delete mode 100644 examples/live_traffic_multi_model_demo.py delete mode 100644 examples/progress_panel_demo.py diff --git a/examples/live_traffic_multi_model_demo.py b/examples/live_traffic_multi_model_demo.py deleted file mode 100644 index f5ccbbec2..000000000 --- a/examples/live_traffic_multi_model_demo.py +++ /dev/null @@ -1,104 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -from __future__ import annotations - -import data_designer.config as dd - -PROVIDER = "nvidia-internal" - -MODEL_CONFIGS = [ - dd.ModelConfig( - alias="gpt-oss-20b", - model="nvidia/openai/gpt-oss-20b", - provider=PROVIDER, - inference_parameters=dd.ChatCompletionInferenceParams( - temperature=0.2, - top_p=0.8, - max_tokens=32, - max_parallel_requests=2, - ), - ), - dd.ModelConfig( - alias="nemotron-31b", - model="nvidia/nvidia/nemotron-nano-31b-v3", - provider=PROVIDER, - inference_parameters=dd.ChatCompletionInferenceParams( - temperature=0.2, - top_p=0.8, - max_tokens=32, - max_parallel_requests=2, - ), - ), - dd.ModelConfig( - alias="gpt-oss-120b", - model="nvcf/openai/gpt-oss-120b", - provider=PROVIDER, - inference_parameters=dd.ChatCompletionInferenceParams( - temperature=0.2, - top_p=0.8, - max_tokens=32, - max_parallel_requests=2, - ), - ), -] - -SYSTEM_PROMPT = ( - "You generate compact customer-support metadata. Answer in one short phrase and do not include markdown." -) - - -def load_config_builder() -> dd.DataDesignerConfigBuilder: - config_builder = dd.DataDesignerConfigBuilder(model_configs=MODEL_CONFIGS) - - config_builder.add_column( - dd.SamplerColumnConfig( - name="topic", - sampler_type=dd.SamplerType.CATEGORY, - params=dd.CategorySamplerParams( - values=[ - "GPU quota increase", - "Training job timeout", - "Invoice dispute", - "Model quality regression", - "Dataset permission issue", - "Deployment latency spike", - "Account recovery", - "Documentation clarification", - ], - ), - ) - ) - config_builder.add_column( - dd.SamplerColumnConfig( - name="customer_tone", - sampler_type=dd.SamplerType.CATEGORY, - params=dd.CategorySamplerParams(values=["calm", "urgent", "frustrated", "curious"]), - ) - ) - config_builder.add_column( - dd.LLMTextColumnConfig( - name="gpt_oss_20b_intent", - model_alias="gpt-oss-20b", - system_prompt=SYSTEM_PROMPT, - prompt=("Classify the likely support intent for a {{ customer_tone }} customer asking about: {{ topic }}."), - ) - ) - config_builder.add_column( - dd.LLMTextColumnConfig( - name="nemotron_31b_next_action", - model_alias="nemotron-31b", - system_prompt=SYSTEM_PROMPT, - prompt=("Suggest the first support action for a {{ customer_tone }} customer asking about: {{ topic }}."), - ) - ) - config_builder.add_column( - dd.LLMTextColumnConfig( - name="gpt_oss_120b_risk_signal", - model_alias="gpt-oss-120b", - system_prompt=SYSTEM_PROMPT, - prompt=("Name the main operational risk for a {{ customer_tone }} customer asking about: {{ topic }}."), - ) - ) - - return config_builder diff --git a/examples/progress_panel_demo.py b/examples/progress_panel_demo.py deleted file mode 100644 index cc9cc5a16..000000000 --- a/examples/progress_panel_demo.py +++ /dev/null @@ -1,81 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -from __future__ import annotations - -import threading -import time - -import data_designer.config as dd -from data_designer.engine.context import current_generation_column -from data_designer.engine.models.usage_events import TokenUsageEvent, emit_token_usage_event - -_WORKERS = threading.Semaphore(6) - - -def _simulate_work(row: dict, *, column: str, base_delay: float) -> str: - topic = str(row["topic"]) - jitter = (sum(ord(ch) for ch in topic + column) % 7) * 0.025 - input_tokens = 80 + (sum(ord(ch) for ch in topic) % 45) - output_tokens = 12 + (sum(ord(ch) for ch in column) % 16) - with _WORKERS: - for _ in range(4): - time.sleep((base_delay + jitter) / 4) - # This example is intentionally credential-free, so emit one synthetic - # model request per generated cell to exercise the model usage table. - emit_token_usage_event( - TokenUsageEvent( - model_alias="progress-panel-demo", - model_name="synthetic-token-stream", - input_tokens=input_tokens, - output_tokens=output_tokens, - column=current_generation_column.get() or column, - ) - ) - return f"{column}:{topic.lower().replace(' ', '-')}" - - -@dd.custom_column_generator(required_columns=["topic"]) -def intent_label(row: dict) -> dict: - row["intent_label"] = _simulate_work(row, column="intent", base_delay=0.12) - return row - - -@dd.custom_column_generator(required_columns=["topic"]) -def risk_signal(row: dict) -> dict: - row["risk_signal"] = _simulate_work(row, column="risk", base_delay=0.16) - return row - - -@dd.custom_column_generator(required_columns=["topic"]) -def routing_bucket(row: dict) -> dict: - row["routing_bucket"] = _simulate_work(row, column="route", base_delay=0.10) - return row - - -def load_config_builder() -> dd.DataDesignerConfigBuilder: - config_builder = dd.DataDesignerConfigBuilder() - - config_builder.add_column( - dd.SamplerColumnConfig( - name="topic", - sampler_type=dd.SamplerType.CATEGORY, - params=dd.CategorySamplerParams( - values=[ - "Account recovery", - "GPU capacity", - "Invoice dispute", - "Model quality", - "Security review", - "Dataset cleanup", - "Latency regression", - "Documentation gap", - ], - ), - ) - ) - config_builder.add_column(dd.CustomColumnConfig(name="intent_label", generator_function=intent_label)) - config_builder.add_column(dd.CustomColumnConfig(name="risk_signal", generator_function=risk_signal)) - config_builder.add_column(dd.CustomColumnConfig(name="routing_bucket", generator_function=routing_bucket)) - - return config_builder From 29db0634171506408cf35d5e89a58e4aeb6040a2 Mon Sep 17 00:00:00 2001 From: "Eric W. Tramel" Date: Thu, 21 May 2026 15:04:20 -0400 Subject: [PATCH 14/19] test: remove redundant progress bar default check --- packages/data-designer-config/tests/config/test_run_config.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/data-designer-config/tests/config/test_run_config.py b/packages/data-designer-config/tests/config/test_run_config.py index 62aae7319..9d216025c 100644 --- a/packages/data-designer-config/tests/config/test_run_config.py +++ b/packages/data-designer-config/tests/config/test_run_config.py @@ -19,10 +19,6 @@ def test_run_config_defaults_to_secure_jinja_renderer() -> None: assert JinjaRenderingEngine(RunConfig().jinja_rendering_engine) == JinjaRenderingEngine.SECURE -def test_run_config_defaults_to_progress_bar_enabled() -> None: - assert RunConfig().progress_bar is True - - def test_run_config_accepts_native_renderer() -> None: run_config = RunConfig(jinja_rendering_engine=JinjaRenderingEngine.NATIVE) assert JinjaRenderingEngine(run_config.jinja_rendering_engine) == JinjaRenderingEngine.NATIVE From be29f697968b91d3c52dc1b8ce9f765855dc3f01 Mon Sep 17 00:00:00 2001 From: "Eric W. Tramel" Date: Thu, 21 May 2026 15:29:49 -0400 Subject: [PATCH 15/19] refactor: organize engine progress visualization --- .../assets/progress-throughput-panel-demo.png | Bin 36291 -> 0 bytes .../dataset_builders/async_scheduler.py | 15 +-- .../dataset_builders/dataset_builder.py | 10 +- .../data_designer/engine/progress/__init__.py | 4 + .../reporter.py} | 25 +++- .../engine/progress/terminal/__init__.py | 4 + .../terminal/throughput_panel.py} | 10 +- .../tracker.py} | 4 +- .../terminal/test_throughput_panel.py} | 112 ++++++++++++++---- .../test_tracker.py} | 4 +- 10 files changed, 140 insertions(+), 48 deletions(-) delete mode 100644 docs/assets/progress-throughput-panel-demo.png create mode 100644 packages/data-designer-engine/src/data_designer/engine/progress/__init__.py rename packages/data-designer-engine/src/data_designer/engine/{dataset_builders/utils/async_progress_reporter.py => progress/reporter.py} (88%) create mode 100644 packages/data-designer-engine/src/data_designer/engine/progress/terminal/__init__.py rename packages/data-designer-engine/src/data_designer/engine/{dataset_builders/utils/sticky_progress_bar.py => progress/terminal/throughput_panel.py} (99%) rename packages/data-designer-engine/src/data_designer/engine/{dataset_builders/utils/progress_tracker.py => progress/tracker.py} (97%) rename packages/data-designer-engine/tests/engine/{dataset_builders/utils/test_sticky_progress_bar.py => progress/terminal/test_throughput_panel.py} (78%) rename packages/data-designer-engine/tests/engine/{dataset_builders/utils/test_progress_tracker.py => progress/test_tracker.py} (98%) diff --git a/docs/assets/progress-throughput-panel-demo.png b/docs/assets/progress-throughput-panel-demo.png deleted file mode 100644 index 297f99617c9d4e9d6236ac5cb476a99588504282..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36291 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 tuple[ProgressTracker, dict[str, Any]]: if generator.get_generation_strategy() != GenerationStrategy.CELL_BY_CELL: raise DatasetGenerationError( @@ -1417,7 +1417,7 @@ def _finalize_fan_out(self, progress_tracker: ProgressTracker) -> None: def _fan_out_with_async(self, generator: ColumnGeneratorWithModelRegistry, max_workers: int) -> None: if getattr(generator.config, "tool_alias", None): logger.info("šŸ› ļø Tool calling enabled") - bar = StickyProgressBar() if self._resource_provider.run_config.progress_bar else None + bar = TerminalThroughputPanel() if self._resource_provider.run_config.progress_bar else None can_skip = self._column_can_skip(generator.config.name) with bar or contextlib.nullcontext(): progress_tracker, executor_kwargs = self._setup_fan_out(generator, max_workers, progress_bar=bar) @@ -1441,7 +1441,7 @@ def _fan_out_with_async(self, generator: ColumnGeneratorWithModelRegistry, max_w def _fan_out_with_threads(self, generator: ColumnGeneratorWithModelRegistry, max_workers: int) -> None: if getattr(generator.config, "tool_alias", None): logger.info("šŸ› ļø Tool calling enabled") - bar = StickyProgressBar() if self._resource_provider.run_config.progress_bar else None + bar = TerminalThroughputPanel() if self._resource_provider.run_config.progress_bar else None can_skip = self._column_can_skip(generator.config.name) with bar or contextlib.nullcontext(): progress_tracker, executor_kwargs = self._setup_fan_out(generator, max_workers, progress_bar=bar) diff --git a/packages/data-designer-engine/src/data_designer/engine/progress/__init__.py b/packages/data-designer-engine/src/data_designer/engine/progress/__init__.py new file mode 100644 index 000000000..f1ea03ddb --- /dev/null +++ b/packages/data-designer-engine/src/data_designer/engine/progress/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations diff --git a/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/async_progress_reporter.py b/packages/data-designer-engine/src/data_designer/engine/progress/reporter.py similarity index 88% rename from packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/async_progress_reporter.py rename to packages/data-designer-engine/src/data_designer/engine/progress/reporter.py index 048d4d261..edb2f74a0 100644 --- a/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/async_progress_reporter.py +++ b/packages/data-designer-engine/src/data_designer/engine/progress/reporter.py @@ -8,13 +8,13 @@ from collections.abc import Callable from typing import TYPE_CHECKING -from data_designer.engine.dataset_builders.utils.progress_tracker import ProgressTracker from data_designer.engine.models.usage_events import TokenUsageEvent, subscribe_token_usage from data_designer.engine.observability import RequestAdmissionEvent, subscribe_request_admission_events +from data_designer.engine.progress.tracker import ProgressTracker from data_designer.logging import LOG_INDENT if TYPE_CHECKING: - from data_designer.engine.dataset_builders.utils.sticky_progress_bar import StickyProgressBar + from data_designer.engine.progress.terminal.throughput_panel import TerminalThroughputPanel logger = logging.getLogger(__name__) @@ -35,10 +35,12 @@ def __init__( trackers: dict[str, ProgressTracker], *, report_interval: float = DEFAULT_REPORT_INTERVAL, - progress_bar: StickyProgressBar | None = None, + progress_bar: TerminalThroughputPanel | None = None, + run_id: str | None = None, ) -> None: self._trackers = trackers self._report_interval = report_interval + self._run_id = run_id self._start_time = time.perf_counter() self._last_report_time: float = self._start_time self._last_bar_report_time: float = self._start_time @@ -133,7 +135,7 @@ def _update_bar(self, *, force: bool = False) -> None: self._bar.update_many(updates, force=force) def _record_token_usage(self, event: TokenUsageEvent) -> None: - if self._bar is not None: + if self._bar is not None and self._matches_run(event.correlation): self._bar.record_model_usage( model_alias=event.model_alias, model_name=event.model_name, @@ -142,9 +144,22 @@ def _record_token_usage(self, event: TokenUsageEvent) -> None: ) def _record_request_admission_event(self, event: RequestAdmissionEvent) -> None: - if self._bar is not None and event.event_kind in FEEDBACK_MARKER_EVENTS: + if ( + self._bar is not None + and event.event_kind in FEEDBACK_MARKER_EVENTS + and self._matches_run(event.captured_correlation) + ): self._bar.record_feedback_signal(event_kind=event.event_kind) + def _matches_run(self, correlation: object) -> bool: + if self._run_id is None: + return True + if correlation is None: + return False + if isinstance(correlation, dict): + return correlation.get("run_id") == self._run_id + return getattr(correlation, "run_id", None) == self._run_id + def _emit(self) -> None: current_total = sum(tracker.get_snapshot(0.0)[0] for tracker in self._trackers.values()) if current_total == self._last_reported_total: diff --git a/packages/data-designer-engine/src/data_designer/engine/progress/terminal/__init__.py b/packages/data-designer-engine/src/data_designer/engine/progress/terminal/__init__.py new file mode 100644 index 000000000..f1ea03ddb --- /dev/null +++ b/packages/data-designer-engine/src/data_designer/engine/progress/terminal/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations diff --git a/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/sticky_progress_bar.py b/packages/data-designer-engine/src/data_designer/engine/progress/terminal/throughput_panel.py similarity index 99% rename from packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/sticky_progress_bar.py rename to packages/data-designer-engine/src/data_designer/engine/progress/terminal/throughput_panel.py index 30a425952..aba5d8fd8 100644 --- a/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/sticky_progress_bar.py +++ b/packages/data-designer-engine/src/data_designer/engine/progress/terminal/throughput_panel.py @@ -260,17 +260,17 @@ class _FeedbackMarker: event_kind: str -class StickyProgressBar: +class TerminalThroughputPanel: """ANSI throughput chart panel that sticks to the bottom of the terminal. Log messages (via standard ``logging``) are rendered above the panel automatically. The panel redraws in-place after each update, tracks one - records-per-second curve per active generation column, and keeps a bounded - height so it does not take over the terminal. + records-per-second curve per active generation column, and gives the chart + a stable height while the column and model tables grow to fit their rows. Usage:: - with StickyProgressBar() as bar: + with TerminalThroughputPanel() as bar: bar.add_bar("col_a", "column 'a'", total=100) for i in range(100): bar.update("col_a", completed=i + 1, success=i + 1) @@ -302,7 +302,7 @@ def drawn_lines(self) -> int: # -- context manager -- - def __enter__(self) -> StickyProgressBar: + def __enter__(self) -> TerminalThroughputPanel: if self._is_tty and shutil.get_terminal_size().columns >= _MIN_TERMINAL_WIDTH: self._active = True self._start_time = time.perf_counter() diff --git a/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/progress_tracker.py b/packages/data-designer-engine/src/data_designer/engine/progress/tracker.py similarity index 97% rename from packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/progress_tracker.py rename to packages/data-designer-engine/src/data_designer/engine/progress/tracker.py index e9a3f691b..d8e1665d0 100644 --- a/packages/data-designer-engine/src/data_designer/engine/dataset_builders/utils/progress_tracker.py +++ b/packages/data-designer-engine/src/data_designer/engine/progress/tracker.py @@ -11,7 +11,7 @@ from data_designer.logging import LOG_INDENT, RandomEmoji if TYPE_CHECKING: - from data_designer.engine.dataset_builders.utils.sticky_progress_bar import StickyProgressBar + from data_designer.engine.progress.terminal.throughput_panel import TerminalThroughputPanel logger = logging.getLogger(__name__) @@ -42,7 +42,7 @@ def __init__( log_interval_percent: int = 10, *, quiet: bool = False, - progress_bar: StickyProgressBar | None = None, + progress_bar: TerminalThroughputPanel | None = None, progress_bar_key: str | None = None, ): self.total_records = total_records diff --git a/packages/data-designer-engine/tests/engine/dataset_builders/utils/test_sticky_progress_bar.py b/packages/data-designer-engine/tests/engine/progress/terminal/test_throughput_panel.py similarity index 78% rename from packages/data-designer-engine/tests/engine/dataset_builders/utils/test_sticky_progress_bar.py rename to packages/data-designer-engine/tests/engine/progress/terminal/test_throughput_panel.py index 83542c899..24e32a1ce 100644 --- a/packages/data-designer-engine/tests/engine/dataset_builders/utils/test_sticky_progress_bar.py +++ b/packages/data-designer-engine/tests/engine/progress/terminal/test_throughput_panel.py @@ -14,18 +14,22 @@ import pytest -from data_designer.engine.dataset_builders.utils.async_progress_reporter import AsyncProgressReporter -from data_designer.engine.dataset_builders.utils.progress_tracker import ProgressTracker -from data_designer.engine.dataset_builders.utils.sticky_progress_bar import ( +from data_designer.engine.models.usage_events import TokenUsageEvent, emit_token_usage_event +from data_designer.engine.observability import ( + RequestAdmissionEvent, + RuntimeCorrelation, + emit_request_admission_event, +) +from data_designer.engine.progress.reporter import AsyncProgressReporter +from data_designer.engine.progress.terminal.throughput_panel import ( _CHART_LINE_COUNT, _MAX_RATE_SAMPLES, _RATE_SAMPLE_INTERVAL_SECONDS, - StickyProgressBar, + TerminalThroughputPanel, _BarState, _fit_series, ) -from data_designer.engine.models.usage_events import TokenUsageEvent, emit_token_usage_event -from data_designer.engine.observability import RequestAdmissionEvent, emit_request_admission_event +from data_designer.engine.progress.tracker import ProgressTracker CURSOR_UP_CLEAR = "\033[A\033[2K" HIDE_CURSOR = "\033[?25l" @@ -34,7 +38,7 @@ class FakeTTY(io.StringIO): - """StringIO that reports itself as a TTY so StickyProgressBar activates.""" + """StringIO that reports itself as a TTY so TerminalThroughputPanel activates.""" def isatty(self) -> bool: return True @@ -55,6 +59,18 @@ def _clean(text: str) -> str: return _ALL_ANSI_RE.sub("", text).replace("\r", "") +def _correlation(run_id: str) -> RuntimeCorrelation: + return RuntimeCorrelation( + run_id=run_id, + row_group=0, + task_column="col_a", + task_type="cell", + scheduling_group_kind="model", + scheduling_group_identity_hash="hash", + task_execution_id="task-exec", + ) + + def _last_panel_lines(output: str) -> list[str]: clean = _clean(output) panel_start = clean.rfind("\nā•­") @@ -74,14 +90,14 @@ def _marker_positions(panel_lines: list[str]) -> list[tuple[int, int]]: def test_no_output_when_not_tty() -> None: stream = io.StringIO() - with StickyProgressBar(stream=stream) as bar: + with TerminalThroughputPanel(stream=stream) as bar: bar.add_bar("a", "col_a", 10) bar.update("a", completed=5, success=5) assert stream.getvalue() == "" def test_hides_and_shows_cursor(tty_stream: FakeTTY) -> None: - with StickyProgressBar(stream=tty_stream): + with TerminalThroughputPanel(stream=tty_stream): pass output = tty_stream.getvalue() assert output.startswith(HIDE_CURSOR) @@ -90,7 +106,7 @@ def test_hides_and_shows_cursor(tty_stream: FakeTTY) -> None: def test_tiny_terminal_falls_back_to_no_panel(tty_stream: FakeTTY) -> None: with patch.object(shutil, "get_terminal_size", return_value=os.terminal_size((20, 24))): - with StickyProgressBar(stream=tty_stream) as bar: + with TerminalThroughputPanel(stream=tty_stream) as bar: assert bar.is_active is False bar.add_bar("a", "col_a", 10) bar.update("a", completed=5, success=5, force=True) @@ -99,7 +115,7 @@ def test_tiny_terminal_falls_back_to_no_panel(tty_stream: FakeTTY) -> None: def test_renders_bounded_throughput_panel(tty_stream: FakeTTY) -> None: - with StickyProgressBar(stream=tty_stream) as bar: + with TerminalThroughputPanel(stream=tty_stream) as bar: bar.add_bar("a", "column 'a'", 100) bar.add_bar("b", "column 'b'", 100) bar.update_many({"a": (10, 10, 0, 0), "b": (20, 20, 0, 0)}, force=True) @@ -130,7 +146,7 @@ def test_renders_bounded_throughput_panel(tty_stream: FakeTTY) -> None: def test_model_usage_rates_render_in_separate_table(tty_stream: FakeTTY) -> None: - with StickyProgressBar(stream=tty_stream) as bar: + with TerminalThroughputPanel(stream=tty_stream) as bar: bar.add_bar("a", "column 'a'", 100) bar.update("a", completed=10, success=10, force=True) bar._start_time = time.perf_counter() - 10.0 # noqa: SLF001 @@ -156,7 +172,7 @@ def test_model_usage_rates_render_in_separate_table(tty_stream: FakeTTY) -> None def test_many_columns_and_models_do_not_shrink_chart(tty_stream: FakeTTY) -> None: - with StickyProgressBar(stream=tty_stream) as bar: + with TerminalThroughputPanel(stream=tty_stream) as bar: for index in range(8): bar.add_bar(f"col_{index}", f"column_{index}", 100) bar.update_many( @@ -184,7 +200,7 @@ def test_many_columns_and_models_do_not_shrink_chart(tty_stream: FakeTTY) -> Non def test_feedback_marker_reprojects_as_elapsed_time_grows(tty_stream: FakeTTY) -> None: - with StickyProgressBar(stream=tty_stream) as bar: + with TerminalThroughputPanel(stream=tty_stream) as bar: bar.add_bar("a", "column_a", 100) state = bar._bars["a"] # noqa: SLF001 state.rates = [0.0, 10.0, 20.0] @@ -204,7 +220,7 @@ def test_feedback_marker_reprojects_as_elapsed_time_grows(tty_stream: FakeTTY) - def test_control_sequences_are_removed_from_labels(tty_stream: FakeTTY) -> None: - with StickyProgressBar(stream=tty_stream) as bar: + with TerminalThroughputPanel(stream=tty_stream) as bar: bar.add_bar("a", "col\x1b[31m_a\nsuffix", 100) bar.update("a", completed=10, success=10, force=True) @@ -238,7 +254,7 @@ def test_sparse_rate_samples_span_chart_width() -> None: def test_frequent_updates_are_redraw_throttled(tty_stream: FakeTTY) -> None: - with StickyProgressBar(stream=tty_stream) as bar: + with TerminalThroughputPanel(stream=tty_stream) as bar: bar.add_bar("a", "col_a", 100) bar.add_bar("b", "col_b", 100) bar.update_many({"a": (1, 1, 0, 0), "b": (2, 2, 0, 0)}, force=True) @@ -261,7 +277,7 @@ def test_log_interleaving_preserves_panel_height(tty_stream: FakeTTY) -> None: root_logger.addHandler(handler) try: - with StickyProgressBar(stream=tty_stream) as bar: + with TerminalThroughputPanel(stream=tty_stream) as bar: bar.add_bar("x", "col_x", 100) bar.add_bar("y", "col_y", 100) @@ -280,7 +296,7 @@ def test_log_interleaving_preserves_panel_height(tty_stream: FakeTTY) -> None: def test_narrow_terminal_keeps_panel_within_width(tty_stream: FakeTTY) -> None: narrow = os.terminal_size((36, 24)) with patch.object(shutil, "get_terminal_size", return_value=narrow): - with StickyProgressBar(stream=tty_stream) as bar: + with TerminalThroughputPanel(stream=tty_stream) as bar: bar.add_bar("a", "column 'verification_1'", 300) bar.update("a", completed=50, success=50, force=True) @@ -290,7 +306,7 @@ def test_narrow_terminal_keeps_panel_within_width(tty_stream: FakeTTY) -> None: def test_update_many_single_redraw(tty_stream: FakeTTY) -> None: - with StickyProgressBar(stream=tty_stream) as bar: + with TerminalThroughputPanel(stream=tty_stream) as bar: bar.add_bar("a", "col_a", 100) bar.add_bar("b", "col_b", 100) before = tty_stream.getvalue() @@ -307,7 +323,7 @@ def test_update_many_single_redraw(tty_stream: FakeTTY) -> None: def test_update_many_includes_failures_and_skips(tty_stream: FakeTTY) -> None: - with StickyProgressBar(stream=tty_stream) as bar: + with TerminalThroughputPanel(stream=tty_stream) as bar: bar.add_bar("a", "col_a", 100) bar.update_many({"a": (10, 7, 2, 1), "unknown": (5, 5, 0, 0)}, force=True) @@ -319,7 +335,7 @@ def test_update_many_includes_failures_and_skips(tty_stream: FakeTTY) -> None: def test_remove_bar_redraws_panel(tty_stream: FakeTTY) -> None: - with StickyProgressBar(stream=tty_stream) as bar: + with TerminalThroughputPanel(stream=tty_stream) as bar: bar.add_bar("a", "col_a", 100) bar.add_bar("b", "col_b", 100) @@ -340,7 +356,7 @@ def test_reporter_updates_and_logs_keep_drawn_lines_in_sync(tty_stream: FakeTTY) root_logger.addHandler(handler) try: - bar = StickyProgressBar(stream=tty_stream) + bar = TerminalThroughputPanel(stream=tty_stream) trackers = { "col_a": ProgressTracker(total_records=100, label="column 'a'", quiet=True), "col_b": ProgressTracker(total_records=100, label="column 'b'", quiet=True), @@ -386,7 +402,7 @@ def test_reporter_updates_and_logs_keep_drawn_lines_in_sync(tty_stream: FakeTTY) def test_reporter_records_feedback_markers_from_request_events(tty_stream: FakeTTY) -> None: trackers = {"col_a": ProgressTracker(total_records=100, label="column 'a'", quiet=True)} - with StickyProgressBar(stream=tty_stream) as bar: + with TerminalThroughputPanel(stream=tty_stream) as bar: reporter = AsyncProgressReporter(trackers, report_interval=0.1, progress_bar=bar) try: emit_request_admission_event( @@ -405,3 +421,53 @@ def test_reporter_records_feedback_markers_from_request_events(tty_stream: FakeT RequestAdmissionEvent.capture("request_rate_limited", sequence=3), ) assert len(bar._feedback_markers) == 1 # noqa: SLF001 + + +def test_reporter_filters_global_events_by_run_id(tty_stream: FakeTTY) -> None: + trackers = {"col_a": ProgressTracker(total_records=100, label="column 'a'", quiet=True)} + + with TerminalThroughputPanel(stream=tty_stream) as bar: + reporter = AsyncProgressReporter(trackers, report_interval=0.1, progress_bar=bar, run_id="run-a") + try: + emit_token_usage_event( + TokenUsageEvent( + model_alias="other", + model_name="other-model", + input_tokens=100, + output_tokens=10, + correlation=_correlation("run-b"), + ) + ) + emit_token_usage_event( + TokenUsageEvent( + model_alias="uncorrelated", + model_name="uncorrelated-model", + input_tokens=100, + output_tokens=10, + ) + ) + assert not bar._model_usage # noqa: SLF001 + + emit_token_usage_event( + TokenUsageEvent( + model_alias="owned", + model_name="owned-model", + input_tokens=120, + output_tokens=30, + correlation=_correlation("run-a"), + ) + ) + assert set(bar._model_usage) == {"owned"} # noqa: SLF001 + + emit_request_admission_event( + RequestAdmissionEvent.capture("request_rate_limited", sequence=1, correlation=_correlation("run-b")) + ) + emit_request_admission_event(RequestAdmissionEvent.capture("request_rate_limited", sequence=2)) + assert not bar._feedback_markers # noqa: SLF001 + + emit_request_admission_event( + RequestAdmissionEvent.capture("request_rate_limited", sequence=3, correlation=_correlation("run-a")) + ) + assert len(bar._feedback_markers) == 1 # noqa: SLF001 + finally: + reporter.close() diff --git a/packages/data-designer-engine/tests/engine/dataset_builders/utils/test_progress_tracker.py b/packages/data-designer-engine/tests/engine/progress/test_tracker.py similarity index 98% rename from packages/data-designer-engine/tests/engine/dataset_builders/utils/test_progress_tracker.py rename to packages/data-designer-engine/tests/engine/progress/test_tracker.py index 13b698a22..47b45ca80 100644 --- a/packages/data-designer-engine/tests/engine/dataset_builders/utils/test_progress_tracker.py +++ b/packages/data-designer-engine/tests/engine/progress/test_tracker.py @@ -1,12 +1,14 @@ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import logging import threading import pytest -from data_designer.engine.dataset_builders.utils.progress_tracker import ProgressTracker +from data_designer.engine.progress.tracker import ProgressTracker @pytest.fixture From a24edaa06afe436015fe5eaea67b1ca92e51b203 Mon Sep 17 00:00:00 2001 From: "Eric W. Tramel" Date: Thu, 21 May 2026 21:36:58 -0400 Subject: [PATCH 16/19] feat: add create progress override flags --- .../src/data_designer/cli/commands/create.py | 9 +++++ .../cli/controllers/generation_controller.py | 5 +++ .../tests/cli/commands/test_create_command.py | 39 +++++++++++++++++++ .../controllers/test_generation_controller.py | 30 ++++++++++++++ packages/data-designer/tests/cli/test_main.py | 36 +++++++++++++++++ 5 files changed, 119 insertions(+) diff --git a/packages/data-designer/src/data_designer/cli/commands/create.py b/packages/data-designer/src/data_designer/cli/commands/create.py index 5a739c25a..86701cafa 100644 --- a/packages/data-designer/src/data_designer/cli/commands/create.py +++ b/packages/data-designer/src/data_designer/cli/commands/create.py @@ -61,6 +61,14 @@ def create_command( "The file is written to //.." ), ), + progress: bool | None = typer.Option( + None, + "--progress/--no-progress", + help=( + "Force the terminal progress panel on or off for this run. " + "When omitted, uses the configured RunConfig setting." + ), + ), ) -> None: """Create a full dataset and save results to disk. @@ -91,4 +99,5 @@ def create_command( artifact_path=artifact_path, resume=resume, output_format=output_format, + progress=progress, ) diff --git a/packages/data-designer/src/data_designer/cli/controllers/generation_controller.py b/packages/data-designer/src/data_designer/cli/controllers/generation_controller.py index 4a4231c41..bc679c93c 100644 --- a/packages/data-designer/src/data_designer/cli/controllers/generation_controller.py +++ b/packages/data-designer/src/data_designer/cli/controllers/generation_controller.py @@ -119,6 +119,7 @@ def run_create( artifact_path: str | None, resume: ResumeMode = ResumeMode.NEVER, output_format: str | None = None, + progress: bool | None = None, ) -> None: """Load config, create a full dataset, and save results to disk. @@ -130,6 +131,8 @@ def run_create( resume: Controls how interrupted runs are handled. output_format: If set, export the dataset to a single file in this format after generation. One of 'jsonl', 'csv', 'parquet'. + progress: If set, overrides the active RunConfig progress_bar setting for this + create invocation. """ config_builder = self._load_config(config_source) @@ -144,6 +147,8 @@ def run_create( try: data_designer = DataDesigner(artifact_path=resolved_artifact_path) + if progress is not None: + data_designer.set_run_config(data_designer.run_config.model_copy(update={"progress_bar": progress})) results = data_designer.create( config_builder, num_records=num_records, diff --git a/packages/data-designer/tests/cli/commands/test_create_command.py b/packages/data-designer/tests/cli/commands/test_create_command.py index 8b3335d4e..60851f082 100644 --- a/packages/data-designer/tests/cli/commands/test_create_command.py +++ b/packages/data-designer/tests/cli/commands/test_create_command.py @@ -26,6 +26,7 @@ def test_create_command_delegates_to_controller(mock_ctrl_cls: MagicMock) -> Non artifact_path=None, resume=ResumeMode.NEVER, output_format=None, + progress=None, ) mock_ctrl_cls.assert_called_once() @@ -36,6 +37,7 @@ def test_create_command_delegates_to_controller(mock_ctrl_cls: MagicMock) -> Non artifact_path=None, resume=ResumeMode.NEVER, output_format=None, + progress=None, ) @@ -52,6 +54,7 @@ def test_create_command_passes_custom_options(mock_ctrl_cls: MagicMock) -> None: artifact_path="/custom/output", resume=ResumeMode.NEVER, output_format=None, + progress=None, ) mock_ctrl.run_create.assert_called_once_with( @@ -61,6 +64,7 @@ def test_create_command_passes_custom_options(mock_ctrl_cls: MagicMock) -> None: artifact_path="/custom/output", resume=ResumeMode.NEVER, output_format=None, + progress=None, ) @@ -77,6 +81,7 @@ def test_create_command_default_artifact_path_is_none(mock_ctrl_cls: MagicMock) artifact_path=None, resume=ResumeMode.NEVER, output_format=None, + progress=None, ) mock_ctrl.run_create.assert_called_once_with( @@ -86,6 +91,7 @@ def test_create_command_default_artifact_path_is_none(mock_ctrl_cls: MagicMock) artifact_path=None, resume=ResumeMode.NEVER, output_format=None, + progress=None, ) @@ -102,6 +108,7 @@ def test_create_command_passes_resume_always(mock_ctrl_cls: MagicMock) -> None: artifact_path=None, resume=ResumeMode.ALWAYS, output_format=None, + progress=None, ) mock_ctrl.run_create.assert_called_once_with( @@ -111,6 +118,7 @@ def test_create_command_passes_resume_always(mock_ctrl_cls: MagicMock) -> None: artifact_path=None, resume=ResumeMode.ALWAYS, output_format=None, + progress=None, ) @@ -127,6 +135,7 @@ def test_create_command_passes_resume_if_possible(mock_ctrl_cls: MagicMock) -> N artifact_path=None, resume=ResumeMode.IF_POSSIBLE, output_format=None, + progress=None, ) mock_ctrl.run_create.assert_called_once_with( @@ -136,6 +145,7 @@ def test_create_command_passes_resume_if_possible(mock_ctrl_cls: MagicMock) -> N artifact_path=None, resume=ResumeMode.IF_POSSIBLE, output_format=None, + progress=None, ) @@ -152,6 +162,7 @@ def test_create_command_passes_output_format(mock_ctrl_cls: MagicMock) -> None: artifact_path=None, resume=ResumeMode.NEVER, output_format="jsonl", + progress=None, ) mock_ctrl.run_create.assert_called_once_with( @@ -161,4 +172,32 @@ def test_create_command_passes_output_format(mock_ctrl_cls: MagicMock) -> None: artifact_path=None, resume=ResumeMode.NEVER, output_format="jsonl", + progress=None, + ) + + +@patch("data_designer.cli.commands.create.GenerationController") +def test_create_command_passes_progress_override(mock_ctrl_cls: MagicMock) -> None: + """Test create_command forwards explicit progress override.""" + mock_ctrl = MagicMock() + mock_ctrl_cls.return_value = mock_ctrl + + create_command( + config_source="config.yaml", + num_records=10, + dataset_name="dataset", + artifact_path=None, + resume=ResumeMode.NEVER, + output_format=None, + progress=False, + ) + + mock_ctrl.run_create.assert_called_once_with( + config_source="config.yaml", + num_records=10, + dataset_name="dataset", + artifact_path=None, + resume=ResumeMode.NEVER, + output_format=None, + progress=False, ) diff --git a/packages/data-designer/tests/cli/controllers/test_generation_controller.py b/packages/data-designer/tests/cli/controllers/test_generation_controller.py index b8047641a..a73cd918c 100644 --- a/packages/data-designer/tests/cli/controllers/test_generation_controller.py +++ b/packages/data-designer/tests/cli/controllers/test_generation_controller.py @@ -13,6 +13,7 @@ from data_designer.cli.utils.config_loader import ConfigLoadError from data_designer.config.config_builder import DataDesignerConfigBuilder from data_designer.config.errors import InvalidConfigError +from data_designer.config.run_config import RunConfig from data_designer.config.utils.constants import DEFAULT_DISPLAY_WIDTH from data_designer.engine.storage.artifact_storage import ResumeMode @@ -711,6 +712,35 @@ def test_run_create_custom_options(mock_load_config: MagicMock, mock_dd_cls: Mag ) +@pytest.mark.parametrize("progress", [True, False]) +@patch(f"{_CTRL}.DataDesigner") +@patch(f"{_CTRL}.load_config_builder") +def test_run_create_applies_progress_override( + mock_load_config: MagicMock, mock_dd_cls: MagicMock, progress: bool +) -> None: + """run_create applies explicit --progress/--no-progress override to RunConfig.""" + mock_load_config.return_value = MagicMock(spec=DataDesignerConfigBuilder) + mock_dd = MagicMock() + mock_dd.run_config = RunConfig(progress_bar=not progress) + mock_dd_cls.return_value = mock_dd + mock_dd.create.return_value = _make_mock_create_results() + + controller = GenerationController() + controller.run_create( + config_source="config.yaml", + num_records=10, + dataset_name="dataset", + artifact_path=None, + progress=progress, + ) + + mock_dd.set_run_config.assert_called_once() + assert mock_dd.set_run_config.call_args.args[0].progress_bar is progress + mock_dd.create.assert_called_once_with( + mock_load_config.return_value, num_records=10, dataset_name="dataset", resume=ResumeMode.NEVER + ) + + @patch(f"{_CTRL}.load_config_builder") def test_run_create_config_load_error(mock_load_config: MagicMock) -> None: """Test create exits with code 1 when config fails to load.""" diff --git a/packages/data-designer/tests/cli/test_main.py b/packages/data-designer/tests/cli/test_main.py index 928e85159..d3baf88b4 100644 --- a/packages/data-designer/tests/cli/test_main.py +++ b/packages/data-designer/tests/cli/test_main.py @@ -177,6 +177,42 @@ def test_app_dispatches_lazy_create_command(mock_controller_cls: Mock) -> None: artifact_path=None, resume=ResumeMode.NEVER, output_format=None, + progress=None, + ) + + +@patch("data_designer.cli.commands.create.GenerationController") +def test_app_dispatches_create_progress_flags(mock_controller_cls: Mock) -> None: + """The create command parses --progress/--no-progress into the progress override.""" + mock_controller = Mock() + mock_controller_cls.return_value = mock_controller + + result = runner.invoke(app, ["create", "config.yaml", "--no-progress"]) + + assert result.exit_code == 0 + mock_controller.run_create.assert_called_once_with( + config_source="config.yaml", + num_records=DEFAULT_NUM_RECORDS, + dataset_name="dataset", + artifact_path=None, + resume=ResumeMode.NEVER, + output_format=None, + progress=False, + ) + + mock_controller.reset_mock() + + result = runner.invoke(app, ["create", "config.yaml", "--progress"]) + + assert result.exit_code == 0 + mock_controller.run_create.assert_called_once_with( + config_source="config.yaml", + num_records=DEFAULT_NUM_RECORDS, + dataset_name="dataset", + artifact_path=None, + resume=ResumeMode.NEVER, + output_format=None, + progress=True, ) From a867a66d589971f28446e1c34a9f6213c87876c9 Mon Sep 17 00:00:00 2001 From: "Eric W. Tramel" Date: Thu, 21 May 2026 21:41:59 -0400 Subject: [PATCH 17/19] fix: rename create progress flags to tui --- .../src/data_designer/cli/commands/create.py | 8 ++--- .../cli/controllers/generation_controller.py | 10 +++--- .../tests/cli/commands/test_create_command.py | 32 +++++++++---------- .../controllers/test_generation_controller.py | 14 ++++---- packages/data-designer/tests/cli/test_main.py | 14 ++++---- 5 files changed, 38 insertions(+), 40 deletions(-) diff --git a/packages/data-designer/src/data_designer/cli/commands/create.py b/packages/data-designer/src/data_designer/cli/commands/create.py index 86701cafa..f99f7ec22 100644 --- a/packages/data-designer/src/data_designer/cli/commands/create.py +++ b/packages/data-designer/src/data_designer/cli/commands/create.py @@ -61,11 +61,11 @@ def create_command( "The file is written to //.." ), ), - progress: bool | None = typer.Option( + tui: bool | None = typer.Option( None, - "--progress/--no-progress", + "--tui/--no-tui", help=( - "Force the terminal progress panel on or off for this run. " + "Force the terminal progress TUI on or off for this run. " "When omitted, uses the configured RunConfig setting." ), ), @@ -99,5 +99,5 @@ def create_command( artifact_path=artifact_path, resume=resume, output_format=output_format, - progress=progress, + tui=tui, ) diff --git a/packages/data-designer/src/data_designer/cli/controllers/generation_controller.py b/packages/data-designer/src/data_designer/cli/controllers/generation_controller.py index bc679c93c..1380ac705 100644 --- a/packages/data-designer/src/data_designer/cli/controllers/generation_controller.py +++ b/packages/data-designer/src/data_designer/cli/controllers/generation_controller.py @@ -119,7 +119,7 @@ def run_create( artifact_path: str | None, resume: ResumeMode = ResumeMode.NEVER, output_format: str | None = None, - progress: bool | None = None, + tui: bool | None = None, ) -> None: """Load config, create a full dataset, and save results to disk. @@ -131,8 +131,8 @@ def run_create( resume: Controls how interrupted runs are handled. output_format: If set, export the dataset to a single file in this format after generation. One of 'jsonl', 'csv', 'parquet'. - progress: If set, overrides the active RunConfig progress_bar setting for this - create invocation. + tui: If set, overrides the active RunConfig progress_bar setting for this + create invocation's terminal UI. """ config_builder = self._load_config(config_source) @@ -147,8 +147,8 @@ def run_create( try: data_designer = DataDesigner(artifact_path=resolved_artifact_path) - if progress is not None: - data_designer.set_run_config(data_designer.run_config.model_copy(update={"progress_bar": progress})) + if tui is not None: + data_designer.set_run_config(data_designer.run_config.model_copy(update={"progress_bar": tui})) results = data_designer.create( config_builder, num_records=num_records, diff --git a/packages/data-designer/tests/cli/commands/test_create_command.py b/packages/data-designer/tests/cli/commands/test_create_command.py index 60851f082..40e9fa63e 100644 --- a/packages/data-designer/tests/cli/commands/test_create_command.py +++ b/packages/data-designer/tests/cli/commands/test_create_command.py @@ -26,7 +26,7 @@ def test_create_command_delegates_to_controller(mock_ctrl_cls: MagicMock) -> Non artifact_path=None, resume=ResumeMode.NEVER, output_format=None, - progress=None, + tui=None, ) mock_ctrl_cls.assert_called_once() @@ -37,7 +37,7 @@ def test_create_command_delegates_to_controller(mock_ctrl_cls: MagicMock) -> Non artifact_path=None, resume=ResumeMode.NEVER, output_format=None, - progress=None, + tui=None, ) @@ -54,7 +54,7 @@ def test_create_command_passes_custom_options(mock_ctrl_cls: MagicMock) -> None: artifact_path="/custom/output", resume=ResumeMode.NEVER, output_format=None, - progress=None, + tui=None, ) mock_ctrl.run_create.assert_called_once_with( @@ -64,7 +64,7 @@ def test_create_command_passes_custom_options(mock_ctrl_cls: MagicMock) -> None: artifact_path="/custom/output", resume=ResumeMode.NEVER, output_format=None, - progress=None, + tui=None, ) @@ -81,7 +81,7 @@ def test_create_command_default_artifact_path_is_none(mock_ctrl_cls: MagicMock) artifact_path=None, resume=ResumeMode.NEVER, output_format=None, - progress=None, + tui=None, ) mock_ctrl.run_create.assert_called_once_with( @@ -91,7 +91,7 @@ def test_create_command_default_artifact_path_is_none(mock_ctrl_cls: MagicMock) artifact_path=None, resume=ResumeMode.NEVER, output_format=None, - progress=None, + tui=None, ) @@ -108,7 +108,7 @@ def test_create_command_passes_resume_always(mock_ctrl_cls: MagicMock) -> None: artifact_path=None, resume=ResumeMode.ALWAYS, output_format=None, - progress=None, + tui=None, ) mock_ctrl.run_create.assert_called_once_with( @@ -118,7 +118,7 @@ def test_create_command_passes_resume_always(mock_ctrl_cls: MagicMock) -> None: artifact_path=None, resume=ResumeMode.ALWAYS, output_format=None, - progress=None, + tui=None, ) @@ -135,7 +135,7 @@ def test_create_command_passes_resume_if_possible(mock_ctrl_cls: MagicMock) -> N artifact_path=None, resume=ResumeMode.IF_POSSIBLE, output_format=None, - progress=None, + tui=None, ) mock_ctrl.run_create.assert_called_once_with( @@ -145,7 +145,7 @@ def test_create_command_passes_resume_if_possible(mock_ctrl_cls: MagicMock) -> N artifact_path=None, resume=ResumeMode.IF_POSSIBLE, output_format=None, - progress=None, + tui=None, ) @@ -162,7 +162,7 @@ def test_create_command_passes_output_format(mock_ctrl_cls: MagicMock) -> None: artifact_path=None, resume=ResumeMode.NEVER, output_format="jsonl", - progress=None, + tui=None, ) mock_ctrl.run_create.assert_called_once_with( @@ -172,13 +172,13 @@ def test_create_command_passes_output_format(mock_ctrl_cls: MagicMock) -> None: artifact_path=None, resume=ResumeMode.NEVER, output_format="jsonl", - progress=None, + tui=None, ) @patch("data_designer.cli.commands.create.GenerationController") -def test_create_command_passes_progress_override(mock_ctrl_cls: MagicMock) -> None: - """Test create_command forwards explicit progress override.""" +def test_create_command_passes_tui_override(mock_ctrl_cls: MagicMock) -> None: + """Test create_command forwards explicit TUI override.""" mock_ctrl = MagicMock() mock_ctrl_cls.return_value = mock_ctrl @@ -189,7 +189,7 @@ def test_create_command_passes_progress_override(mock_ctrl_cls: MagicMock) -> No artifact_path=None, resume=ResumeMode.NEVER, output_format=None, - progress=False, + tui=False, ) mock_ctrl.run_create.assert_called_once_with( @@ -199,5 +199,5 @@ def test_create_command_passes_progress_override(mock_ctrl_cls: MagicMock) -> No artifact_path=None, resume=ResumeMode.NEVER, output_format=None, - progress=False, + tui=False, ) diff --git a/packages/data-designer/tests/cli/controllers/test_generation_controller.py b/packages/data-designer/tests/cli/controllers/test_generation_controller.py index a73cd918c..415053dae 100644 --- a/packages/data-designer/tests/cli/controllers/test_generation_controller.py +++ b/packages/data-designer/tests/cli/controllers/test_generation_controller.py @@ -712,16 +712,14 @@ def test_run_create_custom_options(mock_load_config: MagicMock, mock_dd_cls: Mag ) -@pytest.mark.parametrize("progress", [True, False]) +@pytest.mark.parametrize("tui", [True, False]) @patch(f"{_CTRL}.DataDesigner") @patch(f"{_CTRL}.load_config_builder") -def test_run_create_applies_progress_override( - mock_load_config: MagicMock, mock_dd_cls: MagicMock, progress: bool -) -> None: - """run_create applies explicit --progress/--no-progress override to RunConfig.""" +def test_run_create_applies_tui_override(mock_load_config: MagicMock, mock_dd_cls: MagicMock, tui: bool) -> None: + """run_create applies explicit --tui/--no-tui override to RunConfig.""" mock_load_config.return_value = MagicMock(spec=DataDesignerConfigBuilder) mock_dd = MagicMock() - mock_dd.run_config = RunConfig(progress_bar=not progress) + mock_dd.run_config = RunConfig(progress_bar=not tui) mock_dd_cls.return_value = mock_dd mock_dd.create.return_value = _make_mock_create_results() @@ -731,11 +729,11 @@ def test_run_create_applies_progress_override( num_records=10, dataset_name="dataset", artifact_path=None, - progress=progress, + tui=tui, ) mock_dd.set_run_config.assert_called_once() - assert mock_dd.set_run_config.call_args.args[0].progress_bar is progress + assert mock_dd.set_run_config.call_args.args[0].progress_bar is tui mock_dd.create.assert_called_once_with( mock_load_config.return_value, num_records=10, dataset_name="dataset", resume=ResumeMode.NEVER ) diff --git a/packages/data-designer/tests/cli/test_main.py b/packages/data-designer/tests/cli/test_main.py index d3baf88b4..9e11a656c 100644 --- a/packages/data-designer/tests/cli/test_main.py +++ b/packages/data-designer/tests/cli/test_main.py @@ -177,17 +177,17 @@ def test_app_dispatches_lazy_create_command(mock_controller_cls: Mock) -> None: artifact_path=None, resume=ResumeMode.NEVER, output_format=None, - progress=None, + tui=None, ) @patch("data_designer.cli.commands.create.GenerationController") -def test_app_dispatches_create_progress_flags(mock_controller_cls: Mock) -> None: - """The create command parses --progress/--no-progress into the progress override.""" +def test_app_dispatches_create_tui_flags(mock_controller_cls: Mock) -> None: + """The create command parses --tui/--no-tui into the TUI override.""" mock_controller = Mock() mock_controller_cls.return_value = mock_controller - result = runner.invoke(app, ["create", "config.yaml", "--no-progress"]) + result = runner.invoke(app, ["create", "config.yaml", "--no-tui"]) assert result.exit_code == 0 mock_controller.run_create.assert_called_once_with( @@ -197,12 +197,12 @@ def test_app_dispatches_create_progress_flags(mock_controller_cls: Mock) -> None artifact_path=None, resume=ResumeMode.NEVER, output_format=None, - progress=False, + tui=False, ) mock_controller.reset_mock() - result = runner.invoke(app, ["create", "config.yaml", "--progress"]) + result = runner.invoke(app, ["create", "config.yaml", "--tui"]) assert result.exit_code == 0 mock_controller.run_create.assert_called_once_with( @@ -212,7 +212,7 @@ def test_app_dispatches_create_progress_flags(mock_controller_cls: Mock) -> None artifact_path=None, resume=ResumeMode.NEVER, output_format=None, - progress=True, + tui=True, ) From 7a539c0e3da932ad1100420002301699ccf05014 Mon Sep 17 00:00:00 2001 From: "Eric W. Tramel" Date: Thu, 21 May 2026 21:51:40 -0400 Subject: [PATCH 18/19] fix: make display_tui the canonical run config flag --- .../src/data_designer/config/run_config.py | 48 +++++++++++++++---- .../tests/config/test_run_config.py | 31 ++++++++++++ .../dataset_builders/async_scheduler.py | 4 +- .../dataset_builders/dataset_builder.py | 6 +-- .../test_async_builder_integration.py | 2 +- .../cli/controllers/generation_controller.py | 4 +- .../controllers/test_generation_controller.py | 4 +- 7 files changed, 81 insertions(+), 18 deletions(-) diff --git a/packages/data-designer-config/src/data_designer/config/run_config.py b/packages/data-designer-config/src/data_designer/config/run_config.py index fd096ca38..33a86c41e 100644 --- a/packages/data-designer-config/src/data_designer/config/run_config.py +++ b/packages/data-designer-config/src/data_designer/config/run_config.py @@ -24,6 +24,7 @@ class JinjaRenderingEngine(StrEnum): "RunConfig.throttle and ThrottleConfig are deprecated. Use RunConfig.request_admission with " "RequestAdmissionTuningConfig for supported advanced request-admission tuning." ) +_PROGRESS_BAR_DEPRECATION_MESSAGE = "RunConfig.progress_bar is deprecated. Use RunConfig.display_tui instead." class RequestAdmissionTuningConfig(ConfigBase): @@ -142,9 +143,9 @@ class RunConfig(ConfigBase): Default is 0. async_trace: If True, collect per-task tracing data when using the async engine (DATA_DESIGNER_ASYNC_ENGINE=1). Has no effect on the sync path. Default is False. - progress_bar: If True, display a sticky ANSI throughput chart panel instead of periodic - log lines during generation. Requires a TTY; falls back to log lines in non-TTY - environments. Default is True. + display_tui: If True, display the terminal throughput TUI instead of periodic + log lines during generation. Requires a TTY; falls back to log lines in + non-TTY environments. Default is True. progress_interval: How often (in seconds) the async progress reporter emits a consolidated log block. Must be > 0. Default is 5.0. jinja_rendering_engine: Template renderer used for engine-side Jinja evaluation. @@ -169,7 +170,7 @@ class RunConfig(ConfigBase): max_conversation_restarts: int = Field(default=5, ge=0) max_conversation_correction_steps: int = Field(default=0, ge=0) async_trace: bool = False - progress_bar: bool = True + display_tui: bool = True progress_interval: float = Field(default=5.0, gt=0.0) jinja_rendering_engine: JinjaRenderingEngine = Field( default=JinjaRenderingEngine.SECURE, @@ -182,9 +183,22 @@ class RunConfig(ConfigBase): @model_validator(mode="before") @classmethod - def translate_deprecated_throttle_config(cls, data: Any) -> Any: - if isinstance(data, dict) and "throttle" in data: - normalized = dict(data) + def translate_deprecated_fields(cls, data: Any) -> Any: + if not isinstance(data, dict): + return data + + normalized = dict(data) + + if "progress_bar" in normalized: + progress_bar = normalized.pop("progress_bar") + normalized.setdefault("display_tui", progress_bar) + warnings.warn( + _PROGRESS_BAR_DEPRECATION_MESSAGE, + DeprecationWarning, + stacklevel=2, + ) + + if "throttle" in normalized: throttle = normalized.pop("throttle") if normalized.get("request_admission") is not None: raise ValueError( @@ -202,7 +216,25 @@ def translate_deprecated_throttle_config(cls, data: Any) -> Any: stacklevel=2, ) return normalized - return data + return normalized + + @property + def progress_bar(self) -> bool: + warnings.warn( + _PROGRESS_BAR_DEPRECATION_MESSAGE, + DeprecationWarning, + stacklevel=2, + ) + return self.display_tui + + @progress_bar.setter + def progress_bar(self, value: bool) -> None: + warnings.warn( + _PROGRESS_BAR_DEPRECATION_MESSAGE, + DeprecationWarning, + stacklevel=2, + ) + self.display_tui = value @model_validator(mode="after") def normalize_shutdown_settings(self) -> Self: diff --git a/packages/data-designer-config/tests/config/test_run_config.py b/packages/data-designer-config/tests/config/test_run_config.py index 9d216025c..ffcc24be7 100644 --- a/packages/data-designer-config/tests/config/test_run_config.py +++ b/packages/data-designer-config/tests/config/test_run_config.py @@ -24,6 +24,37 @@ def test_run_config_accepts_native_renderer() -> None: assert JinjaRenderingEngine(run_config.jinja_rendering_engine) == JinjaRenderingEngine.NATIVE +def test_run_config_defaults_to_display_tui_enabled() -> None: + assert RunConfig().display_tui is True + + +def test_run_config_accepts_display_tui() -> None: + assert RunConfig(display_tui=False).display_tui is False + + +def test_run_config_progress_bar_shim_translates_to_display_tui() -> None: + with pytest.warns(DeprecationWarning, match="RunConfig.progress_bar.*RunConfig.display_tui"): + run_config = RunConfig(progress_bar=False) + + assert run_config.display_tui is False + + +def test_run_config_progress_bar_property_getter_warns() -> None: + run_config = RunConfig(display_tui=False) + + with pytest.warns(DeprecationWarning, match="RunConfig.progress_bar.*RunConfig.display_tui"): + assert run_config.progress_bar is False + + +def test_run_config_progress_bar_property_setter_warns() -> None: + run_config = RunConfig(display_tui=False) + + with pytest.warns(DeprecationWarning, match="RunConfig.progress_bar.*RunConfig.display_tui"): + run_config.progress_bar = True + + assert run_config.display_tui is True + + def test_run_config_throttle_shim_rejects_unknown_legacy_fields() -> None: with pytest.raises(ValidationError, match="max_concurrent_requests"): RunConfig(throttle={"max_concurrent_requests": 1}) diff --git a/packages/data-designer-engine/src/data_designer/engine/dataset_builders/async_scheduler.py b/packages/data-designer-engine/src/data_designer/engine/dataset_builders/async_scheduler.py index 0e6c2f900..8623e22cd 100644 --- a/packages/data-designer-engine/src/data_designer/engine/dataset_builders/async_scheduler.py +++ b/packages/data-designer-engine/src/data_designer/engine/dataset_builders/async_scheduler.py @@ -162,7 +162,7 @@ def __init__( num_records: int = 0, buffer_size: int = 0, progress_interval: float | None = None, - progress_bar: bool = False, + display_tui: bool = False, scheduler_event_sink: SchedulerAdmissionEventSink | None = None, run_id: str | None = None, adaptive_row_group_admission: bool = False, @@ -311,7 +311,7 @@ def __init__( self._seed_cols: tuple[str, ...] = tuple(c for c in graph.columns if not graph.get_upstream_columns(c)) # Per-column progress tracking (cell-by-cell only; full-column tasks are instant) - self._progress_bar = TerminalThroughputPanel() if progress_bar else None + self._progress_bar = TerminalThroughputPanel() if display_tui else None self._reporter = self._setup_async_progress_reporter(num_records, buffer_size, progress_interval) def _setup_async_progress_reporter( diff --git a/packages/data-designer-engine/src/data_designer/engine/dataset_builders/dataset_builder.py b/packages/data-designer-engine/src/data_designer/engine/dataset_builders/dataset_builder.py index 7bff2b3f3..216231c54 100644 --- a/packages/data-designer-engine/src/data_designer/engine/dataset_builders/dataset_builder.py +++ b/packages/data-designer-engine/src/data_designer/engine/dataset_builders/dataset_builder.py @@ -1066,7 +1066,7 @@ def on_before_checkpoint(rg_id: int, rg_size: int) -> None: num_records=num_records, buffer_size=buffer_size, progress_interval=self._resource_provider.run_config.progress_interval, - progress_bar=self._resource_provider.run_config.progress_bar, + display_tui=self._resource_provider.run_config.display_tui, request_pressure_provider=self._resource_provider.model_registry.request_admission, request_pressure_advisory=True, ) @@ -1417,7 +1417,7 @@ def _finalize_fan_out(self, progress_tracker: ProgressTracker) -> None: def _fan_out_with_async(self, generator: ColumnGeneratorWithModelRegistry, max_workers: int) -> None: if getattr(generator.config, "tool_alias", None): logger.info("šŸ› ļø Tool calling enabled") - bar = TerminalThroughputPanel() if self._resource_provider.run_config.progress_bar else None + bar = TerminalThroughputPanel() if self._resource_provider.run_config.display_tui else None can_skip = self._column_can_skip(generator.config.name) with bar or contextlib.nullcontext(): progress_tracker, executor_kwargs = self._setup_fan_out(generator, max_workers, progress_bar=bar) @@ -1441,7 +1441,7 @@ def _fan_out_with_async(self, generator: ColumnGeneratorWithModelRegistry, max_w def _fan_out_with_threads(self, generator: ColumnGeneratorWithModelRegistry, max_workers: int) -> None: if getattr(generator.config, "tool_alias", None): logger.info("šŸ› ļø Tool calling enabled") - bar = TerminalThroughputPanel() if self._resource_provider.run_config.progress_bar else None + bar = TerminalThroughputPanel() if self._resource_provider.run_config.display_tui else None can_skip = self._column_can_skip(generator.config.name) with bar or contextlib.nullcontext(): progress_tracker, executor_kwargs = self._setup_fan_out(generator, max_workers, progress_bar=bar) diff --git a/packages/data-designer-engine/tests/engine/dataset_builders/test_async_builder_integration.py b/packages/data-designer-engine/tests/engine/dataset_builders/test_async_builder_integration.py index f01dc1d91..3d0e4f79f 100644 --- a/packages/data-designer-engine/tests/engine/dataset_builders/test_async_builder_integration.py +++ b/packages/data-designer-engine/tests/engine/dataset_builders/test_async_builder_integration.py @@ -205,7 +205,7 @@ def __init__(self, **kwargs: object) -> None: model_registry.request_admission = request_admission provider = SimpleNamespace( model_registry=model_registry, - run_config=SimpleNamespace(progress_interval=5.0, progress_bar=False), + run_config=SimpleNamespace(progress_interval=5.0, display_tui=False), ) processor_runner = MagicMock() processor_runner.has_processors_for.return_value = False diff --git a/packages/data-designer/src/data_designer/cli/controllers/generation_controller.py b/packages/data-designer/src/data_designer/cli/controllers/generation_controller.py index 1380ac705..09f0853d5 100644 --- a/packages/data-designer/src/data_designer/cli/controllers/generation_controller.py +++ b/packages/data-designer/src/data_designer/cli/controllers/generation_controller.py @@ -131,7 +131,7 @@ def run_create( resume: Controls how interrupted runs are handled. output_format: If set, export the dataset to a single file in this format after generation. One of 'jsonl', 'csv', 'parquet'. - tui: If set, overrides the active RunConfig progress_bar setting for this + tui: If set, overrides the active RunConfig display_tui setting for this create invocation's terminal UI. """ config_builder = self._load_config(config_source) @@ -148,7 +148,7 @@ def run_create( try: data_designer = DataDesigner(artifact_path=resolved_artifact_path) if tui is not None: - data_designer.set_run_config(data_designer.run_config.model_copy(update={"progress_bar": tui})) + data_designer.set_run_config(data_designer.run_config.model_copy(update={"display_tui": tui})) results = data_designer.create( config_builder, num_records=num_records, diff --git a/packages/data-designer/tests/cli/controllers/test_generation_controller.py b/packages/data-designer/tests/cli/controllers/test_generation_controller.py index 415053dae..eef3a34fa 100644 --- a/packages/data-designer/tests/cli/controllers/test_generation_controller.py +++ b/packages/data-designer/tests/cli/controllers/test_generation_controller.py @@ -719,7 +719,7 @@ def test_run_create_applies_tui_override(mock_load_config: MagicMock, mock_dd_cl """run_create applies explicit --tui/--no-tui override to RunConfig.""" mock_load_config.return_value = MagicMock(spec=DataDesignerConfigBuilder) mock_dd = MagicMock() - mock_dd.run_config = RunConfig(progress_bar=not tui) + mock_dd.run_config = RunConfig(display_tui=not tui) mock_dd_cls.return_value = mock_dd mock_dd.create.return_value = _make_mock_create_results() @@ -733,7 +733,7 @@ def test_run_create_applies_tui_override(mock_load_config: MagicMock, mock_dd_cl ) mock_dd.set_run_config.assert_called_once() - assert mock_dd.set_run_config.call_args.args[0].progress_bar is tui + assert mock_dd.set_run_config.call_args.args[0].display_tui is tui mock_dd.create.assert_called_once_with( mock_load_config.return_value, num_records=10, dataset_name="dataset", resume=ResumeMode.NEVER ) From 1de5cc4aa1fd45851d579392bd350f6cccf4acb5 Mon Sep 17 00:00:00 2001 From: "Eric W. Tramel" Date: Thu, 21 May 2026 22:07:05 -0400 Subject: [PATCH 19/19] test: make throughput redraw assertion logger-level independent --- .../engine/progress/terminal/test_throughput_panel.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/data-designer-engine/tests/engine/progress/terminal/test_throughput_panel.py b/packages/data-designer-engine/tests/engine/progress/terminal/test_throughput_panel.py index 24e32a1ce..0fe574295 100644 --- a/packages/data-designer-engine/tests/engine/progress/terminal/test_throughput_panel.py +++ b/packages/data-designer-engine/tests/engine/progress/terminal/test_throughput_panel.py @@ -351,6 +351,8 @@ def test_remove_bar_redraws_panel(tty_stream: FakeTTY) -> None: def test_reporter_updates_and_logs_keep_drawn_lines_in_sync(tty_stream: FakeTTY) -> None: root_logger = logging.getLogger() + old_level = root_logger.level + root_logger.setLevel(logging.INFO) handler = logging.StreamHandler(tty_stream) handler.setFormatter(logging.Formatter("%(message)s")) root_logger.addHandler(handler) @@ -393,10 +395,13 @@ def test_reporter_updates_and_logs_keep_drawn_lines_in_sync(tty_stream: FakeTTY) snapshot = tty_stream.getvalue() reporter.log_final() - assert tty_stream.getvalue()[len(snapshot) :].count(CURSOR_UP_CLEAR) == 22 assert bar.drawn_lines == 22 + clear_count = tty_stream.getvalue()[len(snapshot) :].count(CURSOR_UP_CLEAR) + assert clear_count >= bar.drawn_lines + assert clear_count % bar.drawn_lines == 0 finally: root_logger.removeHandler(handler) + root_logger.setLevel(old_level) def test_reporter_records_feedback_markers_from_request_events(tty_stream: FakeTTY) -> None: