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,358 @@
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 Regex CRUD API - 正则表达式节点管理
18
+
19
+ 功能概述:
20
+ - 提供 Regex Node(正则表达式校验/提取节点)的增删改查
21
+ - 支持两层查找:优先 manifest 引用,兼容扫描 patterns/、regex/、regex_nodes/ 目录
22
+ - 包含展示名更新能力
23
+
24
+ 架构设计:
25
+ - 读取时优先从 manifest.regex_nodes 定位文件,失败后扫描多个目录
26
+ - 写入时自动补齐 manifest 引用,默认路径为 regex/{regex_id}.regex.yaml
27
+ - 删除时同步清理 manifest 引用与物理文件
28
+
29
+ 输入示例:
30
+ GET /regex/phone_number
31
+ PUT /regex/phone_number (body: RegexNodeFileV2)
32
+
33
+ 输出示例:
34
+ RegexNodeFileV2 模型实例或 StandardResponse 操作结果
35
+ """
36
+
37
+ import logging
38
+ import os
39
+ from pathlib import Path
40
+
41
+ from fastapi import APIRouter, Depends, HTTPException
42
+
43
+ logger = logging.getLogger(__name__)
44
+
45
+ from app.api.dependencies import get_project_config_path
46
+ from app.shared.core.io.yaml import read_yaml, write_yaml_atomic
47
+ from app.shared.core.project.manifest.types import ProjectManifestV2
48
+
49
+ from .base import (
50
+ DisplayNameUpdateRequest,
51
+ RegexNodeFileV2,
52
+ RegexNodeRefV2,
53
+ StandardResponse,
54
+ _scan_regex_node_file,
55
+ _v2_manifest_path,
56
+ )
57
+ from .helpers import _resolve_project_path, project_lock
58
+ from .manifest import get_v2_manifest
59
+
60
+ router = APIRouter(prefix="", tags=["Project-Regex"])
61
+
62
+
63
+ @router.get(
64
+ "/regex/{regex_id}",
65
+ response_model=RegexNodeFileV2,
66
+ summary="读取 Regex 节点",
67
+ responses={
68
+ 404: {"description": "Regex 节点不存在"},
69
+ 500: {"description": "服务器内部错误"},
70
+ },
71
+ )
72
+ def get_v2_regex_node(regex_id: str, config_path: str = Depends(get_project_config_path)) -> RegexNodeFileV2:
73
+ """
74
+ 读取指定 regex_id 的 regex 节点文件。
75
+
76
+ 查找策略(两层查找,更健壮):
77
+ 1. 从 manifest.regex_nodes 中查找引用(优先)
78
+ 2. 如果未找到,扫描 patterns_dir 目录
79
+
80
+ 参数:
81
+ regex_id: Regex 节点的唯一标识符
82
+ config_path: 项目配置根目录(通过 Depends 注入)
83
+
84
+ 返回:
85
+ RegexNodeFileV2: Regex 节点文件内容
86
+ """
87
+ manifest = get_v2_manifest(config_path)
88
+
89
+ abs_path = None
90
+
91
+ # 1. 优先从 manifest 中查找所有匹配的引用(可能存在重复 ID 的情况)
92
+ refs = [r for r in manifest.regex_nodes if r.id == regex_id]
93
+ for ref in refs:
94
+ path = os.path.join(config_path, ref.path)
95
+ if os.path.isfile(path):
96
+ abs_path = path
97
+ break
98
+
99
+ # 2. 如果 manifest 中未找到有效文件,尝试扫描目录
100
+ if not abs_path:
101
+ scan_dirs = []
102
+
103
+ p_dir = os.path.join(config_path, manifest.patterns_dir or "patterns")
104
+ scan_dirs.append((p_dir, "patterns"))
105
+
106
+ # B13 修复:registry 名应为 "regex",使按 regex/{id} 请求能命中。
107
+ # 过去误写为 "patterns" 导致目录扫描命中的 id 形如 "patterns/xxx",按 regex/{id} 请求失败。
108
+ scan_dirs.append((os.path.join(config_path, "regex"), "regex"))
109
+ scan_dirs.append((os.path.join(config_path, "regex_nodes"), "regex"))
110
+
111
+ found = False
112
+ for d, reg_name in scan_dirs:
113
+ if not os.path.isdir(d):
114
+ continue
115
+ for filename in os.listdir(d):
116
+ if filename.lower().endswith(".yaml") or filename.lower().endswith(".regex.yaml"):
117
+ rid = filename
118
+ if filename.lower().endswith(".regex.yaml"):
119
+ rid = filename[:-11]
120
+ elif filename.lower().endswith(".yaml"):
121
+ rid = filename[:-5]
122
+
123
+ match_id = rid if rid == regex_id else f"{reg_name}/{rid}"
124
+
125
+ if match_id == regex_id:
126
+ path = os.path.join(d, filename)
127
+ if os.path.isfile(path):
128
+ abs_path = path
129
+ found = True
130
+ break
131
+ if found:
132
+ break
133
+
134
+ if not abs_path:
135
+ raise HTTPException(status_code=404, detail=f"未找到这条正则规则,可能已被删除(ID: {regex_id})")
136
+
137
+ if not os.path.isfile(abs_path):
138
+ raise HTTPException(
139
+ status_code=404, detail="该正则规则在清单中登记的配置文件已不存在,请删除这条规则后重新创建"
140
+ )
141
+
142
+ data = read_yaml(Path(abs_path))
143
+
144
+ version = data.get("version", 1)
145
+ if version != 2:
146
+ raise HTTPException(status_code=400, detail=f"不支持的 regex 文件版本: {version},仅支持 V2 格式")
147
+
148
+ return RegexNodeFileV2.model_validate(data)
149
+
150
+
151
+ @router.put(
152
+ "/regex/{regex_id}",
153
+ response_model=StandardResponse,
154
+ summary="写入 Regex 节点",
155
+ responses={
156
+ 400: {"description": "请求参数错误"},
157
+ 404: {"description": "Manifest 不存在"},
158
+ 500: {"description": "服务器内部错误"},
159
+ },
160
+ )
161
+ def put_v2_regex_node(
162
+ regex_id: str,
163
+ regex_node: RegexNodeFileV2,
164
+ config_path: str = Depends(get_project_config_path),
165
+ ) -> dict[str, str]:
166
+ """
167
+ 写入指定 regex_id 的 regex 节点文件。
168
+
169
+ 业务逻辑:
170
+ - 验证 regex_node.id 与路径参数 regex_id 一致
171
+ - 如果 manifest 未引用,自动补齐引用
172
+ - 同时更新 manifest 文件
173
+
174
+ 副作用:
175
+ - 创建或覆盖 regex 文件
176
+ - 更新 manifest 中的引用列表
177
+
178
+ 数据流:
179
+ 1. 验证 regex_node.id 与 regex_id 一致
180
+ 2. 读取 manifest 文件
181
+ 3. 检查 manifest 是否已有该 regex 的引用
182
+ 4. 如果没有,添加新的引用(默认路径:regex/{regex_id}.regex.yaml)
183
+ 5. 写入 regex 文件
184
+ 6. 更新 manifest 文件
185
+
186
+ 参数:
187
+ regex_id: Regex 节点的唯一标识符
188
+ regex_node: Regex 节点数据
189
+ config_path: 项目配置根目录(通过 Depends 注入)
190
+
191
+ 返回:
192
+ StandardResponse: 操作结果消息
193
+ """
194
+ if regex_node.id != regex_id:
195
+ raise HTTPException(status_code=400, detail="请求中的正则规则 ID 与地址栏不一致,请刷新页面后重试")
196
+ manifest_path = _v2_manifest_path(config_path)
197
+ if not os.path.isfile(manifest_path):
198
+ raise HTTPException(
199
+ status_code=404,
200
+ detail={
201
+ # §2.1: 结构化错误码统一(与 dependencies.py 的 manifest 缺失出口同构),
202
+ # 前端按 detail.code 识别项目失效并触发自愈
203
+ "code": "PROJECT_NOT_FOUND",
204
+ "message": f"项目配置文件(project.precis.yaml)不存在,请确认项目是否已被移动或删除(配置目录: {manifest_path})",
205
+ "path": str(
206
+ config_path
207
+ ), # §2.1: 项目根目录(前端与 activeProjectPath 同口径比对,勿传 manifest 文件路径)
208
+ },
209
+ )
210
+ manifest = ProjectManifestV2.model_validate(read_yaml(Path(manifest_path)))
211
+
212
+ ref = next((r for r in manifest.regex_nodes if r.id == regex_id), None)
213
+ if not ref:
214
+ default_rel = f"regex/{regex_id}.regex.yaml"
215
+ manifest.regex_nodes.append(RegexNodeRefV2(id=regex_id, path=default_rel))
216
+ ref_path = default_rel
217
+ else:
218
+ ref_path = ref.path
219
+
220
+ try:
221
+ abs_path = _resolve_project_path(config_path, ref_path)
222
+ except ValueError:
223
+ raise HTTPException(status_code=400, detail="非法的 Regex 文件路径")
224
+ with project_lock(config_path):
225
+ write_yaml_atomic(Path(abs_path), regex_node.model_dump(exclude_none=True))
226
+ write_yaml_atomic(Path(manifest_path), manifest.model_dump(exclude_none=True))
227
+ return {"message": f"V2 regex_node '{regex_id}' 已保存。"}
228
+
229
+
230
+ @router.delete(
231
+ "/regex/{regex_id}",
232
+ response_model=StandardResponse,
233
+ summary="删除 Regex 节点",
234
+ responses={
235
+ 400: {"description": "请求参数错误"},
236
+ 404: {"description": "Regex 节点或 Manifest 不存在"},
237
+ 500: {"description": "服务器内部错误"},
238
+ },
239
+ )
240
+ def delete_v2_regex_node(regex_id: str, config_path: str = Depends(get_project_config_path)) -> dict[str, str]:
241
+ """
242
+ 删除指定 regex_id 的 regex 文件,并从 manifest 中移除引用。
243
+
244
+ 删除策略(两层查找,更健壮):
245
+ 1. 首先从 manifest.regex_nodes 中查找引用(优先)
246
+ 2. 如果未找到,扫描 patterns/ 目录(兼容旧项目)
247
+
248
+ 副作用:
249
+ - 删除对应的 .yaml 文件
250
+ - 从 manifest.regex_nodes 中移除引用(如果存在)
251
+
252
+ 数据流:
253
+ 1. 读取 manifest 文件
254
+ 2. 在 manifest.regex_nodes 中查找引用
255
+ 3. 如果未找到,扫描 patterns/ 目录
256
+ 4. 删除找到的 regex 文件
257
+ 5. 从 manifest 中移除引用
258
+ 6. 保存更新后的 manifest
259
+
260
+ 参数:
261
+ regex_id: Regex 节点的唯一标识符
262
+ config_path: 项目配置根目录(通过 Depends 注入)
263
+
264
+ 返回:
265
+ StandardResponse: 操作结果消息
266
+ """
267
+ manifest_path = _v2_manifest_path(config_path)
268
+ if not os.path.isfile(manifest_path):
269
+ raise HTTPException(
270
+ status_code=404,
271
+ detail={
272
+ # §2.1: 结构化错误码统一(与 dependencies.py 的 manifest 缺失出口同构),
273
+ # 前端按 detail.code 识别项目失效并触发自愈
274
+ "code": "PROJECT_NOT_FOUND",
275
+ "message": f"项目配置文件(project.precis.yaml)不存在,请确认项目是否已被移动或删除(配置目录: {manifest_path})",
276
+ "path": str(
277
+ config_path
278
+ ), # §2.1: 项目根目录(前端与 activeProjectPath 同口径比对,勿传 manifest 文件路径)
279
+ },
280
+ )
281
+ manifest = ProjectManifestV2.model_validate(read_yaml(Path(manifest_path)))
282
+
283
+ ref = next((r for r in manifest.regex_nodes if r.id == regex_id), None)
284
+ if not ref:
285
+ ref = _scan_regex_node_file(
286
+ regex_id,
287
+ config_path,
288
+ manifest.patterns_dir or "patterns",
289
+ "regex",
290
+ )
291
+
292
+ if not ref:
293
+ return {"message": f"未找到 regex_node '{regex_id}',无需删除。"}
294
+
295
+ try:
296
+ abs_path = _resolve_project_path(config_path, ref.path)
297
+ except ValueError:
298
+ raise HTTPException(status_code=400, detail="非法的 Regex 文件路径")
299
+
300
+ with project_lock(config_path):
301
+ try:
302
+ if os.path.isfile(abs_path):
303
+ os.remove(abs_path)
304
+ except Exception as e:
305
+ raise HTTPException(status_code=500, detail=f"删除 regex_node 文件失败: {e}")
306
+
307
+ # B-fix: 该引用原本存在于 manifest 中就写回(即使删除后列表为空)。
308
+ # 此前 `if manifest.regex_nodes:` 在删除最后一个引用时为假而跳过写回,
309
+ # manifest 会留下指向已删除文件的悬空引用。
310
+ remaining_regex_nodes = [r for r in manifest.regex_nodes if r.id != regex_id]
311
+ if len(remaining_regex_nodes) != len(manifest.regex_nodes):
312
+ manifest.regex_nodes = remaining_regex_nodes
313
+ write_yaml_atomic(Path(manifest_path), manifest.model_dump(exclude_none=True))
314
+
315
+ return {"message": f"V2 regex_node '{regex_id}' 已删除。"}
316
+
317
+
318
+ @router.post(
319
+ "/regex/{regex_id}/display-name",
320
+ response_model=StandardResponse,
321
+ summary="更新 Regex 节点展示名",
322
+ responses={
323
+ 404: {"description": "Regex 节点不存在"},
324
+ 500: {"description": "服务器内部错误"},
325
+ },
326
+ )
327
+ def update_v2_regex_node_display_name(
328
+ regex_id: str,
329
+ payload: DisplayNameUpdateRequest,
330
+ config_path: str = Depends(get_project_config_path),
331
+ ) -> dict[str, str]:
332
+ """
333
+ 更新 regex 节点展示名(不改变 regex_id 与文件路径)。
334
+
335
+ 设计意图:
336
+ - 允许用户给 Regex 节点设置友好的显示名称
337
+ - 不改变底层 ID 和文件路径,保持引用完整性
338
+
339
+ 副作用:
340
+ - 会触发两次文件写入(读取-修改-保存)
341
+
342
+ 数据流:
343
+ 1. 调用 get_v2_regex_node 获取当前 regex_node
344
+ 2. 修改 regex_node.name 为新名称
345
+ 3. 调用 put_v2_regex_node 保存更改
346
+
347
+ 参数:
348
+ regex_id: Regex 节点的唯一标识符
349
+ payload: 新的展示名称
350
+ config_path: 项目配置根目录(通过 Depends 注入)
351
+
352
+ 返回:
353
+ StandardResponse: 操作结果消息
354
+ """
355
+ regex_node = get_v2_regex_node(regex_id, config_path)
356
+ regex_node.name = payload.name
357
+ put_v2_regex_node(regex_id, regex_node, config_path)
358
+ return {"message": f"V2 regex_node '{regex_id}' 展示名已更新。"}
@@ -0,0 +1,157 @@
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
+ - 扫描 constraints 目录查找指定 ID 的 constraint 引用
21
+ - 扫描 schemas 目录查找指定 ID 的 schema 引用
22
+ - 扫描 patterns 和 regex 目录查找指定 ID 的 regex_node 引用
23
+
24
+ 架构设计:
25
+ - 纯 Python 实现,不依赖 FastAPI,便于单元测试
26
+ - 两层查找策略:先检查目录存在性,再遍历匹配文件名
27
+ - 兼容 manifest 资源列表为空或目录不存在的边界情况
28
+ - 文件名匹配时自动提取 ID(去掉扩展名后缀)
29
+
30
+ 输入示例:
31
+ ref = _scan_schema_file("users", "/project/root")
32
+
33
+ 输出示例:
34
+ SchemaRefV2(id="users", path="schemas/users.schema.yaml")
35
+ """
36
+
37
+ import os
38
+
39
+ from app.shared.core.project.manifest.types import (
40
+ ConstraintRefV2,
41
+ RegexNodeRefV2,
42
+ SchemaRefV2,
43
+ )
44
+
45
+
46
+ def _scan_constraint_file(constraint_id: str, config_path: str) -> ConstraintRefV2 | None:
47
+ """
48
+ 扫描 constraints 目录查找指定 ID 的 constraint 引用。
49
+
50
+ 扫描逻辑(两层查找,更健壮):
51
+ 1. 检查 constraints/ 目录是否存在
52
+ 2. 遍历目录下所有 .constraint.yaml 文件(大小写不敏感)
53
+ 3. 从文件名中提取 constraint_id(去掉 .constraint.yaml 后缀)
54
+ 4. 如果找到匹配则返回 ConstraintRefV2 对象
55
+
56
+ 兼容的边界情况:
57
+ - manifest.constraints 为空的情况(部分项目可能未更新 manifest)
58
+ - 文件名大小写不敏感的匹配
59
+ - constraints 目录不存在的情况
60
+
61
+ 参数:
62
+ constraint_id: 要查找的 constraint ID
63
+ config_path: 项目配置根目录
64
+
65
+ 返回:
66
+ 找到的 ConstraintRefV2 对象,未找到返回 None
67
+ """
68
+ constraints_dir = os.path.join(config_path, "constraints")
69
+ if not os.path.isdir(constraints_dir):
70
+ return None
71
+
72
+ for filename in os.listdir(constraints_dir):
73
+ if filename.lower().endswith(".constraint.yaml"):
74
+ scanned_id = filename[:-16]
75
+ if scanned_id == constraint_id:
76
+ return ConstraintRefV2(id=constraint_id, path=f"constraints/{filename}")
77
+ return None
78
+
79
+
80
+ def _scan_schema_file(table_id: str, config_path: str) -> SchemaRefV2 | None:
81
+ """
82
+ 扫描 schemas 目录查找指定 ID 的 schema 引用。
83
+
84
+ 扫描逻辑:
85
+ 1. 检查 schemas/ 目录是否存在
86
+ 2. 遍历目录下所有 .schema.yaml 文件
87
+ 3. 从文件名中提取 table_id(去掉 .schema.yaml 后缀)
88
+ 4. 如果找到匹配则返回 SchemaRefV2 对象
89
+
90
+ 兼容的边界情况:
91
+ - manifest.schemas 为空的情况
92
+ - schemas 目录不存在的情况
93
+
94
+ 参数:
95
+ table_id: 要查找的 schema table ID
96
+ config_path: 项目配置根目录
97
+
98
+ 返回:
99
+ 找到的 SchemaRefV2 对象,未找到返回 None
100
+ """
101
+ schemas_dir = os.path.join(config_path, "schemas")
102
+ if not os.path.isdir(schemas_dir):
103
+ return None
104
+
105
+ for filename in os.listdir(schemas_dir):
106
+ if filename.endswith(".schema.yaml"):
107
+ scanned_id = filename[:-12]
108
+ if scanned_id == table_id:
109
+ return SchemaRefV2(id=table_id, path=f"schemas/{filename}")
110
+ return None
111
+
112
+
113
+ def _scan_regex_node_file(
114
+ regex_id: str,
115
+ config_path: str,
116
+ patterns_dir: str = "patterns",
117
+ regex_dir: str = "regex",
118
+ ) -> RegexNodeRefV2 | None:
119
+ """
120
+ 扫描 patterns 和 regex 目录查找指定 ID 的 regex_node 引用。
121
+
122
+ 扫描逻辑:
123
+ 1. 依次检查 regex_dir 和 patterns_dir 目录
124
+ 2. 遍历目录下的 .yaml 文件
125
+ 3. 从文件名中提取 regex_id(去掉扩展名后缀)
126
+ 4. 如果找到匹配则返回 RegexNodeRefV2 对象
127
+
128
+ 兼容的边界情况:
129
+ - manifest.regex_nodes 为空的情况
130
+ - patterns_dir 目录不存在的情况
131
+ - regex_node 可能位于任一目录中
132
+
133
+ 参数:
134
+ regex_id: 要查找的 regex node ID
135
+ config_path: 项目配置根目录
136
+ patterns_dir: patterns 目录名(默认 "patterns")
137
+ regex_dir: regex 目录名(默认 "regex")
138
+
139
+ 返回:
140
+ 找到的 RegexNodeRefV2 对象,未找到返回 None
141
+ """
142
+ regex_dir_path = os.path.join(config_path, regex_dir)
143
+ if os.path.isdir(regex_dir_path):
144
+ for filename in os.listdir(regex_dir_path):
145
+ if filename.endswith(".regex.yaml"):
146
+ scanned_id = filename[:-11]
147
+ if scanned_id == regex_id:
148
+ return RegexNodeRefV2(id=regex_id, path=f"{regex_dir}/{filename}")
149
+
150
+ dir_path = os.path.join(config_path, patterns_dir)
151
+ if os.path.isdir(dir_path):
152
+ for filename in os.listdir(dir_path):
153
+ if filename.endswith(".yaml"):
154
+ scanned_id = filename[:-5]
155
+ if scanned_id == regex_id:
156
+ return RegexNodeRefV2(id=regex_id, path=f"{patterns_dir}/{filename}")
157
+ return None