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.
Potentially problematic release.
This version of precis-cli might be problematic. Click here for more details.
- 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,126 @@
|
|
|
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
|
+
- 捕获 API 请求处理过程中的未处理异常
|
|
21
|
+
- 将异常转换为统一的 JSON 错误响应
|
|
22
|
+
- 避免异常信息泄露,返回安全的内部服务器错误提示
|
|
23
|
+
|
|
24
|
+
架构设计:
|
|
25
|
+
- 继承 Starlette BaseHTTPMiddleware
|
|
26
|
+
- 通过 dispatch 方法包装请求处理流程
|
|
27
|
+
- 对 HTTPException 直接抛出,其他异常统一捕获并记录日志
|
|
28
|
+
|
|
29
|
+
输入示例:
|
|
30
|
+
正常的 HTTP 请求进入中间件处理流程
|
|
31
|
+
|
|
32
|
+
输出示例:
|
|
33
|
+
当发生未处理异常时返回:
|
|
34
|
+
{"error": "Internal Server Error", "detail": "An unexpected error occurred"}
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
import logging
|
|
38
|
+
import traceback
|
|
39
|
+
from collections.abc import Awaitable, Callable
|
|
40
|
+
|
|
41
|
+
from fastapi import HTTPException
|
|
42
|
+
from fastapi.exceptions import RequestValidationError
|
|
43
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
44
|
+
from starlette.requests import Request
|
|
45
|
+
from starlette.responses import JSONResponse, Response
|
|
46
|
+
|
|
47
|
+
from app.shared.core.pydantic_messages import localize_pydantic_msg
|
|
48
|
+
|
|
49
|
+
logger = logging.getLogger(__name__)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
async def request_validation_exception_handler(request: Request, exc: Exception) -> JSONResponse:
|
|
53
|
+
"""422 请求校验错误的全局 handler。
|
|
54
|
+
|
|
55
|
+
保持 FastAPI 默认的响应结构(detail 为 loc/msg/type 数组,前端按此解析),
|
|
56
|
+
仅把每条 msg 本地化为中文——否则 Pydantic 的英文校验消息
|
|
57
|
+
("Input should be a valid integer" 等)会经前端原样展示给用户。
|
|
58
|
+
|
|
59
|
+
签名收窄说明:Starlette 的 add_exception_handler 要求
|
|
60
|
+
Callable[[Request, Exception], ...](参数逆变),用具体异常类型作
|
|
61
|
+
参数注解会挂 mypy 全量检查,故收宽为 Exception 后 isinstance 收窄。
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
request: 当前请求
|
|
65
|
+
exc: FastAPI 请求体/参数校验错误
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
422 JSONResponse,结构与 FastAPI 默认一致
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
exc: 非 RequestValidationError 时原样上抛(注册绑定保证了不会走到)
|
|
72
|
+
"""
|
|
73
|
+
if not isinstance(exc, RequestValidationError):
|
|
74
|
+
raise exc
|
|
75
|
+
errors = []
|
|
76
|
+
for err in exc.errors():
|
|
77
|
+
item = dict(err)
|
|
78
|
+
item["msg"] = localize_pydantic_msg(str(item.get("msg", "")))
|
|
79
|
+
errors.append(item)
|
|
80
|
+
return JSONResponse(status_code=422, content={"detail": errors})
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class ExceptionHandlerMiddleware(BaseHTTPMiddleware):
|
|
84
|
+
"""
|
|
85
|
+
全局异常处理中间件
|
|
86
|
+
|
|
87
|
+
继承自 Starlette 的 BaseHTTPMiddleware,通过包装请求处理流程,
|
|
88
|
+
捕获所有未处理的异常并转换为统一的 JSON 错误响应。
|
|
89
|
+
|
|
90
|
+
处理策略:
|
|
91
|
+
- HTTPException: 直接向上抛出,由 FastAPI 的默认异常处理器处理
|
|
92
|
+
- 其他 Exception: 记录完整堆栈日志,返回安全的 500 错误响应(避免泄露敏感信息)
|
|
93
|
+
|
|
94
|
+
Attributes:
|
|
95
|
+
无额外属性,依赖父类 BaseHTTPMiddleware 的基础设施
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
async def dispatch(self, request: Request, call_next: Callable[[Request], Awaitable[Response]]) -> Response:
|
|
99
|
+
"""
|
|
100
|
+
分发并包装请求处理流程,捕获未处理异常
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
request: 当前 HTTP 请求对象,包含方法、路径、头信息等
|
|
104
|
+
call_next: 下一个中间件或路由处理器的可调用对象
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Response: 正常响应或异常时的 JSONResponse(500 状态码)
|
|
108
|
+
|
|
109
|
+
Raises:
|
|
110
|
+
HTTPException: FastAPI 的 HTTP 异常直接向上抛出,不做拦截
|
|
111
|
+
"""
|
|
112
|
+
try:
|
|
113
|
+
# 调用下游中间件或路由处理器,获取正常响应
|
|
114
|
+
response = await call_next(request)
|
|
115
|
+
return response
|
|
116
|
+
except HTTPException:
|
|
117
|
+
# FastAPI 的 HTTPException 直接抛出,由上层默认处理器生成标准 HTTP 错误响应
|
|
118
|
+
raise
|
|
119
|
+
except Exception:
|
|
120
|
+
# 捕获所有未预料的异常,记录完整堆栈以便排查问题
|
|
121
|
+
logger.error(f"Unhandled exception: {traceback.format_exc()}")
|
|
122
|
+
# 返回安全的通用错误响应,避免将内部异常详情暴露给客户端
|
|
123
|
+
return JSONResponse(
|
|
124
|
+
status_code=500,
|
|
125
|
+
content={"error": "Internal Server Error", "detail": "服务器内部错误,请稍后重试"},
|
|
126
|
+
)
|
|
@@ -0,0 +1,97 @@
|
|
|
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
|
+
- 记录所有 API 请求的访问日志
|
|
21
|
+
- 计算并输出请求处理耗时
|
|
22
|
+
- 根据响应状态码分级输出日志(info/warning/error)
|
|
23
|
+
|
|
24
|
+
架构设计:
|
|
25
|
+
- 继承 Starlette BaseHTTPMiddleware
|
|
26
|
+
- 在请求前后记录时间戳计算耗时
|
|
27
|
+
- 使用 Python logging 模块分级输出
|
|
28
|
+
|
|
29
|
+
输入示例:
|
|
30
|
+
正常的 HTTP 请求进入中间件处理流程
|
|
31
|
+
|
|
32
|
+
输出示例:
|
|
33
|
+
日志输出: "GET /api/validation 200 15.32ms"
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
import logging
|
|
37
|
+
import time
|
|
38
|
+
from collections.abc import Awaitable, Callable
|
|
39
|
+
|
|
40
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
41
|
+
from starlette.requests import Request
|
|
42
|
+
from starlette.responses import Response
|
|
43
|
+
|
|
44
|
+
logger = logging.getLogger(__name__)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class RequestLoggingMiddleware(BaseHTTPMiddleware):
|
|
48
|
+
"""
|
|
49
|
+
请求日志记录中间件
|
|
50
|
+
|
|
51
|
+
继承自 Starlette 的 BaseHTTPMiddleware,在请求处理前后记录时间戳,
|
|
52
|
+
计算请求处理耗时,并根据响应状态码分级输出日志(info/warning/error)。
|
|
53
|
+
|
|
54
|
+
日志分级策略:
|
|
55
|
+
- 2xx/3xx: info 级别(正常请求)
|
|
56
|
+
- 4xx: warning 级别(客户端错误,如参数校验失败)
|
|
57
|
+
- 5xx: error 级别(服务器内部错误)
|
|
58
|
+
|
|
59
|
+
Attributes:
|
|
60
|
+
无额外属性,依赖父类 BaseHTTPMiddleware 的基础设施
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
async def dispatch(self, request: Request, call_next: Callable[[Request], Awaitable[Response]]) -> Response:
|
|
64
|
+
"""
|
|
65
|
+
分发并包装请求处理流程,记录访问日志和耗时
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
request: 当前 HTTP 请求对象,包含 method、url、headers 等信息
|
|
69
|
+
call_next: 下一个中间件或路由处理器的可调用对象
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Response: 下游处理返回的响应对象(原样返回,不做修改)
|
|
73
|
+
"""
|
|
74
|
+
# 记录请求开始时间戳(秒级浮点数)
|
|
75
|
+
start_time = time.time()
|
|
76
|
+
|
|
77
|
+
# 调用下游中间件或路由处理器,获取响应
|
|
78
|
+
response = await call_next(request)
|
|
79
|
+
|
|
80
|
+
# 计算请求处理耗时,转换为毫秒(保留两位小数)
|
|
81
|
+
duration_ms = (time.time() - start_time) * 1000
|
|
82
|
+
|
|
83
|
+
# 拼接日志消息:方法 路径 状态码 耗时
|
|
84
|
+
log_msg = f"{request.method} {request.url.path} {response.status_code} {duration_ms:.2f}ms"
|
|
85
|
+
|
|
86
|
+
# 根据 HTTP 状态码分级输出日志:
|
|
87
|
+
# 5xx 表示服务器内部错误,使用 error 级别
|
|
88
|
+
if response.status_code >= 500:
|
|
89
|
+
logger.error(log_msg)
|
|
90
|
+
# 4xx 表示客户端错误(如 400 Bad Request, 422 Validation Error),使用 warning 级别
|
|
91
|
+
elif response.status_code >= 400:
|
|
92
|
+
logger.warning(log_msg)
|
|
93
|
+
# 2xx/3xx 表示正常响应,使用 info 级别
|
|
94
|
+
else:
|
|
95
|
+
logger.info(log_msg)
|
|
96
|
+
|
|
97
|
+
return response
|
|
@@ -0,0 +1,215 @@
|
|
|
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 后端 API 一次性 token 放行中间件(打包模式 CORS 纵深防御)
|
|
18
|
+
|
|
19
|
+
功能概述:
|
|
20
|
+
- Electron 打包模式每次启动后端生成随机 token:主进程经环境变量 PRECIS_API_TOKEN
|
|
21
|
+
注入本进程,并经 IPC 下发给本应用渲染进程;渲染进程随请求携带 X-Precis-Auth 头
|
|
22
|
+
- 对携带有效 token 的请求放行 CORS:
|
|
23
|
+
* 预检 OPTIONS(Origin 为 null/空/非常规本机源)直接代答 200 预检响应
|
|
24
|
+
* 实际请求在响应缺少 Access-Control-Allow-Origin 时补写回显 Origin 的 CORS 头
|
|
25
|
+
- 无效/缺失 token 的请求不介入,交由 CORSMiddleware 既有规则处理
|
|
26
|
+
(null Origin 仍被拒绝,本机源正常放行)
|
|
27
|
+
|
|
28
|
+
威胁模型:
|
|
29
|
+
- 后端无鉴权且存在文件读取端点,监听 127.0.0.1 动态端口;浏览器中的恶意网页可用
|
|
30
|
+
sandboxed iframe(Origin 恒为 null)+ 端口扫描对任意本机端口发跨域请求,若后端
|
|
31
|
+
对 null Origin 一律回写 ACAO,响应即可被该网页读取(本机数据泄露)
|
|
32
|
+
- 旧方案(PRECIS_ALLOW_NULL_ORIGIN=1 全局放行)对本应用页面与恶意网页不加区分;
|
|
33
|
+
新方案下恶意网页拿不到 token(IPC 仅本应用渲染进程可达),其 null Origin 请求
|
|
34
|
+
依旧被 CORS 拒绝——token 成为"本应用页面"的身份凭据
|
|
35
|
+
- token 比较使用 hmac.compare_digest,避免逐字节比较的时序侧信道
|
|
36
|
+
|
|
37
|
+
生效条件:
|
|
38
|
+
- 仅当 PRECIS_API_TOKEN 已配置(Electron spawn 后端时注入)时介入;Web / 开发模式
|
|
39
|
+
(未配置)完全直通,既有 localhost CORS 行为不变
|
|
40
|
+
- PRECIS_ALLOW_NULL_ORIGIN 兼容开关保留(见 DynamicPortCORSMiddleware 原逻辑),
|
|
41
|
+
打包模式不再注入该变量
|
|
42
|
+
|
|
43
|
+
架构设计:
|
|
44
|
+
- 纯 ASGI 中间件(不继承 BaseHTTPMiddleware):无请求体/响应体解析开销,
|
|
45
|
+
且预检代答只需两个 ASGI send 消息即可完成
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
import hmac
|
|
49
|
+
import os
|
|
50
|
+
import re
|
|
51
|
+
from collections.abc import MutableMapping
|
|
52
|
+
from typing import Any
|
|
53
|
+
|
|
54
|
+
from starlette.datastructures import Headers
|
|
55
|
+
from starlette.types import ASGIApp, Receive, Scope, Send
|
|
56
|
+
|
|
57
|
+
# 携带 token 的自定义请求头(渲染进程 httpClient / sseClient 统一注入)
|
|
58
|
+
AUTH_HEADER_NAME = "X-Precis-Auth"
|
|
59
|
+
|
|
60
|
+
# "常规本机源":http(s)://127.0.0.1[:port] 或 http(s)://localhost[:port]。
|
|
61
|
+
# 这类 Origin 交给 CORSMiddleware 既有规则放行(动态端口正则),无需 token 代答。
|
|
62
|
+
_LOCAL_ORIGIN_RE = re.compile(r"^https?://(127\.0\.0\.1|localhost)(:\d+)?$", re.IGNORECASE)
|
|
63
|
+
|
|
64
|
+
# 预检代答回写的允许头列表(本应用实际使用的全部自定义头 + 常规 Content-Type)
|
|
65
|
+
_ALLOWED_HEADERS = "X-Precis-Auth, X-Project-Config-Path, Content-Type"
|
|
66
|
+
# 预检代答回写的允许方法列表
|
|
67
|
+
_ALLOWED_METHODS = "GET, POST, PUT, DELETE, OPTIONS"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class TokenOriginAuthMiddleware:
|
|
71
|
+
"""
|
|
72
|
+
一次性 token 放行中间件(纯 ASGI)
|
|
73
|
+
|
|
74
|
+
处理流程:
|
|
75
|
+
1. 非 HTTP scope 或未配置 PRECIS_API_TOKEN → 完全直通
|
|
76
|
+
2. X-Precis-Auth 与环境 token 不等(含缺失)→ 完全直通
|
|
77
|
+
(null Origin 预检/请求由 CORSMiddleware 按既有规则拒绝)
|
|
78
|
+
3. OPTIONS 预检 + 非常规本机源 + 预检声明携带 X-Precis-Auth → 直接代答 200
|
|
79
|
+
4. 其余有效 token 请求 → 透传,响应缺少 ACAO 头时补写回显 Origin 的 CORS 头
|
|
80
|
+
|
|
81
|
+
补写时机说明:
|
|
82
|
+
- 本中间件在 add_middleware 顺序上位于 CORSMiddleware 之后添加(先执行,外层),
|
|
83
|
+
能看到 CORS 中间件处理后的响应头;仅当其未回写 ACAO(即 Origin 未被放行,
|
|
84
|
+
如 null Origin)时才补写,避免与 app://. / localhost 等 Origin 的既有
|
|
85
|
+
ACAO 头重复(重复 ACAO 头会被浏览器判定为 CORS 失败)
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
def __init__(self, app: ASGIApp) -> None:
|
|
89
|
+
self.app = app
|
|
90
|
+
|
|
91
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
92
|
+
# 非 HTTP scope(websocket/lifespan)直接透传
|
|
93
|
+
if scope["type"] != "http":
|
|
94
|
+
await self.app(scope, receive, send)
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
# token 逐请求读取(与 PRECIS_ALLOW_NULL_ORIGIN 的读取时机一致),
|
|
98
|
+
# 未配置即 Web / 开发模式,中间件完全直通
|
|
99
|
+
token = os.environ.get("PRECIS_API_TOKEN", "").strip()
|
|
100
|
+
if not token:
|
|
101
|
+
await self.app(scope, receive, send)
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
headers = Headers(scope=scope)
|
|
105
|
+
origin = headers.get("origin", "")
|
|
106
|
+
request_token = headers.get(AUTH_HEADER_NAME, "")
|
|
107
|
+
|
|
108
|
+
# token 比较走 compare_digest 防时序侧信道;编码为 bytes 以兼容任意请求头值
|
|
109
|
+
if not hmac.compare_digest(request_token.encode("utf-8"), token.encode("utf-8")):
|
|
110
|
+
# 无效/缺失 token:不介入。null Origin 的预检与实际请求由
|
|
111
|
+
# CORSMiddleware 按既有规则拒绝(不回写任何 CORS 头 / 预检 400)
|
|
112
|
+
await self.app(scope, receive, send)
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
# 预检代答:仅针对"非常规本机源"(null/空/app:// 等无法被既有 CORS 规则
|
|
116
|
+
# 放行的 Origin),且预检请求声明将携带 X-Precis-Auth 头。常规本机源的
|
|
117
|
+
# 预检直接透传,由 CORSMiddleware 按既有规则应答(行为不变)。
|
|
118
|
+
if (
|
|
119
|
+
scope["method"] == "OPTIONS"
|
|
120
|
+
and not _LOCAL_ORIGIN_RE.match(origin)
|
|
121
|
+
and self._preflight_declares_auth_header(headers)
|
|
122
|
+
):
|
|
123
|
+
await self._send_preflight(send, origin)
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
# OPTIONS 一律透传,不再补写头:预检要么已被上面代答放行,要么由
|
|
127
|
+
# CORSMiddleware 按既有规则应答(本机源放行;携带有效 token 但未声明
|
|
128
|
+
# X-Precis-Auth 的非本机源预检被拒绝 400)——拒绝响应必须保持无 ACAO,
|
|
129
|
+
# 否则"拒绝"语义被破坏且响应头自相矛盾。
|
|
130
|
+
if scope["method"] == "OPTIONS":
|
|
131
|
+
await self.app(scope, receive, send)
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
# 有效 token 的实际请求:透传给下游(CORS + 路由),响应缺少 ACAO 时补写
|
|
135
|
+
await self._call_with_cors_echo(scope, receive, send, origin)
|
|
136
|
+
|
|
137
|
+
@staticmethod
|
|
138
|
+
def _preflight_declares_auth_header(headers: Headers) -> bool:
|
|
139
|
+
"""
|
|
140
|
+
@methoddesc 判断预检请求的 Access-Control-Request-Headers 是否声明了 token 头
|
|
141
|
+
|
|
142
|
+
业务用途:
|
|
143
|
+
- 浏览器预检会列出实际请求将携带的非简单头;仅当其中包含 X-Precis-Auth
|
|
144
|
+
时才需要代答(实际请求才能真正带上 token)
|
|
145
|
+
|
|
146
|
+
参数:
|
|
147
|
+
headers: ASGI 请求头(大小写不敏感访问)
|
|
148
|
+
|
|
149
|
+
返回:
|
|
150
|
+
True 表示预检声明将携带 X-Precis-Auth 头
|
|
151
|
+
"""
|
|
152
|
+
declared = headers.get("access-control-request-headers", "")
|
|
153
|
+
declared_names = {name.strip().lower() for name in declared.split(",") if name.strip()}
|
|
154
|
+
return AUTH_HEADER_NAME.lower() in declared_names
|
|
155
|
+
|
|
156
|
+
@staticmethod
|
|
157
|
+
async def _send_preflight(send: Send, origin: str) -> None:
|
|
158
|
+
"""
|
|
159
|
+
@methoddesc 直接代答 CORS 预检(200 + 回显 Origin 的放行头)
|
|
160
|
+
|
|
161
|
+
业务用途:
|
|
162
|
+
- null Origin 的预检会被 CORSMiddleware 拒绝(400),本应用打包模式页面
|
|
163
|
+
(app:// 协议)恰是 null Origin,故对携带有效 token 的预检在此代答放行
|
|
164
|
+
|
|
165
|
+
参数:
|
|
166
|
+
send: ASGI send 可调用对象
|
|
167
|
+
origin: 请求 Origin 原样回显(null / app://. 等)
|
|
168
|
+
"""
|
|
169
|
+
response_headers = [
|
|
170
|
+
(b"access-control-allow-origin", origin.encode("latin-1")),
|
|
171
|
+
(b"access-control-allow-headers", _ALLOWED_HEADERS.encode("latin-1")),
|
|
172
|
+
(b"access-control-allow-methods", _ALLOWED_METHODS.encode("latin-1")),
|
|
173
|
+
(b"access-control-allow-credentials", b"true"),
|
|
174
|
+
(b"vary", b"Origin"),
|
|
175
|
+
(b"content-length", b"0"),
|
|
176
|
+
]
|
|
177
|
+
await send({"type": "http.response.start", "status": 200, "headers": response_headers})
|
|
178
|
+
await send({"type": "http.response.body", "body": b""})
|
|
179
|
+
|
|
180
|
+
async def _call_with_cors_echo(self, scope: Scope, receive: Receive, send: Send, origin: str) -> None:
|
|
181
|
+
"""
|
|
182
|
+
@methoddesc 透传请求,并在响应缺少 ACAO 头时补写回显 Origin 的 CORS 头
|
|
183
|
+
|
|
184
|
+
业务用途:
|
|
185
|
+
- 有效 token 的实际请求(如 null Origin 的 GET)路由层正常处理,但
|
|
186
|
+
CORSMiddleware 不会为 null Origin 回写 ACAO,浏览器会拦截响应读取;
|
|
187
|
+
此处在响应阶段补写 ACAO(回显 Origin)+ Allow-Credentials。
|
|
188
|
+
对路由返回的 4xx/5xx 同样补写——前端需要能读到错误响应体
|
|
189
|
+
|
|
190
|
+
参数:
|
|
191
|
+
scope/receive/send: 标准 ASGI 三元组
|
|
192
|
+
origin: 请求 Origin(无 Origin 头时为空串,此时不补写任何 CORS 头)
|
|
193
|
+
|
|
194
|
+
[去重说明]
|
|
195
|
+
- allow_credentials=True 时 CORSMiddleware 对所有实际请求都会回写
|
|
196
|
+
access-control-allow-credentials(即使 Origin 未放行),因此每个待补写头
|
|
197
|
+
都必须先做存在性检查——重复的 CORS 头会被浏览器判定为 CORS 检查失败
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
async def send_wrapper(message: MutableMapping[str, Any]) -> None:
|
|
201
|
+
if message["type"] == "http.response.start" and origin:
|
|
202
|
+
response_headers: list[tuple[bytes, bytes]] = list(message.get("headers", []))
|
|
203
|
+
header_names = {key.decode("latin-1").lower() for key, _ in response_headers}
|
|
204
|
+
if "access-control-allow-origin" not in header_names:
|
|
205
|
+
additions: list[tuple[bytes, bytes]] = [(b"access-control-allow-origin", origin.encode("latin-1"))]
|
|
206
|
+
if "access-control-allow-credentials" not in header_names:
|
|
207
|
+
additions.append((b"access-control-allow-credentials", b"true"))
|
|
208
|
+
if "vary" not in header_names:
|
|
209
|
+
additions.append((b"vary", b"Origin"))
|
|
210
|
+
response_headers.extend(additions)
|
|
211
|
+
message = dict(message)
|
|
212
|
+
message["headers"] = response_headers
|
|
213
|
+
await send(message)
|
|
214
|
+
|
|
215
|
+
await self.app(scope, receive, send_wrapper)
|
|
@@ -0,0 +1,151 @@
|
|
|
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/models/__init__.py
|
|
17
|
+
"""
|
|
18
|
+
@fileoverview API 模型包入口
|
|
19
|
+
|
|
20
|
+
功能概述:
|
|
21
|
+
- 作为 API 数据模型的统一出口,聚合所有子模块的模型定义
|
|
22
|
+
- 通过从子模块导入再导出的方式,保持向后兼容性
|
|
23
|
+
- 定义 __all__ 公开 API 列表
|
|
24
|
+
|
|
25
|
+
输入示例:
|
|
26
|
+
from app.api.models import ProjectDetail, WorkspaceConfig, FullValidationResponse
|
|
27
|
+
|
|
28
|
+
输出示例:
|
|
29
|
+
可直接访问所有聚合的模型类:
|
|
30
|
+
- ProjectDetail, ProjectConfigModel, PathsModel, StandardResponse
|
|
31
|
+
- EndpointModel, ConnectionRuleModel, ConnectionRuleConfigModel, ConnectionRulesModel
|
|
32
|
+
- ExternalDataSource, WorkspaceConfig, UIPreferences
|
|
33
|
+
- SchemaSaveRequest, SchemaSaveResponse, HeaderRowChangedRequest
|
|
34
|
+
- ValidationRequest, ValidationResponse, ValidationType
|
|
35
|
+
- FullValidationRequest, FullValidationResponse, FullValidationSummary
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
# 从各子模块导入模型,保持向后兼容
|
|
39
|
+
# ============================================================================
|
|
40
|
+
# 项目配置模型
|
|
41
|
+
# ============================================================================
|
|
42
|
+
# ============================================================================
|
|
43
|
+
# 连接规则模型
|
|
44
|
+
# ============================================================================
|
|
45
|
+
from .connection_rules import (
|
|
46
|
+
ConnectionRuleConfigModel,
|
|
47
|
+
ConnectionRuleModel,
|
|
48
|
+
ConnectionRulesModel,
|
|
49
|
+
EndpointModel,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# ============================================================================
|
|
53
|
+
# 全量校验模型
|
|
54
|
+
# ============================================================================
|
|
55
|
+
from .full_validation import (
|
|
56
|
+
FullValidationErrorItem,
|
|
57
|
+
FullValidationOptions,
|
|
58
|
+
FullValidationRequest,
|
|
59
|
+
FullValidationResponse,
|
|
60
|
+
FullValidationSummary,
|
|
61
|
+
ValidationPassedItem,
|
|
62
|
+
ValidationStatistics,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# ============================================================================
|
|
66
|
+
# 数据预览模型
|
|
67
|
+
# ============================================================================
|
|
68
|
+
# 注:FilePreviewResponse 定义在 app/api.routers.preview.models(路由侧实际使用的超集版本)
|
|
69
|
+
from .project import (
|
|
70
|
+
PathsModel,
|
|
71
|
+
ProjectConfigModel,
|
|
72
|
+
ProjectDetail,
|
|
73
|
+
StandardResponse,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# ============================================================================
|
|
77
|
+
# Schema 操作模型
|
|
78
|
+
# ============================================================================
|
|
79
|
+
from .schema import (
|
|
80
|
+
HeaderRowChangedRequest,
|
|
81
|
+
HeaderRowChangedResponse,
|
|
82
|
+
SchemaSaveRequest,
|
|
83
|
+
SchemaSaveResponse,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# ============================================================================
|
|
87
|
+
# 校验模型
|
|
88
|
+
# ============================================================================
|
|
89
|
+
from .validation import (
|
|
90
|
+
InlineValidationRequest,
|
|
91
|
+
RegexValidationErrorRow,
|
|
92
|
+
RegexValidationRequest,
|
|
93
|
+
RegexValidationResponse,
|
|
94
|
+
RegexValidationResult,
|
|
95
|
+
ValidationErrorRow,
|
|
96
|
+
ValidationRequest,
|
|
97
|
+
ValidationResponse,
|
|
98
|
+
ValidationResult,
|
|
99
|
+
ValidationType,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# ============================================================================
|
|
103
|
+
# 工作区模型
|
|
104
|
+
# ============================================================================
|
|
105
|
+
from .workspace import (
|
|
106
|
+
ExternalDataSource,
|
|
107
|
+
UIPreferences,
|
|
108
|
+
WorkspaceConfig,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# 定义公开的 API 列表
|
|
112
|
+
__all__ = [
|
|
113
|
+
# 项目配置模型
|
|
114
|
+
"ProjectDetail",
|
|
115
|
+
"PathsModel",
|
|
116
|
+
"ProjectConfigModel",
|
|
117
|
+
"StandardResponse",
|
|
118
|
+
# 连接规则模型
|
|
119
|
+
"EndpointModel",
|
|
120
|
+
"ConnectionRuleConfigModel",
|
|
121
|
+
"ConnectionRuleModel",
|
|
122
|
+
"ConnectionRulesModel",
|
|
123
|
+
# 工作区模型
|
|
124
|
+
"ExternalDataSource",
|
|
125
|
+
"UIPreferences",
|
|
126
|
+
"WorkspaceConfig",
|
|
127
|
+
# Schema 操作模型
|
|
128
|
+
"SchemaSaveRequest",
|
|
129
|
+
"SchemaSaveResponse",
|
|
130
|
+
"HeaderRowChangedRequest",
|
|
131
|
+
"HeaderRowChangedResponse",
|
|
132
|
+
# 校验模型
|
|
133
|
+
"InlineValidationRequest",
|
|
134
|
+
"RegexValidationRequest",
|
|
135
|
+
"RegexValidationResponse",
|
|
136
|
+
"RegexValidationResult",
|
|
137
|
+
"RegexValidationErrorRow",
|
|
138
|
+
"ValidationRequest",
|
|
139
|
+
"ValidationResponse",
|
|
140
|
+
"ValidationResult",
|
|
141
|
+
"ValidationErrorRow",
|
|
142
|
+
"ValidationType",
|
|
143
|
+
# 全量校验模型
|
|
144
|
+
"FullValidationRequest",
|
|
145
|
+
"FullValidationOptions",
|
|
146
|
+
"FullValidationSummary",
|
|
147
|
+
"FullValidationErrorItem",
|
|
148
|
+
"FullValidationResponse",
|
|
149
|
+
"ValidationPassedItem",
|
|
150
|
+
"ValidationStatistics",
|
|
151
|
+
]
|
|
@@ -0,0 +1,98 @@
|
|
|
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/models/connection_rules.py
|
|
17
|
+
"""
|
|
18
|
+
@fileoverview 连接规则 API 模型
|
|
19
|
+
|
|
20
|
+
功能概述:
|
|
21
|
+
- 定义连接规则相关的 Pydantic 模型
|
|
22
|
+
- 被 connection_rules 路由及外部调用方复用
|
|
23
|
+
|
|
24
|
+
输入示例:
|
|
25
|
+
{
|
|
26
|
+
"version": "1.0",
|
|
27
|
+
"rules": [
|
|
28
|
+
{
|
|
29
|
+
"id": "schema_to_constraint",
|
|
30
|
+
"name": "Schema -> Constraint",
|
|
31
|
+
"source": {"node_types": ["SchemaNode"]},
|
|
32
|
+
"target": {"node_types": ["ConstraintNode"]}
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
from pydantic import BaseModel, Field
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class EndpointModel(BaseModel):
|
|
44
|
+
"""
|
|
45
|
+
连接端点模型。
|
|
46
|
+
|
|
47
|
+
定义连接规则中源端点或目标端点允许连接的节点类型和 handle。
|
|
48
|
+
- node_types: 允许连接的节点类型列表,如 ["SchemaNode", "ConstraintNode"]
|
|
49
|
+
- handles: 可选,限制只允许特定的 handle ID 建立连接
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
node_types: list[str] = Field(..., description="允许的节点类型列表")
|
|
53
|
+
handles: list[str] | None = Field(default=None, description="允许的 handle ID 列表")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class ConnectionRuleConfigModel(BaseModel):
|
|
57
|
+
"""
|
|
58
|
+
连接规则配置模型。
|
|
59
|
+
|
|
60
|
+
定义单条连接规则的附加行为:
|
|
61
|
+
- allow_multiple: 是否允许一个源端点同时连接多个目标端点
|
|
62
|
+
- validation_mode: 验证严格程度,strict 表示完全匹配,loose 表示允许模糊匹配
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
allow_multiple: bool | None = Field(default=True, description="是否允许多个连接")
|
|
66
|
+
validation_mode: str | None = Field(default="strict", description="验证模式: strict/loose")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class ConnectionRuleModel(BaseModel):
|
|
70
|
+
"""
|
|
71
|
+
单条连接规则模型。
|
|
72
|
+
|
|
73
|
+
定义画布中哪类节点可以连接到哪类节点:
|
|
74
|
+
- id: 规则的唯一标识,用于前端快速查找和调试
|
|
75
|
+
- name: 规则的显示名称,供用户阅读
|
|
76
|
+
- source: 源端点定义(哪些节点可以作为连接起点)
|
|
77
|
+
- target: 目标端点定义(哪些节点可以作为连接终点)
|
|
78
|
+
- config: 可选的附加配置(如是否允许多连、验证模式)
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
id: str = Field(..., description="规则唯一标识")
|
|
82
|
+
name: str = Field(..., description="规则名称")
|
|
83
|
+
source: EndpointModel = Field(..., description="源端点定义")
|
|
84
|
+
target: EndpointModel = Field(..., description="目标端点定义")
|
|
85
|
+
config: ConnectionRuleConfigModel | None = Field(default=None, description="规则配置")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class ConnectionRulesModel(BaseModel):
|
|
89
|
+
"""
|
|
90
|
+
连接规则集合模型。
|
|
91
|
+
|
|
92
|
+
作为 connection-rules.precis.yaml 文件的根对象:
|
|
93
|
+
- version: 规则文件格式版本,便于后续格式升级时做兼容处理
|
|
94
|
+
- rules: 具体的连接规则列表,按顺序依次匹配
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
version: str = Field(default="1.0", description="规则文件版本")
|
|
98
|
+
rules: list[ConnectionRuleModel] = Field(default=[], description="规则列表")
|