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,355 @@
|
|
|
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 LinesParser 策略类
|
|
18
|
+
|
|
19
|
+
功能概述:
|
|
20
|
+
- 实现 JSON Lines 格式的解析器策略
|
|
21
|
+
- 支持逐行解析 JSON Lines 格式数据
|
|
22
|
+
- 自动检测 JSON Lines 格式
|
|
23
|
+
|
|
24
|
+
架构设计:
|
|
25
|
+
- 实现 JSONParserStrategy Protocol 接口
|
|
26
|
+
- 可被 ParserStrategyRegistry 注册管理
|
|
27
|
+
- 独立的解析策略,便于扩展和维护
|
|
28
|
+
|
|
29
|
+
输入示例:
|
|
30
|
+
'{"a": 1}\n{"a": 2}\n{"b": "test"}\n'
|
|
31
|
+
|
|
32
|
+
输出示例:
|
|
33
|
+
[
|
|
34
|
+
{"a": 1},
|
|
35
|
+
{"a": 2},
|
|
36
|
+
{"b": "test"}
|
|
37
|
+
]
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
from __future__ import annotations
|
|
41
|
+
|
|
42
|
+
import json
|
|
43
|
+
from typing import Protocol, runtime_checkable
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@runtime_checkable
|
|
47
|
+
class JSONParserStrategy(Protocol):
|
|
48
|
+
"""
|
|
49
|
+
@classdesc JSON 解析策略协议
|
|
50
|
+
|
|
51
|
+
定义 JSON 解析器必须实现的接口。
|
|
52
|
+
|
|
53
|
+
接口要求:
|
|
54
|
+
- parse: 解析 JSON 内容
|
|
55
|
+
- can_parse: 检测是否能解析给定内容
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def parse(self, content: str) -> list[dict]:
|
|
59
|
+
"""
|
|
60
|
+
@methoddesc 解析 JSON 内容
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
content: JSON 内容字符串
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
解析后的字典列表
|
|
67
|
+
|
|
68
|
+
Raises:
|
|
69
|
+
JSONDecodeError: 解析失败时抛出
|
|
70
|
+
"""
|
|
71
|
+
...
|
|
72
|
+
|
|
73
|
+
def can_parse(self, content: str) -> bool:
|
|
74
|
+
"""
|
|
75
|
+
@methoddesc 检测是否能解析给定内容
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
content: 待检测的内容字符串
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
是否能解析
|
|
82
|
+
"""
|
|
83
|
+
...
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class LinesParser:
|
|
87
|
+
"""
|
|
88
|
+
@classdesc JSON Lines 格式解析器
|
|
89
|
+
|
|
90
|
+
============================================================================
|
|
91
|
+
功能说明
|
|
92
|
+
============================================================================
|
|
93
|
+
专门用于解析 JSON Lines 格式(又称 newline-delimited JSON)的策略类。
|
|
94
|
+
JSON Lines 格式中,每一行都是一个独立的、有效的 JSON 对象,以换行符分隔。
|
|
95
|
+
|
|
96
|
+
格式示例:
|
|
97
|
+
{"name": "Alice", "age": 30}
|
|
98
|
+
{"name": "Bob", "age": 25}
|
|
99
|
+
{"name": "Charlie", "age": 35}
|
|
100
|
+
|
|
101
|
+
============================================================================
|
|
102
|
+
业务场景
|
|
103
|
+
============================================================================
|
|
104
|
+
- 日志文件处理:每行一条日志记录
|
|
105
|
+
- 数据导入导出:适合大文件流式处理
|
|
106
|
+
- 配置文件:多行配置的便捷格式
|
|
107
|
+
- 增量数据同步:每次变更作为一行
|
|
108
|
+
|
|
109
|
+
============================================================================
|
|
110
|
+
使用示例
|
|
111
|
+
============================================================================
|
|
112
|
+
>>> parser = LinesParser()
|
|
113
|
+
>>> content = '{"a": 1}\\n{"a": 2}\\n{"a": 3}'
|
|
114
|
+
>>> result = parser.parse(content)
|
|
115
|
+
>>> print(result)
|
|
116
|
+
[{'a': 1}, {'a': 2}, {'a': 3}]
|
|
117
|
+
|
|
118
|
+
>>> # 检测格式
|
|
119
|
+
>>> parser.can_parse('{"key": "value"}') # True
|
|
120
|
+
>>> parser.can_parse('[1, 2, 3]') # False
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
def parse(self, content: str) -> list[dict]:
|
|
124
|
+
"""
|
|
125
|
+
@methoddesc 解析 JSON Lines 格式内容
|
|
126
|
+
|
|
127
|
+
============================================================================
|
|
128
|
+
处理逻辑
|
|
129
|
+
============================================================================
|
|
130
|
+
1. 按换行符分割内容
|
|
131
|
+
2. 跳过空行和纯空白行
|
|
132
|
+
3. 逐行解析为 JSON 对象
|
|
133
|
+
4. 返回字典列表
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
content: JSON Lines 格式内容
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
解析后的字典列表
|
|
140
|
+
|
|
141
|
+
Raises:
|
|
142
|
+
json.JSONDecodeError: 当某一行无法解析为有效 JSON 时抛出
|
|
143
|
+
错误信息包含行号便于定位问题
|
|
144
|
+
"""
|
|
145
|
+
if not content:
|
|
146
|
+
return []
|
|
147
|
+
|
|
148
|
+
records: list[dict] = []
|
|
149
|
+
lines = content.split("\n")
|
|
150
|
+
|
|
151
|
+
for line_num, line in enumerate(lines, start=1):
|
|
152
|
+
stripped_line = line.strip()
|
|
153
|
+
if not stripped_line:
|
|
154
|
+
continue
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
parsed = json.loads(stripped_line)
|
|
158
|
+
# 保留非 dict 条目并包装为 {"value": ...},避免静默丢弃数据(B19)
|
|
159
|
+
if isinstance(parsed, dict):
|
|
160
|
+
records.append(parsed)
|
|
161
|
+
else:
|
|
162
|
+
records.append({"value": parsed})
|
|
163
|
+
except json.JSONDecodeError as e:
|
|
164
|
+
raise json.JSONDecodeError(f"第 {line_num} 行 JSON 解析失败: {e.msg}", e.doc, e.pos) from e
|
|
165
|
+
|
|
166
|
+
return records
|
|
167
|
+
|
|
168
|
+
def can_parse(self, content: str) -> bool:
|
|
169
|
+
"""
|
|
170
|
+
@methoddesc 检测内容是否为 JSON Lines 格式
|
|
171
|
+
|
|
172
|
+
============================================================================
|
|
173
|
+
检测规则
|
|
174
|
+
============================================================================
|
|
175
|
+
JSON Lines 格式的特征:
|
|
176
|
+
1. 包含多行内容(至少一行非空行)
|
|
177
|
+
2. 每行(非空行)都是独立的有效 JSON 对象
|
|
178
|
+
3. 对象类型必须是字典(而非数组或原始类型)
|
|
179
|
+
|
|
180
|
+
检测策略:
|
|
181
|
+
- 采样前 N 行进行检测(默认 5 行)
|
|
182
|
+
- 如果采样行中非空行都是有效的 JSON 对象字典,则认为是 JSON Lines
|
|
183
|
+
- 单行 JSON 数组或普通 JSON 对象会被判定为非 JSON Lines
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
content: 待检测的内容字符串
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
是否为 JSON Lines 格式
|
|
190
|
+
"""
|
|
191
|
+
if not content:
|
|
192
|
+
return False
|
|
193
|
+
|
|
194
|
+
lines = content.split("\n")
|
|
195
|
+
non_empty_lines = [line.strip() for line in lines if line.strip()]
|
|
196
|
+
|
|
197
|
+
if not non_empty_lines:
|
|
198
|
+
return False
|
|
199
|
+
|
|
200
|
+
if len(non_empty_lines) == 1:
|
|
201
|
+
return False
|
|
202
|
+
|
|
203
|
+
sample_lines = non_empty_lines[:5]
|
|
204
|
+
|
|
205
|
+
for line in sample_lines:
|
|
206
|
+
try:
|
|
207
|
+
parsed = json.loads(line)
|
|
208
|
+
if not isinstance(parsed, dict):
|
|
209
|
+
return False
|
|
210
|
+
except (json.JSONDecodeError, ValueError):
|
|
211
|
+
return False
|
|
212
|
+
|
|
213
|
+
return True
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class StandardJSONParser:
|
|
217
|
+
"""
|
|
218
|
+
@classdesc 标准 JSON 格式解析器
|
|
219
|
+
|
|
220
|
+
============================================================================
|
|
221
|
+
功能说明
|
|
222
|
+
============================================================================
|
|
223
|
+
解析标准 JSON 格式(数组形式)的策略类。
|
|
224
|
+
适用于 JSON 数组格式:[{"a": 1}, {"a": 2}]
|
|
225
|
+
|
|
226
|
+
============================================================================
|
|
227
|
+
与 LinesParser 的区别
|
|
228
|
+
============================================================================
|
|
229
|
+
- LinesParser: 每行一个 JSON 对象,适合流式处理
|
|
230
|
+
- StandardJSONParser: 完整的 JSON 数组,一次性解析
|
|
231
|
+
"""
|
|
232
|
+
|
|
233
|
+
def parse(self, content: str) -> list[dict]:
|
|
234
|
+
"""
|
|
235
|
+
@methoddesc 解析标准 JSON 数组格式
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
content: JSON 数组字符串
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
解析后的字典列表
|
|
242
|
+
|
|
243
|
+
Raises:
|
|
244
|
+
json.JSONDecodeError: 解析失败时抛出
|
|
245
|
+
"""
|
|
246
|
+
if not content:
|
|
247
|
+
return []
|
|
248
|
+
|
|
249
|
+
data = json.loads(content)
|
|
250
|
+
|
|
251
|
+
if isinstance(data, list):
|
|
252
|
+
return [item for item in data if isinstance(item, dict)]
|
|
253
|
+
elif isinstance(data, dict):
|
|
254
|
+
return [data]
|
|
255
|
+
else:
|
|
256
|
+
raise json.JSONDecodeError(f"不支持的 JSON 根类型: {type(data).__name__}", content, 0)
|
|
257
|
+
|
|
258
|
+
def can_parse(self, content: str) -> bool:
|
|
259
|
+
"""
|
|
260
|
+
@methoddesc 检测是否为标准 JSON 格式
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
content: 待检测的内容
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
是否为标准 JSON 格式
|
|
267
|
+
"""
|
|
268
|
+
if not content:
|
|
269
|
+
return False
|
|
270
|
+
|
|
271
|
+
content = content.strip()
|
|
272
|
+
|
|
273
|
+
if not content:
|
|
274
|
+
return False
|
|
275
|
+
|
|
276
|
+
if content.startswith("[") and content.endswith("]"):
|
|
277
|
+
return True
|
|
278
|
+
|
|
279
|
+
try:
|
|
280
|
+
parsed = json.loads(content)
|
|
281
|
+
return isinstance(parsed, dict) and len(content.split("\n")) <= 1
|
|
282
|
+
except (json.JSONDecodeError, ValueError):
|
|
283
|
+
return False
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
class ParserStrategyRegistry:
|
|
287
|
+
"""
|
|
288
|
+
@classdesc 解析器策略注册表
|
|
289
|
+
|
|
290
|
+
============================================================================
|
|
291
|
+
功能说明
|
|
292
|
+
============================================================================
|
|
293
|
+
管理和注册各种 JSON 解析策略,支持自动选择最优解析器。
|
|
294
|
+
按优先级顺序尝试各策略,返回第一个能成功解析的策略结果。
|
|
295
|
+
|
|
296
|
+
============================================================================
|
|
297
|
+
使用示例
|
|
298
|
+
============================================================================
|
|
299
|
+
>>> registry = ParserStrategyRegistry()
|
|
300
|
+
>>> registry.register(LinesParser(), priority=10)
|
|
301
|
+
>>> registry.register(StandardJSONParser(), priority=5)
|
|
302
|
+
|
|
303
|
+
>>> content = '{"a": 1}\\n{"a": 2}'
|
|
304
|
+
>>> parser = registry.get_parser(content)
|
|
305
|
+
>>> result = parser.parse(content)
|
|
306
|
+
"""
|
|
307
|
+
|
|
308
|
+
def __init__(self) -> None:
|
|
309
|
+
"""
|
|
310
|
+
@methoddesc 初始化解析策略注册表
|
|
311
|
+
|
|
312
|
+
创建空的策略列表,后续通过 register() 方法注册解析策略。
|
|
313
|
+
"""
|
|
314
|
+
self._strategies: list[tuple[int, JSONParserStrategy]] = []
|
|
315
|
+
|
|
316
|
+
def register(self, strategy: JSONParserStrategy, priority: int = 0) -> None:
|
|
317
|
+
"""
|
|
318
|
+
@methoddesc 注册解析策略
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
strategy: 解析策略实例
|
|
322
|
+
priority: 优先级,数值越大优先级越高
|
|
323
|
+
"""
|
|
324
|
+
self._strategies.append((priority, strategy))
|
|
325
|
+
self._strategies.sort(key=lambda x: x[0], reverse=True)
|
|
326
|
+
|
|
327
|
+
def get_parser(self, content: str) -> JSONParserStrategy | None:
|
|
328
|
+
"""
|
|
329
|
+
@methoddesc 获取适合内容的解析器
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
content: 待解析的内容
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
匹配的解析器,未找到返回 None
|
|
336
|
+
"""
|
|
337
|
+
for _, strategy in self._strategies:
|
|
338
|
+
if strategy.can_parse(content):
|
|
339
|
+
return strategy
|
|
340
|
+
return None
|
|
341
|
+
|
|
342
|
+
def parse(self, content: str) -> list[dict] | None:
|
|
343
|
+
"""
|
|
344
|
+
@methoddesc 自动解析内容
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
content: 待解析的内容
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
解析结果,解析失败返回 None
|
|
351
|
+
"""
|
|
352
|
+
parser = self.get_parser(content)
|
|
353
|
+
if parser:
|
|
354
|
+
return parser.parse(content)
|
|
355
|
+
return None
|
|
@@ -0,0 +1,278 @@
|
|
|
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 ObjectParser 策略类
|
|
18
|
+
|
|
19
|
+
功能概述:
|
|
20
|
+
- 实现嵌套对象格式的 JSON 解析器策略
|
|
21
|
+
- 支持递归查找最深层的数组作为数据源
|
|
22
|
+
- 处理没有数组时的单条记录转换
|
|
23
|
+
|
|
24
|
+
架构设计:
|
|
25
|
+
- 实现 JSONParserStrategy Protocol 接口
|
|
26
|
+
- 可被 ParserStrategyRegistry 注册管理
|
|
27
|
+
- 独立的解析策略,便于扩展和维护
|
|
28
|
+
|
|
29
|
+
输入示例:
|
|
30
|
+
'{"data": [{"a": 1}, {"a": 2}]}'
|
|
31
|
+
'{"users": {"items": [{"id": 1}, {"id": 2}]}}'
|
|
32
|
+
'{"name": "test"}'
|
|
33
|
+
|
|
34
|
+
输出示例:
|
|
35
|
+
[{"a": 1}, {"a": 2}]
|
|
36
|
+
[{"id": 1}, {"id": 2}]
|
|
37
|
+
[{"name": "test"}]
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
from __future__ import annotations
|
|
41
|
+
|
|
42
|
+
import json
|
|
43
|
+
from typing import Any, Protocol, runtime_checkable
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@runtime_checkable
|
|
47
|
+
class JSONParserStrategy(Protocol):
|
|
48
|
+
"""
|
|
49
|
+
@classdesc JSON 解析策略协议
|
|
50
|
+
|
|
51
|
+
定义 JSON 解析器必须实现的接口。
|
|
52
|
+
|
|
53
|
+
接口要求:
|
|
54
|
+
- parse: 解析 JSON 内容
|
|
55
|
+
- can_parse: 检测是否能解析给定内容
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def parse(self, content: str) -> list[dict]:
|
|
59
|
+
"""
|
|
60
|
+
@methoddesc 解析 JSON 内容
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
content: JSON 内容字符串
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
解析后的字典列表
|
|
67
|
+
|
|
68
|
+
Raises:
|
|
69
|
+
JSONDecodeError: 解析失败时抛出
|
|
70
|
+
"""
|
|
71
|
+
...
|
|
72
|
+
|
|
73
|
+
def can_parse(self, content: str) -> bool:
|
|
74
|
+
"""
|
|
75
|
+
@methoddesc 检测是否能解析给定内容
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
content: 待检测的内容字符串
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
是否能解析
|
|
82
|
+
"""
|
|
83
|
+
...
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class ObjectParser:
|
|
87
|
+
"""
|
|
88
|
+
@classdesc 嵌套对象格式解析器
|
|
89
|
+
|
|
90
|
+
============================================================================
|
|
91
|
+
功能说明
|
|
92
|
+
============================================================================
|
|
93
|
+
专门用于解析嵌套对象格式 JSON 的策略类。
|
|
94
|
+
能够自动查找嵌套结构中最深层的数组作为数据源。
|
|
95
|
+
如果没有找到数组,则将字典转换为单条记录。
|
|
96
|
+
|
|
97
|
+
格式示例:
|
|
98
|
+
- 单层嵌套: {"data": [{"a": 1}, {"a": 2}]}
|
|
99
|
+
- 多层嵌套: {"users": {"items": [{"id": 1}, {"id": 2}]}}
|
|
100
|
+
- 深度嵌套: {"result": {"data": {"records": [{"x": 1}]}}}
|
|
101
|
+
- 单对象: {"name": "test", "value": 123}
|
|
102
|
+
|
|
103
|
+
============================================================================
|
|
104
|
+
业务场景
|
|
105
|
+
============================================================================
|
|
106
|
+
- API 响应数据:大多数 REST API 返回嵌套的 JSON 对象
|
|
107
|
+
- 配置文件:包含嵌套结构的配置文件
|
|
108
|
+
- 导出数据:某些工具导出的嵌套格式数据
|
|
109
|
+
|
|
110
|
+
============================================================================
|
|
111
|
+
使用示例
|
|
112
|
+
============================================================================
|
|
113
|
+
>>> parser = ObjectParser()
|
|
114
|
+
>>> content = '{"data": [{"a": 1}, {"a": 2}]}'
|
|
115
|
+
>>> result = parser.parse(content)
|
|
116
|
+
>>> print(result)
|
|
117
|
+
[{'a': 1}, {'a': 2}]
|
|
118
|
+
|
|
119
|
+
>>> # 单对象转换为单条记录
|
|
120
|
+
>>> content = '{"name": "test"}'
|
|
121
|
+
>>> result = parser.parse(content)
|
|
122
|
+
>>> print(result)
|
|
123
|
+
[{'name': 'test'}]
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
def parse(self, content: str) -> list[dict]:
|
|
127
|
+
"""
|
|
128
|
+
@methoddesc 解析嵌套对象格式的 JSON 内容
|
|
129
|
+
|
|
130
|
+
============================================================================
|
|
131
|
+
处理逻辑
|
|
132
|
+
============================================================================
|
|
133
|
+
1. 解析 JSON 字符串为 Python 对象
|
|
134
|
+
2. 递归查找最深层的数组
|
|
135
|
+
3. 如果找到数组,返回数组内容(每个元素转换为字典)
|
|
136
|
+
4. 如果没有找到数组,将根对象包装为单元素列表
|
|
137
|
+
5. 使用 pandas.json_normalize 扁平化嵌套字段
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
content: 嵌套对象格式的 JSON 内容
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
解析后的字典列表
|
|
144
|
+
|
|
145
|
+
Raises:
|
|
146
|
+
json.JSONDecodeError: JSON 格式错误时抛出
|
|
147
|
+
ValueError: 内容不符合策略解析条件时抛出
|
|
148
|
+
"""
|
|
149
|
+
if not content or not content.strip():
|
|
150
|
+
return []
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
data = json.loads(content)
|
|
154
|
+
except json.JSONDecodeError as e:
|
|
155
|
+
raise json.JSONDecodeError(f"JSON 解析失败: {e.msg}", e.doc, e.pos) from e
|
|
156
|
+
|
|
157
|
+
if isinstance(data, list):
|
|
158
|
+
return [item if isinstance(item, dict) else {"value": item} for item in data]
|
|
159
|
+
|
|
160
|
+
if isinstance(data, dict):
|
|
161
|
+
arrays = self._find_deepest_arrays(data)
|
|
162
|
+
if arrays:
|
|
163
|
+
return [item if isinstance(item, dict) else {"value": item} for item in arrays]
|
|
164
|
+
return [data]
|
|
165
|
+
|
|
166
|
+
return [{"value": data}]
|
|
167
|
+
|
|
168
|
+
def can_parse(self, content: str) -> bool:
|
|
169
|
+
"""
|
|
170
|
+
@methoddesc 检测内容是否为嵌套对象格式
|
|
171
|
+
|
|
172
|
+
============================================================================
|
|
173
|
+
检测规则
|
|
174
|
+
============================================================================
|
|
175
|
+
嵌套对象格式的特征:
|
|
176
|
+
1. 以花括号开头(表示 JSON 对象)
|
|
177
|
+
2. 包含嵌套结构(至少一层嵌套)
|
|
178
|
+
3. 可能包含数组作为某个属性的值
|
|
179
|
+
4. 不是以数组直接开头的格式
|
|
180
|
+
|
|
181
|
+
检测策略:
|
|
182
|
+
- 首先尝试 JSON 解析
|
|
183
|
+
- 解析成功且结果为字典(非数组)时返回 True
|
|
184
|
+
- 如果以数组开头或为空则返回 False
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
content: 待检测的内容字符串
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
是否为嵌套对象格式
|
|
191
|
+
"""
|
|
192
|
+
if not content:
|
|
193
|
+
return False
|
|
194
|
+
|
|
195
|
+
content = content.strip()
|
|
196
|
+
|
|
197
|
+
if content.startswith("["):
|
|
198
|
+
return False
|
|
199
|
+
|
|
200
|
+
if not content.startswith("{"):
|
|
201
|
+
return False
|
|
202
|
+
|
|
203
|
+
try:
|
|
204
|
+
data = json.loads(content)
|
|
205
|
+
return isinstance(data, dict)
|
|
206
|
+
except (json.JSONDecodeError, ValueError):
|
|
207
|
+
return False
|
|
208
|
+
|
|
209
|
+
def _find_deepest_arrays(self, obj: Any, current_depth: int = 0) -> list[Any]:
|
|
210
|
+
"""
|
|
211
|
+
@methoddesc 递归查找最深层的数组
|
|
212
|
+
|
|
213
|
+
============================================================================
|
|
214
|
+
算法说明
|
|
215
|
+
============================================================================
|
|
216
|
+
使用深度优先搜索(DFS)遍历对象结构:
|
|
217
|
+
1. 如果当前对象是数组,记录当前位置并继续深入查找
|
|
218
|
+
2. 如果数组元素是字典,继续在数组元素中查找更深的数组
|
|
219
|
+
3. 返回最深层的数组作为数据源
|
|
220
|
+
|
|
221
|
+
特殊情况处理:
|
|
222
|
+
- 空数组:忽略,继续查找
|
|
223
|
+
- 非字典数组元素:保留,由上层调用方统一包装
|
|
224
|
+
- 混合类型数组:整体返回,由上层调用方统一包装
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
obj: 当前检查的对象
|
|
228
|
+
current_depth: 当前递归深度
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
如果找到最深层的数组,返回数组内容;否则返回空列表
|
|
232
|
+
"""
|
|
233
|
+
if isinstance(obj, list):
|
|
234
|
+
if not obj:
|
|
235
|
+
return []
|
|
236
|
+
|
|
237
|
+
result = []
|
|
238
|
+
for item in obj:
|
|
239
|
+
if isinstance(item, dict):
|
|
240
|
+
nested = self._find_deepest_arrays(item, current_depth + 1)
|
|
241
|
+
if nested:
|
|
242
|
+
return nested
|
|
243
|
+
result.append(item)
|
|
244
|
+
|
|
245
|
+
return result if result else obj[:1] if obj else []
|
|
246
|
+
|
|
247
|
+
if isinstance(obj, dict):
|
|
248
|
+
arrays_found: list[tuple[int, list[Any]]] = []
|
|
249
|
+
|
|
250
|
+
for key, value in obj.items():
|
|
251
|
+
if isinstance(value, list):
|
|
252
|
+
if not value:
|
|
253
|
+
continue
|
|
254
|
+
|
|
255
|
+
# 优先查找列表元素中是否包含更深的数组
|
|
256
|
+
deeper_arrays: list[list[Any]] = []
|
|
257
|
+
for item in value:
|
|
258
|
+
if isinstance(item, dict):
|
|
259
|
+
nested = self._find_deepest_arrays(item, current_depth + 1)
|
|
260
|
+
if nested:
|
|
261
|
+
deeper_arrays.append(nested)
|
|
262
|
+
|
|
263
|
+
if deeper_arrays:
|
|
264
|
+
for nested in deeper_arrays:
|
|
265
|
+
arrays_found.append((current_depth + 2, nested))
|
|
266
|
+
else:
|
|
267
|
+
# 当前列表本身即是最深层数组,整体返回
|
|
268
|
+
arrays_found.append((current_depth + 1, list(value)))
|
|
269
|
+
|
|
270
|
+
elif isinstance(value, dict):
|
|
271
|
+
nested = self._find_deepest_arrays(value, current_depth + 1)
|
|
272
|
+
if nested:
|
|
273
|
+
arrays_found.append((current_depth + 1, nested))
|
|
274
|
+
|
|
275
|
+
if arrays_found:
|
|
276
|
+
return max(arrays_found, key=lambda x: x[0])[1]
|
|
277
|
+
|
|
278
|
+
return []
|
|
@@ -0,0 +1,50 @@
|
|
|
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 数据源 Schema 信息 DTO
|
|
18
|
+
|
|
19
|
+
功能概述:
|
|
20
|
+
- 为 core 层数据加载器提供与 domain 解耦的最小 Schema 信息
|
|
21
|
+
- 仅包含加载数据文件所需的字段,避免 core.data_source.loader 直接依赖 domain.TableSchema
|
|
22
|
+
|
|
23
|
+
架构设计:
|
|
24
|
+
- 纯数据 DTO: 使用 dataclass,只读语义,无业务逻辑
|
|
25
|
+
- 上层服务负责将 domain.TableSchema 或 core.TableSchemaFile 转换为 DataSourceInfo
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
from dataclasses import dataclass, field
|
|
31
|
+
from typing import Any
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class DataSourceInfo:
|
|
36
|
+
"""@classdesc 数据加载器使用的 Schema 信息 DTO
|
|
37
|
+
|
|
38
|
+
字段说明:
|
|
39
|
+
- schema_id: 表/Schema 的唯一标识,作为加载结果字典的键
|
|
40
|
+
- name: 表名(可选,用于日志展示)
|
|
41
|
+
- sheet_name: Excel 工作表名(可选)
|
|
42
|
+
- header_row: 表头行索引,默认 0
|
|
43
|
+
- source_config: 加载器相关配置(如 delimiter、encoding、format 等)
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
schema_id: str
|
|
47
|
+
name: str | None = None
|
|
48
|
+
sheet_name: str | None = None
|
|
49
|
+
header_row: int = 0
|
|
50
|
+
source_config: dict[str, Any] = field(default_factory=dict)
|