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,509 @@
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
+ - 支持大文件(>500MB)的分块读取和校验
21
+ - 对 CSV 文件使用 pandas chunksize 分块读取
22
+ - 对 Excel 文件按行范围分块读取
23
+ - 每个分块独立执行约束校验,合并结果
24
+
25
+ 架构设计:
26
+ - 组合模式: ChunkedDataLoader 使用 DataSourceResolver 做路径解析
27
+ - 与 DataLoader 并行存在,由 ValidationExecutor 根据文件大小决定使用哪个
28
+ - 分块校验结果通过 ChunkedValidationResult 聚合
29
+
30
+ 输入示例:
31
+ loader = ChunkedDataLoader(resolver, dataset_schema, schema_by_id, settings)
32
+ result = loader.load_and_validate_chunked(data_directory, schema, engine_fn)
33
+
34
+ 输出示例:
35
+ result = ChunkedValidationResult(
36
+ parsed_datasets={...},
37
+ errors=[...],
38
+ validation_details={...},
39
+ chunk_count=5,
40
+ total_rows=500000
41
+ )
42
+ """
43
+
44
+ from __future__ import annotations
45
+
46
+ import logging
47
+ import os
48
+ from dataclasses import dataclass, field
49
+ from typing import Any
50
+
51
+ import pandas as pd
52
+
53
+ from app.shared.core.data_source.loaders.csv_loader import build_csv_read_kwargs
54
+ from app.shared.core.data_source.loaders.excel_loader import apply_merged_ranges_fill, read_merged_ranges_from_xlsx
55
+ from app.shared.core.data_source.schema_info import DataSourceInfo
56
+ from app.shared.core.project.schema.types import TableSchemaFile
57
+ from app.shared.domain.dataset_schema import DataSetSchema, TableSchema
58
+
59
+ from .data_loader import collect_foreign_key_tables
60
+ from .memory_monitor import MemoryMonitor
61
+ from .resolver import DataSourceResolver
62
+
63
+ logger = logging.getLogger(__name__)
64
+
65
+
66
+ @dataclass
67
+ class ChunkedValidationResult:
68
+ """分块校验结果聚合。"""
69
+
70
+ parsed_datasets: dict[str, pd.DataFrame] = field(default_factory=dict)
71
+ errors: list[dict] = field(default_factory=list)
72
+ validation_details: dict[str, list[dict]] = field(
73
+ default_factory=lambda: {"format_checks": [], "constraint_checks": []}
74
+ )
75
+ loading_errors: list[dict] = field(default_factory=list)
76
+ chunk_count: int = 0
77
+ total_rows: int = 0
78
+ warnings: list[str] = field(default_factory=list)
79
+
80
+
81
+ class ChunkedDataLoader:
82
+ """
83
+ @classdesc 分块数据加载器
84
+
85
+ 负责:
86
+ - 对大文件进行分块读取
87
+ - 每个分块独立执行格式解析和约束校验
88
+ - 聚合所有分块的校验结果
89
+
90
+ 设计决策:
91
+ - CSV 使用 pandas read_csv(chunksize=...) 流式读取
92
+ - Excel 按行范围分块读取(pandas read_excel + skiprows/nrows)
93
+ - JSON 不支持分块(需全量加载),但仍可对加载后的 DataFrame 分块校验
94
+ """
95
+
96
+ def __init__(
97
+ self,
98
+ resolver: DataSourceResolver,
99
+ dataset_schema: DataSetSchema,
100
+ schema_by_id: dict[str, TableSchemaFile],
101
+ settings: Any,
102
+ memory_monitor: MemoryMonitor | None = None,
103
+ ):
104
+ self._resolver = resolver
105
+ self.dataset_schema = dataset_schema
106
+ self._schema_by_id = schema_by_id
107
+ self.settings = settings
108
+ self._monitor = memory_monitor or MemoryMonitor()
109
+
110
+ def _load_csv_chunked(
111
+ self,
112
+ file_path: str,
113
+ schema: TableSchema,
114
+ chunk_size: int,
115
+ ) -> list[pd.DataFrame]:
116
+ """
117
+ @methoddesc 分块加载 CSV 文件
118
+
119
+ 使用 pandas read_csv 的 chunksize 参数进行流式读取。
120
+
121
+ 参数:
122
+ file_path: CSV 文件路径
123
+ schema: 表 Schema 定义
124
+ chunk_size: 每个分块的行数
125
+
126
+ 返回:
127
+ DataFrame 分块列表
128
+ """
129
+ try:
130
+ # A2 修复:与标准 CSVLoader 共用 read_kwargs 构造(含 utf-8→utf-8-sig BOM 升级、
131
+ # quotechar/on_bad_lines/escapechar/skiprows 透传)。schema.source_config 是
132
+ # SourceSpec.to_loader_config() 的产物,恒含 encoding 真值(默认 "utf-8"),
133
+ # 过去 `or "utf-8-sig"` 兜底永不生效 → BOM 不剥、首列变 \ufeffxxx。
134
+ # 注意:CSVOptions 无 nrows 字段,分块路径不发明 nrows 支持。
135
+ source_config = getattr(schema, "source_config", None) or {}
136
+
137
+ # on_bad_lines 来自自由字典,按标准路径的白名单收窄,非法值兜底 "error"
138
+ # (与 CSVSourceSpec/CSVOptions 默认值同口径:坏行静默跳过=校验工具漏报),
139
+ # 避免把任意字符串透传给 pandas.read_csv 导致加载失败
140
+ on_bad_lines = source_config.get("on_bad_lines")
141
+ if on_bad_lines not in ("error", "warn", "skip"):
142
+ on_bad_lines = "error"
143
+
144
+ read_kwargs = build_csv_read_kwargs(
145
+ header_row=schema.header_row if schema.header_row is not None else 0,
146
+ encoding=source_config.get("encoding") or "utf-8",
147
+ delimiter=source_config.get("delimiter", ","),
148
+ quotechar=source_config.get("quotechar", '"'),
149
+ on_bad_lines=on_bad_lines,
150
+ escapechar=source_config.get("escapechar"),
151
+ skip_rows=int(source_config.get("skip_rows", 0) or 0),
152
+ )
153
+ read_kwargs["chunksize"] = chunk_size
154
+
155
+ chunks: list[pd.DataFrame] = []
156
+ for chunk in pd.read_csv(file_path, **read_kwargs):
157
+ chunks.append(chunk)
158
+
159
+ logger.info(f"CSV 分块加载完成: {os.path.basename(file_path)}, {len(chunks)} 个分块")
160
+ return chunks
161
+
162
+ except Exception as e:
163
+ logger.error(f"CSV 分块加载失败: {file_path}, 错误: {e}")
164
+ raise
165
+
166
+ def _load_excel_chunked(
167
+ self,
168
+ file_path: str,
169
+ sheet_name: str,
170
+ header_row: int,
171
+ chunk_size: int,
172
+ ) -> list[pd.DataFrame]:
173
+ """
174
+ @methoddesc 分块加载 Excel 文件
175
+
176
+ Excel 不支持原生 chunksize,通过多次调用 read_excel(skiprows + nrows)实现分块。
177
+
178
+ 参数:
179
+ file_path: Excel 文件路径
180
+ sheet_name: Sheet 名称
181
+ header_row: 表头行号
182
+ chunk_size: 每个分块的行数
183
+
184
+ 返回:
185
+ DataFrame 分块列表
186
+ """
187
+ try:
188
+ # 先读取表头确定列数
189
+ header_df = pd.read_excel(
190
+ file_path,
191
+ sheet_name=sheet_name,
192
+ header=header_row,
193
+ nrows=0,
194
+ )
195
+ columns = header_df.columns.tolist()
196
+
197
+ if not columns:
198
+ logger.warning(f"Excel 文件无列: {file_path}, sheet={sheet_name}")
199
+ return []
200
+
201
+ # §1.27: 分块路径补 B7 同构的合并单元格前向填充(一次 openpyxl 读取 merged ranges,
202
+ # 每块读出后按"区域首行在本块内"填充;跨块悬挂区域跳过并累计告警)。
203
+ # 与标准路径(excel_loader._apply_merged_cell_fill)共用 apply_merged_ranges_fill
204
+ # 单一事实源,防止两条路径语义再漂移。读取失败降级为不填充(照 B27 惯例留诊断日志)。
205
+ merged_ranges = self._read_excel_merged_ranges(file_path, sheet_name)
206
+
207
+ chunks: list[pd.DataFrame] = []
208
+ current_skip = header_row + 1 # 跳过表头行
209
+ # 回归 #8: 累计已读数据行数(不含表头),用于给每块设置全局连续的 0-based 数据行号,
210
+ # 使约束校验的 row_index 反映行在原文件的真实数据位置(与 CSV 分块的全局连续 index 对齐)。
211
+ data_row_offset = 0
212
+ skipped_cross_total = 0
213
+
214
+ while True:
215
+ read_kwargs: dict[str, Any] = {
216
+ "sheet_name": sheet_name,
217
+ "header": None,
218
+ "skiprows": current_skip,
219
+ "nrows": chunk_size,
220
+ }
221
+
222
+ chunk = pd.read_excel(file_path, **read_kwargs)
223
+
224
+ if chunk.empty:
225
+ break
226
+
227
+ # 设置列名
228
+ if len(chunk.columns) == len(columns):
229
+ chunk.columns = columns
230
+
231
+ if merged_ranges is not None:
232
+ chunk, _filled, skipped_cross = apply_merged_ranges_fill(
233
+ chunk,
234
+ merged_ranges,
235
+ header_row=header_row,
236
+ skip_rows=0,
237
+ global_row_offset=data_row_offset,
238
+ )
239
+ skipped_cross_total += skipped_cross
240
+
241
+ # 回归 #8: 把块内 index 重置为全局连续的数据行号(从 data_row_offset 开始),
242
+ # 而非 pd.read_excel 默认的每块从 0 重启的 RangeIndex。否则第 2 块起 row_index
243
+ # 与原文件位置错位,财务/审计场景无法据报告行号定位错误。
244
+ chunk.index = range(data_row_offset, data_row_offset + len(chunk))
245
+
246
+ chunks.append(chunk)
247
+ current_skip += chunk_size
248
+ data_row_offset += len(chunk)
249
+
250
+ # 如果返回的行数小于 chunk_size,说明已到文件末尾
251
+ if len(chunk) < chunk_size:
252
+ break
253
+
254
+ if skipped_cross_total > 0:
255
+ logger.warning(
256
+ "分块 Excel 合并单元格填充:%d 个合并区域跨越分块边界,其跨块部分未填充"
257
+ "(区域首行所在块已填充该块内部分),可能产生少量 NotNull 误报,file=%s",
258
+ skipped_cross_total,
259
+ file_path,
260
+ )
261
+
262
+ logger.info(f"Excel 分块加载完成: {os.path.basename(file_path)}, {len(chunks)} 个分块")
263
+ return chunks
264
+
265
+ except Exception as e:
266
+ logger.error(f"Excel 分块加载失败: {file_path}, 错误: {e}")
267
+ raise
268
+
269
+ @staticmethod
270
+ def _read_excel_merged_ranges(file_path: str, sheet_name: str) -> Any:
271
+ """一次性读取 sheet 的合并单元格区域及其首行值(§1.27),供分块填充复用。
272
+
273
+ 返回 (min_row, min_col, max_row, max_col, values) 五元组列表——values 为区域
274
+ 首行各列的值,使跨块悬挂区域能直接赋值续填(合并单元格区域内所有格本就同值)。
275
+ 任何失败降级返回 None(调用方跳过填充,与 B27 降级语义一致——填充是尽力而为的增强)。
276
+
277
+ 2026-09-20 修复:改用 excel_loader.read_merged_ranges_from_xlsx
278
+ (zipfile+ElementTree 流式解析)。此前 openpyxl 普通模式 load_workbook 会把
279
+ 全部 sheet 的 Cell 对象图整体物化(read_only 模式的 ReadOnlyWorksheet 无
280
+ merged_cells 属性才退而用普通模式)——0.5MB/15 万格实测峰值 55MB vs
281
+ read_only ≈0MB(~120 倍放大),恰好落在 >500MB 分块阈值场景上。
282
+ """
283
+ try:
284
+ return read_merged_ranges_from_xlsx(file_path, sheet_name)
285
+ except Exception as e:
286
+ logger.warning(f"分块 Excel 合并单元格区域读取失败,跳过填充(可能导致 NotNull 误报): {file_path}: {e}")
287
+ return None
288
+
289
+ def _load_dataframe_chunked(
290
+ self,
291
+ file_path: str,
292
+ schema: TableSchema,
293
+ chunk_size: int,
294
+ ) -> list[pd.DataFrame]:
295
+ """
296
+ @methoddesc 根据文件类型选择分块加载策略
297
+
298
+ 参数:
299
+ file_path: 数据文件路径
300
+ schema: 表 Schema 定义
301
+ chunk_size: 每个分块的行数
302
+
303
+ 返回:
304
+ DataFrame 分块列表
305
+ """
306
+ ext = os.path.splitext(file_path)[1].lower()
307
+
308
+ if ext == ".csv":
309
+ return self._load_csv_chunked(file_path, schema, chunk_size)
310
+ elif ext in (".xlsx", ".xls"):
311
+ sheet_name = getattr(schema, "sheet_name", None) or "Sheet1"
312
+ header_row = getattr(schema, "header_row", 0) or 0
313
+ return self._load_excel_chunked(file_path, sheet_name, header_row, chunk_size)
314
+ else:
315
+ # JSON 等格式不支持分块,全量加载后按行切分
316
+ logger.info(f"文件格式 {ext} 不支持原生分块加载,将全量加载后切分")
317
+ from app.shared.core.data_source.loader import _LOADER_FNS
318
+
319
+ loader_fn = _LOADER_FNS.get(ext)
320
+ if not loader_fn:
321
+ raise ValueError(f"不支持的文件格式: {ext}")
322
+
323
+ info = DataSourceInfo(
324
+ schema_id=getattr(schema, "id", None) or getattr(schema, "name", None) or "",
325
+ name=getattr(schema, "name", None),
326
+ sheet_name=getattr(schema, "sheet_name", None),
327
+ header_row=getattr(schema, "header_row", 0),
328
+ source_config=getattr(schema, "source_config", {}),
329
+ )
330
+ full_datasets = loader_fn(file_path, [info])
331
+ if not full_datasets:
332
+ return []
333
+
334
+ # 取第一个 DataFrame 进行分块切分
335
+ df = next(iter(full_datasets.values()))
336
+ return [df[i : i + chunk_size] for i in range(0, len(df), chunk_size)]
337
+
338
+ def load_chunked_sources(
339
+ self,
340
+ data_directory: str,
341
+ table_filter: str | list[str] | None = None,
342
+ ) -> tuple[dict[str, list[pd.DataFrame]], list[dict]]:
343
+ """
344
+ @methoddesc 分块加载所有数据源
345
+
346
+ 对需要分块的大文件进行分块读取,小文件返回单个分块。
347
+
348
+ 参数:
349
+ data_directory: 数据文件所在目录
350
+ table_filter: 表过滤条件
351
+
352
+ 返回:
353
+ 二元组 (datasets, loading_errors):
354
+ - datasets: 键为表 ID,值为 DataFrame 分块列表
355
+ - loading_errors: 加载阶段产生的错误列表(源找不到、加载异常等),
356
+ 供 executor 上报到校验报告。回归 #5: 原实现吞掉所有错误导致损坏表静默消失。
357
+ """
358
+ chunked_datasets: dict[str, list[pd.DataFrame]] = {}
359
+ loading_errors: list[dict] = []
360
+ # Excel 回退 sheet 名映射:与 data_loader 的标准模式保持一致,
361
+ # schema 未显式指定 sheet_name 时作为加载回退
362
+ file_to_sheet_names: dict[str, str] = {}
363
+
364
+ # 解析搜索目录
365
+ search_directory = data_directory
366
+ first_data_source = self._resolver.resolve_first_data_source()
367
+ if first_data_source:
368
+ search_directory = first_data_source
369
+
370
+ if not os.path.isdir(search_directory):
371
+ logger.error(f"数据目录不存在: {data_directory}")
372
+ return chunked_datasets, loading_errors
373
+
374
+ # 处理过滤
375
+ filter_set: set | None = None
376
+ if table_filter:
377
+ if isinstance(table_filter, str):
378
+ filter_set = {table_filter}
379
+ else:
380
+ filter_set = set(table_filter)
381
+
382
+ # A3 修复: 与 data_loader 标准路径一致,过滤时必须把 FK 目标表并入加载集。
383
+ # 否则按 A 过滤时 B 不加载,FK 校验因目标表缺失误报「表不在提供的数据集中」。
384
+ tables_to_load: set | None = None
385
+ if filter_set:
386
+ tables_to_load = collect_foreign_key_tables(self.dataset_schema, filter_set)
387
+
388
+ for table_id, table_schema in self.dataset_schema.tables.items():
389
+ schema_file = self._schema_by_id.get(table_id)
390
+ if not schema_file:
391
+ continue
392
+
393
+ # 应用过滤
394
+ if tables_to_load:
395
+ table_name = table_schema.name or table_id
396
+ if table_name not in tables_to_load and table_id not in tables_to_load:
397
+ continue
398
+
399
+ table_name = table_schema.name or table_id
400
+
401
+ # 解析数据源路径(第二个返回值是 Excel sheet 名,用于构建回退映射)
402
+ source_path, sheet_name_resolved = self._resolver.resolve_source_path(data_directory, schema_file)
403
+ if not source_path:
404
+ # 回归 #5: 源找不到必须上报,否则该表静默消失、报告显示"全部通过"。
405
+ logger.warning(f"表 '{table_name}' 未找到数据源,跳过分块加载")
406
+ loading_errors.append(
407
+ {
408
+ "error_type": "SourceNotFound",
409
+ "table": table_name,
410
+ "message": f"表 '{table_name}' 未找到数据源,已跳过分块加载。",
411
+ }
412
+ )
413
+ continue
414
+
415
+ if sheet_name_resolved:
416
+ file_to_sheet_names.setdefault(source_path, sheet_name_resolved)
417
+
418
+ # 判断是否需要分块
419
+ if self._monitor.should_chunk(source_path):
420
+ self._monitor.take_snapshot(source_path)
421
+ logger.info(f"表 '{table_name}' 文件较大,启用分块加载模式")
422
+
423
+ try:
424
+ chunks = self._load_dataframe_chunked(source_path, table_schema, self._monitor.chunk_rows)
425
+ chunked_datasets[table_id] = chunks
426
+ except Exception as e:
427
+ logger.error(f"分块加载失败: {table_id}, 错误: {e}")
428
+ # 回退到全量加载
429
+ logger.info(f"回退到全量加载: {table_id}")
430
+ try:
431
+ from app.shared.core.data_source.loader import load_grouped_sources
432
+
433
+ info = DataSourceInfo(
434
+ schema_id=table_id,
435
+ name=getattr(table_schema, "name", table_id),
436
+ sheet_name=getattr(table_schema, "sheet_name", None),
437
+ header_row=getattr(table_schema, "header_row", 0),
438
+ source_config=getattr(table_schema, "source_config", None) or {},
439
+ )
440
+ file_to_schemas = {source_path: [info]}
441
+ # 与 data_loader 标准模式对齐:透传 manifest 级编码/分隔符默认值与 sheet 回退映射
442
+ loaded, _ = load_grouped_sources(
443
+ file_to_schemas,
444
+ default_encoding=self.settings.file_processing.default_encoding,
445
+ csv_delimiter=self.settings.file_processing.csv_delimiter,
446
+ file_to_sheet_names=file_to_sheet_names or None,
447
+ )
448
+ if loaded:
449
+ df = next(iter(loaded.values()))
450
+ chunked_datasets[table_id] = [df]
451
+ else:
452
+ loading_errors.append(
453
+ {
454
+ "error_type": "DataLoadingError",
455
+ "table": table_name,
456
+ "message": f"表 '{table_name}' 分块加载及全量回退均未返回数据。",
457
+ }
458
+ )
459
+ except Exception as fallback_err:
460
+ logger.error(f"全量加载也失败: {table_id}, 错误: {fallback_err}")
461
+ loading_errors.append(
462
+ {
463
+ "error_type": "DataLoadingError",
464
+ "table": table_name,
465
+ "message": f"表 '{table_name}' 加载失败: 分块错误({e}); 全量回退错误({fallback_err})。",
466
+ }
467
+ )
468
+ else:
469
+ # 小文件,全量加载为单个分块
470
+ try:
471
+ from app.shared.core.data_source.loader import load_grouped_sources
472
+
473
+ info = DataSourceInfo(
474
+ schema_id=table_id,
475
+ name=getattr(table_schema, "name", table_id),
476
+ sheet_name=getattr(table_schema, "sheet_name", None),
477
+ header_row=getattr(table_schema, "header_row", 0),
478
+ source_config=getattr(table_schema, "source_config", None) or {},
479
+ )
480
+ file_to_schemas = {source_path: [info]}
481
+ # 与 data_loader 标准模式对齐:透传 manifest 级编码/分隔符默认值与 sheet 回退映射
482
+ loaded, _ = load_grouped_sources(
483
+ file_to_schemas,
484
+ default_encoding=self.settings.file_processing.default_encoding,
485
+ csv_delimiter=self.settings.file_processing.csv_delimiter,
486
+ file_to_sheet_names=file_to_sheet_names or None,
487
+ )
488
+ if loaded:
489
+ df = next(iter(loaded.values()))
490
+ chunked_datasets[table_id] = [df]
491
+ else:
492
+ loading_errors.append(
493
+ {
494
+ "error_type": "DataLoadingError",
495
+ "table": table_name,
496
+ "message": f"表 '{table_name}' 加载未返回数据。",
497
+ }
498
+ )
499
+ except Exception as e:
500
+ logger.error(f"加载失败: {table_id}, 错误: {e}")
501
+ loading_errors.append(
502
+ {
503
+ "error_type": "DataLoadingError",
504
+ "table": table_name,
505
+ "message": f"表 '{table_name}' 加载失败: {e}",
506
+ }
507
+ )
508
+
509
+ return chunked_datasets, loading_errors
@@ -0,0 +1,35 @@
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 DAG 执行包
18
+
19
+ 导出:
20
+ - build_transform_dag: 构建 Transform DAG
21
+ - topological_sort: 拓扑排序
22
+ - execute_transform_dag: 执行 DAG
23
+ """
24
+
25
+ from .builder import ExecutionDAG, build_execution_dag, build_transform_dag
26
+ from .executor import execute_transform_dag
27
+ from .sorter import topological_sort
28
+
29
+ __all__ = [
30
+ "ExecutionDAG",
31
+ "build_execution_dag",
32
+ "build_transform_dag",
33
+ "topological_sort",
34
+ "execute_transform_dag",
35
+ ]
@@ -0,0 +1,141 @@
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 DAG 构建器
18
+
19
+ 功能概述:
20
+ - 从 TransformFile 列表构建有向无环图(DAG)
21
+ - 节点 = transform 或 schema(数据源)
22
+ - 边 = input_from_node 依赖关系
23
+
24
+ 架构设计:
25
+ - 节点类型: "schema" | "transform"
26
+ - 使用邻接表表示图结构
27
+ - 支持检测循环依赖
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import logging
33
+ from dataclasses import dataclass, field
34
+ from typing import Any
35
+
36
+ from app.shared.core.project.regex.types import RegexNodeFile
37
+ from app.shared.core.project.transform.types import TransformFile
38
+
39
+ logger = logging.getLogger(__name__)
40
+
41
+
42
+ @dataclass
43
+ class DAGNode:
44
+ """@classdesc DAG 节点"""
45
+
46
+ id: str
47
+ node_type: str # "schema" | "transform"
48
+ data: Any = None # TransformFile 或 schema 标识
49
+ incoming: list[str] = field(default_factory=list)
50
+ outgoing: list[str] = field(default_factory=list)
51
+
52
+
53
+ @dataclass
54
+ class ExecutionDAG:
55
+ """@classdesc 执行 DAG"""
56
+
57
+ nodes: dict[str, DAGNode] = field(default_factory=dict)
58
+ metadata: dict[str, Any] = field(default_factory=dict)
59
+
60
+
61
+ def build_execution_dag(
62
+ transform_files: dict[str, TransformFile],
63
+ schema_ids: set[str],
64
+ regex_files: dict[str, RegexNodeFile] | None = None,
65
+ ) -> ExecutionDAG:
66
+ """@methoddesc 从 Transform 和 Regex 文件构建执行 DAG
67
+
68
+ 参数:
69
+ transform_files: {transform_id: TransformFile}
70
+ schema_ids: 所有 schema 表 ID 集合
71
+ regex_files: {regex_id: RegexNodeFile},仅 match_mode="extract" 的节点会被加入 DAG
72
+
73
+ 返回:
74
+ ExecutionDAG 实例
75
+ """
76
+ dag = ExecutionDAG()
77
+
78
+ # 1. 注册所有 transform 节点
79
+ for tid, tfile in transform_files.items():
80
+ if not tfile.enabled:
81
+ continue
82
+ dag.nodes[tid] = DAGNode(
83
+ id=tid,
84
+ node_type="transform",
85
+ data=tfile,
86
+ )
87
+
88
+ # 2. 注册所有 schema 数据源节点(虚拟节点,用于连接)
89
+ for sid in schema_ids:
90
+ dag.nodes[sid] = DAGNode(
91
+ id=sid,
92
+ node_type="schema",
93
+ data=None,
94
+ )
95
+
96
+ # 3. 注册 extract 模式的 regex 节点
97
+ if regex_files:
98
+ for rid, rfile in regex_files.items():
99
+ if not rfile.enabled or rfile.match_mode != "extract":
100
+ continue
101
+ dag.nodes[rid] = DAGNode(
102
+ id=rid,
103
+ node_type="regex",
104
+ data=rfile,
105
+ )
106
+
107
+ # 4. 建立边(input_from_node → node)
108
+ dangling_nodes: list[str] = []
109
+ for tid, tfile in transform_files.items():
110
+ if not tfile.enabled:
111
+ continue
112
+ input_node = tfile.input_from_node
113
+ if input_node and input_node in dag.nodes:
114
+ dag.nodes[tid].incoming.append(input_node)
115
+ dag.nodes[input_node].outgoing.append(tid)
116
+ elif input_node and input_node not in dag.nodes:
117
+ logger.warning("[DAG] Transform '%s' 的上游节点 '%s' 不存在(悬空依赖)", tid, input_node)
118
+ dangling_nodes.append(f"{tid} -> {input_node}")
119
+
120
+ if regex_files:
121
+ for rid, rfile in regex_files.items():
122
+ if not rfile.enabled or rfile.match_mode != "extract":
123
+ continue
124
+ input_node = rfile.input_from_node
125
+ if not input_node and rfile.source_ref:
126
+ input_node = rfile.source_ref.table_id
127
+ if input_node and input_node in dag.nodes:
128
+ dag.nodes[rid].incoming.append(input_node)
129
+ dag.nodes[input_node].outgoing.append(rid)
130
+ elif input_node and input_node not in dag.nodes:
131
+ logger.warning("[DAG] Regex '%s' 的上游节点 '%s' 不存在(悬空依赖)", rid, input_node)
132
+ dangling_nodes.append(f"{rid} -> {input_node}")
133
+
134
+ if dangling_nodes:
135
+ dag.metadata["dangling_nodes"] = dangling_nodes
136
+
137
+ return dag
138
+
139
+
140
+ # 向后兼容别名
141
+ build_transform_dag = build_execution_dag