From 1b6763f4a92abdac2c52ebe87ca18a7a21493781 Mon Sep 17 00:00:00 2001 From: Jordan Woods <13803242+jorwoods@users.noreply.github.com> Date: Fri, 7 Mar 2025 08:40:30 -0600 Subject: [PATCH] feat: properly support request option filters Many filters and RequestOptions added in 3.23. Adds explicit support for them and checks for prior versions. --- tableauserverclient/__init__.py | 2 + tableauserverclient/server/__init__.py | 2 + .../server/endpoint/exceptions.py | 4 ++ .../server/endpoint/views_endpoint.py | 6 ++- .../server/endpoint/workbooks_endpoint.py | 32 ++++++++---- tableauserverclient/server/request_options.py | 34 +++++++++++++ test/test_view.py | 38 ++++++++++++++ test/test_workbook.py | 51 +++++++++++++++++-- 8 files changed, 156 insertions(+), 13 deletions(-) diff --git a/tableauserverclient/__init__.py b/tableauserverclient/__init__.py index 39f8267a8..957a820db 100644 --- a/tableauserverclient/__init__.py +++ b/tableauserverclient/__init__.py @@ -56,6 +56,7 @@ ExcelRequestOptions, ImageRequestOptions, PDFRequestOptions, + PPTXRequestOptions, RequestOptions, MissingRequiredFieldError, FailedSignInError, @@ -107,6 +108,7 @@ "Pager", "PaginationItem", "PDFRequestOptions", + "PPTXRequestOptions", "Permission", "PermissionsRule", "PersonalAccessTokenAuth", diff --git a/tableauserverclient/server/__init__.py b/tableauserverclient/server/__init__.py index 87cc9460b..55288fdc9 100644 --- a/tableauserverclient/server/__init__.py +++ b/tableauserverclient/server/__init__.py @@ -5,6 +5,7 @@ ExcelRequestOptions, ImageRequestOptions, PDFRequestOptions, + PPTXRequestOptions, RequestOptions, ) from tableauserverclient.server.filter import Filter @@ -52,6 +53,7 @@ "ExcelRequestOptions", "ImageRequestOptions", "PDFRequestOptions", + "PPTXRequestOptions", "RequestOptions", "Filter", "Sort", diff --git a/tableauserverclient/server/endpoint/exceptions.py b/tableauserverclient/server/endpoint/exceptions.py index 77332da3e..ee931c910 100644 --- a/tableauserverclient/server/endpoint/exceptions.py +++ b/tableauserverclient/server/endpoint/exceptions.py @@ -113,3 +113,7 @@ def __str__(self): class FlowRunCancelledException(FlowRunFailedException): pass + + +class UnsupportedAttributeError(TableauError): + pass diff --git a/tableauserverclient/server/endpoint/views_endpoint.py b/tableauserverclient/server/endpoint/views_endpoint.py index 12b386876..9d1c8b00f 100644 --- a/tableauserverclient/server/endpoint/views_endpoint.py +++ b/tableauserverclient/server/endpoint/views_endpoint.py @@ -3,7 +3,7 @@ from tableauserverclient.models.permissions_item import PermissionsRule from tableauserverclient.server.endpoint.endpoint import QuerysetEndpoint, api -from tableauserverclient.server.endpoint.exceptions import MissingRequiredFieldError +from tableauserverclient.server.endpoint.exceptions import MissingRequiredFieldError, UnsupportedAttributeError from tableauserverclient.server.endpoint.permissions_endpoint import _PermissionsEndpoint from tableauserverclient.server.endpoint.resource_tagger import TaggingMixin from tableauserverclient.server.query import QuerySet @@ -171,6 +171,10 @@ 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+") + view_item._set_image(image_fetcher) logger.info(f"Populated image for view (ID: {view_item.id})") diff --git a/tableauserverclient/server/endpoint/workbooks_endpoint.py b/tableauserverclient/server/endpoint/workbooks_endpoint.py index 4fdcf075b..8507152ba 100644 --- a/tableauserverclient/server/endpoint/workbooks_endpoint.py +++ b/tableauserverclient/server/endpoint/workbooks_endpoint.py @@ -11,7 +11,11 @@ from tableauserverclient.server.query import QuerySet from tableauserverclient.server.endpoint.endpoint import QuerysetEndpoint, api, parameter_added_in -from tableauserverclient.server.endpoint.exceptions import InternalServerError, MissingRequiredFieldError +from tableauserverclient.server.endpoint.exceptions import ( + InternalServerError, + MissingRequiredFieldError, + UnsupportedAttributeError, +) from tableauserverclient.server.endpoint.permissions_endpoint import _PermissionsEndpoint from tableauserverclient.server.endpoint.resource_tagger import TaggingMixin @@ -34,7 +38,7 @@ if TYPE_CHECKING: from tableauserverclient.server import Server - from tableauserverclient.server.request_options import RequestOptions + from tableauserverclient.server.request_options import RequestOptions, PDFRequestOptions, PPTXRequestOptions from tableauserverclient.models import DatasourceItem from tableauserverclient.server.endpoint.schedules_endpoint import AddResponse @@ -472,11 +476,12 @@ def _get_workbook_connections( connections = ConnectionItem.from_response(server_response.content, self.parent_srv.namespace) return connections - # Get the pdf of the entire workbook if its tabs are enabled, pdf of the default view if its tabs are disabled @api(version="3.4") - def populate_pdf(self, workbook_item: WorkbookItem, req_options: Optional["RequestOptions"] = None) -> None: + def populate_pdf(self, workbook_item: WorkbookItem, req_options: Optional["PDFRequestOptions"] = None) -> None: """ - Populates the PDF for the specified workbook item. + Populates the PDF for the specified workbook item. Get the pdf of the + entire workbook if its tabs are enabled, pdf of the default view if its + tabs are disabled. This method populates a PDF with image(s) of the workbook view(s) you specify. @@ -488,7 +493,7 @@ def populate_pdf(self, workbook_item: WorkbookItem, req_options: Optional["Reque workbook_item : WorkbookItem The workbook item to populate the PDF for. - req_options : RequestOptions, optional + req_options : PDFRequestOptions, optional (Optional) You can pass in request options to specify the page type and orientation of the PDF content, as well as the maximum age of the PDF rendered on the server. See PDFRequestOptions class for more @@ -510,17 +515,26 @@ def populate_pdf(self, workbook_item: WorkbookItem, req_options: Optional["Reque def pdf_fetcher() -> bytes: return self._get_wb_pdf(workbook_item, req_options) + if not self.parent_srv.check_at_least_version("3.23") and req_options is not None: + if req_options.view_filters or req_options.view_parameters: + raise UnsupportedAttributeError("view_filters and view_parameters are only supported in 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+") + workbook_item._set_pdf(pdf_fetcher) logger.info(f"Populated pdf for workbook (ID: {workbook_item.id})") - def _get_wb_pdf(self, workbook_item: WorkbookItem, req_options: Optional["RequestOptions"]) -> bytes: + def _get_wb_pdf(self, workbook_item: WorkbookItem, req_options: Optional["PDFRequestOptions"]) -> bytes: url = f"{self.baseurl}/{workbook_item.id}/pdf" server_response = self.get_request(url, req_options) pdf = server_response.content return pdf @api(version="3.8") - def populate_powerpoint(self, workbook_item: WorkbookItem, req_options: Optional["RequestOptions"] = None) -> None: + def populate_powerpoint( + self, workbook_item: WorkbookItem, req_options: Optional["PPTXRequestOptions"] = None + ) -> None: """ Populates the PowerPoint for the specified workbook item. @@ -561,7 +575,7 @@ def pptx_fetcher() -> bytes: workbook_item._set_powerpoint(pptx_fetcher) logger.info(f"Populated powerpoint for workbook (ID: {workbook_item.id})") - def _get_wb_pptx(self, workbook_item: WorkbookItem, req_options: Optional["RequestOptions"]) -> bytes: + def _get_wb_pptx(self, workbook_item: WorkbookItem, req_options: Optional["PPTXRequestOptions"]) -> bytes: url = f"{self.baseurl}/{workbook_item.id}/powerpoint" server_response = self.get_request(url, req_options) pptx = server_response.content diff --git a/tableauserverclient/server/request_options.py b/tableauserverclient/server/request_options.py index c37c0ce42..504f7f3ca 100644 --- a/tableauserverclient/server/request_options.py +++ b/tableauserverclient/server/request_options.py @@ -385,6 +385,8 @@ class PDFRequestOptions(_ImagePDFCommonExportOptions): Options that can be used when exporting a view to PDF. Set the maxage to control the age of the data exported. Filters to the underlying data can be applied using the `vf` and `parameter` methods. + vf and parameter filters are only supported in API version 3.23 and later. + Parameters ---------- page_type: str, optional @@ -438,3 +440,35 @@ def get_query_params(self) -> dict: params["orientation"] = self.orientation return params + + +class PPTXRequestOptions(RequestOptionsBase): + """ + Options that can be used when exporting a view to PPTX. Set the maxage to control the age of the data exported. + + Parameters + ---------- + maxage: int, optional + The maximum age of the data to export. Shortest possible duration is 1 + minute. No upper limit. Default is -1, which means no limit. + """ + + def __init__(self, maxage=-1): + super().__init__() + self.max_age = maxage + + @property + def max_age(self) -> int: + return self._max_age + + @max_age.setter + @property_is_int(range=(0, 240), allowed=[-1]) + def max_age(self, value): + self._max_age = value + + def get_query_params(self): + params = {} + if self.max_age != -1: + params["maxAge"] = self.max_age + + return params diff --git a/test/test_view.py b/test/test_view.py index a89a6d235..3fdaf60e6 100644 --- a/test/test_view.py +++ b/test/test_view.py @@ -6,6 +6,7 @@ import tableauserverclient as TSC from tableauserverclient import UserItem, GroupItem, PermissionsRule from tableauserverclient.datetime_helpers import format_datetime +from tableauserverclient.server.endpoint.exceptions import UnsupportedAttributeError TEST_ASSET_DIR = os.path.join(os.path.dirname(__file__), "assets") @@ -177,6 +178,43 @@ def test_populate_image(self) -> None: self.server.views.populate_image(single_view) self.assertEqual(response, single_view.image) + def test_populate_image_unsupported(self) -> None: + self.server.version = "3.8" + with open(POPULATE_PREVIEW_IMAGE, "rb") as f: + response = f.read() + with requests_mock.mock() as m: + m.get( + self.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/image?vizWidth=1920&vizHeight=1080", + content=response, + ) + single_view = TSC.ViewItem() + single_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5" + + req_option = TSC.ImageRequestOptions(viz_width=1920, viz_height=1080) + + with self.assertRaises(UnsupportedAttributeError): + self.server.views.populate_image(single_view, req_option) + + def test_populate_image_viz_dimensions(self) -> None: + self.server.version = "3.23" + self.baseurl = self.server.views.baseurl + with open(POPULATE_PREVIEW_IMAGE, "rb") as f: + response = f.read() + with requests_mock.mock() as m: + m.get( + self.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/image?vizWidth=1920&vizHeight=1080", + content=response, + ) + single_view = TSC.ViewItem() + single_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5" + + req_option = TSC.ImageRequestOptions(viz_width=1920, viz_height=1080) + + self.server.views.populate_image(single_view, req_option) + self.assertEqual(response, single_view.image) + + history = m.request_history + def test_populate_image_with_options(self) -> None: with open(POPULATE_PREVIEW_IMAGE, "rb") as f: response = f.read() diff --git a/test/test_workbook.py b/test/test_workbook.py index 0aa52f50d..f3c2dd147 100644 --- a/test/test_workbook.py +++ b/test/test_workbook.py @@ -12,7 +12,7 @@ import tableauserverclient as TSC from tableauserverclient.datetime_helpers import format_datetime from tableauserverclient.models import UserItem, GroupItem, PermissionsRule -from tableauserverclient.server.endpoint.exceptions import InternalServerError +from tableauserverclient.server.endpoint.exceptions import InternalServerError, UnsupportedAttributeError from tableauserverclient.server.request_factory import RequestFactory from ._utils import asset @@ -450,6 +450,49 @@ def test_populate_pdf(self) -> None: self.server.workbooks.populate_pdf(single_workbook, req_option) self.assertEqual(response, single_workbook.pdf) + def test_populate_pdf_unsupported(self) -> None: + self.server.version = "3.4" + self.baseurl = self.server.workbooks.baseurl + with requests_mock.mock() as m: + m.get( + self.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2/pdf?type=a5&orientation=landscape", + content=b"", + ) + single_workbook = TSC.WorkbookItem("test") + single_workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" + + type = TSC.PDFRequestOptions.PageType.A5 + orientation = TSC.PDFRequestOptions.Orientation.Landscape + req_option = TSC.PDFRequestOptions(type, orientation) + req_option.vf("Region", "West") + + with self.assertRaises(UnsupportedAttributeError): + self.server.workbooks.populate_pdf(single_workbook, req_option) + + def test_populate_pdf_vf_dims(self) -> None: + self.server.version = "3.23" + self.baseurl = self.server.workbooks.baseurl + with open(POPULATE_PDF, "rb") as f: + response = f.read() + with requests_mock.mock() as m: + m.get( + self.baseurl + + "/1f951daf-4061-451a-9df1-69a8062664f2/pdf?type=a5&orientation=landscape&vf_Region=West&vizWidth=1920&vizHeight=1080", + content=response, + ) + single_workbook = TSC.WorkbookItem("test") + single_workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" + + type = TSC.PDFRequestOptions.PageType.A5 + orientation = TSC.PDFRequestOptions.Orientation.Landscape + req_option = TSC.PDFRequestOptions(type, orientation) + req_option.vf("Region", "West") + req_option.viz_width = 1920 + req_option.viz_height = 1080 + + self.server.workbooks.populate_pdf(single_workbook, req_option) + self.assertEqual(response, single_workbook.pdf) + def test_populate_powerpoint(self) -> None: self.server.version = "3.8" self.baseurl = self.server.workbooks.baseurl @@ -457,13 +500,15 @@ def test_populate_powerpoint(self) -> None: response = f.read() with requests_mock.mock() as m: m.get( - self.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2/powerpoint", + self.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2/powerpoint?maxAge=1", content=response, ) single_workbook = TSC.WorkbookItem("test") single_workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" - self.server.workbooks.populate_powerpoint(single_workbook) + ro = TSC.PPTXRequestOptions(maxage=1) + + self.server.workbooks.populate_powerpoint(single_workbook, ro) self.assertEqual(response, single_workbook.powerpoint) def test_populate_preview_image(self) -> None: