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,467 @@
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
+ - 通过飞书 Webhook/App/Service 三种模式发送错误报告
20
+ - 支持 Interactive Card 消息卡片和自动签名验证
21
+ - 三种模式按安全性优先级自动选择: Service > App > Webhook
22
+
23
+ 架构设计:
24
+ - 继承 Reporter 抽象基类,实现 configure() 和 report() 方法
25
+ - 多模式策略: configure() 根据提供的参数自动选择最佳模式
26
+ - Token 管理: App 模式下 _get_tenant_access_token() 实现缓存 + 自动刷新
27
+ - 签名验证: Webhook 模式下 _generate_sign() 实现 HMAC-SHA256 签名
28
+ - 消息标准化: _create_message_card() 统一生成飞书卡片消息结构
29
+ - 通用请求: _send_request() 封装所有 HTTP 通信
30
+
31
+ 输入示例:
32
+ Service 模式配置 (推荐):
33
+ {"service_url": "https://my-service.com/feishu", "api_key": "xxx"}
34
+
35
+ App 模式配置:
36
+ {"app_id": "cli_xxx", "app_secret": "xxx", "receive_ids": "ou_xxx", "receive_id_type": "open_id"}
37
+
38
+ Webhook 模式配置:
39
+ {"webhook_url": "https://open.feishu.cn/open-apis/bot/v2/hook/xxx", "secret": "xxx"}
40
+
41
+ 输出示例:
42
+ 飞书卡片消息:
43
+ {
44
+ "config": {"wide_screen_mode": True},
45
+ "header": {"template": "red", "title": {"content": "【数据验证警告】发现 3 个错误!", "tag": "plain_text"}},
46
+ "elements": [...]
47
+ }
48
+
49
+ 控制台输出:
50
+ [FeishuReporter] ✓ 配置成功 (模式: 自建应用)。
51
+ [FeishuReporter] ✓ 成功获取 Tenant Access Token。
52
+ [FeishuReporter] ✓ 成功发送消息 给 ou_xxx。
53
+ """
54
+
55
+ import base64
56
+ import hashlib
57
+ import hmac
58
+ import ipaddress
59
+ import json
60
+ import logging
61
+ import time
62
+ import urllib.request
63
+ from datetime import datetime
64
+ from typing import Any
65
+ from urllib.parse import urlparse
66
+
67
+ from .base import TEMPLATE_HEADROOM_BYTES, Reporter, sanitize_url_for_log, truncate_to_byte_limit
68
+
69
+ logger = logging.getLogger(__name__)
70
+
71
+ _ALLOWED_URL_SCHEMES = {"https", "http"}
72
+ _BLOCKED_NETWORKS = [
73
+ ipaddress.ip_network("169.254.0.0/16"),
74
+ ipaddress.ip_network("127.0.0.0/8"),
75
+ ipaddress.ip_network("10.0.0.0/8"),
76
+ ipaddress.ip_network("172.16.0.0/12"),
77
+ ipaddress.ip_network("192.168.0.0/16"),
78
+ ipaddress.ip_network("::1/128"),
79
+ ipaddress.ip_network("fc00::/7"),
80
+ ]
81
+
82
+
83
+ def _validate_webhook_url(url: str) -> None:
84
+ parsed = urlparse(url)
85
+ if parsed.scheme not in _ALLOWED_URL_SCHEMES:
86
+ raise ValueError(f"不支持的 URL scheme: {parsed.scheme},仅允许 {_ALLOWED_URL_SCHEMES}")
87
+ hostname = parsed.hostname
88
+ if not hostname:
89
+ raise ValueError("URL 缺少主机名")
90
+ try:
91
+ for network in _BLOCKED_NETWORKS:
92
+ if ipaddress.ip_address(hostname) in network:
93
+ raise ValueError(f"不允许访问内网地址: {hostname}")
94
+ except ValueError as e:
95
+ # B40 修复:过去用 str(ValueError) 取类本身的字符串(恒为 "<class 'ValueError'>"),
96
+ # 导致内网地址拦截的 raise 永不执行,SSRF 校验形同虚设。应为 str(e) 取异常消息。
97
+ if "不允许访问内网地址" in str(e):
98
+ raise
99
+ except Exception:
100
+ pass
101
+
102
+
103
+ class FeishuReporter(Reporter):
104
+ """
105
+ @classdesc 飞书消息推送报告器
106
+
107
+ 通过飞书发送数据验证错误报告,支持三种发送模式:
108
+ 1. Webhook: 单用户快速集成,零部署
109
+ 2. App: 团队使用,需要分发凭证
110
+ 3. Service: 团队使用,通过中心化服务统一管理,最安全
111
+
112
+ Attributes:
113
+ config: 存储配置参数的字典
114
+ mode: 当前使用的发送模式 ('webhook', 'app', 'service')
115
+ is_configured: 配置是否有效的标志
116
+ tenant_access_token: 飞书应用访问令牌(App 模式专用)
117
+ token_expires_at: 令牌过期时间戳(App 模式专用)
118
+ """
119
+
120
+ def __init__(self) -> None:
121
+ """
122
+ @methoddesc 初始化飞书报告器
123
+
124
+ 设置默认属性,包括配置字典、发送模式、配置状态标志,
125
+ 以及 App 模式专用的访问令牌和过期时间戳。
126
+ """
127
+ super().__init__("FeishuReporter")
128
+ self.config: dict[str, Any] = {} # 存储配置参数
129
+ self.mode: str | None = None # 当前发送模式: 'webhook' | 'app' | 'service'
130
+ self.is_configured = False # 配置是否有效
131
+
132
+ # App 模式专用属性
133
+ self.tenant_access_token = None # 飞书应用访问令牌
134
+ self.token_expires_at = 0 # 令牌过期时间戳
135
+
136
+ def configure(self, **config: Any) -> bool:
137
+ """
138
+ @methoddesc 根据配置参数自动检测并设置最佳操作模式
139
+
140
+ 配置优先级(按安全性排序):
141
+ 1. Service 模式:需要 service_url 和 api_key
142
+ 2. App 模式:需要 app_id, app_secret 和 receive_ids
143
+ 3. Webhook 模式:只需要 webhook_url
144
+
145
+ Args:
146
+ **config: 包含飞书配置的键值对
147
+ - service_url: 中心化服务地址
148
+ - api_key: API 密钥
149
+ - app_id: 应用 ID
150
+ - app_secret: 应用密钥
151
+ - receive_ids: 接收者 ID 列表
152
+ - receive_id_type: 接收者 ID 类型
153
+ - webhook_url: Webhook 地址
154
+ - secret: 签名密钥
155
+
156
+ Returns:
157
+ bool: 配置是否成功
158
+ """
159
+ self.config = config
160
+
161
+ # 模式三:中心化服务模式(最高优先级,最安全)
162
+ # 适用于团队统一管理所有消息通知
163
+ if config.get("service_url") and config.get("api_key"):
164
+ self.mode = "service"
165
+ self.is_configured = True
166
+ print(f"[{self.name}] ✓ 配置成功 (模式: 中心化服务)。")
167
+ return True
168
+
169
+ # 模式二:自建应用模式
170
+ # 适用于需要发送给多个用户且有飞书应用的团队
171
+ if config.get("app_id") and config.get("app_secret") and config.get("receive_ids"):
172
+ self.mode = "app"
173
+ self.is_configured = True
174
+ print(f"[{self.name}] ✓ 配置成功 (模式: 自建应用)。")
175
+ return True
176
+
177
+ # 模式一:Webhook 模式(最低优先级)
178
+ # 适用于快速集成和单用户场景
179
+ if config.get("webhook_url"):
180
+ self.mode = "webhook"
181
+ self.is_configured = True
182
+ print(f"[{self.name}] ✓ 配置成功 (模式: Webhook)。")
183
+ return True
184
+
185
+ # 配置不完整时输出错误提示
186
+ print(
187
+ f"[{self.name}] !! 错误: 配置不完整。请提供 'service_url'/'api_key' 或 'app_id'/'app_secret' 或 'webhook_url'。"
188
+ )
189
+ return False
190
+
191
+ def report(self, errors: list[dict]) -> None:
192
+ """
193
+ @methoddesc 发送错误报告的主入口方法
194
+
195
+ 根据当前配置的模式调用相应的发送方法。
196
+ 如果未配置,则跳过发送并输出提示。
197
+
198
+ Args:
199
+ errors: 错误信息列表,每个元素是一个包含错误详情的字典
200
+ """
201
+ # 前置检查:确保已正确配置
202
+ if not self.is_configured:
203
+ print(f"[{self.name}] 未配置,跳过报告。")
204
+ return
205
+
206
+ # 步骤1: 统一创建消息卡片内容
207
+ # 将错误列表转换为飞书消息卡片格式
208
+ card_content = self._create_message_card(errors)
209
+
210
+ # 步骤2: 根据模式分发到不同的发送方法
211
+ if self.mode == "service":
212
+ self._report_via_service(card_content)
213
+ elif self.mode == "app":
214
+ self._report_via_app(card_content)
215
+ elif self.mode == "webhook":
216
+ self._report_via_webhook(card_content)
217
+
218
+ def _create_message_card(self, errors: list[dict]) -> dict:
219
+ """
220
+ @methoddesc 统一生成飞书 Interactive Card 消息卡片结构体
221
+
222
+ 消息卡片包含:
223
+ - 红色标题栏显示错误数量
224
+ - 错误详情的 JSON 格式化展示
225
+ - 报告时间戳
226
+
227
+ Args:
228
+ errors: 错误信息列表
229
+
230
+ Returns:
231
+ Dict: 飞书消息卡片的 JSON 结构
232
+ """
233
+ # 统计错误数量
234
+ error_count = len(errors)
235
+ # 将错误列表序列化为格式化的 JSON 字符串
236
+ error_details = json.dumps(errors, indent=2, ensure_ascii=False)
237
+
238
+ # 安全截断:飞书消息卡片大小限制约为 30KB
239
+ # 这里做保守截断(28KB)。必须按 UTF-8 字节截断:平台限额按字节计,
240
+ # 中文按字符截断后实际字节数仍可能超限被拒收(见 truncate_to_byte_limit)
241
+ error_details = truncate_to_byte_limit(error_details, 28000 - TEMPLATE_HEADROOM_BYTES)
242
+
243
+ # 构建飞书卡片消息结构
244
+ return {
245
+ "config": {"wide_screen_mode": True}, # 宽屏模式
246
+ "header": {
247
+ "template": "red", # 红色标题表示警告
248
+ "title": {"content": f"【数据验证警告】发现 {error_count} 个错误!", "tag": "plain_text"},
249
+ },
250
+ "elements": [
251
+ # 错误详情区域:使用 Markdown 代码块格式
252
+ {"tag": "div", "text": {"content": f"**错误详情:**\n```{error_details}```", "tag": "lark_md"}},
253
+ {"tag": "hr"}, # 分割线
254
+ # 底部备注:显示报告时间
255
+ {
256
+ "tag": "note",
257
+ "elements": [
258
+ {"content": f"报告时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", "tag": "lark_md"}
259
+ ],
260
+ },
261
+ ],
262
+ }
263
+
264
+ # ==============================================================================
265
+ # 模式一:Webhook 实现
266
+ # ==============================================================================
267
+ def _report_via_webhook(self, card_content: dict) -> None:
268
+ """
269
+ @methoddesc 通过 Webhook 发送消息(Webhook 模式)
270
+
271
+ 适用于单用户快速集成场景。Webhook 是开放接口,
272
+ 通过签名验证请求合法性。
273
+
274
+ Args:
275
+ card_content: 飞书消息卡片内容字典
276
+ """
277
+ # 构建消息负载:包含卡片类型和内容
278
+ payload: dict[str, Any] = {"msg_type": "interactive", "card": card_content}
279
+
280
+ # 可选:添加签名验证(如果配置了 secret)
281
+ secret = self.config.get("secret")
282
+ if secret:
283
+ # 生成签名:timestamp + secret -> HMAC-SHA256 -> Base64
284
+ timestamp = int(time.time())
285
+ sign = self._generate_sign(secret, timestamp)
286
+ payload["timestamp"] = timestamp
287
+ payload["sign"] = sign
288
+
289
+ # 发送 HTTP POST 请求
290
+ self._send_request(
291
+ url=self.config["webhook_url"], payload=payload, headers={"Content-Type": "application/json"}
292
+ )
293
+
294
+ def _generate_sign(self, secret: str, timestamp: int) -> str:
295
+ """
296
+ @methoddesc 生成飞书 Webhook 签名
297
+
298
+ 签名算法:
299
+ 1. 拼接 timestamp 和 secret 成字符串
300
+ 2. 使用 HMAC-SHA256 算法加密
301
+ 3. Base64 编码结果
302
+
303
+ Args:
304
+ secret: Webhook 签名密钥
305
+ timestamp: 时间戳(秒)
306
+
307
+ Returns:
308
+ str: 签名字符串
309
+ """
310
+ # 构造待签名字符串:timestamp + 换行 + secret
311
+ string_to_sign = f"{timestamp}\n{secret}"
312
+ # HMAC-SHA256 加密
313
+ hmac_code = hmac.new(string_to_sign.encode("utf-8"), digestmod=hashlib.sha256).digest()
314
+ # Base64 编码
315
+ return base64.b64encode(hmac_code).decode("utf-8")
316
+
317
+ # ==============================================================================
318
+ # 模式二:自建应用实现
319
+ # ==============================================================================
320
+ def _report_via_app(self, card_content: dict) -> None:
321
+ """
322
+ @methoddesc 通过飞书自建应用发送消息(App 模式)
323
+
324
+ 适用于团队场景,需要预先创建飞书自建应用并配置权限。
325
+ 消息通过飞书开放平台 API 发送。
326
+
327
+ Args:
328
+ card_content: 飞书消息卡片内容字典
329
+ """
330
+ # 步骤1: 获取有效的访问令牌
331
+ token = self._get_tenant_access_token()
332
+ if not token:
333
+ return
334
+
335
+ # 步骤2: 解析接收者列表
336
+ # 支持逗号分隔的多个接收者
337
+ receive_id_list = self.config["receive_ids"].split(",")
338
+ receive_id_type = self.config.get("receive_id_type", "open_id")
339
+
340
+ # 步骤3: 构建飞书 IM API 请求
341
+ url = f"https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type={receive_id_type}"
342
+ headers = {"Content-Type": "application/json", "Authorization": f"Bearer {token}"}
343
+
344
+ # 步骤4: 逐个向接收者发送消息
345
+ for receive_id in receive_id_list:
346
+ # 构建消息负载
347
+ # 注意:content 字段需要 JSON 序列化为字符串
348
+ payload = {"receive_id": receive_id.strip(), "msg_type": "interactive", "content": json.dumps(card_content)}
349
+ self._send_request(url, payload, headers, target_id=receive_id.strip())
350
+
351
+ def _get_tenant_access_token(self) -> str | None:
352
+ """
353
+ @methoddesc 获取飞书应用访问令牌(Tenant Access Token)
354
+
355
+ 令牌用于调用飞书开放平台 API,有效期约 2 小时。
356
+ 实现自动刷新机制:在过期前 5 分钟自动刷新。
357
+
358
+ Returns:
359
+ str: 有效的访问令牌,如果获取失败返回 None
360
+
361
+ Token 刷新逻辑:
362
+ - 检查当前时间是否早于过期时间
363
+ - 如果是,直接返回缓存的令牌
364
+ - 否则调用 API 获取新令牌并更新缓存
365
+ """
366
+ # 检查令牌是否仍然有效(提前 5 分钟刷新)
367
+ if time.time() < self.token_expires_at:
368
+ return self.tenant_access_token
369
+
370
+ # 调用飞书 OAuth API 获取令牌
371
+ url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal"
372
+ payload = {"app_id": self.config["app_id"], "app_secret": self.config["app_secret"]}
373
+ try:
374
+ # 构建 HTTP 请求
375
+ req_data = json.dumps(payload).encode("utf-8")
376
+ req = urllib.request.Request(url, data=req_data, headers={"Content-Type": "application/json"})
377
+
378
+ # 发送请求并解析响应
379
+ with urllib.request.urlopen(req, timeout=10) as response:
380
+ result = json.loads(response.read().decode())
381
+ if result.get("code") == 0:
382
+ # 成功获取令牌
383
+ self.tenant_access_token = result["tenant_access_token"]
384
+ # 设置过期时间:当前时间 + 有效期 - 缓冲时间
385
+ self.token_expires_at = time.time() + result["expire"] - 300
386
+ print(f"[{self.name}] ✓ 成功获取 Tenant Access Token。")
387
+ return self.tenant_access_token
388
+ else:
389
+ print(f"[{self.name}] !! 错误: 获取 Tenant Access Token 失败: {result.get('msg')}")
390
+ return None
391
+ except Exception as e:
392
+ print(f"[{self.name}] !! 错误: 请求 Tenant Access Token 失败: {e}")
393
+ return None
394
+
395
+ # ==============================================================================
396
+ # 模式三:中心化服务实现
397
+ # ==============================================================================
398
+ def _report_via_service(self, card_content: dict) -> None:
399
+ """
400
+ @methoddesc 通过中心化服务发送消息(Service 模式)
401
+
402
+ 适用于团队统一管理消息通知的场景。
403
+ 所有消息通过自建的中转服务发送,安全性最高。
404
+
405
+ Args:
406
+ card_content: 飞书消息卡片内容字典
407
+ """
408
+ # 构建请求头:包含 API 密钥认证
409
+ headers = {"Content-Type": "application/json", "Authorization": f"Bearer {self.config['api_key']}"}
410
+
411
+ # 构建请求负载:将卡片内容序列化为 JSON 字符串
412
+ # 服务端负责解析和发送飞书消息
413
+ payload = {"message": json.dumps(card_content)}
414
+
415
+ # 发送请求到中心化服务
416
+ self._send_request(self.config["service_url"], payload, headers)
417
+
418
+ # ==============================================================================
419
+ # 通用请求发送器
420
+ # ==============================================================================
421
+ def _send_request(self, url: str, payload: dict, headers: dict, target_id: str = "N/A") -> None:
422
+ """
423
+ @methoddesc 通用 HTTP POST 请求发送器
424
+
425
+ 处理所有飞书 API 的 HTTP 通信,包括错误处理和日志记录。
426
+
427
+ Args:
428
+ url: 请求目标 URL
429
+ payload: 请求体数据字典
430
+ headers: HTTP 请求头字典
431
+ target_id: 目标接收者 ID(用于日志记录)
432
+ """
433
+ try:
434
+ _validate_webhook_url(url)
435
+ except ValueError as e:
436
+ print(f"[{self.name}] !! 错误: URL 校验失败: {e}")
437
+ return
438
+
439
+ try:
440
+ # 序列化请求负载为 JSON 字节
441
+ data = json.dumps(payload).encode("utf-8")
442
+
443
+ # 构建 HTTP 请求
444
+ req = urllib.request.Request(url, data=data, headers=headers)
445
+
446
+ # 发送请求并获取响应
447
+ with urllib.request.urlopen(req, timeout=10) as response:
448
+ result_str = response.read().decode("utf-8")
449
+ result = json.loads(result_str)
450
+
451
+ # 判断不同 API 的成功响应码
452
+ # 飞书 Webhook/App API: code == 0
453
+ # 通用 HTTP: status < 300
454
+ if result.get("code") == 0 or result.get("status_code") == 0 or response.status < 300:
455
+ target_info = f"给 {target_id}" if target_id != "N/A" else ""
456
+ print(f"[{self.name}] ✓ 成功发送消息 {target_info}。")
457
+ else:
458
+ # 提取错误信息(不同 API 字段名不同)
459
+ print(f"[{self.name}] !! 错误: 发送消息失败: {result.get('msg', result_str)}")
460
+ except Exception as e:
461
+ # webhook_url 的 token 在 query 中,日志只打 host+path,避免密钥落日志
462
+ logger.warning(
463
+ "[%s] !! 错误: 发送请求到 '%s' 失败: %s",
464
+ self.name,
465
+ sanitize_url_for_log(url),
466
+ e,
467
+ )
@@ -0,0 +1,208 @@
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
+ - 将数据验证错误报告以 JSON 格式写入本地文件系统
20
+ - 支持自动创建日志目录和时间戳命名文件
21
+ - 每个报告生成独立的文件,避免覆盖历史记录
22
+
23
+ 架构设计:
24
+ - 继承 Reporter 抽象基类,实现 configure() 和 report() 方法
25
+ - 目录管理: configure() 自动创建不存在的日志目录
26
+ - 时间戳命名: 使用 datetime.now().strftime("%Y%m%d_%H%M%S_%f") 毫秒级时间戳生成唯一文件名,避免同秒覆盖
27
+ - UTF-8 编码: 确保中文字符正确存储
28
+ - 格式化输出: indent=2 使 JSON 文件便于人工阅读
29
+
30
+ 输入示例:
31
+ 配置参数:
32
+ {"log_dir": "./logs"}
33
+
34
+ 错误数据:
35
+ [
36
+ {"error_type": "TypeValidationError", "row_index": 2, "column": "price", "value": "abc"},
37
+ {"error_type": "NotNullViolation", "row_index": 5, "column": "email"}
38
+ ]
39
+
40
+ 输出示例:
41
+ 生成的日志文件: ./logs/error_report_20240115_143052.json
42
+
43
+ 文件内容:
44
+ [
45
+ {
46
+ "error_type": "TypeValidationError",
47
+ "row_index": 2,
48
+ "column": "price",
49
+ "value": "abc"
50
+ },
51
+ {
52
+ "error_type": "NotNullViolation",
53
+ "row_index": 5,
54
+ "column": "email"
55
+ }
56
+ ]
57
+
58
+ 控制台输出:
59
+ [LocalFileReporter] 日志目录已创建: ./logs
60
+ [LocalFileReporter] ✓ 成功将错误报告写入本地日志: ./logs/error_report_20240115_143052.json
61
+ """
62
+
63
+ import json
64
+ import os
65
+ from datetime import datetime
66
+ from typing import Any
67
+
68
+ from .base import Reporter
69
+
70
+
71
+ class LocalFileReporter(Reporter):
72
+ """
73
+ @classdesc 本地文件报告器 - 将错误报告写入本地文件系统
74
+
75
+ 该报告器将数据验证错误以 JSON 格式写入本地日志目录。
76
+ 适用于需要本地持久化错误日志的场景,或作为邮件报告的补充方案。
77
+
78
+ 属性说明:
79
+ log_dir: 日志文件存放的目录路径
80
+ 可以是相对路径或绝对路径
81
+
82
+ 功能特点:
83
+ - 自动创建日志目录(如果不存在)
84
+ - 每个报告生成独立的时间戳文件名,避免覆盖
85
+ - JSON 格式存储,便于程序化处理
86
+ - UTF-8 编码,支持中文错误信息
87
+
88
+ 使用示例:
89
+ reporter = LocalFileReporter()
90
+ if reporter.configure(log_dir='./logs/errors'):
91
+ reporter.report(errors)
92
+ # 将在 ./logs/errors/ 目录生成 error_report_20240215_143052.json
93
+ """
94
+
95
+ def __init__(self) -> None:
96
+ """
97
+ 初始化本地文件报告器
98
+
99
+ 调用父类构造函数设置报告器名称为 'LocalFileReporter'。
100
+ 初始化日志目录为 None,需要通过 configure() 方法设置。
101
+ """
102
+ super().__init__("LocalFileReporter")
103
+ self.log_dir: str | None = None
104
+
105
+ def configure(self, **config: Any) -> bool:
106
+ """
107
+ @methoddesc 配置日志目录并确保目录存在
108
+
109
+ 该方法设置错误报告文件的存储目录。
110
+ 如果目录不存在,会尝试自动创建。
111
+
112
+ :param log_dir: 日志文件存放的目录路径
113
+ 可以是相对路径(如 './logs')或绝对路径
114
+ :param config: 额外的可选配置参数(当前未使用,保留扩展性)
115
+ :return: 配置是否成功。成功返回 True,失败返回 False
116
+
117
+ 配置流程:
118
+ 1. 将日志目录路径保存到实例变量
119
+ 2. 检查目录是否存在
120
+ 3. 如不存在,尝试创建目录
121
+ 4. 返回配置结果
122
+ """
123
+ # 保存日志目录路径到实例变量
124
+ log_dir = config.get("log_dir")
125
+ if not isinstance(log_dir, str) or not log_dir:
126
+ print(f"[{self.name}] !! 错误: 未提供 log_dir 配置。")
127
+ return False
128
+ self.log_dir = log_dir
129
+
130
+ try:
131
+ # 检查目录是否存在,os.path.exists() 既检查文件也检查目录
132
+ if not os.path.exists(self.log_dir):
133
+ # 目录不存在,尝试创建(包括父目录)
134
+ os.makedirs(self.log_dir)
135
+ print(f"[{self.name}] 日志目录已创建: {self.log_dir}")
136
+
137
+ # 配置成功
138
+ return True
139
+
140
+ except Exception as e:
141
+ # 创建目录失败,打印错误信息
142
+ print(f"[{self.name}] !! 错误: 创建日志目录失败: {e}")
143
+ return False
144
+
145
+ def report(self, errors: list[dict]) -> None:
146
+ """
147
+ @methoddesc 将错误报告写入本地 JSON 文件
148
+
149
+ 在写入前会检查日志目录是否已配置。
150
+ 文件名包含精确到秒的时间戳,确保每次报告都有唯一的文件。
151
+
152
+ :param errors: 错误信息的字典列表,每个字典包含错误详情
153
+ 典型字段:row_index, column, value, error_type, error_message
154
+
155
+ 写入流程:
156
+ 1. 检查日志目录是否已配置
157
+ 2. 生成包含时间戳的唯一文件名
158
+ 3. 构造完整的文件路径
159
+ 4. 使用 json.dump() 将错误列表写入文件
160
+ 5. 打印成功或失败消息
161
+
162
+ 文件格式示例:
163
+ [
164
+ {
165
+ "row_index": 2,
166
+ "column": "price",
167
+ "value": "abc",
168
+ "error_type": "TypeValidationError",
169
+ "error_message": "无法将 'abc' 转换为浮点数"
170
+ }
171
+ ]
172
+ """
173
+ # 前置检查:确保日志目录已配置
174
+ if not self.log_dir:
175
+ print(f"[{self.name}] !! 错误: 未配置日志目录,无法写入日志。")
176
+ return
177
+
178
+ # 生成时间戳,用于构建唯一的文件名
179
+ # 格式:YYYYMMDD_HHMMSS_mmm,例如 20240215_143052_123。
180
+ # 毫秒粒度避免同一秒内的多次报告互相覆盖(同秒覆盖丢报告);
181
+ # 极端情况下(同一毫秒内多次写入)追加序号兜底,保证文件名唯一。
182
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")[:-3]
183
+
184
+ # 构建日志文件名;已存在时追加序号去重
185
+ log_filename = f"error_report_{timestamp}.json"
186
+ sequence = 1
187
+ while os.path.exists(os.path.join(self.log_dir, log_filename)):
188
+ sequence += 1
189
+ log_filename = f"error_report_{timestamp}_{sequence}.json"
190
+
191
+ # 拼接完整的文件路径
192
+ log_filepath = os.path.join(self.log_dir, log_filename)
193
+
194
+ try:
195
+ # 打开文件并写入 JSON 数据
196
+ # 使用 utf-8 编码确保中文字符正确保存
197
+ with open(log_filepath, "w", encoding="utf-8") as f:
198
+ # 使用 json.dump 序列化错误列表
199
+ # indent=2 使输出的 JSON 格式化,便于人工阅读
200
+ # ensure_ascii=False 允许直接写入 Unicode 字符(如中文)
201
+ json.dump(errors, f, indent=2, ensure_ascii=False)
202
+
203
+ # 写入成功,打印确认信息
204
+ print(f"[{self.name}] ✓ 成功将错误报告写入本地日志: {log_filepath}")
205
+
206
+ except Exception as e:
207
+ # 写入失败,打印错误信息
208
+ print(f"[{self.name}] !! 错误: 写入日志文件失败: {e}")