Juggernaut1397 commited on
Commit
0d8c70b
·
verified ·
1 Parent(s): 32556ed

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +603 -487
app.py CHANGED
@@ -1,5 +1,9 @@
1
  import requests
2
  import yaml
 
 
 
 
3
  from identityNow import handle_identitynow_call
4
  from okta import handle_okta_call
5
  from iiq import handle_iiq_call
@@ -8,6 +12,7 @@ import gradio as gr
8
  import os
9
  from dotenv import load_dotenv
10
  from pathlib import Path
 
11
 
12
  # Load environment variables
13
  script_dir = Path(__file__).resolve().parent
@@ -29,7 +34,7 @@ def fetch_api_endpoints_yaml(spec_url):
29
  print("No endpoints found in the specification.")
30
  return {}
31
 
32
- valid_methods = ['get', 'post', 'put', 'delete', 'patch', 'head', 'options']
33
  for path, methods in api_spec["paths"].items():
34
  endpoints[path] = {}
35
  if not methods or not isinstance(methods, dict):
@@ -64,7 +69,7 @@ def fetch_api_endpoints_json(spec_url):
64
  print("No endpoints found in the specification.")
65
  return {}
66
 
67
- valid_methods = ['get', 'post', 'put', 'delete', 'patch', 'head', 'options']
68
  for path, methods in api_spec["paths"].items():
69
  endpoints[path] = {}
70
  if not methods or not isinstance(methods, dict):
@@ -85,7 +90,15 @@ def fetch_api_endpoints_json(spec_url):
85
  endpoints[path][method.lower()] = endpoint_info
86
  return endpoints
87
 
88
- def get_endpoints(spec_choice):
 
 
 
 
 
 
 
 
89
  api_spec_options = {
90
  "Okta (JSON)": os.getenv("OKTA_API_SPEC"),
91
  "SailPoint IdentityNow (YAML)": os.getenv("IDENTITY_NOW_API_SPEC"),
@@ -93,166 +106,119 @@ def get_endpoints(spec_choice):
93
  }
94
  spec_url = api_spec_options.get(spec_choice)
95
  if not spec_url:
96
- return {}
 
 
97
  if "JSON" in spec_choice:
98
- return fetch_api_endpoints_json(spec_url)
99
- return fetch_api_endpoints_yaml(spec_url)
100
 
101
- def group_endpoints(endpoints, spec_choice):
 
102
  groups = {}
103
- if spec_choice == "Okta (JSON)":
104
- for path, methods in endpoints.items():
105
- clean_path = path.replace('/api/v1/', '')
106
- segments = clean_path.strip("/").split("/")
107
- group_key = segments[0] if segments else "other"
108
- if group_key not in groups:
109
- groups[group_key] = {}
110
- groups[group_key][path] = methods
 
 
 
 
 
 
 
 
 
111
  else:
112
- for path, methods in endpoints.items():
113
- segments = path.strip("/").split("/")
114
- group_key = segments[0] if segments[0] != "" else "other"
115
- if group_key not in groups:
116
- groups[group_key] = {}
117
- groups[group_key][path] = methods
118
- return groups
119
-
120
- def verify_credentials(spec_choice, api_base_url, grant_type=None, client_id=None,
121
- client_secret=None, api_token=None, iiq_username=None, iiq_password=None):
122
- if not api_base_url or not api_base_url.strip():
123
- return (
124
- gr.update(value="<div class='error-message'><i class='fas fa-exclamation-circle'></i> API Base URL is required</div>", visible=True),
125
- False,
126
- False
127
- )
128
-
129
- try:
130
  if spec_choice == "Okta (JSON)":
131
- if not api_token or not api_token.strip():
132
- return (
133
- gr.update(value="<div class='error-message'><i class='fas fa-exclamation-circle'></i> API Token is required</div>", visible=True),
134
- False,
135
- False
136
- )
137
- result = handle_okta_call(api_base_url, api_token, "", {}, ["/api/v1/users/me"])
138
-
139
- if isinstance(result[0], dict) and "/api/v1/users/me" in result[0]:
140
- response = result[0]["/api/v1/users/me"]
141
-
142
- if isinstance(response, str) and response.startswith("Error:"):
143
- return (
144
- gr.update(value="<div class='error-message'><i class='fas fa-exclamation-circle'></i> Please check your credentials and try again</div>", visible=True),
145
- False,
146
- False
147
- )
148
-
149
- if isinstance(response, dict) and "error" in response:
150
- return (
151
- gr.update(value="<div class='error-message'><i class='fas fa-exclamation-circle'></i> Please check your credentials and try again</div>", visible=True),
152
- False,
153
- False
154
- )
155
- else:
156
- return (
157
- gr.update(value="<div class='error-message'><i class='fas fa-exclamation-circle'></i> Please check your credentials and try again</div>", visible=True),
158
- False,
159
- False
160
- )
161
-
162
- elif spec_choice == "SailPoint IdentityNow (YAML)":
163
- if not all([grant_type and grant_type.strip(), client_id and client_id.strip(), client_secret and client_secret.strip()]):
164
- return (
165
- gr.update(value="<div class='error-message'><i class='fas fa-exclamation-circle'></i> All IdentityNow credentials are required</div>", visible=True),
166
- False,
167
- False
168
- )
169
- result = handle_identitynow_call(api_base_url, grant_type, client_id, client_secret, "", {}, ["/accounts"])
170
-
171
- if isinstance(result[0], dict):
172
- if "error" in result[0]:
173
- return (
174
- gr.update(value="<div class='error-message'><i class='fas fa-exclamation-circle'></i> Please check your credentials and try again</div>", visible=True),
175
- False,
176
- False
177
- )
178
-
179
- if "accounts" in result[0]:
180
- response = result[0]["accounts"]
181
- if isinstance(response, str) and response.startswith("Error:"):
182
- return (
183
- gr.update(value="<div class='error-message'><i class='fas fa-exclamation-circle'></i> Please check your credentials and try again</div>", visible=True),
184
- False,
185
- False
186
- )
187
- if isinstance(response, dict) and "error" in response:
188
- return (
189
- gr.update(value="<div class='error-message'><i class='fas fa-exclamation-circle'></i> Please check your credentials and try again</div>", visible=True),
190
- False,
191
- False
192
- )
193
- else:
194
- return (
195
- gr.update(value="<div class='error-message'><i class='fas fa-exclamation-circle'></i> Please check your credentials and try again</div>", visible=True),
196
- False,
197
- False
198
- )
199
-
200
- elif spec_choice == "Sailpoint IIQ (YAML)":
201
- if not all([iiq_username and iiq_username.strip(), iiq_password and iiq_password.strip()]):
202
- return (
203
- gr.update(value="<div class='error-message'><i class='fas fa-exclamation-circle'></i> Both IIQ username and password are required</div>", visible=True),
204
- False,
205
- False
206
- )
207
- result = handle_iiq_call(api_base_url, iiq_username, iiq_password, "", {}, ["/ping"])
208
-
209
- if isinstance(result[0], dict):
210
- if "error" in result[0]:
211
- return (
212
- gr.update(value="<div class='error-message'><i class='fas fa-exclamation-circle'></i> Please check your credentials and try again</div>", visible=True),
213
- False,
214
- False
215
- )
216
-
217
- if "/ping" in result[0]:
218
- response = result[0]["/ping"]
219
- if isinstance(response, str) and response.startswith("Error:"):
220
- return (
221
- gr.update(value="<div class='error-message'><i class='fas fa-exclamation-circle'></i> Please check your credentials and try again</div>", visible=True),
222
- False,
223
- False
224
- )
225
- if isinstance(response, dict) and "error" in response:
226
- return (
227
- gr.update(value="<div class='error-message'><i class='fas fa-exclamation-circle'></i> Please check your credentials and try again</div>", visible=True),
228
- False,
229
- False
230
- )
231
- else:
232
- return (
233
- gr.update(value="<div class='error-message'><i class='fas fa-exclamation-circle'></i> Please check your credentials and try again</div>", visible=True),
234
- False,
235
- False
236
- )
237
 
