Modifying Sisense Widgets With PySisense
Summary: Jeremy Friedel provides an overview of using the `swap_widget_dimension.py` script to efficiently modify Sisense chart widgets via the PySisense SDK. The script allows users to swap out fields in widget panels automatically, offering a more streamlined alternative to manual edits. It emphasizes the importance of confirming widget structures and provides guidelines on setup, usage, and troubleshooting. The tool also features options for previewing changes, backing up, and restoring prior states if needed, making it suitable for Sisense administrators and developers. The discussed method ensures that migration between different dashboard versions or correcting fields post-templating is simplified.
Modifying Sisense Widgets With PySisense
Overview
A chart widget's metadata, including the dimension driving its category axis and the measures driving its values, lives inside the widget's own JSON definition rather than on a separate settings screen. Changing one, through the widget's Edit panel, can be done quickly. Mass modifying the same field across many widgets, or repeating the same change every time a dashboard is rebuilt from a template, is not something the Sisense UI is built to do in bulk.
This article describes swap_widget_dimension.py, a Python command-line tool that reads a Sisense chart widget, replaces the field used by one item on a named panel, and writes the result back using the pySisense library. By default it targets the categories panel, a bar chart's category axis, but the same get_widget_by_id and update_widget calls apply to any panel in the widget's metadata.panels structure, including values, so the tool also works for swapping a measure. The worked example throughout is a bar chart with two versions on a dashboard that differ only in their measure: one totals Revenue, the other totals Cost.
The script changes only the field of one panel item on one widget per run. It is meant primarily as an example, and can be adapted to cover other widget attributes as needed.
The full source is available on GitHub in this repository.
About PySisense
PySisense is a Python SDK for the Sisense REST API, installed via pip. It provides wrappers around common Sisense operations and handles authentication, session management, and request errors internally. This script uses three methods from pySisense's Dashboard class: get_widget_by_id, which retrieves a widget's full current definition; update_widget, which writes a modified definition back and strips the fields Sisense manages internally (oid, owner, created, and so on) before sending the request; and, through Dashboard's access_mgmt attribute, get_my_user, which identifies the Sisense user the API token belongs to.
Note: The current release of PySisense on PyPI does not yet include get_widget_by_id, update_widget, or find_widgets_by_type. 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:
How a chart widget stores its fields
A Sisense chart widget's configuration lives under metadata.panels, a list of panel objects. For a bar chart, the panel named categories holds the dimension shown on the category axis, and the panel named values holds the measures. Each panel has an items list, and each item carries a jaql object identifying one field, with table, column, and dim keys (dim in the form [Table.Column]), plus a title and other fields, such as datatype or agg, that vary by field type.
Panel names and item order can differ by widget type and by how a chart was originally built, so it's worth confirming the structure on the specific widget being changed rather than assuming it. Running the script with --show prints a widget's current panels for exactly this purpose, before anything is changed:
Prerequisites
Python 3.10 or later
pySisense (for now, currently the dev branch, this will be merged into the main branch shortly) and
PyYAMLNetwork access to the Sisense instance
A Sisense API token: Admin > REST API > v1.0 >
GET /authentication/tokens/apiOnly a dashboard's owner can write to its widgets. If the API token's user does not own the dashboard being changed, use a token belonging to the dashboard's owner, or transfer ownership first with pySisense's
change_dashboard_owner.
Setup
Create a config.yaml with the Sisense instance details, the widget to change, and the field to switch to:
sisense:
domain: https://your-instance.sisense.com
token: YOUR_API_TOKEN_HERE
is_ssl: true
widget:
dashboard_id: YOUR_DASHBOARD_ID
widget_id: YOUR_WIDGET_ID
dimension:
table: Commerce
column: Region
title: RegionRun with --show first to confirm the widget has the panel expected, then continue with the sequence in Usage.
Configuration reference
sisense.domain, sisense.token, sisense.is_ssl: connection details for the Sisense instance.
widget.dashboard_id, widget.widget_id: the widget to change. Both can be overridden for a single run with --dashboard-id and --widget-id without editing config.yaml.
widget.admin_access: left unset by default, which auto-detects the right setting (see Troubleshooting below). Set to true or false, or pass --admin-access or --no-admin-access, only to skip that auto-detection.
dimension.panel: the metadata.panels entry to change. categories for a bar chart's category axis, values for its measures. Defaults to categories if unset. Can be overridden with --panel.
dimension.table, dimension.column, dimension.title: the new field to apply. table and column identify it; title is the label shown on the widget and defaults to column when omitted. All three can be overridden with --table, --column, and --title.
Usage
Inspect the widget's current panels before changing anything:
Preview the change without writing it:
Apply the change:
If the result is not correct, revert to the widget's prior state:
Troubleshooting
Whether adminAccess is needed depends on ownership, not the Sisense version. get_widget_by_id can fail with a schema validation error mentioning adminAccess, on Sisense instances that reject the parameter outright, or with a permission error, on instances that require it for a widget the API token's user doesn't own. Rather than have the caller guess which applies, the script tries without admin access first, and only retries with admin access if that fails. It also prints a line identifying the token's user and whether that user owns the widget, so the reason either attempt succeeded is visible. --admin-access and --no-admin-access are still available to force one path and skip the auto-detection.
Worked example: swapping a measure from Revenue to Cost
A dashboard can have two versions of the same bar chart widget that differ only in their values panel, one totaling Commerce.Revenue, the other Commerce.Cost. Pointing the script at the Revenue widget and targeting the values panel swaps it to Cost, keeping its sum aggregation intact:
Running --show against the widget beforehand returns:
{
"table": "Commerce",
"column": "Revenue",
"dim": "[Commerce.Revenue]",
"datatype": "numeric",
"columnTitle": "Revenue",
"tableTitle": "Commerce",
"agg": "sum",
"title": "Total Revenue"
}and after --apply:
{
"table": "Commerce",
"column": "Cost",
"dim": "[Commerce.Cost]",
"datatype": "numeric",
"columnTitle": "Cost",
"tableTitle": "Commerce",
"agg": "sum",
"title": "Total Cost"
}table, column, dim, columnTitle, tableTitle, and title all changed to Cost; datatype and the sum aggregation in agg carried over unchanged, exactly as build_new_jaql is designed to do.
Common use cases
Reusing one chart across dashboard variants
A bar chart that should show a different measure, or a different category dimension, on each of several dashboards can be built once and adjusted per dashboard with a single command instead of being rebuilt in the widget editor each time.
Correcting a field after templating
Dashboards built from a template sometimes carry the template's placeholder field. Rather than reopening every affected widget by hand, each one can be pointed at the correct field directly.
Working with more than one item on a panel
A widget with more than one item on the panel being changed, such as a grouped bar chart with two category dimensions, needs --match-column to identify which one to replace:
Full option reference
usage: swap_widget_dimension.py [-h] [--config FILE] [--dashboard-id ID]
[--widget-id ID] [--panel PANEL]
[--table TABLE] [--column COLUMN]
[--title TITLE] [--match-column COLUMN]
[--show] [--apply] [--undo]
[--backup-file FILE] [--admin-access |
--no-admin-access]
Swap the field used by one item on a Sisense chart widget panel.
options:
-h, --help show this help message and exit
--config FILE Path to config.yaml.
--dashboard-id ID Overrides widget.dashboard_id from config.yaml.
--widget-id ID Overrides widget.widget_id from config.yaml.
--panel PANEL Name of the metadata.panels entry to change, for
example categories (a bar chart's category axis) or
values (its measures). Overrides dimension.panel from
config.yaml. Defaults to 'categories' if neither is
set.
--table TABLE Overrides dimension.table from config.yaml.
--column COLUMN Overrides dimension.column from config.yaml.
--title TITLE Overrides dimension.title from config.yaml.
--match-column COLUMN
Column name of the item to replace within --panel, if
it has more than one item. Defaults to the first item.
--show Print the widget's current metadata.panels and exit.
Use this to confirm panel names and item order before
swapping anything.
--apply Write the change to Sisense. Without this flag, only a
preview is printed.
--undo Restore the widget to the state recorded in --backup-
file, then exit.
--backup-file FILE Where the widget's prior state is saved before a
change, and read back from during --undo.
--admin-access Always fetch with adminAccess=true, skipping auto-
detection. Needed when the API token's user does not
own the widget.
--no-admin-access Always fetch without adminAccess=true, skipping auto-
detection. Needed on Sisense instances that reject
adminAccess as an unrecognized query parameter.Use case
Using swap_widget_dimension.py instead of manual widget edits offers several advantages:
Applies a field change to a widget directly through its JSON definition, without opening the widget editor
Works on either a chart's category dimension or its measures, through the same
--paneloptionProvides a way to confirm a widget's panel structure with
--showbefore any change is madeAuto-detects whether admin access is needed, rather than requiring the caller to know their token's relationship to the widget in advance
Provides a backup of the widget's prior state before every applied change, with
--undoto restore itDemonstrates how pySisense's
get_widget_by_idandupdate_widgetcan be used together to read, modify, and write back any field in a widget's definition, not only its dimension
Summary
swap_widget_dimension.py gives Sisense administrators and developers a way to change the field used by a chart widget's category axis or its measures from the command line, with a preview step, a way to inspect the widget's actual structure first, auto-detection of whether admin access is needed, and a backup that makes any applied change reversible. The same get_widget_by_id and update_widget pattern used here, fetch the widget, modify the field that needs to change, write it back, applies to other widget fields beyond the ones shown in this example.
"""swap_widget_dimension.py: swap the field used by one item on a Sisense chart widget panel.
Reads a widget with ``Dashboard.get_widget_by_id()``, replaces the field used
by one item in a named ``metadata.panels`` entry, such as ``categories`` (a
bar chart's category axis) or ``values`` (its measures), and writes the
result back with ``Dashboard.update_widget()``. "Dimension" is used loosely
here, the way most readers of this script will mean it: the field dropped
into that panel, whether it is technically a dimension or a measure. This is
a worked example of pySisense's widget read and write functions, not a
general purpose widget editor: it changes one field on one panel item on one
widget per run, and does not touch anything else on the widget.
Note: get_widget_by_id, update_widget, and find_widgets_by_type are not yet
in the pySisense release on PyPI. Install from the dev branch to use this
script:
pip install "git+https://github.com/sisense/pysisense.git@dev"
Usage
-----
python swap_widget_dimension.py --show # print the widget's panels, change nothing
python swap_widget_dimension.py # preview the swap, change nothing
python swap_widget_dimension.py --apply # write the change to Sisense
python swap_widget_dimension.py --undo # restore from the backup file
python swap_widget_dimension.py --panel values --apply # swap a measure instead of the category axis
Whether a widget fetch needs adminAccess=true depends on whether the API
token's own user already owns the widget. Rather than requiring the caller
to know that in advance, fetch_widget() below tries without admin access
first and only escalates if that fails.
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
from typing import Any
import yaml
from pysisense import Dashboard, SisenseClient
SCRIPT_DIR = Path(__file__).parent
DEFAULT_CONFIG_PATH = SCRIPT_DIR / "config.yaml"
DEFAULT_BACKUP_PATH = SCRIPT_DIR / "widget_backup.json"
# The metadata.panels entry this script targets when --panel and
# dimension.panel are both left unset: a bar chart's category axis.
DEFAULT_PANEL_NAME = "categories"
def load_config(path: Path) -> dict[str, Any]:
"""Load and parse the YAML configuration file.
Parameters
----------
path : Path
Location of the config.yaml file to read.
Returns
-------
dict[str, Any]
The parsed configuration. Empty dict if the file is empty.
Raises
------
SystemExit
If no file exists at ``path``.
"""
if not path.exists():
sys.exit(
f"Config file not found: {path}\n"
"Copy config.example.yaml to config.yaml and fill in the instance URL, "
"token, and widget details."
)
with path.open() as config_file:
return yaml.safe_load(config_file) or {}
def build_client(config: dict[str, Any]) -> SisenseClient:
"""Construct a SisenseClient from the ``sisense`` section of config.yaml.
Parameters
----------
config : dict[str, Any]
The full parsed configuration, as returned by :func:`load_config`.
Returns
-------
SisenseClient
A client ready to use with pySisense's Dashboard class.
Raises
------
SystemExit
If ``sisense.domain`` or ``sisense.token`` is missing.
"""
sisense_config = config.get("sisense", {})
domain = sisense_config.get("domain", "")
token = sisense_config.get("token", "")
if not domain or not token:
sys.exit("config.yaml must include sisense.domain and sisense.token.")
return SisenseClient(domain=domain, token=token, is_ssl=sisense_config.get("is_ssl", True))
def describe_current_user(dashboard: Dashboard) -> dict[str, Any] | None:
"""Look up the Sisense user the API token belongs to.
Used only to print a diagnostic line explaining whether the token's own
user already owns the widget being changed, which is what actually
determines whether admin access is needed (see :func:`fetch_widget`).
A failed lookup here is not fatal to the rest of the script.
Parameters
----------
dashboard : Dashboard
A pySisense Dashboard instance, whose ``access_mgmt`` attribute
exposes ``get_my_user()``.
Returns
-------
dict[str, Any] | None
The logged-in user object (``GET /api/users/loggedin``), or ``None``
if the lookup failed for any reason.
"""
try:
user = dashboard.access_mgmt.get_my_user()
except Exception:
return None
if isinstance(user, dict) and "error" not in user:
return user
return None
def fetch_widget(
dashboard: Dashboard,
dashboard_id: str,
widget_id: str,
*,
admin_access: bool | None,
) -> tuple[dict[str, Any], bool]:
"""Fetch a widget, auto detecting whether admin access is required.
Whether a fetch needs ``adminAccess=true`` depends on whether the API
token's own user already owns the widget, not on the Sisense version.
On top of that, some Sisense versions reject ``adminAccess`` outright as
an unrecognized query parameter, which looks identical from here to "not
the owner". Rather than have the caller guess, this tries the cheaper,
no-admin-access request first, and only escalates if that fails.
Parameters
----------
dashboard : Dashboard
A pySisense Dashboard instance.
dashboard_id : str
The ``oid`` of the dashboard that contains the widget.
widget_id : str
The ``oid`` of the widget to fetch.
admin_access : bool | None
``True`` or ``False`` forces that mode and skips auto-detection.
``None`` tries without admin access first, then retries with admin
access only if the first attempt fails.
Returns
-------
tuple[dict[str, Any], bool]
The fetched widget, and whether admin access was actually used to
retrieve it.
Raises
------
SystemExit
If the widget cannot be fetched in the requested mode(s).
"""
if admin_access is not None:
widget = dashboard.get_widget_by_id(dashboard_id, widget_id, admin_access=admin_access)
if "error" in widget:
sys.exit(f"Could not fetch widget: {widget['error']}")
return widget, admin_access
widget = dashboard.get_widget_by_id(dashboard_id, widget_id, admin_access=False)
if "error" not in widget:
return widget, False
error_without_admin_access = widget["error"]
widget = dashboard.get_widget_by_id(dashboard_id, widget_id, admin_access=True)
if "error" not in widget:
return widget, True
sys.exit(
"Could not fetch widget, with or without admin access.\n"
f" Without admin access: {error_without_admin_access}\n"
f" With admin access: {widget['error']}"
)
def print_fetch_diagnostics(dashboard: Dashboard, widget: dict[str, Any], admin_access_used: bool) -> None:
"""Print a line explaining whether the token's user owns this widget.
Purely informational: makes the reason admin access was, or was not,
needed for this particular widget visible, instead of leaving the
``True``/``False`` in ``admin_access_used`` unexplained. Printed to
stderr, not stdout, so it never lands in ``--show``'s JSON output and
can be piped straight into something like ``jq``.
Parameters
----------
dashboard : Dashboard
A pySisense Dashboard instance, used to look up the current user.
widget : dict[str, Any]
The widget returned by :func:`fetch_widget`.
admin_access_used : bool
Whether the fetch that retrieved ``widget`` used admin access.
Returns
-------
None
"""
access_note = "with admin access" if admin_access_used else "without admin access"
current_user = describe_current_user(dashboard)
if current_user is None:
print(f"Fetched widget {access_note} (could not look up the current user for comparison).", file=sys.stderr)
return
identity = current_user.get("email") or current_user.get("userName") or current_user.get("_id")
owns_widget = current_user.get("_id") == widget.get("owner")
ownership_note = "owns this widget" if owns_widget else "does not own this widget"
print(f"Fetched widget {access_note}. Authenticated as {identity!r}, who {ownership_note}.", file=sys.stderr)
def find_panel_item(
widget: dict[str, Any],
panel_name: str,
match_column: str | None,
) -> tuple[list[dict[str, Any]], int]:
"""Locate the panel item to change.
Parameters
----------
widget : dict[str, Any]
The full widget object, as returned by ``get_widget_by_id``.
panel_name : str
The ``metadata.panels`` entry to search, for example ``"categories"``
(a bar chart's category axis) or ``"values"`` (its measures).
match_column : str | None
If given, selects the item whose current ``jaql.column`` equals it.
If ``None``, the first item in the panel is used, which is correct
whenever that panel only has one item, such as a simple bar chart's
single category dimension or single measure.
Returns
-------
tuple[list[dict[str, Any]], int]
The panel's ``items`` list, and the index within it of the item to
change.
Raises
------
SystemExit
If the widget has no panel named ``panel_name``, the panel has no
items, or ``match_column`` does not match any item in it.
"""
panels = widget.get("metadata", {}).get("panels", [])
panel = next((candidate for candidate in panels if candidate.get("name") == panel_name), None)
if panel is None or not panel.get("items"):
sys.exit(f"No {panel_name!r} panel with items found on this widget. Run --show to inspect its structure.")
items = panel["items"]
if match_column is None:
return items, 0
for index, item in enumerate(items):
if item.get("jaql", {}).get("column") == match_column:
return items, index
sys.exit(f"No item with column {match_column!r} found in the {panel_name!r} panel. Run --show to see the current items.")
def build_new_jaql(existing_jaql: dict[str, Any], table: str, column: str, title: str | None) -> dict[str, Any]:
"""Return a copy of ``existing_jaql`` with its field swapped.
``table``, ``column``, and ``dim`` always change. ``title`` changes to
``title`` or, if that is ``None``, to ``column``. ``columnTitle`` and
``tableTitle`` (Sisense's own display-label mirrors of ``column`` and
``table``, confirmed by inspecting a live widget) are updated to match
whenever the existing jaql already had them, so nothing about the field
still points at the old one under a different key. ``agg`` and every
other key already present (``datatype``, and so on) are carried over
unchanged. Preserving ``agg`` in particular is what makes this safe to
use on a ``values`` panel item, not only a ``categories`` one: swapping
Revenue for Cost keeps the sum aggregation the widget already had,
instead of resetting it.
Parameters
----------
existing_jaql : dict[str, Any]
The jaql object currently on the panel item being replaced.
table : str
The table of the new field.
column : str
The column of the new field.
title : str | None
The label to show on the widget for the new field. Defaults to
``column`` when ``None``.
Returns
-------
dict[str, Any]
A new jaql object with the field swapped.
"""
new_jaql = dict(existing_jaql)
new_jaql["table"] = table
new_jaql["column"] = column
new_jaql["dim"] = f"[{table}.{column}]"
new_jaql["title"] = title or column
if "columnTitle" in new_jaql:
new_jaql["columnTitle"] = column
if "tableTitle" in new_jaql:
new_jaql["tableTitle"] = table
return new_jaql
def write_widget(dashboard: Dashboard, dashboard_id: str, widget_id: str, payload: dict[str, Any]) -> None:
"""Write ``payload`` back to Sisense via ``Dashboard.update_widget()``.
Parameters
----------
dashboard : Dashboard
A pySisense Dashboard instance.
dashboard_id : str
The ``oid`` of the dashboard that contains the widget.
widget_id : str
The ``oid`` of the widget being written.
payload : dict[str, Any]
The full widget object to write back.
Returns
-------
None
Raises
------
SystemExit
If the API reports a write failure.
"""
result = dashboard.update_widget(dashboard_id, widget_id, payload)
if "error" in result:
sys.exit(f"Update failed: {result['error']}")
def parse_args() -> argparse.Namespace:
"""Define and parse this script's command-line arguments.
Returns
-------
argparse.Namespace
The parsed arguments.
"""
parser = argparse.ArgumentParser(description="Swap the field used by one item on a Sisense chart widget panel.")
parser.add_argument("--config", default=str(DEFAULT_CONFIG_PATH), metavar="FILE", help="Path to config.yaml.")
parser.add_argument("--dashboard-id", metavar="ID", help="Overrides widget.dashboard_id from config.yaml.")
parser.add_argument("--widget-id", metavar="ID", help="Overrides widget.widget_id from config.yaml.")
parser.add_argument(
"--panel",
help="Name of the metadata.panels entry to change, for example categories (a bar "
"chart's category axis) or values (its measures). Overrides dimension.panel from "
f"config.yaml. Defaults to {DEFAULT_PANEL_NAME!r} if neither is set.",
)
parser.add_argument("--table", help="Overrides dimension.table from config.yaml.")
parser.add_argument("--column", help="Overrides dimension.column from config.yaml.")
parser.add_argument("--title", help="Overrides dimension.title from config.yaml.")
parser.add_argument(
"--match-column",
metavar="COLUMN",
help="Column name of the item to replace within --panel, if it has more than one item. "
"Defaults to the first item.",
)
parser.add_argument(
"--show",
action="store_true",
help="Print the widget's current metadata.panels and exit. Use this to confirm panel "
"names and item order before swapping anything.",
)
parser.add_argument(
"--apply",
action="store_true",
help="Write the change to Sisense. Without this flag, only a preview is printed.",
)
parser.add_argument(
"--undo",
action="store_true",
help="Restore the widget to the state recorded in --backup-file, then exit.",
)
parser.add_argument(
"--backup-file",
default=str(DEFAULT_BACKUP_PATH),
metavar="FILE",
help="Where the widget's prior state is saved before a change, and read back from during --undo.",
)
admin_access_group = parser.add_mutually_exclusive_group()
admin_access_group.add_argument(
"--admin-access",
dest="admin_access",
action="store_true",
default=None,
help="Always fetch with adminAccess=true, skipping auto-detection. Needed when the API "
"token's user does not own the widget.",
)
admin_access_group.add_argument(
"--no-admin-access",
dest="admin_access",
action="store_false",
help="Always fetch without adminAccess=true, skipping auto-detection. Needed on Sisense "
"instances that reject adminAccess as an unrecognized query parameter.",
)
return parser.parse_args()
def main() -> None:
"""Entry point: parse arguments, then fetch, inspect, swap, or restore a widget's dimension."""
args = parse_args()
config = load_config(Path(args.config))
client = build_client(config)
dashboard = Dashboard(api_client=client)
widget_config = config.get("widget", {})
dashboard_id = args.dashboard_id or widget_config.get("dashboard_id")
widget_id = args.widget_id or widget_config.get("widget_id")
if not dashboard_id or not widget_id:
sys.exit(
"dashboard_id and widget_id are required, either in config.yaml under 'widget' "
"or via --dashboard-id/--widget-id."
)
admin_access = args.admin_access if args.admin_access is not None else widget_config.get("admin_access")
if args.undo:
backup_path = Path(args.backup_file)
if not backup_path.exists():
sys.exit(f"No backup file found at {backup_path}. Nothing to undo.")
original_widget = json.loads(backup_path.read_text())
write_widget(dashboard, dashboard_id, widget_id, original_widget)
print(f"Widget {widget_id} restored from {backup_path}.")
return
widget, admin_access_used = fetch_widget(dashboard, dashboard_id, widget_id, admin_access=admin_access)
print_fetch_diagnostics(dashboard, widget, admin_access_used)
if args.show:
print(json.dumps(widget.get("metadata", {}).get("panels", []), indent=2))
return
dimension_config = config.get("dimension", {})
panel_name = args.panel or dimension_config.get("panel") or DEFAULT_PANEL_NAME
table = args.table or dimension_config.get("table")
column = args.column or dimension_config.get("column")
title = args.title or dimension_config.get("title")
if not table or not column:
sys.exit("table and column are required, either in config.yaml under 'dimension' or via --table/--column.")
items, index = find_panel_item(widget, panel_name, args.match_column)
old_jaql = items[index].get("jaql", {})
new_jaql = build_new_jaql(old_jaql, table, column, title)
print(f"\nWidget: {widget.get('title') or widget_id!r} ({widget.get('type')})")
print(f" {'Panel:':<15}{panel_name!r}")
print(f" {'Current field:':<15}{old_jaql.get('table')}.{old_jaql.get('column')} (title: {old_jaql.get('title')!r})")
print(f" {'New field:':<15}{table}.{column} (title: {new_jaql['title']!r})")
if not args.apply:
print("\n[PREVIEW] No changes made. Re-run with --apply to write this change to Sisense.")
return
backup_path = Path(args.backup_file)
backup_path.write_text(json.dumps(widget, indent=2))
print(f"\nBacked up current widget state to {backup_path}.")
items[index]["jaql"] = new_jaql
write_widget(dashboard, dashboard_id, widget_id, widget)
print(f"Widget {widget_id} updated. To revert: python {Path(__file__).name} --undo --backup-file {backup_path}")
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.