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,374 @@
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 工作区配置 API 路由模块
18
+
19
+ 功能概述:
20
+ - 提供工作区配置的读写接口(数据源列表、UI 偏好设置)
21
+ - 管理数据源的增删改查操作
22
+ - 支持 Electron 和 CLI/Web 两种使用模式
23
+
24
+ 架构设计:
25
+ - 配置优先从新路径 .precis/data_sources.yaml 读取,兼容旧路径
26
+ - 数据源使用 ExternalDataSource 模型 enriched 后返回
27
+ - 通过环境变量 PRECIS_PROJECT_ROOT 确定项目根目录
28
+
29
+ 输入示例:
30
+ POST /workspace/data-sources
31
+ {"fileId": "data/users.xlsx", "name": "用户表", "type": "excel"}
32
+
33
+ 输出示例:
34
+ {
35
+ "version": "1.0.0",
36
+ "data_sources": [{"id": "...", "name": "用户表", "type": "excel", "status": "ready"}],
37
+ "ui_preferences": {"expanded_folders": {}, "startup_loading_enabled": true}
38
+ }
39
+ """
40
+
41
+ import os
42
+ from datetime import datetime
43
+ from pathlib import Path
44
+ from typing import Any
45
+
46
+ import yaml
47
+ from fastapi import APIRouter, Depends, HTTPException, status
48
+
49
+ from app.api.dependencies import get_project_config_path
50
+ from app.api.models import ExternalDataSource, UIPreferences, WorkspaceConfig
51
+ from app.api.services.io_error_messages import describe_io_error
52
+ from app.shared.core.config import ConfigPaths
53
+ from app.shared.core.io.yaml import write_yaml_atomic
54
+
55
+ router = APIRouter(prefix="/api/latest/workspace", tags=["Data Sources"])
56
+
57
+
58
+ def get_workspace_config_path(project_root: str) -> Path:
59
+ """获取工作区配置文件路径(优先新路径,兼容旧路径)"""
60
+ # 优先使用新路径
61
+ new_path = ConfigPaths.data_sources(project_root)
62
+ if new_path.exists():
63
+ return new_path
64
+
65
+ # 向后兼容:检查旧路径
66
+ old_path = Path(project_root) / ".precis-workspace.yaml"
67
+ if old_path.exists():
68
+ return old_path
69
+
70
+ # 默认返回新路径(用于创建)
71
+ return new_path
72
+
73
+
74
+ def load_workspace_config(project_root: str) -> dict[str, Any]:
75
+ """加载工作区配置"""
76
+ config_path = get_workspace_config_path(project_root)
77
+
78
+ if not config_path.exists():
79
+ # 返回默认配置
80
+ return {
81
+ "data_sources": [],
82
+ "alias_mappings": {},
83
+ "ui_preferences": {},
84
+ "version": "1.0",
85
+ "last_updated": datetime.now().isoformat(),
86
+ }
87
+
88
+ try:
89
+ with open(config_path, encoding="utf-8") as f:
90
+ return yaml.safe_load(f) or {}
91
+ except Exception as e:
92
+ raise HTTPException(
93
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"加载数据源配置失败:{describe_io_error(e)}"
94
+ )
95
+
96
+
97
+ def save_workspace_config(project_root: str, config: dict[str, Any]) -> None:
98
+ """保存工作区配置到文件"""
99
+ config_path = get_workspace_config_path(project_root)
100
+
101
+ try:
102
+ write_yaml_atomic(config_path, config)
103
+ except Exception as e:
104
+ raise HTTPException(
105
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"保存数据源配置失败:{describe_io_error(e)}"
106
+ )
107
+
108
+
109
+ def build_ui_preferences(config: dict[str, Any]) -> UIPreferences:
110
+ """构建 UI 偏好设置对象"""
111
+ raw = config.get("ui_preferences") or {}
112
+ expanded_folders: dict[str, bool] = {}
113
+ startup_loading_enabled = True
114
+
115
+ if isinstance(raw, dict):
116
+ startup_loading_enabled = raw.get("startup_loading_enabled", True)
117
+ expanded = raw.get("expanded_folders")
118
+ if isinstance(expanded, dict):
119
+ expanded_folders = {k: bool(v) for k, v in expanded.items()}
120
+ else:
121
+ expanded_folders = {k: bool(v) for k, v in raw.items() if isinstance(v, bool)}
122
+
123
+ return UIPreferences(expanded_folders=expanded_folders, startup_loading_enabled=bool(startup_loading_enabled))
124
+
125
+
126
+ def _get_project_root(config_path: str) -> str:
127
+ """返回项目根目录。
128
+
129
+ B-sec3 加固后 X-Project-Config-Path Header 必须指向含 project.precis.yaml 的
130
+ 项目根目录(见 dependencies._validate_project_root),因此 config_path 本身就是
131
+ 项目根。此前此处误取 config_path 的 parent,导致 data_sources.yaml 被写到项目
132
+ 父目录的 .precis/ 下——同一父目录下的多个项目会互相覆盖数据源配置。
133
+
134
+ 参数:
135
+ config_path: 项目根目录的绝对路径(依赖注入校验后的值)
136
+
137
+ 返回:
138
+ 项目根目录(与 config_path 相同)
139
+ """
140
+ return config_path
141
+
142
+
143
+ @router.get(
144
+ "/config",
145
+ response_model=WorkspaceConfig,
146
+ summary="获取工作区配置",
147
+ responses={
148
+ 500: {"description": "服务器内部错误"},
149
+ },
150
+ )
151
+ async def get_workspace_config(config_path: str = Depends(get_project_config_path)) -> WorkspaceConfig:
152
+ """
153
+ 获取工作区配置
154
+
155
+ 适用于所有模式(Electron/CLI/Web)
156
+ """
157
+ project_root = _get_project_root(config_path)
158
+ config = load_workspace_config(project_root)
159
+
160
+ enriched_data_sources = []
161
+ for ds in config.get("data_sources", []):
162
+ enriched_ds = ExternalDataSource(
163
+ id=ds.get("id", ds.get("fileId", ds.get("fullPath", ""))),
164
+ name=ds.get("name", ""),
165
+ fileId=ds.get("fileId", ds.get("fullPath", "")),
166
+ type=ds.get("type", "excel"),
167
+ status=ds.get("status", "ready"),
168
+ addedAt=ds.get("addedAt", "2024-01-01T00:00:00Z"),
169
+ lastUsed=ds.get("lastUsed", "2024-01-01T00:00:00Z"),
170
+ alias=ds.get("alias", ""),
171
+ error=ds.get("error", ""),
172
+ size=ds.get("size"),
173
+ sourceMode=ds.get("sourceMode"),
174
+ localPath=ds.get("localPath"),
175
+ folderPath=ds.get("folderPath"),
176
+ )
177
+ enriched_data_sources.append(enriched_ds)
178
+
179
+ return WorkspaceConfig(
180
+ version=config.get("version", "1.0.0"),
181
+ data_sources=enriched_data_sources,
182
+ alias_mappings=config.get("alias_mappings", {}),
183
+ ui_preferences=build_ui_preferences(config),
184
+ last_updated=config.get("last_updated", "2024-01-01T00:00:00Z"),
185
+ )
186
+
187
+
188
+ @router.put(
189
+ "/config",
190
+ response_model=WorkspaceConfig,
191
+ summary="更新工作区配置",
192
+ responses={
193
+ 500: {"description": "服务器内部错误"},
194
+ },
195
+ )
196
+ async def update_workspace_config(
197
+ config_data: dict[str, Any], config_path: str = Depends(get_project_config_path)
198
+ ) -> WorkspaceConfig:
199
+ """
200
+ 更新工作区配置
201
+
202
+ 适用于所有模式(Electron/CLI/Web)
203
+ """
204
+ project_root = _get_project_root(config_path)
205
+
206
+ # 更新最后修改时间
207
+ config_data["last_updated"] = datetime.now().isoformat()
208
+
209
+ # 保存到文件
210
+ save_workspace_config(project_root, config_data)
211
+
212
+ # 返回更新后的配置
213
+ return await get_workspace_config(config_path)
214
+
215
+
216
+ @router.post(
217
+ "/data-sources",
218
+ response_model=WorkspaceConfig,
219
+ summary="添加数据源",
220
+ responses={
221
+ 500: {"description": "服务器内部错误"},
222
+ },
223
+ )
224
+ async def add_data_source(
225
+ data_source: dict[str, Any], config_path: str = Depends(get_project_config_path)
226
+ ) -> WorkspaceConfig:
227
+ """
228
+ 添加数据源
229
+
230
+ 适用于所有模式(Electron/CLI/Web)
231
+ """
232
+ project_root = _get_project_root(config_path)
233
+ config = load_workspace_config(project_root)
234
+
235
+ file_id = data_source.get("fileId", data_source.get("fullPath"))
236
+ # 仅在路径为绝对路径时执行 normpath;在 Windows 上 normpath("/a") 会变成 "\a"
237
+ # (去掉前导斜杠、视作相对路径),导致不同 fileId 在比较时塌缩为同一字符串。
238
+ if file_id and os.path.isabs(file_id):
239
+ file_id = os.path.normpath(file_id)
240
+
241
+ # 检查是否已存在
242
+ # 注意:fullPath 比较必须排除 None == None 的情况——否则当 payload 与已有项都缺
243
+ # 少 fullPath 字段时,条件会误判为重复,导致不同 fileId 的新数据源被错误地覆盖。
244
+ new_full_path = data_source.get("fullPath")
245
+ for ds in config.get("data_sources", []):
246
+ if ds.get("fileId") == file_id or (new_full_path and ds.get("fullPath") == new_full_path):
247
+ # 更新现有数据源
248
+ ds["name"] = data_source.get("name", ds.get("name", ""))
249
+ ds["type"] = data_source.get("type", ds.get("type", "excel"))
250
+ ds["status"] = data_source.get("status", "ready")
251
+ ds["lastUsed"] = data_source.get("lastUsed", datetime.now().isoformat())
252
+ if "sourceMode" in data_source:
253
+ ds["sourceMode"] = data_source.get("sourceMode")
254
+ if "localPath" in data_source:
255
+ ds["localPath"] = data_source.get("localPath")
256
+ if "folderPath" in data_source:
257
+ ds["folderPath"] = data_source.get("folderPath")
258
+ if "size" in data_source:
259
+ ds["size"] = data_source.get("size")
260
+ if "id" not in ds:
261
+ ds["id"] = data_source.get("id", file_id)
262
+
263
+ save_workspace_config(project_root, config)
264
+ return await get_workspace_config(config_path)
265
+
266
+ # 添加新数据源
267
+ new_source = {
268
+ "id": data_source.get("id", file_id),
269
+ "name": data_source.get("name", ""),
270
+ "fileId": file_id,
271
+ "type": data_source.get("type", "excel"),
272
+ "status": data_source.get("status", "ready"),
273
+ "addedAt": data_source.get("addedAt", datetime.now().isoformat()),
274
+ "lastUsed": data_source.get("lastUsed", datetime.now().isoformat()),
275
+ "alias": data_source.get("alias", ""),
276
+ "error": data_source.get("error", ""),
277
+ "sourceMode": data_source.get("sourceMode"),
278
+ "localPath": data_source.get("localPath"),
279
+ "folderPath": data_source.get("folderPath"),
280
+ "size": data_source.get("size"),
281
+ }
282
+
283
+ config.setdefault("data_sources", []).insert(0, new_source)
284
+ save_workspace_config(project_root, config)
285
+
286
+ return await get_workspace_config(config_path)
287
+
288
+
289
+ @router.delete(
290
+ "/data-sources/{source_id}",
291
+ response_model=WorkspaceConfig,
292
+ summary="移除数据源",
293
+ responses={
294
+ 500: {"description": "服务器内部错误"},
295
+ },
296
+ )
297
+ async def remove_data_source(source_id: str, config_path: str = Depends(get_project_config_path)) -> WorkspaceConfig:
298
+ """
299
+ 移除数据源
300
+
301
+ 适用于所有模式(Electron/CLI/Web)
302
+ """
303
+ project_root = _get_project_root(config_path)
304
+ config = load_workspace_config(project_root)
305
+
306
+ data_sources = config.get("data_sources", [])
307
+ config["data_sources"] = [ds for ds in data_sources if ds.get("id") != source_id]
308
+
309
+ save_workspace_config(project_root, config)
310
+ return await get_workspace_config(config_path)
311
+
312
+
313
+ @router.put(
314
+ "/data-sources/{source_id}",
315
+ response_model=WorkspaceConfig,
316
+ summary="更新数据源",
317
+ responses={
318
+ 500: {"description": "服务器内部错误"},
319
+ },
320
+ )
321
+ async def update_data_source(
322
+ source_id: str, data_source: dict[str, Any], config_path: str = Depends(get_project_config_path)
323
+ ) -> WorkspaceConfig:
324
+ """
325
+ @methoddesc 更新数据源
326
+
327
+ 业务用途:
328
+ - 在工作区配置中按 id 查找数据源并就地合并更新
329
+ - 同步刷新 lastUsed 字段以反映最近访问时间
330
+ - 适用于所有运行模式(Electron/CLI/Web)
331
+
332
+ 参数:
333
+ source_id: 数据源唯一标识
334
+ data_source: 包含待更新字段的字典
335
+ config_path: 项目配置路径(由依赖注入提供)
336
+
337
+ 返回:
338
+ 更新后的 WorkspaceConfig 完整配置
339
+ """
340
+ project_root = _get_project_root(config_path)
341
+ config = load_workspace_config(project_root)
342
+
343
+ for ds in config.get("data_sources", []):
344
+ if ds.get("id") == source_id:
345
+ ds.update(data_source)
346
+ ds["lastUsed"] = datetime.now().isoformat()
347
+ break
348
+
349
+ save_workspace_config(project_root, config)
350
+ return await get_workspace_config(config_path)
351
+
352
+
353
+ @router.delete(
354
+ "/data-sources",
355
+ response_model=WorkspaceConfig,
356
+ summary="清空所有数据源",
357
+ responses={
358
+ 500: {"description": "服务器内部错误"},
359
+ },
360
+ )
361
+ async def clear_all_data_sources(config_path: str = Depends(get_project_config_path)) -> WorkspaceConfig:
362
+ """
363
+ 清空所有数据源
364
+
365
+ 适用于所有模式(Electron/CLI/Web)
366
+ """
367
+ project_root = _get_project_root(config_path)
368
+ config = load_workspace_config(project_root)
369
+
370
+ config["data_sources"] = []
371
+ config["last_updated"] = datetime.now().isoformat()
372
+
373
+ save_workspace_config(project_root, config)
374
+ return await get_workspace_config(config_path)
@@ -0,0 +1,333 @@
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 正则表达式 API 路由模块
18
+
19
+ 功能概述:
20
+ - 将可视化模式片段解析为可执行的正则表达式
21
+ - 执行正则表达式测试并返回匹配结果和命名组
22
+ - 批量验证数据并提取命名组结果(用于前端正则节点预览)
23
+
24
+ 架构设计:
25
+ - 模式解析:支持 static 和 param 两种片段类型,映射为对应正则
26
+ - 正则验证:使用 Python re 模块编译和执行匹配
27
+ - 提取模式:支持 full/partial/extract 三种匹配模式
28
+
29
+ 输入示例:
30
+ POST /utils/parse-pattern
31
+ {"parts": [{"type": "static", "text": "ID_"}, {"type": "param", "name": "id", "param_type": "int"}]}
32
+
33
+ 输出示例:
34
+ {"regex": "ID_(?P<id>\\d+)", "params": {"id": "int"}}
35
+ """
36
+
37
+ import re
38
+ from typing import Any, Literal
39
+
40
+ from fastapi import APIRouter
41
+ from pydantic import BaseModel, Field
42
+
43
+ router = APIRouter(prefix="/api/latest/utils", tags=["Utilities"])
44
+
45
+
46
+ class PatternPart(BaseModel):
47
+ """正则模式片段模型
48
+
49
+ 用于描述正则表达式的组成部分,每个片段可以是固定文本(static)
50
+ 或参数占位符(param)。
51
+
52
+ 字段说明:
53
+ type: 片段类型,"static" 表示固定文本,"param" 表示参数占位符
54
+ text: 固定文本内容(仅 static 类型使用)
55
+ name: 参数名称(仅 param 类型使用,用于生成命名捕获组)
56
+ param_type: 参数数据类型(仅 param 类型使用,决定正则片段)
57
+ """
58
+
59
+ type: Literal["static", "param"]
60
+ text: str | None = None
61
+ name: str | None = None
62
+ param_type: str | None = None
63
+
64
+
65
+ class ParsePatternRequest(BaseModel):
66
+ """解析模式请求模型
67
+
68
+ 接收前端传来的模式片段列表,将其组合成完整的正则表达式。
69
+
70
+ 字段说明:
71
+ parts: 模式片段列表,按顺序组合
72
+ """
73
+
74
+ parts: list[PatternPart]
75
+
76
+
77
+ class ParsePatternResponse(BaseModel):
78
+ """解析模式响应模型
79
+
80
+ 返回组合后的正则表达式和提取的参数信息。
81
+
82
+ 字段说明:
83
+ regex: 组合后的完整正则表达式字符串
84
+ params: 参数名称到参数类型的映射字典
85
+ """
86
+
87
+ regex: str
88
+ params: dict[str, str]
89
+
90
+
91
+ # 参数类型到正则片段的映射表
92
+ # 根据参数类型选择对应的正则表达式片段
93
+ PARAM_TYPE_REGEX_MAP = {
94
+ "int": r"\d+", # 整数:一个或多个数字
95
+ "float": r"-?\d+(?:\.\d+)?", # 浮点数:可选负号 + 数字 + 可选的小数部分
96
+ "str": r"\w+", # 字符串:一个或多个单词字符
97
+ "word": r"\w+", # 单词:与 str 相同
98
+ "non_space": r"\S+", # 非空白字符:一个或多个非空白字符
99
+ "anything": r".+", # 任意字符:一个或多个任意字符
100
+ }
101
+
102
+
103
+ @router.post(
104
+ "/parse-pattern",
105
+ response_model=ParsePatternResponse,
106
+ summary="将可视化模式片段解析为正则表达式",
107
+ responses={
108
+ 500: {"description": "服务器内部错误"},
109
+ },
110
+ )
111
+ def parse_pattern_to_regex(request: ParsePatternRequest) -> dict[str, Any]:
112
+ """将可视化模式片段解析为正则表达式
113
+
114
+ 遍历请求中的每个片段:
115
+ - static 类型:转义文本中的特殊正则字符后直接拼接
116
+ - param 类型:根据 param_type 查找对应的正则片段,包装为命名捕获组 (?P<name>...)
117
+
118
+ Args:
119
+ request: 包含模式片段列表的请求对象
120
+
121
+ Returns:
122
+ 包含组合后的正则表达式和参数信息的响应对象
123
+ """
124
+ regex_parts = []
125
+ params = {}
126
+ for part in request.parts:
127
+ if part.type == "static" and part.text:
128
+ # 对固定文本进行正则转义,防止特殊字符干扰正则语义
129
+ regex_parts.append(re.escape(part.text))
130
+ elif part.type == "param" and part.name and part.param_type:
131
+ # 根据参数类型获取对应的正则片段,默认为 \w+
132
+ regex_fragment = PARAM_TYPE_REGEX_MAP.get(part.param_type, r"\w+")
133
+ # 构建命名捕获组,如 (?P<id>\d+)
134
+ regex_parts.append(f"(?P<{part.name}>{regex_fragment})")
135
+ params[part.name] = part.param_type
136
+
137
+ final_regex = "".join(regex_parts)
138
+ return {"regex": final_regex, "params": params}
139
+
140
+
141
+ class TestRegexRequest(BaseModel):
142
+ """正则测试请求模型
143
+
144
+ 字段说明:
145
+ regex: 待测试的正则表达式字符串
146
+ test_string: 用于匹配测试的文本
147
+ """
148
+
149
+ regex: str
150
+ test_string: str
151
+
152
+
153
+ class TestRegexResponse(BaseModel):
154
+ """正则测试响应模型
155
+
156
+ 字段说明:
157
+ is_match: 是否匹配成功
158
+ groups: 捕获组结果字典(仅匹配成功时有值)
159
+ error: 错误信息(正则无效时返回)
160
+ """
161
+
162
+ is_match: bool
163
+ groups: dict[str, str] = Field(default_factory=dict)
164
+ error: str | None = None
165
+
166
+
167
+ @router.post(
168
+ "/test-regex",
169
+ response_model=TestRegexResponse,
170
+ summary="测试正则表达式匹配",
171
+ responses={
172
+ 500: {"description": "服务器内部错误"},
173
+ },
174
+ )
175
+ def test_regex_matching(request: TestRegexRequest) -> dict[str, Any]:
176
+ """测试正则表达式匹配
177
+
178
+ 编译用户提供的正则表达式,并在测试字符串中搜索匹配。
179
+ 返回是否匹配以及所有命名捕获组的内容。
180
+
181
+ Args:
182
+ request: 包含正则表达式和测试字符串的请求对象
183
+
184
+ Returns:
185
+ 包含匹配结果、捕获组和错误信息的响应对象
186
+ """
187
+ if not request.regex:
188
+ return {"is_match": False, "error": "正则表达式不能为空。"}
189
+ try:
190
+ # 编译正则表达式,如果格式错误会抛出 re.error
191
+ compiled_regex = re.compile(request.regex)
192
+ # 在测试字符串中搜索匹配(search 模式,不要求完全匹配)
193
+ match = compiled_regex.search(request.test_string)
194
+ if match:
195
+ return {"is_match": True, "groups": match.groupdict()}
196
+ else:
197
+ return {"is_match": False, "groups": {}}
198
+ except re.error as e:
199
+ return {"is_match": False, "error": f"无效的正则表达式: {e}"}
200
+
201
+
202
+ class RegexValidateExtractRequest(BaseModel):
203
+ """批量正则验证与提取请求模型
204
+
205
+ 对一批数据值执行正则匹配,并提取命名捕获组的结果。
206
+
207
+ 字段说明:
208
+ regex_pattern: 正则表达式模式(必填)
209
+ regex_flags: 正则标志字符串,如 "i" 表示忽略大小写
210
+ case_sensitive: 是否区分大小写(默认 True)
211
+ match_mode: 匹配模式,"full" 完全匹配,"partial" 部分匹配,"extract" 提取
212
+ values: 待验证的数据值列表
213
+ """
214
+
215
+ regex_pattern: str = Field(min_length=1)
216
+ regex_flags: str = ""
217
+ case_sensitive: bool = True
218
+ match_mode: Literal["full", "partial", "extract"] = "full"
219
+ values: list[str] = Field(default_factory=list)
220
+
221
+
222
+ class RegexValidateExtractData(BaseModel):
223
+ """批量正则验证与提取的结果数据模型
224
+
225
+ 字段说明:
226
+ total_rows: 总数据行数
227
+ match_count: 匹配成功的数量
228
+ error_count: 匹配失败的数量
229
+ group_names: 命名捕获组名称列表
230
+ extracted_columns: 按组名组织的提取结果字典
231
+ """
232
+
233
+ total_rows: int
234
+ match_count: int
235
+ error_count: int
236
+ group_names: list[str] = Field(default_factory=list)
237
+ extracted_columns: dict[str, list[str]] = Field(default_factory=dict)
238
+
239
+
240
+ class RegexValidateExtractResponse(BaseModel):
241
+ """批量正则验证与提取响应模型
242
+
243
+ 字段说明:
244
+ success: 操作是否成功
245
+ data: 验证和提取的详细结果
246
+ error: 错误信息(失败时返回)
247
+ """
248
+
249
+ success: bool
250
+ data: RegexValidateExtractData | None = None
251
+ error: str | None = None
252
+
253
+
254
+ @router.post(
255
+ "/regex/validate-extract",
256
+ response_model=RegexValidateExtractResponse,
257
+ summary="批量验证数据并提取正则命名组",
258
+ responses={
259
+ 500: {"description": "服务器内部错误"},
260
+ },
261
+ )
262
+ def validate_and_extract_regex(request: RegexValidateExtractRequest) -> dict[str, Any]:
263
+ """批量验证数据并提取正则命名组
264
+
265
+ 对输入的一批数据值执行正则匹配:
266
+ - 根据 regex_flags 和 case_sensitive 设置编译标志
267
+ - 根据 match_mode 选择 match(完全匹配)或 search(部分匹配)
268
+ - 对每条数据执行匹配,统计成功/失败数量
269
+ - 提取所有命名捕获组的结果,按列组织
270
+
271
+ Args:
272
+ request: 包含正则模式、匹配模式和数据值的请求对象
273
+
274
+ Returns:
275
+ 包含匹配统计和提取结果的响应对象
276
+ """
277
+ try:
278
+ # 初始化正则编译标志
279
+ flags = 0
280
+ if "i" in request.regex_flags.lower():
281
+ flags |= re.IGNORECASE # 忽略大小写
282
+ if "m" in request.regex_flags.lower():
283
+ flags |= re.MULTILINE # 多行模式
284
+ if "s" in request.regex_flags.lower():
285
+ flags |= re.DOTALL # 点号匹配换行符
286
+ if not request.case_sensitive:
287
+ flags |= re.IGNORECASE # 不区分大小写覆盖
288
+
289
+ # 编译正则表达式
290
+ compiled = re.compile(request.regex_pattern, flags)
291
+ # 获取所有命名捕获组的名称
292
+ group_names = list(compiled.groupindex.keys())
293
+ # 初始化提取结果字典,每个组名对应一个空列表
294
+ extracted_columns: dict[str, list[str]] = {name: [] for name in group_names}
295
+
296
+ match_count = 0
297
+ error_count = 0
298
+
299
+ # 遍历每条数据值进行匹配
300
+ for value in request.values:
301
+ value_str = "" if value is None else str(value)
302
+ if request.match_mode == "full":
303
+ # 完全匹配:从字符串开头开始匹配
304
+ match = compiled.match(value_str)
305
+ else:
306
+ # 部分匹配:在字符串中搜索匹配
307
+ match = compiled.search(value_str)
308
+
309
+ if match:
310
+ match_count += 1
311
+ groups = match.groupdict()
312
+ for name in group_names:
313
+ extracted_columns[name].append("" if groups.get(name) is None else str(groups.get(name)))
314
+ else:
315
+ error_count += 1
316
+ # 未匹配时,该行的所有提取列填入空字符串
317
+ for name in group_names:
318
+ extracted_columns[name].append("")
319
+
320
+ return {
321
+ "success": True,
322
+ "data": {
323
+ "total_rows": len(request.values),
324
+ "match_count": match_count,
325
+ "error_count": error_count,
326
+ "group_names": group_names,
327
+ "extracted_columns": extracted_columns,
328
+ },
329
+ }
330
+ except re.error as e:
331
+ return {"success": False, "error": f"无效的正则表达式: {e}"}
332
+ except Exception as e:
333
+ return {"success": False, "error": str(e)}