238
- return (
239
- gr.update(value="<div class='success-message'><i class='fas fa-check-circle'></i> Credentials verified successfully!</div>", visible=True),
240
- True,
241
- True
242
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
 
244
- except requests.exceptions.ConnectionError:
245
- return (
246
- gr.update(value="<div class='error-message'><i class='fas fa-exclamation-circle'></i> Please check your credentials and try again</div>", visible=True),
247
- False,
248
- False
249
- )
250
- except Exception as e:
251
- return (
252
- gr.update(value="<div class='error-message'><i class='fas fa-exclamation-circle'></i> Please check your credentials and try again</div>", visible=True),
253
- False,
254
- False
255
- )
 
 
 
 
 
 
 
 
 
 
256
 
257
  # ----------------------------- CSS -----------------------------
258
  custom_css = """
@@ -351,6 +317,27 @@ body, .gradio-container {
351
  margin: 20px auto;
352
  }
353
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
354
  @keyframes spin {
355
  0% { transform: rotate(0deg); }
356
  100% { transform: rotate(360deg); }
@@ -520,6 +507,31 @@ body, .gradio-container {
520
  height: 50px;
521
  }
522
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
523
  @keyframes fadeIn {
524
  from { opacity: 0; }
525
  to { opacity: 1; }
@@ -598,83 +610,59 @@ body, .gradio-container {
598
  """
599
 
600
  with gr.Blocks(css=custom_css) as demo:
601
- user_guide_html = """
 
 
 
602
  <div id="user-guide-overlay" class="user-guide-overlay"></div>
603
  <div id="user-guide-modal" class="user-guide-modal">
604
  <h2>Data Connector - User Guide</h2>
605
- <p>Welcome to Data Connector, a tool that helps you easily extract and explore data from any api platform without writing any code, but that functionality is coming soon for a demo we have locked it for 3 api's.</p>
606
 
607
  <h3>Getting Started</h3>
608
  <p><strong>Step 1: Select an API Platform</strong><br/>
609
  Click on one of the three available platforms:<br/>
610
  Okta: For Okta Identity Cloud data<br/>
611
  SailPoint IdentityNow: For cloud-based identity governance data<br/>
612
- SailPoint IIQ: For on-premise identity governance data</p>
613
-
614
- <p><strong>Step 2: Enter Connection Credentials</strong><br/>
615
- Depending on your selection, enter the required credentials:<br/>
616
- <strong style="color: #0056b3; font-weight: 600;">For Okta:</strong><br/>
617
- API Base URL: Your Okta domain (e.g., https://your-company.okta.com)<br/>
618
- API Token: Your Okta API token<br/>
619
- <strong style="color: #0056b3; font-weight: 600;">For SailPoint IdentityNow:</strong><br/>
620
- API Base URL: Your IdentityNow tenant URL (e.g., https://your-tenant.api.identitynow.com)<br/>
621
- Grant Type: Leave as "client_credentials" (default)<br/>
622
- Client ID: Your OAuth client ID<br/>
623
- Client Secret: Your OAuth client secret<br/>
624
- <strong style="color: #0056b3; font-weight: 600;">For SailPoint IIQ:</strong><br/>
625
- API Base URL: Your IIQ server URL (e.g., https://your-iiq-server.com/identityiq)<br/>
626
- Username: Your IIQ username with API access<br/>
627
- Password: Your IIQ password</p>
628
-
629
- <p><strong>Step 3: Verify Credentials</strong><br/>
630
- Click the "Verify Credentials" button<br/>
631
- Wait for the connection test to complete<br/>
632
- A green success message will appear if verification succeeds<br/>
633
- If verification fails, check your credentials and connection details</p>
634
-
635
- <p><strong>Step 4: Browse and Select Endpoints</strong><br/>
636
- After successful verification, you'll see API endpoints grouped by category<br/>
637
  Click on any category to expand it and see available endpoints<br/>
638
- Select the checkboxes next to the endpoints you want to query<br/>
639
- You can select multiple endpoints across different categories<br/>
640
- Click the "Continue" button when you've finished selecting</p>
641
-
642
- <p><strong>Step 5: Enter Parameters (if required)</strong><br/>
643
- Some endpoints require additional parameters<br/>
644
- If parameters are needed, input fields will appear<br/>
645
- Fill in all required parameters (marked with 🔑 or 🔍)<br/>
646
- Path parameters (🔑) are part of the URL path<br/>
647
- Query parameters (🔍) filter or modify the results</p>
648
-
649
- <p><strong>Step 6: Fetch Data</strong><br/>
650
- Click the "Fetch Data" button<br/>
651
- Wait while the application contacts the API(s)<br/>
652
- A loading indicator will appear during this process</p>
653
 
654
  <p><strong>Step 7: View and Download Results</strong><br/>
655
- Results will display in JSON format in the "Results" section<br/>
656
- Review the data returned from each endpoint<br/>
657
- Click the download button to save all data as a ZIP file<br/>
658
- The ZIP file contains all responses plus metadata about your session</p>
659
 
660
  <h3>Tips and Troubleshooting</h3>
661
  <ul>
662
- <li><strong>API Base URLs:</strong> Always include the full URL with https:// prefix</li>
663
- <li><strong>Connection Issues:</strong> Check your network connection and VPN settings if applicable</li>
664
- <li><strong>Authorization Errors:</strong> Verify that your credentials have the proper permissions</li>
665
- <li><strong>Parameter Format:</strong> Some parameters may require specific formats (UUIDs, dates, etc.)</li>
666
- <li><strong>Session Persistence:</strong> Your data is saved locally during the session for download later</li>
667
  </ul>
668
 
669
- <h3>Data Security Note</h3>
670
- <p>This application processes but does not store your credentials permanently. All data is saved temporarily on your device and can be downloaded as a ZIP file.</p>
671
-
672
  <h3>Need Help?</h3>
673
- <p>If you need assistance or encounter issues, refer to the official API documentation for detailed guidance and instructions on obtaining the necessary credentials:</p>
674
  <ul>
675
- <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> - Find details on API endpoints and how to generate Client ID and Client Secret.</li>
676
- <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> - Learn about API usage and how to set up username and password for API access.</li>
677
- <li><strong>Okta:</strong> <a href="https://developer.okta.com/docs/reference/core-okta-api/" target="_blank">Core Okta API</a> - Instructions for creating an API token and using the Okta API.</li>
678
  </ul>
679
 
680
  <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>
@@ -711,54 +699,91 @@ with gr.Blocks(css=custom_css) as demo:
711
  >
712
  User Guide
713
  </button>
714
- """
715
- user_guide_modal = gr.HTML(user_guide_html, visible=True)
716
 
717
- gr.HTML("<div class='banner'>Data Connector Demo</div>")
718
-
719
- session_id_state = gr.State("")
720
  locked_endpoints_state = gr.State([])
 
 
721
  required_params_count = gr.State(0)
722
 
 
723
  api_options = [
724
  {"icon": "fa-cloud", "title": "Okta", "description": "Identity and Access Management", "value": "Okta (JSON)"},
725
  {"icon": "fa-user-shield", "title": "SailPoint IdentityNow", "description": "Cloud Identity Governance", "value": "SailPoint IdentityNow (YAML)"},
726
  {"icon": "fa-network-wired", "title": "Sailpoint IIQ", "description": "On-premise Identity Governance", "value": "Sailpoint IIQ (YAML)"},
 
727
  ]
 
 
728
  choices = [(opt["title"], opt["value"]) for opt in api_options]
729
- api_choice = gr.Radio(choices=choices, label="", type="value", elem_classes="api-cards")
730
-
731
- with gr.Column(visible=False, elem_classes="cred-section") as okta_cred:
732
- api_base_url_okta = gr.Textbox(label="API Base URL", placeholder="e.g., https://your-okta-domain.okta.com")
733
- api_token = gr.Textbox(label="API Token", type="password", placeholder="Enter your Okta API token")
734
-
735
- with gr.Column(visible=False, elem_classes="cred-section") as inow_cred:
736
- api_base_url_inow = gr.Textbox(label="API Base URL", placeholder="e.g., https://your-org.api.identitynow.com")
737
- grant_type = gr.Textbox(label="Grant Type", value="client_credentials", interactive=False)
738
- client_id = gr.Textbox(label="Client ID", placeholder="Enter your client ID")
739
- client_secret = gr.Textbox(label="Client Secret", type="password", placeholder="Enter your client secret")
740
-
741
- with gr.Column(visible=False, elem_classes="cred-section") as iiq_cred:
742
- api_base_url_iiq = gr.Textbox(label="API Base URL", placeholder="e.g., https://your-iiq-server.com/identityiq")
743
- username = gr.Textbox(label="Username", placeholder="Enter your IIQ username")
744
- password = gr.Textbox(label="Password", type="password", placeholder="Enter your IIQ password")
 
745
 
746
- verify_btn = gr.Button("Verify Credentials", elem_classes="action-btn")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
747
  loading_indicator = gr.HTML("<div class='loading-spinner'></div>", visible=False)
748
- credentials_message = gr.HTML("", visible=False)
749
-
750
- max_groups = 100
751
- with gr.Group(elem_classes="endpoints-section", visible=False) as endpoints_section:
 
 
 
 
 
 
 
 
 
 
 
 
 
752
  with gr.Row():
753
  edit_btn = gr.Button("Edit", interactive=False, elem_classes="reset-btn")
754
  reset_btn = gr.Button("Reset", interactive=False, elem_classes="reset-btn")
 
 
 
755
  accordion_placeholders = []
756
  for i in range(max_groups):
757
  with gr.Accordion(label="", open=False, visible=False, elem_classes="custom-accordion") as acc:
758
  cb = gr.CheckboxGroup(label="", choices=[], value=[], elem_classes="custom-checkbox")
759
  accordion_placeholders.append((acc, cb))
760
- continue_btn = gr.Button("Continue", elem_classes="action-btn")
 
761
 
 
762
  with gr.Group(elem_classes="param-group", visible=False) as param_group:
763
  param_header = gr.Markdown("### Parameters Required")
764
  param_components = []
@@ -768,97 +793,130 @@ with gr.Blocks(css=custom_css) as demo:
768
  param_input = gr.Textbox(label="Parameter Value", visible=False)
769
  param_components.append((group, param_display, param_input))
770
 
771
- with gr.Group(elem_classes="fetch-section", visible=False) as fetch_section:
772
- fetch_btn = gr.Button("Fetch Data", interactive=False, elem_classes="action-btn")
773
- fetch_loading_indicator = gr.HTML("<div class='fetch-loading-indicator'><div class='loading-spinner'></div></div>", visible=False)
774
-
775
- with gr.Group(elem_classes="results-section", visible=False) as results_section:
776
  gr.Markdown("### Results")
777
- results_out = gr.Code(label="API Responses", language="json", elem_classes="gr-code")
778
- download_out = gr.File(label="Download Session Data (ZIP)", elem_classes="download-btn")
779
-
780
- def update_cred_sections(api_choice_value):
781
- return [
782
- gr.update(visible=api_choice_value=="Okta (JSON)"),
783
- gr.update(visible=api_choice_value=="SailPoint IdentityNow (YAML)"),
784
- gr.update(visible=api_choice_value=="Sailpoint IIQ (YAML)"),
785
- gr.update(visible=False),
786
- gr.update(visible=False),
787
- gr.update(visible=False),
788
- gr.update(visible=False),
789
- gr.update(value=None),
790
- gr.update(value=None),
791
- gr.update(value=""),
792
- gr.update(value=[]),
793
- gr.update(interactive=True),
794
- gr.update(interactive=False),
795
- gr.update(interactive=False), # Reset button explicitly set to inactive
796
- ] + [gr.update(interactive=True, value=[]) for _, cb in accordion_placeholders] + [
797
- gr.update(visible=False) for _ in range(len(param_components) * 3)
798
- ] + [gr.update(interactive=False)] + [
799
- gr.update(value=0)
800
- ] + [gr.update(value="") for _, _, input_box in param_components]
801
-
802
- def verify_and_load_endpoints(api_choice_val, base_okta, tok_okta, base_inow, gtype, cid, csecret, base_iiq, uname, pw):
803
  yield (
804
- gr.update(visible=True),
805
- gr.update(value=""),
806
  gr.update(visible=False),
807
  gr.update(visible=False),
808
- *[gr.update(visible=False, label="", value=[]) for _ in range(max_groups * 2)]
 
809
  )
810
-
811
- if api_choice_val == "Okta (JSON)":
812
- status, endpoints_vis, fetch_vis = verify_credentials(api_choice_val, base_okta, api_token=tok_okta)
813
- elif api_choice_val == "SailPoint IdentityNow (YAML)":
814
- status, endpoints_vis, fetch_vis = verify_credentials(api_choice_val, base_inow, grant_type=gtype,
815
- client_id=cid, client_secret=csecret)
 
 
816
  else:
817
- status, endpoints_vis, fetch_vis = verify_credentials(api_choice_val, base_iiq, iiq_username=uname,
818
- iiq_password=pw)
819
-
820
- if not endpoints_vis:
821
- yield (
822
- gr.update(visible=False),
823
- status,
824
- gr.update(visible=False),
825
- gr.update(visible=False),
826
- *[gr.update(visible=False, label="", value=[]) for _ in range(max_groups * 2)]
827
- )
828
- return
829
-
830
- endpoints = get_endpoints(api_choice_val)
 
831
  if not endpoints:
832
  yield (
833
- gr.update(visible=False),
834
- gr.update(value="<div class='error-message'><i class='fas fa-exclamation-circle'></i> No endpoints found</div>", visible=True),
835
  gr.update(visible=False),
836
  gr.update(visible=False),
837
- *[gr.update(visible=False, label="", value=[]) for _ in range(max_groups * 2)]
 
838
  )
839
- return
840
-
841
- groups = group_endpoints(endpoints, api_choice_val)
842
  group_keys = list(groups.keys())
843
  updates = []
 
844
  for i in range(max_groups):
845
  if i < len(group_keys):
846
  group = group_keys[i]
847
  choices = [f"{ep} | GET - {methods['get'].get('summary', 'No summary')}" for ep, methods in groups[group].items() if 'get' in methods]
848
  updates.extend([
849
  gr.update(label=group, visible=bool(choices), open=False),
850
- gr.update(choices=choices, value=[], visible=bool(choices), interactive=True)
851
  ])
852
  else:
853
  updates.extend([gr.update(visible=False, label=""), gr.update(visible=False, choices=[], value=[])])
 
854
  yield (
855
  gr.update(visible=False),
856
- gr.update(value="<div class='success-message'><i class='fas fa-check-circle'></i> Endpoints loaded successfully</div>", visible=True),
857
- gr.update(visible=True),
858
  gr.update(visible=True),
859
- *updates
 
 
860
  )
861
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
862
  def lock_selected_endpoints(*checkbox_values):
863
  all_selected = [sel for group in checkbox_values if isinstance(group, list) for sel in group]
864
  if not all_selected:
@@ -866,194 +924,239 @@ with gr.Blocks(css=custom_css) as demo:
866
  [],
867
  gr.update(interactive=True),
868
  gr.update(interactive=False),
869
- gr.update(interactive=False), # Reset button stays inactive if no selection
870
- *[gr.update(interactive=True) for _ in range(max_groups)],
871
- gr.update(visible=False),
872
- gr.update(visible=False),
873
- *[gr.update(visible=False) for _ in range(len(param_components) * 3)],
874
  gr.update(interactive=False),
875
- gr.update(value=0)
876
  ]
