bt-cli 0.4.35__tar.gz → 0.4.37__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.35/src/bt_cli/data → bt_cli-0.4.37/.claude}/skills/epml/SKILL.md +31 -4
  2. {bt_cli-0.4.35 → bt_cli-0.4.37}/CLAUDE.md +1 -1
  3. {bt_cli-0.4.35 → bt_cli-0.4.37}/PKG-INFO +1 -1
  4. {bt_cli-0.4.35 → bt_cli-0.4.37}/pyproject.toml +1 -1
  5. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/__init__.py +1 -1
  6. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/data/CLAUDE.md +1 -1
  7. {bt_cli-0.4.35/.claude → bt_cli-0.4.37/src/bt_cli/data}/skills/epml/SKILL.md +31 -4
  8. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/epml/client/base.py +54 -8
  9. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/epml/commands/rbp_roles.py +77 -24
  10. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/epml/commands/rbp_tmdategrps.py +62 -9
  11. {bt_cli-0.4.35/src/bt_cli/data → bt_cli-0.4.37/.claude}/skills/bt/SKILL.md +0 -0
  12. {bt_cli-0.4.35/src/bt_cli/data → bt_cli-0.4.37/.claude}/skills/entitle/SKILL.md +0 -0
  13. {bt_cli-0.4.35/src/bt_cli/data → bt_cli-0.4.37/.claude}/skills/epmw/SKILL.md +0 -0
  14. {bt_cli-0.4.35/src/bt_cli/data → bt_cli-0.4.37/.claude}/skills/pra/SKILL.md +0 -0
  15. {bt_cli-0.4.35/src/bt_cli/data → bt_cli-0.4.37/.claude}/skills/pws/SKILL.md +0 -0
  16. {bt_cli-0.4.35 → bt_cli-0.4.37}/.env.example +0 -0
  17. {bt_cli-0.4.35 → bt_cli-0.4.37}/.github/workflows/ci.yml +0 -0
  18. {bt_cli-0.4.35 → bt_cli-0.4.37}/.github/workflows/release.yml +0 -0
  19. {bt_cli-0.4.35 → bt_cli-0.4.37}/.gitignore +0 -0
  20. {bt_cli-0.4.35 → bt_cli-0.4.37}/README.md +0 -0
  21. {bt_cli-0.4.35 → bt_cli-0.4.37}/assets/cli-help.png +0 -0
  22. {bt_cli-0.4.35 → bt_cli-0.4.37}/assets/cli-output.png +0 -0
  23. {bt_cli-0.4.35 → bt_cli-0.4.37}/bt-cli.spec +0 -0
  24. {bt_cli-0.4.35 → bt_cli-0.4.37}/bt_entry.py +0 -0
  25. {bt_cli-0.4.35 → bt_cli-0.4.37}/epml-implementation-plan.md +0 -0
  26. {bt_cli-0.4.35 → bt_cli-0.4.37}/scripts/bt_entry.py +0 -0
  27. {bt_cli-0.4.35 → bt_cli-0.4.37}/scripts/sync-package-data.sh +0 -0
  28. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/cli.py +0 -0
  29. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/commands/__init__.py +0 -0
  30. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/commands/configure.py +0 -0
  31. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/commands/learn.py +0 -0
  32. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/commands/quick.py +0 -0
  33. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/core/__init__.py +0 -0
  34. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/core/auth.py +0 -0
  35. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/core/client.py +0 -0
  36. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/core/config.py +0 -0
  37. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/core/config_file.py +0 -0
  38. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/core/csv_utils.py +0 -0
  39. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/core/errors.py +0 -0
  40. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/core/output.py +0 -0
  41. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/core/prompts.py +0 -0
  42. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/core/rest_debug.py +0 -0
  43. {bt_cli-0.4.35/tests/pws → bt_cli-0.4.37/src/bt_cli/data}/__init__.py +0 -0
  44. {bt_cli-0.4.35/.claude → bt_cli-0.4.37/src/bt_cli/data}/skills/bt/SKILL.md +0 -0
  45. {bt_cli-0.4.35/.claude → bt_cli-0.4.37/src/bt_cli/data}/skills/entitle/SKILL.md +0 -0
  46. {bt_cli-0.4.35/.claude → bt_cli-0.4.37/src/bt_cli/data}/skills/epmw/SKILL.md +0 -0
  47. {bt_cli-0.4.35/.claude → bt_cli-0.4.37/src/bt_cli/data}/skills/pra/SKILL.md +0 -0
  48. {bt_cli-0.4.35/.claude → bt_cli-0.4.37/src/bt_cli/data}/skills/pws/SKILL.md +0 -0
  49. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/entitle/__init__.py +0 -0
  50. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/entitle/client/__init__.py +0 -0
  51. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/entitle/client/base.py +0 -0
  52. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/entitle/commands/__init__.py +0 -0
  53. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/entitle/commands/accounts.py +0 -0
  54. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/entitle/commands/applications.py +0 -0
  55. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/entitle/commands/auth.py +0 -0
  56. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/entitle/commands/bundles.py +0 -0
  57. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/entitle/commands/integrations.py +0 -0
  58. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/entitle/commands/permissions.py +0 -0
  59. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/entitle/commands/policies.py +0 -0
  60. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/entitle/commands/resources.py +0 -0
  61. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/entitle/commands/roles.py +0 -0
  62. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/entitle/commands/users.py +0 -0
  63. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/entitle/commands/workflows.py +0 -0
  64. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/entitle/models/__init__.py +0 -0
  65. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/entitle/models/bundle.py +0 -0
  66. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/entitle/models/common.py +0 -0
  67. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/entitle/models/integration.py +0 -0
  68. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/entitle/models/permission.py +0 -0
  69. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/entitle/models/policy.py +0 -0
  70. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/entitle/models/resource.py +0 -0
  71. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/entitle/models/role.py +0 -0
  72. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/entitle/models/user.py +0 -0
  73. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/entitle/models/workflow.py +0 -0
  74. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/epml/__init__.py +0 -0
  75. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/epml/client/__init__.py +0 -0
  76. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/epml/commands/__init__.py +0 -0
  77. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/epml/commands/audit.py +0 -0
  78. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/epml/commands/auth.py +0 -0
  79. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/epml/commands/client_pkg.py +0 -0
  80. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/epml/commands/external_apis.py +0 -0
  81. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/epml/commands/hosts.py +0 -0
  82. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/epml/commands/iolog.py +0 -0
  83. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/epml/commands/license.py +0 -0
  84. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/epml/commands/quick.py +0 -0
  85. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/epml/commands/rbp_cmdgrps.py +0 -0
  86. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/epml/commands/rbp_entitlement.py +0 -0
  87. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/epml/commands/rbp_hostgrps.py +0 -0
  88. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/epml/commands/rbp_policy.py +0 -0
  89. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/epml/commands/rbp_tests.py +0 -0
  90. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/epml/commands/rbp_tx.py +0 -0
  91. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/epml/commands/rbp_usergrps.py +0 -0
  92. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/epml/commands/settings.py +0 -0
  93. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/epml/commands/siems.py +0 -0
  94. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/epml/commands/users.py +0 -0
  95. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/epml/models/__init__.py +0 -0
  96. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/epmw/__init__.py +0 -0
  97. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/epmw/client/__init__.py +0 -0
  98. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/epmw/client/base.py +0 -0
  99. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/epmw/commands/__init__.py +0 -0
  100. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/epmw/commands/audits.py +0 -0
  101. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/epmw/commands/auth.py +0 -0
  102. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/epmw/commands/computers.py +0 -0
  103. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/epmw/commands/events.py +0 -0
  104. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/epmw/commands/groups.py +0 -0
  105. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/epmw/commands/policies.py +0 -0
  106. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/epmw/commands/quick.py +0 -0
  107. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/epmw/commands/requests.py +0 -0
  108. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/epmw/commands/roles.py +0 -0
  109. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/epmw/commands/tasks.py +0 -0
  110. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/epmw/commands/users.py +0 -0
  111. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/epmw/models/__init__.py +0 -0
  112. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/pra/__init__.py +0 -0
  113. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/pra/client/__init__.py +0 -0
  114. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/pra/client/base.py +0 -0
  115. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/pra/commands/__init__.py +0 -0
  116. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/pra/commands/auth.py +0 -0
  117. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/pra/commands/import_export.py +0 -0
  118. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/pra/commands/jump_clients.py +0 -0
  119. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/pra/commands/jump_groups.py +0 -0
  120. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/pra/commands/jump_items.py +0 -0
  121. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/pra/commands/jumpoints.py +0 -0
  122. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/pra/commands/policies.py +0 -0
  123. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/pra/commands/quick.py +0 -0
  124. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/pra/commands/teams.py +0 -0
  125. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/pra/commands/users.py +0 -0
  126. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/pra/commands/vault.py +0 -0
  127. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/pra/models/__init__.py +0 -0
  128. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/pra/models/common.py +0 -0
  129. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/pra/models/jump_client.py +0 -0
  130. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/pra/models/jump_group.py +0 -0
  131. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/pra/models/jump_item.py +0 -0
  132. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/pra/models/jumpoint.py +0 -0
  133. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/pra/models/team.py +0 -0
  134. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/pra/models/user.py +0 -0
  135. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/pra/models/vault.py +0 -0
  136. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/pws/__init__.py +0 -0
  137. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/pws/client/__init__.py +0 -0
  138. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/pws/client/base.py +0 -0
  139. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/pws/client/beyondinsight.py +0 -0
  140. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/pws/client/passwordsafe.py +0 -0
  141. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/pws/commands/__init__.py +0 -0
  142. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/pws/commands/accounts.py +0 -0
  143. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/pws/commands/assets.py +0 -0
  144. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/pws/commands/attributes.py +0 -0
  145. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/pws/commands/auth.py +0 -0
  146. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/pws/commands/clouds.py +0 -0
  147. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/pws/commands/config.py +0 -0
  148. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/pws/commands/credentials.py +0 -0
  149. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/pws/commands/databases.py +0 -0
  150. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/pws/commands/directories.py +0 -0
  151. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/pws/commands/functional.py +0 -0
  152. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/pws/commands/import_export.py +0 -0
  153. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/pws/commands/platforms.py +0 -0
  154. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/pws/commands/quick.py +0 -0
  155. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/pws/commands/search.py +0 -0
  156. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/pws/commands/secrets.py +0 -0
  157. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/pws/commands/systems.py +0 -0
  158. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/pws/commands/users.py +0 -0
  159. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/pws/commands/workgroups.py +0 -0
  160. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/pws/config.py +0 -0
  161. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/pws/models/__init__.py +0 -0
  162. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/pws/models/account.py +0 -0
  163. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/pws/models/asset.py +0 -0
  164. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/pws/models/common.py +0 -0
  165. {bt_cli-0.4.35 → bt_cli-0.4.37}/src/bt_cli/pws/models/system.py +0 -0
  166. {bt_cli-0.4.35 → bt_cli-0.4.37}/tests/__init__.py +0 -0
  167. {bt_cli-0.4.35 → bt_cli-0.4.37}/tests/conftest.py +0 -0
  168. {bt_cli-0.4.35 → bt_cli-0.4.37}/tests/core/__init__.py +0 -0
  169. {bt_cli-0.4.35 → bt_cli-0.4.37}/tests/core/test_auth.py +0 -0
  170. {bt_cli-0.4.35 → bt_cli-0.4.37}/tests/core/test_config.py +0 -0
  171. {bt_cli-0.4.35 → bt_cli-0.4.37}/tests/core/test_errors.py +0 -0
  172. {bt_cli-0.4.35 → bt_cli-0.4.37}/tests/core/test_rest_debug.py +0 -0
  173. {bt_cli-0.4.35 → bt_cli-0.4.37}/tests/entitle/__init__.py +0 -0
  174. {bt_cli-0.4.35 → bt_cli-0.4.37}/tests/entitle/test_client.py +0 -0
  175. {bt_cli-0.4.35 → bt_cli-0.4.37}/tests/entitle/test_commands.py +0 -0
  176. {bt_cli-0.4.35 → bt_cli-0.4.37}/tests/entitle-smoke-test.sh +0 -0
  177. {bt_cli-0.4.35 → bt_cli-0.4.37}/tests/epml/__init__.py +0 -0
  178. {bt_cli-0.4.35 → bt_cli-0.4.37}/tests/epml/test_client.py +0 -0
  179. {bt_cli-0.4.35 → bt_cli-0.4.37}/tests/epml/test_commands.py +0 -0
  180. {bt_cli-0.4.35 → bt_cli-0.4.37}/tests/epmw/__init__.py +0 -0
  181. {bt_cli-0.4.35 → bt_cli-0.4.37}/tests/epmw/test_client.py +0 -0
  182. {bt_cli-0.4.35 → bt_cli-0.4.37}/tests/epmw/test_commands.py +0 -0
  183. {bt_cli-0.4.35 → bt_cli-0.4.37}/tests/epmw-quick-test-plan.md +0 -0
  184. {bt_cli-0.4.35 → bt_cli-0.4.37}/tests/fixtures/__init__.py +0 -0
  185. {bt_cli-0.4.35 → bt_cli-0.4.37}/tests/fixtures/responses.py +0 -0
  186. {bt_cli-0.4.35 → bt_cli-0.4.37}/tests/integration/__init__.py +0 -0
  187. {bt_cli-0.4.35 → bt_cli-0.4.37}/tests/integration/conftest.py +0 -0
  188. {bt_cli-0.4.35 → bt_cli-0.4.37}/tests/integration/helpers.py +0 -0
  189. {bt_cli-0.4.35 → bt_cli-0.4.37}/tests/integration/test_entitle_integration.py +0 -0
  190. {bt_cli-0.4.35 → bt_cli-0.4.37}/tests/integration/test_epmw_integration.py +0 -0
  191. {bt_cli-0.4.35 → bt_cli-0.4.37}/tests/integration/test_epmw_lifecycle.py +0 -0
  192. {bt_cli-0.4.35 → bt_cli-0.4.37}/tests/integration/test_pra_integration.py +0 -0
  193. {bt_cli-0.4.35 → bt_cli-0.4.37}/tests/integration/test_pra_lifecycle.py +0 -0
  194. {bt_cli-0.4.35 → bt_cli-0.4.37}/tests/integration/test_pws_integration.py +0 -0
  195. {bt_cli-0.4.35 → bt_cli-0.4.37}/tests/integration/test_pws_lifecycle.py +0 -0
  196. {bt_cli-0.4.35 → bt_cli-0.4.37}/tests/pra/__init__.py +0 -0
  197. {bt_cli-0.4.35 → bt_cli-0.4.37}/tests/pra/test_client.py +0 -0
  198. {bt_cli-0.4.35 → bt_cli-0.4.37}/tests/pra/test_commands.py +0 -0
  199. {bt_cli-0.4.35 → bt_cli-0.4.37}/tests/pra-smoke-test.sh +0 -0
  200. {bt_cli-0.4.35 → bt_cli-0.4.37}/tests/pra-test-plan.md +0 -0
  201. {bt_cli-0.4.35/src/bt_cli/data → bt_cli-0.4.37/tests/pws}/__init__.py +0 -0
  202. {bt_cli-0.4.35 → bt_cli-0.4.37}/tests/pws/test_client.py +0 -0
  203. {bt_cli-0.4.35 → bt_cli-0.4.37}/tests/pws/test_commands.py +0 -0
  204. {bt_cli-0.4.35 → bt_cli-0.4.37}/tests/pws-quick-test-plan.md +0 -0
  205. {bt_cli-0.4.35 → bt_cli-0.4.37}/tests/pws-smoke-test.sh +0 -0
