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,274 @@
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
+ """@fileoverview 钉钉自建应用消息推送报告器
17
+
18
+ 功能概述:
19
+ - 通过钉钉企业内部应用发送数据验证错误报告
20
+ - 支持 Markdown 格式消息,自动获取和管理 access_token
21
+ - 实现 access_token 的缓存和自动刷新机制(提前 5 分钟刷新)
22
+
23
+ 架构设计:
24
+ - 继承 Reporter 抽象基类,实现 configure() 和 report() 方法
25
+ - Token 管理: _get_access_token() 实现缓存 + 自动刷新策略
26
+ - 消息截断: 钉钉消息限制约 4KB,超长内容自动截断
27
+ - 统一请求: _send_request() 封装 HTTP POST 请求和错误处理
28
+
29
+ 输入示例:
30
+ 配置参数:
31
+ {
32
+ "app_key": "dingxxxxxxxxxxxx",
33
+ "app_secret": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
34
+ "agent_id": 123456789,
35
+ "userid_list": "user1,user2"
36
+ }
37
+
38
+ 错误数据:
39
+ [{"error_type": "ValidationError", "row_index": 2, "column": "age", "value": "abc"}]
40
+
41
+ 输出示例:
42
+ 钉钉消息 (Markdown 格式):
43
+ ### 【数据验证警告】发现 1 个错误!
44
+ **错误详情:**
45
+ ```json
46
+ [{"error_type": "ValidationError", ...}]
47
+ ```
48
+ > **报告时间:** 2024-01-15 14:30:52
49
+
50
+ 控制台输出:
51
+ [DingTalkAppReporter] ✓ 配置成功。
52
+ [DingTalkAppReporter] ✓ 成功获取 Access Token。
53
+ [DingTalkAppReporter] ✓ 成功发送消息。
54
+ """
55
+
56
+ import json
57
+ import logging
58
+ import time
59
+ import urllib.parse
60
+ import urllib.request
61
+ from datetime import datetime
62
+ from typing import Any
63
+
64
+ from .base import TEMPLATE_HEADROOM_BYTES, Reporter, sanitize_url_for_log, truncate_to_byte_limit
65
+
66
+ logger = logging.getLogger(__name__)
67
+
68
+
69
+ class DingTalkAppReporter(Reporter):
70
+ """
71
+ @classdesc 钉钉自建应用消息推送报告器
72
+
73
+ 通过钉钉企业内部应用发送工作通知,将数据验证错误报告推送给指定用户。
74
+
75
+ Attributes:
76
+ config: 存储配置参数的字典,包含 app_key, app_secret, agent_id, userid_list
77
+ is_configured: 配置是否有效的标志
78
+ access_token: 钉钉 API 访问令牌
79
+ token_expires_at: 令牌过期时间戳
80
+ """
81
+
82
+ def __init__(self) -> None:
83
+ """
84
+ @methoddesc 初始化钉钉报告器
85
+
86
+ 设置默认属性,包括配置字典、配置状态标志、
87
+ 访问令牌及其过期时间戳。
88
+ """
89
+ super().__init__("DingTalkAppReporter")
90
+ self.config: dict[str, Any] = {} # 存储配置参数
91
+ self.is_configured = False # 配置是否有效
92
+ self.access_token = None # 钉钉 API 访问令牌
93
+ self.token_expires_at = 0 # 令牌过期时间戳
94
+
95
+ def configure(self, **kwargs: Any) -> bool:
96
+ """
97
+ @methoddesc 配置钉钉报告器参数
98
+
99
+ 验证所有必需参数是否提供,并保存配置信息。
100
+
101
+ Args:
102
+ app_key: 钉钉应用 Key(企业标识下的唯一 ID)
103
+ app_secret: 钉钉应用密钥(用于获取 access_token)
104
+ agent_id: 钉钉应用 AgentID(用于指定发送目标应用)
105
+ userid_list: 接收者用户 ID 列表(逗号分隔的字符串)
106
+ **kwargs: 额外参数(保留用于扩展)
107
+
108
+ Returns:
109
+ bool: 配置是否成功
110
+ """
111
+ # 参数验证:确保所有必需参数都提供
112
+ app_key = kwargs.get("app_key")
113
+ app_secret = kwargs.get("app_secret")
114
+ agent_id = kwargs.get("agent_id")
115
+ userid_list = kwargs.get("userid_list")
116
+ if not all([app_key, app_secret, agent_id, userid_list]):
117
+ self.is_configured = False
118
+ return False
119
+
120
+ # 显式处理 None,避免 mypy 将 Any | None 传给 int() 时报错
121
+ if agent_id is None:
122
+ self.is_configured = False
123
+ return False
124
+
125
+ # 保存配置参数
126
+ self.config = {
127
+ "app_key": str(app_key),
128
+ "app_secret": str(app_secret),
129
+ "agent_id": int(agent_id),
130
+ "userid_list": str(userid_list),
131
+ }
132
+ self.is_configured = True
133
+ print(f"[{self.name}] ✓ 配置成功。")
134
+ return True
135
+
136
+ def _get_access_token(self) -> str | None:
137
+ """
138
+ @methoddesc 获取或刷新钉钉 API 访问令牌(Access Token)
139
+
140
+ 访问令牌用于调用钉钉 API,有效期默认为 7200 秒(2 小时)。
141
+ 实现自动刷新机制:在过期前 5 分钟自动刷新,避免令牌失效。
142
+
143
+ Token 刷新逻辑:
144
+ - 检查当前时间是否早于过期时间
145
+ - 如果是,直接返回缓存的令牌
146
+ - 否则调用钉钉 API 获取新令牌并更新缓存
147
+
148
+ Returns:
149
+ str: 有效的访问令牌,如果获取失败返回 None
150
+
151
+ API 调用:
152
+ - URL: https://oapi.dingtalk.com/gettoken
153
+ - 参数: appkey, appsecret
154
+ - 响应: access_token, expire (有效期秒数)
155
+ """
156
+ # 检查令牌是否仍然有效(提前 5 分钟刷新)
157
+ if time.time() < self.token_expires_at:
158
+ return self.access_token
159
+
160
+ # 调用钉钉 API 获取令牌
161
+ url = "https://oapi.dingtalk.com/gettoken"
162
+ params = {"appkey": self.config["app_key"], "appsecret": self.config["app_secret"]}
163
+ try:
164
+ # 构建 URL 参数
165
+ full_url = f"{url}?{urllib.parse.urlencode(params)}"
166
+
167
+ # 发送请求
168
+ with urllib.request.urlopen(full_url, timeout=10) as response:
169
+ data = json.loads(response.read().decode())
170
+ if data.get("errcode") == 0:
171
+ # 成功获取令牌
172
+ self.access_token = data["access_token"]
173
+ # 设置过期时间:当前时间 + 有效期 - 缓冲时间(5分钟)
174
+ self.token_expires_at = time.time() + data["expires_in"] - 300
175
+ print(f"[{self.name}] ✓ 成功获取 Access Token。")
176
+ return self.access_token
177
+ else:
178
+ print(f"[{self.name}] !! 错误: 获取 Access Token 失败: {data.get('errmsg')}")
179
+ return None
180
+ except Exception as e:
181
+ # gettoken 的 full_url query 含 appsecret,日志只打无 query 的基础 URL
182
+ logger.warning(
183
+ "[%s] !! 错误: 请求 Access Token 失败 (%s): %s",
184
+ self.name,
185
+ sanitize_url_for_log(url),
186
+ e,
187
+ )
188
+ return None
189
+
190
+ def report(self, errors: list[dict]) -> None:
191
+ """
192
+ @methoddesc 发送错误报告的主入口方法
193
+
194
+ 流程:
195
+ 1. 检查配置是否有效
196
+ 2. 获取有效的访问令牌
197
+ 3. 格式化错误信息为 Markdown
198
+ 4. 调用钉钉发送工作通知 API
199
+
200
+ Args:
201
+ errors: 错误信息列表,每个元素是一个包含错误详情的字典
202
+ """
203
+ # 前置检查:确保已正确配置
204
+ if not self.is_configured:
205
+ return
206
+
207
+ # 步骤1: 获取有效的访问令牌
208
+ token = self._get_access_token()
209
+ if not token:
210
+ return
211
+
212
+ # 步骤2: 统计错误数量并格式化详情
213
+ error_count = len(errors)
214
+ error_details = json.dumps(errors, indent=2, ensure_ascii=False)
215
+
216
+ # 安全截断:钉钉消息内容限制约 4KB
217
+ # 必须按 UTF-8 字节截断:平台限额按字节计,中文按字符截断后实际字节数
218
+ # 仍可能超限被拒收(见 truncate_to_byte_limit)
219
+ error_details = truncate_to_byte_limit(error_details, 4000 - TEMPLATE_HEADROOM_BYTES)
220
+
221
+ # 步骤3: 构建 Markdown 格式消息内容
222
+ title = f"【数据验证警告】发现 {error_count} 个错误!"
223
+ text = f"""### {title}
224
+ **错误详情:**
225
+ ```json
226
+ {error_details}
227
+ ```
228
+ > **报告时间:** {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
229
+ """
230
+
231
+ # 步骤4: 构建发送工作通知的请求负载
232
+ payload = {
233
+ "agent_id": self.config["agent_id"], # 应用 AgentID
234
+ "userid_list": self.config["userid_list"], # 接收者用户列表
235
+ "msg": {
236
+ "msgtype": "markdown", # 消息类型:Markdown
237
+ "markdown": {
238
+ "title": title, # 消息标题(通知标题)
239
+ "text": text, # 消息正文内容
240
+ },
241
+ },
242
+ }
243
+
244
+ # 步骤5: 发送消息
245
+ url = f"https://oapi.dingtalk.com/topapi/message/corpconversation/asyncsend_v2?access_token={token}"
246
+ self._send_request(url, payload)
247
+
248
+ def _send_request(self, url: str, payload: dict[str, Any]) -> None:
249
+ """
250
+ @methoddesc 发送 HTTP POST 请求到钉钉 API
251
+
252
+ 处理所有钉钉 API 的 HTTP 通信,包括错误处理和日志记录。
253
+
254
+ Args:
255
+ url: 请求目标 URL
256
+ payload: 请求体数据字典
257
+ """
258
+ try:
259
+ # 序列化请求负载为 JSON 字节
260
+ data = json.dumps(payload).encode("utf-8")
261
+ headers = {"Content-Type": "application/json"}
262
+
263
+ # 构建 HTTP 请求
264
+ req = urllib.request.Request(url, data=data, headers=headers)
265
+
266
+ # 发送请求并获取响应
267
+ with urllib.request.urlopen(req, timeout=10) as response:
268
+ result = json.loads(response.read().decode("utf-8"))
269
+ if result.get("errcode") == 0:
270
+ print(f"[{self.name}] ✓ 成功发送消息。")
271
+ else:
272
+ print(f"[{self.name}] !! 错误: 发送消息失败: {result.get('errmsg')}")
273
+ except Exception as e:
274
+ logger.warning("[%s] !! 错误: 发送请求失败: %s", self.name, e)
@@ -0,0 +1,271 @@
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
+ """@fileoverview 邮件报告器模块
17
+
18
+ 功能概述:
19
+ - 通过 SMTP 协议发送数据验证错误报告
20
+ - 支持 TLS/SSL 加密传输和 HTML 格式邮件
21
+ - 配置阶段即验证 SMTP 连接和认证,提前发现问题
22
+
23
+ 架构设计:
24
+ - 继承 Reporter 抽象基类,实现 configure() 和 report() 方法
25
+ - 双模式连接: 根据端口自动选择 SMTP + STARTTLS (587) 或 SMTP_SSL (465)
26
+ - 配置即验证: configure() 阶段建立真实连接测试,确保参数正确
27
+ - 资源安全: 使用 with 语句和 try-finally 确保 SMTP 连接正确关闭
28
+
29
+ 输入示例:
30
+ 配置参数:
31
+ {
32
+ "smtp_server": "smtp.gmail.com",
33
+ "smtp_port": 587,
34
+ "sender_email": "precis@example.com",
35
+ "sender_password": "app_password",
36
+ "receiver_email": "admin@example.com"
37
+ }
38
+
39
+ 错误数据:
40
+ [{"error_type": "ForeignKeyViolation", "table": "orders", "value": 99}]
41
+
42
+ 输出示例:
43
+ HTML 邮件内容:
44
+ <html><body>
45
+ <h2>检测到以下数据配置错误:</h2>
46
+ <pre style="...">[...错误JSON...]</pre>
47
+ <p>请尽快检查并修复。</p>
48
+ </body></html>
49
+
50
+ 控制台输出:
51
+ [EmailReporter] ✓ 邮件服务配置验证成功,发件人: precis@example.com
52
+ [EmailReporter] ✓ 成功发送错误报告邮件至 admin@example.com。
53
+ """
54
+
55
+ import html
56
+ import json
57
+ import smtplib
58
+ from email.header import Header
59
+ from email.mime.text import MIMEText
60
+ from email.utils import formataddr
61
+ from typing import Any, cast
62
+
63
+ from .base import Reporter
64
+
65
+
66
+ class EmailReporter(Reporter):
67
+ """
68
+ @classdesc 邮件报告器 - 通过 SMTP 协议发送错误报告
69
+
70
+ 该报告器将数据验证错误格式化为 HTML 邮件并发送给指定的收件人。
71
+ 使用原生 smtplib 实现,避免第三方依赖,提供更好的稳定性和控制力。
72
+
73
+ 属性说明:
74
+ config: 存储邮件服务器配置信息的字典
75
+ 包含 smtp_server, smtp_port, sender_email, sender_password, receiver_email
76
+ is_configured: 标记配置是否通过验证
77
+
78
+ 功能特点:
79
+ - 配置阶段验证 SMTP 连接和认证
80
+ - 支持 TLS(端口 587)和 SSL(端口 465)两种加密方式
81
+ - HTML 格式邮件,带有错误详情表格
82
+ - 中文编码支持,使用 Header 防止乱码
83
+
84
+ 使用示例:
85
+ reporter = EmailReporter()
86
+ if reporter.configure(
87
+ smtp_server='smtp.example.com',
88
+ smtp_port=587,
89
+ sender_email='precis@example.com',
90
+ sender_password='secret',
91
+ receiver_email='admin@example.com'
92
+ ):
93
+ reporter.report(errors)
94
+ """
95
+
96
+ def __init__(self) -> None:
97
+ """
98
+ 初始化邮件报告器
99
+
100
+ 调用父类构造函数设置报告器名称为 'EmailReporter'。
101
+ 初始化配置字典和配置状态标志。
102
+ """
103
+ super().__init__("EmailReporter")
104
+ self.config: dict[str, Any] = {}
105
+ self.is_configured = False
106
+
107
+ def configure(self, **config: Any) -> bool:
108
+ """
109
+ @methoddesc 配置邮件服务器参数并在配置阶段验证连接
110
+
111
+ 该方法会验证所有必需的 SMTP 参数,并尝试建立连接进行认证验证。
112
+ 只有配置验证通过后,才能调用 report() 方法发送邮件。
113
+
114
+ :param smtp_server: SMTP 服务器地址(如 'smtp.gmail.com')
115
+ :param smtp_port: SMTP 服务器端口
116
+ 通常 587 使用 TLS,465 使用 SSL
117
+ :param sender_email: 发件人邮箱地址
118
+ :param sender_password: 发件人邮箱密码或应用专用密码
119
+ :param receiver_email: 收件人邮箱地址
120
+ :param config: 额外的可选配置参数(当前未使用,保留扩展性)
121
+ :return: 配置验证是否成功。成功返回 True,失败返回 False
122
+
123
+ 验证流程:
124
+ 1. 检查所有必需参数是否提供
125
+ 2. 将配置参数保存到实例变量
126
+ 3. 尝试建立 SMTP 连接并登录
127
+ 4. 如果连接成功,设置 is_configured 为 True
128
+ """
129
+ # 参数完整性检查:确保所有必需的配置都已提供
130
+ smtp_server = config.get("smtp_server")
131
+ smtp_port = config.get("smtp_port")
132
+ sender_email = config.get("sender_email")
133
+ sender_password = config.get("sender_password")
134
+ receiver_email = config.get("receiver_email")
135
+ if not all([smtp_server, smtp_port, sender_email, sender_password, receiver_email]):
136
+ print(f"[{self.name}] 邮件配置信息不完整, 无法启用。")
137
+ return False
138
+
139
+ # 显式处理 None,避免 mypy 将 Any | None 传给 int() 时报错
140
+ if smtp_port is None:
141
+ return False
142
+
143
+ # 将配置保存到实例变量中,供后续 report() 方法使用
144
+ self.config = {
145
+ "smtp_server": str(smtp_server),
146
+ "smtp_port": int(smtp_port),
147
+ "sender_email": str(sender_email),
148
+ "sender_password": str(sender_password),
149
+ "receiver_email": str(receiver_email),
150
+ }
151
+
152
+ # 尝试连接和登录以验证配置的正确性
153
+ # 使用 with 语句确保连接被正确关闭,即使发生异常
154
+ try:
155
+ # 与 report() 保持一致的双模式连接:
156
+ # - 端口 465 使用 SMTP_SSL(隐式 SSL,STARTTLS 握手会失败/超时)
157
+ # - 其余端口使用普通 SMTP + STARTTLS 升级加密
158
+ if int(smtp_port) == 465:
159
+ with smtplib.SMTP_SSL(str(smtp_server), int(smtp_port), timeout=10) as smtp:
160
+ # 使用提供的凭证登录
161
+ smtp.login(str(sender_email), str(sender_password))
162
+ else:
163
+ # 建立 SMTP 连接,设置 10 秒超时防止阻塞
164
+ with smtplib.SMTP(str(smtp_server), int(smtp_port), timeout=10) as smtp:
165
+ # 升级到 TLS 加密连接,这是现代 SMTP 服务的安全要求
166
+ # TLS 适用于端口 587,是比 SSL 更常用的选择
167
+ smtp.starttls()
168
+ # 使用提供的凭证登录
169
+ smtp.login(str(sender_email), str(sender_password))
170
+
171
+ # 配置验证成功,打印成功信息(不包含敏感密码)
172
+ print(f"[{self.name}] ✓ 邮件服务配置验证成功,发件人: {sender_email}")
173
+ self.is_configured = True
174
+ return True
175
+
176
+ except Exception as e:
177
+ # 配置验证失败,打印错误信息用于调试
178
+ print(f"[{self.name}] !! 错误: 邮件服务配置验证失败: {e}")
179
+ self.is_configured = False
180
+ return False
181
+
182
+ def report(self, errors: list[dict]) -> None:
183
+ """
184
+ @methoddesc 将错误报告发送到指定的邮件地址
185
+
186
+ 在发送邮件前会检查配置状态,确保邮件服务已正确配置。
187
+ 邮件内容为 HTML 格式,包含错误数量和详细的 JSON 格式错误列表。
188
+
189
+ :param errors: 错误信息的字典列表,每个字典包含错误详情
190
+ 典型字段:row_index, column, value, error_type, error_message
191
+
192
+ 邮件内容格式:
193
+ - 标题:【数据验证警告】发现 N 个错误!
194
+ - 正文:包含错误数量的说明和 JSON 格式的错误详情
195
+
196
+ 发送流程:
197
+ 1. 检查是否已配置(is_configured 标志)
198
+ 2. 构建 HTML 邮件内容
199
+ 3. 设置邮件头部(Subject, From, To)
200
+ 4. 根据端口选择 SMTP 或 SMTP_SSL 连接
201
+ 5. 登录并发送邮件
202
+ 6. 在 finally 块中确保连接关闭
203
+ """
204
+ # 前置检查:确保邮件服务已配置
205
+ if not self.is_configured:
206
+ print(f"[{self.name}] !! 错误: 邮件服务未配置或配置失败,无法发送邮件。")
207
+ return
208
+
209
+ # 构建邮件标题,包含错误数量
210
+ subject = f"【数据验证警告】发现 {len(errors)} 个错误!"
211
+
212
+ # 构建 HTML 邮件正文,使用 JSON 格式化错误列表
213
+ # 使用 pre 标签保持 JSON 的格式和缩进
214
+ # html.escape 防止错误数据中的 <script>/标签注入邮件 HTML(XSS)
215
+ errors_json = html.escape(json.dumps(errors, indent=2, ensure_ascii=False))
216
+ html_body = f"""
217
+ <html><body>
218
+ <h2>检测到以下数据配置错误:</h2>
219
+ <pre style=\"background-color: #f0f0f0; padding: 10px; border-radius: 5px; white-space: pre-wrap; word-wrap: break-word;\">{errors_json}</pre>
220
+ <p>请尽快检查并修复。</p>
221
+ </body></html>
222
+ """
223
+
224
+ # 创建 MIMEText 邮件对象,指定 HTML 格式和 UTF-8 编码
225
+ msg = MIMEText(html_body, "html", "utf-8")
226
+
227
+ # 使用 Header 对象编码邮件标题,确保中文正确显示
228
+ msg["Subject"] = cast(str, Header(subject, "utf-8"))
229
+
230
+ # 使用 formataddr 正确格式化发件人和收件人地址
231
+ # formataddr((友好名称, 邮件地址)) 格式
232
+ # 使用 Header 编码友好名称以支持中文
233
+ msg["From"] = formataddr((Header("数据验证服务", "utf-8").encode(), self.config["sender_email"]))
234
+ msg["To"] = formataddr((Header("项目负责人", "utf-8").encode(), self.config["receiver_email"]))
235
+
236
+ # 建立 SMTP 连接并发送邮件
237
+ smtp: smtplib.SMTP | smtplib.SMTP_SSL | None = None
238
+ try:
239
+ # 根据端口号判断使用哪种连接方式:
240
+ # - 端口 465 通常使用 SSL 加密的 SMTP_SSL
241
+ # - 端口 587 通常使用 STARTTLS 升级到 TLS
242
+ if self.config["smtp_port"] == 465:
243
+ smtp = smtplib.SMTP_SSL(self.config["smtp_server"], self.config["smtp_port"], timeout=10)
244
+ else:
245
+ # 端口不是 465,使用普通 SMTP 然后升级到 TLS
246
+ smtp = smtplib.SMTP(self.config["smtp_server"], self.config["smtp_port"], timeout=10)
247
+ if isinstance(smtp, smtplib.SMTP):
248
+ smtp.starttls()
249
+
250
+ # 使用配置中的凭证登录 SMTP 服务器
251
+ smtp.login(self.config["sender_email"], self.config["sender_password"])
252
+
253
+ # 发送邮件,sender 和 receiver 都可以是列表
254
+ smtp.sendmail(self.config["sender_email"], [self.config["receiver_email"]], msg.as_string())
255
+
256
+ # 发送成功,打印确认信息
257
+ print(f"[{self.name}] ✓ 成功发送错误报告邮件至 {self.config['receiver_email']}。")
258
+
259
+ except Exception as e:
260
+ # 发送失败,打印错误信息(不包含敏感内容)
261
+ print(f"[{self.name}] !! 错误: 发送邮件失败: {e}")
262
+
263
+ finally:
264
+ # 确保 SMTP 连接被正确关闭,释放资源
265
+ # quit() 自身的异常(连接已断等 secondary exception)不得吞掉
266
+ # try/except 块中捕获的原始业务异常
267
+ if smtp:
268
+ try:
269
+ smtp.quit()
270
+ except Exception: # noqa: BLE001 关闭失败的次要异常仅记录
271
+ print(f"[{self.name}] !! 警告: 关闭 SMTP 连接失败(忽略)。")