877
-
878
- all_parameters = get_required_params(api_choice.value, all_selected)
879
- param_updates = update_params(api_choice.value, all_selected)
880
- has_params = len(all_parameters) > 0
881
- fetch_update = gr.update(interactive=not has_params)
882
 
883
  return [
884
  all_selected,
885
  gr.update(interactive=False),
886
  gr.update(interactive=True),
887
- gr.update(interactive=True), # Reset button should be active now - CHANGED THIS LINE
888
- *[gr.update(interactive=False) for _ in range(max_groups)],
889
- *param_updates,
890
- fetch_update,
891
- gr.update(value=len(all_parameters))
892
  ]
893
 
 
894
  def unlock_selected_endpoints():
895
  return [
896
  gr.update(value=[]),
897
  gr.update(interactive=True),
898
  gr.update(interactive=False),
899
- gr.update(interactive=False), # Reset button
900
- *[gr.update(interactive=True) for _ in range(max_groups)],
901
- gr.update(visible=False),
902
- gr.update(visible=False),
903
- *[gr.update(visible=False) for _ in range(len(param_components) * 3)],
904
  gr.update(interactive=False),
905
- gr.update(value=0)
906
  ]
907
 
 
908
  def reset_selected_endpoints():
909
  return [
910
  gr.update(value=[]),
911
  gr.update(interactive=True),
912
  gr.update(interactive=False),
913
- gr.update(interactive=False), # Reset button
914
- *[gr.update(interactive=True, value=[]) for _ in range(max_groups)], # Unselect all endpoints
915
- gr.update(visible=False),
916
- gr.update(visible=False),
917
- *[gr.update(visible=False) for _ in range(len(param_components) * 3)],
918
  gr.update(interactive=False),
919
- gr.update(value=0)
920
  ]
921
 
