Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .jules/palette.md
Original file line number Diff line number Diff line change
@@ -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.
37 changes: 18 additions & 19 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,25 +159,6 @@
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:
Expand Down Expand Up @@ -677,21 +658,39 @@

# 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
for folder_name, folder_id in folders.items()
}

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}")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

A security vulnerability exists here: the folder_id and exception e are interpolated directly into the log message without sanitization. Since folder_id comes from an external API and e can contain untrusted data, this could allow an attacker to inject ANSI escape sequences into the terminal, leading to terminal hijacking or log injection. Please use the existing sanitize_for_log function to prevent this. Additionally, ensure that exceptions from the _fetch_folder_rules function are properly re-raised and propagate to this except block, as logging directly from worker threads can interfere with the progress bar rendering and this handler might not be triggered as expected.

Suggested change
log.warning(f"Failed to fetch rules for folder ID {folder_id}: {e}")
log.warning(f"Failed to fetch rules for folder ID {sanitize_for_log(folder_id)}: {sanitize_for_log(e)}")


render_progress_bar(
completed_folders, total_folders, "Fetching existing rules", prefix="🔍"
)

if USE_COLORS:
sys.stderr.write(f"\r\033[K")

Check warning

Code scanning / Prospector (reported by Codacy)

Using an f-string that does not have any interpolated variables (f-string-without-interpolation) Warning

Using an f-string that does not have any interpolated variables (f-string-without-interpolation)

Check notice

Code scanning / Pylintpython3 (reported by Codacy)

Using an f-string that does not have any interpolated variables Note

Using an f-string that does not have any interpolated variables
sys.stderr.flush()

log.info(f"Total existing rules across all folders: {len(all_rules)}")
return all_rules
except Exception as e:
Expand Down
34 changes: 34 additions & 0 deletions test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
monkeypatch.setattr(m, "list_existing_folders", mock_list_folders)

# Mock _api_get to return different rules for root vs folders
def side_effect(client, url):

Check warning

Code scanning / Pylint (reported by Codacy)

Missing function docstring Warning test

Missing function docstring

Check notice

Code scanning / Pylint (reported by Codacy)

Unused argument 'client' Note test

Unused argument 'client'

Check notice

Code scanning / Pylintpython3 (reported by Codacy)

Unused argument 'client' Note test

Unused argument 'client'
mock_resp = MagicMock()
if url.endswith("/rules"): # Root rules
mock_resp.json.return_value = {"body": {"rules": [{"PK": "rule_root"}]}}
Expand Down Expand Up @@ -174,7 +174,7 @@

# Case 5: push_rules writes colored progress and completion messages to stderr when USE_COLORS is True
def test_push_rules_writes_colored_stderr(monkeypatch):
m = reload_main_with_env(monkeypatch, no_color=None, isatty=True)

Check warning

Code scanning / Pylint (reported by Codacy)

Variable name "m" doesn't conform to snake_case naming style Warning test

Variable name "m" doesn't conform to snake_case naming style

Check warning

Code scanning / Pylintpython3 (reported by Codacy)

Variable name "m" doesn't conform to snake_case naming style Warning test

Variable name "m" doesn't conform to snake_case naming style
monkeypatch.setattr(m, "_api_post_form", MagicMock())

mock_stderr = MagicMock()
Expand Down Expand Up @@ -510,3 +510,37 @@
# 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):

Check warning

Code scanning / Pylintpython3 (reported by Codacy)

Missing function or method docstring Warning test

Missing function or method docstring

Check warning

Code scanning / Pylint (reported by Codacy)

Missing function docstring Warning test

Missing function docstring
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):

Check warning

Code scanning / Prospector (reported by Codacy)

Unused argument 'client' (unused-argument) Warning test

Unused argument 'client' (unused-argument)

Check notice

Code scanning / Pylintpython3 (reported by Codacy)

Unused argument 'url' Note test

Unused argument 'url'

Check warning

Code scanning / Prospector (reported by Codacy)

Unused argument 'url' (unused-argument) Warning test

Unused argument 'url' (unused-argument)

Check notice

Code scanning / Pylint (reported by Codacy)

Unused argument 'client' Note test

Unused argument 'client'
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

Check notice

Code scanning / Bandit

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.

Check notice

Code scanning / Bandit (reported by Codacy)

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.

Check warning

Code scanning / Pylint (reported by Codacy)

Do not use len(SEQUENCE) to determine if a sequence is empty Warning test

Do not use len(SEQUENCE) to determine if a sequence is empty
# Should be called initially (0/5) + for each folder (1/5 ... 5/5)
# Total calls >= 6
assert len(progress_writes) >= 6

Check notice

Code scanning / Bandit

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.

Check notice

Code scanning / Bandit (reported by Codacy)

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
Comment on lines +515 to +546

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This test effectively covers the success path for the progress bar. To ensure the new error handling logic is also working correctly, consider adding a test case for the failure scenario.

You could configure the _api_get mock to raise an httpx.HTTPError for one of the calls. Then you could assert that:

  1. log.warning is called with the expected error message.
  2. The stderr output contains the warning.
  3. The progress bar calls continue for all folders, including the one that failed.

This would provide confidence that errors are handled gracefully without corrupting the progress bar display.

Loading