generate_openapi.py 70 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186
  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. EDGE_SAFE_KEY_PATTERN = (
  28. "^(?!.*(?:raw|rows?|sql|credential|secret|private|password|token|local_path))"
  29. "[a-z][a-z0-9_]{0,79}$"
  30. )
  31. def _edge_safe_schema_lines() -> list[str]:
  32. lines = [
  33. " EdgeSafeSummary:",
  34. " type: object",
  35. " x-max-policy-depth: 12",
  36. " maxProperties: 64",
  37. f" propertyNames: {{type: string, minLength: 1, maxLength: 80, pattern: '{EDGE_SAFE_KEY_PATTERN}'}}",
  38. " additionalProperties: {$ref: '#/components/schemas/EdgeSafeValue12'}",
  39. " description: 'Policy-approved bounded metadata/statistics/health/diagnostic fields only; raw rows, SQL, credentials, and local paths are forbidden.'",
  40. " EdgeSafeScalar:",
  41. " anyOf:",
  42. " - {type: string, maxLength: 4096}",
  43. " - {type: integer, minimum: -9007199254740991, maximum: 9007199254740991}",
  44. " - {type: number, minimum: -1000000000000, maximum: 1000000000000}",
  45. " - {type: boolean}",
  46. " - {type: 'null'}",
  47. " EdgeSafeValue0: {$ref: '#/components/schemas/EdgeSafeScalar'}",
  48. ]
  49. for depth in range(1, 13):
  50. previous = depth - 1
  51. lines.extend(
  52. [
  53. f" EdgeSafeValue{depth}:",
  54. " anyOf:",
  55. " - {$ref: '#/components/schemas/EdgeSafeScalar'}",
  56. f" - {{type: array, maxItems: 100, items: {{$ref: '#/components/schemas/EdgeSafeValue{previous}'}}}}",
  57. f" - {{type: object, maxProperties: 32, propertyNames: {{type: string, minLength: 1, maxLength: 80, pattern: '{EDGE_SAFE_KEY_PATTERN}'}}, additionalProperties: {{$ref: '#/components/schemas/EdgeSafeValue{previous}'}}}}",
  58. ]
  59. )
  60. lines.append(" EdgeSafeValue: {$ref: '#/components/schemas/EdgeSafeValue12'}")
  61. return lines
  62. RESPONSE_FIELDS = {
  63. ("data_development", "summary"): [
  64. "scope",
  65. "metrics",
  66. ],
  67. ("data_development", "details"): [
  68. "metric",
  69. "state",
  70. "records",
  71. "page",
  72. "page_size",
  73. "total",
  74. ],
  75. ("knowledge_base", "ask"): [
  76. "query_id",
  77. "mode",
  78. "answer",
  79. "answer_status",
  80. "degraded_components",
  81. "citations",
  82. "evidence",
  83. "freshness_status",
  84. ],
  85. }
  86. CONNECTOR_REQUEST_SCHEMAS = {
  87. "/api/datasource/connectors/config/validate": "ConnectorConfigValidationRequest",
  88. "/api/datasource/connectors/runs": "ConnectorRunRequest",
  89. "/api/datasource/connectors/machine/runs": "MachineConnectorRunRequest",
  90. "/api/datasource/connectors/source-bindings": "ConnectorSourceBindingRequest",
  91. "/api/datasource/connectors/principals": "ConnectorPrincipalRequest",
  92. "/api/datasource/connectors/{connector_id}/{version}/health": "ConnectorHealthRequest",
  93. "/api/datasource/connectors/principals/{principal_uid}/credentials": "ConnectorCredentialRequest",
  94. "/api/datasource/connectors/credentials/{credential_uid}/{action}": "ConnectorCredentialRequest",
  95. "/api/datasource/graph": "ConnectorGraphRequest",
  96. }
  97. CONNECTOR_RESPONSE_SCHEMAS = {
  98. ("/api/datasource/connectors/manifests", "get"): "ConnectorManifestEnvelope",
  99. ("/api/datasource/connectors/runs", "get"): "ConnectorRunListEnvelope",
  100. ("/api/datasource/connectors/runs", "post"): "ConnectorRunResultEnvelope",
  101. ("/api/datasource/connectors/machine/runs", "post"): "ConnectorRunResultEnvelope",
  102. (
  103. "/api/datasource/connectors/runs/{idempotency_key}/cancel",
  104. "post",
  105. ): "ConnectorRunRecordEnvelope",
  106. (
  107. "/api/datasource/connectors/runs/{idempotency_key}/resume",
  108. "post",
  109. ): "ConnectorRunResultEnvelope",
  110. (
  111. "/api/datasource/connectors/config/validate",
  112. "post",
  113. ): "ConnectorValidationEnvelope",
  114. (
  115. "/api/datasource/connectors/{connector_id}/{version}/health",
  116. "post",
  117. ): "ConnectorHealthEnvelope",
  118. (
  119. "/api/datasource/connectors/{connector_id}/{version}/compatibility",
  120. "get",
  121. ): "ConnectorCompatibilityEnvelope",
  122. ("/api/datasource/connectors/principals", "post"): "ConnectorPrincipalEnvelope",
  123. (
  124. "/api/datasource/connectors/source-bindings",
  125. "post",
  126. ): "ConnectorSourceBindingEnvelope",
  127. (
  128. "/api/datasource/connectors/principals/{principal_uid}/credentials",
  129. "post",
  130. ): "ConnectorCredentialEnvelope",
  131. (
  132. "/api/datasource/connectors/credentials/{credential_uid}/{action}",
  133. "post",
  134. ): "ConnectorCredentialEnvelope",
  135. ("/api/datasource/graph", "post"): "ConnectorGraphEnvelope",
  136. }
  137. NO_REQUEST_BODY_PATHS = {
  138. "/api/datasource/connectors/runs/{idempotency_key}/cancel",
  139. "/api/datasource/connectors/runs/{idempotency_key}/resume",
  140. "/api/datasource/connectors/machine/runs/{idempotency_key}/cancel",
  141. "/api/datasource/connectors/machine/runs/{idempotency_key}/resume",
  142. "/api/datasource/connectors/source-bindings/{binding_uid}/revoke",
  143. "/api/datasource/edge/tasks/{task_id}/cancel",
  144. "/api/datasource/edge/gateways/{gateway_id}/revoke",
  145. }
  146. OPTIONAL_REQUEST_BODY_PATHS = {
  147. "/api/datasource/connectors/principals/{principal_uid}/credentials",
  148. "/api/datasource/connectors/credentials/{credential_uid}/{action}",
  149. }
  150. EDGE_REQUEST_SCHEMAS = {
  151. "/api/datasource/edge/enrollments": "EdgeEnrollmentRequest",
  152. "/api/datasource/edge/register": "EdgeRegisterRequest",
  153. "/api/datasource/edge/gateways/{gateway_id}/heartbeat": "EdgeHeartbeatRequest",
  154. "/api/datasource/edge/gateways/{gateway_id}/rotate": "EdgeRotateRequest",
  155. "/api/datasource/edge/tasks": "EdgeTaskContract",
  156. "/api/datasource/edge/gateways/{gateway_id}/tasks/pull": "EdgeMachineBinding",
  157. "/api/datasource/edge/gateways/{gateway_id}/tasks/{task_id}/outcome": "EdgeTaskOutcomeRequest",
  158. "/api/datasource/edge/gateways/{gateway_id}/events": "EdgeEventRequest",
  159. "/api/datasource/edge/gateways/{gateway_id}/reconcile": "EdgeReconcileRequest",
  160. "/api/datasource/edge/gateways/{gateway_id}/releases": "EdgeReleaseOfferRequest",
  161. "/api/datasource/edge/gateways/{gateway_id}/releases/ack": "EdgeReleaseAckRequest",
  162. }
  163. EDGE_RESPONSE_SCHEMAS = {
  164. ("/api/datasource/edge/enrollments", "post"): "EdgeEnrollmentEnvelope",
  165. ("/api/datasource/edge/register", "post"): "EdgeRegistrationEnvelope",
  166. ("/api/datasource/edge/gateways", "get"): "EdgeGatewayListEnvelope",
  167. (
  168. "/api/datasource/edge/gateways/{gateway_id}/tasks/pull",
  169. "post",
  170. ): "EdgeTaskPullEnvelope",
  171. (
  172. "/api/datasource/edge/gateways/{gateway_id}/events",
  173. "post",
  174. ): "EdgeEventAckEnvelope",
  175. (
  176. "/api/datasource/edge/gateways/{gateway_id}/reconcile",
  177. "post",
  178. ): "EdgeReconcileResponse",
  179. (
  180. "/api/datasource/edge/gateways/{gateway_id}/releases",
  181. "post",
  182. ): "EdgeReleaseEnvelope",
  183. }
  184. PRODUCTION_OBSERVABILITY_REQUEST_SCHEMAS = {
  185. "/api/datafactory/observability/operations/deliveries": "ProductionDeliveryRequest",
  186. }
  187. PRODUCTION_OBSERVABILITY_RESPONSE_SCHEMAS = {
  188. (
  189. "/api/datafactory/observability/operations/deliveries",
  190. "post",
  191. ): "ProductionDeliveryEnvelope",
  192. }
  193. TRUSTED_DELIVERY_PERMISSIONS = {
  194. "/api/system/trusted-delivery/policy-versions": "security-governance:operate",
  195. "/api/system/trusted-delivery/policy-versions/activate": "security-governance:operate",
  196. "/api/system/trusted-delivery/policy-versions/rollback": "security-governance:operate",
  197. "/api/system/trusted-delivery/decisions": "security-governance:operate",
  198. "/api/system/trusted-delivery/provisions": "security-governance:operate",
  199. "/api/system/trusted-delivery/legal-holds": "security-governance:manage",
  200. "/api/system/trusted-delivery/legal-holds/release": "security-governance:manage",
  201. "/api/system/trusted-delivery/reclaim/preview": "security-governance:manage",
  202. "/api/system/trusted-delivery/reclaim/execute": "security-governance:manage",
  203. "/api/system/trusted-delivery/subscriptions": "security-governance:operate",
  204. "/api/system/trusted-delivery/subscriptions/{subscription_uid}/activate": "security-governance:operate",
  205. "/api/system/trusted-delivery/subscriptions/{subscription_uid}/pause": "security-governance:operate",
  206. "/api/system/trusted-delivery/subscriptions/{subscription_uid}/resume": "security-governance:operate",
  207. "/api/system/trusted-delivery/subscriptions/{subscription_uid}/terminate": "security-governance:operate",
  208. "/api/system/trusted-delivery/subscriptions/deliveries/{delivery_uid}/compensate": "security-governance:manage",
  209. "/api/system/trusted-delivery/subscriptions/anomalies": "security-governance:operate",
  210. "/api/system/trusted-delivery/controls/profiles/{profile_id}/active": "security-governance:read",
  211. "/api/system/trusted-delivery/controls/release-gates/evaluate": "security-governance:operate",
  212. "/api/system/trusted-delivery/controls/profiles/activate": "security-governance:manage",
  213. "/api/system/trusted-delivery/controls/profiles/rollback": "security-governance:manage",
  214. "/api/system/trusted-delivery/controls/capability-approvals": "security-governance:manage",
  215. "/api/system/trusted-delivery/controls/evidence": "security-governance:read",
  216. "/api/system/trusted-delivery/controls/destruction-approvals": "security-governance:manage",
  217. }
  218. TENANT_CONTROL_PERMISSIONS = {
  219. "/api/system/tenant/quota-claims": "identity:operate",
  220. "/api/system/tenant/approvals": "identity:manage",
  221. "/api/system/tenant/provisions": "identity:manage",
  222. "/api/system/tenant/lifecycle/activate": "identity:manage",
  223. "/api/system/tenant/lifecycle/freeze": "identity:manage",
  224. "/api/system/tenant/lifecycle/recover": "identity:manage",
  225. "/api/system/tenant/lifecycle/deletion-candidate": "identity:manage",
  226. "/api/system/tenant/lifecycle/rollback": "identity:manage",
  227. "/api/system/tenant/lifecycle/delete": "identity:manage",
  228. "/api/system/tenant/status": "identity:read",
  229. "/api/system/tenant/audit": "identity:read",
  230. "/api/system/tenant/manifests": "identity:read",
  231. }
  232. TENANT_CONTROL_BODY_FIELDS = {
  233. "/api/system/tenant/quota-claims": (
  234. ("quota_name", "string"),
  235. ("amount", "string"),
  236. ("idempotency_key", "string"),
  237. ),
  238. "/api/system/tenant/provisions": (("idempotency_key", "string"),),
  239. "/api/system/tenant/approvals": (
  240. ("operation", "string"),
  241. ("expected_fence", "string"),
  242. ("idempotency_key", "string"),
  243. ("backup_digest", "string"),
  244. ("retention_seconds", "string"),
  245. ),
  246. "/api/system/tenant/lifecycle/activate": (
  247. ("expected_fence", "string"),
  248. ("idempotency_key", "string"),
  249. ("approval_ref", "string"),
  250. ("backup_digest", "string"),
  251. ("retention_seconds", "string"),
  252. ),
  253. "/api/system/tenant/lifecycle/freeze": (
  254. ("expected_fence", "string"),
  255. ("idempotency_key", "string"),
  256. ("approval_ref", "string"),
  257. ("backup_digest", "string"),
  258. ("retention_seconds", "string"),
  259. ),
  260. "/api/system/tenant/lifecycle/recover": (
  261. ("expected_fence", "string"),
  262. ("idempotency_key", "string"),
  263. ("approval_ref", "string"),
  264. ("backup_digest", "string"),
  265. ("retention_seconds", "string"),
  266. ),
  267. "/api/system/tenant/lifecycle/deletion-candidate": (
  268. ("expected_fence", "string"),
  269. ("idempotency_key", "string"),
  270. ("approval_ref", "string"),
  271. ("backup_digest", "string"),
  272. ("retention_seconds", "string"),
  273. ),
  274. "/api/system/tenant/lifecycle/rollback": (
  275. ("expected_fence", "string"),
  276. ("idempotency_key", "string"),
  277. ("approval_ref", "string"),
  278. ("backup_digest", "string"),
  279. ("retention_seconds", "string"),
  280. ),
  281. "/api/system/tenant/lifecycle/delete": (
  282. ("expected_fence", "string"),
  283. ("idempotency_key", "string"),
  284. ("approval_ref", "string"),
  285. ("backup_digest", "string"),
  286. ("retention_seconds", "string"),
  287. ),
  288. }
  289. TENANT_CONTROL_REQUIRED_FIELDS = {
  290. path: tuple(field for field, _ in fields)
  291. for path, fields in TENANT_CONTROL_BODY_FIELDS.items()
  292. if "/lifecycle/" not in path
  293. }
  294. TENANT_CONTROL_REQUIRED_FIELDS.update(
  295. {
  296. path: ("expected_fence", "idempotency_key")
  297. for path in TENANT_CONTROL_BODY_FIELDS
  298. if "/lifecycle/" in path
  299. }
  300. )
  301. TENANT_CONTROL_REQUIRED_FIELDS["/api/system/tenant/approvals"] = (
  302. "operation",
  303. "expected_fence",
  304. "idempotency_key",
  305. )
  306. BI_AI_CATALOG_PERMISSIONS = {
  307. "/api/system/bi-ai-catalog/local-fixture/sync": "bi-ai-catalog:manage",
  308. "/api/system/bi-ai-catalog/assets/search": "bi-ai-catalog:read",
  309. "/api/system/bi-ai-catalog/assets/{external_uid}/impact": "bi-ai-catalog:read",
  310. "/api/system/bi-ai-catalog/audit": "bi-ai-catalog:read",
  311. }
  312. METERING_SHOWBACK_PERMISSIONS = {
  313. "/api/system/metering/events": "metering:manage",
  314. "/api/system/metering/allocations": "metering:manage",
  315. "/api/system/metering/budgets": "metering:manage",
  316. "/api/system/metering/allocation-replay": "metering:read",
  317. "/api/system/metering/showback": "metering:read",
  318. "/api/system/metering/reconciliation": "metering:read",
  319. }
  320. METERING_SHOWBACK_BODY_FIELDS = {
  321. "/api/system/metering/events": (
  322. ("schema_version", "integer"), ("event_uid", "string"), ("event_kind", "string"),
  323. ("occurred_at", "string"), ("window_start", "string"), ("window_end", "string"),
  324. ("quantity", "string"), ("unit", "string"), ("idempotency_key", "string"),
  325. ("evidence", "object"), ("mapping", "object"),
  326. ),
  327. "/api/system/metering/allocations": (
  328. ("schema_version", "integer"), ("rule_uid", "string"), ("rule_version", "integer"),
  329. ("effective_start", "string"), ("effective_end", "string"), ("mapping", "object"), ("allocations", "array"),
  330. ),
  331. "/api/system/metering/budgets": (
  332. ("schema_version", "integer"), ("budget_uid", "string"), ("window", "string"),
  333. ("mapping", "object"), ("limit_micros", "integer"), ("threshold_micros", "integer"),
  334. ),
  335. }
  336. METERING_SHOWBACK_REQUIRED_FIELDS = {
  337. "/api/system/metering/events": tuple(field for field, _ in METERING_SHOWBACK_BODY_FIELDS["/api/system/metering/events"]),
  338. "/api/system/metering/allocations": tuple(field for field, _ in METERING_SHOWBACK_BODY_FIELDS["/api/system/metering/allocations"]),
  339. "/api/system/metering/budgets": tuple(field for field, _ in METERING_SHOWBACK_BODY_FIELDS["/api/system/metering/budgets"]),
  340. }
  341. METERING_SHOWBACK_QUERY_PARAMS = {
  342. "/api/system/metering/showback": (("window", "string"),),
  343. "/api/system/metering/reconciliation": (("window", "string"),),
  344. "/api/system/metering/allocation-replay": (("window", "string"), ("rule_uid", "string"), ("rule_version", "integer")),
  345. }
  346. BI_AI_CATALOG_BODY_FIELDS = {
  347. "/api/system/bi-ai-catalog/local-fixture/sync": (),
  348. "/api/system/bi-ai-catalog/assets/search": (
  349. ("query", "string"),
  350. ("filters", "object"),
  351. ),
  352. }
  353. BI_AI_CATALOG_REQUIRED_FIELDS = {
  354. "/api/system/bi-ai-catalog/local-fixture/sync": (),
  355. "/api/system/bi-ai-catalog/assets/search": ("query", "filters"),
  356. }
  357. EDGE_MACHINE_PATHS = {
  358. "/api/datasource/edge/gateways/{gateway_id}/heartbeat",
  359. "/api/datasource/edge/gateways/{gateway_id}/tasks/pull",
  360. "/api/datasource/edge/gateways/{gateway_id}/tasks/{task_id}/outcome",
  361. "/api/datasource/edge/gateways/{gateway_id}/events",
  362. "/api/datasource/edge/gateways/{gateway_id}/reconcile",
  363. "/api/datasource/edge/gateways/{gateway_id}/releases/ack",
  364. }
  365. EDGE_CREATED_PATHS = {
  366. "/api/datasource/edge/enrollments",
  367. "/api/datasource/edge/register",
  368. "/api/datasource/edge/tasks",
  369. "/api/datasource/edge/gateways/{gateway_id}/releases",
  370. }
  371. def quoted(value: str) -> str:
  372. return json.dumps(value, ensure_ascii=False)
  373. def route_path(raw_path: str) -> tuple[str, list[dict[str, str]]]:
  374. parameters: list[dict[str, str]] = []
  375. def replace(match: re.Match[str]) -> str:
  376. converter = match.group(1) or "string"
  377. name = match.group(2)
  378. schema_type = "integer" if converter in {"int", "float"} else "string"
  379. parameters.append({"name": name, "type": schema_type})
  380. return "{" + name + "}"
  381. return re.sub(
  382. r"<(?:([a-zA-Z_]+):)?([a-zA-Z_][a-zA-Z0-9_]*)>", replace, raw_path
  383. ), parameters
  384. def extract_routes() -> list[dict[str, object]]:
  385. routes: list[dict[str, object]] = []
  386. for module, prefix in PREFIXES.items():
  387. for route_file in sorted((ROOT / "app" / "api" / module).glob("*.py")):
  388. if route_file.name == "__init__.py":
  389. continue
  390. tree = ast.parse(route_file.read_text(encoding="utf-8"))
  391. for node in tree.body:
  392. if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
  393. continue
  394. for decorator in node.decorator_list:
  395. if not isinstance(decorator, ast.Call):
  396. continue
  397. function = decorator.func
  398. if not (
  399. isinstance(function, ast.Attribute)
  400. and isinstance(function.value, ast.Name)
  401. and function.value.id == "bp"
  402. and decorator.args
  403. ):
  404. continue
  405. if function.attr == "route":
  406. methods = ["GET"]
  407. for keyword in decorator.keywords:
  408. if keyword.arg == "methods":
  409. methods = ast.literal_eval(keyword.value)
  410. elif function.attr in SHORTHAND_METHODS:
  411. methods = [function.attr.upper()]
  412. else:
  413. continue
  414. raw_path = ast.literal_eval(decorator.args[0])
  415. path, parameters = route_path(prefix + raw_path)
  416. summary = (
  417. ast.get_docstring(node) or node.name.replace("_", " ")
  418. ).splitlines()[0]
  419. for method in methods:
  420. routes.append(
  421. {
  422. "path": path,
  423. "method": method.lower(),
  424. "tag": module,
  425. "operation_id": f"{module}_{node.name}_{method.lower()}",
  426. "summary": summary,
  427. "parameters": parameters,
  428. "source": str(route_file.relative_to(ROOT)),
  429. }
  430. )
  431. return sorted(routes, key=lambda item: (str(item["path"]), str(item["method"])))
  432. def render(routes: list[dict[str, object]]) -> str:
  433. by_path: dict[str, list[dict[str, object]]] = defaultdict(list)
  434. for route in routes:
  435. by_path[str(route["path"])].append(route)
  436. lines = [
  437. "openapi: 3.1.0",
  438. "info:",
  439. ' title: "DataOps Platform API(当前代码基线)"',
  440. ' version: "2026-08-09"',
  441. ' description: "由 scripts/generate_openapi.py 从 app/api/*/routes.py 生成。请求体与响应细节仍以现有专项 API 文档和代码为准。"',
  442. f"x-route-count: {len(routes)}",
  443. "servers:",
  444. ' - url: "http://localhost:15500"',
  445. ' description: "全本地隔离测试后端"',
  446. "tags:",
  447. ]
  448. for tag in PREFIXES:
  449. lines.extend([f" - name: {tag}", f" description: {quoted(PREFIXES[tag])}"])
  450. lines.append("paths:")
  451. for path, operations in sorted(by_path.items()):
  452. lines.append(f" {quoted(path)}:")
  453. for operation in operations:
  454. lines.extend(
  455. [
  456. f" {operation['method']}:",
  457. f" tags: [{operation['tag']}]",
  458. f" operationId: {operation['operation_id']}",
  459. f" summary: {quoted(str(operation['summary']))}",
  460. f" x-source: {quoted(str(operation['source']))}",
  461. ]
  462. )
  463. if path.startswith("/api/datasource/edge/"):
  464. lines.extend(
  465. [
  466. " x-max-request-bytes: 262144",
  467. " x-max-json-depth: 32",
  468. " x-max-json-nodes: 5000",
  469. ]
  470. )
  471. parameters = operation["parameters"]
  472. if path.startswith("/api/datasource/connectors/machine/runs"):
  473. parameters = [
  474. *parameters,
  475. {
  476. "name": "X-Connector-Credential",
  477. "type": "string",
  478. "header": True,
  479. },
  480. ]
  481. if path == "/api/datasource/edge/register":
  482. parameters = [
  483. *parameters,
  484. {"name": "X-Edge-Enrollment", "type": "string", "header": True},
  485. {
  486. "name": "X-DataOps-Edge-Client-Verify",
  487. "type": "string",
  488. "header": True,
  489. },
  490. {
  491. "name": "X-DataOps-Edge-Client-Cert",
  492. "type": "string",
  493. "header": True,
  494. },
  495. {
  496. "name": "X-Edge-Certificate-SHA256",
  497. "type": "string",
  498. "header": True,
  499. },
  500. ]
  501. elif path in EDGE_MACHINE_PATHS:
  502. parameters = [
  503. *parameters,
  504. {"name": "X-Edge-Credential", "type": "string", "header": True},
  505. {
  506. "name": "X-DataOps-Edge-Client-Verify",
  507. "type": "string",
  508. "header": True,
  509. },
  510. {
  511. "name": "X-DataOps-Edge-Client-Cert",
  512. "type": "string",
  513. "header": True,
  514. },
  515. {
  516. "name": "X-Edge-Certificate-SHA256",
  517. "type": "string",
  518. "header": True,
  519. },
  520. ]
  521. if path == "/api/datasource/edge/gateways" and operation["method"] == "get":
  522. parameters = [
  523. *parameters,
  524. {"name": "limit", "type": "integer", "query": True},
  525. {"name": "offset", "type": "integer", "query": True},
  526. ]
  527. if path in METERING_SHOWBACK_QUERY_PARAMS:
  528. parameters = [
  529. *parameters,
  530. *(
  531. {"name": name, "type": schema_type, "query": True, "required": True}
  532. for name, schema_type in METERING_SHOWBACK_QUERY_PARAMS[path]
  533. ),
  534. ]
  535. if parameters:
  536. lines.append(" parameters:")
  537. for parameter in parameters:
  538. lines.extend(
  539. [
  540. f" - name: {parameter['name']}",
  541. f" in: {'header' if parameter.get('header') else 'query' if parameter.get('query') else 'path'}",
  542. f" required: {'true' if parameter.get('required') else 'false' if parameter.get('query') else 'true'}",
  543. " schema:",
  544. f" type: {parameter['type']}",
  545. ]
  546. )
  547. if parameter["name"] in {"X-Edge-Credential", "X-Edge-Enrollment"}:
  548. lines.extend(
  549. [
  550. " format: password",
  551. " writeOnly: true",
  552. ]
  553. )
  554. if parameter["name"] == "limit":
  555. lines.extend(
  556. [" minimum: 1", " maximum: 100"]
  557. )
  558. if parameter["name"] == "offset":
  559. lines.extend(
  560. [" minimum: 0", " maximum: 10000"]
  561. )
  562. if (
  563. path
  564. == "/api/datasource/connectors/credentials/{credential_uid}/{action}"
  565. and parameter["name"] == "action"
  566. ):
  567. lines.append(" enum: [revoke, rotate]")
  568. if (
  569. operation["method"] in {"post", "put", "patch"}
  570. and path not in NO_REQUEST_BODY_PATHS
  571. ):
  572. connector_schema = CONNECTOR_REQUEST_SCHEMAS.get(path)
  573. request_schema = (
  574. connector_schema
  575. or EDGE_REQUEST_SCHEMAS.get(path)
  576. or PRODUCTION_OBSERVABILITY_REQUEST_SCHEMAS.get(path)
  577. )
  578. lines.extend(
  579. [
  580. " requestBody:",
  581. " required: "
  582. + (
  583. "false"
  584. if path in OPTIONAL_REQUEST_BODY_PATHS
  585. else "true"
  586. if request_schema
  587. or path in TENANT_CONTROL_BODY_FIELDS
  588. or path in BI_AI_CATALOG_BODY_FIELDS
  589. or path in METERING_SHOWBACK_BODY_FIELDS
  590. else "false"
  591. ),
  592. " content:",
  593. " application/json:",
  594. " schema:",
  595. f" $ref: '#/components/schemas/{request_schema}'"
  596. if request_schema
  597. else " type: object",
  598. ]
  599. )
  600. if not request_schema:
  601. lines.append(
  602. " additionalProperties: false"
  603. if path in TRUSTED_DELIVERY_PERMISSIONS
  604. or path in TENANT_CONTROL_PERMISSIONS
  605. or path in BI_AI_CATALOG_PERMISSIONS
  606. or path in METERING_SHOWBACK_PERMISSIONS
  607. else " additionalProperties: true"
  608. )
  609. if path in TENANT_CONTROL_BODY_FIELDS:
  610. fields = TENANT_CONTROL_BODY_FIELDS[path]
  611. lines.append(
  612. " required: ["
  613. + ", ".join(TENANT_CONTROL_REQUIRED_FIELDS[path])
  614. + "]"
  615. )
  616. lines.append(" properties:")
  617. for field, field_type in fields:
  618. lines.extend(
  619. [
  620. f" {field}:",
  621. f" type: {field_type}",
  622. ]
  623. )
  624. if (
  625. path == "/api/system/tenant/quota-claims"
  626. and field == "amount"
  627. ):
  628. lines.append(
  629. " pattern: '^[0-9]+([.][0-9]{1,6})?$'"
  630. )
  631. if path in BI_AI_CATALOG_BODY_FIELDS:
  632. fields = BI_AI_CATALOG_BODY_FIELDS[path]
  633. lines.append(
  634. " required: ["
  635. + ", ".join(BI_AI_CATALOG_REQUIRED_FIELDS[path])
  636. + "]"
  637. )
  638. if fields:
  639. lines.append(" properties:")
  640. for field, field_type in fields:
  641. lines.extend(
  642. [
  643. f" {field}:",
  644. f" type: {field_type}",
  645. ]
  646. )
  647. if path in METERING_SHOWBACK_BODY_FIELDS:
  648. fields = METERING_SHOWBACK_BODY_FIELDS[path]
  649. lines.append(" required: [" + ", ".join(METERING_SHOWBACK_REQUIRED_FIELDS[path]) + "]")
  650. lines.append(" properties:")
  651. for field, field_type in fields:
  652. lines.extend([f" {field}:", f" type: {field_type}"])
  653. if field == "mapping":
  654. lines.extend([" additionalProperties: false", " required: [department, project, cost_center]", " properties:", " department: {type: string}", " project: {type: string}", " cost_center: {type: string}"])
  655. elif field == "evidence":
  656. lines.extend([" additionalProperties: false", " required: [digest, reference]", " properties:", " digest: {type: string}", " reference: {type: string}"])
  657. elif field == "allocations":
  658. lines.extend([" minItems: 1", " maxItems: 32", " items:", " type: object", " additionalProperties: false", " required: [target, weight_micros]", " properties:", " target: {type: string}", " weight_micros: {type: integer, minimum: 1, maximum: 1000000}"])
  659. response_fields = RESPONSE_FIELDS.get(
  660. (str(operation["tag"]), str(operation["operation_id"]).split("_")[-2])
  661. )
  662. if response_fields:
  663. lines.append(
  664. " x-response-fields: [" + ", ".join(response_fields) + "]"
  665. )
  666. connector_response = CONNECTOR_RESPONSE_SCHEMAS.get(
  667. (path, str(operation["method"]))
  668. )
  669. edge_response = EDGE_RESPONSE_SCHEMAS.get((path, str(operation["method"])))
  670. response_status = (
  671. "201"
  672. if operation["method"] == "post"
  673. and (
  674. path in EDGE_CREATED_PATHS
  675. or path
  676. in {
  677. "/api/system/bi-ai-catalog/local-fixture/sync",
  678. "/api/datasource/connectors/runs",
  679. "/api/datasource/connectors/machine/runs",
  680. "/api/datasource/connectors/principals",
  681. "/api/datasource/connectors/principals/{principal_uid}/credentials",
  682. "/api/datasource/connectors/source-bindings",
  683. "/api/datafactory/observability/operations/deliveries",
  684. "/api/system/tenant/quota-claims",
  685. "/api/system/tenant/provisions",
  686. "/api/system/tenant/approvals",
  687. "/api/system/metering/events",
  688. "/api/system/metering/allocations",
  689. "/api/system/metering/budgets",
  690. }
  691. )
  692. else "200"
  693. )
  694. if path.startswith("/api/datasource/connectors"):
  695. required_permission = (
  696. "machine-credential"
  697. if path.startswith("/api/datasource/connectors/machine/runs")
  698. else (
  699. "connectors:manage"
  700. if "/source-bindings" in path
  701. else (
  702. "connectors:read"
  703. if operation["method"] == "get"
  704. else (
  705. "connectors:manage"
  706. if "/principals" in path or "/credentials" in path
  707. else "connectors:operate"
  708. )
  709. )
  710. )
  711. )
  712. lines.append(
  713. f" x-required-permission: {quoted(required_permission)}"
  714. )
  715. elif path == "/api/datasource/graph":
  716. lines.append(' x-required-permission: "connectors:read"')
  717. elif path == "/api/datafactory/observability/operations/deliveries":
  718. lines.append(
  719. ' x-required-permission: "data-observability:operate"'
  720. )
  721. elif path in TRUSTED_DELIVERY_PERMISSIONS:
  722. lines.append(
  723. f" x-required-permission: {quoted(TRUSTED_DELIVERY_PERMISSIONS[path])}"
  724. )
  725. elif path in TENANT_CONTROL_PERMISSIONS:
  726. lines.append(
  727. f" x-required-permission: {quoted(TENANT_CONTROL_PERMISSIONS[path])}"
  728. )
  729. elif path in BI_AI_CATALOG_PERMISSIONS:
  730. lines.append(
  731. f" x-required-permission: {quoted(BI_AI_CATALOG_PERMISSIONS[path])}"
  732. )
  733. elif path in METERING_SHOWBACK_PERMISSIONS:
  734. lines.append(f" x-required-permission: {quoted(METERING_SHOWBACK_PERMISSIONS[path])}")
  735. elif path.startswith("/api/datasource/edge"):
  736. if path == "/api/datasource/edge/register":
  737. permission = "one-time-enrollment+mTLS"
  738. elif path in EDGE_MACHINE_PATHS:
  739. permission = "edge-credential+mTLS"
  740. elif (
  741. path == "/api/datasource/edge/gateways"
  742. and operation["method"] == "get"
  743. ):
  744. permission = "edge-gateways:read"
  745. elif path == "/api/datasource/edge/enrollments":
  746. permission = "edge-gateways:manage"
  747. else:
  748. permission = "edge-gateways:operate"
  749. lines.append(f" x-required-permission: {quoted(permission)}")
  750. if path == "/api/datasource/edge/register":
  751. lines.extend(
  752. [
  753. " security:",
  754. " - mutualTLS: []",
  755. " edgeEnrollment: []",
  756. ]
  757. )
  758. elif path in EDGE_MACHINE_PATHS:
  759. lines.extend(
  760. [
  761. " security:",
  762. " - mutualTLS: []",
  763. " edgeCredential: []",
  764. ]
  765. )
  766. else:
  767. lines.extend([" security:", " - bearerAuth: []"])
  768. success_headers = (
  769. [
  770. " headers:",
  771. " Cache-Control:",
  772. " required: true",
  773. " schema: {type: string, const: no-store}",
  774. ]
  775. if path.startswith("/api/datasource/edge")
  776. or path == "/api/datafactory/observability/operations/deliveries"
  777. or path in TRUSTED_DELIVERY_PERMISSIONS
  778. or path in TENANT_CONTROL_PERMISSIONS
  779. or path in BI_AI_CATALOG_PERMISSIONS
  780. or path in METERING_SHOWBACK_PERMISSIONS
  781. else []
  782. )
  783. lines.extend(
  784. [
  785. " responses:",
  786. f' "{response_status}":',
  787. ' description: "请求已由当前实现处理"',
  788. *success_headers,
  789. " content:",
  790. " application/json:",
  791. " schema:",
  792. f" $ref: '#/components/schemas/{connector_response or edge_response or PRODUCTION_OBSERVABILITY_RESPONSE_SCHEMAS.get((path, str(operation['method']))) or ('EdgeApiEnvelope' if path.startswith('/api/datasource/edge') else 'ApiEnvelope')}'",
  793. " default:",
  794. ' description: "错误响应"',
  795. *success_headers,
  796. " content:",
  797. " application/json:",
  798. " schema:",
  799. f" $ref: '#/components/schemas/{'ConnectorErrorEnvelope' if path.startswith('/api/datasource/connectors') or path == '/api/datasource/graph' else 'EdgeErrorEnvelope' if path.startswith('/api/datasource/edge') else 'ApiEnvelope'}'",
  800. ]
  801. )
  802. lines.extend(
  803. [
  804. "components:",
  805. " schemas:",
  806. " ApiEnvelope:",
  807. " type: object",
  808. " additionalProperties: true",
  809. " required: [code, message, data]",
  810. " properties:",
  811. " code:",
  812. " type: integer",
  813. " message:",
  814. " type: string",
  815. " data: {}",
  816. " error: {type: object}",
  817. " EdgeApiEnvelope:",
  818. " type: object",
  819. " additionalProperties: false",
  820. " required: [code, message, data]",
  821. " properties: {code: {type: integer}, message: {type: string}, data: {type: object}}",
  822. " ProductionDeliveryRequest:",
  823. " type: object",
  824. " additionalProperties: false",
  825. " required: [incident_uid, alert_uid, channel, summary]",
  826. " properties: {incident_uid: {type: string, format: uuid}, alert_uid: {type: string, format: uuid}, channel: {type: string, enum: [monitoring, smtp, enterprise_collaboration, on_call, itsm]}, summary: {type: string, minLength: 1, maxLength: 160}}",
  827. " ProductionDeliveryRecord:",
  828. " type: object",
  829. " additionalProperties: false",
  830. " required: [uid, incident_uid, alert_uid, channel, status, attempt_count, lease_fence]",
  831. " properties: {uid: {type: string, format: uuid}, incident_uid: {type: string, format: uuid}, alert_uid: {type: string, format: uuid}, channel: {type: string, enum: [monitoring, smtp, enterprise_collaboration, on_call, itsm]}, status: {type: string, enum: [pending, processing, delivered, dead_letter, compensated]}, attempt_count: {type: integer, minimum: 0, maximum: 3}, lease_fence: {type: integer, minimum: 0}, next_attempt_at: {type: string, format: date-time}, lease_owner: {type: [string, 'null'], maxLength: 80}, lease_expires_at: {type: [string, 'null'], format: date-time}, last_reason_code: {type: [string, 'null'], maxLength: 64}, compensation_reason_code: {type: [string, 'null'], maxLength: 64}, compensation_receipt_code: {type: [string, 'null'], maxLength: 64}, idempotency_key: {type: string, maxLength: 200}, payload_digest: {type: string, pattern: '^[a-f0-9]{64}$'}, created_at: {type: string, format: date-time}, updated_at: {type: string, format: date-time}}",
  832. " ProductionDeliveryEnvelope:",
  833. " type: object",
  834. " additionalProperties: false",
  835. " required: [code, message, data]",
  836. " properties: {code: {const: 200}, message: {type: string}, data: {$ref: '#/components/schemas/ProductionDeliveryRecord'}}",
  837. " EdgeErrorEnvelope:",
  838. " type: object",
  839. " additionalProperties: false",
  840. " required: [code, message, data, error]",
  841. " properties: {code: {type: integer}, message: {type: string}, data: {type: 'null'}, error: {type: object, additionalProperties: false, required: [code], properties: {code: {type: string, maxLength: 80}}}}",
  842. " EdgeMachineBinding:",
  843. " type: object",
  844. " additionalProperties: false",
  845. " required: [environment, network_zone, generation]",
  846. " properties: {environment: {type: string, enum: [development, staging, production]}, network_zone: {type: string, minLength: 1, maxLength: 120}, generation: {type: integer, minimum: 1}}",
  847. " EdgeEnrollmentRequest:",
  848. " type: object",
  849. " additionalProperties: false",
  850. " required: [gateway_name, environment, network_zone, policy_digest, allowed_control_hosts, allowed_proxy_hosts, expected_certificate_sha256]",
  851. " properties: {gateway_name: {type: string, minLength: 1, maxLength: 200}, environment: {type: string, enum: [development, staging, production]}, network_zone: {type: string, minLength: 1, maxLength: 120}, policy_digest: {$ref: '#/components/schemas/Sha256Digest'}, allowed_control_hosts: {$ref: '#/components/schemas/EdgeExactHosts'}, allowed_proxy_hosts: {$ref: '#/components/schemas/EdgeExactHosts'}, expected_certificate_sha256: {$ref: '#/components/schemas/Sha256Digest'}, ttl_seconds: {type: integer, minimum: 60, maximum: 86400}}",
  852. " EdgeRegisterRequest:",
  853. " type: object",
  854. " additionalProperties: false",
  855. " required: [gateway_id, environment, network_zone, policy_digest, allowed_control_hosts, allowed_proxy_hosts, version]",
  856. " properties: {gateway_id: {type: string, format: uuid}, environment: {type: string, enum: [development, staging, production]}, network_zone: {type: string, minLength: 1, maxLength: 120}, policy_digest: {$ref: '#/components/schemas/Sha256Digest'}, allowed_control_hosts: {$ref: '#/components/schemas/EdgeExactHosts'}, allowed_proxy_hosts: {$ref: '#/components/schemas/EdgeExactHosts'}, version: {$ref: '#/components/schemas/EdgeAgentVersion'}}",
  857. " EdgeHeartbeatRequest:",
  858. " type: object",
  859. " additionalProperties: false",
  860. " required: [environment, network_zone, generation, version, safe_summary]",
  861. " properties: {environment: {type: string, enum: [development, staging, production]}, network_zone: {type: string, minLength: 1, maxLength: 120}, generation: {type: integer, minimum: 1}, version: {$ref: '#/components/schemas/EdgeAgentVersion'}, safe_summary: {$ref: '#/components/schemas/EdgeSafeSummary'}}",
  862. " EdgeRotateRequest:",
  863. " type: object",
  864. " additionalProperties: false",
  865. " required: [certificate_sha256, request_id]",
  866. " properties: {certificate_sha256: {$ref: '#/components/schemas/Sha256Digest'}, request_id: {type: string, minLength: 1, maxLength: 255}}",
  867. " Sha256Digest: {type: string, pattern: '^[a-f0-9]{64}$'}",
  868. " Ed25519Signature: {type: string, pattern: '^[a-f0-9]{128}$'}",
  869. " EdgeExactHosts: {type: array, maxItems: 32, uniqueItems: true, items: {type: string, pattern: '^[a-z0-9][a-z0-9.-]*[a-z0-9]$'}}",
  870. *_edge_safe_schema_lines(),
  871. " EdgeIdentifier: {type: string, minLength: 1, maxLength: 255, pattern: '^[A-Za-z0-9][A-Za-z0-9._:-]{0,254}$'}",
  872. " EdgeCursor: {type: [string, 'null'], minLength: 1, maxLength: 255, pattern: '^[A-Za-z0-9][A-Za-z0-9._:-]{0,254}$'}",
  873. " EdgeAgentVersion: {type: string, minLength: 1, maxLength: 80, pattern: '^[A-Za-z0-9][A-Za-z0-9._+-]{0,79}$'}",
  874. " EdgeVersion: {type: string, pattern: '^(0|[1-9][0-9]{0,9})\\.(0|[1-9][0-9]{0,9})\\.(0|[1-9][0-9]{0,9})$'}",
  875. " EdgeTaskContract:",
  876. " type: object",
  877. " additionalProperties: false",
  878. " required: [task_id, gateway_id, environment, network_zone, purpose, classification, task_type, contract_version, deadline_at, attempt, idempotency_key, policy_digest]",
  879. " properties: {task_id: {$ref: '#/components/schemas/EdgeIdentifier'}, gateway_id: {type: string, format: uuid}, environment: {type: string, enum: [development, staging, production]}, network_zone: {type: string, minLength: 1, maxLength: 120}, purpose: {type: string, minLength: 1, maxLength: 255}, classification: {type: string, enum: [raw, recent_detail, restricted, desensitized_metadata, statistics, lineage, evidence]}, task_type: {type: string, enum: [collect, profile, quality, lineage, controlled_query]}, contract_version: {const: 1}, deadline_at: {type: string, format: date-time}, attempt: {type: integer, minimum: 1, maximum: 5}, idempotency_key: {$ref: '#/components/schemas/EdgeIdentifier'}, policy_digest: {$ref: '#/components/schemas/Sha256Digest'}}",
  880. " EdgeSignedTaskEnvelope:",
  881. " type: object",
  882. " additionalProperties: false",
  883. " required: [task, authority_key_id, signature_algorithm, contract_digest, gateway_id, environment, network_zone, policy_digest, purpose, issued_at, expires_at, signature]",
  884. " properties: {task: {$ref: '#/components/schemas/EdgeTaskContract'}, authority_key_id: {type: string}, signature_algorithm: {const: Ed25519}, contract_digest: {$ref: '#/components/schemas/Sha256Digest'}, gateway_id: {type: string}, environment: {type: string}, network_zone: {type: string}, policy_digest: {$ref: '#/components/schemas/Sha256Digest'}, purpose: {type: string}, issued_at: {type: string, format: date-time}, expires_at: {type: string, format: date-time}, signature: {$ref: '#/components/schemas/Ed25519Signature'}}",
  885. " EdgeEventContract:",
  886. " type: object",
  887. " additionalProperties: false",
  888. " required: [event_id, task_id, gateway_id, environment, network_zone, purpose, classification, contract_version, occurred_at, attempt, idempotency_key, policy_digest, payload]",
  889. " properties: {event_id: {type: string}, task_id: {type: string}, gateway_id: {type: string}, environment: {type: string}, network_zone: {type: string}, purpose: {type: string}, classification: {type: string, enum: [desensitized_metadata, statistics, lineage, evidence, health_summary, diagnostic_summary]}, contract_version: {const: 1}, occurred_at: {type: string, format: date-time}, attempt: {type: integer, minimum: 1, maximum: 5}, idempotency_key: {type: string}, policy_digest: {$ref: '#/components/schemas/Sha256Digest'}, payload: {$ref: '#/components/schemas/EdgeSafeSummary'}}",
  890. " EdgeEventRequest:",
  891. " type: object",
  892. " additionalProperties: false",
  893. " required: [environment, network_zone, generation, event, lease_token]",
  894. " properties: {environment: {type: string}, network_zone: {type: string}, generation: {type: integer, minimum: 1}, event: {$ref: '#/components/schemas/EdgeEventContract'}, lease_token: {type: string, writeOnly: true}}",
  895. " EdgeTaskOutcomeRequest:",
  896. " type: object",
  897. " additionalProperties: false",
  898. " required: [environment, network_zone, generation, outcome, lease_token, safe_summary]",
  899. " properties: {environment: {type: string}, network_zone: {type: string}, generation: {type: integer, minimum: 1}, outcome: {type: string, enum: [completed, failed, cancelled]}, lease_token: {type: string, writeOnly: true}, safe_summary: {$ref: '#/components/schemas/EdgeSafeSummary'}}",
  900. " EdgeReconcileRequest:",
  901. " type: object",
  902. " additionalProperties: false",
  903. " required: [environment, network_zone, generation]",
  904. " properties: {environment: {type: string, enum: [development, staging, production]}, network_zone: {type: string, minLength: 1, maxLength: 120}, generation: {type: integer, minimum: 1}, limit: {type: integer, minimum: 1, maximum: 100}, cancel_cursor: {$ref: '#/components/schemas/EdgeCursor'}, release_cursor: {$ref: '#/components/schemas/EdgeCursor'}}",
  905. " EdgeReconcileResponse:",
  906. " type: object",
  907. " additionalProperties: false",
  908. " required: [code, message, data]",
  909. " properties: {code: {type: integer}, message: {type: string, maxLength: 200}, data: {type: object, additionalProperties: false, required: [cancelled_task_ids, cancel_next_cursor, release_offers, release_next_cursor, release_baseline], properties: {cancelled_task_ids: {type: array, maxItems: 50, items: {$ref: '#/components/schemas/EdgeIdentifier'}}, cancel_next_cursor: {$ref: '#/components/schemas/EdgeCursor'}, release_offers: {type: array, maxItems: 50, items: {$ref: '#/components/schemas/EdgeReleaseManifest'}}, release_next_cursor: {$ref: '#/components/schemas/EdgeCursor'}, release_baseline: {oneOf: [{$ref: '#/components/schemas/EdgeReleaseManifest'}, {type: 'null'}]}}}}",
  910. " EdgeReleaseOfferRequest:",
  911. " type: object",
  912. " additionalProperties: false",
  913. " required: [version, artifact_digest, artifact_name, deadline_at, rollback_version, request_id]",
  914. " properties: {version: {$ref: '#/components/schemas/EdgeVersion'}, artifact_digest: {$ref: '#/components/schemas/Sha256Digest'}, artifact_name: {type: string, minLength: 1, maxLength: 255, pattern: '^[^/\\\\]+$'}, deadline_at: {type: string, format: date-time}, rollback_version: {$ref: '#/components/schemas/EdgeVersion'}, request_id: {$ref: '#/components/schemas/EdgeIdentifier'}}",
  915. " EdgeReleaseAckRequest:",
  916. " type: object",
  917. " additionalProperties: false",
  918. " required: [environment, network_zone, generation, release_id, outcome, safe_summary]",
  919. " properties: {environment: {type: string}, network_zone: {type: string}, generation: {type: integer, minimum: 1}, release_id: {type: string, format: uuid}, outcome: {type: string, enum: [accepted, installed, failed, rollback]}, safe_summary: {$ref: '#/components/schemas/EdgeSafeSummary'}}",
  920. " EdgeReleaseManifest:",
  921. " type: object",
  922. " additionalProperties: false",
  923. " required: [release_id, version, rollback_version, artifact_digest, artifact_name, deadline_at, status, signature_algorithm, key_id, manifest_digest, signature]",
  924. " properties: {release_id: {$ref: '#/components/schemas/EdgeIdentifier'}, version: {$ref: '#/components/schemas/EdgeVersion'}, rollback_version: {$ref: '#/components/schemas/EdgeVersion'}, artifact_digest: {$ref: '#/components/schemas/Sha256Digest'}, artifact_name: {type: string, minLength: 1, maxLength: 255, pattern: '^[^/\\\\]+$'}, deadline_at: {type: string, format: date-time}, status: {type: string, enum: [offered, accepted, failed, installed, rolled_back]}, signature_algorithm: {const: Ed25519}, key_id: {$ref: '#/components/schemas/EdgeIdentifier'}, manifest_digest: {$ref: '#/components/schemas/Sha256Digest'}, signature: {$ref: '#/components/schemas/Ed25519Signature'}}",
  925. " EdgeEnrollmentEnvelope:",
  926. " type: object",
  927. " additionalProperties: false",
  928. " required: [code, message, data]",
  929. " properties: {code: {type: integer}, message: {type: string}, data: {type: object, additionalProperties: false, required: [enrollment_id, gateway_id, enrollment_token, expires_in, returned_once], properties: {enrollment_id: {type: string, format: uuid}, gateway_id: {type: string, format: uuid}, enrollment_token: {type: string, writeOnly: true}, expires_in: {type: integer}, returned_once: {const: true}}}}",
  930. " EdgeRegistrationEnvelope:",
  931. " type: object",
  932. " additionalProperties: false",
  933. " required: [code, message, data]",
  934. " properties: {code: {type: integer}, message: {type: string}, data: {type: object, additionalProperties: false, required: [gateway_id, generation, certificate_sha256, credential, credential_returned_once], properties: {gateway_id: {type: string, format: uuid}, generation: {type: integer}, certificate_sha256: {$ref: '#/components/schemas/Sha256Digest'}, credential: {type: string, writeOnly: true}, credential_returned_once: {const: true}}}}",
  935. " EdgeGatewayListEnvelope:",
  936. " type: object",
  937. " additionalProperties: false",
  938. " required: [code, message, data]",
  939. " properties: {code: {type: integer}, message: {type: string}, data: {type: object, additionalProperties: false, required: [gateways], properties: {gateways: {type: array, maxItems: 100, items: {type: object, description: 'Bounded gateway metadata; credential/token/private key are never included.'}}}}}",
  940. " EdgeTaskPullEnvelope:",
  941. " type: object",
  942. " additionalProperties: false",
  943. " required: [code, message, data]",
  944. " properties: {code: {type: integer}, message: {type: string}, data: {oneOf: [{type: object, additionalProperties: false, required: [task], properties: {task: {type: 'null'}}}, {type: object, additionalProperties: false, required: [task, signed_task_envelope, lease_token, lease_expires_at], properties: {task: {$ref: '#/components/schemas/EdgeTaskContract'}, signed_task_envelope: {$ref: '#/components/schemas/EdgeSignedTaskEnvelope'}, lease_token: {type: string, writeOnly: true}, lease_expires_at: {type: string, format: date-time}}}]}}",
  945. " EdgeEventAckEnvelope:",
  946. " type: object",
  947. " additionalProperties: false",
  948. " required: [code, message, data]",
  949. " properties: {code: {type: integer}, message: {type: string}, data: {type: object, additionalProperties: false, required: [event_id, event_digest, remote_lease_digest, received_at, status], properties: {event_id: {type: string}, event_digest: {$ref: '#/components/schemas/Sha256Digest'}, remote_lease_digest: {$ref: '#/components/schemas/Sha256Digest'}, received_at: {type: string, format: date-time}, status: {const: accepted}}}}",
  950. " EdgeReleaseEnvelope:",
  951. " type: object",
  952. " additionalProperties: false",
  953. " required: [code, message, data]",
  954. " properties: {code: {type: integer}, message: {type: string, maxLength: 200}, data: {type: object, additionalProperties: false, required: [release_id, status, version, artifact_digest, rollback_version, manifest], properties: {release_id: {type: string, format: uuid}, status: {type: string, enum: [offered, accepted, failed, installed, rolled_back]}, version: {$ref: '#/components/schemas/EdgeVersion'}, artifact_digest: {$ref: '#/components/schemas/Sha256Digest'}, rollback_version: {$ref: '#/components/schemas/EdgeVersion'}, manifest: {$ref: '#/components/schemas/EdgeReleaseManifest'}}}}",
  955. " ConnectorConfigValidationRequest:",
  956. " type: object",
  957. " additionalProperties: false",
  958. " required: [connector_id, version, config]",
  959. " properties:",
  960. " connector_id: {type: string, minLength: 3, maxLength: 64}",
  961. " version: {type: string, pattern: '^[0-9]+\\.[0-9]+\\.[0-9]+'}",
  962. " config: {type: object}",
  963. " ConnectorRunRequest:",
  964. " type: object",
  965. " additionalProperties: false",
  966. " required: [connector_id, version, source_uid, operation, config, scope, dry_run]",
  967. " properties:",
  968. " connector_id: {type: string}",
  969. " version: {type: string}",
  970. " source_uid: {type: string, format: uuid}",
  971. " operation: {type: string, enum: [discover, snapshot, incremental, lineage, profile, evidence]}",
  972. " config: {type: object}",
  973. " scope: {$ref: '#/components/schemas/ConnectorScope'}",
  974. " cursor: {type: object}",
  975. " checkpoint: {type: object}",
  976. " idempotency_key: {type: string, pattern: '^[a-f0-9]{64}$'}",
  977. " dry_run: {const: true}",
  978. " process_key: {type: string, minLength: 1, maxLength: 300}",
  979. " MachineConnectorRunRequest:",
  980. " type: object",
  981. " additionalProperties: false",
  982. " required: [connector_id, version, source_uid, business_domain_uid, environment, process_key, operation, scope]",
  983. " properties:",
  984. " connector_id: {type: string}",
  985. " version: {type: string}",
  986. " source_uid: {type: string, format: uuid}",
  987. " business_domain_uid: {type: string, format: uuid}",
  988. " environment: {type: string, enum: [development, staging, production]}",
  989. " process_key: {type: string, minLength: 1, maxLength: 300}",
  990. " operation: {type: string, enum: [discover, snapshot, incremental, lineage, profile, evidence]}",
  991. " scope: {$ref: '#/components/schemas/ConnectorScope'}",
  992. " cursor: {type: object}",
  993. " checkpoint: {type: object}",
  994. " idempotency_key: {type: string, pattern: '^[a-f0-9]{64}$'}",
  995. " dry_run: {type: boolean, default: false}",
  996. " ConnectorPrincipalRequest:",
  997. " type: object",
  998. " additionalProperties: false",
  999. " required: [connector_id, version, source_uid, business_domain_uid, environment, operations, scopes]",
  1000. " properties:",
  1001. " connector_id: {type: string}",
  1002. " version: {type: string}",
  1003. " source_uid: {type: string, format: uuid}",
  1004. " business_domain_uid: {type: string, format: uuid}",
  1005. " environment: {type: string, enum: [development, staging, production]}",
  1006. " operations: {type: array, minItems: 1, uniqueItems: true, items: {type: string}}",
  1007. " scopes: {$ref: '#/components/schemas/ConnectorScope'}",
  1008. " source_binding_uid: {type: string, format: uuid}",
  1009. " source_binding_version: {type: integer, minimum: 1}",
  1010. " ConnectorSourceBindingRequest:",
  1011. " type: object",
  1012. " additionalProperties: false",
  1013. " required: [connector_id, version, source_uid, business_domain_uid, environment, approved_config]",
  1014. " properties:",
  1015. " binding_uid: {type: string, format: uuid}",
  1016. " connector_id: {type: string}",
  1017. " version: {type: string}",
  1018. " source_uid: {type: string, format: uuid}",
  1019. " business_domain_uid: {type: string, format: uuid}",
  1020. " environment: {type: string, enum: [development, staging, production]}",
  1021. " approved_config: {type: object}",
  1022. " ConnectorScope:",
  1023. " type: object",
  1024. " additionalProperties: false",
  1025. " properties:",
  1026. " include_schemas: {type: array, uniqueItems: true, items: {type: string, minLength: 1}}",
  1027. " exclude_schemas: {type: array, uniqueItems: true, items: {type: string, minLength: 1}}",
  1028. " include_tables: {type: array, uniqueItems: true, items: {type: string, minLength: 1}}",
  1029. " exclude_tables: {type: array, uniqueItems: true, items: {type: string, minLength: 1}}",
  1030. " ConnectorHealthRequest:",
  1031. " type: object",
  1032. " additionalProperties: false",
  1033. " required: [config]",
  1034. " properties: {config: {type: object}}",
  1035. " ConnectorCredentialRequest:",
  1036. " type: object",
  1037. " additionalProperties: false",
  1038. " properties: {ttl_seconds: {type: integer, minimum: 60, maximum: 900}}",
  1039. " ConnectorGraphRequest:",
  1040. " type: object",
  1041. " additionalProperties: false",
  1042. " 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}}",
  1043. " ConnectorManifestEnvelope:",
  1044. " type: object",
  1045. " additionalProperties: false",
  1046. " required: [code, message, data]",
  1047. " properties:",
  1048. " code: {type: integer}",
  1049. " message: {type: string}",
  1050. " data: {type: object, additionalProperties: false, required: [manifests], properties: {manifests: {type: array, items: {$ref: '#/components/schemas/ConnectorManifest'}}}}",
  1051. " ConnectorManifest:",
  1052. " type: object",
  1053. " additionalProperties: false",
  1054. " required: [connector_id, version, sdk_version, display_name, capabilities, config_schema]",
  1055. " 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}}",
  1056. " ConnectorOperationResult:",
  1057. " type: object",
  1058. " additionalProperties: false",
  1059. " required: [records, cursor, checkpoint, evidence, status]",
  1060. " properties: {records: {type: array, items: {type: object}}, cursor: {type: object}, checkpoint: {type: object}, evidence: {type: object}, status: {type: string, enum: [succeeded, dry_run]}}",
  1061. " ConnectorRunResultEnvelope:",
  1062. " type: object",
  1063. " additionalProperties: false",
  1064. " required: [code, message, data]",
  1065. " properties: {code: {type: integer}, message: {type: string}, data: {$ref: '#/components/schemas/ConnectorOperationResult'}}",
  1066. " ConnectorRunRecord:",
  1067. " type: object",
  1068. " additionalProperties: false",
  1069. " required: [uid, idempotency_key, connector_id, connector_version, source_uid, operation, status, attempt_count, checkpoint_summary, cursor_summary, dry_run]",
  1070. " 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}}",
  1071. " ConnectorRunRecordEnvelope:",
  1072. " type: object",
  1073. " additionalProperties: false",
  1074. " required: [code, message, data]",
  1075. " properties: {code: {type: integer}, message: {type: string}, data: {$ref: '#/components/schemas/ConnectorRunRecord'}}",
  1076. " ConnectorRunListEnvelope:",
  1077. " type: object",
  1078. " additionalProperties: false",
  1079. " required: [code, message, data]",
  1080. " properties: {code: {type: integer}, message: {type: string}, data: {type: object, additionalProperties: false, required: [runs], properties: {runs: {type: array, items: {$ref: '#/components/schemas/ConnectorRunRecord'}}}}}",
  1081. " ConnectorValidationEnvelope:",
  1082. " type: object",
  1083. " additionalProperties: false",
  1084. " required: [code, message, data]",
  1085. " 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}}",
  1086. " ConnectorErrorEnvelope:",
  1087. " type: object",
  1088. " additionalProperties: false",
  1089. " required: [code, message, data, error]",
  1090. " properties:",
  1091. " code: {type: integer}",
  1092. " message: {type: string}",
  1093. " data: {type: 'null'}",
  1094. " 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}}}",
  1095. " ConnectorHealthEnvelope:",
  1096. " type: object",
  1097. " additionalProperties: false",
  1098. " required: [code, message, data]",
  1099. " properties: {code: {type: integer}, message: {type: string}, data: {type: object, additionalProperties: false, required: [status, detail], properties: {status: {type: string}, detail: {type: string}}}}",
  1100. " ConnectorCompatibilityEnvelope:",
  1101. " type: object",
  1102. " additionalProperties: false",
  1103. " required: [code, message, data]",
  1104. " 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}}}}",
  1105. " ConnectorPrincipalEnvelope:",
  1106. " type: object",
  1107. " additionalProperties: false",
  1108. " required: [code, message, data]",
  1109. " properties: {code: {type: integer}, message: {type: string}, data: {type: object, additionalProperties: false, required: [principal_uid], properties: {principal_uid: {type: string, format: uuid}}}}",
  1110. " ConnectorSourceBindingEnvelope:",
  1111. " type: object",
  1112. " additionalProperties: false",
  1113. " required: [code, message, data]",
  1114. " 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}}}}",
  1115. " ConnectorCredentialEnvelope:",
  1116. " type: object",
  1117. " additionalProperties: false",
  1118. " required: [code, message, data]",
  1119. " 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}}}]}}",
  1120. " ConnectorGraphEnvelope:",
  1121. " type: object",
  1122. " additionalProperties: false",
  1123. " required: [code, message, data]",
  1124. " 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}}}}}}",
  1125. " securitySchemes:",
  1126. " mutualTLS:",
  1127. " type: mutualTLS",
  1128. " edgeCredential:",
  1129. " type: apiKey",
  1130. " in: header",
  1131. " name: X-Edge-Credential",
  1132. ' description: "One-time-returned edge credential; always combined with mutualTLS certificate proof."',
  1133. " edgeEnrollment:",
  1134. " type: apiKey",
  1135. " in: header",
  1136. " name: X-Edge-Enrollment",
  1137. ' description: "Single-use enrollment token; always combined with mutualTLS certificate proof."',
  1138. " bearerAuth:",
  1139. " type: http",
  1140. " scheme: bearer",
  1141. " bearerFormat: JWT",
  1142. ' description: "下一阶段统一认证方案;当前路由尚未全部接入。"',
  1143. ]
  1144. )
  1145. return "\n".join(lines) + "\n"
  1146. def main(output: Path = OUTPUT) -> None:
  1147. routes = extract_routes()
  1148. if not routes:
  1149. raise SystemExit("No Flask routes found")
  1150. output.parent.mkdir(parents=True, exist_ok=True)
  1151. output.write_text(render(routes), encoding="utf-8")
  1152. try:
  1153. label = output.relative_to(ROOT)
  1154. except ValueError:
  1155. label = output
  1156. print(f"Generated {label} with {len(routes)} operations")
  1157. if __name__ == "__main__":
  1158. parser = argparse.ArgumentParser(description=__doc__)
  1159. parser.add_argument("--output", type=Path, default=OUTPUT)
  1160. args = parser.parse_args()
  1161. main(args.output)