@@ -132,13 +132,17 @@ bt epml rbp tmdategrps tmdates replace <tmdategrp_id> --file tmdates.json
132
132
 
133
133
  # Roles + assignments
134
134
  bt epml rbp roles list
135
- bt epml rbp roles create --name "Helpdesk Role"
135
+ bt epml rbp roles create --name "Helpdesk Role" --action A \
136
+ --iolog '/iologs/%date%/%uniqueid%.iolog' \
137
+ --message "This session is logged."
136
138
  bt epml rbp roles duplicate <role_id>
137
- bt epml rbp roles cmdgrps add <role_id> --ids 1,2
139
+ bt epml rbp roles cmdgrps add <role_id> --ids 1,2 # cmdgrps & tmdategrps: just IDs
140
+ bt epml rbp roles tmdategrps add <role_id> --ids 1
141
+ bt epml rbp roles hostgrps add <role_id> --ids 1 --kind B # B = both Submit and Run-as
142
+ bt epml rbp roles usergrps add <role_id> --ids 4 --kind S # S = Submit (who requests)
143
+ bt epml rbp roles usergrps add <role_id> --ids 3 --kind R # R = Run-as (whose identity)
138
144
  bt epml rbp roles cmdgrps remove <role_id> <cmdgrp_id>
139
145
  bt epml rbp roles hostgrps list <role_id>
