bt-cli 0.4.9__tar.gz → 0.4.11__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 (175) hide show
  1. {bt_cli-0.4.9 → bt_cli-0.4.11}/.gitignore +1 -0
  2. {bt_cli-0.4.9 → bt_cli-0.4.11}/CLAUDE.md +1 -1
  3. {bt_cli-0.4.9 → bt_cli-0.4.11}/PKG-INFO +1 -1
  4. {bt_cli-0.4.9 → bt_cli-0.4.11}/pyproject.toml +1 -1
  5. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/__init__.py +1 -1
  6. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/cli.py +5 -5
  7. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/core/csv_utils.py +2 -2
  8. bt_cli-0.4.11/tests/integration/conftest.py +214 -0
  9. bt_cli-0.4.11/tests/integration/helpers.py +75 -0
  10. bt_cli-0.4.11/tests/integration/test_epmw_lifecycle.py +242 -0
  11. bt_cli-0.4.11/tests/integration/test_pra_lifecycle.py +283 -0
  12. bt_cli-0.4.11/tests/integration/test_pws_lifecycle.py +309 -0
  13. bt_cli-0.4.9/.claude/settings.local.json +0 -128
  14. bt_cli-0.4.9/bt.spec +0 -57
  15. bt_cli-0.4.9/tests/integration/conftest.py +0 -96
  16. {bt_cli-0.4.9 → bt_cli-0.4.11}/.claude/skills/bt/SKILL.md +0 -0
  17. {bt_cli-0.4.9 → bt_cli-0.4.11}/.claude/skills/entitle/SKILL.md +0 -0
  18. {bt_cli-0.4.9 → bt_cli-0.4.11}/.claude/skills/epmw/SKILL.md +0 -0
  19. {bt_cli-0.4.9 → bt_cli-0.4.11}/.claude/skills/pra/SKILL.md +0 -0
  20. {bt_cli-0.4.9 → bt_cli-0.4.11}/.claude/skills/pws/SKILL.md +0 -0
  21. {bt_cli-0.4.9 → bt_cli-0.4.11}/.env.example +0 -0
  22. {bt_cli-0.4.9 → bt_cli-0.4.11}/.github/workflows/ci.yml +0 -0
  23. {bt_cli-0.4.9 → bt_cli-0.4.11}/.github/workflows/release.yml +0 -0
  24. {bt_cli-0.4.9 → bt_cli-0.4.11}/README.md +0 -0
  25. {bt_cli-0.4.9 → bt_cli-0.4.11}/assets/cli-help.png +0 -0
  26. {bt_cli-0.4.9 → bt_cli-0.4.11}/assets/cli-output.png +0 -0
  27. /bt_cli-0.4.9/bt-admin.spec → /bt_cli-0.4.11/bt-cli.spec +0 -0
  28. {bt_cli-0.4.9 → bt_cli-0.4.11}/bt_entry.py +0 -0
  29. {bt_cli-0.4.9 → bt_cli-0.4.11}/scripts/bt_entry.py +0 -0
  30. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/commands/__init__.py +0 -0
  31. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/commands/configure.py +0 -0
  32. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/commands/learn.py +0 -0
  33. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/commands/quick.py +0 -0
  34. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/core/__init__.py +0 -0
  35. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/core/auth.py +0 -0
  36. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/core/client.py +0 -0
  37. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/core/config.py +0 -0
  38. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/core/config_file.py +0 -0
  39. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/core/errors.py +0 -0
  40. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/core/output.py +0 -0
  41. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/core/prompts.py +0 -0
  42. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/core/rest_debug.py +0 -0
  43. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/data/CLAUDE.md +0 -0
  44. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/data/__init__.py +0 -0
  45. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/data/skills/bt/SKILL.md +0 -0
  46. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/data/skills/entitle/SKILL.md +0 -0
  47. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/data/skills/epmw/SKILL.md +0 -0
  48. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/data/skills/pra/SKILL.md +0 -0
  49. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/data/skills/pws/SKILL.md +0 -0
  50. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/entitle/__init__.py +0 -0
  51. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/entitle/client/__init__.py +0 -0
  52. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/entitle/client/base.py +0 -0
  53. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/entitle/commands/__init__.py +0 -0
  54. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/entitle/commands/accounts.py +0 -0
  55. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/entitle/commands/applications.py +0 -0
  56. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/entitle/commands/auth.py +0 -0
  57. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/entitle/commands/bundles.py +0 -0
  58. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/entitle/commands/integrations.py +0 -0
  59. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/entitle/commands/permissions.py +0 -0
  60. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/entitle/commands/policies.py +0 -0
  61. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/entitle/commands/resources.py +0 -0
  62. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/entitle/commands/roles.py +0 -0
  63. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/entitle/commands/users.py +0 -0
  64. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/entitle/commands/workflows.py +0 -0
  65. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/entitle/models/__init__.py +0 -0
  66. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/entitle/models/bundle.py +0 -0
  67. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/entitle/models/common.py +0 -0
  68. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/entitle/models/integration.py +0 -0
  69. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/entitle/models/permission.py +0 -0
  70. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/entitle/models/policy.py +0 -0
  71. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/entitle/models/resource.py +0 -0
  72. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/entitle/models/role.py +0 -0
  73. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/entitle/models/user.py +0 -0
  74. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/entitle/models/workflow.py +0 -0
  75. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/epmw/__init__.py +0 -0
  76. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/epmw/client/__init__.py +0 -0
  77. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/epmw/client/base.py +0 -0
  78. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/epmw/commands/__init__.py +0 -0
  79. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/epmw/commands/audits.py +0 -0
  80. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/epmw/commands/auth.py +0 -0
  81. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/epmw/commands/computers.py +0 -0
  82. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/epmw/commands/events.py +0 -0
  83. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/epmw/commands/groups.py +0 -0
  84. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/epmw/commands/policies.py +0 -0
  85. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/epmw/commands/quick.py +0 -0
  86. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/epmw/commands/requests.py +0 -0
  87. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/epmw/commands/roles.py +0 -0
  88. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/epmw/commands/tasks.py +0 -0
  89. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/epmw/commands/users.py +0 -0
  90. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/epmw/models/__init__.py +0 -0
  91. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/pra/__init__.py +0 -0
  92. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/pra/client/__init__.py +0 -0
  93. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/pra/client/base.py +0 -0
  94. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/pra/commands/__init__.py +0 -0
  95. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/pra/commands/auth.py +0 -0
  96. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/pra/commands/import_export.py +0 -0
  97. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/pra/commands/jump_clients.py +0 -0
  98. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/pra/commands/jump_groups.py +0 -0
  99. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/pra/commands/jump_items.py +0 -0
  100. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/pra/commands/jumpoints.py +0 -0
  101. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/pra/commands/policies.py +0 -0
  102. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/pra/commands/quick.py +0 -0
  103. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/pra/commands/teams.py +0 -0
  104. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/pra/commands/users.py +0 -0
  105. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/pra/commands/vault.py +0 -0
  106. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/pra/models/__init__.py +0 -0
  107. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/pra/models/common.py +0 -0
  108. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/pra/models/jump_client.py +0 -0
  109. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/pra/models/jump_group.py +0 -0
  110. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/pra/models/jump_item.py +0 -0
  111. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/pra/models/jumpoint.py +0 -0
  112. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/pra/models/team.py +0 -0
  113. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/pra/models/user.py +0 -0
  114. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/pra/models/vault.py +0 -0
  115. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/pws/__init__.py +0 -0
  116. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/pws/client/__init__.py +0 -0
  117. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/pws/client/base.py +0 -0
  118. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/pws/client/beyondinsight.py +0 -0
  119. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/pws/client/passwordsafe.py +0 -0
  120. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/pws/commands/__init__.py +0 -0
  121. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/pws/commands/accounts.py +0 -0
  122. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/pws/commands/assets.py +0 -0
  123. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/pws/commands/auth.py +0 -0
  124. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/pws/commands/clouds.py +0 -0
  125. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/pws/commands/config.py +0 -0
  126. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/pws/commands/credentials.py +0 -0
  127. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/pws/commands/databases.py +0 -0
  128. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/pws/commands/directories.py +0 -0
  129. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/pws/commands/functional.py +0 -0
  130. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/pws/commands/import_export.py +0 -0
  131. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/pws/commands/platforms.py +0 -0
  132. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/pws/commands/quick.py +0 -0
  133. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/pws/commands/search.py +0 -0
  134. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/pws/commands/secrets.py +0 -0
  135. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/pws/commands/systems.py +0 -0
  136. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/pws/commands/users.py +0 -0
  137. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/pws/commands/workgroups.py +0 -0
  138. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/pws/config.py +0 -0
  139. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/pws/models/__init__.py +0 -0
  140. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/pws/models/account.py +0 -0
  141. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/pws/models/asset.py +0 -0
  142. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/pws/models/common.py +0 -0
  143. {bt_cli-0.4.9 → bt_cli-0.4.11}/src/bt_cli/pws/models/system.py +0 -0
  144. {bt_cli-0.4.9 → bt_cli-0.4.11}/tests/__init__.py +0 -0
  145. {bt_cli-0.4.9 → bt_cli-0.4.11}/tests/conftest.py +0 -0
  146. {bt_cli-0.4.9 → bt_cli-0.4.11}/tests/core/__init__.py +0 -0
  147. {bt_cli-0.4.9 → bt_cli-0.4.11}/tests/core/test_auth.py +0 -0
  148. {bt_cli-0.4.9 → bt_cli-0.4.11}/tests/core/test_config.py +0 -0
  149. {bt_cli-0.4.9 → bt_cli-0.4.11}/tests/core/test_errors.py +0 -0
  150. {bt_cli-0.4.9 → bt_cli-0.4.11}/tests/core/test_rest_debug.py +0 -0
  151. {bt_cli-0.4.9 → bt_cli-0.4.11}/tests/entitle/__init__.py +0 -0
  152. {bt_cli-0.4.9 → bt_cli-0.4.11}/tests/entitle/test_client.py +0 -0
  153. {bt_cli-0.4.9 → bt_cli-0.4.11}/tests/entitle/test_commands.py +0 -0
  154. {bt_cli-0.4.9 → bt_cli-0.4.11}/tests/entitle-smoke-test.sh +0 -0
  155. {bt_cli-0.4.9 → bt_cli-0.4.11}/tests/epmw/__init__.py +0 -0
  156. {bt_cli-0.4.9 → bt_cli-0.4.11}/tests/epmw/test_client.py +0 -0
  157. {bt_cli-0.4.9 → bt_cli-0.4.11}/tests/epmw/test_commands.py +0 -0
  158. {bt_cli-0.4.9 → bt_cli-0.4.11}/tests/epmw-quick-test-plan.md +0 -0
  159. {bt_cli-0.4.9 → bt_cli-0.4.11}/tests/fixtures/__init__.py +0 -0
  160. {bt_cli-0.4.9 → bt_cli-0.4.11}/tests/fixtures/responses.py +0 -0
  161. {bt_cli-0.4.9 → bt_cli-0.4.11}/tests/integration/__init__.py +0 -0
  162. {bt_cli-0.4.9 → bt_cli-0.4.11}/tests/integration/test_entitle_integration.py +0 -0
  163. {bt_cli-0.4.9 → bt_cli-0.4.11}/tests/integration/test_epmw_integration.py +0 -0
  164. {bt_cli-0.4.9 → bt_cli-0.4.11}/tests/integration/test_pra_integration.py +0 -0
  165. {bt_cli-0.4.9 → bt_cli-0.4.11}/tests/integration/test_pws_integration.py +0 -0
  166. {bt_cli-0.4.9 → bt_cli-0.4.11}/tests/pra/__init__.py +0 -0
  167. {bt_cli-0.4.9 → bt_cli-0.4.11}/tests/pra/test_client.py +0 -0
  168. {bt_cli-0.4.9 → bt_cli-0.4.11}/tests/pra/test_commands.py +0 -0
  169. {bt_cli-0.4.9 → bt_cli-0.4.11}/tests/pra-smoke-test.sh +0 -0
  170. {bt_cli-0.4.9 → bt_cli-0.4.11}/tests/pra-test-plan.md +0 -0
  171. {bt_cli-0.4.9 → bt_cli-0.4.11}/tests/pws/__init__.py +0 -0
  172. {bt_cli-0.4.9 → bt_cli-0.4.11}/tests/pws/test_client.py +0 -0
  173. {bt_cli-0.4.9 → bt_cli-0.4.11}/tests/pws/test_commands.py +0 -0
  174. {bt_cli-0.4.9 → bt_cli-0.4.11}/tests/pws-quick-test-plan.md +0 -0
  175. {bt_cli-0.4.9 → bt_cli-0.4.11}/tests/pws-smoke-test.sh +0 -0
