@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.
Files changed (303) hide show
  1. package/package.json +2 -2
  2. package/src/global.js +70 -10
  3. package/tools/skills/vds-scripts-skill/.openskills.json +6 -0
  4. package/tools/skills/vds-scripts-skill/QUALITY.md +44 -0
  5. package/tools/skills/vds-scripts-skill/SKILL.md +135 -0
  6. package/tools/skills/vds-scripts-skill/references/audit-commands.md +171 -0
  7. package/tools/skills/vds-scripts-skill/references/capability-index.md +34 -0
  8. package/tools/skills/vds-scripts-skill/references/development-commands.md +12 -0
  9. package/tools/skills/vds-scripts-skill/references/google-sheets.md +73 -0
  10. package/tools/skills/vds-scripts-skill/references/integration-commands.md +17 -0
  11. package/tools/skills/vds-scripts-skill/references/platform-bootstrap.md +31 -0
  12. package/tools/skills/vds-scripts-skill/references/specialist-routing.md +14 -0
  13. package/tools/skills/vds-scripts-skill/references/validation-commands.md +15 -0
  14. package/tools/skills/vsaf-build/SKILL.md +32 -2
  15. package/tools/skills/vsaf-ship/SKILL.md +41 -10
  16. package/tools/skills/vsaf-test/SKILL.md +8 -0
  17. package/tools/vds-scripts/.mcp.json +11 -0
  18. package/tools/vds-scripts/.secrets.baseline +133 -0
  19. package/tools/vds-scripts/AGENTS.md +152 -0
  20. package/tools/vds-scripts/CLAUDE.md +101 -0
  21. package/tools/vds-scripts/CLI_COMMAND_OPTIMIZATION.md +156 -0
  22. package/tools/vds-scripts/PACKAGE_P125B_IMPLEMENTATION_SUMMARY.md +131 -0
  23. package/tools/vds-scripts/PROJECT_COMPLETION_SUMMARY.md +45 -0
  24. package/tools/vds-scripts/README.md +97 -0
  25. package/tools/vds-scripts/bitbucket_manifest_mapping.toml +34 -0
  26. package/tools/vds-scripts/bitbucket_orchestrator/ARCHITECTURE_ANALYSIS.md +258 -0
  27. package/tools/vds-scripts/bitbucket_orchestrator/BITBUCKET_API_PRACTICES.md +393 -0
  28. package/tools/vds-scripts/bitbucket_orchestrator/EVALUATION_REPORT.md +61 -0
  29. package/tools/vds-scripts/bitbucket_orchestrator/FEATURES.md +908 -0
  30. package/tools/vds-scripts/bitbucket_orchestrator/README.md +687 -0
  31. package/tools/vds-scripts/bitbucket_orchestrator/pyproject.toml +40 -0
  32. package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/__init__.py +20 -0
  33. package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/async_client.py +657 -0
  34. package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/cli.py +2108 -0
  35. package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/client.py +2534 -0
  36. package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/config.py +171 -0
  37. package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/errors.py +67 -0
  38. package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/factory.py +185 -0
  39. package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/protocols.py +244 -0
  40. package/tools/vds-scripts/bitbucket_orchestrator/tests/__init__.py +8 -0
  41. package/tools/vds-scripts/bitbucket_orchestrator/tests/conftest.py +65 -0
  42. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_advanced_search.py +151 -0
  43. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_async_client.py +546 -0
  44. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_branch_permissions.py +145 -0
  45. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_cli.py +115 -0
  46. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client.py +157 -0
  47. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_branch_conditions.py +79 -0
  48. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_code_advanced.py +163 -0
  49. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_code_file.py +32 -0
  50. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_deployment_environments.py +194 -0
  51. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_issues.py +164 -0
  52. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_pipelines_advanced.py +179 -0
  53. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_pr_blockers.py +119 -0
  54. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_repository_variables.py +156 -0
  55. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_code.py +98 -0
  56. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_code_advanced.py +282 -0
  57. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_code_insights.py +335 -0
  58. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_conditions.py +147 -0
  59. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_config.py +131 -0
  60. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_deployment_env.py +352 -0
  61. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_factory.py +371 -0
  62. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_fork_operations.py +204 -0
  63. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_issue_cli.py +261 -0
  64. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_pipeline_advanced.py +270 -0
  65. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_pr_blocker.py +204 -0
  66. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_protocols.py +334 -0
  67. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_repo_settings.py +343 -0
  68. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_repo_variables.py +270 -0
  69. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_webhooks.py +189 -0
  70. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_workspace.py +233 -0
  71. package/tools/vds-scripts/bitbucket_orchestrator/uv.lock +742 -0
  72. package/tools/vds-scripts/confluence_orchestrator/Dockerfile +19 -0
  73. package/tools/vds-scripts/confluence_orchestrator/README.md +412 -0
  74. package/tools/vds-scripts/confluence_orchestrator/SYNC_SCRIPTS.md +127 -0
  75. package/tools/vds-scripts/confluence_orchestrator/SYNC_STANDARDIZATION.md +108 -0
  76. package/tools/vds-scripts/confluence_orchestrator/pyproject.toml +48 -0
  77. package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/__init__.py +20 -0
  78. package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/cli.py +2532 -0
  79. package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/config.py +175 -0
  80. package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/content.py +290 -0
  81. package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/content_v2.py +94 -0
  82. package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/crawl_tree.py +1835 -0
  83. package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/errors.py +80 -0
  84. package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/eventing.py +109 -0
  85. package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/http.py +1114 -0
  86. package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/orchestration.py +165 -0
  87. package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/reporting.py +78 -0
  88. package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/tree.py +121 -0
  89. package/tools/vds-scripts/confluence_orchestrator/sync_pdfs_from_markdown.py +213 -0
  90. package/tools/vds-scripts/confluence_orchestrator/sync_pdfs_to_confluence.py +305 -0
  91. package/tools/vds-scripts/confluence_orchestrator/sync_png_attachments.py +305 -0
  92. package/tools/vds-scripts/confluence_orchestrator/tests/__init__.py +0 -0
  93. package/tools/vds-scripts/confluence_orchestrator/tests/conftest.py +8 -0
  94. package/tools/vds-scripts/confluence_orchestrator/tests/test_advanced_content.py +224 -0
  95. package/tools/vds-scripts/confluence_orchestrator/tests/test_advanced_search.py +188 -0
  96. package/tools/vds-scripts/confluence_orchestrator/tests/test_cache_management.py +247 -0
  97. package/tools/vds-scripts/confluence_orchestrator/tests/test_cli.py +499 -0
  98. package/tools/vds-scripts/confluence_orchestrator/tests/test_config.py +83 -0
  99. package/tools/vds-scripts/confluence_orchestrator/tests/test_content.py +186 -0
  100. package/tools/vds-scripts/confluence_orchestrator/tests/test_content_flags.py +27 -0
  101. package/tools/vds-scripts/confluence_orchestrator/tests/test_crawl_tree.py +2250 -0
  102. package/tools/vds-scripts/confluence_orchestrator/tests/test_draft_management.py +223 -0
  103. package/tools/vds-scripts/confluence_orchestrator/tests/test_eventing.py +71 -0
  104. package/tools/vds-scripts/confluence_orchestrator/tests/test_eventing_chaos.py +37 -0
  105. package/tools/vds-scripts/confluence_orchestrator/tests/test_eventing_rate_limit.py +44 -0
  106. package/tools/vds-scripts/confluence_orchestrator/tests/test_eventing_timeout.py +49 -0
  107. package/tools/vds-scripts/confluence_orchestrator/tests/test_export.py +230 -0
  108. package/tools/vds-scripts/confluence_orchestrator/tests/test_history.py +204 -0
  109. package/tools/vds-scripts/confluence_orchestrator/tests/test_http.py +117 -0
  110. package/tools/vds-scripts/confluence_orchestrator/tests/test_orchestration.py +91 -0
  111. package/tools/vds-scripts/confluence_orchestrator/tests/test_reporting.py +24 -0
  112. package/tools/vds-scripts/confluence_orchestrator/tests/test_search_cql.py +34 -0
  113. package/tools/vds-scripts/confluence_orchestrator/tests/test_space_management.py +237 -0
  114. package/tools/vds-scripts/confluence_orchestrator/tests/test_space_permissions.py +332 -0
  115. package/tools/vds-scripts/confluence_orchestrator/tests/test_user_group_management.py +388 -0
  116. package/tools/vds-scripts/confluence_orchestrator/uv.lock +1023 -0
  117. package/tools/vds-scripts/git_orchestrator/ENHANCEMENT_SUMMARY.md +119 -0
  118. package/tools/vds-scripts/git_orchestrator/README.md +280 -0
  119. package/tools/vds-scripts/git_orchestrator/VERIFICATION_REPORT.md +152 -0
  120. package/tools/vds-scripts/git_orchestrator/pyproject.toml +35 -0
  121. package/tools/vds-scripts/git_orchestrator/src/vds_git_orchestrator/__init__.py +7 -0
  122. package/tools/vds-scripts/git_orchestrator/src/vds_git_orchestrator/__main__.py +4 -0
  123. package/tools/vds-scripts/git_orchestrator/src/vds_git_orchestrator/cli.py +847 -0
  124. package/tools/vds-scripts/git_orchestrator/src/vds_git_orchestrator/logging_config.py +63 -0
  125. package/tools/vds-scripts/git_orchestrator/src/vds_git_orchestrator/manifest.py +129 -0
  126. package/tools/vds-scripts/git_orchestrator/src/vds_git_orchestrator/orchestrator.py +819 -0
  127. package/tools/vds-scripts/git_orchestrator/src/vds_git_orchestrator/reporting.py +53 -0
  128. package/tools/vds-scripts/git_orchestrator/tests/__init__.py +0 -0
  129. package/tools/vds-scripts/git_orchestrator/tests/test_cli_settings.py +21 -0
  130. package/tools/vds-scripts/git_orchestrator/tests/test_integration.py +74 -0
  131. package/tools/vds-scripts/git_orchestrator/tests/test_manifest.py +79 -0
  132. package/tools/vds-scripts/git_orchestrator/tests/test_orchestrator.py +204 -0
  133. package/tools/vds-scripts/git_orchestrator/tests/test_public_api.py +236 -0
  134. package/tools/vds-scripts/git_orchestrator/tests/test_resilience.py +345 -0
  135. package/tools/vds-scripts/git_orchestrator/uv.lock +271 -0
  136. package/tools/vds-scripts/jira_orchestrator/README.md +770 -0
  137. package/tools/vds-scripts/jira_orchestrator/pyproject.toml +39 -0
  138. package/tools/vds-scripts/jira_orchestrator/src/vds_jira_orchestrator/__init__.py +1 -0
  139. package/tools/vds-scripts/jira_orchestrator/src/vds_jira_orchestrator/adapter.py +1320 -0
  140. package/tools/vds-scripts/jira_orchestrator/src/vds_jira_orchestrator/cli.py +2271 -0
  141. package/tools/vds-scripts/jira_orchestrator/src/vds_jira_orchestrator/config.py +138 -0
  142. package/tools/vds-scripts/jira_orchestrator/src/vds_jira_orchestrator/errors.py +67 -0
  143. package/tools/vds-scripts/jira_orchestrator/src/vds_jira_orchestrator/reporting.py +65 -0
  144. package/tools/vds-scripts/jira_orchestrator/tests/__init__.py +1 -0
  145. package/tools/vds-scripts/jira_orchestrator/tests/conftest.py +86 -0
  146. package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_agile_list_payloads.py +54 -0
  147. package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_bulk_operations.py +69 -0
  148. package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_components.py +57 -0
  149. package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_createmeta.py +45 -0
  150. package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_dashboard.py +117 -0
  151. package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_issue_properties.py +54 -0
  152. package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_permissions_compat.py +42 -0
  153. package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_reindex.py +42 -0
  154. package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_remote_links.py +76 -0
  155. package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_transitions.py +91 -0
  156. package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_user_management.py +110 -0
  157. package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_version_management.py +133 -0
  158. package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_watchers.py +41 -0
  159. package/tools/vds-scripts/jira_orchestrator/tests/test_advanced_search.py +164 -0
  160. package/tools/vds-scripts/jira_orchestrator/tests/test_agile.py +256 -0
  161. package/tools/vds-scripts/jira_orchestrator/tests/test_application_properties.py +193 -0
  162. package/tools/vds-scripts/jira_orchestrator/tests/test_backlog.py +91 -0
  163. package/tools/vds-scripts/jira_orchestrator/tests/test_bulk_operations.py +277 -0
  164. package/tools/vds-scripts/jira_orchestrator/tests/test_cli.py +106 -0
  165. package/tools/vds-scripts/jira_orchestrator/tests/test_components.py +106 -0
  166. package/tools/vds-scripts/jira_orchestrator/tests/test_config.py +164 -0
  167. package/tools/vds-scripts/jira_orchestrator/tests/test_dashboard.py +122 -0
  168. package/tools/vds-scripts/jira_orchestrator/tests/test_discover_fields.py +207 -0
  169. package/tools/vds-scripts/jira_orchestrator/tests/test_filter_management.py +333 -0
  170. package/tools/vds-scripts/jira_orchestrator/tests/test_issue_archiving.py +164 -0
  171. package/tools/vds-scripts/jira_orchestrator/tests/test_issue_links.py +257 -0
  172. package/tools/vds-scripts/jira_orchestrator/tests/test_issue_properties.py +171 -0
  173. package/tools/vds-scripts/jira_orchestrator/tests/test_link_types.py +314 -0
  174. package/tools/vds-scripts/jira_orchestrator/tests/test_parse_set.py +37 -0
  175. package/tools/vds-scripts/jira_orchestrator/tests/test_permissions.py +273 -0
  176. package/tools/vds-scripts/jira_orchestrator/tests/test_reindex.py +81 -0
  177. package/tools/vds-scripts/jira_orchestrator/tests/test_remote_links.py +254 -0
  178. package/tools/vds-scripts/jira_orchestrator/tests/test_security_schemes.py +170 -0
  179. package/tools/vds-scripts/jira_orchestrator/tests/test_transitions_changelog.py +114 -0
  180. package/tools/vds-scripts/jira_orchestrator/tests/test_user_management.py +226 -0
  181. package/tools/vds-scripts/jira_orchestrator/tests/test_version_management.py +339 -0
  182. package/tools/vds-scripts/jira_orchestrator/tests/test_watchers.py +101 -0
  183. package/tools/vds-scripts/jira_orchestrator/tests/test_worklog.py +223 -0
  184. package/tools/vds-scripts/jira_orchestrator/uv.lock +738 -0
  185. package/tools/vds-scripts/mcp_server/Dockerfile +34 -0
  186. package/tools/vds-scripts/mcp_server/README.md +140 -0
  187. package/tools/vds-scripts/mcp_server/pyproject.toml +42 -0
  188. package/tools/vds-scripts/mcp_server/src/vds_mcp_server/__init__.py +4 -0
  189. package/tools/vds-scripts/mcp_server/src/vds_mcp_server/config.py +36 -0
  190. package/tools/vds-scripts/mcp_server/src/vds_mcp_server/server.py +66 -0
  191. package/tools/vds-scripts/mcp_server/src/vds_mcp_server/tools/__init__.py +14 -0
  192. package/tools/vds-scripts/mcp_server/src/vds_mcp_server/tools/bitbucket_tools.py +47 -0
  193. package/tools/vds-scripts/mcp_server/src/vds_mcp_server/tools/confluence_tools.py +59 -0
  194. package/tools/vds-scripts/mcp_server/src/vds_mcp_server/tools/git_tools.py +71 -0
  195. package/tools/vds-scripts/mcp_server/src/vds_mcp_server/tools/jira_tools.py +63 -0
  196. package/tools/vds-scripts/mcp_server/tests/__init__.py +2 -0
  197. package/tools/vds-scripts/mcp_server/tests/conftest.py +29 -0
  198. package/tools/vds-scripts/mcp_server/tests/unit/__init__.py +2 -0
  199. package/tools/vds-scripts/mcp_server/tests/unit/test_bitbucket_tools.py +25 -0
  200. package/tools/vds-scripts/mcp_server/tests/unit/test_confluence_tools.py +25 -0
  201. package/tools/vds-scripts/mcp_server/tests/unit/test_git_tools.py +32 -0
  202. package/tools/vds-scripts/mcp_server/tests/unit/test_jira_tools.py +32 -0
  203. package/tools/vds-scripts/mcp_server/tests/verification/__init__.py +2 -0
  204. package/tools/vds-scripts/mcp_server/tests/verification/test_mcp_confluence_tools.py +40 -0
  205. package/tools/vds-scripts/mcp_server/tests/verification/test_mcp_jira_tools.py +37 -0
  206. package/tools/vds-scripts/mcp_server/tests/verification/test_mcp_tool_registration.py +47 -0
  207. package/tools/vds-scripts/mcp_server/uv.lock +1032 -0
  208. package/tools/vds-scripts/mypy.ini +5 -0
  209. package/tools/vds-scripts/pyproject.toml +29 -0
  210. package/tools/vds-scripts/repo-manifest.yaml +273 -0
  211. package/tools/vds-scripts/repo-manifest.yaml.example +25 -0
  212. package/tools/vds-scripts/scripts/BRD-Validation-API.postman_collection.json +706 -0
  213. package/tools/vds-scripts/scripts/BRD-Validation-README.md +308 -0
  214. package/tools/vds-scripts/scripts/README.md +162 -0
  215. package/tools/vds-scripts/scripts/bootstrap_uv.sh +30 -0
  216. package/tools/vds-scripts/scripts/brd-validation-environment.json +51 -0
  217. package/tools/vds-scripts/scripts/brd-validation-test-results.json +13023 -0
  218. package/tools/vds-scripts/scripts/brd_coverage_report.json +276 -0
  219. package/tools/vds-scripts/scripts/create_memory_session.py +35 -0
  220. package/tools/vds-scripts/scripts/deployment/load_docker_images_offline.sh +90 -0
  221. package/tools/vds-scripts/scripts/final_completion_report.md +139 -0
  222. package/tools/vds-scripts/scripts/folder_structure_report.json +321 -0
  223. package/tools/vds-scripts/scripts/generate_completion_report.py +125 -0
  224. package/tools/vds-scripts/scripts/generate_intellij_modules.py +150 -0
  225. package/tools/vds-scripts/scripts/link_integrity_report.json +807 -0
  226. package/tools/vds-scripts/scripts/move_audit_artifact_pages.py +255 -0
  227. package/tools/vds-scripts/scripts/move_audit_artifact_pages_rest.py +165 -0
  228. package/tools/vds-scripts/scripts/move_wrong_dept_pages.py +216 -0
  229. package/tools/vds-scripts/scripts/save_intellij_memories.py +120 -0
  230. package/tools/vds-scripts/scripts/save_memories_to_vds_ai.py +83 -0
  231. package/tools/vds-scripts/scripts/save_memories_vds_style.py +129 -0
  232. package/tools/vds-scripts/scripts/search_intellij_memories.py +50 -0
  233. package/tools/vds-scripts/scripts/setup_intellij_workspace.py +65 -0
  234. package/tools/vds-scripts/scripts/target-state-automation/README.md +89 -0
  235. package/tools/vds-scripts/scripts/target-state-automation/confluence_sync_coordinator.sh +27 -0
  236. package/tools/vds-scripts/scripts/target-state-automation/coordination.sh +114 -0
  237. package/tools/vds-scripts/scripts/target-state-automation/diagram_coordinator.sh +25 -0
  238. package/tools/vds-scripts/scripts/target-state-automation/docs_root.sh +22 -0
  239. package/tools/vds-scripts/scripts/target-state-automation/generate_diagrams.sh +22 -0
  240. package/tools/vds-scripts/scripts/target-state-automation/markdown_coordinator.sh +25 -0
  241. package/tools/vds-scripts/scripts/target-state-automation/progress_dashboard.sh +17 -0
  242. package/tools/vds-scripts/scripts/target-state-automation/schema_coordinator.sh +25 -0
  243. package/tools/vds-scripts/scripts/target-state-automation/sync_confluence.sh +30 -0
  244. package/tools/vds-scripts/scripts/target-state-automation/update_dependencies.sh +19 -0
  245. package/tools/vds-scripts/scripts/target-state-automation/validate_links.sh +86 -0
  246. package/tools/vds-scripts/scripts/target-state-automation/validate_markdown.sh +52 -0
  247. package/tools/vds-scripts/scripts/target-state-automation/validate_schemas.sh +26 -0
  248. package/tools/vds-scripts/scripts/target-state-automation/validate_structure.sh +98 -0
  249. package/tools/vds-scripts/scripts/update_modules_xml.py +190 -0
  250. package/tools/vds-scripts/scripts/uv-workspace-alignment-verification-2026-03-25.md +128 -0
  251. package/tools/vds-scripts/scripts/validate_brd_coverage.py +179 -0
  252. package/tools/vds-scripts/scripts/validate_folder_structure.py +240 -0
  253. package/tools/vds-scripts/scripts/validate_link_integrity.py +272 -0
  254. package/tools/vds-scripts/scripts/vds_sh_helpers.sh +180 -0
  255. package/tools/vds-scripts/scripts/verification/phase2_portable_paths_ubuntu_docker.sh +26 -0
  256. package/tools/vds-scripts/scripts/worktree_uv.sh +48 -0
  257. package/tools/vds-scripts/uv.lock +8 -0
  258. package/tools/vds-scripts/vds_cli/README.md +126 -0
  259. package/tools/vds-scripts/vds_cli/VERIFICATION_REPORT.md +41 -0
  260. package/tools/vds-scripts/vds_cli/pyproject.toml +38 -0
  261. package/tools/vds-scripts/vds_cli/src/vds_cli/__init__.py +3 -0
  262. package/tools/vds-scripts/vds_cli/src/vds_cli/cli.py +173 -0
  263. package/tools/vds-scripts/vds_cli/src/vds_cli/docs_sync.py +1203 -0
  264. package/tools/vds-scripts/vds_cli/src/vds_cli/env.py +41 -0
  265. package/tools/vds-scripts/vds_cli/src/vds_cli/google_sheets_orchestrator/__init__.py +3 -0
  266. package/tools/vds-scripts/vds_cli/src/vds_cli/google_sheets_orchestrator/google_sheets_orchestrator.py +198 -0
  267. package/tools/vds-scripts/vds_cli/src/vds_cli/router.py +93 -0
  268. package/tools/vds-scripts/vds_cli/src/vds_cli/sync_api.py +647 -0
  269. package/tools/vds-scripts/vds_cli/src/vds_cli/sync_service.py +266 -0
  270. package/tools/vds-scripts/vds_cli/tests/__init__.py +2 -0
  271. package/tools/vds-scripts/vds_cli/tests/conftest.py +49 -0
  272. package/tools/vds-scripts/vds_cli/tests/unit/__init__.py +2 -0
  273. package/tools/vds-scripts/vds_cli/tests/unit/test_cli.py +143 -0
  274. package/tools/vds-scripts/vds_cli/tests/unit/test_docs_sync.py +422 -0
  275. package/tools/vds-scripts/vds_cli/tests/unit/test_env.py +51 -0
  276. package/tools/vds-scripts/vds_cli/tests/unit/test_router.py +72 -0
  277. package/tools/vds-scripts/vds_cli/tests/unit/test_sync_api.py +357 -0
  278. package/tools/vds-scripts/vds_cli/tests/unit/test_sync_service.py +160 -0
  279. package/tools/vds-scripts/vds_cli/tests/verification/__init__.py +2 -0
  280. package/tools/vds-scripts/vds_cli/tests/verification/test_bitbucket_real.py +33 -0
  281. package/tools/vds-scripts/vds_cli/tests/verification/test_confluence_real.py +35 -0
  282. package/tools/vds-scripts/vds_cli/tests/verification/test_jira_real.py +41 -0
  283. package/tools/vds-scripts/vds_cli/uv.lock +524 -0
  284. package/tools/vds-scripts/vds_cli_common/README.md +190 -0
  285. package/tools/vds-scripts/vds_cli_common/pyproject.toml +92 -0
  286. package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/__init__.py +34 -0
  287. package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/completers.py +139 -0
  288. package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/context.py +201 -0
  289. package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/env.py +119 -0
  290. package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/errors.py +318 -0
  291. package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/output.py +284 -0
  292. package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/paths.py +78 -0
  293. package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/testing.py +213 -0
  294. package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/version.py +85 -0
  295. package/tools/vds-scripts/vds_cli_common/tests/__init__.py +1 -0
  296. package/tools/vds-scripts/vds_cli_common/tests/test_completers.py +148 -0
  297. package/tools/vds-scripts/vds_cli_common/tests/test_context.py +192 -0
  298. package/tools/vds-scripts/vds_cli_common/tests/test_env.py +102 -0
  299. package/tools/vds-scripts/vds_cli_common/tests/test_errors.py +186 -0
  300. package/tools/vds-scripts/vds_cli_common/tests/test_output.py +229 -0
  301. package/tools/vds-scripts/vds_cli_common/tests/test_paths.py +61 -0
  302. package/tools/vds-scripts/vds_cli_common/tests/test_testing.py +138 -0
  303. package/tools/vds-scripts/vds_cli_common/tests/test_version.py +64 -0
