bt-cli 0.4.37__tar.gz → 0.4.39__tar.gz

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 (205) hide show
  1. {bt_cli-0.4.37 → bt_cli-0.4.39}/.claude/skills/epml/SKILL.md +56 -2
  2. {bt_cli-0.4.37 → bt_cli-0.4.39}/CLAUDE.md +1 -1
  3. {bt_cli-0.4.37 → bt_cli-0.4.39}/PKG-INFO +1 -1
  4. {bt_cli-0.4.37 → bt_cli-0.4.39}/pyproject.toml +1 -1
  5. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/__init__.py +1 -1
  6. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/data/CLAUDE.md +1 -1
  7. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/data/skills/epml/SKILL.md +56 -2
  8. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/epml/client/base.py +23 -0
  9. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/epml/commands/rbp_roles.py +122 -6
  10. {bt_cli-0.4.37/src/bt_cli/data → bt_cli-0.4.39/.claude}/skills/bt/SKILL.md +0 -0
  11. {bt_cli-0.4.37/src/bt_cli/data → bt_cli-0.4.39/.claude}/skills/entitle/SKILL.md +0 -0
  12. {bt_cli-0.4.37/src/bt_cli/data → bt_cli-0.4.39/.claude}/skills/epmw/SKILL.md +0 -0
  13. {bt_cli-0.4.37/src/bt_cli/data → bt_cli-0.4.39/.claude}/skills/pra/SKILL.md +0 -0
  14. {bt_cli-0.4.37/src/bt_cli/data → bt_cli-0.4.39/.claude}/skills/pws/SKILL.md +0 -0
  15. {bt_cli-0.4.37 → bt_cli-0.4.39}/.env.example +0 -0
  16. {bt_cli-0.4.37 → bt_cli-0.4.39}/.github/workflows/ci.yml +0 -0
  17. {bt_cli-0.4.37 → bt_cli-0.4.39}/.github/workflows/release.yml +0 -0
  18. {bt_cli-0.4.37 → bt_cli-0.4.39}/.gitignore +0 -0
  19. {bt_cli-0.4.37 → bt_cli-0.4.39}/README.md +0 -0
  20. {bt_cli-0.4.37 → bt_cli-0.4.39}/assets/cli-help.png +0 -0
  21. {bt_cli-0.4.37 → bt_cli-0.4.39}/assets/cli-output.png +0 -0
  22. {bt_cli-0.4.37 → bt_cli-0.4.39}/bt-cli.spec +0 -0
  23. {bt_cli-0.4.37 → bt_cli-0.4.39}/bt_entry.py +0 -0
  24. {bt_cli-0.4.37 → bt_cli-0.4.39}/epml-implementation-plan.md +0 -0
  25. {bt_cli-0.4.37 → bt_cli-0.4.39}/scripts/bt_entry.py +0 -0
  26. {bt_cli-0.4.37 → bt_cli-0.4.39}/scripts/sync-package-data.sh +0 -0
  27. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/cli.py +0 -0
  28. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/commands/__init__.py +0 -0
  29. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/commands/configure.py +0 -0
  30. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/commands/learn.py +0 -0
  31. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/commands/quick.py +0 -0
  32. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/core/__init__.py +0 -0
  33. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/core/auth.py +0 -0
  34. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/core/client.py +0 -0
  35. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/core/config.py +0 -0
  36. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/core/config_file.py +0 -0
  37. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/core/csv_utils.py +0 -0
  38. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/core/errors.py +0 -0
  39. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/core/output.py +0 -0
  40. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/core/prompts.py +0 -0
  41. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/core/rest_debug.py +0 -0
  42. {bt_cli-0.4.37/tests/pws → bt_cli-0.4.39/src/bt_cli/data}/__init__.py +0 -0
  43. {bt_cli-0.4.37/.claude → bt_cli-0.4.39/src/bt_cli/data}/skills/bt/SKILL.md +0 -0
  44. {bt_cli-0.4.37/.claude → bt_cli-0.4.39/src/bt_cli/data}/skills/entitle/SKILL.md +0 -0
  45. {bt_cli-0.4.37/.claude → bt_cli-0.4.39/src/bt_cli/data}/skills/epmw/SKILL.md +0 -0
  46. {bt_cli-0.4.37/.claude → bt_cli-0.4.39/src/bt_cli/data}/skills/pra/SKILL.md +0 -0
  47. {bt_cli-0.4.37/.claude → bt_cli-0.4.39/src/bt_cli/data}/skills/pws/SKILL.md +0 -0
  48. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/entitle/__init__.py +0 -0
  49. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/entitle/client/__init__.py +0 -0
  50. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/entitle/client/base.py +0 -0
  51. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/entitle/commands/__init__.py +0 -0
  52. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/entitle/commands/accounts.py +0 -0
  53. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/entitle/commands/applications.py +0 -0
  54. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/entitle/commands/auth.py +0 -0
  55. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/entitle/commands/bundles.py +0 -0
  56. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/entitle/commands/integrations.py +0 -0
  57. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/entitle/commands/permissions.py +0 -0
  58. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/entitle/commands/policies.py +0 -0
  59. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/entitle/commands/resources.py +0 -0
  60. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/entitle/commands/roles.py +0 -0
  61. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/entitle/commands/users.py +0 -0
  62. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/entitle/commands/workflows.py +0 -0
  63. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/entitle/models/__init__.py +0 -0
  64. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/entitle/models/bundle.py +0 -0
  65. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/entitle/models/common.py +0 -0
  66. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/entitle/models/integration.py +0 -0
  67. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/entitle/models/permission.py +0 -0
  68. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/entitle/models/policy.py +0 -0
  69. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/entitle/models/resource.py +0 -0
  70. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/entitle/models/role.py +0 -0
  71. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/entitle/models/user.py +0 -0
  72. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/entitle/models/workflow.py +0 -0
  73. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/epml/__init__.py +0 -0
  74. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/epml/client/__init__.py +0 -0
  75. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/epml/commands/__init__.py +0 -0
  76. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/epml/commands/audit.py +0 -0
  77. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/epml/commands/auth.py +0 -0
  78. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/epml/commands/client_pkg.py +0 -0
  79. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/epml/commands/external_apis.py +0 -0
  80. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/epml/commands/hosts.py +0 -0
  81. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/epml/commands/iolog.py +0 -0
  82. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/epml/commands/license.py +0 -0
  83. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/epml/commands/quick.py +0 -0
  84. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/epml/commands/rbp_cmdgrps.py +0 -0
  85. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/epml/commands/rbp_entitlement.py +0 -0
  86. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/epml/commands/rbp_hostgrps.py +0 -0
  87. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/epml/commands/rbp_policy.py +0 -0
  88. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/epml/commands/rbp_tests.py +0 -0
  89. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/epml/commands/rbp_tmdategrps.py +0 -0
  90. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/epml/commands/rbp_tx.py +0 -0
  91. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/epml/commands/rbp_usergrps.py +0 -0
  92. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/epml/commands/settings.py +0 -0
  93. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/epml/commands/siems.py +0 -0
  94. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/epml/commands/users.py +0 -0
  95. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/epml/models/__init__.py +0 -0
  96. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/epmw/__init__.py +0 -0
  97. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/epmw/client/__init__.py +0 -0
  98. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/epmw/client/base.py +0 -0
  99. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/epmw/commands/__init__.py +0 -0
  100. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/epmw/commands/audits.py +0 -0
  101. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/epmw/commands/auth.py +0 -0
  102. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/epmw/commands/computers.py +0 -0
  103. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/epmw/commands/events.py +0 -0
  104. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/epmw/commands/groups.py +0 -0
  105. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/epmw/commands/policies.py +0 -0
  106. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/epmw/commands/quick.py +0 -0
  107. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/epmw/commands/requests.py +0 -0
  108. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/epmw/commands/roles.py +0 -0
  109. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/epmw/commands/tasks.py +0 -0
  110. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/epmw/commands/users.py +0 -0
  111. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/epmw/models/__init__.py +0 -0
  112. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/pra/__init__.py +0 -0
  113. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/pra/client/__init__.py +0 -0
  114. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/pra/client/base.py +0 -0
  115. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/pra/commands/__init__.py +0 -0
  116. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/pra/commands/auth.py +0 -0
  117. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/pra/commands/import_export.py +0 -0
  118. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/pra/commands/jump_clients.py +0 -0
  119. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/pra/commands/jump_groups.py +0 -0
  120. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/pra/commands/jump_items.py +0 -0
  121. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/pra/commands/jumpoints.py +0 -0
  122. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/pra/commands/policies.py +0 -0
  123. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/pra/commands/quick.py +0 -0
  124. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/pra/commands/teams.py +0 -0
  125. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/pra/commands/users.py +0 -0
  126. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/pra/commands/vault.py +0 -0
  127. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/pra/models/__init__.py +0 -0
  128. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/pra/models/common.py +0 -0
  129. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/pra/models/jump_client.py +0 -0
  130. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/pra/models/jump_group.py +0 -0
  131. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/pra/models/jump_item.py +0 -0
  132. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/pra/models/jumpoint.py +0 -0
  133. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/pra/models/team.py +0 -0
  134. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/pra/models/user.py +0 -0
  135. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/pra/models/vault.py +0 -0
  136. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/pws/__init__.py +0 -0
  137. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/pws/client/__init__.py +0 -0
  138. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/pws/client/base.py +0 -0
  139. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/pws/client/beyondinsight.py +0 -0
  140. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/pws/client/passwordsafe.py +0 -0
  141. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/pws/commands/__init__.py +0 -0
  142. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/pws/commands/accounts.py +0 -0
  143. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/pws/commands/assets.py +0 -0
  144. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/pws/commands/attributes.py +0 -0
  145. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/pws/commands/auth.py +0 -0
  146. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/pws/commands/clouds.py +0 -0
  147. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/pws/commands/config.py +0 -0
  148. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/pws/commands/credentials.py +0 -0
  149. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/pws/commands/databases.py +0 -0
  150. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/pws/commands/directories.py +0 -0
  151. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/pws/commands/functional.py +0 -0
  152. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/pws/commands/import_export.py +0 -0
  153. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/pws/commands/platforms.py +0 -0
  154. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/pws/commands/quick.py +0 -0
  155. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/pws/commands/search.py +0 -0
  156. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/pws/commands/secrets.py +0 -0
  157. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/pws/commands/systems.py +0 -0
  158. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/pws/commands/users.py +0 -0
  159. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/pws/commands/workgroups.py +0 -0
  160. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/pws/config.py +0 -0
  161. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/pws/models/__init__.py +0 -0
  162. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/pws/models/account.py +0 -0
  163. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/pws/models/asset.py +0 -0
  164. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/pws/models/common.py +0 -0
  165. {bt_cli-0.4.37 → bt_cli-0.4.39}/src/bt_cli/pws/models/system.py +0 -0
  166. {bt_cli-0.4.37 → bt_cli-0.4.39}/tests/__init__.py +0 -0
  167. {bt_cli-0.4.37 → bt_cli-0.4.39}/tests/conftest.py +0 -0
  168. {bt_cli-0.4.37 → bt_cli-0.4.39}/tests/core/__init__.py +0 -0
  169. {bt_cli-0.4.37 → bt_cli-0.4.39}/tests/core/test_auth.py +0 -0
  170. {bt_cli-0.4.37 → bt_cli-0.4.39}/tests/core/test_config.py +0 -0
  171. {bt_cli-0.4.37 → bt_cli-0.4.39}/tests/core/test_errors.py +0 -0
  172. {bt_cli-0.4.37 → bt_cli-0.4.39}/tests/core/test_rest_debug.py +0 -0
  173. {bt_cli-0.4.37 → bt_cli-0.4.39}/tests/entitle/__init__.py +0 -0
  174. {bt_cli-0.4.37 → bt_cli-0.4.39}/tests/entitle/test_client.py +0 -0
  175. {bt_cli-0.4.37 → bt_cli-0.4.39}/tests/entitle/test_commands.py +0 -0
  176. {bt_cli-0.4.37 → bt_cli-0.4.39}/tests/entitle-smoke-test.sh +0 -0
  177. {bt_cli-0.4.37 → bt_cli-0.4.39}/tests/epml/__init__.py +0 -0
  178. {bt_cli-0.4.37 → bt_cli-0.4.39}/tests/epml/test_client.py +0 -0
  179. {bt_cli-0.4.37 → bt_cli-0.4.39}/tests/epml/test_commands.py +0 -0
  180. {bt_cli-0.4.37 → bt_cli-0.4.39}/tests/epmw/__init__.py +0 -0
  181. {bt_cli-0.4.37 → bt_cli-0.4.39}/tests/epmw/test_client.py +0 -0
  182. {bt_cli-0.4.37 → bt_cli-0.4.39}/tests/epmw/test_commands.py +0 -0
  183. {bt_cli-0.4.37 → bt_cli-0.4.39}/tests/epmw-quick-test-plan.md +0 -0
  184. {bt_cli-0.4.37 → bt_cli-0.4.39}/tests/fixtures/__init__.py +0 -0
  185. {bt_cli-0.4.37 → bt_cli-0.4.39}/tests/fixtures/responses.py +0 -0
  186. {bt_cli-0.4.37 → bt_cli-0.4.39}/tests/integration/__init__.py +0 -0
  187. {bt_cli-0.4.37 → bt_cli-0.4.39}/tests/integration/conftest.py +0 -0
  188. {bt_cli-0.4.37 → bt_cli-0.4.39}/tests/integration/helpers.py +0 -0
  189. {bt_cli-0.4.37 → bt_cli-0.4.39}/tests/integration/test_entitle_integration.py +0 -0
  190. {bt_cli-0.4.37 → bt_cli-0.4.39}/tests/integration/test_epmw_integration.py +0 -0
  191. {bt_cli-0.4.37 → bt_cli-0.4.39}/tests/integration/test_epmw_lifecycle.py +0 -0
  192. {bt_cli-0.4.37 → bt_cli-0.4.39}/tests/integration/test_pra_integration.py +0 -0
  193. {bt_cli-0.4.37 → bt_cli-0.4.39}/tests/integration/test_pra_lifecycle.py +0 -0
  194. {bt_cli-0.4.37 → bt_cli-0.4.39}/tests/integration/test_pws_integration.py +0 -0
  195. {bt_cli-0.4.37 → bt_cli-0.4.39}/tests/integration/test_pws_lifecycle.py +0 -0
  196. {bt_cli-0.4.37 → bt_cli-0.4.39}/tests/pra/__init__.py +0 -0
  197. {bt_cli-0.4.37 → bt_cli-0.4.39}/tests/pra/test_client.py +0 -0
  198. {bt_cli-0.4.37 → bt_cli-0.4.39}/tests/pra/test_commands.py +0 -0
  199. {bt_cli-0.4.37 → bt_cli-0.4.39}/tests/pra-smoke-test.sh +0 -0
  200. {bt_cli-0.4.37 → bt_cli-0.4.39}/tests/pra-test-plan.md +0 -0
  201. {bt_cli-0.4.37/src/bt_cli/data → bt_cli-0.4.39/tests/pws}/__init__.py +0 -0
  202. {bt_cli-0.4.37 → bt_cli-0.4.39}/tests/pws/test_client.py +0 -0
  203. {bt_cli-0.4.37 → bt_cli-0.4.39}/tests/pws/test_commands.py +0 -0
  204. {bt_cli-0.4.37 → bt_cli-0.4.39}/tests/pws-quick-test-plan.md +0 -0
  205. {bt_cli-0.4.37 → bt_cli-0.4.39}/tests/pws-smoke-test.sh +0 -0