@@ -42,3 +42,4 @@ htmlcov/
42
42
  # Misc
43
43
  *.log
44
44
  .DS_Store
45
+ .claude/settings.local.json
@@ -5,7 +5,7 @@ BeyondTrust Platform CLI for Password Safe, Entitle, PRA, and EPM Windows.
5
5
  ## Setup
6
6
 
7
7
  ```bash
8
- cd /home/admin/entitl-sko/bt-cli
8
+ # From project root
9
9
  source .venv/bin/activate && source .env
10
10
  bt whoami # Test all connections
11
11
  ```
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bt-cli
3
- Version: 0.4.9
3
+ Version: 0.4.11
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.9"
7
+ version = "0.4.11"
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.9"
3
+ __version__ = "0.4.11"
@@ -552,9 +552,9 @@ def _get_skills_path() -> Optional[str]:
552
552
  if bundle_path.exists():
553
553
  return str(bundle_path)
554
554
 
555
- # Try importlib.resources
555
+ # Try importlib.resources (files() works with directories in 3.9+)
556
556
  try:
557
- if sys.version_info >= (3, 11):
557
+ if sys.version_info >= (3, 9):
558
558
  from importlib.resources import files
559
559
  data_path = files("bt_cli.data").joinpath("skills")
560
560
  if data_path.is_dir():
