jayanth922 commited on
Commit
1f4fd74
·
1 Parent(s): 5332e69

feat: minimal Auth; remove Profile Switcher & Spec Health panels

Browse files
Files changed (1) hide show
  1. openapi_loader.py +18 -202
openapi_loader.py CHANGED
@@ -1,40 +1,23 @@
1
  # openapi_loader.py
2
  from __future__ import annotations
3
- import json, os, re, sys
4
- from typing import Any, Dict, Tuple, Optional, List
5
  from urllib.parse import urlparse
6
  import requests
7
  import yaml
8
  import gradio as gr
9
- import inspect, keyword, base64, requests
10
  from openapi_spec_validator import validate_spec
11
- import sys, yaml
12
-
13
-
14
 
15
  Json = Dict[str, Any]
16
  _VAR_RE = re.compile(r"\{([^}]+)\}")
17
-
18
-
19
- import keyword
20
- import re
21
 
22
  _PY_IDENT = re.compile(r"^[A-Za-z_]\w*$")
23
 
24
  def _is_valid_identifier(name: str) -> bool:
25
  return bool(_PY_IDENT.match(name)) and not keyword.iskeyword(name)
26
 
27
- def _load_profile_from_yaml(name: str):
28
- """Load (spec, base_url, bearer) from profiles.yaml in CWD."""
29
- path = os.path.join(os.getcwd(), "profiles.yaml")
30
- if not os.path.exists(path):
31
- raise FileNotFoundError("profiles.yaml not found in the working directory.")
32
- data = yaml.safe_load(open(path, "r", encoding="utf-8")) or {}
33
- if name not in data:
34
- raise KeyError(f"profile '{name}' not found. Available: {list(data.keys())}")
35
- prof = data[name] or {}
36
- return (prof.get("spec"), prof.get("base_url"), prof.get("bearer") or None), list(data.keys())
37
-
38
 
39
  def sanitize_header_params(spec: Json) -> Tuple[Json, list]:
