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,274 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2026 Precis Team
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
"""@fileoverview 钉钉自建应用消息推送报告器
|
|
17
|
+
|
|
18
|
+
功能概述:
|
|
19
|
+
- 通过钉钉企业内部应用发送数据验证错误报告
|
|
20
|
+
- 支持 Markdown 格式消息,自动获取和管理 access_token
|
|
21
|
+
- 实现 access_token 的缓存和自动刷新机制(提前 5 分钟刷新)
|
|
22
|
+
|
|
23
|
+
架构设计:
|
|
24
|
+
- 继承 Reporter 抽象基类,实现 configure() 和 report() 方法
|
|
25
|
+
- Token 管理: _get_access_token() 实现缓存 + 自动刷新策略
|
|
26
|
+
- 消息截断: 钉钉消息限制约 4KB,超长内容自动截断
|
|
27
|
+
- 统一请求: _send_request() 封装 HTTP POST 请求和错误处理
|
|
28
|
+
|
|
29
|
+
输入示例:
|
|
30
|
+
配置参数:
|
|
31
|
+
{
|
|
32
|
+
"app_key": "dingxxxxxxxxxxxx",
|
|
33
|
+
"app_secret": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
|
34
|
+
"agent_id": 123456789,
|
|
35
|
+
"userid_list": "user1,user2"
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
错误数据:
|
|
39
|
+
[{"error_type": "ValidationError", "row_index": 2, "column": "age", "value": "abc"}]
|
|
40
|
+
|
|
41
|
+
输出示例:
|
|
42
|
+
钉钉消息 (Markdown 格式):
|
|
43
|
+
### 【数据验证警告】发现 1 个错误!
|
|
44
|
+
**错误详情:**
|
|
45
|
+
```json
|
|
46
|
+
[{"error_type": "ValidationError", ...}]
|
|
47
|
+
```
|
|
48
|
+
> **报告时间:** 2024-01-15 14:30:52
|
|
49
|
+
|
|
50
|
+
控制台输出:
|
|
51
|
+
[DingTalkAppReporter] ✓ 配置成功。
|
|
52
|
+
[DingTalkAppReporter] ✓ 成功获取 Access Token。
|
|
53
|
+
[DingTalkAppReporter] ✓ 成功发送消息。
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
import json
|
|
57
|
+
import logging
|
|
58
|
+
import time
|
|
59
|
+
import urllib.parse
|
|
60
|
+
import urllib.request
|
|
61
|
+
from datetime import datetime
|
|
62
|
+
from typing import Any
|
|
63
|
+
|
|
64
|
+
from .base import TEMPLATE_HEADROOM_BYTES, Reporter, sanitize_url_for_log, truncate_to_byte_limit
|
|
65
|
+
|
|
66
|
+
logger = logging.getLogger(__name__)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class DingTalkAppReporter(Reporter):
|
|
70
|
+
"""
|
|
71
|
+
@classdesc 钉钉自建应用消息推送报告器
|
|
72
|
+
|
|
73
|
+
通过钉钉企业内部应用发送工作通知,将数据验证错误报告推送给指定用户。
|
|
74
|
+
|
|
75
|
+
Attributes:
|
|
76
|
+
config: 存储配置参数的字典,包含 app_key, app_secret, agent_id, userid_list
|
|
77
|
+
is_configured: 配置是否有效的标志
|
|
78
|
+
access_token: 钉钉 API 访问令牌
|
|
79
|
+
token_expires_at: 令牌过期时间戳
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
def __init__(self) -> None:
|
|
83
|
+
"""
|
|
84
|
+
@methoddesc 初始化钉钉报告器
|
|
85
|
+
|
|
86
|
+
设置默认属性,包括配置字典、配置状态标志、
|
|
87
|
+
访问令牌及其过期时间戳。
|
|
88
|
+
"""
|
|
89
|
+
super().__init__("DingTalkAppReporter")
|
|
90
|
+
self.config: dict[str, Any] = {} # 存储配置参数
|
|
91
|
+
self.is_configured = False # 配置是否有效
|
|
92
|
+
self.access_token = None # 钉钉 API 访问令牌
|
|
93
|
+
self.token_expires_at = 0 # 令牌过期时间戳
|
|
94
|
+
|
|
95
|
+
def configure(self, **kwargs: Any) -> bool:
|
|
96
|
+
"""
|
|
97
|
+
@methoddesc 配置钉钉报告器参数
|
|
98
|
+
|
|
99
|
+
验证所有必需参数是否提供,并保存配置信息。
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
app_key: 钉钉应用 Key(企业标识下的唯一 ID)
|
|
103
|
+
app_secret: 钉钉应用密钥(用于获取 access_token)
|
|
104
|
+
agent_id: 钉钉应用 AgentID(用于指定发送目标应用)
|
|
105
|
+
userid_list: 接收者用户 ID 列表(逗号分隔的字符串)
|
|
106
|
+
**kwargs: 额外参数(保留用于扩展)
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
bool: 配置是否成功
|
|
110
|
+
"""
|
|
111
|
+
# 参数验证:确保所有必需参数都提供
|
|
112
|
+
app_key = kwargs.get("app_key")
|
|
113
|
+
app_secret = kwargs.get("app_secret")
|
|
114
|
+
agent_id = kwargs.get("agent_id")
|
|
115
|
+
userid_list = kwargs.get("userid_list")
|
|
116
|
+
if not all([app_key, app_secret, agent_id, userid_list]):
|
|
117
|
+
self.is_configured = False
|
|
118
|
+
return False
|
|
119
|
+
|
|
120
|
+
# 显式处理 None,避免 mypy 将 Any | None 传给 int() 时报错
|
|
121
|
+
if agent_id is None:
|
|
122
|
+
self.is_configured = False
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
# 保存配置参数
|
|
126
|
+
self.config = {
|
|
127
|
+
"app_key": str(app_key),
|
|
128
|
+
"app_secret": str(app_secret),
|
|
129
|
+
"agent_id": int(agent_id),
|
|
130
|
+
"userid_list": str(userid_list),
|
|
131
|
+
}
|
|
132
|
+
self.is_configured = True
|
|
133
|
+
print(f"[{self.name}] ✓ 配置成功。")
|
|
134
|
+
return True
|
|
135
|
+
|
|
136
|
+
def _get_access_token(self) -> str | None:
|
|
137
|
+
"""
|
|
138
|
+
@methoddesc 获取或刷新钉钉 API 访问令牌(Access Token)
|
|
139
|
+
|
|
140
|
+
访问令牌用于调用钉钉 API,有效期默认为 7200 秒(2 小时)。
|
|
141
|
+
实现自动刷新机制:在过期前 5 分钟自动刷新,避免令牌失效。
|
|
142
|
+
|
|
143
|
+
Token 刷新逻辑:
|
|
144
|
+
- 检查当前时间是否早于过期时间
|
|
145
|
+
- 如果是,直接返回缓存的令牌
|
|
146
|
+
- 否则调用钉钉 API 获取新令牌并更新缓存
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
str: 有效的访问令牌,如果获取失败返回 None
|
|
150
|
+
|
|
151
|
+
API 调用:
|
|
152
|
+
- URL: https://oapi.dingtalk.com/gettoken
|
|
153
|
+
- 参数: appkey, appsecret
|
|
154
|
+
- 响应: access_token, expire (有效期秒数)
|
|
155
|
+
"""
|
|
156
|
+
# 检查令牌是否仍然有效(提前 5 分钟刷新)
|
|
157
|
+
if time.time() < self.token_expires_at:
|
|
158
|
+
return self.access_token
|
|
159
|
+
|
|
160
|
+
# 调用钉钉 API 获取令牌
|
|
161
|
+
url = "https://oapi.dingtalk.com/gettoken"
|
|
162
|
+
params = {"appkey": self.config["app_key"], "appsecret": self.config["app_secret"]}
|
|
163
|
+
try:
|
|
164
|
+
# 构建 URL 参数
|
|
165
|
+
full_url = f"{url}?{urllib.parse.urlencode(params)}"
|
|
166
|
+
|
|
167
|
+
# 发送请求
|
|
168
|
+
with urllib.request.urlopen(full_url, timeout=10) as response:
|
|
169
|
+
data = json.loads(response.read().decode())
|
|
170
|
+
if data.get("errcode") == 0:
|
|
171
|
+
# 成功获取令牌
|
|
172
|
+
self.access_token = data["access_token"]
|
|
173
|
+
# 设置过期时间:当前时间 + 有效期 - 缓冲时间(5分钟)
|
|
174
|
+
self.token_expires_at = time.time() + data["expires_in"] - 300
|
|
175
|
+
print(f"[{self.name}] ✓ 成功获取 Access Token。")
|
|
176
|
+
return self.access_token
|
|
177
|
+
else:
|
|
178
|
+
print(f"[{self.name}] !! 错误: 获取 Access Token 失败: {data.get('errmsg')}")
|
|
179
|
+
return None
|
|
180
|
+
except Exception as e:
|
|
181
|
+
# gettoken 的 full_url query 含 appsecret,日志只打无 query 的基础 URL
|
|
182
|
+
logger.warning(
|
|
183
|
+
"[%s] !! 错误: 请求 Access Token 失败 (%s): %s",
|
|
184
|
+
self.name,
|
|
185
|
+
sanitize_url_for_log(url),
|
|
186
|
+
e,
|
|
187
|
+
)
|
|
188
|
+
return None
|
|
189
|
+
|
|
190
|
+
def report(self, errors: list[dict]) -> None:
|
|
191
|
+
"""
|
|
192
|
+
@methoddesc 发送错误报告的主入口方法
|
|
193
|
+
|
|
194
|
+
流程:
|
|
195
|
+
1. 检查配置是否有效
|
|
196
|
+
2. 获取有效的访问令牌
|
|
197
|
+
3. 格式化错误信息为 Markdown
|
|
198
|
+
4. 调用钉钉发送工作通知 API
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
errors: 错误信息列表,每个元素是一个包含错误详情的字典
|
|
202
|
+
"""
|
|
203
|
+
# 前置检查:确保已正确配置
|
|
204
|
+
if not self.is_configured:
|
|
205
|
+
return
|
|
206
|
+
|
|
207
|
+
# 步骤1: 获取有效的访问令牌
|
|
208
|
+
token = self._get_access_token()
|
|
209
|
+
if not token:
|
|
210
|
+
return
|
|
211
|
+
|
|
212
|
+
# 步骤2: 统计错误数量并格式化详情
|
|
213
|
+
error_count = len(errors)
|
|
214
|
+
error_details = json.dumps(errors, indent=2, ensure_ascii=False)
|
|
215
|
+
|
|
216
|
+
# 安全截断:钉钉消息内容限制约 4KB
|
|
217
|
+
# 必须按 UTF-8 字节截断:平台限额按字节计,中文按字符截断后实际字节数
|
|
218
|
+
# 仍可能超限被拒收(见 truncate_to_byte_limit)
|
|
219
|
+
error_details = truncate_to_byte_limit(error_details, 4000 - TEMPLATE_HEADROOM_BYTES)
|
|
220
|
+
|
|
221
|
+
# 步骤3: 构建 Markdown 格式消息内容
|
|
222
|
+
title = f"【数据验证警告】发现 {error_count} 个错误!"
|
|
223
|
+
text = f"""### {title}
|
|
224
|
+
**错误详情:**
|
|
225
|
+
```json
|
|
226
|
+
{error_details}
|
|
227
|
+
```
|
|
228
|
+
> **报告时间:** {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
|
|
229
|
+
"""
|
|
230
|
+
|
|
231
|
+
# 步骤4: 构建发送工作通知的请求负载
|
|
232
|
+
payload = {
|
|
233
|
+
"agent_id": self.config["agent_id"], # 应用 AgentID
|
|
234
|
+
"userid_list": self.config["userid_list"], # 接收者用户列表
|
|
235
|
+
"msg": {
|
|
236
|
+
"msgtype": "markdown", # 消息类型:Markdown
|
|
237
|
+
"markdown": {
|
|
238
|
+
"title": title, # 消息标题(通知标题)
|
|
239
|
+
"text": text, # 消息正文内容
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
# 步骤5: 发送消息
|
|
245
|
+
url = f"https://oapi.dingtalk.com/topapi/message/corpconversation/asyncsend_v2?access_token={token}"
|
|
246
|
+
self._send_request(url, payload)
|
|
247
|
+
|
|
248
|
+
def _send_request(self, url: str, payload: dict[str, Any]) -> None:
|
|
249
|
+
"""
|
|
250
|
+
@methoddesc 发送 HTTP POST 请求到钉钉 API
|
|
251
|
+
|
|
252
|
+
处理所有钉钉 API 的 HTTP 通信,包括错误处理和日志记录。
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
url: 请求目标 URL
|
|
256
|
+
payload: 请求体数据字典
|
|
257
|
+
"""
|
|
258
|
+
try:
|
|
259
|
+
# 序列化请求负载为 JSON 字节
|
|
260
|
+
data = json.dumps(payload).encode("utf-8")
|
|
261
|
+
headers = {"Content-Type": "application/json"}
|
|
262
|
+
|
|
263
|
+
# 构建 HTTP 请求
|
|
264
|
+
req = urllib.request.Request(url, data=data, headers=headers)
|
|
265
|
+
|
|
266
|
+
# 发送请求并获取响应
|
|
267
|
+
with urllib.request.urlopen(req, timeout=10) as response:
|
|
268
|
+
result = json.loads(response.read().decode("utf-8"))
|
|
269
|
+
if result.get("errcode") == 0:
|
|
270
|
+
print(f"[{self.name}] ✓ 成功发送消息。")
|
|
271
|
+
else:
|
|
272
|
+
print(f"[{self.name}] !! 错误: 发送消息失败: {result.get('errmsg')}")
|
|
273
|
+
except Exception as e:
|
|
274
|
+
logger.warning("[%s] !! 错误: 发送请求失败: %s", self.name, e)
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2026 Precis Team
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
"""@fileoverview 邮件报告器模块
|
|
17
|
+
|
|
18
|
+
功能概述:
|
|
19
|
+
- 通过 SMTP 协议发送数据验证错误报告
|
|
20
|
+
- 支持 TLS/SSL 加密传输和 HTML 格式邮件
|
|
21
|
+
- 配置阶段即验证 SMTP 连接和认证,提前发现问题
|
|
22
|
+
|
|
23
|
+
架构设计:
|
|
24
|
+
- 继承 Reporter 抽象基类,实现 configure() 和 report() 方法
|
|
25
|
+
- 双模式连接: 根据端口自动选择 SMTP + STARTTLS (587) 或 SMTP_SSL (465)
|
|
26
|
+
- 配置即验证: configure() 阶段建立真实连接测试,确保参数正确
|
|
27
|
+
- 资源安全: 使用 with 语句和 try-finally 确保 SMTP 连接正确关闭
|
|
28
|
+
|
|
29
|
+
输入示例:
|
|
30
|
+
配置参数:
|
|
31
|
+
{
|
|
32
|
+
"smtp_server": "smtp.gmail.com",
|
|
33
|
+
"smtp_port": 587,
|
|
34
|
+
"sender_email": "precis@example.com",
|
|
35
|
+
"sender_password": "app_password",
|
|
36
|
+
"receiver_email": "admin@example.com"
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
错误数据:
|
|
40
|
+
[{"error_type": "ForeignKeyViolation", "table": "orders", "value": 99}]
|
|
41
|
+
|
|
42
|
+
输出示例:
|
|
43
|
+
HTML 邮件内容:
|
|
44
|
+
<html><body>
|
|
45
|
+
<h2>检测到以下数据配置错误:</h2>
|
|
46
|
+
<pre style="...">[...错误JSON...]</pre>
|
|
47
|
+
<p>请尽快检查并修复。</p>
|
|
48
|
+
</body></html>
|
|
49
|
+
|
|
50
|
+
控制台输出:
|
|
51
|
+
[EmailReporter] ✓ 邮件服务配置验证成功,发件人: precis@example.com
|
|
52
|
+
[EmailReporter] ✓ 成功发送错误报告邮件至 admin@example.com。
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
import html
|
|
56
|
+
import json
|
|
57
|
+
import smtplib
|
|
58
|
+
from email.header import Header
|
|
59
|
+
from email.mime.text import MIMEText
|
|
60
|
+
from email.utils import formataddr
|
|
61
|
+
from typing import Any, cast
|
|
62
|
+
|
|
63
|
+
from .base import Reporter
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class EmailReporter(Reporter):
|
|
67
|
+
"""
|
|
68
|
+
@classdesc 邮件报告器 - 通过 SMTP 协议发送错误报告
|
|
69
|
+
|
|
70
|
+
该报告器将数据验证错误格式化为 HTML 邮件并发送给指定的收件人。
|
|
71
|
+
使用原生 smtplib 实现,避免第三方依赖,提供更好的稳定性和控制力。
|
|
72
|
+
|
|
73
|
+
属性说明:
|
|
74
|
+
config: 存储邮件服务器配置信息的字典
|
|
75
|
+
包含 smtp_server, smtp_port, sender_email, sender_password, receiver_email
|
|
76
|
+
is_configured: 标记配置是否通过验证
|
|
77
|
+
|
|
78
|
+
功能特点:
|
|
79
|
+
- 配置阶段验证 SMTP 连接和认证
|
|
80
|
+
- 支持 TLS(端口 587)和 SSL(端口 465)两种加密方式
|
|
81
|
+
- HTML 格式邮件,带有错误详情表格
|
|
82
|
+
- 中文编码支持,使用 Header 防止乱码
|
|
83
|
+
|
|
84
|
+
使用示例:
|
|
85
|
+
reporter = EmailReporter()
|
|
86
|
+
if reporter.configure(
|
|
87
|
+
smtp_server='smtp.example.com',
|
|
88
|
+
smtp_port=587,
|
|
89
|
+
sender_email='precis@example.com',
|
|
90
|
+
sender_password='secret',
|
|
91
|
+
receiver_email='admin@example.com'
|
|
92
|
+
):
|
|
93
|
+
reporter.report(errors)
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
def __init__(self) -> None:
|
|
97
|
+
"""
|
|
98
|
+
初始化邮件报告器
|
|
99
|
+
|
|
100
|
+
调用父类构造函数设置报告器名称为 'EmailReporter'。
|
|
101
|
+
初始化配置字典和配置状态标志。
|
|
102
|
+
"""
|
|
103
|
+
super().__init__("EmailReporter")
|
|
104
|
+
self.config: dict[str, Any] = {}
|
|
105
|
+
self.is_configured = False
|
|
106
|
+
|
|
107
|
+
def configure(self, **config: Any) -> bool:
|
|
108
|
+
"""
|
|
109
|
+
@methoddesc 配置邮件服务器参数并在配置阶段验证连接
|
|
110
|
+
|
|
111
|
+
该方法会验证所有必需的 SMTP 参数,并尝试建立连接进行认证验证。
|
|
112
|
+
只有配置验证通过后,才能调用 report() 方法发送邮件。
|
|
113
|
+
|
|
114
|
+
:param smtp_server: SMTP 服务器地址(如 'smtp.gmail.com')
|
|
115
|
+
:param smtp_port: SMTP 服务器端口
|
|
116
|
+
通常 587 使用 TLS,465 使用 SSL
|
|
117
|
+
:param sender_email: 发件人邮箱地址
|
|
118
|
+
:param sender_password: 发件人邮箱密码或应用专用密码
|
|
119
|
+
:param receiver_email: 收件人邮箱地址
|
|
120
|
+
:param config: 额外的可选配置参数(当前未使用,保留扩展性)
|
|
121
|
+
:return: 配置验证是否成功。成功返回 True,失败返回 False
|
|
122
|
+
|
|
123
|
+
验证流程:
|
|
124
|
+
1. 检查所有必需参数是否提供
|
|
125
|
+
2. 将配置参数保存到实例变量
|
|
126
|
+
3. 尝试建立 SMTP 连接并登录
|
|
127
|
+
4. 如果连接成功,设置 is_configured 为 True
|
|
128
|
+
"""
|
|
129
|
+
# 参数完整性检查:确保所有必需的配置都已提供
|
|
130
|
+
smtp_server = config.get("smtp_server")
|
|
131
|
+
smtp_port = config.get("smtp_port")
|
|
132
|
+
sender_email = config.get("sender_email")
|
|
133
|
+
sender_password = config.get("sender_password")
|
|
134
|
+
receiver_email = config.get("receiver_email")
|
|
135
|
+
if not all([smtp_server, smtp_port, sender_email, sender_password, receiver_email]):
|
|
136
|
+
print(f"[{self.name}] 邮件配置信息不完整, 无法启用。")
|
|
137
|
+
return False
|
|
138
|
+
|
|
139
|
+
# 显式处理 None,避免 mypy 将 Any | None 传给 int() 时报错
|
|
140
|
+
if smtp_port is None:
|
|
141
|
+
return False
|
|
142
|
+
|
|
143
|
+
# 将配置保存到实例变量中,供后续 report() 方法使用
|
|
144
|
+
self.config = {
|
|
145
|
+
"smtp_server": str(smtp_server),
|
|
146
|
+
"smtp_port": int(smtp_port),
|
|
147
|
+
"sender_email": str(sender_email),
|
|
148
|
+
"sender_password": str(sender_password),
|
|
149
|
+
"receiver_email": str(receiver_email),
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
# 尝试连接和登录以验证配置的正确性
|
|
153
|
+
# 使用 with 语句确保连接被正确关闭,即使发生异常
|
|
154
|
+
try:
|
|
155
|
+
# 与 report() 保持一致的双模式连接:
|
|
156
|
+
# - 端口 465 使用 SMTP_SSL(隐式 SSL,STARTTLS 握手会失败/超时)
|
|
157
|
+
# - 其余端口使用普通 SMTP + STARTTLS 升级加密
|
|
158
|
+
if int(smtp_port) == 465:
|
|
159
|
+
with smtplib.SMTP_SSL(str(smtp_server), int(smtp_port), timeout=10) as smtp:
|
|
160
|
+
# 使用提供的凭证登录
|
|
161
|
+
smtp.login(str(sender_email), str(sender_password))
|
|
162
|
+
else:
|
|
163
|
+
# 建立 SMTP 连接,设置 10 秒超时防止阻塞
|
|
164
|
+
with smtplib.SMTP(str(smtp_server), int(smtp_port), timeout=10) as smtp:
|
|
165
|
+
# 升级到 TLS 加密连接,这是现代 SMTP 服务的安全要求
|
|
166
|
+
# TLS 适用于端口 587,是比 SSL 更常用的选择
|
|
167
|
+
smtp.starttls()
|
|
168
|
+
# 使用提供的凭证登录
|
|
169
|
+
smtp.login(str(sender_email), str(sender_password))
|
|
170
|
+
|
|
171
|
+
# 配置验证成功,打印成功信息(不包含敏感密码)
|
|
172
|
+
print(f"[{self.name}] ✓ 邮件服务配置验证成功,发件人: {sender_email}")
|
|
173
|
+
self.is_configured = True
|
|
174
|
+
return True
|
|
175
|
+
|
|
176
|
+
except Exception as e:
|
|
177
|
+
# 配置验证失败,打印错误信息用于调试
|
|
178
|
+
print(f"[{self.name}] !! 错误: 邮件服务配置验证失败: {e}")
|
|
179
|
+
self.is_configured = False
|
|
180
|
+
return False
|
|
181
|
+
|
|
182
|
+
def report(self, errors: list[dict]) -> None:
|
|
183
|
+
"""
|
|
184
|
+
@methoddesc 将错误报告发送到指定的邮件地址
|
|
185
|
+
|
|
186
|
+
在发送邮件前会检查配置状态,确保邮件服务已正确配置。
|
|
187
|
+
邮件内容为 HTML 格式,包含错误数量和详细的 JSON 格式错误列表。
|
|
188
|
+
|
|
189
|
+
:param errors: 错误信息的字典列表,每个字典包含错误详情
|
|
190
|
+
典型字段:row_index, column, value, error_type, error_message
|
|
191
|
+
|
|
192
|
+
邮件内容格式:
|
|
193
|
+
- 标题:【数据验证警告】发现 N 个错误!
|
|
194
|
+
- 正文:包含错误数量的说明和 JSON 格式的错误详情
|
|
195
|
+
|
|
196
|
+
发送流程:
|
|
197
|
+
1. 检查是否已配置(is_configured 标志)
|
|
198
|
+
2. 构建 HTML 邮件内容
|
|
199
|
+
3. 设置邮件头部(Subject, From, To)
|
|
200
|
+
4. 根据端口选择 SMTP 或 SMTP_SSL 连接
|
|
201
|
+
5. 登录并发送邮件
|
|
202
|
+
6. 在 finally 块中确保连接关闭
|
|
203
|
+
"""
|
|
204
|
+
# 前置检查:确保邮件服务已配置
|
|
205
|
+
if not self.is_configured:
|
|
206
|
+
print(f"[{self.name}] !! 错误: 邮件服务未配置或配置失败,无法发送邮件。")
|
|
207
|
+
return
|
|
208
|
+
|
|
209
|
+
# 构建邮件标题,包含错误数量
|
|
210
|
+
subject = f"【数据验证警告】发现 {len(errors)} 个错误!"
|
|
211
|
+
|
|
212
|
+
# 构建 HTML 邮件正文,使用 JSON 格式化错误列表
|
|
213
|
+
# 使用 pre 标签保持 JSON 的格式和缩进
|
|
214
|
+
# html.escape 防止错误数据中的 <script>/标签注入邮件 HTML(XSS)
|
|
215
|
+
errors_json = html.escape(json.dumps(errors, indent=2, ensure_ascii=False))
|
|
216
|
+
html_body = f"""
|
|
217
|
+
<html><body>
|
|
218
|
+
<h2>检测到以下数据配置错误:</h2>
|
|
219
|
+
<pre style=\"background-color: #f0f0f0; padding: 10px; border-radius: 5px; white-space: pre-wrap; word-wrap: break-word;\">{errors_json}</pre>
|
|
220
|
+
<p>请尽快检查并修复。</p>
|
|
221
|
+
</body></html>
|
|
222
|
+
"""
|
|
223
|
+
|
|
224
|
+
# 创建 MIMEText 邮件对象,指定 HTML 格式和 UTF-8 编码
|
|
225
|
+
msg = MIMEText(html_body, "html", "utf-8")
|
|
226
|
+
|
|
227
|
+
# 使用 Header 对象编码邮件标题,确保中文正确显示
|
|
228
|
+
msg["Subject"] = cast(str, Header(subject, "utf-8"))
|
|
229
|
+
|
|
230
|
+
# 使用 formataddr 正确格式化发件人和收件人地址
|
|
231
|
+
# formataddr((友好名称, 邮件地址)) 格式
|
|
232
|
+
# 使用 Header 编码友好名称以支持中文
|
|
233
|
+
msg["From"] = formataddr((Header("数据验证服务", "utf-8").encode(), self.config["sender_email"]))
|
|
234
|
+
msg["To"] = formataddr((Header("项目负责人", "utf-8").encode(), self.config["receiver_email"]))
|
|
235
|
+
|
|
236
|
+
# 建立 SMTP 连接并发送邮件
|
|
237
|
+
smtp: smtplib.SMTP | smtplib.SMTP_SSL | None = None
|
|
238
|
+
try:
|
|
239
|
+
# 根据端口号判断使用哪种连接方式:
|
|
240
|
+
# - 端口 465 通常使用 SSL 加密的 SMTP_SSL
|
|
241
|
+
# - 端口 587 通常使用 STARTTLS 升级到 TLS
|
|
242
|
+
if self.config["smtp_port"] == 465:
|
|
243
|
+
smtp = smtplib.SMTP_SSL(self.config["smtp_server"], self.config["smtp_port"], timeout=10)
|
|
244
|
+
else:
|
|
245
|
+
# 端口不是 465,使用普通 SMTP 然后升级到 TLS
|
|
246
|
+
smtp = smtplib.SMTP(self.config["smtp_server"], self.config["smtp_port"], timeout=10)
|
|
247
|
+
if isinstance(smtp, smtplib.SMTP):
|
|
248
|
+
smtp.starttls()
|
|
249
|
+
|
|
250
|
+
# 使用配置中的凭证登录 SMTP 服务器
|
|
251
|
+
smtp.login(self.config["sender_email"], self.config["sender_password"])
|
|
252
|
+
|
|
253
|
+
# 发送邮件,sender 和 receiver 都可以是列表
|
|
254
|
+
smtp.sendmail(self.config["sender_email"], [self.config["receiver_email"]], msg.as_string())
|
|
255
|
+
|
|
256
|
+
# 发送成功,打印确认信息
|
|
257
|
+
print(f"[{self.name}] ✓ 成功发送错误报告邮件至 {self.config['receiver_email']}。")
|
|
258
|
+
|
|
259
|
+
except Exception as e:
|
|
260
|
+
# 发送失败,打印错误信息(不包含敏感内容)
|
|
261
|
+
print(f"[{self.name}] !! 错误: 发送邮件失败: {e}")
|
|
262
|
+
|
|
263
|
+
finally:
|
|
264
|
+
# 确保 SMTP 连接被正确关闭,释放资源
|
|
265
|
+
# quit() 自身的异常(连接已断等 secondary exception)不得吞掉
|
|
266
|
+
# try/except 块中捕获的原始业务异常
|
|
267
|
+
if smtp:
|
|
268
|
+
try:
|
|
269
|
+
smtp.quit()
|
|
270
|
+
except Exception: # noqa: BLE001 关闭失败的次要异常仅记录
|
|
271
|
+
print(f"[{self.name}] !! 警告: 关闭 SMTP 连接失败(忽略)。")
|