From 961457686150a94356afbfd4840dffd84bcc31d2 Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Mon, 9 Nov 2026 14:00:04 -0800 Subject: [PATCH 1/5] Make databases.populate_tables raise a clear NotImplementedError populate_tables was gated with @api(version="99"), so it raised a misleading EndpointUnavailableError on every server. It called GET /databases/{id}/tables, which is not part of the REST API at any version. Raise NotImplementedError pointing to the Metadata API and tables.get_by_id instead, and make the DatabaseItem.tables error message accurate. Refs #735 Co-Authored-By: Claude Code --- tableauserverclient/models/database_item.py | 8 +++-- .../server/endpoint/databases_endpoint.py | 36 ++++++++++--------- test/test_database.py | 7 ++++ 3 files changed, 31 insertions(+), 20 deletions(-) diff --git a/tableauserverclient/models/database_item.py b/tableauserverclient/models/database_item.py index 4d4604461..b884d6658 100644 --- a/tableauserverclient/models/database_item.py +++ b/tableauserverclient/models/database_item.py @@ -42,7 +42,7 @@ def __init__(self, name, description=None, content_permissions=None): self._data_quality_warnings = None - self._tables = None # Not implemented yet + self._tables = None def __str__(self): return f"" @@ -177,9 +177,11 @@ def contact_id(self, value): @property def tables(self): if self._tables is None: - error = "Database must be populated with tables first." + error = ( + "Database tables are not available: the REST API cannot list the tables of a database. " + "Use the Metadata API to find them." + ) raise UnpopulatedPropertyError(error) - # Each call to `.tables` should create a new pager, this just runs the callable return self._tables() def _set_values(self, database_values): diff --git a/tableauserverclient/server/endpoint/databases_endpoint.py b/tableauserverclient/server/endpoint/databases_endpoint.py index caf9a985f..6b346fe55 100644 --- a/tableauserverclient/server/endpoint/databases_endpoint.py +++ b/tableauserverclient/server/endpoint/databases_endpoint.py @@ -10,7 +10,7 @@ from tableauserverclient.server.endpoint.permissions_endpoint import _PermissionsEndpoint from tableauserverclient.server.endpoint.resource_tagger import TaggingMixin from tableauserverclient.server import RequestFactory -from tableauserverclient.models import DatabaseItem, TableItem, PaginationItem, Resource +from tableauserverclient.models import DatabaseItem, PaginationItem, Resource from tableauserverclient.helpers.logging import logger @@ -149,24 +149,26 @@ def update(self, database_item: DatabaseItem) -> DatabaseItem: updated_database = DatabaseItem.from_response(server_response.content, self.parent_srv.namespace)[0] return updated_database - # Not Implemented Yet - @api(version="99") - def populate_tables(self, database_item): - if not database_item.id: - error = "database item missing ID. database must be retrieved from server first." - raise MissingRequiredFieldError(error) - - def column_fetcher(): - return self._get_tables_for_database(database_item) + def populate_tables(self, database_item: DatabaseItem) -> None: + """ + Not supported. The REST API has no endpoint that lists the tables of a + database. - database_item._set_tables(column_fetcher) - logger.info(f"Populated tables for database (ID: {database_item.id}") + To find the tables in a database, use the Metadata API with + ``server.metadata.query`` and a query such as + ``databases(filter: {luid: $luid}) { tables { luid name } }``, then + retrieve each table with ``server.tables.get_by_id``. - def _get_tables_for_database(self, database_item): - url = f"{self.baseurl}/{database_item.id}/tables" - server_response = self.get_request(url) - tables = TableItem.from_response(server_response.content, self.parent_srv.namespace) - return tables + Raises + ------ + NotImplementedError + Always. + """ + raise NotImplementedError( + "The REST API has no endpoint to list the tables of a database. Use the Metadata API " + "(server.metadata.query with databases(filter: {luid: $luid}) { tables { luid name } }) " + "to find the table LUIDs, then call server.tables.get_by_id." + ) @api(version="3.5") def populate_permissions(self, item: DatabaseItem) -> None: diff --git a/test/test_database.py b/test/test_database.py index 951eebe00..feba3e1d7 100644 --- a/test/test_database.py +++ b/test/test_database.py @@ -110,3 +110,10 @@ def test_delete(server): with requests_mock.mock() as m: m.delete(server.databases.baseurl + "/0448d2ed-590d-4fa0-b272-a2a8a24555b5", status_code=204) server.databases.delete("0448d2ed-590d-4fa0-b272-a2a8a24555b5") + + +def test_populate_tables_not_supported(server): + database = TSC.DatabaseItem("test") + database._id = "23591f2c-4802-4d6a-9e28-574a8ea9bc4c" + with pytest.raises(NotImplementedError, match="Metadata API"): + server.databases.populate_tables(database) From b68588c5f45f90fb0ad39243f9b62103d31e8d1d Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Mon, 9 Nov 2026 14:01:00 -0800 Subject: [PATCH 2/5] Use (item, rule) argument order in databases.delete_table_default_permissions Every other permissions method takes the item first and the rule second, but delete_table_default_permissions took (rule, item). Switch to (item, rule). Callers using the old order keep working: swapped argument types are detected, swapped back, and a DeprecationWarning pointing at the caller is emitted. Refs #735 Co-Authored-By: Claude Code --- .../server/endpoint/databases_endpoint.py | 28 +++++++++++++++- test/test_database.py | 33 +++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/tableauserverclient/server/endpoint/databases_endpoint.py b/tableauserverclient/server/endpoint/databases_endpoint.py index 6b346fe55..ada346db0 100644 --- a/tableauserverclient/server/endpoint/databases_endpoint.py +++ b/tableauserverclient/server/endpoint/databases_endpoint.py @@ -1,4 +1,5 @@ import logging +import warnings from typing import TYPE_CHECKING from collections.abc import Iterable @@ -193,7 +194,32 @@ def update_table_default_permissions( return self._default_permissions.update_default_permissions(item, rules, Resource.Table) @api(version="3.5") - def delete_table_default_permissions(self, rule: PermissionsRule, item: DatabaseItem) -> None: + def delete_table_default_permissions(self, item: DatabaseItem, rule: PermissionsRule) -> None: + """ + Deletes the specified default table permission rule from the database. + + REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_permissions.htm#delete_default_permission + + Parameters + ---------- + item : DatabaseItem + The database item to delete default table permissions from. + + rule : PermissionsRule + The permissions rule to delete from the database. + + Returns + ------- + None + """ + if isinstance(item, PermissionsRule) and isinstance(rule, DatabaseItem): + warnings.warn( + "delete_table_default_permissions(rule, item) is deprecated; pass (item, rule) like the other " + "permissions methods.", + DeprecationWarning, + stacklevel=3, + ) + item, rule = rule, item self._default_permissions.delete_default_permission(item, rule, Resource.Table) @api(version="3.5") diff --git a/test/test_database.py b/test/test_database.py index feba3e1d7..088c61543 100644 --- a/test/test_database.py +++ b/test/test_database.py @@ -1,3 +1,4 @@ +import warnings from pathlib import Path import pytest @@ -117,3 +118,35 @@ def test_populate_tables_not_supported(server): database._id = "23591f2c-4802-4d6a-9e28-574a8ea9bc4c" with pytest.raises(NotImplementedError, match="Metadata API"): server.databases.populate_tables(database) + + +def _delete_table_default_permission_setup(server, m): + database = TSC.DatabaseItem("test") + database._id = "0448d2ed-590d-4fa0-b272-a2a8a24555b5" + rule = TSC.PermissionsRule( + grantee=TSC.GroupItem.as_reference("5e5e1978-71fa-11e4-87dd-7382f5c437af"), + capabilities={TSC.Permission.Capability.Read: TSC.Permission.Mode.Allow}, + ) + url = ( + f"{server.databases.baseurl}/{database.id}/default-permissions/tables/groups/" + "5e5e1978-71fa-11e4-87dd-7382f5c437af/Read/Allow" + ) + m.delete(url, status_code=204) + return database, rule + + +def test_delete_table_default_permissions(server): + with requests_mock.mock() as m, warnings.catch_warnings(): + warnings.simplefilter("error") + database, rule = _delete_table_default_permission_setup(server, m) + server.databases.delete_table_default_permissions(database, rule) + assert m.call_count == 1 + + +def test_delete_table_default_permissions_swapped_arguments_warns(server): + with requests_mock.mock() as m: + database, rule = _delete_table_default_permission_setup(server, m) + with pytest.warns(DeprecationWarning, match="deprecated") as record: + server.databases.delete_table_default_permissions(rule, database) + assert m.call_count == 1 + assert record[0].filename == __file__ From a78e46e8678ed07996e515f3ceb9bea5abfa5dd0 Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Mon, 9 Nov 2026 14:03:09 -0800 Subject: [PATCH 3/5] Only send set fields in database and table update requests The update requests always sent isCertified, so an item whose certified value was never set sent isCertified="none". description and certificationNote were only sent when non-empty, so they could not be cleared. Send isCertified when certified is not None, and send description and certificationNote whenever they are not None so an empty string clears them. The Update Database REST method accepts contentPermissions and the docstring claims it can set permissions, but the value was never sent. Send it when set. Refs #735 Co-Authored-By: Claude Code --- .../server/endpoint/databases_endpoint.py | 2 +- tableauserverclient/server/request_factory.py | 17 ++++++---- test/test_database.py | 33 +++++++++++++++++++ test/test_table.py | 27 +++++++++++++++ 4 files changed, 72 insertions(+), 7 deletions(-) diff --git a/tableauserverclient/server/endpoint/databases_endpoint.py b/tableauserverclient/server/endpoint/databases_endpoint.py index ada346db0..fb4bd9e9d 100644 --- a/tableauserverclient/server/endpoint/databases_endpoint.py +++ b/tableauserverclient/server/endpoint/databases_endpoint.py @@ -119,7 +119,7 @@ def delete(self, database_id: str) -> None: @api(version="3.5") def update(self, database_item: DatabaseItem) -> DatabaseItem: """ - Update the database description, certify the database, set permissions, + Update the database description, certify the database, set content permissions, or assign a User as the database contact. REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_metadata.htm#update_database diff --git a/tableauserverclient/server/request_factory.py b/tableauserverclient/server/request_factory.py index 517a26956..e63dc45b6 100644 --- a/tableauserverclient/server/request_factory.py +++ b/tableauserverclient/server/request_factory.py @@ -179,14 +179,18 @@ def update_req(self, database_item): contact_element = ET.SubElement(database_element, "contact") contact_element.attrib["id"] = database_item.contact_id - database_element.attrib["isCertified"] = str(database_item.certified).lower() + if database_item.certified is not None: + database_element.attrib["isCertified"] = str(database_item.certified).lower() - if database_item.certification_note: + if database_item.certification_note is not None: database_element.attrib["certificationNote"] = str(database_item.certification_note) - if database_item.description: + if database_item.description is not None: database_element.attrib["description"] = str(database_item.description) + if database_item.content_permissions is not None: + database_element.attrib["contentPermissions"] = str(database_item.content_permissions) + return ET.tostring(xml_request) @@ -945,12 +949,13 @@ def update_req(self, table_item): contact_element = ET.SubElement(table_element, "contact") contact_element.attrib["id"] = table_item.contact_id - table_element.attrib["isCertified"] = str(table_item.certified).lower() + if table_item.certified is not None: + table_element.attrib["isCertified"] = str(table_item.certified).lower() - if table_item.certification_note: + if table_item.certification_note is not None: table_element.attrib["certificationNote"] = str(table_item.certification_note) - if table_item.description: + if table_item.description is not None: table_element.attrib["description"] = str(table_item.description) return ET.tostring(xml_request) diff --git a/test/test_database.py b/test/test_database.py index 088c61543..ba151ebfd 100644 --- a/test/test_database.py +++ b/test/test_database.py @@ -1,10 +1,12 @@ import warnings from pathlib import Path +from xml.etree import ElementTree as ET import pytest import requests_mock import tableauserverclient as TSC +from tableauserverclient.server.request_factory import RequestFactory TEST_ASSET_DIR = Path(__file__).parent / "assets" @@ -150,3 +152,34 @@ def test_delete_table_default_permissions_swapped_arguments_warns(server): server.databases.delete_table_default_permissions(rule, database) assert m.call_count == 1 assert record[0].filename == __file__ + + +def test_update_req_omits_unset_fields() -> None: + item = TSC.DatabaseItem("test") + xml = ET.fromstring(RequestFactory.Database.update_req(item)) + assert xml.findall("database")[0].attrib == {} + + +def test_update_req_sends_set_fields() -> None: + item = TSC.DatabaseItem("test") + item.certified = False + item.certification_note = "note" + item.description = "desc" + attrib = ET.fromstring(RequestFactory.Database.update_req(item)).findall("database")[0].attrib + assert attrib["isCertified"] == "false" + assert attrib["certificationNote"] == "note" + assert attrib["description"] == "desc" + + +def test_update_req_empty_strings_clear_fields() -> None: + item = TSC.DatabaseItem("test") + item.certification_note = "" + item.description = "" + attrib = ET.fromstring(RequestFactory.Database.update_req(item)).findall("database")[0].attrib + assert attrib == {"certificationNote": "", "description": ""} + + +def test_update_req_sends_content_permissions() -> None: + item = TSC.DatabaseItem("test", content_permissions=TSC.DatabaseItem.ContentPermissions.ManagedByOwner) + attrib = ET.fromstring(RequestFactory.Database.update_req(item)).findall("database")[0].attrib + assert attrib == {"contentPermissions": "ManagedByOwner"} diff --git a/test/test_table.py b/test/test_table.py index 2f3c3c8d6..b7a323121 100644 --- a/test/test_table.py +++ b/test/test_table.py @@ -1,9 +1,11 @@ from pathlib import Path +from xml.etree import ElementTree as ET import pytest import requests_mock import tableauserverclient as TSC +from tableauserverclient.server.request_factory import RequestFactory TEST_ASSET_DIR = Path(__file__).parent / "assets" @@ -63,3 +65,28 @@ def test_delete(server: TSC.Server) -> None: with requests_mock.mock() as m: m.delete(server.tables.baseurl + "/0448d2ed-590d-4fa0-b272-a2a8a24555b5", status_code=204) server.tables.delete("0448d2ed-590d-4fa0-b272-a2a8a24555b5") + + +def test_update_req_omits_unset_fields() -> None: + item = TSC.TableItem("test") + xml = ET.fromstring(RequestFactory.Table.update_req(item)) + assert xml.findall("table")[0].attrib == {} + + +def test_update_req_sends_set_fields() -> None: + item = TSC.TableItem("test") + item.certified = False + item.certification_note = "note" + item.description = "desc" + attrib = ET.fromstring(RequestFactory.Table.update_req(item)).findall("table")[0].attrib + assert attrib["isCertified"] == "false" + assert attrib["certificationNote"] == "note" + assert attrib["description"] == "desc" + + +def test_update_req_empty_strings_clear_fields() -> None: + item = TSC.TableItem("test") + item.certification_note = "" + item.description = "" + attrib = ET.fromstring(RequestFactory.Table.update_req(item)).findall("table")[0].attrib + assert attrib == {"certificationNote": "", "description": ""} From 31e656f904c99fbdd1d7cda21dcf99362f6a4db8 Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Mon, 9 Nov 2026 14:03:59 -0800 Subject: [PATCH 4/5] Make update_tags consistent on databases and tables Neither DatabaseItem nor TableItem tracks tags, and the Query Database and Query Table responses do not include a tags element for tables, so update_tags cannot work as it does for workbooks and datasources. Both methods now share one explicit NotImplementedError that points to add_tags and delete_tags, and tables.update_tags carries the same @api(version="3.9") gate as the other tag methods. Refs #735 Co-Authored-By: Claude Code --- .../server/endpoint/databases_endpoint.py | 10 ++++++++-- .../server/endpoint/tables_endpoint.py | 11 +++++++++-- test/test_database.py | 15 +++++++++++++++ test/test_table.py | 15 +++++++++++++++ 4 files changed, 47 insertions(+), 4 deletions(-) diff --git a/tableauserverclient/server/endpoint/databases_endpoint.py b/tableauserverclient/server/endpoint/databases_endpoint.py index fb4bd9e9d..8ab59c238 100644 --- a/tableauserverclient/server/endpoint/databases_endpoint.py +++ b/tableauserverclient/server/endpoint/databases_endpoint.py @@ -20,6 +20,12 @@ from tableauserverclient.server.request_options import RequestOptions +_UPDATE_TAGS_MESSAGE = ( + "update_tags is not supported for databases: the item does not track its tags. " + "Use add_tags and delete_tags with the item or its ID instead." +) + + class Databases(Endpoint, TaggingMixin): def __init__(self, parent_srv): super().__init__(parent_srv) @@ -247,5 +253,5 @@ def delete_tags(self, item: DatabaseItem | str, tags: Iterable[str]) -> None: super().delete_tags(item, tags) @api(version="3.9") - def update_tags(self, item: DatabaseItem) -> None: - raise NotImplementedError("Update tags is not supported for databases.") + def update_tags(self, item: DatabaseItem) -> None: # type: ignore[override] + raise NotImplementedError(_UPDATE_TAGS_MESSAGE) diff --git a/tableauserverclient/server/endpoint/tables_endpoint.py b/tableauserverclient/server/endpoint/tables_endpoint.py index f50f0df0c..ad6201b1f 100644 --- a/tableauserverclient/server/endpoint/tables_endpoint.py +++ b/tableauserverclient/server/endpoint/tables_endpoint.py @@ -19,6 +19,12 @@ from tableauserverclient.models import DQWItem, PermissionsRule +_UPDATE_TAGS_MESSAGE = ( + "update_tags is not supported for tables: the item does not track its tags. " + "Use add_tags and delete_tags with the item or its ID instead." +) + + class Tables(Endpoint, TaggingMixin[TableItem]): def __init__(self, parent_srv): super().__init__(parent_srv) @@ -259,5 +265,6 @@ def add_tags(self, item: TableItem | str, tags: Iterable[str] | str) -> set[str] def delete_tags(self, item: TableItem | str, tags: Iterable[str] | str) -> None: return super().delete_tags(item, tags) - def update_tags(self, item: TableItem) -> None: # type: ignore - raise NotImplementedError("Update tags is not implemented for TableItem") + @api(version="3.9") + def update_tags(self, item: TableItem) -> None: # type: ignore[override] + raise NotImplementedError(_UPDATE_TAGS_MESSAGE) diff --git a/test/test_database.py b/test/test_database.py index ba151ebfd..7bf47c36d 100644 --- a/test/test_database.py +++ b/test/test_database.py @@ -183,3 +183,18 @@ def test_update_req_sends_content_permissions() -> None: item = TSC.DatabaseItem("test", content_permissions=TSC.DatabaseItem.ContentPermissions.ManagedByOwner) attrib = ET.fromstring(RequestFactory.Database.update_req(item)).findall("database")[0].attrib assert attrib == {"contentPermissions": "ManagedByOwner"} + + +def test_update_tags_not_supported(server) -> None: + server.version = "3.9" + item = TSC.DatabaseItem("test") + item._id = "10224773-ecee-42ac-b822-d786b0b8e4d9" + with pytest.raises(NotImplementedError, match="add_tags and delete_tags"): + server.databases.update_tags(item) + + +def test_update_tags_checks_version(server) -> None: + server.version = "3.5" + item = TSC.DatabaseItem("test") + with pytest.raises(TSC.server.exceptions.EndpointUnavailableError): + server.databases.update_tags(item) diff --git a/test/test_table.py b/test/test_table.py index b7a323121..cbd5573a1 100644 --- a/test/test_table.py +++ b/test/test_table.py @@ -90,3 +90,18 @@ def test_update_req_empty_strings_clear_fields() -> None: item.description = "" attrib = ET.fromstring(RequestFactory.Table.update_req(item)).findall("table")[0].attrib assert attrib == {"certificationNote": "", "description": ""} + + +def test_update_tags_not_supported(server) -> None: + server.version = "3.9" + item = TSC.TableItem("test") + item._id = "10224773-ecee-42ac-b822-d786b0b8e4d9" + with pytest.raises(NotImplementedError, match="add_tags and delete_tags"): + server.tables.update_tags(item) + + +def test_update_tags_checks_version(server) -> None: + server.version = "3.5" + item = TSC.TableItem("test") + with pytest.raises(TSC.server.exceptions.EndpointUnavailableError): + server.tables.update_tags(item) From 09c2890624357a39990b122fb54b52bca7391bcd Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Tue, 10 Nov 2026 12:47:08 -0800 Subject: [PATCH 5/5] Remove unused DatabaseItem._set_tables and share the update_tags message Co-Authored-By: Claude Code --- tableauserverclient/models/database_item.py | 3 --- .../server/endpoint/databases_endpoint.py | 10 ++-------- tableauserverclient/server/endpoint/resource_tagger.py | 5 +++++ tableauserverclient/server/endpoint/tables_endpoint.py | 10 ++-------- 4 files changed, 9 insertions(+), 19 deletions(-) diff --git a/tableauserverclient/models/database_item.py b/tableauserverclient/models/database_item.py index b884d6658..658736a3d 100644 --- a/tableauserverclient/models/database_item.py +++ b/tableauserverclient/models/database_item.py @@ -248,9 +248,6 @@ def _set_values(self, database_values): def _set_permissions(self, permissions): self._permissions = permissions - def _set_tables(self, tables): - self._tables = tables - def _set_default_permissions(self, permissions, content_type): attr = f"_default_{content_type}_permissions" setattr( diff --git a/tableauserverclient/server/endpoint/databases_endpoint.py b/tableauserverclient/server/endpoint/databases_endpoint.py index 8ab59c238..d063231a2 100644 --- a/tableauserverclient/server/endpoint/databases_endpoint.py +++ b/tableauserverclient/server/endpoint/databases_endpoint.py @@ -9,7 +9,7 @@ from tableauserverclient.server.endpoint.endpoint import api, Endpoint from tableauserverclient.server.endpoint.exceptions import MissingRequiredFieldError from tableauserverclient.server.endpoint.permissions_endpoint import _PermissionsEndpoint -from tableauserverclient.server.endpoint.resource_tagger import TaggingMixin +from tableauserverclient.server.endpoint.resource_tagger import TaggingMixin, UPDATE_TAGS_UNSUPPORTED_MESSAGE from tableauserverclient.server import RequestFactory from tableauserverclient.models import DatabaseItem, PaginationItem, Resource @@ -20,12 +20,6 @@ from tableauserverclient.server.request_options import RequestOptions -_UPDATE_TAGS_MESSAGE = ( - "update_tags is not supported for databases: the item does not track its tags. " - "Use add_tags and delete_tags with the item or its ID instead." -) - - class Databases(Endpoint, TaggingMixin): def __init__(self, parent_srv): super().__init__(parent_srv) @@ -254,4 +248,4 @@ def delete_tags(self, item: DatabaseItem | str, tags: Iterable[str]) -> None: @api(version="3.9") def update_tags(self, item: DatabaseItem) -> None: # type: ignore[override] - raise NotImplementedError(_UPDATE_TAGS_MESSAGE) + raise NotImplementedError(UPDATE_TAGS_UNSUPPORTED_MESSAGE.format(kind="databases")) diff --git a/tableauserverclient/server/endpoint/resource_tagger.py b/tableauserverclient/server/endpoint/resource_tagger.py index 72c7274c8..471619eef 100644 --- a/tableauserverclient/server/endpoint/resource_tagger.py +++ b/tableauserverclient/server/endpoint/resource_tagger.py @@ -12,6 +12,11 @@ from tableauserverclient.helpers.logging import logger +UPDATE_TAGS_UNSUPPORTED_MESSAGE = ( + "update_tags is not supported for {kind}: the item does not track its tags. " + "Use add_tags and delete_tags with the item or its ID instead." +) + if TYPE_CHECKING: from tableauserverclient.models.column_item import ColumnItem from tableauserverclient.models.database_item import DatabaseItem diff --git a/tableauserverclient/server/endpoint/tables_endpoint.py b/tableauserverclient/server/endpoint/tables_endpoint.py index ad6201b1f..ee7137288 100644 --- a/tableauserverclient/server/endpoint/tables_endpoint.py +++ b/tableauserverclient/server/endpoint/tables_endpoint.py @@ -7,7 +7,7 @@ from tableauserverclient.server.endpoint.endpoint import api, Endpoint from tableauserverclient.server.endpoint.exceptions import MissingRequiredFieldError from tableauserverclient.server.endpoint.permissions_endpoint import _PermissionsEndpoint -from tableauserverclient.server.endpoint.resource_tagger import TaggingMixin +from tableauserverclient.server.endpoint.resource_tagger import TaggingMixin, UPDATE_TAGS_UNSUPPORTED_MESSAGE from tableauserverclient.server import RequestFactory from tableauserverclient.models import TableItem, ColumnItem, PaginationItem from tableauserverclient.server.pager import Pager @@ -19,12 +19,6 @@ from tableauserverclient.models import DQWItem, PermissionsRule -_UPDATE_TAGS_MESSAGE = ( - "update_tags is not supported for tables: the item does not track its tags. " - "Use add_tags and delete_tags with the item or its ID instead." -) - - class Tables(Endpoint, TaggingMixin[TableItem]): def __init__(self, parent_srv): super().__init__(parent_srv) @@ -267,4 +261,4 @@ def delete_tags(self, item: TableItem | str, tags: Iterable[str] | str) -> None: @api(version="3.9") def update_tags(self, item: TableItem) -> None: # type: ignore[override] - raise NotImplementedError(_UPDATE_TAGS_MESSAGE) + raise NotImplementedError(UPDATE_TAGS_UNSUPPORTED_MESSAGE.format(kind="tables"))