generate_openapi.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550
  1. #!/usr/bin/env python3
  2. """Generate the current Flask route inventory as an OpenAPI 3.1 contract."""
  3. from __future__ import annotations
  4. import argparse
  5. import ast
  6. import json
  7. import re
  8. from collections import defaultdict
  9. from pathlib import Path
  10. ROOT = Path(__file__).resolve().parents[1]
  11. OUTPUT = ROOT / "docs" / "architecture" / "OPENAPI.yaml"
  12. PREFIXES = {
  13. "meta_data": "/api/meta",
  14. "data_interface": "/api/interface",
  15. "data_rules": "/api/rules",
  16. "graph": "/api/graph",
  17. "system": "/api/system",
  18. "data_source": "/api/datasource",
  19. "data_development": "/api/development/v1",
  20. "data_flow": "/api/dataflow",
  21. "business_domain": "/api/bd",
  22. "data_factory": "/api/datafactory",
  23. "data_service": "/api/dataservice",
  24. "knowledge_base": "/api/knowledge",
  25. }
  26. SHORTHAND_METHODS = {"get", "post", "put", "patch", "delete"}
  27. RESPONSE_FIELDS = {
  28. ("data_development", "summary"): [
  29. "scope",
  30. "metrics",
  31. ],
  32. ("data_development", "details"): [
  33. "metric",
  34. "state",
  35. "records",
  36. "page",
  37. "page_size",
  38. "total",
  39. ],
  40. ("knowledge_base", "ask"): [
  41. "query_id",
  42. "mode",
  43. "answer",
  44. "answer_status",
  45. "degraded_components",
  46. "citations",
  47. "evidence",
  48. "freshness_status",
  49. ],
  50. }
  51. CONNECTOR_REQUEST_SCHEMAS = {
  52. "/api/datasource/connectors/config/validate": "ConnectorConfigValidationRequest",
  53. "/api/datasource/connectors/runs": "ConnectorRunRequest",
  54. "/api/datasource/connectors/machine/runs": "MachineConnectorRunRequest",
  55. "/api/datasource/connectors/source-bindings": "ConnectorSourceBindingRequest",
  56. "/api/datasource/connectors/principals": "ConnectorPrincipalRequest",
  57. "/api/datasource/connectors/{connector_id}/{version}/health": "ConnectorHealthRequest",
  58. "/api/datasource/connectors/principals/{principal_uid}/credentials": "ConnectorCredentialRequest",
  59. "/api/datasource/connectors/credentials/{credential_uid}/{action}": "ConnectorCredentialRequest",
  60. "/api/datasource/graph": "ConnectorGraphRequest",
  61. }
  62. CONNECTOR_RESPONSE_SCHEMAS = {
  63. ("/api/datasource/connectors/manifests", "get"): "ConnectorManifestEnvelope",
  64. ("/api/datasource/connectors/runs", "get"): "ConnectorRunListEnvelope",
  65. ("/api/datasource/connectors/runs", "post"): "ConnectorRunResultEnvelope",
  66. ("/api/datasource/connectors/machine/runs", "post"): "ConnectorRunResultEnvelope",
  67. (
  68. "/api/datasource/connectors/runs/{idempotency_key}/cancel",
  69. "post",
  70. ): "ConnectorRunRecordEnvelope",
  71. (
  72. "/api/datasource/connectors/runs/{idempotency_key}/resume",
  73. "post",
  74. ): "ConnectorRunResultEnvelope",
  75. (
  76. "/api/datasource/connectors/config/validate",
  77. "post",
  78. ): "ConnectorValidationEnvelope",
  79. (
  80. "/api/datasource/connectors/{connector_id}/{version}/health",
  81. "post",
  82. ): "ConnectorHealthEnvelope",
  83. (
  84. "/api/datasource/connectors/{connector_id}/{version}/compatibility",
  85. "get",
  86. ): "ConnectorCompatibilityEnvelope",
  87. ("/api/datasource/connectors/principals", "post"): "ConnectorPrincipalEnvelope",
  88. (
  89. "/api/datasource/connectors/source-bindings",
  90. "post",
  91. ): "ConnectorSourceBindingEnvelope",
  92. (
  93. "/api/datasource/connectors/principals/{principal_uid}/credentials",
  94. "post",
  95. ): "ConnectorCredentialEnvelope",
  96. (
  97. "/api/datasource/connectors/credentials/{credential_uid}/{action}",
  98. "post",
  99. ): "ConnectorCredentialEnvelope",
  100. ("/api/datasource/graph", "post"): "ConnectorGraphEnvelope",
  101. }
  102. NO_REQUEST_BODY_PATHS = {
  103. "/api/datasource/connectors/runs/{idempotency_key}/cancel",
  104. "/api/datasource/connectors/runs/{idempotency_key}/resume",
  105. "/api/datasource/connectors/machine/runs/{idempotency_key}/cancel",
  106. "/api/datasource/connectors/machine/runs/{idempotency_key}/resume",
  107. "/api/datasource/connectors/source-bindings/{binding_uid}/revoke",
  108. }
  109. OPTIONAL_REQUEST_BODY_PATHS = {
  110. "/api/datasource/connectors/principals/{principal_uid}/credentials",
  111. "/api/datasource/connectors/credentials/{credential_uid}/{action}",
  112. }
  113. def quoted(value: str) -> str:
  114. return json.dumps(value, ensure_ascii=False)
  115. def route_path(raw_path: str) -> tuple[str, list[dict[str, str]]]:
  116. parameters: list[dict[str, str]] = []
  117. def replace(match: re.Match[str]) -> str:
  118. converter = match.group(1) or "string"
  119. name = match.group(2)
  120. schema_type = "integer" if converter in {"int", "float"} else "string"
  121. parameters.append({"name": name, "type": schema_type})
  122. return "{" + name + "}"
  123. return re.sub(
  124. r"<(?:([a-zA-Z_]+):)?([a-zA-Z_][a-zA-Z0-9_]*)>", replace, raw_path
  125. ), parameters
  126. def extract_routes() -> list[dict[str, object]]:
  127. routes: list[dict[str, object]] = []
  128. for module, prefix in PREFIXES.items():
  129. for route_file in sorted((ROOT / "app" / "api" / module).glob("*.py")):
  130. if route_file.name == "__init__.py":
  131. continue
  132. tree = ast.parse(route_file.read_text(encoding="utf-8"))
  133. for node in tree.body:
  134. if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
  135. continue
  136. for decorator in node.decorator_list:
  137. if not isinstance(decorator, ast.Call):
  138. continue
  139. function = decorator.func
  140. if not (
  141. isinstance(function, ast.Attribute)
  142. and isinstance(function.value, ast.Name)
  143. and function.value.id == "bp"
  144. and decorator.args
  145. ):
  146. continue
  147. if function.attr == "route":
  148. methods = ["GET"]
  149. for keyword in decorator.keywords:
  150. if keyword.arg == "methods":
  151. methods = ast.literal_eval(keyword.value)
  152. elif function.attr in SHORTHAND_METHODS:
  153. methods = [function.attr.upper()]
  154. else:
  155. continue
  156. raw_path = ast.literal_eval(decorator.args[0])
  157. path, parameters = route_path(prefix + raw_path)
  158. summary = (
  159. ast.get_docstring(node) or node.name.replace("_", " ")
  160. ).splitlines()[0]
  161. for method in methods:
  162. routes.append(
  163. {
  164. "path": path,
  165. "method": method.lower(),
  166. "tag": module,
  167. "operation_id": f"{module}_{node.name}_{method.lower()}",
  168. "summary": summary,
  169. "parameters": parameters,
  170. "source": str(route_file.relative_to(ROOT)),
  171. }
  172. )
  173. return sorted(routes, key=lambda item: (str(item["path"]), str(item["method"])))
  174. def render(routes: list[dict[str, object]]) -> str:
  175. by_path: dict[str, list[dict[str, object]]] = defaultdict(list)
  176. for route in routes:
  177. by_path[str(route["path"])].append(route)
  178. lines = [
  179. "openapi: 3.1.0",
  180. "info:",
  181. ' title: "DataOps Platform API(当前代码基线)"',
  182. ' version: "2026-07-16"',
  183. ' description: "由 scripts/generate_openapi.py 从 app/api/*/routes.py 生成。请求体与响应细节仍以现有专项 API 文档和代码为准。"',
  184. f"x-route-count: {len(routes)}",
  185. "servers:",
  186. ' - url: "http://localhost:15500"',
  187. ' description: "全本地隔离测试后端"',
  188. "tags:",
  189. ]
  190. for tag in PREFIXES:
  191. lines.extend([f" - name: {tag}", f" description: {quoted(PREFIXES[tag])}"])
  192. lines.append("paths:")
  193. for path, operations in sorted(by_path.items()):
  194. lines.append(f" {quoted(path)}:")
  195. for operation in operations:
  196. lines.extend(
  197. [
  198. f" {operation['method']}:",
  199. f" tags: [{operation['tag']}]",
  200. f" operationId: {operation['operation_id']}",
  201. f" summary: {quoted(str(operation['summary']))}",
  202. f" x-source: {quoted(str(operation['source']))}",
  203. ]
  204. )
  205. parameters = operation["parameters"]
  206. if path.startswith("/api/datasource/connectors/machine/runs"):
  207. parameters = [
  208. *parameters,
  209. {
  210. "name": "X-Connector-Credential",
  211. "type": "string",
  212. "header": True,
  213. },
  214. ]
  215. if parameters:
  216. lines.append(" parameters:")
  217. for parameter in parameters:
  218. lines.extend(
  219. [
  220. f" - name: {parameter['name']}",
  221. f" in: {'header' if parameter.get('header') else 'path'}",
  222. " required: true",
  223. " schema:",
  224. f" type: {parameter['type']}",
  225. ]
  226. )
  227. if (
  228. path
  229. == "/api/datasource/connectors/credentials/{credential_uid}/{action}"
  230. and parameter["name"] == "action"
  231. ):
  232. lines.append(" enum: [revoke, rotate]")
  233. if (
  234. operation["method"] in {"post", "put", "patch"}
  235. and path not in NO_REQUEST_BODY_PATHS
  236. ):
  237. connector_schema = CONNECTOR_REQUEST_SCHEMAS.get(path)
  238. lines.extend(
  239. [
  240. " requestBody:",
  241. " required: "
  242. + (
  243. "false"
  244. if path in OPTIONAL_REQUEST_BODY_PATHS
  245. else "true" if connector_schema else "false"
  246. ),
  247. " content:",
  248. " application/json:",
  249. " schema:",
  250. f" $ref: '#/components/schemas/{connector_schema}'"
  251. if connector_schema
  252. else " type: object",
  253. ]
  254. )
  255. if not connector_schema:
  256. lines.append(" additionalProperties: true")
  257. response_fields = RESPONSE_FIELDS.get(
  258. (str(operation["tag"]), str(operation["operation_id"]).split("_")[-2])
  259. )
  260. if response_fields:
  261. lines.append(
  262. " x-response-fields: [" + ", ".join(response_fields) + "]"
  263. )
  264. connector_response = CONNECTOR_RESPONSE_SCHEMAS.get(
  265. (path, str(operation["method"]))
  266. )
  267. response_status = (
  268. "201"
  269. if operation["method"] == "post"
  270. and path
  271. in {
  272. "/api/datasource/connectors/runs",
  273. "/api/datasource/connectors/machine/runs",
  274. "/api/datasource/connectors/principals",
  275. "/api/datasource/connectors/principals/{principal_uid}/credentials",
  276. "/api/datasource/connectors/source-bindings",
  277. }
  278. else "200"
  279. )
  280. if path.startswith("/api/datasource/connectors"):
  281. required_permission = (
  282. "machine-credential"
  283. if path.startswith("/api/datasource/connectors/machine/runs")
  284. else (
  285. "connectors:manage"
  286. if "/source-bindings" in path
  287. else (
  288. "connectors:read"
  289. if operation["method"] == "get"
  290. else (
  291. "connectors:manage"
  292. if "/principals" in path or "/credentials" in path
  293. else "connectors:operate"
  294. )
  295. )
  296. )
  297. )
  298. lines.append(
  299. f" x-required-permission: {quoted(required_permission)}"
  300. )
  301. elif path == "/api/datasource/graph":
  302. lines.append(' x-required-permission: "connectors:read"')
  303. lines.extend(
  304. [
  305. " responses:",
  306. f' "{response_status}":',
  307. ' description: "请求已由当前实现处理"',
  308. " content:",
  309. " application/json:",
  310. " schema:",
  311. f" $ref: '#/components/schemas/{connector_response or 'ApiEnvelope'}'",
  312. " default:",
  313. ' description: "错误响应"',
  314. " content:",
  315. " application/json:",
  316. " schema:",
  317. f" $ref: '#/components/schemas/{'ConnectorErrorEnvelope' if path.startswith('/api/datasource/connectors') or path == '/api/datasource/graph' else 'ApiEnvelope'}'",
  318. ]
  319. )
  320. lines.extend(
  321. [
  322. "components:",
  323. " schemas:",
  324. " ApiEnvelope:",
  325. " type: object",
  326. " additionalProperties: true",
  327. " required: [code, message, data]",
  328. " properties:",
  329. " code:",
  330. " type: integer",
  331. " message:",
  332. " type: string",
  333. " data: {}",
  334. " error: {type: object}",
  335. " ConnectorConfigValidationRequest:",
  336. " type: object",
  337. " additionalProperties: false",
  338. " required: [connector_id, version, config]",
  339. " properties:",
  340. " connector_id: {type: string, minLength: 3, maxLength: 64}",
  341. " version: {type: string, pattern: '^[0-9]+\\.[0-9]+\\.[0-9]+'}",
  342. " config: {type: object}",
  343. " ConnectorRunRequest:",
  344. " type: object",
  345. " additionalProperties: false",
  346. " required: [connector_id, version, source_uid, operation, config, scope, dry_run]",
  347. " properties:",
  348. " connector_id: {type: string}",
  349. " version: {type: string}",
  350. " source_uid: {type: string, format: uuid}",
  351. " operation: {type: string, enum: [discover, snapshot, incremental, lineage, profile, evidence]}",
  352. " config: {type: object}",
  353. " scope: {$ref: '#/components/schemas/ConnectorScope'}",
  354. " cursor: {type: object}",
  355. " checkpoint: {type: object}",
  356. " idempotency_key: {type: string, pattern: '^[a-f0-9]{64}$'}",
  357. " dry_run: {const: true}",
  358. " process_key: {type: string, minLength: 1, maxLength: 300}",
  359. " MachineConnectorRunRequest:",
  360. " type: object",
  361. " additionalProperties: false",
  362. " required: [connector_id, version, source_uid, business_domain_uid, environment, process_key, operation, scope]",
  363. " properties:",
  364. " connector_id: {type: string}",
  365. " version: {type: string}",
  366. " source_uid: {type: string, format: uuid}",
  367. " business_domain_uid: {type: string, format: uuid}",
  368. " environment: {type: string, enum: [development, staging, production]}",
  369. " process_key: {type: string, minLength: 1, maxLength: 300}",
  370. " operation: {type: string, enum: [discover, snapshot, incremental, lineage, profile, evidence]}",
  371. " scope: {$ref: '#/components/schemas/ConnectorScope'}",
  372. " cursor: {type: object}",
  373. " checkpoint: {type: object}",
  374. " idempotency_key: {type: string, pattern: '^[a-f0-9]{64}$'}",
  375. " dry_run: {type: boolean, default: false}",
  376. " ConnectorPrincipalRequest:",
  377. " type: object",
  378. " additionalProperties: false",
  379. " required: [connector_id, version, source_uid, business_domain_uid, environment, operations, scopes]",
  380. " properties:",
  381. " connector_id: {type: string}",
  382. " version: {type: string}",
  383. " source_uid: {type: string, format: uuid}",
  384. " business_domain_uid: {type: string, format: uuid}",
  385. " environment: {type: string, enum: [development, staging, production]}",
  386. " operations: {type: array, minItems: 1, uniqueItems: true, items: {type: string}}",
  387. " scopes: {$ref: '#/components/schemas/ConnectorScope'}",
  388. " source_binding_uid: {type: string, format: uuid}",
  389. " source_binding_version: {type: integer, minimum: 1}",
  390. " ConnectorSourceBindingRequest:",
  391. " type: object",
  392. " additionalProperties: false",
  393. " required: [connector_id, version, source_uid, business_domain_uid, environment, approved_config]",
  394. " properties:",
  395. " binding_uid: {type: string, format: uuid}",
  396. " connector_id: {type: string}",
  397. " version: {type: string}",
  398. " source_uid: {type: string, format: uuid}",
  399. " business_domain_uid: {type: string, format: uuid}",
  400. " environment: {type: string, enum: [development, staging, production]}",
  401. " approved_config: {type: object}",
  402. " ConnectorScope:",
  403. " type: object",
  404. " additionalProperties: false",
  405. " properties:",
  406. " include_schemas: {type: array, uniqueItems: true, items: {type: string, minLength: 1}}",
  407. " exclude_schemas: {type: array, uniqueItems: true, items: {type: string, minLength: 1}}",
  408. " include_tables: {type: array, uniqueItems: true, items: {type: string, minLength: 1}}",
  409. " exclude_tables: {type: array, uniqueItems: true, items: {type: string, minLength: 1}}",
  410. " ConnectorHealthRequest:",
  411. " type: object",
  412. " additionalProperties: false",
  413. " required: [config]",
  414. " properties: {config: {type: object}}",
  415. " ConnectorCredentialRequest:",
  416. " type: object",
  417. " additionalProperties: false",
  418. " properties: {ttl_seconds: {type: integer, minimum: 60, maximum: 900}}",
  419. " ConnectorGraphRequest:",
  420. " type: object",
  421. " additionalProperties: false",
  422. " 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}}",
  423. " ConnectorManifestEnvelope:",
  424. " type: object",
  425. " additionalProperties: false",
  426. " required: [code, message, data]",
  427. " properties:",
  428. " code: {type: integer}",
  429. " message: {type: string}",
  430. " data: {type: object, additionalProperties: false, required: [manifests], properties: {manifests: {type: array, items: {$ref: '#/components/schemas/ConnectorManifest'}}}}",
  431. " ConnectorManifest:",
  432. " type: object",
  433. " additionalProperties: false",
  434. " required: [connector_id, version, sdk_version, display_name, capabilities, config_schema]",
  435. " 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}}",
  436. " ConnectorOperationResult:",
  437. " type: object",
  438. " additionalProperties: false",
  439. " required: [records, cursor, checkpoint, evidence, status]",
  440. " properties: {records: {type: array, items: {type: object}}, cursor: {type: object}, checkpoint: {type: object}, evidence: {type: object}, status: {type: string, enum: [succeeded, dry_run]}}",
  441. " ConnectorRunResultEnvelope:",
  442. " type: object",
  443. " additionalProperties: false",
  444. " required: [code, message, data]",
  445. " properties: {code: {type: integer}, message: {type: string}, data: {$ref: '#/components/schemas/ConnectorOperationResult'}}",
  446. " ConnectorRunRecord:",
  447. " type: object",
  448. " additionalProperties: false",
  449. " required: [uid, idempotency_key, connector_id, connector_version, source_uid, operation, status, attempt_count, checkpoint_summary, cursor_summary, dry_run]",
  450. " 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}}",
  451. " ConnectorRunRecordEnvelope:",
  452. " type: object",
  453. " additionalProperties: false",
  454. " required: [code, message, data]",
  455. " properties: {code: {type: integer}, message: {type: string}, data: {$ref: '#/components/schemas/ConnectorRunRecord'}}",
  456. " ConnectorRunListEnvelope:",
  457. " type: object",
  458. " additionalProperties: false",
  459. " required: [code, message, data]",
  460. " properties: {code: {type: integer}, message: {type: string}, data: {type: object, additionalProperties: false, required: [runs], properties: {runs: {type: array, items: {$ref: '#/components/schemas/ConnectorRunRecord'}}}}}",
  461. " ConnectorValidationEnvelope:",
  462. " type: object",
  463. " additionalProperties: false",
  464. " required: [code, message, data]",
  465. " 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}}",
  466. " ConnectorErrorEnvelope:",
  467. " type: object",
  468. " additionalProperties: false",
  469. " required: [code, message, data, error]",
  470. " properties:",
  471. " code: {type: integer}",
  472. " message: {type: string}",
  473. " data: {type: 'null'}",
  474. " 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}}}",
  475. " ConnectorHealthEnvelope:",
  476. " type: object",
  477. " additionalProperties: false",
  478. " required: [code, message, data]",
  479. " properties: {code: {type: integer}, message: {type: string}, data: {type: object, additionalProperties: false, required: [status, detail], properties: {status: {type: string}, detail: {type: string}}}}",
  480. " ConnectorCompatibilityEnvelope:",
  481. " type: object",
  482. " additionalProperties: false",
  483. " required: [code, message, data]",
  484. " 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}}}}",
  485. " ConnectorPrincipalEnvelope:",
  486. " type: object",
  487. " additionalProperties: false",
  488. " required: [code, message, data]",
  489. " properties: {code: {type: integer}, message: {type: string}, data: {type: object, additionalProperties: false, required: [principal_uid], properties: {principal_uid: {type: string, format: uuid}}}}",
  490. " ConnectorSourceBindingEnvelope:",
  491. " type: object",
  492. " additionalProperties: false",
  493. " required: [code, message, data]",
  494. " 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}}}}",
  495. " ConnectorCredentialEnvelope:",
  496. " type: object",
  497. " additionalProperties: false",
  498. " required: [code, message, data]",
  499. " 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}}}]}}",
  500. " ConnectorGraphEnvelope:",
  501. " type: object",
  502. " additionalProperties: false",
  503. " required: [code, message, data]",
  504. " 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}}}}}}",
  505. " securitySchemes:",
  506. " bearerAuth:",
  507. " type: http",
  508. " scheme: bearer",
  509. " bearerFormat: JWT",
  510. ' description: "下一阶段统一认证方案;当前路由尚未全部接入。"',
  511. ]
  512. )
  513. return "\n".join(lines) + "\n"
  514. def main(output: Path = OUTPUT) -> None:
  515. routes = extract_routes()
  516. if not routes:
  517. raise SystemExit("No Flask routes found")
  518. output.parent.mkdir(parents=True, exist_ok=True)
  519. output.write_text(render(routes), encoding="utf-8")
  520. try:
  521. label = output.relative_to(ROOT)
  522. except ValueError:
  523. label = output
  524. print(f"Generated {label} with {len(routes)} operations")
  525. if __name__ == "__main__":
  526. parser = argparse.ArgumentParser(description=__doc__)
  527. parser.add_argument("--output", type=Path, default=OUTPUT)
  528. args = parser.parse_args()
  529. main(args.output)