Spaces:
Sleeping
Sleeping
| """ | |
| API Comparator Utility Module | |
| This module provides core functionality for the API Comparator service including: | |
| - API request handling and response fetching | |
| - JSON validation and comparison | |
| - User authentication utilities | |
| - Response diff generation in various formats | |
| """ | |
| import asyncio | |
| import difflib | |
| import html | |
| import json | |
| import ssl | |
| from dataclasses import dataclass | |
| from datetime import datetime | |
| from typing import Tuple, Dict | |
| from typing import Union, Any | |
| from urllib.parse import urlparse | |
| import aiohttp | |
| import structlog | |
| from aiohttp.client_exceptions import ClientSSLError | |
| from deepdiff import DeepDiff | |
| log = structlog.get_logger() | |
| # User credentials (in real app, this would be in a secure database) | |
| USER_CREDENTIALS = { | |
| "user-1": "passWord-123", # For testing only | |
| "user1": "user-1", | |
| } | |
| class ApiRequest: | |
| """Configuration class for API requests. | |
| Attributes: | |
| url: The API endpoint URL | |
| method: HTTP method to use (GET, POST, etc) | |
| payload: Request payload data | |
| headers: Request headers | |
| timeout: Request timeout in seconds | |
| max_retries: Maximum number of retry attempts | |
| """ | |
| url: str | |
| method: str | |
| payload: Dict | |
| headers: Dict | |
| timeout: int = 30 | |
| max_retries: int = 3 | |
| verify_ssl: bool = True | |
| async def fetch_api_response( | |
| request: ApiRequest, | |
| semaphore: asyncio.Semaphore | |
| ) -> Tuple[Dict, float]: | |
| """Fetch response from a single API with retries and optional SSL verification.""" | |
| start_time = datetime.now() | |
| # Optional SSL context to ignore certificate verification | |
| ssl_context = None | |
| if not getattr(request, 'verify_ssl', True): | |
| ssl_context = ssl.create_default_context() | |
| ssl_context.check_hostname = False | |
| ssl_context.verify_mode = ssl.CERT_NONE | |
| async with semaphore: | |
| for attempt in range(1, request.max_retries + 1): | |
| try: | |
| async with aiohttp.ClientSession() as session: | |
| async with session.request( | |
| method=request.method, | |
| url=request.url, | |
| json=request.payload if request.method in ['POST', 'PUT', 'PATCH'] else None, | |
| params=request.payload if request.method == 'GET' else None, | |
| headers=request.headers, | |
| timeout=request.timeout, | |
| ssl=ssl_context # 🔐 This handles the certificate issue | |
| ) as response: | |
| json_response = await response.json() | |
| execution_time = (datetime.now() - start_time).total_seconds() | |
| return { | |
| 'status': response.status, | |
| 'data': json_response | |
| }, execution_time | |
| except ClientSSLError as ssl_err: | |
| log.error("SSL Certificate Verification Failed", error=str(ssl_err), url=request.url) | |
| raise ssl_err | |
| except Exception as e: | |
| if attempt < request.max_retries: | |
| log.warning("Async API call failed, retrying", | |
| url=request.url, | |
| method=request.method, | |
| attempt=attempt, | |
| error=str(e)) | |
| await asyncio.sleep(attempt) | |
| else: | |
| log.error("Async API call failed after all retries", | |
| url=request.url, | |
| method=request.method, | |
| attempts=request.max_retries, | |
| error=str(e)) | |
| raise | |
| async def fetch_api_responses( | |
| url1: str, | |
| method1: str, | |
| payload1: Dict, | |
| headers1: Dict, | |
| url2: str, | |
| method2: str, | |
| payload2: Dict, | |
| headers2: Dict, | |
| timeout: int = 30 | |
| ) -> Tuple[Tuple[aiohttp.ClientResponse, float], Tuple[aiohttp.ClientResponse, float]]: | |
| """Fetch responses from both APIs concurrently""" | |
| log.info("Starting API calls", | |
| api1={"url": url1, "method": method1}, | |
| api2={"url": url2, "method": method2}) | |
| # Create request objects | |
| request1 = ApiRequest(url1, method1, payload1, headers1, timeout) | |
| request2 = ApiRequest(url2, method2, payload2, headers2, timeout) | |
| # Use semaphore to limit concurrent connections | |
| semaphore = asyncio.Semaphore(2) | |
| try: | |
| # Fetch both responses concurrently | |
| responses = await asyncio.gather( | |
| fetch_api_response(request1, semaphore), | |
| fetch_api_response(request2, semaphore) | |
| ) | |
| return responses[0], responses[1] | |
| except Exception as e: | |
| log.error("Error fetching API responses", error=str(e)) | |
| raise | |
| def parse_json_input(input_str: str, label: str) -> Dict: | |
| """Parse JSON input, return empty dict for empty strings""" | |
| if not input_str.strip(): | |
| return {} | |
| try: | |
| return json.loads(input_str) | |
| except json.JSONDecodeError as e: | |
| raise ValueError(f"{label} is not valid JSON: {str(e)}") from e | |
| def validate_urls(*urls: str) -> None: | |
| """Validate that URLs are properly formatted and not identical""" | |
| url_set = set() | |
| for url in urls: | |
| try: | |
| parsed = urlparse(url) | |
| if not all([parsed.scheme, parsed.netloc]): | |
| raise ValueError(f"'{url}' is not a valid URL") | |
| url_set.add(url) | |
| except Exception as e: | |
| raise ValueError(f"'{url}' is not a valid URL") from e | |
| if len(url_set) < len(urls): | |
| raise ValueError("API URLs cannot be identical") | |
| def validate_json_inputs(*inputs: Tuple[str, str]) -> None: | |
| """Validate that all inputs are valid JSON""" | |
| for input_str, name in inputs: | |
| try: | |
| if input_str.strip(): # Only try to parse non-empty strings | |
| json.loads(input_str) | |
| except json.JSONDecodeError as e: | |
| raise ValueError(f"Invalid JSON in {name}: {str(e)}") from e | |
| def compare_responses(json1: Any, json2: Any, view: str = 'tree') -> Union[Dict, str]: | |
| """Compare two JSON responses and return the differences""" | |
| try: | |
| diff_result = DeepDiff(json1, json2, ignore_order=True) | |
| if not diff_result: | |
| return {} | |
| if view == 'tree': | |
| # Convert DeepDiff to a plain dict that's JSON serializable | |
| diff_dict = {} | |
| for change_type, changes in diff_result.items(): | |
| if isinstance(changes, dict): | |
| diff_dict[change_type] = {} | |
| for path, value in changes.items(): | |
| if hasattr(value, '_values'): | |
| diff_dict[change_type][str(path)] = list(value._values) | |
| else: | |
| diff_dict[change_type][str(path)] = str(value) | |
| else: | |
| diff_dict[change_type] = str(changes) | |
| return diff_dict | |
| # Convert DeepDiff to readable text format | |
| text_diff = [] | |
| for change_type, changes in diff_result.items(): | |
| text_diff.append(f"\n{change_type}:") | |
| if isinstance(changes, dict): | |
| for path, value in changes.items(): | |
| text_diff.append(f" {path}: {value}") | |
| else: | |
| text_diff.append(f" {changes}") | |
| return "\n".join(text_diff) | |
| except Exception as e: | |
| return {"error": f"Error comparing responses: {str(e)}"} | |
| def json_line_diff(json1: Any, json2: Any) -> str: | |
| """Generate a line-by-line HTML diff of two JSON objects.""" | |
| try: | |
| # Convert JSON to formatted strings | |
| str1 = json.dumps(json1, indent=4, sort_keys=True).splitlines() | |
| str2 = json.dumps(json2, indent=4, sort_keys=True).splitlines() | |
| # Generate diff and convert to HTML | |
| diff_lines = [] | |
| for line in difflib.unified_diff(str1, str2, lineterm=''): | |
| if line.startswith('+'): | |
| diff_lines.append(f'<span style="color: green;">{html.escape(line)}</span>') | |
| elif line.startswith('-'): | |
| diff_lines.append(f'<span style="color: red;">{html.escape(line)}</span>') | |
| else: | |
| diff_lines.append(html.escape(line)) | |
| return '<br>'.join(diff_lines) | |
| except Exception as e: | |
| return f"Error generating line diff: {str(e)}" | |
| def highlight_json_diff(json1: Any, json2: Any) -> Tuple[str, str]: | |
| """Highlight differences between two JSON objects and return HTML formatted strings.""" | |
| try: | |
| # Convert both objects to formatted strings | |
| str1 = json.dumps(json1, indent=4, sort_keys=True) | |
| str2 = json.dumps(json2, indent=4, sort_keys=True) | |
| # Split into lines | |
| lines1 = str1.splitlines() | |
| lines2 = str2.splitlines() | |
| # Get differences | |
| diff = difflib.SequenceMatcher(None, lines1, lines2) | |
| # Format the differences | |
| html1 = [] | |
| html2 = [] | |
| for tag, i1, i2, j1, j2 in diff.get_opcodes(): | |
| if tag == 'equal': | |
| # Add unchanged lines | |
| html1.extend(html.escape(line) for line in lines1[i1:i2]) | |
| html2.extend(html.escape(line) for line in lines2[j1:j2]) | |
| else: | |
| # Add changed lines with highlighting | |
| html1.extend(f'<span class="diff-highlight-remove">{html.escape(line)}</span>' | |
| for line in lines1[i1:i2]) | |
| html2.extend(f'<span class="diff-highlight-add">{html.escape(line)}</span>' | |
| for line in lines2[j1:j2]) | |
| return '\n'.join(html1), '\n'.join(html2) | |
| except Exception as e: | |
| return str(e), str(e) | |