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
63 changes: 52 additions & 11 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -651,7 +651,7 @@
return [rule["PK"] for rule in folder_rules if rule.get("PK")]
except httpx.HTTPError:
return []
except Exception as e:

Check warning

Code scanning / Pylint (reported by Codacy)

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

Variable name "e" doesn't conform to snake_case naming style
# We log error but don't stop the whole process;
# individual folder failure shouldn't crash the sync
log.warning(f"Error fetching rules for folder {folder_id}: {e}")
Expand Down Expand Up @@ -694,7 +694,7 @@

log.info(f"Total existing rules across all folders: {len(all_rules)}")
return all_rules
except Exception as e:

Check notice

Code scanning / Pylintpython3 (reported by Codacy)

Catching too general exception Exception Note

Catching too general exception Exception
log.error(f"Failed to get existing rules: {sanitize_for_log(e)}")
return set()

Expand Down Expand Up @@ -825,7 +825,7 @@
grp["PK"],
)
return str(grp["PK"])
except Exception as e:

Check notice

Code scanning / Pylint (reported by Codacy)

Catching too general exception Exception Note

Catching too general exception Exception
log.warning(f"Error fetching groups on attempt {attempt}: {e}")

if attempt < MAX_RETRIES:
Expand Down Expand Up @@ -855,7 +855,7 @@
status: int,
hostnames: List[str],
existing_rules: Set[str],
client: httpx.Client,

Check warning

Code scanning / Pylint (reported by Codacy)

Wrong hanging indentation before block (add 4 spaces). Warning

Wrong hanging indentation before block (add 4 spaces).
) -> bool:
if not hostnames:
log.info("Folder %s - no rules to push", sanitize_for_log(folder_name))
Expand Down Expand Up @@ -1036,6 +1036,55 @@
# --------------------------------------------------------------------------- #
# 4. Main workflow
# --------------------------------------------------------------------------- #
def delete_existing_folders_parallel(
client: httpx.Client,

Check warning

Code scanning / Pylint (reported by Codacy)

Wrong hanging indentation before block (add 4 spaces). Warning

Wrong hanging indentation before block (add 4 spaces).
profile_id: str,
folder_data_list: List[Dict[str, Any]],

Check warning

Code scanning / Pylint (reported by Codacy)

Wrong hanging indentation before block (add 4 spaces). Warning

Wrong hanging indentation before block (add 4 spaces).
existing_folders: Dict[str, str],

Check warning

Code scanning / Pylint (reported by Codacy)

Wrong hanging indentation before block (add 4 spaces). Warning

Wrong hanging indentation before block (add 4 spaces).
) -> bool:
"""
Identifies and deletes existing folders that match the new folder list.
Deletes in parallel to save time.
Updates existing_folders in-place by removing deleted folders.
Returns True if any deletion occurred.
"""
folders_to_delete = []
seen_deletions = set()

# Identify folders to delete
for folder_data in folder_data_list:
name = folder_data["group"]["group"].strip()
if name in existing_folders and name not in seen_deletions:
folders_to_delete.append((name, existing_folders[name]))
seen_deletions.add(name)

if not folders_to_delete:
return False

if not USE_COLORS:
log.info(f"Deleting {len(folders_to_delete)} folders in parallel...")

Check notice

Code scanning / Pylintpython3 (reported by Codacy)

Use lazy % formatting in logging functions Note

Use lazy % formatting in logging functions

Check warning

Code scanning / Prospector (reported by Codacy)

Use lazy % formatting in logging functions (logging-fstring-interpolation) Warning

Use lazy % formatting in logging functions (logging-fstring-interpolation)

deletion_occurred = False
# Using 5 workers to be safe with rate limits for write operations
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
future_to_name = {
executor.submit(delete_folder, client, profile_id, name, fid): name
Comment on lines +1068 to +1071
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

The httpx.Client instance is being shared across multiple threads (5 workers). According to httpx documentation, Client instances are not thread-safe and should not be shared across threads without synchronization. While this pattern exists elsewhere in the codebase (e.g., in get_all_existing_rules), the concurrent modifications to the client's connection pool could lead to race conditions. Consider using a lock around client operations, creating a separate client instance in each thread, or using a thread-safe connection pool pattern.

Suggested change
# Using 5 workers to be safe with rate limits for write operations
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
future_to_name = {
executor.submit(delete_folder, client, profile_id, name, fid): name
# Protect shared httpx.Client instance when used across multiple threads.
client_lock = threading.Lock()
def _threadsafe_delete_folder(profile_id_arg: str, name_arg: str, fid_arg: str) -> bool:
"""
Wrapper around delete_folder that serializes access to the shared client.
"""
with client_lock:
return delete_folder(client, profile_id_arg, name_arg, fid_arg)
# Using 5 workers to be safe with rate limits for write operations
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
future_to_name = {
executor.submit(_threadsafe_delete_folder, profile_id, name, fid): name

Copilot uses AI. Check for mistakes.
for name, fid in folders_to_delete
}

for future in concurrent.futures.as_completed(future_to_name):
name = future_to_name[future]
try:
if future.result():
if name in existing_folders:
del existing_folders[name]
deletion_occurred = True
except Exception as e:
log.error(f"Failed to delete folder {sanitize_for_log(name)}: {e}")

Check notice

Code scanning / Pylintpython3 (reported by Codacy)

Use lazy % formatting in logging functions Note

Use lazy % formatting in logging functions

Check warning

Code scanning / Prospector (reported by Codacy)

Use lazy % formatting in logging functions (logging-fstring-interpolation) Warning

Use lazy % formatting in logging functions (logging-fstring-interpolation)

return deletion_occurred


def sync_profile(
profile_id: str,
folder_urls: Sequence[str],
Expand Down Expand Up @@ -1148,21 +1197,13 @@

existing_folders = list_existing_folders(client, profile_id)
if not no_delete:
deletion_occurred = False
for folder_data in folder_data_list:
name = folder_data["group"]["group"].strip()
if name in existing_folders:
# Optimization: Maintain local state of folders to avoid re-fetching
# delete_folder returns True on success
if delete_folder(
client, profile_id, name, existing_folders[name]
):
del existing_folders[name]
deletion_occurred = True
deletion_occurred = delete_existing_folders_parallel(
client, profile_id, folder_data_list, existing_folders
)

# CRITICAL FIX: Increased wait time for massive folders to clear
if deletion_occurred:
if not USE_COLORS:

Check warning

Code scanning / Pylintpython3 (reported by Codacy)

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

Variable name "e" doesn't conform to snake_case naming style
log.info(
"Waiting 60s for deletions to propagate (prevents 'Badware Hoster' zombie state)..."
)
Expand Down
124 changes: 124 additions & 0 deletions tests/test_parallel_delete.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@

Check warning

Code scanning / Pylintpython3 (reported by Codacy)

Missing module docstring Warning test

Missing module docstring

Check warning

Code scanning / Pylint (reported by Codacy)

Missing module docstring Warning test

Missing module docstring
import concurrent.futures

Check notice

Code scanning / Pylintpython3 (reported by Codacy)

Unused import concurrent.futures Note test

Unused import concurrent.futures

Check warning

Code scanning / Prospector (reported by Codacy)

Unused import concurrent.futures (unused-import) Warning test

Unused import concurrent.futures (unused-import)

Check notice

Code scanning / Pylint (reported by Codacy)

Unused import concurrent.futures Note test

Unused import concurrent.futures
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

The import of concurrent.futures is unused in this test file. It should be removed to keep the imports clean.

Suggested change
import concurrent.futures

Copilot uses AI. Check for mistakes.
import time
from unittest.mock import MagicMock
import pytest

Check notice

Code scanning / Pylintpython3 (reported by Codacy)

Unused import pytest Note test

Unused import pytest

Check warning

Code scanning / Prospector (reported by Codacy)

Unused import pytest (unused-import) Warning test

Unused import pytest (unused-import)

Check warning

Code scanning / Prospector (reported by Codacy)

Unable to import 'pytest' (import-error) Warning test

Unable to import 'pytest' (import-error)

Check notice

Code scanning / Pylint (reported by Codacy)

Unused import pytest Note test

Unused import pytest
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

Import of 'pytest' is not used.

Suggested change
import pytest

Copilot uses AI. Check for mistakes.
import main

def test_delete_existing_folders_parallel(monkeypatch):
"""
Verify that delete_existing_folders_parallel:
1. Runs in parallel (speed check).
2. Updates existing_folders correctly.
3. Returns True if deletions occurred.
"""

# Mock delete_folder to simulate network delay
# Each deletion takes 0.1s
mock_delete = MagicMock()
def slow_delete(client, profile_id, name, folder_id):

Check warning

Code scanning / Pylint (reported by Codacy)

Missing function docstring Warning test

Missing function docstring

Check notice

Code scanning / Pylintpython3 (reported by Codacy)

Unused argument 'folder_id' Note test

Unused argument 'folder_id'

Check notice

Code scanning / Pylintpython3 (reported by Codacy)

Unused argument 'client' Note test

Unused argument 'client'

Check warning

Code scanning / Prospector (reported by Codacy)

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

Unused argument 'folder_id' (unused-argument)

Check warning

Code scanning / Prospector (reported by Codacy)

expected 1 blank line before a nested definition, found 0 (E306) Warning test

expected 1 blank line before a nested definition, found 0 (E306)

Check notice

Code scanning / Pylintpython3 (reported by Codacy)

Unused argument 'name' Note test

Unused argument 'name'

Check warning

Code scanning / Prospector (reported by Codacy)

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

Unused argument 'profile_id' (unused-argument)

Check notice

Code scanning / Pylintpython3 (reported by Codacy)

Unused argument 'profile_id' Note test

Unused argument 'profile_id'

Check warning

Code scanning / Prospector (reported by Codacy)

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

Unused argument 'client' (unused-argument)

Check warning

Code scanning / Prospector (reported by Codacy)

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

Unused argument 'name' (unused-argument)

Check notice

Code scanning / Pylint (reported by Codacy)

Unused argument 'profile_id' Note test

Unused argument 'profile_id'

Check notice

Code scanning / Pylint (reported by Codacy)

Unused argument 'client' Note test

Unused argument 'client'

Check notice

Code scanning / Pylint (reported by Codacy)

Unused argument 'name' Note test

Unused argument 'name'

Check notice

Code scanning / Pylint (reported by Codacy)

Unused argument 'folder_id' Note test

Unused argument 'folder_id'
time.sleep(0.1)
return True

mock_delete.side_effect = slow_delete
monkeypatch.setattr(main, "delete_folder", mock_delete)

# Mock log to avoid clutter
mock_log = MagicMock()
monkeypatch.setattr(main, "log", mock_log)

# Setup
client = MagicMock()
profile_id = "test_profile"

# 10 folders to delete
# "group" -> "group" structure mimics the API response structure used in main.py
folder_data_list = [{"group": {"group": f"Folder {i}"}} for i in range(10)]

# All these folders exist in existing_folders
existing_folders = {f"Folder {i}": f"id_{i}" for i in range(10)}
original_len = len(existing_folders)

Check notice

Code scanning / Pylintpython3 (reported by Codacy)

Unused variable 'original_len' Note test

Unused variable 'original_len'

Check notice

Code scanning / Pylint (reported by Codacy)

Unused variable 'original_len' Note test

Unused variable 'original_len'

Check warning

Code scanning / Prospector (reported by Codacy)

Unused variable 'original_len' (unused-variable) Warning test

Unused variable 'original_len' (unused-variable)
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

The variable original_len is assigned but never used. Consider removing it or using it in an assertion to verify the test setup.

Copilot uses AI. Check for mistakes.

# Add one folder that exists but is NOT in folder_data_list (should NOT be deleted)
existing_folders["Keep Me"] = "id_keep"

# Add one folder in data list that does NOT exist (should check but skip deletion)
folder_data_list.append({"group": {"group": "New Folder"}})

start_time = time.time()
result = main.delete_existing_folders_parallel(
client, profile_id, folder_data_list, existing_folders
)
end_time = time.time()
duration = end_time - start_time

# Assertions
assert result is True

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 that the 10 folders were deleted
for i in range(10):
assert f"Folder {i}" not in existing_folders

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 that "Keep Me" is still there
assert "Keep Me" in existing_folders

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.
assert existing_folders["Keep Me"] == "id_keep"

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 that "New Folder" is not in existing_folders (it wasn't there to begin with)
assert "New Folder" not in existing_folders

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 performance
# 10 tasks * 0.1s = 1.0s sequential time.
# With 5 workers, it should take roughly 0.2s + overhead.
# We assert it takes < 0.6s to be safe but prove parallelism.
assert duration < 0.6, f"Execution took {duration}s, expected < 0.6s (parallel)"

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.
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

The timing assertion could be flaky on slower systems or under heavy load. Consider either increasing the threshold (e.g., to 0.8s or 1.0s) or using a more flexible assertion that checks if the parallel version is faster than sequential without relying on absolute time, such as comparing the duration to the theoretical sequential time with a tolerance factor.

Copilot uses AI. Check for mistakes.

# Verify mock calls
assert mock_delete.call_count == 10

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.

def test_delete_existing_folders_parallel_no_deletions(monkeypatch):

Check notice

Code scanning / Pylint (reported by Codacy)

Unused argument 'monkeypatch' Note test

Unused argument 'monkeypatch'

Check notice

Code scanning / Pylintpython3 (reported by Codacy)

Unused argument 'monkeypatch' Note test

Unused argument 'monkeypatch'

Check warning

Code scanning / Prospector (reported by Codacy)

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

Unused argument 'monkeypatch' (unused-argument)
"""Verify behavior when no folders match."""
client = MagicMock()
profile_id = "test_profile"
folder_data_list = [{"group": {"group": "Folder A"}}]
existing_folders = {"Folder B": "id_b"} # No match

result = main.delete_existing_folders_parallel(
client, profile_id, folder_data_list, existing_folders
)

assert result is False

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.
assert "Folder B" in existing_folders

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.

def test_delete_existing_folders_parallel_partial_failure(monkeypatch):
"""Verify behavior when some deletions fail."""

# Mock delete_folder: Fail for even numbers
mock_delete = MagicMock()
def conditional_delete(client, profile_id, name, folder_id):

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 'folder_id' Note test

Unused argument 'folder_id'

Check notice

Code scanning / Pylint (reported by Codacy)

Unused argument 'profile_id' Note test

Unused argument 'profile_id'

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 'folder_id' Note test

Unused argument 'folder_id'

Check warning

Code scanning / Prospector (reported by Codacy)

expected 1 blank line before a nested definition, found 0 (E306) Warning test

expected 1 blank line before a nested definition, found 0 (E306)

Check notice

Code scanning / Pylintpython3 (reported by Codacy)

Unused argument 'profile_id' Note test

Unused argument 'profile_id'

Check warning

Code scanning / Prospector (reported by Codacy)

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

Unused argument 'profile_id' (unused-argument)

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 'client' Note test

Unused argument 'client'

Check warning

Code scanning / Prospector (reported by Codacy)

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

Unused argument 'folder_id' (unused-argument)
# name is "Folder i"
num = int(name.split()[1])
return num % 2 != 0 # Return True for odd (success), False for even (fail)

mock_delete.side_effect = conditional_delete
monkeypatch.setattr(main, "delete_folder", mock_delete)

client = MagicMock()
profile_id = "test_profile"
folder_data_list = [{"group": {"group": f"Folder {i}"}} for i in range(10)]
existing_folders = {f"Folder {i}": f"id_{i}" for i in range(10)}

result = main.delete_existing_folders_parallel(
client, profile_id, folder_data_list, existing_folders
)

assert result is True # At least one succeeded

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 results
for i in range(10):
name = f"Folder {i}"
if i % 2 != 0:
# Odd: Succeeded, should be removed
assert name not in existing_folders

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.
else:
# Even: Failed, should remain
assert name in existing_folders

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.
Loading