bt-cli 0.4.25__tar.gz → 0.4.26__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 (174) hide show
  1. {bt_cli-0.4.25 → bt_cli-0.4.26}/CLAUDE.md +1 -1
  2. {bt_cli-0.4.25 → bt_cli-0.4.26}/PKG-INFO +1 -1
  3. {bt_cli-0.4.25 → bt_cli-0.4.26}/pyproject.toml +1 -1
  4. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/__init__.py +1 -1
  5. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/pra/client/base.py +49 -5
  6. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/pra/commands/jump_items.py +88 -18
  7. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/pws/client/beyondinsight.py +43 -0
  8. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/pws/commands/__init__.py +2 -1
  9. bt_cli-0.4.26/src/bt_cli/pws/commands/attributes.py +176 -0
  10. {bt_cli-0.4.25 → bt_cli-0.4.26}/.claude/skills/bt/SKILL.md +0 -0
  11. {bt_cli-0.4.25 → bt_cli-0.4.26}/.claude/skills/entitle/SKILL.md +0 -0
  12. {bt_cli-0.4.25 → bt_cli-0.4.26}/.claude/skills/epmw/SKILL.md +0 -0
  13. {bt_cli-0.4.25 → bt_cli-0.4.26}/.claude/skills/pra/SKILL.md +0 -0
  14. {bt_cli-0.4.25 → bt_cli-0.4.26}/.claude/skills/pws/SKILL.md +0 -0
  15. {bt_cli-0.4.25 → bt_cli-0.4.26}/.env.example +0 -0
  16. {bt_cli-0.4.25 → bt_cli-0.4.26}/.github/workflows/ci.yml +0 -0
  17. {bt_cli-0.4.25 → bt_cli-0.4.26}/.github/workflows/release.yml +0 -0
  18. {bt_cli-0.4.25 → bt_cli-0.4.26}/.gitignore +0 -0
  19. {bt_cli-0.4.25 → bt_cli-0.4.26}/README.md +0 -0
  20. {bt_cli-0.4.25 → bt_cli-0.4.26}/assets/cli-help.png +0 -0
  21. {bt_cli-0.4.25 → bt_cli-0.4.26}/assets/cli-output.png +0 -0
  22. {bt_cli-0.4.25 → bt_cli-0.4.26}/bt-cli.spec +0 -0
  23. {bt_cli-0.4.25 → bt_cli-0.4.26}/bt_entry.py +0 -0
  24. {bt_cli-0.4.25 → bt_cli-0.4.26}/scripts/bt_entry.py +0 -0
  25. {bt_cli-0.4.25 → bt_cli-0.4.26}/scripts/sync-package-data.sh +0 -0
  26. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/cli.py +0 -0
  27. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/commands/__init__.py +0 -0
  28. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/commands/configure.py +0 -0
  29. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/commands/learn.py +0 -0
  30. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/commands/quick.py +0 -0
  31. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/core/__init__.py +0 -0
  32. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/core/auth.py +0 -0
  33. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/core/client.py +0 -0
  34. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/core/config.py +0 -0
  35. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/core/config_file.py +0 -0
  36. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/core/csv_utils.py +0 -0
  37. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/core/errors.py +0 -0
  38. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/core/output.py +0 -0
  39. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/core/prompts.py +0 -0
  40. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/core/rest_debug.py +0 -0
  41. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/data/CLAUDE.md +0 -0
  42. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/data/__init__.py +0 -0
  43. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/data/skills/bt/SKILL.md +0 -0
  44. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/data/skills/entitle/SKILL.md +0 -0
  45. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/data/skills/epmw/SKILL.md +0 -0
  46. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/data/skills/pra/SKILL.md +0 -0
  47. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/data/skills/pws/SKILL.md +0 -0
  48. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/entitle/__init__.py +0 -0
  49. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/entitle/client/__init__.py +0 -0
  50. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/entitle/client/base.py +0 -0
  51. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/entitle/commands/__init__.py +0 -0
  52. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/entitle/commands/accounts.py +0 -0
  53. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/entitle/commands/applications.py +0 -0
  54. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/entitle/commands/auth.py +0 -0
  55. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/entitle/commands/bundles.py +0 -0
  56. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/entitle/commands/integrations.py +0 -0
  57. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/entitle/commands/permissions.py +0 -0
  58. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/entitle/commands/policies.py +0 -0
  59. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/entitle/commands/resources.py +0 -0
  60. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/entitle/commands/roles.py +0 -0
  61. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/entitle/commands/users.py +0 -0
  62. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/entitle/commands/workflows.py +0 -0
  63. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/entitle/models/__init__.py +0 -0
  64. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/entitle/models/bundle.py +0 -0
  65. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/entitle/models/common.py +0 -0
  66. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/entitle/models/integration.py +0 -0
  67. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/entitle/models/permission.py +0 -0
  68. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/entitle/models/policy.py +0 -0
  69. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/entitle/models/resource.py +0 -0
  70. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/entitle/models/role.py +0 -0
  71. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/entitle/models/user.py +0 -0
  72. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/entitle/models/workflow.py +0 -0
  73. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/epmw/__init__.py +0 -0
  74. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/epmw/client/__init__.py +0 -0
  75. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/epmw/client/base.py +0 -0
  76. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/epmw/commands/__init__.py +0 -0
  77. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/epmw/commands/audits.py +0 -0
  78. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/epmw/commands/auth.py +0 -0
  79. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/epmw/commands/computers.py +0 -0
  80. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/epmw/commands/events.py +0 -0
  81. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/epmw/commands/groups.py +0 -0
  82. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/epmw/commands/policies.py +0 -0
  83. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/epmw/commands/quick.py +0 -0
  84. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/epmw/commands/requests.py +0 -0
  85. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/epmw/commands/roles.py +0 -0
  86. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/epmw/commands/tasks.py +0 -0
  87. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/epmw/commands/users.py +0 -0
  88. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/epmw/models/__init__.py +0 -0
  89. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/pra/__init__.py +0 -0
  90. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/pra/client/__init__.py +0 -0
  91. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/pra/commands/__init__.py +0 -0
  92. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/pra/commands/auth.py +0 -0
  93. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/pra/commands/import_export.py +0 -0
  94. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/pra/commands/jump_clients.py +0 -0
  95. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/pra/commands/jump_groups.py +0 -0
  96. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/pra/commands/jumpoints.py +0 -0
  97. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/pra/commands/policies.py +0 -0
  98. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/pra/commands/quick.py +0 -0
  99. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/pra/commands/teams.py +0 -0
  100. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/pra/commands/users.py +0 -0
  101. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/pra/commands/vault.py +0 -0
  102. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/pra/models/__init__.py +0 -0
  103. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/pra/models/common.py +0 -0
  104. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/pra/models/jump_client.py +0 -0
  105. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/pra/models/jump_group.py +0 -0
  106. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/pra/models/jump_item.py +0 -0
  107. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/pra/models/jumpoint.py +0 -0
  108. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/pra/models/team.py +0 -0
  109. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/pra/models/user.py +0 -0
  110. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/pra/models/vault.py +0 -0
  111. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/pws/__init__.py +0 -0
  112. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/pws/client/__init__.py +0 -0
  113. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/pws/client/base.py +0 -0
  114. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/pws/client/passwordsafe.py +0 -0
  115. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/pws/commands/accounts.py +0 -0
  116. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/pws/commands/assets.py +0 -0
  117. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/pws/commands/auth.py +0 -0
  118. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/pws/commands/clouds.py +0 -0
  119. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/pws/commands/config.py +0 -0
  120. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/pws/commands/credentials.py +0 -0
  121. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/pws/commands/databases.py +0 -0
  122. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/pws/commands/directories.py +0 -0
  123. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/pws/commands/functional.py +0 -0
  124. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/pws/commands/import_export.py +0 -0
  125. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/pws/commands/platforms.py +0 -0
  126. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/pws/commands/quick.py +0 -0
  127. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/pws/commands/search.py +0 -0
  128. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/pws/commands/secrets.py +0 -0
  129. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/pws/commands/systems.py +0 -0
  130. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/pws/commands/users.py +0 -0
  131. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/pws/commands/workgroups.py +0 -0
  132. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/pws/config.py +0 -0
  133. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/pws/models/__init__.py +0 -0
  134. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/pws/models/account.py +0 -0
  135. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/pws/models/asset.py +0 -0
  136. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/pws/models/common.py +0 -0
  137. {bt_cli-0.4.25 → bt_cli-0.4.26}/src/bt_cli/pws/models/system.py +0 -0
  138. {bt_cli-0.4.25 → bt_cli-0.4.26}/tests/__init__.py +0 -0
  139. {bt_cli-0.4.25 → bt_cli-0.4.26}/tests/conftest.py +0 -0
  140. {bt_cli-0.4.25 → bt_cli-0.4.26}/tests/core/__init__.py +0 -0
  141. {bt_cli-0.4.25 → bt_cli-0.4.26}/tests/core/test_auth.py +0 -0
  142. {bt_cli-0.4.25 → bt_cli-0.4.26}/tests/core/test_config.py +0 -0
  143. {bt_cli-0.4.25 → bt_cli-0.4.26}/tests/core/test_errors.py +0 -0
  144. {bt_cli-0.4.25 → bt_cli-0.4.26}/tests/core/test_rest_debug.py +0 -0
  145. {bt_cli-0.4.25 → bt_cli-0.4.26}/tests/entitle/__init__.py +0 -0
  146. {bt_cli-0.4.25 → bt_cli-0.4.26}/tests/entitle/test_client.py +0 -0
  147. {bt_cli-0.4.25 → bt_cli-0.4.26}/tests/entitle/test_commands.py +0 -0
  148. {bt_cli-0.4.25 → bt_cli-0.4.26}/tests/entitle-smoke-test.sh +0 -0
  149. {bt_cli-0.4.25 → bt_cli-0.4.26}/tests/epmw/__init__.py +0 -0
  150. {bt_cli-0.4.25 → bt_cli-0.4.26}/tests/epmw/test_client.py +0 -0
  151. {bt_cli-0.4.25 → bt_cli-0.4.26}/tests/epmw/test_commands.py +0 -0
  152. {bt_cli-0.4.25 → bt_cli-0.4.26}/tests/epmw-quick-test-plan.md +0 -0
  153. {bt_cli-0.4.25 → bt_cli-0.4.26}/tests/fixtures/__init__.py +0 -0
  154. {bt_cli-0.4.25 → bt_cli-0.4.26}/tests/fixtures/responses.py +0 -0
  155. {bt_cli-0.4.25 → bt_cli-0.4.26}/tests/integration/__init__.py +0 -0
  156. {bt_cli-0.4.25 → bt_cli-0.4.26}/tests/integration/conftest.py +0 -0
  157. {bt_cli-0.4.25 → bt_cli-0.4.26}/tests/integration/helpers.py +0 -0
  158. {bt_cli-0.4.25 → bt_cli-0.4.26}/tests/integration/test_entitle_integration.py +0 -0
  159. {bt_cli-0.4.25 → bt_cli-0.4.26}/tests/integration/test_epmw_integration.py +0 -0
  160. {bt_cli-0.4.25 → bt_cli-0.4.26}/tests/integration/test_epmw_lifecycle.py +0 -0
  161. {bt_cli-0.4.25 → bt_cli-0.4.26}/tests/integration/test_pra_integration.py +0 -0
  162. {bt_cli-0.4.25 → bt_cli-0.4.26}/tests/integration/test_pra_lifecycle.py +0 -0
  163. {bt_cli-0.4.25 → bt_cli-0.4.26}/tests/integration/test_pws_integration.py +0 -0
  164. {bt_cli-0.4.25 → bt_cli-0.4.26}/tests/integration/test_pws_lifecycle.py +0 -0
  165. {bt_cli-0.4.25 → bt_cli-0.4.26}/tests/pra/__init__.py +0 -0
  166. {bt_cli-0.4.25 → bt_cli-0.4.26}/tests/pra/test_client.py +0 -0
  167. {bt_cli-0.4.25 → bt_cli-0.4.26}/tests/pra/test_commands.py +0 -0
  168. {bt_cli-0.4.25 → bt_cli-0.4.26}/tests/pra-smoke-test.sh +0 -0
  169. {bt_cli-0.4.25 → bt_cli-0.4.26}/tests/pra-test-plan.md +0 -0
  170. {bt_cli-0.4.25 → bt_cli-0.4.26}/tests/pws/__init__.py +0 -0
  171. {bt_cli-0.4.25 → bt_cli-0.4.26}/tests/pws/test_client.py +0 -0
  172. {bt_cli-0.4.25 → bt_cli-0.4.26}/tests/pws/test_commands.py +0 -0
  173. {bt_cli-0.4.25 → bt_cli-0.4.26}/tests/pws-quick-test-plan.md +0 -0
  174. {bt_cli-0.4.25 → bt_cli-0.4.26}/tests/pws-smoke-test.sh +0 -0
@@ -1,6 +1,6 @@
1
1
  # BT-CLI
2
2
 
3
- BeyondTrust Platform CLI for Password Safe, Entitle, PRA, and EPM Windows. **Version: 0.4.25**
3
+ BeyondTrust Platform CLI for Password Safe, Entitle, PRA, and EPM Windows. **Version: 0.4.26**
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.25
3
+ Version: 0.4.26
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.25"
7
+ version = "0.4.26"
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.25"
3
+ __version__ = "0.4.26"
@@ -278,13 +278,22 @@ class PRAClient:
278
278
  self,
279
279
  jump_group_id: Optional[int] = None,
280
280
  jumpoint_id: Optional[int] = None,
281
+ tag: Optional[str] = None,
282
+ name: Optional[str] = None,
283
+ hostname: Optional[str] = None,
281
284
  ) -> List[Dict[str, Any]]:
282
- """List Shell Jump items."""
285
+ """List Shell Jump items with optional filters."""
283
286
  params = {}
284
287
  if jump_group_id:
285
288
  params["jump_group_id"] = jump_group_id
286
289
  if jumpoint_id:
287
290
  params["jumpoint_id"] = jumpoint_id
291
+ if tag:
292
+ params["tag"] = tag
293
+ if name:
294
+ params["name"] = name
295
+ if hostname:
296
+ params["hostname"] = hostname
288
297
  return self.get_paginated("/jump-item/shell-jump", params)
