Spaces:
Sleeping
Sleeping
| """ | |
| API Comparator Module | |
| This module provides the Streamlit-based web interface for comparing API responses. | |
| It implements the UI components, API request handling, and response comparison functionality. | |
| """ | |
| import difflib | |
| import json | |
| from json.decoder import JSONDecodeError | |
| import streamlit as st | |
| from requests.exceptions import RequestException | |
| from utility import compare_responses, json_line_diff, USER_CREDENTIALS | |
| from utility import validate_urls, validate_json_inputs, parse_json_input, fetch_api_responses | |
| from utils.logger import structlog | |
| log = structlog.get_logger() | |
| st.set_page_config(page_title="ARC", layout="wide") | |
| # Custom CSS including download buttons | |
| CUSTOM_CSS = """ | |
| div.stButton > button:first-child, div.stDownloadButton > button:first-child { | |
| background-color: #4CAF50; | |
| color: white; | |
| border-radius: 8px; | |
| border: none; | |
| padding: 10px 24px; | |
| text-align: center; | |
| text-decoration: none; | |
| display: inline-block; | |
| font-size: 16px; | |
| margin: 4px 2px; | |
| cursor: pointer; | |
| transition-duration: 0.4s; | |
| } | |
| div.stButton > button:first-child:hover, | |
| div.stDownloadButton > button:first-child:hover { | |
| background-color: #45a049; | |
| } | |
| """ | |
| # Apply custom CSS | |
| st.markdown(CUSTOM_CSS, unsafe_allow_html=True) | |
| # Move logo to upper right corner with smaller size | |
| _, logo_col = st.columns([9, 1]) | |
| with logo_col: | |
| st.image("TD-logo.jpeg", width=100) | |
| # Streamlit UI | |
| st.title("API Response Comparator") | |
| col1, col2 = st.columns(2) | |
| def get_supported_methods(): | |
| """Return list of supported HTTP methods.""" | |
| return ["GET", "POST", "PUT", "DELETE", "PATCH"] | |
| def login(): | |
| """Handle user login through Streamlit interface.""" | |
| st.title("Login") | |
| username = st.text_input("Username") | |
| password = st.text_input("Password", type="password") | |
| if st.button("Login"): | |
| if username in USER_CREDENTIALS and USER_CREDENTIALS[username] == password: | |
| st.session_state.logged_in = True | |
| st.session_state.username = username | |
| log.info("User logged in successfully", username=username) | |
| st.success(f"Welcome, {username}!") | |
| st.rerun() | |
| else: | |
| log.warning("Failed login attempt", username=username) | |
| st.error("Invalid username or password") | |
| if "logged_in" not in st.session_state: | |
| st.session_state.logged_in = False | |
| if not st.session_state.logged_in: | |
| login() | |
| st.stop() | |
| # Set defaults before widgets | |
| defaults = { | |
| "api1_url": "", | |
| "api1_method": "GET", | |
| "api1_payload": "{}", | |
| "api2_url": "", | |
| "api2_method": "GET", | |
| "api2_payload": "{}", | |
| } | |
| with col1: | |
| st.header("API 1") | |
| api1_url = st.text_input("Enter API 1 URL:", key="api1_url", value="") | |
| api1_method = st.selectbox("Select API 1 Method:", get_supported_methods(), key="api1_method") | |
| api1_payload = st.text_area( | |
| "Enter API 1 Payload (JSON format):", | |
| key="api1_payload", | |
| value="{}" | |
| ) | |
| api1_headers = st.text_area( | |
| "Enter API 1 Headers (JSON format):", | |
| key="api1_headers", | |
| value="{}" | |
| ) | |
| with col2: | |
| st.header("API 2") | |
| api2_url = st.text_input("Enter API 2 URL:", key="api2_url", value="") | |
| api2_method = st.selectbox("Select API 2 Method:", get_supported_methods(), key="api2_method") | |
| api2_payload = st.text_area( | |
| "Enter API 2 Payload (JSON format):", | |
| key="api2_payload", value="{}" | |
| ) | |
| api2_headers = st.text_area( | |
| "Enter API 2 Headers (JSON format):", | |
| key="api2_headers", | |
| value="{}" | |
| ) | |
| # Add diff view selection | |
| view_mode = st.radio( | |
| "Comparison View Mode:", | |
| ["Line-by-line Diff", | |
| "Tree (DeepDiff)"], | |
| horizontal=True, | |
| index=0 | |
| ) # Setting index=0 makes Line-by-line the default | |
| # Place Compare and Clear buttons in parallel | |
| button_col1, button_col2 = st.columns([1, 1]) | |
| with button_col1: | |
| compare_clicked = st.button("Compare APIs") | |
| if compare_clicked: | |
| try: | |
| log.info("Starting API comparison", | |
| api1_url=api1_url, | |
| api2_url=api2_url) | |
| # Input validation | |
| try: | |
| validate_urls(api1_url, api2_url) | |
| validate_json_inputs( | |
| (api1_payload, "API 1 Payload"), | |
| (api2_payload, "API 2 Payload"), | |
| (api1_headers, "API 1 Headers"), | |
| (api2_headers, "API 2 Headers") | |
| ) | |
| except ValueError as e: | |
| log.error("Validation error", error=str(e)) | |
| st.error(str(e)) | |
| st.stop() | |
| # Parse and validate input JSON | |
| try: | |
| payload1 = parse_json_input(api1_payload, "API 1 Payload") | |
| payload2 = parse_json_input(api2_payload, "API 2 Payload") | |
| headers1 = parse_json_input(api1_headers, "API 1 Headers") | |
| headers2 = parse_json_input(api2_headers, "API 2 Headers") | |
| except JSONDecodeError as e: | |
| log.error("JSON parsing error", error=str(e)) | |
| st.error(f"Invalid JSON format: {e}") | |
| st.stop() | |
| # Fetch API responses | |
| try: | |
| (response1, execution_time_api1), (response2, execution_time_api2) = fetch_api_responses( | |
| api1_url, api1_method, payload1, headers1, | |
| api2_url, api2_method, payload2, headers2 | |
| ) | |
| except RequestException as e: | |
| log.error("API request failed", error=str(e)) | |
| st.error(f"API request failed: {e}") | |
| st.stop() | |
| except Exception as e: | |
| log.error("Unexpected error in API request", error=str(e)) | |
| st.error(f"An unexpected error occurred: {e}") | |
| st.stop() | |
| # Parse JSON responses | |
| try: | |
| json1 = response1.json() | |
| log.info("API 1 response parsed as JSON") | |
| except JSONDecodeError as e: | |
| log.error("API 1 invalid JSON response", error=str(e)) | |
| st.error(f"API 1 did not return valid JSON: {e}") | |
| st.stop() | |
| try: | |
| json2 = response2.json() | |
| log.info("API 2 response parsed as JSON") | |
| except JSONDecodeError as e: | |
| log.error("API 2 invalid JSON response", error=str(e)) | |
| st.error(f"API 2 did not return valid JSON: {e}") | |
| st.stop() | |
| # Compare responses | |
| try: | |
| diff_tree = compare_responses(json1, json2, view='tree') | |
| diff_text = compare_responses(json1, json2, view='text') | |
| _html_diff = json_line_diff(json1, json2) | |
| log.info("Generated comparison results", | |
| view_mode=view_mode) | |
| except ValueError as e: | |
| log.error("Comparison error", error=str(e)) | |
| st.error(f"Error comparing responses: {e}") | |
| st.stop() | |
| # Show results and handle downloads | |
| try: | |
| # Display successful API calls | |
| st.subheader("API Results") | |
| col_api1, col_api2 = st.columns(2) | |
| with col_api1: | |
| st.success( | |
| f"API 1 call executed in {execution_time_api1:.2f} seconds with status code {response1.status_code}.") | |
| st.markdown("**API 1 Result**") | |
| st.json(json1) | |
| st.download_button( | |
| label="Download API 1 Result", | |
| data=json.dumps(json1, indent=4), | |
| file_name="api1_result.json", | |
| mime="application/json", | |
| key="download_api1_result" | |
| ) | |
| with col_api2: | |
| st.success( | |
| f"API 2 call executed in {execution_time_api2:.2f} seconds with status code {response2.status_code}.") | |
| st.markdown("**API 2 Result**") | |
| st.json(json2) | |
| st.download_button( | |
| label="Download API 2 Result", | |
| data=json.dumps(json2, indent=4), | |
| file_name="api2_result.json", | |
| mime="application/json", | |
| key="download_api2_result" | |
| ) | |
| st.subheader("Comparison Result") | |
| if view_mode == "Tree (DeepDiff)": | |
| if isinstance(diff_tree, str): | |
| st.text(diff_tree) | |
| else: | |
| st.json(diff_tree) | |
| elif view_mode == "Line-by-line Diff": | |
| if _html_diff.strip() == '': | |
| st.success("Both responses are identical.") | |
| else: | |
| st.markdown(_html_diff, unsafe_allow_html=True) | |
| # Prepare download data based on view mode | |
| _download_label = "Download Diff as Text" | |
| if view_mode == "Line-by-line Diff": | |
| json1_str = json.dumps(json1, indent=4, sort_keys=True).splitlines() | |
| json2_str = json.dumps(json2, indent=4, sort_keys=True).splitlines() | |
| download_data = '\n'.join(difflib.unified_diff(json1_str, json2_str, lineterm='')) | |
| else: | |
| download_data = str(diff_text) | |
| st.download_button( | |
| label=_download_label, | |
| data=download_data, | |
| file_name="diff_result.txt", | |
| mime="text/plain", | |
| key=f"download_diff_Text_{view_mode.replace(' ', '_')}" | |
| ) | |
| log.info("Successfully displayed and prepared comparison results") | |
| except (ValueError, AttributeError) as e: | |
| log.error("Error displaying results", error=str(e)) | |
| st.error(f"Error displaying results: {e}") | |
| st.stop() | |
| except Exception as e: | |
| log.error("Unexpected error in comparison", error=str(e)) | |
| st.error(f"An unexpected error occurred during comparison: {e}") | |
| st.stop() | |