922
- def get_required_params(spec_choice_val, selected_endpoints):
923
- endpoints = get_endpoints(spec_choice_val)
924
- all_params = []
925
- for ep in selected_endpoints:
926
- endpoint = ep.split(" | GET")[0].strip()
927
- endpoint_spec = endpoints.get(endpoint, {}).get('get', {})
928
- path_params = extract_path_params(endpoint)
929
- query_params = extract_query_params(endpoint_spec)
930
- for param in path_params:
931
- all_params.append({"endpoint": endpoint, "type": "path", "name": param, "description": "Path parameter"})
932
- for _, name, required, desc in query_params:
933
- if required:
934
- all_params.append({"endpoint": endpoint, "type": "query", "name": name, "description": desc or "Query parameter"})
935
- return all_params
936
-
937
- def update_params(spec_choice_val, locked_endpoints):
938
- all_parameters = get_required_params(spec_choice_val, locked_endpoints)
939
- updates = []
940
- if not all_parameters:
941
- updates.extend([
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
942
  gr.update(visible=True),
943
- gr.update(value=" No parameters are required. Press 'Fetch Data' to get data.", visible=True)
944
- ])
945
- updates.extend([gr.update(visible=False) for _ in range(len(param_components) * 3)])
946
- else:
947
- updates.extend([
 
 
 
 
948
  gr.update(visible=True),
949
- gr.update(value=f"⚠️ Required Parameters ({len(all_parameters)})", visible=True)
950
- ])
951
- for i in range(5):
952
- if i < len(all_parameters):
953
- param = all_parameters[i]
954
- emoji = "🔑" if param['type'] == 'path' else "🔍"
955
- updates.extend([
956
- gr.update(visible=True),
957
- gr.update(visible=True, value=f"Endpoint: {param['endpoint']} - {param['type'].title()} Parameter"),
958
- gr.update(visible=True, label=f"{emoji} {param['name']}", placeholder=param['description'])
959
- ])
960
- else:
961
- updates.extend([gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)])
962
- return updates
963
-
964
- def update_fetch_button(required_params_count, *param_values):
965
- if required_params_count == 0:
966
- return gr.update(interactive=True)
967
- filled = all(val and val.strip() for val in param_values[:required_params_count])
968
- return gr.update(interactive=filled)
969
-
970
- def handle_api_call(api_choice_val, base_okta, tok_okta, base_inow, gtype, cid, csecret, base_iiq, uname, pw,
971
- locked_endpoints, *param_values):
972
- yield (
973
- gr.update(visible=True),
974
- gr.update(visible=False),
975
- gr.update(value=None),
976
- gr.update(value=None)
977
- )
978
-
979
- param_dict = {}
980
- param_idx = 0
981
- endpoints = get_endpoints(api_choice_val)
982
- for ep in locked_endpoints:
983
- endpoint = ep.split(" | GET")[0].strip()
984
- endpoint_spec = endpoints.get(endpoint, {}).get('get', {})
985
- for param in extract_path_params(endpoint):
986
- if param_idx < len(param_values):
987
- param_dict[param] = param_values[param_idx]
988
- param_idx += 1
989
- for _, name, required, _ in extract_query_params(endpoint_spec):
990
- if required and param_idx < len(param_values):
991
- param_dict[name] = param_values[param_idx]
992
- param_idx += 1
993
-
994
- clean_endpoints = [ep.split(" | GET")[0].strip() for ep in locked_endpoints]
995
-
996
- if api_choice_val == "Okta (JSON)":
997
- result = handle_okta_call(base_okta, tok_okta, "", param_dict, clean_endpoints)
998
- elif api_choice_val == "SailPoint IdentityNow (YAML)":
999
- result = handle_identitynow_call(base_inow, gtype, cid, csecret, "", param_dict, clean_endpoints)
1000
- else:
1001
- result = handle_iiq_call(base_iiq, uname, pw, "", param_dict, clean_endpoints)
1002
-
1003
- if not result or not isinstance(result, tuple) or len(result) < 2:
1004
- data = {"error": "Invalid API response format"}
1005
- download_file = None
1006
- else:
1007
- api_data = result[0]
1008
- download_file = result[1] if len(result) > 1 else None
1009
- if isinstance(api_data, dict):
1010
- data = api_data
1011
- elif isinstance(api_data, list):
1012
- data = {"results": api_data}
1013
- elif api_data is None:
1014
- data = {"message": "No data returned from the API"}
1015
  else:
1016
- data = {"raw_response": str(api_data)}
1017
-
1018
- # Convert data to JSON string for gr.Code
1019
- import json
1020
- data_str = json.dumps(data, indent=2)
1021
-
1022
- yield (
1023
- gr.update(visible=False),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1024
  gr.update(visible=True),
1025
- gr.update(value=data_str),
1026
- gr.update(value=download_file)
 
1027
  )
1028
 
 
 
 
 
 
 
 
 
 
 
 
1029
  api_choice.change(
1030
- fn=update_cred_sections,
1031
  inputs=[api_choice],
 
 
 
 
 
 
 
1032
  outputs=[
1033
- okta_cred, inow_cred, iiq_cred,
1034
- endpoints_section, param_group, fetch_section, results_section,
1035
- results_out, download_out, credentials_message,
1036
- locked_endpoints_state, continue_btn, edit_btn, reset_btn
1037
- ] + [cb for _, cb in accordion_placeholders] +
1038
- [comp for group, display, input_box in param_components for comp in (group, display, input_box)] +
1039
- [fetch_btn, required_params_count] +
1040
- [input_box for _, _, input_box in param_components]
1041
  )
1042
 
1043
- verify_btn.click(
1044
- fn=verify_and_load_endpoints,
1045
- inputs=[
1046
- api_choice,
1047
- api_base_url_okta, api_token,
1048
- api_base_url_inow, grant_type, client_id, client_secret,
1049
- api_base_url_iiq, username, password
1050
- ],
1051
- outputs=[
1052
- loading_indicator, credentials_message,
1053
- endpoints_section, fetch_section
1054
- ] + [comp for acc, cb in accordion_placeholders for comp in (acc, cb)]
1055
  )
1056
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1057
  continue_btn.click(
1058
  fn=lock_selected_endpoints,
1059
  inputs=[cb for _, cb in accordion_placeholders],
@@ -1062,10 +1165,7 @@ with gr.Blocks(css=custom_css) as demo:
1062
  continue_btn,
1063
  edit_btn,
1064
  reset_btn
1065
- ] + [cb for _, cb in accordion_placeholders] +
1066
- [param_group, param_header] +
1067
- [comp for group, display, input_box in param_components for comp in (group, display, input_box)] +
1068
- [fetch_btn, required_params_count]
1069
  )
1070
 
1071
  edit_btn.click(
@@ -1076,10 +1176,7 @@ with gr.Blocks(css=custom_css) as demo:
1076
  continue_btn,
1077
  edit_btn,
1078
  reset_btn
1079
- ] + [cb for _, cb in accordion_placeholders] +
1080
- [param_group, param_header] +
1081
- [comp for group, display, input_box in param_components for comp in (group, display, input_box)] +
1082
- [fetch_btn, required_params_count]
1083
  )
1084
 
1085
  reset_btn.click(
@@ -1090,35 +1187,54 @@ with gr.Blocks(css=custom_css) as demo:
1090
  continue_btn,
1091
  edit_btn,
1092
  reset_btn
1093
- ] + [cb for _, cb in accordion_placeholders] +
1094
- [param_group, param_header] +
1095
- [comp for group, display, input_box in param_components for comp in (group, display, input_box)] +
1096
- [fetch_btn, required_params_count]
1097
  )
1098
 
1099
- for _, _, input_box in param_components:
1100
- input_box.change(
1101
- fn=update_fetch_button,
1102
- inputs=[required_params_count] + [input_box for _, _, input_box in param_components],
1103
- outputs=[fetch_btn]
1104
- )
1105
-
1106
- fetch_btn.click(
1107
- fn=handle_api_call,
1108
- inputs=[
1109
- api_choice,
1110
- api_base_url_okta, api_token,
1111
- api_base_url_inow, grant_type, client_id, client_secret,
1112
- api_base_url_iiq, username, password,
1113
- locked_endpoints_state
1114
- ] + [input_box for _, _, input_box in param_components],
1115
  outputs=[
1116
- fetch_loading_indicator,
1117
- results_section,
1118
- results_out,
1119
  download_out
1120
  ]
1121
  )
1122
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1123
  if __name__ == "__main__":
1124
- demo.launch(show_error=True)
 
1
  import requests
2
  import yaml
3
+ import json
4
+ import zipfile
5
+ import io
6
+ import tempfile
7
  from identityNow import handle_identitynow_call
8
  from okta import handle_okta_call
9
  from iiq import handle_iiq_call
 
12
  import os
13
  from dotenv import load_dotenv
14
  from pathlib import Path
15
+ from api_schema_generatorV5 import ApiSchemaGeneratorV5
16
 
17
  # Load environment variables
18
  script_dir = Path(__file__).resolve().parent
 
34
  print("No endpoints found in the specification.")
35
  return {}
36
 
37
+ valid_methods = ['get', 'post'] # Only process GET and POST endpoints
38
  for path, methods in api_spec["paths"].items():
39
  endpoints[path] = {}
40
  if not methods or not isinstance(methods, dict):
 
69
  print("No endpoints found in the specification.")
