From 40ec291707afa72ca8d9da5934e13b441b3ae71b Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 30 Jan 2026 22:53:18 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20Add=20progress=20bar?= =?UTF-8?q?=20for=20fetching=20existing=20rules?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added progress bar to `get_all_existing_rules` for better user feedback during parallel fetch. - Removed duplicate `render_progress_bar` definition in `main.py`. - Added test case `test_get_all_existing_rules_shows_progress`. - Created `.jules/palette.md` for journal entries. Co-authored-by: abhimehro <84992105+abhimehro@users.noreply.github.com> --- .jules/palette.md | 9 +++++++++ main.py | 37 ++++++++++++++++++------------------- test_main.py | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 19 deletions(-) create mode 100644 .jules/palette.md diff --git a/.jules/palette.md b/.jules/palette.md new file mode 100644 index 00000000..055b5d93 --- /dev/null +++ b/.jules/palette.md @@ -0,0 +1,9 @@ +## PALETTE'S JOURNAL - CRITICAL LEARNINGS ONLY + +This journal is for recording critical UX/accessibility learnings. + +--- + +## 2024-05-23 - CLI Progress Bars in Parallel Operations +**Learning:** Adding visual feedback (progress bars) to parallel operations (like `ThreadPoolExecutor`) requires careful management of `stderr`. Standard logging (`logging.warning`) can interfere with `\r` carriage returns used for progress bars. +**Action:** Always clear the line (`\r\033[K`) before logging warnings inside a progress-tracked loop, and redraw the progress bar afterwards if necessary. diff --git a/main.py b/main.py index 86792da4..f4f594db 100644 --- a/main.py +++ b/main.py @@ -159,25 +159,6 @@ def sanitize_for_log(text: Any) -> str: return safe -def render_progress_bar( - current: int, total: int, label: str, prefix: str = "🚀" -) -> None: - if not USE_COLORS or total == 0: - return - - width = 20 - progress = min(1.0, current / total) - filled = int(width * progress) - bar = "█" * filled + "░" * (width - filled) - percent = int(progress * 100) - - # Use \033[K to clear line residue - sys.stderr.write( - f"\r\033[K{Colors.CYAN}{prefix} {label}: [{bar}] {percent}% ({current}/{total}){Colors.ENDC}" - ) - sys.stderr.flush() - - def countdown_timer(seconds: int, message: str = "Waiting") -> None: """Shows a countdown timer if strictly in a TTY, otherwise just sleeps.""" if not USE_COLORS: @@ -677,6 +658,10 @@ def _fetch_folder_rules(folder_id: str) -> List[str]: # Parallelize fetching rules from folders. # Using 5 workers to be safe with rate limits, though GETs are usually cheaper. + total_folders = len(folders) + completed_folders = 0 + render_progress_bar(0, total_folders, "Fetching existing rules", prefix="🔍") + with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor: future_to_folder = { executor.submit(_fetch_folder_rules, folder_id): folder_id @@ -684,14 +669,28 @@ def _fetch_folder_rules(folder_id: str) -> List[str]: } for future in concurrent.futures.as_completed(future_to_folder): + completed_folders += 1 try: result = future.result() if result: all_rules.update(result) except Exception as e: + if USE_COLORS: + # Clear line to print warning cleanly + sys.stderr.write("\r\033[K") + sys.stderr.flush() + folder_id = future_to_folder[future] log.warning(f"Failed to fetch rules for folder ID {folder_id}: {e}") + render_progress_bar( + completed_folders, total_folders, "Fetching existing rules", prefix="🔍" + ) + + if USE_COLORS: + sys.stderr.write(f"\r\033[K") + sys.stderr.flush() + log.info(f"Total existing rules across all folders: {len(all_rules)}") return all_rules except Exception as e: diff --git a/test_main.py b/test_main.py index e15c805a..419b526e 100644 --- a/test_main.py +++ b/test_main.py @@ -510,3 +510,37 @@ def test_render_progress_bar(monkeypatch): # Color codes (accessing instance Colors or m.Colors) assert m.Colors.CYAN in combined assert m.Colors.ENDC in combined + +# Case 14: get_all_existing_rules shows progress bar +def test_get_all_existing_rules_shows_progress(monkeypatch): + m = reload_main_with_env(monkeypatch, no_color=None, isatty=True) + mock_client = MagicMock() + profile_id = "test_profile" + + mock_stderr = MagicMock() + monkeypatch.setattr(sys, "stderr", mock_stderr) + + # Mock list_existing_folders to return multiple folders + folders = {f"Folder{i}": f"id_{i}" for i in range(5)} + monkeypatch.setattr(m, "list_existing_folders", MagicMock(return_value=folders)) + + # Mock _api_get + def side_effect(client, url): + mock_resp = MagicMock() + mock_resp.json.return_value = {"body": {"rules": []}} + return mock_resp + monkeypatch.setattr(m, "_api_get", side_effect) + + # Run + m.get_all_existing_rules(mock_client, profile_id) + + # Check that progress bar logic was invoked + # render_progress_bar writes to stderr with \r... + # We check if there were writes containing "Fetching existing rules" + writes = [str(args[0]) for args, _ in mock_stderr.write.call_args_list] + progress_writes = [w for w in writes if "Fetching existing rules" in w] + + assert len(progress_writes) > 0 + # Should be called initially (0/5) + for each folder (1/5 ... 5/5) + # Total calls >= 6 + assert len(progress_writes) >= 6