@ngocsangairvds/vsaf 3.1.27 → 3.2.1
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.
- package/package.json +2 -2
- package/src/global.js +70 -10
- package/tools/skills/vds-scripts-skill/.openskills.json +6 -0
- package/tools/skills/vds-scripts-skill/QUALITY.md +44 -0
- package/tools/skills/vds-scripts-skill/SKILL.md +135 -0
- package/tools/skills/vds-scripts-skill/references/audit-commands.md +171 -0
- package/tools/skills/vds-scripts-skill/references/capability-index.md +34 -0
- package/tools/skills/vds-scripts-skill/references/development-commands.md +12 -0
- package/tools/skills/vds-scripts-skill/references/google-sheets.md +73 -0
- package/tools/skills/vds-scripts-skill/references/integration-commands.md +17 -0
- package/tools/skills/vds-scripts-skill/references/platform-bootstrap.md +31 -0
- package/tools/skills/vds-scripts-skill/references/specialist-routing.md +14 -0
- package/tools/skills/vds-scripts-skill/references/validation-commands.md +15 -0
- package/tools/skills/vsaf-build/SKILL.md +32 -2
- package/tools/skills/vsaf-ship/SKILL.md +41 -10
- package/tools/skills/vsaf-test/SKILL.md +8 -0
- package/tools/vds-scripts/.mcp.json +11 -0
- package/tools/vds-scripts/.secrets.baseline +133 -0
- package/tools/vds-scripts/AGENTS.md +152 -0
- package/tools/vds-scripts/CLAUDE.md +101 -0
- package/tools/vds-scripts/CLI_COMMAND_OPTIMIZATION.md +156 -0
- package/tools/vds-scripts/PACKAGE_P125B_IMPLEMENTATION_SUMMARY.md +131 -0
- package/tools/vds-scripts/PROJECT_COMPLETION_SUMMARY.md +45 -0
- package/tools/vds-scripts/README.md +97 -0
- package/tools/vds-scripts/bitbucket_manifest_mapping.toml +34 -0
- package/tools/vds-scripts/bitbucket_orchestrator/ARCHITECTURE_ANALYSIS.md +258 -0
- package/tools/vds-scripts/bitbucket_orchestrator/BITBUCKET_API_PRACTICES.md +393 -0
- package/tools/vds-scripts/bitbucket_orchestrator/EVALUATION_REPORT.md +61 -0
- package/tools/vds-scripts/bitbucket_orchestrator/FEATURES.md +908 -0
- package/tools/vds-scripts/bitbucket_orchestrator/README.md +687 -0
- package/tools/vds-scripts/bitbucket_orchestrator/pyproject.toml +40 -0
- package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/__init__.py +20 -0
- package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/async_client.py +657 -0
- package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/cli.py +2108 -0
- package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/client.py +2534 -0
- package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/config.py +171 -0
- package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/errors.py +67 -0
- package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/factory.py +185 -0
- package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/protocols.py +244 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/__init__.py +8 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/conftest.py +65 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_advanced_search.py +151 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_async_client.py +546 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_branch_permissions.py +145 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_cli.py +115 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client.py +157 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_branch_conditions.py +79 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_code_advanced.py +163 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_code_file.py +32 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_deployment_environments.py +194 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_issues.py +164 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_pipelines_advanced.py +179 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_pr_blockers.py +119 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_repository_variables.py +156 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_code.py +98 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_code_advanced.py +282 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_code_insights.py +335 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_conditions.py +147 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_config.py +131 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_deployment_env.py +352 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_factory.py +371 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_fork_operations.py +204 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_issue_cli.py +261 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_pipeline_advanced.py +270 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_pr_blocker.py +204 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_protocols.py +334 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_repo_settings.py +343 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_repo_variables.py +270 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_webhooks.py +189 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_workspace.py +233 -0
- package/tools/vds-scripts/bitbucket_orchestrator/uv.lock +742 -0
- package/tools/vds-scripts/confluence_orchestrator/Dockerfile +19 -0
- package/tools/vds-scripts/confluence_orchestrator/README.md +412 -0
- package/tools/vds-scripts/confluence_orchestrator/SYNC_SCRIPTS.md +127 -0
- package/tools/vds-scripts/confluence_orchestrator/SYNC_STANDARDIZATION.md +108 -0
- package/tools/vds-scripts/confluence_orchestrator/pyproject.toml +48 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/__init__.py +20 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/cli.py +2532 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/config.py +175 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/content.py +290 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/content_v2.py +94 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/crawl_tree.py +1835 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/errors.py +80 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/eventing.py +109 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/http.py +1114 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/orchestration.py +165 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/reporting.py +78 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/tree.py +121 -0
- package/tools/vds-scripts/confluence_orchestrator/sync_pdfs_from_markdown.py +213 -0
- package/tools/vds-scripts/confluence_orchestrator/sync_pdfs_to_confluence.py +305 -0
- package/tools/vds-scripts/confluence_orchestrator/sync_png_attachments.py +305 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/__init__.py +0 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/conftest.py +8 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_advanced_content.py +224 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_advanced_search.py +188 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_cache_management.py +247 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_cli.py +499 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_config.py +83 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_content.py +186 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_content_flags.py +27 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_crawl_tree.py +2250 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_draft_management.py +223 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_eventing.py +71 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_eventing_chaos.py +37 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_eventing_rate_limit.py +44 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_eventing_timeout.py +49 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_export.py +230 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_history.py +204 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_http.py +117 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_orchestration.py +91 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_reporting.py +24 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_search_cql.py +34 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_space_management.py +237 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_space_permissions.py +332 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_user_group_management.py +388 -0
- package/tools/vds-scripts/confluence_orchestrator/uv.lock +1023 -0
- package/tools/vds-scripts/git_orchestrator/ENHANCEMENT_SUMMARY.md +119 -0
- package/tools/vds-scripts/git_orchestrator/README.md +280 -0
- package/tools/vds-scripts/git_orchestrator/VERIFICATION_REPORT.md +152 -0
- package/tools/vds-scripts/git_orchestrator/pyproject.toml +35 -0
- package/tools/vds-scripts/git_orchestrator/src/vds_git_orchestrator/__init__.py +7 -0
- package/tools/vds-scripts/git_orchestrator/src/vds_git_orchestrator/__main__.py +4 -0
- package/tools/vds-scripts/git_orchestrator/src/vds_git_orchestrator/cli.py +847 -0
- package/tools/vds-scripts/git_orchestrator/src/vds_git_orchestrator/logging_config.py +63 -0
- package/tools/vds-scripts/git_orchestrator/src/vds_git_orchestrator/manifest.py +129 -0
- package/tools/vds-scripts/git_orchestrator/src/vds_git_orchestrator/orchestrator.py +819 -0
- package/tools/vds-scripts/git_orchestrator/src/vds_git_orchestrator/reporting.py +53 -0
- package/tools/vds-scripts/git_orchestrator/tests/__init__.py +0 -0
- package/tools/vds-scripts/git_orchestrator/tests/test_cli_settings.py +21 -0
- package/tools/vds-scripts/git_orchestrator/tests/test_integration.py +74 -0
- package/tools/vds-scripts/git_orchestrator/tests/test_manifest.py +79 -0
- package/tools/vds-scripts/git_orchestrator/tests/test_orchestrator.py +204 -0
- package/tools/vds-scripts/git_orchestrator/tests/test_public_api.py +236 -0
- package/tools/vds-scripts/git_orchestrator/tests/test_resilience.py +345 -0
- package/tools/vds-scripts/git_orchestrator/uv.lock +271 -0
- package/tools/vds-scripts/jira_orchestrator/README.md +770 -0
- package/tools/vds-scripts/jira_orchestrator/pyproject.toml +39 -0
- package/tools/vds-scripts/jira_orchestrator/src/vds_jira_orchestrator/__init__.py +1 -0
- package/tools/vds-scripts/jira_orchestrator/src/vds_jira_orchestrator/adapter.py +1320 -0
- package/tools/vds-scripts/jira_orchestrator/src/vds_jira_orchestrator/cli.py +2271 -0
- package/tools/vds-scripts/jira_orchestrator/src/vds_jira_orchestrator/config.py +138 -0
- package/tools/vds-scripts/jira_orchestrator/src/vds_jira_orchestrator/errors.py +67 -0
- package/tools/vds-scripts/jira_orchestrator/src/vds_jira_orchestrator/reporting.py +65 -0
- package/tools/vds-scripts/jira_orchestrator/tests/__init__.py +1 -0
- package/tools/vds-scripts/jira_orchestrator/tests/conftest.py +86 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_agile_list_payloads.py +54 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_bulk_operations.py +69 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_components.py +57 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_createmeta.py +45 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_dashboard.py +117 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_issue_properties.py +54 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_permissions_compat.py +42 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_reindex.py +42 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_remote_links.py +76 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_transitions.py +91 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_user_management.py +110 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_version_management.py +133 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_watchers.py +41 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_advanced_search.py +164 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_agile.py +256 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_application_properties.py +193 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_backlog.py +91 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_bulk_operations.py +277 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_cli.py +106 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_components.py +106 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_config.py +164 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_dashboard.py +122 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_discover_fields.py +207 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_filter_management.py +333 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_issue_archiving.py +164 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_issue_links.py +257 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_issue_properties.py +171 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_link_types.py +314 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_parse_set.py +37 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_permissions.py +273 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_reindex.py +81 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_remote_links.py +254 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_security_schemes.py +170 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_transitions_changelog.py +114 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_user_management.py +226 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_version_management.py +339 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_watchers.py +101 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_worklog.py +223 -0
- package/tools/vds-scripts/jira_orchestrator/uv.lock +738 -0
- package/tools/vds-scripts/mcp_server/Dockerfile +34 -0
- package/tools/vds-scripts/mcp_server/README.md +140 -0
- package/tools/vds-scripts/mcp_server/pyproject.toml +42 -0
- package/tools/vds-scripts/mcp_server/src/vds_mcp_server/__init__.py +4 -0
- package/tools/vds-scripts/mcp_server/src/vds_mcp_server/config.py +36 -0
- package/tools/vds-scripts/mcp_server/src/vds_mcp_server/server.py +66 -0
- package/tools/vds-scripts/mcp_server/src/vds_mcp_server/tools/__init__.py +14 -0
- package/tools/vds-scripts/mcp_server/src/vds_mcp_server/tools/bitbucket_tools.py +47 -0
- package/tools/vds-scripts/mcp_server/src/vds_mcp_server/tools/confluence_tools.py +59 -0
- package/tools/vds-scripts/mcp_server/src/vds_mcp_server/tools/git_tools.py +71 -0
- package/tools/vds-scripts/mcp_server/src/vds_mcp_server/tools/jira_tools.py +63 -0
- package/tools/vds-scripts/mcp_server/tests/__init__.py +2 -0
- package/tools/vds-scripts/mcp_server/tests/conftest.py +29 -0
- package/tools/vds-scripts/mcp_server/tests/unit/__init__.py +2 -0
- package/tools/vds-scripts/mcp_server/tests/unit/test_bitbucket_tools.py +25 -0
- package/tools/vds-scripts/mcp_server/tests/unit/test_confluence_tools.py +25 -0
- package/tools/vds-scripts/mcp_server/tests/unit/test_git_tools.py +32 -0
- package/tools/vds-scripts/mcp_server/tests/unit/test_jira_tools.py +32 -0
- package/tools/vds-scripts/mcp_server/tests/verification/__init__.py +2 -0
- package/tools/vds-scripts/mcp_server/tests/verification/test_mcp_confluence_tools.py +40 -0
- package/tools/vds-scripts/mcp_server/tests/verification/test_mcp_jira_tools.py +37 -0
- package/tools/vds-scripts/mcp_server/tests/verification/test_mcp_tool_registration.py +47 -0
- package/tools/vds-scripts/mcp_server/uv.lock +1032 -0
- package/tools/vds-scripts/mypy.ini +5 -0
- package/tools/vds-scripts/pyproject.toml +29 -0
- package/tools/vds-scripts/repo-manifest.yaml +273 -0
- package/tools/vds-scripts/repo-manifest.yaml.example +25 -0
- package/tools/vds-scripts/scripts/BRD-Validation-API.postman_collection.json +706 -0
- package/tools/vds-scripts/scripts/BRD-Validation-README.md +308 -0
- package/tools/vds-scripts/scripts/README.md +162 -0
- package/tools/vds-scripts/scripts/bootstrap_uv.sh +30 -0
- package/tools/vds-scripts/scripts/brd-validation-environment.json +51 -0
- package/tools/vds-scripts/scripts/brd-validation-test-results.json +13023 -0
- package/tools/vds-scripts/scripts/brd_coverage_report.json +276 -0
- package/tools/vds-scripts/scripts/create_memory_session.py +35 -0
- package/tools/vds-scripts/scripts/deployment/load_docker_images_offline.sh +90 -0
- package/tools/vds-scripts/scripts/final_completion_report.md +139 -0
- package/tools/vds-scripts/scripts/folder_structure_report.json +321 -0
- package/tools/vds-scripts/scripts/generate_completion_report.py +125 -0
- package/tools/vds-scripts/scripts/generate_intellij_modules.py +150 -0
- package/tools/vds-scripts/scripts/link_integrity_report.json +807 -0
- package/tools/vds-scripts/scripts/move_audit_artifact_pages.py +255 -0
- package/tools/vds-scripts/scripts/move_audit_artifact_pages_rest.py +165 -0
- package/tools/vds-scripts/scripts/move_wrong_dept_pages.py +216 -0
- package/tools/vds-scripts/scripts/save_intellij_memories.py +120 -0
- package/tools/vds-scripts/scripts/save_memories_to_vds_ai.py +83 -0
- package/tools/vds-scripts/scripts/save_memories_vds_style.py +129 -0
- package/tools/vds-scripts/scripts/search_intellij_memories.py +50 -0
- package/tools/vds-scripts/scripts/setup_intellij_workspace.py +65 -0
- package/tools/vds-scripts/scripts/target-state-automation/README.md +89 -0
- package/tools/vds-scripts/scripts/target-state-automation/confluence_sync_coordinator.sh +27 -0
- package/tools/vds-scripts/scripts/target-state-automation/coordination.sh +114 -0
- package/tools/vds-scripts/scripts/target-state-automation/diagram_coordinator.sh +25 -0
- package/tools/vds-scripts/scripts/target-state-automation/docs_root.sh +22 -0
- package/tools/vds-scripts/scripts/target-state-automation/generate_diagrams.sh +22 -0
- package/tools/vds-scripts/scripts/target-state-automation/markdown_coordinator.sh +25 -0
- package/tools/vds-scripts/scripts/target-state-automation/progress_dashboard.sh +17 -0
- package/tools/vds-scripts/scripts/target-state-automation/schema_coordinator.sh +25 -0
- package/tools/vds-scripts/scripts/target-state-automation/sync_confluence.sh +30 -0
- package/tools/vds-scripts/scripts/target-state-automation/update_dependencies.sh +19 -0
- package/tools/vds-scripts/scripts/target-state-automation/validate_links.sh +86 -0
- package/tools/vds-scripts/scripts/target-state-automation/validate_markdown.sh +52 -0
- package/tools/vds-scripts/scripts/target-state-automation/validate_schemas.sh +26 -0
- package/tools/vds-scripts/scripts/target-state-automation/validate_structure.sh +98 -0
- package/tools/vds-scripts/scripts/update_modules_xml.py +190 -0
- package/tools/vds-scripts/scripts/uv-workspace-alignment-verification-2026-03-25.md +128 -0
- package/tools/vds-scripts/scripts/validate_brd_coverage.py +179 -0
- package/tools/vds-scripts/scripts/validate_folder_structure.py +240 -0
- package/tools/vds-scripts/scripts/validate_link_integrity.py +272 -0
- package/tools/vds-scripts/scripts/vds_sh_helpers.sh +180 -0
- package/tools/vds-scripts/scripts/verification/phase2_portable_paths_ubuntu_docker.sh +26 -0
- package/tools/vds-scripts/scripts/worktree_uv.sh +48 -0
- package/tools/vds-scripts/uv.lock +8 -0
- package/tools/vds-scripts/vds_cli/README.md +126 -0
- package/tools/vds-scripts/vds_cli/VERIFICATION_REPORT.md +41 -0
- package/tools/vds-scripts/vds_cli/pyproject.toml +38 -0
- package/tools/vds-scripts/vds_cli/src/vds_cli/__init__.py +3 -0
- package/tools/vds-scripts/vds_cli/src/vds_cli/cli.py +173 -0
- package/tools/vds-scripts/vds_cli/src/vds_cli/docs_sync.py +1203 -0
- package/tools/vds-scripts/vds_cli/src/vds_cli/env.py +41 -0
- package/tools/vds-scripts/vds_cli/src/vds_cli/google_sheets_orchestrator/__init__.py +3 -0
- package/tools/vds-scripts/vds_cli/src/vds_cli/google_sheets_orchestrator/google_sheets_orchestrator.py +198 -0
- package/tools/vds-scripts/vds_cli/src/vds_cli/router.py +93 -0
- package/tools/vds-scripts/vds_cli/src/vds_cli/sync_api.py +647 -0
- package/tools/vds-scripts/vds_cli/src/vds_cli/sync_service.py +266 -0
- package/tools/vds-scripts/vds_cli/tests/__init__.py +2 -0
- package/tools/vds-scripts/vds_cli/tests/conftest.py +49 -0
- package/tools/vds-scripts/vds_cli/tests/unit/__init__.py +2 -0
- package/tools/vds-scripts/vds_cli/tests/unit/test_cli.py +143 -0
- package/tools/vds-scripts/vds_cli/tests/unit/test_docs_sync.py +422 -0
- package/tools/vds-scripts/vds_cli/tests/unit/test_env.py +51 -0
- package/tools/vds-scripts/vds_cli/tests/unit/test_router.py +72 -0
- package/tools/vds-scripts/vds_cli/tests/unit/test_sync_api.py +357 -0
- package/tools/vds-scripts/vds_cli/tests/unit/test_sync_service.py +160 -0
- package/tools/vds-scripts/vds_cli/tests/verification/__init__.py +2 -0
- package/tools/vds-scripts/vds_cli/tests/verification/test_bitbucket_real.py +33 -0
- package/tools/vds-scripts/vds_cli/tests/verification/test_confluence_real.py +35 -0
- package/tools/vds-scripts/vds_cli/tests/verification/test_jira_real.py +41 -0
- package/tools/vds-scripts/vds_cli/uv.lock +524 -0
- package/tools/vds-scripts/vds_cli_common/README.md +190 -0
- package/tools/vds-scripts/vds_cli_common/pyproject.toml +92 -0
- package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/__init__.py +34 -0
- package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/completers.py +139 -0
- package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/context.py +201 -0
- package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/env.py +119 -0
- package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/errors.py +318 -0
- package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/output.py +284 -0
- package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/paths.py +78 -0
- package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/testing.py +213 -0
- package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/version.py +85 -0
- package/tools/vds-scripts/vds_cli_common/tests/__init__.py +1 -0
- package/tools/vds-scripts/vds_cli_common/tests/test_completers.py +148 -0
- package/tools/vds-scripts/vds_cli_common/tests/test_context.py +192 -0
- package/tools/vds-scripts/vds_cli_common/tests/test_env.py +102 -0
- package/tools/vds-scripts/vds_cli_common/tests/test_errors.py +186 -0
- package/tools/vds-scripts/vds_cli_common/tests/test_output.py +229 -0
- package/tools/vds-scripts/vds_cli_common/tests/test_paths.py +61 -0
- package/tools/vds-scripts/vds_cli_common/tests/test_testing.py +138 -0
- package/tools/vds-scripts/vds_cli_common/tests/test_version.py +64 -0
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
"""HTTP central-state clients for instruction sync."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from datetime import UTC, datetime
|
|
10
|
+
from urllib.error import HTTPError, URLError
|
|
11
|
+
from urllib.parse import quote
|
|
12
|
+
from urllib.request import Request, urlopen
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SyncApiError(RuntimeError):
|
|
16
|
+
"""Base error for central-state HTTP client failures."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SyncApiRevisionConflict(SyncApiError):
|
|
20
|
+
"""Raised when optimistic concurrency check fails."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SyncApiTransientError(SyncApiError):
|
|
24
|
+
"""Raised when HTTP/network failures are transient and retryable."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class SyncApiDocument:
|
|
29
|
+
"""Canonical document representation returned by central-state API."""
|
|
30
|
+
|
|
31
|
+
content: str
|
|
32
|
+
revision: str
|
|
33
|
+
content_hash: str
|
|
34
|
+
source_file: str
|
|
35
|
+
updated_at: str
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class SyncApiClient:
|
|
40
|
+
"""Generic HTTP client with If-Match/If-None-Match semantics."""
|
|
41
|
+
|
|
42
|
+
endpoint: str
|
|
43
|
+
timeout_seconds: int = 10
|
|
44
|
+
auth_token: str | None = None
|
|
45
|
+
|
|
46
|
+
def pull_document(self) -> SyncApiDocument | None:
|
|
47
|
+
request = Request(
|
|
48
|
+
self.endpoint,
|
|
49
|
+
headers=self._auth_headers(),
|
|
50
|
+
method="GET",
|
|
51
|
+
)
|
|
52
|
+
try:
|
|
53
|
+
with urlopen(request, timeout=self.timeout_seconds) as response: # noqa: S310
|
|
54
|
+
payload = self._read_json_response(response)
|
|
55
|
+
if payload is None:
|
|
56
|
+
return None
|
|
57
|
+
revision = _normalize_revision(
|
|
58
|
+
etag=response.headers.get("ETag"),
|
|
59
|
+
payload_revision=payload.get("revision"),
|
|
60
|
+
)
|
|
61
|
+
return _payload_to_document(payload, revision=revision)
|
|
62
|
+
except HTTPError as exc:
|
|
63
|
+
if exc.code == 404:
|
|
64
|
+
return None
|
|
65
|
+
if _is_transient_http_code(exc.code):
|
|
66
|
+
raise SyncApiTransientError(
|
|
67
|
+
f"Transient HTTP {exc.code} when pulling central document."
|
|
68
|
+
) from exc
|
|
69
|
+
raise SyncApiError(f"HTTP {exc.code} when pulling central document.") from exc
|
|
70
|
+
except URLError as exc:
|
|
71
|
+
raise SyncApiTransientError(
|
|
72
|
+
f"Network error pulling central document: {exc.reason}"
|
|
73
|
+
) from exc
|
|
74
|
+
|
|
75
|
+
def push_document_if_match(
|
|
76
|
+
self,
|
|
77
|
+
*,
|
|
78
|
+
content: str,
|
|
79
|
+
source_file: str,
|
|
80
|
+
expected_revision: str | None,
|
|
81
|
+
) -> SyncApiDocument:
|
|
82
|
+
headers = {
|
|
83
|
+
"Content-Type": "application/json",
|
|
84
|
+
**self._auth_headers(),
|
|
85
|
+
}
|
|
86
|
+
if expected_revision is None:
|
|
87
|
+
headers["If-None-Match"] = "*"
|
|
88
|
+
else:
|
|
89
|
+
headers["If-Match"] = _format_etag(expected_revision)
|
|
90
|
+
|
|
91
|
+
payload = {
|
|
92
|
+
"content": content,
|
|
93
|
+
"source_file": source_file,
|
|
94
|
+
"content_hash": hashlib.sha256(content.encode("utf-8")).hexdigest(),
|
|
95
|
+
"updated_at": datetime.now(UTC).isoformat(),
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
request = Request(
|
|
99
|
+
self.endpoint,
|
|
100
|
+
data=json.dumps(payload).encode("utf-8"),
|
|
101
|
+
headers=headers,
|
|
102
|
+
method="PUT",
|
|
103
|
+
)
|
|
104
|
+
try:
|
|
105
|
+
with urlopen(request, timeout=self.timeout_seconds) as response: # noqa: S310
|
|
106
|
+
response_payload = self._read_json_response(response)
|
|
107
|
+
if response_payload is None:
|
|
108
|
+
document = self.pull_document()
|
|
109
|
+
if document is None:
|
|
110
|
+
raise SyncApiError(
|
|
111
|
+
"Central API returned empty PUT response and GET could not load canonical document."
|
|
112
|
+
) from None
|
|
113
|
+
return document
|
|
114
|
+
revision = _normalize_revision(
|
|
115
|
+
etag=response.headers.get("ETag"),
|
|
116
|
+
payload_revision=response_payload.get("revision"),
|
|
117
|
+
)
|
|
118
|
+
return _payload_to_document(response_payload, revision=revision)
|
|
119
|
+
except HTTPError as exc:
|
|
120
|
+
if exc.code == 412:
|
|
121
|
+
raise SyncApiRevisionConflict("Central revision mismatch (HTTP 412).") from exc
|
|
122
|
+
if _is_transient_http_code(exc.code):
|
|
123
|
+
raise SyncApiTransientError(
|
|
124
|
+
f"Transient HTTP {exc.code} when pushing central document."
|
|
125
|
+
) from exc
|
|
126
|
+
raise SyncApiError(f"HTTP {exc.code} when pushing central document.") from exc
|
|
127
|
+
except URLError as exc:
|
|
128
|
+
raise SyncApiTransientError(
|
|
129
|
+
f"Network error pushing central document: {exc.reason}"
|
|
130
|
+
) from exc
|
|
131
|
+
|
|
132
|
+
def _auth_headers(self) -> dict[str, str]:
|
|
133
|
+
if not self.auth_token:
|
|
134
|
+
return {}
|
|
135
|
+
return {"Authorization": f"Bearer {self.auth_token}"}
|
|
136
|
+
|
|
137
|
+
@staticmethod
|
|
138
|
+
def _read_json_response(response) -> dict[str, object] | None:
|
|
139
|
+
raw = response.read()
|
|
140
|
+
if not raw:
|
|
141
|
+
return None
|
|
142
|
+
try:
|
|
143
|
+
loaded = json.loads(raw.decode("utf-8"))
|
|
144
|
+
except json.JSONDecodeError as exc:
|
|
145
|
+
raise SyncApiError("Central API returned non-JSON response payload.") from exc
|
|
146
|
+
if not isinstance(loaded, dict):
|
|
147
|
+
raise SyncApiError("Central API payload must be a JSON object.")
|
|
148
|
+
return loaded
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@dataclass
|
|
152
|
+
class VdsAiMemorySyncApiClient:
|
|
153
|
+
"""VDS AI Memory contract adapter (`/api/v1/memories/{memory_id}`)."""
|
|
154
|
+
|
|
155
|
+
base_url: str
|
|
156
|
+
memory_id: str
|
|
157
|
+
user_id: str
|
|
158
|
+
session_id: str
|
|
159
|
+
timeout_seconds: int = 10
|
|
160
|
+
auth_token: str | None = None
|
|
161
|
+
|
|
162
|
+
def pull_document(self) -> SyncApiDocument | None:
|
|
163
|
+
request = Request(self._memory_url, headers=self._auth_headers(), method="GET")
|
|
164
|
+
try:
|
|
165
|
+
with urlopen(request, timeout=self.timeout_seconds) as response: # noqa: S310
|
|
166
|
+
payload = SyncApiClient._read_json_response(response)
|
|
167
|
+
if payload is None:
|
|
168
|
+
return None
|
|
169
|
+
return _memory_payload_to_document(payload)
|
|
170
|
+
except HTTPError as exc:
|
|
171
|
+
if exc.code == 404:
|
|
172
|
+
return None
|
|
173
|
+
if _is_transient_http_code(exc.code):
|
|
174
|
+
raise SyncApiTransientError(
|
|
175
|
+
f"Transient HTTP {exc.code} when pulling VDS AI Memory document."
|
|
176
|
+
) from exc
|
|
177
|
+
raise SyncApiError(f"HTTP {exc.code} when pulling VDS AI Memory document.") from exc
|
|
178
|
+
except URLError as exc:
|
|
179
|
+
raise SyncApiTransientError(
|
|
180
|
+
f"Network error pulling VDS AI Memory document: {exc.reason}"
|
|
181
|
+
) from exc
|
|
182
|
+
|
|
183
|
+
def push_document_if_match(
|
|
184
|
+
self,
|
|
185
|
+
*,
|
|
186
|
+
content: str,
|
|
187
|
+
source_file: str,
|
|
188
|
+
expected_revision: str | None,
|
|
189
|
+
) -> SyncApiDocument:
|
|
190
|
+
current = self.pull_document()
|
|
191
|
+
if current is None:
|
|
192
|
+
if expected_revision is not None:
|
|
193
|
+
raise SyncApiRevisionConflict(
|
|
194
|
+
"Central VDS AI Memory record missing while expected revision was provided."
|
|
195
|
+
)
|
|
196
|
+
return self._create_memory(content=content, source_file=source_file)
|
|
197
|
+
|
|
198
|
+
if expected_revision is not None and current.revision != expected_revision:
|
|
199
|
+
raise SyncApiRevisionConflict(
|
|
200
|
+
f"Central revision mismatch (expected={expected_revision}, actual={current.revision})."
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
expected_revision_number = (
|
|
204
|
+
_parse_int_revision(expected_revision)
|
|
205
|
+
if expected_revision is not None
|
|
206
|
+
else None
|
|
207
|
+
)
|
|
208
|
+
if expected_revision is not None and expected_revision_number is None:
|
|
209
|
+
raise SyncApiError(
|
|
210
|
+
"VDS AI Memory expected revision must be an integer current_version_number."
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
payload = {
|
|
214
|
+
"content": content,
|
|
215
|
+
"metadata": _sync_metadata(
|
|
216
|
+
source_file=source_file,
|
|
217
|
+
content=content,
|
|
218
|
+
),
|
|
219
|
+
"user_id": self.user_id,
|
|
220
|
+
"session_id": self.session_id,
|
|
221
|
+
}
|
|
222
|
+
headers = {
|
|
223
|
+
"Content-Type": "application/json",
|
|
224
|
+
**self._auth_headers(),
|
|
225
|
+
}
|
|
226
|
+
if expected_revision_number is not None:
|
|
227
|
+
payload["expected_current_version_number"] = expected_revision_number
|
|
228
|
+
headers["If-Match"] = _format_etag(str(expected_revision_number))
|
|
229
|
+
|
|
230
|
+
request = Request(
|
|
231
|
+
self._memory_url,
|
|
232
|
+
data=json.dumps(payload).encode("utf-8"),
|
|
233
|
+
headers=headers,
|
|
234
|
+
method="PATCH",
|
|
235
|
+
)
|
|
236
|
+
try:
|
|
237
|
+
with urlopen(request, timeout=self.timeout_seconds) as response: # noqa: S310
|
|
238
|
+
patch_payload = SyncApiClient._read_json_response(response)
|
|
239
|
+
if patch_payload is not None:
|
|
240
|
+
return _memory_payload_to_document(patch_payload)
|
|
241
|
+
refreshed = self.pull_document()
|
|
242
|
+
if refreshed is None:
|
|
243
|
+
raise SyncApiError("VDS AI Memory patch succeeded but follow-up GET returned empty response.")
|
|
244
|
+
return refreshed
|
|
245
|
+
except HTTPError as exc:
|
|
246
|
+
if exc.code == 409:
|
|
247
|
+
raise SyncApiRevisionConflict("Central revision mismatch (HTTP 409).") from exc
|
|
248
|
+
if _is_transient_http_code(exc.code):
|
|
249
|
+
raise SyncApiTransientError(
|
|
250
|
+
f"Transient HTTP {exc.code} when pushing VDS AI Memory document."
|
|
251
|
+
) from exc
|
|
252
|
+
raise SyncApiError(f"HTTP {exc.code} when pushing VDS AI Memory document.") from exc
|
|
253
|
+
except URLError as exc:
|
|
254
|
+
raise SyncApiTransientError(
|
|
255
|
+
f"Network error pushing VDS AI Memory document: {exc.reason}"
|
|
256
|
+
) from exc
|
|
257
|
+
|
|
258
|
+
@property
|
|
259
|
+
def _api_base(self) -> str:
|
|
260
|
+
base = self.base_url.rstrip("/")
|
|
261
|
+
if base.endswith("/api/v1"):
|
|
262
|
+
return base
|
|
263
|
+
return f"{base}/api/v1"
|
|
264
|
+
|
|
265
|
+
@property
|
|
266
|
+
def _memory_url(self) -> str:
|
|
267
|
+
return f"{self._api_base}/memories/{quote(self.memory_id, safe='')}"
|
|
268
|
+
|
|
269
|
+
@property
|
|
270
|
+
def _create_url(self) -> str:
|
|
271
|
+
return f"{self._api_base}/memories"
|
|
272
|
+
|
|
273
|
+
def _create_memory(self, *, content: str, source_file: str) -> SyncApiDocument:
|
|
274
|
+
payload = {
|
|
275
|
+
"content": content,
|
|
276
|
+
"category": "reference",
|
|
277
|
+
"importance": "medium",
|
|
278
|
+
"user_id": self.user_id,
|
|
279
|
+
"session_id": self.session_id,
|
|
280
|
+
"metadata": _sync_metadata(source_file=source_file, content=content),
|
|
281
|
+
}
|
|
282
|
+
request = Request(
|
|
283
|
+
self._create_url,
|
|
284
|
+
data=json.dumps(payload).encode("utf-8"),
|
|
285
|
+
headers={
|
|
286
|
+
"Content-Type": "application/json",
|
|
287
|
+
**self._auth_headers(),
|
|
288
|
+
},
|
|
289
|
+
method="POST",
|
|
290
|
+
)
|
|
291
|
+
try:
|
|
292
|
+
with urlopen(request, timeout=self.timeout_seconds) as response: # noqa: S310
|
|
293
|
+
create_payload = SyncApiClient._read_json_response(response)
|
|
294
|
+
except HTTPError as exc:
|
|
295
|
+
if _is_transient_http_code(exc.code):
|
|
296
|
+
raise SyncApiTransientError(
|
|
297
|
+
f"Transient HTTP {exc.code} when creating VDS AI Memory record."
|
|
298
|
+
) from exc
|
|
299
|
+
raise SyncApiError(f"HTTP {exc.code} when creating VDS AI Memory record.") from exc
|
|
300
|
+
except URLError as exc:
|
|
301
|
+
raise SyncApiTransientError(
|
|
302
|
+
f"Network error creating VDS AI Memory record: {exc.reason}"
|
|
303
|
+
) from exc
|
|
304
|
+
|
|
305
|
+
if create_payload is None:
|
|
306
|
+
raise SyncApiError("VDS AI Memory create response was empty.")
|
|
307
|
+
created_id = create_payload.get("memory_id")
|
|
308
|
+
if not isinstance(created_id, str) or not created_id.strip():
|
|
309
|
+
raise SyncApiError("VDS AI Memory create response missing memory_id.")
|
|
310
|
+
self.memory_id = created_id
|
|
311
|
+
created = self.pull_document()
|
|
312
|
+
if created is None:
|
|
313
|
+
raise SyncApiError("VDS AI Memory create succeeded but follow-up GET failed.")
|
|
314
|
+
return created
|
|
315
|
+
|
|
316
|
+
def _auth_headers(self) -> dict[str, str]:
|
|
317
|
+
if not self.auth_token:
|
|
318
|
+
return {}
|
|
319
|
+
return {"Authorization": f"Bearer {self.auth_token}"}
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@dataclass
|
|
323
|
+
class ConfluencePropertySyncApiClient:
|
|
324
|
+
"""Confluence content-property contract adapter."""
|
|
325
|
+
|
|
326
|
+
base_url: str
|
|
327
|
+
page_id: str
|
|
328
|
+
property_key: str
|
|
329
|
+
timeout_seconds: int = 10
|
|
330
|
+
auth_token: str | None = None
|
|
331
|
+
username: str | None = None
|
|
332
|
+
password: str | None = None
|
|
333
|
+
|
|
334
|
+
def pull_document(self) -> SyncApiDocument | None:
|
|
335
|
+
request = Request(self._property_url, headers=self._auth_headers(), method="GET")
|
|
336
|
+
try:
|
|
337
|
+
with urlopen(request, timeout=self.timeout_seconds) as response: # noqa: S310
|
|
338
|
+
payload = SyncApiClient._read_json_response(response)
|
|
339
|
+
if payload is None:
|
|
340
|
+
return None
|
|
341
|
+
return _confluence_property_payload_to_document(payload)
|
|
342
|
+
except HTTPError as exc:
|
|
343
|
+
if exc.code == 404:
|
|
344
|
+
return None
|
|
345
|
+
if _is_transient_http_code(exc.code):
|
|
346
|
+
raise SyncApiTransientError(
|
|
347
|
+
f"Transient HTTP {exc.code} when pulling Confluence sync property."
|
|
348
|
+
) from exc
|
|
349
|
+
raise SyncApiError(f"HTTP {exc.code} when pulling Confluence sync property.") from exc
|
|
350
|
+
except URLError as exc:
|
|
351
|
+
raise SyncApiTransientError(
|
|
352
|
+
f"Network error pulling Confluence sync property: {exc.reason}"
|
|
353
|
+
) from exc
|
|
354
|
+
|
|
355
|
+
def push_document_if_match(
|
|
356
|
+
self,
|
|
357
|
+
*,
|
|
358
|
+
content: str,
|
|
359
|
+
source_file: str,
|
|
360
|
+
expected_revision: str | None,
|
|
361
|
+
) -> SyncApiDocument:
|
|
362
|
+
current = self.pull_document()
|
|
363
|
+
if expected_revision is None:
|
|
364
|
+
if current is not None:
|
|
365
|
+
raise SyncApiRevisionConflict(
|
|
366
|
+
f"Expected empty Confluence property but found revision {current.revision}."
|
|
367
|
+
)
|
|
368
|
+
return self._create_property(content=content, source_file=source_file)
|
|
369
|
+
|
|
370
|
+
if current is None:
|
|
371
|
+
raise SyncApiRevisionConflict(
|
|
372
|
+
"Expected existing Confluence property revision but property was missing."
|
|
373
|
+
)
|
|
374
|
+
if current.revision != expected_revision:
|
|
375
|
+
raise SyncApiRevisionConflict(
|
|
376
|
+
f"Central revision mismatch (expected={expected_revision}, actual={current.revision})."
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
next_version = _next_revision_number(current.revision)
|
|
380
|
+
payload = {
|
|
381
|
+
"key": self.property_key,
|
|
382
|
+
"value": _sync_payload_value(
|
|
383
|
+
source_file=source_file,
|
|
384
|
+
content=content,
|
|
385
|
+
),
|
|
386
|
+
"version": {"number": next_version},
|
|
387
|
+
}
|
|
388
|
+
request = Request(
|
|
389
|
+
self._property_url,
|
|
390
|
+
data=json.dumps(payload).encode("utf-8"),
|
|
391
|
+
headers={
|
|
392
|
+
"Content-Type": "application/json",
|
|
393
|
+
**self._auth_headers(),
|
|
394
|
+
},
|
|
395
|
+
method="PUT",
|
|
396
|
+
)
|
|
397
|
+
try:
|
|
398
|
+
with urlopen(request, timeout=self.timeout_seconds) as response: # noqa: S310
|
|
399
|
+
response_payload = SyncApiClient._read_json_response(response)
|
|
400
|
+
if response_payload is None:
|
|
401
|
+
refreshed = self.pull_document()
|
|
402
|
+
if refreshed is None:
|
|
403
|
+
raise SyncApiError(
|
|
404
|
+
"Confluence sync property update succeeded but follow-up GET failed."
|
|
405
|
+
) from None
|
|
406
|
+
return refreshed
|
|
407
|
+
return _confluence_property_payload_to_document(response_payload)
|
|
408
|
+
except HTTPError as exc:
|
|
409
|
+
if exc.code in {409, 412}:
|
|
410
|
+
raise SyncApiRevisionConflict("Central revision mismatch from Confluence.") from exc
|
|
411
|
+
if _is_transient_http_code(exc.code):
|
|
412
|
+
raise SyncApiTransientError(
|
|
413
|
+
f"Transient HTTP {exc.code} when pushing Confluence sync property."
|
|
414
|
+
) from exc
|
|
415
|
+
raise SyncApiError(f"HTTP {exc.code} when pushing Confluence sync property.") from exc
|
|
416
|
+
except URLError as exc:
|
|
417
|
+
raise SyncApiTransientError(
|
|
418
|
+
f"Network error pushing Confluence sync property: {exc.reason}"
|
|
419
|
+
) from exc
|
|
420
|
+
|
|
421
|
+
@property
|
|
422
|
+
def _property_url(self) -> str:
|
|
423
|
+
return (
|
|
424
|
+
f"{self.base_url.rstrip('/')}/rest/api/content/{quote(self.page_id, safe='')}/property/"
|
|
425
|
+
f"{quote(self.property_key, safe='')}"
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
@property
|
|
429
|
+
def _create_url(self) -> str:
|
|
430
|
+
return f"{self.base_url.rstrip('/')}/rest/api/content/{quote(self.page_id, safe='')}/property"
|
|
431
|
+
|
|
432
|
+
def _create_property(self, *, content: str, source_file: str) -> SyncApiDocument:
|
|
433
|
+
payload = {
|
|
434
|
+
"key": self.property_key,
|
|
435
|
+
"value": _sync_payload_value(source_file=source_file, content=content),
|
|
436
|
+
}
|
|
437
|
+
request = Request(
|
|
438
|
+
self._create_url,
|
|
439
|
+
data=json.dumps(payload).encode("utf-8"),
|
|
440
|
+
headers={
|
|
441
|
+
"Content-Type": "application/json",
|
|
442
|
+
**self._auth_headers(),
|
|
443
|
+
},
|
|
444
|
+
method="POST",
|
|
445
|
+
)
|
|
446
|
+
try:
|
|
447
|
+
with urlopen(request, timeout=self.timeout_seconds) as response: # noqa: S310
|
|
448
|
+
response_payload = SyncApiClient._read_json_response(response)
|
|
449
|
+
except HTTPError as exc:
|
|
450
|
+
if exc.code in {409, 412}:
|
|
451
|
+
raise SyncApiRevisionConflict("Confluence property create conflict.") from exc
|
|
452
|
+
if _is_transient_http_code(exc.code):
|
|
453
|
+
raise SyncApiTransientError(
|
|
454
|
+
f"Transient HTTP {exc.code} when creating Confluence sync property."
|
|
455
|
+
) from exc
|
|
456
|
+
raise SyncApiError(f"HTTP {exc.code} when creating Confluence sync property.") from exc
|
|
457
|
+
except URLError as exc:
|
|
458
|
+
raise SyncApiTransientError(
|
|
459
|
+
f"Network error creating Confluence sync property: {exc.reason}"
|
|
460
|
+
) from exc
|
|
461
|
+
|
|
462
|
+
if response_payload is None:
|
|
463
|
+
refreshed = self.pull_document()
|
|
464
|
+
if refreshed is None:
|
|
465
|
+
raise SyncApiError("Confluence property create succeeded but follow-up GET failed.")
|
|
466
|
+
return refreshed
|
|
467
|
+
return _confluence_property_payload_to_document(response_payload)
|
|
468
|
+
|
|
469
|
+
def _auth_headers(self) -> dict[str, str]:
|
|
470
|
+
if self.auth_token:
|
|
471
|
+
return {"Authorization": f"Bearer {self.auth_token}"}
|
|
472
|
+
if self.username and self.password:
|
|
473
|
+
token = base64.b64encode(f"{self.username}:{self.password}".encode()).decode("ascii")
|
|
474
|
+
return {"Authorization": f"Basic {token}"}
|
|
475
|
+
return {}
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def _format_etag(revision: str) -> str:
|
|
479
|
+
candidate = revision.strip()
|
|
480
|
+
if candidate.startswith('"') and candidate.endswith('"'):
|
|
481
|
+
return candidate
|
|
482
|
+
return f'"{candidate}"'
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def _normalize_revision(*, etag: str | None, payload_revision: object | None) -> str:
|
|
486
|
+
if etag:
|
|
487
|
+
candidate = etag.strip()
|
|
488
|
+
if candidate.startswith("W/"):
|
|
489
|
+
candidate = candidate[2:].strip()
|
|
490
|
+
if candidate.startswith('"') and candidate.endswith('"') and len(candidate) >= 2:
|
|
491
|
+
candidate = candidate[1:-1]
|
|
492
|
+
if candidate:
|
|
493
|
+
return candidate
|
|
494
|
+
if payload_revision is not None:
|
|
495
|
+
value = str(payload_revision).strip()
|
|
496
|
+
if value:
|
|
497
|
+
return value
|
|
498
|
+
raise SyncApiError("Central API response missing revision/ETag.")
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def _payload_to_document(payload: dict[str, object], *, revision: str) -> SyncApiDocument:
|
|
502
|
+
content = _first_str(payload, ("content", "document_body", "body"))
|
|
503
|
+
if content is None:
|
|
504
|
+
raise SyncApiError("Central API payload missing content field.")
|
|
505
|
+
content_hash = _first_str(payload, ("content_hash",))
|
|
506
|
+
if content_hash is None:
|
|
507
|
+
content_hash = hashlib.sha256(content.encode("utf-8")).hexdigest()
|
|
508
|
+
source_file = _first_str(payload, ("source_file",))
|
|
509
|
+
if source_file is None:
|
|
510
|
+
source_file = "<remote>"
|
|
511
|
+
updated_at = _first_str(payload, ("updated_at",))
|
|
512
|
+
if updated_at is None:
|
|
513
|
+
updated_at = datetime.now(UTC).isoformat()
|
|
514
|
+
return SyncApiDocument(
|
|
515
|
+
content=content,
|
|
516
|
+
revision=revision,
|
|
517
|
+
content_hash=content_hash,
|
|
518
|
+
source_file=source_file,
|
|
519
|
+
updated_at=updated_at,
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def _memory_payload_to_document(payload: dict[str, object]) -> SyncApiDocument:
|
|
524
|
+
content = _first_str(payload, ("content",))
|
|
525
|
+
if content is None:
|
|
526
|
+
raise SyncApiError("VDS AI Memory payload missing content field.")
|
|
527
|
+
|
|
528
|
+
metadata = payload.get("metadata")
|
|
529
|
+
metadata_dict: dict[str, object] = metadata if isinstance(metadata, dict) else {}
|
|
530
|
+
|
|
531
|
+
revision_obj = payload.get("current_version_number")
|
|
532
|
+
if revision_obj is None:
|
|
533
|
+
revision_obj = payload.get("updated_at")
|
|
534
|
+
if revision_obj is None:
|
|
535
|
+
raise SyncApiError("VDS AI Memory payload missing revision field.")
|
|
536
|
+
revision = str(revision_obj)
|
|
537
|
+
|
|
538
|
+
source_file = _first_str(metadata_dict, ("sync_source_file",))
|
|
539
|
+
if source_file is None:
|
|
540
|
+
source_file = _first_str(payload, ("source_file",))
|
|
541
|
+
if source_file is None:
|
|
542
|
+
source_file = "<vds-ai-memory>"
|
|
543
|
+
|
|
544
|
+
content_hash = _first_str(metadata_dict, ("sync_content_hash",))
|
|
545
|
+
if content_hash is None:
|
|
546
|
+
content_hash = _first_str(payload, ("content_hash",))
|
|
547
|
+
if content_hash is None:
|
|
548
|
+
content_hash = hashlib.sha256(content.encode("utf-8")).hexdigest()
|
|
549
|
+
|
|
550
|
+
updated_at = _first_str(payload, ("updated_at",))
|
|
551
|
+
if updated_at is None:
|
|
552
|
+
updated_at = _first_str(metadata_dict, ("sync_updated_at",))
|
|
553
|
+
if updated_at is None:
|
|
554
|
+
updated_at = datetime.now(UTC).isoformat()
|
|
555
|
+
|
|
556
|
+
return SyncApiDocument(
|
|
557
|
+
content=content,
|
|
558
|
+
revision=revision,
|
|
559
|
+
content_hash=content_hash,
|
|
560
|
+
source_file=source_file,
|
|
561
|
+
updated_at=updated_at,
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def _confluence_property_payload_to_document(payload: dict[str, object]) -> SyncApiDocument:
|
|
566
|
+
value = payload.get("value")
|
|
567
|
+
value_dict: dict[str, object] = value if isinstance(value, dict) else {}
|
|
568
|
+
|
|
569
|
+
content = _first_str(value_dict, ("content", "document_body", "body"))
|
|
570
|
+
if content is None and isinstance(value, str):
|
|
571
|
+
content = value
|
|
572
|
+
if content is None:
|
|
573
|
+
raise SyncApiError("Confluence property payload missing content value.")
|
|
574
|
+
|
|
575
|
+
version_payload = payload.get("version")
|
|
576
|
+
version_dict: dict[str, object] = version_payload if isinstance(version_payload, dict) else {}
|
|
577
|
+
revision_obj = version_dict.get("number")
|
|
578
|
+
if revision_obj is None:
|
|
579
|
+
raise SyncApiError("Confluence property payload missing version.number.")
|
|
580
|
+
revision = str(revision_obj)
|
|
581
|
+
|
|
582
|
+
source_file = _first_str(value_dict, ("source_file",))
|
|
583
|
+
if source_file is None:
|
|
584
|
+
source_file = "<confluence-property>"
|
|
585
|
+
|
|
586
|
+
content_hash = _first_str(value_dict, ("content_hash",))
|
|
587
|
+
if content_hash is None:
|
|
588
|
+
content_hash = hashlib.sha256(content.encode("utf-8")).hexdigest()
|
|
589
|
+
|
|
590
|
+
updated_at = _first_str(value_dict, ("updated_at",))
|
|
591
|
+
if updated_at is None:
|
|
592
|
+
updated_at = _first_str(version_dict, ("when",))
|
|
593
|
+
if updated_at is None:
|
|
594
|
+
updated_at = datetime.now(UTC).isoformat()
|
|
595
|
+
|
|
596
|
+
return SyncApiDocument(
|
|
597
|
+
content=content,
|
|
598
|
+
revision=revision,
|
|
599
|
+
content_hash=content_hash,
|
|
600
|
+
source_file=source_file,
|
|
601
|
+
updated_at=updated_at,
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def _sync_metadata(*, source_file: str, content: str) -> dict[str, str]:
|
|
606
|
+
return {
|
|
607
|
+
"sync_source_file": source_file,
|
|
608
|
+
"sync_content_hash": hashlib.sha256(content.encode("utf-8")).hexdigest(),
|
|
609
|
+
"sync_updated_at": datetime.now(UTC).isoformat(),
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def _sync_payload_value(*, source_file: str, content: str) -> dict[str, str]:
|
|
614
|
+
return {
|
|
615
|
+
"content": content,
|
|
616
|
+
"source_file": source_file,
|
|
617
|
+
"content_hash": hashlib.sha256(content.encode("utf-8")).hexdigest(),
|
|
618
|
+
"updated_at": datetime.now(UTC).isoformat(),
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
def _next_revision_number(revision: str) -> int:
|
|
623
|
+
try:
|
|
624
|
+
return int(revision) + 1
|
|
625
|
+
except ValueError as exc:
|
|
626
|
+
raise SyncApiError(f"Confluence revision is not an integer: {revision}") from exc
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def _first_str(payload: dict[str, object], keys: tuple[str, ...]) -> str | None:
|
|
630
|
+
for key in keys:
|
|
631
|
+
value = payload.get(key)
|
|
632
|
+
if isinstance(value, str):
|
|
633
|
+
return value
|
|
634
|
+
return None
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def _parse_int_revision(revision: str | None) -> int | None:
|
|
638
|
+
if revision is None:
|
|
639
|
+
return None
|
|
640
|
+
try:
|
|
641
|
+
return int(revision)
|
|
642
|
+
except ValueError:
|
|
643
|
+
return None
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
def _is_transient_http_code(code: int) -> bool:
|
|
647
|
+
return code in {408, 429} or 500 <= code < 600
|