70
  return {}
71
 
72
+ valid_methods = ['get', 'post'] # Only process GET and POST endpoints
73
  for path, methods in api_spec["paths"].items():
74
  endpoints[path] = {}
75
  if not methods or not isinstance(methods, dict):
 
90
  endpoints[path][method.lower()] = endpoint_info
91
  return endpoints
92
 
93
+ def get_endpoints(spec_choice, base_url=None):
94
+ # If base_url is provided, use it directly
95
+ if base_url:
96
+ spec_url = base_url
97
+ if "JSON" in spec_choice:
98
+ return fetch_api_endpoints_json(spec_url), spec_url
99
+ return fetch_api_endpoints_yaml(spec_url), spec_url
100
+
101
+ # Otherwise, try to use environment variables as fallback
102
  api_spec_options = {
103
  "Okta (JSON)": os.getenv("OKTA_API_SPEC"),
104
  "SailPoint IdentityNow (YAML)": os.getenv("IDENTITY_NOW_API_SPEC"),
 
106
  }
107
  spec_url = api_spec_options.get(spec_choice)
108
  if not spec_url:
109
+ print(f"No API specification URL found for {spec_choice}")
110
+ return {}, None
111
+
112
  if "JSON" in spec_choice:
113
+ return fetch_api_endpoints_json(spec_url), spec_url
114
+ return fetch_api_endpoints_yaml(spec_url), spec_url
115
 
116
+ def group_endpoints(endpoints, spec_choice, endpoint_type='get'):
117
+ """Group endpoints by their first path segment, filtering by endpoint type"""
118
  groups = {}
119
+ if endpoint_type == 'all':
120
+ # For 'all' type, include all endpoints regardless of method
121
+ if spec_choice == "Okta (JSON)":
122
+ for path, methods in endpoints.items():
123
+ clean_path = path.replace('/api/v1/', '')
124
+ segments = clean_path.strip("/").split("/")
125
+ group_key = segments[0] if segments else "other"
126
+ if group_key not in groups:
127
+ groups[group_key] = {}
128
+ groups[group_key][path] = methods
129
+ else:
130
+ for path, methods in endpoints.items():
131
+ segments = path.strip("/").split("/")
132
+ group_key = segments[0] if segments[0] != "" else "other"
133
+ if group_key not in groups:
134
+ groups[group_key] = {}
135
+ groups[group_key][path] = methods
136
  else:
137
+ # For specific method types (get, post)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  if spec_choice == "Okta (JSON)":
139
+ for path, methods in endpoints.items():
140
+ if endpoint_type not in methods:
141
+ continue
142
+
143
+ clean_path = path.replace('/api/v1/', '')
144
+ segments = clean_path.strip("/").split("/")
145
+ group_key = segments[0] if segments else "other"
146
+ if group_key not in groups:
147
+ groups[group_key] = {}
148
+ groups[group_key][path] = methods
149
+ else:
150
+ for path, methods in endpoints.items():
151
+ if endpoint_type not in methods:
152
+ continue
153
+
154
+ segments = path.strip("/").split("/")
155
+ group_key = segments[0] if segments[0] != "" else "other"
156
+ if group_key not in groups:
157
+ groups[group_key] = {}
158
+ groups[group_key][path] = methods
159
+ return groups
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
 
161
+ def generate_schema_files(api_choice_val, selected_endpoints, api_spec_url, custom_name=None):
162
+ """Generate JSON and XML schema files for selected endpoints"""
163
+ # Determine API name based on choice
164
+ if api_choice_val == "Others" and custom_name:
165
+ api_name = custom_name.lower().replace(" ", "_")
166
+ else:
167
+ api_name = api_choice_val.split(" (")[0].lower().replace(" ", "_")
168
+
169
+ # Debug print to verify api_spec_url is not empty
170
+ print(f"API Choice: {api_choice_val}")
171
+ print(f"API Spec URL: {api_spec_url}")
172
+ print(f"Selected endpoints count: {len(selected_endpoints)}")
173
+ print(f"Using API name: {api_name}")
174
+
175
+ # Create schema generator with path+method pairs
176
+ generator = ApiSchemaGeneratorV5(api_spec_url, api_name=api_name, selected_endpoints=selected_endpoints)
177
+
178
+ # Generate files in memory
179
+ meta_data = generator.generate_datasource_plugin_meta()
180
+ schema_xml = generator.generate_default_schema()
181
+
182
+ # Create JSON string
183
+ json_str = json.dumps(meta_data, indent=2)
184
+
185
+ # Create a zip file in memory
186
+ zip_buffer = io.BytesIO()
187
+ with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
188
+ zip_file.writestr(f"{api_name}_datasource_plugin_meta.json", json_str)
189
+ zip_file.writestr(f"{api_name}_default_schema.orx", schema_xml)
190
+
191
+ zip_buffer.seek(0)
192
+
193
+ # Return both the individual files and the zip
194
+ return {
195
+ "json": json_str,
196
+ "xml": schema_xml,
197
+ "zip": zip_buffer
198
+ }
199
 
200
+ def update_api_url(api_choice_val):
201
+ global current_api_spec_url
202
+
203
+ if api_choice_val == "Others":
204
+ # For "Others", we don't set a URL yet - user will input it
205
+ current_api_spec_url = ""
206
+ return gr.update(visible=True), gr.update(value=""), gr.update(value="")
207
+
208
+ # Get URL from environment variables
209
+ api_spec_options = {
210
+ "Okta (JSON)": os.getenv("OKTA_API_SPEC"),
211
+ "SailPoint IdentityNow (YAML)": os.getenv("IDENTITY_NOW_API_SPEC"),
212
+ "Sailpoint IIQ (YAML)": os.getenv("IIQ_API_SPEC")
213
+ }
214
+
215
+ url = api_spec_options.get(api_choice_val, "")
216
+ print(f"Setting API spec URL to: {url}") # Debug print
217
+
218
+ # Update the global variable
219
+ current_api_spec_url = url
220
+
221
+ return gr.update(visible=False), gr.update(value=url), gr.update(value=url) # Update both text field and state
222
 
223
  # ----------------------------- CSS -----------------------------
224
  custom_css = """
 
317
  margin: 20px auto;
318
  }
319
 
320
+ .loading-text {
321
+ color: black;
322
+ font-size: 24px;
323
+ font-weight: bold;
324
+ margin-top: 20px;
325
+ text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.8);
326
+ }
327
+
328
+ .fetch-loading-indicator {
329
+ display: flex;
330
+ justify-content: center;
331
+ align-items: center;
332
+ height: 100px;
333
+ }
334
+
335
+ .fetch-loading-indicator .loading-spinner {
336
+ border-width: 6px;
337
+ width: 50px;
338
+ height: 50px;
339
+ }
340
+
341
  @keyframes spin {
342
  0% { transform: rotate(0deg); }
343
  100% { transform: rotate(360deg); }
 
507
  height: 50px;
508
  }
509
 
510
+ .endpoint-type-selector {
511
+ margin-bottom: 20px;
512
+ padding: 15px;
513
+ background: #333333;
514
+ border-radius: 8px;
515
+ border: 1px solid #ddd;
516
+ }
517
+
518
+ .endpoint-type-selector label {
519
+ color: white !important;
520
+ font-weight: bold !important;
521
+ font-size: 18px !important;
522
+ }
523
+
524
+ .endpoint-type-selector > .block > label:first-child {
525
+ color: #000000;
526
+ font-weight: 700;
527
+ font-size: 18px;
528
+ }
529
+
530
+ .endpoint-type-selector .gr-radio-group label {
531
+ color: white;
532
+ font-weight: 500;
533
+ }
534
+
535
  @keyframes fadeIn {
536
  from { opacity: 0; }
537
  to { opacity: 1; }
 
610
  """
611
 
612
  with gr.Blocks(css=custom_css) as demo:
613
+ gr.HTML("<div class='banner'>Data Connector Demo</div>")
614
+
615
+ # User Guide Modal
616
+ user_guide_html = gr.HTML("""
617
  <div id="user-guide-overlay" class="user-guide-overlay"></div>
618
  <div id="user-guide-modal" class="user-guide-modal">
619
  <h2>Data Connector - User Guide</h2>
620
+ <p>Welcome to Data Connector, a tool that helps you easily extract and explore data from any API platform and generate schema files.</p>
621
 
622
  <h3>Getting Started</h3>
623
  <p><strong>Step 1: Select an API Platform</strong><br/>
624
  Click on one of the three available platforms:<br/>
625
  Okta: For Okta Identity Cloud data<br/>
626
  SailPoint IdentityNow: For cloud-based identity governance data<br/>
627
+ SailPoint IIQ: For on-premise identity governance data<br/>
628
+ Others: For custom API specifications</p>
629
+
630
+ <p><strong>Step 2: Enter API Specification URL</strong><br/>
631
+ Enter the URL for the API specification (OpenAPI/Swagger) for your selected platform.</p>
632
+
633
+ <p><strong>Step 3: Load API Specification</strong><br/>
634
+ Click the "Load API Specification" button to fetch and parse the API endpoints.</p>
635
+
636
+ <p><strong>Step 4: Select Endpoint Type</strong><br/>
637
+ Choose between GET or POST endpoints to display.</p>
638
+
639
+ <p><strong>Step 5: Browse and Select Endpoints</strong><br/>
640
+ After loading, you'll see API endpoints grouped by category<br/>
 
 
 
 
 
 
 
 
 
 
 
641
  Click on any category to expand it and see available endpoints<br/>
642
+ Select the checkboxes next to the endpoints you want to include<br/>
643
+ You can select multiple endpoints across different categories</p>
644
+
645
+ <p><strong>Step 6: Generate Schema</strong><br/>
646
+ Click the "Generate Schema" button to create schema files based on your selected endpoints.</p>
 
 
 
 
 
 
 
 
 
 
647
 
648
  <p><strong>Step 7: View and Download Results</strong><br/>
649
+ Results will display in both JSON and XML formats<br/>
650
+ Review the generated schema files<br/>
651
+ Click the download button to save all files as a ZIP file</p>
 
652
 
653
  <h3>Tips and Troubleshooting</h3>
654
  <ul>
655
+ <li><strong>API Specification URLs:</strong> Always include the full URL with https:// prefix</li>
656
+ <li><strong>No Endpoints Found:</strong> Check that the API specification is valid and accessible</li>
657
+ <li><strong>Schema Generation:</strong> Make sure to select at least one endpoint before generating schemas</li>
 
 
658
  </ul>
659
 
 
 
 
660
  <h3>Need Help?</h3>
661
+ <p>If you need assistance or encounter issues, refer to the official API documentation for detailed guidance:</p>
662
  <ul>
663
+ <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>
664
+ <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>
665
+ <li><strong>Okta:</strong> <a href="https://developer.okta.com/docs/reference/core-okta-api/" target="_blank">Core Okta API</a></li>
666
  </ul>
667
 
668
  <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>
 
699
  >
700
  User Guide
701
  </button>
702
+ """, visible=True)
 
703
 
704
+ # State variables
 
 
705
  locked_endpoints_state = gr.State([])
706
+ endpoint_type_state = gr.State("get") # Default to GET endpoints
707
+ api_spec_url_state = gr.State("")
708
  required_params_count = gr.State(0)
709
 
710
+ # API selection with card-style UI
711
  api_options = [
712
  {"icon": "fa-cloud", "title": "Okta", "description": "Identity and Access Management", "value": "Okta (JSON)"},
713
  {"icon": "fa-user-shield", "title": "SailPoint IdentityNow", "description": "Cloud Identity Governance", "value": "SailPoint IdentityNow (YAML)"},
714
  {"icon": "fa-network-wired", "title": "Sailpoint IIQ", "description": "On-premise Identity Governance", "value": "Sailpoint IIQ (YAML)"},
715
+ {"icon": "fa-cogs", "title": "Other", "description": "Custom API Specification", "value": "Others"}
716
  ]
717
+
718
+ # Create a radio button with custom styling to look like cards
719
  choices = [(opt["title"], opt["value"]) for opt in api_options]
720
+ api_choice = gr.Radio(choices=choices, label="Select API Platform", type="value", elem_classes="api-cards")
721
+
722
+ # Custom API inputs (visible when "Others" is selected)
723
+ with gr.Group(visible=False) as custom_api_group:
724
+ custom_api_name = gr.Textbox(label="Custom API Name", placeholder="Enter API name (e.g., 'My API')")
725
+ custom_api_format = gr.Radio(choices=["JSON", "YAML"], label="API Format", value="JSON")
726
+
727
+ # API Base URL input
728
+ with gr.Row():
729
+ api_base_url = gr.Textbox(label="API Specification URL", placeholder="Enter API specification URL")
730
+
731
+ # Connect API choice to custom API visibility
732
+ api_choice.change(
733
+ fn=update_api_url,
734
+ inputs=[api_choice],
735
+ outputs=[custom_api_group, api_base_url, api_spec_url_state]
736
+ )
737
 
738
+ # Update API spec URL state when the text field changes
739
+ def update_api_spec_url_state(url):
740
+ global current_api_spec_url
741
+ print(f"Updating API spec URL state to: {url}")
742
+ # Also update the global variable
743
+ current_api_spec_url = url
744
+ return gr.update(value=url)
745
+
746
+ api_base_url.change(
747
+ fn=update_api_spec_url_state,
748
+ inputs=[api_base_url],
749
+ outputs=[api_spec_url_state]
750
+ )
751
+
752
+ # Load API Specification button
753
+ load_btn = gr.Button("Load API Specification", elem_classes="action-btn")
754
  loading_indicator = gr.HTML("<div class='loading-spinner'></div>", visible=False)
755
+ message_box = gr.HTML("", visible=False)
756
+
757
+ # Endpoints section
758
+ with gr.Group(visible=False, elem_classes="endpoints-section") as endpoints_section:
759
+ # Endpoint type selector (GET, POST, or ALL)
760
+ endpoint_type = gr.Radio(
761
+ choices=["GET", "POST", "ALL"],
762
+ value="GET",
763
+ label="Endpoints Type",
764
+ elem_classes="endpoint-type-selector"
765
+ )
766
+
767
+ # Select All / Deselect All checkboxes
768
+ with gr.Row():
769
+ select_all_cb = gr.Checkbox(label="Select All", value=False)
770
+ deselect_all_cb = gr.Checkbox(label="Deselect All", value=False)
771
+
772
  with gr.Row():
773
  edit_btn = gr.Button("Edit", interactive=False, elem_classes="reset-btn")
774
  reset_btn = gr.Button("Reset", interactive=False, elem_classes="reset-btn")
775
+
776
+ # Accordion for endpoint groups
777
+ max_groups = 100 # Reduced for simplicity
778
  accordion_placeholders = []
779
  for i in range(max_groups):
780
  with gr.Accordion(label="", open=False, visible=False, elem_classes="custom-accordion") as acc:
781
  cb = gr.CheckboxGroup(label="", choices=[], value=[], elem_classes="custom-checkbox")
782
  accordion_placeholders.append((acc, cb))
783
+
784
+ continue_btn = gr.Button("Generate Schema", elem_classes="action-btn")
785
 
786
+ # Parameters section
787
  with gr.Group(elem_classes="param-group", visible=False) as param_group:
788
  param_header = gr.Markdown("### Parameters Required")
789
  param_components = []
 
793
  param_input = gr.Textbox(label="Parameter Value", visible=False)
794
  param_components.append((group, param_display, param_input))
795
 
796
+ # Results section
797
+ with gr.Group(visible=False, elem_classes="results-section") as results_section:
 
 
 
798
  gr.Markdown("### Results")
799
+ with gr.Tabs() as result_tabs:
800
+ with gr.TabItem("JSON Schema"):
801
+ json_output = gr.Code(label="JSON Schema", language="json", elem_classes="gr-code")
802
+ with gr.TabItem("XML Schema"):
803
+ xml_output = gr.Textbox(label="XML Schema", lines=20, elem_classes="gr-code")
804
+ download_out = gr.File(label="Download Schema Files (ZIP)", visible=True, interactive=True)
805
+
806
+ # Function to load API specification
807
+ def load_api_spec(api_choice_val, base_url, custom_name=None, custom_format=None):
808
+ global current_api_spec_url
809
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
810
  yield (
811
+ gr.update(visible=True), # Show loading indicator
812
+ gr.update(value="", visible=False),
813
  gr.update(visible=False),
814
  gr.update(visible=False),
815
+ *[gr.update(visible=False, label="", value=[]) for _ in range(max_groups * 2)],
816
+ gr.update(value="")
817
  )
818
+ # Handle custom API
819
+ if api_choice_val == "Others":
820
+ # Create a custom API choice value if name is provided
821
+ if custom_name:
822
+ api_choice_val = f"{custom_name} ({custom_format})"
823
+ # Make sure we use the base_url directly for "Others" option
824
+ spec_url = base_url
825
+ print(f"Using custom API spec URL: {spec_url}")
826
  else:
