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,731 @@
|
|
|
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
|
+
- 实现 DateLogicConstraint 类
|
|
21
|
+
- 支持日期比较和日期计算两种模式
|
|
22
|
+
- 用于验证日期列的逻辑正确性
|
|
23
|
+
|
|
24
|
+
架构设计:
|
|
25
|
+
- 继承基类: 继承自 Constraint 抽象类
|
|
26
|
+
- 两种模式:
|
|
27
|
+
- compare: 日期比较 (大于/小于/等于某个日期)
|
|
28
|
+
- calculation: 日期计算 (计算年龄、天数差等)
|
|
29
|
+
|
|
30
|
+
输入示例:
|
|
31
|
+
# 约束配置 - 比较模式: 出生日期必须 > 1900-01-01
|
|
32
|
+
constraint1 = DateLogicConstraint(
|
|
33
|
+
table="users",
|
|
34
|
+
column="birth_date",
|
|
35
|
+
logic_mode="compare",
|
|
36
|
+
compare_op="gt",
|
|
37
|
+
reference_date="1900-01-01"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# 约束配置 - 比较模式: 结束日期必须 > 开始日期
|
|
41
|
+
constraint2 = DateLogicConstraint(
|
|
42
|
+
table="projects",
|
|
43
|
+
column="end_date",
|
|
44
|
+
logic_mode="compare",
|
|
45
|
+
compare_op="gt",
|
|
46
|
+
reference_column="start_date"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# 约束配置 - 计算模式: 年龄必须 >= 18
|
|
50
|
+
constraint3 = DateLogicConstraint(
|
|
51
|
+
table="users",
|
|
52
|
+
column="birth_date",
|
|
53
|
+
logic_mode="calculation",
|
|
54
|
+
calculation_type="age",
|
|
55
|
+
target_value=18,
|
|
56
|
+
compare_op="gte"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
输出示例:
|
|
60
|
+
# 比较模式验证失败
|
|
61
|
+
{
|
|
62
|
+
"errors": [
|
|
63
|
+
{
|
|
64
|
+
"error_type": "DateLogicViolation",
|
|
65
|
+
"table": "users",
|
|
66
|
+
"row_index": 0,
|
|
67
|
+
"column": "birth_date",
|
|
68
|
+
"value": "1850-01-01",
|
|
69
|
+
"message": "日期逻辑约束冲突: 值 '1850-01-01' 不满足比较条件 (gt 1900-01-01)。"
|
|
70
|
+
}
|
|
71
|
+
],
|
|
72
|
+
"info": {...}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
比较操作符:
|
|
76
|
+
- gt: 大于
|
|
77
|
+
- gte: 大于等于
|
|
78
|
+
- lt: 小于
|
|
79
|
+
- lte: 小于等于
|
|
80
|
+
- eq: 等于
|
|
81
|
+
|
|
82
|
+
计算类型:
|
|
83
|
+
- age: 计算年龄 (当前日期 - 出生日期)
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
from __future__ import annotations
|
|
87
|
+
|
|
88
|
+
# 1. 标准库导入
|
|
89
|
+
from typing import Any
|
|
90
|
+
|
|
91
|
+
# 2. 第三方库导入
|
|
92
|
+
import pandas as pd
|
|
93
|
+
|
|
94
|
+
# 3. 项目内部导入
|
|
95
|
+
from app.shared.domain.constraints.base import Constraint
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class DateLogicConstraint(Constraint):
|
|
99
|
+
"""
|
|
100
|
+
@classdesc 日期逻辑约束
|
|
101
|
+
|
|
102
|
+
验证日期列的逻辑正确性,支持两种模式:
|
|
103
|
+
1. 比较模式: 比较日期与参考日期/参考列的大小关系
|
|
104
|
+
2. 计算模式: 基于日期进行计算(如年龄),再与目标值比较
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
def __init__(
|
|
108
|
+
self,
|
|
109
|
+
table: str,
|
|
110
|
+
column: str,
|
|
111
|
+
logic_mode: str = "compare",
|
|
112
|
+
compare_op: str | None = None,
|
|
113
|
+
reference_date: str | None = None,
|
|
114
|
+
reference_column: str | None = None,
|
|
115
|
+
reference_date_end: str | None = None,
|
|
116
|
+
reference_column_end: str | None = None,
|
|
117
|
+
calculation_type: str | None = None,
|
|
118
|
+
target_value: int | float | str | None = None,
|
|
119
|
+
target_column: str | None = None,
|
|
120
|
+
):
|
|
121
|
+
"""
|
|
122
|
+
@methoddesc 初始化日期逻辑约束
|
|
123
|
+
|
|
124
|
+
参数:
|
|
125
|
+
table: 目标表名
|
|
126
|
+
column: 目标日期列名
|
|
127
|
+
logic_mode: 逻辑模式,"compare"(比较)或 "calculation"(计算)
|
|
128
|
+
compare_op: 比较操作符,可选 gt/gte/lt/lte/eq/range。None 时按模式取默认:
|
|
129
|
+
compare 模式默认 gt;calculation 模式下 age 默认 gte(满 N 岁即通过)、
|
|
130
|
+
days_diff 默认 eq(差值等于目标值通过)。回归 D2: 原统一默认 gt 导致 age==target
|
|
131
|
+
(刚满18岁)与 days_diff==target(对账周期恰好)被误判违规。
|
|
132
|
+
reference_date: 参考日期字符串(比较模式下使用,作为区间起点)
|
|
133
|
+
reference_column: 参考列名(比较模式下使用,与 reference_date 互斥,作为区间起点)
|
|
134
|
+
reference_date_end: 区间终点固定日期(仅 range 模式下使用)
|
|
135
|
+
reference_column_end: 区间终点列名(仅 range 模式下使用)
|
|
136
|
+
calculation_type: 计算类型(计算模式下使用),如 "age" 或 "days_diff"
|
|
137
|
+
target_value: 目标值(计算模式下使用),用于与计算结果比较
|
|
138
|
+
target_column: 目标参考列(计算模式下 days_diff 使用)
|
|
139
|
+
"""
|
|
140
|
+
self.table = table
|
|
141
|
+
self.column = column
|
|
142
|
+
self.logic_mode = logic_mode
|
|
143
|
+
self.compare_op = compare_op
|
|
144
|
+
self.reference_date = reference_date
|
|
145
|
+
self.reference_column = reference_column
|
|
146
|
+
self.reference_date_end = reference_date_end
|
|
147
|
+
self.reference_column_end = reference_column_end
|
|
148
|
+
self.calculation_type = calculation_type
|
|
149
|
+
self.target_value = target_value
|
|
150
|
+
self.target_column = target_column
|
|
151
|
+
|
|
152
|
+
def _get_description(self) -> str:
|
|
153
|
+
"""生成约束描述"""
|
|
154
|
+
desc = f"日期逻辑约束: {self.table}.{self.column}"
|
|
155
|
+
if self.logic_mode == "compare":
|
|
156
|
+
# 回归 D2: compare_op 为 None 时按模式回退(compare→gt)再生成描述
|
|
157
|
+
cmp_op = self.compare_op or "gt"
|
|
158
|
+
if cmp_op == "range":
|
|
159
|
+
start = self.reference_column or self.reference_date
|
|
160
|
+
end = self.reference_column_end or self.reference_date_end
|
|
161
|
+
desc += f" range [{start}, {end}]"
|
|
162
|
+
else:
|
|
163
|
+
ref = self.reference_column or self.reference_date
|
|
164
|
+
desc += f" {cmp_op} {ref}"
|
|
165
|
+
elif self.logic_mode == "calculation":
|
|
166
|
+
desc += f" {self.calculation_type} check"
|
|
167
|
+
return desc
|
|
168
|
+
|
|
169
|
+
def _resolve_compare_boundary(
|
|
170
|
+
self,
|
|
171
|
+
df: pd.DataFrame,
|
|
172
|
+
date_attr: str,
|
|
173
|
+
column_attr: str,
|
|
174
|
+
boundary_name: str = "",
|
|
175
|
+
) -> tuple[Any, list[dict[str, Any]]]:
|
|
176
|
+
"""解析比较模式下的一个边界值(固定日期或列)。
|
|
177
|
+
|
|
178
|
+
返回:
|
|
179
|
+
(边界值, 错误列表)。若存在错误,边界值为 None。
|
|
180
|
+
"""
|
|
181
|
+
errors: list[dict[str, Any]] = []
|
|
182
|
+
date_value = getattr(self, date_attr)
|
|
183
|
+
column_value = getattr(self, column_attr)
|
|
184
|
+
name_prefix = f"{boundary_name}" if boundary_name else ""
|
|
185
|
+
|
|
186
|
+
if column_value:
|
|
187
|
+
if column_value not in df.columns:
|
|
188
|
+
errors.append(
|
|
189
|
+
{
|
|
190
|
+
"error_type": "ConstraintConfigError",
|
|
191
|
+
"table": self.table,
|
|
192
|
+
"column": column_value,
|
|
193
|
+
"message": f"日期逻辑约束失败: {name_prefix}参考列 '{column_value}' 不在表 '{self.table}' 中。",
|
|
194
|
+
}
|
|
195
|
+
)
|
|
196
|
+
return None, errors
|
|
197
|
+
return pd.to_datetime(df[column_value], errors="coerce"), errors
|
|
198
|
+
|
|
199
|
+
if date_value:
|
|
200
|
+
parsed = pd.to_datetime(date_value, errors="coerce")
|
|
201
|
+
if pd.isna(parsed):
|
|
202
|
+
errors.append(
|
|
203
|
+
{
|
|
204
|
+
"error_type": "ConstraintConfigError",
|
|
205
|
+
"table": self.table,
|
|
206
|
+
"message": f"日期逻辑约束失败: 无效的{name_prefix}参考日期 '{date_value}'。",
|
|
207
|
+
}
|
|
208
|
+
)
|
|
209
|
+
return None, errors
|
|
210
|
+
return parsed, errors
|
|
211
|
+
|
|
212
|
+
return None, errors
|
|
213
|
+
|
|
214
|
+
def validate(self, datasets: dict[str, pd.DataFrame], **kwargs: Any) -> dict[str, Any]:
|
|
215
|
+
"""
|
|
216
|
+
@methoddesc 执行日期逻辑验证
|
|
217
|
+
|
|
218
|
+
参数:
|
|
219
|
+
datasets: 数据集字典,键为表名,值为 pandas DataFrame
|
|
220
|
+
**kwargs: 额外的关键字参数(本方法不使用)
|
|
221
|
+
|
|
222
|
+
返回:
|
|
223
|
+
验证结果字典,包含 errors(错误列表)和 info(约束信息)
|
|
224
|
+
"""
|
|
225
|
+
errors: list[dict[str, Any]] = []
|
|
226
|
+
|
|
227
|
+
# 检查目标表是否存在
|
|
228
|
+
if self.table not in datasets:
|
|
229
|
+
errors.append(
|
|
230
|
+
{
|
|
231
|
+
"error_type": "ConstraintConfigError",
|
|
232
|
+
"table": self.table,
|
|
233
|
+
"message": f"日期逻辑约束失败: 表 '{self.table}' 不在数据集中。",
|
|
234
|
+
}
|
|
235
|
+
)
|
|
236
|
+
return {"errors": errors, "info": self.get_constraint_info()}
|
|
237
|
+
|
|
238
|
+
df = datasets[self.table]
|
|
239
|
+
|
|
240
|
+
# 检查目标列是否存在
|
|
241
|
+
if self.column not in df.columns:
|
|
242
|
+
errors.append(
|
|
243
|
+
{
|
|
244
|
+
"error_type": "ConstraintConfigError",
|
|
245
|
+
"table": self.table,
|
|
246
|
+
"column": self.column,
|
|
247
|
+
"message": f"日期逻辑约束失败: 列 '{self.column}' 不在表 '{self.table}' 中。",
|
|
248
|
+
}
|
|
249
|
+
)
|
|
250
|
+
return {"errors": errors, "info": self.get_constraint_info()}
|
|
251
|
+
|
|
252
|
+
# 将目标列转换为 pandas 日期时间类型,无法转换的变为 NaT(Not a Time)
|
|
253
|
+
target_series = pd.to_datetime(df[self.column], errors="coerce")
|
|
254
|
+
|
|
255
|
+
# §1.13: 目标列"非空但解析失败"的行报"日期无效",不再被 mask_valid 静默跳过——
|
|
256
|
+
# 脏值("2024-13-45"/"not_a_date")此前完全不参与该约束的任何检查。真空值豁免。
|
|
257
|
+
self._invalid_date_rows(df, target_series, self.column, errors)
|
|
258
|
+
|
|
259
|
+
# 模式分发:比较/计算两大分支的实现见 _validate_compare / _validate_calculation
|
|
260
|
+
# (extend 而非重新赋值——上面 §1.13 的无效日期条目不能被分支返回值覆盖)
|
|
261
|
+
if self.logic_mode == "compare":
|
|
262
|
+
errors.extend(self._validate_compare(df, target_series))
|
|
263
|
+
elif self.logic_mode == "calculation":
|
|
264
|
+
errors.extend(self._validate_calculation(df, target_series))
|
|
265
|
+
else:
|
|
266
|
+
# 回归: 未识别的 logic_mode 必须报配置错误。原实现 compare/calculation
|
|
267
|
+
# 都不匹配时静默零错误通过(fail-open),拼错的模式名让约束形同虚设。
|
|
268
|
+
# 守卫先例参照 charset.py 的未知 charset_mode 处理。
|
|
269
|
+
errors.append(
|
|
270
|
+
{
|
|
271
|
+
"error_type": "ConstraintConfigError",
|
|
272
|
+
"table": self.table,
|
|
273
|
+
"column": self.column,
|
|
274
|
+
"message": (
|
|
275
|
+
f"日期逻辑约束配置错误: 未知的 logic_mode '{self.logic_mode}',"
|
|
276
|
+
"支持的模式为: compare, calculation。"
|
|
277
|
+
),
|
|
278
|
+
}
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
return {"errors": errors, "info": self.get_constraint_info()}
|
|
282
|
+
|
|
283
|
+
def _invalid_date_rows(
|
|
284
|
+
self,
|
|
285
|
+
df: pd.DataFrame,
|
|
286
|
+
parsed_series: pd.Series,
|
|
287
|
+
source_column: str | None,
|
|
288
|
+
errors: list[dict[str, Any]],
|
|
289
|
+
) -> None:
|
|
290
|
+
"""§1.13: 列引用侧"非空但解析失败"的行报"日期无效"(与目标列同口径,防止只修一侧)。"""
|
|
291
|
+
if not source_column or source_column not in df.columns:
|
|
292
|
+
return
|
|
293
|
+
raw = df[source_column]
|
|
294
|
+
invalid_mask = parsed_series.isna() & raw.notna() & (raw.astype(str).str.strip() != "")
|
|
295
|
+
for idx in df.index[invalid_mask]:
|
|
296
|
+
errors.append(
|
|
297
|
+
{
|
|
298
|
+
"error_type": "DateLogicError",
|
|
299
|
+
"table": self.table,
|
|
300
|
+
"row_index": int(idx),
|
|
301
|
+
"column": source_column,
|
|
302
|
+
"value": str(raw[idx]),
|
|
303
|
+
"message": f"日期无效: 值 '{raw[idx]}' 无法解析为日期。",
|
|
304
|
+
}
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
def _validate_compare(self, df: pd.DataFrame, target_series: pd.Series) -> list[dict[str, Any]]:
|
|
308
|
+
"""
|
|
309
|
+
@methoddesc 比较模式校验:比较目标日期列与参考值(单边界或 range 区间)
|
|
310
|
+
|
|
311
|
+
参数:
|
|
312
|
+
df: 目标表 DataFrame
|
|
313
|
+
target_series: 已转为 datetime 的目标列(无效值为 NaT)
|
|
314
|
+
|
|
315
|
+
返回:
|
|
316
|
+
错误记录列表(配置错误或逐行违规)
|
|
317
|
+
"""
|
|
318
|
+
errors: list[dict[str, Any]] = []
|
|
319
|
+
|
|
320
|
+
# 回归 D2: compare_op 默认 None 时,compare 模式回退到 gt(保持历史行为)。
|
|
321
|
+
# calculation 模式的默认由各计算分支自行解析(age→gte、days_diff→eq)。
|
|
322
|
+
cmp_op = self.compare_op or "gt"
|
|
323
|
+
start_values, start_errors = self._resolve_compare_boundary(df, "reference_date", "reference_column")
|
|
324
|
+
if start_errors:
|
|
325
|
+
errors.extend(start_errors)
|
|
326
|
+
return errors
|
|
327
|
+
# §1.13: 参考列(列引用型)非空但解析失败的行报"参考日期无效"
|
|
328
|
+
if isinstance(start_values, pd.Series):
|
|
329
|
+
self._invalid_date_rows(df, start_values, self.reference_column, errors)
|
|
330
|
+
|
|
331
|
+
# range 模式需要单独解析终点边界
|
|
332
|
+
if cmp_op == "range":
|
|
333
|
+
start_is_date = bool(self.reference_date)
|
|
334
|
+
end_is_date = bool(self.reference_date_end)
|
|
335
|
+
if start_values is None or (start_is_date != end_is_date):
|
|
336
|
+
errors.append(
|
|
337
|
+
{
|
|
338
|
+
"error_type": "ConstraintConfigError",
|
|
339
|
+
"table": self.table,
|
|
340
|
+
"column": self.column,
|
|
341
|
+
"message": "日期逻辑约束失败: range 模式必须同时指定起点和终点,且两者类型一致(同为固定日期或同为列引用)。",
|
|
342
|
+
}
|
|
343
|
+
)
|
|
344
|
+
return errors
|
|
345
|
+
|
|
346
|
+
end_values, end_errors = self._resolve_compare_boundary(
|
|
347
|
+
df, "reference_date_end", "reference_column_end", "终点"
|
|
348
|
+
)
|
|
349
|
+
if end_errors:
|
|
350
|
+
errors.extend(end_errors)
|
|
351
|
+
return errors
|
|
352
|
+
# §1.13: range 终点参考列同样上报无效日期
|
|
353
|
+
if isinstance(end_values, pd.Series):
|
|
354
|
+
self._invalid_date_rows(df, end_values, self.reference_column_end, errors)
|
|
355
|
+
if end_values is None:
|
|
356
|
+
errors.append(
|
|
357
|
+
{
|
|
358
|
+
"error_type": "ConstraintConfigError",
|
|
359
|
+
"table": self.table,
|
|
360
|
+
"column": self.column,
|
|
361
|
+
"message": "日期逻辑约束失败: range 模式必须指定终点(reference_date_end 或 reference_column_end)。",
|
|
362
|
+
}
|
|
363
|
+
)
|
|
364
|
+
return errors
|
|
365
|
+
|
|
366
|
+
# 创建有效值掩码: 目标值、起点、终点都必须是非空的有效日期
|
|
367
|
+
mask_valid = target_series.notna()
|
|
368
|
+
if isinstance(start_values, pd.Series):
|
|
369
|
+
mask_valid &= start_values.notna()
|
|
370
|
+
if isinstance(end_values, pd.Series):
|
|
371
|
+
mask_valid &= end_values.notna()
|
|
372
|
+
|
|
373
|
+
# 闭区间: start <= value <= end
|
|
374
|
+
mask_fail = pd.Series(False, index=df.index, dtype=bool)
|
|
375
|
+
if not isinstance(start_values, pd.Series) and not isinstance(end_values, pd.Series):
|
|
376
|
+
mask_fail[mask_valid] = ~(
|
|
377
|
+
(target_series[mask_valid] >= start_values) & (target_series[mask_valid] <= end_values)
|
|
378
|
+
)
|
|
379
|
+
elif isinstance(start_values, pd.Series) and not isinstance(end_values, pd.Series):
|
|
380
|
+
mask_fail[mask_valid] = ~(
|
|
381
|
+
(target_series[mask_valid] >= start_values[mask_valid]) & (target_series[mask_valid] <= end_values)
|
|
382
|
+
)
|
|
383
|
+
elif not isinstance(start_values, pd.Series) and isinstance(end_values, pd.Series):
|
|
384
|
+
mask_fail[mask_valid] = ~(
|
|
385
|
+
(target_series[mask_valid] >= start_values) & (target_series[mask_valid] <= end_values[mask_valid])
|
|
386
|
+
)
|
|
387
|
+
else:
|
|
388
|
+
mask_fail[mask_valid] = ~(
|
|
389
|
+
(target_series[mask_valid] >= start_values[mask_valid])
|
|
390
|
+
& (target_series[mask_valid] <= end_values[mask_valid])
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
# 收集失败的行并生成错误记录
|
|
394
|
+
failed_indices = df.index[mask_fail]
|
|
395
|
+
for idx in failed_indices:
|
|
396
|
+
val = df.at[idx, self.column]
|
|
397
|
+
start_val = df.at[idx, self.reference_column] if self.reference_column else self.reference_date
|
|
398
|
+
end_val = (
|
|
399
|
+
df.at[idx, self.reference_column_end] if self.reference_column_end else self.reference_date_end
|
|
400
|
+
)
|
|
401
|
+
errors.append(
|
|
402
|
+
{
|
|
403
|
+
"error_type": "DateLogicError",
|
|
404
|
+
"table": self.table,
|
|
405
|
+
"row_index": int(idx),
|
|
406
|
+
"column": self.column,
|
|
407
|
+
"value": str(val),
|
|
408
|
+
"message": f"日期范围校验失败: {val} 不在 [{start_val}, {end_val}] 范围内",
|
|
409
|
+
}
|
|
410
|
+
)
|
|
411
|
+
else:
|
|
412
|
+
# 非 range 模式沿用单边界逻辑
|
|
413
|
+
if start_values is None:
|
|
414
|
+
errors.append(
|
|
415
|
+
{
|
|
416
|
+
"error_type": "ConstraintConfigError",
|
|
417
|
+
"table": self.table,
|
|
418
|
+
"message": "日期逻辑约束失败: 比较模式必须指定 reference_column 或 reference_date。",
|
|
419
|
+
}
|
|
420
|
+
)
|
|
421
|
+
return errors
|
|
422
|
+
|
|
423
|
+
ref_values = start_values
|
|
424
|
+
|
|
425
|
+
# 创建有效值掩码: 目标值和参考值都必须是非空的有效日期
|
|
426
|
+
mask_valid = target_series.notna()
|
|
427
|
+
if isinstance(ref_values, pd.Series):
|
|
428
|
+
mask_valid &= ref_values.notna()
|
|
429
|
+
|
|
430
|
+
# 初始化失败掩码(全 False)
|
|
431
|
+
mask_fail = pd.Series(False, index=df.index, dtype=bool)
|
|
432
|
+
|
|
433
|
+
# 根据比较操作符判断哪些行不满足条件
|
|
434
|
+
# ~ 表示取反,即"不满足比较条件"
|
|
435
|
+
if cmp_op == "gt":
|
|
436
|
+
mask_fail[mask_valid] = ~(
|
|
437
|
+
target_series[mask_valid] > ref_values
|
|
438
|
+
if not isinstance(ref_values, pd.Series)
|
|
439
|
+
else target_series[mask_valid] > ref_values[mask_valid]
|
|
440
|
+
)
|
|
441
|
+
elif cmp_op == "lt":
|
|
442
|
+
mask_fail[mask_valid] = ~(
|
|
443
|
+
target_series[mask_valid] < ref_values
|
|
444
|
+
if not isinstance(ref_values, pd.Series)
|
|
445
|
+
else target_series[mask_valid] < ref_values[mask_valid]
|
|
446
|
+
)
|
|
447
|
+
elif cmp_op == "gte":
|
|
448
|
+
mask_fail[mask_valid] = ~(
|
|
449
|
+
target_series[mask_valid] >= ref_values
|
|
450
|
+
if not isinstance(ref_values, pd.Series)
|
|
451
|
+
else target_series[mask_valid] >= ref_values[mask_valid]
|
|
452
|
+
)
|
|
453
|
+
elif cmp_op == "lte":
|
|
454
|
+
mask_fail[mask_valid] = ~(
|
|
455
|
+
target_series[mask_valid] <= ref_values
|
|
456
|
+
if not isinstance(ref_values, pd.Series)
|
|
457
|
+
else target_series[mask_valid] <= ref_values[mask_valid]
|
|
458
|
+
)
|
|
459
|
+
elif cmp_op == "eq":
|
|
460
|
+
mask_fail[mask_valid] = ~(
|
|
461
|
+
target_series[mask_valid] == ref_values
|
|
462
|
+
if not isinstance(ref_values, pd.Series)
|
|
463
|
+
else target_series[mask_valid] == ref_values[mask_valid]
|
|
464
|
+
)
|
|
465
|
+
else:
|
|
466
|
+
errors.append(
|
|
467
|
+
{
|
|
468
|
+
"error_type": "ConstraintConfigError",
|
|
469
|
+
"table": self.table,
|
|
470
|
+
"column": self.column,
|
|
471
|
+
"message": f"日期逻辑约束失败: 不支持比较操作符 '{cmp_op}',支持的操作符为 gt/gte/lt/lte/eq/range。",
|
|
472
|
+
}
|
|
473
|
+
)
|
|
474
|
+
return errors
|
|
475
|
+
|
|
476
|
+
# 收集失败的行并生成错误记录
|
|
477
|
+
failed_indices = df.index[mask_fail]
|
|
478
|
+
for idx in failed_indices:
|
|
479
|
+
val = df.at[idx, self.column]
|
|
480
|
+
ref_val = df.at[idx, self.reference_column] if self.reference_column else self.reference_date
|
|
481
|
+
errors.append(
|
|
482
|
+
{
|
|
483
|
+
"error_type": "DateLogicError",
|
|
484
|
+
"table": self.table,
|
|
485
|
+
"row_index": int(idx),
|
|
486
|
+
"column": self.column,
|
|
487
|
+
"value": str(val),
|
|
488
|
+
"message": f"日期比较失败: {val} 应该 {cmp_op} {ref_val}",
|
|
489
|
+
}
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
return errors
|
|
493
|
+
|
|
494
|
+
def _validate_calculation(self, df: pd.DataFrame, target_series: pd.Series) -> list[dict[str, Any]]:
|
|
495
|
+
"""
|
|
496
|
+
@methoddesc 计算模式校验:基于日期计算(age 年龄 / days_diff 天数差)后与目标值比较
|
|
497
|
+
|
|
498
|
+
参数:
|
|
499
|
+
df: 目标表 DataFrame
|
|
500
|
+
target_series: 已转为 datetime 的目标列(无效值为 NaT)
|
|
501
|
+
|
|
502
|
+
返回:
|
|
503
|
+
错误记录列表(配置错误或逐行违规)
|
|
504
|
+
"""
|
|
505
|
+
errors: list[dict[str, Any]] = []
|
|
506
|
+
|
|
507
|
+
if self.calculation_type == "age":
|
|
508
|
+
# 年龄计算: 计算从出生日期到参考日期的年龄
|
|
509
|
+
ref_date = pd.Timestamp.now()
|
|
510
|
+
if self.reference_date:
|
|
511
|
+
ref_date = pd.to_datetime(self.reference_date, errors="coerce")
|
|
512
|
+
|
|
513
|
+
if pd.isna(ref_date):
|
|
514
|
+
errors.append(
|
|
515
|
+
{
|
|
516
|
+
"error_type": "ConstraintConfigError",
|
|
517
|
+
"table": self.table,
|
|
518
|
+
"message": "日期计算模式失败: 无效的参考日期。",
|
|
519
|
+
}
|
|
520
|
+
)
|
|
521
|
+
return errors
|
|
522
|
+
|
|
523
|
+
# 只处理非空日期
|
|
524
|
+
mask_valid = target_series.notna()
|
|
525
|
+
|
|
526
|
+
def calculate_age(born: pd.Timestamp) -> int | None:
|
|
527
|
+
"""计算年龄: 年份差,再根据是否过生日调整"""
|
|
528
|
+
if pd.isna(born):
|
|
529
|
+
return None
|
|
530
|
+
# 年份相减,如果还没到今年的生日则再减1
|
|
531
|
+
return int(ref_date.year) - int(born.year) - ((ref_date.month, ref_date.day) < (born.month, born.day))
|
|
532
|
+
|
|
533
|
+
# 对有效日期计算年龄
|
|
534
|
+
ages = target_series[mask_valid].apply(calculate_age)
|
|
535
|
+
|
|
536
|
+
# 与目标值比较
|
|
537
|
+
if self.target_value is not None:
|
|
538
|
+
try:
|
|
539
|
+
target_age = float(self.target_value)
|
|
540
|
+
# §1.12: compare_op 归一大小写 + 未知值报配置错误(原 else 回退 gte 语义,
|
|
541
|
+
# ">" 这类写法静默按另一个语义执行)
|
|
542
|
+
op = str(self.compare_op or "gte").lower()
|
|
543
|
+
if op not in ("gt", "lt", "lte", "eq", "gte"):
|
|
544
|
+
errors.append(
|
|
545
|
+
{
|
|
546
|
+
"error_type": "ConstraintConfigError",
|
|
547
|
+
"table": self.table,
|
|
548
|
+
"column": self.column,
|
|
549
|
+
"message": (
|
|
550
|
+
f"日期计算模式配置错误: 不支持的比较操作符 '{self.compare_op}',"
|
|
551
|
+
"支持的操作符为 gt/gte/lt/lte/eq。"
|
|
552
|
+
),
|
|
553
|
+
}
|
|
554
|
+
)
|
|
555
|
+
return errors
|
|
556
|
+
if op == "gt":
|
|
557
|
+
mask_fail_local = ages <= target_age
|
|
558
|
+
elif op == "lt":
|
|
559
|
+
mask_fail_local = ages >= target_age
|
|
560
|
+
elif op == "lte":
|
|
561
|
+
mask_fail_local = ages > target_age
|
|
562
|
+
elif op == "eq":
|
|
563
|
+
mask_fail_local = ages != target_age
|
|
564
|
+
else:
|
|
565
|
+
mask_fail_local = ages < target_age
|
|
566
|
+
op_desc = {"gt": "大于", "lt": "小于", "gte": "大于等于", "lte": "小于等于", "eq": "等于"}.get(
|
|
567
|
+
op, "大于等于"
|
|
568
|
+
)
|
|
569
|
+
failed_indices = mask_fail_local[mask_fail_local].index
|
|
570
|
+
for idx in failed_indices:
|
|
571
|
+
val = df.at[idx, self.column]
|
|
572
|
+
age = ages[idx]
|
|
573
|
+
errors.append(
|
|
574
|
+
{
|
|
575
|
+
"error_type": "DateLogicError",
|
|
576
|
+
"table": self.table,
|
|
577
|
+
"row_index": int(idx),
|
|
578
|
+
"column": self.column,
|
|
579
|
+
"value": str(val),
|
|
580
|
+
"message": f"年龄检查失败: {val} (年龄 {age}) 不满足条件 ({op_desc} {target_age})",
|
|
581
|
+
}
|
|
582
|
+
)
|
|
583
|
+
except ValueError as e:
|
|
584
|
+
errors.append(
|
|
585
|
+
{
|
|
586
|
+
"error_type": "ConstraintConfigError",
|
|
587
|
+
"table": self.table,
|
|
588
|
+
"column": self.column,
|
|
589
|
+
"message": f"日期计算模式的目标值「{self.target_value}」无法转换为数字({str(e)}),请检查约束配置",
|
|
590
|
+
}
|
|
591
|
+
)
|
|
592
|
+
else:
|
|
593
|
+
# 回归: 缺少 target_value 必须报配置错误。原实现静默零错误通过(fail-open),
|
|
594
|
+
# 用户漏配目标值时误以为年龄约束已生效。
|
|
595
|
+
errors.append(
|
|
596
|
+
{
|
|
597
|
+
"error_type": "ConstraintConfigError",
|
|
598
|
+
"table": self.table,
|
|
599
|
+
"column": self.column,
|
|
600
|
+
"message": "日期计算模式配置错误: calculation_type=age 必须指定 target_value。",
|
|
601
|
+
}
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
elif self.calculation_type == "days_diff":
|
|
605
|
+
# 天数差计算: 计算两个日期列之间的天数差
|
|
606
|
+
if self.target_column and self.target_column not in df.columns:
|
|
607
|
+
errors.append(
|
|
608
|
+
{
|
|
609
|
+
"error_type": "ConstraintConfigError",
|
|
610
|
+
"table": self.table,
|
|
611
|
+
"column": self.target_column,
|
|
612
|
+
"message": f"日期计算模式失败: 参考列 '{self.target_column}' 不在表 '{self.table}' 中。",
|
|
613
|
+
}
|
|
614
|
+
)
|
|
615
|
+
return errors
|
|
616
|
+
|
|
617
|
+
mask_valid = target_series.notna()
|
|
618
|
+
|
|
619
|
+
if self.target_column:
|
|
620
|
+
# 将参考列也转为日期类型
|
|
621
|
+
ref_series = pd.to_datetime(df[self.target_column], errors="coerce")
|
|
622
|
+
# §1.13: 参考列非空但解析失败的行报"日期无效"(与目标列同口径)
|
|
623
|
+
self._invalid_date_rows(df, ref_series, self.target_column, errors)
|
|
624
|
+
# 双方都必须有效
|
|
625
|
+
mask_valid &= ref_series.notna()
|
|
626
|
+
|
|
627
|
+
# 计算天数差(§1.19: 24h 完整天数 float,去掉 .abs() 保留符号——
|
|
628
|
+
# 原实现 .dt.days 截断时间成分(3d1h 算 3 天)且绝对值让 -3 满足 eq 3 双向放行)
|
|
629
|
+
diff_days = (target_series[mask_valid] - ref_series[mask_valid]).dt.total_seconds() / 86400
|
|
630
|
+
|
|
631
|
+
# 与目标值比较(接入 compare_op,过去硬编码 != 导致只能严格等于)
|
|
632
|
+
if self.target_value is not None:
|
|
633
|
+
try:
|
|
634
|
+
expected_diff = float(self.target_value)
|
|
635
|
+
# §1.19: 小数目标值报配置错误(按拍板原文,天数差按完整天语义只接受整数)
|
|
636
|
+
if expected_diff != int(expected_diff):
|
|
637
|
+
raise ValueError(f"天数差目标值 '{self.target_value}' 必须为非负整数天数")
|
|
638
|
+
# §1.12: compare_op 归一大小写 + 未知值报配置错误(原 else 回退 eq 语义)
|
|
639
|
+
op = str(self.compare_op or "eq").lower()
|
|
640
|
+
if op not in ("gt", "lt", "lte", "eq", "gte"):
|
|
641
|
+
errors.append(
|
|
642
|
+
{
|
|
643
|
+
"error_type": "ConstraintConfigError",
|
|
644
|
+
"table": self.table,
|
|
645
|
+
"column": self.column,
|
|
646
|
+
"message": (
|
|
647
|
+
f"日期计算模式配置错误: 不支持的比较操作符 '{self.compare_op}',"
|
|
648
|
+
"支持的操作符为 gt/gte/lt/lte/eq。"
|
|
649
|
+
),
|
|
650
|
+
}
|
|
651
|
+
)
|
|
652
|
+
return errors
|
|
653
|
+
if op == "gt":
|
|
654
|
+
mask_fail_local = diff_days <= expected_diff
|
|
655
|
+
elif op == "lt":
|
|
656
|
+
mask_fail_local = diff_days >= expected_diff
|
|
657
|
+
elif op == "gte":
|
|
658
|
+
mask_fail_local = diff_days < expected_diff
|
|
659
|
+
elif op == "lte":
|
|
660
|
+
mask_fail_local = diff_days > expected_diff
|
|
661
|
+
else:
|
|
662
|
+
mask_fail_local = diff_days != expected_diff
|
|
663
|
+
op_desc = {
|
|
664
|
+
"gt": "大于",
|
|
665
|
+
"lt": "小于",
|
|
666
|
+
"gte": "大于等于",
|
|
667
|
+
"lte": "小于等于",
|
|
668
|
+
"eq": "等于",
|
|
669
|
+
}.get(op, "等于")
|
|
670
|
+
failed_indices = mask_fail_local[mask_fail_local].index
|
|
671
|
+
for idx in failed_indices:
|
|
672
|
+
val = df.at[idx, self.column]
|
|
673
|
+
ref_val = df.at[idx, self.target_column]
|
|
674
|
+
actual = diff_days[idx]
|
|
675
|
+
errors.append(
|
|
676
|
+
{
|
|
677
|
+
"error_type": "DateLogicError",
|
|
678
|
+
"table": self.table,
|
|
679
|
+
"row_index": int(idx),
|
|
680
|
+
"column": self.column,
|
|
681
|
+
"value": str(val),
|
|
682
|
+
"message": f"天数差计算结果与目标不符: {val} vs {ref_val},要求 {op_desc} {expected_diff} 天,实际 {actual} 天",
|
|
683
|
+
}
|
|
684
|
+
)
|
|
685
|
+
except ValueError as e:
|
|
686
|
+
errors.append(
|
|
687
|
+
{
|
|
688
|
+
"error_type": "ConstraintConfigError",
|
|
689
|
+
"table": self.table,
|
|
690
|
+
"column": self.column,
|
|
691
|
+
"message": f"target_value 转换失败: '{self.target_value}' 无法用作天数差目标值 - {str(e)}",
|
|
692
|
+
}
|
|
693
|
+
)
|
|
694
|
+
else:
|
|
695
|
+
# 回归: 缺少 target_value 必须报配置错误(与 age 分支同口径的 fail-closed 守卫)
|
|
696
|
+
errors.append(
|
|
697
|
+
{
|
|
698
|
+
"error_type": "ConstraintConfigError",
|
|
699
|
+
"table": self.table,
|
|
700
|
+
"column": self.column,
|
|
701
|
+
"message": "日期计算模式配置错误: calculation_type=days_diff 必须指定 target_value。",
|
|
702
|
+
}
|
|
703
|
+
)
|
|
704
|
+
else:
|
|
705
|
+
# 回归: 缺少 target_column 必须报配置错误。原实现整段跳过静默零错误通过(fail-open),
|
|
706
|
+
# 与同分支缺 target_value 的守卫(上方 else)及未知 calculation_type 守卫不对称。
|
|
707
|
+
errors.append(
|
|
708
|
+
{
|
|
709
|
+
"error_type": "ConstraintConfigError",
|
|
710
|
+
"table": self.table,
|
|
711
|
+
"column": self.column,
|
|
712
|
+
"message": "日期计算模式配置错误: calculation_type=days_diff 必须指定 target_column(参考列)。",
|
|
713
|
+
}
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
else:
|
|
717
|
+
# 回归: 未识别的 calculation_type 必须报配置错误。原实现 age/days_diff
|
|
718
|
+
# 都不匹配时静默零错误通过(fail-open),拼错的类型名让约束形同虚设。
|
|
719
|
+
errors.append(
|
|
720
|
+
{
|
|
721
|
+
"error_type": "ConstraintConfigError",
|
|
722
|
+
"table": self.table,
|
|
723
|
+
"column": self.column,
|
|
724
|
+
"message": (
|
|
725
|
+
f"日期计算模式配置错误: 未知的 calculation_type '{self.calculation_type}',"
|
|
726
|
+
"支持的类型为: age, days_diff。"
|
|
727
|
+
),
|
|
728
|
+
}
|
|
729
|
+
)
|
|
730
|
+
|
|
731
|
+
return errors
|