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) 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 diff --git a/tableauserverclient/server/filter.py b/tableauserverclient/server/filter.py index 656957d87..2d03970d9 100644 --- a/tableauserverclient/server/filter.py +++ b/tableauserverclient/server/filter.py @@ -13,25 +13,21 @@ 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. 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. 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 or say which versions support it, so confirm it on your Tableau + Server version. Parameters ---------- diff --git a/tableauserverclient/server/query.py b/tableauserverclient/server/query.py index a57e36a85..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 @@ -194,10 +195,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.") 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"