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,325 @@
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 Template CRUD API - 可复用约束模板管理
18
+
19
+ 功能概述:
20
+ - 提供模板定义文件的增删改查
21
+ - 提供模板展开预览接口
22
+ - 读取时从 manifest.templates 定位文件
23
+
24
+ 架构设计:
25
+ - 读取时从 manifest.templates 中查找引用
26
+ - 写入时自动补齐 manifest 引用,默认路径为 templates/{id}.template.yaml
27
+ - 展开预览不写入文件,仅返回展开结果
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import logging
33
+ from pathlib import Path
34
+ from typing import Any
35
+
36
+ from fastapi import APIRouter, Depends, HTTPException
37
+ from pydantic import BaseModel, Field
38
+
39
+ from app.api.dependencies import get_project_config_path
40
+ from app.api.models.project import StandardResponse
41
+ from app.shared.core.io.yaml import read_yaml, write_yaml_atomic
42
+ from app.shared.core.project.template.expander import expand_template
43
+ from app.shared.core.project.template.reader import load_template, validate_template_structure
44
+ from app.shared.core.project.template.types import TemplateFile
45
+
46
+ from .helpers import _resolve_project_path, _v2_manifest_path, project_lock
47
+ from .manifest import get_v2_manifest
48
+
49
+ logger = logging.getLogger(__name__)
50
+
51
+ router = APIRouter(prefix="", tags=["Project-Template"])
52
+
53
+
54
+ def _find_template_path(template_id: str, config_path: str) -> Path | None:
55
+ """从 manifest 中查找模板文件路径"""
56
+ manifest = get_v2_manifest(config_path)
57
+ for ref in manifest.templates:
58
+ if ref.id == template_id:
59
+ return Path(_resolve_project_path(config_path, ref.path))
60
+ return None
61
+
62
+
63
+ # ============================================================================
64
+ # 请求/响应模型
65
+ # ============================================================================
66
+
67
+
68
+ class TemplateExpandRequest(BaseModel):
69
+ """模板展开预览请求"""
70
+
71
+ instance_id: str
72
+ params: dict[str, Any] = Field(default_factory=dict, description="参数绑定值(覆盖模板默认值)")
73
+ input_from_node: str | None = Field(None, description="实例级上游节点 ID(通常指向 Schema)")
74
+
75
+
76
+ class TemplateExpandResponse(BaseModel):
77
+ """模板展开预览响应"""
78
+
79
+ transforms: list[dict[str, Any]]
80
+ constraints: list[dict[str, Any]]
81
+ regex_nodes: list[dict[str, Any]]
82
+ manual_data: list[dict[str, Any]]
83
+ # 展开失败的节点清单(node_id/kind/message)——单节点失败不阻断预览,
84
+ # 其余节点正常返回,失败信息在此可见(默认空,向后兼容)
85
+ node_errors: list[dict[str, str]] = []
86
+
87
+
88
+ # ============================================================================
89
+ # CRUD 端点
90
+ # ============================================================================
91
+
92
+
93
+ @router.get(
94
+ "/template",
95
+ response_model=list[dict[str, Any]],
96
+ summary="列出所有模板定义",
97
+ responses={
98
+ 404: {"description": "Manifest 不存在"},
99
+ 500: {"description": "服务器内部错误"},
100
+ },
101
+ )
102
+ def list_templates(config_path: str = Depends(get_project_config_path)) -> list[dict[str, Any]]:
103
+ """列出所有模板定义"""
104
+ manifest = get_v2_manifest(config_path)
105
+ templates = []
106
+ for ref in manifest.templates:
107
+ tmpl_path = Path(_resolve_project_path(config_path, ref.path))
108
+ if tmpl_path.exists():
109
+ try:
110
+ tmpl = load_template(tmpl_path)
111
+ templates.append(
112
+ {
113
+ "id": tmpl.id,
114
+ "name": tmpl.name,
115
+ "description": tmpl.description,
116
+ "node_count": len(tmpl.nodes),
117
+ "path": ref.path,
118
+ }
119
+ )
120
+ except Exception as e:
121
+ logger.warning(f"模板 '{ref.id}' 加载失败: {e}")
122
+ return templates
123
+
124
+
125
+ @router.get(
126
+ "/template/{template_id}",
127
+ response_model=dict[str, Any],
128
+ summary="读取指定模板定义",
129
+ responses={
130
+ 404: {"description": "模板不存在"},
131
+ 500: {"description": "服务器内部错误"},
132
+ },
133
+ )
134
+ def get_template(template_id: str, config_path: str = Depends(get_project_config_path)) -> dict[str, Any]:
135
+ """读取指定模板定义"""
136
+ tmpl_path = _find_template_path(template_id, config_path)
137
+ if not tmpl_path or not tmpl_path.exists():
138
+ raise HTTPException(status_code=404, detail=f"模板 '{template_id}' 不存在")
139
+ tmpl = load_template(tmpl_path)
140
+ return tmpl.model_dump()
141
+
142
+
143
+ @router.post(
144
+ "/template",
145
+ response_model=StandardResponse,
146
+ summary="创建模板定义文件",
147
+ responses={
148
+ 400: {"description": "请求参数错误"},
149
+ 409: {"description": "模板文件已存在"},
150
+ 500: {"description": "服务器内部错误"},
151
+ },
152
+ )
153
+ def create_template(template_data: dict, config_path: str = Depends(get_project_config_path)) -> StandardResponse:
154
+ """创建模板定义文件"""
155
+ project_dir = Path(config_path)
156
+
157
+ # 验证数据
158
+ try:
159
+ tmpl = TemplateFile.model_validate(template_data)
160
+ validate_template_structure(tmpl)
161
+ except Exception as e:
162
+ raise HTTPException(status_code=400, detail=f"模板数据验证失败: {e}")
163
+
164
+ # 确保目录存在
165
+ templates_dir = project_dir / "templates"
166
+ templates_dir.mkdir(parents=True, exist_ok=True)
167
+
168
+ # 写入文件
169
+ # B-sec: 模板 id 直接拼路径且无格式约束,含 ../ 的 id 可将文件写出项目根之外
170
+ # (其余资源端点均有 _resolve_project_path 校验,此处对齐)。id 应为 UUID 等安全标识。
171
+ try:
172
+ file_path = Path(_resolve_project_path(config_path, f"templates/{tmpl.id}.template.yaml"))
173
+ except ValueError as e:
174
+ raise HTTPException(status_code=400, detail=f"模板 id 非法: {e}")
175
+ if file_path.exists():
176
+ raise HTTPException(status_code=409, detail=f"模板文件已存在: {file_path.name}")
177
+
178
+ write_yaml_atomic(file_path, tmpl.model_dump(exclude_none=True))
179
+
180
+ # 更新 manifest
181
+ manifest_path = Path(_v2_manifest_path(config_path))
182
+ with project_lock(config_path):
183
+ manifest_data = read_yaml(manifest_path)
184
+ templates_list = manifest_data.get("templates") or []
185
+ templates_list.append({"id": tmpl.id, "path": f"templates/{tmpl.id}.template.yaml"})
186
+ manifest_data["templates"] = templates_list
187
+ write_yaml_atomic(manifest_path, manifest_data)
188
+
189
+ return StandardResponse(success=True, message=f"模板 '{tmpl.id}' 创建成功")
190
+
191
+
192
+ @router.put(
193
+ "/template/{template_id}",
194
+ response_model=StandardResponse,
195
+ summary="更新模板定义文件",
196
+ responses={
197
+ 400: {"description": "请求参数错误"},
198
+ 404: {"description": "模板不存在"},
199
+ 500: {"description": "服务器内部错误"},
200
+ },
201
+ )
202
+ def update_template(
203
+ template_id: str,
204
+ template_data: dict,
205
+ config_path: str = Depends(get_project_config_path),
206
+ ) -> StandardResponse:
207
+ """
208
+ @methoddesc 更新模板定义文件
209
+
210
+ 业务用途:
211
+ - 覆盖写入 templates/{template_id}.template.yaml
212
+ - 模板不存在时返回 404
213
+
214
+ 参数:
215
+ template_id: 模板唯一标识
216
+ template_data: 模板新内容(任意字典)
217
+ config_path: 项目配置路径
218
+
219
+ 返回:
220
+ StandardResponse: 写入结果
221
+ """
222
+ tmpl_path = _find_template_path(template_id, config_path)
223
+ if not tmpl_path or not tmpl_path.exists():
224
+ raise HTTPException(status_code=404, detail=f"模板 '{template_id}' 不存在")
225
+
226
+ # 验证数据
227
+ try:
228
+ tmpl = TemplateFile.model_validate(template_data)
229
+ validate_template_structure(tmpl)
230
+ except Exception as e:
231
+ raise HTTPException(status_code=400, detail=f"模板数据验证失败: {e}")
232
+
233
+ write_yaml_atomic(tmpl_path, tmpl.model_dump(exclude_none=True))
234
+ return StandardResponse(success=True, message=f"模板 '{template_id}' 更新成功")
235
+
236
+
237
+ @router.delete(
238
+ "/template/{template_id}",
239
+ response_model=StandardResponse,
240
+ summary="删除模板定义文件",
241
+ responses={
242
+ 404: {"description": "模板不存在"},
243
+ 500: {"description": "服务器内部错误"},
244
+ },
245
+ )
246
+ def delete_template(template_id: str, config_path: str = Depends(get_project_config_path)) -> StandardResponse:
247
+ """删除模板定义文件"""
248
+ tmpl_path = _find_template_path(template_id, config_path)
249
+ if not tmpl_path or not tmpl_path.exists():
250
+ raise HTTPException(status_code=404, detail=f"模板 '{template_id}' 不存在")
251
+
252
+ with project_lock(config_path):
253
+ if tmpl_path and tmpl_path.exists():
254
+ tmpl_path.unlink()
255
+
256
+ manifest_path = Path(_v2_manifest_path(config_path))
257
+ manifest_data = read_yaml(manifest_path)
258
+ templates_list = manifest_data.get("templates") or []
259
+ manifest_data["templates"] = [t for t in templates_list if t.get("id") != template_id]
260
+ # 级联清理:删除模板定义后,仍指向它的实例引用会成为永久悬空引用
261
+ # (每次项目加载都产生 TemplateInstanceMissingTemplate 错误),随定义一并移除
262
+ instances_list = manifest_data.get("template_instances") or []
263
+ manifest_data["template_instances"] = [ti for ti in instances_list if ti.get("template_id") != template_id]
264
+ write_yaml_atomic(manifest_path, manifest_data)
265
+
266
+ return StandardResponse(success=True, message=f"模板 '{template_id}' 删除成功")
267
+
268
+
269
+ @router.post(
270
+ "/template/{template_id}/expand",
271
+ response_model=TemplateExpandResponse,
272
+ summary="预览模板展开结果",
273
+ responses={
274
+ 400: {"description": "请求参数错误或模板展开失败"},
275
+ 404: {"description": "模板不存在"},
276
+ 500: {"description": "服务器内部错误"},
277
+ },
278
+ )
279
+ def preview_template_expand(
280
+ template_id: str,
281
+ request: TemplateExpandRequest,
282
+ config_path: str = Depends(get_project_config_path),
283
+ ) -> TemplateExpandResponse:
284
+ """
285
+ @methoddesc 预览模板展开结果(不写入文件)
286
+
287
+ 业务用途:
288
+ - 在前端"展开前预览"流程中调用
289
+ - 调用后端 expandV2Template 服务计算展开后的 transforms/constraints/regex_nodes,但不落盘
290
+
291
+ 参数:
292
+ template_id: 模板唯一标识
293
+ request: 包含 instance_id 的请求
294
+ config_path: 项目配置路径
295
+
296
+ 返回:
297
+ TemplateExpandResponse: 展开结果预览
298
+ """
299
+ tmpl_path = _find_template_path(template_id, config_path)
300
+ if not tmpl_path or not tmpl_path.exists():
301
+ raise HTTPException(status_code=404, detail=f"模板 '{template_id}' 不存在")
302
+
303
+ tmpl = load_template(tmpl_path)
304
+
305
+ logger.debug(f"模板展开请求: template_id={template_id}, instance_id={request.instance_id}")
306
+ try:
307
+ node_errors: list[dict[str, str]] = []
308
+ transforms, constraints, regex_nodes, manual_data_files = expand_template(
309
+ tmpl,
310
+ request.instance_id,
311
+ params=request.params,
312
+ input_from_node=request.input_from_node,
313
+ errors=node_errors,
314
+ )
315
+ except ValueError as e:
316
+ logger.warning(f"模板展开 ValueError: {e}")
317
+ raise HTTPException(status_code=400, detail=str(e))
318
+
319
+ return TemplateExpandResponse(
320
+ transforms=[t.model_dump(exclude_none=True) for t in transforms],
321
+ constraints=[c.model_dump(exclude_none=True) for c in constraints],
322
+ regex_nodes=[r.model_dump(exclude_none=True) for r in regex_nodes],
323
+ manual_data=[md.model_dump(exclude_none=True) for md in manual_data_files],
324
+ node_errors=node_errors,
325
+ )
@@ -0,0 +1,227 @@
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 全量校验 API
18
+
19
+ 功能概述:
20
+ - 提供项目全量数据校验的 HTTP 入口
21
+ - 委托核心逻辑给 ValidationExecutor,与 CLI 共享执行层
22
+ - 响应构建下沉到 FullValidationResponseBuilder,保持路由层轻薄
23
+
24
+ 架构设计:
25
+ - API 层负责请求校验、参数转换与执行器调用
26
+ - ValidationExecutor 负责加载数据、执行格式检查与约束校验
27
+ - FullValidationResponseBuilder 负责把执行结果转换为 FullValidationResponse
28
+ """
29
+
30
+ import logging
31
+ import os
32
+ import time
33
+ from pathlib import Path
34
+
35
+ from fastapi import APIRouter, Depends, HTTPException
36
+
37
+ from app.api.dependencies import get_project_config_path
38
+ from app.api.services.full_validation_response_builder import FullValidationResponseBuilder
39
+ from app.shared.core.io.yaml import read_yaml
40
+ from app.shared.core.project.manifest.coverage import compute_manifest_coverage, coverage_to_api_dict
41
+ from app.shared.core.project.manifest.types import ProjectManifestV2
42
+ from app.shared.services.validation.executor import ValidationExecutor
43
+
44
+ from ...models import FullValidationRequest, FullValidationResponse
45
+ from .base import _v2_manifest_path
46
+
47
+ logger = logging.getLogger(__name__)
48
+
49
+ router = APIRouter(prefix="", tags=["Project-Validation"])
50
+
51
+
52
+ def _resolve_table_filter_from_file_path(config_path: str, file_path: str) -> str | list[str] | None:
53
+ """根据数据文件绝对路径反查所属 schema table id。
54
+
55
+ 遍历 manifest 的 schemas,解析每个 schema 的 source.path(相对 config_path),
56
+ 匹配到的 schema 的 id 作为校验过滤器。
57
+
58
+ Args:
59
+ config_path: 项目配置根目录
60
+ file_path: 数据文件绝对路径(已归一化)
61
+
62
+ Returns:
63
+ 匹配的 table id(单个返回 str,多个返回 list),无匹配返回 None
64
+ """
65
+ norm_target = os.path.normpath(os.path.abspath(file_path))
66
+ manifest_path = _v2_manifest_path(config_path)
67
+ manifest_data = read_yaml(Path(manifest_path))
68
+ matched: list[str] = []
69
+ for schema_ref in manifest_data.get("schemas", []) or []:
70
+ schema_id = schema_ref.get("id")
71
+ schema_rel = schema_ref.get("path")
72
+ if not schema_id or not schema_rel:
73
+ continue
74
+ schema_abs = os.path.normpath(os.path.join(config_path, schema_rel))
75
+ try:
76
+ schema_data = read_yaml(Path(schema_abs))
77
+ except Exception:
78
+ continue
79
+ source = schema_data.get("source") or {}
80
+ src_path = source.get("path")
81
+ if not src_path:
82
+ continue
83
+ norm_src = os.path.normpath(os.path.abspath(os.path.join(config_path, src_path)))
84
+ if norm_src == norm_target:
85
+ matched.append(schema_id)
86
+ if not matched:
87
+ return None
88
+ return matched[0] if len(matched) == 1 else matched
89
+
90
+
91
+ @router.post(
92
+ "/validate/full",
93
+ response_model=FullValidationResponse,
94
+ summary="执行项目全量数据校验",
95
+ responses={
96
+ 400: {"description": "请求参数错误"},
97
+ 404: {"description": "Manifest 不存在"},
98
+ 422: {"description": "Manifest 损坏或无法解析"},
99
+ 500: {"description": "服务器内部错误"},
100
+ },
101
+ )
102
+ def validate_v2_full(
103
+ request: FullValidationRequest,
104
+ config_path: str = Depends(get_project_config_path),
105
+ ) -> FullValidationResponse:
106
+ """
107
+ 执行项目全量数据校验。
108
+
109
+ 参数:
110
+ request: 校验请求,包含可选的配置覆盖
111
+ config_path: 项目配置根目录(通过 Depends 注入)
112
+
113
+ 返回:
114
+ FullValidationResponse: 校验结果
115
+ """
116
+ started = time.monotonic()
117
+ logger.info(f"[validate_v2_full] 收到请求,config_path: {config_path}")
118
+ manifest_path = _v2_manifest_path(config_path)
119
+ logger.info(f"[validate_v2_full] manifest_path: {manifest_path}")
120
+ if not os.path.isfile(manifest_path):
121
+ raise HTTPException(
122
+ status_code=404,
123
+ detail={
124
+ # §2.1: 结构化错误码统一(与 dependencies.py 的 manifest 缺失出口同构),
125
+ # 前端按 detail.code 识别项目失效并触发自愈
126
+ "code": "PROJECT_NOT_FOUND",
127
+ "message": f"项目配置文件(project.precis.yaml)不存在,请确认项目是否已被移动或删除(配置目录: {manifest_path})",
128
+ "path": str(
129
+ config_path
130
+ ), # §2.1: 项目根目录(前端与 activeProjectPath 同口径比对,勿传 manifest 文件路径)
131
+ },
132
+ )
133
+
134
+ # 清单读取与校验必须先于 coverage 计算独立失败(422,与 GET /manifest 同口径):
135
+ # 原实现把清单损坏吞进 coverage 的 try/except 继续执行,返回 summary 全零的
136
+ # 200 空成功,前端只看 summary 会把"清单损坏"误读为"校验通过零错误"。
137
+ try:
138
+ manifest_data = read_yaml(Path(manifest_path))
139
+ manifest_model = ProjectManifestV2.model_validate(manifest_data)
140
+ except Exception as e:
141
+ logger.error(f"[validate_v2_full] 项目清单损坏: {e}")
142
+ raise HTTPException(
143
+ status_code=422,
144
+ detail=f"项目清单文件损坏或为空,无法解析: {manifest_path}。请修复或回退 project.precis.yaml 后重试。({e})",
145
+ ) from e
146
+
147
+ coverage_payload = None
148
+ try:
149
+ coverage_payload = coverage_to_api_dict(compute_manifest_coverage(config_path, manifest_model))
150
+ except Exception as e:
151
+ logger.warning(f"[validate_v2_full] 计算 coverage 失败: {e}")
152
+
153
+ # 初始化执行器
154
+ try:
155
+ override_settings = None
156
+ if request.options and request.options.override_settings:
157
+ override_settings = request.options.override_settings
158
+ executor = ValidationExecutor(manifest_path, override_settings)
159
+ except Exception as e:
160
+ logger.error(f"[validate_v2_full] 初始化执行器失败: {e}")
161
+ return FullValidationResponseBuilder(
162
+ executor=None,
163
+ started=started,
164
+ coverage=coverage_payload,
165
+ ).build_error(e)
166
+
167
+ builder = FullValidationResponseBuilder(
168
+ executor=executor,
169
+ started=started,
170
+ coverage=coverage_payload,
171
+ )
172
+
173
+ # 确定数据目录
174
+ data_directory = config_path
175
+ if request.options and request.options.data_directory:
176
+ data_directory = request.options.data_directory
177
+
178
+ # 校验目标解析
179
+ table_filter = None
180
+ if request.target:
181
+ if request.target.type == "single_file":
182
+ file_path = (request.target.file_path or "").strip()
183
+ if not file_path:
184
+ raise HTTPException(status_code=400, detail="单文件校验缺少 file_path")
185
+ # 若传入相对路径,基于 config_path 解析为绝对路径
186
+ if not os.path.isabs(file_path):
187
+ file_path = os.path.normpath(os.path.join(config_path, file_path))
188
+ if not os.path.isfile(file_path):
189
+ raise HTTPException(status_code=404, detail=f"数据文件不存在: {file_path}")
190
+ table_filter = _resolve_table_filter_from_file_path(config_path, file_path)
191
+ if table_filter is None:
192
+ raise HTTPException(
193
+ status_code=404,
194
+ detail=f"未找到引用该文件的 schema: {file_path}",
195
+ )
196
+ if request.target.type == "single_table":
197
+ table_filter = (request.target.table_id or "").strip()
198
+ if not table_filter:
199
+ raise HTTPException(status_code=400, detail="单表校验缺少 table_id")
200
+
201
+ # 执行选项
202
+ validation_settings = executor.settings.validation
203
+ timeout_seconds = int(validation_settings.timeout_seconds or 0)
204
+ if timeout_seconds <= 0:
205
+ timeout_seconds = 30
206
+
207
+ from app.shared.services.validation.executor import ValidationOptions
208
+
209
+ options = ValidationOptions(
210
+ timeout_seconds=timeout_seconds,
211
+ error_handling=validation_settings.error_handling or "continue",
212
+ strict_mode=validation_settings.strict_mode or False,
213
+ table_filter=table_filter,
214
+ allow_unsafe_eval=request.options.allow_unsafe_eval if request.options else None,
215
+ )
216
+
217
+ logger.info(f"[validate_v2_full] 开始执行校验,数据目录: {data_directory}")
218
+
219
+ # 执行校验并构建响应
220
+ try:
221
+ result = executor.execute(data_directory, options)
222
+ except Exception as e:
223
+ logger.error(f"[validate_v2_full] 校验执行过程中发生异常: {e}", exc_info=True)
224
+ files_total = len(executor.dataset_schema.tables) if executor.dataset_schema else 0
225
+ return builder.build_error(e, files_total=files_total)
226
+
227
+ return builder.build_from_result(result)