@@ -133,8 +133,13 @@ bt epml rbp tmdategrps tmdates replace <tmdategrp_id> --file tmdates.json
133
133
  # Roles + assignments
134
134
  bt epml rbp roles list
135
135
  bt epml rbp roles create --name "Helpdesk Role" --action A \
136
+ --description "..." --comment "..." --tag "AdminAccess" \
137
+ --risk 6 --rpt 1 \
136
138
  --iolog '/iologs/%date%/%uniqueid%.iolog' \
137
- --message "This session is logged."
139
+ --banner-text "Helpdesk Role" # builds standard ###-framed banner
140
+ # or --message "raw multi-line message"
141
+ # or --banner # banner with %rbprole% template
142
+ bt epml rbp roles update <id> --tag NewTag --risk 9 --rpt 1 # in-place edit
138
143
  bt epml rbp roles duplicate <role_id>
139
144
  bt epml rbp roles cmdgrps add <role_id> --ids 1,2 # cmdgrps & tmdategrps: just IDs
140
145
  bt epml rbp roles tmdategrps add <role_id> --ids 1
@@ -158,6 +163,54 @@ bt epml rbp policy revert
158
163
  bt epml rbp policy delete-all --yes-i-mean-it # destructive, gated
159
164
  ```
160
165
 
166
+ ## Worked example: build a role from scratch
167
+
168
+ End-to-end recipe for a "users in group X can run shells on all hosts at any time" role with I/O logging and a session banner. Mirrors the pattern of the tenant's existing roles (Postgres, Docker Admin, etc.).
169
+
170
+ ```bash
171
+ # 1. Command group containing the commands the role allows
172
+ bt epml rbp cmdgrps create --name "RootShells" --description "Root shells with arguments"
173
+ # captures: id 35
174
+ bt epml rbp cmdgrps commands add 35 --commands "bash *,sh *,ksh *,zsh *,csh *,bash,sh,ksh,zsh,csh"
175
+
176
+ # 2. Role itself, all metadata in one shot
177
+ bt epml rbp roles create \
178
+ --name "RootShells" \
179
+ --description "Root shell access — I/O logged" \
180
+ --comment "Root shell access — I/O logged" \
181
+ --tag "RootAccess" \
182
+ --risk 9 \
183
+ --rpt 1 \
184
+ --action A \
185
+ --iolog '/iologs/%date%/%uniqueid%.iolog' \
186
+ --banner-text "Root Shell Access"
187
+ # captures: id 126
188
+
189
+ # 3. Wire up the four group assignments
190
+ bt epml rbp roles cmdgrps add 126 --ids 35 # cmdgrp: just IDs
191
+ bt epml rbp roles hostgrps add 126 --ids 1 --kind B # All Hosts, both Submit and Run-as
192
+ bt epml rbp roles usergrps add 126 --ids 4 --kind S # Administrators as Submit (who requests)
193
+ bt epml rbp roles usergrps add 126 --ids 3 --kind R # 'root' as Run-as (whose identity)
194
+ bt epml rbp roles tmdategrps add 126 --ids 1 # Any Time
195
+
196
+ # 4. Verify
197
+ bt epml rbp roles list -o json | python3 -c 'import json,sys;[print(json.dumps(r,indent=2)) for r in json.load(sys.stdin) if r["id"]==126]'
198
+ bt epml rbp roles cmdgrps list 126
199
+ bt epml rbp roles hostgrps list 126
200
+ bt epml rbp roles usergrps list 126
201
+ bt epml rbp roles tmdategrps list 126
202
+ bt epml rbp entitlement run -o json | grep RootShells # rpt=1 makes it surface
203
+ ```
204
+
205
+ The resulting banner displayed to users (server-substituted at session time):
206
+ ```
207
+ ############################################################
208
+ Policy: Root Shell Access
209
+ Status: %event%
210
+ Session Recorded: Yes
211
+ ############################################################
212
+ ```
213
+
161
214
  ## Test suites + transactions (the killer workflow)
162
215
 
163
216
  ```bash
