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
4 changes: 4 additions & 0 deletions .jules/palette.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,7 @@
## 2024-03-22 - CLI Interactive Fallbacks
**Learning:** CLI tools often fail hard when config is missing, but interactive contexts allow for graceful recovery. Users appreciate being asked for missing info instead of just receiving an error.
**Action:** When `sys.stdin.isatty()` is true, prompt for missing configuration instead of exiting with an error code.

## 2024-05-24 - Progress vs. Logs in CLI
**Learning:** High-frequency logging (e.g., per-batch updates) floods the terminal and hides errors. Replacing it with an in-place progress bar significantly improves UX, but requires careful handling of non-interactive environments (CI logs) and concurrency (shared stderr).
**Action:** Use `sys.stderr.isatty()` to toggle between "Rich Progress Mode" (interactive) and "Verbose Log Mode" (CI/Pipes).
30 changes: 26 additions & 4 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
ENDC = '\033[0m'
BOLD = '\033[1m'
UNDERLINE = '\033[4m'
CLEAR_LINE = '\033[K'
else:
HEADER = ''
BLUE = ''
Expand All @@ -58,6 +59,7 @@
ENDC = ''
BOLD = ''
UNDERLINE = ''
CLEAR_LINE = ''

class ColoredFormatter(logging.Formatter):
"""Custom formatter to add colors to log levels."""
Expand Down Expand Up @@ -460,38 +462,43 @@
log.error(f"Failed to create folder {sanitize_for_log(name)}: {sanitize_for_log(e)}")
return None

def push_rules(
profile_id: str,
folder_name: str,
folder_id: str,
do: int,
status: int,
hostnames: List[str],
existing_rules: Set[str],
client: httpx.Client,
existing_rules_lock: Optional[threading.Lock] = None,
) -> bool:
if not hostnames:
log.info("Folder %s - no rules to push", sanitize_for_log(folder_name))
return True

original_count = len(hostnames)

# Optimization: Check directly against existing_rules to avoid O(N) copy.
# Membership testing in set is thread-safe, and we don't need a strict snapshot for deduplication.
filtered_hostnames = [h for h in hostnames if h not in existing_rules]
duplicates_count = original_count - len(filtered_hostnames)

if duplicates_count > 0:
log.info(f"Folder {sanitize_for_log(folder_name)}: skipping {duplicates_count} duplicate rules")

if not filtered_hostnames:
log.info(f"Folder {sanitize_for_log(folder_name)} - no new rules to push after filtering duplicates")
return True

successful_batches = 0
total_batches = len(range(0, len(filtered_hostnames), BATCH_SIZE))

# UX: Initialize progress bar (Assumes serial execution for clean output)
if USE_COLORS:
sys.stderr.write(f"\r{Colors.CYAN}Syncing {sanitize_for_log(folder_name)}: [0/{total_batches}] batches{Colors.ENDC}{Colors.CLEAR_LINE}")

Check warning

Code scanning / Pylint (reported by Codacy)

Line too long (144/100) Warning

Line too long (144/100)

Check warning

Code scanning / Pylintpython3 (reported by Codacy)

Line too long (144/100) Warning

Line too long (144/100)
sys.stderr.flush()

for i, start in enumerate(range(0, len(filtered_hostnames), BATCH_SIZE), 1):
batch = filtered_hostnames[start : start + BATCH_SIZE]
data = {
Expand All @@ -504,22 +511,37 @@

try:
_api_post_form(client, f"{API_BASE}/{profile_id}/rules", data=data)
log.info(
"Folder %s – batch %d: added %d rules",
sanitize_for_log(folder_name), i, len(batch)
)

successful_batches += 1

# UX: Update progress bar or log verbose
if USE_COLORS:
sys.stderr.write(f"\r{Colors.CYAN}Syncing {sanitize_for_log(folder_name)}: [{successful_batches}/{total_batches}] batches{Colors.ENDC}{Colors.CLEAR_LINE}")

Check warning

Code scanning / Prospector (reported by Codacy)

line too long (171 > 159 characters) (E501) Warning

line too long (171 > 159 characters) (E501)

Check warning

Code scanning / Pylint (reported by Codacy)

Line too long (171/100) Warning

Line too long (171/100)

Check warning

Code scanning / Pylintpython3 (reported by Codacy)

Line too long (171/100) Warning

Line too long (171/100)
sys.stderr.flush()
else:
log.info(
"Folder %s – batch %d: added %d rules",
sanitize_for_log(folder_name), i, len(batch)
)

if existing_rules_lock:
with existing_rules_lock:
existing_rules.update(batch)
else:
existing_rules.update(batch)
except httpx.HTTPError as e:
if USE_COLORS:
sys.stderr.write("\n")
log.error(f"Failed to push batch {i} for folder {sanitize_for_log(folder_name)}: {sanitize_for_log(e)}")
if hasattr(e, 'response') and e.response is not None:
log.debug(f"Response content: {e.response.text}")

if successful_batches == total_batches:
if USE_COLORS:
# Clear the progress line
sys.stderr.write(f"\r{Colors.CLEAR_LINE}")
sys.stderr.flush()

log.info("Folder %s – finished (%d new rules added)", sanitize_for_log(folder_name), len(filtered_hostnames))
return True
else:
Expand Down
Loading