From f71a89141961043b32660f0b7f14202b5e5c841a Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Wed, 17 Jun 2026 18:27:29 -0700 Subject: [PATCH 1/2] feat: include description in workbook publish and update requests The REST API has supported workbook description since v3.20 (2023.2). Fixes #899 Co-Authored-By: Claude Sonnet 4.6 --- tableauserverclient/server/request_factory.py | 6 +-- test/test_workbook.py | 51 +++++++++++++++++++ 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/tableauserverclient/server/request_factory.py b/tableauserverclient/server/request_factory.py index e22e43e2f..892ffbc9d 100644 --- a/tableauserverclient/server/request_factory.py +++ b/tableauserverclient/server/request_factory.py @@ -1060,11 +1060,7 @@ def update_req(self, workbook_item, parent_srv: "Server | None" = None): if workbook_item.owner_id: owner_element = ET.SubElement(workbook_element, "owner") owner_element.attrib["id"] = workbook_item.owner_id - if ( - workbook_item.description is not None - and parent_srv is not None - and parent_srv.check_at_least_version("3.21") - ): + if workbook_item.description is not None and (parent_srv is None or parent_srv.check_at_least_version("3.20")): workbook_element.attrib["description"] = workbook_item.description if workbook_item._views is not None: views_element = ET.SubElement(workbook_element, "views") diff --git a/test/test_workbook.py b/test/test_workbook.py index c5c4f6662..85c269fda 100644 --- a/test/test_workbook.py +++ b/test/test_workbook.py @@ -245,6 +245,42 @@ def test_update(server: TSC.Server) -> None: assert not single_workbook.data_acceleration_config["accelerate_now"] +def test_update_description_in_request_xml(server: TSC.Server) -> None: + """description should be included in the update request XML when server >= 3.20.""" + server.version = "3.20" + server.workbooks.baseurl + response_xml = UPDATE_XML.read_text() + with requests_mock.mock() as m: + m.put(server.workbooks.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2", text=response_xml) + single_workbook = TSC.WorkbookItem("1d0304cd-3796-429f-b815-7258370b9b74", show_tabs=True) + single_workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" + single_workbook.description = "A great workbook" + server.workbooks.update(single_workbook) + request_body = m.request_history[0].body + xml_root = fromstring(request_body) + workbook_el = xml_root.find(".//workbook") + assert workbook_el is not None + assert workbook_el.get("description") == "A great workbook" + + +def test_update_description_excluded_below_v3_20(server: TSC.Server) -> None: + """description should be excluded from the update request XML when server < 3.20.""" + server.version = "3.19" + server.workbooks.baseurl + response_xml = UPDATE_XML.read_text() + with requests_mock.mock() as m: + m.put(server.workbooks.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2", text=response_xml) + single_workbook = TSC.WorkbookItem("1d0304cd-3796-429f-b815-7258370b9b74", show_tabs=True) + single_workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" + single_workbook.description = "A great workbook" + server.workbooks.update(single_workbook) + request_body = m.request_history[0].body + xml_root = fromstring(request_body) + workbook_el = xml_root.find(".//workbook") + assert workbook_el is not None + assert workbook_el.get("description") is None + + def test_update_missing_id(server: TSC.Server) -> None: single_workbook = TSC.WorkbookItem("test") with pytest.raises(TSC.MissingRequiredFieldError): @@ -592,6 +628,21 @@ def test_publish(server: TSC.Server) -> None: assert "REST API Testing" == new_workbook.description +def test_publish_description_in_request_xml(server: TSC.Server) -> None: + """description should be included in the publish request XML.""" + response_xml = PUBLISH_XML.read_text() + with requests_mock.mock() as m: + m.post(server.workbooks.baseurl, text=response_xml) + new_workbook = TSC.WorkbookItem( + name="Sample", show_tabs=False, project_id="ee8c6e70-43b6-11e6-af4f-f7b0d8e20760" + ) + new_workbook.description = "A great workbook" + sample_workbook = os.path.join(TEST_ASSET_DIR, "SampleWB.twbx") + server.workbooks.publish(new_workbook, sample_workbook, server.PublishMode.CreateNew) + request_body = m.request_history[0].body + assert b'description="A great workbook"' in request_body + + def test_publish_a_packaged_file_object(server: TSC.Server) -> None: response_xml = PUBLISH_XML.read_text() with requests_mock.mock() as m: From c9a0c53f261d80563a619f789e3f5c5c15b9aeeb Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Thu, 18 Jun 2026 22:11:13 -0700 Subject: [PATCH 2/2] fix: correct version gate and test style for workbook description update - Restore version threshold to 3.21 (matching Tableau REST API docs) - Restore parent_srv is None guard to exclude description (original behavior) - Update tests to reflect 3.21 boundary - Use re.search on multipart body for publish test, matching existing pattern - Fix stale sample comment Co-Authored-By: Claude Sonnet 4.6 --- samples/explore_workbook.py | 2 +- tableauserverclient/server/request_factory.py | 6 +++++- test/test_workbook.py | 16 +++++++--------- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/samples/explore_workbook.py b/samples/explore_workbook.py index f51639ab3..d537f21d6 100644 --- a/samples/explore_workbook.py +++ b/samples/explore_workbook.py @@ -72,7 +72,7 @@ def main(): # Pick one workbook from the list sample_workbook = all_workbooks[0] sample_workbook.name = "Name me something cooler" - sample_workbook.description = "That doesn't work" + sample_workbook.description = "Updated description" updated: TSC.WorkbookItem = server.workbooks.update(sample_workbook) print(updated.name, updated.description) diff --git a/tableauserverclient/server/request_factory.py b/tableauserverclient/server/request_factory.py index 892ffbc9d..e22e43e2f 100644 --- a/tableauserverclient/server/request_factory.py +++ b/tableauserverclient/server/request_factory.py @@ -1060,7 +1060,11 @@ def update_req(self, workbook_item, parent_srv: "Server | None" = None): if workbook_item.owner_id: owner_element = ET.SubElement(workbook_element, "owner") owner_element.attrib["id"] = workbook_item.owner_id - if workbook_item.description is not None and (parent_srv is None or parent_srv.check_at_least_version("3.20")): + if ( + workbook_item.description is not None + and parent_srv is not None + and parent_srv.check_at_least_version("3.21") + ): workbook_element.attrib["description"] = workbook_item.description if workbook_item._views is not None: views_element = ET.SubElement(workbook_element, "views") diff --git a/test/test_workbook.py b/test/test_workbook.py index 85c269fda..a3ec7e914 100644 --- a/test/test_workbook.py +++ b/test/test_workbook.py @@ -246,9 +246,8 @@ def test_update(server: TSC.Server) -> None: def test_update_description_in_request_xml(server: TSC.Server) -> None: - """description should be included in the update request XML when server >= 3.20.""" - server.version = "3.20" - server.workbooks.baseurl + """description should be included in the update request XML when server >= 3.21.""" + server.version = "3.21" response_xml = UPDATE_XML.read_text() with requests_mock.mock() as m: m.put(server.workbooks.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2", text=response_xml) @@ -263,10 +262,9 @@ def test_update_description_in_request_xml(server: TSC.Server) -> None: assert workbook_el.get("description") == "A great workbook" -def test_update_description_excluded_below_v3_20(server: TSC.Server) -> None: - """description should be excluded from the update request XML when server < 3.20.""" - server.version = "3.19" - server.workbooks.baseurl +def test_update_description_excluded_below_v3_21(server: TSC.Server) -> None: + """description should be excluded from the update request XML when server < 3.21.""" + server.version = "3.20" response_xml = UPDATE_XML.read_text() with requests_mock.mock() as m: m.put(server.workbooks.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2", text=response_xml) @@ -639,8 +637,8 @@ def test_publish_description_in_request_xml(server: TSC.Server) -> None: new_workbook.description = "A great workbook" sample_workbook = os.path.join(TEST_ASSET_DIR, "SampleWB.twbx") server.workbooks.publish(new_workbook, sample_workbook, server.PublishMode.CreateNew) - request_body = m.request_history[0].body - assert b'description="A great workbook"' in request_body + request_body = m._adapter.request_history[0]._request.body + assert re.search(b'description=\\"A great workbook\\"', request_body) def test_publish_a_packaged_file_object(server: TSC.Server) -> None: