Demo / app.py
Juggernaut1397's picture
Update app.py
c617332 verified
import requests
import yaml
import json
import zipfile
import io
import tempfile
from identityNow import handle_identitynow_call
from okta import handle_okta_call
from iiq import handle_iiq_call
from utils import extract_path_params, extract_query_params
import gradio as gr
import os
from dotenv import load_dotenv
from pathlib import Path
from api_schema_generatorV5 import ApiSchemaGeneratorV5
# Load environment variables
script_dir = Path(__file__).resolve().parent
env_path = script_dir / '.env'
load_dotenv(dotenv_path=env_path)
def fetch_api_endpoints_yaml(spec_url):
try:
response = requests.get(spec_url)
response.raise_for_status()
content = response.text
api_spec = yaml.safe_load(content)
except Exception as e:
print(f"Error fetching/parsing YAML spec from {spec_url}: {e}")
return {}
endpoints = {}
if "paths" not in api_spec:
print("No endpoints found in the specification.")
return {}
valid_methods = ['get', 'post'] # Only process GET and POST endpoints
for path, methods in api_spec["paths"].items():
endpoints[path] = {}
if not methods or not isinstance(methods, dict):
continue
common_params = methods.get("parameters", [])
for method, details in methods.items():
if method.lower() not in valid_methods:
continue
method_params = details.get("parameters", [])
all_params = common_params + method_params
endpoint_info = {
"summary": details.get("summary", ""),
"description": details.get("description", ""),
"parameters": all_params
}
endpoints[path][method.lower()] = endpoint_info
return endpoints
def fetch_api_endpoints_json(spec_url):
try:
response = requests.get(spec_url)
response.raise_for_status()
api_spec = response.json()
except Exception as e:
print(f"Error fetching/parsing JSON spec from {spec_url}: {e}")
return {}
endpoints = {}
if "paths" not in api_spec:
print("No endpoints found in the specification.")
return {}
valid_methods = ['get', 'post'] # Only process GET and POST endpoints
for path, methods in api_spec["paths"].items():
endpoints[path] = {}
if not methods or not isinstance(methods, dict):
continue
common_params = methods.get("parameters", [])
for method, details in methods.items():
if method.lower() not in valid_methods:
continue
method_params = details.get("parameters", [])
all_params = common_params + method_params
endpoint_info = {
"summary": details.get("summary", ""),
"description": details.get("description", ""),
"parameters": all_params
}
endpoints[path][method.lower()] = endpoint_info
return endpoints
def get_endpoints(spec_choice, base_url=None):
# If base_url is provided, use it directly
if base_url:
spec_url = base_url
if "JSON" in spec_choice:
return fetch_api_endpoints_json(spec_url), spec_url
return fetch_api_endpoints_yaml(spec_url), spec_url
# Otherwise, try to use environment variables as fallback
api_spec_options = {
"Okta (JSON)": os.getenv("OKTA_API_SPEC"),
"SailPoint IdentityNow (YAML)": os.getenv("IDENTITY_NOW_API_SPEC"),
"Sailpoint IIQ (YAML)": os.getenv("IIQ_API_SPEC")
}
spec_url = api_spec_options.get(spec_choice)
if not spec_url:
print(f"No API specification URL found for {spec_choice}")
return {}, None
if "JSON" in spec_choice:
return fetch_api_endpoints_json(spec_url), spec_url
return fetch_api_endpoints_yaml(spec_url), spec_url
def group_endpoints(endpoints, spec_choice, endpoint_type='get'):
"""Group endpoints by their first path segment, filtering by endpoint type"""
groups = {}
if endpoint_type == 'all':
# For 'all' type, include all endpoints regardless of method
if spec_choice == "Okta (JSON)":
for path, methods in endpoints.items():
clean_path = path.replace('/api/v1/', '')
segments = clean_path.strip("/").split("/")
group_key = segments[0] if segments else "other"
if group_key not in groups:
groups[group_key] = {}
groups[group_key][path] = methods
else:
for path, methods in endpoints.items():
segments = path.strip("/").split("/")
group_key = segments[0] if segments[0] != "" else "other"
if group_key not in groups:
groups[group_key] = {}
groups[group_key][path] = methods
else:
# For specific method types (get, post)
if spec_choice == "Okta (JSON)":
for path, methods in endpoints.items():
if endpoint_type not in methods:
continue
clean_path = path.replace('/api/v1/', '')
segments = clean_path.strip("/").split("/")
group_key = segments[0] if segments else "other"
if group_key not in groups:
groups[group_key] = {}
groups[group_key][path] = methods
else:
for path, methods in endpoints.items():
if endpoint_type not in methods:
continue
segments = path.strip("/").split("/")
group_key = segments[0] if segments[0] != "" else "other"
if group_key not in groups:
groups[group_key] = {}
groups[group_key][path] = methods
return groups
def generate_schema_files(api_choice_val, selected_endpoints, api_spec_url, custom_name=None):
"""Generate JSON and XML schema files for selected endpoints"""
# Determine API name based on choice
if api_choice_val == "Others" and custom_name:
api_name = custom_name.lower().replace(" ", "_")
else:
api_name = api_choice_val.split(" (")[0].lower().replace(" ", "_")
# Debug print to verify api_spec_url is not empty
print(f"API Choice: {api_choice_val}")
print(f"API Spec URL: {api_spec_url}")
print(f"Selected endpoints count: {len(selected_endpoints)}")
print(f"Using API name: {api_name}")
# Create schema generator with path+method pairs
generator = ApiSchemaGeneratorV5(api_spec_url, api_name=api_name, selected_endpoints=selected_endpoints)
# Generate files in memory
meta_data = generator.generate_datasource_plugin_meta()
schema_xml = generator.generate_default_schema()
# Create JSON string
json_str = json.dumps(meta_data, indent=2)
# Create a zip file in memory
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
zip_file.writestr(f"{api_name}_datasource_plugin_meta.json", json_str)
zip_file.writestr(f"{api_name}_default_schema.orx", schema_xml)
zip_buffer.seek(0)
# Return both the individual files and the zip
return {
"json": json_str,
"xml": schema_xml,
"zip": zip_buffer
}
def update_api_url(api_choice_val):
global current_api_spec_url
if api_choice_val == "Others":
# For "Others", we don't set a URL yet - user will input it
current_api_spec_url = ""
return gr.update(visible=True), gr.update(value=""), gr.update(value="")
# Get URL from environment variables
api_spec_options = {
"Okta (JSON)": os.getenv("OKTA_API_SPEC"),
"SailPoint IdentityNow (YAML)": os.getenv("IDENTITY_NOW_API_SPEC"),
"Sailpoint IIQ (YAML)": os.getenv("IIQ_API_SPEC")
}
url = api_spec_options.get(api_choice_val, "")
print(f"Setting API spec URL to: {url}") # Debug print
# Update the global variable
current_api_spec_url = url
return gr.update(visible=False), gr.update(value=url), gr.update(value=url) # Update both text field and state
# ----------------------------- CSS -----------------------------
custom_css = """
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap');
@import url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css');
body, .gradio-container {
font-family: 'Roboto', sans-serif;
background: #f3f3f3;
color: #333;
margin: 0;
padding: 0;
}
.title {
text-align: center;
margin: 40px 0;
font-size: 2.5em;
color: #222;
animation: fadeIn 1s ease-in;
}
.api-cards {
display: flex;
justify-content: center;
gap: 30px;
margin-bottom: 40px;
animation: slideUp 0.5s ease-out;
}
.api-card {
background: white;
padding: 30px;
border-radius: 15px;
box-shadow: 0 6px 12px rgba(0,0,0,0.1);
text-align: center;
cursor: pointer;
transition: transform 0.3s, box-shadow 0.3s;
width: 220px;
}
.api-card:hover {
transform: translateY(-10px);
box-shadow: 0 8px 16px rgba(0,0,0,0.15);
}
.api-card i {
font-size: 40px;
color: #007BFF;
margin-bottom: 15px;
}
.api-card h2 {
font-size: 20px;
margin: 15px 0;
font-weight: 500;
}
.api-card p {
font-size: 16px;
color: #666;
}
.cred-section {
background: white;
padding: 30px;
border-radius: 15px;
box-shadow: 0 6px 12px rgba(0,0,0,0.1);
margin-bottom: 30px;
animation: fadeIn 0.5s ease-in;
}
.action-btn {
background-color: #007BFF;
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
cursor: pointer;
transition: background-color 0.3s, transform 0.2s;
font-size: 16px;
}
.action-btn:hover {
background-color: #0056b3;
transform: translateY(-2px);
}
.loading-spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 20px auto;
}
.loading-text {
color: black;
font-size: 24px;
font-weight: bold;
margin-top: 20px;
text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.8);
}
.fetch-loading-indicator {
display: flex;
justify-content: center;
align-items: center;
height: 100px;
}
.fetch-loading-indicator .loading-spinner {
border-width: 6px;
width: 50px;
height: 50px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.success-message, .error-message {
padding: 15px;
border-radius: 8px;
margin-top: 15px;
display: flex;
align-items: center;
gap: 10px;
animation: slideIn 0.3s ease-out;
font-size: 16px;
}
.success-message {
background: #d4edda;
color: #155724;
}
.error-message {
background: #f8d7da;
color: #721c24;
}
.endpoints-section, .param-group, .fetch-section, .results-section {
background: white;
padding: 30px;
border-radius: 15px;
box-shadow: 0 6px 12px rgba(0,0,0,0.1);
margin-bottom: 30px;
animation: fadeIn 0.5s ease-in;
}
.custom-accordion {
border: 1px solid #ddd;
border-radius: 8px;
margin-bottom: 15px;
transition: all 0.3s ease;
}
.custom-accordion .gr-accordion-header {
background: #f9f9f9;
padding: 15px;
cursor: pointer;
font-weight: 500;
font-size: 18px;
transition: background-color 0.3s;
}
.custom-accordion .gr-accordion-header:hover {
background-color: #e9ecef;
}
.custom-checkbox .gr-checkbox-group {
display: flex;
flex-direction: column;
gap: 10px;
padding: 15px;
}
.custom-checkbox .gr-checkbox-group label {
font-size: 16px;
color: #333;
transition: color 0.3s;
}
.custom-checkbox .gr-checkbox-group label:hover {
color: #007BFF;
}
.reset-btn {
background-color: #6c757d;
color: white;
border: none;
padding: 10px 20px;
border-radius: 8px;
cursor: pointer;
transition: background-color 0.3s, transform 0.2s;
font-size: 16px;
}
.reset-btn:hover:enabled {
background-color: #5a6268;
transform: translateY(-2px);
}
.gr-code {
background: #f9f9f9;
padding: 15px;
border: 1px solid #ddd;
border-radius: 8px;
max-height: 500px;
overflow: auto;
font-family: 'Courier New', monospace;
font-size: 14px;
white-space: pre-wrap;
word-wrap: break-word;
}
.gr-code pre {
margin: 0;
padding: 0;
white-space: pre-wrap;
word-wrap: break-word;
}
.download-btn {
background-color: #28a745;
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
cursor: pointer;
transition: background-color 0.3s, transform 0.2s;
font-size: 16px;
}
.download-btn:hover {
background-color: #218838;
transform: translateY(-2px);
}
.banner {
background-color: #007BFF;
color: white;
padding: 25px;
text-align: center;
font-size: 28px;
font-weight: 500;
margin-bottom: 30px;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.param-item {
background: #f9f9f9;
padding: 15px;
border-radius: 8px;
margin-bottom: 15px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.param-group .gr-textbox {
border: 1px solid #ddd;
border-radius: 8px;
padding: 10px;
font-size: 16px;
transition: border-color 0.3s;
}
.param-group .gr-textbox:focus {
border-color: #007BFF;
box-shadow: 0 0 5px rgba(0,123,255,0.5);
}
.fetch-loading-indicator {
display: flex;
justify-content: center;
align-items: center;
height: 100px;
}
.fetch-loading-indicator .loading-spinner {
border-width: 6px;
width: 50px;
height: 50px;
}
.endpoint-type-selector {
margin-bottom: 20px;
padding: 15px;
background: #333333;
border-radius: 8px;
border: 1px solid #ddd;
}
.endpoint-type-selector label {
color: white !important;
font-weight: bold !important;
font-size: 18px !important;
}
.endpoint-type-selector > .block > label:first-child {
color: #000000;
font-weight: 700;
font-size: 18px;
}
.endpoint-type-selector .gr-radio-group label {
color: white;
font-weight: 500;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideUp {
from { transform: translateY(30px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
@keyframes slideIn {
from { transform: translateX(-30px); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
.user-guide-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.6);
z-index: 9998;
transition: all 0.5s ease;
}
.user-guide-modal {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: #ffffff;
color: #333333;
padding: 35px;
border-radius: 10px;
box-shadow: 0 10px 25px rgba(0,0,0,0.2);
width: 80%;
max-width: 650px;
max-height: 80vh;
overflow-y: auto;
z-index: 9999;
transition: all 0.5s ease;
font-family: 'Roboto', sans-serif;
font-size: 16px;
line-height: 1.6;
}
.user-guide-modal h2,
.user-guide-modal h3,
.user-guide-modal p,
.user-guide-modal ul,
.user-guide-modal li {
color: #333333;
}
.user-guide-modal h2 {
color: #1a1a1a;
font-size: 24px;
margin-bottom: 20px;
}
.user-guide-modal h3 {
color: #0056b3;
font-size: 20px;
margin-top: 25px;
}
.user-guide-modal strong {
color: #1a1a1a;
}
.collapsed {
opacity: 0;
transform: translate(-50%, -50%) scale(0.9);
}
"""
with gr.Blocks(css=custom_css) as demo:
gr.HTML("<div class='banner'>Data Connector Demo</div>")
# User Guide Modal
user_guide_html = gr.HTML("""
<div id="user-guide-overlay" class="user-guide-overlay"></div>
<div id="user-guide-modal" class="user-guide-modal">
<h2>Data Connector - User Guide</h2>
<p>Welcome to Data Connector, a tool that helps you easily extract and explore data from any API platform and generate schema files.</p>
<h3>Getting Started</h3>
<p><strong>Step 1: Select an API Platform</strong><br/>
Click on one of the three available platforms:<br/>
Okta: For Okta Identity Cloud data<br/>
SailPoint IdentityNow: For cloud-based identity governance data<br/>
SailPoint IIQ: For on-premise identity governance data<br/>
Others: For custom API specifications</p>
<p><strong>Step 2: Enter API Specification URL</strong><br/>
Enter the URL for the API specification (OpenAPI/Swagger) for your selected platform.</p>
<p><strong>Step 3: Load API Specification</strong><br/>
Click the "Load API Specification" button to fetch and parse the API endpoints.</p>
<p><strong>Step 4: Select Endpoint Type</strong><br/>
Choose between GET or POST endpoints to display.</p>
<p><strong>Step 5: Browse and Select Endpoints</strong><br/>
After loading, you'll see API endpoints grouped by category<br/>
Click on any category to expand it and see available endpoints<br/>
Select the checkboxes next to the endpoints you want to include<br/>
You can select multiple endpoints across different categories</p>
<p><strong>Step 6: Generate Schema</strong><br/>
Click the "Generate Schema" button to create schema files based on your selected endpoints.</p>
<p><strong>Step 7: View and Download Results</strong><br/>
Results will display in both JSON and XML formats<br/>
Review the generated schema files<br/>
Click the download button to save all files as a ZIP file</p>
<h3>Tips and Troubleshooting</h3>
<ul>
<li><strong>API Specification URLs:</strong> Always include the full URL with https:// prefix</li>
<li><strong>No Endpoints Found:</strong> Check that the API specification is valid and accessible</li>
<li><strong>Schema Generation:</strong> Make sure to select at least one endpoint before generating schemas</li>
</ul>
<h3>Need Help?</h3>
<p>If you need assistance or encounter issues, refer to the official API documentation for detailed guidance:</p>
<ul>
<li><strong>SailPoint IdentityNow:</strong> <a href="https://developer.sailpoint.com/docs/api/v3/identity-security-cloud-v-3-api" target="_blank">Identity Security Cloud V3 API</a></li>
<li><strong>SailPoint IIQ:</strong> <a href="https://developer.sailpoint.com/docs/api/iiq/identityiq-scim-rest-api/" target="_blank">IdentityIQ SCIM REST API</a></li>
<li><strong>Okta:</strong> <a href="https://developer.okta.com/docs/reference/core-okta-api/" target="_blank">Core Okta API</a></li>
</ul>
<p><strong style="color: #222222; font-style: italic;">You can always access these instructions any time by clicking on the "User Guide" button.</strong></p>
<button class="action-btn"
onclick="
var modal = document.getElementById('user-guide-modal');
var overlay = document.getElementById('user-guide-overlay');
modal.classList.add('collapsed');
overlay.classList.add('collapsed');
setTimeout(function(){
modal.style.display = 'none';
overlay.style.display = 'none';
document.getElementById('user-guide-button').style.display = 'block';
}, 500);
"
>
I Understand, Continue
</button>
</div>
<button id="user-guide-button" class="action-btn"
style="position: fixed; bottom: 20px; right: 20px; display: none; z-index: 9999; box-shadow: 0 4px 10px rgba(0,0,0,0.3);"
onclick="
var modal = document.getElementById('user-guide-modal');
var overlay = document.getElementById('user-guide-overlay');
modal.style.display = 'block';
overlay.style.display = 'block';
setTimeout(function(){
modal.classList.remove('collapsed');
overlay.classList.remove('collapsed');
}, 100);
"
>
User Guide
</button>
""", visible=True)
# State variables
locked_endpoints_state = gr.State([])
endpoint_type_state = gr.State("get") # Default to GET endpoints
api_spec_url_state = gr.State("")
required_params_count = gr.State(0)
# API selection with card-style UI
api_options = [
{"icon": "fa-cloud", "title": "Okta", "description": "Identity and Access Management", "value": "Okta (JSON)"},
{"icon": "fa-user-shield", "title": "SailPoint IdentityNow", "description": "Cloud Identity Governance", "value": "SailPoint IdentityNow (YAML)"},
{"icon": "fa-network-wired", "title": "Sailpoint IIQ", "description": "On-premise Identity Governance", "value": "Sailpoint IIQ (YAML)"},
{"icon": "fa-cogs", "title": "Other", "description": "Custom API Specification", "value": "Others"}
]
# Create a radio button with custom styling to look like cards
choices = [(opt["title"], opt["value"]) for opt in api_options]
api_choice = gr.Radio(choices=choices, label="Select API Platform", type="value", elem_classes="api-cards")
# Custom API inputs (visible when "Others" is selected)
with gr.Group(visible=False) as custom_api_group:
custom_api_name = gr.Textbox(label="Custom API Name", placeholder="Enter API name (e.g., 'My API')")
custom_api_format = gr.Radio(choices=["JSON", "YAML"], label="API Format", value="JSON")
# API Base URL input
with gr.Row():
api_base_url = gr.Textbox(label="API Specification URL", placeholder="Enter API specification URL")
# Connect API choice to custom API visibility
api_choice.change(
fn=update_api_url,
inputs=[api_choice],
outputs=[custom_api_group, api_base_url, api_spec_url_state]
)
# Update API spec URL state when the text field changes
def update_api_spec_url_state(url):
global current_api_spec_url
print(f"Updating API spec URL state to: {url}")
# Also update the global variable
current_api_spec_url = url
return gr.update(value=url)
api_base_url.change(
fn=update_api_spec_url_state,
inputs=[api_base_url],
outputs=[api_spec_url_state]
)
# Load API Specification button
load_btn = gr.Button("Load API Specification", elem_classes="action-btn")
loading_indicator = gr.HTML("<div class='loading-spinner'></div>", visible=False)
message_box = gr.HTML("", visible=False)
# Endpoints section
with gr.Group(visible=False, elem_classes="endpoints-section") as endpoints_section:
# Endpoint type selector (GET, POST, or ALL)
endpoint_type = gr.Radio(
choices=["GET", "POST", "ALL"],
value="GET",
label="Endpoints Type",
elem_classes="endpoint-type-selector"
)
# Deselect All checkbox
with gr.Row():
deselect_all_cb = gr.Checkbox(label="Deselect All", value=False)
with gr.Row():
edit_btn = gr.Button("Edit", interactive=False, elem_classes="reset-btn")
reset_btn = gr.Button("Reset", interactive=False, elem_classes="reset-btn")
# Accordion for endpoint groups
max_groups = 100 # Reduced for simplicity
accordion_placeholders = []
for i in range(max_groups):
with gr.Accordion(label="", open=False, visible=False, elem_classes="custom-accordion") as acc:
cb = gr.CheckboxGroup(label="", choices=[], value=[], elem_classes="custom-checkbox")
accordion_placeholders.append((acc, cb))
continue_btn = gr.Button("Generate Schema", elem_classes="action-btn")
# Parameters section
with gr.Group(elem_classes="param-group", visible=False) as param_group:
param_header = gr.Markdown("### Parameters Required")
param_components = []
for i in range(5):
with gr.Group(visible=False, elem_classes="param-item") as group:
param_display = gr.Markdown(visible=False)
param_input = gr.Textbox(label="Parameter Value", visible=False)
param_components.append((group, param_display, param_input))
# Results section
with gr.Group(visible=False, elem_classes="results-section") as results_section:
gr.Markdown("### Results")
with gr.Tabs() as result_tabs:
with gr.TabItem("JSON Schema"):
json_output = gr.Code(label="JSON Schema", language="json", elem_classes="gr-code")
with gr.TabItem("XML Schema"):
xml_output = gr.Textbox(label="XML Schema", lines=20, elem_classes="gr-code")
download_out = gr.File(label="Download Schema Files (ZIP)", visible=True, interactive=True)
# Function to load API specification
def load_api_spec(api_choice_val, base_url, custom_name=None, custom_format=None):
global current_api_spec_url
yield (
gr.update(visible=True), # Show loading indicator
gr.update(value="", visible=False),
gr.update(visible=False),
gr.update(visible=False),
*[gr.update(visible=False, label="", value=[]) for _ in range(max_groups * 2)],
gr.update(value="")
)
# Handle custom API
if api_choice_val == "Others":
# Create a custom API choice value if name is provided
if custom_name:
api_choice_val = f"{custom_name} ({custom_format})"
# Make sure we use the base_url directly for "Others" option
spec_url = base_url
print(f"Using custom API spec URL: {spec_url}")
else:
# Get endpoints and API spec URL for predefined platforms
_, spec_url = get_endpoints(api_choice_val, None)
if base_url: # If user provided a URL, use it instead
spec_url = base_url
print(f"Using user-provided API spec URL for {api_choice_val}: {spec_url}")
else:
print(f"Using default API spec URL for {api_choice_val}: {spec_url}")
# Store the current API spec URL
current_api_spec_url = spec_url
print(f"Setting current_api_spec_url to: {current_api_spec_url}")
# Get endpoints using the determined spec_url
endpoints, _ = get_endpoints(api_choice_val, spec_url)
if not endpoints:
yield (
gr.update(visible=True),
gr.update(value="<div style='color: red;'>No endpoints found</div>", visible=True),
gr.update(visible=False),
gr.update(visible=False),
*[gr.update(visible=False, label="", value=[]) for _ in range(max_groups * 2)],
gr.update(value=spec_url)
)
# Group endpoints by GET method (default)
groups = group_endpoints(endpoints, api_choice_val, 'get')
group_keys = list(groups.keys())
updates = []
for i in range(max_groups):
if i < len(group_keys):
group = group_keys[i]
choices = [f"{ep} | GET - {methods['get'].get('summary', 'No summary')}" for ep, methods in groups[group].items() if 'get' in methods]
updates.extend([
gr.update(label=group, visible=bool(choices), open=False),
gr.update(choices=choices, value=choices, visible=bool(choices))
])
else:
updates.extend([gr.update(visible=False, label=""), gr.update(visible=False, choices=[], value=[])])
yield (
gr.update(visible=False),
gr.update(value="<div class='success-message'><i class='fas fa-check-circle'></i> API specification loaded successfully</div>", visible=True),
gr.update(visible=True),
gr.update(visible=False),
*updates,
gr.update(value=spec_url)
)
# Function to update endpoints based on selected type (GET, POST, or ALL)
def update_endpoint_display(api_choice_val, endpoint_type_val, api_spec_url):
# Convert endpoint type to lowercase
endpoint_type_val = endpoint_type_val.lower()
# Get endpoints
endpoints, _ = get_endpoints(api_choice_val, api_spec_url)
if not endpoints:
# Even if no endpoints are found, we should still return the endpoint type state
# and ensure the endpoints section remains visible
return [gr.update(visible=False, label="", value=[]) for _ in range(max_groups * 2)] + [gr.update(value=endpoint_type_val)]
# Group endpoints by selected type
groups = group_endpoints(endpoints, api_choice_val, endpoint_type_val)
group_keys = list(groups.keys())
updates = []
for i in range(max_groups):
if i < len(group_keys):
group = group_keys[i]
if endpoint_type_val == 'all':
# For 'all' type, include both GET and POST endpoints
choices = []
for ep, methods in groups[group].items():
for method_type in ['get', 'post']:
if method_type in methods:
choices.append(f"{ep} | {method_type.upper()} - {methods[method_type].get('summary', 'No summary')}")
else:
# For specific method types
choices = [f"{ep} | {endpoint_type_val.upper()} - {methods[endpoint_type_val].get('summary', 'No summary')}"
for ep, methods in groups[group].items() if endpoint_type_val in methods]
updates.extend([
gr.update(label=group, visible=bool(choices), open=False),
gr.update(choices=choices, value=choices, visible=bool(choices))
])
else:
updates.extend([gr.update(visible=False, label=""), gr.update(visible=False, choices=[], value=[])])
return updates + [gr.update(value=endpoint_type_val)]
# Function to lock selected endpoints and generate schema directly
def lock_selected_endpoints(*checkbox_values):
all_selected = [sel for group in checkbox_values if isinstance(group, list) for sel in group]
if not all_selected:
return [
[],
gr.update(interactive=True),
gr.update(interactive=False),
gr.update(interactive=False),
*[gr.update(interactive=True) for _ in range(max_groups)]
]
return [
all_selected,
gr.update(interactive=False),
gr.update(interactive=True),
gr.update(interactive=True),
*[gr.update(interactive=False) for _ in range(max_groups)]
]
# Function to unlock selected endpoints
def unlock_selected_endpoints():
return [
gr.update(value=[]),
gr.update(interactive=True),
gr.update(interactive=False),
gr.update(interactive=False),
*[gr.update(interactive=True) for _ in range(max_groups)]
]
# Function to reset selected endpoints
def reset_selected_endpoints():
return [
gr.update(value=[]),
gr.update(interactive=True),
gr.update(interactive=False),
gr.update(interactive=False),
*[gr.update(interactive=True, value=[]) for _ in range(max_groups)]
]
# Store the current API spec URL in a global variable
current_api_spec_url = ""
# Function to generate schema files
def generate_schemas(api_choice_val, api_spec_url_value, custom_api_name_value, *checkbox_values):
global current_api_spec_url
print(f"Generate schemas called with api_choice_val: {api_choice_val}")
print(f"API spec URL value from state: {api_spec_url_value}")
print(f"Current API spec URL: {current_api_spec_url}")
print(f"Custom API name from UI: {custom_api_name_value}")
# Get custom API name if "Others" is selected
custom_name = None
if api_choice_val == "Others":
# Use the custom API name passed as a parameter
custom_name = custom_api_name_value
# If custom_name is None or empty, use a default name
if not custom_name or custom_name.strip() == "":
custom_name = "custom_api"
print(f"Using default custom API name: {custom_name}")
# Extract both path and method from selected endpoints
all_selected = []
for group in checkbox_values:
if isinstance(group, list):
for sel in group:
# Parse both path and method from the selection
parts = sel.split(" | ")
if len(parts) >= 2:
path = parts[0].strip()
method = parts[1].split(" - ")[0].strip().lower() # Extract GET or POST
all_selected.append((path, method))
if not all_selected:
return (
gr.update(visible=False),
gr.update(value=""),
gr.update(value=""),
gr.update(value=None)
)
# Use the current API spec URL that was set when loading the API specification
api_spec_url = current_api_spec_url
# If we still don't have a URL, try to use the one from the state
if not api_spec_url:
api_spec_url = api_spec_url_value
print(f"Using API spec URL from state: {api_spec_url}")
print(f"API Choice: {api_choice_val}")
print(f"API Spec URL: {api_spec_url}")
print(f"Selected endpoints count: {len(all_selected)}")
# Check if api_choice_val is None
if api_choice_val is None:
print(f"Error: API choice is None")
return (
gr.update(visible=True),
gr.update(value="Error: Please select an API platform first"),
gr.update(value="Error: Please select an API platform first"),
gr.update(value=None)
)
# Check if api_spec_url is empty
if not api_spec_url:
print(f"Error: API spec URL is empty")
return (
gr.update(visible=True),
gr.update(value="Error: API specification URL is empty. Please select a valid API platform or enter a URL."),
gr.update(value="Error: API specification URL is empty. Please select a valid API platform or enter a URL."),
gr.update(value=None)
)
# Generate schema files
try:
# Pass the custom API name if it's available
if api_choice_val == "Others" and custom_name:
result = generate_schema_files(api_choice_val, all_selected, api_spec_url, custom_name)
else:
result = generate_schema_files(api_choice_val, all_selected, api_spec_url)
# Create a temporary file with .zip extension
fd, temp_path = tempfile.mkstemp(suffix='.zip')
os.close(fd)
# Write the BytesIO content to the temporary file
with open(temp_path, 'wb') as f:
f.write(result["zip"].getvalue())
print(f"Temporary zip file created at: {temp_path}")
except Exception as e:
print(f"Error generating schema files: {e}")
return (
gr.update(visible=True),
gr.update(value=f"Error generating schema files: {str(e)}"),
gr.update(value=f"Error generating schema files: {str(e)}"),
gr.update(value=None)
)
return (
gr.update(visible=True),
gr.update(value=result["json"]),
gr.update(value=result["xml"]),
gr.update(value=temp_path) # Use the temporary file path instead of BytesIO
)
# Function to handle API choice change - collapses sections when API is changed
def handle_api_choice_change(api_choice_val):
global current_api_spec_url
# Reset the current API spec URL when changing API choice
current_api_spec_url = ""
# First, get the outputs from update_api_url
custom_api_visibility, api_base_url_value, api_spec_url_value = update_api_url(api_choice_val)
# Then, add the visibility updates for endpoints and results sections
return custom_api_visibility, api_base_url_value, api_spec_url_value, gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)
# Update API choice to handle collapsing sections
api_choice.change(
fn=handle_api_choice_change,
inputs=[api_choice],
outputs=[custom_api_group, api_base_url, api_spec_url_state, endpoints_section, results_section, message_box]
)
# Connect UI components to functions
load_btn.click(
fn=load_api_spec,
inputs=[api_choice, api_base_url, custom_api_name, custom_api_format],
outputs=[
loading_indicator,
message_box,
endpoints_section,
results_section
] + [comp for acc, cb in accordion_placeholders for comp in (acc, cb)] + [api_spec_url_state]
)
# Function to update endpoints and ensure endpoints section remains visible
def update_endpoints_and_maintain_visibility(api_choice_val, endpoint_type_val, api_spec_url):
# Get updates for accordion placeholders and endpoint type state
updates = update_endpoint_display(api_choice_val, endpoint_type_val, api_spec_url)
# Return these updates plus an update to keep the endpoints section visible
return updates + [gr.update(visible=True)]
endpoint_type.change(
fn=update_endpoints_and_maintain_visibility,
inputs=[api_choice, endpoint_type, api_spec_url_state],
outputs=[comp for acc, cb in accordion_placeholders for comp in (acc, cb)] + [endpoint_type_state, endpoints_section]
)
# Function to handle deselect all checkbox
def deselect_all_endpoints(deselect_all, *current_values):
if not deselect_all:
return [gr.update(value=val) for val in current_values]
# Deselect all checkboxes
updates = [gr.update(value=[]) for _ in current_values]
return updates
# Connect deselect all checkbox
deselect_all_cb.change(
fn=deselect_all_endpoints,
inputs=[deselect_all_cb] + [cb for _, cb in accordion_placeholders],
outputs=[cb for _, cb in accordion_placeholders]
)
continue_btn.click(
fn=lock_selected_endpoints,
inputs=[cb for _, cb in accordion_placeholders],
outputs=[
locked_endpoints_state,
continue_btn,
edit_btn,
reset_btn
] + [cb for _, cb in accordion_placeholders]
)
edit_btn.click(
fn=unlock_selected_endpoints,
inputs=[],
outputs=[
locked_endpoints_state,
continue_btn,
edit_btn,
reset_btn
] + [cb for _, cb in accordion_placeholders]
)
reset_btn.click(
fn=reset_selected_endpoints,
inputs=[],
outputs=[
locked_endpoints_state,
continue_btn,
edit_btn,
reset_btn
] + [cb for _, cb in accordion_placeholders]
)
continue_btn.click(
fn=generate_schemas,
inputs=[api_choice, api_spec_url_state, custom_api_name] + [cb for _, cb in accordion_placeholders],
outputs=[
results_section,
json_output,
xml_output,
download_out
]
)
# Function to return the zip file for download
def download_schema_files(json_content, xml_content):
# Generate schema files in memory
if not json_content or not xml_content:
return None
try:
# Determine API name from JSON content
meta_data = json.loads(json_content)
api_name = meta_data.get("name", "schema").lower().replace(" ", "_")
# Create a zip file in memory
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
zip_file.writestr(f"{api_name}_datasource_plugin_meta.json", json_content)
zip_file.writestr(f"{api_name}_default_schema.orx", xml_content)
zip_buffer.seek(0)
# Create a temporary file with .zip extension
fd, temp_path = tempfile.mkstemp(suffix='.zip')
os.close(fd)
# Write the BytesIO content to the temporary file
with open(temp_path, 'wb') as f:
f.write(zip_buffer.getvalue())
# Return the file path with a filename for download
return (temp_path, f"{api_name}_schema_files.zip")
except Exception as e:
print(f"Error creating download file: {e}")
return None
if __name__ == "__main__":
demo.launch(show_error=True)