40
  """
@@ -174,18 +157,6 @@ def best_default_security(spec: Json) -> Optional[str]:
174
  return next(iter(schemes.keys())) if schemes else None
175
 
176
 
177
- def _load_profile_from_yaml(name: str) -> Tuple[Tuple[str, str, Optional[str]], List[str]]:
178
- path = os.path.join(os.getcwd(), "profiles.yaml")
179
- if not os.path.exists(path):
180
- raise FileNotFoundError("profiles.yaml not found.")
181
- data = yaml.safe_load(open(path, "r", encoding="utf-8"))
182
- if name not in data:
183
- raise KeyError(f"profile '{name}' not in profiles.yaml. Have: {list(data.keys())}")
184
- prof = data[name] or {}
185
- profile_names = list(data.keys())
186
- return (prof.get("spec"), prof.get("base_url"), prof.get("bearer")), profile_names
187
-
188
-
189
  def build_gradio_app_from_spec(spec: Json, base_url: Optional[str] = None,
190
  bearer_token: Optional[str] = None) -> gr.Blocks:
191
  if not base_url:
@@ -224,50 +195,6 @@ def build_gradio_app_from_spec(spec: Json, base_url: Optional[str] = None,
224
  gr.Markdown("### AnyAPI→MCP Factory — OpenAPI Explorer")
225
  gr.Markdown(f"**Base URL:** `{base_url}` \n**Auth:** {banner_auth}")
226
 
227
- # --- Profile Switcher (reloads the process with selected profile) ---
228
- with gr.Accordion("Profile switcher (restart with selected profile)", open=False):
229
- profile_list = []
230
- try:
231
- # Load list of profile names (and ignore the selected profile here)
232
- _tmp, profile_list = _load_profile_from_yaml("__dummy__") # will raise, we just want the list
233
- except KeyError as _e:
234
- # _e.args[0] has message with available list from loader; parse a best-effort
235
- msg = str(_e)
236
- start = msg.find("[")
237
- end = msg.rfind("]")
238
- if start != -1 and end != -1:
239
- names_str = msg[start+1:end]
240
- profile_list = [n.strip().strip("'\"") for n in names_str.split(",")] if names_str else []
241
- except FileNotFoundError:
242
- profile_list = []
243
-
244
- prof_dd = gr.Dropdown(choices=profile_list or ["<no profiles.yaml>"],
245
- value=(profile_list[0] if profile_list else "<no profiles.yaml>"),
246
- label="Select profile (from profiles.yaml)")
247
- reload_btn = gr.Button("Reload app with this profile")
248
- reload_out = gr.Textbox(label="Reload status", interactive=False)
249
-
250
- def _reload_with_profile(sel_name: str):
251
- if not sel_name or sel_name == "<no profiles.yaml>":
252
- return "❌ No profiles.yaml or no profiles found."
253
- try:
254
- (p_spec, p_base, p_bearer), _ = _load_profile_from_yaml(sel_name)
255
- if not p_spec or not p_base:
256
- return "❌ Profile missing 'spec' or 'base_url'."
257
- # Build argv: python app.py --spec ... --base-url ... [--bearer ...] [--profile sel_name]
258
- argv = [sys.executable, os.path.abspath(sys.argv[0]),
259
- "--spec", p_spec, "--base-url", p_base]
260
- if p_bearer:
261
- argv += ["--bearer", p_bearer]
262
- argv += ["--profile", sel_name] # keep the profile label in argv for clarity
263
- # Flush response text, then execv to hard-restart the process
264
- # Note: This will drop current connections; expected for a restart.
265
- os.execv(sys.executable, argv)
266
- except Exception as e:
267
- return f"❌ {type(e).__name__}: {e}"
268
-
269
- reload_btn.click(_reload_with_profile, inputs=[prof_dd], outputs=[reload_out])
270
-
271
  # 1) Spec validator (local)
272
  def validate_btn():
273
  try:
@@ -289,8 +216,8 @@ def build_gradio_app_from_spec(spec: Json, base_url: Optional[str] = None,
289
  "oauth2": {"token": None}
290
  })
291
 
292
- # === Auth Wizard (drop-in block) ===
293
- with gr.Accordion("Auth Wizard (reads components.securitySchemes)", open=False):
294
  scheme_names = list(schemes.keys()) or ["<none>"]
295
  scheme_dd = gr.Dropdown(
296
  scheme_names,
@@ -299,8 +226,7 @@ def build_gradio_app_from_spec(spec: Json, base_url: Optional[str] = None,
299
  )
300
 
301
  with gr.Row():
302
- api_in = gr.Dropdown(["header", "query", "cookie"], label="apiKey.in")
303
- # Make it user-editable and NEVER bind it as an output again
304
  api_name = gr.Textbox(label="apiKey.name", interactive=True, placeholder="X-API-Key")
305
  api_value = gr.Textbox(label="apiKey.value (secret)", type="password")
306
 
@@ -311,34 +237,22 @@ def build_gradio_app_from_spec(spec: Json, base_url: Optional[str] = None,
311
  basic_user = gr.Textbox(label="Basic user")
312
  basic_pass = gr.Textbox(label="Basic password", type="password")
313
 
314
- with gr.Accordion("OAuth2 (manual or client_credentials)", open=False):
315
- oauth_token = gr.Textbox(label="Access token (sets Authorization: Bearer ...)", type="password")
316
- token_url = gr.Textbox(label="tokenUrl (for client_credentials)")
317
- client_id = gr.Textbox(label="client_id")
318
- client_secret = gr.Textbox(label="client_secret", type="password")
319
- scope = gr.Textbox(label="scope (space-separated)", placeholder="read write")
320
- fetch_tok = gr.Button("Fetch token (client_credentials)")
321
- fetch_out = gr.Textbox(label="Token fetch result", interactive=False)
322
-
323
- # Prefill only apiKey.in from the selected scheme; leave name editable
324
  def on_scheme_change(sel):
325
  data = schemes.get(sel, {}) if sel and sel in schemes else {}
 
326
  return (data.get("in") or "header")
327
  scheme_dd.change(on_scheme_change, inputs=[scheme_dd], outputs=[api_in])
328
 
329
- # Apply button + handler (this is the one that went missing)
330
- apply_btn = gr.Button("Apply auth to proxy_request")
331
  apply_out = gr.Textbox(label="Auth status", interactive=False)
332
 
333
- def apply_auth(sel, api_in_v, api_name_v, api_value_v, bearer_v, basic_u, basic_p, oauth_tok):
334
  ctx = {
335
  "mode": None,
336
  "apiKey": {"in": None, "name": None, "value": None},
337
  "bearer": {"token": None},
338
  "basic": {"user": None, "pass": None},
339
- "oauth2": {"token": None}
340
  }
341
- # Prefer defined scheme if present
342
  if sel in schemes:
343
  stype = (schemes[sel].get("type") or "").lower()
344
  scheme_name = (schemes[sel].get("scheme") or "").lower()
@@ -355,60 +269,25 @@ def build_gradio_app_from_spec(spec: Json, base_url: Optional[str] = None,
355
  elif stype == "http" and scheme_name == "basic":
356
  ctx["mode"] = "http_basic"
357
  ctx["basic"] = {"user": basic_u, "pass": basic_p}
358
- elif stype == "oauth2":
359
- ctx["mode"] = "oauth2"
360
- ctx["oauth2"]["token"] = oauth_tok
361
- else:
362
- # No securitySchemes: infer from what the user typed
363
  if api_value_v and (api_in_v or api_name_v):
364
  ctx["mode"] = "apiKey"
365
- ctx["apiKey"] = {
366
- "in": api_in_v or "header",
367
- "name": api_name_v or "X-API-Key",
368
- "value": api_value_v
369
- }
370
  elif bearer_v:
371
- ctx["mode"] = "http_bearer"
372
- ctx["bearer"]["token"] = bearer_v
373
  elif basic_u or basic_p:
374
- ctx["mode"] = "http_basic"
375
- ctx["basic"] = {"user": basic_u, "pass": basic_p}
376
- elif oauth_tok:
377
- ctx["mode"] = "oauth2"
378
- ctx["oauth2"]["token"] = oauth_tok
379
 
380
  return ctx, f"Applied auth mode: {ctx['mode']}"
381
 
382
  apply_btn.click(
383
  fn=apply_auth,
384
- inputs=[scheme_dd, api_in, api_name, api_value, bearer_inp, basic_user, basic_pass, oauth_token],
385
  outputs=[auth_ctx, apply_out]
386
  )
387
-
388
- # Client Credentials fetch helper (optional)
389
- def fetch_token_cc(token_url_v, client_id_v, client_secret_v, scope_v):
390
- if not token_url_v:
391
- return "❌ tokenUrl required"
392
- try:
393
- data = {"grant_type": "client_credentials"}
394
- if scope_v: data["scope"] = scope_v
395
- resp = requests.post(
396
- token_url_v, data=data,
397
- auth=(client_id_v or "", client_secret_v or ""), timeout=30
398
- )
399
- resp.raise_for_status()
400
- payload = resp.json()
401
- tok = payload.get("access_token")
402
- return f"✅ got token: {('***' if tok else '<none>')}"
403
- except Exception as e:
404
- return f"❌ {type(e).__name__}: {e}"
405
-
406
- fetch_tok.click(
407
- fetch_token_cc,
408
- inputs=[token_url, client_id, client_secret, scope],
409
- outputs=[fetch_out]
410
- )
411
- # === end Auth Wizard ===
412
 
413
  # 4) Render auto-generated API UI
414
  loaded_app.render()
@@ -476,69 +355,6 @@ def build_gradio_app_from_spec(spec: Json, base_url: Optional[str] = None,
476
  status_out = f"{resp.status_code} {resp.reason}"
477
  return status_out, body_out, headers_out
478
 
479
- # --- Spec Health (Modal) ---
480
- with gr.Accordion("Spec Health (Modal)", open=False):
481
- status_url = gr.Textbox(
482
- label="Status URL (exact)",
483
- placeholder="https://<your-status>.modal.run",
484
- value=os.environ.get("MODAL_STATUS_URL", "")
485
- )
486
- download_url = gr.Textbox(
487
- label="Download URL (exact)",
488
- placeholder="https://<your-download>.modal.run",
489
- value=os.environ.get("MODAL_DOWNLOAD_URL", "")
490
- )
491
-
492
- fetch = gr.Button("Fetch status")
493
- used_status_url = gr.Textbox(label="(debug) used status URL", interactive=False)
494
- status_out = gr.Textbox(label="Result", interactive=False)
495
- status_json = gr.JSON(label="Status JSON")
496
-
497
- validated_dd = gr.Dropdown(choices=[], label="Validated profile")
498
- reload_validated_btn = gr.Button("Reload with validated spec")
499
- reload_validated_out = gr.Textbox(label="Reload result", interactive=False)
500
-
501
- def _fetch_status(status_u: str):
502
- if not status_u:
503
- return "", "❌ Provide the full Status URL", None
504
- try:
505
- # DO NOT append '/status' — this URL *is* the endpoint
506
- r = requests.get(status_u, timeout=20, allow_redirects=True)
507
- r.raise_for_status()
508
- data = r.json()
509
- profiles = sorted(list(data.keys()))
510
- note = f"✅ {len(profiles)} profiles fetched"
511
- dd = gr.update(choices=profiles, value=(profiles[0] if profiles else None))
512
- return status_u, note, data, dd
513
- except Exception as e:
514
- return status_u, f"❌ {type(e).__name__}: {e}", None, gr.update(choices=[], value=None)
515
-
516
- fetch.click(_fetch_status, inputs=[status_url], outputs=[used_status_url, status_out, status_json, validated_dd])
517
-
518
- def _reload_with_validated(profile_name: str, download_u: str):
519
- if not profile_name:
520
- return "❌ Select a profile."
521
- if not download_u:
522
- return "❌ Provide the full Download URL."
523
-
524
- # Read base_url from profiles.yaml
525
- try:
526
- (p_spec, p_base, p_bearer), _ = _load_profile_from_yaml(profile_name)
527
- except Exception:
528
- p_spec, p_base, p_bearer = None, None, None
529
- if not p_base:
530
- return "❌ profiles.yaml must include base_url for this profile."
531
-
532
- # Modal download endpoint uses ?profile=...
533
- spec_url = f"{download_u}{'' if '?' in download_u else ''}?profile={profile_name}"
534
-
535
- argv = [sys.executable, os.path.abspath(sys.argv[0]), "--spec", spec_url, "--base-url", p_base, "--profile", profile_name]
536
- if p_bearer:
537
- argv += ["--bearer", p_bearer]
538
- os.execv(sys.executable, argv)
539
-
540
- reload_validated_btn.click(_reload_with_validated, inputs=[validated_dd, download_url], outputs=[reload_validated_out])
541
-
542
  with gr.Accordion("Advanced request (API-Key / Basic / custom headers)", open=False):
543
  with gr.Row():
544
  method = gr.Dropdown(["GET","POST","PUT","PATCH","DELETE","HEAD","OPTIONS"], value="GET", label="Method")
 
1
  # openapi_loader.py
2
  from __future__ import annotations
3
+ import json, os, re
4
+ from typing import Any, Dict, Tuple, Optional
5
  from urllib.parse import urlparse
6
  import requests
7
  import yaml
8
  import gradio as gr
9
+ import inspect, keyword, base64
10
  from openapi_spec_validator import validate_spec
 
 
 
11
 
12
  Json = Dict[str, Any]
13
  _VAR_RE = re.compile(r"\{([^}]+)\}")
14
+ _PY_IDENT = re.compile(r"^[A-Za-z_]\w*$")
 
 
 
15
 
16
  _PY_IDENT = re.compile(r"^[A-Za-z_]\w*$")
17
 
18
  def _is_valid_identifier(name: str) -> bool:
19
  return bool(_PY_IDENT.match(name)) and not keyword.iskeyword(name)
20
 
 
 
 
 
 
 
 
 
 
 
 
21
 
22
  def sanitize_header_params(spec: Json) -> Tuple[Json, list]:
23
  """
 