@@ -206,7 +259,8 @@ bt epml quick tests-then-deploy --suite <suite_id> # commits if pass; rollb
206
259
  - **Children share the parent's `id`** in GET responses — the listed `id` field is the cmdgrp/hostgrp/usergrp ID, not unique per child. The actual identifier is the `cmd`/`host`/`user` text.
207
260
  - **Role assignments take a single object per request, not an array**. The CLI loops under the hood; the wire body is e.g. `{"cmds": 35}`, `{"hosts": 1, "type": "S"}`, `{"users": 4, "type": "R"}`, `{"tmdates": 1}`.
208
261
  - **Hostgrp / usergrp assignments require a `type` field** (`S` = Submit / who requests, `R` = Run-as / whose identity the command runs under). Without `type` the request 400s with "RBP role type not in: [S,R]". CLI: `--kind S|R|B` on `roles hostgrps add` and `roles usergrps add`. `B` (default) creates both an S row and an R row for the same id — appropriate when the same group plays both roles. For "Admin requests, runs as root", do `--ids 4 --kind S` and `--ids 3 --kind R` separately.
209
- - **Roles need `rpt: 1`** to appear in `bt epml rbp entitlement run`. The role still functions for policy evaluation either way, but only `rpt=1` roles are surfaced in the report. Not yet exposed by the CLI file-level edit via export/import is the workaround.
262
+ - **Roles need `rpt: 1`** to appear in `bt epml rbp entitlement run`. The role still functions for policy evaluation either way, but only `rpt=1` roles are surfaced in the report. Set with `--rpt 1` on `roles create` / `roles update` (added in 0.4.38).
263
+ - **Role update is upsert-on-id, not partial**: hitting POST `/roles` with `{id, ...}` *overwrites* the matching record — fields you omit get cleared. The CLI's `roles update` does read-modify-write (fetch the role, merge changes, post the whole record) so this feels partial. Role-child relations (rolecmds/roleusers/etc.) are filtered out of the merge so assignments survive an update. **If you hit the API directly via curl**, you must send the full record yourself.
210
264
 
