precis-cli 0.1.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (442) hide show
  1. app/__init__.py +16 -0
  2. app/api/__init__.py +32 -0
  3. app/api/dependencies.py +185 -0
  4. app/api/main.py +301 -0
  5. app/api/middleware/__init__.py +34 -0
  6. app/api/middleware/exception_handler.py +126 -0
  7. app/api/middleware/request_logging.py +97 -0
  8. app/api/middleware/token_auth.py +215 -0
  9. app/api/models/__init__.py +151 -0
  10. app/api/models/connection_rules.py +98 -0
  11. app/api/models/files.py +99 -0
  12. app/api/models/full_validation.py +554 -0
  13. app/api/models/project.py +117 -0
  14. app/api/models/projects.py +80 -0
  15. app/api/models/schema.py +121 -0
  16. app/api/models/v2_responses.py +46 -0
  17. app/api/models/validation.py +303 -0
  18. app/api/models/workspace.py +118 -0
  19. app/api/routers/__init__.py +56 -0
  20. app/api/routers/ai/__init__.py +36 -0
  21. app/api/routers/ai/chat.py +243 -0
  22. app/api/routers/ai/generate.py +156 -0
  23. app/api/routers/ai/hardware.py +199 -0
  24. app/api/routers/ai/jobs.py +544 -0
  25. app/api/routers/ai/migrate.py +471 -0
  26. app/api/routers/ai/models.py +352 -0
  27. app/api/routers/ai/ollama.py +108 -0
  28. app/api/routers/ai/providers.py +491 -0
  29. app/api/routers/ai/router.py +44 -0
  30. app/api/routers/ai/stream.py +281 -0
  31. app/api/routers/ai/utils.py +182 -0
  32. app/api/routers/core/__init__.py +34 -0
  33. app/api/routers/core/connection_rules.py +188 -0
  34. app/api/routers/core/data_sources.py +374 -0
  35. app/api/routers/core/regex.py +333 -0
  36. app/api/routers/core/reporting.py +263 -0
  37. app/api/routers/files/__init__.py +24 -0
  38. app/api/routers/files/ops.py +179 -0
  39. app/api/routers/files/transfer.py +101 -0
  40. app/api/routers/preview/__init__.py +35 -0
  41. app/api/routers/preview/content_mode.py +264 -0
  42. app/api/routers/preview/header_row.py +154 -0
  43. app/api/routers/preview/models.py +81 -0
  44. app/api/routers/preview/path_mode.py +440 -0
  45. app/api/routers/preview/router.py +30 -0
  46. app/api/routers/project/__init__.py +61 -0
  47. app/api/routers/project/base.py +111 -0
  48. app/api/routers/project/constraint.py +345 -0
  49. app/api/routers/project/full_config.py +411 -0
  50. app/api/routers/project/full_config_writer.py +327 -0
  51. app/api/routers/project/helpers.py +167 -0
  52. app/api/routers/project/inspection_fix.py +330 -0
  53. app/api/routers/project/manifest.py +605 -0
  54. app/api/routers/project/models.py +177 -0
  55. app/api/routers/project/pattern.py +268 -0
  56. app/api/routers/project/regex.py +358 -0
  57. app/api/routers/project/scanner.py +157 -0
  58. app/api/routers/project/schema.py +567 -0
  59. app/api/routers/project/schema_helpers.py +149 -0
  60. app/api/routers/project/settings.py +399 -0
  61. app/api/routers/project/template.py +325 -0
  62. app/api/routers/project/validation.py +227 -0
  63. app/api/routers/project/view.py +177 -0
  64. app/api/routers/project/workspaces.py +167 -0
  65. app/api/routers/projects/__init__.py +25 -0
  66. app/api/routers/projects/create.py +121 -0
  67. app/api/routers/projects/open.py +103 -0
  68. app/api/routers/projects/scan.py +100 -0
  69. app/api/routers/validation/__init__.py +39 -0
  70. app/api/routers/validation/common.py +263 -0
  71. app/api/routers/validation/content_mode.py +401 -0
  72. app/api/routers/validation/history.py +235 -0
  73. app/api/routers/validation/inline_mode.py +196 -0
  74. app/api/routers/validation/path_mode.py +314 -0
  75. app/api/routers/validation/router.py +51 -0
  76. app/api/services/full_validation_response_builder.py +412 -0
  77. app/api/services/io_error_messages.py +41 -0
  78. app/api/services/preview_service.py +248 -0
  79. app/cli/__init__.py +18 -0
  80. app/cli/__main__.py +30 -0
  81. app/cli/shared_services/__init__.py +42 -0
  82. app/cli/shared_services/config_ops.py +581 -0
  83. app/cli/shared_services/generation_ops.py +160 -0
  84. app/cli/shared_services/project_ops.py +288 -0
  85. app/cli/shell/__init__.py +48 -0
  86. app/cli/shell/commands/__init__.py +63 -0
  87. app/cli/shell/commands/ai/__init__.py +260 -0
  88. app/cli/shell/commands/ai/chat.py +331 -0
  89. app/cli/shell/commands/ai/delete.py +165 -0
  90. app/cli/shell/commands/ai/diff.py +70 -0
  91. app/cli/shell/commands/ai/display.py +428 -0
  92. app/cli/shell/commands/ai/executor.py +225 -0
  93. app/cli/shell/commands/ai/executor_utils.py +271 -0
  94. app/cli/shell/commands/ai/generate.py +285 -0
  95. app/cli/shell/commands/ai/interaction.py +262 -0
  96. app/cli/shell/commands/ai/migrate.py +281 -0
  97. app/cli/shell/commands/ai/resolver.py +224 -0
  98. app/cli/shell/commands/ai/status.py +101 -0
  99. app/cli/shell/commands/ai/switch.py +158 -0
  100. app/cli/shell/commands/ai/utils.py +52 -0
  101. app/cli/shell/commands/base.py +343 -0
  102. app/cli/shell/commands/config/__init__.py +131 -0
  103. app/cli/shell/commands/config/base.py +80 -0
  104. app/cli/shell/commands/config/check.py +246 -0
  105. app/cli/shell/commands/config/edit.py +148 -0
  106. app/cli/shell/commands/config/get.py +111 -0
  107. app/cli/shell/commands/config/init.py +123 -0
  108. app/cli/shell/commands/config/inspect.py +164 -0
  109. app/cli/shell/commands/config/list.py +104 -0
  110. app/cli/shell/commands/config/set.py +119 -0
  111. app/cli/shell/commands/config/show.py +150 -0
  112. app/cli/shell/commands/exit.py +67 -0
  113. app/cli/shell/commands/help.py +104 -0
  114. app/cli/shell/commands/infer_schema.py +156 -0
  115. app/cli/shell/commands/open.py +201 -0
  116. app/cli/shell/commands/project.py +215 -0
  117. app/cli/shell/commands/provider.py +619 -0
  118. app/cli/shell/commands/system.py +170 -0
  119. app/cli/shell/commands/validate.py +455 -0
  120. app/cli/shell/completer.py +231 -0
  121. app/cli/shell/config_storage.py +191 -0
  122. app/cli/shell/exceptions.py +90 -0
  123. app/cli/shell/formatter.py +371 -0
  124. app/cli/shell/interactive_menu.py +348 -0
  125. app/cli/shell/main.py +286 -0
  126. app/cli/shell/parser.py +266 -0
  127. app/cli/start.py +180 -0
  128. app/cli_main.py +52 -0
  129. app/mcp_server.py +311 -0
  130. app/shared/core/__init__.py +18 -0
  131. app/shared/core/app_version.py +59 -0
  132. app/shared/core/config/__init__.py +119 -0
  133. app/shared/core/config/server.py +193 -0
  134. app/shared/core/data_source/__init__.py +106 -0
  135. app/shared/core/data_source/loader.py +348 -0
  136. app/shared/core/data_source/loaders/__init__.py +213 -0
  137. app/shared/core/data_source/loaders/base.py +215 -0
  138. app/shared/core/data_source/loaders/converter.py +337 -0
  139. app/shared/core/data_source/loaders/csv_loader.py +252 -0
  140. app/shared/core/data_source/loaders/excel_loader.py +666 -0
  141. app/shared/core/data_source/loaders/extractor.py +268 -0
  142. app/shared/core/data_source/loaders/json_loader.py +354 -0
  143. app/shared/core/data_source/loaders/registry.py +88 -0
  144. app/shared/core/data_source/loaders/sql_loader.py +313 -0
  145. app/shared/core/data_source/loaders/strategies/__init__.py +100 -0
  146. app/shared/core/data_source/loaders/strategies/array_parser.py +277 -0
  147. app/shared/core/data_source/loaders/strategies/lines_parser.py +355 -0
  148. app/shared/core/data_source/loaders/strategies/object_parser.py +278 -0
  149. app/shared/core/data_source/schema_info.py +50 -0
  150. app/shared/core/data_source/specs/__init__.py +128 -0
  151. app/shared/core/data_source/specs/base.py +258 -0
  152. app/shared/core/data_source/specs/csv_source.py +137 -0
  153. app/shared/core/data_source/specs/excel_source.py +141 -0
  154. app/shared/core/data_source/specs/file_base.py +252 -0
  155. app/shared/core/data_source/specs/json_source.py +226 -0
  156. app/shared/core/data_source/specs/sql_source.py +161 -0
  157. app/shared/core/encoding.py +36 -0
  158. app/shared/core/io/__init__.py +24 -0
  159. app/shared/core/io/yaml.py +379 -0
  160. app/shared/core/manifest_schema/__init__.py +52 -0
  161. app/shared/core/manifest_schema/types.py +99 -0
  162. app/shared/core/manifest_schema/version.py +175 -0
  163. app/shared/core/patterns/__init__.py +24 -0
  164. app/shared/core/patterns/loader.py +134 -0
  165. app/shared/core/patterns/writer.py +244 -0
  166. app/shared/core/project/__init__.py +47 -0
  167. app/shared/core/project/constraint/builders/__init__.py +50 -0
  168. app/shared/core/project/constraint/builders/base.py +100 -0
  169. app/shared/core/project/constraint/builders/composite.py +77 -0
  170. app/shared/core/project/constraint/builders/conditional.py +67 -0
  171. app/shared/core/project/constraint/builders/foreign_key.py +53 -0
  172. app/shared/core/project/constraint/builders/registry.py +64 -0
  173. app/shared/core/project/constraint/builders/scripted.py +51 -0
  174. app/shared/core/project/constraint/builders/single_column.py +86 -0
  175. app/shared/core/project/constraint/builders/unique.py +53 -0
  176. app/shared/core/project/constraint/factory.py +170 -0
  177. app/shared/core/project/constraint/reader.py +214 -0
  178. app/shared/core/project/constraint/registry.py +233 -0
  179. app/shared/core/project/constraint/types/__init__.py +63 -0
  180. app/shared/core/project/constraint/types/constraint_file.py +261 -0
  181. app/shared/core/project/constraint/types/refs.py +460 -0
  182. app/shared/core/project/constraint/types.py +28 -0
  183. app/shared/core/project/constraint/writer.py +181 -0
  184. app/shared/core/project/loader/__init__.py +56 -0
  185. app/shared/core/project/loader/loader.py +30 -0
  186. app/shared/core/project/loader/loader_parts/config_inspector.py +137 -0
  187. app/shared/core/project/loader/loader_parts/embedded_constraints.py +224 -0
  188. app/shared/core/project/loader/loader_parts/file_loaders.py +58 -0
  189. app/shared/core/project/loader/loader_parts/inspection_ids.py +85 -0
  190. app/shared/core/project/loader/loader_parts/inspector_helpers.py +312 -0
  191. app/shared/core/project/loader/loader_parts/inspector_id_checks.py +274 -0
  192. app/shared/core/project/loader/loader_parts/inspector_reference_checks.py +690 -0
  193. app/shared/core/project/loader/loader_parts/inspector_uniqueness_checks.py +286 -0
  194. app/shared/core/project/loader/loader_parts/loading_error_messages.py +298 -0
  195. app/shared/core/project/loader/loader_parts/main.py +432 -0
  196. app/shared/core/project/loader/loader_parts/path_validation.py +117 -0
  197. app/shared/core/project/loader/loader_parts/runtime.py +125 -0
  198. app/shared/core/project/loader/types.py +246 -0
  199. app/shared/core/project/manifest/coverage.py +391 -0
  200. app/shared/core/project/manifest/reader.py +260 -0
  201. app/shared/core/project/manifest/types.py +91 -0
  202. app/shared/core/project/manifest/types_parts/__init__.py +60 -0
  203. app/shared/core/project/manifest/types_parts/constants.py +40 -0
  204. app/shared/core/project/manifest/types_parts/data_source.py +68 -0
  205. app/shared/core/project/manifest/types_parts/info.py +59 -0
  206. app/shared/core/project/manifest/types_parts/manifest.py +294 -0
  207. app/shared/core/project/manifest/types_parts/refs.py +153 -0
  208. app/shared/core/project/manifest/types_parts/settings.py +92 -0
  209. app/shared/core/project/manifest/types_parts/settings_file_processing.py +64 -0
  210. app/shared/core/project/manifest/types_parts/settings_script_security.py +78 -0
  211. app/shared/core/project/manifest/types_parts/settings_validation.py +83 -0
  212. app/shared/core/project/manifest/types_parts/template.py +66 -0
  213. app/shared/core/project/manifest/writer.py +262 -0
  214. app/shared/core/project/manual_data/__init__.py +27 -0
  215. app/shared/core/project/manual_data/types.py +75 -0
  216. app/shared/core/project/regex/reader.py +197 -0
  217. app/shared/core/project/regex/types.py +405 -0
  218. app/shared/core/project/regex/writer.py +123 -0
  219. app/shared/core/project/schema/reader.py +170 -0
  220. app/shared/core/project/schema/types.py +47 -0
  221. app/shared/core/project/schema/types_parts/__init__.py +36 -0
  222. app/shared/core/project/schema/types_parts/column.py +174 -0
  223. app/shared/core/project/schema/types_parts/column_utils.py +72 -0
  224. app/shared/core/project/schema/types_parts/constraint.py +165 -0
  225. app/shared/core/project/schema/types_parts/schema_id.py +66 -0
  226. app/shared/core/project/schema/types_parts/source.py +255 -0
  227. app/shared/core/project/schema/types_parts/source_options.py +347 -0
  228. app/shared/core/project/schema/types_parts/table.py +230 -0
  229. app/shared/core/project/schema/writer.py +139 -0
  230. app/shared/core/project/schema_ref_check.py +95 -0
  231. app/shared/core/project/template/__init__.py +27 -0
  232. app/shared/core/project/template/expander.py +263 -0
  233. app/shared/core/project/template/reader.py +120 -0
  234. app/shared/core/project/template/types.py +114 -0
  235. app/shared/core/project/transform/reader.py +76 -0
  236. app/shared/core/project/transform/types.py +116 -0
  237. app/shared/core/project/transform/writer.py +84 -0
  238. app/shared/core/pydantic_messages.py +59 -0
  239. app/shared/core/reporter/__init__.py +46 -0
  240. app/shared/core/reporter/reporter.py +220 -0
  241. app/shared/core/reporter/reporters/__init__.py +65 -0
  242. app/shared/core/reporter/reporters/base.py +188 -0
  243. app/shared/core/reporter/reporters/dingtalk_app_reporter.py +274 -0
  244. app/shared/core/reporter/reporters/email_reporter.py +271 -0
  245. app/shared/core/reporter/reporters/feishu_app_reporter.py +467 -0
  246. app/shared/core/reporter/reporters/local_file_reporter.py +208 -0
  247. app/shared/core/reporter/reporters/wecom_app_reporter.py +268 -0
  248. app/shared/core/utils/__init__.py +18 -0
  249. app/shared/core/utils/path_utils.py +60 -0
  250. app/shared/core/utils/regex_extract.py +113 -0
  251. app/shared/domain/__init__.py +114 -0
  252. app/shared/domain/constraints/__init__.py +76 -0
  253. app/shared/domain/constraints/allowed_values.py +211 -0
  254. app/shared/domain/constraints/base.py +141 -0
  255. app/shared/domain/constraints/charset.py +337 -0
  256. app/shared/domain/constraints/composite.py +174 -0
  257. app/shared/domain/constraints/condition_registry.py +130 -0
  258. app/shared/domain/constraints/conditional.py +629 -0
  259. app/shared/domain/constraints/date_logic.py +731 -0
  260. app/shared/domain/constraints/foreign_key.py +261 -0
  261. app/shared/domain/constraints/key_normalization.py +56 -0
  262. app/shared/domain/constraints/not_null.py +185 -0
  263. app/shared/domain/constraints/range.py +360 -0
  264. app/shared/domain/constraints/regex.py +218 -0
  265. app/shared/domain/constraints/scripted.py +276 -0
  266. app/shared/domain/constraints/unique.py +233 -0
  267. app/shared/domain/data_engine.py +384 -0
  268. app/shared/domain/data_types.py +113 -0
  269. app/shared/domain/data_types_parts/__init__.py +67 -0
  270. app/shared/domain/data_types_parts/base.py +204 -0
  271. app/shared/domain/data_types_parts/composite.py +250 -0
  272. app/shared/domain/data_types_parts/expression.py +202 -0
  273. app/shared/domain/data_types_parts/extracted.py +106 -0
  274. app/shared/domain/data_types_parts/json_types.py +234 -0
  275. app/shared/domain/data_types_parts/scalars.py +719 -0
  276. app/shared/domain/data_types_parts/sequence.py +129 -0
  277. app/shared/domain/dataset_schema.py +48 -0
  278. app/shared/domain/eval_sandbox.py +63 -0
  279. app/shared/domain/expression_system.py +366 -0
  280. app/shared/domain/regex_flags.py +53 -0
  281. app/shared/domain/schema/builder.py +223 -0
  282. app/shared/domain/schema/models.py +339 -0
  283. app/shared/domain/transforms/__init__.py +28 -0
  284. app/shared/domain/transforms/aggregate.py +141 -0
  285. app/shared/domain/transforms/base.py +171 -0
  286. app/shared/domain/transforms/cast_type.py +120 -0
  287. app/shared/domain/transforms/concat.py +103 -0
  288. app/shared/domain/transforms/conditional_assign.py +114 -0
  289. app/shared/domain/transforms/date_format.py +81 -0
  290. app/shared/domain/transforms/digits.py +77 -0
  291. app/shared/domain/transforms/drop_duplicates.py +94 -0
  292. app/shared/domain/transforms/fill_na.py +94 -0
  293. app/shared/domain/transforms/filter_rows.py +97 -0
  294. app/shared/domain/transforms/lookup.py +78 -0
  295. app/shared/domain/transforms/lower_case.py +70 -0
  296. app/shared/domain/transforms/map_value.py +90 -0
  297. app/shared/domain/transforms/math_expr.py +112 -0
  298. app/shared/domain/transforms/modulo.py +82 -0
  299. app/shared/domain/transforms/regex_extract.py +103 -0
  300. app/shared/domain/transforms/registry.py +95 -0
  301. app/shared/domain/transforms/replace.py +88 -0
  302. app/shared/domain/transforms/sort_rows.py +94 -0
  303. app/shared/domain/transforms/string_split.py +84 -0
  304. app/shared/domain/transforms/strip.py +73 -0
  305. app/shared/domain/transforms/substring.py +92 -0
  306. app/shared/domain/transforms/upper_case.py +70 -0
  307. app/shared/domain/transforms/weighted_sum.py +104 -0
  308. app/shared/domain/validation_constraints.py +69 -0
  309. app/shared/services/__init__.py +62 -0
  310. app/shared/services/ai/__init__.py +48 -0
  311. app/shared/services/ai/agent/__init__.py +40 -0
  312. app/shared/services/ai/agent/chat_tools/__init__.py +47 -0
  313. app/shared/services/ai/agent/chat_tools/apply_actions.py +592 -0
  314. app/shared/services/ai/agent/chat_tools/ask_user.py +237 -0
  315. app/shared/services/ai/agent/chat_tools/read_canvas.py +140 -0
  316. app/shared/services/ai/agent/chat_tools/read_project.py +160 -0
  317. app/shared/services/ai/agent/chat_tools/read_table.py +301 -0
  318. app/shared/services/ai/agent/chat_tools/schemas.py +105 -0
  319. app/shared/services/ai/agent/chat_tools/validate_table.py +131 -0
  320. app/shared/services/ai/agent/executor.py +554 -0
  321. app/shared/services/ai/agent/memory.py +256 -0
  322. app/shared/services/ai/agent/planner.py +282 -0
  323. app/shared/services/ai/agent/tool_registry.py +296 -0
  324. app/shared/services/ai/agent/tools/__init__.py +32 -0
  325. app/shared/services/ai/agent/tools/config_generate.py +133 -0
  326. app/shared/services/ai/agent/tools/config_refine.py +109 -0
  327. app/shared/services/ai/agent/tools/config_validate.py +403 -0
  328. app/shared/services/ai/agent/tools/merge_results.py +136 -0
  329. app/shared/services/ai/agent/tools/plan_chunks.py +79 -0
  330. app/shared/services/ai/agent/tools/script_parse.py +359 -0
  331. app/shared/services/ai/agent/types.py +138 -0
  332. app/shared/services/ai/chat_agent_runner.py +596 -0
  333. app/shared/services/ai/chat_orchestrator.py +725 -0
  334. app/shared/services/ai/failure_messages.py +65 -0
  335. app/shared/services/ai/job_storage.py +274 -0
  336. app/shared/services/ai/migrate_service.py +550 -0
  337. app/shared/services/ai/streaming/__init__.py +65 -0
  338. app/shared/services/ai/streaming/event_journal.py +197 -0
  339. app/shared/services/ai/streaming/orchestrator.py +262 -0
  340. app/shared/services/ai/streaming/pending_interaction_store.py +227 -0
  341. app/shared/services/ai/streaming/sse_response.py +148 -0
  342. app/shared/services/ai/streaming/types.py +73 -0
  343. app/shared/services/ai/types.py +213 -0
  344. app/shared/services/ai/utils.py +322 -0
  345. app/shared/services/diff/config_diff.py +320 -0
  346. app/shared/services/hardware.py +237 -0
  347. app/shared/services/llm/__init__.py +62 -0
  348. app/shared/services/llm/actions/__init__.py +42 -0
  349. app/shared/services/llm/actions/_canvas_validator.py +147 -0
  350. app/shared/services/llm/actions/_constraint_validator.py +401 -0
  351. app/shared/services/llm/actions/_regex_validator.py +85 -0
  352. app/shared/services/llm/actions/_schema_validator.py +82 -0
  353. app/shared/services/llm/actions/_settings_validator.py +76 -0
  354. app/shared/services/llm/actions/_transform_validator.py +78 -0
  355. app/shared/services/llm/actions/action_handlers.py +400 -0
  356. app/shared/services/llm/actions/action_parser.py +63 -0
  357. app/shared/services/llm/actions/action_processor.py +468 -0
  358. app/shared/services/llm/actions/action_validator.py +311 -0
  359. app/shared/services/llm/actions/diff_compute.py +195 -0
  360. app/shared/services/llm/actions/regex_handlers.py +284 -0
  361. app/shared/services/llm/actions/registry.py +336 -0
  362. app/shared/services/llm/actions/schema_handlers.py +335 -0
  363. app/shared/services/llm/actions/settings_handlers.py +180 -0
  364. app/shared/services/llm/actions/specs.py +259 -0
  365. app/shared/services/llm/actions/transform_handlers.py +279 -0
  366. app/shared/services/llm/actions/validation_types.py +147 -0
  367. app/shared/services/llm/cache/__init__.py +18 -0
  368. app/shared/services/llm/cache/response_cache.py +80 -0
  369. app/shared/services/llm/chat/__init__.py +35 -0
  370. app/shared/services/llm/chat/chat_system_prompt.py +561 -0
  371. app/shared/services/llm/chat/response_parser.py +296 -0
  372. app/shared/services/llm/config/__init__.py +45 -0
  373. app/shared/services/llm/config/crypto.py +135 -0
  374. app/shared/services/llm/config/loader.py +258 -0
  375. app/shared/services/llm/config/models.py +202 -0
  376. app/shared/services/llm/config/presets.py +128 -0
  377. app/shared/services/llm/config_generator.py +163 -0
  378. app/shared/services/llm/constraints/__init__.py +41 -0
  379. app/shared/services/llm/constraints/constraint_builder.py +177 -0
  380. app/shared/services/llm/constraints/constraint_deletion.py +84 -0
  381. app/shared/services/llm/constraints/constraint_id.py +174 -0
  382. app/shared/services/llm/constraints/frontend_instructions.py +333 -0
  383. app/shared/services/llm/constraints/inline_batch.py +279 -0
  384. app/shared/services/llm/discovery/__init__.py +35 -0
  385. app/shared/services/llm/discovery/scanner.py +181 -0
  386. app/shared/services/llm/generation/__init__.py +42 -0
  387. app/shared/services/llm/generation/agent_wiring.py +156 -0
  388. app/shared/services/llm/generation/config_builder.py +386 -0
  389. app/shared/services/llm/generation/errors.py +36 -0
  390. app/shared/services/llm/generation/existing_config.py +99 -0
  391. app/shared/services/llm/generation/profiler.py +279 -0
  392. app/shared/services/llm/generation/prompt_builder.py +199 -0
  393. app/shared/services/llm/generation/response_parser.py +144 -0
  394. app/shared/services/llm/generation/service.py +622 -0
  395. app/shared/services/llm/models.py +104 -0
  396. app/shared/services/llm/providers/__init__.py +48 -0
  397. app/shared/services/llm/providers/base.py +310 -0
  398. app/shared/services/llm/providers/cached_provider.py +90 -0
  399. app/shared/services/llm/providers/ollama.py +498 -0
  400. app/shared/services/llm/providers/openai.py +334 -0
  401. app/shared/services/llm/providers/registry.py +108 -0
  402. app/shared/services/llm/schema_resolver.py +141 -0
  403. app/shared/services/llm/suggestion_utils.py +219 -0
  404. app/shared/services/llm/validate_executor.py +163 -0
  405. app/shared/services/llm/yaml_io.py +406 -0
  406. app/shared/services/preview/__init__.py +18 -0
  407. app/shared/services/preview/loader.py +145 -0
  408. app/shared/services/preview/path_validation.py +138 -0
  409. app/shared/services/project_loader.py +57 -0
  410. app/shared/services/schema_inference.py +260 -0
  411. app/shared/services/schema_runtime_builder.py +120 -0
  412. app/shared/services/validation/__init__.py +52 -0
  413. app/shared/services/validation/chunked_loader.py +509 -0
  414. app/shared/services/validation/dag/__init__.py +35 -0
  415. app/shared/services/validation/dag/builder.py +141 -0
  416. app/shared/services/validation/dag/executor.py +225 -0
  417. app/shared/services/validation/dag/sorter.py +77 -0
  418. app/shared/services/validation/data_loader.py +231 -0
  419. app/shared/services/validation/engine.py +446 -0
  420. app/shared/services/validation/executor.py +1030 -0
  421. app/shared/services/validation/extractors.py +266 -0
  422. app/shared/services/validation/history.py +209 -0
  423. app/shared/services/validation/json_payload.py +136 -0
  424. app/shared/services/validation/loader.py +152 -0
  425. app/shared/services/validation/memory_monitor.py +179 -0
  426. app/shared/services/validation/postprocess.py +208 -0
  427. app/shared/services/validation/progress.py +64 -0
  428. app/shared/services/validation/report_export.py +222 -0
  429. app/shared/services/validation/resolver.py +180 -0
  430. app/shared/services/validation/service.py +418 -0
  431. app/shared/services/validation/types.py +238 -0
  432. app/shared/services/validation/validators/__init__.py +36 -0
  433. app/shared/services/validation/validators/adapter.py +182 -0
  434. app/shared/services/validation/validators/base.py +332 -0
  435. app/shared/services/validation/validators/composite.py +233 -0
  436. app/shared/services/validation/validators/date_logic.py +276 -0
  437. app/start_server.py +133 -0
  438. precis_cli-0.1.3.dist-info/METADATA +180 -0
  439. precis_cli-0.1.3.dist-info/RECORD +442 -0
  440. precis_cli-0.1.3.dist-info/WHEEL +5 -0
  441. precis_cli-0.1.3.dist-info/entry_points.txt +4 -0
  442. precis_cli-0.1.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,567 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ #
3
+ # Copyright 2026 Precis Team
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ # backend/app/api/routers/project/schema.py
17
+ """
18
+ @fileoverview Schema CRUD API - 数据表结构管理
19
+
20
+ 功能概述:
21
+ - 负责 Schema(数据表结构定义)的增删改查操作
22
+ - Schema 定义了数据源的列结构、类型约束等信息
23
+ - 支持冲突检测和合并写入策略
24
+
25
+ 架构设计:
26
+ - 两层查找策略: 优先从 manifest 读取引用,兼容扫描目录
27
+ - 文件命名约定: {table_id}.schema.yaml
28
+ - 合并写入模式: CREATE / OVERWRITE / MERGE 三种 SaveMode
29
+ - 冲突检测: 检查同名表在 manifest 和文件系统的一致性
30
+
31
+ 输入示例:
32
+ PUT /schemas/users
33
+ {
34
+ "id": "users",
35
+ "columns": [
36
+ {"id": "name", "type": "string"}
37
+ ]
38
+ }
39
+
40
+ 输出示例:
41
+ {
42
+ "success": true,
43
+ "data": {"table_id": "users", "display_name": "users"}
44
+ }
45
+ """
46
+
47
+ import os
48
+ from enum import Enum
49
+ from pathlib import Path
50
+ from typing import Any
51
+
52
+ from fastapi import APIRouter, Depends, HTTPException
53
+ from pydantic import BaseModel
54
+
55
+ from app.api.dependencies import get_project_config_path
56
+ from app.shared.core.io.yaml import read_yaml, write_yaml_atomic
57
+ from app.shared.core.project.manifest.types import ProjectManifestV2
58
+ from app.shared.core.project.schema_ref_check import find_schema_references
59
+ from app.shared.core.utils.path_utils import make_relative
60
+
61
+ from .base import (
62
+ DisplayNameUpdateRequest,
63
+ SchemaRefV2,
64
+ StandardResponse,
65
+ TableSchemaFileV2,
66
+ _scan_schema_file,
67
+ _v2_manifest_path,
68
+ )
69
+ from .helpers import _resolve_project_path, project_lock
70
+ from .manifest import get_v2_manifest
71
+ from .schema_helpers import _compute_conflicts, _get_schema_path, _merge_schemas
72
+
73
+ router = APIRouter(prefix="", tags=["Project-Schema"])
74
+
75
+
76
+ class SaveMode(str, Enum):
77
+ """Schema 保存模式"""
78
+
79
+ CREATE = "create" # 仅新建,不覆盖已有文件
80
+ MERGE = "merge" # 合并写入
81
+ OVERWRITE = "overwrite" # 直接覆盖
82
+
83
+
84
+ class SchemaConflictInfo(BaseModel):
85
+ """
86
+ Schema 冲突信息模型。
87
+
88
+ 使用场景:
89
+ - 前端在保存 schema 前调用 check-conflict 接口,根据返回结果决定是否提示用户覆盖确认
90
+ - 为前端提供详细的字段级冲突对比,提升用户体验
91
+
92
+ 字段说明:
93
+ - exists: 目标文件是否已存在
94
+ - file_path: 冲突文件相对于项目根目录的路径
95
+ - has_conflict: 是否存在实质性字段冲突
96
+ - conflict_fields: 具体冲突的字段列表(如 columns, constraints 等)
97
+ - existing_schema: 现有 schema 的完整内容(用于前端对比展示)
98
+ - new_schema: 新 schema 的完整内容(用于前端对比展示)
99
+ """
100
+
101
+ exists: bool
102
+ file_path: str
103
+ has_conflict: bool
104
+ conflict_fields: list[str] = []
105
+ existing_schema: dict[str, Any] | None = None
106
+ new_schema: dict[str, Any] | None = None
107
+
108
+
109
+ @router.get(
110
+ "/schemas/{table_id}",
111
+ response_model=TableSchemaFileV2,
112
+ summary="读取 Schema",
113
+ responses={
114
+ 404: {"description": "Schema 不存在"},
115
+ 500: {"description": "服务器内部错误"},
116
+ },
117
+ )
118
+ def get_v2_schema(table_id: str, config_path: str = Depends(get_project_config_path)) -> TableSchemaFileV2:
119
+ """
120
+ 读取指定 table_id 的 schema 文件。
121
+
122
+ 查找策略(支持所有 schema,包括未列入 manifest 的):
123
+ 1. 从 manifest.schemas 中查找引用(优先,性能更好)
124
+ 2. 如果未找到,扫描 schemas/ 目录,读取文件内容中的 id 字段
125
+
126
+ 参数:
127
+ table_id: Schema 对应的表 ID
128
+ config_path: 项目配置根目录(通过 Depends 注入)
129
+
130
+ 返回:
131
+ TableSchemaFileV2: Schema 文件内容
132
+ """
133
+ manifest = get_v2_manifest(config_path)
134
+
135
+ # 策略 1: 从 manifest 中查找(优先)
136
+ ref = next((s for s in manifest.schemas if s.id == table_id), None)
137
+
138
+ if ref:
139
+ schema_path = os.path.join(config_path, ref.path)
140
+ if os.path.isfile(schema_path):
141
+ data = read_yaml(Path(schema_path))
142
+ return TableSchemaFileV2.model_validate(data)
143
+
144
+ # 策略 2: 扫描 schemas/ 目录,读取文件内容中的 id 字段
145
+ schemas_dir = os.path.join(config_path, "schemas")
146
+ if os.path.isdir(schemas_dir):
147
+ for filename in os.listdir(schemas_dir):
148
+ if filename.lower().endswith(".schema.yaml"):
149
+ try:
150
+ file_path = os.path.join(schemas_dir, filename)
151
+ data = read_yaml(Path(file_path))
152
+ if isinstance(data, dict) and data.get("id") == table_id:
153
+ return TableSchemaFileV2.model_validate(data)
154
+ except Exception:
155
+ # 读取失败,继续下一个文件
156
+ continue
157
+
158
+ raise HTTPException(status_code=404, detail=f"未找到这张表结构定义,可能已被删除(ID: {table_id})")
159
+
160
+
161
+ @router.put(
162
+ "/schemas/{table_id}",
163
+ response_model=StandardResponse,
164
+ summary="写入 Schema",
165
+ responses={
166
+ 400: {"description": "请求参数错误"},
167
+ 404: {"description": "Manifest 不存在"},
168
+ 409: {"description": "Schema 文件已存在"},
169
+ 500: {"description": "服务器内部错误"},
170
+ },
171
+ )
172
+ def put_v2_schema(
173
+ table_id: str,
174
+ schema: TableSchemaFileV2,
175
+ mode: SaveMode = SaveMode.OVERWRITE,
176
+ config_path: str = Depends(get_project_config_path),
177
+ ) -> dict[str, str]:
178
+ """
179
+ 写入指定 table_id 的 schema 文件。
180
+
181
+ 业务逻辑:
182
+ - 验证 schema.id 与路径参数 table_id 一致
183
+ - 如果 manifest 未引用,自动补齐引用
184
+ - 同时更新 manifest 文件
185
+ - 支持三种保存模式:create(仅新建)、merge(合并)、overwrite(覆盖)
186
+
187
+ 副作用:
188
+ - 创建或覆盖 schema 文件
189
+ - 更新 manifest 中的引用列表
190
+
191
+ 数据流:
192
+ 1. 验证 schema.id 与 table_id 一致
193
+ 2. 读取 manifest 文件
194
+ 3. 检查 manifest 是否已有该 schema 的引用
195
+ 4. 如果没有,添加新的引用(默认路径:schemas/{table_id}.schema.yaml)
196
+ 5. 根据模式处理文件写入
197
+ - create: 文件存在则报错
198
+ - merge: 读取现有文件并合并
199
+ - overwrite: 直接覆盖
200
+ 6. 更新 manifest 文件
201
+
202
+ 参数:
203
+ table_id: Schema 对应的表 ID
204
+ schema: Schema 数据
205
+ mode: 保存模式 (create/merge/overwrite)
206
+ config_path: 项目配置根目录(通过 Depends 注入)
207
+
208
+ 返回:
209
+ StandardResponse: 操作结果消息
210
+ """
211
+ if schema.id != table_id:
212
+ raise HTTPException(status_code=400, detail="请求中的表结构 ID 与地址栏不一致,请刷新页面后重试")
213
+
214
+ manifest_path = _v2_manifest_path(config_path)
215
+ manifest = (
216
+ ProjectManifestV2.model_validate(read_yaml(Path(manifest_path))) if os.path.isfile(manifest_path) else None
217
+ )
218
+ if not manifest:
219
+ raise HTTPException(
220
+ status_code=404,
221
+ detail={
222
+ # §2.1: 结构化错误码统一(与 dependencies.py 的 manifest 缺失出口同构),
223
+ # 前端按 detail.code 识别项目失效并触发自愈
224
+ "code": "PROJECT_NOT_FOUND",
225
+ "message": f"项目配置文件(project.precis.yaml)不存在,请确认项目是否已被移动或删除(配置目录: {manifest_path})",
226
+ "path": str(
227
+ config_path
228
+ ), # §2.1: 项目根目录(前端与 activeProjectPath 同口径比对,勿传 manifest 文件路径)
229
+ },
230
+ )
231
+
232
+ # 使用 _get_schema_path 获取实际的文件路径(处理大小写不一致问题)
233
+ abs_schema_path = _get_schema_path(manifest, table_id, config_path)
234
+
235
+ ref = next((s for s in manifest.schemas if s.id == table_id), None)
236
+ if not ref:
237
+ schema_name = schema.name or table_id
238
+ default_rel = f"schemas/{schema_name}.schema.yaml"
239
+ manifest.schemas.append(SchemaRefV2(id=table_id, path=default_rel))
240
+ ref_path = default_rel
241
+ if abs_schema_path is None:
242
+ abs_schema_path = os.path.join(config_path, ref_path)
243
+ else:
244
+ ref_path = ref.path
245
+ if abs_schema_path and abs_schema_path != os.path.join(config_path, ref_path):
246
+ actual_rel = os.path.relpath(abs_schema_path, config_path)
247
+ ref.path = actual_rel
248
+ ref_path = actual_rel
249
+ if abs_schema_path is None:
250
+ abs_schema_path = os.path.join(config_path, ref_path)
251
+
252
+ # 验证路径安全,防止路径遍历攻击
253
+ try:
254
+ abs_schema_path = _resolve_project_path(config_path, os.path.relpath(abs_schema_path, config_path))
255
+ except ValueError:
256
+ raise HTTPException(status_code=400, detail="非法的 Schema 文件路径")
257
+
258
+ new_schema_data = schema.model_dump(exclude_none=True)
259
+
260
+ with project_lock(config_path):
261
+ if os.path.exists(abs_schema_path):
262
+ if mode == SaveMode.CREATE:
263
+ raise HTTPException(
264
+ status_code=409, detail=f"Schema 文件已存在: {abs_schema_path}。请使用 merge 或 overwrite 模式。"
265
+ )
266
+
267
+ if mode == SaveMode.MERGE:
268
+ existing_data = read_yaml(Path(abs_schema_path))
269
+ merged_data = _merge_schemas(existing_data, new_schema_data)
270
+ write_yaml_atomic(Path(abs_schema_path), merged_data)
271
+ else:
272
+ write_yaml_atomic(Path(abs_schema_path), new_schema_data)
273
+ else:
274
+ write_yaml_atomic(Path(abs_schema_path), new_schema_data)
275
+
276
+ write_yaml_atomic(Path(manifest_path), manifest.model_dump(exclude_none=True))
277
+
278
+ return {"message": f"V2 schema '{table_id}' 已保存。"}
279
+
280
+
281
+ @router.post(
282
+ "/schemas/{table_id}/check-conflict",
283
+ response_model=SchemaConflictInfo,
284
+ summary="检查 Schema 冲突",
285
+ responses={
286
+ 400: {"description": "请求参数错误"},
287
+ 404: {"description": "Schema 不存在"},
288
+ 500: {"description": "服务器内部错误"},
289
+ },
290
+ )
291
+ def check_schema_conflict(
292
+ table_id: str,
293
+ new_schema: TableSchemaFileV2,
294
+ config_path: str = Depends(get_project_config_path),
295
+ ) -> SchemaConflictInfo:
296
+ """
297
+ 检查 schema 保存时的冲突情况。
298
+
299
+ 用于在保存前检查是否与已存在的文件存在冲突,
300
+ 以便前端决定是否提示用户进行覆盖确认。
301
+
302
+ 参数:
303
+ table_id: Schema 对应的表 ID
304
+ new_schema: 新的 schema 数据
305
+ config_path: 项目配置根目录(通过 Depends 注入)
306
+
307
+ 返回:
308
+ SchemaConflictInfo: 冲突信息(file_path 为相对于 config_path 的路径)
309
+ """
310
+ manifest = get_v2_manifest(config_path)
311
+ schema_path = _get_schema_path(manifest, table_id, config_path)
312
+
313
+ if not schema_path or not os.path.isfile(schema_path):
314
+ # 返回相对于 config_path 的路径(如果存在)
315
+ relative_path = ""
316
+ if schema_path and schema_path.startswith(config_path):
317
+ relative_path = make_relative(config_path, schema_path)
318
+ return SchemaConflictInfo(
319
+ exists=False,
320
+ file_path=relative_path,
321
+ has_conflict=False,
322
+ conflict_fields=[],
323
+ existing_schema=None,
324
+ new_schema=new_schema.model_dump(exclude_none=True),
325
+ )
326
+
327
+ existing_data = read_yaml(Path(schema_path))
328
+ new_data = new_schema.model_dump(exclude_none=True)
329
+ conflict_fields = _compute_conflicts(existing_data, new_data)
330
+
331
+ # 将绝对路径转换为相对于 config_path 的路径
332
+ relative_path = make_relative(config_path, schema_path)
333
+
334
+ return SchemaConflictInfo(
335
+ exists=True,
336
+ file_path=relative_path,
337
+ has_conflict=len(conflict_fields) > 0,
338
+ conflict_fields=conflict_fields,
339
+ existing_schema=existing_data,
340
+ new_schema=new_data,
341
+ )
342
+
343
+
344
+ @router.delete(
345
+ "/schemas/{table_id}",
346
+ response_model=StandardResponse,
347
+ summary="删除 Schema",
348
+ responses={
349
+ 400: {"description": "请求参数错误"},
350
+ 404: {"description": "Schema 或 Manifest 不存在"},
351
+ 409: {"description": "Schema 仍被约束引用"},
352
+ 500: {"description": "服务器内部错误"},
353
+ },
354
+ )
355
+ def delete_v2_schema(table_id: str, config_path: str = Depends(get_project_config_path)) -> dict[str, str]:
356
+ """
357
+ 删除指定 table_id 的 schema 文件,并从 manifest 中移除引用。
358
+
359
+ 删除策略(两层查找,更健壮):
360
+ 1. 首先从 manifest.schemas 中查找引用(优先)
361
+ 2. 如果未找到,扫描 schemas/ 目录(兼容旧项目)
362
+
363
+ 副作用:
364
+ - 删除对应的 .schema.yaml 文件
365
+ - 从 manifest.schemas 中移除引用(如果存在)
366
+
367
+ 数据流:
368
+ 1. 读取 manifest 文件
369
+ 2. 在 manifest.schemas 中查找引用
370
+ 3. 如果未找到,扫描 schemas/ 目录
371
+ 4. 删除找到的 schema 文件
372
+ 5. 从 manifest 中移除引用
373
+ 6. 保存更新后的 manifest
374
+
375
+ 参数:
376
+ table_id: Schema 对应的表 ID
377
+ config_path: 项目配置根目录(通过 Depends 注入)
378
+
379
+ 返回:
380
+ StandardResponse: 操作结果消息
381
+ """
382
+ manifest_path = _v2_manifest_path(config_path)
383
+ if not os.path.isfile(manifest_path):
384
+ raise HTTPException(
385
+ status_code=404,
386
+ detail={
387
+ # §2.1: 结构化错误码统一(与 dependencies.py 的 manifest 缺失出口同构),
388
+ # 前端按 detail.code 识别项目失效并触发自愈
389
+ "code": "PROJECT_NOT_FOUND",
390
+ "message": f"项目配置文件(project.precis.yaml)不存在,请确认项目是否已被移动或删除(配置目录: {manifest_path})",
391
+ "path": str(
392
+ config_path
393
+ ), # §2.1: 项目根目录(前端与 activeProjectPath 同口径比对,勿传 manifest 文件路径)
394
+ },
395
+ )
396
+ manifest = ProjectManifestV2.model_validate(read_yaml(Path(manifest_path)))
397
+
398
+ ref = next((s for s in manifest.schemas if s.id == table_id), None)
399
+ if not ref:
400
+ ref = _scan_schema_file(table_id, config_path)
401
+
402
+ if not ref:
403
+ return {"message": f"未找到 schema '{table_id}',无需删除。"}
404
+
405
+ try:
406
+ abs_schema_path = _resolve_project_path(config_path, ref.path)
407
+ except ValueError:
408
+ raise HTTPException(status_code=400, detail="非法的 Schema 文件路径")
409
+
410
+ with project_lock(config_path):
411
+ # §2.6: 引用检查抽公共函数(与 AI DELETE_SCHEMA 共用单一事实源);
412
+ # 消息格式与原三段检查逐段一致
413
+ refs = find_schema_references(config_path, table_id)
414
+ if refs["constraints"]:
415
+ raise HTTPException(
416
+ status_code=409,
417
+ detail=f"Schema '{table_id}' 仍被 constraint {', '.join(refs['constraints'])} 引用,请先删除引用",
418
+ )
419
+ if refs["regex"]:
420
+ raise HTTPException(
421
+ status_code=409,
422
+ detail=f"Schema '{table_id}' 仍被 regex {', '.join(refs['regex'])} 引用,请先删除引用",
423
+ )
424
+ if refs["transforms"]:
425
+ raise HTTPException(
426
+ status_code=409,
427
+ detail=f"Schema '{table_id}' 仍被 transform {', '.join(refs['transforms'])} 引用,请先删除引用",
428
+ )
429
+
430
+ try:
431
+ if os.path.isfile(abs_schema_path):
432
+ os.remove(abs_schema_path)
433
+ except Exception as e:
434
+ raise HTTPException(status_code=500, detail=f"删除 schema 文件失败: {e}")
435
+
436
+ # B-fix: 该引用原本存在于 manifest 中就写回(即使删除后列表为空)。
437
+ # 此前 `if manifest.schemas:` 在删除最后一个引用时为假而跳过写回,
438
+ # manifest 会留下指向已删除文件的悬空引用。
439
+ remaining_schemas = [s for s in manifest.schemas if s.id != table_id]
440
+ if len(remaining_schemas) != len(manifest.schemas):
441
+ manifest.schemas = remaining_schemas
442
+ write_yaml_atomic(Path(manifest_path), manifest.model_dump(exclude_none=True))
443
+
444
+ return {"message": f"V2 schema '{table_id}' 已删除。"}
445
+
446
+
447
+ @router.post(
448
+ "/schemas/{table_id}/display-name",
449
+ response_model=StandardResponse,
450
+ summary="更新 Schema 展示名",
451
+ responses={
452
+ 404: {"description": "Schema 不存在"},
453
+ 500: {"description": "服务器内部错误"},
454
+ },
455
+ )
456
+ def update_v2_schema_display_name(
457
+ table_id: str,
458
+ payload: DisplayNameUpdateRequest,
459
+ config_path: str = Depends(get_project_config_path),
460
+ ) -> dict[str, str]:
461
+ """
462
+ 更新 schema 的展示名(不改变 table_id 与文件路径)。
463
+
464
+ 设计意图:
465
+ - 允许用户给 Schema 设置友好的显示名称
466
+ - 不改变底层 ID 和文件路径,保持引用完整性
467
+
468
+ 副作用:
469
+ - 会触发两次文件写入(读取-修改-保存)
470
+
471
+ 数据流:
472
+ 1. 调用 get_v2_schema 获取当前 schema
473
+ 2. 修改 schema.name 为新名称
474
+ 3. 调用 put_v2_schema 保存更改
475
+
476
+ 参数:
477
+ table_id: Schema 对应的表 ID
478
+ payload: 新的展示名称
479
+ config_path: 项目配置根目录(通过 Depends 注入)
480
+
481
+ 返回:
482
+ StandardResponse: 操作结果消息
483
+ """
484
+ schema = get_v2_schema(table_id, config_path)
485
+ schema.name = payload.name
486
+ put_v2_schema(table_id, schema, config_path=config_path)
487
+ return {"message": f"V2 schema '{table_id}' 展示名已更新。"}
488
+
489
+
490
+ class ImplicitToExplicitBindingRequest(BaseModel):
491
+ """
492
+ 隐式转显式绑定请求模型。
493
+
494
+ 设计意图:
495
+ - 前端用户点击"锁定关联"按钮时,将列与正则模式的隐式运行时匹配转为显式配置绑定
496
+ - 显式绑定后,正则节点会参与级联渲染,并在配置文件中持久化
497
+
498
+ 字段说明:
499
+ - column_id: 目标列的 ID(或 name,后端会做兼容匹配)
500
+ - pattern_id: 要绑定的正则模式 ID
501
+ - pattern_registry: 模式所在的注册表名称,默认 expression_registry
502
+ """
503
+
504
+ column_id: str
505
+ pattern_id: str
506
+ pattern_registry: str = "expression_registry"
507
+
508
+
509
+ @router.post(
510
+ "/schemas/{table_id}/convert-to-explicit-binding",
511
+ response_model=StandardResponse,
512
+ summary="将隐式正则匹配转换为显式绑定",
513
+ responses={
514
+ 400: {"description": "请求参数错误"},
515
+ 404: {"description": "Schema 或列不存在"},
516
+ 500: {"description": "服务器内部错误"},
517
+ },
518
+ )
519
+ def convert_implicit_to_explicit_binding(
520
+ table_id: str,
521
+ payload: ImplicitToExplicitBindingRequest,
522
+ config_path: str = Depends(get_project_config_path),
523
+ ) -> dict[str, str]:
524
+ """
525
+ 将隐式正则匹配转换为显式绑定。
526
+
527
+ 设计意图:
528
+ - 用户在属性面板中点击"锁定关联"按钮时触发
529
+ - 将运行时动态解析的隐式匹配转换为配置文件中的显式依赖
530
+ - 转换后,正则节点将参与级联渲染
531
+
532
+ 业务逻辑:
533
+ 1. 读取当前 schema 文件
534
+ 2. 找到对应的列定义
535
+ 3. 将列的 type 从隐式转换为显式绑定
536
+ 4. 保存更新后的 schema 文件
537
+
538
+ 参数:
539
+ table_id: Schema 对应的表 ID
540
+ payload: 转换请求,包含列 ID 和目标正则模式 ID
541
+ config_path: 项目配置根目录
542
+
543
+ 返回:
544
+ StandardResponse: 操作结果消息
545
+ """
546
+ schema = get_v2_schema(table_id, config_path)
547
+
548
+ if not schema.columns:
549
+ raise HTTPException(status_code=404, detail="Schema has no columns")
550
+
551
+ target_column = None
552
+ for col in schema.columns:
553
+ if col.id == payload.column_id or col.name == payload.column_id:
554
+ target_column = col
555
+ break
556
+
557
+ if not target_column:
558
+ raise HTTPException(status_code=404, detail=f"Column '{payload.column_id}' not found")
559
+
560
+ # 更新列的绑定信息为显式 (Expr 字典)
561
+ target_column.type = {"name": "Expr", "registry": payload.pattern_registry, "pattern": payload.pattern_id}
562
+
563
+ # 调用通用的更新方法保存
564
+ put_v2_schema(table_id, schema, SaveMode.OVERWRITE, config_path)
565
+ return {
566
+ "message": f"Column '{payload.column_id}' converted to explicit binding with pattern '{payload.pattern_id}'"
567
+ }