140
- bt epml rbp roles usergrps add <role_id> --ids 5
141
- bt epml rbp roles tmdategrps add <role_id> --ids 1
142
146
 
143
147
  # Entitlement report ('who can do what')
144
148
  bt epml rbp entitlement run
@@ -200,11 +204,34 @@ bt epml quick tests-then-deploy --suite <suite_id> # commits if pass; rollb
200
204
  - **`POST /usergrps/multiple`** (bulk create): body is `{"usergroups": [...]}` — undocumented wrapper key.
201
205
  - **POST on child collections is additive**, not replacing. Calling `commands add` twice with the same command will get you a duplicate.
202
206
  - **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
+ - **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
+ - **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.
203
210
 
204
211
  ## Known gaps (TODO)
205
212
 
206
213
  - **`bt epml rbp roles get <id>`** — there is no GET-single-role in the v1 API. The detailed view (role with all child relations resolved) only exists at `/api/v6/pbul/{hostid}/rbp/roledetail/{id}`, which is on the IAM-only authorizer. Not callable with a PAT today. Workaround: use `bt epml rbp roles list` for the role row, then `bt epml rbp roles {cmdgrps,hostgrps,usergrps,tmdategrps} list <id>` to fetch each child relation.
207
214
 
215
+ ## Tenant-side gotchas (not a CLI bug)
216
+
217
+ - **`bt epml rbp tx begin` may return** `400 "RBP Transactions not enabled on host"`. This is a per-tenant feature flag on the PMUL host — transactions need to be enabled at the appliance / settings level before they'll work. The CLI surfaces the server's exact message; nothing client-side to fix. (Verified 2026-05-01 against the lab tenant — host id 100 does not have transactions enabled.) The `quick tests-then-deploy` workflow depends on transactions, so it'll also fail until the host is configured for it.
218
+
219
+ ## tmdate shape (`dotw` map)
220
+
221
+ The OpenAPI spec describes `tmdate` as a `string` field with `x-go-name: "JsonDefinition"`. The actual runtime shape is a structured object:
222
+
223
+ ```json
224
+ {"dotw": {
225
+ "mon": [{"from":"09:00","to":"17:00"}],
226
+ "tue": [{"from":"09:00","to":"17:00"}],
227
+ ...
228
+ "sat": [],
229
+ "sun": []
230
+ }}
231
+ ```
232
+
233
+ A day with an empty list is inactive. A day can have multiple windows (e.g. split shift). The CLI's `--from/--to/--days` short flags build the map for you; for anything more complex use `--file` with the full JSON.
234
+
208
235
  ## Path-version policy (CLI internal)