211
265
  ## Known gaps (TODO)
212
266
 
@@ -1,6 +1,6 @@
1
1
  # BT-CLI
2
2
 
3
- BeyondTrust Platform CLI for Password Safe, Entitle, PRA, EPM Windows, and EPM Linux. **Version: 0.4.37**
3
+ BeyondTrust Platform CLI for Password Safe, Entitle, PRA, EPM Windows, and EPM Linux. **Version: 0.4.39**
4
4
 
5
5
  ## Setup
6
6
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bt-cli
3
- Version: 0.4.37
3
+ Version: 0.4.39
4
4
  Summary: BeyondTrust Platform CLI (unofficial) - Password Safe, Entitle, PRA, EPM
5
5
  Author-email: Dave Grendysz <dgrendysz@beyondtrust.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "bt-cli"
7
- version = "0.4.37"
7
+ version = "0.4.39"
8
8
  description = "BeyondTrust Platform CLI (unofficial) - Password Safe, Entitle, PRA, EPM"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -1,3 +1,3 @@
1
1
  """BeyondTrust Unified Admin CLI."""
2
2
 
3
- __version__ = "0.4.37"
3
+ __version__ = "0.4.39"
@@ -1,6 +1,6 @@
1
1
  # BT-CLI
2
2
 
3
- BeyondTrust Platform CLI for Password Safe, Entitle, PRA, EPM Windows, and EPM Linux. **Version: 0.4.37**
3
+ BeyondTrust Platform CLI for Password Safe, Entitle, PRA, EPM Windows, and EPM Linux. **Version: 0.4.39**
4
4
 
