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,233 @@
|
|
|
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
|
+
- 将多个子约束按逻辑策略(all/any/none)聚合校验结果
|
|
21
|
+
- 通过构造函数注入 validate_fn 回调,调用 UnifiedValidationService 为每个子约束执行校验
|
|
22
|
+
- 避免直接实例化 CompositeConstraint 所需的 create_constraint 工厂 + schema_files 依赖
|
|
23
|
+
|
|
24
|
+
架构设计:
|
|
25
|
+
- 继承 BaseValidator,遵循统一校验器接口
|
|
26
|
+
- 通过构造函数注入 validate_fn 回调,消除循环依赖
|
|
27
|
+
- 每个子约束独立执行,异常被捕获视为该子约束失败
|
|
28
|
+
|
|
29
|
+
输入示例:
|
|
30
|
+
CompositeValidator(validate_fn=UnifiedValidationService.validate).validate(
|
|
31
|
+
df=df,
|
|
32
|
+
column="email",
|
|
33
|
+
logic="all",
|
|
34
|
+
sub_constraints=[
|
|
35
|
+
{"type": "NotNull", "enabled": True, "params": {}},
|
|
36
|
+
{"type": "Unique", "enabled": True, "params": {}},
|
|
37
|
+
]
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
输出示例:
|
|
41
|
+
ValidationResult(
|
|
42
|
+
is_valid=False,
|
|
43
|
+
error_count=2,
|
|
44
|
+
total_rows=100,
|
|
45
|
+
error_rows=[...]
|
|
46
|
+
)
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
from __future__ import annotations
|
|
50
|
+
|
|
51
|
+
import logging
|
|
52
|
+
import time
|
|
53
|
+
from collections.abc import Callable
|
|
54
|
+
from typing import Any
|
|
55
|
+
|
|
56
|
+
import pandas as pd
|
|
57
|
+
|
|
58
|
+
from ..types import ValidationResult, ValidationType
|
|
59
|
+
from .base import BaseValidator
|
|
60
|
+
|
|
61
|
+
logger = logging.getLogger(__name__)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class CompositeValidator(BaseValidator):
|
|
65
|
+
"""@classdesc 复合约束校验器
|
|
66
|
+
|
|
67
|
+
接收子约束配置列表,为每个子约束调用注入的 validate_fn 执行校验,
|
|
68
|
+
然后按 logic 策略(all/any/none)聚合最终结果。
|
|
69
|
+
|
|
70
|
+
字段说明:
|
|
71
|
+
- logic: 聚合策略 ("all" | "any" | "none")
|
|
72
|
+
- sub_constraints: 子约束配置列表,每项包含 type/enabled/params
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def __init__(self, validate_fn: Callable | None = None):
|
|
76
|
+
"""@methoddesc 初始化复合约束校验器
|
|
77
|
+
|
|
78
|
+
参数:
|
|
79
|
+
validate_fn: 校验回调函数,签名为 (validation_type, df, column, **kwargs) -> ValidationResult
|
|
80
|
+
在 service.py 注册时注入,避免循环依赖
|
|
81
|
+
"""
|
|
82
|
+
self._validate_fn = validate_fn
|
|
83
|
+
|
|
84
|
+
def validate(self, df: pd.DataFrame, column: str, **kwargs: Any) -> ValidationResult:
|
|
85
|
+
"""@methoddesc 执行复合约束校验
|
|
86
|
+
|
|
87
|
+
遍历所有子约束配置,逐个调用注入的 validate_fn 执行校验,
|
|
88
|
+
收集结果后按 logic 策略聚合。
|
|
89
|
+
|
|
90
|
+
参数:
|
|
91
|
+
df: 待校验的 DataFrame
|
|
92
|
+
column: 待校验的列名
|
|
93
|
+
**kwargs:
|
|
94
|
+
- logic: 聚合策略,默认 "all"
|
|
95
|
+
- sub_constraints: 子约束配置列表
|
|
96
|
+
|
|
97
|
+
返回:
|
|
98
|
+
ValidationResult: 标准化校验结果
|
|
99
|
+
"""
|
|
100
|
+
validate_fn = self._validate_fn
|
|
101
|
+
if validate_fn is None:
|
|
102
|
+
logger.error("CompositeValidator 未注入 validate_fn,无法执行子约束校验")
|
|
103
|
+
return self._format_errors([], len(df), 0)
|
|
104
|
+
|
|
105
|
+
logic = kwargs.get("logic", "all")
|
|
106
|
+
sub_configs = kwargs.get("sub_constraints", [])
|
|
107
|
+
|
|
108
|
+
# 空子约束视为通过
|
|
109
|
+
if not sub_configs:
|
|
110
|
+
return self._format_errors([], len(df), 0)
|
|
111
|
+
|
|
112
|
+
start_time = time.time()
|
|
113
|
+
all_errors: list[dict] = []
|
|
114
|
+
passed_count = 0
|
|
115
|
+
processed_count = 0
|
|
116
|
+
# 回归 D7: 收集未知子约束类型,上报为配置错误(而非静默跳过判通过)
|
|
117
|
+
unknown_types: list[str] = []
|
|
118
|
+
|
|
119
|
+
# 前端传来的类型名 -> ValidationType 常量映射
|
|
120
|
+
# 支持多种命名风格:NotNull / notnull / not_null
|
|
121
|
+
type_map = {
|
|
122
|
+
"notnull": ValidationType.NOT_NULL,
|
|
123
|
+
"not_null": ValidationType.NOT_NULL,
|
|
124
|
+
"unique": ValidationType.UNIQUE,
|
|
125
|
+
"range": ValidationType.RANGE,
|
|
126
|
+
"allowedvalues": ValidationType.ALLOWED_VALUES,
|
|
127
|
+
"allowed_values": ValidationType.ALLOWED_VALUES,
|
|
128
|
+
"charset": ValidationType.CHARSET,
|
|
129
|
+
"regex": ValidationType.REGEX,
|
|
130
|
+
"scripted": ValidationType.SCRIPTED,
|
|
131
|
+
"foreignkey": ValidationType.FOREIGN_KEY,
|
|
132
|
+
"foreign_key": ValidationType.FOREIGN_KEY,
|
|
133
|
+
"conditional": ValidationType.CONDITIONAL,
|
|
134
|
+
"datelogic": ValidationType.DATE_LOGIC,
|
|
135
|
+
"date_logic": ValidationType.DATE_LOGIC,
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
for cfg in sub_configs:
|
|
139
|
+
# 跳过未启用的子约束
|
|
140
|
+
if not cfg.get("enabled", True):
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
sub_type_raw = str(cfg.get("type", "")).lower().replace(" ", "")
|
|
144
|
+
validation_type = type_map.get(sub_type_raw)
|
|
145
|
+
if not validation_type:
|
|
146
|
+
# 回归 D7: 未知类型不再静默跳过,收集后上报配置错误
|
|
147
|
+
logger.warning(f"Composite 中遇到不支持的子约束类型: {sub_type_raw}")
|
|
148
|
+
unknown_types.append(sub_type_raw)
|
|
149
|
+
continue
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
result = validate_fn(
|
|
153
|
+
validation_type=validation_type,
|
|
154
|
+
df=df,
|
|
155
|
+
column=column,
|
|
156
|
+
**cfg.get("params", {}),
|
|
157
|
+
)
|
|
158
|
+
processed_count += 1
|
|
159
|
+
if result.is_valid:
|
|
160
|
+
passed_count += 1
|
|
161
|
+
else:
|
|
162
|
+
all_errors.extend(result.error_rows)
|
|
163
|
+
except Exception as e:
|
|
164
|
+
logger.warning(f"Composite 子约束校验异常 ({validation_type}): {e}")
|
|
165
|
+
# 子约束异常视为失败:计入 processed_count(防止"全部异常"落入下方
|
|
166
|
+
# processed_count==0 的空子约束分支被误判为通过),并生成占位错误行
|
|
167
|
+
# 参与 logic 聚合——all 语义下异常必须使整条约束失败。
|
|
168
|
+
processed_count += 1
|
|
169
|
+
sub_label = getattr(validation_type, "value", validation_type)
|
|
170
|
+
all_errors.append(
|
|
171
|
+
{
|
|
172
|
+
"row_index": 0,
|
|
173
|
+
"cell_value": None,
|
|
174
|
+
"error_message": f"子约束 {sub_label} 执行异常: {e}",
|
|
175
|
+
}
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# 回归 D7: 存在未知子约束类型时上报配置错误,避免用户以为约束生效了实际全被跳过
|
|
179
|
+
if unknown_types:
|
|
180
|
+
config_errors = [
|
|
181
|
+
{
|
|
182
|
+
"row_index": 0,
|
|
183
|
+
"cell_value": None,
|
|
184
|
+
"error_message": (
|
|
185
|
+
f"复合约束包含不支持的子约束类型: {', '.join(unknown_types)};这些子约束未执行校验。"
|
|
186
|
+
),
|
|
187
|
+
}
|
|
188
|
+
]
|
|
189
|
+
return self._format_errors(config_errors, len(df), time.time() - start_time)
|
|
190
|
+
|
|
191
|
+
# 如果没有成功处理的子约束,视为通过(避免空子约束报错)
|
|
192
|
+
if processed_count == 0:
|
|
193
|
+
return self._format_errors([], len(df), time.time() - start_time)
|
|
194
|
+
|
|
195
|
+
# 根据 logic 策略决定最终错误列表
|
|
196
|
+
final_errors: list[dict] = []
|
|
197
|
+
|
|
198
|
+
if logic == "all":
|
|
199
|
+
# 所有子约束都必须通过:返回所有错误的并集
|
|
200
|
+
final_errors = all_errors
|
|
201
|
+
|
|
202
|
+
elif logic == "any":
|
|
203
|
+
# 至少一个子约束通过即算通过
|
|
204
|
+
if passed_count == 0:
|
|
205
|
+
final_errors = [
|
|
206
|
+
{
|
|
207
|
+
"row_index": 0,
|
|
208
|
+
"cell_value": None,
|
|
209
|
+
"error_message": (
|
|
210
|
+
f"复合约束(logic=any)要求至少一个子约束通过,但全部 {processed_count} 个子约束均失败"
|
|
211
|
+
),
|
|
212
|
+
}
|
|
213
|
+
]
|
|
214
|
+
|
|
215
|
+
elif logic == "none":
|
|
216
|
+
# 所有子约束都必须失败才算通过(反向校验)
|
|
217
|
+
if passed_count > 0:
|
|
218
|
+
final_errors = [
|
|
219
|
+
{
|
|
220
|
+
"row_index": 0,
|
|
221
|
+
"cell_value": None,
|
|
222
|
+
"error_message": (
|
|
223
|
+
f"复合约束(logic=none)要求全部子约束失败,但有 {passed_count} 个子约束通过"
|
|
224
|
+
),
|
|
225
|
+
}
|
|
226
|
+
]
|
|
227
|
+
|
|
228
|
+
else:
|
|
229
|
+
# 未知逻辑策略,回退到 all 行为
|
|
230
|
+
logger.warning(f"Composite 遇到未知的 logic 策略: {logic},回退到 all")
|
|
231
|
+
final_errors = all_errors
|
|
232
|
+
|
|
233
|
+
return self._format_errors(final_errors, len(df), time.time() - start_time)
|
|
@@ -0,0 +1,276 @@
|
|
|
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
|
+
- 委托模式: 将校验逻辑委托给 DateLogicConstraint 模型类处理
|
|
25
|
+
- 复用机制: 使用基础校验器的错误格式化方法
|
|
26
|
+
- 临时表映射: 将 DataFrame 映射为临时表 "temp" 供约束模型使用
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
import time
|
|
30
|
+
from datetime import datetime
|
|
31
|
+
from typing import Any
|
|
32
|
+
|
|
33
|
+
import pandas as pd
|
|
34
|
+
|
|
35
|
+
from app.shared.domain import DateLogicConstraint
|
|
36
|
+
|
|
37
|
+
from ..types import ValidationResult
|
|
38
|
+
from .base import BaseValidator
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class DateLogicValidator(BaseValidator):
|
|
42
|
+
"""
|
|
43
|
+
@classdesc 日期逻辑校验器
|
|
44
|
+
|
|
45
|
+
对日期类型数据进行多种逻辑校验和计算验证:
|
|
46
|
+
- 比较模式:验证日期与参考日期的大小关系(大于、小于、等于等)
|
|
47
|
+
- 计算模式:验证日期计算结果是否符合预期(年龄、天数差等)
|
|
48
|
+
|
|
49
|
+
设计原则:
|
|
50
|
+
- 委托设计:核心校验逻辑委托给 DateLogicConstraint 模型
|
|
51
|
+
- 统一接口:与其他校验器保持一致的 validate 方法签名
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def validate(self, df: pd.DataFrame, column: str, **kwargs: Any) -> ValidationResult:
|
|
55
|
+
"""
|
|
56
|
+
@methoddesc 执行日期逻辑校验
|
|
57
|
+
|
|
58
|
+
对 DataFrame 中指定列的日期数据进行逻辑校验。
|
|
59
|
+
支持两种模式:
|
|
60
|
+
- compare(比较模式):验证日期与参考日期的大小关系
|
|
61
|
+
- calculation(计算模式):验证日期计算结果(如年龄、天数差)
|
|
62
|
+
|
|
63
|
+
参数:
|
|
64
|
+
df: 待校验的 DataFrame 对象
|
|
65
|
+
column: 待校验的日期列名
|
|
66
|
+
**kwargs: 校验参数,包含:
|
|
67
|
+
- logic_mode: 逻辑模式,"compare" 或 "calculation"(默认 "compare")
|
|
68
|
+
- compare_op: 比较操作符,"gt"/"lt"/"eq"/"gte"/"lte"/"range"(默认 "gt")
|
|
69
|
+
- reference_date: 参考日期字符串,比较模式下的区间起点(可选)
|
|
70
|
+
- reference_column: 参考日期列名,比较模式下的区间起点(可选)
|
|
71
|
+
- reference_date_end: 区间终点固定日期,仅 range 模式使用(可选)
|
|
72
|
+
- reference_column_end: 区间终点列名,仅 range 模式使用(可选)
|
|
73
|
+
- calculation_type: 计算类型,"age" 或 "days_diff"(默认 "age")
|
|
74
|
+
- target_value: 目标计算值(可选)
|
|
75
|
+
- target_column: 目标计算列(可选)
|
|
76
|
+
|
|
77
|
+
返回:
|
|
78
|
+
ValidationResult: 标准化校验结果
|
|
79
|
+
"""
|
|
80
|
+
start_time = time.time()
|
|
81
|
+
|
|
82
|
+
# 提取校验参数
|
|
83
|
+
logic_mode = kwargs.get("logic_mode", "compare")
|
|
84
|
+
calculation_type = kwargs.get("calculation_type", "age")
|
|
85
|
+
# compare_op 默认值随计算类型调整:
|
|
86
|
+
# - age 模式默认 gte(满 N 岁即通过;回归 D2: 原默认 gt 会误判刚满 N 岁的边界)
|
|
87
|
+
# - days_diff 模式默认 eq(差值等于目标值通过;对账周期场景)
|
|
88
|
+
# - compare 模式默认 gt(与历史行为一致)
|
|
89
|
+
compare_op = kwargs.get("compare_op")
|
|
90
|
+
if compare_op is None:
|
|
91
|
+
if calculation_type == "days_diff":
|
|
92
|
+
compare_op = "eq"
|
|
93
|
+
elif calculation_type == "age":
|
|
94
|
+
compare_op = "gte"
|
|
95
|
+
else:
|
|
96
|
+
compare_op = "gt"
|
|
97
|
+
reference_date = kwargs.get("reference_date")
|
|
98
|
+
reference_column = kwargs.get("reference_column")
|
|
99
|
+
reference_date_end = kwargs.get("reference_date_end")
|
|
100
|
+
reference_column_end = kwargs.get("reference_column_end")
|
|
101
|
+
target_value = kwargs.get("target_value")
|
|
102
|
+
target_column = kwargs.get("target_column")
|
|
103
|
+
|
|
104
|
+
# 检查校验列是否存在
|
|
105
|
+
if column not in df.columns:
|
|
106
|
+
return ValidationResult(
|
|
107
|
+
is_valid=False,
|
|
108
|
+
error_count=1,
|
|
109
|
+
total_rows=len(df),
|
|
110
|
+
error_rows=[{"row_index": 0, "cell_value": None, "error_message": f"列 '{column}' 不存在"}],
|
|
111
|
+
validation_time=f"{time.time() - start_time:.3f}s",
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# 检查参考列是否存在
|
|
115
|
+
if reference_column and reference_column not in df.columns:
|
|
116
|
+
return ValidationResult(
|
|
117
|
+
is_valid=False,
|
|
118
|
+
error_count=1,
|
|
119
|
+
total_rows=len(df),
|
|
120
|
+
error_rows=[
|
|
121
|
+
{"row_index": 0, "cell_value": None, "error_message": f"参考列 '{reference_column}' 不存在"}
|
|
122
|
+
],
|
|
123
|
+
validation_time=f"{time.time() - start_time:.3f}s",
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# range 模式下检查终点参考列是否存在
|
|
127
|
+
if compare_op == "range" and reference_column_end and reference_column_end not in df.columns:
|
|
128
|
+
return ValidationResult(
|
|
129
|
+
is_valid=False,
|
|
130
|
+
error_count=1,
|
|
131
|
+
total_rows=len(df),
|
|
132
|
+
error_rows=[
|
|
133
|
+
{"row_index": 0, "cell_value": None, "error_message": f"终点参考列 '{reference_column_end}' 不存在"}
|
|
134
|
+
],
|
|
135
|
+
validation_time=f"{time.time() - start_time:.3f}s",
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# 校验逻辑模式是否合法
|
|
139
|
+
if logic_mode not in ("compare", "calculation"):
|
|
140
|
+
return ValidationResult(
|
|
141
|
+
is_valid=False,
|
|
142
|
+
error_count=1,
|
|
143
|
+
total_rows=len(df),
|
|
144
|
+
error_rows=[
|
|
145
|
+
{"row_index": 0, "cell_value": None, "error_message": f"不支持的日期逻辑模式: {logic_mode}"}
|
|
146
|
+
],
|
|
147
|
+
validation_time=f"{time.time() - start_time:.3f}s",
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# 校验计算模式下的目标值是否为有效数值
|
|
151
|
+
if logic_mode == "calculation" and target_value is not None:
|
|
152
|
+
try:
|
|
153
|
+
if calculation_type == "age":
|
|
154
|
+
float(target_value)
|
|
155
|
+
elif calculation_type == "days_diff":
|
|
156
|
+
int(target_value)
|
|
157
|
+
except (ValueError, TypeError):
|
|
158
|
+
type_name = "年龄" if calculation_type == "age" else "天数"
|
|
159
|
+
return ValidationResult(
|
|
160
|
+
is_valid=False,
|
|
161
|
+
error_count=1,
|
|
162
|
+
total_rows=len(df),
|
|
163
|
+
error_rows=[
|
|
164
|
+
{
|
|
165
|
+
"row_index": 0,
|
|
166
|
+
"cell_value": None,
|
|
167
|
+
"error_message": f"无效的目标{type_name}值: {target_value},应为数值",
|
|
168
|
+
}
|
|
169
|
+
],
|
|
170
|
+
validation_time=f"{time.time() - start_time:.3f}s",
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# 校验参考日期格式是否合法
|
|
174
|
+
if reference_date and self._parse_date(str(reference_date)) is None:
|
|
175
|
+
return ValidationResult(
|
|
176
|
+
is_valid=False,
|
|
177
|
+
error_count=1,
|
|
178
|
+
total_rows=len(df),
|
|
179
|
+
error_rows=[
|
|
180
|
+
{"row_index": 0, "cell_value": None, "error_message": f"无效的参考日期格式: {reference_date}"}
|
|
181
|
+
],
|
|
182
|
+
validation_time=f"{time.time() - start_time:.3f}s",
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# range 模式下校验终点参考日期格式
|
|
186
|
+
if compare_op == "range" and reference_date_end and self._parse_date(str(reference_date_end)) is None:
|
|
187
|
+
return ValidationResult(
|
|
188
|
+
is_valid=False,
|
|
189
|
+
error_count=1,
|
|
190
|
+
total_rows=len(df),
|
|
191
|
+
error_rows=[
|
|
192
|
+
{
|
|
193
|
+
"row_index": 0,
|
|
194
|
+
"cell_value": None,
|
|
195
|
+
"error_message": f"无效的终点参考日期格式: {reference_date_end}",
|
|
196
|
+
}
|
|
197
|
+
],
|
|
198
|
+
validation_time=f"{time.time() - start_time:.3f}s",
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
# 预检查:向量化找出无法解析的日期。
|
|
202
|
+
# 严格保持 DATE_FORMATS 顺序(%d/%m/%Y 在 %m/%d/%Y 之前——歧义日期优先按日/月解析)。
|
|
203
|
+
# 对每种格式用 pd.to_datetime(errors="coerce") 尝试,取第一个非 NaT 结果。
|
|
204
|
+
series = df[column]
|
|
205
|
+
non_null_mask = series.notna() & (series != None) # noqa: E711
|
|
206
|
+
str_series = series[non_null_mask].astype(str).str.strip()
|
|
207
|
+
|
|
208
|
+
# 逐格式解析:已解析的不再尝试后续格式(保持 DATE_FORMATS 优先级)
|
|
209
|
+
parsed_mask = pd.Series(False, index=str_series.index)
|
|
210
|
+
for fmt in self.DATE_FORMATS:
|
|
211
|
+
remaining = ~parsed_mask
|
|
212
|
+
if not remaining.any():
|
|
213
|
+
break
|
|
214
|
+
try:
|
|
215
|
+
candidate = pd.to_datetime(str_series[remaining], format=fmt, errors="coerce")
|
|
216
|
+
parsed_mask = parsed_mask | candidate.notna()
|
|
217
|
+
except (ValueError, TypeError):
|
|
218
|
+
continue
|
|
219
|
+
|
|
220
|
+
# 未解析的即为无法解析的日期
|
|
221
|
+
unparseable_idx = str_series.index[~parsed_mask]
|
|
222
|
+
unparseable_errors = [
|
|
223
|
+
{"row_index": idx, "value": str(series[idx]), "message": f"无法解析日期: {series[idx]}"}
|
|
224
|
+
for idx in unparseable_idx
|
|
225
|
+
]
|
|
226
|
+
|
|
227
|
+
# 如果存在无法解析的日期,直接返回错误结果
|
|
228
|
+
if unparseable_errors:
|
|
229
|
+
return self._format_errors(unparseable_errors, len(df), time.time() - start_time)
|
|
230
|
+
|
|
231
|
+
# 创建日期逻辑约束对象并委托校验
|
|
232
|
+
constraint = DateLogicConstraint(
|
|
233
|
+
table="temp",
|
|
234
|
+
column=column,
|
|
235
|
+
logic_mode=logic_mode,
|
|
236
|
+
compare_op=compare_op,
|
|
237
|
+
reference_date=reference_date,
|
|
238
|
+
reference_column=reference_column,
|
|
239
|
+
reference_date_end=reference_date_end,
|
|
240
|
+
reference_column_end=reference_column_end,
|
|
241
|
+
calculation_type=calculation_type,
|
|
242
|
+
target_value=target_value,
|
|
243
|
+
target_column=target_column,
|
|
244
|
+
)
|
|
245
|
+
return self._delegate_validation(df, column, constraint)
|
|
246
|
+
|
|
247
|
+
DATE_FORMATS = [
|
|
248
|
+
"%Y-%m-%d",
|
|
249
|
+
"%Y/%m/%d",
|
|
250
|
+
"%Y年%m月%d日",
|
|
251
|
+
"%d/%m/%Y",
|
|
252
|
+
"%m/%d/%Y",
|
|
253
|
+
"%Y-%m-%d %H:%M:%S",
|
|
254
|
+
"%Y/%m/%d %H:%M:%S",
|
|
255
|
+
]
|
|
256
|
+
|
|
257
|
+
@classmethod
|
|
258
|
+
def _parse_date(cls, date_str: str) -> datetime | None:
|
|
259
|
+
"""
|
|
260
|
+
@methoddesc 解析日期字符串
|
|
261
|
+
|
|
262
|
+
尝试使用预定义的多种日期格式解析字符串。
|
|
263
|
+
|
|
264
|
+
参数:
|
|
265
|
+
date_str: 日期字符串
|
|
266
|
+
|
|
267
|
+
返回:
|
|
268
|
+
解析成功的 datetime 对象,失败则返回 None
|
|
269
|
+
"""
|
|
270
|
+
date_str = str(date_str).strip()
|
|
271
|
+
for fmt in cls.DATE_FORMATS:
|
|
272
|
+
try:
|
|
273
|
+
return datetime.strptime(date_str, fmt)
|
|
274
|
+
except ValueError:
|
|
275
|
+
continue
|
|
276
|
+
return None
|
app/start_server.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
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 Precis 后端服务启动脚本
|
|
18
|
+
|
|
19
|
+
功能概述:
|
|
20
|
+
- 后端启动的统一入口,被 Electron 桌面应用与 Web 开发模式(npm run dev)共用
|
|
21
|
+
- 端口由 OS 原子分配(默认 port=0),通过 ``.backend-port`` 文件向外部发布实际端口
|
|
22
|
+
- 支持自定义端口(固定端口逃生舱)与热重载(开发模式)
|
|
23
|
+
|
|
24
|
+
使用方式:
|
|
25
|
+
python app/start_server.py # OS 动态分配端口(Electron 用)
|
|
26
|
+
python app/start_server.py --reload # 开发模式热重载(npm run dev / backend:dev 用)
|
|
27
|
+
python app/start_server.py --port 18000 # 固定端口(向后兼容)
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
import os
|
|
31
|
+
|
|
32
|
+
# 禁止生成 Python 字节码文件(__pycache__ 和 .pyc)
|
|
33
|
+
os.environ.setdefault("PYTHONDONTWRITEBYTECODE", "1")
|
|
34
|
+
|
|
35
|
+
import argparse
|
|
36
|
+
import logging
|
|
37
|
+
import sys
|
|
38
|
+
|
|
39
|
+
# 添加项目根目录(backend/)到 Python 路径
|
|
40
|
+
# 由于脚本位于 backend/app/ 下,需要上溯两级到 backend/
|
|
41
|
+
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
42
|
+
if project_root not in sys.path:
|
|
43
|
+
sys.path.insert(0, project_root)
|
|
44
|
+
|
|
45
|
+
import uvicorn
|
|
46
|
+
|
|
47
|
+
from app.shared.core.config.server import (
|
|
48
|
+
DEFAULT_BACKEND_HOST,
|
|
49
|
+
acquire_port,
|
|
50
|
+
clear_port_file,
|
|
51
|
+
register_port_file_cleanup,
|
|
52
|
+
resolve_port,
|
|
53
|
+
write_port_file,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
logger = logging.getLogger(__name__)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def main() -> None:
|
|
60
|
+
"""
|
|
61
|
+
主函数 - 解析命令行参数并启动 Uvicorn 服务器
|
|
62
|
+
|
|
63
|
+
端口策略:
|
|
64
|
+
- 默认 port=0 由 OS 动态分配(永不冲突),实际端口写入 .backend-port
|
|
65
|
+
- 显式 --port 或 VITE_BACKEND_PORT 环境变量可固定端口(向后兼容)
|
|
66
|
+
"""
|
|
67
|
+
# 创建命令行参数解析器
|
|
68
|
+
parser = argparse.ArgumentParser(description="Precis Backend Server", prog="start_server")
|
|
69
|
+
|
|
70
|
+
# 添加端口参数
|
|
71
|
+
parser.add_argument(
|
|
72
|
+
"--port",
|
|
73
|
+
type=int,
|
|
74
|
+
default=None,
|
|
75
|
+
help="服务器监听端口 (默认: 0=OS 动态分配;可通过 VITE_BACKEND_PORT 固定端口)",
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# 添加主机参数
|
|
79
|
+
parser.add_argument(
|
|
80
|
+
"--host", type=str, default=DEFAULT_BACKEND_HOST, help=f"服务器监听地址 (默认: {DEFAULT_BACKEND_HOST})"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# 添加热重载参数(开发模式用,Electron 生产模式不启用)
|
|
84
|
+
parser.add_argument(
|
|
85
|
+
"--reload",
|
|
86
|
+
action="store_true",
|
|
87
|
+
help="启用文件变更热重载(仅开发模式使用)",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# 解析参数
|
|
91
|
+
args = parser.parse_args()
|
|
92
|
+
|
|
93
|
+
# 解析端口:CLI 参数 > 环境变量 > 默认 0(OS 动态分配)
|
|
94
|
+
preferred_port = resolve_port(args.port)
|
|
95
|
+
# 获取实际可监听端口(OS 原子分配,无竞态)
|
|
96
|
+
actual_port = acquire_port(preferred_port)
|
|
97
|
+
|
|
98
|
+
# 将实际端口写入端口文件,供 Vite 代理 / Electron 主进程发现
|
|
99
|
+
write_port_file(actual_port)
|
|
100
|
+
|
|
101
|
+
# 通过环境变量把实际端口传递给 uvicorn --reload 派生的 app 子进程。
|
|
102
|
+
# app.api.main 在每次(重)启动时会据此重写端口文件 —— 否则任何其他
|
|
103
|
+
# Precis 进程退出清理端口文件后,本进程虽存活但发现方读到缺失,代理 502。
|
|
104
|
+
os.environ["PRECIS_BACKEND_PORT_ACTUAL"] = str(actual_port)
|
|
105
|
+
|
|
106
|
+
# 注册端口文件清理兜底(atexit + signal),Windows 下 uvicorn 的信号处理可能绕过 finally
|
|
107
|
+
register_port_file_cleanup()
|
|
108
|
+
|
|
109
|
+
# 打印启动信息
|
|
110
|
+
logger.info("[INIT] Precis Backend 启动中...")
|
|
111
|
+
logger.info("[INIT] 监听地址: %s:%s", args.host, actual_port)
|
|
112
|
+
logger.info("[INIT] Python 版本: %s", sys.version)
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
# 启动 Uvicorn 服务器
|
|
116
|
+
# app.api.main:app 指向 FastAPI 应用实例
|
|
117
|
+
uvicorn.run(
|
|
118
|
+
"app.api.main:app",
|
|
119
|
+
host=args.host,
|
|
120
|
+
port=actual_port,
|
|
121
|
+
log_level="info",
|
|
122
|
+
# 热重载:开发模式 --reload 启用,Electron 生产模式关闭
|
|
123
|
+
reload=args.reload,
|
|
124
|
+
# 启用访问日志
|
|
125
|
+
access_log=True,
|
|
126
|
+
)
|
|
127
|
+
finally:
|
|
128
|
+
# 无论正常退出还是异常中断,都清理端口文件,避免残留误导外部发现方
|
|
129
|
+
clear_port_file()
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
if __name__ == "__main__":
|
|
133
|
+
main()
|