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,138 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2026 Precis Team
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
"""@fileoverview 文件路径安全校验
|
|
17
|
+
|
|
18
|
+
功能概述:
|
|
19
|
+
- 提供统一的文件路径安全校验工具,供所有涉及文件系统操作的 HTTP 端点使用
|
|
20
|
+
- 两层校验策略:
|
|
21
|
+
1. assert_no_traversal: 反穿越校验(拒绝 `..` 相对成分 + resolve 解析 symlink),
|
|
22
|
+
适用于用户在项目外选择的数据文件场景(preview/validation/files-ops),保留任意数据文件能力
|
|
23
|
+
2. assert_path_within_root: 限定到指定根目录(白名单语义),
|
|
24
|
+
适用于临时文件(TEMP_DIR)、校验历史(configPath)等有明确归属的场景
|
|
25
|
+
|
|
26
|
+
设计原则:
|
|
27
|
+
- 只做必要的防御:拒绝 `..` 穿越和 resolve 符号链接。不做系统目录黑名单之类的
|
|
28
|
+
额外层——应用层重复 OS 权限边界既不完整(挡不住 ~/.ssh)又增加误伤与维护负担。
|
|
29
|
+
- 校验失败统一抛 HTTPException(403 拒绝 / 400 参数错误 / 404 不存在)
|
|
30
|
+
- 基于 Path.resolve() 解析符号链接与 `..`,避免 abspath+normpath 无法防 symlink 的缺陷
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import os
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
|
|
38
|
+
from fastapi import HTTPException
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def assert_no_traversal(file_path: str, *, must_exist: bool = True) -> str:
|
|
42
|
+
"""
|
|
43
|
+
@methoddesc 反穿越校验,不限根目录
|
|
44
|
+
|
|
45
|
+
适用于用户在项目外选择的数据文件场景(preview/validation/files-ops)。
|
|
46
|
+
保留访问任意合法数据文件的能力,仅阻止路径穿越(`..` 相对成分)。
|
|
47
|
+
|
|
48
|
+
参数:
|
|
49
|
+
file_path: 待校验的文件路径(原始字符串)
|
|
50
|
+
must_exist: 是否要求文件必须存在(默认 True)
|
|
51
|
+
|
|
52
|
+
返回:
|
|
53
|
+
解析后的绝对路径字符串
|
|
54
|
+
|
|
55
|
+
抛出:
|
|
56
|
+
HTTPException: 400 路径为空/含穿越成分; 404 文件不存在
|
|
57
|
+
"""
|
|
58
|
+
if not file_path or not file_path.strip():
|
|
59
|
+
raise HTTPException(status_code=400, detail="文件路径不能为空")
|
|
60
|
+
|
|
61
|
+
raw = file_path.strip()
|
|
62
|
+
|
|
63
|
+
# 拒绝显式 `..` 段:数据文件访问场景不应需要 `..`,用户应直接提供目标文件绝对路径
|
|
64
|
+
path_obj = Path(raw)
|
|
65
|
+
if any(part == ".." for part in path_obj.parts):
|
|
66
|
+
raise HTTPException(
|
|
67
|
+
status_code=400,
|
|
68
|
+
detail="路径包含 `..` 相对穿越成分,请提供目标文件的绝对路径。",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# resolve() 解析符号链接与剩余相对成分,得到真实绝对路径
|
|
72
|
+
try:
|
|
73
|
+
resolved = path_obj.resolve()
|
|
74
|
+
except OSError as e:
|
|
75
|
+
raise HTTPException(status_code=400, detail=f"路径解析失败: {e}")
|
|
76
|
+
|
|
77
|
+
if must_exist and not resolved.exists():
|
|
78
|
+
raise HTTPException(status_code=404, detail=f"文件未找到: {file_path}")
|
|
79
|
+
|
|
80
|
+
return str(resolved)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def assert_path_within_root(file_path: str, root: str, *, must_exist: bool = True) -> str:
|
|
84
|
+
"""
|
|
85
|
+
@methoddesc 限定路径必须落在指定根目录内(白名单语义)
|
|
86
|
+
|
|
87
|
+
适用于临时文件(TEMP_DIR)、校验历史(configPath)等有明确归属的场景。
|
|
88
|
+
基于 resolve() 解析后做包含关系检查,能正确处理符号链接。
|
|
89
|
+
|
|
90
|
+
参数:
|
|
91
|
+
file_path: 待校验的文件路径(原始字符串)
|
|
92
|
+
root: 允许的根目录绝对路径
|
|
93
|
+
must_exist: 是否要求文件必须存在(默认 True)
|
|
94
|
+
|
|
95
|
+
返回:
|
|
96
|
+
解析后的绝对路径字符串
|
|
97
|
+
|
|
98
|
+
抛出:
|
|
99
|
+
HTTPException: 400 路径为空; 403 越出根目录; 404 不存在
|
|
100
|
+
"""
|
|
101
|
+
if not file_path or not file_path.strip():
|
|
102
|
+
raise HTTPException(status_code=400, detail="文件路径不能为空")
|
|
103
|
+
|
|
104
|
+
raw = file_path.strip()
|
|
105
|
+
try:
|
|
106
|
+
resolved = Path(raw).resolve()
|
|
107
|
+
except OSError as e:
|
|
108
|
+
raise HTTPException(status_code=400, detail=f"路径解析失败: {e}")
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
root_resolved = Path(root).resolve()
|
|
112
|
+
except OSError as e:
|
|
113
|
+
raise HTTPException(status_code=500, detail=f"根目录解析失败: {e}")
|
|
114
|
+
|
|
115
|
+
root_str = str(root_resolved)
|
|
116
|
+
resolved_str = str(resolved)
|
|
117
|
+
# 包含关系:resolved == root 或 resolved 在 root 之下
|
|
118
|
+
if resolved_str != root_str and not resolved_str.startswith(root_str + os.sep):
|
|
119
|
+
raise HTTPException(status_code=403, detail="路径越出允许的根目录范围。")
|
|
120
|
+
|
|
121
|
+
if must_exist and not resolved.exists():
|
|
122
|
+
raise HTTPException(status_code=404, detail=f"文件未找到: {file_path}")
|
|
123
|
+
|
|
124
|
+
return resolved_str
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def validate_file_access(file_path: str) -> str:
|
|
128
|
+
"""
|
|
129
|
+
@methoddesc 文件路径可访问性校验(反穿越硬化版)
|
|
130
|
+
|
|
131
|
+
兼容历史调用点(preview/content_mode、path_mode、validation 等),
|
|
132
|
+
内部委托给 assert_no_traversal。返回解析后的绝对路径供调用方使用。
|
|
133
|
+
|
|
134
|
+
抛出:
|
|
135
|
+
HTTPException: 400 路径为空/含 `..` 穿越成分/解析失败; 404 文件不存在
|
|
136
|
+
(本函数不做根目录限定,不会抛 403;403 仅由 assert_path_within_root 抛出)
|
|
137
|
+
"""
|
|
138
|
+
return assert_no_traversal(file_path, must_exist=True)
|
|
@@ -0,0 +1,57 @@
|
|
|
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
|
+
- 负责将 core 层加载的配置文件对象转换为 domain 层运行时数据集 Schema
|
|
21
|
+
- 作为 core 层与 domain 层之间的转换桥梁,避免 core 层反向依赖 domain
|
|
22
|
+
|
|
23
|
+
架构设计:
|
|
24
|
+
- 转换层: core (TableSchemaFile / ConstraintFile) -> domain (DataSetSchema)
|
|
25
|
+
- 由 core.project.loader 的主加载流程调用
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
from typing import Any
|
|
31
|
+
|
|
32
|
+
from app.shared.core.project.constraint.factory import create_constraints
|
|
33
|
+
from app.shared.core.project.schema.types import TableSchemaFile
|
|
34
|
+
from app.shared.domain import DataSetSchema
|
|
35
|
+
|
|
36
|
+
from .schema_runtime_builder import build_runtime_schemas
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def build_dataset_schema(
|
|
40
|
+
schema_files: dict[str, TableSchemaFile],
|
|
41
|
+
constraint_files: dict[str, Any],
|
|
42
|
+
registries: dict[str, Any],
|
|
43
|
+
) -> tuple[DataSetSchema, list[str]]:
|
|
44
|
+
"""@methoddesc 构建 domain 层的 DataSetSchema 运行时对象。
|
|
45
|
+
|
|
46
|
+
将 core 层加载的 TableSchemaFile 和 ConstraintFile 转换为 domain 层的
|
|
47
|
+
TableSchema 列表和约束实例,最终组装为 DataSetSchema。
|
|
48
|
+
|
|
49
|
+
:param schema_files: schema 配置文件字典(table_id -> TableSchemaFile)
|
|
50
|
+
:param constraint_files: constraint 配置文件字典(constraint_id -> ConstraintFile)
|
|
51
|
+
:param registries: 表达式注册表等辅助对象
|
|
52
|
+
:return: (DataSetSchema, 约束构建过程中的警告列表)
|
|
53
|
+
"""
|
|
54
|
+
runtime_tables = build_runtime_schemas(schema_files, registries)
|
|
55
|
+
runtime_constraints, warnings = create_constraints(constraint_files, schema_files)
|
|
56
|
+
dataset_schema = DataSetSchema(tables=runtime_tables, constraints=runtime_constraints)
|
|
57
|
+
return dataset_schema, warnings
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2026 Precis Team
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
"""@fileoverview 数据文件 → Schema YAML 推断服务(headless infer_schema)
|
|
17
|
+
|
|
18
|
+
功能概述:
|
|
19
|
+
- 读取 CSV/Excel/JSON 数据文件的头部样本,推断每列的 V2 数据类型
|
|
20
|
+
- 产出可直接落盘的 schema 文件内容(dict 形式,YAML 序列化由调用方决定)
|
|
21
|
+
- 供 CLI 子命令 `precis infer-schema` 与 MCP tool `infer_schema` 复用
|
|
22
|
+
|
|
23
|
+
推断规则(按列,忽略空值):
|
|
24
|
+
- 全部可解析为整数 → integer
|
|
25
|
+
- 全部可解析为有限浮点(含整数)→ float
|
|
26
|
+
- 全部为 true/false 字面量 → boolean
|
|
27
|
+
- 全部为 ISO 日期(YYYY-MM-DD)→ date
|
|
28
|
+
- 其余(含混合类型、全空列)→ string
|
|
29
|
+
|
|
30
|
+
设计说明:
|
|
31
|
+
- CSV/Excel 以 dtype=str 读入,避免 pandas 预转换掩盖原始格式
|
|
32
|
+
("1.0" 与 "1" 的区别、日期字符串等);JSON 值为原生类型,
|
|
33
|
+
分类器同时接受 str 与原生标量
|
|
34
|
+
- 只采样头部(默认 1000 行)控制大文件成本;类型冲突按保守回退 string
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
import json
|
|
40
|
+
import math
|
|
41
|
+
import uuid
|
|
42
|
+
from datetime import date
|
|
43
|
+
from pathlib import Path
|
|
44
|
+
from typing import Any
|
|
45
|
+
|
|
46
|
+
import pandas as pd
|
|
47
|
+
|
|
48
|
+
# V2 支持的 6 种数据类型(与 backend schema 类型定义一致)
|
|
49
|
+
DATA_TYPES = ("string", "integer", "float", "decimal", "boolean", "date")
|
|
50
|
+
|
|
51
|
+
# 支持的输入扩展名
|
|
52
|
+
_CSV_EXTS = {".csv"}
|
|
53
|
+
_EXCEL_EXTS = {".xlsx", ".xls"}
|
|
54
|
+
_JSON_EXTS = {".json", ".jsonl"}
|
|
55
|
+
|
|
56
|
+
# boolean 字面量集合(大小写不敏感)
|
|
57
|
+
_BOOL_LITERALS = {"true", "false"}
|
|
58
|
+
|
|
59
|
+
# 默认采样行数
|
|
60
|
+
DEFAULT_SAMPLE_ROWS = 1000
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _classify_value(value: Any) -> str | None:
|
|
64
|
+
"""对单个非空值做候选类型判定。
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
value: 单元格值(str 或 JSON 原生标量)
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
候选类型(string/integer/float/boolean/date);无法判定返回 string
|
|
71
|
+
"""
|
|
72
|
+
if isinstance(value, bool):
|
|
73
|
+
return "boolean"
|
|
74
|
+
if isinstance(value, int):
|
|
75
|
+
return "integer"
|
|
76
|
+
if isinstance(value, float):
|
|
77
|
+
# 非有限浮点(NaN/Inf)不能作为类型证据
|
|
78
|
+
return "float" if math.isfinite(value) else "string"
|
|
79
|
+
|
|
80
|
+
text = str(value).strip()
|
|
81
|
+
if not text:
|
|
82
|
+
return None
|
|
83
|
+
if text.lower() in _BOOL_LITERALS:
|
|
84
|
+
return "boolean"
|
|
85
|
+
# 整数:可选符号的纯数字
|
|
86
|
+
if _is_int_text(text):
|
|
87
|
+
return "integer"
|
|
88
|
+
# 浮点:排除 nan/inf 等非有限字面量
|
|
89
|
+
try:
|
|
90
|
+
parsed = float(text)
|
|
91
|
+
if math.isfinite(parsed):
|
|
92
|
+
return "float"
|
|
93
|
+
except ValueError:
|
|
94
|
+
pass
|
|
95
|
+
# 日期:严格 ISO 格式(YYYY-MM-DD)
|
|
96
|
+
try:
|
|
97
|
+
date.fromisoformat(text)
|
|
98
|
+
return "date"
|
|
99
|
+
except ValueError:
|
|
100
|
+
pass
|
|
101
|
+
return "string"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _is_int_text(text: str) -> bool:
|
|
105
|
+
"""判断字符串是否为整数文本(允许前后符号)。"""
|
|
106
|
+
body = text[1:] if text[:1] in "+-" else text
|
|
107
|
+
return body.isdigit() and len(body) > 0
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def infer_column_type(values: list[Any], dominant_ratio: float = 0.9) -> str:
|
|
111
|
+
"""对一列采样值推断 V2 类型。
|
|
112
|
+
|
|
113
|
+
规则:
|
|
114
|
+
1. 全部非空值同属一类 → 该类型(integer/float 合并时 float 优先提升)
|
|
115
|
+
2. 否则若单一数值/日期/布尔候选占比 >= dominant_ratio → 采信该候选
|
|
116
|
+
(数据中的少量脏值正是校验要抓的问题,不应把整列拖回 string)
|
|
117
|
+
3. 其余(含混合类型、全空列)→ string
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
values: 该列的非空采样值列表
|
|
121
|
+
dominant_ratio: 主导类型采信阈值(占非空值的比例)
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
6 种 V2 类型之一;空列表(全空列)返回 string(安全默认)
|
|
125
|
+
"""
|
|
126
|
+
counts: dict[str, int] = {}
|
|
127
|
+
total = 0
|
|
128
|
+
for value in values:
|
|
129
|
+
candidate = _classify_value(value)
|
|
130
|
+
if candidate is None:
|
|
131
|
+
continue
|
|
132
|
+
total += 1
|
|
133
|
+
counts[candidate] = counts.get(candidate, 0) + 1
|
|
134
|
+
if total == 0:
|
|
135
|
+
return "string"
|
|
136
|
+
|
|
137
|
+
def _ratio(kind: str) -> float:
|
|
138
|
+
return counts.get(kind, 0) / total
|
|
139
|
+
|
|
140
|
+
# 数值合并视角:integer 与 float 同属数值,浮点出现则整列提升 float
|
|
141
|
+
numeric_count = counts.get("integer", 0) + counts.get("float", 0)
|
|
142
|
+
if numeric_count == total:
|
|
143
|
+
return "float" if counts.get("float") else "integer"
|
|
144
|
+
if _ratio("boolean") == 1.0:
|
|
145
|
+
return "boolean"
|
|
146
|
+
if _ratio("date") == 1.0:
|
|
147
|
+
return "date"
|
|
148
|
+
|
|
149
|
+
# 主导采信:少量脏值不拖垮整列(如 17 行里 1 个非法日期)
|
|
150
|
+
if numeric_count >= dominant_ratio * total:
|
|
151
|
+
return "float" if counts.get("float") else "integer"
|
|
152
|
+
for kind in ("boolean", "date"):
|
|
153
|
+
if _ratio(kind) >= dominant_ratio:
|
|
154
|
+
return kind
|
|
155
|
+
return "string"
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _read_head(data_file: Path, sample_rows: int) -> pd.DataFrame:
|
|
159
|
+
"""按扩展名读取数据文件头部样本。
|
|
160
|
+
|
|
161
|
+
CSV/Excel 以 dtype=str 读入保留原始格式;JSON 解析原生类型。
|
|
162
|
+
JSONL(.jsonl / 逐行 JSON 对象)按行解析。
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
data_file: 数据文件路径
|
|
166
|
+
sample_rows: 采样行数上限
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
采样 DataFrame(空数据返回空 DataFrame)
|
|
170
|
+
|
|
171
|
+
Raises:
|
|
172
|
+
ValueError: 文件不存在或不支持的扩展名
|
|
173
|
+
"""
|
|
174
|
+
if not data_file.exists():
|
|
175
|
+
raise ValueError(f"数据文件不存在: {data_file}")
|
|
176
|
+
|
|
177
|
+
suffix = data_file.suffix.lower()
|
|
178
|
+
try:
|
|
179
|
+
if suffix in _CSV_EXTS:
|
|
180
|
+
return pd.read_csv(data_file, dtype=str, nrows=sample_rows, keep_default_na=True)
|
|
181
|
+
if suffix in _EXCEL_EXTS:
|
|
182
|
+
# Excel 不支持按行采样,整体读入后取头部(大文件场景后续再优化)
|
|
183
|
+
df = pd.read_excel(data_file, dtype=str)
|
|
184
|
+
return df.head(sample_rows)
|
|
185
|
+
if suffix in _JSON_EXTS:
|
|
186
|
+
return _read_json_head(data_file, sample_rows)
|
|
187
|
+
except pd.errors.EmptyDataError as e:
|
|
188
|
+
raise ValueError(f"数据文件无表头或无数据行,无法推断 schema: {data_file}") from e
|
|
189
|
+
raise ValueError(f"不支持的文件扩展名: {suffix}(支持 CSV/Excel/JSON)")
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _read_json_head(data_file: Path, sample_rows: int) -> pd.DataFrame:
|
|
193
|
+
"""读取 JSON(对象数组)或 JSONL(逐行对象)头部样本。"""
|
|
194
|
+
text = data_file.read_text(encoding="utf-8")
|
|
195
|
+
if data_file.suffix.lower() == ".jsonl":
|
|
196
|
+
records = []
|
|
197
|
+
for line in text.splitlines():
|
|
198
|
+
line = line.strip()
|
|
199
|
+
if line:
|
|
200
|
+
records.append(json.loads(line))
|
|
201
|
+
return pd.DataFrame(records).head(sample_rows)
|
|
202
|
+
data = json.loads(text)
|
|
203
|
+
if isinstance(data, dict):
|
|
204
|
+
data = [data]
|
|
205
|
+
if not isinstance(data, list):
|
|
206
|
+
raise ValueError(f"JSON 顶层必须是对象数组: {data_file}")
|
|
207
|
+
return pd.DataFrame(data).head(sample_rows)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def infer_schema(
|
|
211
|
+
data_file: str | Path,
|
|
212
|
+
sample_rows: int = DEFAULT_SAMPLE_ROWS,
|
|
213
|
+
table_id: str | None = None,
|
|
214
|
+
table_name: str | None = None,
|
|
215
|
+
source_path: str | None = None,
|
|
216
|
+
) -> dict[str, Any]:
|
|
217
|
+
"""推断数据文件的 V2 schema 结构。
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
data_file: 数据文件路径
|
|
221
|
+
sample_rows: 类型推断的采样行数上限
|
|
222
|
+
table_id: 表 ID(默认生成 UUID v4;替换既有 schema 时传入原 id 保持引用不变)
|
|
223
|
+
table_name: 表显示名(默认取文件名去扩展名)
|
|
224
|
+
source_path: 写入 schema 的 source.path(默认为 data_file 原样路径)
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
schema 文件结构的字典(version/id/name/source/columns),
|
|
228
|
+
经 yaml.safe_dump 即可落盘为合法 *.schema.yaml
|
|
229
|
+
|
|
230
|
+
Raises:
|
|
231
|
+
ValueError: 文件不存在、扩展名不支持或无任何列可推断
|
|
232
|
+
"""
|
|
233
|
+
path = Path(data_file)
|
|
234
|
+
df = _read_head(path, sample_rows)
|
|
235
|
+
|
|
236
|
+
if df.empty or len(df.columns) == 0:
|
|
237
|
+
raise ValueError(f"数据文件无表头或无数据行,无法推断 schema: {data_file}")
|
|
238
|
+
|
|
239
|
+
columns: list[dict[str, Any]] = []
|
|
240
|
+
for col in df.columns:
|
|
241
|
+
# 逐列收集非空采样值后推断类型
|
|
242
|
+
non_null = [v for v in df[col].tolist() if v is not None and not (isinstance(v, float) and math.isnan(v))]
|
|
243
|
+
columns.append(
|
|
244
|
+
{
|
|
245
|
+
"id": str(col),
|
|
246
|
+
"name": str(col),
|
|
247
|
+
"type": infer_column_type(non_null),
|
|
248
|
+
}
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
"version": 2,
|
|
253
|
+
"id": table_id or str(uuid.uuid4()),
|
|
254
|
+
"name": table_name or path.stem,
|
|
255
|
+
"source": {
|
|
256
|
+
"mode": "relative_file",
|
|
257
|
+
"path": source_path if source_path is not None else str(data_file),
|
|
258
|
+
},
|
|
259
|
+
"columns": columns,
|
|
260
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
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 运行时对象构建服务
|
|
18
|
+
|
|
19
|
+
功能概述:
|
|
20
|
+
- 将 core 层的 TableSchemaFile 配置转换为 domain 层的运行时 TableSchema 对象
|
|
21
|
+
- 负责 core DTO 与 domain model 之间的转换,避免 core 层直接依赖 domain
|
|
22
|
+
|
|
23
|
+
架构设计:
|
|
24
|
+
- 转换层: config (TableSchemaFile) -> domain (TableSchema/ColumnSchema)
|
|
25
|
+
- 依赖 domain 的 build_type_from_config 解析列类型
|
|
26
|
+
- 由 services 层(如 project_loader、validation)调用
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
from typing import Any
|
|
32
|
+
|
|
33
|
+
from app.shared.core.project.schema.types import TableSchemaFile
|
|
34
|
+
from app.shared.domain import ColumnSchema, TableSchema, build_type_from_config
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def build_runtime_schema(schema_file: TableSchemaFile, registries: dict[str, Any]) -> TableSchema:
|
|
38
|
+
"""@methoddesc 从 TableSchemaFile 构建运行时 TableSchema 对象。
|
|
39
|
+
|
|
40
|
+
本函数位于 services 层,负责将 core 层的配置文件对象转换为 domain 层运行时对象,
|
|
41
|
+
从而消除 core.project.schema.reader 对 domain 的反向依赖。
|
|
42
|
+
|
|
43
|
+
:param schema_file: TableSchemaFile 配置对象
|
|
44
|
+
:param registries: 包含 expression_registry 的字典
|
|
45
|
+
:return: 运行时 TableSchema 对象
|
|
46
|
+
"""
|
|
47
|
+
columns: list[ColumnSchema] = []
|
|
48
|
+
for col in schema_file.columns:
|
|
49
|
+
data_type = build_type_from_config(col.type, registries)
|
|
50
|
+
col_schema = ColumnSchema(
|
|
51
|
+
name=col.name,
|
|
52
|
+
data_type=data_type,
|
|
53
|
+
is_primary_key=col.primary_key,
|
|
54
|
+
expand=col.expand,
|
|
55
|
+
nullable=col.nullable,
|
|
56
|
+
id=col.id,
|
|
57
|
+
json_path=col.json_path,
|
|
58
|
+
)
|
|
59
|
+
if col.children:
|
|
60
|
+
child_columns = []
|
|
61
|
+
for child in col.children:
|
|
62
|
+
child_data_type = build_type_from_config(child.type, registries)
|
|
63
|
+
child_columns.append(
|
|
64
|
+
ColumnSchema(
|
|
65
|
+
name=child.name,
|
|
66
|
+
data_type=child_data_type,
|
|
67
|
+
is_primary_key=child.primary_key,
|
|
68
|
+
expand=child.expand,
|
|
69
|
+
nullable=child.nullable,
|
|
70
|
+
id=child.id,
|
|
71
|
+
json_path=child.json_path,
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
col_schema.children = child_columns
|
|
75
|
+
columns.append(col_schema)
|
|
76
|
+
|
|
77
|
+
# 提取数据源信息
|
|
78
|
+
source_file = None
|
|
79
|
+
sheet_name = None
|
|
80
|
+
header_row = 0
|
|
81
|
+
source_config: dict[str, Any] = {}
|
|
82
|
+
if schema_file.source:
|
|
83
|
+
source_file = schema_file.source.path
|
|
84
|
+
sheet_name = schema_file.source.sheet
|
|
85
|
+
header_row = schema_file.source.header_row
|
|
86
|
+
# 保存完整的 source 配置,用于数据加载
|
|
87
|
+
source_config = schema_file.source.to_loader_config()
|
|
88
|
+
elif schema_file.sheet:
|
|
89
|
+
# 向后兼容:无 source 时回退到顶层 sheet 字段
|
|
90
|
+
# (source.sheet 与顶层 sheet 互斥,但单独的顶层 sheet 是合法配置)
|
|
91
|
+
sheet_name = schema_file.sheet
|
|
92
|
+
|
|
93
|
+
# 构建运行时 TableSchema 对象
|
|
94
|
+
return TableSchema(
|
|
95
|
+
id=schema_file.id,
|
|
96
|
+
name=schema_file.name,
|
|
97
|
+
columns=columns,
|
|
98
|
+
source_file=source_file,
|
|
99
|
+
sheet_name=sheet_name,
|
|
100
|
+
header_row=header_row,
|
|
101
|
+
script_checks=schema_file.script_checks,
|
|
102
|
+
source_config=source_config,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def build_runtime_schemas(
|
|
107
|
+
schema_files: dict[str, TableSchemaFile],
|
|
108
|
+
registries: dict[str, Any],
|
|
109
|
+
) -> dict[str, TableSchema]:
|
|
110
|
+
"""@methoddesc 批量构建运行时 TableSchema 对象。
|
|
111
|
+
|
|
112
|
+
:param schema_files: schema 文件字典(table_id -> TableSchemaFile)
|
|
113
|
+
:param registries: 表达式注册表
|
|
114
|
+
:return: 运行时 TableSchema 字典(table_id -> TableSchema)
|
|
115
|
+
"""
|
|
116
|
+
tables: dict[str, TableSchema] = {}
|
|
117
|
+
for table_id, schema in schema_files.items():
|
|
118
|
+
# 使用 table_id 作为键,与新的文件命名规则保持一致
|
|
119
|
+
tables[table_id] = build_runtime_schema(schema, registries)
|
|
120
|
+
return tables
|
|
@@ -0,0 +1,52 @@
|
|
|
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
|
+
- 提供 ValidationType、ValidationResult 等核心类型
|
|
22
|
+
- 提供 UnifiedValidationService 统一校验服务
|
|
23
|
+
- 提供文件加载便捷函数
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from .chunked_loader import ChunkedDataLoader, ChunkedValidationResult
|
|
27
|
+
from .loader import load_file_data
|
|
28
|
+
from .memory_monitor import MemoryMonitor
|
|
29
|
+
from .service import UnifiedValidationService
|
|
30
|
+
from .types import ValidationResult, ValidationType
|
|
31
|
+
from .validators import (
|
|
32
|
+
BaseValidator,
|
|
33
|
+
CompositeValidator,
|
|
34
|
+
ConstraintAdapter,
|
|
35
|
+
DateLogicValidator,
|
|
36
|
+
PreCheck,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
"ValidationType",
|
|
41
|
+
"ValidationResult",
|
|
42
|
+
"UnifiedValidationService",
|
|
43
|
+
"load_file_data",
|
|
44
|
+
"BaseValidator",
|
|
45
|
+
"ConstraintAdapter",
|
|
46
|
+
"PreCheck",
|
|
47
|
+
"CompositeValidator",
|
|
48
|
+
"DateLogicValidator",
|
|
49
|
+
"MemoryMonitor",
|
|
50
|
+
"ChunkedDataLoader",
|
|
51
|
+
"ChunkedValidationResult",
|
|
52
|
+
]
|