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,406 @@
|
|
|
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 YAML 原子写入与文件锁模块
|
|
18
|
+
|
|
19
|
+
功能概述:
|
|
20
|
+
- 跨平台文件锁(Windows msvcrt / Unix fcntl)保证并发安全
|
|
21
|
+
- 原子性 YAML 文件写入(先写临时文件再重命名)
|
|
22
|
+
- 优先使用 ruamel.yaml 保留注释和格式,回退到标准 yaml
|
|
23
|
+
|
|
24
|
+
架构设计:
|
|
25
|
+
- FileLock 上下文管理器实现跨平台文件锁
|
|
26
|
+
- atomic_write_yaml 提供原子写入能力
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import logging
|
|
32
|
+
import os
|
|
33
|
+
import tempfile
|
|
34
|
+
import time
|
|
35
|
+
import uuid
|
|
36
|
+
from io import TextIOWrapper
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
from types import TracebackType
|
|
39
|
+
from typing import Any, Literal
|
|
40
|
+
|
|
41
|
+
logger = logging.getLogger(__name__)
|
|
42
|
+
|
|
43
|
+
# 陈旧锁判定阈值(秒):锁文件 mtime 超过该值视为持有进程已崩溃残留。
|
|
44
|
+
# 正常写 YAML 的持锁时长远小于该值;Windows 的 open(lock, "x") 独占创建
|
|
45
|
+
# 在进程崩溃后不会自动释放(.lock 文件残留),只能按时间阈值接管。
|
|
46
|
+
STALE_LOCK_SECONDS = 30.0
|
|
47
|
+
|
|
48
|
+
# 文件锁导入(跨平台支持)
|
|
49
|
+
msvcrt: Any = None
|
|
50
|
+
fcntl: Any = None
|
|
51
|
+
_HAS_FILE_LOCK = False
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
# Windows
|
|
55
|
+
import msvcrt
|
|
56
|
+
|
|
57
|
+
_HAS_FILE_LOCK = True
|
|
58
|
+
logger.debug("文件锁: 使用 Windows msvcrt 模式")
|
|
59
|
+
except ImportError:
|
|
60
|
+
logger.debug("Windows文件锁模块未安装", exc_info=True)
|
|
61
|
+
|
|
62
|
+
if not _HAS_FILE_LOCK:
|
|
63
|
+
try:
|
|
64
|
+
# Unix/Linux/Mac
|
|
65
|
+
import fcntl
|
|
66
|
+
|
|
67
|
+
_HAS_FILE_LOCK = True
|
|
68
|
+
logger.debug("文件锁: 使用 Unix fcntl 模式")
|
|
69
|
+
except ImportError:
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
if not _HAS_FILE_LOCK:
|
|
73
|
+
logger.warning("文件锁功能不可用,并发写入时可能存在竞态条件风险")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class ActionParseError(Exception):
|
|
77
|
+
"""
|
|
78
|
+
@classdesc 动作解析异常
|
|
79
|
+
|
|
80
|
+
当 LLM 返回的响应无法解析为有效 JSON 时抛出。
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class YamlUpdateError(Exception):
|
|
87
|
+
"""
|
|
88
|
+
@classdesc YAML 更新异常
|
|
89
|
+
|
|
90
|
+
当 YAML 文件的原子写入或文件锁操作失败时抛出。
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class FileLock:
|
|
97
|
+
"""
|
|
98
|
+
@classdesc 跨平台文件锁上下文管理器
|
|
99
|
+
|
|
100
|
+
支持 Windows(msvcrt) 和 Unix(fcntl) 两种文件锁实现,
|
|
101
|
+
通过上下文管理器确保并发写入安全。
|
|
102
|
+
|
|
103
|
+
使用场景:
|
|
104
|
+
- 多个进程/线程同时写入同一 YAML 文件时防止数据损坏
|
|
105
|
+
- 内联约束批量处理时的文件访问控制
|
|
106
|
+
|
|
107
|
+
使用示例:
|
|
108
|
+
with FileLock("/path/to/file.yaml"):
|
|
109
|
+
# 独占访问文件
|
|
110
|
+
with open("/path/to/file.yaml", 'w') as f:
|
|
111
|
+
yaml.dump(data, f)
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
def __init__(self, file_path: str, timeout: float = 10.0, stale_seconds: float = STALE_LOCK_SECONDS):
|
|
115
|
+
"""
|
|
116
|
+
@methoddesc 初始化文件锁
|
|
117
|
+
|
|
118
|
+
参数:
|
|
119
|
+
file_path: 要加锁的文件路径
|
|
120
|
+
timeout: 获取锁的超时时间(秒),默认 10 秒
|
|
121
|
+
stale_seconds: 陈旧锁判定阈值(秒),锁文件 mtime 超过该值视为崩溃残留并接管
|
|
122
|
+
"""
|
|
123
|
+
self.file_path = file_path
|
|
124
|
+
self.timeout = timeout
|
|
125
|
+
self.stale_seconds = stale_seconds
|
|
126
|
+
self.lock_file: TextIOWrapper | None = None
|
|
127
|
+
self.lock_path = f"{file_path}.lock"
|
|
128
|
+
# 本持有者的唯一令牌:获取锁时写入锁文件,释放前重验归属,避免误删他人重建的锁
|
|
129
|
+
self._lock_token: str | None = None
|
|
130
|
+
|
|
131
|
+
def _break_stale_lock(self) -> None:
|
|
132
|
+
"""陈旧锁检测:锁文件 mtime 超过 stale_seconds 视为持有进程已崩溃,删除后可重试获取。
|
|
133
|
+
|
|
134
|
+
仅处理"进程崩溃残留"场景——持锁进程存活时其 mtime 会不断刷新吗?不会,
|
|
135
|
+
但正常持锁时长(单次 YAML 原子写入)远小于阈值,误接管的概率可忽略;
|
|
136
|
+
删除失败(他方并发占用)时静默返回,交由外层重试循环处理。
|
|
137
|
+
"""
|
|
138
|
+
try:
|
|
139
|
+
stat_result = os.stat(self.lock_path)
|
|
140
|
+
except OSError:
|
|
141
|
+
return # 锁文件不存在,无需处理
|
|
142
|
+
age = time.time() - stat_result.st_mtime
|
|
143
|
+
if age <= self.stale_seconds:
|
|
144
|
+
return
|
|
145
|
+
try:
|
|
146
|
+
os.remove(self.lock_path)
|
|
147
|
+
logger.warning(f"检测到陈旧文件锁({age:.0f}s 前残留),已接管: {self.lock_path}")
|
|
148
|
+
except OSError:
|
|
149
|
+
logger.debug("删除陈旧锁文件失败(可能被并发占用)", exc_info=True)
|
|
150
|
+
|
|
151
|
+
def __enter__(self) -> FileLock:
|
|
152
|
+
"""
|
|
153
|
+
@methoddesc 获取文件锁
|
|
154
|
+
|
|
155
|
+
进入上下文时尝试获取文件锁,如果锁已被占用则等待,
|
|
156
|
+
超过 timeout 时间后抛出 YamlUpdateError。
|
|
157
|
+
每次尝试前先做陈旧锁检测,避免崩溃进程残留的 .lock 文件导致永久超时。
|
|
158
|
+
|
|
159
|
+
返回:
|
|
160
|
+
FileLock 实例自身
|
|
161
|
+
"""
|
|
162
|
+
if not _HAS_FILE_LOCK:
|
|
163
|
+
logger.debug(f"文件锁不可用,跳过加锁: {self.file_path}")
|
|
164
|
+
return self
|
|
165
|
+
|
|
166
|
+
start_time = time.time()
|
|
167
|
+
|
|
168
|
+
while True:
|
|
169
|
+
try:
|
|
170
|
+
if msvcrt:
|
|
171
|
+
# Windows: 先做陈旧锁检测,再用独占模式打开锁文件
|
|
172
|
+
self._break_stale_lock()
|
|
173
|
+
self.lock_file = open(self.lock_path, "x") # 独占创建
|
|
174
|
+
else:
|
|
175
|
+
# Unix: 使用 fcntl 文件锁
|
|
176
|
+
self.lock_file = open(self.lock_path, "w")
|
|
177
|
+
fcntl.flock(self.lock_file.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
178
|
+
|
|
179
|
+
# 写入唯一令牌(进程号 + 随机串):释放前重验锁文件归属,
|
|
180
|
+
# 缓解"自己持有的锁已被陈旧检测接管、他人重建新锁"时的误删竞态
|
|
181
|
+
self._lock_token = f"{os.getpid()}-{uuid.uuid4().hex}"
|
|
182
|
+
try:
|
|
183
|
+
self.lock_file.write(self._lock_token)
|
|
184
|
+
self.lock_file.flush()
|
|
185
|
+
except OSError:
|
|
186
|
+
# 令牌写入失败时释放路径退化为直接删除(保持旧行为)
|
|
187
|
+
logger.debug("写入锁令牌失败", exc_info=True)
|
|
188
|
+
|
|
189
|
+
logger.debug(f"获取文件锁成功: {self.lock_path}")
|
|
190
|
+
return self
|
|
191
|
+
|
|
192
|
+
except OSError as e:
|
|
193
|
+
if self.lock_file:
|
|
194
|
+
self.lock_file.close()
|
|
195
|
+
self.lock_file = None
|
|
196
|
+
|
|
197
|
+
if time.time() - start_time > self.timeout:
|
|
198
|
+
raise YamlUpdateError(f"获取文件锁超时: {self.lock_path}") from e
|
|
199
|
+
|
|
200
|
+
time.sleep(0.1) # 100ms 后重试
|
|
201
|
+
|
|
202
|
+
def __exit__(
|
|
203
|
+
self,
|
|
204
|
+
exc_type: type[BaseException] | None,
|
|
205
|
+
exc_val: BaseException | None,
|
|
206
|
+
exc_tb: TracebackType | None,
|
|
207
|
+
) -> Literal[False]:
|
|
208
|
+
"""
|
|
209
|
+
@methoddesc 释放文件锁
|
|
210
|
+
|
|
211
|
+
退出上下文时释放文件锁并尝试删除锁文件。
|
|
212
|
+
删除前先 close 自身句柄,再重验锁文件内容是否仍为本次写入的令牌:
|
|
213
|
+
若内容已变(自己的锁被陈旧检测接管、他人重建了新锁)则跳过删除,避免误删他人锁。
|
|
214
|
+
不会吞掉异常。
|
|
215
|
+
|
|
216
|
+
返回:
|
|
217
|
+
False(不吞掉异常)
|
|
218
|
+
"""
|
|
219
|
+
if self.lock_file:
|
|
220
|
+
try:
|
|
221
|
+
# 只在有 fcntl 模块的系统上解锁(Unix)
|
|
222
|
+
if fcntl is not None:
|
|
223
|
+
fcntl.flock(self.lock_file.fileno(), fcntl.LOCK_UN)
|
|
224
|
+
# 先 close 自身句柄再删除,缩短"句柄未关即删"的窗口
|
|
225
|
+
self.lock_file.close()
|
|
226
|
+
|
|
227
|
+
# 删除前重验所有权:锁文件内容不再是自己的令牌 → 已被他人接管,跳过删除
|
|
228
|
+
if self._lock_token is not None:
|
|
229
|
+
try:
|
|
230
|
+
with open(self.lock_path, encoding="utf-8") as lf:
|
|
231
|
+
current_token = lf.read()
|
|
232
|
+
if current_token != self._lock_token:
|
|
233
|
+
logger.debug("锁文件已被其他持有者接管,跳过删除: %s", self.lock_path)
|
|
234
|
+
return False
|
|
235
|
+
except FileNotFoundError:
|
|
236
|
+
# 锁文件已不存在(被接管删除),无需再删
|
|
237
|
+
return False
|
|
238
|
+
except OSError:
|
|
239
|
+
logger.debug("重读锁文件失败,按原行为尝试删除", exc_info=True)
|
|
240
|
+
|
|
241
|
+
# 尝试删除锁文件
|
|
242
|
+
try:
|
|
243
|
+
os.remove(self.lock_path)
|
|
244
|
+
except OSError:
|
|
245
|
+
logger.debug("删除锁文件失败", exc_info=True)
|
|
246
|
+
|
|
247
|
+
logger.debug(f"释放文件锁: {self.lock_path}")
|
|
248
|
+
except Exception as e:
|
|
249
|
+
logger.warning(f"释放文件锁时出错: {e}")
|
|
250
|
+
|
|
251
|
+
return False # 不吞掉异常
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def atomic_write_yaml(file_path: Path, data: dict[str, Any], preserve_format: bool = True) -> None:
|
|
255
|
+
"""
|
|
256
|
+
@methoddesc 原子性写入 YAML 文件
|
|
257
|
+
|
|
258
|
+
先写入临时文件,再重命名,确保不会因为崩溃导致原文件损坏。
|
|
259
|
+
当 preserve_format=True 且文件存在时,使用 ruamel.yaml 保留注释和空行。
|
|
260
|
+
|
|
261
|
+
参数:
|
|
262
|
+
file_path: 目标文件路径
|
|
263
|
+
data: 要写入的数据字典
|
|
264
|
+
preserve_format: 是否保留原有格式(注释、空行等)
|
|
265
|
+
|
|
266
|
+
异常:
|
|
267
|
+
YamlUpdateError: 写入失败时抛出
|
|
268
|
+
|
|
269
|
+
示例:
|
|
270
|
+
>>> atomic_write_yaml(Path("config.yaml"), {"version": 2})
|
|
271
|
+
"""
|
|
272
|
+
import yaml
|
|
273
|
+
|
|
274
|
+
file_path = Path(file_path)
|
|
275
|
+
|
|
276
|
+
try:
|
|
277
|
+
# 如果文件已存在且需要保留格式,使用 ruamel.yaml
|
|
278
|
+
if preserve_format and file_path.exists():
|
|
279
|
+
try:
|
|
280
|
+
from ruamel.yaml import YAML
|
|
281
|
+
|
|
282
|
+
# 使用 ruamel.yaml 加载保留格式的 YAML
|
|
283
|
+
yaml_parser = YAML()
|
|
284
|
+
yaml_parser.preserve_quotes = True
|
|
285
|
+
yaml_parser.default_flow_style = False
|
|
286
|
+
|
|
287
|
+
with open(file_path, encoding="utf-8") as f:
|
|
288
|
+
existing_data = yaml_parser.load(f)
|
|
289
|
+
|
|
290
|
+
if existing_data is not None:
|
|
291
|
+
# 递归更新数据,保留格式
|
|
292
|
+
_update_yaml_data(existing_data, data)
|
|
293
|
+
|
|
294
|
+
# 原子性写入
|
|
295
|
+
fd, temp_path = tempfile.mkstemp(
|
|
296
|
+
dir=file_path.parent, prefix=f".{file_path.stem}_tmp_", suffix=".yaml"
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
try:
|
|
300
|
+
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
301
|
+
yaml_parser.dump(existing_data, f)
|
|
302
|
+
os.replace(temp_path, file_path)
|
|
303
|
+
logger.debug(f"原子写入成功(保留格式): {file_path}")
|
|
304
|
+
return
|
|
305
|
+
except OSError:
|
|
306
|
+
try:
|
|
307
|
+
os.remove(temp_path)
|
|
308
|
+
except OSError:
|
|
309
|
+
logger.debug("删除临时文件失败", exc_info=True)
|
|
310
|
+
raise
|
|
311
|
+
except ImportError:
|
|
312
|
+
logger.warning("ruamel.yaml 未安装,无法保留 YAML 格式")
|
|
313
|
+
except Exception as e:
|
|
314
|
+
logger.warning(f"使用 ruamel.yaml 失败,回退到标准方式: {e}")
|
|
315
|
+
|
|
316
|
+
# 标准写入方式(不保留格式)
|
|
317
|
+
fd, temp_path = tempfile.mkstemp(dir=file_path.parent, prefix=f".{file_path.stem}_tmp_", suffix=".yaml")
|
|
318
|
+
|
|
319
|
+
try:
|
|
320
|
+
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
321
|
+
yaml.safe_dump(data, f, allow_unicode=True, sort_keys=False)
|
|
322
|
+
|
|
323
|
+
os.replace(temp_path, file_path)
|
|
324
|
+
logger.debug(f"原子写入成功: {file_path}")
|
|
325
|
+
|
|
326
|
+
except OSError:
|
|
327
|
+
try:
|
|
328
|
+
os.remove(temp_path)
|
|
329
|
+
except OSError:
|
|
330
|
+
logger.debug("删除临时文件失败", exc_info=True)
|
|
331
|
+
raise
|
|
332
|
+
|
|
333
|
+
except Exception as e:
|
|
334
|
+
raise YamlUpdateError(f"原子写入失败: {file_path}") from e
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _update_yaml_data(existing: Any, new: Any) -> None:
|
|
338
|
+
"""
|
|
339
|
+
@methoddesc 递归更新 YAML 数据,保留现有格式
|
|
340
|
+
|
|
341
|
+
用于 ruamel.yaml 加载的数据结构,在保留注释和格式的前提下递归更新数据。
|
|
342
|
+
|
|
343
|
+
参数:
|
|
344
|
+
existing: 现有的 YAML 数据结构(来自 ruamel.yaml)
|
|
345
|
+
new: 新的数据字典
|
|
346
|
+
"""
|
|
347
|
+
from ruamel.yaml.comments import CommentedMap, CommentedSeq
|
|
348
|
+
|
|
349
|
+
if isinstance(existing, CommentedMap) and isinstance(new, dict):
|
|
350
|
+
# 更新或添加键值
|
|
351
|
+
for key, value in new.items():
|
|
352
|
+
if key in existing:
|
|
353
|
+
if isinstance(value, dict) and isinstance(existing[key], CommentedMap):
|
|
354
|
+
_update_yaml_data(existing[key], value)
|
|
355
|
+
elif isinstance(value, list) and isinstance(existing[key], CommentedSeq):
|
|
356
|
+
_update_yaml_list(existing[key], value)
|
|
357
|
+
else:
|
|
358
|
+
existing[key] = value
|
|
359
|
+
else:
|
|
360
|
+
# 新键,直接添加
|
|
361
|
+
existing[key] = value
|
|
362
|
+
elif isinstance(existing, CommentedSeq) and isinstance(new, list):
|
|
363
|
+
_update_yaml_list(existing, new)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _update_yaml_list(existing_list: Any, new_list: list) -> None:
|
|
367
|
+
"""
|
|
368
|
+
@methoddesc 更新 YAML 列表,保留现有格式
|
|
369
|
+
|
|
370
|
+
用于更新 constraints 等列表,根据 id 匹配更新现有项或添加新项。
|
|
371
|
+
没有 id 的项直接追加到列表末尾。
|
|
372
|
+
|
|
373
|
+
参数:
|
|
374
|
+
existing_list: 现有的 YAML 列表(来自 ruamel.yaml)
|
|
375
|
+
new_list: 新的数据列表
|
|
376
|
+
"""
|
|
377
|
+
from ruamel.yaml.comments import CommentedMap
|
|
378
|
+
|
|
379
|
+
if not new_list:
|
|
380
|
+
return
|
|
381
|
+
|
|
382
|
+
# 为现有项建立 id 到索引的映射
|
|
383
|
+
existing_ids = {}
|
|
384
|
+
for idx, item in enumerate(existing_list):
|
|
385
|
+
if isinstance(item, CommentedMap) and "id" in item:
|
|
386
|
+
existing_ids[item["id"]] = idx
|
|
387
|
+
|
|
388
|
+
# 处理新列表中的每一项
|
|
389
|
+
for new_item in new_list:
|
|
390
|
+
if isinstance(new_item, dict) and "id" in new_item:
|
|
391
|
+
item_id = new_item["id"]
|
|
392
|
+
if item_id in existing_ids:
|
|
393
|
+
# 更新现有项
|
|
394
|
+
idx = existing_ids[item_id]
|
|
395
|
+
existing_item = existing_list[idx]
|
|
396
|
+
if isinstance(existing_item, CommentedMap):
|
|
397
|
+
_update_yaml_data(existing_item, new_item)
|
|
398
|
+
else:
|
|
399
|
+
existing_list[idx] = new_item
|
|
400
|
+
else:
|
|
401
|
+
# 添加新项
|
|
402
|
+
existing_list.append(new_item)
|
|
403
|
+
else:
|
|
404
|
+
# 没有 id 的项,直接添加
|
|
405
|
+
if new_item not in existing_list:
|
|
406
|
+
existing_list.append(new_item)
|
|
@@ -0,0 +1,18 @@
|
|
|
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
|
+
"""
|
|
@@ -0,0 +1,145 @@
|
|
|
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
|
+
- 使用新版 Spec + load_source_data 架构
|
|
22
|
+
- header_enabled=False(预览不识别表头,对应旧版 header=None)
|
|
23
|
+
- nrows 在加载时限制(CSV/Excel)
|
|
24
|
+
- 不做空值清洗,保持原始值供 str() 转换
|
|
25
|
+
|
|
26
|
+
架构设计:
|
|
27
|
+
- 职责隔离:preview 有独立入口,不触碰 validation/loader.py
|
|
28
|
+
- 直接利用新版架构(Spec → Loader → Registry),符合项目 V2 方向
|
|
29
|
+
- 为后续扩展(encoding、delimiter 预览参数)提供清晰位置
|
|
30
|
+
|
|
31
|
+
输入示例:
|
|
32
|
+
df, sheet_names = load_preview_data(
|
|
33
|
+
file_path="data/users.xlsx",
|
|
34
|
+
file_type="excel",
|
|
35
|
+
max_rows=100,
|
|
36
|
+
sheet_name="Sheet1"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
输出示例:
|
|
40
|
+
(DataFrame, sheet_names)
|
|
41
|
+
- sheet_names 仅在 excel 时返回,其他格式为 None
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
import logging
|
|
47
|
+
|
|
48
|
+
import pandas as pd
|
|
49
|
+
|
|
50
|
+
from app.shared.core.data_source.loaders import load_source_data
|
|
51
|
+
from app.shared.core.data_source.loaders.excel_loader import get_excel_sheet_names
|
|
52
|
+
from app.shared.core.data_source.specs.csv_source import CSVSourceSpec
|
|
53
|
+
from app.shared.core.data_source.specs.excel_source import ExcelSourceSpec
|
|
54
|
+
from app.shared.core.data_source.specs.json_source import JSONSourceSpec
|
|
55
|
+
|
|
56
|
+
logger = logging.getLogger(__name__)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def load_preview_data(
|
|
60
|
+
file_path: str,
|
|
61
|
+
file_type: str,
|
|
62
|
+
max_rows: int,
|
|
63
|
+
sheet_name: str | None = None,
|
|
64
|
+
source_config: dict | None = None,
|
|
65
|
+
) -> tuple[pd.DataFrame, list[str] | None]:
|
|
66
|
+
"""
|
|
67
|
+
@methoddesc 预览专用数据加载器
|
|
68
|
+
|
|
69
|
+
使用新版 Spec + load_source_data 架构加载预览数据。
|
|
70
|
+
- Excel/CSV:在加载时限制 nrows,不做空值清洗,保持原始数据供前端展示
|
|
71
|
+
- JSON:复用 JSONSourceSpec 加载(无 nrows 字段,加载后按 max_rows 截断)
|
|
72
|
+
|
|
73
|
+
参数:
|
|
74
|
+
file_path: 待加载文件的完整路径
|
|
75
|
+
file_type: 文件类型,可选值为 "excel" / "csv" / "json"
|
|
76
|
+
max_rows: 最大读取行数,用于限制预览数据量
|
|
77
|
+
sheet_name: Excel 工作表名称(仅 Excel 格式有效,可选)
|
|
78
|
+
source_config: 额外的源配置字典,如编码(encoding)、分隔符(delimiter)
|
|
79
|
+
|
|
80
|
+
返回:
|
|
81
|
+
元组 (DataFrame, sheet_names)
|
|
82
|
+
- DataFrame: 加载的原始数据(未清洗,保留空值和原始格式)
|
|
83
|
+
- sheet_names: 仅在 Excel 格式时返回工作表名称列表,其他格式返回 None
|
|
84
|
+
|
|
85
|
+
异常:
|
|
86
|
+
ValueError: 当 file_type 为不支持的类型时抛出
|
|
87
|
+
FileNotFoundError: 当文件不存在时抛出
|
|
88
|
+
"""
|
|
89
|
+
# ======== Excel 格式处理 ========
|
|
90
|
+
if file_type == "excel":
|
|
91
|
+
# 构造 Excel 数据源规格,关闭表头识别、限制读取行数
|
|
92
|
+
spec: ExcelSourceSpec | CSVSourceSpec | JSONSourceSpec = ExcelSourceSpec(
|
|
93
|
+
path=file_path,
|
|
94
|
+
sheet=sheet_name,
|
|
95
|
+
header_enabled=False,
|
|
96
|
+
nrows=max_rows,
|
|
97
|
+
)
|
|
98
|
+
df = load_source_data(spec)
|
|
99
|
+
|
|
100
|
+
# 额外读取全部工作表名称,供前端切换展示。
|
|
101
|
+
# 引擎按扩展名自适应(.xls→xlrd),句柄在助手内确保关闭——
|
|
102
|
+
# 原实现硬编码 openpyxl 且依赖此处 close,曾致 .xls 必炸/句柄泄漏。
|
|
103
|
+
sheet_names = get_excel_sheet_names(file_path)
|
|
104
|
+
|
|
105
|
+
return df, sheet_names
|
|
106
|
+
|
|
107
|
+
# ======== CSV 格式处理 ========
|
|
108
|
+
if file_type == "csv":
|
|
109
|
+
# 从 source_config 提取编码和分隔符,使用默认值兜底
|
|
110
|
+
sc = source_config or {}
|
|
111
|
+
spec = CSVSourceSpec(
|
|
112
|
+
path=file_path,
|
|
113
|
+
header_enabled=False,
|
|
114
|
+
nrows=max_rows,
|
|
115
|
+
encoding=sc.get("encoding", "utf-8"),
|
|
116
|
+
delimiter=sc.get("delimiter", ","),
|
|
117
|
+
)
|
|
118
|
+
df = load_source_data(spec)
|
|
119
|
+
return df, None
|
|
120
|
+
|
|
121
|
+
# ======== JSON 格式处理(复用 JSONSourceSpec + load_source_data 管线) ========
|
|
122
|
+
if file_type == "json":
|
|
123
|
+
# 从 source_config 提取 JSON 格式与 json_path
|
|
124
|
+
sc = source_config or {}
|
|
125
|
+
# D8: format 必填,默认 array(顶层数组);.jsonl/.ndjson 文件强制 lines(与 validation 路径一致)
|
|
126
|
+
import os
|
|
127
|
+
|
|
128
|
+
ext = os.path.splitext(file_path)[1].lower()
|
|
129
|
+
if ext in [".jsonl", ".ndjson"]:
|
|
130
|
+
fmt = "lines"
|
|
131
|
+
else:
|
|
132
|
+
fmt = sc.get("json_format") or "array"
|
|
133
|
+
spec = JSONSourceSpec(
|
|
134
|
+
path=file_path,
|
|
135
|
+
format=fmt,
|
|
136
|
+
json_path=sc.get("json_path"),
|
|
137
|
+
)
|
|
138
|
+
df = load_source_data(spec)
|
|
139
|
+
# JSONSourceSpec 无 nrows 字段,加载后截断到预览行数上限
|
|
140
|
+
if len(df) > max_rows:
|
|
141
|
+
df = df.head(max_rows)
|
|
142
|
+
return df, None
|
|
143
|
+
|
|
144
|
+
# 如果走到这里,说明传入的文件类型不在支持列表中
|
|
145
|
+
raise ValueError(f"不支持的文件类型: {file_type}")
|