209
236
 
210
237
  Where the spec offers both legacy `/api/pbul/{hostid}/rbp/<x>` and newer
@@ -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.35**
3
+ BeyondTrust Platform CLI for Password Safe, Entitle, PRA, EPM Windows, and EPM Linux. **Version: 0.4.37**
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.35
3
+ Version: 0.4.37
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.35"
7
+ version = "0.4.37"
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.35"
3
+ __version__ = "0.4.37"
@@ -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.35**
3
+ BeyondTrust Platform CLI for Password Safe, Entitle, PRA, EPM Windows, and EPM Linux. **Version: 0.4.37**
4
4
 
5
5
  ## Setup
6
6
 
@@ -132,13 +132,17 @@ bt epml rbp tmdategrps tmdates replace <tmdategrp_id> --file tmdates.json
132
132
 
133
133
  # Roles + assignments
134
134
  bt epml rbp roles list
135
- bt epml rbp roles create --name "Helpdesk Role"
135
+ bt epml rbp roles create --name "Helpdesk Role" --action A \
136
+ --iolog '/iologs/%date%/%uniqueid%.iolog' \
137
+ --message "This session is logged."
136
138
  bt epml rbp roles duplicate <role_id>
137
- bt epml rbp roles cmdgrps add <role_id> --ids 1,2
139
+ bt epml rbp roles cmdgrps add <role_id> --ids 1,2 # cmdgrps & tmdategrps: just IDs
140
+ bt epml rbp roles tmdategrps add <role_id> --ids 1
141
+ bt epml rbp roles hostgrps add <role_id> --ids 1 --kind B # B = both Submit and Run-as
142
+ bt epml rbp roles usergrps add <role_id> --ids 4 --kind S # S = Submit (who requests)
143
+ bt epml rbp roles usergrps add <role_id> --ids 3 --kind R # R = Run-as (whose identity)
138
144
  bt epml rbp roles cmdgrps remove <role_id> <cmdgrp_id>
139
145
  bt epml rbp roles hostgrps list <role_id>
140
- bt epml rbp roles usergrps add <role_id> --ids 5
141
- bt epml rbp roles tmdategrps add <role_id> --ids 1
142
146
 
143
147
  # Entitlement report ('who can do what')
144
148
  bt epml rbp entitlement run
@@ -200,11 +204,34 @@ bt epml quick tests-then-deploy --suite <suite_id> # commits if pass; rollb
200
204
  - **`POST /usergrps/multiple`** (bulk create): body is `{"usergroups": [...]}` — undocumented wrapper key.
201
205
  - **POST on child collections is additive**, not replacing. Calling `commands add` twice with the same command will get you a duplicate.
202
206
  - **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
+ - **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
+ - **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.
203
210
 
204
211
  ## Known gaps (TODO)
