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,432 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ #
3
+ # Copyright 2026 Precis Team
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ """
17
+ @fileoverview 项目加载主入口模块
18
+
19
+ 功能概述:
20
+ - 负责从 manifest 文件出发,加载整个项目的全部配置
21
+ - 按顺序加载: manifest -> schemas -> constraints -> regex -> patterns
22
+ - 构建运行时数据结构 (runtime schemas, constraints)
23
+
24
+ 架构设计:
25
+ - 两层加载: 文件加载 (file_loaders) + 运行时转换 (runtime builders)
26
+ - 错误收集机制: 记录所有加载错误和警告,最后返回给调用者
27
+ - 路径验证: 确保所有引用的文件都在项目目录内,防止目录遍历攻击
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import logging
33
+ from collections.abc import Callable
34
+ from pathlib import Path
35
+ from typing import TypeVar
36
+
37
+ from app.shared.core.io.yaml import read_yaml
38
+ from app.shared.core.manifest_schema import is_supported_version
39
+ from app.shared.core.project.constraint.reader import load_constraint
40
+ from app.shared.core.project.loader.loader_parts import loading_error_messages
41
+ from app.shared.core.project.loader.loader_parts.embedded_constraints import collect_constraints_from_schemas
42
+ from app.shared.core.project.loader.loader_parts.file_loaders import load_manual_data_file
43
+ from app.shared.core.project.loader.loader_parts.path_validation import validate_path_inside_project
44
+ from app.shared.core.project.loader.loader_parts.runtime import build_registries
45
+ from app.shared.core.project.loader.types import LoadedProject, LoadingError, SchemaBuilder
46
+ from app.shared.core.project.manifest.reader import load_manifest
47
+ from app.shared.core.project.regex.reader import load_regex_node
48
+ from app.shared.core.project.schema.reader import load_schema
49
+ from app.shared.core.project.template.expander import expand_template
50
+ from app.shared.core.project.template.reader import load_template
51
+ from app.shared.core.project.transform.reader import load_transform
52
+
53
+ logger = logging.getLogger(__name__)
54
+
55
+ T = TypeVar("T")
56
+
57
+
58
+ def _load_referenced_files(
59
+ project_root: Path,
60
+ refs: list,
61
+ load_fn: Callable[[Path], T],
62
+ file_type: str,
63
+ warnings: list[str],
64
+ loading_errors: list[LoadingError],
65
+ ) -> dict[str, T]:
66
+ """@methoddesc 通用文件加载函数。
67
+
68
+ 遍历 manifest 引用列表,验证路径、检查文件存在、加载并解析文件。
69
+ 所有错误和警告都被收集,不会中断加载流程。
70
+
71
+ Args:
72
+ project_root: 项目根目录
73
+ refs: 引用列表(包含 id 和 path)
74
+ load_fn: 文件加载函数
75
+ file_type: 文件类型名称(用于错误信息)
76
+ warnings: 警告列表(会被修改)
77
+ loading_errors: 错误列表(会被修改)
78
+
79
+ Returns:
80
+ 加载成功的文件字典 {id: file_object}
81
+ """
82
+ result: dict[str, T] = {}
83
+ for ref in refs:
84
+ file_path = project_root / ref.path
85
+ try:
86
+ validate_path_inside_project(project_root, file_path, f"{file_type} '{ref.id}'")
87
+ except ValueError as e:
88
+ loading_errors.append(
89
+ LoadingError(
90
+ error_type=f"{file_type}PathValidationError",
91
+ file_path=str(file_path),
92
+ ref_id=ref.id,
93
+ **loading_error_messages.path_validation_error(file_type, ref.id, str(e), str(file_path)),
94
+ )
95
+ )
96
+ continue
97
+
98
+ if not file_path.exists():
99
+ warning_msg = f"跳过不存在的 {file_type} 文件: {file_path} (引用 ID: {ref.id})"
100
+ warnings.append(warning_msg)
101
+ loading_errors.append(
102
+ LoadingError(
103
+ error_type=f"{file_type}NotFound",
104
+ file_path=str(file_path),
105
+ ref_id=ref.id,
106
+ **loading_error_messages.file_not_found_error(file_type, ref.id, str(file_path)),
107
+ )
108
+ )
109
+ logger.warning("[WARN] %s", warning_msg)
110
+ continue
111
+
112
+ try:
113
+ result[ref.id] = load_fn(file_path)
114
+ except Exception as e:
115
+ loading_errors.append(
116
+ LoadingError(
117
+ error_type=f"{file_type}ParseError",
118
+ file_path=str(file_path),
119
+ ref_id=ref.id,
120
+ **loading_error_messages.parse_error(file_type, ref.id, str(file_path), e),
121
+ )
122
+ )
123
+ logger.warning("[WARN] %s 加载失败: %s, %s", file_type, ref.id, e)
124
+
125
+ return result
126
+
127
+
128
+ def _supported_versions_desc() -> str:
129
+ """当前支持的配置版本号描述(单一事实源为 manifest_schema.version 的常量)。"""
130
+ from app.shared.core.manifest_schema.version import get_version_info
131
+
132
+ return ", ".join(str(v) for v in get_version_info()["supported_versions"])
133
+
134
+
135
+ def _check_manifest_version(manifest_file: Path) -> LoadingError | None:
136
+ """显式校验 manifest 顶层 version 字段(P0-1 版本识别机制)。
137
+
138
+ version 在 ProjectManifest 模型中有默认值,缺失时会被 Pydantic 静默补为 2,
139
+ 因此必须在模型解析之前对原始 YAML 做显式检查:
140
+ - 缺失 → 报"缺少 version 字段,当前支持版本为 2"
141
+ - 非 2(含非整数类型)→ 报版本不支持并给迁移指引
142
+ - 等于 2 → 返回 None,走原有加载流程(行为不变)
143
+
144
+ Args:
145
+ manifest_file: manifest 文件路径
146
+
147
+ Returns:
148
+ 版本问题对应的 LoadingError;版本合法时返回 None。
149
+ YAML 本身解析失败会直接抛出(与原有行为一致,由 load_manifest 兜底)。
150
+ """
151
+ raw = read_yaml(manifest_file)
152
+ if not isinstance(raw, dict):
153
+ # 非字典结构(如 YAML 列表)交给后续 Pydantic 校验报错,此处不重复处理
154
+ return None
155
+
156
+ if "version" not in raw or raw.get("version") is None:
157
+ return LoadingError(
158
+ error_type="ManifestVersionError",
159
+ file_path=str(manifest_file),
160
+ **loading_error_messages.manifest_version_missing(_supported_versions_desc()),
161
+ )
162
+
163
+ version = raw["version"]
164
+ if isinstance(version, bool) or not isinstance(version, int) or not is_supported_version(version):
165
+ return LoadingError(
166
+ error_type="ManifestVersionError",
167
+ file_path=str(manifest_file),
168
+ **loading_error_messages.manifest_version_unsupported(version, _supported_versions_desc()),
169
+ )
170
+ return None
171
+
172
+
173
+ def load_project(
174
+ manifest_path: str,
175
+ schema_builder: SchemaBuilder | None = None,
176
+ ) -> LoadedProject:
177
+ """@methoddesc 加载整个 Precis 项目配置。
178
+
179
+ 加载流程 (按顺序执行):
180
+ 1. 读取 manifest 文件,验证版本号
181
+ 2. 构建 patterns 注册表
182
+ 3. 加载所有 schema 文件
183
+ 4. 加载所有 constraint 文件
184
+ 5. 从 schema 中收集内嵌约束
185
+ 6. 加载所有 regex 文件
186
+ 7. 构建运行时 schema
187
+ 8. 创建运行时 constraints
188
+
189
+ :param manifest_path: manifest 文件路径
190
+ :param schema_builder: 运行时数据集 Schema 构建器(依赖注入)。
191
+ core 层不直接依赖 services,由调用方(services 层)注入构建器
192
+ 完成 "core 文件对象 → domain 运行时对象" 的转换。
193
+ 为 None 时 dataset_schema 字段保持为 None,由调用方自行处理。
194
+ """
195
+ manifest_file = Path(manifest_path)
196
+ project_root = manifest_file.parent
197
+
198
+ # P0-1: 在 Pydantic 解析之前显式校验 version 字段。
199
+ # 版本不识别时不进入后续加载阶段:错误经 loading_errors 结构化通道透出
200
+ # (--format json 时在 loading_warnings 可见),并返回空项目让校验流程
201
+ # 可控收场(数据加载阶段会报告"未加载任何数据表")。
202
+ version_error = _check_manifest_version(manifest_file)
203
+ if version_error is not None:
204
+ from app.shared.core.project.manifest.types import ProjectManifest
205
+
206
+ placeholder_manifest = ProjectManifest.model_validate({"project": {"id": "unknown", "name": "unknown"}})
207
+ registries = build_registries(project_root, placeholder_manifest)
208
+ empty_schema: dict = {}
209
+ empty_constraints: dict = {}
210
+ empty_schema_obj = schema_builder(empty_schema, empty_constraints, registries)[0] if schema_builder else None
211
+ return LoadedProject(
212
+ manifest_path=manifest_file,
213
+ manifest=placeholder_manifest,
214
+ schema_files=empty_schema,
215
+ constraint_files=empty_constraints,
216
+ regex_node_files={},
217
+ transform_files={},
218
+ manual_data_files={},
219
+ dataset_schema=empty_schema_obj,
220
+ warnings=[],
221
+ loading_errors=[version_error],
222
+ )
223
+
224
+ manifest = load_manifest(manifest_file)
225
+
226
+ version = manifest.version
227
+ if not is_supported_version(version):
228
+ # _check_manifest_version 已在解析前拦截非 2 版本,此处为防御性兜底
229
+ # (例如 raw 为非字典结构时 Pydantic 仍可能解析出非 2 版本)
230
+ raise ValueError(f"不支持的项目配置版本: {version},支持的版本: {_supported_versions_desc()}")
231
+
232
+ registries = build_registries(project_root, manifest)
233
+
234
+ warnings: list[str] = []
235
+ loading_errors: list[LoadingError] = []
236
+
237
+ # 阶段 1:加载 Schema 文件
238
+ schema_files = _load_referenced_files(
239
+ project_root,
240
+ manifest.schemas,
241
+ load_schema,
242
+ "Schema",
243
+ warnings,
244
+ loading_errors,
245
+ )
246
+
247
+ # 阶段 2:加载独立 Constraint 文件
248
+ constraint_files = _load_referenced_files(
249
+ project_root,
250
+ manifest.constraints,
251
+ load_constraint,
252
+ "Constraint",
253
+ warnings,
254
+ loading_errors,
255
+ )
256
+
257
+ # 阶段 3:收集 Schema 中内嵌的约束
258
+ embedded_constraints = collect_constraints_from_schemas(schema_files)
259
+ for cid, const in embedded_constraints.items():
260
+ if cid in constraint_files:
261
+ warnings.append(f"约束 ID '{cid}' 同时存在于独立文件和内嵌配置中,内嵌配置优先")
262
+ constraint_files[cid] = const
263
+
264
+ # P0-3: 构建约束 ID -> 来源文件路径映射(相对 manifest 目录),供校验错误回溯。
265
+ # 独立约束取 manifest 引用路径;内嵌约束归属其宿主 schema 文件
266
+ # (内嵌约束 ID 形如 "{schema_id}_{item_id}",按 schema id 前缀归属,含下划线表名也正确)。
267
+ constraint_source_files: dict[str, str] = {ref.id: ref.path for ref in manifest.constraints}
268
+ schema_path_by_id = {ref.id: ref.path for ref in manifest.schemas}
269
+ for schema_id, schema_path in schema_path_by_id.items():
270
+ embedded_prefix = f"{schema_id}_"
271
+ for embedded_id in embedded_constraints:
272
+ if embedded_id.startswith(embedded_prefix):
273
+ # 内嵌优先与 constraint_files 覆盖语义一致
274
+ constraint_source_files[embedded_id] = schema_path
275
+
276
+ # 阶段 4:加载 Regex 节点文件
277
+ regex_files = _load_referenced_files(
278
+ project_root,
279
+ manifest.regex_nodes,
280
+ load_regex_node,
281
+ "Regex",
282
+ warnings,
283
+ loading_errors,
284
+ )
285
+
286
+ # 阶段 4b:加载 Transform 节点文件
287
+ transform_files = _load_referenced_files(
288
+ project_root,
289
+ manifest.transforms,
290
+ load_transform,
291
+ "Transform",
292
+ warnings,
293
+ loading_errors,
294
+ )
295
+
296
+ # 阶段 4b2:加载 ManualData 节点文件
297
+ manual_data_files = _load_referenced_files(
298
+ project_root,
299
+ manifest.manual_data,
300
+ load_manual_data_file,
301
+ "ManualData",
302
+ warnings,
303
+ loading_errors,
304
+ )
305
+
306
+ # 阶段 4c:加载模板定义并展开 template_instances
307
+ # 模板实例展开为带命名空间 ID 的 constraint/transform/regex/manual_data,合并进对应字典
308
+ template_files = _load_referenced_files(
309
+ project_root,
310
+ manifest.templates,
311
+ load_template,
312
+ "Template",
313
+ warnings,
314
+ loading_errors,
315
+ )
316
+ for instance in manifest.template_instances:
317
+ if not instance.enabled:
318
+ continue
319
+ tmpl = template_files.get(instance.template_id)
320
+ if tmpl is None:
321
+ loading_errors.append(
322
+ LoadingError(
323
+ error_type="TemplateInstanceMissingTemplate",
324
+ ref_id=instance.id,
325
+ **loading_error_messages.template_expansion_error(
326
+ instance.id,
327
+ ValueError(f"模板 '{instance.template_id}' 未找到"),
328
+ str(manifest_file),
329
+ ),
330
+ )
331
+ )
332
+ continue
333
+ try:
334
+ # 节点级错误收集:单节点展开失败不中断其余节点,但必须上报 loading error
335
+ # (否则该节点约束静默缺失,校验报告给出"全部通过"假阳性)
336
+ node_errors: list[dict[str, str]] = []
337
+ t_list, c_list, r_list, m_list = expand_template(
338
+ tmpl,
339
+ instance.id,
340
+ params=instance.params,
341
+ input_from_node=instance.input_from_node,
342
+ errors=node_errors,
343
+ )
344
+ for node_err in node_errors:
345
+ loading_errors.append(
346
+ LoadingError(
347
+ error_type="TemplateNodeExpansionError",
348
+ file_path=str(manifest_file),
349
+ ref_id=f"{instance.id}/{node_err.get('node_id', '?')}",
350
+ **loading_error_messages.template_node_expansion_error(
351
+ instance.id,
352
+ node_err.get("node_id", "?"),
353
+ node_err.get("message", ""),
354
+ str(manifest_file),
355
+ ),
356
+ )
357
+ )
358
+
359
+ # B05 修复:模板展开后 ID 冲突时记录警告(而非静默覆盖),
360
+ # 但保留覆盖行为以兼容历史项目(部分项目依赖模板实例间 ID 复用)。
361
+ # 记录为 warning 而非 blocker,避免阻断正常加载流程。
362
+ def _record_id_conflict(kind: str, conflicted_id: str) -> None:
363
+ warnings.append(
364
+ f"模板实例 '{instance.id}' 展开产生 ID 冲突:{kind} '{conflicted_id}' 已存在"
365
+ f"(可能来自项目的普通配置文件,也可能是此前展开的其他模板实例产物),"
366
+ f"当前实例的内容将覆盖已有定义。"
367
+ )
368
+
369
+ for tf in t_list:
370
+ if tf.id in transform_files:
371
+ _record_id_conflict("transform", tf.id)
372
+ transform_files[tf.id] = tf
373
+ for cf in c_list:
374
+ if cf.id in constraint_files:
375
+ _record_id_conflict("constraint", cf.id)
376
+ constraint_files[cf.id] = cf
377
+ for rf in r_list:
378
+ if rf.id in regex_files:
379
+ _record_id_conflict("regex", rf.id)
380
+ regex_files[rf.id] = rf
381
+ for mf in m_list:
382
+ if mf.id in manual_data_files:
383
+ _record_id_conflict("manual_data", mf.id)
384
+ manual_data_files[mf.id] = mf
385
+ except Exception as e:
386
+ loading_errors.append(
387
+ LoadingError(
388
+ error_type="TemplateExpansionError",
389
+ file_path=str(manifest_file),
390
+ ref_id=instance.id,
391
+ **loading_error_messages.template_expansion_error(instance.id, e, str(manifest_file)),
392
+ )
393
+ )
394
+
395
+ # 阶段 5:构建运行时数据结构
396
+ # 通过依赖注入的 schema_builder 完成 "core 文件对象 → domain 运行时对象" 的转换,
397
+ # 避免 core 层反向依赖 services 层。调用方(services)传入构建器;
398
+ # 未注入时 dataset_schema 保持为 None,由调用方自行构建。
399
+ if schema_builder is not None:
400
+ dataset_schema, constraint_warnings = schema_builder(schema_files, constraint_files, registries)
401
+ warnings.extend(constraint_warnings)
402
+ else:
403
+ dataset_schema = None
404
+
405
+ # 阶段 6:配置格式自检(ID 一致性、引用完整性等)
406
+ from app.shared.core.project.loader.loader_parts.config_inspector import inspect_config
407
+
408
+ inspect_config(
409
+ manifest_file,
410
+ manifest,
411
+ schema_files,
412
+ constraint_files,
413
+ regex_files,
414
+ transform_files,
415
+ manual_data_files,
416
+ warnings,
417
+ loading_errors,
418
+ )
419
+
420
+ return LoadedProject(
421
+ manifest_path=manifest_file,
422
+ manifest=manifest,
423
+ schema_files=schema_files,
424
+ constraint_files=constraint_files,
425
+ regex_node_files=regex_files,
426
+ transform_files=transform_files,
427
+ manual_data_files=manual_data_files,
428
+ dataset_schema=dataset_schema,
429
+ warnings=warnings,
430
+ loading_errors=loading_errors,
431
+ constraint_source_files=constraint_source_files,
432
+ )
@@ -0,0 +1,117 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ #
3
+ # Copyright 2026 Precis Team
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ """
17
+ @fileoverview 路径验证模块
18
+
19
+ 功能概述:
20
+ - 验证文件路径是否在项目目录内
21
+ - 防止目录遍历攻击 (path traversal attack)
22
+ - 确保安全访问文件系统
23
+
24
+ 架构设计:
25
+ - 路径解析: 使用 Path.resolve() 获取绝对路径
26
+ - 范围检查: 确保解析后的路径以项目根目录开头
27
+
28
+ 输入示例:
29
+ project_root = Path("/path/to/project")
30
+ file_path = Path("/path/to/project/schemas/users.yaml")
31
+ ref_name = "Schema 'users'"
32
+
33
+ 输出示例:
34
+ # 验证通过: 返回 None (无返回值)
35
+ validate_path_inside_project(project_root, file_path, ref_name)
36
+ # 结果: 通过验证,函数正常返回
37
+
38
+ # 验证失败: 抛出 ValueError
39
+ validate_path_inside_project(
40
+ Path("/path/to/project"),
41
+ Path("/path/to/project/../etc/passwd"),
42
+ "文件"
43
+ )
44
+ # 结果: 抛出 ValueError("文件路径 '/path/to/project/../etc/passwd' 超出项目根目录范围")
45
+
46
+ 原理说明:
47
+ 为什么需要路径验证?
48
+ - 防止用户通过路径穿越 (如 ../../../etc/passwd) 访问项目外的敏感文件
49
+ - 确保所有文件操作都在项目目录内
50
+
51
+ 验证逻辑:
52
+ 1. 解析 file_path 和 project_root 为绝对路径
53
+ 2. 检查 file_path 是否以 project_root 开头
54
+ 3. 特殊处理: file_path == project_root 允许 (项目根目录本身)
55
+
56
+ 可能的攻击方式:
57
+ - /path/to/project/../../../etc/passwd
58
+ - /path/to/project/./../etc/passwd
59
+ - /path/to/project/./schemas/../../../etc/passwd
60
+
61
+ 防御机制:
62
+ - resolve() 会解析 . 和 ..,将路径转换为标准形式
63
+ - 然后检查是否在项目目录范围内
64
+ """
65
+
66
+ from __future__ import annotations
67
+
68
+ from pathlib import Path
69
+
70
+
71
+ def validate_path_inside_project(project_root: Path, file_path: Path, ref_name: str = "文件") -> None:
72
+ """@methoddesc 验证文件路径是否在项目目录内
73
+
74
+ 验证目标:
75
+ - 确保 file_path 不会超出 project_root 范围
76
+ - 防止目录遍历攻击
77
+
78
+ 输入示例:
79
+ # 合法路径
80
+ project_root = Path("/path/to/project")
81
+ file_path = Path("/path/to/project/schemas/users.yaml")
82
+ ref_name = "Schema 'users'"
83
+
84
+ # 非法路径 - 目录遍历
85
+ project_root = Path("/path/to/project")
86
+ file_path = Path("/path/to/project/../other_project/data.yaml")
87
+ ref_name = "数据文件"
88
+
89
+ 输出示例 (合法):
90
+ # 返回 None,验证通过
91
+ None
92
+
93
+ 输出示例 (非法):
94
+ # 抛出 ValueError
95
+ ValueError: "数据文件路径 '/path/to/project/../other_project/data.yaml' 超出项目根目录范围"
96
+
97
+ 异常情况:
98
+ - ValueError: 路径超出项目范围
99
+ - ValueError: 路径无效 (如包含非法字符)
100
+ """
101
+ try:
102
+ # 将传入的路径解析为绝对路径(自动处理 . 和 ..)
103
+ resolved = file_path.resolve()
104
+ project_resolved = project_root.resolve()
105
+ resolved_str = str(resolved)
106
+ project_resolved_str = str(project_resolved)
107
+
108
+ # 检查解析后的路径是否以项目根目录开头
109
+ # 允许文件路径等于项目根目录本身(特殊情况)
110
+ if not resolved_str.startswith(project_resolved_str + str(Path("/"))) and resolved_str != project_resolved_str:
111
+ raise ValueError(f"{ref_name}路径 '{file_path}' 超出项目根目录范围")
112
+ except ValueError:
113
+ # 重新抛出 ValueError,保持原始错误信息
114
+ raise
115
+ except Exception as e:
116
+ # 捕获其他异常(如路径格式错误),包装为 ValueError 抛出
117
+ raise ValueError(f"{ref_name}路径 '{file_path}' 无效: {e}")
@@ -0,0 +1,125 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ #
3
+ # Copyright 2026 Precis Team
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ """
17
+ @fileoverview 运行时数据结构构建辅助模块
18
+
19
+ 功能概述:
20
+ - 构建 patterns 注册表
21
+ - 将 ConstraintFile 批量转换为运行时约束对象的薄包装
22
+
23
+ 架构设计:
24
+ - 类型转换层: config -> domain object
25
+ - 列类型 / DataSetSchema 的转换已迁移至 services 层,避免 core 反向依赖 domain
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ from pathlib import Path
31
+ from typing import Any
32
+
33
+ from app.shared.core.patterns.loader import load_patterns_from_config
34
+ from app.shared.core.project.constraint.types import ConstraintFile
35
+ from app.shared.core.project.manifest.types import ProjectManifest
36
+ from app.shared.core.project.schema.types import TableSchemaFile
37
+
38
+ from .path_validation import validate_path_inside_project
39
+
40
+
41
+ def build_runtime_constraints(
42
+ constraint_files: dict[str, ConstraintFile], schema_files: dict[str, TableSchemaFile]
43
+ ) -> tuple[list[Any], list[str]]:
44
+ """@methoddesc 构建运行时约束对象(代理函数)
45
+
46
+ 该函数是约束工厂模块 create_constraints 的薄包装,
47
+ 负责将 ConstraintFile 配置批量转换为运行时约束实例。
48
+
49
+ 输入示例:
50
+ constraint_files = {
51
+ "unique_email": ConstraintFile(
52
+ id="unique_email",
53
+ type="Unique",
54
+ refs={"table_id": "users", "column_ids": ["email"]}
55
+ )
56
+ }
57
+ schema_files = {"users": TableSchemaFile(...)}
58
+
59
+ 输出示例:
60
+ ([UniqueConstraint(...)], []) # (约束实例列表, 警告列表)
61
+ """
62
+ # 导入并委托给 constraint.factory 完成实际的约束实例化
63
+ from app.shared.core.project.constraint.factory import create_constraints
64
+
65
+ return create_constraints(constraint_files, schema_files)
66
+
67
+
68
+ # ============================================================================
69
+ # Patterns 注册表构建(原 registries.py,合并至此以减少碎片化文件)
70
+ # ============================================================================
71
+
72
+
73
+ def build_registries(project_root: Path, manifest: ProjectManifest) -> dict[str, Any]:
74
+ """@methoddesc 构建 Patterns 注册表
75
+
76
+ 从 manifest.patterns_dir 指定的目录加载表达式模式。
77
+
78
+ 输入示例:
79
+ project_root = Path("/path/to/project")
80
+ manifest = Manifest(
81
+ version="2",
82
+ patterns_dir="patterns",
83
+ schemas=[...],
84
+ constraints=[...]
85
+ )
86
+
87
+ # patterns 目录内容:
88
+ # patterns/
89
+ # └── math.yaml
90
+ # ---
91
+ # - name: add
92
+ # pattern: "{a} + {b}"
93
+ # returns: integer
94
+
95
+ 输出示例:
96
+ registries = {
97
+ "expression_registry": ExpressionRegistry(
98
+ patterns={
99
+ "math": [
100
+ Pattern(name="add", pattern="{a} + {b}", returns="integer")
101
+ ]
102
+ }
103
+ )
104
+ }
105
+
106
+ 原理说明:
107
+ - load_patterns_from_config() 会递归扫描 patterns_dir 下所有 .yaml 文件
108
+ - 每个文件对应一个命名空间 (namespace)
109
+ - 表达式在验证时可以通过 namespace.name 引用
110
+ """
111
+ # 根据 manifest 中配置的 patterns_dir 计算绝对路径
112
+ patterns_dir = project_root / manifest.patterns_dir
113
+ registries: dict[str, Any] = {}
114
+ # 验证 patterns 目录是否在项目根目录范围内,防止目录遍历
115
+ try:
116
+ validate_path_inside_project(project_root, patterns_dir, "patterns 目录")
117
+ except ValueError:
118
+ # 如果路径验证失败,返回空的表达式注册表,避免阻塞后续加载
119
+ return {"expression_registry": None}
120
+ # 目录存在时加载所有模式文件;不存在则返回 None
121
+ if patterns_dir.exists():
122
+ registries["expression_registry"] = load_patterns_from_config(str(patterns_dir))
123
+ else:
124
+ registries["expression_registry"] = None
125
+ return registries