Skip to content
20 changes: 9 additions & 11 deletions tableauserverclient/server/endpoint/users_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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)

Expand Down
4 changes: 4 additions & 0 deletions tableauserverclient/server/endpoint/views_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions tableauserverclient/server/endpoint/workbooks_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 15 additions & 19 deletions tableauserverclient/server/filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
----------
Expand Down
8 changes: 4 additions & 4 deletions tableauserverclient/server/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.")
Expand Down
16 changes: 16 additions & 0 deletions test/test_pager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Loading