289
298
 
290
299
  def get_shell_jump(self, item_id: int) -> Dict[str, Any]:
@@ -375,13 +384,22 @@ class PRAClient:
375
384
  self,
376
385
  jump_group_id: Optional[int] = None,
377
386
  jumpoint_id: Optional[int] = None,
387
+ tag: Optional[str] = None,
388
+ name: Optional[str] = None,
389
+ hostname: Optional[str] = None,
378
390
  ) -> List[Dict[str, Any]]:
379
- """List Remote RDP Jump items."""
391
+ """List Remote RDP Jump items with optional filters."""
380
392
  params = {}
381
393
  if jump_group_id:
382
394
  params["jump_group_id"] = jump_group_id
383
395
  if jumpoint_id:
384
396
  params["jumpoint_id"] = jumpoint_id
397
+ if tag:
398
+ params["tag"] = tag
399
+ if name:
400
+ params["name"] = name
401
+ if hostname:
402
+ params["hostname"] = hostname
385
403
  return self.get_paginated("/jump-item/remote-rdp", params)
386
404
 
387
405
  def get_rdp_jump(self, item_id: int) -> Dict[str, Any]:
@@ -423,21 +441,36 @@ class PRAClient:
423
441
  def list_vnc_jumps(
424
442
  self,
425
443
  jump_group_id: Optional[int] = None,
444
+ tag: Optional[str] = None,
445
+ name: Optional[str] = None,
446
+ hostname: Optional[str] = None,
426
447
  ) -> List[Dict[str, Any]]:
427
- """List Remote VNC Jump items."""
448
+ """List Remote VNC Jump items with optional filters."""
428
449
  params = {}
429
450
  if jump_group_id:
430
451
  params["jump_group_id"] = jump_group_id
452
+ if tag:
453
+ params["tag"] = tag
454
+ if name:
455
+ params["name"] = name
456
+ if hostname:
457
+ params["hostname"] = hostname
431
458
  return self.get_paginated("/jump-item/remote-vnc", params)
432
459
 
433
460
  def list_web_jumps(
434
461
  self,
435
462
  jump_group_id: Optional[int] = None,
463
+ tag: Optional[str] = None,
464
+ name: Optional[str] = None,
436
465
  ) -> List[Dict[str, Any]]:
437
- """List Web Jump items."""
466
+ """List Web Jump items with optional filters."""
438
467
  params = {}
439
468
  if jump_group_id:
440
469
  params["jump_group_id"] = jump_group_id
470
+ if tag:
471
+ params["tag"] = tag
472
+ if name:
473
+ params["name"] = name
441
474
  return self.get_paginated("/jump-item/web-jump", params)
442
475
 
443
476
  def get_web_jump(self, item_id: int) -> Dict[str, Any]:
@@ -452,11 +485,22 @@ class PRAClient:
452
485
  self,
453
486
  jump_group_id: Optional[int] = None,
454
487
  tunnel_type: Optional[str] = None,
488
+ tag: Optional[str] = None,
489
+ name: Optional[str] = None,
490
+ hostname: Optional[str] = None,
455
491
  ) -> List[Dict[str, Any]]:
456
- """List Protocol Tunnel Jump items (TCP, MSSQL, K8s)."""
492
+ """List Protocol Tunnel Jump items (TCP, MSSQL, K8s) with optional filters."""
457
493
  params = {}
458
494
  if jump_group_id:
459
495
  params["jump_group_id"] = jump_group_id
496
+ if tunnel_type:
497
+ params["tunnel_type"] = tunnel_type
498
+ if tag:
499
+ params["tag"] = tag
500
+ if name:
501
+ params["name"] = name
502
+ if hostname:
503
+ params["hostname"] = hostname
460
504
  return self.get_paginated("/jump-item/protocol-tunnel-jump", params)
461
505
 
462
506
  def get_protocol_tunnel(self, item_id: int) -> Dict[str, Any]:
@@ -18,6 +18,9 @@ app.add_typer(shell_app, name="shell")
18
18
  def list_shell_jumps(
19
19
  jump_group_id: Optional[int] = typer.Option(None, "--jump-group", "-g", help="Filter by Jump Group"),
20
20
  jumpoint_id: Optional[int] = typer.Option(None, "--jumpoint", "-j", help="Filter by Jumpoint"),
21
+ tag: Optional[str] = typer.Option(None, "--tag", "-t", help="Filter by tag (exact match)"),
22
+ name: Optional[str] = typer.Option(None, "--name", "-n", help="Filter by name (exact match)"),
23
+ hostname: Optional[str] = typer.Option(None, "--hostname", help="Filter by hostname (exact match)"),
21
24
  output: OutputFormat = typer.Option(OutputFormat.TABLE, "--output", "-o"),
22
25
  ):
23
26
  """List Shell Jump items."""
