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,266 @@
|
|
|
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
|
+
- 在格式解析后、约束校验前提取派生列(Extracted 类型列)
|
|
21
|
+
- 通过正则表达式的命名捕获组从源列提取数据
|
|
22
|
+
- 支持向量化提取和类型转换(Integer、Float、Boolean、String)
|
|
23
|
+
|
|
24
|
+
架构设计:
|
|
25
|
+
- 两阶段处理:格式解析 → 提取派生列 → 约束校验
|
|
26
|
+
- 正则驱动:从 schema 中读取正则模式,使用 pandas str.extract 向量化提取
|
|
27
|
+
- 错误上报:提取失败作为验证错误返回,避免静默通过
|
|
28
|
+
|
|
29
|
+
输入示例:
|
|
30
|
+
_extract_derived_columns(
|
|
31
|
+
parsed_datasets={"users": df},
|
|
32
|
+
schema=DataSetSchema(...),
|
|
33
|
+
raw_datasets={"users": raw_df},
|
|
34
|
+
all_errors=[]
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
输出示例:
|
|
38
|
+
parsed_datasets 被就地修改,新增提取列
|
|
39
|
+
all_errors 可能追加提取失败错误
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
import logging
|
|
43
|
+
from typing import cast
|
|
44
|
+
|
|
45
|
+
import pandas as pd
|
|
46
|
+
|
|
47
|
+
from app.shared.domain.dataset_schema import DataSetSchema
|
|
48
|
+
|
|
49
|
+
logger = logging.getLogger(__name__)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# Extracted 类型列需要的属性(使用 Protocol 避免直接依赖 domain 子类)
|
|
53
|
+
class _ExtractedTypeProtocol:
|
|
54
|
+
"""仅用于类型检查的 Extracted 数据类型协议。"""
|
|
55
|
+
|
|
56
|
+
name: str
|
|
57
|
+
source_column: str
|
|
58
|
+
extract_key: str
|
|
59
|
+
result_type: str | None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _extract_derived_columns(
|
|
63
|
+
parsed_datasets: dict[str, pd.DataFrame],
|
|
64
|
+
schema: DataSetSchema,
|
|
65
|
+
raw_datasets: dict[str, pd.DataFrame],
|
|
66
|
+
all_errors: list[dict],
|
|
67
|
+
) -> None:
|
|
68
|
+
"""
|
|
69
|
+
@methoddesc 提取派生列(Extracted 类型列)
|
|
70
|
+
|
|
71
|
+
在格式解析后、执行约束校验前,从 Extracted 类型的列中通过正则提取派生列。
|
|
72
|
+
Extracted 类型标识该列是从另一个列的正则匹配结果中提取的。
|
|
73
|
+
|
|
74
|
+
参数:
|
|
75
|
+
parsed_datasets: 解析后的 DataFrame 字典(会被就地修改)
|
|
76
|
+
schema: 完整的数据集 Schema
|
|
77
|
+
raw_datasets: 原始 DataFrame 字典(用于保留原始字符串格式)
|
|
78
|
+
all_errors: 错误列表(提取失败时会追加错误)
|
|
79
|
+
"""
|
|
80
|
+
import re
|
|
81
|
+
|
|
82
|
+
from app.shared.domain.regex_flags import parse_regex_flags
|
|
83
|
+
|
|
84
|
+
for table_id, parsed_df in parsed_datasets.items():
|
|
85
|
+
logger.debug(f"Processing extracted columns for table: {table_id}")
|
|
86
|
+
if table_id not in schema.tables:
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
table_schema = schema.tables[table_id]
|
|
90
|
+
|
|
91
|
+
# 查找所有 Extracted 类型的列
|
|
92
|
+
extracted_columns = []
|
|
93
|
+
for col in table_schema.columns.values():
|
|
94
|
+
data_type = col.data_type
|
|
95
|
+
logger.debug(f"Column {col.name} has data_type: {data_type}")
|
|
96
|
+
if getattr(data_type, "name", None) == "Extracted":
|
|
97
|
+
extracted_type = cast(_ExtractedTypeProtocol, data_type)
|
|
98
|
+
extracted_columns.append(
|
|
99
|
+
{
|
|
100
|
+
"column_name": col.name,
|
|
101
|
+
"source_column": extracted_type.source_column,
|
|
102
|
+
"extract_key": extracted_type.extract_key,
|
|
103
|
+
"result_type": getattr(extracted_type, "result_type", None),
|
|
104
|
+
}
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
if not extracted_columns:
|
|
108
|
+
continue
|
|
109
|
+
|
|
110
|
+
logger.debug(f"表 '{table_id}' 发现 {len(extracted_columns)} 个提取列")
|
|
111
|
+
|
|
112
|
+
for ext_info in extracted_columns:
|
|
113
|
+
col_name = ext_info["column_name"]
|
|
114
|
+
source_column = ext_info["source_column"]
|
|
115
|
+
if not isinstance(source_column, str):
|
|
116
|
+
continue
|
|
117
|
+
extract_key = ext_info["extract_key"]
|
|
118
|
+
result_type = ext_info.get("result_type")
|
|
119
|
+
|
|
120
|
+
if source_column not in parsed_df.columns:
|
|
121
|
+
all_errors.append(
|
|
122
|
+
{
|
|
123
|
+
"stage": "format",
|
|
124
|
+
"table": table_id,
|
|
125
|
+
"column": col_name,
|
|
126
|
+
"check_type": "ExtractedColumnValidation",
|
|
127
|
+
"error_type": "ExtractedColumnValidationError",
|
|
128
|
+
"message": (
|
|
129
|
+
f"提取列 '{col_name}' 失败:源列 '{source_column}' 不存在于表 '{table_id}'。请检查 Schema 配置。"
|
|
130
|
+
),
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
continue
|
|
134
|
+
|
|
135
|
+
# 获取源列绑定的正则表达式模式
|
|
136
|
+
source_col = table_schema.columns.get(source_column)
|
|
137
|
+
if not source_col:
|
|
138
|
+
all_errors.append(
|
|
139
|
+
{
|
|
140
|
+
"stage": "format",
|
|
141
|
+
"table": table_id,
|
|
142
|
+
"column": col_name,
|
|
143
|
+
"check_type": "ExtractedColumnValidation",
|
|
144
|
+
"error_type": "ExtractedColumnValidationError",
|
|
145
|
+
"message": (
|
|
146
|
+
f"提取列 '{col_name}' 失败:源列 '{source_column}' 未在 schema 中定义。请检查 Schema 配置。"
|
|
147
|
+
),
|
|
148
|
+
}
|
|
149
|
+
)
|
|
150
|
+
continue
|
|
151
|
+
|
|
152
|
+
regex_pattern = None
|
|
153
|
+
regex_flags = "g"
|
|
154
|
+
case_sensitive = True
|
|
155
|
+
|
|
156
|
+
source_data_type = source_col.data_type
|
|
157
|
+
if hasattr(source_data_type, "name") and source_data_type.name == "Expr":
|
|
158
|
+
# 显式 Expr:有 pattern 属性
|
|
159
|
+
if hasattr(source_data_type, "pattern"):
|
|
160
|
+
regex_pattern = source_data_type.pattern
|
|
161
|
+
if hasattr(source_data_type, "flags"):
|
|
162
|
+
regex_flags = source_data_type.flags
|
|
163
|
+
if hasattr(source_data_type, "case_sensitive"):
|
|
164
|
+
case_sensitive = source_data_type.case_sensitive
|
|
165
|
+
# 隐式 Expr:无 pattern 属性,但有 registry
|
|
166
|
+
elif hasattr(source_data_type, "registry") and source_data_type.registry:
|
|
167
|
+
registry = source_data_type.registry
|
|
168
|
+
# 遍历 registry 中的所有 pattern,查找包含 extract_key 的
|
|
169
|
+
if hasattr(registry, "_patterns"):
|
|
170
|
+
for pattern_obj in registry._patterns:
|
|
171
|
+
if hasattr(pattern_obj, "regex") and pattern_obj.regex:
|
|
172
|
+
regex_obj = pattern_obj.regex
|
|
173
|
+
group_index = regex_obj.groupindex
|
|
174
|
+
if extract_key in group_index:
|
|
175
|
+
regex_pattern = regex_obj.pattern
|
|
176
|
+
break
|
|
177
|
+
|
|
178
|
+
if not regex_pattern:
|
|
179
|
+
all_errors.append(
|
|
180
|
+
{
|
|
181
|
+
"stage": "format",
|
|
182
|
+
"table": table_id,
|
|
183
|
+
"column": col_name,
|
|
184
|
+
"check_type": "ExtractedColumnValidation",
|
|
185
|
+
"error_type": "ExtractedColumnValidationError",
|
|
186
|
+
"message": (
|
|
187
|
+
f"提取列 '{col_name}' 失败:源列 '{source_column}' 没有绑定正则表达式。请检查 Schema 配置。"
|
|
188
|
+
),
|
|
189
|
+
}
|
|
190
|
+
)
|
|
191
|
+
continue
|
|
192
|
+
|
|
193
|
+
# 编译正则表达式(flags 整词匹配解析,防 "multiline" 子串误开 IGNORECASE)
|
|
194
|
+
flags = parse_regex_flags(regex_flags)
|
|
195
|
+
if not case_sensitive:
|
|
196
|
+
flags |= re.IGNORECASE
|
|
197
|
+
|
|
198
|
+
# 执行提取
|
|
199
|
+
try:
|
|
200
|
+
compiled = re.compile(regex_pattern, flags)
|
|
201
|
+
group_names = list(compiled.groupindex.keys())
|
|
202
|
+
|
|
203
|
+
# 获取源列的原始数据作为 Series(优先使用 raw_df 保留原始字符串格式)
|
|
204
|
+
raw_df = raw_datasets.get(table_id)
|
|
205
|
+
if raw_df is not None and source_column in raw_df.columns:
|
|
206
|
+
source_series = raw_df[source_column].fillna("").astype(str)
|
|
207
|
+
else:
|
|
208
|
+
source_series = parsed_df[source_column].fillna("").astype(str)
|
|
209
|
+
|
|
210
|
+
# 使用 pandas str.extract 进行向量化提取
|
|
211
|
+
extracted_df = source_series.str.extract(compiled)
|
|
212
|
+
match_count = extracted_df.notna().any(axis=1).sum()
|
|
213
|
+
|
|
214
|
+
if extract_key in extracted_df.columns:
|
|
215
|
+
extracted_series = extracted_df[extract_key].replace("", pd.NA)
|
|
216
|
+
|
|
217
|
+
# 根据 result_type 进行向量化类型转换
|
|
218
|
+
if result_type == "Integer":
|
|
219
|
+
parsed_df[col_name] = pd.to_numeric(extracted_series, errors="coerce").astype("Int64")
|
|
220
|
+
elif result_type == "Float":
|
|
221
|
+
parsed_df[col_name] = pd.to_numeric(extracted_series, errors="coerce")
|
|
222
|
+
elif result_type == "Boolean":
|
|
223
|
+
is_null = extracted_series.isna() | (extracted_series.str.strip() == "")
|
|
224
|
+
bool_series = extracted_series.str.lower().isin(["true", "1", "yes"]).astype("boolean")
|
|
225
|
+
bool_series[is_null] = pd.NA
|
|
226
|
+
parsed_df[col_name] = bool_series
|
|
227
|
+
else:
|
|
228
|
+
# String 或其他类型保持原样
|
|
229
|
+
parsed_df[col_name] = extracted_series
|
|
230
|
+
|
|
231
|
+
logger.debug(
|
|
232
|
+
f"提取列 '{col_name}' <- '{source_column}'[{extract_key}], 匹配 {match_count} 行, 类型: {result_type or 'String'}"
|
|
233
|
+
)
|
|
234
|
+
else:
|
|
235
|
+
error_msg = f"提取键 '{extract_key}' 不在正则表达式的命名捕获组中,可用组: {group_names}"
|
|
236
|
+
logger.warning(error_msg)
|
|
237
|
+
# 将配置错误上报为验证错误,避免用户看到"通过"(M4)
|
|
238
|
+
all_errors.append(
|
|
239
|
+
{
|
|
240
|
+
"stage": "format",
|
|
241
|
+
"table": table_id,
|
|
242
|
+
"column": col_name,
|
|
243
|
+
"check_type": "ExtractedColumnValidation",
|
|
244
|
+
"message": error_msg,
|
|
245
|
+
}
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
except Exception as e:
|
|
249
|
+
error_msg = (
|
|
250
|
+
f"正则提取列 '{col_name}' 执行失败 (来源列: '{source_column}', 提取键: '{extract_key}'): {e}"
|
|
251
|
+
)
|
|
252
|
+
logger.warning(error_msg, exc_info=True)
|
|
253
|
+
# 提取失败也作为验证错误上报:派生列整列缺失时下游约束会在空数据上
|
|
254
|
+
# 运行,报告"通过"属于静默跳过校验(与模块契约"避免静默通过"一致)
|
|
255
|
+
all_errors.append(
|
|
256
|
+
{
|
|
257
|
+
"stage": "format",
|
|
258
|
+
"table": table_id,
|
|
259
|
+
"column": col_name,
|
|
260
|
+
"check_type": "ExtractedColumnValidation",
|
|
261
|
+
"message": error_msg,
|
|
262
|
+
}
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
# 更新 parsed_datasets 中的 DataFrame
|
|
266
|
+
parsed_datasets[table_id] = parsed_df
|
|
@@ -0,0 +1,209 @@
|
|
|
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
|
+
"""@fileoverview 校验历史存储服务
|
|
17
|
+
|
|
18
|
+
功能概述:
|
|
19
|
+
- 将每次校验结果持久化到本地 JSON 文件
|
|
20
|
+
- 支持历史列表查询、单次详情查询、删除
|
|
21
|
+
- 支持聚合统计(趋势数据)
|
|
22
|
+
|
|
23
|
+
存储位置: {projectDir}/.precis/validation_history.json
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import json
|
|
29
|
+
import logging
|
|
30
|
+
import os
|
|
31
|
+
import threading
|
|
32
|
+
import time
|
|
33
|
+
from dataclasses import asdict, dataclass, field
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
from typing import Any
|
|
36
|
+
|
|
37
|
+
logger = logging.getLogger(__name__)
|
|
38
|
+
|
|
39
|
+
HISTORY_FILENAME = "validation_history.json"
|
|
40
|
+
MAX_HISTORY_ENTRIES = 200
|
|
41
|
+
|
|
42
|
+
# 回归 C7: 进程级写锁。ValidationHistoryStore 每请求新建实例(load 全量→改→全量覆写),
|
|
43
|
+
# 并发请求会产生 lost-update。用模块级锁串行化同进程内的 add_run/delete_run 写操作;
|
|
44
|
+
# 但锁只保证互斥不保证新鲜——add_run/delete_run 还须在锁内重新 _load 磁盘最新内容
|
|
45
|
+
# 再合并保存,否则先写实例的记录会被后写实例的旧内存快照覆写(跨进程仍依赖文件锁,
|
|
46
|
+
# 但本地单进程场景已足够)。
|
|
47
|
+
_history_write_lock = threading.Lock()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class ValidationRunRecord:
|
|
52
|
+
"""单次校验运行记录"""
|
|
53
|
+
|
|
54
|
+
id: str
|
|
55
|
+
timestamp: str
|
|
56
|
+
duration_ms: int
|
|
57
|
+
scope: str
|
|
58
|
+
summary: dict[str, Any]
|
|
59
|
+
by_type: dict[str, dict[str, int]]
|
|
60
|
+
by_table: dict[str, dict[str, int]]
|
|
61
|
+
errors: list[dict[str, Any]]
|
|
62
|
+
warnings: list[str] = field(default_factory=list)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class ValidationHistoryStore:
|
|
66
|
+
"""校验历史持久化存储"""
|
|
67
|
+
|
|
68
|
+
def __init__(self, project_dir: str) -> None:
|
|
69
|
+
self._file_path = Path(project_dir) / ".precis" / HISTORY_FILENAME
|
|
70
|
+
self._runs: list[dict[str, Any]] = []
|
|
71
|
+
self._load()
|
|
72
|
+
|
|
73
|
+
def _load(self) -> None:
|
|
74
|
+
"""从磁盘加载历史记录。
|
|
75
|
+
|
|
76
|
+
回归 C7: 文件损坏时不应静默清空后用空列表覆写(否则下次 add_run 永久丢失全部
|
|
77
|
+
历史)。这里把损坏文件重命名为 .corrupted 备份保留现场,内存置空但不破坏磁盘,
|
|
78
|
+
便于用户手工恢复/排查。
|
|
79
|
+
"""
|
|
80
|
+
if not self._file_path.exists():
|
|
81
|
+
self._runs = []
|
|
82
|
+
return
|
|
83
|
+
try:
|
|
84
|
+
with open(self._file_path, encoding="utf-8-sig") as f:
|
|
85
|
+
data = json.load(f)
|
|
86
|
+
self._runs = data.get("runs", [])
|
|
87
|
+
except Exception as e:
|
|
88
|
+
logger.warning(f"[ValidationHistory] 加载历史文件失败: {e}")
|
|
89
|
+
self._runs = []
|
|
90
|
+
# 把损坏文件备份为 .corrupted,避免下次保存静默清空现场(回归 C7)
|
|
91
|
+
try:
|
|
92
|
+
backup = self._file_path.with_suffix(self._file_path.suffix + ".corrupted")
|
|
93
|
+
self._file_path.rename(backup)
|
|
94
|
+
logger.warning(f"[ValidationHistory] 损坏的历史文件已备份至: {backup}")
|
|
95
|
+
except OSError:
|
|
96
|
+
# 重命名失败也不影响:构造阶段不写盘,损坏文件原样保留
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
def _save(self) -> None:
|
|
100
|
+
"""将历史记录写入磁盘(原子写:tmp + os.replace)。
|
|
101
|
+
|
|
102
|
+
回归 C7: 原实现直接以 "w" 打开目标文件,写一半崩溃/断电会留下截断损坏的 JSON,
|
|
103
|
+
下次 _load 失败 → 清空 → 永久丢失全部历史。原子写保证目标文件要么是完整旧内容、
|
|
104
|
+
要么是完整新内容,绝不出现部分写入。
|
|
105
|
+
"""
|
|
106
|
+
self._file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
107
|
+
# 临时文件与目标同目录,保证 os.replace 是原子的(同文件系统)。
|
|
108
|
+
tmp_path = self._file_path.with_suffix(self._file_path.suffix + ".tmp")
|
|
109
|
+
try:
|
|
110
|
+
with open(tmp_path, "w", encoding="utf-8") as f:
|
|
111
|
+
json.dump({"runs": self._runs}, f, ensure_ascii=False, indent=2)
|
|
112
|
+
os.replace(tmp_path, self._file_path)
|
|
113
|
+
except Exception as e:
|
|
114
|
+
logger.error(f"[ValidationHistory] 保存历史文件失败: {e}")
|
|
115
|
+
# 清理可能残留的临时文件,避免堆积
|
|
116
|
+
try:
|
|
117
|
+
tmp_path.unlink()
|
|
118
|
+
except OSError:
|
|
119
|
+
pass
|
|
120
|
+
|
|
121
|
+
def add_run(self, record: ValidationRunRecord) -> str:
|
|
122
|
+
"""添加一次校验记录,返回记录 ID。
|
|
123
|
+
|
|
124
|
+
回归 C7: 加进程级写锁,串行化同进程内并发请求的 load→insert→save。
|
|
125
|
+
回归 lost-update: 锁只保证互斥,不保证新鲜——每请求新建实例时各实例的
|
|
126
|
+
_runs 都是构造时的快照,先写实例落盘的记录会被后写实例的旧快照覆写。
|
|
127
|
+
因此在锁内先重新 _load 磁盘最新内容再合并插入,两个实例交错 add 后
|
|
128
|
+
双方记录都在。
|
|
129
|
+
"""
|
|
130
|
+
entry = asdict(record)
|
|
131
|
+
with _history_write_lock:
|
|
132
|
+
self._load() # 锁内重读,合并其他实例已落盘的记录
|
|
133
|
+
self._runs.insert(0, entry)
|
|
134
|
+
if len(self._runs) > MAX_HISTORY_ENTRIES:
|
|
135
|
+
self._runs = self._runs[:MAX_HISTORY_ENTRIES]
|
|
136
|
+
self._save()
|
|
137
|
+
return record.id
|
|
138
|
+
|
|
139
|
+
def get_runs(self, limit: int = 20, offset: int = 0) -> dict[str, Any]:
|
|
140
|
+
"""获取历史列表(分页)"""
|
|
141
|
+
total = len(self._runs)
|
|
142
|
+
items = self._runs[offset : offset + limit]
|
|
143
|
+
return {"total": total, "limit": limit, "offset": offset, "items": items}
|
|
144
|
+
|
|
145
|
+
def get_run(self, run_id: str) -> dict[str, Any] | None:
|
|
146
|
+
"""获取单次校验详情"""
|
|
147
|
+
for run in self._runs:
|
|
148
|
+
if run.get("id") == run_id:
|
|
149
|
+
return run
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
def delete_run(self, run_id: str) -> bool:
|
|
153
|
+
"""删除单次记录(回归 C7: 加写锁避免并发覆盖;锁内重读避免基于旧快照删除)。"""
|
|
154
|
+
with _history_write_lock:
|
|
155
|
+
self._load() # 锁内重读,基于磁盘最新内容删除
|
|
156
|
+
original_len = len(self._runs)
|
|
157
|
+
self._runs = [r for r in self._runs if r.get("id") != run_id]
|
|
158
|
+
if len(self._runs) < original_len:
|
|
159
|
+
self._save()
|
|
160
|
+
return True
|
|
161
|
+
return False
|
|
162
|
+
|
|
163
|
+
def get_stats(self, last_n: int = 10) -> dict[str, Any]:
|
|
164
|
+
"""获取聚合统计(最近 N 次的趋势数据)"""
|
|
165
|
+
recent = self._runs[:last_n]
|
|
166
|
+
trend = []
|
|
167
|
+
for run in reversed(recent):
|
|
168
|
+
summary = run.get("summary", {})
|
|
169
|
+
trend.append(
|
|
170
|
+
{
|
|
171
|
+
"id": run.get("id"),
|
|
172
|
+
"timestamp": run.get("timestamp"),
|
|
173
|
+
"pass_rate": summary.get("pass_rate", 0),
|
|
174
|
+
"total_checks": summary.get("total_checks", 0),
|
|
175
|
+
"failed_count": summary.get("failed_count", 0),
|
|
176
|
+
}
|
|
177
|
+
)
|
|
178
|
+
latest = self._runs[0] if self._runs else None
|
|
179
|
+
return {
|
|
180
|
+
"total_runs": len(self._runs),
|
|
181
|
+
"trend": trend,
|
|
182
|
+
"latest": {
|
|
183
|
+
"pass_rate": latest.get("summary", {}).get("pass_rate", 0) if latest else 0,
|
|
184
|
+
"total_checks": latest.get("summary", {}).get("total_checks", 0) if latest else 0,
|
|
185
|
+
"passed_count": latest.get("summary", {}).get("passed_count", 0) if latest else 0,
|
|
186
|
+
"failed_count": latest.get("summary", {}).get("failed_count", 0) if latest else 0,
|
|
187
|
+
"timestamp": latest.get("timestamp") if latest else None,
|
|
188
|
+
},
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
_run_id_lock = threading.Lock()
|
|
193
|
+
_run_id_last_us: int = -1
|
|
194
|
+
_run_id_counter: int = 0
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def generate_run_id() -> str:
|
|
198
|
+
"""生成运行 ID(时间戳 + 单调计数器保证唯一性)"""
|
|
199
|
+
global _run_id_last_us, _run_id_counter
|
|
200
|
+
t = time.strftime("%Y%m%d_%H%M%S")
|
|
201
|
+
us = int(time.time() * 1000000) % 1000000
|
|
202
|
+
with _run_id_lock:
|
|
203
|
+
if us == _run_id_last_us:
|
|
204
|
+
_run_id_counter += 1
|
|
205
|
+
else:
|
|
206
|
+
_run_id_last_us = us
|
|
207
|
+
_run_id_counter = 0
|
|
208
|
+
suffix = f"{us:06d}_{_run_id_counter:03d}"
|
|
209
|
+
return f"run_{t}_{suffix}"
|
|
@@ -0,0 +1,136 @@
|
|
|
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
|
+
"""@fileoverview 校验结果 → JSON 契约 payload(CLI 与 MCP 共用单一事实源)
|
|
17
|
+
|
|
18
|
+
契约文档:docs/contracts/validate-json-v1.md(v1 已冻结,只增不减)。
|
|
19
|
+
本模块是 `_build_json_payload` 结构的唯一实现:
|
|
20
|
+
- CLI `--format json` 的 stdout 输出
|
|
21
|
+
- MCP tool `validate_data` 的返回值
|
|
22
|
+
- `--report` 报告导出的数据源
|
|
23
|
+
三处消费同一结构,禁止分叉出第二套输出格式。
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import math
|
|
29
|
+
from collections.abc import Sized
|
|
30
|
+
from typing import Any
|
|
31
|
+
|
|
32
|
+
# JSON 输出契约的版本号(未来契约演进时递增)
|
|
33
|
+
JSON_SCHEMA_VERSION = 1
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def json_safe_value(value: Any) -> Any:
|
|
37
|
+
"""将校验错误中的单元格值转换为 JSON 可序列化的原生类型。
|
|
38
|
+
|
|
39
|
+
pandas 校验链路可能透出 numpy 标量(int64/float64 等)与非有限浮点
|
|
40
|
+
(NaN/Inf),两者均非合法 JSON。numpy 标量经 .item() 归一为 Python
|
|
41
|
+
原生类型;非有限浮点转为字符串保留诊断信息。
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
value: 错误条目中的任意取值
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
JSON 可序列化的值(None/bool/int/有限 float/str)
|
|
48
|
+
"""
|
|
49
|
+
if value is None or isinstance(value, (str, bool, int)):
|
|
50
|
+
return value
|
|
51
|
+
if isinstance(value, float):
|
|
52
|
+
# np.float64 是 float 子类可直接命中;NaN/Inf 不是合法 JSON,转字符串
|
|
53
|
+
return value if math.isfinite(value) else str(value)
|
|
54
|
+
if hasattr(value, "item"):
|
|
55
|
+
# numpy 标量统一经 .item() 归一后再递归判定
|
|
56
|
+
try:
|
|
57
|
+
return json_safe_value(value.item())
|
|
58
|
+
except (AttributeError, ValueError):
|
|
59
|
+
pass
|
|
60
|
+
# 兜底:其余类型(如 dict/list 以外的对象)转字符串,保证不破坏序列化
|
|
61
|
+
return str(value)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def build_json_payload(result: dict) -> dict:
|
|
65
|
+
"""将 ValidationExecutor.execute() 的结果转换为 JSON 输出契约结构。
|
|
66
|
+
|
|
67
|
+
契约字段必须齐全,无法取到的值填 null:
|
|
68
|
+
- tables 来自 validation_details.format_checks(表显示名),
|
|
69
|
+
行数优先取 raw_datasets 中的 DataFrame 长度,分块模式取 row_count
|
|
70
|
+
- summary 来自 validation_details.constraint_checks 的 passed 统计
|
|
71
|
+
- errors 中 table/column/constraint_type(check_type)/constraint_file
|
|
72
|
+
由校验链路提供(constraint_file 由 executor 挂载,见 P0-3)
|
|
73
|
+
- loading_errors 原样透传为 loading_warnings
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
result: ValidationExecutor.execute() 返回的结果字典
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
符合契约 v1 的字典(可直接 json.dumps)
|
|
80
|
+
"""
|
|
81
|
+
errors = result.get("errors", [])
|
|
82
|
+
validation_details = result.get("validation_details") or {}
|
|
83
|
+
constraint_checks = validation_details.get("constraint_checks") or []
|
|
84
|
+
format_checks = validation_details.get("format_checks") or []
|
|
85
|
+
raw_datasets = result.get("raw_datasets") or {}
|
|
86
|
+
|
|
87
|
+
tables: list[dict[str, Any]] = []
|
|
88
|
+
for check in format_checks:
|
|
89
|
+
# postprocess 已把 table 换成显示名并保留原 ID 到 table_id;
|
|
90
|
+
# raw_datasets 以表 ID 为键,优先按 ID 查(显示名与 ID 不同时也能命中行数)
|
|
91
|
+
dataset = raw_datasets.get(check.get("table_id") or check.get("table"))
|
|
92
|
+
rows: int | None = None
|
|
93
|
+
if isinstance(dataset, dict):
|
|
94
|
+
# 分块模式下 raw_datasets 值为 {"chunk_count": ..., "row_count": ...}
|
|
95
|
+
rows = dataset.get("row_count")
|
|
96
|
+
elif isinstance(dataset, Sized):
|
|
97
|
+
# 标准模式下值为 DataFrame,行数即长度
|
|
98
|
+
rows = len(dataset)
|
|
99
|
+
tables.append({"name": check.get("table"), "rows": rows})
|
|
100
|
+
|
|
101
|
+
constraints_total = len(constraint_checks)
|
|
102
|
+
constraints_failed = sum(1 for check in constraint_checks if not check.get("passed", True))
|
|
103
|
+
|
|
104
|
+
error_entries: list[dict[str, Any]] = []
|
|
105
|
+
for error in errors:
|
|
106
|
+
# 格式错误携带 cell_value/error_message;域约束错误携带 value/message,做双字段兼容
|
|
107
|
+
cell_value = error.get("cell_value") if "cell_value" in error else error.get("value")
|
|
108
|
+
error_message = error.get("error_message") if error.get("error_message") is not None else error.get("message")
|
|
109
|
+
error_entries.append(
|
|
110
|
+
{
|
|
111
|
+
"table": error.get("table"),
|
|
112
|
+
"column": error.get("column"),
|
|
113
|
+
"constraint_type": error.get("check_type"),
|
|
114
|
+
"constraint_file": error.get("constraint_file"),
|
|
115
|
+
"row_index": error.get("row_index"),
|
|
116
|
+
"cell_value": json_safe_value(cell_value),
|
|
117
|
+
"error_message": error_message,
|
|
118
|
+
# 可选修复建议(AllowedValues 相近值 / 日期布局提示等),生成器未给则为 null
|
|
119
|
+
"suggestion": error.get("suggestion"),
|
|
120
|
+
}
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
"schema_version": JSON_SCHEMA_VERSION,
|
|
125
|
+
"is_valid": not errors,
|
|
126
|
+
"interrupted": bool(result.get("interrupted", False)),
|
|
127
|
+
"duration_ms": result.get("duration_ms", 0),
|
|
128
|
+
"tables": tables,
|
|
129
|
+
"summary": {
|
|
130
|
+
"constraints_total": constraints_total,
|
|
131
|
+
"constraints_passed": constraints_total - constraints_failed,
|
|
132
|
+
"constraints_failed": constraints_failed,
|
|
133
|
+
},
|
|
134
|
+
"errors": error_entries,
|
|
135
|
+
"loading_warnings": result.get("loading_errors", []),
|
|
136
|
+
}
|