routes.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759
  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. result = _repository().get_published_asset(
  465. asset_type=asset_type,
  466. version_id=version_id,
  467. input_schema_refs=inputs,
  468. output_schema_ref=output,
  469. schema_resolver=_schema_resolver() if inputs is not None else None,
  470. )
  471. if inputs is not None:
  472. # SchemaResolver is a read-through snapshot boundary. Persist the
  473. # IDs returned to this response before making them observable.
  474. db.session.commit()
  475. return jsonify(success(result))
  476. except (TypeError, ValueError, json.JSONDecodeError):
  477. db.session.rollback()
  478. return jsonify(failed("已发布资产不存在或上下文无效", code=404)), 404
  479. except Exception:
  480. db.session.rollback()
  481. current_app.logger.exception("load exact catalog asset failed")
  482. return jsonify(failed("规则目录暂时不可用", code=503)), 503
  483. @bp.get("/catalog")
  484. @bp.get("/catalog/rule-versions")
  485. def published_rule_catalog():
  486. try:
  487. legacy_rule_alias = request.path.endswith("/rule-versions")
  488. allowed = {
  489. "query",
  490. "limit",
  491. "offset",
  492. "input_schema_refs",
  493. "output_schema_ref",
  494. }
  495. if not legacy_rule_alias:
  496. allowed.add("asset_type")
  497. if set(request.args) - allowed:
  498. raise ValueError("catalog query contains unsupported fields")
  499. query = request.args.get("query", "")
  500. limit = int(request.args.get("limit", "50"))
  501. offset = int(request.args.get("offset", "0"))
  502. asset_type = (
  503. "rule"
  504. if legacy_rule_alias
  505. else request.args.get("asset_type") or None
  506. )
  507. if asset_type not in {None, "rule", "standard"}:
  508. raise ValueError("catalog asset_type is invalid")
  509. if len(query) > 200 or limit < 1 or limit > 100:
  510. raise ValueError("catalog bounds are invalid")
  511. if offset < 0 or offset > 1_000_000:
  512. raise ValueError("catalog offset is invalid")
  513. inputs, output = _catalog_schema_context()
  514. catalog_args = {
  515. "query": query,
  516. "asset_type": asset_type,
  517. "limit": limit,
  518. "offset": offset,
  519. }
  520. if inputs is not None:
  521. catalog_args.update(
  522. {
  523. "input_schema_refs": inputs,
  524. "output_schema_ref": output,
  525. "schema_resolver": _schema_resolver(),
  526. }
  527. )
  528. result = _repository().search_published_assets(**catalog_args)
  529. if inputs is not None:
  530. db.session.commit()
  531. return jsonify(success(result))
  532. except (TypeError, ValueError):
  533. db.session.rollback()
  534. return _bad_request("规则目录查询无效")
  535. except Exception:
  536. db.session.rollback()
  537. current_app.logger.exception("load rule catalog failed")
  538. return jsonify(failed("规则目录暂时不可用", code=503)), 503
  539. @bp.post("/execution-plans/<plan_id>/validate")
  540. def validate_physical_plan(plan_id: str):
  541. try:
  542. if request.get_data(cache=True) and request.get_json(
  543. silent=True
  544. ) not in (
  545. None,
  546. {},
  547. ):
  548. raise ValueError("validate request must not contain fields")
  549. result = _physical_publication_service().validate(
  550. plan_id, g.current_user["id"]
  551. )
  552. db.session.commit()
  553. return jsonify(success(result, "物理执行计划编译证据已确认"))
  554. except (TypeError, ValueError):
  555. db.session.rollback()
  556. return jsonify(failed("物理执行计划验证失败", code=409)), 409
  557. except RuntimeError:
  558. db.session.rollback()
  559. return jsonify(failed("物理计划验证服务未配置", code=503)), 503
  560. @bp.post("/execution-plans/<plan_id>/test")
  561. def test_physical_plan(plan_id: str):
  562. try:
  563. if request.get_data(cache=True) and request.get_json(
  564. silent=True
  565. ) not in (
  566. None,
  567. {},
  568. ):
  569. raise ValueError("test request must not contain fields")
  570. result = _physical_publication_service().test(
  571. plan_id, g.current_user["id"]
  572. )
  573. db.session.commit()
  574. return jsonify(success(result, "物理执行计划样本测试成功"))
  575. except (TypeError, ValueError):
  576. db.session.rollback()
  577. return jsonify(failed("物理执行计划样本测试失败", code=409)), 409
  578. except RuntimeError:
  579. db.session.rollback()
  580. return jsonify(failed("物理计划测试服务未配置", code=503)), 503
  581. @bp.post("/execution-plans/<plan_id>/publish")
  582. def publish_physical_plan(plan_id: str):
  583. try:
  584. if request.get_data(cache=True) and request.get_json(
  585. silent=True
  586. ) not in (
  587. None,
  588. {},
  589. ):
  590. raise ValueError("publish request must not contain fields")
  591. result = _physical_publication_service().publish(
  592. plan_id, g.current_user["id"]
  593. )
  594. db.session.commit()
  595. return jsonify(success(result, "物理执行计划发布成功"))
  596. except (TypeError, ValueError):
  597. db.session.rollback()
  598. return jsonify(failed("物理执行计划无法发布", code=409)), 409
  599. except RuntimeError:
  600. db.session.rollback()
  601. return jsonify(failed("物理计划发布服务未配置", code=503)), 503
  602. @bp.post("/standard-versions")
  603. def create_standard_version():
  604. try:
  605. body = _closed_body({"standard_spec", "source_text"})
  606. result = _repository().create_standard_version(
  607. standard_spec=body.get("standard_spec"),
  608. source_text=body.get("source_text"),
  609. created_by=g.current_user["id"],
  610. )
  611. db.session.commit()
  612. return jsonify(success(result, "数据标准版本创建成功")), 201
  613. except (TypeError, ValueError):
  614. db.session.rollback()
  615. return _bad_request("数据标准版本定义无效")
  616. except Exception:
  617. db.session.rollback()
  618. current_app.logger.exception("create standard version failed")
  619. return jsonify(failed("数据标准版本创建失败", code=500)), 500
  620. @bp.post("/standard-versions/<version_id>/publish")
  621. def publish_standard_version(version_id: str):
  622. try:
  623. if request.get_data(cache=True) and request.get_json(
  624. silent=True
  625. ) not in (
  626. None,
  627. {},
  628. ):
  629. raise ValueError("publish request must not contain fields")
  630. result = _repository().publish_standard_version(
  631. version_id=version_id,
  632. published_by=g.current_user["id"],
  633. )
  634. db.session.commit()
  635. return jsonify(success(result, "数据标准版本发布成功"))
  636. except (TypeError, ValueError):
  637. db.session.rollback()
  638. return jsonify(failed("数据标准版本无法发布", code=409)), 409
  639. except Exception:
  640. db.session.rollback()
  641. current_app.logger.exception("publish standard version failed")
  642. return jsonify(failed("数据标准版本发布失败", code=500)), 500
  643. @bp.post("/production-lines/resolve")
  644. def resolve_production_line_preview():
  645. try:
  646. body = _body()
  647. package = resolve_production_line(
  648. body.get("dataflow_spec"),
  649. body.get("standard_versions"),
  650. body.get("rule_versions"),
  651. component_binding_ids=body.get("component_binding_ids"),
  652. )
  653. return jsonify(
  654. success(
  655. {
  656. "preview": True,
  657. "release_ready": False,
  658. "package": package,
  659. }
  660. )
  661. )
  662. except (TypeError, ValueError):
  663. return _bad_request("数据生产线定义无效")
  664. @bp.post("/production-lines/<dataflow_uid>/release")
  665. def release_production_line(dataflow_uid: str):
  666. try:
  667. body = _closed_body(
  668. {
  669. "dataflow_spec",
  670. "source_text",
  671. }
  672. )
  673. result = _release_service().release(
  674. dataflow_uid=dataflow_uid,
  675. dataflow_spec=body.get("dataflow_spec"),
  676. source_text=body.get("source_text"),
  677. created_by=g.current_user["id"],
  678. )
  679. db.session.commit()
  680. return jsonify(success(result, "数据生产线发布成功")), 201
  681. except (TypeError, ValueError):
  682. db.session.rollback()
  683. return jsonify(failed("数据生产线无法发布", code=409)), 409
  684. except Exception:
  685. db.session.rollback()
  686. current_app.logger.exception("release production line failed")
  687. return jsonify(failed("数据生产线发布失败", code=500)), 500