205
212
 
206
213
  - **`bt epml rbp roles get <id>`** — there is no GET-single-role in the v1 API. The detailed view (role with all child relations resolved) only exists at `/api/v6/pbul/{hostid}/rbp/roledetail/{id}`, which is on the IAM-only authorizer. Not callable with a PAT today. Workaround: use `bt epml rbp roles list` for the role row, then `bt epml rbp roles {cmdgrps,hostgrps,usergrps,tmdategrps} list <id>` to fetch each child relation.
207
214
 
215
+ ## Tenant-side gotchas (not a CLI bug)
216
+
217
+ - **`bt epml rbp tx begin` may return** `400 "RBP Transactions not enabled on host"`. This is a per-tenant feature flag on the PMUL host — transactions need to be enabled at the appliance / settings level before they'll work. The CLI surfaces the server's exact message; nothing client-side to fix. (Verified 2026-05-01 against the lab tenant — host id 100 does not have transactions enabled.) The `quick tests-then-deploy` workflow depends on transactions, so it'll also fail until the host is configured for it.
218
+
219
+ ## tmdate shape (`dotw` map)
220
+
221
+ The OpenAPI spec describes `tmdate` as a `string` field with `x-go-name: "JsonDefinition"`. The actual runtime shape is a structured object:
222
+
223
+ ```json
224
+ {"dotw": {
225
+ "mon": [{"from":"09:00","to":"17:00"}],
226
+ "tue": [{"from":"09:00","to":"17:00"}],
227
+ ...
228
+ "sat": [],
229
+ "sun": []
230
+ }}
231
+ ```
232
+
233
+ A day with an empty list is inactive. A day can have multiple windows (e.g. split shift). The CLI's `--from/--to/--days` short flags build the map for you; for anything more complex use `--file` with the full JSON.
234
+
208
235
  ## Path-version policy (CLI internal)
209
236
 
210
237
  Where the spec offers both legacy `/api/pbul/{hostid}/rbp/<x>` and newer
@@ -489,9 +489,13 @@ class EPMLClient:
489
489
  h = self.host(host_id)
490
490
  return self.get(f"/api/pbul/{h}/rbp/roles/{role_id}/cmdgrps")
491
491
 
492
- def add_role_cmdgrps(self, role_id: int, cmdgrp_ids: List[int], host_id: Optional[int] = None) -> Any:
492
+ def add_role_cmdgrps(self, role_id: int, cmdgrp_ids: List[int], host_id: Optional[int] = None) -> List[Any]:
493
+ """Add cmdgrps to a role. The API accepts ONE assignment per request
494
+ as a bare object (`{cmds: <id>}`) — not an array. Loop here.
495
+ """
493
496
  h = self.host(host_id)
494
- return self.post(f"/api/pbul/{h}/rbp/roles/{role_id}/cmdgrps", json=cmdgrp_ids)
497
+ path = f"/api/pbul/{h}/rbp/roles/{role_id}/cmdgrps"
498
+ return [self.post(path, json={"cmds": cid}) for cid in cmdgrp_ids]
495
499
 
496
500
  def remove_role_cmdgrp(self, role_id: int, cmdgrp_id: int, host_id: Optional[int] = None) -> None:
497
501
  h = self.host(host_id)
@@ -501,9 +505,31 @@ class EPMLClient:
501
505
  h = self.host(host_id)
502
506
  return self.get(f"/api/pbul/{h}/rbp/roles/{role_id}/hostgrps")
503
507
 
504
- def add_role_hostgrps(self, role_id: int, hostgrp_ids: List[int], host_id: Optional[int] = None) -> Any:
508
+ def add_role_hostgrps(
509
+ self,
510
+ role_id: int,
511
+ hostgrp_ids: List[int],
512
+ kind: str = "B",
513
+ host_id: Optional[int] = None,
514
+ ) -> List[Any]:
515
+ """Add hostgrps to a role. Each assignment carries a `type`:
516
+ S = Submit (where the request comes from)
517
+ R = Run-as (where the command actually runs)
518
+
519
+ For a typical "users on these hosts can run on these same hosts" rule,
520
+ you usually want BOTH — pass `kind="B"` (default) and the client posts
521
+ twice, once with type S and once with type R.
522
+ """
505
523
  h = self.host(host_id)
506
- return self.post(f"/api/pbul/{h}/rbp/roles/{role_id}/hostgrps", json=hostgrp_ids)
524
+ path = f"/api/pbul/{h}/rbp/roles/{role_id}/hostgrps"
525
+ types = ("S", "R") if kind.upper() == "B" else (kind.upper(),)
526
+ if not all(t in ("S", "R") for t in types):
527
+ raise ValueError(f"hostgrp kind must be S, R, or B (both); got {kind!r}")
528
+ results = []
529
+ for hid in hostgrp_ids:
530
+ for t in types:
531
+ results.append(self.post(path, json={"hosts": hid, "type": t}))
532
+ return results
507
533
 
508
534
  def remove_role_hostgrp(self, role_id: int, hostgrp_id: int, host_id: Optional[int] = None) -> None:
509
535
  h = self.host(host_id)
@@ -513,9 +539,27 @@ class EPMLClient:
513
539
  h = self.host(host_id)
514
540
  return self.get(f"/api/pbul/{h}/rbp/roles/{role_id}/usergrps")
515
541
 
516
- def add_role_usergrps(self, role_id: int, usergrp_ids: List[int], host_id: Optional[int] = None) -> Any:
542
+ def add_role_usergrps(
543
+ self,
544
+ role_id: int,
545
+ usergrp_ids: List[int],
546
+ kind: str = "B",
547
+ host_id: Optional[int] = None,
548
+ ) -> List[Any]:
549
+ """Add usergrps to a role. Same S/R/B `type` semantics as hostgrps.
550
+ S = Submit user (who requests)
551
+ R = Run-as user (whose identity the command runs under)
552
+ """
517
553
  h = self.host(host_id)
