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,567 @@
|
|
|
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
|
+
# backend/app/api/routers/project/schema.py
|
|
17
|
+
"""
|
|
18
|
+
@fileoverview Schema CRUD API - 数据表结构管理
|
|
19
|
+
|
|
20
|
+
功能概述:
|
|
21
|
+
- 负责 Schema(数据表结构定义)的增删改查操作
|
|
22
|
+
- Schema 定义了数据源的列结构、类型约束等信息
|
|
23
|
+
- 支持冲突检测和合并写入策略
|
|
24
|
+
|
|
25
|
+
架构设计:
|
|
26
|
+
- 两层查找策略: 优先从 manifest 读取引用,兼容扫描目录
|
|
27
|
+
- 文件命名约定: {table_id}.schema.yaml
|
|
28
|
+
- 合并写入模式: CREATE / OVERWRITE / MERGE 三种 SaveMode
|
|
29
|
+
- 冲突检测: 检查同名表在 manifest 和文件系统的一致性
|
|
30
|
+
|
|
31
|
+
输入示例:
|
|
32
|
+
PUT /schemas/users
|
|
33
|
+
{
|
|
34
|
+
"id": "users",
|
|
35
|
+
"columns": [
|
|
36
|
+
{"id": "name", "type": "string"}
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
输出示例:
|
|
41
|
+
{
|
|
42
|
+
"success": true,
|
|
43
|
+
"data": {"table_id": "users", "display_name": "users"}
|
|
44
|
+
}
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
import os
|
|
48
|
+
from enum import Enum
|
|
49
|
+
from pathlib import Path
|
|
50
|
+
from typing import Any
|
|
51
|
+
|
|
52
|
+
from fastapi import APIRouter, Depends, HTTPException
|
|
53
|
+
from pydantic import BaseModel
|
|
54
|
+
|
|
55
|
+
from app.api.dependencies import get_project_config_path
|
|
56
|
+
from app.shared.core.io.yaml import read_yaml, write_yaml_atomic
|
|
57
|
+
from app.shared.core.project.manifest.types import ProjectManifestV2
|
|
58
|
+
from app.shared.core.project.schema_ref_check import find_schema_references
|
|
59
|
+
from app.shared.core.utils.path_utils import make_relative
|
|
60
|
+
|
|
61
|
+
from .base import (
|
|
62
|
+
DisplayNameUpdateRequest,
|
|
63
|
+
SchemaRefV2,
|
|
64
|
+
StandardResponse,
|
|
65
|
+
TableSchemaFileV2,
|
|
66
|
+
_scan_schema_file,
|
|
67
|
+
_v2_manifest_path,
|
|
68
|
+
)
|
|
69
|
+
from .helpers import _resolve_project_path, project_lock
|
|
70
|
+
from .manifest import get_v2_manifest
|
|
71
|
+
from .schema_helpers import _compute_conflicts, _get_schema_path, _merge_schemas
|
|
72
|
+
|
|
73
|
+
router = APIRouter(prefix="", tags=["Project-Schema"])
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class SaveMode(str, Enum):
|
|
77
|
+
"""Schema 保存模式"""
|
|
78
|
+
|
|
79
|
+
CREATE = "create" # 仅新建,不覆盖已有文件
|
|
80
|
+
MERGE = "merge" # 合并写入
|
|
81
|
+
OVERWRITE = "overwrite" # 直接覆盖
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class SchemaConflictInfo(BaseModel):
|
|
85
|
+
"""
|
|
86
|
+
Schema 冲突信息模型。
|
|
87
|
+
|
|
88
|
+
使用场景:
|
|
89
|
+
- 前端在保存 schema 前调用 check-conflict 接口,根据返回结果决定是否提示用户覆盖确认
|
|
90
|
+
- 为前端提供详细的字段级冲突对比,提升用户体验
|
|
91
|
+
|
|
92
|
+
字段说明:
|
|
93
|
+
- exists: 目标文件是否已存在
|
|
94
|
+
- file_path: 冲突文件相对于项目根目录的路径
|
|
95
|
+
- has_conflict: 是否存在实质性字段冲突
|
|
96
|
+
- conflict_fields: 具体冲突的字段列表(如 columns, constraints 等)
|
|
97
|
+
- existing_schema: 现有 schema 的完整内容(用于前端对比展示)
|
|
98
|
+
- new_schema: 新 schema 的完整内容(用于前端对比展示)
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
exists: bool
|
|
102
|
+
file_path: str
|
|
103
|
+
has_conflict: bool
|
|
104
|
+
conflict_fields: list[str] = []
|
|
105
|
+
existing_schema: dict[str, Any] | None = None
|
|
106
|
+
new_schema: dict[str, Any] | None = None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@router.get(
|
|
110
|
+
"/schemas/{table_id}",
|
|
111
|
+
response_model=TableSchemaFileV2,
|
|
112
|
+
summary="读取 Schema",
|
|
113
|
+
responses={
|
|
114
|
+
404: {"description": "Schema 不存在"},
|
|
115
|
+
500: {"description": "服务器内部错误"},
|
|
116
|
+
},
|
|
117
|
+
)
|
|
118
|
+
def get_v2_schema(table_id: str, config_path: str = Depends(get_project_config_path)) -> TableSchemaFileV2:
|
|
119
|
+
"""
|
|
120
|
+
读取指定 table_id 的 schema 文件。
|
|
121
|
+
|
|
122
|
+
查找策略(支持所有 schema,包括未列入 manifest 的):
|
|
123
|
+
1. 从 manifest.schemas 中查找引用(优先,性能更好)
|
|
124
|
+
2. 如果未找到,扫描 schemas/ 目录,读取文件内容中的 id 字段
|
|
125
|
+
|
|
126
|
+
参数:
|
|
127
|
+
table_id: Schema 对应的表 ID
|
|
128
|
+
config_path: 项目配置根目录(通过 Depends 注入)
|
|
129
|
+
|
|
130
|
+
返回:
|
|
131
|
+
TableSchemaFileV2: Schema 文件内容
|
|
132
|
+
"""
|
|
133
|
+
manifest = get_v2_manifest(config_path)
|
|
134
|
+
|
|
135
|
+
# 策略 1: 从 manifest 中查找(优先)
|
|
136
|
+
ref = next((s for s in manifest.schemas if s.id == table_id), None)
|
|
137
|
+
|
|
138
|
+
if ref:
|
|
139
|
+
schema_path = os.path.join(config_path, ref.path)
|
|
140
|
+
if os.path.isfile(schema_path):
|
|
141
|
+
data = read_yaml(Path(schema_path))
|
|
142
|
+
return TableSchemaFileV2.model_validate(data)
|
|
143
|
+
|
|
144
|
+
# 策略 2: 扫描 schemas/ 目录,读取文件内容中的 id 字段
|
|
145
|
+
schemas_dir = os.path.join(config_path, "schemas")
|
|
146
|
+
if os.path.isdir(schemas_dir):
|
|
147
|
+
for filename in os.listdir(schemas_dir):
|
|
148
|
+
if filename.lower().endswith(".schema.yaml"):
|
|
149
|
+
try:
|
|
150
|
+
file_path = os.path.join(schemas_dir, filename)
|
|
151
|
+
data = read_yaml(Path(file_path))
|
|
152
|
+
if isinstance(data, dict) and data.get("id") == table_id:
|
|
153
|
+
return TableSchemaFileV2.model_validate(data)
|
|
154
|
+
except Exception:
|
|
155
|
+
# 读取失败,继续下一个文件
|
|
156
|
+
continue
|
|
157
|
+
|
|
158
|
+
raise HTTPException(status_code=404, detail=f"未找到这张表结构定义,可能已被删除(ID: {table_id})")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@router.put(
|
|
162
|
+
"/schemas/{table_id}",
|
|
163
|
+
response_model=StandardResponse,
|
|
164
|
+
summary="写入 Schema",
|
|
165
|
+
responses={
|
|
166
|
+
400: {"description": "请求参数错误"},
|
|
167
|
+
404: {"description": "Manifest 不存在"},
|
|
168
|
+
409: {"description": "Schema 文件已存在"},
|
|
169
|
+
500: {"description": "服务器内部错误"},
|
|
170
|
+
},
|
|
171
|
+
)
|
|
172
|
+
def put_v2_schema(
|
|
173
|
+
table_id: str,
|
|
174
|
+
schema: TableSchemaFileV2,
|
|
175
|
+
mode: SaveMode = SaveMode.OVERWRITE,
|
|
176
|
+
config_path: str = Depends(get_project_config_path),
|
|
177
|
+
) -> dict[str, str]:
|
|
178
|
+
"""
|
|
179
|
+
写入指定 table_id 的 schema 文件。
|
|
180
|
+
|
|
181
|
+
业务逻辑:
|
|
182
|
+
- 验证 schema.id 与路径参数 table_id 一致
|
|
183
|
+
- 如果 manifest 未引用,自动补齐引用
|
|
184
|
+
- 同时更新 manifest 文件
|
|
185
|
+
- 支持三种保存模式:create(仅新建)、merge(合并)、overwrite(覆盖)
|
|
186
|
+
|
|
187
|
+
副作用:
|
|
188
|
+
- 创建或覆盖 schema 文件
|
|
189
|
+
- 更新 manifest 中的引用列表
|
|
190
|
+
|
|
191
|
+
数据流:
|
|
192
|
+
1. 验证 schema.id 与 table_id 一致
|
|
193
|
+
2. 读取 manifest 文件
|
|
194
|
+
3. 检查 manifest 是否已有该 schema 的引用
|
|
195
|
+
4. 如果没有,添加新的引用(默认路径:schemas/{table_id}.schema.yaml)
|
|
196
|
+
5. 根据模式处理文件写入
|
|
197
|
+
- create: 文件存在则报错
|
|
198
|
+
- merge: 读取现有文件并合并
|
|
199
|
+
- overwrite: 直接覆盖
|
|
200
|
+
6. 更新 manifest 文件
|
|
201
|
+
|
|
202
|
+
参数:
|
|
203
|
+
table_id: Schema 对应的表 ID
|
|
204
|
+
schema: Schema 数据
|
|
205
|
+
mode: 保存模式 (create/merge/overwrite)
|
|
206
|
+
config_path: 项目配置根目录(通过 Depends 注入)
|
|
207
|
+
|
|
208
|
+
返回:
|
|
209
|
+
StandardResponse: 操作结果消息
|
|
210
|
+
"""
|
|
211
|
+
if schema.id != table_id:
|
|
212
|
+
raise HTTPException(status_code=400, detail="请求中的表结构 ID 与地址栏不一致,请刷新页面后重试")
|
|
213
|
+
|
|
214
|
+
manifest_path = _v2_manifest_path(config_path)
|
|
215
|
+
manifest = (
|
|
216
|
+
ProjectManifestV2.model_validate(read_yaml(Path(manifest_path))) if os.path.isfile(manifest_path) else None
|
|
217
|
+
)
|
|
218
|
+
if not manifest:
|
|
219
|
+
raise HTTPException(
|
|
220
|
+
status_code=404,
|
|
221
|
+
detail={
|
|
222
|
+
# §2.1: 结构化错误码统一(与 dependencies.py 的 manifest 缺失出口同构),
|
|
223
|
+
# 前端按 detail.code 识别项目失效并触发自愈
|
|
224
|
+
"code": "PROJECT_NOT_FOUND",
|
|
225
|
+
"message": f"项目配置文件(project.precis.yaml)不存在,请确认项目是否已被移动或删除(配置目录: {manifest_path})",
|
|
226
|
+
"path": str(
|
|
227
|
+
config_path
|
|
228
|
+
), # §2.1: 项目根目录(前端与 activeProjectPath 同口径比对,勿传 manifest 文件路径)
|
|
229
|
+
},
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
# 使用 _get_schema_path 获取实际的文件路径(处理大小写不一致问题)
|
|
233
|
+
abs_schema_path = _get_schema_path(manifest, table_id, config_path)
|
|
234
|
+
|
|
235
|
+
ref = next((s for s in manifest.schemas if s.id == table_id), None)
|
|
236
|
+
if not ref:
|
|
237
|
+
schema_name = schema.name or table_id
|
|
238
|
+
default_rel = f"schemas/{schema_name}.schema.yaml"
|
|
239
|
+
manifest.schemas.append(SchemaRefV2(id=table_id, path=default_rel))
|
|
240
|
+
ref_path = default_rel
|
|
241
|
+
if abs_schema_path is None:
|
|
242
|
+
abs_schema_path = os.path.join(config_path, ref_path)
|
|
243
|
+
else:
|
|
244
|
+
ref_path = ref.path
|
|
245
|
+
if abs_schema_path and abs_schema_path != os.path.join(config_path, ref_path):
|
|
246
|
+
actual_rel = os.path.relpath(abs_schema_path, config_path)
|
|
247
|
+
ref.path = actual_rel
|
|
248
|
+
ref_path = actual_rel
|
|
249
|
+
if abs_schema_path is None:
|
|
250
|
+
abs_schema_path = os.path.join(config_path, ref_path)
|
|
251
|
+
|
|
252
|
+
# 验证路径安全,防止路径遍历攻击
|
|
253
|
+
try:
|
|
254
|
+
abs_schema_path = _resolve_project_path(config_path, os.path.relpath(abs_schema_path, config_path))
|
|
255
|
+
except ValueError:
|
|
256
|
+
raise HTTPException(status_code=400, detail="非法的 Schema 文件路径")
|
|
257
|
+
|
|
258
|
+
new_schema_data = schema.model_dump(exclude_none=True)
|
|
259
|
+
|
|
260
|
+
with project_lock(config_path):
|
|
261
|
+
if os.path.exists(abs_schema_path):
|
|
262
|
+
if mode == SaveMode.CREATE:
|
|
263
|
+
raise HTTPException(
|
|
264
|
+
status_code=409, detail=f"Schema 文件已存在: {abs_schema_path}。请使用 merge 或 overwrite 模式。"
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
if mode == SaveMode.MERGE:
|
|
268
|
+
existing_data = read_yaml(Path(abs_schema_path))
|
|
269
|
+
merged_data = _merge_schemas(existing_data, new_schema_data)
|
|
270
|
+
write_yaml_atomic(Path(abs_schema_path), merged_data)
|
|
271
|
+
else:
|
|
272
|
+
write_yaml_atomic(Path(abs_schema_path), new_schema_data)
|
|
273
|
+
else:
|
|
274
|
+
write_yaml_atomic(Path(abs_schema_path), new_schema_data)
|
|
275
|
+
|
|
276
|
+
write_yaml_atomic(Path(manifest_path), manifest.model_dump(exclude_none=True))
|
|
277
|
+
|
|
278
|
+
return {"message": f"V2 schema '{table_id}' 已保存。"}
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
@router.post(
|
|
282
|
+
"/schemas/{table_id}/check-conflict",
|
|
283
|
+
response_model=SchemaConflictInfo,
|
|
284
|
+
summary="检查 Schema 冲突",
|
|
285
|
+
responses={
|
|
286
|
+
400: {"description": "请求参数错误"},
|
|
287
|
+
404: {"description": "Schema 不存在"},
|
|
288
|
+
500: {"description": "服务器内部错误"},
|
|
289
|
+
},
|
|
290
|
+
)
|
|
291
|
+
def check_schema_conflict(
|
|
292
|
+
table_id: str,
|
|
293
|
+
new_schema: TableSchemaFileV2,
|
|
294
|
+
config_path: str = Depends(get_project_config_path),
|
|
295
|
+
) -> SchemaConflictInfo:
|
|
296
|
+
"""
|
|
297
|
+
检查 schema 保存时的冲突情况。
|
|
298
|
+
|
|
299
|
+
用于在保存前检查是否与已存在的文件存在冲突,
|
|
300
|
+
以便前端决定是否提示用户进行覆盖确认。
|
|
301
|
+
|
|
302
|
+
参数:
|
|
303
|
+
table_id: Schema 对应的表 ID
|
|
304
|
+
new_schema: 新的 schema 数据
|
|
305
|
+
config_path: 项目配置根目录(通过 Depends 注入)
|
|
306
|
+
|
|
307
|
+
返回:
|
|
308
|
+
SchemaConflictInfo: 冲突信息(file_path 为相对于 config_path 的路径)
|
|
309
|
+
"""
|
|
310
|
+
manifest = get_v2_manifest(config_path)
|
|
311
|
+
schema_path = _get_schema_path(manifest, table_id, config_path)
|
|
312
|
+
|
|
313
|
+
if not schema_path or not os.path.isfile(schema_path):
|
|
314
|
+
# 返回相对于 config_path 的路径(如果存在)
|
|
315
|
+
relative_path = ""
|
|
316
|
+
if schema_path and schema_path.startswith(config_path):
|
|
317
|
+
relative_path = make_relative(config_path, schema_path)
|
|
318
|
+
return SchemaConflictInfo(
|
|
319
|
+
exists=False,
|
|
320
|
+
file_path=relative_path,
|
|
321
|
+
has_conflict=False,
|
|
322
|
+
conflict_fields=[],
|
|
323
|
+
existing_schema=None,
|
|
324
|
+
new_schema=new_schema.model_dump(exclude_none=True),
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
existing_data = read_yaml(Path(schema_path))
|
|
328
|
+
new_data = new_schema.model_dump(exclude_none=True)
|
|
329
|
+
conflict_fields = _compute_conflicts(existing_data, new_data)
|
|
330
|
+
|
|
331
|
+
# 将绝对路径转换为相对于 config_path 的路径
|
|
332
|
+
relative_path = make_relative(config_path, schema_path)
|
|
333
|
+
|
|
334
|
+
return SchemaConflictInfo(
|
|
335
|
+
exists=True,
|
|
336
|
+
file_path=relative_path,
|
|
337
|
+
has_conflict=len(conflict_fields) > 0,
|
|
338
|
+
conflict_fields=conflict_fields,
|
|
339
|
+
existing_schema=existing_data,
|
|
340
|
+
new_schema=new_data,
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
@router.delete(
|
|
345
|
+
"/schemas/{table_id}",
|
|
346
|
+
response_model=StandardResponse,
|
|
347
|
+
summary="删除 Schema",
|
|
348
|
+
responses={
|
|
349
|
+
400: {"description": "请求参数错误"},
|
|
350
|
+
404: {"description": "Schema 或 Manifest 不存在"},
|
|
351
|
+
409: {"description": "Schema 仍被约束引用"},
|
|
352
|
+
500: {"description": "服务器内部错误"},
|
|
353
|
+
},
|
|
354
|
+
)
|
|
355
|
+
def delete_v2_schema(table_id: str, config_path: str = Depends(get_project_config_path)) -> dict[str, str]:
|
|
356
|
+
"""
|
|
357
|
+
删除指定 table_id 的 schema 文件,并从 manifest 中移除引用。
|
|
358
|
+
|
|
359
|
+
删除策略(两层查找,更健壮):
|
|
360
|
+
1. 首先从 manifest.schemas 中查找引用(优先)
|
|
361
|
+
2. 如果未找到,扫描 schemas/ 目录(兼容旧项目)
|
|
362
|
+
|
|
363
|
+
副作用:
|
|
364
|
+
- 删除对应的 .schema.yaml 文件
|
|
365
|
+
- 从 manifest.schemas 中移除引用(如果存在)
|
|
366
|
+
|
|
367
|
+
数据流:
|
|
368
|
+
1. 读取 manifest 文件
|
|
369
|
+
2. 在 manifest.schemas 中查找引用
|
|
370
|
+
3. 如果未找到,扫描 schemas/ 目录
|
|
371
|
+
4. 删除找到的 schema 文件
|
|
372
|
+
5. 从 manifest 中移除引用
|
|
373
|
+
6. 保存更新后的 manifest
|
|
374
|
+
|
|
375
|
+
参数:
|
|
376
|
+
table_id: Schema 对应的表 ID
|
|
377
|
+
config_path: 项目配置根目录(通过 Depends 注入)
|
|
378
|
+
|
|
379
|
+
返回:
|
|
380
|
+
StandardResponse: 操作结果消息
|
|
381
|
+
"""
|
|
382
|
+
manifest_path = _v2_manifest_path(config_path)
|
|
383
|
+
if not os.path.isfile(manifest_path):
|
|
384
|
+
raise HTTPException(
|
|
385
|
+
status_code=404,
|
|
386
|
+
detail={
|
|
387
|
+
# §2.1: 结构化错误码统一(与 dependencies.py 的 manifest 缺失出口同构),
|
|
388
|
+
# 前端按 detail.code 识别项目失效并触发自愈
|
|
389
|
+
"code": "PROJECT_NOT_FOUND",
|
|
390
|
+
"message": f"项目配置文件(project.precis.yaml)不存在,请确认项目是否已被移动或删除(配置目录: {manifest_path})",
|
|
391
|
+
"path": str(
|
|
392
|
+
config_path
|
|
393
|
+
), # §2.1: 项目根目录(前端与 activeProjectPath 同口径比对,勿传 manifest 文件路径)
|
|
394
|
+
},
|
|
395
|
+
)
|
|
396
|
+
manifest = ProjectManifestV2.model_validate(read_yaml(Path(manifest_path)))
|
|
397
|
+
|
|
398
|
+
ref = next((s for s in manifest.schemas if s.id == table_id), None)
|
|
399
|
+
if not ref:
|
|
400
|
+
ref = _scan_schema_file(table_id, config_path)
|
|
401
|
+
|
|
402
|
+
if not ref:
|
|
403
|
+
return {"message": f"未找到 schema '{table_id}',无需删除。"}
|
|
404
|
+
|
|
405
|
+
try:
|
|
406
|
+
abs_schema_path = _resolve_project_path(config_path, ref.path)
|
|
407
|
+
except ValueError:
|
|
408
|
+
raise HTTPException(status_code=400, detail="非法的 Schema 文件路径")
|
|
409
|
+
|
|
410
|
+
with project_lock(config_path):
|
|
411
|
+
# §2.6: 引用检查抽公共函数(与 AI DELETE_SCHEMA 共用单一事实源);
|
|
412
|
+
# 消息格式与原三段检查逐段一致
|
|
413
|
+
refs = find_schema_references(config_path, table_id)
|
|
414
|
+
if refs["constraints"]:
|
|
415
|
+
raise HTTPException(
|
|
416
|
+
status_code=409,
|
|
417
|
+
detail=f"Schema '{table_id}' 仍被 constraint {', '.join(refs['constraints'])} 引用,请先删除引用",
|
|
418
|
+
)
|
|
419
|
+
if refs["regex"]:
|
|
420
|
+
raise HTTPException(
|
|
421
|
+
status_code=409,
|
|
422
|
+
detail=f"Schema '{table_id}' 仍被 regex {', '.join(refs['regex'])} 引用,请先删除引用",
|
|
423
|
+
)
|
|
424
|
+
if refs["transforms"]:
|
|
425
|
+
raise HTTPException(
|
|
426
|
+
status_code=409,
|
|
427
|
+
detail=f"Schema '{table_id}' 仍被 transform {', '.join(refs['transforms'])} 引用,请先删除引用",
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
try:
|
|
431
|
+
if os.path.isfile(abs_schema_path):
|
|
432
|
+
os.remove(abs_schema_path)
|
|
433
|
+
except Exception as e:
|
|
434
|
+
raise HTTPException(status_code=500, detail=f"删除 schema 文件失败: {e}")
|
|
435
|
+
|
|
436
|
+
# B-fix: 该引用原本存在于 manifest 中就写回(即使删除后列表为空)。
|
|
437
|
+
# 此前 `if manifest.schemas:` 在删除最后一个引用时为假而跳过写回,
|
|
438
|
+
# manifest 会留下指向已删除文件的悬空引用。
|
|
439
|
+
remaining_schemas = [s for s in manifest.schemas if s.id != table_id]
|
|
440
|
+
if len(remaining_schemas) != len(manifest.schemas):
|
|
441
|
+
manifest.schemas = remaining_schemas
|
|
442
|
+
write_yaml_atomic(Path(manifest_path), manifest.model_dump(exclude_none=True))
|
|
443
|
+
|
|
444
|
+
return {"message": f"V2 schema '{table_id}' 已删除。"}
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
@router.post(
|
|
448
|
+
"/schemas/{table_id}/display-name",
|
|
449
|
+
response_model=StandardResponse,
|
|
450
|
+
summary="更新 Schema 展示名",
|
|
451
|
+
responses={
|
|
452
|
+
404: {"description": "Schema 不存在"},
|
|
453
|
+
500: {"description": "服务器内部错误"},
|
|
454
|
+
},
|
|
455
|
+
)
|
|
456
|
+
def update_v2_schema_display_name(
|
|
457
|
+
table_id: str,
|
|
458
|
+
payload: DisplayNameUpdateRequest,
|
|
459
|
+
config_path: str = Depends(get_project_config_path),
|
|
460
|
+
) -> dict[str, str]:
|
|
461
|
+
"""
|
|
462
|
+
更新 schema 的展示名(不改变 table_id 与文件路径)。
|
|
463
|
+
|
|
464
|
+
设计意图:
|
|
465
|
+
- 允许用户给 Schema 设置友好的显示名称
|
|
466
|
+
- 不改变底层 ID 和文件路径,保持引用完整性
|
|
467
|
+
|
|
468
|
+
副作用:
|
|
469
|
+
- 会触发两次文件写入(读取-修改-保存)
|
|
470
|
+
|
|
471
|
+
数据流:
|
|
472
|
+
1. 调用 get_v2_schema 获取当前 schema
|
|
473
|
+
2. 修改 schema.name 为新名称
|
|
474
|
+
3. 调用 put_v2_schema 保存更改
|
|
475
|
+
|
|
476
|
+
参数:
|
|
477
|
+
table_id: Schema 对应的表 ID
|
|
478
|
+
payload: 新的展示名称
|
|
479
|
+
config_path: 项目配置根目录(通过 Depends 注入)
|
|
480
|
+
|
|
481
|
+
返回:
|
|
482
|
+
StandardResponse: 操作结果消息
|
|
483
|
+
"""
|
|
484
|
+
schema = get_v2_schema(table_id, config_path)
|
|
485
|
+
schema.name = payload.name
|
|
486
|
+
put_v2_schema(table_id, schema, config_path=config_path)
|
|
487
|
+
return {"message": f"V2 schema '{table_id}' 展示名已更新。"}
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
class ImplicitToExplicitBindingRequest(BaseModel):
|
|
491
|
+
"""
|
|
492
|
+
隐式转显式绑定请求模型。
|
|
493
|
+
|
|
494
|
+
设计意图:
|
|
495
|
+
- 前端用户点击"锁定关联"按钮时,将列与正则模式的隐式运行时匹配转为显式配置绑定
|
|
496
|
+
- 显式绑定后,正则节点会参与级联渲染,并在配置文件中持久化
|
|
497
|
+
|
|
498
|
+
字段说明:
|
|
499
|
+
- column_id: 目标列的 ID(或 name,后端会做兼容匹配)
|
|
500
|
+
- pattern_id: 要绑定的正则模式 ID
|
|
501
|
+
- pattern_registry: 模式所在的注册表名称,默认 expression_registry
|
|
502
|
+
"""
|
|
503
|
+
|
|
504
|
+
column_id: str
|
|
505
|
+
pattern_id: str
|
|
506
|
+
pattern_registry: str = "expression_registry"
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
@router.post(
|
|
510
|
+
"/schemas/{table_id}/convert-to-explicit-binding",
|
|
511
|
+
response_model=StandardResponse,
|
|
512
|
+
summary="将隐式正则匹配转换为显式绑定",
|
|
513
|
+
responses={
|
|
514
|
+
400: {"description": "请求参数错误"},
|
|
515
|
+
404: {"description": "Schema 或列不存在"},
|
|
516
|
+
500: {"description": "服务器内部错误"},
|
|
517
|
+
},
|
|
518
|
+
)
|
|
519
|
+
def convert_implicit_to_explicit_binding(
|
|
520
|
+
table_id: str,
|
|
521
|
+
payload: ImplicitToExplicitBindingRequest,
|
|
522
|
+
config_path: str = Depends(get_project_config_path),
|
|
523
|
+
) -> dict[str, str]:
|
|
524
|
+
"""
|
|
525
|
+
将隐式正则匹配转换为显式绑定。
|
|
526
|
+
|
|
527
|
+
设计意图:
|
|
528
|
+
- 用户在属性面板中点击"锁定关联"按钮时触发
|
|
529
|
+
- 将运行时动态解析的隐式匹配转换为配置文件中的显式依赖
|
|
530
|
+
- 转换后,正则节点将参与级联渲染
|
|
531
|
+
|
|
532
|
+
业务逻辑:
|
|
533
|
+
1. 读取当前 schema 文件
|
|
534
|
+
2. 找到对应的列定义
|
|
535
|
+
3. 将列的 type 从隐式转换为显式绑定
|
|
536
|
+
4. 保存更新后的 schema 文件
|
|
537
|
+
|
|
538
|
+
参数:
|
|
539
|
+
table_id: Schema 对应的表 ID
|
|
540
|
+
payload: 转换请求,包含列 ID 和目标正则模式 ID
|
|
541
|
+
config_path: 项目配置根目录
|
|
542
|
+
|
|
543
|
+
返回:
|
|
544
|
+
StandardResponse: 操作结果消息
|
|
545
|
+
"""
|
|
546
|
+
schema = get_v2_schema(table_id, config_path)
|
|
547
|
+
|
|
548
|
+
if not schema.columns:
|
|
549
|
+
raise HTTPException(status_code=404, detail="Schema has no columns")
|
|
550
|
+
|
|
551
|
+
target_column = None
|
|
552
|
+
for col in schema.columns:
|
|
553
|
+
if col.id == payload.column_id or col.name == payload.column_id:
|
|
554
|
+
target_column = col
|
|
555
|
+
break
|
|
556
|
+
|
|
557
|
+
if not target_column:
|
|
558
|
+
raise HTTPException(status_code=404, detail=f"Column '{payload.column_id}' not found")
|
|
559
|
+
|
|
560
|
+
# 更新列的绑定信息为显式 (Expr 字典)
|
|
561
|
+
target_column.type = {"name": "Expr", "registry": payload.pattern_registry, "pattern": payload.pattern_id}
|
|
562
|
+
|
|
563
|
+
# 调用通用的更新方法保存
|
|
564
|
+
put_v2_schema(table_id, schema, SaveMode.OVERWRITE, config_path)
|
|
565
|
+
return {
|
|
566
|
+
"message": f"Column '{payload.column_id}' converted to explicit binding with pattern '{payload.pattern_id}'"
|
|
567
|
+
}
|