From 1aa52a0fcd64e5337ad51b262aab9386f14fe497 Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Mon, 9 Nov 2026 14:41:40 -0800 Subject: [PATCH 1/8] Correct the special-character guidance in filter docstrings Tested against Tableau Cloud: only a comma breaks a filter value. Ampersands, colons and square brackets match exactly, because TSC URL-encodes the query string. The docstrings said all five characters were unusable and cited a server-documented wildcard feature that the REST API only documents for the like operator on user and group names. Co-Authored-By: Claude Code --- tableauserverclient/server/filter.py | 32 +++++++++++----------------- tableauserverclient/server/query.py | 7 +++--- 2 files changed, 16 insertions(+), 23 deletions(-) diff --git a/tableauserverclient/server/filter.py b/tableauserverclient/server/filter.py index 656957d87..fc6bb8d73 100644 --- a/tableauserverclient/server/filter.py +++ b/tableauserverclient/server/filter.py @@ -13,25 +13,19 @@ class Filter: conditions. Special characters in filter values - ---------------------------------- - The REST API's filter grammar treats several characters as delimiters. The - server does NOT support escaping them, so a value containing any of these - characters cannot be matched exactly with the `Equals` operator: - - - ``,`` -- separates values in an ``in`` list. - - ``&`` -- separates filter clauses. - - ``:`` -- separates field/operator/value. - - ``[`` and ``]`` -- bracket an ``in`` value list. - - Workaround for names containing these characters: use the ``Equals`` - operator with an asterisk substituted for the special character. Asterisk - behaves as a wildcard (starts-with, ends-with, and contains matching are - supported by the server; see the Tableau REST API docs for "Filtering - and Sorting"), so for example filtering a workbook named - ``T(L-F,SZ&V-MY) - PC`` can be found via ``name="T(L-F*SZ*V-MY) - PC"``. - Wildcard filtering requires Tableau Cloud May 2023 or Tableau Server - 2022.1.14 or later. Post-filter the result client-side to disambiguate - if multiple names could match. + ----------------------------------- + The server splits the ``filter`` parameter on commas, so a value that + contains a comma cannot be sent: the request fails with ``400065: Bad + Request``. URL-encoding the comma or putting a backslash in front of it + does not help. Other characters, including ``&``, ``:``, ``[`` and ``]``, + are matched as written; TSC URL-encodes the query string for you. + + To find a name that contains a comma, replace the comma with ``*`` and + check the results client-side. Tableau Cloud treats ``*`` as a wildcard + in an ``Equals`` value on name fields, for example + ``name="Calculations* Groups & Sets"`` finds the workbook + ``Calculations, Groups & Sets``. The REST API documentation does not + describe this behavior, so confirm it on your Tableau Server version. Parameters ---------- diff --git a/tableauserverclient/server/query.py b/tableauserverclient/server/query.py index a57e36a85..567596268 100644 --- a/tableauserverclient/server/query.py +++ b/tableauserverclient/server/query.py @@ -194,10 +194,9 @@ def filter(self: Self, *invalid, page_size: int | None = None, **kwargs) -> Self https://tableau.github.io/server-client-python/docs/filter-sort for the supported suffixes. - Special-character caveat: values containing ``,``, ``&``, ``:``, ``[`` - or ``]`` cannot be matched exactly with ``Equals``; the REST filter - grammar treats them as delimiters and the server does not support - escaping. See ``Filter`` for details and the wildcard workaround. + Values that contain a comma fail with ``400065: Bad Request``, because + the server splits filters on commas and does not support escaping. + See ``Filter`` for details and the wildcard workaround. """ if invalid: raise RuntimeError("Only accepts keyword arguments.") From d81420465c591fcd76565938307137b03ace62c1 Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Mon, 9 Nov 2026 17:15:42 -0800 Subject: [PATCH 2/8] Use a generic name in the Filter docstring example Co-Authored-By: Claude Code --- tableauserverclient/server/filter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tableauserverclient/server/filter.py b/tableauserverclient/server/filter.py index fc6bb8d73..11974eeb4 100644 --- a/tableauserverclient/server/filter.py +++ b/tableauserverclient/server/filter.py @@ -23,8 +23,8 @@ class Filter: To find a name that contains a comma, replace the comma with ``*`` and check the results client-side. Tableau Cloud treats ``*`` as a wildcard in an ``Equals`` value on name fields, for example - ``name="Calculations* Groups & Sets"`` finds the workbook - ``Calculations, Groups & Sets``. The REST API documentation does not + ``name="Sales* Q1 & Q2"`` finds the workbook + ``Sales, Q1 & Q2``. The REST API documentation does not describe this behavior, so confirm it on your Tableau Server version. Parameters From 00d5a920fd99a04ee91bd8315a89226828354ebf Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Mon, 9 Nov 2026 22:03:13 -0800 Subject: [PATCH 3/8] Qualify & and URL-encoding in filter docstring by REST API version Before REST API 3.7 TSC does not URL-encode the query string, so an ampersand ends the filter. Say so, and scope the Tableau Cloud wildcard example to Cloud. Co-Authored-By: Claude Code --- tableauserverclient/server/filter.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tableauserverclient/server/filter.py b/tableauserverclient/server/filter.py index 11974eeb4..7009df6bb 100644 --- a/tableauserverclient/server/filter.py +++ b/tableauserverclient/server/filter.py @@ -17,8 +17,9 @@ class Filter: The server splits the ``filter`` parameter on commas, so a value that contains a comma cannot be sent: the request fails with ``400065: Bad Request``. URL-encoding the comma or putting a backslash in front of it - does not help. Other characters, including ``&``, ``:``, ``[`` and ``]``, - are matched as written; TSC URL-encodes the query string for you. + does not help. From REST API 3.7, other characters, including ``&``, ``:``, + ``[`` and ``]``, are matched as written because TSC URL-encodes the query + string. Earlier versions do not encode it, so ``&`` ends the filter. To find a name that contains a comma, replace the comma with ``*`` and check the results client-side. Tableau Cloud treats ``*`` as a wildcard From 8b91f1e587ed8fee83518570b860b35f8824994f Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Mon, 9 Nov 2026 22:10:49 -0800 Subject: [PATCH 4/8] Scope the filter wildcard note to what was tested Prefix, suffix and middle * matched in Equals filters on workbook names (Tableau Cloud) and project names (REST API 3.31). Co-Authored-By: Claude Code --- tableauserverclient/server/filter.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tableauserverclient/server/filter.py b/tableauserverclient/server/filter.py index 7009df6bb..22b0177f1 100644 --- a/tableauserverclient/server/filter.py +++ b/tableauserverclient/server/filter.py @@ -22,9 +22,9 @@ class Filter: string. Earlier versions do not encode it, so ``&`` ends the filter. To find a name that contains a comma, replace the comma with ``*`` and - check the results client-side. Tableau Cloud treats ``*`` as a wildcard - in an ``Equals`` value on name fields, for example - ``name="Sales* Q1 & Q2"`` finds the workbook + check the results client-side. In testing, ``*`` acted as a wildcard in + an ``Equals`` value on workbook names (Tableau Cloud) and project names + (REST API 3.31), for example ``name="Sales* Q1 & Q2"`` finds the workbook ``Sales, Q1 & Q2``. The REST API documentation does not describe this behavior, so confirm it on your Tableau Server version. From fbda5b9098080c24d828f229ff728aab90dabc67 Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Mon, 9 Nov 2026 22:11:37 -0800 Subject: [PATCH 5/8] Say the wildcard behavior is not documented per version Co-Authored-By: Claude Code --- tableauserverclient/server/filter.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tableauserverclient/server/filter.py b/tableauserverclient/server/filter.py index 22b0177f1..2d03970d9 100644 --- a/tableauserverclient/server/filter.py +++ b/tableauserverclient/server/filter.py @@ -25,8 +25,9 @@ class Filter: check the results client-side. In testing, ``*`` acted as a wildcard in an ``Equals`` value on workbook names (Tableau Cloud) and project names (REST API 3.31), for example ``name="Sales* Q1 & Q2"`` finds the workbook - ``Sales, Q1 & Q2``. The REST API documentation does not - describe this behavior, so confirm it on your Tableau Server version. + ``Sales, Q1 & Q2``. The REST API documentation does not describe this + behavior or say which versions support it, so confirm it on your Tableau + Server version. Parameters ---------- From 11544a3e5e700b7ac0eccc58a072b7d083ae5245 Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Mon, 9 Nov 2026 22:18:18 -0800 Subject: [PATCH 6/8] Raise server errors other than 400006 when iterating a QuerySet QuerySet.__iter__ caught every ServerResponseError but only handled 400006, so any other error (for example 400065 for a filter value with a comma) ended the loop with no items instead of raising. Co-Authored-By: Claude Code --- tableauserverclient/server/query.py | 1 + test/test_pager.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/tableauserverclient/server/query.py b/tableauserverclient/server/query.py index 567596268..f96bf4aca 100644 --- a/tableauserverclient/server/query.py +++ b/tableauserverclient/server/query.py @@ -87,6 +87,7 @@ def __iter__(self: Self) -> Iterator[T]: # error and return to cleanly end the generator. # (raise StopIteration would cause RuntimeError per PEP 479) return + raise if len(self._result_cache) == 0: return yield from self._result_cache diff --git a/test/test_pager.py b/test/test_pager.py index 82b79016c..b2c712c0a 100644 --- a/test/test_pager.py +++ b/test/test_pager.py @@ -169,3 +169,19 @@ def get(self, req_options=None): # Before the fix this raised: RuntimeError: generator raised StopIteration results = [x for x in qs] assert len(results) == 1 + + +def test_queryset_iteration_raises_other_server_errors() -> None: + """Iterating must surface server errors other than 400006, not yield nothing.""" + from tableauserverclient.server.endpoint.exceptions import ServerResponseError + from tableauserverclient.server.query import QuerySet + + class MockEndpoint: + def get(self, req_options=None): + raise ServerResponseError("400065", "Bad Request", "Error parsing filter query", "http://test") + + qs: QuerySet[TSC.ProjectItem] = QuerySet(MockEndpoint()) # type: ignore[arg-type] + + with pytest.raises(ServerResponseError) as excinfo: + [x for x in qs] + assert excinfo.value.code == "400065" From 56178e751ae7632b764ced83c33957b84f8636fe Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Tue, 10 Nov 2026 12:28:27 -0800 Subject: [PATCH 7/8] Fix users.bulk_add docstring: domain_name is read-only The docstring and example told users to set UserItem.domain_name, which raises AttributeError. Put the domain in the name instead. Co-Authored-By: Claude Code --- .../server/endpoint/users_endpoint.py | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/tableauserverclient/server/endpoint/users_endpoint.py b/tableauserverclient/server/endpoint/users_endpoint.py index a0b2011b8..2fbf2970b 100644 --- a/tableauserverclient/server/endpoint/users_endpoint.py +++ b/tableauserverclient/server/endpoint/users_endpoint.py @@ -416,9 +416,10 @@ def bulk_add(self, users: Iterable[UserItem]) -> JobItem: When adding users in bulk, the server will return a job item that can be used to track the progress of the operation. This method will return the job item that was created when the users were added. - For each user, name is required, and other fields are optional. If connected to activte directory and - the user name is not unique across domains, then the domain attribute must be populated on - the UserItem. + For each user, name is required, and other fields are optional. If connected to Active Directory and + the user name is not unique across domains, include the domain in the name (for example + ``example.com\\user1``). UserItem.domain_name is read-only, so it cannot be set on a UserItem + that you create. The user's display name is read from the fullname attribute. @@ -457,17 +458,14 @@ def bulk_add(self, users: Iterable[UserItem]) -> JobItem: >>> server = TSC.Server('http://localhost') >>> # Login to the server - >>> # Create a list of UserItem objects to add to the site + >>> # Create a list of UserItem objects to add to the site. The domain + >>> # goes in the name, because UserItem.domain_name is read-only. >>> users = [ - >>> TSC.UserItem(name="user1", site_role="Unlicensed"), - >>> TSC.UserItem(name="user2", site_role="Explorer"), - >>> TSC.UserItem(name="user3", site_role="Creator"), + >>> TSC.UserItem(name="example.com\\\\user1", site_role="Unlicensed"), + >>> TSC.UserItem(name="example.com\\\\user2", site_role="Explorer"), + >>> TSC.UserItem(name="example.com\\\\user3", site_role="Creator"), >>> ] - >>> # Set the domain name for the users - >>> for user in users: - >>> user.domain_name = "example.com" - >>> # Add the users to the site >>> job = server.users.bulk_add(users) From a4ac4b2978c21220af9cd0d820c8f9e1ee1ab449 Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Tue, 10 Nov 2026 12:28:39 -0800 Subject: [PATCH 8/8] Point PDF docstrings at workbooks.populate_pdf for multi-view exports Exporting several views as one PDF with a filter applied does not need a per-view download and a client-side merge: workbooks.populate_pdf takes PDFRequestOptions.vf() from REST API 3.23 and returns a single PDF. Co-Authored-By: Claude Code --- tableauserverclient/server/endpoint/views_endpoint.py | 4 ++++ tableauserverclient/server/endpoint/workbooks_endpoint.py | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/tableauserverclient/server/endpoint/views_endpoint.py b/tableauserverclient/server/endpoint/views_endpoint.py index c2779b17d..d35ff4451 100644 --- a/tableauserverclient/server/endpoint/views_endpoint.py +++ b/tableauserverclient/server/endpoint/views_endpoint.py @@ -195,6 +195,10 @@ def populate_pdf(self, view_item: ViewItem, req_options: "PDFRequestOptions | No This method populates a PDF with image(s) of the view you specify. + To export several views of a workbook as one PDF, use + ``server.workbooks.populate_pdf`` instead of merging the PDFs of + individual views. + REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_workbooks_and_views.htm#query_view_pdf Parameters diff --git a/tableauserverclient/server/endpoint/workbooks_endpoint.py b/tableauserverclient/server/endpoint/workbooks_endpoint.py index ada961af1..87d5dad3e 100644 --- a/tableauserverclient/server/endpoint/workbooks_endpoint.py +++ b/tableauserverclient/server/endpoint/workbooks_endpoint.py @@ -607,6 +607,11 @@ def populate_pdf(self, workbook_item: WorkbookItem, req_options: "PDFRequestOpti This method populates a PDF with image(s) of the workbook view(s) you specify. + To export several views of a workbook as one PDF with a filter applied, + pass the filter with PDFRequestOptions.vf() (REST API 3.23 and later). + The server renders a single PDF, so you do not need to download a PDF + for each view and merge them yourself. + REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref.htm#download_workbook_pdf Parameters