@@ -0,0 +1,1114 @@
1
+ """Confluence client wrapper using atlassian-python-api for consistency."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ import time
7
+ from dataclasses import dataclass
8
+ from typing import Any
9
+
10
+ import httpx
11
+ import structlog
12
+
13
+ try: # Prefer new-style Atlassian clients when available
14
+ from atlassian.confluence import ConfluenceCloud as AtlassianConfluenceCloud
15
+ from atlassian.confluence import ConfluenceServer as AtlassianConfluenceServer
16
+ except ImportError: # Fall back to legacy class
17
+ from atlassian import Confluence as AtlassianConfluenceCloud # type: ignore
18
+ AtlassianConfluenceServer = AtlassianConfluenceCloud
19
+
20
+ from .config import ConfluenceSettings
21
+ from .errors import (
22
+ ConfluenceAuthError,
23
+ ConfluenceClientError,
24
+ ConfluenceConflictError,
25
+ ConfluencePermissionError,
26
+ ConfluenceRateLimitedError,
27
+ ConfluenceResponseError,
28
+ ConfluenceTransportError,
29
+ )
30
+
31
+
32
+ @dataclass(slots=True)
33
+ class RetryConfig:
34
+ max_attempts: int = 3
35
+ backoff_factor: float = 0.5
36
+ max_wait: float = 5.0
37
+
38
+
39
+ class ConfluenceHttpClient:
40
+ def __init__(
41
+ self,
42
+ settings: ConfluenceSettings,
43
+ *,
44
+ server: str = "internal",
45
+ client: httpx.Client | None = None,
46
+ retry_config: RetryConfig | None = None,
47
+ ) -> None:
48
+ token = settings.token_for(server)
49
+ if not token:
50
+ raise ConfluenceAuthError(
51
+ f"Token for server '{server}' is missing.",
52
+ context={"server": server},
53
+ )
54
+
55
+ self._settings = settings
56
+ self._server = server
57
+ self._token = token
58
+ self._retry = retry_config or RetryConfig()
59
+ if client is None:
60
+ base_url = str(settings.url_for(server))
61
+ client = httpx.Client(
62
+ base_url=base_url,
63
+ headers={
64
+ "Authorization": f"Bearer {token}",
65
+ "Accept": "application/json",
66
+ },
67
+ timeout=30,
68
+ )
69
+ self._client = client
70
+
71
+ def close(self) -> None:
72
+ self._client.close()
73
+
74
+ def __enter__(self) -> ConfluenceHttpClient:
75
+ return self
76
+
77
+ def __exit__(self, exc_type, exc, tb) -> None: # type: ignore[override]
78
+ self.close()
79
+
80
+ def request(self, method: str, path: str, **kwargs: Any) -> httpx.Response:
81
+ attempt = 0
82
+ while True:
83
+ try:
84
+ response = self._client.request(method, path, **kwargs)
85
+ except httpx.HTTPError as exc: # pragma: no cover - network errors
86
+ raise ConfluenceTransportError(f"HTTP transport error: {exc}") from exc
87
+
88
+ if response.status_code < 400:
89
+ return response
90
+
91
+ if response.status_code in {401, 403}:
92
+ raise ConfluenceAuthError(
93
+ "Authentication failed",
94
+ context={"status_code": response.status_code},
95
+ )
96
+ if response.status_code == 409:
97
+ raise ConfluenceConflictError(
98
+ "Request conflicted with existing state",
99
+ context={"status_code": response.status_code},
100
+ )
101
+
102
+ should_retry = response.status_code in {429, 500, 502, 503, 504}
103
+ if should_retry and attempt < self._retry.max_attempts - 1:
104
+ retry_after = response.headers.get("Retry-After")
105
+ if retry_after:
106
+ try:
107
+ wait = min(float(retry_after), self._retry.max_wait)
108
+ except ValueError:
109
+ wait = self._retry.backoff_factor * (2**attempt)
110
+ else:
111
+ wait = self._retry.backoff_factor * (2**attempt)
112
+ wait = min(wait, self._retry.max_wait)
113
+ time.sleep(wait)
114
+ attempt += 1
115
+ continue
116
+
117
+ raise ConfluenceResponseError(
118
+ f"Unexpected status {response.status_code}",
119
+ status_code=response.status_code,
120
+ context={"status_code": response.status_code, "body": response.text[:256]},
121
+ )
122
+
123
+
124
+ class ConfluenceClient:
125
+ """Confluence client wrapper using atlassian-python-api SDK."""
126
+
127
+ def __init__(
128
+ self,
129
+ settings: ConfluenceSettings,
130
+ *,
131
+ server: str | None = None,
132
+ timeout: int | None = 30,
133
+ ) -> None:
134
+ self._settings = settings
135
+ self._server = server or settings.default_server
136
+ self._timeout = timeout or 30
137
+
138
+ base_url = str(settings.url_for(self._server))
139
+ token = settings.token_for(self._server)
140
+
141
+ # Decide whether this endpoint should be treated as Cloud (v2) or Server (v1)
142
+ self._is_cloud = self._looks_like_cloud(base_url)
143
+ client_cls = AtlassianConfluenceCloud if self._is_cloud else AtlassianConfluenceServer
144
+
145
+ # Configure authentication based on available credentials
146
+ auth_kwargs: dict[str, Any] = {"url": base_url, "timeout": self._timeout}
147
+ if self._is_cloud:
148
+ if not token:
149
+ raise ConfluenceAuthError(
150
+ f"No API token available for cloud server '{self._server}'.",
151
+ context={"server": self._server},
152
+ )
153
+ auth_kwargs["token"] = token
154
+ else:
155
+ if settings.username and settings.password:
156
+ auth_kwargs["username"] = settings.username
157
+ auth_kwargs["password"] = settings.password
158
+ elif token:
159
+ # Recent ConfluenceServer class also supports PATs
160
+ auth_kwargs["token"] = token
161
+ else:
162
+ raise ConfluenceAuthError(
163
+ f"No credentials available for server '{self._server}'. "
164
+ "Provide VDS_USERNAME+VDS_PASSWORD or a CONFLUENCE token.",
165
+ context={"server": self._server},
166
+ )
167
+
168
+ try:
169
+ self._client = client_cls(**auth_kwargs)
170
+ except Exception as exc:
171
+ raise ConfluenceTransportError(
172
+ f"Failed to initialize Confluence client: {exc}", context={"server": self._server, "base_url": base_url}
173
+ ) from exc
174
+
175
+ self._log = structlog.get_logger(__name__).bind(server=self._server, base_url=base_url)
176
+
177
+ # Store retry settings
178
+ self._max_retries = settings.max_retries
179
+ self._retry_backoff_factor = settings.retry_backoff_factor
180
+ self._create_params = set(self._safe_signature_params(self._client.create_page))
181
+ self._update_params = set(self._safe_signature_params(self._client.update_page))
182
+
183
+ def supports_api_version(self, version: str) -> bool:
184
+ version = (version or "v1").lower()
185
+ if version == "v1":
186
+ return True
187
+ if version == "v2":
188
+ return self._is_cloud
189
+ return False
190
+
191
+ @staticmethod
192
+ def _looks_like_cloud(base_url: str) -> bool:
193
+ lowered = base_url.lower()
194
+ return any(indicator in lowered for indicator in ("atlassian.net", "atlassian.com"))
195
+
196
+ @staticmethod
197
+ def _safe_signature_params(method) -> list[str]:
198
+ try:
199
+ return list(inspect.signature(method).parameters.keys())
200
+ except (TypeError, ValueError):
201
+ return []
202
+
203
+ def _handle_api_error(self, error: Exception, operation: str) -> None:
204
+ """Convert atlassian-python-api errors to our standardized errors."""
205
+ error_str = str(error).lower()
206
+
207
+ if "unauthorized" in error_str or "authentication" in error_str or "401" in error_str:
208
+ raise ConfluenceAuthError(f"Authentication failed during {operation}", context={"error": str(error)})
209
+ elif "forbidden" in error_str or "403" in error_str:
210
+ raise ConfluenceAuthError(f"Access forbidden during {operation}", context={"error": str(error)})
211
+ elif "rate limit" in error_str or "429" in error_str:
212
+ raise ConfluenceRateLimitedError(f"Rate limited during {operation}", context={"error": str(error)})
213
+ elif "conflict" in error_str or "409" in error_str:
214
+ raise ConfluenceConflictError(f"Conflict during {operation}", context={"error": str(error)})
215
+ elif "server error" in error_str or "500" in error_str:
216
+ raise ConfluenceResponseError(f"Server error during {operation}", context={"error": str(error)})
217
+ else:
218
+ raise ConfluenceClientError(f"API error during {operation}: {error}", context={"error": str(error)})
219
+
220
+ def _with_retry(self, operation, operation_name: str):
221
+ """Execute an operation with retry logic using instance settings."""
222
+ for attempt in range(self._max_retries + 1):
223
+ try:
224
+ result = operation()
225
+ return result
226
+ except Exception as exc:
227
+ try:
228
+ self._handle_api_error(exc, operation_name)
229
+ except (ConfluenceTransportError, ConfluenceRateLimitedError) as retry_exc:
230
+ if attempt == self._max_retries:
231
+ self._log.error(
232
+ "retry_attempts_exhausted",
233
+ operation=operation_name,
234
+ attempts=self._max_retries,
235
+ final_error=str(retry_exc),
236
+ )
237
+ from .errors import ConfluenceRetryError
238
+
239
+ raise ConfluenceRetryError(
240
+ f"Retry attempts exhausted: {retry_exc}",
241
+ attempts=self._max_retries,
242
+ context={"original_error": str(retry_exc), "operation": operation_name},
243
+ ) from retry_exc
244
+
245
+ wait_time = self._retry_backoff_factor * (2**attempt)
246
+ self._log.warning(
247
+ "retry_attempt",
248
+ operation=operation_name,
249
+ attempt=attempt + 1,
250
+ max_retries=self._max_retries,
251
+ wait_time=wait_time,
252
+ error=str(retry_exc),
253
+ )
254
+ time.sleep(wait_time)
255
+ except ConfluenceClientError:
256
+ # Don't retry on client errors (auth, not found, etc.)
257
+ raise
258
+
259
+ def get_page(self, page_id: str, expand: list[str] | None = None) -> dict[str, Any]:
260
+ """Get a page by ID."""
261
+
262
+ def operation():
263
+ params = {}
264
+ if expand:
265
+ params["expand"] = ",".join(expand)
266
+ result = self._client.get_page_by_id(page_id, expand=",".join(expand) if expand else None)
267
+ self._log.debug("page_retrieved", page_id=page_id)
268
+ return result
269
+
270
+ return self._with_retry(operation, f"get_page {page_id}")
271
+
272
+ def search_cql(
273
+ self, cql: str, limit: int = 25, expand: list[str] | None = None, **kwargs: Any
274
+ ) -> dict[str, Any]:
275
+ """Search using CQL."""
276
+
277
+ def operation():
278
+ result = self._client.cql(cql, limit=limit, expand=",".join(expand) if expand else None, **kwargs)
279
+ self._log.debug("cql_search_completed", cql=cql, limit=limit)
280
+ return result
281
+
282
+ return self._with_retry(operation, f"cql_search {cql}")
283
+
284
+ def cql_advanced(
285
+ self,
286
+ cql: str,
287
+ *,
288
+ limit: int = 25,
289
+ start: int = 0,
290
+ expand: list[str] | None = None,
291
+ excerpt: str | None = None,
292
+ **kwargs: Any,
293
+ ) -> dict[str, Any]:
294
+ """Advanced CQL search with additional options.
295
+
296
+ Args:
297
+ cql: CQL query string
298
+ limit: Maximum number of results (default: 25)
299
+ start: Start index for pagination (default: 0)
300
+ expand: Optional list of fields to expand
301
+ excerpt: Optional excerpt strategy (e.g., "highlighted", "indexed")
302
+ **kwargs: Additional options passed to SDK cql() method
303
+ """
304
+
305
+ def operation():
306
+ expand_str = ",".join(expand) if expand else None
307
+ # SDK cql() method supports start parameter via kwargs
308
+ cql_kwargs = {"limit": limit, "start": start, **kwargs}
309
+ if expand_str:
310
+ cql_kwargs["expand"] = expand_str
311
+ if excerpt:
312
+ cql_kwargs["excerpt"] = excerpt
313
+
314
+ result = self._client.cql(cql, **cql_kwargs) # type: ignore[call-arg]
315
+ self._log.debug("cql_advanced_search_completed", cql=cql, limit=limit, start=start)
316
+ return result
317
+
318
+ return self._with_retry(operation, f"cql_advanced_search {cql}")
319
+
320
+ def search_by_space_and_type(
321
+ self,
322
+ space_key: str | None = None,
323
+ content_type: str | None = None,
324
+ *,
325
+ limit: int = 25,
326
+ start: int = 0,
327
+ expand: list[str] | None = None,
328
+ **kwargs: Any,
329
+ ) -> dict[str, Any]:
330
+ """Search by space and content type (helper method that builds CQL).
331
+
332
+ Args:
333
+ space_key: Optional space key to filter by
334
+ content_type: Optional content type (e.g., "page", "blogpost", "comment")
335
+ limit: Maximum number of results (default: 25)
336
+ start: Start index for pagination (default: 0)
337
+ expand: Optional list of fields to expand
338
+ **kwargs: Additional options passed to CQL search
339
+ """
340
+ # Build CQL query
341
+ cql_parts: list[str] = []
342
+ if space_key:
343
+ cql_parts.append(f'space = "{space_key}"')
344
+ if content_type:
345
+ cql_parts.append(f'type = "{content_type}"')
346
+
347
+ if not cql_parts:
348
+ raise ConfluenceClientError(
349
+ "At least one of space_key or content_type must be provided",
350
+ context={},
351
+ )
352
+
353
+ cql = " AND ".join(cql_parts)
354
+ return self.cql_advanced(cql, limit=limit, start=start, expand=expand, **kwargs)
355
+
356
+ def create_page(
357
+ self, space_key: str, title: str, body: str, parent_id: str | None = None, **kwargs: Any
358
+ ) -> dict[str, Any]:
359
+ """Create a new page."""
360
+
361
+ def operation():
362
+ call_kwargs: dict[str, Any] = {}
363
+ if parent_id is not None:
364
+ call_kwargs["parent_id"] = parent_id
365
+ for key, value in kwargs.items():
366
+ if key in self._create_params and value is not None:
367
+ call_kwargs[key] = value
368
+ result = self._client.create_page(space=space_key, title=title, body=body, **call_kwargs)
369
+ self._log.info("page_created", space_key=space_key, title=title, page_id=result.get("id"))
370
+ return result
371
+
372
+ return self._with_retry(operation, f"create_page {title}")
373
+
374
+ def update_page(
375
+ self, page_id: str, title: str, body: str | None = None, version: int | None = None, **kwargs: Any
376
+ ) -> dict[str, Any]:
377
+ """Update an existing page."""
378
+
379
+ def operation():
380
+ call_kwargs: dict[str, Any] = {}
381
+ for key, value in kwargs.items():
382
+ if key in self._update_params and value is not None:
383
+ call_kwargs[key] = value
384
+ if version is not None and "version" in self._update_params:
385
+ call_kwargs["version"] = version
386
+ result = self._client.update_page(page_id, title, body, **call_kwargs)
387
+ self._log.info("page_updated", page_id=page_id, title=title)
388
+ return result
389
+
390
+ return self._with_retry(operation, f"update_page {page_id}")
391
+
392
+ def delete_page(self, page_id: str) -> None:
393
+ """Delete a page."""
394
+ try:
395
+ # Use remove_page method from atlassian library
396
+ self._client.remove_page(page_id=page_id)
397
+ self._log.info("page_deleted", page_id=page_id)
398
+
399
+ except Exception as exc:
400
+ # Check if it's a permission issue
401
+ if "Unable to trash content" in str(exc) or "Unable to purge content" in str(exc):
402
+ self._log.warning("page_delete_permission_denied", page_id=page_id, error=str(exc))
403
+ raise ConfluencePermissionError(
404
+ f"Permission denied when deleting page {page_id}. The page may be restricted or you may not have delete permissions.",
405
+ context={"page_id": page_id, "original_error": str(exc)},
406
+ ) from exc
407
+ else:
408
+ self._handle_api_error(exc, f"delete_page {page_id}")
409
+
410
+ def request(self, method: str, path: str, **kwargs: Any) -> Any:
411
+ """Make a raw HTTP request using the atlassian client."""
412
+ try:
413
+ # Use the underlying request method from atlassian library
414
+ response = self._client.request(method=method, path=path, **kwargs)
415
+ self._log.debug("http_request", method=method, path=path)
416
+ return response
417
+
418
+ except Exception as exc:
419
+ self._handle_api_error(exc, f"request {method} {path}")
420
+
421
+ def add_attachment(
422
+ self,
423
+ page_id: str,
424
+ filename: str,
425
+ content: bytes,
426
+ content_type: str | None = None,
427
+ *,
428
+ comment: str | None = None,
429
+ ) -> dict[str, Any]:
430
+ """Add an attachment to a page.
431
+
432
+ According to atlassian-python-api docs, attach_content automatically updates
433
+ if the file exists, versioning the new file and keeping the old one.
434
+ See: https://atlassian-python-api.readthedocs.io/confluence.html#
435
+ """
436
+
437
+ def operation():
438
+ # Use attach_content from the official SDK (handles create vs update internally)
439
+ result = self._client.attach_content(
440
+ content=content,
441
+ name=filename,
442
+ content_type=content_type or "application/octet-stream",
443
+ page_id=page_id,
444
+ comment=comment,
445
+ )
446
+ self._log.info("attachment_added", page_id=page_id, filename=filename)
447
+ return result
448
+
449
+ return self._with_retry(operation, f"add_attachment {filename}")
450
+
451
+ def get_space(self, space_key: str) -> dict[str, Any]:
452
+ """Get space information."""
453
+ try:
454
+ result = self._client.get_space(space_key)
455
+ self._log.debug("space_retrieved", space_key=space_key)
456
+ return result
457
+
458
+ except Exception as exc:
459
+ self._handle_api_error(exc, f"get_space {space_key}")
460
+
461
+ def get_space_permissions(self, space_key: str) -> dict[str, Any]:
462
+ """Get all permissions configured for a space."""
463
+
464
+ def operation():
465
+ result = self._client.get_space_permissions(space_key)
466
+ count = len(result) if isinstance(result, (list, tuple)) else len(result or {})
467
+ self._log.debug("space_permissions_retrieved", space_key=space_key, count=count)
468
+ return result
469
+
470
+ return self._with_retry(operation, f"get_space_permissions {space_key}")
471
+
472
+ def set_permissions_to_user_for_space(
473
+ self,
474
+ space_key: str,
475
+ user_key: str,
476
+ *,
477
+ operations: list[str] | None = None,
478
+ ) -> dict[str, Any]:
479
+ """Grant permissions to a specific user for a space."""
480
+
481
+ def operation():
482
+ result = self._client.set_permissions_to_user_for_space(
483
+ space_key,
484
+ user_key,
485
+ operations=operations,
486
+ )
487
+ self._log.info(
488
+ "space_permissions_user_set",
489
+ space_key=space_key,
490
+ user_key=user_key,
491
+ operations=operations or [],
492
+ )
493
+ return result
494
+
495
+ return self._with_retry(operation, f"set_permissions_to_user_for_space {space_key} {user_key}")
496
+
497
+ def set_permissions_to_group_for_space(
498
+ self,
499
+ space_key: str,
500
+ group_name: str,
501
+ *,
502
+ operations: list[str] | None = None,
503
+ ) -> dict[str, Any]:
504
+ """Grant permissions to a group for a space."""
505
+
506
+ def operation():
507
+ result = self._client.set_permissions_to_group_for_space(
508
+ space_key,
509
+ group_name,
510
+ operations=operations,
511
+ )
512
+ self._log.info(
513
+ "space_permissions_group_set",
514
+ space_key=space_key,
515
+ group_name=group_name,
516
+ operations=operations or [],
517
+ )
518
+ return result
519
+
520
+ return self._with_retry(operation, f"set_permissions_to_group_for_space {space_key} {group_name}")
521
+
522
+ def set_permissions_to_anonymous_for_space(
523
+ self,
524
+ space_key: str,
525
+ *,
526
+ operations: list[str] | None = None,
527
+ ) -> dict[str, Any]:
528
+ """Grant permissions to anonymous users for a space."""
529
+
530
+ def operation():
531
+ result = self._client.set_permissions_to_anonymous_for_space(space_key, operations=operations)
532
+ self._log.info(
533
+ "space_permissions_anonymous_set",
534
+ space_key=space_key,
535
+ operations=operations or [],
536
+ )
537
+ return result
538
+
539
+ return self._with_retry(operation, f"set_permissions_to_anonymous_for_space {space_key}")
540
+
541
+ def set_permissions_to_multiple_items_for_space(
542
+ self,
543
+ space_key: str,
544
+ items: list[dict[str, Any]],
545
+ ) -> dict[str, Any]:
546
+ """Grant permissions to multiple users/groups in a single request."""
547
+
548
+ def operation():
549
+ result = self._client.set_permissions_to_multiple_items_for_space(space_key, items)
550
+ self._log.info(
551
+ "space_permissions_bulk_set",
552
+ space_key=space_key,
553
+ items=len(items),
554
+ )
555
+ return result
556
+
557
+ return self._with_retry(operation, f"set_permissions_to_multiple_items_for_space {space_key}")
558
+
559
+ def remove_permissions_from_user_for_space(self, space_key: str, user_key: str) -> dict[str, Any]:
560
+ """Remove all permissions granted to a user for a space."""
561
+
562
+ def operation():
563
+ result = self._client.remove_permissions_from_user_for_space(space_key, user_key)
564
+ self._log.info("space_permissions_user_removed", space_key=space_key, user_key=user_key)
565
+ return result
566
+
567
+ return self._with_retry(operation, f"remove_permissions_from_user_for_space {space_key} {user_key}")
568
+
569
+ def remove_permissions_from_group_for_space(self, space_key: str, group_name: str) -> dict[str, Any]:
570
+ """Remove all permissions granted to a group for a space."""
571
+
572
+ def operation():
573
+ result = self._client.remove_permissions_from_group_for_space(space_key, group_name)
574
+ self._log.info("space_permissions_group_removed", space_key=space_key, group_name=group_name)
575
+ return result
576
+
577
+ return self._with_retry(operation, f"remove_permissions_from_group_for_space {space_key} {group_name}")
578
+
579
+ def remove_permissions_from_anonymous_for_space(self, space_key: str) -> dict[str, Any]:
580
+ """Remove permissions granted to anonymous users for a space."""
581
+
582
+ def operation():
583
+ result = self._client.remove_permissions_from_anonymous_for_space(space_key)
584
+ self._log.info("space_permissions_anonymous_removed", space_key=space_key)
585
+ return result
586
+
587
+ return self._with_retry(operation, f"remove_permissions_from_anonymous_for_space {space_key}")
588
+
589
+ # --- User & Group Management ---
590
+
591
+ def get_all_groups(self, start: int = 0, limit: int = 1000) -> list[dict[str, Any]]:
592
+ """Get all groups in Confluence."""
593
+
594
+ def operation():
595
+ result = self._client.get_all_groups(start=start, limit=limit)
596
+ self._log.debug("groups_retrieved", count=len(result) if isinstance(result, list) else 0, start=start, limit=limit)
597
+ return result if isinstance(result, list) else []
598
+
599
+ return self._with_retry(operation, f"get_all_groups start={start} limit={limit}")
600
+
601
+ def get_group_members(self, group_name: str, start: int = 0, limit: int = 1000) -> list[dict[str, Any]]:
602
+ """Get members of a group."""
603
+
604
+ def operation():
605
+ result = self._client.get_group_members(group_name, start=start, limit=limit)
606
+ self._log.debug("group_members_retrieved", group_name=group_name, count=len(result) if isinstance(result, list) else 0)
607
+ return result if isinstance(result, list) else []
608
+
609
+ return self._with_retry(operation, f"get_group_members {group_name} start={start} limit={limit}")
610
+
611
+ def get_user_details_by_username(self, username: str, expand: str | None = None) -> dict[str, Any]:
612
+ """Get user details by username."""
613
+
614
+ def operation():
615
+ result = self._client.get_user_details_by_username(username, expand=expand)
616
+ self._log.debug("user_retrieved_by_username", username=username)
617
+ return result
618
+
619
+ return self._with_retry(operation, f"get_user_details_by_username {username}")
620
+
621
+ def get_user_details_by_userkey(self, userkey: str, expand: str | None = None) -> dict[str, Any]:
622
+ """Get user details by user key."""
623
+
624
+ def operation():
625
+ result = self._client.get_user_details_by_userkey(userkey, expand=expand)
626
+ self._log.debug("user_retrieved_by_userkey", userkey=userkey)
627
+ return result
628
+
629
+ return self._with_retry(operation, f"get_user_details_by_userkey {userkey}")
630
+
631
+ def change_user_password(self, username: str, password: str) -> None:
632
+ """Change user password."""
633
+
634
+ def operation():
635
+ self._client.change_user_password(username, password)
636
+ self._log.info("user_password_changed", username=username)
637
+ return None
638
+
639
+ return self._with_retry(operation, f"change_user_password {username}")
640
+
641
+ def add_user_to_group(self, username: str, group_name: str) -> None:
642
+ """Add user to group."""
643
+
644
+ def operation():
645
+ self._client.add_user_to_group(username, group_name)
646
+ self._log.info("user_added_to_group", username=username, group_name=group_name)
647
+ return None
648
+
649
+ return self._with_retry(operation, f"add_user_to_group {username} {group_name}")
650
+
651
+ def remove_user_from_group(self, username: str, group_name: str) -> None:
652
+ """Remove user from group."""
653
+
654
+ def operation():
655
+ self._client.remove_user_from_group(username, group_name)
656
+ self._log.info("user_removed_from_group", username=username, group_name=group_name)
657
+ return None
658
+
659
+ return self._with_retry(operation, f"remove_user_from_group {username} {group_name}")
660
+
661
+ # --- Export Operations ---
662
+
663
+ def export_page(self, page_id: str, api_version: str | None = None) -> bytes:
664
+ """Export page as PDF.
665
+
666
+ Note: The SDK auto-detects Cloud/Server mode based on client initialization.
667
+ The api_version parameter is accepted for CLI compatibility but not passed to SDK.
668
+ """
669
+
670
+ def operation():
671
+ # SDK auto-detects Cloud/Server mode, so we don't pass api_version
672
+ # The parameter is kept for CLI compatibility but ignored here
673
+ result = self._client.export_page(page_id) # type: ignore[call-arg]
674
+ detected_mode = "cloud" if self._is_cloud else "server"
675
+ self._log.info("page_exported", page_id=page_id, detected_mode=detected_mode)
676
+ # SDK returns bytes or file path; ensure we return bytes
677
+ if isinstance(result, bytes):
678
+ return result
679
+ elif isinstance(result, str):
680
+ # If it's a file path, read it
681
+ with open(result, "rb") as f:
682
+ return f.read()
683
+ else:
684
+ raise ConfluenceClientError(f"Unexpected export_page return type: {type(result)}", context={"page_id": page_id})
685
+
686
+ return self._with_retry(operation, f"export_page {page_id}")
687
+
688
+ def get_space_export(self, space_key: str, export_type: str) -> str:
689
+ """Get space export download URL."""
690
+
691
+ def operation():
692
+ result = self._client.get_space_export(space_key, export_type)
693
+ self._log.info("space_export_url_retrieved", space_key=space_key, export_type=export_type)
694
+ return result if isinstance(result, str) else str(result)
695
+
696
+ return self._with_retry(operation, f"get_space_export {space_key} {export_type}")
697
+
698
+ # --- Draft Management (Server-only) ---
699
+
700
+ def get_draft_page_by_id(self, page_id: str) -> dict[str, Any]:
701
+ """Get draft page by ID (Server-only)."""
702
+ if self._is_cloud:
703
+ raise ConfluenceClientError(
704
+ "get_draft_page_by_id is only available in Confluence Server/Data Center mode",
705
+ context={"page_id": page_id},
706
+ )
707
+
708
+ def operation():
709
+ result = self._client.get_draft_page_by_id(page_id) # type: ignore[attr-defined]
710
+ self._log.debug("draft_page_retrieved", page_id=page_id)
711
+ return result
712
+
713
+ return self._with_retry(operation, f"get_draft_page_by_id {page_id}")
714
+
715
+ def get_all_draft_pages_from_space(self, space_key: str, limit: int = 25) -> list[dict[str, Any]]:
716
+ """Get all draft pages from a space (Server-only)."""
717
+ if self._is_cloud:
718
+ raise ConfluenceClientError(
719
+ "get_all_draft_pages_from_space is only available in Confluence Server/Data Center mode",
720
+ context={"space_key": space_key},
721
+ )
722
+
723
+ def operation():
724
+ result = self._client.get_all_draft_pages_from_space(space_key, limit=limit) # type: ignore[attr-defined]
725
+ self._log.debug("draft_pages_retrieved", space_key=space_key, count=len(result) if isinstance(result, list) else 0)
726
+ return result if isinstance(result, list) else []
727
+
728
+ return self._with_retry(operation, f"get_all_draft_pages_from_space {space_key}")
729
+
730
+ def remove_page_as_draft(self, page_id: str) -> None:
731
+ """Remove page as draft (Server-only)."""
732
+ if self._is_cloud:
733
+ raise ConfluenceClientError(
734
+ "remove_page_as_draft is only available in Confluence Server/Data Center mode",
735
+ context={"page_id": page_id},
736
+ )
737
+
738
+ def operation():
739
+ self._client.remove_page_as_draft(page_id) # type: ignore[attr-defined]
740
+ self._log.info("draft_page_removed", page_id=page_id)
741
+ return None
742
+
743
+ return self._with_retry(operation, f"remove_page_as_draft {page_id}")
744
+
745
+ # --- Cache Management (Server-only) ---
746
+
747
+ def get_cache_statistics(self) -> dict[str, Any]:
748
+ """Get cache statistics (Server-only)."""
749
+ if self._is_cloud:
750
+ raise ConfluenceClientError(
751
+ "get_cache_statistics is only available in Confluence Server/Data Center mode",
752
+ context={},
753
+ )
754
+
755
+ def operation():
756
+ # SDK doesn't have get_cache_statistics, use REST API directly
757
+ # Cache statistics endpoint: /rest/cache/1.0/stats
758
+ response = self.request(method="GET", path="/rest/cache/1.0/stats")
759
+ # SDK request method returns response object, extract JSON
760
+ if hasattr(response, 'json'):
761
+ result = response.json()
762
+ else:
763
+ result = response # Already JSON dict
764
+ self._log.debug("cache_statistics_retrieved")
765
+ return result
766
+
767
+ return self._with_retry(operation, "get_cache_statistics")
768
+
769
+ def flush_cache(self, cache_name: str | None = None) -> None:
770
+ """Flush cache (Server-only).
771
+
772
+ Args:
773
+ cache_name: Optional cache name to flush specific cache. If None, flushes all caches.
774
+ """
775
+ if self._is_cloud:
776
+ raise ConfluenceClientError(
777
+ "flush_cache is only available in Confluence Server/Data Center mode",
778
+ context={"cache_name": cache_name},
779
+ )
780
+
781
+ def operation():
782
+ if cache_name:
783
+ # Flush specific cache package
784
+ self._client.clean_package_cache(cache_name=cache_name) # type: ignore[attr-defined]
785
+ self._log.info("cache_flushed", cache_name=cache_name)
786
+ else:
787
+ # Flush all caches
788
+ self._client.clean_all_caches() # type: ignore[attr-defined]
789
+ self._log.info("all_caches_flushed")
790
+
791
+ return self._with_retry(operation, f"flush_cache {cache_name or 'all'}")
792
+
793
+ def get_cache_size(self) -> dict[str, Any]:
794
+ """Get cache size information (Server-only)."""
795
+ if self._is_cloud:
796
+ raise ConfluenceClientError(
797
+ "get_cache_size is only available in Confluence Server/Data Center mode",
798
+ context={},
799
+ )
800
+
801
+ def operation():
802
+ # SDK doesn't have get_cache_size, use REST API directly
803
+ # Cache size endpoint: /rest/cache/1.0/size
804
+ response = self.request(method="GET", path="/rest/cache/1.0/size")
805
+ # SDK request method returns response object, extract JSON
806
+ if hasattr(response, 'json'):
807
+ result = response.json()
808
+ else:
809
+ result = response # Already JSON dict
810
+ self._log.debug("cache_size_retrieved")
811
+ return result
812
+
813
+ return self._with_retry(operation, "get_cache_size")
814
+
815
+ # --- Advanced Content Operations ---
816
+
817
+ def get_page_ancestors(self, page_id: str) -> list[dict[str, Any]]:
818
+ """Get page ancestors (parent pages)."""
819
+
820
+ def operation():
821
+ result = self._client.get_page_ancestors(page_id) # type: ignore[attr-defined]
822
+ self._log.debug("page_ancestors_retrieved", page_id=page_id, count=len(result) if isinstance(result, list) else 0)
823
+ return result if isinstance(result, list) else []
824
+
825
+ return self._with_retry(operation, f"get_page_ancestors {page_id}")
826
+
827
+ def move_page(self, page_id: str, target_title: str, position: str = "append") -> dict[str, Any]:
828
+ """Move page to a new location."""
829
+
830
+ def operation():
831
+ result = self._client.move_page(page_id, target_title, position=position) # type: ignore[attr-defined]
832
+ self._log.info("page_moved", page_id=page_id, target_title=target_title, position=position)
833
+ return result
834
+
835
+ return self._with_retry(operation, f"move_page {page_id} {target_title}")
836
+
837
+ def get_tables_from_page(self, page_id: str) -> list[dict[str, Any]]:
838
+ """Extract tables from page."""
839
+
840
+ def operation():
841
+ result = self._client.get_tables_from_page(page_id) # type: ignore[attr-defined]
842
+ self._log.debug("tables_extracted", page_id=page_id, count=len(result) if isinstance(result, list) else 0)
843
+ return result if isinstance(result, list) else []
844
+
845
+ return self._with_retry(operation, f"get_tables_from_page {page_id}")
846
+
847
+ def scrap_regex_from_page(self, page_id: str, regex: str) -> list[dict[str, Any]]:
848
+ """Extract regex matches from page."""
849
+
850
+ def operation():
851
+ result = self._client.scrap_regex_from_page(page_id, regex) # type: ignore[attr-defined]
852
+ self._log.debug("regex_matches_extracted", page_id=page_id, pattern=regex, count=len(result) if isinstance(result, list) else 0)
853
+ return result if isinstance(result, list) else []
854
+
855
+ return self._with_retry(operation, f"scrap_regex_from_page {page_id} {regex}")
856
+
857
+ def get_all_restrictions_for_content(self, content_id: str) -> dict[str, Any]:
858
+ """Get all restrictions for content."""
859
+
860
+ def operation():
861
+ result = self._client.get_all_restrictions_for_content(content_id) # type: ignore[attr-defined]
862
+ self._log.debug("content_restrictions_retrieved", content_id=content_id)
863
+ return result
864
+
865
+ return self._with_retry(operation, f"get_all_restrictions_for_content {content_id}")
866
+
867
+ # --- History & Versioning ---
868
+
869
+ def history(self, page_id: str) -> dict[str, Any]:
870
+ """Get page history."""
871
+
872
+ def operation():
873
+ result = self._client.history(page_id) # type: ignore[attr-defined]
874
+ self._log.debug("page_history_retrieved", page_id=page_id)
875
+ return result
876
+
877
+ return self._with_retry(operation, f"history {page_id}")
878
+
879
+ def get_content_history_by_version_number(self, page_id: str, version: int) -> dict[str, Any]:
880
+ """Get specific version of content."""
881
+
882
+ def operation():
883
+ result = self._client.get_content_history_by_version_number(page_id, version) # type: ignore[attr-defined]
884
+ self._log.debug("content_version_retrieved", page_id=page_id, version=version)
885
+ return result
886
+
887
+ return self._with_retry(operation, f"get_content_history_by_version_number {page_id} {version}")
888
+
889
+ def remove_content_history(self, page_id: str, version: int) -> None:
890
+ """Remove content history (experimental)."""
891
+
892
+ def operation():
893
+ self._client.remove_content_history(page_id, version) # type: ignore[attr-defined]
894
+ self._log.info("content_history_removed", page_id=page_id, version=version)
895
+ return None
896
+
897
+ return self._with_retry(operation, f"remove_content_history {page_id} {version}")
898
+
899
+ def archive_space(self, space_key: str) -> dict[str, Any]:
900
+ """Archive a Confluence space."""
901
+
902
+ def operation():
903
+ result = self._client.archive_space(space_key)
904
+ self._log.info("space_archived", space_key=space_key)
905
+ return result or {"status": "archived"}
906
+
907
+ return self._with_retry(operation, f"archive_space {space_key}")
908
+
909
+ def get_trashed_contents_by_space(
910
+ self,
911
+ space_key: str,
912
+ *,
913
+ cursor: str | None = None,
914
+ expand: str | None = None,
915
+ limit: int = 100,
916
+ ) -> dict[str, Any]:
917
+ """List trashed content for a space."""
918
+
919
+ def operation():
920
+ result = self._client.get_trashed_contents_by_space(
921
+ space_key,
922
+ cursor=cursor,
923
+ expand=expand,
924
+ limit=limit,
925
+ )
926
+ count = len(result.get("results", [])) if isinstance(result, dict) else 0
927
+ self._log.debug(
928
+ "space_trash_listed",
929
+ space_key=space_key,
930
+ count=count,
931
+ cursor=cursor,
932
+ limit=limit,
933
+ )
934
+ return result
935
+
936
+ return self._with_retry(operation, f"get_trashed_contents_by_space {space_key}")
937
+
938
+ def remove_trashed_contents_by_space(self, space_key: str) -> dict[str, Any]:
939
+ """Remove trashed content for a space."""
940
+
941
+ def operation():
942
+ result = self._client.remove_trashed_contents_by_space(space_key)
943
+ self._log.info("space_trash_cleared", space_key=space_key)
944
+ return result or {"status": "removed"}
945
+
946
+ return self._with_retry(operation, f"remove_trashed_contents_by_space {space_key}")
947
+
948
+ def get_child_pages(self, page_id: str) -> list[dict[str, Any]]:
949
+ """Get child pages of a page."""
950
+ try:
951
+ result = self._client.get_child_pages(page_id)
952
+ # Convert generator to list if needed
953
+ if hasattr(result, '__iter__') and not isinstance(result, (list, tuple, dict)):
954
+ result = list(result)
955
+ self._log.debug("child_pages_retrieved", page_id=page_id, count=len(result) if result else 0)
956
+ return result or []
957
+
958
+ except Exception as exc:
959
+ self._handle_api_error(exc, f"get_child_pages {page_id}")
960
+
961
+ def get_page_by_title(self, space_key: str, title: str) -> dict[str, Any] | None:
962
+ """Get a page by space and title."""
963
+ try:
964
+ result = self._client.get_page_by_title(space_key, title)
965
+ self._log.debug("page_retrieved_by_title", space_key=space_key, title=title)
966
+ return result
967
+
968
+ except Exception as exc:
969
+ self._handle_api_error(exc, f"get_page_by_title {space_key}/{title}")
970
+
971
+ def get_attachments(self, page_id: str) -> list[dict[str, Any]]:
972
+ """Get attachments for a page."""
973
+ try:
974
+ result = self._client.get_attachments_from_content(page_id)
975
+ self._log.debug("attachments_retrieved", page_id=page_id, count=len(result) if result else 0)
976
+ return result or []
977
+
978
+ except Exception as exc:
979
+ self._handle_api_error(exc, f"get_attachments {page_id}")
980
+
981
+ def get_attachment_content(self, attachment_id: str) -> bytes:
982
+ """Get attachment content by ID."""
983
+ try:
984
+ result = self._client.get_attachment_data(attachment_id)
985
+ self._log.debug("attachment_content_retrieved", attachment_id=attachment_id)
986
+ return result
987
+
988
+ except Exception as exc:
989
+ self._handle_api_error(exc, f"get_attachment_content {attachment_id}")
990
+
991
+ def update_attachment_data(
992
+ self,
993
+ page_id: str,
994
+ attachment_id: str,
995
+ filename: str,
996
+ content: bytes,
997
+ content_type: str | None = None,
998
+ *,
999
+ comment: str | None = None,
1000
+ ) -> dict[str, Any]:
1001
+ """Update an existing attachment.
1002
+
1003
+ According to atlassian-python-api docs, attach_content automatically updates
1004
+ if the file exists, versioning the new file and keeping the old one.
1005
+ """
1006
+ def operation():
1007
+ # Use attach_content - it automatically updates if file exists
1008
+ # See: https://atlassian-python-api.readthedocs.io/confluence.html#
1009
+ result = self._client.attach_content(
1010
+ content=content,
1011
+ name=filename,
1012
+ content_type=content_type or "application/octet-stream",
1013
+ page_id=page_id,
1014
+ comment=comment,
1015
+ )
1016
+ self._log.info("attachment_updated", page_id=page_id, attachment_id=attachment_id, filename=filename)
1017
+ return result
1018
+
1019
+ return self._with_retry(operation, f"update_attachment {attachment_id}")
1020
+
1021
+ def delete_attachment_by_id(self, page_id: str, attachment_id: str, version: int | None = None) -> None:
1022
+ """Delete an attachment by ID (historic versions only).
1023
+
1024
+ The Atlassian SDK's ``delete_attachment_by_id`` endpoint only removes versions
1025
+ strictly lower than the current one, so callers should provide ``version`` when
1026
+ targeting a specific revision. Cloud docs:
1027
+ https://atlassian-python-api.readthedocs.io/confluence.html#confluence.Confluence.delete_attachment_by_id
1028
+ """
1029
+ try:
1030
+ versions_payload = self._client.get_attachment_history(attachment_id) or []
1031
+ except Exception: # pragma: no cover - defensive
1032
+ versions_payload = []
1033
+
1034
+ versions = []
1035
+ for item in versions_payload:
1036
+ if isinstance(item, dict):
1037
+ number = item.get("number")
1038
+ if isinstance(number, int):
1039
+ versions.append(number)
1040
+
1041
+ if version is not None:
1042
+ versions = [ver for ver in versions if ver == version]
1043
+
1044
+ if not versions:
1045
+ if version is not None:
1046
+ versions = [version]
1047
+ else:
1048
+ raise ConfluenceClientError(
1049
+ f"No attachment versions found for {attachment_id}. Specify --version explicitly.",
1050
+ context={"attachment_id": attachment_id, "page_id": page_id},
1051
+ )
1052
+
1053
+ for ver in sorted(set(versions), reverse=True):
1054
+ try:
1055
+ self._client.delete_attachment_by_id(attachment_id, ver)
1056
+ self._log.info(
1057
+ "attachment_version_deleted",
1058
+ page_id=page_id,
1059
+ attachment_id=attachment_id,
1060
+ version=ver,
1061
+ )
1062
+ except Exception as exc:
1063
+ self._handle_api_error(exc, f"delete_attachment {attachment_id} (version {ver})")
1064
+
1065
+ def delete_attachment(self, page_id: str, filename: str, version: int | None = None) -> None:
1066
+ """Delete the latest version of an attachment by filename.
1067
+
1068
+ Mirrors :py:meth:`Confluence.delete_attachment` from the Atlassian SDK.
1069
+ """
1070
+
1071
+ try:
1072
+ self._client.delete_attachment(page_id, filename, version=version)
1073
+ self._log.info(
1074
+ "attachment_deleted",
1075
+ page_id=page_id,
1076
+ filename=filename,
1077
+ version=version,
1078
+ )
1079
+ except Exception as exc:
1080
+ self._handle_api_error(exc, f"delete_attachment {filename}")
1081
+
1082
+ def add_comment(self, page_id: str, body: str) -> dict[str, Any]:
1083
+ """Add a comment to a page."""
1084
+ try:
1085
+ result = self._client.add_comment(page_id, body)
1086
+ self._log.info("comment_added", page_id=page_id)
1087
+ return result
1088
+
1089
+ except Exception as exc:
1090
+ self._handle_api_error(exc, f"add_comment {page_id}")
1091
+
1092
+ def get_page_history(self, page_id: str) -> dict[str, Any]:
1093
+ """Get page version history."""
1094
+ try:
1095
+ result = self._client.get_history(page_id)
1096
+ self._log.debug("page_history_retrieved", page_id=page_id)
1097
+ return result
1098
+
1099
+ except Exception as exc:
1100
+ self._handle_api_error(exc, f"get_page_history {page_id}")
1101
+
1102
+ def close(self) -> None:
1103
+ """Close the client (cleanup method for compatibility)."""
1104
+ # atlassian-python-api doesn't require explicit cleanup
1105
+ pass
1106
+
1107
+ def __enter__(self) -> ConfluenceClient:
1108
+ return self
1109
+
1110
+ def __exit__(self, exc_type, exc, tb) -> None:
1111
+ self.close()
1112
+
1113
+
1114
+ __all__ = ["ConfluenceClient"]