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,266 @@
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
+ - 在格式解析后、约束校验前提取派生列(Extracted 类型列)
21
+ - 通过正则表达式的命名捕获组从源列提取数据
22
+ - 支持向量化提取和类型转换(Integer、Float、Boolean、String)
23
+
24
+ 架构设计:
25
+ - 两阶段处理:格式解析 → 提取派生列 → 约束校验
26
+ - 正则驱动:从 schema 中读取正则模式,使用 pandas str.extract 向量化提取
27
+ - 错误上报:提取失败作为验证错误返回,避免静默通过
28
+
29
+ 输入示例:
30
+ _extract_derived_columns(
31
+ parsed_datasets={"users": df},
32
+ schema=DataSetSchema(...),
33
+ raw_datasets={"users": raw_df},
34
+ all_errors=[]
35
+ )
36
+
37
+ 输出示例:
38
+ parsed_datasets 被就地修改,新增提取列
39
+ all_errors 可能追加提取失败错误
40
+ """
41
+
42
+ import logging
43
+ from typing import cast
44
+
45
+ import pandas as pd
46
+
47
+ from app.shared.domain.dataset_schema import DataSetSchema
48
+
49
+ logger = logging.getLogger(__name__)
50
+
51
+
52
+ # Extracted 类型列需要的属性(使用 Protocol 避免直接依赖 domain 子类)
53
+ class _ExtractedTypeProtocol:
54
+ """仅用于类型检查的 Extracted 数据类型协议。"""
55
+
56
+ name: str
57
+ source_column: str
58
+ extract_key: str
59
+ result_type: str | None
60
+
61
+
62
+ def _extract_derived_columns(
63
+ parsed_datasets: dict[str, pd.DataFrame],
64
+ schema: DataSetSchema,
65
+ raw_datasets: dict[str, pd.DataFrame],
66
+ all_errors: list[dict],
67
+ ) -> None:
68
+ """
69
+ @methoddesc 提取派生列(Extracted 类型列)
70
+
71
+ 在格式解析后、执行约束校验前,从 Extracted 类型的列中通过正则提取派生列。
72
+ Extracted 类型标识该列是从另一个列的正则匹配结果中提取的。
73
+
74
+ 参数:
75
+ parsed_datasets: 解析后的 DataFrame 字典(会被就地修改)
76
+ schema: 完整的数据集 Schema
77
+ raw_datasets: 原始 DataFrame 字典(用于保留原始字符串格式)
78
+ all_errors: 错误列表(提取失败时会追加错误)
79
+ """
80
+ import re
81
+
82
+ from app.shared.domain.regex_flags import parse_regex_flags
83
+
84
+ for table_id, parsed_df in parsed_datasets.items():
85
+ logger.debug(f"Processing extracted columns for table: {table_id}")
86
+ if table_id not in schema.tables:
87
+ continue
88
+
89
+ table_schema = schema.tables[table_id]
90
+
91
+ # 查找所有 Extracted 类型的列
92
+ extracted_columns = []
93
+ for col in table_schema.columns.values():
94
+ data_type = col.data_type
95
+ logger.debug(f"Column {col.name} has data_type: {data_type}")
96
+ if getattr(data_type, "name", None) == "Extracted":
97
+ extracted_type = cast(_ExtractedTypeProtocol, data_type)
98
+ extracted_columns.append(
99
+ {
100
+ "column_name": col.name,
101
+ "source_column": extracted_type.source_column,
102
+ "extract_key": extracted_type.extract_key,
103
+ "result_type": getattr(extracted_type, "result_type", None),
104
+ }
105
+ )
106
+
107
+ if not extracted_columns:
108
+ continue
109
+
110
+ logger.debug(f"表 '{table_id}' 发现 {len(extracted_columns)} 个提取列")
111
+
112
+ for ext_info in extracted_columns:
113
+ col_name = ext_info["column_name"]
114
+ source_column = ext_info["source_column"]
115
+ if not isinstance(source_column, str):
116
+ continue
117
+ extract_key = ext_info["extract_key"]
118
+ result_type = ext_info.get("result_type")
119
+
120
+ if source_column not in parsed_df.columns:
121
+ all_errors.append(
122
+ {
123
+ "stage": "format",
124
+ "table": table_id,
125
+ "column": col_name,
126
+ "check_type": "ExtractedColumnValidation",
127
+ "error_type": "ExtractedColumnValidationError",
128
+ "message": (
129
+ f"提取列 '{col_name}' 失败:源列 '{source_column}' 不存在于表 '{table_id}'。请检查 Schema 配置。"
130
+ ),
131
+ }
132
+ )
133
+ continue
134
+
135
+ # 获取源列绑定的正则表达式模式
136
+ source_col = table_schema.columns.get(source_column)
137
+ if not source_col:
138
+ all_errors.append(
139
+ {
140
+ "stage": "format",
141
+ "table": table_id,
142
+ "column": col_name,
143
+ "check_type": "ExtractedColumnValidation",
144
+ "error_type": "ExtractedColumnValidationError",
145
+ "message": (
146
+ f"提取列 '{col_name}' 失败:源列 '{source_column}' 未在 schema 中定义。请检查 Schema 配置。"
147
+ ),
148
+ }
149
+ )
150
+ continue
151
+
152
+ regex_pattern = None
153
+ regex_flags = "g"
154
+ case_sensitive = True
155
+
156
+ source_data_type = source_col.data_type
157
+ if hasattr(source_data_type, "name") and source_data_type.name == "Expr":
158
+ # 显式 Expr:有 pattern 属性
159
+ if hasattr(source_data_type, "pattern"):
160
+ regex_pattern = source_data_type.pattern
161
+ if hasattr(source_data_type, "flags"):
162
+ regex_flags = source_data_type.flags
163
+ if hasattr(source_data_type, "case_sensitive"):
164
+ case_sensitive = source_data_type.case_sensitive
165
+ # 隐式 Expr:无 pattern 属性,但有 registry
166
+ elif hasattr(source_data_type, "registry") and source_data_type.registry:
167
+ registry = source_data_type.registry
168
+ # 遍历 registry 中的所有 pattern,查找包含 extract_key 的
169
+ if hasattr(registry, "_patterns"):
170
+ for pattern_obj in registry._patterns:
171
+ if hasattr(pattern_obj, "regex") and pattern_obj.regex:
172
+ regex_obj = pattern_obj.regex
173
+ group_index = regex_obj.groupindex
174
+ if extract_key in group_index:
175
+ regex_pattern = regex_obj.pattern
176
+ break
177
+
178
+ if not regex_pattern:
179
+ all_errors.append(
180
+ {
181
+ "stage": "format",
182
+ "table": table_id,
183
+ "column": col_name,
184
+ "check_type": "ExtractedColumnValidation",
185
+ "error_type": "ExtractedColumnValidationError",
186
+ "message": (
187
+ f"提取列 '{col_name}' 失败:源列 '{source_column}' 没有绑定正则表达式。请检查 Schema 配置。"
188
+ ),
189
+ }
190
+ )
191
+ continue
192
+
193
+ # 编译正则表达式(flags 整词匹配解析,防 "multiline" 子串误开 IGNORECASE)
194
+ flags = parse_regex_flags(regex_flags)
195
+ if not case_sensitive:
196
+ flags |= re.IGNORECASE
197
+
198
+ # 执行提取
199
+ try:
200
+ compiled = re.compile(regex_pattern, flags)
201
+ group_names = list(compiled.groupindex.keys())
202
+
203
+ # 获取源列的原始数据作为 Series(优先使用 raw_df 保留原始字符串格式)
204
+ raw_df = raw_datasets.get(table_id)
205
+ if raw_df is not None and source_column in raw_df.columns:
206
+ source_series = raw_df[source_column].fillna("").astype(str)
207
+ else:
208
+ source_series = parsed_df[source_column].fillna("").astype(str)
209
+
210
+ # 使用 pandas str.extract 进行向量化提取
211
+ extracted_df = source_series.str.extract(compiled)
212
+ match_count = extracted_df.notna().any(axis=1).sum()
213
+
214
+ if extract_key in extracted_df.columns:
215
+ extracted_series = extracted_df[extract_key].replace("", pd.NA)
216
+
217
+ # 根据 result_type 进行向量化类型转换
218
+ if result_type == "Integer":
219
+ parsed_df[col_name] = pd.to_numeric(extracted_series, errors="coerce").astype("Int64")
220
+ elif result_type == "Float":
221
+ parsed_df[col_name] = pd.to_numeric(extracted_series, errors="coerce")
222
+ elif result_type == "Boolean":
223
+ is_null = extracted_series.isna() | (extracted_series.str.strip() == "")
224
+ bool_series = extracted_series.str.lower().isin(["true", "1", "yes"]).astype("boolean")
225
+ bool_series[is_null] = pd.NA
226
+ parsed_df[col_name] = bool_series
227
+ else:
228
+ # String 或其他类型保持原样
229
+ parsed_df[col_name] = extracted_series
230
+
231
+ logger.debug(
232
+ f"提取列 '{col_name}' <- '{source_column}'[{extract_key}], 匹配 {match_count} 行, 类型: {result_type or 'String'}"
233
+ )
234
+ else:
235
+ error_msg = f"提取键 '{extract_key}' 不在正则表达式的命名捕获组中,可用组: {group_names}"
236
+ logger.warning(error_msg)
237
+ # 将配置错误上报为验证错误,避免用户看到"通过"(M4)
238
+ all_errors.append(
239
+ {
240
+ "stage": "format",
241
+ "table": table_id,
242
+ "column": col_name,
243
+ "check_type": "ExtractedColumnValidation",
244
+ "message": error_msg,
245
+ }
246
+ )
247
+
248
+ except Exception as e:
249
+ error_msg = (
250
+ f"正则提取列 '{col_name}' 执行失败 (来源列: '{source_column}', 提取键: '{extract_key}'): {e}"
251
+ )
252
+ logger.warning(error_msg, exc_info=True)
253
+ # 提取失败也作为验证错误上报:派生列整列缺失时下游约束会在空数据上
254
+ # 运行,报告"通过"属于静默跳过校验(与模块契约"避免静默通过"一致)
255
+ all_errors.append(
256
+ {
257
+ "stage": "format",
258
+ "table": table_id,
259
+ "column": col_name,
260
+ "check_type": "ExtractedColumnValidation",
261
+ "message": error_msg,
262
+ }
263
+ )
264
+
265
+ # 更新 parsed_datasets 中的 DataFrame
266
+ parsed_datasets[table_id] = parsed_df
@@ -0,0 +1,209 @@
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 校验历史存储服务
17
+
18
+ 功能概述:
19
+ - 将每次校验结果持久化到本地 JSON 文件
20
+ - 支持历史列表查询、单次详情查询、删除
21
+ - 支持聚合统计(趋势数据)
22
+
23
+ 存储位置: {projectDir}/.precis/validation_history.json
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import json
29
+ import logging
30
+ import os
31
+ import threading
32
+ import time
33
+ from dataclasses import asdict, dataclass, field
34
+ from pathlib import Path
35
+ from typing import Any
36
+
37
+ logger = logging.getLogger(__name__)
38
+
39
+ HISTORY_FILENAME = "validation_history.json"
40
+ MAX_HISTORY_ENTRIES = 200
41
+
42
+ # 回归 C7: 进程级写锁。ValidationHistoryStore 每请求新建实例(load 全量→改→全量覆写),
43
+ # 并发请求会产生 lost-update。用模块级锁串行化同进程内的 add_run/delete_run 写操作;
44
+ # 但锁只保证互斥不保证新鲜——add_run/delete_run 还须在锁内重新 _load 磁盘最新内容
45
+ # 再合并保存,否则先写实例的记录会被后写实例的旧内存快照覆写(跨进程仍依赖文件锁,
46
+ # 但本地单进程场景已足够)。
47
+ _history_write_lock = threading.Lock()
48
+
49
+
50
+ @dataclass
51
+ class ValidationRunRecord:
52
+ """单次校验运行记录"""
53
+
54
+ id: str
55
+ timestamp: str
56
+ duration_ms: int
57
+ scope: str
58
+ summary: dict[str, Any]
59
+ by_type: dict[str, dict[str, int]]
60
+ by_table: dict[str, dict[str, int]]
61
+ errors: list[dict[str, Any]]
62
+ warnings: list[str] = field(default_factory=list)
63
+
64
+
65
+ class ValidationHistoryStore:
66
+ """校验历史持久化存储"""
67
+
68
+ def __init__(self, project_dir: str) -> None:
69
+ self._file_path = Path(project_dir) / ".precis" / HISTORY_FILENAME
70
+ self._runs: list[dict[str, Any]] = []
71
+ self._load()
72
+
73
+ def _load(self) -> None:
74
+ """从磁盘加载历史记录。
75
+
76
+ 回归 C7: 文件损坏时不应静默清空后用空列表覆写(否则下次 add_run 永久丢失全部
77
+ 历史)。这里把损坏文件重命名为 .corrupted 备份保留现场,内存置空但不破坏磁盘,
78
+ 便于用户手工恢复/排查。
79
+ """
80
+ if not self._file_path.exists():
81
+ self._runs = []
82
+ return
83
+ try:
84
+ with open(self._file_path, encoding="utf-8-sig") as f:
85
+ data = json.load(f)
86
+ self._runs = data.get("runs", [])
87
+ except Exception as e:
88
+ logger.warning(f"[ValidationHistory] 加载历史文件失败: {e}")
89
+ self._runs = []
90
+ # 把损坏文件备份为 .corrupted,避免下次保存静默清空现场(回归 C7)
91
+ try:
92
+ backup = self._file_path.with_suffix(self._file_path.suffix + ".corrupted")
93
+ self._file_path.rename(backup)
94
+ logger.warning(f"[ValidationHistory] 损坏的历史文件已备份至: {backup}")
95
+ except OSError:
96
+ # 重命名失败也不影响:构造阶段不写盘,损坏文件原样保留
97
+ pass
98
+
99
+ def _save(self) -> None:
100
+ """将历史记录写入磁盘(原子写:tmp + os.replace)。
101
+
102
+ 回归 C7: 原实现直接以 "w" 打开目标文件,写一半崩溃/断电会留下截断损坏的 JSON,
103
+ 下次 _load 失败 → 清空 → 永久丢失全部历史。原子写保证目标文件要么是完整旧内容、
104
+ 要么是完整新内容,绝不出现部分写入。
105
+ """
106
+ self._file_path.parent.mkdir(parents=True, exist_ok=True)
107
+ # 临时文件与目标同目录,保证 os.replace 是原子的(同文件系统)。
108
+ tmp_path = self._file_path.with_suffix(self._file_path.suffix + ".tmp")
109
+ try:
110
+ with open(tmp_path, "w", encoding="utf-8") as f:
111
+ json.dump({"runs": self._runs}, f, ensure_ascii=False, indent=2)
112
+ os.replace(tmp_path, self._file_path)
113
+ except Exception as e:
114
+ logger.error(f"[ValidationHistory] 保存历史文件失败: {e}")
115
+ # 清理可能残留的临时文件,避免堆积
116
+ try:
117
+ tmp_path.unlink()
118
+ except OSError:
119
+ pass
120
+
121
+ def add_run(self, record: ValidationRunRecord) -> str:
122
+ """添加一次校验记录,返回记录 ID。
123
+
124
+ 回归 C7: 加进程级写锁,串行化同进程内并发请求的 load→insert→save。
125
+ 回归 lost-update: 锁只保证互斥,不保证新鲜——每请求新建实例时各实例的
126
+ _runs 都是构造时的快照,先写实例落盘的记录会被后写实例的旧快照覆写。
127
+ 因此在锁内先重新 _load 磁盘最新内容再合并插入,两个实例交错 add 后
128
+ 双方记录都在。
129
+ """
130
+ entry = asdict(record)
131
+ with _history_write_lock:
132
+ self._load() # 锁内重读,合并其他实例已落盘的记录
133
+ self._runs.insert(0, entry)
134
+ if len(self._runs) > MAX_HISTORY_ENTRIES:
135
+ self._runs = self._runs[:MAX_HISTORY_ENTRIES]
136
+ self._save()
137
+ return record.id
138
+
139
+ def get_runs(self, limit: int = 20, offset: int = 0) -> dict[str, Any]:
140
+ """获取历史列表(分页)"""
141
+ total = len(self._runs)
142
+ items = self._runs[offset : offset + limit]
143
+ return {"total": total, "limit": limit, "offset": offset, "items": items}
144
+
145
+ def get_run(self, run_id: str) -> dict[str, Any] | None:
146
+ """获取单次校验详情"""
147
+ for run in self._runs:
148
+ if run.get("id") == run_id:
149
+ return run
150
+ return None
151
+
152
+ def delete_run(self, run_id: str) -> bool:
153
+ """删除单次记录(回归 C7: 加写锁避免并发覆盖;锁内重读避免基于旧快照删除)。"""
154
+ with _history_write_lock:
155
+ self._load() # 锁内重读,基于磁盘最新内容删除
156
+ original_len = len(self._runs)
157
+ self._runs = [r for r in self._runs if r.get("id") != run_id]
158
+ if len(self._runs) < original_len:
159
+ self._save()
160
+ return True
161
+ return False
162
+
163
+ def get_stats(self, last_n: int = 10) -> dict[str, Any]:
164
+ """获取聚合统计(最近 N 次的趋势数据)"""
165
+ recent = self._runs[:last_n]
166
+ trend = []
167
+ for run in reversed(recent):
168
+ summary = run.get("summary", {})
169
+ trend.append(
170
+ {
171
+ "id": run.get("id"),
172
+ "timestamp": run.get("timestamp"),
173
+ "pass_rate": summary.get("pass_rate", 0),
174
+ "total_checks": summary.get("total_checks", 0),
175
+ "failed_count": summary.get("failed_count", 0),
176
+ }
177
+ )
178
+ latest = self._runs[0] if self._runs else None
179
+ return {
180
+ "total_runs": len(self._runs),
181
+ "trend": trend,
182
+ "latest": {
183
+ "pass_rate": latest.get("summary", {}).get("pass_rate", 0) if latest else 0,
184
+ "total_checks": latest.get("summary", {}).get("total_checks", 0) if latest else 0,
185
+ "passed_count": latest.get("summary", {}).get("passed_count", 0) if latest else 0,
186
+ "failed_count": latest.get("summary", {}).get("failed_count", 0) if latest else 0,
187
+ "timestamp": latest.get("timestamp") if latest else None,
188
+ },
189
+ }
190
+
191
+
192
+ _run_id_lock = threading.Lock()
193
+ _run_id_last_us: int = -1
194
+ _run_id_counter: int = 0
195
+
196
+
197
+ def generate_run_id() -> str:
198
+ """生成运行 ID(时间戳 + 单调计数器保证唯一性)"""
199
+ global _run_id_last_us, _run_id_counter
200
+ t = time.strftime("%Y%m%d_%H%M%S")
201
+ us = int(time.time() * 1000000) % 1000000
202
+ with _run_id_lock:
203
+ if us == _run_id_last_us:
204
+ _run_id_counter += 1
205
+ else:
206
+ _run_id_last_us = us
207
+ _run_id_counter = 0
208
+ suffix = f"{us:06d}_{_run_id_counter:03d}"
209
+ return f"run_{t}_{suffix}"
@@ -0,0 +1,136 @@
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 校验结果 → JSON 契约 payload(CLI 与 MCP 共用单一事实源)
17
+
18
+ 契约文档:docs/contracts/validate-json-v1.md(v1 已冻结,只增不减)。
19
+ 本模块是 `_build_json_payload` 结构的唯一实现:
20
+ - CLI `--format json` 的 stdout 输出
21
+ - MCP tool `validate_data` 的返回值
22
+ - `--report` 报告导出的数据源
23
+ 三处消费同一结构,禁止分叉出第二套输出格式。
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import math
29
+ from collections.abc import Sized
30
+ from typing import Any
31
+
32
+ # JSON 输出契约的版本号(未来契约演进时递增)
33
+ JSON_SCHEMA_VERSION = 1
34
+
35
+
36
+ def json_safe_value(value: Any) -> Any:
37
+ """将校验错误中的单元格值转换为 JSON 可序列化的原生类型。
38
+
39
+ pandas 校验链路可能透出 numpy 标量(int64/float64 等)与非有限浮点
40
+ (NaN/Inf),两者均非合法 JSON。numpy 标量经 .item() 归一为 Python
41
+ 原生类型;非有限浮点转为字符串保留诊断信息。
42
+
43
+ Args:
44
+ value: 错误条目中的任意取值
45
+
46
+ Returns:
47
+ JSON 可序列化的值(None/bool/int/有限 float/str)
48
+ """
49
+ if value is None or isinstance(value, (str, bool, int)):
50
+ return value
51
+ if isinstance(value, float):
52
+ # np.float64 是 float 子类可直接命中;NaN/Inf 不是合法 JSON,转字符串
53
+ return value if math.isfinite(value) else str(value)
54
+ if hasattr(value, "item"):
55
+ # numpy 标量统一经 .item() 归一后再递归判定
56
+ try:
57
+ return json_safe_value(value.item())
58
+ except (AttributeError, ValueError):
59
+ pass
60
+ # 兜底:其余类型(如 dict/list 以外的对象)转字符串,保证不破坏序列化
61
+ return str(value)
62
+
63
+
64
+ def build_json_payload(result: dict) -> dict:
65
+ """将 ValidationExecutor.execute() 的结果转换为 JSON 输出契约结构。
66
+
67
+ 契约字段必须齐全,无法取到的值填 null:
68
+ - tables 来自 validation_details.format_checks(表显示名),
69
+ 行数优先取 raw_datasets 中的 DataFrame 长度,分块模式取 row_count
70
+ - summary 来自 validation_details.constraint_checks 的 passed 统计
71
+ - errors 中 table/column/constraint_type(check_type)/constraint_file
72
+ 由校验链路提供(constraint_file 由 executor 挂载,见 P0-3)
73
+ - loading_errors 原样透传为 loading_warnings
74
+
75
+ Args:
76
+ result: ValidationExecutor.execute() 返回的结果字典
77
+
78
+ Returns:
79
+ 符合契约 v1 的字典(可直接 json.dumps)
80
+ """
81
+ errors = result.get("errors", [])
82
+ validation_details = result.get("validation_details") or {}
83
+ constraint_checks = validation_details.get("constraint_checks") or []
84
+ format_checks = validation_details.get("format_checks") or []
85
+ raw_datasets = result.get("raw_datasets") or {}
86
+
87
+ tables: list[dict[str, Any]] = []
88
+ for check in format_checks:
89
+ # postprocess 已把 table 换成显示名并保留原 ID 到 table_id;
90
+ # raw_datasets 以表 ID 为键,优先按 ID 查(显示名与 ID 不同时也能命中行数)
91
+ dataset = raw_datasets.get(check.get("table_id") or check.get("table"))
92
+ rows: int | None = None
93
+ if isinstance(dataset, dict):
94
+ # 分块模式下 raw_datasets 值为 {"chunk_count": ..., "row_count": ...}
95
+ rows = dataset.get("row_count")
96
+ elif isinstance(dataset, Sized):
97
+ # 标准模式下值为 DataFrame,行数即长度
98
+ rows = len(dataset)
99
+ tables.append({"name": check.get("table"), "rows": rows})
100
+
101
+ constraints_total = len(constraint_checks)
102
+ constraints_failed = sum(1 for check in constraint_checks if not check.get("passed", True))
103
+
104
+ error_entries: list[dict[str, Any]] = []
105
+ for error in errors:
106
+ # 格式错误携带 cell_value/error_message;域约束错误携带 value/message,做双字段兼容
107
+ cell_value = error.get("cell_value") if "cell_value" in error else error.get("value")
108
+ error_message = error.get("error_message") if error.get("error_message") is not None else error.get("message")
109
+ error_entries.append(
110
+ {
111
+ "table": error.get("table"),
112
+ "column": error.get("column"),
113
+ "constraint_type": error.get("check_type"),
114
+ "constraint_file": error.get("constraint_file"),
115
+ "row_index": error.get("row_index"),
116
+ "cell_value": json_safe_value(cell_value),
117
+ "error_message": error_message,
118
+ # 可选修复建议(AllowedValues 相近值 / 日期布局提示等),生成器未给则为 null
119
+ "suggestion": error.get("suggestion"),
120
+ }
121
+ )
122
+
123
+ return {
124
+ "schema_version": JSON_SCHEMA_VERSION,
125
+ "is_valid": not errors,
126
+ "interrupted": bool(result.get("interrupted", False)),
127
+ "duration_ms": result.get("duration_ms", 0),
128
+ "tables": tables,
129
+ "summary": {
130
+ "constraints_total": constraints_total,
131
+ "constraints_passed": constraints_total - constraints_failed,
132
+ "constraints_failed": constraints_failed,
133
+ },
134
+ "errors": error_entries,
135
+ "loading_warnings": result.get("loading_errors", []),
136
+ }