518
- return self.post(f"/api/pbul/{h}/rbp/roles/{role_id}/usergrps", json=usergrp_ids)
554
+ path = f"/api/pbul/{h}/rbp/roles/{role_id}/usergrps"
555
+ types = ("S", "R") if kind.upper() == "B" else (kind.upper(),)
556
+ if not all(t in ("S", "R") for t in types):
557
+ raise ValueError(f"usergrp kind must be S, R, or B (both); got {kind!r}")
558
+ results = []
559
+ for uid in usergrp_ids:
560
+ for t in types:
561
+ results.append(self.post(path, json={"users": uid, "type": t}))
562
+ return results
519
563
 
520
564
  def remove_role_usergrp(self, role_id: int, usergrp_id: int, host_id: Optional[int] = None) -> None:
521
565
  h = self.host(host_id)
@@ -525,9 +569,11 @@ class EPMLClient:
525
569
  h = self.host(host_id)
526
570
  return self.get(f"/api/pbul/{h}/rbp/roles/{role_id}/tmdategrps")
527
571
 
528
- def add_role_tmdategrps(self, role_id: int, tmdategrp_ids: List[int], host_id: Optional[int] = None) -> Any:
572
+ def add_role_tmdategrps(self, role_id: int, tmdategrp_ids: List[int], host_id: Optional[int] = None) -> List[Any]:
573
+ """Add tmdategrps to a role. Single object per request, key is `tmdates`."""
529
574
  h = self.host(host_id)
530
- return self.post(f"/api/pbul/{h}/rbp/roles/{role_id}/tmdategrps", json=tmdategrp_ids)
575
+ path = f"/api/pbul/{h}/rbp/roles/{role_id}/tmdategrps"
576
+ return [self.post(path, json={"tmdates": tid}) for tid in tmdategrp_ids]
531
577
 
532
578
  def remove_role_tmdategrp(self, role_id: int, tmdategrp_id: int, host_id: Optional[int] = None) -> None:
533
579
  h = self.host(host_id)
@@ -46,20 +46,37 @@ def create_role(
46
46
  name: str = typer.Option(..., "--name", "-n"),
47
47
  description: str = typer.Option("", "--description", "-d"),
48
48
  action: str = typer.Option("A", "--action", "-a", help="Role verdict: A=Allow, R=Reject"),
49
+ iolog: Optional[str] = typer.Option(
50
+ None, "--iolog",
51
+ help="I/O log path template (e.g. /iologs/%date%/%uniqueid%.iolog). Omit to disable.",
52
+ ),
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"),
55
+ disabled: bool = typer.Option(False, "--disabled", help="Create the role disabled"),
49
56
  host: Optional[int] = _host_opt(),
50
57
  ):
51
58
  """Create a role.
52
59
 
53
60
  The API requires `action` to be exactly `A` (Allow) or `R` (Reject) —
54
61
  not 'Allow'/'Reject'. Defaults to A.
62
+
63
+ `iolog` accepts the appliance's path template syntax — typical value is
64
+ `/iologs/%date%/%uniqueid%.iolog`. Omit to disable I/O logging on this role.
55
65
  """
56
66
  from bt_cli.epml.client import get_client
57
67
  if action not in ("A", "R"):
58
68
  typer.echo(f"--action must be 'A' or 'R', got {action!r}", err=True)
59
69
  raise typer.Exit(2)
70
+ 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
+ if comment is not None:
76
+ body["comment"] = comment
60
77
  try:
61
78
  with get_client() as c:
62
- result = c.create_role({"name": name, "description": description, "action": action}, host_id=host)
79
+ result = c.create_role(body, host_id=host)
63
80
  print_json(result)
64
81
  except httpx.HTTPStatusError as e:
65
82
  print_api_error(e, "create role"); raise typer.Exit(1)
