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,177 @@
|
|
|
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
|
+
- 视图文件与 manifest 分离,避免 UI 状态污染配置语义
|
|
22
|
+
- 文件不存在时返回默认空视图,保证前端始终能获取有效数据
|
|
23
|
+
|
|
24
|
+
架构设计:
|
|
25
|
+
- 使用 JSON 格式存储,便于前端快速序列化/反序列化
|
|
26
|
+
- 读取时兜底返回默认视图,写入时直接覆盖
|
|
27
|
+
- 通过 _v2_view_path 统一计算视图文件路径
|
|
28
|
+
|
|
29
|
+
输入示例:
|
|
30
|
+
GET /view
|
|
31
|
+
PUT /view (body: ProjectViewV2Model)
|
|
32
|
+
|
|
33
|
+
输出示例:
|
|
34
|
+
ProjectViewV2Model: {version, nodes, viewport}
|
|
35
|
+
StandardResponse: 操作结果消息
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
import json
|
|
39
|
+
import logging
|
|
40
|
+
import os
|
|
41
|
+
import tempfile
|
|
42
|
+
from pathlib import Path
|
|
43
|
+
|
|
44
|
+
from fastapi import APIRouter, Depends, HTTPException
|
|
45
|
+
|
|
46
|
+
from app.api.dependencies import get_project_config_path
|
|
47
|
+
from app.api.services.io_error_messages import describe_io_error
|
|
48
|
+
|
|
49
|
+
from .base import (
|
|
50
|
+
ProjectViewV2Model,
|
|
51
|
+
StandardResponse,
|
|
52
|
+
_v2_view_path,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
logger = logging.getLogger(__name__)
|
|
56
|
+
|
|
57
|
+
router = APIRouter(prefix="", tags=["Project-View"])
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _write_json_atomic(path: str, data: dict) -> None:
|
|
61
|
+
"""原子写入 JSON 文件(临时文件 + os.replace)。
|
|
62
|
+
|
|
63
|
+
view.json 属于高频覆写文件,此前用裸 open("w") 直写:进程在写入中途崩溃/断电
|
|
64
|
+
会留下半个 JSON,下次读取直接 500。参照 write_yaml_atomic 的思路:先写同目录
|
|
65
|
+
临时文件并 fsync,再用 os.replace 原子替换(POSIX 与 Windows 均保证同一文件
|
|
66
|
+
系统内替换的原子性);任一步失败时清理临时文件后原样抛出。
|
|
67
|
+
|
|
68
|
+
参数:
|
|
69
|
+
path: 目标 JSON 文件路径
|
|
70
|
+
data: 待序列化的字典
|
|
71
|
+
"""
|
|
72
|
+
target = Path(path)
|
|
73
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
74
|
+
fd, temp_path = tempfile.mkstemp(dir=target.parent, suffix=".tmp")
|
|
75
|
+
try:
|
|
76
|
+
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
77
|
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
|
78
|
+
f.flush()
|
|
79
|
+
os.fsync(f.fileno())
|
|
80
|
+
os.replace(temp_path, target)
|
|
81
|
+
except Exception:
|
|
82
|
+
# 失败时尽力清理临时文件,避免 .precis/ 目录残留 *.tmp 垃圾
|
|
83
|
+
if os.path.exists(temp_path):
|
|
84
|
+
try:
|
|
85
|
+
os.remove(temp_path)
|
|
86
|
+
except OSError:
|
|
87
|
+
pass
|
|
88
|
+
raise
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@router.get(
|
|
92
|
+
"/view",
|
|
93
|
+
response_model=ProjectViewV2Model,
|
|
94
|
+
summary="获取项目画布视图",
|
|
95
|
+
responses={
|
|
96
|
+
500: {"description": "服务器内部错误"},
|
|
97
|
+
},
|
|
98
|
+
)
|
|
99
|
+
def get_v2_project_view(config_path: str = Depends(get_project_config_path)) -> ProjectViewV2Model:
|
|
100
|
+
"""
|
|
101
|
+
获取 V2 项目视图文件(画布布局)。
|
|
102
|
+
|
|
103
|
+
使用场景:
|
|
104
|
+
- 前端加载项目时恢复画布节点位置
|
|
105
|
+
- 用户打开已保存的项目时恢复之前的查看位置
|
|
106
|
+
|
|
107
|
+
副作用:
|
|
108
|
+
- 如果视图文件不存在,返回默认空视图而非 404,保证前端总能拿到有效数据
|
|
109
|
+
|
|
110
|
+
数据流:
|
|
111
|
+
1. 尝试从文件系统读取 project.view.json
|
|
112
|
+
2. 如果文件不存在,返回默认视图 {version: 1, nodes: {}, viewport: null}
|
|
113
|
+
3. 如果读取失败(如 JSON 解析错误),返回 500 错误
|
|
114
|
+
|
|
115
|
+
参数:
|
|
116
|
+
config_path: 项目配置根目录(通过 Depends 注入)
|
|
117
|
+
|
|
118
|
+
返回:
|
|
119
|
+
ProjectViewV2Model: 包含节点位置和视口信息
|
|
120
|
+
"""
|
|
121
|
+
view_path = _v2_view_path(config_path)
|
|
122
|
+
if not os.path.isfile(view_path):
|
|
123
|
+
return ProjectViewV2Model(version=1, nodes={})
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
with open(view_path, encoding="utf-8") as f:
|
|
127
|
+
data = json.load(f) or {}
|
|
128
|
+
return ProjectViewV2Model(**data)
|
|
129
|
+
except Exception as e:
|
|
130
|
+
raise HTTPException(status_code=500, detail=f"读取视图文件失败:{describe_io_error(e)}")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@router.put(
|
|
134
|
+
"/view",
|
|
135
|
+
response_model=StandardResponse,
|
|
136
|
+
summary="更新项目画布视图",
|
|
137
|
+
responses={
|
|
138
|
+
400: {"description": "请求参数错误"},
|
|
139
|
+
500: {"description": "服务器内部错误"},
|
|
140
|
+
},
|
|
141
|
+
)
|
|
142
|
+
def put_v2_project_view(
|
|
143
|
+
payload: ProjectViewV2Model,
|
|
144
|
+
config_path: str = Depends(get_project_config_path),
|
|
145
|
+
) -> StandardResponse:
|
|
146
|
+
"""
|
|
147
|
+
更新 V2 项目视图文件(画布布局)。
|
|
148
|
+
|
|
149
|
+
使用场景:
|
|
150
|
+
- 用户拖拽节点后保存位置
|
|
151
|
+
- 用户缩放/平移画布后保存视口
|
|
152
|
+
- 定期自动保存画布状态
|
|
153
|
+
|
|
154
|
+
副作用:
|
|
155
|
+
- 直接覆盖写入视图文件
|
|
156
|
+
- 使用 ensure_ascii=False 支持中文节点名称
|
|
157
|
+
|
|
158
|
+
数据流:
|
|
159
|
+
1. 接收前端传来的视图数据(节点坐标 + 视口)
|
|
160
|
+
2. 转换为 JSON 格式写入文件
|
|
161
|
+
3. 返回成功消息
|
|
162
|
+
|
|
163
|
+
参数:
|
|
164
|
+
payload: 前端传来的视图数据
|
|
165
|
+
config_path: 项目配置根目录(通过 Depends 注入)
|
|
166
|
+
|
|
167
|
+
返回:
|
|
168
|
+
StandardResponse: 操作结果消息
|
|
169
|
+
"""
|
|
170
|
+
view_path = _v2_view_path(config_path)
|
|
171
|
+
try:
|
|
172
|
+
# B-reliability: 原子写替代裸 open("w"),进程中断不再留下半个 JSON
|
|
173
|
+
_write_json_atomic(view_path, payload.model_dump())
|
|
174
|
+
except Exception as e:
|
|
175
|
+
raise HTTPException(status_code=500, detail=f"写入视图文件失败:{describe_io_error(e)}")
|
|
176
|
+
|
|
177
|
+
return StandardResponse(message="Project view saved")
|
|
@@ -0,0 +1,167 @@
|
|
|
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
|
+
- 提供前端多工作区(multi-tab canvas)的读写接口
|
|
21
|
+
- 工作区只存储视图差异(可见节点、视口),不存储完整项目配置
|
|
22
|
+
- 项目配置仍由 V2 YAML 体系(manifest/schema/constraint/regex)承载
|
|
23
|
+
|
|
24
|
+
架构设计:
|
|
25
|
+
- 与 project.view.json 分工协作:view.json 存节点坐标,workspaces.json 存工作区列表
|
|
26
|
+
- 文件不存在时返回默认空配置,保证前端始终能获取有效数据
|
|
27
|
+
- 使用 JSON 格式,便于前端快速序列化/反序列化
|
|
28
|
+
|
|
29
|
+
输入示例:
|
|
30
|
+
GET /workspaces
|
|
31
|
+
PUT /workspaces (body: WorkspacesV2Model)
|
|
32
|
+
|
|
33
|
+
输出示例:
|
|
34
|
+
WorkspacesV2Model: {version, activeWorkspaceId, workspaces}
|
|
35
|
+
StandardResponse: 操作结果消息
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
import json
|
|
39
|
+
import logging
|
|
40
|
+
import os
|
|
41
|
+
import tempfile
|
|
42
|
+
from pathlib import Path
|
|
43
|
+
|
|
44
|
+
from fastapi import APIRouter, Depends, HTTPException
|
|
45
|
+
|
|
46
|
+
from app.api.dependencies import get_project_config_path
|
|
47
|
+
from app.api.services.io_error_messages import describe_io_error
|
|
48
|
+
|
|
49
|
+
from .base import (
|
|
50
|
+
StandardResponse,
|
|
51
|
+
WorkspacesV2Model,
|
|
52
|
+
_v2_workspaces_path,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
logger = logging.getLogger(__name__)
|
|
56
|
+
|
|
57
|
+
router = APIRouter(prefix="", tags=["Project-Workspaces"])
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _write_json_atomic(path: str, data: dict) -> None:
|
|
61
|
+
"""原子写入 JSON 文件(临时文件 + os.replace)。
|
|
62
|
+
|
|
63
|
+
workspaces.json 属于高频覆写文件,此前用裸 open("w") 直写:进程在写入中途
|
|
64
|
+
崩溃/断电会留下半个 JSON,下次读取直接 500。参照 write_yaml_atomic 的思路:
|
|
65
|
+
先写同目录临时文件并 fsync,再用 os.replace 原子替换(POSIX 与 Windows 均保证
|
|
66
|
+
同一文件系统内替换的原子性);任一步失败时清理临时文件后原样抛出。
|
|
67
|
+
|
|
68
|
+
参数:
|
|
69
|
+
path: 目标 JSON 文件路径
|
|
70
|
+
data: 待序列化的字典
|
|
71
|
+
"""
|
|
72
|
+
target = Path(path)
|
|
73
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
74
|
+
fd, temp_path = tempfile.mkstemp(dir=target.parent, suffix=".tmp")
|
|
75
|
+
try:
|
|
76
|
+
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
77
|
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
|
78
|
+
f.flush()
|
|
79
|
+
os.fsync(f.fileno())
|
|
80
|
+
os.replace(temp_path, target)
|
|
81
|
+
except Exception:
|
|
82
|
+
# 失败时尽力清理临时文件,避免 .precis/ 目录残留 *.tmp 垃圾
|
|
83
|
+
if os.path.exists(temp_path):
|
|
84
|
+
try:
|
|
85
|
+
os.remove(temp_path)
|
|
86
|
+
except OSError:
|
|
87
|
+
pass
|
|
88
|
+
raise
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@router.get(
|
|
92
|
+
"/workspaces",
|
|
93
|
+
response_model=WorkspacesV2Model,
|
|
94
|
+
summary="获取 V2 项目工作区配置",
|
|
95
|
+
responses={
|
|
96
|
+
500: {"description": "读取工作区文件失败"},
|
|
97
|
+
},
|
|
98
|
+
)
|
|
99
|
+
def get_v2_workspaces(config_path: str = Depends(get_project_config_path)) -> WorkspacesV2Model:
|
|
100
|
+
"""
|
|
101
|
+
获取 V2 项目工作区配置。
|
|
102
|
+
|
|
103
|
+
使用场景:
|
|
104
|
+
- 前端加载项目时恢复多标签页工作区状态
|
|
105
|
+
- 用户刷新页面后恢复之前的工作区列表和活跃标签
|
|
106
|
+
|
|
107
|
+
副作用:
|
|
108
|
+
- 如果工作区文件不存在,返回默认空配置而非 404,保证前端总能拿到有效数据
|
|
109
|
+
|
|
110
|
+
参数:
|
|
111
|
+
config_path: 项目配置根目录(通过 Depends 注入)
|
|
112
|
+
|
|
113
|
+
返回:
|
|
114
|
+
WorkspacesV2Model: 包含工作区列表和当前活跃工作区 ID
|
|
115
|
+
"""
|
|
116
|
+
workspaces_path = _v2_workspaces_path(config_path)
|
|
117
|
+
if not os.path.isfile(workspaces_path):
|
|
118
|
+
return WorkspacesV2Model(version=1, activeWorkspaceId=None, workspaces=[])
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
with open(workspaces_path, encoding="utf-8") as f:
|
|
122
|
+
data = json.load(f) or {}
|
|
123
|
+
return WorkspacesV2Model(**data)
|
|
124
|
+
except Exception as e:
|
|
125
|
+
raise HTTPException(status_code=500, detail=f"读取工作区文件失败:{describe_io_error(e)}")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@router.put(
|
|
129
|
+
"/workspaces",
|
|
130
|
+
response_model=StandardResponse,
|
|
131
|
+
summary="更新 V2 项目工作区配置",
|
|
132
|
+
responses={
|
|
133
|
+
500: {"description": "写入工作区文件失败"},
|
|
134
|
+
},
|
|
135
|
+
)
|
|
136
|
+
def put_v2_workspaces(
|
|
137
|
+
payload: WorkspacesV2Model,
|
|
138
|
+
config_path: str = Depends(get_project_config_path),
|
|
139
|
+
) -> StandardResponse:
|
|
140
|
+
"""
|
|
141
|
+
更新 V2 项目工作区配置。
|
|
142
|
+
|
|
143
|
+
使用场景:
|
|
144
|
+
- 用户创建/重命名/关闭工作区标签页时保存状态
|
|
145
|
+
- 用户切换活跃工作区时保存当前选择
|
|
146
|
+
- 用户拖拽排序工作区时保存新顺序
|
|
147
|
+
|
|
148
|
+
副作用:
|
|
149
|
+
- 自动创建 .precis 目录(如果不存在)
|
|
150
|
+
- 直接覆盖写入工作区文件
|
|
151
|
+
- 使用 ensure_ascii=False 支持中文工作区名称
|
|
152
|
+
|
|
153
|
+
参数:
|
|
154
|
+
payload: 前端传来的工作区数据
|
|
155
|
+
config_path: 项目配置根目录(通过 Depends 注入)
|
|
156
|
+
|
|
157
|
+
返回:
|
|
158
|
+
StandardResponse: 操作结果消息
|
|
159
|
+
"""
|
|
160
|
+
workspaces_path = _v2_workspaces_path(config_path)
|
|
161
|
+
try:
|
|
162
|
+
# B-reliability: 原子写替代裸 open("w"),进程中断不再留下半个 JSON
|
|
163
|
+
_write_json_atomic(workspaces_path, payload.model_dump())
|
|
164
|
+
except Exception as e:
|
|
165
|
+
raise HTTPException(status_code=500, detail=f"写入工作区文件失败:{describe_io_error(e)}")
|
|
166
|
+
|
|
167
|
+
return StandardResponse(message="Workspaces saved")
|
|
@@ -0,0 +1,25 @@
|
|
|
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/projects 下的扫描、打开、创建子路由。"""
|
|
17
|
+
|
|
18
|
+
from fastapi import APIRouter
|
|
19
|
+
|
|
20
|
+
from . import create, open, scan
|
|
21
|
+
|
|
22
|
+
router = APIRouter(prefix="/api/latest/projects", tags=["Projects"])
|
|
23
|
+
router.include_router(scan.router)
|
|
24
|
+
router.include_router(open.router)
|
|
25
|
+
router.include_router(create.router)
|
|
@@ -0,0 +1,121 @@
|
|
|
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 Web 端新建项目 API
|
|
18
|
+
|
|
19
|
+
功能概述:
|
|
20
|
+
- 创建一个新的 Precis 项目脚手架
|
|
21
|
+
- 生成标准目录结构与最小 manifest
|
|
22
|
+
- 设置为"当前项目"
|
|
23
|
+
|
|
24
|
+
架构设计:
|
|
25
|
+
- 与 open.py 对称:create 写入脚手架,open 验证并加载
|
|
26
|
+
- Web 模式下文件系统可见,直接在请求路径下创建
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import os
|
|
32
|
+
import re
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
|
|
35
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
36
|
+
|
|
37
|
+
from app.api.models.projects import CreateProjectRequest, CreateProjectResponse
|
|
38
|
+
from app.shared.core.io.yaml import write_yaml_atomic
|
|
39
|
+
|
|
40
|
+
router = APIRouter(prefix="", tags=["Projects-Create"])
|
|
41
|
+
|
|
42
|
+
# 标准项目子目录
|
|
43
|
+
_REQUIRED_SUBDIRS = [
|
|
44
|
+
"schemas",
|
|
45
|
+
"constraints",
|
|
46
|
+
"regex_nodes",
|
|
47
|
+
"transforms",
|
|
48
|
+
"patterns",
|
|
49
|
+
"templates",
|
|
50
|
+
"data",
|
|
51
|
+
".precis",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# 最小空项目 manifest 模板(与 qa_simple 结构对齐)
|
|
56
|
+
def _build_min_manifest(project_name: str) -> dict:
|
|
57
|
+
"""根据项目名构造最小可用 manifest。"""
|
|
58
|
+
# id 由项目名派生:小写 + 仅保留字母数字与下划线
|
|
59
|
+
raw_id = re.sub(r"[^a-zA-Z0-9_]", "_", project_name.strip().lower()) or "project"
|
|
60
|
+
return {
|
|
61
|
+
"version": 2,
|
|
62
|
+
"project": {"id": raw_id, "name": project_name},
|
|
63
|
+
"settings": {
|
|
64
|
+
"validation": {
|
|
65
|
+
"auto_validate": False,
|
|
66
|
+
"strict_mode": False,
|
|
67
|
+
"error_handling": "continue",
|
|
68
|
+
"timeout_seconds": 30,
|
|
69
|
+
},
|
|
70
|
+
"file_processing": {"default_encoding": "utf-8"},
|
|
71
|
+
"script_security": {"allow_eval": False, "allow_exec": False},
|
|
72
|
+
},
|
|
73
|
+
"schemas": [],
|
|
74
|
+
"constraints": [],
|
|
75
|
+
"regex_nodes": [],
|
|
76
|
+
"transforms": [],
|
|
77
|
+
"data_sources": [],
|
|
78
|
+
"templates": [],
|
|
79
|
+
"template_instances": [],
|
|
80
|
+
"patterns_dir": "patterns",
|
|
81
|
+
"warnings": [],
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@router.post(
|
|
86
|
+
"/create",
|
|
87
|
+
response_model=CreateProjectResponse,
|
|
88
|
+
summary="创建一个新的 Precis 项目脚手架",
|
|
89
|
+
)
|
|
90
|
+
def create_project(
|
|
91
|
+
request: CreateProjectRequest,
|
|
92
|
+
http_request: Request,
|
|
93
|
+
) -> CreateProjectResponse:
|
|
94
|
+
"""创建项目目录、写入最小 manifest 与标准子目录,并将其设为当前项目。"""
|
|
95
|
+
name = request.name.strip()
|
|
96
|
+
if not name:
|
|
97
|
+
raise HTTPException(status_code=400, detail="项目名称不能为空")
|
|
98
|
+
|
|
99
|
+
project_path = os.path.abspath(os.path.normpath(request.path))
|
|
100
|
+
manifest_path = os.path.join(project_path, "project.precis.yaml")
|
|
101
|
+
|
|
102
|
+
# 若已是合法项目(manifest 存在),拒绝覆盖
|
|
103
|
+
if os.path.isfile(manifest_path):
|
|
104
|
+
raise HTTPException(
|
|
105
|
+
status_code=400,
|
|
106
|
+
detail=f"目录已是 Precis 项目(存在 manifest): {project_path}",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# 创建目录与标准子目录
|
|
110
|
+
os.makedirs(project_path, exist_ok=True)
|
|
111
|
+
for sub in _REQUIRED_SUBDIRS:
|
|
112
|
+
os.makedirs(os.path.join(project_path, sub), exist_ok=True)
|
|
113
|
+
|
|
114
|
+
# 写入最小 manifest
|
|
115
|
+
write_yaml_atomic(Path(manifest_path), _build_min_manifest(name))
|
|
116
|
+
|
|
117
|
+
# 设为当前项目(与 open 行为一致)
|
|
118
|
+
http_request.app.state.current_project_path = project_path
|
|
119
|
+
http_request.app.state.current_project_name = name
|
|
120
|
+
|
|
121
|
+
return CreateProjectResponse(success=True, name=name, path=project_path)
|
|
@@ -0,0 +1,103 @@
|
|
|
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
|
+
import yaml
|
|
23
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
24
|
+
|
|
25
|
+
from app.api.models.projects import (
|
|
26
|
+
CloseProjectResponse,
|
|
27
|
+
CurrentProjectResponse,
|
|
28
|
+
OpenProjectRequest,
|
|
29
|
+
OpenProjectResponse,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
router = APIRouter(prefix="", tags=["Projects-Open"])
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _get_project_name(project_path: str) -> str:
|
|
36
|
+
"""从 project.precis.yaml 中读取项目名称,失败时返回目录名。"""
|
|
37
|
+
manifest_path = os.path.join(project_path, "project.precis.yaml")
|
|
38
|
+
try:
|
|
39
|
+
with open(manifest_path, encoding="utf-8") as f:
|
|
40
|
+
manifest = yaml.safe_load(f)
|
|
41
|
+
if isinstance(manifest, dict):
|
|
42
|
+
project = manifest.get("project", {})
|
|
43
|
+
if isinstance(project, dict):
|
|
44
|
+
name = project.get("name")
|
|
45
|
+
if name:
|
|
46
|
+
return str(name)
|
|
47
|
+
except Exception:
|
|
48
|
+
pass
|
|
49
|
+
return os.path.basename(project_path)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@router.post(
|
|
53
|
+
"/open",
|
|
54
|
+
response_model=OpenProjectResponse,
|
|
55
|
+
summary="打开一个 Precis 项目",
|
|
56
|
+
)
|
|
57
|
+
def open_project(
|
|
58
|
+
request: OpenProjectRequest,
|
|
59
|
+
http_request: Request,
|
|
60
|
+
) -> OpenProjectResponse:
|
|
61
|
+
"""打开指定路径的项目,将其设置为"当前项目"。"""
|
|
62
|
+
project_path = os.path.abspath(os.path.normpath(request.path))
|
|
63
|
+
if not os.path.isdir(project_path):
|
|
64
|
+
raise HTTPException(status_code=404, detail=f"项目目录不存在: {project_path}")
|
|
65
|
+
|
|
66
|
+
manifest_path = os.path.join(project_path, "project.precis.yaml")
|
|
67
|
+
if not os.path.isfile(manifest_path):
|
|
68
|
+
raise HTTPException(
|
|
69
|
+
status_code=400,
|
|
70
|
+
detail=f"目录不是有效的 Precis 项目(缺少 project.precis.yaml): {project_path}",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
name = _get_project_name(project_path)
|
|
74
|
+
http_request.app.state.current_project_path = project_path
|
|
75
|
+
http_request.app.state.current_project_name = name
|
|
76
|
+
|
|
77
|
+
return OpenProjectResponse(success=True, name=name, path=project_path)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@router.get(
|
|
81
|
+
"/current",
|
|
82
|
+
response_model=CurrentProjectResponse,
|
|
83
|
+
summary="获取当前打开的项目信息",
|
|
84
|
+
)
|
|
85
|
+
def get_current_project(http_request: Request) -> CurrentProjectResponse:
|
|
86
|
+
"""返回当前打开的项目路径和名称。"""
|
|
87
|
+
path = getattr(http_request.app.state, "current_project_path", None)
|
|
88
|
+
if not path:
|
|
89
|
+
return CurrentProjectResponse(has_current=False)
|
|
90
|
+
name = getattr(http_request.app.state, "current_project_name", None) or os.path.basename(path)
|
|
91
|
+
return CurrentProjectResponse(has_current=True, path=path, name=name)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@router.post(
|
|
95
|
+
"/close",
|
|
96
|
+
response_model=CloseProjectResponse,
|
|
97
|
+
summary="关闭当前项目",
|
|
98
|
+
)
|
|
99
|
+
def close_project(http_request: Request) -> CloseProjectResponse:
|
|
100
|
+
"""关闭当前项目,清除状态。"""
|
|
101
|
+
http_request.app.state.current_project_path = None
|
|
102
|
+
http_request.app.state.current_project_name = None
|
|
103
|
+
return CloseProjectResponse(success=True)
|
|
@@ -0,0 +1,100 @@
|
|
|
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
|
+
"""项目扫描路由:扫描工作目录下所有包含 project.precis.yaml 的项目。"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
from datetime import datetime
|
|
22
|
+
|
|
23
|
+
import yaml
|
|
24
|
+
from fastapi import APIRouter, HTTPException, Query
|
|
25
|
+
|
|
26
|
+
from app.api.models.projects import ProjectInfo, ScanResponse
|
|
27
|
+
|
|
28
|
+
router = APIRouter(prefix="", tags=["Projects-Scan"])
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@router.get(
|
|
32
|
+
"/scan",
|
|
33
|
+
response_model=ScanResponse,
|
|
34
|
+
summary="扫描工作目录下的所有 Precis 项目",
|
|
35
|
+
)
|
|
36
|
+
def scan_projects(
|
|
37
|
+
work_dir: str | None = Query(None, description="要扫描的工作目录绝对路径"),
|
|
38
|
+
) -> ScanResponse:
|
|
39
|
+
"""扫描指定工作目录,找出所有包含 project.precis.yaml 的子目录。"""
|
|
40
|
+
if not work_dir:
|
|
41
|
+
work_dir = os.environ.get("PRECIS_WORK_DIR")
|
|
42
|
+
if not work_dir:
|
|
43
|
+
raise HTTPException(
|
|
44
|
+
status_code=400,
|
|
45
|
+
detail="请指定 work_dir 参数或设置 PRECIS_WORK_DIR 环境变量",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
if not os.path.isdir(work_dir):
|
|
49
|
+
raise HTTPException(status_code=400, detail=f"工作目录不存在: {work_dir}")
|
|
50
|
+
|
|
51
|
+
projects: list[ProjectInfo] = []
|
|
52
|
+
try:
|
|
53
|
+
for entry in os.scandir(work_dir):
|
|
54
|
+
if not entry.is_dir():
|
|
55
|
+
continue
|
|
56
|
+
manifest_path = os.path.join(entry.path, "project.precis.yaml")
|
|
57
|
+
if not os.path.isfile(manifest_path):
|
|
58
|
+
continue
|
|
59
|
+
info = _parse_project(entry.path, manifest_path)
|
|
60
|
+
if info:
|
|
61
|
+
projects.append(info)
|
|
62
|
+
except PermissionError as e:
|
|
63
|
+
raise HTTPException(status_code=403, detail=f"无权限访问目录: {e}")
|
|
64
|
+
|
|
65
|
+
projects.sort(key=lambda p: p.last_modified, reverse=True)
|
|
66
|
+
return ScanResponse(work_dir=os.path.abspath(work_dir), projects=projects)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _parse_project(project_path: str, manifest_path: str) -> ProjectInfo | None:
|
|
70
|
+
"""解析单个项目的 manifest 文件获取元信息。"""
|
|
71
|
+
try:
|
|
72
|
+
with open(manifest_path, encoding="utf-8") as f:
|
|
73
|
+
manifest = yaml.safe_load(f)
|
|
74
|
+
except Exception:
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
if not isinstance(manifest, dict):
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
project = manifest.get("project", {})
|
|
81
|
+
if not isinstance(project, dict):
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
name = project.get("name", os.path.basename(project_path))
|
|
85
|
+
schemas = manifest.get("schemas", []) or []
|
|
86
|
+
constraints = manifest.get("constraints", []) or []
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
mtime = os.path.getmtime(manifest_path)
|
|
90
|
+
last_modified = datetime.fromtimestamp(mtime).isoformat()
|
|
91
|
+
except OSError:
|
|
92
|
+
last_modified = ""
|
|
93
|
+
|
|
94
|
+
return ProjectInfo(
|
|
95
|
+
name=str(name),
|
|
96
|
+
path=os.path.abspath(project_path),
|
|
97
|
+
schema_count=len(schemas) if isinstance(schemas, list) else 0,
|
|
98
|
+
constraint_count=len(constraints) if isinstance(constraints, list) else 0,
|
|
99
|
+
last_modified=last_modified,
|
|
100
|
+
)
|