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,139 @@
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
+ """
17
+ @fileoverview 表结构写入模块
18
+
19
+ 功能概述:
20
+ - 写入 *.schema.yaml 文件
21
+ - 优化字段顺序,提升 YAML 可读性
22
+
23
+ 架构设计:
24
+ - 唯一入口: 封装 Pydantic 模型到 YAML 文件的转换逻辑
25
+ - 字段排序: 按重要程度分组(核心 / 可选 / 内部)
26
+ - 懒加载: ensure_schema_file 提供内存中创建或获取 Schema 对象的能力
27
+
28
+ 输入示例:
29
+ schema = TableSchemaFile(
30
+ version=2,
31
+ id="users",
32
+ name="users",
33
+ source=SourceSpec(mode="relative_file", path="data/users.xlsx"),
34
+ columns=[ColumnSpec(id="user_id", name="user_id", type="string")],
35
+ )
36
+
37
+ 输出示例:
38
+ save_schema(schema, "schemas/users.schema.yaml")
39
+ """
40
+
41
+ from __future__ import annotations
42
+
43
+ from pathlib import Path
44
+ from typing import TYPE_CHECKING
45
+
46
+ from app.shared.core.io.yaml import write_yaml_atomic
47
+
48
+ from .types import TableSchemaFile
49
+
50
+ if TYPE_CHECKING:
51
+ from .types import TableSchemaFile
52
+
53
+
54
+ def save_schema(schema: TableSchemaFile, schema_path: str | Path) -> None:
55
+ """
56
+ @methoddesc 保存表结构到 YAML 文件。
57
+
58
+ 字段顺序优化(按重要程度分组):
59
+ L1(核心):name, source, columns
60
+ L2(可选):primary_key(通过 columns 内字段)
61
+ L3(内部):version, id, _internal
62
+
63
+ 处理流程:
64
+ 1. 将 TableSchemaFile 对象序列化为字典(model_dump)
65
+ 2. 按重要程度重新组织字段顺序
66
+ 3. 从 id 提取 sheet 名并写入 source(如果 source 存在但 sheet 为空)
67
+ 4. 写入 YAML 文件
68
+
69
+ :param schema: TableSchemaFile 对象,包含表的完整结构定义
70
+ :param schema_path: 保存路径,可以是相对路径或绝对路径
71
+ :raises IOError: 文件写入失败时抛出
72
+ """
73
+ # 步骤1:将 Pydantic 模型序列化为字典(exclude_none 与其余 writer 对齐)
74
+ data = schema.model_dump(exclude_none=True)
75
+
76
+ # 步骤2:按重要程度重排字段,剩余字段全量保留。
77
+ # 回归: 原固定白名单只挑 6 个键,会把 TableSchemaFile 的 sheet/constraints/
78
+ # script_checks 等字段在保存时静默丢弃(内嵌约束 roundtrip 即消失)——
79
+ # 与 regex/writer 的"全量保留+重排"修复模式对齐,新增字段不再被白名单吞掉。
80
+ ordered: dict = {}
81
+ for key in ("name", "source", "columns", "version", "id"):
82
+ if key in data:
83
+ ordered[key] = data.pop(key)
84
+ ordered.update(data)
85
+
86
+ # 步骤4:调用底层 YAML 写入工具
87
+ write_yaml_atomic(Path(schema_path), ordered)
88
+
89
+
90
+ def ensure_schema_file(
91
+ schema_files: dict[str, TableSchemaFile],
92
+ table_id: str,
93
+ name: str,
94
+ columns: list | None = None,
95
+ source: dict | None = None,
96
+ ) -> TableSchemaFile:
97
+ """
98
+ @methoddesc 确保 schema_files 字典中包含指定 table_id 的 schema。
99
+ 如果不存在则创建新的 TableSchemaFile。
100
+
101
+ 处理流程:
102
+ 1. 检查 schema_files 字典中是否已存在对应 table_id 的 Schema
103
+ 2. 如存在,直接返回现有对象(复用)
104
+ 3. 如不存在,创建新的 TableSchemaFile 并存入字典
105
+
106
+ 使用场景:
107
+ - 项目初始化时批量创建 Schema
108
+ - 动态添加新的表结构
109
+ - 避免重复创建相同的 Schema 对象
110
+
111
+ :param schema_files: schema 文件字典,key 为 table_id,value 为 TableSchemaFile
112
+ :param table_id: 表 ID,作为字典的 key 使用
113
+ :param name: 表名,用于展示和校验
114
+ :param columns: 列定义列表,每个元素为列配置的字典
115
+ :param source: 数据源描述字典,包含 mode、path 等信息
116
+ :return: TableSchemaFile 对象(已存在或新创建)
117
+ """
118
+ # 步骤1:检查是否已存在对应 table_id 的 Schema
119
+ # 优先复用已有对象,避免重复创建
120
+ if table_id in schema_files:
121
+ return schema_files[table_id]
122
+
123
+ # 步骤2:导入必要的类型,用于构建对象
124
+ from .types import ColumnSpec, SourceSpec, TableSchemaFile
125
+
126
+ # 步骤3:构建新的 TableSchemaFile 对象
127
+ # 使用字典解包方式创建列定义列表
128
+ schema = TableSchemaFile.model_construct(
129
+ id=table_id,
130
+ name=name,
131
+ # 将列配置字典列表转换为 ColumnSpec 对象列表
132
+ columns=[ColumnSpec(**col) for col in (columns or [])],
133
+ # 将源配置字典转换为 SourceSpec 对象
134
+ source=SourceSpec(**source) if source else None,
135
+ )
136
+
137
+ # 步骤4:将新创建的 Schema 存入字典并返回
138
+ schema_files[table_id] = schema
139
+ return schema
@@ -0,0 +1,95 @@
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
+ """@fileoverview Schema 引用扫描(REST 删除守卫与 AI DELETE_SCHEMA 共用,§2.6)
17
+
18
+ 删除 Schema 前扫描 constraints/regex/transforms 三类实体对该表的引用,
19
+ 返回引用清单供调用方决定拒绝(REST 409 / AI 失败消息)——单一事实源防两路再漂移。
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import os
25
+ from pathlib import Path
26
+ from typing import Any
27
+
28
+ from app.shared.core.io.yaml import read_yaml
29
+ from app.shared.core.project.manifest.reader import load_manifest
30
+
31
+
32
+ def find_schema_references(workspace_path: str, table_id: str) -> dict[str, list[str]]:
33
+ """扫描引用指定 schema 的实体。
34
+
35
+ Args:
36
+ workspace_path: 项目根目录(含 project.precis.yaml)
37
+ table_id: 目标表 ID
38
+
39
+ Returns:
40
+ {"constraints": [引用方 id...], "regex": [...], "transforms": [...]}
41
+ 单个引用文件读取失败时跳过(容错与原 REST 守卫一致,不因损坏文件阻断删除)。
42
+ """
43
+ result: dict[str, list[str]] = {"constraints": [], "regex": [], "transforms": []}
44
+ manifest_path = Path(workspace_path) / "project.precis.yaml"
45
+ if not manifest_path.exists():
46
+ return result
47
+
48
+ manifest = load_manifest(manifest_path)
49
+
50
+ for c_ref in manifest.constraints:
51
+ try:
52
+ c_path = os.path.join(workspace_path, c_ref.path)
53
+ if os.path.isfile(c_path):
54
+ refs_data = (read_yaml(Path(c_path)) or {}).get("refs", {})
55
+ if (
56
+ refs_data.get("table_id") == table_id
57
+ or refs_data.get("from_table_id") == table_id
58
+ or refs_data.get("to_table_id") == table_id
59
+ ):
60
+ result["constraints"].append(c_ref.id)
61
+ except Exception:
62
+ continue
63
+
64
+ for r_ref in manifest.regex_nodes or []:
65
+ try:
66
+ r_path = os.path.join(workspace_path, r_ref.path)
67
+ if os.path.isfile(r_path):
68
+ source_ref = (read_yaml(Path(r_path)) or {}).get("source_ref", {})
69
+ if source_ref.get("table_id") == table_id:
70
+ result["regex"].append(r_ref.id)
71
+ except Exception:
72
+ continue
73
+
74
+ for t_ref in manifest.transforms or []:
75
+ try:
76
+ t_path = os.path.join(workspace_path, t_ref.path)
77
+ if os.path.isfile(t_path):
78
+ t_data: dict[str, Any] = read_yaml(Path(t_path)) or {}
79
+ if t_data.get("input_from_node") == table_id:
80
+ result["transforms"].append(t_ref.id)
81
+ except Exception:
82
+ continue
83
+
84
+ return result
85
+
86
+
87
+ def format_reference_report(refs: dict[str, list[str]]) -> str:
88
+ """把引用清单格式化为用户可读的单行报告(空清单返回空串)。"""
89
+ parts = []
90
+ label_map = [("constraints", "constraint"), ("regex", "regex"), ("transforms", "transform")]
91
+ for key, label in label_map:
92
+ ids = refs.get(key) or []
93
+ if ids:
94
+ parts.append(f"{label} {', '.join(ids)}")
95
+ return "、".join(parts)
@@ -0,0 +1,27 @@
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
+ """
17
+ @fileoverview 可复用约束模板模块
18
+
19
+ 功能概述:
20
+ - 定义模板数据模型 (TemplateFile)
21
+ - 提供模板 YAML 读取器 (reader)
22
+ - 提供模板展开器 (expander)
23
+
24
+ 架构设计:
25
+ - 模板是配置层概念,在项目加载阶段展开为标准 TransformFile/ConstraintFile/RegexNodeFile
26
+ - 校验引擎(DAG Builder + Validator)无需任何修改
27
+ """
@@ -0,0 +1,263 @@
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
+ """
17
+ @fileoverview 模板展开器
18
+
19
+ 功能概述:
20
+ - 将模板定义展开为标准的 TransformFile / ConstraintFile / RegexNodeFile / ManualDataFile
21
+ - 处理 ID 命名空间映射 ({instance_id}__{local_id})
22
+ - 处理 input_from_node 内部引用映射
23
+ - 支持 {{param}} 占位符替换(实例参数覆盖模板默认值)
24
+
25
+ 架构设计:
26
+ - 展开在项目加载阶段完成(config-time expansion)
27
+ - 校验引擎(DAG Builder / Validator)看到的永远是展开后的标准文件
28
+ - 展开结果直接合并到 transform_files / constraint_files / regex_files / manual_data_files
29
+ - 参数替换: 先用模板声明的默认值构建 baseline params,再用实例 params 覆盖,
30
+ 最后对节点值中的 {{param}} 占位符做替换
31
+
32
+ 输入示例:
33
+ expand_template(
34
+ template=TemplateFile(id="age_check", parameters=[...], nodes=[...]),
35
+ instance_id="users_age_check",
36
+ params={"min_age": 18, "max_age": 100},
37
+ input_from_node="users-users",
38
+ )
39
+
40
+ 输出示例:
41
+ (
42
+ [TransformFile(id="users_age_check__extract", type="Substring", ...)],
43
+ [ConstraintFile(id="users_age_check__check", type="Range", params={"min":18,"max":100})],
44
+ [],
45
+ [],
46
+ )
47
+ """
48
+
49
+ from __future__ import annotations
50
+
51
+ import logging
52
+ import re
53
+ from typing import Any, cast
54
+
55
+ from app.shared.core.project.constraint.types.constraint_file import ConstraintFile
56
+ from app.shared.core.project.manual_data.types import ManualDataFile
57
+ from app.shared.core.project.regex.types import RegexNodeFile
58
+ from app.shared.core.project.transform.types import TransformFile
59
+
60
+ from .types import TemplateFile
61
+
62
+ logger = logging.getLogger(__name__)
63
+
64
+ # {{param}} 占位符正则
65
+ _PLACEHOLDER_RE = re.compile(r"\{\{\s*(\w+)\s*\}\}")
66
+
67
+
68
+ def _build_effective_params(
69
+ template: TemplateFile,
70
+ params: dict[str, Any] | None,
71
+ input_from_node: str | None,
72
+ ) -> dict[str, Any]:
73
+ """合并模板默认参数 + 实例参数,构造最终替换值表。
74
+
75
+ 优先级: 实例 params > 模板 parameter.default。input_from_node 单独注入。
76
+ """
77
+ effective: dict[str, Any] = {}
78
+ for decl in template.parameters:
79
+ effective[decl.name] = decl.default
80
+ if params:
81
+ effective.update(params)
82
+ # input_from_node 作为特殊参数注入(供 {{input_from_node}} 占位符使用)
83
+ if input_from_node is not None:
84
+ effective["input_from_node"] = input_from_node
85
+ return effective
86
+
87
+
88
+ def _substitute(value: Any, params: dict[str, Any]) -> Any:
89
+ """递归替换 value 中的 {{param}} 占位符。
90
+
91
+ - 字符串: 替换占位符;若整串就是单个占位符,返回原始类型(int/str 等)
92
+ - dict/list: 递归处理
93
+ - 其他类型: 原样返回
94
+ """
95
+ if isinstance(value, str):
96
+ # 整串恰好是单个占位符 → 返回原始类型值(保留 int 等)
97
+ single = _PLACEHOLDER_RE.fullmatch(value.strip())
98
+ if single and single.group(1) in params:
99
+ return params[single.group(1)]
100
+
101
+ # 否则做子串替换(结果始终为字符串)
102
+ def _repl(m: re.Match[str]) -> str:
103
+ key = m.group(1)
104
+ return str(params.get(key, m.group(0)))
105
+
106
+ return _PLACEHOLDER_RE.sub(_repl, value)
107
+ if isinstance(value, dict):
108
+ return {k: _substitute(v, params) for k, v in value.items()}
109
+ if isinstance(value, list):
110
+ return [_substitute(v, params) for v in value]
111
+ return value
112
+
113
+
114
+ def expand_template(
115
+ template: TemplateFile,
116
+ instance_id: str,
117
+ params: dict[str, Any] | None = None,
118
+ input_from_node: str | None = None,
119
+ errors: list[dict[str, str]] | None = None,
120
+ ) -> tuple[list[TransformFile], list[ConstraintFile], list[RegexNodeFile], list[ManualDataFile]]:
121
+ """@methoddesc 展开模板实例为标准配置文件
122
+
123
+ 参数:
124
+ template: 模板定义
125
+ instance_id: 实例 ID(全局唯一)
126
+ params: 实例参数绑定值(覆盖模板默认值),默认 None
127
+ input_from_node: 实例级上游节点 ID(通常指向 Schema),默认 None
128
+ errors: 可选,节点级展开错误收集列表——单节点展开失败不中断其余节点,
129
+ 失败信息以 {"node_id", "kind", "message"} 追加到此列表供调用方上报
130
+ (不传时失败节点仅记日志,保持旧行为)
131
+
132
+ 返回:
133
+ (transforms, constraints, regex_nodes, manual_data_files) 四元组
134
+ """
135
+ # 构建最终替换值表(默认值 ← 实例 params ← input_from_node)
136
+ effective_params = _build_effective_params(template, params, input_from_node)
137
+
138
+ # 步骤 1:构建 ID 命名空间映射 {local_id → global_id}
139
+ id_map = {}
140
+ for node in template.nodes:
141
+ id_map[node.id] = f"{instance_id}__{node.id}"
142
+
143
+ # 步骤 2:展开各节点
144
+ transforms: list[TransformFile] = []
145
+ constraints: list[ConstraintFile] = []
146
+ regex_nodes: list[RegexNodeFile] = []
147
+ manual_data_files: list[ManualDataFile] = []
148
+
149
+ for node in template.nodes:
150
+ if not node.enabled:
151
+ continue
152
+
153
+ try:
154
+ global_id = id_map[node.id]
155
+
156
+ # 参数替换:对节点各字段中的 {{param}} 占位符做替换
157
+ substituted_input = _substitute(node.input_from_node, effective_params)
158
+ substituted_input_column = _substitute(node.input_column, effective_params)
159
+ substituted_params = _substitute(node.params, effective_params)
160
+ substituted_refs = _substitute(node.refs, effective_params)
161
+ substituted_output_columns = _substitute(node.output_columns, effective_params)
162
+ substituted_column_name = _substitute(node.column_name, effective_params)
163
+
164
+ # 解析 input_from_node(替换后的值)
165
+ effective_input = _resolve_input_from_node(substituted_input, id_map)
166
+
167
+ # 根据类型创建对应文件
168
+ if node.kind == "transform":
169
+ tf = TransformFile(
170
+ version=2,
171
+ id=global_id,
172
+ type=cast(Any, node.type),
173
+ enabled=True,
174
+ description=node.description,
175
+ input_from_node=effective_input,
176
+ input_column=substituted_input_column,
177
+ params=substituted_params,
178
+ output_columns=substituted_output_columns,
179
+ )
180
+ transforms.append(tf)
181
+
182
+ elif node.kind == "constraint":
183
+ cf = ConstraintFile(
184
+ version=2,
185
+ id=global_id,
186
+ type=cast(Any, node.type),
187
+ enabled=True,
188
+ description=node.description,
189
+ refs=substituted_refs,
190
+ params=substituted_params,
191
+ input_from_node=effective_input,
192
+ input_column=substituted_input_column,
193
+ )
194
+ constraints.append(cf)
195
+
196
+ elif node.kind == "regex":
197
+ rf = RegexNodeFile(
198
+ version=2,
199
+ id=global_id,
200
+ name=node.description or global_id,
201
+ description=node.description,
202
+ pattern=substituted_params.get("pattern", "") if isinstance(substituted_params, dict) else "",
203
+ uses_pattern=None,
204
+ match_mode="full",
205
+ case_sensitive=False,
206
+ flags="",
207
+ enabled=True,
208
+ input_from_node=effective_input,
209
+ input_column=substituted_input_column,
210
+ output_columns=substituted_output_columns,
211
+ source_ref=None,
212
+ source_column_name=None,
213
+ )
214
+ regex_nodes.append(rf)
215
+
216
+ elif node.kind == "manualData":
217
+ mdf = ManualDataFile(
218
+ version=2,
219
+ id=global_id,
220
+ column_name=substituted_column_name or "Column1",
221
+ column_data_type=cast(Any, node.column_data_type or "string"),
222
+ rows=node.rows,
223
+ enabled=True,
224
+ description=node.description,
225
+ input_from_node=effective_input,
226
+ )
227
+ manual_data_files.append(mdf)
228
+
229
+ except Exception as e:
230
+ logger.warning(f"模板 '{template.id}' 的节点 '{node.id}' 展开失败: {e}")
231
+ # 节点级失败上报出口:失败节点被跳过但不再不可见——调用方
232
+ # (loader)据此生成 loading error,UI/CLI 均可见该约束缺失
233
+ if errors is not None:
234
+ errors.append({"node_id": node.id, "kind": node.kind, "message": str(e)})
235
+ continue
236
+
237
+ logger.debug(
238
+ f"模板 '{template.id}' 实例 '{instance_id}' 展开完成: "
239
+ f"{len(transforms)} transforms, {len(constraints)} constraints, "
240
+ f"{len(regex_nodes)} regex, {len(manual_data_files)} manual_data"
241
+ )
242
+ return transforms, constraints, regex_nodes, manual_data_files
243
+
244
+
245
+ def _resolve_input_from_node(
246
+ raw_input: str | None,
247
+ id_map: dict[str, str],
248
+ ) -> str | None:
249
+ """解析节点的 input_from_node 字段
250
+
251
+ 两种情况:
252
+ - None: 无上游
253
+ - 模板内部引用: 映射为全局 ID
254
+ """
255
+ if raw_input is None:
256
+ return None
257
+
258
+ # 检查是否是模板内部引用
259
+ if raw_input in id_map:
260
+ return id_map[raw_input]
261
+
262
+ # 可能本身就是一个外部节点 ID(直接引用 schema 等)
263
+ return raw_input
@@ -0,0 +1,120 @@
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
+ """
17
+ @fileoverview 模板配置文件读取模块
18
+
19
+ 功能概述:
20
+ - 读取并解析 *.template.yaml 配置文件
21
+ - 使用 Pydantic 模型进行数据验证
22
+ - 校验模板结构合法性(至少 1 个 manualData + 1 个 constraint)
23
+
24
+ 输入示例:
25
+ template_path = Path("/path/to/project/templates/age_check.template.yaml")
26
+
27
+ # 文件内容:
28
+ # version: 2
29
+ # id: age_check
30
+ # name: 年龄校验
31
+ # nodes:
32
+ # - id: md1
33
+ # kind: manualData
34
+ # type: ManualData
35
+ # column_name: age
36
+ # rows: [["18"]]
37
+ # - id: check_range
38
+ # kind: constraint
39
+ # type: Range
40
+ # input_from_node: md1
41
+ # params:
42
+ # min_value: 0
43
+ # max_value: 120
44
+
45
+ 输出示例:
46
+ TemplateFile(
47
+ id="age_check",
48
+ name="年龄校验",
49
+ nodes=[TemplateNode(id="md1", ...), TemplateNode(id="check_range", ...)],
50
+ )
51
+ """
52
+
53
+ from __future__ import annotations
54
+
55
+ from pathlib import Path
56
+
57
+ import yaml
58
+
59
+ from .types import TemplateFile
60
+
61
+
62
+ def validate_template_structure(template: TemplateFile) -> None:
63
+ """@methoddesc 校验模板结构合法性
64
+
65
+ 模板必须满足以下条件:
66
+ - 至少 1 个 manualData 节点(作为输入起点)
67
+ - 至少 1 个 constraint 节点(作为校验终点)
68
+ - 节点 ID 全局唯一
69
+ - 非 manualData 节点的 input_from_node 必须指向模板内部节点
70
+
71
+ 参数:
72
+ template: 模板定义
73
+
74
+ 异常:
75
+ ValueError: 模板结构不合法
76
+ """
77
+ enabled_nodes = [n for n in template.nodes if n.enabled]
78
+ has_manual_data = any(n.kind == "manualData" for n in enabled_nodes)
79
+ has_constraint = any(n.kind == "constraint" for n in enabled_nodes)
80
+
81
+ if not has_manual_data:
82
+ raise ValueError(f"模板 '{template.id}' 结构不合法:至少需要 1 个启用的 manualData 节点作为输入起点")
83
+ if not has_constraint:
84
+ raise ValueError(f"模板 '{template.id}' 结构不合法:至少需要 1 个启用的 constraint 节点作为校验终点")
85
+
86
+ # 节点 ID 唯一性校验
87
+ seen_ids: set[str] = set()
88
+ duplicate_ids: set[str] = set()
89
+ for node in template.nodes:
90
+ if node.id in seen_ids:
91
+ duplicate_ids.add(node.id)
92
+ seen_ids.add(node.id)
93
+ if duplicate_ids:
94
+ raise ValueError(f"模板 '{template.id}' 结构不合法:存在重复节点 ID {sorted(duplicate_ids)}")
95
+
96
+ # 连通性校验:非 manualData 节点只能引用模板内部节点
97
+ enabled_node_ids = {n.id for n in enabled_nodes}
98
+ for node in enabled_nodes:
99
+ if node.kind == "manualData":
100
+ continue
101
+ input_from = node.input_from_node
102
+ if input_from is not None and input_from not in enabled_node_ids:
103
+ raise ValueError(
104
+ f"模板 '{template.id}' 结构不合法:节点 '{node.id}' 的 input_from_node "
105
+ f"指向模板外部节点 '{input_from}',模板必须自包含"
106
+ )
107
+
108
+
109
+ def load_template(template_path: Path) -> TemplateFile:
110
+ """@methoddesc 加载模板配置文件
111
+
112
+ 参数:
113
+ template_path: 模板文件路径
114
+
115
+ 返回:
116
+ TemplateFile 对象
117
+ """
118
+ with open(template_path, encoding="utf-8") as f:
119
+ raw = yaml.safe_load(f) or {}
120
+ return TemplateFile.model_validate(raw)