5
5
  ## Setup
6
6
 
@@ -133,8 +133,13 @@ bt epml rbp tmdategrps tmdates replace <tmdategrp_id> --file tmdates.json
133
133
  # Roles + assignments
134
134
  bt epml rbp roles list
135
135
  bt epml rbp roles create --name "Helpdesk Role" --action A \
136
+ --description "..." --comment "..." --tag "AdminAccess" \
137
+ --risk 6 --rpt 1 \
136
138
  --iolog '/iologs/%date%/%uniqueid%.iolog' \
137
- --message "This session is logged."
139
+ --banner-text "Helpdesk Role" # builds standard ###-framed banner
140
+ # or --message "raw multi-line message"
141
+ # or --banner # banner with %rbprole% template
142
+ bt epml rbp roles update <id> --tag NewTag --risk 9 --rpt 1 # in-place edit
138
143
  bt epml rbp roles duplicate <role_id>
139
144
  bt epml rbp roles cmdgrps add <role_id> --ids 1,2 # cmdgrps & tmdategrps: just IDs
140
145
  bt epml rbp roles tmdategrps add <role_id> --ids 1
@@ -158,6 +163,54 @@ bt epml rbp policy revert
158
163
  bt epml rbp policy delete-all --yes-i-mean-it # destructive, gated
