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
8 changes: 8 additions & 0 deletions .jules/palette.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,11 @@
## 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.

## 2025-05-24 - CLI Accessibility Standards
**Learning:** CLI tools often lack standard accessibility features like `NO_COLOR` support, assuming TTY checks are enough. However, users may want to disable colors even in TTYs for contrast reasons.
**Action:** Always check `os.getenv("NO_COLOR")` in CLI tools alongside TTY checks to respect user preference.

## 2025-05-24 - Reducing CLI Log Noise
**Learning:** High-volume repetitive logs (like batch processing) drown out important errors and context.
**Action:** Use single-line overwriting updates (`\r`) for repetitive progress in interactive sessions, falling back to standard logging for non-interactive streams.
28 changes: 22 additions & 6 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@
load_dotenv()

# Determine if we should use colors
USE_COLORS = sys.stderr.isatty() and sys.stdout.isatty()
# Respect NO_COLOR standard (https://no-color.org/)
if os.getenv("NO_COLOR"):
USE_COLORS = False
else:
USE_COLORS = sys.stderr.isatty() and sys.stdout.isatty()

class Colors:
if USE_COLORS:
Expand Down Expand Up @@ -460,67 +464,79 @@
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))

for i, start in enumerate(range(0, len(filtered_hostnames), BATCH_SIZE), 1):
batch = filtered_hostnames[start : start + BATCH_SIZE]
data = {
"do": str(do),
"status": str(status),
"group": str(folder_id),
}
for j, hostname in enumerate(batch):
data[f"hostnames[{j}]"] = hostname

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

if USE_COLORS:
sys.stderr.write(f"\r{Colors.CYAN}πŸš€ Folder {sanitize_for_log(folder_name)}: Pushing batch {i}/{total_batches}...{Colors.ENDC}")

Check warning

Code scanning / Pylintpython3 (reported by Codacy)

Line too long (143/100) Warning

Line too long (143/100)

Check warning

Code scanning / Pylint (reported by Codacy)

Line too long (143/100) Warning

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

successful_batches += 1
if existing_rules_lock:
with existing_rules_lock:

Check warning

Code scanning / Pylint (reported by Codacy)

Line too long (121/100) Warning

Line too long (121/100)

Check warning

Code scanning / Pylintpython3 (reported by Codacy)

Line too long (121/100) Warning

Line too long (121/100)
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:
log.info("Folder %s – finished (%d new rules added)", sanitize_for_log(folder_name), len(filtered_hostnames))
if USE_COLORS:
sys.stderr.write(f"\r{Colors.GREEN}βœ… Folder {sanitize_for_log(folder_name)}: Finished ({len(filtered_hostnames)} rules) {Colors.ENDC}\n")

Check warning

Code scanning / Pylintpython3 (reported by Codacy)

Line too long (156/100) Warning

Line too long (156/100)

Check warning

Code scanning / Pylint (reported by Codacy)

Line too long (156/100) Warning

Line too long (156/100)
sys.stderr.flush()
else:
log.info("Folder %s – finished (%d new rules added)", sanitize_for_log(folder_name), len(filtered_hostnames))
return True
else:
log.error("Folder %s – only %d/%d batches succeeded", sanitize_for_log(folder_name), successful_batches, total_batches)
Expand Down
Loading