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,401 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2026 Precis Team
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
"""
|
|
17
|
+
@fileoverview 数据校验路由模块(基于文件上传)
|
|
18
|
+
|
|
19
|
+
功能概述:
|
|
20
|
+
- 提供基于文件上传的数据校验接口(非路径访问模式)
|
|
21
|
+
- 支持多种校验类型(唯一性、非空、允许值、外键、正则等)
|
|
22
|
+
- 支持正则表达式校验(含匹配模式、大小写敏感等参数)
|
|
23
|
+
- 支持批量校验请求处理
|
|
24
|
+
- 使用临时文件存储上传内容,处理完成后自动清理
|
|
25
|
+
- 通过 UnifiedValidationService 统一调度各类校验逻辑
|
|
26
|
+
|
|
27
|
+
架构设计:
|
|
28
|
+
- 基于 FastAPI APIRouter 组织路由,挂载到 /validation 前缀下
|
|
29
|
+
- 使用 UploadFile/Form 接收前端上传的文件和表单参数
|
|
30
|
+
- 统一返回 ValidationResponse / RegexValidationResponse 标准化响应结构
|
|
31
|
+
- 调用 UnifiedValidationService 和 load_file_data 完成数据加载与校验
|
|
32
|
+
|
|
33
|
+
输入示例:
|
|
34
|
+
POST /validate/content
|
|
35
|
+
FormData:
|
|
36
|
+
- file: users.xlsx (上传文件)
|
|
37
|
+
- validation_type: Unique
|
|
38
|
+
- target_column_name: email
|
|
39
|
+
- sheet_name: Sheet1
|
|
40
|
+
- header_row: 0
|
|
41
|
+
- validation_config: {"case_sensitive": true}
|
|
42
|
+
- allow_unsafe_eval: false
|
|
43
|
+
|
|
44
|
+
输出示例:
|
|
45
|
+
{
|
|
46
|
+
"success": true,
|
|
47
|
+
"validation_type": "Unique",
|
|
48
|
+
"data": {
|
|
49
|
+
"is_valid": false,
|
|
50
|
+
"error_count": 3,
|
|
51
|
+
"total_rows": 100,
|
|
52
|
+
"match_count": null,
|
|
53
|
+
"error_rows": [{"row_index": 5, "cell_value": "dup@example.com", "error_message": "重复值"}],
|
|
54
|
+
"validation_time": 0.12
|
|
55
|
+
},
|
|
56
|
+
"error": null
|
|
57
|
+
}
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
from __future__ import annotations
|
|
61
|
+
|
|
62
|
+
import asyncio
|
|
63
|
+
import logging
|
|
64
|
+
import os
|
|
65
|
+
import tempfile
|
|
66
|
+
from typing import Any
|
|
67
|
+
|
|
68
|
+
from fastapi import File, Form, HTTPException, UploadFile
|
|
69
|
+
|
|
70
|
+
from app.api.models import (
|
|
71
|
+
RegexValidationRequest,
|
|
72
|
+
RegexValidationResponse,
|
|
73
|
+
ValidationRequest,
|
|
74
|
+
ValidationResponse,
|
|
75
|
+
)
|
|
76
|
+
from app.api.routers.validation.common import (
|
|
77
|
+
convert_validation_result_to_regex,
|
|
78
|
+
execute_standard_validation,
|
|
79
|
+
)
|
|
80
|
+
from app.shared.services.preview.path_validation import validate_file_access
|
|
81
|
+
from app.shared.services.validation import UnifiedValidationService, ValidationType, load_file_data
|
|
82
|
+
|
|
83
|
+
from .router import router
|
|
84
|
+
|
|
85
|
+
logger = logging.getLogger(__name__)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@router.post(
|
|
89
|
+
"/validate",
|
|
90
|
+
response_model=ValidationResponse,
|
|
91
|
+
summary="基于 JSON 请求体的单条数据校验",
|
|
92
|
+
# 行为约定:本端点永远返回 200,业务失败以 success=false + error 字段表达,不产生 500
|
|
93
|
+
responses={
|
|
94
|
+
200: {
|
|
95
|
+
"description": "校验已执行;业务失败(文件未找到、路径非法、校验异常等)同样返回 200,以 success=false + error 字段表达",
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
)
|
|
99
|
+
def validate_data(request: ValidationRequest) -> ValidationResponse:
|
|
100
|
+
"""
|
|
101
|
+
基于 JSON 请求体的单条数据校验接口。
|
|
102
|
+
|
|
103
|
+
参数:
|
|
104
|
+
request (ValidationRequest): 校验请求体,包含文件路径、校验类型、目标列、配置等
|
|
105
|
+
|
|
106
|
+
返回值:
|
|
107
|
+
ValidationResponse: 标准化校验响应,包含校验结果、错误信息、统计数据等
|
|
108
|
+
|
|
109
|
+
业务逻辑:
|
|
110
|
+
1. 确定表头行号(默认第 0 行)
|
|
111
|
+
2. 调用 load_file_data 加载数据文件为 DataFrame
|
|
112
|
+
3. 检查目标列是否存在于 DataFrame 中
|
|
113
|
+
4. 调用 UnifiedValidationService.validate 执行具体校验逻辑
|
|
114
|
+
5. 将错误行数据转换为 ValidationErrorRow 列表
|
|
115
|
+
6. 组装 ValidationResult 并包装为 ValidationResponse 返回
|
|
116
|
+
7. 捕获文件未找到和通用异常,返回友好的错误响应
|
|
117
|
+
|
|
118
|
+
错误语义:
|
|
119
|
+
所有业务失败与意外异常(文件未找到、路径非法、校验出错等)均在本端点内
|
|
120
|
+
捕获并转为 success=false + error 字段随 HTTP 200 返回,不抛出 500。
|
|
121
|
+
"""
|
|
122
|
+
try:
|
|
123
|
+
# 校验文件路径格式合法性(防止路径遍历攻击)
|
|
124
|
+
if request.source_file_path:
|
|
125
|
+
validate_file_access(request.source_file_path)
|
|
126
|
+
|
|
127
|
+
return execute_standard_validation(
|
|
128
|
+
source_file_path=request.source_file_path,
|
|
129
|
+
sheet_name=request.sheet_name,
|
|
130
|
+
header_row=request.header_row,
|
|
131
|
+
validation_type=request.validation_type,
|
|
132
|
+
target_column_name=request.target_column_name,
|
|
133
|
+
validation_config=request.validation_config,
|
|
134
|
+
allow_unsafe_eval=request.allow_unsafe_eval,
|
|
135
|
+
column_data_type=request.column_data_type,
|
|
136
|
+
source_config=request.to_source_config(),
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
except FileNotFoundError as e:
|
|
140
|
+
# 文件未找到时返回明确的错误提示
|
|
141
|
+
return ValidationResponse(success=False, validation_type=request.validation_type, data=None, error=str(e))
|
|
142
|
+
except Exception as e:
|
|
143
|
+
# 捕获所有未预期异常,防止服务端崩溃,返回通用错误信息
|
|
144
|
+
return ValidationResponse(
|
|
145
|
+
success=False,
|
|
146
|
+
validation_type=request.validation_type,
|
|
147
|
+
data=None,
|
|
148
|
+
error=f"校验过程中发生错误: {str(e)}",
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@router.post(
|
|
153
|
+
"/validate/content",
|
|
154
|
+
response_model=ValidationResponse,
|
|
155
|
+
summary="基于文件上传的单条数据校验(Content 模式)",
|
|
156
|
+
# 行为约定:仅"文件内容为空"返回 400(HTTPException 显式抛出并透传),
|
|
157
|
+
# 其余业务失败一律被捕获并以 success=false + error 字段随 200 返回
|
|
158
|
+
responses={
|
|
159
|
+
200: {
|
|
160
|
+
"description": "校验已执行;除文件内容为空(400)外的业务失败同样返回 200,以 success=false + error 字段表达",
|
|
161
|
+
},
|
|
162
|
+
400: {"description": "文件内容为空"},
|
|
163
|
+
},
|
|
164
|
+
)
|
|
165
|
+
async def validate_data_with_file(
|
|
166
|
+
file: UploadFile = File(...),
|
|
167
|
+
validation_type: str = Form(...),
|
|
168
|
+
target_column_name: str = Form(...),
|
|
169
|
+
sheet_name: str = Form(None),
|
|
170
|
+
header_row: int = Form(0),
|
|
171
|
+
validation_config: str = Form(None),
|
|
172
|
+
allow_unsafe_eval: bool = Form(False),
|
|
173
|
+
) -> ValidationResponse:
|
|
174
|
+
"""
|
|
175
|
+
基于文件上传的单条数据校验接口(Content 模式)。
|
|
176
|
+
|
|
177
|
+
参数:
|
|
178
|
+
file (UploadFile): 上传的数据文件(Excel/CSV 等),必填
|
|
179
|
+
validation_type (str): 校验类型,如 Unique、NotNull、Regex 等,必填
|
|
180
|
+
target_column_name (str): 需要校验的目标列名,必填
|
|
181
|
+
sheet_name (str): Excel 工作表名称,可选,默认为 None
|
|
182
|
+
header_row (int): 表头所在行号,可选,默认为 0
|
|
183
|
+
validation_config (str): JSON 格式的校验配置字符串,可选
|
|
184
|
+
allow_unsafe_eval (bool): 是否允许不安全的表达式求值,可选,默认为 False
|
|
185
|
+
|
|
186
|
+
返回值:
|
|
187
|
+
ValidationResponse: 标准化校验响应
|
|
188
|
+
|
|
189
|
+
业务逻辑:
|
|
190
|
+
1. 接收上传的文件和表单参数
|
|
191
|
+
2. 读取文件内容并写入系统临时文件(带原文件扩展名)
|
|
192
|
+
3. 调用 load_file_data 从临时文件加载数据
|
|
193
|
+
4. 检查目标列是否存在
|
|
194
|
+
5. 解析 validation_config JSON 字符串为字典
|
|
195
|
+
6. 调用 UnifiedValidationService.validate 执行校验
|
|
196
|
+
7. 转换错误行数据并组装响应
|
|
197
|
+
8. 在 finally 块中清理临时文件,避免磁盘泄漏
|
|
198
|
+
9. 记录校验完成日志,捕获并处理所有异常
|
|
199
|
+
|
|
200
|
+
错误语义:
|
|
201
|
+
上传文件内容为空时抛出 HTTP 400(由框架直接处理);
|
|
202
|
+
其余业务失败与意外异常均被捕获并转为 success=false + error 字段随 HTTP 200 返回。
|
|
203
|
+
"""
|
|
204
|
+
tmp_path = None
|
|
205
|
+
try:
|
|
206
|
+
# 记录收到的校验请求基本信息,便于后续排查问题
|
|
207
|
+
logger.info(f"[VALIDATION] 收到FormData校验请求: {file.filename}")
|
|
208
|
+
logger.info(f"[VALIDATION] 校验类型: {validation_type}, 目标列: {target_column_name}")
|
|
209
|
+
|
|
210
|
+
# 获取上传文件的原始名称和扩展名,用于创建临时文件时保留格式
|
|
211
|
+
file_name = file.filename or "unknown"
|
|
212
|
+
file_ext = os.path.splitext(file_name)[1].lower()
|
|
213
|
+
# 异步读取上传文件的二进制内容
|
|
214
|
+
content = await file.read()
|
|
215
|
+
|
|
216
|
+
# 如果文件内容为空,直接返回 400 错误
|
|
217
|
+
if len(content) == 0:
|
|
218
|
+
raise HTTPException(status_code=400, detail="文件内容为空")
|
|
219
|
+
|
|
220
|
+
# 创建带原扩展名的临时文件,delete=False 确保我们可以手动控制删除时机
|
|
221
|
+
with tempfile.NamedTemporaryFile(suffix=file_ext, delete=False) as tmp:
|
|
222
|
+
tmp.write(content)
|
|
223
|
+
tmp_path = tmp.name
|
|
224
|
+
|
|
225
|
+
try:
|
|
226
|
+
# 解析前端传来的 JSON 配置字符串,解析失败则使用空配置并记录错误日志
|
|
227
|
+
config = {}
|
|
228
|
+
if validation_config:
|
|
229
|
+
import json
|
|
230
|
+
|
|
231
|
+
try:
|
|
232
|
+
config = json.loads(validation_config)
|
|
233
|
+
except json.JSONDecodeError:
|
|
234
|
+
logger.error("[VALIDATION] validation_config 解析失败,使用空配置")
|
|
235
|
+
|
|
236
|
+
# B-async: 将阻塞的 pandas I/O 与校验逻辑 offload 到线程池,避免阻塞事件循环。
|
|
237
|
+
# execute_standard_validation 内部调用 pd.read_csv/read_excel + 全量校验,是纯同步阻塞操作。
|
|
238
|
+
# agent 工具(apply_actions.py)已采用同样的 asyncio.to_thread 模式。
|
|
239
|
+
response = await asyncio.to_thread(
|
|
240
|
+
execute_standard_validation,
|
|
241
|
+
source_file_path=tmp_path,
|
|
242
|
+
sheet_name=sheet_name,
|
|
243
|
+
header_row=header_row,
|
|
244
|
+
validation_type=validation_type,
|
|
245
|
+
target_column_name=target_column_name,
|
|
246
|
+
validation_config=config,
|
|
247
|
+
allow_unsafe_eval=allow_unsafe_eval,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
if response.success and response.data:
|
|
251
|
+
logger.info(f"[VALIDATION] 校验完成: 成功={response.data.is_valid}, 错误数={response.data.error_count}")
|
|
252
|
+
|
|
253
|
+
return response
|
|
254
|
+
|
|
255
|
+
finally:
|
|
256
|
+
# 无论校验成功与否,都尝试删除临时文件,防止磁盘空间泄漏
|
|
257
|
+
if tmp_path and os.path.exists(tmp_path):
|
|
258
|
+
try:
|
|
259
|
+
os.unlink(tmp_path)
|
|
260
|
+
except Exception as e:
|
|
261
|
+
logger.error(f"[VALIDATION] 删除临时文件失败: {e}")
|
|
262
|
+
|
|
263
|
+
except HTTPException:
|
|
264
|
+
# FastAPI 的 HTTPException 直接抛出,由框架处理响应
|
|
265
|
+
raise
|
|
266
|
+
except Exception as e:
|
|
267
|
+
# 捕获所有未预期异常,记录详细错误堆栈并返回友好错误信息
|
|
268
|
+
logger.error(f"[VALIDATION] 校验异常: {str(e)}")
|
|
269
|
+
import traceback
|
|
270
|
+
|
|
271
|
+
traceback.print_exc()
|
|
272
|
+
return ValidationResponse(
|
|
273
|
+
success=False,
|
|
274
|
+
validation_type=validation_type,
|
|
275
|
+
data=None,
|
|
276
|
+
error=f"校验过程中发生错误: {str(e)}",
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
@router.post(
|
|
281
|
+
"/regex",
|
|
282
|
+
response_model=RegexValidationResponse,
|
|
283
|
+
summary="基于 JSON 请求体的正则表达式校验",
|
|
284
|
+
# 行为约定:本端点永远返回 200,业务失败以 success=false + error 字段表达,不产生 500
|
|
285
|
+
responses={
|
|
286
|
+
200: {
|
|
287
|
+
"description": "校验已执行;业务失败(文件未找到、路径非法、校验异常等)同样返回 200,以 success=false + error 字段表达",
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
)
|
|
291
|
+
def validate_regex(request: RegexValidationRequest) -> RegexValidationResponse:
|
|
292
|
+
"""
|
|
293
|
+
基于 JSON 请求体的正则表达式校验接口。
|
|
294
|
+
|
|
295
|
+
参数:
|
|
296
|
+
request (RegexValidationRequest): 正则校验请求体,包含文件路径、正则模式、
|
|
297
|
+
匹配模式、大小写敏感设置、目标列等
|
|
298
|
+
|
|
299
|
+
返回值:
|
|
300
|
+
RegexValidationResponse: 正则校验专用响应模型,包含匹配统计和错误行信息
|
|
301
|
+
|
|
302
|
+
业务逻辑:
|
|
303
|
+
1. 调用 load_file_data 加载数据文件,支持自定义表头
|
|
304
|
+
2. 检查目标列是否存在
|
|
305
|
+
3. 调用 UnifiedValidationService.validate 执行 REGEX 类型校验
|
|
306
|
+
4. 使用 convert_validation_result_to_regex 转换结果为正则响应模型
|
|
307
|
+
5. 返回标准化响应,捕获文件未找到和通用异常
|
|
308
|
+
|
|
309
|
+
错误语义:
|
|
310
|
+
所有业务失败与意外异常(文件未找到、路径非法、校验出错等)均在本端点内
|
|
311
|
+
捕获并转为 success=false + error 字段随 HTTP 200 返回,不抛出 500。
|
|
312
|
+
"""
|
|
313
|
+
try:
|
|
314
|
+
# 校验文件路径格式合法性(防止路径遍历攻击)
|
|
315
|
+
if request.source_file_path:
|
|
316
|
+
validate_file_access(request.source_file_path)
|
|
317
|
+
|
|
318
|
+
# 加载数据文件,支持自定义表头配置
|
|
319
|
+
df = load_file_data(
|
|
320
|
+
source_file_path=request.source_file_path,
|
|
321
|
+
sheet_name=request.sheet_name,
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
# 检查目标列是否存在于数据中
|
|
325
|
+
if request.target_column_name not in df.columns:
|
|
326
|
+
return RegexValidationResponse(success=False, data=None, error=f"未找到列: {request.target_column_name}")
|
|
327
|
+
|
|
328
|
+
# 调用统一校验服务执行正则校验,传入正则模式、标志、匹配模式和大小写敏感设置
|
|
329
|
+
result = UnifiedValidationService.validate(
|
|
330
|
+
validation_type=ValidationType.REGEX,
|
|
331
|
+
df=df,
|
|
332
|
+
column=request.target_column_name,
|
|
333
|
+
regex_pattern=request.regex_pattern,
|
|
334
|
+
regex_flags=request.regex_flags or "",
|
|
335
|
+
match_mode=request.match_mode,
|
|
336
|
+
case_sensitive=request.case_sensitive,
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
# 将通用校验结果转换为正则专用响应模型
|
|
340
|
+
regex_result = convert_validation_result_to_regex(result)
|
|
341
|
+
|
|
342
|
+
return RegexValidationResponse(success=True, data=regex_result, error=None)
|
|
343
|
+
|
|
344
|
+
except FileNotFoundError as e:
|
|
345
|
+
# 文件未找到时返回错误响应
|
|
346
|
+
return RegexValidationResponse(success=False, data=None, error=str(e))
|
|
347
|
+
except Exception as e:
|
|
348
|
+
# 通用异常处理,防止服务端崩溃
|
|
349
|
+
return RegexValidationResponse(success=False, data=None, error=f"校验过程中发生错误: {str(e)}")
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
@router.post(
|
|
353
|
+
"/validate/batch",
|
|
354
|
+
response_model=dict,
|
|
355
|
+
summary="批量数据校验",
|
|
356
|
+
# 行为约定:单条校验的失败已被 validate_data 逐条捕获,整体仍返回 200
|
|
357
|
+
responses={
|
|
358
|
+
200: {
|
|
359
|
+
"description": "批量校验已执行;单条失败不改变整体状态码,失败体现在该条目的 success=false 与 error 字段",
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
)
|
|
363
|
+
def validate_batch(requests: list[ValidationRequest]) -> dict[str, Any]:
|
|
364
|
+
"""
|
|
365
|
+
批量数据校验接口(基于 JSON 请求体列表)。
|
|
366
|
+
|
|
367
|
+
参数:
|
|
368
|
+
requests (list[ValidationRequest]): 校验请求对象列表,每个元素为独立的校验请求
|
|
369
|
+
|
|
370
|
+
返回值:
|
|
371
|
+
dict: 包含 results 键的字典,值为所有单条校验结果的列表
|
|
372
|
+
|
|
373
|
+
业务逻辑:
|
|
374
|
+
1. 遍历传入的请求列表
|
|
375
|
+
2. 对每个请求调用 validate_data 执行单条校验
|
|
376
|
+
3. 收集每条请求的校验类型、目标列、成功状态、结果数据和错误信息
|
|
377
|
+
4. 将所有结果组装为统一字典返回
|
|
378
|
+
|
|
379
|
+
错误语义:
|
|
380
|
+
单条请求的失败由 validate_data 捕获并写入对应条目的
|
|
381
|
+
success=false 与 error 字段,批量接口本身始终返回 200。
|
|
382
|
+
"""
|
|
383
|
+
# 存储所有批量校验结果的列表
|
|
384
|
+
results = []
|
|
385
|
+
# 逐条处理每个校验请求
|
|
386
|
+
for request in requests:
|
|
387
|
+
# 复用单条校验接口的逻辑
|
|
388
|
+
response = validate_data(request)
|
|
389
|
+
# 提取关键信息组装为简洁的结果字典
|
|
390
|
+
results.append(
|
|
391
|
+
{
|
|
392
|
+
"validation_type": request.validation_type,
|
|
393
|
+
"target_column": request.target_column_name,
|
|
394
|
+
"success": response.success,
|
|
395
|
+
# 如果校验成功且有数据,将结果对象转为字典;否则为 None
|
|
396
|
+
"result": response.data.model_dump() if response.data else None,
|
|
397
|
+
"error": response.error,
|
|
398
|
+
}
|
|
399
|
+
)
|
|
400
|
+
# 返回包含所有结果的响应字典
|
|
401
|
+
return {"results": results}
|
|
@@ -0,0 +1,235 @@
|
|
|
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 校验历史 API 端点
|
|
17
|
+
|
|
18
|
+
提供校验结果的持久化存储和查询接口。
|
|
19
|
+
|
|
20
|
+
安全约定:
|
|
21
|
+
- 项目路径统一从 `X-Project-Config-Path` Header 获取(经 get_project_config_path 校验),
|
|
22
|
+
不再从请求体/Query 接受 project_path,避免任意目录读写。
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import datetime
|
|
28
|
+
import logging
|
|
29
|
+
from typing import Any
|
|
30
|
+
|
|
31
|
+
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
32
|
+
from pydantic import BaseModel, Field
|
|
33
|
+
|
|
34
|
+
from app.api.dependencies import get_project_config_path
|
|
35
|
+
from app.shared.services.validation.history import (
|
|
36
|
+
ValidationHistoryStore,
|
|
37
|
+
ValidationRunRecord,
|
|
38
|
+
generate_run_id,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
logger = logging.getLogger(__name__)
|
|
42
|
+
|
|
43
|
+
router = APIRouter(prefix="/validation/history", tags=["validation-history"])
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _get_store(project_path: str) -> ValidationHistoryStore:
|
|
47
|
+
"""
|
|
48
|
+
@methoddesc 获取项目对应的校验历史存储实例
|
|
49
|
+
|
|
50
|
+
每次请求重新创建实例,确保数据最新(本地文件读取开销极小)
|
|
51
|
+
|
|
52
|
+
参数:
|
|
53
|
+
project_path: 项目配置根目录(已由 get_project_config_path 校验为存在的绝对路径)
|
|
54
|
+
|
|
55
|
+
返回:
|
|
56
|
+
ValidationHistoryStore 实例
|
|
57
|
+
"""
|
|
58
|
+
return ValidationHistoryStore(project_path)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class SaveRunRequest(BaseModel):
|
|
62
|
+
"""保存校验运行记录请求(项目路径从 Header 注入,不在 body 中)"""
|
|
63
|
+
|
|
64
|
+
duration_ms: int = Field(..., description="校验耗时(毫秒)")
|
|
65
|
+
scope: str = Field(default="full", description="校验范围(full/incremental)")
|
|
66
|
+
summary: dict[str, Any] = Field(..., description="汇总统计")
|
|
67
|
+
by_type: dict[str, dict[str, int]] = Field(default_factory=dict, description="按校验类型统计")
|
|
68
|
+
by_table: dict[str, dict[str, int]] = Field(default_factory=dict, description="按表统计")
|
|
69
|
+
errors: list[dict[str, Any]] = Field(default_factory=list, description="错误列表")
|
|
70
|
+
warnings: list[str] = Field(default_factory=list, description="警告信息")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@router.post(
|
|
74
|
+
"",
|
|
75
|
+
summary="保存校验运行记录",
|
|
76
|
+
responses={
|
|
77
|
+
500: {"description": "服务器内部错误"},
|
|
78
|
+
},
|
|
79
|
+
)
|
|
80
|
+
def save_run(req: SaveRunRequest, project_path: str = Depends(get_project_config_path)) -> dict[str, Any]:
|
|
81
|
+
"""
|
|
82
|
+
@methoddesc 保存一次校验运行的完整记录
|
|
83
|
+
|
|
84
|
+
业务用途:
|
|
85
|
+
- 由前端在执行全量/单条校验后调用,将结果落地到项目本地
|
|
86
|
+
- 自动生成 run_id 与时间戳,供后续查询/统计/历史面板展示
|
|
87
|
+
|
|
88
|
+
参数:
|
|
89
|
+
req: 包含运行汇总、按类型/表分组统计、错误列表等的请求
|
|
90
|
+
project_path: 项目配置根目录(Header 注入)
|
|
91
|
+
|
|
92
|
+
返回:
|
|
93
|
+
{"success": True, "run_id": str} 字典
|
|
94
|
+
"""
|
|
95
|
+
store = _get_store(project_path)
|
|
96
|
+
record = ValidationRunRecord(
|
|
97
|
+
id=generate_run_id(),
|
|
98
|
+
timestamp=datetime.datetime.now().isoformat(),
|
|
99
|
+
duration_ms=req.duration_ms,
|
|
100
|
+
scope=req.scope,
|
|
101
|
+
summary=req.summary,
|
|
102
|
+
by_type=req.by_type,
|
|
103
|
+
by_table=req.by_table,
|
|
104
|
+
errors=req.errors,
|
|
105
|
+
warnings=req.warnings,
|
|
106
|
+
)
|
|
107
|
+
run_id = store.add_run(record)
|
|
108
|
+
return {"success": True, "run_id": run_id}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@router.get(
|
|
112
|
+
"",
|
|
113
|
+
summary="获取校验运行记录列表",
|
|
114
|
+
responses={
|
|
115
|
+
500: {"description": "服务器内部错误"},
|
|
116
|
+
},
|
|
117
|
+
)
|
|
118
|
+
def list_runs(
|
|
119
|
+
project_path: str = Depends(get_project_config_path),
|
|
120
|
+
limit: int = Query(20, ge=1, le=100),
|
|
121
|
+
offset: int = Query(0, ge=0),
|
|
122
|
+
) -> dict[str, Any]:
|
|
123
|
+
"""
|
|
124
|
+
@methoddesc 分页获取校验运行记录列表
|
|
125
|
+
|
|
126
|
+
业务用途:
|
|
127
|
+
- 为前端历史记录面板提供数据
|
|
128
|
+
- 按时间倒序返回,limit 限制最大 100,offset 用于分页
|
|
129
|
+
|
|
130
|
+
参数:
|
|
131
|
+
project_path: 项目路径(Header 注入)
|
|
132
|
+
limit: 每页大小,1-100
|
|
133
|
+
offset: 偏移量
|
|
134
|
+
|
|
135
|
+
返回:
|
|
136
|
+
包含 runs/items/total 字段的字典
|
|
137
|
+
"""
|
|
138
|
+
store = _get_store(project_path)
|
|
139
|
+
return store.get_runs(limit=limit, offset=offset)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@router.get(
|
|
143
|
+
"/stats",
|
|
144
|
+
summary="获取校验运行统计信息",
|
|
145
|
+
responses={
|
|
146
|
+
500: {"description": "服务器内部错误"},
|
|
147
|
+
},
|
|
148
|
+
)
|
|
149
|
+
def get_stats(
|
|
150
|
+
project_path: str = Depends(get_project_config_path),
|
|
151
|
+
last_n: int = Query(10, ge=1, le=50),
|
|
152
|
+
) -> dict[str, Any]:
|
|
153
|
+
"""
|
|
154
|
+
@methoddesc 获取最近 N 次校验运行的统计信息
|
|
155
|
+
|
|
156
|
+
业务用途:
|
|
157
|
+
- 用于前端仪表盘展示趋势、平均耗时、通过率等指标
|
|
158
|
+
|
|
159
|
+
参数:
|
|
160
|
+
project_path: 项目路径(Header 注入)
|
|
161
|
+
last_n: 统计最近 N 条记录,1-50
|
|
162
|
+
|
|
163
|
+
返回:
|
|
164
|
+
统计信息字典
|
|
165
|
+
"""
|
|
166
|
+
store = _get_store(project_path)
|
|
167
|
+
return store.get_stats(last_n=last_n)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@router.get(
|
|
171
|
+
"/{run_id}",
|
|
172
|
+
summary="获取单条校验运行记录",
|
|
173
|
+
responses={
|
|
174
|
+
404: {"description": "记录不存在"},
|
|
175
|
+
},
|
|
176
|
+
)
|
|
177
|
+
def get_run(
|
|
178
|
+
run_id: str,
|
|
179
|
+
project_path: str = Depends(get_project_config_path),
|
|
180
|
+
) -> dict[str, Any]:
|
|
181
|
+
"""
|
|
182
|
+
@methoddesc 获取指定 run_id 的完整校验运行记录
|
|
183
|
+
|
|
184
|
+
业务用途:
|
|
185
|
+
- 用户在历史面板点击某条记录时展示完整错误详情
|
|
186
|
+
|
|
187
|
+
参数:
|
|
188
|
+
run_id: 运行记录唯一标识
|
|
189
|
+
project_path: 项目路径(Header 注入)
|
|
190
|
+
|
|
191
|
+
返回:
|
|
192
|
+
完整运行记录字典
|
|
193
|
+
|
|
194
|
+
异常:
|
|
195
|
+
HTTPException: 记录不存在(404)
|
|
196
|
+
"""
|
|
197
|
+
store = _get_store(project_path)
|
|
198
|
+
run = store.get_run(run_id)
|
|
199
|
+
if not run:
|
|
200
|
+
raise HTTPException(status_code=404, detail=f"记录不存在: {run_id}")
|
|
201
|
+
return run
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@router.delete(
|
|
205
|
+
"/{run_id}",
|
|
206
|
+
summary="删除校验运行记录",
|
|
207
|
+
responses={
|
|
208
|
+
404: {"description": "记录不存在"},
|
|
209
|
+
},
|
|
210
|
+
)
|
|
211
|
+
def delete_run(
|
|
212
|
+
run_id: str,
|
|
213
|
+
project_path: str = Depends(get_project_config_path),
|
|
214
|
+
) -> dict[str, Any]:
|
|
215
|
+
"""
|
|
216
|
+
@methoddesc 删除指定 run_id 的校验运行记录
|
|
217
|
+
|
|
218
|
+
业务用途:
|
|
219
|
+
- 用户在历史面板点击"删除"按钮时调用
|
|
220
|
+
|
|
221
|
+
参数:
|
|
222
|
+
run_id: 运行记录唯一标识
|
|
223
|
+
project_path: 项目路径(Header 注入)
|
|
224
|
+
|
|
225
|
+
返回:
|
|
226
|
+
{"success": True} 字典
|
|
227
|
+
|
|
228
|
+
异常:
|
|
229
|
+
HTTPException: 记录不存在(404)
|
|
230
|
+
"""
|
|
231
|
+
store = _get_store(project_path)
|
|
232
|
+
deleted = store.delete_run(run_id)
|
|
233
|
+
if not deleted:
|
|
234
|
+
raise HTTPException(status_code=404, detail=f"记录不存在: {run_id}")
|
|
235
|
+
return {"success": True}
|