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,337 @@
|
|
|
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 TypeConverter 类型转换器
|
|
18
|
+
|
|
19
|
+
功能概述:
|
|
20
|
+
- 实现数据类型转换功能
|
|
21
|
+
- 支持整数、浮点数、布尔值、字符串等类型转换
|
|
22
|
+
- 提供统一的类型转换接口
|
|
23
|
+
|
|
24
|
+
架构设计:
|
|
25
|
+
- 独立的转换器类,职责单一
|
|
26
|
+
- 支持批量转换和单个值转换
|
|
27
|
+
- 完善的错误处理
|
|
28
|
+
|
|
29
|
+
输入示例:
|
|
30
|
+
records = [{"id": "1", "amount": "12.5", "active": "true"}]
|
|
31
|
+
dtype = {"id": "int", "amount": "float", "active": "bool"}
|
|
32
|
+
|
|
33
|
+
输出示例:
|
|
34
|
+
[{"id": 1, "amount": 12.5, "active": True}]
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
from datetime import date, datetime
|
|
40
|
+
from decimal import Decimal, InvalidOperation
|
|
41
|
+
from typing import Any
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class TypeConverter:
|
|
45
|
+
"""
|
|
46
|
+
@classdesc 数据类型转换器
|
|
47
|
+
|
|
48
|
+
============================================================================
|
|
49
|
+
功能说明
|
|
50
|
+
============================================================================
|
|
51
|
+
将数据记录中的指定列转换为目标类型。
|
|
52
|
+
支持的类型:int, integer, float, decimal, bool, str, string, date, datetime
|
|
53
|
+
|
|
54
|
+
============================================================================
|
|
55
|
+
支持的类型
|
|
56
|
+
============================================================================
|
|
57
|
+
- int/integer: 整数类型
|
|
58
|
+
- float: 浮点数类型(科学计算)
|
|
59
|
+
- decimal: 高精度小数类型(财务计算,使用 decimal.Decimal,避免 float 精度丢失)
|
|
60
|
+
- bool/boolean: 布尔类型
|
|
61
|
+
- str/string: 字符串类型
|
|
62
|
+
- date/datetime/timestamp: 日期/日期时间类型(datetime.date / datetime.datetime)
|
|
63
|
+
|
|
64
|
+
============================================================================
|
|
65
|
+
业务场景
|
|
66
|
+
============================================================================
|
|
67
|
+
- 数据清洗和类型规范化
|
|
68
|
+
- CSV/JSON 数据类型统一
|
|
69
|
+
- 数据导入前的类型预处理
|
|
70
|
+
|
|
71
|
+
============================================================================
|
|
72
|
+
使用示例
|
|
73
|
+
============================================================================
|
|
74
|
+
>>> converter = TypeConverter()
|
|
75
|
+
>>>
|
|
76
|
+
>>> records = [
|
|
77
|
+
... {"id": "1", "amount": "12.5", "active": "true"},
|
|
78
|
+
... {"id": "2", "amount": "23.7", "active": "false"}
|
|
79
|
+
... ]
|
|
80
|
+
>>> dtype = {"id": "int", "amount": "decimal", "active": "bool"}
|
|
81
|
+
>>>
|
|
82
|
+
>>> result = converter.convert(records, dtype)
|
|
83
|
+
>>> # [{"id": 1, "amount": Decimal('12.5'), "active": True},
|
|
84
|
+
>>> # {"id": 2, "amount": Decimal('23.7'), "active": False}]
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
TYPE_MAPPING = {
|
|
88
|
+
"int": "int",
|
|
89
|
+
"integer": "int",
|
|
90
|
+
"float": "float",
|
|
91
|
+
"decimal": "decimal",
|
|
92
|
+
"bool": "bool",
|
|
93
|
+
"boolean": "bool",
|
|
94
|
+
"str": "str",
|
|
95
|
+
"string": "str",
|
|
96
|
+
"date": "date",
|
|
97
|
+
"datetime": "datetime",
|
|
98
|
+
"timestamp": "datetime",
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
def __init__(self) -> None:
|
|
102
|
+
"""
|
|
103
|
+
@methoddesc 初始化类型转换器
|
|
104
|
+
|
|
105
|
+
创建转换器实例,内置常见类型映射规则(int/integer、float/decimal、bool/boolean、str/string)。
|
|
106
|
+
"""
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
def convert(self, records: list[dict], dtype: dict[str, str]) -> list[dict]:
|
|
110
|
+
"""
|
|
111
|
+
@methoddesc 批量转换数据记录的类型
|
|
112
|
+
|
|
113
|
+
============================================================================
|
|
114
|
+
处理逻辑
|
|
115
|
+
============================================================================
|
|
116
|
+
1. 遍历每条记录
|
|
117
|
+
2. 对每条记录,根据 dtype 配置转换指定列
|
|
118
|
+
3. 保留原始值,如果转换失败则保留原值
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
records: 数据记录列表
|
|
122
|
+
dtype: 类型配置字典,键为列名,值为目标类型
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
转换后的数据记录列表
|
|
126
|
+
|
|
127
|
+
示例:
|
|
128
|
+
>>> records = [{"id": "1", "name": "Alice"}]
|
|
129
|
+
>>> dtype = {"id": "int"}
|
|
130
|
+
>>> result = converter.convert(records, dtype)
|
|
131
|
+
>>> # [{"id": 1, "name": "Alice"}]
|
|
132
|
+
"""
|
|
133
|
+
if not records or not dtype:
|
|
134
|
+
return records
|
|
135
|
+
|
|
136
|
+
result = []
|
|
137
|
+
for record in records:
|
|
138
|
+
converted = self._convert_record(record, dtype)
|
|
139
|
+
result.append(converted)
|
|
140
|
+
|
|
141
|
+
return result
|
|
142
|
+
|
|
143
|
+
def _convert_record(self, record: dict, dtype: dict[str, str]) -> dict:
|
|
144
|
+
"""
|
|
145
|
+
@methoddesc 转换单条记录
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
record: 单条数据记录
|
|
149
|
+
dtype: 类型配置
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
转换后的记录
|
|
153
|
+
"""
|
|
154
|
+
converted = record.copy()
|
|
155
|
+
|
|
156
|
+
for col, type_str in dtype.items():
|
|
157
|
+
if col in converted:
|
|
158
|
+
converted[col] = self._convert_value(converted[col], type_str)
|
|
159
|
+
|
|
160
|
+
return converted
|
|
161
|
+
|
|
162
|
+
def _convert_value(self, value: Any, type_str: str) -> Any:
|
|
163
|
+
"""
|
|
164
|
+
@methoddesc 转换单个值
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
value: 要转换的值
|
|
168
|
+
type_str: 目标类型字符串
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
转换后的值(如果转换失败,返回原始值)
|
|
172
|
+
|
|
173
|
+
Raises:
|
|
174
|
+
ValueError: 不支持的类型
|
|
175
|
+
"""
|
|
176
|
+
if value is None:
|
|
177
|
+
return None
|
|
178
|
+
|
|
179
|
+
normalized_type = self.TYPE_MAPPING.get(type_str.lower())
|
|
180
|
+
if not normalized_type:
|
|
181
|
+
return value
|
|
182
|
+
|
|
183
|
+
try:
|
|
184
|
+
if normalized_type == "int":
|
|
185
|
+
return self._to_int(value)
|
|
186
|
+
elif normalized_type == "float":
|
|
187
|
+
return self._to_float(value)
|
|
188
|
+
elif normalized_type == "decimal":
|
|
189
|
+
return self._to_decimal(value)
|
|
190
|
+
elif normalized_type == "bool":
|
|
191
|
+
return self._to_bool(value)
|
|
192
|
+
elif normalized_type == "str":
|
|
193
|
+
return str(value)
|
|
194
|
+
elif normalized_type == "date":
|
|
195
|
+
return self._to_date(value)
|
|
196
|
+
elif normalized_type == "datetime":
|
|
197
|
+
return self._to_datetime(value)
|
|
198
|
+
return value
|
|
199
|
+
except (ValueError, TypeError, InvalidOperation):
|
|
200
|
+
# InvalidOperation 继承 ArithmeticError(非 ValueError),非法 decimal 值同样回退原值
|
|
201
|
+
return value
|
|
202
|
+
|
|
203
|
+
def _to_int(self, value: Any) -> int:
|
|
204
|
+
"""
|
|
205
|
+
@methoddesc 转换为整数
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
value: 要转换的值
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
整数
|
|
212
|
+
"""
|
|
213
|
+
if isinstance(value, (int, float)):
|
|
214
|
+
return int(value)
|
|
215
|
+
return int(str(value).strip())
|
|
216
|
+
|
|
217
|
+
def _to_float(self, value: Any) -> float:
|
|
218
|
+
"""
|
|
219
|
+
@methoddesc 转换为浮点数
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
value: 要转换的值
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
浮点数
|
|
226
|
+
"""
|
|
227
|
+
if isinstance(value, (int, float)):
|
|
228
|
+
return float(value)
|
|
229
|
+
return float(str(value).strip())
|
|
230
|
+
|
|
231
|
+
def _to_decimal(self, value: Any) -> Decimal:
|
|
232
|
+
"""
|
|
233
|
+
@methoddesc 转换为高精度 Decimal
|
|
234
|
+
|
|
235
|
+
用于财务数据等需要精确十进制表示的场景,避免 float 的二进制精度丢失。
|
|
236
|
+
始终通过 str() 中转以避免 float -> Decimal 的二进制误差(如 Decimal(0.1) != 0.1)。
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
value: 要转换的值
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
Decimal 对象
|
|
243
|
+
"""
|
|
244
|
+
if isinstance(value, Decimal):
|
|
245
|
+
return value
|
|
246
|
+
if isinstance(value, bool):
|
|
247
|
+
# bool 是 int 子类,需先转 int 再转 Decimal,避免直接 str(True)="True" 报错
|
|
248
|
+
return Decimal(int(value))
|
|
249
|
+
if isinstance(value, (int, float)):
|
|
250
|
+
return Decimal(str(value))
|
|
251
|
+
return Decimal(str(value).strip())
|
|
252
|
+
|
|
253
|
+
def _to_date(self, value: Any) -> date:
|
|
254
|
+
"""
|
|
255
|
+
@methoddesc 转换为日期(date,不含时间分量)
|
|
256
|
+
|
|
257
|
+
优先解析 ISO 8601 格式(YYYY-MM-DD),失败则尝试常见替代格式。
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
value: 要转换的值
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
datetime.date 对象
|
|
264
|
+
"""
|
|
265
|
+
if isinstance(value, datetime):
|
|
266
|
+
return value.date()
|
|
267
|
+
if isinstance(value, date):
|
|
268
|
+
return value
|
|
269
|
+
return date.fromisoformat(str(value).strip())
|
|
270
|
+
|
|
271
|
+
def _to_datetime(self, value: Any) -> datetime:
|
|
272
|
+
"""
|
|
273
|
+
@methoddesc 转换为日期时间(datetime,含时间分量)
|
|
274
|
+
|
|
275
|
+
优先解析 ISO 8601 格式,失败则尝试常见替代格式。
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
value: 要转换的值
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
datetime.datetime 对象
|
|
282
|
+
"""
|
|
283
|
+
if isinstance(value, datetime):
|
|
284
|
+
return value
|
|
285
|
+
if isinstance(value, date):
|
|
286
|
+
return datetime(value.year, value.month, value.day)
|
|
287
|
+
return datetime.fromisoformat(str(value).strip())
|
|
288
|
+
|
|
289
|
+
def _to_bool(self, value: Any) -> bool:
|
|
290
|
+
"""
|
|
291
|
+
@methoddesc 转换为布尔值
|
|
292
|
+
|
|
293
|
+
处理多种布尔值表示:
|
|
294
|
+
- 字符串: "true", "false", "1", "0", "yes", "no"
|
|
295
|
+
- 数字: 1, 0
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
value: 要转换的值
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
布尔值
|
|
302
|
+
"""
|
|
303
|
+
if isinstance(value, bool):
|
|
304
|
+
return value
|
|
305
|
+
|
|
306
|
+
if isinstance(value, (int, float)):
|
|
307
|
+
return bool(value)
|
|
308
|
+
|
|
309
|
+
if isinstance(value, str):
|
|
310
|
+
lower = value.lower().strip()
|
|
311
|
+
if lower in ("true", "1", "yes", "on"):
|
|
312
|
+
return True
|
|
313
|
+
if lower in ("false", "0", "no", "off"):
|
|
314
|
+
return False
|
|
315
|
+
|
|
316
|
+
return bool(value)
|
|
317
|
+
|
|
318
|
+
def convert_single(self, value: Any, type_str: str) -> Any:
|
|
319
|
+
"""
|
|
320
|
+
@methoddesc 转换单个值
|
|
321
|
+
|
|
322
|
+
这是 convert 方法的单值版本,适用于单个值的类型转换。
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
value: 要转换的值
|
|
326
|
+
type_str: 目标类型
|
|
327
|
+
|
|
328
|
+
Returns:
|
|
329
|
+
转换后的值
|
|
330
|
+
|
|
331
|
+
示例:
|
|
332
|
+
>>> converter.convert_single("123", "int")
|
|
333
|
+
123
|
|
334
|
+
>>> converter.convert_single("true", "bool")
|
|
335
|
+
True
|
|
336
|
+
"""
|
|
337
|
+
return self._convert_value(value, type_str)
|
|
@@ -0,0 +1,252 @@
|
|
|
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 CSV 数据源加载器模块
|
|
18
|
+
|
|
19
|
+
功能概述:
|
|
20
|
+
- 加载标准 CSV 文件为 pandas DataFrame
|
|
21
|
+
- 自动检测文件编码(支持 fallback 编码列表)
|
|
22
|
+
- 使用 utf-8-sig 去除 UTF-8 BOM,防止第一列名损坏(B12)
|
|
23
|
+
- 空文件提前检查,给出清晰的 DataLoadError(B13)
|
|
24
|
+
- 支持自定义分隔符、引号字符、转义字符、跳行、限制行数
|
|
25
|
+
|
|
26
|
+
架构设计:
|
|
27
|
+
- 继承 DataSourceLoader[CSVSourceSpec],通过注册表自动发现
|
|
28
|
+
- 编码解析由 _resolve_encoding() 完成(父类提供)
|
|
29
|
+
- 读取参数根据 spec 动态构建,未配置项不传入 pandas
|
|
30
|
+
|
|
31
|
+
输入示例:
|
|
32
|
+
spec = CSVSourceSpec(
|
|
33
|
+
path="data/users.csv",
|
|
34
|
+
header_row=0,
|
|
35
|
+
delimiter=",",
|
|
36
|
+
encoding="auto",
|
|
37
|
+
fallback_encodings=["gbk", "latin1"]
|
|
38
|
+
)
|
|
39
|
+
loader = CSVLoader(spec)
|
|
40
|
+
|
|
41
|
+
输出示例:
|
|
42
|
+
df = loader.load()
|
|
43
|
+
# 返回 pandas.DataFrame,列名已根据 header_row 解析
|
|
44
|
+
# 若 header_enabled=False,列名自动生成为 col_0, col_1, ...
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
from __future__ import annotations
|
|
48
|
+
|
|
49
|
+
import logging
|
|
50
|
+
from pathlib import Path
|
|
51
|
+
from typing import Any
|
|
52
|
+
|
|
53
|
+
import pandas as pd
|
|
54
|
+
|
|
55
|
+
from ..specs.csv_source import CSVSourceSpec
|
|
56
|
+
from .base import DataLoadError, DataSourceLoader
|
|
57
|
+
from .registry import register_loader
|
|
58
|
+
|
|
59
|
+
logger = logging.getLogger(__name__)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def build_csv_read_kwargs(
|
|
63
|
+
*,
|
|
64
|
+
header_row: int | None,
|
|
65
|
+
encoding: str,
|
|
66
|
+
delimiter: str,
|
|
67
|
+
quotechar: str = '"',
|
|
68
|
+
on_bad_lines: str = "error", # 与 CSVSourceSpec 默认同口径(fail-closed)
|
|
69
|
+
escapechar: str | None = None,
|
|
70
|
+
skip_rows: int = 0,
|
|
71
|
+
) -> dict[str, Any]:
|
|
72
|
+
"""@methoddesc 构建 pandas read_csv 参数(标准加载与分块加载共用,A2)
|
|
73
|
+
|
|
74
|
+
标准 CSVLoader 与分块校验路径(chunked_loader)必须使用同一套参数构造,
|
|
75
|
+
否则同一文件在两条路径下解析结果不一致。统一行为:
|
|
76
|
+
- 编码 utf-8 升级为 utf-8-sig 自动去除 BOM(B12),避免首列名变 \\ufeffxxx
|
|
77
|
+
- quotechar/on_bad_lines 始终透传(与 pandas 默认值显式对齐)
|
|
78
|
+
- escapechar/skiprows 仅在配置时传入
|
|
79
|
+
|
|
80
|
+
注意: CSVOptions 无 nrows 字段,两侧 CSV 均不支持 nrows,由调用方按需追加。
|
|
81
|
+
|
|
82
|
+
参数:
|
|
83
|
+
header_row: 表头行号(None 表示无表头,由调用方决定语义)
|
|
84
|
+
encoding: 文件编码("utf-8" 会被升级为 "utf-8-sig")
|
|
85
|
+
delimiter: 字段分隔符
|
|
86
|
+
quotechar: 引号字符
|
|
87
|
+
on_bad_lines: 坏行处理策略(error/warn/skip)
|
|
88
|
+
escapechar: 转义字符(可选)
|
|
89
|
+
skip_rows: 跳过的数据行数(>0 时生效)
|
|
90
|
+
|
|
91
|
+
返回:
|
|
92
|
+
pandas read_csv 关键字参数字典
|
|
93
|
+
"""
|
|
94
|
+
# B12: utf-8 升级为 utf-8-sig 自动去除 UTF-8 BOM
|
|
95
|
+
if encoding == "utf-8":
|
|
96
|
+
encoding = "utf-8-sig"
|
|
97
|
+
|
|
98
|
+
read_kwargs: dict[str, Any] = {
|
|
99
|
+
"header": header_row,
|
|
100
|
+
"encoding": encoding,
|
|
101
|
+
"sep": delimiter,
|
|
102
|
+
"quotechar": quotechar,
|
|
103
|
+
"on_bad_lines": on_bad_lines,
|
|
104
|
+
}
|
|
105
|
+
if escapechar:
|
|
106
|
+
read_kwargs["escapechar"] = escapechar
|
|
107
|
+
if skip_rows > 0:
|
|
108
|
+
read_kwargs["skiprows"] = skip_rows
|
|
109
|
+
return read_kwargs
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@register_loader("csv")
|
|
113
|
+
class CSVLoader(DataSourceLoader[CSVSourceSpec]):
|
|
114
|
+
"""
|
|
115
|
+
@classdesc CSV 文件加载器
|
|
116
|
+
|
|
117
|
+
支持标准 CSV 文件,提供自动编码检测功能。
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
spec_class = CSVSourceSpec
|
|
121
|
+
|
|
122
|
+
def load(self) -> pd.DataFrame:
|
|
123
|
+
"""
|
|
124
|
+
@methoddesc 加载 CSV 文件
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
DataFrame
|
|
128
|
+
|
|
129
|
+
Raises:
|
|
130
|
+
DataLoadError: 加载失败时抛出
|
|
131
|
+
"""
|
|
132
|
+
try:
|
|
133
|
+
# 空文件提前检查,给出清晰错误(B13)
|
|
134
|
+
path = Path(self.spec.path)
|
|
135
|
+
if path.exists() and path.stat().st_size == 0:
|
|
136
|
+
raise DataLoadError(f"CSV 文件为空: {self.spec.path}", self.spec)
|
|
137
|
+
|
|
138
|
+
# 确定编码
|
|
139
|
+
encoding = self._resolve_encoding()
|
|
140
|
+
# 使用 utf-8-sig 自动去除 UTF-8 BOM,避免第一列名损坏(B12,含在 build_csv_read_kwargs 内)
|
|
141
|
+
# 构建读取参数(与分块加载路径共用构造逻辑,A2)
|
|
142
|
+
read_kwargs = build_csv_read_kwargs(
|
|
143
|
+
header_row=self.spec.header_row if self.spec.header_enabled else None,
|
|
144
|
+
encoding=encoding,
|
|
145
|
+
delimiter=self.spec.delimiter,
|
|
146
|
+
quotechar=self.spec.quotechar,
|
|
147
|
+
on_bad_lines=self.spec.on_bad_lines,
|
|
148
|
+
escapechar=self.spec.escapechar,
|
|
149
|
+
skip_rows=self.spec.skip_rows,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# nrows 仅标准加载器支持(分块路径用 chunksize 流式读取,不叠加行数限制)
|
|
153
|
+
if self.spec.nrows:
|
|
154
|
+
read_kwargs["nrows"] = self.spec.nrows
|
|
155
|
+
|
|
156
|
+
# 读取数据
|
|
157
|
+
df = pd.read_csv(self.spec.path, **read_kwargs)
|
|
158
|
+
|
|
159
|
+
# 如果没有表头,生成默认列名
|
|
160
|
+
if not self.spec.header_enabled:
|
|
161
|
+
df.columns = [f"col_{i}" for i in range(len(df.columns))]
|
|
162
|
+
|
|
163
|
+
return df
|
|
164
|
+
|
|
165
|
+
except FileNotFoundError as e:
|
|
166
|
+
raise DataLoadError(f"文件不存在: {self.spec.path}", self.spec, e)
|
|
167
|
+
except UnicodeDecodeError as e:
|
|
168
|
+
raise DataLoadError(
|
|
169
|
+
f"编码错误: 无法使用 {self.spec.encoding} 编码读取文件,尝试的编码: {self.spec.fallback_encodings}",
|
|
170
|
+
self.spec,
|
|
171
|
+
e,
|
|
172
|
+
)
|
|
173
|
+
except Exception as e:
|
|
174
|
+
raise DataLoadError(f"CSV 加载失败: {e}", self.spec, e)
|
|
175
|
+
|
|
176
|
+
def _resolve_encoding(self) -> str:
|
|
177
|
+
"""
|
|
178
|
+
@methoddesc 确定文件编码
|
|
179
|
+
|
|
180
|
+
如果启用自动检测,尝试多个编码;
|
|
181
|
+
否则直接使用配置的编码。
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
确定的编码名称
|
|
185
|
+
|
|
186
|
+
Raises:
|
|
187
|
+
UnicodeDecodeError: 所有编码都失败时抛出
|
|
188
|
+
"""
|
|
189
|
+
# 回归 #1 修复:用户配置 default_encoding="auto"(文档称自动检测)时,data_loader
|
|
190
|
+
# 路径会把 "auto" 原样透传到 spec.encoding。"auto" 不是 Python 可用编码名,
|
|
191
|
+
# 原样喂给 open() 会触发 LookupError(本方法仅 except UnicodeDecodeError,未捕获)→
|
|
192
|
+
# 报误导性的 "unknown encoding: auto" 且该路径所有 CSV 加载失败。
|
|
193
|
+
# 此处将 "auto" 归一化为回退编码列表的首项(utf-8),使其符合"自动检测"语义。
|
|
194
|
+
# fallback_encodings 默认 ["utf-8","gbk","latin1"],首项为 utf-8。
|
|
195
|
+
configured = self.spec.encoding
|
|
196
|
+
if configured == "auto":
|
|
197
|
+
configured = self.spec.fallback_encodings[0] if self.spec.fallback_encodings else "utf-8"
|
|
198
|
+
|
|
199
|
+
if not self.spec.encoding_detection:
|
|
200
|
+
return configured
|
|
201
|
+
|
|
202
|
+
# 尝试的编码列表
|
|
203
|
+
encodings = [configured] + [e for e in self.spec.fallback_encodings if e != configured]
|
|
204
|
+
|
|
205
|
+
last_error = None
|
|
206
|
+
|
|
207
|
+
for encoding in encodings:
|
|
208
|
+
try:
|
|
209
|
+
# 尝试读取前 64KB 来验证编码(B11)
|
|
210
|
+
# 之前只读 2 行,ASCII 表头 + 非 ASCII 数据会导致检测通过但后续读取失败
|
|
211
|
+
with open(self.spec.path, encoding=encoding) as f:
|
|
212
|
+
f.read(65536)
|
|
213
|
+
return encoding
|
|
214
|
+
except UnicodeDecodeError as e:
|
|
215
|
+
last_error = e
|
|
216
|
+
continue
|
|
217
|
+
|
|
218
|
+
# 所有编码都失败
|
|
219
|
+
if last_error:
|
|
220
|
+
raise last_error
|
|
221
|
+
|
|
222
|
+
return self.spec.encoding
|
|
223
|
+
|
|
224
|
+
def validate(self) -> list[str]:
|
|
225
|
+
"""
|
|
226
|
+
@methoddesc 验证 CSV 文件配置和文件本身。
|
|
227
|
+
|
|
228
|
+
检查项:
|
|
229
|
+
- 文件是否存在
|
|
230
|
+
- 文件扩展名是否为 .csv(如果不是则发出警告)
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
错误信息列表,空列表表示验证通过
|
|
234
|
+
|
|
235
|
+
示例:
|
|
236
|
+
>>> errors = loader.validate()
|
|
237
|
+
>>> if errors:
|
|
238
|
+
... print("验证失败:", errors)
|
|
239
|
+
"""
|
|
240
|
+
errors = []
|
|
241
|
+
path = Path(self.spec.path)
|
|
242
|
+
|
|
243
|
+
if not path.exists():
|
|
244
|
+
errors.append(f"文件不存在: {self.spec.path}")
|
|
245
|
+
return errors
|
|
246
|
+
|
|
247
|
+
# B19 修复:扩展名不符只是警告,不应作为错误返回(过去调用方按 errors 非空判定为失败)。
|
|
248
|
+
# 非 .csv 扩展名的文件仍可能合法(如 .tsv/.txt 导出的 CSV),仅记录日志。
|
|
249
|
+
if path.suffix.lower() != ".csv":
|
|
250
|
+
logger.warning(f"CSV 加载器收到的文件扩展名不是 .csv: {path.suffix}(路径: {path}),仍按 CSV 尝试解析")
|
|
251
|
+
|
|
252
|
+
return errors
|