159
164
  ```
160
165
 
166
+ ## Worked example: build a role from scratch
167
+
168
+ End-to-end recipe for a "users in group X can run shells on all hosts at any time" role with I/O logging and a session banner. Mirrors the pattern of the tenant's existing roles (Postgres, Docker Admin, etc.).
169
+
170
+ ```bash
171
+ # 1. Command group containing the commands the role allows
172
+ bt epml rbp cmdgrps create --name "RootShells" --description "Root shells with arguments"
173
+ # captures: id 35
174
+ bt epml rbp cmdgrps commands add 35 --commands "bash *,sh *,ksh *,zsh *,csh *,bash,sh,ksh,zsh,csh"
175
+
176
+ # 2. Role itself, all metadata in one shot
177
+ bt epml rbp roles create \
178
+ --name "RootShells" \
179
+ --description "Root shell access — I/O logged" \
180
+ --comment "Root shell access — I/O logged" \
181
+ --tag "RootAccess" \
182
+ --risk 9 \
183
+ --rpt 1 \
184
+ --action A \
185
+ --iolog '/iologs/%date%/%uniqueid%.iolog' \
186
+ --banner-text "Root Shell Access"
187
+ # captures: id 126
188
+
189
+ # 3. Wire up the four group assignments
190
+ bt epml rbp roles cmdgrps add 126 --ids 35 # cmdgrp: just IDs
191
+ bt epml rbp roles hostgrps add 126 --ids 1 --kind B # All Hosts, both Submit and Run-as
192
+ bt epml rbp roles usergrps add 126 --ids 4 --kind S # Administrators as Submit (who requests)
193
+ bt epml rbp roles usergrps add 126 --ids 3 --kind R # 'root' as Run-as (whose identity)
194
+ bt epml rbp roles tmdategrps add 126 --ids 1 # Any Time
195
+
196
+ # 4. Verify
197
+ bt epml rbp roles list -o json | python3 -c 'import json,sys;[print(json.dumps(r,indent=2)) for r in json.load(sys.stdin) if r["id"]==126]'
198
+ bt epml rbp roles cmdgrps list 126
199
+ bt epml rbp roles hostgrps list 126
200
+ bt epml rbp roles usergrps list 126
201
+ bt epml rbp roles tmdategrps list 126
202
+ bt epml rbp entitlement run -o json | grep RootShells # rpt=1 makes it surface
203
+ ```
204
+
205
+ The resulting banner displayed to users (server-substituted at session time):
206
+ ```
207
+ ############################################################
208
+ Policy: Root Shell Access
209
+ Status: %event%
210
+ Session Recorded: Yes
211
+ ############################################################
212
+ ```
213
+
161
214
  ## Test suites + transactions (the killer workflow)
162
215
 
163
216
  ```bash
@@ -206,7 +259,8 @@ bt epml quick tests-then-deploy --suite <suite_id> # commits if pass; rollb
206
259
  - **Children share the parent's `id`** in GET responses — the listed `id` field is the cmdgrp/hostgrp/usergrp ID, not unique per child. The actual identifier is the `cmd`/`host`/`user` text.
207
260
  - **Role assignments take a single object per request, not an array**. The CLI loops under the hood; the wire body is e.g. `{"cmds": 35}`, `{"hosts": 1, "type": "S"}`, `{"users": 4, "type": "R"}`, `{"tmdates": 1}`.
208
261
  - **Hostgrp / usergrp assignments require a `type` field** (`S` = Submit / who requests, `R` = Run-as / whose identity the command runs under). Without `type` the request 400s with "RBP role type not in: [S,R]". CLI: `--kind S|R|B` on `roles hostgrps add` and `roles usergrps add`. `B` (default) creates both an S row and an R row for the same id — appropriate when the same group plays both roles. For "Admin requests, runs as root", do `--ids 4 --kind S` and `--ids 3 --kind R` separately.
209
- - **Roles need `rpt: 1`** to appear in `bt epml rbp entitlement run`. The role still functions for policy evaluation either way, but only `rpt=1` roles are surfaced in the report. Not yet exposed by the CLI file-level edit via export/import is the workaround.
262
+ - **Roles need `rpt: 1`** to appear in `bt epml rbp entitlement run`. The role still functions for policy evaluation either way, but only `rpt=1` roles are surfaced in the report. Set with `--rpt 1` on `roles create` / `roles update` (added in 0.4.38).
263
+ - **Role update is upsert-on-id, not partial**: hitting POST `/roles` with `{id, ...}` *overwrites* the matching record — fields you omit get cleared. The CLI's `roles update` does read-modify-write (fetch the role, merge changes, post the whole record) so this feels partial. Role-child relations (rolecmds/roleusers/etc.) are filtered out of the merge so assignments survive an update. **If you hit the API directly via curl**, you must send the full record yourself.
210
264
 
211
265
  ## Known gaps (TODO)
212
266
 
@@ -476,6 +476,29 @@ class EPMLClient:
476
476
  body.setdefault("action", "A")
477
477
  return self.post(f"/api/pbul/{h}/rbp/roles", json=body)
478
478
 
