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,268 @@
|
|
|
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 JSONPathExtractor 数据提取器
|
|
18
|
+
|
|
19
|
+
功能概述:
|
|
20
|
+
- 实现 JSONPath 表达式的数据提取
|
|
21
|
+
- 支持 $.key, $.key.subkey, $..key 递归查找等语法
|
|
22
|
+
- 提取嵌套 JSON 数据中的特定路径
|
|
23
|
+
|
|
24
|
+
架构设计:
|
|
25
|
+
- 独立的提取器类,职责单一
|
|
26
|
+
- 支持自定义错误处理
|
|
27
|
+
- 可扩展的路径解析器
|
|
28
|
+
|
|
29
|
+
输入示例:
|
|
30
|
+
data = {"users": [{"name": "Alice"}, {"name": "Bob"}]}
|
|
31
|
+
path = "$.users"
|
|
32
|
+
|
|
33
|
+
输出示例:
|
|
34
|
+
[{"name": "Alice"}, {"name": "Bob"}]
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
from typing import Any
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class JSONPathExtractor:
|
|
43
|
+
"""
|
|
44
|
+
@classdesc JSONPath 数据提取器
|
|
45
|
+
|
|
46
|
+
============================================================================
|
|
47
|
+
功能说明
|
|
48
|
+
============================================================================
|
|
49
|
+
实现简单的 JSONPath 表达式解析和数据提取。
|
|
50
|
+
支持常用的 JSONPath 语法,适用于大多数数据提取场景。
|
|
51
|
+
|
|
52
|
+
============================================================================
|
|
53
|
+
支持的语法
|
|
54
|
+
============================================================================
|
|
55
|
+
- $.key - 访问对象属性
|
|
56
|
+
- $.key.subkey - 嵌套属性访问
|
|
57
|
+
- $..key - 递归查找(返回所有匹配的值)
|
|
58
|
+
- $.key[*] - 数组全部元素
|
|
59
|
+
- $.key[0] - 数组指定索引
|
|
60
|
+
- $ - 根节点
|
|
61
|
+
|
|
62
|
+
============================================================================
|
|
63
|
+
业务场景
|
|
64
|
+
============================================================================
|
|
65
|
+
- 从嵌套 JSON 中提取数据
|
|
66
|
+
- 处理 API 返回的复杂 JSON 结构
|
|
67
|
+
- 数据清洗和转换
|
|
68
|
+
|
|
69
|
+
============================================================================
|
|
70
|
+
使用示例
|
|
71
|
+
============================================================================
|
|
72
|
+
>>> extractor = JSONPathExtractor()
|
|
73
|
+
>>>
|
|
74
|
+
>>> # 基本提取
|
|
75
|
+
>>> data = {"users": [{"name": "Alice"}, {"name": "Bob"}]}
|
|
76
|
+
>>> result = extractor.extract(data, "$.users")
|
|
77
|
+
>>> # [{"name": "Alice"}, {"name": "Bob"}]
|
|
78
|
+
>>>
|
|
79
|
+
>>> # 递归查找
|
|
80
|
+
>>> data = {"a": {"b": {"c": 1}}, "d": 2}
|
|
81
|
+
>>> result = extractor.extract(data, "$..c")
|
|
82
|
+
>>> # [1]
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
def __init__(self) -> None:
|
|
86
|
+
"""
|
|
87
|
+
@methoddesc 初始化 JSONPath 提取器
|
|
88
|
+
|
|
89
|
+
创建提取器实例,无需额外参数。
|
|
90
|
+
"""
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
def extract(self, data: Any, jsonpath: str) -> list[dict]:
|
|
94
|
+
"""
|
|
95
|
+
@methoddesc 从 JSON 数据中提取指定路径的数据
|
|
96
|
+
|
|
97
|
+
============================================================================
|
|
98
|
+
处理逻辑
|
|
99
|
+
============================================================================
|
|
100
|
+
1. 规范化路径(移除 $ 前缀)
|
|
101
|
+
2. 根据路径类型选择提取策略
|
|
102
|
+
- 递归查找 (..key): 使用深度优先搜索
|
|
103
|
+
- 普通路径 (key.subkey): 逐层解析
|
|
104
|
+
3. 展平结果(如果是嵌套列表)
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
data: JSON 数据(字典或列表)
|
|
108
|
+
jsonpath: JSONPath 表达式
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
提取后的数据列表
|
|
112
|
+
|
|
113
|
+
Raises:
|
|
114
|
+
ValueError: 路径不存在或格式错误
|
|
115
|
+
"""
|
|
116
|
+
if not jsonpath or jsonpath == "$":
|
|
117
|
+
return data if isinstance(data, list) else [data]
|
|
118
|
+
|
|
119
|
+
normalized_path = self._normalize_path(jsonpath)
|
|
120
|
+
|
|
121
|
+
if normalized_path.startswith(".."):
|
|
122
|
+
return self._extract_recursive(data, normalized_path[2:])
|
|
123
|
+
|
|
124
|
+
return self._extract_by_path(data, normalized_path)
|
|
125
|
+
|
|
126
|
+
def _normalize_path(self, path: str) -> str:
|
|
127
|
+
"""
|
|
128
|
+
@methoddesc 规范化 JSONPath
|
|
129
|
+
|
|
130
|
+
移除 $ 前缀,统一路径格式。
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
path: 原始路径
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
规范化后的路径
|
|
137
|
+
"""
|
|
138
|
+
if path.startswith("$."):
|
|
139
|
+
return path[2:]
|
|
140
|
+
elif path.startswith("$"):
|
|
141
|
+
return path[1:]
|
|
142
|
+
return path
|
|
143
|
+
|
|
144
|
+
def _extract_by_path(self, data: Any, path: str) -> list[dict]:
|
|
145
|
+
"""
|
|
146
|
+
@methoddesc 根据路径提取数据
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
data: JSON 数据
|
|
150
|
+
path: 路径(不含 $ 前缀)
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
提取后的数据列表
|
|
154
|
+
"""
|
|
155
|
+
current = data
|
|
156
|
+
parts = self._parse_path(path)
|
|
157
|
+
|
|
158
|
+
for part in parts:
|
|
159
|
+
current = self._navigate_step(current, part)
|
|
160
|
+
|
|
161
|
+
if current is None:
|
|
162
|
+
return []
|
|
163
|
+
|
|
164
|
+
if isinstance(current, list):
|
|
165
|
+
return current
|
|
166
|
+
else:
|
|
167
|
+
return [current] if current is not None else []
|
|
168
|
+
|
|
169
|
+
def _parse_path(self, path: str) -> list[str | int]:
|
|
170
|
+
"""
|
|
171
|
+
@methoddesc 解析路径为组成部分
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
path: 路径字符串
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
路径部分列表(字符串或整数)
|
|
178
|
+
"""
|
|
179
|
+
parts: list[str | int] = []
|
|
180
|
+
current = ""
|
|
181
|
+
i = 0
|
|
182
|
+
|
|
183
|
+
while i < len(path):
|
|
184
|
+
char = path[i]
|
|
185
|
+
|
|
186
|
+
if char == ".":
|
|
187
|
+
if current:
|
|
188
|
+
parts.append(current)
|
|
189
|
+
current = ""
|
|
190
|
+
elif char == "[":
|
|
191
|
+
if current:
|
|
192
|
+
parts.append(current)
|
|
193
|
+
current = ""
|
|
194
|
+
end = path.find("]", i)
|
|
195
|
+
if end != -1:
|
|
196
|
+
key = path[i + 1 : end].strip("\"'")
|
|
197
|
+
try:
|
|
198
|
+
parts.append(int(key))
|
|
199
|
+
except ValueError:
|
|
200
|
+
parts.append(key)
|
|
201
|
+
i = end
|
|
202
|
+
else:
|
|
203
|
+
current += char
|
|
204
|
+
|
|
205
|
+
i += 1
|
|
206
|
+
|
|
207
|
+
if current:
|
|
208
|
+
parts.append(current)
|
|
209
|
+
|
|
210
|
+
return parts
|
|
211
|
+
|
|
212
|
+
def _navigate_step(self, data: Any, step: str | int) -> Any:
|
|
213
|
+
"""
|
|
214
|
+
@methoddesc 单步导航
|
|
215
|
+
|
|
216
|
+
根据当前数据类型(字典或列表)和路径步骤,
|
|
217
|
+
导航到下一层数据。
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
data: 当前数据
|
|
221
|
+
step: 路径部分(字符串键或整数索引)
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
下一步的数据
|
|
225
|
+
"""
|
|
226
|
+
if isinstance(data, dict):
|
|
227
|
+
return data.get(step)
|
|
228
|
+
elif isinstance(data, list):
|
|
229
|
+
if isinstance(step, int):
|
|
230
|
+
if 0 <= step < len(data):
|
|
231
|
+
return data[step]
|
|
232
|
+
return None
|
|
233
|
+
elif step == "*":
|
|
234
|
+
return data
|
|
235
|
+
return None
|
|
236
|
+
return None
|
|
237
|
+
|
|
238
|
+
def _extract_recursive(self, data: Any, key: str) -> list[Any]:
|
|
239
|
+
"""
|
|
240
|
+
@methoddesc 递归查找指定键的所有值
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
data: JSON 数据
|
|
244
|
+
key: 要查找的键名
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
所有匹配的值列表
|
|
248
|
+
"""
|
|
249
|
+
results: list[Any] = []
|
|
250
|
+
|
|
251
|
+
def search(obj: Any) -> None:
|
|
252
|
+
"""
|
|
253
|
+
@methoddesc 递归搜索对象中的目标键值
|
|
254
|
+
|
|
255
|
+
参数:
|
|
256
|
+
obj: 当前搜索的对象(字典或列表)
|
|
257
|
+
"""
|
|
258
|
+
if isinstance(obj, dict):
|
|
259
|
+
if key in obj:
|
|
260
|
+
results.append(obj[key])
|
|
261
|
+
for value in obj.values():
|
|
262
|
+
search(value)
|
|
263
|
+
elif isinstance(obj, list):
|
|
264
|
+
for item in obj:
|
|
265
|
+
search(item)
|
|
266
|
+
|
|
267
|
+
search(data)
|
|
268
|
+
return results
|
|
@@ -0,0 +1,354 @@
|
|
|
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 JSON 数据源加载器模块
|
|
18
|
+
|
|
19
|
+
功能概述:
|
|
20
|
+
- 加载 JSON / JSONL / NDJSON 文件为 pandas DataFrame
|
|
21
|
+
- 使用策略模式自动识别 JSON 格式(array / lines / object / auto)
|
|
22
|
+
- 支持 JSONPath 数据提取(B14: 空列表不再回退到原始未过滤数据)
|
|
23
|
+
- 支持 record_path 通过 pd.json_normalize 展开嵌套数组(B15)
|
|
24
|
+
- 加载后应用 dtype 转换(B17)
|
|
25
|
+
- 自动解析器支持递归查找列表元素(B18)
|
|
26
|
+
- JSON Lines 中非字典条目包装为 {"value": parsed}(B19)
|
|
27
|
+
|
|
28
|
+
架构设计:
|
|
29
|
+
- 继承 DataSourceLoader[JSONSourceSpec],通过注册表自动发现
|
|
30
|
+
- 解析逻辑委托给 strategies/ 下的策略类(AutoDetectParser、LinesParser 等)
|
|
31
|
+
- 数据提取委托给 JSONPathExtractor
|
|
32
|
+
- 类型转换委托给 TypeConverter
|
|
33
|
+
|
|
34
|
+
输入示例:
|
|
35
|
+
spec = JSONSourceSpec(
|
|
36
|
+
path="data/users.json",
|
|
37
|
+
format="auto",
|
|
38
|
+
json_path="$.data.users",
|
|
39
|
+
record_path="orders",
|
|
40
|
+
dtype={"age": "int64"}
|
|
41
|
+
)
|
|
42
|
+
loader = JSONLoader(spec)
|
|
43
|
+
|
|
44
|
+
输出示例:
|
|
45
|
+
df = loader.load()
|
|
46
|
+
# 返回 pandas.DataFrame,JSONPath 和 record_path 已应用
|
|
47
|
+
# 若 json_path 返回空列表则抛出 DataLoadError
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
from __future__ import annotations
|
|
51
|
+
|
|
52
|
+
import json
|
|
53
|
+
from pathlib import Path
|
|
54
|
+
|
|
55
|
+
import pandas as pd
|
|
56
|
+
|
|
57
|
+
from ..specs.json_source import JSONSourceSpec
|
|
58
|
+
from .base import DataLoadError, DataSourceLoader
|
|
59
|
+
from .converter import TypeConverter
|
|
60
|
+
from .extractor import JSONPathExtractor
|
|
61
|
+
from .registry import register_loader
|
|
62
|
+
from .strategies import JSONParserStrategy, get_parser
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@register_loader("json")
|
|
66
|
+
class JSONLoader(DataSourceLoader[JSONSourceSpec]):
|
|
67
|
+
"""
|
|
68
|
+
@classdesc JSON 文件加载器
|
|
69
|
+
|
|
70
|
+
============================================================================
|
|
71
|
+
功能说明
|
|
72
|
+
============================================================================
|
|
73
|
+
使用策略模式加载和解析 JSON 文件,支持多种格式。
|
|
74
|
+
核心职责是流程编排,实际解析逻辑委托给策略类和工具类。
|
|
75
|
+
|
|
76
|
+
============================================================================
|
|
77
|
+
支持的 JSON 格式
|
|
78
|
+
============================================================================
|
|
79
|
+
- 标准数组 (array): [{"a": 1}, {"a": 2}]
|
|
80
|
+
- JSON Lines (lines): 每行一个 JSON 对象
|
|
81
|
+
- 嵌套对象 (object): {"data": [{"a": 1}]}
|
|
82
|
+
- 自动检测 (auto): 自动识别最佳格式
|
|
83
|
+
|
|
84
|
+
============================================================================
|
|
85
|
+
架构设计
|
|
86
|
+
============================================================================
|
|
87
|
+
加载流程:
|
|
88
|
+
1. 读取文件内容
|
|
89
|
+
2. 使用 ParserStrategy 解析
|
|
90
|
+
3. 使用 JSONPathExtractor 提取数据
|
|
91
|
+
4. 使用 TypeConverter 转换类型
|
|
92
|
+
5. 转换为 DataFrame
|
|
93
|
+
|
|
94
|
+
============================================================================
|
|
95
|
+
使用示例
|
|
96
|
+
============================================================================
|
|
97
|
+
>>> from app.shared.core.data_source.loaders.json_loader import JSONLoader
|
|
98
|
+
>>> from app.shared.core.data_source.specs.json_source import JSONSourceSpec
|
|
99
|
+
>>>
|
|
100
|
+
>>> spec = JSONSourceSpec(path="data/users.json", format="auto")
|
|
101
|
+
>>> loader = JSONLoader(spec)
|
|
102
|
+
>>> df = loader.load()
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
spec_class = JSONSourceSpec
|
|
106
|
+
|
|
107
|
+
FORMAT_ARRAY = "array"
|
|
108
|
+
FORMAT_LINES = "lines"
|
|
109
|
+
FORMAT_OBJECT = "object"
|
|
110
|
+
|
|
111
|
+
def __init__(self, spec: JSONSourceSpec):
|
|
112
|
+
"""
|
|
113
|
+
初始化 JSON 加载器
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
spec: JSON 数据源配置
|
|
117
|
+
"""
|
|
118
|
+
super().__init__(spec)
|
|
119
|
+
self._parser: JSONParserStrategy | None = None
|
|
120
|
+
self._extractor = JSONPathExtractor()
|
|
121
|
+
self._converter = TypeConverter()
|
|
122
|
+
|
|
123
|
+
def load(self) -> pd.DataFrame:
|
|
124
|
+
"""
|
|
125
|
+
@methoddesc 加载 JSON 文件并返回 DataFrame
|
|
126
|
+
|
|
127
|
+
============================================================================
|
|
128
|
+
处理流程
|
|
129
|
+
============================================================================
|
|
130
|
+
1. 读取文件内容
|
|
131
|
+
2. 使用策略模式解析 JSON
|
|
132
|
+
3. 应用 JSONPath 提取(如果配置了)
|
|
133
|
+
4. 转换数据类型(如果配置了)
|
|
134
|
+
5. 转换为 DataFrame 并扁平化
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
加载的数据 DataFrame
|
|
138
|
+
|
|
139
|
+
Raises:
|
|
140
|
+
DataLoadError: 加载失败时抛出
|
|
141
|
+
"""
|
|
142
|
+
try:
|
|
143
|
+
content = self._read_file()
|
|
144
|
+
|
|
145
|
+
if self.spec.json_path and self.spec.format in (self.FORMAT_OBJECT, self.FORMAT_ARRAY):
|
|
146
|
+
# 用户已通过 json_path 显式指定记录路径(object/array 格式均可),
|
|
147
|
+
# 无需走策略解析器(避免 D8 ArrayParser 对嵌套对象的启发式拒绝)。
|
|
148
|
+
# 直接解析原始 JSON,由 extractor 按 json_path 精确定位数据数组。
|
|
149
|
+
# 注:json_path 即显式消歧,无任何"取最长/取第一个数组"的猜测。
|
|
150
|
+
records = json.loads(content)
|
|
151
|
+
extracted = self._extractor.extract(records, self.spec.json_path)
|
|
152
|
+
# B14: 空列表是合法结果,不回退到未过滤数据。
|
|
153
|
+
# - extracted 为非空 list → 直接使用
|
|
154
|
+
# - extracted 为空 list → 保持空(path 未命中,不回退原始数据)
|
|
155
|
+
# - extracted 为非 list(标量/字典)→ 包装为单元素列表
|
|
156
|
+
if isinstance(extracted, list):
|
|
157
|
+
records = extracted
|
|
158
|
+
else:
|
|
159
|
+
records = [extracted] if extracted is not None else []
|
|
160
|
+
else:
|
|
161
|
+
# 无 json_path(或 lines 格式):走策略解析器。
|
|
162
|
+
# D8 对顶层数组/单条 dict 的处理 + 对嵌套对象的拒绝在此生效。
|
|
163
|
+
parser = self._get_parser()
|
|
164
|
+
records = parser.parse(content)
|
|
165
|
+
|
|
166
|
+
df = self._convert_to_dataframe(records)
|
|
167
|
+
|
|
168
|
+
if self.spec.flatten and self._has_nested_structure(df):
|
|
169
|
+
df = self._flatten_dataframe(df)
|
|
170
|
+
|
|
171
|
+
# dtype 转换在扁平化之后执行,确保列名匹配(B17)
|
|
172
|
+
dtype = getattr(self.spec, "dtype", None)
|
|
173
|
+
if dtype:
|
|
174
|
+
df = self._apply_dtype_to_dataframe(df, dtype)
|
|
175
|
+
|
|
176
|
+
return df
|
|
177
|
+
|
|
178
|
+
except FileNotFoundError as e:
|
|
179
|
+
raise DataLoadError(f"文件不存在: {self.spec.path}", self.spec, e)
|
|
180
|
+
except json.JSONDecodeError as e:
|
|
181
|
+
raise DataLoadError(f"JSON 解析错误 (行 {e.lineno}, 列 {e.colno}): {e.msg}", self.spec, e)
|
|
182
|
+
except DataLoadError:
|
|
183
|
+
raise
|
|
184
|
+
except Exception as e:
|
|
185
|
+
raise DataLoadError(f"JSON 加载失败: {e}", self.spec, e)
|
|
186
|
+
|
|
187
|
+
def _read_file(self) -> str:
|
|
188
|
+
"""
|
|
189
|
+
@methoddesc 读取文件内容
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
文件内容字符串
|
|
193
|
+
"""
|
|
194
|
+
with open(self.spec.path, encoding=self.spec.encoding) as f:
|
|
195
|
+
return f.read()
|
|
196
|
+
|
|
197
|
+
def _get_parser(self) -> JSONParserStrategy:
|
|
198
|
+
"""
|
|
199
|
+
@methoddesc 获取合适的 JSON 解析器
|
|
200
|
+
|
|
201
|
+
根据配置选择或自动检测合适的解析策略。
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
JSON 解析策略实例
|
|
205
|
+
"""
|
|
206
|
+
if self._parser:
|
|
207
|
+
return self._parser
|
|
208
|
+
|
|
209
|
+
# D8: format 必须由 spec 显式提供(auto 已废弃)。spec 层已强制 format 必填,
|
|
210
|
+
# 此处直接取 spec.format;若为 None(异常情况)由 get_parser 报错引导。
|
|
211
|
+
format_type = self.spec.format
|
|
212
|
+
if not format_type:
|
|
213
|
+
raise ValueError(
|
|
214
|
+
"JSON 数据源未指定 format。请显式指定:array(顶层数组)、"
|
|
215
|
+
"lines(JSON Lines)、或 object(嵌套对象,需配合 json_path)。"
|
|
216
|
+
)
|
|
217
|
+
self._parser = get_parser(format_type)
|
|
218
|
+
return self._parser
|
|
219
|
+
|
|
220
|
+
def _convert_to_dataframe(self, data: list[dict]) -> pd.DataFrame:
|
|
221
|
+
"""
|
|
222
|
+
@methoddesc 将数据列表转换为 DataFrame
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
data: 数据字典列表
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
DataFrame
|
|
229
|
+
"""
|
|
230
|
+
if not data:
|
|
231
|
+
return pd.DataFrame()
|
|
232
|
+
|
|
233
|
+
standardized = []
|
|
234
|
+
for record in data:
|
|
235
|
+
if isinstance(record, dict):
|
|
236
|
+
standardized.append(record)
|
|
237
|
+
else:
|
|
238
|
+
standardized.append({"value": record})
|
|
239
|
+
|
|
240
|
+
# 支持 record_path 展开嵌套数组(B15)
|
|
241
|
+
# 设计说明:record_path 统一由 pd.json_normalize 处理,不再需要预标准化步骤。
|
|
242
|
+
# pd.json_normalize 的 record_path 参数会自动将嵌套数组展开为独立行,
|
|
243
|
+
# 与之前的手动 _apply_record_path 逻辑等价,且更可靠。
|
|
244
|
+
record_path = getattr(self.spec, "record_path", None)
|
|
245
|
+
if record_path:
|
|
246
|
+
meta_prefix = getattr(self.spec, "meta_prefix", "meta.")
|
|
247
|
+
sep = getattr(self.spec, "sep", ".")
|
|
248
|
+
df = pd.json_normalize(
|
|
249
|
+
standardized,
|
|
250
|
+
record_path=record_path,
|
|
251
|
+
meta_prefix=meta_prefix,
|
|
252
|
+
sep=sep,
|
|
253
|
+
)
|
|
254
|
+
else:
|
|
255
|
+
df = pd.json_normalize(standardized)
|
|
256
|
+
return df
|
|
257
|
+
|
|
258
|
+
def _has_nested_structure(self, df: pd.DataFrame) -> bool:
|
|
259
|
+
"""
|
|
260
|
+
@methoddesc 检查 DataFrame 是否包含嵌套结构
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
df: DataFrame
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
是否存在嵌套结构
|
|
267
|
+
"""
|
|
268
|
+
for col in df.columns:
|
|
269
|
+
if df[col].dtype == "object":
|
|
270
|
+
sample = df[col].dropna().head(100)
|
|
271
|
+
for val in sample:
|
|
272
|
+
if isinstance(val, (dict, list)):
|
|
273
|
+
return True
|
|
274
|
+
return False
|
|
275
|
+
|
|
276
|
+
def _flatten_dataframe(self, df: pd.DataFrame) -> pd.DataFrame:
|
|
277
|
+
"""
|
|
278
|
+
@methoddesc 扁平化 DataFrame 中的嵌套结构
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
df: 原始 DataFrame
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
扁平化后的 DataFrame
|
|
285
|
+
"""
|
|
286
|
+
if df.empty:
|
|
287
|
+
return df
|
|
288
|
+
|
|
289
|
+
records = df.to_dict("records")
|
|
290
|
+
|
|
291
|
+
flat_df = pd.json_normalize(records, sep=self.spec.sep, max_level=self.spec.max_depth)
|
|
292
|
+
|
|
293
|
+
return flat_df
|
|
294
|
+
|
|
295
|
+
def validate(self) -> list[str]:
|
|
296
|
+
"""
|
|
297
|
+
@methoddesc 验证 JSON 文件配置和文件本身。
|
|
298
|
+
|
|
299
|
+
检查项:
|
|
300
|
+
- 文件是否存在
|
|
301
|
+
- 文件扩展名是否为 .json、.jsonl 或 .ndjson
|
|
302
|
+
- 前 5 行是否为有效的 JSON 格式(仅 JSON Lines 文件)
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
错误信息列表,空列表表示验证通过
|
|
306
|
+
|
|
307
|
+
示例:
|
|
308
|
+
>>> errors = loader.validate()
|
|
309
|
+
>>> if errors:
|
|
310
|
+
... print("验证失败:", errors)
|
|
311
|
+
"""
|
|
312
|
+
errors = []
|
|
313
|
+
path = Path(self.spec.path)
|
|
314
|
+
|
|
315
|
+
if not path.exists():
|
|
316
|
+
errors.append(f"文件不存在: {self.spec.path}")
|
|
317
|
+
return errors
|
|
318
|
+
|
|
319
|
+
if path.suffix.lower() not in [".json", ".jsonl", ".ndjson"]:
|
|
320
|
+
errors.append(f"警告: 文件扩展名不是 .json/.jsonl/.ndjson: {path.suffix}")
|
|
321
|
+
|
|
322
|
+
try:
|
|
323
|
+
with open(path, encoding=self.spec.encoding) as f:
|
|
324
|
+
for _ in range(5):
|
|
325
|
+
line = f.readline()
|
|
326
|
+
if not line:
|
|
327
|
+
break
|
|
328
|
+
if path.suffix.lower() in [".jsonl", ".ndjson"]:
|
|
329
|
+
json.loads(line.strip())
|
|
330
|
+
except json.JSONDecodeError as e:
|
|
331
|
+
errors.append(f"JSON 格式错误: {e.msg}")
|
|
332
|
+
except Exception as e:
|
|
333
|
+
errors.append(f"无法读取文件: {e}")
|
|
334
|
+
|
|
335
|
+
return errors
|
|
336
|
+
|
|
337
|
+
def _apply_dtype_to_dataframe(self, df: pd.DataFrame, dtype: dict[str, str]) -> pd.DataFrame:
|
|
338
|
+
"""@methoddesc 对 DataFrame 的指定列进行类型转换(B17)。"""
|
|
339
|
+
if df.empty or not dtype:
|
|
340
|
+
return df
|
|
341
|
+
for col, type_str in dtype.items():
|
|
342
|
+
if col in df.columns:
|
|
343
|
+
df[col] = df[col].apply(lambda x: self._converter.convert_single(x, type_str))
|
|
344
|
+
return df
|
|
345
|
+
|
|
346
|
+
def get_schema(self) -> dict[str, str]:
|
|
347
|
+
"""
|
|
348
|
+
@methoddesc 获取数据源的 schema
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
列名到类型的映射
|
|
352
|
+
"""
|
|
353
|
+
df = self.preview(nrows=100)
|
|
354
|
+
return {col: str(dtype) for col, dtype in df.dtypes.items()}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2026 Precis Team
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
"""
|
|
17
|
+
@fileoverview 数据源加载器注册表模块
|
|
18
|
+
|
|
19
|
+
功能概述:
|
|
20
|
+
- 提供加载器的集中管理和发现机制
|
|
21
|
+
- 实现装饰器 register_loader 用于自动注册加载器类
|
|
22
|
+
- 支持按数据源类型获取加载器类或实例
|
|
23
|
+
- 提供加载器存在性检测功能
|
|
24
|
+
|
|
25
|
+
架构设计:
|
|
26
|
+
- 全局字典 LOADER_REGISTRY 存储类型到加载器类的映射
|
|
27
|
+
- 装饰器模式实现声明式注册
|
|
28
|
+
- 支持按 discriminator_value 精确匹配
|
|
29
|
+
- 与规格类的注册机制对称设计
|
|
30
|
+
|
|
31
|
+
输入示例:
|
|
32
|
+
>>> @register_loader("json")
|
|
33
|
+
... class JSONLoader(DataSourceLoader[JSONSourceSpec]):
|
|
34
|
+
... spec_class = JSONSourceSpec
|
|
35
|
+
... def load(self) -> pd.DataFrame:
|
|
36
|
+
... pass
|
|
37
|
+
|
|
38
|
+
输出示例:
|
|
39
|
+
>>> loader_class = get_loader_class_for_type("json")
|
|
40
|
+
>>> loader_instance = get_loader_for_spec(spec)
|
|
41
|
+
>>> supports_source_type("csv")
|
|
42
|
+
True
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
from __future__ import annotations
|
|
46
|
+
|
|
47
|
+
from collections.abc import Callable
|
|
48
|
+
from typing import TYPE_CHECKING
|
|
49
|
+
|
|
50
|
+
if TYPE_CHECKING:
|
|
51
|
+
from .base import DataSourceLoader
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
LOADER_REGISTRY: dict[str, type[DataSourceLoader]] = {}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def register_loader(spec_discriminator: str) -> Callable[[type[DataSourceLoader]], type[DataSourceLoader]]:
|
|
58
|
+
"""
|
|
59
|
+
@methoddesc 装饰器:注册加载器到全局注册表
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
spec_discriminator: 数据源类型标识符,应与 Spec 类的 discriminator_value 一致
|
|
63
|
+
|
|
64
|
+
示例:
|
|
65
|
+
>>> @register_loader("json")
|
|
66
|
+
... class JSONLoader(DataSourceLoader[JSONSpec]):
|
|
67
|
+
... pass
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def decorator(loader_class: type[DataSourceLoader]) -> type[DataSourceLoader]:
|
|
71
|
+
"""
|
|
72
|
+
@methoddesc 注册加载器类到全局注册表
|
|
73
|
+
|
|
74
|
+
参数:
|
|
75
|
+
loader_class: 要注册的加载器类,必须继承 DataSourceLoader
|
|
76
|
+
|
|
77
|
+
返回:
|
|
78
|
+
注册后的加载器类(便于装饰器链式使用)
|
|
79
|
+
|
|
80
|
+
异常:
|
|
81
|
+
ValueError: 如果该类型已注册则抛出
|
|
82
|
+
"""
|
|
83
|
+
if spec_discriminator in LOADER_REGISTRY:
|
|
84
|
+
raise ValueError(f"加载器 '{spec_discriminator}' 已注册")
|
|
85
|
+
LOADER_REGISTRY[spec_discriminator] = loader_class
|
|
86
|
+
return loader_class
|
|
87
|
+
|
|
88
|
+
return decorator
|