File size: 9,941 Bytes
2d9b352
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a7f6f38
 
2d9b352
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
"""
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",
}


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