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,268 @@
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 JSONPathExtractor 数据提取器
18
+
19
+ 功能概述:
20
+ - 实现 JSONPath 表达式的数据提取
21
+ - 支持 $.key, $.key.subkey, $..key 递归查找等语法
22
+ - 提取嵌套 JSON 数据中的特定路径
23
+
24
+ 架构设计:
25
+ - 独立的提取器类,职责单一
26
+ - 支持自定义错误处理
27
+ - 可扩展的路径解析器
28
+
29
+ 输入示例:
30
+ data = {"users": [{"name": "Alice"}, {"name": "Bob"}]}
31
+ path = "$.users"
32
+
33
+ 输出示例:
34
+ [{"name": "Alice"}, {"name": "Bob"}]
35
+ """
36
+
37
+ from __future__ import annotations
38
+
39
+ from typing import Any
40
+
41
+
42
+ class JSONPathExtractor:
43
+ """
44
+ @classdesc JSONPath 数据提取器
45
+
46
+ ============================================================================
47
+ 功能说明
48
+ ============================================================================
49
+ 实现简单的 JSONPath 表达式解析和数据提取。
50
+ 支持常用的 JSONPath 语法,适用于大多数数据提取场景。
51
+
52
+ ============================================================================
53
+ 支持的语法
54
+ ============================================================================
55
+ - $.key - 访问对象属性
56
+ - $.key.subkey - 嵌套属性访问
57
+ - $..key - 递归查找(返回所有匹配的值)
58
+ - $.key[*] - 数组全部元素
59
+ - $.key[0] - 数组指定索引
60
+ - $ - 根节点
61
+
62
+ ============================================================================
63
+ 业务场景
64
+ ============================================================================
65
+ - 从嵌套 JSON 中提取数据
66
+ - 处理 API 返回的复杂 JSON 结构
67
+ - 数据清洗和转换
68
+
69
+ ============================================================================
70
+ 使用示例
71
+ ============================================================================
72
+ >>> extractor = JSONPathExtractor()
73
+ >>>
74
+ >>> # 基本提取
75
+ >>> data = {"users": [{"name": "Alice"}, {"name": "Bob"}]}
76
+ >>> result = extractor.extract(data, "$.users")
77
+ >>> # [{"name": "Alice"}, {"name": "Bob"}]
78
+ >>>
79
+ >>> # 递归查找
80
+ >>> data = {"a": {"b": {"c": 1}}, "d": 2}
81
+ >>> result = extractor.extract(data, "$..c")
82
+ >>> # [1]
83
+ """
84
+
85
+ def __init__(self) -> None:
86
+ """
87
+ @methoddesc 初始化 JSONPath 提取器
88
+
89
+ 创建提取器实例,无需额外参数。
90
+ """
91
+ pass
92
+
93
+ def extract(self, data: Any, jsonpath: str) -> list[dict]:
94
+ """
95
+ @methoddesc 从 JSON 数据中提取指定路径的数据
96
+
97
+ ============================================================================
98
+ 处理逻辑
99
+ ============================================================================
100
+ 1. 规范化路径(移除 $ 前缀)
101
+ 2. 根据路径类型选择提取策略
102
+ - 递归查找 (..key): 使用深度优先搜索
103
+ - 普通路径 (key.subkey): 逐层解析
104
+ 3. 展平结果(如果是嵌套列表)
105
+
106
+ Args:
107
+ data: JSON 数据(字典或列表)
108
+ jsonpath: JSONPath 表达式
109
+
110
+ Returns:
111
+ 提取后的数据列表
112
+
113
+ Raises:
114
+ ValueError: 路径不存在或格式错误
115
+ """
116
+ if not jsonpath or jsonpath == "$":
117
+ return data if isinstance(data, list) else [data]
118
+
119
+ normalized_path = self._normalize_path(jsonpath)
120
+
121
+ if normalized_path.startswith(".."):
122
+ return self._extract_recursive(data, normalized_path[2:])
123
+
124
+ return self._extract_by_path(data, normalized_path)
125
+
126
+ def _normalize_path(self, path: str) -> str:
127
+ """
128
+ @methoddesc 规范化 JSONPath
129
+
130
+ 移除 $ 前缀,统一路径格式。
131
+
132
+ Args:
133
+ path: 原始路径
134
+
135
+ Returns:
136
+ 规范化后的路径
137
+ """
138
+ if path.startswith("$."):
139
+ return path[2:]
140
+ elif path.startswith("$"):
141
+ return path[1:]
142
+ return path
143
+
144
+ def _extract_by_path(self, data: Any, path: str) -> list[dict]:
145
+ """
146
+ @methoddesc 根据路径提取数据
147
+
148
+ Args:
149
+ data: JSON 数据
150
+ path: 路径(不含 $ 前缀)
151
+
152
+ Returns:
153
+ 提取后的数据列表
154
+ """
155
+ current = data
156
+ parts = self._parse_path(path)
157
+
158
+ for part in parts:
159
+ current = self._navigate_step(current, part)
160
+
161
+ if current is None:
162
+ return []
163
+
164
+ if isinstance(current, list):
165
+ return current
166
+ else:
167
+ return [current] if current is not None else []
168
+
169
+ def _parse_path(self, path: str) -> list[str | int]:
170
+ """
171
+ @methoddesc 解析路径为组成部分
172
+
173
+ Args:
174
+ path: 路径字符串
175
+
176
+ Returns:
177
+ 路径部分列表(字符串或整数)
178
+ """
179
+ parts: list[str | int] = []
180
+ current = ""
181
+ i = 0
182
+
183
+ while i < len(path):
184
+ char = path[i]
185
+
186
+ if char == ".":
187
+ if current:
188
+ parts.append(current)
189
+ current = ""
190
+ elif char == "[":
191
+ if current:
192
+ parts.append(current)
193
+ current = ""
194
+ end = path.find("]", i)
195
+ if end != -1:
196
+ key = path[i + 1 : end].strip("\"'")
197
+ try:
198
+ parts.append(int(key))
199
+ except ValueError:
200
+ parts.append(key)
201
+ i = end
202
+ else:
203
+ current += char
204
+
205
+ i += 1
206
+
207
+ if current:
208
+ parts.append(current)
209
+
210
+ return parts
211
+
212
+ def _navigate_step(self, data: Any, step: str | int) -> Any:
213
+ """
214
+ @methoddesc 单步导航
215
+
216
+ 根据当前数据类型(字典或列表)和路径步骤,
217
+ 导航到下一层数据。
218
+
219
+ Args:
220
+ data: 当前数据
221
+ step: 路径部分(字符串键或整数索引)
222
+
223
+ Returns:
224
+ 下一步的数据
225
+ """
226
+ if isinstance(data, dict):
227
+ return data.get(step)
228
+ elif isinstance(data, list):
229
+ if isinstance(step, int):
230
+ if 0 <= step < len(data):
231
+ return data[step]
232
+ return None
233
+ elif step == "*":
234
+ return data
235
+ return None
236
+ return None
237
+
238
+ def _extract_recursive(self, data: Any, key: str) -> list[Any]:
239
+ """
240
+ @methoddesc 递归查找指定键的所有值
241
+
242
+ Args:
243
+ data: JSON 数据
244
+ key: 要查找的键名
245
+
246
+ Returns:
247
+ 所有匹配的值列表
248
+ """
249
+ results: list[Any] = []
250
+
251
+ def search(obj: Any) -> None:
252
+ """
253
+ @methoddesc 递归搜索对象中的目标键值
254
+
255
+ 参数:
256
+ obj: 当前搜索的对象(字典或列表)
257
+ """
258
+ if isinstance(obj, dict):
259
+ if key in obj:
260
+ results.append(obj[key])
261
+ for value in obj.values():
262
+ search(value)
263
+ elif isinstance(obj, list):
264
+ for item in obj:
265
+ search(item)
266
+
267
+ search(data)
268
+ return results
@@ -0,0 +1,354 @@
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 JSON 数据源加载器模块
18
+
19
+ 功能概述:
20
+ - 加载 JSON / JSONL / NDJSON 文件为 pandas DataFrame
21
+ - 使用策略模式自动识别 JSON 格式(array / lines / object / auto)
22
+ - 支持 JSONPath 数据提取(B14: 空列表不再回退到原始未过滤数据)
23
+ - 支持 record_path 通过 pd.json_normalize 展开嵌套数组(B15)
24
+ - 加载后应用 dtype 转换(B17)
25
+ - 自动解析器支持递归查找列表元素(B18)
26
+ - JSON Lines 中非字典条目包装为 {"value": parsed}(B19)
27
+
28
+ 架构设计:
29
+ - 继承 DataSourceLoader[JSONSourceSpec],通过注册表自动发现
30
+ - 解析逻辑委托给 strategies/ 下的策略类(AutoDetectParser、LinesParser 等)
31
+ - 数据提取委托给 JSONPathExtractor
32
+ - 类型转换委托给 TypeConverter
33
+
34
+ 输入示例:
35
+ spec = JSONSourceSpec(
36
+ path="data/users.json",
37
+ format="auto",
38
+ json_path="$.data.users",
39
+ record_path="orders",
40
+ dtype={"age": "int64"}
41
+ )
42
+ loader = JSONLoader(spec)
43
+
44
+ 输出示例:
45
+ df = loader.load()
46
+ # 返回 pandas.DataFrame,JSONPath 和 record_path 已应用
47
+ # 若 json_path 返回空列表则抛出 DataLoadError
48
+ """
49
+
50
+ from __future__ import annotations
51
+
52
+ import json
53
+ from pathlib import Path
54
+
55
+ import pandas as pd
56
+
57
+ from ..specs.json_source import JSONSourceSpec
58
+ from .base import DataLoadError, DataSourceLoader
59
+ from .converter import TypeConverter
60
+ from .extractor import JSONPathExtractor
61
+ from .registry import register_loader
62
+ from .strategies import JSONParserStrategy, get_parser
63
+
64
+
65
+ @register_loader("json")
66
+ class JSONLoader(DataSourceLoader[JSONSourceSpec]):
67
+ """
68
+ @classdesc JSON 文件加载器
69
+
70
+ ============================================================================
71
+ 功能说明
72
+ ============================================================================
73
+ 使用策略模式加载和解析 JSON 文件,支持多种格式。
74
+ 核心职责是流程编排,实际解析逻辑委托给策略类和工具类。
75
+
76
+ ============================================================================
77
+ 支持的 JSON 格式
78
+ ============================================================================
79
+ - 标准数组 (array): [{"a": 1}, {"a": 2}]
80
+ - JSON Lines (lines): 每行一个 JSON 对象
81
+ - 嵌套对象 (object): {"data": [{"a": 1}]}
82
+ - 自动检测 (auto): 自动识别最佳格式
83
+
84
+ ============================================================================
85
+ 架构设计
86
+ ============================================================================
87
+ 加载流程:
88
+ 1. 读取文件内容
89
+ 2. 使用 ParserStrategy 解析
90
+ 3. 使用 JSONPathExtractor 提取数据
91
+ 4. 使用 TypeConverter 转换类型
92
+ 5. 转换为 DataFrame
93
+
94
+ ============================================================================
95
+ 使用示例
96
+ ============================================================================
97
+ >>> from app.shared.core.data_source.loaders.json_loader import JSONLoader
98
+ >>> from app.shared.core.data_source.specs.json_source import JSONSourceSpec
99
+ >>>
100
+ >>> spec = JSONSourceSpec(path="data/users.json", format="auto")
101
+ >>> loader = JSONLoader(spec)
102
+ >>> df = loader.load()
103
+ """
104
+
105
+ spec_class = JSONSourceSpec
106
+
107
+ FORMAT_ARRAY = "array"
108
+ FORMAT_LINES = "lines"
109
+ FORMAT_OBJECT = "object"
110
+
111
+ def __init__(self, spec: JSONSourceSpec):
112
+ """
113
+ 初始化 JSON 加载器
114
+
115
+ Args:
116
+ spec: JSON 数据源配置
117
+ """
118
+ super().__init__(spec)
119
+ self._parser: JSONParserStrategy | None = None
120
+ self._extractor = JSONPathExtractor()
121
+ self._converter = TypeConverter()
122
+
123
+ def load(self) -> pd.DataFrame:
124
+ """
125
+ @methoddesc 加载 JSON 文件并返回 DataFrame
126
+
127
+ ============================================================================
128
+ 处理流程
129
+ ============================================================================
130
+ 1. 读取文件内容
131
+ 2. 使用策略模式解析 JSON
132
+ 3. 应用 JSONPath 提取(如果配置了)
133
+ 4. 转换数据类型(如果配置了)
134
+ 5. 转换为 DataFrame 并扁平化
135
+
136
+ Returns:
137
+ 加载的数据 DataFrame
138
+
139
+ Raises:
140
+ DataLoadError: 加载失败时抛出
141
+ """
142
+ try:
143
+ content = self._read_file()
144
+
145
+ if self.spec.json_path and self.spec.format in (self.FORMAT_OBJECT, self.FORMAT_ARRAY):
146
+ # 用户已通过 json_path 显式指定记录路径(object/array 格式均可),
147
+ # 无需走策略解析器(避免 D8 ArrayParser 对嵌套对象的启发式拒绝)。
148
+ # 直接解析原始 JSON,由 extractor 按 json_path 精确定位数据数组。
149
+ # 注:json_path 即显式消歧,无任何"取最长/取第一个数组"的猜测。
150
+ records = json.loads(content)
151
+ extracted = self._extractor.extract(records, self.spec.json_path)
152
+ # B14: 空列表是合法结果,不回退到未过滤数据。
153
+ # - extracted 为非空 list → 直接使用
154
+ # - extracted 为空 list → 保持空(path 未命中,不回退原始数据)
155
+ # - extracted 为非 list(标量/字典)→ 包装为单元素列表
156
+ if isinstance(extracted, list):
157
+ records = extracted
158
+ else:
159
+ records = [extracted] if extracted is not None else []
160
+ else:
161
+ # 无 json_path(或 lines 格式):走策略解析器。
162
+ # D8 对顶层数组/单条 dict 的处理 + 对嵌套对象的拒绝在此生效。
163
+ parser = self._get_parser()
164
+ records = parser.parse(content)
165
+
166
+ df = self._convert_to_dataframe(records)
167
+
168
+ if self.spec.flatten and self._has_nested_structure(df):
169
+ df = self._flatten_dataframe(df)
170
+
171
+ # dtype 转换在扁平化之后执行,确保列名匹配(B17)
172
+ dtype = getattr(self.spec, "dtype", None)
173
+ if dtype:
174
+ df = self._apply_dtype_to_dataframe(df, dtype)
175
+
176
+ return df
177
+
178
+ except FileNotFoundError as e:
179
+ raise DataLoadError(f"文件不存在: {self.spec.path}", self.spec, e)
180
+ except json.JSONDecodeError as e:
181
+ raise DataLoadError(f"JSON 解析错误 (行 {e.lineno}, 列 {e.colno}): {e.msg}", self.spec, e)
182
+ except DataLoadError:
183
+ raise
184
+ except Exception as e:
185
+ raise DataLoadError(f"JSON 加载失败: {e}", self.spec, e)
186
+
187
+ def _read_file(self) -> str:
188
+ """
189
+ @methoddesc 读取文件内容
190
+
191
+ Returns:
192
+ 文件内容字符串
193
+ """
194
+ with open(self.spec.path, encoding=self.spec.encoding) as f:
195
+ return f.read()
196
+
197
+ def _get_parser(self) -> JSONParserStrategy:
198
+ """
199
+ @methoddesc 获取合适的 JSON 解析器
200
+
201
+ 根据配置选择或自动检测合适的解析策略。
202
+
203
+ Returns:
204
+ JSON 解析策略实例
205
+ """
206
+ if self._parser:
207
+ return self._parser
208
+
209
+ # D8: format 必须由 spec 显式提供(auto 已废弃)。spec 层已强制 format 必填,
210
+ # 此处直接取 spec.format;若为 None(异常情况)由 get_parser 报错引导。
211
+ format_type = self.spec.format
212
+ if not format_type:
213
+ raise ValueError(
214
+ "JSON 数据源未指定 format。请显式指定:array(顶层数组)、"
215
+ "lines(JSON Lines)、或 object(嵌套对象,需配合 json_path)。"
216
+ )
217
+ self._parser = get_parser(format_type)
218
+ return self._parser
219
+
220
+ def _convert_to_dataframe(self, data: list[dict]) -> pd.DataFrame:
221
+ """
222
+ @methoddesc 将数据列表转换为 DataFrame
223
+
224
+ Args:
225
+ data: 数据字典列表
226
+
227
+ Returns:
228
+ DataFrame
229
+ """
230
+ if not data:
231
+ return pd.DataFrame()
232
+
233
+ standardized = []
234
+ for record in data:
235
+ if isinstance(record, dict):
236
+ standardized.append(record)
237
+ else:
238
+ standardized.append({"value": record})
239
+
240
+ # 支持 record_path 展开嵌套数组(B15)
241
+ # 设计说明:record_path 统一由 pd.json_normalize 处理,不再需要预标准化步骤。
242
+ # pd.json_normalize 的 record_path 参数会自动将嵌套数组展开为独立行,
243
+ # 与之前的手动 _apply_record_path 逻辑等价,且更可靠。
244
+ record_path = getattr(self.spec, "record_path", None)
245
+ if record_path:
246
+ meta_prefix = getattr(self.spec, "meta_prefix", "meta.")
247
+ sep = getattr(self.spec, "sep", ".")
248
+ df = pd.json_normalize(
249
+ standardized,
250
+ record_path=record_path,
251
+ meta_prefix=meta_prefix,
252
+ sep=sep,
253
+ )
254
+ else:
255
+ df = pd.json_normalize(standardized)
256
+ return df
257
+
258
+ def _has_nested_structure(self, df: pd.DataFrame) -> bool:
259
+ """
260
+ @methoddesc 检查 DataFrame 是否包含嵌套结构
261
+
262
+ Args:
263
+ df: DataFrame
264
+
265
+ Returns:
266
+ 是否存在嵌套结构
267
+ """
268
+ for col in df.columns:
269
+ if df[col].dtype == "object":
270
+ sample = df[col].dropna().head(100)
271
+ for val in sample:
272
+ if isinstance(val, (dict, list)):
273
+ return True
274
+ return False
275
+
276
+ def _flatten_dataframe(self, df: pd.DataFrame) -> pd.DataFrame:
277
+ """
278
+ @methoddesc 扁平化 DataFrame 中的嵌套结构
279
+
280
+ Args:
281
+ df: 原始 DataFrame
282
+
283
+ Returns:
284
+ 扁平化后的 DataFrame
285
+ """
286
+ if df.empty:
287
+ return df
288
+
289
+ records = df.to_dict("records")
290
+
291
+ flat_df = pd.json_normalize(records, sep=self.spec.sep, max_level=self.spec.max_depth)
292
+
293
+ return flat_df
294
+
295
+ def validate(self) -> list[str]:
296
+ """
297
+ @methoddesc 验证 JSON 文件配置和文件本身。
298
+
299
+ 检查项:
300
+ - 文件是否存在
301
+ - 文件扩展名是否为 .json、.jsonl 或 .ndjson
302
+ - 前 5 行是否为有效的 JSON 格式(仅 JSON Lines 文件)
303
+
304
+ Returns:
305
+ 错误信息列表,空列表表示验证通过
306
+
307
+ 示例:
308
+ >>> errors = loader.validate()
309
+ >>> if errors:
310
+ ... print("验证失败:", errors)
311
+ """
312
+ errors = []
313
+ path = Path(self.spec.path)
314
+
315
+ if not path.exists():
316
+ errors.append(f"文件不存在: {self.spec.path}")
317
+ return errors
318
+
319
+ if path.suffix.lower() not in [".json", ".jsonl", ".ndjson"]:
320
+ errors.append(f"警告: 文件扩展名不是 .json/.jsonl/.ndjson: {path.suffix}")
321
+
322
+ try:
323
+ with open(path, encoding=self.spec.encoding) as f:
324
+ for _ in range(5):
325
+ line = f.readline()
326
+ if not line:
327
+ break
328
+ if path.suffix.lower() in [".jsonl", ".ndjson"]:
329
+ json.loads(line.strip())
330
+ except json.JSONDecodeError as e:
331
+ errors.append(f"JSON 格式错误: {e.msg}")
332
+ except Exception as e:
333
+ errors.append(f"无法读取文件: {e}")
334
+
335
+ return errors
336
+
337
+ def _apply_dtype_to_dataframe(self, df: pd.DataFrame, dtype: dict[str, str]) -> pd.DataFrame:
338
+ """@methoddesc 对 DataFrame 的指定列进行类型转换(B17)。"""
339
+ if df.empty or not dtype:
340
+ return df
341
+ for col, type_str in dtype.items():
342
+ if col in df.columns:
343
+ df[col] = df[col].apply(lambda x: self._converter.convert_single(x, type_str))
344
+ return df
345
+
346
+ def get_schema(self) -> dict[str, str]:
347
+ """
348
+ @methoddesc 获取数据源的 schema
349
+
350
+ Returns:
351
+ 列名到类型的映射
352
+ """
353
+ df = self.preview(nrows=100)
354
+ return {col: str(dtype) for col, dtype in df.dtypes.items()}
@@ -0,0 +1,88 @@
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
+ - 实现装饰器 register_loader 用于自动注册加载器类
22
+ - 支持按数据源类型获取加载器类或实例
23
+ - 提供加载器存在性检测功能
24
+
25
+ 架构设计:
26
+ - 全局字典 LOADER_REGISTRY 存储类型到加载器类的映射
27
+ - 装饰器模式实现声明式注册
28
+ - 支持按 discriminator_value 精确匹配
29
+ - 与规格类的注册机制对称设计
30
+
31
+ 输入示例:
32
+ >>> @register_loader("json")
33
+ ... class JSONLoader(DataSourceLoader[JSONSourceSpec]):
34
+ ... spec_class = JSONSourceSpec
35
+ ... def load(self) -> pd.DataFrame:
36
+ ... pass
37
+
38
+ 输出示例:
39
+ >>> loader_class = get_loader_class_for_type("json")
40
+ >>> loader_instance = get_loader_for_spec(spec)
41
+ >>> supports_source_type("csv")
42
+ True
43
+ """
44
+
45
+ from __future__ import annotations
46
+
47
+ from collections.abc import Callable
48
+ from typing import TYPE_CHECKING
49
+
50
+ if TYPE_CHECKING:
51
+ from .base import DataSourceLoader
52
+
53
+
54
+ LOADER_REGISTRY: dict[str, type[DataSourceLoader]] = {}
55
+
56
+
57
+ def register_loader(spec_discriminator: str) -> Callable[[type[DataSourceLoader]], type[DataSourceLoader]]:
58
+ """
59
+ @methoddesc 装饰器:注册加载器到全局注册表
60
+
61
+ Args:
62
+ spec_discriminator: 数据源类型标识符,应与 Spec 类的 discriminator_value 一致
63
+
64
+ 示例:
65
+ >>> @register_loader("json")
66
+ ... class JSONLoader(DataSourceLoader[JSONSpec]):
67
+ ... pass
68
+ """
69
+
70
+ def decorator(loader_class: type[DataSourceLoader]) -> type[DataSourceLoader]:
71
+ """
72
+ @methoddesc 注册加载器类到全局注册表
73
+
74
+ 参数:
75
+ loader_class: 要注册的加载器类,必须继承 DataSourceLoader
76
+
77
+ 返回:
78
+ 注册后的加载器类(便于装饰器链式使用)
79
+
80
+ 异常:
81
+ ValueError: 如果该类型已注册则抛出
82
+ """
83
+ if spec_discriminator in LOADER_REGISTRY:
84
+ raise ValueError(f"加载器 '{spec_discriminator}' 已注册")
85
+ LOADER_REGISTRY[spec_discriminator] = loader_class
86
+ return loader_class
87
+
88
+ return decorator