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,406 @@
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 YAML 原子写入与文件锁模块
18
+
19
+ 功能概述:
20
+ - 跨平台文件锁(Windows msvcrt / Unix fcntl)保证并发安全
21
+ - 原子性 YAML 文件写入(先写临时文件再重命名)
22
+ - 优先使用 ruamel.yaml 保留注释和格式,回退到标准 yaml
23
+
24
+ 架构设计:
25
+ - FileLock 上下文管理器实现跨平台文件锁
26
+ - atomic_write_yaml 提供原子写入能力
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import logging
32
+ import os
33
+ import tempfile
34
+ import time
35
+ import uuid
36
+ from io import TextIOWrapper
37
+ from pathlib import Path
38
+ from types import TracebackType
39
+ from typing import Any, Literal
40
+
41
+ logger = logging.getLogger(__name__)
42
+
43
+ # 陈旧锁判定阈值(秒):锁文件 mtime 超过该值视为持有进程已崩溃残留。
44
+ # 正常写 YAML 的持锁时长远小于该值;Windows 的 open(lock, "x") 独占创建
45
+ # 在进程崩溃后不会自动释放(.lock 文件残留),只能按时间阈值接管。
46
+ STALE_LOCK_SECONDS = 30.0
47
+
48
+ # 文件锁导入(跨平台支持)
49
+ msvcrt: Any = None
50
+ fcntl: Any = None
51
+ _HAS_FILE_LOCK = False
52
+
53
+ try:
54
+ # Windows
55
+ import msvcrt
56
+
57
+ _HAS_FILE_LOCK = True
58
+ logger.debug("文件锁: 使用 Windows msvcrt 模式")
59
+ except ImportError:
60
+ logger.debug("Windows文件锁模块未安装", exc_info=True)
61
+
62
+ if not _HAS_FILE_LOCK:
63
+ try:
64
+ # Unix/Linux/Mac
65
+ import fcntl
66
+
67
+ _HAS_FILE_LOCK = True
68
+ logger.debug("文件锁: 使用 Unix fcntl 模式")
69
+ except ImportError:
70
+ pass
71
+
72
+ if not _HAS_FILE_LOCK:
73
+ logger.warning("文件锁功能不可用,并发写入时可能存在竞态条件风险")
74
+
75
+
76
+ class ActionParseError(Exception):
77
+ """
78
+ @classdesc 动作解析异常
79
+
80
+ 当 LLM 返回的响应无法解析为有效 JSON 时抛出。
81
+ """
82
+
83
+ pass
84
+
85
+
86
+ class YamlUpdateError(Exception):
87
+ """
88
+ @classdesc YAML 更新异常
89
+
90
+ 当 YAML 文件的原子写入或文件锁操作失败时抛出。
91
+ """
92
+
93
+ pass
94
+
95
+
96
+ class FileLock:
97
+ """
98
+ @classdesc 跨平台文件锁上下文管理器
99
+
100
+ 支持 Windows(msvcrt) 和 Unix(fcntl) 两种文件锁实现,
101
+ 通过上下文管理器确保并发写入安全。
102
+
103
+ 使用场景:
104
+ - 多个进程/线程同时写入同一 YAML 文件时防止数据损坏
105
+ - 内联约束批量处理时的文件访问控制
106
+
107
+ 使用示例:
108
+ with FileLock("/path/to/file.yaml"):
109
+ # 独占访问文件
110
+ with open("/path/to/file.yaml", 'w') as f:
111
+ yaml.dump(data, f)
112
+ """
113
+
114
+ def __init__(self, file_path: str, timeout: float = 10.0, stale_seconds: float = STALE_LOCK_SECONDS):
115
+ """
116
+ @methoddesc 初始化文件锁
117
+
118
+ 参数:
119
+ file_path: 要加锁的文件路径
120
+ timeout: 获取锁的超时时间(秒),默认 10 秒
121
+ stale_seconds: 陈旧锁判定阈值(秒),锁文件 mtime 超过该值视为崩溃残留并接管
122
+ """
123
+ self.file_path = file_path
124
+ self.timeout = timeout
125
+ self.stale_seconds = stale_seconds
126
+ self.lock_file: TextIOWrapper | None = None
127
+ self.lock_path = f"{file_path}.lock"
128
+ # 本持有者的唯一令牌:获取锁时写入锁文件,释放前重验归属,避免误删他人重建的锁
129
+ self._lock_token: str | None = None
130
+
131
+ def _break_stale_lock(self) -> None:
132
+ """陈旧锁检测:锁文件 mtime 超过 stale_seconds 视为持有进程已崩溃,删除后可重试获取。
133
+
134
+ 仅处理"进程崩溃残留"场景——持锁进程存活时其 mtime 会不断刷新吗?不会,
135
+ 但正常持锁时长(单次 YAML 原子写入)远小于阈值,误接管的概率可忽略;
136
+ 删除失败(他方并发占用)时静默返回,交由外层重试循环处理。
137
+ """
138
+ try:
139
+ stat_result = os.stat(self.lock_path)
140
+ except OSError:
141
+ return # 锁文件不存在,无需处理
142
+ age = time.time() - stat_result.st_mtime
143
+ if age <= self.stale_seconds:
144
+ return
145
+ try:
146
+ os.remove(self.lock_path)
147
+ logger.warning(f"检测到陈旧文件锁({age:.0f}s 前残留),已接管: {self.lock_path}")
148
+ except OSError:
149
+ logger.debug("删除陈旧锁文件失败(可能被并发占用)", exc_info=True)
150
+
151
+ def __enter__(self) -> FileLock:
152
+ """
153
+ @methoddesc 获取文件锁
154
+
155
+ 进入上下文时尝试获取文件锁,如果锁已被占用则等待,
156
+ 超过 timeout 时间后抛出 YamlUpdateError。
157
+ 每次尝试前先做陈旧锁检测,避免崩溃进程残留的 .lock 文件导致永久超时。
158
+
159
+ 返回:
160
+ FileLock 实例自身
161
+ """
162
+ if not _HAS_FILE_LOCK:
163
+ logger.debug(f"文件锁不可用,跳过加锁: {self.file_path}")
164
+ return self
165
+
166
+ start_time = time.time()
167
+
168
+ while True:
169
+ try:
170
+ if msvcrt:
171
+ # Windows: 先做陈旧锁检测,再用独占模式打开锁文件
172
+ self._break_stale_lock()
173
+ self.lock_file = open(self.lock_path, "x") # 独占创建
174
+ else:
175
+ # Unix: 使用 fcntl 文件锁
176
+ self.lock_file = open(self.lock_path, "w")
177
+ fcntl.flock(self.lock_file.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
178
+
179
+ # 写入唯一令牌(进程号 + 随机串):释放前重验锁文件归属,
180
+ # 缓解"自己持有的锁已被陈旧检测接管、他人重建新锁"时的误删竞态
181
+ self._lock_token = f"{os.getpid()}-{uuid.uuid4().hex}"
182
+ try:
183
+ self.lock_file.write(self._lock_token)
184
+ self.lock_file.flush()
185
+ except OSError:
186
+ # 令牌写入失败时释放路径退化为直接删除(保持旧行为)
187
+ logger.debug("写入锁令牌失败", exc_info=True)
188
+
189
+ logger.debug(f"获取文件锁成功: {self.lock_path}")
190
+ return self
191
+
192
+ except OSError as e:
193
+ if self.lock_file:
194
+ self.lock_file.close()
195
+ self.lock_file = None
196
+
197
+ if time.time() - start_time > self.timeout:
198
+ raise YamlUpdateError(f"获取文件锁超时: {self.lock_path}") from e
199
+
200
+ time.sleep(0.1) # 100ms 后重试
201
+
202
+ def __exit__(
203
+ self,
204
+ exc_type: type[BaseException] | None,
205
+ exc_val: BaseException | None,
206
+ exc_tb: TracebackType | None,
207
+ ) -> Literal[False]:
208
+ """
209
+ @methoddesc 释放文件锁
210
+
211
+ 退出上下文时释放文件锁并尝试删除锁文件。
212
+ 删除前先 close 自身句柄,再重验锁文件内容是否仍为本次写入的令牌:
213
+ 若内容已变(自己的锁被陈旧检测接管、他人重建了新锁)则跳过删除,避免误删他人锁。
214
+ 不会吞掉异常。
215
+
216
+ 返回:
217
+ False(不吞掉异常)
218
+ """
219
+ if self.lock_file:
220
+ try:
221
+ # 只在有 fcntl 模块的系统上解锁(Unix)
222
+ if fcntl is not None:
223
+ fcntl.flock(self.lock_file.fileno(), fcntl.LOCK_UN)
224
+ # 先 close 自身句柄再删除,缩短"句柄未关即删"的窗口
225
+ self.lock_file.close()
226
+
227
+ # 删除前重验所有权:锁文件内容不再是自己的令牌 → 已被他人接管,跳过删除
228
+ if self._lock_token is not None:
229
+ try:
230
+ with open(self.lock_path, encoding="utf-8") as lf:
231
+ current_token = lf.read()
232
+ if current_token != self._lock_token:
233
+ logger.debug("锁文件已被其他持有者接管,跳过删除: %s", self.lock_path)
234
+ return False
235
+ except FileNotFoundError:
236
+ # 锁文件已不存在(被接管删除),无需再删
237
+ return False
238
+ except OSError:
239
+ logger.debug("重读锁文件失败,按原行为尝试删除", exc_info=True)
240
+
241
+ # 尝试删除锁文件
242
+ try:
243
+ os.remove(self.lock_path)
244
+ except OSError:
245
+ logger.debug("删除锁文件失败", exc_info=True)
246
+
247
+ logger.debug(f"释放文件锁: {self.lock_path}")
248
+ except Exception as e:
249
+ logger.warning(f"释放文件锁时出错: {e}")
250
+
251
+ return False # 不吞掉异常
252
+
253
+
254
+ def atomic_write_yaml(file_path: Path, data: dict[str, Any], preserve_format: bool = True) -> None:
255
+ """
256
+ @methoddesc 原子性写入 YAML 文件
257
+
258
+ 先写入临时文件,再重命名,确保不会因为崩溃导致原文件损坏。
259
+ 当 preserve_format=True 且文件存在时,使用 ruamel.yaml 保留注释和空行。
260
+
261
+ 参数:
262
+ file_path: 目标文件路径
263
+ data: 要写入的数据字典
264
+ preserve_format: 是否保留原有格式(注释、空行等)
265
+
266
+ 异常:
267
+ YamlUpdateError: 写入失败时抛出
268
+
269
+ 示例:
270
+ >>> atomic_write_yaml(Path("config.yaml"), {"version": 2})
271
+ """
272
+ import yaml
273
+
274
+ file_path = Path(file_path)
275
+
276
+ try:
277
+ # 如果文件已存在且需要保留格式,使用 ruamel.yaml
278
+ if preserve_format and file_path.exists():
279
+ try:
280
+ from ruamel.yaml import YAML
281
+
282
+ # 使用 ruamel.yaml 加载保留格式的 YAML
283
+ yaml_parser = YAML()
284
+ yaml_parser.preserve_quotes = True
285
+ yaml_parser.default_flow_style = False
286
+
287
+ with open(file_path, encoding="utf-8") as f:
288
+ existing_data = yaml_parser.load(f)
289
+
290
+ if existing_data is not None:
291
+ # 递归更新数据,保留格式
292
+ _update_yaml_data(existing_data, data)
293
+
294
+ # 原子性写入
295
+ fd, temp_path = tempfile.mkstemp(
296
+ dir=file_path.parent, prefix=f".{file_path.stem}_tmp_", suffix=".yaml"
297
+ )
298
+
299
+ try:
300
+ with os.fdopen(fd, "w", encoding="utf-8") as f:
301
+ yaml_parser.dump(existing_data, f)
302
+ os.replace(temp_path, file_path)
303
+ logger.debug(f"原子写入成功(保留格式): {file_path}")
304
+ return
305
+ except OSError:
306
+ try:
307
+ os.remove(temp_path)
308
+ except OSError:
309
+ logger.debug("删除临时文件失败", exc_info=True)
310
+ raise
311
+ except ImportError:
312
+ logger.warning("ruamel.yaml 未安装,无法保留 YAML 格式")
313
+ except Exception as e:
314
+ logger.warning(f"使用 ruamel.yaml 失败,回退到标准方式: {e}")
315
+
316
+ # 标准写入方式(不保留格式)
317
+ fd, temp_path = tempfile.mkstemp(dir=file_path.parent, prefix=f".{file_path.stem}_tmp_", suffix=".yaml")
318
+
319
+ try:
320
+ with os.fdopen(fd, "w", encoding="utf-8") as f:
321
+ yaml.safe_dump(data, f, allow_unicode=True, sort_keys=False)
322
+
323
+ os.replace(temp_path, file_path)
324
+ logger.debug(f"原子写入成功: {file_path}")
325
+
326
+ except OSError:
327
+ try:
328
+ os.remove(temp_path)
329
+ except OSError:
330
+ logger.debug("删除临时文件失败", exc_info=True)
331
+ raise
332
+
333
+ except Exception as e:
334
+ raise YamlUpdateError(f"原子写入失败: {file_path}") from e
335
+
336
+
337
+ def _update_yaml_data(existing: Any, new: Any) -> None:
338
+ """
339
+ @methoddesc 递归更新 YAML 数据,保留现有格式
340
+
341
+ 用于 ruamel.yaml 加载的数据结构,在保留注释和格式的前提下递归更新数据。
342
+
343
+ 参数:
344
+ existing: 现有的 YAML 数据结构(来自 ruamel.yaml)
345
+ new: 新的数据字典
346
+ """
347
+ from ruamel.yaml.comments import CommentedMap, CommentedSeq
348
+
349
+ if isinstance(existing, CommentedMap) and isinstance(new, dict):
350
+ # 更新或添加键值
351
+ for key, value in new.items():
352
+ if key in existing:
353
+ if isinstance(value, dict) and isinstance(existing[key], CommentedMap):
354
+ _update_yaml_data(existing[key], value)
355
+ elif isinstance(value, list) and isinstance(existing[key], CommentedSeq):
356
+ _update_yaml_list(existing[key], value)
357
+ else:
358
+ existing[key] = value
359
+ else:
360
+ # 新键,直接添加
361
+ existing[key] = value
362
+ elif isinstance(existing, CommentedSeq) and isinstance(new, list):
363
+ _update_yaml_list(existing, new)
364
+
365
+
366
+ def _update_yaml_list(existing_list: Any, new_list: list) -> None:
367
+ """
368
+ @methoddesc 更新 YAML 列表,保留现有格式
369
+
370
+ 用于更新 constraints 等列表,根据 id 匹配更新现有项或添加新项。
371
+ 没有 id 的项直接追加到列表末尾。
372
+
373
+ 参数:
374
+ existing_list: 现有的 YAML 列表(来自 ruamel.yaml)
375
+ new_list: 新的数据列表
376
+ """
377
+ from ruamel.yaml.comments import CommentedMap
378
+
379
+ if not new_list:
380
+ return
381
+
382
+ # 为现有项建立 id 到索引的映射
383
+ existing_ids = {}
384
+ for idx, item in enumerate(existing_list):
385
+ if isinstance(item, CommentedMap) and "id" in item:
386
+ existing_ids[item["id"]] = idx
387
+
388
+ # 处理新列表中的每一项
389
+ for new_item in new_list:
390
+ if isinstance(new_item, dict) and "id" in new_item:
391
+ item_id = new_item["id"]
392
+ if item_id in existing_ids:
393
+ # 更新现有项
394
+ idx = existing_ids[item_id]
395
+ existing_item = existing_list[idx]
396
+ if isinstance(existing_item, CommentedMap):
397
+ _update_yaml_data(existing_item, new_item)
398
+ else:
399
+ existing_list[idx] = new_item
400
+ else:
401
+ # 添加新项
402
+ existing_list.append(new_item)
403
+ else:
404
+ # 没有 id 的项,直接添加
405
+ if new_item not in existing_list:
406
+ existing_list.append(new_item)
@@ -0,0 +1,18 @@
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
+ """
@@ -0,0 +1,145 @@
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
+ - 预览场景专用数据加载入口
21
+ - 使用新版 Spec + load_source_data 架构
22
+ - header_enabled=False(预览不识别表头,对应旧版 header=None)
23
+ - nrows 在加载时限制(CSV/Excel)
24
+ - 不做空值清洗,保持原始值供 str() 转换
25
+
26
+ 架构设计:
27
+ - 职责隔离:preview 有独立入口,不触碰 validation/loader.py
28
+ - 直接利用新版架构(Spec → Loader → Registry),符合项目 V2 方向
29
+ - 为后续扩展(encoding、delimiter 预览参数)提供清晰位置
30
+
31
+ 输入示例:
32
+ df, sheet_names = load_preview_data(
33
+ file_path="data/users.xlsx",
34
+ file_type="excel",
35
+ max_rows=100,
36
+ sheet_name="Sheet1"
37
+ )
38
+
39
+ 输出示例:
40
+ (DataFrame, sheet_names)
41
+ - sheet_names 仅在 excel 时返回,其他格式为 None
42
+ """
43
+
44
+ from __future__ import annotations
45
+
46
+ import logging
47
+
48
+ import pandas as pd
49
+
50
+ from app.shared.core.data_source.loaders import load_source_data
51
+ from app.shared.core.data_source.loaders.excel_loader import get_excel_sheet_names
52
+ from app.shared.core.data_source.specs.csv_source import CSVSourceSpec
53
+ from app.shared.core.data_source.specs.excel_source import ExcelSourceSpec
54
+ from app.shared.core.data_source.specs.json_source import JSONSourceSpec
55
+
56
+ logger = logging.getLogger(__name__)
57
+
58
+
59
+ def load_preview_data(
60
+ file_path: str,
61
+ file_type: str,
62
+ max_rows: int,
63
+ sheet_name: str | None = None,
64
+ source_config: dict | None = None,
65
+ ) -> tuple[pd.DataFrame, list[str] | None]:
66
+ """
67
+ @methoddesc 预览专用数据加载器
68
+
69
+ 使用新版 Spec + load_source_data 架构加载预览数据。
70
+ - Excel/CSV:在加载时限制 nrows,不做空值清洗,保持原始数据供前端展示
71
+ - JSON:复用 JSONSourceSpec 加载(无 nrows 字段,加载后按 max_rows 截断)
72
+
73
+ 参数:
74
+ file_path: 待加载文件的完整路径
75
+ file_type: 文件类型,可选值为 "excel" / "csv" / "json"
76
+ max_rows: 最大读取行数,用于限制预览数据量
77
+ sheet_name: Excel 工作表名称(仅 Excel 格式有效,可选)
78
+ source_config: 额外的源配置字典,如编码(encoding)、分隔符(delimiter)
79
+
80
+ 返回:
81
+ 元组 (DataFrame, sheet_names)
82
+ - DataFrame: 加载的原始数据(未清洗,保留空值和原始格式)
83
+ - sheet_names: 仅在 Excel 格式时返回工作表名称列表,其他格式返回 None
84
+
85
+ 异常:
86
+ ValueError: 当 file_type 为不支持的类型时抛出
87
+ FileNotFoundError: 当文件不存在时抛出
88
+ """
89
+ # ======== Excel 格式处理 ========
90
+ if file_type == "excel":
91
+ # 构造 Excel 数据源规格,关闭表头识别、限制读取行数
92
+ spec: ExcelSourceSpec | CSVSourceSpec | JSONSourceSpec = ExcelSourceSpec(
93
+ path=file_path,
94
+ sheet=sheet_name,
95
+ header_enabled=False,
96
+ nrows=max_rows,
97
+ )
98
+ df = load_source_data(spec)
99
+
100
+ # 额外读取全部工作表名称,供前端切换展示。
101
+ # 引擎按扩展名自适应(.xls→xlrd),句柄在助手内确保关闭——
102
+ # 原实现硬编码 openpyxl 且依赖此处 close,曾致 .xls 必炸/句柄泄漏。
103
+ sheet_names = get_excel_sheet_names(file_path)
104
+
105
+ return df, sheet_names
106
+
107
+ # ======== CSV 格式处理 ========
108
+ if file_type == "csv":
109
+ # 从 source_config 提取编码和分隔符,使用默认值兜底
110
+ sc = source_config or {}
111
+ spec = CSVSourceSpec(
112
+ path=file_path,
113
+ header_enabled=False,
114
+ nrows=max_rows,
115
+ encoding=sc.get("encoding", "utf-8"),
116
+ delimiter=sc.get("delimiter", ","),
117
+ )
118
+ df = load_source_data(spec)
119
+ return df, None
120
+
121
+ # ======== JSON 格式处理(复用 JSONSourceSpec + load_source_data 管线) ========
122
+ if file_type == "json":
123
+ # 从 source_config 提取 JSON 格式与 json_path
124
+ sc = source_config or {}
125
+ # D8: format 必填,默认 array(顶层数组);.jsonl/.ndjson 文件强制 lines(与 validation 路径一致)
126
+ import os
127
+
128
+ ext = os.path.splitext(file_path)[1].lower()
129
+ if ext in [".jsonl", ".ndjson"]:
130
+ fmt = "lines"
131
+ else:
132
+ fmt = sc.get("json_format") or "array"
133
+ spec = JSONSourceSpec(
134
+ path=file_path,
135
+ format=fmt,
136
+ json_path=sc.get("json_path"),
137
+ )
138
+ df = load_source_data(spec)
139
+ # JSONSourceSpec 无 nrows 字段,加载后截断到预览行数上限
140
+ if len(df) > max_rows:
141
+ df = df.head(max_rows)
142
+ return df, None
143
+
144
+ # 如果走到这里,说明传入的文件类型不在支持列表中
145
+ raise ValueError(f"不支持的文件类型: {file_type}")