bt-cli 0.4.64__tar.gz → 0.4.65__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.
- {bt_cli-0.4.64 → bt_cli-0.4.65}/CLAUDE.md +2 -2
- {bt_cli-0.4.64 → bt_cli-0.4.65}/PKG-INFO +1 -1
- {bt_cli-0.4.64 → bt_cli-0.4.65}/pyproject.toml +1 -1
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/__init__.py +1 -1
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/data/skills/pws/SKILL.md +20 -11
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pws/commands/sessions.py +55 -17
- {bt_cli-0.4.64 → bt_cli-0.4.65}/tests/pws/test_commands.py +42 -2
- {bt_cli-0.4.64 → bt_cli-0.4.65}/.claude/skills/bt/SKILL.md +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/.claude/skills/entitle/SKILL.md +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/.claude/skills/epml/SKILL.md +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/.claude/skills/epmw/SKILL.md +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/.claude/skills/pra/SKILL.md +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/.claude/skills/pws/SKILL.md +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/.env.example +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/.github/workflows/ci.yml +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/.github/workflows/release.yml +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/.gitignore +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/README.md +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/assets/cli-help.png +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/assets/cli-output.png +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/bt-cli.spec +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/bt_entry.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/epml-clients-server-side-filters-plan.md +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/epml-implementation-plan.md +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/pf-implementation-plan.md +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/scripts/bt_entry.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/scripts/pf_onboard.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/scripts/sync-package-data.sh +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/cli.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/commands/__init__.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/commands/configure.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/commands/learn.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/commands/quick.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/core/__init__.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/core/auth.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/core/client.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/core/config.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/core/config_file.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/core/csv_utils.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/core/errors.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/core/output.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/core/prompts.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/core/rest_debug.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/data/CLAUDE.md +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/data/__init__.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/data/skills/bt/SKILL.md +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/data/skills/entitle/SKILL.md +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/data/skills/epml/SKILL.md +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/data/skills/epmw/SKILL.md +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/data/skills/pf/SKILL.md +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/data/skills/pra/SKILL.md +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/data/skills/secrets/SKILL.md +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/entitle/__init__.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/entitle/client/__init__.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/entitle/client/base.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/entitle/commands/__init__.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/entitle/commands/accounts.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/entitle/commands/applications.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/entitle/commands/auth.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/entitle/commands/bundles.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/entitle/commands/integrations.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/entitle/commands/permissions.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/entitle/commands/policies.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/entitle/commands/requests.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/entitle/commands/resources.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/entitle/commands/roles.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/entitle/commands/users.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/entitle/commands/workflows.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/entitle/models/__init__.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/entitle/models/bundle.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/entitle/models/common.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/entitle/models/integration.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/entitle/models/permission.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/entitle/models/policy.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/entitle/models/resource.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/entitle/models/role.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/entitle/models/user.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/entitle/models/workflow.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/epml/__init__.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/epml/client/__init__.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/epml/client/base.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/epml/commands/__init__.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/epml/commands/audit.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/epml/commands/auth.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/epml/commands/client_pkg.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/epml/commands/clients.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/epml/commands/external_apis.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/epml/commands/hosts.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/epml/commands/iolog.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/epml/commands/license.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/epml/commands/quick.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/epml/commands/rbp_cmdgrps.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/epml/commands/rbp_entitlement.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/epml/commands/rbp_hostgrps.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/epml/commands/rbp_policy.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/epml/commands/rbp_roles.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/epml/commands/rbp_tests.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/epml/commands/rbp_tmdategrps.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/epml/commands/rbp_tx.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/epml/commands/rbp_usergrps.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/epml/commands/settings.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/epml/commands/siems.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/epml/commands/users.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/epml/models/__init__.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/epmw/__init__.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/epmw/client/__init__.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/epmw/client/base.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/epmw/commands/__init__.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/epmw/commands/audits.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/epmw/commands/auth.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/epmw/commands/computers.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/epmw/commands/events.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/epmw/commands/groups.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/epmw/commands/policies.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/epmw/commands/quick.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/epmw/commands/requests.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/epmw/commands/roles.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/epmw/commands/tasks.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/epmw/commands/users.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/epmw/models/__init__.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pf/__init__.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pf/client/__init__.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pf/client/base.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pf/commands/__init__.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pf/commands/auth.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pf/commands/machines.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pf/commands/tokens.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pf/commands/user.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pf/commands/users.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pra/__init__.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pra/client/__init__.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pra/client/base.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pra/commands/__init__.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pra/commands/auth.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pra/commands/group_policies.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pra/commands/import_export.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pra/commands/jump_clients.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pra/commands/jump_groups.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pra/commands/jump_items.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pra/commands/jumpoints.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pra/commands/policies.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pra/commands/quick.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pra/commands/teams.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pra/commands/users.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pra/commands/vault.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pra/models/__init__.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pra/models/common.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pra/models/group_policy.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pra/models/jump_client.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pra/models/jump_group.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pra/models/jump_item.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pra/models/jumpoint.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pra/models/team.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pra/models/user.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pra/models/vault.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pws/__init__.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pws/client/__init__.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pws/client/base.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pws/client/beyondinsight.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pws/client/passwordsafe.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pws/commands/__init__.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pws/commands/accounts.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pws/commands/assets.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pws/commands/attributes.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pws/commands/audit.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pws/commands/auth.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pws/commands/clouds.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pws/commands/config.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pws/commands/credentials.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pws/commands/databases.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pws/commands/directories.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pws/commands/functional.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pws/commands/import_export.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pws/commands/platforms.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pws/commands/quick.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pws/commands/search.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pws/commands/secrets.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pws/commands/systems.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pws/commands/users.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pws/commands/workgroups.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pws/config.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pws/models/__init__.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pws/models/account.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pws/models/asset.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pws/models/common.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/pws/models/system.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/secrets/__init__.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/secrets/client/__init__.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/secrets/client/base.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/secrets/commands/__init__.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/secrets/commands/_hints.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/secrets/commands/auth.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/secrets/commands/dynamic.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/secrets/commands/folders.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/secrets/commands/integrations.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/secrets/commands/leases.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/secrets/commands/static.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/src/bt_cli/secrets/models/__init__.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/tests/__init__.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/tests/conftest.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/tests/core/__init__.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/tests/core/test_auth.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/tests/core/test_config.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/tests/core/test_config_file.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/tests/core/test_errors.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/tests/core/test_output.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/tests/core/test_rest_debug.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/tests/entitle/__init__.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/tests/entitle/test_client.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/tests/entitle/test_commands.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/tests/entitle-smoke-test.sh +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/tests/epml/__init__.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/tests/epml/test_client.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/tests/epml/test_commands.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/tests/epmw/__init__.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/tests/epmw/test_client.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/tests/epmw/test_commands.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/tests/epmw-quick-test-plan.md +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/tests/fixtures/__init__.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/tests/fixtures/responses.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/tests/integration/__init__.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/tests/integration/conftest.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/tests/integration/helpers.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/tests/integration/test_entitle_integration.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/tests/integration/test_epmw_integration.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/tests/integration/test_epmw_lifecycle.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/tests/integration/test_pra_integration.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/tests/integration/test_pra_lifecycle.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/tests/integration/test_pws_integration.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/tests/integration/test_pws_lifecycle.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/tests/pf/__init__.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/tests/pf/test_client.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/tests/pf/test_commands.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/tests/pra/__init__.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/tests/pra/test_client.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/tests/pra/test_commands.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/tests/pra/test_jump_item_fixes.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/tests/pra-smoke-test.sh +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/tests/pra-test-plan.md +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/tests/pws/__init__.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/tests/pws/test_client.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/tests/pws/test_quick_sessions.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/tests/pws-quick-test-plan.md +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/tests/pws-smoke-test.sh +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/tests/secrets/__init__.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/tests/secrets/test_client.py +0 -0
- {bt_cli-0.4.64 → bt_cli-0.4.65}/tests/secrets/test_commands.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# BT-CLI
|
|
2
2
|
|
|
3
|
-
BeyondTrust Platform CLI for Password Safe, Entitle, PRA, EPM Windows, EPM Linux, and the BeyondTrust Secrets API. **Version: 0.4.
|
|
3
|
+
BeyondTrust Platform CLI for Password Safe, Entitle, PRA, EPM Windows, EPM Linux, and the BeyondTrust Secrets API. **Version: 0.4.65**
|
|
4
4
|
|
|
5
5
|
## Setup
|
|
6
6
|
|
|
@@ -88,7 +88,7 @@ PASSWORD=$(bt pws quick checkout -s server -a admin --raw)
|
|
|
88
88
|
- **PWS `/Sessions` ignores `limit`/`offset`** and returns the full session *history* every call — never `paginate()` it (infinite loop); `list_sessions` does a single GET and `bt pws sessions list` caps display client-side. `Status` codes (live-confirmed): 0=Not started, 1=Active, 2=Completed, 5=Locked, 7=Terminated; `--active`=Status 1. Active sessions have `EndTime`==`StartTime` (placeholder, not null) — identify live sessions by Status, not timestamps. **`terminate` = `POST /Sessions/{id}/Terminate`** (symmetric w/ `/Lock`); `/Sessions/{id}` is GET/PUT only (DELETE → 405).
|
|
89
89
|
- **PWS SSH/RDP session establishment = `POST /Requests/{id}/Sessions`** (`{"SessionType":"SSH"|"RDP"}`), NOT `POST /Sessions` (collection is GET-only → 405). Create the request with matching `AccessType` first. **SSH** returns a ready `Command` (`ssh -p 4422 <ticket>@<proxyhost>`) + `Link`; **RDP** returns `Host`/`Port`/`Ticket` only — the `.rdp` is built client-side (`full address` = proxy `Host:Port`, `username` = `Ticket`); there's no server-side launch file (`/Sessions/RDPFile` is audit-playback → 403). Ticket is the proxy username; password is proxy-injected. `bt pws quick checkout --access-type ssh|rdp` and `bt pws quick ssh`/`quick rdp` (with `--launch`); session commands do **not** auto-checkin.
|
|
90
90
|
- **PWS session keystroke transcript = `GET /Sessions/{id}/Keystrokes`** → events `{KeystrokeID, TimeMarker (epoch ms), Type, Data}`. **Type 1 = user input** (typed keys; special keys as glyphs `⌫`/`→`/`↑`), **Type 2 = terminal output** (SSH stdout). SSH captures a full input+output transcript; **RDP is sparse** (typed keys / window events only, off by default → often empty). Surfaced as `bt pws sessions transcript <id>` (default = time-ordered readable transcript; `--input-only`; text/json/csv). Text trail, not the visual recording. (There is **no** `bt pws sessions export` — session *metadata* evidence is `bt pws audit export`.)
|
|
91
|
-
- **PWS session recording download = headless replay under `/pbsm/replay`** (there is **no** video-export API — `?video/?mp4/?webm` return JSON, not media). Flow: `GET /Sessions/{id}` for `Token`+`RecordKey`+`Protocol` → `POST /pbsm/replay` `{id:Token, record_key, protocol:"RDP"|"SSH", headless:true}` → `PUT /pbsm/replay/{rid}` `{offset:<ms>}` (seek) → `GET /pbsm/replay/{rid}?png=<scale>` (one PNG per offset) → `DELETE /pbsm/replay/{rid}`. **Warm-up:** the first `PUT` 404s (`"no such file or directory"`) until the renderer loads — retry ~0.5s. The seek response is state `{offset, next, eof, duration}`; **`next` = next recorded frame's offset**, enabling event-driven stepping. Surfaced as `bt pws sessions replay <id
|
|
91
|
+
- **PWS session recording download = headless replay under `/pbsm/replay`** (there is **no** video-export API — `?video/?mp4/?webm` return JSON, not media). Flow: `GET /Sessions/{id}` for `Token`+`RecordKey`+`Protocol` → `POST /pbsm/replay` `{id:Token, record_key, protocol:"RDP"|"SSH", headless:true}` → `PUT /pbsm/replay/{rid}` `{offset:<ms>}` (seek) → `GET /pbsm/replay/{rid}?png=<scale>` (one PNG per offset) → `DELETE /pbsm/replay/{rid}`. **Warm-up:** the first `PUT` 404s (`"no such file or directory"`) until the renderer loads — retry ~0.5s. The seek response is state `{offset, next, eof, duration}`; **`next` = next recorded frame's offset**, enabling event-driven stepping. Surfaced as `bt pws sessions replay <id>`. **Default = even coverage + dedup**: ~60 evenly-spaced frames across the whole session (`--frames N`) with consecutive-identical frames skipped (`--dedup`, sha1-based) — because event-driven over-samples RDP (it records many deltas as the screen paints, so `next`-stepping clusters at the login and burns the frame budget before covering the timeline). Modes are mutually exclusive: `--frames` (default) / `--fps` / `--interval` / `--all-frames` (event-driven forensic). Also `--from/--to`, `--scale`, `--max-frames`, `--no-dedup`; `--mp4` stitches via ffmpeg, else prints the command + install hint. Always `DELETE`s the replay in `finally`.
|
|
92
92
|
- Entitle permissions: Use server-side filters (`--resource`, `--user`, `--integration`) - dataset can be 20k+ records
|
|
93
93
|
- Entitle users: Use `--all` flag only when needed - default fetches first page only
|
|
94
94
|
- Windows builds: Rich must be pinned to `<14.0.0` (Rich 14 has PyInstaller unicode issues)
|
|
@@ -215,22 +215,31 @@ recording. For session *metadata* evidence, use `bt pws audit export`.
|
|
|
215
215
|
|
|
216
216
|
```bash
|
|
217
217
|
# Download the VISUAL recording as PNG frames (RDP screen / SSH terminal)
|
|
218
|
-
bt pws sessions replay <session_id> #
|
|
219
|
-
bt pws sessions replay <session_id> --
|
|
218
|
+
bt pws sessions replay <session_id> # ~60 frames across the session, deduped
|
|
219
|
+
bt pws sessions replay <session_id> --frames 30 # ~30 evenly-spaced frames
|
|
220
220
|
bt pws sessions replay <session_id> --fps 1 --to 120 # 1 fps for the first 2 minutes
|
|
221
|
-
bt pws sessions replay <session_id> --
|
|
221
|
+
bt pws sessions replay <session_id> --all-frames --no-dedup # every recorded frame, verbatim
|
|
222
|
+
bt pws sessions replay <session_id> --mp4 # also stitch an MP4 (needs ffmpeg)
|
|
222
223
|
```
|
|
223
224
|
|
|
224
225
|
`replay` drives the headless renderer at `/pbsm/replay` (there is **no**
|
|
225
226
|
video-export API): create replay → seek by offset → pull one PNG per frame →
|
|
226
|
-
delete. By default it
|
|
227
|
-
`
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
227
|
+
delete. **By default it captures ~60 evenly-spaced frames across the whole
|
|
228
|
+
session and skips identical frames** (`--dedup`), so you see the full timeline
|
|
229
|
+
rather than just the busy start. Sampling modes are mutually exclusive:
|
|
230
|
+
|
|
231
|
+
- `--frames N` (default, N=60) — N frames spread evenly across the session
|
|
232
|
+
- `--fps F` / `--interval MS` — fixed-rate sampling
|
|
233
|
+
- `--all-frames` — event-driven, one frame per recorded change (forensic; can be
|
|
234
|
+
huge — RDP records many deltas while the screen paints, which is why this
|
|
235
|
+
isn't the default)
|
|
236
|
+
|
|
237
|
+
**One network round-trip per frame**, so bound long captures with `--frames`,
|
|
238
|
+
`--from/--to`, or `--max-frames` (default 3600). `--no-dedup` keeps every
|
|
239
|
+
sampled frame. `--mp4` stitches with ffmpeg (H.264); if ffmpeg is missing it
|
|
240
|
+
prints the exact command + install instructions and leaves the frames. Works for
|
|
241
|
+
RDP (screen) and SSH (rendered terminal). Gotcha: the renderer needs ~0.5s
|
|
242
|
+
warm-up (first seek 404s), handled automatically.
|
|
234
243
|
|
|
235
244
|
**Gotchas:**
|
|
236
245
|
- `/Sessions` returns session **history**, not just active sessions, and **ignores `limit`/`offset`** (returns the full set every call - the client does a single GET, the CLI caps display client-side). Use `--active` for the live ones.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""CLI commands for managing active Password Safe sessions."""
|
|
2
2
|
|
|
3
|
+
import hashlib
|
|
3
4
|
import json
|
|
4
5
|
import shutil
|
|
5
6
|
import subprocess
|
|
@@ -365,8 +366,11 @@ def _warmup_seek(client, replay_id: str, offset_ms: int, quiet: bool, attempts:
|
|
|
365
366
|
def session_replay(
|
|
366
367
|
session_id: int = typer.Argument(..., help="Session ID (from `sessions list`)"),
|
|
367
368
|
out: str = typer.Option(None, "--out", help="Output directory (default: ./session-<id>-replay)"),
|
|
368
|
-
|
|
369
|
-
|
|
369
|
+
frames: int = typer.Option(None, "--frames", help="Target number of evenly-spaced frames across the session (default: 60)"),
|
|
370
|
+
fps: float = typer.Option(None, "--fps", help="Fixed sampling rate in frames/sec (alternative to --frames)"),
|
|
371
|
+
interval: int = typer.Option(None, "--interval", help="Fixed sampling interval in ms (alternative to --frames)"),
|
|
372
|
+
all_frames: bool = typer.Option(False, "--all-frames", help="Capture every recorded frame change (event-driven; forensic, can be very large)"),
|
|
373
|
+
dedup: bool = typer.Option(True, "--dedup/--no-dedup", help="Skip consecutive identical frames (on by default)"),
|
|
370
374
|
scale: int = typer.Option(1, "--scale", help="PNG scale factor (1 = full size)"),
|
|
371
375
|
start: float = typer.Option(0, "--from", help="Start offset in seconds"),
|
|
372
376
|
end: float = typer.Option(None, "--to", help="End offset in seconds (default: end of session)"),
|
|
@@ -381,21 +385,34 @@ def session_replay(
|
|
|
381
385
|
Password Safe has no video-export API, so this drives the headless replay
|
|
382
386
|
renderer: it seeks through the recording and pulls one PNG per frame, then
|
|
383
387
|
(with --mp4) stitches them with ffmpeg. Works for RDP (screen) and SSH
|
|
384
|
-
(rendered terminal).
|
|
385
|
-
|
|
388
|
+
(rendered terminal).
|
|
389
|
+
|
|
390
|
+
By default it captures ~60 evenly-spaced frames across the whole session
|
|
391
|
+
(so you see the full timeline, not just the busy start) and skips identical
|
|
392
|
+
frames (--dedup). Use --frames/--fps/--interval to change the sampling rate,
|
|
393
|
+
or --all-frames for the forensic every-recorded-change mode (can be huge -
|
|
394
|
+
RDP records many deltas while the screen paints). One network round-trip per
|
|
395
|
+
frame, so bound long captures with --frames, --from/--to, or --max-frames.
|
|
386
396
|
|
|
387
397
|
Examples:
|
|
388
|
-
bt pws sessions replay 1460 #
|
|
389
|
-
bt pws sessions replay 1460 --
|
|
398
|
+
bt pws sessions replay 1460 # ~60 frames across the session, deduped
|
|
399
|
+
bt pws sessions replay 1460 --frames 30 # ~30 evenly-spaced frames
|
|
390
400
|
bt pws sessions replay 1460 --fps 1 --to 120 # 1 fps for the first 2 minutes
|
|
391
|
-
bt pws sessions replay 1460 --
|
|
401
|
+
bt pws sessions replay 1460 --all-frames --no-dedup # every recorded frame, verbatim
|
|
402
|
+
bt pws sessions replay 1460 --mp4 # also build an MP4
|
|
392
403
|
"""
|
|
393
404
|
quiet = output == "json"
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
405
|
+
# Resolve sampling mode (mutually exclusive). Default = even-coverage frames.
|
|
406
|
+
n_modes = sum(x is not None for x in (frames, fps, interval)) + int(all_frames)
|
|
407
|
+
if n_modes > 1:
|
|
408
|
+
print_error("Choose one of --frames / --fps / --interval / --all-frames.")
|
|
397
409
|
raise typer.Exit(2)
|
|
398
|
-
|
|
410
|
+
|
|
411
|
+
fixed_interval = None # set for fps/interval; computed later for frames mode
|
|
412
|
+
frames_target = None # set for even-coverage frames mode
|
|
413
|
+
if all_frames:
|
|
414
|
+
pass # event-driven: fixed_interval stays None
|
|
415
|
+
elif fps is not None:
|
|
399
416
|
if fps <= 0:
|
|
400
417
|
print_error("--fps must be positive.")
|
|
401
418
|
raise typer.Exit(2)
|
|
@@ -405,6 +422,11 @@ def session_replay(
|
|
|
405
422
|
print_error("--interval must be positive.")
|
|
406
423
|
raise typer.Exit(2)
|
|
407
424
|
fixed_interval = interval
|
|
425
|
+
else:
|
|
426
|
+
frames_target = frames if frames is not None else 60
|
|
427
|
+
if frames_target <= 0:
|
|
428
|
+
print_error("--frames must be positive.")
|
|
429
|
+
raise typer.Exit(2)
|
|
408
430
|
|
|
409
431
|
out_dir = Path(out) if out else Path(f"session-{session_id}-replay")
|
|
410
432
|
replay_id = None
|
|
@@ -435,7 +457,15 @@ def session_replay(
|
|
|
435
457
|
total_ms = state.get("duration") or int(duration_s * 1000)
|
|
436
458
|
end_ms = int(end * 1000) if end is not None else total_ms
|
|
437
459
|
|
|
460
|
+
# Even-coverage mode: spread the target frame count across the span
|
|
461
|
+
# (endpoints included), then capture like a fixed interval.
|
|
462
|
+
if frames_target is not None:
|
|
463
|
+
span = max(0, end_ms - start_ms)
|
|
464
|
+
fixed_interval = max(1, span // frames_target) if frames_target > 1 else span + 1
|
|
465
|
+
|
|
438
466
|
frames: list[dict] = []
|
|
467
|
+
deduped = 0
|
|
468
|
+
last_hash = None
|
|
439
469
|
offset = start_ms
|
|
440
470
|
settle = settle_ms / 1000.0
|
|
441
471
|
while True:
|
|
@@ -450,13 +480,19 @@ def session_replay(
|
|
|
450
480
|
if fixed_interval is None:
|
|
451
481
|
break
|
|
452
482
|
else:
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
483
|
+
digest = hashlib.sha1(png).digest()
|
|
484
|
+
if dedup and digest == last_hash:
|
|
485
|
+
deduped += 1 # identical to the previous kept frame - skip
|
|
486
|
+
else:
|
|
487
|
+
last_hash = digest
|
|
488
|
+
idx = len(frames)
|
|
489
|
+
fname = f"frame_{idx:04d}.png"
|
|
490
|
+
(out_dir / fname).write_bytes(png)
|
|
491
|
+
frames.append({"file": fname, "offset_ms": offset})
|
|
457
492
|
if not quiet:
|
|
458
493
|
pct = min(100, int(offset / total_ms * 100)) if total_ms else 0
|
|
459
|
-
|
|
494
|
+
dd = f" (-{deduped} dup)" if deduped else ""
|
|
495
|
+
console.print(f"\r[cyan] {len(frames)} frames {offset/1000:.1f}s ({pct}%){dd}[/cyan]", end="")
|
|
460
496
|
|
|
461
497
|
# Advance
|
|
462
498
|
if fixed_interval is not None:
|
|
@@ -515,13 +551,15 @@ def session_replay(
|
|
|
515
551
|
"protocol": protocol,
|
|
516
552
|
"output_dir": str(out_dir.resolve()),
|
|
517
553
|
"frame_count": len(frames),
|
|
554
|
+
"deduped": deduped,
|
|
518
555
|
"mp4": str(mp4_path.resolve()) if mp4_path else None,
|
|
519
556
|
}, default=str))
|
|
520
557
|
else:
|
|
521
558
|
if not frames:
|
|
522
559
|
console.print(f"[yellow]No frames captured for session {session_id}.[/yellow]")
|
|
523
560
|
else:
|
|
524
|
-
|
|
561
|
+
dd = f" ({deduped} duplicate(s) skipped)" if deduped else ""
|
|
562
|
+
console.print(f"[green]Captured {len(frames)} frame(s).{dd}[/green]")
|
|
525
563
|
console.print(f" Frames: [cyan]{out_dir.resolve()}[/cyan]")
|
|
526
564
|
if mp4_path:
|
|
527
565
|
console.print(f" MP4: [cyan]{mp4_path.resolve()}[/cyan]")
|
|
@@ -428,7 +428,7 @@ class TestPWSSessionsReplay:
|
|
|
428
428
|
|
|
429
429
|
@respx.mock
|
|
430
430
|
def test_replay_fixed_interval_captures_frames(self, runner, mock_pws_config, tmp_path):
|
|
431
|
-
"""Fixed --interval captures a bounded set of frames + index.json."""
|
|
431
|
+
"""Fixed --interval captures a bounded set of frames + index.json (dedup off)."""
|
|
432
432
|
self._mock_replay({
|
|
433
433
|
"SessionID": 1460, "Token": "tok", "RecordKey": "rk",
|
|
434
434
|
"Protocol": 0, "Duration": 3, "AssetName": "win", "ManagedAccountName": "admin",
|
|
@@ -437,7 +437,7 @@ class TestPWSSessionsReplay:
|
|
|
437
437
|
with patch("bt_cli.pws.client.base.load_pws_config", return_value=mock_pws_config):
|
|
438
438
|
result = runner.invoke(app, [
|
|
439
439
|
"pws", "sessions", "replay", "1460",
|
|
440
|
-
"--interval", "1000", "--to", "3", "--settle", "0", "--out", str(out),
|
|
440
|
+
"--interval", "1000", "--to", "3", "--settle", "0", "--no-dedup", "--out", str(out),
|
|
441
441
|
])
|
|
442
442
|
|
|
443
443
|
assert result.exit_code == 0, result.output
|
|
@@ -448,6 +448,46 @@ class TestPWSSessionsReplay:
|
|
|
448
448
|
assert index["frame_count"] == 4
|
|
449
449
|
assert index["protocol"] == "RDP"
|
|
450
450
|
|
|
451
|
+
@respx.mock
|
|
452
|
+
def test_replay_dedup_collapses_identical_frames(self, runner, mock_pws_config, tmp_path):
|
|
453
|
+
"""Default dedup keeps only one of a run of identical frames."""
|
|
454
|
+
self._mock_replay({
|
|
455
|
+
"SessionID": 1460, "Token": "tok", "RecordKey": "rk",
|
|
456
|
+
"Protocol": 0, "Duration": 3, "AssetName": "win", "ManagedAccountName": "admin",
|
|
457
|
+
})
|
|
458
|
+
out = tmp_path / "r"
|
|
459
|
+
with patch("bt_cli.pws.client.base.load_pws_config", return_value=mock_pws_config):
|
|
460
|
+
result = runner.invoke(app, [
|
|
461
|
+
"pws", "sessions", "replay", "1460",
|
|
462
|
+
"--interval", "1000", "--to", "3", "--settle", "0", "--out", str(out), "-o", "json",
|
|
463
|
+
])
|
|
464
|
+
|
|
465
|
+
assert result.exit_code == 0, result.output
|
|
466
|
+
summary = json.loads(result.output[result.output.index("{"):])
|
|
467
|
+
assert summary["frame_count"] == 1 # all 4 mock frames are identical
|
|
468
|
+
assert summary["deduped"] == 3
|
|
469
|
+
|
|
470
|
+
@respx.mock
|
|
471
|
+
def test_replay_frames_mode_even_coverage(self, runner, mock_pws_config, tmp_path):
|
|
472
|
+
"""Default --frames spreads capture across the whole session span (3000ms per the mock state)."""
|
|
473
|
+
self._mock_replay({
|
|
474
|
+
"SessionID": 1460, "Token": "tok", "RecordKey": "rk",
|
|
475
|
+
"Protocol": 0, "Duration": 3, "AssetName": "win", "ManagedAccountName": "admin",
|
|
476
|
+
})
|
|
477
|
+
out = tmp_path / "r"
|
|
478
|
+
with patch("bt_cli.pws.client.base.load_pws_config", return_value=mock_pws_config):
|
|
479
|
+
result = runner.invoke(app, [
|
|
480
|
+
"pws", "sessions", "replay", "1460",
|
|
481
|
+
"--frames", "5", "--settle", "0", "--no-dedup", "--out", str(out),
|
|
482
|
+
])
|
|
483
|
+
|
|
484
|
+
assert result.exit_code == 0, result.output
|
|
485
|
+
index = json.loads((out / "index.json").read_text())
|
|
486
|
+
offsets = [f["offset_ms"] for f in index["frames"]]
|
|
487
|
+
# frames spread across the 3000ms span, reaching near the end (not clustered at 0)
|
|
488
|
+
assert offsets[0] == 0
|
|
489
|
+
assert offsets[-1] >= 2400
|
|
490
|
+
|
|
451
491
|
@respx.mock
|
|
452
492
|
def test_replay_no_recording(self, runner, mock_pws_config):
|
|
453
493
|
"""A session without a recording (no Token/RecordKey) exits 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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|