827
+ # Get endpoints and API spec URL for predefined platforms
828
+ _, spec_url = get_endpoints(api_choice_val, None)
829
+ if base_url: # If user provided a URL, use it instead
830
+ spec_url = base_url
831
+ print(f"Using user-provided API spec URL for {api_choice_val}: {spec_url}")
832
+ else:
833
+ print(f"Using default API spec URL for {api_choice_val}: {spec_url}")
834
+
835
+ # Store the current API spec URL
836
+ current_api_spec_url = spec_url
837
+ print(f"Setting current_api_spec_url to: {current_api_spec_url}")
838
+
839
+ # Get endpoints using the determined spec_url
840
+ endpoints, _ = get_endpoints(api_choice_val, spec_url)
841
+
842
  if not endpoints:
843
  yield (
844
+ gr.update(visible=True),
845
+ gr.update(value="<div style='color: red;'>No endpoints found</div>", visible=True),
846
  gr.update(visible=False),
847
  gr.update(visible=False),
848
+ *[gr.update(visible=False, label="", value=[]) for _ in range(max_groups * 2)],
849
+ gr.update(value=spec_url)
850
  )
851
+
852
+ # Group endpoints by GET method (default)
853
+ groups = group_endpoints(endpoints, api_choice_val, 'get')
854
  group_keys = list(groups.keys())
855
  updates = []
856
+
857
  for i in range(max_groups):
858
  if i < len(group_keys):
859
  group = group_keys[i]
860
  choices = [f"{ep} | GET - {methods['get'].get('summary', 'No summary')}" for ep, methods in groups[group].items() if 'get' in methods]
861
  updates.extend([
862
  gr.update(label=group, visible=bool(choices), open=False),
863
+ gr.update(choices=choices, value=[], visible=bool(choices))
864
  ])
865
  else:
866
  updates.extend([gr.update(visible=False, label=""), gr.update(visible=False, choices=[], value=[])])
867
+
868
  yield (
869
  gr.update(visible=False),
870
+ gr.update(value="<div class='success-message'><i class='fas fa-check-circle'></i> API specification loaded successfully</div>", visible=True),
 
871
  gr.update(visible=True),
872
+ gr.update(visible=False),
873
+ *updates,
874
+ gr.update(value=spec_url)
875
  )
876
 
877
+ # Function to update endpoints based on selected type (GET, POST, or ALL)
878
+ def update_endpoint_display(api_choice_val, endpoint_type_val, api_spec_url):
879
+ # Convert endpoint type to lowercase
880
+ endpoint_type_val = endpoint_type_val.lower()
881
+
882
+ # Get endpoints
883
+ endpoints, _ = get_endpoints(api_choice_val, api_spec_url)
884
+ if not endpoints:
885
+ # Even if no endpoints are found, we should still return the endpoint type state
886
+ # and ensure the endpoints section remains visible
887
+ return [gr.update(visible=False, label="", value=[]) for _ in range(max_groups * 2)] + [gr.update(value=endpoint_type_val)]
888
+
889
+ # Group endpoints by selected type
890
+ groups = group_endpoints(endpoints, api_choice_val, endpoint_type_val)
891
+ group_keys = list(groups.keys())
892
+ updates = []
893
+
894
+ for i in range(max_groups):
895
+ if i < len(group_keys):
896
+ group = group_keys[i]
897
+
898
+ if endpoint_type_val == 'all':
899
+ # For 'all' type, include both GET and POST endpoints
900
+ choices = []
901
+ for ep, methods in groups[group].items():
902
+ for method_type in ['get', 'post']:
903
+ if method_type in methods:
904
+ choices.append(f"{ep} | {method_type.upper()} - {methods[method_type].get('summary', 'No summary')}")
905
+ else:
906
+ # For specific method types
907
+ choices = [f"{ep} | {endpoint_type_val.upper()} - {methods[endpoint_type_val].get('summary', 'No summary')}"
908
+ for ep, methods in groups[group].items() if endpoint_type_val in methods]
909
+
910
+ updates.extend([
911
+ gr.update(label=group, visible=bool(choices), open=False),
912
+ gr.update(choices=choices, value=[], visible=bool(choices))
913
+ ])
914
+ else:
915
+ updates.extend([gr.update(visible=False, label=""), gr.update(visible=False, choices=[], value=[])])
916
+
917
+ return updates + [gr.update(value=endpoint_type_val)]
918
+
919
+ # Function to lock selected endpoints and generate schema directly
920
  def lock_selected_endpoints(*checkbox_values):
921
  all_selected = [sel for group in checkbox_values if isinstance(group, list) for sel in group]
922
  if not all_selected:
 
924
  [],
925
  gr.update(interactive=True),
926
  gr.update(interactive=False),
 
 
 
 
 
927
  gr.update(interactive=False),
928
+ *[gr.update(interactive=True) for _ in range(max_groups)]
929
  ]
 
 
 
 
 
930
 
931
  return [
932
  all_selected,
933
  gr.update(interactive=False),
934
  gr.update(interactive=True),
935
+ gr.update(interactive=True),
936
+ *[gr.update(interactive=False) for _ in range(max_groups)]
 
 
 
937
  ]
938
 
939
+ # Function to unlock selected endpoints
940
  def unlock_selected_endpoints():
941
  return [
942
  gr.update(value=[]),
943
  gr.update(interactive=True),
944
  gr.update(interactive=False),
 
 
 
 
 
945
  gr.update(interactive=False),
946
+ *[gr.update(interactive=True) for _ in range(max_groups)]
947
  ]
948
 
949
+ # Function to reset selected endpoints
950
  def reset_selected_endpoints():
951
  return [
952
  gr.update(value=[]),
953
  gr.update(interactive=True),
954
  gr.update(interactive=False),
 
 
 
 
 
955
  gr.update(interactive=False),
956
+ *[gr.update(interactive=True, value=[]) for _ in range(max_groups)]
957
  ]
958
 
959
+ # Store the current API spec URL in a global variable
960
+ current_api_spec_url = ""
961
+
962
+ # Function to generate schema files
963
+ def generate_schemas(api_choice_val, api_spec_url_value, custom_api_name_value, *checkbox_values):
964
+ global current_api_spec_url
965
+
966
+ print(f"Generate schemas called with api_choice_val: {api_choice_val}")
967
+ print(f"API spec URL value from state: {api_spec_url_value}")
968
+ print(f"Current API spec URL: {current_api_spec_url}")
969
+ print(f"Custom API name from UI: {custom_api_name_value}")
970
+
971
+ # Get custom API name if "Others" is selected
972
+ custom_name = None
973
+ if api_choice_val == "Others":
974
+ # Use the custom API name passed as a parameter
975
+ custom_name = custom_api_name_value
976
+
977
+ # If custom_name is None or empty, use a default name
978
+ if not custom_name or custom_name.strip() == "":
979
+ custom_name = "custom_api"
980
+ print(f"Using default custom API name: {custom_name}")
981
+
982
+ # Extract both path and method from selected endpoints
983
+ all_selected = []
984
+ for group in checkbox_values:
985
+ if isinstance(group, list):
986
+ for sel in group:
987
+ # Parse both path and method from the selection
988
+ parts = sel.split(" | ")
989
+ if len(parts) >= 2:
990
+ path = parts[0].strip()
991
+ method = parts[1].split(" - ")[0].strip().lower() # Extract GET or POST
992
+ all_selected.append((path, method))
993
+
994
+ if not all_selected:
995
+ return (
996
+ gr.update(visible=False),
997
+ gr.update(value=""),
998
+ gr.update(value=""),
999
+ gr.update(value=None)
1000
+ )
1001
+
1002
+ # Use the current API spec URL that was set when loading the API specification
1003
+ api_spec_url = current_api_spec_url
1004
+
1005
+ # If we still don't have a URL, try to use the one from the state
1006
+ if not api_spec_url:
1007
+ api_spec_url = api_spec_url_value
1008
+ print(f"Using API spec URL from state: {api_spec_url}")
1009
+
1010
+ print(f"API Choice: {api_choice_val}")
1011
+ print(f"API Spec URL: {api_spec_url}")
1012
+ print(f"Selected endpoints count: {len(all_selected)}")
1013
+
1014
+ # Check if api_choice_val is None
1015
+ if api_choice_val is None:
1016
+ print(f"Error: API choice is None")
1017
+ return (
1018
  gr.update(visible=True),
1019
+ gr.update(value="Error: Please select an API platform first"),
1020
+ gr.update(value="Error: Please select an API platform first"),
1021
+ gr.update(value=None)
1022
+ )
1023
+
1024
+ # Check if api_spec_url is empty
1025
+ if not api_spec_url:
1026
+ print(f"Error: API spec URL is empty")
1027
+ return (
1028
  gr.update(visible=True),
1029
+ gr.update(value="Error: API specification URL is empty. Please select a valid API platform or enter a URL."),
1030
+ gr.update(value="Error: API specification URL is empty. Please select a valid API platform or enter a URL."),
1031
+ gr.update(value=None)
1032
+ )
1033
+
1034
+ # Generate schema files
1035
+ try:
1036
+ # Pass the custom API name if it's available
1037
+ if api_choice_val == "Others" and custom_name:
1038
+ result = generate_schema_files(api_choice_val, all_selected, api_spec_url, custom_name)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1039
  else:
