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,225 @@
|
|
|
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 DAG 执行器
|
|
18
|
+
|
|
19
|
+
功能概述:
|
|
20
|
+
- 按拓扑顺序执行 DAG 中的 transform 节点
|
|
21
|
+
- 管理每个节点的输出 DataFrame
|
|
22
|
+
- 将 transform 结果写回 parsed_datasets
|
|
23
|
+
|
|
24
|
+
数据流:
|
|
25
|
+
schema 数据源 → [transform] → [transform] → ...
|
|
26
|
+
每个节点的输出 DataFrame 存储在 node_outputs 中
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import logging
|
|
32
|
+
import re
|
|
33
|
+
|
|
34
|
+
import pandas as pd
|
|
35
|
+
|
|
36
|
+
from app.shared.core.project.regex.types import RegexNodeFile
|
|
37
|
+
from app.shared.core.project.transform.types import TransformFile
|
|
38
|
+
from app.shared.domain.regex_flags import parse_regex_flags
|
|
39
|
+
from app.shared.domain.transforms import create_runner
|
|
40
|
+
|
|
41
|
+
from .builder import ExecutionDAG
|
|
42
|
+
from .sorter import topological_sort
|
|
43
|
+
|
|
44
|
+
logger = logging.getLogger(__name__)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def execute_transform_dag(
|
|
48
|
+
dag: ExecutionDAG,
|
|
49
|
+
parsed_datasets: dict[str, pd.DataFrame],
|
|
50
|
+
) -> tuple[dict[str, pd.DataFrame], list[dict]]:
|
|
51
|
+
"""@methoddesc 执行 Transform DAG
|
|
52
|
+
|
|
53
|
+
参数:
|
|
54
|
+
dag: 执行 DAG(包含 transform 和 schema 节点)
|
|
55
|
+
parsed_datasets: 已解析的 DataFrame 字典(键为 schema 表 ID)
|
|
56
|
+
|
|
57
|
+
返回:
|
|
58
|
+
二元组 (parsed_datasets, dag_errors):
|
|
59
|
+
- parsed_datasets: 更新后的 DataFrame 字典(包含 transform 产生的新列)
|
|
60
|
+
- dag_errors: 节点执行失败产生的错误列表(含节点 ID、类型、原因)。
|
|
61
|
+
回归 #6: 原实现 transform/regex 节点执行异常仅 logger.exception 后把未转换的
|
|
62
|
+
输入复制为输出继续,execute_transform_dag 无错误返回通道,engine 无从感知 →
|
|
63
|
+
下游约束在未转换数据上"假通过"。现在把失败上报给调用方。
|
|
64
|
+
"""
|
|
65
|
+
if not dag.nodes:
|
|
66
|
+
return parsed_datasets, []
|
|
67
|
+
|
|
68
|
+
order = topological_sort(dag)
|
|
69
|
+
dag_errors: list[dict] = []
|
|
70
|
+
|
|
71
|
+
# 使用单一数据源:node_outputs 既是下游输入也是最终结果
|
|
72
|
+
# schema 节点直接引用 parsed_datasets 中的 DataFrame(延迟 copy,按需隔离)
|
|
73
|
+
node_outputs: dict[str, pd.DataFrame] = {}
|
|
74
|
+
schema_ids_in_dag: set[str] = set()
|
|
75
|
+
|
|
76
|
+
for sid, df in parsed_datasets.items():
|
|
77
|
+
if sid in dag.nodes:
|
|
78
|
+
node_outputs[sid] = df
|
|
79
|
+
schema_ids_in_dag.add(sid)
|
|
80
|
+
|
|
81
|
+
for node_id in order:
|
|
82
|
+
node = dag.nodes[node_id]
|
|
83
|
+
if node.node_type == "schema":
|
|
84
|
+
continue
|
|
85
|
+
|
|
86
|
+
if node.node_type == "transform":
|
|
87
|
+
tfile: TransformFile = node.data
|
|
88
|
+
if not tfile or not tfile.enabled:
|
|
89
|
+
continue
|
|
90
|
+
|
|
91
|
+
input_node_id = tfile.input_from_node or node_id
|
|
92
|
+
input_df = node_outputs.get(input_node_id)
|
|
93
|
+
|
|
94
|
+
if input_df is None:
|
|
95
|
+
logger.warning(f"Transform '{node_id}' 的输入节点 '{input_node_id}' 无可用数据,跳过")
|
|
96
|
+
continue
|
|
97
|
+
|
|
98
|
+
input_column = tfile.input_column
|
|
99
|
+
if input_column and input_column not in input_df.columns:
|
|
100
|
+
logger.warning(f"Transform '{node_id}' 的输入列 '{input_column}' 不存在,跳过")
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
runner = create_runner(tfile.type)
|
|
105
|
+
output_df = runner.execute(
|
|
106
|
+
input_df.copy(),
|
|
107
|
+
input_column or input_df.columns[0],
|
|
108
|
+
tfile.params,
|
|
109
|
+
tfile.output_columns,
|
|
110
|
+
)
|
|
111
|
+
node_outputs[node_id] = output_df
|
|
112
|
+
|
|
113
|
+
# 将 transform 结果合并回输入节点的输出
|
|
114
|
+
# 统一通过 node_outputs 操作,消除双数据源同步问题
|
|
115
|
+
if input_node_id in node_outputs:
|
|
116
|
+
existing_df = node_outputs[input_node_id]
|
|
117
|
+
if len(output_df) != len(existing_df):
|
|
118
|
+
# 行数改变(如 FilterRows/DropDuplicates/Aggregate):整体替换
|
|
119
|
+
node_outputs[input_node_id] = output_df.copy()
|
|
120
|
+
elif list(output_df.columns) == list(existing_df.columns):
|
|
121
|
+
# 回归 #7: output_df 列与输入完全一致 → 这是行级整表操作(如 SortRows
|
|
122
|
+
# 重排行、不产生新列)。原实现按 output_columns 循环贴回:
|
|
123
|
+
# - output_columns 为空(SortRows 正确用法)→ 循环不执行 → 排序结果
|
|
124
|
+
# 永远写不回,校验的是未排序数据;
|
|
125
|
+
# - output_columns 误填 → output_df[col].values 按位置贴回未排序行 →
|
|
126
|
+
# 行数据错配(id 与 val 对不上)。
|
|
127
|
+
# 行顺序已变的整表操作必须整体替换,不能按列贴回。
|
|
128
|
+
node_outputs[input_node_id] = output_df.copy()
|
|
129
|
+
else:
|
|
130
|
+
# 行数不变且产出新列(如 StringSplit/MathExpr 派生列):按列贴回
|
|
131
|
+
# 如果 existing_df 与 parsed_datasets 共享引用,先隔离再修改
|
|
132
|
+
if input_node_id in schema_ids_in_dag:
|
|
133
|
+
existing_df = existing_df.copy()
|
|
134
|
+
node_outputs[input_node_id] = existing_df
|
|
135
|
+
schema_ids_in_dag.discard(input_node_id)
|
|
136
|
+
for col in tfile.output_columns:
|
|
137
|
+
if col in output_df.columns:
|
|
138
|
+
existing_df[col] = output_df[col].values
|
|
139
|
+
except Exception as e:
|
|
140
|
+
logger.exception(f"Transform '{node_id}' 执行失败: {e}")
|
|
141
|
+
node_outputs[node_id] = input_df.copy()
|
|
142
|
+
dag_errors.append(
|
|
143
|
+
{
|
|
144
|
+
"error_type": "TransformExecutionError",
|
|
145
|
+
"node_id": node_id,
|
|
146
|
+
"node_type": "transform",
|
|
147
|
+
"message": f"Transform '{node_id}' 执行失败: {e}",
|
|
148
|
+
}
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
elif node.node_type == "regex":
|
|
152
|
+
rfile: RegexNodeFile = node.data
|
|
153
|
+
if not rfile or not rfile.enabled:
|
|
154
|
+
continue
|
|
155
|
+
|
|
156
|
+
input_node_id = rfile.input_from_node or ""
|
|
157
|
+
if not input_node_id and rfile.source_ref:
|
|
158
|
+
input_node_id = rfile.source_ref.table_id
|
|
159
|
+
|
|
160
|
+
input_df = node_outputs.get(input_node_id) if input_node_id else None
|
|
161
|
+
if input_df is None:
|
|
162
|
+
logger.warning(f"Regex '{node_id}' 的输入节点无可用数据,跳过")
|
|
163
|
+
continue
|
|
164
|
+
|
|
165
|
+
input_column = rfile.input_column or rfile.source_column_name
|
|
166
|
+
if not input_column or input_column not in input_df.columns:
|
|
167
|
+
logger.warning(f"Regex '{node_id}' 的输入列 '{input_column}' 不存在,跳过")
|
|
168
|
+
continue
|
|
169
|
+
|
|
170
|
+
pattern_str = rfile.pattern
|
|
171
|
+
if not pattern_str:
|
|
172
|
+
logger.warning(f"Regex '{node_id}' 未配置直接模式 pattern(uses_pattern 暂不支持 DAG 执行),跳过")
|
|
173
|
+
continue
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
# 透传 rfile.flags / case_sensitive(过去完全忽略,导致大小写等配置在 DAG 提取时失效)
|
|
177
|
+
re_flags = parse_regex_flags(getattr(rfile, "flags", "") or "")
|
|
178
|
+
if getattr(rfile, "case_sensitive", True) is False:
|
|
179
|
+
re_flags |= re.IGNORECASE
|
|
180
|
+
|
|
181
|
+
compiled = re.compile(pattern_str, re_flags)
|
|
182
|
+
extracted = input_df[input_column].astype(str).str.extract(compiled)
|
|
183
|
+
output_df = input_df.copy()
|
|
184
|
+
# 按位置映射 output_columns 到 extracted 列(支持无名捕获组)
|
|
185
|
+
for i, col in enumerate(rfile.output_columns or []):
|
|
186
|
+
if i < len(extracted.columns):
|
|
187
|
+
output_df[col] = extracted.iloc[:, i].values
|
|
188
|
+
node_outputs[node_id] = output_df
|
|
189
|
+
|
|
190
|
+
# 将提取的列合并回输入节点的输出
|
|
191
|
+
if input_node_id in node_outputs:
|
|
192
|
+
existing_df = node_outputs[input_node_id]
|
|
193
|
+
# 如果与 parsed_datasets 共享引用,先隔离再修改
|
|
194
|
+
if input_node_id in schema_ids_in_dag:
|
|
195
|
+
existing_df = existing_df.copy()
|
|
196
|
+
node_outputs[input_node_id] = existing_df
|
|
197
|
+
schema_ids_in_dag.discard(input_node_id)
|
|
198
|
+
for col in rfile.output_columns or []:
|
|
199
|
+
if col in output_df.columns:
|
|
200
|
+
existing_df[col] = output_df[col].values
|
|
201
|
+
except Exception as e:
|
|
202
|
+
# 提示面向用户:用节点名而非 UUID 定位,并给出人话成因与修法,
|
|
203
|
+
# Python 原始报错保留在 cause 位置供高级用户排查
|
|
204
|
+
regex_display = getattr(rfile, "name", "") or node_id
|
|
205
|
+
logger.exception(f"Regex '{node_id}' 执行失败: {e}")
|
|
206
|
+
node_outputs[node_id] = input_df.copy()
|
|
207
|
+
dag_errors.append(
|
|
208
|
+
{
|
|
209
|
+
"error_type": "RegexExecutionError",
|
|
210
|
+
"node_id": node_id,
|
|
211
|
+
"node_type": "regex",
|
|
212
|
+
"message": (
|
|
213
|
+
f"正则「{regex_display}」执行失败:{e}。"
|
|
214
|
+
"通常是它的匹配规则写法有误(例如命名分组的名字不能是纯数字),"
|
|
215
|
+
"请修改该正则的匹配规则后重新校验"
|
|
216
|
+
),
|
|
217
|
+
}
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
# 最终:将 node_outputs 中 schema 节点的结果写回 parsed_datasets
|
|
221
|
+
for sid in parsed_datasets:
|
|
222
|
+
if sid in node_outputs:
|
|
223
|
+
parsed_datasets[sid] = node_outputs[sid]
|
|
224
|
+
|
|
225
|
+
return parsed_datasets, dag_errors
|
|
@@ -0,0 +1,77 @@
|
|
|
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
|
+
- 对执行 DAG 进行拓扑排序
|
|
21
|
+
- 使用 Kahn 算法(基于入度)
|
|
22
|
+
- 检测循环依赖
|
|
23
|
+
|
|
24
|
+
使用示例:
|
|
25
|
+
order = topological_sort(dag)
|
|
26
|
+
# order = ["schema_1", "transform_a", "transform_b", "transform_c"]
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
from collections import deque
|
|
32
|
+
|
|
33
|
+
from .builder import ExecutionDAG
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def topological_sort(dag: ExecutionDAG) -> list[str]:
|
|
37
|
+
"""@methoddesc 对 DAG 进行拓扑排序
|
|
38
|
+
|
|
39
|
+
使用 Kahn 算法,按入度为零的顺序输出节点。
|
|
40
|
+
|
|
41
|
+
参数:
|
|
42
|
+
dag: 执行 DAG
|
|
43
|
+
|
|
44
|
+
返回:
|
|
45
|
+
节点 ID 的拓扑排序列表
|
|
46
|
+
|
|
47
|
+
异常:
|
|
48
|
+
ValueError: 存在循环依赖时抛出
|
|
49
|
+
"""
|
|
50
|
+
# 计算每个节点的入度
|
|
51
|
+
in_degree: dict[str, int] = {}
|
|
52
|
+
for node_id, node in dag.nodes.items():
|
|
53
|
+
in_degree[node_id] = len(node.incoming)
|
|
54
|
+
|
|
55
|
+
# 初始化队列,放入所有入度为 0 的节点
|
|
56
|
+
queue: deque[str] = deque()
|
|
57
|
+
for node_id, degree in in_degree.items():
|
|
58
|
+
if degree == 0:
|
|
59
|
+
queue.append(node_id)
|
|
60
|
+
|
|
61
|
+
result: list[str] = []
|
|
62
|
+
|
|
63
|
+
while queue:
|
|
64
|
+
current = queue.popleft()
|
|
65
|
+
result.append(current)
|
|
66
|
+
|
|
67
|
+
for neighbor in dag.nodes[current].outgoing:
|
|
68
|
+
in_degree[neighbor] -= 1
|
|
69
|
+
if in_degree[neighbor] == 0:
|
|
70
|
+
queue.append(neighbor)
|
|
71
|
+
|
|
72
|
+
if len(result) != len(dag.nodes):
|
|
73
|
+
# 存在循环依赖
|
|
74
|
+
unresolved = [nid for nid in dag.nodes if nid not in result]
|
|
75
|
+
raise ValueError(f"Transform DAG 存在循环依赖,无法完成拓扑排序。未解决节点: {unresolved}")
|
|
76
|
+
|
|
77
|
+
return result
|
|
@@ -0,0 +1,231 @@
|
|
|
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
|
+
- 按解析后的文件路径分组 Schema,支持同一 Excel 多 sheet 加载
|
|
21
|
+
- 支持表过滤(table_filter)及外键关联表自动收集
|
|
22
|
+
- 批量加载数据文件并返回原始 DataFrame 字典
|
|
23
|
+
- 收集加载过程中的错误(如目录不存在、数据源未找到)
|
|
24
|
+
|
|
25
|
+
架构设计:
|
|
26
|
+
- 单一职责模式: 只做数据加载,不涉及校验逻辑
|
|
27
|
+
- 组合依赖 DataSourceResolver 做路径解析
|
|
28
|
+
- 从 ValidationExecutor 中提取,独立为可复用组件
|
|
29
|
+
|
|
30
|
+
输入示例:
|
|
31
|
+
loader = DataLoader(resolver, dataset_schema, schema_by_id, settings)
|
|
32
|
+
raw_datasets, errors = loader.load_data_sources("./data", table_filter=["users"])
|
|
33
|
+
|
|
34
|
+
输出示例:
|
|
35
|
+
raw_datasets = {"users": pd.DataFrame(...), "orders": pd.DataFrame(...)}
|
|
36
|
+
errors = [{"error_type": "SourceNotFound", "message": "...", "table": "..."}]
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
import logging
|
|
40
|
+
import os
|
|
41
|
+
from collections import defaultdict
|
|
42
|
+
from typing import Any
|
|
43
|
+
|
|
44
|
+
import pandas as pd
|
|
45
|
+
|
|
46
|
+
from app.shared.core.data_source.loader import load_grouped_sources
|
|
47
|
+
from app.shared.core.data_source.schema_info import DataSourceInfo
|
|
48
|
+
from app.shared.core.project.schema.types import TableSchemaFile
|
|
49
|
+
from app.shared.domain.dataset_schema import DataSetSchema
|
|
50
|
+
|
|
51
|
+
from .resolver import DataSourceResolver
|
|
52
|
+
|
|
53
|
+
logger = logging.getLogger(__name__)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def collect_foreign_key_tables(dataset_schema: DataSetSchema, filter_set: set) -> set:
|
|
57
|
+
"""@methoddesc 收集外键关联表(标准/分块两条加载路径共用,A3)
|
|
58
|
+
|
|
59
|
+
当指定了 table_filter 时,除了过滤表本身,
|
|
60
|
+
还需要加载这些表引用的外键目标表,否则外键约束无法校验。
|
|
61
|
+
|
|
62
|
+
参数:
|
|
63
|
+
dataset_schema: 数据集 schema(读取 constraints 列表)
|
|
64
|
+
filter_set: 用户指定的过滤表集合
|
|
65
|
+
|
|
66
|
+
返回:
|
|
67
|
+
扩展后的表集合(包含外键引用的目标表)
|
|
68
|
+
"""
|
|
69
|
+
tables_to_load = set(filter_set)
|
|
70
|
+
for constraint in dataset_schema.constraints:
|
|
71
|
+
# ForeignKeyConstraints 使用 from_table 而非 table 属性
|
|
72
|
+
constraint_table = getattr(constraint, "from_table", None)
|
|
73
|
+
if constraint_table and constraint_table in filter_set:
|
|
74
|
+
if hasattr(constraint, "to_table") and constraint.to_table:
|
|
75
|
+
tables_to_load.add(constraint.to_table)
|
|
76
|
+
if hasattr(constraint, "reference_table") and constraint.reference_table:
|
|
77
|
+
tables_to_load.add(constraint.reference_table)
|
|
78
|
+
return tables_to_load
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class DataLoader:
|
|
82
|
+
"""
|
|
83
|
+
@classdesc 数据加载器
|
|
84
|
+
|
|
85
|
+
负责:
|
|
86
|
+
- 按解析后的文件路径分组 Schema(支持同一 Excel 多 sheet)
|
|
87
|
+
- 表过滤(table_filter)及外键关联表自动收集
|
|
88
|
+
- 批量加载数据文件并返回原始 DataFrame 字典
|
|
89
|
+
|
|
90
|
+
设计决策:
|
|
91
|
+
- 从 ValidationExecutor 中提取,单一职责:只做数据加载,不涉及校验
|
|
92
|
+
- 通过组合依赖 DataSourceResolver 做路径解析
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
def __init__(
|
|
96
|
+
self,
|
|
97
|
+
resolver: DataSourceResolver,
|
|
98
|
+
dataset_schema: DataSetSchema,
|
|
99
|
+
schema_by_id: dict[str, TableSchemaFile],
|
|
100
|
+
settings: Any,
|
|
101
|
+
):
|
|
102
|
+
self._resolver = resolver
|
|
103
|
+
self.dataset_schema = dataset_schema
|
|
104
|
+
self._schema_by_id = schema_by_id
|
|
105
|
+
self.settings = settings
|
|
106
|
+
|
|
107
|
+
def _resolve_search_directory(self, data_directory: str) -> str:
|
|
108
|
+
"""
|
|
109
|
+
@methoddesc 解析实际的数据搜索目录
|
|
110
|
+
|
|
111
|
+
如果 manifest 中配置了数据源目录,优先使用配置目录;
|
|
112
|
+
否则使用传入的数据目录。
|
|
113
|
+
|
|
114
|
+
参数:
|
|
115
|
+
data_directory: 默认数据目录
|
|
116
|
+
|
|
117
|
+
返回:
|
|
118
|
+
实际用于搜索数据文件的目录路径
|
|
119
|
+
"""
|
|
120
|
+
search_directory = data_directory
|
|
121
|
+
first_data_source = self._resolver.resolve_first_data_source()
|
|
122
|
+
if first_data_source:
|
|
123
|
+
search_directory = first_data_source
|
|
124
|
+
return search_directory
|
|
125
|
+
|
|
126
|
+
def _collect_foreign_key_tables(self, filter_set: set) -> set:
|
|
127
|
+
"""@methoddesc 收集外键关联表(委托给模块级 collect_foreign_key_tables,
|
|
128
|
+
分块加载路径共用同一逻辑)。"""
|
|
129
|
+
return collect_foreign_key_tables(self.dataset_schema, filter_set)
|
|
130
|
+
|
|
131
|
+
def load_data_sources(
|
|
132
|
+
self,
|
|
133
|
+
data_directory: str,
|
|
134
|
+
table_filter: str | list[str] | None = None,
|
|
135
|
+
) -> tuple[dict[str, pd.DataFrame], list[dict]]:
|
|
136
|
+
"""
|
|
137
|
+
@methoddesc 批量加载数据文件
|
|
138
|
+
|
|
139
|
+
按照 Schema 定义,解析并加载所有需要的数据文件。
|
|
140
|
+
支持同一 Excel 文件多 Sheet 的批量加载优化。
|
|
141
|
+
|
|
142
|
+
参数:
|
|
143
|
+
data_directory: 数据文件所在目录
|
|
144
|
+
table_filter: 表过滤条件,只加载指定表及其外键关联表
|
|
145
|
+
|
|
146
|
+
返回:
|
|
147
|
+
(原始数据集字典, 加载错误列表) 元组
|
|
148
|
+
"""
|
|
149
|
+
raw_datasets: dict[str, pd.DataFrame] = {}
|
|
150
|
+
loading_errors: list[dict] = []
|
|
151
|
+
|
|
152
|
+
# Step 1: 解析实际搜索目录
|
|
153
|
+
search_directory = self._resolve_search_directory(data_directory)
|
|
154
|
+
|
|
155
|
+
# 检查目录是否存在
|
|
156
|
+
if not os.path.isdir(search_directory):
|
|
157
|
+
loading_errors.append(
|
|
158
|
+
{
|
|
159
|
+
"error_type": "DirectoryNotFound",
|
|
160
|
+
"message": f"数据目录不存在: {data_directory}",
|
|
161
|
+
"source_path": data_directory,
|
|
162
|
+
}
|
|
163
|
+
)
|
|
164
|
+
return {}, loading_errors
|
|
165
|
+
|
|
166
|
+
# Step 2: 处理表过滤条件
|
|
167
|
+
filter_set: set | None = None
|
|
168
|
+
if table_filter:
|
|
169
|
+
if isinstance(table_filter, str):
|
|
170
|
+
filter_set = {table_filter}
|
|
171
|
+
else:
|
|
172
|
+
filter_set = set(table_filter)
|
|
173
|
+
|
|
174
|
+
# 收集需要加载的表(含外键关联表)
|
|
175
|
+
tables_to_load: set | None = None
|
|
176
|
+
if filter_set:
|
|
177
|
+
tables_to_load = self._collect_foreign_key_tables(filter_set)
|
|
178
|
+
|
|
179
|
+
# Step 3: 按文件路径分组 Schema(同一文件多个 Sheet 合并加载)
|
|
180
|
+
file_to_schemas: dict[str, list[DataSourceInfo]] = defaultdict(list)
|
|
181
|
+
file_to_sheet_names: dict[str, str] = {}
|
|
182
|
+
|
|
183
|
+
for table_id, table_schema in self.dataset_schema.tables.items():
|
|
184
|
+
schema_file = self._schema_by_id.get(table_id)
|
|
185
|
+
if not schema_file:
|
|
186
|
+
continue
|
|
187
|
+
|
|
188
|
+
# 应用表过滤
|
|
189
|
+
if tables_to_load:
|
|
190
|
+
table_name = table_schema.name or table_id
|
|
191
|
+
if table_name not in tables_to_load and table_id not in tables_to_load:
|
|
192
|
+
continue
|
|
193
|
+
|
|
194
|
+
# 解析数据源路径
|
|
195
|
+
source_path, sheet_name = self._resolver.resolve_source_path(data_directory, schema_file)
|
|
196
|
+
if not source_path:
|
|
197
|
+
loading_errors.append(
|
|
198
|
+
{
|
|
199
|
+
"error_type": "SourceNotFound",
|
|
200
|
+
"message": f"表 '{table_schema.name}' 未找到数据源(已查找目录: {search_directory})",
|
|
201
|
+
"table": table_schema.name,
|
|
202
|
+
}
|
|
203
|
+
)
|
|
204
|
+
continue
|
|
205
|
+
|
|
206
|
+
# 按文件路径分组,同一文件的不同 Sheet 放在一起加载
|
|
207
|
+
# 使用 DataSourceInfo DTO 将 domain TableSchema 转换为 core 层可用的最小信息
|
|
208
|
+
# 使用 getattr 以兼容测试中的 MockTableSchema 等不完整对象
|
|
209
|
+
file_to_schemas[source_path].append(
|
|
210
|
+
DataSourceInfo(
|
|
211
|
+
schema_id=table_id,
|
|
212
|
+
name=getattr(table_schema, "name", table_id),
|
|
213
|
+
sheet_name=getattr(table_schema, "sheet_name", None),
|
|
214
|
+
header_row=getattr(table_schema, "header_row", 0),
|
|
215
|
+
source_config=getattr(table_schema, "source_config", None) or {},
|
|
216
|
+
)
|
|
217
|
+
)
|
|
218
|
+
if sheet_name:
|
|
219
|
+
file_to_sheet_names[source_path] = sheet_name
|
|
220
|
+
|
|
221
|
+
# Step 4: 批量加载分组后的数据文件
|
|
222
|
+
loaded_data, load_errors = load_grouped_sources(
|
|
223
|
+
file_to_schemas,
|
|
224
|
+
default_encoding=self.settings.file_processing.default_encoding,
|
|
225
|
+
csv_delimiter=self.settings.file_processing.csv_delimiter,
|
|
226
|
+
file_to_sheet_names=file_to_sheet_names if file_to_sheet_names else None,
|
|
227
|
+
)
|
|
228
|
+
raw_datasets.update(loaded_data)
|
|
229
|
+
loading_errors.extend(load_errors)
|
|
230
|
+
|
|
231
|
+
return raw_datasets, loading_errors
|