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