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.
- app/__init__.py +16 -0
- app/api/__init__.py +32 -0
- app/api/dependencies.py +185 -0
- app/api/main.py +301 -0
- app/api/middleware/__init__.py +34 -0
- app/api/middleware/exception_handler.py +126 -0
- app/api/middleware/request_logging.py +97 -0
- app/api/middleware/token_auth.py +215 -0
- app/api/models/__init__.py +151 -0
- app/api/models/connection_rules.py +98 -0
- app/api/models/files.py +99 -0
- app/api/models/full_validation.py +554 -0
- app/api/models/project.py +117 -0
- app/api/models/projects.py +80 -0
- app/api/models/schema.py +121 -0
- app/api/models/v2_responses.py +46 -0
- app/api/models/validation.py +303 -0
- app/api/models/workspace.py +118 -0
- app/api/routers/__init__.py +56 -0
- app/api/routers/ai/__init__.py +36 -0
- app/api/routers/ai/chat.py +243 -0
- app/api/routers/ai/generate.py +156 -0
- app/api/routers/ai/hardware.py +199 -0
- app/api/routers/ai/jobs.py +544 -0
- app/api/routers/ai/migrate.py +471 -0
- app/api/routers/ai/models.py +352 -0
- app/api/routers/ai/ollama.py +108 -0
- app/api/routers/ai/providers.py +491 -0
- app/api/routers/ai/router.py +44 -0
- app/api/routers/ai/stream.py +281 -0
- app/api/routers/ai/utils.py +182 -0
- app/api/routers/core/__init__.py +34 -0
- app/api/routers/core/connection_rules.py +188 -0
- app/api/routers/core/data_sources.py +374 -0
- app/api/routers/core/regex.py +333 -0
- app/api/routers/core/reporting.py +263 -0
- app/api/routers/files/__init__.py +24 -0
- app/api/routers/files/ops.py +179 -0
- app/api/routers/files/transfer.py +101 -0
- app/api/routers/preview/__init__.py +35 -0
- app/api/routers/preview/content_mode.py +264 -0
- app/api/routers/preview/header_row.py +154 -0
- app/api/routers/preview/models.py +81 -0
- app/api/routers/preview/path_mode.py +440 -0
- app/api/routers/preview/router.py +30 -0
- app/api/routers/project/__init__.py +61 -0
- app/api/routers/project/base.py +111 -0
- app/api/routers/project/constraint.py +345 -0
- app/api/routers/project/full_config.py +411 -0
- app/api/routers/project/full_config_writer.py +327 -0
- app/api/routers/project/helpers.py +167 -0
- app/api/routers/project/inspection_fix.py +330 -0
- app/api/routers/project/manifest.py +605 -0
- app/api/routers/project/models.py +177 -0
- app/api/routers/project/pattern.py +268 -0
- app/api/routers/project/regex.py +358 -0
- app/api/routers/project/scanner.py +157 -0
- app/api/routers/project/schema.py +567 -0
- app/api/routers/project/schema_helpers.py +149 -0
- app/api/routers/project/settings.py +399 -0
- app/api/routers/project/template.py +325 -0
- app/api/routers/project/validation.py +227 -0
- app/api/routers/project/view.py +177 -0
- app/api/routers/project/workspaces.py +167 -0
- app/api/routers/projects/__init__.py +25 -0
- app/api/routers/projects/create.py +121 -0
- app/api/routers/projects/open.py +103 -0
- app/api/routers/projects/scan.py +100 -0
- app/api/routers/validation/__init__.py +39 -0
- app/api/routers/validation/common.py +263 -0
- app/api/routers/validation/content_mode.py +401 -0
- app/api/routers/validation/history.py +235 -0
- app/api/routers/validation/inline_mode.py +196 -0
- app/api/routers/validation/path_mode.py +314 -0
- app/api/routers/validation/router.py +51 -0
- app/api/services/full_validation_response_builder.py +412 -0
- app/api/services/io_error_messages.py +41 -0
- app/api/services/preview_service.py +248 -0
- app/cli/__init__.py +18 -0
- app/cli/__main__.py +30 -0
- app/cli/shared_services/__init__.py +42 -0
- app/cli/shared_services/config_ops.py +581 -0
- app/cli/shared_services/generation_ops.py +160 -0
- app/cli/shared_services/project_ops.py +288 -0
- app/cli/shell/__init__.py +48 -0
- app/cli/shell/commands/__init__.py +63 -0
- app/cli/shell/commands/ai/__init__.py +260 -0
- app/cli/shell/commands/ai/chat.py +331 -0
- app/cli/shell/commands/ai/delete.py +165 -0
- app/cli/shell/commands/ai/diff.py +70 -0
- app/cli/shell/commands/ai/display.py +428 -0
- app/cli/shell/commands/ai/executor.py +225 -0
- app/cli/shell/commands/ai/executor_utils.py +271 -0
- app/cli/shell/commands/ai/generate.py +285 -0
- app/cli/shell/commands/ai/interaction.py +262 -0
- app/cli/shell/commands/ai/migrate.py +281 -0
- app/cli/shell/commands/ai/resolver.py +224 -0
- app/cli/shell/commands/ai/status.py +101 -0
- app/cli/shell/commands/ai/switch.py +158 -0
- app/cli/shell/commands/ai/utils.py +52 -0
- app/cli/shell/commands/base.py +343 -0
- app/cli/shell/commands/config/__init__.py +131 -0
- app/cli/shell/commands/config/base.py +80 -0
- app/cli/shell/commands/config/check.py +246 -0
- app/cli/shell/commands/config/edit.py +148 -0
- app/cli/shell/commands/config/get.py +111 -0
- app/cli/shell/commands/config/init.py +123 -0
- app/cli/shell/commands/config/inspect.py +164 -0
- app/cli/shell/commands/config/list.py +104 -0
- app/cli/shell/commands/config/set.py +119 -0
- app/cli/shell/commands/config/show.py +150 -0
- app/cli/shell/commands/exit.py +67 -0
- app/cli/shell/commands/help.py +104 -0
- app/cli/shell/commands/infer_schema.py +156 -0
- app/cli/shell/commands/open.py +201 -0
- app/cli/shell/commands/project.py +215 -0
- app/cli/shell/commands/provider.py +619 -0
- app/cli/shell/commands/system.py +170 -0
- app/cli/shell/commands/validate.py +455 -0
- app/cli/shell/completer.py +231 -0
- app/cli/shell/config_storage.py +191 -0
- app/cli/shell/exceptions.py +90 -0
- app/cli/shell/formatter.py +371 -0
- app/cli/shell/interactive_menu.py +348 -0
- app/cli/shell/main.py +286 -0
- app/cli/shell/parser.py +266 -0
- app/cli/start.py +180 -0
- app/cli_main.py +52 -0
- app/mcp_server.py +311 -0
- app/shared/core/__init__.py +18 -0
- app/shared/core/app_version.py +59 -0
- app/shared/core/config/__init__.py +119 -0
- app/shared/core/config/server.py +193 -0
- app/shared/core/data_source/__init__.py +106 -0
- app/shared/core/data_source/loader.py +348 -0
- app/shared/core/data_source/loaders/__init__.py +213 -0
- app/shared/core/data_source/loaders/base.py +215 -0
- app/shared/core/data_source/loaders/converter.py +337 -0
- app/shared/core/data_source/loaders/csv_loader.py +252 -0
- app/shared/core/data_source/loaders/excel_loader.py +666 -0
- app/shared/core/data_source/loaders/extractor.py +268 -0
- app/shared/core/data_source/loaders/json_loader.py +354 -0
- app/shared/core/data_source/loaders/registry.py +88 -0
- app/shared/core/data_source/loaders/sql_loader.py +313 -0
- app/shared/core/data_source/loaders/strategies/__init__.py +100 -0
- app/shared/core/data_source/loaders/strategies/array_parser.py +277 -0
- app/shared/core/data_source/loaders/strategies/lines_parser.py +355 -0
- app/shared/core/data_source/loaders/strategies/object_parser.py +278 -0
- app/shared/core/data_source/schema_info.py +50 -0
- app/shared/core/data_source/specs/__init__.py +128 -0
- app/shared/core/data_source/specs/base.py +258 -0
- app/shared/core/data_source/specs/csv_source.py +137 -0
- app/shared/core/data_source/specs/excel_source.py +141 -0
- app/shared/core/data_source/specs/file_base.py +252 -0
- app/shared/core/data_source/specs/json_source.py +226 -0
- app/shared/core/data_source/specs/sql_source.py +161 -0
- app/shared/core/encoding.py +36 -0
- app/shared/core/io/__init__.py +24 -0
- app/shared/core/io/yaml.py +379 -0
- app/shared/core/manifest_schema/__init__.py +52 -0
- app/shared/core/manifest_schema/types.py +99 -0
- app/shared/core/manifest_schema/version.py +175 -0
- app/shared/core/patterns/__init__.py +24 -0
- app/shared/core/patterns/loader.py +134 -0
- app/shared/core/patterns/writer.py +244 -0
- app/shared/core/project/__init__.py +47 -0
- app/shared/core/project/constraint/builders/__init__.py +50 -0
- app/shared/core/project/constraint/builders/base.py +100 -0
- app/shared/core/project/constraint/builders/composite.py +77 -0
- app/shared/core/project/constraint/builders/conditional.py +67 -0
- app/shared/core/project/constraint/builders/foreign_key.py +53 -0
- app/shared/core/project/constraint/builders/registry.py +64 -0
- app/shared/core/project/constraint/builders/scripted.py +51 -0
- app/shared/core/project/constraint/builders/single_column.py +86 -0
- app/shared/core/project/constraint/builders/unique.py +53 -0
- app/shared/core/project/constraint/factory.py +170 -0
- app/shared/core/project/constraint/reader.py +214 -0
- app/shared/core/project/constraint/registry.py +233 -0
- app/shared/core/project/constraint/types/__init__.py +63 -0
- app/shared/core/project/constraint/types/constraint_file.py +261 -0
- app/shared/core/project/constraint/types/refs.py +460 -0
- app/shared/core/project/constraint/types.py +28 -0
- app/shared/core/project/constraint/writer.py +181 -0
- app/shared/core/project/loader/__init__.py +56 -0
- app/shared/core/project/loader/loader.py +30 -0
- app/shared/core/project/loader/loader_parts/config_inspector.py +137 -0
- app/shared/core/project/loader/loader_parts/embedded_constraints.py +224 -0
- app/shared/core/project/loader/loader_parts/file_loaders.py +58 -0
- app/shared/core/project/loader/loader_parts/inspection_ids.py +85 -0
- app/shared/core/project/loader/loader_parts/inspector_helpers.py +312 -0
- app/shared/core/project/loader/loader_parts/inspector_id_checks.py +274 -0
- app/shared/core/project/loader/loader_parts/inspector_reference_checks.py +690 -0
- app/shared/core/project/loader/loader_parts/inspector_uniqueness_checks.py +286 -0
- app/shared/core/project/loader/loader_parts/loading_error_messages.py +298 -0
- app/shared/core/project/loader/loader_parts/main.py +432 -0
- app/shared/core/project/loader/loader_parts/path_validation.py +117 -0
- app/shared/core/project/loader/loader_parts/runtime.py +125 -0
- app/shared/core/project/loader/types.py +246 -0
- app/shared/core/project/manifest/coverage.py +391 -0
- app/shared/core/project/manifest/reader.py +260 -0
- app/shared/core/project/manifest/types.py +91 -0
- app/shared/core/project/manifest/types_parts/__init__.py +60 -0
- app/shared/core/project/manifest/types_parts/constants.py +40 -0
- app/shared/core/project/manifest/types_parts/data_source.py +68 -0
- app/shared/core/project/manifest/types_parts/info.py +59 -0
- app/shared/core/project/manifest/types_parts/manifest.py +294 -0
- app/shared/core/project/manifest/types_parts/refs.py +153 -0
- app/shared/core/project/manifest/types_parts/settings.py +92 -0
- app/shared/core/project/manifest/types_parts/settings_file_processing.py +64 -0
- app/shared/core/project/manifest/types_parts/settings_script_security.py +78 -0
- app/shared/core/project/manifest/types_parts/settings_validation.py +83 -0
- app/shared/core/project/manifest/types_parts/template.py +66 -0
- app/shared/core/project/manifest/writer.py +262 -0
- app/shared/core/project/manual_data/__init__.py +27 -0
- app/shared/core/project/manual_data/types.py +75 -0
- app/shared/core/project/regex/reader.py +197 -0
- app/shared/core/project/regex/types.py +405 -0
- app/shared/core/project/regex/writer.py +123 -0
- app/shared/core/project/schema/reader.py +170 -0
- app/shared/core/project/schema/types.py +47 -0
- app/shared/core/project/schema/types_parts/__init__.py +36 -0
- app/shared/core/project/schema/types_parts/column.py +174 -0
- app/shared/core/project/schema/types_parts/column_utils.py +72 -0
- app/shared/core/project/schema/types_parts/constraint.py +165 -0
- app/shared/core/project/schema/types_parts/schema_id.py +66 -0
- app/shared/core/project/schema/types_parts/source.py +255 -0
- app/shared/core/project/schema/types_parts/source_options.py +347 -0
- app/shared/core/project/schema/types_parts/table.py +230 -0
- app/shared/core/project/schema/writer.py +139 -0
- app/shared/core/project/schema_ref_check.py +95 -0
- app/shared/core/project/template/__init__.py +27 -0
- app/shared/core/project/template/expander.py +263 -0
- app/shared/core/project/template/reader.py +120 -0
- app/shared/core/project/template/types.py +114 -0
- app/shared/core/project/transform/reader.py +76 -0
- app/shared/core/project/transform/types.py +116 -0
- app/shared/core/project/transform/writer.py +84 -0
- app/shared/core/pydantic_messages.py +59 -0
- app/shared/core/reporter/__init__.py +46 -0
- app/shared/core/reporter/reporter.py +220 -0
- app/shared/core/reporter/reporters/__init__.py +65 -0
- app/shared/core/reporter/reporters/base.py +188 -0
- app/shared/core/reporter/reporters/dingtalk_app_reporter.py +274 -0
- app/shared/core/reporter/reporters/email_reporter.py +271 -0
- app/shared/core/reporter/reporters/feishu_app_reporter.py +467 -0
- app/shared/core/reporter/reporters/local_file_reporter.py +208 -0
- app/shared/core/reporter/reporters/wecom_app_reporter.py +268 -0
- app/shared/core/utils/__init__.py +18 -0
- app/shared/core/utils/path_utils.py +60 -0
- app/shared/core/utils/regex_extract.py +113 -0
- app/shared/domain/__init__.py +114 -0
- app/shared/domain/constraints/__init__.py +76 -0
- app/shared/domain/constraints/allowed_values.py +211 -0
- app/shared/domain/constraints/base.py +141 -0
- app/shared/domain/constraints/charset.py +337 -0
- app/shared/domain/constraints/composite.py +174 -0
- app/shared/domain/constraints/condition_registry.py +130 -0
- app/shared/domain/constraints/conditional.py +629 -0
- app/shared/domain/constraints/date_logic.py +731 -0
- app/shared/domain/constraints/foreign_key.py +261 -0
- app/shared/domain/constraints/key_normalization.py +56 -0
- app/shared/domain/constraints/not_null.py +185 -0
- app/shared/domain/constraints/range.py +360 -0
- app/shared/domain/constraints/regex.py +218 -0
- app/shared/domain/constraints/scripted.py +276 -0
- app/shared/domain/constraints/unique.py +233 -0
- app/shared/domain/data_engine.py +384 -0
- app/shared/domain/data_types.py +113 -0
- app/shared/domain/data_types_parts/__init__.py +67 -0
- app/shared/domain/data_types_parts/base.py +204 -0
- app/shared/domain/data_types_parts/composite.py +250 -0
- app/shared/domain/data_types_parts/expression.py +202 -0
- app/shared/domain/data_types_parts/extracted.py +106 -0
- app/shared/domain/data_types_parts/json_types.py +234 -0
- app/shared/domain/data_types_parts/scalars.py +719 -0
- app/shared/domain/data_types_parts/sequence.py +129 -0
- app/shared/domain/dataset_schema.py +48 -0
- app/shared/domain/eval_sandbox.py +63 -0
- app/shared/domain/expression_system.py +366 -0
- app/shared/domain/regex_flags.py +53 -0
- app/shared/domain/schema/builder.py +223 -0
- app/shared/domain/schema/models.py +339 -0
- app/shared/domain/transforms/__init__.py +28 -0
- app/shared/domain/transforms/aggregate.py +141 -0
- app/shared/domain/transforms/base.py +171 -0
- app/shared/domain/transforms/cast_type.py +120 -0
- app/shared/domain/transforms/concat.py +103 -0
- app/shared/domain/transforms/conditional_assign.py +114 -0
- app/shared/domain/transforms/date_format.py +81 -0
- app/shared/domain/transforms/digits.py +77 -0
- app/shared/domain/transforms/drop_duplicates.py +94 -0
- app/shared/domain/transforms/fill_na.py +94 -0
- app/shared/domain/transforms/filter_rows.py +97 -0
- app/shared/domain/transforms/lookup.py +78 -0
- app/shared/domain/transforms/lower_case.py +70 -0
- app/shared/domain/transforms/map_value.py +90 -0
- app/shared/domain/transforms/math_expr.py +112 -0
- app/shared/domain/transforms/modulo.py +82 -0
- app/shared/domain/transforms/regex_extract.py +103 -0
- app/shared/domain/transforms/registry.py +95 -0
- app/shared/domain/transforms/replace.py +88 -0
- app/shared/domain/transforms/sort_rows.py +94 -0
- app/shared/domain/transforms/string_split.py +84 -0
- app/shared/domain/transforms/strip.py +73 -0
- app/shared/domain/transforms/substring.py +92 -0
- app/shared/domain/transforms/upper_case.py +70 -0
- app/shared/domain/transforms/weighted_sum.py +104 -0
- app/shared/domain/validation_constraints.py +69 -0
- app/shared/services/__init__.py +62 -0
- app/shared/services/ai/__init__.py +48 -0
- app/shared/services/ai/agent/__init__.py +40 -0
- app/shared/services/ai/agent/chat_tools/__init__.py +47 -0
- app/shared/services/ai/agent/chat_tools/apply_actions.py +592 -0
- app/shared/services/ai/agent/chat_tools/ask_user.py +237 -0
- app/shared/services/ai/agent/chat_tools/read_canvas.py +140 -0
- app/shared/services/ai/agent/chat_tools/read_project.py +160 -0
- app/shared/services/ai/agent/chat_tools/read_table.py +301 -0
- app/shared/services/ai/agent/chat_tools/schemas.py +105 -0
- app/shared/services/ai/agent/chat_tools/validate_table.py +131 -0
- app/shared/services/ai/agent/executor.py +554 -0
- app/shared/services/ai/agent/memory.py +256 -0
- app/shared/services/ai/agent/planner.py +282 -0
- app/shared/services/ai/agent/tool_registry.py +296 -0
- app/shared/services/ai/agent/tools/__init__.py +32 -0
- app/shared/services/ai/agent/tools/config_generate.py +133 -0
- app/shared/services/ai/agent/tools/config_refine.py +109 -0
- app/shared/services/ai/agent/tools/config_validate.py +403 -0
- app/shared/services/ai/agent/tools/merge_results.py +136 -0
- app/shared/services/ai/agent/tools/plan_chunks.py +79 -0
- app/shared/services/ai/agent/tools/script_parse.py +359 -0
- app/shared/services/ai/agent/types.py +138 -0
- app/shared/services/ai/chat_agent_runner.py +596 -0
- app/shared/services/ai/chat_orchestrator.py +725 -0
- app/shared/services/ai/failure_messages.py +65 -0
- app/shared/services/ai/job_storage.py +274 -0
- app/shared/services/ai/migrate_service.py +550 -0
- app/shared/services/ai/streaming/__init__.py +65 -0
- app/shared/services/ai/streaming/event_journal.py +197 -0
- app/shared/services/ai/streaming/orchestrator.py +262 -0
- app/shared/services/ai/streaming/pending_interaction_store.py +227 -0
- app/shared/services/ai/streaming/sse_response.py +148 -0
- app/shared/services/ai/streaming/types.py +73 -0
- app/shared/services/ai/types.py +213 -0
- app/shared/services/ai/utils.py +322 -0
- app/shared/services/diff/config_diff.py +320 -0
- app/shared/services/hardware.py +237 -0
- app/shared/services/llm/__init__.py +62 -0
- app/shared/services/llm/actions/__init__.py +42 -0
- app/shared/services/llm/actions/_canvas_validator.py +147 -0
- app/shared/services/llm/actions/_constraint_validator.py +401 -0
- app/shared/services/llm/actions/_regex_validator.py +85 -0
- app/shared/services/llm/actions/_schema_validator.py +82 -0
- app/shared/services/llm/actions/_settings_validator.py +76 -0
- app/shared/services/llm/actions/_transform_validator.py +78 -0
- app/shared/services/llm/actions/action_handlers.py +400 -0
- app/shared/services/llm/actions/action_parser.py +63 -0
- app/shared/services/llm/actions/action_processor.py +468 -0
- app/shared/services/llm/actions/action_validator.py +311 -0
- app/shared/services/llm/actions/diff_compute.py +195 -0
- app/shared/services/llm/actions/regex_handlers.py +284 -0
- app/shared/services/llm/actions/registry.py +336 -0
- app/shared/services/llm/actions/schema_handlers.py +335 -0
- app/shared/services/llm/actions/settings_handlers.py +180 -0
- app/shared/services/llm/actions/specs.py +259 -0
- app/shared/services/llm/actions/transform_handlers.py +279 -0
- app/shared/services/llm/actions/validation_types.py +147 -0
- app/shared/services/llm/cache/__init__.py +18 -0
- app/shared/services/llm/cache/response_cache.py +80 -0
- app/shared/services/llm/chat/__init__.py +35 -0
- app/shared/services/llm/chat/chat_system_prompt.py +561 -0
- app/shared/services/llm/chat/response_parser.py +296 -0
- app/shared/services/llm/config/__init__.py +45 -0
- app/shared/services/llm/config/crypto.py +135 -0
- app/shared/services/llm/config/loader.py +258 -0
- app/shared/services/llm/config/models.py +202 -0
- app/shared/services/llm/config/presets.py +128 -0
- app/shared/services/llm/config_generator.py +163 -0
- app/shared/services/llm/constraints/__init__.py +41 -0
- app/shared/services/llm/constraints/constraint_builder.py +177 -0
- app/shared/services/llm/constraints/constraint_deletion.py +84 -0
- app/shared/services/llm/constraints/constraint_id.py +174 -0
- app/shared/services/llm/constraints/frontend_instructions.py +333 -0
- app/shared/services/llm/constraints/inline_batch.py +279 -0
- app/shared/services/llm/discovery/__init__.py +35 -0
- app/shared/services/llm/discovery/scanner.py +181 -0
- app/shared/services/llm/generation/__init__.py +42 -0
- app/shared/services/llm/generation/agent_wiring.py +156 -0
- app/shared/services/llm/generation/config_builder.py +386 -0
- app/shared/services/llm/generation/errors.py +36 -0
- app/shared/services/llm/generation/existing_config.py +99 -0
- app/shared/services/llm/generation/profiler.py +279 -0
- app/shared/services/llm/generation/prompt_builder.py +199 -0
- app/shared/services/llm/generation/response_parser.py +144 -0
- app/shared/services/llm/generation/service.py +622 -0
- app/shared/services/llm/models.py +104 -0
- app/shared/services/llm/providers/__init__.py +48 -0
- app/shared/services/llm/providers/base.py +310 -0
- app/shared/services/llm/providers/cached_provider.py +90 -0
- app/shared/services/llm/providers/ollama.py +498 -0
- app/shared/services/llm/providers/openai.py +334 -0
- app/shared/services/llm/providers/registry.py +108 -0
- app/shared/services/llm/schema_resolver.py +141 -0
- app/shared/services/llm/suggestion_utils.py +219 -0
- app/shared/services/llm/validate_executor.py +163 -0
- app/shared/services/llm/yaml_io.py +406 -0
- app/shared/services/preview/__init__.py +18 -0
- app/shared/services/preview/loader.py +145 -0
- app/shared/services/preview/path_validation.py +138 -0
- app/shared/services/project_loader.py +57 -0
- app/shared/services/schema_inference.py +260 -0
- app/shared/services/schema_runtime_builder.py +120 -0
- app/shared/services/validation/__init__.py +52 -0
- app/shared/services/validation/chunked_loader.py +509 -0
- app/shared/services/validation/dag/__init__.py +35 -0
- app/shared/services/validation/dag/builder.py +141 -0
- app/shared/services/validation/dag/executor.py +225 -0
- app/shared/services/validation/dag/sorter.py +77 -0
- app/shared/services/validation/data_loader.py +231 -0
- app/shared/services/validation/engine.py +446 -0
- app/shared/services/validation/executor.py +1030 -0
- app/shared/services/validation/extractors.py +266 -0
- app/shared/services/validation/history.py +209 -0
- app/shared/services/validation/json_payload.py +136 -0
- app/shared/services/validation/loader.py +152 -0
- app/shared/services/validation/memory_monitor.py +179 -0
- app/shared/services/validation/postprocess.py +208 -0
- app/shared/services/validation/progress.py +64 -0
- app/shared/services/validation/report_export.py +222 -0
- app/shared/services/validation/resolver.py +180 -0
- app/shared/services/validation/service.py +418 -0
- app/shared/services/validation/types.py +238 -0
- app/shared/services/validation/validators/__init__.py +36 -0
- app/shared/services/validation/validators/adapter.py +182 -0
- app/shared/services/validation/validators/base.py +332 -0
- app/shared/services/validation/validators/composite.py +233 -0
- app/shared/services/validation/validators/date_logic.py +276 -0
- app/start_server.py +133 -0
- precis_cli-0.1.3.dist-info/METADATA +180 -0
- precis_cli-0.1.3.dist-info/RECORD +442 -0
- precis_cli-0.1.3.dist-info/WHEEL +5 -0
- precis_cli-0.1.3.dist-info/entry_points.txt +4 -0
- precis_cli-0.1.3.dist-info/top_level.txt +1 -0
app/cli/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
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 CLI 模块入口
|
|
18
|
+
"""
|
app/cli/__main__.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
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
|
+
# backend/app/cli/__main__.py
|
|
17
|
+
"""
|
|
18
|
+
@fileoverview CLI 入口启动模块
|
|
19
|
+
|
|
20
|
+
功能概述:
|
|
21
|
+
- 作为 python -m app.cli 的入口点启动交互式 CLI
|
|
22
|
+
- 委托给 shell.main.main 执行主循环
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import sys
|
|
26
|
+
|
|
27
|
+
from app.cli.shell.main import main
|
|
28
|
+
|
|
29
|
+
if __name__ == "__main__":
|
|
30
|
+
sys.exit(main())
|
|
@@ -0,0 +1,42 @@
|
|
|
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
|
+
# backend/app/cli/shared_services/__init__.py
|
|
17
|
+
"""
|
|
18
|
+
@fileoverview CLI/TUI 共享薄壳层(Layer 2)
|
|
19
|
+
|
|
20
|
+
功能概述:
|
|
21
|
+
- 收敛 CLI 与 TUI 共用的非核心业务逻辑,确保「改一处即可」
|
|
22
|
+
- 三个子模块:project_ops(项目操作)、config_ops(配置操作)、generation_ops(生成/迁移落盘)
|
|
23
|
+
|
|
24
|
+
架构设计:
|
|
25
|
+
- 本包只含纯逻辑与文件 IO,不含任何 UI/交互
|
|
26
|
+
- CLI 的 execute() 与 TUI 的 service 都从这里 import,不再复制业务规则
|
|
27
|
+
- 核心业务逻辑仍在 app.shared.*(Layer 1),本层只收口「不该复制但不属于核心」的薄壳
|
|
28
|
+
|
|
29
|
+
接口契约(P0b 冻结,P1/P3/P5 直接 import 不修改):
|
|
30
|
+
project_ops: load_history / add_to_history / find_manifest /
|
|
31
|
+
load_manifest_config / resolve_project_label / open_project / OpenResult
|
|
32
|
+
config_ops: find_config_file / get_by_dotpath / set_by_dotpath /
|
|
33
|
+
parse_config_value / list_config_files / load_config_content /
|
|
34
|
+
check_yaml_syntax / ConfigFileInfo / YamlCheckResult
|
|
35
|
+
generation_ops: apply_generated_config / scan_data_files / SUPPORTED_EXTENSIONS
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
from app.cli.shared_services import config_ops, generation_ops, project_ops
|
|
41
|
+
|
|
42
|
+
__all__ = ["config_ops", "generation_ops", "project_ops"]
|
|
@@ -0,0 +1,581 @@
|
|
|
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
|
+
# backend/app/cli/shared_services/config_ops.py
|
|
17
|
+
"""
|
|
18
|
+
@fileoverview 配置操作共享逻辑(CLI/TUI 同源)
|
|
19
|
+
|
|
20
|
+
功能概述:
|
|
21
|
+
- 收敛 config show/get/set/list/check 共用的纯业务逻辑:
|
|
22
|
+
配置文件定位、点号路径读写、值类型解析、YAML 语法检查
|
|
23
|
+
- 供 CLI 的 config 子命令与未来 TUI 的 config 屏共同调用,确保「改一处即可」
|
|
24
|
+
|
|
25
|
+
架构设计:
|
|
26
|
+
- 本模块只含纯逻辑与文件 IO,不含任何 UI/渲染
|
|
27
|
+
- check_yaml_syntax 只负责「检查 + 收集错误位置/上下文/建议」,rich.Syntax 渲染留在各 UI 层
|
|
28
|
+
- find_config_file 含路径穿越防护(原 config/base.py 逻辑)
|
|
29
|
+
|
|
30
|
+
接口契约(P0b 冻结):
|
|
31
|
+
def find_config_file(project_path: str, name: str | None) -> str | None
|
|
32
|
+
def resolve_config_file(project_path: str, name: str | None) -> tuple[str | None, str | None]
|
|
33
|
+
def get_by_dotpath(data: dict, key_path: str) -> tuple[bool, Any]
|
|
34
|
+
def set_by_dotpath(data: dict, key_path: str, value: Any) -> dict
|
|
35
|
+
def parse_config_value(value_str: str) -> tuple[bool, Any, str]
|
|
36
|
+
def list_config_files(project_path: str) -> list[ConfigFileInfo]
|
|
37
|
+
def load_config_content(project_path: str, filename: str) -> dict | str
|
|
38
|
+
def check_yaml_syntax(content: str, filename: str) -> YamlCheckResult
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
|
|
43
|
+
import logging
|
|
44
|
+
import os
|
|
45
|
+
import tempfile
|
|
46
|
+
from collections.abc import Iterator
|
|
47
|
+
from dataclasses import dataclass
|
|
48
|
+
from pathlib import Path
|
|
49
|
+
from typing import Any
|
|
50
|
+
|
|
51
|
+
import yaml
|
|
52
|
+
from ruamel.yaml import YAML
|
|
53
|
+
from ruamel.yaml.comments import CommentedMap
|
|
54
|
+
from ruamel.yaml.error import YAMLError as RuamelYAMLError
|
|
55
|
+
|
|
56
|
+
from app.shared.core.utils.path_utils import paths_equal
|
|
57
|
+
|
|
58
|
+
logger = logging.getLogger(__name__)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class ConfigFileInfo:
|
|
63
|
+
"""单个配置文件的信息(list 子命令用)。
|
|
64
|
+
|
|
65
|
+
Attributes:
|
|
66
|
+
name: 相对项目根的显示名(根目录文件为文件名,子目录文件为相对路径)
|
|
67
|
+
size: 文件大小(字节)
|
|
68
|
+
path: 文件绝对路径
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
name: str
|
|
72
|
+
size: int
|
|
73
|
+
path: str
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
class YamlCheckResult:
|
|
78
|
+
"""YAML 语法检查结果(check 子命令用,纯逻辑,不含渲染)。
|
|
79
|
+
|
|
80
|
+
Attributes:
|
|
81
|
+
file: 文件名
|
|
82
|
+
valid: 是否通过语法检查
|
|
83
|
+
line_no: 错误所在行号(1-based);无位置信息或通过时为 None
|
|
84
|
+
snippet: 错误附近的代码片段(纯文本,无高亮);无则 None
|
|
85
|
+
problem: 简化后的问题描述;无则 None
|
|
86
|
+
hint: 修复建议;无则 None
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
file: str
|
|
90
|
+
valid: bool
|
|
91
|
+
line_no: int | None
|
|
92
|
+
snippet: str | None
|
|
93
|
+
problem: str | None
|
|
94
|
+
hint: str | None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def find_config_file(project_path: str, filename: str | None) -> str | None:
|
|
98
|
+
"""查找配置文件。
|
|
99
|
+
|
|
100
|
+
首先尝试直接拼接路径,如果失败则在项目目录下递归查找
|
|
101
|
+
(先精确相对路径匹配,再按文件名模糊回退)。
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
project_path: 项目根目录路径
|
|
105
|
+
filename: 文件名(可能包含子目录);为 None 或空时返回 None
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
文件的完整路径,如果未找到则返回 None
|
|
109
|
+
|
|
110
|
+
Security:
|
|
111
|
+
- 验证 project_path 是合法目录
|
|
112
|
+
- 验证 filename 不包含路径分隔符或父目录引用
|
|
113
|
+
- 解析后的文件路径必须在 project_path 范围内
|
|
114
|
+
"""
|
|
115
|
+
path, _match_kind = resolve_config_file(project_path, filename)
|
|
116
|
+
return path
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# 路径匹配方式(resolve_config_file 返回值第二元素)
|
|
120
|
+
MATCH_DIRECT = "direct" # 直接路径命中(project_path/filename 存在)
|
|
121
|
+
MATCH_EXACT = "exact" # 递归查找中精确相对路径命中(rel_path == filename)
|
|
122
|
+
MATCH_BASENAME = "basename" # 按文件名模糊回退命中(仅当直接/精确均未命中时)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def resolve_config_file(project_path: str, filename: str | None) -> tuple[str | None, str | None]:
|
|
126
|
+
"""查找配置文件,返回 (路径, 匹配方式)。
|
|
127
|
+
|
|
128
|
+
与 find_config_file 的定位逻辑一致,额外返回匹配方式以便调用方
|
|
129
|
+
区分"精确命中"与"文件名模糊回退"——后者命中时必须向用户披露实际
|
|
130
|
+
写入的解析路径(如 config set 的成功提示)。
|
|
131
|
+
|
|
132
|
+
匹配优先级:直接路径 > 精确相对路径(递归)> 文件名模糊回退(递归)。
|
|
133
|
+
精确匹配整轮优先于模糊回退:输入 "schemas/x.yaml" 而 schemas/ 下不存在
|
|
134
|
+
该文件时,若 regex/ 下存在同名文件,模糊回退仍命中 regex/x.yaml,
|
|
135
|
+
但任何目录下的精确相对路径命中都先于模糊回退返回。
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
project_path: 项目根目录路径
|
|
139
|
+
filename: 文件名(可能包含子目录);为 None 或空时返回 (None, None)
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
(文件的完整路径, 匹配方式);未找到时返回 (None, None)。
|
|
143
|
+
匹配方式取值为 MATCH_DIRECT / MATCH_EXACT / MATCH_BASENAME。
|
|
144
|
+
|
|
145
|
+
Security:
|
|
146
|
+
- 验证 project_path 是合法目录
|
|
147
|
+
- 验证 filename 不包含路径分隔符或父目录引用
|
|
148
|
+
- 解析后的文件路径必须在 project_path 范围内
|
|
149
|
+
"""
|
|
150
|
+
# 验证 project_path 是合法目录
|
|
151
|
+
if not project_path or not isinstance(project_path, str):
|
|
152
|
+
return None, None
|
|
153
|
+
|
|
154
|
+
project_path = os.path.realpath(project_path)
|
|
155
|
+
if not os.path.isdir(project_path):
|
|
156
|
+
return None, None
|
|
157
|
+
|
|
158
|
+
# 验证 filename 不包含危险字符(路径穿越防护)
|
|
159
|
+
if not filename or not isinstance(filename, str):
|
|
160
|
+
return None, None
|
|
161
|
+
|
|
162
|
+
# 禁止绝对路径和父目录引用
|
|
163
|
+
if os.path.isabs(filename):
|
|
164
|
+
return None, None
|
|
165
|
+
# 父目录引用按路径段检查(子串检查会误伤 "my..data.yaml" 这类合法文件名)
|
|
166
|
+
if any(seg == ".." for seg in Path(filename).parts) or filename.startswith("~"):
|
|
167
|
+
return None, None
|
|
168
|
+
|
|
169
|
+
# 首先尝试直接路径
|
|
170
|
+
direct_path = os.path.normpath(os.path.join(project_path, filename))
|
|
171
|
+
|
|
172
|
+
# 确保解析后的路径在项目目录范围内
|
|
173
|
+
if not direct_path.startswith(project_path):
|
|
174
|
+
return None, None
|
|
175
|
+
|
|
176
|
+
if os.path.isfile(direct_path):
|
|
177
|
+
return direct_path, MATCH_DIRECT
|
|
178
|
+
|
|
179
|
+
# 递归查找(跳过隐藏目录)
|
|
180
|
+
def _walk_visible() -> Iterator[tuple[str, list[str]]]:
|
|
181
|
+
for root, _, files in os.walk(project_path):
|
|
182
|
+
if any(part.startswith(".") for part in root.split(os.sep)):
|
|
183
|
+
continue
|
|
184
|
+
yield root, files
|
|
185
|
+
|
|
186
|
+
# 第一遍:精确相对路径匹配(如 "schemas/x.yaml" 必须命中 schemas/ 下的该文件)
|
|
187
|
+
for root, files in _walk_visible():
|
|
188
|
+
for f in files:
|
|
189
|
+
rel_path = os.path.relpath(os.path.join(root, f), project_path)
|
|
190
|
+
if paths_equal(rel_path, filename):
|
|
191
|
+
full_path = os.path.join(root, f)
|
|
192
|
+
# 验证找到的完整路径在项目范围内
|
|
193
|
+
if os.path.realpath(full_path).startswith(project_path):
|
|
194
|
+
return full_path, MATCH_EXACT
|
|
195
|
+
|
|
196
|
+
# 第二遍:文件名模糊回退(仅当直接路径与精确相对路径均未命中时)
|
|
197
|
+
for root, files in _walk_visible():
|
|
198
|
+
if os.path.basename(filename) in files:
|
|
199
|
+
full_path = os.path.join(root, os.path.basename(filename))
|
|
200
|
+
# 验证找到的完整路径在项目范围内
|
|
201
|
+
if os.path.realpath(full_path).startswith(project_path):
|
|
202
|
+
return full_path, MATCH_BASENAME
|
|
203
|
+
|
|
204
|
+
return None, None
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def get_by_dotpath(data: dict, key_path: str) -> tuple[bool, Any]:
|
|
208
|
+
"""按点号路径从字典中查找值。
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
data: 配置字典
|
|
212
|
+
key_path: 点号路径,如 "project.name"
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
(找到, 值);未找到时返回 (False, None)
|
|
216
|
+
"""
|
|
217
|
+
keys = key_path.split(".")
|
|
218
|
+
value: Any = data
|
|
219
|
+
for key in keys:
|
|
220
|
+
if isinstance(value, dict) and key in value:
|
|
221
|
+
value = value[key]
|
|
222
|
+
else:
|
|
223
|
+
return False, None
|
|
224
|
+
return True, value
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def set_by_dotpath(data: dict, key_path: str, value: Any) -> dict:
|
|
228
|
+
"""按点号路径写入值,返回新 data(不改原 dict)。
|
|
229
|
+
|
|
230
|
+
中间层级不存在时自动创建空字典。
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
data: 原配置字典(不会被修改)
|
|
234
|
+
key_path: 点号路径,如 "project.name"
|
|
235
|
+
value: 要写入的值
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
写入后的新字典(深拷贝原 data 后写入)
|
|
239
|
+
"""
|
|
240
|
+
# 深拷贝避免修改原 dict(YAML 配置是 JSON 安全数据,用 JSON 拷贝即可)
|
|
241
|
+
import copy
|
|
242
|
+
|
|
243
|
+
new_data = copy.deepcopy(data) if data else {}
|
|
244
|
+
keys = key_path.split(".")
|
|
245
|
+
current = new_data
|
|
246
|
+
for key in keys[:-1]:
|
|
247
|
+
if key not in current or not isinstance(current[key], dict):
|
|
248
|
+
current[key] = {}
|
|
249
|
+
current = current[key]
|
|
250
|
+
current[keys[-1]] = value
|
|
251
|
+
return new_data
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def parse_config_value(value_str: str) -> tuple[bool, Any, str]:
|
|
255
|
+
"""解析字符串值为合适的类型。
|
|
256
|
+
|
|
257
|
+
按以下顺序尝试转换:
|
|
258
|
+
1. 布尔值(true/false)
|
|
259
|
+
2. null(null/none)
|
|
260
|
+
3. 整数
|
|
261
|
+
4. 浮点数
|
|
262
|
+
5. 去除引号的字符串
|
|
263
|
+
6. YAML 列表或字典
|
|
264
|
+
7. 原样字符串
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
value_str: 原始字符串值
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
(成功, 转换后的值, 错误信息);本实现始终成功,错误信息为空字符串。
|
|
271
|
+
保留 tuple 三元组以匹配接口契约,便于未来扩展严格模式。
|
|
272
|
+
"""
|
|
273
|
+
# 尝试布尔值
|
|
274
|
+
if value_str.lower() == "true":
|
|
275
|
+
return True, True, ""
|
|
276
|
+
if value_str.lower() == "false":
|
|
277
|
+
return True, False, ""
|
|
278
|
+
if value_str.lower() == "null" or value_str.lower() == "none":
|
|
279
|
+
return True, None, ""
|
|
280
|
+
|
|
281
|
+
# 尝试整数
|
|
282
|
+
try:
|
|
283
|
+
return True, int(value_str), ""
|
|
284
|
+
except ValueError:
|
|
285
|
+
pass
|
|
286
|
+
|
|
287
|
+
# 尝试浮点数
|
|
288
|
+
try:
|
|
289
|
+
return True, float(value_str), ""
|
|
290
|
+
except ValueError:
|
|
291
|
+
pass
|
|
292
|
+
|
|
293
|
+
# 去除引号
|
|
294
|
+
if (value_str.startswith('"') and value_str.endswith('"')) or (
|
|
295
|
+
value_str.startswith("'") and value_str.endswith("'")
|
|
296
|
+
):
|
|
297
|
+
return True, value_str[1:-1], ""
|
|
298
|
+
|
|
299
|
+
# 尝试 YAML 列表或对象
|
|
300
|
+
try:
|
|
301
|
+
parsed = yaml.safe_load(value_str)
|
|
302
|
+
if isinstance(parsed, (list, dict)):
|
|
303
|
+
return True, parsed, ""
|
|
304
|
+
except Exception:
|
|
305
|
+
logger.debug("解析 YAML 值失败: %s", value_str, exc_info=True)
|
|
306
|
+
|
|
307
|
+
# 默认返回字符串
|
|
308
|
+
return True, value_str, ""
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def set_config_value_in_file(project_path: str, filename: str, key_path: str, value: Any) -> tuple[bool, str]:
|
|
312
|
+
"""按点号路径设置 YAML 配置文件中的值:保留注释与格式,原子写回。
|
|
313
|
+
|
|
314
|
+
与 ``set_by_dotpath``(纯内存 dict)不同,本函数负责文件级写入,采用
|
|
315
|
+
ruamel.yaml round-trip 加载——仅目标键的值被替换,其余行(含注释、空行、
|
|
316
|
+
引号风格)原样保留;写回经由「临时文件 + os.replace」保证原子性,避免
|
|
317
|
+
进程中断导致配置文件损坏。
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
project_path: 项目根目录绝对路径
|
|
321
|
+
filename: 配置文件名(相对项目根,可含子目录)
|
|
322
|
+
key_path: 点号路径,如 "project.name"
|
|
323
|
+
value: 要写入的值(已由 parse_config_value 解析为目标类型)
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
(成功, 错误信息);成功时错误信息为空字符串。
|
|
327
|
+
|
|
328
|
+
Security:
|
|
329
|
+
路径解析统一走 find_config_file(拒绝绝对路径 / ".." 父目录引用 /
|
|
330
|
+
解析后越出项目根的路径),防止 CLI 入参穿越到项目外任意文件。
|
|
331
|
+
"""
|
|
332
|
+
config_path = find_config_file(project_path, filename)
|
|
333
|
+
if not config_path:
|
|
334
|
+
return False, f"配置文件不存在: {filename}"
|
|
335
|
+
|
|
336
|
+
# round-trip 模式加载:CommentedMap 携带注释/格式元数据,回写时保留
|
|
337
|
+
yaml_parser = YAML()
|
|
338
|
+
yaml_parser.preserve_quotes = True
|
|
339
|
+
|
|
340
|
+
try:
|
|
341
|
+
with open(config_path, encoding="utf-8") as f:
|
|
342
|
+
data = yaml_parser.load(f)
|
|
343
|
+
except RuamelYAMLError as e:
|
|
344
|
+
return False, f"YAML 解析失败: {e}"
|
|
345
|
+
|
|
346
|
+
# 空文件:从空映射开始(首键写入后无注释可保,属预期)
|
|
347
|
+
if data is None:
|
|
348
|
+
data = CommentedMap()
|
|
349
|
+
if not isinstance(data, dict):
|
|
350
|
+
return False, "配置根节点不是字典,无法按点号路径设置"
|
|
351
|
+
|
|
352
|
+
# 按点号路径逐层定位后原地写入(不经 set_by_dotpath 的 deepcopy——
|
|
353
|
+
# 直接在 ruamel 结构上替换目标键的值,其余键的注释元数据原封不动)
|
|
354
|
+
keys = key_path.split(".")
|
|
355
|
+
current: dict[str, Any] = data
|
|
356
|
+
for key in keys[:-1]:
|
|
357
|
+
child = current.get(key)
|
|
358
|
+
if not isinstance(child, dict):
|
|
359
|
+
if child is None:
|
|
360
|
+
# 中间层级不存在:创建空映射(与 set_by_dotpath 语义一致)
|
|
361
|
+
child = CommentedMap()
|
|
362
|
+
current[key] = child
|
|
363
|
+
else:
|
|
364
|
+
return False, f"路径 '{key_path}' 的中间键 '{key}' 不是字典,无法设置"
|
|
365
|
+
current = child
|
|
366
|
+
current[keys[-1]] = value
|
|
367
|
+
|
|
368
|
+
# 原子写回:同目录临时文件 + os.replace(同分区 rename,原子生效)
|
|
369
|
+
config_dir = os.path.dirname(config_path) or "."
|
|
370
|
+
try:
|
|
371
|
+
fd, temp_path = tempfile.mkstemp(
|
|
372
|
+
dir=config_dir,
|
|
373
|
+
prefix=f".{os.path.basename(config_path)}_tmp_",
|
|
374
|
+
suffix=".yaml",
|
|
375
|
+
)
|
|
376
|
+
try:
|
|
377
|
+
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
378
|
+
yaml_parser.dump(data, f)
|
|
379
|
+
os.replace(temp_path, config_path)
|
|
380
|
+
except OSError:
|
|
381
|
+
# 写入或替换失败:清理临时文件,原配置文件不受影响
|
|
382
|
+
try:
|
|
383
|
+
os.remove(temp_path)
|
|
384
|
+
except OSError:
|
|
385
|
+
logger.debug("删除临时文件失败", exc_info=True)
|
|
386
|
+
raise
|
|
387
|
+
except OSError as e:
|
|
388
|
+
return False, f"写入失败: {e}"
|
|
389
|
+
|
|
390
|
+
return True, ""
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def list_config_files(project_path: str) -> list[ConfigFileInfo]:
|
|
394
|
+
"""扫描项目中的所有 YAML 配置文件。
|
|
395
|
+
|
|
396
|
+
先扫描项目根目录,再递归扫描 schemas/constraints/patterns/regex 子目录。
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
project_path: 项目根目录绝对路径
|
|
400
|
+
|
|
401
|
+
Returns:
|
|
402
|
+
配置文件信息列表,按名称排序
|
|
403
|
+
"""
|
|
404
|
+
config_files: list[ConfigFileInfo] = []
|
|
405
|
+
|
|
406
|
+
# 根目录
|
|
407
|
+
for f in os.listdir(project_path):
|
|
408
|
+
if f.endswith((".yaml", ".yml")):
|
|
409
|
+
config_path = os.path.join(project_path, f)
|
|
410
|
+
try:
|
|
411
|
+
stat = os.stat(config_path)
|
|
412
|
+
config_files.append(ConfigFileInfo(name=f, size=stat.st_size, path=config_path))
|
|
413
|
+
except Exception:
|
|
414
|
+
logger.error("获取文件信息失败", exc_info=True)
|
|
415
|
+
|
|
416
|
+
# 递归子目录
|
|
417
|
+
subdirs = ["schemas", "constraints", "patterns", "regex"]
|
|
418
|
+
for subdir in subdirs:
|
|
419
|
+
subdir_path = os.path.join(project_path, subdir)
|
|
420
|
+
if os.path.isdir(subdir_path):
|
|
421
|
+
for root, _, files in os.walk(subdir_path):
|
|
422
|
+
for f in files:
|
|
423
|
+
if f.endswith((".yaml", ".yml")):
|
|
424
|
+
rel_path = os.path.relpath(os.path.join(root, f), project_path)
|
|
425
|
+
config_path = os.path.join(root, f)
|
|
426
|
+
try:
|
|
427
|
+
stat = os.stat(config_path)
|
|
428
|
+
config_files.append(ConfigFileInfo(name=rel_path, size=stat.st_size, path=config_path))
|
|
429
|
+
except Exception:
|
|
430
|
+
pass
|
|
431
|
+
|
|
432
|
+
# 按名称排序
|
|
433
|
+
config_files.sort(key=lambda x: x.name)
|
|
434
|
+
return config_files
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def load_config_content(project_path: str, filename: str) -> dict | str:
|
|
438
|
+
"""读取单个配置文件内容。
|
|
439
|
+
|
|
440
|
+
Args:
|
|
441
|
+
project_path: 项目根目录绝对路径
|
|
442
|
+
filename: 配置文件名(相对项目根)
|
|
443
|
+
|
|
444
|
+
Returns:
|
|
445
|
+
解析后的 dict;文件不存在时返回错误描述字符串;
|
|
446
|
+
文件为空时返回 "(空文件)";读取失败时返回 "(读取失败: ...)"。
|
|
447
|
+
"""
|
|
448
|
+
config_path = find_config_file(project_path, filename)
|
|
449
|
+
if not config_path:
|
|
450
|
+
return f"配置文件不存在: {filename}"
|
|
451
|
+
try:
|
|
452
|
+
with open(config_path, encoding="utf-8") as f:
|
|
453
|
+
content = yaml.safe_load(f)
|
|
454
|
+
if content:
|
|
455
|
+
return content if isinstance(content, dict) else str(content)
|
|
456
|
+
return "(空文件)"
|
|
457
|
+
except yaml.YAMLError as e:
|
|
458
|
+
return f"YAML 解析失败: {e}"
|
|
459
|
+
except Exception as e:
|
|
460
|
+
return f"读取失败: {e}"
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def _simplify_error_message(msg: str) -> str:
|
|
464
|
+
"""将技术性的错误信息转换为更友好的描述。
|
|
465
|
+
|
|
466
|
+
Args:
|
|
467
|
+
msg: 原始错误信息
|
|
468
|
+
|
|
469
|
+
Returns:
|
|
470
|
+
翻译后的中文描述,如果没有匹配则返回原文
|
|
471
|
+
"""
|
|
472
|
+
translations = {
|
|
473
|
+
"expected '<document start>', but found": "期望文件开始标记,但实际发现",
|
|
474
|
+
"expected <block end>, but found": "期望块结束,但实际发现",
|
|
475
|
+
"expected ',' or ']', but got": "期望逗号或右括号,但实际得到",
|
|
476
|
+
"while parsing a block mapping": "解析对象/字典时出错",
|
|
477
|
+
"while parsing a block collection": "解析列表/数组时出错",
|
|
478
|
+
"mapping values are not allowed here": "此处不允许键值对(可能缺少冒号或缩进错误)",
|
|
479
|
+
"could not determine a constructor for the tag": "无法识别的标签类型",
|
|
480
|
+
"found character": "发现不期望的字符",
|
|
481
|
+
"that cannot start any token": "无法作为任何标记的开始",
|
|
482
|
+
"unacceptable character": "包含不可接受的字符(可能是编码问题)",
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
for tech, friendly in translations.items():
|
|
486
|
+
if tech.lower() in msg.lower():
|
|
487
|
+
return friendly
|
|
488
|
+
|
|
489
|
+
return msg
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def check_yaml_syntax(content: str, filename: str) -> YamlCheckResult:
|
|
493
|
+
"""检查 YAML 文本语法,返回纯逻辑结果(不含 rich 渲染)。
|
|
494
|
+
|
|
495
|
+
Args:
|
|
496
|
+
content: 文件原始文本内容
|
|
497
|
+
filename: 文件名(用于结果记录)
|
|
498
|
+
|
|
499
|
+
Returns:
|
|
500
|
+
YamlCheckResult:包含 valid/行号/代码片段/问题描述/修复建议。
|
|
501
|
+
代码片段为纯文本(错误行附近 ±2 行),rich.Syntax 高亮由调用方自行渲染。
|
|
502
|
+
"""
|
|
503
|
+
content_lines = content.split("\n")
|
|
504
|
+
|
|
505
|
+
try:
|
|
506
|
+
yaml.safe_load(content)
|
|
507
|
+
return YamlCheckResult(
|
|
508
|
+
file=filename,
|
|
509
|
+
valid=True,
|
|
510
|
+
line_no=None,
|
|
511
|
+
snippet=None,
|
|
512
|
+
problem=None,
|
|
513
|
+
hint=None,
|
|
514
|
+
)
|
|
515
|
+
except yaml.YAMLError as e:
|
|
516
|
+
# 获取错误位置
|
|
517
|
+
problem_mark = getattr(e, "problem_mark", None)
|
|
518
|
+
error_line_no = problem_mark.line if problem_mark else None # 0-based
|
|
519
|
+
|
|
520
|
+
# 代码片段(纯文本,错误行附近 ±2 行)
|
|
521
|
+
snippet: str | None = None
|
|
522
|
+
if error_line_no is not None:
|
|
523
|
+
context_start = max(0, error_line_no - 2)
|
|
524
|
+
context_end = min(len(content_lines), error_line_no + 3)
|
|
525
|
+
snippet_lines = content_lines[context_start:context_end]
|
|
526
|
+
# 1-based 起始行号标注在每行前,便于 UI 渲染
|
|
527
|
+
start_line = context_start + 1
|
|
528
|
+
snippet = "\n".join(f"{start_line + idx:>4} | {line}" for idx, line in enumerate(snippet_lines))
|
|
529
|
+
|
|
530
|
+
# 问题与上下文描述
|
|
531
|
+
problem: str | None = None
|
|
532
|
+
if hasattr(e, "problem") and e.problem:
|
|
533
|
+
problem = _simplify_error_message(e.problem)
|
|
534
|
+
|
|
535
|
+
if hasattr(e, "context") and e.context:
|
|
536
|
+
context = _simplify_error_message(e.context)
|
|
537
|
+
problem = f"{problem}(上下文: {context})" if problem else context
|
|
538
|
+
|
|
539
|
+
# 修复建议
|
|
540
|
+
hint: str | None = None
|
|
541
|
+
problem_str = str(getattr(e, "problem", "")).lower()
|
|
542
|
+
context_str = str(getattr(e, "context", "")).lower()
|
|
543
|
+
|
|
544
|
+
if "block end" in problem_str and "scalar" in problem_str:
|
|
545
|
+
hint = "可能是上一行缺少冒号,或括号/引号不匹配"
|
|
546
|
+
# 检查前一行的内容
|
|
547
|
+
if error_line_no is not None and error_line_no > 0:
|
|
548
|
+
prev_line = content_lines[error_line_no - 1].strip()
|
|
549
|
+
if "required" in prev_line and ":" not in prev_line:
|
|
550
|
+
hint += f";上一行 '{prev_line[:30]}...' 可能缺少冒号"
|
|
551
|
+
elif "mapping" in context_str:
|
|
552
|
+
hint = "检查键值对格式是否正确(key: value)"
|
|
553
|
+
elif "could not determine a constructor" in problem_str:
|
|
554
|
+
hint = "可能包含特殊字符或不支持的 YAML 语法"
|
|
555
|
+
|
|
556
|
+
return YamlCheckResult(
|
|
557
|
+
file=filename,
|
|
558
|
+
valid=False,
|
|
559
|
+
line_no=(error_line_no + 1) if error_line_no is not None else None,
|
|
560
|
+
snippet=snippet,
|
|
561
|
+
problem=problem,
|
|
562
|
+
hint=hint,
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
__all__ = [
|
|
567
|
+
"MATCH_BASENAME",
|
|
568
|
+
"MATCH_DIRECT",
|
|
569
|
+
"MATCH_EXACT",
|
|
570
|
+
"ConfigFileInfo",
|
|
571
|
+
"YamlCheckResult",
|
|
572
|
+
"check_yaml_syntax",
|
|
573
|
+
"find_config_file",
|
|
574
|
+
"get_by_dotpath",
|
|
575
|
+
"list_config_files",
|
|
576
|
+
"load_config_content",
|
|
577
|
+
"parse_config_value",
|
|
578
|
+
"resolve_config_file",
|
|
579
|
+
"set_by_dotpath",
|
|
580
|
+
"set_config_value_in_file",
|
|
581
|
+
]
|