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,170 @@
|
|
|
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 CLI Shell 系统命令模块
|
|
18
|
+
|
|
19
|
+
功能概述:
|
|
20
|
+
- 提供 pwd 命令显示当前工作目录或已打开的项目路径
|
|
21
|
+
- 提供 ls 命令列出项目目录或指定路径下的文件和文件夹
|
|
22
|
+
- 支持相对路径和绝对路径,自动区分文件与目录并按名称排序
|
|
23
|
+
|
|
24
|
+
架构设计:
|
|
25
|
+
- PwdCommand 和 LsCommand 均继承自 Command 基类
|
|
26
|
+
- LsCommand 未指定路径时优先显示项目根目录,其次显示系统工作目录
|
|
27
|
+
- 使用 lambda 排序实现先文件夹后文件的展示效果
|
|
28
|
+
|
|
29
|
+
输入示例:
|
|
30
|
+
precis> pwd
|
|
31
|
+
precis> ls
|
|
32
|
+
precis> ls ../data
|
|
33
|
+
|
|
34
|
+
输出示例:
|
|
35
|
+
目录: D:/Project/Precis
|
|
36
|
+
📁 backend/
|
|
37
|
+
📁 frontend/
|
|
38
|
+
📄 README.md
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
import os
|
|
42
|
+
|
|
43
|
+
from app.cli.shell.commands.base import Command, CommandResult, ProjectContext
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class PwdCommand(Command):
|
|
47
|
+
"""显示当前工作路径命令。
|
|
48
|
+
|
|
49
|
+
显示系统当前工作目录,如果已打开项目则同时显示项目路径。
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(self) -> None:
|
|
53
|
+
super().__init__("pwd", aliases=["cwd"])
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def description(self) -> str:
|
|
57
|
+
return "显示当前工作目录或已打开的项目路径"
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def usage(self) -> str:
|
|
61
|
+
return "pwd"
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def help_text(self) -> str:
|
|
65
|
+
return "显示当前系统的工作目录路径。\n\n如果已经通过 'open' 命令打开了项目,它会同时显示当前项目所在的路径。"
|
|
66
|
+
|
|
67
|
+
def execute(self, args: list[str], context: ProjectContext) -> CommandResult:
|
|
68
|
+
"""执行 pwd 命令。
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
args: 命令参数列表(此命令不需要参数)
|
|
72
|
+
context: 项目上下文
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
包含当前工作目录和项目路径的结果
|
|
76
|
+
"""
|
|
77
|
+
cwd = os.getcwd()
|
|
78
|
+
result_text = f"当前系统工作目录: {cwd}"
|
|
79
|
+
|
|
80
|
+
if context.is_project_open:
|
|
81
|
+
result_text += f"\n当前已打开项目路径: {context.project_path}"
|
|
82
|
+
|
|
83
|
+
return CommandResult.ok(result_text)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class LsCommand(Command):
|
|
87
|
+
"""列出目录内容命令。
|
|
88
|
+
|
|
89
|
+
列出指定目录下的文件和文件夹,未指定路径时默认显示项目根目录。
|
|
90
|
+
支持相对路径和绝对路径。
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
def __init__(self) -> None:
|
|
94
|
+
super().__init__("ls", aliases=["dir"])
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def description(self) -> str:
|
|
98
|
+
return "列出当前目录下的文件"
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def usage(self) -> str:
|
|
102
|
+
return "ls [path]"
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def help_text(self) -> str:
|
|
106
|
+
return (
|
|
107
|
+
"列出指定目录下的文件和文件夹列表。\n\n"
|
|
108
|
+
"如果没有提供 path 参数:\n"
|
|
109
|
+
" - 若已打开项目,将列出项目根目录的内容。\n"
|
|
110
|
+
" - 若未打开项目,将列出当前系统工作目录的内容。\n\n"
|
|
111
|
+
"示例:\n"
|
|
112
|
+
" ls (列出当前或项目目录)\n"
|
|
113
|
+
" ls ../data (列出相对路径目录)\n"
|
|
114
|
+
" ls /var/log (列出绝对路径目录)"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
def execute(self, args: list[str], context: ProjectContext) -> CommandResult:
|
|
118
|
+
"""执行 ls 命令。
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
args: 命令参数列表,可能包含要列出的路径
|
|
122
|
+
context: 项目上下文
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
包含目录内容的格式化结果,或错误提示
|
|
126
|
+
"""
|
|
127
|
+
if args:
|
|
128
|
+
target_path = args[0]
|
|
129
|
+
elif context.is_project_open:
|
|
130
|
+
project_path = context.project_path
|
|
131
|
+
if project_path is None:
|
|
132
|
+
target_path = os.getcwd()
|
|
133
|
+
else:
|
|
134
|
+
target_path = project_path
|
|
135
|
+
else:
|
|
136
|
+
target_path = os.getcwd()
|
|
137
|
+
|
|
138
|
+
# 展开用户主目录符号(如 ~)并转换为绝对路径
|
|
139
|
+
target_path = os.path.abspath(os.path.expanduser(target_path))
|
|
140
|
+
|
|
141
|
+
if not os.path.exists(target_path):
|
|
142
|
+
return CommandResult.error(f"路径不存在: {target_path}")
|
|
143
|
+
|
|
144
|
+
if not os.path.isdir(target_path):
|
|
145
|
+
return CommandResult.error(f"非目录路径: {target_path}")
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
items = os.listdir(target_path)
|
|
149
|
+
# 排序逻辑:先文件夹后文件,然后按名称字母顺序排序
|
|
150
|
+
items.sort(key=lambda x: (not os.path.isdir(os.path.join(target_path, x)), x.lower()))
|
|
151
|
+
|
|
152
|
+
output = []
|
|
153
|
+
output.append(f"目录: {target_path}\n")
|
|
154
|
+
|
|
155
|
+
for item in items:
|
|
156
|
+
item_path = os.path.join(target_path, item)
|
|
157
|
+
if os.path.isdir(item_path):
|
|
158
|
+
output.append(f" 📁 {item}/")
|
|
159
|
+
else:
|
|
160
|
+
output.append(f" 📄 {item}")
|
|
161
|
+
|
|
162
|
+
if not items:
|
|
163
|
+
output.append(" (空目录)")
|
|
164
|
+
|
|
165
|
+
return CommandResult.ok("\n".join(output))
|
|
166
|
+
|
|
167
|
+
except PermissionError:
|
|
168
|
+
return CommandResult.error(f"没有权限读取目录: {target_path}")
|
|
169
|
+
except Exception as e:
|
|
170
|
+
return CommandResult.error(f"无法读取目录: {str(e)}")
|
|
@@ -0,0 +1,455 @@
|
|
|
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
|
+
# backend/app/cli/shell/commands/validate.py
|
|
17
|
+
"""
|
|
18
|
+
@fileoverview CLI Shell 数据校验命令模块
|
|
19
|
+
|
|
20
|
+
功能概述:
|
|
21
|
+
- 提供 validate 命令在 Shell 中执行数据校验
|
|
22
|
+
- 支持按表名过滤或校验全部数据表
|
|
23
|
+
- 支持 standalone 模式:通过 --manifest / --data-directory 参数直接执行,无需先 open 项目
|
|
24
|
+
- 集成 Spinner 动画与格式化结果输出
|
|
25
|
+
- 从项目配置中读取校验超时时间和脚本安全设置
|
|
26
|
+
|
|
27
|
+
架构设计:
|
|
28
|
+
- ValidateCommand 继承 Command 基类
|
|
29
|
+
- 两种工作模式:
|
|
30
|
+
- Shell 模式: 先 open 项目,再 validate [table_name],从上下文读取设置
|
|
31
|
+
- Standalone 模式: validate --manifest <path> [--data-directory <path>] [--table <name>]
|
|
32
|
+
不依赖项目上下文,适合脚本自动化 / CI 场景
|
|
33
|
+
- 委托 ValidationExecutor 执行核心校验逻辑
|
|
34
|
+
- 使用 Spinner 在终端显示加载动画
|
|
35
|
+
- 通过 Formatter 格式化输出校验结果
|
|
36
|
+
|
|
37
|
+
输入示例:
|
|
38
|
+
# Shell 模式(交互式)
|
|
39
|
+
precis> open /my/project
|
|
40
|
+
precis> validate
|
|
41
|
+
precis> validate users
|
|
42
|
+
|
|
43
|
+
# Standalone 模式(单次执行,适合自动化)
|
|
44
|
+
precis validate --manifest /my/project/project.precis.yaml
|
|
45
|
+
precis validate --manifest /my/project/project.precis.yaml --data-directory /my/project/data --table users
|
|
46
|
+
precis validate --manifest /my/project/project.precis.yaml --format json
|
|
47
|
+
|
|
48
|
+
输出示例:
|
|
49
|
+
CommandResult.ok("验证通过", data={"errors": [], "duration_ms": 120})
|
|
50
|
+
CommandResult.error("验证完成,发现 3 个错误", data={"errors": [...], "duration_ms": 120})
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
import json
|
|
54
|
+
import os
|
|
55
|
+
import sys
|
|
56
|
+
|
|
57
|
+
from rich.console import Console
|
|
58
|
+
|
|
59
|
+
from app.cli.shell.commands.base import Command, CommandResult, ProjectContext
|
|
60
|
+
from app.cli.shell.exceptions import ValidationError
|
|
61
|
+
from app.cli.shell.formatter import Formatter, Spinner
|
|
62
|
+
from app.shared.services.validation.json_payload import build_json_payload as _build_json_payload
|
|
63
|
+
|
|
64
|
+
_console = Console()
|
|
65
|
+
|
|
66
|
+
# --format 的合法取值(JSON 契约版本号单一事实源在 validation.json_payload)
|
|
67
|
+
_FORMAT_CHOICES = ("human", "json")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# 选项表(单一事实源):standalone 解析与 Shell 模式选项剥离共用
|
|
71
|
+
_STANDALONE_OPTIONS: dict[str, str] = {
|
|
72
|
+
"--manifest": "manifest",
|
|
73
|
+
"-m": "manifest",
|
|
74
|
+
"--data-directory": "data_directory",
|
|
75
|
+
"-d": "data_directory",
|
|
76
|
+
"--table": "table",
|
|
77
|
+
"-t": "table",
|
|
78
|
+
"--format": "format",
|
|
79
|
+
"--report": "report",
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _parse_standalone_args(args: list[str]) -> dict:
|
|
84
|
+
"""解析 standalone 模式的命名参数。
|
|
85
|
+
|
|
86
|
+
从参数列表中提取 --manifest/-m、--data-directory/-d、--table/-t、--format 选项。
|
|
87
|
+
不以 -- 或 - 开头的裸参数在无 --manifest 时被忽略(由 Shell 模式处理)。
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
args: 命令参数列表
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
包含 manifest、data_directory、table、format、report 键的字典,未提供的键值为 None
|
|
94
|
+
"""
|
|
95
|
+
result: dict[str, str | None] = {
|
|
96
|
+
"manifest": None,
|
|
97
|
+
"data_directory": None,
|
|
98
|
+
"table": None,
|
|
99
|
+
"format": None,
|
|
100
|
+
"report": None,
|
|
101
|
+
}
|
|
102
|
+
i = 0
|
|
103
|
+
while i < len(args):
|
|
104
|
+
arg = args[i]
|
|
105
|
+
key = _STANDALONE_OPTIONS.get(arg)
|
|
106
|
+
if key and i + 1 < len(args):
|
|
107
|
+
result[key] = args[i + 1]
|
|
108
|
+
i += 2
|
|
109
|
+
else:
|
|
110
|
+
i += 1
|
|
111
|
+
return result
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _split_positional_args(args: list[str]) -> list[str]:
|
|
115
|
+
"""剥离 args 中的选项及选项值,返回剩余位置参数。
|
|
116
|
+
|
|
117
|
+
Shell 模式下用户可能混用选项(如 `validate --table users`),
|
|
118
|
+
直接取 args[0] 当表名会把 "--table" 当成表名。此函数按选项表
|
|
119
|
+
跳过每个选项及其值;尾部悬挂选项(缺值)仅跳过自身。
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
args: 命令参数列表
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
剩余的位置参数列表
|
|
126
|
+
"""
|
|
127
|
+
positional: list[str] = []
|
|
128
|
+
i = 0
|
|
129
|
+
while i < len(args):
|
|
130
|
+
arg = args[i]
|
|
131
|
+
if arg in _STANDALONE_OPTIONS and i + 1 < len(args):
|
|
132
|
+
i += 2 # 跳过选项及其值
|
|
133
|
+
elif arg in _STANDALONE_OPTIONS:
|
|
134
|
+
i += 1 # 尾部悬挂选项(缺值),仅跳过自身
|
|
135
|
+
else:
|
|
136
|
+
positional.append(arg)
|
|
137
|
+
i += 1
|
|
138
|
+
return positional
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class ValidateCommand(Command):
|
|
142
|
+
"""数据验证命令。
|
|
143
|
+
|
|
144
|
+
根据项目配置对数据文件执行校验,可指定表名或验证全部。
|
|
145
|
+
支持别名 'check'。
|
|
146
|
+
|
|
147
|
+
两种工作模式:
|
|
148
|
+
- Shell 模式: 需先 open 项目,从上下文读取设置
|
|
149
|
+
- Standalone 模式: 通过 --manifest 指定清单文件,不依赖项目上下文
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
def __init__(self) -> None:
|
|
153
|
+
super().__init__("validate", aliases=["check"])
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def description(self) -> str:
|
|
157
|
+
return "执行数据验证,可指定表名或验证全部"
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def usage(self) -> str:
|
|
161
|
+
return (
|
|
162
|
+
"validate [table_name]\n"
|
|
163
|
+
" validate --manifest <path> [--data-directory <path>] [--table <name>] "
|
|
164
|
+
"[--format human|json] [--report <path>]"
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
def execute(self, args: list[str], context: ProjectContext) -> CommandResult:
|
|
168
|
+
"""执行数据校验命令。
|
|
169
|
+
|
|
170
|
+
自动检测工作模式:
|
|
171
|
+
- 如果 args 中包含 --manifest/-m,进入 standalone 模式
|
|
172
|
+
- 否则进入 Shell 模式(要求已 open 项目)
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
args: 命令参数列表
|
|
176
|
+
context: 项目上下文
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
校验结果,成功表示无错误,失败表示发现数据问题
|
|
180
|
+
|
|
181
|
+
Raises:
|
|
182
|
+
ValidationError: 当校验过程中发生异常时抛出
|
|
183
|
+
"""
|
|
184
|
+
# 检测是否为 standalone 模式
|
|
185
|
+
parsed = _parse_standalone_args(args)
|
|
186
|
+
if parsed["manifest"] is not None:
|
|
187
|
+
return self._execute_standalone(parsed)
|
|
188
|
+
|
|
189
|
+
# Shell 模式:要求已打开项目
|
|
190
|
+
return self._execute_shell(args, context)
|
|
191
|
+
|
|
192
|
+
def _execute_shell(self, args: list[str], context: ProjectContext) -> CommandResult:
|
|
193
|
+
"""Shell 模式执行校验。
|
|
194
|
+
|
|
195
|
+
从项目上下文中读取路径和设置,执行校验。
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
args: 命令参数列表,可能包含表名
|
|
199
|
+
context: 命令上下文,必须包含已打开的项目
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
校验结果
|
|
203
|
+
"""
|
|
204
|
+
project_path = context.project_path
|
|
205
|
+
if project_path is None:
|
|
206
|
+
return CommandResult.error("未打开项目,请先使用 'open <path>' 命令打开项目")
|
|
207
|
+
|
|
208
|
+
# 表名过滤来源优先级:--table/-t 选项 > 首个位置参数 > None(校验全部)
|
|
209
|
+
# 位置参数须先剥离选项及选项值,否则 `validate --table users` 会把 "--table" 当表名
|
|
210
|
+
parsed_shell = _parse_standalone_args(args)
|
|
211
|
+
positional = _split_positional_args(args)
|
|
212
|
+
table_name = parsed_shell["table"] or (positional[0] if positional else None)
|
|
213
|
+
|
|
214
|
+
# 构建清单文件和数据目录路径
|
|
215
|
+
manifest_path = os.path.join(project_path, "project.precis.yaml")
|
|
216
|
+
data_dir = project_path
|
|
217
|
+
|
|
218
|
+
# 从项目配置中读取设置
|
|
219
|
+
validation_settings = context.project_config.get("validation", {}) if context.project_config else {}
|
|
220
|
+
script_security = context.project_config.get("script_security", {}) if context.project_config else {}
|
|
221
|
+
|
|
222
|
+
return self._run_validation(
|
|
223
|
+
manifest_path,
|
|
224
|
+
data_dir,
|
|
225
|
+
table_name,
|
|
226
|
+
validation_settings,
|
|
227
|
+
script_security,
|
|
228
|
+
# 契约:--format 仅 standalone 模式生效,Shell/REPL 一律 human 输出
|
|
229
|
+
"human",
|
|
230
|
+
parsed_shell["report"],
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
def _execute_standalone(self, parsed: dict) -> CommandResult:
|
|
234
|
+
"""Standalone 模式执行校验。
|
|
235
|
+
|
|
236
|
+
直接从命令行参数获取路径,不依赖项目上下文。
|
|
237
|
+
使用默认设置(timeout=30, 安全沙箱)。
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
parsed: 解析后的参数字典,包含 manifest/data_directory/table/format/report
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
校验结果
|
|
244
|
+
"""
|
|
245
|
+
# 输出格式:--format 缺省为 human(与历史行为完全一致),仅 standalone 模式生效
|
|
246
|
+
output_format = (parsed["format"] or "human").strip().lower()
|
|
247
|
+
if output_format not in _FORMAT_CHOICES:
|
|
248
|
+
return CommandResult.error(
|
|
249
|
+
f"--format 仅支持 {' 或 '.join(_FORMAT_CHOICES)},收到: {parsed['format']}",
|
|
250
|
+
exit_code=2,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
manifest_path = os.path.abspath(parsed["manifest"])
|
|
254
|
+
|
|
255
|
+
if not os.path.exists(manifest_path):
|
|
256
|
+
return CommandResult.error(f"清单文件不存在: {manifest_path}", exit_code=2)
|
|
257
|
+
if os.path.isdir(manifest_path):
|
|
258
|
+
# 指向目录时 open() 在 Windows 抛 PermissionError、Linux 抛 IsADirectoryError,
|
|
259
|
+
# 消息均误导排障方向("权限"而非"这是目录")——前置判明
|
|
260
|
+
return CommandResult.error(
|
|
261
|
+
f"清单路径是一个目录,请指向项目清单文件(project.precis.yaml): {manifest_path}",
|
|
262
|
+
exit_code=2,
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
# 数据目录:显式指定 > 清单文件所在目录
|
|
266
|
+
if parsed["data_directory"]:
|
|
267
|
+
data_dir = os.path.abspath(parsed["data_directory"])
|
|
268
|
+
else:
|
|
269
|
+
data_dir = os.path.dirname(manifest_path)
|
|
270
|
+
|
|
271
|
+
if not os.path.isdir(data_dir):
|
|
272
|
+
return CommandResult.error(f"数据目录不存在: {data_dir}", exit_code=2)
|
|
273
|
+
|
|
274
|
+
table_name = parsed["table"]
|
|
275
|
+
|
|
276
|
+
# standalone 模式使用默认设置
|
|
277
|
+
return self._run_validation(manifest_path, data_dir, table_name, {}, {}, output_format, parsed["report"])
|
|
278
|
+
|
|
279
|
+
def _run_validation(
|
|
280
|
+
self,
|
|
281
|
+
manifest_path: str,
|
|
282
|
+
data_dir: str,
|
|
283
|
+
table_name: str | None,
|
|
284
|
+
validation_settings: dict,
|
|
285
|
+
script_security: dict,
|
|
286
|
+
output_format: str = "human",
|
|
287
|
+
report_path: str | None = None,
|
|
288
|
+
) -> CommandResult:
|
|
289
|
+
"""执行校验的核心逻辑,Shell 和 Standalone 模式共享。
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
manifest_path: 清单文件路径
|
|
293
|
+
data_dir: 数据目录路径
|
|
294
|
+
table_name: 可选的表名过滤
|
|
295
|
+
validation_settings: 校验设置字典
|
|
296
|
+
script_security: 脚本安全设置字典
|
|
297
|
+
output_format: 输出格式,"human"(rich 人类可读,默认)或 "json"
|
|
298
|
+
(stdout 仅输出单个 JSON 文档,供 agent/CI 消费)
|
|
299
|
+
report_path: 可选的报告输出路径(.html/.xlsx,按扩展名分派;
|
|
300
|
+
与 --format json 可同时使用,报告与 JSON 内容同源)
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
校验结果
|
|
304
|
+
|
|
305
|
+
Raises:
|
|
306
|
+
ValidationError: 当校验过程中发生异常时抛出
|
|
307
|
+
"""
|
|
308
|
+
# 配置自检(inspect_config)在 load_project 内部执行,会通过 logger
|
|
309
|
+
# 向 stderr 输出 "WARNING: [配置自检] ..." 噪声行。这些问题的详情已通过
|
|
310
|
+
# loading_errors 结构化返回并在下方"加载警告"区展示,日志行属重复噪声。
|
|
311
|
+
# 校验期间临时调高 inspector logger 级别以抑制日志,结束后在 finally 恢复。
|
|
312
|
+
import logging
|
|
313
|
+
|
|
314
|
+
inspector_logger = logging.getLogger("app.shared.core.project.loader.loader_parts.config_inspector")
|
|
315
|
+
_prev_level = inspector_logger.level
|
|
316
|
+
inspector_logger.setLevel(logging.ERROR)
|
|
317
|
+
|
|
318
|
+
try:
|
|
319
|
+
from app.shared.services.validation.executor import ValidationExecutor, ValidationOptions
|
|
320
|
+
|
|
321
|
+
timeout_seconds = int(validation_settings.get("timeout_seconds", 30))
|
|
322
|
+
if timeout_seconds <= 0:
|
|
323
|
+
timeout_seconds = 30
|
|
324
|
+
|
|
325
|
+
# 脚本执行权限:显式传入时优先使用传入值,否则回退到项目配置
|
|
326
|
+
allow_eval = script_security.get("allow_eval")
|
|
327
|
+
allow_exec = script_security.get("allow_exec")
|
|
328
|
+
if allow_eval is None and allow_exec is None:
|
|
329
|
+
allow_unsafe_eval = None
|
|
330
|
+
else:
|
|
331
|
+
allow_unsafe_eval = bool(allow_eval or allow_exec)
|
|
332
|
+
|
|
333
|
+
options = ValidationOptions(
|
|
334
|
+
timeout_seconds=timeout_seconds,
|
|
335
|
+
allow_unsafe_eval=allow_unsafe_eval,
|
|
336
|
+
table_filter=table_name,
|
|
337
|
+
# C6 遇错即停:从项目配置读 error_handling(stop 时发现首个错误即停止)
|
|
338
|
+
error_handling=validation_settings.get("error_handling", "continue"),
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
executor = ValidationExecutor(manifest_path)
|
|
342
|
+
|
|
343
|
+
# JSON 模式下抑制全部人类可读输出(header/Spinner/摘要/结果),
|
|
344
|
+
# 保证 stdout 只含单个 JSON 文档
|
|
345
|
+
json_mode = output_format == "json"
|
|
346
|
+
|
|
347
|
+
spinner: Spinner | None = None if json_mode else Spinner("正在校验数据")
|
|
348
|
+
if spinner is not None:
|
|
349
|
+
Formatter.print_header("开始执行数据校验")
|
|
350
|
+
spinner.start()
|
|
351
|
+
|
|
352
|
+
try:
|
|
353
|
+
result = executor.execute(data_dir, options)
|
|
354
|
+
finally:
|
|
355
|
+
if spinner is not None:
|
|
356
|
+
spinner.stop(success=True)
|
|
357
|
+
|
|
358
|
+
errors = result.get("errors", [])
|
|
359
|
+
duration_ms = result.get("duration_ms", 0)
|
|
360
|
+
|
|
361
|
+
if json_mode:
|
|
362
|
+
payload = _build_json_payload(result)
|
|
363
|
+
# --report 与 --format json 可并用;提示走 stderr 保持 stdout 纯 JSON。
|
|
364
|
+
# 2026-09-20 契约修复:导出失败不再提前 return 留空 stdout——契约要求
|
|
365
|
+
# stdout 始终输出单个 JSON 文档(docs/contracts/validate-json-v1.md),
|
|
366
|
+
# 改为 payload 照常输出 + 错误经 stderr 透出 + 退出码维持 2
|
|
367
|
+
report_note = self._export_report_if_requested(payload, report_path)
|
|
368
|
+
report_failure: CommandResult | None = None
|
|
369
|
+
if isinstance(report_note, CommandResult):
|
|
370
|
+
report_failure = report_note
|
|
371
|
+
elif report_note:
|
|
372
|
+
print(report_note, file=sys.stderr)
|
|
373
|
+
# stdout 只输出 JSON 文档(UTF-8,ensure_ascii=False 保留中文原文)
|
|
374
|
+
print(json.dumps(payload, ensure_ascii=False))
|
|
375
|
+
if report_failure is not None:
|
|
376
|
+
# 错误文案由调用方(单发模式 main / REPL 渲染器)按 CommandResult 呈现,
|
|
377
|
+
# 此处不再重复 print,避免 stderr 出现两条相同消息
|
|
378
|
+
return report_failure
|
|
379
|
+
if errors:
|
|
380
|
+
return CommandResult.error("", data={"errors": errors, "duration_ms": duration_ms})
|
|
381
|
+
return CommandResult.ok("", data={"errors": [], "duration_ms": duration_ms})
|
|
382
|
+
|
|
383
|
+
# 处理并显示加载阶段的警告信息
|
|
384
|
+
# loading_errors 来自 LoadingError.to_dict(),友好信息在 title/description/fix_hint
|
|
385
|
+
# 字段(message 可能为空,如 inspect 级错误),故优先展示 title 等字段。
|
|
386
|
+
loading_errors = result.get("loading_errors", [])
|
|
387
|
+
if loading_errors:
|
|
388
|
+
_console.print("\n[yellow]加载警告:[/yellow]")
|
|
389
|
+
for err in loading_errors:
|
|
390
|
+
error_type = err.get("error_type", "Unknown")
|
|
391
|
+
title = err.get("title") or err.get("message") or ""
|
|
392
|
+
_console.print(f" - [{error_type}] {title}")
|
|
393
|
+
if err.get("description"):
|
|
394
|
+
_console.print(f" 说明: {err['description']}")
|
|
395
|
+
if err.get("fix_hint"):
|
|
396
|
+
_console.print(f" 建议: {err['fix_hint']}")
|
|
397
|
+
|
|
398
|
+
interrupted = result.get("interrupted", False)
|
|
399
|
+
|
|
400
|
+
# C6 遇错即停:中断时提示用户剩余校验未执行(区别于正常完成)
|
|
401
|
+
if interrupted:
|
|
402
|
+
_console.print(f"\n⚠ 校验已停止(遇错即停),耗时: {duration_ms} ms")
|
|
403
|
+
_console.print(" 发现首个错误即停止,剩余检查未执行。调整 error_handling 可跑完全部。")
|
|
404
|
+
else:
|
|
405
|
+
_console.print(f"\n校验完成,耗时: {duration_ms} ms")
|
|
406
|
+
|
|
407
|
+
# 输出校验摘要:列出加载的表/行数与每项约束的通过状态,
|
|
408
|
+
# 证明 validate 确实执行了校验(而非空转返回通过)。
|
|
409
|
+
summary = Formatter.format_validation_summary(
|
|
410
|
+
result.get("validation_details"),
|
|
411
|
+
result.get("raw_datasets"),
|
|
412
|
+
)
|
|
413
|
+
_console.print(summary)
|
|
414
|
+
|
|
415
|
+
output = Formatter.format_validation_result(errors)
|
|
416
|
+
_console.print(output)
|
|
417
|
+
|
|
418
|
+
# human 模式同样支持 --report(内容与 JSON 契约同源)
|
|
419
|
+
report_note = self._export_report_if_requested(_build_json_payload(result), report_path)
|
|
420
|
+
if isinstance(report_note, CommandResult):
|
|
421
|
+
return report_note
|
|
422
|
+
if report_note:
|
|
423
|
+
_console.print(report_note)
|
|
424
|
+
|
|
425
|
+
if errors:
|
|
426
|
+
return CommandResult.error("", data={"errors": errors, "duration_ms": duration_ms})
|
|
427
|
+
else:
|
|
428
|
+
return CommandResult.ok("", data={"errors": [], "duration_ms": duration_ms})
|
|
429
|
+
|
|
430
|
+
except Exception as e:
|
|
431
|
+
raise ValidationError(str(e))
|
|
432
|
+
finally:
|
|
433
|
+
inspector_logger.setLevel(_prev_level)
|
|
434
|
+
|
|
435
|
+
@staticmethod
|
|
436
|
+
def _export_report_if_requested(payload: dict, report_path: str | None) -> str | CommandResult:
|
|
437
|
+
"""按需导出报告文件。
|
|
438
|
+
|
|
439
|
+
Args:
|
|
440
|
+
payload: JSON 契约 payload(报告内容与 stdout JSON 同源)
|
|
441
|
+
report_path: --report 给出的输出路径;None 表示未请求
|
|
442
|
+
|
|
443
|
+
Returns:
|
|
444
|
+
成功时返回提示文案(调用方决定走 stdout/stderr);
|
|
445
|
+
未请求时返回空串;失败时返回 exit_code=2 的 CommandResult
|
|
446
|
+
"""
|
|
447
|
+
if not report_path:
|
|
448
|
+
return ""
|
|
449
|
+
from app.shared.services.validation.report_export import export_report
|
|
450
|
+
|
|
451
|
+
try:
|
|
452
|
+
written = export_report(payload, report_path)
|
|
453
|
+
except (ValueError, OSError) as e:
|
|
454
|
+
return CommandResult.error(f"报告导出失败: {e}", exit_code=2)
|
|
455
|
+
return f"报告已写入: {written}"
|