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,719 @@
|
|
|
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
|
+
- 定义基础标量数据类型 (Integer, String, Float, Boolean 等)
|
|
21
|
+
- 实现数据类型验证和解析
|
|
22
|
+
|
|
23
|
+
架构设计:
|
|
24
|
+
- 继承基类: 所有类型继承自 DataType 基类
|
|
25
|
+
- validate: 验证值是否符合类型要求
|
|
26
|
+
- parse: 将值转换为目标类型
|
|
27
|
+
|
|
28
|
+
输入示例:
|
|
29
|
+
# 验证各种类型的值
|
|
30
|
+
values = [123, "hello", 3.14, True, "2024-01-01"]
|
|
31
|
+
|
|
32
|
+
# IntegerType 验证
|
|
33
|
+
IntegerType().validate(123) # (True, None)
|
|
34
|
+
IntegerType().validate("abc") # (False, "不是整数")
|
|
35
|
+
|
|
36
|
+
# StringType 验证
|
|
37
|
+
StringType().validate("hello") # (True, None)
|
|
38
|
+
StringType().validate(None) # (False, "值不能为空")
|
|
39
|
+
|
|
40
|
+
# FloatType 验证
|
|
41
|
+
FloatType().validate(3.14) # (True, None)
|
|
42
|
+
FloatType().validate("abc") # (False, "不是浮点数")
|
|
43
|
+
|
|
44
|
+
输出示例:
|
|
45
|
+
# validate() 返回 (is_valid: bool, error_message: str | None)
|
|
46
|
+
(True, None) # 验证通过
|
|
47
|
+
(False, "错误信息") # 验证失败
|
|
48
|
+
|
|
49
|
+
# parse() 返回转换后的值
|
|
50
|
+
IntegerType().parse("123") # 123 (int)
|
|
51
|
+
StringType().parse(123) # "123" (str)
|
|
52
|
+
FloatType().parse("3.14") # 3.14 (float)
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
from __future__ import annotations
|
|
56
|
+
|
|
57
|
+
# 1. 标准库导入
|
|
58
|
+
import re
|
|
59
|
+
from datetime import date
|
|
60
|
+
from decimal import Decimal
|
|
61
|
+
from typing import Any
|
|
62
|
+
|
|
63
|
+
# 2. 第三方库导入
|
|
64
|
+
import numpy as np
|
|
65
|
+
import pandas as pd
|
|
66
|
+
|
|
67
|
+
# 3. 项目内部导入
|
|
68
|
+
from app.shared.domain.data_types_parts.base import DataType
|
|
69
|
+
|
|
70
|
+
# 日期错误建议探测:Y-M-D 三段式(分隔符 -/. 皆可)的取值范围正则
|
|
71
|
+
_YMD_RE = re.compile(r"(\d{4})[-/.](\d{1,2})[-/.](\d{1,2})")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def date_range_hint(value: Any) -> str | None:
|
|
75
|
+
"""对 Y-M-D 形态但取值非法的日期给出定位建议(哪一段超范围)。
|
|
76
|
+
|
|
77
|
+
§1.21 宽松解析下斜线/点分等布局会被当作合法日期接受(不报错),
|
|
78
|
+
真正报错的是"形似日期但月/日取值非法"的值——对这类值指出
|
|
79
|
+
超范围字段比笼统的"不是有效日期"更可操作。非该形态返回 None。
|
|
80
|
+
"""
|
|
81
|
+
import calendar
|
|
82
|
+
from datetime import date as _date
|
|
83
|
+
|
|
84
|
+
text = str(value).strip()
|
|
85
|
+
m = _YMD_RE.fullmatch(text)
|
|
86
|
+
if not m:
|
|
87
|
+
return None
|
|
88
|
+
year, month, day = int(m[1]), int(m[2]), int(m[3])
|
|
89
|
+
if not 1 <= month <= 12:
|
|
90
|
+
return f"月份取值 {month} 超出 1-12,请核对数据。"
|
|
91
|
+
if not 1 <= day <= 31:
|
|
92
|
+
return f"日取值 {day} 超出 1-31,请核对数据。"
|
|
93
|
+
try:
|
|
94
|
+
_date(year, month, day)
|
|
95
|
+
except ValueError:
|
|
96
|
+
last_day = calendar.monthrange(year, month)[1]
|
|
97
|
+
return f"{year} 年 {month} 月只有 {last_day} 天,请核对数据。"
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class IntegerType(DataType):
|
|
102
|
+
"""
|
|
103
|
+
@classdesc 整数类型
|
|
104
|
+
|
|
105
|
+
验证和解析整数值。
|
|
106
|
+
支持正整数、负整数和零,格式要求为纯数字(可选前导负号)。
|
|
107
|
+
|
|
108
|
+
使用场景:
|
|
109
|
+
- 用户 ID、订单号等整数字段
|
|
110
|
+
- 年龄、数量等数值字段
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
_int_pattern = re.compile(r"^-?\d+$")
|
|
114
|
+
|
|
115
|
+
def validate(self, value: Any) -> tuple[bool, Any]:
|
|
116
|
+
"""
|
|
117
|
+
@methoddesc 验证值是否为整数
|
|
118
|
+
|
|
119
|
+
使用正则表达式匹配纯数字格式,并尝试转换为 int 类型。
|
|
120
|
+
|
|
121
|
+
参数:
|
|
122
|
+
value: 要验证的值
|
|
123
|
+
|
|
124
|
+
返回:
|
|
125
|
+
元组 (is_valid, error_message)
|
|
126
|
+
"""
|
|
127
|
+
if value is None:
|
|
128
|
+
return False, "值不能为空。"
|
|
129
|
+
value_str = str(value)
|
|
130
|
+
if self._int_pattern.fullmatch(value_str):
|
|
131
|
+
try:
|
|
132
|
+
int(value_str)
|
|
133
|
+
return True, None
|
|
134
|
+
except ValueError:
|
|
135
|
+
return False, f"'{value_str}' 看起来像整数,但无法转换。"
|
|
136
|
+
return False, f"'{value_str}' 不是一个严格格式的整数(只允许数字和可选的负号)。"
|
|
137
|
+
|
|
138
|
+
def parse(self, value: Any) -> int:
|
|
139
|
+
"""
|
|
140
|
+
@methoddesc 将值解析为整数
|
|
141
|
+
|
|
142
|
+
参数:
|
|
143
|
+
value: 要解析的值
|
|
144
|
+
|
|
145
|
+
返回:
|
|
146
|
+
整数
|
|
147
|
+
"""
|
|
148
|
+
return int(value)
|
|
149
|
+
|
|
150
|
+
def process_column(self, series: pd.Series, col_name: str, nullable: bool = True) -> tuple[pd.Series, list[dict]]:
|
|
151
|
+
"""向量化整数列验证和解析。"""
|
|
152
|
+
errors: list[dict] = []
|
|
153
|
+
is_na = series.isna()
|
|
154
|
+
|
|
155
|
+
# nullable 检查
|
|
156
|
+
if not nullable:
|
|
157
|
+
blank_mask = series.fillna("").astype(str).str.strip() == ""
|
|
158
|
+
notnull_violations = is_na | blank_mask
|
|
159
|
+
for index in series.index[notnull_violations]:
|
|
160
|
+
errors.append(
|
|
161
|
+
{
|
|
162
|
+
"row_index": index,
|
|
163
|
+
"column": col_name,
|
|
164
|
+
"value": series[index],
|
|
165
|
+
"error_type": "NotNullViolation",
|
|
166
|
+
"error_message": f"列 '{col_name}' 不允许为空",
|
|
167
|
+
}
|
|
168
|
+
)
|
|
169
|
+
else:
|
|
170
|
+
notnull_violations = pd.Series(False, index=series.index)
|
|
171
|
+
|
|
172
|
+
# 对非空值进行类型验证
|
|
173
|
+
non_na = ~is_na & ~notnull_violations
|
|
174
|
+
|
|
175
|
+
# 回归 #2 修复:含空值的整数列被 pandas 推断为 float64,str(1.0)="1.0" 不匹配整数正则,
|
|
176
|
+
# 导致整列每个非空值误报 TypeValidationError。对"值是整数"的 float 在字符串化前回收为整数,
|
|
177
|
+
# 避免把 1.5 这样的真实小数误判为整数(仅当 float.is_integer() 为真时规整)。
|
|
178
|
+
def _to_int_str(v: Any) -> str:
|
|
179
|
+
if isinstance(v, float) and v.is_integer():
|
|
180
|
+
return str(int(v))
|
|
181
|
+
return str(v)
|
|
182
|
+
|
|
183
|
+
series_str = series.apply(_to_int_str)
|
|
184
|
+
valid_format = series_str.str.fullmatch(r"^-?\d+$")
|
|
185
|
+
|
|
186
|
+
# 标记类型验证失败的行
|
|
187
|
+
type_error_mask = non_na & ~valid_format
|
|
188
|
+
for index in series.index[type_error_mask]:
|
|
189
|
+
val = series[index]
|
|
190
|
+
errors.append(
|
|
191
|
+
{
|
|
192
|
+
"row_index": index,
|
|
193
|
+
"column": col_name,
|
|
194
|
+
"value": val,
|
|
195
|
+
"error_type": "TypeValidationError",
|
|
196
|
+
"error_message": f"'{val}' 不是一个严格格式的整数(只允许数字和可选的负号)。",
|
|
197
|
+
}
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# 解析:用 pd.to_numeric 批量转换
|
|
201
|
+
parsed = pd.to_numeric(series_str, errors="coerce")
|
|
202
|
+
|
|
203
|
+
# 溢出检测必须在 to_numeric 升位 float64 之前基于原始字符串进行:
|
|
204
|
+
# 含空值的列 to_numeric 返回 float64,2^53+1(9007199254740993)会被舍入为
|
|
205
|
+
# 2^53.0(9007199254740992.0),之后 abs > 2**53 恒为 False,溢出被静默吞掉。
|
|
206
|
+
# 与 valid_format 相同口径:仅对严格整数格式的字符串做精度检查。
|
|
207
|
+
def _exceeds_safe_int_range(value_str: str) -> bool:
|
|
208
|
+
if self._int_pattern.fullmatch(value_str):
|
|
209
|
+
return abs(int(value_str)) > 2**53
|
|
210
|
+
return False
|
|
211
|
+
|
|
212
|
+
overflow_mask = non_na & series_str.apply(_exceeds_safe_int_range)
|
|
213
|
+
for index in series.index[overflow_mask]:
|
|
214
|
+
errors.append(
|
|
215
|
+
{
|
|
216
|
+
"row_index": index,
|
|
217
|
+
"column": col_name,
|
|
218
|
+
"value": series[index],
|
|
219
|
+
"error_type": "TypeValidationError",
|
|
220
|
+
"error_message": f"'{series[index]}' 超出安全整数范围(±2^53),可能丢失精度。",
|
|
221
|
+
}
|
|
222
|
+
)
|
|
223
|
+
# 将无效/空值位置设为 None
|
|
224
|
+
failed = is_na | notnull_violations | type_error_mask | overflow_mask
|
|
225
|
+
parsed = parsed.where(~failed, None)
|
|
226
|
+
|
|
227
|
+
return parsed, errors
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class StringType(DataType):
|
|
231
|
+
"""
|
|
232
|
+
@classdesc 字符串类型
|
|
233
|
+
|
|
234
|
+
验证值非空,并将其解析为字符串。
|
|
235
|
+
最基本的类型,几乎所有字段都可以使用。
|
|
236
|
+
|
|
237
|
+
使用场景:
|
|
238
|
+
- 姓名、地址、描述等文本字段
|
|
239
|
+
- 不需要特定格式验证的通用字段
|
|
240
|
+
"""
|
|
241
|
+
|
|
242
|
+
def validate(self, value: Any) -> tuple[bool, Any]:
|
|
243
|
+
"""
|
|
244
|
+
@methoddesc 验证值是否非空
|
|
245
|
+
|
|
246
|
+
参数:
|
|
247
|
+
value: 要验证的值
|
|
248
|
+
|
|
249
|
+
返回:
|
|
250
|
+
元组 (is_valid, error_message)
|
|
251
|
+
"""
|
|
252
|
+
if value is None:
|
|
253
|
+
return False, "值不能为空。"
|
|
254
|
+
return True, None
|
|
255
|
+
|
|
256
|
+
def parse(self, value: Any) -> str:
|
|
257
|
+
"""
|
|
258
|
+
@methoddesc 将值解析为字符串
|
|
259
|
+
|
|
260
|
+
参数:
|
|
261
|
+
value: 要解析的值
|
|
262
|
+
|
|
263
|
+
返回:
|
|
264
|
+
字符串
|
|
265
|
+
"""
|
|
266
|
+
return str(value)
|
|
267
|
+
|
|
268
|
+
def process_column(self, series: pd.Series, col_name: str, nullable: bool = True) -> tuple[pd.Series, list[dict]]:
|
|
269
|
+
"""向量化字符串列验证和解析。"""
|
|
270
|
+
errors: list[dict] = []
|
|
271
|
+
is_na = series.isna()
|
|
272
|
+
|
|
273
|
+
# nullable 检查
|
|
274
|
+
if not nullable:
|
|
275
|
+
blank_mask = series.fillna("").astype(str).str.strip() == ""
|
|
276
|
+
notnull_violations = is_na | blank_mask
|
|
277
|
+
for index in series.index[notnull_violations]:
|
|
278
|
+
errors.append(
|
|
279
|
+
{
|
|
280
|
+
"row_index": index,
|
|
281
|
+
"column": col_name,
|
|
282
|
+
"value": series[index],
|
|
283
|
+
"error_type": "NotNullViolation",
|
|
284
|
+
"error_message": f"列 '{col_name}' 不允许为空",
|
|
285
|
+
}
|
|
286
|
+
)
|
|
287
|
+
else:
|
|
288
|
+
notnull_violations = pd.Series(False, index=series.index)
|
|
289
|
+
|
|
290
|
+
# StringType 几乎不产生类型验证错误,直接转为字符串
|
|
291
|
+
# 保留空值位置为 None
|
|
292
|
+
failed = is_na | notnull_violations
|
|
293
|
+
parsed = series.astype(str).where(~failed, None)
|
|
294
|
+
|
|
295
|
+
return parsed, errors
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
class FloatType(DataType):
|
|
299
|
+
"""
|
|
300
|
+
@classdesc 浮点数类型
|
|
301
|
+
|
|
302
|
+
验证和解析浮点数值。
|
|
303
|
+
支持整数形式和小数形式的数值。
|
|
304
|
+
|
|
305
|
+
使用场景:
|
|
306
|
+
- 价格、比率、测量值等需要小数精度的字段
|
|
307
|
+
"""
|
|
308
|
+
|
|
309
|
+
def validate(self, value: Any) -> tuple[bool, Any]:
|
|
310
|
+
"""
|
|
311
|
+
@methoddesc 验证值是否为有效的浮点数
|
|
312
|
+
|
|
313
|
+
尝试将值转换为 float 类型来验证。回归 D10: 拒绝 inf/-inf/nan 等非有限值
|
|
314
|
+
(float("inf") 在 Python 合法但会污染下游统计),与 DecimalType 的 is_finite 检查对齐。
|
|
315
|
+
|
|
316
|
+
参数:
|
|
317
|
+
value: 要验证的值
|
|
318
|
+
|
|
319
|
+
返回:
|
|
320
|
+
元组 (is_valid, error_message)
|
|
321
|
+
"""
|
|
322
|
+
try:
|
|
323
|
+
parsed = float(value)
|
|
324
|
+
except (ValueError, TypeError):
|
|
325
|
+
return False, f"'{value}' 不是一个有效的浮点数。"
|
|
326
|
+
# 拒绝非有限值(inf/-inf/nan),避免污染下游统计与约束
|
|
327
|
+
import math
|
|
328
|
+
|
|
329
|
+
if math.isnan(parsed) or math.isinf(parsed):
|
|
330
|
+
return False, f"'{value}' 不是有限的浮点数(不接受 inf/nan)。"
|
|
331
|
+
return True, None
|
|
332
|
+
|
|
333
|
+
def parse(self, value: Any) -> float:
|
|
334
|
+
"""
|
|
335
|
+
@methoddesc 将值解析为浮点数
|
|
336
|
+
|
|
337
|
+
参数:
|
|
338
|
+
value: 要解析的值
|
|
339
|
+
|
|
340
|
+
返回:
|
|
341
|
+
浮点数
|
|
342
|
+
|
|
343
|
+
异常:
|
|
344
|
+
ValueError: 值无法解析为有限浮点数时抛出(回归 D10: 拒绝 inf/nan)
|
|
345
|
+
"""
|
|
346
|
+
parsed = float(value)
|
|
347
|
+
import math
|
|
348
|
+
|
|
349
|
+
if math.isnan(parsed) or math.isinf(parsed):
|
|
350
|
+
raise ValueError(f"'{value}' 不是有限的浮点数(不接受 inf/nan)。")
|
|
351
|
+
return parsed
|
|
352
|
+
|
|
353
|
+
def process_column(self, series: pd.Series, col_name: str, nullable: bool = True) -> tuple[pd.Series, list[dict]]:
|
|
354
|
+
"""向量化浮点数列验证和解析。"""
|
|
355
|
+
errors: list[dict] = []
|
|
356
|
+
is_na = series.isna()
|
|
357
|
+
|
|
358
|
+
# nullable 检查
|
|
359
|
+
if not nullable:
|
|
360
|
+
blank_mask = series.fillna("").astype(str).str.strip() == ""
|
|
361
|
+
notnull_violations = is_na | blank_mask
|
|
362
|
+
for index in series.index[notnull_violations]:
|
|
363
|
+
errors.append(
|
|
364
|
+
{
|
|
365
|
+
"row_index": index,
|
|
366
|
+
"column": col_name,
|
|
367
|
+
"value": series[index],
|
|
368
|
+
"error_type": "NotNullViolation",
|
|
369
|
+
"error_message": f"列 '{col_name}' 不允许为空",
|
|
370
|
+
}
|
|
371
|
+
)
|
|
372
|
+
else:
|
|
373
|
+
notnull_violations = pd.Series(False, index=series.index)
|
|
374
|
+
|
|
375
|
+
# 用 pd.to_numeric 批量转换,coerce 将无效值转为 NaN
|
|
376
|
+
parsed = pd.to_numeric(series, errors="coerce")
|
|
377
|
+
|
|
378
|
+
# 标记类型验证失败的行(非空但转换后为 NaN)
|
|
379
|
+
non_na = ~is_na & ~notnull_violations
|
|
380
|
+
type_error_mask = non_na & parsed.isna()
|
|
381
|
+
# 回归 D10 补漏: 单值 validate() 已拒绝 inf/nan,但向量化路径 pd.to_numeric 会把
|
|
382
|
+
# "inf"/"Infinity" 解析为 inf(非 NaN)绕过掩码,inf 流入下游污染 Range 与统计。
|
|
383
|
+
# 非有限值(±inf)同样按类型错误处理;where(~failed, None) 统一置 None。
|
|
384
|
+
type_error_mask = type_error_mask | (non_na & parsed.notna() & ~np.isfinite(parsed))
|
|
385
|
+
for index in series.index[type_error_mask]:
|
|
386
|
+
val = series[index]
|
|
387
|
+
errors.append(
|
|
388
|
+
{
|
|
389
|
+
"row_index": index,
|
|
390
|
+
"column": col_name,
|
|
391
|
+
"value": val,
|
|
392
|
+
"error_type": "TypeValidationError",
|
|
393
|
+
"error_message": f"'{val}' 不是一个有效的浮点数。",
|
|
394
|
+
}
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
# 将无效/空值位置设为 None
|
|
398
|
+
failed = is_na | notnull_violations | type_error_mask
|
|
399
|
+
parsed = parsed.where(~failed, None)
|
|
400
|
+
|
|
401
|
+
return parsed, errors
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
class DecimalType(DataType):
|
|
405
|
+
"""
|
|
406
|
+
@classdesc 高精度小数类型
|
|
407
|
+
|
|
408
|
+
基于 Python Decimal 实现的高精度数值类型。
|
|
409
|
+
支持精度(总有效数字位数)和小数位数限制。
|
|
410
|
+
|
|
411
|
+
使用场景:
|
|
412
|
+
- 货币金额计算(避免浮点数精度问题)
|
|
413
|
+
- 需要精确小数表示的业务场景
|
|
414
|
+
"""
|
|
415
|
+
|
|
416
|
+
def __init__(self, precision: int = 28, scale: int | None = None):
|
|
417
|
+
"""
|
|
418
|
+
@methoddesc 初始化高精度小数类型
|
|
419
|
+
|
|
420
|
+
参数:
|
|
421
|
+
precision: 总精度(最大有效数字位数),默认 28
|
|
422
|
+
scale: 小数位数限制,None 表示不限制
|
|
423
|
+
"""
|
|
424
|
+
self.precision = precision
|
|
425
|
+
self.scale = scale
|
|
426
|
+
|
|
427
|
+
def validate(self, value: Any) -> tuple[bool, Any]:
|
|
428
|
+
"""
|
|
429
|
+
@methoddesc 验证值是否为有效的高精度小数
|
|
430
|
+
|
|
431
|
+
检查值能否转换为 Decimal,以及是否满足精度和位数限制。
|
|
432
|
+
|
|
433
|
+
参数:
|
|
434
|
+
value: 要验证的值
|
|
435
|
+
|
|
436
|
+
返回:
|
|
437
|
+
元组 (is_valid, error_message)
|
|
438
|
+
"""
|
|
439
|
+
from decimal import Decimal, InvalidOperation
|
|
440
|
+
|
|
441
|
+
try:
|
|
442
|
+
decimal_value = Decimal(str(value))
|
|
443
|
+
if not decimal_value.is_finite():
|
|
444
|
+
return False, f"'{value}' 不是有限的数值(NaN 或 Infinity 不被接受)"
|
|
445
|
+
if self.precision:
|
|
446
|
+
# §1.20: 按数值语义计数精度——去尾随零与指数形态后数有效位数,
|
|
447
|
+
# 同值 "150"/"1.5E+2"/"150.00" 判定一致(原实现按存储表示计数,同值不同精度)。
|
|
448
|
+
# 2026-09-20 修复:不得经过 Decimal.normalize()——它受 decimal 上下文
|
|
449
|
+
# prec=28 影响,>28 位有效数字会被就地舍入回 28 位再计数,precision=28 时
|
|
450
|
+
# 29/30/33 位超限值假阴性放行(实测修复前三者全部通过)。改为直接对
|
|
451
|
+
# as_tuple 数字串剥尾随零,与 normalize 的有效位语义等价且无上下文舍入。
|
|
452
|
+
_sign, digits, _exponent = decimal_value.as_tuple()
|
|
453
|
+
significant = "".join(map(str, digits)).rstrip("0")
|
|
454
|
+
total_digits = len(significant) if significant else 1 # 全零值有效位记 1
|
|
455
|
+
if total_digits > self.precision:
|
|
456
|
+
return False, f"'{value}' 超出精度限制(最大 {self.precision} 位)"
|
|
457
|
+
if self.scale is not None:
|
|
458
|
+
exponent = decimal_value.as_tuple().exponent
|
|
459
|
+
if isinstance(exponent, int):
|
|
460
|
+
decimal_places = -exponent
|
|
461
|
+
if decimal_places > self.scale:
|
|
462
|
+
return False, f"'{value}' 小数位数超出限制(最大 {self.scale} 位)"
|
|
463
|
+
return True, None
|
|
464
|
+
except (InvalidOperation, ValueError, TypeError):
|
|
465
|
+
return False, f"'{value}' 不是一个有效的数值。"
|
|
466
|
+
|
|
467
|
+
def parse(self, value: Any) -> Decimal:
|
|
468
|
+
"""
|
|
469
|
+
@methoddesc 将值解析为 Decimal
|
|
470
|
+
|
|
471
|
+
参数:
|
|
472
|
+
value: 要解析的值
|
|
473
|
+
|
|
474
|
+
返回:
|
|
475
|
+
Decimal 对象
|
|
476
|
+
"""
|
|
477
|
+
from decimal import Decimal
|
|
478
|
+
|
|
479
|
+
return Decimal(str(value))
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
class BooleanType(DataType):
|
|
483
|
+
"""
|
|
484
|
+
@classdesc 布尔类型
|
|
485
|
+
|
|
486
|
+
验证和解析布尔值。
|
|
487
|
+
支持多种布尔值表示形式,如 true/false、yes/no、1/0、是/否等。
|
|
488
|
+
|
|
489
|
+
使用场景:
|
|
490
|
+
- 开关、状态、标记等只有两种状态的字段
|
|
491
|
+
"""
|
|
492
|
+
|
|
493
|
+
TRUE_VALUES = {"true", "yes", "on", "1", "是", "t", "y"}
|
|
494
|
+
FALSE_VALUES = {"false", "no", "off", "0", "否", "f", "n"}
|
|
495
|
+
|
|
496
|
+
def validate(self, value: Any) -> tuple[bool, Any]:
|
|
497
|
+
"""
|
|
498
|
+
@methoddesc 验证值是否为有效的布尔值
|
|
499
|
+
|
|
500
|
+
支持布尔类型、字符串形式(大小写不敏感)等多种输入。
|
|
501
|
+
|
|
502
|
+
参数:
|
|
503
|
+
value: 要验证的值
|
|
504
|
+
|
|
505
|
+
返回:
|
|
506
|
+
元组 (is_valid, error_message)
|
|
507
|
+
"""
|
|
508
|
+
if value is None:
|
|
509
|
+
return False, "值不能为空。"
|
|
510
|
+
|
|
511
|
+
if isinstance(value, bool):
|
|
512
|
+
return True, None
|
|
513
|
+
|
|
514
|
+
str_val = str(value).strip().lower()
|
|
515
|
+
if str_val in self.TRUE_VALUES or str_val in self.FALSE_VALUES:
|
|
516
|
+
return True, None
|
|
517
|
+
|
|
518
|
+
return False, f"'{value}' 不是一个有效的布尔值。"
|
|
519
|
+
|
|
520
|
+
def parse(self, value: Any) -> bool:
|
|
521
|
+
"""
|
|
522
|
+
@methoddesc 将值解析为布尔值
|
|
523
|
+
|
|
524
|
+
如果值在 TRUE_VALUES 集合中,返回 True;否则返回 False。
|
|
525
|
+
|
|
526
|
+
参数:
|
|
527
|
+
value: 要解析的值
|
|
528
|
+
|
|
529
|
+
返回:
|
|
530
|
+
布尔值
|
|
531
|
+
"""
|
|
532
|
+
if isinstance(value, bool):
|
|
533
|
+
return value
|
|
534
|
+
|
|
535
|
+
str_val = str(value).strip().lower()
|
|
536
|
+
return str_val in self.TRUE_VALUES
|
|
537
|
+
|
|
538
|
+
def process_column(self, series: pd.Series, col_name: str, nullable: bool = True) -> tuple[pd.Series, list[dict]]:
|
|
539
|
+
"""向量化布尔列验证和解析。"""
|
|
540
|
+
errors: list[dict] = []
|
|
541
|
+
is_na = series.isna()
|
|
542
|
+
|
|
543
|
+
# nullable 检查
|
|
544
|
+
if not nullable:
|
|
545
|
+
blank_mask = series.fillna("").astype(str).str.strip() == ""
|
|
546
|
+
notnull_violations = is_na | blank_mask
|
|
547
|
+
for index in series.index[notnull_violations]:
|
|
548
|
+
errors.append(
|
|
549
|
+
{
|
|
550
|
+
"row_index": index,
|
|
551
|
+
"column": col_name,
|
|
552
|
+
"value": series[index],
|
|
553
|
+
"error_type": "NotNullViolation",
|
|
554
|
+
"error_message": f"列 '{col_name}' 不允许为空",
|
|
555
|
+
}
|
|
556
|
+
)
|
|
557
|
+
else:
|
|
558
|
+
notnull_violations = pd.Series(False, index=series.index)
|
|
559
|
+
|
|
560
|
+
# 对非空值进行类型验证
|
|
561
|
+
non_na = ~is_na & ~notnull_violations
|
|
562
|
+
is_python_bool = series.apply(lambda v: isinstance(v, bool))
|
|
563
|
+
|
|
564
|
+
# 回归 #3 修复:0/1 布尔列带空值被 pandas 推断为 float64,"1.0"/"0.0" 不在合法布尔集合 →
|
|
565
|
+
# 整列误报。对 is_integer() 为真的 float 在字符串化前回收为整数,使 1.0→"1"、0.0→"0" 命中合法集合。
|
|
566
|
+
def _to_int_str(v: Any) -> str:
|
|
567
|
+
if isinstance(v, float) and v.is_integer():
|
|
568
|
+
return str(int(v))
|
|
569
|
+
return str(v)
|
|
570
|
+
|
|
571
|
+
series_str = series.apply(_to_int_str).str.strip().str.lower()
|
|
572
|
+
is_true = series_str.isin(self.TRUE_VALUES)
|
|
573
|
+
is_false = series_str.isin(self.FALSE_VALUES)
|
|
574
|
+
valid_mask = is_python_bool | is_true | is_false
|
|
575
|
+
|
|
576
|
+
# 标记类型验证失败的行
|
|
577
|
+
type_error_mask = non_na & ~valid_mask
|
|
578
|
+
for index in series.index[type_error_mask]:
|
|
579
|
+
val = series[index]
|
|
580
|
+
errors.append(
|
|
581
|
+
{
|
|
582
|
+
"row_index": index,
|
|
583
|
+
"column": col_name,
|
|
584
|
+
"value": val,
|
|
585
|
+
"error_type": "TypeValidationError",
|
|
586
|
+
"error_message": f"'{val}' 不是一个有效的布尔值。",
|
|
587
|
+
}
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
# 解析:逐行判定(布尔类型需要处理 Python bool + 字符串混合输入)
|
|
591
|
+
result_values: list[Any] = []
|
|
592
|
+
for i, val in series.items():
|
|
593
|
+
if is_na[i] or notnull_violations[i] or type_error_mask[i]:
|
|
594
|
+
result_values.append(None)
|
|
595
|
+
elif isinstance(val, bool):
|
|
596
|
+
result_values.append(val)
|
|
597
|
+
else:
|
|
598
|
+
# 回归 #3: float 1.0/0.0 需回收为整数再判定(与上面的字符串化规整保持一致)
|
|
599
|
+
check_val = str(int(val)) if (isinstance(val, float) and val.is_integer()) else str(val)
|
|
600
|
+
result_values.append(check_val.strip().lower() in self.TRUE_VALUES)
|
|
601
|
+
parsed = pd.Series(result_values, index=series.index)
|
|
602
|
+
|
|
603
|
+
return parsed, errors
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
class DateType(DataType):
|
|
607
|
+
"""
|
|
608
|
+
@classdesc 日期类型
|
|
609
|
+
|
|
610
|
+
验证和解析日期值。
|
|
611
|
+
格式要求: YYYY-MM-DD
|
|
612
|
+
|
|
613
|
+
使用场景:
|
|
614
|
+
- 出生日期、订单日期、有效期等日期字段
|
|
615
|
+
"""
|
|
616
|
+
|
|
617
|
+
def validate(self, value: Any) -> tuple[bool, Any]:
|
|
618
|
+
"""
|
|
619
|
+
@methoddesc 验证值是否为有效的日期格式
|
|
620
|
+
|
|
621
|
+
使用 strptime 按照 YYYY-MM-DD 格式解析来验证。
|
|
622
|
+
|
|
623
|
+
参数:
|
|
624
|
+
value: 要验证的值
|
|
625
|
+
|
|
626
|
+
返回:
|
|
627
|
+
元组 (is_valid, error_message)
|
|
628
|
+
"""
|
|
629
|
+
if value is None:
|
|
630
|
+
return False, "值不能为空。"
|
|
631
|
+
from datetime import datetime
|
|
632
|
+
|
|
633
|
+
try:
|
|
634
|
+
datetime.strptime(str(value), "%Y-%m-%d")
|
|
635
|
+
return True, None
|
|
636
|
+
except ValueError:
|
|
637
|
+
pass
|
|
638
|
+
# §1.21: 严格纯日期失败后兜底通用解析(带时间的 datetime 常见于 Excel 导入),
|
|
639
|
+
# 按日期部分视为合法 date——原实现整列误报 TypeValidationError
|
|
640
|
+
try:
|
|
641
|
+
pd.to_datetime(str(value), errors="raise")
|
|
642
|
+
return True, None
|
|
643
|
+
except (ValueError, TypeError, OverflowError):
|
|
644
|
+
return False, f"'{value}' 不是有效的日期格式 (YYYY-MM-DD)。"
|
|
645
|
+
|
|
646
|
+
def parse(self, value: Any) -> date:
|
|
647
|
+
"""
|
|
648
|
+
@methoddesc 将值解析为日期对象
|
|
649
|
+
|
|
650
|
+
按照 YYYY-MM-DD 格式解析字符串并返回 date 对象。
|
|
651
|
+
|
|
652
|
+
参数:
|
|
653
|
+
value: 要解析的值
|
|
654
|
+
|
|
655
|
+
返回:
|
|
656
|
+
datetime.date 对象
|
|
657
|
+
"""
|
|
658
|
+
from datetime import datetime
|
|
659
|
+
|
|
660
|
+
return datetime.strptime(str(value), "%Y-%m-%d").date()
|
|
661
|
+
|
|
662
|
+
def process_column(self, series: pd.Series, col_name: str, nullable: bool = True) -> tuple[pd.Series, list[dict]]:
|
|
663
|
+
"""向量化日期列验证和解析。"""
|
|
664
|
+
errors: list[dict] = []
|
|
665
|
+
is_na = series.isna()
|
|
666
|
+
|
|
667
|
+
# nullable 检查
|
|
668
|
+
if not nullable:
|
|
669
|
+
blank_mask = series.fillna("").astype(str).str.strip() == ""
|
|
670
|
+
notnull_violations = is_na | blank_mask
|
|
671
|
+
for index in series.index[notnull_violations]:
|
|
672
|
+
errors.append(
|
|
673
|
+
{
|
|
674
|
+
"row_index": index,
|
|
675
|
+
"column": col_name,
|
|
676
|
+
"value": series[index],
|
|
677
|
+
"error_type": "NotNullViolation",
|
|
678
|
+
"error_message": f"列 '{col_name}' 不允许为空",
|
|
679
|
+
}
|
|
680
|
+
)
|
|
681
|
+
else:
|
|
682
|
+
notnull_violations = pd.Series(False, index=series.index)
|
|
683
|
+
|
|
684
|
+
# 用 pd.to_datetime 批量解析(§1.21 两段式:严格纯日期优先,
|
|
685
|
+
# 失败的行再逐元素通用解析取日期部分——Excel 常见的带时间 datetime 不再整列误报。
|
|
686
|
+
# 不能整列 lenient 重解析:混合格式列会被 pandas 按首行格式统一推断,带时间的行仍 NaT)
|
|
687
|
+
non_na = ~is_na & ~notnull_violations
|
|
688
|
+
parsed_dt = pd.to_datetime(series.astype(str), format="%Y-%m-%d", errors="coerce")
|
|
689
|
+
need_lenient = parsed_dt.isna() & non_na
|
|
690
|
+
for idx in series.index[need_lenient]:
|
|
691
|
+
parsed_dt[idx] = pd.to_datetime(series[idx], errors="coerce")
|
|
692
|
+
|
|
693
|
+
# 标记类型验证失败的行
|
|
694
|
+
type_error_mask = non_na & parsed_dt.isna()
|
|
695
|
+
for index in series.index[type_error_mask]:
|
|
696
|
+
val = series[index]
|
|
697
|
+
type_error: dict[str, Any] = {
|
|
698
|
+
"row_index": index,
|
|
699
|
+
"column": col_name,
|
|
700
|
+
"value": val,
|
|
701
|
+
"error_type": "TypeValidationError",
|
|
702
|
+
"error_message": f"'{val}' 不是有效的日期格式 (YYYY-MM-DD)。",
|
|
703
|
+
}
|
|
704
|
+
# suggestion(可选):形似 Y-M-D 但取值非法时指出超范围字段
|
|
705
|
+
range_hint = date_range_hint(val)
|
|
706
|
+
if range_hint:
|
|
707
|
+
type_error["suggestion"] = range_hint
|
|
708
|
+
errors.append(type_error)
|
|
709
|
+
|
|
710
|
+
# 转换为 date 对象,保留原索引
|
|
711
|
+
result_values: list[Any] = []
|
|
712
|
+
for i in series.index:
|
|
713
|
+
if is_na[i] or notnull_violations[i] or type_error_mask[i]:
|
|
714
|
+
result_values.append(None)
|
|
715
|
+
else:
|
|
716
|
+
result_values.append(parsed_dt[i].date())
|
|
717
|
+
parsed = pd.Series(result_values, index=series.index)
|
|
718
|
+
|
|
719
|
+
return parsed, errors
|