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
|
@@ -0,0 +1,605 @@
|
|
|
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 Manifest API - 项目清单管理
|
|
18
|
+
|
|
19
|
+
功能概述:
|
|
20
|
+
- 负责 project.precis.yaml 的读写操作
|
|
21
|
+
- 支持整体替换与合并写入(replace 参数控制)
|
|
22
|
+
- 提供 schema / constraint / regex 单个引用的更新接口
|
|
23
|
+
|
|
24
|
+
架构设计:
|
|
25
|
+
- Manifest 作为单一真相来源,记录所有资源的路径引用
|
|
26
|
+
- 资源文件分散存储在 schemas/、constraints/、regex/ 等目录
|
|
27
|
+
- 合并写入时保留原有未覆盖的引用,避免误删
|
|
28
|
+
- 单个引用更新通过 _upsert_manifest_ref 收敛为统一入口
|
|
29
|
+
|
|
30
|
+
输入示例:
|
|
31
|
+
GET /manifest
|
|
32
|
+
PUT /manifest (body: ProjectManifestV2, query: replace=false)
|
|
33
|
+
PUT /manifest/schema (body: SchemaRef)
|
|
34
|
+
|
|
35
|
+
输出示例:
|
|
36
|
+
ProjectManifestV2: 项目清单对象
|
|
37
|
+
StandardResponse: 操作结果消息
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
import logging
|
|
41
|
+
import os
|
|
42
|
+
from pathlib import Path
|
|
43
|
+
|
|
44
|
+
from fastapi import APIRouter, Depends, HTTPException
|
|
45
|
+
|
|
46
|
+
from app.api.dependencies import get_project_config_path
|
|
47
|
+
from app.api.models.v2_responses import ManifestResponse
|
|
48
|
+
from app.shared.core.io.yaml import read_yaml, write_yaml_atomic
|
|
49
|
+
from app.shared.core.project.manifest.types import ConstraintRef, ProjectManifestV2, RegexRef, SchemaRef
|
|
50
|
+
from app.shared.core.project.manifest.types_parts.template import TemplateInstanceRef
|
|
51
|
+
|
|
52
|
+
from .base import (
|
|
53
|
+
StandardResponse,
|
|
54
|
+
_v2_manifest_path,
|
|
55
|
+
)
|
|
56
|
+
from .helpers import project_lock
|
|
57
|
+
|
|
58
|
+
logger = logging.getLogger(__name__)
|
|
59
|
+
|
|
60
|
+
router = APIRouter(prefix="", tags=["Project-Manifest"])
|
|
61
|
+
|
|
62
|
+
# 引用类型联合,用于 _upsert_manifest_ref 的参数类型标注
|
|
63
|
+
_Ref = SchemaRef | ConstraintRef | RegexRef
|
|
64
|
+
|
|
65
|
+
# manifest 中引用列表字段名到资源类型中文标签的映射
|
|
66
|
+
_FIELD_LABEL_MAP: dict[str, str] = {
|
|
67
|
+
"schemas": "Schema",
|
|
68
|
+
"constraints": "Constraint",
|
|
69
|
+
"regex_nodes": "Regex",
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _read_manifest(manifest_path: str) -> ProjectManifestV2 | None:
|
|
74
|
+
"""
|
|
75
|
+
@methoddesc 从文件读取并解析 manifest
|
|
76
|
+
|
|
77
|
+
参数:
|
|
78
|
+
manifest_path: manifest 文件路径
|
|
79
|
+
|
|
80
|
+
返回:
|
|
81
|
+
解析后的 ProjectManifestV2 对象,文件不存在或解析失败返回 None
|
|
82
|
+
"""
|
|
83
|
+
if not os.path.isfile(manifest_path):
|
|
84
|
+
return None
|
|
85
|
+
try:
|
|
86
|
+
existing_data = read_yaml(Path(manifest_path))
|
|
87
|
+
if existing_data:
|
|
88
|
+
return ProjectManifestV2.model_validate(existing_data)
|
|
89
|
+
except Exception:
|
|
90
|
+
logger.exception("读取manifest文件失败: %s", manifest_path)
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _upsert_manifest_ref(
|
|
95
|
+
config_path: str,
|
|
96
|
+
field_name: str,
|
|
97
|
+
ref: _Ref,
|
|
98
|
+
) -> StandardResponse:
|
|
99
|
+
"""
|
|
100
|
+
@methoddesc 在 manifest 中对单个引用执行 upsert(更新或插入)
|
|
101
|
+
|
|
102
|
+
通用入口函数,被 schema / constraint / regex 三个 PUT 端点共用。
|
|
103
|
+
|
|
104
|
+
流程:
|
|
105
|
+
1. 读取现有 manifest 文件
|
|
106
|
+
2. 在指定字段对应的列表中按 id 查找
|
|
107
|
+
3. 已存在则更新 path,不存在则追加
|
|
108
|
+
4. 写回文件
|
|
109
|
+
|
|
110
|
+
参数:
|
|
111
|
+
config_path: 项目配置根目录
|
|
112
|
+
field_name: manifest 中的引用列表字段名(schemas / constraints / regex_nodes)
|
|
113
|
+
ref: 要 upsert 的引用对象(必须包含 id 字段)
|
|
114
|
+
|
|
115
|
+
返回:
|
|
116
|
+
StandardResponse: 操作结果消息
|
|
117
|
+
|
|
118
|
+
抛出:
|
|
119
|
+
HTTPException 404: manifest 文件不存在时
|
|
120
|
+
"""
|
|
121
|
+
label = _FIELD_LABEL_MAP.get(field_name, field_name)
|
|
122
|
+
manifest_path = _v2_manifest_path(config_path)
|
|
123
|
+
|
|
124
|
+
with project_lock(config_path):
|
|
125
|
+
existing_manifest = _read_manifest(manifest_path)
|
|
126
|
+
|
|
127
|
+
if not existing_manifest:
|
|
128
|
+
raise HTTPException(status_code=404, detail="项目配置文件不存在,请先保存项目")
|
|
129
|
+
|
|
130
|
+
# 获取当前引用列表(拷贝一份,避免就地修改原对象的列表)
|
|
131
|
+
items = list(getattr(existing_manifest, field_name, None) or [])
|
|
132
|
+
existing_ids = {item.id for item in items}
|
|
133
|
+
|
|
134
|
+
# upsert: 已存在则替换,不存在则追加
|
|
135
|
+
if ref.id in existing_ids:
|
|
136
|
+
for i, item in enumerate(items):
|
|
137
|
+
if item.id == ref.id:
|
|
138
|
+
items[i] = ref
|
|
139
|
+
break
|
|
140
|
+
else:
|
|
141
|
+
items.append(ref)
|
|
142
|
+
|
|
143
|
+
# 使用 model_copy 避免手动重建整个对象
|
|
144
|
+
updated_manifest = existing_manifest.model_copy(update={field_name: items})
|
|
145
|
+
write_yaml_atomic(Path(manifest_path), updated_manifest.model_dump(exclude_none=True))
|
|
146
|
+
|
|
147
|
+
return StandardResponse(message=f"{label} 引用 '{ref.id}' 已更新")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@router.get(
|
|
151
|
+
"/manifest",
|
|
152
|
+
response_model=ManifestResponse,
|
|
153
|
+
summary="读取项目 V2 清单",
|
|
154
|
+
responses={
|
|
155
|
+
404: {"description": "Manifest 不存在"},
|
|
156
|
+
500: {"description": "服务器内部错误"},
|
|
157
|
+
},
|
|
158
|
+
)
|
|
159
|
+
def get_v2_manifest(config_path: str = Depends(get_project_config_path)) -> ProjectManifestV2:
|
|
160
|
+
"""
|
|
161
|
+
读取当前项目的 V2 清单(project.precis.yaml)。
|
|
162
|
+
|
|
163
|
+
使用场景:
|
|
164
|
+
- 前端获取项目概览时
|
|
165
|
+
- 各资源 API 需要定位资源文件路径时
|
|
166
|
+
- 全量校验时需要获取所有资源引用时
|
|
167
|
+
|
|
168
|
+
副作用:
|
|
169
|
+
- 如果 manifest 文件不存在,返回 404 错误
|
|
170
|
+
|
|
171
|
+
数据流:
|
|
172
|
+
1. 计算 manifest 文件路径
|
|
173
|
+
2. 检查文件是否存在
|
|
174
|
+
3. 读取并解析 YAML 文件
|
|
175
|
+
4. 转换为 Pydantic 模型返回
|
|
176
|
+
|
|
177
|
+
参数:
|
|
178
|
+
config_path: 项目配置根目录(通过 Depends 注入)
|
|
179
|
+
|
|
180
|
+
返回:
|
|
181
|
+
ProjectManifestV2: 项目清单对象,包含 schemas/constraints/regex_nodes 引用列表
|
|
182
|
+
"""
|
|
183
|
+
manifest_path = _v2_manifest_path(config_path)
|
|
184
|
+
if not os.path.isfile(manifest_path):
|
|
185
|
+
raise HTTPException(
|
|
186
|
+
status_code=404,
|
|
187
|
+
detail={
|
|
188
|
+
# §2.1: 结构化错误码统一(与 dependencies.py 的 manifest 缺失出口同构),
|
|
189
|
+
# 前端按 detail.code 识别项目失效并触发自愈
|
|
190
|
+
"code": "PROJECT_NOT_FOUND",
|
|
191
|
+
"message": f"项目配置文件(project.precis.yaml)不存在,请确认项目是否已被移动或删除(配置目录: {manifest_path})",
|
|
192
|
+
"path": str(
|
|
193
|
+
config_path
|
|
194
|
+
), # §2.1: 项目根目录(前端与 activeProjectPath 同口径比对,勿传 manifest 文件路径)
|
|
195
|
+
},
|
|
196
|
+
)
|
|
197
|
+
# B-fix: 清单文件为空/损坏时 model_validate 会抛裸 ValidationError → 无 detail 的 500。
|
|
198
|
+
# 对照 settings.py 的做法捕获并转为带说明的 422,让前端能提示用户修复清单。
|
|
199
|
+
try:
|
|
200
|
+
data = read_yaml(Path(manifest_path))
|
|
201
|
+
return ProjectManifestV2.model_validate(data)
|
|
202
|
+
except Exception as exc:
|
|
203
|
+
logger.exception("解析 manifest 文件失败: %s", manifest_path)
|
|
204
|
+
raise HTTPException(
|
|
205
|
+
status_code=422,
|
|
206
|
+
detail=f"项目清单文件损坏或为空,无法解析: {manifest_path}。"
|
|
207
|
+
f"请修复或回退 project.precis.yaml 后重试。({exc})",
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@router.put(
|
|
212
|
+
"/manifest",
|
|
213
|
+
response_model=StandardResponse,
|
|
214
|
+
summary="写入项目 V2 清单",
|
|
215
|
+
responses={
|
|
216
|
+
400: {"description": "请求参数错误"},
|
|
217
|
+
500: {"description": "服务器内部错误"},
|
|
218
|
+
},
|
|
219
|
+
)
|
|
220
|
+
def put_v2_manifest(
|
|
221
|
+
manifest: ProjectManifestV2, config_path: str = Depends(get_project_config_path), replace: bool = False
|
|
222
|
+
) -> dict[str, str]:
|
|
223
|
+
"""
|
|
224
|
+
写入当前项目的 V2 清单。
|
|
225
|
+
|
|
226
|
+
参数:
|
|
227
|
+
manifest: 项目清单数据
|
|
228
|
+
config_path: 项目配置根目录(通过 Depends 注入)
|
|
229
|
+
replace: 是否完全替换(默认 False,即合并写)
|
|
230
|
+
|
|
231
|
+
当 replace=False(默认)时,使用合并策略:
|
|
232
|
+
- 保留原有的 schemas 中不在当前 manifest 中的引用
|
|
233
|
+
- 保留原有的 constraints 中不在当前 manifest 中的引用
|
|
234
|
+
- 保留原有的 regex_nodes 中不在当前 manifest 中的引用
|
|
235
|
+
|
|
236
|
+
当 replace=True 时,完全替换现有清单。
|
|
237
|
+
|
|
238
|
+
返回:
|
|
239
|
+
StandardResponse: 操作结果消息
|
|
240
|
+
"""
|
|
241
|
+
manifest_path = _v2_manifest_path(config_path)
|
|
242
|
+
|
|
243
|
+
with project_lock(config_path):
|
|
244
|
+
existing_manifest = _read_manifest(manifest_path)
|
|
245
|
+
|
|
246
|
+
if replace or not existing_manifest:
|
|
247
|
+
final_manifest = manifest
|
|
248
|
+
else:
|
|
249
|
+
merged_schemas = manifest.schemas.copy() if manifest.schemas else []
|
|
250
|
+
merged_constraints = manifest.constraints.copy() if manifest.constraints else []
|
|
251
|
+
merged_regex_nodes = manifest.regex_nodes.copy() if manifest.regex_nodes else []
|
|
252
|
+
merged_transforms = manifest.transforms.copy() if manifest.transforms else []
|
|
253
|
+
merged_manual_data = manifest.manual_data.copy() if manifest.manual_data else []
|
|
254
|
+
merged_data_sources = manifest.data_sources.copy() if manifest.data_sources else []
|
|
255
|
+
merged_templates = manifest.templates.copy() if manifest.templates else []
|
|
256
|
+
merged_template_instances = manifest.template_instances.copy() if manifest.template_instances else []
|
|
257
|
+
|
|
258
|
+
existing_schema_ids = {s.id for s in merged_schemas}
|
|
259
|
+
existing_constraint_ids = {c.id for c in merged_constraints}
|
|
260
|
+
existing_regex_ids = {r.id for r in merged_regex_nodes}
|
|
261
|
+
existing_transform_ids = {t.id for t in merged_transforms}
|
|
262
|
+
existing_manual_data_ids = {m.id for m in merged_manual_data}
|
|
263
|
+
existing_data_source_ids = {d.id for d in merged_data_sources}
|
|
264
|
+
existing_template_ids = {t.id for t in merged_templates}
|
|
265
|
+
existing_instance_ids = {ti.id for ti in merged_template_instances}
|
|
266
|
+
|
|
267
|
+
for s in existing_manifest.schemas or []:
|
|
268
|
+
if s.id not in existing_schema_ids:
|
|
269
|
+
merged_schemas.append(s)
|
|
270
|
+
|
|
271
|
+
for c in existing_manifest.constraints or []:
|
|
272
|
+
if c.id not in existing_constraint_ids:
|
|
273
|
+
merged_constraints.append(c)
|
|
274
|
+
|
|
275
|
+
for r in existing_manifest.regex_nodes or []:
|
|
276
|
+
if r.id not in existing_regex_ids:
|
|
277
|
+
merged_regex_nodes.append(r)
|
|
278
|
+
|
|
279
|
+
for t in existing_manifest.transforms or []:
|
|
280
|
+
if t.id not in existing_transform_ids:
|
|
281
|
+
merged_transforms.append(t)
|
|
282
|
+
|
|
283
|
+
for m in existing_manifest.manual_data or []:
|
|
284
|
+
if m.id not in existing_manual_data_ids:
|
|
285
|
+
merged_manual_data.append(m)
|
|
286
|
+
|
|
287
|
+
for d in existing_manifest.data_sources or []:
|
|
288
|
+
if d.id not in existing_data_source_ids:
|
|
289
|
+
merged_data_sources.append(d)
|
|
290
|
+
|
|
291
|
+
for tmpl in existing_manifest.templates or []:
|
|
292
|
+
if tmpl.id not in existing_template_ids:
|
|
293
|
+
merged_templates.append(tmpl)
|
|
294
|
+
|
|
295
|
+
for ti in existing_manifest.template_instances or []:
|
|
296
|
+
if ti.id not in existing_instance_ids:
|
|
297
|
+
merged_template_instances.append(ti)
|
|
298
|
+
|
|
299
|
+
final_manifest = ProjectManifestV2(
|
|
300
|
+
version=manifest.version,
|
|
301
|
+
project=manifest.project,
|
|
302
|
+
settings=manifest.settings,
|
|
303
|
+
schemas=merged_schemas,
|
|
304
|
+
constraints=merged_constraints,
|
|
305
|
+
regex_nodes=merged_regex_nodes,
|
|
306
|
+
transforms=merged_transforms,
|
|
307
|
+
manual_data=merged_manual_data,
|
|
308
|
+
data_sources=merged_data_sources,
|
|
309
|
+
templates=merged_templates,
|
|
310
|
+
template_instances=merged_template_instances,
|
|
311
|
+
patterns_dir=manifest.patterns_dir,
|
|
312
|
+
warnings=manifest.warnings,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
write_yaml_atomic(Path(manifest_path), final_manifest.model_dump(exclude_none=True))
|
|
316
|
+
return {"message": "V2 manifest 已保存。"}
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
@router.put(
|
|
320
|
+
"/manifest/schema",
|
|
321
|
+
response_model=StandardResponse,
|
|
322
|
+
summary="更新 manifest 中单个 Schema 引用",
|
|
323
|
+
responses={
|
|
324
|
+
400: {"description": "请求参数错误"},
|
|
325
|
+
404: {"description": "Manifest 不存在"},
|
|
326
|
+
500: {"description": "服务器内部错误"},
|
|
327
|
+
},
|
|
328
|
+
)
|
|
329
|
+
def update_manifest_schema_ref(
|
|
330
|
+
schema_ref: SchemaRef, config_path: str = Depends(get_project_config_path)
|
|
331
|
+
) -> StandardResponse:
|
|
332
|
+
"""
|
|
333
|
+
更新 manifest 中单个 schema 引用。
|
|
334
|
+
|
|
335
|
+
逻辑:
|
|
336
|
+
- 如果 schema_ref.id 已存在,则更新其 path
|
|
337
|
+
- 如果不存在,则添加新引用
|
|
338
|
+
|
|
339
|
+
参数:
|
|
340
|
+
schema_ref: 单个 schema 引用(id + path)
|
|
341
|
+
config_path: 项目配置根目录
|
|
342
|
+
|
|
343
|
+
返回:
|
|
344
|
+
StandardResponse: 操作结果消息
|
|
345
|
+
"""
|
|
346
|
+
return _upsert_manifest_ref(config_path, "schemas", schema_ref)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
@router.put(
|
|
350
|
+
"/manifest/constraint",
|
|
351
|
+
response_model=StandardResponse,
|
|
352
|
+
summary="更新 manifest 中单个 Constraint 引用",
|
|
353
|
+
responses={
|
|
354
|
+
400: {"description": "请求参数错误"},
|
|
355
|
+
404: {"description": "Manifest 不存在"},
|
|
356
|
+
500: {"description": "服务器内部错误"},
|
|
357
|
+
},
|
|
358
|
+
)
|
|
359
|
+
def update_manifest_constraint_ref(
|
|
360
|
+
constraint_ref: ConstraintRef, config_path: str = Depends(get_project_config_path)
|
|
361
|
+
) -> StandardResponse:
|
|
362
|
+
"""
|
|
363
|
+
更新 manifest 中单个 constraint 引用。
|
|
364
|
+
|
|
365
|
+
逻辑:
|
|
366
|
+
- 如果 constraint_ref.id 已存在,则更新其 path
|
|
367
|
+
- 如果不存在,则添加新引用
|
|
368
|
+
|
|
369
|
+
参数:
|
|
370
|
+
constraint_ref: 单个 constraint 引用(id + path)
|
|
371
|
+
config_path: 项目配置根目录
|
|
372
|
+
|
|
373
|
+
返回:
|
|
374
|
+
StandardResponse: 操作结果消息
|
|
375
|
+
"""
|
|
376
|
+
return _upsert_manifest_ref(config_path, "constraints", constraint_ref)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
@router.put(
|
|
380
|
+
"/manifest/regex",
|
|
381
|
+
response_model=StandardResponse,
|
|
382
|
+
summary="更新 manifest 中单个 Regex 引用",
|
|
383
|
+
responses={
|
|
384
|
+
400: {"description": "请求参数错误"},
|
|
385
|
+
404: {"description": "Manifest 不存在"},
|
|
386
|
+
500: {"description": "服务器内部错误"},
|
|
387
|
+
},
|
|
388
|
+
)
|
|
389
|
+
def update_manifest_regex_ref(
|
|
390
|
+
regex_ref: RegexRef, config_path: str = Depends(get_project_config_path)
|
|
391
|
+
) -> StandardResponse:
|
|
392
|
+
"""
|
|
393
|
+
更新 manifest 中单个 regex 节点引用。
|
|
394
|
+
|
|
395
|
+
逻辑:
|
|
396
|
+
- 如果 regex_ref.id 已存在,则更新其 path
|
|
397
|
+
- 如果不存在,则添加新引用
|
|
398
|
+
|
|
399
|
+
参数:
|
|
400
|
+
regex_ref: 单个 regex 节点引用(id + path)
|
|
401
|
+
config_path: 项目配置根目录
|
|
402
|
+
|
|
403
|
+
返回:
|
|
404
|
+
StandardResponse: 操作结果消息
|
|
405
|
+
"""
|
|
406
|
+
return _upsert_manifest_ref(config_path, "regex_nodes", regex_ref)
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
@router.put(
|
|
410
|
+
"/manifest/template-instance",
|
|
411
|
+
response_model=StandardResponse,
|
|
412
|
+
summary="更新 manifest 中单个 Template Instance 引用",
|
|
413
|
+
responses={
|
|
414
|
+
400: {"description": "请求参数错误"},
|
|
415
|
+
404: {"description": "Manifest 不存在"},
|
|
416
|
+
500: {"description": "服务器内部错误"},
|
|
417
|
+
},
|
|
418
|
+
)
|
|
419
|
+
def update_manifest_template_instance_ref(
|
|
420
|
+
instance_ref: TemplateInstanceRef,
|
|
421
|
+
config_path: str = Depends(get_project_config_path),
|
|
422
|
+
) -> dict[str, str]:
|
|
423
|
+
"""
|
|
424
|
+
更新 manifest 中单个 template_instance 引用。
|
|
425
|
+
|
|
426
|
+
逻辑:
|
|
427
|
+
- 如果 instance_ref.id 已存在,则更新其内容
|
|
428
|
+
- 如果不存在,则添加新引用
|
|
429
|
+
|
|
430
|
+
参数:
|
|
431
|
+
instance_ref: 模板实例引用(id + template_id + enabled)
|
|
432
|
+
config_path: 项目配置根目录
|
|
433
|
+
|
|
434
|
+
返回:
|
|
435
|
+
StandardResponse: 操作结果消息
|
|
436
|
+
"""
|
|
437
|
+
manifest_path = _v2_manifest_path(config_path)
|
|
438
|
+
|
|
439
|
+
with project_lock(config_path):
|
|
440
|
+
existing_manifest = _read_manifest(manifest_path)
|
|
441
|
+
|
|
442
|
+
if not existing_manifest:
|
|
443
|
+
raise HTTPException(status_code=404, detail="项目配置文件不存在,请先保存项目")
|
|
444
|
+
|
|
445
|
+
items = list(existing_manifest.template_instances or [])
|
|
446
|
+
existing_ids = {item.id for item in items}
|
|
447
|
+
|
|
448
|
+
if instance_ref.id in existing_ids:
|
|
449
|
+
for i, item in enumerate(items):
|
|
450
|
+
if item.id == instance_ref.id:
|
|
451
|
+
items[i] = instance_ref
|
|
452
|
+
break
|
|
453
|
+
else:
|
|
454
|
+
items.append(instance_ref)
|
|
455
|
+
|
|
456
|
+
updated_manifest = existing_manifest.model_copy(update={"template_instances": items})
|
|
457
|
+
write_yaml_atomic(Path(manifest_path), updated_manifest.model_dump(exclude_none=True))
|
|
458
|
+
|
|
459
|
+
return {"message": f"TemplateInstance 引用 '{instance_ref.id}' 已更新"}
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
@router.delete(
|
|
463
|
+
"/manifest/template-instance/{instance_id}",
|
|
464
|
+
response_model=StandardResponse,
|
|
465
|
+
summary="删除 manifest 中单个 Template Instance 引用",
|
|
466
|
+
responses={
|
|
467
|
+
404: {"description": "Manifest 不存在"},
|
|
468
|
+
500: {"description": "服务器内部错误"},
|
|
469
|
+
},
|
|
470
|
+
)
|
|
471
|
+
def delete_manifest_template_instance_ref(
|
|
472
|
+
instance_id: str,
|
|
473
|
+
config_path: str = Depends(get_project_config_path),
|
|
474
|
+
) -> dict[str, str]:
|
|
475
|
+
"""
|
|
476
|
+
@methoddesc 删除 manifest 中单个 template_instance 引用(幂等)
|
|
477
|
+
|
|
478
|
+
业务用途:
|
|
479
|
+
- 全量保存对空 template_instances 列表采取"视为未携带、从磁盘合并"的防御
|
|
480
|
+
(防清单误清空),因此"删除实例"必须走本端点同步清引用——否则画布删光
|
|
481
|
+
实例后保存重载,会从磁盘合并出已删引用(幽灵复活)。前端 nodeOps 删除
|
|
482
|
+
templateInstance 节点时调用(节点 id 即 manifest 引用 id)。
|
|
483
|
+
|
|
484
|
+
参数:
|
|
485
|
+
instance_id: 实例唯一标识
|
|
486
|
+
config_path: 项目配置根目录
|
|
487
|
+
|
|
488
|
+
返回:
|
|
489
|
+
StandardResponse: 操作结果消息(引用不存在时同样成功,画布同步场景容错)
|
|
490
|
+
"""
|
|
491
|
+
manifest_path = _v2_manifest_path(config_path)
|
|
492
|
+
|
|
493
|
+
with project_lock(config_path):
|
|
494
|
+
existing_manifest = _read_manifest(manifest_path)
|
|
495
|
+
|
|
496
|
+
if not existing_manifest:
|
|
497
|
+
raise HTTPException(status_code=404, detail="项目配置文件不存在,请先保存项目")
|
|
498
|
+
|
|
499
|
+
items = list(existing_manifest.template_instances or [])
|
|
500
|
+
remaining = [item for item in items if item.id != instance_id]
|
|
501
|
+
if len(remaining) == len(items):
|
|
502
|
+
return {"message": f"TemplateInstance 引用 '{instance_id}' 不存在(视为已删除)"}
|
|
503
|
+
|
|
504
|
+
updated_manifest = existing_manifest.model_copy(update={"template_instances": remaining})
|
|
505
|
+
write_yaml_atomic(Path(manifest_path), updated_manifest.model_dump(exclude_none=True))
|
|
506
|
+
|
|
507
|
+
return {"message": f"TemplateInstance 引用 '{instance_id}' 已删除"}
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
@router.post(
|
|
511
|
+
"/manifest/constraint/deduplicate",
|
|
512
|
+
response_model=StandardResponse,
|
|
513
|
+
summary="去重 manifest 中的 Constraint 引用",
|
|
514
|
+
responses={
|
|
515
|
+
404: {"description": "Manifest 不存在"},
|
|
516
|
+
500: {"description": "服务器内部错误"},
|
|
517
|
+
},
|
|
518
|
+
)
|
|
519
|
+
def deduplicate_constraint_refs(
|
|
520
|
+
config_path: str = Depends(get_project_config_path),
|
|
521
|
+
) -> dict[str, str]:
|
|
522
|
+
"""
|
|
523
|
+
@methoddesc 扫描 manifest.constraints,删除指向同文件但 id 不匹配的重复条目。
|
|
524
|
+
|
|
525
|
+
算法:
|
|
526
|
+
1. 读取所有 constraint 文件,建立 {path -> file.id} 映射
|
|
527
|
+
2. 对每条 ref:
|
|
528
|
+
- 如果存在另一条 ref 指向同 path 且 ref.id 与 file.id 一致("正确条目")
|
|
529
|
+
- 且当前 ref.id 与 file.id 不一致("错误条目")
|
|
530
|
+
- 则删除当前 ref(保留正确条目)
|
|
531
|
+
3. 写回 manifest
|
|
532
|
+
|
|
533
|
+
安全保证:
|
|
534
|
+
- 仅删除 manifest 中多余的 ref,不修改任何约束文件
|
|
535
|
+
- 仅在确实存在"正确条目"时才删除重复,避免误删唯一引用
|
|
536
|
+
- 不影响 schema / regex / transform 等其他类型
|
|
537
|
+
|
|
538
|
+
参数:
|
|
539
|
+
config_path: 项目配置根目录
|
|
540
|
+
|
|
541
|
+
返回:
|
|
542
|
+
StandardResponse: 操作结果消息(含删除数量)
|
|
543
|
+
"""
|
|
544
|
+
manifest_path = _v2_manifest_path(config_path)
|
|
545
|
+
|
|
546
|
+
if not os.path.isfile(manifest_path):
|
|
547
|
+
raise HTTPException(status_code=404, detail="项目配置文件不存在,请先保存项目")
|
|
548
|
+
|
|
549
|
+
with project_lock(config_path):
|
|
550
|
+
manifest = _read_manifest(manifest_path)
|
|
551
|
+
if not manifest:
|
|
552
|
+
raise HTTPException(status_code=404, detail="Manifest 文件无法解析")
|
|
553
|
+
|
|
554
|
+
constraints = list(manifest.constraints or [])
|
|
555
|
+
if not constraints:
|
|
556
|
+
return {"message": "manifest 中无 constraint 引用,无需去重"}
|
|
557
|
+
|
|
558
|
+
# 读取每个 constraint 文件,建立 {normalized_path -> file.id} 映射
|
|
559
|
+
path_to_file_id: dict[str, str] = {}
|
|
560
|
+
for ref in constraints:
|
|
561
|
+
abs_path = os.path.normpath(os.path.join(config_path, ref.path))
|
|
562
|
+
if abs_path in path_to_file_id:
|
|
563
|
+
continue
|
|
564
|
+
if not os.path.isfile(abs_path):
|
|
565
|
+
continue
|
|
566
|
+
try:
|
|
567
|
+
from app.shared.core.project.constraint.reader import load_constraint
|
|
568
|
+
|
|
569
|
+
file_obj = load_constraint(Path(abs_path))
|
|
570
|
+
path_to_file_id[abs_path] = file_obj.id
|
|
571
|
+
except Exception as e:
|
|
572
|
+
logger.warning("[deduplicate_constraint_refs] 读取失败,跳过: %s, 错误: %s", abs_path, e)
|
|
573
|
+
|
|
574
|
+
# 第一遍:标记哪些 (path, ref_id) 是"正确条目"(id 与 file id 匹配)
|
|
575
|
+
correct_keys: set[tuple[str, str]] = set()
|
|
576
|
+
for ref in constraints:
|
|
577
|
+
abs_path = os.path.normpath(os.path.join(config_path, ref.path))
|
|
578
|
+
file_id = path_to_file_id.get(abs_path)
|
|
579
|
+
if file_id and file_id == ref.id:
|
|
580
|
+
correct_keys.add((abs_path, ref.id))
|
|
581
|
+
|
|
582
|
+
# 第二遍:删除不正确的重复 ref(仅当同 path 存在正确条目时)
|
|
583
|
+
to_keep: list[ConstraintRef] = []
|
|
584
|
+
removed: list[str] = []
|
|
585
|
+
for ref in constraints:
|
|
586
|
+
abs_path = os.path.normpath(os.path.join(config_path, ref.path))
|
|
587
|
+
file_id = path_to_file_id.get(abs_path)
|
|
588
|
+
is_correct = file_id == ref.id
|
|
589
|
+
# 当前 ref.id 与 file id 不一致,且同 path 存在 ref.id == file id 的"正确条目"
|
|
590
|
+
is_dup_with_correct = not is_correct and file_id is not None and (abs_path, file_id) in correct_keys
|
|
591
|
+
if is_dup_with_correct:
|
|
592
|
+
removed.append(ref.id)
|
|
593
|
+
continue
|
|
594
|
+
to_keep.append(ref)
|
|
595
|
+
|
|
596
|
+
if removed:
|
|
597
|
+
updated_manifest = manifest.model_copy(update={"constraints": to_keep})
|
|
598
|
+
write_yaml_atomic(Path(manifest_path), updated_manifest.model_dump(exclude_none=True))
|
|
599
|
+
logger.info(
|
|
600
|
+
"[deduplicate_constraint_refs] 已删除 %d 个重复条目: %s",
|
|
601
|
+
len(removed),
|
|
602
|
+
removed,
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
return {"message": (f"已删除 {len(removed)} 个重复条目" if removed else "未发现可去重的重复条目")}
|