Managing Sisense Plugin States with PySisense Overview Enabling and disabling Sisense plugins through the admin interface becomes time consuming when maintaining consistent plugin configurations across environments, testing against a clean instance, or iterating on plugin development. This article describes plugin_manager_pysisense.py , a Python command-line tool that manages Sisense plugin states using the PySisense library and a configuration file. The script is also a working example of how PySisense can be used for Sisense automation. This tool covers the same functionality as plugin_manager.py , documented in Managing Sisense Plugin States via the REST API . The difference is that this version delegates all API request code to PySisense rather than using the requests library directly. The two scripts share the same config.yaml format and produce identical output. The script enables and disables plugins, isolates a single plugin for faster development builds, and saves and restores named snapshots of plugin state. It does not install, uninstall, or update plugins, and does not roll back plugin versions. About PySisense PySisense is introduced in PySisense: Programmable Sisense Environment Management on the Sisense Community as a Python SDK for the Sisense REST API. It provides wrappers around common Sisense operations, including plugin management, dashboard access, and user administration. It is installed via pip and does not need to be placed alongside the script. This script provides a practical example of PySisense applied to a real automation task. Using PySisense rather than writing directly against the REST API reduces the amount of code required and removes several categories of boilerplate. Authentication headers, HTTP session management, SSL configuration, pagination across large result sets, and request logging are all handled by the library. The consistent SisenseClient and module pattern also makes it straightforward to extend a script to cover other Sisense operations, such as users, dashboards, or data models, without reworking connection and authentication code. Scripts built on PySisense are shorter and easier to read than equivalent scripts built on requests directly, and the library is maintained and tested independently of the scripts that use it. Note: The current release of PySisense on PyPI does not yet include the Plugins module used in this script. This functionality is planned for the next PySisense release. The development branch of the PySisense repository already contains it and can be installed directly from there. This script uses the Plugins class from PySisense: get_all_plugins() returns the full plugin list as a list of dicts, handling the pagination in the underlying API automatically. enable_plugins(names, bulk=True) enables one or more plugins by name. When bulk=True , all changes are sent in a single PATCH request. disable_plugins(names, bulk=True) disables one or more plugins by name, with the same bulk option. A SisenseClient instance is constructed from the config values and passed to the Plugins class. All HTTP calls are then handled by PySisense. from pysisense import Plugins, SisenseClient
client = SisenseClient(domain="https://your-sisense-instance.com", token="YOUR_TOKEN")
plugins = Plugins(api_client=client)
# List all installed plugins
all_plugins = plugins.get_all_plugins()
# Enable plugins by short name, full name, or folderName
result = plugins.enable_plugins(["SwitchDimension", "plugin-CustomPlugin"], bulk=True)
# Disable plugins
result = plugins.disable_plugins(["UnneededPlugin"], bulk=True)
Both enable_plugins and disable_plugins return a summary dict with changed , already_enabled or already_disabled , not_found , and errors keys. Features that require knowing the current plugin state before making changes, such as strict mode, enable-one, and dry-run previews, call get_all_plugins() once at the start of the command and compute the set of changes at the script level. Plugin name matching Plugin names in the configuration file and on the command line accept either the short form ( SwitchDimension ) or the full form ( plugin-SwitchDimension ). Matching is case-insensitive. PySisense normalizes names internally, so both forms work with enable_plugins() and disable_plugins() as well. Prerequisites Python 3.10 or later Network access to the Sisense instance (the tool runs remotely and does not need to be installed on the server) A Sisense API token: Admin > REST API > v1.0 > GET /authentication/tokens/api Setup Install the required Python packages: pip install pysisense pyyaml
Create a config.yaml and fill in the Sisense instance URL and API token: sisense:
url: https://your-sisense-instance.com
token: YOUR_API_TOKEN_HERE
Add the plugins to enable to the enable_plugins list. See the Configuration reference section for all available settings. The script is then ready to run. Configuration reference sisense:
url: https://your-sisense-instance.com
token: YOUR_API_TOKEN_HERE # Admin → REST API → v1.0 → GET /authentication/tokens/api
bulk: true # set false to update plugins one at a time instead of a single batch request
# Plugins to enable.
# Accepts short names (SwitchDimension) or full names (plugin-SwitchDimension).
# Matching is case-insensitive.
#
# Default behavior: enable listed plugins; leave every other plugin unchanged.
# With --strict: also disable any currently-enabled plugin NOT in this list,
# making the instance exactly match this config.
enable_plugins:
- examplePlugin
# Plugins to explicitly disable (optional).
# Only needed in the default sync (no --strict), where unlisted plugins are
# left alone. Listing a plugin here ensures it stays off and documents the
# intentional exclusion.
# disable_plugins:
# - anotherExamplePlugin
enable_plugins : the list of plugins to enable. By default, plugins not on this list are left in their current state. With --strict , any currently enabled plugin not on this list will be disabled. disable_plugins : optional. Plugins to explicitly disable in the default sync without --strict . Useful for documenting intentional exclusions when strict mode is not being used. bulk : controls whether plugin state changes are sent as a single batch API request or one at a time. Set to false for individual requests, which is slower but can be useful for diagnosing issues with specific plugins. Usage Run the script with no arguments to enable plugins in enable_plugins , disable plugins in disable_plugins , and leave everything else unchanged: python plugin_manager_pysisense.py Before making any changes, the script saves a timestamped snapshot of the current plugin state. To restore the most recent snapshot: python plugin_manager_pysisense.py --restore-snapshot To preview what would change without making any API calls: python plugin_manager_pysisense.py --dry-run Common use cases Maintaining a known good plugin baseline Organizations that need to ensure consistent plugin configuration across upgrades, support sessions, or environment changes can maintain an enable_plugins list in the configuration file. Running the script applies the defined configuration to the instance. To also disable any currently enabled plugin not in the list: python plugin_manager_pysisense.py --strict Testing on a stock Sisense instance Plugin interference is a common source of issues that are difficult to reproduce. To quickly disable all plugins and test against a clean instance, clear enable_plugins in the configuration file and run with --strict . The current state is saved automatically before any changes are made, and can be restored when testing is complete: # with enable_plugins cleared in config.yaml
python plugin_manager_pysisense.py --strict
# restore when done
python plugin_manager_pysisense.py --restore-snapshot Developing and testing a single plugin When many plugins are installed, Sisense rebuilds all of them whenever any plugin changes. Using --enable-one disables all currently enabled plugins and enables only the specified plugin, which can significantly reduce rebuild time on instances with many plugins: python plugin_manager_pysisense.py --enable-one MyPlugin Snapshots The script saves snapshots as YAML files to a snapshots/ folder alongside the script. Snapshots capture the full enabled and disabled plugin state at a point in time. Snapshots are saved automatically before any run that changes state. Named snapshots can also be saved and restored explicitly: # save the current state with a specific name
python plugin_manager_pysisense.py --save-snapshot before-upgrade
# list all saved snapshots
python plugin_manager_pysisense.py --list-snapshots
# restore a specific snapshot
python plugin_manager_pysisense.py --restore-snapshot before-upgrade Including --log-version when saving a snapshot records the version of each plugin at that point in time. Snapshots saved by plugin_manager_pysisense.py and plugin_manager.py are compatible. Both scripts read from the same snapshots/ directory, and each handles the other's snapshot format. Full option reference usage: plugin_manager_pysisense.py [-h] [--config FILE] [--dry-run] [--no-bulk]
[--skip-snapshot] [--log-version] [--strict]
[--enable-only | --disable-only]
[--enable-one PLUGIN | --save-snapshot [NAME] |
--list-snapshots | --restore-snapshot [NAME]]
Manage Sisense plugin states via PySisense.
options:
-h, --help show this help message and exit
--config FILE Path to config.yaml (default: config.yaml next to this
script)
--dry-run Preview what would change; make no API calls and save
no snapshot
--no-bulk Send one API call per plugin instead of a single batch
PATCH. Slower but easier to diagnose partial failures.
Overrides sisense.bulk in config.
--skip-snapshot Do not automatically save a snapshot before this run
--log-version Record each plugin's version field (from the API
response) in any snapshot saved during this run
--strict Also disable every currently enabled plugin that is
not listed in enable_plugins. Without this flag, only
plugins in disable_plugins are disabled; everything
else is left alone. Always active for --restore-snapshot.
--enable-only Run only the enable step; skip all disables
--disable-only Run only the disable step; skip all enables
--enable-one PLUGIN Enable exactly one plugin by name and disable all
currently enabled others. Accepts the short or full
plugin name.
--save-snapshot [NAME]
Save the current plugin state as a named snapshot.
Omit NAME for an auto generated timestamp filename.
Combine with --log-version to also record versions.
--list-snapshots List all saved snapshots with creation date and
enabled plugin count
--restore-snapshot [NAME]
Restore plugin state to exactly match a saved snapshot.
Omit NAME to use the most recently created snapshot.
Always strict (disables plugins not in snapshot)
unless --enable-only.
Plugin names accept the short form (SwitchDimension) or the full
form (plugin-SwitchDimension) in both CLI args and config.yaml.
Default behavior: enable plugins in enable_plugins, disable plugins
in disable_plugins, leave everything else unchanged.
Use --strict to also disable any enabled plugin not in enable_plugins.
A snapshot is saved automatically before each run that
modifies state. Use --skip-snapshot to suppress this.
Use Case Managing plugins via PySisense and a configuration file offers several advantages over manual administration: Applies plugin active list consistently and repeatably without manual steps Provides automatic rollback through snapshots saved before each change Supports quickly switching between plugin states for testing or development Reduces the time required to restore a known working plugin list after an issue is resolved Demonstrates how PySisense can be used to build automation scripts for Sisense instances Summary With this tool in place, administrators and developers can manage Sisense plugin states quickly and reliably from the command line. The configuration file serves as a single source of truth for the intended plugin set, and the snapshot system ensures that any change can be reversed. The script also serves as a concrete example of how PySisense can be used in automation scripts: constructing a SisenseClient from config values, passing it to a feature class, and using the returned result dicts to drive output and error handling. The code is not compressed or obfuscated and can be modified as needed or used as a reference for similar tools. j"""
plugin_manager_pysisense.py — Manage Sisense plugin states via pySisense.
Same feature set as plugin_manager.py but delegates all API calls to the
pySisense library instead of calling the Sisense REST API directly.
Configuration is read from config.yaml.
This script is also a working example of how pySisense can be used for
Sisense automation. Install pySisense with: pip install pysisense
Overview
Manages Sisense plugin states via pySisense. Treats plugin configuration
as code: define which plugins should be enabled in config.yaml, run the script,
and the instance matches, quickly, from anywhere with network access.
Before applying any changes, the current plugin state is automatically captured
in a timestamped snapshot. If a change needs to be reverted, a single
--restore-snapshot command rolls the instance back to exactly how it was.
By default only the plugins explicitly listed in config.yaml are enabled or
disabled, everything else is left as is. Use --strict to also disable any
enabled plugin not in the list, making the instance enabled plugin list
exactly match the config.
Common use cases
Maintain a known good plugin baseline
Keep enable_plugins in config.yaml up to date. Run the script after
upgrades, support sessions, or any time the instance may have drifted
from the expected state.
Test against a stock (no plugins) Sisense instance
Clear enable_plugins in config.yaml, run --strict to disable everything,
test, then restore:
python plugin_manager_pysisense.py --strict
python plugin_manager_pysisense.py --restore-snapshot
Develop and iterate on a single plugin quickly
python plugin_manager_pysisense.py --enable-one MyPlugin
Roll back any change instantly
python plugin_manager_pysisense.py --list-snapshots
python plugin_manager_pysisense.py --restore-snapshot before-upgrade
Usage
# Enable listed plugins, disable listed disable_plugins, leave rest unchanged
python plugin_manager_pysisense.py
# Strict: also disables any enabled plugin not in enable_plugins
python plugin_manager_pysisense.py --strict
# Preview what would change (no API calls, no auto snapshot)
python plugin_manager_pysisense.py --dry-run
python plugin_manager_pysisense.py --strict --dry-run
# Run only one half of the sync
python plugin_manager_pysisense.py --enable-only # enable listed; skip all disables
python plugin_manager_pysisense.py --disable-only # disable listed; skip all enables
# Enable exactly one plugin and disable every other currently enabled plugin
python plugin_manager_pysisense.py --enable-one SwitchDimension
# Snapshots
python plugin_manager_pysisense.py --save-snapshot [NAME]
python plugin_manager_pysisense.py --save-snapshot [NAME] --log-version
python plugin_manager_pysisense.py --list-snapshots
python plugin_manager_pysisense.py --restore-snapshot [NAME] # omit NAME for most recent
# Other flags (combinable with any of the above)
--no-bulk one API call per plugin instead of a single batch request
--skip-snapshot do not save an auto-snapshot before this run
--log-version record each plugin's version in any snapshot saved this run
--config FILE use a config file other than the default config.yaml
Plugin names
All plugin names accept either the short form (SwitchDimension) or the full
form (plugin-SwitchDimension), both in CLI arguments and in config.yaml.
Matching is case insensitive.
API token
Admin → REST API → v1.0 → GET /authentication/tokens/api
Setup
pip install pysisense pyyaml # Python 3.10+ required
cp config.yaml.example config.yaml
# Edit config.yaml: set sisense.url, sisense.token, and enable_plugins list
"""
import sys
from datetime import datetime, timezone
from pathlib import Path
import yaml
from pysisense import Plugins, SisenseClient
SCRIPT_DIR = Path(__file__).parent
DEFAULT_CONFIG = SCRIPT_DIR / "config.yaml"
SNAPSHOTS_DIR = SCRIPT_DIR / "snapshots"
# Config
def load_config(path: Path) -> dict:
if not path.exists():
print(f"Config file not found: {path}")
print("Copy config.yaml.example to config.yaml and fill in your instance URL and API token.")
sys.exit(1)
with path.open() as f:
return yaml.safe_load(f)
def build_client(sisense_cfg: dict) -> SisenseClient:
url = sisense_cfg.get("url", "")
token = sisense_cfg.get("token", "")
if not url or not token:
print("config.yaml must include sisense.url and sisense.token.")
sys.exit(1)
is_ssl = not url.startswith("http://")
return SisenseClient(domain=url, token=token, is_ssl=is_ssl)
# Name matching — mirrors pySisense internals, used for dry-run previews and
# strict-mode computation (pySisense handles matching internally for real calls)
def _normalize(name: str) -> str:
lower = name.lower()
return lower[len("plugin-"):] if lower.startswith("plugin-") else lower
def _build_lookup(names: list) -> set:
lookup: set = set()
for n in names:
norm = _normalize(n)
lookup.add(norm)
lookup.add("plugin-" + norm)
return lookup
def _matches_lookup(plugin: dict, lookup: set) -> bool:
return bool({plugin["folderName"].lower(), plugin["name"].lower()} & lookup)
# Snapshot file I/O
def write_snapshot_file(name: str, snapshot: dict) -> Path:
SNAPSHOTS_DIR.mkdir(exist_ok=True)
path = SNAPSHOTS_DIR / f"{name}.yaml"
with path.open("w") as f:
yaml.dump(snapshot, f, sort_keys=False, default_flow_style=False, allow_unicode=True)
return path
def read_snapshot_file(name: str) -> dict:
path = SNAPSHOTS_DIR / f"{name}.yaml"
if not path.exists():
print(f"Snapshot not found: {path}")
sys.exit(1)
with path.open() as f:
data = yaml.safe_load(f)
# Translate 'enable_plugins' key (plugin_manager.py snapshot format) to 'plugins'
if "enable_plugins" in data and "plugins" not in data:
data["plugins"] = data["enable_plugins"]
return data
def list_snapshot_files() -> list:
if not SNAPSHOTS_DIR.exists():
return []
return sorted(SNAPSHOTS_DIR.glob("*.yaml"))
def latest_snapshot_name() -> str:
files = list_snapshot_files()
if not files:
print("No snapshots found.")
sys.exit(1)
def created_at(path: Path) -> str:
with path.open() as f:
return yaml.safe_load(f).get("created", "")
return max(files, key=created_at).stem
def _auto_save_snapshot(all_plugins: list, include_version: bool) -> None:
name = datetime.now().strftime("auto_%Y%m%d_%H%M%S")
print(f"\nSaving snapshot '{name}' before applying changes...")
try:
enabled = sorted(p["folderName"] for p in all_plugins if p.get("isEnabled"))
disabled = sorted(p["folderName"] for p in all_plugins if not p.get("isEnabled"))
snapshot: dict = {
"plugins": enabled,
"created": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
}
if include_version:
versions: dict = {}
for p in all_plugins:
v = p.get("version") or p.get("pluginVersion")
if v:
versions[p["folderName"]] = str(v)
if versions:
snapshot["plugin_versions"] = versions
path = write_snapshot_file(name, snapshot)
print(f" Saved to {path} ({len(enabled)} enabled / {len(disabled)} disabled)")
except Exception as exc:
print(f" Warning: auto snapshot failed: {exc}")
# Sync computation — determines which plugins need their state changed.
# Used for dry-run previews, strict mode, and enable-one.
def _compute_updates(
all_plugins: list,
enable_names: list,
disable_names: list,
*,
strict: bool,
enable_only: bool,
disable_only: bool,
) -> tuple:
enable_lookup = _build_lookup(enable_names)
disable_lookup = _build_lookup(disable_names)
matched_enable = [p for p in all_plugins if _matches_lookup(p, enable_lookup)]
enable_folder_set = {p["folderName"] for p in matched_enable}
to_enable: list = [] if disable_only else sorted(
p["folderName"] for p in matched_enable if not p.get("isEnabled")
)
if enable_only:
to_disable: list = []
elif strict:
to_disable = sorted(
p["folderName"] for p in all_plugins
if p.get("isEnabled") and p["folderName"] not in enable_folder_set
)
else:
to_disable = sorted(
p["folderName"] for p in all_plugins
if p.get("isEnabled") and _matches_lookup(p, disable_lookup)
)
matched_norms = (
{_normalize(p["folderName"]) for p in matched_enable} |
{_normalize(p["name"]) for p in matched_enable}
)
unmatched = sorted(n for n in enable_names if _normalize(n) not in matched_norms)
return to_enable, to_disable, unmatched
# Output helpers
def _print_update_plan(
to_enable: list,
to_disable: list,
already_correct: int,
untouched: int = 0,
) -> None:
print(f" To enable: {len(to_enable)}")
print(f" To disable: {len(to_disable)}")
if already_correct > 0:
print(f" Already set: {already_correct}")
if untouched > 0:
print(f" Untouched: {untouched} (not listed in config; left as-is)")
print()
def _print_dry_run_preview(to_enable: list, to_disable: list) -> None:
if not to_enable and not to_disable:
print("Nothing to change.")
if to_enable:
print("Would enable:")
for f in to_enable:
print(f" + {f}")
if to_disable:
print("Would disable:")
for f in to_disable:
print(f" - {f}")
print("\n[DRY RUN] No changes made.")
def _report_unmatched(unmatched: list) -> None:
if unmatched:
print(f"\nNot found in instance ({len(unmatched)}):")
for n in unmatched:
print(f" - {n}")
def _apply_enable(plugins_obj: Plugins, to_enable: list, bulk: bool) -> int:
if not to_enable:
return 0
result = plugins_obj.enable_plugins(to_enable, bulk=bulk)
if "error" in result:
print(f"Failed to enable plugins: {result['error']}")
sys.exit(1)
for f in result["changed"]:
print(f" [ENABLED] {f}")
return len(result.get("errors", []))
def _apply_disable(plugins_obj: Plugins, to_disable: list, bulk: bool) -> int:
if not to_disable:
return 0
result = plugins_obj.disable_plugins(to_disable, bulk=bulk)
if "error" in result:
print(f"Failed to disable plugins: {result['error']}")
sys.exit(1)
for f in result["changed"]:
print(f" [DISABLED] {f}")
return len(result.get("errors", []))
# Commands
def cmd_sync_to_config(
plugins_obj: Plugins,
enable_names: list,
disable_names: list,
*,
dry_run: bool,
bulk: bool,
strict: bool,
enable_only: bool,
disable_only: bool,
skip_snapshot: bool,
include_version: bool,
) -> None:
print("Fetching plugin list...")
all_plugins = plugins_obj.get_all_plugins()
if all_plugins and "error" in all_plugins[0]:
print(f"Failed to fetch plugins: {all_plugins[0]['error']}")
sys.exit(1)
print(f" {len(all_plugins)} plugins total\n")
to_enable, to_disable, unmatched = _compute_updates(
all_plugins, enable_names, disable_names,
strict=strict, enable_only=enable_only, disable_only=disable_only,
)
enable_lookup = _build_lookup(enable_names)
disable_lookup = _build_lookup(disable_names)
matched = [p for p in all_plugins if _matches_lookup(p, enable_lookup)]
already_correct = sum(1 for p in matched if p.get("isEnabled"))
untouched = 0 if strict or enable_only else sum(
1 for p in all_plugins
if not _matches_lookup(p, enable_lookup) and not _matches_lookup(p, disable_lookup)
)
if matched:
print(f"Matched {len(matched)} plugin(s) from enable_plugins:\n")
print(f" {'Folder name':<45} {'API name':<35} {'Enabled'}")
print(f" {'-'*45} {'-'*35} {'-'*7}")
for p in sorted(matched, key=lambda x: x["folderName"]):
print(f" {p['folderName']:<45} {p['name']:<35} {p['isEnabled']}")
print()
_print_update_plan(to_enable, to_disable, already_correct, untouched)
if dry_run:
_print_dry_run_preview(to_enable, to_disable)
_report_unmatched(unmatched)
return
if not skip_snapshot and (to_enable or to_disable):
_auto_save_snapshot(all_plugins, include_version)
errors = _apply_disable(plugins_obj, to_disable, bulk) + _apply_enable(plugins_obj, to_enable, bulk)
if not to_enable and not to_disable:
print("Nothing to change.")
else:
print(f"\nDone. Enabled: {len(to_enable)} Disabled: {len(to_disable)} Errors: {errors}")
_report_unmatched(unmatched)
def cmd_enable_one(
plugins_obj: Plugins,
plugin_name: str,
*,
dry_run: bool,
bulk: bool,
skip_snapshot: bool,
include_version: bool,
) -> None:
target_lookup = _build_lookup([plugin_name])
print("Fetching plugin list...")
all_plugins = plugins_obj.get_all_plugins()
if all_plugins and "error" in all_plugins[0]:
print(f"Failed to fetch plugins: {all_plugins[0]['error']}")
sys.exit(1)
print(f" {len(all_plugins)} plugins total\n")
target = next((p for p in all_plugins if _matches_lookup(p, target_lookup)), None)
if target is None:
print(f"Plugin not found: {plugin_name!r}")
print("Tip: check spelling against the Sisense admin panel, or run --save-snapshot to capture current names.")
sys.exit(1)
target_folder = target["folderName"]
target_on = target.get("isEnabled", False)
to_enable = [] if target_on else [target_folder]
to_disable = sorted(
p["folderName"] for p in all_plugins
if p.get("isEnabled") and p["folderName"] != target_folder
)
print(f"Target plugin: {target_folder}")
print(f" Currently: {'enabled' if target_on else 'disabled'}\n")
_print_update_plan(to_enable, to_disable, already_correct=1 if target_on else 0)
if dry_run:
_print_dry_run_preview(to_enable, to_disable)
return
if not skip_snapshot and (to_enable or to_disable):
_auto_save_snapshot(all_plugins, include_version)
errors = _apply_disable(plugins_obj, to_disable, bulk) + _apply_enable(plugins_obj, to_enable, bulk)
if not to_enable and not to_disable:
print("Nothing to change.")
else:
print(f"\nDone. Enabled: {len(to_enable)} Disabled: {len(to_disable)} Errors: {errors}")
def cmd_save_named_snapshot(plugins_obj: Plugins, name: str, include_version: bool) -> None:
print("Fetching plugin list...")
all_plugins = plugins_obj.get_all_plugins()
if all_plugins and "error" in all_plugins[0]:
print(f"Failed to fetch plugins: {all_plugins[0]['error']}")
sys.exit(1)
print(f" {len(all_plugins)} plugins total\n")
enabled = sorted(p["folderName"] for p in all_plugins if p.get("isEnabled"))
disabled = sorted(p["folderName"] for p in all_plugins if not p.get("isEnabled"))
snapshot: dict = {
"plugins": enabled,
"created": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
}
if include_version:
versions: dict = {}
for p in all_plugins:
v = p.get("version") or p.get("pluginVersion")
if v:
versions[p["folderName"]] = str(v)
if versions:
snapshot["plugin_versions"] = versions
path = write_snapshot_file(name, snapshot)
print(f"Snapshot '{name}' saved to {path}")
print(f" {len(enabled)} enabled / {len(disabled)} disabled plugin(s) captured.")
if include_version and snapshot.get("plugin_versions"):
print(f" Versions recorded for {len(snapshot['plugin_versions'])} plugin(s).")
def cmd_list_snapshots() -> None:
files = list_snapshot_files()
if not files:
print("No snapshots found.")
return
print(f"Snapshots ({len(files)}):\n")
print(f" {'Name':<32} {'Created':<22} {'Enabled':<9} {'Versions'}")
print(f" {'-'*32} {'-'*22} {'-'*9} {'-'*8}")
for path in files:
with path.open() as f:
data = yaml.safe_load(f)
enabled_count = len(data.get("plugins") or data.get("enable_plugins", []))
versions_logged = "yes" if data.get("plugin_versions") else "no"
print(
f" {path.stem:<32} "
f"{data.get('created', 'unknown'):<22} "
f"{enabled_count:<9} "
f"{versions_logged}"
)
def cmd_restore_snapshot(
plugins_obj: Plugins,
name: str,
*,
dry_run: bool,
bulk: bool,
enable_only: bool,
disable_only: bool,
skip_snapshot: bool,
include_version: bool,
) -> None:
snapshot = read_snapshot_file(name)
snapshot_plugins = set(snapshot.get("plugins", []))
print(f"Snapshot '{name}' ({snapshot.get('created', 'unknown')})")
print(f" {len(snapshot_plugins)} plugin(s) enabled in snapshot")
if snapshot.get("plugin_versions"):
print(f" Plugin versions recorded: {len(snapshot['plugin_versions'])}")
print()
print("Fetching plugin list...")
all_plugins = plugins_obj.get_all_plugins()
if all_plugins and "error" in all_plugins[0]:
print(f"Failed to fetch plugins: {all_plugins[0]['error']}")
sys.exit(1)
print(f" {len(all_plugins)} plugins total\n")
by_folder = {p["folderName"]: p for p in all_plugins}
to_enable: list = [] if disable_only else sorted(
f for f in snapshot_plugins if f in by_folder and not by_folder[f].get("isEnabled")
)
to_disable: list = [] if enable_only else sorted(
p["folderName"] for p in all_plugins
if p.get("isEnabled") and p["folderName"] not in snapshot_plugins
)
not_in_instance = sorted(f for f in snapshot_plugins if f not in by_folder)
already_correct = max(0, len(snapshot_plugins) - len(to_enable) - len(not_in_instance))
_print_update_plan(to_enable, to_disable, already_correct)
if dry_run:
_print_dry_run_preview(to_enable, to_disable)
if not_in_instance:
print(f"\nNot found in instance ({len(not_in_instance)}):")
for f in not_in_instance:
print(f" - {f}")
return
if not skip_snapshot and (to_enable or to_disable):
_auto_save_snapshot(all_plugins, include_version)
errors = _apply_disable(plugins_obj, to_disable, bulk) + _apply_enable(plugins_obj, to_enable, bulk)
if not to_enable and not to_disable:
print("Nothing to change.")
else:
print(f"\nDone. Enabled: {len(to_enable)} Disabled: {len(to_disable)} Errors: {errors}")
if not_in_instance:
print(f"\nNot found in instance ({len(not_in_instance)}):")
for f in not_in_instance:
print(f" - {f}")
# Main
def main() -> None:
import argparse
parser = argparse.ArgumentParser(
prog="plugin_manager_pysisense.py",
description="Manage Sisense plugin states via pySisense.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=(
"Plugin names accept the short form (SwitchDimension) or the full\n"
"form (plugin-SwitchDimension) in both CLI args and config.yaml.\n"
"\n"
"Default behavior: enable plugins in enable_plugins, disable plugins\n"
"in disable_plugins, leave everything else unchanged.\n"
"Use --strict to also disable any enabled plugin not in enable_plugins.\n"
"\n"
"A snapshot is saved automatically before each run that\n"
"modifies state. Use --skip-snapshot to suppress this."
),
)
parser.add_argument(
"--config", default=str(DEFAULT_CONFIG), metavar="FILE",
help="Path to config.yaml (default: config.yaml next to this script)",
)
parser.add_argument(
"--dry-run", action="store_true",
help="Preview what would change; make no API calls and save no snapshot",
)
parser.add_argument(
"--no-bulk", action="store_true",
help=(
"Send one API call per plugin instead of a single batch PATCH. "
"Slower but easier to diagnose partial failures. "
"Overrides sisense.bulk in config."
),
)
parser.add_argument(
"--skip-snapshot", action="store_true",
help="Do not automatically save a snapshot before this run",
)
parser.add_argument(
"--log-version", action="store_true",
help=(
"Record each plugin's version field (from the API response) in any "
"snapshot saved during this run (manual --save-snapshot or auto saved snapshot)"
),
)
parser.add_argument(
"--strict", action="store_true",
help=(
"Also disable every currently enabled plugin that is not listed in "
"enable_plugins. Without this flag, only plugins in disable_plugins are "
"disabled; everything else is left alone. "
"Always active for --restore-snapshot."
),
)
direction_group = parser.add_mutually_exclusive_group()
direction_group.add_argument(
"--enable-only", action="store_true",
help="Run only the enable step; skip all disables",
)
direction_group.add_argument(
"--disable-only", action="store_true",
help="Run only the disable step; skip all enables",
)
mode_group = parser.add_mutually_exclusive_group()
mode_group.add_argument(
"--enable-one", metavar="PLUGIN",
help=(
"Enable exactly one plugin by name and disable all currently enabled "
"others. Accepts the short or full plugin name."
),
)
mode_group.add_argument(
"--save-snapshot", metavar="NAME", nargs="?", const="__auto__",
help=(
"Save the current plugin state as a named snapshot. "
"Omit NAME for an auto generated timestamp filename. "
"Combine with --log-version to also record versions for each plugin."
),
)
mode_group.add_argument(
"--list-snapshots", action="store_true",
help="List all saved snapshots with creation date and enabled plugin count",
)
mode_group.add_argument(
"--restore-snapshot", metavar="NAME", nargs="?", const="__latest__",
help=(
"Restore plugin state to exactly match a saved snapshot. "
"Omit NAME to use the most recently created snapshot. "
"Always strict (disables plugins not in snapshot) unless --enable-only."
),
)
args = parser.parse_args()
if args.list_snapshots:
cmd_list_snapshots()
return
cfg = load_config(Path(args.config))
sisense_cfg = cfg.get("sisense", {})
bulk = sisense_cfg.get("bulk", True) and not args.no_bulk
client = build_client(sisense_cfg)
plugins_obj = Plugins(api_client=client)
if args.enable_one:
cmd_enable_one(
plugins_obj, args.enable_one,
dry_run=args.dry_run,
bulk=bulk,
skip_snapshot=args.skip_snapshot,
include_version=args.log_version,
)
elif args.save_snapshot is not None:
name = (
datetime.now().strftime("snapshot_%Y%m%d_%H%M%S")
if args.save_snapshot == "__auto__"
else args.save_snapshot
)
cmd_save_named_snapshot(plugins_obj, name, args.log_version)
elif args.restore_snapshot is not None:
name = (
latest_snapshot_name()
if args.restore_snapshot == "__latest__"
else args.restore_snapshot
)
cmd_restore_snapshot(
plugins_obj, name,
dry_run=args.dry_run,
bulk=bulk,
enable_only=args.enable_only,
disable_only=args.disable_only,
skip_snapshot=args.skip_snapshot,
include_version=args.log_version,
)
else:
enable_names = cfg.get("enable_plugins") or cfg.get("plugins", [])
disable_names = cfg.get("disable_plugins", [])
if not enable_names and not disable_names:
print("config.yaml must include at least one entry in 'enable_plugins'.")
sys.exit(1)
cmd_sync_to_config(
plugins_obj, enable_names, disable_names,
dry_run=args.dry_run,
bulk=bulk,
strict=args.strict,
enable_only=args.enable_only,
disable_only=args.disable_only,
skip_snapshot=args.skip_snapshot,
include_version=args.log_version,
)
if __name__ == "__main__":
main() Disclaimer: This post outlines a potential custom workaround for a specific use case or provides instructions regarding a specific task. The solution may not work in all scenarios or Sisense versions, so we strongly recommend testing it in your environment before deployment. If you need further assistance with this, please let us know.