157
  return next(iter(schemes.keys())) if schemes else None
158
 
159
 
 
 
 
 
 
 
 
 
 
 
 
 
160
  def build_gradio_app_from_spec(spec: Json, base_url: Optional[str] = None,
161
  bearer_token: Optional[str] = None) -> gr.Blocks:
162
  if not base_url:
 
195
  gr.Markdown("### AnyAPI→MCP Factory — OpenAPI Explorer")
196
  gr.Markdown(f"**Base URL:** `{base_url}` \n**Auth:** {banner_auth}")
197
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
  # 1) Spec validator (local)
199
  def validate_btn():
200
  try:
 
216
  "oauth2": {"token": None}
217
  })
218
 
219
+ # === Auth (minimal) ===
220
+ with gr.Accordion("Auth", open=True):
221
  scheme_names = list(schemes.keys()) or ["<none>"]
222
  scheme_dd = gr.Dropdown(
223
  scheme_names,
 
226
  )
227
 
228
  with gr.Row():
229
+ api_in = gr.Dropdown(["header", "query", "cookie"], value="header", label="apiKey.in")
 
230
  api_name = gr.Textbox(label="apiKey.name", interactive=True, placeholder="X-API-Key")
231
  api_value = gr.Textbox(label="apiKey.value (secret)", type="password")
232
 
 
237
  basic_user = gr.Textbox(label="Basic user")
238
  basic_pass = gr.Textbox(label="Basic password", type="password")
239
 
 
 
 
 
 
 
 
 
 
 
240
  def on_scheme_change(sel):
241
  data = schemes.get(sel, {}) if sel and sel in schemes else {}
242
+ # Only prefill apiKey.in; leave name editable
243
  return (data.get("in") or "header")
244
  scheme_dd.change(on_scheme_change, inputs=[scheme_dd], outputs=[api_in])
245
 
246
+ apply_btn = gr.Button("Apply auth")
 
247
  apply_out = gr.Textbox(label="Auth status", interactive=False)
248
 
249
+ def apply_auth(sel, api_in_v, api_name_v, api_value_v, bearer_v, basic_u, basic_p):
250
  ctx = {
251
  "mode": None,
252
  "apiKey": {"in": None, "name": None, "value": None},
253
  "bearer": {"token": None},
254
  "basic": {"user": None, "pass": None},
 
255
  }
 
256
  if sel in schemes:
257
  stype = (schemes[sel].get("type") or "").lower()
258
  scheme_name = (schemes[sel].get("scheme") or "").lower()
 
269
  elif stype == "http" and scheme_name == "basic":
270
  ctx["mode"] = "http_basic"
271
  ctx["basic"] = {"user": basic_u, "pass": basic_p}
272
+
273
+ # Fallbacks if the spec doesn't define a scheme
274
+ if ctx["mode"] is None:
 
 
275
  if api_value_v and (api_in_v or api_name_v):
276
  ctx["mode"] = "apiKey"
277
+ ctx["apiKey"] = {"in": api_in_v or "header", "name": api_name_v or "X-API-Key", "value": api_value_v}
 
 
 
 
278
  elif bearer_v:
279
+ ctx["mode"] = "http_bearer"; ctx["bearer"]["token"] = bearer_v
 
280
  elif basic_u or basic_p:
281
+ ctx["mode"] = "http_basic"; ctx["basic"] = {"user": basic_u, "pass": basic_p}
 
 
 
 
282
 
283
  return ctx, f"Applied auth mode: {ctx['mode']}"
284
 
285
  apply_btn.click(
286
  fn=apply_auth,
287
+ inputs=[scheme_dd, api_in, api_name, api_value, bearer_inp, basic_user, basic_pass],
288
  outputs=[auth_ctx, apply_out]
289
  )
290
+ # === end Auth ===
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
291
 
292
  # 4) Render auto-generated API UI
293
  loaded_app.render()
 
355
  status_out = f"{resp.status_code} {resp.reason}"
356
  return status_out, body_out, headers_out
357
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
358
  with gr.Accordion("Advanced request (API-Key / Basic / custom headers)", open=False):
359
  with gr.Row():
360
  method = gr.Dropdown(["GET","POST","PUT","PATCH","DELETE","HEAD","OPTIONS"], value="GET", label="Method")