1040
+ result = generate_schema_files(api_choice_val, all_selected, api_spec_url)
1041
+
1042
+ # Create a temporary file with .zip extension
1043
+ fd, temp_path = tempfile.mkstemp(suffix='.zip')
1044
+ os.close(fd)
1045
+
1046
+ # Write the BytesIO content to the temporary file
1047
+ with open(temp_path, 'wb') as f:
1048
+ f.write(result["zip"].getvalue())
1049
+
1050
+ print(f"Temporary zip file created at: {temp_path}")
1051
+
1052
+ except Exception as e:
1053
+ print(f"Error generating schema files: {e}")
1054
+ return (
1055
+ gr.update(visible=True),
1056
+ gr.update(value=f"Error generating schema files: {str(e)}"),
1057
+ gr.update(value=f"Error generating schema files: {str(e)}"),
1058
+ gr.update(value=None)
1059
+ )
1060
+
1061
+ return (
1062
  gr.update(visible=True),
1063
+ gr.update(value=result["json"]),
1064
+ gr.update(value=result["xml"]),
1065
+ gr.update(value=temp_path) # Use the temporary file path instead of BytesIO
1066
  )
1067
 
1068
+ # Function to handle API choice change - collapses sections when API is changed
1069
+ def handle_api_choice_change(api_choice_val):
1070
+ global current_api_spec_url
1071
+ # Reset the current API spec URL when changing API choice
1072
+ current_api_spec_url = ""
1073
+ # First, get the outputs from update_api_url
1074
+ custom_api_visibility, api_base_url_value, api_spec_url_value = update_api_url(api_choice_val)
1075
+ # Then, add the visibility updates for endpoints and results sections
1076
+ return custom_api_visibility, api_base_url_value, api_spec_url_value, gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)
1077
+
1078
+ # Update API choice to handle collapsing sections
1079
  api_choice.change(
1080
+ fn=handle_api_choice_change,
1081
  inputs=[api_choice],
1082
+ outputs=[custom_api_group, api_base_url, api_spec_url_state, endpoints_section, results_section, message_box]
1083
+ )
1084
+
1085
+ # Connect UI components to functions
1086
+ load_btn.click(
1087
+ fn=load_api_spec,
1088
+ inputs=[api_choice, api_base_url, custom_api_name, custom_api_format],
1089
  outputs=[
1090
+ loading_indicator,
1091
+ message_box,
1092
+ endpoints_section,
1093
+ results_section
1094
+ ] + [comp for acc, cb in accordion_placeholders for comp in (acc, cb)] + [api_spec_url_state]
 
 
 
1095
  )
1096
 
1097
+ # Function to update endpoints and ensure endpoints section remains visible
1098
+ def update_endpoints_and_maintain_visibility(api_choice_val, endpoint_type_val, api_spec_url):
1099
+ # Get updates for accordion placeholders and endpoint type state
1100
+ updates = update_endpoint_display(api_choice_val, endpoint_type_val, api_spec_url)
1101
+ # Return these updates plus an update to keep the endpoints section visible
1102
+ return updates + [gr.update(visible=True)]
1103
+
1104
+ endpoint_type.change(
1105
+ fn=update_endpoints_and_maintain_visibility,
1106
+ inputs=[api_choice, endpoint_type, api_spec_url_state],
1107
+ outputs=[comp for acc, cb in accordion_placeholders for comp in (acc, cb)] + [endpoint_type_state, endpoints_section]
 
1108
  )
1109
 
1110
+ # Function to handle select all checkbox
1111
+ def select_all_endpoints(select_all, endpoint_type_val, *current_values):
1112
+ if not select_all:
1113
+ return [gr.update(value=False)] + [gr.update(value=val) for val in current_values]
1114
+
1115
+ # Get all visible checkboxes
1116
+ updates = []
1117
+
1118
+ # For each checkbox in the accordion
1119
+ for i, (acc, cb) in enumerate(accordion_placeholders):
1120
+ # If this is a valid index in current_values
1121
+ if i < len(current_values):
1122
+ # Get all choices for this checkbox
1123
+ if hasattr(cb, 'choices'):
1124
+ updates.append(gr.update(value=cb.choices))
1125
+ else:
1126
+ # If we can't get choices, use the update_endpoint_display function
1127
+ # to get the choices for this accordion group
1128
+ updates.append(gr.update(value=current_values[i]))
1129
+ else:
1130
+ # If we don't have a current value, just append an empty update
1131
+ updates.append(gr.update(value=[]))
1132
+
1133
+ # Reset deselect all checkbox
1134
+ return [gr.update(value=False)] + updates
1135
+
1136
+ # Function to handle deselect all checkbox
1137
+ def deselect_all_endpoints(deselect_all, *current_values):
1138
+ if not deselect_all:
1139
+ return [gr.update(value=False)] + [gr.update(value=val) for val in current_values]
1140
+
1141
+ # Deselect all checkboxes
1142
+ updates = [gr.update(value=[]) for _ in current_values]
1143
+
1144
+ # Reset select all checkbox
1145
+ return [gr.update(value=False)] + updates
1146
+
1147
+ # Connect select all and deselect all checkboxes
1148
+ select_all_cb.change(
1149
+ fn=select_all_endpoints,
1150
+ inputs=[select_all_cb, endpoint_type_state] + [cb for _, cb in accordion_placeholders],
1151
+ outputs=[deselect_all_cb] + [cb for _, cb in accordion_placeholders]
1152
+ )
1153
+
1154
+ deselect_all_cb.change(
1155
+ fn=deselect_all_endpoints,
1156
+ inputs=[deselect_all_cb] + [cb for _, cb in accordion_placeholders],
1157
+ outputs=[select_all_cb] + [cb for _, cb in accordion_placeholders]
1158
+ )
1159
+
1160
  continue_btn.click(
1161
  fn=lock_selected_endpoints,
1162
  inputs=[cb for _, cb in accordion_placeholders],
 
1165
  continue_btn,
1166
  edit_btn,
1167
  reset_btn
1168
+ ] + [cb for _, cb in accordion_placeholders]
 
 
 
1169
  )
1170
 
1171
  edit_btn.click(
 
1176
  continue_btn,
1177
  edit_btn,
1178
  reset_btn
1179
+ ] + [cb for _, cb in accordion_placeholders]
 
 
 
1180
  )
1181
 
1182
  reset_btn.click(
 
1187
  continue_btn,
1188
  edit_btn,
1189
  reset_btn
1190
+ ] + [cb for _, cb in accordion_placeholders]
 
 
 
1191
  )
1192
 
1193
+ continue_btn.click(
1194
+ fn=generate_schemas,
1195
+ inputs=[api_choice, api_spec_url_state, custom_api_name] + [cb for _, cb in accordion_placeholders],
 
 
 
 
 
 
 
 
 
 
 
 
 
1196
  outputs=[
1197
+ results_section,
1198
+ json_output,
1199
+ xml_output,
1200
  download_out
1201
  ]
1202
  )
1203
 
1204
+ # Function to return the zip file for download
1205
+ def download_schema_files(json_content, xml_content):
1206
+ # Generate schema files in memory
1207
+ if not json_content or not xml_content:
1208
+ return None
1209
+
1210
+ try:
1211
+ # Determine API name from JSON content
1212
+ meta_data = json.loads(json_content)
1213
+ api_name = meta_data.get("name", "schema").lower().replace(" ", "_")
1214
+
1215
+ # Create a zip file in memory
1216
+ zip_buffer = io.BytesIO()
1217
+ with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
1218
+ zip_file.writestr(f"{api_name}_datasource_plugin_meta.json", json_content)
1219
+ zip_file.writestr(f"{api_name}_default_schema.orx", xml_content)
1220
+
1221
+ zip_buffer.seek(0)
1222
+
1223
+ # Create a temporary file with .zip extension
1224
+ fd, temp_path = tempfile.mkstemp(suffix='.zip')
1225
+ os.close(fd)
1226
+
1227
+ # Write the BytesIO content to the temporary file
1228
+ with open(temp_path, 'wb') as f:
1229
+ f.write(zip_buffer.getvalue())
1230
+
1231
+ # Return the file path with a filename for download
1232
+ return (temp_path, f"{api_name}_schema_files.zip")
1233
+
1234
+ except Exception as e:
1235
+ print(f"Error creating download file: {e}")
1236
+ return None
1237
+
1238
+
1239
  if __name__ == "__main__":
1240
+ demo.launch(show_error=True)