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,263 @@
|
|
|
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 路由模块
|
|
18
|
+
|
|
19
|
+
功能概述:
|
|
20
|
+
- 提供校验结果报告配置的读取和修改接口
|
|
21
|
+
- 支持多种报告渠道:本地文件、邮件、企业微信、飞书、钉钉
|
|
22
|
+
- 管理各报告服务的启用/禁用状态和详细配置
|
|
23
|
+
|
|
24
|
+
架构设计:
|
|
25
|
+
- 每种报告渠道对应独立的 Pydantic 配置模型
|
|
26
|
+
- 配置持久化为项目目录下的 reporting_config.yaml
|
|
27
|
+
- 不存在的配置返回默认值(local_file 启用,其余禁用)
|
|
28
|
+
|
|
29
|
+
输入示例:
|
|
30
|
+
POST /reporting/config
|
|
31
|
+
{
|
|
32
|
+
"reporters": {
|
|
33
|
+
"local_file": {"enabled": true, "log_dir": "logs"},
|
|
34
|
+
"email": {"enabled": false}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
输出示例:
|
|
39
|
+
{"message": "报告配置已成功更新。"}
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
import os
|
|
43
|
+
from pathlib import Path
|
|
44
|
+
|
|
45
|
+
import yaml
|
|
46
|
+
from fastapi import APIRouter, Depends, HTTPException
|
|
47
|
+
from pydantic import BaseModel, EmailStr, Field
|
|
48
|
+
|
|
49
|
+
from app.api.dependencies import ProjectStore, get_project_store
|
|
50
|
+
from app.api.services.io_error_messages import describe_io_error
|
|
51
|
+
from app.shared.core.io.yaml import write_yaml_atomic
|
|
52
|
+
|
|
53
|
+
router = APIRouter(prefix="/api/latest")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class LocalFileConfig(BaseModel):
|
|
57
|
+
"""本地文件报告配置
|
|
58
|
+
|
|
59
|
+
将校验结果输出到本地日志文件。
|
|
60
|
+
|
|
61
|
+
字段说明:
|
|
62
|
+
enabled: 是否启用本地文件报告
|
|
63
|
+
log_dir: 日志文件存放目录(相对于项目路径)
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
enabled: bool
|
|
67
|
+
log_dir: str = "logs"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class EmailConfig(BaseModel):
|
|
71
|
+
"""邮件报告配置
|
|
72
|
+
|
|
73
|
+
通过 SMTP 服务器发送校验结果邮件。
|
|
74
|
+
|
|
75
|
+
字段说明:
|
|
76
|
+
enabled: 是否启用邮件报告
|
|
77
|
+
smtp_server: SMTP 服务器地址
|
|
78
|
+
smtp_port: SMTP 服务器端口
|
|
79
|
+
sender_email: 发件人邮箱
|
|
80
|
+
sender_password: 发件人邮箱密码
|
|
81
|
+
receiver_email: 收件人邮箱
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
enabled: bool
|
|
85
|
+
smtp_server: str | None = None
|
|
86
|
+
smtp_port: int | None = None
|
|
87
|
+
sender_email: EmailStr | None = None
|
|
88
|
+
sender_password: str | None = None
|
|
89
|
+
receiver_email: EmailStr | None = None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class WeComAppConfig(BaseModel):
|
|
93
|
+
"""企业微信应用报告配置
|
|
94
|
+
|
|
95
|
+
通过企业微信应用发送校验结果消息。
|
|
96
|
+
|
|
97
|
+
字段说明:
|
|
98
|
+
enabled: 是否启用企业微信报告
|
|
99
|
+
corp_id: 企业ID
|
|
100
|
+
corp_secret: 应用Secret
|
|
101
|
+
agent_id: 应用ID
|
|
102
|
+
touser: 接收消息的成员ID列表,多个用'|'分隔,默认@all(所有人)
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
enabled: bool
|
|
106
|
+
corp_id: str | None = Field(None, description="企业ID")
|
|
107
|
+
corp_secret: str | None = Field(None, description="应用Secret")
|
|
108
|
+
agent_id: int | None = Field(None, description="应用ID")
|
|
109
|
+
touser: str | None = Field("@all", description="接收消息的成员ID列表,多个用'|'分隔")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class FeishuAppConfig(BaseModel):
|
|
113
|
+
"""飞书应用报告配置
|
|
114
|
+
|
|
115
|
+
通过飞书应用发送校验结果消息。
|
|
116
|
+
|
|
117
|
+
字段说明:
|
|
118
|
+
enabled: 是否启用飞书报告
|
|
119
|
+
app_id: 应用App ID
|
|
120
|
+
app_secret: 应用App Secret
|
|
121
|
+
receive_id_type: 接收者ID类型(open_id/user_id/email)
|
|
122
|
+
receive_ids: 接收者ID,多个用','分隔
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
enabled: bool
|
|
126
|
+
app_id: str | None = Field(None, description="应用App ID")
|
|
127
|
+
app_secret: str | None = Field(None, description="应用App Secret")
|
|
128
|
+
receive_id_type: str | None = Field("open_id", description="接收者ID类型, 例如: open_id, user_id, email")
|
|
129
|
+
receive_ids: str | None = Field(None, description="接收者ID, 多个用','分隔")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class DingTalkAppConfig(BaseModel):
|
|
133
|
+
"""钉钉应用报告配置
|
|
134
|
+
|
|
135
|
+
通过钉钉应用发送校验结果消息。
|
|
136
|
+
|
|
137
|
+
字段说明:
|
|
138
|
+
enabled: 是否启用钉钉报告
|
|
139
|
+
app_key: 应用AppKey(client_id)
|
|
140
|
+
app_secret: 应用AppSecret(client_secret)
|
|
141
|
+
agent_id: 应用AgentId
|
|
142
|
+
userid_list: 接收者的用户ID列表,多个用','分隔
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
enabled: bool
|
|
146
|
+
app_key: str | None = Field(None, description="应用AppKey (client_id)")
|
|
147
|
+
app_secret: str | None = Field(None, description="应用AppSecret (client_secret)")
|
|
148
|
+
agent_id: int | None = Field(None, description="应用AgentId")
|
|
149
|
+
userid_list: str | None = Field(None, description="接收者的用户ID列表, 多个用','分隔")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class ReportersConfig(BaseModel):
|
|
153
|
+
"""报告渠道汇总配置
|
|
154
|
+
|
|
155
|
+
包含所有支持的报告渠道配置,每个渠道独立管理。
|
|
156
|
+
|
|
157
|
+
字段说明:
|
|
158
|
+
local_file: 本地文件报告配置
|
|
159
|
+
email: 邮件报告配置
|
|
160
|
+
wecom: 企业微信报告配置
|
|
161
|
+
feishu: 飞书报告配置
|
|
162
|
+
dingtalk: 钉钉报告配置
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
local_file: LocalFileConfig
|
|
166
|
+
email: EmailConfig
|
|
167
|
+
wecom: WeComAppConfig
|
|
168
|
+
feishu: FeishuAppConfig
|
|
169
|
+
dingtalk: DingTalkAppConfig
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class ReportingConfig(BaseModel):
|
|
173
|
+
"""报告配置根模型
|
|
174
|
+
|
|
175
|
+
作为 API 请求和响应的根对象,包含所有报告渠道的配置。
|
|
176
|
+
|
|
177
|
+
字段说明:
|
|
178
|
+
reporters: 报告渠道汇总配置
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
reporters: ReportersConfig
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@router.get(
|
|
185
|
+
"/reporting/config",
|
|
186
|
+
response_model=ReportingConfig,
|
|
187
|
+
summary="获取报告配置",
|
|
188
|
+
responses={
|
|
189
|
+
500: {"description": "读取报告配置文件失败"},
|
|
190
|
+
},
|
|
191
|
+
)
|
|
192
|
+
def get_reporting_config(store: ProjectStore = Depends(get_project_store)) -> ReportingConfig:
|
|
193
|
+
"""获取报告配置
|
|
194
|
+
|
|
195
|
+
从项目目录下的 reporting_config.yaml 读取配置。
|
|
196
|
+
如果文件不存在,返回默认配置(仅启用本地文件报告)。
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
store: 项目存储依赖,包含项目路径
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
当前项目的报告配置对象
|
|
203
|
+
|
|
204
|
+
Raises:
|
|
205
|
+
HTTPException: 读取配置文件失败时返回 500 错误
|
|
206
|
+
"""
|
|
207
|
+
# 构建配置文件完整路径
|
|
208
|
+
config_path = os.path.join(store.project_path, "reporting_config.yaml")
|
|
209
|
+
if not os.path.exists(config_path):
|
|
210
|
+
# 配置文件不存在时返回默认配置
|
|
211
|
+
return ReportingConfig(
|
|
212
|
+
reporters=ReportersConfig(
|
|
213
|
+
local_file=LocalFileConfig(enabled=True, log_dir="logs"),
|
|
214
|
+
email=EmailConfig(enabled=False),
|
|
215
|
+
wecom=WeComAppConfig(enabled=False, corp_id=None, corp_secret=None, agent_id=None, touser="@all"),
|
|
216
|
+
feishu=FeishuAppConfig(
|
|
217
|
+
enabled=False, app_id=None, app_secret=None, receive_id_type="open_id", receive_ids=None
|
|
218
|
+
),
|
|
219
|
+
dingtalk=DingTalkAppConfig(
|
|
220
|
+
enabled=False, app_key=None, app_secret=None, agent_id=None, userid_list=None
|
|
221
|
+
),
|
|
222
|
+
)
|
|
223
|
+
)
|
|
224
|
+
try:
|
|
225
|
+
# 读取并解析 YAML 配置文件
|
|
226
|
+
with open(config_path, encoding="utf-8") as f:
|
|
227
|
+
config_data = yaml.safe_load(f)
|
|
228
|
+
return ReportingConfig(**config_data)
|
|
229
|
+
except Exception as e:
|
|
230
|
+
raise HTTPException(status_code=500, detail=f"读取报告配置文件失败:{describe_io_error(e)}")
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@router.post(
|
|
234
|
+
"/reporting/config",
|
|
235
|
+
summary="更新报告配置",
|
|
236
|
+
responses={
|
|
237
|
+
500: {"description": "写入报告配置文件失败"},
|
|
238
|
+
},
|
|
239
|
+
)
|
|
240
|
+
def update_reporting_config(
|
|
241
|
+
config: ReportingConfig, store: ProjectStore = Depends(get_project_store)
|
|
242
|
+
) -> dict[str, str]:
|
|
243
|
+
"""更新报告配置
|
|
244
|
+
|
|
245
|
+
将新的报告配置持久化保存到项目目录下的 reporting_config.yaml。
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
config: 新的报告配置对象
|
|
249
|
+
store: 项目存储依赖,包含项目路径
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
更新成功消息
|
|
253
|
+
|
|
254
|
+
Raises:
|
|
255
|
+
HTTPException: 写入配置文件失败时返回 500 错误
|
|
256
|
+
"""
|
|
257
|
+
# 构建配置文件完整路径
|
|
258
|
+
config_path = os.path.join(store.project_path, "reporting_config.yaml")
|
|
259
|
+
try:
|
|
260
|
+
write_yaml_atomic(Path(config_path), config.model_dump())
|
|
261
|
+
return {"message": "报告配置已成功更新。"}
|
|
262
|
+
except Exception as e:
|
|
263
|
+
raise HTTPException(status_code=500, detail=f"写入报告配置文件失败:{describe_io_error(e)}")
|
|
@@ -0,0 +1,24 @@
|
|
|
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
|
+
"""文件操作路由聚合:/api/latest/files 下的读写操作与上传传输子路由。"""
|
|
17
|
+
|
|
18
|
+
from fastapi import APIRouter
|
|
19
|
+
|
|
20
|
+
from . import ops, transfer
|
|
21
|
+
|
|
22
|
+
router = APIRouter(prefix="/api/latest/files", tags=["Files"])
|
|
23
|
+
router.include_router(ops.router)
|
|
24
|
+
router.include_router(transfer.router)
|
|
@@ -0,0 +1,179 @@
|
|
|
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
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
|
|
22
|
+
from fastapi import APIRouter, HTTPException, Query
|
|
23
|
+
|
|
24
|
+
from app.api.dependencies import _validate_project_root
|
|
25
|
+
from app.api.models.files import (
|
|
26
|
+
DirectoryEntry,
|
|
27
|
+
FileExistsResponse,
|
|
28
|
+
MkdirRequest,
|
|
29
|
+
MkdirResponse,
|
|
30
|
+
ReadFileRequest,
|
|
31
|
+
ReadFileResponse,
|
|
32
|
+
ScanDirectoryRequest,
|
|
33
|
+
ScanDirectoryResponse,
|
|
34
|
+
WriteFileRequest,
|
|
35
|
+
WriteFileResponse,
|
|
36
|
+
)
|
|
37
|
+
from app.shared.services.preview.path_validation import assert_path_within_root
|
|
38
|
+
|
|
39
|
+
router = APIRouter(prefix="", tags=["Files-Ops"])
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _validated_root(root: str) -> str:
|
|
43
|
+
"""校验并规范化白名单根目录。
|
|
44
|
+
|
|
45
|
+
B-sec: 此前 root 完全由客户端请求体提供,传 root="C:\\Users\\x" 即可通过
|
|
46
|
+
assert_path_within_root 的包含检查,构成任意文件读/写原语(B-sec1 防线形同虚设)。
|
|
47
|
+
现要求 root 必须是含 project.precis.yaml 的合法 Precis 项目根(复用 dependencies
|
|
48
|
+
的统一校验)。前端 Web 模式唯一调用方传的 root 即项目配置根,正常用法不受影响;
|
|
49
|
+
Electron 模式不走本端点(经原生 IPC)。
|
|
50
|
+
"""
|
|
51
|
+
return _validate_project_root(root)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@router.post(
|
|
55
|
+
"/read",
|
|
56
|
+
response_model=ReadFileResponse,
|
|
57
|
+
summary="读取文件内容",
|
|
58
|
+
)
|
|
59
|
+
def read_file(request: ReadFileRequest) -> ReadFileResponse:
|
|
60
|
+
"""读取指定路径的文件内容。
|
|
61
|
+
|
|
62
|
+
B-sec1 安全约束: path 必须位于 root(白名单根)下,由 assert_path_within_root 强制校验,
|
|
63
|
+
拒绝项目外任意文件读取。root 通常为当前项目配置目录(X-Project-Config-Path)。
|
|
64
|
+
"""
|
|
65
|
+
root = _validated_root(request.root)
|
|
66
|
+
path = assert_path_within_root(request.path, root, must_exist=False)
|
|
67
|
+
if not os.path.isfile(path):
|
|
68
|
+
raise HTTPException(status_code=404, detail=f"文件不存在: {path}")
|
|
69
|
+
try:
|
|
70
|
+
with open(path, encoding="utf-8") as f:
|
|
71
|
+
content = f.read()
|
|
72
|
+
return ReadFileResponse(content=content)
|
|
73
|
+
except UnicodeDecodeError:
|
|
74
|
+
raise HTTPException(status_code=400, detail="文件编码不是 UTF-8,暂时不支持")
|
|
75
|
+
except PermissionError:
|
|
76
|
+
raise HTTPException(status_code=403, detail="无权限读取文件")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@router.post(
|
|
80
|
+
"/write",
|
|
81
|
+
response_model=WriteFileResponse,
|
|
82
|
+
summary="写入文件内容",
|
|
83
|
+
)
|
|
84
|
+
def write_file(request: WriteFileRequest) -> WriteFileResponse:
|
|
85
|
+
"""写入内容到指定文件(自动创建父目录)。
|
|
86
|
+
|
|
87
|
+
B-sec1 安全约束: path 必须位于 root(白名单根)下,由 assert_path_within_root 强制校验,
|
|
88
|
+
拒绝项目外任意文件写入。
|
|
89
|
+
"""
|
|
90
|
+
# 写入场景允许目标不存在,故 must_exist=False
|
|
91
|
+
root = _validated_root(request.root)
|
|
92
|
+
path = assert_path_within_root(request.path, root, must_exist=False)
|
|
93
|
+
try:
|
|
94
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
95
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
96
|
+
f.write(request.content)
|
|
97
|
+
return WriteFileResponse(success=True)
|
|
98
|
+
except PermissionError:
|
|
99
|
+
raise HTTPException(status_code=403, detail="无权限写入文件")
|
|
100
|
+
except OSError as e:
|
|
101
|
+
raise HTTPException(status_code=500, detail=f"写入文件失败: {e}")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@router.get(
|
|
105
|
+
"/exists",
|
|
106
|
+
response_model=FileExistsResponse,
|
|
107
|
+
summary="检查文件是否存在",
|
|
108
|
+
)
|
|
109
|
+
def check_file_exists(
|
|
110
|
+
path: str,
|
|
111
|
+
root: str = Query(..., description="项目根目录绝对路径,作为白名单根;path 必须落于此目录内"),
|
|
112
|
+
) -> FileExistsResponse:
|
|
113
|
+
"""检查指定路径的文件是否存在。
|
|
114
|
+
|
|
115
|
+
B-sec1 安全约束: path 必须位于 root(白名单根)下。GET 端点通过 query 参数传 root。
|
|
116
|
+
"""
|
|
117
|
+
root = _validated_root(root)
|
|
118
|
+
resolved = assert_path_within_root(path, root, must_exist=False)
|
|
119
|
+
return FileExistsResponse(exists=os.path.isfile(resolved) or os.path.isdir(resolved))
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@router.post(
|
|
123
|
+
"/scan",
|
|
124
|
+
response_model=ScanDirectoryResponse,
|
|
125
|
+
summary="扫描目录内容",
|
|
126
|
+
)
|
|
127
|
+
def scan_directory(request: ScanDirectoryRequest) -> ScanDirectoryResponse:
|
|
128
|
+
"""扫描指定目录,返回文件和子目录列表。
|
|
129
|
+
|
|
130
|
+
B-sec1 安全约束: path 必须位于 root(白名单根)下。
|
|
131
|
+
"""
|
|
132
|
+
root = _validated_root(request.root)
|
|
133
|
+
path = assert_path_within_root(request.path, root, must_exist=False)
|
|
134
|
+
if not os.path.isdir(path):
|
|
135
|
+
raise HTTPException(status_code=404, detail=f"目录不存在: {path}")
|
|
136
|
+
try:
|
|
137
|
+
entries: list[DirectoryEntry] = []
|
|
138
|
+
for entry in os.scandir(path):
|
|
139
|
+
name = entry.name
|
|
140
|
+
if request.extensions and not entry.is_dir():
|
|
141
|
+
ext = os.path.splitext(name)[1].lower()
|
|
142
|
+
# B16 修复:扩展名归一化,允许调用方传 ['csv'] 或 ['.csv'] 均可命中
|
|
143
|
+
# 过去要求带前导点,传 ['csv'] 会因 '.csv' not in ['csv'] 被全部过滤
|
|
144
|
+
normalized_exts = {e if e.startswith(".") else f".{e}" for e in request.extensions}
|
|
145
|
+
normalized_exts_lower = {e.lower() for e in normalized_exts}
|
|
146
|
+
if ext not in normalized_exts_lower:
|
|
147
|
+
continue
|
|
148
|
+
entries.append(
|
|
149
|
+
DirectoryEntry(
|
|
150
|
+
name=name,
|
|
151
|
+
path=os.path.abspath(entry.path),
|
|
152
|
+
is_dir=entry.is_dir(),
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
entries.sort(key=lambda e: (not e.is_dir, e.name.lower()))
|
|
156
|
+
return ScanDirectoryResponse(entries=entries)
|
|
157
|
+
except PermissionError:
|
|
158
|
+
raise HTTPException(status_code=403, detail="无权限访问目录")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@router.post(
|
|
162
|
+
"/mkdir",
|
|
163
|
+
response_model=MkdirResponse,
|
|
164
|
+
summary="创建目录(含父目录)",
|
|
165
|
+
)
|
|
166
|
+
def make_directory(request: MkdirRequest) -> MkdirResponse:
|
|
167
|
+
"""递归创建目录。
|
|
168
|
+
|
|
169
|
+
B-sec1 安全约束: path 必须位于 root(白名单根)下。
|
|
170
|
+
"""
|
|
171
|
+
root = _validated_root(request.root)
|
|
172
|
+
path = assert_path_within_root(request.path, root, must_exist=False)
|
|
173
|
+
try:
|
|
174
|
+
os.makedirs(path, exist_ok=True)
|
|
175
|
+
return MkdirResponse(success=True)
|
|
176
|
+
except PermissionError:
|
|
177
|
+
raise HTTPException(status_code=403, detail="无权限创建目录")
|
|
178
|
+
except OSError as e:
|
|
179
|
+
raise HTTPException(status_code=500, detail=f"创建目录失败: {e}")
|
|
@@ -0,0 +1,101 @@
|
|
|
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
|
+
"""文件传输路由:Web 模式下的文件上传/下载(经服务器临时目录中转)。"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
import tempfile
|
|
22
|
+
import uuid
|
|
23
|
+
|
|
24
|
+
from fastapi import APIRouter, File, HTTPException, UploadFile
|
|
25
|
+
from fastapi.responses import FileResponse
|
|
26
|
+
|
|
27
|
+
from app.shared.services.preview.path_validation import assert_path_within_root
|
|
28
|
+
|
|
29
|
+
router = APIRouter(prefix="", tags=["Files-Transfer"])
|
|
30
|
+
|
|
31
|
+
# 临时文件存储目录
|
|
32
|
+
TEMP_DIR = os.path.join(tempfile.gettempdir(), "precis-web-uploads")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@router.post(
|
|
36
|
+
"/upload",
|
|
37
|
+
summary="上传文件到临时目录",
|
|
38
|
+
)
|
|
39
|
+
async def upload_file(file: UploadFile = File(...)) -> dict:
|
|
40
|
+
"""上传文件到服务器临时目录,返回临时路径。"""
|
|
41
|
+
os.makedirs(TEMP_DIR, exist_ok=True)
|
|
42
|
+
file_id = str(uuid.uuid4())
|
|
43
|
+
# 保留原始扩展名
|
|
44
|
+
ext = os.path.splitext(file.filename or "file")[1] if file.filename else ""
|
|
45
|
+
dest_path = os.path.join(TEMP_DIR, f"{file_id}{ext}")
|
|
46
|
+
try:
|
|
47
|
+
content = await file.read()
|
|
48
|
+
with open(dest_path, "wb") as f:
|
|
49
|
+
f.write(content)
|
|
50
|
+
return {
|
|
51
|
+
"success": True,
|
|
52
|
+
"temp_path": dest_path,
|
|
53
|
+
"original_name": file.filename or "unknown",
|
|
54
|
+
"size": len(content),
|
|
55
|
+
}
|
|
56
|
+
except Exception as e:
|
|
57
|
+
raise HTTPException(status_code=500, detail=f"文件上传失败: {e}")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@router.get(
|
|
61
|
+
"/download",
|
|
62
|
+
summary="下载临时文件",
|
|
63
|
+
)
|
|
64
|
+
def download_file(path: str) -> FileResponse:
|
|
65
|
+
"""
|
|
66
|
+
下载指定的临时上传文件。
|
|
67
|
+
|
|
68
|
+
安全约束:仅允许下载 TEMP_DIR 内的文件,防止任意文件下载。
|
|
69
|
+
"""
|
|
70
|
+
resolved = assert_path_within_root(path, TEMP_DIR, must_exist=True)
|
|
71
|
+
filename = os.path.basename(resolved)
|
|
72
|
+
return FileResponse(resolved, filename=filename, media_type="application/octet-stream")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@router.delete(
|
|
76
|
+
"/temp/{file_id:path}",
|
|
77
|
+
summary="清理临时文件",
|
|
78
|
+
)
|
|
79
|
+
def delete_temp_file(file_id: str) -> dict:
|
|
80
|
+
"""
|
|
81
|
+
删除临时目录中的文件。
|
|
82
|
+
|
|
83
|
+
安全约束:file_id 经反穿越校验后限定在 TEMP_DIR 内,防止 `..` 逃逸。
|
|
84
|
+
"""
|
|
85
|
+
# file_id 来自 path 参数,可能含 `..`;先组装再用 assert_path_within_root 限定到 TEMP_DIR
|
|
86
|
+
candidate = os.path.join(TEMP_DIR, file_id)
|
|
87
|
+
try:
|
|
88
|
+
resolved = assert_path_within_root(candidate, TEMP_DIR, must_exist=False)
|
|
89
|
+
except HTTPException:
|
|
90
|
+
raise
|
|
91
|
+
# 精确匹配
|
|
92
|
+
if os.path.isfile(resolved):
|
|
93
|
+
os.unlink(resolved)
|
|
94
|
+
return {"success": True}
|
|
95
|
+
# 尝试匹配前缀(file_id 可能不含扩展名),前缀比对基于 basename 防止逃逸
|
|
96
|
+
target_prefix = os.path.basename(resolved)
|
|
97
|
+
for f in os.listdir(TEMP_DIR):
|
|
98
|
+
if f.startswith(target_prefix):
|
|
99
|
+
os.unlink(os.path.join(TEMP_DIR, f))
|
|
100
|
+
return {"success": True}
|
|
101
|
+
raise HTTPException(status_code=404, detail="临时文件未找到")
|
|
@@ -0,0 +1,35 @@
|
|
|
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
|
+
- 导出 preview 模块共享的 APIRouter 实例
|
|
21
|
+
- 显式导入各子路由模块,确保装饰器在模块加载时完成注册
|
|
22
|
+
|
|
23
|
+
架构设计:
|
|
24
|
+
- `router.py` 只负责创建带 `/preview` 前缀的路由对象
|
|
25
|
+
- `path_mode.py`、`content_mode.py`、`header_row.py` 通过副作用方式向同一个 router 挂载端点
|
|
26
|
+
- 因此包入口必须导入这些模块,否则 FastAPI 只能拿到一个没有任何子路由的空 router
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
# 显式导入子模块,触发 `@router.*` 装饰器执行,确保路由被真正注册。
|
|
30
|
+
from . import content_mode, header_row, path_mode # noqa: F401
|
|
31
|
+
from .router import router
|
|
32
|
+
|
|
33
|
+
preview_router = router
|
|
34
|
+
|
|
35
|
+
__all__ = ["router", "preview_router"]
|