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,276 @@
|
|
|
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
|
+
- 实现 ScriptedConstraint 类
|
|
21
|
+
- 通过 Python 表达式定义自定义验证逻辑
|
|
22
|
+
- 允许执行复杂的业务规则验证
|
|
23
|
+
|
|
24
|
+
架构设计:
|
|
25
|
+
- 继承基类: 继承自 Constraint 抽象类
|
|
26
|
+
- 安全控制: 需要显式开启 allow_unsafe_eval 才能执行
|
|
27
|
+
- 表达式验证: 使用 simpleeval 库执行表达式,提供沙箱环境
|
|
28
|
+
|
|
29
|
+
输入示例:
|
|
30
|
+
# 约束配置 - 验证手机号格式
|
|
31
|
+
constraint1 = ScriptedConstraint(
|
|
32
|
+
table="users",
|
|
33
|
+
name="valid_phone",
|
|
34
|
+
expression="re_match(r'^1[3-9]\\d{9}$', str(value))",
|
|
35
|
+
column="phone"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# 约束配置 - 验证复杂业务规则
|
|
39
|
+
constraint2 = ScriptedConstraint(
|
|
40
|
+
table="orders",
|
|
41
|
+
name="valid_discount",
|
|
42
|
+
expression="value <= row['total_amount'] * 0.5",
|
|
43
|
+
column="discount"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
输出示例:
|
|
47
|
+
# 验证失败
|
|
48
|
+
{
|
|
49
|
+
"errors": [
|
|
50
|
+
{
|
|
51
|
+
"error_type": "ScriptedConstraintViolation",
|
|
52
|
+
"table": "users",
|
|
53
|
+
"row_index": 0,
|
|
54
|
+
"column": "phone",
|
|
55
|
+
"value": "12345",
|
|
56
|
+
"message": "脚本约束冲突: 表达式 're_match(r'^1[3-9]\\d{9}$', str(value))' 验证失败。"
|
|
57
|
+
}
|
|
58
|
+
],
|
|
59
|
+
"info": {...}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
原理说明:
|
|
63
|
+
表达式上下文:
|
|
64
|
+
- value: 当前单元格的值
|
|
65
|
+
- row: 当前行的数据 (字典形式)
|
|
66
|
+
- re_match: 正则匹配函数,接收 pattern 和 string,返回是否匹配
|
|
67
|
+
- len, sum, max, min 等基础函数
|
|
68
|
+
|
|
69
|
+
安全警告:
|
|
70
|
+
- 默认禁用脚本约束,需要设置 allow_unsafe_eval=True
|
|
71
|
+
- simpleeval 提供了比原生 eval() 更严格的沙箱环境
|
|
72
|
+
- 仍需谨慎使用,避免执行恶意代码
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
from __future__ import annotations
|
|
76
|
+
|
|
77
|
+
# 1. 标准库导入
|
|
78
|
+
import logging
|
|
79
|
+
import os
|
|
80
|
+
import re
|
|
81
|
+
import time
|
|
82
|
+
from typing import Any
|
|
83
|
+
|
|
84
|
+
logger = logging.getLogger(__name__)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _resolve_server_allow_unsafe_eval() -> bool:
|
|
88
|
+
"""读取服务端总开关 PRECIS_ALLOW_UNSAFE_EVAL(启动时读一次,运行时不变)。
|
|
89
|
+
|
|
90
|
+
B-sec6: 取值 "1"/"true"/"yes"(不区分大小写)视为开启,其余(含未设置)视为关闭。
|
|
91
|
+
"""
|
|
92
|
+
raw = os.environ.get("PRECIS_ALLOW_UNSAFE_EVAL", "").strip().lower()
|
|
93
|
+
return raw in ("1", "true", "yes")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# 模块级缓存:进程启动时读一次,避免每行约束重复读环境变量。
|
|
97
|
+
_SERVER_ALLOW_UNSAFE_EVAL: bool = _resolve_server_allow_unsafe_eval()
|
|
98
|
+
|
|
99
|
+
# 2. 第三方库导入
|
|
100
|
+
import numpy as np
|
|
101
|
+
import pandas as pd
|
|
102
|
+
from simpleeval import SimpleEval
|
|
103
|
+
|
|
104
|
+
# 3. 项目内部导入
|
|
105
|
+
from app.shared.domain.constraints.base import Constraint
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class ScriptedConstraint(Constraint):
|
|
109
|
+
"""
|
|
110
|
+
@classdesc 脚本化约束
|
|
111
|
+
|
|
112
|
+
通过 Python 表达式定义自定义验证逻辑。
|
|
113
|
+
每行数据独立执行表达式,表达式返回 True 表示通过,False 表示失败。
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
def __init__(self, table: str, name: str, expression: str, column: str | None = None):
|
|
117
|
+
"""
|
|
118
|
+
@methoddesc 初始化脚本化约束
|
|
119
|
+
|
|
120
|
+
参数:
|
|
121
|
+
table: 目标表名
|
|
122
|
+
name: 约束规则名称,用于错误消息中标识
|
|
123
|
+
expression: Python 表达式字符串,在每一行上执行
|
|
124
|
+
column: 目标列名,可选。如果指定,该列的值会作为 value 变量传入表达式
|
|
125
|
+
"""
|
|
126
|
+
self.table = table
|
|
127
|
+
self.name = name
|
|
128
|
+
self.expression = expression
|
|
129
|
+
self.column = column
|
|
130
|
+
|
|
131
|
+
def _get_description(self) -> str:
|
|
132
|
+
"""生成脚本约束描述"""
|
|
133
|
+
return f"脚本约束: {self.table}.{self.name}"
|
|
134
|
+
|
|
135
|
+
def validate(self, datasets: dict[str, pd.DataFrame], **kwargs: Any) -> dict[str, Any]:
|
|
136
|
+
"""
|
|
137
|
+
@methoddesc 执行脚本化验证
|
|
138
|
+
|
|
139
|
+
参数:
|
|
140
|
+
datasets: 数据集字典,键为表名,值为 pandas DataFrame
|
|
141
|
+
**kwargs: 额外的关键字参数,必须包含 allow_unsafe_eval=True 才能执行
|
|
142
|
+
|
|
143
|
+
返回:
|
|
144
|
+
验证结果字典,包含 errors(错误列表)和 info(约束信息)
|
|
145
|
+
|
|
146
|
+
逻辑说明:
|
|
147
|
+
1. 检查 allow_unsafe_eval 是否开启,未开启则跳过
|
|
148
|
+
2. 检查目标表是否存在
|
|
149
|
+
3. 配置 simpleeval 沙箱环境,注册安全的基础函数
|
|
150
|
+
4. 遍历每一行数据,构建表达式执行上下文
|
|
151
|
+
5. 执行表达式并检查结果
|
|
152
|
+
6. 如果结果不是布尔值,记录定义错误
|
|
153
|
+
7. 如果结果为 False,记录业务逻辑违规
|
|
154
|
+
8. 如果表达式执行抛出异常,记录执行错误
|
|
155
|
+
"""
|
|
156
|
+
errors: list[dict[str, Any]] = []
|
|
157
|
+
|
|
158
|
+
# 安全检查: 必须显式开启 allow_unsafe_eval 才能执行脚本约束
|
|
159
|
+
# 【安全加固】使用 is True 严格检查,防止 numpy bool 等意外 truthy 值绕过
|
|
160
|
+
#
|
|
161
|
+
# B-sec6: 请求体的 allow_unsafe_eval 不再单独决定执行权——叠加服务端总开关
|
|
162
|
+
# PRECIS_ALLOW_UNSAFE_EVAL(默认 False)。只有"服务端允许 AND 请求体也开启"
|
|
163
|
+
# 才真正执行。这样即便不可信客户端在请求体里设 true,服务端未授权时仍被拒。
|
|
164
|
+
# 读取缓存的模块级常量,避免每行重复读环境变量。
|
|
165
|
+
client_allowed = kwargs.get("allow_unsafe_eval") is True
|
|
166
|
+
if not (_SERVER_ALLOW_UNSAFE_EVAL and client_allowed):
|
|
167
|
+
errors.append(
|
|
168
|
+
{
|
|
169
|
+
"error_type": "PermissionError",
|
|
170
|
+
"table": self.table,
|
|
171
|
+
"message": f"脚本约束「{self.name}」已跳过:项目设置中的『允许执行脚本 eval』尚未开启。",
|
|
172
|
+
}
|
|
173
|
+
)
|
|
174
|
+
return {"errors": errors, "info": self.get_constraint_info()}
|
|
175
|
+
|
|
176
|
+
# 检查目标表是否存在
|
|
177
|
+
if self.table not in datasets:
|
|
178
|
+
errors.append(
|
|
179
|
+
{
|
|
180
|
+
"error_type": "ConstraintConfigError",
|
|
181
|
+
"table": self.table,
|
|
182
|
+
"column": self.column,
|
|
183
|
+
"message": f"脚本约束失败: 表 '{self.table}' 不在数据集中。",
|
|
184
|
+
}
|
|
185
|
+
)
|
|
186
|
+
return {"errors": errors, "info": self.get_constraint_info()}
|
|
187
|
+
|
|
188
|
+
df = datasets[self.table]
|
|
189
|
+
|
|
190
|
+
# ============================================================================
|
|
191
|
+
# 配置 simpleeval 沙箱环境
|
|
192
|
+
# ============================================================================
|
|
193
|
+
# B-sec5: 复用共享沙箱白名单(app.shared.domain.eval_sandbox),
|
|
194
|
+
# 与 MathExpr 转换保持单一事实源,避免两处白名单漂移。
|
|
195
|
+
from app.shared.domain.eval_sandbox import SAFE_FUNCTIONS
|
|
196
|
+
|
|
197
|
+
base_functions = dict(SAFE_FUNCTIONS)
|
|
198
|
+
|
|
199
|
+
# 遍历每一行数据执行表达式
|
|
200
|
+
# D6: deadline 检查点。逐行约束内部必须有中断点,否则百万行表跑几十秒,
|
|
201
|
+
# timeout_seconds 形同虚设(原 engine 只在约束之间检查)。每 200 行查一次(避免
|
|
202
|
+
# 每行调 time.monotonic 的开销),超时则中断循环并返回 Timeout 标记。
|
|
203
|
+
deadline = kwargs.get("deadline")
|
|
204
|
+
check_interval = 200
|
|
205
|
+
for i, (index, row_dict) in enumerate(df.to_dict("index").items()):
|
|
206
|
+
if deadline is not None and i % check_interval == 0 and time.monotonic() > deadline:
|
|
207
|
+
errors.append(
|
|
208
|
+
{
|
|
209
|
+
"error_type": "Timeout",
|
|
210
|
+
"stage": "constraint",
|
|
211
|
+
"table": self.table,
|
|
212
|
+
"message": (f"脚本约束 '{self.name}' 执行超时,已在第 {i} 行中断,剩余 {len(df) - i} 行未校验。"),
|
|
213
|
+
}
|
|
214
|
+
)
|
|
215
|
+
logger.warning(f"脚本约束 '{self.name}' 超时中断: 已处理 {i}/{len(df)} 行")
|
|
216
|
+
return {"errors": errors, "info": self.get_constraint_info()}
|
|
217
|
+
row_index = int(index) if index is not None else 0
|
|
218
|
+
|
|
219
|
+
# 获取当前单元格的值(如果指定了 column)
|
|
220
|
+
cell_value = row_dict.get(self.column) if self.column else None
|
|
221
|
+
|
|
222
|
+
# 创建 simpleeval 求值器,注入 row 和 value 变量
|
|
223
|
+
evaluator = SimpleEval(names={"row": row_dict, "value": cell_value})
|
|
224
|
+
# 注册基础函数
|
|
225
|
+
evaluator.functions.update(base_functions)
|
|
226
|
+
# 注册正则匹配函数 re_match(pattern, string)
|
|
227
|
+
# §1.9: 对齐 regex 约束 fullmatch 严格语义——原 re.match 是前缀匹配且 $ 容忍尾随换行,
|
|
228
|
+
# 同一模式在两套"匹配"下判定相反
|
|
229
|
+
evaluator.functions["re_match"] = lambda p, s: re.fullmatch(p, s) is not None
|
|
230
|
+
|
|
231
|
+
try:
|
|
232
|
+
# 执行表达式
|
|
233
|
+
result = evaluator.eval(self.expression)
|
|
234
|
+
|
|
235
|
+
# 检查结果类型: 必须是布尔值
|
|
236
|
+
if not isinstance(result, (bool, np.bool_)):
|
|
237
|
+
errors.append(
|
|
238
|
+
{
|
|
239
|
+
"error_type": "ScriptCheckDefinitionError",
|
|
240
|
+
"table": self.table,
|
|
241
|
+
"row_index": int(row_index),
|
|
242
|
+
"message": f"规则 '{self.name}' 的表达式没有返回布尔值(True/False),而是返回了 {type(result).__name__}。",
|
|
243
|
+
}
|
|
244
|
+
)
|
|
245
|
+
continue
|
|
246
|
+
|
|
247
|
+
# 结果为 False 表示验证失败
|
|
248
|
+
if not bool(result):
|
|
249
|
+
errors.append(
|
|
250
|
+
{
|
|
251
|
+
"error_type": "BusinessLogicViolation",
|
|
252
|
+
"table": self.table,
|
|
253
|
+
"row_index": int(row_index),
|
|
254
|
+
"value": f"整行数据: {row_dict}",
|
|
255
|
+
"message": f"业务逻辑检查失败: '{self.name}'",
|
|
256
|
+
}
|
|
257
|
+
)
|
|
258
|
+
except Exception as e:
|
|
259
|
+
# 表达式执行过程中抛出异常
|
|
260
|
+
# 记录详细异常信息到日志,但向用户返回脱敏后的通用错误消息
|
|
261
|
+
logger.warning(
|
|
262
|
+
f"脚本约束执行错误: 规则='{self.name}', 表='{self.table}', "
|
|
263
|
+
f"行={row_index}, 表达式='{self.expression}', 错误={e}",
|
|
264
|
+
exc_info=True,
|
|
265
|
+
)
|
|
266
|
+
errors.append(
|
|
267
|
+
{
|
|
268
|
+
"error_type": "ScriptCheckExecutionError",
|
|
269
|
+
"table": self.table,
|
|
270
|
+
"row_index": int(row_index),
|
|
271
|
+
"message": f"执行规则 '{self.name}' 时发生错误,请检查表达式语法或数据类型。",
|
|
272
|
+
"expression": self.expression,
|
|
273
|
+
}
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
return {"errors": errors, "info": self.get_constraint_info()}
|
|
@@ -0,0 +1,233 @@
|
|
|
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
|
+
- 实现 UniqueConstraint 类
|
|
21
|
+
- 验证列或列组合的值是否唯一
|
|
22
|
+
- 支持单列唯一和联合唯一 (多列组合唯一)
|
|
23
|
+
|
|
24
|
+
架构设计:
|
|
25
|
+
- 继承基类: 继承自 Constraint 抽象类
|
|
26
|
+
- 联合唯一: 支持多列组合的唯一性检查
|
|
27
|
+
- 重复检测: 使用 pandas duplicated() 方法
|
|
28
|
+
|
|
29
|
+
输入示例:
|
|
30
|
+
# 约束配置 - 单列唯一
|
|
31
|
+
constraint1 = UniqueConstraint(table="users", column="email")
|
|
32
|
+
|
|
33
|
+
# 约束配置 - 联合唯一
|
|
34
|
+
constraint2 = UniqueConstraint(
|
|
35
|
+
table="orders",
|
|
36
|
+
columns=["user_id", "product_id"] # 同一用户对同一产品只能下一单
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# 数据集输入
|
|
40
|
+
datasets = {
|
|
41
|
+
"users": DataFrame({
|
|
42
|
+
"id": [1, 2, 3],
|
|
43
|
+
"email": ["alice@test.com", "bob@test.com", "alice@test.com"] # 重复 email
|
|
44
|
+
}),
|
|
45
|
+
"orders": DataFrame({
|
|
46
|
+
"id": [1, 2, 3],
|
|
47
|
+
"user_id": [1, 1, 1],
|
|
48
|
+
"product_id": [100, 100, 100] # 重复 (user_id, product_id) 组合
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
输出示例:
|
|
53
|
+
# 单列唯一性验证失败 (第3行 email 重复)
|
|
54
|
+
{
|
|
55
|
+
"errors": [
|
|
56
|
+
{
|
|
57
|
+
"error_type": "UniqueViolation",
|
|
58
|
+
"table": "users",
|
|
59
|
+
"row_index": 2,
|
|
60
|
+
"columns": ["email"],
|
|
61
|
+
"value": "alice@test.com",
|
|
62
|
+
"message": "唯一性冲突: 值 'alice@test.com' 在列 'email' 中不唯一。"
|
|
63
|
+
}
|
|
64
|
+
],
|
|
65
|
+
"info": {...}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# 联合唯一性验证失败 (第2、3行重复)
|
|
69
|
+
{
|
|
70
|
+
"errors": [
|
|
71
|
+
{
|
|
72
|
+
"error_type": "UniqueViolation",
|
|
73
|
+
"table": "orders",
|
|
74
|
+
"row_index": 1,
|
|
75
|
+
"columns": ["user_id", "product_id"],
|
|
76
|
+
"value": {"user_id": 1, "product_id": 100},
|
|
77
|
+
"message": "唯一性冲突: 值 '{'user_id': 1, 'product_id': 100}' 在列 'user_id, product_id' 中不唯一。"
|
|
78
|
+
}
|
|
79
|
+
],
|
|
80
|
+
"info": {...}
|
|
81
|
+
}
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
from __future__ import annotations
|
|
85
|
+
|
|
86
|
+
# 1. 标准库导入
|
|
87
|
+
from typing import Any
|
|
88
|
+
|
|
89
|
+
# 2. 第三方库导入
|
|
90
|
+
import pandas as pd
|
|
91
|
+
|
|
92
|
+
# 3. 项目内部导入
|
|
93
|
+
from app.shared.domain.constraints.base import Constraint
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class UniqueConstraint(Constraint):
|
|
97
|
+
"""
|
|
98
|
+
@classdesc 唯一性约束
|
|
99
|
+
|
|
100
|
+
验证指定列或列组合中的值是否唯一。
|
|
101
|
+
支持单列唯一(如 email 不重复)和联合唯一(如 user_id + product_id 组合不重复)。
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
def __init__(self, table: str, column: str | list[str]):
|
|
105
|
+
"""
|
|
106
|
+
@methoddesc 初始化唯一性约束
|
|
107
|
+
|
|
108
|
+
参数:
|
|
109
|
+
table: 目标表名
|
|
110
|
+
column: 目标列名,可以是字符串(单列)或字符串列表(联合唯一)
|
|
111
|
+
|
|
112
|
+
抛出:
|
|
113
|
+
ValueError: 当 table 为空、column 为空或列表中包含空值时抛出
|
|
114
|
+
"""
|
|
115
|
+
if not table or not table.strip():
|
|
116
|
+
raise ValueError("table 不能为空")
|
|
117
|
+
|
|
118
|
+
# 统一将 column 转为列表,方便后续处理
|
|
119
|
+
if isinstance(column, str):
|
|
120
|
+
if not column.strip():
|
|
121
|
+
raise ValueError("column 不能为空")
|
|
122
|
+
columns = [column]
|
|
123
|
+
else:
|
|
124
|
+
if not column:
|
|
125
|
+
raise ValueError("column 列表不能为空")
|
|
126
|
+
if not all(c and c.strip() for c in column):
|
|
127
|
+
raise ValueError("column 列表中包含空值")
|
|
128
|
+
columns = list(column)
|
|
129
|
+
|
|
130
|
+
self.table = table
|
|
131
|
+
self.columns = columns
|
|
132
|
+
|
|
133
|
+
def _get_description(self) -> str:
|
|
134
|
+
"""生成唯一性约束描述"""
|
|
135
|
+
columns_str = ", ".join(self.columns) if isinstance(self.columns, list) else self.columns
|
|
136
|
+
return f"唯一性约束: {self.table}.{columns_str}"
|
|
137
|
+
|
|
138
|
+
def validate(self, datasets: dict[str, pd.DataFrame], **kwargs: Any) -> dict[str, Any]:
|
|
139
|
+
"""
|
|
140
|
+
@methoddesc 执行唯一性验证
|
|
141
|
+
|
|
142
|
+
参数:
|
|
143
|
+
datasets: 数据集字典,键为表名,值为 pandas DataFrame
|
|
144
|
+
**kwargs: 额外的关键字参数(本方法不使用)
|
|
145
|
+
|
|
146
|
+
返回:
|
|
147
|
+
验证结果字典,包含 errors(错误列表)和 info(约束信息)
|
|
148
|
+
|
|
149
|
+
逻辑说明:
|
|
150
|
+
1. 检查目标表是否存在于数据集中
|
|
151
|
+
2. 检查 columns 列表是否为空
|
|
152
|
+
3. 检查每个列是否都存在于目标表中
|
|
153
|
+
4. 使用 pandas duplicated() 找出重复的行(keep=False 表示所有重复行都标记)
|
|
154
|
+
5. 为每个重复行生成错误记录
|
|
155
|
+
"""
|
|
156
|
+
errors: list[dict[str, Any]] = []
|
|
157
|
+
|
|
158
|
+
# 检查目标表是否存在
|
|
159
|
+
if self.table not in datasets:
|
|
160
|
+
errors.append(
|
|
161
|
+
{
|
|
162
|
+
"error_type": "ConstraintConfigError",
|
|
163
|
+
"table": self.table,
|
|
164
|
+
"column": self.columns[0] if self.columns else None,
|
|
165
|
+
"message": f"唯一约束失败: 表 '{self.table}' 不在提供的数据集中。",
|
|
166
|
+
}
|
|
167
|
+
)
|
|
168
|
+
return {"errors": errors, "info": self.get_constraint_info()}
|
|
169
|
+
|
|
170
|
+
df = datasets[self.table]
|
|
171
|
+
|
|
172
|
+
# 检查是否指定了列
|
|
173
|
+
if not self.columns:
|
|
174
|
+
errors.append(
|
|
175
|
+
{
|
|
176
|
+
"error_type": "ConstraintConfigError",
|
|
177
|
+
"table": self.table,
|
|
178
|
+
"column": None,
|
|
179
|
+
"message": "唯一约束配置错误: 未指定任何列。",
|
|
180
|
+
}
|
|
181
|
+
)
|
|
182
|
+
return {"errors": errors, "info": self.get_constraint_info()}
|
|
183
|
+
|
|
184
|
+
# 检查每个列是否存在于表中
|
|
185
|
+
for col in self.columns:
|
|
186
|
+
if col not in df.columns:
|
|
187
|
+
errors.append(
|
|
188
|
+
{
|
|
189
|
+
"error_type": "ConstraintConfigError",
|
|
190
|
+
"table": self.table,
|
|
191
|
+
"column": col,
|
|
192
|
+
"message": f"唯一约束失败: 列 '{col}' 在表 '{self.table}' 中不存在。",
|
|
193
|
+
}
|
|
194
|
+
)
|
|
195
|
+
return {"errors": errors, "info": self.get_constraint_info()}
|
|
196
|
+
|
|
197
|
+
# 使用 duplicated() 找出重复的行
|
|
198
|
+
# keep=False 表示将所有重复的行都标记为重复(包括第一次出现的)
|
|
199
|
+
# 回归 #4 修复:SQL 及主流数据质量工具的唯一性语义豁免 NULL。原实现裸调用
|
|
200
|
+
# duplicated(keep=False) 会把 NaN 视为彼此相等,可空唯一列的多个空值全部误报
|
|
201
|
+
# UniqueViolation: 值 'None' 不唯一。这里先剔除"任一唯一列含空值"的行,
|
|
202
|
+
# 使空值组合不再参与重复判定(与 SQL 行为一致)。
|
|
203
|
+
# 空字符串口径对齐: NotNull 与各 process_column 均把 ""/" " 视为空,
|
|
204
|
+
# 唯一约束若把空串当普通值会与其打架(同列一处报"为空"一处报"不唯一")。
|
|
205
|
+
# 故空/全空白字符串同样豁免唯一性判定。astype(str) 对数值列不会产生空串,无副作用。
|
|
206
|
+
null_mask = df[self.columns].isna().any(axis=1)
|
|
207
|
+
blank_mask = (
|
|
208
|
+
df[self.columns].apply(lambda col: col.map(lambda v: isinstance(v, str) and v.strip() == "")).any(axis=1)
|
|
209
|
+
)
|
|
210
|
+
df_non_null = df[~(null_mask | blank_mask)]
|
|
211
|
+
duplicates = df_non_null[df_non_null.duplicated(subset=self.columns, keep=False)]
|
|
212
|
+
|
|
213
|
+
# 遍历所有重复行,生成错误记录
|
|
214
|
+
for row_tuple in duplicates[self.columns].itertuples(index=True, name=None):
|
|
215
|
+
index = row_tuple[0]
|
|
216
|
+
# 如果是多列联合唯一,value 用字典表示;如果是单列,直接用值
|
|
217
|
+
if len(self.columns) > 1:
|
|
218
|
+
value = dict(zip(self.columns, row_tuple[1:]))
|
|
219
|
+
else:
|
|
220
|
+
value = row_tuple[1]
|
|
221
|
+
row_index = int(index) if index is not None else 0
|
|
222
|
+
errors.append(
|
|
223
|
+
{
|
|
224
|
+
"error_type": "UniqueViolation",
|
|
225
|
+
"table": self.table,
|
|
226
|
+
"row_index": row_index,
|
|
227
|
+
"columns": self.columns,
|
|
228
|
+
"value": value,
|
|
229
|
+
"message": f"唯一性冲突: 值 '{value}' 在列 '{', '.join(self.columns)}' 中不唯一。",
|
|
230
|
+
}
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
return {"errors": errors, "info": self.get_constraint_info()}
|