@@ -25,7 +28,13 @@ def list_shell_jumps(
25
28
 
26
29
  try:
27
30
  client = get_client()
28
- items = client.list_shell_jumps(jump_group_id=jump_group_id, jumpoint_id=jumpoint_id)
31
+ items = client.list_shell_jumps(
32
+ jump_group_id=jump_group_id,
33
+ jumpoint_id=jumpoint_id,
34
+ tag=tag,
35
+ name=name,
36
+ hostname=hostname,
37
+ )
29
38
 
30
39
  if output == OutputFormat.JSON:
31
40
  print_json(items)
@@ -199,15 +208,23 @@ def update_shell_jump(
199
208
  def delete_shell_jump(
200
209
  item_id: int = typer.Argument(..., help="Shell Jump ID"),
201
210
  force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
211
+ dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be deleted without deleting"),
202
212
  ):
203
213
  """Delete a Shell Jump item."""
204
214
  from bt_cli.pra.client import get_client
205
215
 
206
- if not force:
207
- typer.confirm(f"Delete shell jump {item_id}?", abort=True)
208
-
209
216
  try:
210
217
  client = get_client()
218
+ # Fetch item details for display
219
+ if dry_run:
220
+ item = client.get_shell_jump(item_id)
221
+ print_success(f"[DRY-RUN] Would delete shell jump: {item.get('name')} (ID: {item_id})")
222
+ return
223
+
224
+ if not force:
225
+ item = client.get_shell_jump(item_id)
226
+ typer.confirm(f"Delete shell jump '{item.get('name')}' (ID: {item_id})?", abort=True)
227
+
211
228
  client.delete_shell_jump(item_id)
212
229
  print_success(f"Deleted shell jump {item_id}")
213
230
  except httpx.HTTPStatusError as e:
@@ -230,6 +247,9 @@ app.add_typer(rdp_app, name="rdp")
230
247
  def list_rdp_jumps(
231
248
  jump_group_id: Optional[int] = typer.Option(None, "--jump-group", "-g", help="Filter by Jump Group"),
232
249
  jumpoint_id: Optional[int] = typer.Option(None, "--jumpoint", "-j", help="Filter by Jumpoint"),
250
+ tag: Optional[str] = typer.Option(None, "--tag", "-t", help="Filter by tag (exact match)"),
251
+ name: Optional[str] = typer.Option(None, "--name", "-n", help="Filter by name (exact match)"),
252
+ hostname: Optional[str] = typer.Option(None, "--hostname", help="Filter by hostname (exact match)"),
233
253
  output: OutputFormat = typer.Option(OutputFormat.TABLE, "--output", "-o"),
234
254
  ):
235
255
  """List Remote RDP Jump items."""
@@ -237,7 +257,13 @@ def list_rdp_jumps(
237
257
 
238
258
  try:
239
259
  client = get_client()
240
- items = client.list_rdp_jumps(jump_group_id=jump_group_id, jumpoint_id=jumpoint_id)
260
+ items = client.list_rdp_jumps(
261
+ jump_group_id=jump_group_id,
262
+ jumpoint_id=jumpoint_id,
263
+ tag=tag,
264
+ name=name,
265
+ hostname=hostname,
266
+ )
241
267
 
242
268
  if output == OutputFormat.JSON:
243
269
  print_json(items)
@@ -358,15 +384,22 @@ def create_rdp_jump(
358
384
  def delete_rdp_jump(
359
385
  item_id: int = typer.Argument(..., help="RDP Jump ID"),
360
386
  force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
387
+ dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be deleted without deleting"),
361
388
  ):
362
389
  """Delete a Remote RDP Jump item."""
363
390
  from bt_cli.pra.client import get_client
364
391
 
365
- if not force:
366
- typer.confirm(f"Delete RDP jump {item_id}?", abort=True)
367
-
368
392
  try:
369
393
  client = get_client()
394
+ if dry_run:
395
+ item = client.get_rdp_jump(item_id)
396
+ print_success(f"[DRY-RUN] Would delete RDP jump: {item.get('name')} (ID: {item_id})")
397
+ return
398
+
399
+ if not force:
400
+ item = client.get_rdp_jump(item_id)
401
+ typer.confirm(f"Delete RDP jump '{item.get('name')}' (ID: {item_id})?", abort=True)
402
+
370
403
  client.delete_rdp_jump(item_id)
371
404
  print_success(f"Deleted RDP jump {item_id}")
372
405
  except httpx.HTTPStatusError as e:
@@ -388,6 +421,9 @@ app.add_typer(vnc_app, name="vnc")
388
421
  @vnc_app.command("list")
389
422
  def list_vnc_jumps(
390
423
  jump_group_id: Optional[int] = typer.Option(None, "--jump-group", "-g", help="Filter by Jump Group"),
424
+ tag: Optional[str] = typer.Option(None, "--tag", "-t", help="Filter by tag (exact match)"),
425
+ name: Optional[str] = typer.Option(None, "--name", "-n", help="Filter by name (exact match)"),
426
+ hostname: Optional[str] = typer.Option(None, "--hostname", help="Filter by hostname (exact match)"),
391
427
  output: OutputFormat = typer.Option(OutputFormat.TABLE, "--output", "-o"),
392
428
  ):
393
429
  """List Remote VNC Jump items."""
@@ -395,7 +431,12 @@ def list_vnc_jumps(
395
431
 
396
432
  try:
397
433
  client = get_client()
398
- items = client.list_vnc_jumps(jump_group_id=jump_group_id)
434
+ items = client.list_vnc_jumps(
435
+ jump_group_id=jump_group_id,
436
+ tag=tag,
437
+ name=name,
438
+ hostname=hostname,
439
+ )
399
440
 
400
441
  if output == OutputFormat.JSON:
401
442
  print_json(items)
@@ -427,6 +468,8 @@ app.add_typer(web_app, name="web")
427
468
  @web_app.command("list")
428
469
  def list_web_jumps(
429
470
  jump_group_id: Optional[int] = typer.Option(None, "--jump-group", "-g", help="Filter by Jump Group"),
471
+ tag: Optional[str] = typer.Option(None, "--tag", "-t", help="Filter by tag (exact match)"),
472
+ name: Optional[str] = typer.Option(None, "--name", "-n", help="Filter by name (exact match)"),
430
473
  output: OutputFormat = typer.Option(OutputFormat.TABLE, "--output", "-o"),
431
474
  ):
432
475
  """List Web Jump items."""
@@ -434,7 +477,11 @@ def list_web_jumps(
434
477
 
435
478
  try:
436
479
  client = get_client()
437
- items = client.list_web_jumps(jump_group_id=jump_group_id)
480
+ items = client.list_web_jumps(
481
+ jump_group_id=jump_group_id,
482
+ tag=tag,
483
+ name=name,
484
+ )
438
485
 
439
486
  if output == OutputFormat.JSON:
440
487
  print_json(items)
@@ -500,15 +547,22 @@ def get_web_jump(
500
547
  def delete_web_jump(
501
548
  item_id: int = typer.Argument(..., help="Web Jump ID"),
502
549
  force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
550
+ dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be deleted without deleting"),
503
551
  ):
504
552
  """Delete a Web Jump item."""
505
553
  from bt_cli.pra.client import get_client
506
554
 
507
- if not force:
508
- typer.confirm(f"Delete web jump {item_id}?", abort=True)
509
-
510
555
  try:
511
556
  client = get_client()
557
+ if dry_run:
558
+ item = client.get_web_jump(item_id)
559
+ print_success(f"[DRY-RUN] Would delete web jump: {item.get('name')} (ID: {item_id})")
560
+ return
561
+
562
+ if not force:
563
+ item = client.get_web_jump(item_id)
564
+ typer.confirm(f"Delete web jump '{item.get('name')}' (ID: {item_id})?", abort=True)
565
+
512
566
  client.delete_web_jump(item_id)
513
567
  print_success(f"Deleted web jump {item_id}")
514
568
  except httpx.HTTPStatusError as e:
@@ -530,7 +584,10 @@ app.add_typer(tunnel_app, name="tunnel")
530
584
  @tunnel_app.command("list")
531
585
  def list_protocol_tunnels(
532
586
  jump_group_id: Optional[int] = typer.Option(None, "--jump-group", "-g", help="Filter by Jump Group"),
533
- tunnel_type: Optional[str] = typer.Option(None, "--type", "-t", help="Filter by type: tcp, mssql, psql, mysql, k8s"),
587
+ tunnel_type: Optional[str] = typer.Option(None, "--type", help="Filter by type: tcp, mssql, psql, mysql, k8s"),
588
+ tag: Optional[str] = typer.Option(None, "--tag", "-t", help="Filter by tag (exact match)"),
589
+ name: Optional[str] = typer.Option(None, "--name", "-n", help="Filter by name (exact match)"),
590
+ hostname: Optional[str] = typer.Option(None, "--hostname", help="Filter by hostname (exact match)"),
534
591
  output: OutputFormat = typer.Option(OutputFormat.TABLE, "--output", "-o"),
535
592
  ):
536
593
  """List Protocol Tunnel Jump items."""
@@ -538,7 +595,13 @@ def list_protocol_tunnels(
538
595
 
539
596
  try:
540
597
  client = get_client()
541
- items = client.list_protocol_tunnels(jump_group_id=jump_group_id, tunnel_type=tunnel_type)
598
+ items = client.list_protocol_tunnels(
599
+ jump_group_id=jump_group_id,
600
+ tunnel_type=tunnel_type,
601
+ tag=tag,
602
+ name=name,
603
+ hostname=hostname,
604
+ )
542
605
 
543
606
  if output == OutputFormat.JSON:
544
607
  print_json(items)
@@ -680,15 +743,22 @@ def create_protocol_tunnel(
680
743
  def delete_protocol_tunnel(
681
744
  item_id: int = typer.Argument(..., help="Protocol Tunnel ID"),
682
745
  force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
746
+ dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be deleted without deleting"),
683
747
  ):
684
748
  """Delete a Protocol Tunnel Jump item."""
685
749
  from bt_cli.pra.client import get_client
686
750
 
687
- if not force:
688
- typer.confirm(f"Delete protocol tunnel {item_id}?", abort=True)
689
-
690
751
  try:
691
752
  client = get_client()
753
+ if dry_run:
754
+ item = client.get_protocol_tunnel(item_id)
755
+ print_success(f"[DRY-RUN] Would delete protocol tunnel: {item.get('name')} (ID: {item_id})")
756
+ return
757
+
758
+ if not force:
759
+ item = client.get_protocol_tunnel(item_id)
760
+ typer.confirm(f"Delete protocol tunnel '{item.get('name')}' (ID: {item_id})?", abort=True)
761
+
692
762
  client.delete_protocol_tunnel(item_id)
693
763
  print_success(f"Deleted protocol tunnel {item_id}")
694
764
  except httpx.HTTPStatusError as e:
@@ -867,3 +867,46 @@ class BeyondInsightMixin:
867
867
  Attribute object
868
868
  """
869
869
  return self.get(f"/Attributes/{attribute_id}")
870
+
871
+ def get_managed_system_attributes(
872
+ self: "PasswordSafeClient",
873
+ managed_system_id: int,
874
+ ) -> list[dict[str, Any]]:
875
+ """Get attributes for a managed system.
876
+
877
+ Args:
878
+ managed_system_id: Managed system ID
879
+
880
+ Returns:
881
+ List of attribute objects assigned to this system
882
+ """
883
+ return self.get(f"/ManagedSystems/{managed_system_id}/Attributes")
884
+
885
+ def assign_managed_system_attribute(
886
+ self: "PasswordSafeClient",
887
+ managed_system_id: int,
888
+ attribute_id: int,
889
+ ) -> dict[str, Any]:
890
+ """Assign an attribute to a managed system.
891
+
892
+ Args:
893
+ managed_system_id: Managed system ID
894
+ attribute_id: Attribute ID to assign
895
+
896
+ Returns:
897
+ The created assignment
898
+ """
899
+ return self.post(f"/ManagedSystems/{managed_system_id}/Attributes/{attribute_id}")
900
+
901
+ def remove_managed_system_attribute(
902
+ self: "PasswordSafeClient",
903
+ managed_system_id: int,
904
+ attribute_id: int,
905
+ ) -> None:
906
+ """Remove an attribute from a managed system.
907
+
908
+ Args:
909
+ managed_system_id: Managed system ID
910
+ attribute_id: Attribute ID to remove
911
+ """
912
+ self.delete(f"/ManagedSystems/{managed_system_id}/Attributes/{attribute_id}")
@@ -10,7 +10,7 @@ app = typer.Typer(
10
10
 
11
11
  # Import and register command groups
12
12
  from . import auth, systems, accounts, assets, workgroups, platforms, users
13
- from . import credentials, config, databases, directories, clouds, secrets, quick, functional, search
13
+ from . import credentials, config, databases, directories, clouds, secrets, quick, functional, search, attributes
14
14
  from .import_export import import_app, export_app
15
15
 
16
16
  app.add_typer(auth.app, name="auth", help="Authentication commands")
@@ -29,5 +29,6 @@ app.add_typer(secrets.app, name="secrets", help="Secrets Safe (folders and secre
29
29
  app.add_typer(functional.app, name="functional", help="Functional accounts for auto-management")
30
30
  app.add_typer(users.app, name="users", help="Manage users, user groups, and roles")
31
31
  app.add_typer(quick.app, name="quick", help="Quick commands - common multi-step operations")
32
+ app.add_typer(attributes.app, name="attributes", help="Manage attributes for systems/accounts")
32
33
  app.add_typer(import_app, name="import", help="Import resources from CSV")
33
34
  app.add_typer(export_app, name="export", help="Export sample CSV templates")
@@ -0,0 +1,176 @@
1
+ """Attribute commands for Password Safe."""
2
+
3
+ from typing import Optional
4
+
5
+ import httpx
6
+ import typer
7
+
8
+ from bt_cli.core.output import OutputFormat, print_table, print_json, print_error, print_success, print_api_error
9
+
10
+ app = typer.Typer(no_args_is_help=True, help="Manage attributes")
11
+
12
+
13
+ @app.command("list")
14
+ def list_attributes(
15
+ attribute_type: Optional[str] = typer.Option(
16
+ None, "--type", "-t",
17
+ help="Filter by type: Asset, ManagedSystem, ManagedAccount"
18
+ ),
19
+ output: OutputFormat = typer.Option(OutputFormat.TABLE, "--output", "-o"),
20
+ ):
21
+ """List all defined attributes."""
22
+ from bt_cli.pws.client import get_client
23
+
24
+ try:
25
+ with get_client() as client:
26
+ attributes = client.list_attributes(attribute_type=attribute_type)
27
+
28
+ if output == OutputFormat.JSON:
29
+ print_json(attributes)
30
+ else:
31
+ columns = [
32
+ ("ID", "AttributeID"),
33
+ ("Name", "AttributeName"),
34
+ ("Type", "AttributeType"),
35
+ ("Description", "Description"),
36
+ ]
37
+ print_table(attributes, columns, title="Attributes")
38
+ except httpx.HTTPStatusError as e:
39
+ print_api_error(e, "list attributes")
40
+ raise typer.Exit(1)
41
+ except httpx.RequestError as e:
42
+ print_api_error(e, "list attributes")
43
+ raise typer.Exit(1)
44
+ except Exception as e:
45
+ print_api_error(e, "list attributes")
46
+ raise typer.Exit(1)
47
+
48
+
49
+ @app.command("get")
50
+ def get_attribute(
51
+ attribute_id: int = typer.Argument(..., help="Attribute ID"),
52
+ output: OutputFormat = typer.Option(OutputFormat.TABLE, "--output", "-o"),
53
+ ):
54
+ """Get attribute details."""
55
+ from bt_cli.pws.client import get_client
56
+ from rich.console import Console
57
+ from rich.panel import Panel
58
+
59
+ console = Console()
60
+
61
+ try:
62
+ with get_client() as client:
63
+ attr = client.get_attribute(attribute_id)
64
+
65
+ if output == OutputFormat.JSON:
66
+ print_json(attr)
67
+ else:
68
+ name = attr.get("AttributeName", "")
69
+ attr_type = attr.get("AttributeType", "")
70
+ description = attr.get("Description", "") or "-"
71
+
72
+ console.print(Panel(
73
+ f"[bold]{name}[/bold]\n\n"
74
+ f"[dim]Type:[/dim] {attr_type}\n"
75
+ f"[dim]Description:[/dim] {description}",
76
+ title="Attribute Details",
77
+ subtitle=f"ID: {attr.get('AttributeID', '')}",
78
+ ))
79
+ except httpx.HTTPStatusError as e:
80
+ print_api_error(e, "get attribute")
81
+ raise typer.Exit(1)
82
+ except httpx.RequestError as e:
83
+ print_api_error(e, "get attribute")
84
+ raise typer.Exit(1)
85
+ except Exception as e:
86
+ print_api_error(e, "get attribute")
87
+ raise typer.Exit(1)
88
+
89
+
90
+ @app.command("show-for-system")
91
+ def show_system_attributes(
92
+ system_id: int = typer.Argument(..., help="Managed System ID"),
93
+ output: OutputFormat = typer.Option(OutputFormat.TABLE, "--output", "-o"),
94
+ ):
95
+ """Show attributes assigned to a managed system."""
96
+ from bt_cli.pws.client import get_client
97
+
98
+ try:
99
+ with get_client() as client:
100
+ attributes = client.get_managed_system_attributes(system_id)
101
+
102
+ if output == OutputFormat.JSON:
103
+ print_json(attributes)
104
+ else:
105
+ if not attributes:
106
+ print_error(f"No attributes assigned to managed system {system_id}")
107
+ return
108
+
109
+ columns = [
110
+ ("ID", "AttributeID"),
111
+ ("Name", "AttributeName"),
112
+ ("Type", "AttributeType"),
113
+ ]
114
+ print_table(attributes, columns, title=f"Attributes for Managed System {system_id}")
115
+ except httpx.HTTPStatusError as e:
116
+ print_api_error(e, "show system attributes")
117
+ raise typer.Exit(1)
118
+ except httpx.RequestError as e:
119
+ print_api_error(e, "show system attributes")
120
+ raise typer.Exit(1)
121
+ except Exception as e:
122
+ print_api_error(e, "show system attributes")
123
+ raise typer.Exit(1)
124
+
125
+
126
+ @app.command("assign")
127
+ def assign_attribute(
128
+ system_id: int = typer.Argument(..., help="Managed System ID"),
129
+ attribute_id: int = typer.Argument(..., help="Attribute ID to assign"),
130
+ ):
131
+ """Assign an attribute to a managed system."""
132
+ from bt_cli.pws.client import get_client
133
+
134
+ try:
135
+ with get_client() as client:
136
+ client.assign_managed_system_attribute(system_id, attribute_id)
137
+ print_success(f"Assigned attribute {attribute_id} to managed system {system_id}")
138
+ except httpx.HTTPStatusError as e:
139
+ print_api_error(e, "assign attribute")
140
+ raise typer.Exit(1)
141
+ except httpx.RequestError as e:
142
+ print_api_error(e, "assign attribute")
143
+ raise typer.Exit(1)
144
+ except Exception as e:
145
+ print_api_error(e, "assign attribute")
146
+ raise typer.Exit(1)
147
+
148
+
149
+ @app.command("remove")
150
+ def remove_attribute(
151
+ system_id: int = typer.Argument(..., help="Managed System ID"),
152
+ attribute_id: int = typer.Argument(..., help="Attribute ID to remove"),
153
+ force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
154
+ ):
155
+ """Remove an attribute from a managed system."""
156
+ from bt_cli.pws.client import get_client
157
+
158
+ if not force:
159
+ typer.confirm(
160
+ f"Remove attribute {attribute_id} from managed system {system_id}?",
161
+ abort=True
162
+ )
163
+
164
+ try:
165
+ with get_client() as client:
166
+ client.remove_managed_system_attribute(system_id, attribute_id)
167
+ print_success(f"Removed attribute {attribute_id} from managed system {system_id}")
168
+ except httpx.HTTPStatusError as e:
169
+ print_api_error(e, "remove attribute")
170
+ raise typer.Exit(1)
171
+ except httpx.RequestError as e:
172
+ print_api_error(e, "remove attribute")
173
+ raise typer.Exit(1)
174
+ except Exception as e:
175
+ print_api_error(e, "remove attribute")
176
+ raise typer.Exit(1)
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