@@ -564,7 +564,7 @@ def _get_skills_path() -> Optional[str]:
564
564
  with path("bt_cli.data", "skills") as p:
565
565
  if p.exists():
566
566
  return str(p)
567
- except (ImportError, ModuleNotFoundError, TypeError, FileNotFoundError):
567
+ except (ImportError, ModuleNotFoundError, TypeError, FileNotFoundError, IsADirectoryError):
568
568
  pass
569
569
 
570
570
  # Fall back to source directory
@@ -587,9 +587,9 @@ def _get_claude_md_path() -> Optional[str]:
587
587
  if bundle_path.exists():
588
588
  return str(bundle_path)
589
589
 
590
- # Try importlib.resources
590
+ # Try importlib.resources (files() available in 3.9+)
591
591
  try:
592
- if sys.version_info >= (3, 11):
592
+ if sys.version_info >= (3, 9):
593
593
  from importlib.resources import files
594
594
  data_path = files("bt_cli.data").joinpath("CLAUDE.md")
595
595
  if data_path.is_file():
@@ -2,7 +2,7 @@
2
2
 
3
3
  import csv
4
4
  from pathlib import Path
5
- from typing import Any
5
+ from typing import Any, Optional
6
6
 
7
7
 
8
8
  def read_csv(file_path: str) -> list[dict[str, Any]]:
