add:stage

This commit is contained in:
gongwenxin
2025-07-11 17:56:56 +08:00
parent 8fb86a34e9
commit cd1c6a340e
386 changed files with 272591 additions and 316502 deletions
+259 -4
View File
@@ -1,6 +1,9 @@
import json
import logging
from typing import List, Dict, Any, Optional, Union # Ensure Union is imported
from typing import List, Dict, Any, Optional, Union, Tuple
import requests
from urllib.parse import urljoin
import copy
logger = logging.getLogger(__name__)
@@ -373,9 +376,53 @@ class SwaggerEndpoint(BaseEndpoint): # Inherit from BaseEndpoint
def __repr__(self):
return f"<SwaggerEndpoint Method:{self.method} Path:{self.path} Summary:'{self.summary}'>"
class DMSEndpoint(BaseEndpoint):
"""Represents an API endpoint discovered dynamically from the DMS service."""
def __init__(self, method: str, path: str, title: str,
request_body: Optional[Dict[str, Any]],
responses: Dict[str, Any],
parameters: Optional[List[Dict[str, Any]]] = None,
category_name: Optional[str] = None,
raw_record: Optional[Dict[str, Any]] = None,
test_mode: str = 'standalone',
operation_id: Optional[str] = None):
super().__init__(method=method.upper(), path=path)
self.title = title
self.request_body = request_body
self.responses = responses
self.parameters = parameters if parameters is not None else []
self.category_name = category_name
self._raw_record = raw_record
self.test_mode = test_mode
self.operation_id = operation_id or f"{self.method.lower()}_{self.category_name or 'dms'}_{title.replace(' ', '_')}"
def to_dict(self) -> Dict[str, Any]:
"""Converts the DMS endpoint data into a standardized OpenAPI-like dictionary."""
endpoint_dict = {
"method": self.method,
"path": self.path,
"title": self.title,
"summary": self.title,
"description": self.title,
"operationId": self.operation_id,
"tags": [self.category_name] if self.category_name else [],
"parameters": self.parameters,
"requestBody": self.request_body,
"responses": self.responses,
"_source_format": "dms",
"_dms_raw_record": self._raw_record,
"_test_mode": self.test_mode
}
return endpoint_dict
def __repr__(self):
return f"<DMSEndpoint Method:{self.method} Path:{self.path} Title:'{self.title}' Mode:{self.test_mode}>"
Endpoint = Union[YAPIEndpoint, SwaggerEndpoint, DMSEndpoint]
class ParsedAPISpec:
"""解析后的API规范的通用基类"""
def __init__(self, spec_type: str, endpoints: List[Union[YAPIEndpoint, SwaggerEndpoint]], spec: Dict[str, Any]):
"""Base class for a parsed API specification from any source."""
def __init__(self, spec_type: str, endpoints: List[Union[YAPIEndpoint, SwaggerEndpoint, 'DMSEndpoint']], spec: Dict[str, Any]):
self.spec_type = spec_type
self.endpoints = endpoints
self.spec = spec # Store the original full spec dictionary, useful for $ref resolution if not pre-resolved
@@ -392,6 +439,11 @@ class ParsedSwaggerSpec(ParsedAPISpec):
super().__init__(spec_type="swagger", endpoints=endpoints, spec=spec)
self.tags = tags
class ParsedDMSSpec(ParsedAPISpec):
"""Parsed specification from the dynamic DMS source."""
def __init__(self, endpoints: List[DMSEndpoint], spec: Dict[str, Any]):
super().__init__(spec_type="dms", endpoints=endpoints, spec=spec)
class InputParser:
"""负责解析输入(如YAPI JSON)并提取API端点信息"""
def __init__(self):
@@ -478,4 +530,207 @@ class InputParser:
self.logger.error(f"Error decoding JSON from Swagger spec file {file_path}: {e}")
except Exception as e:
self.logger.error(f"An unexpected error occurred while parsing Swagger spec {file_path}: {e}", exc_info=True)
return None
return None
def parse_dms_spec(self, domain_mapping_path: str, base_url: str, headers: Optional[Dict[str, str]] = None) -> Optional[ParsedDMSSpec]:
self.logger.info(f"Starting DMS spec parsing. Base URL: {base_url}, Domain Map: {domain_mapping_path}")
headers = headers or {}
try:
with open(domain_mapping_path, 'r', encoding='utf-8') as f:
DOMAIN_MAP = json.load(f)
except (FileNotFoundError, json.JSONDecodeError) as e:
self.logger.warning(f"Could not load or parse domain map file '{domain_mapping_path}'. Using default domain. Error: {e}")
DOMAIN_MAP = {}
list_url = urljoin(base_url, "/api/schema/manage/schema")
self.logger.info(f"Fetching API list from: {list_url}")
try:
response = requests.get(list_url, headers=headers)
response.raise_for_status()
api_list_data = response.json()
# 检查业务代码是否成功
if api_list_data.get("code") != 0:
self.logger.error(f"DMS API list endpoint returned a business error: {api_list_data.get('message')}")
return None
# 从分页结构中提取 'records'
api_records = api_list_data.get("data", {}).get("records", [])
if not api_records:
self.logger.warning("DMS API list is empty or 'records' key is missing in the response data.")
# Returning an empty spec is valid if the list is just empty.
return ParsedDMSSpec(endpoints=[], spec={"dms_api_list": []})
except requests.exceptions.RequestException as e:
self.logger.error(f"Failed to fetch API list from DMS: {e}")
return None
except json.JSONDecodeError:
self.logger.error("Failed to decode JSON response from DMS API list.")
return None
endpoints: List[DMSEndpoint] = []
for item in api_records:
domain_name = item.get('domain')
name = item.get('name')
model_id = item.get('id')
if not all(k in item for k in ['domain', 'name', 'id']):
self.logger.warning(f"Skipping an item in API list because it's missing 'domain', 'name', or 'id': {item}")
continue
instance_code = DOMAIN_MAP.get(domain_name, domain_name)
model_url = urljoin(base_url, f"/api/schema/manage/schema/{model_id}")
self.logger.info(f"Fetching model for '{name}' from: {model_url}")
try:
response = requests.get(model_url, headers=headers)
response.raise_for_status()
model_schema_response = response.json()
except requests.exceptions.RequestException as e:
self.logger.error(f"Error fetching model for '{name}': {e}")
continue
except json.JSONDecodeError:
self.logger.error(f"Failed to decode JSON for model '{name}'.")
continue
if not model_schema_response or 'data' not in model_schema_response or not model_schema_response['data']:
self.logger.warning(f"Skipping API '{name}' due to missing or empty model schema in response.")
continue
model_data = model_schema_response['data']
model = model_data.get('model')
if not model or 'properties' not in model or not model['properties']:
self.logger.warning(f"Skipping API '{name}' due to missing or invalid 'model' object in schema.")
continue
pk_name = next(iter(model['properties']), None)
if not pk_name:
self.logger.warning(f"Skipping API '{name}' because no properties found in model to identify a primary key.")
continue
pk_schema = model['properties'][pk_name]
version = model_data.get('version', '1.0.0')
dms_instance_code = instance_code
category_name = domain_name
success_response = {
"200": { "description": "Success", "content": {"application/json": {"schema": {"type": "object", "properties": {"code": {"type": "integer"}, "message": {"type": "string"}, "data": {"type": "boolean"}}}}}}}
# Create Endpoint (POST)
create_path = f"/api/dms/{dms_instance_code}/v1/{name}"
create_request_body_schema = {"type": "object", "properties": {"version": {"type": "string", "example": version}, "act": {"type": "integer", "example": 0}, "data": {"type": "array", "items": model}}, "required": ["data"]}
endpoints.append(DMSEndpoint(path=create_path, method='post', title=f"Create {name}", request_body={'content': {'application/json': {'schema': create_request_body_schema}}}, responses=success_response, test_mode='standalone', operation_id=f"create_{name}", category_name=category_name, raw_record=item))
# List Endpoint (POST)
list_path = f"/api/dms/{dms_instance_code}/v1/{name}/{version}"
list_response_schema = {"type": "object", "properties": {"code": {"type": "integer"}, "message": {"type": "string"}, "data": {"type": "array", "items": model}}}
endpoints.append(DMSEndpoint(path=list_path, method='post', title=f"List {name}", request_body={'content': {'application/json': {'schema': {}}}}, responses={'200': {'description': 'Successful Operation', 'content': {'application/json': {'schema': list_response_schema}}}}, test_mode='scenario_only', operation_id=f"list_{name}", category_name=category_name, raw_record=item))
# Read Endpoint (GET)
read_path = f"/api/dms/{dms_instance_code}/v1/{name}/{version}/{{id}}"
read_response_schema = {"type": "object", "properties": {"code": {"type": "integer"}, "message": {"type": "string"}, "data": model}}
read_parameters = [{'name': 'id', 'in': 'path', 'required': True, 'description': f'The ID of the {name}, maps to {pk_name}', 'schema': pk_schema}]
endpoints.append(DMSEndpoint(path=read_path, method='get', title=f"Read {name}", request_body=None, responses={'200': {'description': 'Successful Operation', 'content': {'application/json': {'schema': read_response_schema}}}}, parameters=read_parameters, test_mode='scenario_only', operation_id=f"read_{name}", category_name=category_name, raw_record=item))
# Update Endpoint (PUT)
update_path = f"/api/dms/{dms_instance_code}/v1/{name}"
endpoints.append(DMSEndpoint(path=update_path, method='put', title=f"Update {name}", request_body={'content': {'application/json': {'schema': create_request_body_schema}}}, responses=success_response, test_mode='scenario_only', operation_id=f"update_{name}", category_name=category_name, raw_record=item))
# Delete Endpoint (DELETE)
delete_path = f"/api/dms/{dms_instance_code}/v1/{name}"
delete_request_body_schema = {"type": "object", "properties": {"version": {"type": "string", "example": version}, "data": {"type": "array", "items": {"type": "object", "properties": { pk_name: pk_schema }, "required": [pk_name]}}}, "required": ["data"]}
endpoints.append(DMSEndpoint(path=delete_path, method='delete', title=f"Delete {name}", request_body={'content': {'application/json': {'schema': delete_request_body_schema}}}, responses=success_response, test_mode='scenario_only', operation_id=f"delete_{name}", category_name=category_name, raw_record=item))
# The 'spec' for ParsedDMSSpec should represent the whole document.
# We can construct a dictionary holding all the raw data we fetched.
dms_full_spec_dict = {"dms_api_list": api_records}
return ParsedDMSSpec(endpoints=endpoints, spec=dms_full_spec_dict)
class DmsConfig:
def __init__(self, base_url: str, domain_map_file: str, headers: Optional[Dict[str, str]] = None):
self.base_url = base_url
self.domain_map_file = domain_map_file
self.headers = headers
def get_endpoints_from_swagger(file_path: str) -> Tuple[List[SwaggerEndpoint], str]:
"""
Parses a Swagger/OpenAPI JSON file and returns a list of SwaggerEndpoint objects
and the base path of the API.
"""
logger.info(f"Parsing Swagger/OpenAPI spec from: {file_path}")
all_endpoints: List[SwaggerEndpoint] = []
swagger_tags: List[Dict[str, Any]] = []
raw_spec_data_dict: Optional[Dict[str, Any]] = None # Swagger/OpenAPI is a single root object
try:
with open(file_path, 'r', encoding='utf-8') as f:
# TODO: Add YAML support if needed, e.g., using PyYAML
raw_spec_data_dict = json.load(f)
if not isinstance(raw_spec_data_dict, dict):
logger.error(f"Swagger spec file {file_path} does not contain a JSON object as expected.")
return [], ""
swagger_tags = raw_spec_data_dict.get("tags", [])
paths = raw_spec_data_dict.get("paths", {})
for path, path_item_obj in paths.items():
if not isinstance(path_item_obj, dict): continue
for method, operation_obj in path_item_obj.items():
# Common methods, can be extended
if method.lower() not in ["get", "post", "put", "delete", "patch", "options", "head", "trace"]:
continue # Skip non-standard HTTP methods or extensions like 'parameters' at path level
if not isinstance(operation_obj, dict): continue
try:
# Pass the full raw_spec_data_dict for $ref resolution within SwaggerEndpoint
swagger_endpoint = SwaggerEndpoint(path, method, operation_obj, global_spec=raw_spec_data_dict)
all_endpoints.append(swagger_endpoint)
except Exception as e_ep:
logger.error(f"Error processing Swagger endpoint: {method.upper()} {path}. Error: {e_ep}", exc_info=True)
return all_endpoints, raw_spec_data_dict.get("basePath", "") if raw_spec_data_dict.get("basePath") else ""
except FileNotFoundError:
# It's better to log this error. Assuming a logger is available at self.logger
logger.error(f"Swagger spec file not found: {file_path}")
return [], ""
except json.JSONDecodeError as e:
logger.error(f"Error decoding JSON from Swagger spec file {file_path}: {e}")
return [], ""
except Exception as e:
logger.error(f"An unexpected error occurred while parsing {file_path}: {e}", exc_info=True)
return [], ""
def parse_input_to_endpoints(input_type: str, input_path: str, dms_config: DmsConfig = None) -> Tuple[List[Endpoint], str]:
"""
Parses input from a given type (YAPI, Swagger, DMS) and returns a list of Endpoint objects
and the base path of the API.
"""
parser = InputParser()
if input_type == "yapi":
parsed_spec = parser.parse_yapi_spec(input_path)
if parsed_spec:
return parsed_spec.endpoints, "" # YAPI doesn't have a base path in the same sense as Swagger
elif input_type == "swagger":
# The standalone get_endpoints_from_swagger is simple, but for consistency let's use the parser
parsed_spec = parser.parse_swagger_spec(input_path)
if parsed_spec:
base_path = parsed_spec.spec.get("basePath", "") or ""
# servers URL might be more modern (OpenAPI 3)
if not base_path and "servers" in parsed_spec.spec and parsed_spec.spec["servers"]:
# Use the first server URL
base_path = parsed_spec.spec["servers"][0].get("url", "")
return parsed_spec.endpoints, base_path
elif input_type == "dms":
if dms_config:
parsed_spec = parser.parse_dms_spec(dms_config.domain_map_file, dms_config.base_url, dms_config.headers)
if parsed_spec:
return parsed_spec.endpoints, dms_config.base_url
else:
logger.error("DMS configuration not provided for DMS input type.")
return [], ""
else:
logger.error(f"Unsupported input type: {input_type}")
return [], ""
return [], ""