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,325 @@
|
|
|
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 Template CRUD API - 可复用约束模板管理
|
|
18
|
+
|
|
19
|
+
功能概述:
|
|
20
|
+
- 提供模板定义文件的增删改查
|
|
21
|
+
- 提供模板展开预览接口
|
|
22
|
+
- 读取时从 manifest.templates 定位文件
|
|
23
|
+
|
|
24
|
+
架构设计:
|
|
25
|
+
- 读取时从 manifest.templates 中查找引用
|
|
26
|
+
- 写入时自动补齐 manifest 引用,默认路径为 templates/{id}.template.yaml
|
|
27
|
+
- 展开预览不写入文件,仅返回展开结果
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import logging
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
from typing import Any
|
|
35
|
+
|
|
36
|
+
from fastapi import APIRouter, Depends, HTTPException
|
|
37
|
+
from pydantic import BaseModel, Field
|
|
38
|
+
|
|
39
|
+
from app.api.dependencies import get_project_config_path
|
|
40
|
+
from app.api.models.project import StandardResponse
|
|
41
|
+
from app.shared.core.io.yaml import read_yaml, write_yaml_atomic
|
|
42
|
+
from app.shared.core.project.template.expander import expand_template
|
|
43
|
+
from app.shared.core.project.template.reader import load_template, validate_template_structure
|
|
44
|
+
from app.shared.core.project.template.types import TemplateFile
|
|
45
|
+
|
|
46
|
+
from .helpers import _resolve_project_path, _v2_manifest_path, project_lock
|
|
47
|
+
from .manifest import get_v2_manifest
|
|
48
|
+
|
|
49
|
+
logger = logging.getLogger(__name__)
|
|
50
|
+
|
|
51
|
+
router = APIRouter(prefix="", tags=["Project-Template"])
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _find_template_path(template_id: str, config_path: str) -> Path | None:
|
|
55
|
+
"""从 manifest 中查找模板文件路径"""
|
|
56
|
+
manifest = get_v2_manifest(config_path)
|
|
57
|
+
for ref in manifest.templates:
|
|
58
|
+
if ref.id == template_id:
|
|
59
|
+
return Path(_resolve_project_path(config_path, ref.path))
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ============================================================================
|
|
64
|
+
# 请求/响应模型
|
|
65
|
+
# ============================================================================
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class TemplateExpandRequest(BaseModel):
|
|
69
|
+
"""模板展开预览请求"""
|
|
70
|
+
|
|
71
|
+
instance_id: str
|
|
72
|
+
params: dict[str, Any] = Field(default_factory=dict, description="参数绑定值(覆盖模板默认值)")
|
|
73
|
+
input_from_node: str | None = Field(None, description="实例级上游节点 ID(通常指向 Schema)")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class TemplateExpandResponse(BaseModel):
|
|
77
|
+
"""模板展开预览响应"""
|
|
78
|
+
|
|
79
|
+
transforms: list[dict[str, Any]]
|
|
80
|
+
constraints: list[dict[str, Any]]
|
|
81
|
+
regex_nodes: list[dict[str, Any]]
|
|
82
|
+
manual_data: list[dict[str, Any]]
|
|
83
|
+
# 展开失败的节点清单(node_id/kind/message)——单节点失败不阻断预览,
|
|
84
|
+
# 其余节点正常返回,失败信息在此可见(默认空,向后兼容)
|
|
85
|
+
node_errors: list[dict[str, str]] = []
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# ============================================================================
|
|
89
|
+
# CRUD 端点
|
|
90
|
+
# ============================================================================
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@router.get(
|
|
94
|
+
"/template",
|
|
95
|
+
response_model=list[dict[str, Any]],
|
|
96
|
+
summary="列出所有模板定义",
|
|
97
|
+
responses={
|
|
98
|
+
404: {"description": "Manifest 不存在"},
|
|
99
|
+
500: {"description": "服务器内部错误"},
|
|
100
|
+
},
|
|
101
|
+
)
|
|
102
|
+
def list_templates(config_path: str = Depends(get_project_config_path)) -> list[dict[str, Any]]:
|
|
103
|
+
"""列出所有模板定义"""
|
|
104
|
+
manifest = get_v2_manifest(config_path)
|
|
105
|
+
templates = []
|
|
106
|
+
for ref in manifest.templates:
|
|
107
|
+
tmpl_path = Path(_resolve_project_path(config_path, ref.path))
|
|
108
|
+
if tmpl_path.exists():
|
|
109
|
+
try:
|
|
110
|
+
tmpl = load_template(tmpl_path)
|
|
111
|
+
templates.append(
|
|
112
|
+
{
|
|
113
|
+
"id": tmpl.id,
|
|
114
|
+
"name": tmpl.name,
|
|
115
|
+
"description": tmpl.description,
|
|
116
|
+
"node_count": len(tmpl.nodes),
|
|
117
|
+
"path": ref.path,
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
except Exception as e:
|
|
121
|
+
logger.warning(f"模板 '{ref.id}' 加载失败: {e}")
|
|
122
|
+
return templates
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@router.get(
|
|
126
|
+
"/template/{template_id}",
|
|
127
|
+
response_model=dict[str, Any],
|
|
128
|
+
summary="读取指定模板定义",
|
|
129
|
+
responses={
|
|
130
|
+
404: {"description": "模板不存在"},
|
|
131
|
+
500: {"description": "服务器内部错误"},
|
|
132
|
+
},
|
|
133
|
+
)
|
|
134
|
+
def get_template(template_id: str, config_path: str = Depends(get_project_config_path)) -> dict[str, Any]:
|
|
135
|
+
"""读取指定模板定义"""
|
|
136
|
+
tmpl_path = _find_template_path(template_id, config_path)
|
|
137
|
+
if not tmpl_path or not tmpl_path.exists():
|
|
138
|
+
raise HTTPException(status_code=404, detail=f"模板 '{template_id}' 不存在")
|
|
139
|
+
tmpl = load_template(tmpl_path)
|
|
140
|
+
return tmpl.model_dump()
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@router.post(
|
|
144
|
+
"/template",
|
|
145
|
+
response_model=StandardResponse,
|
|
146
|
+
summary="创建模板定义文件",
|
|
147
|
+
responses={
|
|
148
|
+
400: {"description": "请求参数错误"},
|
|
149
|
+
409: {"description": "模板文件已存在"},
|
|
150
|
+
500: {"description": "服务器内部错误"},
|
|
151
|
+
},
|
|
152
|
+
)
|
|
153
|
+
def create_template(template_data: dict, config_path: str = Depends(get_project_config_path)) -> StandardResponse:
|
|
154
|
+
"""创建模板定义文件"""
|
|
155
|
+
project_dir = Path(config_path)
|
|
156
|
+
|
|
157
|
+
# 验证数据
|
|
158
|
+
try:
|
|
159
|
+
tmpl = TemplateFile.model_validate(template_data)
|
|
160
|
+
validate_template_structure(tmpl)
|
|
161
|
+
except Exception as e:
|
|
162
|
+
raise HTTPException(status_code=400, detail=f"模板数据验证失败: {e}")
|
|
163
|
+
|
|
164
|
+
# 确保目录存在
|
|
165
|
+
templates_dir = project_dir / "templates"
|
|
166
|
+
templates_dir.mkdir(parents=True, exist_ok=True)
|
|
167
|
+
|
|
168
|
+
# 写入文件
|
|
169
|
+
# B-sec: 模板 id 直接拼路径且无格式约束,含 ../ 的 id 可将文件写出项目根之外
|
|
170
|
+
# (其余资源端点均有 _resolve_project_path 校验,此处对齐)。id 应为 UUID 等安全标识。
|
|
171
|
+
try:
|
|
172
|
+
file_path = Path(_resolve_project_path(config_path, f"templates/{tmpl.id}.template.yaml"))
|
|
173
|
+
except ValueError as e:
|
|
174
|
+
raise HTTPException(status_code=400, detail=f"模板 id 非法: {e}")
|
|
175
|
+
if file_path.exists():
|
|
176
|
+
raise HTTPException(status_code=409, detail=f"模板文件已存在: {file_path.name}")
|
|
177
|
+
|
|
178
|
+
write_yaml_atomic(file_path, tmpl.model_dump(exclude_none=True))
|
|
179
|
+
|
|
180
|
+
# 更新 manifest
|
|
181
|
+
manifest_path = Path(_v2_manifest_path(config_path))
|
|
182
|
+
with project_lock(config_path):
|
|
183
|
+
manifest_data = read_yaml(manifest_path)
|
|
184
|
+
templates_list = manifest_data.get("templates") or []
|
|
185
|
+
templates_list.append({"id": tmpl.id, "path": f"templates/{tmpl.id}.template.yaml"})
|
|
186
|
+
manifest_data["templates"] = templates_list
|
|
187
|
+
write_yaml_atomic(manifest_path, manifest_data)
|
|
188
|
+
|
|
189
|
+
return StandardResponse(success=True, message=f"模板 '{tmpl.id}' 创建成功")
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@router.put(
|
|
193
|
+
"/template/{template_id}",
|
|
194
|
+
response_model=StandardResponse,
|
|
195
|
+
summary="更新模板定义文件",
|
|
196
|
+
responses={
|
|
197
|
+
400: {"description": "请求参数错误"},
|
|
198
|
+
404: {"description": "模板不存在"},
|
|
199
|
+
500: {"description": "服务器内部错误"},
|
|
200
|
+
},
|
|
201
|
+
)
|
|
202
|
+
def update_template(
|
|
203
|
+
template_id: str,
|
|
204
|
+
template_data: dict,
|
|
205
|
+
config_path: str = Depends(get_project_config_path),
|
|
206
|
+
) -> StandardResponse:
|
|
207
|
+
"""
|
|
208
|
+
@methoddesc 更新模板定义文件
|
|
209
|
+
|
|
210
|
+
业务用途:
|
|
211
|
+
- 覆盖写入 templates/{template_id}.template.yaml
|
|
212
|
+
- 模板不存在时返回 404
|
|
213
|
+
|
|
214
|
+
参数:
|
|
215
|
+
template_id: 模板唯一标识
|
|
216
|
+
template_data: 模板新内容(任意字典)
|
|
217
|
+
config_path: 项目配置路径
|
|
218
|
+
|
|
219
|
+
返回:
|
|
220
|
+
StandardResponse: 写入结果
|
|
221
|
+
"""
|
|
222
|
+
tmpl_path = _find_template_path(template_id, config_path)
|
|
223
|
+
if not tmpl_path or not tmpl_path.exists():
|
|
224
|
+
raise HTTPException(status_code=404, detail=f"模板 '{template_id}' 不存在")
|
|
225
|
+
|
|
226
|
+
# 验证数据
|
|
227
|
+
try:
|
|
228
|
+
tmpl = TemplateFile.model_validate(template_data)
|
|
229
|
+
validate_template_structure(tmpl)
|
|
230
|
+
except Exception as e:
|
|
231
|
+
raise HTTPException(status_code=400, detail=f"模板数据验证失败: {e}")
|
|
232
|
+
|
|
233
|
+
write_yaml_atomic(tmpl_path, tmpl.model_dump(exclude_none=True))
|
|
234
|
+
return StandardResponse(success=True, message=f"模板 '{template_id}' 更新成功")
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
@router.delete(
|
|
238
|
+
"/template/{template_id}",
|
|
239
|
+
response_model=StandardResponse,
|
|
240
|
+
summary="删除模板定义文件",
|
|
241
|
+
responses={
|
|
242
|
+
404: {"description": "模板不存在"},
|
|
243
|
+
500: {"description": "服务器内部错误"},
|
|
244
|
+
},
|
|
245
|
+
)
|
|
246
|
+
def delete_template(template_id: str, config_path: str = Depends(get_project_config_path)) -> StandardResponse:
|
|
247
|
+
"""删除模板定义文件"""
|
|
248
|
+
tmpl_path = _find_template_path(template_id, config_path)
|
|
249
|
+
if not tmpl_path or not tmpl_path.exists():
|
|
250
|
+
raise HTTPException(status_code=404, detail=f"模板 '{template_id}' 不存在")
|
|
251
|
+
|
|
252
|
+
with project_lock(config_path):
|
|
253
|
+
if tmpl_path and tmpl_path.exists():
|
|
254
|
+
tmpl_path.unlink()
|
|
255
|
+
|
|
256
|
+
manifest_path = Path(_v2_manifest_path(config_path))
|
|
257
|
+
manifest_data = read_yaml(manifest_path)
|
|
258
|
+
templates_list = manifest_data.get("templates") or []
|
|
259
|
+
manifest_data["templates"] = [t for t in templates_list if t.get("id") != template_id]
|
|
260
|
+
# 级联清理:删除模板定义后,仍指向它的实例引用会成为永久悬空引用
|
|
261
|
+
# (每次项目加载都产生 TemplateInstanceMissingTemplate 错误),随定义一并移除
|
|
262
|
+
instances_list = manifest_data.get("template_instances") or []
|
|
263
|
+
manifest_data["template_instances"] = [ti for ti in instances_list if ti.get("template_id") != template_id]
|
|
264
|
+
write_yaml_atomic(manifest_path, manifest_data)
|
|
265
|
+
|
|
266
|
+
return StandardResponse(success=True, message=f"模板 '{template_id}' 删除成功")
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
@router.post(
|
|
270
|
+
"/template/{template_id}/expand",
|
|
271
|
+
response_model=TemplateExpandResponse,
|
|
272
|
+
summary="预览模板展开结果",
|
|
273
|
+
responses={
|
|
274
|
+
400: {"description": "请求参数错误或模板展开失败"},
|
|
275
|
+
404: {"description": "模板不存在"},
|
|
276
|
+
500: {"description": "服务器内部错误"},
|
|
277
|
+
},
|
|
278
|
+
)
|
|
279
|
+
def preview_template_expand(
|
|
280
|
+
template_id: str,
|
|
281
|
+
request: TemplateExpandRequest,
|
|
282
|
+
config_path: str = Depends(get_project_config_path),
|
|
283
|
+
) -> TemplateExpandResponse:
|
|
284
|
+
"""
|
|
285
|
+
@methoddesc 预览模板展开结果(不写入文件)
|
|
286
|
+
|
|
287
|
+
业务用途:
|
|
288
|
+
- 在前端"展开前预览"流程中调用
|
|
289
|
+
- 调用后端 expandV2Template 服务计算展开后的 transforms/constraints/regex_nodes,但不落盘
|
|
290
|
+
|
|
291
|
+
参数:
|
|
292
|
+
template_id: 模板唯一标识
|
|
293
|
+
request: 包含 instance_id 的请求
|
|
294
|
+
config_path: 项目配置路径
|
|
295
|
+
|
|
296
|
+
返回:
|
|
297
|
+
TemplateExpandResponse: 展开结果预览
|
|
298
|
+
"""
|
|
299
|
+
tmpl_path = _find_template_path(template_id, config_path)
|
|
300
|
+
if not tmpl_path or not tmpl_path.exists():
|
|
301
|
+
raise HTTPException(status_code=404, detail=f"模板 '{template_id}' 不存在")
|
|
302
|
+
|
|
303
|
+
tmpl = load_template(tmpl_path)
|
|
304
|
+
|
|
305
|
+
logger.debug(f"模板展开请求: template_id={template_id}, instance_id={request.instance_id}")
|
|
306
|
+
try:
|
|
307
|
+
node_errors: list[dict[str, str]] = []
|
|
308
|
+
transforms, constraints, regex_nodes, manual_data_files = expand_template(
|
|
309
|
+
tmpl,
|
|
310
|
+
request.instance_id,
|
|
311
|
+
params=request.params,
|
|
312
|
+
input_from_node=request.input_from_node,
|
|
313
|
+
errors=node_errors,
|
|
314
|
+
)
|
|
315
|
+
except ValueError as e:
|
|
316
|
+
logger.warning(f"模板展开 ValueError: {e}")
|
|
317
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
318
|
+
|
|
319
|
+
return TemplateExpandResponse(
|
|
320
|
+
transforms=[t.model_dump(exclude_none=True) for t in transforms],
|
|
321
|
+
constraints=[c.model_dump(exclude_none=True) for c in constraints],
|
|
322
|
+
regex_nodes=[r.model_dump(exclude_none=True) for r in regex_nodes],
|
|
323
|
+
manual_data=[md.model_dump(exclude_none=True) for md in manual_data_files],
|
|
324
|
+
node_errors=node_errors,
|
|
325
|
+
)
|
|
@@ -0,0 +1,227 @@
|
|
|
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 全量校验 API
|
|
18
|
+
|
|
19
|
+
功能概述:
|
|
20
|
+
- 提供项目全量数据校验的 HTTP 入口
|
|
21
|
+
- 委托核心逻辑给 ValidationExecutor,与 CLI 共享执行层
|
|
22
|
+
- 响应构建下沉到 FullValidationResponseBuilder,保持路由层轻薄
|
|
23
|
+
|
|
24
|
+
架构设计:
|
|
25
|
+
- API 层负责请求校验、参数转换与执行器调用
|
|
26
|
+
- ValidationExecutor 负责加载数据、执行格式检查与约束校验
|
|
27
|
+
- FullValidationResponseBuilder 负责把执行结果转换为 FullValidationResponse
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
import logging
|
|
31
|
+
import os
|
|
32
|
+
import time
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
|
|
35
|
+
from fastapi import APIRouter, Depends, HTTPException
|
|
36
|
+
|
|
37
|
+
from app.api.dependencies import get_project_config_path
|
|
38
|
+
from app.api.services.full_validation_response_builder import FullValidationResponseBuilder
|
|
39
|
+
from app.shared.core.io.yaml import read_yaml
|
|
40
|
+
from app.shared.core.project.manifest.coverage import compute_manifest_coverage, coverage_to_api_dict
|
|
41
|
+
from app.shared.core.project.manifest.types import ProjectManifestV2
|
|
42
|
+
from app.shared.services.validation.executor import ValidationExecutor
|
|
43
|
+
|
|
44
|
+
from ...models import FullValidationRequest, FullValidationResponse
|
|
45
|
+
from .base import _v2_manifest_path
|
|
46
|
+
|
|
47
|
+
logger = logging.getLogger(__name__)
|
|
48
|
+
|
|
49
|
+
router = APIRouter(prefix="", tags=["Project-Validation"])
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _resolve_table_filter_from_file_path(config_path: str, file_path: str) -> str | list[str] | None:
|
|
53
|
+
"""根据数据文件绝对路径反查所属 schema table id。
|
|
54
|
+
|
|
55
|
+
遍历 manifest 的 schemas,解析每个 schema 的 source.path(相对 config_path),
|
|
56
|
+
匹配到的 schema 的 id 作为校验过滤器。
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
config_path: 项目配置根目录
|
|
60
|
+
file_path: 数据文件绝对路径(已归一化)
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
匹配的 table id(单个返回 str,多个返回 list),无匹配返回 None
|
|
64
|
+
"""
|
|
65
|
+
norm_target = os.path.normpath(os.path.abspath(file_path))
|
|
66
|
+
manifest_path = _v2_manifest_path(config_path)
|
|
67
|
+
manifest_data = read_yaml(Path(manifest_path))
|
|
68
|
+
matched: list[str] = []
|
|
69
|
+
for schema_ref in manifest_data.get("schemas", []) or []:
|
|
70
|
+
schema_id = schema_ref.get("id")
|
|
71
|
+
schema_rel = schema_ref.get("path")
|
|
72
|
+
if not schema_id or not schema_rel:
|
|
73
|
+
continue
|
|
74
|
+
schema_abs = os.path.normpath(os.path.join(config_path, schema_rel))
|
|
75
|
+
try:
|
|
76
|
+
schema_data = read_yaml(Path(schema_abs))
|
|
77
|
+
except Exception:
|
|
78
|
+
continue
|
|
79
|
+
source = schema_data.get("source") or {}
|
|
80
|
+
src_path = source.get("path")
|
|
81
|
+
if not src_path:
|
|
82
|
+
continue
|
|
83
|
+
norm_src = os.path.normpath(os.path.abspath(os.path.join(config_path, src_path)))
|
|
84
|
+
if norm_src == norm_target:
|
|
85
|
+
matched.append(schema_id)
|
|
86
|
+
if not matched:
|
|
87
|
+
return None
|
|
88
|
+
return matched[0] if len(matched) == 1 else matched
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@router.post(
|
|
92
|
+
"/validate/full",
|
|
93
|
+
response_model=FullValidationResponse,
|
|
94
|
+
summary="执行项目全量数据校验",
|
|
95
|
+
responses={
|
|
96
|
+
400: {"description": "请求参数错误"},
|
|
97
|
+
404: {"description": "Manifest 不存在"},
|
|
98
|
+
422: {"description": "Manifest 损坏或无法解析"},
|
|
99
|
+
500: {"description": "服务器内部错误"},
|
|
100
|
+
},
|
|
101
|
+
)
|
|
102
|
+
def validate_v2_full(
|
|
103
|
+
request: FullValidationRequest,
|
|
104
|
+
config_path: str = Depends(get_project_config_path),
|
|
105
|
+
) -> FullValidationResponse:
|
|
106
|
+
"""
|
|
107
|
+
执行项目全量数据校验。
|
|
108
|
+
|
|
109
|
+
参数:
|
|
110
|
+
request: 校验请求,包含可选的配置覆盖
|
|
111
|
+
config_path: 项目配置根目录(通过 Depends 注入)
|
|
112
|
+
|
|
113
|
+
返回:
|
|
114
|
+
FullValidationResponse: 校验结果
|
|
115
|
+
"""
|
|
116
|
+
started = time.monotonic()
|
|
117
|
+
logger.info(f"[validate_v2_full] 收到请求,config_path: {config_path}")
|
|
118
|
+
manifest_path = _v2_manifest_path(config_path)
|
|
119
|
+
logger.info(f"[validate_v2_full] manifest_path: {manifest_path}")
|
|
120
|
+
if not os.path.isfile(manifest_path):
|
|
121
|
+
raise HTTPException(
|
|
122
|
+
status_code=404,
|
|
123
|
+
detail={
|
|
124
|
+
# §2.1: 结构化错误码统一(与 dependencies.py 的 manifest 缺失出口同构),
|
|
125
|
+
# 前端按 detail.code 识别项目失效并触发自愈
|
|
126
|
+
"code": "PROJECT_NOT_FOUND",
|
|
127
|
+
"message": f"项目配置文件(project.precis.yaml)不存在,请确认项目是否已被移动或删除(配置目录: {manifest_path})",
|
|
128
|
+
"path": str(
|
|
129
|
+
config_path
|
|
130
|
+
), # §2.1: 项目根目录(前端与 activeProjectPath 同口径比对,勿传 manifest 文件路径)
|
|
131
|
+
},
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# 清单读取与校验必须先于 coverage 计算独立失败(422,与 GET /manifest 同口径):
|
|
135
|
+
# 原实现把清单损坏吞进 coverage 的 try/except 继续执行,返回 summary 全零的
|
|
136
|
+
# 200 空成功,前端只看 summary 会把"清单损坏"误读为"校验通过零错误"。
|
|
137
|
+
try:
|
|
138
|
+
manifest_data = read_yaml(Path(manifest_path))
|
|
139
|
+
manifest_model = ProjectManifestV2.model_validate(manifest_data)
|
|
140
|
+
except Exception as e:
|
|
141
|
+
logger.error(f"[validate_v2_full] 项目清单损坏: {e}")
|
|
142
|
+
raise HTTPException(
|
|
143
|
+
status_code=422,
|
|
144
|
+
detail=f"项目清单文件损坏或为空,无法解析: {manifest_path}。请修复或回退 project.precis.yaml 后重试。({e})",
|
|
145
|
+
) from e
|
|
146
|
+
|
|
147
|
+
coverage_payload = None
|
|
148
|
+
try:
|
|
149
|
+
coverage_payload = coverage_to_api_dict(compute_manifest_coverage(config_path, manifest_model))
|
|
150
|
+
except Exception as e:
|
|
151
|
+
logger.warning(f"[validate_v2_full] 计算 coverage 失败: {e}")
|
|
152
|
+
|
|
153
|
+
# 初始化执行器
|
|
154
|
+
try:
|
|
155
|
+
override_settings = None
|
|
156
|
+
if request.options and request.options.override_settings:
|
|
157
|
+
override_settings = request.options.override_settings
|
|
158
|
+
executor = ValidationExecutor(manifest_path, override_settings)
|
|
159
|
+
except Exception as e:
|
|
160
|
+
logger.error(f"[validate_v2_full] 初始化执行器失败: {e}")
|
|
161
|
+
return FullValidationResponseBuilder(
|
|
162
|
+
executor=None,
|
|
163
|
+
started=started,
|
|
164
|
+
coverage=coverage_payload,
|
|
165
|
+
).build_error(e)
|
|
166
|
+
|
|
167
|
+
builder = FullValidationResponseBuilder(
|
|
168
|
+
executor=executor,
|
|
169
|
+
started=started,
|
|
170
|
+
coverage=coverage_payload,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# 确定数据目录
|
|
174
|
+
data_directory = config_path
|
|
175
|
+
if request.options and request.options.data_directory:
|
|
176
|
+
data_directory = request.options.data_directory
|
|
177
|
+
|
|
178
|
+
# 校验目标解析
|
|
179
|
+
table_filter = None
|
|
180
|
+
if request.target:
|
|
181
|
+
if request.target.type == "single_file":
|
|
182
|
+
file_path = (request.target.file_path or "").strip()
|
|
183
|
+
if not file_path:
|
|
184
|
+
raise HTTPException(status_code=400, detail="单文件校验缺少 file_path")
|
|
185
|
+
# 若传入相对路径,基于 config_path 解析为绝对路径
|
|
186
|
+
if not os.path.isabs(file_path):
|
|
187
|
+
file_path = os.path.normpath(os.path.join(config_path, file_path))
|
|
188
|
+
if not os.path.isfile(file_path):
|
|
189
|
+
raise HTTPException(status_code=404, detail=f"数据文件不存在: {file_path}")
|
|
190
|
+
table_filter = _resolve_table_filter_from_file_path(config_path, file_path)
|
|
191
|
+
if table_filter is None:
|
|
192
|
+
raise HTTPException(
|
|
193
|
+
status_code=404,
|
|
194
|
+
detail=f"未找到引用该文件的 schema: {file_path}",
|
|
195
|
+
)
|
|
196
|
+
if request.target.type == "single_table":
|
|
197
|
+
table_filter = (request.target.table_id or "").strip()
|
|
198
|
+
if not table_filter:
|
|
199
|
+
raise HTTPException(status_code=400, detail="单表校验缺少 table_id")
|
|
200
|
+
|
|
201
|
+
# 执行选项
|
|
202
|
+
validation_settings = executor.settings.validation
|
|
203
|
+
timeout_seconds = int(validation_settings.timeout_seconds or 0)
|
|
204
|
+
if timeout_seconds <= 0:
|
|
205
|
+
timeout_seconds = 30
|
|
206
|
+
|
|
207
|
+
from app.shared.services.validation.executor import ValidationOptions
|
|
208
|
+
|
|
209
|
+
options = ValidationOptions(
|
|
210
|
+
timeout_seconds=timeout_seconds,
|
|
211
|
+
error_handling=validation_settings.error_handling or "continue",
|
|
212
|
+
strict_mode=validation_settings.strict_mode or False,
|
|
213
|
+
table_filter=table_filter,
|
|
214
|
+
allow_unsafe_eval=request.options.allow_unsafe_eval if request.options else None,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
logger.info(f"[validate_v2_full] 开始执行校验,数据目录: {data_directory}")
|
|
218
|
+
|
|
219
|
+
# 执行校验并构建响应
|
|
220
|
+
try:
|
|
221
|
+
result = executor.execute(data_directory, options)
|
|
222
|
+
except Exception as e:
|
|
223
|
+
logger.error(f"[validate_v2_full] 校验执行过程中发生异常: {e}", exc_info=True)
|
|
224
|
+
files_total = len(executor.dataset_schema.tables) if executor.dataset_schema else 0
|
|
225
|
+
return builder.build_error(e, files_total=files_total)
|
|
226
|
+
|
|
227
|
+
return builder.build_from_result(result)
|