@@ -73,7 +73,7 @@ def parse_bool(value: str) -> bool:
73
73
  return value.lower().strip() in ('true', 'yes', '1', 'y')
74
74
 
75
75
 
76
- def parse_int(value: str, default: int | None = None) -> int | None:
76
+ def parse_int(value: str, default: Optional[int] = None) -> Optional[int]:
77
77
  """Parse integer from CSV string.
78
78
 
79
79
  Args:
@@ -0,0 +1,214 @@
1
+ """Integration test fixtures.
2
+
3
+ These fixtures provide real API clients when credentials are configured.
4
+ Tests are automatically skipped if required environment variables are missing.
5
+ """
6
+
7
+ import os
8
+
9
+ import pytest
10
+
11
+
12
+ def pytest_configure(config):
13
+ """Register integration marker."""
14
+ config.addinivalue_line(
15
+ "markers", "integration: mark test as integration test requiring real API credentials"
16
+ )
17
+
18
+
19
+ @pytest.fixture
20
+ def pws_integration_config():
21
+ """Load PWS config from environment, skip if not available.
22
+
23
+ Requires BT_PWS_API_URL and either:
24
+ - BT_PWS_API_KEY (for API key auth)
25
+ - BT_PWS_CLIENT_ID and BT_PWS_CLIENT_SECRET (for OAuth)
26
+ """
27
+ from bt_cli.core.config import load_pws_config
28
+
29
+ if not os.getenv("BT_PWS_API_URL"):
30
+ pytest.skip("PWS credentials not configured (BT_PWS_API_URL not set)")
31
+
32
+ if not os.getenv("BT_PWS_API_KEY") and not (
33
+ os.getenv("BT_PWS_CLIENT_ID") and os.getenv("BT_PWS_CLIENT_SECRET")
34
+ ):
35
+ pytest.skip("PWS credentials not configured (missing API key or OAuth credentials)")
36
+
37
+ return load_pws_config()
38
+
39
+
40
+ @pytest.fixture
41
+ def pra_integration_config():
42
+ """Load PRA config from environment, skip if not available.
43
+
44
+ Requires:
45
+ - BT_PRA_API_URL
46
+ - BT_PRA_CLIENT_ID
47
+ - BT_PRA_CLIENT_SECRET
48
+ """
49
+ from bt_cli.core.config import load_pra_config
50
+
51
+ if not os.getenv("BT_PRA_API_URL"):
52
+ pytest.skip("PRA credentials not configured (BT_PRA_API_URL not set)")
53
+
54
+ if not os.getenv("BT_PRA_CLIENT_ID") or not os.getenv("BT_PRA_CLIENT_SECRET"):
55
+ pytest.skip("PRA credentials not configured (missing OAuth credentials)")
56
+
57
+ return load_pra_config()
58
+
59
+
60
+ @pytest.fixture
61
+ def epmw_integration_config():
62
+ """Load EPMW config from environment, skip if not available.
63
+
64
+ Requires:
65
+ - BT_EPM_API_URL
66
+ - BT_EPM_CLIENT_ID
67
+ - BT_EPM_CLIENT_SECRET
68
+ """
69
+ from bt_cli.core.config import load_epmw_config
70
+
71
+ if not os.getenv("BT_EPM_API_URL"):
72
+ pytest.skip("EPMW credentials not configured (BT_EPM_API_URL not set)")
73
+
74
+ if not os.getenv("BT_EPM_CLIENT_ID") or not os.getenv("BT_EPM_CLIENT_SECRET"):
75
+ pytest.skip("EPMW credentials not configured (missing OAuth credentials)")
76
+
77
+ return load_epmw_config()
78
+
79
+
80
+ @pytest.fixture
81
+ def entitle_integration_config():
82
+ """Load Entitle config from environment, skip if not available.
83
+
84
+ Requires:
85
+ - BT_ENTITLE_API_URL
86
+ - BT_ENTITLE_API_KEY
87
+ """
88
+ from bt_cli.core.config import load_entitle_config
89
+
90
+ if not os.getenv("BT_ENTITLE_API_URL"):
91
+ pytest.skip("Entitle credentials not configured (BT_ENTITLE_API_URL not set)")
92
+
93
+ if not os.getenv("BT_ENTITLE_API_KEY"):
94
+ pytest.skip("Entitle credentials not configured (BT_ENTITLE_API_KEY not set)")
95
+
96
+ return load_entitle_config()
97
+
98
+
99
+ # =============================================================================
100
+ # Auto-Discovery Fixtures for PWS
101
+ # =============================================================================
102
+
103
+ @pytest.fixture
104
+ def pws_workgroup_id(pws_integration_config):
105
+ """Auto-discover first available workgroup ID."""
106
+ from bt_cli.pws.client.base import FullPasswordSafeClient
107
+
108
+ with FullPasswordSafeClient(pws_integration_config) as client:
109
+ client.authenticate()
110
+ workgroups = client.list_workgroups()
111
+ if not workgroups:
112
+ pytest.skip("No workgroups available")
113
+ return workgroups[0]["ID"]
114
+
115
+
116
+ @pytest.fixture
117
+ def pws_linux_platform_id(pws_integration_config):
118
+ """Auto-discover Linux platform ID."""
119
+ from bt_cli.pws.client.base import FullPasswordSafeClient
120
+
121
+ with FullPasswordSafeClient(pws_integration_config) as client:
122
+ client.authenticate()
123
+ platforms = client.list_platforms()
124
+ for p in platforms:
125
+ if "Linux" in p.get("Name", ""):
126
+ return p["PlatformID"]
127
+ pytest.skip("No Linux platform found")
128
+
129
+
130
+ @pytest.fixture
131
+ def pws_windows_platform_id(pws_integration_config):
132
+ """Auto-discover Windows platform ID."""
133
+ from bt_cli.pws.client.base import FullPasswordSafeClient
134
+
135
+ with FullPasswordSafeClient(pws_integration_config) as client:
136
+ client.authenticate()
137
+ platforms = client.list_platforms()
138
+ for p in platforms:
139
+ if "Windows" in p.get("Name", "") and "Domain" not in p.get("Name", ""):
140
+ return p["PlatformID"]
141
+ pytest.skip("No Windows platform found")
142
+
143
+
144
+ @pytest.fixture
145
+ def pws_functional_account_id(pws_integration_config):
146
+ """Auto-discover first available functional account ID."""
147
+ from bt_cli.pws.client.base import FullPasswordSafeClient
148
+
149
+ with FullPasswordSafeClient(pws_integration_config) as client:
150
+ client.authenticate()
151
+ accounts = client.list_functional_accounts()
152
+ if not accounts:
153
+ pytest.skip("No functional accounts available")
154
+ return accounts[0]["FunctionalAccountID"]
155
+
156
+
157
+ # =============================================================================
158
+ # Auto-Discovery Fixtures for PRA
159
+ # =============================================================================
160
+
161
+ @pytest.fixture
162
+ def pra_jumpoint_id(pra_integration_config):
163
+ """Auto-discover first available jumpoint ID."""
164
+ from bt_cli.pra.client.base import PRAClient
165
+
166
+ with PRAClient(pra_integration_config) as client:
167
+ # PRA uses automatic OAuth authentication
168
+ jumpoints = client.list_jumpoints()
169
+ if not jumpoints:
170
+ pytest.skip("No jumpoints available")
171
+ return jumpoints[0]["id"]
172
+
173
+
174
+ @pytest.fixture
175
+ def pra_jump_group_id(pra_integration_config):
176
+ """Auto-discover first available jump group ID."""
177
+ from bt_cli.pra.client.base import PRAClient
178
+
179
+ with PRAClient(pra_integration_config) as client:
180
+ # PRA uses automatic OAuth authentication
181
+ groups = client.list_jump_groups()
182
+ if not groups:
183
+ pytest.skip("No jump groups available")
184
+ return groups[0]["id"]
185
+
186
+
187
+ # =============================================================================
188
+ # Auto-Discovery Fixtures for EPMW
189
+ # =============================================================================
190
+
191
+ @pytest.fixture
192
+ def epmw_policy_id(epmw_integration_config):
193
+ """Auto-discover first available policy ID."""
194
+ from bt_cli.epmw.client.base import EPMWClient
195
+
196
+ with EPMWClient(epmw_integration_config) as client:
197
+ # EPMW uses auto-auth via OAuth token on each request
198
+ policies = client.list_policies()
199
+ if not policies:
200
+ pytest.skip("No policies available")
201
+ return policies[0]["id"]
202
+
203
+
204
+ @pytest.fixture
205
+ def epmw_computer_id(epmw_integration_config):
206
+ """Auto-discover first available computer ID."""
207
+ from bt_cli.epmw.client.base import EPMWClient
208
+
209
+ with EPMWClient(epmw_integration_config) as client:
210
+ # EPMW uses auto-auth via OAuth token on each request
211
+ computers = client.list_computers()
212
+ if not computers:
213
+ pytest.skip("No computers available")
214
+ return computers[0]["id"]
@@ -0,0 +1,75 @@
1
+ """Shared utilities for integration tests."""
2
+
3
+ import uuid
4
+ from contextlib import contextmanager
5
+ from typing import Callable, Any
6
+
7
+
8
+ def unique_name(prefix: str) -> str:
9
+ """Generate unique test resource name.
10
+
11
+ Args:
12
+ prefix: Resource type prefix (e.g., 'safe', 'system', 'jump')
13
+
14
+ Returns:
15
+ Unique name like 'pytest-safe-a1b2c3d4'
16
+ """
17
+ return f"pytest-{prefix}-{uuid.uuid4().hex[:8]}"
18
+
19
+
20
+ @contextmanager
21
+ def cleanup_on_exit(delete_func: Callable[[Any], Any], resource_id: Any):
22
+ """Context manager to ensure resource cleanup even on test failure.
23
+
24
+ Args:
25
+ delete_func: Function to call for deletion
26
+ resource_id: ID to pass to delete function
27
+
28
+ Example:
29
+ with cleanup_on_exit(client.delete_safe, safe_id):
30
+ # do stuff with safe
31
+ pass
32
+ # safe is deleted even if exception occurs
33
+ """
34
+ try:
35
+ yield
36
+ finally:
37
+ try:
38
+ delete_func(resource_id)
39
+ except Exception:
40
+ pass # Best effort cleanup
41
+
42
+
43
+ class ResourceTracker:
44
+ """Track created resources for cleanup at end of test.
45
+
46
+ Example:
47
+ tracker = ResourceTracker()
48
+ tracker.add(client.delete_safe, safe_id)
49
+ tracker.add(client.delete_folder, folder_id)
50
+ # ... test code ...
51
+ tracker.cleanup() # Deletes in reverse order
52
+ """
53
+
54
+ def __init__(self):
55
+ self._resources: list[tuple[Callable, Any]] = []
56
+
57
+ def add(self, delete_func: Callable[[Any], Any], resource_id: Any) -> None:
58
+ """Register a resource for cleanup."""
59
+ self._resources.append((delete_func, resource_id))
60
+
61
+ def cleanup(self) -> None:
62
+ """Delete all tracked resources in reverse order (LIFO)."""
63
+ while self._resources:
64
+ delete_func, resource_id = self._resources.pop()
65
+ try:
66
+ delete_func(resource_id)
67
+ except Exception:
68
+ pass # Best effort cleanup
69
+
70
+ def __enter__(self):
71
+ return self
72
+
73
+ def __exit__(self, exc_type, exc_val, exc_tb):
74
+ self.cleanup()
75
+ return False # Don't suppress exceptions
@@ -0,0 +1,242 @@
1
+ """EPMW CRUD lifecycle integration tests.
2
+
3
+ These tests create, verify, and clean up real resources in EPM Windows.
4
+ Run with: pytest tests/integration/test_epmw_lifecycle.py -v
5
+ """
6
+
7
+ import pytest
8
+
9
+ from tests.integration.helpers import unique_name, ResourceTracker
10
+
11
+
12
+ @pytest.mark.integration
13
+ class TestGroupLifecycle:
14
+ """Test EPMW Group CRUD lifecycle."""
15
+
16
+ def test_group_lifecycle(self, epmw_integration_config):
17
+ """Create group → update → delete."""
18
+ from bt_cli.epmw.client.base import EPMWClient
19
+
20
+ with EPMWClient(epmw_integration_config) as client:
21
+ # EPMW uses auto-auth via OAuth token on each request
22
+
23
+ # CREATE (returns just the group ID string)
24
+ group_name = unique_name("group")
25
+ group_id = client.create_group({
26
+ "name": group_name,
27
+ "description": "Integration test group",
28
+ })
29
+
30
+ try:
31
+ # VERIFY exists
32
+ groups = client.list_groups()
33
+ assert any(g["id"] == group_id for g in groups), "Group not found"
34
+
35
+ # GET details
36
+ retrieved = client.get_group(group_id)
37
+ assert retrieved["name"] == group_name
38
+
39
+ # UPDATE (may fail with 405 if API doesn't support PUT)
40
+ new_desc = "Updated integration test group"
41
+ try:
42
+ client.update_group(group_id, {
43
+ "name": group_name,
44
+ "description": new_desc,
45
+ })
46
+ # VERIFY update
47
+ updated = client.get_group(group_id)
48
+ assert updated["description"] == new_desc
49
+ except Exception as e:
50
+ if "405" in str(e):
51
+ pass # API doesn't support update, skip verification
52
+ else:
53
+ raise
54
+
55
+ finally:
56
+ # DELETE (may fail with 405 if API doesn't support it)
57
+ try:
58
+ client.delete_group(group_id)
59
+ except Exception as e:
60
+ if "405" in str(e):
61
+ # API doesn't support delete, test passed for create/update
62
+ return
63
+ raise
64
+
65
+ # VERIFY deleted
66
+ groups = client.list_groups()
67
+ assert not any(g["id"] == group_id for g in groups), "Group still exists"
68
+
69
+
70
+ @pytest.mark.integration
71
+ class TestGroupWithPolicyLifecycle:
72
+ """Test Group with Policy assignment lifecycle."""
73
+
74
+ def test_group_policy_assignment(self, epmw_integration_config, epmw_policy_id):
75
+ """Create group → assign policy → verify → delete."""
76
+ from bt_cli.epmw.client.base import EPMWClient
77
+
78
+ with EPMWClient(epmw_integration_config) as client:
79
+ # EPMW uses auto-auth via OAuth token
80
+
81
+ # CREATE group (returns just the group ID string)
82
+ group_name = unique_name("group")
83
+ group_id = client.create_group({
84
+ "name": group_name,
85
+ "description": "Policy assignment test",
86
+ })
87
+
88
+ try:
89
+ # ASSIGN policy to group (skip if API doesn't support it)
90
+ try:
91
+ client.assign_policy_to_group(group_id, epmw_policy_id)
92
+ except Exception as e:
93
+ if "404" in str(e) or "405" in str(e):
94
+ pytest.skip(f"Policy assignment not supported: {e}")
95
+ raise
96
+
97
+ # VERIFY assignment (get group and check policy)
98
+ retrieved = client.get_group(group_id)
99
+ # Policy assignment might be reflected in group data
100
+ assert retrieved is not None
101
+
102
+ finally:
103
+ # DELETE group (may fail with 405)
104
+ try:
105
+ client.delete_group(group_id)
106
+ except Exception:
107
+ pass # Best effort cleanup
108
+
109
+
110
+ @pytest.mark.integration
111
+ class TestComputerOperations:
112
+ """Test Computer archive/unarchive operations.
113
+
114
+ Note: We can't create computers via API (they're agent-enrolled),
115
+ so we test archive/unarchive on existing computers if available.
116
+ """
117
+
118
+ def test_computer_archive_unarchive(self, epmw_integration_config, epmw_computer_id):
119
+ """Archive computer → verify → unarchive → verify."""
120
+ from bt_cli.epmw.client.base import EPMWClient
121
+
122
+ with EPMWClient(epmw_integration_config) as client:
123
+ # EPMW uses auto-auth via OAuth token
124
+
125
+ # Get initial state (field is 'archived' not 'isArchived')
126
+ computer = client.get_computer(epmw_computer_id)
127
+ initial_archived = computer.get("archived", False)
128
+
129
+ # Only test if not already archived
130
+ if initial_archived:
131
+ pytest.skip("Computer already archived, skipping test")
132
+
133
+ try:
134
+ # ARCHIVE
135
+ client.archive_computer(epmw_computer_id)
136
+
137
+ # VERIFY archived (API may process async, skip if not immediate)
138
+ archived = client.get_computer(epmw_computer_id)
139
+ if not archived.get("archived", False):
140
+ pytest.skip("Archive operation may be async or not supported")
141
+
142
+ finally:
143
+ # UNARCHIVE (restore original state)
144
+ try:
145
+ client.unarchive_computer(epmw_computer_id)
146
+ except Exception:
147
+ pass # Best effort
148
+
149
+ # VERIFY unarchived
150
+ restored = client.get_computer(epmw_computer_id)
151
+ assert not restored.get("archived", True), "Computer still archived"
152
+
153
+
154
+ @pytest.mark.integration
155
+ class TestUserLifecycle:
156
+ """Test EPMW User CRUD lifecycle.
157
+
158
+ Note: User creation may require specific permissions.
159
+ Tests will skip if permissions are insufficient.
160
+ """
161
+
162
+ def test_user_lifecycle(self, epmw_integration_config):
163
+ """Create user → enable → disable → delete."""
164
+ from bt_cli.epmw.client.base import EPMWClient
165
+
166
+ with EPMWClient(epmw_integration_config) as client:
167
+ # EPMW uses auto-auth via OAuth token
168
+
169
+ # CREATE user (may fail with 400/403 if not permitted)
170
+ username = unique_name("user")
171
+ try:
172
+ user = client.create_user({
173
+ "username": f"{username}@test.local",
174
+ "firstName": "Test",
175
+ "lastName": "User",
176
+ "email": f"{username}@example.com",
177
+ })
178
+ except Exception as e:
179
+ if "permission" in str(e).lower() or "400" in str(e) or "403" in str(e):
180
+ pytest.skip(f"User creation not permitted: {e}")
181
+ raise
182
+
183
+ user_id = user["id"]
184
+
185
+ try:
186
+ # VERIFY exists
187
+ users = client.list_users()
188
+ assert any(u["id"] == user_id for u in users), "User not found"
189
+
190
+ # DISABLE
191
+ client.disable_user(user_id)
192
+
193
+ # VERIFY disabled
194
+ disabled = client.get_user(user_id)
195
+ assert disabled.get("isDisabled", False), "User not disabled"
196
+
197
+ # ENABLE
198
+ client.enable_user(user_id)
199
+
200
+ # VERIFY enabled
201
+ enabled = client.get_user(user_id)
202
+ assert not enabled.get("isDisabled", True), "User still disabled"
203
+
204
+ finally:
205
+ # DELETE
206
+ try:
207
+ # Note: EPMW may not have delete_user, may need to disable instead
208
+ if hasattr(client, 'delete_user'):
209
+ client.delete_user(user_id)
210
+ else:
211
+ # Just disable if can't delete
212
+ client.disable_user(user_id)
213
+ except Exception:
214
+ pass # Best effort cleanup
215
+
216
+
217
+ @pytest.mark.integration
218
+ class TestAdminRequestOperations:
219
+ """Test Admin Request approval/denial.
220
+
221
+ Note: These tests require pending admin requests to exist.
222
+ They will skip if no requests are available.
223
+ """
224
+
225
+ def test_list_admin_requests(self, epmw_integration_config):
226
+ """List admin requests (read-only test)."""
227
+ from bt_cli.epmw.client.base import EPMWClient
228
+
229
+ with EPMWClient(epmw_integration_config) as client:
230
+ # EPMW uses auto-auth via OAuth token
231
+
232
+ # LIST requests
233
+ requests = client.list_admin_requests()
234
+
235
+ # Just verify we can list (may be empty)
236
+ assert isinstance(requests, list)
237
+
238
+ # If there are pending requests, verify structure
239
+ if requests:
240
+ req = requests[0]
241
+ # Structure has nested fields like requestInfo, accessDecision
242
+ assert "requestInfo" in req or "id" in req