-
Notifications
You must be signed in to change notification settings - Fork 1
⚡ Bolt: Optimize cache warm-up and fix validation bug #416
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 | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -680,6 +680,27 @@ | |||||||||||||||||||||||||||||||||||||||||||||||
| _rate_limit_lock = threading.Lock() # Protect _rate_limit_info updates | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| def _is_cache_fresh(url: str) -> bool: | ||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||
| Checks if the URL is in the persistent cache and within TTL. | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| This optimization allows skipping expensive DNS validation for | ||||||||||||||||||||||||||||||||||||||||||||||||
| content that is already known to be safe (validated at fetch time). | ||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||
| # Check in-memory cache first | ||||||||||||||||||||||||||||||||||||||||||||||||
| with _cache_lock: | ||||||||||||||||||||||||||||||||||||||||||||||||
| if url in _cache: | ||||||||||||||||||||||||||||||||||||||||||||||||
| return True | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+683
to
+694
|
||||||||||||||||||||||||||||||||||||||||||||||||
| # Check disk cache | ||||||||||||||||||||||||||||||||||||||||||||||||
| entry = _disk_cache.get(url) | ||||||||||||||||||||||||||||||||||||||||||||||||
| if entry: | ||||||||||||||||||||||||||||||||||||||||||||||||
| last_validated = entry.get("last_validated", 0) | ||||||||||||||||||||||||||||||||||||||||||||||||
| if time.time() - last_validated < CACHE_TTL_SECONDS: | ||||||||||||||||||||||||||||||||||||||||||||||||
| return True | ||||||||||||||||||||||||||||||||||||||||||||||||
| return False | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+683
to
+701
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| def get_cache_dir() -> Path: | ||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||
| Returns platform-specific cache directory for ctrld-sync. | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -1060,7 +1081,7 @@ | |||||||||||||||||||||||||||||||||||||||||||||||
| if any(c in _DANGEROUS_FOLDER_CHARS or c in _BIDI_CONTROL_CHARS for c in name): | ||||||||||||||||||||||||||||||||||||||||||||||||
| return False | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| # Security: Block path traversal attempts | ||||||||||||||||||||||||||||||||||||||||||||||||
Check noticeCode scanning / Pylintpython3 (reported by Codacy) Use lazy % formatting in logging functions Note
Use lazy % formatting in logging functions
|
||||||||||||||||||||||||||||||||||||||||||||||||
| # Check stripped name to prevent whitespace bypass (e.g. " . ") | ||||||||||||||||||||||||||||||||||||||||||||||||
| clean_name = name.strip() | ||||||||||||||||||||||||||||||||||||||||||||||||
| if clean_name in (".", ".."): | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -1073,14 +1094,14 @@ | |||||||||||||||||||||||||||||||||||||||||||||||
| return True | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| def validate_folder_data(data: Dict[str, Any], url: str) -> bool: | ||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||
| Validates folder JSON data structure and content. | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| Checks for required fields (name, action, rules), validates folder name | ||||||||||||||||||||||||||||||||||||||||||||||||
| and action type, and ensures rules are valid. Logs specific validation errors. | ||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||
| if not isinstance(data, dict): | ||||||||||||||||||||||||||||||||||||||||||||||||
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)
|
||||||||||||||||||||||||||||||||||||||||||||||||
| log.error( | ||||||||||||||||||||||||||||||||||||||||||||||||
| f"Invalid data from {sanitize_for_log(url)}: Root must be a JSON object." | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -1111,7 +1132,7 @@ | |||||||||||||||||||||||||||||||||||||||||||||||
| f"Invalid data from {sanitize_for_log(url)}: Invalid folder name (empty, unsafe characters, or non-printable)." | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||
| return False | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
Check warningCode scanning / Pylint (reported by Codacy) Line too long (108/100) Warning
Line too long (108/100)
Check warningCode scanning / Pylintpython3 (reported by Codacy) Line too long (108/100) Warning
Line too long (108/100)
|
||||||||||||||||||||||||||||||||||||||||||||||||
| # Validate 'rules' if present (must be a list) | ||||||||||||||||||||||||||||||||||||||||||||||||
| if "rules" in data and not isinstance(data["rules"], list): | ||||||||||||||||||||||||||||||||||||||||||||||||
| log.error(f"Invalid data from {sanitize_for_log(url)}: 'rules' must be a list.") | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -1131,19 +1152,22 @@ | |||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||
| return False | ||||||||||||||||||||||||||||||||||||||||||||||||
| if "rules" in rg: | ||||||||||||||||||||||||||||||||||||||||||||||||
| if not isinstance (rg["rules"], list): | ||||||||||||||||||||||||||||||||||||||||||||||||
| log. error ( | ||||||||||||||||||||||||||||||||||||||||||||||||
| f"Invalid data from {sanitize_for_log(url)} : rule_groups[fil].rules must be a list." | ||||||||||||||||||||||||||||||||||||||||||||||||
| if not isinstance(rg["rules"], list): | ||||||||||||||||||||||||||||||||||||||||||||||||
| log.error( | ||||||||||||||||||||||||||||||||||||||||||||||||
| f"Invalid data from {sanitize_for_log(url)}: rule_groups[{i}].rules must be a list." | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||
| return False | ||||||||||||||||||||||||||||||||||||||||||||||||
| # Ensure each rule within the group is an object (dict), | ||||||||||||||||||||||||||||||||||||||||||||||||
| # because later code treats each rule as a mapping (e.g., rule.get(...)). | ||||||||||||||||||||||||||||||||||||||||||||||||
| for j, rule in enumerate (rgi"rules"1): | ||||||||||||||||||||||||||||||||||||||||||||||||
| if not isinstance (rule, dict): | ||||||||||||||||||||||||||||||||||||||||||||||||
| log. error ( | ||||||||||||||||||||||||||||||||||||||||||||||||
| f"Invalid data from {sanitize_for_log(u rl)}: rule_groups[fiłl.rules[kił] must be an object." | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||
| return False | ||||||||||||||||||||||||||||||||||||||||||||||||
| # Ensure each rule within the group is an object (dict), | ||||||||||||||||||||||||||||||||||||||||||||||||
| # because later code treats each rule as a mapping (e.g., rule.get(...)). | ||||||||||||||||||||||||||||||||||||||||||||||||
| for j, rule in enumerate(rg["rules"]): | ||||||||||||||||||||||||||||||||||||||||||||||||
| if not isinstance(rule, dict): | ||||||||||||||||||||||||||||||||||||||||||||||||
| log.error( | ||||||||||||||||||||||||||||||||||||||||||||||||
| f"Invalid data from {sanitize_for_log(url)}: rule_groups[{i}].rules[{j}] must be an object." | ||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||
| return False | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| return True | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| # Lock to protect updates to _api_stats in multi-threaded contexts. | ||||||||||||||||||||||||||||||||||||||||||||||||
| # Without this, concurrent increments can lose updates because `+=` is not atomic. | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -1285,216 +1309,216 @@ | |||||||||||||||||||||||||||||||||||||||||||||||
| time.sleep(wait_time) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| def _gh_get(url: str) -> Dict: | ||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||
| Fetch blocklist data from URL with HTTP cache header support. | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| CACHING STRATEGY: | ||||||||||||||||||||||||||||||||||||||||||||||||
| 1. Check in-memory cache first (fastest) | ||||||||||||||||||||||||||||||||||||||||||||||||
| 2. Check disk cache and send conditional request (If-None-Match/If-Modified-Since) | ||||||||||||||||||||||||||||||||||||||||||||||||
| 3. If 304 Not Modified: reuse cached data (cache validation) | ||||||||||||||||||||||||||||||||||||||||||||||||
| 4. If 200 OK: download new data and update cache | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| SECURITY: Validates data structure regardless of cache source | ||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||
| global _cache_stats, _api_stats | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| # First check: Quick check without holding lock for long | ||||||||||||||||||||||||||||||||||||||||||||||||
| with _cache_lock: | ||||||||||||||||||||||||||||||||||||||||||||||||
| if url in _cache: | ||||||||||||||||||||||||||||||||||||||||||||||||
| _cache_stats["hits"] += 1 | ||||||||||||||||||||||||||||||||||||||||||||||||
| return _cache[url] | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| # Track that we're about to make a blocklist fetch | ||||||||||||||||||||||||||||||||||||||||||||||||
| with _cache_lock: | ||||||||||||||||||||||||||||||||||||||||||||||||
| _api_stats["blocklist_fetches"] += 1 | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| # Check disk cache for TTL-based hit or conditional request headers | ||||||||||||||||||||||||||||||||||||||||||||||||
| headers = {} | ||||||||||||||||||||||||||||||||||||||||||||||||
| cached_entry = _disk_cache.get(url) | ||||||||||||||||||||||||||||||||||||||||||||||||
| if cached_entry: | ||||||||||||||||||||||||||||||||||||||||||||||||
| last_validated = cached_entry.get("last_validated", 0) | ||||||||||||||||||||||||||||||||||||||||||||||||
| if time.time() - last_validated < CACHE_TTL_SECONDS: | ||||||||||||||||||||||||||||||||||||||||||||||||
| # Within TTL: return cached data directly without any HTTP request | ||||||||||||||||||||||||||||||||||||||||||||||||
| data = cached_entry["data"] | ||||||||||||||||||||||||||||||||||||||||||||||||
| with _cache_lock: | ||||||||||||||||||||||||||||||||||||||||||||||||
| _cache[url] = data | ||||||||||||||||||||||||||||||||||||||||||||||||
| _cache_stats["hits"] += 1 | ||||||||||||||||||||||||||||||||||||||||||||||||
| log.debug(f"Disk cache hit (within TTL) for {sanitize_for_log(url)}") | ||||||||||||||||||||||||||||||||||||||||||||||||
| return data | ||||||||||||||||||||||||||||||||||||||||||||||||
| # Beyond TTL: send conditional request using cached ETag/Last-Modified | ||||||||||||||||||||||||||||||||||||||||||||||||
| # Server returns 304 if content hasn't changed | ||||||||||||||||||||||||||||||||||||||||||||||||
| # NOTE: Cached values may be None if the server didn't send these headers. | ||||||||||||||||||||||||||||||||||||||||||||||||
| # httpx requires header values to be str/bytes, so we only add headers | ||||||||||||||||||||||||||||||||||||||||||||||||
| # when the cached value is truthy. | ||||||||||||||||||||||||||||||||||||||||||||||||
| etag = cached_entry.get("etag") | ||||||||||||||||||||||||||||||||||||||||||||||||
| if etag: | ||||||||||||||||||||||||||||||||||||||||||||||||
| headers["If-None-Match"] = etag | ||||||||||||||||||||||||||||||||||||||||||||||||
| last_modified = cached_entry.get("last_modified") | ||||||||||||||||||||||||||||||||||||||||||||||||
| if last_modified: | ||||||||||||||||||||||||||||||||||||||||||||||||
| headers["If-Modified-Since"] = last_modified | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| # Fetch data (or validate cache) | ||||||||||||||||||||||||||||||||||||||||||||||||
| # Explicitly let HTTPError propagate (no need to catch just to re-raise) | ||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||
| with _gh.stream("GET", url, headers=headers) as r: | ||||||||||||||||||||||||||||||||||||||||||||||||
| # Handle 304 Not Modified - cached data is still valid | ||||||||||||||||||||||||||||||||||||||||||||||||
| if r.status_code == 304: | ||||||||||||||||||||||||||||||||||||||||||||||||
| if cached_entry and "data" in cached_entry: | ||||||||||||||||||||||||||||||||||||||||||||||||
| log.debug(f"Cache validated (304) for {sanitize_for_log(url)}") | ||||||||||||||||||||||||||||||||||||||||||||||||
| _cache_stats["validations"] += 1 | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| # Update in-memory cache with validated data | ||||||||||||||||||||||||||||||||||||||||||||||||
| data = cached_entry["data"] | ||||||||||||||||||||||||||||||||||||||||||||||||
| with _cache_lock: | ||||||||||||||||||||||||||||||||||||||||||||||||
| _cache[url] = data | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| # Update timestamp in disk cache to track last validation | ||||||||||||||||||||||||||||||||||||||||||||||||
| cached_entry["last_validated"] = time.time() | ||||||||||||||||||||||||||||||||||||||||||||||||
| return data | ||||||||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||||||||
| # Shouldn't happen, but handle gracefully | ||||||||||||||||||||||||||||||||||||||||||||||||
| log.warning(f"Got 304 but no cached data for {sanitize_for_log(url)}, re-fetching") | ||||||||||||||||||||||||||||||||||||||||||||||||
| _cache_stats["errors"] += 1 | ||||||||||||||||||||||||||||||||||||||||||||||||
| # Close the original streaming response before retrying | ||||||||||||||||||||||||||||||||||||||||||||||||
| r.close() | ||||||||||||||||||||||||||||||||||||||||||||||||
| # Retry without conditional headers using streaming again so that | ||||||||||||||||||||||||||||||||||||||||||||||||
| # MAX_RESPONSE_SIZE and related protections still apply. | ||||||||||||||||||||||||||||||||||||||||||||||||
| headers = {} | ||||||||||||||||||||||||||||||||||||||||||||||||
| with _gh.stream("GET", url, headers=headers) as r_retry: | ||||||||||||||||||||||||||||||||||||||||||||||||
| r_retry.raise_for_status() | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| # 1. Check Content-Length header if present | ||||||||||||||||||||||||||||||||||||||||||||||||
| cl = r_retry.headers.get("Content-Length") | ||||||||||||||||||||||||||||||||||||||||||||||||
| if cl: | ||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||
| if int(cl) > MAX_RESPONSE_SIZE: | ||||||||||||||||||||||||||||||||||||||||||||||||
| raise ValueError( | ||||||||||||||||||||||||||||||||||||||||||||||||
| f"Response too large from {sanitize_for_log(url)} " | ||||||||||||||||||||||||||||||||||||||||||||||||
| f"({int(cl) / (1024 * 1024):.2f} MB)" | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||
| except ValueError as e: | ||||||||||||||||||||||||||||||||||||||||||||||||
| # Only catch the conversion error, let the size error propagate | ||||||||||||||||||||||||||||||||||||||||||||||||
| if "Response too large" in str(e): | ||||||||||||||||||||||||||||||||||||||||||||||||
| raise e | ||||||||||||||||||||||||||||||||||||||||||||||||
| log.warning( | ||||||||||||||||||||||||||||||||||||||||||||||||
| f"Malformed Content-Length header from {sanitize_for_log(url)}: {cl!r}. " | ||||||||||||||||||||||||||||||||||||||||||||||||
| "Falling back to streaming size check." | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| # 2. Stream and check actual size | ||||||||||||||||||||||||||||||||||||||||||||||||
| chunks = [] | ||||||||||||||||||||||||||||||||||||||||||||||||
| current_size = 0 | ||||||||||||||||||||||||||||||||||||||||||||||||
| for chunk in r_retry.iter_bytes(): | ||||||||||||||||||||||||||||||||||||||||||||||||
| current_size += len(chunk) | ||||||||||||||||||||||||||||||||||||||||||||||||
| if current_size > MAX_RESPONSE_SIZE: | ||||||||||||||||||||||||||||||||||||||||||||||||
| raise ValueError( | ||||||||||||||||||||||||||||||||||||||||||||||||
| f"Response too large from {sanitize_for_log(url)} " | ||||||||||||||||||||||||||||||||||||||||||||||||
| f"(> {MAX_RESPONSE_SIZE / (1024 * 1024):.2f} MB)" | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||
| chunks.append(chunk) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||
| data = json.loads(b"".join(chunks)) | ||||||||||||||||||||||||||||||||||||||||||||||||
| except json.JSONDecodeError as e: | ||||||||||||||||||||||||||||||||||||||||||||||||
| raise ValueError( | ||||||||||||||||||||||||||||||||||||||||||||||||
| f"Invalid JSON response from {sanitize_for_log(url)}" | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) from e | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| # Store cache headers for future conditional requests | ||||||||||||||||||||||||||||||||||||||||||||||||
| # ETag is preferred over Last-Modified (more reliable) | ||||||||||||||||||||||||||||||||||||||||||||||||
| etag = r_retry.headers.get("ETag") | ||||||||||||||||||||||||||||||||||||||||||||||||
| last_modified = r_retry.headers.get("Last-Modified") | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| # Update disk cache with new data and headers | ||||||||||||||||||||||||||||||||||||||||||||||||
| _disk_cache[url] = { | ||||||||||||||||||||||||||||||||||||||||||||||||
| "data": data, | ||||||||||||||||||||||||||||||||||||||||||||||||
| "etag": etag, | ||||||||||||||||||||||||||||||||||||||||||||||||
| "last_modified": last_modified, | ||||||||||||||||||||||||||||||||||||||||||||||||
| "fetched_at": time.time(), | ||||||||||||||||||||||||||||||||||||||||||||||||
| "last_validated": time.time(), | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| _cache_stats["misses"] += 1 | ||||||||||||||||||||||||||||||||||||||||||||||||
| return data | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| r.raise_for_status() | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| # Security: Validate Content-Type | ||||||||||||||||||||||||||||||||||||||||||||||||
| # Prevent processing of unexpected content types (e.g., HTML/XML from captive portals or attack sites) | ||||||||||||||||||||||||||||||||||||||||||||||||
| content_type = r.headers.get("Content-Type", "").lower() | ||||||||||||||||||||||||||||||||||||||||||||||||
| allowed_types = ["application/json", "text/json", "text/plain"] | ||||||||||||||||||||||||||||||||||||||||||||||||
| if not any(t in content_type for t in allowed_types): | ||||||||||||||||||||||||||||||||||||||||||||||||
| raise ValueError( | ||||||||||||||||||||||||||||||||||||||||||||||||
| f"Invalid Content-Type from {sanitize_for_log(url)}: {content_type}. " | ||||||||||||||||||||||||||||||||||||||||||||||||
| f"Expected one of: {', '.join(allowed_types)}" | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| # 1. Check Content-Length header if present | ||||||||||||||||||||||||||||||||||||||||||||||||
| cl = r.headers.get("Content-Length") | ||||||||||||||||||||||||||||||||||||||||||||||||
| if cl: | ||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||
| if int(cl) > MAX_RESPONSE_SIZE: | ||||||||||||||||||||||||||||||||||||||||||||||||
| raise ValueError( | ||||||||||||||||||||||||||||||||||||||||||||||||
| f"Response too large from {sanitize_for_log(url)} " | ||||||||||||||||||||||||||||||||||||||||||||||||
| f"({int(cl) / (1024 * 1024):.2f} MB)" | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||
| except ValueError as e: | ||||||||||||||||||||||||||||||||||||||||||||||||
| # Only catch the conversion error, let the size error propagate | ||||||||||||||||||||||||||||||||||||||||||||||||
| if "Response too large" in str(e): | ||||||||||||||||||||||||||||||||||||||||||||||||
| raise e | ||||||||||||||||||||||||||||||||||||||||||||||||
| log.warning( | ||||||||||||||||||||||||||||||||||||||||||||||||
| f"Malformed Content-Length header from {sanitize_for_log(url)}: {cl!r}. " | ||||||||||||||||||||||||||||||||||||||||||||||||
| "Falling back to streaming size check." | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| # 2. Stream and check actual size | ||||||||||||||||||||||||||||||||||||||||||||||||
| chunks = [] | ||||||||||||||||||||||||||||||||||||||||||||||||
| current_size = 0 | ||||||||||||||||||||||||||||||||||||||||||||||||
| # Optimization: Use 16KB chunks to reduce loop overhead/appends for large files | ||||||||||||||||||||||||||||||||||||||||||||||||
| for chunk in r.iter_bytes(chunk_size=16 * 1024): | ||||||||||||||||||||||||||||||||||||||||||||||||
| current_size += len(chunk) | ||||||||||||||||||||||||||||||||||||||||||||||||
| if current_size > MAX_RESPONSE_SIZE: | ||||||||||||||||||||||||||||||||||||||||||||||||
| raise ValueError( | ||||||||||||||||||||||||||||||||||||||||||||||||
| f"Response too large from {sanitize_for_log(url)} " | ||||||||||||||||||||||||||||||||||||||||||||||||
| f"(> {MAX_RESPONSE_SIZE / (1024 * 1024):.2f} MB)" | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||
| chunks.append(chunk) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||
| data = json.loads(b"".join(chunks)) | ||||||||||||||||||||||||||||||||||||||||||||||||
| except json.JSONDecodeError as e: | ||||||||||||||||||||||||||||||||||||||||||||||||
| raise ValueError( | ||||||||||||||||||||||||||||||||||||||||||||||||
| f"Invalid JSON response from {sanitize_for_log(url)}" | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) from e | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| # Store cache headers for future conditional requests | ||||||||||||||||||||||||||||||||||||||||||||||||
| # ETag is preferred over Last-Modified (more reliable) | ||||||||||||||||||||||||||||||||||||||||||||||||
| etag = r.headers.get("ETag") | ||||||||||||||||||||||||||||||||||||||||||||||||
| last_modified = r.headers.get("Last-Modified") | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| # Update disk cache with new data and headers | ||||||||||||||||||||||||||||||||||||||||||||||||
| _disk_cache[url] = { | ||||||||||||||||||||||||||||||||||||||||||||||||
| "data": data, | ||||||||||||||||||||||||||||||||||||||||||||||||
| "etag": etag, | ||||||||||||||||||||||||||||||||||||||||||||||||
| "last_modified": last_modified, | ||||||||||||||||||||||||||||||||||||||||||||||||
| "fetched_at": time.time(), | ||||||||||||||||||||||||||||||||||||||||||||||||
| "last_validated": time.time(), | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| _cache_stats["misses"] += 1 | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| except httpx.HTTPStatusError as e: | ||||||||||||||||||||||||||||||||||||||||||||||||
| # Re-raise with original exception (don't catch and re-raise) | ||||||||||||||||||||||||||||||||||||||||||||||||
| raise | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| # Double-checked locking: Check again after fetch to avoid duplicate fetches | ||||||||||||||||||||||||||||||||||||||||||||||||
| # If another thread already cached it while we were fetching, use theirs | ||||||||||||||||||||||||||||||||||||||||||||||||
| # for consistency (return _cache[url] instead of data to ensure single source of truth) | ||||||||||||||||||||||||||||||||||||||||||||||||
| with _cache_lock: | ||||||||||||||||||||||||||||||||||||||||||||||||
| if url not in _cache: | ||||||||||||||||||||||||||||||||||||||||||||||||
| _cache[url] = data | ||||||||||||||||||||||||||||||||||||||||||||||||
| return _cache[url] | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| def check_api_access(client: httpx.Client, profile_id: str) -> bool: | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -1560,125 +1584,125 @@ | |||||||||||||||||||||||||||||||||||||||||||||||
| return {} | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| def verify_access_and_get_folders( | ||||||||||||||||||||||||||||||||||||||||||||||||
| client: httpx.Client, profile_id: str | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) -> 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 cause the operation to fail unexpectedly. | ||||||||||||||||||||||||||||||||||||||||||||||||
| 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: | ||||||||||||||||||||||||||||||||||||||||||||||||
| 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") | ||||||||||||||||||||||||||||||||||||||||||||||||
| # Skip entries with empty or None values for required fields | ||||||||||||||||||||||||||||||||||||||||||||||||
| if not name or not pk: | ||||||||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| pk_str = str(pk) | ||||||||||||||||||||||||||||||||||||||||||||||||
| if not validate_folder_id(pk_str): | ||||||||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| result[str(name).strip()] = pk_str | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| return result | ||||||||||||||||||||||||||||||||||||||||||||||||
| except (ValueError, TypeError, AttributeError) as err: | ||||||||||||||||||||||||||||||||||||||||||||||||
| # As a final safeguard, catch any remaining parsing/shape errors so | ||||||||||||||||||||||||||||||||||||||||||||||||
| # that a malformed response cannot crash the caller. | ||||||||||||||||||||||||||||||||||||||||||||||||
| log.error("Failed to parse folders data: %s", sanitize_for_log(err)) | ||||||||||||||||||||||||||||||||||||||||||||||||
| return None | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| except httpx.HTTPStatusError as e: | ||||||||||||||||||||||||||||||||||||||||||||||||
| 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}" | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||
| log.critical( | ||||||||||||||||||||||||||||||||||||||||||||||||
| f"{Colors.FAIL} Please check your token at: https://controld.com/account/manage-account{Colors.ENDC}" | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||
| elif code == 403: | ||||||||||||||||||||||||||||||||||||||||||||||||
| log.critical( | ||||||||||||||||||||||||||||||||||||||||||||||||
| "%s🚫 Access Denied: Token lacks permission for " | ||||||||||||||||||||||||||||||||||||||||||||||||
| "Profile %s.%s", | ||||||||||||||||||||||||||||||||||||||||||||||||
| Colors.FAIL, | ||||||||||||||||||||||||||||||||||||||||||||||||
| sanitize_for_log(profile_id), | ||||||||||||||||||||||||||||||||||||||||||||||||
| Colors.ENDC, | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||
| elif code == 404: | ||||||||||||||||||||||||||||||||||||||||||||||||
| log.critical( | ||||||||||||||||||||||||||||||||||||||||||||||||
| f"{Colors.FAIL}🔍 Profile Not Found: The ID '{sanitize_for_log(profile_id)}' does not exist.{Colors.ENDC}" | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||
| 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 err: | ||||||||||||||||||||||||||||||||||||||||||||||||
| if attempt == MAX_RETRIES - 1: | ||||||||||||||||||||||||||||||||||||||||||||||||
| log.error( | ||||||||||||||||||||||||||||||||||||||||||||||||
| "Network error during access verification: %s", | ||||||||||||||||||||||||||||||||||||||||||||||||
| sanitize_for_log(err), | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||
| return None | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| wait_time = RETRY_DELAY * (2**attempt) | ||||||||||||||||||||||||||||||||||||||||||||||||
| log.warning( | ||||||||||||||||||||||||||||||||||||||||||||||||
| "Request failed (attempt %d/%d). Retrying in %ds...", | ||||||||||||||||||||||||||||||||||||||||||||||||
| attempt + 1, | ||||||||||||||||||||||||||||||||||||||||||||||||
| MAX_RETRIES, | ||||||||||||||||||||||||||||||||||||||||||||||||
| wait_time, | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||
| time.sleep(wait_time) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| def get_all_existing_rules( | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -1788,6 +1812,11 @@ | |||||||||||||||||||||||||||||||||||||||||||||||
| # OPTIMIZATION: Combine validation (DNS) and fetching (HTTP) in one task | ||||||||||||||||||||||||||||||||||||||||||||||||
| # to allow validation latency to be parallelized. | ||||||||||||||||||||||||||||||||||||||||||||||||
| def _validate_and_fetch(url: str): | ||||||||||||||||||||||||||||||||||||||||||||||||
| # Optimization: Skip DNS validation if cache is fresh | ||||||||||||||||||||||||||||||||||||||||||||||||
| # This saves blocking I/O for known-good content | ||||||||||||||||||||||||||||||||||||||||||||||||
| if _is_cache_fresh(url): | ||||||||||||||||||||||||||||||||||||||||||||||||
| return _gh_get(url) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1817
to
+1818
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. While this optimization aims to improve startup performance by skipping DNS validation if the cache is fresh, it introduces a high-severity Server-Side Request Forgery (SSRF) vulnerability. The |
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| if validate_folder_url(url): | ||||||||||||||||||||||||||||||||||||||||||||||||
| return _gh_get(url) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1815
to
1821
|
||||||||||||||||||||||||||||||||||||||||||||||||
| # Optimization: Skip DNS validation if cache is fresh | |
| # This saves blocking I/O for known-good content | |
| if _is_cache_fresh(url): | |
| return _gh_get(url) | |
| if validate_folder_url(url): | |
| return _gh_get(url) | |
| # Optimization: Skip DNS validation if cache is fresh. | |
| # We still route through fetch_folder_data so that: | |
| # - Cached entries are loaded via the same path as normal sync. | |
| # - Schema validation (via validate_folder_data) is consistently applied | |
| # whether data comes from disk cache or the network. | |
| if _is_cache_fresh(url): | |
| # This should load and validate from cache without forcing a re-download. | |
| # Any validation errors will surface as exceptions and be logged by the | |
| # outer warm_up_cache loop. | |
| fetch_folder_data(url) | |
| return None | |
| # For non-fresh entries, validate the URL (DNS/format) first to avoid | |
| # unnecessary network calls, then fetch+validate the folder data. | |
| if validate_folder_url(url): | |
| fetch_folder_data(url) |
Copilot
AI
Feb 22, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
warm_up_cache now skips validate_folder_url entirely when _is_cache_fresh(url) is true. Since load_disk_cache only sanitizes structure (it does not validate that cache keys are https URLs with a hostname), a corrupted/hand-edited disk cache could cause an unsafe/non-https URL to be accepted from cache without any URL-level validation. Consider still performing lightweight URL/scheme/hostname validation (without DNS) before returning _gh_get(url), or splitting validate_folder_url into a cheap parse/scheme check plus an optional DNS-resolution check so only the DNS part is skipped.
Check warning
Code scanning / Prospector (reported by Codacy)
Use lazy % formatting in logging functions (logging-fstring-interpolation) Warning
Check notice
Code scanning / Pylintpython3 (reported by Codacy)
Use lazy % formatting in logging functions Note
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,118 @@ | ||||||
| """ | ||||||
| Tests for the warm_up_cache optimization. | ||||||
|
|
||||||
| This module verifies that warm_up_cache skips DNS validation when the | ||||||
| URL is already fresh in the disk cache. | ||||||
| """ | ||||||
| import unittest | ||||||
| from unittest.mock import patch, MagicMock | ||||||
Check warningCode scanning / Prospector (reported by Codacy) Unused MagicMock imported from unittest.mock (unused-import) Warning test
Unused MagicMock imported from unittest.mock (unused-import)
Check noticeCode scanning / Pylint (reported by Codacy) Unused MagicMock imported from unittest.mock Note test
Unused MagicMock imported from unittest.mock
Check noticeCode scanning / Pylintpython3 (reported by Codacy) Unused MagicMock imported from unittest.mock Note test
Unused MagicMock imported from unittest.mock
|
||||||
| from unittest.mock import patch, MagicMock | |
| from unittest.mock import patch |
Check warning
Code scanning / Pylint (reported by Codacy)
Missing class docstring Warning test
Check warning
Code scanning / Pylintpython3 (reported by Codacy)
Missing class docstring Warning test
Check warning
Code scanning / Prospector (reported by Codacy)
Import "import main" should be placed at the top of the module (wrong-import-position) Warning test
Check warning
Code scanning / Pylintpython3 (reported by Codacy)
Import outside toplevel (main) Warning test
Check warning
Code scanning / Prospector (reported by Codacy)
Access to a protected member _cache of a client class (protected-access) Warning test
Check notice
Code scanning / Pylint (reported by Codacy)
Access to a protected member _cache of a client class Note test
Check notice
Code scanning / Pylintpython3 (reported by Codacy)
Access to a protected member _cache of a client class Note test
Check warning
Code scanning / Prospector (reported by Codacy)
Access to a protected member _disk_cache of a client class (protected-access) Warning test
Check notice
Code scanning / Pylint (reported by Codacy)
Access to a protected member _disk_cache of a client class Note test
Check notice
Code scanning / Pylintpython3 (reported by Codacy)
Access to a protected member _disk_cache of a client class Note test
Check warning
Code scanning / Prospector (reported by Codacy)
Access to a protected member _cache of a client class (protected-access) Warning test
Check notice
Code scanning / Pylint (reported by Codacy)
Access to a protected member _cache of a client class Note test
Check notice
Code scanning / Pylintpython3 (reported by Codacy)
Access to a protected member _cache of a client class Note test
Check warning
Code scanning / Prospector (reported by Codacy)
Access to a protected member _disk_cache of a client class (protected-access) Warning test
Check notice
Code scanning / Pylint (reported by Codacy)
Access to a protected member _disk_cache of a client class Note test
Check notice
Code scanning / Pylintpython3 (reported by Codacy)
Access to a protected member _disk_cache of a client class Note test
Check warning
Code scanning / Prospector (reported by Codacy)
Access to a protected member _cache_lock of a client class (protected-access) Warning test
Check notice
Code scanning / Pylint (reported by Codacy)
Access to a protected member _cache_lock of a client class Note test
Check notice
Code scanning / Pylintpython3 (reported by Codacy)
Access to a protected member _cache_lock of a client class Note test
Check warning
Code scanning / Prospector (reported by Codacy)
Access to a protected member _disk_cache of a client class (protected-access) Warning test
Check notice
Code scanning / Pylint (reported by Codacy)
Access to a protected member _disk_cache of a client class Note test
Check notice
Code scanning / Pylintpython3 (reported by Codacy)
Access to a protected member _disk_cache of a client class Note test
Check warning
Code scanning / Prospector (reported by Codacy)
Access to a protected member _cache_lock of a client class (protected-access) Warning test
Check notice
Code scanning / Pylint (reported by Codacy)
Access to a protected member _cache_lock of a client class Note test
Check notice
Code scanning / Pylintpython3 (reported by Codacy)
Access to a protected member _cache_lock of a client class Note test
Check warning
Code scanning / Prospector (reported by Codacy)
Access to a protected member _disk_cache of a client class (protected-access) Warning test
Check notice
Code scanning / Pylint (reported by Codacy)
Access to a protected member _disk_cache of a client class Note test
Check notice
Code scanning / Pylintpython3 (reported by Codacy)
Access to a protected member _disk_cache of a client class Note test
Check warning
Code scanning / Prospector (reported by Codacy)
Access to a protected member _disk_cache of a client class (protected-access) Warning test
Check notice
Code scanning / Pylint (reported by Codacy)
Access to a protected member _disk_cache of a client class Note test
Check notice
Code scanning / Pylintpython3 (reported by Codacy)
Access to a protected member _disk_cache of a client class Note test
Check warning
Code scanning / Prospector (reported by Codacy)
expected 2 blank lines after class or function definition, found 1 (E305) Warning test
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Check notice
Code scanning / Remark-lint (reported by Codacy)
Warn when references to undefined definitions are found. Note