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,446 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2026 Precis Team
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
"""
|
|
17
|
+
@fileoverview 数据校验服务层
|
|
18
|
+
|
|
19
|
+
功能概述:
|
|
20
|
+
- 执行完整的数据验证流程,包括格式解析和约束校验
|
|
21
|
+
- 支持两阶段验证:格式解析验证 + 逻辑约束验证
|
|
22
|
+
- 为前端可视化校验和命令行校验提供统一的校验入口
|
|
23
|
+
- 聚合格式错误和约束错误后统一返回
|
|
24
|
+
|
|
25
|
+
架构设计:
|
|
26
|
+
- 阶段一(格式解析):逐表解析原始数据,应用字段类型转换和格式验证
|
|
27
|
+
- 阶段二(逻辑约束):对解析后的数据进行跨表约束校验(外键、唯一性、条件、脚本化约束)
|
|
28
|
+
- 错误聚合:将两阶段的错误合并后统一返回
|
|
29
|
+
|
|
30
|
+
输入示例:
|
|
31
|
+
raw_datasets = {
|
|
32
|
+
"users": pd.DataFrame({
|
|
33
|
+
"id": ["1", "2", "3"],
|
|
34
|
+
"age": ["25", "30", "abc"]
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
schema = DataSetSchema(tables={...}, constraints=[...])
|
|
38
|
+
parsed, errors, details = validate_full_dataset(raw_datasets, schema)
|
|
39
|
+
|
|
40
|
+
输出示例:
|
|
41
|
+
parsed_datasets: 解析后的 DataFrame 字典
|
|
42
|
+
all_errors: 格式错误 + 约束错误列表
|
|
43
|
+
validation_details: 详细校验信息
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
import logging
|
|
47
|
+
import time
|
|
48
|
+
|
|
49
|
+
import pandas as pd
|
|
50
|
+
|
|
51
|
+
from app.shared.core.project.regex.types import RegexNodeFile
|
|
52
|
+
from app.shared.core.project.transform.types import TransformFile
|
|
53
|
+
from app.shared.domain.data_engine import process_dataframe
|
|
54
|
+
from app.shared.domain.dataset_schema import DataSetSchema
|
|
55
|
+
|
|
56
|
+
logger = logging.getLogger(__name__)
|
|
57
|
+
|
|
58
|
+
from .dag import build_transform_dag, execute_transform_dag
|
|
59
|
+
from .extractors import _extract_derived_columns
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def execute_dag_if_needed(
|
|
63
|
+
parsed_datasets: dict[str, pd.DataFrame],
|
|
64
|
+
*,
|
|
65
|
+
transform_files: dict[str, TransformFile] | None = None,
|
|
66
|
+
regex_files: dict[str, RegexNodeFile] | None = None,
|
|
67
|
+
) -> tuple[dict[str, pd.DataFrame], list[dict]]:
|
|
68
|
+
"""
|
|
69
|
+
@methoddesc 按需构建并执行 Transform DAG,返回更新后的 parsed_datasets 与 DAG 错误
|
|
70
|
+
|
|
71
|
+
抽离此函数是为了让分块加载路径能在 concat 全量 parsed 后单独执行 DAG,
|
|
72
|
+
修复分块模式下行变 transform(FilterRows/DropDuplicates/Aggregate 等)
|
|
73
|
+
只在块内生效、结果与全量不一致的正确性问题。
|
|
74
|
+
|
|
75
|
+
参数:
|
|
76
|
+
parsed_datasets: 已解析的 DataFrame 字典
|
|
77
|
+
transform_files: transform 配置文件字典
|
|
78
|
+
regex_files: regex 节点配置文件字典
|
|
79
|
+
|
|
80
|
+
返回:
|
|
81
|
+
二元组 (parsed_datasets, dag_errors):
|
|
82
|
+
- parsed_datasets: 经 DAG 处理后的 DataFrame 字典
|
|
83
|
+
- dag_errors: transform/regex 节点执行失败的错误列表(回归 #6)
|
|
84
|
+
"""
|
|
85
|
+
if not (transform_files or regex_files):
|
|
86
|
+
return parsed_datasets, []
|
|
87
|
+
logger.debug("执行 Transform DAG")
|
|
88
|
+
dag = build_transform_dag(
|
|
89
|
+
transform_files or {},
|
|
90
|
+
set(parsed_datasets.keys()),
|
|
91
|
+
regex_files,
|
|
92
|
+
)
|
|
93
|
+
return execute_transform_dag(dag, parsed_datasets)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def validate_full_dataset(
|
|
97
|
+
raw_datasets: dict[str, pd.DataFrame],
|
|
98
|
+
schema: DataSetSchema,
|
|
99
|
+
allow_unsafe_eval: bool = False,
|
|
100
|
+
table_filter: str | list[str] | None = None,
|
|
101
|
+
transform_files: dict[str, TransformFile] | None = None,
|
|
102
|
+
regex_files: dict[str, RegexNodeFile] | None = None,
|
|
103
|
+
deadline: float | None = None,
|
|
104
|
+
skip_constraints: bool = False,
|
|
105
|
+
stop_on_first_error: bool = False,
|
|
106
|
+
) -> tuple[dict[str, pd.DataFrame], list[dict], dict[str, list[dict]]]:
|
|
107
|
+
"""
|
|
108
|
+
@methoddesc 执行完整的验证流程,包括格式解析和约束校验
|
|
109
|
+
|
|
110
|
+
这是数据校验的主入口函数,执行两阶段验证:
|
|
111
|
+
1. 阶段一:格式解析验证 - 对每个表的原始数据进行类型转换和格式检查
|
|
112
|
+
2. 阶段二:逻辑约束验证 - 对解析后的数据进行业务规则校验
|
|
113
|
+
|
|
114
|
+
参数:
|
|
115
|
+
raw_datasets: 包含原始 DataFrame 的字典,键为表名,值为 pandas DataFrame
|
|
116
|
+
schema: 完整的数据集 Schema 定义,包含表结构和约束配置
|
|
117
|
+
allow_unsafe_eval: 是否允许执行不安全的脚本化约束。默认为 False(安全模式)
|
|
118
|
+
table_filter: 只验证与这些表相关的约束
|
|
119
|
+
deadline: 超时截止时间(time.monotonic 绝对时间),超过此时间后跳过剩余约束
|
|
120
|
+
skip_constraints: 仅执行阶段一(格式解析+派生列+Transform DAG),跳过阶段二约束校验。
|
|
121
|
+
供分块加载路径使用——分块时每块只解析格式,约束在 concat 全量 parsed 后统一执行
|
|
122
|
+
(见 validate_constraints),以避免跨表 ForeignKey 假阳性与跨块 Unique 假阴性。
|
|
123
|
+
|
|
124
|
+
返回:
|
|
125
|
+
元组 (parsed_datasets, all_errors, validation_details)
|
|
126
|
+
- parsed_datasets: 解析后的 DataFrame 字典
|
|
127
|
+
- all_errors: 所有格式错误和逻辑错误的聚合列表
|
|
128
|
+
- validation_details: 校验详情,包含 format_checks 和 constraint_checks
|
|
129
|
+
|
|
130
|
+
使用示例:
|
|
131
|
+
>>> from app.shared.domain.dataset_schema import DataSetSchema
|
|
132
|
+
>>> raw_data = {'users': pd.DataFrame({'id': [1, 2], 'name': ['Alice', 'Bob']})}
|
|
133
|
+
>>> schema = DataSetSchema(...)
|
|
134
|
+
>>> parsed, errors, details = validate_full_dataset(raw_data, schema)
|
|
135
|
+
"""
|
|
136
|
+
# 初始化错误列表和解析结果字典
|
|
137
|
+
# all_errors: 用于存储两阶段产生的所有错误
|
|
138
|
+
# parsed_datasets: 用于存储解析后的DataFrame,供阶段二使用
|
|
139
|
+
all_errors = []
|
|
140
|
+
parsed_datasets: dict[str, pd.DataFrame] = {}
|
|
141
|
+
|
|
142
|
+
# 初始化校验详情
|
|
143
|
+
# validation_details: 用于存储所有校验过程的详细信息
|
|
144
|
+
validation_details: dict[str, list[dict]] = {
|
|
145
|
+
"format_checks": [], # 格式校验信息
|
|
146
|
+
"constraint_checks": [], # 约束校验信息
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
# ========================
|
|
150
|
+
# 阶段一:格式解析验证
|
|
151
|
+
# ========================
|
|
152
|
+
# 逐表遍历原始数据,应用字段类型转换和格式验证
|
|
153
|
+
# process_dataframe 会根据 table_schema 中的字段定义进行:
|
|
154
|
+
# - 类型转换(字符串转日期、数字等)
|
|
155
|
+
# - 格式验证(正则匹配、范围检查等)
|
|
156
|
+
#
|
|
157
|
+
# 【派生列处理说明】
|
|
158
|
+
# 对于 regex 提取的派生列(ColumnSpec.derived_from 不为 None):
|
|
159
|
+
# - 当前版本:从原始数据文件加载时,这些列的数据不存在
|
|
160
|
+
# - 未来版本:需要从缓存文件或通过重新执行 regex 提取来获取派生列数据
|
|
161
|
+
# - 实现思路:
|
|
162
|
+
# 1. 识别 schema 中的派生列(使用 is_derived_column 函数)
|
|
163
|
+
# 2. 从缓存文件或内存中加载派生列数据
|
|
164
|
+
# 3. 将派生列数据合并到 parsed_df 中
|
|
165
|
+
logger.debug("开始阶段一: 格式解析验证")
|
|
166
|
+
for table_id, raw_df in raw_datasets.items():
|
|
167
|
+
# 跳过未在Schema中定义的表(允许数据集包含额外表)
|
|
168
|
+
if table_id not in schema.tables:
|
|
169
|
+
logger.warning(f"数据集中存在未在Schema中定义的表 '{table_id}', 将跳过。")
|
|
170
|
+
continue
|
|
171
|
+
|
|
172
|
+
# 获取表的Schema定义,进行格式解析
|
|
173
|
+
table_schema = schema.tables[table_id]
|
|
174
|
+
# process_dataframe 返回:(解析后的DataFrame, 错误字典列表)
|
|
175
|
+
parsed_df, parsing_errors = process_dataframe(raw_df, table_schema)
|
|
176
|
+
|
|
177
|
+
# 存储解析后的数据,供阶段二约束校验使用(使用 table_id 作为键)
|
|
178
|
+
parsed_datasets[table_id] = parsed_df
|
|
179
|
+
# 将解析错误聚合到总错误列表
|
|
180
|
+
if parsing_errors:
|
|
181
|
+
for item in parsing_errors:
|
|
182
|
+
all_errors.append(
|
|
183
|
+
{
|
|
184
|
+
**item,
|
|
185
|
+
"stage": "format",
|
|
186
|
+
"table": table_id,
|
|
187
|
+
"table_id": table_id,
|
|
188
|
+
"check_type": "FormatValidation",
|
|
189
|
+
"message": item.get("error_message"),
|
|
190
|
+
}
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
# 记录格式校验信息
|
|
194
|
+
validation_details["format_checks"].append(
|
|
195
|
+
{"table": table_id, "error_count": len(parsing_errors), "passed": len(parsing_errors) == 0}
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
logger.debug(f"表 '{table_id}' 解析完成。发现 {len(parsing_errors)} 个格式错误。")
|
|
199
|
+
|
|
200
|
+
# C6 遇错即停:格式校验阶段发现错误即停,不处理剩余表、不进入约束阶段。
|
|
201
|
+
if stop_on_first_error and parsing_errors:
|
|
202
|
+
remaining_tables = len(raw_datasets) - (list(raw_datasets.keys()).index(table_id) + 1)
|
|
203
|
+
all_errors.append(
|
|
204
|
+
{
|
|
205
|
+
"stage": "format",
|
|
206
|
+
"check_type": "ValidationInterrupted",
|
|
207
|
+
"table": table_id,
|
|
208
|
+
"error_type": "ValidationInterrupted",
|
|
209
|
+
"message": (
|
|
210
|
+
f"已按项目设置『遇错即停』停止:表 '{table_id}' 的格式校验发现首个错误,"
|
|
211
|
+
f"剩余 {remaining_tables} 个表及约束校验未执行。"
|
|
212
|
+
),
|
|
213
|
+
}
|
|
214
|
+
)
|
|
215
|
+
logger.info(f"遇错即停:格式阶段在表 '{table_id}' 停止,跳过剩余表与约束校验")
|
|
216
|
+
return parsed_datasets, all_errors, validation_details
|
|
217
|
+
|
|
218
|
+
# ========================
|
|
219
|
+
# 阶段一续:提取派生列
|
|
220
|
+
# ========================
|
|
221
|
+
# 派生列是通过正则表达式等规则从原始列中提取的新列
|
|
222
|
+
# 例如:从邮箱列中提取域名作为派生列
|
|
223
|
+
# 【数据流】在格式解析后、约束校验前执行,确保派生列可用于后续约束
|
|
224
|
+
logger.debug("开始阶段一续: 提取派生列")
|
|
225
|
+
_extract_derived_columns(parsed_datasets, schema, raw_datasets, all_errors)
|
|
226
|
+
|
|
227
|
+
# ========================
|
|
228
|
+
# 阶段一续二:执行 Transform DAG
|
|
229
|
+
# ========================
|
|
230
|
+
# Transform DAG 是有向无环图,用于编排多个 transform 规则的执行顺序
|
|
231
|
+
# 【拓扑排序】根据依赖关系确定执行顺序,确保前置 transform 先执行
|
|
232
|
+
# 只有当存在 transform_files 或 regex_files 时才需要执行
|
|
233
|
+
if transform_files or regex_files:
|
|
234
|
+
logger.debug("开始阶段一续二: 执行 Transform DAG")
|
|
235
|
+
parsed_datasets, dag_errors = execute_dag_if_needed(
|
|
236
|
+
parsed_datasets,
|
|
237
|
+
transform_files=transform_files,
|
|
238
|
+
regex_files=regex_files,
|
|
239
|
+
)
|
|
240
|
+
# 回归 #6: DAG 节点(transform/regex)执行失败必须上报,否则下游约束在未转换数据上
|
|
241
|
+
# "假通过"。把 dag_errors 归入 all_errors(stage="loading",属数据预处理阶段失败)。
|
|
242
|
+
for err in dag_errors:
|
|
243
|
+
err_with_stage = {"stage": "loading", **err}
|
|
244
|
+
all_errors.append(err_with_stage)
|
|
245
|
+
|
|
246
|
+
# ========================
|
|
247
|
+
# 阶段二:逻辑约束验证
|
|
248
|
+
# ========================
|
|
249
|
+
# 对解析后的数据进行业务规则校验
|
|
250
|
+
# 支持的约束类型:
|
|
251
|
+
# - ForeignKeyConstraints: 外键约束(跨表引用完整性)
|
|
252
|
+
# - UniqueConstraint: 唯一性约束(列值不重复)
|
|
253
|
+
# - AllowedValuesConstraint: 允许值约束(枚举值检查)
|
|
254
|
+
# - ConditionalConstraint: 条件约束(基于条件的规则)
|
|
255
|
+
# - ScriptedConstraint: 脚本化约束(自定义脚本逻辑)
|
|
256
|
+
logger.debug("开始阶段二: 逻辑约束验证")
|
|
257
|
+
if skip_constraints:
|
|
258
|
+
# 分块加载路径:每块只解析格式,约束校验在 concat 全量 parsed 后由调用方统一执行
|
|
259
|
+
logger.debug("skip_constraints=True,跳过阶段二约束校验(分块解析模式)")
|
|
260
|
+
return parsed_datasets, all_errors, validation_details
|
|
261
|
+
all_errors, validation_details = validate_constraints(
|
|
262
|
+
parsed_datasets,
|
|
263
|
+
schema,
|
|
264
|
+
all_errors=all_errors,
|
|
265
|
+
validation_details=validation_details,
|
|
266
|
+
allow_unsafe_eval=allow_unsafe_eval,
|
|
267
|
+
table_filter=table_filter,
|
|
268
|
+
deadline=deadline,
|
|
269
|
+
stop_on_first_error=stop_on_first_error,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
# 返回解析后的数据集、所有错误和校验详情
|
|
273
|
+
# 【结果说明】前端可以根据 is_empty(all_errors) 判断验证是否通过
|
|
274
|
+
# validation_details 用于展示所有校验过程的详细信息
|
|
275
|
+
return parsed_datasets, all_errors, validation_details
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def validate_constraints(
|
|
279
|
+
parsed_datasets: dict[str, pd.DataFrame],
|
|
280
|
+
schema: DataSetSchema,
|
|
281
|
+
*,
|
|
282
|
+
all_errors: list[dict] | None = None,
|
|
283
|
+
validation_details: dict[str, list[dict]] | None = None,
|
|
284
|
+
allow_unsafe_eval: bool = False,
|
|
285
|
+
table_filter: str | list[str] | None = None,
|
|
286
|
+
deadline: float | None = None,
|
|
287
|
+
stop_on_first_error: bool = False,
|
|
288
|
+
) -> tuple[list[dict], dict[str, list[dict]]]:
|
|
289
|
+
"""
|
|
290
|
+
@methoddesc 仅执行阶段二(逻辑约束验证),对已解析数据跑所有约束
|
|
291
|
+
|
|
292
|
+
抽离此函数是为了让分块加载路径能在 concat 全量 parsed 后单独重跑约束,
|
|
293
|
+
修复分块模式下跨表 ForeignKey(假阳性)与跨块 Unique(假阴性)的正确性问题。
|
|
294
|
+
|
|
295
|
+
参数:
|
|
296
|
+
parsed_datasets: 已解析的 DataFrame 字典(键为 table_id)
|
|
297
|
+
schema: 完整的数据集 Schema 定义
|
|
298
|
+
all_errors: 已有错误列表(会在此基础上追加),默认新建
|
|
299
|
+
validation_details: 已有校验详情(会在此基础上追加),默认新建
|
|
300
|
+
allow_unsafe_eval: 是否允许执行不安全的脚本化约束
|
|
301
|
+
table_filter: 只验证与这些表相关的约束
|
|
302
|
+
deadline: 超时截止时间
|
|
303
|
+
stop_on_first_error: C6 遇错即停。True 时发现第一个约束错误即停止剩余约束校验,
|
|
304
|
+
追加一条 ValidationInterrupted 标记(供调用方感知中断)。
|
|
305
|
+
|
|
306
|
+
返回:
|
|
307
|
+
元组 (all_errors, validation_details)
|
|
308
|
+
"""
|
|
309
|
+
if all_errors is None:
|
|
310
|
+
all_errors = []
|
|
311
|
+
if validation_details is None:
|
|
312
|
+
validation_details = {"format_checks": [], "constraint_checks": []}
|
|
313
|
+
|
|
314
|
+
# 如果所有表都为空,跳过约束验证以避免无意义计算和边界错误(M3)
|
|
315
|
+
# 【防御性编程】空数据集无需执行约束校验,直接返回
|
|
316
|
+
has_any_data = any(not df.empty for df in parsed_datasets.values())
|
|
317
|
+
if not has_any_data:
|
|
318
|
+
logger.debug("所有解析后的表均为空,跳过阶段二约束验证")
|
|
319
|
+
return all_errors, validation_details
|
|
320
|
+
|
|
321
|
+
# 处理 table_filter 参数(只接受表ID)
|
|
322
|
+
# 【类型适配】支持字符串或列表形式,统一转换为集合便于成员检查
|
|
323
|
+
filter_tables: set | None = None
|
|
324
|
+
if table_filter:
|
|
325
|
+
if isinstance(table_filter, str):
|
|
326
|
+
filter_tables = {table_filter}
|
|
327
|
+
else:
|
|
328
|
+
filter_tables = set(table_filter)
|
|
329
|
+
|
|
330
|
+
# 遍历所有注册的约束,逐个执行校验
|
|
331
|
+
# 【容错设计】单个约束异常不中断整个管线,继续执行后续约束
|
|
332
|
+
for i, constraint in enumerate(schema.constraints):
|
|
333
|
+
# 【超时中断】在每个约束执行前检查是否已超过截止时间
|
|
334
|
+
if deadline is not None and time.monotonic() > deadline:
|
|
335
|
+
logger.warning(f"超时中断:跳过剩余 {len(schema.constraints) - i} 个约束(含约束 {i + 1})")
|
|
336
|
+
all_errors.append(
|
|
337
|
+
{
|
|
338
|
+
"stage": "constraint",
|
|
339
|
+
"check_type": "Timeout",
|
|
340
|
+
"table": None,
|
|
341
|
+
"error_type": "Timeout",
|
|
342
|
+
"message": f"校验超时,剩余 {len(schema.constraints) - i} 个约束未执行",
|
|
343
|
+
}
|
|
344
|
+
)
|
|
345
|
+
break
|
|
346
|
+
|
|
347
|
+
# 获取约束信息,判断是否需要跳过
|
|
348
|
+
try:
|
|
349
|
+
constraint_info = constraint.get_constraint_info()
|
|
350
|
+
except Exception as e:
|
|
351
|
+
# 【异常处理】约束信息获取失败时记录错误并跳过该约束
|
|
352
|
+
logger.error(f"约束 {i + 1} 获取信息失败: {e}")
|
|
353
|
+
validation_details["constraint_checks"].append(
|
|
354
|
+
{
|
|
355
|
+
"constraint_type": constraint.__class__.__name__,
|
|
356
|
+
"table": None,
|
|
357
|
+
"description": "",
|
|
358
|
+
"error_count": 1,
|
|
359
|
+
"passed": False,
|
|
360
|
+
"error": str(e),
|
|
361
|
+
}
|
|
362
|
+
)
|
|
363
|
+
continue
|
|
364
|
+
|
|
365
|
+
constraint_table = constraint_info.get("table")
|
|
366
|
+
|
|
367
|
+
# 如果指定了 table_filter,只验证相关表的约束
|
|
368
|
+
# 【过滤逻辑】当 filter_tables 存在且当前约束的表不在过滤列表中时跳过
|
|
369
|
+
if filter_tables and constraint_table not in filter_tables:
|
|
370
|
+
logger.debug(f"跳过约束 {i + 1} ({constraint.__class__.__name__}): 表 '{constraint_table}' 不在过滤列表中")
|
|
371
|
+
continue
|
|
372
|
+
|
|
373
|
+
# 传递 allow_unsafe_eval 参数以控制脚本约束的执行
|
|
374
|
+
# 【安全策略】当 allow_unsafe_eval=False 时,脚本约束会在安全沙箱中运行
|
|
375
|
+
# 单个约束异常不中断整个管线,继续执行后续约束
|
|
376
|
+
# D6: 透传 deadline,让逐行慢约束(Scripted/Conditional)在循环内部检查超时、提前中断,
|
|
377
|
+
# 而非跑到约束返回后才在循环顶部(:319)检查——否则单个慢约束会让 timeout_seconds 形同虚设。
|
|
378
|
+
try:
|
|
379
|
+
result = constraint.validate(parsed_datasets, allow_unsafe_eval=allow_unsafe_eval, deadline=deadline)
|
|
380
|
+
except Exception as e:
|
|
381
|
+
# 【异常处理】约束执行异常时构造错误结果,不中断后续约束校验
|
|
382
|
+
logger.error(f"约束 {i + 1} ({constraint.__class__.__name__}) 执行异常: {e}")
|
|
383
|
+
result = {
|
|
384
|
+
"errors": [
|
|
385
|
+
{
|
|
386
|
+
"error_type": "ConstraintExecutionError",
|
|
387
|
+
"table": constraint_table,
|
|
388
|
+
"message": f"约束执行异常: {e}",
|
|
389
|
+
}
|
|
390
|
+
],
|
|
391
|
+
"info": constraint_info,
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
# 获取约束错误和约束信息
|
|
395
|
+
constraint_errors = result.get("errors", [])
|
|
396
|
+
constraint_info = result.get("info", {})
|
|
397
|
+
|
|
398
|
+
# 聚合错误
|
|
399
|
+
# 【数据标准化】统一错误字段格式,确保前端能够正确解析
|
|
400
|
+
if constraint_errors:
|
|
401
|
+
for item in constraint_errors:
|
|
402
|
+
all_errors.append(
|
|
403
|
+
{
|
|
404
|
+
**item,
|
|
405
|
+
"stage": item.get("stage") or "constraint",
|
|
406
|
+
"check_type": item.get("check_type")
|
|
407
|
+
or constraint_info.get("constraint_type", constraint.__class__.__name__),
|
|
408
|
+
"table": item.get("table") or constraint_info.get("table"),
|
|
409
|
+
# P0-3: 透出来源约束文件路径(executor 已按 constraint_id 挂载);
|
|
410
|
+
# 格式校验/超时等非约束错误不走此分支,JSON 输出中为 null
|
|
411
|
+
"constraint_file": getattr(constraint, "constraint_file", None),
|
|
412
|
+
}
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
# 记录约束校验信息
|
|
416
|
+
validation_details["constraint_checks"].append(
|
|
417
|
+
{
|
|
418
|
+
"constraint_type": constraint_info.get("constraint_type", constraint.__class__.__name__),
|
|
419
|
+
"table": constraint_info.get("table"),
|
|
420
|
+
"description": constraint_info.get("description", ""),
|
|
421
|
+
"error_count": len(constraint_errors),
|
|
422
|
+
"passed": len(constraint_errors) == 0,
|
|
423
|
+
}
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
logger.debug(
|
|
427
|
+
f"约束 {i + 1} ({constraint.__class__.__name__}) 验证完成。发现 {len(constraint_errors)} 个逻辑错误。"
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
# C6 遇错即停:发现第一个约束错误后立即停止剩余约束(快速失败),追加中断标记。
|
|
431
|
+
# 仿上方 deadline 超时中断模式(:296-307)。constraint_checks 已记录当前约束,故在此 break。
|
|
432
|
+
if stop_on_first_error and constraint_errors:
|
|
433
|
+
remaining = len(schema.constraints) - i - 1
|
|
434
|
+
all_errors.append(
|
|
435
|
+
{
|
|
436
|
+
"stage": "constraint",
|
|
437
|
+
"check_type": "ValidationInterrupted",
|
|
438
|
+
"table": None,
|
|
439
|
+
"error_type": "ValidationInterrupted",
|
|
440
|
+
"message": (f"已按项目设置『遇错即停』停止:发现首个错误,剩余 {remaining} 个约束未执行。"),
|
|
441
|
+
}
|
|
442
|
+
)
|
|
443
|
+
logger.info(f"遇错即停:跳过剩余 {remaining} 个约束(含约束 {i + 2} 起)")
|
|
444
|
+
break
|
|
445
|
+
|
|
446
|
+
return all_errors, validation_details
|