routes.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753
  1. """Governed control-plane endpoints for AI-authored data rules."""
  2. from __future__ import annotations
  3. import hashlib
  4. import json
  5. from datetime import UTC, datetime, timedelta
  6. from typing import Any
  7. from flask import current_app, g, jsonify, request
  8. from app import db
  9. from app.api.data_rules import bp
  10. from app.core.data_rules.authoring import (
  11. OpenAICompatibleRuleModel,
  12. RuleAuthoringAgent,
  13. )
  14. from app.core.data_rules.contracts import (
  15. dataflow_spec_hash,
  16. rule_spec_hash,
  17. standard_spec_hash,
  18. validate_dataflow_spec,
  19. validate_rule_spec,
  20. validate_standard_spec,
  21. )
  22. from app.core.data_rules.production_line import resolve_production_line
  23. from app.core.data_rules.publication import (
  24. GenerationReceiptSigner,
  25. LogicalRuleCompiler,
  26. PhysicalPlanPublicationService,
  27. RulePublicationService,
  28. RuleValidationRejected,
  29. ServerOwnedLogicalDryRunRunner,
  30. ServerOwnedPhysicalPreflightRunner,
  31. generation_receipt_claims,
  32. )
  33. from app.core.data_rules.release import ProductionLineReleaseService
  34. from app.core.data_rules.repository import DataRuleRepository
  35. from app.core.data_rules.schema_resolver import (
  36. Neo4jSchemaMetadataCatalog,
  37. SchemaResolver,
  38. )
  39. from app.models.result import failed, success
  40. _VALIDATORS = {
  41. "rule": (validate_rule_spec, rule_spec_hash),
  42. "standard": (validate_standard_spec, standard_spec_hash),
  43. "dataflow": (validate_dataflow_spec, dataflow_spec_hash),
  44. }
  45. def _body() -> dict[str, Any]:
  46. value = request.get_json(silent=True)
  47. if not isinstance(value, dict):
  48. raise ValueError("request body must be an object")
  49. return value
  50. def _bad_request(message: str = "规则请求无效"):
  51. return jsonify(failed(message, code=400)), 400
  52. def _closed_body(allowed: set[str]) -> dict[str, Any]:
  53. body = _body()
  54. if set(body) - allowed:
  55. raise ValueError("request contains unsupported fields")
  56. return body
  57. def _repository() -> DataRuleRepository:
  58. configured = current_app.extensions.get("data_rule_repository")
  59. if configured is not None:
  60. return configured
  61. return DataRuleRepository(db.session)
  62. def _release_service() -> ProductionLineReleaseService:
  63. configured = current_app.extensions.get("production_line_release_service")
  64. if configured is not None:
  65. return configured
  66. repository = _repository()
  67. resolver = current_app.extensions.get("data_rule_schema_resolver")
  68. if resolver is None:
  69. catalog = current_app.extensions.get("data_rule_metadata_catalog")
  70. resolver = SchemaResolver(
  71. catalog or Neo4jSchemaMetadataCatalog(), repository
  72. )
  73. return ProductionLineReleaseService(repository, schema_resolver=resolver)
  74. def _schema_resolver() -> SchemaResolver:
  75. configured = current_app.extensions.get("data_rule_schema_resolver")
  76. if configured is not None:
  77. return configured
  78. repository = _repository()
  79. catalog = current_app.extensions.get("data_rule_metadata_catalog")
  80. return SchemaResolver(catalog or Neo4jSchemaMetadataCatalog(), repository)
  81. def _receipt_signer() -> GenerationReceiptSigner:
  82. configured = current_app.extensions.get("generation_receipt_signer")
  83. if configured is not None:
  84. return configured
  85. secret = current_app.config.get("RULE_GENERATION_RECEIPT_SECRET")
  86. if not isinstance(secret, str) or not secret.strip():
  87. raise RuntimeError(
  88. "dedicated generation receipt secret is not configured"
  89. )
  90. signer = GenerationReceiptSigner(secret)
  91. current_app.extensions["generation_receipt_signer"] = signer
  92. return signer
  93. def _rule_artifact_store():
  94. configured = current_app.extensions.get("rule_artifact_store")
  95. if configured is not None:
  96. return configured
  97. from minio import Minio
  98. from app.runner.artifacts import ArtifactStore
  99. store = ArtifactStore(
  100. Minio(
  101. current_app.config["MINIO_HOST"],
  102. access_key=current_app.config["MINIO_USER"],
  103. secret_key=current_app.config["MINIO_PASSWORD"],
  104. secure=bool(current_app.config["MINIO_SECURE"]),
  105. ),
  106. bucket=current_app.config["MINIO_BUCKET"],
  107. max_artifact_bytes=32 * 1024 * 1024,
  108. max_rows=100_000,
  109. memory_limit_bytes=256 * 1024 * 1024,
  110. max_ttl_seconds=3600,
  111. )
  112. current_app.extensions["rule_artifact_store"] = store
  113. return store
  114. def _publication_service() -> RulePublicationService:
  115. configured = current_app.extensions.get("rule_publication_service")
  116. if configured is not None:
  117. return configured
  118. test_runner = current_app.extensions.get("rule_validation_test_runner")
  119. if test_runner is None:
  120. try:
  121. test_runner = ServerOwnedLogicalDryRunRunner(
  122. _rule_artifact_store()
  123. )
  124. except Exception as exc:
  125. raise RuntimeError(
  126. "trusted rule validation test runner is not configured"
  127. ) from exc
  128. current_app.extensions["rule_validation_test_runner"] = test_runner
  129. service = RulePublicationService(
  130. _repository(),
  131. receipt_signer=_receipt_signer(),
  132. compiler=LogicalRuleCompiler(),
  133. test_runner=test_runner,
  134. )
  135. current_app.extensions["rule_publication_service"] = service
  136. return service
  137. def _physical_publication_service() -> PhysicalPlanPublicationService:
  138. configured = current_app.extensions.get(
  139. "physical_plan_publication_service"
  140. )
  141. if configured is not None:
  142. return configured
  143. test_runner = current_app.extensions.get("rule_physical_test_runner")
  144. if test_runner is None:
  145. try:
  146. from app.core.data_source.runtime import get_data_source_manager
  147. test_runner = ServerOwnedPhysicalPreflightRunner(
  148. _rule_artifact_store(),
  149. datasource_manager=get_data_source_manager(),
  150. )
  151. except Exception as exc:
  152. raise RuntimeError(
  153. "trusted physical plan test runner is not configured"
  154. ) from exc
  155. current_app.extensions["rule_physical_test_runner"] = test_runner
  156. service = PhysicalPlanPublicationService(
  157. _repository(), test_runner=test_runner
  158. )
  159. current_app.extensions["physical_plan_publication_service"] = service
  160. return service
  161. def _metadata_hash(value: Any) -> str:
  162. return hashlib.sha256(
  163. json.dumps(
  164. value,
  165. sort_keys=True,
  166. separators=(",", ":"),
  167. ensure_ascii=False,
  168. ).encode("utf-8")
  169. ).hexdigest()
  170. @bp.get("/capabilities")
  171. def capabilities():
  172. return jsonify(
  173. success(
  174. {
  175. "natural_language_authoring": True,
  176. "schema_constrained_candidates": True,
  177. "production_line_preview": True,
  178. "immutable_asset_versions": True,
  179. "server_side_publishing": True,
  180. "production_line_release": True,
  181. "data_factory_activation": False,
  182. }
  183. )
  184. )
  185. @bp.post("/production-lines/draft-identity")
  186. def create_production_line_draft_identity():
  187. """Persist a short-lived, single-use, actor-bound DataFlow reservation."""
  188. try:
  189. _closed_body(set())
  190. result = _repository().reserve_dataflow_draft(
  191. actor_uid=g.current_user["id"]
  192. )
  193. db.session.commit()
  194. return (
  195. jsonify(
  196. success(
  197. result,
  198. "生产线草稿身份已创建",
  199. code=201,
  200. )
  201. ),
  202. 201,
  203. )
  204. except (TypeError, ValueError):
  205. db.session.rollback()
  206. return _bad_request("生产线草稿身份请求无效")
  207. except Exception:
  208. db.session.rollback()
  209. current_app.logger.exception("create DataFlow draft reservation failed")
  210. return jsonify(failed("生产线草稿身份暂时不可用", code=503)), 503
  211. @bp.post("/validate")
  212. def validate_asset():
  213. try:
  214. body = _closed_body({"asset_type", "spec"})
  215. asset_type = body.get("asset_type")
  216. if asset_type not in _VALIDATORS:
  217. raise ValueError("unsupported asset type")
  218. validator, hasher = _VALIDATORS[asset_type]
  219. normalized = validator(body.get("spec"))
  220. return jsonify(
  221. success(
  222. {
  223. "asset_type": asset_type,
  224. "normalized": normalized,
  225. "spec_hash": hasher(normalized),
  226. }
  227. )
  228. )
  229. except (TypeError, ValueError):
  230. return _bad_request("规则定义无效")
  231. def _authoring_agent() -> RuleAuthoringAgent:
  232. configured = current_app.extensions.get("data_rule_authoring_agent")
  233. if configured is not None:
  234. return configured
  235. api_key = current_app.config.get("LLM_API_KEY") or current_app.config.get(
  236. "DEEPSEEK_API_KEY"
  237. )
  238. if not api_key:
  239. raise RuntimeError("rule authoring model is not configured")
  240. agent = RuleAuthoringAgent(model=OpenAICompatibleRuleModel())
  241. current_app.extensions["data_rule_authoring_agent"] = agent
  242. return agent
  243. @bp.post("/interpret")
  244. def interpret_rule():
  245. try:
  246. body = _closed_body({"source_text", "authoring_surface", "context"})
  247. receipt_signer = _receipt_signer()
  248. repository = _repository()
  249. validation_context = repository.resolve_validation_context(
  250. body.get("context", {})
  251. )
  252. result = _authoring_agent().interpret(
  253. source_text=body.get("source_text"),
  254. authoring_surface=body.get("authoring_surface"),
  255. context=validation_context,
  256. )
  257. candidate = result.get("candidate")
  258. if (
  259. body.get("authoring_surface") == "data_standard"
  260. and (
  261. not isinstance(candidate, dict)
  262. or candidate.get("candidate_type") != "rule"
  263. or not isinstance(candidate.get("rule_spec"), dict)
  264. )
  265. ):
  266. raise ValueError(
  267. "data_standard authoring must produce a rule candidate"
  268. )
  269. audit = repository.record_generation_run(
  270. evidence=result,
  271. created_by=g.current_user["id"],
  272. validation_context=validation_context,
  273. )
  274. result = {
  275. **result,
  276. "generation_run_id": audit["id"],
  277. "correlation_id": audit["correlation_id"],
  278. }
  279. if (
  280. result.get("status") == "ready"
  281. and isinstance(candidate, dict)
  282. and candidate.get("candidate_type") == "rule"
  283. and isinstance(candidate.get("rule_spec"), dict)
  284. ):
  285. claims = generation_receipt_claims(
  286. generation_run_id=audit["id"],
  287. actor_uid=g.current_user["id"],
  288. source_text=result["source_text"],
  289. candidate_hash=result["candidate_hash"],
  290. rule_spec=candidate["rule_spec"],
  291. model_hash=result.get("model_hash")
  292. or _metadata_hash(
  293. {
  294. "provider": result.get("model_provider", "unknown"),
  295. "name": result.get("model_name", "unknown"),
  296. }
  297. ),
  298. prompt_hash=result.get("prompt_hash")
  299. or _metadata_hash(result.get("prompt_version", "unknown")),
  300. context_hash=result["context_hash"],
  301. expires_at=datetime.now(UTC) + timedelta(minutes=10),
  302. )
  303. result["generation_receipt"] = receipt_signer.issue(claims)
  304. db.session.commit()
  305. return jsonify(success(result))
  306. except (TypeError, ValueError):
  307. db.session.rollback()
  308. return _bad_request("自然语言规则描述无效")
  309. except RuntimeError:
  310. db.session.rollback()
  311. return jsonify(failed("AI 规则解析服务未配置", code=503)), 503
  312. except Exception:
  313. db.session.rollback()
  314. current_app.logger.exception("AI rule interpretation failed")
  315. return jsonify(failed("AI 规则解析暂时不可用", code=503)), 503
  316. @bp.post("/rule-versions")
  317. def create_rule_version():
  318. try:
  319. body = _closed_body(
  320. {
  321. "rule_spec",
  322. "source_text",
  323. "generation_receipt",
  324. "category",
  325. "source_language",
  326. "generated_kind",
  327. }
  328. )
  329. # Enforce V2 at the HTTP boundary even when a test/different
  330. # repository implementation is injected.
  331. rule_spec = validate_rule_spec(body.get("rule_spec"))
  332. result = _publication_service().create_draft(
  333. rule_spec=rule_spec,
  334. source_text=body.get("source_text"),
  335. category=body.get("category", "general"),
  336. source_language=body.get("source_language", "zh-CN"),
  337. generated_kind=body.get("generated_kind", "rulespec"),
  338. actor_uid=g.current_user["id"],
  339. generation_receipt=body.get("generation_receipt"),
  340. )
  341. db.session.commit()
  342. return jsonify(success(result, "规则版本创建成功")), 201
  343. except (TypeError, ValueError):
  344. db.session.rollback()
  345. return _bad_request("规则版本定义无效")
  346. except Exception:
  347. db.session.rollback()
  348. current_app.logger.exception("create rule version failed")
  349. return jsonify(failed("规则版本创建失败", code=500)), 500
  350. @bp.post("/rule-versions/<version_id>/publish")
  351. def publish_rule_version(version_id: str):
  352. try:
  353. if request.get_data(cache=True) and request.get_json(
  354. silent=True
  355. ) not in (
  356. None,
  357. {},
  358. ):
  359. raise ValueError("publish request must not contain fields")
  360. result = _publication_service().publish(
  361. version_id, g.current_user["id"]
  362. )
  363. db.session.commit()
  364. return jsonify(success(result, "规则版本发布成功"))
  365. except (TypeError, ValueError):
  366. db.session.rollback()
  367. return jsonify(failed("规则版本无法发布", code=409)), 409
  368. except Exception:
  369. db.session.rollback()
  370. current_app.logger.exception("publish rule version failed")
  371. return jsonify(failed("规则版本发布失败", code=500)), 500
  372. @bp.post("/rule-versions/<version_id>/validate")
  373. def validate_rule_version(version_id: str):
  374. try:
  375. if request.get_data(cache=True) and request.get_json(
  376. silent=True
  377. ) not in (
  378. None,
  379. {},
  380. ):
  381. raise ValueError("validate request must not contain fields")
  382. result = _publication_service().validate(
  383. version_id, g.current_user["id"]
  384. )
  385. db.session.commit()
  386. return jsonify(success(result, "规则版本编译验证成功"))
  387. except RuleValidationRejected:
  388. db.session.commit()
  389. return jsonify(failed("规则版本编译验证失败", code=409)), 409
  390. except (TypeError, ValueError):
  391. db.session.rollback()
  392. return jsonify(failed("规则版本编译验证失败", code=409)), 409
  393. except RuntimeError:
  394. db.session.rollback()
  395. return jsonify(failed("规则验证服务未配置", code=503)), 503
  396. @bp.post("/rule-versions/<version_id>/test")
  397. def test_rule_version(version_id: str):
  398. try:
  399. body = _closed_body({"plan_id"})
  400. result = _publication_service().test(
  401. version_id,
  402. g.current_user["id"],
  403. plan_id=body.get("plan_id"),
  404. )
  405. db.session.commit()
  406. return jsonify(success(result, "规则版本样本测试成功"))
  407. except (TypeError, ValueError):
  408. db.session.rollback()
  409. return jsonify(failed("规则版本样本测试失败", code=409)), 409
  410. except RuntimeError:
  411. db.session.rollback()
  412. return jsonify(failed("规则测试服务未配置", code=503)), 503
  413. @bp.get("/rule-versions/<version_id>/evidence")
  414. def rule_version_evidence(version_id: str):
  415. try:
  416. return jsonify(
  417. success(
  418. _repository().get_asset_evidence(
  419. asset_type="rule", version_id=version_id
  420. )
  421. )
  422. )
  423. except (TypeError, ValueError):
  424. return jsonify(failed("规则证据不存在", code=404)), 404
  425. except Exception:
  426. current_app.logger.exception("load rule evidence failed")
  427. return jsonify(failed("规则证据暂时不可用", code=503)), 503
  428. @bp.get("/catalog/assets/<asset_type>/<version_id>/evidence")
  429. def catalog_asset_evidence(asset_type: str, version_id: str):
  430. try:
  431. return jsonify(
  432. success(
  433. _repository().get_asset_evidence(
  434. asset_type=asset_type,
  435. version_id=version_id,
  436. )
  437. )
  438. )
  439. except (TypeError, ValueError):
  440. return jsonify(failed("资产证据不存在", code=404)), 404
  441. except Exception:
  442. current_app.logger.exception("load catalog asset evidence failed")
  443. return jsonify(failed("资产证据暂时不可用", code=503)), 503
  444. def _catalog_schema_context():
  445. raw_inputs = request.args.get("input_schema_refs")
  446. output = request.args.get("output_schema_ref")
  447. if raw_inputs is None and output is None:
  448. return None, None
  449. if raw_inputs is None or output is None:
  450. raise ValueError("catalog schema context is incomplete")
  451. inputs = json.loads(raw_inputs)
  452. if not isinstance(inputs, list):
  453. raise ValueError("catalog input_schema_refs must be an array")
  454. return inputs, output
  455. @bp.get("/catalog/assets/<asset_type>/<version_id>")
  456. def catalog_asset(asset_type: str, version_id: str):
  457. try:
  458. if set(request.args) - {
  459. "input_schema_refs",
  460. "output_schema_ref",
  461. }:
  462. raise ValueError("catalog asset query contains unsupported fields")
  463. inputs, output = _catalog_schema_context()
  464. return jsonify(
  465. success(
  466. _repository().get_published_asset(
  467. asset_type=asset_type,
  468. version_id=version_id,
  469. input_schema_refs=inputs,
  470. output_schema_ref=output,
  471. schema_resolver=_schema_resolver() if inputs is not None else None,
  472. )
  473. )
  474. )
  475. except (TypeError, ValueError, json.JSONDecodeError):
  476. return jsonify(failed("已发布资产不存在或上下文无效", code=404)), 404
  477. except Exception:
  478. current_app.logger.exception("load exact catalog asset failed")
  479. return jsonify(failed("规则目录暂时不可用", code=503)), 503
  480. @bp.get("/catalog")
  481. @bp.get("/catalog/rule-versions")
  482. def published_rule_catalog():
  483. try:
  484. legacy_rule_alias = request.path.endswith("/rule-versions")
  485. allowed = {
  486. "query",
  487. "limit",
  488. "offset",
  489. "input_schema_refs",
  490. "output_schema_ref",
  491. }
  492. if not legacy_rule_alias:
  493. allowed.add("asset_type")
  494. if set(request.args) - allowed:
  495. raise ValueError("catalog query contains unsupported fields")
  496. query = request.args.get("query", "")
  497. limit = int(request.args.get("limit", "50"))
  498. offset = int(request.args.get("offset", "0"))
  499. asset_type = (
  500. "rule"
  501. if legacy_rule_alias
  502. else request.args.get("asset_type") or None
  503. )
  504. if asset_type not in {None, "rule", "standard"}:
  505. raise ValueError("catalog asset_type is invalid")
  506. if len(query) > 200 or limit < 1 or limit > 100:
  507. raise ValueError("catalog bounds are invalid")
  508. if offset < 0 or offset > 1_000_000:
  509. raise ValueError("catalog offset is invalid")
  510. inputs, output = _catalog_schema_context()
  511. catalog_args = {
  512. "query": query,
  513. "asset_type": asset_type,
  514. "limit": limit,
  515. "offset": offset,
  516. }
  517. if inputs is not None:
  518. catalog_args.update(
  519. {
  520. "input_schema_refs": inputs,
  521. "output_schema_ref": output,
  522. "schema_resolver": _schema_resolver(),
  523. }
  524. )
  525. return jsonify(
  526. success(_repository().search_published_assets(**catalog_args))
  527. )
  528. except (TypeError, ValueError):
  529. return _bad_request("规则目录查询无效")
  530. except Exception:
  531. current_app.logger.exception("load rule catalog failed")
  532. return jsonify(failed("规则目录暂时不可用", code=503)), 503
  533. @bp.post("/execution-plans/<plan_id>/validate")
  534. def validate_physical_plan(plan_id: str):
  535. try:
  536. if request.get_data(cache=True) and request.get_json(
  537. silent=True
  538. ) not in (
  539. None,
  540. {},
  541. ):
  542. raise ValueError("validate request must not contain fields")
  543. result = _physical_publication_service().validate(
  544. plan_id, g.current_user["id"]
  545. )
  546. db.session.commit()
  547. return jsonify(success(result, "物理执行计划编译证据已确认"))
  548. except (TypeError, ValueError):
  549. db.session.rollback()
  550. return jsonify(failed("物理执行计划验证失败", code=409)), 409
  551. except RuntimeError:
  552. db.session.rollback()
  553. return jsonify(failed("物理计划验证服务未配置", code=503)), 503
  554. @bp.post("/execution-plans/<plan_id>/test")
  555. def test_physical_plan(plan_id: str):
  556. try:
  557. if request.get_data(cache=True) and request.get_json(
  558. silent=True
  559. ) not in (
  560. None,
  561. {},
  562. ):
  563. raise ValueError("test request must not contain fields")
  564. result = _physical_publication_service().test(
  565. plan_id, g.current_user["id"]
  566. )
  567. db.session.commit()
  568. return jsonify(success(result, "物理执行计划样本测试成功"))
  569. except (TypeError, ValueError):
  570. db.session.rollback()
  571. return jsonify(failed("物理执行计划样本测试失败", code=409)), 409
  572. except RuntimeError:
  573. db.session.rollback()
  574. return jsonify(failed("物理计划测试服务未配置", code=503)), 503
  575. @bp.post("/execution-plans/<plan_id>/publish")
  576. def publish_physical_plan(plan_id: str):
  577. try:
  578. if request.get_data(cache=True) and request.get_json(
  579. silent=True
  580. ) not in (
  581. None,
  582. {},
  583. ):
  584. raise ValueError("publish request must not contain fields")
  585. result = _physical_publication_service().publish(
  586. plan_id, g.current_user["id"]
  587. )
  588. db.session.commit()
  589. return jsonify(success(result, "物理执行计划发布成功"))
  590. except (TypeError, ValueError):
  591. db.session.rollback()
  592. return jsonify(failed("物理执行计划无法发布", code=409)), 409
  593. except RuntimeError:
  594. db.session.rollback()
  595. return jsonify(failed("物理计划发布服务未配置", code=503)), 503
  596. @bp.post("/standard-versions")
  597. def create_standard_version():
  598. try:
  599. body = _closed_body({"standard_spec", "source_text"})
  600. result = _repository().create_standard_version(
  601. standard_spec=body.get("standard_spec"),
  602. source_text=body.get("source_text"),
  603. created_by=g.current_user["id"],
  604. )
  605. db.session.commit()
  606. return jsonify(success(result, "数据标准版本创建成功")), 201
  607. except (TypeError, ValueError):
  608. db.session.rollback()
  609. return _bad_request("数据标准版本定义无效")
  610. except Exception:
  611. db.session.rollback()
  612. current_app.logger.exception("create standard version failed")
  613. return jsonify(failed("数据标准版本创建失败", code=500)), 500
  614. @bp.post("/standard-versions/<version_id>/publish")
  615. def publish_standard_version(version_id: str):
  616. try:
  617. if request.get_data(cache=True) and request.get_json(
  618. silent=True
  619. ) not in (
  620. None,
  621. {},
  622. ):
  623. raise ValueError("publish request must not contain fields")
  624. result = _repository().publish_standard_version(
  625. version_id=version_id,
  626. published_by=g.current_user["id"],
  627. )
  628. db.session.commit()
  629. return jsonify(success(result, "数据标准版本发布成功"))
  630. except (TypeError, ValueError):
  631. db.session.rollback()
  632. return jsonify(failed("数据标准版本无法发布", code=409)), 409
  633. except Exception:
  634. db.session.rollback()
  635. current_app.logger.exception("publish standard version failed")
  636. return jsonify(failed("数据标准版本发布失败", code=500)), 500
  637. @bp.post("/production-lines/resolve")
  638. def resolve_production_line_preview():
  639. try:
  640. body = _body()
  641. package = resolve_production_line(
  642. body.get("dataflow_spec"),
  643. body.get("standard_versions"),
  644. body.get("rule_versions"),
  645. component_binding_ids=body.get("component_binding_ids"),
  646. )
  647. return jsonify(
  648. success(
  649. {
  650. "preview": True,
  651. "release_ready": False,
  652. "package": package,
  653. }
  654. )
  655. )
  656. except (TypeError, ValueError):
  657. return _bad_request("数据生产线定义无效")
  658. @bp.post("/production-lines/<dataflow_uid>/release")
  659. def release_production_line(dataflow_uid: str):
  660. try:
  661. body = _closed_body(
  662. {
  663. "dataflow_spec",
  664. "source_text",
  665. }
  666. )
  667. result = _release_service().release(
  668. dataflow_uid=dataflow_uid,
  669. dataflow_spec=body.get("dataflow_spec"),
  670. source_text=body.get("source_text"),
  671. created_by=g.current_user["id"],
  672. )
  673. db.session.commit()
  674. return jsonify(success(result, "数据生产线发布成功")), 201
  675. except (TypeError, ValueError):
  676. db.session.rollback()
  677. return jsonify(failed("数据生产线无法发布", code=409)), 409
  678. except Exception:
  679. db.session.rollback()
  680. current_app.logger.exception("release production line failed")
  681. return jsonify(failed("数据生产线发布失败", code=500)), 500