479
+ def update_role(self, role_id: int, partial: Dict[str, Any], host_id: Optional[int] = None) -> Any:
480
+ """Update an existing role (read-modify-write).
481
+
482
+ The API's create/update endpoint OVERWRITES on `id` match — fields not
483
+ in the body get zeroed/cleared. To make an update feel like a partial
484
+ modification, this helper fetches the current role, merges in your
485
+ changes, and posts the merged record. Avoids accidentally clobbering
486
+ `action`, `name`, `iolog`, etc. when you only meant to change `tag`.
487
+
488
+ Caveat: the v1 API has no GET-single-role, so we list and filter.
489
+ """
490
+ h = self.host(host_id)
491
+ roles = self.list_roles(host_id=host_id) or []
492
+ current = next((r for r in roles if r.get("id") == role_id), None)
493
+ if current is None:
494
+ raise ValueError(f"role id {role_id} not found")
495
+ # role child relations (rolecmds/roleusers/etc.) get filtered out so we
496
+ # don't accidentally rewrite assignments — those have their own endpoints.
497
+ merged = {k: v for k, v in current.items() if not k.startswith("role")}
498
+ merged.update(partial)
499
+ merged["id"] = role_id
500
+ return self.post(f"/api/pbul/{h}/rbp/roles", json=merged)
501
+
479
502
  def delete_roles(self, ids: List[int], host_id: Optional[int] = None) -> None:
480
503
  h = self.host(host_id)
481
504
  for rid in ids:
@@ -14,6 +14,37 @@ def _host_opt():
14
14
  return typer.Option(None, "--host", "-H", help="PMUL host id (default: BT_EPML_DEFAULT_HOST)")
15
15
 
16
16
 
17
+ _BAR = "#" * 60
18
+
19
+
20
+ def _build_banner(title: Optional[str] = None) -> str:
21
+ """Build a banner-style policy message.
22
+
23
+ Server substitutes `%rbprole%` and `%event%` at session time. Pass a
24
+ title string to replace the role-name line with literal text.
25
+ """
26
+ title_line = f" Policy: {title}\r\n" if title else " Policy: %rbprole%\r\n"
27
+ return (
28
+ "\r\n"
29
+ + _BAR + "\r\n"
30
+ + title_line
31
+ + " Status: %event%\r\n"
32
+ + " Session Recorded: Yes\r\n"
33
+ + _BAR + "\r\n"
34
+ )
35
+
36
+
37
+ def _resolve_message(message: Optional[str], banner: bool, banner_text: Optional[str]) -> Optional[str]:
38
+ """Pick the final message: explicit --message wins; else build a banner if asked."""
39
+ if message is not None:
40
+ return message
41
+ if banner_text is not None:
42
+ return _build_banner(banner_text)
43
+ if banner:
44
+ return _build_banner()
45
+ return None # leave message unset
46
+
47
+
17
48
  @app.command("list")
