| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550 |
- #!/usr/bin/env python3
- """Generate the current Flask route inventory as an OpenAPI 3.1 contract."""
- from __future__ import annotations
- import argparse
- import ast
- import json
- import re
- from collections import defaultdict
- from pathlib import Path
- ROOT = Path(__file__).resolve().parents[1]
- OUTPUT = ROOT / "docs" / "architecture" / "OPENAPI.yaml"
- PREFIXES = {
- "meta_data": "/api/meta",
- "data_interface": "/api/interface",
- "data_rules": "/api/rules",
- "graph": "/api/graph",
- "system": "/api/system",
- "data_source": "/api/datasource",
- "data_development": "/api/development/v1",
- "data_flow": "/api/dataflow",
- "business_domain": "/api/bd",
- "data_factory": "/api/datafactory",
- "data_service": "/api/dataservice",
- "knowledge_base": "/api/knowledge",
- }
- SHORTHAND_METHODS = {"get", "post", "put", "patch", "delete"}
- RESPONSE_FIELDS = {
- ("data_development", "summary"): [
- "scope",
- "metrics",
- ],
- ("data_development", "details"): [
- "metric",
- "state",
- "records",
- "page",
- "page_size",
- "total",
- ],
- ("knowledge_base", "ask"): [
- "query_id",
- "mode",
- "answer",
- "answer_status",
- "degraded_components",
- "citations",
- "evidence",
- "freshness_status",
- ],
- }
- CONNECTOR_REQUEST_SCHEMAS = {
- "/api/datasource/connectors/config/validate": "ConnectorConfigValidationRequest",
- "/api/datasource/connectors/runs": "ConnectorRunRequest",
- "/api/datasource/connectors/machine/runs": "MachineConnectorRunRequest",
- "/api/datasource/connectors/source-bindings": "ConnectorSourceBindingRequest",
- "/api/datasource/connectors/principals": "ConnectorPrincipalRequest",
- "/api/datasource/connectors/{connector_id}/{version}/health": "ConnectorHealthRequest",
- "/api/datasource/connectors/principals/{principal_uid}/credentials": "ConnectorCredentialRequest",
- "/api/datasource/connectors/credentials/{credential_uid}/{action}": "ConnectorCredentialRequest",
- "/api/datasource/graph": "ConnectorGraphRequest",
- }
- CONNECTOR_RESPONSE_SCHEMAS = {
- ("/api/datasource/connectors/manifests", "get"): "ConnectorManifestEnvelope",
- ("/api/datasource/connectors/runs", "get"): "ConnectorRunListEnvelope",
- ("/api/datasource/connectors/runs", "post"): "ConnectorRunResultEnvelope",
- ("/api/datasource/connectors/machine/runs", "post"): "ConnectorRunResultEnvelope",
- (
- "/api/datasource/connectors/runs/{idempotency_key}/cancel",
- "post",
- ): "ConnectorRunRecordEnvelope",
- (
- "/api/datasource/connectors/runs/{idempotency_key}/resume",
- "post",
- ): "ConnectorRunResultEnvelope",
- (
- "/api/datasource/connectors/config/validate",
- "post",
- ): "ConnectorValidationEnvelope",
- (
- "/api/datasource/connectors/{connector_id}/{version}/health",
- "post",
- ): "ConnectorHealthEnvelope",
- (
- "/api/datasource/connectors/{connector_id}/{version}/compatibility",
- "get",
- ): "ConnectorCompatibilityEnvelope",
- ("/api/datasource/connectors/principals", "post"): "ConnectorPrincipalEnvelope",
- (
- "/api/datasource/connectors/source-bindings",
- "post",
- ): "ConnectorSourceBindingEnvelope",
- (
- "/api/datasource/connectors/principals/{principal_uid}/credentials",
- "post",
- ): "ConnectorCredentialEnvelope",
- (
- "/api/datasource/connectors/credentials/{credential_uid}/{action}",
- "post",
- ): "ConnectorCredentialEnvelope",
- ("/api/datasource/graph", "post"): "ConnectorGraphEnvelope",
- }
- NO_REQUEST_BODY_PATHS = {
- "/api/datasource/connectors/runs/{idempotency_key}/cancel",
- "/api/datasource/connectors/runs/{idempotency_key}/resume",
- "/api/datasource/connectors/machine/runs/{idempotency_key}/cancel",
- "/api/datasource/connectors/machine/runs/{idempotency_key}/resume",
- "/api/datasource/connectors/source-bindings/{binding_uid}/revoke",
- }
- OPTIONAL_REQUEST_BODY_PATHS = {
- "/api/datasource/connectors/principals/{principal_uid}/credentials",
- "/api/datasource/connectors/credentials/{credential_uid}/{action}",
- }
- def quoted(value: str) -> str:
- return json.dumps(value, ensure_ascii=False)
- def route_path(raw_path: str) -> tuple[str, list[dict[str, str]]]:
- parameters: list[dict[str, str]] = []
- def replace(match: re.Match[str]) -> str:
- converter = match.group(1) or "string"
- name = match.group(2)
- schema_type = "integer" if converter in {"int", "float"} else "string"
- parameters.append({"name": name, "type": schema_type})
- return "{" + name + "}"
- return re.sub(
- r"<(?:([a-zA-Z_]+):)?([a-zA-Z_][a-zA-Z0-9_]*)>", replace, raw_path
- ), parameters
- def extract_routes() -> list[dict[str, object]]:
- routes: list[dict[str, object]] = []
- for module, prefix in PREFIXES.items():
- for route_file in sorted((ROOT / "app" / "api" / module).glob("*.py")):
- if route_file.name == "__init__.py":
- continue
- tree = ast.parse(route_file.read_text(encoding="utf-8"))
- for node in tree.body:
- if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
- continue
- for decorator in node.decorator_list:
- if not isinstance(decorator, ast.Call):
- continue
- function = decorator.func
- if not (
- isinstance(function, ast.Attribute)
- and isinstance(function.value, ast.Name)
- and function.value.id == "bp"
- and decorator.args
- ):
- continue
- if function.attr == "route":
- methods = ["GET"]
- for keyword in decorator.keywords:
- if keyword.arg == "methods":
- methods = ast.literal_eval(keyword.value)
- elif function.attr in SHORTHAND_METHODS:
- methods = [function.attr.upper()]
- else:
- continue
- raw_path = ast.literal_eval(decorator.args[0])
- path, parameters = route_path(prefix + raw_path)
- summary = (
- ast.get_docstring(node) or node.name.replace("_", " ")
- ).splitlines()[0]
- for method in methods:
- routes.append(
- {
- "path": path,
- "method": method.lower(),
- "tag": module,
- "operation_id": f"{module}_{node.name}_{method.lower()}",
- "summary": summary,
- "parameters": parameters,
- "source": str(route_file.relative_to(ROOT)),
- }
- )
- return sorted(routes, key=lambda item: (str(item["path"]), str(item["method"])))
- def render(routes: list[dict[str, object]]) -> str:
- by_path: dict[str, list[dict[str, object]]] = defaultdict(list)
- for route in routes:
- by_path[str(route["path"])].append(route)
- lines = [
- "openapi: 3.1.0",
- "info:",
- ' title: "DataOps Platform API(当前代码基线)"',
- ' version: "2026-07-16"',
- ' description: "由 scripts/generate_openapi.py 从 app/api/*/routes.py 生成。请求体与响应细节仍以现有专项 API 文档和代码为准。"',
- f"x-route-count: {len(routes)}",
- "servers:",
- ' - url: "http://localhost:15500"',
- ' description: "全本地隔离测试后端"',
- "tags:",
- ]
- for tag in PREFIXES:
- lines.extend([f" - name: {tag}", f" description: {quoted(PREFIXES[tag])}"])
- lines.append("paths:")
- for path, operations in sorted(by_path.items()):
- lines.append(f" {quoted(path)}:")
- for operation in operations:
- lines.extend(
- [
- f" {operation['method']}:",
- f" tags: [{operation['tag']}]",
- f" operationId: {operation['operation_id']}",
- f" summary: {quoted(str(operation['summary']))}",
- f" x-source: {quoted(str(operation['source']))}",
- ]
- )
- parameters = operation["parameters"]
- if path.startswith("/api/datasource/connectors/machine/runs"):
- parameters = [
- *parameters,
- {
- "name": "X-Connector-Credential",
- "type": "string",
- "header": True,
- },
- ]
- if parameters:
- lines.append(" parameters:")
- for parameter in parameters:
- lines.extend(
- [
- f" - name: {parameter['name']}",
- f" in: {'header' if parameter.get('header') else 'path'}",
- " required: true",
- " schema:",
- f" type: {parameter['type']}",
- ]
- )
- if (
- path
- == "/api/datasource/connectors/credentials/{credential_uid}/{action}"
- and parameter["name"] == "action"
- ):
- lines.append(" enum: [revoke, rotate]")
- if (
- operation["method"] in {"post", "put", "patch"}
- and path not in NO_REQUEST_BODY_PATHS
- ):
- connector_schema = CONNECTOR_REQUEST_SCHEMAS.get(path)
- lines.extend(
- [
- " requestBody:",
- " required: "
- + (
- "false"
- if path in OPTIONAL_REQUEST_BODY_PATHS
- else "true" if connector_schema else "false"
- ),
- " content:",
- " application/json:",
- " schema:",
- f" $ref: '#/components/schemas/{connector_schema}'"
- if connector_schema
- else " type: object",
- ]
- )
- if not connector_schema:
- lines.append(" additionalProperties: true")
- response_fields = RESPONSE_FIELDS.get(
- (str(operation["tag"]), str(operation["operation_id"]).split("_")[-2])
- )
- if response_fields:
- lines.append(
- " x-response-fields: [" + ", ".join(response_fields) + "]"
- )
- connector_response = CONNECTOR_RESPONSE_SCHEMAS.get(
- (path, str(operation["method"]))
- )
- response_status = (
- "201"
- if operation["method"] == "post"
- and path
- in {
- "/api/datasource/connectors/runs",
- "/api/datasource/connectors/machine/runs",
- "/api/datasource/connectors/principals",
- "/api/datasource/connectors/principals/{principal_uid}/credentials",
- "/api/datasource/connectors/source-bindings",
- }
- else "200"
- )
- if path.startswith("/api/datasource/connectors"):
- required_permission = (
- "machine-credential"
- if path.startswith("/api/datasource/connectors/machine/runs")
- else (
- "connectors:manage"
- if "/source-bindings" in path
- else (
- "connectors:read"
- if operation["method"] == "get"
- else (
- "connectors:manage"
- if "/principals" in path or "/credentials" in path
- else "connectors:operate"
- )
- )
- )
- )
- lines.append(
- f" x-required-permission: {quoted(required_permission)}"
- )
- elif path == "/api/datasource/graph":
- lines.append(' x-required-permission: "connectors:read"')
- lines.extend(
- [
- " responses:",
- f' "{response_status}":',
- ' description: "请求已由当前实现处理"',
- " content:",
- " application/json:",
- " schema:",
- f" $ref: '#/components/schemas/{connector_response or 'ApiEnvelope'}'",
- " default:",
- ' description: "错误响应"',
- " content:",
- " application/json:",
- " schema:",
- f" $ref: '#/components/schemas/{'ConnectorErrorEnvelope' if path.startswith('/api/datasource/connectors') or path == '/api/datasource/graph' else 'ApiEnvelope'}'",
- ]
- )
- lines.extend(
- [
- "components:",
- " schemas:",
- " ApiEnvelope:",
- " type: object",
- " additionalProperties: true",
- " required: [code, message, data]",
- " properties:",
- " code:",
- " type: integer",
- " message:",
- " type: string",
- " data: {}",
- " error: {type: object}",
- " ConnectorConfigValidationRequest:",
- " type: object",
- " additionalProperties: false",
- " required: [connector_id, version, config]",
- " properties:",
- " connector_id: {type: string, minLength: 3, maxLength: 64}",
- " version: {type: string, pattern: '^[0-9]+\\.[0-9]+\\.[0-9]+'}",
- " config: {type: object}",
- " ConnectorRunRequest:",
- " type: object",
- " additionalProperties: false",
- " required: [connector_id, version, source_uid, operation, config, scope, dry_run]",
- " properties:",
- " connector_id: {type: string}",
- " version: {type: string}",
- " source_uid: {type: string, format: uuid}",
- " operation: {type: string, enum: [discover, snapshot, incremental, lineage, profile, evidence]}",
- " config: {type: object}",
- " scope: {$ref: '#/components/schemas/ConnectorScope'}",
- " cursor: {type: object}",
- " checkpoint: {type: object}",
- " idempotency_key: {type: string, pattern: '^[a-f0-9]{64}$'}",
- " dry_run: {const: true}",
- " process_key: {type: string, minLength: 1, maxLength: 300}",
- " MachineConnectorRunRequest:",
- " type: object",
- " additionalProperties: false",
- " required: [connector_id, version, source_uid, business_domain_uid, environment, process_key, operation, scope]",
- " properties:",
- " connector_id: {type: string}",
- " version: {type: string}",
- " source_uid: {type: string, format: uuid}",
- " business_domain_uid: {type: string, format: uuid}",
- " environment: {type: string, enum: [development, staging, production]}",
- " process_key: {type: string, minLength: 1, maxLength: 300}",
- " operation: {type: string, enum: [discover, snapshot, incremental, lineage, profile, evidence]}",
- " scope: {$ref: '#/components/schemas/ConnectorScope'}",
- " cursor: {type: object}",
- " checkpoint: {type: object}",
- " idempotency_key: {type: string, pattern: '^[a-f0-9]{64}$'}",
- " dry_run: {type: boolean, default: false}",
- " ConnectorPrincipalRequest:",
- " type: object",
- " additionalProperties: false",
- " required: [connector_id, version, source_uid, business_domain_uid, environment, operations, scopes]",
- " properties:",
- " connector_id: {type: string}",
- " version: {type: string}",
- " source_uid: {type: string, format: uuid}",
- " business_domain_uid: {type: string, format: uuid}",
- " environment: {type: string, enum: [development, staging, production]}",
- " operations: {type: array, minItems: 1, uniqueItems: true, items: {type: string}}",
- " scopes: {$ref: '#/components/schemas/ConnectorScope'}",
- " source_binding_uid: {type: string, format: uuid}",
- " source_binding_version: {type: integer, minimum: 1}",
- " ConnectorSourceBindingRequest:",
- " type: object",
- " additionalProperties: false",
- " required: [connector_id, version, source_uid, business_domain_uid, environment, approved_config]",
- " properties:",
- " binding_uid: {type: string, format: uuid}",
- " connector_id: {type: string}",
- " version: {type: string}",
- " source_uid: {type: string, format: uuid}",
- " business_domain_uid: {type: string, format: uuid}",
- " environment: {type: string, enum: [development, staging, production]}",
- " approved_config: {type: object}",
- " ConnectorScope:",
- " type: object",
- " additionalProperties: false",
- " properties:",
- " include_schemas: {type: array, uniqueItems: true, items: {type: string, minLength: 1}}",
- " exclude_schemas: {type: array, uniqueItems: true, items: {type: string, minLength: 1}}",
- " include_tables: {type: array, uniqueItems: true, items: {type: string, minLength: 1}}",
- " exclude_tables: {type: array, uniqueItems: true, items: {type: string, minLength: 1}}",
- " ConnectorHealthRequest:",
- " type: object",
- " additionalProperties: false",
- " required: [config]",
- " properties: {config: {type: object}}",
- " ConnectorCredentialRequest:",
- " type: object",
- " additionalProperties: false",
- " properties: {ttl_seconds: {type: integer, minimum: 60, maximum: 900}}",
- " ConnectorGraphRequest:",
- " type: object",
- " additionalProperties: false",
- " properties: {source_uid: {type: string, format: uuid}, business_domain_uid: {type: string, format: uuid}, process_key: {type: string}, run_uid: {type: string, format: uuid}, limit: {type: integer, minimum: 1, maximum: 1000}}",
- " ConnectorManifestEnvelope:",
- " type: object",
- " additionalProperties: false",
- " required: [code, message, data]",
- " properties:",
- " code: {type: integer}",
- " message: {type: string}",
- " data: {type: object, additionalProperties: false, required: [manifests], properties: {manifests: {type: array, items: {$ref: '#/components/schemas/ConnectorManifest'}}}}",
- " ConnectorManifest:",
- " type: object",
- " additionalProperties: false",
- " required: [connector_id, version, sdk_version, display_name, capabilities, config_schema]",
- " properties: {connector_id: {type: string}, version: {type: string}, sdk_version: {type: string}, display_name: {type: string}, capabilities: {type: array, items: {type: string}}, config_schema: {type: object}}",
- " ConnectorOperationResult:",
- " type: object",
- " additionalProperties: false",
- " required: [records, cursor, checkpoint, evidence, status]",
- " properties: {records: {type: array, items: {type: object}}, cursor: {type: object}, checkpoint: {type: object}, evidence: {type: object}, status: {type: string, enum: [succeeded, dry_run]}}",
- " ConnectorRunResultEnvelope:",
- " type: object",
- " additionalProperties: false",
- " required: [code, message, data]",
- " properties: {code: {type: integer}, message: {type: string}, data: {$ref: '#/components/schemas/ConnectorOperationResult'}}",
- " ConnectorRunRecord:",
- " type: object",
- " additionalProperties: false",
- " required: [uid, idempotency_key, connector_id, connector_version, source_uid, operation, status, attempt_count, checkpoint_summary, cursor_summary, dry_run]",
- " properties: {uid: {type: string, format: uuid}, idempotency_key: {type: string}, connector_id: {type: string}, connector_version: {type: string}, source_uid: {type: string, format: uuid}, principal_uid: {type: [string, 'null'], format: uuid}, business_domain_uid: {type: [string, 'null'], format: uuid}, environment: {type: [string, 'null']}, process_key: {type: [string, 'null']}, operation: {type: string}, status: {type: string}, attempt_count: {type: integer}, checkpoint_summary: {type: object}, cursor_summary: {type: object}, scope: {type: object}, error_category: {type: [string, 'null']}, dry_run: {type: boolean}, created_at: {type: string}, updated_at: {type: string}}",
- " ConnectorRunRecordEnvelope:",
- " type: object",
- " additionalProperties: false",
- " required: [code, message, data]",
- " properties: {code: {type: integer}, message: {type: string}, data: {$ref: '#/components/schemas/ConnectorRunRecord'}}",
- " ConnectorRunListEnvelope:",
- " type: object",
- " additionalProperties: false",
- " required: [code, message, data]",
- " properties: {code: {type: integer}, message: {type: string}, data: {type: object, additionalProperties: false, required: [runs], properties: {runs: {type: array, items: {$ref: '#/components/schemas/ConnectorRunRecord'}}}}}",
- " ConnectorValidationEnvelope:",
- " type: object",
- " additionalProperties: false",
- " required: [code, message, data]",
- " properties: {code: {type: integer}, message: {type: string}, data: {type: object, required: [valid, config_keys], properties: {valid: {const: true}, config_keys: {type: array, items: {type: string}}}, additionalProperties: false}}",
- " ConnectorErrorEnvelope:",
- " type: object",
- " additionalProperties: false",
- " required: [code, message, data, error]",
- " properties:",
- " code: {type: integer}",
- " message: {type: string}",
- " data: {type: 'null'}",
- " error: {type: object, additionalProperties: false, required: [code, category, retryable], properties: {code: {const: CONNECTOR_ERROR}, category: {type: string, enum: [configuration, conflict, authentication, permission, rate_limit, timeout, upstream, contract, cancelled]}, retryable: {type: boolean}}}",
- " ConnectorHealthEnvelope:",
- " type: object",
- " additionalProperties: false",
- " required: [code, message, data]",
- " properties: {code: {type: integer}, message: {type: string}, data: {type: object, additionalProperties: false, required: [status, detail], properties: {status: {type: string}, detail: {type: string}}}}",
- " ConnectorCompatibilityEnvelope:",
- " type: object",
- " additionalProperties: false",
- " required: [code, message, data]",
- " properties: {code: {type: integer}, message: {type: string}, data: {type: object, additionalProperties: false, required: [compatible, connector_version, sdk_version, detail], properties: {compatible: {type: boolean}, connector_version: {type: string}, sdk_version: {type: string}, detail: {type: string}}}}",
- " ConnectorPrincipalEnvelope:",
- " type: object",
- " additionalProperties: false",
- " required: [code, message, data]",
- " properties: {code: {type: integer}, message: {type: string}, data: {type: object, additionalProperties: false, required: [principal_uid], properties: {principal_uid: {type: string, format: uuid}}}}",
- " ConnectorSourceBindingEnvelope:",
- " type: object",
- " additionalProperties: false",
- " required: [code, message, data]",
- " properties: {code: {type: integer}, message: {type: string}, data: {type: object, additionalProperties: false, required: [binding_uid, binding_version, status, rebound_principals], properties: {binding_uid: {type: string, format: uuid}, binding_version: {type: integer, minimum: 1}, status: {const: approved}, rebound_principals: {type: integer, minimum: 0}}}}",
- " ConnectorCredentialEnvelope:",
- " type: object",
- " additionalProperties: false",
- " required: [code, message, data]",
- " properties: {code: {type: integer}, message: {type: string}, data: {oneOf: [{type: object, additionalProperties: false, required: [credential_uid, credential, expires_in, returned_once], properties: {credential_uid: {type: string, format: uuid}, credential: {type: string, writeOnly: true}, expires_in: {type: integer}, returned_once: {const: true}}}, {type: object, additionalProperties: false, required: [revoked], properties: {revoked: {type: boolean}}}]}}",
- " ConnectorGraphEnvelope:",
- " type: object",
- " additionalProperties: false",
- " required: [code, message, data]",
- " properties: {code: {type: integer}, message: {type: string}, data: {type: object, additionalProperties: false, required: [nodes, edges, summary], properties: {nodes: {type: array, items: {type: object, additionalProperties: false, required: [type, key], properties: {type: {type: string, enum: [source, asset, process, business_domain, run]}, key: {type: string}}}}, edges: {type: array, items: {type: object}}, summary: {type: object, additionalProperties: false, required: [node_count, edge_count], properties: {node_count: {type: integer}, edge_count: {type: integer}}}}}}",
- " securitySchemes:",
- " bearerAuth:",
- " type: http",
- " scheme: bearer",
- " bearerFormat: JWT",
- ' description: "下一阶段统一认证方案;当前路由尚未全部接入。"',
- ]
- )
- return "\n".join(lines) + "\n"
- def main(output: Path = OUTPUT) -> None:
- routes = extract_routes()
- if not routes:
- raise SystemExit("No Flask routes found")
- output.parent.mkdir(parents=True, exist_ok=True)
- output.write_text(render(routes), encoding="utf-8")
- try:
- label = output.relative_to(ROOT)
- except ValueError:
- label = output
- print(f"Generated {label} with {len(routes)} operations")
- if __name__ == "__main__":
- parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument("--output", type=Path, default=OUTPUT)
- args = parser.parse_args()
- main(args.output)
|