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,719 @@
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
+ - 定义基础标量数据类型 (Integer, String, Float, Boolean 等)
21
+ - 实现数据类型验证和解析
22
+
23
+ 架构设计:
24
+ - 继承基类: 所有类型继承自 DataType 基类
25
+ - validate: 验证值是否符合类型要求
26
+ - parse: 将值转换为目标类型
27
+
28
+ 输入示例:
29
+ # 验证各种类型的值
30
+ values = [123, "hello", 3.14, True, "2024-01-01"]
31
+
32
+ # IntegerType 验证
33
+ IntegerType().validate(123) # (True, None)
34
+ IntegerType().validate("abc") # (False, "不是整数")
35
+
36
+ # StringType 验证
37
+ StringType().validate("hello") # (True, None)
38
+ StringType().validate(None) # (False, "值不能为空")
39
+
40
+ # FloatType 验证
41
+ FloatType().validate(3.14) # (True, None)
42
+ FloatType().validate("abc") # (False, "不是浮点数")
43
+
44
+ 输出示例:
45
+ # validate() 返回 (is_valid: bool, error_message: str | None)
46
+ (True, None) # 验证通过
47
+ (False, "错误信息") # 验证失败
48
+
49
+ # parse() 返回转换后的值
50
+ IntegerType().parse("123") # 123 (int)
51
+ StringType().parse(123) # "123" (str)
52
+ FloatType().parse("3.14") # 3.14 (float)
53
+ """
54
+
55
+ from __future__ import annotations
56
+
57
+ # 1. 标准库导入
58
+ import re
59
+ from datetime import date
60
+ from decimal import Decimal
61
+ from typing import Any
62
+
63
+ # 2. 第三方库导入
64
+ import numpy as np
65
+ import pandas as pd
66
+
67
+ # 3. 项目内部导入
68
+ from app.shared.domain.data_types_parts.base import DataType
69
+
70
+ # 日期错误建议探测:Y-M-D 三段式(分隔符 -/. 皆可)的取值范围正则
71
+ _YMD_RE = re.compile(r"(\d{4})[-/.](\d{1,2})[-/.](\d{1,2})")
72
+
73
+
74
+ def date_range_hint(value: Any) -> str | None:
75
+ """对 Y-M-D 形态但取值非法的日期给出定位建议(哪一段超范围)。
76
+
77
+ §1.21 宽松解析下斜线/点分等布局会被当作合法日期接受(不报错),
78
+ 真正报错的是"形似日期但月/日取值非法"的值——对这类值指出
79
+ 超范围字段比笼统的"不是有效日期"更可操作。非该形态返回 None。
80
+ """
81
+ import calendar
82
+ from datetime import date as _date
83
+
84
+ text = str(value).strip()
85
+ m = _YMD_RE.fullmatch(text)
86
+ if not m:
87
+ return None
88
+ year, month, day = int(m[1]), int(m[2]), int(m[3])
89
+ if not 1 <= month <= 12:
90
+ return f"月份取值 {month} 超出 1-12,请核对数据。"
91
+ if not 1 <= day <= 31:
92
+ return f"日取值 {day} 超出 1-31,请核对数据。"
93
+ try:
94
+ _date(year, month, day)
95
+ except ValueError:
96
+ last_day = calendar.monthrange(year, month)[1]
97
+ return f"{year} 年 {month} 月只有 {last_day} 天,请核对数据。"
98
+ return None
99
+
100
+
101
+ class IntegerType(DataType):
102
+ """
103
+ @classdesc 整数类型
104
+
105
+ 验证和解析整数值。
106
+ 支持正整数、负整数和零,格式要求为纯数字(可选前导负号)。
107
+
108
+ 使用场景:
109
+ - 用户 ID、订单号等整数字段
110
+ - 年龄、数量等数值字段
111
+ """
112
+
113
+ _int_pattern = re.compile(r"^-?\d+$")
114
+
115
+ def validate(self, value: Any) -> tuple[bool, Any]:
116
+ """
117
+ @methoddesc 验证值是否为整数
118
+
119
+ 使用正则表达式匹配纯数字格式,并尝试转换为 int 类型。
120
+
121
+ 参数:
122
+ value: 要验证的值
123
+
124
+ 返回:
125
+ 元组 (is_valid, error_message)
126
+ """
127
+ if value is None:
128
+ return False, "值不能为空。"
129
+ value_str = str(value)
130
+ if self._int_pattern.fullmatch(value_str):
131
+ try:
132
+ int(value_str)
133
+ return True, None
134
+ except ValueError:
135
+ return False, f"'{value_str}' 看起来像整数,但无法转换。"
136
+ return False, f"'{value_str}' 不是一个严格格式的整数(只允许数字和可选的负号)。"
137
+
138
+ def parse(self, value: Any) -> int:
139
+ """
140
+ @methoddesc 将值解析为整数
141
+
142
+ 参数:
143
+ value: 要解析的值
144
+
145
+ 返回:
146
+ 整数
147
+ """
148
+ return int(value)
149
+
150
+ def process_column(self, series: pd.Series, col_name: str, nullable: bool = True) -> tuple[pd.Series, list[dict]]:
151
+ """向量化整数列验证和解析。"""
152
+ errors: list[dict] = []
153
+ is_na = series.isna()
154
+
155
+ # nullable 检查
156
+ if not nullable:
157
+ blank_mask = series.fillna("").astype(str).str.strip() == ""
158
+ notnull_violations = is_na | blank_mask
159
+ for index in series.index[notnull_violations]:
160
+ errors.append(
161
+ {
162
+ "row_index": index,
163
+ "column": col_name,
164
+ "value": series[index],
165
+ "error_type": "NotNullViolation",
166
+ "error_message": f"列 '{col_name}' 不允许为空",
167
+ }
168
+ )
169
+ else:
170
+ notnull_violations = pd.Series(False, index=series.index)
171
+
172
+ # 对非空值进行类型验证
173
+ non_na = ~is_na & ~notnull_violations
174
+
175
+ # 回归 #2 修复:含空值的整数列被 pandas 推断为 float64,str(1.0)="1.0" 不匹配整数正则,
176
+ # 导致整列每个非空值误报 TypeValidationError。对"值是整数"的 float 在字符串化前回收为整数,
177
+ # 避免把 1.5 这样的真实小数误判为整数(仅当 float.is_integer() 为真时规整)。
178
+ def _to_int_str(v: Any) -> str:
179
+ if isinstance(v, float) and v.is_integer():
180
+ return str(int(v))
181
+ return str(v)
182
+
183
+ series_str = series.apply(_to_int_str)
184
+ valid_format = series_str.str.fullmatch(r"^-?\d+$")
185
+
186
+ # 标记类型验证失败的行
187
+ type_error_mask = non_na & ~valid_format
188
+ for index in series.index[type_error_mask]:
189
+ val = series[index]
190
+ errors.append(
191
+ {
192
+ "row_index": index,
193
+ "column": col_name,
194
+ "value": val,
195
+ "error_type": "TypeValidationError",
196
+ "error_message": f"'{val}' 不是一个严格格式的整数(只允许数字和可选的负号)。",
197
+ }
198
+ )
199
+
200
+ # 解析:用 pd.to_numeric 批量转换
201
+ parsed = pd.to_numeric(series_str, errors="coerce")
202
+
203
+ # 溢出检测必须在 to_numeric 升位 float64 之前基于原始字符串进行:
204
+ # 含空值的列 to_numeric 返回 float64,2^53+1(9007199254740993)会被舍入为
205
+ # 2^53.0(9007199254740992.0),之后 abs > 2**53 恒为 False,溢出被静默吞掉。
206
+ # 与 valid_format 相同口径:仅对严格整数格式的字符串做精度检查。
207
+ def _exceeds_safe_int_range(value_str: str) -> bool:
208
+ if self._int_pattern.fullmatch(value_str):
209
+ return abs(int(value_str)) > 2**53
210
+ return False
211
+
212
+ overflow_mask = non_na & series_str.apply(_exceeds_safe_int_range)
213
+ for index in series.index[overflow_mask]:
214
+ errors.append(
215
+ {
216
+ "row_index": index,
217
+ "column": col_name,
218
+ "value": series[index],
219
+ "error_type": "TypeValidationError",
220
+ "error_message": f"'{series[index]}' 超出安全整数范围(±2^53),可能丢失精度。",
221
+ }
222
+ )
223
+ # 将无效/空值位置设为 None
224
+ failed = is_na | notnull_violations | type_error_mask | overflow_mask
225
+ parsed = parsed.where(~failed, None)
226
+
227
+ return parsed, errors
228
+
229
+
230
+ class StringType(DataType):
231
+ """
232
+ @classdesc 字符串类型
233
+
234
+ 验证值非空,并将其解析为字符串。
235
+ 最基本的类型,几乎所有字段都可以使用。
236
+
237
+ 使用场景:
238
+ - 姓名、地址、描述等文本字段
239
+ - 不需要特定格式验证的通用字段
240
+ """
241
+
242
+ def validate(self, value: Any) -> tuple[bool, Any]:
243
+ """
244
+ @methoddesc 验证值是否非空
245
+
246
+ 参数:
247
+ value: 要验证的值
248
+
249
+ 返回:
250
+ 元组 (is_valid, error_message)
251
+ """
252
+ if value is None:
253
+ return False, "值不能为空。"
254
+ return True, None
255
+
256
+ def parse(self, value: Any) -> str:
257
+ """
258
+ @methoddesc 将值解析为字符串
259
+
260
+ 参数:
261
+ value: 要解析的值
262
+
263
+ 返回:
264
+ 字符串
265
+ """
266
+ return str(value)
267
+
268
+ def process_column(self, series: pd.Series, col_name: str, nullable: bool = True) -> tuple[pd.Series, list[dict]]:
269
+ """向量化字符串列验证和解析。"""
270
+ errors: list[dict] = []
271
+ is_na = series.isna()
272
+
273
+ # nullable 检查
274
+ if not nullable:
275
+ blank_mask = series.fillna("").astype(str).str.strip() == ""
276
+ notnull_violations = is_na | blank_mask
277
+ for index in series.index[notnull_violations]:
278
+ errors.append(
279
+ {
280
+ "row_index": index,
281
+ "column": col_name,
282
+ "value": series[index],
283
+ "error_type": "NotNullViolation",
284
+ "error_message": f"列 '{col_name}' 不允许为空",
285
+ }
286
+ )
287
+ else:
288
+ notnull_violations = pd.Series(False, index=series.index)
289
+
290
+ # StringType 几乎不产生类型验证错误,直接转为字符串
291
+ # 保留空值位置为 None
292
+ failed = is_na | notnull_violations
293
+ parsed = series.astype(str).where(~failed, None)
294
+
295
+ return parsed, errors
296
+
297
+
298
+ class FloatType(DataType):
299
+ """
300
+ @classdesc 浮点数类型
301
+
302
+ 验证和解析浮点数值。
303
+ 支持整数形式和小数形式的数值。
304
+
305
+ 使用场景:
306
+ - 价格、比率、测量值等需要小数精度的字段
307
+ """
308
+
309
+ def validate(self, value: Any) -> tuple[bool, Any]:
310
+ """
311
+ @methoddesc 验证值是否为有效的浮点数
312
+
313
+ 尝试将值转换为 float 类型来验证。回归 D10: 拒绝 inf/-inf/nan 等非有限值
314
+ (float("inf") 在 Python 合法但会污染下游统计),与 DecimalType 的 is_finite 检查对齐。
315
+
316
+ 参数:
317
+ value: 要验证的值
318
+
319
+ 返回:
320
+ 元组 (is_valid, error_message)
321
+ """
322
+ try:
323
+ parsed = float(value)
324
+ except (ValueError, TypeError):
325
+ return False, f"'{value}' 不是一个有效的浮点数。"
326
+ # 拒绝非有限值(inf/-inf/nan),避免污染下游统计与约束
327
+ import math
328
+
329
+ if math.isnan(parsed) or math.isinf(parsed):
330
+ return False, f"'{value}' 不是有限的浮点数(不接受 inf/nan)。"
331
+ return True, None
332
+
333
+ def parse(self, value: Any) -> float:
334
+ """
335
+ @methoddesc 将值解析为浮点数
336
+
337
+ 参数:
338
+ value: 要解析的值
339
+
340
+ 返回:
341
+ 浮点数
342
+
343
+ 异常:
344
+ ValueError: 值无法解析为有限浮点数时抛出(回归 D10: 拒绝 inf/nan)
345
+ """
346
+ parsed = float(value)
347
+ import math
348
+
349
+ if math.isnan(parsed) or math.isinf(parsed):
350
+ raise ValueError(f"'{value}' 不是有限的浮点数(不接受 inf/nan)。")
351
+ return parsed
352
+
353
+ def process_column(self, series: pd.Series, col_name: str, nullable: bool = True) -> tuple[pd.Series, list[dict]]:
354
+ """向量化浮点数列验证和解析。"""
355
+ errors: list[dict] = []
356
+ is_na = series.isna()
357
+
358
+ # nullable 检查
359
+ if not nullable:
360
+ blank_mask = series.fillna("").astype(str).str.strip() == ""
361
+ notnull_violations = is_na | blank_mask
362
+ for index in series.index[notnull_violations]:
363
+ errors.append(
364
+ {
365
+ "row_index": index,
366
+ "column": col_name,
367
+ "value": series[index],
368
+ "error_type": "NotNullViolation",
369
+ "error_message": f"列 '{col_name}' 不允许为空",
370
+ }
371
+ )
372
+ else:
373
+ notnull_violations = pd.Series(False, index=series.index)
374
+
375
+ # 用 pd.to_numeric 批量转换,coerce 将无效值转为 NaN
376
+ parsed = pd.to_numeric(series, errors="coerce")
377
+
378
+ # 标记类型验证失败的行(非空但转换后为 NaN)
379
+ non_na = ~is_na & ~notnull_violations
380
+ type_error_mask = non_na & parsed.isna()
381
+ # 回归 D10 补漏: 单值 validate() 已拒绝 inf/nan,但向量化路径 pd.to_numeric 会把
382
+ # "inf"/"Infinity" 解析为 inf(非 NaN)绕过掩码,inf 流入下游污染 Range 与统计。
383
+ # 非有限值(±inf)同样按类型错误处理;where(~failed, None) 统一置 None。
384
+ type_error_mask = type_error_mask | (non_na & parsed.notna() & ~np.isfinite(parsed))
385
+ for index in series.index[type_error_mask]:
386
+ val = series[index]
387
+ errors.append(
388
+ {
389
+ "row_index": index,
390
+ "column": col_name,
391
+ "value": val,
392
+ "error_type": "TypeValidationError",
393
+ "error_message": f"'{val}' 不是一个有效的浮点数。",
394
+ }
395
+ )
396
+
397
+ # 将无效/空值位置设为 None
398
+ failed = is_na | notnull_violations | type_error_mask
399
+ parsed = parsed.where(~failed, None)
400
+
401
+ return parsed, errors
402
+
403
+
404
+ class DecimalType(DataType):
405
+ """
406
+ @classdesc 高精度小数类型
407
+
408
+ 基于 Python Decimal 实现的高精度数值类型。
409
+ 支持精度(总有效数字位数)和小数位数限制。
410
+
411
+ 使用场景:
412
+ - 货币金额计算(避免浮点数精度问题)
413
+ - 需要精确小数表示的业务场景
414
+ """
415
+
416
+ def __init__(self, precision: int = 28, scale: int | None = None):
417
+ """
418
+ @methoddesc 初始化高精度小数类型
419
+
420
+ 参数:
421
+ precision: 总精度(最大有效数字位数),默认 28
422
+ scale: 小数位数限制,None 表示不限制
423
+ """
424
+ self.precision = precision
425
+ self.scale = scale
426
+
427
+ def validate(self, value: Any) -> tuple[bool, Any]:
428
+ """
429
+ @methoddesc 验证值是否为有效的高精度小数
430
+
431
+ 检查值能否转换为 Decimal,以及是否满足精度和位数限制。
432
+
433
+ 参数:
434
+ value: 要验证的值
435
+
436
+ 返回:
437
+ 元组 (is_valid, error_message)
438
+ """
439
+ from decimal import Decimal, InvalidOperation
440
+
441
+ try:
442
+ decimal_value = Decimal(str(value))
443
+ if not decimal_value.is_finite():
444
+ return False, f"'{value}' 不是有限的数值(NaN 或 Infinity 不被接受)"
445
+ if self.precision:
446
+ # §1.20: 按数值语义计数精度——去尾随零与指数形态后数有效位数,
447
+ # 同值 "150"/"1.5E+2"/"150.00" 判定一致(原实现按存储表示计数,同值不同精度)。
448
+ # 2026-09-20 修复:不得经过 Decimal.normalize()——它受 decimal 上下文
449
+ # prec=28 影响,>28 位有效数字会被就地舍入回 28 位再计数,precision=28 时
450
+ # 29/30/33 位超限值假阴性放行(实测修复前三者全部通过)。改为直接对
451
+ # as_tuple 数字串剥尾随零,与 normalize 的有效位语义等价且无上下文舍入。
452
+ _sign, digits, _exponent = decimal_value.as_tuple()
453
+ significant = "".join(map(str, digits)).rstrip("0")
454
+ total_digits = len(significant) if significant else 1 # 全零值有效位记 1
455
+ if total_digits > self.precision:
456
+ return False, f"'{value}' 超出精度限制(最大 {self.precision} 位)"
457
+ if self.scale is not None:
458
+ exponent = decimal_value.as_tuple().exponent
459
+ if isinstance(exponent, int):
460
+ decimal_places = -exponent
461
+ if decimal_places > self.scale:
462
+ return False, f"'{value}' 小数位数超出限制(最大 {self.scale} 位)"
463
+ return True, None
464
+ except (InvalidOperation, ValueError, TypeError):
465
+ return False, f"'{value}' 不是一个有效的数值。"
466
+
467
+ def parse(self, value: Any) -> Decimal:
468
+ """
469
+ @methoddesc 将值解析为 Decimal
470
+
471
+ 参数:
472
+ value: 要解析的值
473
+
474
+ 返回:
475
+ Decimal 对象
476
+ """
477
+ from decimal import Decimal
478
+
479
+ return Decimal(str(value))
480
+
481
+
482
+ class BooleanType(DataType):
483
+ """
484
+ @classdesc 布尔类型
485
+
486
+ 验证和解析布尔值。
487
+ 支持多种布尔值表示形式,如 true/false、yes/no、1/0、是/否等。
488
+
489
+ 使用场景:
490
+ - 开关、状态、标记等只有两种状态的字段
491
+ """
492
+
493
+ TRUE_VALUES = {"true", "yes", "on", "1", "是", "t", "y"}
494
+ FALSE_VALUES = {"false", "no", "off", "0", "否", "f", "n"}
495
+
496
+ def validate(self, value: Any) -> tuple[bool, Any]:
497
+ """
498
+ @methoddesc 验证值是否为有效的布尔值
499
+
500
+ 支持布尔类型、字符串形式(大小写不敏感)等多种输入。
501
+
502
+ 参数:
503
+ value: 要验证的值
504
+
505
+ 返回:
506
+ 元组 (is_valid, error_message)
507
+ """
508
+ if value is None:
509
+ return False, "值不能为空。"
510
+
511
+ if isinstance(value, bool):
512
+ return True, None
513
+
514
+ str_val = str(value).strip().lower()
515
+ if str_val in self.TRUE_VALUES or str_val in self.FALSE_VALUES:
516
+ return True, None
517
+
518
+ return False, f"'{value}' 不是一个有效的布尔值。"
519
+
520
+ def parse(self, value: Any) -> bool:
521
+ """
522
+ @methoddesc 将值解析为布尔值
523
+
524
+ 如果值在 TRUE_VALUES 集合中,返回 True;否则返回 False。
525
+
526
+ 参数:
527
+ value: 要解析的值
528
+
529
+ 返回:
530
+ 布尔值
531
+ """
532
+ if isinstance(value, bool):
533
+ return value
534
+
535
+ str_val = str(value).strip().lower()
536
+ return str_val in self.TRUE_VALUES
537
+
538
+ def process_column(self, series: pd.Series, col_name: str, nullable: bool = True) -> tuple[pd.Series, list[dict]]:
539
+ """向量化布尔列验证和解析。"""
540
+ errors: list[dict] = []
541
+ is_na = series.isna()
542
+
543
+ # nullable 检查
544
+ if not nullable:
545
+ blank_mask = series.fillna("").astype(str).str.strip() == ""
546
+ notnull_violations = is_na | blank_mask
547
+ for index in series.index[notnull_violations]:
548
+ errors.append(
549
+ {
550
+ "row_index": index,
551
+ "column": col_name,
552
+ "value": series[index],
553
+ "error_type": "NotNullViolation",
554
+ "error_message": f"列 '{col_name}' 不允许为空",
555
+ }
556
+ )
557
+ else:
558
+ notnull_violations = pd.Series(False, index=series.index)
559
+
560
+ # 对非空值进行类型验证
561
+ non_na = ~is_na & ~notnull_violations
562
+ is_python_bool = series.apply(lambda v: isinstance(v, bool))
563
+
564
+ # 回归 #3 修复:0/1 布尔列带空值被 pandas 推断为 float64,"1.0"/"0.0" 不在合法布尔集合 →
565
+ # 整列误报。对 is_integer() 为真的 float 在字符串化前回收为整数,使 1.0→"1"、0.0→"0" 命中合法集合。
566
+ def _to_int_str(v: Any) -> str:
567
+ if isinstance(v, float) and v.is_integer():
568
+ return str(int(v))
569
+ return str(v)
570
+
571
+ series_str = series.apply(_to_int_str).str.strip().str.lower()
572
+ is_true = series_str.isin(self.TRUE_VALUES)
573
+ is_false = series_str.isin(self.FALSE_VALUES)
574
+ valid_mask = is_python_bool | is_true | is_false
575
+
576
+ # 标记类型验证失败的行
577
+ type_error_mask = non_na & ~valid_mask
578
+ for index in series.index[type_error_mask]:
579
+ val = series[index]
580
+ errors.append(
581
+ {
582
+ "row_index": index,
583
+ "column": col_name,
584
+ "value": val,
585
+ "error_type": "TypeValidationError",
586
+ "error_message": f"'{val}' 不是一个有效的布尔值。",
587
+ }
588
+ )
589
+
590
+ # 解析:逐行判定(布尔类型需要处理 Python bool + 字符串混合输入)
591
+ result_values: list[Any] = []
592
+ for i, val in series.items():
593
+ if is_na[i] or notnull_violations[i] or type_error_mask[i]:
594
+ result_values.append(None)
595
+ elif isinstance(val, bool):
596
+ result_values.append(val)
597
+ else:
598
+ # 回归 #3: float 1.0/0.0 需回收为整数再判定(与上面的字符串化规整保持一致)
599
+ check_val = str(int(val)) if (isinstance(val, float) and val.is_integer()) else str(val)
600
+ result_values.append(check_val.strip().lower() in self.TRUE_VALUES)
601
+ parsed = pd.Series(result_values, index=series.index)
602
+
603
+ return parsed, errors
604
+
605
+
606
+ class DateType(DataType):
607
+ """
608
+ @classdesc 日期类型
609
+
610
+ 验证和解析日期值。
611
+ 格式要求: YYYY-MM-DD
612
+
613
+ 使用场景:
614
+ - 出生日期、订单日期、有效期等日期字段
615
+ """
616
+
617
+ def validate(self, value: Any) -> tuple[bool, Any]:
618
+ """
619
+ @methoddesc 验证值是否为有效的日期格式
620
+
621
+ 使用 strptime 按照 YYYY-MM-DD 格式解析来验证。
622
+
623
+ 参数:
624
+ value: 要验证的值
625
+
626
+ 返回:
627
+ 元组 (is_valid, error_message)
628
+ """
629
+ if value is None:
630
+ return False, "值不能为空。"
631
+ from datetime import datetime
632
+
633
+ try:
634
+ datetime.strptime(str(value), "%Y-%m-%d")
635
+ return True, None
636
+ except ValueError:
637
+ pass
638
+ # §1.21: 严格纯日期失败后兜底通用解析(带时间的 datetime 常见于 Excel 导入),
639
+ # 按日期部分视为合法 date——原实现整列误报 TypeValidationError
640
+ try:
641
+ pd.to_datetime(str(value), errors="raise")
642
+ return True, None
643
+ except (ValueError, TypeError, OverflowError):
644
+ return False, f"'{value}' 不是有效的日期格式 (YYYY-MM-DD)。"
645
+
646
+ def parse(self, value: Any) -> date:
647
+ """
648
+ @methoddesc 将值解析为日期对象
649
+
650
+ 按照 YYYY-MM-DD 格式解析字符串并返回 date 对象。
651
+
652
+ 参数:
653
+ value: 要解析的值
654
+
655
+ 返回:
656
+ datetime.date 对象
657
+ """
658
+ from datetime import datetime
659
+
660
+ return datetime.strptime(str(value), "%Y-%m-%d").date()
661
+
662
+ def process_column(self, series: pd.Series, col_name: str, nullable: bool = True) -> tuple[pd.Series, list[dict]]:
663
+ """向量化日期列验证和解析。"""
664
+ errors: list[dict] = []
665
+ is_na = series.isna()
666
+
667
+ # nullable 检查
668
+ if not nullable:
669
+ blank_mask = series.fillna("").astype(str).str.strip() == ""
670
+ notnull_violations = is_na | blank_mask
671
+ for index in series.index[notnull_violations]:
672
+ errors.append(
673
+ {
674
+ "row_index": index,
675
+ "column": col_name,
676
+ "value": series[index],
677
+ "error_type": "NotNullViolation",
678
+ "error_message": f"列 '{col_name}' 不允许为空",
679
+ }
680
+ )
681
+ else:
682
+ notnull_violations = pd.Series(False, index=series.index)
683
+
684
+ # 用 pd.to_datetime 批量解析(§1.21 两段式:严格纯日期优先,
685
+ # 失败的行再逐元素通用解析取日期部分——Excel 常见的带时间 datetime 不再整列误报。
686
+ # 不能整列 lenient 重解析:混合格式列会被 pandas 按首行格式统一推断,带时间的行仍 NaT)
687
+ non_na = ~is_na & ~notnull_violations
688
+ parsed_dt = pd.to_datetime(series.astype(str), format="%Y-%m-%d", errors="coerce")
689
+ need_lenient = parsed_dt.isna() & non_na
690
+ for idx in series.index[need_lenient]:
691
+ parsed_dt[idx] = pd.to_datetime(series[idx], errors="coerce")
692
+
693
+ # 标记类型验证失败的行
694
+ type_error_mask = non_na & parsed_dt.isna()
695
+ for index in series.index[type_error_mask]:
696
+ val = series[index]
697
+ type_error: dict[str, Any] = {
698
+ "row_index": index,
699
+ "column": col_name,
700
+ "value": val,
701
+ "error_type": "TypeValidationError",
702
+ "error_message": f"'{val}' 不是有效的日期格式 (YYYY-MM-DD)。",
703
+ }
704
+ # suggestion(可选):形似 Y-M-D 但取值非法时指出超范围字段
705
+ range_hint = date_range_hint(val)
706
+ if range_hint:
707
+ type_error["suggestion"] = range_hint
708
+ errors.append(type_error)
709
+
710
+ # 转换为 date 对象,保留原索引
711
+ result_values: list[Any] = []
712
+ for i in series.index:
713
+ if is_na[i] or notnull_violations[i] or type_error_mask[i]:
714
+ result_values.append(None)
715
+ else:
716
+ result_values.append(parsed_dt[i].date())
717
+ parsed = pd.Series(result_values, index=series.index)
718
+
719
+ return parsed, errors