18
49
  def list_roles(
19
50
  host: Optional[int] = _host_opt(),
@@ -45,13 +76,18 @@ def list_roles(
45
76
  def create_role(
46
77
  name: str = typer.Option(..., "--name", "-n"),
47
78
  description: str = typer.Option("", "--description", "-d"),
79
+ comment: Optional[str] = typer.Option(None, "--comment", "-c", help="Internal comment (defaults to description if omitted)"),
80
+ tag: Optional[str] = typer.Option(None, "--tag", help="Free-form tag for filtering/reporting"),
81
+ risk: Optional[int] = typer.Option(None, "--risk", "-r", help="Risk score 0-9 (Postgres-style example uses 6)"),
82
+ rpt: Optional[int] = typer.Option(None, "--rpt", help="Entitlement reporting: 1 to surface in `entitlement run`, 0 to hide"),
48
83
  action: str = typer.Option("A", "--action", "-a", help="Role verdict: A=Allow, R=Reject"),
49
84
  iolog: Optional[str] = typer.Option(
50
85
  None, "--iolog",
51
86
  help="I/O log path template (e.g. /iologs/%date%/%uniqueid%.iolog). Omit to disable.",
52
87
  ),
53
- message: Optional[str] = typer.Option(None, "--message", "-m", help="Message shown to the requesting user"),
54
- comment: Optional[str] = typer.Option(None, "--comment", help="Internal comment"),
88
+ message: Optional[str] = typer.Option(None, "--message", "-m", help="Raw message shown to the requesting user (multi-line OK; verbatim)"),
89
+ banner: bool = typer.Option(False, "--banner", help="Use a standard ###-framed banner with %rbprole% and %event% template variables"),
90
+ banner_text: Optional[str] = typer.Option(None, "--banner-text", help="Like --banner but use the given title text instead of %rbprole% substitution"),
55
91
  disabled: bool = typer.Option(False, "--disabled", help="Create the role disabled"),
56
92
  host: Optional[int] = _host_opt(),
57
93
  ):
@@ -62,18 +98,42 @@ def create_role(
62
98
 
63
99
  `iolog` accepts the appliance's path template syntax — typical value is
64
100
  `/iologs/%date%/%uniqueid%.iolog`. Omit to disable I/O logging on this role.
101
+
102
+ Three ways to set the user-visible message (in priority order):
103
+ --message "raw text" verbatim
104
+ --banner-text "Custom Title" builds the standard banner with this title
105
+ --banner builds the standard banner using %rbprole%
106
+
107
+ The standard banner format mirrors the appliance's existing roles:
108
+ ##############################
109
+ Policy: <title or %rbprole%>
110
+ Status: %event%
111
+ Session Recorded: Yes
112
+ ##############################
65
113
  """
66
114
  from bt_cli.epml.client import get_client
67
115
  if action not in ("A", "R"):
68
116
  typer.echo(f"--action must be 'A' or 'R', got {action!r}", err=True)
69
117
  raise typer.Exit(2)
118
+
70
119
  body = {"name": name, "description": description, "action": action, "disabled": disabled}
71
- if iolog is not None:
72
- body["iolog"] = iolog
73
- if message is not None:
74
- body["message"] = message
75
120
  if comment is not None:
76
121
  body["comment"] = comment
122
+ elif description:
123
+ body["comment"] = description # mirror description into comment (matches existing roles)
124
+ if tag is not None:
125
+ body["tag"] = tag
126
+ if risk is not None:
127
+ body["risk"] = risk
128
+ if rpt is not None:
129
+ body["rpt"] = rpt
130
+ if iolog is not None:
131
+ body["iolog"] = iolog
132
+
133
+ final_message = _resolve_message(message, banner, banner_text)
134
+ if final_message is not None:
135
+ body["message"] = final_message
136
+
77
137
  try:
78
138
  with get_client() as c:
79
139
  result = c.create_role(body, host_id=host)
@@ -84,6 +144,62 @@ def create_role(
84
144
  print_api_error(e, "create role"); raise typer.Exit(1)
85
145
 
86
146
 
147
+ @app.command("update")
148
+ def update_role(
149
+ role_id: int = typer.Argument(..., help="Role ID to update"),
150
+ name: Optional[str] = typer.Option(None, "--name", "-n", help="Rename the role"),
151
+ description: Optional[str] = typer.Option(None, "--description", "-d"),
152
+ comment: Optional[str] = typer.Option(None, "--comment", "-c"),
153
+ tag: Optional[str] = typer.Option(None, "--tag"),
154
+ risk: Optional[int] = typer.Option(None, "--risk", "-r"),
155
+ rpt: Optional[int] = typer.Option(None, "--rpt"),
156
+ action: Optional[str] = typer.Option(None, "--action", "-a", help="A=Allow / R=Reject"),
157
+ iolog: Optional[str] = typer.Option(None, "--iolog", help="I/O log template; pass '' to disable"),
158
+ message: Optional[str] = typer.Option(None, "--message", "-m"),
159
+ banner: bool = typer.Option(False, "--banner"),
160
+ banner_text: Optional[str] = typer.Option(None, "--banner-text"),
161
+ disabled: Optional[bool] = typer.Option(None, "--disabled/--enabled"),
162
+ host: Optional[int] = _host_opt(),
163
+ ):
164
+ """Update a role's metadata in place. Only fields you pass are modified.
165
+
166
+ Examples:
167
+ bt epml rbp roles update 126 --tag "RootAccess" --risk 9 --rpt 1
168
+ bt epml rbp roles update 126 --banner-text "Root Shell Access"
169
+ bt epml rbp roles update 126 --message "Custom session header..."
170
+ """
171
+ from bt_cli.epml.client import get_client
172
+ if action is not None and action not in ("A", "R"):
173
+ typer.echo(f"--action must be 'A' or 'R', got {action!r}", err=True)
174
+ raise typer.Exit(2)
175
+
176
+ body: dict = {}
177
+ for key, val in (
178
+ ("name", name), ("description", description), ("comment", comment),
179
+ ("tag", tag), ("risk", risk), ("rpt", rpt), ("action", action),
180
+ ("iolog", iolog), ("disabled", disabled),
181
+ ):
182
+ if val is not None:
183
+ body[key] = val
184
+
185
+ final_message = _resolve_message(message, banner, banner_text)
186
+ if final_message is not None:
187
+ body["message"] = final_message
188
+
189
+ if not body:
190
+ typer.echo("Nothing to update — pass at least one field flag.", err=True)
191
+ raise typer.Exit(2)
192
+
193
+ try:
194
+ with get_client() as c:
195
+ result = c.update_role(role_id, body, host_id=host)
196
+ print_json(result)
197
+ except httpx.HTTPStatusError as e:
198
+ print_api_error(e, "update role"); raise typer.Exit(1)
199
+ except Exception as e:
200
+ print_api_error(e, "update role"); raise typer.Exit(1)
201
+
202
+
87
203
  @app.command("delete")
88
204
  def delete_role(
89
205
  ids: str = typer.Argument(..., help="Comma-separated role IDs"),
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes