-
Notifications
You must be signed in to change notification settings - Fork 1
Combine API access check and folder listing into single request; close stale Jules PRs #178
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -444,7 +444,7 @@ | |
| ip_str = res[4][0] | ||
| ip = ipaddress.ip_address(ip_str) | ||
| if not ip.is_global or ip.is_multicast: | ||
| log.warning( | ||
Check warningCode scanning / Prospector (reported by Codacy) Use lazy % formatting in logging functions (logging-fstring-interpolation) Warning
Use lazy % formatting in logging functions (logging-fstring-interpolation)
|
||
| f"Skipping unsafe URL (domain {hostname} resolves to non-global/multicast IP {ip}): {sanitize_for_log(url)}" | ||
| ) | ||
| return False | ||
|
|
@@ -533,7 +533,7 @@ | |
|
|
||
| def validate_folder_data(data: Dict[str, Any], url: str) -> bool: | ||
| if not isinstance(data, dict): | ||
| log.error( | ||
Check noticeCode scanning / Pylintpython3 (reported by Codacy) Use lazy % formatting in logging functions Note
Use lazy % formatting in logging functions
|
||
| f"Invalid data from {sanitize_for_log(url)}: Root must be a JSON object." | ||
| ) | ||
| return False | ||
|
|
@@ -541,12 +541,12 @@ | |
| log.error(f"Invalid data from {sanitize_for_log(url)}: Missing 'group' key.") | ||
| return False | ||
| if not isinstance(data["group"], dict): | ||
| log.error( | ||
Check noticeCode scanning / Pylintpython3 (reported by Codacy) Use lazy % formatting in logging functions Note
Use lazy % formatting in logging functions
|
||
| f"Invalid data from {sanitize_for_log(url)}: 'group' must be an object." | ||
| ) | ||
| return False | ||
| if "group" not in data["group"]: | ||
| log.error( | ||
Check noticeCode scanning / Pylintpython3 (reported by Codacy) Use lazy % formatting in logging functions Note
Use lazy % formatting in logging functions
|
||
| f"Invalid data from {sanitize_for_log(url)}: Missing 'group.group' (folder name)." | ||
| ) | ||
| return False | ||
|
|
@@ -676,13 +676,13 @@ | |
| resp = client.get(url) | ||
| resp.raise_for_status() | ||
| return True | ||
| except httpx.HTTPStatusError as e: | ||
Check warningCode 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
|
||
| code = e.response.status_code | ||
| if code == 401: | ||
| log.critical( | ||
Check warningCode scanning / Prospector (reported by Codacy) Use lazy % formatting in logging functions (logging-fstring-interpolation) Warning
Use lazy % formatting in logging functions (logging-fstring-interpolation)
|
||
| f"{Colors.FAIL}❌ Authentication Failed: The API Token is invalid.{Colors.ENDC}" | ||
| ) | ||
| log.critical( | ||
Check noticeCode scanning / Pylintpython3 (reported by Codacy) Use lazy % formatting in logging functions Note
Use lazy % formatting in logging functions
|
||
| f"{Colors.FAIL} Please check your token at: https://controld.com/account/manage-account{Colors.ENDC}" | ||
| ) | ||
| elif code == 403: | ||
|
|
@@ -693,13 +693,13 @@ | |
| log.critical( | ||
| f"{Colors.FAIL}🔍 Profile Not Found: The ID '{profile_id}' does not exist.{Colors.ENDC}" | ||
| ) | ||
| log.critical( | ||
Check warningCode scanning / Prospector (reported by Codacy) Use lazy % formatting in logging functions (logging-fstring-interpolation) Warning
Use lazy % formatting in logging functions (logging-fstring-interpolation)
Check noticeCode scanning / Pylintpython3 (reported by Codacy) Use lazy % formatting in logging functions Note
Use lazy % formatting in logging functions
|
||
| f"{Colors.FAIL} Please verify the Profile ID from your Control D Dashboard URL.{Colors.ENDC}" | ||
Check warningCode scanning / Pylint (reported by Codacy) Line too long (111/100) Warning
Line too long (111/100)
|
||
| ) | ||
| else: | ||
| log.error(f"API Access Check Failed ({code}): {e}") | ||
| return False | ||
| except httpx.RequestError as e: | ||
Check warningCode 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.error(f"Network Error during access check: {e}") | ||
| return False | ||
|
|
||
|
|
@@ -718,6 +718,116 @@ | |
| return {} | ||
|
|
||
|
|
||
| def verify_access_and_get_folders( | ||
Check warningCode scanning / Pylint (reported by Codacy) Too many return statements (8/6) Warning
Too many return statements (8/6)
Check warningCode scanning / Pylint (reported by Codacy) Too many branches (17/12) Warning
Too many branches (17/12)
Check warningCode scanning / Pylintpython3 (reported by Codacy) Too many branches (17/12) Warning
Too many branches (17/12)
Check warningCode scanning / Pylintpython3 (reported by Codacy) Too many return statements (8/6) Warning
Too many return statements (8/6)
|
||
| client: httpx.Client, profile_id: str | ||
Check warningCode scanning / Pylint (reported by Codacy) Wrong hanging indentation before block (add 4 spaces). Warning
Wrong hanging indentation before block (add 4 spaces).
|
||
| ) -> Optional[Dict[str, str]]: | ||
| """Combine access check and folder listing into a single API request. | ||
|
|
||
| Returns: | ||
| Dict of {folder_name: folder_id} on success. | ||
| None if access is denied or the request fails after retries. | ||
| """ | ||
| url = f"{API_BASE}/{profile_id}/groups" | ||
|
|
||
| for attempt in range(MAX_RETRIES): | ||
| try: | ||
| resp = client.get(url) | ||
| resp.raise_for_status() | ||
|
|
||
| try: | ||
| data = resp.json() | ||
|
|
||
| # Ensure we got the expected top-level JSON structure. | ||
| # We defensively validate types here so that unexpected but valid | ||
| # JSON (e.g., a list or a scalar) doesn't cause AttributeError/TypeError | ||
| # and crash the sync logic. | ||
| if not isinstance(data, dict): | ||
| log.error( | ||
|
||
| "Failed to parse folders data: expected JSON object at top level, " | ||
| f"got {type(data).__name__}" | ||
| ) | ||
| return None | ||
|
|
||
| body = data.get("body") | ||
| if not isinstance(body, dict): | ||
| log.error( | ||
| "Failed to parse folders data: expected 'body' to be an object, " | ||
| f"got {type(body).__name__ if body is not None else 'None'}" | ||
| ) | ||
| return None | ||
|
|
||
| folders = body.get("groups", []) | ||
| if not isinstance(folders, list): | ||
| log.error( | ||
| "Failed to parse folders data: expected 'body[\"groups\"]' to be a list, " | ||
| f"got {type(folders).__name__}" | ||
| ) | ||
| return None | ||
|
|
||
| # Only process entries that are dicts and have the required keys. | ||
| result: Dict[str, str] = {} | ||
| for f in folders: | ||
Check warningCode scanning / Pylint (reported by Codacy) Variable name "f" doesn't conform to snake_case naming style Warning
Variable name "f" doesn't conform to snake_case naming style
Check warningCode scanning / Pylintpython3 (reported by Codacy) Variable name "f" doesn't conform to snake_case naming style Warning
Variable name "f" doesn't conform to snake_case naming style
|
||
| if not isinstance(f, dict): | ||
| # Skip non-dict entries instead of crashing; this protects | ||
| # against partial data corruption or unexpected API changes. | ||
| continue | ||
| name = f.get("group") | ||
| pk = f.get("PK") | ||
Check warningCode scanning / Pylintpython3 (reported by Codacy) Variable name "pk" doesn't conform to snake_case naming style Warning
Variable name "pk" doesn't conform to snake_case naming style
Check warningCode scanning / Pylint (reported by Codacy) Variable name "pk" doesn't conform to snake_case naming style Warning
Variable name "pk" doesn't conform to snake_case naming style
|
||
| if not name or not pk: | ||
| continue | ||
| result[str(name).strip()] = str(pk) | ||
|
|
||
| return result | ||
| except (KeyError, ValueError, TypeError, AttributeError) as e: | ||
Check warningCode 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
Check warningCode 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
|
||
| # As a final safeguard, catch any remaining parsing/shape errors so | ||
| # that a malformed response cannot crash the caller. | ||
| log.error(f"Failed to parse folders data: {sanitize_for_log(e)}") | ||
Check warningCode scanning / Prospector (reported by Codacy) Use lazy % formatting in logging functions (logging-fstring-interpolation) Warning
Use lazy % formatting in logging functions (logging-fstring-interpolation)
Check noticeCode scanning / Pylintpython3 (reported by Codacy) Use lazy % formatting in logging functions Note
Use lazy % formatting in logging functions
|
||
| return None | ||
|
|
||
| except httpx.HTTPStatusError as e: | ||
Check warningCode 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
Check warningCode 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
|
||
| code = e.response.status_code | ||
| if code in (401, 403, 404): | ||
| if code == 401: | ||
| log.critical( | ||
|
||
| f"{Colors.FAIL}❌ Authentication Failed: The API Token is invalid.{Colors.ENDC}" | ||
Check warningCode scanning / Pylint (reported by Codacy) Line too long (103/100) Warning
Line too long (103/100)
Check warningCode scanning / Pylintpython3 (reported by Codacy) Line too long (103/100) Warning
Line too long (103/100)
|
||
| ) | ||
| log.critical( | ||
|
||
| f"{Colors.FAIL} Please check your token at: https://controld.com/account/manage-account{Colors.ENDC}" | ||
|
||
| ) | ||
| elif code == 403: | ||
| log.critical( | ||
| f"{Colors.FAIL}🚫 Access Denied: Token lacks permission for Profile {sanitize_for_log(profile_id)}.{Colors.ENDC}" | ||
Check warningCode scanning / Pylint (reported by Codacy) Line too long (136/100) Warning
Line too long (136/100)
|
||
| ) | ||
| elif code == 404: | ||
| log.critical( | ||
|
||
| f"{Colors.FAIL}🔍 Profile Not Found: The ID '{sanitize_for_log(profile_id)}' does not exist.{Colors.ENDC}" | ||
Check warningCode scanning / Pylint (reported by Codacy) Line too long (121/100) Warning
Line too long (121/100)
|
||
| ) | ||
| log.critical( | ||
| f"{Colors.FAIL} Please verify the Profile ID from your Control D Dashboard URL.{Colors.ENDC}" | ||
| ) | ||
| return None | ||
|
|
||
| if attempt == MAX_RETRIES - 1: | ||
| log.error(f"API Request Failed ({code}): {sanitize_for_log(e)}") | ||
|
||
| return None | ||
|
|
||
| except httpx.RequestError as e: | ||
| if attempt == MAX_RETRIES - 1: | ||
| log.error( | ||
| f"Network error during access verification: {sanitize_for_log(e)}" | ||
| ) | ||
| return None | ||
|
|
||
| wait_time = RETRY_DELAY * (2**attempt) | ||
| log.warning( | ||
| f"Request failed (attempt {attempt + 1}/{MAX_RETRIES}). Retrying in {wait_time}s..." | ||
| ) | ||
| time.sleep(wait_time) | ||
|
|
||
| log.error("Access verification failed after all retries") | ||
| return None | ||
|
|
||
|
|
||
| def get_all_existing_rules( | ||
| client: httpx.Client, | ||
| profile_id: str, | ||
|
|
@@ -806,7 +916,7 @@ | |
| return None | ||
|
|
||
| completed = 0 | ||
| with concurrent.futures.ThreadPoolExecutor() as executor: | ||
Check warningCode scanning / Prospector (reported by Codacy) Use lazy % formatting in logging functions (logging-fstring-interpolation) Warning
Use lazy % formatting in logging functions (logging-fstring-interpolation)
|
||
| futures = { | ||
| executor.submit(_validate_and_fetch, url): url for url in urls_to_process | ||
| } | ||
|
|
@@ -853,7 +963,7 @@ | |
|
|
||
| def create_folder( | ||
| client: httpx.Client, profile_id: str, name: str, do: int, status: int | ||
| ) -> Optional[str]: | ||
Check noticeCode scanning / Pylintpython3 (reported by Codacy) Use lazy % formatting in logging functions Note
Use lazy % formatting in logging functions
|
||
| """ | ||
| Create a new folder and return its ID. | ||
| Attempts to read ID from response first, then falls back to polling. | ||
|
|
@@ -902,7 +1012,7 @@ | |
| if grp["group"].strip() == name.strip(): | ||
| log.info( | ||
| "Created folder %s (ID %s) [Polled]", | ||
| sanitize_for_log(name), | ||
Check warningCode scanning / Prospector (reported by Codacy) Use lazy % formatting in logging functions (logging-fstring-interpolation) Warning
Use lazy % formatting in logging functions (logging-fstring-interpolation)
|
||
| grp["PK"], | ||
| ) | ||
| return str(grp["PK"]) | ||
|
|
@@ -1048,7 +1158,7 @@ | |
| else: | ||
| log.info( | ||
| "Folder %s – finished (%d new rules added)", | ||
| sanitize_for_log(folder_name), | ||
Check warningCode scanning / Prospector (reported by Codacy) Use lazy % formatting in logging functions (logging-fstring-interpolation) Warning
Use lazy % formatting in logging functions (logging-fstring-interpolation)
Check noticeCode scanning / Pylintpython3 (reported by Codacy) Use lazy % formatting in logging functions Note
Use lazy % formatting in logging functions
|
||
| len(filtered_hostnames), | ||
| ) | ||
| return True | ||
|
|
@@ -1224,11 +1334,11 @@ | |
| # Initial client for getting existing state AND processing folders | ||
| # Optimization: Reuse the same client session to keep TCP connections alive | ||
| with _api_client() as client: | ||
| # Check for API access problems first (401/403/404) | ||
| if not check_api_access(client, profile_id): | ||
| # Verify access and list existing folders in one request | ||
| existing_folders = verify_access_and_get_folders(client, profile_id) | ||
| if existing_folders is None: | ||
| return False | ||
|
|
||
| existing_folders = list_existing_folders(client, profile_id) | ||
| if not no_delete: | ||
| deletion_occurred = False | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.