From dce0a3fdb3674a482cbce527c6ef6b60fa441bd5 Mon Sep 17 00:00:00 2001 From: Renata Murzina Date: Fri, 20 Mar 2026 15:29:44 -0700 Subject: [PATCH 1/3] feat: add SVG format support for view and custom view images --- .../server/endpoint/custom_views_endpoint.py | 9 ++- .../server/endpoint/views_endpoint.py | 12 ++-- tableauserverclient/server/request_options.py | 15 ++++- test/test_custom_view.py | 63 +++++++++++++++++++ test/test_view.py | 61 ++++++++++++++++++ 5 files changed, 154 insertions(+), 6 deletions(-) diff --git a/tableauserverclient/server/endpoint/custom_views_endpoint.py b/tableauserverclient/server/endpoint/custom_views_endpoint.py index 8d78dca7a..a82f1fa67 100644 --- a/tableauserverclient/server/endpoint/custom_views_endpoint.py +++ b/tableauserverclient/server/endpoint/custom_views_endpoint.py @@ -121,7 +121,7 @@ def populate_image(self, view_item: CustomViewItem, req_options: Optional["Image view_item : CustomViewItem req_options : ImageRequestOptions, optional - Options to customize the image returned, by default None + Options to customize the image returned, including format (PNG or SVG), by default None Returns ------- @@ -139,6 +139,13 @@ def populate_image(self, view_item: CustomViewItem, req_options: Optional["Image def image_fetcher(): return self._get_view_image(view_item, req_options) + if req_options is not None: + if not self.parent_srv.check_at_least_version("3.29"): + if req_options.format: + from tableauserverclient.server.endpoint.exceptions import UnsupportedAttributeError + + raise UnsupportedAttributeError("format parameter is only supported in 3.29+") + view_item._set_image(image_fetcher) logger.info(f"Populated image for custom view (ID: {view_item.id})") diff --git a/tableauserverclient/server/endpoint/views_endpoint.py b/tableauserverclient/server/endpoint/views_endpoint.py index 162c04105..b95f3be0a 100644 --- a/tableauserverclient/server/endpoint/views_endpoint.py +++ b/tableauserverclient/server/endpoint/views_endpoint.py @@ -158,7 +158,7 @@ def populate_image(self, view_item: ViewItem, req_options: Optional["ImageReques req_options: Optional[ImageRequestOptions], default None Optional request options for the request. These options can include - parameters such as image resolution and max age. + parameters such as image resolution, max age, and format (PNG or SVG). Returns ------- @@ -171,9 +171,13 @@ def populate_image(self, view_item: ViewItem, req_options: Optional["ImageReques def image_fetcher(): return self._get_view_image(view_item, req_options) - if not self.parent_srv.check_at_least_version("3.23") and req_options is not None: - if req_options.viz_height or req_options.viz_width: - raise UnsupportedAttributeError("viz_height and viz_width are only supported in 3.23+") + if req_options is not None: + if not self.parent_srv.check_at_least_version("3.23"): + if req_options.viz_height or req_options.viz_width: + raise UnsupportedAttributeError("viz_height and viz_width are only supported in 3.23+") + if not self.parent_srv.check_at_least_version("3.29"): + if req_options.format: + raise UnsupportedAttributeError("format parameter is only supported in 3.29+") view_item._set_image(image_fetcher) logger.info(f"Populated image for view (ID: {view_item.id})") diff --git a/tableauserverclient/server/request_options.py b/tableauserverclient/server/request_options.py index 70c85d140..2038c81da 100644 --- a/tableauserverclient/server/request_options.py +++ b/tableauserverclient/server/request_options.py @@ -497,6 +497,10 @@ class ImageRequestOptions(_ImagePDFCommonExportOptions): viz_width: int, optional The width of the viz in pixels. If specified, viz_height must also be specified. + format: str, optional + The format of the image to export. Use Format.PNG, Format.SVG, Format.png, or Format.svg. + Default is "PNG". Available in API version 3.29+. + """ extension = "png" @@ -505,14 +509,23 @@ class ImageRequestOptions(_ImagePDFCommonExportOptions): class Resolution: High = "high" - def __init__(self, imageresolution=None, maxage=-1, viz_height=None, viz_width=None): + class Format: + PNG = "PNG" + SVG = "SVG" + png = "PNG" + svg = "SVG" + + def __init__(self, imageresolution=None, maxage=-1, viz_height=None, viz_width=None, format=None): super().__init__(maxage=maxage, viz_height=viz_height, viz_width=viz_width) self.image_resolution = imageresolution + self.format = format def get_query_params(self): params = super().get_query_params() if self.image_resolution: params["resolution"] = self.image_resolution + if self.format: + params["format"] = self.format return params diff --git a/test/test_custom_view.py b/test/test_custom_view.py index 2a3932726..19265ad14 100644 --- a/test/test_custom_view.py +++ b/test/test_custom_view.py @@ -116,6 +116,69 @@ def test_populate_image_with_options(server: TSC.Server) -> None: assert response == single_view.image +def test_populate_image_svg_format(server: TSC.Server) -> None: + server.version = "3.29" + response = b"test" + with requests_mock.mock() as m: + m.get( + server.custom_views.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/image?format=SVG", + content=response, + ) + single_view = TSC.CustomViewItem() + single_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5" + req_option = TSC.ImageRequestOptions(format=TSC.ImageRequestOptions.Format.SVG) + server.custom_views.populate_image(single_view, req_option) + assert response == single_view.image + + +def test_populate_image_png_format(server: TSC.Server) -> None: + server.version = "3.29" + response = POPULATE_PREVIEW_IMAGE.read_bytes() + with requests_mock.mock() as m: + m.get( + server.custom_views.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/image?format=PNG", + content=response, + ) + single_view = TSC.CustomViewItem() + single_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5" + req_option = TSC.ImageRequestOptions(format=TSC.ImageRequestOptions.Format.PNG) + server.custom_views.populate_image(single_view, req_option) + assert response == single_view.image + + +def test_populate_image_svg_format_lowercase_alias(server: TSC.Server) -> None: + server.version = "3.29" + response = b"test" + with requests_mock.mock() as m: + m.get( + server.custom_views.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/image?format=SVG", + content=response, + ) + single_view = TSC.CustomViewItem() + single_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5" + req_option = TSC.ImageRequestOptions(format=TSC.ImageRequestOptions.Format.svg) + server.custom_views.populate_image(single_view, req_option) + assert response == single_view.image + + +def test_populate_image_format_unsupported_version(server: TSC.Server) -> None: + from tableauserverclient.server.endpoint.exceptions import UnsupportedAttributeError + + server.version = "3.28" + response = POPULATE_PREVIEW_IMAGE.read_bytes() + with requests_mock.mock() as m: + m.get( + server.custom_views.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/image?format=SVG", + content=response, + ) + single_view = TSC.CustomViewItem() + single_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5" + req_option = TSC.ImageRequestOptions(format=TSC.ImageRequestOptions.Format.SVG) + + with pytest.raises(UnsupportedAttributeError): + server.custom_views.populate_image(single_view, req_option) + + def test_populate_image_missing_id(server: TSC.Server) -> None: single_view = TSC.CustomViewItem() single_view._id = None diff --git a/test/test_view.py b/test/test_view.py index b16f47c72..c9fefc8fd 100644 --- a/test/test_view.py +++ b/test/test_view.py @@ -238,6 +238,67 @@ def test_populate_image_with_options(server: TSC.Server) -> None: assert response == single_view.image +def test_populate_image_svg_format(server: TSC.Server) -> None: + server.version = "3.29" + response = b"test" + with requests_mock.mock() as m: + m.get( + server.views.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/image?format=SVG", + content=response, + ) + single_view = TSC.ViewItem() + single_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5" + req_option = TSC.ImageRequestOptions(format=TSC.ImageRequestOptions.Format.SVG) + server.views.populate_image(single_view, req_option) + assert response == single_view.image + + +def test_populate_image_png_format(server: TSC.Server) -> None: + server.version = "3.29" + response = POPULATE_PREVIEW_IMAGE.read_bytes() + with requests_mock.mock() as m: + m.get( + server.views.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/image?format=PNG", + content=response, + ) + single_view = TSC.ViewItem() + single_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5" + req_option = TSC.ImageRequestOptions(format=TSC.ImageRequestOptions.Format.PNG) + server.views.populate_image(single_view, req_option) + assert response == single_view.image + + +def test_populate_image_svg_format_lowercase_alias(server: TSC.Server) -> None: + server.version = "3.29" + response = b"test" + with requests_mock.mock() as m: + m.get( + server.views.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/image?format=SVG", + content=response, + ) + single_view = TSC.ViewItem() + single_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5" + req_option = TSC.ImageRequestOptions(format=TSC.ImageRequestOptions.Format.svg) + server.views.populate_image(single_view, req_option) + assert response == single_view.image + + +def test_populate_image_format_unsupported_version(server: TSC.Server) -> None: + server.version = "3.28" + response = POPULATE_PREVIEW_IMAGE.read_bytes() + with requests_mock.mock() as m: + m.get( + server.views.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/image?format=SVG", + content=response, + ) + single_view = TSC.ViewItem() + single_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5" + req_option = TSC.ImageRequestOptions(format=TSC.ImageRequestOptions.Format.SVG) + + with pytest.raises(UnsupportedAttributeError): + server.views.populate_image(single_view, req_option) + + def test_populate_pdf(server: TSC.Server) -> None: response = POPULATE_PDF.read_bytes() with requests_mock.mock() as m: From 9218118309f7e32595394e06ff1dc5f02405cd66 Mon Sep 17 00:00:00 2001 From: Renata Murzina Date: Fri, 20 Mar 2026 16:14:02 -0700 Subject: [PATCH 2/3] fix: remove trailing whitespace in Format class Remove trailing spaces after comments in ImageRequestOptions.Format class to pass black formatting check. Co-Authored-By: Claude Sonnet 4.5 --- tableauserverclient/server/request_options.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tableauserverclient/server/request_options.py b/tableauserverclient/server/request_options.py index 2038c81da..4ffa4f718 100644 --- a/tableauserverclient/server/request_options.py +++ b/tableauserverclient/server/request_options.py @@ -512,8 +512,8 @@ class Resolution: class Format: PNG = "PNG" SVG = "SVG" - png = "PNG" - svg = "SVG" + png = "PNG" + svg = "SVG" def __init__(self, imageresolution=None, maxage=-1, viz_height=None, viz_width=None, format=None): super().__init__(maxage=maxage, viz_height=viz_height, viz_width=viz_width) From 45c7a689729fb31ba5c2a3eb40eceded84a56f22 Mon Sep 17 00:00:00 2001 From: Renata Murzina Date: Fri, 20 Mar 2026 16:54:25 -0700 Subject: [PATCH 3/3] Remove redundant lowercase png and svg aliases --- tableauserverclient/server/request_options.py | 2 -- test/test_custom_view.py | 15 --------------- test/test_view.py | 15 --------------- 3 files changed, 32 deletions(-) diff --git a/tableauserverclient/server/request_options.py b/tableauserverclient/server/request_options.py index 4ffa4f718..870435eb0 100644 --- a/tableauserverclient/server/request_options.py +++ b/tableauserverclient/server/request_options.py @@ -512,8 +512,6 @@ class Resolution: class Format: PNG = "PNG" SVG = "SVG" - png = "PNG" - svg = "SVG" def __init__(self, imageresolution=None, maxage=-1, viz_height=None, viz_width=None, format=None): super().__init__(maxage=maxage, viz_height=viz_height, viz_width=viz_width) diff --git a/test/test_custom_view.py b/test/test_custom_view.py index 19265ad14..6cbe4b454 100644 --- a/test/test_custom_view.py +++ b/test/test_custom_view.py @@ -146,21 +146,6 @@ def test_populate_image_png_format(server: TSC.Server) -> None: assert response == single_view.image -def test_populate_image_svg_format_lowercase_alias(server: TSC.Server) -> None: - server.version = "3.29" - response = b"test" - with requests_mock.mock() as m: - m.get( - server.custom_views.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/image?format=SVG", - content=response, - ) - single_view = TSC.CustomViewItem() - single_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5" - req_option = TSC.ImageRequestOptions(format=TSC.ImageRequestOptions.Format.svg) - server.custom_views.populate_image(single_view, req_option) - assert response == single_view.image - - def test_populate_image_format_unsupported_version(server: TSC.Server) -> None: from tableauserverclient.server.endpoint.exceptions import UnsupportedAttributeError diff --git a/test/test_view.py b/test/test_view.py index c9fefc8fd..a940e1d18 100644 --- a/test/test_view.py +++ b/test/test_view.py @@ -268,21 +268,6 @@ def test_populate_image_png_format(server: TSC.Server) -> None: assert response == single_view.image -def test_populate_image_svg_format_lowercase_alias(server: TSC.Server) -> None: - server.version = "3.29" - response = b"test" - with requests_mock.mock() as m: - m.get( - server.views.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/image?format=SVG", - content=response, - ) - single_view = TSC.ViewItem() - single_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5" - req_option = TSC.ImageRequestOptions(format=TSC.ImageRequestOptions.Format.svg) - server.views.populate_image(single_view, req_option) - assert response == single_view.image - - def test_populate_image_format_unsupported_version(server: TSC.Server) -> None: server.version = "3.28" response = POPULATE_PREVIEW_IMAGE.read_bytes()