@@ -104,15 +121,20 @@ def duplicate_role(
104
121
 
105
122
  # ---- assignments: cmdgrps / hostgrps / usergrps / tmdategrps ----
106
123
 
107
- def _make_assignment_app(label: str, list_fn: str, add_fn: str, remove_fn: str):
108
- """Build a sub-typer for managing one role-child resource type."""
124
+ def _make_assignment_app(label: str, list_fn: str, add_fn: str, remove_fn: str, has_kind: bool = False):
125
+ """Build a sub-typer for managing one role-child resource type.
126
+
127
+ has_kind: if True, expose --kind S|R|B (Submit / Run-as / Both). Used for
128
+ hostgrps and usergrps where each assignment carries a type. Cmdgrps and
129
+ tmdategrps don't take a kind.
130
+ """
109
131
  sub = typer.Typer(no_args_is_help=True, help=f"Manage {label} on a role")
110
132
 
111
133
  @sub.command("list")
112
134
  def _list(
113
135
  role_id: int = typer.Argument(..., help="Role ID"),
114
136
  host: Optional[int] = _host_opt(),
115
- output: OutputFormat = typer.Option(OutputFormat.TABLE, "--output", "-o"),
137
+ output: OutputFormat = typer.Option(OutputFormat.JSON, "--output", "-o"),
116
138
  ):
117
139
  f"""List {label} assigned to a role."""
118
140
  from bt_cli.epml.client import get_client
@@ -123,29 +145,60 @@ def _make_assignment_app(label: str, list_fn: str, add_fn: str, remove_fn: str):
123
145
  print_json(data)
124
146
  else:
125
147
  rows = data if isinstance(data, list) else (data.get("data", []) if isinstance(data, dict) else [])
126
- print_table(rows, [("ID", "id"), ("Name", "name")], title=f"{label} on role {role_id}")
148
+ # Best-effort table: include `type` column when present
149
+ if rows and isinstance(rows[0], dict) and "type" in rows[0]:
150
+ cols = [(k.upper(), k) for k in rows[0].keys()]
151
+ else:
152
+ cols = [("ID", "id"), ("Name", "name")]
153
+ print_table(rows, cols, title=f"{label} on role {role_id}")
127
154
  except httpx.HTTPStatusError as e:
128
155
  print_api_error(e, f"list role {label}"); raise typer.Exit(1)
129
156
  except Exception as e:
130
157
  print_api_error(e, f"list role {label}"); raise typer.Exit(1)
131
158
 
132
- @sub.command("add")
133
- def _add(
134
- role_id: int = typer.Argument(..., help="Role ID"),
135
- ids: str = typer.Option(..., "--ids", help=f"Comma-separated {label} IDs to add"),
136
- host: Optional[int] = _host_opt(),
137
- ):
138
- f"""Add {label} to a role."""
139
- from bt_cli.epml.client import get_client
140
- try:
141
- id_list = [int(x.strip()) for x in ids.split(",") if x.strip()]
142
- with get_client() as c:
143
- result = getattr(c, add_fn)(role_id, id_list, host_id=host)
144
- print_json(result)
145
- except httpx.HTTPStatusError as e:
146
- print_api_error(e, f"add role {label}"); raise typer.Exit(1)
147
- except Exception as e:
148
- print_api_error(e, f"add role {label}"); raise typer.Exit(1)
159
+ if has_kind:
160
+ @sub.command("add")
161
+ def _add(
162
+ role_id: int = typer.Argument(..., help="Role ID"),
163
+ ids: str = typer.Option(..., "--ids", help=f"Comma-separated {label} IDs to add"),
164
+ kind: str = typer.Option(
165
+ "B", "--kind", "-k",
166
+ help="Assignment type: S=Submit, R=Run-as, B=Both (creates two assignments per id)",
167
+ ),
168
+ host: Optional[int] = _host_opt(),
169
+ ):
170
+ f"""Add {label} to a role."""
171
+ from bt_cli.epml.client import get_client
172
+ if kind.upper() not in ("S", "R", "B"):
173
+ typer.echo(f"--kind must be S, R, or B; got {kind!r}", err=True)
174
+ raise typer.Exit(2)
175
+ try:
176
+ id_list = [int(x.strip()) for x in ids.split(",") if x.strip()]
177
+ with get_client() as c:
178
+ result = getattr(c, add_fn)(role_id, id_list, kind=kind.upper(), host_id=host)
179
+ print_json(result)
180
+ except httpx.HTTPStatusError as e:
181
+ print_api_error(e, f"add role {label}"); raise typer.Exit(1)
182
+ except Exception as e:
183
+ print_api_error(e, f"add role {label}"); raise typer.Exit(1)
184
+ else:
185
+ @sub.command("add")
186
+ def _add(
187
+ role_id: int = typer.Argument(..., help="Role ID"),
188
+ ids: str = typer.Option(..., "--ids", help=f"Comma-separated {label} IDs to add"),
189
+ host: Optional[int] = _host_opt(),
190
+ ):
191
+ f"""Add {label} to a role."""
192
+ from bt_cli.epml.client import get_client
193
+ try:
194
+ id_list = [int(x.strip()) for x in ids.split(",") if x.strip()]
195
+ with get_client() as c:
196
+ result = getattr(c, add_fn)(role_id, id_list, host_id=host)
197
+ print_json(result)
198
+ except httpx.HTTPStatusError as e:
199
+ print_api_error(e, f"add role {label}"); raise typer.Exit(1)
200
+ except Exception as e:
201
+ print_api_error(e, f"add role {label}"); raise typer.Exit(1)
149
202
 
150
203
  @sub.command("remove")
151
204
  def _remove(
@@ -168,6 +221,6 @@ def _make_assignment_app(label: str, list_fn: str, add_fn: str, remove_fn: str):
168
221
 
169
222
 
170
223
  app.add_typer(_make_assignment_app("cmdgrps", "list_role_cmdgrps", "add_role_cmdgrps", "remove_role_cmdgrp"), name="cmdgrps")
171
- app.add_typer(_make_assignment_app("hostgrps", "list_role_hostgrps", "add_role_hostgrps", "remove_role_hostgrp"), name="hostgrps")
172
- app.add_typer(_make_assignment_app("usergrps", "list_role_usergrps", "add_role_usergrps", "remove_role_usergrp"), name="usergrps")
224
+ app.add_typer(_make_assignment_app("hostgrps", "list_role_hostgrps", "add_role_hostgrps", "remove_role_hostgrp", has_kind=True), name="hostgrps")
225
+ app.add_typer(_make_assignment_app("usergrps", "list_role_usergrps", "add_role_usergrps", "remove_role_usergrp", has_kind=True), name="usergrps")
173
226
  app.add_typer(_make_assignment_app("tmdategrps", "list_role_tmdategrps", "add_role_tmdategrps", "remove_role_tmdategrp"), name="tmdategrps")
@@ -95,7 +95,12 @@ def list_tmdates(
95
95
  host: Optional[int] = _host_opt(),
96
96
  output: OutputFormat = typer.Option(OutputFormat.JSON, "--output", "-o"),
97
97
  ):
98
- """List time/date entries in a group."""
98
+ """List time/date entries in a group.
99
+
100
+ Each entry's schedule lives in the nested `dotw` map. Defaults to JSON
101
+ output because the structure is non-tabular; use `-o table` for a
102
+ summarized day-by-day view.
103
+ """
99
104
  from bt_cli.epml.client import get_client
100
105
  try:
101
106
  with get_client() as c:
@@ -104,10 +109,26 @@ def list_tmdates(
104
109
  print_json(data)
105
110
  else:
106
111
  rows = data if isinstance(data, list) else (data.get("data", []) if isinstance(data, dict) else [])
107
- print_table(rows, [
108
- ("ID", "id"),
112
+ view = []
113
+ for r in rows:
114
+ dotw = (r.get("dotw") or {})
115
+ active_days = [d for d in ("mon","tue","wed","thu","fri","sat","sun") if dotw.get(d)]
116
+ # Show window summary using the first day's first window if any
117
+ window = ""
118
+ for d in active_days:
119
+ rng = (dotw.get(d) or [{}])[0]
120
+ if rng.get("from") and rng.get("to"):
121
+ window = f"{rng['from']}-{rng['to']}"
122
+ break
123
+ view.append({
124
+ "description": r.get("description", "") or r.get("name", "") or "-",
125
+ "days": ",".join(active_days) or "-",
126
+ "window": window or "-",
127
+ })
128
+ print_table(view, [
109
129
  ("Description", "description"),
110
- ("Disabled", "disabled"),
130
+ ("Days", "days"),
131
+ ("Window", "window"),
111
132
  ], title=f"tmdates in tmdategrp {tmdategrp_id}")
112
133
  except httpx.HTTPStatusError as e:
113
134
  print_api_error(e, "list tmdates"); raise typer.Exit(1)
@@ -115,18 +136,46 @@ def list_tmdates(
115
136
  print_api_error(e, "list tmdates"); raise typer.Exit(1)
116
137
 
117
138
 
139
+ _VALID_DAYS = ("mon", "tue", "wed", "thu", "fri", "sat", "sun")
140
+
141
+
142
+ def _build_dotw(from_: str, to: str, days: str) -> dict:
143
+ """Build a `dotw` map (day-of-the-week → list of from/to ranges).
144
+
145
+ The spec calls the per-tmdate field `tmdate: string` (a JSON definition)
146
+ but the actual API uses a structured object: `{"dotw": {"mon": [{"from":"...","to":"..."}], ...}}`.
147
+ Days that aren't in `--days` get an empty list (= not active).
148
+ """
149
+ selected = {d.strip().lower() for d in days.split(",") if d.strip()}
150
+ bad = selected - set(_VALID_DAYS)
151
+ if bad:
152
+ raise ValueError(f"unknown days: {','.join(sorted(bad))} — must be in {','.join(_VALID_DAYS)}")
153
+ window = {"from": from_, "to": to}
154
+ return {d: ([window] if d in selected else []) for d in _VALID_DAYS}
155
+
156
+
118
157
  @tmdates_app.command("add")
119
158
  def add_tmdates(
120
159
  tmdategrp_id: int = typer.Argument(..., help="Time/Date group ID"),
121
- file: Optional[typer.FileText] = typer.Option(None, "--file", "-f", help="JSON file: array of tmdate objects"),
160
+ file: Optional[typer.FileText] = typer.Option(None, "--file", "-f", help="JSON file: array of tmdate objects (full shape, with `dotw`)"),
122
161
  description: Optional[str] = typer.Option(None, "--description", "-d", help="Description for a single tmdate (used with --from/--to)"),
123
162
  from_: Optional[str] = typer.Option(None, "--from", help="Time window start (e.g. 09:00)"),
124
163
  to: Optional[str] = typer.Option(None, "--to", help="Time window end (e.g. 17:00)"),
164
+ days: str = typer.Option("mon,tue,wed,thu,fri", "--days", help="Comma-separated days when --from/--to applies"),
125
165
  host: Optional[int] = _host_opt(),
126
166
  ):
127
- """Add tmdate entries. Provide --file for full bodies, or --from/--to/--description for one quick entry.
167
+ """Add tmdate entries.
128
168
 
129
- The API wraps the body as `{"tmdates": [...]}` (the spec doesn't document this).
169
+ Two ways to specify entries:
170
+
171
+ - **--from / --to / --days**: quick one-window helper. Builds a per-day-of-week
172
+ schedule (`dotw` map). Default `--days mon,tue,wed,thu,fri` (weekdays).
173
+ - **--file**: a JSON array of full tmdate objects. Each object should be
174
+ `{"dotw": {"mon": [{"from":"09:00","to":"17:00"}], ...}}` — every weekday key
175
+ with a list of windows. Days you omit are silently treated as inactive.
176
+
177
+ The wrapper around the array (`{"tmdates": [...]}`) is added by the client
178
+ automatically — it's not in the spec but is required by the server.
130
179
  """
131
180
  from bt_cli.epml.client import get_client
132
181
  if file:
@@ -136,9 +185,13 @@ def add_tmdates(
136
185
  raise typer.Exit(2)
137
186
  else:
138
187
  if not (from_ and to):
139
- typer.echo("Provide --file, or both --from and --to (with optional --description)", err=True)
188
+ typer.echo("Provide --file, or both --from and --to (with optional --description / --days)", err=True)
189
+ raise typer.Exit(2)
190
+ try:
191
+ entry = {"dotw": _build_dotw(from_, to, days)}
192
+ except ValueError as e:
193
+ typer.echo(f"Bad --days: {e}", err=True)
140
194
  raise typer.Exit(2)
141
- entry = {"from": from_, "to": to}
142
195
  if description:
143
196
  entry["description"] = description
144
197
  tmdates = [entry]
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