mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2026-04-02 00:02:19 -07:00
Improve OpenAPI specs
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
from typing import Any, Dict, Optional
|
||||
import graphene
|
||||
|
||||
from .registry import register_tool, _operation_ids
|
||||
from .schemas import GraphQLRequest
|
||||
from .schema_converter import pydantic_to_json_schema, resolve_schema_refs
|
||||
|
||||
|
||||
def introspect_graphql_schema(schema: graphene.Schema):
|
||||
@@ -26,6 +28,7 @@ def introspect_graphql_schema(schema: graphene.Schema):
|
||||
operation_id="graphql_query",
|
||||
summary="GraphQL Endpoint",
|
||||
description="Execute arbitrary GraphQL queries against the system schema.",
|
||||
request_model=GraphQLRequest,
|
||||
tags=["graphql"]
|
||||
)
|
||||
|
||||
@@ -36,6 +39,20 @@ def _flask_to_openapi_path(flask_path: str) -> str:
|
||||
return re.sub(r'<(?:\w+:)?(\w+)>', r'{\1}', flask_path)
|
||||
|
||||
|
||||
def _get_openapi_metadata(func: Any) -> Optional[Dict[str, Any]]:
|
||||
"""Recursively find _openapi_metadata in wrapped functions."""
|
||||
# Check current function
|
||||
metadata = getattr(func, "_openapi_metadata", None)
|
||||
if metadata:
|
||||
return metadata
|
||||
|
||||
# Check __wrapped__ (standard for @wraps)
|
||||
if hasattr(func, "__wrapped__"):
|
||||
return _get_openapi_metadata(func.__wrapped__)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def introspect_flask_app(app: Any):
|
||||
"""
|
||||
Introspect the Flask application to find routes decorated with @validate_request
|
||||
@@ -47,14 +64,13 @@ def introspect_flask_app(app: Any):
|
||||
if not view_func:
|
||||
continue
|
||||
|
||||
# Check for our decorator's metadata
|
||||
metadata = getattr(view_func, "_openapi_metadata", None)
|
||||
if not metadata:
|
||||
# Fallback for wrapped functions
|
||||
if hasattr(view_func, "__wrapped__"):
|
||||
metadata = getattr(view_func.__wrapped__, "_openapi_metadata", None)
|
||||
# Check for our decorator's metadata recursively
|
||||
metadata = _get_openapi_metadata(view_func)
|
||||
|
||||
if metadata:
|
||||
if metadata.get("exclude_from_spec"):
|
||||
continue
|
||||
|
||||
op_id = metadata["operation_id"]
|
||||
|
||||
# Register the tool with real path and method from Flask
|
||||
@@ -75,11 +91,8 @@ def introspect_flask_app(app: Any):
|
||||
# Determine tags - create a copy to avoid mutating shared metadata
|
||||
tags = list(metadata.get("tags") or ["rest"])
|
||||
if path.startswith("/mcp/"):
|
||||
# Move specific tags to secondary position or just add MCP
|
||||
if "rest" in tags:
|
||||
tags.remove("rest")
|
||||
if "mcp" not in tags:
|
||||
tags.append("mcp")
|
||||
# For MCP endpoints, we want them exclusively in the 'mcp' tag section
|
||||
tags = ["mcp"]
|
||||
|
||||
# Ensure unique operationId
|
||||
original_op_id = op_id
|
||||
@@ -89,6 +102,38 @@ def introspect_flask_app(app: Any):
|
||||
unique_op_id = f"{op_id}_{count}"
|
||||
count += 1
|
||||
|
||||
# Filter path_params to only include those that are actually in the path
|
||||
path_params = metadata.get("path_params")
|
||||
if path_params:
|
||||
path_params = [
|
||||
p for p in path_params
|
||||
if f"{{{p['name']}}}" in path
|
||||
]
|
||||
|
||||
# Auto-generate query_params from request_model for GET requests
|
||||
query_params = metadata.get("query_params")
|
||||
if method == 'GET' and not query_params and metadata.get("request_model"):
|
||||
try:
|
||||
schema = pydantic_to_json_schema(metadata["request_model"])
|
||||
properties = schema.get("properties", {})
|
||||
query_params = []
|
||||
for name, prop in properties.items():
|
||||
is_required = name in schema.get("required", [])
|
||||
# Create param definition, preserving enum/schema
|
||||
param_def = {
|
||||
"name": name,
|
||||
"in": "query",
|
||||
"required": is_required,
|
||||
"description": prop.get("description", ""),
|
||||
"schema": prop
|
||||
}
|
||||
# Remove description from schema to avoid duplication
|
||||
if "description" in param_def["schema"]:
|
||||
del param_def["schema"]["description"]
|
||||
query_params.append(param_def)
|
||||
except Exception:
|
||||
pass # Fallback to empty if schema generation fails
|
||||
|
||||
register_tool(
|
||||
path=path,
|
||||
method=method,
|
||||
@@ -98,9 +143,11 @@ def introspect_flask_app(app: Any):
|
||||
description=metadata["description"],
|
||||
request_model=metadata.get("request_model"),
|
||||
response_model=metadata.get("response_model"),
|
||||
path_params=metadata.get("path_params"),
|
||||
query_params=metadata.get("query_params"),
|
||||
path_params=path_params,
|
||||
query_params=query_params,
|
||||
tags=tags,
|
||||
allow_multipart_payload=metadata.get("allow_multipart_payload", False)
|
||||
allow_multipart_payload=metadata.get("allow_multipart_payload", False),
|
||||
response_content_types=metadata.get("response_content_types"),
|
||||
links=metadata.get("links")
|
||||
)
|
||||
registered_ops.add(op_key)
|
||||
|
||||
Reference in New Issue
Block a user